← Alle artikelen

Ontwikkelaars · 10 min

Hoe we een promofilm maakten die niet kan liegen

De film voor 0.3.0 van Caddy Configuration Tools voor VS Code is een Playwright-test: elke stap wordt gecontroleerd tegen de echte extensie en een echte Caddy, en het commentaar is de eigen stem van de auteur, lokaal gekloond op onze eigen GPU.

  • vscode
  • caddy
  • playwright
  • video
  • voice

TL;DR: De promofilm voor 0.3.0 van Caddy Configuration Tools voor VS Code is geen schermopname die iemand met de hand heeft gemaakt. Het is een Playwright-run tegen de vers gebouwde extensie en een echte Caddy, en elke stap controleert of de functie die hij toont echt heeft gewerkt. Het commentaar is de eigen stem van de auteur, lokaal gekloond met Chatterbox Multilingual. Er is niets geüpload om het te maken.

Bekijk de film op YouTube. We linken de film in plaats van hem in te bedden: een ingebedde speler laadt bij elke paginaweergave de servers van Google.


Een promo voor een editor-extensie is meestal een schermopname. Iemand oefent de klikken, neemt op tot een take goed gaat en knipt de fouten eruit. Soms is het erger: een nagemaakte interface, of een video van een functie die inmiddels is veranderd. In beide gevallen zegt de film niets over of de extensie vandaag werkt.

Wij wilden een film die niet kan liegen. Als een functie breekt, kan de film niet gemaakt worden.

Het storyboard is code

De hele film staat in één bestand, demo/storyboard.ts. Het is een lijst met scènes, en elke scène is een lijst met stappen. Een stap heeft vijf onderdelen:

  • caption: de ondertitel die in het beeld wordt gebrand
  • say: wat de stem zegt, als dat afwijkt van de ondertitel
  • act: wat de gebruiker doet: de muis bewegen, klikken, typen, toetsen indrukken
  • check: een Playwright expect die moet slagen zodra de act klaar is
  • hold: de minimale tijd dat de stap in beeld blijft

Het storyboard heeft 27 stappen, en elke stap heeft een check. De checks draaien in elke variant van de film. Als er een faalt, zet de speler de stap in de tijdlijn als mislukt en stopt de opname, en is er geen film. Wat overblijft is een gedeeltelijke tijdlijn die laat zien waar het misging en hoe het scherm er op dat moment uitzag.

Hier is de stap waarin de snelle URL-fix wordt toegepast. De vorige stap typte uit gewoonte http://localhost:3000 in een dial-veld, terwijl Caddy host:port wil:

ts

Als de extensie die codeactie niet meer aanbiedt, of er een andere naam aan geeft, breekt de opname hier.

De regels voor een act staan bovenaan in demo/lib/types.ts: alleen wat een gebruiker in VS Code kan doen. Geen bestanden schrijven langs de editor om heen, en geen commando’s via een API uitvoeren. De extensie moet alles produceren wat je ziet.

Het Ctrl+. menu in VS Code dat voorstelt een URL in een Caddy-dial-veld te wijzigen naar localhost:3000
De quick-fix-stap, zoals de rendering-stap hem uit de ruwe opname heeft opgeslagen.

Een echte editor en een echte Caddy

Voordat er iets wordt gefilmd, bouwt demo/global-setup.ts de extensie met npm run package en installeert het nieuwe .vsix in code-server. Dat gebeurt bij elke run, tenzij we expliciet vragen de vorige build te hergebruiken, want een opname moet de extensie tonen zoals hij nu is.

Dezelfde setup start een wegwerp-Caddy met caddy run: een admin-API op de ene poort en één lege HTTP-server op een andere. De instellingen van de extensie wijzen caddy.adminUrl naar die admin-poort.

De scène “toepassen op de draaiende Caddy” gaat via die admin-API. De live-commando’s van de extensie gebruiken CaddyClient uit @viaalink/caddy-api-client, onze TypeScript-client voor de Admin API van Caddy. Daarna opent de film een terminal en typt een echte curl:

ts

Als de nieuwe route Caddy nooit bereikt, verschijnt “Hello from Caddy Tools!” nooit in de terminal en stopt de film daar.

Wat er klaargezet is

