Add separate MiniMax Music 3 advertisement workflow

This commit is contained in:
danrh
2026-08-24 02:48:59 -05:00
parent aaeec80037
commit db71a81524
41 changed files with 26505 additions and 19 deletions
+1 -1
View File
@@ -26,7 +26,7 @@ jobs:
run: |
python -m pip install --upgrade pip
python -m pip install --index-url https://download.pytorch.org/whl/cpu torch
python -m pip install "jsonschema>=4.26.0"
python -m pip install "jsonschema>=4.26.0" "numpy>=1.26.0" "Pillow>=10.0.0" "safetensors>=0.7.0"
- name: Run unit tests
run: python -m unittest discover -s tests -p "test_*.py" -v
+26 -3
View File
@@ -6,8 +6,15 @@ All notable changes to DiffusionGemma Director are recorded here. The format fol
### Added
- Added a documented real-world 30-second beverage-ad field test covering GPT Image 2 campaign references, two-lane MiniMax H3 execution, instrumental upload routing, identity retention, relay comparison, delivered masters, and a prioritized commercial-production hardening backlog.
- Added 17 exported audio, timing, and production-planning nodes, bringing the registered ComfyUI surface to 33 nodes.
- Added a documented real-world 30-second beverage-ad field test covering GPT Image 2 campaign references, two-lane MiniMax H3 execution, instrumental upload routing, identity retention, relay comparison, rendered review masters, and a prioritized commercial-production hardening backlog.
- Added a separate twenty-two-node Advertisement contract, shared-control, audio, Director-transport repair, planning, relay, runtime-memory, finishing, and QA surface, bringing the registered ComfyUI total to 55 nodes while leaving the established 33-node surface intact.
- Added versioned Advertisement schemas for campaign fields and claims, ordered performer/product references, soundtrack policy, locked audio, H3 planning, retained-tail relay, final mix, delivery status, adaptations, and media QA.
- Added a native MiniMax Music 3 Advertisement route with structured commercial timing, five-second candidate headroom, the locally verified pruned INT8/ConvRot text-encoder path, and lazy MiniMax Music 3 / Upload song / Legacy ACE-Step source selection.
- Added `examples/16_minimax_h3_ref2va_advertisement_music3_v1.json` as a separate user-editable Advertisement workflow with a fresh graph identity, flattened independent nodes, and immutable V6 lineage guards, plus a governed API-format companion fixture.
- Added deterministic exact-copy end-card rendering, exact 30-second master finishing, real 15-second and 6-second frame/audio cutdowns, and explicit status-only reporting for unrendered 1:1 and 16:9 adaptations.
- Added an Advertisement-owned model-memory barrier so Music 3 resources can be released before Director and H3 without inheriting unrelated cleanup-node packs.
- Added end-to-end runtime evidence for the exact release API graph on isolated ComfyUI `0.33.1`: two 15-second H3 lanes, persistent Picture 4 relay, every diagnostic root, a 720-frame/30-second master, and real 360-frame/15-second and 144-frame/6-second cutdowns with exact matching audio clocks. Visual spot review found stable performer/product continuity; formal QA remains blocked pending evidence-backed identity, copy, and audio-sync approval.
- Added 17 exported audio, timing, and production-planning nodes, bringing the established non-Advertisement ComfyUI surface to 33 nodes.
- Added decoded-song audition and locking with measured tempo, onset, tonal, clipping, silence, artifact, vocal-activity, recovery-window, excerpt, and whole-song diagnostics. Candidate and waveform-hash locks now fail closed instead of silently selecting different music.
- Added a custom-song path with a lazy upload node and independent **Generate with ACE-Step / Upload song** router. Uploaded songs reuse the established selector, excerpt, hash-lock, audio-guide, planner, and pristine final-mux path without waking dormant ACE candidate lanes.
- Added a single **Natural / audio-led sync**, **Dance / music sync**, or **Lyrics + lip sync** performance control shared by Director guidance, lyric selection, timed analysis, and H3 planning.
@@ -23,6 +30,12 @@ All notable changes to DiffusionGemma Director are recorded here. The format fol
### Changed
- Centralized Advertisement v1's 30-second duration, 9:16 aspect, native-shot count, Natural/Dance performance, Ref2VA mode, H3 audio/dialogue policies, excerpt start, generation model, 15-second lane ceiling, splitter aspect, and governed 30/15/6 deliverables in one shared control. Unsupported reference layouts, seam modes, Lyrics-without-evidence, and VO-without-input choices are hidden from the shipped UI.
- Labeled automatically saved Advertisement media as review drafts. QA approval remains separate, requires evidence for every pass, and never treats queue completion as delivery approval.
- Assigned Advertisement references by role: Picture 1 performer hero, Picture 2 same-performer contact sheet, Picture 3 independent product/package sheet, and Picture 4 persistent retained-tail relay for continuity only. Product and performer authority remain separate across every H3 lane.
- Split Advertisement audio into a soundtrack-only H3 motion guide and an exact final delivery mix. Optional non-diegetic VO, ducking, mix hashes, and clipping diagnostics affect only the final mix, never H3 conditioning, and cannot become an accidental lip-sync instruction.
- Made Advertisement soundtrack QC content-aware: instrumental generations do not require a vocal proxy, vocal generations do, and uploaded or legacy sources retain their meaningful technical-integrity and locking policies.
- Added explicit `pass`, `fail`, and `not_measured` Advertisement media-QA states plus an unremovable host baseline for product identity, performer identity, copy legibility, audio sync, and technical integrity. Queue completion, widget edits, and missing evidence can no longer be presented as identity, typography, product, or sync approval.
- Separated MiniMax-H3 native `[Shot N]` blocks from duration-bounded generation lanes. A single H3 invocation may now retain several native shots, and up to four planned lanes can cover a 60-second project when the configured 15-second lane ceiling and capacity constraints permit it.
- Reworked H3 lane boundaries to prefer native cuts with measured recovery, then native or mixed evidence, with deterministic balanced seams as a safe fallback. Non-native seams carry the active source shot and rebase timestamped cues rather than changing the master timeline.
- Made Project Master duration, aspect, soundtrack hash, excerpt, lane ceiling, and deliverables authoritative while leaving native shot choreography to the Director and H3 target profile.
@@ -35,6 +48,16 @@ All notable changes to DiffusionGemma Director are recorded here. The format fol
### Fixed
- Preserved the host Director packet's grounding, runtime, cache, and reference-authority evidence during bounded Advertisement transport recovery. Raw output is considered only after an explicit host JSON-parse failure with successful plain salvage and no true template fallback; duplicate keys, non-finite values, malformed Unicode, metadata disagreement, excess repairs, and authority drift remain blocked.
- Removed invalid `model` inputs from all four native `MiniMaxH3ReferenceToVideo` conditioning nodes while retaining model links on the scheduler and guiders. The UI builder now rejects every connected API input that is absent from live ComfyUI node metadata, preventing the same unexpected-keyword failure class from reaching execution.
- Corrected the saved Grounding Guard title to state its intentional `off` baseline instead of implying audit mode; the independent Advertisement reference and generation contracts remain active.
- Corrected the four H3 lane labels to distinguish persistent Picture 3 product authority from Picture 4 relay continuity, corrected the scheduler label to its actual seven-step setting, made the Director checkpoint path application-relative, and made the API manifest enumerate only its real source branch, all required Director/H3/Music 3 assets, and real reference hashes.
- Kept a runtime-observed Music 3 tempo miss honest: the technically passing candidate's `87.890625` BPM signal estimate versus requested `122` remains a visible advisory, while H3 continues to follow the selected waveform and exact-tempo campaigns can use a locked upload.
- Fixed the MiniMax Music 3 selection boundary where time-to-sample rounding could leave an otherwise valid excerpt one sample short. The mixer records and right-pads only deficits up to one millisecond; material shortages still fail.
- Fixed Advertisement control-link combo and ACE integer-BPM typing, selected-source BPM provenance and mismatch evidence, exact brand-case rendering, canonical report/tensor hash verification, frame-clock and pixel-aspect alignment, retained-tail cutdown selection, and false LoadImage missing-input warnings in the generated UI workflow.
- Canonicalized only unambiguous `Picture N` citations inside governed Advertisement subject-definition rows before strict validation, eliminating meaningless Director retries without relaxing free-form prompt validation.
- Prevented Advertisement product references from being parsed as a second performer identity, and prevented the retained-tail Picture 4 relay from becoming a new source identity or product authority.
- Prevented added voice-over from entering the H3 motion guide, generated instrumental candidates from failing for lack of vocal evidence, and incomplete visual/audio QA from becoming a false pass.
- Repaired MiniMax-H3 Ref2VA packets deterministically when model output contains malformed subject definitions, reference usage or retention rows, misplaced or short detailed descriptions, invalid cut timestamps, or omitted per-shot camera direction.
- Removed the one-shot Project Master mismatch that incorrectly compared H3 native shot blocks with the number of generation invocations.
- Prevented generic camera-safety text from making H3 output creatively anemic while retaining fail-closed validation for genuinely malformed camera contracts.
@@ -47,7 +70,7 @@ All notable changes to DiffusionGemma Director are recorded here. The format fol
### Repository hygiene
- Excluded generated runtime videos and identity/reference frames from Git and ComfyUI package archives. These files remain local and are not required by the source or tests.
- Kept migration tools and offline benchmark manifests in Git while excluding them from the install archive, and updated package metadata and the release checklist for the 33-node surface.
- Kept migration tools and offline benchmark manifests in Git while excluding them from the install archive, and updated package metadata and the release checklist for the 55-node surface.
- Made the legacy external V5 Desktop/ComfyUI byte-equality audit explicitly opt-in; the default release suite now relies on the checked-in V6 workflow's hermetic hash and contract validation.
## [0.2.0] - 2026-08-07
+14 -6
View File
@@ -4,15 +4,20 @@ Use this checklist from the standalone `ComfyUI-DiffusionGemmaPromptBuilder` rep
## Product Boundary
- Keep the exported surface at thirty-three node ids:
- Keep the exported surface at fifty-five node ids: the established thirty-three-node surface remains unchanged, and the separate Advertisement surface adds exactly twenty-two ids:
- eleven stable Director nodes: Model Loader, Context Hub, H3 Reference Context, Grounding Guard Settings, CoT Generator, JSON Splitter, Generation Gate, Branch Generation Gate, and the LTX-2.5, MiniMax-H3, and Ideogram 4 target profiles
- one deprecated universal Target Profile retained for saved-workflow compatibility
- two optional reference-preparation helpers: Reference Prep and H3 Reference Pair Prep
- two experimental SplatStage planners
- eleven audio/music-video controls: ten decoded-audio, source-routing, ACE, performance, and production-concept nodes plus Timed Lyrics Analyzer
- six production-planning controls: Project Master, Audio-Aware H3 Multi-Shot Planner, H3 Relay Reference Gate, Multi-Format Delivery Planner, H3 Shot Seed Fanout, and H3 Shot Assembler
- six Advertisement contract/settings nodes: Workflow Controls, Campaign Contract, Reference Contract, Reference Asset Prep, Soundtrack Contract, and Music3 Prompt Adapter
- three Advertisement audio nodes: Soundtrack Source, Audio Audition & Lock, and Motion Guide / Final Mix
- six Advertisement planning/relay nodes: Director Packet Repair, Master Contract, Planning Defaults, Multi-Shot Planner, Relay Artifact, and Relay Gate
- six Advertisement finishing/QA nodes: Master Assembler, End Card Renderer, Master Finisher, Adaptation Status, Cutdown Renderer, and Media QA Gate
- one Advertisement runtime node: Memory Barrier
- Keep the two SplatStage planners visibly experimental. Their mapping ids stay compatible, their display names end in `(Experimental)`, and their category is `prompt/diffusiongemma/experimental-motion-planning`.
- Confirm `NODE_CLASS_MAPPINGS` and `NODE_DISPLAY_NAME_MAPPINGS` contain the same thirty-three ids.
- Confirm `NODE_CLASS_MAPPINGS` and `NODE_DISPLAY_NAME_MAPPINGS` contain the same fifty-five ids, with no collision between the established thirty-three and the separate twenty-two Advertisement ids.
- Treat `ltx_reframe.py` and the reframe workflows as unexported prototypes. Do not describe them as installed nodes unless the package entry point and frontend are deliberately restored together.
- Do not bundle model weights or generated tensor dumps. The NVFP4 bridge remains source code in `nodes.py`.
@@ -38,7 +43,7 @@ node --check web/js/model_loader.js
node --test tests/*.mjs
```
- The unit suite must validate the exact thirty-three-node surface, the dedicated target-node boundaries, the Director/optional/experimental/audio/production-planning category split, both bundled SplatStage schemas, and retained reframe helpers.
- The unit suite must validate the exact fifty-five-node surface, preserve the established thirty-three-node boundary, validate the separate twenty-two-node Advertisement contract/audio/planning/finishing/runtime split, the dedicated target-node boundaries, both bundled SplatStage schemas, and retained reframe helpers.
- The readiness/schema smoke must finish with `Prompt Builder readiness/schema smoke passed`.
- The JavaScript suite must exercise the shipped `model_loader.js`. No test may import the retired `ltx_reframe_layout.js`.
- The default release suite validates checked-in artifacts only. Set `DG_VERIFY_DEPLOYED_V5_ARTIFACTS=1` solely when intentionally auditing the superseded, mutable V5 Desktop and ComfyUI copies; divergence there is deployment evidence to reconcile, not a hermetic release gate.
@@ -52,15 +57,18 @@ node --test tests/*.mjs
python proof_gates.py --model-path C:\ComfyUI\models\LLM\diffusiongemma-26B-A4B-it-NVFP4
```
2. Fresh-install the repository in a clean ComfyUI `0.30.1` custom-nodes folder and confirm all thirty-three nodes appear: eleven stable Director nodes, one legacy compatibility target, two optional reference-preparation nodes, two SplatStage planners only under the experimental motion-planning category, eleven audio/music-video controls, and six production-planning controls.
2. Fresh-install the repository in a clean ComfyUI `0.30.1` custom-nodes folder and confirm the established thirty-three-node surface still imports without regression. On ComfyUI `>=0.33.1`, confirm all fifty-five nodes appear: the established thirty-three plus the separate twenty-two Advertisement nodes under `prompt/diffusiongemma/advertising`.
3. Copy `examples/assets/character_motion_transfer` to `ComfyUI/input/character_motion_transfer` and load `examples/07_ltx23_character_motion_transfer.json`.
4. Load the MiniMax H3 T2VA and Ref2VA examples and confirm the Generation Gate blocks invalid or empty prompt packets.
5. Load `examples/15_minimax_h3_ref2va_music_video_v6.json`, replace both unbundled identity-image choices, and verify the default ACE source remains lazy with a blank upload. Exercise Upload song, Natural, Dance, Lyrics, and optional relay as separate branch tests; do not infer a successful media render from graph validation alone.
6. Treat longer experimental SplatStage and reframe workflows as separate opt-in validation, not as evidence for the stable Director compatibility promise.
6. On ComfyUI `>=0.33.1`, load `examples/16_minimax_h3_ref2va_advertisement_music3_v1.json`, select Picture 1 performer hero, Picture 2 same-performer sheet, and Picture 3 independent product sheet, and confirm the portable Director checkpoint path resolves locally. Verify MiniMax Music 3 remains the default and Upload song / Legacy ACE-Step stay lazy UI fallbacks.
7. Run the separate API fixture through the full two-lane H3 path. Require actual 30-second, 15-second, and 6-second review-draft videos, an exact 30-second soundtrack, all Director/QC/QA diagnostic roots, exact frame/audio clocks, and a persisted Picture 4 retained-tail relay. Queue completion alone is not proof.
8. Keep delivery blocked while performer identity, product identity, copy legibility, or audio sync remains `not_measured`. Review the media and record evidence before changing any QA state to `pass`; never treat deterministic end-card copy as proof that generated in-scene package text is exact.
9. Treat longer experimental SplatStage and reframe workflows as separate opt-in validation, not as evidence for the stable Director compatibility promise.
## Files Tracked on GitHub
- Package entry point and source: `__init__.py`, `nodes.py`, `audio_production_nodes.py`, `production_planning_nodes.py`, `timed_lyrics_nodes.py`, `ltx25_contract.py`, `ltx_reframe.py`, `proof_gates.py`
- Package entry point and source: `__init__.py`, `nodes.py`, `audio_production_nodes.py`, `production_planning_nodes.py`, `timed_lyrics_nodes.py`, `advertising_contract_nodes.py`, `advertising_audio_nodes.py`, `advertising_planning_nodes.py`, `advertising_finishing_nodes.py`, `advertising_runtime_nodes.py`, `ltx25_contract.py`, `ltx_reframe.py`, `proof_gates.py`
- Metadata and policy: `README.md`, `LICENSE`, `requirements.txt`, `pyproject.toml`, `.gitignore`, `.comfyignore`, this checklist
- Runtime contracts: `schemas/`, `web/`, `scripts/`
- Verification: `tests/`, `.github/workflows/ci.yml`
+80 -5
View File
@@ -4,13 +4,13 @@
# ComfyUI DiffusionGemma Prompt Builder
DiffusionGemma Director provides a stable five-node prompt-authoring core for ComfyUI: choose one model-specific Target Profile node between Context Hub and the shared Director/validation path. Four stable Director companion nodes, two optional reference-preparation nodes, eleven audio/music-video production controls, and six production-planning controls extend that core. The package also exports two experimental SplatStage motion-planning nodes. DiffusionGemma writes and validates prompts or blueprints; it is not the downstream image or video sampler.
DiffusionGemma Director provides a stable five-node prompt-authoring core for ComfyUI: choose one model-specific Target Profile node between Context Hub and the shared Director/validation path. Four stable Director companion nodes, two optional reference-preparation nodes, eleven audio/music-video production controls, and six production-planning controls extend that core. The package also exports two experimental SplatStage motion-planning nodes. A separate twenty-two-node Advertisement surface adds typed campaign, reference, soundtrack, shared settings, transport repair, planning, relay, finishing, and QA contracts without changing the established workflows. DiffusionGemma writes and validates prompts or blueprints; it is not the downstream image or video sampler.
The primary stable workflow is the working LTX 2.3 character motion-transfer graph in `examples/07_ltx23_character_motion_transfer.json`. Additional Director examples cover native MiniMax H3 T2VA and Ref2VA paths. The accumulated synchronized music-video production graph is preserved in `examples/15_minimax_h3_ref2va_music_video_v6.json`; replace its two saved `LoadImage` choices with your own primary identity image and same-person contact sheet before running it.
The primary stable workflow is the working LTX 2.3 character motion-transfer graph in `examples/07_ltx23_character_motion_transfer.json`. Additional Director examples cover native MiniMax H3 T2VA and Ref2VA paths. The accumulated synchronized music-video production graph is preserved in `examples/15_minimax_h3_ref2va_music_video_v6.json`; replace its two saved `LoadImage` choices with your own primary identity image and same-person contact sheet before running it. Advertisement work is additive and isolated: it does not migrate, overwrite, or silently change that known-good V6 graph.
## Exported Nodes
Thirty-three nodes are registered in ComfyUI. Their stable, compatibility, optional, and experimental surfaces are intentionally separate.
Fifty-five nodes are registered in ComfyUI: the established thirty-three-node Director/music-video surface and the separate twenty-two-node Advertisement surface. Stable, compatibility, optional, experimental, and Advertisement responsibilities remain intentionally distinct.
### Stable Director Assistant
@@ -86,7 +86,21 @@ These controls appear under `prompt/diffusiongemma/production-planning` and prov
- `DiffusionGemmaH3ShotAssembler` lazily requests only the planned H3 lanes, trims every decoded segment at its retained cut, verifies the master clock and soundtrack duration, concatenates exact frames, and passes the pristine audio object through unchanged.
- `DiffusionGemmaMultiFormatDeliveryPlanner` records requested reframes and cutdowns as plan-only deliverables; it never claims alternate media was rendered.
The Project Contract is authoritative for aspect, maximum H3 generation-segment length, and resulting lane count. Director and the MiniMax-H3 Target Profile remain authoritative for the unique native `[Shot N]` schedule inside those lanes. Missing ideal cut evidence is advisory rather than fatal; the plan exposes boundary-policy revision 2, selected strategy, evidence booleans, carry-in fragments, realized lane durations, and whether the non-binding preferred duration was actually realizable. Structural errors such as invalid clocks, duplicate/gapped/empty native shot blocks, out-of-range timestamps, stale audio locks, malformed identity manifests, impossible minimum/maximum limits, or more than four required lanes still fail closed. Missing, stale, weak, or malformed lyric timing also remains non-fatal: the planner records a warning and uses Natural/audio-led articulation. Written lyrics are never treated as a timing schedule. H3-generated audio is intentionally discarded; only the locked ACE excerpt is muxed into the assembled video.
### Advertisement Production System
The Advertisement nodes appear under `prompt/diffusiongemma/advertising`. They form a separate, versioned contract stack so commercial fields and delivery rules do not leak into the generic Director or the known-good music-video V6 workflow.
- Campaign and reference contracts type the brand, product, audience, objective, exact copy, substantiated claims, duration, aspect, end card, and deliverables. Reference preparation normalizes and hashes three user-selected image inputs; users do not paste hashes. Picture 1 is the performer hero, Picture 2 is a same-performer contact sheet, Picture 3 is an independent product/package contact sheet, and Picture 4 is a hash-verified, downscaled copy of the exact retained final frame from the previous H3 lane. Picture 4 is continuity evidence only and never becomes performer or product authority. Performer and product references are not merged into one identity sheet.
- `DiffusionGemmaAdvertisementWorkflowControls` is the saved workflow's one shared settings authority. Advertisement v1 intentionally locks the governed 30-second, 9:16 master and 30/15/6 deliverable contract; Ref2VA mode; H3 audio/dialogue policies; zero-second excerpt start; MiniMax H3 generation model; and 15-second lane ceiling. It shares one native-shot count with Director and the Advertisement planner, drives the splitter aspect, derives tail cutdown starts, and exposes only Natural or Dance performance. Unsupported reference layouts, non-relay seam modes, Lyrics performance without timed evidence, and separate VO without an input path are hidden from this v1 UI rather than offered as broken choices.
- The soundtrack contract supports `Instrumental`, `Vocal`, and `Auto` content policy and emits a structured MiniMax Music 3 caption plus separately tagged lyrics. The lazy source router exposes three origins: **MiniMax Music 3** by default, **Upload song**, and **Legacy ACE-Step**. An instrumental generated candidate does not need a vocal proxy; a vocal candidate does. All selected sources still pass technical integrity, excerpt, provenance, and waveform-hash locking.
- A 30-second campaign requests a 35-second MiniMax Music 3 candidate so the selector has five seconds of headroom; generated duration is capped at 60 seconds. The verified local route uses `minimax_music3_text_encoder_pruned_int8_convrot.safetensors` with the native MiniMax Music 3 text encoder, `minimax_music3_dit_fp16.safetensors`, and `minimax_music3_dav.safetensors`.
- The Advertisement mixer sends a soundtrack-only motion guide to H3. Its reusable node contract supports later non-diegetic voice-over with ducking, hashes, and clipping diagnostics, but the shipped v1 workflow locks VO to `None` until an upload/loader path is connected. A sub-millisecond source-window rounding deficit may be right-padded by at most one millisecond and is recorded; materially short audio still fails closed.
- The Advertisement planner preserves the Picture 1/2 performer authorities and Picture 3 product authority on every lane, while Picture 4 carries only the retained previous-lane tail. The finishing path rejects raster dimensions outside the model-compatible contracted-aspect tolerance, assembles an exact 30-second 9:16 master, replaces its governed tail with a deterministic exact-copy end card, and renders actual 15-second and 6-second frame/audio cutdowns. Requested 1:1 and 16:9 adaptations remain explicitly unrendered until a semantic reframe renderer is connected.
- Media QA reports `pass`, `fail`, or `not_measured` per check. Product identity, performer identity, copy legibility, audio sync, and technical integrity are a host-owned baseline that an edited widget cannot remove. A bare `pass` without evidence is downgraded to `not_measured`. The three automatic video saves are explicitly labeled and stored as review drafts; a completed ComfyUI queue is not delivery approval.
The Advertisement-owned Director transport repair uses the host-final packet by default and preserves its grounding, runtime, and cache evidence. It may recover the raw model-authored H3 object only when the host explicitly reports JSON parsing failed, plain salvage succeeded, and no real template fallback occurred. Recovery is bounded to invalid in-string backslashes plus exact governed Subject-row delimiters completed from the SHA-locked reference contract; duplicate keys, non-finite values, malformed Unicode, excess repairs, extra prose, metadata disagreement, and reference-authority changes fail closed. The existing H3 validator remains authoritative for prompt structure. The Advertisement-owned memory barrier releases Music 3 resources before Director and H3 without requiring unrelated cleanup-node packs. Schemas under `schemas/advertisement_*.schema.json` version the campaign, references, soundtrack, planning, relay, mix, delivery, adaptation, and QA payloads. Assembly, end-card, and finish reports are hash- and tensor-verified by their host nodes but do not currently have standalone JSON Schema files.
The Project Contract is authoritative for aspect, maximum H3 generation-segment length, and resulting lane count. Director and the MiniMax-H3 Target Profile remain authoritative for the unique native `[Shot N]` schedule inside those lanes. Missing ideal cut evidence is advisory rather than fatal; the plan exposes boundary-policy revision 2, selected strategy, evidence booleans, carry-in fragments, realized lane durations, and whether the non-binding preferred duration was actually realizable. Structural errors such as invalid clocks, duplicate/gapped/empty native shot blocks, out-of-range timestamps, stale audio locks, malformed identity manifests, impossible minimum/maximum limits, or more than four required lanes still fail closed. Missing, stale, weak, or malformed lyric timing also remains non-fatal: the planner records a warning and uses Natural/audio-led articulation. Written lyrics are never treated as a timing schedule. H3-generated audio is intentionally discarded; only the selected, hash-locked soundtrack excerpt is muxed into the assembled video.
The former universal `DiffusionGemmaTargetProfile` remains registered and marked deprecated so existing saved workflows continue to load with their exact widget order. New workflows should use one of the three model-specific targets. Other compatibility and utility classes remain in source where exported nodes reuse their behavior internally. The `ltx_reframe.py` module and LTX reframe workflows are retained as unexported prototypes; the retired `ltx_reframe_layout.js` frontend is not part of this release.
@@ -126,6 +140,39 @@ examples/assets/character_motion_transfer/diffusiongemma_ltx_character_demo.mp4
examples/assets/character_motion_transfer/diffusiongemma_ltx_character_demo.gif
```
## Separate Advertisement Workflow
The governed GlowBloom advertisement path is a separate MiniMax-H3 Ref2VA system rather than an edit to the music-video V6 graph. Its user-editable ComfyUI workflow is:
```text
examples/16_minimax_h3_ref2va_advertisement_music3_v1.json
```
The corresponding API-format production fixture is a deliberately Music3-only validation graph:
```text
examples/api/17_minimax_h3_ref2va_advertisement_music3_glowbloom_v1_api.json
```
The workflow has a fresh revision-1 graph identity and shares no mutable subgraph UUID with V6. It uses MiniMax Music 3 as its saved default, with Upload song and Legacy ACE-Step available through the lazy source router; the smaller API fixture contains only the default MiniMax Music 3 source and does not claim those UI-only fallbacks. It expects three user assets in the Picture 1 performer / Picture 2 same-performer sheet / Picture 3 product sheet order; Picture 4 is supplied only by the retained-tail relay between H3 lanes. Reference Asset Prep derives all three SHA-256 values from the selected images and keeps the product budget independent. `ADVERTISEMENT CONTROLS` is the one shared 30-second duration, native-shot, performance, and 30/15/6 delivery authority; duplicated consumer widgets are linked rather than independently editable. The saved Grounding Guard is explicitly labeled as an **OFF baseline**; the separate reference contract, governed packet repair, JSON splitter, and branch gate remain active and fail closed.
The Director loader uses the portable path `models/LLM/diffusiongemma-26B-A4B-it-NVFP4`, relative to the ComfyUI application directory. If ComfyUI is launched from a different working directory or the checkpoint is stored elsewhere, edit that one **Load DiffusionGemma Director** field to the full local repo-folder path before queueing.
For the 30-second preset, Music 3 generates a 35-second candidate and the selector locks one exact 30-second excerpt. H3 receives the music-only motion guide. The reusable mixer supports separate final-mix VO, but this v1 workflow keeps VO disabled until it has an actual input path. The finishing nodes render a deterministic three-second end card containing normalized, case-preserved governed copy and produce real tail-selected 15-second and 6-second cutdowns from the finished master.
The exact release graph is runtime-proven end to end on an isolated ComfyUI `0.33.1` installation with the pruned INT8/ConvRot Music 3 text encoder and the listed H3 assets. The 99-node API graph completed both 15-second H3 lanes, persistent Picture 4 relay, deterministic finishing, every diagnostic root, and real 30-second/15-second/6-second review drafts. The master was exactly 720 frames at 480x864 and 24 fps with a 30.000-second stereo soundtrack; the cutdowns were exactly 360 and 144 frames with matching 15.000-second and 6.000-second audio. A sampled visual contact-sheet review found stable performer/wardrobe and recognizable product continuity plus a legible deterministic end card. These facts establish technical execution, not delivery approval: the saved QA status still blocks delivery until evidence-backed performer, product, copy, and audio-sync review is recorded.
Known limitations:
- MiniMax-H3 generation remains stochastic. Multiple references and retained-tail continuity reduce drift but cannot guarantee performer likeness, product geometry, packaging text, or demographic presentation.
- Exact brand, CTA, and legal copy is guaranteed only in the deterministic end card. Do not treat generated in-scene labels as reliable typography or a substantiated claim.
- The 15-second and 6-second outputs are real temporal cutdowns. Requested 1:1 and 16:9 adaptations are status-only until a semantic reframe renderer is connected.
- Visual identity, package consistency, copy legibility, and audio sync require measured or human evidence. The QA gate intentionally reports `not_measured` and keeps delivery unapproved when that evidence is absent.
- MiniMax Music 3 tempo remains generative. The runtime-proven candidate passed technical QC but the signal analyzer estimated `87.890625` BPM against the requested `122`, and recorded that mismatch as an advisory. H3 follows the selected waveform itself; use a locked upload or stricter candidate policy when an exact tempo is mandatory.
- Automatic saves are review drafts, not QA-gated delivery exports. A pass requires evidence for every required check; a bare pass is downgraded to `not_measured`.
- The combined editable workflow includes a real Legacy ACE fallback. ComfyUI may validate linked lazy loader/model choices before branch selection, so `acestep_v1.5_turbo.safetensors`, `qwen_0.6b_ace15.safetensors`, `qwen_4b_ace15.safetensors`, and `ace_1.5_vae.safetensors` must remain visible even when MiniMax Music 3 is selected. The Music3-only API fixture does not require that fallback branch.
- The fictional GlowBloom references, soundtrack, and rendered campaign media are not bundled as distributable production assets. Replace the fixture's local input selections and review every claim before commercial use.
## Recommended Wiring
For custom workflows, the core DiffusionGemma path is:
@@ -322,7 +369,9 @@ Official references:
## Compatibility
The verified baseline is ComfyUI `0.30.1` with Python 3.11. Registry metadata declares `requires-comfyui = ">=0.30.1"`: this is the Registry's minimum-compatible-version constraint, while `0.30.1` remains the version exercised by the compatibility and release checks. Raise that floor when an exported node begins to require a newer ComfyUI API.
The established Director surface retains its ComfyUI `0.30.1` / Python 3.11 compatibility baseline. Registry metadata declares `requires-comfyui = ">=0.30.1"`: this remains the package-wide minimum-compatible-version constraint unless an exported node makes a newer core mandatory.
The separate Advertisement + native MiniMax Music 3 workflow requires ComfyUI `>=0.33.1`. Its complete two-lane H3 campaign path was runtime-proven on `0.33.1`, including the pruned INT8/ConvRot Music 3 text encoder, portable Director checkpoint path, governed two-call Director refinement, persistent relay, and exact 30/15/6 finishing. This narrower Advertisement requirement does not change the preserved V6 workflow, and technical completion does not override the Advertisement QA gate's evidence requirements.
## Runtime Dependencies
@@ -332,6 +381,8 @@ This pack assumes a working ComfyUI install with CUDA-capable PyTorch. The Pytho
- `accelerate>=1.14.0`
- `comfy-kitchen>=0.2.10`
- `jsonschema>=4.26.0`
- `numpy>=1.26.0`
- `Pillow>=10.0.0`
- `safetensors>=0.7.0`
- `tqdm>=4.67.0`
@@ -351,6 +402,30 @@ Place the whole repo folder at:
ComfyUI/models/LLM/diffusiongemma-26B-A4B-it-NVFP4
```
MiniMax Music 3 models used by the separate Advertisement fixture:
```text
ComfyUI/models/text_encoders/minimax_music3_text_encoder_pruned_int8_convrot.safetensors
ComfyUI/models/diffusion_models/minimax_music3_dit_fp16.safetensors
ComfyUI/models/vae/minimax_music3_dav.safetensors
```
The INT8/ConvRot text-encoder selection is the locally runtime-proven ComfyUI `0.33.1` path. Model files are not bundled with this repository.
MiniMax H3 models used by both Advertisement artifacts:
```text
ComfyUI/models/diffusion_models/minimax_h3_ref2va_pruned_int8_convrot.safetensors
ComfyUI/models/text_encoders/qwen3vl_32b_minimax_h3_nvfp4_awq.safetensors
ComfyUI/models/vae/minimax_h3_video_vae_fp16.safetensors
ComfyUI/models/vae/minimax_h3_audio_vae_fp32.safetensors
ComfyUI/models/loras/minimax_h3_ref2v_turbo_4step_v0.1_comfyui_bf16.safetensors
```
The saved baseline uses seven scheduler steps with the Turbo LoRA at strength `0.75`, video sigma shift `12`, and audio sigma shift `3`. These H3 files are also local dependencies and are not bundled.
The combined user-editable workflow also retains the Legacy ACE-Step fallback. Because ComfyUI can validate linked lazy model selections before executing the chosen branch, `acestep_v1.5_turbo.safetensors`, `qwen_0.6b_ace15.safetensors`, `qwen_4b_ace15.safetensors`, and `ace_1.5_vae.safetensors` must be discoverable even when the saved source mode remains MiniMax Music 3. The ACE fallback has no LoRA dependency. Use the Music3-only API fixture when that fallback is intentionally unavailable.
LTX 2.3 diffusion model:
- [Kijai/LTX2.3_comfy - ltx-2.3-22b-distilled_transformer_only_fp8_input_scaled_v3.safetensors](https://huggingface.co/Kijai/LTX2.3_comfy/blob/main/diffusion_models/ltx-2.3-22b-distilled_transformer_only_fp8_input_scaled_v3.safetensors)
+30
View File
@@ -17,18 +17,48 @@ from .timed_lyrics_nodes import (
NODE_CLASS_MAPPINGS as _TIMED_LYRICS_NODE_CLASS_MAPPINGS,
NODE_DISPLAY_NAME_MAPPINGS as _TIMED_LYRICS_NODE_DISPLAY_NAME_MAPPINGS,
)
from .advertising_contract_nodes import (
NODE_CLASS_MAPPINGS as _ADVERTISING_CONTRACT_NODE_CLASS_MAPPINGS,
NODE_DISPLAY_NAME_MAPPINGS as _ADVERTISING_CONTRACT_NODE_DISPLAY_NAME_MAPPINGS,
)
from .advertising_audio_nodes import (
NODE_CLASS_MAPPINGS as _ADVERTISING_AUDIO_NODE_CLASS_MAPPINGS,
NODE_DISPLAY_NAME_MAPPINGS as _ADVERTISING_AUDIO_NODE_DISPLAY_NAME_MAPPINGS,
)
from .advertising_planning_nodes import (
NODE_CLASS_MAPPINGS as _ADVERTISING_PLANNING_NODE_CLASS_MAPPINGS,
NODE_DISPLAY_NAME_MAPPINGS as _ADVERTISING_PLANNING_NODE_DISPLAY_NAME_MAPPINGS,
)
from .advertising_finishing_nodes import (
NODE_CLASS_MAPPINGS as _ADVERTISING_FINISHING_NODE_CLASS_MAPPINGS,
NODE_DISPLAY_NAME_MAPPINGS as _ADVERTISING_FINISHING_NODE_DISPLAY_NAME_MAPPINGS,
)
from .advertising_runtime_nodes import (
NODE_CLASS_MAPPINGS as _ADVERTISING_RUNTIME_NODE_CLASS_MAPPINGS,
NODE_DISPLAY_NAME_MAPPINGS as _ADVERTISING_RUNTIME_NODE_DISPLAY_NAME_MAPPINGS,
)
NODE_CLASS_MAPPINGS = {
**_DIRECTOR_NODE_CLASS_MAPPINGS,
**_AUDIO_NODE_CLASS_MAPPINGS,
**_PRODUCTION_PLANNING_NODE_CLASS_MAPPINGS,
**_TIMED_LYRICS_NODE_CLASS_MAPPINGS,
**_ADVERTISING_CONTRACT_NODE_CLASS_MAPPINGS,
**_ADVERTISING_AUDIO_NODE_CLASS_MAPPINGS,
**_ADVERTISING_PLANNING_NODE_CLASS_MAPPINGS,
**_ADVERTISING_FINISHING_NODE_CLASS_MAPPINGS,
**_ADVERTISING_RUNTIME_NODE_CLASS_MAPPINGS,
}
NODE_DISPLAY_NAME_MAPPINGS = {
**_DIRECTOR_NODE_DISPLAY_NAME_MAPPINGS,
**_AUDIO_NODE_DISPLAY_NAME_MAPPINGS,
**_PRODUCTION_PLANNING_NODE_DISPLAY_NAME_MAPPINGS,
**_TIMED_LYRICS_NODE_DISPLAY_NAME_MAPPINGS,
**_ADVERTISING_CONTRACT_NODE_DISPLAY_NAME_MAPPINGS,
**_ADVERTISING_AUDIO_NODE_DISPLAY_NAME_MAPPINGS,
**_ADVERTISING_PLANNING_NODE_DISPLAY_NAME_MAPPINGS,
**_ADVERTISING_FINISHING_NODE_DISPLAY_NAME_MAPPINGS,
**_ADVERTISING_RUNTIME_NODE_DISPLAY_NAME_MAPPINGS,
}
WEB_DIRECTORY = "./web"
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
+103
View File
@@ -0,0 +1,103 @@
# Copyright (c) 2026 exportAnything. All rights reserved.
# SPDX-License-Identifier: MIT
"""Self-contained runtime memory barriers for Advertisement workflows.
The established music-video workflows use convenience nodes from several
third-party packs to release text encoders and VAEs between expensive stages.
Advertisement workflows keep the same useful scheduling boundary without
depending on those unrelated packs.
"""
from __future__ import annotations
import gc
from typing import Any
CATEGORY = "prompt/diffusiongemma/advertising"
POLICIES = (
"Unload models + clear CUDA cache",
"Unload models + soft cache",
"Passthrough (no release)",
)
class _AnyType(str):
"""ComfyUI wildcard socket compatible with arbitrary pass-through values."""
def __ne__(self, value: object) -> bool:
return False
ANY_TYPE = _AnyType("*")
class AdvertisementMemoryBarrier:
"""Pass a value through after an explicit, observable model-memory release."""
@classmethod
def INPUT_TYPES(cls):
return {
"required": {
"value": (ANY_TYPE,),
"policy": (list(POLICIES), {"default": POLICIES[0]}),
}
}
RETURN_TYPES = (ANY_TYPE, "STRING")
RETURN_NAMES = ("value", "memory_status")
FUNCTION = "release"
CATEGORY = CATEGORY
DESCRIPTION = (
"Advertisement-owned scheduling barrier. It preserves its input exactly while releasing loaded "
"ComfyUI models before the next high-memory stage; no third-party cleanup pack is required."
)
def release(self, value: Any, policy: str):
selected = str(policy or "").strip()
if selected not in POLICIES:
raise ValueError("Advertisement memory policy is unsupported.")
if selected == POLICIES[2]:
return value, "Advertisement memory barrier passed through without releasing models."
try:
import comfy.model_management as model_management
except ImportError as exc: # pragma: no cover - exercised only outside ComfyUI.
raise RuntimeError(
"Advertisement memory release requires the ComfyUI runtime."
) from exc
model_management.unload_all_models()
model_management.soft_empty_cache(True)
gc.collect()
cuda_cleared = False
if selected == POLICIES[0]:
try:
import torch
if torch.cuda.is_available():
torch.cuda.empty_cache()
cuda_cleared = True
except (ImportError, RuntimeError):
cuda_cleared = False
suffix = " CUDA cache cleared." if cuda_cleared else " Soft cache cleared."
return value, "Advertisement memory barrier released all loaded models." + suffix
NODE_CLASS_MAPPINGS = {
"DiffusionGemmaAdvertisementMemoryBarrier": AdvertisementMemoryBarrier,
}
NODE_DISPLAY_NAME_MAPPINGS = {
"DiffusionGemmaAdvertisementMemoryBarrier": "DiffusionGemma Advertisement Memory Barrier",
}
__all__ = [
"AdvertisementMemoryBarrier",
"NODE_CLASS_MAPPINGS",
"NODE_DISPLAY_NAME_MAPPINGS",
]
+18 -3
View File
@@ -1,21 +1,22 @@
# Deferred DiffusionGemma Agent Skill Brief
Status: **Pinned for post-hardening implementation.** This document is intentionally outside `.agents/skills`, so Codex and other compatible agents do not discover or invoke an operating contract that is not finished yet.
Status: **Pinned for post-hardening implementation; inactive.** This document is intentionally outside `.agents/skills`, so Codex and other compatible agents do not discover or invoke an operating contract that is not finished yet. The separate Advertisement workflow expands the future scope but does not activate or install this Skill.
Pinned: 2026-08-23
Last reviewed: 2026-08-24
## Canonical skill identity
```yaml
name: diffusiongemma-director
description: Configure, run, validate, and troubleshoot DiffusionGemma Director LTX and MiniMax-H3 workflows in a user's local ComfyUI through an available local operations interface. Use for this package's target profiles, reference preparation, soundtrack and performance routing, native-shot and generation-lane planning, identity relay, contract failures, queueing, or output verification; do not use for unrelated ComfyUI graphs or generic prompt writing.
description: Configure, run, validate, and troubleshoot DiffusionGemma Director LTX, MiniMax-H3 music-video, and separate Advertisement workflows in a user's local ComfyUI through an available local operations interface. Use for this package's target profiles, typed campaign and reference contracts, soundtrack source and performance routing, native-shot and generation-lane planning, identity/product continuity, deterministic finishing, media QA, contract failures, queueing, or output verification; do not use for unrelated ComfyUI graphs, generic prompt writing, or ungoverned advertising claims.
```
Treat that `name` and `description` as the canonical starting point when the active skill is created. Revise them only if the hardened public surface materially changes the skill's scope or trigger boundary.
## Intended outcome
The eventual skill should translate an ordinary-language production request plus user-supplied images, video, or audio into a correctly configured DiffusionGemma workflow; operate it through an available local ComfyUI interface; diagnose contract or runtime failures; and verify actual branch media output rather than treating a completed queue as proof of generation.
The eventual skill should translate an ordinary-language production request plus user-supplied images, video, or audio into a correctly configured DiffusionGemma workflow; operate it through an available local ComfyUI interface; diagnose contract or runtime failures; and verify actual branch media output rather than treating a completed queue as proof of generation. For Advertisement requests it must also preserve exact approved copy and claims, distinguish performer and product authority, separate the H3 motion guide from final voice-over, and report delivery evidence honestly.
The skill supplies operating knowledge. It does not itself grant filesystem, browser, model-download, or ComfyUI execution access. Those capabilities must come from explicitly available tools, ideally a local ComfyUI MCP server declared as a plugin dependency.
@@ -27,6 +28,13 @@ The skill supplies operating knowledge. It does not itself grant filesystem, bro
- Treat Natural, Dance, and Lyrics as mutually exclusive performance authorities. Written lyrics are not a timing schedule without verified timed-lyrics evidence.
- Keep clean identity references authoritative. Previous-lane relay is optional continuity evidence, not a replacement identity source.
- Preserve the selected soundtrack source, pristine final audio, exact retained-frame clock, and user-selected camera intent.
- Keep the known-good V6 workflow and generic contracts separate from Advertisement contracts. Never migrate or overwrite V6 to satisfy an ad request.
- For Advertisement references, preserve Picture 1 as performer hero, Picture 2 as same-performer sheet, Picture 3 as independent product/package sheet, and Picture 4 as retained-tail continuity only. Never make Picture 4 a new identity or product authority, and never combine performer and product sheets to save a reference slot.
- Distinguish the three Advertisement soundtrack origins—MiniMax Music 3, Upload song, and Legacy ACE-Step—from the `Instrumental` / `Vocal` / `Auto` content policy. Do not require vocal evidence for a governed instrumental.
- Send only the selected soundtrack to H3 as the motion guide. Optional non-diegetic VO belongs only in the final delivery mix and must not become dialogue or lip-sync conditioning.
- Keep exact brand, CTA, and legal copy in the deterministic end card. Never invent or strengthen a product claim, and never represent generated in-scene typography as exact.
- Treat the 30-second master plus rendered 15-second and 6-second temporal cutdowns as media outputs. Treat 1:1 or 16:9 adaptations as unrendered until an actual semantic reframe renderer produces them.
- Preserve `pass`, `fail`, and `not_measured` as distinct QA states. A completed queue, valid graph, or successful Music 3 canary is not evidence that performer identity, product identity, copy legibility, sync, or the complete H3 campaign passed.
- Require fresh authorization before downloads, installs, external publication, destructive changes, or other actions not already placed in scope.
## Hardening exit criteria
@@ -39,6 +47,12 @@ Promote this brief to `.agents/skills/diffusiongemma-director/SKILL.md`, or pack
- validation errors, cache refresh/reuse behavior, lane assembly, and media-output checks have repeatable tests;
- required versus optional models and custom-node packs are expressed without machine-specific paths;
- representative Natural, Dance, Lyrics, upload-song, identity-relay, long-duration, and failure-recovery requests have been forward-tested.
- the Advertisement workflow has clean-install coverage on ComfyUI `>=0.33.1`, including the locally verified MiniMax Music 3 INT8/ConvRot text-encoder path and all three soundtrack origins;
- Picture 1/2 performer authority, Picture 3 product authority, Picture 4 retained-tail relay, soundtrack-only H3 guidance, optional final-mix VO, deterministic end cards, actual 15-second/6-second cutdowns, and honest QA states have repeatable integration tests;
- at least one complete Advertisement H3 render—not only an audio canary or serialized fixture—has been reviewed for performer/product retention, copy legibility, audio sync, and exact delivery clocks;
- model-generated package text, stochastic reference drift, missing semantic reframing, and unmeasured QA evidence remain documented as limitations rather than silently repaired or marked passed.
Current hardening evidence: the exact release API graph has completed a full two-lane 30-second Advertisement render with retained-tail relay, deterministic end card, exact 30/15/6 media clocks, and all diagnostic roots. A sampled visual contact-sheet review found stable performer/wardrobe and recognizable product continuity. The active Skill is still deferred because evidence-backed listening/audio-sync approval, representative mode/source coverage, and the remaining clean-install matrix above are not complete; the workflow QA result remains `not_measured`, not promoted to `pass`.
## Expected active-skill structure
@@ -48,6 +62,7 @@ diffusiongemma-director/
|-- agents/
| `-- openai.yaml
`-- references/
|-- advertisement-workflow.md
|-- workflow-modes.md
|-- node-contracts.md
`-- troubleshooting.md
File diff suppressed because it is too large Load Diff
+65
View File
@@ -152,6 +152,71 @@ Replace both saved `LoadImage` choices with your own primary identity image and
The repository copy is the validated V6 graph used by the Desktop and ComfyUI deployments at the time of this changelog. Its migration marker and canonical JSON SHA-256 are regression-tested without depending on those mutable external copies or platform-specific line endings.
## Separate MiniMax-H3 Advertisement production path
Advertisement production is additive and does not replace or migrate V6. The user-editable workflow has a fresh revision-1 graph identity and no shared mutable subgraph UUID with V6:
```text
16_minimax_h3_ref2va_advertisement_music3_v1.json
```
Its governed API-format fixture is:
```text
api/17_minimax_h3_ref2va_advertisement_music3_glowbloom_v1_api.json
```
Its companion manifest records graph hashes, model selections, reference roles, deliverables, adaptation status, and the default honest-QA posture. The earlier `api/16_minimax_h3_ref2va_advertisement_glowbloom_v1_api.json` remains as field-test lineage; new Advertisement work should use the Music 3 fixture rather than treating the earlier graph as the hardened contract.
The saved reference contract is:
```text
<Picture 1> performer hero identity
<Picture 2> same-performer contact sheet
<Picture 3> independent product/package contact sheet
<Picture 4> hash-verified, downscaled copy of the exact retained prior-lane final frame; continuity only
```
Select three images in that order. `DiffusionGemmaAdvertisementReferenceAssetPrep` normalizes them and computes the three SHA-256 values automatically. Picture 3 retains a separate product budget and must not be merged with either performer reference. Picture 4 is reserved by the contract and is populated only by a hash-verified, downscaled copy of the previous H3 lane's exact retained final frame; it is never a source identity.
`ADVERTISEMENT CONTROLS` is the only shared duration, native-shot, performance, aspect, Ref2VA mode, H3 audio/dialogue policy, excerpt-start, generation-model, lane-ceiling, and deliverable authority in Advertisement v1. The release intentionally locks a 30-second 9:16 master with derived tail-selected 15-second and 6-second cutdowns, keeps one shared shot count, drives the splitter aspect, and exposes Natural or Dance performance. Alternate reference layouts, non-relay seam modes, Lyrics without timed evidence, and VO without an input path are hidden instead of offered as broken combinations.
`SOUNDTRACK SOURCE` has three lazy origins:
- **MiniMax Music 3** is the default generated path.
- **Upload song** accepts a user-selected soundtrack without evaluating either generator.
- **Legacy ACE-Step** preserves the earlier generated-music route as a fallback.
The separate soundtrack content policy is `Instrumental`, `Vocal`, or `Auto`. Instrumental Music 3 output is not rejected for lacking a vocal proxy; Vocal mode requires vocal evidence and governed tagged lyrics. The 30-second preset requests 35 seconds from Music 3 so the selector can lock an exact 30-second candidate window. Its verified local model selections are:
```text
models/text_encoders/minimax_music3_text_encoder_pruned_int8_convrot.safetensors
models/diffusion_models/minimax_music3_dit_fp16.safetensors
models/vae/minimax_music3_dav.safetensors
```
The combined editable graph retains the Legacy ACE-Step fallback. ComfyUI may validate linked lazy loader/model choices before executing the selected source, so the saved ACE assets and `qwen_4b_ace15.safetensors` text encoder must still be discoverable even when MiniMax Music 3 is selected. The Music3-only API fixture avoids this fallback dependency.
The exact 99-node API fixture is runtime-proven end to end on an isolated ComfyUI `0.33.1` environment: both 15-second H3 lanes, retained-tail Picture 4 relay, deterministic end card, all diagnostic roots, and exact 30-second/15-second/6-second review media completed without node errors. The master was 720 frames at 480x864/24 fps with exactly 30.000 seconds of stereo audio; the cutdowns were 360 and 144 frames with exact matching audio clocks. A sampled visual contact-sheet review found stable performer/wardrobe and recognizable package continuity. The formal QA status correctly remains blocked because identity, package, copy, and audio sync have not all received evidence-backed human or measured approval.
The API fixture is intentionally MiniMax Music 3-only; Upload song and Legacy ACE-Step exist only in the combined editable UI workflow. Both artifacts also require the native H3 files below:
```text
models/diffusion_models/minimax_h3_ref2va_pruned_int8_convrot.safetensors
models/text_encoders/qwen3vl_32b_minimax_h3_nvfp4_awq.safetensors
models/vae/minimax_h3_video_vae_fp16.safetensors
models/vae/minimax_h3_audio_vae_fp32.safetensors
models/loras/minimax_h3_ref2v_turbo_4step_v0.1_comfyui_bf16.safetensors
```
The Director loader is saved as `models/LLM/diffusiongemma-26B-A4B-it-NVFP4`, relative to the ComfyUI application directory. Edit that field to a full local repo-folder path if the process uses a different working directory or model layout.
H3 receives only the selected soundtrack as its motion guide. The reusable mixer supports separate non-diegetic voice-over only in the final delivery mix and never in H3 conditioning, but Advertisement v1 locks VO to `None` until an actual upload/loader path is connected. Finishing replaces the governed master tail with a deterministic exact-copy end card, then emits actual 15-second and 6-second frame/audio cutdowns alongside the 30-second 9:16 master. Requested 1:1 and 16:9 adaptations remain explicitly unrendered until a semantic reframe renderer exists.
The runtime-proven MiniMax Music 3 candidate passed technical QC but produced a signal-estimated `87.890625` BPM instead of the requested `122`; the selector records this as an advisory rather than pretending the generative tempo is exact. H3 still follows the selected waveform itself. Use Upload song or a stricter candidate lock when the numerical BPM is a hard campaign requirement.
The QA node distinguishes `pass`, `fail`, and `not_measured`; a pass without evidence is downgraded to `not_measured`. Automatic MP4 and soundtrack saves are labeled review drafts rather than delivery approvals. Technical assembly can pass while performer identity, product identity, copy legibility, or audio sync remains unmeasured. Generated in-scene packaging text is not guaranteed, exact copy is governed only on the deterministic end card, and stochastic performer/product drift still requires human or measured review. The fictional GlowBloom inputs and rendered campaign media are not bundled.
## LTX 2.3 Reframe with selectable audio
The provisional, unexported repository prototype is:
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,80 @@
{
"schema": "diffusiongemma.advertisement_runtime_fixture",
"version": 1,
"prompt_id": "d9fe3260-c297-4004-ab6b-bfd85b5a2651",
"completed": true,
"api_graph_sha256": "6296d86a7640ce1babe307f68c3ddab845341e5e1185cb2216924097e9b4f743",
"api_node_count": 137,
"class_counts": {
"BasicGuider": 4,
"BasicScheduler": 1,
"BatchImagesNode": 1,
"CLIPLoader": 1,
"CM_IntToFloat": 1,
"ComfyMathExpression": 3,
"ConditioningZeroOut": 4,
"CreateVideo": 1,
"DiffusionGemmaACECoverConditioning": 4,
"DiffusionGemmaACEReferenceMode": 1,
"DiffusionGemmaAudioAwareMultiShotPlanner": 1,
"DiffusionGemmaAudioCandidateSelector": 1,
"DiffusionGemmaBranchGenerationGate": 1,
"DiffusionGemmaCoTGenerator": 1,
"DiffusionGemmaContextHub": 1,
"DiffusionGemmaGroundingGuardSettings": 1,
"DiffusionGemmaH3ReferenceContext": 1,
"DiffusionGemmaH3ReferencePairPrep": 1,
"DiffusionGemmaH3RelayReferenceGate": 3,
"DiffusionGemmaH3ShotAssembler": 1,
"DiffusionGemmaH3ShotSeedFanout": 1,
"DiffusionGemmaJSONSplitter": 1,
"DiffusionGemmaLTXAudioGuide": 1,
"DiffusionGemmaLTXPerformancePrompt": 1,
"DiffusionGemmaMiniMaxH3TargetProfile": 1,
"DiffusionGemmaModelLoader": 1,
"DiffusionGemmaMultiFormatDeliveryPlanner": 1,
"DiffusionGemmaMusicProductionConcept": 1,
"DiffusionGemmaMusicVideoPerformanceMode": 1,
"DiffusionGemmaProjectMasterContract": 1,
"DiffusionGemmaSongSeedFanout": 1,
"DiffusionGemmaSongSourceRouter": 1,
"DiffusionGemmaSplatStagePlanner": 1,
"DiffusionGemmaTimedLyricsAnalyzer": 1,
"DiffusionGemmaUploadSong": 1,
"DualCLIPLoader": 1,
"EasyCache": 1,
"EmptyAceStep1.5LatentAudio": 1,
"FL_Audio_Separation": 1,
"FL_UnloadAllModels": 4,
"ImageFromBatch": 3,
"ImageScaleToTotalPixels": 3,
"KSampler": 4,
"KSamplerSelect": 1,
"Load Whisper (mtb)": 1,
"LoadImage": 2,
"LoraLoaderModelOnly": 1,
"MiniMaxH3ReferenceToVideo": 4,
"MiniMaxH3SigmaShift": 1,
"ModelSamplingAuraFlow": 1,
"PathchSageAttentionKJ": 1,
"PreviewAny": 16,
"PreviewAudio": 2,
"PrimitiveFloat": 2,
"PrimitiveStringMultiline": 3,
"RandomNoise": 4,
"SamplerCustomAdvanced": 4,
"SaveVideo": 1,
"SeedNode": 2,
"SplatStageBlueprintRouter": 1,
"TextEncodeAceStepAudio1.5": 4,
"TrimAudioDuration": 5,
"UNETLoader": 2,
"VAEDecode": 4,
"VAEDecodeAudio": 4,
"VAELoader": 3,
"easy cleanGpuUsed": 2
},
"known_good_ui_path": "C:/ComfyUI/app/custom_nodes/ComfyUI-DiffusionGemmaPromptBuilder/examples/15_minimax_h3_ref2va_music_video_v6.json",
"known_good_ui_sha256": "594f6aef94531f87b74659e1e8004cccdff00716d0cfe263cee6bf2149545c83",
"source": "local_comfyui_history"
}
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,66 @@
{
"schema": "diffusiongemma.advertisement_music3_runtime_fixture",
"version": 1,
"source_api": "examples/api/16_minimax_h3_ref2va_advertisement_glowbloom_v1_api.json",
"source_api_canonical_sha256": "6296d86a7640ce1babe307f68c3ddab845341e5e1185cb2216924097e9b4f743",
"known_good_v6": "examples/15_minimax_h3_ref2va_music_video_v6.json",
"known_good_v6_sha256": "594f6aef94531f87b74659e1e8004cccdff00716d0cfe263cee6bf2149545c83",
"known_good_v6_is_never_overwritten": true,
"api_graph_canonical_sha256": "d6f8d8de431f233c00a1172afca39815b8bd8bd619b633e8e5162db310fffc82",
"api_graph_file_sha256": "d491c2c897a66dce1eace9487445003c224a3492dc60d2c0868fe8787567a9be",
"node_count": 99,
"models": {
"director": {
"checkpoint_directory": "models/LLM/diffusiongemma-26B-A4B-it-NVFP4"
},
"minimax_h3": {
"diffusion_model": "models/diffusion_models/minimax_h3_ref2va_pruned_int8_convrot.safetensors",
"text_encoder": "models/text_encoders/qwen3vl_32b_minimax_h3_nvfp4_awq.safetensors",
"video_vae": "models/vae/minimax_h3_video_vae_fp16.safetensors",
"audio_vae": "models/vae/minimax_h3_audio_vae_fp32.safetensors",
"turbo_lora": "models/loras/minimax_h3_ref2v_turbo_4step_v0.1_comfyui_bf16.safetensors"
},
"minimax_music3": {
"diffusion_model": "models/diffusion_models/minimax_music3_dit_fp16.safetensors",
"text_encoder": "models/text_encoders/minimax_music3_text_encoder_pruned_int8_convrot.safetensors",
"vae": "models/vae/minimax_music3_dav.safetensors"
}
},
"assets": {
"performer_hero": {
"relative_input_path": "diffusiongemma_campaigns/glowbloom_20260823/glowbloom_hero_identity.png",
"sha256": "98ced6b029fe36888b4ada491f5cbd7cb9efd4724001da064f3713b3da4798e4"
},
"performer_sheet": {
"relative_input_path": "diffusiongemma_campaigns/glowbloom_20260823/glowbloom_performer_contact_sheet_v1.png",
"sha256": "cf13e146624290c1932727fb1a54e38bbe275a866b60ed649d1ca0ee4fd1939b"
},
"product_sheet": {
"relative_input_path": "diffusiongemma_campaigns/glowbloom_20260823/glowbloom_product_contact_sheet_v1.png",
"sha256": "ed534b55bc417c1610051f7d91fa807360bf67757e7aa41a96bcc655d4e5db8e"
}
},
"audio_source_default": "MiniMax Music 3",
"api_audio_sources": [
"MiniMax Music 3"
],
"companion_ui_only_audio_sources": [
"Upload song",
"Legacy ACE-Step"
],
"h3_reference_policy": {
"picture_1": "performer hero",
"picture_2": "same-performer contact sheet",
"picture_3": "independent product/package contact sheet",
"picture_4": "hash-verified, downscaled copy of the exact retained prior-lane final frame; continuity only"
},
"deliverables": [
"30s 9:16 review draft",
"15s 9:16 review draft",
"6s 9:16 review draft"
],
"adaptations": "status-only until a semantic reframe renderer is connected; never falsely marked rendered",
"qa_default": "not_measured for visual identity, copy legibility, and audio sync until a human or measured checker supplies evidence",
"save_policy": "Saved media are review drafts; QA pass with evidence is required before delivery approval.",
"manifest_sha256": "0c1b6f9d994f6eabf24f31bf35fd151267140df7ea136505ab8ca06916efaca4"
}
+3 -1
View File
@@ -1,13 +1,15 @@
[project]
name = "diffusiongemma-prompt-builder"
version = "0.2.0"
description = "DiffusionGemma Director prompt authoring, measured-audio controls, and synchronized LTX/MiniMax-H3 production planning for ComfyUI."
description = "DiffusionGemma Director prompt authoring, measured-audio controls, synchronized video planning, and governed MiniMax-H3 advertisement production for ComfyUI."
license = { file = "LICENSE" }
requires-python = ">=3.11"
dependencies = [
"accelerate>=1.14.0",
"comfy-kitchen>=0.2.10",
"jsonschema>=4.26.0",
"numpy>=1.26.0",
"Pillow>=10.0.0",
"safetensors>=0.7.0",
"tqdm>=4.67.0",
"transformers>=5.12.1",
+2
View File
@@ -1,6 +1,8 @@
accelerate>=1.14.0
comfy-kitchen>=0.2.10
jsonschema>=4.26.0
numpy>=1.26.0
Pillow>=10.0.0
safetensors>=0.7.0
tqdm>=4.67.0
transformers>=5.12.1
@@ -0,0 +1,20 @@
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"$id": "https://exportanything.dev/schemas/advertisement_adaptation_status.schema.json",
"title": "DiffusionGemma Advertisement Adaptation Status",
"type": "object",
"required": ["schema", "version", "campaign_id", "advertisement_contract_sha256", "master_aspect_ratio", "adaptations", "all_requested_rendered", "pixel_inputs_received", "ready", "report_sha256"],
"properties": {
"schema": {"const": "diffusiongemma.advertisement_adaptation_status"},
"version": {"const": 1},
"campaign_id": {"$ref": "#/$defs/sha256"},
"advertisement_contract_sha256": {"$ref": "#/$defs/sha256"},
"master_aspect_ratio": {"type": "string"},
"adaptations": {"type": "array", "items": {"type": "object", "required": ["request", "aspect_ratio", "status", "reason"], "properties": {"status": {"enum": ["planned_not_rendered", "not_applicable_same_as_master", "impossible_invalid_aspect"]}}}},
"all_requested_rendered": {"const": false},
"pixel_inputs_received": {"const": false},
"ready": {"const": true},
"report_sha256": {"$ref": "#/$defs/sha256"}
},
"$defs": {"sha256": {"type": "string", "pattern": "^[0-9a-f]{64}$"}}
}
+102
View File
@@ -0,0 +1,102 @@
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"$id": "diffusiongemma.advertisement_audio_mix@1",
"title": "DiffusionGemma Advertisement Audio Mix",
"type": "object",
"additionalProperties": false,
"required": [
"schema",
"version",
"soundtrack_contract_sha256",
"duration_seconds",
"sample_rate",
"sample_count",
"duration_alignment",
"channels",
"batch_count",
"voice_over",
"ducking",
"levels",
"motion_guide_sha256",
"final_mix_sha256",
"motion_guide_policy",
"final_mix_policy",
"ready"
],
"properties": {
"schema": { "const": "diffusiongemma.advertisement_audio_mix" },
"version": { "const": 1 },
"soundtrack_contract_sha256": { "type": "string", "pattern": "^[0-9a-f]{64}$" },
"duration_seconds": { "type": "number", "exclusiveMinimum": 0, "maximum": 60 },
"sample_rate": { "type": "integer", "minimum": 1, "maximum": 384000 },
"sample_count": { "type": "integer", "minimum": 1 },
"duration_alignment": {
"type": "object",
"additionalProperties": false,
"required": ["source_sample_count", "target_sample_count", "right_padding_samples", "rounding_tolerance_samples", "policy"],
"properties": {
"source_sample_count": { "type": "integer", "minimum": 1 },
"target_sample_count": { "type": "integer", "minimum": 1 },
"right_padding_samples": { "type": "integer", "minimum": 0 },
"rounding_tolerance_samples": { "type": "integer", "minimum": 1 },
"policy": { "const": "right_pad_at_most_one_millisecond_then_exact_crop" }
}
},
"channels": { "type": "integer", "minimum": 1, "maximum": 32 },
"batch_count": { "type": "integer", "minimum": 1, "maximum": 64 },
"voice_over": {
"type": "object",
"additionalProperties": false,
"required": ["policy", "connected", "start_seconds", "included_in_motion_guide", "included_in_final_mix", "activity_fraction_signal_proxy"],
"properties": {
"policy": { "enum": ["none", "separate_non_diegetic"] },
"connected": { "type": "boolean" },
"start_seconds": { "type": "number", "minimum": 0, "maximum": 60 },
"included_in_motion_guide": { "const": false },
"included_in_final_mix": { "type": "boolean" },
"activity_fraction_signal_proxy": { "type": "number", "minimum": 0, "maximum": 1 }
}
},
"ducking": {
"type": "object",
"additionalProperties": false,
"required": ["ducking_db", "attack_ms", "release_ms", "minimum_applied_gain", "algorithm"],
"properties": {
"ducking_db": { "type": "number", "minimum": -30, "maximum": 0 },
"attack_ms": { "type": "number", "minimum": 0, "maximum": 1000 },
"release_ms": { "type": "number", "minimum": 0, "maximum": 3000 },
"minimum_applied_gain": { "type": "number", "minimum": 0, "maximum": 1 },
"algorithm": { "const": "20ms_absolute_level_proxy_with_bounded_attack_release_smoothing" }
}
},
"levels": {
"type": "object",
"additionalProperties": false,
"required": [
"motion_guide_sample_peak_dbfs",
"pre_ceiling_final_mix_sample_peak_dbfs",
"final_mix_sample_peak_dbfs",
"peak_ceiling_dbfs",
"global_attenuation_db",
"final_clipped_sample_fraction",
"lufs",
"true_peak"
],
"properties": {
"motion_guide_sample_peak_dbfs": { "type": "number", "minimum": -120, "maximum": 24 },
"pre_ceiling_final_mix_sample_peak_dbfs": { "type": "number", "minimum": -120, "maximum": 24 },
"final_mix_sample_peak_dbfs": { "type": "number", "minimum": -120, "maximum": 0.001 },
"peak_ceiling_dbfs": { "type": "number", "minimum": -12, "maximum": 0 },
"global_attenuation_db": { "type": "number", "minimum": -120, "maximum": 0.001 },
"final_clipped_sample_fraction": { "type": "number", "minimum": 0, "maximum": 1 },
"lufs": { "const": "not_measured" },
"true_peak": { "const": "not_measured" }
}
},
"motion_guide_sha256": { "type": "string", "pattern": "^[0-9a-f]{64}$" },
"final_mix_sha256": { "type": "string", "pattern": "^[0-9a-f]{64}$" },
"motion_guide_policy": { "const": "music_only_never_non_diegetic_voice_over" },
"final_mix_policy": { "const": "music_plus_optional_separate_non_diegetic_voice_over" },
"ready": { "const": true }
}
}
@@ -0,0 +1,83 @@
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"$id": "diffusiongemma.advertisement_audio_selection@1",
"title": "DiffusionGemma Advertisement Audio Selection",
"type": "object",
"additionalProperties": false,
"required": [
"schema",
"version",
"settings",
"lock",
"candidates",
"selected_candidate_index",
"selected_waveform_sha256",
"selection_lock_token",
"status",
"ready"
],
"properties": {
"schema": { "const": "diffusiongemma.advertisement_audio_selection" },
"version": { "const": 1 },
"settings": {
"type": "object",
"additionalProperties": false,
"required": [
"candidate_count",
"selection_mode",
"expected_bpm",
"excerpt_duration_seconds",
"minimum_score",
"source_policy",
"soundtrack_content_mode",
"content_evidence_policy"
],
"properties": {
"candidate_count": { "type": "integer", "minimum": 1, "maximum": 4 },
"selection_mode": { "enum": ["auto_select", "lock_candidate_1", "lock_candidate_2", "lock_candidate_3", "lock_candidate_4", "lock_by_hash"] },
"expected_bpm": { "type": "number", "minimum": 0, "maximum": 300 },
"excerpt_duration_seconds": { "type": "number", "exclusiveMinimum": 0, "maximum": 600 },
"minimum_score": { "type": "number", "minimum": 0, "maximum": 1 },
"source_policy": { "enum": ["minimax_music3", "ace_step", "uploaded_song"] },
"soundtrack_content_mode": { "enum": ["instrumental", "vocal"] },
"content_evidence_policy": { "enum": ["vocal_proxy_required", "vocal_proxy_not_required"] }
}
},
"lock": {
"type": "object",
"additionalProperties": true,
"required": ["active", "requested_waveform_sha256", "requested_start_seconds", "satisfied", "failure_reason"],
"properties": {
"active": { "type": "boolean" },
"requested_waveform_sha256": { "type": "string", "maxLength": 64 },
"requested_start_seconds": { "type": "number", "minimum": -1 },
"satisfied": { "type": "boolean" },
"failure_reason": { "type": "string" }
}
},
"candidates": {
"type": "array",
"minItems": 1,
"maxItems": 4,
"items": {
"type": "object",
"additionalProperties": true,
"required": ["index", "present", "passed", "failures", "advisories"],
"properties": {
"index": { "type": "integer", "minimum": 1, "maximum": 4 },
"present": { "type": "boolean" },
"passed": { "type": "boolean" },
"failures": { "type": "array", "items": { "type": "string" } },
"advisories": { "type": "array", "items": { "type": "string" } },
"analysis": { "type": "object" },
"error": { "type": "string" }
}
}
},
"selected_candidate_index": { "type": "integer", "minimum": 1, "maximum": 4 },
"selected_waveform_sha256": { "type": "string", "pattern": "^[0-9a-f]{64}$" },
"selection_lock_token": { "type": "string", "pattern": "^[0-9a-f]{64}$" },
"status": { "type": "string", "minLength": 1 },
"ready": { "const": true }
}
}
+132
View File
@@ -0,0 +1,132 @@
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"$id": "diffusiongemma.advertisement_campaign_contract@1",
"title": "DiffusionGemma Advertisement Campaign Contract",
"type": "object",
"additionalProperties": false,
"required": [
"schema",
"version",
"campaign_name",
"brand",
"audience",
"objective",
"copy",
"claims",
"timeline",
"deliverables",
"exact_copy_sha256",
"ready"
],
"properties": {
"schema": { "const": "diffusiongemma.advertisement_campaign_contract" },
"version": { "const": 1 },
"campaign_name": { "type": "string", "minLength": 1, "maxLength": 160 },
"brand": {
"type": "object",
"additionalProperties": false,
"required": ["brand_name", "product_name", "variant_name"],
"properties": {
"brand_name": { "type": "string", "minLength": 1, "maxLength": 120 },
"product_name": { "type": "string", "minLength": 1, "maxLength": 160 },
"variant_name": { "type": "string", "maxLength": 120 }
}
},
"audience": { "type": "string", "minLength": 1, "maxLength": 1000 },
"objective": { "type": "string", "minLength": 1, "maxLength": 1500 },
"copy": {
"type": "object",
"additionalProperties": false,
"required": [
"brand_name",
"product_name",
"variant_name",
"headline",
"call_to_action",
"legal_line",
"offer_text",
"price_text",
"approved_claims",
"render_policy"
],
"properties": {
"brand_name": { "type": "string", "minLength": 1, "maxLength": 120 },
"product_name": { "type": "string", "minLength": 1, "maxLength": 160 },
"variant_name": { "type": "string", "maxLength": 120 },
"headline": { "type": "string", "minLength": 1, "maxLength": 300 },
"call_to_action": { "type": "string", "minLength": 1, "maxLength": 240 },
"legal_line": { "type": "string", "maxLength": 1000 },
"offer_text": { "type": "string", "maxLength": 300 },
"price_text": { "type": "string", "maxLength": 120 },
"approved_claims": {
"type": "array",
"maxItems": 32,
"items": { "type": "string", "minLength": 1, "maxLength": 300 }
},
"render_policy": { "const": "deterministic_finishing_overlay" }
}
},
"claims": {
"type": "array",
"maxItems": 32,
"items": {
"type": "object",
"additionalProperties": false,
"required": ["text", "status", "evidence"],
"properties": {
"text": { "type": "string", "minLength": 1, "maxLength": 300 },
"status": { "enum": ["approved", "draft", "prohibited"] },
"evidence": { "type": "string", "maxLength": 1000 }
},
"allOf": [
{
"if": { "properties": { "status": { "const": "approved" } }, "required": ["status"] },
"then": { "properties": { "evidence": { "minLength": 1 } } }
}
]
}
},
"timeline": {
"type": "object",
"additionalProperties": false,
"required": ["duration_seconds", "aspect_ratio", "end_card"],
"properties": {
"duration_seconds": { "type": "number", "exclusiveMinimum": 0, "maximum": 60 },
"aspect_ratio": { "enum": ["9:16", "16:9", "1:1", "4:3", "3:4", "3:2", "2:3", "21:9"] },
"end_card": {
"type": "object",
"additionalProperties": false,
"required": ["start_seconds", "duration_seconds", "render_status"],
"properties": {
"start_seconds": { "type": "number", "minimum": 0, "maximum": 60 },
"duration_seconds": { "type": "number", "minimum": 0, "maximum": 10 },
"render_status": { "const": "contracted_not_rendered" }
}
}
}
},
"deliverables": {
"type": "object",
"additionalProperties": false,
"required": ["primary", "adaptations", "cutdowns_seconds", "render_status"],
"properties": {
"primary": { "enum": ["9:16", "16:9", "1:1", "4:3", "3:4", "3:2", "2:3", "21:9"] },
"adaptations": {
"type": "array",
"uniqueItems": true,
"maxItems": 8,
"items": { "enum": ["9:16", "16:9", "1:1", "4:3", "3:4", "3:2", "2:3", "21:9"] }
},
"cutdowns_seconds": {
"type": "array",
"uniqueItems": true,
"maxItems": 8,
"items": { "type": "number", "exclusiveMinimum": 0, "maximum": 60 }
},
"render_status": { "const": "requested_not_rendered" }
}
},
"exact_copy_sha256": { "type": "string", "pattern": "^[0-9a-f]{64}$" },
"ready": { "const": true }
}
}
@@ -0,0 +1,20 @@
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"$id": "https://exportanything.dev/schemas/advertisement_delivery_manifest.schema.json",
"title": "DiffusionGemma Advertisement Delivery Manifest",
"type": "object",
"required": ["schema", "version", "campaign_id", "advertisement_contract_sha256", "master", "cutdowns", "adaptations", "honesty_policy", "ready", "manifest_sha256"],
"properties": {
"schema": {"const": "diffusiongemma.advertisement_delivery_manifest"},
"version": {"const": 1},
"campaign_id": {"$ref": "#/$defs/sha256"},
"advertisement_contract_sha256": {"$ref": "#/$defs/sha256"},
"master": {"type": "object", "required": ["status", "duration_seconds", "frame_count", "pixel_sha256", "audio_sha256"], "properties": {"status": {"const": "rendered"}}},
"cutdowns": {"type": "array", "minItems": 2, "maxItems": 2, "items": {"type": "object", "required": ["id", "status", "duration_seconds", "frame_count", "audio_sample_count"], "properties": {"status": {"const": "rendered"}}}},
"adaptations": {"type": "array", "items": {"type": "object", "required": ["request", "aspect_ratio", "status", "reason"], "properties": {"status": {"enum": ["planned_not_rendered", "not_applicable_same_as_master", "impossible_invalid_aspect"]}}}},
"honesty_policy": {"type": "string", "minLength": 1},
"ready": {"const": true},
"manifest_sha256": {"$ref": "#/$defs/sha256"}
},
"$defs": {"sha256": {"type": "string", "pattern": "^[0-9a-f]{64}$"}}
}
+38
View File
@@ -0,0 +1,38 @@
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"$id": "https://exportanything.dev/schemas/advertisement_h3_plan.schema.json",
"title": "DiffusionGemma Advertisement H3 Plan",
"type": "object",
"required": ["schema", "version", "campaign_id", "advertisement_contract_sha256", "plan_sha256", "fps", "source_native_shot_count", "effective_generation_lane_count", "lanes", "ready"],
"properties": {
"schema": {"const": "diffusiongemma.advertisement_h3_plan"},
"version": {"const": 1},
"campaign_id": {"$ref": "#/$defs/sha256"},
"advertisement_contract_sha256": {"$ref": "#/$defs/sha256"},
"plan_sha256": {"$ref": "#/$defs/sha256"},
"fps": {"const": 24},
"source_native_shot_count": {"type": "integer", "minimum": 1},
"effective_generation_lane_count": {"type": "integer", "minimum": 1, "maximum": 4},
"lanes": {
"type": "array", "minItems": 1, "maxItems": 4,
"items": {
"type": "object",
"required": ["lane_index", "start_seconds", "duration_seconds", "retained_master_frames", "generated_h3_frames", "source_native_shot_indices", "product_presence", "entry_state", "exit_state", "boundary_mode", "reference_picture_tags", "relay_required", "prompt", "prompt_sha256", "ready"],
"properties": {
"lane_index": {"type": "integer", "minimum": 1, "maximum": 4},
"duration_seconds": {"type": "number", "minimum": 5, "maximum": 15},
"retained_master_frames": {"type": "integer", "minimum": 1},
"generated_h3_frames": {"type": "integer", "minimum": 1},
"boundary_mode": {"enum": ["project_start", "hard_cut", "match_state", "relay_continuity", "practical_transition"]},
"reference_picture_tags": {"type": "array", "contains": {"const": "<Picture 3>"}},
"relay_required": {"type": "boolean"},
"prompt": {"type": "string", "minLength": 1},
"prompt_sha256": {"$ref": "#/$defs/sha256"},
"ready": {"const": true}
}
}
},
"ready": {"const": true}
},
"$defs": {"sha256": {"type": "string", "pattern": "^[0-9a-f]{64}$"}}
}
@@ -0,0 +1,74 @@
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"$id": "https://exportanything.dev/schemas/advertisement_master_contract.schema.json",
"title": "DiffusionGemma Advertisement Master Contract",
"type": "object",
"required": [
"schema", "version", "campaign_id", "contract_sha256", "parent_project_id",
"upstream_contracts", "brand_name", "product_name", "production_duration_seconds",
"fps", "master_aspect_ratio", "audio_lock", "reference_assets", "subject_retention",
"h3_planning", "end_card", "mandatory_copy", "requested_deliverables"
],
"properties": {
"schema": {"const": "diffusiongemma.advertisement_master_contract"},
"version": {"const": 1},
"campaign_id": {"$ref": "#/$defs/sha256"},
"contract_sha256": {"$ref": "#/$defs/sha256"},
"parent_project_id": {"type": "string", "minLength": 1},
"upstream_contracts": {
"type": "object",
"required": ["campaign_contract_sha256", "reference_contract_sha256", "soundtrack_contract_sha256"],
"properties": {
"campaign_contract_sha256": {"$ref": "#/$defs/sha256"},
"reference_contract_sha256": {"$ref": "#/$defs/sha256"},
"soundtrack_contract_sha256": {"$ref": "#/$defs/sha256"}
}
},
"brand_name": {"type": "string", "minLength": 1},
"product_name": {"type": "string", "minLength": 1},
"production_duration_seconds": {"type": "number", "exclusiveMinimum": 0, "maximum": 60},
"fps": {"const": 24},
"master_aspect_ratio": {"type": "string", "pattern": "^[0-9]+:[0-9]+$"},
"audio_lock": {
"type": "object",
"required": ["waveform_sha256", "excerpt_duration_seconds", "soundtrack_content_mode"],
"properties": {
"waveform_sha256": {"$ref": "#/$defs/sha256"},
"excerpt_duration_seconds": {"type": "number", "exclusiveMinimum": 0},
"soundtrack_content_mode": {"enum": ["instrumental", "vocal"]}
}
},
"reference_assets": {
"type": "object",
"required": ["picture_1_performer_hero_sha256", "picture_2_performer_contact_sheet_sha256", "picture_3_product_package_sha256", "picture_4_role"]
},
"subject_retention": {
"type": "object",
"required": ["<Subject 1>", "<Subject 2>"]
},
"h3_planning": {
"type": "object",
"required": ["native_shot_count", "generation_lane_count", "maximum_lane_seconds", "boundary_mode"],
"properties": {
"native_shot_count": {"type": "integer", "minimum": 1},
"generation_lane_count": {"type": "integer", "minimum": 1, "maximum": 4},
"maximum_lane_seconds": {"type": "number", "minimum": 5, "maximum": 15},
"boundary_mode": {"enum": ["hard_cut", "match_state", "relay_continuity", "practical_transition"]}
}
},
"end_card": {
"type": "object",
"required": ["duration_seconds", "frame_count", "mode"],
"properties": {
"duration_seconds": {"type": "number", "minimum": 0},
"frame_count": {"type": "integer", "minimum": 0},
"mode": {"const": "replace_master_tail"}
}
},
"mandatory_copy": {"type": "object"},
"requested_deliverables": {"type": ["object", "array"]}
},
"$defs": {
"sha256": {"type": "string", "pattern": "^[0-9a-f]{64}$"}
}
}
@@ -0,0 +1,21 @@
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"$id": "https://exportanything.dev/schemas/advertisement_media_qa.schema.json",
"title": "DiffusionGemma Advertisement Media QA",
"type": "object",
"required": ["schema", "version", "campaign_id", "advertisement_contract_sha256", "host_required_checks", "checks", "overall_status", "ready_for_delivery", "report_sha256"],
"properties": {
"schema": {"const": "diffusiongemma.advertisement_media_qa"},
"version": {"const": 1},
"campaign_id": {"$ref": "#/$defs/sha256"},
"advertisement_contract_sha256": {"$ref": "#/$defs/sha256"},
"host_required_checks": {
"const": ["product_identity", "performer_identity", "copy_legibility", "audio_sync", "technical_integrity"]
},
"checks": {"type": "array", "minItems": 1, "items": {"type": "object", "required": ["check", "status", "evidence"], "properties": {"status": {"enum": ["pass", "fail", "not_measured"]}}}},
"overall_status": {"enum": ["pass", "fail", "not_measured"]},
"ready_for_delivery": {"type": "boolean"},
"report_sha256": {"$ref": "#/$defs/sha256"}
},
"$defs": {"sha256": {"type": "string", "pattern": "^[0-9a-f]{64}$"}}
}
@@ -0,0 +1,86 @@
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"$id": "diffusiongemma.advertisement_reference_contract@1",
"title": "DiffusionGemma Advertisement Reference Contract",
"type": "object",
"additionalProperties": false,
"required": [
"schema",
"version",
"slot_policy",
"subjects",
"picture_slots",
"relay",
"reference_manifest",
"reference_manifest_sha256",
"expected_subject_count",
"ready"
],
"properties": {
"schema": { "const": "diffusiongemma.advertisement_reference_contract" },
"version": { "const": 1 },
"slot_policy": { "enum": ["performer_2_product_1_relay_1", "performer_1_product_1_relay_1", "performer_2_product_1_relay_off"] },
"subjects": {
"type": "array",
"minItems": 2,
"maxItems": 2,
"items": {
"type": "object",
"additionalProperties": false,
"required": ["subject_tag", "role", "description", "retention_attributes"],
"properties": {
"subject_tag": { "enum": ["<Subject 1>", "<Subject 2>"] },
"role": { "enum": ["performer", "product_package"] },
"description": { "type": "string", "minLength": 1, "maxLength": 2000 },
"retention_attributes": { "type": "string", "minLength": 1, "maxLength": 1000 },
"exact_package_copy": {
"type": "array",
"uniqueItems": true,
"maxItems": 32,
"items": { "type": "string", "minLength": 1, "maxLength": 160 }
},
"copy_fidelity_policy": { "const": "qa_required_not_guaranteed_by_generation" }
}
}
},
"picture_slots": {
"type": "array",
"minItems": 2,
"maxItems": 3,
"items": {
"type": "object",
"additionalProperties": false,
"required": ["picture_tag", "role", "subject_tag", "retention", "description", "non_transfer"],
"properties": {
"picture_tag": { "enum": ["<Picture 1>", "<Picture 2>", "<Picture 3>"] },
"role": { "enum": ["performer_primary", "performer_supplemental", "product_package"] },
"subject_tag": { "enum": ["<Subject 1>", "<Subject 2>"] },
"retention": { "const": "fully_preserved" },
"description": { "type": "string", "minLength": 1, "maxLength": 2000 },
"reference_kind": { "enum": ["single_packshot", "product_contact_sheet"] },
"non_transfer": {
"type": "array",
"uniqueItems": true,
"maxItems": 8,
"items": { "type": "string", "minLength": 1, "maxLength": 80 }
}
}
}
},
"relay": {
"type": "object",
"additionalProperties": false,
"required": ["enabled", "picture_tag", "role", "manifest_reservation"],
"properties": {
"enabled": { "type": "boolean" },
"picture_tag": { "enum": ["", "<Picture 3>", "<Picture 4>"] },
"role": { "const": "previous_lane_pose_spatial_lighting_motion_only_not_identity_or_product" },
"manifest_reservation": { "type": "string", "maxLength": 1000 }
}
},
"reference_manifest": { "type": "string", "minLength": 1, "maxLength": 10000 },
"reference_manifest_sha256": { "type": "string", "pattern": "^[0-9a-f]{64}$" },
"expected_subject_count": { "const": 2 },
"ready": { "const": true }
}
}
@@ -0,0 +1,105 @@
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"$id": "diffusiongemma.advertisement_reference_assets@1",
"title": "DiffusionGemma Advertisement Reference Assets",
"type": "object",
"additionalProperties": false,
"required": [
"schema",
"version",
"generation_width",
"generation_height",
"performer_budget",
"product_budget",
"assets",
"director_batch",
"identity_product_combined_sheet",
"ready"
],
"properties": {
"schema": { "const": "diffusiongemma.advertisement_reference_assets" },
"version": { "const": 1 },
"generation_width": { "type": "integer", "minimum": 256, "maximum": 8192 },
"generation_height": { "type": "integer", "minimum": 256, "maximum": 8192 },
"performer_budget": {
"type": "object",
"additionalProperties": false,
"required": ["area_ratio", "hero_share", "roles"],
"properties": {
"area_ratio": { "type": "number", "minimum": 0.25, "maximum": 2 },
"hero_share": { "type": "number", "minimum": 0.2, "maximum": 0.8 },
"roles": { "const": ["performer_hero", "performer_sheet"] }
}
},
"product_budget": {
"type": "object",
"additionalProperties": false,
"required": ["area_ratio", "independent_from_performer_budget", "role"],
"properties": {
"area_ratio": { "type": "number", "minimum": 0.1, "maximum": 1.5 },
"independent_from_performer_budget": { "const": true },
"role": { "const": "product_sheet" }
}
},
"assets": {
"type": "array",
"minItems": 3,
"maxItems": 3,
"prefixItems": [
{ "$ref": "#/$defs/hero" },
{ "$ref": "#/$defs/performerSheet" },
{ "$ref": "#/$defs/productSheet" }
],
"items": false
},
"director_batch": {
"type": "object",
"additionalProperties": false,
"required": ["count", "width", "height", "aspect_policy", "role_order"],
"properties": {
"count": { "const": 3 },
"width": { "type": "integer", "minimum": 256, "maximum": 8192 },
"height": { "type": "integer", "minimum": 256, "maximum": 8192 },
"aspect_policy": { "const": "preserve_then_neutral_pad" },
"role_order": { "const": ["performer_hero", "performer_sheet", "product_sheet"] }
}
},
"identity_product_combined_sheet": { "const": false },
"ready": { "const": true }
},
"$defs": {
"baseAsset": {
"type": "object",
"additionalProperties": false,
"required": ["picture_tag", "role", "sha256", "prepared_role_sha256", "width", "height", "pixel_area_budget", "upscaled"],
"properties": {
"picture_tag": { "enum": ["<Picture 1>", "<Picture 2>", "<Picture 3>"] },
"role": { "enum": ["performer_hero", "performer_sheet", "product_sheet"] },
"sha256": { "type": "string", "pattern": "^[0-9a-f]{64}$" },
"prepared_role_sha256": { "type": "string", "pattern": "^[0-9a-f]{64}$" },
"width": { "type": "integer", "minimum": 1, "maximum": 8192 },
"height": { "type": "integer", "minimum": 1, "maximum": 8192 },
"pixel_area_budget": { "type": "integer", "minimum": 1 },
"upscaled": { "const": false }
}
},
"hero": {
"allOf": [
{ "$ref": "#/$defs/baseAsset" },
{ "properties": { "picture_tag": { "const": "<Picture 1>" }, "role": { "const": "performer_hero" } } }
]
},
"performerSheet": {
"allOf": [
{ "$ref": "#/$defs/baseAsset" },
{ "properties": { "picture_tag": { "const": "<Picture 2>" }, "role": { "const": "performer_sheet" } } }
]
},
"productSheet": {
"allOf": [
{ "$ref": "#/$defs/baseAsset" },
{ "properties": { "picture_tag": { "const": "<Picture 3>" }, "role": { "const": "product_sheet" } } }
]
}
}
}
@@ -0,0 +1,21 @@
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"$id": "https://exportanything.dev/schemas/advertisement_relay_artifact.schema.json",
"title": "DiffusionGemma Advertisement Relay Artifact",
"type": "object",
"required": ["schema", "version", "campaign_id", "plan_sha256", "artifact_id", "completed_lane_index", "next_lane_index", "picture_tag", "pixel_sha256", "shape", "filename", "file_sha256", "persistent"],
"properties": {
"schema": {"const": "diffusiongemma.advertisement_relay_artifact"},
"version": {"const": 1},
"campaign_id": {"$ref": "#/$defs/sha256"},
"plan_sha256": {"$ref": "#/$defs/sha256"},
"artifact_id": {"$ref": "#/$defs/sha256"},
"picture_tag": {"const": "<Picture 4>"},
"pixel_sha256": {"$ref": "#/$defs/sha256"},
"shape": {"type": "array", "prefixItems": [{"const": 1}, {"type": "integer", "minimum": 1}, {"type": "integer", "minimum": 1}, {"const": 3}], "minItems": 4, "maxItems": 4},
"filename": {"type": "string", "pattern": "^[0-9a-f]{64}\\.safetensors$"},
"file_sha256": {"$ref": "#/$defs/sha256"},
"persistent": {"const": true}
},
"$defs": {"sha256": {"type": "string", "pattern": "^[0-9a-f]{64}$"}}
}
@@ -0,0 +1,50 @@
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"$id": "diffusiongemma.advertisement_soundtrack_contract@1",
"title": "DiffusionGemma Advertisement Soundtrack Contract",
"type": "object",
"additionalProperties": false,
"required": [
"schema",
"version",
"requested_content_mode",
"effective_content_mode",
"music3",
"voice_over",
"content_evidence_policy",
"ready"
],
"properties": {
"schema": { "const": "diffusiongemma.advertisement_soundtrack_contract" },
"version": { "const": 1 },
"requested_content_mode": { "enum": ["instrumental", "vocal", "auto"] },
"effective_content_mode": { "enum": ["instrumental", "vocal"] },
"music3": {
"type": "object",
"additionalProperties": false,
"required": ["caption", "tagged_lyrics", "bpm", "time_signature", "language", "target_duration_seconds", "generation_duration_seconds", "selection_headroom_seconds"],
"properties": {
"caption": { "type": "string", "minLength": 1, "maxLength": 4000 },
"tagged_lyrics": { "type": "string", "minLength": 1, "maxLength": 4000 },
"bpm": { "type": "number", "minimum": 0, "maximum": 300 },
"time_signature": { "enum": ["2", "3", "4", "6"] },
"language": { "type": "string", "minLength": 2, "maxLength": 16 },
"target_duration_seconds": { "type": "number", "exclusiveMinimum": 0, "maximum": 60 },
"generation_duration_seconds": { "type": "number", "exclusiveMinimum": 0, "maximum": 60 },
"selection_headroom_seconds": { "type": "number", "minimum": 0, "maximum": 5 }
}
},
"voice_over": {
"type": "object",
"additionalProperties": false,
"required": ["policy", "included_in_motion_guide", "included_in_final_mix"],
"properties": {
"policy": { "enum": ["none", "separate_non_diegetic"] },
"included_in_motion_guide": { "const": false },
"included_in_final_mix": { "type": "boolean" }
}
},
"content_evidence_policy": { "enum": ["vocal_proxy_required", "vocal_proxy_not_required"] },
"ready": { "const": true }
}
}
@@ -0,0 +1,199 @@
from __future__ import annotations
import copy
import hashlib
import importlib.util
import json
import re
import sys
import unittest
from pathlib import Path
ROOT = Path(__file__).resolve().parents[1]
BUILDER_PATH = ROOT / "tools" / "build_advertisement_music3_glowbloom_api.py"
SPEC = importlib.util.spec_from_file_location("advertisement_music3_api_builder", BUILDER_PATH)
assert SPEC is not None and SPEC.loader is not None
builder = importlib.util.module_from_spec(SPEC)
sys.modules[SPEC.name] = builder
SPEC.loader.exec_module(builder)
class AdvertisementMusic3APIFixtureTests(unittest.TestCase):
@classmethod
def setUpClass(cls):
cls.known_good_bytes = builder.KNOWN_GOOD_UI.read_bytes()
cls.source = builder._load_guarded_json(
builder.SOURCE_API,
canonical_hash=builder.SOURCE_API_CANONICAL_SHA256,
)
cls.graph = builder.build_graph(cls.source)
def test_known_good_v6_is_hash_guarded_and_unchanged(self):
self.assertEqual(
hashlib.sha256(self.known_good_bytes).hexdigest(),
builder.KNOWN_GOOD_UI_SHA256,
)
def test_graph_is_acyclic_and_reference_prep_has_explicit_format_size(self):
builder.validate_graph(self.graph)
self.assertEqual(self.graph["803"]["inputs"]["generation_width"], 480)
self.assertEqual(self.graph["803"]["inputs"]["generation_height"], 864)
self.assertFalse(
any(
isinstance(value, list) and value[0] == "187"
for value in self.graph["803"]["inputs"].values()
)
)
def test_graph_rejects_a_dependency_cycle(self):
graph = copy.deepcopy(self.graph)
graph["803"]["inputs"]["generation_width"] = ["187", 10]
with self.assertRaisesRegex(builder.FixtureError, "dependency cycle"):
builder.validate_graph(graph)
def test_runtime_baseline_is_self_contained(self):
classes = {node["class_type"] for node in self.graph.values()}
self.assertIn("DiffusionGemmaAdvertisementMemoryBarrier", classes)
self.assertIn("DiffusionGemmaAdvertisementDirectorPacketRepair", classes)
self.assertNotIn("PathchSageAttentionKJ", classes)
self.assertNotIn("easy cleanGpuUsed", classes)
self.assertNotIn("FL_UnloadAllModels", classes)
self.assertNotIn("EasyCache", classes)
self.assertNotIn("TorchCompileModel", classes)
self.assertEqual(self.graph["716"]["inputs"]["model"], ["713", 0])
self.assertEqual(self.graph["182"]["inputs"]["model_path"], builder.DIRECTOR_MODEL_PATH)
def test_visible_h3_labels_match_the_actual_reference_and_step_contracts(self):
self.assertEqual(
self.graph["710:658"]["_meta"]["title"],
"Shared H3 schedule — simple / 7 sampling steps",
)
self.assertEqual(self.graph["710:658"]["inputs"]["steps"], 7)
expected = (
"H3 GENERATION LANE 1 — P1+P2 performer + P3 product + Audio 1",
"H3 GENERATION LANE 2 — P1+P2 performer + P3 product + P4 relay + Audio 1",
"H3 GENERATION LANE 3 — P1+P2 performer + P3 product + P4 relay + Audio 1",
"H3 GENERATION LANE 4 — P1+P2 performer + P3 product + P4 relay + Audio 1",
)
for node_id, title in zip(("710:667", "710:684", "710:692", "710:700"), expected):
self.assertEqual(self.graph[node_id]["_meta"]["title"], title)
def test_manifest_describes_the_exact_api_graph_and_uses_real_hashes(self):
manifest = builder._manifest(self.graph)
self.assertEqual(manifest["api_audio_sources"], ["MiniMax Music 3"])
self.assertEqual(
manifest["companion_ui_only_audio_sources"],
["Upload song", "Legacy ACE-Step"],
)
self.assertEqual(manifest["models"], builder.MODEL_INVENTORY)
self.assertEqual(set(manifest["models"]), {"director", "minimax_h3", "minimax_music3"})
self.assertEqual(len(manifest["models"]["minimax_h3"]), 5)
for asset in manifest["assets"].values():
self.assertRegex(asset["sha256"], re.compile(r"^[0-9a-f]{64}$"))
def test_director_transport_repair_is_narrow_and_observable(self):
self.assertEqual(self.graph["185"]["inputs"]["mode"], "off")
self.assertEqual(
self.graph["185"]["_meta"]["title"],
"5b. H3 Grounding Guard — OFF baseline (contract validation stays active)",
)
self.assertEqual(
self.graph["820"]["class_type"],
"DiffusionGemmaAdvertisementDirectorPacketRepair",
)
self.assertEqual(self.graph["820"]["inputs"]["director_final_json"], ["186", 0])
self.assertEqual(self.graph["820"]["inputs"]["director_raw_response"], ["186", 2])
self.assertEqual(self.graph["820"]["inputs"]["director_metadata_json"], ["186", 3])
self.assertEqual(self.graph["820"]["inputs"]["grounding_status"], ["186", 4])
self.assertEqual(self.graph["820"]["inputs"]["grounding_report_json"], ["186", 5])
self.assertEqual(self.graph["820"]["inputs"]["reference_contract_json"], ["802", 0])
self.assertEqual(self.graph["187"]["inputs"]["final_json"], ["820", 0])
self.assertEqual(self.graph["902"]["inputs"]["source"], ["186", 2])
def test_music3_and_reference_topology_matches_advertisement_contract(self):
self.assertEqual(
self.graph["806"]["inputs"]["clip_name"],
builder.MUSIC3_MODELS["text_encoder"],
)
self.assertEqual(
self.graph["807"]["inputs"]["unet_name"],
builder.MUSIC3_MODELS["diffusion_model"],
)
self.assertEqual(
self.graph["808"]["inputs"]["vae_name"],
builder.MUSIC3_MODELS["vae"],
)
for lane, node_id in enumerate(("710:667", "710:684", "710:692", "710:700"), start=1):
inputs = self.graph[node_id]["inputs"]
self.assertNotIn("model", inputs)
self.assertEqual(inputs["ref_images.ref_image_0"], ["803", 0])
self.assertEqual(inputs["ref_images.ref_image_1"], ["803", 1])
self.assertEqual(inputs["ref_images.ref_image_2"], ["803", 2])
self.assertEqual("ref_images.ref_image_3" in inputs, lane > 1)
for node_id in ("710:658", "710:660", "710:686", "710:694", "710:702"):
self.assertEqual(self.graph[node_id]["inputs"]["model"], ["716", 0])
def test_fixture_has_master_two_real_cutdowns_and_honest_qa(self):
save_nodes = [
node for node in self.graph.values() if node["class_type"] == "SaveVideo"
]
self.assertEqual(len(save_nodes), 3)
self.assertTrue(all("REVIEW DRAFT" in node["_meta"]["title"] for node in save_nodes))
self.assertTrue(
all("/review_drafts/" in node["inputs"]["filename_prefix"] for node in save_nodes)
)
checks = json.loads(self.graph["836"]["inputs"]["checks_json"])
self.assertEqual(checks["technical_integrity"]["status"], "pass")
self.assertEqual(checks["product_identity"], "not_measured")
self.assertEqual(checks["performer_identity"], "not_measured")
self.assertEqual(checks["copy_legibility"], "not_measured")
self.assertEqual(checks["audio_sync"], "not_measured")
self.assertEqual(self.graph["900"]["class_type"], "PreviewAny")
self.assertEqual(self.graph["900"]["inputs"]["source"], ["815", 4])
self.assertEqual(self.graph["901"]["class_type"], "PreviewAny")
self.assertEqual(self.graph["901"]["inputs"]["source"], ["187", 4])
self.assertEqual(self.graph["902"]["class_type"], "PreviewAny")
self.assertEqual(self.graph["902"]["inputs"]["source"], ["186", 2])
def test_one_settings_node_controls_every_duplicate_prone_consumer(self):
self.assertEqual(
self.graph["849"]["class_type"],
"DiffusionGemmaAdvertisementWorkflowControls",
)
for node_id, input_name, slot in (
("801", "production_duration_seconds", 0),
("804", "target_duration_seconds", 0),
("183", "target_duration_seconds", 0),
("673", "target_duration_seconds", 0),
("676", "production_duration_seconds", 0),
("815", "excerpt_duration_seconds", 0),
("816", "duration", 0),
("817", "target_duration_seconds", 0),
("818", "excerpt_duration_seconds", 0),
("673", "custom_shot_count", 1),
("821", "native_shot_count", 1),
("675", "performance_mode", 2),
("823", "performance_mode", 2),
("835", "cutdown_15_start_seconds", 3),
("835", "cutdown_6_start_seconds", 4),
("801", "deliverables_json", 5),
("676", "deliverables_json", 5),
("801", "aspect_ratio", 6),
("676", "aspect_ratio", 6),
("673", "generation_mode", 7),
("673", "shot_count", 8),
("673", "shot_count_override", 9),
("673", "audio_mode", 10),
("673", "dialogue_mode", 11),
("676", "excerpt_start_seconds", 12),
("676", "generation_model", 13),
("676", "max_h3_shot_seconds", 14),
("187", "resolution_aspect_ratio_override", 15),
):
self.assertEqual(self.graph[node_id]["inputs"][input_name], ["849", slot])
self.assertEqual(self.graph["818"]["inputs"]["expected_bpm"], ["814", 5])
if __name__ == "__main__":
unittest.main()
+352
View File
@@ -0,0 +1,352 @@
from __future__ import annotations
import copy
from contextlib import redirect_stdout
from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer
import importlib.util
import io
import json
from pathlib import Path
import sys
import tempfile
import threading
import unittest
import uuid
ROOT = Path(__file__).resolve().parents[1]
BUILDER_PATH = ROOT / "tools" / "build_advertisement_music3_workflow.py"
SPEC = importlib.util.spec_from_file_location("advertisement_music3_workflow_builder", BUILDER_PATH)
if SPEC is None or SPEC.loader is None: # pragma: no cover - import machinery guard
raise RuntimeError("Could not load Advertisement Music3 workflow builder.")
builder = importlib.util.module_from_spec(SPEC)
sys.modules[SPEC.name] = builder
SPEC.loader.exec_module(builder)
def _scalar_definition(value: object) -> list[object]:
if isinstance(value, bool):
return ["BOOLEAN", {"default": value}]
if isinstance(value, int):
return ["INT", {"default": value}]
if isinstance(value, float):
return ["FLOAT", {"default": value}]
return ["STRING", {"default": value, "multiline": isinstance(value, str) and "\n" in value}]
def synthetic_object_info(graph: dict[str, object]) -> dict[str, object]:
"""Build the smallest /object_info envelope needed by this exact graph."""
class_inputs: dict[str, dict[str, list[object]]] = {}
class_order: dict[str, list[str]] = {}
maximum_output_slot: dict[str, int] = {}
class_titles: dict[str, str] = {}
for node in graph.values():
class_type = str(node["class_type"])
class_inputs.setdefault(class_type, {})
class_order.setdefault(class_type, [])
class_titles.setdefault(class_type, class_type)
for name, value in node.get("inputs", {}).items():
if name not in class_order[class_type]:
class_order[class_type].append(name)
if not builder._is_connection(value):
class_inputs[class_type][name] = _scalar_definition(value)
else:
class_inputs[class_type].setdefault(name, ["*", {}])
for node in graph.values():
for value in node.get("inputs", {}).values():
if not builder._is_connection(value):
continue
origin_id, origin_slot = value
origin_type = str(graph[origin_id]["class_type"])
maximum_output_slot[origin_type] = max(
maximum_output_slot.get(origin_type, -1), int(origin_slot)
)
result: dict[str, object] = {}
for class_type, inputs in class_inputs.items():
output_count = maximum_output_slot.get(class_type, -1) + 1
result[class_type] = {
"input": {"required": inputs},
"input_order": {"required": class_order[class_type]},
"output": ["*"] * output_count,
"output_name": [f"output_{index}" for index in range(output_count)],
"display_name": class_titles[class_type],
"python_module": (
"custom_nodes.ComfyUI-DiffusionGemmaPromptBuilder"
if class_type.startswith("DiffusionGemma")
else "test.hermetic_object_info"
),
}
return result
class _ObjectInfoServer:
def __init__(self, payload: dict[str, object], port: int = 0):
encoded = json.dumps(payload, separators=(",", ":")).encode("utf-8")
class Handler(BaseHTTPRequestHandler):
def do_GET(self): # noqa: N802 - stdlib handler API
if self.path.rstrip("/") != "/object_info":
self.send_error(404)
return
self.send_response(200)
self.send_header("Content-Type", "application/json")
self.send_header("Content-Length", str(len(encoded)))
self.end_headers()
self.wfile.write(encoded)
def log_message(self, _format, *args):
del args
self.server = ThreadingHTTPServer(("127.0.0.1", int(port)), Handler)
self.thread = threading.Thread(target=self.server.serve_forever, daemon=True)
@property
def url(self) -> str:
return f"http://127.0.0.1:{self.server.server_port}"
def __enter__(self):
self.thread.start()
return self
def __exit__(self, exc_type, exc, traceback):
del exc_type, exc, traceback
self.server.shutdown()
self.server.server_close()
self.thread.join(timeout=5)
def _node(workflow: dict[str, object], class_type: str) -> dict[str, object]:
matches = [node for node in workflow["nodes"] if node.get("type") == class_type]
if len(matches) != 1:
raise AssertionError(f"Expected exactly one {class_type}, found {len(matches)}.")
return matches[0]
def _link(workflow: dict[str, object], target: dict[str, object], input_name: str) -> list[object]:
slot = next(item for item in target["inputs"] if item.get("name") == input_name)
return next(item for item in workflow["links"] if item[0] == slot["link"])
class AdvertisementMusic3WorkflowTests(unittest.TestCase):
@classmethod
def setUpClass(cls):
cls.field_test = builder._load_guarded(
builder.FIELD_TEST_API,
canonical_hash=builder.FIELD_TEST_API_CANONICAL_SHA256,
)
cls.music3 = builder._load_guarded(
builder.MUSIC3_API,
canonical_hash=builder.MUSIC3_API_CANONICAL_SHA256,
)
cls.graph = builder._with_lazy_fallbacks(cls.music3, cls.field_test)
cls.object_info = synthetic_object_info(cls.graph)
cls.workflow = builder.build_workflow(cls.graph, cls.object_info)
def test_v6_byte_hash_is_guarded_and_output_records_lineage(self):
source = builder._load_guarded(
builder.KNOWN_GOOD_UI,
byte_hash=builder.KNOWN_GOOD_UI_SHA256,
)
self.assertEqual(
builder._sha256_bytes(builder.KNOWN_GOOD_UI.read_bytes()),
builder.KNOWN_GOOD_UI_SHA256,
)
marker = self.workflow["extra"][builder.SCHEMA]
self.assertEqual(marker["known_good_v6_file_sha256"], builder.KNOWN_GOOD_UI_SHA256)
self.assertTrue(marker["known_good_v6_is_never_overwritten"])
with tempfile.TemporaryDirectory() as temporary:
tampered = Path(temporary) / "v6.json"
tampered.write_bytes(builder.KNOWN_GOOD_UI.read_bytes() + b"\n")
with self.assertRaisesRegex(builder.WorkflowError, "Immutable source changed"):
builder._load_guarded(tampered, byte_hash=builder.KNOWN_GOOD_UI_SHA256)
self.assertNotEqual(self.workflow["id"], source.get("id"))
def test_workflow_uuid_is_fresh_valid_and_flattened(self):
parsed = uuid.UUID(str(self.workflow["id"]))
self.assertEqual(str(parsed), builder.WORKFLOW_UUID)
self.assertEqual(self.workflow["revision"], 1)
marker = self.workflow["extra"][builder.SCHEMA]
self.assertTrue(marker["workflow_uuid_is_fresh"])
self.assertEqual(marker["graph_form"], "flattened_independent_no_shared_subgraph_uuid")
def test_music3_default_has_lazy_upload_and_ace_fallback_links(self):
router = _node(self.workflow, "DiffusionGemmaAdvertisementSoundtrackSourceRouter")
self.assertEqual(router["widgets_values"][0], "MiniMax Music 3")
music_link = _link(self.workflow, router, "music3_candidate_1")
upload_link = _link(self.workflow, router, "uploaded_audio")
ace_links = [_link(self.workflow, router, f"ace_candidate_{index}") for index in range(1, 5)]
by_id = {node["id"]: node for node in self.workflow["nodes"]}
self.assertEqual(by_id[music_link[1]]["type"], "VAEDecodeAudioTiled")
self.assertEqual(by_id[upload_link[1]]["type"], "DiffusionGemmaUploadSong")
self.assertTrue(all(by_id[link[1]]["type"] == "VAEDecodeAudio" for link in ace_links))
self.assertEqual(self.workflow["extra"][builder.SCHEMA]["audio_source_default"], "MiniMax Music 3")
self.assertEqual(
self.workflow["extra"][builder.SCHEMA]["fallbacks"],
["Upload song", "Legacy ACE-Step"],
)
selectors = [
node
for node in self.workflow["nodes"]
if node.get("type") == "DiffusionGemmaAdvertisementAudioCandidateSelector"
]
verifier = next(
node for node in selectors if "Verify exact" in str(node.get("title", ""))
)
policy_link = _link(self.workflow, verifier, "source_policy")
self.assertEqual(by_id[policy_link[1]]["type"], "DiffusionGemmaAdvertisementSoundtrackSourceRouter")
self.assertEqual(policy_link[2], 8)
bpm_link = _link(self.workflow, verifier, "expected_bpm")
self.assertEqual(by_id[bpm_link[1]]["type"], "DiffusionGemmaAdvertisementSoundtrackSourceRouter")
self.assertEqual(bpm_link[2], 5)
def test_h3_topology_keeps_p1_p2_p3_and_persistent_p4_only_after_lane_one(self):
h3_nodes = sorted(
[node for node in self.workflow["nodes"] if node.get("type") == "MiniMaxH3ReferenceToVideo"],
key=lambda node: float(node["pos"][1]),
)
self.assertEqual(len(h3_nodes), 4)
by_id = {node["id"]: node for node in self.workflow["nodes"]}
for lane, node in enumerate(h3_nodes, start=1):
for name, expected_slot in (
("ref_images.ref_image_0", 0),
("ref_images.ref_image_1", 1),
("ref_images.ref_image_2", 2),
):
link = _link(self.workflow, node, name)
self.assertEqual(by_id[link[1]]["type"], "DiffusionGemmaAdvertisementReferenceAssetPrep")
self.assertEqual(link[2], expected_slot)
p4 = [item for item in node["inputs"] if item.get("name") == "ref_images.ref_image_3"]
if lane == 1:
self.assertEqual(p4, [])
else:
self.assertEqual(len(p4), 1)
link = next(item for item in self.workflow["links"] if item[0] == p4[0]["link"])
self.assertEqual(by_id[link[1]]["type"], "DiffusionGemmaAdvertisementRelayGate")
self.assertEqual(
sum(node.get("type") == "DiffusionGemmaAdvertisementRelayArtifact" for node in self.workflow["nodes"]),
3,
)
self.assertEqual(
sum(node.get("type") == "DiffusionGemmaAdvertisementRelayGate" for node in self.workflow["nodes"]),
3,
)
def test_baseline_rejects_acceleration_and_inherited_convenience_runtime_nodes(self):
types = [str(node.get("type", "")) for node in self.workflow["nodes"]]
self.assertFalse(set(types).intersection(builder.FORBIDDEN_RUNTIME_CLASSES))
self.assertEqual(types.count("DiffusionGemmaAdvertisementMemoryBarrier"), 5)
self.assertNotIn("DiffusionGemmaSplatStagePlanner", types)
self.assertNotIn("SplatStageBlueprintRouter", types)
self.assertNotIn("DiffusionGemmaContextHub", types)
self.assertEqual(types.count("SaveVideo"), 3)
self.assertEqual(types.count("PreviewAny"), 4)
self.assertEqual(types.count("PreviewAudio"), 1)
self.assertEqual(types.count("SaveAudio"), 1)
def test_saved_workflow_is_live_metadata_built_and_has_editable_performance_guidance(self):
saved = json.loads(builder.OUTPUT.read_text(encoding="utf-8"))
builder.validate_workflow(saved)
self.assertEqual(len(saved["nodes"]), 135)
self.assertEqual(len(saved["links"]), 356)
controls = _node(saved, "DiffusionGemmaAdvertisementWorkflowControls")
self.assertEqual(controls["widgets_values"], ["30 seconds", 8, "Natural / audio-led sync"])
combo_outputs = {
"performance_mode",
"master_aspect_ratio",
"h3_generation_mode",
"h3_shot_count_mode",
"h3_audio_mode",
"h3_dialogue_mode",
}
by_id = {node["id"]: node for node in saved["nodes"]}
by_link = {link[0]: link for link in saved["links"]}
for output in controls["outputs"]:
if output["name"] not in combo_outputs:
continue
self.assertEqual(output["type"], "COMBO")
for link_id in output.get("links") or []:
link = by_link[link_id]
self.assertEqual(link[5], "COMBO")
target = by_id[link[3]]
self.assertEqual(target["inputs"][link[4]]["type"], "COMBO")
performance = _node(saved, "DiffusionGemmaMusicVideoPerformanceMode")
guidance_link = _link(saved, performance, "base_audio_guidance")
self.assertEqual(by_id[guidance_link[1]]["type"], "PrimitiveStringMultiline")
note = _node(saved, "MarkdownNote")
self.assertEqual(note["properties"], {})
load_images = [node for node in saved["nodes"] if node.get("type") == "LoadImage"]
self.assertEqual(len(load_images), 3)
self.assertTrue(
all(
len(node["widgets_values"]) == 2
and node["widgets_values"][1] == "image"
for node in load_images
)
)
expected_inputs = [
{
"localized_name": "image",
"name": "image",
"type": "COMBO",
"widget": {"name": "image"},
"link": None,
},
{
"localized_name": "choose file to upload",
"name": "upload",
"type": "IMAGEUPLOAD",
"widget": {"name": "upload"},
"link": None,
},
]
self.assertTrue(all(node["inputs"] == expected_inputs for node in load_images))
def test_check_and_generation_run_against_hermetic_object_info(self):
with _ObjectInfoServer(self.object_info) as server, tempfile.TemporaryDirectory() as temporary:
check_stdout = io.StringIO()
with redirect_stdout(check_stdout):
self.assertEqual(
builder.main(["--object-info-url", server.url, "--check"]),
0,
)
checked = json.loads(check_stdout.getvalue())
self.assertEqual(checked["workflow_id"], builder.WORKFLOW_UUID)
output = Path(temporary) / "advertisement.json"
generate_stdout = io.StringIO()
with redirect_stdout(generate_stdout):
self.assertEqual(
builder.main(
["--object-info-url", server.url, "--output", str(output)]
),
0,
)
generated = json.loads(output.read_text(encoding="utf-8"))
summary = json.loads(generate_stdout.getvalue())
self.assertEqual(summary["action"], "created")
self.assertEqual(summary["sha256"], builder._sha256_bytes(output.read_bytes()))
self.assertEqual(generated, self.workflow)
def test_validator_rejects_nonmatching_saved_socket_types(self):
saved = json.loads(builder.OUTPUT.read_text(encoding="utf-8"))
tampered = copy.deepcopy(saved)
campaign = _node(tampered, "DiffusionGemmaAdvertisementCampaignContract")
duration_input = next(
item for item in campaign["inputs"] if item["name"] == "production_duration_seconds"
)
duration_input["type"] = "INT"
with self.assertRaisesRegex(builder.WorkflowError, "link type mismatch"):
builder.validate_workflow(tampered)
def test_live_builder_rejects_an_unknown_api_input_name(self):
graph = copy.deepcopy(self.graph)
graph["710:667"]["inputs"]["definitely_unknown"] = ["716", 0]
with self.assertRaisesRegex(
builder.WorkflowError,
r"MiniMaxH3ReferenceToVideo has no connected input definitely_unknown \(710:667\)",
):
builder.build_workflow(graph, self.object_info)
if __name__ == "__main__":
unittest.main()
+311
View File
@@ -0,0 +1,311 @@
from __future__ import annotations
import json
import math
import unittest
from unittest.mock import patch
import torch
import advertising_audio_nodes as audio
import advertising_contract_nodes as contracts
def soundtrack_contract(content_mode="Instrumental", voice_over_policy="None"):
lyrics = "[Verse]\nA real sung line." if content_mode == "Vocal" else ""
language = "en" if content_mode == "Vocal" else "unknown"
return contracts.DiffusionGemmaAdvertisementSoundtrackContract().build(
content_mode=content_mode,
genre_style="percussive alternative pop",
mood="bright",
instrumentation="bass, drums, handclaps",
target_duration_seconds=2.0,
bpm=105.0,
time_signature="4",
language=language,
lyrics=lyrics,
voice_over_policy=voice_over_policy,
arrangement_notes="One complete two-second test arc.",
)[0]
def fake_audio(value=0.1, seconds=3.0, sample_rate=1000):
waveform = torch.full((1, 1, int(seconds * sample_rate)), float(value))
return {"waveform": waveform, "sample_rate": sample_rate}
def fake_analysis(candidate, vocal_proxy=False):
return {
"waveform_sha256": audio.legacy_audio.waveform_sha256(candidate),
"duration_seconds": 3.0,
"production_score": 0.9,
"tempo_relation": "direct",
"expected_bpm": 105.0,
"detected_bpm": 105.0,
"canonical_tempo_relative_error": 0.0,
"suggested_excerpt": {
"start_seconds": 0.0,
"score": 0.9,
"likely_vocal_active_proxy": bool(vocal_proxy),
"onset_density_per_second": 1.0,
"low_density_visual_recovery_coverage": 1.0,
"tonal_family_consistency": 1.0,
"tonal_transition_stability": 1.0,
},
}
class AdvertisementAudioSelectorTests(unittest.TestCase):
def select(self, contract, candidate, **overrides):
values = {
"soundtrack_contract_json": contract,
"candidate_count": 1,
"selection_mode": "auto_select",
"expected_bpm": 105.0,
"excerpt_duration_seconds": 2.0,
"minimum_score": 0.5,
"candidate_1": candidate,
"source_policy": "minimax_music3",
}
values.update(overrides)
return audio.DiffusionGemmaAdvertisementAudioCandidateSelector().select(**values)
def test_generated_instrumental_does_not_require_vocal_proxy(self):
candidate = fake_audio()
analysis = fake_analysis(candidate, vocal_proxy=False)
with patch.object(audio.legacy_audio, "analyze_decoded_audio", return_value=analysis), patch.object(
audio.legacy_audio,
"_candidate_failures",
return_value=["no_non_silent_vocal_active_proxy_window"],
), patch.object(audio.legacy_audio, "_candidate_advisories", return_value=[]):
result = self.select(soundtrack_contract("Instrumental"), candidate)
self.assertTrue(result[-1])
report = json.loads(result[4])
self.assertEqual(report["settings"]["source_policy"], "minimax_music3")
self.assertIn(
"instrumental_contract_vocal_proxy_not_required",
report["candidates"][0]["advisories"],
)
def test_requested_tempo_mismatch_is_reported_even_when_octave_relation_is_direct(self):
candidate = fake_audio()
analysis = fake_analysis(candidate, vocal_proxy=False)
analysis.update(
{
"expected_bpm": 122.0,
"detected_bpm": 87.890625,
"canonical_tempo_relative_error": abs(87.890625 - 122.0) / 122.0,
}
)
with patch.object(audio.legacy_audio, "analyze_decoded_audio", return_value=analysis), patch.object(
audio.legacy_audio,
"_candidate_failures",
return_value=[],
), patch.object(audio.legacy_audio, "_candidate_advisories", return_value=[]):
result = self.select(soundtrack_contract("Instrumental"), candidate, expected_bpm=122.0)
self.assertTrue(result[-1])
candidate_report = json.loads(result[4])["candidates"][0]
self.assertIn("requested_tempo_mismatch_signal_estimate", candidate_report["advisories"])
self.assertFalse(candidate_report["analysis"]["requested_tempo_match_within_10_percent"])
self.assertAlmostEqual(candidate_report["analysis"]["expected_bpm"], 122.0)
def test_vocal_mode_requires_proxy_and_instrumental_keeps_other_failures(self):
candidate = fake_audio()
analysis = fake_analysis(candidate, vocal_proxy=False)
with patch.object(audio.legacy_audio, "analyze_decoded_audio", return_value=analysis), patch.object(
audio.legacy_audio,
"_candidate_failures",
return_value=["no_non_silent_vocal_active_proxy_window"],
), patch.object(audio.legacy_audio, "_candidate_advisories", return_value=[]):
vocal = self.select(soundtrack_contract("Vocal"), candidate)
self.assertFalse(vocal[-1])
self.assertIn("no_non_silent_vocal", vocal[-2])
with patch.object(audio.legacy_audio, "analyze_decoded_audio", return_value=analysis), patch.object(
audio.legacy_audio,
"_candidate_failures",
return_value=["no_non_silent_vocal_active_proxy_window", "excessive_clipping"],
), patch.object(audio.legacy_audio, "_candidate_advisories", return_value=[]):
instrumental = self.select(soundtrack_contract("Instrumental"), candidate)
self.assertFalse(instrumental[-1])
self.assertIn("excessive_clipping", instrumental[-2])
def test_hash_lock_selects_exact_candidate_without_fallback(self):
first = fake_audio(0.05)
second = fake_audio(0.1)
def analyze(candidate, **_kwargs):
return fake_analysis(candidate, vocal_proxy=False)
lock_hash = audio.legacy_audio.waveform_sha256(second)
with patch.object(audio.legacy_audio, "analyze_decoded_audio", side_effect=analyze), patch.object(
audio.legacy_audio,
"_candidate_failures",
return_value=["no_non_silent_vocal_active_proxy_window"],
), patch.object(audio.legacy_audio, "_candidate_advisories", return_value=[]):
result = self.select(
soundtrack_contract("Instrumental"),
first,
candidate_count=2,
candidate_2=second,
selection_mode="lock_by_hash",
locked_waveform_sha256=lock_hash,
)
self.assertTrue(result[-1])
self.assertIs(result[0], second)
self.assertEqual(result[2], lock_hash)
self.assertEqual(json.loads(result[4])["selected_candidate_index"], 2)
class AdvertisementSoundtrackRouterTests(unittest.TestCase):
def test_lazy_status_requests_only_selected_source(self):
pending = audio.DiffusionGemmaAdvertisementSoundtrackSourceRouter.check_lazy_status(
source_mode="MiniMax Music 3",
music3_candidate_count=1,
music3_expected_bpm=105.0,
music3_lyrics="[Instrumental]",
music3_duration_seconds=35.0,
music3_candidate_1=None,
uploaded_audio=None,
ace_candidate_1=None,
)
self.assertEqual(pending, ["music3_candidate_1"])
def test_music3_default_and_upload_emit_distinct_provenance(self):
candidate = fake_audio(seconds=3.0)
router = audio.DiffusionGemmaAdvertisementSoundtrackSourceRouter()
generated = router.route(
"MiniMax Music 3",
music3_candidate_count=1,
music3_expected_bpm=105.0,
music3_lyrics="[Instrumental]",
music3_duration_seconds=3.0,
music3_candidate_1=candidate,
)
self.assertEqual(generated[4], 1)
self.assertEqual(generated[8], "minimax_music3")
uploaded = router.route(
"Upload song",
uploaded_audio=candidate,
uploaded_duration_seconds=3.0,
uploaded_expected_bpm=0.0,
uploaded_lyrics="",
uploaded_waveform_sha256=audio.legacy_audio.waveform_sha256(candidate),
uploaded_ready=True,
)
self.assertEqual(uploaded[8], "uploaded_song")
self.assertIs(uploaded[0], candidate)
class AdvertisementAudioMixerTests(unittest.TestCase):
def test_voice_over_never_enters_motion_guide_but_enters_final_mix(self):
sample_rate = 1000
clock = torch.arange(2000, dtype=torch.float32) / sample_rate
music_wave = (0.08 * torch.sin(2.0 * math.pi * 30.0 * clock)).reshape(1, 1, -1)
voice_wave = torch.full((1, 1, 500), 0.08)
music = {"waveform": music_wave, "sample_rate": sample_rate}
voice = {"waveform": voice_wave, "sample_rate": sample_rate}
result = audio.DiffusionGemmaAdvertisementAudioMixer().mix(
music_audio=music,
soundtrack_contract_json=soundtrack_contract(
"Instrumental", "Separate non-diegetic VO"
),
target_duration_seconds=2.0,
music_gain_db=0.0,
voice_over_gain_db=0.0,
ducking_db=-9.0,
duck_attack_ms=20.0,
duck_release_ms=50.0,
peak_ceiling_dbfs=-1.0,
clipping_policy="Attenuate mix to ceiling",
voice_over=voice,
voice_over_start_seconds=0.5,
)
motion, final = result[0], result[1]
report = json.loads(result[4])
self.assertTrue(torch.equal(motion["waveform"], music_wave))
self.assertFalse(torch.equal(final["waveform"], motion["waveform"]))
self.assertNotEqual(result[2], result[3])
self.assertFalse(report["voice_over"]["included_in_motion_guide"])
self.assertTrue(report["voice_over"]["included_in_final_mix"])
self.assertEqual(report["levels"]["lufs"], "not_measured")
self.assertEqual(final["waveform"].shape[-1], 2000)
self.assertLess(report["ducking"]["minimum_applied_gain"], 1.0)
def test_no_voice_over_keeps_motion_and_final_hashes_equal(self):
music = fake_audio(0.05, seconds=2.0)
result = audio.DiffusionGemmaAdvertisementAudioMixer().mix(
music,
soundtrack_contract("Instrumental", "None"),
2.0,
0.0,
0.0,
-9.0,
20.0,
50.0,
-1.0,
"Attenuate mix to ceiling",
)
self.assertEqual(result[2], result[3])
self.assertTrue(torch.equal(result[0]["waveform"], result[1]["waveform"]))
def test_one_sample_rounding_deficit_is_padded_but_material_shortfall_fails(self):
sample_rate = 44100
short_by_one = {
"waveform": torch.full((1, 2, sample_rate - 1), 0.05),
"sample_rate": sample_rate,
}
result = audio.DiffusionGemmaAdvertisementAudioMixer().mix(
short_by_one,
soundtrack_contract("Instrumental", "None"),
1.0,
0.0,
0.0,
-9.0,
20.0,
50.0,
-1.0,
"Attenuate mix to ceiling",
)
report = json.loads(result[4])
self.assertEqual(result[0]["waveform"].shape[-1], sample_rate)
self.assertEqual(report["duration_alignment"]["right_padding_samples"], 1)
self.assertEqual(float(result[0]["waveform"][..., -1].abs().max()), 0.0)
materially_short = {
"waveform": torch.full((1, 2, sample_rate - 100), 0.05),
"sample_rate": sample_rate,
}
with self.assertRaisesRegex(ValueError, "shorter than"):
audio.DiffusionGemmaAdvertisementAudioMixer().mix(
materially_short,
soundtrack_contract("Instrumental", "None"),
1.0,
0.0,
0.0,
-9.0,
20.0,
50.0,
-1.0,
"Attenuate mix to ceiling",
)
def test_clipping_fail_policy_is_explicit(self):
music = fake_audio(1.0, seconds=2.0)
with self.assertRaisesRegex(ValueError, "exceeds the configured"):
audio.DiffusionGemmaAdvertisementAudioMixer().mix(
music,
soundtrack_contract("Instrumental", "None"),
2.0,
6.0,
0.0,
-9.0,
20.0,
50.0,
-1.0,
"Fail on clipping",
)
if __name__ == "__main__":
unittest.main()
+322
View File
@@ -0,0 +1,322 @@
from __future__ import annotations
import hashlib
import json
from pathlib import Path
import unittest
import torch
import advertising_contract_nodes as contracts
ROOT = Path(__file__).resolve().parents[1]
def build_campaign(**overrides):
values = {
"brand_name": "AFTERS",
"product_name": "Sparkling Tea",
"variant_name": "Yuzu + White Peach",
"audience": "Trend-conscious adults in their twenties",
"campaign_objective": "Build awareness for a sophisticated zero-proof social drink.",
"headline": "STAY FOR THE AFTER.",
"call_to_action": "FIND YOUR AFTER.",
"claims_json": json.dumps(
[
{
"text": "Zero proof",
"status": "approved",
"evidence": "Approved formulation specification 2026-08",
},
{"text": "Improves focus", "status": "prohibited", "evidence": ""},
]
),
"production_duration_seconds": 30.0,
"aspect_ratio": "9:16",
"end_card_duration_seconds": 3.0,
"deliverables_json": '{"primary":"9:16","adaptations":["1:1","16:9"],"cutdowns_seconds":[15,6]}',
"campaign_name": "Stay for the After",
"legal_line": "Product shown is a fictional concept.",
"offer_text": "",
"price_text": "",
}
values.update(overrides)
return contracts.DiffusionGemmaAdvertisementCampaignContract().build(**values)
def build_soundtrack(**overrides):
values = {
"content_mode": "Instrumental",
"genre_style": "percussive alternative pop, not EDM",
"mood": "bright, spontaneous, tactile",
"instrumentation": "rubbery bass, dry drums, handclaps, muted guitar",
"target_duration_seconds": 30.0,
"bpm": 105.0,
"time_signature": "4",
"language": "unknown",
"lyrics": "",
"voice_over_policy": "Separate non-diegetic VO",
"arrangement_notes": "Clean edit points near 6, 15, and 27 seconds.",
"do_not_sound_like": "four-on-the-floor nightclub EDM",
}
values.update(overrides)
return contracts.DiffusionGemmaAdvertisementSoundtrackContract().build(**values)
class AdvertisementCampaignContractTests(unittest.TestCase):
def test_workflow_controls_are_one_safe_authority_and_derive_tail_cutdowns(self):
node = contracts.DiffusionGemmaAdvertisementWorkflowControls()
result = node.resolve("30 seconds", 8, "Natural / audio-led sync")
self.assertEqual(result[:5], (30.0, 8, "Natural / audio-led sync", 15.0, 24.0))
self.assertEqual(json.loads(result[5])["cutdowns_seconds"], [15, 6])
self.assertEqual(result[6], "9:16")
self.assertEqual(
result[7:15],
(
"ref2va",
"custom",
0,
"auto_scene_audio",
"off",
0.0,
"MiniMax H3 Ref2VA",
15.0,
),
)
self.assertEqual(result[15], "9:16")
self.assertTrue(result[-1])
self.assertEqual(
contracts.DiffusionGemmaAdvertisementWorkflowControls.INPUT_TYPES()["required"]["master_duration"][0],
["30 seconds"],
)
self.assertEqual(
contracts.DiffusionGemmaAdvertisementWorkflowControls.INPUT_TYPES()["required"]["performance_mode"][0],
["Natural / audio-led sync", "Dance / music sync"],
)
self.assertEqual(
contracts.DiffusionGemmaAdvertisementWorkflowControls.RETURN_TYPES[2],
["Dance / music sync", "Lyrics + lip sync", "Natural / audio-led sync"],
)
self.assertEqual(
contracts.DiffusionGemmaAdvertisementWorkflowControls.RETURN_TYPES[7:15],
(
["t2va", "ref2va"],
["auto", "1", "2", "3", "4", "5", "6", "7", "8", "9", "10", "11", "12", "custom"],
"INT",
["auto_scene_audio", "explicit_sound_design", "visual_only"],
["auto", "required", "off"],
"FLOAT",
"STRING",
"FLOAT",
),
)
self.assertEqual(
contracts.DiffusionGemmaAdvertisementWorkflowControls.RETURN_TYPES[15],
"STRING",
)
def test_exact_copy_claim_governance_timing_and_delivery_are_locked(self):
result = build_campaign()
payload = json.loads(result[0])
exact_copy = json.loads(result[2])
self.assertTrue(result[-1])
self.assertEqual(payload["schema"], contracts.CAMPAIGN_SCHEMA)
self.assertEqual(payload["timeline"]["end_card"]["start_seconds"], 27.0)
self.assertEqual(payload["deliverables"]["render_status"], "requested_not_rendered")
self.assertEqual(exact_copy["approved_claims"], ["Zero proof"])
self.assertNotIn("Improves focus", result[1])
self.assertEqual(
payload["exact_copy_sha256"],
hashlib.sha256(result[2].encode("utf-8")).hexdigest(),
)
def test_exact_copy_is_normalized_once_without_changing_brand_case(self):
result = build_campaign(
brand_name="eBay Labs",
headline="Stay curious",
call_to_action="Try it",
)
payload = json.loads(result[0])
exact = json.loads(result[2])
self.assertEqual(payload["brand"]["brand_name"], "eBay Labs")
self.assertEqual(exact["headline"], "Stay curious")
self.assertEqual(exact["call_to_action"], "Try it")
def test_approved_claim_without_evidence_fails_closed(self):
with self.assertRaisesRegex(ValueError, "no substantiation"):
build_campaign(
claims_json='[{"text":"Clinically proven energy","status":"approved","evidence":""}]'
)
def test_end_card_and_cutdowns_must_fit_inside_master(self):
with self.assertRaisesRegex(ValueError, "24 fps"):
build_campaign(production_duration_seconds=30.1)
with self.assertRaisesRegex(ValueError, "must be from"):
build_campaign(end_card_duration_seconds=0.0)
with self.assertRaisesRegex(ValueError, "shorter than the production"):
build_campaign(
production_duration_seconds=10.0,
end_card_duration_seconds=10.0,
deliverables_json='{"primary":"9:16","adaptations":[],"cutdowns_seconds":[6]}',
)
with self.assertRaisesRegex(ValueError, "cutdown must be shorter"):
build_campaign(
deliverables_json='{"primary":"9:16","adaptations":[],"cutdowns_seconds":[30]}'
)
class AdvertisementReferenceContractTests(unittest.TestCase):
def test_saved_ui_exposes_only_the_reference_topology_the_shipped_graph_supports(self):
options = contracts.DiffusionGemmaAdvertisementReferenceContract.INPUT_TYPES()["required"]["slot_policy"][0]
self.assertEqual(options, ["2 performer + 1 product + relay"])
def test_default_uses_two_performer_assets_product_three_and_relay_four(self):
result = contracts.DiffusionGemmaAdvertisementReferenceContract().build(
slot_policy=contracts.REFERENCE_SLOT_POLICIES[0],
performer_description="A clearly adult performer in a cobalt jacket.",
product_description="A slim warm-bone can with a citron stripe.",
product_retention_attributes="slim geometry, warm-bone body, citron stripe, cobalt wordmark layout",
product_reference_kind="product contact sheet",
package_copy_json='["AFTERS","YUZU + WHITE PEACH"]',
)
payload = json.loads(result[0])
self.assertEqual(result[2:6], (2, 2, "<Picture 3>", "<Picture 4>"))
self.assertEqual(
[slot["role"] for slot in payload["picture_slots"]],
["performer_primary", "performer_supplemental", "product_package"],
)
self.assertEqual(payload["subjects"][1]["subject_tag"], "<Subject 2>")
self.assertTrue(payload["relay"]["enabled"])
self.assertIn("continuity_relay", payload["relay"]["manifest_reservation"])
self.assertIn("never performer identity or product", payload["relay"]["manifest_reservation"])
self.assertIn("panel multiplicity", payload["picture_slots"][2]["non_transfer"])
def test_semantic_parser_uses_annotations_not_wording_passwords(self):
first = (
"<Picture 1>: [dg:identity,appearance] "
"[ad:role=performer_primary;subject=subject_1;retention=fully_preserved] same adult woman only"
)
second = (
"<Picture 1>: [dg:identity,appearance] "
"[ad:role=performer_primary;subject=subject_1;retention=fully_preserved] hero portrait with completely different prose"
)
keys = ("tag", "role", "subject", "retention")
parsed_first = contracts.parse_advertisement_reference_manifest(first)[0]
parsed_second = contracts.parse_advertisement_reference_manifest(second)[0]
self.assertEqual(
{key: parsed_first[key] for key in keys},
{key: parsed_second[key] for key in keys},
)
with self.assertRaisesRegex(ValueError, "exactly one \[ad"):
contracts.parse_advertisement_reference_manifest(
"<Picture 1>: same subject identity with no typed annotation"
)
def test_reference_asset_prep_keeps_three_roles_separate_and_hash_locked(self):
image = torch.linspace(0.0, 1.0, steps=96 * 64 * 3).reshape(1, 96, 64, 3)
performer_sheet = torch.flip(image, dims=(2,))
product_sheet = 1.0 - image
result = contracts.DiffusionGemmaAdvertisementReferenceAssetPrep().prepare(
image,
performer_sheet,
product_sheet,
256,
256,
0.5,
0.6,
0.25,
)
report = json.loads(result[7])
self.assertEqual(result[6].shape, (3, 256, 256, 3))
self.assertEqual(len({result[3], result[4], result[5]}), 3)
self.assertEqual(
[asset["role"] for asset in report["assets"]],
["performer_hero", "performer_sheet", "product_sheet"],
)
self.assertTrue(all(asset["prepared_role_sha256"] for asset in report["assets"]))
self.assertTrue(report["product_budget"]["independent_from_performer_budget"])
self.assertFalse(report["identity_product_combined_sheet"])
repeat = contracts.DiffusionGemmaAdvertisementReferenceAssetPrep().prepare(
image, performer_sheet, product_sheet, 256, 256, 0.5, 0.6, 0.25
)
self.assertEqual(result[3:6], repeat[3:6])
with self.assertRaisesRegex(ValueError, "three distinct source images"):
contracts.DiffusionGemmaAdvertisementReferenceAssetPrep().prepare(
image, image, image, 256, 256, 0.5, 0.6, 0.25
)
class AdvertisementSoundtrackContractTests(unittest.TestCase):
def test_saved_ui_hides_voice_over_until_the_graph_has_a_vo_input(self):
options = contracts.DiffusionGemmaAdvertisementSoundtrackContract.INPUT_TYPES()["required"]["voice_over_policy"][0]
self.assertEqual(options, ["None"])
def test_instrumental_music3_caption_is_structured_and_has_headroom(self):
result = build_soundtrack()
payload = json.loads(result[0])
self.assertEqual(
result[2],
"[Intro]\n\n[Instrumental]\n\n[Bridge]\n\n[Instrumental]\n\n[Outro]",
)
self.assertEqual(result[3], "instrumental")
self.assertEqual(result[5], 35.0)
self.assertIn("Global Metadata:", result[1])
self.assertIn("Vocal Details:", result[1])
self.assertIn("Arrangement:", result[1])
self.assertIn("Selectable advertisement master: 30 seconds", result[1])
self.assertEqual(payload["music3"]["generation_duration_seconds"], 35.0)
self.assertFalse(payload["voice_over"]["included_in_motion_guide"])
def test_vocal_and_auto_modes_emit_tagged_lyrics(self):
vocal = build_soundtrack(
content_mode="Vocal",
lyrics="Plans changed, perfect.",
language="en",
voice_over_policy="None",
)
self.assertTrue(vocal[2].startswith("[Verse]\n"))
self.assertEqual(vocal[3], "vocal")
auto = build_soundtrack(
content_mode="Auto",
lyrics="[Chorus]\nStay for the after.",
language="en",
voice_over_policy="None",
)
self.assertEqual(auto[3], "vocal")
with self.assertRaisesRegex(ValueError, "cannot contain sung lyrics"):
build_soundtrack(content_mode="Instrumental", lyrics="Sing this line")
def test_music3_adapter_preserves_governed_prompt_and_duration(self):
soundtrack = build_soundtrack()
adapted = contracts.DiffusionGemmaAdvertisementMusic3PromptAdapter().adapt(
soundtrack[0]
)
self.assertEqual(adapted[:5], (soundtrack[1], soundtrack[2], "instrumental", 105.0, 35.0))
self.assertEqual(adapted[5], 105)
self.assertEqual(contracts.DiffusionGemmaAdvertisementMusic3PromptAdapter.RETURN_TYPES[5], "INT")
unspecified = contracts.DiffusionGemmaAdvertisementMusic3PromptAdapter().adapt(
build_soundtrack(bpm=0.0)[0]
)
self.assertEqual(unspecified[3], 0.0)
self.assertEqual(unspecified[5], 120)
class LegacyArtifactImmutabilityTests(unittest.TestCase):
EXPECTED = {
"audio_production_nodes.py": "3f0d1f5b3f5e002d6944a6eb914c6a46c0fe4679828f3546fd672a8b1a700e21",
"production_planning_nodes.py": "7ae98e808062aab16d8a470c5c8143c9881333354167b79a4d88a3110ab6e1e8",
"nodes.py": "fecd6ba7e28e1f286171e348511b24dc3a7537b296cf568cf8fca540e5b23ed0",
"examples/15_minimax_h3_ref2va_music_video_v6.json": "594f6aef94531f87b74659e1e8004cccdff00716d0cfe263cee6bf2149545c83",
}
def test_advertisement_extension_did_not_mutate_known_good_legacy_files(self):
for relative, expected in self.EXPECTED.items():
with self.subTest(relative=relative):
actual = hashlib.sha256((ROOT / relative).read_bytes()).hexdigest()
self.assertEqual(actual, expected)
if __name__ == "__main__":
unittest.main()
+373
View File
@@ -0,0 +1,373 @@
from __future__ import annotations
import hashlib
import json
import sys
import unittest
from pathlib import Path
import torch
from jsonschema import Draft202012Validator
ROOT = Path(__file__).resolve().parents[1]
TESTS = Path(__file__).resolve().parent
for path in (ROOT, TESTS):
if str(path) not in sys.path:
sys.path.insert(0, str(path))
import advertising_finishing_nodes as finishing
from audio_production_nodes import waveform_sha256
from test_advertising_planning_nodes import build_plan
def canonical(value: object) -> str:
return json.dumps(value, ensure_ascii=False, sort_keys=True, separators=(",", ":"))
def revised_contract(contract_json: str, adaptations: list[str]) -> str:
contract = json.loads(contract_json)
contract["requested_deliverables"]["adaptations"] = adaptations
contract.pop("campaign_id", None)
contract.pop("contract_sha256", None)
campaign_id = hashlib.sha256(canonical(contract).encode("utf-8")).hexdigest()
contract["campaign_id"] = campaign_id
contract["contract_sha256"] = hashlib.sha256(canonical(contract).encode("utf-8")).hexdigest()
return canonical(contract)
def revised_exact_copy(contract_json: str, **changes: object) -> str:
contract = json.loads(contract_json)
contract["mandatory_copy"].update(changes)
if "call_to_action" in changes:
contract["call_to_action"] = str(changes["call_to_action"])
contract.pop("campaign_id", None)
contract.pop("contract_sha256", None)
campaign_id = hashlib.sha256(canonical(contract).encode("utf-8")).hexdigest()
contract["campaign_id"] = campaign_id
contract["contract_sha256"] = hashlib.sha256(canonical(contract).encode("utf-8")).hexdigest()
return canonical(contract)
def revised_cutdowns(contract_json: str, cutdowns: list[int]) -> str:
contract = json.loads(contract_json)
contract["requested_deliverables"]["cutdowns_seconds"] = cutdowns
contract.pop("campaign_id", None)
contract.pop("contract_sha256", None)
campaign_id = hashlib.sha256(canonical(contract).encode("utf-8")).hexdigest()
contract["campaign_id"] = campaign_id
contract["contract_sha256"] = hashlib.sha256(canonical(contract).encode("utf-8")).hexdigest()
return canonical(contract)
class AdvertisementFinishingTests(unittest.TestCase):
sample_rate = 240
def setUp(self):
self.audio = {
"waveform": torch.linspace(-0.5, 0.5, 30 * self.sample_rate, dtype=torch.float32)
.view(1, 1, -1)
.repeat(1, 2, 1),
"sample_rate": self.sample_rate,
}
audio_hash = waveform_sha256(self.audio)
self.contract_json, plan_result = build_plan(audio_hash=audio_hash)
self.plan_json = plan_result[0]
self.plan = json.loads(self.plan_json)
lane_images = []
for lane_index, lane in enumerate(self.plan["lanes"], start=1):
generated = int(lane["generated_h3_frames"])
lane_images.append(torch.full((generated, 112, 64, 3), lane_index / 4.0))
assembled = finishing.AdvertisementMasterAssembler().assemble(
self.contract_json,
self.plan_json,
self.audio,
lane_1_images=lane_images[0],
lane_2_images=lane_images[1],
)
self.assembled = assembled[0]
self.assembly_report = assembled[2]
def _finish(self):
end_card = finishing.EndCardRenderer().render(
self.contract_json,
64,
112,
"#10141f",
"#ffffff",
"#76f7c5",
)
finished = finishing.AdvertisementMasterFinisher().finish(
self.assembled,
self.audio,
end_card[0],
end_card[1],
self.contract_json,
self.assembly_report,
)
return end_card, finished
def test_master_assembly_verifies_identity_audio_and_exact_frames(self):
report = json.loads(self.assembly_report)
self.assertEqual(self.assembled.shape[0], 30 * 24)
self.assertEqual(report["exact_master_frames"], 720)
self.assertEqual([row["retained_frames"] for row in report["lanes"]], [360, 360])
self.assertTrue(all(row["discarded_padding_frames"] >= 0 for row in report["lanes"]))
self.assertTrue(report["pixel_aspect_evidence"]["matched"])
mismatched_plan = json.loads(self.plan_json)
mismatched_plan["campaign_id"] = "f" * 64
with self.assertRaisesRegex(ValueError, "plan_sha256"):
finishing.AdvertisementMasterAssembler().assemble(
self.contract_json,
json.dumps(mismatched_plan),
self.audio,
lane_1_images=torch.zeros((362, 64, 64, 3)),
lane_2_images=torch.zeros((362, 64, 64, 3)),
)
def test_master_assembly_rejects_pixels_that_disagree_with_contract_aspect(self):
lanes = []
for lane in self.plan["lanes"]:
lanes.append(torch.zeros((int(lane["generated_h3_frames"]), 64, 64, 3)))
with self.assertRaisesRegex(ValueError, "does not match the contracted 9:16"):
finishing.AdvertisementMasterAssembler().assemble(
self.contract_json,
self.plan_json,
self.audio,
lane_1_images=lanes[0],
lane_2_images=lanes[1],
)
def test_end_card_is_exactly_72_frames_and_finisher_replaces_tail(self):
end_card, finished = self._finish()
report = json.loads(end_card[1])
finish_report = json.loads(finished[2])
self.assertEqual(end_card[0].shape[0], 72)
self.assertEqual(report["frame_count"], 72)
self.assertEqual(report["rendered_copy"]["headline"], "Open something brighter")
self.assertEqual(report["rendered_copy"]["variant_name"], "Yuzu Mint")
self.assertEqual(report["rendered_copy"]["call_to_action"], "Find your flavor")
self.assertEqual(finished[0].shape[0], 720)
self.assertEqual(finish_report["end_card_replaced_tail_frames"], 72)
self.assertFalse(finish_report["duration_changed"])
self.assertTrue(torch.equal(finished[0][-72:], end_card[0]))
def test_finisher_rejects_stale_reports_and_mismatched_pixels(self):
end_card = finishing.EndCardRenderer().render(
self.contract_json, 64, 112, "#10141f", "#ffffff", "#76f7c5"
)
stale = json.loads(self.assembly_report)
stale["exact_master_frames"] -= 1
with self.assertRaisesRegex(ValueError, "report_sha256"):
finishing.AdvertisementMasterFinisher().finish(
self.assembled,
self.audio,
end_card[0],
end_card[1],
self.contract_json,
json.dumps(stale),
)
changed = self.assembled.clone()
changed[0, 0, 0, 0] = 1.0 - changed[0, 0, 0, 0]
with self.assertRaisesRegex(ValueError, "pixels do not match"):
finishing.AdvertisementMasterFinisher().finish(
changed,
self.audio,
end_card[0],
end_card[1],
self.contract_json,
self.assembly_report,
)
def test_end_card_renders_all_nonempty_exact_copy_in_a_recorded_hierarchy(self):
contract = revised_exact_copy(
self.contract_json,
offer_text="Two cans for the first tasting",
price_text="$5",
legal_line="Limited availability. See site for details.",
approved_claims=["Contains zero grams of sugar."],
)
node = finishing.EndCardRenderer()
first = node.render(contract, 96, 160, "#10141f", "#ffffff", "#76f7c5")
second = node.render(contract, 96, 160, "#10141f", "#ffffff", "#76f7c5")
report = json.loads(first[1])
rendered = report["rendered_copy"]
self.assertEqual(first[0].shape, (72, 160, 96, 3))
self.assertTrue(torch.equal(first[0], second[0]))
self.assertEqual(first[1], second[1])
self.assertEqual(rendered["headline"], "Open something brighter")
self.assertEqual(rendered["offer_text"], "Two cans for the first tasting")
self.assertEqual(rendered["price_text"], "$5")
self.assertEqual(rendered["legal_line"], "Limited availability. See site for details.")
self.assertEqual(rendered["approved_claims"], ["Contains zero grams of sugar."])
hierarchy_fields = [row["field"] for row in report["copy_hierarchy"]]
self.assertEqual(
hierarchy_fields,
[
"brand_name",
"product_name+variant_name",
"headline",
"offer_text",
"price_text",
"call_to_action",
"approved_claims",
"legal_line",
],
)
self.assertEqual(report["copy_fit_policy"], "deterministic_scale_and_pixel_wrap_fail_closed_no_clipping")
def test_end_card_preserves_authored_brand_case_in_pixels(self):
mixed = revised_exact_copy(self.contract_json)
mixed_payload = json.loads(mixed)
mixed_payload["brand_name"] = "eBay"
mixed_payload["mandatory_copy"]["brand_name"] = "eBay"
mixed_payload.pop("campaign_id", None)
mixed_payload.pop("contract_sha256", None)
mixed_payload["campaign_id"] = hashlib.sha256(canonical(mixed_payload).encode("utf-8")).hexdigest()
mixed_payload["contract_sha256"] = hashlib.sha256(canonical(mixed_payload).encode("utf-8")).hexdigest()
upper_payload = json.loads(canonical(mixed_payload))
upper_payload["brand_name"] = "EBAY"
upper_payload["mandatory_copy"]["brand_name"] = "EBAY"
upper_payload.pop("campaign_id", None)
upper_payload.pop("contract_sha256", None)
upper_payload["campaign_id"] = hashlib.sha256(canonical(upper_payload).encode("utf-8")).hexdigest()
upper_payload["contract_sha256"] = hashlib.sha256(canonical(upper_payload).encode("utf-8")).hexdigest()
node = finishing.EndCardRenderer()
mixed_images = node.render(canonical(mixed_payload), 96, 160, "#10141f", "#ffffff", "#76f7c5")[0]
upper_images = node.render(canonical(upper_payload), 96, 160, "#10141f", "#ffffff", "#76f7c5")[0]
self.assertFalse(torch.equal(mixed_images, upper_images))
def test_end_card_copy_overflow_fails_closed_without_changing_timing(self):
contract = revised_exact_copy(self.contract_json, legal_line="LEGAL" * 300)
with self.assertRaisesRegex(ValueError, "does not fit"):
finishing.EndCardRenderer().render(
contract, 64, 64, "#10141f", "#ffffff", "#76f7c5"
)
def test_end_card_rejects_oversized_tensor_before_allocation(self):
with self.assertRaisesRegex(ValueError, "2 GiB allocation ceiling"):
finishing.EndCardRenderer().render(
self.contract_json,
4096,
4096,
"#10141f",
"#ffffff",
"#76f7c5",
)
def test_real_15_and_6_second_cutdowns_slice_exact_frames_and_audio(self):
_end_card, finished = self._finish()
result = finishing.AdvertisementCutdownRenderer().render(
finished[0],
finished[1],
self.contract_json,
0.0,
24.0,
)
manifest = json.loads(result[4])
self.assertEqual(result[0].shape[0], 15 * 24)
self.assertEqual(result[1]["waveform"].shape[-1], 15 * self.sample_rate)
self.assertEqual(result[2].shape[0], 6 * 24)
self.assertEqual(result[3]["waveform"].shape[-1], 6 * self.sample_rate)
self.assertEqual([row["status"] for row in manifest["cutdowns"]], ["rendered", "rendered"])
self.assertTrue(torch.equal(result[0], finished[0][: 15 * 24]))
self.assertTrue(torch.equal(result[2], finished[0][24 * 24 : 30 * 24]))
self.assertTrue(torch.equal(result[3]["waveform"], self.audio["waveform"][..., 24 * self.sample_rate :]))
schema = json.loads((ROOT / "schemas" / "advertisement_delivery_manifest.schema.json").read_text(encoding="utf-8"))
Draft202012Validator(schema).validate(manifest)
with self.assertRaisesRegex(ValueError, "requested cutdowns"):
finishing.AdvertisementCutdownRenderer().render(
finished[0],
finished[1],
revised_cutdowns(self.contract_json, [10]),
15.0,
24.0,
)
def test_impossible_and_unrendered_adaptations_are_never_claimed_rendered(self):
_end_card, finished = self._finish()
contract = revised_contract(self.contract_json, ["9:16", "1:1", "cinema-circle"])
adaptation_result = finishing.AdvertisementAdaptationStatus().evaluate(contract)
self.assertFalse(adaptation_result[2])
result = finishing.AdvertisementCutdownRenderer().render(
finished[0], finished[1], contract, 0.0, 24.0, adaptation_result[0]
)
statuses = {row["aspect_ratio"]: row["status"] for row in json.loads(result[4])["adaptations"]}
self.assertEqual(statuses["9:16"], "not_applicable_same_as_master")
self.assertEqual(statuses["1:1"], "planned_not_rendered")
self.assertEqual(statuses["cinema-circle"], "impossible_invalid_aspect")
self.assertNotIn("rendered", statuses.values())
schema = json.loads((ROOT / "schemas" / "advertisement_adaptation_status.schema.json").read_text(encoding="utf-8"))
Draft202012Validator(schema).validate(json.loads(adaptation_result[0]))
def test_media_qa_gate_distinguishes_pass_fail_and_not_measured(self):
_end_card, finished = self._finish()
delivery = finishing.AdvertisementCutdownRenderer().render(
finished[0], finished[1], self.contract_json, 0.0, 24.0
)[4]
gate = finishing.MediaQAGate()
required = '["product_identity","audio_sync"]'
unknown = gate.evaluate(self.contract_json, delivery, '{"product_identity":"pass"}', required)
self.assertEqual(unknown[1], "not_measured")
self.assertFalse(unknown[3])
failed = gate.evaluate(
self.contract_json,
delivery,
'{"product_identity":"pass","audio_sync":{"status":"fail","evidence":"visible drift"}}',
required,
)
self.assertEqual(failed[1], "fail")
self.assertFalse(failed[3])
passed = gate.evaluate(
self.contract_json,
delivery,
json.dumps(
{
name: {"status": "pass", "evidence": f"measured {name}"}
for name in finishing.MANDATORY_MEDIA_QA_CHECKS
}
),
required,
)
report = json.loads(passed[0])
self.assertEqual(passed[1], "pass")
self.assertTrue(passed[3])
self.assertEqual(report["host_required_checks"], list(finishing.MANDATORY_MEDIA_QA_CHECKS))
schema = json.loads((ROOT / "schemas" / "advertisement_media_qa.schema.json").read_text(encoding="utf-8"))
Draft202012Validator(schema).validate(report)
omitted = gate.evaluate(
self.contract_json,
delivery,
'{"technical_integrity":{"status":"pass","evidence":"exact assembly"}}',
'["technical_integrity"]',
)
self.assertEqual(omitted[1], "not_measured")
self.assertFalse(omitted[3])
self.assertEqual(
{row["check"] for row in json.loads(omitted[0])["checks"]},
set(finishing.MANDATORY_MEDIA_QA_CHECKS),
)
with self.assertRaisesRegex(ValueError, "duplicate"):
gate.evaluate(
self.contract_json,
delivery,
"{}",
'["technical_integrity","technical_integrity"]',
)
bare_pass = gate.evaluate(
self.contract_json,
delivery,
'{"product_identity":"pass","audio_sync":{"status":"pass","evidence":"measured"}}',
required,
)
bare_report = json.loads(bare_pass[0])
self.assertEqual(bare_pass[1], "not_measured")
self.assertEqual(bare_report["checks"][0]["status"], "not_measured")
self.assertIn("without evidence", bare_report["checks"][0]["evidence"])
if __name__ == "__main__":
unittest.main()
+512
View File
@@ -0,0 +1,512 @@
from __future__ import annotations
import hashlib
import inspect
import json
import sys
import tempfile
import unittest
from pathlib import Path
import torch
from jsonschema import Draft202012Validator
ROOT = Path(__file__).resolve().parents[1]
if str(ROOT) not in sys.path:
sys.path.insert(0, str(ROOT))
import advertising_contract_nodes as creative
import advertising_planning_nodes as advertising
import nodes as director_core
import production_planning_nodes as legacy
EIGHT_SHOT_PROMPT = """subject_definitions:
<Subject 1>: the same performer jointly defined by <Picture 1> and <Picture 2>.
<Subject 2>: the same beverage package defined by <Picture 3>.
summary:
A colorful beverage advertisement follows one performer and one governed product package.
retention_analysis:
<Subject 1>: fully_preserved - face, hair, body, and wardrobe remain the same.
<Subject 2>: fully_preserved - package geometry, material, colors, and label remain the same.
<Audio 1>: reference - exact campaign soundtrack and timing authority.
detailed_description:
Bright live-action commercial photography with readable stable framing.
[Shot 1] A wide opening introduces the performer carrying the beverage.
[Shot 2] At 00:03.000, a clean cut shows a medium walking view.
[Shot 3] At 00:06.000, a close product presentation fills the foreground.
[Shot 4] At 00:09.000, a stable group composition responds to the beat.
[Shot 5] At 00:15.000, a clean cut opens the second campaign movement.
[Shot 6] At 00:18.000, the performer opens and enjoys the beverage.
[Shot 7] At 00:21.000, a product beauty shot holds package geometry clearly.
[Shot 8] At 00:24.000, the campaign resolves toward the finishing card.
overall_soundscape:
The supplied <Audio 1> remains the exact soundtrack authority.
non_diegetic_music:
Use the supplied <Audio 1> without inventing a replacement song."""
def build_upstream(audio_hash: str = "a" * 64) -> tuple[str, str, str, str]:
campaign = creative.DiffusionGemmaAdvertisementCampaignContract().build(
"GLOWBLOOM",
"Sparkling Botanical Water",
"Yuzu Mint",
"Trend-aware adults in their twenties",
"Drive trial and brand recognition",
"Open something brighter",
"Find your flavor",
"[]",
30.0,
"9:16",
3.0,
'{"primary":"9:16","adaptations":["1:1","16:9"],"cutdowns_seconds":[15,6]}',
campaign_name="GLOWBLOOM Brighter",
)[0]
references = creative.DiffusionGemmaAdvertisementReferenceContract().build(
creative.REFERENCE_SLOT_POLICIES[0],
"One adult performer in a cobalt jacket with consistent face, hair, body, and wardrobe.",
"One slim GLOWBLOOM Yuzu Mint can with fixed geometry, green-yellow palette, and printed label.",
"geometry, proportions, material, label placement, green-yellow color system",
"product contact sheet",
)[0]
soundtrack = creative.DiffusionGemmaAdvertisementSoundtrackContract().build(
"Instrumental",
"bright indie electronic pop with a tactile human groove",
"optimistic, stylish, lightly playful",
"dry drums, bass guitar, hand percussion, glassy synth accents",
30.0,
108.0,
"4",
"unknown",
"",
"None",
)[0]
project = legacy.DiffusionGemmaProjectMasterContract().build(
"Create the contracted GLOWBLOOM advertisement.",
30.0,
"9:16",
audio_hash,
"",
0.0,
30.0,
"MiniMax H3 Ref2VA",
'{"primary":"9:16","adaptations":["1:1","16:9"],"cutdowns_seconds":[15,6]}',
15.0,
)[0]
return campaign, references, soundtrack, project
def build_contract(audio_hash: str = "a" * 64, boundary: str = "relay_continuity") -> tuple:
campaign, references, soundtrack, project = build_upstream(audio_hash)
return advertising.AdvertisementMasterContract().build(
campaign,
references,
soundtrack,
project,
"1" * 64,
"2" * 64,
"3" * 64,
audio_hash,
8,
boundary,
)
def build_plan(boundary: str = "relay_continuity", audio_hash: str = "a" * 64) -> tuple[str, tuple]:
contract = build_contract(audio_hash=audio_hash, boundary=boundary)[0]
defaults = advertising.AdvertisementPlanningDefaults().build(
contract,
"The performer carries, opens, presents, and drinks the can while its package remains recognizable.",
"Campaign default",
)
result = advertising.AdvertisementMultiShotPlanner().plan(
contract,
EIGHT_SHOT_PROMPT,
defaults[0],
defaults[1],
"Natural / audio-led sync",
)
return contract, result
class AdvertisementPlanningTests(unittest.TestCase):
@staticmethod
def _director_host_inputs(*, parse_valid: bool, blocked: bool = False):
report = {
"schema": "dg-grounding-report/1",
"analysis_status": "not_run",
"decision": "block" if blocked else "disabled",
"would_block": blocked,
"grounding_guard_would_block": blocked,
}
metadata = {
"json_parse_valid": parse_valid,
"grounding_guard": report,
"director_runtime": {"schema": "dg-director-runtime/1", "sentinel": "preserve"},
"director_cache": {"schema": "dg-director-cache/2", "sentinel": "preserve"},
}
if not parse_valid:
metadata.update(
{
"plain_text_salvage": True,
"salvage_warning": "invalid JSON was salvaged",
"json_parse_warning": "model output was invalid JSON",
"template_used_for_missing_fields": True,
"used_template_fallback": False,
}
)
packet = {
"ltx_prompt": "",
"ideogram_prompt": "",
"metadata": metadata,
"minimax_h3_prompt": "HOST_VALID" if parse_valid else "HOST_FALLBACK",
"scene_segments": [],
}
return (
json.dumps(packet),
json.dumps(metadata),
"not_run",
json.dumps(report),
)
def test_director_transport_repair_preserves_host_governance_and_rejects_duplicate_keys(self):
raw = r'''<|final_json|>
{"ltx_prompt":"","ideogram_prompt":"","metadata":{},"minimax_h3_prompt":"subject_definitions:\n<Subject 1> An adult performer with consistent face hair wardrobe and build.\n<Subject 2> A slim orange beverage can with fixed geometry color label and material.\n\nsummary:\n[keyframe completion] A governed advertisement.\n\nretention_analysis:\n<Subject 1>: fully_preserved - performer.\n<Subject 2>: fully_preserved - package.\n\ndetail_description:\nBright commercial photography.\n[Shot 1] The performer presents the package.\n\noverall_soundscape:\nA can opens.\n\nnon_diegetic_music:\An upbeat score.","scene_segments":[]}
<|end_final_json|>'''
final_json, metadata_json, status, report_json = self._director_host_inputs(parse_valid=False)
references = build_upstream()[1]
result = advertising.AdvertisementDirectorPacketRepair().repair(
final_json,
raw,
metadata_json,
status,
report_json,
references,
)
payload = json.loads(result[0])
report = json.loads(result[1])
self.assertIn(r"non_diegetic_music:\An upbeat score.", payload["minimax_h3_prompt"])
self.assertIn("Reference sources: <Picture 1>, <Picture 2>.", payload["minimax_h3_prompt"])
self.assertIn("Reference sources: <Picture 3>.", payload["minimax_h3_prompt"])
self.assertEqual(report["invalid_escape_repair_count"], 1)
self.assertFalse(report["creative_content_synthesized"])
self.assertEqual(report["route"], "raw_transport_recovery")
self.assertEqual(payload["metadata"]["director_runtime"]["sentinel"], "preserve")
self.assertEqual(payload["metadata"]["director_cache"]["sentinel"], "preserve")
self.assertEqual(payload["metadata"]["grounding_guard"], json.loads(report_json))
self.assertTrue(payload["metadata"]["json_parse_valid"])
self.assertNotIn("plain_text_salvage", payload["metadata"])
self.assertTrue(result[-1])
manifest = json.loads(references)["reference_manifest"]
normalized = director_core._normalize_minimax_h3_prompt(
payload["minimax_h3_prompt"],
"ref2va",
)
normalized, _repairs = director_core._repair_minimax_h3_ref2va_contract_delimiters(
normalized
)
normalized = director_core._repair_minimax_h3_ref2va_structure(
normalized,
"Create the governed advertisement.",
manifest,
30.0,
)
sections = director_core._minimax_h3_ref_sections(normalized)
self.assertIsNotNone(sections)
self.assertEqual(director_core._minimax_h3_ref2va_subject_count(normalized), 2)
self.assertNotIn("fully_preserved - (", sections["retention_analysis"])
self.assertIn("fully_preserved - performer.", sections["retention_analysis"])
self.assertIn("fully_preserved - package.", sections["retention_analysis"])
duplicate = r'<|final_json|>{"minimax_h3_prompt":"one\A","minimax_h3_prompt":"two"}<|end_final_json|>'
with self.assertRaisesRegex(ValueError, "duplicate JSON key"):
advertising.AdvertisementDirectorPacketRepair().repair(
final_json,
duplicate,
metadata_json,
status,
report_json,
references,
)
for nonfinite in ("1e999", "-1e999", "NaN", "Infinity"):
with self.assertRaisesRegex(ValueError, "non-finite"):
advertising._strict_director_packet_json(f'{{"value":{nonfinite}}}')
stale_references = json.loads(references)
stale_references["picture_slots"][0]["subject_tag"] = "<Subject 2>"
with self.assertRaisesRegex(ValueError, "hashed manifest authority"):
advertising.AdvertisementDirectorPacketRepair().repair(
final_json,
raw,
metadata_json,
status,
report_json,
json.dumps(stale_references),
)
def test_director_packet_adapter_uses_host_valid_packet_and_preserves_grounding_block(self):
references = build_upstream()[1]
valid_final, valid_metadata, status, valid_report = self._director_host_inputs(parse_valid=True)
passthrough = advertising.AdvertisementDirectorPacketRepair().repair(
valid_final,
"malicious raw text that must not replace the host packet",
valid_metadata,
status,
valid_report,
references,
)
passthrough_packet = json.loads(passthrough[0])
self.assertEqual(passthrough_packet["minimax_h3_prompt"], "HOST_VALID")
self.assertEqual(json.loads(passthrough[1])["route"], "host_final_json_passthrough")
blocked_final, blocked_metadata, blocked_status, blocked_report = self._director_host_inputs(
parse_valid=False,
blocked=True,
)
blocked = advertising.AdvertisementDirectorPacketRepair().repair(
blocked_final,
r'<|final_json|>{"minimax_h3_prompt":"apparently valid\A"}<|end_final_json|>',
blocked_metadata,
blocked_status,
blocked_report,
references,
)
blocked_packet = json.loads(blocked[0])
self.assertEqual(blocked_packet["minimax_h3_prompt"], "HOST_FALLBACK")
self.assertFalse(blocked[-1])
self.assertFalse(blocked_packet["metadata"]["json_parse_valid"])
self.assertTrue(blocked_packet["metadata"]["grounding_guard"]["would_block"])
self.assertEqual(json.loads(blocked[1])["route"], "host_grounding_block_preserved")
tampered_metadata = json.loads(valid_metadata)
tampered_metadata["director_runtime"]["sentinel"] = "changed"
with self.assertRaisesRegex(ValueError, "does not match"):
advertising.AdvertisementDirectorPacketRepair().repair(
valid_final,
"unused",
json.dumps(tampered_metadata),
status,
valid_report,
references,
)
for parse_valid, ambiguous_value in (
(True, None),
(True, 1),
(False, None),
(False, 0),
):
base_final, base_metadata, base_status, base_report = self._director_host_inputs(
parse_valid=parse_valid
)
packet = json.loads(base_final)
metadata = json.loads(base_metadata)
metadata["model_output_json_parse_valid"] = ambiguous_value
packet["metadata"] = metadata
with self.assertRaisesRegex(ValueError, "JSON boolean"):
advertising.AdvertisementDirectorPacketRepair().repair(
json.dumps(packet),
"unused",
json.dumps(metadata),
base_status,
base_report,
references,
)
for flag_name in ("plain_text_salvage", "used_template_fallback"):
base_final, base_metadata, base_status, base_report = self._director_host_inputs(
parse_valid=False
)
packet = json.loads(base_final)
metadata = json.loads(base_metadata)
metadata[flag_name] = 1
packet["metadata"] = metadata
with self.assertRaisesRegex(ValueError, "JSON boolean"):
advertising.AdvertisementDirectorPacketRepair().repair(
json.dumps(packet),
"unused",
json.dumps(metadata),
base_status,
base_report,
references,
)
strict_final, strict_metadata, strict_status, strict_report = self._director_host_inputs(
parse_valid=False,
blocked=True,
)
numeric_metadata = json.loads(strict_metadata)
numeric_report = json.loads(strict_report)
numeric_metadata["grounding_guard"]["would_block"] = 1
numeric_metadata["grounding_guard"]["grounding_guard_would_block"] = 1
numeric_report["would_block"] = 1
numeric_report["grounding_guard_would_block"] = 1
with self.assertRaisesRegex(ValueError, "does not match"):
advertising.AdvertisementDirectorPacketRepair().repair(
strict_final,
"unused",
json.dumps(numeric_metadata),
strict_status,
json.dumps(numeric_report),
references,
)
numeric_packet = json.loads(strict_final)
numeric_packet["metadata"] = numeric_metadata
with self.assertRaisesRegex(ValueError, "JSON booleans"):
advertising.AdvertisementDirectorPacketRepair().repair(
json.dumps(numeric_packet),
"unused",
json.dumps(numeric_metadata),
strict_status,
json.dumps(numeric_report),
references,
)
def test_saved_ui_exposes_only_relay_seams_supported_by_the_shipped_graph(self):
master_options = advertising.AdvertisementMasterContract.INPUT_TYPES()["required"]["boundary_mode"][0]
seam_options = advertising.AdvertisementPlanningDefaults.INPUT_TYPES()["required"]["seam_style"][0]
self.assertEqual(master_options, ["relay_continuity"])
self.assertEqual(seam_options, ["Campaign default"])
self.assertEqual(
advertising.AdvertisementMultiShotPlanner.INPUT_TYPES()["required"]["performance_mode"][0],
["Dance / music sync", "Lyrics + lip sync", "Natural / audio-led sync"],
)
def test_contract_and_plan_embedded_hashes_are_enforced(self):
contract_json, result = build_plan()
contract = json.loads(contract_json)
contract["message"] = "tampered"
with self.assertRaisesRegex(ValueError, "contract_sha256"):
advertising._contract(json.dumps(contract))
plan = json.loads(result[0])
plan["source_native_shot_count"] = 7
with self.assertRaisesRegex(ValueError, "plan_sha256"):
advertising._plan(json.dumps(plan))
def test_tensor_hash_preserves_digest_without_materializing_multi_gigabyte_bytes(self):
value = torch.arange(48, dtype=torch.float32).reshape(2, 2, 4, 3)
header = advertising._json(
{"shape": list(value.shape), "dtype": "float32"}
).encode("utf-8")
expected = hashlib.sha256(header + value.numpy().tobytes(order="C")).hexdigest()
self.assertEqual(advertising._tensor_sha256(value), expected)
self.assertNotIn(".tobytes", inspect.getsource(advertising._tensor_sha256))
def test_30_seconds_uses_two_lanes_and_preserves_eight_native_shots_and_four_roles(self):
contract_json, result = build_plan()
contract = json.loads(contract_json)
plan = json.loads(result[0])
self.assertEqual(contract["h3_planning"]["generation_lane_count"], 2)
self.assertEqual(result[3], 2)
self.assertEqual(plan["source_native_shot_count"], 8)
self.assertEqual(
sorted({value for lane in plan["lanes"] for value in lane["source_native_shot_indices"]}),
list(range(1, 9)),
)
self.assertEqual([lane["duration_seconds"] for lane in plan["lanes"]], [15.0, 15.0])
self.assertEqual(plan["lanes"][0]["reference_picture_tags"], ["<Picture 1>", "<Picture 2>", "<Picture 3>"])
self.assertEqual(plan["lanes"][1]["reference_picture_tags"], ["<Picture 1>", "<Picture 2>", "<Picture 3>", "<Picture 4>"])
self.assertIn("<Subject 2>", result[4])
self.assertIn("<Picture 3>", result[9])
self.assertTrue(all(item["product_action"] for lane in plan["lanes"] for item in lane["product_presence"]))
def test_plain_picture_citations_in_governed_subject_rows_are_canonicalized(self):
prompt = EIGHT_SHOT_PROMPT.replace(
"<Subject 1>: the same performer jointly defined by <Picture 1> and <Picture 2>.",
"<Subject 1>: (Picture 1, Picture 2): the same performer.",
).replace(
"<Subject 2>: the same beverage package defined by <Picture 3>.",
"<Subject 2>: (Picture 3): the same beverage package.",
)
contract = build_contract()[0]
defaults = advertising.AdvertisementPlanningDefaults().build(
contract,
"Keep the can visible.",
"Campaign default",
)
result = advertising.AdvertisementMultiShotPlanner().plan(
contract,
prompt,
defaults[0],
defaults[1],
"Natural / audio-led sync",
)
plan = json.loads(result[0])
self.assertIn("(<Picture 1>, <Picture 2>)", plan["lanes"][0]["prompt"])
self.assertIn("(<Picture 3>)", plan["lanes"][0]["prompt"])
def test_defaults_guarantee_hero_product_and_valid_typed_seam_state(self):
contract = build_contract()[0]
defaults = advertising.AdvertisementPlanningDefaults().build(contract, "Keep the can visible.", "Campaign default")
shots = json.loads(defaults[0])
states = json.loads(defaults[1])
self.assertTrue(any(item["product_presence"] == "hero" for item in shots))
self.assertTrue(all(item["product_action"] == "Keep the can visible." for item in shots))
self.assertEqual(states[0]["exit_state"], states[1]["entry_state"])
self.assertEqual(states[1]["boundary_mode"], "relay_continuity")
self.assertTrue(defaults[4])
def test_seam_mismatch_fails_closed(self):
contract = build_contract()[0]
defaults = advertising.AdvertisementPlanningDefaults().build(contract, "Keep the can visible.", "Campaign default")
states = json.loads(defaults[1])
states[1]["entry_state"]["composition"] = "wrong composition"
with self.assertRaisesRegex(ValueError, "seam mismatch"):
advertising.AdvertisementMultiShotPlanner().plan(
contract,
EIGHT_SHOT_PROMPT,
defaults[0],
json.dumps(states),
"Natural / audio-led sync",
)
def test_relay_persists_exact_retained_tail_not_h3_padding(self):
contract_json, result = build_plan()
plan = json.loads(result[0])
retained = plan["lanes"][0]["retained_master_frames"]
images = torch.zeros((retained + 2, 8, 8, 3), dtype=torch.float32)
images[retained - 1] = 0.375
images[-1] = 0.875
with tempfile.TemporaryDirectory() as temporary:
persisted = advertising.AdvertisementRelayArtifact().persist(
images,
result[0],
plan["campaign_id"],
1,
2,
artifact_root=temporary,
)
manifest = json.loads(persisted[0])
self.assertEqual(manifest["source_retained_frame_index"], retained - 1)
loaded = advertising.AdvertisementRelayGate().load(
persisted[0],
result[0],
plan["campaign_id"],
2,
artifact_root=temporary,
)
self.assertTrue(torch.equal(loaded[0], images[retained - 1 : retained]))
self.assertTrue(Path(temporary, manifest["filename"]).is_file())
with self.assertRaisesRegex(ValueError, "campaign identity"):
advertising.AdvertisementRelayGate().load(
persisted[0], result[0], "f" * 64, 2, artifact_root=temporary
)
def test_outputs_validate_against_advertisement_schemas(self):
contract_json, result = build_plan()
for filename, payload in (
("advertisement_master_contract.schema.json", json.loads(contract_json)),
("advertisement_h3_plan.schema.json", json.loads(result[0])),
):
schema = json.loads((ROOT / "schemas" / filename).read_text(encoding="utf-8"))
Draft202012Validator(schema).validate(payload)
if __name__ == "__main__":
unittest.main()
+53
View File
@@ -0,0 +1,53 @@
from __future__ import annotations
import sys
import types
import unittest
from pathlib import Path
from unittest import mock
ROOT = Path(__file__).resolve().parents[1]
if str(ROOT) not in sys.path:
sys.path.insert(0, str(ROOT))
import advertising_runtime_nodes as runtime
class AdvertisementRuntimeNodeTests(unittest.TestCase):
def test_passthrough_policy_preserves_exact_object_without_comfy_runtime(self):
value = {"timing_report": "locked"}
result = runtime.AdvertisementMemoryBarrier().release(
value, "Passthrough (no release)"
)
self.assertIs(result[0], value)
self.assertIn("without releasing", result[1])
def test_release_calls_only_advertisement_owned_comfy_memory_boundary(self):
calls: list[object] = []
management = types.ModuleType("comfy.model_management")
management.unload_all_models = lambda: calls.append("unload")
management.soft_empty_cache = lambda force: calls.append(("soft", force))
comfy = types.ModuleType("comfy")
comfy.model_management = management
value = object()
with mock.patch.dict(
sys.modules,
{"comfy": comfy, "comfy.model_management": management},
), mock.patch("advertising_runtime_nodes.gc.collect") as collect, mock.patch(
"torch.cuda.is_available", return_value=False
):
result = runtime.AdvertisementMemoryBarrier().release(
value, "Unload models + clear CUDA cache"
)
self.assertIs(result[0], value)
self.assertEqual(calls, ["unload", ("soft", True)])
collect.assert_called_once_with()
def test_unknown_policy_fails_closed(self):
with self.assertRaisesRegex(ValueError, "unsupported"):
runtime.AdvertisementMemoryBarrier().release("value", "mystery")
if __name__ == "__main__":
unittest.main()
@@ -0,0 +1,981 @@
#!/usr/bin/env python3
"""Build the isolated GlowBloom Advertisement + MiniMax Music 3 API graph.
This is a validation fixture builder, not a migration of the user's known-good
V6 workflow. It starts from the exact runtime-proven GlowBloom API capture,
keeps the proven Director/H3 model path, replaces the temporary uploaded-song
branch with native MiniMax Music 3, and routes the result through the additive
Advertisement contracts, four-reference relay plan, deterministic finishing,
real cutdowns, and honest QA status.
The source capture and the checked-in V6 UI workflow are hash guarded and are
never written by this tool.
"""
from __future__ import annotations
import argparse
import copy
import hashlib
import json
import os
from pathlib import Path
import tempfile
from typing import Any
REPOSITORY = Path(__file__).resolve().parents[1]
KNOWN_GOOD_UI = REPOSITORY / "examples" / "15_minimax_h3_ref2va_music_video_v6.json"
KNOWN_GOOD_UI_SHA256 = "594f6aef94531f87b74659e1e8004cccdff00716d0cfe263cee6bf2149545c83"
SOURCE_API = (
REPOSITORY
/ "examples"
/ "api"
/ "16_minimax_h3_ref2va_advertisement_glowbloom_v1_api.json"
)
SOURCE_API_CANONICAL_SHA256 = "6296d86a7640ce1babe307f68c3ddab845341e5e1185cb2216924097e9b4f743"
OUTPUT_API = (
REPOSITORY
/ "examples"
/ "api"
/ "17_minimax_h3_ref2va_advertisement_music3_glowbloom_v1_api.json"
)
OUTPUT_MANIFEST = OUTPUT_API.with_suffix(".manifest.json")
SCHEMA = "diffusiongemma.advertisement_music3_runtime_fixture"
VERSION = 1
ASSET_PATHS = {
"performer_hero": "diffusiongemma_campaigns/glowbloom_20260823/glowbloom_hero_identity.png",
"performer_sheet": "diffusiongemma_campaigns/glowbloom_20260823/glowbloom_performer_contact_sheet_v1.png",
"product_sheet": "diffusiongemma_campaigns/glowbloom_20260823/glowbloom_product_contact_sheet_v1.png",
}
ASSET_SHA256 = {
"performer_hero": "98ced6b029fe36888b4ada491f5cbd7cb9efd4724001da064f3713b3da4798e4",
"performer_sheet": "cf13e146624290c1932727fb1a54e38bbe275a866b60ed649d1ca0ee4fd1939b",
"product_sheet": "ed534b55bc417c1610051f7d91fa807360bf67757e7aa41a96bcc655d4e5db8e",
}
DIRECTOR_MODEL_PATH = "models/LLM/diffusiongemma-26B-A4B-it-NVFP4"
MUSIC3_MODELS = {
"diffusion_model": "minimax_music3_dit_fp16.safetensors",
"text_encoder": "minimax_music3_text_encoder_pruned_int8_convrot.safetensors",
"vae": "minimax_music3_dav.safetensors",
}
MODEL_INVENTORY = {
"director": {
"checkpoint_directory": DIRECTOR_MODEL_PATH,
},
"minimax_h3": {
"diffusion_model": "models/diffusion_models/minimax_h3_ref2va_pruned_int8_convrot.safetensors",
"text_encoder": "models/text_encoders/qwen3vl_32b_minimax_h3_nvfp4_awq.safetensors",
"video_vae": "models/vae/minimax_h3_video_vae_fp16.safetensors",
"audio_vae": "models/vae/minimax_h3_audio_vae_fp32.safetensors",
"turbo_lora": "models/loras/minimax_h3_ref2v_turbo_4step_v0.1_comfyui_bf16.safetensors",
},
"minimax_music3": {
"diffusion_model": "models/diffusion_models/minimax_music3_dit_fp16.safetensors",
"text_encoder": "models/text_encoders/minimax_music3_text_encoder_pruned_int8_convrot.safetensors",
"vae": "models/vae/minimax_music3_dav.safetensors",
},
}
class FixtureError(RuntimeError):
"""Raised when an immutable fixture or graph invariant is violated."""
def _sha256_bytes(value: bytes) -> str:
return hashlib.sha256(value).hexdigest()
def _canonical_sha256(value: Any) -> str:
payload = json.dumps(
value, ensure_ascii=False, sort_keys=True, separators=(",", ":")
).encode("utf-8")
return _sha256_bytes(payload)
def _json_bytes(value: Any) -> bytes:
return json.dumps(value, ensure_ascii=False, indent=2).encode("utf-8") + b"\n"
def _load_guarded_json(path: Path, *, byte_hash: str = "", canonical_hash: str = "") -> Any:
data = path.read_bytes()
if byte_hash and _sha256_bytes(data) != byte_hash:
raise FixtureError(f"Immutable source changed: {path}")
try:
parsed = json.loads(data.decode("utf-8"))
except (UnicodeDecodeError, json.JSONDecodeError) as exc:
raise FixtureError(f"Invalid UTF-8 JSON source: {path}") from exc
if canonical_hash and _canonical_sha256(parsed) != canonical_hash:
raise FixtureError(f"Immutable canonical graph changed: {path}")
return parsed
def _node(class_type: str, title: str, **inputs: Any) -> dict[str, Any]:
return {"class_type": class_type, "inputs": inputs, "_meta": {"title": title}}
def _copy_node(source: dict[str, Any], node_id: str) -> dict[str, Any]:
try:
return copy.deepcopy(source[node_id])
except KeyError as exc:
raise FixtureError(f"Runtime source is missing node {node_id}.") from exc
def _required_source_nodes(source: dict[str, Any]) -> dict[str, dict[str, Any]]:
top_level = (
"179", "182", "183", "185", "186", "187", "188", "418",
"651", "653", "654", "661", "662", "673", "674", "675",
"676", "678", "681", "683", "691", "699", "713", "716",
)
removed_h3 = {
"710:665", "710:682", "710:689", "710:690", "710:697", "710:698",
"710:725", "710:726", "710:727", "710:728", "710:729", "710:730",
}
selected: dict[str, dict[str, Any]] = {
node_id: _copy_node(source, node_id) for node_id in top_level
}
for node_id in source:
if node_id.startswith("710:") and node_id not in removed_h3:
selected[node_id] = _copy_node(source, node_id)
return selected
def _patch_director_and_h3(graph: dict[str, dict[str, Any]]) -> None:
graph["182"]["inputs"]["model_path"] = DIRECTOR_MODEL_PATH
graph["179"]["inputs"] = {"image": ASSET_PATHS["performer_hero"]}
graph["179"]["_meta"]["title"] = "Picture 1 — performer hero identity"
# Keep the runtime-proven off baseline explicit to the operator. The
# Advertisement reference contract, packet repair governance checks, JSON
# splitter, and branch gate remain independently fail-closed downstream.
graph["185"]["_meta"]["title"] = (
"5b. H3 Grounding Guard — OFF baseline (contract validation stays active)"
)
graph["721"] = _node(
"LoadImage",
"Picture 2 — same-performer contact sheet",
image=ASSET_PATHS["performer_sheet"],
)
graph["800"] = _node(
"LoadImage",
"Picture 3 — independent product/package contact sheet",
image=ASSET_PATHS["product_sheet"],
)
graph["183"]["inputs"].update(
{
"user_prompt": ["801", 1],
"image": ["803", 6],
"target_duration_seconds": ["849", 0],
"max_duration_seconds": ["849", 0],
}
)
graph["186"]["inputs"].update(
{
"gemma_context": ["674", 0],
"target_profile_config": ["673", 0],
"measured_audio_report_json": ["819", 0],
"temperature": 0.35,
"creativity_mode": "cinematic",
"creative_strength": 0.7,
"thinking_mode": "off",
"cache_mode": "reuse",
}
)
graph["820"] = _node(
"DiffusionGemmaAdvertisementDirectorPacketRepair",
"6b. Recover transport-only JSON inside host-governed Director packet",
director_final_json=["186", 0],
director_raw_response=["186", 2],
director_metadata_json=["186", 3],
grounding_status=["186", 4],
grounding_report_json=["186", 5],
reference_contract_json=["802", 0],
)
graph["187"]["inputs"].update(
{
"final_json": ["820", 0],
"gemma_context": ["674", 0],
"target_profile_config": ["673", 0],
"resolution_selector_megapixels": 0.4,
"resolution_selector_multiple": 32,
"resolution_aspect_ratio": "9:16 (Portrait Widescreen)",
"resolution_aspect_ratio_override": ["849", 15],
}
)
graph["188"]["inputs"] = {
"prompt": ["187", 13],
"ready_for_generation": ["187", 12],
"metadata_json": ["187", 4],
}
graph["673"]["inputs"].update(
{
"generation_mode": ["849", 7],
"target_duration_seconds": ["849", 0],
"audio_mode": ["849", 10],
"audio_guidance": ["675", 1],
"shot_count": ["849", 8],
"custom_shot_count": ["849", 1],
"dialogue_mode": ["849", 11],
"dialogue_line_count": 2,
"dialogue_guidance": "",
"negative_prompt_mode": "auto",
"negative_prompt_guidance": (
"No extra people, duplicate performer, identity drift, masculinization, alternate face, "
"alternate hair or wardrobe, product drift, alternate package, malformed can, alcohol cues, "
"nightclub, concert, unintended singing, speaking, or lip sync outside the selected performance "
"mode, floating text, invented logos, unreadable "
"packaging, incoherent camera path, flicker, or discontinuous product placement."
),
"shot_count_override": ["849", 9],
}
)
graph["674"]["inputs"].update(
{
"reference_manifest": ["802", 1],
"sample_fps": 1.0,
"max_duration_seconds": 60.0,
"max_frames": 60,
"overlong_policy": "trim",
"visual_description": (
"Picture 1 and Picture 2 show the same sole adult performer and wardrobe. Picture 3 is a "
"product-only multi-view contact sheet for the same orange GLOWBLOOM YUZU PEAR can. The "
"product is independent Subject 2; contact-sheet grids, seams, repeated panels, backgrounds, "
"and viewpoint sequences never transfer into the video."
),
"reference_manifest_preset": "custom",
"expected_subject_count": ["802", 2],
"user_prompt": ["801", 1],
"reference_images": ["803", 6],
}
)
graph["675"]["inputs"] = {
"performance_mode": ["849", 2],
"base_audio_guidance": (
"Use the exact selected soundtrack as the timing authority. Synchronize edits, hand actions, "
"product reveals, practical transitions, and physically coherent expressive H3 camera accents to "
"measured rhythm and recovery. Follow the single selected performance mode: Natural avoids forced "
"singing or choreography, Dance emphasizes whole-body music sync without lyric mouthing, and Lyrics "
"permits vocal articulation only when real lyrics and timing evidence are present. Allow controlled "
"arcs, parallax, sweeps, and compound camera paths when performer and package remain legible."
),
}
graph["676"]["inputs"] = {
"production_duration_seconds": ["849", 0],
"aspect_ratio": ["849", 6],
"excerpt_start_seconds": ["849", 12],
"excerpt_duration_seconds": ["849", 0],
"generation_model": ["849", 13],
"deliverables_json": ["849", 5],
"max_h3_shot_seconds": ["849", 14],
"creative_brief": ["801", 1],
"master_audio_sha256": ["818", 2],
"lyrics": ["804", 2],
}
graph["678"]["inputs"] = {
"root_seed": ["418", 0],
"project_manifest_json": ["676", 0],
}
# Disable measured 1.00x acceleration experiments and keep the baseline
# self-contained. Native ComfyUI attention is used after the proven LoRA;
# sigma shifts, scheduler, and sampler remain unchanged.
graph["716"]["inputs"]["model"] = ["713", 0]
graph["713"]["inputs"]["strength_model"] = 0.75
graph["713"]["_meta"]["title"] = "Official Ref2VA Turbo LoRA — Advertisement baseline strength 0.75"
graph["710:658"]["_meta"]["title"] = "Shared H3 schedule — simple / 7 sampling steps"
for node_id in ("710:717", "710:718", "710:719", "710:720"):
graph[node_id] = _node(
"DiffusionGemmaAdvertisementMemoryBarrier",
graph[node_id]["_meta"]["title"],
value=graph[node_id]["inputs"]["value"],
policy="Unload models + clear CUDA cache",
)
planner = "823"
trim_ids = ("681", "683", "691", "699")
h3_ids = ("710:667", "710:684", "710:692", "710:700")
decoder_ids = ("710:656", "710:688", "710:696", "710:704")
relay_gates = (None, "826", "828", "830")
for index, (trim_id, h3_id, decoder_id, relay_gate) in enumerate(
zip(trim_ids, h3_ids, decoder_ids, relay_gates), start=1
):
base_output = 4 + (index - 1) * 5
graph[trim_id]["inputs"] = {
"start_index": [planner, base_output + 1],
"duration": [planner, base_output + 2],
"audio": ["817", 0],
}
inputs = graph[h3_id]["inputs"]
# MiniMaxH3ReferenceToVideo builds conditioning only. The diffusion
# MODEL belongs on the downstream scheduler/guiders, and current live
# ComfyUI rejects a model keyword on this node. Remove a stale source
# key before applying the Advertisement-owned conditioning inputs.
inputs.pop("model", None)
inputs.update(
{
"clip": ["662", 0],
"vae": ["653", 0],
"audio_vae": ["654", 0],
"ref_images.ref_image_0": ["803", 0],
"ref_images.ref_image_1": ["803", 1],
"ref_images.ref_image_2": ["803", 2],
"ref_audios.ref_audio_0": [trim_id, 0],
"prompt": [planner, base_output],
"width": ["187", 10],
"height": ["187", 11],
"length": [planner, base_output + 3],
"ref_image_size": "match",
}
)
if relay_gate is None:
inputs.pop("ref_images.ref_image_3", None)
graph[h3_id]["_meta"]["title"] = (
"H3 GENERATION LANE 1 — P1+P2 performer + P3 product + Audio 1"
)
else:
inputs["ref_images.ref_image_3"] = [relay_gate, 0]
graph[h3_id]["_meta"]["title"] = (
f"H3 GENERATION LANE {index} — P1+P2 performer + P3 product + P4 relay + Audio 1"
)
graph[decoder_id]["_meta"]["title"] = f"Advertisement H3 lane {index} decoded frames"
graph["710:663"]["inputs"]["noise_seed"] = ["678", 0]
graph["710:685"]["inputs"]["noise_seed"] = ["678", 1]
graph["710:693"]["inputs"]["noise_seed"] = ["678", 2]
graph["710:701"]["inputs"]["noise_seed"] = ["678", 3]
def build_graph(source: dict[str, Any]) -> dict[str, Any]:
graph = _required_source_nodes(source)
graph["849"] = _node(
"DiffusionGemmaAdvertisementWorkflowControls",
"ADVERTISEMENT CONTROLS — single duration / shot / performance authority",
master_duration="30 seconds",
native_shot_count=8,
performance_mode="Natural / audio-led sync",
)
_patch_director_and_h3(graph)
graph["801"] = _node(
"DiffusionGemmaAdvertisementCampaignContract",
"ADVERTISEMENT CAMPAIGN — exact copy, claims, timing and delivery",
brand_name="GLOWBLOOM",
product_name="Sparkling Botanical Beverage",
variant_name="Yuzu Pear",
audience="Style-conscious adults in their twenties and thirties who want a bright, non-alcoholic social ritual.",
campaign_objective="Build distinct product recognition and make GLOWBLOOM feel tactile, modern, optimistic, and naturally social.",
headline="FIND YOUR BRIGHT SIDE",
call_to_action="SIP THE SHIFT",
claims_json="[]",
production_duration_seconds=["849", 0],
aspect_ratio=["849", 6],
end_card_duration_seconds=3.0,
deliverables_json=["849", 5],
campaign_name="GlowBloom Bright Side",
legal_line="",
offer_text="",
price_text="",
)
graph["802"] = _node(
"DiffusionGemmaAdvertisementReferenceContract",
"ADVERTISEMENT REFERENCES — performer P1/P2, product P3, relay P4",
slot_policy="2 performer + 1 product + relay",
performer_description=(
"The same adult woman in every performer reference: warm medium-brown skin, oval face, dark "
"textured shoulder-length hair, slim athletic build, confident friendly presence, and the same "
"modern coral-and-cream wardrobe."
),
product_description=(
"The same slim orange GLOWBLOOM YUZU PEAR sparkling botanical beverage can with a silver top, "
"cream vertical brand panel, warm yellow-orange gradient, and precise cylindrical proportions."
),
product_retention_attributes=(
"can silhouette, height-to-width ratio, silver lid, orange-to-yellow palette, cream brand panel, "
"GLOWBLOOM wordmark placement, YUZU PEAR variant hierarchy, material highlights"
),
product_reference_kind="product contact sheet",
performer_retention_attributes="face, skin tone, hair, body proportions, wardrobe, age, gender presentation",
package_copy_json='["GLOWBLOOM","YUZU PEAR"]',
)
graph["803"] = _node(
"DiffusionGemmaAdvertisementReferenceAssetPrep",
"ADVERTISEMENT ASSET PREP — three independent hash-locked roles",
performer_hero_image=["179", 0],
performer_sheet_image=["721", 0],
product_sheet_image=["800", 0],
# This is the exact 0.4 MP, 9:16, multiple-of-32 result used by
# JSON Splitter below. Keeping it explicit breaks the otherwise hidden
# reference-prep -> Director -> reference-context dependency cycle.
generation_width=480,
generation_height=864,
performer_area_ratio=1.0,
performer_hero_share=0.6,
product_area_ratio=0.5,
)
graph["804"] = _node(
"DiffusionGemmaAdvertisementSoundtrackContract",
"ADVERTISEMENT SOUNDTRACK — governed MiniMax Music 3 instrumental",
content_mode="Instrumental",
genre_style="luminous future-disco and modern indie-pop instrumental for a premium beverage commercial",
mood="energetic, stylish, optimistic, tactile, polished, cool and human rather than aggressive",
instrumentation="tight kick, dry claps, rubbery disco bass, muted funk guitar, fizzy glass percussion, bright arpeggio and shimmering analog pads",
target_duration_seconds=["849", 0],
bpm=122.0,
time_signature="4",
language="unknown",
lyrics="",
voice_over_policy="None",
arrangement_notes=(
"Open immediately with a crisp pickup and two-note motif; establish the groove by four seconds; "
"lift near twelve seconds; thin briefly near twenty seconds for a liquid-splash accent; return to "
"the fullest hook for the hero reveal; simplify from twenty-eight seconds into a confident final "
"cadence and short sparkling tail for the end card."
),
do_not_sound_like="nightclub EDM, festival drops, aggressive trap, novelty jingle, vocals, chants or spoken words",
)
graph["805"] = _node(
"DiffusionGemmaAdvertisementMusic3PromptAdapter",
"MiniMax Music 3 governed prompt adapter",
soundtrack_contract_json=["804", 0],
)
graph["806"] = _node(
"CLIPLoader",
"MiniMax Music 3 pruned INT8 text encoder",
clip_name=MUSIC3_MODELS["text_encoder"],
type="minimax",
device="default",
)
graph["807"] = _node(
"UNETLoader",
"MiniMax Music 3 DiT FP16",
unet_name=MUSIC3_MODELS["diffusion_model"],
weight_dtype="default",
)
graph["808"] = _node(
"VAELoader",
"MiniMax Music 3 DAV",
vae_name=MUSIC3_MODELS["vae"],
)
graph["809"] = _node(
"MiniMaxMusic3TextEncode",
"MiniMax Music 3 — 35s commercial candidate conditioning",
clip=["806", 0],
caption=["805", 0],
lyrics=["805", 1],
seed=2026082302,
max_duration=["805", 4],
cfg_scale=1.7,
top_k=50,
)
graph["810"] = _node(
"ConditioningZeroOut",
"MiniMax Music 3 negative conditioning",
conditioning=["809", 0],
)
graph["811"] = _node(
"EmptyMiniMaxMusic3LatentAudio",
"MiniMax Music 3 candidate latent",
seconds=["809", 1],
batch_size=1,
)
graph["812"] = _node(
"KSampler",
"MiniMax Music 3 — official 30-step path",
model=["807", 0],
seed=2026082302,
steps=30,
cfg=1.7,
sampler_name="euler",
scheduler="simple",
positive=["809", 0],
negative=["810", 0],
latent_image=["811", 0],
denoise=1.0,
)
graph["813"] = _node(
"VAEDecodeAudioTiled",
"MiniMax Music 3 tiled decode",
samples=["812", 0],
vae=["808", 0],
tile_size=512,
overlap=64,
)
graph["814"] = _node(
"DiffusionGemmaAdvertisementSoundtrackSourceRouter",
"SOUNDTRACK SOURCE — MiniMax Music 3 default / upload / ACE fallback",
source_mode="MiniMax Music 3",
music3_candidate_count=1,
music3_expected_bpm=["805", 3],
music3_lyrics=["805", 1],
music3_duration_seconds=["809", 1],
music3_candidate_1=["813", 0],
)
graph["815"] = _node(
"DiffusionGemmaAdvertisementAudioCandidateSelector",
"MiniMax Music 3 audition — select a locked 30s excerpt",
soundtrack_contract_json=["804", 0],
candidate_count=["814", 4],
selection_mode="auto_select",
expected_bpm=["814", 5],
excerpt_duration_seconds=["849", 0],
minimum_score=0.52,
locked_waveform_sha256="",
locked_start_seconds=-1.0,
candidate_1=["814", 0],
candidate_2=["814", 1],
candidate_3=["814", 2],
candidate_4=["814", 3],
source_policy=["814", 8],
)
graph["816"] = _node(
"TrimAudioDuration",
"Selected MiniMax Music 3 30s master window",
audio=["815", 0],
start_index=["815", 1],
duration=["849", 0],
)
graph["817"] = _node(
"DiffusionGemmaAdvertisementAudioMixer",
"Music-only motion guide and exact final mix",
music_audio=["816", 0],
soundtrack_contract_json=["804", 0],
target_duration_seconds=["849", 0],
music_gain_db=0.0,
voice_over_gain_db=0.0,
ducking_db=-9.0,
duck_attack_ms=80.0,
duck_release_ms=250.0,
peak_ceiling_dbfs=-1.0,
clipping_policy="Attenuate mix to ceiling",
)
graph["818"] = _node(
"DiffusionGemmaAdvertisementAudioCandidateSelector",
"Verify exact 30s master audio and rebuild relative timing report",
soundtrack_contract_json=["804", 0],
candidate_count=1,
selection_mode="lock_candidate_1",
expected_bpm=["814", 5],
excerpt_duration_seconds=["849", 0],
minimum_score=0.0,
locked_waveform_sha256="",
locked_start_seconds=0.0,
candidate_1=["817", 1],
source_policy="minimax_music3",
)
graph["819"] = _node(
"DiffusionGemmaAdvertisementMemoryBarrier",
"Unload MiniMax Music 3 before Director and H3",
value=["818", 3],
policy="Unload models + clear CUDA cache",
)
graph["821"] = _node(
"DiffusionGemmaAdvertisementMasterContract",
"ADVERTISEMENT MASTER — campaign, references, soundtrack and H3 lanes",
campaign_contract_json=["801", 0],
reference_contract_json=["802", 0],
soundtrack_contract_json=["804", 0],
project_manifest_json=["676", 0],
performer_hero_sha256=["803", 3],
performer_contact_sheet_sha256=["803", 4],
product_package_sha256=["803", 5],
master_audio_sha256=["818", 2],
native_shot_count=["849", 1],
boundary_mode="relay_continuity",
)
graph["822"] = _node(
"DiffusionGemmaAdvertisementPlanningDefaults",
"PRODUCT PRESENCE + RELAY — non-technical defaults",
advertisement_contract_json=["821", 0],
product_use=(
"The performer naturally discovers, carries, opens, presents, pours, and enjoys the can while "
"its orange package, silver top, cream panel, and proportions remain clearly recognizable."
),
seam_style="Campaign default",
)
graph["823"] = _node(
"DiffusionGemmaAdvertisementMultiShotPlanner",
"ADVERTISEMENT H3 PLAN — 8 native shots across 2x15s lanes",
advertisement_contract_json=["821", 0],
base_h3_prompt=["188", 0],
shot_metadata_json=["822", 0],
lane_states_json=["822", 1],
performance_mode=["849", 2],
)
for completed, next_lane, artifact_id, gate_id, decoder_id in (
(1, 2, "825", "826", "710:656"),
(2, 3, "827", "828", "710:688"),
(3, 4, "829", "830", "710:696"),
):
graph[artifact_id] = _node(
"DiffusionGemmaAdvertisementRelayArtifact",
f"Persist exact retained tail from lane {completed} for Picture 4",
lane_images=[decoder_id, 0],
advertisement_plan_json=["823", 0],
campaign_id=["821", 1],
completed_lane_index=completed,
next_lane_index=next_lane,
max_megapixels=0.064,
)
graph[gate_id] = _node(
"DiffusionGemmaAdvertisementRelayGate",
f"Verify persistent Picture 4 for lane {next_lane}",
relay_artifact_json=[artifact_id, 0],
advertisement_plan_json=["823", 0],
campaign_id=["821", 1],
lane_index=next_lane,
enabled=True,
)
graph["831"] = _node(
"DiffusionGemmaAdvertisementMasterAssembler",
"Exact 30s Advertisement master assembly",
advertisement_contract_json=["821", 0],
advertisement_plan_json=["823", 0],
master_audio=["817", 1],
lane_1_images=["710:656", 0],
lane_2_images=["710:688", 0],
lane_3_images=["710:696", 0],
lane_4_images=["710:704", 0],
)
graph["832"] = _node(
"DiffusionGemmaAdvertisementEndCardRenderer",
"Deterministic 3s GLOWBLOOM end card",
advertisement_contract_json=["821", 0],
width=["187", 10],
height=["187", 11],
background_color="#ef6f3c",
brand_color="#fff8e7",
accent_color="#ffe06b",
)
graph["833"] = _node(
"DiffusionGemmaAdvertisementMasterFinisher",
"Replace exact master tail with governed end card",
assembled_master=["831", 0],
master_audio=["831", 1],
end_card_images=["832", 0],
end_card_report_json=["832", 1],
advertisement_contract_json=["821", 0],
assembly_report_json=["831", 2],
)
graph["834"] = _node(
"DiffusionGemmaAdvertisementAdaptationStatus",
"Requested aspect adaptations — honest render status",
advertisement_contract_json=["821", 0],
)
graph["835"] = _node(
"DiffusionGemmaAdvertisementCutdownRenderer",
"Render exact 15s and 6s cutdowns",
finished_master=["833", 0],
finished_audio=["833", 1],
advertisement_contract_json=["821", 0],
cutdown_15_start_seconds=["849", 3],
cutdown_6_start_seconds=["849", 4],
adaptation_status_json=["834", 0],
)
graph["836"] = _node(
"DiffusionGemmaAdvertisementMediaQAGate",
"REVIEW QA — unmeasured identity/copy checks never approve delivery",
advertisement_contract_json=["821", 0],
delivery_manifest_json=["835", 4],
checks_json=(
'{"technical_integrity":{"status":"pass","evidence":"exact frame/audio assembly contract"},'
'"product_identity":"not_measured","performer_identity":"not_measured",'
'"copy_legibility":"not_measured","audio_sync":"not_measured"}'
),
required_checks_json='["product_identity","performer_identity","copy_legibility","audio_sync","technical_integrity"]',
)
graph["837"] = _node(
"CreateVideo", "Mux exact 30s Advertisement master", images=["833", 0], audio=["833", 1], fps=24.0, bit_depth=8
)
graph["838"] = _node(
"SaveVideo", "SAVE REVIEW DRAFT — 30s GLOWBLOOM master", video=["837", 0],
filename_prefix="diffusiongemma_advertisement/glowbloom_music3/review_drafts/master_30s", format="mp4", codec="h264", **{"codec.encoding": "auto"}
)
graph["839"] = _node(
"CreateVideo", "Mux exact 15s cutdown", images=["835", 0], audio=["835", 1], fps=24.0, bit_depth=8
)
graph["840"] = _node(
"SaveVideo", "SAVE REVIEW DRAFT — 15s GLOWBLOOM cutdown", video=["839", 0],
filename_prefix="diffusiongemma_advertisement/glowbloom_music3/review_drafts/cutdown_15s", format="mp4", codec="h264", **{"codec.encoding": "auto"}
)
graph["841"] = _node(
"CreateVideo", "Mux exact 6s cutdown", images=["835", 2], audio=["835", 3], fps=24.0, bit_depth=8
)
graph["842"] = _node(
"SaveVideo", "SAVE REVIEW DRAFT — 6s GLOWBLOOM cutdown", video=["841", 0],
filename_prefix="diffusiongemma_advertisement/glowbloom_music3/review_drafts/cutdown_6s", format="mp4", codec="h264", **{"codec.encoding": "auto"}
)
graph["843"] = _node("PreviewAny", "ADVERTISEMENT QA STATUS", source=["836", 2])
graph["844"] = _node(
"SaveAudio", "SAVE REVIEW DRAFT — exact 30s MiniMax Music 3 master", audio=["817", 1],
filename_prefix="diffusiongemma_advertisement/glowbloom_music3/review_drafts/soundtrack_master_30s"
)
graph["845"] = _node("PreviewAudio", "AUDITION exact 30s MiniMax Music 3 master", audio=["817", 1])
graph["900"] = _node(
"PreviewAny",
"SOUNDTRACK QC — always visible, including blocked no-media runs",
source=["815", 4],
)
graph["901"] = _node(
"PreviewAny",
"DIRECTOR VALIDATION AUDIT — always visible, including blocked no-media runs",
source=["187", 4],
)
graph["902"] = _node(
"PreviewAny",
"DIRECTOR RAW RESPONSE AUDIT — visible when structured output is malformed",
source=["186", 2],
)
# The old runtime capture's output node is retained only as a recognizable
# source id, but it now points at the new deterministic finished master.
graph.pop("651", None)
validate_graph(graph)
return graph
def _connection_origins(graph: dict[str, Any]) -> set[str]:
origins: set[str] = set()
for node in graph.values():
for value in node.get("inputs", {}).values():
if isinstance(value, list) and len(value) == 2 and isinstance(value[0], str):
origins.add(value[0])
return origins
def _assert_acyclic(graph: dict[str, Any]) -> None:
dependencies = {
node_id: {
value[0]
for value in node.get("inputs", {}).values()
if isinstance(value, list)
and len(value) == 2
and isinstance(value[0], str)
and value[0] in graph
}
for node_id, node in graph.items()
}
visiting: set[str] = set()
visited: set[str] = set()
def visit(node_id: str, trail: list[str]) -> None:
if node_id in visited:
return
if node_id in visiting:
start = trail.index(node_id) if node_id in trail else 0
cycle = trail[start:] + [node_id]
raise FixtureError("Advertisement graph contains a dependency cycle: " + " -> ".join(cycle))
visiting.add(node_id)
trail.append(node_id)
for dependency in sorted(dependencies[node_id]):
visit(dependency, trail)
trail.pop()
visiting.remove(node_id)
visited.add(node_id)
for node_id in sorted(graph):
visit(node_id, [])
def validate_graph(graph: dict[str, Any]) -> None:
missing = sorted(_connection_origins(graph) - set(graph))
if missing:
raise FixtureError("Advertisement graph has dangling origins: " + ", ".join(missing))
_assert_acyclic(graph)
required_classes = {
"DiffusionGemmaAdvertisementWorkflowControls",
"MiniMaxMusic3TextEncode",
"DiffusionGemmaAdvertisementCampaignContract",
"DiffusionGemmaAdvertisementReferenceAssetPrep",
"DiffusionGemmaAdvertisementSoundtrackSourceRouter",
"DiffusionGemmaAdvertisementAudioCandidateSelector",
"DiffusionGemmaAdvertisementMemoryBarrier",
"DiffusionGemmaAdvertisementDirectorPacketRepair",
"DiffusionGemmaAdvertisementMasterContract",
"DiffusionGemmaAdvertisementMultiShotPlanner",
"DiffusionGemmaAdvertisementRelayArtifact",
"DiffusionGemmaAdvertisementRelayGate",
"DiffusionGemmaAdvertisementMasterAssembler",
"DiffusionGemmaAdvertisementEndCardRenderer",
"DiffusionGemmaAdvertisementCutdownRenderer",
"DiffusionGemmaAdvertisementMediaQAGate",
}
classes = {str(node.get("class_type", "")) for node in graph.values()}
absent = sorted(required_classes - classes)
if absent:
raise FixtureError("Advertisement graph is missing required classes: " + ", ".join(absent))
outputs = [node for node in graph.values() if node.get("class_type") == "SaveVideo"]
if len(outputs) != 3:
raise FixtureError("Advertisement graph must save one master and two real cutdowns.")
if graph.get("900", {}).get("inputs", {}).get("source") != ["815", 4]:
raise FixtureError("Advertisement soundtrack QC must remain independently visible when media is blocked.")
if graph.get("901", {}).get("inputs", {}).get("source") != ["187", 4]:
raise FixtureError("Advertisement Director validation metadata must remain visible when media is blocked.")
if graph.get("902", {}).get("inputs", {}).get("source") != ["186", 2]:
raise FixtureError("Advertisement raw Director response must remain inspectable when structured output is malformed.")
if graph.get("818", {}).get("inputs", {}).get("expected_bpm") != ["814", 5]:
raise FixtureError("Advertisement verifier must use the selected soundtrack source BPM authority.")
shared = graph.get("849", {})
if shared.get("class_type") != "DiffusionGemmaAdvertisementWorkflowControls":
raise FixtureError("Advertisement graph lost its single shared settings authority.")
for node_id, input_name, output_slot in (
("801", "production_duration_seconds", 0),
("804", "target_duration_seconds", 0),
("183", "target_duration_seconds", 0),
("183", "max_duration_seconds", 0),
("673", "target_duration_seconds", 0),
("676", "production_duration_seconds", 0),
("815", "excerpt_duration_seconds", 0),
("816", "duration", 0),
("817", "target_duration_seconds", 0),
("818", "excerpt_duration_seconds", 0),
("673", "custom_shot_count", 1),
("821", "native_shot_count", 1),
("675", "performance_mode", 2),
("823", "performance_mode", 2),
("835", "cutdown_15_start_seconds", 3),
("835", "cutdown_6_start_seconds", 4),
("801", "deliverables_json", 5),
("676", "deliverables_json", 5),
("801", "aspect_ratio", 6),
("676", "aspect_ratio", 6),
("673", "generation_mode", 7),
("673", "shot_count", 8),
("673", "shot_count_override", 9),
("673", "audio_mode", 10),
("673", "dialogue_mode", 11),
("676", "excerpt_start_seconds", 12),
("676", "generation_model", 13),
("676", "max_h3_shot_seconds", 14),
("187", "resolution_aspect_ratio_override", 15),
):
if graph[node_id]["inputs"].get(input_name) != ["849", output_slot]:
raise FixtureError(f"Advertisement shared control drifted at {node_id}:{input_name}.")
forbidden_runtime_classes = {
"EasyCache",
"TorchCompileModel",
"PathchSageAttentionKJ",
"easy cleanGpuUsed",
"FL_UnloadAllModels",
}
if forbidden_runtime_classes & classes:
raise FixtureError("Unmeasured acceleration experiments must not enter the Advertisement baseline.")
for lane, node_id in enumerate(("710:667", "710:684", "710:692", "710:700"), start=1):
inputs = graph[node_id]["inputs"]
for reference_index in range(3):
if f"ref_images.ref_image_{reference_index}" not in inputs:
raise FixtureError(f"H3 lane {lane} lost persistent Picture {reference_index + 1}.")
if lane == 1 and "ref_images.ref_image_3" in inputs:
raise FixtureError("H3 lane 1 cannot consume relay Picture 4.")
if lane > 1 and "ref_images.ref_image_3" not in inputs:
raise FixtureError(f"H3 lane {lane} lost relay Picture 4.")
def _manifest(graph: dict[str, Any]) -> dict[str, Any]:
value = {
"schema": SCHEMA,
"version": VERSION,
"source_api": str(SOURCE_API.relative_to(REPOSITORY)).replace("\\", "/"),
"source_api_canonical_sha256": SOURCE_API_CANONICAL_SHA256,
"known_good_v6": str(KNOWN_GOOD_UI.relative_to(REPOSITORY)).replace("\\", "/"),
"known_good_v6_sha256": KNOWN_GOOD_UI_SHA256,
"known_good_v6_is_never_overwritten": True,
"api_graph_canonical_sha256": _canonical_sha256(graph),
"api_graph_file_sha256": _sha256_bytes(_json_bytes(graph)),
"node_count": len(graph),
"models": MODEL_INVENTORY,
"assets": {
key: {"relative_input_path": path, "sha256": ASSET_SHA256[key]}
for key, path in ASSET_PATHS.items()
},
"audio_source_default": "MiniMax Music 3",
"api_audio_sources": ["MiniMax Music 3"],
"companion_ui_only_audio_sources": ["Upload song", "Legacy ACE-Step"],
"h3_reference_policy": {
"picture_1": "performer hero",
"picture_2": "same-performer contact sheet",
"picture_3": "independent product/package contact sheet",
"picture_4": "hash-verified, downscaled copy of the exact retained prior-lane final frame; continuity only",
},
"deliverables": ["30s 9:16 review draft", "15s 9:16 review draft", "6s 9:16 review draft"],
"adaptations": "status-only until a semantic reframe renderer is connected; never falsely marked rendered",
"qa_default": "not_measured for visual identity, copy legibility, and audio sync until a human or measured checker supplies evidence",
"save_policy": "Saved media are review drafts; QA pass with evidence is required before delivery approval.",
}
value["manifest_sha256"] = _canonical_sha256(value)
return value
def _write_guarded(path: Path, payload: bytes, *, force: bool) -> str:
path = path.resolve()
path.parent.mkdir(parents=True, exist_ok=True)
existed = path.exists()
if existed:
current = path.read_bytes()
if current == payload:
return "verified"
if not force:
raise FixtureError(f"Refusing to overwrite a different artifact: {path}")
descriptor, temporary = tempfile.mkstemp(prefix=path.name + ".", suffix=".tmp", dir=path.parent)
try:
with os.fdopen(descriptor, "wb") as handle:
handle.write(payload)
handle.flush()
os.fsync(handle.fileno())
os.replace(temporary, path)
finally:
if os.path.exists(temporary):
os.unlink(temporary)
return "replaced" if existed else "created"
def main(argv: list[str] | None = None) -> int:
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument("--output", type=Path, default=OUTPUT_API)
parser.add_argument("--manifest", type=Path, default=OUTPUT_MANIFEST)
parser.add_argument("--check", action="store_true")
parser.add_argument("--force", action="store_true")
args = parser.parse_args(argv)
_load_guarded_json(KNOWN_GOOD_UI, byte_hash=KNOWN_GOOD_UI_SHA256)
source = _load_guarded_json(SOURCE_API, canonical_hash=SOURCE_API_CANONICAL_SHA256)
if not isinstance(source, dict):
raise FixtureError("Source API graph must be a JSON object.")
graph = build_graph(source)
manifest = _manifest(graph)
if args.check:
print(
json.dumps(
{
"graph_nodes": len(graph),
"graph_canonical_sha256": manifest["api_graph_canonical_sha256"],
"known_good_v6_sha256": KNOWN_GOOD_UI_SHA256,
},
sort_keys=True,
)
)
return 0
graph_action = _write_guarded(args.output, _json_bytes(graph), force=args.force)
manifest_action = _write_guarded(args.manifest, _json_bytes(manifest), force=args.force)
print(
json.dumps(
{
"graph": graph_action,
"graph_path": str(args.output.resolve()),
"manifest": manifest_action,
"manifest_path": str(args.manifest.resolve()),
"graph_canonical_sha256": manifest["api_graph_canonical_sha256"],
},
sort_keys=True,
)
)
return 0
if __name__ == "__main__": # pragma: no cover
raise SystemExit(main())
@@ -0,0 +1,804 @@
#!/usr/bin/env python3
"""Build the separate, user-editable Advertisement + Music 3 workflow.
The production graph is deliberately flattened: it does not reuse either V6
subgraph UUID, so edits to this Advertisement workflow cannot mutate the
known-good music-video workflow. Node socket metadata is read from a live
ComfyUI ``/object_info`` endpoint only while generating the checked-in UI
artifact; the resulting workflow is self-contained.
The native Music 3 route is the default. The uploaded-song and legacy ACE
branches are also present and connected through the lazy Advertisement source
router, but remain dormant unless selected.
"""
from __future__ import annotations
import argparse
import copy
import hashlib
import json
import math
import os
from pathlib import Path
import re
import tempfile
from typing import Any, Mapping
from urllib.request import urlopen
import uuid
REPOSITORY = Path(__file__).resolve().parents[1]
KNOWN_GOOD_UI = REPOSITORY / "examples" / "15_minimax_h3_ref2va_music_video_v6.json"
KNOWN_GOOD_UI_SHA256 = "594f6aef94531f87b74659e1e8004cccdff00716d0cfe263cee6bf2149545c83"
FIELD_TEST_API = (
REPOSITORY / "examples" / "api" / "16_minimax_h3_ref2va_advertisement_glowbloom_v1_api.json"
)
FIELD_TEST_API_CANONICAL_SHA256 = "6296d86a7640ce1babe307f68c3ddab845341e5e1185cb2216924097e9b4f743"
MUSIC3_API = (
REPOSITORY / "examples" / "api" / "17_minimax_h3_ref2va_advertisement_music3_glowbloom_v1_api.json"
)
MUSIC3_API_CANONICAL_SHA256 = "d6f8d8de431f233c00a1172afca39815b8bd8bd619b633e8e5162db310fffc82"
OUTPUT = REPOSITORY / "examples" / "16_minimax_h3_ref2va_advertisement_music3_v1.json"
SCHEMA = "diffusiongemma.advertisement_music3_workflow"
VERSION = 1
WORKFLOW_UUID = "d9118b98-cc97-5cf5-81d8-86eff1336120"
FORBIDDEN_RUNTIME_CLASSES = frozenset(
{
"EasyCache",
"TorchCompileModel",
"PathchSageAttentionKJ",
"PatchSageAttentionKJ",
"easy cleanGpuUsed",
"FL_UnloadAllModels",
}
)
class WorkflowError(RuntimeError):
"""Raised when workflow lineage or live node metadata is invalid."""
def _sha256_bytes(value: bytes) -> str:
return hashlib.sha256(value).hexdigest()
def _canonical_sha256(value: Any) -> str:
return _sha256_bytes(
json.dumps(value, ensure_ascii=False, sort_keys=True, separators=(",", ":")).encode("utf-8")
)
def _json_bytes(value: Any) -> bytes:
return json.dumps(value, ensure_ascii=False, indent=2).encode("utf-8") + b"\n"
def _load_guarded(path: Path, *, byte_hash: str = "", canonical_hash: str = "") -> Any:
data = path.read_bytes()
if byte_hash and _sha256_bytes(data) != byte_hash:
raise WorkflowError(f"Immutable source changed: {path}")
try:
parsed = json.loads(data.decode("utf-8"))
except (UnicodeDecodeError, json.JSONDecodeError) as exc:
raise WorkflowError(f"Invalid UTF-8 JSON source: {path}") from exc
if canonical_hash and _canonical_sha256(parsed) != canonical_hash:
raise WorkflowError(f"Immutable canonical graph changed: {path}")
return parsed
def _object_info(url: str) -> dict[str, Any]:
endpoint = url.rstrip("/") + "/object_info"
try:
with urlopen(endpoint, timeout=30) as response: # noqa: S310 - explicit local endpoint
payload = json.loads(response.read().decode("utf-8"))
except Exception as exc: # pragma: no cover - environment-specific transport
raise WorkflowError(f"Could not read live ComfyUI node metadata from {endpoint}: {exc}") from exc
if not isinstance(payload, dict):
raise WorkflowError("ComfyUI /object_info did not return a JSON object.")
return payload
def _is_connection(value: Any) -> bool:
return (
isinstance(value, list)
and len(value) == 2
and isinstance(value[0], str)
and isinstance(value[1], int)
)
def _api_node(class_type: str, title: str, **inputs: Any) -> dict[str, Any]:
return {"class_type": class_type, "inputs": inputs, "_meta": {"title": title}}
def _copy_ancestors(
source: Mapping[str, Any], target: dict[str, Any], roots: list[str]
) -> None:
pending = list(roots)
while pending:
node_id = pending.pop()
if node_id in target:
continue
node = source.get(node_id)
if not isinstance(node, Mapping):
raise WorkflowError(f"Legacy fallback source is missing node {node_id}.")
target[node_id] = copy.deepcopy(node)
for value in node.get("inputs", {}).values():
if _is_connection(value):
pending.append(value[0])
def _with_lazy_fallbacks(
music3: dict[str, Any], field_test: Mapping[str, Any]
) -> dict[str, Any]:
graph = copy.deepcopy(music3)
# Reference preparation must not wait for Director resolution because the
# Director itself consumes the prepared reference batch. The saved 9:16
# advertisement baseline uses the contract node's native 480x864 defaults;
# H3's final generation dimensions remain Director-controlled downstream.
graph["803"]["inputs"]["generation_width"] = 480
graph["803"]["inputs"]["generation_height"] = 864
ace_candidates = ["623:567", "623:597", "623:602", "623:607"]
_copy_ancestors(field_test, graph, ace_candidates + ["562"])
upload = field_test.get("736")
if not isinstance(upload, Mapping):
raise WorkflowError("Field-test graph has no uploaded-song loader.")
graph["736"] = copy.deepcopy(upload)
graph["736"]["inputs"]["audio_file"] = ""
graph["736"]["_meta"]["title"] = "UPLOAD SONG — used only when Upload song is selected"
# Force-input primitives make fallback controls visible and editable in the
# UI while keeping the router's selected branch truly lazy.
graph["846"] = _api_node("SeedNode", "Music 3 candidate count — one", seed=1)
graph["847"] = _api_node("PrimitiveFloat", "Uploaded-song expected BPM — zero means measure", value=0.0)
graph["848"] = _api_node("PrimitiveStringMultiline", "Uploaded-song lyrics — optional outside Lyrics mode", value="")
graph["850"] = _api_node("SeedNode", "Legacy ACE candidate count — four", seed=4)
graph["851"] = _api_node(
"PrimitiveStringMultiline",
"Advertisement performance guidance — music-only H3 motion authority",
value=graph["675"]["inputs"]["base_audio_guidance"],
)
graph["675"]["inputs"]["base_audio_guidance"] = ["851", 0]
# The old ACE audition plan depended on the H3 Director context, while the
# Director waits for the selected soundtrack. Reuse only the proven ACE
# model/sampler branch and feed it from the already-governed Advertisement
# Soundtrack contract. This keeps ACE a real lazy fallback without a
# circular song<->Director dependency.
for node_id in ("623:561", "623:594", "623:599", "623:604"):
inputs = graph[node_id]["inputs"]
inputs["tags"] = ["805", 0]
inputs["lyrics"] = ["805", 1]
inputs["bpm"] = ["805", 5]
inputs["timesignature"] = "4"
inputs["language"] = "unknown"
inputs["keyscale"] = "C major"
# These nodes existed only to derive the former Director-dependent ACE
# blueprint or to preview intermediate audio. They are not production
# outputs and must not leak into the independent Advertisement workflow.
for node_id in ("183", "556", "578", "609"):
graph.pop(node_id, None)
router = graph["814"]["inputs"]
router.update(
{
"music3_candidate_count": ["846", 0],
"uploaded_audio": ["736", 0],
"uploaded_duration_seconds": ["736", 1],
"uploaded_expected_bpm": ["847", 0],
"uploaded_lyrics": ["848", 0],
"uploaded_waveform_sha256": ["736", 2],
"uploaded_status": ["736", 3],
"uploaded_ready": ["736", 4],
"ace_candidate_count": ["850", 0],
"ace_expected_bpm": ["805", 3],
"ace_lyrics": ["805", 1],
"ace_duration_seconds": ["562", 0],
"ace_candidate_1": [ace_candidates[0], 0],
"ace_candidate_2": [ace_candidates[1], 0],
"ace_candidate_3": [ace_candidates[2], 0],
"ace_candidate_4": [ace_candidates[3], 0],
}
)
graph["818"]["inputs"]["source_policy"] = ["814", 8]
inherited = sorted(
(node_id, str(node.get("class_type", "")))
for node_id, node in graph.items()
if str(node.get("class_type", "")) in FORBIDDEN_RUNTIME_CLASSES
)
if inherited:
raise WorkflowError(f"Forbidden inherited runtime nodes entered Advertisement UI source: {inherited}")
return graph
def _definition(info: Mapping[str, Any], name: str) -> tuple[Any, dict[str, Any]] | None:
for section in ("required", "optional"):
raw = info.get("input", {}).get(section, {}).get(name)
if isinstance(raw, list) and raw:
config = raw[1] if len(raw) > 1 and isinstance(raw[1], Mapping) else {}
return raw[0], dict(config)
if "." in name:
group, _child = name.split(".", 1)
raw = info.get("input", {}).get("optional", {}).get(group)
if isinstance(raw, list) and len(raw) > 1 and isinstance(raw[1], Mapping):
template = raw[1].get("template", {})
required = template.get("input", {}).get("required", {})
if isinstance(required, Mapping) and required:
first = next(iter(required.values()))
if isinstance(first, list) and first:
config = first[1] if len(first) > 1 and isinstance(first[1], Mapping) else {}
return first[0], dict(config)
return None
def _ordered_inputs(info: Mapping[str, Any], api_inputs: Mapping[str, Any]) -> list[str]:
values: list[str] = []
order = info.get("input_order", {})
for section in ("required", "optional"):
for name in order.get(section, []):
if name in api_inputs:
values.append(str(name))
elif any(str(item).startswith(str(name) + ".") for item in api_inputs):
values.extend(
str(item) for item in api_inputs if str(item).startswith(str(name) + ".")
)
for name in api_inputs:
if str(name) not in values:
values.append(str(name))
return values
def _widget_compatible(type_name: Any, config: Mapping[str, Any]) -> bool:
if bool(config.get("forceInput")):
return False
if isinstance(type_name, list):
return True
return str(type_name) in {"STRING", "INT", "FLOAT", "BOOLEAN", "COMBO"}
def _default_widget(type_name: Any, config: Mapping[str, Any]) -> Any:
if "default" in config:
return copy.deepcopy(config["default"])
if isinstance(type_name, list):
return copy.deepcopy(type_name[0]) if type_name else ""
if str(type_name) == "COMBO":
options = config.get("options", [])
return copy.deepcopy(options[0]) if options else ""
return {"STRING": "", "INT": 0, "FLOAT": 0.0, "BOOLEAN": False}.get(str(type_name), "")
def _socket_type(type_name: Any, config: Mapping[str, Any]) -> str:
if isinstance(type_name, list) or str(type_name) == "COMBO":
return "COMBO"
return str(type_name)
def _cnr_id(info: Mapping[str, Any]) -> str:
module = str(info.get("python_module", ""))
if "ComfyUI-DiffusionGemmaPromptBuilder" in module:
return "diffusiongemma-prompt-builder"
if "easy" in module.casefold():
return "comfyui-easy-use"
if module.startswith("comfy") or module == "nodes":
return "comfy-core"
return module.rsplit(".", 1)[-1] or "comfy-core"
def _size_for(class_type: str, widget_count: int, input_count: int) -> list[float]:
explicit = {
"LoadImage": [430.0, 520.0],
"DiffusionGemmaAdvertisementCampaignContract": [620.0, 720.0],
"DiffusionGemmaAdvertisementReferenceContract": [620.0, 600.0],
"DiffusionGemmaAdvertisementReferenceAssetPrep": [620.0, 500.0],
"DiffusionGemmaAdvertisementSoundtrackContract": [620.0, 660.0],
"DiffusionGemmaAdvertisementSoundtrackSourceRouter": [650.0, 850.0],
"DiffusionGemmaAdvertisementMultiShotPlanner": [700.0, 880.0],
"MiniMaxH3ReferenceToVideo": [560.0, 620.0],
"DiffusionGemmaCoTGenerator": [520.0, 420.0],
"DiffusionGemmaH3ReferenceContext": [620.0, 620.0],
"DiffusionGemmaAdvertisementMediaQAGate": [620.0, 430.0],
"SaveVideo": [420.0, 170.0],
}
if class_type in explicit:
return explicit[class_type]
return [380.0, float(max(90, min(620, 70 + widget_count * 28 + input_count * 22)))]
def _position(api_id: str, class_type: str, index: int) -> list[float]:
fixed: dict[str, tuple[float, float]] = {
"179": (-3850, -2600), "721": (-3850, -1450), "800": (-3850, -300),
"801": (-3200, -2600), "802": (-3200, -1450), "803": (-2450, -1600),
"804": (-3200, 650), "805": (-2450, 650), "806": (-2450, 1120),
"807": (-2450, 1280), "808": (-2450, 1440), "809": (-1900, 900),
"810": (-1400, 1120), "811": (-1400, 1280), "812": (-980, 980),
"813": (-520, 1020), "814": (0, 650), "815": (760, 800),
"816": (1450, 900), "817": (1900, 760), "818": (2440, 800),
"819": (2950, 850), "844": (3400, 650), "845": (3400, 900),
"183": (-1850, -2600), "674": (-1150, -2600), "185": (-1850, -1800),
"182": (-1850, -1400), "673": (-1150, -1850), "675": (-500, -1850),
"186": (-350, -2600), "820": (300, -2600), "187": (950, -2600), "188": (1550, -2600),
"676": (1450, -2600), "821": (2200, -2600), "822": (2950, -2600),
"823": (3700, -2600), "418": (3050, -1750), "678": (3650, -1650),
"661": (2800, -1250), "713": (3250, -1250), "715": (3700, -1250),
"716": (4150, -1250), "662": (2800, -1050), "653": (2800, -850),
"654": (2800, -650), "710:657": (4550, -1250), "710:658": (4550, -1050),
"831": (6600, -2500), "832": (6600, -1750), "833": (7300, -2300),
"834": (7300, -1600), "835": (7950, -2300), "836": (8650, -2300),
"837": (8650, -1700), "838": (9150, -1700), "839": (8650, -1350),
"840": (9150, -1350), "841": (8650, -1000), "842": (9150, -1000),
"843": (9300, -2300), "844": (9300, 650), "845": (9300, 900),
"900": (1400, 1450), "901": (900, -2050), "902": (200, -2050),
"736": (-3200, 1600), "846": (-2450, 1850), "847": (-3200, 1850),
"848": (-3200, 2020), "851": (-500, -1450),
"849": (-2350, -3000),
}
if api_id in fixed:
return [float(fixed[api_id][0]), float(fixed[api_id][1])]
lane_map = {
"710:667": 0, "710:663": 0, "710:660": 0, "710:717": 0,
"710:659": 0, "710:656": 0,
"710:684": 1, "710:685": 1, "710:686": 1, "710:718": 1,
"710:687": 1, "710:688": 1,
"710:692": 2, "710:693": 2, "710:694": 2, "710:719": 2,
"710:695": 2, "710:696": 2,
"710:700": 3, "710:701": 3, "710:702": 3, "710:720": 3,
"710:703": 3, "710:704": 3,
}
if api_id in lane_map:
lane = lane_map[api_id]
y = -500 + lane * 1050
column = {
"MiniMaxH3ReferenceToVideo": 4300,
"RandomNoise": 4900,
"BasicGuider": 4900,
"FL_UnloadAllModels": 4900,
"SamplerCustomAdvanced": 5350,
"VAEDecode": 5750,
}.get(class_type, 5200)
if class_type in {"BasicGuider", "FL_UnloadAllModels"}:
y += 260 if class_type == "BasicGuider" else 500
return [float(column), float(y)]
relay_positions = {
"681": (3800, -300), "683": (3800, 750), "691": (3800, 1800), "699": (3800, 2850),
"825": (6150, 250), "826": (6600, 250), "827": (6150, 1300), "828": (6600, 1300),
"829": (6150, 2350), "830": (6600, 2350),
}
if api_id in relay_positions:
x, y = relay_positions[api_id]
return [float(x), float(y)]
if api_id.startswith("623:"):
try:
inner = int(api_id.split(":", 1)[1])
except ValueError:
inner = index
return [-1600.0 + float((inner % 4) * 420), 1650.0 + float(((inner // 4) % 6) * 280)]
if api_id in {"556", "558", "559", "560", "562", "565", "578", "589", "590", "591", "592", "609"}:
slot = sorted(["556", "558", "559", "560", "562", "565", "578", "589", "590", "591", "592", "609"]).index(api_id)
return [-2450.0 + float((slot % 4) * 430), 1650.0 + float((slot // 4) * 310)]
return [float(9600 + (index % 4) * 420), float(-2500 + (index // 4) * 260)]
def _api_sort_key(value: str) -> tuple[int, int, str]:
match = re.fullmatch(r"(\d+)(?::(\d+))?", value)
if match:
return int(match.group(1)), int(match.group(2) or -1), value
return 1_000_000, 0, value
def _topological_order(graph: Mapping[str, Any]) -> list[str]:
pending = set(graph)
ordered: list[str] = []
while pending:
ready = sorted(
[
node_id
for node_id in pending
if all(
not _is_connection(value) or value[0] not in pending
for value in graph[node_id].get("inputs", {}).values()
)
],
key=_api_sort_key,
)
if not ready:
raise WorkflowError("API graph contains a dependency cycle.")
ordered.extend(ready)
pending.difference_update(ready)
return ordered
def build_workflow(graph: dict[str, Any], object_info: Mapping[str, Any]) -> dict[str, Any]:
api_ids = sorted(graph, key=_api_sort_key)
numeric_ids = {api_id: index + 1 for index, api_id in enumerate(api_ids)}
topo = _topological_order(graph)
topo_order = {api_id: index for index, api_id in enumerate(topo)}
nodes: list[dict[str, Any]] = []
records: dict[str, dict[str, Any]] = {}
for index, api_id in enumerate(api_ids):
api_node = graph[api_id]
class_type = str(api_node.get("class_type", ""))
info = object_info.get(class_type)
if not isinstance(info, Mapping):
raise WorkflowError(f"Live ComfyUI has no node metadata for {class_type} ({api_id}).")
api_inputs = api_node.get("inputs", {})
input_records: list[dict[str, Any]] = []
widget_values: list[Any] = []
ordered_names = _ordered_inputs(info, api_inputs)
for name in ordered_names:
definition = _definition(info, name)
if definition is None:
# Some custom nodes persist frontend-only scalar widgets that
# are intentionally absent from the execution contract. A
# connected socket is different: ComfyUI forwards it to the
# node function, so an unknown connected name is a runtime
# TypeError waiting to happen. Fail that case closed while
# preserving established frontend-only widget state.
if _is_connection(api_inputs.get(name)):
raise WorkflowError(
f"Live ComfyUI {class_type} has no connected input {name} ({api_id})."
)
if name in api_inputs:
widget_values.append(copy.deepcopy(api_inputs[name]))
continue
type_name, config = definition
value = api_inputs.get(name)
socket_type = _socket_type(type_name, config)
if _is_connection(value):
item: dict[str, Any] = {"name": name, "type": socket_type, "link": None}
if _widget_compatible(type_name, config):
item["widget"] = {"name": name}
if "." in name:
item["localized_name"] = name
item["label"] = name.split(".", 1)[1]
input_records.append(item)
elif bool(config.get("forceInput")):
# Guarded above by explicit primitive nodes in this workflow.
if value is not None:
raise WorkflowError(f"Force-input scalar {api_id}:{name} needs a primitive connection.")
if _widget_compatible(type_name, config):
widget_values.append(
copy.deepcopy(value) if value is not None and not _is_connection(value) else _default_widget(type_name, config)
)
if bool(config.get("control_after_generate")):
widget_values.append("fixed")
# Current ComfyUI serializes both LoadImage's ordinary image widget and
# its frontend-only upload widget as input descriptors. Widget values
# alone preview correctly but the media validator reports a false
# missing-image error unless these exact descriptors are also present.
if class_type == "LoadImage":
input_records = [
{
"localized_name": "image",
"name": "image",
"type": "COMBO",
"widget": {"name": "image"},
"link": None,
},
{
"localized_name": "choose file to upload",
"name": "upload",
"type": "IMAGEUPLOAD",
"widget": {"name": "upload"},
"link": None,
},
]
if len(widget_values) == 1:
widget_values.append("image")
output_types = list(info.get("output", []))
output_names = list(info.get("output_name", []))
outputs = []
for slot, output_type in enumerate(output_types):
name = str(output_names[slot] if slot < len(output_names) else output_type)
outputs.append({"name": name, "type": _socket_type(output_type, {}), "links": []})
size = _size_for(class_type, len(widget_values), len(input_records))
title = str(api_node.get("_meta", {}).get("title") or info.get("display_name") or class_type)
node = {
"id": numeric_ids[api_id],
"type": class_type,
"pos": _position(api_id, class_type, index),
"size": size,
"flags": {},
"order": topo_order[api_id],
"mode": 0,
"inputs": input_records,
"outputs": outputs,
"title": title,
"properties": {
"cnr_id": _cnr_id(info),
"Node name for S&R": class_type,
"diffusiongemma_api_node_id": api_id,
},
"widgets_values": widget_values,
}
nodes.append(node)
records[api_id] = node
links: list[list[Any]] = []
next_link = 0
for target_api_id in api_ids:
target = records[target_api_id]
api_inputs = graph[target_api_id].get("inputs", {})
for input_name, value in api_inputs.items():
if not _is_connection(value):
continue
origin_api_id, origin_slot = value
if origin_api_id not in records:
raise WorkflowError(f"Dangling API origin {origin_api_id} -> {target_api_id}:{input_name}.")
origin = records[origin_api_id]
if int(origin_slot) >= len(origin["outputs"]):
raise WorkflowError(f"Invalid output slot {origin_api_id}:{origin_slot}.")
target_slots = [
slot for slot, item in enumerate(target["inputs"]) if item.get("name") == input_name
]
if len(target_slots) != 1:
raise WorkflowError(f"UI socket reconstruction failed for {target_api_id}:{input_name}.")
target_slot = target_slots[0]
next_link += 1
link_type = str(origin["outputs"][int(origin_slot)].get("type") or target["inputs"][target_slot].get("type") or "*")
links.append(
[
next_link,
int(origin["id"]),
int(origin_slot),
int(target["id"]),
int(target_slot),
link_type,
]
)
target["inputs"][target_slot]["link"] = next_link
origin["outputs"][int(origin_slot)]["links"].append(next_link)
note_id = len(nodes) + 1
note = {
"id": note_id,
"type": "MarkdownNote",
"pos": [-3850.0, -3300.0],
"size": [1850.0, 560.0],
"flags": {},
"order": 0,
"mode": 0,
"inputs": [],
"outputs": [],
"title": "START HERE — separate Advertisement system / MiniMax Music 3 default",
"properties": {},
"widgets_values": [
"# DiffusionGemma Advertisement — MiniMax Music 3 + MiniMax H3\n\n"
"This is a new, flattened workflow with its own workflow UUID. The known-good V6 music-video workflow is not a parent graph and is never overwritten.\n\n"
"1. Picture 1 is the performer hero. Picture 2 is a same-performer identity sheet. Picture 3 is a product-only contact sheet. Picture 4 is created automatically from the exact retained prior-lane tail and is continuity evidence only.\n"
"2. Edit CAMPAIGN, REFERENCES, and SOUNDTRACK. MiniMax Music 3 is the default. SONG SOURCE also exposes Upload song and Legacy ACE-Step; unselected branches remain lazy.\n"
"3. Use ADVERTISEMENT CONTROLS as the single duration, native-shot, and performance authority. The default 30-second master uses eight native H3 shots inside two 15-second generation lanes. EasyCache and TorchCompile are absent from the baseline because they provided no measured gain.\n"
"4. The motion guide is music-only. Optional non-diegetic VO is mixed only into the delivery master and never fed to H3.\n"
"5. Exact campaign copy is rendered in the deterministic three-second end card. The workflow saves review drafts of the master plus real 15s and 6s cutdowns. Requested 1:1/16:9 adaptations remain explicitly unrendered until a semantic reframe renderer is connected.\n"
"6. QA defaults visual identity, copy legibility, and sync to not_measured. Saved files are review drafts; every delivery pass requires named evidence."
],
}
nodes.append(note)
groups = [
{"id": 1, "title": "1. Campaign + independent performer/product references", "bounding": [-3950, -3400, 4200, 3250], "color": "#d66b3d", "font_size": 36, "flags": {}},
{"id": 2, "title": "2. MiniMax Music 3 default + lazy Upload / ACE fallbacks", "bounding": [-3300, 500, 7300, 3400], "color": "#4f8cc9", "font_size": 36, "flags": {}},
{"id": 3, "title": "3. Advertisement Director + typed H3 plan", "bounding": [-1950, -2800, 6500, 1750], "color": "#8064b4", "font_size": 36, "flags": {}},
{"id": 4, "title": "4. Four-reference H3 lanes + persistent retained-tail relay", "bounding": [2700, -1400, 4300, 5200], "color": "#b88632", "font_size": 36, "flags": {}},
{"id": 5, "title": "5. Deterministic end card, exact master, real cutdowns, honest QA", "bounding": [6500, -2700, 3400, 2050], "color": "#4d9a75", "font_size": 36, "flags": {}},
]
workflow = {
"id": WORKFLOW_UUID,
"revision": 1,
"last_node_id": note_id,
"last_link_id": next_link,
"nodes": nodes,
"links": links,
"groups": groups,
"config": {},
"extra": {
SCHEMA: {
"version": VERSION,
"known_good_v6_file_sha256": KNOWN_GOOD_UI_SHA256,
"known_good_v6_is_never_overwritten": True,
"field_test_api_canonical_sha256": FIELD_TEST_API_CANONICAL_SHA256,
"music3_api_canonical_sha256": MUSIC3_API_CANONICAL_SHA256,
"workflow_uuid_is_fresh": True,
"graph_form": "flattened_independent_no_shared_subgraph_uuid",
"audio_source_default": "MiniMax Music 3",
"fallbacks": ["Upload song", "Legacy ACE-Step"],
"easycache_default": "absent_measured_1.00x",
"torchcompile_default": "absent_outside_baseline",
"advertisement_memory_barriers": 5,
},
"workflow_note": (
"Separate Advertisement workflow: MiniMax Music 3 default; independent P1/P2 performer and P3 product roles; "
"persistent P4 retained-tail relay; deterministic copy finishing; exact 30/15/6 deliverables; V6 untouched."
),
},
"version": 0.4,
}
validate_workflow(workflow)
return workflow
def _origin(workflow: Mapping[str, Any], node: Mapping[str, Any], input_name: str) -> Mapping[str, Any]:
slots = [index for index, item in enumerate(node.get("inputs", [])) if item.get("name") == input_name]
if len(slots) != 1:
raise WorkflowError(f"Expected one input {node.get('id')}:{input_name}.")
link_id = node["inputs"][slots[0]].get("link")
link = next((item for item in workflow.get("links", []) if int(item[0]) == int(link_id)), None)
if link is None:
raise WorkflowError(f"Missing link for {node.get('id')}:{input_name}.")
origin = next((item for item in workflow.get("nodes", []) if int(item["id"]) == int(link[1])), None)
if origin is None:
raise WorkflowError("Workflow link has a missing origin node.")
return origin
def validate_workflow(workflow: Mapping[str, Any]) -> None:
if workflow.get("id") != WORKFLOW_UUID or int(workflow.get("revision", 0)) != 1:
raise WorkflowError("Advertisement workflow must keep its fresh revision-1 UUID.")
if workflow.get("id") == _load_guarded(KNOWN_GOOD_UI, byte_hash=KNOWN_GOOD_UI_SHA256).get("id"):
raise WorkflowError("Advertisement workflow reused the known-good V6 UUID.")
nodes = list(workflow.get("nodes", []))
types = [str(node.get("type", "")) for node in nodes]
for required in (
"DiffusionGemmaAdvertisementWorkflowControls",
"DiffusionGemmaAdvertisementCampaignContract",
"DiffusionGemmaAdvertisementReferenceAssetPrep",
"DiffusionGemmaAdvertisementSoundtrackSourceRouter",
"MiniMaxMusic3TextEncode",
"DiffusionGemmaAdvertisementMultiShotPlanner",
"DiffusionGemmaAdvertisementRelayArtifact",
"DiffusionGemmaAdvertisementEndCardRenderer",
"DiffusionGemmaAdvertisementCutdownRenderer",
"DiffusionGemmaAdvertisementMediaQAGate",
):
if required not in types:
raise WorkflowError(f"Advertisement UI workflow is missing {required}.")
forbidden = sorted(set(types).intersection(FORBIDDEN_RUNTIME_CLASSES))
if forbidden:
raise WorkflowError(f"Forbidden acceleration/convenience runtime nodes entered the Advertisement UI baseline: {forbidden}")
if types.count("DiffusionGemmaAdvertisementMemoryBarrier") != 5:
raise WorkflowError("Advertisement UI workflow must retain five self-contained memory barriers.")
if types.count("SaveVideo") != 3:
raise WorkflowError("Advertisement UI workflow must save master, 15s, and 6s videos.")
if types.count("DiffusionGemmaAdvertisementWorkflowControls") != 1:
raise WorkflowError("Advertisement UI workflow must have one shared settings authority.")
by_api_id = {
str(node.get("properties", {}).get("diffusiongemma_api_node_id", "")): node
for node in nodes
}
by_id = {int(node["id"]): node for node in nodes}
for link in workflow.get("links", []):
origin = by_id.get(int(link[1]))
target = by_id.get(int(link[3]))
if origin is None or target is None:
raise WorkflowError("Advertisement UI link references a missing node.")
origin_type = str(origin["outputs"][int(link[2])].get("type", "*"))
target_type = str(target["inputs"][int(link[4])].get("type", "*"))
link_type = str(link[5])
if origin_type != "*" and target_type != "*" and origin_type != target_type:
raise WorkflowError(
f"Advertisement UI link type mismatch: {origin_type} -> {target_type} "
f"at {origin.get('type')}:{link[2]} -> {target.get('type')}:{link[4]}."
)
if link_type != origin_type and origin_type != "*":
raise WorkflowError("Advertisement UI link metadata does not match its origin socket type.")
for api_id, input_name in (
("801", "production_duration_seconds"),
("804", "target_duration_seconds"),
("673", "target_duration_seconds"),
("676", "production_duration_seconds"),
("815", "excerpt_duration_seconds"),
("816", "duration"),
("817", "target_duration_seconds"),
("818", "excerpt_duration_seconds"),
("673", "custom_shot_count"),
("821", "native_shot_count"),
("675", "performance_mode"),
("823", "performance_mode"),
("835", "cutdown_15_start_seconds"),
("835", "cutdown_6_start_seconds"),
("801", "deliverables_json"),
("676", "deliverables_json"),
("801", "aspect_ratio"),
("676", "aspect_ratio"),
("673", "generation_mode"),
("673", "shot_count"),
("673", "shot_count_override"),
("673", "audio_mode"),
("673", "dialogue_mode"),
("676", "excerpt_start_seconds"),
("676", "generation_model"),
("676", "max_h3_shot_seconds"),
("187", "resolution_aspect_ratio_override"),
):
if api_id not in by_api_id or _origin(workflow, by_api_id[api_id], input_name).get("type") != "DiffusionGemmaAdvertisementWorkflowControls":
raise WorkflowError(f"Advertisement UI shared control drifted at {api_id}:{input_name}.")
router = next(node for node in nodes if node.get("type") == "DiffusionGemmaAdvertisementSoundtrackSourceRouter")
if not router.get("widgets_values") or router["widgets_values"][0] != "MiniMax Music 3":
raise WorkflowError("MiniMax Music 3 must remain the saved soundtrack default.")
for name in ("music3_candidate_1", "uploaded_audio", "ace_candidate_1"):
_origin(workflow, router, name)
h3_nodes = [node for node in nodes if node.get("type") == "MiniMaxH3ReferenceToVideo"]
if len(h3_nodes) != 4:
raise WorkflowError("Advertisement UI workflow must retain four lazy H3 generation lanes.")
h3_nodes.sort(key=lambda node: float(node.get("pos", [0, 0])[1]))
for lane, node in enumerate(h3_nodes, start=1):
for name in ("ref_images.ref_image_0", "ref_images.ref_image_1", "ref_images.ref_image_2"):
_origin(workflow, node, name)
relay_slots = [item for item in node.get("inputs", []) if item.get("name") == "ref_images.ref_image_3"]
if lane == 1 and relay_slots:
raise WorkflowError("Lane 1 cannot consume relay Picture 4.")
if lane > 1 and len(relay_slots) != 1:
raise WorkflowError(f"Lane {lane} must consume persistent relay Picture 4.")
marker = workflow.get("extra", {}).get(SCHEMA, {})
if marker.get("known_good_v6_file_sha256") != KNOWN_GOOD_UI_SHA256 or not marker.get("known_good_v6_is_never_overwritten"):
raise WorkflowError("Advertisement UI lineage guard is incomplete.")
def _write_guarded(path: Path, payload: bytes, *, force: bool) -> str:
path = path.resolve()
path.parent.mkdir(parents=True, exist_ok=True)
existed = path.exists()
if existed:
current = path.read_bytes()
if current == payload:
return "verified"
if not force:
raise WorkflowError(f"Refusing to overwrite a different Advertisement workflow: {path}")
try:
existing = json.loads(current.decode("utf-8"))
marker = existing.get("extra", {}).get(SCHEMA, {})
except (UnicodeDecodeError, json.JSONDecodeError, AttributeError) as exc:
raise WorkflowError(f"Refusing to replace an unrecognized artifact: {path}") from exc
if marker.get("version") != VERSION or existing.get("id") != WORKFLOW_UUID:
raise WorkflowError(f"Refusing to replace an artifact without Advertisement ownership: {path}")
descriptor, temporary = tempfile.mkstemp(prefix=path.name + ".", suffix=".tmp", dir=path.parent)
try:
with os.fdopen(descriptor, "wb") as handle:
handle.write(payload)
handle.flush()
os.fsync(handle.fileno())
os.replace(temporary, path)
finally:
if os.path.exists(temporary):
os.unlink(temporary)
return "replaced" if existed else "created"
def main(argv: list[str] | None = None) -> int:
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument("--object-info-url", default="http://127.0.0.1:8191")
parser.add_argument("--output", type=Path, default=OUTPUT)
parser.add_argument("--check", action="store_true")
parser.add_argument("--force", action="store_true")
args = parser.parse_args(argv)
_load_guarded(KNOWN_GOOD_UI, byte_hash=KNOWN_GOOD_UI_SHA256)
field_test = _load_guarded(FIELD_TEST_API, canonical_hash=FIELD_TEST_API_CANONICAL_SHA256)
music3 = _load_guarded(MUSIC3_API, canonical_hash=MUSIC3_API_CANONICAL_SHA256)
graph = _with_lazy_fallbacks(music3, field_test)
workflow = build_workflow(graph, _object_info(args.object_info_url))
payload = _json_bytes(workflow)
summary = {
"workflow_id": workflow["id"],
"nodes": len(workflow["nodes"]),
"links": len(workflow["links"]),
"sha256": _sha256_bytes(payload),
"canonical_sha256": _canonical_sha256(workflow),
"known_good_v6_sha256": KNOWN_GOOD_UI_SHA256,
}
if args.check:
print(json.dumps(summary, sort_keys=True))
return 0
summary["action"] = _write_guarded(args.output, payload, force=args.force)
summary["path"] = str(args.output.resolve())
print(json.dumps(summary, sort_keys=True))
return 0
if __name__ == "__main__": # pragma: no cover
raise SystemExit(main())
@@ -0,0 +1,120 @@
"""Export a completed ComfyUI prompt as an immutable advertisement API fixture.
The exported prompt graph remains directly submit-able to ComfyUI. A sibling
manifest records its runtime provenance and canonical SHA-256 without adding
non-node keys to the graph itself.
"""
from __future__ import annotations
import argparse
import hashlib
import json
import pathlib
import urllib.request
from typing import Any
KNOWN_GOOD_V6_SHA256 = (
"594f6aef94531f87b74659e1e8004cccdff00716d0cfe263cee6bf2149545c83"
)
def _canonical_json(value: Any) -> str:
return json.dumps(value, ensure_ascii=False, sort_keys=True, separators=(",", ":"))
def _sha256_bytes(value: bytes) -> str:
return hashlib.sha256(value).hexdigest()
def _read_json(url: str) -> Any:
with urllib.request.urlopen(url, timeout=30) as response:
return json.loads(response.read().decode("utf-8"))
def _parse_args() -> argparse.Namespace:
parser = argparse.ArgumentParser()
parser.add_argument("--prompt-id", required=True)
parser.add_argument("--output", type=pathlib.Path, required=True)
parser.add_argument("--base-url", default="http://127.0.0.1:8188")
parser.add_argument(
"--known-good-ui",
type=pathlib.Path,
default=pathlib.Path(__file__).resolve().parents[1]
/ "examples"
/ "15_minimax_h3_ref2va_music_video_v6.json",
)
parser.add_argument("--force", action="store_true")
return parser.parse_args()
def main() -> int:
args = _parse_args()
ui_bytes = args.known_good_ui.read_bytes()
ui_hash = _sha256_bytes(ui_bytes)
if ui_hash != KNOWN_GOOD_V6_SHA256:
raise SystemExit(
"Known-good V6 hash mismatch; refusing to derive an advertisement fixture. "
f"Expected {KNOWN_GOOD_V6_SHA256}, received {ui_hash}."
)
output = args.output.resolve()
manifest_path = output.with_suffix(".manifest.json")
if not args.force and (output.exists() or manifest_path.exists()):
raise SystemExit("Output already exists; pass --force only for an intentional refresh.")
history = _read_json(
f"{args.base_url.rstrip('/')}/history/{args.prompt_id}"
)
entry = history.get(args.prompt_id)
if not isinstance(entry, dict):
raise SystemExit(f"Prompt {args.prompt_id} was not found in ComfyUI history.")
prompt_record = entry.get("prompt")
if not isinstance(prompt_record, list) or len(prompt_record) < 3:
raise SystemExit("History entry does not contain an API prompt graph.")
graph = prompt_record[2]
if not isinstance(graph, dict) or not graph:
raise SystemExit("History API prompt graph is empty or malformed.")
status = entry.get("status", {})
if not isinstance(status, dict) or not bool(status.get("completed")):
raise SystemExit("Only a completed prompt may become a runtime fixture.")
graph_hash = _sha256_bytes(_canonical_json(graph).encode("utf-8"))
class_counts: dict[str, int] = {}
for node in graph.values():
if not isinstance(node, dict):
raise SystemExit("History prompt contains a malformed node record.")
class_type = str(node.get("class_type", ""))
if not class_type:
raise SystemExit("History prompt contains a node without class_type.")
class_counts[class_type] = class_counts.get(class_type, 0) + 1
manifest = {
"schema": "diffusiongemma.advertisement_runtime_fixture",
"version": 1,
"prompt_id": args.prompt_id,
"completed": True,
"api_graph_sha256": graph_hash,
"api_node_count": len(graph),
"class_counts": dict(sorted(class_counts.items())),
"known_good_ui_path": args.known_good_ui.resolve().as_posix(),
"known_good_ui_sha256": ui_hash,
"source": "local_comfyui_history",
}
output.parent.mkdir(parents=True, exist_ok=True)
output.write_text(
json.dumps(graph, ensure_ascii=False, indent=2) + "\n", encoding="utf-8"
)
manifest_path.write_text(
json.dumps(manifest, ensure_ascii=False, indent=2) + "\n",
encoding="utf-8",
)
print(json.dumps(manifest, ensure_ascii=False, indent=2))
return 0
if __name__ == "__main__":
raise SystemExit(main())