From 2c10a203a519cf3d9b131d9927ac631d780b67d9 Mon Sep 17 00:00:00 2001 From: Tiago Ribeiro Date: Fri, 14 Jul 2023 12:08:25 +0100 Subject: [PATCH] Finalized the Speaking module exercise --- src/components/Exercises/Speaking.tsx | 21 +++++++-- src/components/Exercises/Writing.tsx | 2 +- src/interfaces/exam.ts | 7 ++- src/pages/api/evaluate/speaking.ts | 21 +++++---- src/pages/api/speaking.ts | 22 ++++++++++ src/pages/exam.tsx | 62 ++++++++++++++++++++++++--- src/pages/exercises.tsx | 4 +- src/utils/score.ts | 13 ++++++ 8 files changed, 128 insertions(+), 24 deletions(-) create mode 100644 src/pages/api/speaking.ts diff --git a/src/components/Exercises/Speaking.tsx b/src/components/Exercises/Speaking.tsx index 1cbfb7ca..cc7b2fdd 100644 --- a/src/components/Exercises/Speaking.tsx +++ b/src/components/Exercises/Speaking.tsx @@ -38,6 +38,7 @@ export default function Speaking({id, title, text, type, prompts, onNext, onBack const formData = new FormData(); formData.append("audio", audioFile, "audio.wav"); + formData.append("question", `${text.replaceAll("\n", "")} You should talk about: ${prompts.join(", ")}`); const config = { headers: { @@ -51,7 +52,7 @@ export default function Speaking({id, title, text, type, prompts, onNext, onBack }; if (mediaBlob) uploadFile(); - }, [mediaBlob]); + }, [mediaBlob, text, prompts]); return (
@@ -200,14 +201,28 @@ export default function Speaking({id, title, text, type, prompts, onNext, onBack diff --git a/src/components/Exercises/Writing.tsx b/src/components/Exercises/Writing.tsx index 43b82d64..07f14ecb 100644 --- a/src/components/Exercises/Writing.tsx +++ b/src/components/Exercises/Writing.tsx @@ -100,7 +100,7 @@ export default function Writing({id, prompt, info, type, wordCounter, attachment diff --git a/src/interfaces/exam.ts b/src/interfaces/exam.ts index 0bbb8f52..c09137c0 100644 --- a/src/interfaces/exam.ts +++ b/src/interfaces/exam.ts @@ -69,12 +69,11 @@ export type Exercise = | WritingExercise | SpeakingExercise; -export interface WritingEvaluation { +export interface Evaluation { comment: string; overall: number; task_response: {[key: string]: number}; } - export interface WritingExercise { id: string; type: "writing"; @@ -85,11 +84,10 @@ export interface WritingExercise { url: string; description: string; }; //* The url for an image to work as an attachment to show the user - evaluation?: WritingEvaluation; userSolutions: { id: string; solution: string; - evaluation?: WritingEvaluation; + evaluation?: Evaluation; }[]; } @@ -102,6 +100,7 @@ export interface SpeakingExercise { userSolutions: { id: string; solution: string; + evaluation?: Evaluation; }[]; } diff --git a/src/pages/api/evaluate/speaking.ts b/src/pages/api/evaluate/speaking.ts index eb42337f..beef374a 100644 --- a/src/pages/api/evaluate/speaking.ts +++ b/src/pages/api/evaluate/speaking.ts @@ -1,6 +1,5 @@ // Next.js API route support: https://nextjs.org/docs/api-routes/introduction import type {NextApiRequest, NextApiResponse} from "next"; -import {getFirestore, doc, getDoc} from "firebase/firestore"; import {withIronSessionApiRoute} from "iron-session/next"; import {sessionOptions} from "@/lib/session"; import axios from "axios"; @@ -8,6 +7,7 @@ import formidable from "formidable"; import PersistentFile from "formidable/PersistentFile"; import {getStorage, ref, uploadBytes} from "firebase/storage"; import fs from "fs"; +import {app} from "@/firebase"; export default withIronSessionApiRoute(handler, sessionOptions); @@ -17,7 +17,7 @@ async function handler(req: NextApiRequest, res: NextApiResponse) { return; } - const storage = getStorage(); + const storage = getStorage(app); const form = formidable({keepExtensions: true, uploadDir: "./"}); form.parse(req, (err, fields, files) => { @@ -26,17 +26,20 @@ async function handler(req: NextApiRequest, res: NextApiResponse) { const binary = fs.readFileSync((audioFile as any).filepath).buffer; uploadBytes(audioFileRef, binary).then(async (snapshot) => { - // const backendRequest = await axios.post(`${process.env.BACKEND_URL}/speaking_task`, req.body as Body, { - // headers: { - // Authorization: `Bearer ${process.env.BACKEND_JWT}`, - // }, - // }); + const backendRequest = await axios.post( + `${process.env.BACKEND_URL}/speaking_task_1`, + {question: (fields.question as string[]).join(""), answer: snapshot.metadata.fullPath}, + { + headers: { + Authorization: `Bearer ${process.env.BACKEND_JWT}`, + }, + }, + ); fs.rmSync((audioFile as any).filepath); + res.status(200).json({...backendRequest.data, fullPath: snapshot.metadata.fullPath}); }); }); - - res.status(200).json({ok: true}); } export const config = { diff --git a/src/pages/api/speaking.ts b/src/pages/api/speaking.ts new file mode 100644 index 00000000..5678693c --- /dev/null +++ b/src/pages/api/speaking.ts @@ -0,0 +1,22 @@ +// Next.js API route support: https://nextjs.org/docs/api-routes/introduction +import type {NextApiRequest, NextApiResponse} from "next"; +import {withIronSessionApiRoute} from "iron-session/next"; +import {sessionOptions} from "@/lib/session"; +import {getDownloadURL, getStorage, ref} from "firebase/storage"; +import {app} from "@/firebase"; + +// export default withIronSessionApiRoute(handler, sessionOptions); +export default handler; + +async function handler(req: NextApiRequest, res: NextApiResponse) { + // if (!req.session.user) { + // res.status(401).json({ok: false}); + // return; + // } + + const storage = getStorage(app); + const {path} = req.body as {path: string}; + + const pathReference = ref(storage, path); + getDownloadURL(pathReference).then((url) => res.status(200).json({url})); +} diff --git a/src/pages/exam.tsx b/src/pages/exam.tsx index f702939e..b7b5e5e7 100644 --- a/src/pages/exam.tsx +++ b/src/pages/exam.tsx @@ -5,7 +5,17 @@ import {Module} from "@/interfaces"; import Selection from "@/exams/Selection"; import Reading from "@/exams/Reading"; -import {Exam, ListeningExam, ReadingExam, SpeakingExam, UserSolution, WritingEvaluation, WritingExam, WritingExercise} from "@/interfaces/exam"; +import { + Exam, + ListeningExam, + ReadingExam, + SpeakingExam, + UserSolution, + Evaluation, + WritingExam, + WritingExercise, + SpeakingExercise, +} from "@/interfaces/exam"; import Listening from "@/exams/Listening"; import Writing from "@/exams/Writing"; import {ToastContainer, toast} from "react-toastify"; @@ -19,7 +29,7 @@ import {v4 as uuidv4} from "uuid"; import useUser from "@/hooks/useUser"; import useExamStore from "@/stores/examStore"; import Layout from "@/components/High/Layout"; -import {writingReverseMarking} from "@/utils/score"; +import {speakingReverseMarking, writingReverseMarking} from "@/utils/score"; export const getServerSideProps = withIronSessionSsr(({req, res}) => { const user = req.session.user; @@ -119,11 +129,47 @@ export default function Page() { } }; + const evaluateSpeakingAnswer = async (examId: string, exerciseId: string, solution: UserSolution) => { + const speakingExam = exams.find((x) => x.id === examId)!; + const exercise = speakingExam.exercises.find((x) => x.id === exerciseId)! as SpeakingExercise; + + const blobResponse = await axios.get(exercise.userSolutions[0].solution.trim()); + const audioBlob = Buffer.from(blobResponse.data, "binary"); + const audioFile = new File([audioBlob], "audio.wav", {type: "audio/wav"}); + + const formData = new FormData(); + formData.append("audio", audioFile, "audio.wav"); + formData.append("question", `${exercise.text.replaceAll("\n", "")} You should talk about: ${exercise.prompts.join(", ")}`); + + const config = { + headers: { + "Content-Type": "audio/mp3", + }, + }; + + const response = await axios.post("/api/evaluate/speaking", formData, config); + + if (response.status === 200) { + setUserSolutions([ + ...userSolutions.filter((x) => x.exercise !== exerciseId), + { + ...solution, + score: { + correct: speakingReverseMarking[response.data.overall], + missing: 0, + total: 100, + }, + solutions: [{id: exerciseId, solution: response.data.fullPath, evaluation: response.data}], + }, + ]); + } + }; + const evaluateWritingAnswer = async (examId: string, exerciseId: string, solution: UserSolution) => { const writingExam = exams.find((x) => x.id === examId)!; const exercise = writingExam.exercises.find((x) => x.id === exerciseId)! as WritingExercise; - const response = await axios.post("/api/evaluate/writing", { + const response = await axios.post("/api/evaluate/writing", { question: `${exercise.prompt} ${exercise.attachment ? exercise.attachment.description : ""}`.replaceAll("\n", ""), answer: solution.solutions[0].solution.trim().replaceAll("\n", " "), }); @@ -155,11 +201,17 @@ export default function Page() { const onFinish = (solutions: UserSolution[]) => { const solutionIds = solutions.map((x) => x.exercise); - if (exam && exam.module === "writing" && solutions.length > 0 && !showSolutions) { + if (exam && (exam.module === "writing" || exam.module === "speaking") && solutions.length > 0 && !showSolutions) { setHasBeenUploaded(true); setIsEvaluationLoading(true); Promise.all( - exam.exercises.map((exercise) => evaluateWritingAnswer(exam.id, exercise.id, solutions.find((x) => x.exercise === exercise.id)!)), + exam.exercises.map((exercise) => + (exam.module === "writing" ? evaluateWritingAnswer : evaluateSpeakingAnswer)( + exam.id, + exercise.id, + solutions.find((x) => x.exercise === exercise.id)!, + ), + ), ).finally(() => { setIsEvaluationLoading(false); setHasBeenUploaded(false); diff --git a/src/pages/exercises.tsx b/src/pages/exercises.tsx index d85b69b8..1f3a008b 100644 --- a/src/pages/exercises.tsx +++ b/src/pages/exercises.tsx @@ -6,7 +6,7 @@ import {Module} from "@/interfaces"; import Selection from "@/exams/Selection"; import Reading from "@/exams/Reading"; -import {Exam, ListeningExam, ReadingExam, SpeakingExam, UserSolution, WritingEvaluation, WritingExam, WritingExercise} from "@/interfaces/exam"; +import {Exam, ListeningExam, ReadingExam, SpeakingExam, UserSolution, Evaluation, WritingExam, WritingExercise} from "@/interfaces/exam"; import Listening from "@/exams/Listening"; import Writing from "@/exams/Writing"; import {ToastContainer, toast} from "react-toastify"; @@ -126,7 +126,7 @@ export default function Page() { const writingExam = exams.find((x) => x.id === examId)!; const exercise = writingExam.exercises.find((x) => x.id === exerciseId)! as WritingExercise; - const response = await axios.post("/api/evaluate/writing", { + const response = await axios.post("/api/evaluate/writing", { question: `${exercise.prompt} ${exercise.attachment ? exercise.attachment.description : ""}`.replaceAll("\n", ""), answer: solution.solutions[0].solution.trim().replaceAll("\n", " "), }); diff --git a/src/utils/score.ts b/src/utils/score.ts index 68359686..2c9bb5da 100644 --- a/src/utils/score.ts +++ b/src/utils/score.ts @@ -15,6 +15,19 @@ export const writingReverseMarking: {[key: number]: number} = { 0: 0, }; +export const speakingReverseMarking: {[key: number]: number} = { + 9: 90, + 8: 80, + 7: 70, + 6: 60, + 5: 50, + 4: 40, + 3: 30, + 2: 20, + 1: 10, + 0: 0, +}; + const writingMarking: {[key: number]: number} = { 90: 9, 80: 8,