diff --git a/api/video_flow.ts b/api/video_flow.ts index 218f41e..f24c9ac 100644 --- a/api/video_flow.ts +++ b/api/video_flow.ts @@ -987,7 +987,7 @@ export const batchUpdateVideoSegments = async (request: { /** 新的视频地址列表 */ video_urls: string[]; /** 新的状态 0:视频加载中 1:任务已完成 2:任务失败 */ - status: number; + status: number | null; /** 优化后的描述文本 */ optimized_description?: string; /** 关键词列表 */ @@ -1033,7 +1033,8 @@ export const getCharacterShots = async (request: { video_id: string; video_status: number|null; }[]; - + /** 视频状态 */ + video_status: number|null; }>; /** 总数量 */ total_count: number; diff --git a/app/service/Interaction/RoleShotService.ts b/app/service/Interaction/RoleShotService.ts index 2726b1c..4c75ed1 100644 --- a/app/service/Interaction/RoleShotService.ts +++ b/app/service/Interaction/RoleShotService.ts @@ -98,7 +98,7 @@ export const useRoleShotServiceHook = (projectId: string,selectRole?:RoleEntity, name: `视频片段_${scene.video_id}`, sketchUrl: "", videoUrl: scene.video_urls,// 保持为string[]类型 - status:scene.video_urls.length>0?1:0, // 默认为已完成状态 + status: scene.video_status !== null? scene.video_status : scene.video_urls.length>0?1:0, // 默认为已完成状态 lens: [], selected: false, applied: true // 由于是通过角色查询到的,所以都是已应用的 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..d7139f5 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; +} + /** * 场景实体接口 */ @@ -76,7 +88,7 @@ export interface VideoSegmentEntity { video_status: number | null; }[]; /**视频片段状态 0:视频加载中 1:任务已完成 2:任务失败 */ - status: number; + status: number|null; /**镜头项 */ lens: LensType[]; } 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..7487fd3 100644 --- a/components/ui/replace-panel.tsx +++ b/components/ui/replace-panel.tsx @@ -1,7 +1,8 @@ -import React, { useState, useRef } from 'react'; +import React, { useState, useRef, useEffect } from 'react'; import { motion } from 'framer-motion'; -import { Check, X, CircleAlert, ArrowLeft, ArrowRight } from 'lucide-react'; +import { Check, X, CircleAlert, ArrowLeft, ArrowRight, Loader2 } from 'lucide-react'; import { cn } from '@/public/lib/utils'; +import { throttle } from 'lodash'; interface ReplacePanelProps { isLoading: boolean; @@ -29,33 +30,12 @@ export function ReplacePanel({ ); const [addToLibrary, setAddToLibrary] = useState(false); const [hoveredVideoId, setHoveredVideoId] = useState(null); - const [isAtStart, setIsAtStart] = useState(true); - const [isAtEnd, setIsAtEnd] = useState(false); const videoRefs = useRef<{ [key: string]: HTMLVideoElement }>({}); const shotsRef = useRef(null); - // 检查滚动位置 - const checkScrollPosition = () => { - if (!shotsRef.current) return; - - const { scrollLeft, scrollWidth, clientWidth } = shotsRef.current; - setIsAtStart(scrollLeft <= 0); - setIsAtEnd(Math.ceil(scrollLeft + clientWidth) >= scrollWidth); - }; - - // 添加滚动事件监听 - React.useEffect(() => { - const shotsElement = shotsRef.current; - if (!shotsElement) return; - - shotsElement.addEventListener('scroll', checkScrollPosition); - // 初始检查 - checkScrollPosition(); - - return () => { - shotsElement.removeEventListener('scroll', checkScrollPosition); - }; - }, []); + useEffect(() => { + console.log('replace-panel-shots', shots); + }, [shots]); const handleShotToggle = (shotId: string) => { // setSelectedShots(prev => @@ -65,10 +45,6 @@ export function ReplacePanel({ // ); }; - const handleSelectAllShots = (checked: boolean) => { - setSelectedShots(checked ? shots.map(shot => shot.id) : []); - }; - const handleMouseEnter = (shotId: string) => { setHoveredVideoId(shotId); if (videoRefs.current[shotId]) { @@ -84,8 +60,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 = () => { @@ -158,29 +148,34 @@ export function ReplacePanel({ whileHover={{ scale: 1.02 }} whileTap={{ scale: 0.98 }} > - {shot.videoUrl && shot.videoUrl.length > 0 && ( -