fix: restore generation and startup broken by the rename (#1417) (#1420)

Two bugs from the v0.4.2 rename sweep:

- The lazy model import was rewritten to `from omnivoice.models.omnivoice import VoiceStudio`, a class the library does not export. ImportError is not ModuleNotFoundError, so the #564 source fallback never caught it and /generate 500'd on every default-engine request. The class keeps its library name — it is a checkpoint-referenced identifier, not branding.
- alembic resolved a bare relative script_location against the process cwd, and the desktop shell launches the backend from frontend/src-tauri, so a pending migration killed startup. script_location and prepend_sys_path are now anchored with %(here)s, with path_separator = os so Windows drive letters and paths with spaces survive. alembic floor raised to >=1.16.

Regression tests verified fail-before/pass-after for both.
This commit is contained in:
Palash Debnath
2026-08-08 02:40:52 +05:30
committed by GitHub
parent ea2d7155bc
commit 46c47b68ce
10 changed files with 99 additions and 14 deletions
+2
View File
@@ -40,6 +40,8 @@ The bundled TTS model package (`pyproject.toml`) is versioned independently.
### Fixed
- Generating with the default engine works again on everything built from `main` since the rename — source checkouts, preview builds and Docker `:latest` all run the same backend, whose model import had been rewritten to a class name the library doesn't export, failing every generation with "cannot import name 'VoiceStudio'". The class keeps its library name, and a guard test now pins it. (#1420)
- Running from source no longer dies at startup when a database migration is pending. Alembic resolved the migrations folder relative to wherever the app was launched from — fine from the repo root, fatal from the desktop shell (`tauri dev`), which reported "Path doesn't exist: backend/migrations" and stopped. The path is now anchored to the repo, wherever you start it. (#1420)
- The voice-design model on Apple Silicon works again. Its description was being dropped before it reached the engine, so every generation failed with a raw 400 no matter what you typed. (#1405)
- The first-run setup screen no longer times out while it waits for you. Taking more than two minutes to choose an install location, region or mirror made the app declare "Setup failed", and Retry landed back on the same screen with the same clock — so a first install could never be completed. (#1376)
- Transcription on an NVIDIA machine whose cuDNN 8 libraries are missing no longer kills the backend outright. The app checks the library before picking a transcription engine and falls back to PyTorch Whisper, instead of handing off to a component that aborts the process with no error and restarts into the same crash. (#1371)
+11 -3
View File
@@ -1,5 +1,5 @@
# Alembic configuration for VoiceStudio.
# Run from the repo root: alembic -c alembic.ini <command>
# Run from anywhere: alembic -c <repo>/alembic.ini <command>
# Default commands:
# alembic upgrade head — apply all pending migrations
# alembic revision -m "…" — create a new migration
@@ -9,8 +9,16 @@
# See backend/migrations/env.py.
[alembic]
script_location = backend/migrations
prepend_sys_path = backend
# %(here)s = this file's directory. Alembic resolves bare relative paths
# against the process CWD, not the ini — and the app doesn't always start
# from the repo root (`tauri dev` runs the backend with
# cwd=frontend/src-tauri), which made startup migrations die with
# "Path doesn't exist: backend/migrations" the first time one was pending.
script_location = %(here)s/backend/migrations
prepend_sys_path = %(here)s/backend
# Split multi-path options on os.pathsep, not the legacy space/comma/colon
# set — a colon-split would shred "C:\..." absolute paths on Windows.
path_separator = os
# sqlalchemy.url is set programmatically in env.py — do NOT set it here.
sqlalchemy.url =
+5 -2
View File
@@ -29,7 +29,10 @@ def _lazy_omnivoice():
global _OmniVoice
if _OmniVoice is None:
try:
from omnivoice.models.omnivoice import VoiceStudio as _OV
# The class is OmniVoice — a library identifier, not product
# branding. The VoiceStudio rename must not touch it (checkpoint
# configs reference the class name via transformers architectures).
from omnivoice.models.omnivoice import OmniVoice as _OV
except ModuleNotFoundError:
# The venv's editable install is missing/broken (#564). main.py wires
# the source fallback at startup, but resolve it here too so the
@@ -37,7 +40,7 @@ def _lazy_omnivoice():
from core.omnivoice_path import ensure_omnivoice_importable
_backend_dir = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
ensure_omnivoice_importable(_backend_dir, logger)
from omnivoice.models.omnivoice import VoiceStudio as _OV
from omnivoice.models.omnivoice import OmniVoice as _OV
_OmniVoice = _OV
return _OmniVoice
+4 -4
View File
@@ -1,15 +1,15 @@
# Generation Parameters
Parameters can be passed as keyword arguments to `model.generate(...)` or via the `VoiceStudioGenerationConfig` dataclass. See below for the full list and which category each belongs to.
Parameters can be passed as keyword arguments to `model.generate(...)` or via the `OmniVoiceGenerationConfig` dataclass. See below for the full list and which category each belongs to.
```python
# 1) Direct keyword arguments
audio = model.generate(text="Hello world", num_step=32, guidance_scale=2.0)
# 2) Via VoiceStudioGenerationConfig dataclass
from omnivoice import VoiceStudioGenerationConfig
# 2) Via OmniVoiceGenerationConfig dataclass
from omnivoice import OmniVoiceGenerationConfig
config = VoiceStudioGenerationConfig(num_step=32, guidance_scale=2.0)
config = OmniVoiceGenerationConfig(num_step=32, guidance_scale=2.0)
audio = model.generate(text="Hello world", generation_config=config)
```
+2 -2
View File
@@ -7,9 +7,9 @@ generates a matching voice on the fly.
```python
import torch
from omnivoice import VoiceStudio
from omnivoice import OmniVoice
model = VoiceStudio.from_pretrained(
model = OmniVoice.from_pretrained(
"k2-fsa/OmniVoice",
device_map="cuda:0",
dtype=torch.float16
+1 -1
View File
@@ -173,7 +173,7 @@
" \"print(f'torch {torch.__version__}, torchaudio {torchaudio.__version__}, \"\n",
" \"transformers {transformers.__version__}, \"\n",
" \"CUDA available: {torch.cuda.is_available()}'); \"\n",
" \"from omnivoice.models.omnivoice import VoiceStudio; \"\n",
" \"from omnivoice.models.omnivoice import OmniVoice; \"\n",
" \"from transformers import HiggsAudioV2TokenizerModel; \"\n",
" \"print('Install OK - backend model stack imports cleanly')\"],\n",
" cwd=REPO_DIR, what=\"import sanity check\")\n"
+4 -1
View File
@@ -86,7 +86,10 @@ dependencies = [
"parakeet-mlx>=0.5.2 ; sys_platform == 'darwin' and platform_machine == 'arm64'",
"demucs>=4.0.1",
"yt-dlp>=2024.12.13",
"alembic>=1.13",
# >=1.16: alembic.ini relies on path_separator=os (new in 1.16.0), which
# older alembic silently ignores and then colon-splits C:\ paths /
# space-splits POSIX paths containing spaces.
"alembic>=1.16",
# Lightweight English TTS "Turbo" tier — 25-80 MB ONNX model, 8 preset
# voices (Bella, Jasper, Luna, Bruno, Rosie, Hugo, Kiki, Leo), CPU
# realtime on any platform. Complements OmniVoice's 2.4 GB multilingual
+55
View File
@@ -12,6 +12,7 @@
continue on a half-migrated DB.
"""
import sqlite3
from pathlib import Path
import pytest
@@ -76,6 +77,60 @@ def test_pending_migrations_snapshot_first_then_upgrade(tmp_path, monkeypatch):
assert stamped, "upgrade must have stamped the DB at head"
def test_pending_migrations_apply_from_any_cwd(tmp_path, monkeypatch):
"""The dev app launches the backend with cwd=frontend/src-tauri, not the
repo root. alembic resolves a bare relative script_location against the
CWD, so alembic.ini must use %(here)s otherwise the first pending
migration kills startup with "Path doesn't exist: backend/migrations"."""
db = tmp_path / "omnivoice.db"
_seed_user_db(db)
monkeypatch.setattr(db_module, "DB_PATH", str(db))
monkeypatch.chdir(tmp_path) # anywhere but the repo root
_run_alembic_upgrade() # must not raise MigrationError
conn = sqlite3.connect(str(db))
try:
stamped = [r[0] for r in conn.execute("SELECT version_num FROM alembic_version")]
finally:
conn.close()
assert stamped, "upgrade must have stamped the DB at head"
def test_alembic_ini_paths_survive_spaces_and_drive_letters(tmp_path, monkeypatch):
"""The other half of the %(here)s fix: path_separator=os. Without it,
alembic legacy-splits prepend_sys_path on spaces, commas AND colons
shredding "C:\\..." into ["C", "\\..."] on Windows and any POSIX path
containing a space. The cwd-only test above can't see that (core.config
is already imported when it runs), so exercise the ini's own resolution
from a directory with a space in its name and assert exactly one, intact
sys.path entry gets prepended."""
import shutil
import sys
from alembic.config import Config
from alembic.script import ScriptDirectory
repo = Path(__file__).resolve().parents[1]
here = tmp_path / "a b" # the space is the point
(here / "backend").mkdir(parents=True)
shutil.copytree(repo / "backend" / "migrations", here / "backend" / "migrations")
shutil.copy(repo / "alembic.ini", here / "alembic.ini")
monkeypatch.chdir(tmp_path)
before = list(sys.path)
try:
script = ScriptDirectory.from_config(Config(str(here / "alembic.ini")))
added = [p for p in sys.path if p not in before]
finally:
sys.path[:] = before
assert Path(script.dir).resolve() == (here / "backend" / "migrations").resolve()
assert len(added) == 1 and Path(added[0]).resolve() == (here / "backend").resolve(), (
f"prepend_sys_path was shredded by legacy splitting: {added}"
)
def test_up_to_date_db_is_not_resnapshotted(tmp_path, monkeypatch):
"""Once at head, later launches must not churn new backups."""
db = tmp_path / "omnivoice.db"
@@ -174,6 +174,20 @@ def test_the_python_package_name_is_unchanged():
assert re.search(r'(?m)^name\s*=\s*"omnivoice"', pyproject), WHY
def test_the_model_class_name_is_unchanged():
# `OmniVoice` (omnivoice.models.omnivoice) is a transformers PreTrainedModel
# — a library identifier baked into checkpoint configs, not product
# branding. The 0.4.2 rename sweep rewrote the backend's import to a
# nonexistent `VoiceStudio` class, which killed every default-engine
# generation with "cannot import name 'VoiceStudio'". Behavioral, not a
# source-text grep: the backend's lazy loader must resolve to the very
# class the library exports, however either side spells the import.
import omnivoice
from services.model_manager import _lazy_omnivoice
assert _lazy_omnivoice() is omnivoice.OmniVoice, WHY
@pytest.mark.parametrize("env_var", ["OMNIVOICE_DATA_DIR", "OMNIVOICE_CACHE_DIR"])
def test_the_public_env_var_prefix_is_unchanged(env_var):
# ~150 OMNIVOICE_* vars are a public configuration contract: every user's
Generated
+1 -1
View File
@@ -3320,7 +3320,7 @@ dev = [
[package.metadata]
requires-dist = [
{ name = "accelerate" },
{ name = "alembic", specifier = ">=1.13" },
{ name = "alembic", specifier = ">=1.16" },
{ name = "argostranslate", specifier = ">=1.9.0" },
{ name = "audioseal", specifier = ">=0.1.3" },
{ name = "cryptography", specifier = ">=41" },