ADR-0014 — La vidéo est un job asynchrone
La vidéo et le lipsync sont des jobs asynchrones fermés par webhook, lecture ou balayage.
Statut
Proposé · 2026-09-27
Remplace ou amende : Rien. Remplace l'attente synchrone du clip, portée de la plateforme.
Origine : ADR 0004 du dépôt applicatif Orbit, renumérotée 0014 à l’intégration dans cette série (une seule série d’ADR vit désormais ici).
Statut dans Orbit (2026-10) : en vigueur. Décision reprise de l'ancien dépôt Studio. L'origine publique du webhook vidéo est
ORBIT_PUBLIC_URL(https://orbit.easyconnector.appen production) ; l'ancien nomSTUDIO_PUBLIC_URLn'est plus relu qu'en alias de lecture tant que le nouveau est vide (RENAMED_ENV). « La plateforme » est l'ancienne application dont le code était porté.
Répond aux constats G2-06, G1-17 et G2-08 de l'audit de la phase 3. Sources, lues le 2026-09-27 : la page vidéo de l'AI Gateway (
vercel.com/docs/ai-gateway/getting-started/videoet…/modalities/video-generation:doStart,doStatus, webhook signéx-ai-gateway-signature) ; les types installés deai7.0.116 (experimental_startVideo,experimental_getVideoStatus) et de@ai-sdk/gateway4.0.94 (GatewayAsyncJobMetadata, route/video-model/startet/video-model/status,callbackUrl).
Contexte
La plateforme, et le studio jusqu'ici, attendaient le clip Veo (et le lipsync)
dans la requête : experimental_generateVideo ouvre un flux SSE et ne rend
la main qu'avec la vidéo. Trois défauts en découlaient.
- Un clip en retard était perdu (G1-17). Veo met souvent une à trois minutes ; passé la limite de la fonction (300 s), la fonction mourait, le balayage remboursait la réserve comme un appel orphelin, et le clip que le fournisseur rendait quand même, et facturait, n'arrivait nulle part.
- La requête tenait une fonction pendant toute la génération (G2-06) : 270 s de budget, un emplacement de la capacité du membre, une page qui attend, pour un travail qui se fait ailleurs.
- Aucune limite de fonction n'était déclarée (G2-08) : seul le cron
exportait
maxDuration, et rien ne tenait les budgets des fournisseurs sous cette limite.
La Gateway offre maintenant un chemin asynchrone : doStart rend une
operation opaque (et, avec webhookUrl, un id de job et un secret de
signature propre au job), doStatus lit cette opération depuis n'importe quel
processus, et un webhook signé annonce la fin. L'AI SDK 7 l'expose à un niveau
plus haut que le modèle : experimental_startVideo (qui accepte une chaîne de
modèle routée par la Gateway, les en-têtes, maxRetries, abortSignal et
webhookUrl) et experimental_getVideoStatus.
Décision
Une vidéo ou un lipsync est un job, jamais un appel attendu.
- Départ (
startVideoJob,packages/engine/src/generation/video-job.ts) : ligneQUEUED, réserve à l'estimation, ligneSUBMITTEDavec sa réserve avant l'appel, puisexperimental_startVideoavec la clé d'idempotence de la génération (un départ réessayé est un seul job), l'utilisateur et les étiquettes de la Gateway, et le webhook quand le studio a une URL publique HTTPS. L'id du job (unique), l'opération et le secret sont écrits sur la ligne ; la requête répondrunningaussitôt. - Un seul fermeur (
settleVideoJob), atteint par trois chemins, tous parclaimInFlight: le webhookPOST /api/webhooks/video/[generationId], une lectureGET /api/generations/[id]quand la ligne tourne depuis plus de 15 s, et le balayage après 5 minutes. Il lit le statut avant de prendre la ligne ; terminé, il télécharge le clip (borné en octets et en temps), le mesure, le range sous le dossier de la ligne, crée l'asset et règle ; en échec, il rembourse. Un téléchargement ou une écriture ratés rendent la ligne au suivant, sans argent. - Le webhook ne fait pas foi par son contenu. La signature (
t,v1= HMAC-SHA256 de<t>.<corps brut>sous le secret du job, temps constant, 5 minutes) ouvre la porte, puis le statut est redemandé à la Gateway. SansORBIT_PUBLIC_URL, aucun webhook n'est enregistré et la lecture ferme le job. - Abandon à la vie d'une réserve : un job toujours en cours après 2 h
(
VIDEO_GIVE_UP_AFTER_MS,DEFAULT_HOLD_TTL_MS) estFAILED video_timeoutet remboursé. - Une limite de fonction,
STUDIO_FUNCTION_MAX_DURATION_S = 300, exportée par chaque route qui génère ou ferme, et par la page qui liera les actions (max-duration.test.ts).
Pourquoi experimental_startVideo plutôt que gateway.videoModel(id).doStart
que montre la documentation de la Gateway : c'est la même requête
(/video-model/start, callbackUrl), mais par la fonction publique du SDK,
avec la chaîne de modèle, les tentatives et l'annulation que le reste du
studio utilise déjà (ADR 0012), et sans instance de fournisseur. Pourquoi pas
experimental_generateVideo avec webhook ou poll : les deux attendent
dans le processus appelant, ce qui est exactement ce qu'on retire.
Conséquences
- La réponse d'une vidéo n'a plus d'URL : le cockpit lit
GET /api/generations/<id>(le contrat Contrat serveur du cockpit dit à quel rythme). - Trois colonnes sur
StudioGeneration(migrationvideo_async_job) :providerJobId(unique),providerOperation,providerWebhookSecret(effacé dès que la ligne se ferme). - Une nouvelle porte machine publique au proxy, pour la seule forme
^/api/webhooks/video/g[0-9a-f]{24}$. - Le balayage lit la Gateway : il reçoit les fournisseurs et le stockage des
générations (
SweepDeps.video). - Le fondateur doit poser
ORBIT_PUBLIC_URL(HTTPS) en production pour que les vidéos se ferment par webhook ; sans lui, elles se ferment par lecture, plus lentement mais sans perte. - Seedance et Kling (WP7) passeront par le même job : c'est la fondation.
Vérification
packages/engine/src/generation/video-job.integration.test.ts(Postgres) : départrunningavec ligneSUBMITTED, un seul appel et la réserve à l'estimation ; webhook signé qui ferme sur les millisecondes mesurées ; webhook perdu fermé par le balayage ; clip en retard récupéré ; job en échec remboursé ; deux lectures concurrentes, un seul règlement ; abandonvideo_timeout; téléchargement raté puis repris ; règlement en attente réglé par le balayage ; lipsync par le même chemin.packages/engine/src/generation/video-job.test.ts: signature (falsifiée, périmée, malformée, rotation), URL du webhook, téléchargement borné.packages/engine/src/generation/providers.transport.test.ts: sur le fil,/video-model/startaveccallbackUrl, réessayé sous la même clé, puis/video-model/status.apps/web/src/app/api/webhooks/video/[generationId]/route.test.ts: une signature falsifiée ou périmée répond 401 et ne change rien.apps/web/src/test/max-duration.test.ts,apps/web/e2e/phase3.spec.ts(E2E-08 par HTTP :running, puisdonepar la route de statut).