forked from 77media/video-flow
363 lines
12 KiB
TypeScript
363 lines
12 KiB
TypeScript
'use client';
|
||
|
||
import { useEffect, useState, useRef, useCallback } from "react";
|
||
import { streamJsonPost } from "@/api/request";
|
||
import { useSearchParams } from "next/navigation";
|
||
import { Heart, Camera, Film, Scissors } from "lucide-react";
|
||
import { getSceneJson, getShotSketchJson, getVideoJson } from "@/api/video_flow";
|
||
import { useAppSelector } from "@/lib/store/hooks";
|
||
|
||
interface ScriptContent {
|
||
acts?: Array<{
|
||
id: string;
|
||
stableId: string;
|
||
title: string;
|
||
desc: string;
|
||
beats: string[];
|
||
}>;
|
||
characters?: Array<{
|
||
id: string;
|
||
stableId: string;
|
||
name: string;
|
||
role: string;
|
||
arc: string;
|
||
desc: string;
|
||
color: string;
|
||
}>;
|
||
dialogue?: {
|
||
stableId: string;
|
||
rhythm: string;
|
||
style: string;
|
||
};
|
||
themes?: Array<{
|
||
id: string;
|
||
stableId: string;
|
||
theme: string;
|
||
desc: string;
|
||
depth: string;
|
||
}>;
|
||
dramaticLine?: {
|
||
stableId: string;
|
||
points: Array<{
|
||
id: string;
|
||
stableId: string;
|
||
title: string;
|
||
desc: string;
|
||
intensity: number; // 0-100 情感强度
|
||
}>;
|
||
};
|
||
}
|
||
interface Stage {
|
||
id: string;
|
||
title: string;
|
||
icon: React.ElementType;
|
||
color: string;
|
||
profession: string;
|
||
}
|
||
|
||
const stages: Stage[] = [
|
||
{
|
||
id: 'script',
|
||
title: 'Scriptwriter',
|
||
icon: Heart,
|
||
color: '#8b5cf6',
|
||
profession: 'Scriptwriter'
|
||
},
|
||
{
|
||
id: 'storyboard',
|
||
title: 'Storyboard artist',
|
||
icon: Camera,
|
||
color: '#06b6d4',
|
||
profession: 'Storyboard artist'
|
||
},
|
||
{
|
||
id: 'production',
|
||
title: 'Visual director',
|
||
icon: Film,
|
||
color: '#10b981',
|
||
profession: 'Visual director'
|
||
},
|
||
{
|
||
id: 'editing',
|
||
title: 'Editor',
|
||
icon: Scissors,
|
||
color: '#f59e0b',
|
||
profession: 'Editor'
|
||
}
|
||
];
|
||
const detailThinkingText = [
|
||
{
|
||
default: 'is thinking...',
|
||
acts: 'is thinking about the story structure...',
|
||
characters: 'is thinking about the characters...',
|
||
dialogue: 'is thinking about the dialogue...',
|
||
themes: 'is thinking about the themes...',
|
||
dramaticLine: 'is thinking about the dramatic line...',
|
||
another: 'is thinking about the another...'
|
||
}, {
|
||
default: 'is thinking...',
|
||
}, {
|
||
default: 'is thinking...',
|
||
}, {
|
||
default: 'is thinking...',
|
||
}
|
||
]
|
||
|
||
export function useWorkofficeData(currentStage: number, isOpen: boolean, currentLoadingText: string) {
|
||
const [scriptwriterData, setScriptwriterData] = useState<ScriptContent>({
|
||
acts: [],
|
||
characters: [],
|
||
dialogue: undefined,
|
||
themes: [],
|
||
dramaticLine: undefined
|
||
});
|
||
const [thinkingColor, setThinkingColor] = useState<string>('');
|
||
const [thinkingText, setThinkingText] = useState<string>('');
|
||
const searchParams = useSearchParams();
|
||
const project_id = searchParams.get('episodeId') || '';
|
||
const [sceneData, setSceneData] = useState<any>([]);
|
||
const [shotSketchData, setShotSketchData] = useState<any>([]);
|
||
const [videoData, setVideoData] = useState<any>([]);
|
||
const [storyboardData, setStoryboardData] = useState<any>({});
|
||
const [sketchType, setSketchType] = useState<string>('');
|
||
const { sketchCount, videoCount } = useAppSelector((state) => state.workflow);
|
||
|
||
// 使用 ref 存储临时数据,避免重复创建对象
|
||
const tempDataRef = useRef<ScriptContent>({
|
||
acts: [],
|
||
characters: [],
|
||
dialogue: undefined,
|
||
themes: [],
|
||
dramaticLine: undefined
|
||
});
|
||
|
||
// 深度比较两个对象是否相等
|
||
const isEqual = (obj1: any, obj2: any): boolean => {
|
||
if (obj1 === obj2) return true;
|
||
if (typeof obj1 !== 'object' || typeof obj2 !== 'object') return obj1 === obj2;
|
||
if (obj1 === null || obj2 === null) return obj1 === obj2;
|
||
|
||
const keys1 = Object.keys(obj1);
|
||
const keys2 = Object.keys(obj2);
|
||
|
||
if (keys1.length !== keys2.length) return false;
|
||
|
||
for (const key of keys1) {
|
||
if (!isEqual(obj1[key], obj2[key])) return false;
|
||
}
|
||
|
||
return true;
|
||
};
|
||
|
||
// 更新思考文本的函数
|
||
const updateThinkingText = useCallback((data: ScriptContent) => {
|
||
console.log('updateThinkingText', currentStage, isOpen);
|
||
if (currentStage === 0) {
|
||
if (!data.acts || data.acts.length === 0) {
|
||
return `${stages[currentStage].profession} ${detailThinkingText[currentStage].acts}`;
|
||
}
|
||
if (!data.characters || data.characters.length === 0) {
|
||
return `${stages[currentStage].profession} ${detailThinkingText[currentStage].characters}`;
|
||
}
|
||
if (!data.dialogue) {
|
||
return `${stages[currentStage].profession} ${detailThinkingText[currentStage].dialogue}`;
|
||
}
|
||
if (!data.themes || data.themes.length === 0) {
|
||
return `${stages[currentStage].profession} ${detailThinkingText[currentStage].themes}`;
|
||
}
|
||
if (!data.dramaticLine) {
|
||
return `${stages[currentStage].profession} ${detailThinkingText[currentStage].dramaticLine}`;
|
||
}
|
||
return `${stages[currentStage].profession} ${detailThinkingText[currentStage].another}`;
|
||
}
|
||
return `${stages[currentStage].profession} ${detailThinkingText[currentStage].default}`;
|
||
}, [currentStage]);
|
||
|
||
useEffect(() => {
|
||
console.log('currentStage---changed', currentStage);
|
||
if (!isOpen) return;
|
||
setThinkingColor(stages[currentStage].color);
|
||
if (currentStage !== 0) return; // 暂时仅支持剧本创作阶段获取真实数据
|
||
// 剧本创作阶段
|
||
if (currentStage === 0) {
|
||
// 重置临时数据
|
||
tempDataRef.current = {
|
||
acts: [],
|
||
characters: [],
|
||
dialogue: undefined,
|
||
themes: [],
|
||
dramaticLine: undefined
|
||
};
|
||
handleStreamData();
|
||
} else if (currentStage === 1) {
|
||
// 不需要每次isOpen为true都请求,只需要请求一次,所以这里需要判断是否已经请求过
|
||
// 场景设计、分镜设计
|
||
if (!sceneData.length) {
|
||
let tempSceneData: any[] = [];
|
||
// 场景设计
|
||
getSceneJson({ project_id: project_id }).then((res: any) => {
|
||
console.log('sceneJson', res);
|
||
if (res.successful) {
|
||
for (const [index, scene] of res.data.sketches.entries()) {
|
||
tempSceneData.push({
|
||
id: `SC-${index + 1}`,
|
||
location: scene.sketch_name,
|
||
description: scene.sketch_description,
|
||
core_atmosphere: scene.core_atmosphere
|
||
});
|
||
}
|
||
setSceneData(tempSceneData);
|
||
}
|
||
});
|
||
}
|
||
if (!shotSketchData.length) {
|
||
let tempShotSketchData: any[] = [];
|
||
// 分镜设计
|
||
getShotSketchJson({ project_id: project_id }).then((res: any) => {
|
||
console.log('shotSketchJson', res);
|
||
if (res.successful) {
|
||
for (const [index, shot] of res.data.shot_sketches.entries()) {
|
||
tempShotSketchData.push({
|
||
id: index + 1,
|
||
shotLanguage: shot.shot_type.split(', '),
|
||
frame_description: shot.frame_description,
|
||
atmosphere: shot.atmosphere.split(', '),
|
||
camera_motion: shot.cinematography_blueprint_camera_motion.split(', '),
|
||
composition: shot.cinematography_blueprint_composition,
|
||
key_action: shot.key_action,
|
||
dialogue_performance: {
|
||
speaker: shot.dialogue_performance_speaker,
|
||
language: shot.dialogue_performance_language,
|
||
delivery: shot.dialogue_performance_delivery,
|
||
line: shot.dialogue_performance_line
|
||
}
|
||
});
|
||
}
|
||
setShotSketchData(tempShotSketchData);
|
||
}
|
||
});
|
||
}
|
||
} else if (currentStage === 2) {
|
||
// 视觉导演
|
||
getVideoJson({ project_id: project_id }).then((res: any) => {
|
||
console.log('videoJson', res);
|
||
});
|
||
}
|
||
}, [currentStage, isOpen]);
|
||
|
||
useEffect(() => {
|
||
const newThinkingText = updateThinkingText(scriptwriterData);
|
||
if (newThinkingText !== thinkingText) {
|
||
setThinkingText(newThinkingText);
|
||
}
|
||
}, [scriptwriterData, updateThinkingText]);
|
||
|
||
useEffect(() => {
|
||
console.log('------sketchCount, sceneData, shotSketchData', sketchCount, sceneData, shotSketchData);
|
||
if (!isOpen) return;
|
||
if (!currentLoadingText.includes('shot sketch')) {
|
||
if (!sceneData.length || sketchCount > sceneData.length) return;
|
||
setSketchType('scene');
|
||
setStoryboardData(sceneData[sketchCount ? sketchCount - 1 : 0]);
|
||
setThinkingText(`Drawing scene sketch: ${sceneData[sketchCount ? sketchCount - 1 : 0].id}...`);
|
||
} else {
|
||
if (!shotSketchData.length || sketchCount > shotSketchData.length) return;
|
||
setSketchType('shot');
|
||
setStoryboardData(shotSketchData[sketchCount ? sketchCount - 1 : 0]);
|
||
setThinkingText(`Drawing shot sketch: ${shotSketchData[sketchCount ? sketchCount - 1 : 0].id}...`);
|
||
}
|
||
}, [sceneData, shotSketchData, currentLoadingText, sketchCount, isOpen]);
|
||
|
||
const updateStateIfChanged = useCallback((newData: Partial<ScriptContent>) => {
|
||
const hasChanges = Object.entries(newData).some(([key, value]) => {
|
||
return !isEqual(value, tempDataRef.current[key as keyof ScriptContent]);
|
||
});
|
||
|
||
if (hasChanges) {
|
||
const updatedData = {
|
||
...tempDataRef.current,
|
||
...newData
|
||
};
|
||
|
||
// 只有在数据真正变化时才更新状态
|
||
if (!isEqual(updatedData, tempDataRef.current)) {
|
||
tempDataRef.current = updatedData;
|
||
setScriptwriterData(updatedData);
|
||
}
|
||
}
|
||
}, []);
|
||
|
||
async function handleStreamData() {
|
||
await streamJsonPost('/movie/analyze_movie_script_stream', { project_id: project_id }, (data: any) => {
|
||
console.log('workoffice---chunk', data);
|
||
const newData: Partial<ScriptContent> = {};
|
||
|
||
// 三幕结构
|
||
if (data.name === 'Three-act Structure') {
|
||
newData.acts = data.acts.map((act: any, index: number) => ({
|
||
id: String(index),
|
||
stableId: `act${index}`,
|
||
title: act.name,
|
||
desc: act.description,
|
||
beats: act.tags
|
||
}));
|
||
}
|
||
// 人物设定
|
||
if (data.name === 'Character Arc Design') {
|
||
newData.characters = data.characters.map((protagonist: any, index: number) => ({
|
||
id: String(index),
|
||
stableId: `protagonist${index}`,
|
||
name: protagonist.name,
|
||
role: protagonist.role,
|
||
arc: 'Growth and transformation',
|
||
desc: protagonist.description,
|
||
color: protagonist.gender === 'male' ? '#8b5cf6' : '#ec4899'
|
||
}));
|
||
}
|
||
// 对话节奏
|
||
if (data.name === 'Dialogue Rhythm') {
|
||
newData.dialogue = {
|
||
stableId: 'dialogue1',
|
||
rhythm: data.rhythm_and_pacing,
|
||
style: data.style_and_voice
|
||
};
|
||
}
|
||
// 主题深化过程
|
||
if (data.name === 'Thematic Development') {
|
||
newData.themes = data.themes.map((theme: any, index: number) => ({
|
||
id: String(index),
|
||
stableId: `theme${index}`,
|
||
theme: theme.name,
|
||
desc: theme.desc,
|
||
depth: theme.depth
|
||
}));
|
||
}
|
||
// 戏演线
|
||
if (data.name === 'Dramatic Line') {
|
||
newData.dramaticLine = {
|
||
stableId: 'dramaticLine1',
|
||
points: data.stages.map((point: any, index: number) => ({
|
||
id: String(index),
|
||
stableId: `point${index}`,
|
||
title: point.stage,
|
||
desc: point.desc,
|
||
intensity: point.score
|
||
}))
|
||
};
|
||
}
|
||
|
||
// 只在数据真正变化时更新状态
|
||
if (Object.keys(newData).length > 0) {
|
||
updateStateIfChanged(newData);
|
||
}
|
||
});
|
||
}
|
||
|
||
return {
|
||
scriptwriterData,
|
||
thinkingText,
|
||
thinkingColor,
|
||
storyboardData,
|
||
sketchType
|
||
};
|
||
} |