fix(engines): hide redundant OmniVoice MPS sidecar

This commit is contained in:
psiberfunk
2026-09-07 21:43:58 -04:00
parent 9790d28922
commit 8d60929236
8 changed files with 132 additions and 11 deletions
+1
View File
@@ -50,6 +50,7 @@ the frozen-backend fallback mirror it for their toolchains.
### Fixed
- Apple Silicon now shows one canonical OmniVoice choice in the engine picker while retaining its automatic crash-isolated sidecar runtime (#1913)
- Install documentation help now prints correctly on Windows consoles using legacy encodings (#1815) — thanks @dajiaohuang!
- Saved transcriptions with missing or invalid timestamps now remain readable (#1799) — thanks @yunaremaia and @tvbht!
- Copying a saved transcription now uses the shared clipboard helper and reports failed copies accurately (#1803) — thanks @tvbht!
+1 -1
View File
@@ -229,7 +229,7 @@ Engine support is capability-specific. Check cloning, language, platform, memory
| [**Sherpa-ONNX**](docs/engines/sherpa-onnx.md) | 20+ | No | No | CUDA/CPU | CPU | CUDA/CPU | Apache-2.0 |
| [**IndexTTS 2.5** ⚡](docs/engines/indextts.md) | ZH · EN · JA · ES · AR | Yes | No | CUDA/CPU | CPU | CUDA/CPU | Bilibili model license¹ |
| [**OmniVoice GGUF** ⚡](docs/engines/omnivoice-gguf.md) | 600+ | Yes | Yes | CUDA/CPU | MPS/CPU | CUDA/CPU | [AGPL-3.0](LICENSE) app · [review the derivative model terms](https://huggingface.co/Serveurperso/OmniVoice-GGUF#license)³ |
| [**OmniVoice (subprocess)** ⚡](docs/engines/omnivoice-subprocess.md) | 600+ | Yes | Yes | CUDA/CPU | MPS | CUDA/CPU | [AGPL-3.0](LICENSE) app · [Apache-2.0 code, CC-BY-NC weights](https://huggingface.co/k2-fsa/OmniVoice#license)³ |
| [**OmniVoice (subprocess; opt-in off MPS)** ⚡](docs/engines/omnivoice-subprocess.md) | 600+ | Yes | Yes | CUDA/CPU | MPS via default OmniVoice | CUDA/CPU | [AGPL-3.0](LICENSE) app · [Apache-2.0 code, CC-BY-NC weights](https://huggingface.co/k2-fsa/OmniVoice#license)³ |
| [**PocketTTS** ⚡](docs/engines/pockettts.md) | EN · FR · DE · PT · IT · ES | Yes | No | CPU | CPU | CPU | CC-BY-4.0, gated² |
| [**Supertonic 3** ⚡](docs/engines/supertonic3.md) | 31 | No | No | CPU | CPU | CPU | OpenRAIL-M |
| [**MOSS-TTS-v1.5** ⚡](docs/engines/moss-tts-v15.md) | 31 | Yes | No | CUDA/CPU | CPU | CUDA/CPU | Apache-2.0 |
+9 -1
View File
@@ -589,7 +589,15 @@ def select_engine(req: SelectEngineRequest):
if not family:
raise HTTPException(400, f"Unknown family: {req.family}. Expected one of tts/asr/llm.")
module, pref_key = family
available = {b["id"]: b for b in module.list_backends()}
# MPS intentionally hides the redundant explicit OmniVoice sidecar from
# the picker, but existing scripts and saved preferences may still submit
# that supported compatibility id directly.
rows = (
module.list_backends(include_hidden=True)
if req.family == "tts"
else module.list_backends()
)
available = {b["id"]: b for b in rows}
if req.backend_id not in available:
raise HTTPException(400, f"Unknown {req.family} backend: {req.backend_id!r}")
entry = available[req.backend_id]
+15 -2
View File
@@ -2391,8 +2391,15 @@ def _sidecar_installable_ids() -> frozenset[str]:
return frozenset()
def list_backends() -> list[dict]:
"""Enumerate every registered backend with its availability state.
def list_backends(*, include_hidden: bool = False) -> list[dict]:
"""Enumerate the engine catalogue with each backend's availability state.
On MPS, the canonical ``omnivoice`` id already resolves to the killable
OmniVoice sidecar. The explicit ``omnivoice-subprocess`` compatibility id
is therefore omitted from the normal catalogue so the picker does not
advertise two choices with the same runtime behavior. Internal callers
that must validate or preserve a stored compatibility id can pass
``include_hidden=True``.
Per-entry shape (ENGINE-05 + ENGINE-06):
@@ -2445,6 +2452,12 @@ def list_backends() -> list[dict]:
out: list[dict] = []
for bid, cls in _REGISTRY.items():
if (
not include_hidden
and caps.family == "mps"
and bid == "omnivoice-subprocess"
):
continue
cls = _effective_backend_class(bid, cls, caps.family)
try:
ok, msg = cls.is_available()
@@ -160,6 +160,67 @@ def test_engine_catalogue_reports_effective_mps_isolation(monkeypatch):
assert row["isolation_mode"] == "subprocess"
def test_mps_catalogue_hides_redundant_explicit_omnivoice_sidecar(monkeypatch):
"""The picker advertises the canonical id, while legacy callers retain both."""
from core.device_caps import HostCaps
from services import tts_backend
monkeypatch.setattr(
tts_backend,
"_REGISTRY",
{
"omnivoice": OmniVoiceBackend,
"omnivoice-subprocess": OmniVoiceSubprocessBackend,
},
)
monkeypatch.setattr(
"core.device_caps.detect_host_caps",
lambda: HostCaps(family="mps", available_families=("mps", "cpu")),
)
monkeypatch.setattr(
OmniVoiceSubprocessBackend,
"is_available",
classmethod(lambda cls: (True, "ready")),
)
picker_ids = {item["id"] for item in list_backends()}
assert picker_ids == {"omnivoice"}
assert get_backend_class("omnivoice") is OmniVoiceMPSSubprocessBackend
all_ids = {item["id"] for item in list_backends(include_hidden=True)}
assert all_ids == {"omnivoice", "omnivoice-subprocess"}
assert get_backend_class("omnivoice-subprocess") is OmniVoiceSubprocessBackend
@pytest.mark.parametrize("family", ("cuda", "cpu"))
def test_non_mps_catalogue_keeps_explicit_omnivoice_sidecar(monkeypatch, family):
from core.device_caps import HostCaps
from services import tts_backend
monkeypatch.setattr(
tts_backend,
"_REGISTRY",
{
"omnivoice": OmniVoiceBackend,
"omnivoice-subprocess": OmniVoiceSubprocessBackend,
},
)
monkeypatch.setattr(
"core.device_caps.detect_host_caps",
lambda: HostCaps(family=family, available_families=(family, "cpu")),
)
monkeypatch.setattr(
OmniVoiceSubprocessBackend,
"is_available",
classmethod(lambda cls: (True, "ready")),
)
assert {item["id"] for item in list_backends()} == {
"omnivoice",
"omnivoice-subprocess",
}
def test_mps_startup_does_not_preload_native_model(monkeypatch):
from core.device_caps import HostCaps
from services import model_manager
+1 -1
View File
@@ -36,7 +36,7 @@ approval), [Windows](../install/windows.md), [Linux](../install/linux.md),
| Supertonic-3 | [supertonic3](supertonic3.md) | CPU | — (7 preset voices) | `uv sync --extra supertonic` + license |
| MOSS-TTS-v1.5 (8B) | [moss-tts-v15](moss-tts-v15.md) | CUDA · CPU | ✅ | clone + env var |
| dots.tts (2B) | [dots-tts](dots-tts.md) | CUDA · CPU (not Windows) | ✅ | clone + env var |
| OmniVoice (subprocess) | [omnivoice-subprocess](omnivoice-subprocess.md) | CUDA · MPS · CPU | ✅ | opt-in pick, no install |
| OmniVoice (subprocess) | [omnivoice-subprocess](omnivoice-subprocess.md) | CUDA · MPS · CPU | ✅ | opt-in pick off MPS; automatic via default OmniVoice on MPS |
| PocketTTS (Kyutai) | [pockettts](pockettts.md) | CPU (not Intel Mac) | ✅ | `uv sync --extra pockettts` + license |
| Confucius4-TTS | [confucius4-tts](confucius4-tts.md) | CUDA · CPU | ✅ | clone + env var |
+6 -4
View File
@@ -32,12 +32,14 @@ lower call overhead.
## Selecting it
- **Model Catalogue → Engines**, or
- **Model Catalogue → Engines** on CUDA, ROCm, or CPU, or
- `OMNIVOICE_TTS_BACKEND=omnivoice-subprocess`
The explicit engine is opt-in on CUDA, ROCm, and CPU. Apple Silicon gets the
same isolation automatically while keeping the default `omnivoice` id in APIs,
Settings, and saved projects.
The explicit engine is opt-in on CUDA, ROCm, and CPU. On Apple Silicon it is
not listed separately: the canonical `omnivoice` choice automatically uses the
same isolation while keeping that default id in APIs, Settings, and saved
projects. Existing explicit `omnivoice-subprocess` configuration remains
accepted for compatibility.
## Platform support
+38 -2
View File
@@ -228,7 +228,8 @@ def test_indextts2_entry_has_subprocess_isolation_mode(fresh_app):
assert by_id["indextts2"]["isolation_mode"] == "subprocess"
def test_omnivoice_entry_has_in_process_isolation_mode(fresh_app):
def test_omnivoice_entry_has_in_process_isolation_mode(fresh_app, monkeypatch):
_force_cpu_host(monkeypatch)
client = _client(fresh_app)
r = client.get("/engines")
assert r.status_code == 200
@@ -237,8 +238,43 @@ def test_omnivoice_entry_has_in_process_isolation_mode(fresh_app):
assert by_id["omnivoice"]["isolation_mode"] == "in-process"
def test_gpu_compat_omnivoice_variants_include_rocm(fresh_app):
def test_mps_picker_hides_redundant_omnivoice_sidecar_but_api_accepts_it(
fresh_app, monkeypatch
):
"""Keep stored/direct compatibility ids valid without advertising a duplicate."""
from engines.omnivoice_subprocess import OmniVoiceSubprocessBackend
from services import tts_backend
_force_host(monkeypatch, "mps")
monkeypatch.delenv("OMNIVOICE_TTS_BACKEND", raising=False)
monkeypatch.setattr(
OmniVoiceSubprocessBackend,
"is_available",
classmethod(lambda cls: (True, "ready")),
)
client = _client(fresh_app)
rows = {row["id"] for row in client.get("/engines/tts").json()["backends"]}
assert "omnivoice" in rows
assert "omnivoice-subprocess" not in rows
canonical = next(
row for row in client.get("/engines/tts").json()["backends"] if row["id"] == "omnivoice"
)
assert canonical["isolation_mode"] == "subprocess"
response = client.post(
"/engines/select",
json={"family": "tts", "backend_id": "omnivoice-subprocess"},
)
assert response.status_code == 200, response.text
assert response.json()["active"] == "omnivoice-subprocess"
assert tts_backend.get_backend_class("omnivoice-subprocess") is OmniVoiceSubprocessBackend
def test_gpu_compat_omnivoice_variants_include_rocm(fresh_app, monkeypatch):
"""Both OmniVoice paths use torch's HIP-backed CUDA device on ROCm."""
_force_host(monkeypatch, "rocm")
client = _client(fresh_app)
r = client.get("/engines")
by_id = {b["id"]: b for b in r.json()["tts"]["backends"]}