reshoot 0.3.0__tar.gz → 0.3.2__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {reshoot-0.3.0 → reshoot-0.3.2}/.claude-plugin/plugin.json +1 -1
- reshoot-0.3.2/CHANGELOG.md +93 -0
- {reshoot-0.3.0 → reshoot-0.3.2}/PKG-INFO +4 -3
- {reshoot-0.3.0 → reshoot-0.3.2}/README.md +3 -2
- {reshoot-0.3.0 → reshoot-0.3.2}/pyproject.toml +1 -1
- {reshoot-0.3.0 → reshoot-0.3.2}/skills/check-privacy/SKILL.md +4 -0
- {reshoot-0.3.0 → reshoot-0.3.2}/skills/keep-current/SKILL.md +1 -1
- {reshoot-0.3.0 → reshoot-0.3.2}/skills/reshoot-demo/SKILL.md +3 -3
- {reshoot-0.3.0 → reshoot-0.3.2}/skills/reshoot-refresh/SKILL.md +2 -2
- {reshoot-0.3.0 → reshoot-0.3.2}/skills/reshoot-review/SKILL.md +1 -1
- {reshoot-0.3.0 → reshoot-0.3.2}/src/reshoot/browser.py +33 -9
- {reshoot-0.3.0 → reshoot-0.3.2}/src/reshoot/check.py +2 -1
- {reshoot-0.3.0 → reshoot-0.3.2}/src/reshoot/cli.py +41 -9
- {reshoot-0.3.0 → reshoot-0.3.2}/src/reshoot/project.py +27 -4
- {reshoot-0.3.0 → reshoot-0.3.2}/src/reshoot/rec.js +6 -1
- {reshoot-0.3.0 → reshoot-0.3.2}/src/reshoot/rec.py +8 -1
- {reshoot-0.3.0 → reshoot-0.3.2}/src/reshoot/review.py +194 -1
- {reshoot-0.3.0 → reshoot-0.3.2}/src/reshoot/run.py +30 -2
- {reshoot-0.3.0 → reshoot-0.3.2}/src/reshoot/story.py +41 -10
- {reshoot-0.3.0 → reshoot-0.3.2}/tests/boards/selectors.html +1 -0
- {reshoot-0.3.0 → reshoot-0.3.2}/tests/person.py +10 -0
- {reshoot-0.3.0 → reshoot-0.3.2}/tests/test_project.py +25 -0
- {reshoot-0.3.0 → reshoot-0.3.2}/tests/test_reshoot.py +2 -0
- {reshoot-0.3.0 → reshoot-0.3.2}/tests/test_review.py +142 -0
- {reshoot-0.3.0 → reshoot-0.3.2}/tests/test_story.py +97 -0
- reshoot-0.3.0/CHANGELOG.md +0 -35
- {reshoot-0.3.0 → reshoot-0.3.2}/.claude-plugin/marketplace.json +0 -0
- {reshoot-0.3.0 → reshoot-0.3.2}/.gitignore +0 -0
- {reshoot-0.3.0 → reshoot-0.3.2}/LICENSE +0 -0
- {reshoot-0.3.0 → reshoot-0.3.2}/examples/notes/about.html +0 -0
- {reshoot-0.3.0 → reshoot-0.3.2}/examples/notes/edit.py +0 -0
- {reshoot-0.3.0 → reshoot-0.3.2}/examples/notes/index.html +0 -0
- {reshoot-0.3.0 → reshoot-0.3.2}/examples/notes/phone.py +0 -0
- {reshoot-0.3.0 → reshoot-0.3.2}/examples/notes/release-v1.toml +0 -0
- {reshoot-0.3.0 → reshoot-0.3.2}/examples/notes/release-v2/about.html +0 -0
- {reshoot-0.3.0 → reshoot-0.3.2}/examples/notes/release-v2/index.html +0 -0
- {reshoot-0.3.0 → reshoot-0.3.2}/examples/notes/release-v2.toml +0 -0
- {reshoot-0.3.0 → reshoot-0.3.2}/examples/notes/releases.md +0 -0
- {reshoot-0.3.0 → reshoot-0.3.2}/examples/notes/reshoot.toml +0 -0
- {reshoot-0.3.0 → reshoot-0.3.2}/examples/notes/storyboard.py +0 -0
- {reshoot-0.3.0 → reshoot-0.3.2}/examples/notes/storyboard.toml +0 -0
- {reshoot-0.3.0 → reshoot-0.3.2}/examples/notes/terminal.tape +0 -0
- {reshoot-0.3.0 → reshoot-0.3.2}/examples/notes/voiced.py +0 -0
- {reshoot-0.3.0 → reshoot-0.3.2}/skills/README.md +0 -0
- {reshoot-0.3.0 → reshoot-0.3.2}/skills/cut-video/SKILL.md +0 -0
- {reshoot-0.3.0 → reshoot-0.3.2}/skills/plan-demo/SKILL.md +0 -0
- {reshoot-0.3.0 → reshoot-0.3.2}/skills/record-terminal/SKILL.md +0 -0
- {reshoot-0.3.0 → reshoot-0.3.2}/skills/record-terminal/template.tape +0 -0
- {reshoot-0.3.0 → reshoot-0.3.2}/skills/record-web/SKILL.md +0 -0
- {reshoot-0.3.0 → reshoot-0.3.2}/skills/record-window/SKILL.md +0 -0
- {reshoot-0.3.0 → reshoot-0.3.2}/skills/reshoot-demo/agents/openai.yaml +0 -0
- {reshoot-0.3.0 → reshoot-0.3.2}/skills/reshoot-refresh/agents/openai.yaml +0 -0
- {reshoot-0.3.0 → reshoot-0.3.2}/skills/reshoot-review/agents/openai.yaml +0 -0
- {reshoot-0.3.0 → reshoot-0.3.2}/src/reshoot/__init__.py +0 -0
- {reshoot-0.3.0 → reshoot-0.3.2}/src/reshoot/artifacts.py +0 -0
- {reshoot-0.3.0 → reshoot-0.3.2}/src/reshoot/config.py +0 -0
- {reshoot-0.3.0 → reshoot-0.3.2}/src/reshoot/cut.py +0 -0
- {reshoot-0.3.0 → reshoot-0.3.2}/src/reshoot/overlay.js +0 -0
- {reshoot-0.3.0 → reshoot-0.3.2}/src/reshoot/safety.py +0 -0
- {reshoot-0.3.0 → reshoot-0.3.2}/src/reshoot/scan.py +0 -0
- {reshoot-0.3.0 → reshoot-0.3.2}/src/reshoot/skills.py +0 -0
- {reshoot-0.3.0 → reshoot-0.3.2}/src/reshoot/templates/BRIEF.md +0 -0
- {reshoot-0.3.0 → reshoot-0.3.2}/src/reshoot/templates/about.html +0 -0
- {reshoot-0.3.0 → reshoot-0.3.2}/src/reshoot/templates/edit.py +0 -0
- {reshoot-0.3.0 → reshoot-0.3.2}/src/reshoot/templates/edit.toml +0 -0
- {reshoot-0.3.0 → reshoot-0.3.2}/src/reshoot/templates/index.html +0 -0
- {reshoot-0.3.0 → reshoot-0.3.2}/src/reshoot/templates/reshoot.toml +0 -0
- {reshoot-0.3.0 → reshoot-0.3.2}/src/reshoot/templates/storyboard.py +0 -0
- {reshoot-0.3.0 → reshoot-0.3.2}/src/reshoot/templates/storyboard.toml +0 -0
- {reshoot-0.3.0 → reshoot-0.3.2}/src/reshoot/timeline.py +0 -0
- {reshoot-0.3.0 → reshoot-0.3.2}/src/reshoot/voice.py +0 -0
- {reshoot-0.3.0 → reshoot-0.3.2}/src/reshoot/web.py +0 -0
- {reshoot-0.3.0 → reshoot-0.3.2}/src/reshoot/window.py +0 -0
- {reshoot-0.3.0 → reshoot-0.3.2}/src/reshoot/zoom.py +0 -0
- {reshoot-0.3.0 → reshoot-0.3.2}/tests/boards/blur.py +0 -0
- {reshoot-0.3.0 → reshoot-0.3.2}/tests/boards/pages.py +0 -0
- {reshoot-0.3.0 → reshoot-0.3.2}/tests/boards/voice.py +0 -0
- {reshoot-0.3.0 → reshoot-0.3.2}/tests/conftest.py +0 -0
- {reshoot-0.3.0 → reshoot-0.3.2}/tests/test_distribution.py +0 -0
- {reshoot-0.3.0 → reshoot-0.3.2}/tests/test_edit.py +0 -0
- {reshoot-0.3.0 → reshoot-0.3.2}/tests/test_failures.py +0 -0
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"$schema": "https://anthropic.com/claude-code/plugin.schema.json",
|
|
3
3
|
"name": "reshoot",
|
|
4
|
-
"version": "0.3.
|
|
4
|
+
"version": "0.3.2",
|
|
5
5
|
"description": "Agent skills to create, refresh and review software demo videos. Use the reshoot CLI to record the app, export an MP4 and README GIF, and keep the demo source beside the code.",
|
|
6
6
|
"author": { "name": "Mumit Khan", "url": "https://github.com/mumit" },
|
|
7
7
|
"homepage": "https://github.com/mumit/reshoot",
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
Each release names what changed for people making demos. Versions follow
|
|
4
|
+
[semantic versioning](https://semver.org/); before 1.0, a minor version may
|
|
5
|
+
change project files or commands.
|
|
6
|
+
|
|
7
|
+
## 0.3.2 (2026-10-23)
|
|
8
|
+
|
|
9
|
+
### Added
|
|
10
|
+
|
|
11
|
+
- A `select` action chooses a dropdown option by its value or visible label,
|
|
12
|
+
and `reshoot rec` writes one for each dropdown choice.
|
|
13
|
+
|
|
14
|
+
### Fixed
|
|
15
|
+
|
|
16
|
+
- A result could appear a whole hold late in the video while every check passed: the evidence screenshot after
|
|
17
|
+
each beat could take the frame showing it. The evidence frame is now the last frame the video shows, saved at
|
|
18
|
+
the end of the hold. Frame times also no longer depend on when the first frame arrives.
|
|
19
|
+
- Storyboard errors name the beat and the action or check, such as
|
|
20
|
+
`beat 'fill', action 2 (key): target needs exactly one of css, test_id, or role`.
|
|
21
|
+
- The demo skill says to narrow the viewport or zoom when an app's content is
|
|
22
|
+
too small to read in the README GIF.
|
|
23
|
+
- A port Chrome refuses, such as 5060, is explained with the flag that allows
|
|
24
|
+
it, and a failure while opening the page no longer reports `beat None`.
|
|
25
|
+
- The demo skill and guides say how to write checks for apps that keep data
|
|
26
|
+
between takes and have no reset.
|
|
27
|
+
|
|
28
|
+
## 0.3.1 (2026-10-23)
|
|
29
|
+
|
|
30
|
+
### Added
|
|
31
|
+
|
|
32
|
+
- `reshoot accept RUN` records the owner's verdict in one step: the exports,
|
|
33
|
+
every beat, every scan item and the scan checklist, bound to the run's exact
|
|
34
|
+
files and scan policy. Each flagged item must be named with `--flagged`, and
|
|
35
|
+
`--reject` marks an item that must not be shared. It asks for confirmation,
|
|
36
|
+
and without a terminal it refuses unless given `--yes`.
|
|
37
|
+
- `reshoot run` and `reshoot preview` write the review page and print the
|
|
38
|
+
`reshoot accept` command for the run.
|
|
39
|
+
- `reshoot runs PROJECT` lists a project's runs, newest first, with their
|
|
40
|
+
result and review status. Run paths accept `latest` (the newest successful
|
|
41
|
+
run) and `accepted` (the newest accepted run), such as `demo/accepted`.
|
|
42
|
+
Run reports record `finished_at`.
|
|
43
|
+
- A refresh pull request in CI. `examples/github/refresh.yml` records the demo
|
|
44
|
+
when the app changes, compares it with the accepted run and opens a pull
|
|
45
|
+
request with the new MP4, GIF and a beat table. Merging it accepts the
|
|
46
|
+
recording.
|
|
47
|
+
- `reshoot accept` writes `demo/accepted/`, a small committed record of the run
|
|
48
|
+
(report, beat results and evidence frames), so CI and fresh clones can
|
|
49
|
+
compare against it. `reshoot export --record DIR` writes one for any run.
|
|
50
|
+
- `reshoot export --replace` updates media already in the destination, and
|
|
51
|
+
`reshoot compare --markdown FILE` writes a summary for a pull request.
|
|
52
|
+
- In GitHub Actions, each run writes a job summary with its checks, scan and
|
|
53
|
+
errors.
|
|
54
|
+
|
|
55
|
+
### Fixed
|
|
56
|
+
|
|
57
|
+
- `reshoot init` moves the notes example off port 8765 when something else
|
|
58
|
+
is using it.
|
|
59
|
+
- A busy port names its loopback address instead of a redacted hash.
|
|
60
|
+
- `reshoot web --help` describes TOML storyboards, and `review`, `compare`
|
|
61
|
+
and `export` explain their arguments.
|
|
62
|
+
- A target that matches no element, or several, names the target and the count.
|
|
63
|
+
- A run's report includes the failed beat's error, not only the failed step.
|
|
64
|
+
|
|
65
|
+
## 0.3.0 (2026-10-23)
|
|
66
|
+
|
|
67
|
+
The first release on PyPI. Install it with `uv tool install 'reshoot[browser]'`.
|
|
68
|
+
|
|
69
|
+
### Added
|
|
70
|
+
|
|
71
|
+
- Published to PyPI, with a release workflow that builds, checks and publishes
|
|
72
|
+
each tagged version.
|
|
73
|
+
- An agent-first entry point: a first-demo prompt in the README, the agent
|
|
74
|
+
quickstart and a skill catalog.
|
|
75
|
+
- Guides for each stage after the first recording: features, customizing,
|
|
76
|
+
fuller demos and tutorials, publishing, CI and faster shoots.
|
|
77
|
+
|
|
78
|
+
### Fixed
|
|
79
|
+
|
|
80
|
+
- Review pages show baseline GIFs as images; MP4s keep their video players.
|
|
81
|
+
- Joined title cards no longer vanish or freeze in GIF exports.
|
|
82
|
+
- Clip joins keep an exact frame rate.
|
|
83
|
+
- Captions stay visible across same-origin page loads.
|
|
84
|
+
- Previews that mix web and VHS takes keep matching dimensions.
|
|
85
|
+
- Runs leave incomplete expectations unverified instead of passing them.
|
|
86
|
+
- `doctor` rejects a `RESHOOT_CHROME` that does not name a usable browser.
|
|
87
|
+
|
|
88
|
+
## 0.2.0
|
|
89
|
+
|
|
90
|
+
Repeatable demo projects: `reshoot.toml`, TOML storyboards with checked
|
|
91
|
+
expectations, `init`, `doctor`, `preview`, isolated versioned runs, `compare`,
|
|
92
|
+
`review` with bound decisions, `export`, and shared Claude Code and Codex skills
|
|
93
|
+
installed by `reshoot skills`. Installed from Git only.
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: reshoot
|
|
3
|
-
Version: 0.3.
|
|
3
|
+
Version: 0.3.2
|
|
4
4
|
Summary: Agent skills and recording tools for repeatable software demo videos
|
|
5
5
|
Project-URL: Homepage, https://github.com/mumit/reshoot
|
|
6
6
|
Project-URL: Documentation, https://github.com/mumit/reshoot/tree/main/guide
|
|
@@ -182,8 +182,9 @@ takes outside it. Select an explicit baseline when comparing updates.
|
|
|
182
182
|
|
|
183
183
|
Native demos keep their capture instructions and edit script separately; see
|
|
184
184
|
the [macOS window workflow](https://github.com/mumit/reshoot/blob/main/guide/macos-window.md).
|
|
185
|
-
|
|
186
|
-
|
|
185
|
+
The [CI workflow](https://github.com/mumit/reshoot/blob/main/guide/ci.md) records the demo again when the app changes and
|
|
186
|
+
opens a pull request with the new video, GIF and a beat-by-beat comparison with
|
|
187
|
+
the accepted run. Merging it accepts the new recording.
|
|
187
188
|
|
|
188
189
|
## Choose the recording for the task
|
|
189
190
|
|
|
@@ -153,8 +153,9 @@ takes outside it. Select an explicit baseline when comparing updates.
|
|
|
153
153
|
|
|
154
154
|
Native demos keep their capture instructions and edit script separately; see
|
|
155
155
|
the [macOS window workflow](guide/macos-window.md).
|
|
156
|
-
|
|
157
|
-
|
|
156
|
+
The [CI workflow](guide/ci.md) records the demo again when the app changes and
|
|
157
|
+
opens a pull request with the new video, GIF and a beat-by-beat comparison with
|
|
158
|
+
the accepted run. Merging it accepts the new recording.
|
|
158
159
|
|
|
159
160
|
## Choose the recording for the task
|
|
160
161
|
|
|
@@ -34,6 +34,10 @@ keeps a face, a name or a house number out of a public video.
|
|
|
34
34
|
bytes, so the next round shows only what's new.
|
|
35
35
|
6. Repeat until `reshoot check` says everything has been checked.
|
|
36
36
|
|
|
37
|
+
For a versioned run, `reshoot accept RUN` replaces steps 4 and 5 when this CLI provides it: it records the scan
|
|
38
|
+
checklist and the recording verdict together, and needs each flagged label named with `--flagged` or `--reject`.
|
|
39
|
+
The person runs it, or tells you explicitly to.
|
|
40
|
+
|
|
37
41
|
## What fails an image
|
|
38
42
|
|
|
39
43
|
- A person: faces, heads at the edge of the frame, tiny figures far away, reflections in glass, people behind
|
|
@@ -38,4 +38,4 @@ description: Set up a reshoot.toml so one command (`reshoot run`) records a proj
|
|
|
38
38
|
|
|
39
39
|
The run report and Action outputs distinguish `scan_status` and `review_status`. `ok=true` means execution completed, not approval to publish. A required check error stops the run, and the app remains available until its URL-based checks finish.
|
|
40
40
|
|
|
41
|
-
Versioned runs keep separate candidate files. Compare two explicit runs with `reshoot compare OLD NEW --out FILE`, then generate `reshoot review NEW --previous OLD`. Matching beat IDs do not imply matching results. No new run promotes itself to the baseline.
|
|
41
|
+
For a versioned project (`schema_version = 1`), use `../../examples/github/refresh.yml` instead of the legacy workflow above. Accept a run locally first and commit the `demo/accepted/` record that `reshoot accept` writes; the workflow compares each new run with it, copies the new media to `docs/demo/` and opens a pull request whose merge accepts the recording. Changing repository settings, pushing, and merging are the person's call. Versioned runs keep separate candidate files. Compare two explicit runs with `reshoot compare OLD NEW --out FILE`, then generate `reshoot review NEW --previous OLD`. Matching beat IDs do not imply matching results. No new run promotes itself to the baseline.
|
|
@@ -15,13 +15,13 @@ For a new browser project, use `reshoot init DIR --url DEMO_URL`; omit `--url` t
|
|
|
15
15
|
|
|
16
16
|
For a native macOS app, save a brief, capture recipe and edit script in a demo folder. Use `reshoot window --list` to select the exact demo window, then `reshoot window APP NEW_TAKE.mov --id ID -s SECONDS`. Check Screen Recording permission for the command's host. Drive the app through app-specific automation or coordinate the owner's interaction during capture; the window command does not operate controls. Look up the ID each take and choose unused output paths. Versioned projects support web and VHS, not native window takes. Do not invent a native run manifest, browser overlay or result assertions. Export with `reshoot edit EDIT.toml NEW_TAKE.mov`, using numeric cuts, and a GIF export profile, or a Python edit. The primary skill can follow this route without supporting plugin skills.
|
|
17
17
|
|
|
18
|
-
Use made-up data in a separate account, folder, or database. Document and run its reset before each take. Record only a headless browser page, VHS terminal, or one macOS window by ID. Never capture the whole screen or reuse a person's browser profile.
|
|
18
|
+
Use made-up data in a separate account, folder, or database. Document and run its reset before each take. When a running app keeps data and offers no reset, ask for one before the first take, or write checks that pass however many times the task has run: check the item the take created, not totals that earlier takes change. Record only a headless browser page, VHS terminal, or one macOS window by ID. Never capture the whole screen or reuse a person's browser profile.
|
|
19
19
|
|
|
20
|
-
For browser/VHS projects, run `reshoot preview CONFIG`, inspect frames and expected results, then `reshoot run CONFIG`. Read the printed run report. For native apps, inspect a standalone test take, then capture and edit a fresh final in a new folder. Generate check sheets for source and final media with `reshoot check SOURCES --full --review NEW_REVIEW_DIR`. Inspect actual outputs on either route. Execution success does not mean a scan passed or a person reviewed the video. When the owner wants a quicker refresh of checked data, follow the trade-offs in the guide's `speed.md` and say which checks were reduced; agent-written terminal text always needs reading. Fix failed inputs and actions rather than suppressing checks. Give the owner the final files and pending review items.
|
|
20
|
+
For browser/VHS projects, run `reshoot preview CONFIG`, inspect frames and expected results, then `reshoot run CONFIG`. Read the printed run report. For native apps, inspect a standalone test take, then capture and edit a fresh final in a new folder. Generate check sheets for source and final media with `reshoot check SOURCES --full --review NEW_REVIEW_DIR`. Inspect actual outputs on either route. Execution success does not mean a scan passed or a person reviewed the video. When the owner wants a quicker refresh of checked data, follow the trade-offs in the guide's `speed.md` and say which checks were reduced; agent-written terminal text always needs reading. Fix failed inputs and actions rather than suppressing checks. Give the owner the final files and pending review items. For a versioned run, if this CLI provides `reshoot accept`, the run prints the command that records the owner's verdict; pass it on with `--flagged LABEL` for each flagged item. Run it, or pass `--yes`, only when the owner explicitly asks you to accept.
|
|
21
21
|
|
|
22
22
|
For combined web and VHS edits, configure matching take dimensions. Mixed previews retain the browser's configured dimensions because VHS keeps its tape dimensions.
|
|
23
23
|
|
|
24
|
-
Keep captions short and explain the result, not the click. Leave time to read. Identify mocked responses or shortened waits. Do not change the product claim merely to make a recording finish.
|
|
24
|
+
Keep captions short and explain the result, not the click. Leave time to read. Judge text size in the README GIF, which is usually 800 px wide: when the app's content is a narrow column in the 1280x720 frame, set a narrower `size`, such as `"960x540"`, or `zoom = true`, so the result stays readable. A narrower viewport enlarges the text; a higher pixel ratio does not. For a dropdown, use a `select` action with the option's visible label, if this CLI supports it, rather than key presses. Identify mocked responses or shortened waits. Do not change the product claim merely to make a recording finish.
|
|
25
25
|
|
|
26
26
|
Inspect captions after page navigation. The overlay restores the current caption on same-origin page loads; zoomed or styled captions are drawn after recording.
|
|
27
27
|
|
|
@@ -7,7 +7,7 @@ description: Update an existing reshoot demo after an app or release changes. Us
|
|
|
7
7
|
|
|
8
8
|
For combined web and VHS edits, keep take dimensions matched. Mixed previews retain the configured browser dimensions to match the unchanged VHS output size.
|
|
9
9
|
|
|
10
|
-
Requires reshoot CLI 0.2 or newer. Run `reshoot --version`. For a versioned browser/VHS project, read its brief, config, storyboard and previous run before changing anything, then run `reshoot doctor --config CONFIG`.
|
|
10
|
+
Requires reshoot CLI 0.2 or newer. Run `reshoot --version`. For a versioned browser/VHS project, read its brief, config, storyboard and previous run before changing anything, then run `reshoot doctor --config CONFIG`. If this CLI provides `reshoot runs`, use it to list runs; a run path ending in `accepted` or `latest` resolves to the newest accepted or successful run. Record the exact ID of the baseline you compare against, since aliases move as new runs finish.
|
|
11
11
|
|
|
12
12
|
For a standalone native recording, read the brief, capture/edit scripts and exact previous media instead of expecting a versioned config or run report. Look up the current demo window ID, reset synthetic data, and use `reshoot window APP NEW_TAKE.mov --id ID -s SECONDS` with app-specific automation or coordinated manual interaction. Keep test and fresh final takes in new folders; edit and scan final exports. Compare the old/new MP4s and animated GIFs manually. Disclose that browser assertions, versioned `preview`/`run`, and automated beat comparison do not apply. Follow the versioned flow below only for browser/VHS projects.
|
|
13
13
|
|
|
@@ -19,4 +19,4 @@ A failed or missing browser take leaves the overall expectations unverified unle
|
|
|
19
19
|
|
|
20
20
|
If this CLI version provides `reshoot compare`, compare the previous and new runs by beat and inspect the actual frames. Otherwise, show the old and new recordings and disclose that comparison is manual. A successful run must not silently replace the chosen comparison baseline or inherit approval from it.
|
|
21
21
|
|
|
22
|
-
Use only made-up data and page, terminal, or single-window capture. Call out changes to the story's claim, fixture meaning, speed, or narration. Present the new files and unresolved items. Publishing and repository changes follow the owner's existing authorization.
|
|
22
|
+
Use only made-up data and page, terminal, or single-window capture. Call out changes to the story's claim, fixture meaning, speed, or narration. Present the new files and unresolved items. The new run needs its own verdict: give the owner its `reshoot accept RUN` command when this CLI provides it, and do not run it without their explicit instruction. Publishing and repository changes follow the owner's existing authorization.
|
|
@@ -19,7 +19,7 @@ For a versioned run, if this CLI provides `reshoot review`, use its local page t
|
|
|
19
19
|
Previous and current GIFs display as images in the comparison page; inspect their
|
|
20
20
|
animation and loop as well as the MP4 playback.
|
|
21
21
|
|
|
22
|
-
The owner decides whether a recording is accepted. Do not run `check --ok
|
|
22
|
+
The owner decides whether a recording is accepted. For a versioned run, if this CLI provides `reshoot accept`, give the owner the exact command: `reshoot accept RUN`, plus `--flagged LABEL` for each flagged item they must inspect, or `--reject LABEL` for one that fails. Do not run `accept`, `check --ok` or an import, pass `--yes`, or promote a baseline merely because your inspection found nothing. Carry out those actions only when the owner explicitly authorizes the decision. Give the owner the files, evidence, and unresolved findings; do not publish without authorization.
|
|
23
23
|
|
|
24
24
|
`check --text-file FINAL.srt` scans exact caption text. Runs also check generated
|
|
25
25
|
subtitle and narration sidecars. Inspect text findings at their recorded cue times.
|
|
@@ -37,8 +37,9 @@ async def action(page, entry, events, root):
|
|
|
37
37
|
if kind == 'wait':
|
|
38
38
|
await item.wait_for(state='visible', timeout=timeout)
|
|
39
39
|
return
|
|
40
|
-
if await item.count() != 1:
|
|
41
|
-
|
|
40
|
+
if (found := await item.count()) != 1:
|
|
41
|
+
target = ', '.join(f'{k} = {v!r}' for k, v in entry['target'].items())
|
|
42
|
+
raise ValueError(f'{kind} target {{{target}}} matched {found} elements; it must match exactly one')
|
|
42
43
|
await item.scroll_into_view_if_needed(timeout=timeout)
|
|
43
44
|
box = await item.bounding_box()
|
|
44
45
|
rect = [box[k] for k in ('x', 'y', 'width', 'height')] if box else None
|
|
@@ -52,6 +53,9 @@ async def action(page, entry, events, root):
|
|
|
52
53
|
if entry.get('clear', True):
|
|
53
54
|
await item.fill('', timeout=timeout)
|
|
54
55
|
await item.press_sequentially(entry['text'], delay=65, timeout=timeout)
|
|
56
|
+
elif kind == 'select':
|
|
57
|
+
await page.wait_for_timeout(250) # let the cursor rest on the control before the choice changes
|
|
58
|
+
await item.select_option(entry['option'], timeout=timeout)
|
|
55
59
|
elif kind == 'key':
|
|
56
60
|
await item.press(entry['key'], timeout=timeout)
|
|
57
61
|
elif kind == 'upload':
|
|
@@ -59,6 +63,19 @@ async def action(page, entry, events, root):
|
|
|
59
63
|
events.append((time.monotonic(), {'kind': 'act', 'phase': 'end', 'rect': rect}))
|
|
60
64
|
|
|
61
65
|
|
|
66
|
+
async def evidence(page, frames, shot):
|
|
67
|
+
"""Save the beat's evidence: the last frame the video shows, at the end of its hold. A screenshot taken during
|
|
68
|
+
capture can take the frame that shows a result, and a still page sends no other, so the video would show the old
|
|
69
|
+
page through the whole hold."""
|
|
70
|
+
last = frames[-1][1] if len(frames) else None
|
|
71
|
+
if last is not None:
|
|
72
|
+
import io
|
|
73
|
+
from PIL import Image
|
|
74
|
+
Image.open(io.BytesIO(last)).save(shot)
|
|
75
|
+
else:
|
|
76
|
+
await page.screenshot(path=str(shot))
|
|
77
|
+
|
|
78
|
+
|
|
62
79
|
async def assertion(page, entry):
|
|
63
80
|
from playwright.async_api import expect
|
|
64
81
|
kind = entry['type']
|
|
@@ -150,11 +167,12 @@ async def capture(board, a, frames, out, metadata, spoken):
|
|
|
150
167
|
'mobile': bool(a.mobile or board.get('mobile')), 'screenWidth': w, 'screenHeight': h,
|
|
151
168
|
'dontSetVisibleSize': False})
|
|
152
169
|
async def frame(event):
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
170
|
+
# Chrome stamps each frame with the wall-clock time it was painted. Map that onto the beats' clock with
|
|
171
|
+
# an offset measured directly, not from when a frame arrives: large frames arrive late, and an offset
|
|
172
|
+
# taken from a late first frame would shift the whole video behind the actions.
|
|
173
|
+
timestamp = event['metadata'].get('timestamp')
|
|
174
|
+
painted = timestamp + clock if timestamp else time.monotonic()
|
|
175
|
+
frames.append((painted, base64.b64decode(event['data'])))
|
|
158
176
|
await cdp.send('Page.screencastFrameAck', {'sessionId': event['sessionId']})
|
|
159
177
|
frame_tasks = []
|
|
160
178
|
frame_handler = lambda event: frame_tasks.append(asyncio.create_task(frame(event)))
|
|
@@ -172,6 +190,7 @@ async def capture(board, a, frames, out, metadata, spoken):
|
|
|
172
190
|
await assertion(page, entry)
|
|
173
191
|
metadata['replayed_before'] = ids[:split]
|
|
174
192
|
events.clear()
|
|
193
|
+
clock = time.monotonic() - time.time()
|
|
175
194
|
await cdp.send('Page.startScreencast', {'format': 'jpeg', 'quality': 92, 'maxWidth': int(w*cap_dpr), 'maxHeight': int(h*cap_dpr)})
|
|
176
195
|
for beat in board['beat'][split:]:
|
|
177
196
|
current = beat['id']
|
|
@@ -195,10 +214,10 @@ async def capture(board, a, frames, out, metadata, spoken):
|
|
|
195
214
|
result.update(verified_at=time.monotonic(), trim_wait=bool(beat.get('trim_wait')), min_reading=max(beat.get('min_reading', 0), board.get('caption', {}).get('min_seconds', 0)))
|
|
196
215
|
shot = out.parent / (out.stem + '.evidence') / (current + '.png')
|
|
197
216
|
shot.parent.mkdir(exist_ok=True)
|
|
198
|
-
await page.screenshot(path=str(shot))
|
|
199
217
|
result.update(status='passed' if beat.get('expect') else 'unverified', screenshot=str(shot.relative_to(out.parent)), start=start)
|
|
200
218
|
await asyncio.sleep(max(beat.get('hold', 1), beat.get('min_reading', 0), board.get('caption', {}).get('min_seconds', 0) if beat.get('caption') else 0, spoken.get(current, ('', 0))[1] + .3 - (time.monotonic() - start)))
|
|
201
219
|
result['end'] = time.monotonic()
|
|
220
|
+
await evidence(page, frames, shot)
|
|
202
221
|
stop = time.monotonic()
|
|
203
222
|
await cdp.send('Page.stopScreencast')
|
|
204
223
|
cdp.remove_listener('Page.screencastFrame', frame_handler)
|
|
@@ -220,7 +239,12 @@ async def capture(board, a, frames, out, metadata, spoken):
|
|
|
220
239
|
metadata['failure_screenshot'] = str(failure.relative_to(out.parent))
|
|
221
240
|
except Exception:
|
|
222
241
|
pass
|
|
223
|
-
|
|
242
|
+
where = f'beat {current!r}' if current else 'opening the storyboard url'
|
|
243
|
+
hint = ''
|
|
244
|
+
if 'ERR_UNSAFE_PORT' in str(e):
|
|
245
|
+
hint = ('. Chrome refuses some ports, such as 5060 and 6000: run the app on another port, or set '
|
|
246
|
+
'RESHOOT_CHROME_FLAGS=--explicitly-allowed-ports=PORT')
|
|
247
|
+
raise RuntimeError(f'{where} failed: {redact(str(e))}{hint}') from None
|
|
224
248
|
finally:
|
|
225
249
|
cdp.remove_listener('Page.screencastFrame', frame_handler)
|
|
226
250
|
page.remove_listener('response', resource_handler)
|
|
@@ -252,7 +252,8 @@ def perform(a, report):
|
|
|
252
252
|
for label, flags in flagged.items():
|
|
253
253
|
print(f" {label} {key[label][0]}: " + "; ".join(f"{why} {part}" for why, part in flags))
|
|
254
254
|
print(f"{len(key)} frames, images, or text findings not checked yet, on {(len(todo) + 3) // 4} sheets in {review}/. Look at each one "
|
|
255
|
-
"at full size
|
|
255
|
+
"at full size. For a run, record the verdict with `reshoot accept RUN`; for these sheets alone, run "
|
|
256
|
+
"`reshoot check --fail LABEL...` for any that fail and `reshoot check --ok`.")
|
|
256
257
|
return 1
|
|
257
258
|
|
|
258
259
|
|
|
@@ -8,9 +8,10 @@ def main(argv=None):
|
|
|
8
8
|
ap.add_argument("--version", action="version", version=version("reshoot"))
|
|
9
9
|
sub = ap.add_subparsers(dest="cmd", required=True)
|
|
10
10
|
|
|
11
|
-
from reshoot import web, window, check
|
|
11
|
+
from reshoot import web, window, check, story
|
|
12
12
|
p = sub.add_parser("web", help="record a web page acting out a storyboard, in headless Chrome",
|
|
13
|
-
description=web.__doc__,
|
|
13
|
+
description=story.__doc__ + "\nLegacy Python storyboards\n\n" + web.__doc__,
|
|
14
|
+
formatter_class=argparse.RawDescriptionHelpFormatter)
|
|
14
15
|
p.add_argument("storyboard"); p.add_argument("out", help="the video to write, such as takes/web.mp4")
|
|
15
16
|
p.add_argument("--url", help="the page to open (default: the storyboard's URL)")
|
|
16
17
|
p.add_argument("--size", help="CSS width x height @ device pixel ratio, such as 390x844@3 (default 1280x720@1.5)")
|
|
@@ -95,15 +96,46 @@ def main(argv=None):
|
|
|
95
96
|
p.add_argument('--directory', default='.', help='project root receiving the skill bundle')
|
|
96
97
|
p.set_defaults(run=skills.install)
|
|
97
98
|
from reshoot import review
|
|
98
|
-
|
|
99
|
-
|
|
99
|
+
runs = ('A run is its folder, such as demo/takes/runs/ID. End the path with `latest` for the newest successful '
|
|
100
|
+
'run or `accepted` for the newest accepted one, such as demo/latest.')
|
|
101
|
+
p = sub.add_parser('runs', help='list a project\'s runs, newest first, with their result and review status',
|
|
102
|
+
description='List the runs of a versioned project, newest first. ' + runs)
|
|
103
|
+
p.add_argument('project', nargs='?', default='.', help='the project folder or its reshoot.toml (default: here)')
|
|
104
|
+
p.set_defaults(run=review.listing)
|
|
105
|
+
p = sub.add_parser('compare', help='compare matching beat IDs between two explicit runs',
|
|
106
|
+
description='Compare two runs beat by beat: which beat IDs match, and whether each beat\'s '
|
|
107
|
+
'definition, duration, evidence frame and results changed. A matched ID does not mean equal '
|
|
108
|
+
'pixels. ' + runs)
|
|
109
|
+
p.add_argument('previous', help='the earlier run, usually the accepted one')
|
|
110
|
+
p.add_argument('current', help='the run to compare against it')
|
|
111
|
+
p.add_argument('--out', help='write the comparison JSON here instead of printing it')
|
|
112
|
+
p.add_argument('--markdown', metavar='FILE', help='also write a short Markdown summary, such as for a pull request')
|
|
100
113
|
p.set_defaults(run=lambda a: (review.compare(a), 0)[1])
|
|
101
|
-
p = sub.add_parser('review', help='create a local review page or import owner decisions'
|
|
102
|
-
|
|
114
|
+
p = sub.add_parser('review', help='create a local review page or import owner decisions',
|
|
115
|
+
description='Write RUN/review.html: the exports, each beat\'s result and evidence, and the scan '
|
|
116
|
+
'items, with a comparison when --previous is given. Runs write this page themselves; use this to '
|
|
117
|
+
'add a comparison or rebuild it. ' + runs)
|
|
118
|
+
p.add_argument('run_path', help='the run to review')
|
|
119
|
+
p.add_argument('--previous', help='an earlier run to compare against, usually the accepted one')
|
|
120
|
+
p.add_argument('--import-decisions', metavar='FILE',
|
|
121
|
+
help='record per-beat decisions downloaded from the page; `reshoot accept` is the one-step route')
|
|
103
122
|
p.set_defaults(run=review.main)
|
|
104
|
-
p = sub.add_parser('
|
|
105
|
-
|
|
106
|
-
|
|
123
|
+
p = sub.add_parser('accept', help='accept a run after watching it: exports, beats and scan items in one step',
|
|
124
|
+
description='Accept a run after watching its exports and looking at its scan sheets. Unflagged '
|
|
125
|
+
'items are accepted together; each flagged item must be named with --flagged or --reject. The '
|
|
126
|
+
'verdict is bound to the run\'s exact files and scan policy.')
|
|
127
|
+
p.add_argument('run_path', help='the run folder, such as demo/takes/runs/ID or demo/latest')
|
|
128
|
+
p.add_argument('--flagged', nargs='+', metavar='LABEL', help='flagged items you inspected and accept')
|
|
129
|
+
p.add_argument('--reject', nargs='+', metavar='LABEL', help='items that must not be shared; marks the run failed')
|
|
130
|
+
p.add_argument('--yes', action='store_true', help='skip the confirmation; only for the owner\'s own scripts')
|
|
131
|
+
p.set_defaults(run=review.accept)
|
|
132
|
+
p = sub.add_parser('export', help='copy explicitly chosen run media to a local directory', description=runs)
|
|
133
|
+
p.add_argument('run_path', help='the run to copy from, such as demo/latest')
|
|
134
|
+
p.add_argument('--directory', help='where to copy the chosen --file media')
|
|
135
|
+
p.add_argument('--file', action='append', help='a file listed in run.json, such as takes/demo-final.mp4')
|
|
136
|
+
p.add_argument('--replace', action='store_true', help='overwrite media already in --directory, such as the repo\'s demo')
|
|
137
|
+
p.add_argument('--record', metavar='DIR', help='also write the run\'s small record (report, beat results, evidence '
|
|
138
|
+
'frames) to DIR, such as demo/accepted, for later comparisons')
|
|
107
139
|
p.set_defaults(run=project.export)
|
|
108
140
|
a = ap.parse_args(argv)
|
|
109
141
|
try:
|
|
@@ -56,8 +56,11 @@ def init(a):
|
|
|
56
56
|
if conflicts:
|
|
57
57
|
raise ValueError('existing files would be overwritten: ' + ', '.join(conflicts))
|
|
58
58
|
root.mkdir(parents=True, exist_ok=True)
|
|
59
|
+
port = None if a.url else free_port()
|
|
59
60
|
for name in names:
|
|
60
61
|
text = template.joinpath(name).read_text()
|
|
62
|
+
if port:
|
|
63
|
+
text = text.replace('8765', str(port))
|
|
61
64
|
if a.url:
|
|
62
65
|
if name == 'storyboard.toml':
|
|
63
66
|
text = 'schema_version = 1\nurl = ' + json.dumps(a.url) + '\n[[beat]]\nid = "intro"\ncaption = "Replace this with one useful task"\nhold = 2\n'
|
|
@@ -69,19 +72,39 @@ def init(a):
|
|
|
69
72
|
return 0
|
|
70
73
|
|
|
71
74
|
|
|
75
|
+
def free_port():
|
|
76
|
+
"""8765 if nothing is using it, otherwise any free local port, so the notes example doesn't collide with another app."""
|
|
77
|
+
import socket
|
|
78
|
+
for port in (8765, 0):
|
|
79
|
+
with socket.socket() as s:
|
|
80
|
+
try:
|
|
81
|
+
s.bind(('127.0.0.1', port))
|
|
82
|
+
except OSError:
|
|
83
|
+
continue
|
|
84
|
+
return s.getsockname()[1]
|
|
85
|
+
|
|
86
|
+
|
|
72
87
|
def export(a):
|
|
73
|
-
from reshoot.review import read
|
|
88
|
+
from reshoot.review import read, record
|
|
74
89
|
root, report = read(a.run_path)
|
|
75
90
|
selected = a.file or []
|
|
91
|
+
if not selected and not getattr(a, 'record', None):
|
|
92
|
+
raise ValueError('choose at least one --file from run.json files, or --record')
|
|
93
|
+
if selected and not a.directory:
|
|
94
|
+
raise ValueError('--file needs --directory')
|
|
95
|
+
if getattr(a, 'record', None):
|
|
96
|
+
print(record(root, report, a.record))
|
|
76
97
|
if not selected:
|
|
77
|
-
|
|
98
|
+
return 0
|
|
78
99
|
known = {item['path'] for item in report.get('files', [])}
|
|
79
100
|
if not set(selected) <= known:
|
|
80
101
|
raise ValueError('export names an artifact not recorded in this run')
|
|
81
102
|
destination = Path(a.directory).resolve()
|
|
82
103
|
targets = [destination / Path(name).name for name in selected]
|
|
83
|
-
if len(set(targets)) != len(targets)
|
|
84
|
-
raise ValueError('export would
|
|
104
|
+
if len(set(targets)) != len(targets):
|
|
105
|
+
raise ValueError('export would collide with another selected name')
|
|
106
|
+
if not getattr(a, 'replace', False) and any(p.exists() for p in targets):
|
|
107
|
+
raise ValueError('export would overwrite a file; pass --replace to update media already there')
|
|
85
108
|
if any(p.suffix not in ('.mp4', '.gif', '.srt', '.mov', '.png') for p in targets):
|
|
86
109
|
raise ValueError('export accepts final media and subtitle files')
|
|
87
110
|
destination.mkdir(parents=True, exist_ok=True)
|
|
@@ -74,7 +74,12 @@
|
|
|
74
74
|
send({ kind: "key", key: ev.key, where: locate(ev.target) });
|
|
75
75
|
}, true);
|
|
76
76
|
addEventListener("change", ev => {
|
|
77
|
-
if (ev.isTrusted
|
|
77
|
+
if (!ev.isTrusted) return;
|
|
78
|
+
const e = ev.target;
|
|
79
|
+
// A dropdown choice replays as a select action, found by the option's visible label.
|
|
80
|
+
if (e.matches("select:not([multiple])") && e.selectedOptions.length)
|
|
81
|
+
send({ kind: "select", where: locate(e), option: e.selectedOptions[0].label });
|
|
82
|
+
else if (e.matches("select,input[type=checkbox],input[type=radio]"))
|
|
78
83
|
send({kind: "unsupported-change"});
|
|
79
84
|
}, true);
|
|
80
85
|
addEventListener("drop", ev => { if (ev.isTrusted) send({kind: "unsupported-drop"}); }, true);
|
|
@@ -51,6 +51,11 @@ def steps_from(calls, include_tab=False):
|
|
|
51
51
|
secret=ev.get("secret")))
|
|
52
52
|
elif kind == "click":
|
|
53
53
|
steps.append(dict(start=t, verb="click", where=ev["where"]))
|
|
54
|
+
elif kind == "select":
|
|
55
|
+
if last and last["verb"] == "click" and last["where"] == ev["where"]:
|
|
56
|
+
steps.pop() # the click that opened the dropdown; select replays the whole choice
|
|
57
|
+
t = last["start"]
|
|
58
|
+
steps.append(dict(start=t, verb="select", where=ev["where"], option=ev["option"]))
|
|
54
59
|
elif kind == "key" and ev["key"] in (("Enter", "Escape", "Tab") if include_tab else ("Enter", "Escape")):
|
|
55
60
|
steps.append(dict(start=t, verb="key", where=ev["where"], key=ev["key"]))
|
|
56
61
|
elif kind == "scroll":
|
|
@@ -153,7 +158,7 @@ def main(a):
|
|
|
153
158
|
|
|
154
159
|
|
|
155
160
|
def write_toml(path, url, size, calls):
|
|
156
|
-
unsupported = [ev['kind'] for _, ev in calls if ev['kind'] not in ('click', 'type', 'key', 'scroll')]
|
|
161
|
+
unsupported = [ev['kind'] for _, ev in calls if ev['kind'] not in ('click', 'type', 'select', 'key', 'scroll')]
|
|
157
162
|
if unsupported:
|
|
158
163
|
raise ValueError('recording includes unsupported interactions: ' + ', '.join(sorted(set(unsupported))))
|
|
159
164
|
lines = ['schema_version = 1', 'url = ' + json.dumps(url), 'size = ' + json.dumps(size)]
|
|
@@ -173,6 +178,8 @@ def write_toml(path, url, size, calls):
|
|
|
173
178
|
if step['verb'] == 'type':
|
|
174
179
|
value = 'demo-password' if step.get('secret') else step['value']
|
|
175
180
|
lines += ['text = ' + json.dumps(value), 'clear = true']
|
|
181
|
+
elif step['verb'] == 'select':
|
|
182
|
+
lines += ['option = ' + json.dumps(step['option'])]
|
|
176
183
|
elif step['verb'] == 'key':
|
|
177
184
|
lines += ['key = ' + json.dumps(step['key'])]
|
|
178
185
|
elif step['verb'] == 'scroll':
|