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 (
+
+ );
+ }
+
// 如果没有角色数据,显示占位内容
- 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 = () => {