Files
wildminder-ComfyUI-VibeVoice/tests/test_workflow.py
T
WildAi e20b4fd9d8 feat: external model input via VibeVoiceLoadExternalModel node (v2.2.0)
Add a dedicated loader node that loads VibeVoice checkpoints from user-provided .safetensors/.pt files in models/diffusion_models, since ComfyUI's stock Load Diffusion Model cannot detect VibeVoice architecture. The loader emits a VIBEVOICE_MODEL custom type bundle (model/processor/config/state_dict) consumed by an optional external_model input on the TTS, Realtime, and ASR nodes.

- modules/custom_types.py: VibeVoiceModel = io.Custom("VIBEVOICE_MODEL")

- modules/external_loader.py: sidecar config/preprocessor/tokenizer resolution + load_external_vibevoice_model() (TTS/streaming) and load_external_vibevoice_asr_model() (ASR); CPU-first load, in-memory state-dict injection, dtype cast, optional 4-bit quant (TTS only), SageAttention

- nodes/external_loader_node.py: VibeVoiceLoadExternalModel node

- modules/generation.py: ExternalVibeVoiceModelHandler + load_vibevoice_from_external()

- modules/asr_generation.py: ExternalVibeVoiceASRModelHandler + load_asr_from_external()

- nodes/tts_node.py, realtime_node.py, asr_node.py: optional external_model input with kind guards (streaming/ASR/TTS mismatch rejection)

- README: Loading External Models section + v2.2.0 changelog

- example_workflows/VibeVoice_external_model_example.json

- docs/plans/2026-08-16-external-model-input.md (status: COMPLETED)

Tests: +122 new/extended (test_custom_types, test_external_loader, test_external_loader_node, plus extensions to node_schema, generation, asr_generation, realtime_node, asr_node, patcher_behavioral, integration, docs_consistency, workflow, imports, extension). Full suite: 687 passed, 5 pre-existing failures, 4 skipped.
2026-08-16 23:59:04 +03:00

103 lines
3.8 KiB
Python

"""Tests for example workflow JSON."""
import os
import json
import pytest
class TestWorkflow:
"""Test the example workflow JSON."""
def _get_workflow_path(self):
root = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
return os.path.join(root, "example_workflows", "VibeVoice_example.json")
def test_workflow_json_valid(self):
"""The workflow JSON should be valid JSON."""
path = self._get_workflow_path()
with open(path, "r", encoding="utf-8") as f:
data = json.load(f)
assert data is not None
def test_workflow_has_nodes(self):
"""The workflow should contain nodes."""
path = self._get_workflow_path()
with open(path, "r", encoding="utf-8") as f:
data = json.load(f)
# Workflow format varies; check for common keys
if "nodes" in data:
assert len(data["nodes"]) > 0
elif "prompt" in data:
assert len(data["prompt"]) > 0
class TestExternalModelWorkflow:
"""Phase 6.2: the external-model example workflow must be valid."""
def _get_workflow_path(self):
root = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
return os.path.join(
root, "example_workflows", "VibeVoice_external_model_example.json"
)
def test_external_model_workflow_json_is_valid(self):
"""The external-model workflow JSON parses and contains nodes."""
path = self._get_workflow_path()
with open(path, "r", encoding="utf-8") as f:
data = json.load(f)
assert data is not None
assert "nodes" in data
assert len(data["nodes"]) > 0
def test_external_model_workflow_has_loader_node(self):
"""The workflow contains the Load VibeVoice Model node."""
path = self._get_workflow_path()
with open(path, "r", encoding="utf-8") as f:
data = json.load(f)
node_types = [n.get("type") for n in data["nodes"]]
assert "VibeVoiceLoadExternalModel" in node_types
def test_external_model_workflow_has_tts_node(self):
"""The workflow contains the VibeVoice TTS node."""
path = self._get_workflow_path()
with open(path, "r", encoding="utf-8") as f:
data = json.load(f)
node_types = [n.get("type") for n in data["nodes"]]
assert "VibeVoiceTTS" in node_types
def test_external_model_workflow_links_loader_to_tts(self):
"""A VIBEVOICE_MODEL link connects the loader node to the TTS node."""
path = self._get_workflow_path()
with open(path, "r", encoding="utf-8") as f:
data = json.load(f)
# Map node id → type.
node_by_id = {n["id"]: n.get("type") for n in data["nodes"]}
# links format: [link_id, from_node, from_slot, to_node, to_slot, type]
vibevoice_model_links = [
link for link in data.get("links", [])
if link[5] == "VIBEVOICE_MODEL"
]
assert len(vibevoice_model_links) >= 1
link = vibevoice_model_links[0]
assert node_by_id.get(link[1]) == "VibeVoiceLoadExternalModel"
assert node_by_id.get(link[3]) == "VibeVoiceTTS"
def test_external_model_workflow_tts_has_external_model_input(self):
"""The TTS node declares an external_model input wired to the link."""
path = self._get_workflow_path()
with open(path, "r", encoding="utf-8") as f:
data = json.load(f)
tts_node = next(
n for n in data["nodes"] if n.get("type") == "VibeVoiceTTS"
)
ext_inputs = [
i for i in tts_node.get("inputs", [])
if i.get("name") == "external_model"
]
assert len(ext_inputs) == 1
assert ext_inputs[0].get("type") == "VIBEVOICE_MODEL"
assert ext_inputs[0].get("link") is not None