Files
wildminder-ComfyUI-VibeVoice/tests/test_integration.py
T
WildAi 4d8686f9ad test: clean the suite to match the current architecture, and de-localise it
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.
2026-10-01 15:49:15 +03:00

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()