techfreakworm commited on
Commit
fee06dd
·
unverified ·
1 Parent(s): 03937ef

fix(workflow): support dict-keyed widgets_values for VHS audio/video nodes

Browse files

VHS_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.

Files changed (3) hide show
  1. modes.py +4 -4
  2. tests/test_workflow.py +16 -0
  3. 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, 0, 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,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, 0, 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,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, 0, 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
  ]
 
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: Position within the node's widgets_values list.
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") == node_id:
36
- widgets = node.setdefault("widgets_values", [])
37
- while len(widgets) <= widget_index:
38
- widgets.append(None)
 
 
 
 
 
 
 
 
 
 
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