feat(eo): complete EO simulation flow (T1 + T3) with Gemini transcription

- Gemini batch transcription (no Deepgram live)
- blobToBase64 helper (shared/lib/audio.ts)
- AudioRecorder: remove onChunk, add maxSeconds/onMaxReached auto-submit
- Timer stops at maxSeconds and triggers auto-submission
- EnregistrementEOPage: audioBase64 to backend, fix race condition step=done
- SimulationFlowProvider: submitEoAudio(audioBase64, mimeType, nclcCible)
- MIME normalization (strip codec params)
- Split CORRECTION_EE_TIMEOUT_MS (60s) / CORRECTION_EO_TIMEOUT_MS (120s)
- PresentationGenereeT1Page: localStorage persistence

Typecheck: OK · Tests: 159/159 

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Hermann_Kitio 2026-04-25 08:28:51 +03:00
parent 71c1ad3018
commit d1c8b548bb
34 changed files with 3255 additions and 70 deletions

View file

@ -0,0 +1,49 @@
/**
* Tests du domaine `transcription` Sprint 4c-1.
*
* Valide :
* - succès : retourne le token et expires_in
* - erreur : ApiError propagée par apiFetch
*/
import { describe, expect, it, vi, beforeEach } from 'vitest'
vi.mock('@/shared/lib/api-client', () => ({
apiFetch: vi.fn(),
}))
import { apiFetch } from '@/shared/lib/api-client'
import { requestDeepgramToken } from '../api'
const mocked = vi.mocked(apiFetch)
describe('requestDeepgramToken', () => {
beforeEach(() => {
mocked.mockReset()
})
it('retourne le token et expires_in en cas de succès', async () => {
mocked.mockResolvedValueOnce({ token: 'dg-temp-abc', expires_in: 600 })
const result = await requestDeepgramToken()
expect(result).toEqual({ token: 'dg-temp-abc', expires_in: 600 })
expect(mocked).toHaveBeenCalledWith('/transcriptions/token', {
method: 'POST',
timeoutMs: 10_000,
retry: { max: 0, baseDelayMs: 0 },
})
})
it('propage les ApiError du backend', async () => {
mocked.mockRejectedValueOnce({
error: true,
code: 'AUTH_REQUIRED',
message: 'Auth required',
})
await expect(requestDeepgramToken()).rejects.toMatchObject({
code: 'AUTH_REQUIRED',
})
})
})

View file

@ -0,0 +1,21 @@
/**
* Appels API du domaine `transcription`.
*
* `POST /transcriptions/token` : timeout 10 s, retry désactivé.
* Le retry est désactivé volontairement : un POST non-idempotent qui
* consomme un crédit Deepgram à chaque appel ne doit pas être rejoué
* silencieusement en cas d'erreur réseau transitoire.
*/
import { apiFetch } from '@/shared/lib/api-client'
import type { TranscriptionToken } from './types'
const TOKEN_TIMEOUT_MS = 10_000
export function requestDeepgramToken(): Promise<TranscriptionToken> {
return apiFetch<TranscriptionToken>('/transcriptions/token', {
method: 'POST',
timeoutMs: TOKEN_TIMEOUT_MS,
retry: { max: 0, baseDelayMs: 0 },
})
}

View file

@ -0,0 +1,15 @@
/**
* Types publics du domaine `transcription`.
*
* Le frontend obtient un token Deepgram éphémère via le backend
* (`POST /transcriptions/token`) puis ouvre une connexion WebSocket
* directe vers Deepgram pour la transcription live. La clé maître
* Deepgram reste côté backend (cf. SECURITY.md).
*/
export interface TranscriptionToken {
/** JWT éphémère Deepgram (durée de vie ~10 min). */
token: string
/** Durée de validité du token, en secondes. */
expires_in: number
}