Module split: extract Gemini-specific helpers to server/gemini-helpers.js

• PRICING table             — per-1M-token rates by model
  • calcCost(model, usage)    — Gemini usage object → cost record
  • buildAnalysisPrompt(...)  — JSON-output topic-analysis prompt

These all share the Gemini contract — pricing schema, usage shape, and
prompt format. When we add other providers, each gets its own
provider-specific helpers file; this becomes the basis of the Gemini
provider implementation.

server/index.js: 2828 → 2758 lines.

Smoke tested: server boots; /api/license-status, /api/health, and /
(frontend) all respond. No behavior change.
This commit is contained in:
Keysat
2026-05-08 16:50:34 -05:00
parent ffc8c31130
commit 1c78e46ebd
2 changed files with 93 additions and 73 deletions
+3 -73
View File
@@ -18,6 +18,7 @@ import {
safeText,
retryGemini,
} from "./util.js";
import { calcCost, buildAnalysisPrompt } from "./gemini-helpers.js";
const execFileAsync = promisify(execFile);
const app = express();
@@ -427,41 +428,7 @@ async function autoUpdateYtdlp() {
}
}
// ── Pricing (per 1M tokens) ───────────────────────────────────────────────
const PRICING = {
"gemini-3-flash-preview": { input: 0.50, output: 3.00, thinking: 3.00 },
"gemini-3-pro-preview": { input: 2.00, output: 12.00, thinking: 12.00 },
"gemini-3.1-pro-preview": { input: 2.00, output: 12.00, thinking: 12.00 },
"gemini-2.5-flash": { input: 0.15, output: 0.60, thinking: 0.60 },
// Fallback for unknown models
"default": { input: 1.00, output: 5.00, thinking: 5.00 },
};
function calcCost(modelName, usage) {
const rates = PRICING[modelName] || PRICING["default"];
const inputTokens = usage.promptTokenCount || 0;
const outputTokens = usage.candidatesTokenCount || 0;
const thinkingTokens = usage.thoughtsTokenCount || 0;
const inputCost = (inputTokens / 1_000_000) * rates.input;
const outputCost = (outputTokens / 1_000_000) * rates.output;
const thinkingCost = (thinkingTokens / 1_000_000) * rates.thinking;
const totalCost = inputCost + outputCost + thinkingCost;
return {
inputTokens,
outputTokens,
thinkingTokens,
totalTokens: usage.totalTokenCount || (inputTokens + outputTokens + thinkingTokens),
inputCost: inputCost.toFixed(6),
outputCost: outputCost.toFixed(6),
thinkingCost: thinkingCost.toFixed(6),
totalCost: totalCost.toFixed(6),
totalCostDisplay: totalCost < 0.01 ? `$${(totalCost * 100).toFixed(3)}¢` : `$${totalCost.toFixed(4)}`,
};
}
// PRICING + calcCost + buildAnalysisPrompt moved to ./gemini-helpers.js
// safeText + retryGemini moved to ./util.js
// ── Health check ───────────────────────────────────────────────────────────
@@ -2714,44 +2681,7 @@ async function splitAudioFile(inputPath, outputDir, chunkSeconds = 2700) {
// sendEvent / extractVideoId / formatTime / parseTimestampedTranscript moved to ./util.js
function buildAnalysisPrompt(entries) {
const numbered = entries
.map((e, i) => `[${i}] (${formatTime(e.offset)}) ${e.text}`)
.join("\n");
return `You are analyzing a video transcript. Your job is to identify natural topic boundaries and group the transcript into discussion-based sections.
TRANSCRIPT (each line is numbered with a timestamp):
${numbered}
INSTRUCTIONS:
1. Read the entire transcript carefully.
2. Identify where the discussion naturally shifts from one topic to another.
3. Group consecutive transcript segments by topic. Some sections may be short (a quick aside) and some may be long (an extended deep-dive). Let the content dictate the length.
4. For each section, write:
- A short, specific topic title (3-8 words)
- A 1-3 sentence summary of what's discussed
- The start and end segment indices (inclusive)
IMPORTANT:
- Sections must be chronological and non-overlapping.
- Every segment index from 0 to ${entries.length - 1} must belong to exactly one section.
- startIndex of section N+1 must equal endIndex of section N plus 1.
- Create as many or as few sections as the content naturally requires.
- Titles should be descriptive and specific, not generic like "Introduction" unless it truly is one.
Respond with ONLY valid JSON in this exact format, no other text:
{
"sections": [
{
"title": "Brief Topic Title",
"summary": "1-3 sentence summary of this discussion section.",
"startIndex": 0,
"endIndex": 15
}
]
}`;
}
// buildAnalysisPrompt moved to ./gemini-helpers.js
// ── Network mode ──────────────────────────────────────────────────────────
// On StartOS (DATA_DIR=/data): always bind to 0.0.0.0 (container networking)