feat(scripts): add dev-sync-audio — scoped sync for audio-lipsync iteration

`dev-sync-audio` is a scoped wrapper around `dev-sync` for the
[`docs/automations/LTX-2.3/audio-lipsync/`](docs/automations/LTX-2.3/audio-lipsync/)
iteration loop. It copies *only* the paths that module touches — the
`forks/whatdreamscost_koolook/` subtree and the root `__init__.py` —
instead of `rmtree`-ing the whole dest `forks/` and re-copying every
Koolook fork on every render. Same `KOLOOK_COMFYUI_DEV_PATH` target,
same ComfyUI-Manager restart, same two-line chat-report shape.

Changes
- `scripts/sync_to_dev_audio.py` (new). Thin wrapper that reuses
  `sync_to_dev`'s env loader, target validator, restart helper, and
  chat-report builder, supplying its own `AUDIO_PATHS` tuple. Defaults
  the `--scope` tag to `"audio-lipsync fork edit"` so the build
  identifier names this module even when invoked without an explicit
  scope.
- `scripts/sync_to_dev.py` — `sync()` gains an optional `paths` kwarg
  (defaults to the existing `RUNTIME_PATHS`). Backwards-compatible with
  every existing call site.
- `docs/automations/LTX-2.3/audio-lipsync/README.md` and
  `runs/LOOP.md` updated to point at `dev-sync-audio` instead of plain
  `dev-sync` for the iteration step.
- Project `CLAUDE.md` — new "Per-module scoped variants" subsection
  under the existing `dev-sync` section documents the
  `dev-sync-<module>` pattern, the trigger-name → script-name mapping,
  and reiterates that the user-initiated-only rule applies verbatim to
  every `dev-sync-*` variant.

Verification
- ruff check . → all passed
- python scripts/sync_to_dev_audio.py --dry-run -v →
    would copy: __init__.py
    would copy: forks/whatdreamscost_koolook
    e7f3ab4 - beautiful-mahavira-ae6da6
    would sync 2 entries -> <KOLOOK_COMFYUI_DEV_PATH>  (dev-sync-audio)
  (Confirms scope is correct and the existing dev-sync chat-report
  shape is preserved.)

User-initiated only — never automatic. Trigger phrase: `dev-sync-audio`
in chat (also: "copy audio fork", "sync audio fork"). Same rule as
`dev-sync` — see project CLAUDE.md.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
malkuthro
2026-05-28 21:56:44 +02:00
co-authored by Claude Opus 4.7
parent e7f3ab4fe8
commit 15d56882c5
5 changed files with 245 additions and 16 deletions
+29
View File
@@ -103,6 +103,35 @@ install-missing button on Nodes row
Broader iteration pattern (push/publish gates, what survives re-syncs)
is in [`docs/maintainers/dev-iteration-loop.md`](docs/maintainers/dev-iteration-loop.md).
### Per-module scoped variants — `dev-sync-<module>`
When an automation module under `docs/automations/` iterates on code that
lives in a single subtree (e.g. one fork under `forks/`), a *scoped*
dev-sync wrapper avoids churning the rest of the live install on every
iteration. Same `KOLOOK_COMFYUI_DEV_PATH` target, same restart, same
chat-report shape — just a smaller set of paths copied.
Current per-module variants:
- **`dev-sync-audio`** (chat phrase) → `python scripts/sync_to_dev_audio.py`.
Copies `forks/whatdreamscost_koolook/` + root `__init__.py` only.
Driven by the
[`docs/automations/LTX-2.3/audio-lipsync/`](docs/automations/LTX-2.3/audio-lipsync/)
iteration. Leaves `forks/radiance_koolook/` and the rest of the live
install alone across iterations.
When a new code-touching automation module is added, follow the same
pattern: a `scripts/sync_to_dev_<module>.py` wrapper that imports the
shared infrastructure from `sync_to_dev` and supplies its own
`PATHS` tuple. The chat trigger is the file name minus `sync_to_dev_`
and minus `.py`, with the underscore between module segments turned
into a hyphen: `sync_to_dev_audio.py` → `dev-sync-audio`. Document the
new trigger in this section *and* in the module's `runs/LOOP.md`.
**All scoped variants follow the same user-initiated-only rule** as
plain `dev-sync` (the bullet list above applies verbatim). Never run any
`dev-sync-*` from an automated flow.
## Kforge Labs sidebar — starter preset distribution
The "Kforge Labs" ComfyUI sidebar tab (implemented in
@@ -83,11 +83,12 @@ audio-lipsync/
## Iteration loop
1. **Edit a knob.** Either:
- **Widget-only swap** — change `relay_overrides` JSON on the LTXDirector node in ComfyUI. No restart needed; just queue.
- **Widget-only swap** — change `relay_overrides` JSON on the LTXDirector node in ComfyUI. No sync, no restart; just queue.
- **Code-level change** — edit
[`../../../../forks/whatdreamscost_koolook/versions/v1_3_2/prompt_relay.py`](../../../../forks/whatdreamscost_koolook/versions/v1_3_2/prompt_relay.py)
or [`ltx_director.py`](../../../../forks/whatdreamscost_koolook/versions/v1_3_2/ltx_director.py)
in this repo. Then sync into the maintainer's live ComfyUI via the standard `dev-sync` (user-initiated; see project `CLAUDE.md`). Restart Comfy for Python module reload.
in this repo. Then run **`dev-sync-audio`** (chat phrase or directly
[`scripts/sync_to_dev_audio.py`](../../../../scripts/sync_to_dev_audio.py)) — a scoped variant of `dev-sync` that copies just the fork dir + the root `__init__.py` into the live ComfyUI install at `$KOLOOK_COMFYUI_DEV_PATH`, leaves `forks/radiance_koolook/` and the rest of the tree alone, then triggers a ComfyUI-Manager reboot so the Python module re-imports. User-initiated only — same rule as `dev-sync`; see project `CLAUDE.md`.
2. **Save workflow** — `Workflow → Save (API Format)` into the working folder.
3. **Render** — queue.
4. **Report** in chat — verbal feedback on sync state, motion, prompt adherence.
@@ -119,5 +120,6 @@ install is untouched.
Edits to `forks/whatdreamscost_koolook/versions/v1_3_2/*.py` are normal
git-tracked changes — `git restore` / `git stash` work as usual. Once
`dev-sync` is run, the running ComfyUI install is updated; restart Comfy
so the Python module reimports.
`dev-sync-audio` is run, the running ComfyUI install is updated; the
auto-restart triggered at the end of the sync re-imports the Python
module.
@@ -10,7 +10,7 @@
| Step | Who | What |
|---|---|---|
| 1 | Maintainer | Either edit `relay_overrides` JSON on the **`LTX Director (Koolook v1.3.2)`** node in ComfyUI, OR edit a `forks/whatdreamscost_koolook/versions/v1_3_2/*.py` source in this repo + run `dev-sync` + restart Comfy. |
| 1 | Maintainer | Either edit `relay_overrides` JSON on the **`LTX Director (Koolook v1.3.2)`** node in ComfyUI, OR edit a `forks/whatdreamscost_koolook/versions/v1_3_2/*.py` source in this repo + run **`dev-sync-audio`** (auto-restarts Comfy). |
| 2 | Maintainer | **Save** workflow → overwrites the current workflow file at the working folder. |
| 3 | Maintainer | Queue render in ComfyUI. |
| 4 | Maintainer | Report result in chat (verbal feedback — sync state, motion state, prompt adherence). |
@@ -51,18 +51,39 @@ Auto-generated from key knob state + feedback summary, e.g.:
The maintainer can override any label by stating it explicitly.
## Syncing fork-code edits to the live install
## Syncing fork-code edits to the live install — `dev-sync-audio`
When the iteration step requires editing a `forks/whatdreamscost_koolook/`
source (not just the widget), the standard
[`dev-sync`](../../../../scripts/sync_to_dev.py) flow applies — it's a
user-initiated, never-automatic action per the project `CLAUDE.md`. Quick
recipe:
source (not just the widget), run the **scoped** sync for this module —
not the full `dev-sync`:
```
python scripts/sync_to_dev.py --scope "audio-lipsync fork edit"
python scripts/sync_to_dev_audio.py
# or just say "dev-sync-audio" in chat — the agent runs the script
```
Then restart Comfy so the Python module re-imports. Widget-only changes
(editing `relay_overrides` JSON on the canvas) do **not** require sync or
restart — just queue the next render.
What it does:
- Copies only `forks/whatdreamscost_koolook/` + the root `__init__.py`
to `$KOLOOK_COMFYUI_DEV_PATH` (read from `.env`).
- Leaves the rest of the live install (radiance fork, the root `k_*.py`
nodes, the sidebar `web/` bundle, `video_formats/`) untouched.
- Auto-triggers a ComfyUI-Manager reboot so the Python module re-imports
(custom-node `.py` files load once at server start; without a restart
the new code stays invisible). Use `--no-restart` to opt out.
- Writes the scope tag into `<target>/web/_dev_build.json` so the
Kforge Labs sidebar footer shows which build is live.
Flags mirror `dev-sync`: `--dry-run`, `--init`, `--verbose`,
`--scope "<≤10-word change>"`, `--no-restart`, `--restart-url`.
User-initiated only — same rule as `dev-sync` (see project `CLAUDE.md`).
Never automatic. Never on commit, never on session end.
Widget-only changes (editing `relay_overrides` JSON on the canvas) do
**not** require sync or restart — just queue the next render.
Why not full `dev-sync`? Because it `rmtree`s the entire dest `forks/`
tree and re-copies both Koolook forks. For this module's iteration
loop, that churns `forks/radiance_koolook/` for no reason. Scoping to
`forks/whatdreamscost_koolook/` keeps the radiance side of the live
install stable across audio-lipsync iterations.
+17 -2
View File
@@ -251,9 +251,24 @@ def _ignore(_dir: str, names: list[str]) -> list[str]:
return [n for n in names if n in DIR_EXCLUDES]
def sync(target: Path, dry_run: bool, verbose: bool) -> int:
def sync(
target: Path,
dry_run: bool,
verbose: bool,
paths: tuple[str, ...] = RUNTIME_PATHS,
) -> int:
"""Copy each entry in ``paths`` (relative to the repo root) to the
matching subpath under ``target``. Directories are recursively
copied with ``DIR_EXCLUDES`` filtered out and the previous dest
subtree removed first; files are overwritten in place.
The optional ``paths`` argument lets scoped wrappers (e.g.
``sync_to_dev_audio.py``) reuse this function with a smaller set —
just the subtree their automation module touches. The default is
the full ``RUNTIME_PATHS`` (every file ComfyUI loads at runtime).
"""
copied = 0
for rel in RUNTIME_PATHS:
for rel in paths:
src = REPO_ROOT / rel
if not src.exists():
continue
+162
View File
@@ -0,0 +1,162 @@
#!/usr/bin/env python3
"""
Scoped variant of ``dev-sync`` for the LTX 2.3 audio-lipsync automation.
Copies only what that automation's iteration loop touches:
- forks/whatdreamscost_koolook/ the Koolook fork of LTXDirector +
its modified prompt_relay + the
verbatim-vendored patches.py
- __init__.py root loader (registers the fork's
node mappings; needs to be re-synced
when the fork's NODE_CLASS_MAPPINGS
set changes)
Everything else in the live install — ``forks/radiance_koolook/``, the
root ``k_*.py`` nodes, ``web/``, ``video_formats/`` — is left untouched.
USER-INITIATED ONLY. Same rule as plain ``dev-sync`` (see project
``CLAUDE.md`` → ``dev-sync`` section). Never run automatically:
* after a commit
* after a PR merge or ``/ship-pr``
* at session end / wrap-up
* on hook completion or "task complete" cleanup
* from any agent skill that doesn't explicitly require it
Run only on the explicit user trigger phrase ``dev-sync-audio`` (or
``copy audio fork``, ``sync audio``, etc.). The maintainer typically has
multiple parallel sessions across worktrees; an unsolicited sync from one
silently destroys what another is reviewing.
The target path comes from ``KOLOOK_COMFYUI_DEV_PATH`` in ``.env`` —
identical to plain ``dev-sync``. No new env var.
Usage:
python scripts/sync_to_dev_audio.py
python scripts/sync_to_dev_audio.py --dry-run
python scripts/sync_to_dev_audio.py --scope "vstr=10 trial"
python scripts/sync_to_dev_audio.py --no-restart
Exit codes mirror ``sync_to_dev.py`` exactly:
0 success
2 KOLOOK_COMFYUI_DEV_PATH unset or target missing without --init
3 --init refused
After copying, the script triggers a ComfyUI-Manager reboot so the new
Python files are actually loaded (custom-node modules load once at server
start; a file-only sync leaves ``.py`` changes invisible). Use
``--no-restart`` to opt out.
"""
from __future__ import annotations
import argparse
import os
import sys
from pathlib import Path
# Reuse the dev-sync infrastructure verbatim — same ``.env`` loader,
# same target validation, same restart, same chat-report shape — so the
# only thing that differs between full and scoped syncs is *which paths
# get copied*. Per-module wrappers stay small and the safety guarantees
# (target validation, parent-directory sanity check, restart fallbacks)
# only need to be audited in one place.
_SCRIPT_DIR = Path(__file__).resolve().parent
if str(_SCRIPT_DIR) not in sys.path:
sys.path.insert(0, str(_SCRIPT_DIR))
import sync_to_dev as _dev # noqa: E402
AUDIO_PATHS: tuple[str, ...] = (
"__init__.py",
"forks/whatdreamscost_koolook",
)
def main() -> int:
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument(
"--dry-run",
action="store_true",
help="Print what would be copied without touching the target.",
)
parser.add_argument(
"--init",
action="store_true",
help=(
"Create the target directory if it doesn't exist. The parent "
"(typically a ComfyUI custom_nodes/ folder) must already be "
"in place. Use on first-run only."
),
)
parser.add_argument(
"--verbose",
"-v",
action="store_true",
help="Print each file/dir as it's copied.",
)
parser.add_argument(
"--scope",
type=str,
default="audio-lipsync fork edit",
help=(
"Short (≤10 word) description for the chat report's second line "
"and the in-browser footer in `web/_dev_build.json`. Defaults to "
"'audio-lipsync fork edit' so the build identifier always names "
"this module even when the maintainer doesn't pass a scope."
),
)
parser.add_argument(
"--no-restart",
action="store_true",
help=(
"Skip the auto-restart of the live ComfyUI server. Without a "
"restart, the new fork code stays loaded as the previous version "
"(custom-node .py files load once at server start)."
),
)
parser.add_argument(
"--restart-url",
type=str,
default=_dev.DEFAULT_RESTART_URL,
help=(
f"Override the restart endpoint. Default: {_dev.DEFAULT_RESTART_URL}. "
f"Ignored when --no-restart is set or --dry-run is used."
),
)
args = parser.parse_args()
_dev.load_dotenv(_dev.REPO_ROOT / ".env")
target_str = os.environ.get("KOLOOK_COMFYUI_DEV_PATH")
if not target_str:
print(
"KOLOOK_COMFYUI_DEV_PATH not set. Add it to .env (see .env.example).",
file=sys.stderr,
)
return 2
target = Path(target_str).expanduser()
err = _dev.ensure_target(target, init=args.init)
if err is not None:
return err
n = _dev.sync(
target,
dry_run=args.dry_run,
verbose=args.verbose,
paths=AUDIO_PATHS,
)
verb = "would sync" if args.dry_run else "synced"
print(_dev._build_line())
print(f"{verb} {n} entries -> {target} (dev-sync-audio)")
if not args.dry_run:
_dev.write_build_info(target, args.scope)
if not args.no_restart:
_ok, msg = _dev.trigger_restart(args.restart_url)
print(msg)
return 0
if __name__ == "__main__":
sys.exit(main())