Le montage d'une vidéo face caméra, c'est surtout du travail de nettoyage. On place le curseur, on écoute, on coupe le silence, on recommence. Puis on retire les « euh », puis la phrase qu'on a dû dire trois fois. Rien de créatif, et pourtant c'est ce qui prend le plus de temps.
AutoLad est né de cette frustration : une app desktop qui fait ce nettoyage toute seule, sans rien envoyer dans le cloud. Le rush reste sur la machine, la transcription tourne sur la carte graphique, et le résultat s'ouvre dans une timeline classique où l'on peut tout reprendre à la main. Et comme je voulais aussi pouvoir dire à Claude « enlève les hésitations et exporte en brouillon », l'app est également un serveur MCP qu'un agent peut piloter pendant qu'on le regarde faire.
36
commits
~11,5k
lignes de Rust
161
tests
21
outils MCP
Le principe
Tout le logiciel tourne autour d'un objet simple : l'EDL (Edit Decision List). C'est la liste ordonnée des morceaux de rushes qu'on garde. Couper un silence, retirer un mot, déplacer un plan : tout revient à modifier cette liste. Le rendu final ne fait que la traduire en commande ffmpeg.
Le flux d'auto-montage tient en six étapes :
- Import : ffprobe lit le fichier, un hash l'identifie et sert de clé de cache.
- Préparation en tâche de fond : proxy 540p pour la lecture, forme d'onde, bande de vignettes.
- Analyse : détection des silences par ffmpeg, transcription par Whisper.
- Construction de l'EDL à partir des analyses et des réglages.
- Édition sur la timeline, avec preview sur les proxys.
- Export : l'EDL devient un graphe de filtres ffmpeg, rendu sur les sources originales.
Tout ce qui est calculé est mis en cache par hash : rouvrir un projet ne relance jamais une transcription déjà faite.
L'architecture
Le projet est un workspace Rust découpé en crates, avec une règle stricte : le domaine ne dépend de rien. core contient les types et les algorithmes purs (silences → segments, transcript, sous-titres, historique…) et ne connaît ni Tauri, ni ffmpeg, ni Whisper. Il déclare seulement des traits aux frontières d'entrée/sortie, que media et transcribe implémentent.
core
Aucune dépendance infra. Types (Project, Asset, Edl, Cut), algos purs (silences → segments, transcript, sous-titres, FCPXML, historique) et traits aux frontières I/O uniquement.
utilisé par : mcp, media, transcribe
L'autre décision structurante, c'est la crate mcp. Au départ, elle ne devait servir qu'aux agents. Elle est finalement devenue le moteur de toute l'app : l'interface desktop appelle le même Engine que l'agent. Résultat, une seule logique, testée une seule fois, et aucune fonctionnalité qui existe dans l'UI sans exister pour l'agent (ou l'inverse).
Côté front, aucun type n'est écrit à la main : les bindings TypeScript sont générés depuis Rust par tauri-specta, et un test échoue s'ils sont périmés.
Couper les silences
C'est la fonction de base, et elle est plus subtile qu'elle n'en a l'air. ffmpeg (silencedetect) donne des plages où le son reste sous un seuil. Les inverser donne la parole à garder, mais un découpage brut produit un montage haché : des micro-coupes au milieu des phrases, des débuts de mots mangés.
D'où quatre étapes, chacune avec son réglage :
pub fn build_silence_cut_edl(
asset: &Asset,
silences: &[TimeRange],
settings: &SilenceSettings,
) -> Result<Edl, CoreError> {
settings.validate()?;
let speech = silences_to_segments(silences, asset.duration);
let speech = merge_close(&speech, settings.max_gap);
let speech = apply_margins(&speech, settings.margin, asset.duration);
let speech = drop_short(&speech, settings.min_segment);
Ok(Edl { cuts: speech.into_iter().map(|range| Cut { asset: asset.id.clone(), range }).collect() })
}Le plus parlant, c'est de jouer avec. Le schéma ci-dessous applique exactement ces quatre fonctions, réécrites en TypeScript, sur un faux clip de 20 secondes :
Montage
20.0 s
Source
14.4 s
Montage
−28 %
Gagné
5
Cuts
Le toc sur le bureau est gardé. Il ne dure que 80 ms, mais les marges passent avant le filtre de durée et le font grossir au-dessus de min_segment.
Deux choses à remarquer. Le souffle vers 7,5 s est au-dessus du seuil de -30 dB : il n'est pas considéré comme un silence, ce qui est voulu (couper une respiration au milieu d'une idée sonne faux). Et le toc sur le bureau à 14 s survit au montage avec les réglages par défaut. C'est un vrai défaut, repéré en préparant cet article : les marges sont appliquées avant le filtre de durée, donc un bruit de 80 ms devient un segment de 280 ms. Il est dans la roadmap en fin d'article.
Monter par le texte
Les silences ne suffisent pas. Le vrai gain vient de la transcription : une fois qu'on sait quel mot est dit à quel moment, on peut monter en éditant du texte. AutoLad propose trois outils :
cut_text: on écrit la phrase à retirer, elle disparaît du montage. La recherche ignore la casse et la ponctuation, et tolère qu'un mot sur cinq manque au transcript (sauf le premier et le dernier, pour ne pas couper au mauvais endroit).remove_fillers: retire les sons d'hésitation. La liste est fermée (« euh », « hum », « bah »…) : jamais de vrais mots comme « ben » ou « donc », qui portent parfois du sens.remove_retakes: détecte les phrases recommencées. Si une phrase commence comme la suivante, ou la répète presque entièrement (plus longue sous-séquence commune ≥ 80 %), c'est une prise ratée : on garde la dernière. Une interjection courte entre les deux (« Non, pardon. ») part avec.
Bonjour à tous, euh, aujourd'hui on parle de montage. On va voir comment on peut… Non, pardon. On va voir comment couper les silences tout seul. Hum, c'est parti !
Ce que dit le montage
Bonjour à tous, euh, aujourd'hui on parle de montage. On va voir comment on peut… Non, pardon. On va voir comment couper les silences tout seul. Hum, c'est parti !
parole 9.6 s → 9.6 s · clip 13.5 s · 0 mot retiré
Toutes ces opérations produisent des retraits de plages sources, appliqués en un seul changement annulable. Un Ctrl+Z annule « remove_retakes » d'un coup, pas mot par mot.
Le piège des timestamps par mot
whisper.cpp peut donner des timestamps par mot, très précis. Mais en mode mot, il perd parfois des mots : j'ai vu « apprendre » disparaître purement et simplement d'une phrase. La solution retenue : deux passes. Le mode phrase donne le texte fiable, affiché dans l'onglet Transcription. Le mode mot sert uniquement à placer les coupes, et n'est calculé qu'à la première demande, puis stocké dans le projet.
Un agent dans la timeline
C'est la partie la plus amusante du projet. Le même autolad.exe, lancé avec --mcp, devient un serveur MCP en stdio. Claude Desktop ou Claude Code peuvent alors importer un rush, transcrire, couper, prévisualiser une image et lancer un rendu.
Mais un agent qui travaille dans un process invisible, c'est frustrant : on ne voit rien. Alors quand l'app est ouverte, le process --mcp se branche dessus au lieu de travailler seul. On voit un curseur « Claude » se déplacer dans l'interface et cliquer là où il agit.
spawn (stdio)Claude lance le même exécutable avec --mcp. Pas de fenêtre : sur stdout ne passe que du JSON-RPC, jamais un println!.
Le détail qui compte : chaque action de l'agent passe par le même historique que celles de l'utilisateur. Si Claude coupe un passage qu'on voulait garder, Ctrl+Z l'annule. Et inversement, l'agent dispose d'outils undo/redo qui annulent aussi les actions de l'utilisateur.
Les difficultés
Des chemins trop longs pour Windows
whisper.cpp compilé avec le backend Vulkan imbrique des dossiers CMake sur plusieurs niveaux, et le FileTracker de MSBuild ne gère pas les chemins de plus de 260 caractères (FTK1011). La solution est peu élégante mais efficace : le dossier de build Cargo est déplacé à la racine, en C:/t. La marge reste faible, et c'est noté comme tel dans la doc du projet.
Un callback mal casté dans whisper-rs
Pour annuler une transcription en cours, whisper.cpp interroge régulièrement un callback d'abandon. Dans whisper-rs 0.16, avec une simple closure, ce callback lisait n'importe quoi et whisper_full échouait avec le code -6. En cause : un cast de pointeur vers le mauvais type dans la lib. Le contournement consiste à lui passer directement le type qu'il caste :
// whisper-rs 0.16.0 casts the callback pointer to the closure's concrete type, but
// stores it as a boxed trait object: with a bare closure the abort callback reads
// garbage and whisper_full fails with -6. Passing the box itself as `F` makes the
// cast match. Revisit when whisper-rs fixes its trampoline.
let abort: Box<dyn FnMut() -> bool> = Box::new(move || cancel.load(Ordering::Relaxed));
params.set_abort_callback_safe::<_, Box<dyn FnMut() -> bool>>(abort);Toutes les formes de vidéo
Une vidéo de smartphone est souvent stockée en paysage avec une métadonnée de rotation. Certaines caméras ont des pixels non carrés (anamorphiques). D'autres sortent des dimensions impaires, que l'encodeur H.264 refuse. AutoLad raisonne donc toujours en taille affichée : rotation et ratio de pixel appliqués dès l'analyse, mise à l'échelle en pixels carrés et côtés arrondis au pair partout (proxy, preview, rendu). Un test unique fait passer huit cas par toute la chaîne (paysage, portrait, pivoté, anamorphique, minuscule, impair, ultra-large, UHD).
Des fichiers de 8 Go
Le rendu est encodé en qualité constante : l'encodeur dépense ce qu'il faut pour garder une qualité stable. Sur des rushes 4K à 60 i/s avec du grain ou du feuillage, ça montait à ~100 Mb/s, soit 8 Go pour dix minutes. Un plafond fixe ne marchait pas : soit il étouffait la 4K, soit il laissait la 1080p granuleuse prendre trois fois ce dont elle avait besoin. Le plafond suit donc le nombre de pixels par seconde :
0.2 × 1920 × 1080 × 30 = 12.4 Mb/s
Mesuré sur une 1080p30 granuleuse : passer de 40 à 12 Mb/s divise le fichier par plus de 3 pour moins d'un demi-point de VMAF. J'ai aussi testé les options « qualité » de NVENC (lookahead, AQ spatial et temporel) : fichiers 40 % plus lourds, sans gain mesurable. Écartées.
Des sous-titres et des backslashs
L'incrustation des sous-titres passe par le filtre subtitles de ffmpeg, qui reçoit un chemin de fichier… à l'intérieur d'une chaîne de filtres, avec ses propres règles d'échappement. Avec un chemin Windows (C:\Users\...), c'est un cauchemar. Plutôt que d'échapper, ffmpeg est lancé depuis le dossier temporaire où se trouve le .srt, et le filtre ne reçoit qu'un nom de fichier ASCII. Le problème n'a plus lieu d'être.
Les points d'attention
Quelques règles du projet qui ne se voient pas mais évitent beaucoup de bugs :
- Sur stdout en mode
--mcp, uniquement du JSON-RPC. Un seulprintln!de debug oublié suffit à corrompre le flux, et l'erreur côté client est rarement explicite. - Annuler = dropper le future. Les process ffmpeg sont lancés avec
kill_on_drop: annuler un rendu tue réellement le process, au lieu de le laisser tourner en arrière-plan. - Écritures atomiques partout. Projet, cache, config de Claude : on écrit dans un
.part, puis on renomme. Un crash au milieu d'une écriture ne laisse jamais un fichier à moitié écrit. - Jamais de calcul lourd dans la webview. Formes d'onde et vignettes sont calculées en Rust et transmises en binaire, pas en gros JSON.
- L'historique est en mémoire. 200 étapes d'undo, mais remises à zéro à la fermeture. Le projet, lui, est sauvegardé automatiquement.
Les optimisations à venir
Le moteur couvre aujourd'hui bien son premier cas d'usage, la face cam. Voici ce qui reste sur la liste, avec une estimation honnête de l'effort :
S = quelques heures · M = quelques jours · L = un vrai chantier
Les deux chantiers qui m'intéressent le plus sont les hésitations non transcrites et l'alignement forcé des mots. Les deux visent la même limite : AutoLad est aussi bon que la transcription sur laquelle il s'appuie. Améliorer ce que l'on sait de quel son est produit à quel moment, c'est améliorer toutes les fonctions de montage d'un coup.
Si vous montez des vidéos face caméra, si vous voulez tester AutoLad ou simplement en discuter, écrivez-moi.