diff --git a/package.json b/package.json
index ef580cc0..6f504b18 100644
--- a/package.json
+++ b/package.json
@@ -23,6 +23,8 @@
"eslint": "8.33.0",
"eslint-config-next": "13.1.6",
"firebase": "9.19.1",
+ "formidable": "^3.5.0",
+ "formidable-serverless": "^1.1.1",
"framer-motion": "^9.0.2",
"iron-session": "^6.3.1",
"lodash": "^4.17.21",
@@ -48,6 +50,7 @@
"zustand": "^4.3.6"
},
"devDependencies": {
+ "@types/formidable": "^3.4.0",
"@types/lodash": "^4.14.191",
"@types/uuid": "^9.0.1",
"@types/wavesurfer.js": "^6.0.6",
diff --git a/src/components/Exercises/Speaking.tsx b/src/components/Exercises/Speaking.tsx
index 55b19322..1cbfb7ca 100644
--- a/src/components/Exercises/Speaking.tsx
+++ b/src/components/Exercises/Speaking.tsx
@@ -1,14 +1,10 @@
-import {errorButtonStyle, infoButtonStyle} from "@/constants/buttonStyles";
-import {SpeakingExercise, WritingExercise} from "@/interfaces/exam";
-import {mdiArrowLeft, mdiArrowRight} from "@mdi/js";
-import Icon from "@mdi/react";
-import clsx from "clsx";
+import {SpeakingExercise} from "@/interfaces/exam";
import {CommonProps} from ".";
import {Fragment, useEffect, useState} from "react";
-import {toast} from "react-toastify";
import {BsCheckCircleFill, BsMicFill, BsPauseCircle, BsPlayCircle, BsTrashFill} from "react-icons/bs";
import dynamic from "next/dynamic";
import Button from "../Low/Button";
+import axios from "axios";
const Waveform = dynamic(() => import("../Waveform"), {ssr: false});
const ReactMediaRecorder = dynamic(() => import("react-media-recorder").then((mod) => mod.ReactMediaRecorder), {
@@ -33,6 +29,30 @@ export default function Speaking({id, title, text, type, prompts, onNext, onBack
};
}, [isRecording]);
+ useEffect(() => {
+ const uploadFile = () => {
+ if (mediaBlob) {
+ axios.get(mediaBlob, {responseType: "arraybuffer"}).then((response) => {
+ const audioBlob = Buffer.from(response.data, "binary");
+ const audioFile = new File([audioBlob], "audio.wav", {type: "audio/wav"});
+
+ const formData = new FormData();
+ formData.append("audio", audioFile, "audio.wav");
+
+ const config = {
+ headers: {
+ "Content-Type": "audio/mp3",
+ },
+ };
+
+ axios.post("/api/evaluate/speaking", formData, config);
+ });
+ }
+ };
+
+ if (mediaBlob) uploadFile();
+ }, [mediaBlob]);
+
return (
diff --git a/src/pages/api/evaluate/speaking.ts b/src/pages/api/evaluate/speaking.ts
new file mode 100644
index 00000000..ce275aba
--- /dev/null
+++ b/src/pages/api/evaluate/speaking.ts
@@ -0,0 +1,46 @@
+// 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";
+import formidable from "formidable";
+import PersistentFile from "formidable/PersistentFile";
+import {getStorage, ref, uploadBytes} from "firebase/storage";
+import fs from "fs";
+
+export default withIronSessionApiRoute(handler, sessionOptions);
+
+async function handler(req: NextApiRequest, res: NextApiResponse) {
+ if (!req.session.user) {
+ res.status(401).json({ok: false});
+ return;
+ }
+
+ const storage = getStorage();
+
+ const form = formidable({keepExtensions: true, uploadDir: "./"});
+ form.parse(req, (err, fields, files) => {
+ const audioFile = (files.audio as unknown as PersistentFile[])[0];
+ const audioFileRef = ref(storage, `speaking_recordings/${(audioFile as any).newFilename}`);
+
+ const binary = fs.readFileSync((audioFile as any).filepath).buffer;
+ uploadBytes(audioFileRef, binary).then(async (snapshot) => {
+ const backendRequest = await axios.post(`${process.env.BACKEND_URL}/writing_task2`, req.body as Body, {
+ headers: {
+ Authorization: `Bearer ${process.env.BACKEND_JWT}`,
+ },
+ });
+
+ fs.rmSync((audioFile as any).filepath);
+ });
+ });
+
+ res.status(200).json({ok: true});
+}
+
+export const config = {
+ api: {
+ bodyParser: false,
+ },
+};
diff --git a/src/pages/api/exam/[module]/evaluate.ts b/src/pages/api/evaluate/writing.ts
similarity index 60%
rename from src/pages/api/exam/[module]/evaluate.ts
rename to src/pages/api/evaluate/writing.ts
index 3527fe67..bc5812c0 100644
--- a/src/pages/api/exam/[module]/evaluate.ts
+++ b/src/pages/api/evaluate/writing.ts
@@ -18,19 +18,11 @@ async function handler(req: NextApiRequest, res: NextApiResponse) {
return;
}
- const {module} = req.query as {module: string};
+ const backendRequest = await axios.post(`${process.env.BACKEND_URL}/writing_task2`, req.body as Body, {
+ headers: {
+ Authorization: `Bearer ${process.env.BACKEND_JWT}`,
+ },
+ });
- if (module === "writing") {
- const backendRequest = await axios.post(`${process.env.BACKEND_URL}/writing_task2`, req.body as Body, {
- headers: {
- Authorization: `Bearer ${process.env.BACKEND_JWT}`,
- },
- });
-
- res.status(backendRequest.status).json(backendRequest.data);
- return;
- }
-
- res.status(404).json({ok: false});
- return;
+ res.status(backendRequest.status).json(backendRequest.data);
}
diff --git a/src/pages/exam.tsx b/src/pages/exam.tsx
index 52822cca..f702939e 100644
--- a/src/pages/exam.tsx
+++ b/src/pages/exam.tsx
@@ -123,7 +123,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/exam/writing/evaluate", {
+ 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/pages/exercises.tsx b/src/pages/exercises.tsx
index 8bb87b52..d85b69b8 100644
--- a/src/pages/exercises.tsx
+++ b/src/pages/exercises.tsx
@@ -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/exam/writing/evaluate", {
+ 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/yarn.lock b/yarn.lock
index 2ff5c405..7988ba0e 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -797,6 +797,13 @@
"@types/qs" "*"
"@types/serve-static" "*"
+"@types/formidable@^3.4.0":
+ version "3.4.0"
+ resolved "https://registry.yarnpkg.com/@types/formidable/-/formidable-3.4.0.tgz#5a671de8f88f3f313b31041f4746141e4431f82b"
+ integrity sha512-JXP+LsspYYBIJJxZ9VJsswb5U1hkUUhLmtAb6EB1SWcDDbJQlWRhGoZYasMSnk2NsqtUEHd3uuaiImmSys+8AQ==
+ dependencies:
+ "@types/node" "*"
+
"@types/http-assert@*":
version "1.5.3"
resolved "https://registry.yarnpkg.com/@types/http-assert/-/http-assert-1.5.3.tgz#ef8e3d1a8d46c387f04ab0f2e8ab8cb0c5078661"
@@ -1126,6 +1133,11 @@ array.prototype.tosorted@^1.1.1:
es-shim-unscopables "^1.0.0"
get-intrinsic "^1.1.3"
+asap@^2.0.0:
+ version "2.0.6"
+ resolved "https://registry.yarnpkg.com/asap/-/asap-2.0.6.tgz#e50347611d7e690943208bbdafebcbc2fb866d46"
+ integrity sha512-BSHWgDSAiKs50o2Re8ppvp3seVHXSRM44cdSsT9FfNEUUZLOGWVCsiWaRPWM1Znn+mqZ1OfVZ3z3DWEzSp7hRA==
+
asn1js@^3.0.1, asn1js@^3.0.5:
version "3.0.5"
resolved "https://registry.yarnpkg.com/asn1js/-/asn1js-3.0.5.tgz#5ea36820443dbefb51cc7f88a2ebb5b462114f38"
@@ -1477,6 +1489,14 @@ dequal@^2.0.3:
resolved "https://registry.yarnpkg.com/dequal/-/dequal-2.0.3.tgz#2644214f1997d39ed0ee0ece72335490a7ac67be"
integrity sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==
+dezalgo@^1.0.4:
+ version "1.0.4"
+ resolved "https://registry.yarnpkg.com/dezalgo/-/dezalgo-1.0.4.tgz#751235260469084c132157dfa857f386d4c33d81"
+ integrity sha512-rXSP0bf+5n0Qonsb+SVVfNfIsimO4HEtmnIpPHY8Q1UCzKlQrDMfdobr8nJOOsRgWCyMRqeSBQzmWUMq7zvVig==
+ dependencies:
+ asap "^2.0.0"
+ wrappy "1"
+
didyoumean@^1.2.2:
version "1.2.2"
resolved "https://registry.yarnpkg.com/didyoumean/-/didyoumean-1.2.2.tgz#989346ffe9e839b4555ecf5666edea0d3e8ad037"
@@ -1994,6 +2014,27 @@ form-data@^4.0.0:
combined-stream "^1.0.8"
mime-types "^2.1.12"
+formidable-serverless@^1.1.1:
+ version "1.1.1"
+ resolved "https://registry.yarnpkg.com/formidable-serverless/-/formidable-serverless-1.1.1.tgz#2b668d6e92d5222794a48ed485bcf7cd47efebfb"
+ integrity sha512-IUVI2m7d46YnWRZ9RG+wvmLoX0tTrk9P6oFc53QVjM9k0XhqzeyjwDi05PLvpGNKbyLvB3gfsAc+Lk6efM3FGQ==
+ dependencies:
+ formidable "^1.2.2"
+
+formidable@^1.2.2:
+ version "1.2.6"
+ resolved "https://registry.yarnpkg.com/formidable/-/formidable-1.2.6.tgz#d2a51d60162bbc9b4a055d8457a7c75315d1a168"
+ integrity sha512-KcpbcpuLNOwrEjnbpMC0gS+X8ciDoZE1kkqzat4a8vrprf+s9pKNQ/QIwWfbfs4ltgmFl3MD177SNTkve3BwGQ==
+
+formidable@^3.5.0:
+ version "3.5.0"
+ resolved "https://registry.yarnpkg.com/formidable/-/formidable-3.5.0.tgz#3605a9325130d05c550d57be8e81d1757baa12d6"
+ integrity sha512-WwsMWvPmY+Kv37C3+KP3A+2Ym1aZoac4nz4ZEe5z0UPBoCg0O/wHay3eeYkZr4KJIbCzpSUeno+STMhde+KCfw==
+ dependencies:
+ dezalgo "^1.0.4"
+ hexoid "^1.0.0"
+ once "^1.4.0"
+
fraction.js@^4.2.0:
version "4.2.0"
resolved "https://registry.yarnpkg.com/fraction.js/-/fraction.js-4.2.0.tgz#448e5109a313a3527f5a3ab2119ec4cf0e0e2950"
@@ -2218,6 +2259,11 @@ has@^1.0.3:
dependencies:
function-bind "^1.1.1"
+hexoid@^1.0.0:
+ version "1.0.0"
+ resolved "https://registry.yarnpkg.com/hexoid/-/hexoid-1.0.0.tgz#ad10c6573fb907de23d9ec63a711267d9dc9bc18"
+ integrity sha512-QFLV0taWQOZtvIRIAdBChesmogZrtuXvVWsFHZTk2SU+anspqZ2vMnoLg7IE1+Uk16N19APic1BuF8bC8c2m5g==
+
http-parser-js@>=0.5.1:
version "0.5.8"
resolved "https://registry.yarnpkg.com/http-parser-js/-/http-parser-js-0.5.8.tgz#af23090d9ac4e24573de6f6aecc9d84a48bf20e3"
@@ -2823,7 +2869,7 @@ object.values@^1.1.6:
define-properties "^1.1.4"
es-abstract "^1.20.4"
-once@^1.3.0:
+once@^1.3.0, once@^1.4.0:
version "1.4.0"
resolved "https://registry.yarnpkg.com/once/-/once-1.4.0.tgz#583b1aa775961d4b113ac17d9c50baef9dd76bd1"
integrity sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==