Skip to content

Export per-step stills (--stills): reuse a render as the screenshot source for help docs #71

Description

@jbrecht

Friction

Help documentation needs screenshots. Producing them by hand means driving the app, framing each screen, cropping, and — worst — re-doing all of it every time the UI changes. Meanwhile a tutorial already encodes exactly that walkthrough, and a render already takes the screenshots we want and then deletes them.

record writes a settled, full-viewport PNG per step to <workDir>/steps/<id>.png (record.ts:194), captured after the narration hold — the frame the viewer sees at the end of the step. It's gated on screenshots: wantContactSheet || options.debug (render.ts:79), consumed only by the contact sheet, and then the work dir is removed (render.ts:113).

So today the only way to get doc-ready stills is --contact-sheet --keep-work-dir and digging through .forge/. This issue makes that a first-class output instead of debris.

Proposal

A --stills option that exports the per-step frames as a durable, named artifact next to the video.

1. Durable destination. Copy steps/*.png to <output dir>/<name>-stills/NN-<step-id>.png, mirroring how GIF and chapter sidecars are emitted. Index-prefixed so docs order matches tutorial order.

2. An index sidecar. JSON (and/or markdown) mapping image → step id → narration. This is the part no generic screenshot tool can offer: the narration is already written alt text / figure captions. A docs author gets

![Click Save to commit the change](tutorial-stills/03-save.png)

essentially for free, and it stays in sync with the video's script.

3. Selection — "key" images. Default to all steps. Then:

  • --stills=<step-ids> (mirrors the existing --gif steps: shape) for a one-off subset, and
  • a per-step still?: true field on Step for tutorials that always want the same few frames.

Flag first; the per-step field can follow.

Design decisions worth settling up front

  • The fake cursor is in the shot. #__forge_cursor__ is a persistent DOM element (cursor.ts:16), so every still has the arrow parked on the step's focused control. For "click this button" docs that's a feature; for clean UI screenshots it isn't. Playwright's page.screenshot({ style }) can hide it per-shot, but it injects CSS mid-recording — small risk of a one-frame blip in the video. Wants a stills.cursor toggle, and a decision on the default (proposed: cursor on, matching the video).
  • Callouts are not in the shot. The highlight ring self-removes ~200ms after the action (callout.ts:28), long before the settled frame. So stills come out clean of rings. Getting callouts into stills would be a separate, larger change (re-draw + capture), explicitly out of scope here.
  • No zoom. Zoom is an ffmpeg filter applied in post (post/zoom.ts), so stills are always the full un-zoomed viewport. If docs want the zoomed crop, reuse computeZoomWindows to crop the PNG — small, but a follow-up.
  • Before vs. after. Stills capture end-of-step state; docs often want the state before the click (the button you're about to press). Debug mode already writes <id>-before.png / <id>-after.png, so a --stills=before,after variant is nearly free. Ship end-of-step first.
  • 1× scale, video aspect. deviceScaleFactor is 1 and the viewport is video-shaped (1920×1080). Fine for most docs, not retina-crisp. True 2× would need a separate post-record screenshot pass, since DSF also affects the video — propose punting.
  • Partial output on failure. Completed steps already have settled screenshots on disk when a later step fails; the partial contact-sheet path (render.ts:126) proves the pattern. Stills should degrade the same way — export what exists, never mask the StepError.

The fast path worth documenting

Because of #50, --phase record runs without TTS. That makes

forge render --phase record --stills

a full screenshot set with no TTS cost and no ffmpeg — the cheap "just give me the images" mode. Should be the documented recipe in the docs section.

Acceptance

  • forge render --stills leaves a directory of per-step PNGs next to the video that survives work-dir cleanup
  • Each image is named so docs order is obvious, and an index sidecar carries step id + narration for alt text
  • forge render --phase record --stills produces the image set without TTS or ffmpeg
  • A failed render still exports the stills for the steps that completed
  • Documented in docs/writing-tutorials.md, including the cursor caveat and the no-zoom/no-callout framing

Implementation sketch

Shaped like the contact-sheet PR (#9): a new pipeline/stills.ts (copy + index emit), option threaded types.tsconfig.ts zod → main.ts flag → render.ts wiring (reusing the existing screenshots: gate), stillsPath/stillsFiles on RenderResult and the CLI summary, unit tests for naming + index generation. The risky parts — capturing settled frames, surviving partial failures — are already built and proven.


Motivated by wanting to reuse tutorial renders as the source of screenshots for help documentation.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    enhancementNew feature or request

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions