CtrlK
BlogDocsLog inGet started
Tessl Logo

slidey-authoring

Author and iterate on declarative videos using the slidey pipeline (a JSON scene spec rendered to a narrated MP4). Use when the user asks to create a new video, add/edit a scene, tweak narration, adjust visuals, re-cut a video, or generate an MP4 from a JSON spec. Covers the iteration loop, scene-type vocabulary, narration budgeting, and the gotchas accumulated across many cuts.

SKILL.md
Quality
Evals
Security

Slidey authoring

Slidey is a deterministic spec-driven Puppeteer + ffmpeg pipeline at the repo root. You describe a video as a JSON array of scenes; slidey renders each scene to PNG frames via headless Chrome, generates narration via edge-tts, and muxes everything into an MP4. Output is reproducible: same JSON spec + same template = byte-identical frames.

Slidey specs use the .slidey.json extension — this is what the file-tree sidebar, the VS Code preview extension, and the MCP workspace tree auto-discover. Specs live in examples/ alongside their output MP4s. The minimal starting point is examples/hello.slidey.json; examples/kitsoki-pitch.slidey.json is a full real-world sample of the legacy scene types, and examples/layout-gallery.slidey.json exercises every variant of the cards/code/table/chart content primitives (copy variants straight from it).

Starting a new video

Create a .slidey.json file in examples/ (or anywhere) with this shape:

{
  "meta": {
    "mode": "pitch",
    "narration": { "voice": "en-AU-NatashaNeural" }
  },
  "scenes": [
    { "type": "title", "title": "My Feature" },
    ...
  ]
}

Then follow the iteration loop below. Name the output MP4 alongside the spec (examples/my-feature.mp4).

The iteration loop

A full MP4 render takes 7–12 minutes. Never trigger one to spot-check a single scene. Use PNG or PDF for all visual iteration — they share the same render bundle and run in seconds.

Build the render bundle first. The visuals are Vue components compiled to dist-render/render.html; all pipelines (MP4/PDF/PNG) load that file. After any change under web/ (or on a fresh checkout), run npm run build:render — otherwise rendering errors with "render bundle missing". --estimate/--list don't need it.

Four outputs, one spec. The output path picks the format:

Output pathFormatSpeedUse for
path/to/dir (no extension)PNG directory~1–3s/sceneVisual spot-check; LLM can Read files directly
.pdfPDF (vector, one page per reveal step)~3s totalFull deck review, sharing
.mp4Video with narration7–12 minFinal deliverable only
--estimate / --listConsole table~50msNarration budget check
--checkConsole validation~instantDiagram-svg sizing/overlap guard (CI-usable)

Don't run two Puppeteer renders at once — concurrent headless Chrome instances can crash each other's screenshot capture.

VS Code preview is available. For local authoring in VS Code, install the extension with make vscode-install-local. It previews .slidey.json, .json, and .jsonl files in a read-only webview using the same viewer as slidey <file>. The extension auto-discovers .slidey.json specs and .jsonl traces; plain .json files can be opened explicitly. If you change the JSON, edit the source file and let the preview reload from disk.

npm run build:render                                 # (re)build the Vue bundle after editing web/

# 1. Sanity-check — no render, ~50ms (no bundle needed)
slidey examples/my-video.slidey.json --estimate

# 1b. Validate diagram-svg sizing/overlap — console only, ~instant, no Chrome (no bundle needed)
slidey examples/my-video.slidey.json out.mp4 --check     # or: node src/index.js my-video.slidey.json out.mp4 --check
# Checks every diagram-svg node's w/h against its text and flags node overlaps.
# Exits non-zero on violations — safe to run anytime, and usable as a CI gate.

# 2. PNG spot-check one scene — ~1-3s, LLM-readable
slidey examples/my-video.slidey.json .artifacts/check --scenes 5
# Produces: .artifacts/check/05-01.png, 05-02.png, 05-03.png (one per reveal step)
# Then: Read .artifacts/check/05-02.png  ← vision model checks layout directly

# 3. Full deck as PDF — ~3s, good for a complete pass before MP4 render
slidey examples/my-video.slidey.json .artifacts/deck.pdf

# 4. Iterate on narration text only — ~10s (reuses cached MP4 frames)
slidey examples/my-video.slidey.json .artifacts/out.mp4 \
  --frames-dir .artifacts/frames \
  --keep-frames \
  --skip-render

# 5. Final full render — 7–12 min, only when layout and narration are confirmed
slidey examples/my-video.slidey.json examples/my-video.mp4

