fix(engines): hide redundant OmniVoice MPS sidecar
This commit is contained in:
@@ -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!
|
||||
|
||||
@@ -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 |
|
||||
|
||||
@@ -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]
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 |
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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"]}
|
||||
|
||||
Reference in New Issue
Block a user