Compare commits

...
4 Commits
Author SHA1 Message Date
Palash DebnathandClaude Opus 4.8 5fbc654e82 chore(release): v0.3.5 (#272)
Patch release. Version bumped across all sources + lock files; [0.3.5] CHANGELOG.

Ships:
- #270 — speaker diarization fixed on PyTorch >=2.6 (weights_only=True rejected
  the pyannote checkpoint's TorchVersion global); the loader now registers the
  shared safe-globals allowlist before loading.

Tagging v0.3.5 triggers release.yml (desktop) + docker.yml.

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-03 10:45:25 +05:30
Palash DebnathandClaude Opus 4.8 f7d34a1433 fix(diarization): register torch safe-globals before pyannote load (#270) (#271)
On torch>=2.6, `Pipeline.from_pretrained("pyannote/speaker-diarization-3.1")`
fails with "Weights only load failed ... Unsupported global: GLOBAL
torch.torch_version.TorchVersion" — PyTorch 2.6 flipped torch.load's default to
weights_only=True and its secure unpickler rejects the checkpoint's metadata
globals. This broke diarization on torch>=2.6 even when the license IS accepted
(reported on v0.3.4, RTX 4070 Ti, license accepted).

The WhisperX VAD load already solved this via
`WhisperXBackend._allow_vad_pickle_globals()` (allowlists TorchVersion,
omegaconf nodes, pyannote metadata, builtins, numpy, …). `get_diarization_pipeline`
just never called it. Reuse it before the diarization load — idempotent,
per-process, verified to register TorchVersion on torch 2.8.

Graceful fallback (silence-gap heuristic) is preserved if anything still fails.

Tests: tests/test_diarization_weights_only.py (allowlist runs before load;
no-token short-circuit). Existing diarization classification tests still pass.

Cross-platform (the torch 2.6 weights_only change affects all platforms).

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-03 10:37:37 +05:30
Palash DebnathandClaude Opus 4.8 fa64dcaa92 chore(release): v0.3.4 (#269)
Patch release. Version bumped across all sources + lock files; [0.3.4] CHANGELOG.

Ships:
- #255 — PyTorch-Whisper backend works as a standalone fallback (no cuDNN 8,
  no OMNIVOICE_PRELOAD_TTS_ASR=1), unblocking Windows+NVIDIA users hitting the
  cudnn_ops_infer64_8.dll error.

Tagging v0.3.4 triggers release.yml (desktop) + docker.yml.

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-03 08:31:34 +05:30
Palash DebnathandClaude Opus 4.8 63a0d00f09 fix(asr): PyTorch-Whisper fallback works without cuDNN 8 or preload (#255) (#268)
Windows + NVIDIA users hit `Could not locate cudnn_ops_infer64_8.dll`:
WhisperX/faster-whisper run on CTranslate2, which needs cuDNN 8, but PyTorch
2.8 ships cuDNN 9 and the side-loaded `cudnn8_compat` libs were missing from
the venv. The PyTorch-Whisper backend should have been the fallback, but it
errored "set OMNIVOICE_PRELOAD_TTS_ASR=1" because it only worked when the TTS
model preloaded an ASR head.

- `PyTorchWhisperBackend._ensure_pipe()` now builds its OWN transformers ASR
  pipeline on demand (PyTorch stack → cuDNN 9, no CTranslate2/cuDNN-8), without
  loading the full TTS model and without the preload env var. A constructor-
  passed pipe (when the TTS model already has one) is still reused. Model is
  overridable via OMNIVOICE_PYTORCH_ASR_MODEL.
- dub_core transcribe preflight no longer hard-rejects pytorch-whisper when no
  pipe is preloaded — it lazy-loads; any failure surfaces per-chunk with the
  real cause.

So a Windows box without cuDNN 8 can switch ASR backend to "PyTorch Whisper"
in Settings → Models and transcription works. Docs: troubleshooting entry.

Tests: tests/test_pytorch_whisper_fallback.py (lazy standalone build, reuse of
a passed pipe, no get_model() call, env override). Full tests/ suite: 700 pass.

Does NOT close #255 — pending the reporter confirming the fallback works on
their machine; the cuDNN-8 install gap (faster-whisper path) is a follow-up.

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-03 08:23:58 +05:30
14 changed files with 237 additions and 29 deletions
+22
View File
@@ -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
+4 -5
View File
@@ -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)
+1 -1
View File
@@ -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"
+26 -16
View File
@@ -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
+12
View File
@@ -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)
+18 -1
View File
@@ -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 -1
View File
@@ -1,7 +1,7 @@
{
"name": "omnivoice-studio",
"private": true,
"version": "0.3.3",
"version": "0.3.5",
"type": "module",
"scripts": {
"dev": "vite",
+1 -1
View File
@@ -2878,7 +2878,7 @@ dependencies = [
[[package]]
name = "omnivoice-studio"
version = "0.3.3"
version = "0.3.5"
dependencies = [
"dirs-next",
"enigo",
+1 -1
View File
@@ -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 -1
View File
@@ -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
View File
@@ -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
+65
View File
@@ -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
+83
View File
@@ -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"
Generated
+1 -1
View File
@@ -3058,7 +3058,7 @@ wheels = [
[[package]]
name = "omnivoice"
version = "0.3.3"
version = "0.3.5"
source = { editable = "." }
dependencies = [
{ name = "accelerate" },