termux-tts 1.5.0 → 1.5.2
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.
- package/bin/cli.js +40 -22
- package/package.json +9 -1
- package/.github/workflows/release.yml +0 -98
- package/CHANGELOG.md +0 -93
- package/doc.config.yaml +0 -595
- package/docs/benchmarks.md +0 -12
- package/docs/guide.md +0 -552
- package/docs/tts_guide.md +0 -552
- package/install.sh +0 -53
- package/pyproject.toml +0 -81
- package/setup.py +0 -38
- package/termux_tts/__init__.py +0 -70
- package/termux_tts/adapter.py +0 -202
- package/termux_tts/audio.py +0 -101
- package/termux_tts/cli.py +0 -156
- package/termux_tts/control/__init__.py +0 -4
- package/termux_tts/control/component.py +0 -398
- package/termux_tts/control/errors.py +0 -52
- package/termux_tts/control/instances.py +0 -137
- package/termux_tts/control/models.py +0 -151
- package/termux_tts/control/status.py +0 -13
- package/termux_tts/engine.py +0 -340
- package/termux_tts/engine_expressive.py +0 -216
- package/termux_tts/engine_multilingual.py +0 -409
- package/termux_tts/engine_native.py +0 -104
- package/termux_tts/engine_onnx.py +0 -8
- package/termux_tts/engine_sherpa.py +0 -388
- package/termux_tts/engine_sherpa_capi.py +0 -508
- package/termux_tts/engine_vulkan.py +0 -395
- package/termux_tts/exceptions.py +0 -28
- package/termux_tts/g2p_korean.py +0 -242
- package/termux_tts/hardware.py +0 -166
- package/termux_tts/installer.py +0 -381
- package/termux_tts/script_classifier.py +0 -253
- package/termux_tts/tokenizer.py +0 -186
- package/termux_tts/tokenizer_melo.py +0 -159
- package/termux_tts/transliteration.py +0 -250
|
@@ -1,151 +0,0 @@
|
|
|
1
|
-
"""
|
|
2
|
-
termux_tts.control.models
|
|
3
|
-
ModelRegistry 연동 — TTS 모델 + Voice 분리 관리.
|
|
4
|
-
|
|
5
|
-
TTS 특이사항:
|
|
6
|
-
- model_id: "dsp-ko", "piper-ko", "fastspeech-ko" 등 엔진/언어 조합
|
|
7
|
-
- voice_id: "ko-speaker-01", "en-speaker-01" 등 화자 ID (모델에 종속)
|
|
8
|
-
- 내장 엔진(dsp/native)은 파일 없이도 항상 사용 가능 → missing 상태로 표기 금지
|
|
9
|
-
- ONNX 모델은 파일 존재만으로 installed 처리 금지 → Registry 기반
|
|
10
|
-
"""
|
|
11
|
-
from __future__ import annotations
|
|
12
|
-
|
|
13
|
-
import os
|
|
14
|
-
import time
|
|
15
|
-
from pathlib import Path
|
|
16
|
-
from typing import Any
|
|
17
|
-
|
|
18
|
-
from ameva_component import ModelRegistry
|
|
19
|
-
from ameva_component.manifest import ModelState
|
|
20
|
-
|
|
21
|
-
COMPONENT_ID = "termux-tts"
|
|
22
|
-
|
|
23
|
-
# ------------------------------------------------------------------
|
|
24
|
-
# 내장(Built-in) 엔진 정의 — 파일 불필요, 항상 사용 가능
|
|
25
|
-
# ------------------------------------------------------------------
|
|
26
|
-
|
|
27
|
-
# format: {model_id: {description, engine, voices, backend_key}}
|
|
28
|
-
BUILTIN_MODELS: dict[str, dict[str, Any]] = {
|
|
29
|
-
"dsp-ko": {
|
|
30
|
-
"model_id": "dsp-ko",
|
|
31
|
-
"description": "Pure Parametric DSP Formant Synthesizer (Korean, zero-dependency)",
|
|
32
|
-
"engine": "dsp",
|
|
33
|
-
"format": "builtin",
|
|
34
|
-
"language": "ko",
|
|
35
|
-
"voices": [
|
|
36
|
-
{"voice_id": "ko-default", "description": "Default Korean DSP voice", "language": "ko"},
|
|
37
|
-
],
|
|
38
|
-
"state": ModelState.ACTIVE.value, # 내장 엔진은 항상 사용 가능
|
|
39
|
-
},
|
|
40
|
-
"dsp-en": {
|
|
41
|
-
"model_id": "dsp-en",
|
|
42
|
-
"description": "Pure Parametric DSP Formant Synthesizer (English, zero-dependency)",
|
|
43
|
-
"engine": "dsp",
|
|
44
|
-
"format": "builtin",
|
|
45
|
-
"language": "en",
|
|
46
|
-
"voices": [
|
|
47
|
-
{"voice_id": "en-default", "description": "Default English DSP voice", "language": "en"},
|
|
48
|
-
],
|
|
49
|
-
"state": ModelState.ACTIVE.value,
|
|
50
|
-
},
|
|
51
|
-
"native-system": {
|
|
52
|
-
"model_id": "native-system",
|
|
53
|
-
"description": "Android System Native Voice (Samsung/Google TTS bridge)",
|
|
54
|
-
"engine": "native",
|
|
55
|
-
"format": "builtin",
|
|
56
|
-
"language": "ko,en",
|
|
57
|
-
"voices": [
|
|
58
|
-
{"voice_id": "system-default", "description": "Android system TTS voice", "language": "ko,en"},
|
|
59
|
-
],
|
|
60
|
-
"state": ModelState.ACTIVE.value,
|
|
61
|
-
},
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
class TTSModelRegistry:
|
|
66
|
-
"""
|
|
67
|
-
ModelRegistry를 감싸는 TTS 전용 레지스트리.
|
|
68
|
-
|
|
69
|
-
내장 엔진(dsp, native)은 파일 없이도 항상 사용 가능하며,
|
|
70
|
-
ONNX 모델만 ModelRegistry를 통해 상태를 추적합니다.
|
|
71
|
-
"""
|
|
72
|
-
|
|
73
|
-
def __init__(self, registry_dir: Path | None = None) -> None:
|
|
74
|
-
self._reg = ModelRegistry(COMPONENT_ID, registry_dir=registry_dir)
|
|
75
|
-
|
|
76
|
-
# ------------------------------------------------------------------
|
|
77
|
-
# 전체 목록 조회
|
|
78
|
-
# ------------------------------------------------------------------
|
|
79
|
-
|
|
80
|
-
def list_all(self) -> list[dict]:
|
|
81
|
-
"""
|
|
82
|
-
모든 모델 레코드를 반환합니다.
|
|
83
|
-
- 내장 엔진: BUILTIN_MODELS에서 직접 반환
|
|
84
|
-
- ONNX 모델: ModelRegistry에서 실제 상태 조회
|
|
85
|
-
"""
|
|
86
|
-
result: list[dict] = list(BUILTIN_MODELS.values())
|
|
87
|
-
# Registry에서 외부 ONNX 모델 추가
|
|
88
|
-
for rec in self._reg.list_all():
|
|
89
|
-
result.append(self._enrich_onnx_record(rec))
|
|
90
|
-
return result
|
|
91
|
-
|
|
92
|
-
def list_voices_for_model(self, model_id: str) -> list[dict]:
|
|
93
|
-
"""특정 model_id의 voice 목록을 반환합니다."""
|
|
94
|
-
if model_id in BUILTIN_MODELS:
|
|
95
|
-
return BUILTIN_MODELS[model_id].get("voices", [])
|
|
96
|
-
rec = self._reg.get(model_id)
|
|
97
|
-
if rec is None:
|
|
98
|
-
return []
|
|
99
|
-
return rec.get("voices", [])
|
|
100
|
-
|
|
101
|
-
def get(self, model_id: str) -> dict | None:
|
|
102
|
-
"""단일 모델 레코드를 반환합니다. 없으면 None."""
|
|
103
|
-
if model_id in BUILTIN_MODELS:
|
|
104
|
-
return BUILTIN_MODELS[model_id]
|
|
105
|
-
rec = self._reg.get(model_id)
|
|
106
|
-
if rec is None:
|
|
107
|
-
return None
|
|
108
|
-
return self._enrich_onnx_record(rec)
|
|
109
|
-
|
|
110
|
-
def get_state(self, model_id: str) -> ModelState | None:
|
|
111
|
-
"""모델 상태를 반환합니다. 없으면 None."""
|
|
112
|
-
if model_id in BUILTIN_MODELS:
|
|
113
|
-
return ModelState.ACTIVE
|
|
114
|
-
rec = self._reg.get(model_id)
|
|
115
|
-
if rec is None:
|
|
116
|
-
return None
|
|
117
|
-
try:
|
|
118
|
-
return ModelState(rec.get("state", "missing"))
|
|
119
|
-
except ValueError:
|
|
120
|
-
return None
|
|
121
|
-
|
|
122
|
-
# ------------------------------------------------------------------
|
|
123
|
-
# 상태 변경
|
|
124
|
-
# ------------------------------------------------------------------
|
|
125
|
-
|
|
126
|
-
def set_onnx_active(self, model_id: str) -> None:
|
|
127
|
-
"""ONNX 모델을 active 상태로 변경합니다."""
|
|
128
|
-
self._reg.set_state(model_id, ModelState.ACTIVE)
|
|
129
|
-
|
|
130
|
-
def set_onnx_inactive(self, model_id: str) -> None:
|
|
131
|
-
"""ONNX 모델을 inactive 상태로 변경합니다."""
|
|
132
|
-
self._reg.set_state(model_id, ModelState.INACTIVE)
|
|
133
|
-
|
|
134
|
-
def record_onnx_install(self, model_id: str, manifest, verified_at: float) -> None:
|
|
135
|
-
"""ONNX 모델 설치 완료를 기록합니다."""
|
|
136
|
-
self._reg.record_install(model_id, manifest, verified_at)
|
|
137
|
-
|
|
138
|
-
def set_onnx_state(self, model_id: str, state: ModelState, *, last_error: str | None = None) -> None:
|
|
139
|
-
"""ONNX 모델 상태를 직접 갱신합니다."""
|
|
140
|
-
self._reg.set_state(model_id, state, last_error=last_error)
|
|
141
|
-
|
|
142
|
-
# ------------------------------------------------------------------
|
|
143
|
-
# 내부 헬퍼
|
|
144
|
-
# ------------------------------------------------------------------
|
|
145
|
-
|
|
146
|
-
def _enrich_onnx_record(self, rec: dict) -> dict:
|
|
147
|
-
"""ONNX 레코드에 engine/voices 필드가 없으면 기본값을 추가합니다."""
|
|
148
|
-
enriched = dict(rec)
|
|
149
|
-
enriched.setdefault("engine", "onnx")
|
|
150
|
-
enriched.setdefault("voices", [])
|
|
151
|
-
return enriched
|
|
@@ -1,13 +0,0 @@
|
|
|
1
|
-
"""
|
|
2
|
-
termux_tts.control.status — TTS 상태 파일 Writer + Heartbeat
|
|
3
|
-
"""
|
|
4
|
-
from __future__ import annotations
|
|
5
|
-
from typing import Any
|
|
6
|
-
from ameva_component.heartbeat import HeartbeatWriter
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
class TTSStatusWriter(HeartbeatWriter):
|
|
10
|
-
"""TTSControl 상태를 10초마다 상태 파일에 원자적으로 기록합니다."""
|
|
11
|
-
|
|
12
|
-
def __init__(self, control: Any) -> None:
|
|
13
|
-
super().__init__(control, name="tts")
|
package/termux_tts/engine.py
DELETED
|
@@ -1,340 +0,0 @@
|
|
|
1
|
-
"""
|
|
2
|
-
Unified 4-Tier Multi-Backend TTS Gateway for termux-tts:
|
|
3
|
-
- Tier 1: Pure Parametric DSP Formant Synthesizer Engine (ParametricDSPEngine - 0MB Zero-Dependency)
|
|
4
|
-
- Tier 2: Android System Native Samsung/Google Voice Engine Bridge (NativeAndroidEngine - 0MB OS IPC)
|
|
5
|
-
- Tier 3: Authentic C++ Isolated Sherpa-ONNX VITS Neural Vocoder (SherpaNeuralEngine - Deep Learning)
|
|
6
|
-
- Tier 4: Pure On-Device Expressive Emotional Synthesizer (ExpressiveEngine - Conversational Tags)
|
|
7
|
-
|
|
8
|
-
Strict Zero-Silent-Fallback Protocol:
|
|
9
|
-
- If caller explicitly requests 'neural' and assets are missing -> Raise TTSModelLoadError (FAIL-FAST)
|
|
10
|
-
- If caller explicitly requests 'native' and termux-api is missing -> Raise TTSInferenceError (FAIL-FAST)
|
|
11
|
-
- Never swallow exceptions or mask failures with robotic buzzers without explicit instruction.
|
|
12
|
-
"""
|
|
13
|
-
from __future__ import annotations
|
|
14
|
-
|
|
15
|
-
import logging
|
|
16
|
-
import os
|
|
17
|
-
from typing import Optional, Union, Dict, Any
|
|
18
|
-
|
|
19
|
-
from .exceptions import TTSInferenceError, TTSModelLoadError, VulkanInitializationError
|
|
20
|
-
from .engine_native import NativeAndroidEngine, NativeResult
|
|
21
|
-
from .engine_sherpa import SherpaNeuralEngine, SherpaResult
|
|
22
|
-
from .engine_vulkan import VulkanNeuralEngine, VulkanResult
|
|
23
|
-
from .engine_expressive import ExpressiveEngine, ExpressiveResult
|
|
24
|
-
from .engine_multilingual import MultilingualNeuralEngine, MultilingualResult
|
|
25
|
-
from .script_classifier import MultilingualTokenizer
|
|
26
|
-
from .hardware import (
|
|
27
|
-
resolve_device_backend,
|
|
28
|
-
bind_tts_hardware,
|
|
29
|
-
_resolve_ameva_runtime,
|
|
30
|
-
ERROR_AMEVA_TTS_E001,
|
|
31
|
-
)
|
|
32
|
-
|
|
33
|
-
logger = logging.getLogger("termux_tts.engine")
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
QUALITY_PRESETS = {
|
|
37
|
-
"fast": {"sample_rate": 16000, "description": "16kHz Low Latency"},
|
|
38
|
-
"balanced": {"sample_rate": 22050, "description": "22.05kHz Standard Audio"},
|
|
39
|
-
"expressive": {"sample_rate": 24000, "description": "24kHz Expressive High Fidelity"},
|
|
40
|
-
"ultra": {"sample_rate": 44100, "description": "44.1kHz Studio Master"},
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
class TTSEngine:
|
|
45
|
-
"""Production 4-Tier Multi-Backend Gateway supporting Synth, Native, Neural, and Expressive engines."""
|
|
46
|
-
|
|
47
|
-
def __init__(
|
|
48
|
-
self,
|
|
49
|
-
model_path: Optional[str] = None,
|
|
50
|
-
language: str = "ko",
|
|
51
|
-
preset: str = "balanced",
|
|
52
|
-
device: str = "auto",
|
|
53
|
-
threads: int = 4,
|
|
54
|
-
sample_rate: Optional[int] = None,
|
|
55
|
-
engine_type: str = "auto",
|
|
56
|
-
model_tier: Optional[str] = None,
|
|
57
|
-
):
|
|
58
|
-
self.language = language.lower()
|
|
59
|
-
self.preset = preset.lower()
|
|
60
|
-
self.requested_device = device.lower()
|
|
61
|
-
self.requested_engine_type = engine_type.lower()
|
|
62
|
-
self.model_path = model_path
|
|
63
|
-
self.threads = threads
|
|
64
|
-
self.sample_rate = sample_rate
|
|
65
|
-
self.model_tier = model_tier
|
|
66
|
-
self._is_closed = False
|
|
67
|
-
|
|
68
|
-
# 1. Resolve effective device and engine_type via safe hardware gateway
|
|
69
|
-
self.device, effective_engine = resolve_device_backend(
|
|
70
|
-
self.requested_device, self.requested_engine_type
|
|
71
|
-
)
|
|
72
|
-
if self.requested_engine_type == "auto" and effective_engine != "auto":
|
|
73
|
-
self.requested_engine_type = effective_engine
|
|
74
|
-
|
|
75
|
-
self.backend = "auto"
|
|
76
|
-
|
|
77
|
-
# 2. Attempt ameva-runtime hardware binding if available
|
|
78
|
-
self._binding_plan = self._bind_hardware()
|
|
79
|
-
|
|
80
|
-
self.native_engine = NativeAndroidEngine(language=language)
|
|
81
|
-
self._multilingual_engine: Optional[MultilingualNeuralEngine] = None
|
|
82
|
-
self.synth_engine = self._resolve_synth_engine()
|
|
83
|
-
|
|
84
|
-
def _bind_hardware(self):
|
|
85
|
-
return bind_tts_hardware(self, self.requested_device)
|
|
86
|
-
|
|
87
|
-
def _resolve_synth_engine(self):
|
|
88
|
-
t = self.requested_engine_type
|
|
89
|
-
from .script_classifier import normalize_language_code
|
|
90
|
-
norm_lang = normalize_language_code(self.language)
|
|
91
|
-
|
|
92
|
-
# Extended Languages (hi, ru, ja, zh, es, fr, de, ar) route to MultilingualNeuralEngine
|
|
93
|
-
if norm_lang in ("hi", "ru", "ja", "zh", "es", "fr", "de", "ar"):
|
|
94
|
-
return self._get_multilingual_engine()
|
|
95
|
-
|
|
96
|
-
# Explicit Vulkan GPU MeloTTS Tier (Plan 1 NCNN Sliced / Plan 2 MNN Vulkan)
|
|
97
|
-
if t in ("melo", "melo_vulkan", "melo_ncnn", "melo_mnn") and (self.requested_device in ("vulkan", "gpu") or self.device == "vulkan"):
|
|
98
|
-
return VulkanNeuralEngine(
|
|
99
|
-
model_path=self.model_path,
|
|
100
|
-
language=self.language,
|
|
101
|
-
device=self.requested_device,
|
|
102
|
-
threads=self.threads,
|
|
103
|
-
sample_rate=self.sample_rate or 44100,
|
|
104
|
-
model_tier=self.model_tier,
|
|
105
|
-
model_type=t,
|
|
106
|
-
)
|
|
107
|
-
|
|
108
|
-
# Explicit Vulkan GPU Tier (Vulkan NCNN engine targets English Lessac)
|
|
109
|
-
if (t in ("vulkan", "gpu", "ncnn") or (self.requested_device in ("vulkan", "gpu") and t in ("neural", "vits", "auto"))) and norm_lang in ("en", "auto"):
|
|
110
|
-
try:
|
|
111
|
-
return VulkanNeuralEngine(
|
|
112
|
-
model_path=self.model_path,
|
|
113
|
-
language=self.language,
|
|
114
|
-
device=self.requested_device,
|
|
115
|
-
threads=self.threads,
|
|
116
|
-
sample_rate=self.sample_rate or 22050,
|
|
117
|
-
model_tier=self.model_tier,
|
|
118
|
-
model_type="vits",
|
|
119
|
-
)
|
|
120
|
-
except (VulkanInitializationError, TTSModelLoadError) as err:
|
|
121
|
-
if t in ("vulkan", "gpu", "ncnn") or self.requested_device in ("vulkan", "gpu"):
|
|
122
|
-
raise
|
|
123
|
-
logger.debug("Vulkan neural engine not ready, trying fallback: %s", err)
|
|
124
|
-
|
|
125
|
-
# Explicit Tier 3: Neural (Sherpa-ONNX CPU)
|
|
126
|
-
if t in ("neural", "onnx", "sherpa", "vits"):
|
|
127
|
-
return SherpaNeuralEngine(
|
|
128
|
-
model_path=self.model_path,
|
|
129
|
-
language=self.language,
|
|
130
|
-
device=self.requested_device,
|
|
131
|
-
threads=self.threads,
|
|
132
|
-
sample_rate=self.sample_rate or 22050,
|
|
133
|
-
model_type="vits",
|
|
134
|
-
)
|
|
135
|
-
|
|
136
|
-
# BigTech 3rd-Party Neural Speech Engines (StyleTTS2/Kokoro, MeloTTS, Supertonic)
|
|
137
|
-
elif t in ("kokoro", "melo", "supertonic"):
|
|
138
|
-
return SherpaNeuralEngine(
|
|
139
|
-
model_path=self.model_path,
|
|
140
|
-
language=self.language,
|
|
141
|
-
device=self.requested_device,
|
|
142
|
-
threads=self.threads,
|
|
143
|
-
sample_rate=self.sample_rate or 22050,
|
|
144
|
-
model_type=t,
|
|
145
|
-
)
|
|
146
|
-
|
|
147
|
-
# Explicit Tier 4: Expressive (Fail-Fast)
|
|
148
|
-
elif t in ("expressive", "chat", "conversational"):
|
|
149
|
-
return ExpressiveEngine(
|
|
150
|
-
model_path=self.model_path,
|
|
151
|
-
language=self.language,
|
|
152
|
-
device=self.requested_device,
|
|
153
|
-
threads=self.threads,
|
|
154
|
-
sample_rate=self.sample_rate or 22050,
|
|
155
|
-
)
|
|
156
|
-
|
|
157
|
-
# Explicit Multilingual / Code-Switching Tier
|
|
158
|
-
elif t in ("multilingual", "codeswitch", "hybrid"):
|
|
159
|
-
return self._get_multilingual_engine()
|
|
160
|
-
|
|
161
|
-
# Explicit Tier 2: Native
|
|
162
|
-
elif t == "native":
|
|
163
|
-
return self.native_engine
|
|
164
|
-
|
|
165
|
-
# Auto Mode (Zero-Silent-Fallback)
|
|
166
|
-
elif t == "auto":
|
|
167
|
-
# 1. Check if SherpaNeuralEngine assets exist
|
|
168
|
-
try:
|
|
169
|
-
return SherpaNeuralEngine(
|
|
170
|
-
model_path=self.model_path,
|
|
171
|
-
language=self.language,
|
|
172
|
-
device=self.requested_device,
|
|
173
|
-
threads=self.threads,
|
|
174
|
-
sample_rate=self.sample_rate or 22050,
|
|
175
|
-
)
|
|
176
|
-
except TTSModelLoadError as err:
|
|
177
|
-
logger.info("Auto tier: Neural model assets not ready (%s). Probing native system voice...", err)
|
|
178
|
-
|
|
179
|
-
# 2. Check if NativeAndroidEngine is available
|
|
180
|
-
if self.native_engine.binary:
|
|
181
|
-
return self.native_engine
|
|
182
|
-
|
|
183
|
-
# 3. Fail-Fast: No silent robotic fallback
|
|
184
|
-
raise TTSModelLoadError(
|
|
185
|
-
"[FAIL-FAST] No TTS voice engine available. Neural speech model is not installed, "
|
|
186
|
-
"and native Android TTS engine is unavailable.\n"
|
|
187
|
-
" To install official neural models automatically, run:\n"
|
|
188
|
-
" termux-tts install\n"
|
|
189
|
-
)
|
|
190
|
-
else:
|
|
191
|
-
raise TTSInferenceError(
|
|
192
|
-
f"[FAIL-FAST] Unknown engine_type '{self.requested_engine_type}'. "
|
|
193
|
-
f"Available tiers: ['auto', 'native', 'neural', 'expressive', 'multilingual', 'kokoro', 'melo', 'supertonic']"
|
|
194
|
-
)
|
|
195
|
-
|
|
196
|
-
@property
|
|
197
|
-
def active_backend(self) -> str:
|
|
198
|
-
return getattr(self.synth_engine, "backend", "NATIVE_SYSTEM")
|
|
199
|
-
|
|
200
|
-
@property
|
|
201
|
-
def model_name(self) -> str:
|
|
202
|
-
return getattr(self.synth_engine, "model_name", "native-system-voice")
|
|
203
|
-
|
|
204
|
-
@property
|
|
205
|
-
def binary(self) -> Optional[str]:
|
|
206
|
-
return getattr(self.synth_engine, "binary", getattr(self.native_engine, "binary", None))
|
|
207
|
-
|
|
208
|
-
def _get_multilingual_engine(self) -> MultilingualNeuralEngine:
|
|
209
|
-
if self._multilingual_engine is None:
|
|
210
|
-
self._multilingual_engine = MultilingualNeuralEngine(
|
|
211
|
-
threads=self.threads,
|
|
212
|
-
device=self.device,
|
|
213
|
-
sample_rate=self.sample_rate or 22050,
|
|
214
|
-
)
|
|
215
|
-
return self._multilingual_engine
|
|
216
|
-
|
|
217
|
-
def speak(self, text: str, stream: Optional[str] = None) -> NativeResult:
|
|
218
|
-
"""Speak text directly through physical Android speaker (Native Engine)."""
|
|
219
|
-
if self._is_closed:
|
|
220
|
-
raise TTSInferenceError("Cannot speak: Engine session is closed.")
|
|
221
|
-
return self.native_engine.speak(text, stream=stream)
|
|
222
|
-
|
|
223
|
-
def synthesize(
|
|
224
|
-
self,
|
|
225
|
-
text: str,
|
|
226
|
-
output: Optional[str] = None,
|
|
227
|
-
speed: float = 1.0,
|
|
228
|
-
preset: Optional[str] = None,
|
|
229
|
-
language: Optional[str] = None,
|
|
230
|
-
mode: str = "unified",
|
|
231
|
-
) -> Union[SherpaResult, ExpressiveResult, NativeResult, MultilingualResult]:
|
|
232
|
-
"""Synthesize text into speech audio buffer / WAV file with Zero-Config intelligent routing."""
|
|
233
|
-
if self._is_closed:
|
|
234
|
-
raise TTSInferenceError("Cannot synthesize: Engine session is closed.")
|
|
235
|
-
|
|
236
|
-
clean_text = text.strip() if text else ""
|
|
237
|
-
if not clean_text:
|
|
238
|
-
raise TTSInferenceError("Cannot synthesize empty text.")
|
|
239
|
-
|
|
240
|
-
from .script_classifier import normalize_language_code
|
|
241
|
-
target_lang = normalize_language_code(language or self.language)
|
|
242
|
-
|
|
243
|
-
# 1. If user explicitly pinned to OS Native voice, speak directly
|
|
244
|
-
if self.requested_engine_type == "native":
|
|
245
|
-
return self.native_engine.speak(clean_text)
|
|
246
|
-
|
|
247
|
-
# 2. Multilingual auto-detection:
|
|
248
|
-
# Route to MultilingualNeuralEngine if multiple languages are present in text
|
|
249
|
-
# or if caller specifically requested multilingual / hybrid engine.
|
|
250
|
-
detected_langs = MultilingualTokenizer.detect_languages(clean_text)
|
|
251
|
-
is_mixed_text = len(detected_langs) > 1
|
|
252
|
-
|
|
253
|
-
should_route_multilingual = (
|
|
254
|
-
(self.requested_engine_type in ("multilingual", "codeswitch", "hybrid")) or
|
|
255
|
-
(self.requested_engine_type == "auto" and (is_mixed_text or self._multilingual_engine is not None)) or
|
|
256
|
-
(is_mixed_text and self.requested_engine_type != "native") or
|
|
257
|
-
(target_lang in ("hi", "ru", "ja", "zh", "es", "fr", "de", "ar") and self.requested_engine_type != "native")
|
|
258
|
-
)
|
|
259
|
-
|
|
260
|
-
if should_route_multilingual:
|
|
261
|
-
multi_engine = self._get_multilingual_engine()
|
|
262
|
-
kwargs = {}
|
|
263
|
-
if target_lang != "auto":
|
|
264
|
-
kwargs["language"] = target_lang
|
|
265
|
-
return multi_engine.synthesize(
|
|
266
|
-
clean_text,
|
|
267
|
-
output=output,
|
|
268
|
-
speed=speed,
|
|
269
|
-
mode=mode,
|
|
270
|
-
**kwargs
|
|
271
|
-
)
|
|
272
|
-
|
|
273
|
-
# 4. Standard single-language engine
|
|
274
|
-
if hasattr(self.synth_engine, "synthesize"):
|
|
275
|
-
return self.synth_engine.synthesize(clean_text, output=output, speed=speed, preset=preset)
|
|
276
|
-
elif hasattr(self.synth_engine, "speak"):
|
|
277
|
-
return self.synth_engine.speak(clean_text)
|
|
278
|
-
raise TTSInferenceError(f"Selected engine '{type(self.synth_engine).__name__}' does not support synthesize.")
|
|
279
|
-
|
|
280
|
-
def close(self) -> None:
|
|
281
|
-
self._is_closed = True
|
|
282
|
-
self.native_engine.close()
|
|
283
|
-
if hasattr(self.synth_engine, "close"):
|
|
284
|
-
self.synth_engine.close()
|
|
285
|
-
if self._multilingual_engine is not None:
|
|
286
|
-
self._multilingual_engine.close()
|
|
287
|
-
|
|
288
|
-
def __enter__(self):
|
|
289
|
-
return self
|
|
290
|
-
|
|
291
|
-
def __exit__(self, exc_type, exc_val, exc_tb):
|
|
292
|
-
self.close()
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
def load(
|
|
296
|
-
model: Optional[str] = None,
|
|
297
|
-
language: str = "auto",
|
|
298
|
-
preset: str = "balanced",
|
|
299
|
-
device: str = "auto",
|
|
300
|
-
threads: int = 4,
|
|
301
|
-
sample_rate: Optional[int] = None,
|
|
302
|
-
engine: str = "auto",
|
|
303
|
-
tier: Optional[str] = None,
|
|
304
|
-
) -> TTSEngine:
|
|
305
|
-
return TTSEngine(
|
|
306
|
-
model_path=model,
|
|
307
|
-
language=language,
|
|
308
|
-
preset=preset,
|
|
309
|
-
device=device,
|
|
310
|
-
threads=threads,
|
|
311
|
-
sample_rate=sample_rate,
|
|
312
|
-
engine_type=engine,
|
|
313
|
-
model_tier=tier,
|
|
314
|
-
)
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
def doctor() -> Dict[str, Any]:
|
|
318
|
-
ameva_mod = _resolve_ameva_runtime()
|
|
319
|
-
if ameva_mod is not None:
|
|
320
|
-
try:
|
|
321
|
-
from ameva_runtime.adapters import TtsAdapter
|
|
322
|
-
adapter = TtsAdapter()
|
|
323
|
-
rep = adapter.resolve_diagnostic_report()
|
|
324
|
-
return {
|
|
325
|
-
"doctor_report": rep,
|
|
326
|
-
"overall_success": getattr(rep, "overall_success", False),
|
|
327
|
-
"passed_stages": getattr(rep, "passed_stages", 0),
|
|
328
|
-
"recommended_backend": getattr(rep, "recommended_backend", "cpu_neon"),
|
|
329
|
-
"status": "DIAGNOSED_VIA_AMEVA",
|
|
330
|
-
}
|
|
331
|
-
except Exception as e:
|
|
332
|
-
logger.debug("AMEVA Doctor invocation exception: %s", e)
|
|
333
|
-
|
|
334
|
-
return {
|
|
335
|
-
"doctor_report": None,
|
|
336
|
-
"overall_success": False,
|
|
337
|
-
"passed_stages": 0,
|
|
338
|
-
"recommended_backend": "cpu_neon",
|
|
339
|
-
"status": "NO_AMEVA_RUNTIME",
|
|
340
|
-
}
|