import React, { useState, useRef } from 'react'; import { Box, Typography, Alert, LinearProgress, TextField, MenuItem, } from '@mui/material'; import SmartDisplayIcon from '@mui/icons-material/SmartDisplay'; import { useStoryWriterState } from '../../../hooks/useStoryWriterState'; import { storyWriterApi } from '../../../services/storyWriterApi'; import { triggerSubscriptionError } from '../../../api/client'; import { OperationButton } from '../../shared/OperationButton'; import SceneVideoApproval from './SceneVideoApproval'; // Simple logger for frontend const logger = { error: (message: string, ...args: any[]) => console.error(`[HdVideoSection] ${message}`, ...args), warn: (message: string, ...args: any[]) => console.warn(`[HdVideoSection] ${message}`, ...args), info: (message: string, ...args: any[]) => console.info(`[HdVideoSection] ${message}`, ...args), }; const VIDEO_MODEL_OPTIONS = [ { id: 'huggingface:tencent/HunyuanVideo', provider: 'huggingface', model: 'tencent/HunyuanVideo', label: 'HuggingFace · HunyuanVideo', }, { id: 'wavespeed:hunyuan-video-1.5', provider: 'wavespeed', model: 'hunyuan-video-1.5', label: 'WaveSpeed · HunyuanVideo-1.5', }, ]; const getDefaultVideoOptionId = (storyMode: 'marketing' | 'pure') => storyMode === 'marketing' ? 'huggingface:tencent/HunyuanVideo' : 'wavespeed:hunyuan-video-1.5'; interface HdVideoSectionProps { state: ReturnType; error: string | null; onError: (error: string | null) => void; } export const HdVideoSection: React.FC = ({ state, onError }) => { const [isGeneratingHdVideo, setIsGeneratingHdVideo] = useState(false); const [hdVideoProgress, setHdVideoProgress] = useState(0); const [hdVideoMessage, setHdVideoMessage] = useState(''); const [hdVideoPrompts, setHdVideoPrompts] = useState>(new Map()); const [selectedVideoOptionId, setSelectedVideoOptionId] = useState( getDefaultVideoOptionId(state.storyMode), ); const [approvalModal, setApprovalModal] = useState<{ open: boolean; sceneNumber: number; sceneTitle: string; videoUrl: string; promptUsed: string; } | null>(null); const [regeneratingScene, setRegeneratingScene] = useState(null); const processSceneRef = useRef<((sceneIndex: number) => Promise) | null>(null); const selectedVideoOption = VIDEO_MODEL_OPTIONS.find((option) => option.id === selectedVideoOptionId) || VIDEO_MODEL_OPTIONS[0]; const handleGenerateHdVideo = async () => { if (!state.outlineScenes || state.outlineScenes.length === 0) { onError('Please generate a structured outline first'); return; } const scenes = state.outlineScenes; const totalScenes = scenes.length; if (!state.sceneHdVideos) { state.setSceneHdVideos(new Map()); } setHdVideoPrompts(new Map()); state.setHdVideoGenerationStatus('generating'); setIsGeneratingHdVideo(true); onError(null); const storyContext = { persona: state.persona, story_setting: state.storySetting, characters: state.characters, plot_elements: state.plotElements, writing_style: state.writingStyle, story_tone: state.storyTone, narrative_pov: state.narrativePOV, audience_age_group: state.audienceAgeGroup, content_rating: state.contentRating, premise: state.premise || '', outline: state.outline || '', story_content: state.storyContent || '', }; const processScene = async (sceneIndex: number): Promise => { if (sceneIndex >= totalScenes) { state.setHdVideoGenerationStatus('completed'); setIsGeneratingHdVideo(false); setHdVideoProgress(100); const approvedCount = state.sceneHdVideos?.size || 0; setHdVideoMessage(`HD video generation complete! ${approvedCount} of ${totalScenes} scenes approved.`); return; } const scene = scenes[sceneIndex]; const sceneNumber = scene.scene_number || sceneIndex + 1; state.setCurrentHdSceneIndex(sceneIndex); setHdVideoProgress(Math.round((sceneIndex / totalScenes) * 100)); setHdVideoMessage(`Generating HD video for Scene ${sceneNumber}...`); try { const result = await storyWriterApi.generateHdVideoScene({ scene_number: sceneNumber, scene_data: scene, story_context: storyContext, all_scenes: scenes, provider: selectedVideoOption.provider, model: selectedVideoOption.model, num_frames: 50, guidance_scale: 7.5, }); setHdVideoPrompts((prev) => { const newPrompts = new Map(prev); newPrompts.set(sceneNumber, result.prompt_used); return newPrompts; }); state.setHdVideoGenerationStatus('awaiting_approval'); setApprovalModal({ open: true, sceneNumber: sceneNumber, sceneTitle: scene.title || `Scene ${sceneNumber}`, videoUrl: result.video_url, promptUsed: result.prompt_used, }); } catch (err: any) { // Check if this is a subscription error (429/402) and trigger global subscription modal const status = err?.response?.status; if (status === 429 || status === 402) { const handled = await triggerSubscriptionError(err); if (handled) { // Subscription modal is showing, stop processing scenes setIsGeneratingHdVideo(false); state.setHdVideoGenerationStatus('idle'); return; } } // Extract error message as string (handle both string and object responses) let errorMessage: string; if (err.response?.data?.detail) { const detail = err.response.data.detail; if (typeof detail === 'string') { errorMessage = detail; } else if (typeof detail === 'object' && detail !== null) { // Handle object response like {error: "...", message: "..."} errorMessage = detail.message || detail.error || JSON.stringify(detail); } else { errorMessage = String(detail); } } else { errorMessage = err.message || `Failed to generate HD video for scene ${sceneNumber}`; } onError(errorMessage); // CRITICAL: Stop processing on ANY error to prevent wasting money on expensive video API calls // This is an expensive operation ($0.40/video) - don't continue if there's an error // Only retry/continue if the user explicitly requests it logger.error(`[HdVideoSection] Video generation failed for scene ${sceneNumber}: ${errorMessage}`); logger.error(`[HdVideoSection] Stopping video generation to prevent wasted API calls`); setIsGeneratingHdVideo(false); state.setHdVideoGenerationStatus('idle'); // Don't continue to next scene - stop immediately to save money return; } }; processSceneRef.current = processScene; await processScene(0); }; const handleApprove = () => { if (!approvalModal) return; const sceneNumber = approvalModal.sceneNumber; const hdVideos = state.sceneHdVideos || new Map(); hdVideos.set(sceneNumber, approvalModal.videoUrl); state.setSceneHdVideos(new Map(hdVideos)); setApprovalModal(null); const currentIndex = state.currentHdSceneIndex; const scenes = state.outlineScenes || []; if (currentIndex + 1 < scenes.length && processSceneRef.current) { state.setHdVideoGenerationStatus('generating'); processSceneRef.current(currentIndex + 1); } else { state.setHdVideoGenerationStatus('completed'); setIsGeneratingHdVideo(false); const approvedCount = state.sceneHdVideos?.size || 0; setHdVideoMessage(`HD video generation complete! ${approvedCount} of ${scenes.length} scenes approved.`); } }; const handleReject = () => { if (!approvalModal) return; setApprovalModal(null); const currentIndex = state.currentHdSceneIndex; const scenes = state.outlineScenes || []; if (currentIndex + 1 < scenes.length && processSceneRef.current) { state.setHdVideoGenerationStatus('generating'); processSceneRef.current(currentIndex + 1); } else { state.setHdVideoGenerationStatus('completed'); setIsGeneratingHdVideo(false); const approvedCount = state.sceneHdVideos?.size || 0; setHdVideoMessage(`HD video generation complete! ${approvedCount} of ${scenes.length} scenes approved.`); } }; const handleRegenerate = async () => { if (!approvalModal) return; const sceneNumber = approvalModal.sceneNumber; const scenes = state.outlineScenes || []; const sceneIndex = scenes.findIndex((s: any) => (s.scene_number || 0) === sceneNumber); const scene = scenes[sceneIndex]; if (!scene) return; setRegeneratingScene(sceneNumber); try { const storyContext = { persona: state.persona, story_setting: state.storySetting, characters: state.characters, plot_elements: state.plotElements, writing_style: state.writingStyle, story_tone: state.storyTone, narrative_pov: state.narrativePOV, audience_age_group: state.audienceAgeGroup, content_rating: state.contentRating, premise: state.premise || '', outline: state.outline || '', story_content: state.storyContent || '', }; const result = await storyWriterApi.generateHdVideoScene({ scene_number: sceneNumber, scene_data: scene, story_context: storyContext, all_scenes: scenes, provider: selectedVideoOption.provider, model: selectedVideoOption.model, num_frames: 50, guidance_scale: 7.5, }); setHdVideoPrompts((prev) => { const newPrompts = new Map(prev); newPrompts.set(sceneNumber, result.prompt_used); return newPrompts; }); setApprovalModal({ open: true, sceneNumber: sceneNumber, sceneTitle: scene.title || `Scene ${sceneNumber}`, videoUrl: result.video_url, promptUsed: result.prompt_used, }); } catch (err: any) { // Check if this is a subscription error (429/402) and trigger global subscription modal const status = err?.response?.status; if (status === 429 || status === 402) { const handled = await triggerSubscriptionError(err); if (handled) { // Subscription modal is showing, stop here setRegeneratingScene(null); return; } } // Extract error message as string (handle both string and object responses) let errorMessage: string; if (err.response?.data?.detail) { const detail = err.response.data.detail; if (typeof detail === 'string') { errorMessage = detail; } else if (typeof detail === 'object' && detail !== null) { // Handle object response like {error: "...", message: "..."} errorMessage = detail.message || detail.error || JSON.stringify(detail); } else { errorMessage = String(detail); } } else { errorMessage = err.message || 'Failed to regenerate video'; } onError(errorMessage); } finally { setRegeneratingScene(null); } }; return ( <> setSelectedVideoOptionId(e.target.value)} size="small" sx={{ maxWidth: 320 }} disabled={ isGeneratingHdVideo || state.hdVideoGenerationStatus === 'awaiting_approval' || state.hdVideoGenerationStatus === 'generating' } > {VIDEO_MODEL_OPTIONS.map((option) => ( {option.label} ))} } showCost={true} checkOnHover={true} checkOnMount={false} onClick={handleGenerateHdVideo} disabled={isGeneratingHdVideo || state.hdVideoGenerationStatus === 'awaiting_approval'} loading={isGeneratingHdVideo || state.hdVideoGenerationStatus === 'awaiting_approval'} tooltipPlacement="top" buttonProps={{ children: isGeneratingHdVideo || state.hdVideoGenerationStatus === 'awaiting_approval' ? 'Generating HD Animation...' : undefined, }} /> {(isGeneratingHdVideo || state.hdVideoGenerationStatus === 'generating' || state.hdVideoGenerationStatus === 'awaiting_approval') && ( {hdVideoMessage || 'Generating HD video...'} {hdVideoProgress}% {state.hdVideoGenerationStatus === 'awaiting_approval' && ( ⏸ Awaiting your approval for Scene {state.currentHdSceneIndex + 1} of {state.outlineScenes?.length || 0} )} {state.hdVideoGenerationStatus === 'generating' && ( Processing Scene {state.currentHdSceneIndex + 1} of {state.outlineScenes?.length || 0}... )} {state.sceneHdVideos && state.sceneHdVideos.size > 0 && ( ✓ {state.sceneHdVideos.size} of {state.outlineScenes?.length || 0} scenes approved )} {hdVideoPrompts.size > 0 && ( Generated Prompts: {Array.from(hdVideoPrompts.entries()) .sort(([a], [b]) => a - b) .map(([sceneNum, prompt]) => ( Scene {sceneNum}: {prompt.length > 200 ? `${prompt.substring(0, 200)}...` : prompt} ))} )} )} {state.hdVideoGenerationStatus === 'completed' && ( HD video generation complete! {state.sceneHdVideos?.size || 0} of {state.outlineScenes?.length || 0} scenes were approved. )} {approvalModal && state.outlineScenes && ( { if (!isGeneratingHdVideo && !regeneratingScene) { setApprovalModal(null); state.setHdVideoGenerationStatus('paused'); } }} /> )} ); };