Communitygithub.com

johnwhoyou/screen-video

Nimm eine Bildschirmaufnahme einer laufenden Web-App mit Playwright auf – mit Untertiteln, optionaler Sprachausgabe, flüssigem Cursor, Tasten- und Klickgeräuschen, Zoom und Schwenk, scharf bis 4K – und liefere sie dort ab, wo sie gebraucht wird.

Was ist screen-video?

Das Skill **Screen Video** erstellt professionelle, untertitelte und optional vertonte Bildschirmaufnahmen von Webanwendungen direkt über Playwright und liefert sie an verschiedene Zielorte aus – als einfache `.mp4`-Datei, inline in einem GitHub-PR/-Issue, in einem Linear-Ticket, in Slack oder als Kunden-Tutorial-Video. Es zeichnet nicht den ganzen Bildschirm auf, sondern nutzt Chromes Screencast, um ein flüssiges 60-fps H.264 `.mp4` des Browserinhalts zu generieren und nachträglich mit Untertiteln, Zoom-Effekten, Cursor- und Tastaturgeräuschen sowie KI-Sprachausgabe (über Google Gemini TTS) zu veredeln. Das Skill eignet sich besonders für Demo-Videos, UI/UX-Walkthroughs, Dokumentation von Änderungen, Bug-Reproduktionen oder Onboarding-Tutorials und kann bei Bedarf auch Gmail-E-Mail-Szenen einblenden. Es unterstützt die direkte Einbettung in GitHub (über Browser-Upload), Linear (über MCP), Slack (über MCP) sowie die Bereitstellung als reine Datei. Entwickler sollten die spezifischen Fallstricke wie korrekte Initialisierung, Umgang mit Vollbildmodus, Zoom-Budget und Datenschutz (keine echten Kundendaten) beachten.

Funktioniert mit~Claude Code✓Codex CLI✓Cursor✓Gemini CLI
npx skills add https://github.com/johnwhoyou/screen-video/tree/HEAD/plugins/screen-video/skills/screen-video

In Ihrer bevorzugten KI fragen

Öffnet einen neuen Chat, in dem dieser Agent-Skill bereits geladen ist.

Vorschau

Aus dem Skill-README

walkthrough.mp4

Dokumentation

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:

  1. 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)?
  2. Sprachausgabe? Nur Untertitel oder mit Sprachausgabe (KI-Voice-Over)? Kunden-Videos haben immer eine Sprachausgabe.
  3. 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.
  4. Welche Daten? Welche lokale DB oder welcher Account soll für die Aufnahme verwendet werden. Niemals Produktion, niemals echte Kundendaten.

1. Aufnahme

  1. Starte die App lokal und öffne jede Seite einmal vorher: Andernfalls landet die erste Kompilierung des Dev-Servers im Video.

  2. Schreibe ein Flow-Skript auf Basis von scripts/record-lib.mjs (kopiere examples/playwright-site/flow.mjs, eine ausführbare Aufnahme einer öffentlichen Website). Verwende step(id, caption, action) (oder nacktes caption()), 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 dem click(locator) / hover(locator) des Flows, nicht mit locator.click(): Sie gleiten den gezeichneten Cursor zuerst zum Ziel. Ein nacktes locator.click() funktioniert zwar auch, aber der Cursor springt. zoom(locator, { scale }) zoomt sanft auf ein Ziel heran, unzoom() zoomt wieder heraus, und click(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 zoom fü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. click und type protokollieren jeden Klick und Tastendruck in <video>.sfx.json, und narrate.mjs legt aufgenommene Sounds unter die Stimme: aufgezeichnete Mausklick- und mechanische Tastatur-Samples aus sounds/. SFX_VOLUME skaliert sie (Standard 1), SFX=0 für keine. page.keyboard.type und nacktes locator.click() erzeugen keinen Ton.

  3. node flow.mjs gibt den .mp4-Pfad aus. Es benötigt ffmpeg ($FFMPEG oder 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)

  1. Schreibe script.json: [{ "id": "intro", "say": "…" }, …], eine gesprochene Zeile pro step-ID. Schreibe es für das Gehör: kurze Sätze, Zahlen als Wörter („eintausendfünfhundert“).
  2. GOOGLE_API_KEY=… node <skill-dir>/scripts/tts.mjs script.json <audio-dir> schreibt eine WAV-Datei pro Zeile und durations.json. Gemini 3.8 Flash TTS (TTS_MODEL, TTS_VOICE zum Überschreiben) klang natürlicher als OpenAI gpt-4o-mini-tts. Hole dir einen Schlüssel unter aistudio.google.com.
  3. Nimm mit { narration: '<audio-dir>/durations.json' } auf. Jeder step wartet, bis seine Zeile beendet ist, und die Startzeiten landen in <video>.mp4.marks.json.
  4. node <skill-dir>/scripts/narrate.mjs <video.mp4> <audio-dir> <out.mp4> mischt die Clips und Sounds ein (codiert neu mit CRF, Standard 18). Liefere <out.mp4> aus.

TTS-Tücken:

  • Sende nur die Zeile. Ein Präfix wie „Sage warmherzig: …“ wird vorgelesen, und systemInstruction gibt 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.mjs extrahiert 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 du pressSequentially` 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() über locator.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 = undefined vor 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.com und 555-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.mjs für die Zeilen, die diese Daten beschreiben, erneut auszuführen.
  • zsh trennt $VAR nicht nach Wörtern. Übergib Framzeiten wörtlich an frames.mjs oder verwende eine Schleife unter bash.

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 }) aus scripts/gmail-scene.mjs. Füttere es mit der echten gerenderten E-Mail der App. Es schreibt target="_blank" in _top um, da ein Popup die Aufnahme auf der falschen Seite warten lässt. Antworten ist eingebaut: #reply-btn, #reply-text, #send-btn. Übergib mailbox: 'sam' für mehrere Posteingänge (bereitgestellt unter /sam und /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 nie load. 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=1 lö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.

  1. prepare_attachment_upload mit issue, filename, contentType (video/mp4) und size (exakte Bytes: stat -f%z <file> auf macOS, stat -c%s auf Linux).
  2. Innerhalb von 60 s: curl -X PUT --data-binary @<file> "<uploadRequest.url>" mit jedem Eintrag von uploadRequest.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.
  3. create_attachment_from_upload mit issue und assetUrl.
  4. Um es im Thread abspielbar zu machen, verwende save_comment mit ![walkthrough.mp4](<assetUrl>) plus einer einzeiligen Bildunterschrift. Öffne das Ticket einmal, um zu bestätigen, dass es als Player gerendert wird.
  5. Ersetzen eines Videos: Lade das neue hoch, bearbeite denselben Kommentar (save_comment mit id), lösche dann das alte Attachment mit delete_attachment.

Slack

Mit dem Slack-MCP, nachdem der Benutzer den Kanal bestätigt hat (Teilen kann nicht rückgängig gemacht werden):

  1. slack_get_file_upload_url mit filename und content_length (exakte Bytes).
  2. 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.
  3. slack_complete_file_upload mit file_id, channel_id (eine C…/D…-ID, kein Name; schlage sie mit slack_search_channels nach), optional thread_ts und initial_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.

Verwandte Skills