Produit · 2024
Captiv
Sous-titrer, personnaliser et exporter une vidéo
Captiv — un moteur de sous-titrage vidéo piloté par HTML et CSS
Transcription au mot près · 2 traitements séparés · capacité de calcul de 0 à 64 vCPU · ≈ 1 minute de traitement par minute de vidéo
Résumé
- Problème
- Les sous-titres incrustés font partie du montage des formats courts, mais les générateurs automatiques laissaient peu de contrôle graphique. Il fallait retrouver dans la vidéo finale le style composé dans le navigateur.
- Statut
- Produit, 2024.
- Rôle
- Conception produit, architecture et implémentation.
- Solution
- Transcription Whisper au mot près, style décrit comme une donnée, rendu HTML/CSS par Chromium image par image, puis deux traitements AWS Batch séparés.
- Résultat
- Chaîne complète, de l’import au MP4. Sur les formats courts, environ une minute de traitement par minute de vidéo, ordre de grandeur observé pendant le développement.
- Limite
- Aucun mécanisme ne réconcilie l’état de l’application avec celui des traitements si un callback se perd. La capacité configurée, jusqu’à 64 vCPU, n’a pas été démontrée à grande échelle.
Usage et démonstration
La démonstration
Le produit
En 2024, les sous-titres incrustés sont devenus systématiques sur les formats courts, Shorts, Reels et TikTok. Ils ne servent plus seulement à rendre une vidéo lisible sans le son, ils font partie du montage.
Les générateurs automatiques disponibles produisaient des rendus très reconnaissables : quelques styles, une typographie imposée, peu de contrôle sur le résultat. Captiv visait le même automatisme avec une vraie latitude graphique.
L'utilisateur importe une vidéo. Le service la transcrit, la découpe en segments synchronisés, puis ouvre un éditeur où le rendu se travaille visuellement : police, taille, graisse, casse, couleur, contour, hauteur de ligne, position, ombres multiples, effets de diffusion, styles prédéfinis et animations. Le résultat est ensuite réencodé dans la vidéo.
Le produit se rapproche d'un petit logiciel de motion design spécialisé, avec le parcours d'un SaaS : importer, ajuster, exporter.
L'éditeur
L'écran principal réunit le lecteur, le transcript et les réglages graphiques.
Le transcript se restructure en séparant ou en fusionnant des segments, et se reformate à partir de la ponctuation, des intervalles entre les mots et de la longueur du texte.
Le panneau de style agit directement sur l'aperçu. Les ombres sont cumulables avec des niveaux de flou différents, ce qui permet de construire une diffusion lumineuse ou un néon sans effet dédié. Les animations d'apparition, les variations sur le mot en cours de lecture et les styles réutilisables d'un projet à l'autre relèvent du même modèle : chaque réglage doit pouvoir traverser toute la chaîne jusqu'au fichier final.
Décisions techniques
Le problème
L’utilisateur compose l’apparence des sous-titres dans le navigateur. L’export doit reproduire cette composition dans le fichier vidéo.
Il faut faire coïncider les mots produits par la transcription, leurs timestamps, les segments affichés, les propriétés graphiques choisies, l'aperçu à l'écran, les images produites au rendu et la timeline finale. Une modification anodine dans l'interface, un contour épaissi ou un texte déplacé, doit rester valable sur une vidéo dont la résolution n'a rien à voir avec celle de l'aperçu.
Le style est décrit par des données structurées, indépendantes de l’écran de composition et réutilisées pour l’export.
Transcription
À l'import, la vidéo part dans S3 et un premier traitement extrait l'audio, produit les fichiers nécessaires au projet et transcrit la bande son.
Une première version s'appuyait sur l'API de transcription d'OpenAI. La version finale exécute Whisper large-v3-turbo dans le conteneur de préparation, avec stable-ts. Le traitement gagne le prétraitement de stable-ts, notamment la détection d'activité vocale et la séparation des sources, qui affinent le calage des mots ; le coût cesse de dépendre d'une facturation à la minute transcrite ; et l'audio ne sort plus de l'infrastructure du service.
La transcription conserve les timestamps au niveau du mot, puis plusieurs règles de segmentation s'appliquent : ponctuation, silences à partir de 0,3 seconde, longueur maximale d'environ 30 caractères par groupe.
La transcription associe chaque mot à ses timestamps :
Bonjour 00:00.00 → 00:00.54aujourd'hui 00:00.55 → 00:01.12nous 00:01.14 → 00:01.35allons 00:01.36 → 00:01.71
Cette granularité détermine ce que le produit peut faire ensuite : segmentation fine, navigation précise dans le transcript, et effets calés sur le mot prononcé plutôt que sur la phrase.
Le style comme donnée
Dans l’éditeur, le style d’un sous-titre est décrit par un ensemble de propriétés.
{ "color": "#ffffff", "size": 10, "weight": 600, "fontFamily": "Roboto", "outline": { "color": "#000000", "width": 0.5 }, "shadows": [ { "color": "#ff0099", "offsetX": 0, "offsetY": 0, "blur": 4.2 } ], "topPercent": 66, "leftPercent": 50}
L'interface construit du CSS à partir de ces paramètres et affiche le résultat par-dessus la vidéo. Le moteur d'export reprend les mêmes paramètres sans dépendre du navigateur de l'utilisateur. Six styles prêts à l'emploi sont fournis, dont Comic book, Neon, Newspaper et SynthWave ; ce ne sont pas des templates mais des jeux de valeurs sur les mêmes propriétés, que l'utilisateur peut ensuite modifier.
Les dimensions sont exprimées relativement à la largeur de la vidéo plutôt qu'en pixels. Une taille ou un contour n'est donc pas attaché aux quelques centaines de pixels de l'aperçu : la conversion en pixels réels se fait au moment du rendu, avec la même règle des deux côtés, en TypeScript pour la prévisualisation et en Python pour l'export.
HTML, CSS et Chromium
Produire ces rendus dans une vidéo pouvait passer par Pillow, OpenCV ou les primitives de ffmpeg. Il aurait alors fallu réimplémenter le chargement des polices, les contours, les ombres multiples, le flou, le positionnement, puis les animations. Un navigateur fait tout cela.
Captiv utilise donc HTML et CSS comme langage de composition, et Chromium comme moteur de rendu. Pour chaque portion de sous-titre, le renderer récupère le texte et les paramètres de style, adapte les valeurs à la largeur réelle de la vidéo, injecte le tout dans un template HTML, crée une page aux dimensions nécessaires sur fond transparent, la charge dans Chromium headless via pyppeteer et capture le rendu en PNG avec canal alpha.
Le frontend et le renderer ne partagent pas de code : l'un est en TypeScript et React, l'autre en Python. Ils partagent les mêmes propriétés et les mêmes règles de conversion, ce qui fait du style composé dans l'éditeur la spécification du rendu final.
Style structuré (JSON)
HTML/CSS → PNG transparent, frame par frame
Overlay sur la vidéo source
H.264 / AAC → MP4
Le contrôle des animations
Une animation CSS a un état déterminé pour un instant donné. Il n'est donc pas nécessaire de la lancer puis d'espérer capturer l'écran au bon moment : le renderer prend le contrôle de sa timeline.
Pour une vidéo à n images par seconde, il calcule l'instant correspondant à chaque frame, positionne l'animation à cet instant en agissant sur son délai et son état de lecture, capture le rendu transparent, puis passe à la suivante.
Le rendu ne dépend alors ni de la vitesse de la machine ni d'un setTimeout, et une même animation produit la même séquence d'images à chaque export.
La composition finale
Chaque séquence d'images devient un clip MoviePy, placé au timestamp correspondant et superposé à la vidéo originale, qui reste le premier calque et conserve son audio. MoviePy orchestre la composition, ffmpeg produit le fichier final en H.264 et AAC.
Sur les formats courts visés, une minute de vidéo demandait environ une minute de traitement complet. C'est un ordre de grandeur observé pendant le développement, pas une mesure de performance.
Deux traitements séparés
La transcription et le rendu vidéo n'ont pas les mêmes besoins. Le premier charge Whisper et traite de l'audio, le second pilote Chromium, génère des images et encode une vidéo. Ils tournent donc dans deux conteneurs et deux environnements AWS Batch distincts : 4 vCPU et 7 500 Mio de mémoire pour la préparation, 8 vCPU et 15 500 Mio pour l'export.
Les deux environnements descendent à zéro machine quand aucun traitement n'est demandé et montent jusqu'à 64 vCPU. Cela dimensionne chaque tâche selon son besoin réel plutôt que de maintenir en permanence un serveur vidéo capable d'absorber les pointes, et cela colle au modèle économique du produit : sans export en cours, aucune machine de rendu ne tourne.
L'orchestration
L'application est construite avec Next.js et gère l'interface, l'authentification, les projets, MongoDB et la soumission des traitements. Les fichiers lourds restent dans S3, servis aux utilisateurs authentifiés par des URLs présignées d'une heure, sans distribuer de credentials AWS au navigateur.
Les jobs sont asynchrones. Next.js soumet une tâche à AWS Batch et rend la main immédiatement ; les conteneurs signalent leur progression et leur résultat par callback HTTP, tandis que l'interface interroge l'état toutes les dix secondes. L'utilisateur peut donc quitter le projet et y revenir sans interrompre le traitement.
L'infrastructure en code
L'infrastructure est gérée comme du code (infrastructure as code, IaC) avec Pulumi, qui provisionne le réseau, les registres ECR, les rôles IAM, les environnements de calcul Batch, les queues, les définitions de jobs et les permissions S3.
Le nombre de briques nécessaires au seul pipeline de traitement rendait la reproductibilité plus importante que la rapidité de mise en place initiale : reconstruire l'ensemble depuis le code plutôt que depuis une suite de manipulations manuelles.
Stack
Limites
Les limites de cette orchestration
L'état applicatif dépend de la bonne réception du callback envoyé par le conteneur. Si un job est tué brutalement, si le callback échoue ou si l'application est momentanément indisponible, rien ne réconcilie MongoDB avec l'état réel de Batch : un projet peut alors rester affiché comme requested ou processing alors que son traitement n'aboutira pas.
Il reste à fiabiliser la livraison des callbacks et à réconcilier l’état de l’application avec celui des traitements. Ce que j'ajouterais aujourd'hui tient en trois mécanismes : une réconciliation périodique interrogeant Batch sur les jobs que l'application croit actifs, un délai d'expiration au delà duquel un traitement est considéré comme perdu, et des callbacks idempotents rejouables sans effet de bord.