126 lines
4.5 KiB
TypeScript
126 lines
4.5 KiB
TypeScript
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<string> => {
|
|
while (true) {
|
|
try {
|
|
const { data } = await axios.get<VideoResponse>(`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<VideoGeneration> => {
|
|
try {
|
|
const { data } = await axios.post<VideoResponse>('/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;
|
|
}
|
|
}
|