Files
ALwrity/frontend/src/components/YouTubeCreator/hooks/useYouTubeRenderQueue.ts
ajaysi 7512933c65 AI Image and Audio Generation Improvements.
AI Video Generation Pre-Flight Checklist. Cost Estimate Improvements.
2025-12-25 16:26:08 +05:30

269 lines
7.9 KiB
TypeScript

import { useCallback, useEffect, useRef, useState } from 'react';
import {
youtubeApi,
Scene,
SceneVideoRenderRequest,
TaskStatus,
VideoPlan,
} from '../../../services/youtubeApi';
type SceneStatus = 'idle' | 'running' | 'completed' | 'failed';
interface SceneVideoState {
status: SceneStatus;
progress: number;
taskId?: string;
error?: string;
videoUrl?: string;
}
interface UseYouTubeRenderQueueParams {
scenes: Scene[];
videoPlan: VideoPlan | null;
resolution: '480p' | '720p' | '1080p';
onScenesUpdate: (updated: Scene[]) => void;
onError?: (msg: string) => void;
onInfo?: (msg: string) => void;
onSuccess?: (msg: string) => void;
}
interface UseYouTubeRenderQueueResult {
sceneStatuses: Record<number, SceneVideoState>;
finalVideoUrl: string | null;
combining: boolean;
combiningProgress: number;
combiningMessage: string;
runSceneVideo: (scene: Scene) => Promise<void>;
combineVideos: () => Promise<void>;
}
const POLL_MS = 3000;
export function useYouTubeRenderQueue({
scenes,
videoPlan,
resolution,
onScenesUpdate,
onError,
onInfo,
onSuccess,
}: UseYouTubeRenderQueueParams): UseYouTubeRenderQueueResult {
const [sceneStatuses, setSceneStatuses] = useState<Record<number, SceneVideoState>>({});
const [finalVideoUrl, setFinalVideoUrl] = useState<string | null>(null);
const [combining, setCombining] = useState(false);
const [combiningProgress, setCombiningProgress] = useState(0);
const [combiningMessage, setCombiningMessage] = useState('Combining videos...');
const pollingRefs = useRef<Map<string, NodeJS.Timeout>>(new Map());
const updateSceneStatus = useCallback((sceneNumber: number, updates: Partial<SceneVideoState>) => {
setSceneStatuses((prev) => ({
...prev,
[sceneNumber]: {
...prev[sceneNumber],
status: prev[sceneNumber]?.status || 'idle',
progress: prev[sceneNumber]?.progress || 0,
...updates,
},
}));
}, []);
const clearPolling = useCallback((taskId: string) => {
const timers = pollingRefs.current;
timers.forEach((interval, key) => {
if (key === taskId) {
clearInterval(interval);
timers.delete(key);
}
});
}, []);
useEffect(() => {
return () => {
pollingRefs.current.forEach((interval) => clearInterval(interval));
pollingRefs.current.clear();
};
}, []);
const pollTask = useCallback(
(taskId: string, sceneNumber: number) => {
const interval = setInterval(async () => {
try {
const status: TaskStatus = await youtubeApi.getRenderStatus(taskId);
const progress = status.progress ?? 0;
if (status.status === 'completed') {
const videoUrl =
status.result?.video_url ||
status.result?.final_video_url ||
status.result?.scene_results?.[0]?.video_url ||
null;
updateSceneStatus(sceneNumber, {
status: 'completed',
progress: 100,
videoUrl: videoUrl || undefined,
taskId,
error: undefined,
});
if (videoUrl) {
const updatedScenes = scenes.map((s) =>
s.scene_number === sceneNumber ? { ...s, videoUrl } : s
);
onScenesUpdate(updatedScenes);
}
clearPolling(taskId);
} else if (status.status === 'failed') {
const errorMessage =
status.error ||
status.message ||
status.result?.error ||
'Video generation failed';
updateSceneStatus(sceneNumber, {
status: 'failed',
progress,
error: errorMessage,
taskId,
});
clearPolling(taskId);
onError?.(errorMessage);
} else {
updateSceneStatus(sceneNumber, {
status: 'running',
progress,
taskId,
});
}
} catch (err: any) {
const msg = err?.message || 'Failed to poll render status';
updateSceneStatus(sceneNumber, {
status: 'failed',
progress: 0,
error: msg,
taskId,
});
clearPolling(taskId);
onError?.(msg);
}
}, POLL_MS);
pollingRefs.current.set(taskId, interval);
},
[clearPolling, onError, onScenesUpdate, scenes, updateSceneStatus]
);
const runSceneVideo = useCallback(
async (scene: Scene) => {
if (!videoPlan) {
onError?.('Video plan is missing');
return;
}
const sn = scene.scene_number;
const existing = sceneStatuses[sn];
if (existing?.status === 'running') return;
updateSceneStatus(sn, { status: 'running', progress: 5, error: undefined });
const payload: SceneVideoRenderRequest = {
scene,
video_plan: videoPlan,
resolution,
generate_audio_enabled: false,
voice_id: 'Wise_Woman',
};
try {
const resp = await youtubeApi.generateSceneVideo(payload);
if (resp.success && resp.task_id) {
updateSceneStatus(sn, { status: 'running', progress: 5, taskId: resp.task_id });
pollTask(resp.task_id, sn);
} else {
const msg = resp.message || 'Failed to start scene render';
updateSceneStatus(sn, { status: 'failed', progress: 0, error: msg });
onError?.(msg);
}
} catch (err: any) {
const msg = err?.message || 'Failed to start scene render';
updateSceneStatus(sn, { status: 'failed', progress: 0, error: msg });
onError?.(msg);
}
},
[pollTask, resolution, sceneStatuses, updateSceneStatus, videoPlan, onError]
);
const combineVideos = useCallback(async () => {
const readyVideos = scenes
.filter((s) => s.enabled !== false && s.videoUrl)
.map((s) => s.videoUrl as string);
if (readyVideos.length < 2) {
onError?.('Need at least two scene videos to combine.');
return;
}
setCombining(true);
setCombiningProgress(5);
setCombiningMessage('Starting combination...');
try {
const resp = await youtubeApi.combineVideos({
scene_video_urls: readyVideos,
video_plan: videoPlan || undefined,
resolution,
});
if (!resp.success || !resp.task_id) {
const msg = resp.message || 'Failed to start video combine';
setCombining(false);
setCombiningProgress(0);
setCombiningMessage(msg);
onError?.(msg);
return;
}
const taskId = resp.task_id;
let done = false;
while (!done) {
await new Promise((r) => setTimeout(r, POLL_MS));
const status = await youtubeApi.getRenderStatus(taskId);
const progress = status.progress ?? 0;
setCombiningProgress(progress);
setCombiningMessage(status.message || 'Combining...');
if (status.status === 'completed') {
const url = status.result?.video_url || status.result?.final_video_url;
setFinalVideoUrl(url || null);
setCombining(false);
setCombiningProgress(100);
setCombiningMessage('Combined successfully');
onSuccess?.('Final video combined successfully');
done = true;
} else if (status.status === 'failed') {
const msg = status.error || status.message || 'Combine failed';
setCombining(false);
setCombiningMessage(msg);
onError?.(msg);
done = true;
}
}
} catch (err: any) {
const msg = err?.message || 'Combine failed';
setCombining(false);
setCombiningMessage(msg);
onError?.(msg);
}
}, [onError, resolution, scenes, videoPlan]);
return {
sceneStatuses,
finalVideoUrl,
combining,
combiningProgress,
combiningMessage,
runSceneVideo,
combineVideos,
};
}