1c78e46ebd
• 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.
91 lines
4.3 KiB
JavaScript
91 lines
4.3 KiB
JavaScript
// Gemini-specific helpers: pricing table, cost calculation, prompt
|
|
// builder. Pure module — no state, no I/O. When we add other providers,
|
|
// each provider gets its own equivalent of this file.
|
|
|
|
import { formatTime } from "./util.js";
|
|
|
|
// ── Pricing (per 1M tokens) ─────────────────────────────────────────────────
|
|
// Only the models we actually use as analysis fallbacks. Keep flat — the
|
|
// numbers are operational data, not configuration. Update when Google
|
|
// changes published rates.
|
|
export 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 / future models — better an estimate than nothing.
|
|
"default": { input: 1.00, output: 5.00, thinking: 5.00 },
|
|
};
|
|
|
|
// ── Cost calculation ────────────────────────────────────────────────────────
|
|
// Takes a Gemini SDK `usage` object (response.usageMetadata) and produces
|
|
// a structured cost record. Display strings are formatted at extraction
|
|
// time so callers don't reformat. Returns zeros for unknown models (uses
|
|
// the "default" rates).
|
|
export 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)}`,
|
|
};
|
|
}
|
|
|
|
// ── Topic-analysis prompt builder ───────────────────────────────────────────
|
|
// Takes the parsed transcript entries and builds the JSON-output prompt
|
|
// fed to the analysis model. Indices in the response are positional into
|
|
// the same `entries` array — the caller relies on that contract.
|
|
export 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
|
|
}
|
|
]
|
|
}`;
|
|
}
|