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:
@@ -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
@@ -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 =
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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)
|
||||
```
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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" },
|
||||
|
||||
Reference in New Issue
Block a user