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
|
|
@ -17,79 +17,82 @@
|
|||
* les colonnes `*_status` restent en 'pending' indéfiniment.
|
||||
*/
|
||||
|
||||
import { supabase } from '../lib/supabase.js'
|
||||
import { supabase } from "../lib/supabase.js";
|
||||
import {
|
||||
correctEE as deepseekCorrectEE,
|
||||
correctEO as deepseekCorrectEO,
|
||||
generateProductionModele,
|
||||
generateExercices,
|
||||
type CorrectionRapport,
|
||||
type EORapport,
|
||||
type NclcCible,
|
||||
type TacheEE,
|
||||
} from '../lib/deepseek.js'
|
||||
import { PLANS, type Plan } from '../lib/access.js'
|
||||
import type { AuthProfile } from '../middleware/auth.js'
|
||||
type TacheEO,
|
||||
type TacheCorrection,
|
||||
} from "../lib/deepseek.js";
|
||||
import { PLANS, type Plan } from "../lib/access.js";
|
||||
import type { AuthProfile } from "../middleware/auth.js";
|
||||
|
||||
type CorrectionError = {
|
||||
error: true
|
||||
code: string
|
||||
message: string
|
||||
status: number
|
||||
}
|
||||
error: true;
|
||||
code: string;
|
||||
message: string;
|
||||
status: number;
|
||||
};
|
||||
|
||||
export interface CorrectEEInput {
|
||||
simulationId: string
|
||||
contenu: string
|
||||
tache: TacheEE
|
||||
nclcCible: NclcCible
|
||||
simulationId: string;
|
||||
contenu: string;
|
||||
tache: TacheEE;
|
||||
nclcCible: NclcCible;
|
||||
}
|
||||
|
||||
export async function correctEE(
|
||||
input: CorrectEEInput,
|
||||
profile: AuthProfile,
|
||||
): Promise<{ data: CorrectionRapport & { simulation_id: string } } | CorrectionError> {
|
||||
const { simulationId, contenu, tache, nclcCible } = input
|
||||
): Promise<
|
||||
{ data: CorrectionRapport & { simulation_id: string } } | CorrectionError
|
||||
> {
|
||||
const { simulationId, contenu, tache, nclcCible } = input;
|
||||
|
||||
// 1. Vérifier que la production existe et appartient à l'utilisateur
|
||||
const { data: production, error: fetchError } = await supabase
|
||||
.from('productions')
|
||||
.select('id, user_id, tache, sujet_id, rapport')
|
||||
.eq('id', simulationId)
|
||||
.single()
|
||||
.from("productions")
|
||||
.select("id, user_id, tache, sujet_id, rapport")
|
||||
.eq("id", simulationId)
|
||||
.single();
|
||||
|
||||
if (fetchError || !production) {
|
||||
return {
|
||||
error: true,
|
||||
code: 'SIMULATION_NOT_FOUND',
|
||||
message: 'Simulation introuvable.',
|
||||
code: "SIMULATION_NOT_FOUND",
|
||||
message: "Simulation introuvable.",
|
||||
status: 404,
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
if (production.user_id !== profile.id) {
|
||||
return {
|
||||
error: true,
|
||||
code: 'AUTH_REQUIRED',
|
||||
message: 'Cette simulation ne vous appartient pas.',
|
||||
code: "AUTH_REQUIRED",
|
||||
message: "Cette simulation ne vous appartient pas.",
|
||||
status: 401,
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
// 2. Charger le sujet pour alimenter le prompt maître (consigne + docs T3)
|
||||
let sujetConsigne: string | null = null
|
||||
let sourceDoc1: string | null = null
|
||||
let sourceDoc2: string | null = null
|
||||
let sujetConsigne: string | null = null;
|
||||
let sourceDoc1: string | null = null;
|
||||
let sourceDoc2: string | null = null;
|
||||
if (production.sujet_id) {
|
||||
const { data: sujetRow } = await supabase
|
||||
.from('sujets')
|
||||
.select('consigne, doc1_texte, doc2_texte')
|
||||
.eq('id', production.sujet_id)
|
||||
.single()
|
||||
.from("sujets")
|
||||
.select("consigne, doc1_texte, doc2_texte")
|
||||
.eq("id", production.sujet_id)
|
||||
.single();
|
||||
if (sujetRow) {
|
||||
sujetConsigne = (sujetRow.consigne as string | null) ?? null
|
||||
sourceDoc1 = (sujetRow.doc1_texte as string | null) ?? null
|
||||
sourceDoc2 = (sujetRow.doc2_texte as string | null) ?? null
|
||||
sujetConsigne = (sujetRow.consigne as string | null) ?? null;
|
||||
sourceDoc1 = (sujetRow.doc1_texte as string | null) ?? null;
|
||||
sourceDoc2 = (sujetRow.doc2_texte as string | null) ?? null;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -105,36 +108,37 @@ export async function correctEE(
|
|||
sourceDoc1,
|
||||
sourceDoc2,
|
||||
nclcCible,
|
||||
})
|
||||
});
|
||||
|
||||
const nclcObtenuEstime = nclcCible - 1
|
||||
const nclcObtenuEstime = nclcCible - 1;
|
||||
void runModeleJob({
|
||||
simulationId,
|
||||
tache,
|
||||
sujet: sujetConsigne,
|
||||
texte: contenu,
|
||||
nclcObtenu: nclcObtenuEstime,
|
||||
})
|
||||
});
|
||||
|
||||
let rapport: CorrectionRapport
|
||||
let rapport: CorrectionRapport;
|
||||
try {
|
||||
rapport = await correctionPromise
|
||||
rapport = await correctionPromise;
|
||||
} catch (err) {
|
||||
const message = err instanceof Error ? err.message : String(err)
|
||||
const stack = err instanceof Error ? err.stack : undefined
|
||||
console.error('[correctionController.correctEE] correction failed', {
|
||||
const message = err instanceof Error ? err.message : String(err);
|
||||
const stack = err instanceof Error ? err.stack : undefined;
|
||||
console.error("[correctionController.correctEE] correction failed", {
|
||||
simulationId,
|
||||
tache,
|
||||
nclcCible,
|
||||
message,
|
||||
stack,
|
||||
})
|
||||
});
|
||||
return {
|
||||
error: true,
|
||||
code: 'INTERNAL_ERROR',
|
||||
message: 'Erreur lors de la correction. Veuillez réessayer dans quelques instants.',
|
||||
code: "INTERNAL_ERROR",
|
||||
message:
|
||||
"Erreur lors de la correction. Veuillez réessayer dans quelques instants.",
|
||||
status: 500,
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
// 4. Persister la correction.
|
||||
|
|
@ -145,7 +149,7 @@ export async function correctEE(
|
|||
// Les colonnes *_status sont initialisées à 'pending' par la migration
|
||||
// (DEFAULT) et gérées exclusivement par runModeleJob / runExercicesJob.
|
||||
const { error: updateError } = await supabase
|
||||
.from('productions')
|
||||
.from("productions")
|
||||
.update({
|
||||
score: rapport.score,
|
||||
nclc: rapport.nclc,
|
||||
|
|
@ -156,193 +160,300 @@ export async function correctEE(
|
|||
erreurs_codes: rapport.erreurs_codes,
|
||||
rapport: JSON.stringify(rapport),
|
||||
})
|
||||
.eq('id', simulationId)
|
||||
.eq("id", simulationId);
|
||||
|
||||
if (updateError) {
|
||||
return {
|
||||
error: true,
|
||||
code: 'INTERNAL_ERROR',
|
||||
message: 'Erreur lors de la sauvegarde du rapport. Veuillez réessayer.',
|
||||
code: "INTERNAL_ERROR",
|
||||
message: "Erreur lors de la sauvegarde du rapport. Veuillez réessayer.",
|
||||
status: 500,
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
// 5. Lancer les exercices maintenant qu'on a rapport.erreurs_codes + criteres.
|
||||
// Ne JAMAIS await — cette promesse vit après la réponse HTTP.
|
||||
void runExercicesJob({ simulationId, tache, rapport })
|
||||
void runExercicesJob({ simulationId, tache, rapport });
|
||||
|
||||
// 6. Incrémenter simulations_used si le plan a une limite (non bloquant).
|
||||
if (PLANS[profile.plan as Plan].simulations_lifetime !== null) {
|
||||
await supabase
|
||||
.from('profiles')
|
||||
.from("profiles")
|
||||
.update({ simulations_used: profile.simulations_used + 1 })
|
||||
.eq('id', profile.id)
|
||||
.eq("id", profile.id);
|
||||
}
|
||||
|
||||
return { data: { ...rapport, simulation_id: simulationId } }
|
||||
return { data: { ...rapport, simulation_id: simulationId } };
|
||||
}
|
||||
|
||||
// ── Jobs asynchrones — modèle + exercices ───────────────────────────────
|
||||
|
||||
interface ModeleJobInput {
|
||||
simulationId: string
|
||||
tache: TacheEE
|
||||
sujet: string | null
|
||||
texte: string
|
||||
nclcObtenu: number
|
||||
simulationId: string;
|
||||
tache: TacheCorrection;
|
||||
sujet: string | null;
|
||||
texte: string;
|
||||
nclcObtenu: number;
|
||||
}
|
||||
|
||||
async function runModeleJob(input: ModeleJobInput): Promise<void> {
|
||||
const { simulationId, tache, sujet, texte, nclcObtenu } = input
|
||||
console.log('[runModeleJob] START', { simulationId, tache, nclcObtenu })
|
||||
const { simulationId, tache, sujet, texte, nclcObtenu } = input;
|
||||
console.log("[runModeleJob] START", { simulationId, tache, nclcObtenu });
|
||||
try {
|
||||
const modele = await generateProductionModele({ tache, sujet, texte, nclcObtenu })
|
||||
console.log('[runModeleJob] DeepSeek OK, updating productions', {
|
||||
const modele = await generateProductionModele({
|
||||
tache,
|
||||
sujet,
|
||||
texte,
|
||||
nclcObtenu,
|
||||
});
|
||||
console.log("[runModeleJob] DeepSeek OK, updating productions", {
|
||||
simulationId,
|
||||
modeleWordCount: modele.tcf_word_count,
|
||||
})
|
||||
});
|
||||
const { error: updateErr, data: updateData } = await supabase
|
||||
.from('productions')
|
||||
.update({ modele, modele_status: 'ready' })
|
||||
.eq('id', simulationId)
|
||||
.select('id, modele_status')
|
||||
console.log('[runModeleJob] update result', { simulationId, updateErr, updateData })
|
||||
.from("productions")
|
||||
.update({ modele, modele_status: "ready" })
|
||||
.eq("id", simulationId)
|
||||
.select("id, modele_status");
|
||||
console.log("[runModeleJob] update result", {
|
||||
simulationId,
|
||||
updateErr,
|
||||
updateData,
|
||||
});
|
||||
} catch (err) {
|
||||
const message = err instanceof Error ? err.message : String(err)
|
||||
const stack = err instanceof Error ? err.stack : undefined
|
||||
console.error('[runModeleJob] CAUGHT ERROR', { simulationId, message, stack })
|
||||
const message = err instanceof Error ? err.message : String(err);
|
||||
const stack = err instanceof Error ? err.stack : undefined;
|
||||
console.error("[runModeleJob] CAUGHT ERROR", {
|
||||
simulationId,
|
||||
message,
|
||||
stack,
|
||||
});
|
||||
try {
|
||||
const { error: fallbackErr } = await supabase
|
||||
.from('productions')
|
||||
.update({ modele_status: 'error' })
|
||||
.eq('id', simulationId)
|
||||
console.log('[runModeleJob] fallback update result', { simulationId, fallbackErr })
|
||||
} catch (fallbackExc) {
|
||||
console.error('[runModeleJob] FALLBACK UPDATE THREW', {
|
||||
.from("productions")
|
||||
.update({ modele_status: "error" })
|
||||
.eq("id", simulationId);
|
||||
console.log("[runModeleJob] fallback update result", {
|
||||
simulationId,
|
||||
message: fallbackExc instanceof Error ? fallbackExc.message : String(fallbackExc),
|
||||
})
|
||||
fallbackErr,
|
||||
});
|
||||
} catch (fallbackExc) {
|
||||
console.error("[runModeleJob] FALLBACK UPDATE THREW", {
|
||||
simulationId,
|
||||
message:
|
||||
fallbackExc instanceof Error
|
||||
? fallbackExc.message
|
||||
: String(fallbackExc),
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
interface ExercicesJobInput {
|
||||
simulationId: string
|
||||
tache: TacheEE
|
||||
rapport: CorrectionRapport
|
||||
simulationId: string;
|
||||
tache: TacheCorrection;
|
||||
rapport: CorrectionRapport;
|
||||
}
|
||||
|
||||
async function runExercicesJob(input: ExercicesJobInput): Promise<void> {
|
||||
const { simulationId, tache, rapport } = input
|
||||
console.log('[runExercicesJob] START', {
|
||||
const { simulationId, tache, rapport } = input;
|
||||
console.log("[runExercicesJob] START", {
|
||||
simulationId,
|
||||
tache,
|
||||
erreursCodesCount: rapport.erreurs_codes.length,
|
||||
})
|
||||
});
|
||||
try {
|
||||
const exercices = await generateExercices({
|
||||
tache,
|
||||
erreursCodes: rapport.erreurs_codes,
|
||||
criteres: rapport.criteres,
|
||||
})
|
||||
console.log('[runExercicesJob] DeepSeek OK, updating productions', {
|
||||
});
|
||||
console.log("[runExercicesJob] DeepSeek OK, updating productions", {
|
||||
simulationId,
|
||||
exercicesCount: exercices.length,
|
||||
})
|
||||
});
|
||||
const { error: updateErr, data: updateData } = await supabase
|
||||
.from('productions')
|
||||
.update({ exercices, exercices_status: 'ready' })
|
||||
.eq('id', simulationId)
|
||||
.select('id, exercices_status')
|
||||
console.log('[runExercicesJob] update result', { simulationId, updateErr, updateData })
|
||||
.from("productions")
|
||||
.update({ exercices, exercices_status: "ready" })
|
||||
.eq("id", simulationId)
|
||||
.select("id, exercices_status");
|
||||
console.log("[runExercicesJob] update result", {
|
||||
simulationId,
|
||||
updateErr,
|
||||
updateData,
|
||||
});
|
||||
} catch (err) {
|
||||
const message = err instanceof Error ? err.message : String(err)
|
||||
const stack = err instanceof Error ? err.stack : undefined
|
||||
console.error('[runExercicesJob] CAUGHT ERROR', { simulationId, message, stack })
|
||||
const message = err instanceof Error ? err.message : String(err);
|
||||
const stack = err instanceof Error ? err.stack : undefined;
|
||||
console.error("[runExercicesJob] CAUGHT ERROR", {
|
||||
simulationId,
|
||||
message,
|
||||
stack,
|
||||
});
|
||||
try {
|
||||
const { error: fallbackErr } = await supabase
|
||||
.from('productions')
|
||||
.update({ exercices_status: 'error' })
|
||||
.eq('id', simulationId)
|
||||
console.log('[runExercicesJob] fallback update result', { simulationId, fallbackErr })
|
||||
} catch (fallbackExc) {
|
||||
console.error('[runExercicesJob] FALLBACK UPDATE THREW', {
|
||||
.from("productions")
|
||||
.update({ exercices_status: "error" })
|
||||
.eq("id", simulationId);
|
||||
console.log("[runExercicesJob] fallback update result", {
|
||||
simulationId,
|
||||
message: fallbackExc instanceof Error ? fallbackExc.message : String(fallbackExc),
|
||||
})
|
||||
fallbackErr,
|
||||
});
|
||||
} catch (fallbackExc) {
|
||||
console.error("[runExercicesJob] FALLBACK UPDATE THREW", {
|
||||
simulationId,
|
||||
message:
|
||||
fallbackExc instanceof Error
|
||||
? fallbackExc.message
|
||||
: String(fallbackExc),
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ── EO — inchangé par Sprint 3.6a ───────────────────────────────────────
|
||||
// ── EO — Sprint 4b : transcript-only (audio géré côté frontend) ─────────
|
||||
//
|
||||
// Décision Sprint 4b : Deepgram en connexion directe navigateur ↔ Deepgram via
|
||||
// token éphémère (cf. /transcriptions/token). Le backend reçoit uniquement le
|
||||
// transcript final ; aucun audio n'est stocké côté serveur.
|
||||
//
|
||||
// Flux POST /corrections/eo :
|
||||
// 1. Vérifier que la production existe, appartient à l'utilisateur.
|
||||
// 2. Charger la consigne (utile au prompt EO).
|
||||
// 3. Lancer correction EO + modèle EO en parallèle (mêmes patterns que EE).
|
||||
// 4. Persister le rapport (revelation, diagnostic, conseil_nclc, erreurs_codes,
|
||||
// contenu = transcript).
|
||||
// 5. Lancer les exercices fire-and-forget.
|
||||
// 6. Incrémenter le quota.
|
||||
//
|
||||
// Le risque race-condition décrit dans correctEE s'applique aussi ici : on ne
|
||||
// touche PAS aux colonnes *_status dans l'update final.
|
||||
|
||||
export interface CorrectEOInput {
|
||||
simulationId: string;
|
||||
tache: TacheEO;
|
||||
nclcCible: NclcCible;
|
||||
transcript: string;
|
||||
}
|
||||
|
||||
export async function correctEO(
|
||||
simulationId: string,
|
||||
transcript: string,
|
||||
tache: string,
|
||||
input: CorrectEOInput,
|
||||
profile: AuthProfile,
|
||||
): Promise<{ data: EORapport & { simulation_id: string } } | CorrectionError> {
|
||||
): Promise<
|
||||
{ data: CorrectionRapport & { simulation_id: string } } | CorrectionError
|
||||
> {
|
||||
const { simulationId, tache, nclcCible, transcript } = input;
|
||||
|
||||
// 1. Vérifier la production + ownership.
|
||||
const { data: production, error: fetchError } = await supabase
|
||||
.from('productions')
|
||||
.select('id, user_id, tache')
|
||||
.eq('id', simulationId)
|
||||
.single()
|
||||
.from("productions")
|
||||
.select("id, user_id, tache, sujet_id")
|
||||
.eq("id", simulationId)
|
||||
.single();
|
||||
|
||||
if (fetchError || !production) {
|
||||
return {
|
||||
error: true,
|
||||
code: 'SIMULATION_NOT_FOUND',
|
||||
message: 'Simulation introuvable.',
|
||||
code: "SIMULATION_NOT_FOUND",
|
||||
message: "Simulation introuvable.",
|
||||
status: 404,
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
if (production.user_id !== profile.id) {
|
||||
return {
|
||||
error: true,
|
||||
code: 'AUTH_REQUIRED',
|
||||
message: 'Cette simulation ne vous appartient pas.',
|
||||
code: "AUTH_REQUIRED",
|
||||
message: "Cette simulation ne vous appartient pas.",
|
||||
status: 401,
|
||||
};
|
||||
}
|
||||
|
||||
// 2. Charger la consigne (utile au prompt EO).
|
||||
let sujetConsigne: string | null = null;
|
||||
if (production.sujet_id) {
|
||||
const { data: sujetRow } = await supabase
|
||||
.from("sujets")
|
||||
.select("consigne")
|
||||
.eq("id", production.sujet_id)
|
||||
.single();
|
||||
if (sujetRow) {
|
||||
sujetConsigne = (sujetRow.consigne as string | null) ?? null;
|
||||
}
|
||||
}
|
||||
|
||||
let rapport: EORapport
|
||||
// 3. Lancer correction EO + modèle EO en parallèle.
|
||||
const correctionPromise = deepseekCorrectEO(
|
||||
transcript,
|
||||
tache,
|
||||
nclcCible,
|
||||
sujetConsigne,
|
||||
);
|
||||
|
||||
const nclcObtenuEstime = nclcCible - 1;
|
||||
void runModeleJob({
|
||||
simulationId,
|
||||
tache,
|
||||
sujet: sujetConsigne,
|
||||
texte: transcript,
|
||||
nclcObtenu: nclcObtenuEstime,
|
||||
});
|
||||
|
||||
let rapport: CorrectionRapport;
|
||||
try {
|
||||
rapport = await deepseekCorrectEO(transcript, tache)
|
||||
} catch {
|
||||
rapport = await correctionPromise;
|
||||
} catch (err) {
|
||||
const message = err instanceof Error ? err.message : String(err);
|
||||
console.error("[correctionController.correctEO] correction failed", {
|
||||
simulationId,
|
||||
tache,
|
||||
nclcCible,
|
||||
message,
|
||||
});
|
||||
return {
|
||||
error: true,
|
||||
code: 'INTERNAL_ERROR',
|
||||
message: 'Erreur lors de la correction. Veuillez réessayer dans quelques instants.',
|
||||
code: "INTERNAL_ERROR",
|
||||
message:
|
||||
"Erreur lors de la correction. Veuillez réessayer dans quelques instants.",
|
||||
status: 500,
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
// 5. Persister le rapport. Pas de *_status (race condition — cf. correctEE).
|
||||
const { error: updateError } = await supabase
|
||||
.from('productions')
|
||||
.from("productions")
|
||||
.update({
|
||||
contenu: transcript,
|
||||
score: rapport.score,
|
||||
nclc: rapport.nclc,
|
||||
nclc_cible: rapport.nclc_cible,
|
||||
revelation: rapport.revelation,
|
||||
diagnostic: rapport.diagnostic,
|
||||
conseil_nclc: rapport.conseil_nclc,
|
||||
erreurs_codes: rapport.erreurs_codes,
|
||||
rapport: JSON.stringify(rapport),
|
||||
})
|
||||
.eq('id', simulationId)
|
||||
.eq("id", simulationId);
|
||||
|
||||
if (updateError) {
|
||||
return {
|
||||
error: true,
|
||||
code: 'INTERNAL_ERROR',
|
||||
message: 'Erreur lors de la sauvegarde du rapport. Veuillez réessayer.',
|
||||
code: "INTERNAL_ERROR",
|
||||
message: "Erreur lors de la sauvegarde du rapport. Veuillez réessayer.",
|
||||
status: 500,
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
// 6. Exercices fire-and-forget.
|
||||
void runExercicesJob({ simulationId, tache, rapport });
|
||||
|
||||
// 7. Quota.
|
||||
if (PLANS[profile.plan as Plan].simulations_lifetime !== null) {
|
||||
await supabase
|
||||
.from('profiles')
|
||||
.from("profiles")
|
||||
.update({ simulations_used: profile.simulations_used + 1 })
|
||||
.eq('id', profile.id)
|
||||
.eq("id", profile.id);
|
||||
}
|
||||
|
||||
return { data: { ...rapport, simulation_id: simulationId } }
|
||||
return { data: { ...rapport, simulation_id: simulationId } };
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue