video-flow-toon/src/utils/generateScript.ts

142 lines
5.2 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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<string> {
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 mainPrompts = prompts?.customValue || prompts?.defaultValue || "不论用户说什么请直接输出AI配置异常";
const result = await u.ai.text.invoke({
messages: [
{ role: "system", content: mainPrompts },
{ role: "user", content: userPrompt },
],
});
return result.text ?? "";
}