Een deel ervan is voor de camera klaargezet, en dat zeggen we liever erbij:

  • De startwerkruimte is vooraf gevuld. De bestanden in demo/seed/ worden voor elke opname opnieuw gekopieerd, zodat de regelnummers in het storyboard altijd kloppen. De route die op de curl antwoordt is een van die seed-bestanden: een static_response-handler met de body Hello from Caddy Tools!. De extensie stuurt hem naar Caddy, en Caddy antwoordt ermee.
  • code-server is opgemaakt voor de camera. Donker thema, een groter lettertype, geen minimap, geen welkomstpagina en geen telemetrie. De terminalprompt luidt my-gateway $, zodat geen gebruikersnaam of thuismap in een publieke video terechtkomt.
  • De cursor is getekend. Playwright stuurt de muis via het Chrome DevTools Protocol, dus de opname heeft helemaal geen aanwijzer, en een kijker zou menu’s zien openen zonder dat iemand ze aanraakt. demo/lib/overlay.ts injecteert een kleine laag die een pijl tekent die elke mousemove volgt, een rimpel bij elke klik, en een badge zoals “Ctrl + Space” voordat een sneltoets wordt ingedrukt. De laag heeft pointer-events: none en zit buiten de DOM van de werkbank, dus hij kan geen klik onderscheppen.
  • De titel- en eindkaarten zijn gemaakt met ffmpeg, niet opgenomen.

Eerst de stem, dan het beeld

Gewoonlijk neem je het beeld op en perst je de woorden in de gaten. Wij hebben dat omgedraaid.

De stemfase draait vóór de opname. Die spreekt elke regel van het storyboard uit en schrijft voice.json: voor elke regel het audiobestand en hoeveel milliseconden het duurt. De speler in demo/lib/player.ts leest dat bestand en houdt elke stap minstens max(hold × pace, spokenMs + tailMs(regel)) in beeld. De tijd die de act zelf kost telt mee in dat venster, dus een stap die twee seconden typt blijft daarna niet nog eens twee seconden stilstaan.

De pauze na een regel hangt af van hoe de regel eindigt:

ts

Een komma betekent dat de gedachte doorloopt, dus de volgende regel volgt bijna meteen. Een vraag krijgt een beat, en een punt iets meer. Dit verving een vaste 450 ms na elke regel, waardoor het commentaar klonk als een opsomming die werd voorgelezen.

Eén ffmpeg-slag

demo/scripts/render.ts maakt van de opname en de tijdlijn de afgewerkte film. Eén ffmpeg-filtergraaf doet het meeste werk: de titelkaart, de opname met de voorloop weggeknipt en de bijschriften als ASS-ondertitels erin gebrand, en de eindkaart, allemaal aaneen. Elke stemregel wordt op het begin van zijn stap ingevoegd met adelay en gemengd met amix. Eén encodering houdt de tekst in de editor scherp.

De renderstap schrijft ook:

  • een .srt-bestand met de bijschriften op de tijd van de afgewerkte film
  • steps.png, een contactblad met één genummerde still per stap, om te beoordelen zonder te kijken
  • schone stills (zonder bijschriften) voor stappen die als still zijn gemarkeerd, uit de ruwe opname gesneden
  • een GIF voor stappen die dezelfde clip-naam delen, voor de README

Er zijn twee varianten van hetzelfde storyboard. promo heeft de kaarten, gewone bijschriften en de stem. qa heeft geen kaarten, draait op 1,6 keer het tempo, bevat zes extra stappen en nummert zijn bijschriften (“12. Scène · bijschrift”), zodat een bugmelding kan zeggen “het breekt bij 12”. Beide varianten doen dezelfde dingen in de editor.

De stemkloon

Het commentaar is de eigen stem van de auteur, gemaakt door een kloon die op onze eigen machine draait. Hij staat in een gedeeld pakket, packages/voice-clone, zodat elk project hem kan gebruiken.

De engine is Chatterbox Multilingual, lokaal op onze eigen GPU. De referentie-opnames, de modelgewichten en de uitvoer blijven op die machine. Chatterbox zet een Perth-watermerk op zijn uitvoer, en dat laten we staan: een film met een synthetische stem erin hoort de markering te dragen die dat zegt.

De referentie opnemen

bin/record.ts is een terminal-prompter. Hij neemt eerst vijf seconden kamergeluid op en toont dan één zin tegelijk uit een leesblad. Het Engelse blad, sheets/en.txt, heeft 54 zinnen in secties: opwarmen, product, getallen, vragen, algemene dekking, en twee gereserveerde regels. Takes worden ruw opgenomen: 48 kHz, 24-bit mono, zonder bewerking. Na elke take controleert hij het niveau en laat je afspelen, overdoen of verder gaan.

