Promotion and re-promotion (#57) #112

Merged
crowmaki merged 2 commits from issue-57-promotion into main 2026-10-09 06:10:56 +00:00
Owner

Closes #57. Part of #24 (User Stories 129, 148, 149, 154–157).

What changes

A filmmaker can finish the draft they like, and the final matches it.

  • Promote. promote_take(take, refine_seed?) queues an h3.promotion job built from what the draft recorded:
    • The prompt blob, the frame count and the exact reference bytes, wired into the same slots. They come from the Take's take_input rows, not from the Shot's current inputs.
    • Both latents, loaded as LoadLatent "<file> [output]" under the names the draft saved them as. The video latent goes through MinimaxH3LatentUpscaler3D and LTXVConcatAVLatent, then a refine pass at the final size of the Take's own Generation profile. The LoRAs and speed mode come from that profile too.
    • The draft's audio, which is always frozen by SetLatentNoiseMask with a 0 SolidMask.
    • A new random refine seed for each Promotion, unless one is typed. The seed is recorded on the final (rendition.refine_seed, V29).
  • Refusals. Promote refuses a Take with no rendered draft (a Single-pass final has none), a Take already being promoted (the existing unique index allows one pending final per Take), purged latents and a seed of 2^53 or more.
  • The Take stays as it was until the final lands. A Promotion is a final Rendition via = 'promotion'. While it is queued, rendering or finishing, the Take's status is still its ready draft, or the final being replaced. The Promotion shows beside the Take as TakeView.promotion (status, refine_seed, replacing), and the wall, table and details show a Promoting… / Re-promoting… badge with progress.
  • Re-promotion replaces the final only on success. The newest ready final is what plays, so a failed re-promotion leaves the old final playing. A failed Promotion shows Promotion failed with the reason, and Retry (retry_take) runs the same job again with the same refine seed.
  • A new final marks the Take unseen and keeps its circle, pin and notes. This happens only when it first lands. Retrying its post-steps later leaves seen alone.
  • Batch and keys. Promote in the Dailies table promotes every ticked Take, or none when any of them cannot be promoted. P in screening mode promotes, or re-promotes, the current Take. The details panel has an optional refine-seed field and Promote / Re-promote.
  • Queue. Promotion jobs are JobClass::Final, so Hold finals applies, and they are labelled 1A-3 promotion.
  • Other changes.
    • takes::build_graph is shared by drafts, Single-pass finals and Promotion.
    • The "Draft only" filter now counts a Take whose final is still on its way.

Tests

  • tests/promotion.rs drives everything through ProjectSession against the fake ComfyUI. It covers the graph, landing, re-promotion while blocked, failure and retry, refusals, batch, and seen on a post-step retry.
  • The graph test compares the refine pass with the research job from research/h3-latent-reuse. That job is copied into tests/fixtures/research-promotion.json. The test checks the scheduler, sampler, upscaler and sparse-attention settings, and the latent → upscaler → concat → sampler wiring. It also checks that the conditioning matches the draft's prompt and references.
  • The research graph has no audio mask (that measurement is what showed one is needed), so the zero mask is checked on its own.
  • src/lib/dailies/promotion.test.ts covers the pure UI rules.

Not here, or worth a look

  • Story 154's stale-input warning is in #58, which lists "Promoting a stale Take warns, naming the stale inputs". Staleness doesn't exist yet.
  • Unverified on real hardware:
    • The upscaler keeps the template's megapixels mode. The round trip measured 2.1 MP, which turned a 1376×768 draft into a 1984×1120 final, and I scale that number by the final's area.
    • The default 16:9 profile reproduces the measured run exactly.
    • At other ratios the upscaler's own rounding may land up to 32 px off the refine size. If it does, the fix is to switch the template to the node's target dimensions mode, but I couldn't confirm that mode's input names without a real ComfyUI.
  • Old finals are kept. A re-promotion doesn't delete the final it replaces. The old row and files stay, and Storage (#69/#70) can reclaim them. Later consumers (Animatic, export) must pick the newest ready final, which is what playableRendition already does.
  • Line marks: they don't exist yet (#63). A Promotion never touches Take judgments, so they carry over when they arrive.
  • Test run: cargo test --workspace --all-features passes except two ffmpeg::tests proxy tests. They fail on this machine because its ffmpeg has no libopenh264 encoder. bun test src and bun run check are clean.

Screenshots

The wall: 1A-1 is re-promoting and still shows its earlier final (F). 1A-3 is circled with a landed final. 1A-4 and 1B are drafts.

Wall

Details of 1A-1 while it re-promotes with refine seed 424242. The final it replaces (refine 7876…) keeps playing.

Re-promoting

A failed re-promotion of 1A-2: the reason, Retry, and the old final still in place.

Promotion failed

The table after Promote on two ticked drafts.

Table

Screening mode after P on 1B-1.

Screening

The clips come from a stand-in ComfyUI that serves ffmpeg test clips.

Closes #57. Part of #24 (User Stories 129, 148, 149, 154–157). ## What changes A filmmaker can finish the draft they like, and the final matches it. - **Promote.** `promote_take(take, refine_seed?)` queues an `h3.promotion` job built from what the draft recorded: - The prompt blob, the frame count and the exact reference bytes, wired into the same slots. They come from the Take's `take_input` rows, not from the Shot's current inputs. - Both latents, loaded as `LoadLatent "<file> [output]"` under the names the draft saved them as. The video latent goes through `MinimaxH3LatentUpscaler3D` and `LTXVConcatAVLatent`, then a refine pass at the final size of the Take's own Generation profile. The LoRAs and speed mode come from that profile too. - The draft's audio, which is always frozen by `SetLatentNoiseMask` with a 0 `SolidMask`. - A new random refine seed for each Promotion, unless one is typed. The seed is recorded on the final (`rendition.refine_seed`, V29). - **Refusals.** Promote refuses a Take with no rendered draft (a Single-pass final has none), a Take already being promoted (the existing unique index allows one pending final per Take), purged latents and a seed of 2^53 or more. - **The Take stays as it was until the final lands.** A Promotion is a final Rendition `via = 'promotion'`. While it is queued, rendering or finishing, the Take's status is still its ready draft, or the final being replaced. The Promotion shows beside the Take as `TakeView.promotion` (`status`, `refine_seed`, `replacing`), and the wall, table and details show a **Promoting… / Re-promoting…** badge with progress. - **Re-promotion replaces the final only on success.** The newest ready final is what plays, so a failed re-promotion leaves the old final playing. A failed Promotion shows **Promotion failed** with the reason, and **Retry** (`retry_take`) runs the same job again with the same refine seed. - **A new final** marks the Take unseen and keeps its circle, pin and notes. This happens only when it first lands. Retrying its post-steps later leaves seen alone. - **Batch and keys.** **Promote** in the Dailies table promotes every ticked Take, or none when any of them cannot be promoted. **P** in screening mode promotes, or re-promotes, the current Take. The details panel has an optional refine-seed field and **Promote / Re-promote**. - **Queue.** Promotion jobs are `JobClass::Final`, so Hold finals applies, and they are labelled `1A-3 promotion`. - **Other changes.** - `takes::build_graph` is shared by drafts, Single-pass finals and Promotion. - The "Draft only" filter now counts a Take whose final is still on its way. ### Tests - `tests/promotion.rs` drives everything through `ProjectSession` against the fake ComfyUI. It covers the graph, landing, re-promotion while blocked, failure and retry, refusals, batch, and seen on a post-step retry. - The graph test compares the refine pass with the research job from `research/h3-latent-reuse`. That job is copied into `tests/fixtures/research-promotion.json`. The test checks the scheduler, sampler, upscaler and sparse-attention settings, and the latent → upscaler → concat → sampler wiring. It also checks that the conditioning matches the draft's prompt and references. - The research graph has no audio mask (that measurement is what showed one is needed), so the zero mask is checked on its own. - `src/lib/dailies/promotion.test.ts` covers the pure UI rules. ### Not here, or worth a look - **Story 154's stale-input warning** is in #58, which lists "Promoting a stale Take warns, naming the stale inputs". Staleness doesn't exist yet. - **Unverified on real hardware:** - The upscaler keeps the template's `megapixels` mode. The round trip measured 2.1 MP, which turned a 1376×768 draft into a 1984×1120 final, and I scale that number by the final's area. - The default 16:9 profile reproduces the measured run exactly. - At other ratios the upscaler's own rounding may land up to 32 px off the refine size. If it does, the fix is to switch the template to the node's `target dimensions` mode, but I couldn't confirm that mode's input names without a real ComfyUI. - **Old finals are kept.** A re-promotion doesn't delete the final it replaces. The old row and files stay, and Storage (#69/#70) can reclaim them. Later consumers (Animatic, export) must pick the newest ready final, which is what `playableRendition` already does. - **Line marks:** they don't exist yet (#63). A Promotion never touches Take judgments, so they carry over when they arrive. - **Test run:** `cargo test --workspace --all-features` passes except two `ffmpeg::tests` proxy tests. They fail on this machine because its ffmpeg has no `libopenh264` encoder. `bun test src` and `bun run check` are clean. ## Screenshots The wall: 1A-1 is re-promoting and still shows its earlier final (F). 1A-3 is circled with a landed final. 1A-4 and 1B are drafts. ![Wall](https://git.crowmaki.me/attachments/31c53c24-c3f7-4018-aa7a-46b3a122ad5f) Details of 1A-1 while it re-promotes with refine seed 424242. The final it replaces (refine 7876…) keeps playing. ![Re-promoting](https://git.crowmaki.me/attachments/a5d1665c-002c-4f2a-a71d-46a3490245aa) A failed re-promotion of 1A-2: the reason, Retry, and the old final still in place. ![Promotion failed](https://git.crowmaki.me/attachments/46169f90-e839-4587-b26c-d6d33d19b412) The table after **Promote** on two ticked drafts. ![Table](https://git.crowmaki.me/attachments/a73922d9-3db4-4810-af63-16e88c070397) Screening mode after **P** on 1B-1. ![Screening](https://git.crowmaki.me/attachments/180f07e9-fa20-49b4-afb0-64f549c7115f) The clips come from a stand-in ComfyUI that serves ffmpeg test clips.
Promote queues h3.promotion from a draft's saved latents, rebuilt from the
draft's recorded prompt, slots and bytes, with the audio frozen and a new
refine seed recorded on the final. A Promotion that has not landed shows
beside the Take; re-promoting replaces the final only once the new one lands,
and a failed one offers Retry. Batch Promote in the Dailies table and P in
screening mode.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Retrying a landed final's post-steps no longer marks its Take unseen; only
the first landing of a Promotion does. The Take view tells a Promotion by
its via, promotable reads the draft into a typed row, and the Dailies share
one began-as-draft rule.
crowmaki deleted branch issue-57-promotion 2026-10-09 06:10:56 +00:00
Sign in to join this conversation.
No description provided.