From c12b77a74e85f5a89c609e26e01b0c070055cf3e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=8C=97=E6=9E=B3?= <7854742+wang_rumeng@user.noreply.gitee.com> Date: Sun, 10 Aug 2025 20:39:23 +0800 Subject: [PATCH 1/3] =?UTF-8?q?=E6=9B=B4=E6=96=B0=E5=B7=A5=E4=BD=9C?= =?UTF-8?q?=E6=B5=81=E7=BB=84=E4=BB=B6=EF=BC=8C=E6=96=B0=E5=A2=9E=E5=9B=9E?= =?UTF-8?q?=E9=80=80=E5=88=B0=E6=8C=87=E5=AE=9A=E6=AD=A5=E9=AA=A4=E7=9A=84?= =?UTF-8?q?=E5=8A=9F=E8=83=BD=EF=BC=8C=E5=B9=B6=E4=BC=98=E5=8C=96=E6=9A=82?= =?UTF-8?q?=E5=81=9C/=E6=92=AD=E6=94=BE=E6=8C=89=E9=92=AE=E7=9A=84?= =?UTF-8?q?=E6=98=BE=E7=A4=BA=E9=80=BB=E8=BE=91=E3=80=82=E5=90=8C=E6=97=B6?= =?UTF-8?q?=EF=BC=8C=E7=BC=96=E8=BE=91=E6=A8=A1=E6=80=81=E6=A1=86=E4=B8=AD?= =?UTF-8?q?=E9=9B=86=E6=88=90=E4=BA=86=E5=9B=9E=E9=80=80=E5=8A=9F=E8=83=BD?= =?UTF-8?q?=EF=BC=8C=E7=A1=AE=E4=BF=9D=E5=9C=A8=E4=B8=8D=E5=90=8C=E9=80=89?= =?UTF-8?q?=E9=A1=B9=E4=B8=8B=E8=83=BD=E5=A4=9F=E6=AD=A3=E7=A1=AE=E5=BA=94?= =?UTF-8?q?=E7=94=A8=E5=89=A7=E6=9C=AC=E3=80=82=E6=9B=B4=E6=96=B0=E4=BA=86?= =?UTF-8?q?=E7=9B=B8=E5=85=B3=E6=8E=A5=E5=8F=A3=E7=9A=84=E5=AF=BC=E5=85=A5?= =?UTF-8?q?=EF=BC=8C=E6=8F=90=E5=8D=87=E4=BA=86=E4=BB=A3=E7=A0=81=E7=9A=84?= =?UTF-8?q?=E5=8F=AF=E8=AF=BB=E6=80=A7=E5=92=8C=E5=8A=9F=E8=83=BD=E6=80=A7?= =?UTF-8?q?=E3=80=82?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- components/pages/work-flow.tsx | 9 ++++++--- .../pages/work-flow/use-workflow-data.tsx | 19 +++++++++++++++---- components/ui/edit-modal.tsx | 17 +++++++++++++---- components/ui/shot-tab-content.tsx | 2 +- 4 files changed, 35 insertions(+), 12 deletions(-) diff --git a/components/pages/work-flow.tsx b/components/pages/work-flow.tsx index 1f024ed..5966bd3 100644 --- a/components/pages/work-flow.tsx +++ b/components/pages/work-flow.tsx @@ -46,7 +46,8 @@ export default function WorkFlow() { mode, setIsPauseWorkFlow, setAnyAttribute, - applyScript + applyScript, + fallbackToStep } = useWorkflowData(); const { @@ -225,8 +226,8 @@ export default function WorkFlow() { {/* 暂停/播放按钮 */} { - currentStep !== '6' && ( -
+ (currentStep !== '6' && currentStep !== '0') && ( +
diff --git a/components/pages/work-flow/use-workflow-data.tsx b/components/pages/work-flow/use-workflow-data.tsx index 26e99d4..2a77e2b 100644 --- a/components/pages/work-flow/use-workflow-data.tsx +++ b/components/pages/work-flow/use-workflow-data.tsx @@ -2,7 +2,7 @@ import { useState, useEffect, useCallback } from 'react'; import { useSearchParams } from 'next/navigation'; -import { detailScriptEpisodeNew, getScriptTitle, getRunningStreamData } from '@/api/video_flow'; +import { detailScriptEpisodeNew, getScriptTitle, getRunningStreamData, pauseMovieProjectPlan, resumeMovieProjectPlan } from '@/api/video_flow'; import { useAppDispatch, useAppSelector } from '@/lib/store/hooks'; import { setSketchCount, setVideoCount } from '@/lib/store/workflowSlice'; import { useScriptService } from "@/app/service/Interaction/ScriptService"; @@ -96,7 +96,7 @@ export function useWorkflowData() { console.log('开始初始化剧本', originalText); originalText && initializeFromProject(episodeId, originalText).then(() => { console.log('应用剧本'); - // 默认模式下 应用剧本 + // 自动模式下 应用剧本;手动模式 需要点击 下一步 触发 mode.includes('auto') && applyScript(); }); }, [originalText]); @@ -110,7 +110,11 @@ export function useWorkflowData() { }, [scriptBlocksMemo]); // 监听继续 请求更新数据 useEffect(() => { - + if (isPauseWorkFlow) { + pauseMovieProjectPlan({ project_id: episodeId }); + } else { + resumeMovieProjectPlan({ project_id: episodeId }); + } }, [isPauseWorkFlow]); // 自动开始播放一轮 @@ -568,6 +572,12 @@ export function useWorkflowData() { } }; + // 回退到 指定状态 重新获取数据 + const fallbackToStep = (step: string) => { + setCurrentStep(step); + setNeedStreamData(true); + } + // 重试加载数据 const retryLoadData = () => { setDataLoadError(null); @@ -617,6 +627,7 @@ export function useWorkflowData() { mode, setIsPauseWorkFlow, setAnyAttribute, - applyScript + applyScript, + fallbackToStep }; } diff --git a/components/ui/edit-modal.tsx b/components/ui/edit-modal.tsx index 36b7baa..3c3024b 100644 --- a/components/ui/edit-modal.tsx +++ b/components/ui/edit-modal.tsx @@ -29,6 +29,7 @@ interface EditModalProps { isPauseWorkFlow: boolean; scriptData: any[] | null; applyScript: any; + fallbackToStep: any; } const tabs = [ @@ -57,7 +58,8 @@ export function EditModal({ setAnyAttribute, isPauseWorkFlow, scriptData, - applyScript + applyScript, + fallbackToStep }: EditModalProps) { const [activeTab, setActiveTab] = useState(activeEditTab); const [currentIndex, setCurrentIndex] = useState(currentSketchIndex); @@ -104,6 +106,13 @@ export function EditModal({ const handleConfirmGotoFallback = () => { console.log('handleConfirmGotoFallback'); + if (activeTab === '0') { + fallbackToStep('0'); + // 应用剧本 + applyScript(); + } else { + fallbackToStep('1'); + } } const handleCloseRemindFallbackPanel = () => { setIsRemindFallbackOpen(false); @@ -295,7 +304,7 @@ export function EditModal({
-

将重新生成视频并剪辑,是否需要继续?

+

The task will be regenerated and edited. Do you want to continue?

@@ -305,7 +314,7 @@ export function EditModal({ className="px-4 py-2 bg-blue-600 hover:bg-blue-700 rounded-md transition-colors duration-200 flex items-center gap-2" > - 继续 + Continue
diff --git a/components/ui/shot-tab-content.tsx b/components/ui/shot-tab-content.tsx index 1bfc3b5..11cbf63 100644 --- a/components/ui/shot-tab-content.tsx +++ b/components/ui/shot-tab-content.tsx @@ -236,7 +236,7 @@ export function ShotTabContent({ >
- Segment {index + 1} + Segment {index + 1} {shot.status === 0 && ( )} From dbd803391d301e93ad9daee722a6073c7c9c6483 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=8C=97=E6=9E=B3?= <7854742+wang_rumeng@user.noreply.gitee.com> Date: Sun, 10 Aug 2025 21:23:04 +0800 Subject: [PATCH 2/3] =?UTF-8?q?=E6=9B=B4=E6=96=B0=20use-edit-data=20?= =?UTF-8?q?=E7=BB=84=E4=BB=B6=EF=BC=8C=E7=A7=BB=E9=99=A4=20mock=20?= =?UTF-8?q?=E6=95=B0=E6=8D=AE=EF=BC=8C=E6=96=B0=E5=A2=9E=E8=A7=92=E8=89=B2?= =?UTF-8?q?=E6=95=B0=E6=8D=AE=E5=A4=84=E7=90=86=E9=80=BB=E8=BE=91=EF=BC=8C?= =?UTF-8?q?=E4=BC=98=E5=8C=96=E8=A7=86=E9=A2=91=E7=89=87=E6=AE=B5=E5=92=8C?= =?UTF-8?q?=E8=A7=92=E8=89=B2=E9=80=89=E6=8B=A9=E5=8A=9F=E8=83=BD=E3=80=82?= =?UTF-8?q?=E5=90=8C=E6=97=B6=EF=BC=8C=E8=B0=83=E6=95=B4=E8=A7=92=E8=89=B2?= =?UTF-8?q?=E6=95=B0=E6=8D=AE=E7=9A=84=E5=8A=A0=E8=BD=BD=E5=92=8C=E9=94=99?= =?UTF-8?q?=E8=AF=AF=E5=A4=84=E7=90=86=EF=BC=8C=E7=A1=AE=E4=BF=9D=E5=9C=A8?= =?UTF-8?q?=E4=B8=8D=E5=90=8C=E9=80=89=E9=A1=B9=E4=B8=8B=E8=83=BD=E5=A4=9F?= =?UTF-8?q?=E6=AD=A3=E7=A1=AE=E5=BA=94=E7=94=A8=E8=A7=92=E8=89=B2=E4=BF=A1?= =?UTF-8?q?=E6=81=AF=E3=80=82?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- components/pages/work-flow/use-edit-data.tsx | 103 ++++++------------- components/ui/character-tab-content.tsx | 52 ++++++---- 2 files changed, 65 insertions(+), 90 deletions(-) diff --git a/components/pages/work-flow/use-edit-data.tsx b/components/pages/work-flow/use-edit-data.tsx index 5b28a82..64a7ce6 100644 --- a/components/pages/work-flow/use-edit-data.tsx +++ b/components/pages/work-flow/use-edit-data.tsx @@ -2,74 +2,7 @@ import { useEffect, useState } from "react"; import { useShotService } from "@/app/service/Interaction/ShotService"; import { useSearchParams } from 'next/navigation'; - -const mockShotData = [ - { - id: '1', - name: 'Shot 1', - sketchUrl: 'https://example.com/sketch.png', - videoUrl: ['https://video-base-imf.oss-ap-southeast-7.aliyuncs.com/uploads/FJ1-0-20250725023719.mp4'], - status: 1, // 0:视频加载中 1:任务已完成 2:任务失败 - lens: [ - { - name: 'Shot 1', - script: '镜头聚焦在 President Alfred King 愤怒的脸上,他被压制住了。他发出一声绝望的吼叫,盖过了枪声。', - content: [{ - roleName: 'President Alfred King', - content: '我需要一个镜头,镜头聚焦在 愤怒的脸上,他被压制住了。他发出一声绝望的吼叫,盖过了枪声。' - }] - } - ] - }, { - id: '2', - name: 'Shot 2', - sketchUrl: 'https://example.com/sketch.png', - videoUrl: ['https://video-base-imf.oss-ap-southeast-7.aliyuncs.com/uploads/FJ3-0-20250725023725.mp4'], - status: 1, // 0:视频加载中 1:任务已完成 2:任务失败 - lens: [ - { - name: 'Shot 1', - script: '镜头聚焦在 Samuel Ryan 愤怒的脸上,他被压制住了。他发出一声绝望的吼叫,盖过了枪声。', - content: [{ - roleName: 'Samuel Ryan', - content: '我需要一个镜头,镜头聚焦在 Samuel Ryan 愤怒的脸上,他被压制住了。他发出一声绝望的吼叫,盖过了枪声。' - }] - } - ] - }, { - id: '3', - name: 'Shot 3', - sketchUrl: 'https://example.com/sketch.png', - videoUrl: [], - status: 0, // 0:视频加载中 1:任务已完成 2:任务失败 - lens: [ - { - name: 'Shot 1', - script: '镜头聚焦在 Samuel Ryan 愤怒的脸上,他被压制住了。他发出一声绝望的吼叫,盖过了枪声。', - content: [{ - roleName: 'Samuel Ryan', - content: '我需要一个镜头,镜头聚焦在 Samuel Ryan 愤怒的脸上,他被压制住了。他发出一声绝望的吼叫,盖过了枪声。' - }] - } - ] - }, { - id: '4', - name: 'Shot 4', - sketchUrl: 'https://example.com/sketch.png', - videoUrl: [], - status: 2, // 0:视频加载中 1:任务已完成 2:任务失败 - lens: [ - { - name: 'Shot 1', - script: '镜头聚焦在 Samuel Ryan 愤怒的脸上,他被压制住了。他发出一声绝望的吼叫,盖过了枪声。', - content: [{ - roleName: 'Samuel Ryan', - content: '我需要一个镜头,镜头聚焦在 Samuel Ryan 愤怒的脸上,他被压制住了。他发出一声绝望的吼叫,盖过了枪声。' - }] - } - ] - } -] +import { useRoleServiceHook } from "@/app/service/Interaction/RoleService"; export const useEditData = (tabType: string) => { const searchParams = useSearchParams(); @@ -77,6 +10,8 @@ export const useEditData = (tabType: string) => { const [loading, setLoading] = useState(true); const [shotData, setShotData] = useState([]); + const [roleData, setRoleData] = useState([]); + const { videoSegments, getVideoSegmentList, @@ -85,13 +20,30 @@ export const useEditData = (tabType: string) => { filterRole } = useShotService(); + const { + roleList, + selectedRole, + userRoleLibrary, + fetchRoleList, + selectRole, + fetchUserRoleLibrary + } = useRoleServiceHook(); + useEffect(() => { if (tabType === 'shot') { getVideoSegmentList(projectId).then(() => { setLoading(false); }).catch((err) => { console.log('useEditData-----err', err); - setShotData(mockShotData); + setShotData([]); + setLoading(false); + }); + } else if (tabType === 'role') { + fetchRoleList(projectId).then(() => { + setLoading(false); + }).catch((err) => { + console.log('useEditData-----err', err); + setRoleData([]); setLoading(false); }); } @@ -102,11 +54,22 @@ export const useEditData = (tabType: string) => { setShotData(videoSegments); }, [videoSegments]); + useEffect(() => { + setRoleData(roleList); + }, [roleList]); + return { loading, + // shot shotData, setSelectedSegment, regenerateVideoSegment, - filterRole + filterRole, + // role + roleData, + selectRole, + selectedRole, + userRoleLibrary, + fetchUserRoleLibrary } } \ No newline at end of file diff --git a/components/ui/character-tab-content.tsx b/components/ui/character-tab-content.tsx index 3c0e8bb..79b8710 100644 --- a/components/ui/character-tab-content.tsx +++ b/components/ui/character-tab-content.tsx @@ -8,7 +8,7 @@ import FloatingGlassPanel from './FloatingGlassPanel'; import { ReplaceCharacterPanel, mockShots, mockCharacter } from './replace-character-panel'; import { CharacterLibrarySelector } from './character-library-selector'; import HorizontalScroller from './HorizontalScroller'; -import { useRoleServiceHook } from '@/app/service/Interaction/RoleService'; +import { useEditData } from '@/components/pages/work-flow/use-edit-data'; interface Appearance { hairStyle: string; @@ -63,8 +63,6 @@ export function CharacterTabContent({ onSketchSelect, roles = [mockRole] }: CharacterTabContentProps) { - const [localRole, setLocalRole] = useState(mockRole); - const [currentRole, setCurrentRole] = useState(roles[currentRoleIndex]); const [isReplacePanelOpen, setIsReplacePanelOpen] = useState(false); const [replacePanelKey, setReplacePanelKey] = useState(0); const [ignoreReplace, setIgnoreReplace] = useState(false); @@ -74,14 +72,22 @@ export function CharacterTabContent({ const fileInputRef = useRef(null); const [enableAnimation, setEnableAnimation] = useState(true); const [showAddToLibrary, setShowAddToLibrary] = useState(true); - const {fetchRoleList,roleList,fetchUserRoleLibrary,userRoleLibrary} = useRoleServiceHook() + + const { + loading, + roleData, + selectRole, + selectedRole, + userRoleLibrary, + fetchUserRoleLibrary + } = useEditData('role'); + useEffect(() => { - // 从url 获取 episodeId 作为projctId - const projectId = new URLSearchParams(window.location.search).get('episodeId'); - if (projectId) { - fetchRoleList(projectId); + if (roleData.length > 0) { + selectRole(roleData[selectRoleIndex].id); } - }, [fetchRoleList]); + }, [selectRoleIndex, roleData]); + const handleConfirmGotoReplace = () => { setIsRemindReplacePanelOpen(false); setIsReplacePanelOpen(true); @@ -94,10 +100,7 @@ export function CharacterTabContent({ const handleReplaceCharacter = (url: string) => { setEnableAnimation(true); - setCurrentRole({ - ...currentRole, - url: url - }); + // 替换角色 setIsReplacePanelOpen(true); }; @@ -115,7 +118,7 @@ export function CharacterTabContent({ }; const handleChangeRole = (index: number) => { - if (currentRole.url !== roles[selectRoleIndex].url && !ignoreReplace) { + if (selectedRole?.imageUrl !== roleData[selectRoleIndex].imageUrl && !ignoreReplace) { // 提示 角色已修改,弹出替换角色面板 setIsRemindReplacePanelOpen(true); return; @@ -125,7 +128,6 @@ export function CharacterTabContent({ setIgnoreReplace(false); setSelectRoleIndex(index); - setCurrentRole(roles[index]); }; // 从角色库中选择角色 @@ -177,12 +179,22 @@ export function CharacterTabContent({ event.target.value = ''; }; + // 如果loading 显示loading状态 + if (loading) { + return ( +
+
+

Loading...

+
+ ); + } + // 如果没有角色数据,显示占位内容 - if (!roles || roles.length === 0) { + if (roleData.length === 0) { return (
-

No character data

+

No role data

); } @@ -210,7 +222,7 @@ export function CharacterTabContent({ selectedIndex={selectRoleIndex} onItemClick={(i: number) => handleChangeRole(i)} > - {roleList.map((role, index) => ( + {roleData.map((role, index) => ( Date: Tue, 12 Aug 2025 14:30:10 +0800 Subject: [PATCH 3/3] =?UTF-8?q?=E6=9B=B4=E6=96=B0=E6=96=87=E6=9C=AC?= =?UTF-8?q?=E9=80=82=E9=85=8D=E5=99=A8=EF=BC=8C=E6=96=B0=E5=A2=9E=E9=AB=98?= =?UTF-8?q?=E4=BA=AE=E6=96=87=E6=9C=AC=E8=A7=A3=E6=9E=90=E5=8A=9F=E8=83=BD?= =?UTF-8?q?=EF=BC=8C=E6=94=AF=E6=8C=81=E5=B0=86=E6=A0=87=E7=AD=BE=E8=BD=AC?= =?UTF-8?q?=E6=8D=A2=E4=B8=BA=E8=8A=82=E7=82=B9=E6=95=B0=E7=BB=84=E3=80=82?= =?UTF-8?q?=E5=90=8C=E6=97=B6=EF=BC=8C=E6=89=A9=E5=B1=95=E8=A7=92=E8=89=B2?= =?UTF-8?q?=E7=BC=96=E8=BE=91=E5=99=A8=E4=BB=A5=E6=94=AF=E6=8C=81=E9=AB=98?= =?UTF-8?q?=E4=BA=AE=E6=96=87=E6=9C=AC=E7=9A=84=E5=A4=84=E7=90=86=EF=BC=8C?= =?UTF-8?q?=E4=BC=98=E5=8C=96=E5=86=85=E5=AE=B9=E6=B8=B2=E6=9F=93=E9=80=BB?= =?UTF-8?q?=E8=BE=91=EF=BC=8C=E7=A1=AE=E4=BF=9D=E8=A7=92=E8=89=B2=E6=8F=8F?= =?UTF-8?q?=E8=BF=B0=E7=9A=84=E5=87=86=E7=A1=AE=E6=80=A7=E5=92=8C=E5=8F=AF?= =?UTF-8?q?=E8=A7=86=E5=8C=96=E6=95=88=E6=9E=9C=E3=80=82?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/service/adapter/textToShot.ts | 103 ++++++++++++++++++- app/service/domain/valueObject.ts | 2 + components/pages/work-flow/use-edit-data.tsx | 27 ++++- components/ui/character-editor.tsx | 49 +++++++-- components/ui/character-tab-content.tsx | 31 ++++-- components/ui/main-editor/MainEditor.tsx | 18 +++- components/ui/shot-tab-content.tsx | 2 +- 7 files changed, 211 insertions(+), 21 deletions(-) diff --git a/app/service/adapter/textToShot.ts b/app/service/adapter/textToShot.ts index 3c6d5ca..de4da67 100644 --- a/app/service/adapter/textToShot.ts +++ b/app/service/adapter/textToShot.ts @@ -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; + } } \ No newline at end of file diff --git a/app/service/domain/valueObject.ts b/app/service/domain/valueObject.ts index 0d6e30a..25b5be2 100644 --- a/app/service/domain/valueObject.ts +++ b/app/service/domain/valueObject.ts @@ -93,6 +93,8 @@ export interface TagValueObject { loadingProgress: number; /** 禁止编辑 */ disableEdit: boolean; + /** 颜色 */ + color?: string; } diff --git a/components/pages/work-flow/use-edit-data.tsx b/components/pages/work-flow/use-edit-data.tsx index 64a7ce6..54495de 100644 --- a/components/pages/work-flow/use-edit-data.tsx +++ b/components/pages/work-flow/use-edit-data.tsx @@ -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 } } \ No newline at end of file diff --git a/components/ui/character-editor.tsx b/components/ui/character-editor.tsx index b0af4c8..1834e9d 100644 --- a/components/ui/character-editor.tsx +++ b/components/ui/character-editor.tsx @@ -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(({ className, -}: CharacterEditorProps) { + description, + highlight, + onSmartPolish +}, ref) => { const [isOptimizing, setIsOptimizing] = useState(false); + const [content, setContent] = useState([]); + 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 (
{/* 自由输入区域 */} - + { + !isInit && + } {/* 智能润色按钮 */}
); -} +}); diff --git a/components/ui/character-tab-content.tsx b/components/ui/character-tab-content.tsx index 79b8710..bfc05c0 100644 --- a/components/ui/character-tab-content.tsx +++ b/components/ui/character-tab-content.tsx @@ -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(null); const [enableAnimation, setEnableAnimation] = useState(true); const [showAddToLibrary, setShowAddToLibrary] = useState(true); + const characterEditorRef = useRef(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({ {/* 角色预览图 */}
{/* 重新生成按钮、替换形象按钮 */}
diff --git a/components/ui/main-editor/MainEditor.tsx b/components/ui/main-editor/MainEditor.tsx index 35a96a2..605d1ac 100644 --- a/components/ui/main-editor/MainEditor.tsx +++ b/components/ui/main-editor/MainEditor.tsx @@ -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(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) { diff --git a/components/ui/shot-tab-content.tsx b/components/ui/shot-tab-content.tsx index 11cbf63..d2679b4 100644 --- a/components/ui/shot-tab-content.tsx +++ b/components/ui/shot-tab-content.tsx @@ -45,7 +45,7 @@ export function ShotTabContent({ if (shotData.length > 0) { setSelectedSegment(shotData[selectedIndex]); } - }, [selectedIndex]); + }, [selectedIndex, shotData]); // 处理扫描开始 const handleScan = () => {