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,