Skip to content

Commit c95d0db

Browse files
committed
Special mode for users from China: uses HF mirrir site if model network error
1 parent 2dc2709 commit c95d0db

3 files changed

Lines changed: 191 additions & 26 deletions

File tree

docs/release_notes_v1.8.7b1.md

Lines changed: 23 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,28 @@ Pre-release for testing. Feedback welcome via [Issues](https://github.com/meizho
66

77
## What's New in beta.1
88

9-
- **#204 — SSL/VPN resilience for HuggingFace downloads** — Users behind corporate proxies or Chinese VPN services (e.g., v2rayN) no longer get blocked by SSL certificate validation errors when models are already cached locally. A single startup-time monkeypatch transparently retries failed HuggingFace Hub downloads from local cache. Covers all download paths: faster-whisper models, NeMo VAD, speech enhancement models, and ensemble subprocess workers.
9+
### Network Resilience for Users in China (#204)
10+
11+
WhisperJAV now handles unstable network conditions gracefully — especially for users in China who connect through VPN/proxy services like v2rayN, Clash, or corporate proxies.
12+
13+
**The problem:** HuggingFace Hub (where AI models are hosted) always contacts its servers to check for updates before loading a model — even when the model is already fully downloaded and cached on your machine. In China, VPN proxy tools often break SSL certificate validation, causing these checks to fail with `CERTIFICATE_VERIFY_FAILED` errors. The result: WhisperJAV refuses to start even though all required models are already on disk.
14+
15+
**The fix:** WhisperJAV now automatically detects SSL/network failures and uses a 3-step fallback strategy. No configuration needed — it works transparently at startup.
16+
17+
**What you'll see (3-step fallback):**
18+
19+
1. **Step 1 — Normal download from huggingface.co:** Works as before. If this succeeds, nothing changes.
20+
2. **Step 2 — Local cache fallback:** If Step 1 fails with a network/SSL error, WhisperJAV checks your local model cache. If the model was downloaded before, it loads from cache and continues normally. You'll see a warning in the log, but processing is not interrupted.
21+
3. **Step 3 — China mirror (hf-mirror.com):** If the model is not in your local cache, WhisperJAV automatically tries downloading from `hf-mirror.com`, the official HuggingFace mirror for China. This works even when your VPN blocks `huggingface.co`. Once downloaded, the model is cached locally for future use.
22+
23+
If all 3 steps fail, WhisperJAV shows a diagnostic summary with:
24+
- The exact model name and download URLs (both huggingface.co and hf-mirror.com)
25+
- Your local cache directory path, so you can download and place files manually
26+
- The `HF_ENDPOINT` environment variable you can set as a permanent workaround
27+
28+
**Coverage:** This protection applies to all AI model downloads — Whisper models (faster-whisper), VAD models (NeMo/Silero), speech enhancement models (ZipEnhancer, BS-RoFormer, ClearVoice), and ensemble mode subprocess workers. Every entry point (CLI, GUI, ensemble workers) is protected.
29+
30+
**For users in China:** In most cases, WhisperJAV will now "just work" — even on first run — because the China mirror fallback handles the initial download automatically. No need to disconnect your VPN.
1031

1132
## Carried from beta.0
1233

@@ -35,7 +56,7 @@ Pre-release for testing. Feedback welcome via [Issues](https://github.com/meizho
3556
## Installation
3657

3758
```bash
38-
pip install "whisperjav @ git+https://github.com/meizhong986/whisperjav.git@v1.8.7-beta.1"
59+
pip install "whisperjav @ git+https://github.com/meizhong986/whisperjav.git@v1.8.7b1"
3960
```
4061

4162
Or for Mac users:

tests/test_model_loader.py

Lines changed: 62 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -235,8 +235,41 @@ def mock_download(*args, **kwargs):
235235
with pytest.raises(ValueError, match="Invalid repo_id"):
236236
patched("bad/repo/format")
237237

238-
def test_ssl_fail_no_cache_raises_original_error(self):
239-
"""When SSL fails and cache miss, the ORIGINAL SSL error is raised."""
238+
def test_ssl_fail_no_cache_tries_mirror(self):
239+
"""When SSL fails and cache miss, should try hf-mirror.com."""
240+
import huggingface_hub
241+
import whisperjav.utils.model_loader as ml
242+
243+
call_log = []
244+
245+
def mock_download(*args, **kwargs):
246+
call_log.append(kwargs.copy())
247+
if kwargs.get("local_files_only"):
248+
raise OSError("model not found in cache")
249+
if kwargs.get("endpoint") == ml._HF_MIRROR_ENDPOINT:
250+
return "/mirror/model/path"
251+
raise OSError(
252+
"urlopen error [SSL: CERTIFICATE_VERIFY_FAILED] cert verify failed"
253+
)
254+
255+
ml._patched = False
256+
with patch.object(huggingface_hub, "snapshot_download", mock_download):
257+
self._apply_patch()
258+
patched = huggingface_hub.snapshot_download
259+
260+
result = patched("Systran/faster-whisper-large-v2")
261+
assert result == "/mirror/model/path"
262+
assert len(call_log) == 3
263+
# Step 1: normal attempt
264+
assert not call_log[0].get("local_files_only")
265+
assert not call_log[0].get("endpoint")
266+
# Step 2: local cache
267+
assert call_log[1]["local_files_only"] is True
268+
# Step 3: mirror
269+
assert call_log[2]["endpoint"] == ml._HF_MIRROR_ENDPOINT
270+
271+
def test_ssl_fail_no_cache_no_mirror_raises_original_error(self):
272+
"""When all 3 steps fail, the ORIGINAL SSL error is raised."""
240273
import huggingface_hub
241274
import whisperjav.utils.model_loader as ml
242275

@@ -247,6 +280,8 @@ def test_ssl_fail_no_cache_raises_original_error(self):
247280
def mock_download(*args, **kwargs):
248281
if kwargs.get("local_files_only"):
249282
raise OSError("model not found in cache")
283+
if kwargs.get("endpoint"):
284+
raise ConnectionError("mirror also unreachable")
250285
raise original_error
251286

252287
ml._patched = False
@@ -322,6 +357,31 @@ def mock_download(*args, **kwargs):
322357
assert len(call_log) == 2
323358
assert call_log[1]["local_files_only"] is True
324359

360+
def test_hf_hub_download_mirror_fallback(self):
361+
"""SSL error + cache miss should try hf-mirror.com for hf_hub_download."""
362+
import huggingface_hub
363+
import whisperjav.utils.model_loader as ml
364+
365+
call_log = []
366+
367+
def mock_download(*args, **kwargs):
368+
call_log.append(kwargs.copy())
369+
if kwargs.get("local_files_only"):
370+
raise OSError("not in cache")
371+
if kwargs.get("endpoint") == ml._HF_MIRROR_ENDPOINT:
372+
return "/mirror/file/path"
373+
raise ssl.SSLError("record layer failure")
374+
375+
ml._patched = False
376+
with patch.object(huggingface_hub, "hf_hub_download", mock_download):
377+
self._apply_patch()
378+
patched = huggingface_hub.hf_hub_download
379+
380+
result = patched("org/repo", filename="model.bin")
381+
assert result == "/mirror/file/path"
382+
assert len(call_log) == 3
383+
assert call_log[2]["endpoint"] == ml._HF_MIRROR_ENDPOINT
384+
325385
def test_hf_hub_download_non_network_error_raises(self):
326386
"""Non-network errors should not trigger fallback for hf_hub_download."""
327387
import huggingface_hub

whisperjav/utils/model_loader.py

Lines changed: 106 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,16 @@
11
"""Resilient HuggingFace Hub downloads with network fallback.
22
3-
Monkeypatches huggingface_hub.snapshot_download() to gracefully handle
4-
network errors (SSL failures, timeouts, proxy issues) by falling back
5-
to locally cached models when available.
3+
Monkeypatches huggingface_hub.snapshot_download() and hf_hub_download()
4+
to gracefully handle network errors (SSL failures, timeouts, proxy issues)
5+
with a 3-step fallback strategy:
6+
7+
1. Normal download from huggingface.co
8+
2. Load from local cache (local_files_only=True)
9+
3. Download from hf-mirror.com (official China mirror)
610
711
This is critical for users behind corporate proxies or Chinese VPN
8-
services (e.g., v2rayN) where SSL certificate validation often fails
9-
even though the model cache is already complete.
12+
services (e.g., v2rayN, Clash) where SSL certificate validation often
13+
fails even though the model cache is already complete.
1014
1115
Architecture: A single monkeypatch applied once at startup protects ALL
1216
code paths that download from HuggingFace — faster-whisper, stable-ts,
@@ -26,6 +30,9 @@
2630

2731
_patched = False
2832

33+
# Official HuggingFace mirror for China — maintained by HuggingFace
34+
_HF_MIRROR_ENDPOINT = "https://hf-mirror.com"
35+
2936
# Error strings that indicate a network/SSL problem (not a model/CUDA problem)
3037
_NETWORK_ERROR_INDICATORS = [
3138
"ssl",
@@ -75,10 +82,23 @@ def _is_network_error(error: Exception) -> bool:
7582
return False
7683

7784

85+
def _get_cache_dir():
86+
"""Get the HuggingFace Hub cache directory path for diagnostics."""
87+
try:
88+
from huggingface_hub import constants
89+
return constants.HF_HUB_CACHE
90+
except Exception:
91+
return "(unknown)"
92+
93+
7894
def _make_resilient_wrapper(original_fn, fn_name):
7995
"""Create a resilient wrapper for a HuggingFace Hub download function.
8096
81-
On SSL/network errors, retries with local_files_only=True.
97+
3-step fallback on SSL/network errors:
98+
1. Try normal download from huggingface.co
99+
2. Try loading from local cache (local_files_only=True)
100+
3. Try downloading from hf-mirror.com (official China mirror)
101+
82102
Works for both snapshot_download and hf_hub_download.
83103
"""
84104

@@ -95,35 +115,97 @@ def _resilient_wrapper(*args, **kwargs):
95115

96116
# Extract identifier for messaging (first positional arg or kwarg)
97117
resource_id = args[0] if args else kwargs.get("repo_id", "unknown")
118+
hf_url = f"https://huggingface.co/{resource_id}"
119+
cache_dir = _get_cache_dir()
98120

99121
logger.warning(
100-
"Network/SSL error while checking '%s' on HuggingFace: %s",
101-
resource_id, e,
122+
"[HF Download] Step 1 FAILED — network/SSL error downloading "
123+
"'%s' from %s",
124+
resource_id, hf_url,
102125
)
103126
logger.warning(
104-
"Attempting to load from local cache..."
127+
"[HF Download] Error: %s: %s",
128+
type(e).__name__, e,
105129
)
106130

131+
# --- Step 2: Try local cache ---
132+
logger.info(
133+
"[HF Download] Step 2 — checking local cache at: %s",
134+
cache_dir,
135+
)
107136
try:
108137
result = original_fn(
109138
*args, **{**kwargs, "local_files_only": True}
110139
)
111140
logger.info(
112-
"Loaded '%s' from local cache. "
141+
"[HF Download] Step 2 OK — loaded '%s' from local cache. "
113142
"Network issues did not affect model loading.",
114143
resource_id,
115144
)
116145
return result
117146
except Exception:
118-
logger.error(
119-
"Model '%s' not found in local cache. "
120-
"A working internet connection is required for first-time "
121-
"model download. Please check your VPN/proxy settings, "
122-
"or try disconnecting your VPN and downloading the model "
123-
"once with a direct connection.",
147+
logger.warning(
148+
"[HF Download] Step 2 FAILED — '%s' not found in local "
149+
"cache.",
124150
resource_id,
125151
)
126-
raise e # Re-raise original network error
152+
153+
# --- Step 3: Try China mirror ---
154+
mirror_url = f"{_HF_MIRROR_ENDPOINT}/{resource_id}"
155+
logger.info(
156+
"[HF Download] Step 3 — trying China mirror: %s",
157+
mirror_url,
158+
)
159+
try:
160+
result = original_fn(
161+
*args,
162+
**{**kwargs, "endpoint": _HF_MIRROR_ENDPOINT},
163+
)
164+
logger.info(
165+
"[HF Download] Step 3 OK — downloaded '%s' from mirror "
166+
"(%s). Model is now cached locally for future use.",
167+
resource_id, _HF_MIRROR_ENDPOINT,
168+
)
169+
return result
170+
except Exception as mirror_err:
171+
logger.error(
172+
"[HF Download] Step 3 FAILED — mirror download also "
173+
"failed: %s: %s",
174+
type(mirror_err).__name__, mirror_err,
175+
)
176+
177+
# --- All steps failed: comprehensive diagnostics ---
178+
logger.error(
179+
"[HF Download] All download methods failed for '%s'. "
180+
"Diagnostic summary:",
181+
resource_id,
182+
)
183+
logger.error(
184+
" Model: %s", resource_id,
185+
)
186+
logger.error(
187+
" Source URL: %s", hf_url,
188+
)
189+
logger.error(
190+
" Mirror URL: %s", mirror_url,
191+
)
192+
logger.error(
193+
" Cache dir: %s", cache_dir,
194+
)
195+
logger.error(
196+
" Error type: %s", type(e).__name__,
197+
)
198+
logger.error(
199+
" To download manually, visit: %s", mirror_url,
200+
)
201+
logger.error(
202+
" Then place the downloaded files in: %s", cache_dir,
203+
)
204+
logger.error(
205+
" Or set environment variable: "
206+
"HF_ENDPOINT=https://hf-mirror.com"
207+
)
208+
raise e # Re-raise original network error
127209

128210
_resilient_wrapper.__name__ = f"_resilient_{fn_name}"
129211
_resilient_wrapper.__qualname__ = f"_resilient_{fn_name}"
@@ -137,12 +219,14 @@ def patch_hf_hub_downloads():
137219
and hf_hub_download (used for individual file downloads like NeMo, LLM
138220
GGUF files, classifiers).
139221
140-
On SSL/connection/timeout errors, automatically retries with
141-
local_files_only=True to use cached files. Provides clear user
142-
messages for each scenario:
222+
On SSL/connection/timeout errors, uses a 3-step fallback with full
223+
diagnostic logging at each step:
143224
144-
1. SSL fail + cache hit -> warning + continues normally
145-
2. SSL fail + no cache -> error with actionable guidance
225+
1. Normal download fails -> log error type and source URL
226+
2. Try local cache -> success: continue; fail: try mirror
227+
3. Try hf-mirror.com (China) -> success: model cached for future use
228+
4. All failed -> comprehensive diagnostic summary with
229+
manual download instructions
146230
"""
147231
global _patched
148232
if _patched:

0 commit comments

Comments
 (0)