import { ContentItem, LensType, SimpleCharacter, TagValueObject } from '../domain/valueObject'; // 定义角色属性接口 interface CharacterAttributes { name: string; // gender: string; // age: string; avatar: string; } // 定义高亮属性接口 interface HighlightAttributes { text: string; color: string; } // 定义文本节点接口 interface TextNode { type: 'text'; text: string; } // 定义角色标记节点接口 interface CharacterTokenNode { type: 'characterToken'; attrs: CharacterAttributes; } // 定义高亮节点接口 interface HighlightNode { type: 'highlightText'; attrs: HighlightAttributes; } // 定义内容节点类型(文本或角色标记) type ContentNode = TextNode | CharacterTokenNode | HighlightNode; // 定义段落接口 interface Paragraph { type: 'paragraph'; content: ContentNode[]; } // 定义shot 接口 interface Shot { name: string; shotDescContent: Paragraph[]; shotDialogsContent: Paragraph[]; } export class TextToShotAdapter { /** * 解析文本,识别角色并转换为节点数组 * @param text 要解析的文本 * @param roles 角色列表 * @returns ContentNode[] 节点数组 */ public static parseText(text: string, roles: SimpleCharacter[]): ContentNode[] { const nodes: ContentNode[] = []; let currentText = text; // 按角色名称长度降序排序,避免短名称匹配到长名称的一部分 // 既要兼容 每个单词 首字母大写 其余小写、还要兼容 全部大写 const sortedRoles = [...roles].sort((a, b) => b.name.length - a.name.length).map(role => ({ ...role, name: role.name.split(' ').map(word => word.charAt(0).toUpperCase() + word.slice(1).toLowerCase()).join(' ') })).concat([...roles].map(role => ({ ...role, name: role.name.toUpperCase() }))); while (currentText.length > 0) { let matchFound = false; // 尝试匹配角色 for (const role of sortedRoles) { if (currentText.startsWith(role.name)) { // 如果当前文本以角色名开头 if (currentText.length > role.name.length) { // 添加角色标记节点 nodes.push({ type: 'characterToken', attrs: { name: role.name, avatar: role.imageUrl } }); // 移除已处理的角色名 currentText = currentText.slice(role.name.length); matchFound = true; break; } } } if (!matchFound) { // 如果没有找到角色匹配,处理普通文本 // 查找下一个可能的角色名位置 let nextRoleIndex = currentText.length; for (const role of sortedRoles) { const index = currentText.indexOf(role.name); if (index !== -1 && index < nextRoleIndex) { nextRoleIndex = index; } } // 添加文本节点 const textContent = currentText.slice(0, nextRoleIndex); if (textContent) { nodes.push({ type: 'text', text: textContent }); } // 移除已处理的文本 currentText = currentText.slice(nextRoleIndex); } } return nodes; } /** * 解析高亮文本,识别tag并转换为节点数组 * @param text 要解析的文本 * @param tags 标签列表 * @returns ContentNode[] 节点数组 */ public static parseHighlight(text: string, tags: TagValueObject[]): ContentNode[] { const nodes: ContentNode[] = []; let currentText = text; // 按内容长度降序排序,避免短名称匹配到长名称的一部分 const sortedTags = [...tags].sort((a, b) => String(b.content).length - String(a.content).length); while (currentText.length > 0) { let matchFound = false; // 尝试匹配 for (const tag of sortedTags) { if (currentText.startsWith(String(tag.content))) { // 如果当前文本以tag内容开头 if (currentText.length > String(tag.content).length) { // 添加标记节点 nodes.push({ type: 'highlightText', attrs: { text: String(tag.content), color: tag?.color || 'yellow' } }); // 移除已处理的tag内容 currentText = currentText.slice(String(tag.content).length); matchFound = true; break; } } } if (!matchFound) { // 如果没有找到tag匹配,处理普通文本 // 查找下一个可能的tag内容位置 let nextTagIndex = currentText.length; for (const tag of sortedTags) { const index = currentText.indexOf(String(tag.content)); if (index !== -1 && index < nextTagIndex) { nextTagIndex = index; } } // 添加文本节点 const textContent = currentText.slice(0, nextTagIndex); if (textContent) { nodes.push({ type: 'text', text: textContent }); } // 移除已处理的文本 currentText = currentText.slice(nextTagIndex); } } return nodes; } private readonly ShotData: Shot; constructor(shotData: Shot) { this.ShotData = shotData; } toShot() { return this.ShotData; } /** * 将 LensType 转换为 Paragraph 格式 * @param lensType LensType 实例 * @returns Paragraph 格式的数据 */ public static fromLensType(lensType: LensType, roles: SimpleCharacter[]): Shot { const shotDescContent: Paragraph[] = []; const shotDialogsContent: Paragraph[] = []; // 处理镜头描述 通过roles name 匹配镜头描述中出现的角色 并添加到shotDescContent if (lensType.script) { const descNodes = TextToShotAdapter.parseText(lensType.script, roles); shotDescContent.push({ type: 'paragraph', content: descNodes }); } // 处理对话内容 通过roles name 匹配对话内容中出现的角色 并添加到shotDialogsContent lensType.content.forEach(item => { const dialogNodes = TextToShotAdapter.parseText(item.content, roles); // 确保对话内容以角色标记开始 const roleMatch = roles.find(role => role.name === item.roleName); if (roleMatch) { const dialogContent: Paragraph = { type: 'paragraph', content: [{ type: 'characterToken', attrs: { name: roleMatch.name, avatar: roleMatch.imageUrl }}, ...dialogNodes ] }; shotDialogsContent.push(dialogContent); } }); return { name: lensType.name, shotDescContent, shotDialogsContent }; } /** * 将 Paragraph 转换为 LensType 格式 * @param paragraphData Paragraph 格式的数据 * @returns LensType 实例 */ /** * 将 Shot 格式转换为 LensType 格式 * @param shotData Shot 格式的数据 * @returns LensType 实例 */ public static toLensType(shotData: Shot): LensType { const content: ContentItem[] = []; let currentScript = ''; // 处理镜头描述 if (shotData.shotDescContent.length > 0) { // 合并所有描述段落的文本内容 shotData.shotDescContent.forEach(paragraph => { if (paragraph.content) { paragraph.content.forEach(node => { if (node.type === 'text') { currentScript += node.text; } if (node.type === 'characterToken') { currentScript += node.attrs.name; } }); } }); } // 处理对话内容 shotData.shotDialogsContent.forEach(paragraph => { let dialogRoleName = ''; let dialogContent = ''; let firstFindRole = false; // 遍历段落内容 if (paragraph.content) { paragraph.content.forEach((node, index) => { if (node.type === 'characterToken') { // 记录说话角色的名称 if (!firstFindRole) { dialogRoleName = node.attrs.name; firstFindRole = true; } else { dialogContent += node.attrs.name; } } else if (node.type === 'text') { // 累积对话内容 dialogContent += node.text; } }); } // 如果有角色名和对话内容,添加到结果中 if (dialogRoleName && dialogContent) { content.push({ roleName: dialogRoleName, content: dialogContent.trim() }); } }); return new LensType( shotData.name, // 使用 Shot 中的 name currentScript.trim(), content ); } public static fromTextToRole(description: string, tags: TagValueObject[]): Paragraph[] { const paragraph: Paragraph = { type: 'paragraph', content: [] }; const highlightNodes = TextToShotAdapter.parseHighlight(description, tags); paragraph.content.push(...highlightNodes); return [paragraph]; } public static fromRoleToText(paragraphs: Paragraph[]): string { let text = ''; paragraphs.forEach(paragraph => { if (paragraph?.content) { paragraph.content.forEach(node => { if (node.type === 'highlightText') { text += node.attrs.text; } else if (node.type === 'text') { text += node.text; } else if (node.type === 'characterToken') { text += node.attrs.name; } }); } }); return text; } }