feat(eo): restore audioBase64 mode for Gemini batch transcription
- POST /corrections/eo accepts audioBase64 + mimeType (XOR with transcript) - Gemini transcribeAudio called server-side before correction - No audio storage (client downloads locally) - /transcriptions/token kept for future Deepgram live use Typecheck: OK · Tests: all green Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
14880fe94c
commit
8f8a900449
4 changed files with 352 additions and 31 deletions
|
|
@ -73,7 +73,7 @@ describe("POST /corrections/eo — Sprint 4a", () => {
|
|||
expect(res.status).toBe(400);
|
||||
});
|
||||
|
||||
it("400 si transcript manquant", async () => {
|
||||
it("400 si ni transcript ni audioBase64 fournis", async () => {
|
||||
const app = buildApp();
|
||||
const res = await app.request("/corrections/eo", {
|
||||
method: "POST",
|
||||
|
|
@ -85,6 +85,36 @@ describe("POST /corrections/eo — Sprint 4a", () => {
|
|||
expect(body.code).toBe("VALIDATION_ERROR");
|
||||
});
|
||||
|
||||
it("400 si transcript ET audioBase64 fournis simultanément (XOR)", async () => {
|
||||
const app = buildApp();
|
||||
const res = await app.request("/corrections/eo", {
|
||||
method: "POST",
|
||||
headers: JSON_HEADERS,
|
||||
body: JSON.stringify({
|
||||
simulationId: "s1",
|
||||
tache: "EO_T1",
|
||||
transcript: "t",
|
||||
audioBase64: "AAAA",
|
||||
mimeType: "audio/webm",
|
||||
}),
|
||||
});
|
||||
expect(res.status).toBe(400);
|
||||
});
|
||||
|
||||
it("400 si audioBase64 sans mimeType", async () => {
|
||||
const app = buildApp();
|
||||
const res = await app.request("/corrections/eo", {
|
||||
method: "POST",
|
||||
headers: JSON_HEADERS,
|
||||
body: JSON.stringify({
|
||||
simulationId: "s1",
|
||||
tache: "EO_T1",
|
||||
audioBase64: "AAAA",
|
||||
}),
|
||||
});
|
||||
expect(res.status).toBe(400);
|
||||
});
|
||||
|
||||
it("400 si nclc_cible invalide", async () => {
|
||||
const app = buildApp();
|
||||
const res = await app.request("/corrections/eo", {
|
||||
|
|
@ -133,6 +163,34 @@ describe("POST /corrections/eo — Sprint 4a", () => {
|
|||
);
|
||||
});
|
||||
|
||||
it("200 mode batch audio (transmet audioBase64 + mimeType au controller)", async () => {
|
||||
correctEOMock.mockResolvedValue({
|
||||
data: { score: 14, nclc: 9, simulation_id: "s-audio", diagnostic: "d" },
|
||||
});
|
||||
const app = buildApp();
|
||||
const res = await app.request("/corrections/eo", {
|
||||
method: "POST",
|
||||
headers: JSON_HEADERS,
|
||||
body: JSON.stringify({
|
||||
simulationId: "s-audio",
|
||||
tache: "EO_T1",
|
||||
audioBase64: "AAAA",
|
||||
mimeType: "audio/webm",
|
||||
}),
|
||||
});
|
||||
expect(res.status).toBe(200);
|
||||
expect(correctEOMock).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
simulationId: "s-audio",
|
||||
tache: "EO_T1",
|
||||
nclcCible: 9,
|
||||
audioBase64: "AAAA",
|
||||
mimeType: "audio/webm",
|
||||
}),
|
||||
expect.any(Object),
|
||||
);
|
||||
});
|
||||
|
||||
it("200 avec nclc_cible=10 transmis au controller", async () => {
|
||||
correctEOMock.mockResolvedValue({
|
||||
data: { score: 16, nclc: 10, simulation_id: "s2", diagnostic: "d" },
|
||||
|
|
|
|||
|
|
@ -91,15 +91,19 @@ corrections.post("/ee", authMiddleware, async (c) => {
|
|||
return c.json(result.data, 200);
|
||||
});
|
||||
|
||||
// Sprint 4b — POST /corrections/eo reçoit uniquement le transcript final.
|
||||
// La transcription live est gérée navigateur ↔ Deepgram (cf. /transcriptions/token).
|
||||
// Aucun audio n'est stocké côté backend.
|
||||
// Sprint 4b.2 — POST /corrections/eo accepte SOIT un transcript texte
|
||||
// SOIT un audio base64 + mimeType (transcrit côté backend via Gemini).
|
||||
// Aucun audio n'est stocké côté serveur ; le client garde une copie locale.
|
||||
const MAX_AUDIO_BASE64_LEN = 14 * 1024 * 1024;
|
||||
|
||||
corrections.post("/eo", authMiddleware, async (c) => {
|
||||
let body: {
|
||||
simulationId?: unknown;
|
||||
transcript?: unknown;
|
||||
tache?: unknown;
|
||||
nclc_cible?: unknown;
|
||||
audioBase64?: unknown;
|
||||
mimeType?: unknown;
|
||||
};
|
||||
try {
|
||||
body = await c.req.json();
|
||||
|
|
@ -125,17 +129,6 @@ corrections.post("/eo", authMiddleware, async (c) => {
|
|||
);
|
||||
}
|
||||
|
||||
if (!body.transcript || typeof body.transcript !== "string") {
|
||||
return c.json(
|
||||
{
|
||||
error: true,
|
||||
code: "VALIDATION_ERROR",
|
||||
message: "transcript est requis.",
|
||||
},
|
||||
400,
|
||||
);
|
||||
}
|
||||
|
||||
if (!body.tache || !VALID_TACHES_EO.includes(body.tache as string)) {
|
||||
return c.json(
|
||||
{
|
||||
|
|
@ -147,6 +140,46 @@ corrections.post("/eo", authMiddleware, async (c) => {
|
|||
);
|
||||
}
|
||||
|
||||
// XOR : transcript OU (audioBase64 + mimeType). Pas les deux, pas aucun.
|
||||
const hasTranscript =
|
||||
typeof body.transcript === "string" && body.transcript.length > 0;
|
||||
const hasAudio =
|
||||
typeof body.audioBase64 === "string" && body.audioBase64.length > 0;
|
||||
if (hasTranscript === hasAudio) {
|
||||
return c.json(
|
||||
{
|
||||
error: true,
|
||||
code: "VALIDATION_ERROR",
|
||||
message:
|
||||
"Fournir exactement un des deux : `transcript` (texte) ou `audioBase64` + `mimeType` (audio).",
|
||||
},
|
||||
400,
|
||||
);
|
||||
}
|
||||
|
||||
if (hasAudio) {
|
||||
if (typeof body.mimeType !== "string" || body.mimeType.length === 0) {
|
||||
return c.json(
|
||||
{
|
||||
error: true,
|
||||
code: "VALIDATION_ERROR",
|
||||
message: "`mimeType` est requis quand `audioBase64` est fourni.",
|
||||
},
|
||||
400,
|
||||
);
|
||||
}
|
||||
if ((body.audioBase64 as string).length > MAX_AUDIO_BASE64_LEN) {
|
||||
return c.json(
|
||||
{
|
||||
error: true,
|
||||
code: "VALIDATION_ERROR",
|
||||
message: "Audio trop volumineux (max ~10 Mo).",
|
||||
},
|
||||
413,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// nclc_cible optionnel (défaut 9, valeurs 9 ou 10).
|
||||
let nclcCible: 9 | 10 = 9;
|
||||
if (body.nclc_cible !== undefined) {
|
||||
|
|
@ -169,13 +202,15 @@ corrections.post("/eo", authMiddleware, async (c) => {
|
|||
simulationId: body.simulationId,
|
||||
tache: body.tache as "EO_T1" | "EO_T3",
|
||||
nclcCible,
|
||||
transcript: body.transcript,
|
||||
transcript: hasTranscript ? (body.transcript as string) : undefined,
|
||||
audioBase64: hasAudio ? (body.audioBase64 as string) : undefined,
|
||||
mimeType: hasAudio ? (body.mimeType as string) : undefined,
|
||||
},
|
||||
profile,
|
||||
);
|
||||
|
||||
if ("error" in result) {
|
||||
return c.json(result, result.status as 401 | 404 | 500);
|
||||
return c.json(result, result.status as 400 | 401 | 404 | 500);
|
||||
}
|
||||
|
||||
return c.json(result.data, 200);
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue