| 1 | 'use client'; |
| 2 | |
| 3 | import React, { useState, useRef, useEffect, useCallback } from 'react'; |
| 4 | import { useRouter, useSearchParams } from 'next/navigation'; |
| 5 | import { |
| 6 | startProject, |
| 7 | executeStage, |
| 8 | parseStreamEvents, |
| 9 | getProjectStatus, |
| 10 | getProjectStatusFromDisk, |
| 11 | continueWorkflow, |
| 12 | stopProject, |
| 13 | intervene, |
| 14 | getArtifact, |
| 15 | fetchSessions, |
| 16 | updateModels, |
| 17 | deleteSession, |
| 18 | saveSelections, |
| 19 | } from '@/lib/workflowApi'; |
| 20 | import TopBar, { STAGES, type ModelConfig } from './TopBar'; |
| 21 | import HomePage, { type ProjectParams } from './HomePage'; |
| 22 | import { |
| 23 | ScriptStage, |
| 24 | CharacterStage, |
| 25 | StoryboardStage, |
| 26 | ReferenceStage, |
| 27 | VideoStage, |
| 28 | PostProductionStage, |
| 29 | type StageState, |
| 30 | type StageStatus, |
| 31 | } from './stages'; |
| 32 | |
| 33 | const STAGE_ORDER: string[] = STAGES.map(s => s.id); |
| 34 | |
| 35 | const STAGE_COMPONENTS: Record<string, React.ComponentType<any>> = { |
| 36 | script_generation: ScriptStage, |
| 37 | character_design: CharacterStage, |
| 38 | storyboard: StoryboardStage, |
| 39 | reference_generation: ReferenceStage, |
| 40 | video_generation: VideoStage, |
| 41 | post_production: PostProductionStage, |
| 42 | }; |
| 43 | |
| 44 | interface HistoryItem { |
| 45 | id: string; |
| 46 | idea: string; |
| 47 | style?: string; |
| 48 | date: string; |
| 49 | status: string; |
| 50 | stages?: Record<string, string>; // stageId => status 映射 |
| 51 | } |
| 52 | |
| 53 | function initStageStates(): Record<string, StageState> { |
| 54 | const states: Record<string, StageState> = {}; |
| 55 | for (const s of STAGE_ORDER) { |
| 56 | states[s] = { |
| 57 | status: 'pending', |
| 58 | progress: 0, |
| 59 | progressMessage: '', |
| 60 | artifact: null, |
| 61 | error: null, |
| 62 | }; |
| 63 | } |
| 64 | return states; |
| 65 | } |
| 66 | |
| 67 | function hasDoneArtifactItems(artifact: any): boolean { |
| 68 | if (!artifact) return false; |
| 69 | return ['clips', 'images', 'scenes', 'characters', 'settings'].some(key => { |
| 70 | const items = artifact?.[key]; |
| 71 | return Array.isArray(items) && items.some((item: any) => item?.status === 'done' || item?.selected); |
| 72 | }); |
| 73 | } |
| 74 | |
| 75 | function hasRunningArtifactItems(artifact: any): boolean { |
| 76 | if (!artifact) return false; |
| 77 | return ['clips', 'images', 'scenes', 'characters', 'settings'].some(key => { |
| 78 | const items = artifact?.[key]; |
| 79 | return Array.isArray(items) && items.some((item: any) => item?.status === 'running'); |
| 80 | }); |
| 81 | } |
| 82 | |
| 83 | function mergeAssetVersions(current?: string[], incoming?: string[]): string[] { |
| 84 | const merged: string[] = []; |
| 85 | [...(Array.isArray(current) ? current : []), ...(Array.isArray(incoming) ? incoming : [])].forEach(path => { |
| 86 | if (path && !merged.includes(path)) merged.push(path); |
| 87 | }); |
| 88 | return merged; |
| 89 | } |
| 90 | |
| 91 | function mergeAssetUpdateItem(item: any, assetUpdate: any): any { |
| 92 | const hasExistingSelection = Boolean(item?.selected); |
| 93 | const nextStatus = hasExistingSelection && ['done', 'failed'].includes(assetUpdate.status) |
| 94 | ? 'done' |
| 95 | : (assetUpdate.status ?? item.status); |
| 96 | return { |
| 97 | ...item, |
| 98 | status: nextStatus, |
| 99 | selected: hasExistingSelection |
| 100 | ? item.selected |
| 101 | : (assetUpdate.selected || item.selected), |
| 102 | versions: assetUpdate.versions |
| 103 | ? mergeAssetVersions(item.versions, assetUpdate.versions) |
| 104 | : item.versions, |
| 105 | rewrite_result: assetUpdate.rewrite_result ?? item.rewrite_result, |
| 106 | }; |
| 107 | } |
| 108 | |
| 109 | function getStageProgressSnapshot(status: any, stageId: string): Partial<StageState> { |
| 110 | const snapshot = status?.stage_progress?.[stageId]; |
| 111 | if (!snapshot || typeof snapshot !== 'object') return {}; |
| 112 | const progress = typeof snapshot.percent === 'number' |
| 113 | ? Math.max(0, Math.min(100, Math.round(snapshot.percent))) |
| 114 | : undefined; |
| 115 | const progressMessage = typeof snapshot.message === 'string' && snapshot.message.trim() |
| 116 | ? snapshot.message |
| 117 | : (typeof snapshot.step === 'string' ? snapshot.step : undefined); |
| 118 | return { |
| 119 | ...(typeof progress === 'number' ? { progress } : {}), |
| 120 | ...(progressMessage ? { progressMessage } : {}), |
| 121 | }; |
| 122 | } |
| 123 | |
| 124 | export default function WorkflowPanel() { |
| 125 | const router = useRouter(); |
| 126 | const searchParams = useSearchParams(); |
| 127 | |
| 128 | const [sessionId, setSessionId] = useState<string | null>(null); |
| 129 | const [activeStage, setActiveStage] = useState<string | null>(null); |
| 130 | const [stageStates, setStageStates] = useState<Record<string, StageState>>(initStageStates()); |
| 131 | const [isRunning, setIsRunning] = useState(false); |
| 132 | const [history, setHistory] = useState<HistoryItem[]>([]); |
| 133 | const [projectParams, setProjectParams] = useState<ProjectParams | null>(null); |
| 134 | const [autoMode, setAutoMode] = useState(false); |
| 135 | // 用于顶栏流程图状态判断 |
| 136 | const [currentStageFromSession, setCurrentStageFromSession] = useState<string | null>(null); |
| 137 | const [completedStagesFromSession, setCompletedStagesFromSession] = useState<string[]>([]); |
| 138 | |
| 139 | const abortRef = useRef<AbortController | null>(null); |
| 140 | const stoppedRef = useRef(false); |
| 141 | const autoModeRef = useRef(autoMode); |
| 142 | const pollRef = useRef<Set<string>>(new Set()); |
| 143 | |
| 144 | useEffect(() => { |
| 145 | autoModeRef.current = autoMode; |
| 146 | }, [autoMode]); |
| 147 | |
| 148 | const handleAutoModeChange = useCallback((nextAutoMode: boolean) => { |
| 149 | autoModeRef.current = nextAutoMode; |
| 150 | setAutoMode(nextAutoMode); |
| 151 | }, []); |
| 152 | |
| 153 | // 清理轮询 |
| 154 | useEffect(() => { |
| 155 | return () => { pollRef.current.clear(); }; |
| 156 | }, []); |
| 157 | |
| 158 | // 轮询等待后端阶段完成。运行中优先读后端内存,后端重启后再回退到磁盘快照。 |
| 159 | const pollForCompletion = useCallback(async (sid: string, stageId: string) => { |
| 160 | const key = `${sid}:${stageId}`; |
| 161 | pollRef.current.add(key); |
| 162 | for (let i = 0; i < 300; i++) { // 增加轮询次数(最多10分钟) |
| 163 | await new Promise(r => setTimeout(r, 2000)); |
| 164 | if (!pollRef.current.has(key)) return; |
| 165 | try { |
| 166 | let status: any; |
| 167 | try { |
| 168 | status = await getProjectStatus(sid); |
| 169 | } catch { |
| 170 | status = await getProjectStatusFromDisk(sid); |
| 171 | } |
| 172 | if (!pollRef.current.has(key)) return; |
| 173 | setGlobalStatusMap(status.status || {}); |
| 174 | const done = Object.keys(status.status || {}).filter(k => ["completed", "session_completed"].includes(status.status[k])); |
| 175 | const currentStageStatus = status.status?.[stageId] || 'idle'; |
| 176 | const artifacts = status.artifacts || {}; |
| 177 | const currentArtifact = artifacts[stageId]; |
| 178 | const hasRunningItems = hasRunningArtifactItems(currentArtifact); |
| 179 | if (done.includes(stageId) && !hasRunningItems) { |
| 180 | const isWait = currentStageStatus === 'waiting' && status.current_stage === stageId; |
| 181 | let artifact = null; |
| 182 | try { artifact = (await getArtifact(sid, stageId)).artifact; } catch {} |
| 183 | updateStageState(stageId, { |
| 184 | status: isWait ? 'waiting' : 'completed', |
| 185 | progress: 100, |
| 186 | progressMessage: isWait ? '等待确认' : '已完成', |
| 187 | artifact, |
| 188 | }); |
| 189 | pollRef.current.delete(key); |
| 190 | return; |
| 191 | } |
| 192 | if (currentStageStatus === 'error' && !hasRunningItems) { |
| 193 | updateStageState(stageId, { |
| 194 | status: 'error', |
| 195 | error: status.error || '执行出错', |
| 196 | progressMessage: '执行失败', |
| 197 | }); |
| 198 | pollRef.current.delete(key); |
| 199 | return; |
| 200 | } |
| 201 | if (currentStageStatus === 'stopped' && !hasRunningItems) { |
| 202 | updateStageState(stageId, { |
| 203 | status: 'stopped', |
| 204 | error: '手动停止', |
| 205 | progressMessage: '已停止', |
| 206 | }); |
| 207 | pollRef.current.delete(key); |
| 208 | return; |
| 209 | } |
| 210 | // 后台单卡重生成时,阶段状态可能仍是 waiting/completed;只要 item 还在 running 就继续轮询。 |
| 211 | if (currentStageStatus !== 'running' && !hasRunningItems) { |
| 212 | updateStageState(stageId, { |
| 213 | status: currentStageStatus === 'waiting' |
| 214 | ? 'waiting' |
| 215 | : (done.includes(stageId) ? 'completed' : 'pending'), |
| 216 | progress: done.includes(stageId) ? 100 : undefined, |
| 217 | progressMessage: currentStageStatus === 'waiting' |
| 218 | ? '等待确认' |
| 219 | : (done.includes(stageId) ? '已完成' : undefined), |
| 220 | artifact: currentArtifact || null, |
| 221 | }); |
| 222 | pollRef.current.delete(key); |
| 223 | return; |
| 224 | } |
| 225 | // 更新进度信息(优先使用后端持久化的 stage_progress 快照) |
| 226 | const update: Partial<StageState> = { |
| 227 | status: 'running', |
| 228 | ...getStageProgressSnapshot(status, stageId), |
| 229 | }; |
| 230 | |
| 231 | // 无论何种状态,只要有数据就更新 artifact |
| 232 | if (currentArtifact) { |
| 233 | update.artifact = currentArtifact; |
| 234 | } |
| 235 | |
| 236 | if (!update.progressMessage && currentArtifact) { |
| 237 | // 检查是否有已生成的参考图/视频/分镜 |
| 238 | const hasProgress = currentArtifact.scenes?.some((s: any) => s.versions?.length > 0) || |
| 239 | currentArtifact.clips?.some((c: any) => c.versions?.length > 0) || |
| 240 | currentArtifact.episodes?.some((e: any) => e.segments?.length > 0) || |
| 241 | currentArtifact.characters?.length > 0 || |
| 242 | currentArtifact.shots?.length > 0; |
| 243 | if (hasProgress) { |
| 244 | update.progressMessage = '执行中...(已生成部分资源)'; |
| 245 | } else { |
| 246 | update.progressMessage = '执行中...'; |
| 247 | } |
| 248 | } else if (!update.progressMessage) { |
| 249 | update.progressMessage = '执行中...'; |
| 250 | } |
| 251 | |
| 252 | if (Object.keys(update).length > 0) { |
| 253 | updateStageState(stageId, update); |
| 254 | } |
| 255 | } catch { /* retry */ } |
| 256 | } |
| 257 | pollRef.current.delete(key); |
| 258 | }, []); |
| 259 | |
| 260 | // 检查 URL 参数,加载指定的 session 和阶段 |
| 261 | useEffect(() => { |
| 262 | const sessionParam = searchParams.get('session'); |
| 263 | const stageParam = searchParams.get('stage'); |
| 264 | if (sessionParam) { |
| 265 | // 保存目标阶段,等会话加载完成后再设置 |
| 266 | const targetStage = stageParam && STAGE_ORDER.includes(stageParam) ? stageParam : null; |
| 267 | handleResumeProject(sessionParam, targetStage); |
| 268 | } |
| 269 | }, [searchParams]); |
| 270 | |
| 271 | // 页面加载时从后端获取历史记录 |
| 272 | useEffect(() => { |
| 273 | fetchSessions() |
| 274 | .then(sessions => { |
| 275 | setHistory( |
| 276 | sessions.map((s: any) => ({ |
| 277 | id: s.id, |
| 278 | idea: (s.title || s.idea || 'Untitled').slice(0, 60), |
| 279 | style: s.style || '', |
| 280 | date: s.date |
| 281 | ? new Date(s.date * 1000).toLocaleDateString('zh-CN') |
| 282 | : '', |
| 283 | status: Object.keys(s.status || {}).length > 0 ? 'partial' : 'new', |
| 284 | stages: s.status || {}, |
| 285 | })) |
| 286 | ); |
| 287 | }) |
| 288 | .catch(() => {}); |
| 289 | }, []); |
| 290 | |
| 291 | const updateStageState = (stageId: string, update: Partial<StageState>) => { |
| 292 | setStageStates(prev => { |
| 293 | const current = prev[stageId]; |
| 294 | const nextUpdate = { ...update }; |
| 295 | if ( |
| 296 | current?.status === 'running' && |
| 297 | typeof update.progress === 'number' && |
| 298 | update.progress < (current.progress || 0) && |
| 299 | update.status !== 'completed' && |
| 300 | update.status !== 'waiting' |
| 301 | ) { |
| 302 | nextUpdate.progress = current.progress; |
| 303 | } |
| 304 | return { |
| 305 | ...prev, |
| 306 | [stageId]: { ...current, ...nextUpdate }, |
| 307 | }; |
| 308 | }); |
| 309 | }; |
| 310 | |
| 311 | // ── 停止执行 ── |
| 312 | const handleStop = async () => { |
| 313 | stoppedRef.current = true; |
| 314 | // 1. 断开前端 SSE 流 |
| 315 | if (abortRef.current) { |
| 316 | abortRef.current.abort(); |
| 317 | abortRef.current = null; |
| 318 | } |
| 319 | // 2. 通知后端停止 |
| 320 | if (sessionId) { |
| 321 | try { await stopProject(sessionId); } catch { /* ignore */ } |
| 322 | } |
| 323 | setIsRunning(false); |
| 324 | // 3. 将所有 running 阶段标记为 stopped,保留已有的 artifact |
| 325 | setStageStates(prev => { |
| 326 | const next = { ...prev }; |
| 327 | for (const s of STAGE_ORDER) { |
| 328 | if (next[s]?.status === 'running') { |
| 329 | // 如果已有 artifact 数据(如部分已生成的视频片段),保留为 waiting 状态以便用户操作 |
| 330 | const hasArtifact = hasDoneArtifactItems(next[s]?.artifact); |
| 331 | if (hasArtifact) { |
| 332 | next[s] = { ...next[s], status: 'waiting', error: null, progressMessage: '已停止(保留已完成内容)' }; |
| 333 | } else { |
| 334 | next[s] = { ...next[s], status: 'error', error: '已手动停止', progressMessage: '已停止' }; |
| 335 | } |
| 336 | } |
| 337 | } |
| 338 | return next; |
| 339 | }); |
| 340 | // 4. 尝试从后端获取最新 artifact(后端可能已保存部分结果) |
| 341 | if (sessionId) { |
| 342 | setTimeout(async () => { |
| 343 | for (const s of STAGE_ORDER) { |
| 344 | try { |
| 345 | const artResult = await getArtifact(sessionId, s); |
| 346 | if (artResult?.artifact) { |
| 347 | setStageStates(prev => { |
| 348 | const cur = prev[s]; |
| 349 | if (!cur || cur.status === 'completed') return prev; |
| 350 | // 检查 artifact 是否含有效内容 |
| 351 | const hasDone = hasDoneArtifactItems(artResult.artifact); |
| 352 | if (hasDone) { |
| 353 | return { ...prev, [s]: { ...cur, status: 'waiting', artifact: artResult.artifact, error: null, progressMessage: '已停止(保留已完成内容)' } }; |
| 354 | } |
| 355 | return prev; |
| 356 | }); |
| 357 | } |
| 358 | } catch { /* 该阶段无 artifact,跳过 */ } |
| 359 | } |
| 360 | }, 500); |
| 361 | } |
| 362 | }; |
| 363 | |
| 364 | // ── 执行单个阶段 ── |
| 365 | const runStage = async (sid: string, stageId: string, inputData: Record<string, any>) => { |
| 366 | if (stoppedRef.current) throw new Error('Stopped'); |
| 367 | updateStageState(stageId, { status: 'running', progress: 0, progressMessage: '启动中...', error: null }); |
| 368 | setActiveStage(stageId); |
| 369 | |
| 370 | const controller = new AbortController(); |
| 371 | abortRef.current = controller; |
| 372 | |
| 373 | try { |
| 374 | const response = await executeStage(sid, stageId, inputData, controller.signal); |
| 375 | |
| 376 | for await (const event of parseStreamEvents(response)) { |
| 377 | if (event.type === 'progress') { |
| 378 | updateStageState(stageId, { |
| 379 | progress: event.percent || 0, |
| 380 | progressMessage: event.message || '', |
| 381 | }); |
| 382 | // 处理素材预览和逐个完成事件 |
| 383 | if (event.data?.assets_preview) { |
| 384 | updateStageState(stageId, { artifact: event.data.assets_preview }); |
| 385 | } |
| 386 | if (event.data?.asset_complete) { |
| 387 | const assetUpdate = event.data.asset_complete; |
| 388 | setStageStates(prev => { |
| 389 | const prevArt = prev[stageId]?.artifact; |
| 390 | if (!prevArt) return prev; |
| 391 | const key = assetUpdate.type as string; |
| 392 | const items = [...(prevArt[key] || [])]; |
| 393 | const idx = items.findIndex((item: any) => item.id === assetUpdate.id); |
| 394 | if (idx >= 0) { |
| 395 | items[idx] = mergeAssetUpdateItem(items[idx], assetUpdate); |
| 396 | } |
| 397 | return { |
| 398 | ...prev, |
| 399 | [stageId]: { ...prev[stageId], artifact: { ...prevArt, [key]: items } }, |
| 400 | }; |
| 401 | }); |
| 402 | } |
| 403 | // 处理剧本逐幕增量事件 |
| 404 | if (event.data?.beat_sheet) { |
| 405 | setStageStates(prev => { |
| 406 | const prevArt = prev[stageId]?.artifact || {}; |
| 407 | return { |
| 408 | ...prev, |
| 409 | [stageId]: { |
| 410 | ...prev[stageId], |
| 411 | artifact: { ...prevArt, phase: 'generating', beat_sheet: event.data.beat_sheet }, |
| 412 | }, |
| 413 | }; |
| 414 | }); |
| 415 | } |
| 416 | if (event.data?.act_complete) { |
| 417 | const actData = event.data.act_complete; |
| 418 | setStageStates(prev => { |
| 419 | const prevArt = prev[stageId]?.artifact || {}; |
| 420 | const prevActs = prevArt.completed_acts || []; |
| 421 | return { |
| 422 | ...prev, |
| 423 | [stageId]: { |
| 424 | ...prev[stageId], |
| 425 | artifact: { |
| 426 | ...prevArt, |
| 427 | phase: 'generating', |
| 428 | completed_acts: [...prevActs, actData], |
| 429 | }, |
| 430 | }, |
| 431 | }; |
| 432 | }); |
| 433 | } |
| 434 | // 处理分镜逐场完成事件 |
| 435 | if (event.data?.scene_shots_complete) { |
| 436 | const sceneData = event.data.scene_shots_complete; |
| 437 | setStageStates(prev => { |
| 438 | const prevArt = prev[stageId]?.artifact || {}; |
| 439 | const prevShots = prevArt.shots || []; |
| 440 | return { |
| 441 | ...prev, |
| 442 | [stageId]: { |
| 443 | ...prev[stageId], |
| 444 | artifact: { |
| 445 | ...prevArt, |
| 446 | phase: 'generating', |
| 447 | shots: [...prevShots, ...sceneData.shots], |
| 448 | }, |
| 449 | }, |
| 450 | }; |
| 451 | }); |
| 452 | } |
| 453 | } else if (event.type === 'stage_complete') { |
| 454 | const newStatus: StageStatus = event.requires_intervention ? 'waiting' : 'completed'; |
| 455 | |
| 456 | let artifact = null; |
| 457 | try { |
| 458 | const artResult = await getArtifact(sid, stageId); |
| 459 | artifact = artResult.artifact; |
| 460 | } catch { /* ignore */ } |
| 461 | |
| 462 | // 如果是后处理阶段,即使 artifact 为空也尝试从事件中读取预览数据 |
| 463 | if (stageId === 'post_production' && !artifact && event.data?.assets_preview) { |
| 464 | artifact = event.data.assets_preview; |
| 465 | } |
| 466 | |
| 467 | updateStageState(stageId, { |
| 468 | status: newStatus, |
| 469 | progress: 100, |
| 470 | progressMessage: newStatus === 'waiting' ? '等待确认' : '已完成', |
| 471 | artifact, |
| 472 | }); |
| 473 | } else if (event.type === 'error') { |
| 474 | // 取消/停止类错误:尝试获取部分结果而不是直接报错 |
| 475 | const isCancelError = /cancel|取消|停止/i.test(event.content || ''); |
| 476 | if (isCancelError) { |
| 477 | let artifact = null; |
| 478 | try { |
| 479 | const artResult = await getArtifact(sid, stageId); |
| 480 | artifact = artResult?.artifact; |
| 481 | } catch { /* ignore */ } |
| 482 | // 如果有已完成的内容,显示为 waiting 状态 |
| 483 | const hasDone = hasDoneArtifactItems(artifact); |
| 484 | if (hasDone) { |
| 485 | updateStageState(stageId, { |
| 486 | status: 'waiting', |
| 487 | progress: 100, |
| 488 | progressMessage: '已停止(保留已完成内容)', |
| 489 | artifact, |
| 490 | error: null, |
| 491 | }); |
| 492 | return; // 不抛异常,让工作流正常停下 |
| 493 | } |
| 494 | } |
| 495 | updateStageState(stageId, { |
| 496 | status: 'error', |
| 497 | error: event.content || 'Unknown error', |
| 498 | progressMessage: '执行失败', |
| 499 | }); |
| 500 | throw new Error(event.content); |
| 501 | } |
| 502 | } |
| 503 | } catch (error: any) { |
| 504 | if (error.name !== 'AbortError') { |
| 505 | updateStageState(stageId, { |
| 506 | status: 'error', |
| 507 | error: error.message, |
| 508 | progressMessage: '执行失败', |
| 509 | }); |
| 510 | } |
| 511 | throw error; |
| 512 | } |
| 513 | }; |
| 514 | |
| 515 | // ── 启动新项目 ── |
| 516 | const handleStartProject = async (params: ProjectParams, autoOverride?: boolean) => { |
| 517 | if (isRunning) return; |
| 518 | stoppedRef.current = false; |
| 519 | const useAutoMode = autoOverride !== undefined ? autoOverride : autoMode; |
| 520 | if (autoOverride !== undefined) handleAutoModeChange(autoOverride); |
| 521 | setIsRunning(true); |
| 522 | setStageStates(initStageStates()); |
| 523 | setProjectParams(params); |
| 524 | |
| 525 | try { |
| 526 | const result = await startProject({ |
| 527 | idea: params.idea, |
| 528 | file_path: params.file_path, // 修复:将上传的文件路径传给后端 |
| 529 | style: params.style, |
| 530 | video_ratio: params.video_ratio, |
| 531 | video_resolution: params.video_resolution, |
| 532 | llm_model: params.llm_model, |
| 533 | vlm_model: params.vlm_model, |
| 534 | image_t2i_model: params.image_t2i_model, |
| 535 | image_it2i_model: params.image_it2i_model, |
| 536 | video_generation_mode: params.video_generation_mode, |
| 537 | video_first_frame_model: params.video_first_frame_model, |
| 538 | video_start_end_model: params.video_start_end_model, |
| 539 | video_reference_model: params.video_reference_model, |
| 540 | video_model: params.video_model, |
| 541 | enable_concurrency: params.enable_concurrency, |
| 542 | web_search: params.web_search, |
| 543 | expand_idea: params.expand_idea, |
| 544 | episodes: params.episodes, |
| 545 | }); |
| 546 | setSessionId(result.session_id); |
| 547 | |
| 548 | // 添加到历史 |
| 549 | setHistory(prev => [ |
| 550 | { |
| 551 | id: result.session_id, |
| 552 | idea: params.idea.slice(0, 60), |
| 553 | style: params.style, |
| 554 | date: new Date().toLocaleDateString('zh-CN'), |
| 555 | status: 'running', |
| 556 | stages: {}, |
| 557 | }, |
| 558 | ...prev, |
| 559 | ]); |
| 560 | |
| 561 | const inputData: Record<string, any> = { |
| 562 | idea: params.idea, |
| 563 | session_id: result.session_id, |
| 564 | style: params.style, |
| 565 | video_ratio: params.video_ratio, |
| 566 | video_resolution: params.video_resolution, |
| 567 | llm_model: params.llm_model, |
| 568 | vlm_model: params.vlm_model, |
| 569 | image_t2i_model: params.image_t2i_model, |
| 570 | image_it2i_model: params.image_it2i_model, |
| 571 | video_generation_mode: params.video_generation_mode, |
| 572 | video_first_frame_model: params.video_first_frame_model, |
| 573 | video_start_end_model: params.video_start_end_model, |
| 574 | video_reference_model: params.video_reference_model, |
| 575 | video_model: params.video_model, |
| 576 | scene_number: result.params?.scene_number, |
| 577 | expand_idea: params.expand_idea, |
| 578 | enable_concurrency: params.enable_concurrency, |
| 579 | web_search: params.web_search, |
| 580 | episodes: params.episodes, |
| 581 | auto_mode: useAutoMode, |
| 582 | }; |
| 583 | |
| 584 | for (const stageId of STAGE_ORDER) { |
| 585 | if (stoppedRef.current) break; |
| 586 | await runStage(result.session_id, stageId, inputData); |
| 587 | |
| 588 | // 剧本生成完成后,用实际场景数更新 scene_number |
| 589 | if (stageId === 'script_generation') { |
| 590 | try { |
| 591 | const artResult = await getArtifact(result.session_id, 'script_generation'); |
| 592 | if (artResult?.artifact?.scenes?.length) { |
| 593 | inputData.scene_number = artResult.artifact.scenes.length; |
| 594 | } |
| 595 | } catch { /* ignore */ } |
| 596 | } |
| 597 | |
| 598 | const status = await getProjectStatus(result.session_id); |
| 599 | const stageStatus = status?.status?.[stageId]; |
| 600 | setGlobalStatusMap(status.status || {}); |
| 601 | // waiting: 等待用户介入(如选择角色/图片),不能自动 continue |
| 602 | // completed: 阶段完成,等待确认进入下一阶段 |
| 603 | if (stageStatus === 'completed' || stageStatus === 'session_completed') { |
| 604 | if (autoModeRef.current) { |
| 605 | // 代理模式:自动确认并继续 |
| 606 | await continueWorkflow(result.session_id); |
| 607 | updateStageState(stageId, { status: 'completed', progressMessage: '已自动确认' }); |
| 608 | } else { |
| 609 | updateStageState(stageId, { status: 'completed' }); |
| 610 | setActiveStage(stageId); |
| 611 | setIsRunning(false); |
| 612 | return; |
| 613 | } |
| 614 | } else if (stageStatus === 'waiting') { |
| 615 | // 用户需要介入,停止自动执行 |
| 616 | updateStageState(stageId, { status: 'waiting' }); |
| 617 | setActiveStage(stageId); |
| 618 | setIsRunning(false); |
| 619 | return; |
| 620 | } |
| 621 | } |
| 622 | } catch (error: any) { |
| 623 | if (!stoppedRef.current) console.error('Workflow error:', error); |
| 624 | } finally { |
| 625 | setIsRunning(false); |
| 626 | } |
| 627 | }; |
| 628 | |
| 629 | // ── 确认阶段并继续 ── |
| 630 | const handleConfirmStage = async (stageId: string) => { |
| 631 | if (!sessionId || isRunning) return; |
| 632 | setIsRunning(true); |
| 633 | |
| 634 | try { |
| 635 | const result: any = await continueWorkflow(sessionId); |
| 636 | updateStageState(stageId, { status: 'completed', progressMessage: '已确认' }); |
| 637 | |
| 638 | // 更新顶栏状态 |
| 639 | if (result.status_map) { |
| 640 | setGlobalStatusMap(result.status_map); |
| 641 | } |
| 642 | setCompletedStagesFromSession(prev => [...prev, stageId]); |
| 643 | if (result.next_stage) { |
| 644 | setCurrentStageFromSession(result.next_stage); |
| 645 | } |
| 646 | |
| 647 | if (result.next_stage) { |
| 648 | const idx = STAGE_ORDER.indexOf(stageId); |
| 649 | |
| 650 | // 构建完整的 inputData |
| 651 | const inputData: Record<string, any> = { |
| 652 | session_id: sessionId, |
| 653 | style: projectParams?.style, |
| 654 | llm_model: projectParams?.llm_model, |
| 655 | vlm_model: projectParams?.vlm_model, |
| 656 | image_t2i_model: projectParams?.image_t2i_model, |
| 657 | image_it2i_model: projectParams?.image_it2i_model, |
| 658 | video_generation_mode: projectParams?.video_generation_mode, |
| 659 | video_first_frame_model: projectParams?.video_first_frame_model, |
| 660 | video_start_end_model: projectParams?.video_start_end_model, |
| 661 | video_reference_model: projectParams?.video_reference_model, |
| 662 | video_model: projectParams?.video_model, |
| 663 | video_ratio: projectParams?.video_ratio, |
| 664 | video_resolution: projectParams?.video_resolution, |
| 665 | video_sound: 'on', |
| 666 | video_shot_type: 'multi', |
| 667 | }; |
| 668 | // 从剧本产物获取实际场景数 |
| 669 | try { |
| 670 | const scriptArt = await getArtifact(sessionId, 'script_generation'); |
| 671 | if (scriptArt?.artifact?.scenes?.length) { |
| 672 | inputData.scene_number = scriptArt.artifact.scenes.length; |
| 673 | } |
| 674 | } catch { /* ignore */ } |
| 675 | |
| 676 | // 从参考图产物获取用户选择的图片版本,传递给视频生成阶段 |
| 677 | if (stageId === 'reference_generation') { |
| 678 | const refArt = stageStates['reference_generation']?.artifact; |
| 679 | if (refArt?.scenes) { |
| 680 | const selectedImages: Record<string, string> = {}; |
| 681 | refArt.scenes.forEach((s: any) => { |
| 682 | if (s.id && s.selected) { |
| 683 | selectedImages[s.id] = s.selected; |
| 684 | } |
| 685 | }); |
| 686 | inputData.selected_images = selectedImages; |
| 687 | } |
| 688 | } |
| 689 | |
| 690 | // 从视频产物获取用户选择的视频版本,传递给后期制作阶段 |
| 691 | if (stageId === 'video_generation') { |
| 692 | const vidArt = stageStates['video_generation']?.artifact; |
| 693 | if (vidArt?.clips) { |
| 694 | const selectedClips: Record<string, string> = {}; |
| 695 | vidArt.clips.forEach((c: any) => { |
| 696 | if (c.id && c.selected) { |
| 697 | selectedClips[c.id] = c.selected; |
| 698 | } |
| 699 | }); |
| 700 | inputData.selected_clips = selectedClips; |
| 701 | } |
| 702 | } |
| 703 | |
| 704 | for (let i = idx + 1; i < STAGE_ORDER.length; i++) { |
| 705 | if (stoppedRef.current) break; |
| 706 | const nextStage = STAGE_ORDER[i]; |
| 707 | await runStage(sessionId, nextStage, inputData); |
| 708 | |
| 709 | const status = await getProjectStatus(sessionId); |
| 710 | setGlobalStatusMap(status.status || {}); |
| 711 | const nStageStatus = status?.status?.[nextStage]; |
| 712 | // waiting: 等待用户介入(如选择角色/图片),不能自动 continue |
| 713 | // completed: 阶段完成,等待确认进入下一阶段,可以自动 continue |
| 714 | if (nStageStatus === 'completed' || nStageStatus === 'session_completed') { |
| 715 | if (autoModeRef.current) { |
| 716 | await continueWorkflow(sessionId); |
| 717 | updateStageState(nextStage, { status: 'completed', progressMessage: '已自动确认' }); |
| 718 | setCompletedStagesFromSession(prev => [...prev, nextStage]); |
| 719 | setCurrentStageFromSession(nextStage); |
| 720 | } else { |
| 721 | updateStageState(nextStage, { status: 'completed' }); |
| 722 | setCompletedStagesFromSession(prev => [...prev, nextStage]); |
| 723 | setCurrentStageFromSession(nextStage); |
| 724 | setActiveStage(nextStage); |
| 725 | break; |
| 726 | } |
| 727 | } else if (nStageStatus === 'waiting') { |
| 728 | // 用户需要介入,停止自动执行 |
| 729 | updateStageState(nextStage, { status: 'waiting' }); |
| 730 | setCurrentStageFromSession(nextStage); |
| 731 | setActiveStage(nextStage); |
| 732 | break; |
| 733 | } |
| 734 | } |
| 735 | } |
| 736 | } catch (error: any) { |
| 737 | console.error('Continue error:', error); |
| 738 | } finally { |
| 739 | setIsRunning(false); |
| 740 | } |
| 741 | }; |
| 742 | |
| 743 | // ── 用户介入修改 ── |
| 744 | const handleIntervene = async (stageId: string, modifications: Record<string, any>) => { |
| 745 | if (!sessionId) return; |
| 746 | const isBackgroundItemRegeneration = |
| 747 | (stageId === 'character_design' && ( |
| 748 | Array.isArray(modifications.regenerate_characters) || |
| 749 | Array.isArray(modifications.regenerate_settings) |
| 750 | )) || |
| 751 | (stageId === 'reference_generation' && Array.isArray(modifications.regenerate_scenes)) || |
| 752 | (stageId === 'video_generation' && Array.isArray(modifications.regenerate_clips)) || |
| 753 | (stageId === 'post_production' && Array.isArray(modifications.regenerate_episodes)); |
| 754 | |
| 755 | // 构建完整的 inputData 传给后端,确保比例等参数能传递 |
| 756 | const inputData: Record<string, any> = { |
| 757 | ...modifications, |
| 758 | session_id: sessionId, |
| 759 | style: projectParams?.style, |
| 760 | video_ratio: projectParams?.video_ratio, |
| 761 | video_resolution: projectParams?.video_resolution, |
| 762 | llm_model: projectParams?.llm_model, |
| 763 | vlm_model: projectParams?.vlm_model, |
| 764 | image_t2i_model: projectParams?.image_t2i_model, |
| 765 | image_it2i_model: projectParams?.image_it2i_model, |
| 766 | video_generation_mode: projectParams?.video_generation_mode, |
| 767 | video_first_frame_model: projectParams?.video_first_frame_model, |
| 768 | video_start_end_model: projectParams?.video_start_end_model, |
| 769 | video_reference_model: projectParams?.video_reference_model, |
| 770 | video_model: projectParams?.video_model, |
| 771 | video_sound: 'on', |
| 772 | video_shot_type: 'multi', |
| 773 | }; |
| 774 | |
| 775 | // 设置 running 状态以便显示进度条(如 Logline 选择后生成剧本) |
| 776 | // 若选择了 Logline,将其保存到 artifact 以便 ScriptStage 在生成期间展示 |
| 777 | // 若选择了模式,保留已选 Logline 并设置 generating 状态 |
| 778 | const artifactPatch = modifications.selected_logline |
| 779 | ? { phase: 'generating', selected_logline: modifications.selected_logline } |
| 780 | : modifications.selected_mode |
| 781 | ? { phase: 'generating', selected_logline: stageStates[stageId]?.artifact?.selected_logline } |
| 782 | : undefined; |
| 783 | if (!isBackgroundItemRegeneration) { |
| 784 | setIsRunning(true); |
| 785 | updateStageState(stageId, { status: 'running', progress: 0, progressMessage: '处理中...', ...(artifactPatch ? { artifact: artifactPatch } : {}) }); |
| 786 | } else if (artifactPatch) { |
| 787 | updateStageState(stageId, { artifact: artifactPatch }); |
| 788 | } |
| 789 | try { |
| 790 | const response = await intervene(sessionId, stageId, inputData); |
| 791 | for await (const event of parseStreamEvents(response)) { |
| 792 | if (event.type === 'progress') { |
| 793 | if (!isBackgroundItemRegeneration) { |
| 794 | updateStageState(stageId, { |
| 795 | progress: event.percent || 0, |
| 796 | progressMessage: event.message || '', |
| 797 | }); |
| 798 | } |
| 799 | // 处理 asset_complete 实时事件 |
| 800 | if (event.data?.asset_complete) { |
| 801 | const assetUpdate = event.data.asset_complete; |
| 802 | setStageStates(prev => { |
| 803 | const prevArt = prev[stageId]?.artifact; |
| 804 | if (!prevArt) return prev; |
| 805 | const key = assetUpdate.type as string; |
| 806 | const items = [...(prevArt[key] || [])]; |
| 807 | const idx = items.findIndex((item: any) => item.id === assetUpdate.id); |
| 808 | if (idx >= 0) { |
| 809 | items[idx] = mergeAssetUpdateItem(items[idx], assetUpdate); |
| 810 | } |
| 811 | return { |
| 812 | ...prev, |
| 813 | [stageId]: { ...prev[stageId], artifact: { ...prevArt, [key]: items } }, |
| 814 | }; |
| 815 | }); |
| 816 | } |
| 817 | // 处理剧本逐幕增量事件 (介入模式) |
| 818 | if (event.data?.beat_sheet) { |
| 819 | setStageStates(prev => { |
| 820 | const prevArt = prev[stageId]?.artifact || {}; |
| 821 | return { |
| 822 | ...prev, |
| 823 | [stageId]: { |
| 824 | ...prev[stageId], |
| 825 | artifact: { ...prevArt, phase: 'generating', beat_sheet: event.data.beat_sheet }, |
| 826 | }, |
| 827 | }; |
| 828 | }); |
| 829 | } |
| 830 | if (event.data?.act_complete) { |
| 831 | const actData = event.data.act_complete; |
| 832 | setStageStates(prev => { |
| 833 | const prevArt = prev[stageId]?.artifact || {}; |
| 834 | const prevActs = prevArt.completed_acts || []; |
| 835 | return { |
| 836 | ...prev, |
| 837 | [stageId]: { |
| 838 | ...prev[stageId], |
| 839 | artifact: { |
| 840 | ...prevArt, |
| 841 | phase: 'generating', |
| 842 | completed_acts: [...prevActs, actData], |
| 843 | }, |
| 844 | }, |
| 845 | }; |
| 846 | }); |
| 847 | } |
| 848 | // 处理分镜逐场完成事件 (介入模式) |
| 849 | if (event.data?.scene_shots_complete) { |
| 850 | const sceneData = event.data.scene_shots_complete; |
| 851 | setStageStates(prev => { |
| 852 | const prevArt = prev[stageId]?.artifact || {}; |
| 853 | const prevShots = prevArt.shots || []; |
| 854 | return { |
| 855 | ...prev, |
| 856 | [stageId]: { |
| 857 | ...prev[stageId], |
| 858 | artifact: { |
| 859 | ...prevArt, |
| 860 | phase: 'generating', |
| 861 | shots: [...prevShots, ...sceneData.shots], |
| 862 | }, |
| 863 | }, |
| 864 | }; |
| 865 | }); |
| 866 | } |
| 867 | } else if (event.type === 'stage_complete') { |
| 868 | const artResult = await getArtifact(sessionId, stageId); |
| 869 | if (isBackgroundItemRegeneration) { |
| 870 | updateStageState(stageId, { artifact: artResult.artifact }); |
| 871 | } else { |
| 872 | updateStageState(stageId, { artifact: artResult.artifact, status: 'waiting' }); |
| 873 | } |
| 874 | } else if (event.type === 'error') { |
| 875 | console.error('Intervention error:', event.content); |
| 876 | } |
| 877 | } |
| 878 | } catch (error: any) { |
| 879 | console.error('Intervention error:', error); |
| 880 | } finally { |
| 881 | if (!isBackgroundItemRegeneration) { |
| 882 | setIsRunning(false); |
| 883 | } |
| 884 | } |
| 885 | }; |
| 886 | |
| 887 | // ── 本地更新产物(不触发服务端调用) ── |
| 888 | const handleUpdateArtifact = (stageId: string, patch: Record<string, any>) => { |
| 889 | setStageStates(prev => ({ |
| 890 | ...prev, |
| 891 | [stageId]: { |
| 892 | ...prev[stageId], |
| 893 | artifact: { ...(prev[stageId]?.artifact || {}), ...patch }, |
| 894 | }, |
| 895 | })); |
| 896 | }; |
| 897 | |
| 898 | // ── 保存用户选择到服务端 ── |
| 899 | const handleSaveSelections = async (stageId: string, selections: Record<string, any>): Promise<void> => { |
| 900 | if (!sessionId) return; |
| 901 | try { |
| 902 | // 构建更新: 把用户选择写回 artifact 中每个 item 的 selected 字段 |
| 903 | const art = stageStates[stageId]?.artifact; |
| 904 | if (!art) return; |
| 905 | |
| 906 | let patch: Record<string, any> = {}; |
| 907 | if (stageId === 'script_generation') { |
| 908 | // 剧本阶段:直接保存整个 data 覆盖 artifact |
| 909 | patch = selections; |
| 910 | } else if (stageId === 'character_design') { |
| 911 | const { _editDescs, ...restSelections } = selections; |
| 912 | const chars = (art.characters || []).map((c: any) => ({ |
| 913 | ...c, |
| 914 | selected: restSelections[c.id] || c.selected, |
| 915 | description: _editDescs?.characters?.[c.id] ?? c.description, |
| 916 | })); |
| 917 | const sets = (art.settings || []).map((s: any) => ({ |
| 918 | ...s, |
| 919 | selected: restSelections[s.id] || s.selected, |
| 920 | description: _editDescs?.settings?.[s.id] ?? s.description, |
| 921 | })); |
| 922 | patch = { characters: chars, settings: sets }; |
| 923 | } else if (stageId === 'storyboard') { |
| 924 | // 分镜阶段:保存 shots 数据(排除 original_shots,只保留 artifact 需要的字段) |
| 925 | const { original_shots, ...rest } = selections; |
| 926 | patch = { ...rest, user_modified: true }; |
| 927 | } else if (stageId === 'reference_generation') { |
| 928 | const { _editDescs, ...restSelections } = selections; |
| 929 | const scenes = (art.scenes || []).map((s: any) => ({ |
| 930 | ...s, |
| 931 | selected: restSelections[s.id] || s.selected, |
| 932 | description: _editDescs?.[s.id] ?? s.description, |
| 933 | })); |
| 934 | patch = { scenes }; |
| 935 | } else if (stageId === 'video_generation') { |
| 936 | const { _editDescs, ...restSelections } = selections; |
| 937 | const clips = (art.clips || []).map((c: any) => ({ |
| 938 | ...c, |
| 939 | selected: Object.prototype.hasOwnProperty.call(restSelections, c.id) |
| 940 | ? restSelections[c.id] |
| 941 | : c.selected, |
| 942 | description: _editDescs?.[c.id] ?? c.description, |
| 943 | })); |
| 944 | patch = { clips }; |
| 945 | } |
| 946 | |
| 947 | // 本地更新 |
| 948 | handleUpdateArtifact(stageId, patch); |
| 949 | |
| 950 | // 服务端持久化 |
| 951 | await saveSelections(sessionId, stageId, patch); |
| 952 | // 保存成功后标记为已完成,顶栏显示对勾 |
| 953 | updateStageState(stageId, { status: 'completed' }); |
| 954 | |
| 955 | // 如果是分镜阶段保存,刷���第3和第4阶段的 artifact |
| 956 | console.log('[handleSaveSelections] stageId:', stageId, 'patch:', patch); |
| 957 | if (stageId === 'storyboard') { |
| 958 | // 更新本地 artifact 以清除 is_new 标记 |
| 959 | handleUpdateArtifact(stageId, patch); |
| 960 | |
| 961 | // 获取第4阶段当前已有 artifact |
| 962 | const oldRefArtifact = stageStates['reference_generation']?.artifact; |
| 963 | const oldScenes = (oldRefArtifact?.scenes || []) as any[]; |
| 964 | const oldScenesMap = new Map(oldScenes.map((s: any) => [s.id, s])); |
| 965 | |
| 966 | // 使用 segments 构建第4阶段 scenes 和第5阶段 clips |
| 967 | const sourceSegments = patch.segments || []; |
| 968 | console.log('[handleSaveSelections] sourceSegments:', sourceSegments); |
| 969 | |
| 970 | const newScenes = sourceSegments.map((seg: any, idx: number) => { |
| 971 | const seg_id = seg.segment_id; |
| 972 | const oldScene = oldScenesMap.get(seg_id) as any; |
| 973 | const isNew = seg.is_new; |
| 974 | |
| 975 | const shots = seg.shots || []; |
| 976 | const desc_ref = shots.map((sh: any) => sh.visual_prompt || sh.plot || sh.content || '').join(' ').trim(); |
| 977 | |
| 978 | return { |
| 979 | id: seg_id, |
| 980 | name: `片段_${seg_id}`, |
| 981 | index: idx + 1, |
| 982 | description: desc_ref, |
| 983 | selected: isNew ? '' : (oldScene?.selected || ''), |
| 984 | versions: isNew ? [] : (oldScene?.versions || []), |
| 985 | status: isNew ? 'pending' : 'done', |
| 986 | }; |
| 987 | }); |
| 988 | |
| 989 | // 更新第4阶段的 artifact |
| 990 | updateStageState('reference_generation', { |
| 991 | artifact: { session_id: sessionId, scenes: newScenes }, |
| 992 | status: 'completed' |
| 993 | }); |
| 994 | |
| 995 | // 同步更新第5阶段(video_generation) |
| 996 | const oldVidArtifact = stageStates['video_generation']?.artifact; |
| 997 | const oldClips = (oldVidArtifact?.clips || []) as any[]; |
| 998 | const oldClipsMap = new Map(oldClips.map((c: any) => [c.id, c])); |
| 999 | |
| 1000 | const newClips = sourceSegments.map((seg: any, idx: number) => { |
| 1001 | const seg_id = seg.segment_id; |
| 1002 | const oldClip = oldClipsMap.get(seg_id) as any; |
| 1003 | |
| 1004 | const shots = seg.shots || []; |
| 1005 | const desc_video = shots.map((sh: any) => sh.plot || sh.content || '').join(' ').trim(); |
| 1006 | const total_dur = seg.total_duration || shots.reduce((acc: number, sh: any) => acc + (sh.duration || 0), 0) || 10; |
| 1007 | |
| 1008 | return { |
| 1009 | id: seg_id, |
| 1010 | name: `片段_${seg_id}`, |
| 1011 | index: idx + 1, |
| 1012 | description: desc_video, |
| 1013 | duration: total_dur, |
| 1014 | selected: oldClip?.selected || '', |
| 1015 | versions: oldClip?.versions || [], |
| 1016 | status: 'pending', |
| 1017 | }; |
| 1018 | }); |
| 1019 | |
| 1020 | // 更新第5阶段的 artifact |
| 1021 | updateStageState('video_generation', { |
| 1022 | artifact: { session_id: sessionId, clips: newClips }, |
| 1023 | status: 'completed' |
| 1024 | }); |
| 1025 | console.log('[handleSaveSelections] 第5阶段已更新'); |
| 1026 | } |
| 1027 | } catch (error) { |
| 1028 | console.error('Save selections error:', error); |
| 1029 | throw error; // 抛出让 StageActions 捕获以恢复按钮状态 |
| 1030 | } |
| 1031 | }; |
| 1032 | |
| 1033 | // ── 重新生成当前阶段 ── |
| 1034 | const handleRegenerate = async (stageId: string) => { |
| 1035 | if (!sessionId || isRunning) return; |
| 1036 | stoppedRef.current = false; |
| 1037 | setIsRunning(true); |
| 1038 | |
| 1039 | // 清空当前阶段状态 |
| 1040 | updateStageState(stageId, { |
| 1041 | status: 'running', |
| 1042 | progress: 0, |
| 1043 | progressMessage: '重新生成中...', |
| 1044 | artifact: null, |
| 1045 | error: null, |
| 1046 | }); |
| 1047 | |
| 1048 | // 将该阶段之后的所有阶段重置为 pending |
| 1049 | const idx = STAGE_ORDER.indexOf(stageId); |
| 1050 | for (let i = idx + 1; i < STAGE_ORDER.length; i++) { |
| 1051 | updateStageState(STAGE_ORDER[i], { |
| 1052 | status: 'pending', |
| 1053 | progress: 0, |
| 1054 | progressMessage: '', |
| 1055 | artifact: null, |
| 1056 | error: null, |
| 1057 | }); |
| 1058 | } |
| 1059 | |
| 1060 | try { |
| 1061 | const inputData: Record<string, any> = { |
| 1062 | session_id: sessionId, |
| 1063 | style: projectParams?.style, |
| 1064 | llm_model: projectParams?.llm_model, |
| 1065 | vlm_model: projectParams?.vlm_model, |
| 1066 | image_t2i_model: projectParams?.image_t2i_model, |
| 1067 | image_it2i_model: projectParams?.image_it2i_model, |
| 1068 | video_generation_mode: projectParams?.video_generation_mode, |
| 1069 | video_first_frame_model: projectParams?.video_first_frame_model, |
| 1070 | video_start_end_model: projectParams?.video_start_end_model, |
| 1071 | video_reference_model: projectParams?.video_reference_model, |
| 1072 | video_model: projectParams?.video_model, |
| 1073 | video_ratio: projectParams?.video_ratio, |
| 1074 | video_resolution: projectParams?.video_resolution, |
| 1075 | }; |
| 1076 | |
| 1077 | // 尝试获取场景数 |
| 1078 | try { |
| 1079 | const scriptArt = await getArtifact(sessionId, 'script_generation'); |
| 1080 | if (scriptArt?.artifact?.scenes?.length) { |
| 1081 | inputData.scene_number = scriptArt.artifact.scenes.length; |
| 1082 | } |
| 1083 | } catch { /* ignore */ } |
| 1084 | |
| 1085 | await runStage(sessionId, stageId, inputData); |
| 1086 | |
| 1087 | const status = await getProjectStatus(sessionId); |
| 1088 | if (status?.status?.[stageId] === 'waiting') { |
| 1089 | updateStageState(stageId, { status: 'waiting' }); |
| 1090 | setActiveStage(stageId); |
| 1091 | } |
| 1092 | } catch (error: any) { |
| 1093 | if (!stoppedRef.current) console.error('Regenerate error:', error); |
| 1094 | } finally { |
| 1095 | setIsRunning(false); |
| 1096 | } |
| 1097 | }; |
| 1098 | |
| 1099 | // ── 恢复历史项目 ── |
| 1100 | const handleResumeProject = async (sid: string, targetStage: string | null = null) => { |
| 1101 | // 如果正在执行的就是同一个项目,直接恢复视图,不重置状态 |
| 1102 | if (sid === sessionId) { |
| 1103 | const runningStage = STAGE_ORDER.find(s => stageStates[s]?.status === 'running'); |
| 1104 | const waitingStage = STAGE_ORDER.find(s => stageStates[s]?.status === 'waiting'); |
| 1105 | const lastCompleted = [...STAGE_ORDER].reverse().find(s => stageStates[s]?.status === 'completed'); |
| 1106 | setActiveStage(targetStage || runningStage || waitingStage || lastCompleted || STAGE_ORDER[0]); |
| 1107 | return; |
| 1108 | } |
| 1109 | |
| 1110 | setSessionId(sid); |
| 1111 | // 更新 URL 参数(包含阶段) |
| 1112 | const stageParam = targetStage ? `&stage=${targetStage}` : ''; |
| 1113 | router.push(`/?session=${sid}${stageParam}`); |
| 1114 | try { |
| 1115 | let status: any; |
| 1116 | try { |
| 1117 | status = await getProjectStatus(sid); |
| 1118 | } catch { |
| 1119 | status = await getProjectStatusFromDisk(sid); |
| 1120 | } |
| 1121 | setGlobalStatusMap(status.status || {}); |
| 1122 | const newStates = initStageStates(); |
| 1123 | const stMap = status.status || {}; |
| 1124 | const allStatusStages = Object.keys(stMap); |
| 1125 | const currentStage = status.current_stage; |
| 1126 | |
| 1127 | // 保存到 state 供顶栏流程图使用 |
| 1128 | setCurrentStageFromSession(currentStage || null); |
| 1129 | const completedStages = allStatusStages.filter(k => ["completed", "session_completed"].includes(stMap[k])); |
| 1130 | setCompletedStagesFromSession(completedStages); |
| 1131 | |
| 1132 | for (const sName of allStatusStages) { |
| 1133 | const cStatus = stMap[sName]; |
| 1134 | if (cStatus === 'pending' || cStatus === 'idle') { |
| 1135 | // 即使是 pending,如果 artifacts 中有数据,也尝试恢复数据(用于初始占位显示) |
| 1136 | if (status.artifacts?.[sName]) { |
| 1137 | newStates[sName].artifact = status.artifacts[sName]; |
| 1138 | } |
| 1139 | continue; |
| 1140 | } |
| 1141 | |
| 1142 | const isCompleted = ["completed", "session_completed"].includes(cStatus); |
| 1143 | const isError = cStatus === 'error'; |
| 1144 | const isWaiting = cStatus === 'waiting'; |
| 1145 | const isStopped = cStatus === 'stopped'; |
| 1146 | const isRunningStatus = cStatus === 'running'; |
| 1147 | const progressSnapshot = getStageProgressSnapshot(status, sName); |
| 1148 | |
| 1149 | newStates[sName] = { |
| 1150 | status: isError ? 'error' : (isWaiting ? 'waiting' : (isStopped ? 'stopped' : (isCompleted ? 'completed' : 'running'))), |
| 1151 | progress: isCompleted ? 100 : (typeof progressSnapshot.progress === 'number' ? progressSnapshot.progress : 0), |
| 1152 | progressMessage: isError |
| 1153 | ? '执行失败' |
| 1154 | : (isWaiting |
| 1155 | ? '等待确认' |
| 1156 | : (isStopped |
| 1157 | ? '已停止' |
| 1158 | : (isCompleted |
| 1159 | ? '已完成' |
| 1160 | : (progressSnapshot.progressMessage || (isRunningStatus ? '执行中...' : '进行中'))))), |
| 1161 | artifact: status.artifacts?.[sName] || null, |
| 1162 | error: isError ? (status.error || '执行出错') : (isStopped ? '手动停止' : null), |
| 1163 | }; |
| 1164 | |
| 1165 | // 如果内存中没有,尝试异步拉取 |
| 1166 | if (!newStates[sName].artifact) { |
| 1167 | try { |
| 1168 | const artResult = await getArtifact(sid, sName); |
| 1169 | newStates[sName].artifact = artResult.artifact; |
| 1170 | } catch { /* ignore */ } |
| 1171 | } |
| 1172 | } |
| 1173 | |
| 1174 | // 恢复项目参数:会话级生成配置统一从 meta 中读取,兼容少量旧扁平字段。 |
| 1175 | const meta = ((status as any).meta || {}) as any; |
| 1176 | const s = Object.keys(meta).length > 0 ? meta : (status as any); |
| 1177 | if (s.idea || s.user_textbox_input) { |
| 1178 | // Legacy session compatibility: old sessions only stored one video_model, so hydrate it as the first-frame model. |
| 1179 | const restoredFirstFrameVideoModel = s.video_first_frame_model || s.video_model || ''; |
| 1180 | setProjectParams({ |
| 1181 | idea: s.idea || s.user_textbox_input || '', |
| 1182 | style: s.style || '', |
| 1183 | video_ratio: s.video_ratio || '16:9', |
| 1184 | video_resolution: s.video_resolution || '720P', |
| 1185 | llm_model: s.llm_model || '', |
| 1186 | vlm_model: s.vlm_model || '', |
| 1187 | image_t2i_model: s.image_t2i_model || '', |
| 1188 | image_it2i_model: s.image_it2i_model || '', |
| 1189 | video_generation_mode: s.video_generation_mode || 'first_frame', |
| 1190 | video_first_frame_model: restoredFirstFrameVideoModel, |
| 1191 | video_start_end_model: s.video_start_end_model || 'wan2.7-i2v', |
| 1192 | video_reference_model: s.video_reference_model || 'wan2.7-r2v', |
| 1193 | video_model: s.video_model || '', |
| 1194 | expand_idea: s.expand_idea || false, |
| 1195 | enable_concurrency: s.enable_concurrency || false, |
| 1196 | web_search: s.web_search || false, |
| 1197 | }); |
| 1198 | } |
| 1199 | |
| 1200 | // 确定要显示的阶段(优先使用 URL 中的目标阶段) |
| 1201 | let finalStage = targetStage; |
| 1202 | if (!finalStage) { |
| 1203 | finalStage = currentStage || (completedStages.length > 0 ? completedStages[completedStages.length - 1] : STAGE_ORDER[0]); |
| 1204 | } |
| 1205 | setActiveStage(finalStage); |
| 1206 | setStageStates(newStates); |
| 1207 | |
| 1208 | // 轮询正在执行的阶段;后台单卡重生成时阶段可能不是 running,但 item 会是 running。 |
| 1209 | for (const stageId of STAGE_ORDER) { |
| 1210 | if (stMap[stageId] === 'running' || hasRunningArtifactItems(status.artifacts?.[stageId])) { |
| 1211 | pollForCompletion(sid, stageId); |
| 1212 | } |
| 1213 | } |
| 1214 | } catch { |
| 1215 | setActiveStage(STAGE_ORDER[0]); |
| 1216 | } |
| 1217 | }; |
| 1218 | |
| 1219 | // ── 返回首页 ── |
| 1220 | // 处理阶段点击,更新 URL |
| 1221 | const handleStageClick = (stage: string) => { |
| 1222 | setActiveStage(stage); |
| 1223 | if (sessionId) { |
| 1224 | router.push(`/?session=${sessionId}&stage=${stage}`); |
| 1225 | } |
| 1226 | }; |
| 1227 | |
| 1228 | const handleGoHome = () => { |
| 1229 | setActiveStage(null); |
| 1230 | router.push('/'); |
| 1231 | }; |
| 1232 | |
| 1233 | // ── 删除历史记录 ── |
| 1234 | const handleDeleteSession = async (sid: string) => { |
| 1235 | await deleteSession(sid); |
| 1236 | setHistory(prev => prev.filter(h => h.id !== sid)); |
| 1237 | }; |
| 1238 | |
| 1239 | // ── 模型配置变更处理 ── |
| 1240 | const handleModelConfigChange = (config: ModelConfig) => { |
| 1241 | setProjectParams(prev => prev ? { ...prev, ...config } : null); |
| 1242 | // 同步到后端会话元数据 |
| 1243 | if (sessionId) { |
| 1244 | const payload: Record<string, any> = { ...config }; |
| 1245 | updateModels(sessionId, payload).catch(console.error); |
| 1246 | } |
| 1247 | }; |
| 1248 | |
| 1249 | // 构建 modelConfig for TopBar |
| 1250 | const modelConfig: ModelConfig | undefined = projectParams |
| 1251 | ? { |
| 1252 | llm_model: projectParams.llm_model, |
| 1253 | vlm_model: projectParams.vlm_model, |
| 1254 | image_t2i_model: projectParams.image_t2i_model, |
| 1255 | image_it2i_model: projectParams.image_it2i_model, |
| 1256 | // Legacy session compatibility: projectParams may come from an old session with only video_model. |
| 1257 | video_generation_mode: projectParams.video_generation_mode || 'first_frame', |
| 1258 | video_first_frame_model: projectParams.video_first_frame_model || projectParams.video_model, |
| 1259 | video_start_end_model: projectParams.video_start_end_model || 'wan2.7-i2v', |
| 1260 | video_reference_model: projectParams.video_reference_model || 'wan2.7-r2v', |
| 1261 | video_model: projectParams.video_model, |
| 1262 | video_ratio: projectParams.video_ratio, |
| 1263 | video_resolution: projectParams.video_resolution || '720P', |
| 1264 | enable_concurrency: projectParams.enable_concurrency || false, |
| 1265 | // web_search: projectParams.web_search || false, |
| 1266 | // expand_idea: projectParams.expand_idea || false, |
| 1267 | } |
| 1268 | : undefined; |
| 1269 | |
| 1270 | // ── 计算 stageStatuses for TopBar ── |
| 1271 | // 直接根据后端返回的 status 数据计算图标状态 |
| 1272 | const [globalStatusMap, setGlobalStatusMap] = useState<Record<string, string>>({}); |
| 1273 | |
| 1274 | const stageStatuses: Record<string, StageStatus> = {}; |
| 1275 | |
| 1276 | for (let i = 0; i < STAGE_ORDER.length; i++) { |
| 1277 | const s = STAGE_ORDER[i]; |
| 1278 | const backendStatus = globalStatusMap[s]; // get status from global mapping updated from backend |
| 1279 | |
| 1280 | if (backendStatus === 'completed') { |
| 1281 | stageStatuses[s] = 'completed'; |
| 1282 | } else if (backendStatus === 'waiting') { |
| 1283 | stageStatuses[s] = 'waiting'; |
| 1284 | } else if (backendStatus === 'running') { |
| 1285 | stageStatuses[s] = 'running'; |
| 1286 | } else if (backendStatus === 'error') { |
| 1287 | stageStatuses[s] = 'error'; |
| 1288 | } else { |
| 1289 | stageStatuses[s] = 'pending'; |
| 1290 | } |
| 1291 | } |
| 1292 | |
| 1293 | // ── 计算项目状态 ── |
| 1294 | // 后端阶段状态: pending, running, waiting, completed, stopped, error |
| 1295 | // 前端 StageStatus: pending, running, waiting, completed, error |
| 1296 | const hasRunning = Object.values(stageStates).some(s => s.status === 'running'); |
| 1297 | const hasWaiting = Object.values(stageStates).some(s => s.status === 'waiting'); |
| 1298 | const hasError = Object.values(stageStates).some(s => s.status === 'error'); |
| 1299 | const allCompleted = Object.values(stageStates).every(s => s.status === 'completed' || s.status === 'pending'); |
| 1300 | const effectiveIsRunning = isRunning || hasRunning; |
| 1301 | |
| 1302 | let computedStatus: string; |
| 1303 | if (hasRunning) computedStatus = 'running'; |
| 1304 | else if (hasWaiting) computedStatus = 'waiting'; |
| 1305 | else if (hasError) computedStatus = 'error'; |
| 1306 | // Check if stopped in stageStates |
| 1307 | else if (Object.values(stageStates).some(s => s.status === 'stopped')) computedStatus = 'stopped'; |
| 1308 | else if (allCompleted && stageStates[STAGE_ORDER[STAGE_ORDER.length - 1]]?.status === 'completed') { |
| 1309 | computedStatus = 'completed'; |
| 1310 | } else if (allCompleted) { |
| 1311 | computedStatus = 'completed'; |
| 1312 | } else { |
| 1313 | computedStatus = Object.values(stageStates).some(s => s.status === 'stopped') ? 'stopped' : 'pending'; |
| 1314 | } |
| 1315 | |
| 1316 | const projectStatus = sessionId ? computedStatus : undefined; |
| 1317 | |
| 1318 | // ── 渲染阶段内容 ── |
| 1319 | const renderStageContent = () => { |
| 1320 | if (!activeStage) return null; |
| 1321 | const Component = STAGE_COMPONENTS[activeStage]; |
| 1322 | if (!Component) return null; |
| 1323 | |
| 1324 | const state = stageStates[activeStage]; |
| 1325 | |
| 1326 | // 判断后续阶段是否已执行过:如有,则隐藏"确认并继续" |
| 1327 | const idx = STAGE_ORDER.indexOf(activeStage); |
| 1328 | const hasSubsequentExecution = STAGE_ORDER.slice(idx + 1).some( |
| 1329 | s => stageStates[s]?.status && stageStates[s].status !== 'pending' |
| 1330 | ); |
| 1331 | const showConfirm = !hasSubsequentExecution; |
| 1332 | |
| 1333 | // 计算是否有待生成的项(阶段2、4、5) |
| 1334 | let hasPendingItems = false; |
| 1335 | if (activeStage === 'character_design') { |
| 1336 | const chars = state?.artifact?.characters || []; |
| 1337 | const sets = state?.artifact?.settings || []; |
| 1338 | hasPendingItems = chars.some((c: any) => !c.selected) || sets.some((s: any) => !s.selected); |
| 1339 | } else if (activeStage === 'reference_generation') { |
| 1340 | const scenes = state?.artifact?.scenes || []; |
| 1341 | hasPendingItems = scenes.some((s: any) => !s.selected); |
| 1342 | } else if (activeStage === 'video_generation') { |
| 1343 | const clips = state?.artifact?.clips || []; |
| 1344 | // 检查是否有未选中的 clips |
| 1345 | const hasUnselected = clips.some((c: any) => !c.selected); |
| 1346 | // 检查参考图阶段的 scenes 数量是否大于 video clips 数量 |
| 1347 | // 如果是,说明有新分镜需要生成 |
| 1348 | const refScenes = stageStates['reference_generation']?.artifact?.scenes || []; |
| 1349 | const hasNewShots = refScenes.length > clips.length; |
| 1350 | hasPendingItems = hasUnselected || hasNewShots; |
| 1351 | } |
| 1352 | |
| 1353 | // 计算后续阶段是否已开始(阶段1、3) |
| 1354 | const hasNextStageStarted = hasSubsequentExecution; |
| 1355 | |
| 1356 | // 传递第4阶段的 artifact 到第5阶段用于依赖检查 |
| 1357 | const referenceArtifact = activeStage === 'video_generation' |
| 1358 | ? stageStates['reference_generation']?.artifact |
| 1359 | : undefined; |
| 1360 | |
| 1361 | // 传递第1阶段的 artifact 到第4、5、6阶段用于获取剧集标题 |
| 1362 | const scriptArtifact = (activeStage === 'reference_generation' || activeStage === 'video_generation' || activeStage === 'post_production') |
| 1363 | ? stageStates['script_generation']?.artifact |
| 1364 | : undefined; |
| 1365 | |
| 1366 | return ( |
| 1367 | <Component |
| 1368 | state={state} |
| 1369 | sessionId={sessionId || ''} |
| 1370 | onConfirm={() => handleConfirmStage(activeStage)} |
| 1371 | onIntervene={(mods: Record<string, any>) => handleIntervene(activeStage, mods)} |
| 1372 | onRegenerate={() => handleRegenerate(activeStage)} |
| 1373 | onUpdateArtifact={(patch: Record<string, any>) => handleUpdateArtifact(activeStage, patch)} |
| 1374 | onSaveSelections={(selections: Record<string, any>) => handleSaveSelections(activeStage, selections)} |
| 1375 | showConfirm={showConfirm} |
| 1376 | isRunning={effectiveIsRunning} |
| 1377 | hasPendingItems={hasPendingItems} |
| 1378 | hasNextStageStarted={hasNextStageStarted} |
| 1379 | referenceArtifact={referenceArtifact} |
| 1380 | scriptArtifact={scriptArtifact} |
| 1381 | artifacts={Object.keys(stageStates).reduce((acc: any, key) => { |
| 1382 | acc[key] = stageStates[key].artifact; |
| 1383 | return acc; |
| 1384 | }, {})} |
| 1385 | /> |
| 1386 | ); |
| 1387 | }; |
| 1388 | |
| 1389 | const showHome = activeStage === null; |
| 1390 | |
| 1391 | return ( |
| 1392 | <div className="flex h-screen w-full min-w-0 flex-col bg-gray-50/50"> |
| 1393 | <TopBar |
| 1394 | activeStage={activeStage} |
| 1395 | stageStatuses={stageStatuses} |
| 1396 | onStageClick={handleStageClick} |
| 1397 | onHomeClick={handleGoHome} |
| 1398 | hasSession={sessionId !== null} |
| 1399 | isRunning={effectiveIsRunning} |
| 1400 | onStop={handleStop} |
| 1401 | autoMode={autoMode} |
| 1402 | onAutoModeChange={handleAutoModeChange} |
| 1403 | modelConfig={modelConfig} |
| 1404 | onModelConfigChange={handleModelConfigChange} |
| 1405 | projectStatus={projectStatus} |
| 1406 | /> |
| 1407 | |
| 1408 | <main className="flex-1 min-w-0 overflow-hidden"> |
| 1409 | {showHome ? ( |
| 1410 | <HomePage |
| 1411 | onStartProject={handleStartProject} |
| 1412 | onResumeProject={handleResumeProject} |
| 1413 | onDeleteSession={handleDeleteSession} |
| 1414 | history={history} |
| 1415 | /> |
| 1416 | ) : ( |
| 1417 | renderStageContent() |
| 1418 | )} |
| 1419 | </main> |
| 1420 | </div> |
| 1421 | ); |
| 1422 | } |
| 1423 |