Updated the speaking and interactive speaking to the new format
This commit is contained in:
@@ -16,8 +16,9 @@ const ReactMediaRecorder = dynamic(() => import("react-media-recorder").then((mo
|
||||
export default function InteractiveSpeaking({
|
||||
id,
|
||||
title,
|
||||
first_title,
|
||||
second_title,
|
||||
examID,
|
||||
text,
|
||||
type,
|
||||
prompts,
|
||||
userSolutions,
|
||||
@@ -35,31 +36,6 @@ export default function InteractiveSpeaking({
|
||||
|
||||
const hasExamEnded = useExamStore((state) => state.hasExamEnded);
|
||||
|
||||
const saveToStorage = async (previousURL?: string) => {
|
||||
if (mediaBlob && mediaBlob.startsWith("blob")) {
|
||||
const blobBuffer = await downloadBlob(mediaBlob);
|
||||
const audioFile = new File([blobBuffer], "audio.wav", {type: "audio/wav"});
|
||||
|
||||
const seed = Math.random().toString().replace("0.", "");
|
||||
|
||||
const formData = new FormData();
|
||||
formData.append("audio", audioFile, `${seed}.wav`);
|
||||
formData.append("root", "speaking_recordings");
|
||||
|
||||
const config = {
|
||||
headers: {
|
||||
"Content-Type": "audio/wav",
|
||||
},
|
||||
};
|
||||
|
||||
const response = await axios.post<{path: string}>("/api/storage/insert", formData, config);
|
||||
if (previousURL && !previousURL.startsWith("blob")) await axios.post("/api/storage/delete", {path: previousURL});
|
||||
return response.data.path;
|
||||
}
|
||||
|
||||
return undefined;
|
||||
};
|
||||
|
||||
const back = async () => {
|
||||
setIsLoading(true);
|
||||
|
||||
@@ -177,7 +153,7 @@ export default function InteractiveSpeaking({
|
||||
<div className="flex flex-col h-full w-full gap-9">
|
||||
<div className="flex flex-col w-full gap-8 bg-mti-gray-smoke rounded-xl py-8 pb-12 px-16">
|
||||
<div className="flex flex-col gap-3">
|
||||
<span className="font-semibold">{title}</span>
|
||||
<span className="font-semibold">{!!first_title && !!second_title ? `${first_title} & ${second_title}` : title}</span>
|
||||
</div>
|
||||
{prompts && prompts.length > 0 && (
|
||||
<div className="flex flex-col gap-4 w-full items-center">
|
||||
|
||||
@@ -2,7 +2,7 @@ import {renderExercise} from "@/components/Exercises";
|
||||
import ModuleTitle from "@/components/Medium/ModuleTitle";
|
||||
import {renderSolution} from "@/components/Solutions";
|
||||
import {infoButtonStyle} from "@/constants/buttonStyles";
|
||||
import {UserSolution, SpeakingExam} from "@/interfaces/exam";
|
||||
import {UserSolution, SpeakingExam, SpeakingExercise, InteractiveSpeakingExercise} from "@/interfaces/exam";
|
||||
import useExamStore from "@/stores/examStore";
|
||||
import {defaultUserSolutions} from "@/utils/exams";
|
||||
import {countExercises} from "@/utils/moduleUtils";
|
||||
@@ -64,8 +64,9 @@ export default function Speaking({exam, showSolutions = false, onFinish}: Props)
|
||||
const exercise = exam.exercises[exerciseIndex];
|
||||
return {
|
||||
...exercise,
|
||||
variant: exerciseIndex < 2 && exercise.type === "interactiveSpeaking" ? "initial" : undefined,
|
||||
userSolutions: userSolutions.find((x) => x.exercise === exercise.id)?.solutions || [],
|
||||
};
|
||||
} as SpeakingExercise | InteractiveSpeakingExercise;
|
||||
};
|
||||
|
||||
return (
|
||||
|
||||
@@ -177,6 +177,8 @@ export interface InteractiveSpeakingExercise {
|
||||
id: string;
|
||||
type: "interactiveSpeaking";
|
||||
title: string;
|
||||
first_title?: string;
|
||||
second_title?: string;
|
||||
text: string;
|
||||
prompts: {text: string; video_url: string}[];
|
||||
userSolutions: {
|
||||
@@ -185,6 +187,9 @@ export interface InteractiveSpeakingExercise {
|
||||
evaluation?: InteractiveSpeakingEvaluation;
|
||||
}[];
|
||||
topic?: string;
|
||||
first_topic?: string;
|
||||
second_topic?: string;
|
||||
variant?: "initial" | "final";
|
||||
}
|
||||
|
||||
export interface FillBlanksExercise {
|
||||
|
||||
@@ -133,14 +133,22 @@ export default function ExamPage({page}: Props) {
|
||||
!!exam &&
|
||||
timeSpent > 0 &&
|
||||
!showSolutions &&
|
||||
moduleIndex < selectedModules.length
|
||||
moduleIndex < selectedModules.length &&
|
||||
selectedModules[moduleIndex] !== "speaking"
|
||||
)
|
||||
saveSession();
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [assignment, exam, exams, moduleIndex, selectedModules, sessionId, userSolutions, user, exerciseIndex, partIndex, questionIndex]);
|
||||
|
||||
useEffect(() => {
|
||||
if (timeSpent % 20 === 0 && timeSpent > 0 && moduleIndex < selectedModules.length && !showSolutions) saveSession();
|
||||
if (
|
||||
timeSpent % 20 === 0 &&
|
||||
timeSpent > 0 &&
|
||||
moduleIndex < selectedModules.length &&
|
||||
selectedModules[moduleIndex] !== "speaking" &&
|
||||
!showSolutions
|
||||
)
|
||||
saveSession();
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [timeSpent]);
|
||||
|
||||
|
||||
@@ -45,6 +45,7 @@ const PartTab = ({
|
||||
.then((result) => {
|
||||
playSound(typeof result.data === "string" ? "error" : "check");
|
||||
if (typeof result.data === "string") return toast.error("Something went wrong, please try to generate again.");
|
||||
console.log(result.data);
|
||||
setPart(result.data);
|
||||
})
|
||||
.catch((error) => {
|
||||
@@ -54,7 +55,7 @@ const PartTab = ({
|
||||
.finally(() => setIsLoading(false));
|
||||
};
|
||||
|
||||
const generateVideo = () => {
|
||||
const generateVideo = async () => {
|
||||
if (!part) return toast.error("Please generate the first part before generating the video!");
|
||||
toast.info("This will take quite a while, please do not leave this page or close the tab/window.");
|
||||
|
||||
@@ -64,11 +65,12 @@ const PartTab = ({
|
||||
const initialTime = moment();
|
||||
|
||||
axios
|
||||
.post(`/api/exam/speaking/generate/speaking/generate_${index === 3 ? "interactive" : "speaking"}_video`, {...part, avatar: avatar?.id})
|
||||
.post(`/api/exam/speaking/generate/speaking/generate_video_${index}`, {...part, avatar: avatar?.id})
|
||||
.then((result) => {
|
||||
const isError = typeof result.data === "string" || moment().diff(initialTime, "seconds") < 60;
|
||||
|
||||
playSound(isError ? "error" : "check");
|
||||
console.log(result.data);
|
||||
if (isError) return toast.error("Something went wrong, please try to generate the video again.");
|
||||
setPart({...part, result: {...result.data, topic: part?.topic}, gender, avatar});
|
||||
})
|
||||
@@ -139,7 +141,9 @@ const PartTab = ({
|
||||
)}
|
||||
{part && !isLoading && (
|
||||
<div className="flex flex-col gap-2 w-full overflow-y-scroll scrollbar-hide h-96">
|
||||
<h3 className="text-xl font-semibold">{part.topic}</h3>
|
||||
<h3 className="text-xl font-semibold">
|
||||
{!!part.first_topic && !!part.second_topic ? `${part.first_topic} & ${part.second_topic}` : part.topic}
|
||||
</h3>
|
||||
{part.question && <span className="w-full">{part.question}</span>}
|
||||
{part.questions && (
|
||||
<div className="flex flex-col gap-1">
|
||||
@@ -177,6 +181,8 @@ interface SpeakingPart {
|
||||
question?: string;
|
||||
questions?: string[];
|
||||
topic: string;
|
||||
first_topic?: string;
|
||||
second_topic?: string;
|
||||
result?: SpeakingExercise | InteractiveSpeakingExercise;
|
||||
gender?: "male" | "female";
|
||||
avatar?: (typeof AVATARS)[number];
|
||||
@@ -208,10 +214,18 @@ const SpeakingGeneration = () => {
|
||||
|
||||
const genders = [part1?.gender, part2?.gender, part3?.gender].filter((x) => !!x);
|
||||
|
||||
const exercises = [part1?.result, part2?.result, part3?.result]
|
||||
.filter((x) => !!x)
|
||||
.map((x) => ({
|
||||
...x,
|
||||
first_title: x?.type === "interactiveSpeaking" ? x.first_topic : undefined,
|
||||
second_title: x?.type === "interactiveSpeaking" ? x.second_topic : undefined,
|
||||
}));
|
||||
|
||||
const exam: SpeakingExam = {
|
||||
id: v4(),
|
||||
isDiagnostic: false,
|
||||
exercises: [part1?.result, part2?.result, part3?.result].filter((x) => !!x) as (SpeakingExercise | InteractiveSpeakingExercise)[],
|
||||
exercises: exercises as (SpeakingExercise | InteractiveSpeakingExercise)[],
|
||||
minTimer,
|
||||
variant: minTimer >= 14 ? "full" : "partial",
|
||||
module: "speaking",
|
||||
|
||||
@@ -47,7 +47,7 @@ async function handler(req: NextApiRequest, res: NextApiResponse) {
|
||||
res.status(200).json(null);
|
||||
|
||||
console.log("🌱 - Still processing");
|
||||
const backendRequest = await evaluate({answers: uploadingAudios});
|
||||
const backendRequest = await evaluate({answers: uploadingAudios}, fields.variant);
|
||||
console.log("🌱 - Process complete");
|
||||
|
||||
const correspondingStat = await getCorrespondingStat(fields.id, 1);
|
||||
@@ -79,8 +79,8 @@ async function getCorrespondingStat(id: string, index: number): Promise<Stat> {
|
||||
return getCorrespondingStat(id, index + 1);
|
||||
}
|
||||
|
||||
async function evaluate(body: {answers: object[]}): Promise<AxiosResponse> {
|
||||
const backendRequest = await axios.post(`${process.env.BACKEND_URL}/speaking_task_3`, body, {
|
||||
async function evaluate(body: {answers: object[]}, variant?: "initial" | "final"): Promise<AxiosResponse> {
|
||||
const backendRequest = await axios.post(`${process.env.BACKEND_URL}/speaking_task_${variant === "initial" ? "1" : "3"}`, body, {
|
||||
headers: {
|
||||
Authorization: `Bearer ${process.env.BACKEND_JWT}`,
|
||||
},
|
||||
|
||||
@@ -78,7 +78,7 @@ async function getCorrespondingStat(id: string, index: number): Promise<Stat> {
|
||||
}
|
||||
|
||||
async function evaluate(body: {answer: string; question: string}, task: number): Promise<AxiosResponse> {
|
||||
const backendRequest = await axios.post(`${process.env.BACKEND_URL}/speaking_task_${task}`, body, {
|
||||
const backendRequest = await axios.post(`${process.env.BACKEND_URL}/speaking_task_2`, body, {
|
||||
headers: {
|
||||
Authorization: `Bearer ${process.env.BACKEND_JWT}`,
|
||||
},
|
||||
|
||||
@@ -51,7 +51,7 @@ export const evaluateSpeakingAnswer = async (
|
||||
case "speaking":
|
||||
return {...(await evaluateSpeakingExercise(exercise, exercise.id, solution, id, task)), id} as UserSolution;
|
||||
case "interactiveSpeaking":
|
||||
return {...(await evaluateInteractiveSpeakingExercise(exercise.id, solution, id)), id} as UserSolution;
|
||||
return {...(await evaluateInteractiveSpeakingExercise(exercise.id, solution, id, exercise.variant)), id} as UserSolution;
|
||||
default:
|
||||
return undefined;
|
||||
}
|
||||
@@ -110,7 +110,12 @@ const evaluateSpeakingExercise = async (
|
||||
return undefined;
|
||||
};
|
||||
|
||||
const evaluateInteractiveSpeakingExercise = async (exerciseId: string, solution: UserSolution, id: string): Promise<UserSolution | undefined> => {
|
||||
const evaluateInteractiveSpeakingExercise = async (
|
||||
exerciseId: string,
|
||||
solution: UserSolution,
|
||||
id: string,
|
||||
variant?: "initial" | "final",
|
||||
): Promise<UserSolution | undefined> => {
|
||||
const promiseParts = solution.solutions.map(async (x: {prompt: string; blob: string}) => {
|
||||
const blob = await downloadBlob(x.blob);
|
||||
if (!x.blob.startsWith("blob")) await axios.post("/api/storage/delete", {path: x.blob});
|
||||
@@ -132,6 +137,7 @@ const evaluateInteractiveSpeakingExercise = async (exerciseId: string, solution:
|
||||
formData.append(`answer_${seed}`, audioFile, `${seed}.wav`);
|
||||
});
|
||||
formData.append("id", id);
|
||||
formData.append("variant", variant || "final");
|
||||
|
||||
const config = {
|
||||
headers: {
|
||||
|
||||
Reference in New Issue
Block a user