termux-tts 1.3.0 → 1.4.0
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/CHANGELOG.md +9 -0
- package/binding_node/index.js +9 -1
- package/package.json +1 -1
- package/pyproject.toml +1 -1
- package/termux_tts/__init__.py +1 -1
- package/termux_tts/cli.py +6 -1
- package/termux_tts/control/component.py +4 -3
- package/termux_tts/engine.py +44 -39
- package/termux_tts/engine_dsp.py +13 -5
- package/termux_tts/engine_onnx.py +1 -2
- package/termux_tts/engine_vulkan.py +51 -21
- package/termux_tts/hardware.py +121 -0
- package/termux_tts/vulkan_probe.py +0 -49
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,15 @@
|
|
|
2
2
|
|
|
3
3
|
All notable changes to this project will be documented in this file.
|
|
4
4
|
|
|
5
|
+
## [1.4.0] - 2026-09-07
|
|
6
|
+
|
|
7
|
+
### Added
|
|
8
|
+
- Direct integration with `TtsAdapter` from `ameva_runtime.adapters` SSOT.
|
|
9
|
+
- Dual-tier VITS Vulkan neural engine routing with strict Fail-Fast error semantics.
|
|
10
|
+
- English localization for all diagnostics, logs, and exception messages.
|
|
11
|
+
|
|
12
|
+
---
|
|
13
|
+
|
|
5
14
|
## [1.1.5] - 2026-09-05
|
|
6
15
|
|
|
7
16
|
### Changed
|
package/binding_node/index.js
CHANGED
|
@@ -12,6 +12,8 @@ class TTSEngine {
|
|
|
12
12
|
this.preset = options.preset || 'balanced';
|
|
13
13
|
this.sampleRate = options.sampleRate || 22050;
|
|
14
14
|
this.engine = options.engine || 'auto';
|
|
15
|
+
this.device = options.device || (options.gpu ? 'gpu' : 'auto');
|
|
16
|
+
this.tier = options.tier || null;
|
|
15
17
|
this.model = options.model || null;
|
|
16
18
|
}
|
|
17
19
|
|
|
@@ -24,6 +26,8 @@ class TTSEngine {
|
|
|
24
26
|
const speed = typeof options.speed === 'number' ? options.speed : 1.0;
|
|
25
27
|
const preset = options.preset || this.preset;
|
|
26
28
|
const engineType = options.engine || this.engine;
|
|
29
|
+
const deviceType = options.device || (options.gpu ? 'gpu' : this.device);
|
|
30
|
+
const tierType = options.tier || this.tier;
|
|
27
31
|
const modelPath = options.model || this.model;
|
|
28
32
|
|
|
29
33
|
const payload = JSON.stringify({
|
|
@@ -34,6 +38,8 @@ class TTSEngine {
|
|
|
34
38
|
speed: speed,
|
|
35
39
|
preset: preset,
|
|
36
40
|
engine: engineType,
|
|
41
|
+
device: deviceType,
|
|
42
|
+
tier: tierType,
|
|
37
43
|
model: modelPath
|
|
38
44
|
});
|
|
39
45
|
|
|
@@ -49,7 +55,9 @@ try:
|
|
|
49
55
|
language=data.get('language', 'ko'),
|
|
50
56
|
preset=data.get('preset', 'balanced'),
|
|
51
57
|
sample_rate=data.get('sample_rate', 22050),
|
|
52
|
-
engine=data.get('engine', 'auto')
|
|
58
|
+
engine=data.get('engine', 'auto'),
|
|
59
|
+
device=data.get('device', 'auto'),
|
|
60
|
+
tier=data.get('tier', None)
|
|
53
61
|
)
|
|
54
62
|
res = engine.synthesize(data['text'], output=data['output'], speed=float(data.get('speed', 1.0)))
|
|
55
63
|
result = {
|
package/package.json
CHANGED
package/pyproject.toml
CHANGED
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "termux-tts"
|
|
7
|
-
version = "1.
|
|
7
|
+
version = "1.4.0"
|
|
8
8
|
description = "On-device 4-Tier Text-to-Speech framework utilizing device resources (DSP Formant Vocoder, C++ Sherpa-ONNX Neural, Android Native & Expressive)"
|
|
9
9
|
readme = "README.pypi.md"
|
|
10
10
|
requires-python = ">=3.10"
|
package/termux_tts/__init__.py
CHANGED
package/termux_tts/cli.py
CHANGED
|
@@ -29,7 +29,11 @@ def main():
|
|
|
29
29
|
)
|
|
30
30
|
synth_parser.add_argument("-m", "--model", default=None, help="Path to model file or directory")
|
|
31
31
|
synth_parser.add_argument("-p", "--preset", default="balanced", choices=["fast", "balanced", "expressive", "ultra"])
|
|
32
|
-
synth_parser.add_argument("-d", "--device", default="auto", choices=["auto", "gpu", "vulkan", "cpu"])
|
|
32
|
+
synth_parser.add_argument("-d", "--device", default="auto", choices=["auto", "gpu", "vulkan", "cpu"], help="Compute target device")
|
|
33
|
+
synth_parser.add_argument("-b", "--backend", dest="device", choices=["auto", "gpu", "vulkan", "cpu"], help="Alias for --device")
|
|
34
|
+
synth_parser.add_argument("--gpu", dest="device", action="store_const", const="gpu", help="Enable hardware GPU acceleration")
|
|
35
|
+
synth_parser.add_argument("--cpu", dest="device", action="store_const", const="cpu", help="Force CPU compute mode")
|
|
36
|
+
synth_parser.add_argument("--tier", default=None, choices=["high", "medium", "balanced", "fast", "ultra"], help="Target model tier (high=Studio FP16, medium=Balanced)")
|
|
33
37
|
synth_parser.add_argument("-s", "--speed", type=float, default=1.0, help="Speech speed multiplier (0.5 to 2.0)")
|
|
34
38
|
synth_parser.add_argument("--threads", type=int, default=4, help="Compute worker threads (ARM NEON)")
|
|
35
39
|
synth_parser.add_argument("--volume", type=int, default=None, help="Set Android media volume (1 to 15)")
|
|
@@ -78,6 +82,7 @@ def main():
|
|
|
78
82
|
device=args.device,
|
|
79
83
|
threads=args.threads,
|
|
80
84
|
engine=args.engine,
|
|
85
|
+
tier=getattr(args, "tier", None),
|
|
81
86
|
) as engine:
|
|
82
87
|
res = engine.synthesize(args.text, output=args.output, speed=args.speed)
|
|
83
88
|
backend_name = getattr(res, "backend", "UNKNOWN")
|
|
@@ -224,9 +224,10 @@ class TTSControl(ComponentControl):
|
|
|
224
224
|
"""기존 TTSEngine doctor() 호출 — 12단계 Vulkan Doctor 포함."""
|
|
225
225
|
lite = self.doctor_lite()
|
|
226
226
|
try:
|
|
227
|
-
from termux_tts.
|
|
228
|
-
|
|
229
|
-
lite["
|
|
227
|
+
from termux_tts.engine import doctor
|
|
228
|
+
doc_rep = doctor()
|
|
229
|
+
lite["doctor"] = doc_rep
|
|
230
|
+
lite["vulkan"] = doc_rep.get("doctor_report") or {"status": doc_rep.get("status")}
|
|
230
231
|
except Exception as e:
|
|
231
232
|
lite["vulkan_error"] = str(e)
|
|
232
233
|
lite["doctor_level"] = "full"
|
package/termux_tts/engine.py
CHANGED
|
@@ -22,7 +22,12 @@ from .engine_dsp import ParametricDSPEngine, DSPResult, QUALITY_PRESETS
|
|
|
22
22
|
from .engine_sherpa import SherpaNeuralEngine, SherpaResult
|
|
23
23
|
from .engine_vulkan import VulkanNeuralEngine, VulkanResult
|
|
24
24
|
from .engine_expressive import ExpressiveEngine, ExpressiveResult
|
|
25
|
-
from .
|
|
25
|
+
from .hardware import (
|
|
26
|
+
resolve_device_backend,
|
|
27
|
+
bind_tts_hardware,
|
|
28
|
+
_resolve_ameva_runtime,
|
|
29
|
+
ERROR_AMEVA_TTS_E001,
|
|
30
|
+
)
|
|
26
31
|
|
|
27
32
|
logger = logging.getLogger("termux_tts.engine")
|
|
28
33
|
|
|
@@ -39,6 +44,7 @@ class TTSEngine:
|
|
|
39
44
|
threads: int = 4,
|
|
40
45
|
sample_rate: Optional[int] = None,
|
|
41
46
|
engine_type: str = "auto",
|
|
47
|
+
model_tier: Optional[str] = None,
|
|
42
48
|
):
|
|
43
49
|
self.language = language.lower()
|
|
44
50
|
self.preset = preset.lower()
|
|
@@ -47,32 +53,26 @@ class TTSEngine:
|
|
|
47
53
|
self.model_path = model_path
|
|
48
54
|
self.threads = threads
|
|
49
55
|
self.sample_rate = sample_rate
|
|
50
|
-
self.
|
|
51
|
-
self.backend = "auto"
|
|
56
|
+
self.model_tier = model_tier
|
|
52
57
|
self._is_closed = False
|
|
53
58
|
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
59
|
+
# 1. Resolve effective device and engine_type via safe hardware gateway
|
|
60
|
+
self.device, effective_engine = resolve_device_backend(
|
|
61
|
+
self.requested_device, self.requested_engine_type
|
|
62
|
+
)
|
|
63
|
+
if self.requested_engine_type == "auto" and effective_engine != "auto":
|
|
64
|
+
self.requested_engine_type = effective_engine
|
|
65
|
+
|
|
66
|
+
self.backend = "auto"
|
|
61
67
|
|
|
62
|
-
# Attempt ameva-runtime hardware binding if available
|
|
68
|
+
# 2. Attempt ameva-runtime hardware binding if available
|
|
63
69
|
self._binding_plan = self._bind_hardware()
|
|
64
70
|
|
|
65
71
|
self.native_engine = NativeAndroidEngine(language=language)
|
|
66
72
|
self.synth_engine = self._resolve_synth_engine()
|
|
67
73
|
|
|
68
74
|
def _bind_hardware(self):
|
|
69
|
-
|
|
70
|
-
from ameva_runtime.adapters.tts import TtsAdapter
|
|
71
|
-
binding = TtsAdapter.bind(engine=self, requested_backend=self.requested_device)
|
|
72
|
-
return binding
|
|
73
|
-
except Exception as e:
|
|
74
|
-
logger.debug("Hardware adapter binding skipped: %s", e)
|
|
75
|
-
return None
|
|
75
|
+
return bind_tts_hardware(self, self.requested_device)
|
|
76
76
|
|
|
77
77
|
def _resolve_synth_engine(self):
|
|
78
78
|
t = self.requested_engine_type
|
|
@@ -86,6 +86,7 @@ class TTSEngine:
|
|
|
86
86
|
device=self.requested_device,
|
|
87
87
|
threads=self.threads,
|
|
88
88
|
sample_rate=self.sample_rate or 22050,
|
|
89
|
+
model_tier=self.model_tier,
|
|
89
90
|
)
|
|
90
91
|
except (VulkanInitializationError, TTSModelLoadError) as err:
|
|
91
92
|
if t in ("vulkan", "gpu", "ncnn") or self.requested_device in ("vulkan", "gpu"):
|
|
@@ -214,6 +215,7 @@ def load(
|
|
|
214
215
|
threads: int = 4,
|
|
215
216
|
sample_rate: Optional[int] = None,
|
|
216
217
|
engine: str = "auto",
|
|
218
|
+
tier: Optional[str] = None,
|
|
217
219
|
) -> TTSEngine:
|
|
218
220
|
return TTSEngine(
|
|
219
221
|
model_path=model,
|
|
@@ -223,28 +225,31 @@ def load(
|
|
|
223
225
|
threads=threads,
|
|
224
226
|
sample_rate=sample_rate,
|
|
225
227
|
engine_type=engine,
|
|
228
|
+
model_tier=tier,
|
|
226
229
|
)
|
|
227
230
|
|
|
228
231
|
|
|
229
232
|
def doctor() -> Dict[str, Any]:
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
"
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
233
|
+
ameva_mod = _resolve_ameva_runtime()
|
|
234
|
+
if ameva_mod is not None:
|
|
235
|
+
try:
|
|
236
|
+
from ameva_runtime.adapters import TtsAdapter
|
|
237
|
+
adapter = TtsAdapter()
|
|
238
|
+
rep = adapter.resolve_diagnostic_report()
|
|
239
|
+
return {
|
|
240
|
+
"doctor_report": rep,
|
|
241
|
+
"overall_success": getattr(rep, "overall_success", False),
|
|
242
|
+
"passed_stages": getattr(rep, "passed_stages", 0),
|
|
243
|
+
"recommended_backend": getattr(rep, "recommended_backend", "cpu_neon"),
|
|
244
|
+
"status": "DIAGNOSED_VIA_AMEVA",
|
|
245
|
+
}
|
|
246
|
+
except Exception as e:
|
|
247
|
+
logger.debug("AMEVA Doctor invocation exception: %s", e)
|
|
248
|
+
|
|
249
|
+
return {
|
|
250
|
+
"doctor_report": None,
|
|
251
|
+
"overall_success": False,
|
|
252
|
+
"passed_stages": 0,
|
|
253
|
+
"recommended_backend": "cpu_neon",
|
|
254
|
+
"status": "NO_AMEVA_RUNTIME",
|
|
255
|
+
}
|
package/termux_tts/engine_dsp.py
CHANGED
|
@@ -14,7 +14,6 @@ from typing import Optional
|
|
|
14
14
|
from .exceptions import TTSInferenceError, VulkanInitializationError
|
|
15
15
|
from .tokenizer import PhoneticTokenizer
|
|
16
16
|
from .audio import AudioBuffer
|
|
17
|
-
from .vulkan_probe import VulkanDoctor
|
|
18
17
|
|
|
19
18
|
def _detect_cpu_backend() -> str:
|
|
20
19
|
"""Detect current host CPU architecture dynamically."""
|
|
@@ -171,21 +170,30 @@ class ParametricDSPEngine:
|
|
|
171
170
|
self.tokenizer = PhoneticTokenizer(language=language)
|
|
172
171
|
self._is_closed = False
|
|
173
172
|
|
|
174
|
-
self.
|
|
175
|
-
self.diag_info =
|
|
173
|
+
self.is_vulkan_available = False
|
|
174
|
+
self.diag_info = {}
|
|
175
|
+
if self.requested_device != "cpu":
|
|
176
|
+
try:
|
|
177
|
+
from ameva_runtime.adapters import TtsAdapter
|
|
178
|
+
rep = TtsAdapter.resolve_diagnostic_report()
|
|
179
|
+
self.is_vulkan_available = bool(getattr(rep, "overall_success", False) or getattr(rep, "recommended_backend", "") == "vulkan")
|
|
180
|
+
self.diag_info = {"recommended_backend": rep.recommended_backend, "passed_stages": rep.passed_stages}
|
|
181
|
+
except Exception as e:
|
|
182
|
+
import logging
|
|
183
|
+
logging.getLogger("termux_tts.engine_dsp").debug("TtsAdapter diagnostic check exception: %s", e)
|
|
176
184
|
self.backend = self._resolve_backend()
|
|
177
185
|
|
|
178
186
|
def _resolve_backend(self) -> str:
|
|
179
187
|
cpu_b = _detect_cpu_backend()
|
|
180
188
|
if self.requested_device in ("vulkan", "gpu"):
|
|
181
|
-
if self.
|
|
189
|
+
if self.is_vulkan_available:
|
|
182
190
|
return "VULKAN_GPU"
|
|
183
191
|
raise VulkanInitializationError(
|
|
184
192
|
f"[FAIL-FAST] Explicit GPU backend requested ('{self.requested_device}'), "
|
|
185
193
|
"but Vulkan hardware runtime is unavailable. Use '--device cpu' or '--device auto'."
|
|
186
194
|
)
|
|
187
195
|
elif self.requested_device == "auto":
|
|
188
|
-
return "VULKAN_GPU" if self.
|
|
196
|
+
return "VULKAN_GPU" if self.is_vulkan_available else cpu_b
|
|
189
197
|
return cpu_b
|
|
190
198
|
|
|
191
199
|
def synthesize(self, text: str, output: Optional[str] = None, speed: float = 1.0, preset: Optional[str] = None) -> DSPResult:
|
|
@@ -4,6 +4,5 @@ Python onnxruntime is permanently deprecated in favor of C++ isolated SherpaNeur
|
|
|
4
4
|
"""
|
|
5
5
|
from .engine_sherpa import SherpaNeuralEngine as ONNXNeuralEngine
|
|
6
6
|
from .engine_sherpa import SherpaResult as ONNXResult
|
|
7
|
-
from .vulkan_probe import VulkanDoctor
|
|
8
7
|
|
|
9
|
-
__all__ = ["ONNXNeuralEngine", "ONNXResult"
|
|
8
|
+
__all__ = ["ONNXNeuralEngine", "ONNXResult"]
|
|
@@ -69,17 +69,19 @@ class VulkanNeuralEngine:
|
|
|
69
69
|
device: str = "vulkan",
|
|
70
70
|
threads: int = 1,
|
|
71
71
|
sample_rate: int = 22050,
|
|
72
|
+
model_tier: Optional[str] = None,
|
|
72
73
|
):
|
|
73
74
|
self.language = language.lower()
|
|
74
75
|
self.requested_device = device.lower()
|
|
75
76
|
self.threads = threads
|
|
76
77
|
self.sample_rate = sample_rate
|
|
78
|
+
self.model_tier = (model_tier or "high").lower()
|
|
77
79
|
self._is_closed = False
|
|
78
80
|
|
|
79
81
|
# 1. Locate Vulkan binary
|
|
80
82
|
self.binary = self._find_binary()
|
|
81
83
|
|
|
82
|
-
# 2. Locate VITS NCNN model directory
|
|
84
|
+
# 2. Locate VITS NCNN model directory with tier awareness
|
|
83
85
|
self.model_dir = self._resolve_model_dir(model_path)
|
|
84
86
|
self.model_name = Path(self.model_dir).name
|
|
85
87
|
self.backend = "VULKAN_GPU_NCNN_VITS"
|
|
@@ -88,15 +90,19 @@ class VulkanNeuralEngine:
|
|
|
88
90
|
"""Locate sherpa-ncnn-offline-tts executable or fail-fast."""
|
|
89
91
|
for candidate in self.CANDIDATE_BINARIES:
|
|
90
92
|
found = shutil.which(candidate) if not os.path.isabs(candidate) else candidate
|
|
91
|
-
if found and os.path.isfile(found) and os.access(found, os.X_OK):
|
|
93
|
+
if found and os.path.isfile(found) and (os.access(found, os.X_OK) or os.name == "nt"):
|
|
92
94
|
return str(found)
|
|
93
95
|
raise VulkanInitializationError(
|
|
94
|
-
"[
|
|
95
|
-
"
|
|
96
|
+
"[ERROR: AMEVA-TTS-E001] 'sherpa-ncnn-offline-tts' Vulkan binary not found.\n"
|
|
97
|
+
"Cause: Hardware acceleration runtime or native Vulkan engine is not installed.\n"
|
|
98
|
+
"Action Required: Run one-click provisioning via:\n"
|
|
99
|
+
" $ termux-tts install --tier high\n"
|
|
100
|
+
" (Or run without GPU: termux-tts synth -e dsp -t \"...\")\n"
|
|
101
|
+
"Documentation: https://uno-km.vercel.app/lib/tts/"
|
|
96
102
|
)
|
|
97
103
|
|
|
98
104
|
def _resolve_model_dir(self, model_path: Optional[str]) -> str:
|
|
99
|
-
"""Locate directory containing config.json and *.ncnn.bin files."""
|
|
105
|
+
"""Locate directory containing config.json and *.ncnn.bin files with tier awareness."""
|
|
100
106
|
search_dirs: List[Path] = []
|
|
101
107
|
if model_path:
|
|
102
108
|
p = Path(model_path).expanduser().resolve()
|
|
@@ -104,20 +110,32 @@ class VulkanNeuralEngine:
|
|
|
104
110
|
raise TTSModelLoadError(f"[FAIL-FAST] Explicit model path not found: '{model_path}'")
|
|
105
111
|
search_dirs = [p]
|
|
106
112
|
else:
|
|
113
|
+
preferred_dir_keyword = "amy-medium" if self.model_tier == "medium" else "lessac-high"
|
|
114
|
+
|
|
115
|
+
# Prioritize tier-specific matching directories first
|
|
107
116
|
for s in self.STANDARD_MODEL_DIRS:
|
|
108
117
|
if s.exists():
|
|
109
|
-
|
|
118
|
+
if preferred_dir_keyword in s.name.lower():
|
|
119
|
+
search_dirs.insert(0, s)
|
|
120
|
+
else:
|
|
121
|
+
search_dirs.append(s)
|
|
110
122
|
for child in s.glob("ncnn-vits*"):
|
|
111
123
|
if child.is_dir():
|
|
112
|
-
|
|
124
|
+
if preferred_dir_keyword in child.name.lower():
|
|
125
|
+
search_dirs.insert(0, child)
|
|
126
|
+
else:
|
|
127
|
+
search_dirs.append(child)
|
|
113
128
|
|
|
114
129
|
for d in search_dirs:
|
|
115
130
|
if (d / "config.json").exists() and (d / "decoder.ncnn.bin").exists():
|
|
116
131
|
return str(d)
|
|
117
132
|
|
|
118
133
|
raise TTSModelLoadError(
|
|
119
|
-
"[
|
|
120
|
-
"
|
|
134
|
+
f"[ERROR: AMEVA-TTS-E001] No valid VITS NCNN model directory found (tier: '{self.model_tier}').\n"
|
|
135
|
+
f"Cause: Model weights not found in standard directories.\n"
|
|
136
|
+
f"Action Required: Run one-click provisioning via:\n"
|
|
137
|
+
f" $ termux-tts install --tier {self.model_tier}\n"
|
|
138
|
+
f"Documentation: https://uno-km.vercel.app/lib/tts/"
|
|
121
139
|
)
|
|
122
140
|
|
|
123
141
|
def synthesize(
|
|
@@ -140,18 +158,30 @@ class VulkanNeuralEngine:
|
|
|
140
158
|
temp_wav = tmp_file.name
|
|
141
159
|
|
|
142
160
|
try:
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
161
|
+
try:
|
|
162
|
+
from ameva_runtime.adapters.tts import TtsAdapter
|
|
163
|
+
cmd = TtsAdapter.build_cli_args(
|
|
164
|
+
executable=self.binary,
|
|
165
|
+
model_dir=self.model_dir,
|
|
166
|
+
text=clean_text,
|
|
167
|
+
output_filename=temp_wav,
|
|
168
|
+
threads=self.threads,
|
|
169
|
+
use_vulkan=True,
|
|
170
|
+
)
|
|
171
|
+
is_mali = "mali" in str(getattr(self, "gpu_device", "")).lower()
|
|
172
|
+
env = TtsAdapter.get_execution_env(is_mali=is_mali)
|
|
173
|
+
except Exception:
|
|
174
|
+
cmd = [
|
|
175
|
+
self.binary,
|
|
176
|
+
f"--vits-model-dir={self.model_dir}",
|
|
177
|
+
"--use-vulkan-compute=1",
|
|
178
|
+
f"--num-threads={self.threads}",
|
|
179
|
+
f"--output-filename={temp_wav}",
|
|
180
|
+
clean_text
|
|
181
|
+
]
|
|
182
|
+
env = os.environ.copy()
|
|
183
|
+
env["LD_LIBRARY_PATH"] = f"/system/lib64:{env.get('LD_LIBRARY_PATH', '')}"
|
|
184
|
+
env["AMEVA_VK_DSP_ACCEL"] = "1"
|
|
155
185
|
|
|
156
186
|
proc = subprocess.run(
|
|
157
187
|
cmd,
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Hardware abstraction gateway and dynamic ameva-runtime soft-dependency resolver for termux-tts.
|
|
3
|
+
Provides strict Zero-Silent-Fallback [AMEVA-TTS-E001] compliance, hardware-agnostic routing,
|
|
4
|
+
and seamless fallback to Tier 1 DSP Formant / Tier 2 Native voice when running standalone.
|
|
5
|
+
"""
|
|
6
|
+
from __future__ import annotations
|
|
7
|
+
|
|
8
|
+
import importlib.util
|
|
9
|
+
import logging
|
|
10
|
+
import sys
|
|
11
|
+
from typing import Optional, Tuple, Any
|
|
12
|
+
|
|
13
|
+
from .exceptions import VulkanInitializationError
|
|
14
|
+
|
|
15
|
+
logger = logging.getLogger("termux_tts.hardware")
|
|
16
|
+
|
|
17
|
+
ERROR_AMEVA_TTS_E001 = (
|
|
18
|
+
"[FAIL-FAST] [ERROR: AMEVA-TTS-E001] GPU acceleration requires 'ameva-runtime' and provisioned Vulkan assets.\n"
|
|
19
|
+
"Cause: Hardware acceleration runtime or native Vulkan engine is not installed.\n"
|
|
20
|
+
"Action Required: Run one-click provisioning via:\n"
|
|
21
|
+
" $ termux-tts install --tier high\n"
|
|
22
|
+
" (Or run without GPU: termux-tts synth -e dsp -t \"...\")\n"
|
|
23
|
+
"Documentation: https://uno-km.vercel.app/lib/tts/"
|
|
24
|
+
)
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def _resolve_ameva_runtime() -> Optional[Any]:
|
|
28
|
+
"""Check for ameva_runtime availability without top-level static dependency.
|
|
29
|
+
|
|
30
|
+
Returns the ameva_runtime module if installed, otherwise None.
|
|
31
|
+
"""
|
|
32
|
+
try:
|
|
33
|
+
spec = importlib.util.find_spec("ameva_runtime")
|
|
34
|
+
if spec is not None:
|
|
35
|
+
import ameva_runtime
|
|
36
|
+
return ameva_runtime
|
|
37
|
+
except (ImportError, AttributeError):
|
|
38
|
+
pass
|
|
39
|
+
return None
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def resolve_device_backend(
|
|
43
|
+
requested_device: str,
|
|
44
|
+
requested_engine: str = "auto",
|
|
45
|
+
) -> Tuple[str, str]:
|
|
46
|
+
"""Resolve user requested device/engine into (device, engine_type) with fail-fast compliance.
|
|
47
|
+
|
|
48
|
+
Returns:
|
|
49
|
+
Tuple[str, str]: (device, engine_type) e.g. ('vulkan', 'vulkan') or ('cpu', 'dsp')
|
|
50
|
+
"""
|
|
51
|
+
req_dev = (requested_device or "auto").lower().strip()
|
|
52
|
+
req_eng = (requested_engine or "auto").lower().strip()
|
|
53
|
+
ameva_mod = _resolve_ameva_runtime()
|
|
54
|
+
|
|
55
|
+
# Explicit GPU/Vulkan requested
|
|
56
|
+
if req_dev in ("vulkan", "gpu") or req_eng in ("vulkan", "gpu", "ncnn"):
|
|
57
|
+
if ameva_mod is None:
|
|
58
|
+
raise VulkanInitializationError(ERROR_AMEVA_TTS_E001)
|
|
59
|
+
|
|
60
|
+
# Check Vulkan doctor from ameva_runtime TtsAdapter
|
|
61
|
+
try:
|
|
62
|
+
from ameva_runtime.adapters import TtsAdapter
|
|
63
|
+
adapter = TtsAdapter()
|
|
64
|
+
report = adapter.resolve_diagnostic_report()
|
|
65
|
+
is_vk = getattr(report, "overall_success", False) or getattr(report, "recommended_backend", "") == "vulkan"
|
|
66
|
+
if not is_vk:
|
|
67
|
+
raise VulkanInitializationError(
|
|
68
|
+
f"[FAIL-FAST] [ERROR: AMEVA-TTS-E002] Vulkan hardware acceleration is not supported on this device.\n"
|
|
69
|
+
f"Cause: No usable Vulkan physical device or ICD driver library found.\n"
|
|
70
|
+
f"Action Required: Use CPU or DSP synthesis via '--device cpu' or '--engine dsp'."
|
|
71
|
+
)
|
|
72
|
+
except VulkanInitializationError:
|
|
73
|
+
raise
|
|
74
|
+
except Exception as e:
|
|
75
|
+
logger.debug("TtsAdapter diagnostic exception: %s", e)
|
|
76
|
+
raise VulkanInitializationError(
|
|
77
|
+
f"[FAIL-FAST] [ERROR: AMEVA-TTS-E002] Vulkan hardware acceleration check failed: {e}\n"
|
|
78
|
+
f"Action Required: Use CPU or DSP synthesis via '--device cpu' or '--engine dsp'."
|
|
79
|
+
) from e
|
|
80
|
+
|
|
81
|
+
return "vulkan", "vulkan"
|
|
82
|
+
|
|
83
|
+
# Auto mode: probe if ameva-runtime is available
|
|
84
|
+
if req_dev == "auto":
|
|
85
|
+
if ameva_mod is None:
|
|
86
|
+
sys.stdout.write(
|
|
87
|
+
"[INFO] ameva-runtime GPU engine is not provisioned. Operating in Tier 1 (Parametric DSP Formant) mode.\n"
|
|
88
|
+
)
|
|
89
|
+
sys.stdout.flush()
|
|
90
|
+
return "cpu", req_eng
|
|
91
|
+
|
|
92
|
+
try:
|
|
93
|
+
from ameva_runtime.adapters import TtsAdapter
|
|
94
|
+
adapter = TtsAdapter()
|
|
95
|
+
report = adapter.resolve_diagnostic_report()
|
|
96
|
+
is_vk = getattr(report, "overall_success", False) or getattr(report, "recommended_backend", "") == "vulkan"
|
|
97
|
+
bin_path = adapter.resolve_binary_path()
|
|
98
|
+
if is_vk and bin_path and req_eng in ("auto", "neural", "vits"):
|
|
99
|
+
return "vulkan", "vulkan"
|
|
100
|
+
except Exception as e:
|
|
101
|
+
logger.debug("TtsAdapter auto-routing probe exception: %s", e)
|
|
102
|
+
|
|
103
|
+
return "cpu", req_eng
|
|
104
|
+
|
|
105
|
+
# Explicit CPU
|
|
106
|
+
return "cpu", req_eng
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
def bind_tts_hardware(engine: Any, requested_device: str) -> Optional[Any]:
|
|
110
|
+
"""Safely invoke AMEVA-Runtime TtsAdapter if present to configure engine instance."""
|
|
111
|
+
ameva_mod = _resolve_ameva_runtime()
|
|
112
|
+
if ameva_mod is None:
|
|
113
|
+
return None
|
|
114
|
+
|
|
115
|
+
try:
|
|
116
|
+
from ameva_runtime.adapters.tts import TtsAdapter
|
|
117
|
+
binding = TtsAdapter.bind(engine=engine, requested_backend=requested_device)
|
|
118
|
+
return binding
|
|
119
|
+
except Exception as e:
|
|
120
|
+
logger.debug("Hardware adapter binding skipped: %s", e)
|
|
121
|
+
return None
|
|
@@ -1,49 +0,0 @@
|
|
|
1
|
-
"""
|
|
2
|
-
[DEPRECATED] This file is deprecated. 12-Stage Vulkan diagnostics are 100% delegated to ameva-runtime.
|
|
3
|
-
"""
|
|
4
|
-
from typing import Dict, Any
|
|
5
|
-
|
|
6
|
-
class VulkanDoctor:
|
|
7
|
-
"""Legacy compatibility bridge delegating directly to ameva-runtime."""
|
|
8
|
-
def __init__(self):
|
|
9
|
-
self.is_vulkan_available = False
|
|
10
|
-
self.ameva_runtime_bound = True
|
|
11
|
-
self.report = None
|
|
12
|
-
self._check_availability()
|
|
13
|
-
|
|
14
|
-
def _check_availability(self) -> None:
|
|
15
|
-
try:
|
|
16
|
-
from ameva_runtime import vulkan as avr
|
|
17
|
-
self.is_vulkan_available = bool(avr.is_available())
|
|
18
|
-
except Exception:
|
|
19
|
-
self.is_vulkan_available = False
|
|
20
|
-
|
|
21
|
-
def probe_all(self) -> Dict[str, Any]:
|
|
22
|
-
try:
|
|
23
|
-
from ameva_runtime import vulkan as avr
|
|
24
|
-
doc = avr.Doctor()
|
|
25
|
-
rep = doc.run_self_test(verbose=False)
|
|
26
|
-
self.report = rep
|
|
27
|
-
rec = getattr(rep, "recommended_backend", "")
|
|
28
|
-
passed = getattr(rep, "passed_stages", 0)
|
|
29
|
-
self.is_vulkan_available = bool(
|
|
30
|
-
getattr(rep, "overall_success", False)
|
|
31
|
-
or rec in ("vulkan", "vulkan_driver_only")
|
|
32
|
-
or passed >= 7
|
|
33
|
-
or avr.is_available()
|
|
34
|
-
)
|
|
35
|
-
device_name = getattr(rep, "device_name", None) or doc.quick_probe_device() or "Mali-G68"
|
|
36
|
-
return {
|
|
37
|
-
"overall_success": self.is_vulkan_available,
|
|
38
|
-
"passed_stages": passed,
|
|
39
|
-
"recommended_backend": rec or "vulkan",
|
|
40
|
-
"status": "BOUND_AMEVA_VULKAN",
|
|
41
|
-
"DeviceName": device_name,
|
|
42
|
-
"DeviceModel": device_name,
|
|
43
|
-
}
|
|
44
|
-
except Exception as e:
|
|
45
|
-
self.is_vulkan_available = False
|
|
46
|
-
return {"overall_success": False, "passed_stages": 0, "status": "FALLBACK_CPU", "error": str(e)}
|
|
47
|
-
|
|
48
|
-
def probe(self) -> Dict[str, Any]:
|
|
49
|
-
return self.probe_all()
|