94 lines
3.3 KiB
TypeScript
94 lines
3.3 KiB
TypeScript
import type { EpisodeConfig, StructuredScript } from "../types";
|
||
|
||
const FORMAT_GUIDANCE: Record<EpisodeConfig["format"], string> = {
|
||
SOLO: "A single host speaking directly to the listener. Use only the host speaker.",
|
||
INTERVIEW:
|
||
"A host interviewing a guest. Alternate naturally between host questions and guest answers.",
|
||
MULTI_HOST:
|
||
"A panel of co-hosts in lively conversation. Distribute lines across all speakers and let them react to each other.",
|
||
};
|
||
|
||
/** Roughly 150 spoken words per minute → target word budget for the whole episode. */
|
||
function wordBudget(minutes: number): number {
|
||
return Math.round(minutes * 150);
|
||
}
|
||
|
||
export function buildScriptMessages(config: EpisodeConfig) {
|
||
const speakerList = config.speakers
|
||
.map((s) => `- key "${s.speakerKey}" = ${s.displayName}`)
|
||
.join("\n");
|
||
|
||
const system = [
|
||
"You are an expert podcast scriptwriter and showrunner.",
|
||
"You write natural, engaging, spoken-word scripts that sound great when read aloud by AI voices.",
|
||
"Avoid stage directions, sound-effect notes, and parentheticals — output only spoken dialogue.",
|
||
"Return STRICT JSON only, matching the requested schema. Do not include markdown fences.",
|
||
].join(" ");
|
||
|
||
const user = [
|
||
`Write a complete podcast episode script in ${config.language}.`,
|
||
"",
|
||
`Topic: ${config.topic}`,
|
||
`Tone: ${config.tone}`,
|
||
`Format: ${config.format} — ${FORMAT_GUIDANCE[config.format]}`,
|
||
config.audience ? `Target audience: ${config.audience}` : "",
|
||
`Approximate length: ${config.targetLengthMin} minutes (~${wordBudget(
|
||
config.targetLengthMin
|
||
)} words total).`,
|
||
"",
|
||
"Speakers (use ONLY these keys in `speakerKey`):",
|
||
speakerList,
|
||
"",
|
||
"Structure the episode into 3–6 sections (e.g. intro, main segments, outro).",
|
||
"Each section has a short title and a list of turns. Each turn is one speaker's spoken line.",
|
||
"",
|
||
"Return JSON with this exact shape:",
|
||
`{
|
||
"title": "string — a catchy episode title",
|
||
"sections": [
|
||
{
|
||
"id": "kebab-case-id",
|
||
"title": "string",
|
||
"turns": [
|
||
{ "speakerKey": "host", "text": "spoken line..." }
|
||
]
|
||
}
|
||
]
|
||
}`,
|
||
]
|
||
.filter(Boolean)
|
||
.join("\n");
|
||
|
||
return [
|
||
{ role: "system" as const, content: system },
|
||
{ role: "user" as const, content: user },
|
||
];
|
||
}
|
||
|
||
export function buildSectionMessages(
|
||
config: EpisodeConfig,
|
||
script: StructuredScript,
|
||
sectionId: string
|
||
) {
|
||
const section = script.sections.find((s) => s.id === sectionId);
|
||
const speakerList = config.speakers.map((s) => `"${s.speakerKey}"=${s.displayName}`).join(", ");
|
||
|
||
const system =
|
||
"You are an expert podcast scriptwriter. Rewrite a single section of an existing episode, keeping the same speakers, tone, and language. Return STRICT JSON for just that one section.";
|
||
|
||
const user = [
|
||
`Episode title: ${script.title}`,
|
||
`Tone: ${config.tone}. Language: ${config.language}. Speakers: ${speakerList}.`,
|
||
"",
|
||
`Rewrite the section titled "${section?.title ?? sectionId}" (id "${sectionId}") to be fresh and engaging while serving the same purpose in the episode.`,
|
||
"",
|
||
"Return JSON with this exact shape:",
|
||
`{ "id": "${sectionId}", "title": "string", "turns": [ { "speakerKey": "host", "text": "..." } ] }`,
|
||
].join("\n");
|
||
|
||
return [
|
||
{ role: "system" as const, content: system },
|
||
{ role: "user" as const, content: user },
|
||
];
|
||
}
|