#!/usr/bin/env python3 """ ``loop-audio`` — record one iteration of the LTX 2.3 audio-lipsync loop. End-to-end automation for one render cycle: locate the workflow the maintainer just saved inside ComfyUI, snapshot the workflow JSON + relay-overrides + fork state + notes into a numbered ``runs/run-NNN_`` folder under this module, render the audio-lipsync card alongside, and append a row to ``runs/log.md``. No manual steps. Settings — *which* module, *which* workflow filename to look for, *which* multiline titles to extract, *which* fork dir to pin — all live in :file:`loop_audio.config.json` next to this script. Edit the config, not the Python, when the convention shifts. USER-INITIATED ONLY. Same rule as ``dev-sync`` and ``dev-sync-audio`` (see project ``CLAUDE.md``). Trigger phrase: ``loop-audio``. Discovery chain: KOLOOK_COMFYUI_DEV_PATH ── walk up two levels ──► / └── ── glob workflow_pattern ──► newest mtime (excluding skip_filename_substring) ──► workflow.json Snapshot folder layout (always created): /runs/run-NNN_/ ├── runNNN_workflow.json copy of the workflow at submission ├── relay_overrides.txt RELAY_OVERRIDES multiline body ├── patch_state.txt MAIN sha + last dev-sync-audio + fork-dir status ├── metadata.json structured run/setup/director/repo metadata ├── notes.md feedback + scores + mechanical interp └── card.png when render_card=true in config Plus one row appended to /runs/log.md. Usage: python scripts/loop_audio.py python scripts/loop_audio.py --dry-run python scripts/loop_audio.py --label python scripts/loop_audio.py --no-log python scripts/loop_audio.py --no-card python scripts/loop_audio.py --workflow python scripts/loop_audio.py --config Exit codes: 0 snapshot landed (or would have, in --dry-run) 2 KOLOOK_COMFYUI_DEV_PATH unset / ComfyUI workflows dir missing / config missing 3 no workflow matching the configured pattern 4 config JSON malformed """ from __future__ import annotations import argparse import hashlib import json import os import re import shutil import subprocess import sys import tomllib from datetime import date from pathlib import Path from typing import Any, Optional REPO_ROOT = Path(__file__).resolve().parent.parent DEFAULT_CONFIG_PATH = Path(__file__).resolve().with_suffix(".config.json") # -------------------------------------------------------------------------- # Config — read once into a small dataclass-ish dict. Underscore-prefixed # keys are ignored (same convention as the LTXDirector relay_overrides # widget — keeps inline documentation co-located with the values). # -------------------------------------------------------------------------- REQUIRED_CONFIG_KEYS = ( "job_name", "module_path", "comfyui_workflows_subpath", "workflow_pattern", "skip_filename_substring", "tracked_multilines", "fork_to_track", "render_card", ) def load_config(path: Path) -> dict[str, Any]: if not path.is_file(): print( f"Config not found: {path}\n" f"Each loop script defaults to