Desktop Video Proofs¶
Desktop automation can optionally record a short MP4 proof for macOS local targets. Video is for UX interactions where a still screenshot or before/after diff does not explain enough.
Developer convenience, not an SDK feature
The video-proof composer is developer-only tooling in tools/local-ci
for reviewing automation runs. It is not part of the Pulp SDK and is never
shipped in end-user plugins. Pulp commits only its own glue source; the
third-party packages it drives — Remotion (source-available) and
ffmpeg-static (GPL/LGPL binaries), or a system PULP_FFMPEG — are
installed on demand via npm into a gitignored node_modules, never
vendored, and used under their own licenses. See
licensing.
Requirements¶
- The desktop target should enable optional
video_capture. - The runner needs ffmpeg, either from
PULP_FFMPEG,PATH, or the repo-local developer install.
macOS permissions (two are required)¶
The controlling app — Terminal.app when you use --run-in-terminal — needs
both of these in System Settings > Privacy & Security:
- Screen Recording — to capture pixels (screencapture + AVFoundation).
- Accessibility — to post real cursor clicks (
--click/ view-targeted clicks) and raise the captured window. Not needed for--pulp-app-automationclicks, which inject in-app without a visible cursor.
Enable Terminal.app under each, then quit and reopen Terminal (macOS only
applies these to newly launched processes). pulp ci-local desktop doctor mac
and desktop video-doctor mac report a screencapture check (Screen Recording)
and an accessibility check, and the interaction path fails fast with the exact
remediation when Accessibility is missing — so missing permissions are detected
and reported, never silently skipped.
Choosing a recorder for motion¶
--video-recorder avfoundation records the live display and captures continuous
motion (animations, a cursor moving, a control changing). screencapture -l
(the frame-sequence path) reliably captures discrete state changes but not
continuous motion from GPU/CAMetalLayer windows — use AVFoundation when the
proof is about motion. AVFoundation crops to the window using the probe's screen
backing-scale factor, so Retina (2×) captures land on the correct region.
Full vs zoomed framing — --video-focus¶
For an interaction proof (a click with a known target), the composed proof can either zoom into the control that changed or show the whole window:
--video-focus auto(default) — embed a focus clip zoomed to the clicked control so a small change (a toggle flip, a knob move) fills the frame and is obviously visible.--video-focus off— keep the full-window framing (embed the whole captured window); use this when the surrounding layout/context is the point.
The tap marker and zoom-center overlays are placed correctly in either mode — a single decision in the recorder drives both what is embedded and how the overlay coordinates are mapped, so they always match what is shown.
Install Model¶
The feature-branch setup below still supports direct source-tree commands while the workflow is being reviewed. The intended user-facing install shape is now an optional Pulp tool add-on:
pulp tool install video-proof
pulp tool info video-proof --json
pulp tool doctor video-proof --run
pulp ci-local desktop video-setup mac --check
The video-proof tool is not a normal pulp add package. pulp add is for
project dependencies that update a project's package lock file, generated CMake
wiring, dependency metadata, and notices. Validation video proof capture is
machine-level developer tooling: it needs host permissions, ffmpeg, Node/npm,
Remotion composition, Terminal.app permission handoff on macOS, and optional
host/simulator integrations. Those dependencies should stay outside user
projects and outside the core runtime.
The long-term split should stay narrow:
- Core Pulp/local-CI keeps the command surface, run manifest schema, report schema, attachment budget logic, and clear "video-proof tooling is not installed" remediation text.
- The optional
video-prooftool owns ffmpeg/Remotion bootstrap, composition templates, renderer scripts, and any large or license-sensitive developer dependencies. - On the feature branch,
pulp tool install video-proofinstalls the repo-localtools/local-cinpm package and writes a managed wrapper under~/.pulp/tools/npm-packages/video-proof/. That wrapper defaults to the Remotion smoke proof when run without arguments. pulp tool info video-proof --jsonexposes this as a machine-scopedtool_addonwithpackage_format: not_pulp_addand, on this feature branch,artifact_status: source_tree_iteration. The same payload includesartifact_pack_command,artifact_pack_npm_script,artifact_verify_command, andartifact_manifest_schemaso setup automation can discover the artifact review step without scraping this document.- The source-tree developer path can still use
npm --prefix tools/local-ci installfor direct iteration. - Before mainline release, produce a reviewable source add-on artifact with
python3 tools/local-ci/pack_video_proof_tool.py --json, then verify the generated manifest with--verify. The artifact is a small zip plus manifest that contains only Pulp-owned package source,package-lock.json, Remotion template source, and wrapper scripts; it explicitly excludesnode_modules, Remotion caches, and generated videos. - Reusable demo scenarios or source material may later be distributed as reviewed Pulp kits/content packs, but the recorder/composer itself should remain a tool add-on.
This keeps normal Pulp installs small, avoids installing Remotion or ffmpeg unless the user asks for video evidence, and gives Remotion the same developer-supplied dependency treatment as other optional SDK/tool integrations.
Print the portable first-run checklist for another Mac, such as blackbook:
Use --json for a handoff artifact and --check --run-in-terminal to include
the current video-doctor readiness payload via Terminal.app's Screen Recording
grant:
python3 tools/local-ci/local_ci.py desktop video-setup mac --machine blackbook --check --run-in-terminal --json
On a fresh checkout, add --init-config to create the ignored local config from
tools/local-ci/config.example.json before setup checks run:
python3 tools/local-ci/local_ci.py desktop video-setup mac \
--machine blackbook \
--init-config \
--enable-video-capture \
--check \
--json
Add --check-tool-addon when the setup run should also prove the eventual
managed pulp tool install video-proof path. If pulp on PATH is not the
fresh branch CLI, pass --pulp-command or set PULP_CLI:
python3 tools/local-ci/local_ci.py desktop video-setup mac \
--machine blackbook \
--init-config \
--enable-video-capture \
--check \
--check-tool-addon \
--pulp-command ./build-video-proof-cli/tools/cli/pulp-cpp \
--run-in-terminal \
--json
With --check, the setup report includes a separate
setup_prerequisites block for machine-level setup tools (pulp, npm,
node, and cmake) before the video-doctor capture readiness payload. This
lets a fresh Mac fail with actionable install remediations even if the later
screen-capture checks are not yet meaningful. --enable-video-capture is the
explicit local-config opt-in for recording on that target. With
--check-tool-addon, the report also includes a tool_addon block for
pulp tool info video-proof --json and pulp tool doctor video-proof --run.
To inspect another Mac without changing its checkout, add --probe-host with
an SSH host alias. The command runs read-only command -v probes and emits a
remote_setup_prerequisites block. Remote probes run through zsh -lc with
$HOME/.local/bin, /opt/homebrew/bin, and /usr/local/bin prepended, so
Homebrew or local tool installs are visible even when raw non-interactive SSH
would not find node or npm:
python3 tools/local-ci/local_ci.py desktop video-setup mac \
--machine blackbook \
--check \
--probe-host blackbook \
--skip-remotion-smoke \
--json
When the setup report should also prove that a specific design-parity
comparison is runnable, pass the same concrete inputs used by video-matrix:
python3 tools/local-ci/local_ci.py desktop video-setup mac \
--machine blackbook \
--check \
--design-parity-manifest /path/to/run/manifest.json \
--design-parity-source-image planning/screenshots/reference.png \
--json
If you prefer to create the machine-local config manually:
Install and validate the optional video-proof tool on a new Mac:
For direct feature-branch iteration, the equivalent source-tree install is
npm --prefix tools/local-ci install, and the source-tree smoke check is
npm --prefix tools/local-ci run smoke-video-proof.
Pack the current source-tree tool payload for review before changing the registry from source-tree iteration to a versioned add-on artifact:
SOURCE_DATE_EPOCH=0 python3 tools/local-ci/pack_video_proof_tool.py \
--output-dir /tmp/pulp-video-proof-tool-pack \
--version 0.0.0-local \
--json
python3 tools/local-ci/pack_video_proof_tool.py \
--verify /tmp/pulp-video-proof-tool-pack/pulp-video-proof-tool-0.0.0-local.manifest.json \
--json
pulp tool install video-proof \
--artifact-manifest /tmp/pulp-video-proof-tool-pack/pulp-video-proof-tool-0.0.0-local.manifest.json \
--force
The generated manifest records the zip path, sha256, size, included files,
Remotion/ffmpeg-static npm pins, and the policy flags that keep this machine
tool separate from core Pulp and pulp add. The verifier checks the manifest
schema, install policy, archive size and sha256, zip entry list, required
payload files, excluded generated/dependency paths, and required npm scripts and
dependency pins.
The --artifact-manifest install path is an explicit review lane for verified
local artifacts. The default pulp tool install video-proof path remains the
source-tree npm install on this feature branch until humans approve switching
the registry to a versioned add-on source.
Validate the eventual optional tool install path from a fresh source checkout:
cmake -S . -B build-video-proof-cli \
-DCMAKE_BUILD_TYPE=Release \
-DPULP_BUILD_TESTS=ON \
-DPULP_BUILD_EXAMPLES=OFF
cmake --build build-video-proof-cli --target pulp-cli pulp-test-cli-tool-registry -j$(sysctl -n hw.ncpu)
./build-video-proof-cli/test/pulp-test-cli-tool-registry '~[slow]'
tmp_home=$(mktemp -d /tmp/pulp-video-proof-tool-home.XXXXXX)
PULP_HOME="$tmp_home" ./build-video-proof-cli/tools/cli/pulp-cpp tool install video-proof --force
PULP_HOME="$tmp_home" ./build-video-proof-cli/tools/cli/pulp-cpp tool doctor video-proof
PULP_HOME="$tmp_home" ./build-video-proof-cli/tools/cli/pulp-cpp tool doctor video-proof --run
rm -rf "$tmp_home"
PULP_BUILD_EXAMPLES=OFF keeps this scoped to the CLI/tool-registry path on
fresh machines that do not have binary Skia installed yet. Do not set
PULP_ENABLE_GPU=OFF for this smoke: the C++ CLI target is only generated in the
desktop GPU build graph.
Verify the Remotion/ffmpeg composition stack without Screen Recording permission:
Enable video capture for the mac target:
Prepare the mac desktop target receipt:
Run the video-specific readiness gate:
For a faster config/tooling check that skips the Remotion smoke render:
For a host/plugin recipe, include the recipe details so setup problems are reported before recording starts:
python3 tools/local-ci/local_ci.py desktop video-doctor mac \
--recipe reaper-plugin-editor \
--plugin PulpSynth \
--plugin-format clap
video-doctor should show PASS backend.recorder, PASS screencapture,
PASS video_capture, PASS target.video_capture, and PASS remotion_smoke
before --record-video can produce a composed clip. PASS avfoundation_screen
is preferred because it uses the primary ffmpeg/AVFoundation path; if it fails
while screencapture passes, the recorder can still use the screencapture
fallback. If
screencapture reports
could not create image from display, grant Screen Recording permission to the
terminal/agent app that runs local CI, then restart that app.
When Terminal.app has Screen Recording permission but the current agent process does not, run the readiness check through Terminal:
--run-in-terminal re-invokes the same local-ci command inside Terminal.app,
removes the flag for the child process, and relays the child stdout/stderr and
exit code. It also sends a short caffeinate -u display-wake pulse before the
child command so idle/remote displays do not produce black captures.
For audio-bearing proofs, validate the explicit AVFoundation audio device too:
PULP_VIDEO_AUDIO_DEVICE="BlackHole 2ch" \
python3 tools/local-ci/local_ci.py desktop video-doctor mac \
--run-in-terminal \
--video-audio system
The audio check should show PASS avfoundation_audio. The harness does not
guess an input device; use a loopback device when recording app/system output.
Capture¶
The dedicated entry point records video by default and renders the Remotion
composition unless --compose-video-proof is already set explicitly:
python3 tools/local-ci/local_ci.py desktop video mac \
--run-in-terminal \
--command ./build/pulp \
--click 120,80 \
--video-note "Clicked the bypass control and verified the meter stayed visible." \
--duration 8 \
--video-fps 30 \
--video-audio none \
--video-attachment-budget-mb 100
Use --action smoke, --action click, or --action inspect to choose the
underlying desktop action. The command currently records video on macOS only;
Linux and Windows fail explicitly rather than producing a run bundle without a
video artifact. Use --run-in-terminal on macOS when Terminal is the process
with Screen Recording permission.
Use repeatable --video-note flags to add short reviewer-facing proof points to
the Remotion composition. Notes are most useful for host/plugin workflows where
the video shows the host window while logs or setup prove what was inserted,
loaded, or compared.
For console-style standalone apps that do not create their own GUI window, ask the harness to record a titled Terminal.app window instead of waiting for the child process to expose a window:
python3 tools/local-ci/local_ci.py desktop video mac \
--run-in-terminal \
--action smoke \
--video-capture-target terminal \
--command ./build/examples/pulp-tone/PulpTone.app/Contents/MacOS/PulpTone \
--duration 5
Terminal capture is for smoke proofs only. It tees the command output into the normal run logs, keeps the Terminal window open for the capture duration, then signals the child process and records the command return code in the manifest.
Named recipes apply reviewer-friendly defaults for common proof scenarios:
python3 tools/local-ci/local_ci.py desktop video mac \
--recipe audio-inspector-demo \
--command ./build-video-nogpu/examples/audio-inspector-demo/pulp-audio-inspector-demo \
--duration 4 \
--video-fps 4
python3 tools/local-ci/local_ci.py desktop video mac \
--recipe standalone-interaction \
--source-mode exact-sha \
--command ./build-desktop-automation/examples/ui-preview/pulp-ui-preview \
--prepare-command 'cmake -S . -B build-desktop-automation -DCMAKE_BUILD_TYPE=Release -DPULP_BUILD_TESTS=OFF && cmake --build build-desktop-automation --target pulp-ui-preview -j$(sysctl -n hw.ncpu)' \
--pulp-app-automation \
--click 120,80 \
--duration 8
python3 tools/local-ci/local_ci.py desktop video mac \
--recipe reaper-plugin-editor \
--plugin PulpEffect \
--plugin-format vst3 \
--duration 10
The REAPER recipe generates a temporary wrapper command and ReaScript when no
explicit --command is supplied. The wrapper launches a fresh REAPER instance,
adds a track, tries common REAPER plugin-name prefixes for the requested format,
opens the matching plugin editor, and records the REAPER window via
--capture-bundle-id com.cockos.reaper. The default action is a smoke proof;
use --click X,Y for coordinate clicks in the host window. ViewInspector
selectors such as --click-view-id are rejected because REAPER is not a Pulp
ViewInspector target. Pass an explicit --command to keep a
prepared-session workflow; in that mode, add --capture-bundle-id
com.cockos.reaper yourself if the command is only a wrapper process.
Generated REAPER proofs wait for the TrackFX_Show floating-editor mode=3
status marker, then refine capture from the main REAPER window to a visible
secondary window that looks like the floating plugin editor. The run manifest
records this as capture_window_refinement so reviewers can see the original
host window and the final captured editor window.
For generated CLAP recipes, build and install the plugin bundle before
recording. The recipe checks that
~/Library/Audio/Plug-Ins/CLAP/<Plugin>.clap/Contents/MacOS/<Plugin> exists and
fails early if the bundle is only a partial package:
cmake --build build-video-nogpu --target PulpSynth_CLAP -j$(sysctl -n hw.ncpu)
mkdir -p "$HOME/Library/Audio/Plug-Ins/CLAP"
ln -sfn "$(pwd)/build-video-nogpu/CLAP/PulpSynth.clap" \
"$HOME/Library/Audio/Plug-Ins/CLAP/PulpSynth.clap"
If REAPER previously scanned a partial CLAP bundle, its cache can contain a
[<Plugin>.clap] stanza without a plugin descriptor such as
com.pulp.synth=1|PulpSynth (Pulp). Generated CLAP recipes fail early in this
state because REAPER will not find the plugin during the proof. Open REAPER's
Preferences > Plug-ins > CLAP and rescan, or remove the stale stanza from
~/Library/Application Support/REAPER/reaper-clap-macos-aarch64.ini and relaunch
REAPER.
The same checks are available without recording:
python3 tools/local-ci/local_ci.py desktop video-doctor mac \
--recipe reaper-plugin-editor \
--plugin PulpSynth \
--plugin-format clap
python3 tools/local-ci/local_ci.py desktop video mac \
--recipe inspector-workflow \
--source-mode exact-sha \
--command ./build-video-nogpu/examples/audio-inspector-demo/pulp-audio-inspector-demo \
--prepare-command 'cmake -S . -B build-video-nogpu -DCMAKE_BUILD_TYPE=Release -DPULP_BUILD_TESTS=OFF -DPULP_ENABLE_GPU=OFF && cmake --build build-video-nogpu --target pulp-audio-inspector-demo -j$(sysctl -n hw.ncpu)' \
--duration 8
python3 tools/local-ci/local_ci.py desktop video mac \
--recipe component-zoom \
--source-mode exact-sha \
--command ./build-desktop-automation/examples/ui-preview/pulp-ui-preview \
--prepare-command 'cmake -S . -B build-desktop-automation -DCMAKE_BUILD_TYPE=Release -DPULP_BUILD_TESTS=OFF && cmake --build build-desktop-automation --target pulp-ui-preview -j$(sysctl -n hw.ncpu)' \
--pulp-app-automation \
--component-id bypass-toggle \
--duration 8
python3 tools/local-ci/local_ci.py desktop video mac \
--recipe design-parity \
--command ./build/pulp \
--source-image planning/screenshots/reference.png \
--source-label "Figma reference" \
--duration 8
component-zoom enables ViewInspector capture and before/diff capture, then
uses --component-id as the click target when no explicit click selector was
provided. The ui-preview matrix demo uses the real bypass-toggle selector.
It also selects the Remotion component-zoom template and records
video_proof_composition.focus metadata so the composed proof can show a
component label, focus rectangle, and zoom detail inset. Recipes also write
video_proof_composition.context, which the composed video, index.html, and
review.md display as setup context such as recipe, host, plugin, format,
component, bundle id, or launch mode. reaper-plugin-editor selects the
plugin-host template, inspector recipes select inspector-workflow, and
standalone interaction proofs select standalone. audio-inspector-demo
records a smoke proof of a built standalone
Audio Inspector demo window without requiring a UI snapshot artifact.
design-parity records an inspect proof and composes it with the Remotion
design-parity template directly during capture.
For audio-bearing Audio Inspector proofs, build the paired HeadlessHost renderer
and use its WAV as the plugin-origin audio source. The renderer exercises the
same AudioInspectorDemoProcessor factory as the standalone window, writes a
deterministic WAV, and records a small metadata JSON next to it:
cmake --build build-video-nogpu \
--target pulp-audio-inspector-demo pulp-audio-inspector-demo-render \
-j$(sysctl -n hw.ncpu)
./build-video-nogpu/examples/audio-inspector-demo/pulp-audio-inspector-demo-render \
--output /tmp/pulp-audio-inspector-headless-proof.wav \
--metadata-json /tmp/pulp-audio-inspector-headless-proof.json \
--duration 4 \
--frequency 440 \
--level-db -12
The same recorder can be enabled on lower-level desktop actions:
./tools/local-ci/local_ci.py desktop click mac \
--command ./build/pulp \
--click 120,80 \
--capture-before \
--record-video \
--compose-video-proof \
--video-duration 8 \
--video-fps 30 \
--video-attachment-budget-mb 100
The run bundle records:
video/proof.mp4video/proof-composed.mp4when--compose-video-proofis setvideo/proof.issue.mp4as the configured GitHub/pro-account attachment targetvideo/proof.small.mp4whendesktop video --small-videoordesktop compose-video --small-videois usedvideo/metadata.jsonvideo/issue-metadata.jsonvideo/small-metadata.jsonwhen the small fallback is rendered- screenshot, before/diff screenshot, logs, and
manifest.json
metadata.json includes size_bytes, the attachment budget, and
fits_attachment_budget. Keep issue-ready clips under the configured budget.
For GitHub issue review, use 100 MB only for paid-plan video uploaders who are
eligible to upload videos above 10 MB; otherwise use 10 MB or fall back to a
local/internal link. GitHub supports .mp4, .mov, and .webm attachments and
recommends H.264 for compatibility.
When the review source is already under budget, proof.issue.mp4 is a copy of
that source. When it is over budget, the tool runs a bounded H.264 retry ladder:
balanced 720p at 24 fps, compact 720p at 15 fps, then compact 540p at 15 fps.
The transcode ladder maps optional audio and writes AAC when the source has an
audio stream.
issue-metadata.json records each attempt, the selected attempt when one fits,
and final status=transcoded, exceeds-budget, or transcode-failed.
For review lanes that should always prepare a 10 MB fallback, rerender with
--small-video; this writes proof.small.mp4 and small-metadata.json using
--small-video-budget-mb independently from the pro-account issue budget.
--video-audio none remains the default. --video-audio system records an
explicit macOS AVFoundation audio input into the raw MP4 and resulting
issue-ready clip. Pass --video-audio-device <index-or-name> or set
PULP_VIDEO_AUDIO_DEVICE; common loopback setups use a device such as
BlackHole 2ch. The harness does not guess a microphone/system device, and an
audio-requested capture will not silently fall back to a no-audio frame
sequence.
For Pulp-owned plugin or standalone validation where the harness can render the expected audio directly, prefer the explicit plugin-audio mux path:
python3 tools/local-ci/local_ci.py desktop video mac \
--recipe audio-inspector-demo \
--command ./build-video-nogpu/examples/audio-inspector-demo/pulp-audio-inspector-demo \
--video-audio plugin \
--video-audio-file /tmp/pulp-audio-inspector-headless-proof.wav \
--compose-video-proof
The WAV is copied into the run bundle as video/audio.wav, muxed into
video/proof.mp4 as AAC, and then carried through Remotion composition and the
issue-size retry ladder. This path avoids microphone guessing and host loopback
setup when the proof should use deterministic plugin-rendered audio.
The macOS screencapture frame-sequence fallback normalizes encoded video
dimensions to even pixel counts. This avoids libx264 failures when a host
exposes an odd-sized floating editor window.
Remotion Composition¶
--compose-video-proof uses the repo-local Remotion tooling to render an
annotated proof video with run context, source identity, a launch/action/capture
timeline, attachment status, and the raw recording. The composed clip is what
reviewers should watch first.
You can rerender a composed clip from an existing run bundle:
That command writes video/proof-composed.mp4, video/composed-metadata.json,
video/proof.issue.mp4, and video/issue-metadata.json, then updates the run
manifest. Add repeatable --note flags when an existing raw capture needs more
reviewer context in the visible proof steps. The lower-level npm script is still
available for template iteration:
composed-metadata.json includes a review_storyboard block with the title,
template, launch/action/capture/review steps, selected source identity, capture
details, and issue-variant state. desktop publish carries that storyboard into
index.html, review.md, review-package.json, and generated
github-issue.md, so reviewers can understand what the clip is proving before
they open the MP4.
For source/design comparison reviews from two still images, use
desktop design-proof. This creates a run manifest, generates a source/native
diff, renders the Remotion design-parity proof, and writes both the normal
issue MP4 and optional 10 MB fallback:
python3 tools/local-ci/local_ci.py desktop design-proof \
--source-image planning/screenshots/reference.png \
--native-image /tmp/native-render.png \
--label figma-native-design-proof \
--source-label "Figma reference" \
--title "Design parity proof" \
--note "The source reference was exported from Figma before composition." \
--context fixture=elysium.pulp.zip \
--video-attachment-budget-mb 100 \
--small-video \
--small-video-budget-mb 10
Use the lower-level two-step path when comparing a source image against an existing recorded run manifest, or when rerendering a proof with a custom diff:
python3 tools/local-ci/local_ci.py desktop design-diff \
--source-image planning/screenshots/reference.png \
--manifest /path/to/run/manifest.json \
--output planning/screenshots/source-vs-native-diff.png \
--resized-source-output planning/screenshots/source-resized-to-native.png \
--json
python3 tools/local-ci/local_ci.py desktop compose-video /path/to/run/manifest.json \
--template design-parity \
--source-image planning/screenshots/reference.png \
--source-label "Figma reference" \
--diff-image planning/screenshots/source-vs-native-diff.png \
--diff-label "Source vs native screenshot diff" \
--title "Design parity proof" \
--note "The imported layout keeps the same primary control grouping." \
--video-attachment-budget-mb 100 \
--small-video \
--small-video-budget-mb 10
desktop design-proof and desktop design-diff both compare the
source/reference image with a native screenshot, resizing the source image to
the native dimensions when needed. design-proof is the repeatable one-shot
route for still-image source/native reviews; design-diff writes a visual diff
image and JSON metadata that includes copy-paste compose_args for an existing
run. Both use Pillow when available and fall back to a stdlib PNG path for
8-bit RGB/RGBA PNG exports and screenshots. The composition renders the
source/reference image beside the native proof and adds the diff image to the
proof context panel. If the run manifest already has artifacts.diff_screenshot,
compose-video uses it automatically unless --diff-image overrides it. The
commands record a video_proof_composition block in the run manifest.
npm --prefix tools/local-ci run compose-video-proof -- \
--manifest /path/to/run/manifest.json \
--output /path/to/run/video/proof-composed.mp4
Local Review¶
Before recording a demo batch, print the curated proof matrix:
The matrix lists the full-service proof scenarios, current readiness, Remotion
template, doctor command, concrete Release prepare command, recording/compose
command, publish command, draft review-issue command, create-with-map review
issue command, review-status/review-watch closeout commands, background
serve/status/stop commands, and what a reviewer should look for. Use
--target mac, --target
ubuntu, --target windows, --target ios-simulator, --scenario
audio-inspector-demo, --scenario component-zoom, --status ready, or
--json when an agent needs a narrower machine-readable plan. Add --check
to include lightweight machine-local readiness checks, such as whether cmake,
the in-tree audio-inspector demo source, adb, xcrun, REAPER, or
external/skia-build/libskia.a are available before attempting a demo, and
whether design-parity has a source/reference image plus either an existing run
manifest to recompose or a native screenshot for the one-shot desktop
design-proof helper. For design parity, pass concrete inputs when checking a
specific recorded run:
python3 tools/local-ci/local_ci.py desktop video-matrix \
--target mac \
--scenario design-parity \
--check \
--design-parity-manifest /path/to/run/manifest.json \
--design-parity-source-image planning/screenshots/reference.png
For a still-image source/native review with no prior recording, pass the native
image instead. The matrix emits a ready desktop design-proof command:
python3 tools/local-ci/local_ci.py desktop video-matrix \
--target mac \
--scenario design-parity \
--check \
--design-parity-source-image planning/screenshots/reference.png \
--design-parity-native-image /tmp/native-render.png
If --design-parity-source-image is omitted, the check first tries
planning/screenshots/reference.png. If either design-parity input is still
missing, video-matrix --check also searches the configured published report
root for the latest design-parity report that contains both a copied run
manifest and a copied source/reference image. Explicit paths always win. The
failed checks include remediation text with the next setup step. The status
filter uses declared matrix status by default; with --check, it uses computed
local readiness, so --target mac --status ready --check prints the macOS
proofs this machine can run now. The
audio-inspector-demo row is the fast no-GPU macOS proof path and does not
require Skia. The iOS Simulator row uses the working
simulator video recorder. The
Android row uses the working android video command for adb-connected
emulators/devices with screenrecord and timed open-url/deep-link actions. The
Linux/Xvfb and Windows/session-agent rows are marked planned; their
desktop video-doctor checks fail backend.recorder until the ffmpeg
x11grab and ddagrab/gdigrab backends land.
iOS Simulator Video Proofs¶
For simulator validation, boot the target simulator first, then run:
Build the PulpSineSynth HostApp, then record a bounded MP4 proof of the booted simulator with app install and launch:
cmake -S . -B build-ios-sim-video-proof -G Xcode \
-DCMAKE_SYSTEM_NAME=iOS \
-DCMAKE_OSX_SYSROOT=iphonesimulator \
-DCMAKE_OSX_ARCHITECTURES=arm64 \
-DCMAKE_OSX_DEPLOYMENT_TARGET=16.4 \
-DCMAKE_BUILD_TYPE=Release \
-DPULP_ENABLE_GPU=OFF \
-DPULP_BUILD_TESTS=OFF \
-DPULP_BUILD_EXAMPLES=ON
cmake --build build-ios-sim-video-proof \
--target PulpSineSynth_HostApp_Embed \
--config Release -- -sdk iphonesimulator
python3 tools/local-ci/local_ci.py simulator video \
--app build-ios-sim-video-proof/AUv3/Release/PulpSineSynth.app \
--bundle-id com.pulp.examples.sinesynth.host \
--open-url https://example.com \
--action-label "open validation URL after HostApp launch" \
--label ios-pulpsinesynth-hostapp-proof \
--duration 6 \
--video-fps 8 \
--compose-video-proof \
--video-title "Pulp iOS HostApp proof" \
--video-note "Installs and launches the PulpSineSynth iOS HostApp, then records the simulator while simctl opens a validation URL." \
--small-video
The command writes video/proof.mp4 and manifest.json under the simulator run
directory. With --compose-video-proof, it immediately renders the
mobile-simulator Remotion proof and adds video/proof-composed.mp4,
video/proof.issue.mp4, optional video/proof.small.mp4, composition metadata,
and a storyboard to the simulator manifest before publishing. This first
simulator lane proves capture/install/launch plumbing via xcrun simctl io
screenshot frames encoded with ffmpeg. When --open-url is provided, the URL
opens during capture and the manifest stores a mobile-simulator action marker.
Coordinate tap driving still needs a future automation backend because this
Xcode's simctl does not expose a tap command.
You can still rerender an existing simulator run with
desktop compose-video --template mobile-simulator when only the title, notes,
or size budget need to change.
Android Emulator Video Proofs¶
For Android validation, start the emulator or connect a device first, then run:
Record a bounded MP4 proof with optional APK install, package/activity launch, and a timed tap action:
python3 tools/local-ci/local_ci.py android video \
--apk android/app/build/outputs/apk/debug/app-debug.apk \
--package com.pulp.demo \
--activity .MainActivity \
--tap 540,1200 \
--action-label "tap validation control" \
--label android-emulator-proof \
--duration 8 \
--compose-video-proof \
--video-title "Android emulator tap proof" \
--video-note "The emulator receives a timed adb input tap during recording." \
--small-video
The command writes video/proof.mp4 and manifest.json under the Android run
directory using adb shell screenrecord. With --compose-video-proof, it
immediately renders the mobile-emulator Remotion proof and adds
video/proof-composed.mp4, video/proof.issue.mp4, optional
video/proof.small.mp4, composition metadata, and a storyboard to the Android
manifest before publishing. With --tap x,y, the command runs adb shell input
tap during capture and stores a mobile-emulator action marker. Use
--open-url instead when a URL/deep link is the right validation action; do not
combine both action types in one proof.
You can still rerender an existing Android run with
desktop compose-video --template mobile-emulator when only the title, notes,
or size budget need to change.
Stage a local report after one or more desktop runs:
To stage a specific run or generated demo manifest, pass it explicitly:
python3 tools/local-ci/local_ci.py desktop publish \
--manifest /path/to/run/manifest.json \
--label validation-video-proof
The generated index.html renders videos with native browser controls. To
share from a Tailscale-visible machine, serve the publish directory with a local
static server and link that URL in the review issue. Use background mode when
the link needs to stay alive after the agent response:
python3 tools/local-ci/local_ci.py desktop serve /path/to/published-report \
--host 0.0.0.0 \
--port 8765 \
--auto-port \
--background \
--label validation-video-proof \
--json
desktop serve serves the latest published desktop report unless a report
directory is passed explicitly. When bound to 0.0.0.0, it prints candidate
watch URLs: localhost, this machine's hostname, any comma-separated
PULP_DESKTOP_SERVE_HOSTS, and tailscale ip -4 results when the Tailscale CLI
is available. Set PULP_DESKTOP_SERVE_HOSTS=blackbook.tailnet-name.ts.net when
you already know the friendly Tailnet DNS name you want reviewers to tap.
Manage a background server with the same label:
python3 tools/local-ci/local_ci.py desktop serve --status --label validation-video-proof --json
python3 tools/local-ci/local_ci.py desktop serve --stop --label validation-video-proof --json
After reviewers have watched or attached the proof, prune old published reports without touching run bundles:
python3 tools/local-ci/local_ci.py desktop cleanup --published --older-than-days 14 --keep-last 3 --json
Use --auto-port for shared review links so a stale server on 8765 does not
produce a dead URL; the JSON response records the actual port and candidate
watch URLs. If background startup returns status: failed, the URL is not live.
Check the JSON stderr_tail for bind errors such as Address already in use,
then stop the conflicting labeled server or choose another port before sharing
the link.
When background startup succeeds, the command probes the primary URL and writes
serve_verification into index.json and review-package.json. Treat
serve_verification.status == "ok" as the local proof that the fallback link
was live before you share it.
desktop publish also writes review.md and review-package.json next to
index.html. The publish step records the same candidate watch URLs in
index.json, review.md, and review-package.json; start desktop serve for
that report directory to make those URLs live. review.md is the human issue
body. It includes the local report path, foreground and background
Tailscale/local serve commands, status/stop commands for cleanup, each run's
video artifact, attachment-budget status, and the expected reviewer response.
review-package.json is the machine-readable handoff for future upload
automation: it records each run's primary or small attachment decision, absolute
MP4 path when a file should be attached, size/budget fields, and the served
report fallback including background/status/stop commands. It also preserves
review context from the run manifest: exact
launch command when available, source branch/SHA/mode, host, adapter, and copied
manifest path. When Remotion composition metadata is available, it also preserves
the review_storyboard steps so issue automation can show what launched, what
action happened, what changed, and what the reviewer should verify. When a small fallback exists, the markdown also lists
proof.small.mp4, its 10 MB budget status, and whether that smaller file should
be attached instead of the pro-account issue video. Both files include the
concrete attach/do-not-attach decision and desktop verdict commands for
approval or follow-up. They also include a desktop review-status command for
checking the issue's actionable approval or needs-work trigger before recording
the final verdict. GitHub's
current hosted attachment policy is 100 MB for paid-plan video uploads when the
uploader is eligible; otherwise plan for the 10 MB video cap and use the served
report link.
Generate a local GitHub issue draft from the review package before opening the issue:
python3 tools/local-ci/local_ci.py desktop review-issue /path/to/published-report \
--repo owner/repo \
--check-files
The command accepts either the report directory or its review-package.json.
It writes github-issue.md and github-issue.json next to the report without
calling GitHub. The JSON draft lists attachable MP4 paths, fallback links,
source/command/manifest context for each run, the close trigger (looks good to
me), a read-only desktop review-status polling command, and a suggested gh
issue create command. --check-files verifies that
every attachable MP4 still exists and fits its recorded attachment budget before
writing the draft; runs that use the served-report fallback must have
serve_verification.status == "ok" recorded by desktop serve --background
before the draft is accepted.
To create the GitHub issue directly after writing the local draft, add
--create. Labels and assignees are passed through to gh issue create:
python3 tools/local-ci/local_ci.py desktop review-issue /path/to/published-report \
--repo owner/repo \
--check-files \
--create \
--manifest-map-output /tmp/pulp-video-review-manifests.json \
--label video-review \
--assignee @me
This does not upload MP4 attachments; gh issue create accepts the issue body
but not local binary attachments. Use the generated JSON attachment decisions to
attach proof.issue.mp4 or proof.small.mp4 manually, or include the served
report link. When the review package has exactly one run manifest,
--manifest-map-output writes a JSON map keyed by issue URL, issue number, and
#number so desktop review-watch --manifest-map can suggest the exact
verdict command later.
Before recording the final verdict, check whether the review issue has an actionable approval or needs-work trigger:
python3 tools/local-ci/local_ci.py desktop review-status \
https://github.com/owner/repo/issues/123 \
--manifest /path/to/run/manifest.json \
--close-issue
This command is read-only. It uses gh issue view to inspect comments for
looks good to me, needs work, needs changes, needs another pass, or
not approved. The latest actionable comment wins: approval reports
approved: true, while requested changes report needs_work: true. In either
case, it prints a suggested desktop verdict command when a manifest is
provided. Add --json for automation handoff, and add --repo owner/repo
when passing a bare issue number.
The intended review loop is:
- Publish the report.
- Generate
github-issue.md/github-issue.jsonwithdesktop review-issue --check-files. - Open a GitHub issue with
github-issue.md. - Attach the MP4 manually when it fits the configured budget, or use the served report link when it does not.
- Check the issue for a reviewer approval or needs-work phrase.
- Record the review verdict in the run manifest once the reviewer responds.
- Close the issue once the reviewer comments
looks good to me.
python3 tools/local-ci/local_ci.py desktop review-status \
https://github.com/owner/repo/issues/123 \
--manifest /path/to/run/manifest.json \
--close-issue
desktop review-status is read-only. It calls gh issue view, detects the
latest actionable close trigger (looks good to me) or needs-work trigger, and
prints or JSON-emits a suggested desktop verdict command. Use --repo
owner/repo when the issue argument is a bare number or when GitHub context
cannot be inferred.
To converge on all open video-review issues at session start, use
desktop review-watch. It first lists open issues with the video-review
label, then views only uncached or changed issues when a state file is provided:
python3 tools/local-ci/local_ci.py desktop review-watch \
--repo owner/repo \
--label video-review \
--state-file /tmp/pulp-video-review-watch.json \
--manifest-map /tmp/pulp-video-review-manifests.json \
--close-issue \
--json
The optional manifest map is a JSON object keyed by issue URL, issue number, or
#number, with values pointing to run manifest.json files. When an approved
or needs-work issue has a mapped manifest, review-watch emits the exact
desktop verdict ... --approved --issue-url ... or
desktop verdict ... --needs-work --issue-url ... --comment-issue --notes ...
command. It includes --close-issue for approved issues when that flag was
passed, and includes --comment-issue for needs-work issues so the generated
same-issue checklist is posted before the local manifest is updated. Use
--interval N --max-iterations M only for short post-ping watch windows;
one-shot mode is the default.
python3 tools/local-ci/local_ci.py desktop verdict /path/to/run/manifest.json \
--approved \
--issue-url https://github.com/owner/repo/issues/123
To close the review issue through gh in the same step after an approved
verdict, add --close-issue:
python3 tools/local-ci/local_ci.py desktop verdict /path/to/run/manifest.json \
--approved \
--issue-url https://github.com/owner/repo/issues/123 \
--close-issue
If the proof needs another iteration, record that state instead:
python3 tools/local-ci/local_ci.py desktop verdict /path/to/run/manifest.json \
--needs-work \
--issue-url https://github.com/owner/repo/issues/123 \
--comment-issue \
--notes "Zoom starts too late; recapture with the component centered."
The manifest review block records approved or needs-work, the review
timestamp, optional reviewer notes, and whether the review issue can be closed.
The command also writes review-verdict.md and review-verdict.json next to
the run manifest. Use --comment-issue when the GitHub issue should receive
the generated summary/checklist immediately; without it, use the markdown file
as the pasteable issue closeout comment or same-issue follow-up checklist. The
JSON file is the automation handoff.
Current Scope¶
This first lane records the target window region on macOS with H.264 video. It
can include AAC audio from either explicit AVFoundation system capture or an
explicit plugin-rendered WAV mux. It uses ffmpeg/AVFoundation screen capture as
the primary recorder and falls back to a short sequence of trusted
screencapture -l window frames when the ffmpeg recorder cannot start and no
system audio was requested. If macOS refuses window-ID capture but allows
full-screen capture, the fallback captures full-screen frames and crops them to
the target window bounds during encoding. Final still screenshots use a
last-resort full-screen fallback for the same TCC edge case.
Remotion composition and local review artifacts are implemented in this branch. iOS Simulator, Android, richer REAPER plugin interaction automation, and GitHub issue automation are planned follow-on layers.