WIP: implement audio base front+back #29

Draft
matthieu1312 wants to merge 1 commit from audio into main
Collaborator
No description provided.
Collaborator

Commentaire de Claude mdr ->

Le conflit n'est pas rattrapable côté front

apps/web/components/ChapterAudioPlayer.tsx n'existe plus sur main : il a été supprimé par
a670abd (refonte lecture 2026-07), arrivé sur main via e1535bf (PR #30) — et e1535bf est
justement la base de cette PR. La résolution du conflit ne peut donc être que « la suppression
gagne ». Ce qui l'a remplacé :

  • components/AudioPlayerProvider.tsx — un seul élément <audio> caché pour toute l'app, monté
    une fois dans app/layout.tsx, derrière un contexte React. La lecture survit à la navigation.
  • components/AudioPlayerBar.tsx — la barre de transport fixe du handoff 2026-07 : −15 s /
    play-pause / +15 s, titre + m:ss / m:ss, role="slider" utilisable au clavier comme à la
    souris, vitesse cyclique (1× → 1,25× → 1,5× → 0,75×), bascule « Lecture continue »
    (aria-pressed), fermeture.
  • Plus : reprise de la position par piste (localStorage, aec-audio:v1), mode audiobook
    (enchaînement sur nextHref en fin de piste), Espace = play/pause avec garde sur les champs de
    saisie, et le CTA jaune ListenButton.
  • components/AudioPlayer.test.tsx — 9 tests, dont un axe sans violation en état ouvert et
    fermé.

Du coup, remplacer le lecteur ne serait pas un changement d'un fichier : six appelants dépendent
de cette API (app/layout.tsx, app/page.tsx, app/programme/[slug]/page.tsx,
app/programme/[slug]/[section]/page.tsx, components/RandomMeasureCard.tsx,
components/ReadingComfortBar.tsx).

Trois points bloquants indépendamment du conflit

  1. autoPlay — c'est un manquement WCAG 1.4.2 / RGAA : la lecture ne doit démarrer que sur
    action explicite. Un test existant l'affirme noir sur blanc (« stays closed until a listen
    button is pressed — never autoplays on mount ») et vérifie que play() n'est pas appelé.
  2. L'aria-label mis en commentaire — ça retire le seul nom accessible du lecteur.
  3. src="http://localhost:3001/audio/track.mp3" en dur — ça court-circuite
    getAudioUrl(slug) (apps/web/lib/audio.ts), qui existe précisément pour ça :
    docs/plans/audio-narration-rgaa/research.md §5 point 3 dit « don't hardcode
    public/audio/... paths through the app … so swapping local files for a CDN base URL later is
    a one-line change, not a refactor ». Effet de bord : un seul track.mp3 pour tous les
    chapitres, et slug/title deviennent inutilisés.

Trois remarques plus mineures : apps/backend/public/track.mp3 fait 4,8 Mo commités — le dépôt
est déjà à ~154 Mo et il n'y a pas de LFS, donc l'audio réel (18 chapitres × ~5 Mo ≈ 80 Mo) ne peut
pas vivre dans l'historique git ; l'import getAudioUrl devenu inutilisé traînerait au lint ; et le
port 3001 est déjà pris — c'est celui sur lequel le docker compose d'open-dataec publie son
API en local (vérifié sur ma machine : 0.0.0.0:3001->3000). On a pris 8081 pour cette raison.

Ce qu'on garde de ta PR — et où ça a atterri

L'idée de fond est la bonne et on la reprend :

  • Servir l'audio depuis une origine HTTP séparée plutôt que depuis public/. Repris, mais
    branché sur le point de config existant : AUDIO_CDN_BASE_URL + getAudioUrl(slug), sans URL
    en dur dans un composant. On a aussi profité de l'occasion pour corriger une doc devenue
    fausse — cette variable était documentée comme build arg au motif que les pages étaient
    prégénérées, alors qu'elles sont toutes en force-dynamic depuis l'intégration open-dataec :
    elle redevient une variable d'environnement de runtime, modifiable sans rebuild.
  • Épingler la version de Node : il n'y avait effectivement aucun pin dans le dépôt (ni
    mise.toml, ni .nvmrc, ni engines, ni CI). On prend, en un seul pin à la racine, et on
    monte l'image Docker à node:24-alpine pour que le pin et le conteneur soient d'accord — les
    deux mise.toml par app à Node 24 face à un Dockerfile en node:22-alpine, c'était le
    scénario « ça marche chez moi ».

