142 lines
5.2 KiB
TypeScript
142 lines
5.2 KiB
TypeScript
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 ?? "";
|
||
}
|