forked from 77media/video-flow
- [x] 显示 原分镜,剪辑后
- [x] 支持批量下载分镜视频 Chatbox查找消息是否有任务 还在执行中
This commit is contained in:
parent
254b226c6e
commit
9365896bb6
@ -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,
|
||||||
|
|||||||
@ -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}
|
||||||
|
|||||||
@ -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 () => {
|
||||||
|
|||||||
@ -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>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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
|
||||||
|
|||||||
@ -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);
|
||||||
|
}
|
||||||
|
};
|
||||||
Loading…
x
Reference in New Issue
Block a user