From dc8ffdffd59101f07d139bed3bcffd6435b6c004 Mon Sep 17 00:00:00 2001 From: Ben Younes <2910651+ousamabenyounes@users.noreply.github.com> Date: Sun, 6 Sep 2026 19:11:53 +0000 Subject: [PATCH] fix(app): warm up model at startup to avoid first-request connection errors --- app.py | 6 +++ tests/test_app_warmup.py | 85 ++++++++++++++++++++++++++++++++++++++++ 2 files changed, 91 insertions(+) create mode 100644 tests/test_app_warmup.py diff --git a/app.py b/app.py index 15448281..d67e5d49 100644 --- a/app.py +++ b/app.py @@ -563,6 +563,12 @@ def run_demo( ): demo = VoxCPMDemo(model_id=model_id, device=device) interface = create_demo_interface(demo) + # Warm up the model at startup instead of lazily on the first request. + # A multi-GB from_pretrained running inside the first click blocks that + # request for minutes and surfaces any load/download failure as an opaque + # per-request connection error; loading here fails fast and keeps the first + # generation responsive. + demo.get_or_load_voxcpm() interface.queue(max_size=10, default_concurrency_limit=1).launch( server_name=server_name, server_port=server_port, diff --git a/tests/test_app_warmup.py b/tests/test_app_warmup.py new file mode 100644 index 00000000..215d7227 --- /dev/null +++ b/tests/test_app_warmup.py @@ -0,0 +1,85 @@ +"""Regression test for issue #367: the web demo must warm up the model at +server startup, not lazily on the first request. + +Loading the multi-GB model inside the first click blocks that request for +minutes and surfaces any load/download failure as an opaque connection error. +``run_demo`` must therefore call ``get_or_load_voxcpm()`` *before* the Gradio +server is launched. This test records the call order and fails if the warm-up +is removed. + +Heavy deps (gradio/funasr/voxcpm/torch) are not importable in the test env, so +they are stubbed before importing ``app`` — same approach as test_cli.py. +""" +from __future__ import annotations + +import importlib.util +import sys +import types +from pathlib import Path +from unittest import mock + +ROOT = Path(__file__).resolve().parents[1] +APP_PATH = ROOT / "app.py" + + +def _install_stub_deps() -> None: + # --- fake gradio (only the symbols app.py touches at import time) --- + gr = types.ModuleType("gradio") + + def _passthrough(*args, **kwargs): + return mock.MagicMock() + + gr.I18n = _passthrough + themes = types.SimpleNamespace(Soft=_passthrough, GoogleFont=_passthrough) + gr.themes = themes + sys.modules["gradio"] = gr + + # --- fake funasr --- + funasr = types.ModuleType("funasr") + funasr.AutoModel = mock.MagicMock() + sys.modules["funasr"] = funasr + + # --- fake voxcpm + voxcpm.model.utils --- + voxcpm = types.ModuleType("voxcpm") + voxcpm.VoxCPM = mock.MagicMock() + model_pkg = types.ModuleType("voxcpm.model") + utils = types.ModuleType("voxcpm.model.utils") + utils.resolve_runtime_device = lambda device, fallback: "cpu" + model_pkg.utils = utils + voxcpm.model = model_pkg + sys.modules["voxcpm"] = voxcpm + sys.modules["voxcpm.model"] = model_pkg + sys.modules["voxcpm.model.utils"] = utils + + +def _load_app_module(): + _install_stub_deps() + spec = importlib.util.spec_from_file_location("voxcpm_app_under_test", APP_PATH) + module = importlib.util.module_from_spec(spec) + spec.loader.exec_module(module) + return module + + +def test_model_is_warmed_up_before_launch(): + app = _load_app_module() + + calls: list[str] = [] + + class _FakeInterface: + def queue(self, *args, **kwargs): + return self + + def launch(self, *args, **kwargs): + calls.append("launch") + + with mock.patch.object(app, "create_demo_interface", return_value=_FakeInterface()), \ + mock.patch.object(app.VoxCPMDemo, "get_or_load_voxcpm", + autospec=True, + side_effect=lambda self: calls.append("warmup")): + app.run_demo(device="cpu") + + print(f"call order: {calls}") + assert "warmup" in calls, "model was never warmed up (loads lazily on first request)" + assert calls.index("warmup") < calls.index("launch"), ( + f"model must load before launch, got order: {calls}" + )