* refactor(loop-audio): card reads only multilines + Koolook Director (#177)
The audio-lipsync card is now bounded to two source families: the five
tracked Text Multiline nodes and the LTX Director (Koolook v1.3.2)
node's own widgets + input wiring. SAMPLER block (BasicScheduler /
KSamplerSelect / RandomNoise / CFGGuider scrapes), FORK STATE block
(_dev_build.json + git status), and INERT warning are gone — they were
either widget scrapes outside the two source families or duplicates of
the Director's own state.
New card layout (top to bottom):
HEADER run NNN — {name} + date + workflow filename
KNOB STATE relay_overrides (the per-render knob)
BASE · NOTES overlay - info verbatim, blank lines preserved
BASE · LOCKED epsilon · Audio src · Working folder (path-wrapped)
BASE · SCENE Segments (N) parent, indented `1) 0 to 5 seconds`
child list, flat Prompt/Audio/Keyframe coverage
POST-RENDER overlay - feedback + parsed motion/sync/sharp
Audio src now derived structurally from a 4-state machine
(audio_vae link × use_custom_audio × audioSegments count) that mirrors
ltx_director.py:610-622 — never from substring matches on prompt text.
Per-segment Audio coverage checks audioSegments[] overlap, not the
maintainer's [Audio]: prompt formatting.
Also fixed: upstream-LTXDirector widget index bug (saved workflow keeps
legacy order even after Koolook fork reordered the schema; DIRECTOR_WIDX
matches the serialised order); loop_audio.py's .env discovery now falls
back to the main repo when run from a worktree (same pattern as
make_card.py); notes.md drops the widget-scraped Mechanical interp
section; log.md columns realigned (Audio src + Segments replace Phase 1
/ Phase 2 / Custom audio).
Three new docs alongside the module:
reading-graph.html visual flow (dark theme, color-coded bands)
reading-graph.schema.yaml machine-readable mirror of the same graph
CHEATSHEET.md refreshed against the new design
All 231 tests pass.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* fix(lint): split semicolon statements + drop unused symbols in make_card_audio
Ruff (project's Lint & Smoke gate) flagged 10 issues in the refactored
card renderer:
- F401 unused imports — `typing.Optional` at the top of the file and
`loop_audio.REPO_ROOT` in the standalone CLI's import block.
- F841 unused locals — `dur_f` and `dur_s` were left over from the
dropped "Duration" row; segment time ranges convey the same info now.
Kept in the state dict for notes.md; not read in the renderer.
- E702 six semicolon-joined statements in the HEADER and POST-RENDER
sections (`draw.text(...); y += N`). Split into separate lines.
Rendering is unchanged — verified by re-running the standalone CLI
against run-002. All 249 tests still pass; ruff now clean.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* fix(loop-audio): resolve PR #185 multi-agent review findings
Anchored by the verified multi-agent review on PR #185 — every fix
traces to a confirmed finding (false-positive rate was 0/11).
HIGH:
- derive_audio_state now distinguishes "(no director)" from "off (no
VAE)". Pre-fix, is_input_wired(None, ...) returned None, the
truthiness check collapsed director-missing into VAE-unwired, and
the card / log silently agreed on the wrong story. Added an early
return when director_node is None. Also documented that vae_wired
can be None (input socket absent in older schemas) and that we
intentionally collapse it with False at the unwired branch — after
the missing-director case has been split off.
- extract_multilines now iterates needles longest-first. Pre-fix the
per-node `break` was order-dependent on config insertion: a future
short substring (e.g. "relay") in the same tracked_multilines list
would have silently shadowed "relay_overrides". Sorting by length
descending removes that footgun without breaking the current 5-needle
layout.
- parse_timeline now coerces per-segment numeric fields (start /
length / trimStart) to int via int(float(...)) with try/except → 0
fallback. The upstream Director itself does this defensively at
ltx_director.py:232-234; without coercion a string-valued field
silently breaks video_segment_has_audio's `<` comparisons
(lexical) and crashes the renderer at `start / fps`. Also drops
non-dict entries from segments / audioSegments arrays so a
malformed JSON can't slip past json.loads + isinstance checks.
- Duplicated wrap_path + video_segment_has_audio bodies in
make_card_audio.py are gone. Renderer now imports them at module
level from loop_audio so the two scripts agree byte-for-byte on
path-wrapping rules and audio-overlap geometry.
MEDIUM:
- DIRECTOR_WIDX now carries a MIRROR: comment pointing at the YAML
schema's `widget_order:` block (the cross-link the review asked for
as the minimum drift-prevention fix).
- duration_frames / duration_seconds removed from the state dict and
from render_audio_card's docstring. The renderer dropped the
Duration row earlier in the PR; notes.md reads them straight from
director_node via director_widget, so keeping them in state was
documenting a contract the renderer didn't honour. YAML schema now
splits them out under a notes_only section to match.
- Log-row score formatting now uses an explicit `is not None` check
(`_score()` helper) instead of `or '?'` — 0 was a legitimate score
the maintainer could type that pre-fix collapsed to '?' because 0
is falsy. Matches the card's existing `_s()` helper for parity.
- Card already surfaces the missing-Director state distinctly thanks
to derive_audio_state returning "(no director)" — the BASE · LOCKED
Audio src row shows that label loudly when applicable. No
additional row needed.
LOW:
- Renderer falls back to "1) frames 0-120 (no fps)" when frame_rate
is None instead of computing "0 to 0 seconds" silently.
Test coverage:
- New tests/scripts/test_loop_audio.py — 41 tests covering all five
audio_src states, multiline ordering edge cases, parse_timeline
coercion + malformed inputs, video_segment_has_audio overlap
boundaries, parse_feedback (with explicit zero-score regression
test), is_input_wired None vs False semantics, wrap_path
separator-only wrapping, pick_existing_path filesystem fallback,
autogen_label slug derivation.
Docs:
- reading-graph.html lists five audio states (was four).
- reading-graph.schema.yaml audio_state_machine adds "(no director)"
state + director_present input; state_dict moves duration_frames /
duration_seconds into a notes_only section; invariants block grows
with the missing-director split, type-coercion guarantee, longest-
needle-wins guarantee, and zero-score preservation.
- CHEATSHEET.md Audio src table grows from 4 to 5 rows.
Validation:
- 290 tests pass (was 249 — 41 new + 0 regressions).
- ruff clean on scripts/loop_audio.py, scripts/make_card_audio.py,
tests/scripts/test_loop_audio.py.
- Standalone CLI re-renders run-002 card identically (same workflow,
same pixels).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>