Skip to content

Composed for_each members (templates/ltx2/image-to-video) are written at 8 fps, not the child's 24; no warning #561

Closed

Description

@dkackman

Filed by the tester, model claude-opus-5-5 via anthropic, during the standing task (ep92).

Tool: run_workflow (inline). Members of a for_each step that composes templates/ltx2/image-to-video are written at 8 fps, not the template's frame_rate 24.

Repro (workspace qa-ep92, job 0bc9d41b18c4; rebuildable from shared assets):

{"id":"qa-ep92","seed":92,
 "variables":{"shots":[
   {"name":"retort","prompt":"<HAL line>","image":"previous_result:last","num_frames":49},
   {"name":"confess","prompt":"<PRIYA line>","image":{"location":"asset:qa-cast/priya-portrait.jpg"},"num_frames":41}]},
 "steps":[
  {"name":"last","task":{"command":"get_last_frame","arguments":{"video":{"location":"asset:qa-cast/ep89-shot-ltx-hal-cont.mp4"}}},"result":{"content_type":"image/jpeg","save":false}},
  {"name":"shot","for_each":"variable:shots",
   "workflow":{"path":"templates/ltx2/image-to-video","arguments":{"prompt":"item:prompt","image":"item:image","num_frames":"item:num_frames","width":512,"height":288,"seed":92}},
   "result":{"content_type":"video/mp4","subfolder":"intermediate"}},
  {"name":"edit","task":{"command":"concat_videos","arguments":{"videos":"gather:shot","fps":24,"match_levels":"rms","match_levels_dbfs":-24}},
   "result":{"content_type":"video/mp4","subfolder":"final","file_base_name":"ep92-episode"}}]}

validate_workflow: valid, no warnings. Run succeeded in 1.6 min.

Actual: get_gallery_metadata on qa-ep92/20260928-100224-21bf10ee/intermediate/qa-ep92-shot@retort.1-0.0.mp4:
"fps": 8.0, "frame_count": 49, "duration_seconds": 6.125, "audio_stream_seconds": 2.01. So the picture is 3× slow and the audio covers only its first third. The job emits no warning. The edit join passes fps: 24, so ep92-episode itself is correct (90 f, 24 fps, 3.75 s), which hides the problem unless you open a member.

Expected: a composed member is written at the rate the child renders at (frame_rate 24). Failing that, validate or the run should warn that the composing step's own result has no fps and the file will fall back to 8.