Always run --estimate first. It catches narration overruns (the #1 cause of wasted full renders) and prints scene start times so you don't have to count.

Use PNG for all visual iteration. The LLM can Read PNG files directly (multimodal) — this makes layout review a first-class part of the authoring loop without waiting 30s for an MP4 render per scene.

Scene types

All are declared in JSON; render handlers live in src/scenes/:

typeUse forKey fieldsTypical duration
titleCold open, chapter break (e.g. "vs.")title, subtitle, eyebrow3s
narrativeEyebrow + body + lede text beatseyebrow, body, lede, hold7–9s
diagramASCII/code panel comparison (e.g. "before vs after")panels: [{label, ascii}]10–14s
diagram-svgProper SVG diagrams (the architecturally important visuals)panels: [{viewBox, nodes, edges}]10–15s
mermaidMermaid flowchart / sequence / state diagram rendered as themed SVGsource, scale, caption8–12s
traceMulti-layer lookup cascade with HIT/MISS badgesturns: [{user, layers, intent, no_llm}]12s
threadMocked issue-tracker / review comment threadspanels: [{system, ref, stage, messages}]14–17s
statBig gradient number + captionvalue, label, detail7s
ctaWordmark + tagline + URL end cardwordmark, tagline, url8s
terminal-gifEmbed a recorded gif in a fake-terminal chromegif, title, caption8–12s
videoEmbed a demo MP4, rrweb log, or captured tour — fullscreen or inset in a slide, with chapter captions, annotations, and synced narrationsrc | rrweb | capture, mode, annotations[], narration[]= source length
requestAPI request/response card (live/mock/playback)see src/scenes/request.jsvaries
transcriptFull agent/chat session as per-turn cardsturns: [...]varies
cardsPeer items OR side-by-side contrastvariant, cards[] | left/right | question/answer8–14s
objectivesObjective/status report with large visual status glyphsitems: [{label, status, detail}]8–12s
evidenceStatus-forward checks, artifacts, commands, paths, and logsitems: [{label, status, detail, refType, ref}]8–12s
personasReusable cast intro or persona-attributed use-case rowsmeta.personas, variant, personas[], cases[]8–12s
codeReal text artifacts (source, diff, function I/O, tree, config, log)variant, lang, code, highlight[], annotations[]8–12s
tableData / comparison / scorecard tablesvariant, columns[], rows: [{cells[], highlight}], winner8–12s
chartHand-built SVG charts (bar/line/area/pie/scatter/quadrant)variant, series: [{name, color, points[]}], axes, unit8–12s
imageStatic screenshot / imported SVG / generated visualsrc, fit, caption6–10s
image-compareBefore/after screenshot comparisonleft, right, fit, variant8–12s
bookBook-cover bibliography / recommended readingbooks[] with title, authors, cover, takeaway8–12s
memeMeme-template slide (200+ templates, each knows its caption boxes)template, text[] or fields{}, style, caption6–10s

Choosing a layout — semantic taxonomy

The full catalogue (45 layouts across 9 communicative families) and the design rationale live in docs/layout-taxonomy.md in the repo. Pick a scene by what you're communicating, not by how it looks — that's the whole point of the taxonomy. The four variant-driven primitives below are closed semantic menus: choose the variant whose intent matches, and the renderer handles the pixels.

If you want to…Usevariant
list N peer items (no flow)cardsgrid · list · numbered · icon-row · agenda
weigh X against Y / before↔after / claim↔rebuttalcardsbefore-after · versus · point-counterpoint · pros-cons
stage a question + its answercardsqa
show objective state / done vs issue vs nextobjectivesdone · issue · blocked · next · progress
show evidence state plus rerun/inspect command or pathevidencecommand · artifact · path · log · doc · test
show the literal text of an artifactcodesource · diff · function-io · tree · config · log
compare named options across criteria / show exact valuestablecomparison · scorecard · data
show what the numbers saychartbar (compare) · line/area (trend) · pie (composition) · scatter/quadrant (relate)
show A→B→C flow, hierarchy, or how things connectdiagram-svg(vary rankdir + edge style — see below)
render existing Mermaid sourcemermaidflowchart · sequenceDiagram · stateDiagram-v2
show a real UI state or assetimage / image-comparescreenshot, SVG, before/after
embed a product walkthroughvideosrc MP4 · rrweb log · capture tour
keep role identity consistent across a storypersonascast · use-cases

Disambiguation: list vs flow — if removing the arrows loses nothing, it's cards, not diagram-svg. compare vs quantify — comparing options on qualitiescards/table; comparing measured valueschart. diagram vs code — a conceptual relationship is diagram-svg; the literal text of a file is code. Mermaid vs diagram-svg — use mermaid when the Mermaid text is already the maintained artifact or when a standard sequence/state/flow diagram is enough; use diagram-svg when Slidey needs exact layout control, custom node styling, or --check geometry validation.

cards — peer items & contrast

  • Set variants (grid/list/numbered/icon-row/agenda): cards: [{label, sub, lines:[], style}], optional columns:N. One reveal step per card.
  • Two-column variants (before-after/versus/point-counterpoint/pros-cons): left: {label, lines:[]} and right: {label, lines:[]} — contrasting accents, centre divider. pros-cons auto-glyphs ✓/✗.
  • qa: question: "…", answer: ["…","…"] (string or array of lines).
  • Common: title, caption. Supersedes the legacy ASCII diagram comparison.

objectives — objective status with visual feedback

Use objectives for eval/report decks where the viewer needs to know whether the work is done, in progress, blocked, or has issues. Do not use a generic table for this; status needs a large glyph and color, not just text in a cell.

{
  "type": "objectives",
  "title": "Objective status",
  "items": [
    { "label": "Harness objective", "status": "done",
      "detail": "One local entrypoint and project catalog are in place." },
    { "label": "HTML preview", "status": "issue",
      "detail": "Bundle render is blocked by sandbox write permissions." },
    { "label": "Product-site journey", "status": "next",
      "detail": "Run production web build for A/B walkthroughs." }
  ],
  "caption": "Core harness complete; runtime preview still needs a clean environment."
}

Statuses: done renders a large green checkmark; issue and blocked render a large red exclamation mark; next, progress, pending, and skipped are visually distinct but less final. Keep to six items or fewer.

evidence — checks, artifacts, commands, and paths

Use evidence for report decks where each row answers "what proof exists, what state is it in, and where do I rerun or inspect it?" Do not use a wide table for commands or artifact paths; the evidence layout gives each row a status glyph and keeps the command/path in a monospace chip.

{
  "type": "evidence",
  "title": "Latest check state",
  "items": [
    { "label": "PostgreSQL", "status": "validated",
      "detail": "ALTER DOMAIN oracle proves baseline red and fix green.",
      "refType": "command",
      "ref": "bash tools/product-journey/checks/postgresql-oracle.sh" },
    { "label": "Run log", "status": "implemented",
      "detail": "Chronological job state and lane validation record.",
      "refType": "log",
      "ref": ".context/product-journey-runlog.md" }
  ],
  "caption": "Each evidence row has a visible state plus a concrete reference."
}

Statuses: done, validated, and implemented render green checkmarks; issue and blocked render red exclamation marks; next, progress, pending, and skipped are visually distinct but less final. refType may be command, artifact, path, log, doc, or test. Keep to six rows or fewer.

personas — reusable cast and who-does-what rows

Use personas when a deck has recurring people, roles, stakeholders, or user types and the viewer needs to remember who performs each action. Define the cast once in meta.personas; scenes can reference ids or provide inline persona objects.

{
  "meta": {
    "personas": [
      { "id": "pm", "name": "Priya", "role": "Product manager",
        "intro": "Turns a rough idea into a validated plan.",
        "color": "#58a6ff", "glyph": "PM" },
      { "id": "dev", "name": "Devon", "role": "Engineer",
        "intro": "Turns the plan into reviewable changes.",
        "color": "#7ee787", "glyph": "DE" }
    ]
  },
  "scenes": [
    { "type": "personas", "variant": "cast",
      "title": "The cast", "personas": ["pm", "dev"] },
    { "type": "personas", "variant": "use-cases",
      "title": "Plan to patch",
      "cases": [
        { "who": "pm", "action": "Frames the customer problem",
          "detail": "keeps the deck anchored in a real user" },
        { "who": "dev", "action": "Splits the patch into reviewable pieces",
          "detail": "small enough for agent and human review" }
      ] }
  ]
}
  • meta.personas[]: reusable registry. Each entry needs id; add name, role, intro, color, and glyph for richer cards.
  • variant:"cast": personas is a list of ids or inline objects. Renders cards with avatar, name, role, and intro. columns can override the grid.
  • variant:"use-cases": cases[] rows use who to resolve an avatar, then show action and optional detail.

code — text artifacts

variant, title (renders as a filename chrome bar), lang, code (string with \n). Plus: highlight: [lineNos], annotations: [{line, text}]; function-io uses call/returns; diff colours +/- lines; config tuned for JSON/YAML; log/tree for console + directory listings.

table — tabular data

variant, columns: ["…"], rows: [{cells: ["…"], highlight: colIndex?}], optional winner: colIndex (scorecard crowns it). comparison/scorecard render ✓/✗ glyphs and a highlighted column. Caps: ~6 columns × ~8 rows (excess is clipped to keep reveal-step count in sync). One reveal step per body row.

chart — hand-built SVG charts (no chart lib, deterministic)

variant, title, caption, unit, axes: {x, y}, series: [{name, color, points: [{x, y}]}]. color is a token name (primary/secondary/green/orange/teal/red). One reveal step per series. Pie uses a single series (≤6 slices); quadrant places points by x/y for a 2×2. Guidance: bar for category comparison, line for time trend, pie only for simple composition.

diagram-svg — the workhorse for proper diagrams

The most-used type. Each panel has an array of nodes and edges. auto_layout: true is the default — let dagre compute all x/y positions and the renderer auto-size boxes to fit text. Hand-authored x/y/w/h is a deliberate EXCEPTION, only for layouts dagre can't express (e.g. uniform full-width list rows, or two-panel comparisons with specific spatial semantics). When you DO hand-author coordinates you MUST run --check and verify with a PNG render: the auto-size loop can only grow a box in place — it can't reposition neighbors — so an undersized hand-authored box overlaps the boxes and arrows around it.

Nodes

  • label (big text), sub (medium), lines: [] (smaller lines stacked below)
  • style: "primary" (blue accent) or "secondary" (purple) to highlight the focal node
  • No coordinates needed — auto_layout places them; auto-size expands boxes to fit text

Panels

{
  "auto_layout": true,
  "rankdir": "TB",
  "nodes": [...],
  "edges": [...]
}
  • auto_layout: true — dagre computes all x/y positions; viewBox is auto-computed
  • rankdir: "TB" (default) — top-to-bottom flow; use "LR" for hub-and-spoke diagrams where one node fans out to many children
  • Terminal nodes (no outgoing edges, multiple predecessors) are automatically ranked last

Edges

  • label — text beside the arrow
  • gate: "gate · label text" — replaces the arrow with a dashed orange checkpoint bar
  • elbow: true — orthogonal Z-bend routing instead of straight diagonal. When multiple edges share the same source, bus routing kicks in automatically — all branches share a common trunk line extending from the source, producing a clean tree appearance. No extra flag needed.
  • side: "left"|"right" — offset for parallel bidirectional arrows

Layouts by topology

TopologyrankdirEdge styleExample
Pipeline (A→B→C)TBstraightBugfix rooms
Hub-and-spoke (A→many)LRelbow: trueStory anatomy
Comparisontwo panelsstraightJudge polymorphism
Decision treeTBstraightRoom lifecycle

Single-panel vs two-panel diagrams

Slidey's CSS treats diagram-svg panels differently based on count:

  • Two panels (side-by-side comparison): smaller boxes, smaller fonts. Good for comparisons.
  • Single panel (hero diagram): bigger boxes, bigger fonts, SVG height 680px. Good for standalone diagrams.

If you need both panels visually consistent, use TWO single-panel scenes back-to-back with a title scene transition between them.

mermaid — themed Mermaid diagrams

Use mermaid when you already have Mermaid source, or when the fastest path is a standard Mermaid diagram rather than hand-authored node geometry. Slidey renders the Mermaid output in the Vue bundle with the active deck theme, so PDF output stays vector and the web viewer uses the same component as MP4/PNG rendering.

{ "type": "mermaid",
  "title": "Request lifecycle",
  "source": "flowchart LR\n  browser[Browser] --> api[API]\n  api --> db[(Database)]",
  "scale": 1,
  "caption": "Mermaid source remains the editable artifact." }

Fields:

  • source: Mermaid text. Common choices are flowchart, sequenceDiagram, and stateDiagram-v2.
  • sourceFile: optional provenance path for import/bundling workflows; the renderer uses source.
  • scale: shrink or enlarge dense diagrams without editing the Mermaid text.
  • title, caption, narration, hold: same role as other slide scenes.

Limitations: --check only validates diagram-svg, not Mermaid. If a Mermaid diagram is cramped, split it into multiple scenes or reduce scale; if you need precise edge labels, node sizing, or deterministic geometry checks, convert the concept to diagram-svg.

Media embedding — images, GIFs, MP4, rrweb, and tour capture

Use media scenes when the artifact itself carries the point: screenshots, screen-recorded demos, rrweb bug reports, terminal recordings, or visual diffs.

Static images and screenshot comparisons

{ "type": "image",
  "title": "Architecture",
  "src": "assets/architecture.svg",
  "fit": "contain",
  "caption": "Local assets resolve relative to the spec." }

image accepts local paths, absolute paths, URLs, and data URIs. Use fit:"contain" for diagrams/screenshots that must stay uncropped; use fit:"cover" for full-bleed visuals where cropping is acceptable.

{ "type": "image-compare",
  "title": "Migration fidelity",
  "left":  { "label": "Before", "src": "compare/marp.png" },
  "right": { "label": "After",  "src": "compare/slidey.png" },
  "variant": "qa",
  "caption": "Use for visual reviews and before/after UI checks." }

image-compare is for old/new screenshots. variant:"qa" reduces chrome and enlarges the previews for review decks.

Terminal GIFs

{ "type": "terminal-gif",
  "gif": "assets/run.gif",
  "title": "demo",
  "caption": "A recorded terminal session, framed and captioned." }

Use this for VHS-style terminal recordings. Prefer video for browser/product demos, especially when chapters, annotations, trimming, or narration cues matter.

Demo videos — the video scene + tour capture

Slidey can splice a recorded product demo into a deck. Two halves:

1. Capture a demo (the tour engine)

slidey capture <tour.json> <out.mp4> drives any live web app through a time-based storyboard with headless Chrome and records a deterministic demo MP4 + a <out>.mp4.chapters.json sidecar. It is the generalized, app-agnostic successor to a per-app Playwright recording harness — overlays (a setup curtain, a caption banner, a spotlight + dim) are injected as plain DOM styled from the deck palette, so a captured demo already looks like the rest of the deck.

{
  "target": { "launch": "myapp serve --addr 127.0.0.1:8123", "addr": "127.0.0.1:8123" },
  // or:     { "url": "http://localhost:5173/" }   // app you already serve
  "startPath": "/", "viewport": { "width": 1600, "height": 900 },
  "curtain": "My App", "pace": 1,                  // pace 0 ⇒ 1 frame/step (fast check)
  "steps": [
    { "id": "home", "label": "The home screen", "caption": "Welcome",
      "waitFor": "[data-testid=home]", "dwellMs": 4000, "kind": "explain" },
    { "id": "open", "label": "Open a project", "target": "[data-testid=go]",
      "dwellMs": 2500, "kind": "action", "advance": "click-target" }
  ]
}

Steps: target is a CSS selector (targetText disambiguates by text); kind:"action" clicks the target to advance (advance:"route-match" + advanceUrl waits for a URL change); before:[…] runs off-camera setup actions (goto/click/type/waitFor/wait/eval). Default capture is freeze-frame (settle, screenshot, hold for the dwell) — byte-identical reruns.

rrweb capture (--format rrweb, or an *.rrweb.json output path) records a real-time DOM session log instead: slidey capture <tour.json> out.rrweb.json --format rrwebout.rrweb.json + out.rrweb.json.chapters.json. It captures true motion as compact JSON (not pixels), needs no baked overlays (the log stays a clean app capture), and marks each step with a slidey.chapter custom event. The same log feeds both the baked seek-rasterizer and the live web-viewer player, and it's the same artifact a kitsoki bug report captures (see the library exports below).

2. Embed it (the video scene)

{ "type": "video",
  "src": "demos/tour.mp4",        // a pre-rendered MP4, OR:
  "rrweb": "demos/tour.rrweb.json", // an rrweb DOM-session log, OR:
  "capture": "demos/tour.json",   // capture on the fly, then embed
  "mode": "fullscreen",           // or "embedded" (inset in a slide w/ chrome)
  "fit": "contain",               // contain = letterbox on deck bg; cover = crop
  "start": 0, "end": null, "speed": 1,
  "eyebrow": "Live demo", "title": "My App", "caption": "…",   // embedded chrome
  "chapters": "auto",             // lower-third captions from the sidecar
  "annotations": [ { "at": 4, "until": 7, "text": "Note this", "sub": "…" },
                   { "chapter": "open", "text": "…" } ],
  "narration": [ { "at": 0, "text": "We open on the home screen." },
                 { "chapter": "open", "text": "Now we open a project." } ] }
  • The MP4 is ffmpeg-extracted into the frame sequence (scaled/letterboxed to the deck resolution, or inset for embedded). One ffmpeg pass composites the overlays. The scene's duration equals the video length--estimate probes it (or use a duration hint).
  • Prefer src for fast repeatable deck renders. It can be a Slidey-produced capture, a Kitsoki demo MP4, or any MP4 with an optional sibling <src>.chapters.json.
  • chapters: "auto" (default for src) derives a deck-styled lower-third per chapter from the sidecar; default is OFF for capture (its captions are already baked in). Set false to disable, or a path to an explicit sidecar.
  • annotations (deck-styled, timed by at/until seconds or a chapter id) composite on top — for emphasis on a clean video.
  • narration may be the usual whole-scene string or an array of time-keyed cues ({at|chapter, text}) so the voiceover tracks demo moments.
  • PNG/PDF export shows a representative poster frame (the source video is only made for video output) — so the normal PNG/PDF iteration loop still works.
  • rrweb source ("rrweb": "…rrweb.json"): baked output seek-rasterizes the log via Replayer.goto(t) (real motion, but much slower — opt-in; freeze frame stays default); the web viewer mounts a live, scrubbable, chapter-aware player you can grab to interact with. Chapters come from in-log custom events (or a sibling sidecar). See examples/rrweb-demo.slidey.json.
  • capture source ("capture": "…tour.json"): runs the tour during render. Use this for one-command final production; during iteration, run slidey capture first and embed the resulting MP4 or rrweb log so layout checks do not repeatedly drive the app.

Kitsoki demos already emit this exact chapter-sidecar shape, so an existing …-demo.mp4 + .chapters.json drops straight into a video scene with "chapters": "auto".

3. rrweb as a library for kitsoki

slidey owns the rrweb pieces; kitsoki consumes them via subpath exports (so a bug report and a demo are the same artifact type):

  • slidey/rrweb-buffer — app-agnostic rolling-buffer recorder (the bug-report capture engine; generalized from kitsoki's session-capture.ts).
  • slidey/rrweb-playerRrwebPlayer.vue, themeable (--rrp-* CSS vars) scrubbable player with chapter markers + interactive toggle. Source SFC.
  • slidey/rrweb-format (node) / slidey/rrweb-chapters (browser) — the *.rrweb.json envelope + chapter extraction.

Book-cover bibliography

{ "type": "book",
  "title": "Further reading",
  "books": [
    { "title": "Thinking in Systems",
      "authors": "Donella H. Meadows",
      "year": "2008",
      "cover": "assets/books/thinking-in-systems.jpg",
      "takeaway": "A practical vocabulary for feedback loops." }
  ],
  "caption": "One to three books per scene." }

Use book for recommended reading or bibliography slides. Keep it to one to three books. Each book needs title, authors, cover, and takeaway; subtitle, publisher, year, isbn, and alt are optional. Covers are resolved relative to the spec and validated for useful resolution.

Meme templates

{ "type": "meme",
  "template": "db",
  "title": "Multi-box · landscape",
  "text": ["Old slide types", "Authors", "Meme layouts"],
  "caption": "Distracted Boyfriend" }

meme turns a common meme template into a slide layout. There are 200+ templates (sourced from the open memegen.link catalog); each template knows its own caption boxes, their semantic fields, and its orientation (tall / wide / square all letterbox cleanly onto the stage). Discover templates and build slides through MCP:

  • slidey_meme_search — search by name / keyword / example caption, optionally filter by orientation. Returns each template's id plus its fields and example hints. The id is what you pass as template.
  • slidey_add_meme — insert a meme slide. Fill captions with text (positional, in box order) or fields (keyed by the template's field names). Omit both to seed the template's example captions.

Captions match the deck theme by default with a legibility outline; auto-fit sizes each caption to its box. For the classic look, set "style": { "impact": true } (bold uppercase white with a heavy black outline). Other style overrides: color, stroke, font, uppercase. Blank template images are fetched from memegen.link and cached locally on first render (~/.cache/slidey/memes).

Narration

Voice: en-AU-NatashaNeural (Australian female, set in meta.narration.voice). Speech rate calibrates to ~1.85 words/sec for budget estimation — measured across many cuts (real range 1.7–2.3 depending on punctuation and word length).

Budgeting

Audio must be shorter than its scene. Margin of >0.6s is comfortable; 0–0.6s is tight; negative is overrun (audio bleeds into the next scene). --estimate flags all three.

Per-scene word budget formula: max_words ≈ (scene_duration_seconds × 1.5). Conservative; gives ~70% scene utilisation. For a 10s scene, plan ~15 words.

Pronunciation tips

Edge TTS ignores SSML <phoneme> tags (the read-aloud endpoint strips custom markup), so the only reliable fix for a mispronounced word is to respell it phonetically. Two ways:

  • Inline in the narration text for one-offs.
  • meta.narration.pronunciations — a { "term": "respelling" } map applied whole-word and case-insensitively to the spoken text only (the narration shown in specs/--list stays clean). Define a tricky term once and it's fixed in every scene. Longest terms win, so "API key" can override "API". Example:
    "narration": { "voice": "en-AU-NatashaNeural",
      "pronunciations": { "Anthropic": "an-THROP-ik", "SDLC": "S D L C", "kitsoki": "kit-SOH-kee" } }
    --estimate accounts for respellings, so a multi-word expansion (e.g. SDLC → S D L C) correctly shows the extra duration.
  • URLs read literally. Write them phonetically: "github dot com slash org slash repo". Even then, URLs are slow (3–5s for a single short URL) — often better to drop the URL from narration and let it appear only on the visual.
  • Em-dashes () create a natural pause (~0.4s). Useful for pacing but eats into your budget.
  • Numbers: write "seventy-eight percent" not "78%" — TTS reads it the same way but writing it out makes the budget more predictable.

Iteration

When narration overruns, two fixes:

  1. Trim the text (preferred — usually the script was over-detailed)
  2. Extend the scene's hold (in frames; 30 frames = 1s)

If you only change narration text:

node src/index.js spec.slidey.json .artifacts/out.mp4 --frames-dir .artifacts/frames --keep-frames --skip-render

This regenerates audio + remuxes onto cached frames — ~10s instead of 7min.

Common gotchas (battle-tested)

Text width vs box width

For monospace at viewBox-unit font-size F, character width is approximately 0.6 × F (in the same viewBox units). For SVG nodes in diagram-svg:

ModeLabel fontChar widthMargin needed (each side)
Two-panel30~18 vbox~20 vbox
Single-panel44~26 vbox~30 vbox

So a 12-character label in single-panel mode needs box width ≥ 12 × 26 + 60 = 372 viewBox units.

The sub and lines[] fonts are SMALLER than the label, so the label usually dominates box width — but a long sub/line can still overflow: single-panel is sub 28 / line 26, two-panel is sub 19 / line 18. Note sub renders as a SINGLE line (it is NOT auto-wrapped on " · "); for stacked multi-line content use the lines: [] array, one entry per line.

Run --check to catch sizing/overlap automatically instead of eyeballing this.

Title/caption clipping

If a scene's content overflows the available stage height (1080px − 192px padding = 888px on screen), overflow: hidden clips top and bottom equally, pushing the title against the top edge and clipping the caption.

For two-panel diagrams: the SVG aspect-ratio is 1/0.7, so each panel SVG is ~70% as tall as it is wide. The CONTENT inside scales to fit via preserveAspectRatio="meet". Make panel content fit by either (a) reducing content lines per node, or (b) reducing SVG aspect via the panel's viewBox.

For single-panel diagrams: SVG is hard-capped at 680px height. Title (40px) + caption (32–36px wrapped to 2 lines) + gaps eats ~200px, leaving ~680px for the SVG itself. If content needs more, you've overpacked the scene.

"mode": "pitch" is required in meta

Without "mode": "pitch", the template keeps #pitch-stage hidden (display: none) — every page of every PDF and every video frame renders as a blank background. Always include it:

{ "meta": { "mode": "pitch", "narration": { "voice": "en-AU-NatashaNeural" } } }

The --scenes flag rewrites the output file

--scenes 4 scopes any output format to only scene 4. Works for PNG, PDF, and MP4. When spot-checking, prefer PNG (not MP4):

slidey spec.slidey.json .artifacts/check --scenes 4     # PNG, ~2s, LLM-readable
slidey spec.slidey.json .artifacts/check.pdf --scenes 4 # PDF, ~2s, viewer-friendly
# MP4 is ~30s and produces audio you don't need during layout iteration

Edge label overflow on diagrams

Edge labels render TO THE SIDE of the line. If the label is wider than the gap to the panel edge, it gets clipped by the SVG viewBox boundary. Keep edge labels under ~16 characters, or move the content to a panel caption.

Headless Chrome doesn't render colour emoji

🟢 renders as a tofu box. Use plain Unicode (, , ) or descriptive text instead.

Two render processes can race for the output file

Check ps aux | grep "node src/index" before launching a new render to avoid corrupt/truncated output.

File map

slidey/
├── src/
│   ├── index.js              # CLI entry point — argv parsing, orchestration
│   ├── renderer.js           # Per-scene dispatch; tracks scene start frames
│   ├── assembler.js          # ffmpeg invocation; muxes audio if segments supplied
│   ├── narration.js          # Calls edge-tts CLI per scene; bundles segments
│   ├── runner.js             # Live-HTTP runner for `request` scenes
│   ├── timing.js             # Frame counts per state name + estimateScene/estimateBoundaries
│   ├── video.js              # ffmpeg helpers for the `video` scene (probe/extract/poster)
│   ├── overlay-render.js     # Deck-styled transparent overlay PNGs (captions/chrome)
│   ├── rrweb-format.js       # *.rrweb.json envelope + chaptersFromEvents (node; exported)
│   ├── rrweb-render.js       # rrweb baked rasterizer (Replayer.goto→frames) + poster
│   ├── tour/                 # Tour-capture engine for `slidey capture` (app-agnostic)
│   │   ├── index.js          #   capture → ffmpeg MP4 + chapter sidecar (+ rrweb log)
│   │   ├── capture.js        #   Puppeteer freeze-frame driver
│   │   ├── rrweb-capture.js  #   real-time rrweb capture driver (--format rrweb)
│   │   ├── overlays.js       #   portable curtain/caption/spotlight (deck theme)
│   │   ├── launch.js         #   optional spawn + health-poll of the target app
│   │   └── chapters.js       #   ChapterRecorder + producer-agnostic sidecar
│   └── scenes/
│       ├── title.js
│       ├── narrative.js
│       ├── diagram.js          # ASCII (legacy comparison)
│       ├── diagram-svg.js      # Proper SVG (the workhorse)
│       ├── mermaid.js          # Mermaid source rendered as themed SVG
│       ├── terminal-gif.js
│       ├── trace.js
│       ├── thread.js
│       ├── stat.js
│       ├── cta.js
│       ├── request.js          # API request/response card
│       ├── transcript.js
│       ├── cards.js            # peer items / contrast (see web/components/CardsScene.vue)
│       ├── personas.js         # reusable cast / persona-attributed use cases
│       ├── code.js             # text artifacts (see web/components/CodeScene.vue)
│       ├── table.js            # tabular data (see web/components/TableScene.vue)
│       ├── chart.js            # SVG charts (see web/components/ChartScene.vue)
│       ├── image.js            # static screenshot / imported image
│       ├── image-compare.js    # side-by-side screenshot comparison
│       ├── book.js             # book-cover bibliography
│       └── video.js            # `video` scene (MP4 / rrweb source; overlays; cues)
├── web/rrweb/                # rrweb library — exported as slidey/rrweb-* subpaths
│   ├── buffer.js             #   rolling-buffer recorder (→ slidey/rrweb-buffer)
│   ├── RrwebPlayer.vue       #   themeable scrubbable player (→ slidey/rrweb-player)
│   └── chapters.js           #   browser chapter extractor (→ slidey/rrweb-chapters)
└── examples/
    ├── hello.slidey.json          # Minimal starting spec
    ├── kitsoki-pitch.slidey.json  # Full real-world sample (every legacy scene type; safe to delete)
    ├── layout-gallery.slidey.json # Exercises every cards/code/table/chart variant — copy from here
    ├── rrweb-demo.slidey.json     # `video` scene with an rrweb source (live player + baked)
    └── <name>.slidey.json         # Additional specs live here

The reveal-driven visuals are Vue components in web/components/<Name>Scene.vue, compiled to dist-render/render.html by npm run build:render. The src/scenes/<type>.js module is the thin Puppeteer driver. See docs/layout-taxonomy.md for the full layout catalogue and the primitive-vs-preset architecture.

To add a new scene type (the Vue components in web/ are the one renderer the viewer and every headless export share):

  1. Add the visual as web/components/<Name>Scene.vue (reads store.scene; mark reveal targets with the reveal class + store.isRevealed('<id>'))
  2. Register it in web/components/DeckHost.vue's scene-dispatch map
  3. Add the reveal-step list (stepsForScene) and the slidey.show<Name> call (applyShow) for the type in web/sceneSteps.mjs
  4. Add show<Name>(scene) + any reveal ids to web/store.js (and the PITCH_REVEALS step→id table)
  5. Add the thin Puppeteer driver src/scenes/<name>.js ({ render(page, scene, ctx) }) and register it in src/renderer.js's SCENE_MODULES
  6. Add reveal state names + an estimateScene() branch in src/timing.js so --estimate knows the duration
  7. npm run build:render to rebuild the headless bundle (or just run any export — it auto-rebuilds when web/ is newer; see src/render-bundle.js)

Render pipeline (one-line summary)

spec.slidey.json → renderer.js (Puppeteer screenshots) → frames/*.png → [narration.js (edge-tts) → audio/*.mp3] → assembler.js (ffmpeg) → output.mp4

Determinism comes from: (1) viewport size fixed at 1920×1080, (2) timing in frame counts not seconds, (3) JSON spec — no LLM in the rendering loop, ever.

Iteration workflow

  1. slidey examples/my-video.slidey.json --list — see what scenes exist and page counts.
  2. Edit the spec JSON.
  3. slidey examples/my-video.slidey.json --estimate — verify no narration overrun.
  4. slidey examples/my-video.slidey.json out.mp4 --check — validate diagram-svg sizing/overlap (~instant, no render; exits non-zero on violations, so it doubles as a CI gate).
  5. slidey examples/my-video.slidey.json .artifacts/check --scenes N — PNG spot-check (~1-3s). Read the output files directly to review layout with vision.
  6. Repeat 2–5 until the scene looks right.
  7. slidey examples/my-video.slidey.json .artifacts/deck.pdf — full pass as PDF (~3s) to review the whole deck in sequence.
  8. slidey examples/my-video.slidey.json examples/my-video.mp4 — final MP4 render only when layout and narration are confirmed.

Never render MP4 to "see if it works." PNG or PDF first.

Repository
constructorfabric/studio-slidey
Last updated
First committed

Is this your skill?

If you maintain this skill, you can claim it as your own. Once claimed, you can manage eval scenarios, bundle related skills, attach documentation or rules, and ensure cross-agent compatibility.