mirror of
https://github.com/jamiepine/voicebox.git
synced 2026-09-29 07:05:14 -07:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ed2eec591a | ||
|
|
d61e884104 | ||
|
|
74e004400f | ||
|
|
0047352df1 | ||
|
|
7e7feeac54 |
+1
-1
@@ -1,5 +1,5 @@
|
|||||||
[bumpversion]
|
[bumpversion]
|
||||||
current_version = 0.4.3
|
current_version = 0.4.5
|
||||||
commit = True
|
commit = True
|
||||||
tag = True
|
tag = True
|
||||||
tag_name = v{new_version}
|
tag_name = v{new_version}
|
||||||
|
|||||||
+22
-1
@@ -7,6 +7,25 @@
|
|||||||
|
|
||||||
## [Unreleased]
|
## [Unreleased]
|
||||||
|
|
||||||
|
## [0.4.5] - 2026-04-22
|
||||||
|
|
||||||
|
Second hotfix for the "offline mode is enabled" crash on model load. 0.4.4 reverted the inference-path offline guards but kept the same trap on the load path, so users who updated to 0.4.4 kept hitting the exact error the release was supposed to fix ([#526](https://github.com/jamiepine/voicebox/issues/526)). This release removes the load-path guards and patches the transformers tokenizer load to be robust to HuggingFace metadata failures at the source, so the class of bug can't recur.
|
||||||
|
|
||||||
|
### Reliability
|
||||||
|
|
||||||
|
- **Load no longer fails with "offline mode is enabled"** ([#530](https://github.com/jamiepine/voicebox/pull/530), fixes [#526](https://github.com/jamiepine/voicebox/issues/526)). transformers 4.57.x added an unconditional `huggingface_hub.model_info()` call inside `AutoTokenizer.from_pretrained` (via `_patch_mistral_regex`) that runs for every non-local repo load, regardless of cache state or whether the target model is actually a Mistral variant. The load-time `HF_HUB_OFFLINE` guard from 0.4.2 turned that into a hard crash for cached online users the moment 0.4.4 removed the inference-path guard that had been masking the problem. Fix wraps `_patch_mistral_regex` so any exception from the HF metadata check is caught and the tokenizer is returned unchanged — matching the success-path behavior for non-Mistral repos. The wrapper installs at `backend.backends` import time so it covers Qwen Base, Qwen CustomVoice, TADA, and every other transformers-backed engine on Windows, Linux, and CUDA alike. The load-time `force_offline_if_cached` guards were removed — with the wrapper in place they provide zero value and only risk re-introducing the same failure mode.
|
||||||
|
- **No more 30s pause when generating without a network.** The HuggingFace metadata timeout called out as a known caveat in 0.4.4 is covered by the same patch; offline users no longer wait for the check to time out before load completes.
|
||||||
|
|
||||||
|
## [0.4.4] - 2026-04-21
|
||||||
|
|
||||||
|
Hotfix for a regression in 0.4.3 where generation and transcription could fail outright with "offline mode is enabled" even when the user was online.
|
||||||
|
|
||||||
|
### Reliability
|
||||||
|
|
||||||
|
- **Inference no longer fails with "offline mode is enabled" while online** ([#524](https://github.com/jamiepine/voicebox/pull/524), reverts the inference-path guards from [#503](https://github.com/jamiepine/voicebox/pull/503)). 0.4.3 wrapped every inference body (`generate`, `transcribe`, `create_voice_clone_prompt`) with a process-wide `HF_HUB_OFFLINE` flip to stop lazy HuggingFace lookups from hanging when the network drops mid-inference ([#462](https://github.com/jamiepine/voicebox/issues/462)). That flag also blocks legitimate metadata calls (e.g. `HfApi().model_info` for revision resolution) so online users started seeing generation fail outright. Inference now runs with the process's default HF state. Load-time offline guards — which weren't the source of the regression — stay in place.
|
||||||
|
|
||||||
|
**Known caveat**: users generating without an internet connection may see brief pauses during inference while HuggingFace metadata lookups time out (typically ~30s, after which the library recovers). A proper offline-mode toggle is planned for 0.4.5.
|
||||||
|
|
||||||
## [0.4.3] - 2026-04-20
|
## [0.4.3] - 2026-04-20
|
||||||
|
|
||||||
A patch focused on two user-impacting reliability fixes: macOS DMG notarization (unblocks `brew install voicebox` on macOS 15 Sequoia and fixes spurious "app isn't signed" Gatekeeper dialogs on older Intel Macs) and Kokoro Japanese voice initialization on fresh installs.
|
A patch focused on two user-impacting reliability fixes: macOS DMG notarization (unblocks `brew install voicebox` on macOS 15 Sequoia and fixes spurious "app isn't signed" Gatekeeper dialogs on older Intel Macs) and Kokoro Japanese voice initialization on fresh installs.
|
||||||
@@ -638,7 +657,9 @@ The first public release of Voicebox — an open-source voice synthesis studio p
|
|||||||
|
|
||||||
Tauri v2, React, TypeScript, Tailwind CSS, FastAPI, Qwen3-TTS, Whisper, SQLite
|
Tauri v2, React, TypeScript, Tailwind CSS, FastAPI, Qwen3-TTS, Whisper, SQLite
|
||||||
|
|
||||||
[Unreleased]: https://github.com/jamiepine/voicebox/compare/v0.4.3...HEAD
|
[Unreleased]: https://github.com/jamiepine/voicebox/compare/v0.4.5...HEAD
|
||||||
|
[0.4.5]: https://github.com/jamiepine/voicebox/compare/v0.4.4...v0.4.5
|
||||||
|
[0.4.4]: https://github.com/jamiepine/voicebox/compare/v0.4.3...v0.4.4
|
||||||
[0.4.3]: https://github.com/jamiepine/voicebox/compare/v0.4.2...v0.4.3
|
[0.4.3]: https://github.com/jamiepine/voicebox/compare/v0.4.2...v0.4.3
|
||||||
[0.4.2]: https://github.com/jamiepine/voicebox/compare/v0.4.1...v0.4.2
|
[0.4.2]: https://github.com/jamiepine/voicebox/compare/v0.4.1...v0.4.2
|
||||||
[0.4.1]: https://github.com/jamiepine/voicebox/compare/v0.4.0...v0.4.1
|
[0.4.1]: https://github.com/jamiepine/voicebox/compare/v0.4.0...v0.4.1
|
||||||
|
|||||||
+1
-1
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@voicebox/app",
|
"name": "@voicebox/app",
|
||||||
"version": "0.4.3",
|
"version": "0.4.5",
|
||||||
"private": true,
|
"private": true,
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
|
|||||||
+1
-1
@@ -1,3 +1,3 @@
|
|||||||
# Backend package
|
# Backend package
|
||||||
|
|
||||||
__version__ = "0.4.3"
|
__version__ = "0.4.5"
|
||||||
|
|||||||
@@ -5,6 +5,13 @@ Provides a unified interface for MLX and PyTorch backends,
|
|||||||
and a model config registry that eliminates per-engine dispatch maps.
|
and a model config registry that eliminates per-engine dispatch maps.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
# Install HF compatibility patches before any backend imports transformers /
|
||||||
|
# huggingface_hub. The module runs ``patch_transformers_mistral_regex`` at
|
||||||
|
# import time, which wraps transformers' tokenizer load against the
|
||||||
|
# unconditional HuggingFace metadata call that otherwise raises on
|
||||||
|
# HF_HUB_OFFLINE=1 and on network failures.
|
||||||
|
from ..utils import hf_offline_patch # noqa: F401
|
||||||
|
|
||||||
import threading
|
import threading
|
||||||
from dataclasses import dataclass, field
|
from dataclasses import dataclass, field
|
||||||
from typing import Protocol, Optional, Tuple, List
|
from typing import Protocol, Optional, Tuple, List
|
||||||
|
|||||||
@@ -20,7 +20,6 @@ ensure_original_qwen_config_cached()
|
|||||||
from . import TTSBackend, STTBackend, LANGUAGE_CODE_TO_NAME, WHISPER_HF_REPOS
|
from . import TTSBackend, STTBackend, LANGUAGE_CODE_TO_NAME, WHISPER_HF_REPOS
|
||||||
from .base import is_model_cached, combine_voice_prompts as _combine_voice_prompts, model_load_progress
|
from .base import is_model_cached, combine_voice_prompts as _combine_voice_prompts, model_load_progress
|
||||||
from ..utils.cache import get_cache_key, get_cached_voice_prompt, cache_voice_prompt
|
from ..utils.cache import get_cache_key, get_cached_voice_prompt, cache_voice_prompt
|
||||||
from ..utils.hf_offline_patch import force_offline_if_cached
|
|
||||||
|
|
||||||
|
|
||||||
class MLXTTSBackend:
|
class MLXTTSBackend:
|
||||||
@@ -99,7 +98,6 @@ class MLXTTSBackend:
|
|||||||
|
|
||||||
logger.info("Loading MLX TTS model %s...", model_size)
|
logger.info("Loading MLX TTS model %s...", model_size)
|
||||||
|
|
||||||
with force_offline_if_cached(is_cached, model_name):
|
|
||||||
self.model = load(model_path)
|
self.model = load(model_path)
|
||||||
|
|
||||||
self._current_model_size = model_size
|
self._current_model_size = model_size
|
||||||
@@ -193,8 +191,6 @@ class MLXTTSBackend:
|
|||||||
|
|
||||||
logger.info("Generating audio for text: %s", text)
|
logger.info("Generating audio for text: %s", text)
|
||||||
|
|
||||||
model_name = f"qwen-tts-{self._current_model_size}"
|
|
||||||
|
|
||||||
def _generate_sync():
|
def _generate_sync():
|
||||||
"""Run synchronous generation in thread pool."""
|
"""Run synchronous generation in thread pool."""
|
||||||
# MLX generate() returns a generator yielding GenerationResult objects
|
# MLX generate() returns a generator yielding GenerationResult objects
|
||||||
@@ -220,14 +216,12 @@ class MLXTTSBackend:
|
|||||||
logger.warning("Regenerating without voice prompt.")
|
logger.warning("Regenerating without voice prompt.")
|
||||||
ref_audio = None
|
ref_audio = None
|
||||||
|
|
||||||
# Model is loaded → weights are on disk. Force offline so
|
# Inference runs with the process's default HF_HUB_OFFLINE
|
||||||
# lazy tokenizer/config lookups inside mlx_audio don't hang
|
# state. Forcing offline here (previously used to avoid lazy
|
||||||
# when the user is disconnected (issue #462).
|
# mlx_audio lookups hanging when the network drops mid-inference,
|
||||||
with force_offline_if_cached(True, model_name):
|
# issue #462) regressed online users because libraries make
|
||||||
# Check if model supports voice cloning via generate method
|
# legitimate metadata calls during generation.
|
||||||
# MLX API may support ref_audio parameter directly
|
|
||||||
try:
|
try:
|
||||||
# Try with voice cloning parameters if supported
|
|
||||||
if ref_audio:
|
if ref_audio:
|
||||||
# Check if generate accepts ref_audio parameter
|
# Check if generate accepts ref_audio parameter
|
||||||
import inspect
|
import inspect
|
||||||
@@ -315,7 +309,6 @@ class MLXSTTBackend:
|
|||||||
model_name = WHISPER_HF_REPOS.get(model_size, f"openai/whisper-{model_size}")
|
model_name = WHISPER_HF_REPOS.get(model_size, f"openai/whisper-{model_size}")
|
||||||
logger.info("Loading MLX Whisper model %s...", model_size)
|
logger.info("Loading MLX Whisper model %s...", model_size)
|
||||||
|
|
||||||
with force_offline_if_cached(is_cached, progress_model_name):
|
|
||||||
self.model = load(model_name)
|
self.model = load(model_name)
|
||||||
|
|
||||||
self.model_size = model_size
|
self.model_size = model_size
|
||||||
@@ -347,8 +340,6 @@ class MLXSTTBackend:
|
|||||||
"""
|
"""
|
||||||
await self.load_model_async(model_size)
|
await self.load_model_async(model_size)
|
||||||
|
|
||||||
progress_model_name = f"whisper-{self.model_size}"
|
|
||||||
|
|
||||||
def _transcribe_sync():
|
def _transcribe_sync():
|
||||||
"""Run synchronous transcription in thread pool."""
|
"""Run synchronous transcription in thread pool."""
|
||||||
# MLX Whisper transcription using generate method
|
# MLX Whisper transcription using generate method
|
||||||
@@ -357,10 +348,9 @@ class MLXSTTBackend:
|
|||||||
if language:
|
if language:
|
||||||
decode_options["language"] = language
|
decode_options["language"] = language
|
||||||
|
|
||||||
# Model is loaded → weights are on disk. Force offline so
|
# Inference runs with the process's default HF_HUB_OFFLINE
|
||||||
# lazy tokenizer/config lookups don't hang when the user is
|
# state — see the comment in MLXTTSBackend.generate for the
|
||||||
# disconnected (issue #462).
|
# regression this revert fixes (issue #462).
|
||||||
with force_offline_if_cached(True, progress_model_name):
|
|
||||||
result = self.model.generate(str(audio_path), **decode_options)
|
result = self.model.generate(str(audio_path), **decode_options)
|
||||||
|
|
||||||
# Extract text from result
|
# Extract text from result
|
||||||
|
|||||||
@@ -21,7 +21,6 @@ from .base import (
|
|||||||
)
|
)
|
||||||
from ..utils.cache import get_cache_key, get_cached_voice_prompt, cache_voice_prompt
|
from ..utils.cache import get_cache_key, get_cached_voice_prompt, cache_voice_prompt
|
||||||
from ..utils.audio import load_audio
|
from ..utils.audio import load_audio
|
||||||
from ..utils.hf_offline_patch import force_offline_if_cached
|
|
||||||
|
|
||||||
|
|
||||||
class PyTorchTTSBackend:
|
class PyTorchTTSBackend:
|
||||||
@@ -106,7 +105,6 @@ class PyTorchTTSBackend:
|
|||||||
from huggingface_hub import constants as hf_constants
|
from huggingface_hub import constants as hf_constants
|
||||||
tts_cache_dir = hf_constants.HF_HUB_CACHE
|
tts_cache_dir = hf_constants.HF_HUB_CACHE
|
||||||
|
|
||||||
with force_offline_if_cached(is_cached, model_name):
|
|
||||||
if self.device == "cpu":
|
if self.device == "cpu":
|
||||||
self.model = Qwen3TTSModel.from_pretrained(
|
self.model = Qwen3TTSModel.from_pretrained(
|
||||||
model_path,
|
model_path,
|
||||||
@@ -172,14 +170,12 @@ class PyTorchTTSBackend:
|
|||||||
# This shouldn't happen in practice, but handle it
|
# This shouldn't happen in practice, but handle it
|
||||||
return {"prompt": cached_prompt}, True
|
return {"prompt": cached_prompt}, True
|
||||||
|
|
||||||
model_name = f"qwen-tts-{self._current_model_size}"
|
|
||||||
|
|
||||||
def _create_prompt_sync():
|
def _create_prompt_sync():
|
||||||
"""Run synchronous voice prompt creation in thread pool."""
|
"""Run synchronous voice prompt creation in thread pool."""
|
||||||
# Model is loaded → weights are on disk. Force offline so
|
# Inference runs with the process's default HF_HUB_OFFLINE
|
||||||
# lazy tokenizer/config lookups inside qwen_tts don't hang
|
# state. Forcing offline here (issue #462) regressed online
|
||||||
# when the user is disconnected (issue #462).
|
# users whose libraries issue legitimate metadata lookups
|
||||||
with force_offline_if_cached(True, model_name):
|
# during voice-prompt creation.
|
||||||
return self.model.create_voice_clone_prompt(
|
return self.model.create_voice_clone_prompt(
|
||||||
ref_audio=str(audio_path),
|
ref_audio=str(audio_path),
|
||||||
ref_text=reference_text,
|
ref_text=reference_text,
|
||||||
@@ -227,18 +223,14 @@ class PyTorchTTSBackend:
|
|||||||
# Load model
|
# Load model
|
||||||
await self.load_model_async(None)
|
await self.load_model_async(None)
|
||||||
|
|
||||||
model_name = f"qwen-tts-{self._current_model_size}"
|
|
||||||
|
|
||||||
def _generate_sync():
|
def _generate_sync():
|
||||||
"""Run synchronous generation in thread pool."""
|
"""Run synchronous generation in thread pool."""
|
||||||
# Set seed if provided
|
# Set seed if provided
|
||||||
if seed is not None:
|
if seed is not None:
|
||||||
manual_seed(seed, self.device)
|
manual_seed(seed, self.device)
|
||||||
|
|
||||||
# Model is loaded → weights are on disk. Force offline so
|
# See _create_prompt_sync comment — inference runs with the
|
||||||
# lazy tokenizer/config lookups inside qwen_tts don't hang
|
# process's default HF_HUB_OFFLINE state (issue #462).
|
||||||
# when the user is disconnected (issue #462).
|
|
||||||
with force_offline_if_cached(True, model_name):
|
|
||||||
wavs, sample_rate = self.model.generate_voice_clone(
|
wavs, sample_rate = self.model.generate_voice_clone(
|
||||||
text=text,
|
text=text,
|
||||||
voice_clone_prompt=voice_prompt,
|
voice_clone_prompt=voice_prompt,
|
||||||
@@ -303,7 +295,6 @@ class PyTorchSTTBackend:
|
|||||||
model_name = WHISPER_HF_REPOS.get(model_size, f"openai/whisper-{model_size}")
|
model_name = WHISPER_HF_REPOS.get(model_size, f"openai/whisper-{model_size}")
|
||||||
logger.info("Loading Whisper model %s on %s...", model_size, self.device)
|
logger.info("Loading Whisper model %s on %s...", model_size, self.device)
|
||||||
|
|
||||||
with force_offline_if_cached(is_cached, progress_model_name):
|
|
||||||
self.processor = WhisperProcessor.from_pretrained(model_name)
|
self.processor = WhisperProcessor.from_pretrained(model_name)
|
||||||
self.model = WhisperForConditionalGeneration.from_pretrained(model_name)
|
self.model = WhisperForConditionalGeneration.from_pretrained(model_name)
|
||||||
|
|
||||||
@@ -342,17 +333,15 @@ class PyTorchSTTBackend:
|
|||||||
"""
|
"""
|
||||||
await self.load_model_async(model_size)
|
await self.load_model_async(model_size)
|
||||||
|
|
||||||
progress_model_name = f"whisper-{self.model_size}"
|
|
||||||
|
|
||||||
def _transcribe_sync():
|
def _transcribe_sync():
|
||||||
"""Run synchronous transcription in thread pool."""
|
"""Run synchronous transcription in thread pool."""
|
||||||
# Load audio
|
# Load audio
|
||||||
audio, _sr = load_audio(audio_path, sample_rate=16000)
|
audio, _sr = load_audio(audio_path, sample_rate=16000)
|
||||||
|
|
||||||
# Model is loaded → weights are on disk. Force offline so
|
# Inference runs with the process's default HF_HUB_OFFLINE
|
||||||
# `get_decoder_prompt_ids` and any lazy tokenizer lookups
|
# state — forcing offline here (issue #462) broke online users
|
||||||
# don't hang when the user is disconnected (issue #462).
|
# whose `get_decoder_prompt_ids` / tokenizer calls issue
|
||||||
with force_offline_if_cached(True, progress_model_name):
|
# legitimate metadata lookups.
|
||||||
# Process audio
|
# Process audio
|
||||||
inputs = self.processor(
|
inputs = self.processor(
|
||||||
audio,
|
audio,
|
||||||
|
|||||||
@@ -28,7 +28,6 @@ from .base import (
|
|||||||
combine_voice_prompts as _combine_voice_prompts,
|
combine_voice_prompts as _combine_voice_prompts,
|
||||||
model_load_progress,
|
model_load_progress,
|
||||||
)
|
)
|
||||||
from ..utils.hf_offline_patch import force_offline_if_cached
|
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
@@ -105,7 +104,6 @@ class QwenCustomVoiceBackend:
|
|||||||
model_path = self._get_model_path(model_size)
|
model_path = self._get_model_path(model_size)
|
||||||
logger.info("Loading Qwen CustomVoice %s on %s...", model_size, self.device)
|
logger.info("Loading Qwen CustomVoice %s on %s...", model_size, self.device)
|
||||||
|
|
||||||
with force_offline_if_cached(is_cached, model_name):
|
|
||||||
if self.device == "cpu":
|
if self.device == "cpu":
|
||||||
self.model = Qwen3TTSModel.from_pretrained(
|
self.model = Qwen3TTSModel.from_pretrained(
|
||||||
model_path,
|
model_path,
|
||||||
@@ -186,7 +184,6 @@ class QwenCustomVoiceBackend:
|
|||||||
await self.load_model_async(None)
|
await self.load_model_async(None)
|
||||||
|
|
||||||
speaker = voice_prompt.get("preset_voice_id") or QWEN_CV_DEFAULT_SPEAKER
|
speaker = voice_prompt.get("preset_voice_id") or QWEN_CV_DEFAULT_SPEAKER
|
||||||
model_name = f"qwen-custom-voice-{self._current_model_size}"
|
|
||||||
|
|
||||||
def _generate_sync():
|
def _generate_sync():
|
||||||
if seed is not None:
|
if seed is not None:
|
||||||
@@ -206,10 +203,10 @@ class QwenCustomVoiceBackend:
|
|||||||
if instruct:
|
if instruct:
|
||||||
kwargs["instruct"] = instruct
|
kwargs["instruct"] = instruct
|
||||||
|
|
||||||
# Model is loaded → weights are on disk. Force offline so
|
# Inference runs with the process's default HF_HUB_OFFLINE
|
||||||
# lazy tokenizer/config lookups inside qwen_tts don't hang
|
# state. Forcing offline here (issue #462) regressed online
|
||||||
# when the user is disconnected (issue #462).
|
# users whose libraries issue legitimate metadata lookups
|
||||||
with force_offline_if_cached(True, model_name):
|
# during generation.
|
||||||
wavs, sample_rate = self.model.generate_custom_voice(**kwargs)
|
wavs, sample_rate = self.model.generate_custom_voice(**kwargs)
|
||||||
return wavs[0], sample_rate
|
return wavs[0], sample_rate
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,113 @@
|
|||||||
|
"""
|
||||||
|
Unit tests for ``patch_transformers_mistral_regex``.
|
||||||
|
|
||||||
|
Verifies that our wrapper around
|
||||||
|
``transformers.PreTrainedTokenizerBase._patch_mistral_regex`` catches
|
||||||
|
exceptions from the unconditional ``huggingface_hub.model_info()`` lookup
|
||||||
|
and returns the tokenizer unchanged — matching the success-path behavior
|
||||||
|
for non-Mistral repos (transformers 4.57.3, ``tokenization_utils_base.py:2503``).
|
||||||
|
|
||||||
|
NOTE: These tests mutate ``transformers.PreTrainedTokenizerBase`` globally;
|
||||||
|
run serially, not under ``pytest-xdist`` with per-worker process isolation.
|
||||||
|
"""
|
||||||
|
|
||||||
|
import sys
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
|
||||||
|
sys.path.insert(0, str(Path(__file__).parent.parent))
|
||||||
|
|
||||||
|
from huggingface_hub.errors import OfflineModeIsEnabled # noqa: E402
|
||||||
|
from transformers.tokenization_utils_base import PreTrainedTokenizerBase # noqa: E402
|
||||||
|
|
||||||
|
import utils.hf_offline_patch as hf_offline_patch # noqa: E402
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture(autouse=True)
|
||||||
|
def restore_mistral_regex():
|
||||||
|
"""Snapshot the current ``_patch_mistral_regex`` and restore after each test."""
|
||||||
|
saved = PreTrainedTokenizerBase.__dict__.get("_patch_mistral_regex")
|
||||||
|
saved_flag = hf_offline_patch._mistral_regex_patched
|
||||||
|
try:
|
||||||
|
yield
|
||||||
|
finally:
|
||||||
|
if saved is not None:
|
||||||
|
PreTrainedTokenizerBase._patch_mistral_regex = saved
|
||||||
|
hf_offline_patch._mistral_regex_patched = saved_flag
|
||||||
|
|
||||||
|
|
||||||
|
def _apply_patch():
|
||||||
|
hf_offline_patch._mistral_regex_patched = False
|
||||||
|
hf_offline_patch.patch_transformers_mistral_regex()
|
||||||
|
|
||||||
|
|
||||||
|
def test_suppresses_offline_mode_is_enabled(monkeypatch):
|
||||||
|
_apply_patch()
|
||||||
|
|
||||||
|
import huggingface_hub
|
||||||
|
|
||||||
|
def raise_offline(*_args, **_kwargs):
|
||||||
|
raise OfflineModeIsEnabled("offline")
|
||||||
|
|
||||||
|
monkeypatch.setattr(huggingface_hub, "model_info", raise_offline)
|
||||||
|
|
||||||
|
sentinel = object()
|
||||||
|
result = PreTrainedTokenizerBase._patch_mistral_regex(
|
||||||
|
sentinel, "Qwen/Qwen3-TTS-12Hz-1.7B-Base"
|
||||||
|
)
|
||||||
|
assert result is sentinel
|
||||||
|
|
||||||
|
|
||||||
|
def test_suppresses_connection_errors(monkeypatch):
|
||||||
|
_apply_patch()
|
||||||
|
|
||||||
|
import huggingface_hub
|
||||||
|
|
||||||
|
def raise_connection(*_args, **_kwargs):
|
||||||
|
raise ConnectionError("network unreachable")
|
||||||
|
|
||||||
|
monkeypatch.setattr(huggingface_hub, "model_info", raise_connection)
|
||||||
|
|
||||||
|
sentinel = object()
|
||||||
|
result = PreTrainedTokenizerBase._patch_mistral_regex(
|
||||||
|
sentinel, "Qwen/Qwen3-TTS-12Hz-1.7B-Base"
|
||||||
|
)
|
||||||
|
assert result is sentinel
|
||||||
|
|
||||||
|
|
||||||
|
def test_passthrough_on_success(monkeypatch):
|
||||||
|
"""When model_info returns non-Mistral tags the original falls through and returns the tokenizer unchanged."""
|
||||||
|
_apply_patch()
|
||||||
|
|
||||||
|
import huggingface_hub
|
||||||
|
|
||||||
|
class FakeInfo:
|
||||||
|
tags = ["model-type:qwen", "language:en"]
|
||||||
|
|
||||||
|
monkeypatch.setattr(huggingface_hub, "model_info", lambda *_a, **_kw: FakeInfo())
|
||||||
|
|
||||||
|
sentinel = object()
|
||||||
|
result = PreTrainedTokenizerBase._patch_mistral_regex(
|
||||||
|
sentinel, "Qwen/Qwen3-TTS-12Hz-1.7B-Base"
|
||||||
|
)
|
||||||
|
assert result is sentinel
|
||||||
|
|
||||||
|
|
||||||
|
def test_idempotent():
|
||||||
|
_apply_patch()
|
||||||
|
first = PreTrainedTokenizerBase._patch_mistral_regex
|
||||||
|
hf_offline_patch.patch_transformers_mistral_regex()
|
||||||
|
second = PreTrainedTokenizerBase._patch_mistral_regex
|
||||||
|
assert first.__func__ is second.__func__
|
||||||
|
|
||||||
|
|
||||||
|
def test_missing_method_is_noop(monkeypatch):
|
||||||
|
monkeypatch.delattr(PreTrainedTokenizerBase, "_patch_mistral_regex", raising=False)
|
||||||
|
hf_offline_patch._mistral_regex_patched = False
|
||||||
|
hf_offline_patch.patch_transformers_mistral_regex()
|
||||||
|
assert hf_offline_patch._mistral_regex_patched is False
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
pytest.main([__file__, "-v"])
|
||||||
@@ -142,6 +142,57 @@ def force_offline_if_cached(is_cached: bool, model_label: str = ""):
|
|||||||
_saved_transformers_const = None
|
_saved_transformers_const = None
|
||||||
|
|
||||||
|
|
||||||
|
_mistral_regex_patched = False
|
||||||
|
|
||||||
|
|
||||||
|
def patch_transformers_mistral_regex():
|
||||||
|
"""Make transformers' tokenizer load robust to HuggingFace metadata failures.
|
||||||
|
|
||||||
|
transformers 4.57.x added ``PreTrainedTokenizerBase._patch_mistral_regex``
|
||||||
|
which unconditionally calls ``huggingface_hub.model_info(repo_id)`` during
|
||||||
|
every non-local tokenizer load to check whether the model is a Mistral
|
||||||
|
variant. That call raises on ``HF_HUB_OFFLINE=1`` and on plain network
|
||||||
|
failures, killing unrelated loads (Qwen TTS, TADA, etc.).
|
||||||
|
|
||||||
|
Voicebox never loads Mistral models, so the rewrite the function would
|
||||||
|
apply is a no-op for us anyway. Wrap the method so any exception from the
|
||||||
|
metadata lookup returns the tokenizer unchanged — matching the success-path
|
||||||
|
behavior for non-Mistral repos (transformers 4.57.3,
|
||||||
|
``tokenization_utils_base.py:2503``).
|
||||||
|
"""
|
||||||
|
global _mistral_regex_patched
|
||||||
|
if _mistral_regex_patched:
|
||||||
|
return
|
||||||
|
|
||||||
|
try:
|
||||||
|
from transformers.tokenization_utils_base import PreTrainedTokenizerBase
|
||||||
|
except ImportError:
|
||||||
|
logger.debug("transformers not available, skipping mistral-regex patch")
|
||||||
|
return
|
||||||
|
|
||||||
|
original = getattr(PreTrainedTokenizerBase, "_patch_mistral_regex", None)
|
||||||
|
if original is None:
|
||||||
|
logger.debug(
|
||||||
|
"transformers has no _patch_mistral_regex attribute, skipping patch",
|
||||||
|
)
|
||||||
|
return
|
||||||
|
|
||||||
|
def safe_patch_mistral_regex(cls, tokenizer, pretrained_model_name_or_path, *args, **kwargs):
|
||||||
|
try:
|
||||||
|
return original(tokenizer, pretrained_model_name_or_path, *args, **kwargs)
|
||||||
|
except Exception as exc:
|
||||||
|
logger.debug(
|
||||||
|
"[mistral-regex-patch] suppressed %s for %r, returning tokenizer as-is",
|
||||||
|
type(exc).__name__,
|
||||||
|
pretrained_model_name_or_path,
|
||||||
|
)
|
||||||
|
return tokenizer
|
||||||
|
|
||||||
|
PreTrainedTokenizerBase._patch_mistral_regex = classmethod(safe_patch_mistral_regex)
|
||||||
|
_mistral_regex_patched = True
|
||||||
|
logger.debug("installed _patch_mistral_regex wrapper")
|
||||||
|
|
||||||
|
|
||||||
def patch_huggingface_hub_offline():
|
def patch_huggingface_hub_offline():
|
||||||
"""Monkey-patch huggingface_hub to force offline mode."""
|
"""Monkey-patch huggingface_hub to force offline mode."""
|
||||||
try:
|
try:
|
||||||
@@ -215,4 +266,5 @@ def ensure_original_qwen_config_cached():
|
|||||||
|
|
||||||
if os.environ.get("VOICEBOX_OFFLINE_PATCH", "1") != "0":
|
if os.environ.get("VOICEBOX_OFFLINE_PATCH", "1") != "0":
|
||||||
patch_huggingface_hub_offline()
|
patch_huggingface_hub_offline()
|
||||||
|
patch_transformers_mistral_regex()
|
||||||
ensure_original_qwen_config_cached()
|
ensure_original_qwen_config_cached()
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@voicebox/landing",
|
"name": "@voicebox/landing",
|
||||||
"version": "0.4.3",
|
"version": "0.4.5",
|
||||||
"description": "Landing page for voicebox.sh",
|
"description": "Landing page for voicebox.sh",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "next dev --turbo",
|
"dev": "next dev --turbo",
|
||||||
|
|||||||
Binary file not shown.
|
After Width: | Height: | Size: 127 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 186 KiB |
@@ -29,10 +29,10 @@ const TUTORIALS: (Tutorial | null)[] = [
|
|||||||
thumbnail: "/tutorials/kqxqjRsdD5E.jpg",
|
thumbnail: "/tutorials/kqxqjRsdD5E.jpg",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: "yu9QHqOEqqA",
|
id: "05YBqrWTLQ0",
|
||||||
title: "This FREE AI Tool Just Destroyed ElevenLabs Voice Cloning (VoiceBox)",
|
title: "2026年最好的声音克隆工具?Voicebox完整测评:从下载到API调用,附速度对比",
|
||||||
author: "Danish Sofi",
|
author: "Tech指南",
|
||||||
thumbnail: "/tutorials/yu9QHqOEqqA.jpg",
|
thumbnail: "/tutorials/05YBqrWTLQ0.jpg",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: "RRRBxNXgeKQ",
|
id: "RRRBxNXgeKQ",
|
||||||
|
|||||||
+1
-1
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "voicebox",
|
"name": "voicebox",
|
||||||
"version": "0.4.3",
|
"version": "0.4.5",
|
||||||
"private": true,
|
"private": true,
|
||||||
"workspaces": [
|
"workspaces": [
|
||||||
"app",
|
"app",
|
||||||
|
|||||||
+1
-1
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"name": "@voicebox/tauri",
|
"name": "@voicebox/tauri",
|
||||||
"private": true,
|
"private": true,
|
||||||
"version": "0.4.3",
|
"version": "0.4.5",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "vite",
|
"dev": "vite",
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "voicebox"
|
name = "voicebox"
|
||||||
version = "0.4.3"
|
version = "0.4.5"
|
||||||
description = "A production-quality desktop app for Qwen3-TTS voice cloning and generation"
|
description = "A production-quality desktop app for Qwen3-TTS voice cloning and generation"
|
||||||
authors = ["you"]
|
authors = ["you"]
|
||||||
license = ""
|
license = ""
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"$schema": "https://schema.tauri.app/config/2",
|
"$schema": "https://schema.tauri.app/config/2",
|
||||||
"productName": "Voicebox",
|
"productName": "Voicebox",
|
||||||
"version": "0.4.3",
|
"version": "0.4.5",
|
||||||
"identifier": "sh.voicebox.app",
|
"identifier": "sh.voicebox.app",
|
||||||
"build": {
|
"build": {
|
||||||
"beforeDevCommand": "bun run dev",
|
"beforeDevCommand": "bun run dev",
|
||||||
|
|||||||
+1
-1
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"name": "@voicebox/web",
|
"name": "@voicebox/web",
|
||||||
"private": true,
|
"private": true,
|
||||||
"version": "0.4.3",
|
"version": "0.4.5",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "vite",
|
"dev": "vite",
|
||||||
|
|||||||
Reference in New Issue
Block a user