Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
5fbc654e82 | ||
|
|
f7d34a1433 | ||
|
|
fa64dcaa92 | ||
|
|
63a0d00f09 |
@@ -6,6 +6,28 @@ The format is loosely based on [Keep a Changelog](https://keepachangelog.com/).
|
||||
Versions track the desktop app (`tauri.conf.json` + `frontend/src-tauri/Cargo.toml`).
|
||||
The bundled TTS model package (`pyproject.toml`) is versioned independently.
|
||||
|
||||
## [0.3.5] — 2026-06-03
|
||||
|
||||
### Fixed
|
||||
- **Speaker diarization failed on PyTorch ≥ 2.6** (`Weights only load failed …
|
||||
Unsupported global: torch.torch_version.TorchVersion`) even with the pyannote
|
||||
license accepted. PyTorch 2.6 made `torch.load` default to
|
||||
`weights_only=True`, whose secure unpickler rejects the pyannote checkpoint's
|
||||
metadata globals. The diarization loader now registers the same safe-globals
|
||||
allowlist the WhisperX VAD load already uses, so the secure load succeeds.
|
||||
(#270)
|
||||
|
||||
## [0.3.4] — 2026-06-03
|
||||
|
||||
### Fixed
|
||||
- **Transcription on Windows + NVIDIA failed with `Could not locate
|
||||
cudnn_ops_infer64_8.dll`.** WhisperX/faster-whisper need cuDNN 8 (via
|
||||
CTranslate2); when the side-loaded `cudnn8_compat` libs are missing, the
|
||||
**PyTorch Whisper** backend (Settings → Models) now works as a drop-in
|
||||
fallback — it builds its own transformers pipeline on PyTorch's cuDNN-9
|
||||
stack, with no CTranslate2/cuDNN-8 dependency and no
|
||||
`OMNIVOICE_PRELOAD_TTS_ASR=1` required. (#255)
|
||||
|
||||
## [0.3.3] — 2026-06-03
|
||||
|
||||
### Fixed
|
||||
|
||||
@@ -404,12 +404,11 @@ async def dub_transcribe_stream(job_id: str):
|
||||
else:
|
||||
from services.asr_backend import get_active_asr_backend
|
||||
try:
|
||||
# The PyTorch-Whisper backend lazily builds its own pipeline
|
||||
# when no preloaded `_asr_pipe` is present (issue #255), so it
|
||||
# no longer needs OMNIVOICE_PRELOAD_TTS_ASR=1 — don't reject it
|
||||
# here; any load failure surfaces per-chunk with a real cause.
|
||||
_asr_backend = get_active_asr_backend(asr_pipe=getattr(_model, "_asr_pipe", None))
|
||||
if _asr_backend.id == "pytorch-whisper" and getattr(_model, "_asr_pipe", None) is None:
|
||||
preflight_error = (
|
||||
"No ASR backend is ready. Install WhisperX/faster-whisper/MLX Whisper "
|
||||
"or set OMNIVOICE_PRELOAD_TTS_ASR=1 before launch to use the PyTorch fallback."
|
||||
)
|
||||
except Exception as e:
|
||||
from core.failure import build_failure
|
||||
f = build_failure(e, stage="transcribe-preflight", include_diagnostic=False)
|
||||
|
||||
@@ -12,4 +12,4 @@ from importlib.metadata import PackageNotFoundError, version
|
||||
try:
|
||||
APP_VERSION = version("omnivoice")
|
||||
except PackageNotFoundError: # non-installed source checkout
|
||||
APP_VERSION = "0.3.3"
|
||||
APP_VERSION = "0.3.5"
|
||||
|
||||
@@ -577,22 +577,32 @@ class PyTorchWhisperBackend(ASRBackend):
|
||||
def _ensure_pipe(self):
|
||||
if self._pipe is not None:
|
||||
return
|
||||
# Fall back to grabbing the TTS model's ASR head.
|
||||
import asyncio
|
||||
from services.model_manager import get_model
|
||||
try:
|
||||
loop = asyncio.get_running_loop()
|
||||
if loop.is_running():
|
||||
raise RuntimeError(
|
||||
"PyTorchWhisperBackend needs the ASR pipe — pass it via constructor "
|
||||
"when calling from an async context."
|
||||
)
|
||||
model = loop.run_until_complete(get_model())
|
||||
except RuntimeError:
|
||||
model = asyncio.run(get_model())
|
||||
self._pipe = getattr(model, "_asr_pipe", None)
|
||||
if self._pipe is None:
|
||||
raise RuntimeError("Loaded TTS model has no `_asr_pipe` attribute.")
|
||||
# Build a standalone transformers Whisper pipeline on demand. This runs
|
||||
# on PyTorch's own stack (cuDNN 9 ships with torch), so it works as a
|
||||
# fallback on machines where WhisperX / faster-whisper can't load
|
||||
# cuDNN 8 (the `cudnn_ops_infer64_8.dll` failure, issue #255) — and it
|
||||
# needs neither OMNIVOICE_PRELOAD_TTS_ASR=1 nor a loaded TTS model.
|
||||
# When the TTS model already has an ASR head, dub_core passes it via the
|
||||
# constructor and this path is skipped.
|
||||
import torch
|
||||
from transformers import pipeline as hf_pipeline
|
||||
from services.model_manager import get_best_device
|
||||
|
||||
model_name = os.environ.get(
|
||||
"OMNIVOICE_PYTORCH_ASR_MODEL", "openai/whisper-large-v3-turbo"
|
||||
)
|
||||
device = get_best_device()
|
||||
asr_dtype = torch.float16 if str(device).startswith("cuda") else torch.float32
|
||||
logger.info(
|
||||
"PyTorchWhisperBackend: loading standalone ASR pipeline %s on %s",
|
||||
model_name, device,
|
||||
)
|
||||
self._pipe = hf_pipeline(
|
||||
"automatic-speech-recognition",
|
||||
model=model_name,
|
||||
dtype=asr_dtype,
|
||||
device_map=device,
|
||||
)
|
||||
|
||||
def transcribe(self, audio_path: str, *, word_timestamps: bool = True) -> dict:
|
||||
import soundfile as sf
|
||||
|
||||
@@ -633,6 +633,18 @@ def get_diarization_pipeline(return_error: bool = False):
|
||||
try:
|
||||
torch = _lazy_torch()
|
||||
_ensure_pyannote_hf_token_compat() # #167: use_auth_token -> token
|
||||
# PyTorch 2.6 flipped torch.load's default to weights_only=True, whose
|
||||
# secure unpickler rejects the pyannote checkpoint's metadata globals
|
||||
# (torch_version.TorchVersion, omegaconf nodes, …) — surfacing as
|
||||
# "Weights only load failed / Unsupported global" and breaking
|
||||
# diarization on torch>=2.6 even after the license is accepted (#270).
|
||||
# Reuse the exact allowlist the WhisperX VAD load registers so the
|
||||
# secure load path succeeds; it is idempotent and per-process.
|
||||
try:
|
||||
from services.asr_backend import WhisperXBackend
|
||||
WhisperXBackend._allow_vad_pickle_globals()
|
||||
except Exception as _glob_e:
|
||||
logger.debug("pyannote safe-globals allowlist skipped: %s", _glob_e)
|
||||
from pyannote.audio import Pipeline
|
||||
logger.info("Loading Pyannote Diarization Pipeline...")
|
||||
_diar_pipeline = Pipeline.from_pretrained("pyannote/speaker-diarization-3.1", use_auth_token=hf_token)
|
||||
|
||||
@@ -121,7 +121,24 @@ falling back to faster-whisper`.
|
||||
path and is still fast. If you want the latest CT2 wheels, run `uv sync`
|
||||
from a fresh source checkout.
|
||||
|
||||
## 10. IndexTTS / CosyVoice / ChatterboxTTS clash
|
||||
## 10. Windows: `Could not locate cudnn_ops_infer64_8.dll` during transcription
|
||||
|
||||
**Symptom:** on Windows + NVIDIA, transcription/dubbing fails and the backend
|
||||
log shows `Could not locate cudnn_ops_infer64_8.dll`. Settings → Models shows
|
||||
WhisperX or faster-whisper selected.
|
||||
|
||||
**Cause:** WhisperX and faster-whisper run on **CTranslate2**, which needs
|
||||
**cuDNN 8**, but PyTorch 2.8 ships cuDNN 9. OmniVoice side-loads a cuDNN-8 copy
|
||||
from `.venv\Lib\site-packages\cudnn8_compat\`; if that folder is missing
|
||||
(some upgrade paths don't install it), CTranslate2 can't find the DLL.
|
||||
|
||||
**Fix:** switch the ASR backend to **PyTorch Whisper** in **Settings → Models**.
|
||||
It runs on PyTorch's own stack (cuDNN 9, bundled with torch) and needs no
|
||||
cuDNN-8 DLL — it loads its Whisper pipeline on demand (no extra env var). To
|
||||
keep using faster-whisper/WhisperX instead, reinstall to restore the bundled
|
||||
`cudnn8_compat` libraries.
|
||||
|
||||
## 11. IndexTTS / CosyVoice / ChatterboxTTS clash
|
||||
|
||||
**Symptom:** installing one of these engines breaks the others — e.g. after
|
||||
installing CosyVoice, IndexTTS errors out with import conflicts.
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "omnivoice-studio",
|
||||
"private": true,
|
||||
"version": "0.3.3",
|
||||
"version": "0.3.5",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
|
||||
Generated
+1
-1
@@ -2878,7 +2878,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "omnivoice-studio"
|
||||
version = "0.3.3"
|
||||
version = "0.3.5"
|
||||
dependencies = [
|
||||
"dirs-next",
|
||||
"enigo",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "omnivoice-studio"
|
||||
version = "0.3.3"
|
||||
version = "0.3.5"
|
||||
description = "OmniVoice Studio – AI voice cloning & dubbing desktop app"
|
||||
authors = ["Debpalash"]
|
||||
license = "AGPL-3.0"
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"$schema": "../node_modules/@tauri-apps/cli/config.schema.json",
|
||||
"productName": "OmniVoice Studio",
|
||||
"version": "0.3.3",
|
||||
"version": "0.3.5",
|
||||
"identifier": "com.debpalash.omnivoice-studio",
|
||||
"build": {
|
||||
"frontendDist": "../dist",
|
||||
|
||||
+1
-1
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
|
||||
|
||||
[project]
|
||||
name = "omnivoice"
|
||||
version = "0.3.3"
|
||||
version = "0.3.5"
|
||||
description = "OmniVoice: Towards Omnilingual Zero-Shot Text-to-Speech with Diffusion Language Models"
|
||||
readme = "README.md"
|
||||
# Source-available under FSL-1.1-ALv2 (see LICENSE); each release converts to
|
||||
|
||||
@@ -0,0 +1,65 @@
|
||||
"""Diarization must register torch safe-globals before loading (issue #270).
|
||||
|
||||
PyTorch 2.6+ defaults `torch.load` to `weights_only=True`, whose secure
|
||||
unpickler rejects the pyannote checkpoint's metadata globals
|
||||
(`torch_version.TorchVersion`, omegaconf nodes, …). `get_diarization_pipeline`
|
||||
must register the same allowlist the WhisperX VAD load uses, before calling
|
||||
`Pipeline.from_pretrained`, or diarization breaks even with the license
|
||||
accepted.
|
||||
"""
|
||||
import sys
|
||||
import types
|
||||
|
||||
import pytest
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def reset_diar(monkeypatch):
|
||||
import services.model_manager as mm
|
||||
monkeypatch.setattr(mm, "_diar_pipeline", None, raising=False)
|
||||
yield mm
|
||||
monkeypatch.setattr(mm, "_diar_pipeline", None, raising=False)
|
||||
|
||||
|
||||
def test_loads_pyannote_after_registering_safe_globals(reset_diar, monkeypatch):
|
||||
mm = reset_diar
|
||||
order = []
|
||||
|
||||
# Token present (App source).
|
||||
monkeypatch.setattr(
|
||||
"services.token_resolver.resolve",
|
||||
lambda: types.SimpleNamespace(token="hf_test", source="app", user="u"),
|
||||
)
|
||||
|
||||
# Spy on the shared allowlister; must run BEFORE from_pretrained.
|
||||
from services import asr_backend as ab
|
||||
monkeypatch.setattr(
|
||||
ab.WhisperXBackend, "_allow_vad_pickle_globals",
|
||||
staticmethod(lambda: order.append("allow")),
|
||||
)
|
||||
|
||||
fake_pipe = object()
|
||||
|
||||
def _from_pretrained(*a, **k):
|
||||
order.append("load")
|
||||
return fake_pipe
|
||||
|
||||
fake_mod = types.ModuleType("pyannote.audio")
|
||||
fake_mod.Pipeline = types.SimpleNamespace(from_pretrained=_from_pretrained)
|
||||
monkeypatch.setitem(sys.modules, "pyannote.audio", fake_mod)
|
||||
|
||||
# CPU device → no .to() call on the fake pipe.
|
||||
monkeypatch.setattr(mm, "get_best_device", lambda: "cpu")
|
||||
|
||||
result = mm.get_diarization_pipeline()
|
||||
|
||||
assert result is fake_pipe
|
||||
assert order == ["allow", "load"], f"allowlist must precede load, got {order}"
|
||||
|
||||
|
||||
def test_no_token_short_circuits_without_loading(reset_diar, monkeypatch):
|
||||
mm = reset_diar
|
||||
monkeypatch.setattr("services.token_resolver.resolve", lambda: None)
|
||||
pipe, err = mm.get_diarization_pipeline(return_error=True)
|
||||
assert pipe is None
|
||||
assert err == mm.DIARIZATION_ERR_NO_TOKEN
|
||||
@@ -0,0 +1,83 @@
|
||||
"""PyTorch-Whisper backend must work as a standalone fallback (issue #255).
|
||||
|
||||
On machines where WhisperX / faster-whisper can't load cuDNN 8
|
||||
(`cudnn_ops_infer64_8.dll` missing), the PyTorch-Whisper backend should build
|
||||
its own transformers pipeline on demand — without OMNIVOICE_PRELOAD_TTS_ASR=1
|
||||
and without loading the full TTS model.
|
||||
"""
|
||||
import sys
|
||||
import types
|
||||
|
||||
import pytest
|
||||
|
||||
from services import asr_backend as ab
|
||||
|
||||
|
||||
def test_is_available_when_transformers_present():
|
||||
ok, msg = ab.PyTorchWhisperBackend.is_available()
|
||||
assert ok is True
|
||||
assert msg == "ready"
|
||||
|
||||
|
||||
def test_reuses_constructor_pipe_without_building(monkeypatch):
|
||||
sentinel = object()
|
||||
be = ab.PyTorchWhisperBackend(asr_pipe=sentinel)
|
||||
|
||||
def _boom(*a, **k):
|
||||
raise AssertionError("must not build a pipeline when one was passed in")
|
||||
|
||||
# transformers.pipeline is imported lazily inside _ensure_pipe.
|
||||
fake_tf = types.ModuleType("transformers")
|
||||
fake_tf.pipeline = _boom
|
||||
monkeypatch.setitem(sys.modules, "transformers", fake_tf)
|
||||
|
||||
be._ensure_pipe()
|
||||
assert be._pipe is sentinel
|
||||
|
||||
|
||||
def test_lazy_builds_standalone_pipeline(monkeypatch):
|
||||
"""No preloaded pipe → build a standalone transformers ASR pipeline, with no
|
||||
call into the TTS model loader (get_model)."""
|
||||
captured = {}
|
||||
|
||||
def fake_pipeline(task, **kw):
|
||||
captured["task"] = task
|
||||
captured["kw"] = kw
|
||||
return lambda *a, **k: {"chunks": []}
|
||||
|
||||
fake_tf = types.ModuleType("transformers")
|
||||
fake_tf.pipeline = fake_pipeline
|
||||
monkeypatch.setitem(sys.modules, "transformers", fake_tf)
|
||||
monkeypatch.setattr("services.model_manager.get_best_device", lambda: "cpu")
|
||||
|
||||
# Guard: building the standalone pipe must NOT pull in the full TTS model.
|
||||
import services.model_manager as mm
|
||||
|
||||
def _no_get_model(*a, **k):
|
||||
raise AssertionError("standalone ASR build must not call get_model()")
|
||||
|
||||
monkeypatch.setattr(mm, "get_model", _no_get_model, raising=False)
|
||||
|
||||
be = ab.PyTorchWhisperBackend(asr_pipe=None)
|
||||
be._ensure_pipe()
|
||||
|
||||
assert be._pipe is not None
|
||||
assert captured["task"] == "automatic-speech-recognition"
|
||||
assert captured["kw"]["model"] # a concrete model name was chosen
|
||||
|
||||
|
||||
def test_pytorch_asr_model_overridable_via_env(monkeypatch):
|
||||
captured = {}
|
||||
|
||||
def fake_pipeline(task, **kw):
|
||||
captured["kw"] = kw
|
||||
return object()
|
||||
|
||||
fake_tf = types.ModuleType("transformers")
|
||||
fake_tf.pipeline = fake_pipeline
|
||||
monkeypatch.setitem(sys.modules, "transformers", fake_tf)
|
||||
monkeypatch.setattr("services.model_manager.get_best_device", lambda: "cpu")
|
||||
monkeypatch.setenv("OMNIVOICE_PYTORCH_ASR_MODEL", "openai/whisper-small")
|
||||
|
||||
ab.PyTorchWhisperBackend(asr_pipe=None)._ensure_pipe()
|
||||
assert captured["kw"]["model"] == "openai/whisper-small"
|
||||
Reference in New Issue
Block a user