Demo-Videos aufnehmen
Schreiben Sie ein ad-hoc Playwright-Storyboard, führen Sie dann die eigenständige Engine von jedem Arbeitsverzeichnis aus.
Fügen Sie das Storyboard nicht zu einem Anwendungs-Repository hinzu, es sei denn, der Benutzer wünscht dies.
Halten Sie das Storyboard eigenständig; die Runner-Aufrufe dieser Datei innerhalb der Engine, bevor
Playwright sie lädt. Verwenden Sie daher keine storyboard-relativen Hilfsimporte.
<skill-dir> bezeichnet das Verzeichnis, in das dieses Repository geklont ist, z.B.
~/.claude/skills/record-demo.
Storyboard schreiben
Importieren Sie die DSL mit diesem portablen Import:
import { demo } from "record-demo/storyboard";
Der Runner schreibt diesen Bezeichner beim Bereitstellen in den tatsächlichen Pfad der Engine um, so dass die
Storyboard-Datei überall liegen kann. Die Editor-Typauflösung für
record-demo/storyboard erfolgt über engine/tsconfig.json paths – außerhalb des
Repos, mappen Sie record-demo/storyboard auf den absoluten
<skill-dir>/engine/lib/storyboard.ts-Pfad in Ihrer tsconfig paths, oder behalten Sie die
Datei in engine/scripts/.
Verwenden Sie engine/scripts/example-site.demo.ts als Referenz. Bevorzugen Sie getByRole-Selektoren,
verwenden Sie d.click(locator) anstelle von locator.click(), steuern Sie Aktionen mit d.hold(...) und
behaupten oder warten Sie auf den Endzustand.
demo("storyboard-title", async (d) => {
await d.seed(async (page) => {
await page.getByRole("heading", { name: "Example Domain" }).waitFor();
});
await d.step("Seite lesen", async (page) => {
const heading = page.getByRole("heading", { name: "Example Domain" });
await d.zoomTo(heading, { padding: 48 });
await d.hold(2_000);
d.zoomOut();
await heading.waitFor({ state: "visible" });
});
});
Die Engine überschreibt den Timeline-Namen mit --name oder dem Skript-Basename, daher muss der
an demo(...) übergebene String nicht mit dem Ausgabe-Slug übereinstimmen.
Vorbereitung (vor jeder Aufnahme)
Bei Aufnahmen gegen ein lokal bereitgestelltes Checkout: git fetch origin im bereitstellenden
Checkout ausführen und HEAD == origin/main (oder den benannten Branch) behaupten – ein veraltetes Checkout
zeichnet ein veraltetes Design auf. Falls veraltet, zuerst den Server neu erstellen/neu starten.
Den bereitstellenden SHA in die Aufnahme-Notizen drucken, damit jede Aufnahme zuordenbar ist.
Aufnehmen
Erfordert Node >= 22.18 (nvm oder system), ffmpeg, Xvfb und xdpyinfo. Abhängigkeiten
einmal innerhalb von engine/ mit pnpm install installieren.
node <skill-dir>/engine/record.mjs \
--script /absoluter/oder/cwd-relativer/demo.demo.ts \
--url https://example.org \
[--name output-slug] [--formats 16x9,1x1,9x16] \
[--chrome none|macos|browser] [--storage-state <pfad>] [--raw]
Verwenden Sie --formats, um eines oder mehrere Kompositions-Presets auszuwählen (Standard 16x9) und
--chrome, um das gerenderte Fenster-Frame zu wählen (Standard none). Die Formate werden in der
angeforderten Reihenfolge gerendert, und das erste wird für die GIF-Codierung verwendet.
Das 9x16-Format ist ein aggressiver Center-Crop (etwa 44% der Seitenbreite ist in der Totale sichtbar), halten Sie daher die Aktion zentriert, wenn Sie für Hochformat schreiben.
Verwenden Sie --raw, um nach der sauberen Aufnahme zu stoppen. Ohne werden diese Dateien erwartet:
engine/out/<name>-raw.mp4engine/out/<name>-<format>.mp4für jedes angeforderte Formatengine/out/<name>.gif, codiert aus dem primären Format
Der Runner gibt die absoluten Dateipfade aus, wenn er fertig ist. Behandeln Sie die Reproduzierbarkeit der Live-Website als Verantwortung der Zielseite und betten Sie niemals echte Anmeldedaten in ein Storyboard ein.
Authentifizierte Websites
Zeichnen Sie authentifizierte Abläufe nur mit Staging- oder Testkonten auf; verwenden Sie niemals Produktionsanmeldedaten.
Der Agent sollte sich zuerst einmalig mit seinen eigenen Werkzeugen anmelden, unter Verwendung eines headless Playwright-Skripts.
Wenn die Anmeldung ein per E-Mail gesendetes OTP oder einen Magic Link erfordert, ist der bevorzugte
Weg der E-Mail-Zugriff des Agenten – ein verbundener Gmail-Connector/MCP auf Claude Code,
oder ein Mail-CLI auf anderen Laufzeiten – damit er den Code selbst liest; ansonsten den Benutzer bitten,
den Code einzufügen. Nach der Anmeldung die Sitzung speichern mit
await context.storageState({ path: storageStatePath }), wobei storageStatePath
ein einmaliger temporärer Pfad ist, erstellt mit
storageStatePath="$(mktemp -d)/<konto>.storage-state.json". Dann den Aufnahmebefehl
mit --storage-state "$storageStatePath" ausführen.
Playwrights storageState stellt nur Cookies und localStorage wieder her. Apps, die Auth
in IndexedDB oder sessionStorage speichern, zeichnen stillschweigend einen abgemeldeten Bildschirm auf. Verifizieren Sie daher
die authentifizierte Seite vor der Aufnahme. Die Storage-State-Datei ist ein Geheimnis: niemals drucken
oder committen. Der Rekorder löscht sie sofort nach dem Laden; es ist eine einmalige
Übergabe, daher erfordert eine erneute Aufnahme eine neue Anmeldung und es verbleiben keine auth-bezogenen Daten auf der
Festplatte. Das einmalige Anmelde-Hilfsskript enthält ebenfalls Anmeldedaten: halten Sie es temporär und
committen Sie es niemals.
Aufgenommenes Demo in Remotion Studio bearbeiten
Nach der Aufnahme ein eigenständiges Projekt exportieren, dann Studio installieren und öffnen:
node <skill-dir>/engine/scripts/export-project.mjs <name>
cd <skill-dir>/engine/out/<name>-project
pnpm install
pnpm studio
Das Timeline-Panel von Studio zeigt Editor-Spuren (Bildschirm-Filmstreifen, ein benannter Clip pro
Untertitel, Spotlight, Zoom-Bewegung und Outro) – ziehen Sie die Abspielposition, um zu spulen, klicken Sie auf die Zeile eines Clips, um
dessen Ereignis zu finden. Verwenden Sie das Eigenschaften-Panel oder fixtures/timeline.json, um Untertitel,
Zoom-Timing (Zoom-Bewegungen bei oder über der 1650ms-Untergrenze halten), Spotlight-Timing und den
Outro-Titel zu bearbeiten. Bearbeiten Sie
remotion/theme.ts für Farben, Typografie, Abstände und Bewegung. Rendern Sie das fertige
16:9-Video mit pnpm render.
Bewegungs- und Storyboard-Regeln (wann ein Clip postbar ist)
- Eine Kamerabewegung pro Szene, nicht pro Feld. Einmal in den Bereich zoomen, darin agieren. Zooms pro Eingabe wirken wie scharfkantige Schnitte.
- Bei Seitenübergängen weit einblenden, bei spärlichen Seiten weit bleiben.
d.zoomOut()VOR einem Klick, der navigiert; niemals auf eine Status-Pille auf einer fast leeren Seite zoomen. - Bereiche am unteren Seitenrand:
d.zoomTo([...locators], { scroll: "center", padding: 48 })– die Kamera-Klammer kann ein Rechteck am Seitenrand nicht zentrieren; Scroll-Zentrierung behebt dies an der ersten Stelle.Locator[]rahmt einen gesamten Bereich über die vereinigte Bounding-Box. d.spotlight([...locators])= Apple-artiger Fokus-Dim (Ausschnitt + Ring). GroßzügigeholdMs, explizit beenden mitd.spotlightOff()+hold(600)vor Schrittsende. Spotlight-Rechtecke müssen in den Viewport passen (erzwungen); Zoom-Rechtecke dürfen das Design überschreiten.- Seed wartet auf echte Daten, nicht nur auf das Triggerelement – sonst öffnet das Video auf einem Platzhalter. Warten Sie auf einen bekannten Content-Knoten + ~600ms.
d.settle(locator)vor Ergebnis-Untertiteln. Ansichten, die nach dem Absenden neu laden, flackern kurz; ein einfacheswaitForlöst beim ersten Malen aus und der Ergebnis-Untertitel erscheint über einer weißen Seite.- Untertitel sind Imperative an die Cursor-Aktion gebunden; setzen Sie einen Ergebnis-Untertitel
(
d.caption(...)spät im Schritt) auf Abschlussszenen; kürzen Sie Schritt-Untertitel, so dass Imperative niemals über abgeschlossenen Aktionen hängen. d.outro(title)schließt postbare Clips mit einer ruhigen Endtafel nach der letzten Szene.- Endzustand immer behaupten – ohne dies rendert die Pipeline ein poliertes Video eines defekten Ablaufs.
- Versenden Sie niemals einen Clip mit einer localhost-URL, einem internen Hostnamen, einem Anbieternamen oder echten Anmeldedaten auf dem Bildschirm. Maskieren Sie präsentationsrelevante Strings im Seed (window.open Patch und MutationObserver Input-Neuschreibung).
Vor der Meldung überprüfen
pnpm typecheck && pnpm testinengine/.node scripts/score-motion.mjs out/timeline.json– FAIL blockt; WARN Zoom-Rate ~0.78/s ist der akzeptierte stationäre Zustand; jedes Diskontinuitäts-Unteil ist ein Kamera-Fehler; Dead-Air-WARNs bedeuten einen statischen Abschnitt >= 3,5s mit unter 40px Cursor-Bewegung – neu takten oder untertiteln.node scripts/prove-composition.mjsnach Engine-Änderungen.- Den tatsächlichen Render ansehen:
ffmpeg -i out/<name>-16x9.mp4 -vf fps=1 out/frames/%03d.pngund die Bilder lesen. Zoom-Beschneidung, Untertitel-Verzögerung, Ausschnitts-Desync und Entwicklungs-URLs auf dem Bildschirm sind in einem grünen Testlauf unsichtbar.
Fallstricke
| Symptom | Ursache / Behebung |
|---|---|
Abbruch wegen Invalid demo viewport ... | Geometrie abgedriftet – Launch-Flags korrigieren, nie die Behauptung lockern. |
| Cursor-Offset ~174px vertikal | Chrome lief als Tab, nicht --app. |
| Klappenrahmen nicht gefunden | Detektor-Toleranz ist 64 (externe Aufnahmen sind durchschnittlich unrein); falls weiterhin fehlend, prüfen ob die Aufnahme nicht schwarz ist (Chrome auf falschem DISPLAY). |
| Untertitel/Cursor driften über den Clip | Klappen-Synchronisation – nicht per Wanduhr subtrahieren. |
| Spotlight-Ausschnitt neben Ziel | Rect hat mit einem sanften Scrollen um die Wette gerungen – die Stabilitätsschleife von measureTarget sollte es halten; falls wiederholt, dessen Timeout erhöhen. |
| Wörter am Crop-Rand durchtrennt | zoomTo(locator, { padding: 120 }) für Modale. |
| Zoom schneidet Inhalte aus dem Frame | maximumScale Geschmacksproblem (1,45 funktioniert), kein Klammerfehler. |
Engine-Änderungen verifizieren
Ausführen von engine/:
pnpm typecheck && pnpm test
node scripts/prove-composition.mjs