← Tous les manuels · Portail
↗ Ouvrir l'app

Éditeur Podcast (podcast-editor)

Mini éditeur de montage multicam web pour valider/ajuster les montages produits par le skill podcast-montage, faire quelques cuts à la main, et exporter la vidéo soi-même.

Rôle

Charge un projet podcast (2 rushes + cutlist.json généré par montage.py analyze), affiche une timeline façon logiciel de montage avec preview (via proxys 480p), permet d'éditer les plans (caméra, coupe, trim, suppression, synchro) puis d'exporter le rendu pleine qualité depuis les rushes.

URL / Service / Port

  • - URL publique : https://montage.panelbay.com
  • - Service systemd : podcast-editor.service (systemctl status/restart podcast-editor)
  • - Port local : 8792
  • - nginx vhost : /etc/nginx/sites-enabled/montage.panelbay.com.conf
  • Structure

  • - app.py : backend FastAPI. Endpoints :
  • - static/index.html : l'éditeur (vanilla JS), 2 onglets partageant le même moteur de timeline (chacun son tableau de plans dans store.{montage,intro}, alias segs = onglet actif). Preview = 2 <video> (cam1 audio, cam2 muet) synchronisés par l'offset off.cam2, on affiche cam1/cam2/split selon le plan ; l'audio vient toujours de cam1 (mix master).
  • - proxies/cam1.mp4 (avec audio) + proxies/cam2.mp4 (muet) : générés par make_proxies.sh (480p, keyframes denses pour un scrub fluide). Régénérer si on change de projet.
  • Projet ciblé

    Défini par l'env PODCAST_WORK (défaut /root/workspace/podcast-2609/work). Pour éditer un autre podcast : changer PODCAST_WORK dans le service, régénérer les proxys depuis les rushes de ce projet, systemctl restart podcast-editor.

    Modèle de synchro (important)

    off.cam2 = de combien la cam2 a démarré après la cam1. Source vidéo cam2 pour le temps master t = t - off.cam2 (identique à montage.py srct). Ne jamais additionner (bug historique corrigé le 2026-09-22, cf. podcast-montage/references/technique.md).

    Pièges export / progression

  • - L'export ré-encode chaque plan depuis les rushes (pleine qualité) : ~10 min pour un montage de ~177 plans + l'intro. C'est normal que ce soit long.
  • - La barre de progression lit les lignes seg i/N de montage.py render_exact. Avant le 2026-09-23, ce log ne sortait que tous les 40 segments → la barre semblait figée à 2 % pendant plusieurs minutes (mais le rendu tournait). Corrigé : log plan par plan (montage.py) + message Rendu du montage : plan i/N (app.py). Vérifier un export « bloqué » : ls work/segx/*.mp4 | wc -l doit augmenter, et ps aux | grep ffmpeg.
  • Pièges

  • - Les rushes sont lourds (~1.4 Go), d'où les proxys pour la preview. L'export, lui, repart des rushes (pleine qualité).
  • - L'export d'un montage de ~19 min prend plusieurs minutes (re-encode) : barre de progression via /api/export/status.
  • - off.cam1 reste 0 (master = cam1).
  • CHANGELOG

  • - 2026-09-23 (p) : à la fin de l'export, téléchargement auto (downloadNow() clique un <a download> sur /download, 1x via _dlDone) + gros bouton « ⬇ Télécharger la vidéo » (exportDoneUI). Avant : seulement un petit lien texte qu'il fallait cliquer (l'utilisateur croyait l'export raté). Au chargement d'un export déjà fini : bouton sans déclenchement auto.
  • - 2026-09-23 (o) : le zoom par caméra devient un cadrage manuel (zoom + repositionnement). Modèle cam_frame:{cam1:{zoom,x,y},cam2:{...}} (x,y ∈ [-1,1], 0=centre) dans cutlist.json (rétro-compat cam_zoom scalaire). UI « Cadrage caméra » : sliders zoom (100-160%) + glisser l'image de l'aperçu pour recadrer la cam affichée (curseur grab quand zoomée, hint si pas zoomée). Aperçu live via transform: translate+scale calculé (camTransform), cohérent avec le crop d'export. montage.py render_exact applique scale(up)+crop décalé par cam (solo + demi split). API /api/project renvoie cam_frame, /api/cutlist le persiste (clamp zoom 1-2, x/y ±1). Remplace le (n).
  • - 2026-09-23 (n) : zoom par caméra (réglage global appliqué à TOUS les plans d'une cam). Sliders Matt/Seb (100-130%) dans le panneau montage, aperçu live (CSS scale(var(--z)) sur la vidéo de la cam affichée, désactivé en onglet intro), persisté dans cutlist.json (cam_zoom:{cam1,cam2}) et appliqué à l'export : montage.py render_exact insère scale(up)+crop centré par cam (solo cam1/cam2 en 1920x1080, et chaque demi en split 960x1080). Endpoints : /api/project renvoie cam_zoom, /api/cutlist le persiste (clampé 1.0-2.0). Autosave inclus. Base d'un futur « appliquer un réglage à tous les plans d'une cam ».
  • - 2026-09-23 (m) : feedback visuel de l'export. Au clic sur Exporter, le bouton passe en état busy (spinner + « Rendu… X % » dedans, désactivé, rouge), barre de progression plus haute (14px) et pleine largeur avec transition. exportBtn() gère l'état ; pollExport met le % dans le bouton + la barre. Reprise à l'ouverture si un export tourne déjà (P.export.state==='running'). Frontend seul (pas de restart). Avant : rien ne montrait que le clic avait pris, barre discrète.
  • - 2026-09-23 (l) : transport à focus. Play/Espace suivent la dernière zone touchée (transport = 'timeline' | 'plan') au lieu de rester bloqués sur le plan sélectionné. Toucher la timeline/ruler/scrub ou déplacer un plan → 'timeline' ; scrub dans l'atelier ou ▶ Lire le plan → 'plan'. Indicateur #tphint (« Espace ▶ montage complet » / « Espace ▶ ce plan »), #detail.focused + #dplay.on en surbrillance. Remplace le (h) où Play restait capté par le plan tant qu'on n'avait pas fait Échap. Résout la plainte : après édition d'un clip, Espace rejoue le montage complet dès qu'on reclique la timeline.
  • - 2026-09-23 (k) : poignées de bord de l'atelier (#dstrip .dh) refaites en fine ligne nette (2px + petit onglet de saisie en haut, zone de grab invisible de 16px, hover jaune) au lieu du gros taquet opaque qui cachait la forme d'onde. CSS seul.
  • - 2026-09-23 (j) : sauvegarde automatique. Un intervalle (1,5 s) compare un snapshot des 2 onglets (montage : segs+off ; intro : clips+réglages) et enregistre en fond via /api/cutlist et /api/intro dès qu'il y a un changement. Indicateur #savestat (« enregistrement… » / « ✓ enregistré »). Le bouton Enregistrer + l'export mettent le snapshot à jour (pas de double save). Avant : sauvegarde manuelle seulement.
  • - 2026-09-23 (i) : réordonner les plans en drag & drop sur la timeline (startPlanDrag : mousedown sur le corps d'un plan, glisser = déplacer avec marqueur d'insertion #insmark, sans glisser = simple clic/sélection) ; marche montage + intro (rerender candidats). Clic dans la zone vide de la timeline (#track/#wave/#inner) = deselectPlan() : ferme l'atelier, Play/Pause repart sur la timeline complète. Curseur des plans grab.
  • - 2026-09-23 (h) : Play/Pause contextuel. Quand un plan est ouvert dans l'atelier du bas (detailOpen()), le bouton Play et la barre Espace pilotent la lecture de CE plan (via #dplay) au lieu de toute la timeline. Échap désélectionne (ferme l'atelier) et rend Play à la timeline globale. stopPreview remet le libellé du bouton.
  • - 2026-09-23 (g) : dans l'onglet Intro, clic sur un clip proposé = l'éditer à la main (editCand) : l'ajoute à la timeline d'intro si absent (sinon sélectionne l'existant par id) et ouvre l'atelier, comme un plan normal. Boutons ▶/+ font stopPropagation. Carte cursor:pointer.
  • - 2026-09-23 (f) : poignées de trim de la timeline principale masquées par défaut, visibles seulement au survol / sur le plan sélectionné (moins d'encombrement, le redimensionnement précis se fait dans l'atelier du bas). CSS seul.
  • - 2026-09-23 (e) : piste son + atelier scrubable. Piste de forme d'onde (#wave canvas) sous la timeline principale (les 2 onglets) : le son master (cam1) suit les plans, donc les coupes/silences se voient. Pics servis par GET /api/peaks (fichier work/peaks.json, 50 pics/s normalisés, généré à la volée depuis cam1.wav si absent). L'atelier du plan (bas) a maintenant sa propre forme d'onde de la fenêtre + playhead : clic/glisser sur #dstrip = scrub/prévisualisation du plan, bouton ▶ Lire le plan (#dplay) joue [dpos→fin]. Playhead principal prolongé pour traverser la piste son. Stage réduit à 44vh. Demandé par Matt (« timeline sur le plan » + « timeline son partout »).
  • - 2026-09-23 (d) : aperçu fidèle de l'intro + pistes/réglages. Dans l'onglet Intro le lecteur affiche maintenant le rendu du teaser : N&B + vignette (CSS sur #stage.bw), titre « DANS CETTE VIDÉO… » qui glisse (#titleOv, même formule que teaser.py, piloté par la position de lecture), et musique suspense (<audio id="music"> servie par GET /api/intro/music, jouée pendant la lecture de l'intro). Pistes visibles sous la timeline (#lanes) : Titre, Musique, Structure (noir + clips + noir). Réglages modifiables (panneau intro) : ouverture/fermeture noire, volume musique, coupe des silences → sauvés dans work/intro.json (intro_black, outro_black, music_volume, gap_max) et appliqués à l'export. teaser.py étendu (rétro-compatible) pour lire ces clés. Titre verrouillé (règle Matt). Demandé par Matt.
  • - 2026-09-23 (c) : aperçu des clips d'intro. Bouton ▶ sur chaque clip proposé (candidat) et sur le clip d'intro sélectionné (#pvSel) : joue juste [start,end] avec l'audio master (cam1), sans l'ajouter. Moteur previewClip/stopPreview (état preview, géré dans tick, stoppé par play/pause/scrub). Garde-fou setOut pour sélection vide.
  • - 2026-09-23 (b) : atelier du plan sous la timeline. En sélectionnant un plan (Montage ou Intro), une bande zoomée (#detail/#dstrip, zoom auto = plan + marge) apparaît en dessous : tirer les bords (poignées .dh) pour agrandir/réduire précisément, et outil coupe (#cutTool) pour retirer un morceau au milieu (glisser sur la zone → le plan est coupé en deux, avant + après, keep_head sur le 1er, keep_tail sur le 2e). Fonctionne aussi sur les clips d'intro. JS : openDetail/drawDetail/dragEdge + handler cut sur #dstrip, hideDetail() à la désélection. Demandé par Matt.
  • - 2026-09-23 : ajout de l'onglet Intro. À chaque montage, une dizaine de clips candidats sont pré-sélectionnés dans work/intro_candidates.json (chacun {id,cam,start,end,text,keep_tail?}, temps master) ; dans l'onglet Intro, Matt choisit lesquels garder et les range dans l'ordre voulu (réordon., cam, trim, keep_tail). La sélection est sauvée dans work/intro.json (format teaser.py) et sert à (re)construire le teaser à l'export (teaser.py). Nouveaux endpoints GET/POST /api/intro. Le teaser d'export vient désormais de la sélection éditée (avant : TEASER.mp4 pré-généré figé). Demandé par Matt.
  • - 2026-09-22 (b) : fix bloquant $('#off+') (sélecteur CSS invalide → tout le JS plantait, bandeau proxys figé). IDs synchro renommés offdec/offreset/offinc. Ajout capteur d'erreur global + ligne de diagnostic dans le bandeau + anti-cache (Cache-Control: no-store) sur /. Boucle de chargement rendue robuste (retry sur erreur fetch). Preview : rendu d'une image forcé au loadeddata (sinon lecteur noir), player 16:9 centré (align-self:center + hauteur explicite).
  • - 2026-09-22 : création. Éditeur MVP (timeline, preview proxys, cuts/trim/split/delete, nudge synchro, export + teaser optionnel). Exposé sur montage.panelbay.com. Demandé par Matt pour faire ses 2-3 cuts lui-même sur le podcast Matt+Seb.