bin/reference.ts bouwt het fragment waarvan de kloon leert. De opwarmtakes worden de kop (minstens 10 seconden, want daaruit leest Chatterbox de timbre), gevolgd door alle andere takes. De twee gereserveerde zinnen komen nooit in de referentie. Ze dienen alleen om te meten hoeveel de kloon op de echte stem lijkt.

Beste van N, teruggeluisterd

Een gesynthetiseerde take kan een woord laten vallen, of een geluid maken dat een mens niet maakt. Daarom wordt elke regel meer dan één keer gegenereerd (standaard drie takes), en gaat elke take door twee controles voordat hij bruikbaar is:

  • Terugluisteren. whisper.cpp, ook lokaal, transcribeert de take. Als een woord van drie letters of meer niet terugkomt, is de take onvolledig. Het letterfoutpercentage wordt als tweede signaal gerapporteerd. Een volledige take wint altijd van een onvolledige, hoe schoon die ook klinkt. Als geen enkele take van een regel volledig is, faalt de render met die regel in de foutmelding.
  • Artefacten. tts_artefacts.py controleert elke take op gedreun, fluittonen, klikken en gezoem, de geluiden die een vocoder maakt als hij de spraak kwijtraakt.

Gelijkenis meten

voice-similarity.py vergelijkt de sprekerseenbedding van elke gegenereerde regel met die van de twee gereserveerde opnames, met de eigen stemencoder van Chatterbox. De ondergrens is 0,80. De 21 regels van de promo scoren gemiddeld 0,825. Eén korte regel, “Pick host, type the domain.”, scoort 0,73, onder de ondergrens. Het script rapporteert de getallen en beslist zelf niets: we luisteren naar het resultaat voordat het naar buiten gaat.

Wat misging, en wat we hebben opgelost

De eerste versie klonk schokkerig. Erger: je hoorde bij het begin van elke regel een sissen aanzetten en aan het eind weer uit.

De oorzaak: Chatterbox kopieert de ruimte van de referentie samen met de stem. Onze referentie was opgenomen met een webcammicrofoon, dus elke take droeg het gesis van die ruimte mee. Tussen de regels is de film digitale stilte, dus het gesis kwam en ging met elke zin. We hadden bovendien de luidheid van elke regel apart genormaliseerd, en dat maakte het erger: normalisatie per regel tilt het geruis van korte regels op en laat buurregels in niveau springen.

De oplossing is cleanChain() in src/speak.ts, toegepast op elke take:

ts

In volgorde: een high-pass op 80 Hz tegen gedreun, RNNoise (ffmpeg’s arnndn) om het gesis uit de pauzes te halen, een lichte FFT-ruisonderdrukking voor de constante ruisvloer die overblijft, een knip van beide uiteinden op -55 dB, en een fade-in van 10 ms en een fade-out van 80 ms, zodat geen regel met een klik begint of eindigt. Hij is bewust zacht: een zwaardere ruisonderdrukking begint medeklinkers op te eten.

Na die opschoning krijgen alle regels samen één vaste versterking, zodat de stem als geheel op -16 LUFS uitkomt, gevolgd door een limiter. De regels houden hun natuurlijke balans. Die aanpak komt uit de stempijplijn van een eerder project van ons.

Met dit alles aan, schakelt het gesis niet meer aan en uit bij elke zin: een spectrogram van een regel voor en na de bewerking laat de constante ruis tussen de woorden verdwijnen, en elke regel begint en eindigt nu in stilte. In de afgewerkte promo vult spraak ongeveer 91% van het schermopname.

Twee kleinere problemen:

  • Afkortingen. whisper hoort “API” als “A.P.I.”, dus de controle op ontbrekende woorden sloeg aan op takes die goed waren. Afkortingen die letter voor letter worden uitgesproken (API, URL, CLI, DNS, JSON en een paar andere) worden bij die controle nu overgeslagen.
  • Korte regels. Eénregelige zinnen zoals “It is live.” eindigden lang voordat de actie op het scherm klaar was, waardoor het beeld in stilte doorliep. We hebben ze herschreven zodat ze de actie vullen: “It is live.” werd “And it is live: one curl, and the new route answers right away.”

Wat hierna komt

Hetzelfde storyboard, dezelfde stem en dezelfde renderpijplijn worden omgezet in een gedeeld pakket, zodat de films voor de Viaalink CLI en Relay op dezelfde manier gemaakt kunnen worden.

Contact

Een onderwerp voor ons?

Schrijf ons.