forked from 77media/video-flow
228 lines
6.1 KiB
TypeScript
228 lines
6.1 KiB
TypeScript
import { ContentItem, LensType, SimpleCharacter } from '../domain/valueObject';
|
|
|
|
// 定义角色属性接口
|
|
interface CharacterAttributes {
|
|
name: string;
|
|
// gender: string;
|
|
// age: string;
|
|
avatar: string;
|
|
}
|
|
|
|
// 定义文本节点接口
|
|
interface TextNode {
|
|
type: 'text';
|
|
text: string;
|
|
}
|
|
|
|
// 定义角色标记节点接口
|
|
interface CharacterTokenNode {
|
|
type: 'characterToken';
|
|
attrs: CharacterAttributes;
|
|
}
|
|
|
|
// 定义内容节点类型(文本或角色标记)
|
|
type ContentNode = TextNode | CharacterTokenNode;
|
|
|
|
// 定义段落接口
|
|
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);
|
|
|
|
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;
|
|
}
|
|
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 => {
|
|
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
|
|
);
|
|
}
|
|
} |