( candidates: CandidateTopic[], maxTopics: number, videoInfo?: Partial<VideoInfo>, minTopics: number = 0, segmentLabel?: string, language?: string )
| 278 | } |
| 279 | |
| 280 | function buildReducePrompt( |
| 281 | candidates: CandidateTopic[], |
| 282 | maxTopics: number, |
| 283 | videoInfo?: Partial<VideoInfo>, |
| 284 | minTopics: number = 0, |
| 285 | segmentLabel?: string, |
| 286 | language?: string |
| 287 | ): string { |
| 288 | const videoInfoBlock = formatVideoInfoForPrompt(videoInfo); |
| 289 | const segmentContext = segmentLabel |
| 290 | ? `Focus: ${segmentLabel} of the video.` |
| 291 | : ''; |
| 292 | const safeMin = Math.max(0, Math.min(minTopics, maxTopics)); |
| 293 | const selectionGuidance = |
| 294 | safeMin > 0 |
| 295 | ? `Return between ${safeMin} and ${maxTopics} standout highlights. If fewer than ${safeMin} candidates truly meet the quality bar, respond with only the clips that do—even if that's less. Never exceed ${maxTopics}.` |
| 296 | : `Return up to ${maxTopics} standout highlights that maximize diversity, insight, and narrative punch while reusing the provided quotes. It's acceptable to send back a single clip if only one deserves the spotlight.`; |
| 297 | const languageInstruction = language |
| 298 | ? (() => { |
| 299 | const langName = getLanguageName(language); |
| 300 | return `\n<languageRequirement>IMPORTANT: You MUST generate all titles in ${langName} to match the transcript language. Keep quote text and timestamps unchanged.</languageRequirement>\n`; |
| 301 | })() |
| 302 | : ''; |
| 303 | |
| 304 | const candidateBlock = candidates |
| 305 | .map((candidate, idx) => { |
| 306 | const timestamp = candidate.quote?.timestamp ?? '[??:??-??:??]'; |
| 307 | const quoteText = candidate.quote?.text ?? ''; |
| 308 | const chunkWindow = `[${formatTime(candidate.chunkStart)}-${formatTime( |
| 309 | candidate.chunkEnd |
| 310 | )}]`; |
| 311 | return `Candidate ${idx + 1} |
| 312 | Chunk window: ${chunkWindow} |
| 313 | Original title: ${candidate.title} |
| 314 | Quote timestamp: ${timestamp} |
| 315 | Quote text: ${quoteText}`; |
| 316 | }) |
| 317 | .join('\n\n'); |
| 318 | |
| 319 | return `<task> |
| 320 | <role>You are a senior editorial strategist assembling the final highlight reel lineup.</role>${languageInstruction} |
| 321 | <context> |
| 322 | ${videoInfoBlock} |
| 323 | You have ${candidates.length} candidate quotes extracted from the transcript. |
| 324 | ${segmentContext} |
| 325 | </context> |
| 326 | <goal>Choose the strongest highlights for this segment of the video.</goal> |
| 327 | <instructions> |
| 328 | <item>${selectionGuidance}</item> |
| 329 | <item>Review the candidates and choose the strongest, most distinct ideas within this segment.</item> |
| 330 | <item>If two candidates overlap, keep the better one.</item> |
| 331 | <item>You may rewrite titles for clarity, but you must keep the quote text and timestamp as provided.</item> |
| 332 | <item>Respond with strict JSON: [{"candidateIndex":number,"title":"string"}]. Indices are 1-based and reference the numbered candidates below.</item> |
| 333 | </instructions> |
| 334 | <candidates><![CDATA[ |
| 335 | ${candidateBlock} |
| 336 | ]]></candidates> |
| 337 | </task>`; |
no test coverage detected