- [x] 显示 原分镜,剪辑后

- [x] 支持批量下载分镜视频
Chatbox查找消息是否有任务 还在执行中
This commit is contained in:
北枳 2025-09-05 17:24:16 +08:00
parent 254b226c6e
commit 9365896bb6
6 changed files with 80 additions and 18 deletions

View File

@ -267,6 +267,15 @@ export function useMessages({ config, onMessagesUpdate }: UseMessagesProps): [Me
} }
}, [updateMessages]); }, [updateMessages]);
// 监听 消息列表中是否存在 pending 状态的消息,有的话 将 loading 置为 true
useEffect(() => {
if (displayMessages.some(msg => msg.status === 'pending')) {
setIsLoading(true);
} else {
setIsLoading(false);
}
}, [displayMessages]);
return [ return [
{ {
messages: displayMessages, messages: displayMessages,

View File

@ -216,7 +216,7 @@ const WorkFlow = React.memo(function WorkFlow() {
{isLoading ? ( {isLoading ? (
<Skeleton className="w-full aspect-video rounded-lg" /> <Skeleton className="w-full aspect-video rounded-lg" />
) : ( ) : (
<div className={`relative heroVideo-FIzuK1 ${['final_video', 'script'].includes(taskObject.currentStage) ? 'h-[calc(100vh-6rem)] w-[calc((100vh-6rem)/9*16)]' : 'h-[calc(100vh-6rem-200px)] w-[calc((100vh-6rem-200px)/9*16)]'}`} style={{ aspectRatio: "16 / 9" }} key={taskObject.currentStage+'_'+currentSketchIndex}> <div className={`relative heroVideo-FIzuK1 ${['script'].includes(taskObject.currentStage) ? 'h-[calc(100vh-6rem)] w-[calc((100vh-6rem)/9*16)]' : 'h-[calc(100vh-6rem-200px)] w-[calc((100vh-6rem-200px)/9*16)]'}`} style={{ aspectRatio: "16 / 9" }} key={taskObject.currentStage+'_'+currentSketchIndex}>
<MediaViewer <MediaViewer
taskObject={taskObject} taskObject={taskObject}
scriptData={scriptData} scriptData={scriptData}
@ -242,7 +242,7 @@ const WorkFlow = React.memo(function WorkFlow() {
</div> </div>
)} )}
</div> </div>
{taskObject.currentStage !== 'final_video' && taskObject.currentStage !== 'script' && ( {taskObject.currentStage !== 'script' && (
<div className="h-[123px] w-[calc((100vh-6rem-200px)/9*16)]"> <div className="h-[123px] w-[calc((100vh-6rem-200px)/9*16)]">
<ThumbnailGrid <ThumbnailGrid
isDisabledFocus={isEditModalOpen || isPauseWorkFlow || isFocusChatInput} isDisabledFocus={isEditModalOpen || isPauseWorkFlow || isFocusChatInput}

View File

@ -2,7 +2,7 @@
import React, { useRef, useEffect, useState, SetStateAction, useMemo } from 'react'; import React, { useRef, useEffect, useState, SetStateAction, useMemo } from 'react';
import { motion, AnimatePresence } from 'framer-motion'; import { motion, AnimatePresence } from 'framer-motion';
import { Edit3, Play, Pause, Volume2, VolumeX, Maximize, Minimize, Loader2, X, Scissors, RotateCcw, MessageCircleMore, Download } from 'lucide-react'; import { Edit3, Play, Pause, Volume2, VolumeX, Maximize, Minimize, Loader2, X, Scissors, RotateCcw, MessageCircleMore, Download, ArrowDownWideNarrow } from 'lucide-react';
import { ProgressiveReveal, presets } from '@/components/ui/progressive-reveal'; import { ProgressiveReveal, presets } from '@/components/ui/progressive-reveal';
import { GlassIconButton } from '@/components/ui/glass-icon-button'; import { GlassIconButton } from '@/components/ui/glass-icon-button';
import { ScriptRenderer } from '@/components/script-renderer/ScriptRenderer'; import { ScriptRenderer } from '@/components/script-renderer/ScriptRenderer';
@ -10,7 +10,7 @@ import { mockScriptData } from '@/components/script-renderer/mock';
import { Skeleton } from '@/components/ui/skeleton'; import { Skeleton } from '@/components/ui/skeleton';
import { TaskObject } from '@/api/DTO/movieEdit'; import { TaskObject } from '@/api/DTO/movieEdit';
import { Button, Tooltip } from 'antd'; import { Button, Tooltip } from 'antd';
import { downloadVideo } from '@/utils/tools'; import { downloadVideo, downloadAllVideos } from '@/utils/tools';
interface MediaViewerProps { interface MediaViewerProps {
taskObject: TaskObject; taskObject: TaskObject;
@ -65,6 +65,7 @@ export const MediaViewer = React.memo(function MediaViewer({
const [userHasInteracted, setUserHasInteracted] = useState(false); const [userHasInteracted, setUserHasInteracted] = useState(false);
const [toosBtnRight, setToodsBtnRight] = useState('1rem'); const [toosBtnRight, setToodsBtnRight] = useState('1rem');
const [isLoadingDownloadBtn, setIsLoadingDownloadBtn] = useState(false); const [isLoadingDownloadBtn, setIsLoadingDownloadBtn] = useState(false);
const [isLoadingDownloadAllVideosBtn, setIsLoadingDownloadAllVideosBtn] = useState(false);
useEffect(() => { useEffect(() => {
if (isSmartChatBoxOpen) { if (isSmartChatBoxOpen) {
@ -364,6 +365,14 @@ export const MediaViewer = React.memo(function MediaViewer({
onClick={() => handleEditClick('3', 'final')} onClick={() => handleEditClick('3', 'final')}
/> />
</Tooltip> </Tooltip>
{/* 下载所有视频按钮 */}
<Tooltip placement="top" title="Download all videos">
<GlassIconButton icon={ArrowDownWideNarrow} size='sm' loading={isLoadingDownloadAllVideosBtn} onClick={ async () => {
setIsLoadingDownloadAllVideosBtn(true);
await downloadAllVideos(taskObject.videos.data.flatMap((video: any) => video.urls));
setIsLoadingDownloadAllVideosBtn(false);
}} />
</Tooltip>
{/* 下载按钮 */} {/* 下载按钮 */}
<Tooltip placement="top" title="Download video"> <Tooltip placement="top" title="Download video">
<GlassIconButton icon={Download} loading={isLoadingDownloadBtn} size='sm' onClick={async () => { <GlassIconButton icon={Download} loading={isLoadingDownloadBtn} size='sm' onClick={async () => {
@ -493,6 +502,14 @@ export const MediaViewer = React.memo(function MediaViewer({
} }
}} /> }} />
</Tooltip> </Tooltip>
{/* 下载所有视频按钮 */}
<Tooltip placement="top" title="Download all videos">
<GlassIconButton icon={ArrowDownWideNarrow} size='sm' loading={isLoadingDownloadAllVideosBtn} onClick={ async () => {
setIsLoadingDownloadAllVideosBtn(true);
await downloadAllVideos(taskObject.videos.data.flatMap((video: any) => video.urls));
setIsLoadingDownloadAllVideosBtn(false);
}} />
</Tooltip>
{/* 下载按钮 */} {/* 下载按钮 */}
<Tooltip placement="top" title="Download video"> <Tooltip placement="top" title="Download video">
<GlassIconButton icon={Download} loading={isLoadingDownloadBtn} size='sm' onClick={async () => { <GlassIconButton icon={Download} loading={isLoadingDownloadBtn} size='sm' onClick={async () => {

View File

@ -15,6 +15,9 @@ interface ThumbnailGridProps {
onRetryVideo: (video_id: string) => void; onRetryVideo: (video_id: string) => void;
} }
/**
* hover时播放视频预览
*/
export function ThumbnailGrid({ export function ThumbnailGrid({
isDisabledFocus, isDisabledFocus,
taskObject, taskObject,
@ -22,6 +25,17 @@ export function ThumbnailGrid({
onSketchSelect, onSketchSelect,
onRetryVideo onRetryVideo
}: ThumbnailGridProps) { }: ThumbnailGridProps) {
const [hoveredIndex, setHoveredIndex] = useState<number | null>(null);
/** 处理鼠标进入缩略图事件 */
const handleMouseEnter = useCallback((index: number) => {
setHoveredIndex(index);
}, []);
/** 处理鼠标离开缩略图事件 */
const handleMouseLeave = useCallback((index: number) => {
setHoveredIndex(null);
}, []);
const thumbnailsRef = useRef<HTMLDivElement>(null); const thumbnailsRef = useRef<HTMLDivElement>(null);
const [isDragging, setIsDragging] = useState(false); const [isDragging, setIsDragging] = useState(false);
const [startX, setStartX] = useState(0); const [startX, setStartX] = useState(0);
@ -159,13 +173,8 @@ export function ThumbnailGrid({
console.log('taskObject.currentStage_thumbnail-grid', taskObject.currentStage); console.log('taskObject.currentStage_thumbnail-grid', taskObject.currentStage);
}, [taskObject.currentStage]); }, [taskObject.currentStage]);
// 粗剪/精剪最终成片阶段不显示缩略图
if (taskObject.currentStage === 'final_video') {
return null;
}
// 渲染视频阶段的缩略图 // 渲染视频阶段的缩略图
const renderVideoThumbnails = () => ( const renderVideoThumbnails = (disabled: boolean = false) => (
taskObject.videos.data.map((video, index) => { taskObject.videos.data.map((video, index) => {
const urls: string = video.urls ? video.urls.join(',') : ''; const urls: string = video.urls ? video.urls.join(',') : '';
@ -173,8 +182,8 @@ export function ThumbnailGrid({
<div <div
key={`video-${urls}-${index}`} key={`video-${urls}-${index}`}
className={`relative aspect-video rounded-lg overflow-hidden className={`relative aspect-video rounded-lg overflow-hidden
${currentSketchIndex === index ? 'ring-2 ring-blue-500 z-10' : 'hover:ring-2 hover:ring-blue-500/50'}`} ${(currentSketchIndex === index && !disabled) ? 'ring-2 ring-blue-500 z-10' : 'hover:ring-2 hover:ring-blue-500/50'}`}
onClick={() => !isDragging && onSketchSelect(index)} onClick={() => !isDragging && !disabled && onSketchSelect(index)}
> >
{/* 视频层 */} {/* 视频层 */}
@ -202,11 +211,28 @@ export function ThumbnailGrid({
// loop // loop
// muted // muted
// /> // />
<img <div
className="w-full h-full object-cover" className="w-full h-full relative"
src={`${taskObject.videos.data[index].urls[0]}?x-oss-process=video/snapshot,t_1000,f_jpg`} onMouseEnter={() => handleMouseEnter(index)}
draggable="false" onMouseLeave={() => handleMouseLeave(index)}
/> >
<img
className="w-full h-full object-cover"
src={`${taskObject.videos.data[index].urls[0]}?x-oss-process=video/snapshot,t_1000,f_jpg`}
draggable="false"
alt="video thumbnail"
/>
{hoveredIndex === index && (
<video
className="absolute inset-0 w-full h-full object-cover"
src={taskObject.videos.data[index].urls[0]}
autoPlay
muted
playsInline
loop
/>
)}
</div>
) : ( ) : (
<div className="w-full h-full transform hover:scale-105 transition-transform duration-500"> <div className="w-full h-full transform hover:scale-105 transition-transform duration-500">
@ -316,6 +342,7 @@ export function ThumbnailGrid({
> >
{taskObject.currentStage === 'video' && renderVideoThumbnails()} {taskObject.currentStage === 'video' && renderVideoThumbnails()}
{(taskObject.currentStage === 'scene' || taskObject.currentStage === 'character') && renderSketchThumbnails(getCurrentData())} {(taskObject.currentStage === 'scene' || taskObject.currentStage === 'character') && renderSketchThumbnails(getCurrentData())}
{taskObject.currentStage === 'final_video' && renderVideoThumbnails(true)}
</div> </div>
); );
} }

View File

@ -716,7 +716,6 @@ export function useWorkflowData({ onEditPlanGenerated }: UseWorkflowDataProps =
applyScript, applyScript,
fallbackToStep, fallbackToStep,
originalText: state.originalText, originalText: state.originalText,
// showGotoCutButton: from && currentLoadingText.includes('Post-production') ? true : false,
showGotoCutButton: (canGoToCut && (isGenerateEditPlan || taskObject.currentStage === 'final_video') || isShowError) ? true : false, showGotoCutButton: (canGoToCut && (isGenerateEditPlan || taskObject.currentStage === 'final_video') || isShowError) ? true : false,
generateEditPlan: openEditPlan, generateEditPlan: openEditPlan,
handleRetryVideo handleRetryVideo

View File

@ -94,3 +94,13 @@ export const downloadVideo = async (url: string) => {
console.error('下载视频失败:', error); console.error('下载视频失败:', error);
} }
}; };
/**
*
* @param urls URL列表
*/
export const downloadAllVideos = async (urls: string[]) => {
for (const url of urls) {
await downloadVideo(url);
}
};