Reverted Level to only utas placement test exercises, Speaking, bug fixes, placeholder
This commit is contained in:
@@ -42,6 +42,8 @@ export function generate(
|
||||
|
||||
const url = `/api/exam/generate/${module}/${sectionId}${queryString ? `?${queryString}` : ''}`;
|
||||
|
||||
console.log(config.body);
|
||||
|
||||
const request = config.method === 'POST'
|
||||
? axios.post(url, config.body)
|
||||
: axios.get(url);
|
||||
|
||||
@@ -14,7 +14,12 @@ interface Props {
|
||||
}
|
||||
|
||||
const GenerateBtn: React.FC<Props> = ({module, sectionId, genType, generateFnc, className}) => {
|
||||
const {generating} = useExamEditorStore((store) => store.modules[module].sections.find((s)=> s.sectionId == sectionId))!;
|
||||
const section = useExamEditorStore((store) => store.modules[module].sections.find((s)=> s.sectionId == sectionId));
|
||||
if (section === undefined) return;
|
||||
|
||||
const {generating} = section;
|
||||
|
||||
|
||||
const loading = generating && generating === genType;
|
||||
return (
|
||||
<button
|
||||
|
||||
@@ -0,0 +1,125 @@
|
||||
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;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user