feat(eo): align correction EO on 3.6a format + Deepgram token + T1 presentation generation
Sprint 4a:
- correctEO aligned on CorrectionRapport format (revelation, diagnostic, criteres, conseil_nclc, erreurs_codes)
- nclc_cible parameter (default 9, accepts 9|10)
- Fire-and-forget modele + exercices jobs (same pattern as EE)
- EO-specific DeepSeek prompt (oral transcript tolerance, 4 TCF criteria)
- Gemini transcribeAudio: 30s timeout + 1 retry
- POST /presentations/generate: 5-field questionnaire → DeepSeek generates oral presentation (~220-260 words, NCLC 7-8)
- Migration 006_sprint_4a_eo.sql (documentation only — no audio storage)
Sprint 4b:
- POST /transcriptions/token: Deepgram temporary API key (600s TTL)
- Removed audio storage pipeline (audioStorage.ts, XOR validation, 14MB limit)
- Backend receives transcript text only, no audio files
- TD-10/TD-11 resolved (Sprint 3.6c), TD-16/17/18 resolved (4b cleanup)
Typecheck: OK · Tests: 241/241 ✅
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
f5954e6d72
commit
7cac057062
18 changed files with 2907 additions and 911 deletions
161
src/routes/__tests__/correctionsEO.test.ts
Normal file
161
src/routes/__tests__/correctionsEO.test.ts
Normal file
|
|
@ -0,0 +1,161 @@
|
|||
import { describe, it, expect, vi, beforeEach } from "vitest";
|
||||
import { Hono } from "hono";
|
||||
|
||||
// ─── Mocks ───────────────────────────────────────────────────────────────
|
||||
|
||||
vi.mock("../../middleware/auth", () => ({
|
||||
authMiddleware: async (c: any, next: any) => {
|
||||
const authHeader = c.req.header("Authorization");
|
||||
if (!authHeader || !authHeader.startsWith("Bearer ")) {
|
||||
return c.json({ error: true, code: "AUTH_REQUIRED" }, 401);
|
||||
}
|
||||
c.set("profile", {
|
||||
id: "user-1",
|
||||
email: "u@test.com",
|
||||
plan: "standard",
|
||||
simulations_used: 3,
|
||||
});
|
||||
await next();
|
||||
},
|
||||
}));
|
||||
|
||||
const { correctEOMock } = vi.hoisted(() => ({ correctEOMock: vi.fn() }));
|
||||
vi.mock("../../controllers/correctionController", () => ({
|
||||
correctEE: vi.fn(),
|
||||
correctEO: correctEOMock,
|
||||
}));
|
||||
|
||||
import correctionsRoutes from "../corrections";
|
||||
|
||||
function buildApp() {
|
||||
const app = new Hono();
|
||||
app.route("/corrections", correctionsRoutes);
|
||||
return app;
|
||||
}
|
||||
|
||||
const AUTH = { Authorization: "Bearer x" };
|
||||
const JSON_HEADERS = { ...AUTH, "Content-Type": "application/json" };
|
||||
|
||||
describe("POST /corrections/eo — Sprint 4a", () => {
|
||||
beforeEach(() => {
|
||||
correctEOMock.mockReset();
|
||||
});
|
||||
|
||||
it("401 sans Authorization", async () => {
|
||||
const app = buildApp();
|
||||
const res = await app.request("/corrections/eo", { method: "POST" });
|
||||
expect(res.status).toBe(401);
|
||||
});
|
||||
|
||||
it("400 si simulationId manquant", async () => {
|
||||
const app = buildApp();
|
||||
const res = await app.request("/corrections/eo", {
|
||||
method: "POST",
|
||||
headers: JSON_HEADERS,
|
||||
body: JSON.stringify({ tache: "EO_T1", transcript: "t" }),
|
||||
});
|
||||
expect(res.status).toBe(400);
|
||||
const body = await res.json();
|
||||
expect(body.code).toBe("VALIDATION_ERROR");
|
||||
});
|
||||
|
||||
it("400 si tache invalide (EO_T2 par exemple)", async () => {
|
||||
const app = buildApp();
|
||||
const res = await app.request("/corrections/eo", {
|
||||
method: "POST",
|
||||
headers: JSON_HEADERS,
|
||||
body: JSON.stringify({
|
||||
simulationId: "s1",
|
||||
tache: "EO_T2",
|
||||
transcript: "t",
|
||||
}),
|
||||
});
|
||||
expect(res.status).toBe(400);
|
||||
});
|
||||
|
||||
it("400 si transcript manquant", async () => {
|
||||
const app = buildApp();
|
||||
const res = await app.request("/corrections/eo", {
|
||||
method: "POST",
|
||||
headers: JSON_HEADERS,
|
||||
body: JSON.stringify({ simulationId: "s1", tache: "EO_T1" }),
|
||||
});
|
||||
expect(res.status).toBe(400);
|
||||
const body = await res.json();
|
||||
expect(body.code).toBe("VALIDATION_ERROR");
|
||||
});
|
||||
|
||||
it("400 si nclc_cible invalide", 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",
|
||||
nclc_cible: 8,
|
||||
}),
|
||||
});
|
||||
expect(res.status).toBe(400);
|
||||
});
|
||||
|
||||
it("200 quand le controller renvoie un rapport (mode transcript)", async () => {
|
||||
correctEOMock.mockResolvedValue({
|
||||
data: {
|
||||
score: 14,
|
||||
nclc: 9,
|
||||
simulation_id: "s1",
|
||||
diagnostic: "d",
|
||||
},
|
||||
});
|
||||
const app = buildApp();
|
||||
const res = await app.request("/corrections/eo", {
|
||||
method: "POST",
|
||||
headers: JSON_HEADERS,
|
||||
body: JSON.stringify({
|
||||
simulationId: "s1",
|
||||
tache: "EO_T1",
|
||||
transcript: "Bonjour je m appelle Pierre",
|
||||
}),
|
||||
});
|
||||
expect(res.status).toBe(200);
|
||||
const body = await res.json();
|
||||
expect(body.score).toBe(14);
|
||||
expect(correctEOMock).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
simulationId: "s1",
|
||||
tache: "EO_T1",
|
||||
nclcCible: 9,
|
||||
transcript: "Bonjour je m appelle Pierre",
|
||||
}),
|
||||
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" },
|
||||
});
|
||||
const app = buildApp();
|
||||
const res = await app.request("/corrections/eo", {
|
||||
method: "POST",
|
||||
headers: JSON_HEADERS,
|
||||
body: JSON.stringify({
|
||||
simulationId: "s2",
|
||||
tache: "EO_T1",
|
||||
transcript: "Bonjour",
|
||||
nclc_cible: 10,
|
||||
}),
|
||||
});
|
||||
expect(res.status).toBe(200);
|
||||
expect(correctEOMock).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
simulationId: "s2",
|
||||
nclcCible: 10,
|
||||
transcript: "Bonjour",
|
||||
}),
|
||||
expect.any(Object),
|
||||
);
|
||||
});
|
||||
});
|
||||
132
src/routes/__tests__/presentationsGenerate.test.ts
Normal file
132
src/routes/__tests__/presentationsGenerate.test.ts
Normal file
|
|
@ -0,0 +1,132 @@
|
|||
import { describe, it, expect, vi, beforeEach } from "vitest";
|
||||
import { Hono } from "hono";
|
||||
|
||||
vi.mock("../../middleware/auth", () => ({
|
||||
authMiddleware: async (c: any, next: any) => {
|
||||
const authHeader = c.req.header("Authorization");
|
||||
if (!authHeader || !authHeader.startsWith("Bearer ")) {
|
||||
return c.json({ error: true, code: "AUTH_REQUIRED" }, 401);
|
||||
}
|
||||
c.set("profile", {
|
||||
id: "user-1",
|
||||
email: "u@test.com",
|
||||
plan: "standard",
|
||||
simulations_used: 0,
|
||||
});
|
||||
await next();
|
||||
},
|
||||
}));
|
||||
|
||||
const { generateMock } = vi.hoisted(() => ({ generateMock: vi.fn() }));
|
||||
vi.mock("../../controllers/presentationController", () => ({
|
||||
generate: generateMock,
|
||||
}));
|
||||
|
||||
import presentationsRoutes from "../presentations";
|
||||
|
||||
function buildApp() {
|
||||
const app = new Hono();
|
||||
app.route("/presentations", presentationsRoutes);
|
||||
return app;
|
||||
}
|
||||
|
||||
const JSON_HEADERS = {
|
||||
Authorization: "Bearer x",
|
||||
"Content-Type": "application/json",
|
||||
};
|
||||
|
||||
describe("POST /presentations/generate", () => {
|
||||
beforeEach(() => {
|
||||
generateMock.mockReset();
|
||||
});
|
||||
|
||||
it("401 sans Authorization", async () => {
|
||||
const app = buildApp();
|
||||
const res = await app.request("/presentations/generate", {
|
||||
method: "POST",
|
||||
});
|
||||
expect(res.status).toBe(401);
|
||||
});
|
||||
|
||||
it("400 si body JSON invalide", async () => {
|
||||
const app = buildApp();
|
||||
const res = await app.request("/presentations/generate", {
|
||||
method: "POST",
|
||||
headers: JSON_HEADERS,
|
||||
body: "not-json",
|
||||
});
|
||||
expect(res.status).toBe(400);
|
||||
});
|
||||
|
||||
it("propage l'erreur de validation du controller", async () => {
|
||||
generateMock.mockResolvedValue({
|
||||
error: true,
|
||||
code: "VALIDATION_ERROR",
|
||||
message: "field missing",
|
||||
status: 400,
|
||||
});
|
||||
const app = buildApp();
|
||||
const res = await app.request("/presentations/generate", {
|
||||
method: "POST",
|
||||
headers: JSON_HEADERS,
|
||||
body: JSON.stringify({ reponses: {} }),
|
||||
});
|
||||
expect(res.status).toBe(400);
|
||||
const body = await res.json();
|
||||
expect(body.code).toBe("VALIDATION_ERROR");
|
||||
});
|
||||
|
||||
it("200 avec { presentation } quand le controller réussit", async () => {
|
||||
generateMock.mockResolvedValue({
|
||||
data: { presentation: "Bonjour, je m'appelle Pierre…" },
|
||||
});
|
||||
const app = buildApp();
|
||||
const res = await app.request("/presentations/generate", {
|
||||
method: "POST",
|
||||
headers: JSON_HEADERS,
|
||||
body: JSON.stringify({
|
||||
reponses: {
|
||||
prenom_age_ville: "Pierre",
|
||||
formation_metier: "Ingénieur",
|
||||
situation_familiale: "Marié",
|
||||
loisirs: "Lecture",
|
||||
motivation_canada: "Travail",
|
||||
},
|
||||
}),
|
||||
});
|
||||
expect(res.status).toBe(200);
|
||||
const body = await res.json();
|
||||
expect(body.presentation).toContain("Pierre");
|
||||
expect(generateMock).toHaveBeenCalledWith({
|
||||
prenom_age_ville: "Pierre",
|
||||
formation_metier: "Ingénieur",
|
||||
situation_familiale: "Marié",
|
||||
loisirs: "Lecture",
|
||||
motivation_canada: "Travail",
|
||||
});
|
||||
});
|
||||
|
||||
it("500 si DeepSeek down (controller renvoie INTERNAL_ERROR)", async () => {
|
||||
generateMock.mockResolvedValue({
|
||||
error: true,
|
||||
code: "INTERNAL_ERROR",
|
||||
message: "fail",
|
||||
status: 500,
|
||||
});
|
||||
const app = buildApp();
|
||||
const res = await app.request("/presentations/generate", {
|
||||
method: "POST",
|
||||
headers: JSON_HEADERS,
|
||||
body: JSON.stringify({
|
||||
reponses: {
|
||||
prenom_age_ville: "a",
|
||||
formation_metier: "b",
|
||||
situation_familiale: "c",
|
||||
loisirs: "d",
|
||||
motivation_canada: "e",
|
||||
},
|
||||
}),
|
||||
});
|
||||
expect(res.status).toBe(500);
|
||||
});
|
||||
});
|
||||
142
src/routes/__tests__/transcriptionsToken.test.ts
Normal file
142
src/routes/__tests__/transcriptionsToken.test.ts
Normal file
|
|
@ -0,0 +1,142 @@
|
|||
import { describe, it, expect, vi, beforeEach } from "vitest";
|
||||
import { Hono } from "hono";
|
||||
|
||||
vi.mock("../../middleware/auth", () => ({
|
||||
authMiddleware: async (c: any, next: any) => {
|
||||
const authHeader = c.req.header("Authorization");
|
||||
if (!authHeader || !authHeader.startsWith("Bearer ")) {
|
||||
return c.json({ error: true, code: "AUTH_REQUIRED" }, 401);
|
||||
}
|
||||
c.set("profile", {
|
||||
id: "user-1",
|
||||
email: "u@test.com",
|
||||
plan: "standard",
|
||||
simulations_used: 0,
|
||||
});
|
||||
await next();
|
||||
},
|
||||
}));
|
||||
|
||||
import transcriptionsRoutes from "../transcriptions";
|
||||
|
||||
function buildApp() {
|
||||
const app = new Hono();
|
||||
app.route("/transcriptions", transcriptionsRoutes);
|
||||
return app;
|
||||
}
|
||||
|
||||
const JSON_HEADERS = {
|
||||
Authorization: "Bearer x",
|
||||
"Content-Type": "application/json",
|
||||
};
|
||||
|
||||
describe("POST /transcriptions/token — Sprint 4b", () => {
|
||||
beforeEach(() => {
|
||||
vi.restoreAllMocks();
|
||||
});
|
||||
|
||||
it("401 sans Authorization", async () => {
|
||||
const app = buildApp();
|
||||
const res = await app.request("/transcriptions/token", { method: "POST" });
|
||||
expect(res.status).toBe(401);
|
||||
});
|
||||
|
||||
it("200 + { token, expires_in } quand Deepgram répond OK", async () => {
|
||||
vi.stubGlobal(
|
||||
"fetch",
|
||||
vi.fn().mockResolvedValue({
|
||||
ok: true,
|
||||
json: async () => ({
|
||||
access_token: "dg-temp-abc123",
|
||||
expires_in: 600,
|
||||
}),
|
||||
}),
|
||||
);
|
||||
|
||||
const app = buildApp();
|
||||
const res = await app.request("/transcriptions/token", {
|
||||
method: "POST",
|
||||
headers: JSON_HEADERS,
|
||||
});
|
||||
|
||||
expect(res.status).toBe(200);
|
||||
const body = await res.json();
|
||||
expect(body.token).toBe("dg-temp-abc123");
|
||||
expect(body.expires_in).toBe(600);
|
||||
});
|
||||
|
||||
it("appelle Deepgram avec ttl_seconds=600 et Authorization Token <key>", async () => {
|
||||
const fetchMock = vi.fn().mockResolvedValue({
|
||||
ok: true,
|
||||
json: async () => ({ access_token: "tok", expires_in: 600 }),
|
||||
});
|
||||
vi.stubGlobal("fetch", fetchMock);
|
||||
|
||||
const app = buildApp();
|
||||
await app.request("/transcriptions/token", {
|
||||
method: "POST",
|
||||
headers: JSON_HEADERS,
|
||||
});
|
||||
|
||||
expect(fetchMock).toHaveBeenCalledTimes(1);
|
||||
const [url, init] = fetchMock.mock.calls[0];
|
||||
expect(String(url)).toContain("/v1/auth/grant");
|
||||
const headers = (init?.headers ?? {}) as Record<string, string>;
|
||||
expect(headers["Authorization"]).toMatch(/^Token /);
|
||||
expect(JSON.parse(init.body as string)).toEqual({ ttl_seconds: 600 });
|
||||
});
|
||||
|
||||
it("500 INTERNAL_ERROR si Deepgram non-OK", async () => {
|
||||
vi.stubGlobal(
|
||||
"fetch",
|
||||
vi.fn().mockResolvedValue({
|
||||
ok: false,
|
||||
status: 401,
|
||||
statusText: "Unauthorized",
|
||||
}),
|
||||
);
|
||||
|
||||
const app = buildApp();
|
||||
const res = await app.request("/transcriptions/token", {
|
||||
method: "POST",
|
||||
headers: JSON_HEADERS,
|
||||
});
|
||||
|
||||
expect(res.status).toBe(500);
|
||||
const body = await res.json();
|
||||
expect(body.code).toBe("INTERNAL_ERROR");
|
||||
});
|
||||
|
||||
it("500 INTERNAL_ERROR si fetch throw (timeout réseau)", async () => {
|
||||
vi.stubGlobal(
|
||||
"fetch",
|
||||
vi.fn().mockRejectedValue(new Error("network down")),
|
||||
);
|
||||
|
||||
const app = buildApp();
|
||||
const res = await app.request("/transcriptions/token", {
|
||||
method: "POST",
|
||||
headers: JSON_HEADERS,
|
||||
});
|
||||
|
||||
expect(res.status).toBe(500);
|
||||
});
|
||||
|
||||
it("500 INTERNAL_ERROR si access_token absent dans la réponse Deepgram", async () => {
|
||||
vi.stubGlobal(
|
||||
"fetch",
|
||||
vi.fn().mockResolvedValue({
|
||||
ok: true,
|
||||
json: async () => ({ expires_in: 600 }),
|
||||
}),
|
||||
);
|
||||
|
||||
const app = buildApp();
|
||||
const res = await app.request("/transcriptions/token", {
|
||||
method: "POST",
|
||||
headers: JSON_HEADERS,
|
||||
});
|
||||
|
||||
expect(res.status).toBe(500);
|
||||
});
|
||||
});
|
||||
|
|
@ -1,137 +1,184 @@
|
|||
import { Hono } from 'hono'
|
||||
import { authMiddleware } from '../middleware/auth.js'
|
||||
import type { AppVariables } from '../middleware/auth.js'
|
||||
import * as correctionController from '../controllers/correctionController.js'
|
||||
import { Hono } from "hono";
|
||||
import { authMiddleware } from "../middleware/auth.js";
|
||||
import type { AppVariables } from "../middleware/auth.js";
|
||||
import * as correctionController from "../controllers/correctionController.js";
|
||||
|
||||
const VALID_TACHES_EE = ['EE_T1', 'EE_T2', 'EE_T3']
|
||||
const VALID_TACHES_EO = ['EO_T1', 'EO_T3']
|
||||
const VALID_TACHES_EE = ["EE_T1", "EE_T2", "EE_T3"];
|
||||
const VALID_TACHES_EO = ["EO_T1", "EO_T3"];
|
||||
|
||||
const corrections = new Hono<{ Variables: AppVariables }>()
|
||||
const corrections = new Hono<{ Variables: AppVariables }>();
|
||||
|
||||
corrections.post('/ee', authMiddleware, async (c) => {
|
||||
corrections.post("/ee", authMiddleware, async (c) => {
|
||||
let body: {
|
||||
simulationId?: unknown
|
||||
contenu?: unknown
|
||||
tache?: unknown
|
||||
nclc_cible?: unknown
|
||||
}
|
||||
simulationId?: unknown;
|
||||
contenu?: unknown;
|
||||
tache?: unknown;
|
||||
nclc_cible?: unknown;
|
||||
};
|
||||
try {
|
||||
body = await c.req.json()
|
||||
body = await c.req.json();
|
||||
} catch {
|
||||
return c.json(
|
||||
{ error: true, code: 'VALIDATION_ERROR', message: 'Corps de la requête invalide.' },
|
||||
400
|
||||
)
|
||||
{
|
||||
error: true,
|
||||
code: "VALIDATION_ERROR",
|
||||
message: "Corps de la requête invalide.",
|
||||
},
|
||||
400,
|
||||
);
|
||||
}
|
||||
|
||||
if (!body.simulationId || typeof body.simulationId !== 'string') {
|
||||
if (!body.simulationId || typeof body.simulationId !== "string") {
|
||||
return c.json(
|
||||
{ error: true, code: 'VALIDATION_ERROR', message: 'simulationId est requis.' },
|
||||
400
|
||||
)
|
||||
{
|
||||
error: true,
|
||||
code: "VALIDATION_ERROR",
|
||||
message: "simulationId est requis.",
|
||||
},
|
||||
400,
|
||||
);
|
||||
}
|
||||
|
||||
if (!body.contenu || typeof body.contenu !== 'string') {
|
||||
if (!body.contenu || typeof body.contenu !== "string") {
|
||||
return c.json(
|
||||
{ error: true, code: 'VALIDATION_ERROR', message: 'contenu est requis.' },
|
||||
400
|
||||
)
|
||||
{ error: true, code: "VALIDATION_ERROR", message: "contenu est requis." },
|
||||
400,
|
||||
);
|
||||
}
|
||||
|
||||
if (!body.tache || !VALID_TACHES_EE.includes(body.tache as string)) {
|
||||
return c.json(
|
||||
{
|
||||
error: true,
|
||||
code: 'VALIDATION_ERROR',
|
||||
message: `Tâche invalide. Valeurs acceptées : ${VALID_TACHES_EE.join(', ')}`,
|
||||
code: "VALIDATION_ERROR",
|
||||
message: `Tâche invalide. Valeurs acceptées : ${VALID_TACHES_EE.join(", ")}`,
|
||||
},
|
||||
400
|
||||
)
|
||||
400,
|
||||
);
|
||||
}
|
||||
|
||||
// Sprint 3.6a — nclc_cible optionnel (défaut 9). Seules les valeurs 9 et 10 sont acceptées.
|
||||
let nclcCible: 9 | 10 = 9
|
||||
let nclcCible: 9 | 10 = 9;
|
||||
if (body.nclc_cible !== undefined) {
|
||||
if (body.nclc_cible !== 9 && body.nclc_cible !== 10) {
|
||||
return c.json(
|
||||
{
|
||||
error: true,
|
||||
code: 'VALIDATION_ERROR',
|
||||
message: 'nclc_cible doit être 9 ou 10.',
|
||||
code: "VALIDATION_ERROR",
|
||||
message: "nclc_cible doit être 9 ou 10.",
|
||||
},
|
||||
400
|
||||
)
|
||||
400,
|
||||
);
|
||||
}
|
||||
nclcCible = body.nclc_cible
|
||||
nclcCible = body.nclc_cible;
|
||||
}
|
||||
|
||||
const profile = c.get('profile')
|
||||
const profile = c.get("profile");
|
||||
const result = await correctionController.correctEE(
|
||||
{
|
||||
simulationId: body.simulationId,
|
||||
contenu: body.contenu,
|
||||
tache: body.tache as 'EE_T1' | 'EE_T2' | 'EE_T3',
|
||||
tache: body.tache as "EE_T1" | "EE_T2" | "EE_T3",
|
||||
nclcCible,
|
||||
},
|
||||
profile,
|
||||
)
|
||||
);
|
||||
|
||||
if ('error' in result) {
|
||||
return c.json(result, result.status as 401 | 404 | 500)
|
||||
if ("error" in result) {
|
||||
return c.json(result, result.status as 401 | 404 | 500);
|
||||
}
|
||||
|
||||
return c.json(result.data, 200)
|
||||
})
|
||||
return c.json(result.data, 200);
|
||||
});
|
||||
|
||||
corrections.post('/eo', authMiddleware, async (c) => {
|
||||
let body: { simulationId?: unknown; transcript?: unknown; tache?: unknown }
|
||||
// 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.
|
||||
corrections.post("/eo", authMiddleware, async (c) => {
|
||||
let body: {
|
||||
simulationId?: unknown;
|
||||
transcript?: unknown;
|
||||
tache?: unknown;
|
||||
nclc_cible?: unknown;
|
||||
};
|
||||
try {
|
||||
body = await c.req.json()
|
||||
body = await c.req.json();
|
||||
} catch {
|
||||
return c.json(
|
||||
{ error: true, code: 'VALIDATION_ERROR', message: 'Corps de la requête invalide.' },
|
||||
400
|
||||
)
|
||||
{
|
||||
error: true,
|
||||
code: "VALIDATION_ERROR",
|
||||
message: "Corps de la requête invalide.",
|
||||
},
|
||||
400,
|
||||
);
|
||||
}
|
||||
|
||||
if (!body.simulationId || typeof body.simulationId !== 'string') {
|
||||
if (!body.simulationId || typeof body.simulationId !== "string") {
|
||||
return c.json(
|
||||
{ error: true, code: 'VALIDATION_ERROR', message: 'simulationId est requis.' },
|
||||
400
|
||||
)
|
||||
{
|
||||
error: true,
|
||||
code: "VALIDATION_ERROR",
|
||||
message: "simulationId est requis.",
|
||||
},
|
||||
400,
|
||||
);
|
||||
}
|
||||
|
||||
if (!body.transcript || typeof body.transcript !== 'string') {
|
||||
if (!body.transcript || typeof body.transcript !== "string") {
|
||||
return c.json(
|
||||
{ error: true, code: 'VALIDATION_ERROR', message: 'transcript est requis.' },
|
||||
400
|
||||
)
|
||||
{
|
||||
error: true,
|
||||
code: "VALIDATION_ERROR",
|
||||
message: "transcript est requis.",
|
||||
},
|
||||
400,
|
||||
);
|
||||
}
|
||||
|
||||
if (!body.tache || !VALID_TACHES_EO.includes(body.tache as string)) {
|
||||
return c.json(
|
||||
{
|
||||
error: true,
|
||||
code: 'VALIDATION_ERROR',
|
||||
message: `Tâche invalide. Valeurs acceptées : ${VALID_TACHES_EO.join(', ')}`,
|
||||
code: "VALIDATION_ERROR",
|
||||
message: `Tâche invalide. Valeurs acceptées : ${VALID_TACHES_EO.join(", ")}`,
|
||||
},
|
||||
400
|
||||
)
|
||||
400,
|
||||
);
|
||||
}
|
||||
|
||||
const profile = c.get('profile')
|
||||
// nclc_cible optionnel (défaut 9, valeurs 9 ou 10).
|
||||
let nclcCible: 9 | 10 = 9;
|
||||
if (body.nclc_cible !== undefined) {
|
||||
if (body.nclc_cible !== 9 && body.nclc_cible !== 10) {
|
||||
return c.json(
|
||||
{
|
||||
error: true,
|
||||
code: "VALIDATION_ERROR",
|
||||
message: "nclc_cible doit être 9 ou 10.",
|
||||
},
|
||||
400,
|
||||
);
|
||||
}
|
||||
nclcCible = body.nclc_cible;
|
||||
}
|
||||
|
||||
const profile = c.get("profile");
|
||||
const result = await correctionController.correctEO(
|
||||
body.simulationId as string,
|
||||
body.transcript as string,
|
||||
body.tache as string,
|
||||
profile
|
||||
)
|
||||
{
|
||||
simulationId: body.simulationId,
|
||||
tache: body.tache as "EO_T1" | "EO_T3",
|
||||
nclcCible,
|
||||
transcript: body.transcript,
|
||||
},
|
||||
profile,
|
||||
);
|
||||
|
||||
if ('error' in result) {
|
||||
return c.json(result, result.status as 401 | 404 | 500)
|
||||
if ("error" in result) {
|
||||
return c.json(result, result.status as 401 | 404 | 500);
|
||||
}
|
||||
|
||||
return c.json(result.data, 200)
|
||||
})
|
||||
return c.json(result.data, 200);
|
||||
});
|
||||
|
||||
export default corrections
|
||||
export default corrections;
|
||||
|
|
|
|||
39
src/routes/presentations.ts
Normal file
39
src/routes/presentations.ts
Normal file
|
|
@ -0,0 +1,39 @@
|
|||
import { Hono } from "hono";
|
||||
import { authMiddleware } from "../middleware/auth.js";
|
||||
import type { AppVariables } from "../middleware/auth.js";
|
||||
import * as presentationController from "../controllers/presentationController.js";
|
||||
|
||||
const presentations = new Hono<{ Variables: AppVariables }>();
|
||||
|
||||
// Sprint 4a — POST /presentations/generate
|
||||
//
|
||||
// Body : { reponses: { prenom_age_ville, formation_metier, situation_familiale,
|
||||
// loisirs, motivation_canada } }
|
||||
// Réponse : { presentation: string }
|
||||
//
|
||||
// Pas de stockage en base — le frontend gère la persistance locale (MVP).
|
||||
presentations.post("/generate", authMiddleware, async (c) => {
|
||||
let body: { reponses?: unknown };
|
||||
try {
|
||||
body = await c.req.json();
|
||||
} catch {
|
||||
return c.json(
|
||||
{
|
||||
error: true,
|
||||
code: "VALIDATION_ERROR",
|
||||
message: "Corps de la requête invalide.",
|
||||
},
|
||||
400,
|
||||
);
|
||||
}
|
||||
|
||||
const result = await presentationController.generate(body.reponses);
|
||||
|
||||
if ("error" in result) {
|
||||
return c.json(result, result.status as 400 | 500);
|
||||
}
|
||||
|
||||
return c.json(result.data, 200);
|
||||
});
|
||||
|
||||
export default presentations;
|
||||
36
src/routes/transcriptions.ts
Normal file
36
src/routes/transcriptions.ts
Normal file
|
|
@ -0,0 +1,36 @@
|
|||
import { Hono } from "hono";
|
||||
import { authMiddleware } from "../middleware/auth.js";
|
||||
import type { AppVariables } from "../middleware/auth.js";
|
||||
import { createTemporaryToken } from "../lib/deepgram.js";
|
||||
|
||||
// Sprint 4b — POST /transcriptions/token
|
||||
//
|
||||
// Délivre un token Deepgram éphémère (10 min) que le frontend utilise pour
|
||||
// ouvrir une connexion directe à l'API Deepgram. La clé maître DEEPGRAM_API_KEY
|
||||
// reste côté serveur. Aucun proxy WebSocket — la transcription live est gérée
|
||||
// navigateur ↔ Deepgram.
|
||||
|
||||
const TOKEN_TTL_SECONDS = 600;
|
||||
|
||||
const transcriptions = new Hono<{ Variables: AppVariables }>();
|
||||
|
||||
transcriptions.post("/token", authMiddleware, async (c) => {
|
||||
try {
|
||||
const { token, expires_in } = await createTemporaryToken(TOKEN_TTL_SECONDS);
|
||||
return c.json({ token, expires_in }, 200);
|
||||
} catch (err) {
|
||||
const message = err instanceof Error ? err.message : String(err);
|
||||
console.error("[transcriptions.token] generation failed", { message });
|
||||
return c.json(
|
||||
{
|
||||
error: true,
|
||||
code: "INTERNAL_ERROR",
|
||||
message:
|
||||
"Impossible de générer le token de transcription. Veuillez réessayer.",
|
||||
},
|
||||
500,
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
export default transcriptions;
|
||||
Loading…
Add table
Add a link
Reference in a new issue