最终视频与分镜列表切换

This commit is contained in:
北枳 2025-09-20 19:24:18 +08:00
parent 5b35e274b9
commit 3cf9915877
5 changed files with 125 additions and 14 deletions

View File

@ -19,6 +19,7 @@ import { Drawer, Tooltip, notification } from 'antd';
import { showEditingNotification } from "@/components/pages/work-flow/editing-notification";
// import { AIEditingIframeButton } from './work-flow/ai-editing-iframe';
import { exportVideoWithRetry } from '@/utils/export-service';
import { getFirstFrame } from '@/utils/tools';
// 临时禁用视频编辑功能
// import { EditPoint as EditPointType } from './work-flow/video-edit/types';
import { AIEditingIframeButton } from './work-flow/ai-editing-iframe';
@ -51,6 +52,8 @@ const WorkFlow = React.memo(function WorkFlow() {
const [previewVideoUrl, setPreviewVideoUrl] = React.useState<string | null>(null);
const [previewVideoId, setPreviewVideoId] = React.useState<string | null>(null);
const [isFocusChatInput, setIsFocusChatInput] = React.useState(false);
const [selectedView, setSelectedView] = React.useState<'final' | 'video' | null>(null);
const [isFinalBarOpen, setIsFinalBarOpen] = React.useState(true);
const [aiEditingResult, setAiEditingResult] = React.useState<any>(null);
// const aiEditingButtonRef = useRef<{ handleAIEditing: () => Promise<void> }>(null);
@ -236,6 +239,13 @@ const WorkFlow = React.memo(function WorkFlow() {
console.log('changedIndex_work-flow', currentSketchIndex, taskObject);
}, [currentSketchIndex, taskObject]);
// 当最终视频出现时,默认选中最终视频
useEffect(() => {
if (taskObject?.final?.url && selectedView === null) {
setSelectedView('final');
}
}, [taskObject?.final?.url, selectedView]);
// 监听粗剪是否完成
useEffect(() => {
console.log('🎬 final video useEffect triggered:', {
@ -409,6 +419,7 @@ Please process this video editing request.`;
title={taskObject.title}
current={currentSketchIndex + 1}
taskObject={taskObject}
selectedView={selectedView}
currentLoadingText={currentLoadingText}
/>
) : (
@ -431,11 +442,44 @@ Please process this video editing request.`;
>
{isDesktop ? (
<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}>
{/* 左侧最终视频缩略图栏(仅桌面) */}
{taskObject?.final?.url && (
<div
className="absolute -left-36 top-0 z-[50]"
data-alt="final-sidebar"
>
<div className={`flex items-start`}>
{isFinalBarOpen && (
<div
className="w-28 max-h-[60vh] overflow-y-auto rounded-lg backdrop-blur-lg bg-black/30 border border-white/20 shadow-xl p-2 mr-2"
data-alt="final-thumbnails"
>
{/* 预留历史列表,目前仅展示当前最终视频 */}
<button
type="button"
onClick={() => setSelectedView('final')}
className={`block w-full overflow-hidden rounded-md border ${selectedView === 'final' ? 'border-blue-500' : 'border-white/20'}`}
data-alt="final-thumb-item"
aria-label="Select final video"
>
<img
src={getFirstFrame(taskObject.final.url)}
alt="final"
className="w-full h-auto object-cover"
/>
<div className="text-xs text-white/80 text-center py-1">Final</div>
</button>
</div>
)}
</div>
</div>
)}
<MediaViewer
taskObject={taskObject}
scriptData={scriptData}
currentSketchIndex={currentSketchIndex}
isVideoPlaying={isVideoPlaying}
selectedView={selectedView}
onEditModalOpen={handleEditModalOpen}
onToggleVideoPlay={toggleVideoPlay}
setIsPauseWorkFlow={setIsPauseWorkFlow}
@ -459,6 +503,7 @@ Please process this video editing request.`;
taskObject={taskObject}
scriptData={scriptData}
currentSketchIndex={currentSketchIndex}
selectedView={selectedView}
mode={mode}
setIsPauseWorkFlow={setIsPauseWorkFlow}
setAnyAttribute={setAnyAttribute}
@ -474,6 +519,7 @@ Please process this video editing request.`;
onGotoCut={generateEditPlan}
isSmartChatBoxOpen={isSmartChatBoxOpen}
onRetryVideo={(video_id) => handleRetryVideo(video_id)}
onSelectView={(view) => setSelectedView(view)}
// 临时禁用视频编辑功能: enableVideoEdit={true} onVideoEditDescriptionSubmit={handleVideoEditDescriptionSubmit}
/>
)}
@ -484,9 +530,13 @@ Please process this video editing request.`;
isDisabledFocus={isEditModalOpen || isPauseWorkFlow || isFocusChatInput}
taskObject={taskObject}
currentSketchIndex={currentSketchIndex}
onSketchSelect={setCurrentSketchIndex}
onSketchSelect={(index) => {
setSelectedView('video');
setCurrentSketchIndex(index);
}}
onRetryVideo={handleRetryVideo}
className={isDesktop ? 'auto-cols-[20%]' : (isTablet ? 'auto-cols-[25%]' : 'auto-cols-[40%]')}
selectedView={selectedView}
/>
</div>
)}

View File

@ -18,6 +18,8 @@ interface H5MediaViewerProps {
scriptData: any;
/** 当前索引(视频/分镜阶段用于定位) */
currentSketchIndex: number;
/** 选中视图final 或 video用于覆盖阶段渲染 */
selectedView?: 'final' | 'video' | null;
/** 渲染模式(仅剧本阶段透传) */
mode: string;
/** 以下为剧本阶段透传必需项(与桌面版保持一致) */
@ -39,6 +41,8 @@ interface H5MediaViewerProps {
isSmartChatBoxOpen?: boolean;
/** 失败重试生成视频 */
onRetryVideo?: (video_id: string) => void;
/** 切换选择视图final 或 video */
onSelectView?: (view: 'final' | 'video') => void;
}
/**
@ -50,6 +54,7 @@ export function H5MediaViewer({
taskObject,
scriptData,
currentSketchIndex,
selectedView,
mode,
setIsPauseWorkFlow,
setAnyAttribute,
@ -61,7 +66,8 @@ export function H5MediaViewer({
showGotoCutButton,
onGotoCut,
isSmartChatBoxOpen,
onRetryVideo
onRetryVideo,
onSelectView
}: H5MediaViewerProps) {
const carouselRef = useRef<CarouselRef>(null);
const videoRefs = useRef<Array<HTMLVideoElement | null>>([]);
@ -69,9 +75,12 @@ export function H5MediaViewer({
const [activeIndex, setActiveIndex] = useState<number>(0);
const [isPlaying, setIsPlaying] = useState<boolean>(false);
const [isCatalogOpen, setIsCatalogOpen] = useState<boolean>(false);
const [isFinalBarOpen, setIsFinalBarOpen] = useState<boolean>(true);
// 计算当前阶段类型
const stage = taskObject.currentStage;
const stage = (selectedView === 'final' && taskObject.final?.url)
? 'final_video'
: (selectedView === 'video' ? 'video' : taskObject.currentStage);
// 生成各阶段对应的 slides 数据
const videoUrls = useMemo(() => {
@ -385,6 +394,27 @@ export function H5MediaViewer({
// 其他阶段:使用 Carousel
return (
<div ref={rootRef} data-alt="h5-media-viewer" className="w-[100vw] relative px-4">
{/* 左侧最终视频缩略图栏H5 */}
{taskObject?.final?.url && (
<div className="absolute left-4 top-0 z-[60]" data-alt="final-sidebar-h5">
<div className="flex items-start">
{isFinalBarOpen && (
<div className="w-20 max-h-[50vh] overflow-y-auto rounded-md backdrop-blur-lg bg-black/30 border border-white/20 shadow-xl p-1 mr-2" data-alt="final-thumbnails">
<button
type="button"
onClick={() => onSelectView && onSelectView('final')}
className={`block w-full overflow-hidden rounded border ${selectedView === 'final' ? 'border-blue-500' : 'border-white/20'}`}
data-alt="final-thumb-item"
aria-label="Select final video"
>
<img src={getFirstFrame(taskObject.final.url)} alt="final" className="w-full h-auto object-cover" />
<div className="text-[10px] text-white/80 text-center py-0.5">Final</div>
</button>
</div>
)}
</div>
</div>
)}
{stage === 'final_video' && videoUrls.length > 0 && renderVideoSlides()}
{stage === 'video' && videoUrls.length > 0 && renderVideoSlides()}
{(stage === 'scene' || stage === 'character') && imageUrls.length > 0 && renderImageSlides()}

View File

@ -14,6 +14,8 @@ interface H5TaskInfoProps {
taskObject: TaskObject
className?: string
currentLoadingText: string
/** 选中视图final 或 video */
selectedView?: 'final' | 'video' | null
}
const H5TaskInfo: React.FC<H5TaskInfoProps> = ({
@ -21,7 +23,8 @@ const H5TaskInfo: React.FC<H5TaskInfoProps> = ({
current,
taskObject,
className,
currentLoadingText
currentLoadingText,
selectedView
}) => {
type StageIndex = 0 | 1 | 2 | 3
@ -72,6 +75,25 @@ const H5TaskInfo: React.FC<H5TaskInfoProps> = ({
return 0
}, [taskObject, current, total])
// 构造副标题文本:优先根据 selectedView 覆盖
const subtitle = useMemo(() => {
if (selectedView === 'final' && taskObject.final?.url) {
return 'Final 1/1'
}
if (selectedView === 'video') {
const videosTotal = taskObject.videos?.total_count || taskObject.videos?.data?.length || 0
return `Shots ${Math.max(displayCurrent, 1)}/${Math.max(videosTotal, 1)}`
}
// 回退到原有逻辑
if (taskObject.currentStage === 'video' || taskObject.currentStage === 'final_video') {
return `Shots ${Math.max(displayCurrent, 1)}/${Math.max(total, 1)}`
}
if (taskObject.currentStage === 'scene' || taskObject.currentStage === 'character') {
return `Roles & Scenes ${Math.max(displayCurrent, 1)}/${Math.max(total, 1)}`
}
return null
}, [selectedView, taskObject, displayCurrent, total])
return (
<div
data-alt="h5-header"
@ -89,9 +111,9 @@ const H5TaskInfo: React.FC<H5TaskInfoProps> = ({
>
{title || '...'}
</h1>
{shouldShowCount && (
{shouldShowCount && subtitle && (
<span data-alt="shot-count" className="flex items-center gap-4 text-sm text-slate-300">
{taskObject.currentStage === 'video' ? 'Shots' : 'Roles & Scenes '} {displayCurrent}/{Math.max(total, 1)}
{subtitle}
</span>
)}

View File

@ -21,6 +21,7 @@ interface MediaViewerProps {
scriptData: any;
currentSketchIndex: number;
isVideoPlaying: boolean;
selectedView?: 'final' | 'video' | null;
onEditModalOpen: (tab: string) => void;
onToggleVideoPlay: () => void;
setIsPauseWorkFlow: (isPause: boolean) => void;
@ -44,6 +45,7 @@ export const MediaViewer = React.memo(function MediaViewer({
scriptData,
currentSketchIndex,
isVideoPlaying,
selectedView,
onEditModalOpen,
onToggleVideoPlay,
setIsPauseWorkFlow,
@ -734,20 +736,25 @@ export const MediaViewer = React.memo(function MediaViewer({
);
};
// 计算生效阶段selectedView 优先于 taskObject.currentStage
const effectiveStage = (selectedView === 'final' && taskObject.final?.url)
? 'final_video'
: (selectedView === 'video' ? 'video' : taskObject.currentStage);
// 根据当前步骤渲染对应内容
if (taskObject.currentStage === 'final_video') {
if (effectiveStage === 'final_video') {
return renderFinalVideo();
}
if (taskObject.currentStage === 'video') {
if (effectiveStage === 'video') {
return renderVideoContent(onGotoCut);
}
if (taskObject.currentStage === 'script') {
if (effectiveStage === 'script') {
return renderScriptContent();
}
if (taskObject.currentStage === 'scene' || taskObject.currentStage === 'character') {
if (effectiveStage === 'scene' || effectiveStage === 'character') {
return renderSketchContent([...taskObject.roles.data, ...taskObject.scenes.data][currentSketchIndex]);
}

View File

@ -15,6 +15,7 @@ interface ThumbnailGridProps {
onSketchSelect: (index: number) => void;
onRetryVideo: (video_id: string) => void;
className: string;
selectedView?: 'final' | 'video' | null;
}
/**
@ -26,7 +27,8 @@ export function ThumbnailGrid({
currentSketchIndex,
onSketchSelect,
onRetryVideo,
className
className,
selectedView
}: ThumbnailGridProps) {
const [hoveredIndex, setHoveredIndex] = useState<number | null>(null);
@ -185,7 +187,7 @@ export function ThumbnailGrid({
<div
key={`video-${urls}-${index}`}
className={`relative aspect-video rounded-lg overflow-hidden
${(currentSketchIndex === index && !disabled) ? 'ring-2 ring-blue-500 z-10' : 'hover:ring-2 hover:ring-blue-500/50'}`}
${(currentSketchIndex === index && !disabled && selectedView !== 'final') ? 'ring-2 ring-blue-500 z-10' : 'hover:ring-2 hover:ring-blue-500/50'}`}
onClick={() => !isDragging && !disabled && onSketchSelect(index)}
>
@ -294,6 +296,7 @@ export function ThumbnailGrid({
className="w-full h-full object-cover select-none"
src={sketch.url}
draggable="false"
alt={sketch.type ? String(sketch.type) : 'sketch'}
/>
</div>
)}
@ -343,9 +346,8 @@ export function ThumbnailGrid({
onFocus={() => setIsFocused(true)}
onBlur={() => setIsFocused(false)}
>
{taskObject.currentStage === 'video' && renderVideoThumbnails()}
{(taskObject.currentStage === 'video' || taskObject.currentStage === 'final_video') && renderVideoThumbnails()}
{(taskObject.currentStage === 'scene' || taskObject.currentStage === 'character') && renderSketchThumbnails(getCurrentData())}
{taskObject.currentStage === 'final_video' && renderVideoThumbnails(true)}
</div>
);
}