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:
Palash Debnath
2026-06-26 03:37:46 +05:30
committed by GitHub
co-authored by mergetest Claude Opus 4.8
parent 883a06e9c0
commit aa17e3319f
7 changed files with 46 additions and 8 deletions
+4 -1
View File
@@ -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
+1 -1
View File
@@ -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 "
+6 -1
View File
@@ -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,
),
)
+2 -2
View File
@@ -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(),
+1 -1
View File
@@ -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,
+5 -2
View File
@@ -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)
+27
View File
@@ -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)
)