'use client'; import React, { useRef, useEffect, useState, forwardRef } from 'react'; import { motion, AnimatePresence } from 'framer-motion'; import { RefreshCw, User, Loader2, X, Download, Video, CircleX } from 'lucide-react'; import { cn } from '@/public/lib/utils'; import { PersonDetection, PersonDetectionScene } from './person-detection'; import { ShotsEditor } from './shot-editor/ShotsEditor'; import { CharacterLibrarySelector } from './character-library-selector'; import FloatingGlassPanel from './FloatingGlassPanel'; import { ReplaceCharacterPanel } from './replace-character-panel'; import HorizontalScroller from './HorizontalScroller'; import { useEditData } from '@/components/pages/work-flow/use-edit-data'; import { RoleEntity, VideoSegmentEntity } from '@/app/service/domain/Entities'; import { ShotVideo } from '@/api/DTO/movieEdit'; import { downloadVideo } from '@/utils/tools'; interface ShotTabContentProps { currentSketchIndex: number; originalVideos: ShotVideo[]; onApply: () => void; onClose: () => void; setActiveTab: (tabId: string) => void; } export const ShotTabContent = forwardRef< { switchBefore: (tabId: string) => boolean, saveOrCloseBefore: (type: 'apply' | 'close') => void }, ShotTabContentProps >((props, ref) => { const { currentSketchIndex = 0, onApply, onClose, originalVideos, setActiveTab } = props; const { loading, shotData, selectedSegment, scriptRoles, setSelectedSegment, regenerateVideoSegment, filterRole, fetchUserRoleLibrary, userRoleLibrary, fetchRoleShots, shotSelectionList, applyRoleToSelectedShots, calculateRecognitionBoxes, setSelectedRole } = useEditData('shot'); const [detections, setDetections] = useState([]); const [scanState, setScanState] = useState<'idle' | 'scanning' | 'detected' | 'failed' | 'timeout'>('idle'); const [isScanFailed, setIsScanFailed] = useState(false); const [isLoadingLibrary, setIsLoadingLibrary] = useState(false); const [isReplaceLibraryOpen, setIsReplaceLibraryOpen] = useState(false); const [isReplacePanelOpen, setIsReplacePanelOpen] = useState(false); const [selectedCharacter, setSelectedCharacter] = useState(null); const [selectedLibaryRole, setSelectedLibaryRole] = useState(null); const [isLoadingShots, setIsLoadingShots] = useState(false); const shotsEditorRef = useRef(null); const [isRegenerate, setIsRegenerate] = useState(false); const [pendingRegeneration, setPendingRegeneration] = useState(false); const [isInitialized, setIsInitialized] = useState(true); const [triggerType, setTriggerType] = useState<'tab' | 'apply' | 'close'>('tab'); const [nextToTabId, setNextToTabId] = useState(''); const [isRemindApplyUpdate, setIsRemindApplyUpdate] = useState(false); const [updateData, setUpdateData] = useState([]); const [isLoadingDownloadBtn, setIsLoadingDownloadBtn] = useState(false); useEffect(() => { console.log('shotTabContent-----selectedSegment', selectedSegment); }, [selectedSegment]); useEffect(() => { console.log('-==========shotTabContent===========-', shotData, currentSketchIndex, originalVideos); // 只在初始化且有角色数据时执行 if (isInitialized && shotData.length > 0) { setIsInitialized(false); const defaultSelectIndex = currentSketchIndex >= shotData.length ? 0 : currentSketchIndex; setSelectedSegment(shotData[defaultSelectIndex]); } }, [shotData, isInitialized]); // 暴露方法给父组件 React.useImperativeHandle(ref, () => ({ switchBefore: (tabId: string) => { setNextToTabId(tabId); // 判断 是否修改数据 const isChange = handleGetUpdateData().length > 0; console.log('switchBefore', isChange); if (isChange) { setTriggerType('tab'); setIsRemindApplyUpdate(true); } return isChange; }, saveOrCloseBefore: (type: 'apply' | 'close') => { console.log('saveOrCloseBefore'); // 判断 是否修改数据 const isChange = handleGetUpdateData().length > 0; if (isChange) { setTriggerType(type); setIsRemindApplyUpdate(true); } else { onClose(); } } })); useEffect(() => { if (pendingRegeneration) { console.log('pendingRegeneration', pendingRegeneration, selectedSegment?.lens); regenerateVideoSegment().then(() => { setPendingRegeneration(false); setIsRegenerate(false); }); } }, [pendingRegeneration]); // 监听当前选中segment变化 useEffect(() => { console.log('shotTabContent-----shotData', shotData); if (shotData.length > 0 && !selectedSegment) { // 清空检测状态 和 检测结果 setScanState('idle'); setDetections([]); } }, [shotData, selectedSegment]); // 获取修改的数据 const handleGetUpdateData = () => { console.log('handleGetUpdateData', shotData, originalVideos); const updateData: VideoSegmentEntity[] = []; shotData.forEach((shot, index) => { const a = shot.videoUrl.map((url) => url.video_url).join(','); const b = originalVideos[index].urls?.join(',') || ''; if (a !== b) { updateData.push({ ...shot, name: 'Segment ' + (index + 1) }); } }); console.log('updateData', updateData); setUpdateData(updateData); return updateData; } // 处理扫描开始 const handleScan = async () => { if (scanState === 'detected') { // 如果已经有检测结果,点击按钮退出检测状态 setScanState('idle'); setDetections([]); // 清除检测结果 return; } setScanState('scanning'); const containerElement = document.getElementById('person-detection-video') as HTMLVideoElement; try { const roleRecognitionResponse = await filterRole(containerElement); console.log('roleRecognitionResponse', roleRecognitionResponse); if (roleRecognitionResponse && roleRecognitionResponse.recognition_result.code === 200) { const recognitionBoxes = calculateRecognitionBoxes(containerElement, roleRecognitionResponse.recognition_result.data); console.log('recognitionBoxes', recognitionBoxes); setDetections(recognitionBoxes.map((person: any) => ({ id: person.person_id, name: person.person_id, description: roleRecognitionResponse.characters_used.find((character: any) => character.character_name === person.person_id)?.character_description || '', imageUrl: roleRecognitionResponse.characters_used.find((character: any) => character.character_name === person.person_id)?.avatar || '', position: { top: person.top, left: person.left, width: person.width, height: person.height } }))); } else { setIsScanFailed(true); } setScanState('detected'); } catch (error) { setIsScanFailed(true); } }; // 处理扫描超时 const handleScanTimeout = () => { setIsScanFailed(true); setScanState('timeout'); setDetections([]); }; // 处理退出扫描 const handleScanExit = () => { setScanState('idle'); setDetections([]); }; // 处理检测到结果 const handleDetectionsChange = (newDetections: PersonDetection[]) => { // console.log('handleDetectionsChange', newDetections); // if (newDetections.length > 0 && scanState === 'scanning') { // setScanState('detected'); // } }; // 处理人物点击 打开角色库 const handlePersonClick = async (person: PersonDetection) => { console.log('person', person); const role: RoleEntity = { id: person.id, name: person.name, generateText: person.description, tags: [], // 由于 person 对象中没有标签信息,我们设置为空数组 imageUrl: person.imageUrl, fromDraft: false, // 默认不是来自草稿箱 isChangeRole: true, // 默认没有发生角色形象的变更 }; console.log('role', role); setSelectedRole(role); setSelectedCharacter(person); setIsLoadingLibrary(true); setIsReplaceLibraryOpen(true); await fetchUserRoleLibrary(role.generateText); setIsLoadingLibrary(false); }; // 从角色库中选择角色 const handleSelectCharacter = (index: number) => { console.log('index', index); setSelectedLibaryRole(userRoleLibrary[index]); setIsReplaceLibraryOpen(false); handleStartReplaceCharacter(); }; const handleStartReplaceCharacter = async () => { setIsLoadingShots(true); setIsReplacePanelOpen(true); // 获取当前角色对应的视频片段 await fetchRoleShots(selectedCharacter?.name || ''); // 打开替换角色面板 setIsLoadingShots(false); }; // 确认替换角色 const handleConfirmReplace = () => { applyRoleToSelectedShots(selectedLibaryRole); setIsReplacePanelOpen(false); onApply(); }; // 应用修改 const handleApplyUpdate = () => { console.log('apply update'); onApply(); } // 忽略修改 const handleIgnoreUpdate = () => { console.log('ignore update'); if (triggerType === 'apply') { onClose(); } else if (triggerType === 'tab') { setActiveTab(nextToTabId); } } // 点击按钮重新生成 const handleRegenerate = async () => { console.log('regenerate'); setIsRegenerate(true); const shotInfo = shotsEditorRef.current.getShotInfo(); console.log('shotInfo', shotInfo); if (selectedSegment) { setSelectedSegment({ ...selectedSegment, lens: shotInfo }); } setTimeout(() => { setPendingRegeneration(true); }, 1000); }; // 新增分镜 const handleAddShot = () => { console.log('add shot'); shotsEditorRef.current.addShot(); }; // 切换选择分镜 const handleSelectShot = (index: number) => { // 通过 video_id 找到对应的分镜 const selectedVideo = originalVideos[index]; const targetSegment = shotData.find(shot => shot.id === selectedVideo.video_id ); if (targetSegment) { setSelectedSegment(targetSegment); } }; // 如果没有数据,显示空状态 if (originalVideos.length === 0) { return (
); } return (
{/* 上部分 */} {/* 分镜缩略图行 */}
selectedSegment?.id === shot.video_id)} onItemClick={(i: number) => handleSelectShot(i)} > {originalVideos.map((shot, index) => ( {/** 进行中 显示加载中 */} {shot.video_status === 0 && (
)} {/** 失败 显示失败 */} {shot.video_status === 2 && (
)} {/** 成功 显示视频 */} {shot.video_status === 1 && shot.urls && shot.urls.length > 0 && (
))}
{/* 视频描述行 - 单行滚动 */}
selectedSegment?.id === shot.video_id)} onItemClick={(i: number) => handleSelectShot(i)} > {originalVideos.map((shot, index) => { const isActive = selectedSegment?.id === shot.video_id; return (
Segment {index + 1} {index < originalVideos.length - 1 && ( | )}
); })}
{/* 渐变遮罩 */}
{/* 下部分 */} {loading ? (

Loading...

) : selectedSegment && ( {/* 视频预览和操作 */}
{/* 选中的视频预览 */} <> {(selectedSegment?.status === 0) && (
Loading...
)} {selectedSegment?.status === 1 && selectedSegment?.videoUrl.length && ( {/* 人物替换按钮 */} { window.msg.error('No permission!'); return; handleScan() }} className={`p-2 backdrop-blur-sm transition-colors z-10 rounded-full ${scanState === 'detected' ? 'bg-cyan-500/50 hover:bg-cyan-500/70 text-white' : 'bg-black/50 hover:bg-black/70 text-white' }`} whileHover={{ scale: 1.05 }} whileTap={{ scale: 0.95 }} > {scanState === 'scanning' ? ( ) : scanState === 'detected' ? ( ) : ( )} {/** 下载视频按钮 */} { setIsLoadingDownloadBtn(true); downloadVideo(selectedSegment?.videoUrl[0].video_url); setIsLoadingDownloadBtn(false); }} className="p-2 backdrop-blur-sm transition-colors z-10 rounded-full bg-black/50 hover:bg-black/70 text-white disabled:opacity-50 disabled:cursor-not-allowed" disabled={isLoadingDownloadBtn} > {isLoadingDownloadBtn ? ( ) : ( )} )} {(selectedSegment?.status === 2) && (
Failed, click to regenerate
)}
{/* 基础配置 */}
{/* 重新生成按钮、新增分镜按钮 */}
{/* handleAddShot()} className="flex items-center justify-center gap-2 px-4 py-3 bg-pink-500/10 hover:bg-pink-500/20 text-pink-500 rounded-lg transition-colors" whileHover={{ scale: 1.02 }} whileTap={{ scale: 0.98 }} > Add Shot */} { handleRegenerate(); }} className="flex items-center justify-center gap-2 px-4 py-3 bg-blue-500/10 hover:bg-blue-500/20 text-blue-500 rounded-lg transition-colors disabled:opacity-50 disabled:cursor-not-allowed" whileHover={{ scale: 1.02 }} whileTap={{ scale: 0.98 }} disabled={isRegenerate} > {isRegenerate ? 'Regenerating...' : 'Regenerate'}
)} setIsReplacePanelOpen(false)} > setIsReplacePanelOpen(false)} onConfirm={handleConfirmReplace} /> setIsRemindApplyUpdate(false)} >
{/* 提示文字 有分镜被修改 是否需要应用 */}
Three are some segments have been modified, do you need to apply?
{/* 已修改分镜列表 */} {updateData.map((shot, index) => ( {(shot.status === 0 || shot.videoUrl.length === 0) && (
)} {shot.status === 1 && shot.videoUrl[0] && (
))}
{/* 按钮 应用 忽略 */}
handleApplyUpdate()} className='px-4 py-2 bg-blue-500/10 hover:bg-blue-500/20 text-blue-500 rounded-lg transition-colors' > Apply handleIgnoreUpdate()} className='px-4 py-2 bg-gray-500/10 hover:bg-gray-500/20 text-gray-500 rounded-lg transition-colors' > Ignore
); }); ShotTabContent.displayName = 'ShotTabContent';