J’étais plongé jusqu’au cou dans un pipeline d’avatars et il me fallait deux trucs sans gloire en même temps: une passe de lipsync, et une poignée d’opérations ffmpeg autour, couper la source, muxer l’audio, transcoder la sortie, sortir une planche de vignettes. Chaque “solution” que je trouvais était un wrapper SaaS par-dessus un modèle que je ne pouvais pas inspecter, facturé à la seconde de sortie, planqué derrière une clé d’API, avec un watermark cuit dans le coin, parce qu’il ne faudrait surtout pas que je paie 40 dollars par mois et que je possède quand même mon propre rendu. L’un d’eux voulait la vidéo de mon visage ET mon audio uploadés sur ses serveurs avant même de me donner un prix. Un autre avait un palier “licence commerciale” qui coûtait plus cher que ma carte graphique. Je suis resté là à me dire: j’ai une 3060 qui dort dans un boîtier et qui ne fout rien après 18h, ffmpeg existe depuis que le monde est monde, et Wav2Lip est open source depuis 2020. Pourquoi diable est-ce que je loue ça à un inconnu.
Alors je ne l’ai pas fait. flickies, c’est la moitié vidéo de la boîte à outils auto-hébergée que je construis depuis un moment, docker run, tu le pointes sur un visage et une piste audio, tu récupères un mp4. Pas de compte, pas de compteur à la seconde, pas de watermark, pas de merde à moi uploadée sur la ferme d’inférence de quelqu’un d’autre. C’est le frère d’audiolla (audio) et de talkies (parole), même modèle de jobs asynchrones, même histoire de bind mount sur /data, même barrière non-commerciale en opt-in, même attitude “un port, zéro cloud”. Il se branche directement dans aigate comme moteur vidéo, derrière la même porte d’entrée nginx que tout le reste de ce que je fais tourner.
Relouer Ton Propre Visage
Le lipsync en SaaS est une arnaque d’un genre particulier, et j’ai une liste:
- Tu uploades un visage humain sur le GPU d’un inconnu. Pas une photo de chat. Un visage, en train de raconter ce que tu as foutu dans l’audio. Ces données ne s’évaporent pas une fois le rendu fini, elles restent sur le disque de quelqu’un d’autre, avec la politique de rétention de quelqu’un d’autre.
- La facturation à la seconde de sortie transforme l’expérimentation en exercice de comptabilité. Tu veux itérer sur dix prises pour caler le sync? Félicitations, tu viens de payer dix fois.
- Watermarks et murs de palier, le palier “gratuit” te colle un logo sur la sortie, et le palier payant qui l’enlève coûte plus cher que le matériel sur lequel tu ferais tourner ça chez toi en un week-end.
- Personne ne te raconte l’histoire de la licence. Un nombre choquant de ces wrappers SaaS sont posés sur Wav2Lip, qui est entraîné sur LRS2, un jeu de données avec une clause non-commerciale explicite. Le SaaS te prend de l’argent pour faire tourner un modèle non-commercial et, tout simplement… ne mentionne pas ce détail. Ce n’est pas à moi de résoudre ça pour toi si tu t’auto-héberges, mais au moins je te fais activement basculer un interrupteur pour le reconnaître, au lieu de le planquer dans des CGU que personne ne lit.
- Zéro introspection. Tu as droit à un endpoint REST boîte noire et à un “fais-nous confiance”, aucune idée de quelle variante de modèle a tourné, avec quels réglages, si ta passe de restauration de visage est enchaînée ou pas.
Et du côté ffmpeg, couper, transcoder, coller de l’audio sur une vidéo, sortir une grille de vignettes, chaque “API de traitement vidéo” que j’ai trouvée pour ça était d’une façon ou d’une autre AUSSI un produit payant, pour des opérations qui tiennent en une invocation de ffmpeg avec les bons flags. J’avais déjà réglé le traitement de fichiers sur le réseau avec mediaproc; flickies fait le même truc de “appelle un vrai outil, arrête de le réinventer”, mais cadré spécifiquement sur la vidéo et câblé aussi pour la moitié ML.
La Spec d’Abord, le Container Ensuite
flickies est un service FastAPI qui parle un seul format de fil pour deux types de travail très différents: des opérations ffmpeg pures sur CPU, et de l’inférence de modèle sur GPU pour le lipsync et la restauration de visage. Chaque endpoint qui produit une vidéo prend la même forme de requête: exactement une entrée (file_path posé en local, ou file_url que le serveur va chercher pour toi) et exactement une sortie (output_path écrit sous FILES_DIR, ou output_url sur lequel le serveur PUT le résultat, URL S3 présignée, ce que tu veux). Tu mélanges comme tu veux. Tu poses un fichier en local, tu récupères une URL présignée. Tu vas chercher depuis une URL, tu écris sur le disque local. Il s’en fout.
Le côté ML passe par un registre qui gère un seul pool de GPU avec éviction à chaud: tu demandes wav2lip, il se charge. Tu demandes gfpgan ensuite, le registre évince d’abord wav2lip (del sur les refs, gc.collect(), puis torch.cuda.empty_cache(), dans cet ordre exact, parce que les graphes de modèles PyTorch tiennent des cycles de références et que sauter l’étape gc.collect() fait que “décharger” un modèle ne libère pas vraiment la VRAM, un bug que j’ai livré en v0.1.0/v0.2.0 et corrigé pour de bon en v0.3.1). Un balayeur en tâche de fond décharge aussi ce qui est résident dès que ça traîne inactif plus longtemps que FLICKIES_IDLE_UNLOAD_SECS (600s par défaut). Un seul modèle vit en VRAM à la fois; c’est tout le design.
Tout ce qui est en aval de ça, les routes, les formes de requête et de réponse, les codes d’erreur, vient d’un seul fichier: openapi.yaml. Ce n’est pas de la documentation écrite après coup, c’est l’entrée réelle du générateur pour trois choses distinctes: les modèles de validation Pydantic du serveur, le client Go et le client Python. Tu changes la spec, tu lances make generate, les trois se régénèrent ensemble. make generate-check est une barrière de CI qui fait échouer le build si l’un d’eux dérive de la spec. J’explique plus bas pourquoi ça compte, parce que c’est la partie de ce projet dont je me vante le plus.
Démarrage Rapide
docker run -d --name flickies
-v $HOME/flickies-data:/data
-p 8000:8000
psyb0t/flickies:latest
curl -s -X POST https://ciprian.51k.eu00/v1/video/info
-H "Content-Type: application/json"
-d '{"file_path": "uploads/clip.mp4"}' | jqDeux images: psyb0t/flickies:latest (CPU, base python:3.12-slim) et psyb0t/flickies:latest-cuda (base nvidia/cuda 12.4 runtime). L’image CPU fait tourner toutes les opérations ffmpeg plus Wav2Lip sur CPU, lentement, mais pour de vrai; l’image CUDA fait tourner le tout à une vitesse que tu supporterais réellement. La cible testée est une RTX 3060 12GB.
Quatre Moteurs ML: Lipsync et Restauration de Visage
engines.json définit exactement quatre moteurs ML, chacun avec un slug, un drapeau d’exigence CUDA, un plancher de VRAM et, là où ça compte, une barrière de licence:
wav2lip / wav2lip-gan
Rudrabha/Wav2Lip, embarqué dans le repo, résolution native 96×96. Deux variantes qui partagent une même classe de moteur, commutées par un champ variant: base (précision de sync maximale, bouche plus molle) et gan (raffineur GAN, bouche plus nette, sync très légèrement moins bon). Les deux choisissent leur périphérique toutes seules, FLICKIES_DEVICE=auto teste torch.cuda.is_available() et retombe proprement sur CPU. Sur le benchmark de la première release, ça donne ~44 secondes pour un clip de 3 secondes sur CPU, ~22 secondes sur GPU. Ce chiffre CPU n’est pas une blague, c’est vraiment utilisable pour des clips courts, ce que je ne peux pas dire de la plupart des repos open source de lipsync “GPU obligatoire” qui se contentent de planter sur une machine sans carte au lieu de se dégrader élégamment.
latentsync-1.5
LatentSync 1.5 de ByteDance, Apache-2.0, épinglé spécifiquement sur le checkpoint 1.5 parce que le 1.6 veut 18GB de VRAM et que mon plafond matériel est à 12. Colonne vertébrale en espace latent SD-1.5, embeddings audio Whisper-tiny en cross-attention dans un UNet3D via AnimateDiff, de la machinerie plus lourde que Wav2Lip, et ça se voit: ~170 secondes pour un clip de 6 secondes sur la 3060, avec un pic autour de 9.6GB de VRAM. C’est le seul moteur de tout le lot qui exige impérativement CUDA, le code teste torch.cuda.is_available() au chargement et lève un 400 si ce n’est pas là, sans tenter le moindre repli CPU. C’est aussi le moteur par défaut quand la barrière non-commerciale n’est pas levée, parce que contrairement à Wav2Lip il ne traîne aucun bagage LRS2.
gfpgan
GFPGAN v1.4 de TencentARC, Apache-2.0. Celui-là s’enchaîne après Wav2Lip pour réparer le crop de bouche mou et basse résolution que laisse derrière elle l’inférence native en 96×96, Wav2Lip cale le sync, GFPGAN nettoie le désastre visuel autour. Il marche aussi tout seul via POST /v1/video/restore si tu veux juste une passe de restauration de visage sur des images existantes. Même sélection automatique de périphérique que Wav2Lip, repli sur CPU. Image par image: lecture via cv2, restaurateur sur chaque image, écriture dans un mp4 muet, puis remuxage de l’audio d’origine par-dessus les images restaurées.
Les poids des quatre vivent dans la structure de cache HuggingFace standard, sous /data/hf/hub/models--<org>--<name>/, blobs adressés par contenu, symlinks de snapshot, réutilisables par tout ce qui connaît HF et partage ce bind mount. Paresseux par défaut: chaque moteur va chercher son repo à la première requête. FLICKIES_PREFETCH_ALL=1 ou un FLICKIES_ENABLED_ENGINES=wav2lip,gfpgan restreint tire les poids au boot, avant même qu’uvicorn démarre, pour que ta première vraie requête n’avale pas plusieurs minutes de téléchargement à froid.
La barrière de licence n’est pas décorative
Les poids de Wav2Lip sont entraînés sur LRS2, un jeu de données non-commercial. flickies n’enterre pas ça dans un README que personne ne lit, le code du serveur refuse physiquement de charger l’une ou l’autre variante de wav2lip tant que FLICKIES_ENABLE_NONCOMMERCIAL=1 n’est pas posé dans l’environnement du serveur. Essaie d’acquérir le moteur sans ça et require_noncommercial_optin() lève un NonCommercialOptInRequired, que l’API fait remonter comme une vraie erreur au lieu d’un 500 silencieux. LatentSync 1.5 et GFPGAN sont tous deux en Apache-2.0, pas de barrière, chargement libre. Même mécanisme que celui qu’audiolla utilise pour ses propres barrières non-commerciales sur MusicGen et matchering, j’ai volé le motif à moi-même, ce que j’ai le droit de faire.
Sept Opérations ffmpeg, Parce Que Tout N’a Pas Besoin d’un GPU
La moitié de ce dont les gens ont réellement besoin dans une “API vidéo” n’a rien à voir avec le ML, c’est ffmpeg avec des défauts sensés et de la gestion d’erreurs. flickies expose sept opérations purement ffmpeg, aucun modèle chargé, CPU pur, disponibles dans les deux images:
- trim, coupe à
[start_sec, end_sec]. Le mode par défaut est la copie de flux-c copy(rapide, mais ça s’aligne sur la keyframe la plus proche, ça peut bouffer jusqu’à un GOP de contenu au début). Poseprecise: trueet il réencode vialibx264 -crf 18 -preset veryfastplus AAC 192k, pour des bornes exactes à l’image près. - concat, colle 2 vidéos ou plus dans l’ordre via le demuxer concat. Même compromis copie-de-flux contre précis que trim;
precise: trueréencode à travers le demuxer avec des paramètres de codec uniformes, pour que des entrées aux encodeurs dépareillés se collent vraiment au lieu de se corrompre. - transcode, réencodage universel entre mp4/webm/mov/mkv, avec surcharges de codec, crf, preset et fps. Gère aussi la sortie gif comme un chemin spécial en deux passes:
palettegenpuispaletteuseà travers un filter_complex, parce qu’une conversion naïve d’ffmpeg vers gif ressemble à de la merde et tout le monde le sait. - scale, redimensionne en largeur×hauteur, avec un pad optionnel qui préserve le ratio.
- mux_audio, remplace ou fusionne une piste audio dans une vidéo.
- extract_audio, sort la piste audio en wav/mp3/m4a/ogg/flac.
- thumbnail_grid, planche de sprites PNG via les filtres
thumbnailettile, lignes, colonnes et taille de cellule toutes configurables.
Il y a une huitième capacité vidéo qui n’est pas dans cette liste d'”opérations” parce qu’elle ne produit pas de vidéo, /v1/video/info appelle ffprobe et te rend durée, codec, fps, dimensions, bitrate. Absolument chacune d’entre elles passe par un unique goulot dans ffmpeg.py qui lance le processus via asyncio.create_subprocess_exec, capture stderr, et lève une erreur structurée FFMPEG_FAILED en cas de code de sortie non nul, au lieu de laisser fuiter une trace brute.
Jobs Asynchrones et Webhooks, Pour Quand Tu Ne Vas Pas Rester Planté à Attendre
LatentSync à plus de 170 secondes par clip, ce n’est pas le genre de chose pour laquelle tu veux garder une connexion HTTP ouverte. Chaque endpoint qui produit une vidéo accepte async_job: true (ou tu omets simplement les deux champs de sortie et c’est implicite): le serveur préalloue un id de job, planifie le travail comme tâche asyncio de fond, et renvoie immédiatement 202 {job_id, status: "accepted"}. Tu interroges GET /v1/jobs/{job_id} pour pending → running → complete/failed/cancelled. Si tu ne veux pas interroger, passe un webhook_url et le serveur livre lui-même l’état final du job.
La livraison du webhook n’est pas un POST balancé au hasard avec un haussement d’épaules. Elle est signée en HMAC-SHA256 sur timestamp + "." + body, envoyée en en-têtes X-Webhook-Timestamp et X-Webhook-Signature: t={ts},v1={hex}, avec un vrai calendrier de réessais en backoff exponentiel sur tout ce qui n’est pas un 2xx: 30s, 1m, 5m, 30m, 2h, 12h, puis il écrit une entrée de dead-letter et abandonne. On attend de ton récepteur qu’il dédoublonne sur (timestamp, signature), pour l’idempotence. C’est la même forme qu’un contrat de webhook de prestataire de paiement, parce que c’est le seul précédent qui vaille la peine d’être copié pour “prévenir quelqu’un de façon fiable qu’un long job est fini”.
Onze Outils Sur le Fil MCP
Monté sur /v1/mcp en JSON-RPC sur HTTP streamable, flickies expose onze outils MCP qui reflètent la surface REST presque 1:1: list_engines, info, lipsync, restore, transcode, trim, concat, scale, mux_audio, extract_audio, thumbnail_grid. Pointe dessus un LLM à function calling, LibreChat, Cursor, Claude avec le connecteur MCP, n’importe quel framework d’agents que tu fais tourner, et il peut piloter tout le pipeline lui-même: poser une vidéo de visage, poser un clip audio, appeler lipsync avec restore_face: true pour enchaîner GFPGAN automatiquement après la passe de sync (ce qui, il faut le noter, déclenche une éviction à chaud du modèle de lipsync au milieu de l’appel d’outil, c’est intentionnel, ça libère la VRAM dont la passe de restauration a besoin), puis te rendre un chemin ou une taille.
C’est aussi la couche vers laquelle aigate fait proxy quand tu bascules FLICKIES=1 ou FLICKIES_CUDA=1, une seule porte d’entrée nginx devant chacun de mes services IA auto-hébergés, celui-ci compris.
La Spec d’Abord: Clients Go et Python Générés Depuis le Même Putain de Fichier
C’est la partie qui, pour moi, sépare vraiment flickies de “encore un wrapper d’API par-dessus du ML”. openapi.yaml n’est pas une décoration écrite après le code pour faire pro, c’est la source unique de vérité DEPUIS laquelle trois artefacts distincts sont générés, pas écrits pour correspondre:
make generate # regenerate all three: server models + Go client + Python client
make generate-models # just server-side Pydantic (src/flickies/schema/_generated.py)
make generate-client-go # just the Go client (pkg/clients/go/client.gen.go)
make generate-client-python # just the Python client (pkg/clients/python/flickies-client/)
make generate-check # CI gate — fails the build if generated files drift from openapi.yamlNe jamais éditer un fichier généré à la main. Tu édites la spec, tu lances make generate, tu commites le tout ensemble. Le client Go vient d’oapi-codegen, celui en Python d’openapi-python-client, tous deux de vrais paquets typés et importables, pas du curl enveloppé dans une fonction en arrière-pensée:
go get github.com/psyb0t/docker-flickies/pkg/clients/go@latestimport flickies "github.com/psyb0t/docker-flickies/pkg/clients/go"
c, _ := flickies.NewClient("https://ciprian.51k.eu00")
resp, err := c.PostVideoLipsync(ctx, flickies.VideoLipsyncRequest{...})pip install "git+https://github.com/psyb0t/docker-flickies.git#subdirectory=pkg/clients/python/flickies-client"from flickies_client import Client
from flickies_client.api.lipsync import post_video_lipsync
from flickies_client.models import VideoLipsyncRequest
client = Client(base_url="https://ciprian.51k.eu00")
result = post_video_lipsync.sync(client=client, body=VideoLipsyncRequest(...))Ce n’est pas parfait, les structs VideoTrimRequest et VideoConcatRequest du client Go perdent pour l’instant start_sec/end_sec/precise, à cause d’une limitation connue d’oapi-codegen avec les schémas composés par allOf (le bloc properties inline sur le type composé saute). En attendant, les appelants Go sérialisent ces corps précis via map[string]any. Je préfère documenter une vraie limitation de générateur que prétendre que le pipeline est sans faille, l’idée n’est pas que la génération de code est magique, c’est que la spec et chaque client qui la parle sont mécaniquement incapables de diverger, puisqu’ils viennent tous de la même étape de build.
Logs, Auth, Limites de Débit, la Merde Ennuyeuse Qui Compte Vraiment à 3h du Mat
Les logs structurés en JSON partent sur stderr ET dans un fichier rotatif (FLICKIES_LOG_FILE, 50MB × 5 sauvegardes par défaut), avec un formateur maison qui caviarde récursivement tout ce qui correspond à password|token|secret|api_key|authorization|cookie|hf_*|sk-ant-*, dans les clés comme dans les valeurs, au moment du formatage, avant que la ligne soit jamais écrite. Chaque enregistrement porte un trace_id et un request_id passés dans un ContextVar, amorcés depuis l’en-tête X-Request-Id entrant s’il a une forme valide (UUID v4 ou ULID, 64 caractères max, pas de retours à la ligne, une entrée pourrie se voit simplement attribuer un UUID tout neuf au lieu d’être renvoyée en écho, ce qui ferme un vecteur d’injection dans les logs). Pose FLICKIES_LOG_LEVEL=DEBUG et tu obtiens un traçage de qualité reconstruction: chaque commande ffmpeg et ffprobe, chaque décision de trim ou concat entre copie de flux et réencodage précis, le temps mur d’inférence de chaque moteur, les octets récupérés et envoyés par URL, les transitions du cycle de vie des jobs. Les URL loguées ont d’abord leur query string retirée, donc un output_url présigné ne fait pas fuiter sa signature dans tes fichiers de log.
L’auth est un bearer token statique via FLICKIES_AUTH_TOKEN, vérifié avec hmac.compare_digest pour ne pas être attaquable par chronométrage, avec /healthz exempté pour que les sondes de ton orchestrateur continuent de marcher. Ne pose pas la variable et l’auth est simplement désactivée, ton choix, ta frontière réseau. Par-dessus: un limiteur de débit à seau de jetons par IP (60 requêtes par minute par défaut, réglable), et une couche de déduplication basée sur Idempotency-Key qui met en cache (statut, corps) par (clé, méthode, chemin), pour qu’un POST réessayé ne relance pas deux fois un rendu coûteux. Les trois sont des middlewares ASGI en pure bibliothèque standard, sans la moindre dépendance en plus juste pour limiter le débit des requêtes.
Où Ça Vit
flickies se monte à l’intérieur d’aigate sur /flickies/ et /flickies-cuda/, derrière le même nginx qui est devant tous les autres services IA auto-hébergés que je fais tourner, un seul make run-bg, les deux variantes basculées avec FLICKIES=1 et FLICKIES_CUDA=1. C’est la pièce en forme de vidéo du même puzzle qu’audiolla et talkies remplissent pour l’audio et la parole, même contrat de fil, même ergonomie pour l’opérateur, donc ajouter un quatrième ou un cinquième service à cette stack plus tard ne veut pas dire tout réapprendre.
Ce Que Tu Obtiens Vraiment
flickies est une boîte à outils vidéo qui se trouve inclure du lipsync depuis le début: quatre vrais moteurs derrière un seul pool GPU à échange à chaud, sept opérations ffmpeg qui ne prétendent pas être plus fancy que ffmpeg, des jobs asynchrones avec des webhooks réellement signés au lieu d’un POST balancé au hasard, onze outils MCP pour n’importe quel agent que tu pointes dessus, et des clients typés en Go et en Python générés depuis la spec exacte contre laquelle le serveur valide, pas maintenus à la main, pas en train de dériver, pas en train de te mentir sur ce que l’API accepte réellement. Auto-hébergé, WTFPL, tourne sur un GPU que tu possèdes déjà.
Chope-le sur github.com/psyb0t/docker-flickies ou tire les images directement de Docker Hub. Fais-en ce que tu veux, mais arrête de payer un SaaS pour faire tourner un modèle open source sur du matériel que tu aurais pu acheter cash avec trois mois de leur facture.
L’Installer Dans Ton Agent
Confier une boîte à outils vidéo à un agent marche mieux quand il connaît déjà la liste d’outils. Tout ce qui est sous .agents/ est catalogué dans un seul marketplace, donc ça fait deux commandes:
claude plugin marketplace add psyb0t/agents
claude plugin install flickies@psyb0tCodex utilise le même marketplace avec un verbe différent, codex plugin add flickies@psyb0t, parce qu’il n’existe pas de codex plugin install. Il trouve aussi le skill tout seul dans un checkout du repo, puisqu’il scanne .agents/skills/ nativement sans que rien ne soit installé. Il est aussi listé sur le MCP Registry officiel maintenant, donc un client qui résout ses serveurs depuis là peut le trouver sans qu’on lui donne une URL.