forked from 77media/video-flow
337 lines
9.5 KiB
TypeScript
337 lines
9.5 KiB
TypeScript
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 => {
|
||
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;
|
||
}
|
||
} |