docs(roadmap): add #108 image support and #109 audio/video players
CI / lint (push) Successful in 1m55s
CI / security (push) Successful in 1m25s
CI / test (push) Successful in 4m33s
CI / build (push) Successful in 1m15s
CI / e2e (push) Successful in 11m59s

- #108: images parity — tree + indexing (never read bytes into TF-IDF),
  fix broken standalone viewer (img src points to JSON /raw instead of
  /api/image), zoom/pan viewer, thumbnails, SVG sandbox hardening
- #109: audio/video — HTML5 players, shared media streaming endpoint
  with Range/206 (extract helper from existing pdf/stream), codec
  fallback UI, PWA/mobile notes
- Update effort summary (8 items, ~28-43 days)
This commit is contained in:
2026-09-22 23:28:55 -04:00
parent 705f755b6b
commit 69cee4d93a
+106 -1
View File
@@ -80,6 +80,110 @@
---
## ⚪ Backlog — Média (images, audio, vidéo)
### 108. Support complet des images — arborescence, visionneuse, indexation
- **Effort :** 3-4 jours | **Impact :** 🟡 | **Zone :** backend (indexer, main) + frontend (viewer, utils)
- **Statut :** ⚪ Prévu
- **Description :** parité fonctionnelle des images avec les autres fichiers du vault : apparition dans l'arborescence, ouverture directe dans une visionneuse dédiée (zoom molette, pan, navigation dossier), indexation (nom + métadonnées), miniatures. Actuellement seul l'affichage *inline* dans un document markdown (`![[image.png]]`) fonctionne ; l'image isolée est invisible dans l'arborescence et son affichage standalone est cassé.
- **Architecture actuelle (vérifiée sur `main` @ v2.16.6) :**
- `SUPPORTED_EXTENSIONS` (`backend/indexer.py:61`) : aucune extension image → images filtrées de l'arborescence et de l'index.
- `api_file_view()` (`backend/main.py:~2411`) : branche `IMAGE_EXTENSIONS` (`.png .jpg .jpeg .gif .svg .webp .bmp .ico`) qui retourne `is_image: true`, MAIS le `<img src>` généré pointe vers `/api/file/{vault}/raw?path=…` — un endpoint qui retourne du **JSON** (`FileRawResponse`, `main.py:1569`), pas des octets d'image. Affichage standalone cassé. Le frontend (`frontend/js/viewer.js:600`, branche `data.is_image`) reproduit la même URL cassée pour le bouton « Plein écran ».
- `/api/image/{vault}` (`backend/main.py:3160`) : endpoint existant et fonctionnel qui sert les octets avec le bon MIME type. C'est LUI que doivent utiliser le `<img>` backend et le frontend. L'auth navigateur fonctionne (middleware JWT avec fallback cookie `access_token`).
- `image_processor.py` + `attachment_indexer.py` : réécriture des 4 syntaxes Obsidian (`![[img]]`, `![alt](path)`…) vers `/api/image` avec résolution multi-stratégie → **l'inline markdown fonctionne déjà**, aucun changement requis.
- CSP : `img-src 'self' data: blob:` — compatible, rien à assouplir.
- **Sous-tâches :**
##### A. Backend — arborescence & indexation (1 j)
- [ ] **A1.** Créer `backend/media_types.py` : constantes partagées `IMAGE_EXTENSIONS`, `AUDIO_EXTENSIONS`, `VIDEO_EXTENSIONS` (réutilisées par #109) ; remplacer le set local de `api_file_view()`.
- [ ] **A2.** Intégrer les extensions images à `SUPPORTED_EXTENSIONS` (`indexer.py:61`) **avec branche binaire** : `index_document()` ne doit JAMAIS `read_text()`/`read_bytes()` une image — indexer nom/taille/mtime uniquement, `content: ""` pour que le TF-IDF reste propre.
- [ ] **A3.** Vérifier le reindex watchdog sur création/suppression d'image (même filtre d'extensions, couvert par A2).
- [ ] **A4.** Recherche : le nom de fichier est déjà indexé (`chatScreenshot.png` trouvable par « screenshot ») ; ajouter le filtre `ext:png`/`ext:jpg` (mécanisme des filtres `ext:` existants) ; comptabiliser les images à part dans les compteurs du dashboard (#11).
##### B. Backend — correction affichage standalone (0,5 j)
- [ ] **B1.** Fix `<img>` cassé : `api_file_view()` → `src="/api/image/{vault}?path=…"` (URL encodée) ; idem `viewer.js:600` (bouton Plein écran).
- [ ] **B2.** Métadonnées : dimensions via `<img onload>` frontend (`naturalWidth/naturalHeight` — gratuit, recommandé ; Pillow seulement si C1 est retenu).
- [ ] **B3.** Sécurité SVG : sur `/api/image`, ajouter `Content-Security-Policy: sandbox` (ou `Content-Disposition: attachment`) quand `ext == .svg` — un SVG ouvert directement dans un onglet exécute son JS (XSS same-origin) ; dans une balise `<img>` il est inoffensif.
##### C. Backend — miniatures (1 j, phase 2 tolérable)
- [ ] **C1.** Dépendance `pillow>=10.0` (wheels précompilés, rien de système dans `python:3.11-slim`).
- [ ] **C2.** `GET /api/media/{vault}/thumb` : miniature 256 px WebP, cache disque `/data/.obsigate-cache/thumbs/{sha1(path+mtime)}.webp` (invalidation naturelle par mtime). SVG : servir l'original. GIF animé : première frame.
- [ ] **C3.** Génération dans un thread pool (`run_in_executor`), fallback original si timeout 2 s.
##### D. Frontend — visionneuse (1 j)
- [ ] **D1.** `EXT_ICONS` (`frontend/js/utils.js`) : extensions images → icône Lucide `image`.
- [ ] **D2.** Visionneuse digne de ce nom dans `viewer.js` : image centrée `object-fit:contain`, **zoom molette (0,1×–8×), pan au drag, double-clic reset**, boutons +/−/reset, badge de zoom.
- [ ] **D3.** Navigation ←/→ entre images du même dossier (données `list_directory` déjà disponibles) ; bouton « Ouvrir original » (nouvel onglet, `/api/image`).
- [ ] **D4.** Barre d'outils : Télécharger (déjà fonctionnel), lightbox plein viewport (fond `rgba(0,0,0,.9)`), panneau métadonnées repliable (dimensions, taille, type, date).
- [ ] **D5.** (Optionnel 🟢) Vue galerie vignettes pour les dossiers d'images via `/api/media/thumb`, `loading="lazy"`.
- [ ] **D6.** Split View (#75) : rendu dans `getContentArea()` du panneau actif ; vérifier le cache d'onglets.
##### E. Tests (0,5 j)
- [ ] **E1.** `test_image_api.py` : `/api/image` octets + MIME ; SVG avec header sandbox ; `api_file_view` → `is_image: true` avec URL `/api/image` dans le html.
- [ ] **E2.** `test_image_indexing.py` : image visible dans `list_directory`, indexée avec contenu vide, watcher OK, rien de binaire dans le TF-IDF.
- [ ] **E3.** Playwright (#58) : clic sur une image de l'arborescence → visionneuse rendue ; zoom molette applique la transform.
- **Points d'attention / risques**
- **Ne jamais lire les octets dans l'indexeur** — dérive n°1 d'un A2 bâclé (UnicodeDecodeError de masse dans les logs, TF-IDF pollué).
- **`_attachments/`** : les vaults Obsidian réels contiennent des milliers d'images → vérifier la fluidité de l'arborescence et le coût du scan d'index.
- **HEIC (iPhone)** : non décodable par les navigateurs → hors scope v1, documenter la limitation ; `pillow-heif` en v2 si demande.
- **Parité Obsidian Desktop** : la visionneuse D2 doit atteindre le même confort zoom/pan, sinon l'utilisateur repart sur le desktop.
---
### 109. Support audio & vidéo — lecteurs intégrés HTML5
- **Effort :** 2-3 jours | **Impact :** 🟡 | **Zone :** backend (media streaming) + frontend (viewer)
- **Statut :** ⚪ Prévu — dépend du socle `media_types.py` de #108-A1
- **Description :** prise en charge des fichiers audio (`.mp3 .m4a .aac .wav .ogg .oga .opus .flac`) et vidéo (`.mp4 .webm .mov .m4v`) avec la même parité que les autres fichiers : apparition dans l'arborescence, indexation du nom, lecture directe dans le viewer via les balises HTML5 `<audio>` / `<video>`. Actuellement ces fichiers tombent dans le chemin binaire « Ce fichier est binaire et ne peut pas être affiché » + bouton download (`frontend/js/viewer.js:622`).
- **Choix technique — lecteurs HTML5 natifs, pas de transcodage :**
- `<video>`/`<audio>` sont natifs partout (Chrome, Firefox, Edge, Safari, mobile) : play, scrub, volume, vitesse 0,25–2×, PiP, plein écran — sans une ligne de JS de contrôle.
- **Zéro dépendance lourde** : pas de ffmpeg dans l'image Docker, pas de file de transcodage. Les formats web-natifs (MP4/H.264, WebM, MP3, AAC, Opus, FLAC, WAV) couvrent l'immense majorité des notes vocales et screencasts.
- Le vrai travail serveur est le **support HTTP `Range` / 206 Partial Content** : sans lui, pas de scrub (le navigateur retélécharge tout pour se positionner) et Safari refuse le MP4. Bonne nouvelle : `pdf/stream` (`backend/main.py:2617`) **implémente déjà les Range** (206, `Content-Range`, 416) — extraire cette logique en helper `stream_file_with_range(file_path, request, media_type)` et s'en servir pour les trois médias.
- Métadonnées (durée, résolution) lues **côté client** via `loadedmetadata` (`duration`, `videoWidth/videoHeight`) — aucun ffprobe serveur nécessaire.
- **Sous-tâches :**
##### A. Backend — socle média partagé (1 j) — bénéficie aussi à #74/#108
- [ ] **A1.** Ajouter `AUDIO_EXTENSIONS` / `VIDEO_EXTENSIONS` dans `backend/media_types.py` (#108-A1) et les intégrer à `SUPPORTED_EXTENSIONS` avec la branche « contenu non parsé » (#108-A2).
- [ ] **A2.** Extraire le helper Range de `pdf/stream` en fonction réutilisable ; nouvel endpoint `GET /api/media/{vault}?path=…` : streaming par chunks 64 ko (`aiofiles`), 206 + `Content-Range` + `Accept-Ranges`, 416 sur Range invalide, MIME via `mimetypes` + map de secours (`.m4a→audio/mp4`, `.opus→audio/ogg`, `.mov→video/quicktime`, `.m4v→video/mp4`).
- [ ] **A3.** Gardes-fous : `_resolve_safe_path`, `check_vault_access`, taille inline max `OBSIGATE_MEDIA_MAX_INLINE_MB` (défaut 500 — au-delà, UI download).
- [ ] **A4.** `api_file_view()` : branches AVANT le `read_text()` → `is_audio: true` / `is_video: true` (+ `size_bytes`, `stream_url` vers `/api/media`). `pdf/stream` bascule sur le helper A2 (comportement inchangé, tests de régression).
##### B. Frontend — lecteur audio (0,5 j)
- [ ] **B1.** Dispatch `data.is_audio` dans `viewer.js` : `<audio controls src=stream_url>` pleine largeur, artwork placeholder (icône Lucide `music`), titre + voûte + taille dans la toolbar, durée formatée via `loadedmetadata`.
- [ ] **B2.** `EXT_ICONS` : audio → Lucide `audio-lines` ; vidéo → `video`.
- [ ] **B3.** Pause au changement d'onglet (comportement fichier standard, cohérence #75) — pas de lecture persistante en v1.
##### C. Frontend — lecteur vidéo (0,5 j)
- [ ] **C1.** Dispatch `data.is_video` : `<video controls playsinline>` centré, `max-height: calc(100vh - header)`, fond noir, letterboxing.
- [ ] **C2.** Vérifier le bouton pop-out avec médias (médias same-origin, `X-Frame-Options: SAMEORIGIN` compatible).
- [ ] **C3.** Codec non supporté (`.mkv`, `.avi`, HEVC…) : sur l'événement `error` de l'élément média → remplacer le lecteur par l'UI binaire existante + message « Ce format ne peut pas être lu dans votre navigateur » + Télécharger / Ouvrir dans un nouvel onglet (le player OS prend le relais).
##### D. Recherche & intégrations (0,5 j)
- [ ] **D1.** Filtres `ext:mp3`, `ext:mp4`, etc. (mécanisme #108-A4).
- [ ] **D2.** Fichiers récents (#35) / dashboards : exclure les médias des previews de contenu (aucun texte).
- [ ] **D3.** BooksLM (#76) : exclure les médias du contexte collecté (pas de texte extractible).
- [ ] **D4.** (Hors scope, noter la porte) Transcription audio via Whisper sur le provider AI existant.
##### E. Mobile & PWA (0,25 j)
- [ ] **E1.** Tester le scrub sur iOS Safari (exige un Range strictement correct) et Android Chrome.
- [ ] **E2.** Service worker (#59) : exclure les médias du cache offline (taille), stratégie Network First inchangée pour le reste.
##### F. Tests (0,5 j)
- [ ] **F1.** `test_media_stream.py` : `bytes=0-1023` → 206 + Content-Range + 1024 octets ; Range hors borne → 416 ; sans Range → 200 + Accept-Ranges ; path traversal refusé ; voûte sans accès refusée. Régression : `pdf/stream` inchangé après extraction du helper.
- [ ] **F2.** `test_media_indexing.py` : un `.mp3` apparaît dans l'arborescence, indexé sans contenu, watcher sans erreur.
- [ ] **F3.** Playwright : fixtures binaires minimaux commités dans `tests/fixtures/` (mini `.mp3` sine wave ~2 ko, mini `.mp4`) → `<audio>` et `<video>` rendus.
- **Points d'attention / risques**
- **Range est LE prérequis** (sous-tâche la plus testée, pas une option) : sans 206 correct, Safari casse et le scrub hache la bande passante.
- **Formats hors web-natifs** (`.mkv`, `.avi`, HEVC, AC-4) : non lisibles sans transcodage (ffmpeg hors scope) — le fallback C3 les rend téléchargeables proprement au lieu de silencieusement cassés.
- **Poids disque** : une vidéo de 1 Go dans un vault ralentit les backups (#17, gzip) — envisager d'exclure les gros médias de la compression gzip.
- **Sous uvicorn (worker unique)** : lectures vidéo simultanées = file descriptors + bande passante ; surveiller via le health check enrichi (#68).
- **Hors scope v1** : HLS, sous-titres (`<track src=.vtt>`), thumbnails vidéo (première frame) — extensions naturelles documentées ici.
---
## ⚪ Backlog — Sécurité, architecture & performance (P0/P1)
### 84. Consolidation & sécurité — revue statique 2026-09-13 (phase 1)
@@ -191,9 +295,10 @@
|---|---|---|
| ✅ Complété | #1 → #59, #61–72, #74–76, #78–84, #86, #88–93, #94–100, #102–107, #92 | ~120 jours réalisés |
| 🔵 P2 restant | #77 Desktop : signature de code (non retenue), 6 tests E2E **manuels** ([protocole](./DESKTOP_E2E_CHECKLIST.md)) | ~0,5-1 jour |
| ⚪ Média | #108 Images (arborescence, visionneuse, indexation) + #109 Audio/vidéo (lecteurs HTML5, Range) | 5-7 jours |
| ⚪ P4 restant | #73 Sync (6-8j) | 6-8 jours |
| ⚪ P0/P1 restant | #85, #87 Refonte architecturale, CI/CD (BUG-035 → BUG-040 corrigés, #86 livré) | ~11-17 jours |
| **Total restant** | **6 items + finitions** | **~23-36 jours** |
| **Total restant** | **8 items + finitions** | **~28-43 jours** |
---