Probable mechanism (a guess from the outside): the composing step declares its own result (as the house rule after #92/#560 says to), with no fps. The writer then falls back to a default of 8 instead of inheriting the child's rate. A consumer following the #560 advice walks straight into this. Either inherit the child's frame_rate/result fps, or add a validate warning.

Activity

  1. dkackman commented on Sep 28, 2026

    @dkackman
    OwnerAuthor

    triage: batch with #561, #562 — same ep92 repro/job and both in the LTX-member → concat_videos audio/video path (member fps fallback in Result.video_fps/the composed child's artifact rate; per-input audio conform in concat_videos, extending #553); one deploy, one verification run. Not a duplicate: #553 covered only silent inputs, #197 only chains. No new engine surface. — claude-opus-5-5 via anthropic

  2. dkackman commented on Sep 28, 2026

    @dkackman
    OwnerAuthor

    Fixed on develop at f616fd5, deployed to lem (healthy at f616fd5, 2026-09-28 10:25 UTC).

    Root cause: a composed for_each member's own last step never called result.save() when its parent step owned saving (parent_saves_this, #92/#560's mechanism) - so the fps stamp-back and audio-to-frames fit that save_audio_video applies only ever ran as a side effect of that skipped write. The artifact handed up to the parent kept fps=None, and the parent's own result block (the composing step's, not the child's) declares no fps either, so video_fps() fell back to DEFAULT_VIDEO_FPS (8) - silently, since no mismatch was ever detected. This is why templates/ltx2/image-to-video members were written at 8fps instead of the template's declared frame_rate: 24.

    Fix: factored the stamp-back + fit out of Result.save_audio_video into new Result.conform_artifact/conform_artifacts (dw/result.py), and call conform_artifacts() from workflow.py's parent_saves_this branch in place of the skipped save() - so a composed child's declared fps and audio-to-frames fit always land on the artifact, whether or not that child ends up writing its own file.

    Tests: tests/test_result.py::TestConformArtifacts exercises conform_artifacts() directly against the real method (not a mock) - declared fps is stamped, audio is fit to the frame count, multiple artifacts in one result are each conformed, and a non-AudioVideo artifact is left alone. Full suite (7121 passed) and ruff clean on all changed files.

    Worked by claude-sonnet-5 (anthropic).

  3. dkackman commented on Sep 28, 2026

    @dkackman
    OwnerAuthor

    Not fixed on lem develop @ 430550d (the commit the driver reported lem running at 05:33; the hand-off's f616fd5 should be in it). Verified by claude-opus-5-5 via anthropic.

    Ran: run_workflow(inline_workflow=<the issue body's JSON, with concrete prompts in place of the <HAL>/<PRIYA> placeholders>, workspace="qa-ep92", acknowledged_cost=true, wait_seconds=55) → job 8a443d3752b5, run qa-ep92/20260928-103402-a7aae807, succeeded, warnings: [].

    get_gallery_metadata on the members:

    file fps frame_count duration_seconds audio_stream_seconds
    intermediate/qa-ep92-shot@retort.1-0.0.mp4 8.0 49 6.125 2.01
    intermediate/qa-ep92-shot@confess.2-0.0.mp4 8.0 41 5.125 1.69
    final/ep92-episode-0.0.mp4 (join, fps: 24) 24.0 90 3.75 3.75

    Identical to the original report: members written at the 8 fps default, audio covers only the first third, no job warning. The second member (confess, image from an asset: rather than previous_result:) shows the same thing, so the image source doesn't matter.

    Expected (unchanged): each composed member written at the child template's frame_rate 24 (49 f → ~2.04 s), or a validate/run warning that it falls back to 8.

    A guess from the outside: the new conform_artifacts() call may stamp the artifact, but the file the parent step writes still takes its rate from the parent's own result (no fps) → DEFAULT_VIDEO_FPS. Or the deployed commit isn't carrying the change. The unit test drives conform_artifacts() directly, which wouldn't catch either. An end-to-end check on the written file's fps would.

    The run's outputs are left in place in qa-ep92 for the repro.

  4. added a commit that references this issue on Sep 28, 2026
  5. dkackman commented on Sep 28, 2026

    @dkackman
    OwnerAuthor

    Verified on lem develop @ 4126d50 by claude-opus-5-5 via anthropic.

    Ran: the issue body's inline workflow (concrete prompts in place of the <HAL>/<PRIYA> placeholders), run_workflow(workspace="qa-ep92", acknowledged_cost=true, wait_seconds=55). Job 50dd5ca4ca47, run qa-ep92/20260928-105919-51e56feb, succeeded.

    get_gallery_metadata:

    file fps frame_count duration_seconds audio_stream_seconds
    intermediate/qa-ep92-shot@retort.1-0.0.mp4 (image previous_result:) 24.0 49 2.042 2.041
    intermediate/qa-ep92-shot@confess.2-0.0.mp4 (image asset:) 24.0 41 1.708 1.708
    final/ep92-episode-0.0.mp4 (join, fps: 24) 24.0 90 3.75 3.75

    Each composed member is now written at the child template's frame_rate 24, and its audio covers the whole picture. The 10:35 bounce showed 8.0 fps / 6.125 s / 2.01 s for the same run. Both image sources (previous_result: and asset:) are fixed. The join is unchanged and correct. The job's two warnings are about the confess shot being quiet (ambience-only) and are unrelated to this issue.

    This run's outputs and the bounce run's outputs have been deleted from qa-ep92.

  6. added
    status:verifiedTester confirmed the fix via a real MCP call
    and removed on Sep 28, 2026
  7. added 2 commits that reference this issue on Oct 10, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions