Spaces:
Running on Zero
Running on Zero
fix(workflow): support dict-keyed widgets_values for VHS audio/video nodes
Browse filesVHS_LoadAudioUpload and VHS_LoadVideo carry dict-style widgets_values.
set_input now accepts string keys for dict widgets. A2V, Lipsync, and Style
parameterize_fns updated to use the right keys so file paths actually flow
through at runtime.
- modes.py +4 -4
- tests/test_workflow.py +16 -0
- workflow.py +36 -7
modes.py
CHANGED
|
@@ -20,7 +20,7 @@ from collections.abc import Callable
|
|
| 20 |
from dataclasses import dataclass, field
|
| 21 |
from typing import Any
|
| 22 |
|
| 23 |
-
Patch = tuple[int, int, Any]
|
| 24 |
ParameterizeFn = Callable[[dict[str, Any]], list[Patch]]
|
| 25 |
|
| 26 |
|
|
@@ -159,7 +159,7 @@ def _a2v_parameterize(inp: dict[str, Any]) -> list[Patch]:
|
|
| 159 |
return [
|
| 160 |
(A2V_NODE_PROMPT, 0, inp["prompt"]),
|
| 161 |
(A2V_NODE_NEG_PROMPT, 0, inp.get("negative_prompt", "")),
|
| 162 |
-
(A2V_NODE_AUDIO,
|
| 163 |
(A2V_NODE_WIDTH, 0, int(inp["width"])),
|
| 164 |
(A2V_NODE_HEIGHT, 0, int(inp["height"])),
|
| 165 |
(A2V_NODE_FPS, 0, int(inp["fps"])),
|
|
@@ -172,7 +172,7 @@ def _lipsync_parameterize(inp: dict[str, Any]) -> list[Patch]:
|
|
| 172 |
(LIPSYNC_NODE_PROMPT, 0, inp["prompt"]),
|
| 173 |
(LIPSYNC_NODE_NEG_PROMPT, 0, inp.get("negative_prompt", "")),
|
| 174 |
(LIPSYNC_NODE_IMAGE, 0, inp["image"]),
|
| 175 |
-
(LIPSYNC_NODE_AUDIO,
|
| 176 |
(LIPSYNC_NODE_FPS, 0, int(inp["fps"])),
|
| 177 |
(LIPSYNC_NODE_CLIP_LENGTH, 0, _frames_to_seconds(int(inp["frames"]), int(inp["fps"]))),
|
| 178 |
]
|
|
@@ -193,7 +193,7 @@ def _style_parameterize(inp: dict[str, Any]) -> list[Patch]:
|
|
| 193 |
return [
|
| 194 |
(STYLE_NODE_PROMPT, 0, inp["prompt"]),
|
| 195 |
(STYLE_NODE_NEG_PROMPT, 0, inp.get("negative_prompt", "")),
|
| 196 |
-
(STYLE_NODE_INPUT_VIDEO,
|
| 197 |
(STYLE_NODE_FPS, 0, int(inp["fps"])),
|
| 198 |
(STYLE_NODE_CLIP_LENGTH, 0, _frames_to_seconds(int(inp["frames"]), int(inp["fps"]))),
|
| 199 |
]
|
|
|
|
| 20 |
from dataclasses import dataclass, field
|
| 21 |
from typing import Any
|
| 22 |
|
| 23 |
+
Patch = tuple[int, int | str, Any]
|
| 24 |
ParameterizeFn = Callable[[dict[str, Any]], list[Patch]]
|
| 25 |
|
| 26 |
|
|
|
|
| 159 |
return [
|
| 160 |
(A2V_NODE_PROMPT, 0, inp["prompt"]),
|
| 161 |
(A2V_NODE_NEG_PROMPT, 0, inp.get("negative_prompt", "")),
|
| 162 |
+
(A2V_NODE_AUDIO, "audio", inp["audio"]),
|
| 163 |
(A2V_NODE_WIDTH, 0, int(inp["width"])),
|
| 164 |
(A2V_NODE_HEIGHT, 0, int(inp["height"])),
|
| 165 |
(A2V_NODE_FPS, 0, int(inp["fps"])),
|
|
|
|
| 172 |
(LIPSYNC_NODE_PROMPT, 0, inp["prompt"]),
|
| 173 |
(LIPSYNC_NODE_NEG_PROMPT, 0, inp.get("negative_prompt", "")),
|
| 174 |
(LIPSYNC_NODE_IMAGE, 0, inp["image"]),
|
| 175 |
+
(LIPSYNC_NODE_AUDIO, "audio", inp["audio"]),
|
| 176 |
(LIPSYNC_NODE_FPS, 0, int(inp["fps"])),
|
| 177 |
(LIPSYNC_NODE_CLIP_LENGTH, 0, _frames_to_seconds(int(inp["frames"]), int(inp["fps"]))),
|
| 178 |
]
|
|
|
|
| 193 |
return [
|
| 194 |
(STYLE_NODE_PROMPT, 0, inp["prompt"]),
|
| 195 |
(STYLE_NODE_NEG_PROMPT, 0, inp.get("negative_prompt", "")),
|
| 196 |
+
(STYLE_NODE_INPUT_VIDEO, "video", inp["input_video"]),
|
| 197 |
(STYLE_NODE_FPS, 0, int(inp["fps"])),
|
| 198 |
(STYLE_NODE_CLIP_LENGTH, 0, _frames_to_seconds(int(inp["frames"]), int(inp["fps"]))),
|
| 199 |
]
|
tests/test_workflow.py
CHANGED
|
@@ -54,3 +54,19 @@ def test_validate_rejects_orphan_link():
|
|
| 54 |
wf["links"].append([99999, 1, 0, 999_999_999, 0, "INT"]) # destination doesn't exist
|
| 55 |
with pytest.raises(ValueError, match="orphan link"):
|
| 56 |
workflow.validate(wf)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 54 |
wf["links"].append([99999, 1, 0, 999_999_999, 0, "INT"]) # destination doesn't exist
|
| 55 |
with pytest.raises(ValueError, match="orphan link"):
|
| 56 |
workflow.validate(wf)
|
| 57 |
+
|
| 58 |
+
|
| 59 |
+
def test_set_input_handles_dict_widgets_values():
|
| 60 |
+
"""VHS_* nodes carry dict-style widgets_values; set_input must support str keys."""
|
| 61 |
+
wf = workflow.load_template("a2v")
|
| 62 |
+
# Find a node whose widgets_values is a dict (e.g., VHS_LoadAudioUpload).
|
| 63 |
+
target = next(
|
| 64 |
+
(n for n in wf["nodes"] if isinstance(n.get("widgets_values"), dict)),
|
| 65 |
+
None,
|
| 66 |
+
)
|
| 67 |
+
assert target is not None, "no dict-widgets node in a2v template"
|
| 68 |
+
# Pick an existing key to patch (don't invent one — tests should reflect real graph shape).
|
| 69 |
+
existing_key = next(iter(target["widgets_values"].keys()))
|
| 70 |
+
workflow.set_input(wf, target["id"], existing_key, "/tmp/new_value.wav")
|
| 71 |
+
refetched = next(n for n in wf["nodes"] if n["id"] == target["id"])
|
| 72 |
+
assert refetched["widgets_values"][existing_key] == "/tmp/new_value.wav"
|
workflow.py
CHANGED
|
@@ -19,25 +19,54 @@ def load_template(mode: str) -> dict[str, Any]:
|
|
| 19 |
return copy.deepcopy(json.loads(path.read_text()))
|
| 20 |
|
| 21 |
|
| 22 |
-
def set_input(workflow: dict[str, Any], node_id: int, widget_index: int, value: Any) -> None:
|
| 23 |
"""Patch a node's widgets_values in place.
|
| 24 |
|
|
|
|
|
|
|
|
|
|
|
|
|
| 25 |
Args:
|
| 26 |
workflow: A workflow dict (must have a "nodes" list).
|
| 27 |
node_id: The id of the node to patch.
|
| 28 |
-
widget_index:
|
| 29 |
value: New value.
|
| 30 |
|
| 31 |
Raises:
|
| 32 |
-
KeyError: If no node with the given id exists
|
|
|
|
|
|
|
| 33 |
"""
|
| 34 |
for node in workflow["nodes"]:
|
| 35 |
-
if node.get("id") =
|
| 36 |
-
|
| 37 |
-
|
| 38 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 39 |
widgets[widget_index] = value
|
| 40 |
return
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 41 |
raise KeyError(f"node id {node_id} not found in workflow")
|
| 42 |
|
| 43 |
|
|
|
|
| 19 |
return copy.deepcopy(json.loads(path.read_text()))
|
| 20 |
|
| 21 |
|
| 22 |
+
def set_input(workflow: dict[str, Any], node_id: int, widget_index: int | str, value: Any) -> None:
|
| 23 |
"""Patch a node's widgets_values in place.
|
| 24 |
|
| 25 |
+
Supports both list-style widgets_values (most ComfyUI nodes — patch by integer index,
|
| 26 |
+
auto-extending with None) and dict-style widgets_values (VHS_LoadAudioUpload and
|
| 27 |
+
similar — patch by string key, raising KeyError if the key doesn't exist).
|
| 28 |
+
|
| 29 |
Args:
|
| 30 |
workflow: A workflow dict (must have a "nodes" list).
|
| 31 |
node_id: The id of the node to patch.
|
| 32 |
+
widget_index: Integer index (for list widgets) or string key (for dict widgets).
|
| 33 |
value: New value.
|
| 34 |
|
| 35 |
Raises:
|
| 36 |
+
KeyError: If no node with the given id exists, or for dict widgets, if the key
|
| 37 |
+
doesn't already exist on the target dict (we don't add new keys).
|
| 38 |
+
TypeError: If widget_index type doesn't match the node's widgets_values type.
|
| 39 |
"""
|
| 40 |
for node in workflow["nodes"]:
|
| 41 |
+
if node.get("id") != node_id:
|
| 42 |
+
continue
|
| 43 |
+
widgets = node.get("widgets_values")
|
| 44 |
+
if isinstance(widgets, dict):
|
| 45 |
+
if not isinstance(widget_index, str):
|
| 46 |
+
raise TypeError(
|
| 47 |
+
f"node {node_id} has dict widgets_values; widget_index must be str, "
|
| 48 |
+
f"got {type(widget_index).__name__}"
|
| 49 |
+
)
|
| 50 |
+
if widget_index not in widgets:
|
| 51 |
+
raise KeyError(
|
| 52 |
+
f"node {node_id} dict widgets_values has no key {widget_index!r}; "
|
| 53 |
+
f"available keys: {list(widgets.keys())}"
|
| 54 |
+
)
|
| 55 |
widgets[widget_index] = value
|
| 56 |
return
|
| 57 |
+
# List/None case — preserve existing list-extension behavior.
|
| 58 |
+
if not isinstance(widget_index, int):
|
| 59 |
+
raise TypeError(
|
| 60 |
+
f"node {node_id} has list widgets_values; widget_index must be int, "
|
| 61 |
+
f"got {type(widget_index).__name__}"
|
| 62 |
+
)
|
| 63 |
+
if widgets is None:
|
| 64 |
+
widgets = []
|
| 65 |
+
node["widgets_values"] = widgets
|
| 66 |
+
while len(widgets) <= widget_index:
|
| 67 |
+
widgets.append(None)
|
| 68 |
+
widgets[widget_index] = value
|
| 69 |
+
return
|
| 70 |
raise KeyError(f"node id {node_id} not found in workflow")
|
| 71 |
|
| 72 |
|