fix(model): route every OMNIVOICE_MODEL read through the resolver; tighten WinError 193 match (#693, #705) (#707)
Follow-up from independent verification of #693/#705. #693 (whole-class): the resolver only guarded the model-load site. A leaked engine id in OMNIVOICE_MODEL still hit four other raw reads — most importantly preload_model()'s model_info() probe, which failed on the bad value and SILENTLY disabled warm-up (first /generate then ate the full load). Plus the Settings 'model_checkpoint' display, the loaded-models list, and the engine_id baked into exported persona bundles. Route all of them through resolve_omnivoice_checkpoint() (personas keeps its '' unset marker, sanitizing only a set value). Add a source-level recurrence guard so a future raw read can't reintroduce the class. #705: tighten 'winerror 193' -> '[winerror 193]' so the substring can't also match WinError 1930-1939 (the portable 'is not a valid win32 application' clause still covers non-Windows formatting). 48 tests pass (resolver + guard + audio-guard + route inventory); edited routers/services import clean. Co-authored-by: mergetest <test@local> Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
co-authored by
mergetest
Claude Opus 4.8
parent
883a06e9c0
commit
aa17e3319f
+4
-1
@@ -137,7 +137,10 @@ of error messages across dub, generate, and design.
|
||||
used to fail every launch with *"omnivoice is not a local folder and is not a
|
||||
valid model identifier."* It now self-heals — only a real HF repo id
|
||||
(`org/repo`) or an explicit local path is honored; anything else falls back to
|
||||
the default with a logged warning. (#693)
|
||||
the default with a logged warning. Every consumer of the setting routes through
|
||||
the same resolver, so a bad value also can't silently disable model warm-up,
|
||||
mislabel the Settings checkpoint, or get baked into an exported persona bundle.
|
||||
(#693)
|
||||
- **ASR no longer crashes the dub/transcribe preflight when CTranslate2's native
|
||||
library can't load.** On hardened kernels / newer glibc (e.g. WSL2) the
|
||||
CTranslate2 `.so` is rejected with *"cannot enable executable stack"* — an
|
||||
|
||||
@@ -196,7 +196,7 @@ def _oom_friendly_reraise(e):
|
||||
# — torch, ffmpeg, or a bundled engine binary) fails to load/spawn on Windows
|
||||
# with "[WinError 193] %1 is not a valid Win32 application". That is NOT OOM,
|
||||
# and Flush won't help — reinstalling/repairing the component is the real fix.
|
||||
if "winerror 193" in _low or "is not a valid win32 application" in _low:
|
||||
if "[winerror 193]" in _low or "is not a valid win32 application" in _low:
|
||||
raise RuntimeError(
|
||||
f"A native component (a DLL / .pyd / .exe — e.g. torch, ffmpeg, or an "
|
||||
f"engine binary) is corrupt or built for the wrong architecture "
|
||||
|
||||
@@ -60,6 +60,11 @@ async def export_persona(
|
||||
profile = dict(row)
|
||||
|
||||
tag_list = [t.strip() for t in tags.split(",") if t.strip()]
|
||||
# #693: if OMNIVOICE_MODEL is set, record the *resolved* checkpoint in the
|
||||
# exported bundle so a leaked engine id (e.g. "omnivoice") can't be baked in;
|
||||
# keep "" when unset (the bundle's "engine unspecified" marker).
|
||||
from services.model_manager import resolve_omnivoice_checkpoint
|
||||
engine_id = resolve_omnivoice_checkpoint() if os.environ.get("OMNIVOICE_MODEL", "").strip() else ""
|
||||
try:
|
||||
loop = asyncio.get_running_loop()
|
||||
content = await loop.run_in_executor(
|
||||
@@ -70,7 +75,7 @@ async def export_persona(
|
||||
license_spdx=license_spdx,
|
||||
tags=tag_list,
|
||||
include_reference=include_reference,
|
||||
engine_id=os.environ.get("OMNIVOICE_MODEL", ""),
|
||||
engine_id=engine_id,
|
||||
omnivoice_version=APP_VERSION,
|
||||
),
|
||||
)
|
||||
|
||||
@@ -18,7 +18,7 @@ import shutil
|
||||
|
||||
from core.config import OUTPUTS_DIR, DATA_DIR, CRASH_LOG_PATH, LOG_PATH, IDLE_TIMEOUT_SECONDS
|
||||
from core.version import APP_VERSION
|
||||
from services.model_manager import get_model_status, get_best_device
|
||||
from services.model_manager import get_model_status, get_best_device, resolve_omnivoice_checkpoint
|
||||
from services.ffmpeg_utils import find_ffmpeg, run_ffmpeg
|
||||
|
||||
# Router-level loopback gate. Every route mounted on `router` (GET + POST,
|
||||
@@ -208,7 +208,7 @@ def system_info():
|
||||
"outputs_dir": OUTPUTS_DIR,
|
||||
"crash_log_path": CRASH_LOG_PATH,
|
||||
"idle_timeout_seconds": IDLE_TIMEOUT_SECONDS,
|
||||
"model_checkpoint": os.environ.get("OMNIVOICE_MODEL", "k2-fsa/OmniVoice"),
|
||||
"model_checkpoint": resolve_omnivoice_checkpoint(), # #693: show the effective checkpoint, not a leaked raw value
|
||||
"asr_model": os.environ.get("ASR_MODEL", "Systran/faster-whisper-large-v3"),
|
||||
"translate_provider": os.environ.get("TRANSLATE_PROVIDER", "google"),
|
||||
"has_hf_token": _has_hf_token(),
|
||||
|
||||
@@ -60,7 +60,7 @@ def list_loaded() -> dict:
|
||||
models.append({
|
||||
"id": "tts",
|
||||
"name": "OmniVoice TTS",
|
||||
"checkpoint": os.environ.get("OMNIVOICE_MODEL", "k2-fsa/OmniVoice"),
|
||||
"checkpoint": mm.resolve_omnivoice_checkpoint(), # #693: effective checkpoint, not a leaked raw value
|
||||
"device": device,
|
||||
"vram_mb": round(_tts_vram_mb(), 1),
|
||||
"unloadable": True,
|
||||
|
||||
@@ -846,8 +846,11 @@ async def preload_model():
|
||||
return # already loaded
|
||||
try:
|
||||
# Check if the required model checkpoint exists before attempting
|
||||
# a heavy load that would fail and pollute startup logs.
|
||||
checkpoint = os.environ.get("OMNIVOICE_MODEL", "k2-fsa/OmniVoice")
|
||||
# a heavy load that would fail and pollute startup logs. Use the same
|
||||
# resolver as the load path (#693) so a leaked engine id in
|
||||
# OMNIVOICE_MODEL can't make this model_info() probe fail and silently
|
||||
# disable warm-up (then the first /generate eats the full load).
|
||||
checkpoint = resolve_omnivoice_checkpoint()
|
||||
try:
|
||||
from huggingface_hub import model_info
|
||||
model_info(checkpoint, timeout=5)
|
||||
|
||||
@@ -48,3 +48,30 @@ def test_existing_local_dir_is_kept(monkeypatch, tmp_path):
|
||||
def test_blank_or_whitespace_falls_back(monkeypatch):
|
||||
_set(monkeypatch, " ")
|
||||
assert resolve_omnivoice_checkpoint() == _DEFAULT_OMNIVOICE_CHECKPOINT
|
||||
|
||||
|
||||
def test_omnivoice_model_is_only_read_through_the_resolver():
|
||||
"""#693 recurrence guard: the raw OMNIVOICE_MODEL env var may only be read
|
||||
inside resolve_omnivoice_checkpoint() (the resolver) and the personas export
|
||||
guard (which routes the value through the resolver). Any other raw read
|
||||
reintroduces the whole bug class — a bare engine id reaching a HF call, or a
|
||||
leaked value mislabeling a UI field / exported persona bundle."""
|
||||
import pathlib
|
||||
|
||||
backend = pathlib.Path(__file__).resolve().parents[1] / "backend"
|
||||
allowed = {"services/model_manager.py", "api/routers/personas.py"}
|
||||
offenders = []
|
||||
for py in backend.rglob("*.py"):
|
||||
if "__pycache__" in py.parts:
|
||||
continue
|
||||
rel = py.relative_to(backend).as_posix()
|
||||
if rel in allowed:
|
||||
continue
|
||||
for i, line in enumerate(py.read_text(encoding="utf-8").splitlines(), 1):
|
||||
# note the closing quote — excludes the unrelated OMNIVOICE_MODEL_LOAD_TIMEOUT
|
||||
if "environ" in line and ('OMNIVOICE_MODEL"' in line or "OMNIVOICE_MODEL'" in line):
|
||||
offenders.append(f"{rel}:{i}")
|
||||
assert not offenders, (
|
||||
"raw OMNIVOICE_MODEL reads outside the resolver (route them through "
|
||||
"resolve_omnivoice_checkpoint()): " + ", ".join(offenders)
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user