diff --git a/app/service/Interaction/ShotService.ts b/app/service/Interaction/ShotService.ts index 1b5f0b2..08010e4 100644 --- a/app/service/Interaction/ShotService.ts +++ b/app/service/Interaction/ShotService.ts @@ -6,7 +6,7 @@ import { MatchedPerson, RoleRecognitionResponse } from "@/api/DTO/movieEdit"; -import { VideoSegmentEntity } from "../domain/Entities"; +import { ScriptRoleEntity, VideoSegmentEntity } from "../domain/Entities"; import { LensType, SimpleCharacter } from "../domain/valueObject"; import { getUploadToken, uploadToQiniu } from "@/api/common"; import { SaveEditUseCase } from "../usecase/SaveEditUseCase"; @@ -21,6 +21,8 @@ export interface UseShotService { loading: boolean; /** 视频片段列表 */ videoSegments: VideoSegmentEntity[]; + /** 剧本中角色列表 */ + scriptRoles: ScriptRoleEntity[]; /** 当前选中的视频片段 */ selectedSegment: VideoSegmentEntity | null; /** 识别出的人物信息 */ @@ -69,6 +71,7 @@ export const useShotService = (): UseShotService => { // 响应式状态 const [loading, setLoading] = useState(false); const [videoSegments, setVideoSegments] = useState([]); + const [scriptRoles, setScriptRoles] = useState([]); const [selectedSegment, setSelectedSegment] = useState(null); const [projectId, setProjectId] = useState(""); @@ -90,9 +93,10 @@ export const useShotService = (): UseShotService => { try { setLoading(true); - const segments = await vidoEditUseCase.getVideoSegmentList(projectId); + const { segments, roles } = await vidoEditUseCase.getVideoSegmentList(projectId); setProjectId(projectId); setVideoSegments(segments); + setScriptRoles(roles); setIntervalIdHandler(projectId); } catch (error) { console.error("获取视频片段列表失败:", error); @@ -112,7 +116,7 @@ export const useShotService = (): UseShotService => { // 定义定时任务,每5秒执行一次 const newIntervalId = setInterval(async () => { try { - const segments = await vidoEditUseCase.getVideoSegmentList(projectId,()=>{ + const { segments } = await vidoEditUseCase.getVideoSegmentList(projectId,()=>{ if (intervalId) { clearInterval(intervalId); setIntervalId(null); @@ -474,6 +478,7 @@ export const useShotService = (): UseShotService => { // 响应式状态 loading, videoSegments, + scriptRoles, selectedSegment, matched_persons, // 操作方法 diff --git a/app/service/adapter/oldErrAdapter.ts b/app/service/adapter/oldErrAdapter.ts index d576d82..52f09a4 100644 --- a/app/service/adapter/oldErrAdapter.ts +++ b/app/service/adapter/oldErrAdapter.ts @@ -1,7 +1,7 @@ /**============因协同任务开发流程没有明确管理,导致的必要的适配=================**/ -import { VideoSegmentEntity } from "../domain/Entities"; +import { ScriptRoleEntity, VideoSegmentEntity } from "../domain/Entities"; import { LensType, ContentItem } from "../domain/valueObject"; @@ -73,6 +73,7 @@ export class VideoSegmentEntityAdapter { video_status: number|null; }>; }> = []; + project_characters: ScriptRoleEntity[] = []; /** * @description 解析shotContent,分离镜头描述和对话内容 @@ -208,7 +209,7 @@ export class VideoSegmentEntityAdapter { // 如果有对话内容,添加到镜头描述后面 if (lensItem.content && lensItem.content.length > 0) { const dialogueLines = lensItem.content.map(dialogue => - `${dialogue.roleName} [CH-01]: ${dialogue.content}` + `${dialogue.roleName} : ${dialogue.content}` ); fullContent += '\n' + dialogueLines.join('\n'); } @@ -312,7 +313,7 @@ export class VideoSegmentEntityAdapter { // 如果有对话内容,添加到镜头描述后面 if (lensType.content && lensType.content.length > 0) { const dialogueLines = lensType.content.map(dialogue => - `${dialogue.roleName} [CH-01]: ${dialogue.content}` + `${dialogue.roleName} : ${dialogue.content}` ); fullContent += '\n' + dialogueLines.join('\n'); } diff --git a/app/service/adapter/textToShot.ts b/app/service/adapter/textToShot.ts index b9c8111..cc466a3 100644 --- a/app/service/adapter/textToShot.ts +++ b/app/service/adapter/textToShot.ts @@ -1,12 +1,5 @@ import { ContentItem, LensType, SimpleCharacter, TagValueObject } from '../domain/valueObject'; - -// 定义角色属性接口 -interface CharacterAttributes { - name: string; - // gender: string; - // age: string; - avatar: string; -} +import { ScriptRoleEntity } from '../domain/Entities'; // 定义高亮属性接口 interface HighlightAttributes { @@ -23,7 +16,7 @@ interface TextNode { // 定义角色标记节点接口 interface CharacterTokenNode { type: 'characterToken'; - attrs: CharacterAttributes; + attrs: ScriptRoleEntity; } // 定义高亮节点接口 @@ -55,7 +48,7 @@ export class TextToShotAdapter { * @param roles 角色列表 * @returns ContentNode[] 节点数组 */ - public static parseText(text: string, roles: SimpleCharacter[]): ContentNode[] { + public static parseText(text: string, roles: ScriptRoleEntity[]): ContentNode[] { const nodes: ContentNode[] = []; let currentText = text; @@ -63,31 +56,32 @@ export class TextToShotAdapter { // 既要兼容 每个单词 首字母大写 其余小写、还要兼容 全部大写 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(' ') + match_name: role.name.split(' ').map(word => word.charAt(0).toUpperCase() + word.slice(1).toLowerCase()).join(' ') + ' [' + role.id + ']' })).concat([...roles].map(role => ({ ...role, - name: role.name.toUpperCase() + match_name: role.name.toUpperCase() + ' [' + role.id + ']' }))); + console.log('shots-匹配角色', text, sortedRoles); + while (currentText.length > 0) { let matchFound = false; // 尝试匹配角色 for (const role of sortedRoles) { - if (currentText.startsWith(role.name)) { + if (currentText.startsWith(role.match_name)) { // 如果当前文本以角色名开头 - if (currentText.length > role.name.length) { + if (currentText.length > role.match_name.length) { // 添加角色标记节点 nodes.push({ type: 'characterToken', attrs: { - name: role.name, - avatar: role.imageUrl + ...role } }); // 移除已处理的角色名 - currentText = currentText.slice(role.name.length); + currentText = currentText.slice(role.match_name.length); matchFound = true; break; } @@ -99,7 +93,7 @@ export class TextToShotAdapter { // 查找下一个可能的角色名位置 let nextRoleIndex = currentText.length; for (const role of sortedRoles) { - const index = currentText.indexOf(role.name); + const index = currentText.indexOf(role.match_name); if (index !== -1 && index < nextRoleIndex) { nextRoleIndex = index; } @@ -196,7 +190,7 @@ export class TextToShotAdapter { * @param lensType LensType 实例 * @returns Paragraph 格式的数据 */ - public static fromLensType(lensType: LensType, roles: SimpleCharacter[]): Shot { + public static fromLensType(lensType: LensType, roles: ScriptRoleEntity[]): Shot { const shotDescContent: Paragraph[] = []; const shotDialogsContent: Paragraph[] = []; @@ -213,16 +207,15 @@ export class TextToShotAdapter { lensType.content.forEach(item => { const dialogNodes = TextToShotAdapter.parseText(item.content, roles); - // 确保对话内容以角色标记开始 - const roleMatch = roles.find(role => role.name === item.roleName); + // 确保对话内容以角色标记开始 角色名都大写再匹配 + const roleMatch = roles.find(role => role.name.toUpperCase().includes(item.roleName.toUpperCase())); if (roleMatch) { const dialogContent: Paragraph = { type: 'paragraph', content: [{ type: 'characterToken', attrs: { - name: roleMatch.name, - avatar: roleMatch.imageUrl + ...roleMatch }}, ...dialogNodes ] @@ -263,7 +256,7 @@ export class TextToShotAdapter { currentScript += node.text; } if (node.type === 'characterToken') { - currentScript += node.attrs.name; + currentScript = currentScript + node.attrs.name + ' ' + node.attrs.id; } }); } @@ -282,10 +275,10 @@ export class TextToShotAdapter { if (node.type === 'characterToken') { // 记录说话角色的名称 if (!firstFindRole) { - dialogRoleName = node.attrs.name; + dialogRoleName = node.attrs.name + ' [' + node.attrs.id + ']'; firstFindRole = true; } else { - dialogContent += node.attrs.name; + dialogContent += node.attrs.name + ' [' + node.attrs.id + ']'; } } else if (node.type === 'text') { // 累积对话内容 @@ -329,7 +322,7 @@ export class TextToShotAdapter { } else if (node.type === 'text') { text += node.text; } else if (node.type === 'characterToken') { - text += node.attrs.name; + text += node.attrs.name + ' ' + node.attrs.id; } }); } diff --git a/app/service/domain/Entities.ts b/app/service/domain/Entities.ts index b7e9f06..93fa67d 100644 --- a/app/service/domain/Entities.ts +++ b/app/service/domain/Entities.ts @@ -43,6 +43,18 @@ export interface RoleEntity { isChangeRole: boolean; } +/** + * 剧本中角色实体接口 + */ +export interface ScriptRoleEntity { + /** 唯一标识 */ + readonly id: string; + /** 角色名称 */ + name: string; + /** 角色照片 */ + image_url: string; +} + /** * 场景实体接口 */ diff --git a/app/service/usecase/ShotEditUsecase.ts b/app/service/usecase/ShotEditUsecase.ts index f5e41da..1ed7458 100644 --- a/app/service/usecase/ShotEditUsecase.ts +++ b/app/service/usecase/ShotEditUsecase.ts @@ -1,6 +1,6 @@ import { VideoFlowProjectResponse } from "@/api/DTO/movieEdit"; import { task_item, VideoSegmentEntityAdapter } from "../adapter/oldErrAdapter"; -import { VideoSegmentEntity } from "../domain/Entities"; +import { ScriptRoleEntity, VideoSegmentEntity } from "../domain/Entities"; import { LensType } from "../domain/valueObject"; import { getShotList, @@ -24,7 +24,7 @@ export class VideoSegmentEditUseCase { * @param projectId 项目ID * @returns Promise 视频片段列表 */ - async getVideoSegmentList(projectId: string,callback?:()=>void): Promise { + async getVideoSegmentList(projectId: string,callback?:()=>void): Promise<{ segments: VideoSegmentEntity[], roles: ScriptRoleEntity[] }> { try { this.loading = true; @@ -35,7 +35,6 @@ export class VideoSegmentEditUseCase { } if(response.data.task_status=="COMPLETED"){ callback?.(); - } const Segments = VideoSegmentEntityAdapter.toVideoSegmentEntity(response.data) || []; const detail = await detailScriptEpisodeNew({ project_id: projectId }); @@ -43,7 +42,10 @@ export class VideoSegmentEditUseCase { throw new Error(detail.message || "获取视频片段列表失败"); } // 匹配视频片段ID - return this.matchVideoSegmentsWithIds(Segments, detail.data); + return { + segments: this.matchVideoSegmentsWithIds(Segments, detail.data), + roles: response.data.project_characters + }; } catch (error) { console.error("获取视频片段列表失败:", error); throw error; @@ -265,7 +267,7 @@ export class VideoSegmentEditUseCase { this.loading = true; // 获取当前项目的视频片段列表 - const segments = await this.getVideoSegmentList(projectId); + const { segments } = await this.getVideoSegmentList(projectId); // 过滤出除当前选中片段外的所有片段 const otherSegments = segments.filter(segment => segment.id !== currentSegmentId); diff --git a/components/pages/work-flow/use-edit-data.tsx b/components/pages/work-flow/use-edit-data.tsx index fe49b45..5217cf5 100644 --- a/components/pages/work-flow/use-edit-data.tsx +++ b/components/pages/work-flow/use-edit-data.tsx @@ -24,6 +24,7 @@ export const useEditData = (tabType: string, originalText?: string) => { const { videoSegments, + scriptRoles, getVideoSegmentList, setSelectedSegment, regenerateVideoSegment, @@ -104,9 +105,10 @@ export const useEditData = (tabType: string, originalText?: string) => { }, [scriptBlocksMemo]); useEffect(() => { - console.log('useEditData-----videoSegments', videoSegments); + console.log('useEditData-----videoSegments', videoSegments, scriptRoles); setShotData(videoSegments); - }, [videoSegments]); + setRoleData(scriptRoles); + }, [videoSegments, scriptRoles]); useEffect(() => { setRoleData(roleList); @@ -121,6 +123,7 @@ export const useEditData = (tabType: string, originalText?: string) => { applyScript, // shot shotData, + scriptRoles, setSelectedSegment, regenerateVideoSegment, filterRole, diff --git a/components/ui/character-tab-content.tsx b/components/ui/character-tab-content.tsx index 1507d25..9a691df 100644 --- a/components/ui/character-tab-content.tsx +++ b/components/ui/character-tab-content.tsx @@ -165,8 +165,8 @@ CharacterTabContentProps // 处理替换确认逻辑 console.log('Selected shots:', selectedShots); console.log('Add to library:', addToLibrary); - await applyRoleToSelectedShots(selectedRole || {} as RoleEntity); setIsReplacePanelOpen(false); + await applyRoleToSelectedShots(selectedRole || {} as RoleEntity); if(addToLibrary){ await saveRoleToLibrary(); } diff --git a/components/ui/edit-modal.tsx b/components/ui/edit-modal.tsx index 33ee43f..b8c6c4f 100644 --- a/components/ui/edit-modal.tsx +++ b/components/ui/edit-modal.tsx @@ -56,7 +56,8 @@ export function EditModal({ const scriptTabContentRef = useRef(null); const characterTabContentRef = useRef(null); // 添加一个状态来标记是否是从切换tab触发的提醒 -const [pendingSwitchTabId, setPendingSwitchTabId] = useState(null); + const [pendingSwitchTabId, setPendingSwitchTabId] = useState(null); + const [disabledBtn, setDisabledBtn] = useState(false); useEffect(() => { setCurrentIndex(currentSketchIndex); @@ -129,6 +130,7 @@ const [pendingSwitchTabId, setPendingSwitchTabId] = useState(null } const handleConfirmGotoFallback = () => { + setDisabledBtn(true); console.log('handleConfirmGotoFallback'); SaveEditUseCase.saveData(); if (activeTab === '0') { @@ -140,6 +142,7 @@ const [pendingSwitchTabId, setPendingSwitchTabId] = useState(null setIsRemindFallbackOpen(false); // 关闭弹窗 onClose(); + setDisabledBtn(false); } const handleCloseRemindFallbackPanel = () => { if (pendingSwitchTabId) { @@ -316,18 +319,20 @@ const [pendingSwitchTabId, setPendingSwitchTabId] = useState(null
Reset {handleSave()}} + disabled={disabledBtn} > Apply diff --git a/components/ui/replace-panel.tsx b/components/ui/replace-panel.tsx index 2ebff54..c27dce2 100644 --- a/components/ui/replace-panel.tsx +++ b/components/ui/replace-panel.tsx @@ -2,6 +2,7 @@ import React, { useState, useRef } from 'react'; import { motion } from 'framer-motion'; import { Check, X, CircleAlert, ArrowLeft, ArrowRight } from 'lucide-react'; import { cn } from '@/public/lib/utils'; +import { throttle } from 'lodash'; interface ReplacePanelProps { isLoading: boolean; @@ -84,8 +85,22 @@ export function ReplacePanel({ } }; + const throttledConfirm = React.useCallback( + throttle(() => { + onConfirm(selectedShots, addToLibrary); + }, 1000, { trailing: false }), // 1秒内只能触发一次,不要执行最后一次调用 + [selectedShots, addToLibrary, onConfirm] + ); + + // 在组件卸载时取消节流函数中的定时器 + React.useEffect(() => { + return () => { + throttledConfirm.cancel(); + }; + }, [throttledConfirm]); + const handleConfirm = () => { - onConfirm(selectedShots, addToLibrary); + throttledConfirm(); }; const handleLeftArrowClick = () => { diff --git a/components/ui/shot-editor/CharacterToken.tsx b/components/ui/shot-editor/CharacterToken.tsx index cb2a703..5cbcd90 100644 --- a/components/ui/shot-editor/CharacterToken.tsx +++ b/components/ui/shot-editor/CharacterToken.tsx @@ -3,28 +3,16 @@ import { ReactNodeViewRenderer, NodeViewWrapper, ReactNodeViewProps } from '@tip import { motion, AnimatePresence } from 'framer-motion' import { useState, useRef, useEffect } from 'react' import { Check, CircleUserRound } from 'lucide-react' - -interface CharacterAttributes { - id: string | null; - name: string; - avatar: string; - gender: string; - age: string; -} - -interface Role { - name: string; - url: string; -} +import { ScriptRoleEntity } from '@/app/service/domain/Entities'; interface CharacterTokenOptions { - roles?: Role[]; + roles?: ScriptRoleEntity[]; } export function CharacterToken(props: ReactNodeViewProps) { const [showRoleList, setShowRoleList] = useState(false); const [listPosition, setListPosition] = useState({ top: 0, left: 0 }); - const { name, avatar } = props.node.attrs as CharacterAttributes; + const { name } = props.node.attrs as ScriptRoleEntity; const extension = props.extension as Node; const roles = extension.options.roles || []; @@ -76,7 +64,7 @@ export function CharacterToken(props: ReactNodeViewProps) { } }, [showRoleList]); - const handleRoleSelect = (role: Role) => { + const handleRoleSelect = (role: ScriptRoleEntity) => { const { editor } = props; const pos = props.getPos(); @@ -84,8 +72,9 @@ export function CharacterToken(props: ReactNodeViewProps) { const { tr } = editor.state; tr.setNodeMarkup(pos, undefined, { ...props.node.attrs, + id: role.id, name: role.name, - avatar: role.url, + image_url: role.image_url, }); editor.view.dispatch(tr); } @@ -137,7 +126,7 @@ export function CharacterToken(props: ReactNodeViewProps) { >
{role.name} handleRoleSelect({ name: 'Voiceover', url: '' })} + onClick={() => handleRoleSelect({ name: 'Voiceover', image_url: '', id: 'voiceover' } as ScriptRoleEntity)} >
{ const { loading, shotData, + scriptRoles, setSelectedSegment, regenerateVideoSegment, filterRole, @@ -50,8 +51,8 @@ export const ShotTabContent = (props: ShotTabContentProps) => { const [pendingRegeneration, setPendingRegeneration] = useState(false); useEffect(() => { - console.log('shotTabContent-----roles', roles); - }, [roles]); + console.log('shotTabContent-----scriptRoles', scriptRoles); + }, [scriptRoles]); useEffect(() => { if (pendingRegeneration) { @@ -395,7 +396,7 @@ export const ShotTabContent = (props: ShotTabContentProps) => {