Deux écarts d'implémentation, pour info :

  • Pas de serveur Express dans apps/backend/. Ce dossier est un placeholder assumé, sans
    package.json, justement pour que npm workspaces ne le prenne pas comme package (voir
    apps/backend/README.md et CLAUDE.md) ; lui en donner un changerait ce sur quoi
    npm run dev|build|test|typecheck se propage. Et comme la cible de prod est du stockage objet
    (R2/S3 + CDN, §5.3), un serveur maison serait un service à maintenir et dockeriser pour zéro
    gain en prod. On passe donc par un service nginx:alpine dans docker-compose.yml qui monte un
    dossier local en lecture seule : mêmes capacités en dev (dont les requêtes Range, nécessaires
    pour le seek de la barre), zéro code à notre charge, et l'audio reste hors de git et hors de
    l'image.
  • Pas d'en-tête CORS. Access-Control-Allow-Origin: * n'est pas nécessaire ici : un
    <audio src> charge en cross-origin sans CORS tant qu'on ne met pas crossOrigin, ce que le
    provider ne fait pas. Ça le redeviendra si on ajoute un jour une analyse Web Audio API
    (forme d'onde).

Enfin, ta PR a eu un effet secondaire utile : elle nous a fait regarder de près
lib/audioAvailability.ts, qui déclarait en dur deux chapitres comme ayant une narration alors
que les seuls fichiers présents s'appelaient chapitre-1/2.mp3 — donc tous les boutons
« Écouter le chapitre » renvoyaient un 404
, avec un test vert par-dessus. Comme il n'existe
aucun fichier de narration réel pour l'instant, la disponibilité devient déclarative et vide par
défaut : pas de fichier, pas de bouton (règle d'honnêteté du contenu de design.md).

Le détail est dans docs/plans/pr29-audio-backend/ sur la branche pr29-audio-backend. Vu l'état
du conflit, le plus simple est probablement de fermer cette PR une fois la branche partie en
review — mais dis-nous si tu préfères la garder ouverte pour suivre le sujet.

Commentaire de Claude mdr -> ### Le conflit n'est pas rattrapable côté front `apps/web/components/ChapterAudioPlayer.tsx` **n'existe plus sur `main`** : il a été supprimé par `a670abd` (refonte lecture 2026-07), arrivé sur `main` via `e1535bf` (PR #30) — et `e1535bf` est justement la base de cette PR. La résolution du conflit ne peut donc être que « la suppression gagne ». Ce qui l'a remplacé : - `components/AudioPlayerProvider.tsx` — un seul élément `<audio>` caché pour toute l'app, monté une fois dans `app/layout.tsx`, derrière un contexte React. La lecture survit à la navigation. - `components/AudioPlayerBar.tsx` — la barre de transport fixe du handoff 2026-07 : −15 s / play-pause / +15 s, titre + `m:ss / m:ss`, `role="slider"` utilisable **au clavier** comme à la souris, vitesse cyclique (1× → 1,25× → 1,5× → 0,75×), bascule « Lecture continue » (`aria-pressed`), fermeture. - Plus : reprise de la position par piste (`localStorage`, `aec-audio:v1`), mode audiobook (enchaînement sur `nextHref` en fin de piste), Espace = play/pause avec garde sur les champs de saisie, et le CTA jaune `ListenButton`. - `components/AudioPlayer.test.tsx` — 9 tests, dont un `axe` sans violation en état ouvert et fermé. Du coup, remplacer le lecteur ne serait pas un changement d'un fichier : six appelants dépendent de cette API (`app/layout.tsx`, `app/page.tsx`, `app/programme/[slug]/page.tsx`, `app/programme/[slug]/[section]/page.tsx`, `components/RandomMeasureCard.tsx`, `components/ReadingComfortBar.tsx`). ### Trois points bloquants indépendamment du conflit 1. **`autoPlay`** — c'est un manquement WCAG 1.4.2 / RGAA : la lecture ne doit démarrer que sur action explicite. Un test existant l'affirme noir sur blanc (« stays closed until a listen button is pressed — never autoplays on mount ») et vérifie que `play()` n'est pas appelé. 2. **L'`aria-label` mis en commentaire** — ça retire le seul nom accessible du lecteur. 3. **`src="http://localhost:3001/audio/track.mp3"` en dur** — ça court-circuite `getAudioUrl(slug)` (`apps/web/lib/audio.ts`), qui existe précisément pour ça : `docs/plans/audio-narration-rgaa/research.md` §5 point 3 dit « don't hardcode `public/audio/...` paths through the app … so swapping local files for a CDN base URL later is a one-line change, not a refactor ». Effet de bord : un seul `track.mp3` pour tous les chapitres, et `slug`/`title` deviennent inutilisés. Trois remarques plus mineures : `apps/backend/public/track.mp3` fait **4,8 Mo commités** — le dépôt est déjà à ~154 Mo et il n'y a pas de LFS, donc l'audio réel (18 chapitres × ~5 Mo ≈ 80 Mo) ne peut pas vivre dans l'historique git ; l'import `getAudioUrl` devenu inutilisé traînerait au lint ; et le **port 3001 est déjà pris** — c'est celui sur lequel le `docker compose` d'open-dataec publie son API en local (vérifié sur ma machine : `0.0.0.0:3001->3000`). On a pris 8081 pour cette raison. ### Ce qu'on garde de ta PR — et où ça a atterri L'idée de fond est la bonne et on la reprend : - **Servir l'audio depuis une origine HTTP séparée** plutôt que depuis `public/`. Repris, mais branché sur le point de config existant : `AUDIO_CDN_BASE_URL` + `getAudioUrl(slug)`, sans URL en dur dans un composant. On a aussi profité de l'occasion pour corriger une doc devenue fausse — cette variable était documentée comme *build arg* au motif que les pages étaient prégénérées, alors qu'elles sont toutes en `force-dynamic` depuis l'intégration open-dataec : elle redevient une variable d'environnement de **runtime**, modifiable sans rebuild. - **Épingler la version de Node** : il n'y avait effectivement aucun pin dans le dépôt (ni `mise.toml`, ni `.nvmrc`, ni `engines`, ni CI). On prend, en un seul pin à la racine, et on monte l'image Docker à `node:24-alpine` pour que le pin et le conteneur soient d'accord — les deux `mise.toml` par app à Node 24 face à un `Dockerfile` en `node:22-alpine`, c'était le scénario « ça marche chez moi ». Deux écarts d'implémentation, pour info : - **Pas de serveur Express dans `apps/backend/`.** Ce dossier est un placeholder assumé, sans `package.json`, justement pour que npm workspaces ne le prenne pas comme package (voir `apps/backend/README.md` et `CLAUDE.md`) ; lui en donner un changerait ce sur quoi `npm run dev|build|test|typecheck` se propage. Et comme la cible de prod est du stockage objet (R2/S3 + CDN, §5.3), un serveur maison serait un service à maintenir et dockeriser pour zéro gain en prod. On passe donc par un service `nginx:alpine` dans `docker-compose.yml` qui monte un dossier local en lecture seule : mêmes capacités en dev (dont les requêtes `Range`, nécessaires pour le seek de la barre), zéro code à notre charge, et l'audio reste hors de git et hors de l'image. - **Pas d'en-tête CORS.** `Access-Control-Allow-Origin: *` n'est pas nécessaire ici : un `<audio src>` charge en cross-origin sans CORS tant qu'on ne met pas `crossOrigin`, ce que le provider ne fait pas. Ça le redeviendra si on ajoute un jour une analyse Web Audio API (forme d'onde). Enfin, ta PR a eu un effet secondaire utile : elle nous a fait regarder de près `lib/audioAvailability.ts`, qui déclarait en dur deux chapitres comme ayant une narration alors que les seuls fichiers présents s'appelaient `chapitre-1/2.mp3` — donc **tous les boutons « Écouter le chapitre » renvoyaient un 404**, avec un test vert par-dessus. Comme il n'existe aucun fichier de narration réel pour l'instant, la disponibilité devient déclarative et vide par défaut : pas de fichier, pas de bouton (règle d'honnêteté du contenu de `design.md`). Le détail est dans `docs/plans/pr29-audio-backend/` sur la branche `pr29-audio-backend`. Vu l'état du conflit, le plus simple est probablement de fermer cette PR une fois la branche partie en review — mais dis-nous si tu préfères la garder ouverte pour suivre le sujet.
This pull request has changes conflicting with the target branch.
  • .gitignore
View command line instructions

Checkout

From your project repository, check out a new branch and test the changes.
git fetch -u origin audio:audio
git switch audio
Sign in to join this conversation.
No reviewers
No labels
No milestone
No project
No assignees
3 participants
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
repos/aec2027!29
No description provided.