openspeechapi 0.2.10__tar.gz → 0.2.11__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {openspeechapi-0.2.10 → openspeechapi-0.2.11}/PKG-INFO +5 -1
- {openspeechapi-0.2.10 → openspeechapi-0.2.11}/openspeechapi/config.py +15 -0
- {openspeechapi-0.2.10 → openspeechapi-0.2.11}/openspeechapi/core/base.py +5 -0
- openspeechapi-0.2.11/openspeechapi/core/model_hub.py +488 -0
- {openspeechapi-0.2.10 → openspeechapi-0.2.11}/openspeechapi/dispatch/dispatcher.py +9 -6
- {openspeechapi-0.2.10 → openspeechapi-0.2.11}/openspeechapi/local_engines/aim_resolver.py +14 -2
- {openspeechapi-0.2.10 → openspeechapi-0.2.11}/openspeechapi/providers/stt/alibaba.py +87 -86
- {openspeechapi-0.2.10 → openspeechapi-0.2.11}/openspeechapi/providers/stt/baidu.py +136 -135
- {openspeechapi-0.2.10 → openspeechapi-0.2.11}/openspeechapi/providers/stt/dolphin_stt.py +11 -11
- {openspeechapi-0.2.10 → openspeechapi-0.2.11}/openspeechapi/providers/stt/elevenlabs.py +1 -0
- {openspeechapi-0.2.10 → openspeechapi-0.2.11}/openspeechapi/providers/stt/faster_whisper.py +212 -211
- {openspeechapi-0.2.10 → openspeechapi-0.2.11}/openspeechapi/providers/stt/fireredasr_stt.py +1 -0
- {openspeechapi-0.2.10 → openspeechapi-0.2.11}/openspeechapi/providers/stt/funasr_stt.py +14 -9
- {openspeechapi-0.2.10 → openspeechapi-0.2.11}/openspeechapi/providers/stt/gemma4.py +97 -18
- {openspeechapi-0.2.10 → openspeechapi-0.2.11}/openspeechapi/providers/stt/kimi_audio_stt.py +9 -12
- {openspeechapi-0.2.10 → openspeechapi-0.2.11}/openspeechapi/providers/stt/mlx_whisper_stt.py +10 -11
- {openspeechapi-0.2.10 → openspeechapi-0.2.11}/openspeechapi/providers/stt/openai.py +94 -93
- {openspeechapi-0.2.10 → openspeechapi-0.2.11}/openspeechapi/providers/stt/paraformer.py +1 -0
- {openspeechapi-0.2.10 → openspeechapi-0.2.11}/openspeechapi/providers/stt/parakeet_mlx_stt.py +11 -11
- {openspeechapi-0.2.10 → openspeechapi-0.2.11}/openspeechapi/providers/stt/phi4_multimodal_stt.py +1 -0
- {openspeechapi-0.2.10 → openspeechapi-0.2.11}/openspeechapi/providers/stt/qwen3_asr.py +12 -11
- {openspeechapi-0.2.10 → openspeechapi-0.2.11}/openspeechapi/providers/stt/qwen3_omni_stt.py +1 -0
- {openspeechapi-0.2.10 → openspeechapi-0.2.11}/openspeechapi/providers/stt/sensevoice.py +17 -16
- {openspeechapi-0.2.10 → openspeechapi-0.2.11}/openspeechapi/providers/stt/tencent.py +213 -212
- {openspeechapi-0.2.10 → openspeechapi-0.2.11}/openspeechapi/providers/stt/volcengine.py +108 -107
- {openspeechapi-0.2.10 → openspeechapi-0.2.11}/openspeechapi/providers/stt/vosk_stt.py +1 -0
- {openspeechapi-0.2.10 → openspeechapi-0.2.11}/openspeechapi/providers/stt/voxtral_stt.py +10 -11
- {openspeechapi-0.2.10 → openspeechapi-0.2.11}/openspeechapi/providers/stt/wenet_stt.py +1 -0
- {openspeechapi-0.2.10 → openspeechapi-0.2.11}/openspeechapi/providers/stt/whisper.py +154 -153
- {openspeechapi-0.2.10 → openspeechapi-0.2.11}/openspeechapi/server/app.py +32 -0
- {openspeechapi-0.2.10 → openspeechapi-0.2.11}/openspeechapi/server/extras_installer.py +22 -2
- {openspeechapi-0.2.10 → openspeechapi-0.2.11}/openspeechapi/server/routes/management.py +8 -4
- {openspeechapi-0.2.10 → openspeechapi-0.2.11}/openspeechapi/server/webui/app.js +3 -1
- {openspeechapi-0.2.10 → openspeechapi-0.2.11}/pyproject.toml +10 -2
- openspeechapi-0.2.10/openspeechapi/core/model_hub.py +0 -257
- {openspeechapi-0.2.10 → openspeechapi-0.2.11}/.gitignore +0 -0
- {openspeechapi-0.2.10 → openspeechapi-0.2.11}/README.md +0 -0
- {openspeechapi-0.2.10 → openspeechapi-0.2.11}/openspeechapi/__init__.py +0 -0
- {openspeechapi-0.2.10 → openspeechapi-0.2.11}/openspeechapi/__main__.py +0 -0
- {openspeechapi-0.2.10 → openspeechapi-0.2.11}/openspeechapi/cli.py +0 -0
- {openspeechapi-0.2.10 → openspeechapi-0.2.11}/openspeechapi/client/__init__.py +0 -0
- {openspeechapi-0.2.10 → openspeechapi-0.2.11}/openspeechapi/client/client.py +0 -0
- {openspeechapi-0.2.10 → openspeechapi-0.2.11}/openspeechapi/core/__init__.py +0 -0
- {openspeechapi-0.2.10 → openspeechapi-0.2.11}/openspeechapi/core/enums.py +0 -0
- {openspeechapi-0.2.10 → openspeechapi-0.2.11}/openspeechapi/core/models.py +0 -0
- {openspeechapi-0.2.10 → openspeechapi-0.2.11}/openspeechapi/core/registry.py +0 -0
- {openspeechapi-0.2.10 → openspeechapi-0.2.11}/openspeechapi/core/settings.py +0 -0
- {openspeechapi-0.2.10 → openspeechapi-0.2.11}/openspeechapi/demo.py +0 -0
- {openspeechapi-0.2.10 → openspeechapi-0.2.11}/openspeechapi/dispatch/__init__.py +0 -0
- {openspeechapi-0.2.10 → openspeechapi-0.2.11}/openspeechapi/dispatch/aim_provision.py +0 -0
- {openspeechapi-0.2.10 → openspeechapi-0.2.11}/openspeechapi/dispatch/context.py +0 -0
- {openspeechapi-0.2.10 → openspeechapi-0.2.11}/openspeechapi/dispatch/executors/__init__.py +0 -0
- {openspeechapi-0.2.10 → openspeechapi-0.2.11}/openspeechapi/dispatch/executors/base.py +0 -0
- {openspeechapi-0.2.10 → openspeechapi-0.2.11}/openspeechapi/dispatch/executors/in_process.py +0 -0
- {openspeechapi-0.2.10 → openspeechapi-0.2.11}/openspeechapi/dispatch/executors/remote.py +0 -0
- {openspeechapi-0.2.10 → openspeechapi-0.2.11}/openspeechapi/dispatch/executors/subprocess_exec.py +0 -0
- {openspeechapi-0.2.10 → openspeechapi-0.2.11}/openspeechapi/dispatch/fanout.py +0 -0
- {openspeechapi-0.2.10 → openspeechapi-0.2.11}/openspeechapi/dispatch/filters.py +0 -0
- {openspeechapi-0.2.10 → openspeechapi-0.2.11}/openspeechapi/dispatch/lifecycle.py +0 -0
- {openspeechapi-0.2.10 → openspeechapi-0.2.11}/openspeechapi/dispatch/watcher.py +0 -0
- {openspeechapi-0.2.10 → openspeechapi-0.2.11}/openspeechapi/engine_catalog.py +0 -0
- {openspeechapi-0.2.10 → openspeechapi-0.2.11}/openspeechapi/engine_registry.yaml +0 -0
- {openspeechapi-0.2.10 → openspeechapi-0.2.11}/openspeechapi/exceptions.py +0 -0
- {openspeechapi-0.2.10 → openspeechapi-0.2.11}/openspeechapi/factory.py +0 -0
- {openspeechapi-0.2.10 → openspeechapi-0.2.11}/openspeechapi/local_engines/__init__.py +0 -0
- {openspeechapi-0.2.10 → openspeechapi-0.2.11}/openspeechapi/local_engines/backends/__init__.py +0 -0
- {openspeechapi-0.2.10 → openspeechapi-0.2.11}/openspeechapi/local_engines/backends/docker_backend.py +0 -0
- {openspeechapi-0.2.10 → openspeechapi-0.2.11}/openspeechapi/local_engines/backends/native_backend.py +0 -0
- {openspeechapi-0.2.10 → openspeechapi-0.2.11}/openspeechapi/local_engines/base.py +0 -0
- {openspeechapi-0.2.10 → openspeechapi-0.2.11}/openspeechapi/local_engines/engines/__init__.py +0 -0
- {openspeechapi-0.2.10 → openspeechapi-0.2.11}/openspeechapi/local_engines/engines/faster_whisper.py +0 -0
- {openspeechapi-0.2.10 → openspeechapi-0.2.11}/openspeechapi/local_engines/engines/fish_speech.py +0 -0
- {openspeechapi-0.2.10 → openspeechapi-0.2.11}/openspeechapi/local_engines/engines/sherpa_onnx.py +0 -0
- {openspeechapi-0.2.10 → openspeechapi-0.2.11}/openspeechapi/local_engines/engines/whisper.py +0 -0
- {openspeechapi-0.2.10 → openspeechapi-0.2.11}/openspeechapi/local_engines/engines/whisperlivekit.py +0 -0
- {openspeechapi-0.2.10 → openspeechapi-0.2.11}/openspeechapi/local_engines/isolated_venv.py +0 -0
- {openspeechapi-0.2.10 → openspeechapi-0.2.11}/openspeechapi/local_engines/manager.py +0 -0
- {openspeechapi-0.2.10 → openspeechapi-0.2.11}/openspeechapi/local_engines/models.py +0 -0
- {openspeechapi-0.2.10 → openspeechapi-0.2.11}/openspeechapi/local_engines/progress.py +0 -0
- {openspeechapi-0.2.10 → openspeechapi-0.2.11}/openspeechapi/local_engines/registry.py +0 -0
- {openspeechapi-0.2.10 → openspeechapi-0.2.11}/openspeechapi/local_engines/task_store.py +0 -0
- {openspeechapi-0.2.10 → openspeechapi-0.2.11}/openspeechapi/local_engines/tasks.py +0 -0
- {openspeechapi-0.2.10 → openspeechapi-0.2.11}/openspeechapi/logging_config.py +0 -0
- {openspeechapi-0.2.10 → openspeechapi-0.2.11}/openspeechapi/observe/__init__.py +0 -0
- {openspeechapi-0.2.10 → openspeechapi-0.2.11}/openspeechapi/observe/base.py +0 -0
- {openspeechapi-0.2.10 → openspeechapi-0.2.11}/openspeechapi/observe/debug.py +0 -0
- {openspeechapi-0.2.10 → openspeechapi-0.2.11}/openspeechapi/observe/latency.py +0 -0
- {openspeechapi-0.2.10 → openspeechapi-0.2.11}/openspeechapi/observe/metrics.py +0 -0
- {openspeechapi-0.2.10 → openspeechapi-0.2.11}/openspeechapi/observe/tracing.py +0 -0
- {openspeechapi-0.2.10 → openspeechapi-0.2.11}/openspeechapi/observe/usage.py +0 -0
- {openspeechapi-0.2.10 → openspeechapi-0.2.11}/openspeechapi/providers/__init__.py +0 -0
- {openspeechapi-0.2.10 → openspeechapi-0.2.11}/openspeechapi/providers/_template.py +0 -0
- {openspeechapi-0.2.10 → openspeechapi-0.2.11}/openspeechapi/providers/stt/__init__.py +0 -0
- {openspeechapi-0.2.10 → openspeechapi-0.2.11}/openspeechapi/providers/stt/_local_audio.py +0 -0
- {openspeechapi-0.2.10 → openspeechapi-0.2.11}/openspeechapi/providers/stt/assemblyai.py +0 -0
- {openspeechapi-0.2.10 → openspeechapi-0.2.11}/openspeechapi/providers/stt/azure_speech.py +0 -0
- {openspeechapi-0.2.10 → openspeechapi-0.2.11}/openspeechapi/providers/stt/canary_qwen_stt.py +0 -0
- {openspeechapi-0.2.10 → openspeechapi-0.2.11}/openspeechapi/providers/stt/deepgram.py +0 -0
- {openspeechapi-0.2.10 → openspeechapi-0.2.11}/openspeechapi/providers/stt/google_cloud.py +0 -0
- {openspeechapi-0.2.10 → openspeechapi-0.2.11}/openspeechapi/providers/stt/iflytek.py +0 -0
- {openspeechapi-0.2.10 → openspeechapi-0.2.11}/openspeechapi/providers/stt/macos_speech.py +0 -0
- {openspeechapi-0.2.10 → openspeechapi-0.2.11}/openspeechapi/providers/stt/mms_languages.json +0 -0
- {openspeechapi-0.2.10 → openspeechapi-0.2.11}/openspeechapi/providers/stt/mms_stt.py +0 -0
- {openspeechapi-0.2.10 → openspeechapi-0.2.11}/openspeechapi/providers/stt/moonshine_stt.py +0 -0
- {openspeechapi-0.2.10 → openspeechapi-0.2.11}/openspeechapi/providers/stt/sherpa_onnx.py +0 -0
- {openspeechapi-0.2.10 → openspeechapi-0.2.11}/openspeechapi/providers/stt/whisperlivekit.py +0 -0
- {openspeechapi-0.2.10 → openspeechapi-0.2.11}/openspeechapi/providers/stt/windows_speech.py +0 -0
- {openspeechapi-0.2.10 → openspeechapi-0.2.11}/openspeechapi/providers/tts/__init__.py +0 -0
- {openspeechapi-0.2.10 → openspeechapi-0.2.11}/openspeechapi/providers/tts/alibaba.py +0 -0
- {openspeechapi-0.2.10 → openspeechapi-0.2.11}/openspeechapi/providers/tts/azure_speech.py +0 -0
- {openspeechapi-0.2.10 → openspeechapi-0.2.11}/openspeechapi/providers/tts/baidu.py +0 -0
- {openspeechapi-0.2.10 → openspeechapi-0.2.11}/openspeechapi/providers/tts/coqui.py +0 -0
- {openspeechapi-0.2.10 → openspeechapi-0.2.11}/openspeechapi/providers/tts/cosyvoice.py +0 -0
- {openspeechapi-0.2.10 → openspeechapi-0.2.11}/openspeechapi/providers/tts/deepgram.py +0 -0
- {openspeechapi-0.2.10 → openspeechapi-0.2.11}/openspeechapi/providers/tts/elevenlabs.py +0 -0
- {openspeechapi-0.2.10 → openspeechapi-0.2.11}/openspeechapi/providers/tts/fish_speech.py +0 -0
- {openspeechapi-0.2.10 → openspeechapi-0.2.11}/openspeechapi/providers/tts/google_cloud.py +0 -0
- {openspeechapi-0.2.10 → openspeechapi-0.2.11}/openspeechapi/providers/tts/iflytek.py +0 -0
- {openspeechapi-0.2.10 → openspeechapi-0.2.11}/openspeechapi/providers/tts/macos_say.py +0 -0
- {openspeechapi-0.2.10 → openspeechapi-0.2.11}/openspeechapi/providers/tts/minimax.py +0 -0
- {openspeechapi-0.2.10 → openspeechapi-0.2.11}/openspeechapi/providers/tts/openai.py +0 -0
- {openspeechapi-0.2.10 → openspeechapi-0.2.11}/openspeechapi/providers/tts/piper.py +0 -0
- {openspeechapi-0.2.10 → openspeechapi-0.2.11}/openspeechapi/providers/tts/tencent.py +0 -0
- {openspeechapi-0.2.10 → openspeechapi-0.2.11}/openspeechapi/providers/tts/volcengine.py +0 -0
- {openspeechapi-0.2.10 → openspeechapi-0.2.11}/openspeechapi/providers/tts/windows_sapi.py +0 -0
- {openspeechapi-0.2.10 → openspeechapi-0.2.11}/openspeechapi/server/__init__.py +0 -0
- {openspeechapi-0.2.10 → openspeechapi-0.2.11}/openspeechapi/server/auth.py +0 -0
- {openspeechapi-0.2.10 → openspeechapi-0.2.11}/openspeechapi/server/middleware.py +0 -0
- {openspeechapi-0.2.10 → openspeechapi-0.2.11}/openspeechapi/server/native_installer.py +0 -0
- {openspeechapi-0.2.10 → openspeechapi-0.2.11}/openspeechapi/server/routes/__init__.py +0 -0
- {openspeechapi-0.2.10 → openspeechapi-0.2.11}/openspeechapi/server/routes/stt.py +0 -0
- {openspeechapi-0.2.10 → openspeechapi-0.2.11}/openspeechapi/server/routes/tts.py +0 -0
- {openspeechapi-0.2.10 → openspeechapi-0.2.11}/openspeechapi/server/routes/webui.py +0 -0
- {openspeechapi-0.2.10 → openspeechapi-0.2.11}/openspeechapi/server/webui/index.html +0 -0
- {openspeechapi-0.2.10 → openspeechapi-0.2.11}/openspeechapi/server/webui/styles.css +0 -0
- {openspeechapi-0.2.10 → openspeechapi-0.2.11}/openspeechapi/server/ws/__init__.py +0 -0
- {openspeechapi-0.2.10 → openspeechapi-0.2.11}/openspeechapi/server/ws/stt_stream.py +0 -0
- {openspeechapi-0.2.10 → openspeechapi-0.2.11}/openspeechapi/server/ws/tts_stream.py +0 -0
- {openspeechapi-0.2.10 → openspeechapi-0.2.11}/openspeechapi/telemetry/__init__.py +0 -0
- {openspeechapi-0.2.10 → openspeechapi-0.2.11}/openspeechapi/telemetry/perf.py +0 -0
- {openspeechapi-0.2.10 → openspeechapi-0.2.11}/openspeechapi/utils/__init__.py +0 -0
- {openspeechapi-0.2.10 → openspeechapi-0.2.11}/openspeechapi/utils/audio_converter.py +0 -0
- {openspeechapi-0.2.10 → openspeechapi-0.2.11}/openspeechapi/utils/audio_playback.py +0 -0
- {openspeechapi-0.2.10 → openspeechapi-0.2.11}/openspeechapi/vendor_registry.yaml +0 -0
- {openspeechapi-0.2.10 → openspeechapi-0.2.11}/providers.example.yaml +0 -0
- {openspeechapi-0.2.10 → openspeechapi-0.2.11}/scripts/aim_adopt.py +0 -0
- {openspeechapi-0.2.10 → openspeechapi-0.2.11}/scripts/aim_consumers.py +0 -0
- {openspeechapi-0.2.10 → openspeechapi-0.2.11}/scripts/engines/cloud/install.sh +0 -0
- {openspeechapi-0.2.10 → openspeechapi-0.2.11}/scripts/engines/faster-whisper/native/install.sh +0 -0
- {openspeechapi-0.2.10 → openspeechapi-0.2.11}/scripts/engines/fish-speech/native/install.sh +0 -0
- {openspeechapi-0.2.10 → openspeechapi-0.2.11}/scripts/engines/macos-stt/_bundle.sh +0 -0
- {openspeechapi-0.2.10 → openspeechapi-0.2.11}/scripts/engines/macos-stt/install.sh +0 -0
- {openspeechapi-0.2.10 → openspeechapi-0.2.11}/scripts/engines/macos-stt/macos_stt.swift +0 -0
- {openspeechapi-0.2.10 → openspeechapi-0.2.11}/scripts/engines/macos-stt/request_auth.swift +0 -0
- {openspeechapi-0.2.10 → openspeechapi-0.2.11}/scripts/engines/sherpa-onnx/native/install.sh +0 -0
- {openspeechapi-0.2.10 → openspeechapi-0.2.11}/scripts/engines/sherpa-onnx/native/run_streaming_server.py +0 -0
- {openspeechapi-0.2.10 → openspeechapi-0.2.11}/scripts/engines/whisper/native/install.sh +0 -0
- {openspeechapi-0.2.10 → openspeechapi-0.2.11}/scripts/engines/whisperlivekit/native/install.sh +0 -0
- {openspeechapi-0.2.10 → openspeechapi-0.2.11}/scripts/gen_mms_languages.py +0 -0
- {openspeechapi-0.2.10 → openspeechapi-0.2.11}/scripts/preload_stt_model.py +0 -0
- {openspeechapi-0.2.10 → openspeechapi-0.2.11}/scripts/release.sh +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: openspeechapi
|
|
3
|
-
Version: 0.2.
|
|
3
|
+
Version: 0.2.11
|
|
4
4
|
Summary: Unified speech interface for STT/TTS providers
|
|
5
5
|
Requires-Python: >=3.11
|
|
6
6
|
Requires-Dist: httpx>=0.27
|
|
@@ -70,7 +70,11 @@ Requires-Dist: funasr>=1.1.0; extra == 'funasr-stt'
|
|
|
70
70
|
Requires-Dist: torch; extra == 'funasr-stt'
|
|
71
71
|
Requires-Dist: torchaudio; extra == 'funasr-stt'
|
|
72
72
|
Provides-Extra: gemma4-stt
|
|
73
|
+
Requires-Dist: accelerate; (sys_platform != 'darwin') and extra == 'gemma4-stt'
|
|
74
|
+
Requires-Dist: librosa; (sys_platform != 'darwin') and extra == 'gemma4-stt'
|
|
73
75
|
Requires-Dist: mlx-vlm<0.6.2,>=0.6.1; (sys_platform == 'darwin') and extra == 'gemma4-stt'
|
|
76
|
+
Requires-Dist: torch; (sys_platform != 'darwin') and extra == 'gemma4-stt'
|
|
77
|
+
Requires-Dist: transformers; (sys_platform != 'darwin') and extra == 'gemma4-stt'
|
|
74
78
|
Provides-Extra: google
|
|
75
79
|
Provides-Extra: google-stt
|
|
76
80
|
Provides-Extra: google-tts
|
|
@@ -99,6 +99,13 @@ class ServerConfig:
|
|
|
99
99
|
# Root dir for non-aim model downloads. "" → ~/.openspeechapi/models. When aim
|
|
100
100
|
# is installed it manages its own ~/AI store and this is unused.
|
|
101
101
|
model_storage_root: str = ""
|
|
102
|
+
# Robust download: HF mirror endpoints tried (in order) when huggingface.co is
|
|
103
|
+
# unreachable — model_hub auto-routes HF downloads here so HF-blocked boxes work
|
|
104
|
+
# without per-box hf_endpoint config. Exports OSA_HF_MIRRORS for workers.
|
|
105
|
+
hf_mirrors: list[str] = field(default_factory=lambda: ["https://hf-mirror.com"])
|
|
106
|
+
# Per-source download attempts (huggingface_hub resumes between attempts) before
|
|
107
|
+
# switching source. Exports OSA_MODEL_DOWNLOAD_RETRIES for workers.
|
|
108
|
+
model_download_retries: int = 3
|
|
102
109
|
|
|
103
110
|
|
|
104
111
|
@dataclass
|
|
@@ -220,6 +227,14 @@ def load_config(path: Path) -> OpenSpeechConfig:
|
|
|
220
227
|
server_cfg.hf_endpoint = srv.get("hf_endpoint", server_cfg.hf_endpoint)
|
|
221
228
|
server_cfg.prefer_modelscope = bool(srv.get("prefer_modelscope", server_cfg.prefer_modelscope))
|
|
222
229
|
server_cfg.model_storage_root = srv.get("model_storage_root", server_cfg.model_storage_root)
|
|
230
|
+
mirrors = srv.get("hf_mirrors")
|
|
231
|
+
if isinstance(mirrors, list) and mirrors:
|
|
232
|
+
server_cfg.hf_mirrors = [str(m).strip() for m in mirrors if str(m).strip()]
|
|
233
|
+
try:
|
|
234
|
+
server_cfg.model_download_retries = max(
|
|
235
|
+
1, int(srv.get("model_download_retries", server_cfg.model_download_retries)))
|
|
236
|
+
except (TypeError, ValueError):
|
|
237
|
+
pass
|
|
223
238
|
# explicit default_model_source wins; else legacy prefer_modelscope → ms
|
|
224
239
|
src = str(srv.get("default_model_source", "")).strip().lower()
|
|
225
240
|
if src in ("hf", "ms"):
|
|
@@ -68,6 +68,11 @@ class SpeechProvider(ABC):
|
|
|
68
68
|
settings_cls: type[BaseSettings] = BaseSettings # Override in subclass
|
|
69
69
|
capabilities: set[Capability]
|
|
70
70
|
field_options: dict[str, list] = {} # Dropdown choices per setting key (override in subclass)
|
|
71
|
+
# Languages this engine supports, for the Engines/Catalog language-filter tags ONLY
|
|
72
|
+
# (canonical ISO codes, e.g. ["zh","en","ja"]). Use this for multilingual engines that
|
|
73
|
+
# don't enumerate languages in field_options.language (gemma4/phi4/qwen3-omni/whisper…).
|
|
74
|
+
# The backend merges it with field_options.language when deriving the `languages` shown.
|
|
75
|
+
supported_languages: list[str] = []
|
|
71
76
|
# UI category: "cloud" | "local" | "native". Leave None to auto-derive from
|
|
72
77
|
# ``execution_mode`` (remote→cloud, subprocess/local/in_process→local) and
|
|
73
78
|
# the OS-native provider set. Only set explicitly to override that default.
|
|
@@ -0,0 +1,488 @@
|
|
|
1
|
+
"""Resolve a model reference (local path or repo id) for transformers/mlx loaders,
|
|
2
|
+
with optional ModelScope acceleration (hub=ms) for HF-blocked / slow networks.
|
|
3
|
+
|
|
4
|
+
On the China deploy box ModelScope is ~9× faster than hf-mirror and sidesteps HF's
|
|
5
|
+
Xet CAS (unreachable there). See docs/architecture/modelscope-hub.md.
|
|
6
|
+
"""
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
import os
|
|
10
|
+
import time
|
|
11
|
+
from collections.abc import Mapping
|
|
12
|
+
from typing import Any
|
|
13
|
+
|
|
14
|
+
from openspeechapi.core.base import AimModelSpec, aim_local_path_or, aim_model_id
|
|
15
|
+
from openspeechapi.logging_config import logger
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def _prefer_ms_env() -> bool:
|
|
19
|
+
"""True when the server-level prefer_modelscope switch is on. ``create_app``
|
|
20
|
+
exports ``OSA_PREFER_MODELSCOPE=1`` from ``server.prefer_modelscope``; subprocess
|
|
21
|
+
workers inherit it (same mechanism as ``HF_ENDPOINT``)."""
|
|
22
|
+
return os.environ.get("OSA_PREFER_MODELSCOPE", "").strip().lower() in ("1", "true", "yes", "on")
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
# ── network reachability + retry policy (robust model download) ──────────────────
|
|
26
|
+
# Downloads fail on HF-blocked / flaky nets in two ways diagnosed in the field:
|
|
27
|
+
# (1) HF unreachable → loaders that hit huggingface.co hang then "model not found";
|
|
28
|
+
# (2) a reachable source's large file (e.g. SenseVoice's 893MB model.pt) breaks
|
|
29
|
+
# mid-transfer and the SDK doesn't resume → total failure.
|
|
30
|
+
# The helpers below add: a cached reachability probe, an auto HF-mirror, and retrying
|
|
31
|
+
# snapshot downloads with cross-source fallback. Used by resolve_model_source /
|
|
32
|
+
# resolve_hub_model so every provider that downloads through model_hub benefits.
|
|
33
|
+
|
|
34
|
+
_DEFAULT_HF_MIRRORS = ("https://hf-mirror.com",)
|
|
35
|
+
_REACH_TTL_S = 300.0 # cache reachability for 5 min (avoid re-probing per load)
|
|
36
|
+
_REACH_PROBE_TIMEOUT_S = 4.0
|
|
37
|
+
_reach_cache: dict[str, tuple[bool, float]] = {}
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def _hf_mirrors() -> list[str]:
|
|
41
|
+
"""HF mirror endpoints to try when huggingface.co is unreachable. Override via
|
|
42
|
+
OSA_HF_MIRRORS (comma-separated); exported from server.hf_mirrors."""
|
|
43
|
+
raw = os.environ.get("OSA_HF_MIRRORS", "").strip()
|
|
44
|
+
if raw:
|
|
45
|
+
return [m.strip() for m in raw.split(",") if m.strip()]
|
|
46
|
+
return list(_DEFAULT_HF_MIRRORS)
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def _download_retries() -> int:
|
|
50
|
+
"""Per-source download attempts (resume between attempts). Override via
|
|
51
|
+
OSA_MODEL_DOWNLOAD_RETRIES; exported from server.model_download_retries."""
|
|
52
|
+
try:
|
|
53
|
+
return max(1, int(os.environ.get("OSA_MODEL_DOWNLOAD_RETRIES", "3")))
|
|
54
|
+
except ValueError:
|
|
55
|
+
return 3
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def _probe(url: str, timeout: float) -> bool:
|
|
59
|
+
"""True if `url` answers (any non-5xx) within `timeout`s. httpx is a core dep;
|
|
60
|
+
import lazily so local-path-only resolves pay nothing."""
|
|
61
|
+
try:
|
|
62
|
+
import httpx
|
|
63
|
+
return httpx.get(url, timeout=timeout, follow_redirects=True).status_code < 500
|
|
64
|
+
except Exception:
|
|
65
|
+
return False
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
def _reachable(url: str, *, ttl: float = _REACH_TTL_S,
|
|
69
|
+
timeout: float = _REACH_PROBE_TIMEOUT_S) -> bool:
|
|
70
|
+
"""Cached reachability for `url` (TTL-bounded so flapping nets re-probe)."""
|
|
71
|
+
now = time.monotonic()
|
|
72
|
+
hit = _reach_cache.get(url)
|
|
73
|
+
if hit is not None and (now - hit[1]) < ttl:
|
|
74
|
+
return hit[0]
|
|
75
|
+
ok = _probe(url, timeout)
|
|
76
|
+
_reach_cache[url] = (ok, now)
|
|
77
|
+
return ok
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
def reachable_sources() -> dict[str, bool]:
|
|
81
|
+
"""Which model sources are reachable right now (cached): real HF, any HF mirror,
|
|
82
|
+
ModelScope. Drives reachability-aware source ordering."""
|
|
83
|
+
return {
|
|
84
|
+
"hf": _reachable("https://huggingface.co"),
|
|
85
|
+
"hf_mirror": any(_reachable(m) for m in _hf_mirrors()),
|
|
86
|
+
"ms": _reachable("https://www.modelscope.cn"),
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
def ensure_reachable_download_env() -> None:
|
|
91
|
+
"""Point HF downloads at a reachable endpoint so a loader's OWN sub-downloads
|
|
92
|
+
(e.g. funasr's VAD model) also succeed on HF-blocked nets — not just the main
|
|
93
|
+
weights we materialise ourselves.
|
|
94
|
+
|
|
95
|
+
Respects an existing HF_ENDPOINT (operator/config override). Otherwise, when
|
|
96
|
+
huggingface.co is unreachable but a known mirror is up, exports the mirror and
|
|
97
|
+
disables Xet (mirrors don't proxy HF's Xet CAS → classic LFS). Idempotent."""
|
|
98
|
+
if os.environ.get("HF_ENDPOINT", "").strip():
|
|
99
|
+
return
|
|
100
|
+
if _reachable("https://huggingface.co"):
|
|
101
|
+
return
|
|
102
|
+
for m in _hf_mirrors():
|
|
103
|
+
if _reachable(m):
|
|
104
|
+
os.environ["HF_ENDPOINT"] = m
|
|
105
|
+
os.environ.setdefault("HF_HUB_DISABLE_XET", "1")
|
|
106
|
+
logger.info("huggingface.co unreachable → auto-routing HF downloads via "
|
|
107
|
+
"mirror {} (Xet disabled)", m)
|
|
108
|
+
return
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
def effective_source(hub: str) -> str:
|
|
112
|
+
"""Effective download source for a model: 'hf' or 'ms'.
|
|
113
|
+
|
|
114
|
+
- explicit engine hub 'ms'/'hf' wins
|
|
115
|
+
- ``'auto'`` → reachability-aware: the operator default (OSA_DEFAULT_MODEL_SOURCE) when
|
|
116
|
+
its source is reachable; else ModelScope if reachable (funasr-native, works on
|
|
117
|
+
HF-blocked nets); else HuggingFace (direct or via auto-mirror). This only decides a
|
|
118
|
+
*fresh* download's source — _materialise reuses any already-downloaded copy first.
|
|
119
|
+
- hub '' → global OSA_DEFAULT_MODEL_SOURCE (default 'hf')
|
|
120
|
+
- back-compat: OSA_PREFER_MODELSCOPE=1 with no explicit default → 'ms'
|
|
121
|
+
"""
|
|
122
|
+
h = (hub or "").strip().lower()
|
|
123
|
+
if h in ("hf", "ms"):
|
|
124
|
+
return h
|
|
125
|
+
glob = os.environ.get("OSA_DEFAULT_MODEL_SOURCE", "").strip().lower()
|
|
126
|
+
if h == "auto":
|
|
127
|
+
reach = reachable_sources()
|
|
128
|
+
if glob == "hf" and (reach["hf"] or reach["hf_mirror"]):
|
|
129
|
+
return "hf"
|
|
130
|
+
if glob == "ms" and reach["ms"]:
|
|
131
|
+
return "ms"
|
|
132
|
+
if reach["ms"]:
|
|
133
|
+
return "ms" # ModelScope reachable → prefer it (HF-block-proof)
|
|
134
|
+
return "hf" # HF direct, or auto-mirror via ensure_reachable_download_env
|
|
135
|
+
if glob in ("hf", "ms"):
|
|
136
|
+
return glob
|
|
137
|
+
if _prefer_ms_env():
|
|
138
|
+
return "ms"
|
|
139
|
+
return "hf"
|
|
140
|
+
|
|
141
|
+
|
|
142
|
+
def default_storage_root() -> str:
|
|
143
|
+
"""Root dir for non-aim downloads. OSA_MODEL_ROOT if set, else
|
|
144
|
+
~/.openspeechapi/models. (When aim manages models this is unused.)"""
|
|
145
|
+
root = os.environ.get("OSA_MODEL_ROOT", "").strip()
|
|
146
|
+
if root:
|
|
147
|
+
return os.path.expanduser(root)
|
|
148
|
+
return os.path.expanduser("~/.openspeechapi/models")
|
|
149
|
+
|
|
150
|
+
|
|
151
|
+
def _ensure_symlink(link: str, target: str) -> str:
|
|
152
|
+
"""Make `link` point at `target` (for loaders that require a fixed path).
|
|
153
|
+
Idempotent: repoints a stale symlink; NEVER clobbers a real file/dir
|
|
154
|
+
(logs and returns it as-is — see the safety rule). Returns the path to use."""
|
|
155
|
+
link = os.path.expanduser(link)
|
|
156
|
+
target = os.path.expanduser(target)
|
|
157
|
+
if os.path.islink(link):
|
|
158
|
+
if os.path.realpath(link) == os.path.realpath(target):
|
|
159
|
+
return link
|
|
160
|
+
os.unlink(link) # stale symlink → repoint
|
|
161
|
+
elif os.path.exists(link):
|
|
162
|
+
logger.warning("symlink: {} is a real path (not a symlink); using as-is, "
|
|
163
|
+
"not linking to {}", link, target)
|
|
164
|
+
return link
|
|
165
|
+
parent = os.path.dirname(link)
|
|
166
|
+
if parent:
|
|
167
|
+
os.makedirs(parent, exist_ok=True)
|
|
168
|
+
os.symlink(target, link)
|
|
169
|
+
return link
|
|
170
|
+
|
|
171
|
+
|
|
172
|
+
def _maybe_symlink(fixed_path: str, resolved: str) -> str:
|
|
173
|
+
"""If a loader requires a fixed path, symlink it to the resolved dir; else
|
|
174
|
+
return the resolved dir unchanged."""
|
|
175
|
+
return _ensure_symlink(fixed_path, resolved) if fixed_path else resolved
|
|
176
|
+
|
|
177
|
+
|
|
178
|
+
def _aim_ensure(aim_spec: Any) -> str | None:
|
|
179
|
+
"""Thin seam over aim_ensure_model (kept separate so tests can monkeypatch)."""
|
|
180
|
+
if aim_spec is None:
|
|
181
|
+
return None
|
|
182
|
+
try:
|
|
183
|
+
from openspeechapi.local_engines.aim_resolver import aim_ensure_model
|
|
184
|
+
return aim_ensure_model(aim_spec)
|
|
185
|
+
except Exception as exc: # noqa: BLE001 - aim missing/broken → fall through
|
|
186
|
+
logger.warning("aim resolve failed ({}); falling back to source download", exc)
|
|
187
|
+
return None
|
|
188
|
+
|
|
189
|
+
|
|
190
|
+
def _ms_snapshot(repo: str, *, retries: int = 1) -> str | None:
|
|
191
|
+
"""modelscope.snapshot_download(repo) into the storage root, retried; None on
|
|
192
|
+
failure. ModelScope's SDK doesn't resume reliably, so a broken large file restarts
|
|
193
|
+
— the cross-source fallback in _materialise (→ hf-mirror) is the real safety net."""
|
|
194
|
+
try:
|
|
195
|
+
from modelscope import snapshot_download
|
|
196
|
+
except ImportError:
|
|
197
|
+
return None
|
|
198
|
+
cache = os.path.join(default_storage_root(), "modelscope")
|
|
199
|
+
os.makedirs(cache, exist_ok=True)
|
|
200
|
+
last: Exception | None = None
|
|
201
|
+
for attempt in range(1, retries + 1):
|
|
202
|
+
try:
|
|
203
|
+
return snapshot_download(repo, cache_dir=cache)
|
|
204
|
+
except Exception as exc: # noqa: BLE001 - no ms mirror / network
|
|
205
|
+
last = exc
|
|
206
|
+
logger.warning("ModelScope snapshot {} attempt {}/{} failed ({})",
|
|
207
|
+
repo, attempt, retries, exc)
|
|
208
|
+
logger.warning("ModelScope snapshot failed for {} after {} attempt(s) ({})",
|
|
209
|
+
repo, retries, last)
|
|
210
|
+
return None
|
|
211
|
+
|
|
212
|
+
|
|
213
|
+
def _hf_snapshot(repo: str, *, endpoint: str | None = None, retries: int = 1) -> str | None:
|
|
214
|
+
"""huggingface_hub.snapshot_download(repo) into the storage root, retried; None on
|
|
215
|
+
failure. huggingface_hub resumes partially-downloaded files between attempts, so
|
|
216
|
+
retrying recovers from broken large transfers (the SenseVoice model.pt case).
|
|
217
|
+
|
|
218
|
+
``endpoint`` overrides HF_ENDPOINT for this call (e.g. an auto-selected mirror);
|
|
219
|
+
a mirror endpoint also disables Xet (mirrors don't proxy HF's Xet CAS). Runs in the
|
|
220
|
+
provider's worker (one model at a time) so the env save/restore can't race."""
|
|
221
|
+
try:
|
|
222
|
+
from huggingface_hub import snapshot_download
|
|
223
|
+
except ImportError:
|
|
224
|
+
return None
|
|
225
|
+
cache = os.path.join(default_storage_root(), "huggingface")
|
|
226
|
+
os.makedirs(cache, exist_ok=True)
|
|
227
|
+
saved = {k: os.environ.get(k) for k in ("HF_ENDPOINT", "HF_HUB_DISABLE_XET")}
|
|
228
|
+
if endpoint:
|
|
229
|
+
os.environ["HF_ENDPOINT"] = endpoint
|
|
230
|
+
if "huggingface.co" not in endpoint:
|
|
231
|
+
os.environ["HF_HUB_DISABLE_XET"] = "1"
|
|
232
|
+
last: Exception | None = None
|
|
233
|
+
try:
|
|
234
|
+
for attempt in range(1, retries + 1):
|
|
235
|
+
try:
|
|
236
|
+
return snapshot_download(repo, cache_dir=cache)
|
|
237
|
+
except Exception as exc: # noqa: BLE001 - network/not found → caller falls back
|
|
238
|
+
last = exc
|
|
239
|
+
logger.warning("HF snapshot {} attempt {}/{} failed ({})",
|
|
240
|
+
repo, attempt, retries, exc)
|
|
241
|
+
logger.warning("HF snapshot failed for {} after {} attempt(s) ({})",
|
|
242
|
+
repo, retries, last)
|
|
243
|
+
return None
|
|
244
|
+
finally:
|
|
245
|
+
for k, v in saved.items():
|
|
246
|
+
if v is None:
|
|
247
|
+
os.environ.pop(k, None)
|
|
248
|
+
else:
|
|
249
|
+
os.environ[k] = v
|
|
250
|
+
|
|
251
|
+
|
|
252
|
+
def _download_archive(url: str, model_id: str) -> str | None:
|
|
253
|
+
"""Download an archive (zip/tar) from a custom URL into <root>/url/<id> and
|
|
254
|
+
unpack it; return the dir. None on failure. (Engines without an HF/ms source.)"""
|
|
255
|
+
import shutil
|
|
256
|
+
import tempfile
|
|
257
|
+
import urllib.request
|
|
258
|
+
dest = os.path.join(default_storage_root(), "url", model_id)
|
|
259
|
+
marker = os.path.join(dest, ".download_complete")
|
|
260
|
+
if os.path.isfile(marker):
|
|
261
|
+
return dest
|
|
262
|
+
try:
|
|
263
|
+
os.makedirs(dest, exist_ok=True)
|
|
264
|
+
with tempfile.NamedTemporaryFile(delete=False) as tf:
|
|
265
|
+
with urllib.request.urlopen(url) as resp: # noqa: S310 - operator-configured URL
|
|
266
|
+
shutil.copyfileobj(resp, tf)
|
|
267
|
+
tmp = tf.name
|
|
268
|
+
try:
|
|
269
|
+
shutil.unpack_archive(tmp, dest)
|
|
270
|
+
finally:
|
|
271
|
+
os.unlink(tmp)
|
|
272
|
+
open(marker, "w").close() # noqa: WPS515 - sentinel written only on success
|
|
273
|
+
return dest
|
|
274
|
+
except Exception as exc: # noqa: BLE001
|
|
275
|
+
logger.warning("custom-url download failed for {} ({})", url, exc)
|
|
276
|
+
return None
|
|
277
|
+
|
|
278
|
+
|
|
279
|
+
def _download_candidates(
|
|
280
|
+
repo: str, hub: str, ms_repo_map: Mapping[str, str] | None,
|
|
281
|
+
) -> list[tuple[str, str, str | None]]:
|
|
282
|
+
"""Ordered ``(kind, repo_id, endpoint)`` download attempts for ``repo``, by hub
|
|
283
|
+
preference and current reachability.
|
|
284
|
+
|
|
285
|
+
- ms candidate only when an ms repo id is known (explicit map, or hub=ms so the
|
|
286
|
+
configured id *is* the ms id) AND ModelScope is reachable — never guess an HF org
|
|
287
|
+
onto ModelScope (the "FunAudioLLM/… not exists on ms" failure mode).
|
|
288
|
+
- hf candidate endpoint: operator HF_ENDPOINT > real HF (if up) > reachable mirror.
|
|
289
|
+
"""
|
|
290
|
+
reach = reachable_sources()
|
|
291
|
+
ms_map = ms_repo_map or {}
|
|
292
|
+
pref = effective_source(hub)
|
|
293
|
+
explicit_endpoint = os.environ.get("HF_ENDPOINT", "").strip() or None
|
|
294
|
+
|
|
295
|
+
def hf_cand() -> tuple[str, str, str | None] | None:
|
|
296
|
+
if explicit_endpoint:
|
|
297
|
+
return ("hf", repo, explicit_endpoint)
|
|
298
|
+
if reach["hf"]:
|
|
299
|
+
return ("hf", repo, None)
|
|
300
|
+
for m in _hf_mirrors():
|
|
301
|
+
if _reachable(m):
|
|
302
|
+
return ("hf", repo, m)
|
|
303
|
+
return None
|
|
304
|
+
|
|
305
|
+
def ms_cand() -> tuple[str, str, str | None] | None:
|
|
306
|
+
rid = ms_map.get(repo) or (repo if (hub or "").strip().lower() == "ms" else None)
|
|
307
|
+
return ("ms", rid, None) if (rid and reach["ms"]) else None
|
|
308
|
+
|
|
309
|
+
ordered = [ms_cand(), hf_cand()] if pref == "ms" else [hf_cand(), ms_cand()]
|
|
310
|
+
return [c for c in ordered if c is not None]
|
|
311
|
+
|
|
312
|
+
|
|
313
|
+
def _hf_local(repo: str) -> str | None:
|
|
314
|
+
"""Path to an already-downloaded HF snapshot of ``repo`` (no network), or None.
|
|
315
|
+
Checks both the default HF cache (where funasr/transformers download) and our storage
|
|
316
|
+
root, so an existing copy is reused no matter which path originally fetched it."""
|
|
317
|
+
try:
|
|
318
|
+
from huggingface_hub import snapshot_download
|
|
319
|
+
except ImportError:
|
|
320
|
+
return None
|
|
321
|
+
for cache_dir in (None, os.path.join(default_storage_root(), "huggingface")):
|
|
322
|
+
try:
|
|
323
|
+
return snapshot_download(repo, cache_dir=cache_dir, local_files_only=True)
|
|
324
|
+
except Exception: # noqa: BLE001 - not in this cache → try the next / give up
|
|
325
|
+
continue
|
|
326
|
+
return None
|
|
327
|
+
|
|
328
|
+
|
|
329
|
+
def _ms_local(repo: str) -> str | None:
|
|
330
|
+
"""Path to an already-downloaded ModelScope snapshot of ``repo`` (no network), or None.
|
|
331
|
+
Checks our storage root and the default modelscope cache."""
|
|
332
|
+
for base in (os.path.join(default_storage_root(), "modelscope"),
|
|
333
|
+
os.path.expanduser("~/.cache/modelscope/hub")):
|
|
334
|
+
d = os.path.join(base, *repo.split("/"))
|
|
335
|
+
if os.path.isdir(d) and os.listdir(d):
|
|
336
|
+
return d
|
|
337
|
+
return None
|
|
338
|
+
|
|
339
|
+
|
|
340
|
+
def _local_hit(kind: str, rid: str) -> str | None:
|
|
341
|
+
"""An already-downloaded local copy for this candidate (no network), or None."""
|
|
342
|
+
return _ms_local(rid) if kind == "ms" else _hf_local(rid)
|
|
343
|
+
|
|
344
|
+
|
|
345
|
+
def _materialise(repo: str, hub: str, ms_repo_map: Mapping[str, str] | None) -> str | None:
|
|
346
|
+
"""Resolve ``repo`` to a local dir. **Existing copy first**: reuse an already-downloaded
|
|
347
|
+
copy from ANY candidate platform (local, no network, no re-download) before fetching —
|
|
348
|
+
so a model already present via one hub is NOT re-downloaded just because the other hub
|
|
349
|
+
is preferred (e.g. ms-preferred but the hf copy already exists). Only when nothing is
|
|
350
|
+
local does it download from the preferred reachable source, switching sources on
|
|
351
|
+
failure. None if all candidates fail."""
|
|
352
|
+
cands = _download_candidates(repo, hub, ms_repo_map)
|
|
353
|
+
# Pass 1: reuse any already-downloaded copy (any candidate platform).
|
|
354
|
+
for kind, rid, _endpoint in cands:
|
|
355
|
+
hit = _local_hit(kind, rid)
|
|
356
|
+
if hit:
|
|
357
|
+
logger.info("model download: reusing local {} copy of {} ({})", kind, rid, hit)
|
|
358
|
+
return hit
|
|
359
|
+
# Pass 2: nothing local → download from the preferred reachable source.
|
|
360
|
+
retries = _download_retries()
|
|
361
|
+
for kind, rid, endpoint in cands:
|
|
362
|
+
logger.info("model download: {} ← {} source {} (endpoint={})",
|
|
363
|
+
repo, kind, rid, endpoint or "default")
|
|
364
|
+
got = (_ms_snapshot(rid, retries=retries) if kind == "ms"
|
|
365
|
+
else _hf_snapshot(rid, endpoint=endpoint, retries=retries))
|
|
366
|
+
if got:
|
|
367
|
+
logger.info("model download: {} ready at {} (via {})", repo, got, kind)
|
|
368
|
+
return got
|
|
369
|
+
logger.warning("model download: {} source {} failed; switching source", kind, rid)
|
|
370
|
+
return None
|
|
371
|
+
|
|
372
|
+
|
|
373
|
+
def local_model_path(repo: str, ms_repo_map: Mapping[str, str] | None = None) -> str | None:
|
|
374
|
+
"""An already-downloaded local copy of ``repo`` from EITHER platform (HF cache or
|
|
375
|
+
ModelScope cache), or None. Lets a caller skip a re-download when a copy already exists
|
|
376
|
+
under the other hub's id/cache — e.g. so `hub=auto` preferring ModelScope won't refetch
|
|
377
|
+
a model that's already present from a prior HuggingFace download."""
|
|
378
|
+
ms_repo = (ms_repo_map or {}).get(repo, repo)
|
|
379
|
+
return _hf_local(repo) or _ms_local(ms_repo)
|
|
380
|
+
|
|
381
|
+
|
|
382
|
+
def build_aim_spec(model: str, hub: str, ms_repo_map: Mapping[str, str] | None = None,
|
|
383
|
+
category: str = "asr/model") -> AimModelSpec | None:
|
|
384
|
+
"""Shared ``aim_model_spec`` body for HF⇄ModelScope-mirrored models.
|
|
385
|
+
|
|
386
|
+
Returns None when a copy is already downloaded on EITHER platform — so the worker's
|
|
387
|
+
existing-copy-first resolve reuses it instead of aim re-downloading from the (possibly
|
|
388
|
+
different) preferred hub. Otherwise resolves ``hub`` ('auto' → a reachable platform) to a
|
|
389
|
+
concrete source and builds the spec with the matching repo id (``<scheme>-<org>-<repo>``,
|
|
390
|
+
so `aim scan`-adopted weights resolve). aim-store copies are found via `aim resolve`, so
|
|
391
|
+
those are still reused even though local_model_path doesn't see the store."""
|
|
392
|
+
if local_model_path(model, ms_repo_map):
|
|
393
|
+
return None
|
|
394
|
+
scheme = "ms" if effective_source(hub) == "ms" else "hf"
|
|
395
|
+
repo = (ms_repo_map or {}).get(model, model) if scheme == "ms" else model
|
|
396
|
+
return AimModelSpec(source=f"{scheme}:{repo}",
|
|
397
|
+
model_id=aim_model_id(repo, scheme), category=category)
|
|
398
|
+
|
|
399
|
+
|
|
400
|
+
def resolve_model_source(
|
|
401
|
+
repo: str,
|
|
402
|
+
*,
|
|
403
|
+
hub: str = "",
|
|
404
|
+
model_path: str = "",
|
|
405
|
+
aim_spec: Any = None,
|
|
406
|
+
ms_repo_map: Mapping[str, str] | None = None,
|
|
407
|
+
custom_url: str = "",
|
|
408
|
+
fixed_path: str = "",
|
|
409
|
+
category: str = "asr/model", # noqa: ARG001 - reserved; aim_spec already carries it
|
|
410
|
+
snapshot: bool = False,
|
|
411
|
+
) -> str:
|
|
412
|
+
"""Return a loadable local path (or a repo id for HF loaders). See
|
|
413
|
+
docs/architecture/unified-model-download.md §2 for the priority chain.
|
|
414
|
+
|
|
415
|
+
When ``snapshot=True`` and the source falls through to HF, the function
|
|
416
|
+
calls ``_hf_snapshot`` to materialise a real local directory (honoring
|
|
417
|
+
``HF_ENDPOINT``). Loaders that require a local dir (dolphin, etc.) use this
|
|
418
|
+
instead of receiving a bare repo id. On failure it falls back to the repo id
|
|
419
|
+
so callers are never broken."""
|
|
420
|
+
# 1. explicit local path wins.
|
|
421
|
+
local = aim_local_path_or(model_path, "")
|
|
422
|
+
if local:
|
|
423
|
+
return _maybe_symlink(fixed_path, local)
|
|
424
|
+
# 2. aim (when available + opted in). None → native-CAS already in aim cache OR
|
|
425
|
+
# aim absent → fall through (the hf branch returns the repo id).
|
|
426
|
+
aim_path = _aim_ensure(aim_spec)
|
|
427
|
+
if aim_path:
|
|
428
|
+
return _maybe_symlink(fixed_path, aim_path)
|
|
429
|
+
# 3. engine custom url (engines with no HF/ms repo-id load path).
|
|
430
|
+
if custom_url:
|
|
431
|
+
url_dir = _download_archive(custom_url, repo.replace("/", "-"))
|
|
432
|
+
if url_dir:
|
|
433
|
+
return _maybe_symlink(fixed_path, url_dir)
|
|
434
|
+
# 4. make a loader's OWN sub-downloads (e.g. funasr's VAD) reach HF on blocked nets.
|
|
435
|
+
ensure_reachable_download_env()
|
|
436
|
+
# 5. robust download → local dir, when one is needed: the loader wants a local dir
|
|
437
|
+
# (snapshot), ModelScope is the source (loaders take a dir there), or HF is
|
|
438
|
+
# unreachable (handing back a bare repo id would dead-end). Reachability-aware,
|
|
439
|
+
# retrying, cross-source. Otherwise (HF reachable, hf loader) return the repo id
|
|
440
|
+
# so the loader fetches directly — the long-standing fast path.
|
|
441
|
+
if (snapshot or (hub or "").strip().lower() == "auto"
|
|
442
|
+
or effective_source(hub) == "ms" or not reachable_sources()["hf"]):
|
|
443
|
+
got = _materialise(repo, hub, ms_repo_map)
|
|
444
|
+
if got:
|
|
445
|
+
return _maybe_symlink(fixed_path, got)
|
|
446
|
+
logger.warning("model download: all sources failed for {}; returning repo id "
|
|
447
|
+
"(loader will try directly)", repo)
|
|
448
|
+
return repo
|
|
449
|
+
|
|
450
|
+
|
|
451
|
+
def effective_hub(hub: str) -> str:
|
|
452
|
+
"""Effective download hub for a model.
|
|
453
|
+
|
|
454
|
+
- explicit ``ms`` → ``ms``
|
|
455
|
+
- ``""`` / ``hf`` while the global ``OSA_PREFER_MODELSCOPE`` switch is on → ``ms``
|
|
456
|
+
- else → ``hf``
|
|
457
|
+
"""
|
|
458
|
+
h = (hub or "").strip().lower()
|
|
459
|
+
if h == "ms":
|
|
460
|
+
return "ms"
|
|
461
|
+
if h in ("", "hf") and _prefer_ms_env():
|
|
462
|
+
return "ms"
|
|
463
|
+
return "hf"
|
|
464
|
+
|
|
465
|
+
|
|
466
|
+
def resolve_hub_model(
|
|
467
|
+
model: str,
|
|
468
|
+
hub: str = "hf",
|
|
469
|
+
*,
|
|
470
|
+
model_path: str = "",
|
|
471
|
+
ms_repo_map: Mapping[str, str] | None = None,
|
|
472
|
+
) -> str:
|
|
473
|
+
"""Resolve a ``from_pretrained``-style model ref (funasr / transformers loaders) to
|
|
474
|
+
a loadable local dir or a repo id, with reachability-aware, retrying, cross-source
|
|
475
|
+
download (auto HF mirror on blocked nets, ModelScope ↔ HF fallback). Returns a bare
|
|
476
|
+
repo id only when HF is reachable and the loader can fetch it itself (fast path).
|
|
477
|
+
|
|
478
|
+
No aim trigger here — callers aim-provision ``model_path`` out of band (main
|
|
479
|
+
process); an aim-provisioned local path wins and survives aim uninstall (no
|
|
480
|
+
re-download). Missing ``modelscope`` no longer raises: the cross-source fallback
|
|
481
|
+
routes to HF / a mirror instead of breaking the load."""
|
|
482
|
+
local = aim_local_path_or(model_path, "")
|
|
483
|
+
if local:
|
|
484
|
+
return local
|
|
485
|
+
return resolve_model_source(
|
|
486
|
+
model, hub=hub, model_path=model_path, ms_repo_map=ms_repo_map,
|
|
487
|
+
aim_spec=None, snapshot=False,
|
|
488
|
+
)
|
|
@@ -397,13 +397,16 @@ class ServiceDispatcher:
|
|
|
397
397
|
c.value if hasattr(c, "value") else str(c) for c in caps
|
|
398
398
|
)
|
|
399
399
|
|
|
400
|
-
# Supported languages
|
|
401
|
-
#
|
|
400
|
+
# Supported languages for the Engines tag filter: field_options.language plus the
|
|
401
|
+
# provider's `supported_languages` (multilingual engines that don't enumerate a
|
|
402
|
+
# language setting). Drop the auto-detect / non-speech sentinels — not languages.
|
|
402
403
|
field_opts = getattr(provider_cls, "field_options", {}) or {}
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
404
|
+
_langs = list(field_opts.get("language") or []) + list(
|
|
405
|
+
getattr(provider_cls, "supported_languages", []) or []
|
|
406
|
+
)
|
|
407
|
+
languages = list(dict.fromkeys(
|
|
408
|
+
str(x) for x in _langs if str(x).lower() not in ("auto", "nospeech")
|
|
409
|
+
))
|
|
407
410
|
|
|
408
411
|
# Pick display-friendly settings
|
|
409
412
|
display_info = {}
|
|
@@ -8,6 +8,8 @@ import shutil
|
|
|
8
8
|
import subprocess
|
|
9
9
|
from typing import Any
|
|
10
10
|
|
|
11
|
+
from openspeechapi.logging_config import logger
|
|
12
|
+
|
|
11
13
|
# Resolve is a quick registry lookup; downloads can be large. Both are bounded so
|
|
12
14
|
# a stalled aim CLI (e.g. a blocked network) can't hang forever — on timeout the
|
|
13
15
|
# caller falls back to the provider's own download.
|
|
@@ -147,13 +149,23 @@ def _aim_download(source: str, model_id: str, category: str) -> bool:
|
|
|
147
149
|
|
|
148
150
|
Bounded by ``OSA_AIM_DOWNLOAD_TIMEOUT_S`` (default 1800s); on timeout/failure
|
|
149
151
|
returns False so the caller falls back to the provider's own download."""
|
|
150
|
-
|
|
152
|
+
# ``-y`` auto-confirms installing aim's missing backend tools (modelscope /
|
|
153
|
+
# huggingface_hub) so a server-driven (no-stdin) ``aim download`` never blocks on the
|
|
154
|
+
# interactive "Install backend? [y/N]" prompt and aborts — aim >= 0.2.0. (Equivalent:
|
|
155
|
+
# AIM_ASSUME_YES=1 env or defaults.auto_install_backend in aim's config.)
|
|
156
|
+
args = ["download", source, "--name", model_id, "-y"]
|
|
151
157
|
if category:
|
|
152
158
|
args += ["--category", category]
|
|
153
159
|
try:
|
|
154
160
|
_run_aim(args, timeout=_download_timeout_s())
|
|
155
161
|
return True
|
|
156
|
-
except Exception:
|
|
162
|
+
except Exception as exc: # noqa: BLE001 - any failure → provider falls back, but log why
|
|
163
|
+
# Don't fail silently — surface the reason (network down, backend install failed, …)
|
|
164
|
+
# so it's diagnosable; the provider then falls back to its own robust download.
|
|
165
|
+
logger.warning(
|
|
166
|
+
"aim download failed for {} (name={}): {} — falling back to the provider's "
|
|
167
|
+
"own download.", source, model_id, str(exc)[:300],
|
|
168
|
+
)
|
|
157
169
|
return False
|
|
158
170
|
|
|
159
171
|
|