Initial commit: PodcastYes — AI podcast platform
This commit is contained in:
@@ -0,0 +1,90 @@
|
||||
import { z } from "zod";
|
||||
import { openai, SCRIPT_MODEL } from "../openai";
|
||||
import { buildScriptMessages, buildSectionMessages } from "../prompts/script";
|
||||
import type {
|
||||
EpisodeConfig,
|
||||
ScriptProvider,
|
||||
ScriptSection,
|
||||
StructuredScript,
|
||||
TokenUsage,
|
||||
} from "../types";
|
||||
|
||||
const turnSchema = z.object({
|
||||
speakerKey: z.string().min(1),
|
||||
text: z.string().min(1),
|
||||
});
|
||||
|
||||
const sectionSchema = z.object({
|
||||
id: z.string().min(1),
|
||||
title: z.string().min(1),
|
||||
turns: z.array(turnSchema).min(1),
|
||||
});
|
||||
|
||||
const scriptSchema = z.object({
|
||||
title: z.string().min(1),
|
||||
sections: z.array(sectionSchema).min(1),
|
||||
});
|
||||
|
||||
/** Coerce/repair speakerKeys the model may have invented to the configured set. */
|
||||
function normalizeSpeakers(script: StructuredScript, config: EpisodeConfig): StructuredScript {
|
||||
const valid = new Set(config.speakers.map((s) => s.speakerKey));
|
||||
const fallback = config.speakers[0]?.speakerKey ?? "host";
|
||||
return {
|
||||
...script,
|
||||
sections: script.sections.map((sec) => ({
|
||||
...sec,
|
||||
turns: sec.turns.map((t) => ({
|
||||
...t,
|
||||
speakerKey: valid.has(t.speakerKey) ? t.speakerKey : fallback,
|
||||
})),
|
||||
})),
|
||||
};
|
||||
}
|
||||
|
||||
function usageFrom(u: { prompt_tokens?: number; completion_tokens?: number } | undefined): TokenUsage {
|
||||
return { inputTokens: u?.prompt_tokens ?? 0, outputTokens: u?.completion_tokens ?? 0 };
|
||||
}
|
||||
|
||||
export class OpenAIScriptProvider implements ScriptProvider {
|
||||
readonly model = SCRIPT_MODEL;
|
||||
|
||||
async generate(config: EpisodeConfig): Promise<{ script: StructuredScript; usage: TokenUsage }> {
|
||||
const res = await openai().chat.completions.create({
|
||||
model: this.model,
|
||||
messages: buildScriptMessages(config),
|
||||
response_format: { type: "json_object" },
|
||||
temperature: 0.8,
|
||||
});
|
||||
const content = res.choices[0]?.message?.content ?? "{}";
|
||||
const parsed = scriptSchema.parse(JSON.parse(content));
|
||||
return { script: normalizeSpeakers(parsed, config), usage: usageFrom(res.usage) };
|
||||
}
|
||||
|
||||
async regenerateSection(
|
||||
config: EpisodeConfig,
|
||||
script: StructuredScript,
|
||||
sectionId: string
|
||||
): Promise<{ section: ScriptSection; usage: TokenUsage }> {
|
||||
const res = await openai().chat.completions.create({
|
||||
model: this.model,
|
||||
messages: buildSectionMessages(config, script, sectionId),
|
||||
response_format: { type: "json_object" },
|
||||
temperature: 0.9,
|
||||
});
|
||||
const content = res.choices[0]?.message?.content ?? "{}";
|
||||
const section = sectionSchema.parse(JSON.parse(content));
|
||||
const valid = new Set(config.speakers.map((s) => s.speakerKey));
|
||||
const fallback = config.speakers[0]?.speakerKey ?? "host";
|
||||
return {
|
||||
section: {
|
||||
...section,
|
||||
id: sectionId,
|
||||
turns: section.turns.map((t) => ({
|
||||
...t,
|
||||
speakerKey: valid.has(t.speakerKey) ? t.speakerKey : fallback,
|
||||
})),
|
||||
},
|
||||
usage: usageFrom(res.usage),
|
||||
};
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user