961a95b9bd0d64c19a93eb7f8f5434fcc52eb06e
15
Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
ba37ec3d41 |
feat: LTX Director 2.0.2 fork, keyframe grid-snap, and audio→video bind schedule (#259)
* feat(forks): LTX Director v2_0_2 fork + keyframe latent-grid snap (#258) Bump the WhatDreamsCost Koolook fork to upstream 2.0.2 (pinned fe09f73) in a new versions/v2_0_2 namespace, and add `snap_keyframes_to_grid` (default on) to fix hard-pinned image keyframes "jumping" at latent-bucket edges. Why: a Director image keyframe is injected as a single latent frame at the segment's left edge, quantized to the model temporal stride s (L(f)=0 if f==0 else 1+(f-1)//s). Pins near a bucket boundary round ambiguously and two pins in one bucket collide. Snapping each pin to its bucket center removes the ambiguity without changing which latent frame it lands on; colliding pins are bumped to the next free bucket with a warning. - forks/whatdreamscost_koolook/versions/v2_0_2/: vendored 2.0.2 sources with the v1.3.9 Koolook deltas carried forward (relay_overrides, audio_transcript_json, per-segment sigma — build_segments is byte-identical 1.3.9->2.0.2 so the sigma patch drops in verbatim), plus new pure keyframe_grid.py snapper wired into the insert_frames build. - Canonical LTXDirector__koolook rolls to 2.0.2 (entrypoint loads v2_0_2 last); LTXDirector__koolook_v1_3_2 alias preserved; v1.3.9 kept on disk. - LICENSE re-verified GPL-3.0 (2026-06-22). Manifest + THIRD_PARTY + CHANGELOG + audio-lipsync findings/CHEATSHEET updated. - 37 new unit tests for the snapper; 107 fork tests pass. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * fix(forks): append Koolook LTX Director widgets last for clean load Move the v2_0_2 Koolook widgets (relay_overrides, audio_transcript_json, snap_keyframes_to_grid) to the end of the schema, after override_audio. They were inserted after img_compression, which shifted the last upstream widget when an upstream-2.0.x-saved workflow is loaded onto LTXDirector__koolook. With them last, an upstream graph's positional widgets_values map onto the upstream widgets untouched and only the Koolook widgets append with defaults (snap_keyframes_to_grid=on). Execution is unaffected (kwargs by name). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * refactor(forks): make v2_0_2 a clean upstream-2.0.2 replica + grid-snap only Per maintainer direction: the v2_0_2 Koolook LTX Director should faithfully replicate upstream 2.0.2 (so all latest features work) rather than carry the older customizations, which are unused. - Drop relay_overrides, audio_transcript_json, and the per-segment sigma patch. prompt_relay.py and patches.py are now vendored VERBATIM from upstream 2.0.2; _relay_overrides.py removed. - ltx_director.py is upstream 2.0.2 verbatim except two additive changes: the namespaced node ID LTXDirector__koolook, and the snap_keyframes_to_grid latent-bucket keyframe snap (issue #258), appended last in the schema. - v2_0_2 registers only LTXDirector__koolook; the legacy LTXDirector__koolook_v1_3_2 ID stays backed by the v1_3_9 package. v1.3.9 (with the older customizations) remains on disk for rollback. - Manifest / THIRD_PARTY / UPSTREAM_PIN / CHANGELOG updated to the clean scope. grid-snap unit tests unchanged (107 fork tests pass). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * feat(forks): vendor upstream 2.0.2 timeline editor JS for LTXDirector__koolook The v2_0_2 Python was upstream-exact but the node still rendered with the old 1.3.9-era timeline editor (fewer toolbar actions, no IC-LoRA track, leaked *_second widgets, frame_rate NaN). Replace web/whatdreamscost_koolook/ ltx_director.js with upstream 2.0.2's js/ltx_director.js vendored verbatim, retargeted with only two edits: unique extension name 'LTXDirector_Koolook' and node guard nodeData.name === 'LTXDirector__koolook'. UX now matches upstream 2.0.2 exactly; only the Python differs (grid-snap). The old koolook base64-scrub deltas are dropped as obsolete — upstream 2.0.2 persists timeline media via imageFile/audioFile + /api/view URLs (saved timelines are already lean), and the localStorage quota guard stays in the global web/koolook_draft_guard.js (upstream does not override localStorage). Rewrote tests/sidebar/test_ltx_director_draft_guard.py to the new contract (no embedded guard, retargeted to the koolook node, pinned-upstream provenance). 110 fork+contract tests pass. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * fix(scripts): resolve .env from main repo when dev-sync runs in a worktree Unrelated to the LTX work: plain `dev-sync` failed with "KOLOOK_COMFYUI_DEV_PATH not set" from any worktree because scripts/sync_to_dev.py only loaded `.env` from the worktree root, where the committed `.env` does not exist (it lives in the main checkout). sync_to_dev_audio.py already had the worktree->main fallback, which is why dev-sync-audio worked. Move that fallback into a shared sync_to_dev.find_dotenv() and use it in both scripts so they can't drift. Adds regression tests for the worktree fallback, the worktree-own-.env preference, and the not-a-worktree case. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * chore: gitignore root-level generated workflow JSONs Node-swapped/converted workflow copies (e.g. *.koolook.json) are dropped at the repo root during dev iteration; they're scratch deliverables, not package content. Ignore root-level *.json only (leading `/`), with a `!config.json` exception, so the 30+ tracked JSON in subdirs (test fixtures, web/, .claude/, docs/runs/*/workflow.json) stay tracked. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * feat: LTX A/V Bind Schedule node — gate audio→video binding by sigma New Koolook node LTXAVBindSchedule decouples big motion from lip-sync in LTX 2.3. The AV transformer (comfy/ldm/lightricks/av_model.py, BasicAVTransformerBlock) adds audio into video via a gated `audio_to_video_attn` per block, run only when transformer_options["a2v_cross_attn"] is set. This node patches each block's audio_to_video_attn.forward to scale its output by a per-step gain, and a unet-function-wrapper computes that gain from the current sigma: early/high-sigma steps run near `early_gain` (coarse motion settles audio-blind), ramping (smoothstep) to full at low sigma (lips bind onto settled motion). One pass, no re-noise — the fix for keyframe transitions jumping on audio peaks. early_gain=1.0 reproduces stock behavior; a graph without the node is unchanged. Registered as a root node + added to RUNTIME_PATHS so dev-sync ships it. Pure ramp (a2v_gain) unit-tested (9 cases); model wiring validated by live render. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * feat: LTX A/V Bind Schedule — add transition-window mode (lips synced + smooth) The global sigma ramp decoupled motion from lips but desynced the mouth (lip timing is coarse motion too, so holding audio out globally moved the mouth audio-blind). Add a WINDOWED mode: audio binds at full strength everywhere (lips sync across the whole clip) EXCEPT a +/-transition_window band around each keyframe transition, where the sigma ramp applies — so only those moments are motion-protected early and rejoined late. Implemented as a per-latent-frame gain on a2v_out: read grid_sizes from transformer_options to map frame-major video tokens -> latent frames, build a gain vector (1.0 outside bands, smoothstep-tapered to sigma_gain at each transition center), expand per-frame -> per-token, scale. New optional inputs: transition_frames, total_frames, transition_window. Empty transition_frames = prior global-ramp behavior. windowed_frame_gains unit-tested (7 cases). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * feat: LTXAVBindSchedule — one-shot diagnostic log (shapes/grid/mode) Logs out.shape, grid_sizes, transition_fracs, and whether the per-frame mask fired vs fell back to scalar — so a single render reveals whether the a2v patch hooks the model and whether the windowed token->frame mapping aligns, instead of guessing blind. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * docs(audio-lipsync): record validated a2v bind-schedule knee (0.35/0.25/0.55) Promote the finding that LTX 2.3's audio->video coupling must be sigma-ramped (not full-on) to keep motion smooth AND lips synced, with the validated knee early_gain=0.35 / bind_start=0.25 / bind_end=0.55 on the 8-step base pass, plus the trade-axis table and the per-frame-mode caveat (grid_sizes None + appended guide frames -> scalar fallback). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * feat(forks): keyframe ease — strength-ramped neighbor pins to de-robotic transitions Adds `keyframe_ease` / `ease_falloff` to the Koolook LTX Director (default off). Each hard image pin keeps its exact center pose, but emits same-pose neighbor pins one latent bucket apart at decreasing strength (center*falloff**k), so the model glides into and out of the locked pose instead of snapping to a dead stop — softening the "stop/dissolve/stop" robotic feel without softening the pose. Implemented as a pure list expansion (`expand_keyframe_ease` in keyframe_grid.py, 6 unit tests) applied to guide_data after grid-snap, so the upstream guide node is untouched (no fork). Center stays exact; neighbors that fall off-clip or onto another center are skipped. Costs extra guide frames (more compute), hence opt-in. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * docs(audio-lipsync): downgrade a2v knee to 'promising' — note lows-placement confound The 0.35/0.25/0.55 win was measured on a clip whose transitions sit on audio lows, so 'no jumps' may be the placement, not the node. Mark the controlled on-peak test (early_gain 1.0 vs 0.35 on the same on-peak layout) as still pending. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * chore: mark vendored WhatDreamsCost fork files linguist-vendored The koolook Director ships upstream 2.0.2 code (the 8.5k-line timeline-editor JS + verbatim prompt_relay/patches) so the pack runs and distributes, but it's upstream, not authored here. Mark those linguist-vendored (drop from language stats) and the JS -diff (collapse the large blob in PR diffs). Our edited/new files stay attributed to us. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * fix(ltx-av): drop unused `import math` (ruff F401) a2v_gain uses an explicit smoothstep polynomial, not math.*, so the import was dead. Unblocks the Lint & Smoke CI gate. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * ci(codeql): advanced setup that excludes vendored upstream code CodeQL default setup cannot read a config file, so it scanned the verbatim-vendored WhatDreamsCost editor (web/whatdreamscost_koolook/ ltx_director.js) and raised 4 high-sev DOM-XSS alerts in upstream code we do not own. Switch to advanced setup with a committed config whose paths-ignore mirrors the .gitattributes linguist-vendored block, so the scanner treats vendored upstream as out-of-scope while still fully scanning our own nodes and sidebar JS. Languages match the prior default-setup matrix (actions, javascript-typescript, python). NOTE: requires disabling "default setup" once in repo Settings before this workflow can run (GitHub rejects advanced runs while default setup is enabled). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * fix(ci): scope vendored fork exclusions to v2_0_2 * fix(forks): preserve final LTX keyframe snap bucket --------- Co-authored-by: AI Machine <ai.machine@x.baconcph.com> Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com> |
||
|
|
3e2b541a61 | fix(registry): expose nodes to scanners (#251) | ||
|
|
e165c96fcc | fix(ci): enforce dependency audit gate (#247) | ||
|
|
1cf2fd31a2 |
feat(setups): add published setup catalog API (#214)
* feat(setups): add published setup catalog API * fix(setups): report registry storage diagnostics |
||
|
|
8c8546c17c |
Add queue-driven loop status controller (#205)
* Add queue-driven loop status node * Document ComfyUI server API testing * Harden loop queue controller * Guard loop controller HTTP requests |
||
|
|
447465ecd8 |
refactor(nodes): remove superseded LTX Director extra nodes + harden registry (#198) (#199)
* refactor(nodes): remove superseded LTX Director extra nodes + harden registry (#198) Remove the two experimental nodes layered on the WhatDreamsCost fork — KoolookAudioTranscriptTimeline (k_audio_timeline.py) and KoolookTimelineEditor (k_timeline_editor.py) — plus the transcribe_audio_timeline.py CLI and its test. The audio-transcript to Director-timing job now lives inside the fork via the Director's audio_transcript_json input, so the standalone nodes are redundant. Also drops the orphaned [audio]/faster-whisper extra and updates packaging, dev-sync, web, and docs references. Harden the node registry against the #183 failure: __init__.py now imports each node group independently and installs the /koolook/presets/* routes regardless of node-import results, so one missing or broken module degrades to 'that node absent' instead of zeroing every node and 404'ing the sidebar disk routes. A new test keeps every root k_*.py listed in dev-sync's RUNTIME_PATHS. Closes #198. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(nodes): ship loader gates via dev-sync-audio; generalize RUNTIME_PATHS guard (#198) Address the K-Flame review on #199: MEDIUM — the #198 refactor made koolook_versioning.py a single registration gate, but dev-sync-audio shipped the new __init__.py without it (and the k_*.py-glob guard test couldn't see it). Add koolook_versioning.py AND koolook_install_guard.py (whose absolute-import fallback raises uncaught) to AUDIO_PATHS so a scoped sync can never leave the loader pointing at a module the target lacks. Generalize the guard test: parse __init__.py's actual load-time import targets (relative imports + _merge_node_group calls) and assert each is in RUNTIME_PATHS, instead of globbing k_*.py which missed the non-k_ gates. Add an AUDIO_PATHS gate-coverage test. LOW — CHANGELOG now records the [audio]/faster-whisper + transcribe_audio_timeline.py removal as an external-contract break. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
b706a94030 |
chore(testenv): lock + audit test deps; pre-authorize auto-bootstrap (#182)
Make the Python test-env bootstrap safe to auto-run so contributors and
agents no longer have to approve a fresh-worktree install every session.
* `constraints-test.txt` — pinned resolve of `.[test]` extras + their full
transitive closure (39 entries); committed lockfile becomes the
dependency-change review surface.
* `scripts/bootstrap_test_env.{sh,ps1}` — install via the lock when
present, then gate with `pip-audit` (a known CVE fails the bootstrap).
New flags: `--force`/`-Force`, `--relock`/`-Relock`,
`--no-audit`/`-NoAudit`. `--relock` without `--force` now warns
instead of silently no-op'ing.
* `tests/scripts/test_bootstrap_constraints.py` — guards the lock stays
in sync with the `[test]` extras declared in pyproject.toml.
* `CLAUDE.md` — documents that the bootstrap is pre-authorized to run
automatically and supersedes the global `/warmup` MISSING_VENV
hard-stop; dependency *changes* still land as a reviewed diff.
* `.gitattributes` — normalize `constraints-test.txt` to LF so the
PowerShell `--relock` path doesn't flip the file to CRLF.
CI still installs its own minimal `pytest`+`aiohttp` subset directly
(not yet on the lock/audit path) — noted in the CHANGELOG; CI hardening
left as a fast-follow.
Verified: `--force` rebuild from the lock → pip-audit clean → 235 tests
pass (231 + 4 new guards). bash `-n` syntax check OK; arg-error path
exits 2; `--relock` without `--force` warns + exits 0.
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
|
||
|
|
3a0f835814 | fix(audio-loop): read active director card values (#195) | ||
|
|
63f5c1db7b |
fix(audio-loop): capture card metadata and Director pins (#193)
* fix(audio-loop): capture card metadata and Director pins * fix(audio-loop): address card metadata review * fix(audio-loop): remove duplicate card import |
||
|
|
201cdf3001 |
Track audio loop card setup metadata (#191)
* feat(audio): surface director flavor and prompt mode * Track audio loop card setup metadata * Fix audio card review issues * Harden audio card metadata capture |
||
|
|
1d9d40ab70 |
Add audio transcript timeline node (#189)
* Add audio transcript timeline node * Hook audio transcripts into Koolook Director * Add modular timeline audio timing workflow * fix(frontend): guard Comfy workflow draft cache * fix(audio): compose transcript timeline prompts * fix(audio): address transcript review findings * fix(ci): keep transcript tests lightweight * fix(timeline): restore image previews from saved paths * fix(timeline): scrub restored preview blobs |
||
|
|
a5b6494848 | fix(audio): remove stale director web sync folder (#188) | ||
|
|
031af80f43 |
feat(forks): upgrade Koolook Director to stable v1.3.9 (#187)
* feat(forks): upgrade Koolook Director to stable v1.3.9 * docs(forks): clarify Director upgrade compatibility |
||
|
|
26124e08d5 |
refactor(loop-audio): card reads only multilines + Koolook Director (#177) (#185)
* 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> |
||
|
|
9372032b46 |
feat(automations): workflow validator + LTX 2.3 stage-2-only docs (#170)
* feat(automations): workflow JSON validator + LTX 2.3 stage-2-only findings Adds scripts/validate_workflow.py — a stdlib-only CLI that checks a ComfyUI workflow JSON for internal consistency: link-table declarations vs node-IO references, slot index bounds and types, endpoint cross- checks, last_node_id/last_link_id bounds, and duplicate IDs. Catches the class of bugs that come from hand-editing workflow JSONs (e.g. when an agent re-wires a graph to build a slim variant from a Director workflow). Comfy's loader silently drops or mis-wires bad parts, so the cost of an undetected drift is "graph looks fine but behaves wrong" — exactly the failure mode this script blocks. Exit codes: 0 clean, 1 problems on stderr, 2 file missing or bad JSON. Docs: - docs/automations/CONVENTIONS.md §11 — when and how to run the validator, full list of checks performed. - docs/automations/LTX-2.3/findings.md — two new sections covering the spatial upscaler's architectural facts (ltx-2.3-spatial-upscaler-x2 is hardcoded x2, the brief enters only via Director > guide_data > DirectorGuide, attention scales (W * H * T)^2) plus three open hypotheses for a stage-2-only refine path. - CHANGELOG.md [Unreleased] entry. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix(scripts): make validate_workflow.py robust against malformed JSON Addresses PR #170 review (Codex GPT-5.3) — the validator crashed with AttributeError on malformed-but-valid JSON, defeating its goal as a safe preflight tool. Two repros from the review: {"nodes":[1],"links":[],"last_node_id":0,"last_link_id":0} -> AttributeError: 'int' object has no attribute 'get' [1,2] -> AttributeError: 'list' object has no attribute 'get' Both confirmed locally. Now every shape assumption is guarded: - Top-level must be a JSON object; non-object short-circuits with one clear problem line. - 'nodes' and 'links' must be lists; non-list reported and treated as empty so subsequent checks still run. - Each node entry must be a dict; bad entries reported and dropped. - Each input/output entry must be a dict (extracted into a _sanitize_slots helper, cached per node so duplicates aren't reported when the same node is touched by multiple links). - Each link entry must be a 6-element list; malformed link entries reported and skipped without crashing later phases. - node_by_id only keys on int node IDs (non-int IDs can't match a link's int src/dst, so we don't mask bad data by accepting them). Adds tests/scripts/test_validate_workflow.py — 24 tests covering both review repros, every new guard, and a sampling of the existing consistency checks (link-to-missing-node, type mismatch, endpoint desync, duplicate IDs, last_node_id bound, CLI exit codes). Full suite: 168 passed in 1.59s. CHANGELOG entry updated to reflect the defensive behavior + test coverage. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |