Bildschirmvideo
Chroms Screencast zeichnet den Browser selbst auf und erstellt daraus ein flüssiges 60-fps H.264 .mp4. Untertitel, Cursor, Zoom und Sounds werden nachträglich hinzugefügt, sodass die Aufnahme unabhängig von der Seitenaktivität glatt bleibt.
Skripte lösen playwright aus dem aktuellen Repository-node_modules auf – führe sie daher aus dem Repository-Stammverzeichnis aus. <skill-dir> ist der Ordner dieses Skills.
0. Vorher fragen
Falls die Anfrage es nicht bereits enthält, frage in einer Nachricht:
- Welche Geschichte? Welche App und URL, welche Seiten, die 3–6 Szenen, die der Betrachter sehen soll. Wer schaut zu (Teamkollegen, ein Reviewer, ein Kunde)?
- Sprachausgabe? Nur Untertitel oder mit Sprachausgabe (KI-Voice-Over)? Kunden-Videos haben immer eine Sprachausgabe.
- Wohin damit? Nur die
.mp4-Datei (Standard), ein GitHub-PR oder -Issue, ein Linear-Ticket, Slack oder ein anderer Ort. Frage nach dem PR/Issue/Kanal; erstelle niemals ein Ticket im Namen einer anderen Person. - Welche Daten? Welche lokale DB oder welcher Account soll für die Aufnahme verwendet werden. Niemals Produktion, niemals echte Kundendaten.
1. Aufnahme
-
Starte die App lokal und öffne jede Seite einmal vorher: Andernfalls landet die erste Kompilierung des Dev-Servers im Video.
-
Schreibe ein Flow-Skript auf Basis von
scripts/record-lib.mjs(kopiereexamples/playwright-site/flow.mjs, eine ausführbare Aufnahme einer öffentlichen Website). Verwendestep(id, caption, action)(oder nacktescaption()), um für jeden Schritt eine Untertitelzeile auf dem Bildschirm anzuzeigen: unten zentriert, über der Play-Leiste eines Players, durchscheinend (captionStyle: 'top'für ein Banner). Halte es bei etwa 1 Minute. Klicke mit demclick(locator)/hover(locator)des Flows, nicht mitlocator.click(): Sie gleiten den gezeichneten Cursor zuerst zum Ziel. Ein nackteslocator.click()funktioniert zwar auch, aber der Cursor springt.zoom(locator, { scale })zoomt sanft auf ein Ziel heran,unzoom()zoomt wieder heraus, undclick(locator, { zoom: true })zoomt vor einem Klick hinein. Der Zoom bleibt erhalten, bis du ihn bewegst; ein Klick außerhalb der gezoomten Ansicht schwenkt dorthin.Zoom-Budget: Es sollte sich anfühlen, als würde eine Person sich vorbeugen, nicht wie ein Reflex.
- Zoome nur, wenn kleiner Text der Punkt ist: ein Diff, ein geänderter Wert, eine Dialog-Nachricht. Niemals für Navigation, zum Öffnen eines Menüs oder für einen Knopfdruck, den jeder sehen kann.
- Etwa ein Zoom pro 15–20 Sekunden. Zwei hintereinander: schwenke dazwischen (rufe
zoomfür das nächste Ziel auf) anstatt heraus- und wieder hineinzuzoomen. - Bleibe gezoomt, während die nächsten Aktionen im selben Bereich stattfinden;
unzoom(), wenn die Geschichte woanders hingeht. - Skalierung 1,3–1,5. Gehe nur auf 2 für etwas Winziges, wie ein einzelnes Icon. Halte mindestens 2 Sekunden.
- Bevorzuge eine vertonte Pause gegenüber einem Zoom: Wenn der Satz in voller Größe gut lesbar ist, zoome nicht.
Tippe mit
type(locator, text, { clear }): Es tippt im Tempo eines Menschen.clickundtypeprotokollieren jeden Klick und Tastendruck in<video>.sfx.json, undnarrate.mjslegt aufgenommene Sounds unter die Stimme: aufgezeichnete Mausklick- und mechanische Tastatur-Samples aussounds/.SFX_VOLUMEskaliert sie (Standard 1),SFX=0für keine.page.keyboard.typeund nackteslocator.click()erzeugen keinen Ton. -
node flow.mjsgibt den.mp4-Pfad aus. Es benötigt ffmpeg ($FFMPEGoder im PATH; kein ffmpeg?npm i --prefix <scratch> ffmpeg-static).
Qualität
record(flow, { scale }) layoutet die Seite in size (Standard 1280×800 CSS-Px, damit die UI normal aussieht) und rendert sie mit scale× Pixeldichte: 2 (Standard) ergibt 2560×1600, 3 ergibt 3840×2400. Text wird schärfer, nicht kleiner. Erwarte etwa 15–40 MB pro Minute bei 2 und das 2–3-Fache bei 3. Für ein zielgrößenbegrenztes Ziel (GitHub kostenlos 10 MB, Slack-Vorschauen) verwende scale: 1 oder CRF=24.
Sprachausgabe (wenn nach Stimme gefragt)
- Schreibe
script.json:[{ "id": "intro", "say": "…" }, …], eine gesprochene Zeile prostep-ID. Schreibe es für das Gehör: kurze Sätze, Zahlen als Wörter („eintausendfünfhundert“). GOOGLE_API_KEY=… node <skill-dir>/scripts/tts.mjs script.json <audio-dir>schreibt eine WAV-Datei pro Zeile unddurations.json. Gemini 3.8 Flash TTS (TTS_MODEL,TTS_VOICEzum Überschreiben) klang natürlicher als OpenAIgpt-4o-mini-tts. Hole dir einen Schlüssel unter aistudio.google.com.- Nimm mit
{ narration: '<audio-dir>/durations.json' }auf. Jederstepwartet, bis seine Zeile beendet ist, und die Startzeiten landen in<video>.mp4.marks.json. node <skill-dir>/scripts/narrate.mjs <video.mp4> <audio-dir> <out.mp4>mischt die Clips und Sounds ein (codiert neu mitCRF, Standard 18). Liefere<out.mp4>aus.
TTS-Tücken:
- Sende nur die Zeile. Ein Präfix wie „Sage warmherzig: …“ wird vorgelesen, und
systemInstructiongibt 400 zurück. - Gemini gibt eine ganze WAV plus einen Provenienz-Chunk zurück; das Einwickeln in einen zweiten Header führt zu Rauschen an beiden Enden jeder Zeile.
tts.mjsextrahiert daher den Daten-Chunk; behalte dies bei. - Um die Synchronisation zu überprüfen, transkribiere die mp4-Audio mit Zeitstempeln (OpenAI
whisper-1,response_format=verbose_json) und vergleiche sie mit den Markierungen.
Fallstricke, die eine Wiederholung kosten
- Leere ein vorausgefülltes Eingabefeld mit
.fill('')', bevor dupressSequentially` verwendest, das nur anhängt. - Jede Aufnahme ändert Daten (veröffentlicht, akzeptiert, speichert). Setze die Daten zwischen den Aufnahmen zurück.
- Warte auf Autosaves und KI-Hintergrundausfüllungen vor dem nächsten Schritt, sonst springt die UI.
- Rad-Scrolling kann einen einklappenden Header zusammenfalten und die nächste Schaltfläche verstecken. Bevorzuge
el.scrollIntoView()überlocator.evaluate. - Lasse die Seite niemals in den Browser-Vollbildmodus wechseln: Die Frames werden skaliert und das Video auf die obere linke Ecke beschnitten. Führe
document.documentElement.requestFullscreen = undefinedvor einer Vollbild-Schaltfläche aus.
Demo-Daten
- Seed für genau den Zustand, den die Geschichte benötigt, in eine lokale DB mit glaubwürdigen Personen bei
example.comund555-Nummern. Niemals ein echter Kunde, niemals eine Vorschau- oder Produktions-DB. - Verlaufs-, Audit- und Ereignistabellen sind append-only: Jede Wiederholung fügt Zeilen hinzu, die die nächste Aufnahme inszeniert aussehen lassen. Verwende einen frischen Datensatz pro Aufnahme oder halte die Geschichte auf Aktionen, die keine Spur hinterlassen.
- Trockentest zuerst: Ein
DRY=1-Zweig im Flow klickt bei jeder destruktiven Bestätigung auf Abbrechen. Ziehe Einzelbilder an einigen Schrittmarkierungen, korrigiere Locators und Bildausschnitt, führe dann die eine echte Aufnahme durch. - Überprüfe nach der Aufnahme, ob der DB-Endzustand mit dem übereinstimmt, was die Untertitel oder die Sprachausgabe behaupten.
- Das Ändern der Daten bedeutet,
tts.mjsfür die Zeilen, die diese Daten beschreiben, erneut auszuführen. - zsh trennt
$VARnicht nach Wörtern. Übergib Framzeiten wörtlich anframes.mjsoder verwende eine Schleife unterbash.
Kunden-Tutorial-Videos (Onboarding, Anleitungen)
- Immer mit Sprachausgabe. Halte die Untertitel kurz; die Stimme trägt die Details.
- Ein E-Mail-Schritt erfolgt in Gmail:
mountGmail(page, { emailHtml, subject, sender, me })ausscripts/gmail-scene.mjs. Füttere es mit der echten gerenderten E-Mail der App. Es schreibttarget="_blank"in_topum, da ein Popup die Aufnahme auf der falschen Seite warten lässt. Antworten ist eingebaut:#reply-btn,#reply-text,#send-btn. Übergibmailbox: 'sam'für mehrere Posteingänge (bereitgestellt unter/samund/sam/message). Eine Antwort erreicht die App nie, also schreibe ihre Wirkung direkt in die lokale DB. - Warte auf Elemente, nicht auf
waitForURL: Auth-Weiterleitungsketten brechen Navigationen ab (ERR_ABORTED) und einige Seiten feuern nieload. Gib navigierenden Klicks{ noWaitAfter: true }mit. - Wärme nach dem (Neu-)Starten eines Dev-Servers jede Seite und jeden API-Endpunkt vor, den die Aufnahme erreicht.
- Führe vor jeder Aufnahme ein erneutes Seeding durch. Magic-Link-Tokens sind Einmal-Tokens, daher verbraucht ein fehlgeschlagener Versuch sie.
2. Überprüfung
node <skill-dir>/scripts/frames.mjs <video.mp4> 10 30 50 schreibt frame-<t>.jpg neben das Video. Sieh sie dir vor der Auslieferung an. Bei Sprachausgabe bitte den Benutzer, einmal zuzuhören: Einzelbilder können keine Audiofehler erkennen. Prüfe auf echte Kundendaten (Namen, E-Mails, Telefone): Das Hochladen veröffentlicht sie für alle, die das Ziel sehen können.
3. Auslieferung
Nur die Datei (Standard)
Gib den absoluten Pfad der endgültigen .mp4 aus. Fertig.
GitHub-PR, -Issue oder -Kommentar (wird inline abgespielt)
GitHub spielt ein Video nur inline von einer https://github.com/user-attachments/assets/<uuid>-URL in einer eigenen Zeile ab, und diese URL kommt nur von GitHub's Web-Upload: gh, die REST-API und committed Dateien geben stattdessen einen Download-Link. Uploads sind privat für Repository-Mitglieder und werden niemals committed.
node <skill-dir>/scripts/attach-video.mjs <GitHub PR or issue URL> <video.mp4>
- Öffnet ein sichtbares Chromium-Fenster. Wenn der gespeicherte Login abgelaufen ist, zeigt es die GitHub-Anmeldeseite: Der Benutzer meldet sich an. Frage niemals nach dem Passwort und lese niemals die Browser-Cookies.
- Lädt über das Kommentarfeld der Seite hoch; der Kommentar wird nie gepostet. Gibt
ATTACHMENT_URL https://github.com/user-attachments/assets/<uuid>aus. - Merkt sich den Login in
~/.cache/screen-video/gh-profile.FORGET_SESSION=1löscht ihn nach dem Upload. Ein gleichzeitiger Upload wartet auf das Profil (WAITING_FOR_PROFILE).
Setze die URL allein in eine eigene Zeile im Body oder Kommentar (gh pr edit <n> --body-file <file>, gh issue comment <n> --body-file <file>). Um zu bestätigen, überprüfe das body_html des PR auf ein <video src=…private-user-images…>-Tag; ein curl -sI auf die Asset-URL kann 404 ergeben, selbst wenn es abgespielt wird. Größenlimit: 10 MB bei kostenlosen Plänen, 100 MB bei kostenpflichtigen.
Linear-Ticket
Der Linear-MCP lädt direkt hoch; kein Browser nötig.
prepare_attachment_uploadmitissue,filename,contentType(video/mp4) undsize(exakte Bytes:stat -f%z <file>auf macOS,stat -c%sauf Linux).- Innerhalb von 60 s:
curl -X PUT --data-binary @<file> "<uploadRequest.url>"mit jedem Eintrag vonuploadRequest.headers, der wörtlich gesendet wird. Das Ändern der Groß-/Kleinschreibung eines Headers oder das Weglassen eines führt zu 403. Lade eine Datei nach der anderen hoch. create_attachment_from_uploadmitissueundassetUrl.- Um es im Thread abspielbar zu machen, verwende
save_commentmitplus einer einzeiligen Bildunterschrift. Öffne das Ticket einmal, um zu bestätigen, dass es als Player gerendert wird. - Ersetzen eines Videos: Lade das neue hoch, bearbeite denselben Kommentar (
save_commentmitid), lösche dann das alte Attachment mitdelete_attachment.
Slack
Mit dem Slack-MCP, nachdem der Benutzer den Kanal bestätigt hat (Teilen kann nicht rückgängig gemacht werden):
slack_get_file_upload_urlmitfilenameundcontent_length(exakte Bytes).URL='<upload_url>' && curl -X POST "$URL" -H 'Content-Type: video/mp4' --data-binary @<file>. Behalte die URL byteweise bei; eine 401 bedeutet, dass sie verstümmelt oder abgelaufen ist, also hole eine neue.slack_complete_file_uploadmitfile_id,channel_id(eineC…/D…-ID, kein Name; schlage sie mitslack_search_channelsnach), optionalthread_tsundinitial_comment.
Ohne MCP gib dem Benutzer den Dateipfad zum Hineinziehen.
Überall sonst
Übergib den Dateipfad und die Größe; erwähne scale: 1 / CRF=24, wenn das Ziel die Größe begrenzt.