Compare commits

..
5 Commits
Author SHA1 Message Date
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
Palash DebnathandClaude Opus 4.8 4e65774610 chore(release): v0.3.3 (#267)
Patch release. Bumps version across all sources + lock files; adds [0.3.3]
CHANGELOG.

Ships:
- #262 — Settings → About now shows the server's CPU architecture (was the
  client browser's platform, e.g. "Win32", in Docker).
- Validates the bash-3.2 checksum CI fix on a real release (the macOS
  SHA256SUMS should now upload automatically).

Tagging v0.3.3 triggers release.yml (desktop) + docker.yml (GHCR
:0.3.3/:0.3/:latest).

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-03 07:13:34 +05:30
Palash DebnathandClaude Opus 4.8 e740786a08 fix(about): show server CPU arch, not the client browser's platform (#262) (#266)
Settings → About → Architecture rendered `navigator.platform` — the *client
browser's* OS. In the Docker/web build that's the remote machine (e.g. "Win32"
when browsing from Windows), not the container, which is misleading.

Expose the server's `platform.machine()` as `arch` on /system/info and render
that instead, so the row reflects the machine OmniVoice actually runs on — for
both the desktop app (local backend) and Docker.

Note: the *blank* version/GPU/RAM/VRAM in the same report were the loopback-gate
403s fixed in v0.3.2 (#261); this PR fixes the remaining architecture row.

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-03 07:06:30 +05:30
Palash DebnathandClaude Opus 4.8 35b62ad0c0 fix(ci): make SHA-256 checksum step bash-3.2 safe (macOS runner) (#265)
The "Compute SHA-256 checksums" step used `mapfile -t` (a bash 4+ builtin) but
macOS GitHub runners execute `shell: bash` as /bin/bash 3.2, which has no
`mapfile`. The step exited 127 ("mapfile: command not found") on the macOS leg,
so `SHA256SUMS-macOS Apple Silicon.txt` was never produced/uploaded for v0.3.1
and v0.3.2 (the binaries themselves shipped fine; only the macOS checksum file
was missing and had to be regenerated by hand each time).

Replace `mapfile` with a portable `while IFS= read -r … done < <(find … | sort)`
loop (works on bash 3.2). Verified on bash 3.2.57: builds the array correctly,
handles spaces in bundle filenames. Linux/Windows legs are unaffected.

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-03 06:56:42 +05:30
18 changed files with 181 additions and 31 deletions
+7 -1
View File
@@ -540,7 +540,13 @@ jobs:
# Gather artifact paths per matrix leg's `bundles` (msi/app/dmg/deb/appimage/updater).
# `find` is portable across all three runners (Git Bash on Windows).
mapfile -t ARTIFACTS < <(find "$BUNDLE_DIR" -type f \
# NB: macOS runners use /bin/bash 3.2, which has no `mapfile` (a bash 4+
# builtin) — using it 127'd this step and dropped the macOS SHA256SUMS
# for v0.3.1 and v0.3.2. A `while read` loop is portable to bash 3.2.
ARTIFACTS=()
while IFS= read -r artifact; do
ARTIFACTS+=("$artifact")
done < <(find "$BUNDLE_DIR" -type f \
\( -name "*.dmg" -o -name "*.app.tar.gz" -o -name "*.app.tar.gz.sig" \
-o -name "*.msi" -o -name "*.msi.sig" \
-o -name "*.AppImage" -o -name "*.AppImage.sig" \
+26
View File
@@ -6,6 +6,32 @@ 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.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
- **Settings → About showed the wrong architecture in the Docker/web build.**
The "Architecture" row rendered the *client browser's* platform
(`navigator.platform` → e.g. "Win32"); it now reports the **server's** CPU
architecture from the backend (`platform.machine()`), correct for both the
desktop app and Docker. The blank version/GPU/RAM/VRAM in the same report
were the loopback-gate 403s already fixed in v0.3.2. (#262)
### CI
- The release SHA-256 checksum step no longer uses `mapfile` (a bash 4+
builtin) — it broke on the macOS runner's bash 3.2 and dropped the macOS
`SHA256SUMS` for v0.3.1/v0.3.2. Now portable to bash 3.2.
## [0.3.2] — 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)
+3
View File
@@ -1,5 +1,6 @@
import os
import sys
import platform
import uuid
import psutil
import asyncio
@@ -181,6 +182,7 @@ def system_info():
"device": get_best_device(),
"python": sys.version.split()[0],
"platform": sys.platform,
"arch": platform.machine(),
"ffmpeg_ok": bool(_ffmpeg),
"ffmpeg_path": _ffmpeg or "",
"proxy_url": os.environ.get("HTTP_PROXY") or os.environ.get("http_proxy") or "",
@@ -207,6 +209,7 @@ def system_info():
"device": "cpu",
"python": sys.version.split()[0],
"platform": sys.platform,
"arch": platform.machine(),
"proxy_url": "",
"share_enabled": network_share.get_state().enabled,
"share_port": network_share.get_state().share_port,
+1
View File
@@ -37,6 +37,7 @@ class SystemInfoResponse(BaseModel):
device: str = "cpu"
python: str = ""
platform: str = ""
arch: str = ""
error: str | None = None
ffmpeg_ok: bool = False
ffmpeg_path: str = ""
+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.2"
APP_VERSION = "0.3.4"
+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
+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.2",
"version": "0.3.4",
"type": "module",
"scripts": {
"dev": "vite",
+1 -1
View File
@@ -2878,7 +2878,7 @@ dependencies = [
[[package]]
name = "omnivoice-studio"
version = "0.3.2"
version = "0.3.4"
dependencies = [
"dirs-next",
"enigo",
+1 -1
View File
@@ -1,6 +1,6 @@
[package]
name = "omnivoice-studio"
version = "0.3.2"
version = "0.3.4"
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.2",
"version": "0.3.4",
"identifier": "com.debpalash.omnivoice-studio",
"build": {
"frontendDist": "../dist",
+1
View File
@@ -60,6 +60,7 @@ export interface SystemInfo {
app_version?: string;
python?: string;
platform?: string;
arch?: string;
device?: string;
data_dir?: string;
outputs_dir?: string;
+1 -1
View File
@@ -1355,7 +1355,7 @@ export default function Settings() {
<Row label={t('about.version')} value={appVersion || info?.app_version || '—'} mono />
<Row label={t('about.tauri_runtime')} value={tauriVersion || (isTauri() ? '—' : t('about.web_preview'))} mono />
<Row label={t('about.platform')} value={info?.platform || '—'} />
<Row label={t('about.architecture')} value={typeof navigator !== 'undefined' ? (navigator.userAgentData?.platform || navigator.platform || '—') : '—'} mono />
<Row label={t('about.architecture')} value={info?.arch || '—'} mono />
<Row label={t('about.python')} value={info?.python || '—'} mono />
<Row label={t('about.compute_device')} value={info?.device || '—'} mono />
<Row label={t('about.gpu_active')} value={hw?.gpu_active
+1 -1
View File
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
[project]
name = "omnivoice"
version = "0.3.2"
version = "0.3.4"
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
+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"
+4
View File
@@ -37,6 +37,10 @@ def test_system_info_smoke(client):
# running version from here so it shows the real version, not a dash (#249).
from core.version import APP_VERSION
assert body["app_version"] == APP_VERSION
# Settings → About → Architecture must reflect the SERVER's machine, not the
# client browser's navigator.platform (which showed "Win32" in Docker, #262).
import platform as _pf
assert body["arch"] == _pf.machine()
def test_health_exposes_version(client):
Generated
+1 -1
View File
@@ -3058,7 +3058,7 @@ wheels = [
[[package]]
name = "omnivoice"
version = "0.3.2"
version = "0.3.4"
source = { editable = "." }
dependencies = [
{ name = "accelerate" },