Suite goes from 1894 passed / 66 failed to 1876 passed / 0 failed.
The 66 failures were not 66 problems. Two mechanisms caused all of them:
* comfy_stream._STREAMING_CONVERSION_ENABLED is False and nothing in
production sets it, so convert_tree_for_streaming is a no-op. Every
test asserting "this module was converted" was asserting on a sweep
that never ran.
* select_patcher_class ignores both its arguments and returns
legacy_cls. Every test parametrized over a "dynamic" arm was
exercising a configuration production cannot produce.
Deleted (assert wiring that no longer exists): test_dynamic_patcher_
selection.py, test_dynamic_vram_mechanism.py, test_ram_residual_
attribution.py, test_loader_streaming_integration.py (three of its five
tests were passing VACUOUSLY once conversion went), and
TestCensusOnRealDynamicRoute.
Corrected to the real contract, keeping the coverage: the conversion
tests now enable the gate explicitly and test the capability, which is
what they were written for; the TestP1 load tests now assert that the
LOADER places each tensor (it took a target_device) instead of asserting
the superseded CPU staging; the both-patcher-classes tests were
de-parametrized onto the selector's real answer.
Two production error messages had lost the hints their tests pinned, and
the tests were right: UnmappedKeyError no longer told a user with a
non-standard converter to place a sidecar config, and the GGUF block-size
error no longer said the layout could not be recovered.
Also fixes a latent bug: test_comfy_stream.py imported `modules.comfy_stream`
while everything else imported `ComfyUI_VibeVoice.modules.comfy_stream`.
Under pytest those are two distinct module objects with independent
globals, so a gate set through one was invisible to the other. All bare
`modules.*` imports in collected tests now use the package name.
Portability, verified by simulation rather than inspection:
* conftest no longer falls back to a hardcoded C:\_Dev ComfyUI path. It
used to put a nonexistent directory on sys.path off-box and surface
as an opaque import error across 14 test files; now it names
COMFYUI_ROOT and stops.
* The 5.4 GB and 3.2 GB checkpoint fixtures are opt-in via env vars with
no machine-specific default (VIBEVOICE_TEST_DENSE_CHECKPOINT,
VIBEVOICE_TEST_GGUF).
* comfy_kitchen guards added. It is genuinely optional; a driver
script that blocks the import went from 9 failed to 37 passed /
11 skipped / 0 errors.
* comfy_aimdo import guarded, though it is in practice a hard
dependency of the ComfyUI fork (core imports it unconditionally).
* test_audio_backend writes to tmp_path instead of into the repo tree.
* test_asr_loader no longer registers the real folder_paths.models_dir
in an autouse fixture without restoring it.
The large-checkpoint and .dev-symlink tests skip cleanly off-box; that is
the intended shape for files that cannot ship with the repo.
569 lines
24 KiB
Python
569 lines
24 KiB
Python
"""Integration tests for VibeVoice TTS pipeline (mocked)."""
|
|
|
|
import numpy as np
|
|
import torch
|
|
import pytest
|
|
from unittest.mock import patch, MagicMock
|
|
|
|
from ComfyUI_VibeVoice.modules.generation import load_vibevoice_model, generate_audio, force_offload_model
|
|
from ComfyUI_VibeVoice.modules.utils import VIBEVOICE_PATCHER_CACHE
|
|
from ComfyUI_VibeVoice.modules.loader import LOADED_MODELS_CACHE
|
|
|
|
|
|
def _mock_voice_sample(length: int = 24000) -> np.ndarray:
|
|
"""Create a mock 1-D voice sample numpy array."""
|
|
return np.random.randn(length).astype(np.float32)
|
|
|
|
|
|
def _mock_audio_dict(length: int = 24000, sr: int = 24000) -> dict:
|
|
"""Create a mock ComfyUI AUDIO dict."""
|
|
return {"waveform": torch.randn(1, 1, length), "sample_rate": sr}
|
|
|
|
|
|
def _patch_model_management():
|
|
"""Patch generation.model_management with a realistic stand-in.
|
|
|
|
BUG-012: generate_audio places inputs on the runtime's compute device
|
|
(``model_management.get_torch_device()``), NOT ``model.device`` (which
|
|
lies under partial offload). A bare MagicMock breaks ``Tensor.to()``,
|
|
so the mock must return a real torch.device.
|
|
"""
|
|
mm = MagicMock()
|
|
mm.get_torch_device.return_value = torch.device("cpu")
|
|
return patch("ComfyUI_VibeVoice.modules.generation.model_management", mm)
|
|
|
|
|
|
class TestFullPipelineMocked:
|
|
"""End-to-end pipeline tests with mocked model."""
|
|
|
|
def test_full_pipeline_mocked(self):
|
|
"""Test the full pipeline with a mocked model."""
|
|
VIBEVOICE_PATCHER_CACHE.clear()
|
|
LOADED_MODELS_CACHE.clear()
|
|
|
|
mock_model = MagicMock()
|
|
mock_model.device = torch.device("cpu")
|
|
mock_output = MagicMock()
|
|
mock_output.speech_outputs = [torch.randn(24000)]
|
|
mock_model.generate.return_value = mock_output
|
|
|
|
mock_processor = MagicMock()
|
|
mock_processor.return_value = {"input_ids": torch.randint(0, 100, (1, 10))}
|
|
mock_processor.tokenizer = MagicMock()
|
|
|
|
mock_patcher = MagicMock()
|
|
mock_patcher.model.model = mock_model
|
|
mock_patcher.model.processor = mock_processor
|
|
|
|
with patch("ComfyUI_VibeVoice.modules.generation.VibeVoiceModelHandler") as mock_handler_cls, \
|
|
patch("ComfyUI_VibeVoice.modules.generation.VibeVoicePatcher", return_value=mock_patcher), \
|
|
patch("ComfyUI_VibeVoice.modules.generation.model_management.load_model_gpu"), \
|
|
patch("ComfyUI_VibeVoice.modules.generation.ProgressBarWithConsole"), \
|
|
patch("ComfyUI_VibeVoice.modules.generation.preprocess_comfy_audio", return_value=_mock_voice_sample()):
|
|
mock_handler = MagicMock()
|
|
mock_handler.size = 1000
|
|
mock_handler_cls.return_value = mock_handler
|
|
|
|
patcher, model, processor = load_vibevoice_model(
|
|
model_name="TestModel",
|
|
device="cpu",
|
|
dtype="fp32",
|
|
attention_mode="sdpa",
|
|
)
|
|
|
|
assert model is not None
|
|
assert processor is not None
|
|
|
|
waveform, sr = generate_audio(
|
|
model=model,
|
|
processor=processor,
|
|
text="[1] Hello world",
|
|
voice_samples=[_mock_audio_dict()],
|
|
speaker_ids=[1],
|
|
)
|
|
|
|
assert waveform is not None
|
|
assert sr == 24000
|
|
assert waveform.ndim == 3
|
|
|
|
VIBEVOICE_PATCHER_CACHE.clear()
|
|
LOADED_MODELS_CACHE.clear()
|
|
|
|
def test_model_loading_flow(self):
|
|
"""Test that model loading creates a patcher and loads via ComfyUI."""
|
|
VIBEVOICE_PATCHER_CACHE.clear()
|
|
LOADED_MODELS_CACHE.clear()
|
|
|
|
mock_patcher = MagicMock()
|
|
mock_patcher.model.model = MagicMock()
|
|
mock_patcher.model.processor = MagicMock()
|
|
|
|
with patch("ComfyUI_VibeVoice.modules.generation.VibeVoiceModelHandler") as mock_handler_cls, \
|
|
patch("ComfyUI_VibeVoice.modules.generation.VibeVoicePatcher", return_value=mock_patcher) as mock_patcher_cls, \
|
|
patch("ComfyUI_VibeVoice.modules.generation.model_management.load_model_gpu") as mock_load_gpu:
|
|
mock_handler = MagicMock()
|
|
mock_handler.size = 1000
|
|
mock_handler_cls.return_value = mock_handler
|
|
|
|
patcher, model, processor = load_vibevoice_model(
|
|
model_name="TestModel",
|
|
device="cpu",
|
|
dtype="fp32",
|
|
attention_mode="sdpa",
|
|
)
|
|
|
|
mock_handler_cls.assert_called_once()
|
|
mock_patcher_cls.assert_called_once()
|
|
mock_load_gpu.assert_called_once_with(mock_patcher)
|
|
|
|
VIBEVOICE_PATCHER_CACHE.clear()
|
|
LOADED_MODELS_CACHE.clear()
|
|
|
|
def test_force_offload(self):
|
|
"""Test that force_offload calls unpatch_model.
|
|
|
|
Plan 2026-08-18 D5: the cold (user-requested) force-offload path is
|
|
destructive, so it must pass destroy=True.
|
|
"""
|
|
mock_patcher = MagicMock()
|
|
mock_patcher.is_loaded = True
|
|
|
|
with _patch_model_management():
|
|
force_offload_model(mock_patcher, "TestModel")
|
|
|
|
mock_patcher.unpatch_model.assert_called_once_with(
|
|
unpatch_weights=True, destroy=True
|
|
)
|
|
|
|
def test_multi_speaker(self):
|
|
"""Test multi-speaker generation with reference audio."""
|
|
mock_model = MagicMock()
|
|
mock_model.device = torch.device("cpu")
|
|
mock_output = MagicMock()
|
|
mock_output.speech_outputs = [torch.randn(48000)]
|
|
mock_model.generate.return_value = mock_output
|
|
|
|
mock_processor = MagicMock()
|
|
mock_processor.return_value = {"input_ids": torch.randint(0, 100, (1, 10))}
|
|
mock_processor.tokenizer = MagicMock()
|
|
|
|
with patch("ComfyUI_VibeVoice.modules.generation.ProgressBarWithConsole"), \
|
|
_patch_model_management(), \
|
|
patch("ComfyUI_VibeVoice.modules.generation.preprocess_comfy_audio", return_value=_mock_voice_sample()):
|
|
waveform, sr = generate_audio(
|
|
model=mock_model,
|
|
processor=mock_processor,
|
|
text="[1] Hello\n[2] Hi there",
|
|
voice_samples=[_mock_audio_dict(), _mock_audio_dict()],
|
|
speaker_ids=[1, 2],
|
|
)
|
|
|
|
assert waveform is not None
|
|
assert sr == 24000
|
|
|
|
def test_zero_shot(self):
|
|
"""Test zero-shot TTS (no reference audio)."""
|
|
mock_model = MagicMock()
|
|
mock_model.device = torch.device("cpu")
|
|
mock_output = MagicMock()
|
|
mock_output.speech_outputs = [torch.randn(24000)]
|
|
mock_model.generate.return_value = mock_output
|
|
|
|
mock_processor = MagicMock()
|
|
mock_processor.return_value = {"input_ids": torch.randint(0, 100, (1, 10))}
|
|
mock_processor.tokenizer = MagicMock()
|
|
|
|
with patch("ComfyUI_VibeVoice.modules.generation.ProgressBarWithConsole"), \
|
|
_patch_model_management(), \
|
|
patch("ComfyUI_VibeVoice.modules.generation.preprocess_comfy_audio", return_value=_mock_voice_sample()):
|
|
waveform, sr = generate_audio(
|
|
model=mock_model,
|
|
processor=mock_processor,
|
|
text="[1] This is zero-shot TTS",
|
|
voice_samples=[_mock_audio_dict()],
|
|
speaker_ids=[1],
|
|
)
|
|
|
|
assert waveform is not None
|
|
assert sr == 24000
|
|
|
|
def test_interrupt_handling(self):
|
|
"""Test that interrupt during generation raises InterruptProcessingException."""
|
|
import comfy.model_management as mm
|
|
InterruptException = mm.InterruptProcessingException
|
|
|
|
mock_model = MagicMock()
|
|
mock_model.device = torch.device("cpu")
|
|
mock_model.generate.side_effect = InterruptException()
|
|
|
|
mock_processor = MagicMock()
|
|
mock_processor.return_value = {"input_ids": torch.randint(0, 100, (1, 10))}
|
|
mock_processor.tokenizer = MagicMock()
|
|
|
|
# Patch ProgressBar but keep model_management real for the exception class
|
|
with patch("ComfyUI_VibeVoice.modules.generation.ProgressBarWithConsole"), \
|
|
patch("ComfyUI_VibeVoice.modules.generation.model_management.interrupt_current_processing", return_value=False), \
|
|
patch("ComfyUI_VibeVoice.modules.generation.preprocess_comfy_audio", return_value=_mock_voice_sample()):
|
|
with pytest.raises(InterruptException):
|
|
generate_audio(
|
|
model=mock_model,
|
|
processor=mock_processor,
|
|
text="[1] Test",
|
|
voice_samples=[_mock_audio_dict()],
|
|
speaker_ids=[1],
|
|
)
|
|
|
|
|
|
class TestNodeProgressIntegration:
|
|
"""Phase 6 (2026-08-15 progress plan): each node's execute path must drive
|
|
the standard ComfyUI ProgressBar with increasing values. The mocked model
|
|
invokes the progress hook 3x, simulating a 3-step loop."""
|
|
|
|
def test_tts_node_reports_increasing_progress(self):
|
|
from ComfyUI_VibeVoice.nodes.tts_node import VibeVoiceTTSNode
|
|
|
|
mock_model = MagicMock()
|
|
mock_model.device = torch.device("cpu")
|
|
mock_output = MagicMock()
|
|
mock_output.speech_outputs = [torch.randn(24000)]
|
|
|
|
def _generate(**kwargs):
|
|
cb = kwargs.get("progress_callback")
|
|
if cb is not None:
|
|
for i in (1, 2, 3):
|
|
cb(i, 10)
|
|
return mock_output
|
|
|
|
mock_model.generate.side_effect = _generate
|
|
mock_processor = MagicMock()
|
|
mock_processor.return_value = {"input_ids": torch.randint(0, 100, (1, 10))}
|
|
mock_processor.tokenizer = MagicMock()
|
|
mock_patcher = MagicMock()
|
|
|
|
with patch("ComfyUI_VibeVoice.nodes.tts_node.load_vibevoice_model",
|
|
return_value=(mock_patcher, mock_model, mock_processor)), \
|
|
patch("ComfyUI_VibeVoice.nodes.tts_node.ui.PreviewAudio", MagicMock()), \
|
|
patch("ComfyUI_VibeVoice.modules.generation.ProgressBarWithConsole") as mock_pbar_cls, \
|
|
patch("ComfyUI_VibeVoice.modules.generation.model_management.throw_exception_if_processing_interrupted"), \
|
|
patch("ComfyUI_VibeVoice.modules.generation.preprocess_comfy_audio",
|
|
return_value=_mock_voice_sample()):
|
|
mock_pbar = MagicMock()
|
|
mock_pbar.total = 10
|
|
mock_pbar_cls.return_value = mock_pbar
|
|
|
|
VibeVoiceTTSNode.execute(
|
|
model_name="TestModel",
|
|
text="[1] Hello world",
|
|
quantize_llm_4bit=False,
|
|
attention_mode="sdpa",
|
|
cfg_scale=1.3,
|
|
inference_steps=10,
|
|
seed=42,
|
|
do_sample=True,
|
|
temperature=0.95,
|
|
top_p=0.95,
|
|
top_k=0,
|
|
force_offload=False,
|
|
device="cpu",
|
|
dtype="fp32",
|
|
max_new_tokens=0,
|
|
speaker_1_voice=_mock_audio_dict(),
|
|
)
|
|
|
|
values = [c.args[0] for c in mock_pbar.update_absolute.call_args_list if c.args]
|
|
assert 1 in values and 2 in values and 3 in values
|
|
assert values == sorted(values), f"progress must be monotonic, got {values}"
|
|
|
|
def test_canonical_node_routes_realtime_to_realtime_adapter(self):
|
|
from ComfyUI_VibeVoice.nodes.tts_node import VibeVoiceTTSNode
|
|
|
|
realtime_model = type("VibeVoiceStreamingForConditionalGenerationInference", (), {})()
|
|
realtime_processor = type("VibeVoiceStreamingProcessor", (), {})()
|
|
with patch(
|
|
"ComfyUI_VibeVoice.nodes.tts_node.load_vibevoice_model",
|
|
return_value=(MagicMock(), realtime_model, realtime_processor),
|
|
), patch(
|
|
"ComfyUI_VibeVoice.nodes.tts_node.get_cached_voice_preset",
|
|
return_value={"lm": MagicMock()},
|
|
), patch(
|
|
"ComfyUI_VibeVoice.nodes.tts_node.generate_realtime_audio",
|
|
return_value=(torch.ones(1, 1, 24), 24000),
|
|
) as generate, patch(
|
|
"ComfyUI_VibeVoice.nodes.tts_node.generate_audio"
|
|
) as generate_standard, patch(
|
|
"ComfyUI_VibeVoice.nodes.tts_node.ui.PreviewAudio", MagicMock()
|
|
):
|
|
VibeVoiceTTSNode.execute(
|
|
model_name="VibeVoice-Realtime-0.5B",
|
|
text="[1] Hello world",
|
|
quantize_llm_4bit=False,
|
|
attention_mode="sdpa",
|
|
cfg_scale=1.3,
|
|
inference_steps=10,
|
|
seed=42,
|
|
do_sample=True,
|
|
temperature=0.95,
|
|
top_p=0.95,
|
|
top_k=0,
|
|
force_offload=False,
|
|
device="cpu",
|
|
dtype="fp32",
|
|
voice_preset="en-Carter_man",
|
|
)
|
|
generate.assert_called_once()
|
|
generate_standard.assert_not_called()
|
|
|
|
def test_realtime_progress_callbacks_reach_comfyui_progress_bar(self):
|
|
"""The realtime adapter drives the ComfyUI progress bar and closes it."""
|
|
from ComfyUI_VibeVoice.modules.realtime_generation import generate_realtime_audio
|
|
from ComfyUI_VibeVoice.modules.voice_presets import PRESET_CACHE_KEYS
|
|
|
|
class VibeVoiceStreamingForConditionalGenerationInference:
|
|
def __init__(self):
|
|
self.steps = None
|
|
|
|
def set_ddpm_inference_steps(self, num_steps):
|
|
self.steps = num_steps
|
|
|
|
def generate(self, **kwargs):
|
|
callback = kwargs.get("progress_callback")
|
|
for index in (1, 2, 3):
|
|
callback(index, 10)
|
|
output = MagicMock()
|
|
output.speech_outputs = [torch.ones(1, 1, 24)]
|
|
return output
|
|
|
|
class VibeVoiceStreamingProcessor:
|
|
tokenizer = MagicMock()
|
|
|
|
def process_input_with_cached_prompt(self, **kwargs):
|
|
# The realtime adapter sizes its auto length budget from the
|
|
# script tokens and the TTS-LM prompt length, so both must be
|
|
# present alongside the generic input_ids.
|
|
return {
|
|
"input_ids": torch.zeros(1, 2, dtype=torch.long),
|
|
"tts_text_ids": torch.ones(1, 4, dtype=torch.long),
|
|
"tts_lm_input_ids": torch.ones(1, 6, dtype=torch.long),
|
|
}
|
|
|
|
preset = {
|
|
key: MagicMock(last_hidden_state=torch.zeros(1, 2, 8))
|
|
for key in PRESET_CACHE_KEYS
|
|
}
|
|
with patch(
|
|
"ComfyUI_VibeVoice.modules.realtime_generation.ProgressBarWithConsole"
|
|
) as mock_pbar_cls, patch(
|
|
"ComfyUI_VibeVoice.modules.realtime_generation.model_management.get_torch_device",
|
|
return_value=torch.device("cpu"),
|
|
), patch(
|
|
"ComfyUI_VibeVoice.modules.realtime_generation.model_management.throw_exception_if_processing_interrupted"
|
|
):
|
|
mock_pbar = MagicMock()
|
|
mock_pbar.total = 10
|
|
mock_pbar_cls.return_value = mock_pbar
|
|
|
|
waveform, sample_rate = generate_realtime_audio(
|
|
model=VibeVoiceStreamingForConditionalGenerationInference(),
|
|
processor=VibeVoiceStreamingProcessor(),
|
|
text="[1] Hello world",
|
|
voice_preset=preset,
|
|
diffusion_steps=10,
|
|
)
|
|
|
|
assert sample_rate == 24000
|
|
assert waveform.shape == (1, 1, 24)
|
|
mock_pbar_cls.assert_called_once_with(1)
|
|
values = [c.args[0] for c in mock_pbar.update_absolute.call_args_list if c.args]
|
|
assert 1 in values and 2 in values and 3 in values
|
|
assert values == sorted(values)
|
|
mock_pbar.close.assert_called_once()
|
|
|
|
def test_realtime_cancellation_reaches_comfyui_interrupt_handling(self):
|
|
import comfy.model_management as model_management
|
|
|
|
from ComfyUI_VibeVoice.nodes.tts_node import VibeVoiceTTSNode
|
|
|
|
realtime_model = type("VibeVoiceStreamingForConditionalGenerationInference", (), {})()
|
|
realtime_processor = type("VibeVoiceStreamingProcessor", (), {})()
|
|
with patch(
|
|
"ComfyUI_VibeVoice.nodes.tts_node.load_vibevoice_model",
|
|
return_value=(MagicMock(), realtime_model, realtime_processor),
|
|
), patch(
|
|
"ComfyUI_VibeVoice.nodes.tts_node.get_cached_voice_preset",
|
|
return_value={"lm": MagicMock()},
|
|
), patch(
|
|
"ComfyUI_VibeVoice.nodes.tts_node.generate_realtime_audio",
|
|
side_effect=model_management.InterruptProcessingException(),
|
|
):
|
|
result = VibeVoiceTTSNode.execute(
|
|
model_name="VibeVoice-Realtime-0.5B",
|
|
text="[1] Hello world",
|
|
quantize_llm_4bit=False,
|
|
attention_mode="sdpa",
|
|
cfg_scale=1.3,
|
|
inference_steps=10,
|
|
seed=42,
|
|
do_sample=True,
|
|
temperature=0.95,
|
|
top_p=0.95,
|
|
top_k=0,
|
|
force_offload=False,
|
|
device="cpu",
|
|
dtype="fp32",
|
|
voice_preset="en-Carter_man",
|
|
)
|
|
|
|
assert result[0]["waveform"].abs().sum().item() == 0.0
|
|
assert result[0]["sample_rate"] == 24000
|
|
|
|
def test_asr_node_reports_increasing_progress(self):
|
|
from ComfyUI_VibeVoice.nodes.asr_node import VibeVoiceASRNode
|
|
|
|
mock_model = MagicMock()
|
|
mock_param = MagicMock()
|
|
mock_param.device = torch.device("cpu")
|
|
mock_model.parameters.return_value = iter([mock_param])
|
|
|
|
def _generate(**kwargs):
|
|
streamer = kwargs.get("streamer")
|
|
if streamer is not None:
|
|
for tok in (10, 11, 12):
|
|
streamer.put(torch.tensor([tok]))
|
|
streamer.end()
|
|
return torch.tensor([[1, 2, 3, 10, 11, 12, 0]])
|
|
|
|
mock_model.generate.side_effect = _generate
|
|
mock_processor = MagicMock()
|
|
mock_processor.return_value = {"input_ids": torch.tensor([[1, 2, 3]])}
|
|
mock_processor.pad_id = 0
|
|
mock_processor.tokenizer.eos_token_id = 0
|
|
mock_processor.decode.return_value = "Hello world"
|
|
mock_processor.post_process_transcription.return_value = []
|
|
mock_patcher = MagicMock()
|
|
|
|
with patch("ComfyUI_VibeVoice.nodes.asr_node.load_asr_model_patched",
|
|
return_value=(mock_patcher, mock_model, mock_processor)), \
|
|
patch("ComfyUI_VibeVoice.modules.asr_generation.ProgressBarWithConsole") as mock_pbar_cls, \
|
|
patch("ComfyUI_VibeVoice.modules.asr_generation.model_management.throw_exception_if_processing_interrupted"):
|
|
mock_pbar = MagicMock()
|
|
mock_pbar.total = 32768
|
|
mock_pbar_cls.return_value = mock_pbar
|
|
|
|
VibeVoiceASRNode.execute(
|
|
model_name="VibeVoice-ASR",
|
|
audio=_mock_audio_dict(),
|
|
context_info="",
|
|
max_new_tokens=32768,
|
|
temperature=0.0,
|
|
top_p=1.0,
|
|
do_sample=False,
|
|
num_beams=1,
|
|
device="cpu",
|
|
dtype="fp32",
|
|
attention_mode="sdpa",
|
|
force_offload=False,
|
|
)
|
|
|
|
values = [c.args[0] for c in mock_pbar.update_absolute.call_args_list if c.args]
|
|
assert 1 in values and 2 in values and 3 in values
|
|
assert values == sorted(values), f"progress must be monotonic, got {values}"
|
|
|
|
|
|
class TestExternalModelFullFlow:
|
|
"""Phase 7.2: end-to-end mock test from weight file → bundle → patcher → generate."""
|
|
|
|
def test_integration_external_model_full_flow(self, tmp_path):
|
|
"""External model: load_external_vibevoice_model → load_vibevoice_from_external → generate_audio."""
|
|
from ComfyUI_VibeVoice.modules.external_loader import load_external_vibevoice_model
|
|
from ComfyUI_VibeVoice.modules.generation import load_vibevoice_from_external, generate_audio
|
|
|
|
VIBEVOICE_PATCHER_CACHE.clear()
|
|
LOADED_MODELS_CACHE.clear()
|
|
|
|
# A REAL safetensors file, not a placeholder: the loader calls
|
|
# warn_if_lowbit_quantization(), which parses the header to decide
|
|
# whether the checkpoint is a naive int8 cast. b"dummy" made that
|
|
# raise SafetensorError before any of the flow under test ran.
|
|
from safetensors.torch import save_file
|
|
|
|
weight_file = tmp_path / "weight.safetensors"
|
|
save_file({"model.language_model.weight": torch.zeros(2, 2)},
|
|
str(weight_file))
|
|
|
|
# --- Stage 1: load_external_vibevoice_model (mocked internals) ---
|
|
fake_state_dict = {"model.language_model.weight": torch.zeros(2, 2)}
|
|
fake_config = MagicMock(spec=[])
|
|
fake_tokenizer = MagicMock()
|
|
fake_processor = MagicMock()
|
|
fake_model = MagicMock()
|
|
fake_model.load_state_dict.return_value = ([], [])
|
|
fake_model.to.return_value = fake_model
|
|
|
|
from ComfyUI_VibeVoice.modules import external_loader
|
|
|
|
with patch.object(external_loader.comfy.utils, "load_torch_file", return_value=fake_state_dict), \
|
|
patch.object(external_loader.VibeVoiceLoader, "_load_config", return_value=fake_config), \
|
|
patch.object(external_loader.VibeVoiceLoader, "_load_tokenizer", return_value=fake_tokenizer), \
|
|
patch.object(external_loader.VibeVoiceLoader, "_load_processor", return_value=fake_processor), \
|
|
patch.object(external_loader.VibeVoiceLoader, "_instantiate_model", return_value=fake_model), \
|
|
patch.object(external_loader, "resolve_sidecar_config", return_value="/fake/config.json"), \
|
|
patch.object(external_loader, "resolve_sidecar_preprocessor", return_value=""), \
|
|
patch.object(external_loader, "resolve_sidecar_tokenizer_dir", return_value="/fake/dir"), \
|
|
patch.object(external_loader, "resolve_dtype", return_value=torch.float32), \
|
|
patch.object(external_loader, "resolve_attention_mode", side_effect=lambda m, q: m), \
|
|
patch.object(external_loader, "get_attn_implementation_for_load", return_value="sdpa"), \
|
|
patch.object(external_loader, "VibeVoiceStreamingConfig", type("_FakeStreamingCfg", (), {})):
|
|
bundle = load_external_vibevoice_model(str(weight_file), "VibeVoice-1.5B")
|
|
|
|
# Bundle must have the correct structure.
|
|
assert bundle["model"] is fake_model
|
|
assert bundle["processor"] is fake_processor
|
|
assert bundle["model_name"] == "VibeVoice-1.5B"
|
|
assert bundle["is_streaming"] is False
|
|
assert bundle["is_asr"] is False
|
|
|
|
# --- Stage 2: load_vibevoice_from_external (wrap in patcher) ---
|
|
mock_patcher = MagicMock()
|
|
mock_patcher.model.model = fake_model
|
|
mock_patcher.model.processor = fake_processor
|
|
|
|
with patch("ComfyUI_VibeVoice.modules.generation.ExternalVibeVoiceModelHandler") as mock_handler_cls, \
|
|
patch("ComfyUI_VibeVoice.modules.generation.VibeVoicePatcher", return_value=mock_patcher), \
|
|
patch("ComfyUI_VibeVoice.modules.generation.model_management.load_model_gpu"):
|
|
mock_handler = MagicMock()
|
|
mock_handler.size = 1000
|
|
mock_handler_cls.return_value = mock_handler
|
|
|
|
patcher, model, processor = load_vibevoice_from_external(
|
|
bundle, device="cpu", dtype="fp32", attention_mode="sdpa"
|
|
)
|
|
|
|
assert model is fake_model
|
|
assert processor is fake_processor
|
|
|
|
# --- Stage 3: generate_audio (mocked) ---
|
|
fake_model.device = torch.device("cpu")
|
|
mock_output = MagicMock()
|
|
mock_output.speech_outputs = [torch.randn(24000)]
|
|
fake_model.generate.return_value = mock_output
|
|
fake_processor.return_value = {"input_ids": torch.randint(0, 100, (1, 10))}
|
|
fake_processor.tokenizer = MagicMock()
|
|
|
|
with patch("ComfyUI_VibeVoice.modules.generation.ProgressBarWithConsole"), \
|
|
patch("ComfyUI_VibeVoice.modules.generation.preprocess_comfy_audio",
|
|
return_value=_mock_voice_sample()):
|
|
waveform, sr = generate_audio(
|
|
model=model,
|
|
processor=processor,
|
|
text="[1] Hello world",
|
|
voice_samples=[_mock_audio_dict()],
|
|
speaker_ids=[1],
|
|
)
|
|
|
|
assert waveform is not None
|
|
assert sr == 24000
|
|
assert waveform.ndim == 3
|
|
|
|
VIBEVOICE_PATCHER_CACHE.clear()
|
|
LOADED_MODELS_CACHE.clear()
|