feat(info): report what a scenario's media assets actually are - #178
Merged
Conversation
Closes the "media metadata readable from the scenario" gap. Nothing answered
"how long is this audio track" or "what size is this image", so an author
timing a scene against a track had to measure it somewhere else, by hand.
`rustmotion info` grows a Media assets section, following the same
walk-and-print shape the Springs and Text sizes sections already use rather
than adding a `probe` subcommand that would have rebuilt the same plumbing
and split "everything knowable about this scenario" across two commands.
Three probes, each reusing what already existed:
- Audio goes through `decode_audio_file`, the same decoder every render
uses — never a second decode path. The cost is a full decode, and that is
stated rather than hidden: this repository has no header-only audio path.
- Images use `image`'s `into_dimensions`, a header-only read the crate
already offered and nothing used; every existing decode goes through
`Image::from_encoded`, which rasterises in full to answer a question about
dimensions.
- Video shells one `ffprobe -show_streams -show_format`, mirroring the
existing `ffmpeg_available()` guard, and never decodes a frame.
**Probing can never fail the command.** A remote asset is never opened —
not the network, not the disk — because reading a header is not worth an
unbounded download. A missing file is reported as missing before any decode
is attempted. An unreadable one carries the exact reason. In all three
cases `info` exits 0 and every other section still prints:
audio track 1: audio "does-not-exist.wav" → file not found
layer 1: image "corrupt.png" → could not read: unexpected end of file
layer 2: image "https://…/remote.png" → remote asset, not fetched
The second gap in this batch — secondary artefacts emitted during the
render — is deliberately not delivered. Three independent blockers, each
sufficient alone, are recorded in the issue filed alongside: the only
construction site of `ResolvedScenario` is outside this change's scope (and
a second file would break compiling anyway); per-frame RGBA buffers only
exist inside the per-format encode loops, so the one in-scope function able
to produce a frame *is* the render, and calling it again reproduces exactly
the cost of the separate `still` invocation the feature is meant to replace;
and `captions.rs` writes non-atomically, contradicting the discipline PR
#145 and #151 established, while being frozen here.
LeadcodeDev
added a commit
that referenced
this pull request
Aug 12, 2026
…s re-scored (#180) Six features landed in #168-#178 that neither CLAUDE.md nor any rule mentions. For a tool whose scenarios are generated, a capability the rules do not describe may as well not exist — issue #165 was exactly this failure one level down, where the distribution channel dropped 17 rules. Two new rules: - `templates-and-iteration.md` — `components` / `for-each` / `use`, the largest new vocabulary, aimed at the failure mode the audit called dominant. - `motion-path.md` — the effect, its SVG path syntax, and why coordinates are deltas from the layout position. Two extended: - `geometry-safety.md` gains `text-autofit` as a fourth viewport control, next to `white-space`, `auto_scroll` and `overflow` — with the floor and the fact that a violation is still reported when shrinking is not enough. - `timeline-sequencing.md` gains what `style.transition` actually smooths: the four interpolated properties, and the three reasons a property snaps instead, since `validate` now names them. CLAUDE.md picks up the same, plus `--frames` / `concat`, `--hardware-acceleration`, and the `--fix` refusals. Writing the docs found a real trap, which is why the examples were run rather than only written. A `for-each` item that omits a field, forwarded through a `use`'s `props`, passes the literal `$name` instead of falling back to the component's `default` — a `params` default applies when `props` omits the key, not when it forwards an unresolved binding. Nothing is silent about it (an unresolved-variable warning, then the consumer rejects the value) but it is the natural way to write "optional field", so it now has its own section. The example that exposed it is corrected and validates. The two new rules ship without touching `skills.rs`: `skills install` goes from 49 files to 51, and the exhaustiveness test stays green. That is PR #168's build script doing what it was written for.
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.
Closes the "media metadata readable from the scenario" gap (High/M). The second gap in this batch — secondary artefacts during render — is deliberately not delivered; reasoning below and in the issue filed alongside.
What was missing
Nothing answered "how long is this audio track" or "what size is this image". An author timing a scene against a track had to measure it somewhere else, by hand.
Why
infoand not aprobesubcommandinfohad just gained two sections (Springs, Text sizes) with the same walk-scenario-then-print-if-non-empty shape. A third follows it without duplicating the loading, the view/scene traversal or the container recursion — and keeps "everything knowable about this scenario" in one command instead of two to remember.Assumed trade-off:
infois no longer instant once a scenario references media. That is the same category of cost the previous sections already introduced (Skia text measurement), not a new one, and it stays bounded because no remote asset is ever fetched.Each probe reuses what existed
decode_audio_file— the same decoder every render uses, never a second path. The cost is a full decode, stated rather than hidden: this repository has no header-only audio path.image'sinto_dimensions, a header-only read the crate already offered and nothing used. Every existing decode goes throughImage::from_encoded, which rasterises in full to answer a question about dimensions.ffprobe -show_streams -show_format, mirroring the existingffmpeg_available()guard, and never decodes a frame.Probing can never fail the command
Verified end to end, exit code included:
A remote asset is never opened — not the network, not the disk. A missing file is reported before any decode is attempted. An unreadable one carries the exact reason. Every other section still prints.
Why the artefacts gap is refused
Three independent blockers, each sufficient on its own, found by reading the code rather than assumed:
ResolvedScenario— whatrender/still/info/batchactually consume — is constructed in exactly one place,include.rs:76-82, outside this change's file list. A field added toScenariowould stay invisible torender.rs. Andrustmotion-studio'sempty_scenario()would break compiling regardless, a second out-of-scope file.h264.rs,ffmpeg.rs,formats.rs), none in scope — the frame is converted to YUV and dropped immediately inside arayonclosure. The one in-scope function able to produce an arbitrary frame is the render; calling it a second time reproduces exactly the cost of the separaterustmotion stillinvocation this feature is meant to replace.cmd_captionsshellswhisper.cpp(absent from this environment, so unverifiable here) and writes with a barestd::fs::write— not atomic, contradicting the discipline PR fix(cli): stop the helper commands destroying files they do not own #145 and fix(encode): make every output format render the frames it claims #151 established and that the brief required.captions.rsis frozen here, so that cannot be fixed in passing.An honest path exists for later and is recorded in the issue: extract the thumbnail from the already-produced video file with
ffmpeg -ss, which costs zero additional Skia work. It is useless until blocker 1 is resolved.Two halves — a non-atomic write contradicting an explicit requirement, or a scope breach — would have been a worse result than one gap closed and one refused with reasons.
Verification
cargo test --workspace: 30 targets, 1136 tests, 0 failurescargo fmt --all --checkandcargo clippy --workspace --all-targets -- -D warnings: cleanrustmotion infoexits 0 on all 8, and the new section is absent from every one — none references media, confirming unchanged behaviour for scenarios that do not use itimage'sgif, so theGifcomponent's dimensions can be read);Cargo.lockunchanged, the dependency was already resolved elsewhere in the workspace