diff --git a/docs/product requirements docs/appendix-quality-bar.md b/docs/product requirements docs/appendix-quality-bar.md index 8d577b54..c10517de 100644 --- a/docs/product requirements docs/appendix-quality-bar.md +++ b/docs/product requirements docs/appendix-quality-bar.md @@ -8,7 +8,7 @@ |------|-------|--------------------------| | Dashboard | 8 | health, auth middleware, proxy auth, registry routes, service pressure, ComfyUI packs, dependencies, performance | | ComfyUI | 5 | API client, workflow manager defaults, queue prompt (+ integration), default model env | -| Orchestration | 5 | API, e2e, outbox, workflow versioning, desk-influencer workflow | +| Orchestration | 5 | API, e2e, outbox, workflow versioning | | RAG | 2 | ingestion chunking/embedding, status | | Ops / secrets / stack | 6 | secrets isolation, Caddyfile invariants, stack-monitor sanitize/versions, storage purge, settings validation | | GPU / hardware | 3 | GPU stats, GPU routes, llama.cpp turboquant | diff --git a/scripts/build_desk_influencer_workflow.py b/scripts/build_desk_influencer_workflow.py deleted file mode 100644 index 3f113dfc..00000000 --- a/scripts/build_desk_influencer_workflow.py +++ /dev/null @@ -1,127 +0,0 @@ -"""Build LTX-2.3 Desk Influencer workflow from the street-interview clone. - -Applies parameter-level mutations only. Structural I2V additions are -performed by the operator in ComfyUI's UI per the accompanying README. -""" -from __future__ import annotations - -import argparse -import json -from pathlib import Path - -DESK_NEGATIVE_PROMPT = ( - "subject changing identity mid-shot, face morphing, identity drift between frames, " - "multiple subjects, two people in frame, crowd, " - "handheld camera, shaky cam, dutch angle, fisheye, vlog selfie distortion, " - "two desks, multiple desks, room layout changing, walls warping, " - "background morphing, lighting changing mid-shot, props moving on desk" -) - - -def _node_by_id(data: dict, node_id: int, expected_type_substring: str) -> dict: - """Find a node by id and assert its type contains the expected substring. - - Bare next() lookups raise StopIteration with no context if a node is - removed, and silently overwrite the wrong widget if an ID gets reused - for a different node type. This wrapper makes both failure modes loud. - """ - for node in data["nodes"]: - if node.get("id") == node_id: - actual_type = node.get("type", "") - if expected_type_substring not in actual_type: - raise RuntimeError( - f"Node {node_id} expected to contain {expected_type_substring!r} in its type, " - f"got {actual_type!r}" - ) - return node - raise RuntimeError(f"Node id {node_id} not found in workflow") - - -def _replace_negative_prompt(data: dict) -> None: - """Swap negative prompt for desk-specific rejections. - - Drops street-interview's "missing microphone" clause (desk mics - don't need to be on-frame for influencer content) and adds rejections - for handheld/shaky/dutch-angle camera, multi-desk scenes, and - background morphing. Identity-drift rejections are preserved. - """ - neg_node = _node_by_id(data, 110, "CLIPTextEncode") - neg_node["widgets_values"][0] = DESK_NEGATIVE_PROMPT - - -DESK_SHOT_DATA = """[VISUAL]: man in his early 30s seated at a modern podcast desk, soft ring light from upper-left, a single condenser mic on a low boom in front of him, monitor glow behind, clean tech-podcast aesthetic. He's looking directly at the camera, talking energetically with hand gestures. Vertical 9:16 framing, chest-up, depth of field on the background. - -[SPEECH]: \"...\" - -[SOUNDS]: room tone, soft keyboard tap, distant HVAC.""" - - -def _replace_shot_data(data: dict) -> None: - """Replace the SHOT_DATA primitive with the desk-influencer template. - - Swaps street-interview's NYC-sidewalk vox-pop scene for a podcast-desk - aesthetic (ring light, condenser mic on boom, monitor glow) while - preserving the [VISUAL]/[SPEECH]/[SOUNDS] structured-prompt format - that the downstream encoder expects. - """ - shot_node = _node_by_id(data, 352, "Primitive") - shot_node["widgets_values"][0] = DESK_SHOT_DATA - - -def _normalize_id_loras(data: dict) -> None: - """Force every ID-LoRA row in the Power Lora Loader to {on: False, strength: 1.0}. - - Anchored mode (the desk-influencer default per the spec's mode-switch - table) wants no identity injection — the master scene still + prompt - carry the character. Operator flips a single ID-LoRA row ON in - ComfyUI's UI for re-angle mode. - - The street-interview source has multiple ID-LoRA rows (TalkVid x2, - CelebVHQ x1), some on, some off. We iterate the whole list — early - return would leave duplicate-on rows live. - """ - power_lora = _node_by_id(data, 301, "Power Lora Loader") - found = 0 - for widget in power_lora["widgets_values"]: - if isinstance(widget, dict) and "ID-LoRA" in widget.get("lora", ""): - widget["on"] = False - widget["strength"] = 1 - found += 1 - if found == 0: - raise RuntimeError("No ID-LoRA rows found in Power Lora Loader (node 301)") - - -def _retune_cameraman_lora(data: dict) -> None: - """Drop Cameraman IC-LoRA strength from street-interview's 0.5 to 0.15. - - Desk content is mostly static. 0.15 keeps a touch of breath without - pushing toward handheld. Operator can crank to 0.3 for vlog energy. - """ - power_lora = _node_by_id(data, 301, "Power Lora Loader") - for widget in power_lora["widgets_values"]: - if isinstance(widget, dict) and widget.get("lora", "").startswith("LTX-2.3-Cameraman"): - widget["strength"] = 0.15 - return - raise RuntimeError("Cameraman IC-LoRA row not found in Power Lora Loader (node 301)") - - -def build(source: Path, target: Path) -> None: - data = json.loads(source.read_text(encoding="utf-8")) - _retune_cameraman_lora(data) - _replace_negative_prompt(data) - _replace_shot_data(data) - _normalize_id_loras(data) - target.write_text(json.dumps(data, indent=2), encoding="utf-8") - - -def main() -> None: - p = argparse.ArgumentParser(description=__doc__) - p.add_argument("--source", type=Path, required=True) - p.add_argument("--target", type=Path, required=True) - args = p.parse_args() - build(args.source, args.target) - print(f"wrote {args.target}") - - -if __name__ == "__main__": - main() diff --git a/tests/test_build_desk_influencer_workflow.py b/tests/test_build_desk_influencer_workflow.py deleted file mode 100644 index 109c2385..00000000 --- a/tests/test_build_desk_influencer_workflow.py +++ /dev/null @@ -1,101 +0,0 @@ -import json -import subprocess -import sys -from pathlib import Path - -import pytest - -REPO_ROOT = Path(__file__).resolve().parents[1] -SCRIPT = REPO_ROOT / "scripts" / "build_desk_influencer_workflow.py" -SOURCE = REPO_ROOT / "data/comfyui-storage/ComfyUI/user/default/workflows/ltx-video/LTX-2.3_-_Street_Interview.json" -TARGET = REPO_ROOT / "data/comfyui-storage/ComfyUI/user/default/workflows/ltx-video/LTX-2.3_-_Desk_Influencer.json" - -# These tests require an operator-local ComfyUI workflow fixture under -# `data/comfyui-storage/...`, which is gitignored. CI runners don't have it. -# Skip cleanly there; run normally on dev boxes that do have the fixture. -pytestmark = pytest.mark.skipif( - not SOURCE.exists(), - reason=f"workflow fixture not present: {SOURCE.relative_to(REPO_ROOT)}", -) - - -def run_builder(tmp_target: Path) -> dict: - """Run the builder pointing at a tmp output path; return parsed JSON.""" - subprocess.run( - [sys.executable, str(SCRIPT), "--source", str(SOURCE), "--target", str(tmp_target)], - check=True, - ) - return json.loads(tmp_target.read_text(encoding="utf-8")) - - -def test_builder_produces_valid_json(tmp_path): - out = run_builder(tmp_path / "out.json") - assert set(out.keys()) >= {"nodes", "links", "groups", "version"} - assert isinstance(out["nodes"], list) and len(out["nodes"]) > 0 - - -def test_cameraman_lora_strength_is_0_15(tmp_path): - out = run_builder(tmp_path / "out.json") - power_lora = next(n for n in out["nodes"] if n.get("id") == 301) - cameraman = next( - w for w in power_lora["widgets_values"] - if isinstance(w, dict) and w.get("lora", "").startswith("LTX-2.3-Cameraman") - ) - assert cameraman["on"] is True - assert cameraman["strength"] == 0.15 - - -def test_negative_prompt_has_desk_additions_and_drops_microphone(tmp_path): - out = run_builder(tmp_path / "out.json") - neg_node = next(n for n in out["nodes"] if n.get("id") == 110) - text = neg_node["widgets_values"][0] - for phrase in ["handheld camera", "shaky cam", "two desks", "background morphing", "lighting changing mid-shot"]: - assert phrase in text, f"expected {phrase!r} in negative prompt" - assert "identity" in text.lower() or "morph" in text.lower() - assert "missing microphone" not in text - - -def test_shot_data_is_desk_template(tmp_path): - out = run_builder(tmp_path / "out.json") - shot_node = next(n for n in out["nodes"] if n.get("id") == 352) - text = shot_node["widgets_values"][0] - assert "podcast desk" in text - assert "ring light" in text - assert "NYC sidewalk" not in text - assert "vox-pop" not in text.lower() - assert "[VISUAL]" in text and "[SPEECH]" in text and "[SOUNDS]" in text - - -def test_all_id_loras_off_at_strength_one(tmp_path): - out = run_builder(tmp_path / "out.json") - power_lora = next(n for n in out["nodes"] if n.get("id") == 301) - id_loras = [ - w for w in power_lora["widgets_values"] - if isinstance(w, dict) and "ID-LoRA" in w.get("lora", "") - ] - assert id_loras, "expected at least one ID-LoRA row in Power Lora Loader" - # Spec mode-switch (anchored default): ALL ID-LoRA rows must be OFF - # at strength 1.0 so operator can flip exactly one ON for re-angle mode. - assert all(w["on"] is False for w in id_loras), \ - f"expected every ID-LoRA row OFF, got {[(w['lora'], w['on']) for w in id_loras]}" - assert all(w["strength"] == 1 for w in id_loras) - - -def test_builder_is_idempotent(tmp_path): - """Running the builder on its own output should produce identical JSON. - - All mutations set to fixed values rather than applying deltas, so - re-running must be a no-op. Locks that property in. - """ - first = tmp_path / "first.json" - subprocess.run( - [sys.executable, str(SCRIPT), "--source", str(SOURCE), "--target", str(first)], - check=True, - ) - # Now re-build using the previous output as source - second = tmp_path / "second.json" - subprocess.run( - [sys.executable, str(SCRIPT), "--source", str(first), "--target", str(second)], - check=True, - ) - assert first.read_text(encoding="utf-8") == second.read_text(encoding="utf-8")