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:
parent
71c1ad3018
commit
d1c8b548bb
34 changed files with 3255 additions and 70 deletions
54
src/shared/lib/__tests__/audio.test.ts
Normal file
54
src/shared/lib/__tests__/audio.test.ts
Normal file
|
|
@ -0,0 +1,54 @@
|
|||
/**
|
||||
* Tests de `blobToBase64` — Sprint 4c-3.
|
||||
*
|
||||
* jsdom fournit FileReader. On vérifie :
|
||||
* - encodage correct (base64 sans préfixe data URI)
|
||||
* - rejet propre si le reader émet onerror
|
||||
*/
|
||||
|
||||
import { describe, it, expect, vi } from 'vitest'
|
||||
import { blobToBase64 } from '../audio'
|
||||
|
||||
describe('blobToBase64', () => {
|
||||
it('encode un Blob en base64 sans le préfixe data URI', async () => {
|
||||
const blob = new Blob(['hello'], { type: 'audio/webm' })
|
||||
const base64 = await blobToBase64(blob)
|
||||
// 'hello' en base64 = 'aGVsbG8='
|
||||
expect(base64).toBe('aGVsbG8=')
|
||||
})
|
||||
|
||||
it('reject si FileReader émet une erreur', async () => {
|
||||
class FailingFileReader {
|
||||
onerror: (() => void) | null = null
|
||||
onload: (() => void) | null = null
|
||||
result: unknown = null
|
||||
readAsDataURL() {
|
||||
// Simule une erreur asynchrone.
|
||||
setTimeout(() => this.onerror?.(), 0)
|
||||
}
|
||||
}
|
||||
vi.stubGlobal('FileReader', FailingFileReader)
|
||||
|
||||
await expect(blobToBase64(new Blob(['x']))).rejects.toThrow(
|
||||
/FileReader: lecture du Blob audio impossible/,
|
||||
)
|
||||
|
||||
vi.unstubAllGlobals()
|
||||
})
|
||||
|
||||
it("reject si le résultat n'est pas une data URI bien formée", async () => {
|
||||
class WeirdFileReader {
|
||||
onerror: (() => void) | null = null
|
||||
onload: (() => void) | null = null
|
||||
result: string = 'pas-une-data-uri'
|
||||
readAsDataURL() {
|
||||
setTimeout(() => this.onload?.(), 0)
|
||||
}
|
||||
}
|
||||
vi.stubGlobal('FileReader', WeirdFileReader)
|
||||
|
||||
await expect(blobToBase64(new Blob(['x']))).rejects.toThrow(/format data URI/)
|
||||
|
||||
vi.unstubAllGlobals()
|
||||
})
|
||||
})
|
||||
36
src/shared/lib/audio.ts
Normal file
36
src/shared/lib/audio.ts
Normal file
|
|
@ -0,0 +1,36 @@
|
|||
/**
|
||||
* Helpers audio partagés — Sprint 4c-3.
|
||||
*/
|
||||
|
||||
/**
|
||||
* Convertit un Blob en chaîne base64 (sans le préfixe `data:<mime>;base64,`).
|
||||
*
|
||||
* Utilise FileReader.readAsDataURL puis strip le préfixe avant retour. Le
|
||||
* payload audio EO est ensuite envoyé tel quel dans le body JSON de
|
||||
* `POST /corrections/eo` (cf. SimulationFlowProvider.submitEoAudio).
|
||||
*
|
||||
* Reject si le reader émet une erreur ou si le résultat n'est pas une chaîne
|
||||
* data URI bien formée.
|
||||
*/
|
||||
export function blobToBase64(blob: Blob): Promise<string> {
|
||||
return new Promise((resolve, reject) => {
|
||||
const reader = new FileReader()
|
||||
reader.onerror = () => {
|
||||
reject(new Error('FileReader: lecture du Blob audio impossible.'))
|
||||
}
|
||||
reader.onload = () => {
|
||||
const result = reader.result
|
||||
if (typeof result !== 'string') {
|
||||
reject(new Error('FileReader: résultat inattendu (non-string).'))
|
||||
return
|
||||
}
|
||||
const commaIdx = result.indexOf(',')
|
||||
if (commaIdx < 0 || !result.startsWith('data:')) {
|
||||
reject(new Error('FileReader: résultat non conforme au format data URI.'))
|
||||
return
|
||||
}
|
||||
resolve(result.slice(commaIdx + 1))
|
||||
}
|
||||
reader.readAsDataURL(blob)
|
||||
})
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue