aimusic / ComfyUI /custom_nodes /koolook /tests /scripts /test_loop_audio.py
vidfom's picture
Upload folder using huggingface_hub (part 8)
0c85e62 verified
Raw
History Blame Contribute Delete
34.7 kB
"""Pure-function coverage for scripts/loop_audio.py.
Anchored by the PR #185 multi-agent review which surfaced three
silent-failure HIGHs on the audio-source-derivation path:
* derive_audio_state collapsed missing-Director and unwired-VAE
into the same label;
* extract_multilines used dict-insertion order, so a shorter needle
declared first could silently shadow a longer match;
* parse_timeline returned segments verbatim, so a string-valued
`start` field would crash the renderer at `start / fps` or quietly
break `<` comparisons in video_segment_has_audio.
These tests pin each fix plus the surrounding helpers so future edits
can't silently regress.
"""
from __future__ import annotations
import importlib.util
import json
import sys
from pathlib import Path
import pytest
# Load the script directly — it lives under scripts/, not under a package.
_SCRIPT = Path(__file__).resolve().parents[2] / "scripts" / "loop_audio.py"
sys.path.insert(0, str(_SCRIPT.parent))
_spec = importlib.util.spec_from_file_location("loop_audio", _SCRIPT)
assert _spec is not None and _spec.loader is not None
loop_audio = importlib.util.module_from_spec(_spec)
_spec.loader.exec_module(loop_audio)
# --- helpers used by multiple tests -----------------------------------------
def _director(
*,
node_type: str = "LTXDirector__koolook",
use_custom_audio: bool = False,
audio_vae_link: int | None = None,
timeline: dict | None = None,
epsilon: float = 0.001,
fps: int = 24,
) -> dict:
"""Build a minimal Koolook Director node dict matching the saved
widget order (DIRECTOR_WIDX). Only fields the helpers under test
actually read are populated; the rest are placeholders."""
import json as _json
timeline_str = _json.dumps(timeline) if timeline is not None else ""
wv = [
"", # 0 global_prompt
120, # 1 duration_frames
5, # 2 duration_seconds
timeline_str, # 3 timeline_data
"", # 4 local_prompts
"120", # 5 segment_lengths
epsilon, # 6 epsilon
"1.00", # 7 guide_strength
use_custom_audio, # 8 use_custom_audio
fps, # 9 frame_rate
"seconds", # 10 display_mode
0, # 11 custom_width
0, # 12 custom_height
"maintain aspect ratio", # 13 resize_method
32, # 14 divisible_by
18, # 15 img_compression
"", # 16 relay_overrides
]
return {
"type": node_type,
"widgets_values": wv,
"inputs": [
{"name": "audio_vae", "link": audio_vae_link},
{"name": "use_custom_audio", "link": None},
],
}
# --- extract_director — stable, legacy, and upstream IDs --------------------
@pytest.mark.parametrize(
"node_type",
["LTXDirector__koolook", "LTXDirector__koolook_v1_3_2", "LTXDirector"],
)
def test_extract_director_accepts_supported_director_ids(node_type):
node = _director(node_type=node_type)
assert loop_audio.extract_director([node]) is node
def test_extract_director_prefers_koolook_over_upstream():
upstream = _director(node_type="LTXDirector")
koolook = _director(node_type="LTXDirector__koolook")
assert loop_audio.extract_director([upstream, koolook]) is koolook
def test_extract_director_prefers_guide_wired_upstream_over_idle_koolook():
upstream = {"id": 10, **_director(node_type="LTXDirector")}
upstream["outputs"] = [{"name": "guide_data", "links": [100]}]
koolook = {"id": 20, **_director(node_type="LTXDirector__koolook")}
koolook["outputs"] = [{"name": "guide_data", "links": []}]
reroute = {
"id": 30,
"type": "Reroute",
"inputs": [{"name": "", "link": 100}],
"outputs": [{"name": "", "links": [101]}],
}
guide = {
"id": 40,
"type": "LTXDirectorGuide",
"inputs": [{"name": "guide_data", "link": 101}],
}
assert (
loop_audio.extract_director(
[koolook, upstream, reroute, guide],
[
[100, 10, 4, 30, 0, "GUIDE_DATA"],
[101, 30, 0, 40, 4, "GUIDE_DATA"],
],
)
is upstream
)
def test_director_widget_uses_saved_director_widget_order():
node = {
"type": "LTXDirector__koolook",
"inputs": [
{"name": "duration_frames", "widget": {"name": "duration_frames"}},
{"name": "epsilon", "widget": {"name": "epsilon"}},
{"name": "use_custom_audio", "widget": {"name": "use_custom_audio"}},
],
"widgets_values": [
"",
144,
6,
"",
"",
"",
0.004,
"1.00,1.00,1.00",
True,
24,
],
}
assert loop_audio.director_widget(node, "epsilon") == 0.004
assert loop_audio.director_widget(node, "use_custom_audio") is True
assert loop_audio.director_widget(node, "frame_rate") == 24
def test_director_widget_keeps_legacy_positional_fallback():
node = _director(epsilon=0.002)
assert loop_audio.director_widget(node, "epsilon") == 0.002
def test_active_relay_overrides_reads_only_wired_director_input():
director = {
"id": 10,
**_director(),
"inputs": [
{"name": "audio_vae", "link": 99},
{"name": "relay_overrides", "link": 20},
],
}
relay_note = _multiline("RELAY_OVERRIDES", '{"video_strength": 1.0}')
relay_note["id"] = 5
relay_note["outputs"] = [{"name": "STRING", "links": [20]}]
assert loop_audio.active_relay_overrides(
[director, relay_note],
[[20, 5, 0, 10, 16, "STRING"]],
director,
) == '{"video_strength": 1.0}'
def test_active_relay_overrides_ignores_unwired_note():
director = {
"id": 10,
**_director(),
"inputs": [
{"name": "audio_vae", "link": 99},
{"name": "relay_overrides", "link": None},
],
}
relay_note = _multiline("RELAY_OVERRIDES", '{"video_strength": 1.0}')
relay_note["id"] = 5
assert loop_audio.active_relay_overrides(
[director, relay_note], [], director
) == ""
@pytest.mark.parametrize(
"node_type, expected",
[
("LTXDirector__koolook", "Koolook"),
("LTXDirector__koolook_v1_3_2", "Koolook"),
("LTXDirector", "Original upstream"),
],
)
def test_director_flavor_labels_supported_directors(node_type, expected):
assert loop_audio.director_flavor(_director(node_type=node_type)) == expected
@pytest.mark.parametrize(
"node_type, upstream_version, expected",
[
("LTXDirector__koolook", "", "v1.3.9"),
("LTXDirector__koolook_v1_3_2", "", "v1.3.9"),
("LTXDirector", "1.3.2", "v1.3.2"),
("LTXDirector", "", "(unknown upstream pin)"),
],
)
def test_director_pin_tag_labels_lock_version(node_type, upstream_version, expected):
assert (
loop_audio.director_pin_tag(
_director(node_type=node_type), upstream_version
)
== expected
)
# --- derive_audio_state — 5 distinct states --------------------------------
@pytest.mark.parametrize(
"director_node, timeline, expected",
[
# (no director) — split off so a missing-Director workflow can't
# silently look identical to a director-present-but-VAE-unwired
# one (PR #185 review HIGH-1).
(None, {"segments": [], "audioSegments": []}, "(no director)"),
# off (no VAE) — Director present, audio_vae socket unwired.
(
_director(audio_vae_link=None),
{"segments": [], "audioSegments": []},
"off (no VAE)",
),
# model-gen — VAE wired, use_custom_audio=False. audioSegments
# are ignored on this path.
(
_director(audio_vae_link=42, use_custom_audio=False),
{"segments": [], "audioSegments": []},
"model-gen",
),
# custom — VAE wired, use_custom_audio=True, audioSegments
# non-empty.
(
_director(audio_vae_link=42, use_custom_audio=True),
{"segments": [], "audioSegments": [{"start": 0, "length": 100}]},
"custom",
),
# custom (empty) — VAE wired, use_custom_audio=True, but no
# audioSegments uploaded.
(
_director(audio_vae_link=42, use_custom_audio=True),
{"segments": [], "audioSegments": []},
"custom (empty)",
),
],
)
def test_derive_audio_state_all_five_states(director_node, timeline, expected):
assert loop_audio.derive_audio_state(director_node, timeline) == expected
# --- extract_multilines — longest-needle-wins guards future drift ----------
def _multiline(title: str, body: str) -> dict:
return {
"type": "Text Multiline",
"title": title,
"widgets_values": [body],
}
def test_extract_multilines_keeps_all_hits_for_one_needle():
"""Working_Folder_PATH legitimately appears twice on the canvas
(project mount + local mirror). Both bodies must come back so the
caller can pick by reachability."""
nodes = [
_multiline("Working_Folder_PATH", "W:/projects/foo"),
_multiline("Working_Folder_PATH", "e:/local/foo"),
]
out = loop_audio.extract_multilines(nodes, ["working_folder"])
assert out["working_folder"] == ["W:/projects/foo", "e:/local/foo"]
def test_extract_multilines_longest_needle_wins():
"""If a future config adds a short substring that's contained in
an existing longer one, the longer match should win — regardless
of declaration order. The pre-fix loop iterated in dict-insertion
order and would have routed both nodes to the shorter needle."""
nodes = [
_multiline("RELAY_OVERRIDES", "{video_strength: 10}"),
]
# Shorter "relay" declared FIRST in the config — pre-fix this would
# have shadowed "relay_overrides". Longest-first ordering routes
# the node to the more specific needle.
out = loop_audio.extract_multilines(
nodes, ["relay", "relay_overrides"]
)
assert out["relay_overrides"] == ["{video_strength: 10}"]
assert out["relay"] == []
def test_extract_multilines_first_match_wins_per_node():
"""A node's title can match multiple needles. The loop should
record at most one match per node (the longest) to avoid
double-counting."""
nodes = [_multiline("NAME overlay - info combined", "one node")]
out = loop_audio.extract_multilines(
nodes, ["name", "overlay - info"]
)
# Both needles match the title, but the longer one wins and the
# per-node break keeps the same node from being counted twice.
assert out["name"] == []
assert out["overlay - info"] == ["one node"]
def test_extract_multilines_accepts_prioritized_alias_map():
nodes = [
_multiline("NAME", "old setup name"),
_multiline("GLOBAL [ base name ]", "new base name"),
_multiline("GLOBAL [ path ] - working folder", "E:/runs"),
]
out = loop_audio.extract_multilines(
nodes,
{
"name": ["global [ base name ]", "name"],
"working_folder": [
"global [ path ] - working folder",
"working_folder",
],
},
)
assert out["name"] == ["new base name", "old setup name"]
assert out["working_folder"] == ["E:/runs"]
def test_extract_setup_variables_reads_text_and_primitive_source_nodes():
nodes = [
_multiline("INPUT Path [ EXR ]", "W:/plates"),
{
"type": "PrimitiveInt",
"title": "GLOBAL [ version ]",
"widgets_values": [1, "fixed"],
},
{
"type": "PrimitiveInt",
"title": "GLOBAL [ run offset ]",
"widgets_values": [7, "fixed"],
},
{
"type": "GetNode",
"title": "Get_GLOBAL [ version ]",
"widgets_values": ["GLOBAL [ version ]"],
},
{
"type": "SetNode",
"title": "Set_GLOBAL [ run ]",
"widgets_values": ["GLOBAL [ run ]"],
},
]
out = loop_audio.extract_setup_variables(
nodes,
{
"input_path_exr": ["input path [ exr ]"],
"version": ["global [ version ]"],
"run_offset": ["global [ run offset ]"],
},
)
assert out["input_path_exr"] == ["W:/plates"]
assert out["version"] == ["1"]
assert out["run_offset"] == ["7"]
def test_expected_output_tracking_uses_current_setup_values():
nodes = [
{
"type": "Easy_VideoCombine",
"widgets_values": {
"format": "video/koolook-ASTRA-h264",
},
},
{
"type": "easy showAnything",
"widgets_values": [
'[true, ["E:/old/Previous_h264_v001.mp4"]]',
],
},
]
out = loop_audio.expected_output_tracking(
nodes,
{
"working_folder": ["E:/current/renders"],
"name": ["Bear_2x-FR_AudioFile-K_Dir"],
},
{"version": ["2"]},
)
assert out["folder"] == "E:/current/renders"
assert out["name"] == "Bear_2x-FR_AudioFile-K_Dir_h264_v002"
def test_output_suffix_uses_named_widget_before_positional_fallback():
nodes = [
{
"type": "Easy_VideoCombine",
"inputs": [
{"name": "frame_rate", "widget": {"name": "frame_rate"}},
{"name": "format", "widget": {"name": "format"}},
{"name": "version", "widget": {"name": "version"}},
],
"widgets_values": [24, "video/ProRes", "999"],
},
]
assert loop_audio.output_suffix_from_workflow(nodes) == "ProRes"
def test_output_suffix_keeps_legacy_positional_fallback():
nodes = [
{
"type": "Easy_VideoCombine",
"widgets_values": [24, 0, "upscaled", "video/koolook-ASTRA-h264"],
},
]
assert loop_audio.output_suffix_from_workflow(nodes) == "h264"
def test_delivery_card_path_uses_output_folder_and_name():
out = loop_audio.delivery_card_path({
"folder": "E:/current/renders",
"name": "Bear_h264_v002",
})
assert out == Path("E:/current/renders") / "cards" / "Bear_h264_v002_card.png"
def test_delivery_card_path_can_include_run_number():
out = loop_audio.delivery_card_path(
{
"folder": "E:/current/renders",
"name": "Bear_h264_v002",
},
7,
)
assert out == (
Path("E:/current/renders")
/ "cards"
/ "Bear_h264_v002_run007_card.png"
)
def test_delivery_card_path_skips_when_output_is_unknown():
assert loop_audio.delivery_card_path({"folder": "E:/renders"}) is None
def test_copy_delivery_card_reports_failure_without_raising(monkeypatch, tmp_path: Path):
def fail_copy(_src, _dst):
raise OSError("drive unavailable")
monkeypatch.setattr(loop_audio.shutil, "copy2", fail_copy)
status = loop_audio.copy_delivery_card(
tmp_path / "card.png",
{"folder": str(tmp_path), "name": "Bear_h264_v002"},
)
assert status == "failed (drive unavailable)"
def test_copy_delivery_card_creates_missing_cards_folder(tmp_path: Path):
card = tmp_path / "source.png"
folder = tmp_path / "renders"
card.write_text("new", encoding="utf-8")
status = loop_audio.copy_delivery_card(
card,
{"folder": str(folder), "name": "Bear_h264_v002"},
7,
)
expected = folder / "cards" / "Bear_h264_v002_run007_card.png"
assert status == str(expected)
assert expected.read_text(encoding="utf-8") == "new"
def test_copy_delivery_card_leaves_existing_file_in_place(tmp_path: Path):
card = tmp_path / "source.png"
folder = tmp_path / "renders"
existing = folder / "cards" / "Bear_h264_v002_card.png"
card.write_text("new", encoding="utf-8")
existing.parent.mkdir(parents=True)
existing.write_text("old", encoding="utf-8")
status = loop_audio.copy_delivery_card(
card,
{"folder": str(folder), "name": "Bear_h264_v002"},
)
assert status.startswith("exists (left in place:")
assert existing.read_text(encoding="utf-8") == "old"
def test_copy_delivery_card_can_overwrite_existing_file(tmp_path: Path):
card = tmp_path / "source.png"
folder = tmp_path / "renders"
existing = folder / "cards" / "Bear_h264_v002_run007_card.png"
card.write_text("new", encoding="utf-8")
existing.parent.mkdir(parents=True)
existing.write_text("old", encoding="utf-8")
status = loop_audio.copy_delivery_card(
card,
{"folder": str(folder), "name": "Bear_h264_v002"},
7,
overwrite=True,
)
assert status == str(existing)
assert existing.read_text(encoding="utf-8") == "new"
def test_card_metadata_scrubs_path_bearing_fields():
metadata = loop_audio.card_metadata(
4,
"label",
Path("run004_workflow.json"),
{"name": ["Bear"], "relay_overrides": ["{}"]},
{"input_path_exr": ["W:/projects/client_codename/shot/v003"]},
{
"folder": "E:/Jobs/Client/Comfy/Runs",
"name": "Bear_h264_v002",
"version_tag": "v002",
"format_suffix": "h264",
},
_director(),
{"segments": [], "audioSegments": []},
"custom",
{},
"clean",
"1.3.2",
)
encoded = json.dumps(metadata)
assert "W:/projects" not in encoded
assert "client_codename" not in encoded
assert "E:/Jobs" not in encoded
assert "path-sha256:" in encoded
def test_card_metadata_does_not_store_boolean_frame_rate():
metadata = loop_audio.card_metadata(
4,
"label",
Path("run004_workflow.json"),
{"name": ["Bear"], "relay_overrides": ["{}"]},
{},
{"folder": "", "name": ""},
_director(fps=True),
{"segments": [], "audioSegments": []},
"model-gen",
{},
"clean",
"1.3.2",
)
assert metadata["director"]["frame_rate"] is None
def test_sanitize_workflow_for_archive_redacts_absolute_paths():
workflow = {
"nodes": [
{
"widgets_values": [
"E:/Jobs/Client/Comfy/Runs",
"notes\nW:/projects/client_codename/shot/v003\nok",
'["e:\\\\G-Drive-BaconX\\\\Jobs\\\\Jeep_Animals\\\\render.json"]',
"<PROJECTS>/samsung_goat/vfx/assets",
"W:/projects/client_codename/shot/v003",
"relative/path/is-kept",
"https://example.com/kept",
]
}
]
}
sanitized = loop_audio.sanitize_workflow_for_archive(workflow)
encoded = json.dumps(sanitized)
assert "E:/Jobs" not in encoded
assert "W:/projects" not in encoded
assert "client_codename" not in encoded
assert "samsung_goat" not in encoded
assert "relative/path/is-kept" in encoded
assert "https://example.com/kept" in encoded
assert "path-sha256:" in encoded
def test_render_notes_scrubs_paths_and_rejects_boolean_fps():
notes = loop_audio.render_notes_md(
5,
Path("LTX-23-audio_tests_03.json"),
{
"name": ["Bear"],
"relay_overrides": [""],
"overlay - info": [""],
"overlay - feedback": [""],
"working_folder": ["E:/Jobs/Client/Comfy/Runs"],
},
{
"input_path_exr": ["W:/projects/client_codename/shot/v003"],
"version": ["1"],
"run_offset": ["0"],
},
_director(fps=True),
{"segments": [], "audioSegments": []},
"custom",
"",
[],
{"motion": None, "sync": None, "sharp": None},
{"folder": "E:/Jobs/Client/Comfy/Runs", "name": "Bear_h264_v001"},
)
assert "E:/Jobs" not in notes
assert "W:/projects" not in notes
assert "client_codename" not in notes
assert "path-sha256:" in notes
assert "True fps" not in notes
assert "(unknown fps)" in notes
def test_audio_card_embeds_metadata_payload(tmp_path: Path):
from PIL import Image
from make_card_audio import render_audio_card
metadata = {
"schema": "koolook.audio_loop.card_metadata.v1",
"repo": {"main_sha": "abc1234"},
"output": {"name": "Bear_h264_v002"},
}
out = tmp_path / "card.png"
render_audio_card(
{
"run_number": 3,
"date": "2026-05-30",
"workflow_name": "workflow.json",
"name": "Bear",
"relay_overrides_raw": "{}",
"info_body": "",
"feedback_lines": [],
"scores": {},
"work_folder": "E:/renders",
"output_folder": "E:/renders",
"output_name": "Bear_h264_v002",
"director_flavor": "Koolook v1.3.9",
"audio_src": "custom",
"epsilon": 0.001,
"frame_rate": 24,
"segments": [],
"audio_segments": [],
"segment_prompt_mode": "none",
"metadata": metadata,
},
out,
)
embedded = json.loads(Image.open(out).info["koolook_audio_loop"])
assert embedded == metadata
def test_rebuild_state_handles_non_numeric_run_dir_and_bom_workflow(tmp_path: Path):
from make_card_audio import _rebuild_state_from_run_dir
run_dir = tmp_path / "run-foo_label"
run_dir.mkdir()
(run_dir / "workflow.json").write_text(
json.dumps({"nodes": []}),
encoding="utf-8-sig",
)
state = _rebuild_state_from_run_dir(run_dir)
assert state["run_number"] == 0
assert state["run_label"] == "label"
def test_rebuild_state_preserves_date_and_splits_repo_sync_metadata(tmp_path: Path):
from make_card_audio import _rebuild_state_from_run_dir
run_dir = tmp_path / "run-005_label"
run_dir.mkdir()
(run_dir / "run005_workflow.json").write_text(
json.dumps({"nodes": [], "links": []}),
encoding="utf-8",
)
(run_dir / "metadata.json").write_text(
json.dumps({"run": {"date": "2026-05-01"}}),
encoding="utf-8",
)
(run_dir / "patch_state.txt").write_text(
"\n".join(
[
"MAIN SHA : abc1234",
"Last dev-sync-audio : def5678 (2026-05-02 11:22)",
"Sync scope tag : relay parser",
"Sync worktree : ComfyUI-Koolook",
"Fork dir status : clean",
]
),
encoding="utf-8",
)
state = _rebuild_state_from_run_dir(run_dir)
metadata = state["metadata"]
assert state["date"] == "2026-05-01"
assert metadata["run"]["date"] == "2026-05-01"
assert metadata["repo"]["last_dev_sync_audio"] == "def5678"
assert metadata["repo"]["last_dev_sync_at"] == "2026-05-02 11:22"
def test_extract_multilines_ignores_non_text_multiline_nodes():
nodes = [
{"type": "LTXDirector__koolook_v1_3_2", "title": "NAME"},
_multiline("NAME", "real"),
]
out = loop_audio.extract_multilines(nodes, ["name"])
assert out["name"] == ["real"]
# --- parse_timeline — coerce numeric segment fields ------------------------
def test_parse_timeline_coerces_string_numerics():
"""The Comfy frontend sometimes saves numeric segment fields as
strings. parse_timeline must coerce them so downstream arithmetic
in the renderer (`start / fps`, `<` comparisons) stays type-safe
instead of crashing or doing lexical comparisons."""
node = _director(timeline={
"segments": [
{"id": "a", "start": "0", "length": "120", "prompt": "x"},
],
"audioSegments": [],
})
tl = loop_audio.parse_timeline(node)
seg = tl["segments"][0]
assert seg["start"] == 0
assert seg["length"] == 120
assert isinstance(seg["start"], int)
assert isinstance(seg["length"], int)
# Non-numeric fields pass through.
assert seg["prompt"] == "x"
def test_parse_timeline_coerces_float_to_int():
node = _director(timeline={
"segments": [{"start": 1.7, "length": 12.4}],
"audioSegments": [],
})
tl = loop_audio.parse_timeline(node)
# int(float(...)) truncates toward 0, matching the upstream
# Director's _build_combined_audio coercion behavior.
assert tl["segments"][0]["start"] == 1
assert tl["segments"][0]["length"] == 12
def test_parse_timeline_collapses_bad_values_to_zero():
"""A malformed segment shouldn't crash the whole loop — the
offending field collapses to 0 and we keep going."""
node = _director(timeline={
"segments": [{"start": "not a number", "length": 100}],
"audioSegments": [],
})
tl = loop_audio.parse_timeline(node)
assert tl["segments"][0]["start"] == 0
assert tl["segments"][0]["length"] == 100
def test_parse_timeline_handles_malformed_json():
"""Invalid JSON in timeline_data must not raise — the helper
returns empty lists so the renderer falls back to "(N=0)" and
proceeds."""
bad = _director()
bad["widgets_values"][3] = "{not json"
assert loop_audio.parse_timeline(bad) == {
"segments": [], "audioSegments": [],
}
def test_parse_timeline_handles_missing_director():
assert loop_audio.parse_timeline(None) == {
"segments": [], "audioSegments": [],
}
def test_parse_timeline_drops_non_dict_segments():
node = _director(timeline={
"segments": [
{"start": 0, "length": 60}, # kept
"not a dict", # dropped
None, # dropped
],
"audioSegments": [],
})
tl = loop_audio.parse_timeline(node)
assert len(tl["segments"]) == 1
# --- video_segment_has_audio — overlap boundaries --------------------------
@pytest.mark.parametrize(
"video, audio_segs, expected",
[
# Same range — overlaps.
({"start": 0, "length": 100}, [{"start": 0, "length": 100}], True),
# Audio starts inside video — overlaps.
({"start": 0, "length": 100}, [{"start": 50, "length": 50}], True),
# Audio ends inside video — overlaps.
({"start": 50, "length": 100}, [{"start": 0, "length": 75}], True),
# Audio strictly before video — no overlap.
({"start": 100, "length": 50}, [{"start": 0, "length": 100}], False),
# Audio strictly after video — no overlap.
({"start": 0, "length": 50}, [{"start": 100, "length": 50}], False),
# Audio exactly touches video end — half-open intervals
# (a_start < v_end) treats this as NOT overlapping.
({"start": 0, "length": 50}, [{"start": 50, "length": 50}], False),
# Multiple audio segs — any one overlap is enough.
(
{"start": 0, "length": 50},
[{"start": 100, "length": 10}, {"start": 25, "length": 10}],
True,
),
# No audio segs — false.
({"start": 0, "length": 50}, [], False),
],
)
def test_video_segment_has_audio_boundaries(video, audio_segs, expected):
assert loop_audio.video_segment_has_audio(video, audio_segs) is expected
# --- segment_prompt_mode — same vs per-segment prompt check -----------------
@pytest.mark.parametrize(
"segments, expected",
[
([], "none"),
([{"prompt": "one prompt"}], "single"),
(
[{"prompt": "same prompt"}, {"prompt": "same prompt"}],
"same",
),
(
[{"prompt": "wide shot"}, {"prompt": "close up"}],
"per-segment",
),
(
[{"prompt": "wide shot"}, {"prompt": ""}],
"missing",
),
],
)
def test_segment_prompt_mode_classifies_prompt_sequence(segments, expected):
assert loop_audio.segment_prompt_mode(segments) == expected
# --- parse_feedback — score 0 must round-trip (PR #185 review MEDIUM-9) ----
def test_parse_feedback_extracts_scores_with_lines():
body = (
"Looking solid overall\n"
"Sync drifts in the last second\n"
"motion: 4/5\n"
"sync: 3/5\n"
"sharpness: 5/5\n"
)
scores, lines = loop_audio.parse_feedback(body)
assert scores == {"motion": 4, "sync": 3, "sharp": 5}
assert lines == ["Looking solid overall", "Sync drifts in the last second"]
def test_parse_feedback_preserves_zero_as_score():
"""0 is a legitimate score. Pre-fix the log row coerced it to '?'
via `or '?'`; parse_feedback itself stores 0 correctly — this test
pins that contract so a future refactor can't subtly inject `or 0`
semantics."""
scores, _ = loop_audio.parse_feedback("motion: 0/5\nsync: 0\nsharp: 0\n")
assert scores == {"motion": 0, "sync": 0, "sharp": 0}
def test_render_log_row_preserves_zero_scores():
row = loop_audio.render_log_row(
3,
_director(),
"",
"model-gen",
{"segments": [], "audioSegments": []},
{"motion": 0, "sync": 0, "sharp": 0},
[],
)
assert "M0·S0·Sh0" in row
def test_render_log_row_records_video_and_audio_segment_counts():
row = loop_audio.render_log_row(
3,
_director(),
"",
"custom",
{"segments": [{}, {}], "audioSegments": [{}, {}]},
{"motion": None, "sync": None, "sharp": None},
[],
)
assert "| custom | 2v/2a |" in row
def test_parse_feedback_accepts_sharpness_alias():
"""Both 'sharp' and 'sharpness' are accepted axis names. Both map
to the 'sharp' key."""
scores, _ = loop_audio.parse_feedback("sharpness: 4\n")
assert scores["sharp"] == 4
def test_parse_feedback_case_insensitive():
scores, _ = loop_audio.parse_feedback("MOTION: 3\nSync 4\n")
assert scores["motion"] == 3
assert scores["sync"] == 4
def test_parse_feedback_empty_body_returns_blank_scores():
scores, lines = loop_audio.parse_feedback("")
assert scores == {"motion": None, "sync": None, "sharp": None}
assert lines == []
# --- is_input_wired — None vs False semantics matter for derive_audio_state ---
def test_is_input_wired_none_when_director_missing():
assert loop_audio.is_input_wired(None, "audio_vae") is None
def test_is_input_wired_true_when_link_set():
node = {"inputs": [{"name": "audio_vae", "link": 42}]}
assert loop_audio.is_input_wired(node, "audio_vae") is True
def test_is_input_wired_false_when_link_null():
node = {"inputs": [{"name": "audio_vae", "link": None}]}
assert loop_audio.is_input_wired(node, "audio_vae") is False
def test_is_input_wired_none_when_input_socket_absent():
"""An older Director schema might not have the named socket at
all — same outcome as 'unwired' at runtime (no audio latent
produced), but a distinct value here so callers can tell."""
node = {"inputs": [{"name": "model", "link": 1}]}
assert loop_audio.is_input_wired(node, "audio_vae") is None
# --- wrap_path — long mount paths shouldn't bleed past the card edge -------
def test_wrap_path_breaks_on_separator():
out = loop_audio.wrap_path(
"e:/G-Drive-BaconX/Jobs/Jeep_Animals/ComfyUI_LTX23/Phase2",
max_chars=30,
)
# Joined with backslashes, never split mid-segment.
for line in out:
assert "/" not in line # normalised to backslash separators
joined = "".join(out)
assert joined == "e:\\G-Drive-BaconX\\Jobs\\Jeep_Animals\\ComfyUI_LTX23\\Phase2"
def test_wrap_path_returns_single_empty_string_for_empty_input():
assert loop_audio.wrap_path("") == [""]
def test_wrap_path_handles_single_segment_longer_than_max():
"""A directory name longer than max_chars still gets its own
line — we never split mid-name."""
out = loop_audio.wrap_path("verylongsingledirectoryname", max_chars=10)
assert len(out) == 1
assert out[0] == "verylongsingledirectoryname"
# --- pick_existing_path — picks reachable path, falls back to first ------
def test_pick_existing_path_prefers_real_directory(tmp_path):
real = tmp_path / "real"
real.mkdir()
out = loop_audio.pick_existing_path([
"Z:/never-exists",
str(real),
"Y:/also-never",
])
assert out == str(real)
def test_pick_existing_path_falls_back_to_first_nonempty():
out = loop_audio.pick_existing_path(["Z:/never-exists", "Y:/also-never"])
assert out == "Z:/never-exists"
def test_pick_existing_path_returns_empty_when_all_empty():
assert loop_audio.pick_existing_path(["", " ", "\""]) == ""
# --- first_multiline + autogen_label sanity checks ------------------------
def test_first_multiline_returns_first_or_empty():
assert loop_audio.first_multiline({"name": ["foo", "bar"]}, "name") == "foo"
assert loop_audio.first_multiline({}, "name") == ""
assert loop_audio.first_multiline({"name": []}, "name") == ""
def test_autogen_label_when_director_missing():
label = loop_audio.autogen_label("Bear_3x", None, "")
assert "missing" in label
assert "audio-off" in label
def test_autogen_label_when_director_present():
node = _director(use_custom_audio=True, audio_vae_link=42)
label = loop_audio.autogen_label(
"Bear_3x", node, '{"video_strength": 10.0}'
)
assert "koolook" in label
assert "audio-on" in label
assert "vstr10.0" in label
def test_autogen_label_when_director_is_upstream():
label = loop_audio.autogen_label(
"Bear_3x", _director(node_type="LTXDirector"), ""
)
assert "upstream" in label
assert "audio-off" in label
def test_relay_overrides_txt_marks_upstream_director_inert():
txt = loop_audio.render_relay_overrides_txt(
'{"video_strength": 10.0}',
_director(node_type="LTXDirector"),
)
assert "INERT" in txt
assert "LTXDirector" in txt
def test_next_run_number_reads_folders_and_log(tmp_path):
runs = tmp_path / "runs"
runs.mkdir()
(runs / "run-001_alpha").mkdir()
(runs / "log.md").write_text(
"| Run | Date |\n"
"|---|---|\n"
"| 002 | 2026-05-29 |\n",
encoding="utf-8",
)
assert loop_audio.next_run_number(runs) == 3