Repository navigation
feat: GIF render target, Playwright command player, synthetic cursor - #34
Merged
Merged
Conversation
Three additions that together let walkthru record a browser demo end to end in Python, which it could describe but not play. GifRenderTarget (adapters/gif) is the second RenderTarget, and a different kind from reelee's: reelee builds a film from panels, this re-frames a screencast the recorder already produced -- the document supplies the camera track, the video supplies the pixels. A GIF plays inline in a README on GitHub and PyPI where a video does not, which is what made a secondary target real (PLAN 6). All the meaning is pure -- camera_segments, crop_box, gif_filtergraph -- and only video_to_gif shells out, through an injected runner, so the path is tested with no ffmpeg installed. ffmpeg is a system dependency, so there is no extra to install; check_ffmpeg() raises with install commands instead. PlaywrightCommandPlayer (adapters/playwright/player.py) is the CommandPlayer the Python side lacked: the adapters could locate, wait and record, but nothing could act. The id -> handler table is a keyword argument, and an unknown id raises by default -- a silently skipped command produces a demo that is wrong in a way nobody notices until they watch the video. install_synthetic_cursor is the Python-side renderer for CursorCue. Playwright's screencast omits the pointer, so a demo whose point is 'hover this and watch' records as a tooltip appearing for no reason. Two sharp edges found by a real render and pinned by tests: every segment needs setsar=1 or concat rejects it (cropping different rects changes the sample aspect ratio), and the cursor SVG must be base64 -- a raw data URI ends the surrounding JS string at its first apostrophe, and an init script that fails to parse leaves no trace except a video with no cursor in it. See DECISIONS.md D14 and D15.
… the suite does The suite deliberately has no pytest-asyncio dependency -- every async entry point is driven through asyncio.run from a sync test. The new tests used the marker instead, which passes locally where the plugin happens to be installed and fails in CI with 'async def functions are not natively supported'.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Three additions that together let walkthru record a browser demo end to end in Python — something it could describe but not play.
They came out of a real job: documenting an interactive HTML report in a README. Each gap was found by hitting it.
GifRenderTarget—walkthru.adapters.gifThe second
RenderTarget, and a different kind from reelee's, which is the part worth naming: reelee builds a film from panels; this re-frames a screencast the recorder already produced. The Demo Document supplies the camera track, the video supplies the pixels.PLAN §6 kept secondary targets for "build only when real". A GIF plays inline in a README on GitHub and PyPI where a video does not — that made it real.
All the meaning is pure and tested without ffmpeg installed:
camera_segments(timeline, total_ms=…)crop_box(rect, zoom, frame_w=…, frame_h=…, aspect=…)gif_filtergraph(segments, …)-filter_complexstringvideo_to_gif(…, runner=subprocess.run)ffmpeg is a system dependency, so there is no extra — an extra with nothing for pip to install would be a lie.
check_ffmpeg()raises with install commands instead of letting aFileNotFoundErrorsurface.PlaywrightCommandPlayer—walkthru.adapters.playwright.playerThe
CommandPlayerthe Python side lacked. The adapters could locate, wait and record; nothing could act, so a browser demo needed an app-specific executor.PAGE_COMMANDS(the id → handler table) is a keyword argument, so an app with its own vocabulary reuses the player:commands={**PAGE_COMMANDS, "app.save": handler}. An unknown id raises by default — a silently skipped command produces a demo that is wrong in a way nobody notices until they watch the video.install_synthetic_cursor—walkthru.adapters.playwright.cursorThe Python-side renderer for
CursorCue, which has been in the schema since the first draft. Playwright's screencast — like every OS-level capture — omits the mouse pointer, so a demo whose whole point is "hover this and watch what happens" records as a tooltip appearing for no visible reason. The overlay is inert (pointer-events: none) and installed as an init script, so it survives navigation.Two sharp edges, both found by a real render
Both are pinned by tests, because neither is visible from unit tests alone:
setsar=1per segment. Cropping different rects gives each segment a different sample aspect ratio, andconcatrefuses inputs whose SAR differs — it fails at configure time withInput link parameters do not match.utf8,<svg xmlns='http://…'>data URI terminates the surrounding JavaScript string at its first apostrophe. An init script that fails to parse throws where nobody is watching: no exception reaches Python, the page renders normally, and the only symptom is a finished video with no cursor in it. That is exactly how the first version shipped.Testing
182 passed (was 172). New:
tests/test_gif_render_target.py(17),tests/test_playwright_player.py(15),tests/test_playwright_cursor.py(8). No browser and no ffmpeg needed — every adapter is duck-typed or takes an injected runner. The firewall test still passes: the core imports none of this.First real use is ductus's README demo, whose recorder script is ~150 lines against this API.