import { enforceScriptLimit, MAX_SCRIPT_CHARS, MIN_SCRIPT_CHARS, TARGET_SECONDS, } from "@/lib/script-limits"; import type { GitHubRepoData } from "@/types"; // ponytail: 2.5 Flash is closed to new API keys; 3.8 Flash is Google's suggested replacement const GEMINI_MODEL = "gemini-3.8-flash"; interface GeminiResponse { candidates?: Array<{ content?: { parts?: Array<{ text?: string }> }; }>; } export async function callLlm( prompt: string, { maxTokens = 300, temperature = 0.8, timeoutMs = 150_000 } = {}, ): Promise { const apiKey = process.env.GEMINI_API_KEY; if (!apiKey) throw new Error("GEMINI_API_KEY is not set"); // ponytail: 3 tries with 2s/4s backoff on 429/503 ("high demand") — these are transient let response!: Response; for (let attempt = 1; attempt <= 3; attempt++) { response = await fetch( `https://generativelanguage.googleapis.com/v1beta/models/${GEMINI_MODEL}:generateContent`, { method: "POST", headers: { "Content-Type": "application/json", "x-goog-api-key": apiKey }, body: JSON.stringify({ contents: [{ role: "user", parts: [{ text: prompt }] }], generationConfig: { temperature, maxOutputTokens: maxTokens, // ponytail: Flash thinks by default and burns maxOutputTokens on it; off for short copy thinkingConfig: { thinkingBudget: 0 }, }, }), signal: AbortSignal.timeout(timeoutMs), }, ); if (response.status !== 429 && response.status !== 503) break; if (attempt < 3) await new Promise((r) => setTimeout(r, 2000 * attempt)); } if (!response.ok) { const errorText = await response.text(); throw new Error(`Gemini request failed (${response.status}): ${errorText.slice(0, 200)}`); } const data = (await response.json()) as GeminiResponse; const raw = data.candidates?.[0]?.content?.parts ?.map((p) => p.text ?? "") .join("") .trim(); if (!raw) throw new Error("Gemini returned an empty response"); return raw; } export async function generateIntroScript(repo: GitHubRepoData): Promise { const prompt = `You write viral ${TARGET_SECONDS} second TikTok/Reels intro scripts for GitHub repos. Repo: ${repo.fullName} Description: ${repo.description || "No description"} Language: ${repo.language} Stars: ${repo.stars} Topics: ${repo.topics.join(", ") || "none"} README excerpt: ${repo.readme.slice(0, 2500)} Write a punchy spoken intro script that: - Takes ${TARGET_SECONDS} seconds to read aloud: 65-80 words, between ${MIN_SCRIPT_CHARS} and ${MAX_SCRIPT_CHARS} characters - Never shorter than ${MIN_SCRIPT_CHARS} characters — add a concrete detail from the README rather than padding - Opens with a hook about what the project does - Highlights 2-3 key features or use cases - Ends by asking viewers to comment below to get the repo link directly in their DM - Uses short sentences, no markdown, no emojis - Sounds natural when read aloud Return ONLY the script text, nothing else.`; const raw = await callLlm(prompt, { maxTokens: 300 }); const script = enforceScriptLimit(raw.replace(/^["']|["']$/g, "")); if (!script) throw new Error("LLM returned empty script"); return script; }