Skip to content

feat(info): report what a scenario's media assets actually are - #178

Merged
LeadcodeDev merged 1 commit into
mainfrom
feat/media-io
Aug 12, 2026
Merged

feat(info): report what a scenario's media assets actually are#178
LeadcodeDev merged 1 commit into
mainfrom
feat/media-io

Conversation

@LeadcodeDev

Copy link
Copy Markdown
Owner

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.

$ rustmotion info -f scenario.json
...
Media assets:
  audio track 1: audio "voice.wav" → 3.00s, 44100Hz stereo
  view 1 / scene 1 / layer 1: image "logo.png" → 200×150
  view 1 / scene 1 / layer 2: video "clip.mp4" → 3.00s, 1080×1920 @ 30.00fps

Why info and not a probe subcommand

info had 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: info is 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

  • Audio goes through 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.
  • 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 a single ffprobe -show_streams -show_format, mirroring the existing ffmpeg_available() guard, and never decodes a frame.

Probing can never fail the command

Verified end to end, exit code included:

Media assets:
  audio track 1: audio "does-not-exist.wav" → file not found
  view 1 / scene 1 / layer 1: image "corrupt.png" → could not read: Failed to load image 'corrupt.png': unexpected end of file
  view 1 / scene 1 / layer 2: image "https://example.com/remote.png" → remote asset, not fetched
                                (http/https — would risk an unbounded download just to read a header)
EXIT=0

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:

  1. The schema cannot carry the field within scope. ResolvedScenario — what render/still/info/batch actually consume — is constructed in exactly one place, include.rs:76-82, outside this change's file list. A field added to Scenario would stay invisible to render.rs. And rustmotion-studio's empty_scenario() would break compiling regardless, a second out-of-scope file.
  2. "Do not re-render" is architecturally blocked. Per-frame RGBA buffers only materialise inside the per-format encode loops (h264.rs, ffmpeg.rs, formats.rs), none in scope — the frame is converted to YUV and dropped immediately inside a rayon closure. 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 separate rustmotion still invocation this feature is meant to replace.
  3. Captions cannot be reused cleanly. cmd_captions shells whisper.cpp (absent from this environment, so unverifiable here) and writes with a bare std::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.rs is 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 failures
  • cargo fmt --all --check and cargo clippy --workspace --all-targets -- -D warnings: clean
  • The 8 examples: 7 validate (the eighth is examples/ferriskey-presentation.json does not pass rustmotion validate (pre-existing on main) #157); rustmotion info exits 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 it
  • One additive Cargo feature (image's gif, so the Gif component's dimensions can be read); Cargo.lock unchanged, the dependency was already resolved elsewhere in the workspace

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 LeadcodeDev added the enhancement New feature or request label Aug 12, 2026
@LeadcodeDev LeadcodeDev self-assigned this Aug 12, 2026
@LeadcodeDev
LeadcodeDev merged commit 216292d into main Aug 12, 2026
3 checks passed
@LeadcodeDev
LeadcodeDev deleted the feat/media-io branch August 12, 2026 08:02
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.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

enhancement New feature or request

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant