File size: 3,177 Bytes
ef4c36f | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 | import {
enforceScriptLimit,
MAX_SCRIPT_CHARS,
MIN_SCRIPT_CHARS,
TARGET_SECONDS,
} from "@/lib/script-limits";
import type { GitHubRepoData } from "@/types";
const DEFAULT_LLM_URL = "https://cocoxkhushal.online/v1/chat/completions";
interface ChatCompletionResponse {
choices?: Array<{
message?: {
content?: string;
};
}>;
}
function getLlmUrl(): string {
return process.env.LLM_API_URL || DEFAULT_LLM_URL;
}
function buildHeaders(): Record<string, string> {
const headers: Record<string, string> = {
accept: "application/json",
"Content-Type": "application/json",
};
const apiKey = process.env.LLM_API_KEY || process.env.OPENAI_API_KEY;
if (apiKey) {
headers.Authorization = `Bearer ${apiKey}`;
}
return headers;
}
// ponytail: one shared call so social copy doesn't duplicate the fetch/parse dance
export async function callLlm(
prompt: string,
{ maxTokens = 300, temperature = 0.8, timeoutMs = 150_000 } = {},
): Promise<string> {
const body: Record<string, unknown> = {
messages: [{ role: "user", content: prompt }],
temperature,
top_p: 1,
max_tokens: maxTokens,
stream: false,
top_k: 0,
presence_penalty: 0,
frequency_penalty: 0,
};
const model = process.env.LLM_MODEL || process.env.OPENAI_MODEL;
if (model) body.model = model;
// ponytail: this model occasionally stalls; bail at timeoutMs so callers can retry
// instead of hanging on undici's 300s headers timeout
const response = await fetch(getLlmUrl(), {
method: "POST",
headers: buildHeaders(),
body: JSON.stringify(body),
signal: AbortSignal.timeout(timeoutMs),
});
if (!response.ok) {
const errorText = await response.text();
throw new Error(`LLM request failed (${response.status}): ${errorText.slice(0, 200)}`);
}
const data = (await response.json()) as ChatCompletionResponse;
const raw = data.choices?.[0]?.message?.content?.trim();
if (!raw) throw new Error("LLM returned an empty response");
return raw;
}
export async function generateIntroScript(repo: GitHubRepoData): Promise<string> {
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 with a call to check the repo
- 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;
}
|