import { InteractiveSpeakingExercise, SpeakingExercise } from "@/interfaces/exam"; import { Avatar } from "../speaking"; import axios from "axios"; interface VideoResponse { status: 'STARTED' | 'ERROR' | 'COMPLETED' | 'IN_PROGRESS'; result: string; } interface VideoGeneration { index: number; text: string; videoId?: string; url?: string; } export async function generateVideos(section: InteractiveSpeakingExercise | SpeakingExercise, focusedSection: number, selectedAvatar: Avatar | null, speakingAvatars: Avatar[]) { const abortController = new AbortController(); let activePollingIds: string[] = []; const avatarToUse = selectedAvatar || speakingAvatars[Math.floor(Math.random() * speakingAvatars.length)]; const pollVideoGeneration = async (videoId: string): Promise => { while (true) { try { const { data } = await axios.get(`api/exam/media/poll?videoId=${videoId}`, { signal: abortController.signal }); if (data.status === 'ERROR') { abortController.abort(); throw new Error('Video generation failed'); } if (data.status === 'COMPLETED') { const videoResponse = await axios.get(data.result, { responseType: 'blob', signal: abortController.signal }); const videoUrl = URL.createObjectURL( new Blob([videoResponse.data], { type: 'video/mp4' }) ); return videoUrl; } await new Promise(resolve => setTimeout(resolve, 10000)); // 10 secs } catch (error: any) { if (error.name === 'AbortError' || axios.isCancel(error)) { throw new Error('Operation aborted'); } throw error; } } }; const generateSingleVideo = async (text: string, index: number): Promise => { try { const { data } = await axios.post('/api/exam/media/speaking', { text, avatar: avatarToUse.name }, { headers: { 'Content-Type': 'application/json', }, signal: abortController.signal } ); if (data.status === 'ERROR') { abortController.abort(); throw new Error('Initial video generation failed'); } activePollingIds.push(data.result); const videoUrl = await pollVideoGeneration(data.result); return { index, text, videoId: data.result, url: videoUrl }; } catch (error) { abortController.abort(); throw error; } }; try { let videosToGenerate: { text: string; index: number }[] = []; switch (focusedSection) { case 1: { const interactiveSection = section as InteractiveSpeakingExercise; videosToGenerate = interactiveSection.prompts.map((prompt, index) => ({ text: index === 0 ? prompt.text.replace("{avatar}", avatarToUse.name) : prompt.text, index })); break; } case 2: { const speakingSection = section as SpeakingExercise; videosToGenerate = [{ text: `${speakingSection.text}. You have 1 minute to take notes.`, index: 0 }]; break; } case 3: { const interactiveSection = section as InteractiveSpeakingExercise; videosToGenerate = interactiveSection.prompts.map((prompt, index) => ({ text: prompt.text, index })); break; } } // Generate all videos concurrently const results = await Promise.all( videosToGenerate.map(({ text, index }) => generateSingleVideo(text, index)) ); // by order which they came in return results.sort((a, b) => a.index - b.index); } catch (error) { // Clean up any ongoing requests abortController.abort(); // Clean up any created URLs activePollingIds.forEach(id => { if (id) URL.revokeObjectURL(id); }); throw error; } }