import u from "@/utils"; interface Scene { name: string; description: string; } interface Character { name: string; description: string; } interface Prop { name: string; description: string; } export interface Episode { episodeIndex: number; title: string; chapterRange: number[]; scenes: Scene[]; // 按 outline 出场顺序排列 characters: Character[]; // 按 outline 出场顺序排列 props: Prop[]; // 按 outline 出场顺序排列 coreConflict: string; outline: string; // 最高优先级,剧本生成的唯一权威 openingHook: string; // outline 第一句话的视觉化,开篇第一个镜头 keyEvents: string[]; // 4个元素:[起, 承, 转, 合],严格按 outline 顺序 emotionalCurve: string; // 对应 keyEvents 各阶段 visualHighlights: string[]; // 按 outline 顺序排列的标志性镜头 endingHook: string; // outline 之后的悬念延伸 classicQuotes: string[]; } /** * 格式化Episode为结构化提示 */ function formatEpisodePrompt(episode: Episode): string { const scenesStr = episode.scenes.map((s, i) => ` 场景${i + 1}:${s.name}\n 环境描写:${s.description}`).join("\n"); const charactersStr = episode.characters.map((c, i) => ` 角色${i + 1}:${c.name}\n 人设样貌:${c.description}`).join("\n"); const propsStr = episode.props.map((p, i) => ` 道具${i + 1}:${p.name}\n 样式描写:${p.description}`).join("\n"); // keyEvents 是数组格式,按顺序对应:起、承、转、合 const keyEventsLabels = ["起", "承", "转", "合"]; const keyEventsStr = episode.keyEvents.map((e, i) => ` 【${keyEventsLabels[i] || i + 1}】${e}`).join("\n"); const quotesStr = episode.classicQuotes.map((q, i) => ` 金句${i + 1}:「${q}」`).join("\n"); const highlightsStr = episode.visualHighlights.map((h, i) => ` 镜头${i + 1}:${h}`).join("\n"); return ` ═══════════════════════════════════════ 第${episode.episodeIndex}集:${episode.title} 关联章节:第${episode.chapterRange.join("、")}章 ═══════════════════════════════════════ 【场景列表】必须全部使用(按出场顺序排列) ${scenesStr} 【出场角色】必须全部使用(按出场顺序排列),首次出场需完整描述外貌 ${charactersStr} 【关键道具】必须全部展示(按出场顺序排列) ${propsStr} 【核心矛盾】贯穿全集的主线冲突 ${episode.coreConflict} 【剧情主干】⚠️ 最高优先级,剧本必须严格按此顺序展开 ${episode.outline} 【开场镜头】⚠️ 必须作为剧本第一个镜头(outline开头的视觉化) ${episode.openingHook} 【剧情节点】必须严格按顺序呈现(起→承→转→合),顺序与剧情主干一致 ${keyEventsStr} 【情绪曲线】必须在对应剧情节点体现情绪强度 ${episode.emotionalCurve} 【视觉重点】标志性镜头,必须按剧情主干顺序呈现 ${highlightsStr} 【结尾悬念】必须作为收尾,后接【黑屏】 ${episode.endingHook} 【黄金金句】必须原文出现在剧本高潮段落 ${quotesStr} `; } /** * 生成单集剧本 * @param episode 已解析的Episode对象 * @param novelData 原文内容 */ export async function generateScript(episode: Episode, novelData: string): Promise { const episodePrompt = formatEpisodePrompt(episode); const userPrompt = `请根据以下结构化大纲生成剧本。 【⚠️ 最高优先级:剧情主干(outline)是唯一权威】 剧本必须严格按照【剧情主干】的叙事顺序展开,不得调整、跳跃或打乱顺序! 【强制要求】 1. ⚠️ 【开场镜头】必须是剧本的第一个镜头(这是outline开头的视觉化) 2. ⚠️ 严格按【剧情主干】顺序展开剧情,这是剧本的唯一权威 3. ⚠️ 【剧情节点】四步必须严格按顺序呈现:起→承→转→合,不输出标记 4. emotionalCurve必须在对应剧情节点体现 5. classicQuotes必须原文出现在高潮段落 6. endingHook必须作为收尾 7. scenes/characters/props必须全部使用,按出场顺序 8. visualHighlights中的镜头必须按剧情主干顺序全部呈现 9. 500-800字 10. 以【黑屏】结尾 ═══════════════════════════════════════ 大纲数据 ═══════════════════════════════════════ ${episodePrompt} ═══════════════════════════════════════ 原文参考(仅用于补充细节和对话优化) ═══════════════════════════════════════ ${novelData}`; const prompts = await u.db("t_prompts").where("code", "script").first(); const promptConfig = await u.getPromptAi("generateScript"); const mainPrompts = prompts?.customValue || prompts?.defaultValue || "不论用户说什么,请直接输出AI配置异常"; const result = await u.ai.text.invoke( { messages: [ { role: "system", content: mainPrompts }, { role: "user", content: userPrompt }, ], }, promptConfig, ); return result.text ?? ""; }