forked from 77media/video-flow
Merge branch 'dev' of https://git.qikongjian.com/77media/video-flow into dev
This commit is contained in:
commit
4de0ddb206
@ -621,7 +621,7 @@ export interface RoleResponse {
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
interface Role {
|
export interface Role {
|
||||||
name: string;
|
name: string;
|
||||||
url: string;
|
url: string;
|
||||||
status: number;
|
status: number;
|
||||||
@ -636,12 +636,30 @@ interface ShotSketch {
|
|||||||
script: string;
|
script: string;
|
||||||
status: number;
|
status: number;
|
||||||
}
|
}
|
||||||
interface Video {
|
export interface ShotVideo {
|
||||||
video_id: string;
|
video_id: string;
|
||||||
urls: string[];
|
urls: string[];
|
||||||
video_status: number;
|
video_status: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 执行loading文字映射
|
||||||
|
export const LOADING_TEXT_MAP = {
|
||||||
|
initializing: 'initializing...',
|
||||||
|
script: 'Generating script...',
|
||||||
|
getSketchStatus: 'Getting sketch status...',
|
||||||
|
sketch: (count: number, total: number) => `Generating sketch ${count}/${total}...`,
|
||||||
|
character: 'Getting character status...',
|
||||||
|
newCharacter: (count: number, total: number) => `Drawing character ${count}/${total}...`,
|
||||||
|
getShotSketchStatus: 'Getting shot sketch status...',
|
||||||
|
shotSketch: (count: number, total: number) => `Generating shot sketch ${count}/${total}...`,
|
||||||
|
getVideoStatus: 'Getting video status...',
|
||||||
|
video: (count: number, total: number) => `Generating video ${count}/${total}...`,
|
||||||
|
audio: 'Generating background audio...',
|
||||||
|
postProduction: (step: string) => `Post-production: ${step}...`,
|
||||||
|
final: 'Generating final product...',
|
||||||
|
complete: 'Task completed'
|
||||||
|
} as const;
|
||||||
|
|
||||||
export type Status = 'IN_PROGRESS' | 'COMPLETED' | 'FAILED';
|
export type Status = 'IN_PROGRESS' | 'COMPLETED' | 'FAILED';
|
||||||
export type Stage = 'script' | 'character' | 'scene' | 'shot_sketch' | 'video' | 'final_video';
|
export type Stage = 'script' | 'character' | 'scene' | 'shot_sketch' | 'video' | 'final_video';
|
||||||
// 添加 TaskObject 接口
|
// 添加 TaskObject 接口
|
||||||
@ -663,7 +681,7 @@ export interface TaskObject {
|
|||||||
total_count: number;
|
total_count: number;
|
||||||
}; // 分镜草图
|
}; // 分镜草图
|
||||||
videos: {
|
videos: {
|
||||||
data: Video[];
|
data: ShotVideo[];
|
||||||
total_count: number;
|
total_count: number;
|
||||||
}; // 视频
|
}; // 视频
|
||||||
final: {
|
final: {
|
||||||
|
|||||||
@ -179,6 +179,7 @@ export const useRoleShotServiceHook = (projectId: string,selectRole?:RoleEntity,
|
|||||||
return role.fromDraft
|
return role.fromDraft
|
||||||
});
|
});
|
||||||
console.log('newDraftRoleList', newDraftRoleList)
|
console.log('newDraftRoleList', newDraftRoleList)
|
||||||
|
console.log('应用角色到分镜', shotSelectionList)
|
||||||
// 循环调用接口,为每个选中的分镜单独调用
|
// 循环调用接口,为每个选中的分镜单独调用
|
||||||
const res = await Promise.all( shotSelectionList.map(async (shot) => {
|
const res = await Promise.all( shotSelectionList.map(async (shot) => {
|
||||||
// 调用应用角色到分镜接口(不等待完成)
|
// 调用应用角色到分镜接口(不等待完成)
|
||||||
@ -191,6 +192,8 @@ export const useRoleShotServiceHook = (projectId: string,selectRole?:RoleEntity,
|
|||||||
})
|
})
|
||||||
}))
|
}))
|
||||||
|
|
||||||
|
console.log('应用角色到分镜', res);
|
||||||
|
|
||||||
SaveEditUseCase.setVideoTasks([
|
SaveEditUseCase.setVideoTasks([
|
||||||
...SaveEditUseCase.videoTasks,
|
...SaveEditUseCase.videoTasks,
|
||||||
...res.map(item=>{
|
...res.map(item=>{
|
||||||
|
|||||||
@ -33,17 +33,9 @@ const WorkFlow = React.memo(function WorkFlow() {
|
|||||||
const {
|
const {
|
||||||
taskObject,
|
taskObject,
|
||||||
scriptData,
|
scriptData,
|
||||||
taskSketch,
|
|
||||||
taskVideos,
|
|
||||||
sketchCount,
|
|
||||||
isLoading,
|
isLoading,
|
||||||
currentStep,
|
|
||||||
currentSketchIndex,
|
currentSketchIndex,
|
||||||
isGeneratingSketch,
|
|
||||||
isGeneratingVideo,
|
|
||||||
currentLoadingText,
|
currentLoadingText,
|
||||||
totalSketchCount,
|
|
||||||
final,
|
|
||||||
dataLoadError,
|
dataLoadError,
|
||||||
setCurrentSketchIndex,
|
setCurrentSketchIndex,
|
||||||
retryLoadData,
|
retryLoadData,
|
||||||
@ -58,9 +50,8 @@ const WorkFlow = React.memo(function WorkFlow() {
|
|||||||
|
|
||||||
const {
|
const {
|
||||||
isVideoPlaying,
|
isVideoPlaying,
|
||||||
togglePlay,
|
|
||||||
toggleVideoPlay,
|
toggleVideoPlay,
|
||||||
} = usePlaybackControls(taskSketch, taskVideos, currentStep);
|
} = usePlaybackControls(taskObject.videos.data, taskObject.currentStage);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
console.log('changedIndex_work-flow', currentSketchIndex, taskObject);
|
console.log('changedIndex_work-flow', currentSketchIndex, taskObject);
|
||||||
@ -97,10 +88,8 @@ const WorkFlow = React.memo(function WorkFlow() {
|
|||||||
<div className="info-UUGkPJ">
|
<div className="info-UUGkPJ">
|
||||||
<ErrorBoundary>
|
<ErrorBoundary>
|
||||||
<TaskInfo
|
<TaskInfo
|
||||||
isLoading={isLoading}
|
|
||||||
taskObject={taskObject}
|
taskObject={taskObject}
|
||||||
currentLoadingText={currentLoadingText}
|
currentLoadingText={currentLoadingText}
|
||||||
dataLoadError={dataLoadError}
|
|
||||||
roles={taskObject.roles.data}
|
roles={taskObject.roles.data}
|
||||||
isPauseWorkFlow={isPauseWorkFlow}
|
isPauseWorkFlow={isPauseWorkFlow}
|
||||||
/>
|
/>
|
||||||
@ -170,14 +159,7 @@ const WorkFlow = React.memo(function WorkFlow() {
|
|||||||
<ThumbnailGrid
|
<ThumbnailGrid
|
||||||
isDisabledFocus={isEditModalOpen || isPauseWorkFlow}
|
isDisabledFocus={isEditModalOpen || isPauseWorkFlow}
|
||||||
taskObject={taskObject}
|
taskObject={taskObject}
|
||||||
isLoading={isLoading}
|
|
||||||
currentSketchIndex={currentSketchIndex}
|
currentSketchIndex={currentSketchIndex}
|
||||||
taskSketch={taskSketch}
|
|
||||||
taskVideos={taskVideos}
|
|
||||||
isGeneratingSketch={isGeneratingSketch}
|
|
||||||
isGeneratingVideo={isGeneratingVideo}
|
|
||||||
sketchCount={sketchCount}
|
|
||||||
totalSketchCount={totalSketchCount}
|
|
||||||
onSketchSelect={setCurrentSketchIndex}
|
onSketchSelect={setCurrentSketchIndex}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
@ -228,6 +210,7 @@ const WorkFlow = React.memo(function WorkFlow() {
|
|||||||
SaveEditUseCase.clearData();
|
SaveEditUseCase.clearData();
|
||||||
setIsEditModalOpen(false)
|
setIsEditModalOpen(false)
|
||||||
}}
|
}}
|
||||||
|
taskObject={taskObject}
|
||||||
currentSketchIndex={currentSketchIndex}
|
currentSketchIndex={currentSketchIndex}
|
||||||
roles={taskObject.roles.data}
|
roles={taskObject.roles.data}
|
||||||
setIsPauseWorkFlow={setIsPauseWorkFlow}
|
setIsPauseWorkFlow={setIsPauseWorkFlow}
|
||||||
|
|||||||
@ -10,12 +10,11 @@ import {
|
|||||||
Film,
|
Film,
|
||||||
Scissors
|
Scissors
|
||||||
} from 'lucide-react';
|
} from 'lucide-react';
|
||||||
|
import { TaskObject } from '@/api/DTO/movieEdit';
|
||||||
|
|
||||||
interface TaskInfoProps {
|
interface TaskInfoProps {
|
||||||
isLoading: boolean;
|
taskObject: TaskObject;
|
||||||
taskObject: any;
|
|
||||||
currentLoadingText: string;
|
currentLoadingText: string;
|
||||||
dataLoadError?: string | null;
|
|
||||||
roles: any[];
|
roles: any[];
|
||||||
isPauseWorkFlow: boolean;
|
isPauseWorkFlow: boolean;
|
||||||
}
|
}
|
||||||
@ -120,17 +119,14 @@ const StageIcons = ({ currentStage, isExpanded, isPauseWorkFlow }: { currentStag
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export function TaskInfo({
|
export function TaskInfo({
|
||||||
isLoading,
|
taskObject,
|
||||||
taskObject,
|
|
||||||
currentLoadingText,
|
currentLoadingText,
|
||||||
dataLoadError,
|
|
||||||
roles,
|
roles,
|
||||||
isPauseWorkFlow
|
isPauseWorkFlow
|
||||||
}: TaskInfoProps) {
|
}: TaskInfoProps) {
|
||||||
const [isScriptModalOpen, setIsScriptModalOpen] = useState(false);
|
const [isScriptModalOpen, setIsScriptModalOpen] = useState(false);
|
||||||
const [currentStage, setCurrentStage] = useState(0);
|
const [currentStage, setCurrentStage] = useState(0);
|
||||||
const [isShowScriptIcon, setIsShowScriptIcon] = useState(true);
|
|
||||||
const [isStageIconsExpanded, setIsStageIconsExpanded] = useState(false);
|
const [isStageIconsExpanded, setIsStageIconsExpanded] = useState(false);
|
||||||
const timerRef = useRef<NodeJS.Timeout | null>(null);
|
const timerRef = useRef<NodeJS.Timeout | null>(null);
|
||||||
|
|
||||||
@ -146,72 +142,39 @@ export function TaskInfo({
|
|||||||
timerRef.current = null;
|
timerRef.current = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 统一更新currentStage
|
||||||
|
if (currentLoadingText.includes('initializing') || currentLoadingText.includes('script') || currentLoadingText.includes('character')) {
|
||||||
|
setCurrentStage(0);
|
||||||
|
} else if (currentLoadingText.includes('sketch') && !currentLoadingText.includes('shot sketch')) {
|
||||||
|
setCurrentStage(1);
|
||||||
|
} else if (!currentLoadingText.includes('Post-production') && (currentLoadingText.includes('shot sketch') || currentLoadingText.includes('video'))) {
|
||||||
|
setCurrentStage(2);
|
||||||
|
} else if (currentLoadingText.includes('Post-production')) {
|
||||||
|
setCurrentStage(3);
|
||||||
|
}
|
||||||
|
|
||||||
if (currentLoadingText.includes('Task completed')) {
|
if (currentLoadingText.includes('Task completed')) {
|
||||||
console.log('Closing modal at completion');
|
console.log('Closing modal at completion');
|
||||||
setIsScriptModalOpen(false);
|
setIsScriptModalOpen(false);
|
||||||
setIsShowScriptIcon(false);
|
|
||||||
}
|
}
|
||||||
if (currentLoadingText.includes('Post-production')) {
|
if (currentLoadingText.includes('Post-production') || currentLoadingText.includes('status')) {
|
||||||
if (isScriptModalOpen) {
|
|
||||||
setIsScriptModalOpen(false);
|
|
||||||
}
|
|
||||||
setCurrentStage(3);
|
|
||||||
console.log('isScriptModalOpen-Post-production', currentLoadingText, isScriptModalOpen);
|
console.log('isScriptModalOpen-Post-production', currentLoadingText, isScriptModalOpen);
|
||||||
timerRef.current = setTimeout(() => {
|
|
||||||
setIsScriptModalOpen(true);
|
|
||||||
}, 8000);
|
|
||||||
}
|
|
||||||
if (currentLoadingText.includes('Generating video')) {
|
|
||||||
console.log('isScriptModalOpen-video', currentLoadingText, isScriptModalOpen);
|
|
||||||
if (isScriptModalOpen) {
|
if (isScriptModalOpen) {
|
||||||
setIsScriptModalOpen(false);
|
setIsScriptModalOpen(false);
|
||||||
setCurrentStage(2);
|
|
||||||
|
|
||||||
// 延迟8s 再次打开
|
|
||||||
timerRef.current = setTimeout(() => {
|
|
||||||
setIsScriptModalOpen(true);
|
|
||||||
}, 8000);
|
|
||||||
} else {
|
} else {
|
||||||
setIsScriptModalOpen(true);
|
setIsScriptModalOpen(true);
|
||||||
setCurrentStage(2);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (currentLoadingText.includes('video status')) {
|
|
||||||
if (isScriptModalOpen) {
|
|
||||||
setIsScriptModalOpen(false);
|
|
||||||
}
|
|
||||||
setCurrentStage(2);
|
|
||||||
}
|
|
||||||
if (currentLoadingText.includes('Generating sketch') || currentLoadingText.includes('Generating shot sketch')) {
|
|
||||||
console.log('isScriptModalOpen-sketch', currentLoadingText, isScriptModalOpen);
|
|
||||||
if (isScriptModalOpen) {
|
|
||||||
setIsScriptModalOpen(false);
|
|
||||||
setCurrentStage(1);
|
|
||||||
|
|
||||||
// 延迟8s 再次打开
|
|
||||||
timerRef.current = setTimeout(() => {
|
timerRef.current = setTimeout(() => {
|
||||||
setIsScriptModalOpen(true);
|
setIsScriptModalOpen(false);
|
||||||
}, 8000);
|
}, 8000);
|
||||||
} else {
|
|
||||||
setIsScriptModalOpen(true);
|
|
||||||
setCurrentStage(1);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (currentLoadingText.includes('sketch status')) {
|
|
||||||
if (isScriptModalOpen) {
|
|
||||||
setIsScriptModalOpen(false);
|
|
||||||
}
|
|
||||||
setCurrentStage(1);
|
|
||||||
}
|
|
||||||
if (currentLoadingText.includes('script')) {
|
if (currentLoadingText.includes('script')) {
|
||||||
console.log('isScriptModalOpen-script', currentLoadingText, isScriptModalOpen);
|
console.log('isScriptModalOpen-script', currentLoadingText, isScriptModalOpen);
|
||||||
setIsScriptModalOpen(true);
|
setIsScriptModalOpen(true);
|
||||||
setCurrentStage(0);
|
|
||||||
}
|
}
|
||||||
if (currentLoadingText.includes('initializing')) {
|
if (currentLoadingText.includes('initializing')) {
|
||||||
console.log('isScriptModalOpen-initializing', currentLoadingText, isScriptModalOpen);
|
console.log('isScriptModalOpen-initializing', currentLoadingText, isScriptModalOpen);
|
||||||
setIsScriptModalOpen(true);
|
setIsScriptModalOpen(true);
|
||||||
setCurrentStage(0);
|
|
||||||
}
|
}
|
||||||
return () => {
|
return () => {
|
||||||
if (timerRef.current) {
|
if (timerRef.current) {
|
||||||
|
|||||||
@ -10,28 +10,14 @@ import { TaskObject } from '@/api/DTO/movieEdit';
|
|||||||
interface ThumbnailGridProps {
|
interface ThumbnailGridProps {
|
||||||
isDisabledFocus: boolean;
|
isDisabledFocus: boolean;
|
||||||
taskObject: TaskObject;
|
taskObject: TaskObject;
|
||||||
isLoading: boolean;
|
|
||||||
currentSketchIndex: number;
|
currentSketchIndex: number;
|
||||||
taskSketch: any[];
|
|
||||||
taskVideos: any[];
|
|
||||||
isGeneratingSketch: boolean;
|
|
||||||
isGeneratingVideo: boolean;
|
|
||||||
sketchCount: number;
|
|
||||||
totalSketchCount: number;
|
|
||||||
onSketchSelect: (index: number) => void;
|
onSketchSelect: (index: number) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function ThumbnailGrid({
|
export function ThumbnailGrid({
|
||||||
isDisabledFocus,
|
isDisabledFocus,
|
||||||
taskObject,
|
taskObject,
|
||||||
isLoading,
|
|
||||||
currentSketchIndex,
|
currentSketchIndex,
|
||||||
taskSketch,
|
|
||||||
taskVideos,
|
|
||||||
isGeneratingSketch,
|
|
||||||
isGeneratingVideo,
|
|
||||||
sketchCount,
|
|
||||||
totalSketchCount,
|
|
||||||
onSketchSelect
|
onSketchSelect
|
||||||
}: ThumbnailGridProps) {
|
}: ThumbnailGridProps) {
|
||||||
const thumbnailsRef = useRef<HTMLDivElement>(null);
|
const thumbnailsRef = useRef<HTMLDivElement>(null);
|
||||||
@ -166,89 +152,11 @@ export function ThumbnailGrid({
|
|||||||
console.log('taskObject.currentStage_thumbnail-grid', taskObject.currentStage);
|
console.log('taskObject.currentStage_thumbnail-grid', taskObject.currentStage);
|
||||||
}, [taskObject.currentStage]);
|
}, [taskObject.currentStage]);
|
||||||
|
|
||||||
// 渲染加载状态
|
|
||||||
if (isLoading) {
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<Skeleton className="w-[128px] h-[128px] rounded-lg" />
|
|
||||||
<Skeleton className="w-[128px] h-[128px] rounded-lg" />
|
|
||||||
<Skeleton className="w-[128px] h-[128px] rounded-lg" />
|
|
||||||
<Skeleton className="w-[128px] h-[128px] rounded-lg" />
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
// 粗剪/精剪最终成片阶段不显示缩略图
|
// 粗剪/精剪最终成片阶段不显示缩略图
|
||||||
if (taskObject.currentStage === 'final_video') {
|
if (taskObject.currentStage === 'final_video') {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 渲染生成中的缩略图
|
|
||||||
const renderGeneratingThumbnail = () => {
|
|
||||||
const currentSketch = taskSketch[currentSketchIndex];
|
|
||||||
const defaultBgColors = ['RGB(45, 50, 70)', 'RGB(75, 80, 100)', 'RGB(105, 110, 130)'];
|
|
||||||
const bgColors = currentSketch?.bg_rgb || defaultBgColors;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<motion.div
|
|
||||||
className="relative aspect-video rounded-lg overflow-hidden"
|
|
||||||
initial={{ opacity: 0, scale: 0.8 }}
|
|
||||||
animate={{ opacity: 1, scale: 1 }}
|
|
||||||
transition={{ duration: 0.3 }}
|
|
||||||
>
|
|
||||||
{/* 动态渐变背景 */}
|
|
||||||
<motion.div
|
|
||||||
className={`absolute inset-0 bg-gradient-to-r from-[${bgColors[0]}] via-[${bgColors[1]}] to-[${bgColors[2]}]`}
|
|
||||||
animate={{
|
|
||||||
backgroundPosition: ["0% 50%", "100% 50%", "0% 50%"],
|
|
||||||
}}
|
|
||||||
transition={{
|
|
||||||
duration: 5,
|
|
||||||
repeat: Infinity,
|
|
||||||
ease: "linear"
|
|
||||||
}}
|
|
||||||
style={{
|
|
||||||
backgroundSize: "200% 200%",
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
{/* 动态光效 */}
|
|
||||||
<motion.div
|
|
||||||
className="absolute inset-0 opacity-50"
|
|
||||||
style={{
|
|
||||||
background: "radial-gradient(circle at center, rgba(255,255,255,0.8) 0%, transparent 50%)",
|
|
||||||
}}
|
|
||||||
animate={{
|
|
||||||
scale: [1, 1.2, 1],
|
|
||||||
}}
|
|
||||||
transition={{
|
|
||||||
duration: 2,
|
|
||||||
repeat: Infinity,
|
|
||||||
ease: "easeInOut"
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
<div className="absolute inset-0 flex items-center justify-center">
|
|
||||||
<div className="relative">
|
|
||||||
<motion.div
|
|
||||||
className="absolute -inset-4 bg-gradient-to-r from-white via-sky-200 to-cyan-200 rounded-full opacity-60 blur-xl"
|
|
||||||
animate={{
|
|
||||||
scale: [1, 1.2, 1],
|
|
||||||
rotate: [0, 180, 360],
|
|
||||||
}}
|
|
||||||
transition={{
|
|
||||||
duration: 4,
|
|
||||||
repeat: Infinity,
|
|
||||||
ease: "linear"
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div className="absolute bottom-0 left-0 right-0 p-2 bg-gradient-to-t from-black/60 to-transparent">
|
|
||||||
<span className="text-xs text-white/90">Scene {sketchCount + 1}</span>
|
|
||||||
</div>
|
|
||||||
</motion.div>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
// 渲染视频阶段的缩略图
|
// 渲染视频阶段的缩略图
|
||||||
const renderVideoThumbnails = () => (
|
const renderVideoThumbnails = () => (
|
||||||
taskObject.videos.data.map((video, index) => {
|
taskObject.videos.data.map((video, index) => {
|
||||||
@ -354,7 +262,6 @@ export function ThumbnailGrid({
|
|||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
{isGeneratingSketch && sketchCount < totalSketchCount && renderGeneratingThumbnail()}
|
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|||||||
@ -5,13 +5,14 @@ import { useSearchParams } from 'next/navigation';
|
|||||||
import { useRoleServiceHook } from "@/app/service/Interaction/RoleService";
|
import { useRoleServiceHook } from "@/app/service/Interaction/RoleService";
|
||||||
import { useRoleShotServiceHook } from "@/app/service/Interaction/RoleShotService";
|
import { useRoleShotServiceHook } from "@/app/service/Interaction/RoleShotService";
|
||||||
import { useScriptService } from "@/app/service/Interaction/ScriptService";
|
import { useScriptService } from "@/app/service/Interaction/ScriptService";
|
||||||
|
import { VideoSegmentEntity } from "@/app/service/domain/Entities";
|
||||||
|
|
||||||
export const useEditData = (tabType: string, originalText?: string) => {
|
export const useEditData = (tabType: string, originalText?: string) => {
|
||||||
const searchParams = useSearchParams();
|
const searchParams = useSearchParams();
|
||||||
const projectId = searchParams.get('episodeId') || '';
|
const projectId = searchParams.get('episodeId') || '';
|
||||||
const [loading, setLoading] = useState(true);
|
const [loading, setLoading] = useState(true);
|
||||||
const [scriptData, setScriptData] = useState<any[]>([]);
|
const [scriptData, setScriptData] = useState<any[]>([]);
|
||||||
const [shotData, setShotData] = useState<any[]>([]);
|
const [shotData, setShotData] = useState<VideoSegmentEntity[]>([]);
|
||||||
|
|
||||||
const [roleData, setRoleData] = useState<any[]>([]);
|
const [roleData, setRoleData] = useState<any[]>([]);
|
||||||
|
|
||||||
@ -24,6 +25,7 @@ export const useEditData = (tabType: string, originalText?: string) => {
|
|||||||
|
|
||||||
const {
|
const {
|
||||||
videoSegments,
|
videoSegments,
|
||||||
|
selectedSegment,
|
||||||
scriptRoles,
|
scriptRoles,
|
||||||
getVideoSegmentList,
|
getVideoSegmentList,
|
||||||
setSelectedSegment,
|
setSelectedSegment,
|
||||||
@ -123,6 +125,7 @@ export const useEditData = (tabType: string, originalText?: string) => {
|
|||||||
applyScript,
|
applyScript,
|
||||||
// shot
|
// shot
|
||||||
shotData,
|
shotData,
|
||||||
|
selectedSegment,
|
||||||
scriptRoles,
|
scriptRoles,
|
||||||
setSelectedSegment,
|
setSelectedSegment,
|
||||||
regenerateVideoSegment,
|
regenerateVideoSegment,
|
||||||
|
|||||||
@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
import { useState, useRef, useEffect, useCallback } from 'react';
|
import { useState, useRef, useEffect, useCallback } from 'react';
|
||||||
|
|
||||||
export function usePlaybackControls(taskSketch: any[], taskVideos: any[], currentStep: string) {
|
export function usePlaybackControls(taskVideos: any[], currentStage: string) {
|
||||||
const [isPlaying, setIsPlaying] = useState(false);
|
const [isPlaying, setIsPlaying] = useState(false);
|
||||||
const [isVideoPlaying, setIsVideoPlaying] = useState(true);
|
const [isVideoPlaying, setIsVideoPlaying] = useState(true);
|
||||||
const [showControls, setShowControls] = useState(false);
|
const [showControls, setShowControls] = useState(false);
|
||||||
@ -19,24 +19,6 @@ export function usePlaybackControls(taskSketch: any[], taskVideos: any[], curren
|
|||||||
setIsVideoPlaying(prev => !prev);
|
setIsVideoPlaying(prev => !prev);
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
// 自动播放逻辑 - 分镜草图(移除重复的定时器逻辑,由主组件处理)
|
|
||||||
// useEffect(() => {
|
|
||||||
// if (isPlaying && taskSketch.length > 0) {
|
|
||||||
// playTimerRef.current = setInterval(() => {
|
|
||||||
// // 这里的切换逻辑需要在父组件中处理
|
|
||||||
// // 因为需要访问 setCurrentSketchIndex
|
|
||||||
// }, 1000);
|
|
||||||
// } else if (playTimerRef.current) {
|
|
||||||
// clearInterval(playTimerRef.current);
|
|
||||||
// }
|
|
||||||
|
|
||||||
// return () => {
|
|
||||||
// if (playTimerRef.current) {
|
|
||||||
// clearInterval(playTimerRef.current);
|
|
||||||
// }
|
|
||||||
// };
|
|
||||||
// }, [isPlaying, taskSketch.length]);
|
|
||||||
|
|
||||||
// 视频自动播放逻辑
|
// 视频自动播放逻辑
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (isVideoPlaying && taskVideos.length > 0) {
|
if (isVideoPlaying && taskVideos.length > 0) {
|
||||||
@ -55,20 +37,12 @@ export function usePlaybackControls(taskSketch: any[], taskVideos: any[], curren
|
|||||||
};
|
};
|
||||||
}, [isVideoPlaying, taskVideos.length]);
|
}, [isVideoPlaying, taskVideos.length]);
|
||||||
|
|
||||||
// 当切换到视频模式时,停止分镜草图播放(注释掉,让用户手动控制)
|
|
||||||
// useEffect(() => {
|
|
||||||
// if (Number(currentStep) >= 3) {
|
|
||||||
// console.log('切换到步骤3+,停止分镜草图播放');
|
|
||||||
// setIsPlaying(false);
|
|
||||||
// }
|
|
||||||
// }, [currentStep]);
|
|
||||||
|
|
||||||
// 当切换到分镜草图模式时,停止视频播放
|
// 当切换到分镜草图模式时,停止视频播放
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (currentStep !== '3') {
|
if (currentStage !== 'video') {
|
||||||
setIsVideoPlaying(false);
|
setIsVideoPlaying(false);
|
||||||
}
|
}
|
||||||
}, [currentStep]);
|
}, [currentStage]);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
isPlaying,
|
isPlaying,
|
||||||
|
|||||||
@ -3,43 +3,9 @@
|
|||||||
import { useState, useEffect, useCallback, useRef, useMemo } from 'react';
|
import { useState, useEffect, useCallback, useRef, useMemo } from 'react';
|
||||||
import { useSearchParams } from 'next/navigation';
|
import { useSearchParams } from 'next/navigation';
|
||||||
import { detailScriptEpisodeNew, getScriptTitle, getRunningStreamData, pauseMovieProjectPlan, resumeMovieProjectPlan } from '@/api/video_flow';
|
import { detailScriptEpisodeNew, getScriptTitle, getRunningStreamData, pauseMovieProjectPlan, resumeMovieProjectPlan } from '@/api/video_flow';
|
||||||
import { useAppDispatch, useAppSelector } from '@/lib/store/hooks';
|
|
||||||
import { setSketchCount, setVideoCount } from '@/lib/store/workflowSlice';
|
|
||||||
import { useScriptService } from "@/app/service/Interaction/ScriptService";
|
import { useScriptService } from "@/app/service/Interaction/ScriptService";
|
||||||
import { useUpdateEffect } from '@/app/hooks/useUpdateEffect';
|
import { useUpdateEffect } from '@/app/hooks/useUpdateEffect';
|
||||||
import { TaskObject, Status, Stage } from '@/api/DTO/movieEdit';
|
import { LOADING_TEXT_MAP, TaskObject, Status, Stage } from '@/api/DTO/movieEdit';
|
||||||
|
|
||||||
// 步骤映射
|
|
||||||
const STEP_MAP = {
|
|
||||||
'initializing': '0',
|
|
||||||
'sketch': '1',
|
|
||||||
'character': '2',
|
|
||||||
'video': '3',
|
|
||||||
'music': '4',
|
|
||||||
'final_video': '6'
|
|
||||||
} as const;
|
|
||||||
// 执行loading文字映射
|
|
||||||
const LOADING_TEXT_MAP = {
|
|
||||||
initializing: 'initializing...',
|
|
||||||
script: 'Generating script...',
|
|
||||||
getSketchStatus: 'Getting sketch status...',
|
|
||||||
sketch: (count: number, total: number) => `Generating sketch ${count}/${total}...`,
|
|
||||||
sketchComplete: 'Sketch generation complete',
|
|
||||||
character: 'Drawing characters...',
|
|
||||||
newCharacter: (count: number, total: number) => `Drawing character ${count}/${total}...`,
|
|
||||||
getShotSketchStatus: 'Getting shot sketch status...',
|
|
||||||
shotSketch: (count: number, total: number) => `Generating shot sketch ${count}/${total}...`,
|
|
||||||
getVideoStatus: 'Getting video status...',
|
|
||||||
video: (count: number, total: number) => `Generating video ${count}/${total}...`,
|
|
||||||
videoComplete: 'Video generation complete',
|
|
||||||
audio: 'Generating background audio...',
|
|
||||||
postProduction: (step: string) => `Post-production: ${step}...`,
|
|
||||||
final: 'Generating final product...',
|
|
||||||
complete: 'Task completed'
|
|
||||||
} as const;
|
|
||||||
|
|
||||||
type ApiStep = keyof typeof STEP_MAP;
|
|
||||||
|
|
||||||
|
|
||||||
export function useWorkflowData() {
|
export function useWorkflowData() {
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@ -53,7 +19,7 @@ export function useWorkflowData() {
|
|||||||
let tempTaskObject = useRef<TaskObject>({
|
let tempTaskObject = useRef<TaskObject>({
|
||||||
title: '',
|
title: '',
|
||||||
tags: [],
|
tags: [],
|
||||||
currentStage: 'script',
|
currentStage: 'script' as Stage,
|
||||||
status: 'IN_PROGRESS' as Status,
|
status: 'IN_PROGRESS' as Status,
|
||||||
roles: {
|
roles: {
|
||||||
data: [],
|
data: [],
|
||||||
@ -81,19 +47,8 @@ export function useWorkflowData() {
|
|||||||
|
|
||||||
// 更新 taskObject 的类型
|
// 更新 taskObject 的类型
|
||||||
const [taskObject, setTaskObject] = useState<TaskObject>(tempTaskObject.current);
|
const [taskObject, setTaskObject] = useState<TaskObject>(tempTaskObject.current);
|
||||||
const [taskSketch, setTaskSketch] = useState<any[]>([]);
|
|
||||||
const [taskScenes, setTaskScenes] = useState<any[]>([]);
|
|
||||||
const [taskShotSketch, setTaskShotSketch] = useState<any[]>([]);
|
|
||||||
const [taskVideos, setTaskVideos] = useState<any[]>([]);
|
|
||||||
const [currentStep, setCurrentStep] = useState('0');
|
|
||||||
const [currentSketchIndex, setCurrentSketchIndex] = useState(0);
|
const [currentSketchIndex, setCurrentSketchIndex] = useState(0);
|
||||||
const [isGeneratingSketch, setIsGeneratingSketch] = useState(false);
|
|
||||||
const [isGeneratingVideo, setIsGeneratingVideo] = useState(false);
|
|
||||||
const [currentLoadingText, setCurrentLoadingText] = useState('loading project info...');
|
const [currentLoadingText, setCurrentLoadingText] = useState('loading project info...');
|
||||||
const [totalSketchCount, setTotalSketchCount] = useState(0);
|
|
||||||
const [roles, setRoles] = useState<any[]>([]);
|
|
||||||
const [music, setMusic] = useState<any[]>([]);
|
|
||||||
const [final, setFinal] = useState<any>(null);
|
|
||||||
const [dataLoadError, setDataLoadError] = useState<string | null>(null);
|
const [dataLoadError, setDataLoadError] = useState<string | null>(null);
|
||||||
const [needStreamData, setNeedStreamData] = useState(false);
|
const [needStreamData, setNeedStreamData] = useState(false);
|
||||||
const [isPauseWorkFlow, setIsPauseWorkFlow] = useState(false);
|
const [isPauseWorkFlow, setIsPauseWorkFlow] = useState(false);
|
||||||
@ -103,10 +58,6 @@ export function useWorkflowData() {
|
|||||||
isLoading: true
|
isLoading: true
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
const dispatch = useAppDispatch();
|
|
||||||
const { sketchCount, videoCount } = useAppSelector((state) => state.workflow);
|
|
||||||
|
|
||||||
const {
|
const {
|
||||||
scriptBlocksMemo, // 渲染剧本数据
|
scriptBlocksMemo, // 渲染剧本数据
|
||||||
initializeFromProject,
|
initializeFromProject,
|
||||||
@ -170,24 +121,24 @@ export function useWorkflowData() {
|
|||||||
console.log('应用剧本');
|
console.log('应用剧本');
|
||||||
// 自动模式下 应用剧本;手动模式 需要点击 下一步 触发
|
// 自动模式下 应用剧本;手动模式 需要点击 下一步 触发
|
||||||
// 确保仅自动触发一次
|
// 确保仅自动触发一次
|
||||||
state.mode.includes('auto') && loadingText.current !== LOADING_TEXT_MAP.getSketchStatus && applyScript();
|
state.mode.includes('auto') && loadingText.current !== LOADING_TEXT_MAP.character && applyScript();
|
||||||
loadingText.current = LOADING_TEXT_MAP.getSketchStatus;
|
loadingText.current = LOADING_TEXT_MAP.character;
|
||||||
} else {
|
} else {
|
||||||
loadingText.current = LOADING_TEXT_MAP.script;
|
loadingText.current = LOADING_TEXT_MAP.script;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (taskObject.currentStage === 'scene') {
|
|
||||||
const realSketchResultData = taskObject.scenes.data.filter((item: any) => item.status !== 0);
|
|
||||||
if (taskObject.scenes.total_count > realSketchResultData.length) {
|
|
||||||
loadingText.current = LOADING_TEXT_MAP.sketch(realSketchResultData.length, taskObject.scenes.total_count);
|
|
||||||
} else {
|
|
||||||
loadingText.current = LOADING_TEXT_MAP.character;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (taskObject.currentStage === 'character') {
|
if (taskObject.currentStage === 'character') {
|
||||||
const realCharacterResultData = taskObject.roles.data.filter((item: any) => item.status !== 0);
|
const realCharacterResultData = taskObject.roles.data.filter((item: any) => item.status !== 0);
|
||||||
if (taskObject.roles.total_count > realCharacterResultData.length) {
|
if (taskObject.roles.total_count > realCharacterResultData.length) {
|
||||||
loadingText.current = LOADING_TEXT_MAP.newCharacter(realCharacterResultData.length, taskObject.roles.total_count);
|
loadingText.current = LOADING_TEXT_MAP.newCharacter(realCharacterResultData.length, taskObject.roles.total_count);
|
||||||
|
} else {
|
||||||
|
loadingText.current = LOADING_TEXT_MAP.getSketchStatus;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (taskObject.currentStage === 'scene') {
|
||||||
|
const realSketchResultData = taskObject.scenes.data.filter((item: any) => item.status !== 0);
|
||||||
|
if (taskObject.scenes.total_count > realSketchResultData.length) {
|
||||||
|
loadingText.current = LOADING_TEXT_MAP.sketch(realSketchResultData.length, taskObject.scenes.total_count);
|
||||||
} else {
|
} else {
|
||||||
loadingText.current = LOADING_TEXT_MAP.getShotSketchStatus;
|
loadingText.current = LOADING_TEXT_MAP.getShotSketchStatus;
|
||||||
}
|
}
|
||||||
@ -217,16 +168,6 @@ export function useWorkflowData() {
|
|||||||
setCurrentLoadingText(loadingText.current);
|
setCurrentLoadingText(loadingText.current);
|
||||||
}, [scriptBlocksMemo, taskObject.currentStage, taskObject.scenes.data, taskObject.roles.data, taskObject.shot_sketch.data, taskObject.videos.data, taskObject.status], {mode: 'none'});
|
}, [scriptBlocksMemo, taskObject.currentStage, taskObject.scenes.data, taskObject.roles.data, taskObject.shot_sketch.data, taskObject.videos.data, taskObject.status], {mode: 'none'});
|
||||||
|
|
||||||
// 更新 setSketchCount
|
|
||||||
const updateSketchCount = useCallback((count: number) => {
|
|
||||||
dispatch(setSketchCount(count));
|
|
||||||
}, [dispatch]);
|
|
||||||
|
|
||||||
// 更新 setVideoCount
|
|
||||||
const updateVideoCount = useCallback((count: number) => {
|
|
||||||
dispatch(setVideoCount(count));
|
|
||||||
}, [dispatch]);
|
|
||||||
|
|
||||||
// 将 sketchCount 和 videoCount 放到 redux 中 每一次变化也要更新
|
// 将 sketchCount 和 videoCount 放到 redux 中 每一次变化也要更新
|
||||||
|
|
||||||
// 添加手动播放控制
|
// 添加手动播放控制
|
||||||
@ -249,13 +190,7 @@ export function useWorkflowData() {
|
|||||||
throw new Error(response.message);
|
throw new Error(response.message);
|
||||||
}
|
}
|
||||||
|
|
||||||
let sketchCount = 0;
|
|
||||||
const all_task_data = response.data;
|
const all_task_data = response.data;
|
||||||
// all_task_data 下标0 和 下标1 换位置
|
|
||||||
const temp = all_task_data[0];
|
|
||||||
all_task_data[0] = all_task_data[1];
|
|
||||||
all_task_data[1] = temp;
|
|
||||||
|
|
||||||
const { current: taskCurrent } = tempTaskObject;
|
const { current: taskCurrent } = tempTaskObject;
|
||||||
|
|
||||||
console.log('---look-all_task_data', all_task_data);
|
console.log('---look-all_task_data', all_task_data);
|
||||||
@ -266,32 +201,6 @@ export function useWorkflowData() {
|
|||||||
|
|
||||||
for (const task of all_task_data) {
|
for (const task of all_task_data) {
|
||||||
// 如果有已完成的数据,同步到状态
|
// 如果有已完成的数据,同步到状态
|
||||||
if (task.task_name === 'generate_sketch' && task.task_result && task.task_result.data) {
|
|
||||||
let realSketchResultData = task.task_result.data.filter((item: any) => item.image_path);
|
|
||||||
if (task.task_status === 'COMPLETED') {
|
|
||||||
realSketchResultData = taskCurrent.scenes.data.filter((item: any) => item.status !== 0);
|
|
||||||
}
|
|
||||||
console.log('---look-realSketchResultData', realSketchResultData);
|
|
||||||
taskCurrent.scenes.total_count = task.task_result.total_count;
|
|
||||||
if (task.task_status !== 'COMPLETED' || taskCurrent.scenes.total_count !== realSketchResultData.length) {
|
|
||||||
taskCurrent.currentStage = 'scene';
|
|
||||||
// 正在生成草图中 替换 sketch 数据
|
|
||||||
const sketchList = [];
|
|
||||||
for (const sketch of task.task_result.data) {
|
|
||||||
sketchList.push({
|
|
||||||
url: sketch.image_path,
|
|
||||||
script: sketch.sketch_name,
|
|
||||||
status: sketch.image_path ? 1 : (task.task_status === 'COMPLETED' ? 2 : 0)
|
|
||||||
});
|
|
||||||
}
|
|
||||||
taskCurrent.scenes.data = sketchList;
|
|
||||||
if (task.task_status === 'COMPLETED') {
|
|
||||||
// 草图生成完成
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (task.task_name === 'generate_character' && task.task_result && task.task_result.data) {
|
if (task.task_name === 'generate_character' && task.task_result && task.task_result.data) {
|
||||||
let realCharacterResultData = task.task_result.data.filter((item: any) => item.image_path);
|
let realCharacterResultData = task.task_result.data.filter((item: any) => item.image_path);
|
||||||
if (task.task_status === 'COMPLETED') {
|
if (task.task_status === 'COMPLETED') {
|
||||||
@ -317,6 +226,32 @@ export function useWorkflowData() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (task.task_name === 'generate_sketch' && task.task_result && task.task_result.data) {
|
||||||
|
let realSketchResultData = task.task_result.data.filter((item: any) => item.image_path);
|
||||||
|
if (task.task_status === 'COMPLETED') {
|
||||||
|
realSketchResultData = taskCurrent.scenes.data.filter((item: any) => item.status !== 0);
|
||||||
|
}
|
||||||
|
console.log('---look-realSketchResultData', realSketchResultData);
|
||||||
|
taskCurrent.scenes.total_count = task.task_result.total_count;
|
||||||
|
if (task.task_status !== 'COMPLETED' || taskCurrent.scenes.total_count !== realSketchResultData.length) {
|
||||||
|
taskCurrent.currentStage = 'scene';
|
||||||
|
// 正在生成草图中 替换 sketch 数据
|
||||||
|
const sketchList = [];
|
||||||
|
for (const sketch of task.task_result.data) {
|
||||||
|
sketchList.push({
|
||||||
|
url: sketch.image_path,
|
||||||
|
script: sketch.sketch_name,
|
||||||
|
status: sketch.image_path ? 1 : (task.task_status === 'COMPLETED' ? 2 : 0)
|
||||||
|
});
|
||||||
|
}
|
||||||
|
taskCurrent.scenes.data = sketchList;
|
||||||
|
if (task.task_status === 'COMPLETED') {
|
||||||
|
// 草图生成完成
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// debugger;
|
// debugger;
|
||||||
if (task.task_name === 'generate_shot_sketch' && task.task_result && task.task_result.data) {
|
if (task.task_name === 'generate_shot_sketch' && task.task_result && task.task_result.data) {
|
||||||
@ -485,27 +420,6 @@ export function useWorkflowData() {
|
|||||||
|
|
||||||
// 如果有已完成的数据,同步到状态
|
// 如果有已完成的数据,同步到状态
|
||||||
if (data) {
|
if (data) {
|
||||||
if (data.sketch && data.sketch.data) {
|
|
||||||
taskCurrent.currentStage = 'scene';
|
|
||||||
const realSketchResultData = data.sketch.data.filter((item: any) => item.image_path);
|
|
||||||
const sketchList = [];
|
|
||||||
for (const sketch of data.sketch.data) {
|
|
||||||
sketchList.push({
|
|
||||||
url: sketch.image_path,
|
|
||||||
script: sketch.sketch_name,
|
|
||||||
status: sketch.image_path ? 1 : (data.sketch.task_status === 'COMPLETED' ? 2 : 0)
|
|
||||||
});
|
|
||||||
}
|
|
||||||
taskCurrent.scenes.data = sketchList;
|
|
||||||
taskCurrent.scenes.total_count = data.sketch.total_count;
|
|
||||||
// 设置为最后一个草图
|
|
||||||
if (data.sketch.total_count > realSketchResultData.length) {
|
|
||||||
// 场景生成中
|
|
||||||
setIsGeneratingSketch(true);
|
|
||||||
} else {
|
|
||||||
// 场景生成完成
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (data.character && data.character.data && data.character.data.length > 0) {
|
if (data.character && data.character.data && data.character.data.length > 0) {
|
||||||
taskCurrent.currentStage = 'character';
|
taskCurrent.currentStage = 'character';
|
||||||
const characterList = [];
|
const characterList = [];
|
||||||
@ -524,6 +438,26 @@ export function useWorkflowData() {
|
|||||||
// 角色生成完成
|
// 角色生成完成
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if (data.sketch && data.sketch.data) {
|
||||||
|
taskCurrent.currentStage = 'scene';
|
||||||
|
const realSketchResultData = data.sketch.data.filter((item: any) => item.image_path);
|
||||||
|
const sketchList = [];
|
||||||
|
for (const sketch of data.sketch.data) {
|
||||||
|
sketchList.push({
|
||||||
|
url: sketch.image_path,
|
||||||
|
script: sketch.sketch_name,
|
||||||
|
status: sketch.image_path ? 1 : (data.sketch.task_status === 'COMPLETED' ? 2 : 0)
|
||||||
|
});
|
||||||
|
}
|
||||||
|
taskCurrent.scenes.data = sketchList;
|
||||||
|
taskCurrent.scenes.total_count = data.sketch.total_count;
|
||||||
|
// 设置为最后一个草图
|
||||||
|
if (data.sketch.total_count > realSketchResultData.length) {
|
||||||
|
// 场景生成中
|
||||||
|
} else {
|
||||||
|
// 场景生成完成
|
||||||
|
}
|
||||||
|
}
|
||||||
if (data.shot_sketch && data.shot_sketch.data) {
|
if (data.shot_sketch && data.shot_sketch.data) {
|
||||||
taskCurrent.currentStage = 'shot_sketch';
|
taskCurrent.currentStage = 'shot_sketch';
|
||||||
const realShotResultData = data.shot_sketch.data.filter((item: any) => item.url);
|
const realShotResultData = data.shot_sketch.data.filter((item: any) => item.url);
|
||||||
@ -629,17 +563,7 @@ export function useWorkflowData() {
|
|||||||
// 重试加载数据
|
// 重试加载数据
|
||||||
const retryLoadData = () => {
|
const retryLoadData = () => {
|
||||||
setDataLoadError(null);
|
setDataLoadError(null);
|
||||||
// 重置所有状态
|
|
||||||
setTaskSketch([]);
|
|
||||||
setTaskScenes([]);
|
|
||||||
setTaskVideos([]);
|
|
||||||
updateSketchCount(0);
|
|
||||||
updateVideoCount(0);
|
|
||||||
setRoles([]);
|
|
||||||
setMusic([]);
|
|
||||||
setFinal(null);
|
|
||||||
setCurrentSketchIndex(0);
|
setCurrentSketchIndex(0);
|
||||||
setCurrentStep('0');
|
|
||||||
// 重新初始化
|
// 重新初始化
|
||||||
initializeWorkflow();
|
initializeWorkflow();
|
||||||
};
|
};
|
||||||
@ -652,21 +576,9 @@ export function useWorkflowData() {
|
|||||||
return {
|
return {
|
||||||
taskObject,
|
taskObject,
|
||||||
scriptData,
|
scriptData,
|
||||||
taskSketch,
|
|
||||||
taskScenes,
|
|
||||||
taskShotSketch,
|
|
||||||
taskVideos,
|
|
||||||
sketchCount,
|
|
||||||
isLoading: state.isLoading,
|
isLoading: state.isLoading,
|
||||||
currentStep,
|
|
||||||
currentSketchIndex,
|
currentSketchIndex,
|
||||||
isGeneratingSketch,
|
|
||||||
isGeneratingVideo,
|
|
||||||
currentLoadingText,
|
currentLoadingText,
|
||||||
totalSketchCount,
|
|
||||||
roles,
|
|
||||||
music,
|
|
||||||
final,
|
|
||||||
dataLoadError,
|
dataLoadError,
|
||||||
setCurrentSketchIndex,
|
setCurrentSketchIndex,
|
||||||
retryLoadData,
|
retryLoadData,
|
||||||
|
|||||||
@ -11,30 +11,10 @@ import HorizontalScroller from './HorizontalScroller';
|
|||||||
import { useEditData } from '@/components/pages/work-flow/use-edit-data';
|
import { useEditData } from '@/components/pages/work-flow/use-edit-data';
|
||||||
import { useSearchParams } from 'next/navigation';
|
import { useSearchParams } from 'next/navigation';
|
||||||
import { RoleEntity } from '@/app/service/domain/Entities';
|
import { RoleEntity } from '@/app/service/domain/Entities';
|
||||||
|
import { Role } from '@/api/DTO/movieEdit';
|
||||||
interface Appearance {
|
|
||||||
hairStyle: string;
|
|
||||||
skinTone: string;
|
|
||||||
facialFeatures: string;
|
|
||||||
bodyType: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface Role {
|
|
||||||
name: string;
|
|
||||||
url: string;
|
|
||||||
sound: string;
|
|
||||||
soundDescription: string;
|
|
||||||
roleDescription: string;
|
|
||||||
age: number;
|
|
||||||
gender: 'male' | 'female' | 'other';
|
|
||||||
ethnicity: string;
|
|
||||||
appearance: Appearance;
|
|
||||||
// 新增标签数组
|
|
||||||
tags: string[];
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
interface CharacterTabContentProps {
|
interface CharacterTabContentProps {
|
||||||
|
originalRoles: Role[];
|
||||||
onClose: () => void;
|
onClose: () => void;
|
||||||
onApply: () => void;
|
onApply: () => void;
|
||||||
setActiveTab: (tabId: string) => void;
|
setActiveTab: (tabId: string) => void;
|
||||||
@ -42,10 +22,10 @@ interface CharacterTabContentProps {
|
|||||||
|
|
||||||
|
|
||||||
export const CharacterTabContent = forwardRef<
|
export const CharacterTabContent = forwardRef<
|
||||||
{ switchBefore: (tabId: string) => boolean, saveBefore: () => void },
|
{ switchBefore: (tabId: string) => boolean, saveOrCloseBefore: () => void },
|
||||||
CharacterTabContentProps
|
CharacterTabContentProps
|
||||||
>((props, ref) => {
|
>((props, ref) => {
|
||||||
const { onClose, onApply, setActiveTab } = props;
|
const { onClose, onApply, setActiveTab, originalRoles } = props;
|
||||||
const [isReplacePanelOpen, setIsReplacePanelOpen] = useState(false);
|
const [isReplacePanelOpen, setIsReplacePanelOpen] = useState(false);
|
||||||
const [replacePanelKey, setReplacePanelKey] = useState(0);
|
const [replacePanelKey, setReplacePanelKey] = useState(0);
|
||||||
const [ignoreReplace, setIgnoreReplace] = useState(false);
|
const [ignoreReplace, setIgnoreReplace] = useState(false);
|
||||||
@ -76,7 +56,6 @@ CharacterTabContentProps
|
|||||||
regenerateRole,
|
regenerateRole,
|
||||||
fetchUserRoleLibrary,
|
fetchUserRoleLibrary,
|
||||||
uploadImageAndUpdateRole,
|
uploadImageAndUpdateRole,
|
||||||
changeTabCallback,
|
|
||||||
// role shot
|
// role shot
|
||||||
shotSelectionList,
|
shotSelectionList,
|
||||||
fetchRoleShots,
|
fetchRoleShots,
|
||||||
@ -91,7 +70,8 @@ CharacterTabContentProps
|
|||||||
switchBefore: (tabId: string) => {
|
switchBefore: (tabId: string) => {
|
||||||
setNextToTabId(tabId);
|
setNextToTabId(tabId);
|
||||||
// 判断 角色是否修改
|
// 判断 角色是否修改
|
||||||
const isChange = selectedRole!.isChangeRole
|
const currentIndex = getCurrentIndex();
|
||||||
|
const isChange = currentIndex !== -1 && isRoleChange(originalRoles[currentIndex]);
|
||||||
console.log('switchBefore', isChange);
|
console.log('switchBefore', isChange);
|
||||||
if (isChange) {
|
if (isChange) {
|
||||||
setTriggerType('tab');
|
setTriggerType('tab');
|
||||||
@ -99,15 +79,16 @@ CharacterTabContentProps
|
|||||||
}
|
}
|
||||||
return isChange;
|
return isChange;
|
||||||
},
|
},
|
||||||
saveBefore: () => {
|
saveOrCloseBefore: () => {
|
||||||
console.log('saveBefore');
|
console.log('saveOrCloseBefore');
|
||||||
// 判断 角色是否修改
|
// 判断 角色是否修改
|
||||||
changeTabCallback((isChange: Boolean) => {
|
const currentIndex = getCurrentIndex();
|
||||||
if (isChange) {
|
if (currentIndex !== -1 && isRoleChange(originalRoles[currentIndex])) {
|
||||||
setTriggerType('apply');
|
setTriggerType('apply');
|
||||||
handleStartReplaceCharacter();
|
handleStartReplaceCharacter();
|
||||||
}
|
} else {
|
||||||
});
|
onClose();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}));
|
}));
|
||||||
|
|
||||||
@ -179,24 +160,33 @@ CharacterTabContentProps
|
|||||||
setIsReplacePanelOpen(false);
|
setIsReplacePanelOpen(false);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// 对比角色是否修改
|
||||||
|
const isRoleChange = (role: Role) => {
|
||||||
|
console.log('对比角色是否修改', role, selectedRole);
|
||||||
|
return role.name !== selectedRole?.name || role.url !== selectedRole?.imageUrl;
|
||||||
|
};
|
||||||
|
// 获取当前选中下标
|
||||||
|
const getCurrentIndex = () => {
|
||||||
|
return originalRoles.findIndex(role => role.name === selectedRole?.name);
|
||||||
|
};
|
||||||
|
|
||||||
const handleChangeRole = (index: number) => {
|
const handleChangeRole = (index: number) => {
|
||||||
const oldRole = roleData.find(role => role.id === selectedRole?.id);
|
|
||||||
console.log('切换角色前对比');
|
console.log('切换角色前对比');
|
||||||
changeTabCallback((isChange: Boolean) => {
|
const currentIndex = getCurrentIndex();
|
||||||
if (isChange) {
|
if (currentIndex === index) return;
|
||||||
setTriggerType('user');
|
if (currentIndex !== -1 && isRoleChange(originalRoles[currentIndex])) {
|
||||||
setIsRemindReplacePanelOpen(true);
|
setTriggerType('user');
|
||||||
setNextToUserIndex(index);
|
setIsRemindReplacePanelOpen(true);
|
||||||
return;
|
setNextToUserIndex(index);
|
||||||
}
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
// 重置替换规则
|
// 重置替换规则
|
||||||
setEnableAnimation(false);
|
setEnableAnimation(false);
|
||||||
setIgnoreReplace(false);
|
setIgnoreReplace(false);
|
||||||
setIsRegenerate(false);
|
setIsRegenerate(false);
|
||||||
|
|
||||||
selectRole(roleData[index]);
|
selectRole(roleData[index]);
|
||||||
});
|
|
||||||
};
|
};
|
||||||
|
|
||||||
// 从角色库中选择角色
|
// 从角色库中选择角色
|
||||||
@ -266,18 +256,8 @@ CharacterTabContentProps
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
// 如果loading 显示loading状态
|
|
||||||
if (loading) {
|
|
||||||
return (
|
|
||||||
<div className="flex flex-col items-center justify-center min-h-[400px] text-white/50">
|
|
||||||
<div className="w-12 h-12 mb-4 animate-spin rounded-full border-b-2 border-blue-600" />
|
|
||||||
<p>Loading...</p>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
// 如果没有角色数据,显示占位内容
|
// 如果没有角色数据,显示占位内容
|
||||||
if (roleData.length === 0) {
|
if (originalRoles.length === 0) {
|
||||||
return (
|
return (
|
||||||
<div className="flex flex-col items-center justify-center min-h-[400px] text-white/50">
|
<div className="flex flex-col items-center justify-center min-h-[400px] text-white/50">
|
||||||
<Users className="w-16 h-16 mb-4" />
|
<Users className="w-16 h-16 mb-4" />
|
||||||
@ -306,22 +286,22 @@ CharacterTabContentProps
|
|||||||
<HorizontalScroller
|
<HorizontalScroller
|
||||||
itemWidth={96}
|
itemWidth={96}
|
||||||
gap={0}
|
gap={0}
|
||||||
selectedIndex={roleData.findIndex(role => role.id === selectedRole?.id)}
|
selectedIndex={originalRoles?.findIndex(role => role.name === selectedRole?.name)}
|
||||||
onItemClick={(i: number) => handleChangeRole(i)}
|
onItemClick={(i: number) => handleChangeRole(i)}
|
||||||
>
|
>
|
||||||
{roleData.map((role, index) => (
|
{originalRoles.map((role, index) => (
|
||||||
<motion.div
|
<motion.div
|
||||||
key={`role-${index}`}
|
key={`role-${index}`}
|
||||||
className={cn(
|
className={cn(
|
||||||
'relative flex-shrink-0 w-24 rounded-lg overflow-hidden cursor-pointer',
|
'relative flex-shrink-0 w-24 rounded-lg overflow-hidden cursor-pointer',
|
||||||
'aspect-[9/16]',
|
'aspect-[9/16]',
|
||||||
role.id === selectedRole?.id ? 'ring-2 ring-blue-500' : 'hover:ring-2 hover:ring-blue-500/50'
|
role.name === selectedRole?.name ? 'ring-2 ring-blue-500' : 'hover:ring-2 hover:ring-blue-500/50'
|
||||||
)}
|
)}
|
||||||
whileHover={{ scale: 1.05 }}
|
whileHover={{ scale: 1.05 }}
|
||||||
whileTap={{ scale: 0.95 }}
|
whileTap={{ scale: 0.95 }}
|
||||||
>
|
>
|
||||||
<img
|
<img
|
||||||
src={role.imageUrl}
|
src={role.url}
|
||||||
alt={role.name}
|
alt={role.name}
|
||||||
className="w-full h-full object-cover"
|
className="w-full h-full object-cover"
|
||||||
/>
|
/>
|
||||||
@ -335,78 +315,83 @@ CharacterTabContentProps
|
|||||||
</motion.div>
|
</motion.div>
|
||||||
|
|
||||||
{/* 下部分:角色详情 */}
|
{/* 下部分:角色详情 */}
|
||||||
<motion.div
|
{ loading ? (
|
||||||
className="grid grid-cols-2 gap-6"
|
<div className="flex flex-col items-center justify-center min-h-[300px] text-white/50">
|
||||||
initial={{ opacity: 0, y: 20 }}
|
<Loader2 className="w-12 h-12 mb-4 animate-spin" />
|
||||||
animate={{ opacity: 1, y: 0 }}
|
<p>Loading...</p>
|
||||||
transition={{ delay: 0.2 }}
|
</div>
|
||||||
>
|
) : (
|
||||||
|
<motion.div
|
||||||
|
className="grid grid-cols-2 gap-6"
|
||||||
|
initial={{ opacity: 0, y: 20 }}
|
||||||
|
animate={{ opacity: 1, y: 0 }}
|
||||||
|
transition={{ delay: 0.2 }}
|
||||||
|
>
|
||||||
|
|
||||||
{/* 左列:角色预览 */}
|
{/* 左列:角色预览 */}
|
||||||
<div className="space-y-4">
|
<div className="space-y-4">
|
||||||
{/* 角色预览图 */}
|
{/* 角色预览图 */}
|
||||||
<div className="w-full h-full mx-auto rounded-lg relative group">
|
<div className="w-full h-full mx-auto rounded-lg relative group">
|
||||||
<ImageBlurTransition
|
<ImageBlurTransition
|
||||||
src={selectedRole?.imageUrl || ''}
|
src={selectedRole?.imageUrl || ''}
|
||||||
alt={selectedRole?.name || ''}
|
alt={selectedRole?.name || ''}
|
||||||
width='100%'
|
width='100%'
|
||||||
height='100%'
|
height='100%'
|
||||||
enableAnimation={enableAnimation}
|
enableAnimation={enableAnimation}
|
||||||
|
/>
|
||||||
|
{/* 应用角色按钮 */}
|
||||||
|
<div className='absolute top-3 right-3 flex gap-2'>
|
||||||
|
<motion.button
|
||||||
|
className="p-2 bg-black/50 hover:bg-black/70
|
||||||
|
text-white rounded-full backdrop-blur-sm transition-colors z-10"
|
||||||
|
whileHover={{ scale: 1.05 }}
|
||||||
|
whileTap={{ scale: 0.95 }}
|
||||||
|
onClick={handleUploadClick}
|
||||||
|
disabled={isUploading}
|
||||||
|
>
|
||||||
|
{isUploading ? <Loader2 className="w-4 h-4 animate-spin" /> : <ImageUp className="w-4 h-4" />}
|
||||||
|
</motion.button>
|
||||||
|
<motion.button
|
||||||
|
className="p-2 bg-black/50 hover:bg-black/70
|
||||||
|
text-white rounded-full backdrop-blur-sm transition-colors z-10"
|
||||||
|
whileHover={{ scale: 1.05 }}
|
||||||
|
whileTap={{ scale: 0.95 }}
|
||||||
|
onClick={() => handleOpenReplaceLibrary()}
|
||||||
|
>
|
||||||
|
<Library className="w-4 h-4" />
|
||||||
|
</motion.button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
{/* 右列:角色信息 */}
|
||||||
|
<div className="space-y-4">
|
||||||
|
<CharacterEditor
|
||||||
|
ref={characterEditorRef}
|
||||||
|
className="min-h-[calc(100%-4rem)]"
|
||||||
|
description={selectedRole?.generateText || ''}
|
||||||
|
highlight={selectedRole?.tags || []}
|
||||||
|
onSmartPolish={handleSmartPolish}
|
||||||
|
onUpdateText={(text: string) => updateRoleText(text)}
|
||||||
/>
|
/>
|
||||||
{/* 应用角色按钮 */}
|
{/* 重新生成按钮、替换形象按钮 */}
|
||||||
<div className='absolute top-3 right-3 flex gap-2'>
|
<div className="grid grid-cols-1 gap-2">
|
||||||
<motion.button
|
<motion.button
|
||||||
className="p-2 bg-black/50 hover:bg-black/70
|
onClick={() => handleRegenerate()}
|
||||||
text-white rounded-full backdrop-blur-sm transition-colors z-10"
|
className="flex items-center justify-center gap-2 px-4 py-3 bg-blue-500/10 hover:bg-blue-500/20
|
||||||
whileHover={{ scale: 1.05 }}
|
text-blue-500 rounded-lg transition-colors disabled:opacity-50 disabled:cursor-not-allowed"
|
||||||
whileTap={{ scale: 0.95 }}
|
whileHover={{ scale: 1.02 }}
|
||||||
onClick={handleUploadClick}
|
whileTap={{ scale: 0.98 }}
|
||||||
disabled={isUploading}
|
disabled={isRegenerate}
|
||||||
>
|
>
|
||||||
{isUploading ? <Loader2 className="w-4 h-4 animate-spin" /> : <ImageUp className="w-4 h-4" />}
|
<RefreshCw className="w-4 h-4" />
|
||||||
</motion.button>
|
<span>{isRegenerate ? 'Regenerating...' : 'Regenerate'}</span>
|
||||||
<motion.button
|
|
||||||
className="p-2 bg-black/50 hover:bg-black/70
|
|
||||||
text-white rounded-full backdrop-blur-sm transition-colors z-10"
|
|
||||||
whileHover={{ scale: 1.05 }}
|
|
||||||
whileTap={{ scale: 0.95 }}
|
|
||||||
onClick={() => handleOpenReplaceLibrary()}
|
|
||||||
>
|
|
||||||
<Library className="w-4 h-4" />
|
|
||||||
</motion.button>
|
</motion.button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
</motion.div>
|
||||||
</div>
|
)}
|
||||||
{/* 右列:角色信息 */}
|
|
||||||
<div className="space-y-4">
|
|
||||||
<CharacterEditor
|
|
||||||
ref={characterEditorRef}
|
|
||||||
className="min-h-[calc(100%-4rem)]"
|
|
||||||
description={selectedRole?.generateText || ''}
|
|
||||||
highlight={selectedRole?.tags || []}
|
|
||||||
onSmartPolish={handleSmartPolish}
|
|
||||||
onUpdateText={(text: string) => updateRoleText(text)}
|
|
||||||
/>
|
|
||||||
{/* 重新生成按钮、替换形象按钮 */}
|
|
||||||
<div className="grid grid-cols-1 gap-2">
|
|
||||||
<motion.button
|
|
||||||
onClick={() => 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}
|
|
||||||
>
|
|
||||||
<RefreshCw className="w-4 h-4" />
|
|
||||||
<span>{isRegenerate ? 'Regenerating...' : 'Regenerate'}</span>
|
|
||||||
</motion.button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
</motion.div>
|
|
||||||
|
|
||||||
<FloatingGlassPanel
|
<FloatingGlassPanel
|
||||||
open={isReplacePanelOpen}
|
open={isReplacePanelOpen}
|
||||||
|
|||||||
@ -12,6 +12,7 @@ import { CharacterTabContent } from './character-tab-content';
|
|||||||
import { MusicTabContent } from './music-tab-content';
|
import { MusicTabContent } from './music-tab-content';
|
||||||
import FloatingGlassPanel from './FloatingGlassPanel';
|
import FloatingGlassPanel from './FloatingGlassPanel';
|
||||||
import { SaveEditUseCase } from '@/app/service/usecase/SaveEditUseCase';
|
import { SaveEditUseCase } from '@/app/service/usecase/SaveEditUseCase';
|
||||||
|
import { TaskObject } from '@/api/DTO/movieEdit';
|
||||||
|
|
||||||
interface EditModalProps {
|
interface EditModalProps {
|
||||||
isOpen: boolean;
|
isOpen: boolean;
|
||||||
@ -23,6 +24,7 @@ interface EditModalProps {
|
|||||||
isPauseWorkFlow: boolean;
|
isPauseWorkFlow: boolean;
|
||||||
fallbackToStep: any;
|
fallbackToStep: any;
|
||||||
originalText?: string;
|
originalText?: string;
|
||||||
|
taskObject?: TaskObject;
|
||||||
}
|
}
|
||||||
|
|
||||||
const tabs = [
|
const tabs = [
|
||||||
@ -44,7 +46,8 @@ export function EditModal({
|
|||||||
setIsPauseWorkFlow,
|
setIsPauseWorkFlow,
|
||||||
isPauseWorkFlow,
|
isPauseWorkFlow,
|
||||||
fallbackToStep,
|
fallbackToStep,
|
||||||
originalText
|
originalText,
|
||||||
|
taskObject
|
||||||
}: EditModalProps) {
|
}: EditModalProps) {
|
||||||
const [activeTab, setActiveTab] = useState(activeEditTab);
|
const [activeTab, setActiveTab] = useState(activeEditTab);
|
||||||
const [currentIndex, setCurrentIndex] = useState(currentSketchIndex);
|
const [currentIndex, setCurrentIndex] = useState(currentSketchIndex);
|
||||||
@ -55,6 +58,7 @@ export function EditModal({
|
|||||||
const [remindFallbackText, setRemindFallbackText] = useState('The task will be regenerated and edited. Do you want to continue?');
|
const [remindFallbackText, setRemindFallbackText] = useState('The task will be regenerated and edited. Do you want to continue?');
|
||||||
const scriptTabContentRef = useRef<any>(null);
|
const scriptTabContentRef = useRef<any>(null);
|
||||||
const characterTabContentRef = useRef<any>(null);
|
const characterTabContentRef = useRef<any>(null);
|
||||||
|
const shotTabContentRef = useRef<any>(null);
|
||||||
// 添加一个状态来标记是否是从切换tab触发的提醒
|
// 添加一个状态来标记是否是从切换tab触发的提醒
|
||||||
const [pendingSwitchTabId, setPendingSwitchTabId] = useState<string | null>(null);
|
const [pendingSwitchTabId, setPendingSwitchTabId] = useState<string | null>(null);
|
||||||
const [disabledBtn, setDisabledBtn] = useState(false);
|
const [disabledBtn, setDisabledBtn] = useState(false);
|
||||||
@ -72,9 +76,9 @@ export function EditModal({
|
|||||||
const isTabDisabled = (tabId: string) => {
|
const isTabDisabled = (tabId: string) => {
|
||||||
if (tabId === 'settings') return false;
|
if (tabId === 'settings') return false;
|
||||||
// 换成 如果对应标签下 数据存在 就不禁用
|
// 换成 如果对应标签下 数据存在 就不禁用
|
||||||
// if (tabId === '1') return roles.length === 0;
|
if (tabId === '1') return taskObject?.roles.data.length === 0;
|
||||||
// if (tabId === '2') return taskScenes.length === 0;
|
// if (tabId === '2') return taskScenes.length === 0;
|
||||||
// if (tabId === '3') return sketchVideo.length === 0;
|
if (tabId === '3') return taskObject?.videos.data.length === 0;
|
||||||
if (tabId === '4') return false;
|
if (tabId === '4') return false;
|
||||||
return false;
|
return false;
|
||||||
};
|
};
|
||||||
@ -98,6 +102,11 @@ export function EditModal({
|
|||||||
if (characterTabContent) {
|
if (characterTabContent) {
|
||||||
return characterTabContent.switchBefore(tabId);
|
return characterTabContent.switchBefore(tabId);
|
||||||
}
|
}
|
||||||
|
} else if (activeTab === '3') {
|
||||||
|
const shotTabContent = shotTabContentRef.current;
|
||||||
|
if (shotTabContent) {
|
||||||
|
return shotTabContent.switchBefore(tabId);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
@ -117,11 +126,11 @@ export function EditModal({
|
|||||||
console.log('handleSave');
|
console.log('handleSave');
|
||||||
// setIsRemindFallbackOpen(true);
|
// setIsRemindFallbackOpen(true);
|
||||||
if (activeTab === '0') {
|
if (activeTab === '0') {
|
||||||
scriptTabContentRef.current.saveBefore();
|
scriptTabContentRef.current.saveOrCloseBefore();
|
||||||
} else if (activeTab === '1') {
|
} else if (activeTab === '1') {
|
||||||
characterTabContentRef.current.saveBefore();
|
characterTabContentRef.current.saveOrCloseBefore();
|
||||||
} else if (activeTab === '3') {
|
} else if (activeTab === '3') {
|
||||||
handleConfirmGotoFallback();
|
shotTabContentRef.current.saveOrCloseBefore('apply');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -170,7 +179,13 @@ export function EditModal({
|
|||||||
const handleClickClose = () => {
|
const handleClickClose = () => {
|
||||||
// TODO 关闭前 检查 当前tab 下是否有更新 如果有更新 则提醒用户 是否确认应用
|
// TODO 关闭前 检查 当前tab 下是否有更新 如果有更新 则提醒用户 是否确认应用
|
||||||
// 暂时 默认弹出提醒
|
// 暂时 默认弹出提醒
|
||||||
setIsRemindCloseOpen(true);
|
if (activeTab === '0') {
|
||||||
|
scriptTabContentRef.current.saveOrCloseBefore();
|
||||||
|
} else if (activeTab === '1') {
|
||||||
|
characterTabContentRef.current.saveOrCloseBefore();
|
||||||
|
} else if (activeTab === '3') {
|
||||||
|
shotTabContentRef.current.saveOrCloseBefore('close');
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const handleConfirmApply = () => {
|
const handleConfirmApply = () => {
|
||||||
@ -195,6 +210,7 @@ export function EditModal({
|
|||||||
originalText={originalText}
|
originalText={originalText}
|
||||||
onApply={handleApply}
|
onApply={handleApply}
|
||||||
setActiveTab={setActiveTab}
|
setActiveTab={setActiveTab}
|
||||||
|
onClose={onClose}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
case '1':
|
case '1':
|
||||||
@ -204,6 +220,7 @@ export function EditModal({
|
|||||||
onClose={onClose}
|
onClose={onClose}
|
||||||
onApply={handleApply}
|
onApply={handleApply}
|
||||||
setActiveTab={setActiveTab}
|
setActiveTab={setActiveTab}
|
||||||
|
originalRoles={taskObject?.roles.data || []}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
case '2':
|
case '2':
|
||||||
@ -215,9 +232,12 @@ export function EditModal({
|
|||||||
case '3':
|
case '3':
|
||||||
return (
|
return (
|
||||||
<ShotTabContent
|
<ShotTabContent
|
||||||
|
ref={shotTabContentRef}
|
||||||
|
originalVideos={taskObject?.videos.data || []}
|
||||||
currentSketchIndex={currentIndex}
|
currentSketchIndex={currentIndex}
|
||||||
roles={roles}
|
|
||||||
onApply={handleApply}
|
onApply={handleApply}
|
||||||
|
onClose={onClose}
|
||||||
|
setActiveTab={setActiveTab}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
case '4':
|
case '4':
|
||||||
|
|||||||
@ -12,13 +12,14 @@ interface ScriptTabContentProps {
|
|||||||
originalText?: string;
|
originalText?: string;
|
||||||
onApply: () => void;
|
onApply: () => void;
|
||||||
setActiveTab: (tabId: string) => void;
|
setActiveTab: (tabId: string) => void;
|
||||||
|
onClose: () => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const ScriptTabContent = forwardRef<
|
export const ScriptTabContent = forwardRef<
|
||||||
{ switchBefore: (tabId: string) => boolean, saveBefore: () => void },
|
{ switchBefore: (tabId: string) => boolean, saveOrCloseBefore: () => void },
|
||||||
ScriptTabContentProps
|
ScriptTabContentProps
|
||||||
>((props, ref) => {
|
>((props, ref) => {
|
||||||
const { setIsPauseWorkFlow, isPauseWorkFlow, originalText, onApply, setActiveTab } = props;
|
const { setIsPauseWorkFlow, isPauseWorkFlow, originalText, onApply, setActiveTab, onClose } = props;
|
||||||
const { loading, scriptData, setAnyAttribute, applyScript } = useEditData('script', originalText);
|
const { loading, scriptData, setAnyAttribute, applyScript } = useEditData('script', originalText);
|
||||||
|
|
||||||
const [isUpdate, setIsUpdate] = useState(false);
|
const [isUpdate, setIsUpdate] = useState(false);
|
||||||
@ -39,10 +40,12 @@ export const ScriptTabContent = forwardRef<
|
|||||||
}
|
}
|
||||||
return isUpdate;
|
return isUpdate;
|
||||||
},
|
},
|
||||||
saveBefore: () => {
|
saveOrCloseBefore: () => {
|
||||||
console.log('saveBefore');
|
console.log('saveOrCloseBefore');
|
||||||
if (isUpdate) {
|
if (isUpdate) {
|
||||||
onApply();
|
onApply();
|
||||||
|
} else {
|
||||||
|
onClose();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}));
|
}));
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
import React, { useRef, useEffect, useState } from 'react';
|
import React, { useRef, useEffect, useState, forwardRef } from 'react';
|
||||||
import { motion, AnimatePresence } from 'framer-motion';
|
import { motion, AnimatePresence } from 'framer-motion';
|
||||||
import { RefreshCw, User, Loader2, X, Plus, Video, CircleX } from 'lucide-react';
|
import { RefreshCw, User, Loader2, X, Plus, Video, CircleX } from 'lucide-react';
|
||||||
import { cn } from '@/public/lib/utils';
|
import { cn } from '@/public/lib/utils';
|
||||||
@ -11,19 +11,26 @@ import FloatingGlassPanel from './FloatingGlassPanel';
|
|||||||
import { ReplaceCharacterPanel } from './replace-character-panel';
|
import { ReplaceCharacterPanel } from './replace-character-panel';
|
||||||
import HorizontalScroller from './HorizontalScroller';
|
import HorizontalScroller from './HorizontalScroller';
|
||||||
import { useEditData } from '@/components/pages/work-flow/use-edit-data';
|
import { useEditData } from '@/components/pages/work-flow/use-edit-data';
|
||||||
import { RoleEntity } from '@/app/service/domain/Entities';
|
import { RoleEntity, VideoSegmentEntity } from '@/app/service/domain/Entities';
|
||||||
|
import { ShotVideo } from '@/api/DTO/movieEdit';
|
||||||
|
|
||||||
interface ShotTabContentProps {
|
interface ShotTabContentProps {
|
||||||
currentSketchIndex: number;
|
currentSketchIndex: number;
|
||||||
roles?: any[];
|
originalVideos: ShotVideo[];
|
||||||
onApply: () => void;
|
onApply: () => void;
|
||||||
|
onClose: () => void;
|
||||||
|
setActiveTab: (tabId: string) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const ShotTabContent = (props: ShotTabContentProps) => {
|
export const ShotTabContent = forwardRef<
|
||||||
const { currentSketchIndex = 0, roles = [], onApply } = props;
|
{ switchBefore: (tabId: string) => boolean, saveOrCloseBefore: (type: 'apply' | 'close') => void },
|
||||||
|
ShotTabContentProps
|
||||||
|
>((props, ref) => {
|
||||||
|
const { currentSketchIndex = 0, onApply, onClose, originalVideos, setActiveTab } = props;
|
||||||
const {
|
const {
|
||||||
loading,
|
loading,
|
||||||
shotData,
|
shotData,
|
||||||
|
selectedSegment,
|
||||||
scriptRoles,
|
scriptRoles,
|
||||||
setSelectedSegment,
|
setSelectedSegment,
|
||||||
regenerateVideoSegment,
|
regenerateVideoSegment,
|
||||||
@ -36,8 +43,6 @@ export const ShotTabContent = (props: ShotTabContentProps) => {
|
|||||||
calculateRecognitionBoxes,
|
calculateRecognitionBoxes,
|
||||||
setSelectedRole
|
setSelectedRole
|
||||||
} = useEditData('shot');
|
} = useEditData('shot');
|
||||||
const [selectedIndex, setSelectedIndex] = useState(currentSketchIndex);
|
|
||||||
|
|
||||||
const [detections, setDetections] = useState<PersonDetection[]>([]);
|
const [detections, setDetections] = useState<PersonDetection[]>([]);
|
||||||
const [scanState, setScanState] = useState<'idle' | 'scanning' | 'detected' | 'failed' | 'timeout'>('idle');
|
const [scanState, setScanState] = useState<'idle' | 'scanning' | 'detected' | 'failed' | 'timeout'>('idle');
|
||||||
const [isScanFailed, setIsScanFailed] = useState(false);
|
const [isScanFailed, setIsScanFailed] = useState(false);
|
||||||
@ -51,13 +56,54 @@ export const ShotTabContent = (props: ShotTabContentProps) => {
|
|||||||
const [isRegenerate, setIsRegenerate] = useState(false);
|
const [isRegenerate, setIsRegenerate] = useState(false);
|
||||||
const [pendingRegeneration, setPendingRegeneration] = useState(false);
|
const [pendingRegeneration, setPendingRegeneration] = useState(false);
|
||||||
|
|
||||||
|
const [isInitialized, setIsInitialized] = useState(true);
|
||||||
|
const [triggerType, setTriggerType] = useState<'tab' | 'apply' | 'close'>('tab');
|
||||||
|
const [nextToTabId, setNextToTabId] = useState<string>('');
|
||||||
|
const [isRemindApplyUpdate, setIsRemindApplyUpdate] = useState(false);
|
||||||
|
const [updateData, setUpdateData] = useState<VideoSegmentEntity[]>([]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
console.log('shotTabContent-----shotData', shotData);
|
console.log('shotTabContent-----shotData', shotData);
|
||||||
}, [shotData]);
|
}, [shotData]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
console.log('-==========shotData===========-', shotData);
|
||||||
|
// 只在初始化且有角色数据时执行
|
||||||
|
if (isInitialized && shotData.length > 0) {
|
||||||
|
setIsInitialized(false);
|
||||||
|
setSelectedSegment(shotData[0]);
|
||||||
|
}
|
||||||
|
}, [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(() => {
|
useEffect(() => {
|
||||||
if (pendingRegeneration) {
|
if (pendingRegeneration) {
|
||||||
console.log('pendingRegeneration', pendingRegeneration, shotData[selectedIndex]?.lens);
|
console.log('pendingRegeneration', pendingRegeneration, selectedSegment?.lens);
|
||||||
regenerateVideoSegment().then(() => {
|
regenerateVideoSegment().then(() => {
|
||||||
setPendingRegeneration(false);
|
setPendingRegeneration(false);
|
||||||
setIsRegenerate(false);
|
setIsRegenerate(false);
|
||||||
@ -65,16 +111,35 @@ export const ShotTabContent = (props: ShotTabContentProps) => {
|
|||||||
}
|
}
|
||||||
}, [pendingRegeneration]);
|
}, [pendingRegeneration]);
|
||||||
|
|
||||||
// 监听当前选中index变化
|
// 监听当前选中segment变化
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
console.log('shotTabContent-----shotData', shotData);
|
console.log('shotTabContent-----shotData', shotData);
|
||||||
if (shotData.length > 0) {
|
if (shotData.length > 0 && !selectedSegment) {
|
||||||
// 清空检测状态 和 检测结果
|
// 清空检测状态 和 检测结果
|
||||||
setScanState('idle');
|
setScanState('idle');
|
||||||
setDetections([]);
|
setDetections([]);
|
||||||
setSelectedSegment(shotData[selectedIndex]);
|
setSelectedSegment(shotData[0]);
|
||||||
}
|
}
|
||||||
}, [selectedIndex, shotData]);
|
}, [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 () => {
|
const handleScan = async () => {
|
||||||
@ -174,16 +239,34 @@ export const ShotTabContent = (props: ShotTabContentProps) => {
|
|||||||
onApply();
|
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 () => {
|
const handleRegenerate = async () => {
|
||||||
console.log('regenerate');
|
console.log('regenerate');
|
||||||
setIsRegenerate(true);
|
setIsRegenerate(true);
|
||||||
const shotInfo = shotsEditorRef.current.getShotInfo();
|
const shotInfo = shotsEditorRef.current.getShotInfo();
|
||||||
console.log('shotInfo', shotInfo);
|
console.log('shotInfo', shotInfo);
|
||||||
setSelectedSegment({
|
if (selectedSegment) {
|
||||||
...shotData[selectedIndex],
|
setSelectedSegment({
|
||||||
lens: shotInfo
|
...selectedSegment,
|
||||||
});
|
lens: shotInfo
|
||||||
|
});
|
||||||
|
}
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
setPendingRegeneration(true);
|
setPendingRegeneration(true);
|
||||||
}, 1000);
|
}, 1000);
|
||||||
@ -197,22 +280,20 @@ export const ShotTabContent = (props: ShotTabContentProps) => {
|
|||||||
|
|
||||||
// 切换选择分镜
|
// 切换选择分镜
|
||||||
const handleSelectShot = (index: number) => {
|
const handleSelectShot = (index: number) => {
|
||||||
// 切换前 判断数据是否发生变化
|
// 通过 video_id 找到对应的分镜
|
||||||
setSelectedIndex(index);
|
const selectedVideo = originalVideos[index];
|
||||||
|
const targetSegment = shotData.find(shot =>
|
||||||
|
shot.videoUrl.some(url => url.video_id === selectedVideo.video_id)
|
||||||
|
);
|
||||||
|
if (targetSegment) {
|
||||||
|
setSelectedSegment(targetSegment);
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// 如果loading 显示loading状态
|
|
||||||
if (loading) {
|
|
||||||
return (
|
|
||||||
<div className="flex flex-col items-center justify-center min-h-[400px] text-white/50">
|
|
||||||
<div className="w-12 h-12 mb-4 animate-spin rounded-full border-b-2 border-blue-600" />
|
|
||||||
<p>Loading...</p>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
// 如果没有数据,显示空状态
|
// 如果没有数据,显示空状态
|
||||||
if (shotData.length === 0) {
|
if (originalVideos.length === 0) {
|
||||||
return (
|
return (
|
||||||
<div className="flex flex-col items-center justify-center min-h-[400px] text-white/50">
|
<div className="flex flex-col items-center justify-center min-h-[400px] text-white/50">
|
||||||
<Video className="w-16 h-16 mb-4" />
|
<Video className="w-16 h-16 mb-4" />
|
||||||
@ -233,28 +314,23 @@ export const ShotTabContent = (props: ShotTabContentProps) => {
|
|||||||
<HorizontalScroller
|
<HorizontalScroller
|
||||||
itemWidth={128}
|
itemWidth={128}
|
||||||
gap={0}
|
gap={0}
|
||||||
selectedIndex={selectedIndex}
|
selectedIndex={originalVideos.findIndex(shot => selectedSegment?.videoUrl.some(url => url.video_id === shot.video_id))}
|
||||||
onItemClick={(i: number) => handleSelectShot(i)}
|
onItemClick={(i: number) => handleSelectShot(i)}
|
||||||
>
|
>
|
||||||
{shotData.map((shot, index) => (
|
{originalVideos.map((shot, index) => (
|
||||||
<motion.div
|
<motion.div
|
||||||
key={shot.id || index}
|
key={shot.video_id || index}
|
||||||
className={cn(
|
className={cn(
|
||||||
'relative flex-shrink-0 w-32 aspect-video rounded-lg overflow-hidden cursor-pointer group',
|
'relative flex-shrink-0 w-32 aspect-video rounded-lg overflow-hidden cursor-pointer group',
|
||||||
selectedIndex === index ? 'ring-2 ring-blue-500' : 'hover:ring-2 hover:ring-blue-500/50'
|
selectedSegment?.videoUrl.some(url => url.video_id === shot.video_id) ? 'ring-2 ring-blue-500' : 'hover:ring-2 hover:ring-blue-500/50'
|
||||||
)}
|
)}
|
||||||
whileHover={{ scale: 1.02 }}
|
whileHover={{ scale: 1.02 }}
|
||||||
whileTap={{ scale: 0.98 }}
|
whileTap={{ scale: 0.98 }}
|
||||||
>
|
>
|
||||||
{(shot.status === 0 || shot.videoUrl.length === 0) && (
|
{shot.urls.length > 0 && (
|
||||||
<div className="w-full h-full flex items-center justify-center">
|
|
||||||
<Loader2 className="w-4 h-4 animate-spin text-blue-500" />
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
{shot.status === 1 && shot.videoUrl[0] && (
|
|
||||||
<video
|
<video
|
||||||
src={shot.videoUrl[0].video_url}
|
src={shot.urls[0]}
|
||||||
key={shot.videoUrl[0].video_url}
|
key={shot.urls[0]}
|
||||||
className="w-full h-full object-cover"
|
className="w-full h-full object-cover"
|
||||||
muted
|
muted
|
||||||
loop
|
loop
|
||||||
@ -263,12 +339,6 @@ export const ShotTabContent = (props: ShotTabContentProps) => {
|
|||||||
onMouseLeave={(e) => e.currentTarget.pause()}
|
onMouseLeave={(e) => e.currentTarget.pause()}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
{/* 任务失败 */}
|
|
||||||
{shot.status === 2 && (
|
|
||||||
<div className="w-full h-full flex items-center justify-center bg-red-500/10">
|
|
||||||
<CircleX className="w-4 h-4 text-red-500" />
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
<div className="absolute bottom-0 left-0 right-0 p-1 bg-gradient-to-t from-black/60 to-transparent">
|
<div className="absolute bottom-0 left-0 right-0 p-1 bg-gradient-to-t from-black/60 to-transparent">
|
||||||
<span className="text-xs text-white/90">{index + 1}</span>
|
<span className="text-xs text-white/90">{index + 1}</span>
|
||||||
</div>
|
</div>
|
||||||
@ -291,14 +361,14 @@ export const ShotTabContent = (props: ShotTabContentProps) => {
|
|||||||
<HorizontalScroller
|
<HorizontalScroller
|
||||||
itemWidth={'auto'}
|
itemWidth={'auto'}
|
||||||
gap={0}
|
gap={0}
|
||||||
selectedIndex={selectedIndex}
|
selectedIndex={originalVideos.findIndex(shot => selectedSegment?.videoUrl.some(url => url.video_id === shot.video_id))}
|
||||||
onItemClick={(i: number) => handleSelectShot(i)}
|
onItemClick={(i: number) => handleSelectShot(i)}
|
||||||
>
|
>
|
||||||
{shotData.map((shot, index) => {
|
{originalVideos.map((shot, index) => {
|
||||||
const isActive = selectedIndex === index;
|
const isActive = selectedSegment?.videoUrl.some(url => url.video_id === shot.video_id);
|
||||||
return (
|
return (
|
||||||
<motion.div
|
<motion.div
|
||||||
key={shot.id || index}
|
key={shot.video_id || index}
|
||||||
className={cn(
|
className={cn(
|
||||||
'flex-shrink-0 cursor-pointer transition-all duration-300',
|
'flex-shrink-0 cursor-pointer transition-all duration-300',
|
||||||
isActive ? 'text-white' : 'text-white/50 hover:text-white/80'
|
isActive ? 'text-white' : 'text-white/50 hover:text-white/80'
|
||||||
@ -311,14 +381,8 @@ export const ShotTabContent = (props: ShotTabContentProps) => {
|
|||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<span className="text-sm whitespace-nowrap flex items-center gap-1">
|
<span className="text-sm whitespace-nowrap flex items-center gap-1">
|
||||||
<span>Segment {index + 1}</span>
|
<span>Segment {index + 1}</span>
|
||||||
{shot.status === 0 && (
|
|
||||||
<Loader2 className="w-4 h-4 animate-spin text-blue-500" />
|
|
||||||
)}
|
|
||||||
{shot.status === 2 && (
|
|
||||||
<CircleX className="w-4 h-4 text-red-500" />
|
|
||||||
)}
|
|
||||||
</span>
|
</span>
|
||||||
{index < shotData.length - 1 && (
|
{index < originalVideos.length - 1 && (
|
||||||
<span className="text-white/20">|</span>
|
<span className="text-white/20">|</span>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
@ -335,7 +399,12 @@ export const ShotTabContent = (props: ShotTabContentProps) => {
|
|||||||
|
|
||||||
|
|
||||||
{/* 下部分 */}
|
{/* 下部分 */}
|
||||||
{shotData[selectedIndex] && (
|
{loading ? (
|
||||||
|
<div className="flex flex-col items-center justify-center min-h-[400px] text-white/50">
|
||||||
|
<div className="w-12 h-12 mb-4 animate-spin rounded-full border-b-2 border-blue-600" />
|
||||||
|
<p>Loading...</p>
|
||||||
|
</div>
|
||||||
|
) : selectedSegment && (
|
||||||
<motion.div
|
<motion.div
|
||||||
className="grid grid-cols-2 gap-4 w-full"
|
className="grid grid-cols-2 gap-4 w-full"
|
||||||
initial={{ opacity: 0, y: 20 }}
|
initial={{ opacity: 0, y: 20 }}
|
||||||
@ -346,23 +415,23 @@ export const ShotTabContent = (props: ShotTabContentProps) => {
|
|||||||
<div className="space-y-4 col-span-1">
|
<div className="space-y-4 col-span-1">
|
||||||
{/* 选中的视频预览 */}
|
{/* 选中的视频预览 */}
|
||||||
<>
|
<>
|
||||||
{(shotData[selectedIndex]?.status === 0) && (
|
{(selectedSegment?.status === 0) && (
|
||||||
<div className="w-full h-full flex items-center gap-1 justify-center rounded-lg bg-black/30">
|
<div className="w-full h-full flex items-center gap-1 justify-center rounded-lg bg-black/30">
|
||||||
<Loader2 className="w-4 h-4 animate-spin text-blue-500" />
|
<Loader2 className="w-4 h-4 animate-spin text-blue-500" />
|
||||||
<span className="text-white/50">Loading...</span>
|
<span className="text-white/50">Loading...</span>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
<AnimatePresence mode="wait">
|
<AnimatePresence mode="wait">
|
||||||
{shotData[selectedIndex]?.status === 1 && shotData[selectedIndex]?.videoUrl.length && (
|
{selectedSegment?.status === 1 && selectedSegment?.videoUrl.length && (
|
||||||
<motion.div
|
<motion.div
|
||||||
className="aspect-video rounded-lg overflow-hidden relative group"
|
className="aspect-video rounded-lg overflow-hidden relative group"
|
||||||
key={`video-preview-${selectedIndex}`}
|
key={`video-preview-${selectedSegment?.id}`}
|
||||||
initial={{ opacity: 0 }}
|
initial={{ opacity: 0 }}
|
||||||
animate={{ opacity: 1 }}
|
animate={{ opacity: 1 }}
|
||||||
exit={{ opacity: 0 }}
|
exit={{ opacity: 0 }}
|
||||||
>
|
>
|
||||||
<PersonDetectionScene
|
<PersonDetectionScene
|
||||||
videoSrc={shotData[selectedIndex]?.videoUrl[0].video_url}
|
videoSrc={selectedSegment?.videoUrl[0].video_url}
|
||||||
detections={detections}
|
detections={detections}
|
||||||
scanState={scanState}
|
scanState={scanState}
|
||||||
triggerScan={scanState === 'scanning'}
|
triggerScan={scanState === 'scanning'}
|
||||||
@ -396,7 +465,7 @@ export const ShotTabContent = (props: ShotTabContentProps) => {
|
|||||||
</motion.div>
|
</motion.div>
|
||||||
)}
|
)}
|
||||||
</AnimatePresence>
|
</AnimatePresence>
|
||||||
{(shotData[selectedIndex]?.status === 2) && (
|
{(selectedSegment?.status === 2) && (
|
||||||
<div className="w-full h-full flex gap-1 items-center justify-center rounded-lg bg-red-500/10">
|
<div className="w-full h-full flex gap-1 items-center justify-center rounded-lg bg-red-500/10">
|
||||||
<CircleX className="w-4 h-4 text-red-500" />
|
<CircleX className="w-4 h-4 text-red-500" />
|
||||||
<span className="text-white/50">Failed, click to regenerate</span>
|
<span className="text-white/50">Failed, click to regenerate</span>
|
||||||
@ -406,11 +475,11 @@ export const ShotTabContent = (props: ShotTabContentProps) => {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* 基础配置 */}
|
{/* 基础配置 */}
|
||||||
<div className='space-y-4 col-span-1' key={selectedIndex}>
|
<div className='space-y-4 col-span-1' key={selectedSegment?.id}>
|
||||||
<ShotsEditor
|
<ShotsEditor
|
||||||
ref={shotsEditorRef}
|
ref={shotsEditorRef}
|
||||||
roles={scriptRoles}
|
roles={scriptRoles}
|
||||||
shotInfo={shotData[selectedIndex].lens}
|
shotInfo={selectedSegment.lens}
|
||||||
style={{height: 'calc(100% - 4rem)'}}
|
style={{height: 'calc(100% - 4rem)'}}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
@ -466,6 +535,79 @@ export const ShotTabContent = (props: ShotTabContentProps) => {
|
|||||||
setIsReplaceLibraryOpen={setIsReplaceLibraryOpen}
|
setIsReplaceLibraryOpen={setIsReplaceLibraryOpen}
|
||||||
onSelect={handleSelectCharacter}
|
onSelect={handleSelectCharacter}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
<FloatingGlassPanel
|
||||||
|
open={isRemindApplyUpdate}
|
||||||
|
width='66vw'
|
||||||
|
onClose={() => setIsRemindApplyUpdate(false)}
|
||||||
|
>
|
||||||
|
<div className='h-full w-full flex flex-col'>
|
||||||
|
{/* 提示文字 有分镜被修改 是否需要应用 */}
|
||||||
|
<div className='flex-1 flex items-center justify-center'>
|
||||||
|
<span className='text-white/50'>Three are some segments have been modified, do you need to apply?</span>
|
||||||
|
</div>
|
||||||
|
{/* 已修改分镜列表 */}
|
||||||
|
<HorizontalScroller
|
||||||
|
itemWidth={128}
|
||||||
|
gap={0}
|
||||||
|
>
|
||||||
|
{updateData.map((shot, index) => (
|
||||||
|
<motion.div
|
||||||
|
key={shot.id || index}
|
||||||
|
className={cn(
|
||||||
|
'relative flex-shrink-0 w-32 aspect-video rounded-lg overflow-hidden cursor-pointer group'
|
||||||
|
)}
|
||||||
|
whileHover={{ scale: 1.02 }}
|
||||||
|
whileTap={{ scale: 0.98 }}
|
||||||
|
>
|
||||||
|
{(shot.status === 0 || shot.videoUrl.length === 0) && (
|
||||||
|
<div className="w-full h-full flex items-center justify-center">
|
||||||
|
<Loader2 className="w-4 h-4 animate-spin text-blue-500" />
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{shot.status === 1 && shot.videoUrl[0] && (
|
||||||
|
<video
|
||||||
|
src={shot.videoUrl[0].video_url}
|
||||||
|
key={shot.videoUrl[0].video_url}
|
||||||
|
className="w-full h-full object-cover"
|
||||||
|
muted
|
||||||
|
loop
|
||||||
|
playsInline
|
||||||
|
onMouseEnter={(e) => e.currentTarget.play()}
|
||||||
|
onMouseLeave={(e) => e.currentTarget.pause()}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
{/* 任务失败 */}
|
||||||
|
{shot.status === 2 && (
|
||||||
|
<div className="w-full h-full flex items-center justify-center bg-red-500/10">
|
||||||
|
<CircleX className="w-4 h-4 text-red-500" />
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
<div className="absolute bottom-0 left-0 right-0 p-1 bg-gradient-to-t from-black/60 to-transparent">
|
||||||
|
<span className="text-xs text-white/90">{shot.name}</span>
|
||||||
|
</div>
|
||||||
|
</motion.div>
|
||||||
|
))}
|
||||||
|
</HorizontalScroller>
|
||||||
|
{/* 按钮 应用 忽略 */}
|
||||||
|
<div className='flex items-center justify-end gap-2'>
|
||||||
|
<motion.button
|
||||||
|
onClick={() => handleApplyUpdate()}
|
||||||
|
className='px-4 py-2 bg-blue-500/10 hover:bg-blue-500/20
|
||||||
|
text-blue-500 rounded-lg transition-colors'
|
||||||
|
>
|
||||||
|
<span>Apply</span>
|
||||||
|
</motion.button>
|
||||||
|
<motion.button
|
||||||
|
onClick={() => handleIgnoreUpdate()}
|
||||||
|
className='px-4 py-2 bg-gray-500/10 hover:bg-gray-500/20
|
||||||
|
text-gray-500 rounded-lg transition-colors'
|
||||||
|
>
|
||||||
|
<span>Ignore</span>
|
||||||
|
</motion.button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</FloatingGlassPanel>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
});
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user