forked from 77media/video-flow
更新文本适配器,新增高亮文本解析功能,支持将标签转换为节点数组。同时,扩展角色编辑器以支持高亮文本的处理,优化内容渲染逻辑,确保角色描述的准确性和可视化效果。
This commit is contained in:
parent
dbd803391d
commit
4f8d2188cf
@ -1,4 +1,4 @@
|
||||
import { ContentItem, LensType, SimpleCharacter } from '../domain/valueObject';
|
||||
import { ContentItem, LensType, SimpleCharacter, TagValueObject } from '../domain/valueObject';
|
||||
|
||||
// 定义角色属性接口
|
||||
interface CharacterAttributes {
|
||||
@ -8,6 +8,12 @@ interface CharacterAttributes {
|
||||
avatar: string;
|
||||
}
|
||||
|
||||
// 定义高亮属性接口
|
||||
interface HighlightAttributes {
|
||||
text: string;
|
||||
color: string;
|
||||
}
|
||||
|
||||
// 定义文本节点接口
|
||||
interface TextNode {
|
||||
type: 'text';
|
||||
@ -20,8 +26,14 @@ interface CharacterTokenNode {
|
||||
attrs: CharacterAttributes;
|
||||
}
|
||||
|
||||
// 定义高亮节点接口
|
||||
interface HighlightNode {
|
||||
type: 'highlightText';
|
||||
attrs: HighlightAttributes;
|
||||
}
|
||||
|
||||
// 定义内容节点类型(文本或角色标记)
|
||||
type ContentNode = TextNode | CharacterTokenNode;
|
||||
type ContentNode = TextNode | CharacterTokenNode | HighlightNode;
|
||||
|
||||
// 定义段落接口
|
||||
interface Paragraph {
|
||||
@ -100,6 +112,68 @@ export class TextToShotAdapter {
|
||||
|
||||
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;
|
||||
@ -225,4 +299,29 @@ export class TextToShotAdapter {
|
||||
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 => {
|
||||
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;
|
||||
}
|
||||
}
|
||||
@ -93,6 +93,8 @@ export interface TagValueObject {
|
||||
loadingProgress: number;
|
||||
/** 禁止编辑 */
|
||||
disableEdit: boolean;
|
||||
/** 颜色 */
|
||||
color?: string;
|
||||
}
|
||||
|
||||
|
||||
|
||||
@ -4,6 +4,20 @@ import { useShotService } from "@/app/service/Interaction/ShotService";
|
||||
import { useSearchParams } from 'next/navigation';
|
||||
import { useRoleServiceHook } from "@/app/service/Interaction/RoleService";
|
||||
|
||||
const mockRoleData = [{
|
||||
id: '1',
|
||||
name: 'KAPI',
|
||||
imageUrl: 'https://c.huiying.video/images/420bfb4f-b5d4-475c-a2fb-5e40af770b29.jpg',
|
||||
generateText: 'A 3 to 5-year-old boy with a light to medium olive skin tone, full cheeks, and warm brown eyes. He has short, straight, dark brown hair, neatly styled with a part on his left side. His facial structure includes a small, slightly upturned nose. His lips are typically held in a slight, gentle, closed-mouth smile, which can part to show his small, white teeth.',
|
||||
tags: [
|
||||
{ id: '1', content: 'boy', color: 'red' },
|
||||
{ id: '2', content: '3 to 5-year-old', color: 'yellow' },
|
||||
{ id: '3', content: 'light to medium olive skin tone', color: 'green' },
|
||||
{ id: '4', content: 'full cheeks', color: 'blue' },
|
||||
{ id: '5', content: 'warm brown eyes', color: 'purple' },
|
||||
]
|
||||
}]
|
||||
|
||||
export const useEditData = (tabType: string) => {
|
||||
const searchParams = useSearchParams();
|
||||
const projectId = searchParams.get('episodeId') || '';
|
||||
@ -26,7 +40,10 @@ export const useEditData = (tabType: string) => {
|
||||
userRoleLibrary,
|
||||
fetchRoleList,
|
||||
selectRole,
|
||||
fetchUserRoleLibrary
|
||||
fetchUserRoleLibrary,
|
||||
optimizeRoleText,
|
||||
updateRoleText,
|
||||
regenerateRole
|
||||
} = useRoleServiceHook();
|
||||
|
||||
useEffect(() => {
|
||||
@ -39,6 +56,7 @@ export const useEditData = (tabType: string) => {
|
||||
setLoading(false);
|
||||
});
|
||||
} else if (tabType === 'role') {
|
||||
fetchUserRoleLibrary();
|
||||
fetchRoleList(projectId).then(() => {
|
||||
setLoading(false);
|
||||
}).catch((err) => {
|
||||
@ -55,7 +73,8 @@ export const useEditData = (tabType: string) => {
|
||||
}, [videoSegments]);
|
||||
|
||||
useEffect(() => {
|
||||
setRoleData(roleList);
|
||||
// setRoleData(roleList);
|
||||
setRoleData(mockRoleData);
|
||||
}, [roleList]);
|
||||
|
||||
return {
|
||||
@ -70,6 +89,8 @@ export const useEditData = (tabType: string) => {
|
||||
selectRole,
|
||||
selectedRole,
|
||||
userRoleLibrary,
|
||||
fetchUserRoleLibrary
|
||||
optimizeRoleText,
|
||||
updateRoleText,
|
||||
regenerateRole
|
||||
}
|
||||
}
|
||||
@ -1,11 +1,16 @@
|
||||
import { useState, useRef } from "react";
|
||||
import React, { useState, useRef, useEffect, forwardRef } from "react";
|
||||
import { motion } from "framer-motion";
|
||||
import { Sparkles, X, Plus, RefreshCw } from 'lucide-react';
|
||||
import MainEditor from "./main-editor/MainEditor";
|
||||
import { cn } from "@/public/lib/utils";
|
||||
import { TextToShotAdapter } from "@/app/service/adapter/textToShot";
|
||||
import { TagValueObject } from "@/app/service/domain/valueObject";
|
||||
|
||||
interface CharacterEditorProps {
|
||||
className?: string;
|
||||
description: string;
|
||||
highlight: TagValueObject[];
|
||||
onSmartPolish: (text: string) => void;
|
||||
}
|
||||
|
||||
const mockContent = [
|
||||
@ -30,20 +35,50 @@ const mockContent = [
|
||||
},
|
||||
];
|
||||
|
||||
|
||||
export default function CharacterEditor({
|
||||
export const CharacterEditor = forwardRef<any, CharacterEditorProps>(({
|
||||
className,
|
||||
}: CharacterEditorProps) {
|
||||
description,
|
||||
highlight,
|
||||
onSmartPolish
|
||||
}, ref) => {
|
||||
const [isOptimizing, setIsOptimizing] = useState(false);
|
||||
const [content, setContent] = useState<any[]>([]);
|
||||
const [isInit, setIsInit] = useState(true);
|
||||
|
||||
const handleSmartPolish = async () => {
|
||||
|
||||
setIsOptimizing(true);
|
||||
console.log('-==========handleSmartPolish===========-', content);
|
||||
const text = TextToShotAdapter.fromRoleToText(content);
|
||||
console.log('-==========getText===========-', text);
|
||||
onSmartPolish(text);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
setIsInit(true);
|
||||
console.log('-==========description===========-', description);
|
||||
console.log('-==========highlight===========-', highlight);
|
||||
const paragraphs = TextToShotAdapter.fromTextToRole(description, highlight);
|
||||
console.log('-==========paragraphs===========-', paragraphs);
|
||||
setContent(paragraphs);
|
||||
setTimeout(() => {
|
||||
setIsInit(false);
|
||||
setIsOptimizing(false);
|
||||
}, 100);
|
||||
}, [description, highlight]);
|
||||
|
||||
// 暴露方法给父组件
|
||||
React.useImperativeHandle(ref, () => ({
|
||||
getRoleText: () => {
|
||||
return TextToShotAdapter.fromRoleToText(content);
|
||||
}
|
||||
}));
|
||||
|
||||
return (
|
||||
<div className={cn("space-y-2 border border-white/10 relative p-2 rounded-[0.5rem] pb-12", className)}>
|
||||
{/* 自由输入区域 */}
|
||||
<MainEditor content={mockContent} />
|
||||
{
|
||||
!isInit && <MainEditor content={content} onChangeContent={setContent} />
|
||||
}
|
||||
|
||||
{/* 智能润色按钮 */}
|
||||
<motion.button
|
||||
@ -60,4 +95,4 @@ export default function CharacterEditor({
|
||||
</motion.button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
@ -2,7 +2,7 @@ import React, { useState, useRef, useEffect } from 'react';
|
||||
import { motion, AnimatePresence } from 'framer-motion';
|
||||
import { ImageUp, Library, Play, Pause, RefreshCw, Wand2, Users, Check, ReplaceAll, X, TriangleAlert } from 'lucide-react';
|
||||
import { cn } from '@/public/lib/utils';
|
||||
import CharacterEditor from './character-editor';
|
||||
import { CharacterEditor } from './character-editor';
|
||||
import ImageBlurTransition from './ImageBlurTransition';
|
||||
import FloatingGlassPanel from './FloatingGlassPanel';
|
||||
import { ReplaceCharacterPanel, mockShots, mockCharacter } from './replace-character-panel';
|
||||
@ -72,6 +72,7 @@ export function CharacterTabContent({
|
||||
const fileInputRef = useRef<HTMLInputElement>(null);
|
||||
const [enableAnimation, setEnableAnimation] = useState(true);
|
||||
const [showAddToLibrary, setShowAddToLibrary] = useState(true);
|
||||
const characterEditorRef = useRef<any>(null);
|
||||
|
||||
const {
|
||||
loading,
|
||||
@ -79,15 +80,25 @@ export function CharacterTabContent({
|
||||
selectRole,
|
||||
selectedRole,
|
||||
userRoleLibrary,
|
||||
fetchUserRoleLibrary
|
||||
optimizeRoleText,
|
||||
updateRoleText,
|
||||
regenerateRole
|
||||
} = useEditData('role');
|
||||
|
||||
useEffect(() => {
|
||||
console.log('-==========roleData===========-', roleData);
|
||||
if (roleData.length > 0) {
|
||||
selectRole(roleData[selectRoleIndex].id);
|
||||
}
|
||||
}, [selectRoleIndex, roleData]);
|
||||
|
||||
const handleSmartPolish = (text: string) => {
|
||||
// 首先更新
|
||||
updateRoleText(text);
|
||||
// 然后调用优化角色文本
|
||||
optimizeRoleText(text);
|
||||
};
|
||||
|
||||
const handleConfirmGotoReplace = () => {
|
||||
setIsRemindReplacePanelOpen(false);
|
||||
setIsReplacePanelOpen(true);
|
||||
@ -148,12 +159,16 @@ export function CharacterTabContent({
|
||||
const handleOpenReplaceLibrary = () => {
|
||||
setIsReplaceLibraryOpen(true);
|
||||
setShowAddToLibrary(true);
|
||||
fetchUserRoleLibrary();
|
||||
};
|
||||
|
||||
const handleRegenerate = () => {
|
||||
console.log('Regenerate');
|
||||
setShowAddToLibrary(true);
|
||||
const text = characterEditorRef.current.getRoleText();
|
||||
console.log('-==========text===========-', text);
|
||||
// 重生前 更新 当前项 generateText
|
||||
updateRoleText(text);
|
||||
// 然后调用重新生成角色
|
||||
regenerateRole();
|
||||
};
|
||||
|
||||
const handleUploadClick = () => {
|
||||
@ -260,8 +275,8 @@ export function CharacterTabContent({
|
||||
{/* 角色预览图 */}
|
||||
<div className="w-full h-full mx-auto rounded-lg relative group">
|
||||
<ImageBlurTransition
|
||||
src={selectedRole?.imageUrl || ''}
|
||||
alt={selectedRole?.name}
|
||||
src={roleData[selectRoleIndex].imageUrl || ''}
|
||||
alt={roleData[selectRoleIndex].name}
|
||||
width='100%'
|
||||
height='100%'
|
||||
enableAnimation={enableAnimation}
|
||||
@ -293,7 +308,11 @@ export function CharacterTabContent({
|
||||
{/* 右列:角色信息 */}
|
||||
<div className="space-y-4">
|
||||
<CharacterEditor
|
||||
ref={characterEditorRef}
|
||||
className="min-h-[calc(100%-4rem)]"
|
||||
description={roleData[selectRoleIndex].generateText || ''}
|
||||
highlight={roleData[selectRoleIndex].tags || []}
|
||||
onSmartPolish={handleSmartPolish}
|
||||
/>
|
||||
{/* 重新生成按钮、替换形象按钮 */}
|
||||
<div className="grid grid-cols-2 gap-2">
|
||||
|
||||
@ -1,19 +1,27 @@
|
||||
import React, { useState, useCallback, useEffect } from 'react';
|
||||
import { flushSync } from 'react-dom';
|
||||
import { EditorContent, useEditor } from '@tiptap/react';
|
||||
import StarterKit from '@tiptap/starter-kit';
|
||||
import { HighlightTextExtension } from './HighlightText';
|
||||
|
||||
interface MainEditorProps {
|
||||
content: any[];
|
||||
onChangeContent?: (content: any[]) => void;
|
||||
}
|
||||
|
||||
export default function MainEditor({ content }: MainEditorProps) {
|
||||
export default function MainEditor({ content, onChangeContent }: MainEditorProps) {
|
||||
const [renderContent, setRenderContent] = useState<any[]>(content);
|
||||
|
||||
useEffect(() => {
|
||||
onChangeContent?.(renderContent);;
|
||||
}, [renderContent]);
|
||||
|
||||
const editor = useEditor({
|
||||
extensions: [
|
||||
StarterKit,
|
||||
HighlightTextExtension,
|
||||
],
|
||||
content: { type: 'doc', content: content },
|
||||
content: { type: 'doc', content: renderContent },
|
||||
editorProps: {
|
||||
attributes: {
|
||||
class: 'prose prose-invert max-w-none focus:outline-none'
|
||||
@ -23,6 +31,12 @@ export default function MainEditor({ content }: MainEditorProps) {
|
||||
onCreate: ({ editor }) => {
|
||||
editor.setOptions({ editable: true })
|
||||
},
|
||||
onUpdate: ({ editor }) => {
|
||||
const json = editor.getJSON();
|
||||
flushSync(() => {
|
||||
setRenderContent(json.content);
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
if (!editor) {
|
||||
|
||||
@ -45,7 +45,7 @@ export function ShotTabContent({
|
||||
if (shotData.length > 0) {
|
||||
setSelectedSegment(shotData[selectedIndex]);
|
||||
}
|
||||
}, [selectedIndex]);
|
||||
}, [selectedIndex, shotData]);
|
||||
|
||||
// 处理扫描开始
|
||||
const handleScan = () => {
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user