/**不同类型 将有不同元数据 */ export enum ScriptSliceType { /** 文本 */ text = "text", /**角色 */ role = "role", /** 场景 */ scene = "scene", } /** * 剧本片段值对象 * @description 代表剧本中的一个片段,按值相等判断,不可变 */ export class ScriptSlice { /**唯一标识符 - 仅作为局部唯一性标识,不作为全局Entity id */ readonly id: string; /** 类型 */ readonly type: ScriptSliceType; /** 剧本内容 */ readonly text: string; /** 元数据 */ metaData: any; constructor( id: string, type: ScriptSliceType, text: string, metaData: any = {} ) { this.id = id; this.type = type; this.text = text; this.metaData = metaData; } /** * 值对象相等性比较 * @param other 另一个ScriptSlice实例 * @returns 是否相等 */ equals(other: ScriptSlice): boolean { return ( this.type === other.type && this.text === other.text && JSON.stringify(this.metaData) === JSON.stringify(other.metaData) ); } } /** * 对话内容项值对象 * @description 代表对话中的一个内容项,按值相等判断,不可变 */ export class ContentItem { /** 角色ID */ readonly roleId: string; /** 对话内容 */ readonly content: string; constructor(roleId: string, content: string) { this.roleId = roleId; this.content = content; } /** * 值对象相等性比较 * @param other 另一个ContentItem实例 * @returns 是否相等 */ equals(other: ContentItem): boolean { return this.roleId === other.roleId && this.content === other.content; } } /** * 镜头值对象 * @description 代表镜头信息,按值相等判断,不可变 */ export class LensType { /** 镜头名称 */ readonly name: string; /** 镜头描述 */ readonly content: string; /**运镜描述 */ readonly movement: string; constructor(name: string, content: string, movement: string) { this.name = name; this.content = content; this.movement = movement; } /** * 值对象相等性比较 * @param other 另一个LensType实例 * @returns 是否相等 */ equals(other: LensType): boolean { return ( this.name === other.name && this.content === other.content && this.movement === other.movement ); } } /** * 这是一个符合DDD概念的值对象,代表故事的核心细节。 * 它封装了故事的四个关键元素:梗概、分类、主角和框架内容。 * - 值对象是不可变的,一旦创建,其内部属性不会改变。 * - 构造函数是私有的,强制使用工厂方法创建实例,以确保数据处理的封装性。 */ export class StoryDetails { /** 故事梗概 */ public synopsis: string = ""; /** 故事分类标签 */ public categories: string[] = []; /** 主角名称 */ public protagonist: string = ""; /** 激励事件 */ public incitingIncident: string = ""; /** 问题与新目标 */ public problem: string = ""; /** 冲突与障碍 */ public conflict: string = ""; /** 赌注 */ public stakes: string = ""; /** 人物弧线完成 */ public characterArc: string = ""; /** * 更新剧本 * @param text 剧本文本 */ updateScript(text: string) { const scriptObject = this.createFromText(text); this.synopsis = scriptObject.synopsis; 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; } mergeframework():string{ return ` ${this.incitingIncident}\n ${this.problem} ${this.conflict} ${this.stakes} ${this.characterArc} ` } constructor(text: string) { this.updateScript(text); } /** * 工厂方法:从原始英文文本中提取并创建 StoryDetails 值对象。 * 该方法负责所有的解析逻辑,包括智能提取梗概和去除格式标签。 * @param text 包含故事所有细节的原始Markdown或富文本。 */ public createFromText(text: string) { // --- 智能解析梗概 --- // 梗概通常位于“Core Elements”部分,描述主角和初始事件。 // 我们提取“Protagonist”和“The Inciting Incident”的描述,并组合起来。 const protagonistText = this.extractContentByHeader(text, "Core Identity:"); const incitingIncidentText = this.extractContentByHeader( text, "The Inciting Incident" ); // --- 其他字段的提取保持不变,使用标题进行查找 --- const categories = this.extractContentByHeader(text, "GENRE") .split(",") .map((s) => s.trim()) .filter((s) => s.length > 0); const protagonist = this.extractContentByHeader(text, "Core Identity:"); const problem = this.extractContentByHeader(text, "The Problem & New Goal"); const conflict = this.extractContentByHeader(text, "Conflict & Obstacles"); const stakes = this.extractContentByHeader(text, "The Stakes"); const characterArc = this.extractContentByHeader( text, "Character Arc Accomplished" ); // 1. 梗概 - 从主角描述和激励事件组合生成 const synopsis = `${protagonistText} ${incitingIncidentText}`.trim(); return { synopsis, categories, protagonist, incitingIncident: incitingIncidentText, problem, conflict, stakes, characterArc }; } /** * 辅助方法:根据标题提取其下的内容,并去除所有Markdown标签。 * @param fullText 完整的源文本。 * @param headerName 要提取内容所属的标题名(不区分大小写)。 * @returns 提取出的纯文本内容。 */ public extractContentByHeader(fullText: string, headerName: string): string { try { console.log(`正在查找标题: "${headerName}"`); // 转义正则表达式中的特殊字符 const escapeRegex = (text: string): string => { return text.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); }; // 清理Markdown格式和多余的空白字符 const cleanMarkdownContent = (content: string): string => { return content // 移除markdown的粗体标记 .replace(/\*\*([^*]+)\*\*/g, '$1') // 移除markdown的斜体标记 .replace(/\*([^*]+)\*/g, '$1') // 移除列表标记(- 或 * 开头的行) .replace(/^\s*[-*]\s+/gm, '') // 移除代码块 .replace(/```[\s\S]*?```/g, '') // 移除行内代码 .replace(/`([^`]+)`/g, '$1') // 规范化空白字符:将多个连续空白(包括换行)替换为单个空格 .replace(/\s+/g, ' ') // 移除引号 .replace(/["""'']/g, '') // 最终清理首尾空白 .trim(); }; // 标准化headerName,移除末尾的冒号和空格 const normalizedHeaderName = headerName.replace(/:?\s*$/, '').trim(); const escapedHeaderName = escapeRegex(normalizedHeaderName); let content = ""; // 定义多种匹配模式,按优先级排序 const patterns = [ // 1. 匹配带编号的主标题:数字. **标题:** 格式 new RegExp(`\\d+\\.\\s*\\*\\*${escapedHeaderName}:?\\*\\*\\s*([\\s\\S]*?)(?=\\d+\\.\\s*\\*\\*[^*]+:?\\*\\*|\\*\\*[A-Z]+:\\*\\*|---|\$)`, "i"), // 2. 匹配子标题:*标题:* 格式(在主标题下的子项) new RegExp(`\\*\\s*\\*\\*${escapedHeaderName}:?\\*\\*\\s*([\\s\\S]*?)(?=\\*\\s*\\*\\*[^*]+:?\\*\\*|\\d+\\.\\s*\\*\\*[^*]+:?\\*\\*|\\*\\*[A-Z]+:\\*\\*|---|\$)`, "i"), // 3. 匹配独立的粗体标题:**标题:** 格式 new RegExp(`^\\s*\\*\\*${escapedHeaderName}:?\\*\\*\\s*([\\s\\S]*?)(?=^\\s*\\*\\*[^*]+:?\\*\\*|\\d+\\.\\s*\\*\\*[^*]+:?\\*\\*|^\\s*\\*\\*[A-Z]+:\\*\\*|^---|\$)`, "im"), // 4. 匹配简单的**标题:**格式(可能在段落中) new RegExp(`\\*\\*${escapedHeaderName}:?\\*\\*\\s*([\\s\\S]*?)(?=\\*\\*[^*]+:?\\*\\*|\\d+\\.\\s*\\*\\*[^*]+:?\\*\\*|\\n\\n|---|\$)`, "i"), // 5. 匹配markdown标题:## 标题 格式 new RegExp(`^#{1,6}\\s*${escapedHeaderName}:?\\s*\\n([\\s\\S]*?)(?=^#{1,6}\\s|^\\*\\*[^*]+:?\\*\\*|^---|\$)`, "im"), // 6. 匹配冒号后的内容:标题: 内容(适用于简单的键值对格式) new RegExp(`^\\s*${escapedHeaderName}:?\\s*([\\s\\S]*?)(?=^\\s*[A-Za-z][^:]*:|^\\*\\*[^*]+:?\\*\\*|^---|\$)`, "im"), ]; // 尝试每种模式 for (let i = 0; i < patterns.length; i++) { const regex = patterns[i]; const match = fullText.match(regex); if (match && match[1] && match[1].trim()) { content = match[1].trim(); console.log(`使用模式 ${i + 1} 匹配成功`); console.log(`匹配的完整内容: "${match[0].substring(0, 150)}..."`); break; } } if (!content) { console.log(`没有找到标题 "${headerName}" 对应的内容`); // 尝试更宽松的匹配作为最后手段 const loosePattern = new RegExp(`${escapedHeaderName}[:\\s]*([\\s\\S]{1,500}?)(?=\\n\\n|\\*\\*[^*]+\\*\\*|#{1,6}\\s|---|\$)`, "i"); const looseMatch = fullText.match(loosePattern); if (looseMatch && looseMatch[1]) { content = looseMatch[1].trim(); console.log("使用宽松匹配模式找到内容"); } else { return ""; } } console.log(`原始匹配内容: "${content.substring(0, 100)}..."`); // 清理内容格式 content = cleanMarkdownContent(content); // 如果内容太短,可能是匹配错误,返回空 if (content.length < 10) { console.log("匹配到的内容太短,可能匹配错误"); return ""; } console.log(`清理后的内容: "${content.substring(0, 100)}..."`); return content; } catch (error) { console.error("内容提取出错:", error); return ""; } } toObject() { return { synopsis: this.synopsis, categories: this.categories, protagonist: this.protagonist, incitingIncident: this.incitingIncident, problem: this.problem, conflict: this.conflict, stakes: this.stakes, characterArc: this.characterArc, }; } } /** * 剧本聚合根 * @description 作为聚合根,封装ScriptSlice的管理与行为 */ export class ScriptValueObject { /** 剧本片段数组 - 值对象数组 */ private readonly _scriptSlices: ScriptSlice[] = []; /** * 获取剧本片段数组的只读副本 */ get scriptSlices(): readonly ScriptSlice[] { return [...this._scriptSlices]; } scriptText: string = ""; storyDetails: StoryDetails; /** * @description: 构造函数,初始化剧本 * @param scriptText 剧本文本字符串 */ constructor(scriptText?: string) { this.storyDetails = new StoryDetails(""); if (scriptText) { this.parseFromString(scriptText); } } /** * @description: 从字符串解析剧本片段 * @param scriptText 剧本文本字符串 */ parseFromString(scriptText: string) { this.scriptText += scriptText; return this.storyDetails.updateScript(this.scriptText); } /** * @description: 将剧本片段转换为字符串 * @returns string */ toString(): string { return this.scriptText; } }