Finalized the Speaking module exercise
This commit is contained in:
@@ -38,6 +38,7 @@ export default function Speaking({id, title, text, type, prompts, onNext, onBack
|
|||||||
|
|
||||||
const formData = new FormData();
|
const formData = new FormData();
|
||||||
formData.append("audio", audioFile, "audio.wav");
|
formData.append("audio", audioFile, "audio.wav");
|
||||||
|
formData.append("question", `${text.replaceAll("\n", "")} You should talk about: ${prompts.join(", ")}`);
|
||||||
|
|
||||||
const config = {
|
const config = {
|
||||||
headers: {
|
headers: {
|
||||||
@@ -51,7 +52,7 @@ export default function Speaking({id, title, text, type, prompts, onNext, onBack
|
|||||||
};
|
};
|
||||||
|
|
||||||
if (mediaBlob) uploadFile();
|
if (mediaBlob) uploadFile();
|
||||||
}, [mediaBlob]);
|
}, [mediaBlob, text, prompts]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex flex-col h-full w-full gap-9">
|
<div className="flex flex-col h-full w-full gap-9">
|
||||||
@@ -200,14 +201,28 @@ export default function Speaking({id, title, text, type, prompts, onNext, onBack
|
|||||||
<Button
|
<Button
|
||||||
color="green"
|
color="green"
|
||||||
variant="outline"
|
variant="outline"
|
||||||
onClick={() => onBack({exercise: id, solutions: [], score: {correct: 1, total: 1, missing: 0}, type})}
|
onClick={() =>
|
||||||
|
onBack({
|
||||||
|
exercise: id,
|
||||||
|
solutions: mediaBlob ? [{id, solution: mediaBlob}] : [],
|
||||||
|
score: {correct: 1, total: 1, missing: 0},
|
||||||
|
type,
|
||||||
|
})
|
||||||
|
}
|
||||||
className="max-w-[200px] self-end w-full">
|
className="max-w-[200px] self-end w-full">
|
||||||
Back
|
Back
|
||||||
</Button>
|
</Button>
|
||||||
<Button
|
<Button
|
||||||
color="green"
|
color="green"
|
||||||
disabled={!mediaBlob}
|
disabled={!mediaBlob}
|
||||||
onClick={() => onNext({exercise: id, solutions: [], score: {correct: 1, total: 1, missing: 0}, type})}
|
onClick={() =>
|
||||||
|
onNext({
|
||||||
|
exercise: id,
|
||||||
|
solutions: mediaBlob ? [{id, solution: mediaBlob}] : [],
|
||||||
|
score: {correct: 1, total: 1, missing: 0},
|
||||||
|
type,
|
||||||
|
})
|
||||||
|
}
|
||||||
className="max-w-[200px] self-end w-full">
|
className="max-w-[200px] self-end w-full">
|
||||||
Next
|
Next
|
||||||
</Button>
|
</Button>
|
||||||
|
|||||||
@@ -100,7 +100,7 @@ export default function Writing({id, prompt, info, type, wordCounter, attachment
|
|||||||
<Button
|
<Button
|
||||||
color="green"
|
color="green"
|
||||||
variant="outline"
|
variant="outline"
|
||||||
onClick={() => onBack({exercise: id, solutions: [inputText], score: {correct: 1, total: 1, missing: 0}, type})}
|
onClick={() => onBack({exercise: id, solutions: [{id, solution: inputText}], score: {correct: 1, total: 1, missing: 0}, type})}
|
||||||
className="max-w-[200px] self-end w-full">
|
className="max-w-[200px] self-end w-full">
|
||||||
Back
|
Back
|
||||||
</Button>
|
</Button>
|
||||||
|
|||||||
@@ -69,12 +69,11 @@ export type Exercise =
|
|||||||
| WritingExercise
|
| WritingExercise
|
||||||
| SpeakingExercise;
|
| SpeakingExercise;
|
||||||
|
|
||||||
export interface WritingEvaluation {
|
export interface Evaluation {
|
||||||
comment: string;
|
comment: string;
|
||||||
overall: number;
|
overall: number;
|
||||||
task_response: {[key: string]: number};
|
task_response: {[key: string]: number};
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface WritingExercise {
|
export interface WritingExercise {
|
||||||
id: string;
|
id: string;
|
||||||
type: "writing";
|
type: "writing";
|
||||||
@@ -85,11 +84,10 @@ export interface WritingExercise {
|
|||||||
url: string;
|
url: string;
|
||||||
description: string;
|
description: string;
|
||||||
}; //* The url for an image to work as an attachment to show the user
|
}; //* The url for an image to work as an attachment to show the user
|
||||||
evaluation?: WritingEvaluation;
|
|
||||||
userSolutions: {
|
userSolutions: {
|
||||||
id: string;
|
id: string;
|
||||||
solution: string;
|
solution: string;
|
||||||
evaluation?: WritingEvaluation;
|
evaluation?: Evaluation;
|
||||||
}[];
|
}[];
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -102,6 +100,7 @@ export interface SpeakingExercise {
|
|||||||
userSolutions: {
|
userSolutions: {
|
||||||
id: string;
|
id: string;
|
||||||
solution: string;
|
solution: string;
|
||||||
|
evaluation?: Evaluation;
|
||||||
}[];
|
}[];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
// Next.js API route support: https://nextjs.org/docs/api-routes/introduction
|
// Next.js API route support: https://nextjs.org/docs/api-routes/introduction
|
||||||
import type {NextApiRequest, NextApiResponse} from "next";
|
import type {NextApiRequest, NextApiResponse} from "next";
|
||||||
import {getFirestore, doc, getDoc} from "firebase/firestore";
|
|
||||||
import {withIronSessionApiRoute} from "iron-session/next";
|
import {withIronSessionApiRoute} from "iron-session/next";
|
||||||
import {sessionOptions} from "@/lib/session";
|
import {sessionOptions} from "@/lib/session";
|
||||||
import axios from "axios";
|
import axios from "axios";
|
||||||
@@ -8,6 +7,7 @@ import formidable from "formidable";
|
|||||||
import PersistentFile from "formidable/PersistentFile";
|
import PersistentFile from "formidable/PersistentFile";
|
||||||
import {getStorage, ref, uploadBytes} from "firebase/storage";
|
import {getStorage, ref, uploadBytes} from "firebase/storage";
|
||||||
import fs from "fs";
|
import fs from "fs";
|
||||||
|
import {app} from "@/firebase";
|
||||||
|
|
||||||
export default withIronSessionApiRoute(handler, sessionOptions);
|
export default withIronSessionApiRoute(handler, sessionOptions);
|
||||||
|
|
||||||
@@ -17,7 +17,7 @@ async function handler(req: NextApiRequest, res: NextApiResponse) {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const storage = getStorage();
|
const storage = getStorage(app);
|
||||||
|
|
||||||
const form = formidable({keepExtensions: true, uploadDir: "./"});
|
const form = formidable({keepExtensions: true, uploadDir: "./"});
|
||||||
form.parse(req, (err, fields, files) => {
|
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;
|
const binary = fs.readFileSync((audioFile as any).filepath).buffer;
|
||||||
uploadBytes(audioFileRef, binary).then(async (snapshot) => {
|
uploadBytes(audioFileRef, binary).then(async (snapshot) => {
|
||||||
// const backendRequest = await axios.post(`${process.env.BACKEND_URL}/speaking_task`, req.body as Body, {
|
const backendRequest = await axios.post(
|
||||||
// headers: {
|
`${process.env.BACKEND_URL}/speaking_task_1`,
|
||||||
// Authorization: `Bearer ${process.env.BACKEND_JWT}`,
|
{question: (fields.question as string[]).join(""), answer: snapshot.metadata.fullPath},
|
||||||
// },
|
{
|
||||||
// });
|
headers: {
|
||||||
|
Authorization: `Bearer ${process.env.BACKEND_JWT}`,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
fs.rmSync((audioFile as any).filepath);
|
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 = {
|
export const config = {
|
||||||
|
|||||||
22
src/pages/api/speaking.ts
Normal file
22
src/pages/api/speaking.ts
Normal file
@@ -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}));
|
||||||
|
}
|
||||||
@@ -5,7 +5,17 @@ import {Module} from "@/interfaces";
|
|||||||
|
|
||||||
import Selection from "@/exams/Selection";
|
import Selection from "@/exams/Selection";
|
||||||
import Reading from "@/exams/Reading";
|
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 Listening from "@/exams/Listening";
|
||||||
import Writing from "@/exams/Writing";
|
import Writing from "@/exams/Writing";
|
||||||
import {ToastContainer, toast} from "react-toastify";
|
import {ToastContainer, toast} from "react-toastify";
|
||||||
@@ -19,7 +29,7 @@ import {v4 as uuidv4} from "uuid";
|
|||||||
import useUser from "@/hooks/useUser";
|
import useUser from "@/hooks/useUser";
|
||||||
import useExamStore from "@/stores/examStore";
|
import useExamStore from "@/stores/examStore";
|
||||||
import Layout from "@/components/High/Layout";
|
import Layout from "@/components/High/Layout";
|
||||||
import {writingReverseMarking} from "@/utils/score";
|
import {speakingReverseMarking, writingReverseMarking} from "@/utils/score";
|
||||||
|
|
||||||
export const getServerSideProps = withIronSessionSsr(({req, res}) => {
|
export const getServerSideProps = withIronSessionSsr(({req, res}) => {
|
||||||
const user = req.session.user;
|
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 evaluateWritingAnswer = async (examId: string, exerciseId: string, solution: UserSolution) => {
|
||||||
const writingExam = exams.find((x) => x.id === examId)!;
|
const writingExam = exams.find((x) => x.id === examId)!;
|
||||||
const exercise = writingExam.exercises.find((x) => x.id === exerciseId)! as WritingExercise;
|
const exercise = writingExam.exercises.find((x) => x.id === exerciseId)! as WritingExercise;
|
||||||
|
|
||||||
const response = await axios.post<WritingEvaluation>("/api/evaluate/writing", {
|
const response = await axios.post<Evaluation>("/api/evaluate/writing", {
|
||||||
question: `${exercise.prompt} ${exercise.attachment ? exercise.attachment.description : ""}`.replaceAll("\n", ""),
|
question: `${exercise.prompt} ${exercise.attachment ? exercise.attachment.description : ""}`.replaceAll("\n", ""),
|
||||||
answer: solution.solutions[0].solution.trim().replaceAll("\n", " "),
|
answer: solution.solutions[0].solution.trim().replaceAll("\n", " "),
|
||||||
});
|
});
|
||||||
@@ -155,11 +201,17 @@ export default function Page() {
|
|||||||
const onFinish = (solutions: UserSolution[]) => {
|
const onFinish = (solutions: UserSolution[]) => {
|
||||||
const solutionIds = solutions.map((x) => x.exercise);
|
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);
|
setHasBeenUploaded(true);
|
||||||
setIsEvaluationLoading(true);
|
setIsEvaluationLoading(true);
|
||||||
Promise.all(
|
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(() => {
|
).finally(() => {
|
||||||
setIsEvaluationLoading(false);
|
setIsEvaluationLoading(false);
|
||||||
setHasBeenUploaded(false);
|
setHasBeenUploaded(false);
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ import {Module} from "@/interfaces";
|
|||||||
|
|
||||||
import Selection from "@/exams/Selection";
|
import Selection from "@/exams/Selection";
|
||||||
import Reading from "@/exams/Reading";
|
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 Listening from "@/exams/Listening";
|
||||||
import Writing from "@/exams/Writing";
|
import Writing from "@/exams/Writing";
|
||||||
import {ToastContainer, toast} from "react-toastify";
|
import {ToastContainer, toast} from "react-toastify";
|
||||||
@@ -126,7 +126,7 @@ export default function Page() {
|
|||||||
const writingExam = exams.find((x) => x.id === examId)!;
|
const writingExam = exams.find((x) => x.id === examId)!;
|
||||||
const exercise = writingExam.exercises.find((x) => x.id === exerciseId)! as WritingExercise;
|
const exercise = writingExam.exercises.find((x) => x.id === exerciseId)! as WritingExercise;
|
||||||
|
|
||||||
const response = await axios.post<WritingEvaluation>("/api/evaluate/writing", {
|
const response = await axios.post<Evaluation>("/api/evaluate/writing", {
|
||||||
question: `${exercise.prompt} ${exercise.attachment ? exercise.attachment.description : ""}`.replaceAll("\n", ""),
|
question: `${exercise.prompt} ${exercise.attachment ? exercise.attachment.description : ""}`.replaceAll("\n", ""),
|
||||||
answer: solution.solutions[0].solution.trim().replaceAll("\n", " "),
|
answer: solution.solutions[0].solution.trim().replaceAll("\n", " "),
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -15,6 +15,19 @@ export const writingReverseMarking: {[key: number]: number} = {
|
|||||||
0: 0,
|
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} = {
|
const writingMarking: {[key: number]: number} = {
|
||||||
90: 9,
|
90: 9,
|
||||||
80: 8,
|
80: 8,
|
||||||
|
|||||||
Reference in New Issue
Block a user