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;
}