更新 API 路径以获取生成的剧本,重构 ScriptService 以支持渲染数据的缓存,新增解析脚本块的功能,优化 StoryDetails 类以简化属性设置,更新测试用例以适应新的逻辑。

This commit is contained in:
海龙 2025-08-07 11:46:49 +08:00
parent ee29ff949b
commit 600b68901f
6 changed files with 261 additions and 171 deletions

View File

@ -640,7 +640,7 @@ export const getProjectScript = async (request: {
/** 生成的剧本文本 */ /** 生成的剧本文本 */
generated_script: string; generated_script: string;
}> }>
>("/movie/get_project_script", request); >("/movie/get_generated_script_by_project_id", request);
}; };
/** /**

View File

@ -1,6 +1,8 @@
import { useState, useCallback, Dispatch, SetStateAction } from "react"; import { useState, useCallback, Dispatch, SetStateAction, useMemo } from "react";
import { ScriptEditUseCase,ScriptEditKey } from "../usecase/ScriptEditUseCase"; import { ScriptEditUseCase,ScriptEditKey } from "../usecase/ScriptEditUseCase";
import { getProjectScript, abortVideoTask, pausePlanFlow, resumePlanFlow } from "../../../api/video_flow"; import { getProjectScript, abortVideoTask, pausePlanFlow, resumePlanFlow } from "../../../api/video_flow";
import { parseScriptBlock } from "../domain/service";
import { ScriptBlock } from "@/components/script-renderer/types";
/** /**
* Hook接口 * Hook接口
@ -32,6 +34,8 @@ export interface UseScriptService {
planId: string; planId: string;
/** AI优化要求 */ /** AI优化要求 */
aiOptimizing: string; aiOptimizing: string;
/** 渲染数据 */
scriptBlocksMemo: ScriptBlock[];
// 操作方法 // 操作方法
/** 根据用户想法生成剧本并自动创建项目 */ /** 根据用户想法生成剧本并自动创建项目 */
generateScriptFromIdea: (idea: string) => Promise<void>; generateScriptFromIdea: (idea: string) => Promise<void>;
@ -123,7 +127,7 @@ export const useScriptService = (): UseScriptService => {
// 剧本生成完成后,自动创建项目 // 剧本生成完成后,自动创建项目
const projectData = await newScriptEditUseCase.createProject( const projectData = await newScriptEditUseCase.createProject(
idea, idea,
"user123", JSON.parse(localStorage.getItem('currentUser') || '{}').id,
"auto", "auto",
"720p" "720p"
); );
@ -377,6 +381,20 @@ export const useScriptService = (): UseScriptService => {
} }
}, [scriptEditUseCase, synopsis, focusedField, aiOptimizing, projectId]); }, [scriptEditUseCase, synopsis, focusedField, aiOptimizing, projectId]);
// 在ScriptService中添加一个方法来获取渲染数据
const scriptBlocksMemo = useMemo((): ScriptBlock[] => {
return [
parseScriptBlock('synopsis', 'Logline', synopsis || ''),
parseScriptBlock('categories', 'GENRE', categories.join(', ') || ''),
parseScriptBlock('protagonist', 'Core Identity', protagonist || ''),
parseScriptBlock('incitingIncident', 'The Inciting Incident', incitingIncident || ''),
parseScriptBlock('problem', 'The Problem & New Goal', problem || ''),
parseScriptBlock('conflict', 'Conflict & Obstacles', conflict || ''),
parseScriptBlock('stakes', 'The Stakes', stakes || ''),
parseScriptBlock('characterArc', 'Character Arc Accomplished', characterArc || '')
];
}, [synopsis, categories, protagonist, incitingIncident, problem, conflict, stakes, characterArc]);
return { return {
// 响应式状态 // 响应式状态
loading, loading,
@ -391,7 +409,7 @@ export const useScriptService = (): UseScriptService => {
projectId, projectId,
planId, planId,
aiOptimizing, aiOptimizing,
scriptBlocksMemo,
// 操作方法 // 操作方法
generateScriptFromIdea, generateScriptFromIdea,
initializeFromProject, initializeFromProject,

View File

@ -0,0 +1,27 @@
import { ScriptBlock, ScriptData } from "@/components/script-renderer/types";
import { ScriptEditKey } from "../usecase/ScriptEditUseCase";
/**
*
* @param key
* @param headerName
* @param scriptText
* @returns
*/
export function parseScriptBlock(
key: ScriptEditKey,
headerName: string,
scriptText: string
): ScriptBlock {
return {
id: key,
title: headerName,
type: "core",
content: [
{
type: "paragraph",
text: scriptText,
},
],
};
}

View File

@ -1,5 +1,6 @@
/**不同类型 将有不同元数据 */ /**不同类型 将有不同元数据 */
export enum ScriptSliceType { export enum ScriptSliceType {
/** 文本 */ /** 文本 */
text = "text", text = "text",
@ -143,24 +144,27 @@ export class StoryDetails {
*/ */
updateScript(text: string) { updateScript(text: string) {
const scriptObject = this.createFromText(text); const scriptObject = this.createFromText(text);
this.synopsis = scriptObject.synopsis; this.setProperties(scriptObject);
this.categories = scriptObject.categories;
this.protagonist = scriptObject.protagonist;
this.incitingIncident = scriptObject.incitingIncident;
this.problem = scriptObject.problem;
this.conflict = scriptObject.conflict;
this.stakes = scriptObject.stakes;
this.characterArc = scriptObject.characterArc;
return scriptObject; return scriptObject;
} }
mergeframework():string{ setProperties(properties: {
return ` synopsis: string;
${this.incitingIncident}\n categories: string[];
${this.problem} protagonist: string;
${this.conflict} incitingIncident: string;
${this.stakes} problem: string;
${this.characterArc} conflict: string;
` stakes: string;
characterArc: string;
}) {
this.synopsis = properties.synopsis;
this.categories = properties.categories;
this.protagonist = properties.protagonist;
this.incitingIncident = properties.incitingIncident;
this.problem = properties.problem;
this.conflict = properties.conflict;
this.stakes = properties.stakes;
this.characterArc = properties.characterArc;
} }
constructor(text: string) { constructor(text: string) {
this.updateScript(text); this.updateScript(text);
@ -175,7 +179,7 @@ export class StoryDetails {
// --- 智能解析梗概 --- // --- 智能解析梗概 ---
// 梗概通常位于“Core Elements”部分描述主角和初始事件。 // 梗概通常位于“Core Elements”部分描述主角和初始事件。
// 我们提取“Protagonist”和“The Inciting Incident”的描述并组合起来。 // 我们提取“Protagonist”和“The Inciting Incident”的描述并组合起来。
const protagonistText = this.extractContentByHeader(text, "Core Identity:"); const synopsis = this.extractContentByHeader(text, "Logline");
const incitingIncidentText = this.extractContentByHeader( const incitingIncidentText = this.extractContentByHeader(
text, text,
"The Inciting Incident" "The Inciting Incident"
@ -195,9 +199,6 @@ export class StoryDetails {
"Character Arc Accomplished" "Character Arc Accomplished"
); );
// 1. 梗概 - 从主角描述和激励事件组合生成
const synopsis = `${protagonistText} ${incitingIncidentText}`.trim();
return { return {
synopsis, synopsis,
categories, categories,
@ -206,116 +207,142 @@ export class StoryDetails {
problem, problem,
conflict, conflict,
stakes, stakes,
characterArc characterArc,
}; };
} }
/** /**
* Markdown标签 * Markdown标签
* @param fullText * @param fullText
* @param headerName * @param headerName
* @returns * @returns
*/ */
public extractContentByHeader(fullText: string, headerName: string): string { public extractContentByHeader(
try { fullText: string,
console.log(`正在查找标题: "${headerName}"`); headerName: string,
debug: boolean = false
): string {
try {
debug&& console.log(`正在查找标题: "${headerName}"`);
// 转义正则表达式中的特殊字符 // 转义正则表达式中的特殊字符
const escapeRegex = (text: string): string => { const escapeRegex = (text: string): string => {
return text.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); return text.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
}; };
// 清理Markdown格式和多余的空白字符 // 清理Markdown格式和多余的空白字符
const cleanMarkdownContent = (content: string): string => { const cleanMarkdownContent = (content: string): string => {
return content return (
// 移除markdown的粗体标记 content
.replace(/\*\*([^*]+)\*\*/g, '$1') // 移除markdown的粗体标记
// 移除markdown的斜体标记 .replace(/\*\*([^*]+)\*\*/g, "$1")
.replace(/\*([^*]+)\*/g, '$1') // 移除markdown的斜体标记
// 移除列表标记(- 或 * 开头的行) .replace(/\*([^*]+)\*/g, "$1")
.replace(/^\s*[-*]\s+/gm, '') // 移除列表标记(- 或 * 开头的行)
// 移除代码块 .replace(/^\s*[-*]\s+/gm, "")
.replace(/```[\s\S]*?```/g, '') // 移除代码块
// 移除行内代码 .replace(/```[\s\S]*?```/g, "")
.replace(/`([^`]+)`/g, '$1') // 移除行内代码
// 规范化空白字符:将多个连续空白(包括换行)替换为单个空格 .replace(/`([^`]+)`/g, "$1")
.replace(/\s+/g, ' ') // 规范化空白字符:将多个连续空白(包括换行)替换为单个空格
// 移除引号 .replace(/\s+/g, " ")
.replace(/["""'']/g, '') // 移除引号
// 最终清理首尾空白 .replace(/["""'']/g, "")
.trim(); // 最终清理首尾空白
}; .trim()
);
};
// 标准化headerName移除末尾的冒号和空格 // 标准化headerName移除末尾的冒号和空格
const normalizedHeaderName = headerName.replace(/:?\s*$/, '').trim(); const normalizedHeaderName = headerName.replace(/:?\s*$/, "").trim();
const escapedHeaderName = escapeRegex(normalizedHeaderName); const escapedHeaderName = escapeRegex(normalizedHeaderName);
let content = ""; let content = "";
// 定义多种匹配模式,按优先级排序 // 定义多种匹配模式,按优先级排序
const patterns = [ const patterns = [
// 1. 匹配带编号的主标题:数字. **标题:** 格式 // 1. 匹配带编号的主标题:数字. **标题:** 格式
new RegExp(`\\d+\\.\\s*\\*\\*${escapedHeaderName}:?\\*\\*\\s*([\\s\\S]*?)(?=\\d+\\.\\s*\\*\\*[^*]+:?\\*\\*|\\*\\*[A-Z]+:\\*\\*|---|\$)`, "i"), new RegExp(
`\\d+\\.\\s*\\*\\*${escapedHeaderName}:?\\*\\*\\s*([\\s\\S]*?)(?=\\d+\\.\\s*\\*\\*[^*]+:?\\*\\*|\\*\\*[A-Z]+:\\*\\*|---|\$)`,
"i"
),
// 2. 匹配子标题:*标题:* 格式(在主标题下的子项) // 2. 匹配子标题:*标题:* 格式(在主标题下的子项)
new RegExp(`\\*\\s*\\*\\*${escapedHeaderName}:?\\*\\*\\s*([\\s\\S]*?)(?=\\*\\s*\\*\\*[^*]+:?\\*\\*|\\d+\\.\\s*\\*\\*[^*]+:?\\*\\*|\\*\\*[A-Z]+:\\*\\*|---|\$)`, "i"), new RegExp(
`\\*\\s*\\*\\*${escapedHeaderName}:?\\*\\*\\s*([\\s\\S]*?)(?=\\*\\s*\\*\\*[^*]+:?\\*\\*|\\d+\\.\\s*\\*\\*[^*]+:?\\*\\*|\\*\\*[A-Z]+:\\*\\*|---|\$)`,
"i"
),
// 3. 匹配独立的粗体标题:**标题:** 格式 // 3. 匹配独立的粗体标题:**标题:** 格式
new RegExp(`^\\s*\\*\\*${escapedHeaderName}:?\\*\\*\\s*([\\s\\S]*?)(?=^\\s*\\*\\*[^*]+:?\\*\\*|\\d+\\.\\s*\\*\\*[^*]+:?\\*\\*|^\\s*\\*\\*[A-Z]+:\\*\\*|^---|\$)`, "im"), new RegExp(
`^\\s*\\*\\*${escapedHeaderName}:?\\*\\*\\s*([\\s\\S]*?)(?=^\\s*\\*\\*[^*]+:?\\*\\*|\\d+\\.\\s*\\*\\*[^*]+:?\\*\\*|^\\s*\\*\\*[A-Z]+:\\*\\*|^---|\$)`,
"im"
),
// 4. 匹配简单的**标题:**格式(可能在段落中) // 4. 匹配简单的**标题:**格式(可能在段落中)
new RegExp(`\\*\\*${escapedHeaderName}:?\\*\\*\\s*([\\s\\S]*?)(?=\\*\\*[^*]+:?\\*\\*|\\d+\\.\\s*\\*\\*[^*]+:?\\*\\*|\\n\\n|---|\$)`, "i"), new RegExp(
`\\*\\*${escapedHeaderName}:?\\*\\*\\s*([\\s\\S]*?)(?=\\*\\*[^*]+:?\\*\\*|\\d+\\.\\s*\\*\\*[^*]+:?\\*\\*|\\n\\n|---|\$)`,
"i"
),
// 5. 匹配markdown标题## 标题 格式 // 5. 匹配markdown标题## 标题 格式
new RegExp(`^#{1,6}\\s*${escapedHeaderName}:?\\s*\\n([\\s\\S]*?)(?=^#{1,6}\\s|^\\*\\*[^*]+:?\\*\\*|^---|\$)`, "im"), new RegExp(
`^#{1,6}\\s*${escapedHeaderName}:?\\s*\\n([\\s\\S]*?)(?=^#{1,6}\\s|^\\*\\*[^*]+:?\\*\\*|^---|\$)`,
"im"
),
// 6. 匹配冒号后的内容:标题: 内容(适用于简单的键值对格式) // 6. 匹配冒号后的内容:标题: 内容(适用于简单的键值对格式)
new RegExp(`^\\s*${escapedHeaderName}:?\\s*([\\s\\S]*?)(?=^\\s*[A-Za-z][^:]*:|^\\*\\*[^*]+:?\\*\\*|^---|\$)`, "im"), new RegExp(
]; `^\\s*${escapedHeaderName}:?\\s*([\\s\\S]*?)(?=^\\s*[A-Za-z][^:]*:|^\\*\\*[^*]+:?\\*\\*|^---|\$)`,
"im"
),
];
// 尝试每种模式 // 尝试每种模式
for (let i = 0; i < patterns.length; i++) { for (let i = 0; i < patterns.length; i++) {
const regex = patterns[i]; const regex = patterns[i];
const match = fullText.match(regex); const match = fullText.match(regex);
if (match && match[1] && match[1].trim()) { if (match && match[1] && match[1].trim()) {
content = match[1].trim(); content = match[1].trim();
console.log(`使用模式 ${i + 1} 匹配成功`); debug && console.log(`使用模式 ${i + 1} 匹配成功`);
console.log(`匹配的完整内容: "${match[0].substring(0, 150)}..."`); debug && console.log(`匹配的完整内容: "${match[0].substring(0, 150)}..."`);
break; break;
}
} }
}
if (!content) { if (!content) {
console.log(`没有找到标题 "${headerName}" 对应的内容`); debug && console.log(`没有找到标题 "${headerName}" 对应的内容`);
// 尝试更宽松的匹配作为最后手段 // 尝试更宽松的匹配作为最后手段
const loosePattern = new RegExp(`${escapedHeaderName}[:\\s]*([\\s\\S]{1,500}?)(?=\\n\\n|\\*\\*[^*]+\\*\\*|#{1,6}\\s|---|\$)`, "i"); const loosePattern = new RegExp(
const looseMatch = fullText.match(loosePattern); `${escapedHeaderName}[:\\s]*([\\s\\S]{1,500}?)(?=\\n\\n|\\*\\*[^*]+\\*\\*|#{1,6}\\s|---|\$)`,
if (looseMatch && looseMatch[1]) { "i"
content = looseMatch[1].trim(); );
console.log("使用宽松匹配模式找到内容"); const looseMatch = fullText.match(loosePattern);
} else { if (looseMatch && looseMatch[1]) {
content = looseMatch[1].trim();
debug && console.log("使用宽松匹配模式找到内容");
} else {
return "";
}
}
debug && console.log(`原始匹配内容: "${content.substring(0, 100)}..."`);
// 清理内容格式
content = cleanMarkdownContent(content);
// 如果内容太短,可能是匹配错误,返回空
if (content.length < 10) {
debug && console.log("匹配到的内容太短,可能匹配错误");
return ""; return "";
} }
}
console.log(`原始匹配内容: "${content.substring(0, 100)}..."`); debug && console.log(`清理后的内容: "${content.substring(0, 100)}..."`);
return content;
// 清理内容格式 } catch (error) {
content = cleanMarkdownContent(content); console.error("内容提取出错:", error);
// 如果内容太短,可能是匹配错误,返回空
if (content.length < 10) {
console.log("匹配到的内容太短,可能匹配错误");
return ""; return "";
} }
console.log(`清理后的内容: "${content.substring(0, 100)}..."`);
return content;
} catch (error) {
console.error("内容提取出错:", error);
return "";
} }
}
toObject() { toObject() {
return { return {
synopsis: this.synopsis, synopsis: this.synopsis,
@ -373,3 +400,4 @@ export class ScriptValueObject {
return this.scriptText; return this.scriptText;
} }
} }

File diff suppressed because one or more lines are too long

View File

@ -124,7 +124,7 @@ export class ScriptEditUseCase {
*/ */
async createProject( async createProject(
prompt: string, prompt: string,
userId: string = "user123", userId: string ,
mode: "auto" | "manual" = "auto", mode: "auto" | "manual" = "auto",
resolution: "720p" | "1080p" | "4k" = "720p" resolution: "720p" | "1080p" | "4k" = "720p"
) { ) {