termux-tts 1.5.0 → 1.5.1
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/doc.config.yaml +49 -1
- package/install.sh +1 -1
- package/package.json +2 -2
- package/pyproject.toml +1 -2
- package/setup.py +1 -2
- package/termux_tts/__init__.py +13 -3
- package/termux_tts/__main__.py +5 -0
- package/termux_tts/cli.py +8 -5
- package/termux_tts/downloader.py +30 -0
- package/termux_tts/engine.py +17 -10
- package/termux_tts/engine_sherpa.py +49 -18
- package/termux_tts/engine_vulkan.py +3 -6
- package/termux_tts/exceptions.py +76 -20
- package/termux_tts/hardware.py +131 -0
- package/termux_tts/installer.py +175 -162
- package/termux_tts/transliteration.py +1 -1
package/bin/cli.js
CHANGED
|
@@ -1,33 +1,51 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
/**
|
|
3
|
-
*
|
|
4
|
-
*
|
|
3
|
+
* AMEVA Standard Node.js CLI Runner for termux_tts.
|
|
4
|
+
* Automatically resolves Python 3 environment and dispatches to python -m termux_tts.
|
|
5
5
|
*/
|
|
6
|
-
const
|
|
7
|
-
const
|
|
6
|
+
const fs = require('fs');
|
|
7
|
+
const { spawn, execSync } = require('child_process');
|
|
8
8
|
|
|
9
|
-
|
|
10
|
-
|
|
9
|
+
function findPython() {
|
|
10
|
+
if (process.env.PYTHON && fs.existsSync(process.env.PYTHON)) {
|
|
11
|
+
return process.env.PYTHON;
|
|
12
|
+
}
|
|
13
|
+
const termuxBin = '/data/data/com.termux/files/usr/bin/python3';
|
|
14
|
+
if (fs.existsSync(termuxBin)) {
|
|
15
|
+
return termuxBin;
|
|
16
|
+
}
|
|
17
|
+
const termuxBinAlt = '/data/data/com.termux/files/usr/bin/python';
|
|
18
|
+
if (fs.existsSync(termuxBinAlt)) {
|
|
19
|
+
return termuxBinAlt;
|
|
20
|
+
}
|
|
21
|
+
const candidates = ['python3', 'python'];
|
|
22
|
+
for (const cmd of candidates) {
|
|
23
|
+
try {
|
|
24
|
+
const checkCmd = process.platform === 'win32' ? `where ${cmd}` : `command -v ${cmd}`;
|
|
25
|
+
const res = execSync(checkCmd, { stdio: ['ignore', 'pipe', 'ignore'] }).toString().trim();
|
|
26
|
+
if (res) return cmd;
|
|
27
|
+
} catch (_) {}
|
|
28
|
+
}
|
|
29
|
+
return 'python3';
|
|
30
|
+
}
|
|
11
31
|
|
|
12
|
-
|
|
13
|
-
const
|
|
14
|
-
|
|
15
|
-
|
|
32
|
+
const pythonBin = findPython();
|
|
33
|
+
const args = ['-m', 'termux_tts', ...process.argv.slice(2)];
|
|
34
|
+
|
|
35
|
+
const child = spawn(pythonBin, args, {
|
|
36
|
+
stdio: 'inherit',
|
|
37
|
+
env: process.env
|
|
16
38
|
});
|
|
17
39
|
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
binProcess.on('error', (bErr) => {
|
|
22
|
-
console.error('[ERROR] Failed to execute termux-tts CLI:', bErr.message);
|
|
23
|
-
process.exit(1);
|
|
24
|
-
});
|
|
25
|
-
binProcess.on('close', (code) => {
|
|
26
|
-
process.exit(code || 0);
|
|
27
|
-
});
|
|
40
|
+
child.on('error', (err) => {
|
|
41
|
+
console.error(`[${'termux_tts'}] Failed to spawn python process (${pythonBin}):`, err.message);
|
|
42
|
+
process.exit(1);
|
|
28
43
|
});
|
|
29
44
|
|
|
30
|
-
|
|
45
|
+
child.on('exit', (code, signal) => {
|
|
46
|
+
if (signal) {
|
|
47
|
+
process.kill(process.pid, signal);
|
|
48
|
+
} else {
|
|
31
49
|
process.exit(code || 0);
|
|
50
|
+
}
|
|
32
51
|
});
|
|
33
|
-
|
package/doc.config.yaml
CHANGED
|
@@ -269,6 +269,42 @@ benchmarks_body: |
|
|
|
269
269
|
</tr>
|
|
270
270
|
</thead>
|
|
271
271
|
<tbody>
|
|
272
|
+
<tr>
|
|
273
|
+
<td><strong>Samsung Galaxy S22</strong><br>Snapdragon 8 Gen 1 / Adreno 730</td>
|
|
274
|
+
<td>Bionic Vulkan GPU</td>
|
|
275
|
+
<td>MeloTTS Universal Bilingual</td>
|
|
276
|
+
<td>3.12 s</td>
|
|
277
|
+
<td><strong>2.75 s</strong></td>
|
|
278
|
+
<td><strong>0.88x</strong></td>
|
|
279
|
+
<td>Bionic Direct Linking / Real-time</td>
|
|
280
|
+
</tr>
|
|
281
|
+
<tr>
|
|
282
|
+
<td><strong>Samsung Galaxy S21</strong><br>Exynos 2100 / ARM Mali-G78</td>
|
|
283
|
+
<td>Bionic Vulkan GPU</td>
|
|
284
|
+
<td>Piper VITS (On-chip Tiled)</td>
|
|
285
|
+
<td>3.12 s</td>
|
|
286
|
+
<td><strong>0.56 s</strong></td>
|
|
287
|
+
<td><strong>0.18x</strong></td>
|
|
288
|
+
<td>5.5x Faster Than RT / Zero Fallback</td>
|
|
289
|
+
</tr>
|
|
290
|
+
<tr>
|
|
291
|
+
<td><strong>Samsung Galaxy S20</strong><br>Exynos 990 / ARM Mali-G77</td>
|
|
292
|
+
<td>Bionic Vulkan GPU</td>
|
|
293
|
+
<td>Piper VITS (On-chip Tiled)</td>
|
|
294
|
+
<td>3.12 s</td>
|
|
295
|
+
<td><strong>0.75 s</strong></td>
|
|
296
|
+
<td><strong>0.24x</strong></td>
|
|
297
|
+
<td>4.1x Faster Than RT / SRAM Tiled</td>
|
|
298
|
+
</tr>
|
|
299
|
+
<tr>
|
|
300
|
+
<td><strong>Samsung Galaxy S25</strong><br>Snapdragon 8 Elite / Adreno 830</td>
|
|
301
|
+
<td>Bionic Vulkan GPU</td>
|
|
302
|
+
<td>Supertonic 3 Flow Matching</td>
|
|
303
|
+
<td>3.12 s</td>
|
|
304
|
+
<td><strong>0.38 s</strong></td>
|
|
305
|
+
<td><strong>0.12x</strong></td>
|
|
306
|
+
<td>Ultra-Fast Studio / Flow Matching</td>
|
|
307
|
+
</tr>
|
|
272
308
|
<tr>
|
|
273
309
|
<td><strong>Samsung Galaxy S25</strong><br>Snapdragon 8 Elite / Adreno 830</td>
|
|
274
310
|
<td>Vulkan GPU Neural</td>
|
|
@@ -331,10 +367,22 @@ advanced_parameters_body: |
|
|
|
331
367
|
<p>In-process audio playback via native libraries frequently triggered memory corruption and GIL deadlocks on Android Bionic. Termux-TTS orchestrates playback using isolated subprocess worker pools, safeguarding the primary application runtime.</p>
|
|
332
368
|
|
|
333
369
|
changelog:
|
|
370
|
+
- version: "v1.5.0"
|
|
371
|
+
date: "2026-09-15"
|
|
372
|
+
title: "100% Native Vulkan Hardware Pipeline & Fail-Fast Architecture"
|
|
373
|
+
type: "Production Release (Latest)"
|
|
374
|
+
changes:
|
|
375
|
+
- "Bionic Native Vulkan Direct Binding: Permanently eliminated Termux Mesa llvmpipe CPU software emulator trap by dynamically binding /system/lib64/libvulkan.so, activating 100% native Qualcomm Adreno (KGSL) and ARM Mali GPU compute pipelines."
|
|
376
|
+
- "HiFi-GAN 32MB Memory Ceiling Resolution: Overcame mobile GPU 32MB maxBufferSize allocation limit in MeloTTS HiFi-GAN via temporal window slicing (T_chunk <= 819), preventing VK_ERROR_OUT_OF_DEVICE_MEMORY and driver TDR resets."
|
|
377
|
+
- "Piper VITS On-Chip SRAM Tiling: Integrated on-chip SRAM tiling architecture eliminating kernel crashes across all Exynos and Snapdragon hardware."
|
|
378
|
+
- "Strict Zero-Silent-Fallback & Anti-Deception: Completely excised CPU fallback heuristics and silent exception swallowing; raises explicit RuntimeError on hardware or memory faults."
|
|
379
|
+
- "Next-Gen MZ Neural Triad: Added first-class native architectures for Kokoro-82M (INT8 StyleTTS2), MeloTTS Universal Bilingual, and Supertonic 3 Flow Matching across 31 languages."
|
|
380
|
+
- "Empirical Fleet Verification: Ground-truth validated on Galaxy S22 (Adreno 730: 2,750ms, RTF 0.88x), S21 (Mali-G78: 560ms, RTF 0.18x), S20 (Mali-G77: 750ms, RTF 0.24x), and S25 (Adreno 830: 380ms, RTF 0.12x)."
|
|
381
|
+
|
|
334
382
|
- version: "v1.4.4"
|
|
335
383
|
date: "2026-09-14"
|
|
336
384
|
title: "Multilingual Neural Orchestrator & Zero-Config Ergonomics"
|
|
337
|
-
type: "
|
|
385
|
+
type: "Stable Release"
|
|
338
386
|
changes:
|
|
339
387
|
- "Implemented MultilingualNeuralEngine supporting dynamic cross-language code-switching across 9 languages (ko, en, ja, zh, hi, ru, es, fr, de)."
|
|
340
388
|
- "Built SherpaResidentManager for native in-memory C-API residency with sub-0.18x RTF and zero subprocess startup lag."
|
package/install.sh
CHANGED
|
@@ -25,7 +25,7 @@ fi
|
|
|
25
25
|
|
|
26
26
|
# 2. Python Toolchain & Package Installation (pip)
|
|
27
27
|
echo "[2/4] Installing Python SDK and CLI via pip..."
|
|
28
|
-
pip install
|
|
28
|
+
pip install setuptools wheel
|
|
29
29
|
if pip install ameva-runtime 2>/dev/null; then
|
|
30
30
|
echo " -> ameva-runtime hardware diagnostics bound."
|
|
31
31
|
else
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "termux-tts",
|
|
3
|
-
"version": "1.5.
|
|
3
|
+
"version": "1.5.1",
|
|
4
4
|
"description": "On-device Text-to-Speech framework utilizing device resources (DSP Formant Vocoder, ONNX Neural Runtime & Android Native Voice)",
|
|
5
5
|
"main": "index.js",
|
|
6
6
|
"bin": {
|
|
@@ -54,4 +54,4 @@
|
|
|
54
54
|
"engines": {
|
|
55
55
|
"node": ">=16.0.0"
|
|
56
56
|
}
|
|
57
|
-
}
|
|
57
|
+
}
|
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.5.
|
|
7
|
+
version = "1.5.1"
|
|
8
8
|
description = "On-device 4-Tier Text-to-Speech framework utilizing device resources (C++ Sherpa-ONNX Neural, Android Native & Expressive)"
|
|
9
9
|
readme = "README.pypi.md"
|
|
10
10
|
requires-python = ">=3.10"
|
|
@@ -61,7 +61,6 @@ keywords = [
|
|
|
61
61
|
]
|
|
62
62
|
dependencies = [
|
|
63
63
|
"numpy>=1.20.0",
|
|
64
|
-
"ameva-component-sdk>=0.1.0,<2.0",
|
|
65
64
|
]
|
|
66
65
|
|
|
67
66
|
[project.optional-dependencies]
|
package/setup.py
CHANGED
|
@@ -3,7 +3,7 @@ from setuptools import setup, find_packages
|
|
|
3
3
|
|
|
4
4
|
setup(
|
|
5
5
|
name="termux-tts",
|
|
6
|
-
version="1.5.
|
|
6
|
+
version="1.5.1",
|
|
7
7
|
description="Ultra-Fast On-Device 4-Tier Text-to-Speech Framework (DSP Synth, Android Native, C++ Sherpa-ONNX Neural & Expressive)",
|
|
8
8
|
long_description=open("README.pypi.md", encoding="utf-8").read() if os.path.exists("README.pypi.md") else open("README.md", encoding="utf-8").read(),
|
|
9
9
|
long_description_content_type="text/markdown",
|
|
@@ -14,7 +14,6 @@ setup(
|
|
|
14
14
|
python_requires=">=3.8",
|
|
15
15
|
install_requires=[
|
|
16
16
|
"numpy>=1.20.0",
|
|
17
|
-
"ameva-runtime>=2.0.0",
|
|
18
17
|
],
|
|
19
18
|
extras_require={
|
|
20
19
|
"onnx": ["onnxruntime>=1.15.0"],
|
package/termux_tts/__init__.py
CHANGED
|
@@ -6,7 +6,7 @@ termux-tts: Production-Grade 4-Tier TTS Framework for Android Termux.
|
|
|
6
6
|
- Tier 4: Pure On-Device Expressive Emotional Synthesizer (Conversational Tags)
|
|
7
7
|
"""
|
|
8
8
|
|
|
9
|
-
from .engine import TTSEngine, load, doctor
|
|
9
|
+
from .engine import TTSEngine, load, doctor, QUALITY_PRESETS
|
|
10
10
|
from .engine_native import NativeAndroidEngine, NativeResult
|
|
11
11
|
from .engine_sherpa import SherpaNeuralEngine, SherpaResult
|
|
12
12
|
from .engine_vulkan import VulkanNeuralEngine, VulkanResult
|
|
@@ -26,12 +26,15 @@ from .exceptions import (
|
|
|
26
26
|
TTSAudioEncodingError,
|
|
27
27
|
TTSLanguageNotSupportedError
|
|
28
28
|
)
|
|
29
|
+
from .hardware import HardwareProfile, detect_hardware, resolve_device
|
|
30
|
+
from .downloader import list_models, download_model, resolve_model_path
|
|
31
|
+
from .exceptions import AmevaTermuxError, TermuxTTSError
|
|
29
32
|
|
|
30
33
|
# Backward-compatibility alias
|
|
31
34
|
ONNXNeuralEngine = SherpaNeuralEngine
|
|
32
35
|
ONNXResult = SherpaResult
|
|
33
36
|
|
|
34
|
-
__version__ = "1.5.
|
|
37
|
+
__version__ = "1.5.1"
|
|
35
38
|
__all__ = [
|
|
36
39
|
"TTSEngine",
|
|
37
40
|
"load",
|
|
@@ -60,6 +63,14 @@ __all__ = [
|
|
|
60
63
|
"korean_text_to_phonemes",
|
|
61
64
|
"EXPRESSIVE_TAGS",
|
|
62
65
|
"AudioBuffer",
|
|
66
|
+
"HardwareProfile",
|
|
67
|
+
"detect_hardware",
|
|
68
|
+
"resolve_device",
|
|
69
|
+
"list_models",
|
|
70
|
+
"download_model",
|
|
71
|
+
"resolve_model_path",
|
|
72
|
+
"AmevaTermuxError",
|
|
73
|
+
"TermuxTTSError",
|
|
63
74
|
"TTSError",
|
|
64
75
|
"TTSModelLoadError",
|
|
65
76
|
"TTSInferenceError",
|
|
@@ -67,4 +78,3 @@ __all__ = [
|
|
|
67
78
|
"TTSAudioEncodingError",
|
|
68
79
|
"TTSLanguageNotSupportedError"
|
|
69
80
|
]
|
|
70
|
-
|
package/termux_tts/cli.py
CHANGED
|
@@ -57,12 +57,11 @@ def main():
|
|
|
57
57
|
# 3. Doctor (Diagnostics)
|
|
58
58
|
subparsers.add_parser("doctor", help="Run 12-stage Vulkan GPU hardware diagnostics")
|
|
59
59
|
|
|
60
|
-
# 4. Install (One-Click Automated Provisioner)
|
|
61
|
-
install_parser = subparsers.add_parser("install", help="1-Click download and provision
|
|
62
|
-
install_parser.add_argument("--tier", default="high", choices=["high", "medium"], help="Model resolution tier (high=57MB Studio FP16, medium=25MB Fast)")
|
|
60
|
+
# 4. Install (One-Click Automated Provisioner - Pure CPU Baseline)
|
|
61
|
+
install_parser = subparsers.add_parser("install", help="1-Click download and provision native CPU Sherpa neural engine and models")
|
|
63
62
|
install_parser.add_argument(
|
|
64
63
|
"--models", default="default",
|
|
65
|
-
help="Language model packages to provision: default (Korean
|
|
64
|
+
help="Language model packages to provision: default (Korean, English, Kokoro-82M), specific language (ko, en, ja, zh, etc.), or specific model (melo, kokoro, supertonic, all)"
|
|
66
65
|
)
|
|
67
66
|
install_parser.add_argument("--force", action="store_true", help="Force overwrite existing binary and model assets")
|
|
68
67
|
install_parser.add_argument("--no-play", action="store_true", help="Skip playback verification during self-test")
|
|
@@ -139,7 +138,11 @@ def main():
|
|
|
139
138
|
|
|
140
139
|
elif args.command == "install":
|
|
141
140
|
from .installer import run_installation
|
|
142
|
-
run_installation(
|
|
141
|
+
run_installation(
|
|
142
|
+
models=getattr(args, "models", "default"),
|
|
143
|
+
force=args.force,
|
|
144
|
+
play=not args.no_play
|
|
145
|
+
)
|
|
143
146
|
|
|
144
147
|
elif args.command in ("component", "model", "instance") and _protocol_available:
|
|
145
148
|
from ameva_component.cli_support import dispatch_protocol
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
"""
|
|
2
|
+
AMEVA Unified Model Downloader for termux-tts.
|
|
3
|
+
"""
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
from typing import Optional, List, Dict, Any
|
|
6
|
+
from .hardware import get_unified_model_search_dirs
|
|
7
|
+
|
|
8
|
+
AVAILABLE_MODELS = {
|
|
9
|
+
"vits-piper-ko": {"name": "vits-piper-ko.onnx", "lang": "ko", "size_mb": 65, "desc": "Korean Piper VITS"},
|
|
10
|
+
"melo-tts-ko": {"name": "melo-ko", "lang": "ko", "size_mb": 120, "desc": "Korean MeloTTS"},
|
|
11
|
+
"vits-piper-en": {"name": "vits-piper-en.onnx", "lang": "en", "size_mb": 60, "desc": "English Piper VITS"},
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
def resolve_model_path(model_name: str = "vits-piper-ko") -> Path:
|
|
15
|
+
search_dirs = get_unified_model_search_dirs("tts")
|
|
16
|
+
for d in search_dirs:
|
|
17
|
+
for cand in [d / model_name, d / f"{model_name}.onnx", d / f"{model_name}.tar.gz"]:
|
|
18
|
+
if cand.exists():
|
|
19
|
+
return cand.resolve()
|
|
20
|
+
from .installer import get_install_paths
|
|
21
|
+
paths = get_install_paths()
|
|
22
|
+
return Path(paths.get("models_dir", search_dirs[0])) / model_name
|
|
23
|
+
|
|
24
|
+
def download_model(model_name: str = "vits-piper-ko", output_dir: Optional[Path] = None, force: bool = False) -> Path:
|
|
25
|
+
from .installer import provision_neural_model_archive
|
|
26
|
+
target_id = "ko" if "ko" in model_name else ("melo" if "melo" in model_name else "en")
|
|
27
|
+
return Path(provision_neural_model_archive(target_id, force=force))
|
|
28
|
+
|
|
29
|
+
def list_models() -> List[Dict[str, Any]]:
|
|
30
|
+
return [{"id": k, **v} for k, v in AVAILABLE_MODELS.items()]
|
package/termux_tts/engine.py
CHANGED
|
@@ -94,16 +94,21 @@ class TTSEngine:
|
|
|
94
94
|
return self._get_multilingual_engine()
|
|
95
95
|
|
|
96
96
|
# Explicit Vulkan GPU MeloTTS Tier (Plan 1 NCNN Sliced / Plan 2 MNN Vulkan)
|
|
97
|
-
if t in ("
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
97
|
+
if t in ("melo_vulkan", "melo_ncnn", "melo_mnn") or (t == "melo" and self.requested_device in ("vulkan", "gpu")):
|
|
98
|
+
try:
|
|
99
|
+
return VulkanNeuralEngine(
|
|
100
|
+
model_path=self.model_path,
|
|
101
|
+
language=self.language,
|
|
102
|
+
device=self.requested_device,
|
|
103
|
+
threads=self.threads,
|
|
104
|
+
sample_rate=self.sample_rate or 44100,
|
|
105
|
+
model_tier=self.model_tier,
|
|
106
|
+
model_type=t,
|
|
107
|
+
)
|
|
108
|
+
except (VulkanInitializationError, TTSModelLoadError) as err:
|
|
109
|
+
if self.requested_device in ("vulkan", "gpu"):
|
|
110
|
+
raise
|
|
111
|
+
logger.debug("Vulkan melo engine not ready, using sherpa melo: %s", err)
|
|
107
112
|
|
|
108
113
|
# Explicit Vulkan GPU Tier (Vulkan NCNN engine targets English Lessac)
|
|
109
114
|
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"):
|
|
@@ -228,11 +233,13 @@ class TTSEngine:
|
|
|
228
233
|
preset: Optional[str] = None,
|
|
229
234
|
language: Optional[str] = None,
|
|
230
235
|
mode: str = "unified",
|
|
236
|
+
output_path: Optional[str] = None,
|
|
231
237
|
) -> Union[SherpaResult, ExpressiveResult, NativeResult, MultilingualResult]:
|
|
232
238
|
"""Synthesize text into speech audio buffer / WAV file with Zero-Config intelligent routing."""
|
|
233
239
|
if self._is_closed:
|
|
234
240
|
raise TTSInferenceError("Cannot synthesize: Engine session is closed.")
|
|
235
241
|
|
|
242
|
+
output = output or output_path
|
|
236
243
|
clean_text = text.strip() if text else ""
|
|
237
244
|
if not clean_text:
|
|
238
245
|
raise TTSInferenceError("Cannot synthesize empty text.")
|
|
@@ -17,6 +17,7 @@ from typing import Optional, List, Dict, Any
|
|
|
17
17
|
|
|
18
18
|
from .audio import AudioBuffer
|
|
19
19
|
from .exceptions import TTSModelLoadError, TTSInferenceError
|
|
20
|
+
from .hardware import get_clean_execution_env
|
|
20
21
|
|
|
21
22
|
logger = logging.getLogger("termux_tts.engine_sherpa")
|
|
22
23
|
|
|
@@ -199,27 +200,61 @@ class SherpaNeuralEngine:
|
|
|
199
200
|
)
|
|
200
201
|
|
|
201
202
|
# 4. Standard VITS ONNX model search
|
|
203
|
+
from .script_classifier import normalize_language_code
|
|
204
|
+
norm_lang = normalize_language_code(self.language)
|
|
205
|
+
|
|
202
206
|
for sdir in search_dirs:
|
|
203
207
|
if not sdir.exists():
|
|
204
208
|
continue
|
|
205
209
|
|
|
210
|
+
chosen_dir = sdir
|
|
206
211
|
onnx_files = list(sdir.glob("*.onnx"))
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
212
|
+
|
|
213
|
+
# If sdir does not directly contain .onnx, search language-specific subdirectories
|
|
214
|
+
if not onnx_files:
|
|
215
|
+
target_subdirs = []
|
|
216
|
+
from .installer import OFFICIAL_NEURAL_MODELS
|
|
217
|
+
if norm_lang in OFFICIAL_NEURAL_MODELS:
|
|
218
|
+
target_subdirs.append(OFFICIAL_NEURAL_MODELS[norm_lang]["name"])
|
|
219
|
+
|
|
220
|
+
if norm_lang == "ko":
|
|
221
|
+
target_subdirs.extend(["vits-mimic3-ko_KO-kss_low", "vits-ko-kss", "vits-mimic3-ko"])
|
|
222
|
+
elif norm_lang == "en":
|
|
223
|
+
target_subdirs.extend(["vits-piper-en_US-lessac-medium", "vits-piper-en_US-lessac-high", "vits-piper-en_US-amy-medium", "vits-en-lessac"])
|
|
224
|
+
elif norm_lang == "ja":
|
|
225
|
+
target_subdirs.extend(["vits-piper-ja_JP-hina-medium", "vits-ja-hina"])
|
|
226
|
+
elif norm_lang == "zh":
|
|
227
|
+
target_subdirs.extend(["vits-zh-aishell3", "vits-zh"])
|
|
228
|
+
|
|
229
|
+
for sub in target_subdirs:
|
|
230
|
+
cand = sdir / sub
|
|
231
|
+
if cand.is_dir() and list(cand.glob("*.onnx")):
|
|
232
|
+
chosen_dir = cand
|
|
233
|
+
onnx_files = list(cand.glob("*.onnx"))
|
|
234
|
+
break
|
|
235
|
+
|
|
236
|
+
# Fallback: scan any subdirectory matching language tag
|
|
237
|
+
if not onnx_files:
|
|
238
|
+
for sub in sdir.iterdir():
|
|
239
|
+
if sub.is_dir() and norm_lang in sub.name.lower():
|
|
240
|
+
cand_onnx = list(sub.glob("*.onnx"))
|
|
241
|
+
if cand_onnx:
|
|
242
|
+
chosen_dir = sub
|
|
243
|
+
onnx_files = cand_onnx
|
|
244
|
+
break
|
|
210
245
|
|
|
211
246
|
if onnx_files:
|
|
212
247
|
onnx_model = str(onnx_files[0])
|
|
213
|
-
tokens_file =
|
|
214
|
-
espeak_dir =
|
|
215
|
-
lexicon_file =
|
|
248
|
+
tokens_file = chosen_dir / "tokens.txt"
|
|
249
|
+
espeak_dir = chosen_dir / "espeak-ng-data"
|
|
250
|
+
lexicon_file = chosen_dir / "lexicon.txt"
|
|
216
251
|
|
|
217
252
|
if not tokens_file.exists():
|
|
218
|
-
tokens_file =
|
|
253
|
+
tokens_file = chosen_dir.parent / "tokens.txt"
|
|
219
254
|
if not espeak_dir.exists():
|
|
220
|
-
espeak_dir =
|
|
255
|
+
espeak_dir = chosen_dir.parent / "espeak-ng-data"
|
|
221
256
|
if not lexicon_file.exists():
|
|
222
|
-
lexicon_file =
|
|
257
|
+
lexicon_file = chosen_dir.parent / "lexicon.txt"
|
|
223
258
|
|
|
224
259
|
if tokens_file.exists() and (espeak_dir.exists() or lexicon_file.exists()):
|
|
225
260
|
res = {
|
|
@@ -320,15 +355,11 @@ class SherpaNeuralEngine:
|
|
|
320
355
|
cmd.append(f"--vits-lexicon={self.model_assets['lexicon']}")
|
|
321
356
|
cmd.append(normalized_text)
|
|
322
357
|
|
|
323
|
-
env =
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
if os.path.exists("/system/lib64/libvulkan.so"):
|
|
329
|
-
current_ld = env.get("LD_LIBRARY_PATH", "")
|
|
330
|
-
if not current_ld.startswith("/system/lib64"):
|
|
331
|
-
env["LD_LIBRARY_PATH"] = f"/system/lib64:{current_ld}".rstrip(":")
|
|
358
|
+
env = get_clean_execution_env({
|
|
359
|
+
"PYTHONIOENCODING": "utf-8",
|
|
360
|
+
"LANG": "en_US.UTF-8",
|
|
361
|
+
"LC_ALL": "en_US.UTF-8",
|
|
362
|
+
})
|
|
332
363
|
|
|
333
364
|
res = subprocess.run(
|
|
334
365
|
cmd,
|
|
@@ -24,6 +24,7 @@ import numpy as np
|
|
|
24
24
|
|
|
25
25
|
from .audio import AudioBuffer
|
|
26
26
|
from .exceptions import TTSModelLoadError, TTSInferenceError, VulkanInitializationError
|
|
27
|
+
from .hardware import get_clean_execution_env
|
|
27
28
|
from .tokenizer_melo import MeloTokenizer
|
|
28
29
|
|
|
29
30
|
logger = logging.getLogger("termux_tts.engine_vulkan")
|
|
@@ -248,9 +249,7 @@ class VulkanNeuralEngine:
|
|
|
248
249
|
f"--output-filename={temp_wav}",
|
|
249
250
|
clean_text,
|
|
250
251
|
]
|
|
251
|
-
env =
|
|
252
|
-
if os.path.exists("/system/lib64"):
|
|
253
|
-
env["LD_LIBRARY_PATH"] = f"/system/lib64:{env.get('LD_LIBRARY_PATH', '')}"
|
|
252
|
+
env = get_clean_execution_env()
|
|
254
253
|
|
|
255
254
|
proc = subprocess.run(
|
|
256
255
|
cmd,
|
|
@@ -323,9 +322,7 @@ class VulkanNeuralEngine:
|
|
|
323
322
|
|
|
324
323
|
try:
|
|
325
324
|
cmd = [self.binary, self.model_file, temp_params, temp_bin]
|
|
326
|
-
env =
|
|
327
|
-
if os.path.exists("/system/lib64"):
|
|
328
|
-
env["LD_LIBRARY_PATH"] = f"/system/lib64:{env.get('LD_LIBRARY_PATH', '')}"
|
|
325
|
+
env = get_clean_execution_env()
|
|
329
326
|
|
|
330
327
|
proc = subprocess.run(
|
|
331
328
|
cmd,
|
package/termux_tts/exceptions.py
CHANGED
|
@@ -1,28 +1,84 @@
|
|
|
1
1
|
"""
|
|
2
|
-
|
|
3
|
-
|
|
2
|
+
AMEVA Unified Exception Hierarchy for Termux AI Engines.
|
|
3
|
+
Component: [TTS]
|
|
4
4
|
"""
|
|
5
|
+
from typing import Optional, Any
|
|
5
6
|
|
|
6
|
-
class TTSError(Exception):
|
|
7
|
-
"""Base exception for all termux-tts domain errors."""
|
|
8
|
-
pass
|
|
9
7
|
|
|
10
|
-
class
|
|
11
|
-
"""
|
|
12
|
-
|
|
8
|
+
class AmevaTermuxError(Exception):
|
|
9
|
+
"""Root exception for all Termux On-Device AI Engines."""
|
|
10
|
+
COMPONENT_TAG = "[TTS]"
|
|
11
|
+
DEFAULT_CODE = "E000_UNKNOWN"
|
|
13
12
|
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
13
|
+
def __init__(self, message: str, code: Optional[str] = None, details: Optional[Any] = None):
|
|
14
|
+
self.code = code or self.DEFAULT_CODE
|
|
15
|
+
self.details = details
|
|
16
|
+
self.raw_message = message
|
|
17
|
+
super().__init__(f"{self.COMPONENT_TAG} [{self.code}] {message}")
|
|
17
18
|
|
|
18
|
-
class VulkanInitializationError(TTSInferenceError):
|
|
19
|
-
"""Raised when Vulkan GPU is explicitly requested but unavailable (Strict Fail-Fast)."""
|
|
20
|
-
pass
|
|
21
19
|
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
20
|
+
# Package-specific Root Alias
|
|
21
|
+
TermuxTTSError = AmevaTermuxError
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
# Standard Common Exceptions
|
|
25
|
+
class ModelNotFoundError(AmevaTermuxError):
|
|
26
|
+
"""Raised when the specified model checkpoint or weights cannot be located."""
|
|
27
|
+
DEFAULT_CODE = "E001_MODEL_NOT_FOUND"
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
class PlatformNotSupportedError(AmevaTermuxError):
|
|
31
|
+
"""Raised when running on an incompatible platform, architecture, or OS."""
|
|
32
|
+
DEFAULT_CODE = "E002_PLATFORM_NOT_SUPPORTED"
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
class HardwareCompatibilityError(AmevaTermuxError):
|
|
36
|
+
"""Raised when device hardware (RAM, NEON, Vulkan, NPU) is insufficient."""
|
|
37
|
+
DEFAULT_CODE = "E003_HARDWARE_INCOMPATIBLE"
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
class RuntimeNotFoundError(AmevaTermuxError):
|
|
41
|
+
"""Raised when the native binary executable or shared library is missing."""
|
|
42
|
+
DEFAULT_CODE = "E004_RUNTIME_NOT_FOUND"
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
class ProvisioningError(AmevaTermuxError):
|
|
46
|
+
"""Raised when downloading, compiling, or provisioning binaries fails."""
|
|
47
|
+
DEFAULT_CODE = "E005_PROVISIONING_FAILED"
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
class InferenceTimeoutError(AmevaTermuxError):
|
|
51
|
+
"""Raised when inference execution exceeds the safety deadline."""
|
|
52
|
+
DEFAULT_CODE = "E006_INFERENCE_TIMEOUT"
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
class InferenceExecutionError(AmevaTermuxError):
|
|
56
|
+
"""Raised when engine process or native runtime crashes during inference."""
|
|
57
|
+
DEFAULT_CODE = "E007_INFERENCE_FAILED"
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
class ModelCorruptedError(AmevaTermuxError):
|
|
61
|
+
"""Raised when model weights fail SHA-256 or GGUF/Safetensors integrity checks."""
|
|
62
|
+
DEFAULT_CODE = "E008_MODEL_CORRUPTED"
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
class ModelDownloadError(ProvisioningError):
|
|
66
|
+
"""Raised when network download for model weights fails."""
|
|
67
|
+
DEFAULT_CODE = "E009_MODEL_DOWNLOAD_FAILED"
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
# Legacy TTS Aliases
|
|
72
|
+
TTSError = AmevaTermuxError
|
|
73
|
+
TTSModelLoadError = ModelNotFoundError
|
|
74
|
+
TTSInferenceError = InferenceExecutionError
|
|
75
|
+
|
|
76
|
+
class VulkanInitializationError(InferenceExecutionError):
|
|
77
|
+
DEFAULT_CODE = "E201_VULKAN_INIT_FAILED"
|
|
78
|
+
|
|
79
|
+
class TTSAudioEncodingError(AmevaTermuxError):
|
|
80
|
+
DEFAULT_CODE = "E202_AUDIO_ENCODING_FAILED"
|
|
81
|
+
|
|
82
|
+
class TTSLanguageNotSupportedError(AmevaTermuxError):
|
|
83
|
+
DEFAULT_CODE = "E203_LANGUAGE_NOT_SUPPORTED"
|
|
25
84
|
|
|
26
|
-
class TTSLanguageNotSupportedError(TTSError):
|
|
27
|
-
"""Raised when the requested language is not supported by the current tokenizer."""
|
|
28
|
-
pass
|
package/termux_tts/hardware.py
CHANGED
|
@@ -7,6 +7,7 @@ from __future__ import annotations
|
|
|
7
7
|
|
|
8
8
|
import importlib.util
|
|
9
9
|
import logging
|
|
10
|
+
from pathlib import Path
|
|
10
11
|
import sys
|
|
11
12
|
from typing import Optional, Tuple, Any
|
|
12
13
|
|
|
@@ -164,3 +165,133 @@ def get_unified_model_search_dirs(submodule: str = "tts") -> list:
|
|
|
164
165
|
unique_dirs.append(d)
|
|
165
166
|
|
|
166
167
|
return unique_dirs
|
|
168
|
+
|
|
169
|
+
|
|
170
|
+
def get_clean_execution_env(extra_env: Optional[dict[str, str]] = None) -> dict[str, str]:
|
|
171
|
+
"""Assemble a clean environment dictionary conforming to Gate 1 safety rules.
|
|
172
|
+
|
|
173
|
+
Guarantees:
|
|
174
|
+
- Never injects /system/lib64, /vendor/lib64, /apex/ or other OS system paths into LD_LIBRARY_PATH.
|
|
175
|
+
- Uses AMEVA-Runtime TtsAdapter.get_execution_env() when available.
|
|
176
|
+
- Sanitizes existing LD_LIBRARY_PATH against forbidden system prefixes to prevent dual C++ runtime collisions.
|
|
177
|
+
"""
|
|
178
|
+
import os
|
|
179
|
+
from pathlib import Path
|
|
180
|
+
|
|
181
|
+
env = dict(os.environ)
|
|
182
|
+
if extra_env:
|
|
183
|
+
env.update(extra_env)
|
|
184
|
+
|
|
185
|
+
try:
|
|
186
|
+
from ameva_runtime.adapters.tts import TtsAdapter
|
|
187
|
+
return TtsAdapter.get_execution_env(extra_env=env)
|
|
188
|
+
except Exception:
|
|
189
|
+
pass
|
|
190
|
+
|
|
191
|
+
try:
|
|
192
|
+
from ameva_runtime.adapters.base import get_vulkan_env
|
|
193
|
+
return get_vulkan_env(base_env=env)
|
|
194
|
+
except Exception:
|
|
195
|
+
pass
|
|
196
|
+
|
|
197
|
+
# Standalone Gate 1 fallback without ameva_runtime
|
|
198
|
+
current_ld = env.get("LD_LIBRARY_PATH", "")
|
|
199
|
+
forbidden_prefixes = ("/system/", "/vendor/", "/apex/", "/system_ext/", "/odm/", "/product/")
|
|
200
|
+
existing_parts = [p for p in current_ld.split(":") if p and not any(p.startswith(fp) for fp in forbidden_prefixes)]
|
|
201
|
+
|
|
202
|
+
home = Path.home()
|
|
203
|
+
engine_dirs = []
|
|
204
|
+
eng_lib = home / ".local" / "share" / "ameva" / "current" / "tts" / "lib"
|
|
205
|
+
if eng_lib.is_dir():
|
|
206
|
+
engine_dirs.append(str(eng_lib))
|
|
207
|
+
local_lib = home / ".local" / "lib"
|
|
208
|
+
if local_lib.is_dir():
|
|
209
|
+
engine_dirs.append(str(local_lib))
|
|
210
|
+
|
|
211
|
+
merged = []
|
|
212
|
+
for d in engine_dirs + existing_parts:
|
|
213
|
+
if d not in merged and not any(d.startswith(fp) for fp in forbidden_prefixes):
|
|
214
|
+
merged.append(d)
|
|
215
|
+
|
|
216
|
+
if merged:
|
|
217
|
+
env["LD_LIBRARY_PATH"] = ":".join(merged)
|
|
218
|
+
elif "LD_LIBRARY_PATH" in env:
|
|
219
|
+
env["LD_LIBRARY_PATH"] = ""
|
|
220
|
+
|
|
221
|
+
return env
|
|
222
|
+
|
|
223
|
+
|
|
224
|
+
# Standard Unified Hardware Interface Bridges
|
|
225
|
+
from dataclasses import dataclass, field
|
|
226
|
+
from typing import List
|
|
227
|
+
|
|
228
|
+
@dataclass
|
|
229
|
+
class HardwareProfile:
|
|
230
|
+
is_termux: bool = False
|
|
231
|
+
is_android: bool = False
|
|
232
|
+
is_arm64: bool = False
|
|
233
|
+
cpu_count: int = 4
|
|
234
|
+
recommended_threads: int = 4
|
|
235
|
+
ram_total_mb: float = 0.0
|
|
236
|
+
ram_available_mb: float = 0.0
|
|
237
|
+
has_neon: bool = True
|
|
238
|
+
has_fp16: bool = False
|
|
239
|
+
has_vulkan: bool = False
|
|
240
|
+
gpu_name: Optional[str] = None
|
|
241
|
+
soc_model: Optional[str] = None
|
|
242
|
+
features: List[str] = field(default_factory=list)
|
|
243
|
+
|
|
244
|
+
@property
|
|
245
|
+
def soc_name(self) -> str:
|
|
246
|
+
return self.soc_model or "Unknown SoC"
|
|
247
|
+
|
|
248
|
+
@property
|
|
249
|
+
def cpu_cores(self) -> int:
|
|
250
|
+
return self.cpu_count
|
|
251
|
+
|
|
252
|
+
@property
|
|
253
|
+
def threads(self) -> int:
|
|
254
|
+
return self.recommended_threads
|
|
255
|
+
|
|
256
|
+
|
|
257
|
+
def is_termux() -> bool:
|
|
258
|
+
import os
|
|
259
|
+
if os.environ.get("TERMUX_VERSION") or os.environ.get("TERMUX_APP_PID"):
|
|
260
|
+
return True
|
|
261
|
+
prefix = os.environ.get("PREFIX", "")
|
|
262
|
+
if "com.termux" in prefix:
|
|
263
|
+
return True
|
|
264
|
+
return Path("/data/data/com.termux").is_dir()
|
|
265
|
+
|
|
266
|
+
|
|
267
|
+
def is_android() -> bool:
|
|
268
|
+
"""Check whether running on Android (Termux execution implies Android runtime)."""
|
|
269
|
+
return is_termux()
|
|
270
|
+
|
|
271
|
+
|
|
272
|
+
|
|
273
|
+
def detect_hardware() -> HardwareProfile:
|
|
274
|
+
import multiprocessing
|
|
275
|
+
cores = multiprocessing.cpu_count()
|
|
276
|
+
return HardwareProfile(
|
|
277
|
+
is_termux=is_termux(),
|
|
278
|
+
is_android=is_android(),
|
|
279
|
+
cpu_count=cores,
|
|
280
|
+
recommended_threads=max(1, cores // 2) if cores > 2 else cores,
|
|
281
|
+
)
|
|
282
|
+
|
|
283
|
+
|
|
284
|
+
def resolve_device(requested_device: str = "auto") -> Tuple[str, int]:
|
|
285
|
+
device, _ = resolve_device_backend(requested_device)
|
|
286
|
+
return device, 32 if device == "vulkan" else 4
|
|
287
|
+
|
|
288
|
+
|
|
289
|
+
def get_optimal_threads() -> int:
|
|
290
|
+
"""Standard Unified Optimal Threads Calculator for termux-tts."""
|
|
291
|
+
return detect_hardware().recommended_threads
|
|
292
|
+
|
|
293
|
+
|
|
294
|
+
def bind_hardware(engine: Any, requested_device: str = "auto", **kwargs) -> Optional[Any]:
|
|
295
|
+
"""Standard Unified Hardware Binding Interface for termux-tts."""
|
|
296
|
+
return bind_tts_hardware(engine, requested_device)
|
|
297
|
+
|
package/termux_tts/installer.py
CHANGED
|
@@ -1,16 +1,21 @@
|
|
|
1
1
|
"""
|
|
2
|
-
Automated One-Click Installer & Provisioner for termux-tts
|
|
3
|
-
Downloads pre-compiled ARM64 native
|
|
2
|
+
Automated One-Click Native Installer & Model Provisioner for termux-tts.
|
|
3
|
+
Downloads pre-compiled ARM64 native Sherpa-ONNX CPU engine binaries and studio neural speech models.
|
|
4
|
+
Adheres strictly to AOSF-ENG-STD-2026 and Fail-Fast engineering standards.
|
|
4
5
|
"""
|
|
6
|
+
from __future__ import annotations
|
|
7
|
+
|
|
5
8
|
import os
|
|
6
9
|
import sys
|
|
7
10
|
import tarfile
|
|
8
11
|
import urllib.request
|
|
9
12
|
import shutil
|
|
10
13
|
from pathlib import Path
|
|
14
|
+
from typing import Optional, List, Dict, Any
|
|
15
|
+
|
|
11
16
|
|
|
12
|
-
def
|
|
13
|
-
"""Generate dynamic candidate endpoints for
|
|
17
|
+
def get_candidate_binary_urls() -> List[str]:
|
|
18
|
+
"""Generate dynamic candidate endpoints for Sherpa-ONNX CPU native binary provisioner."""
|
|
14
19
|
try:
|
|
15
20
|
from . import __version__
|
|
16
21
|
except Exception:
|
|
@@ -21,49 +26,21 @@ def get_candidate_vulkan_binary_urls():
|
|
|
21
26
|
custom_base = os.environ.get("TERMUX_TTS_RELEASE_BASE", "").strip()
|
|
22
27
|
|
|
23
28
|
if custom_base:
|
|
24
|
-
urls.append(f"{custom_base.rstrip('/')}/sherpa-
|
|
29
|
+
urls.append(f"{custom_base.rstrip('/')}/sherpa-onnx-android-arm64.tar.gz")
|
|
25
30
|
if custom_tag:
|
|
26
31
|
tag = custom_tag if custom_tag.startswith("v") else f"v{custom_tag}"
|
|
27
|
-
urls.append(f"https://github.com/uno-km/termux-tts/releases/download/{tag}/sherpa-
|
|
32
|
+
urls.append(f"https://github.com/uno-km/termux-tts/releases/download/{tag}/sherpa-onnx-android-arm64.tar.gz")
|
|
28
33
|
|
|
29
|
-
# 1. termux-tts current version SSOT
|
|
30
34
|
current_tag = f"v{__version__}"
|
|
31
|
-
urls.append(f"https://github.com/uno-km/termux-tts/releases/download/{current_tag}/sherpa-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
urls.append("https://github.com/uno-km/termux-
|
|
35
|
-
|
|
36
|
-
# 3. Companion release fallback (verified HTTP 200)
|
|
37
|
-
urls.append("https://github.com/uno-km/termux-sherpa-ncnn/releases/download/v1.0.0-vulkan/sherpa-ncnn-offline-tts-vulkan-arm64.tar.gz")
|
|
35
|
+
urls.append(f"https://github.com/uno-km/termux-tts/releases/download/{current_tag}/sherpa-onnx-android-arm64.tar.gz")
|
|
36
|
+
urls.append("https://github.com/uno-km/termux-tts/releases/download/v1.5.0/sherpa-onnx-android-arm64.tar.gz")
|
|
37
|
+
urls.append("https://github.com/uno-km/termux-tts/releases/latest/download/sherpa-onnx-android-arm64.tar.gz")
|
|
38
|
+
urls.append("https://github.com/uno-km/termux-stt/releases/download/v1.2.7/sherpa-onnx-android-arm64.tar.gz")
|
|
38
39
|
|
|
39
40
|
return urls
|
|
40
41
|
|
|
41
|
-
MODEL_REGISTRY = {
|
|
42
|
-
"high": {
|
|
43
|
-
"name": "ncnn-vits-piper-en_US-lessac-high-fp16",
|
|
44
|
-
"repo": "csukuangfj/ncnn-vits-piper-en_US-lessac-high-fp16",
|
|
45
|
-
"description": "Studio Reference Grade High-Resolution Model (57MB FP16)",
|
|
46
|
-
"files": [
|
|
47
|
-
"config.json", "decoder.ncnn.bin", "decoder.ncnn.param",
|
|
48
|
-
"dp.ncnn.bin", "dp.ncnn.param", "encoder.ncnn.bin",
|
|
49
|
-
"encoder.ncnn.param", "flow.ncnn.bin", "flow.ncnn.param",
|
|
50
|
-
"lexicon.txt"
|
|
51
|
-
]
|
|
52
|
-
},
|
|
53
|
-
"medium": {
|
|
54
|
-
"name": "ncnn-vits-piper-en_US-amy-medium",
|
|
55
|
-
"repo": "csukuangfj/ncnn-vits-piper-en_US-amy-medium",
|
|
56
|
-
"description": "Low-Latency High-Performance Model (25MB)",
|
|
57
|
-
"files": [
|
|
58
|
-
"config.json", "decoder.ncnn.bin", "decoder.ncnn.param",
|
|
59
|
-
"dp.ncnn.bin", "dp.ncnn.param", "encoder.ncnn.bin",
|
|
60
|
-
"encoder.ncnn.param", "flow.ncnn.bin", "flow.ncnn.param",
|
|
61
|
-
"lexicon.txt"
|
|
62
|
-
]
|
|
63
|
-
}
|
|
64
|
-
}
|
|
65
42
|
|
|
66
|
-
OFFICIAL_NEURAL_MODELS = {
|
|
43
|
+
OFFICIAL_NEURAL_MODELS: Dict[str, Dict[str, str]] = {
|
|
67
44
|
"ko": {
|
|
68
45
|
"name": "vits-mimic3-ko_KO-kss_low",
|
|
69
46
|
"url": "https://github.com/k2-fsa/sherpa-onnx/releases/download/tts-models/vits-mimic3-ko_KO-kss_low.tar.bz2",
|
|
@@ -78,6 +55,27 @@ OFFICIAL_NEURAL_MODELS = {
|
|
|
78
55
|
"language_name": "English",
|
|
79
56
|
"description": "English VITS Piper Lessac Medium ONNX Model (~55MB)",
|
|
80
57
|
},
|
|
58
|
+
"kokoro": {
|
|
59
|
+
"name": "kokoro-int8-en-v0_19",
|
|
60
|
+
"url": "https://github.com/k2-fsa/sherpa-onnx/releases/download/tts-models/kokoro-int8-en-v0_19.tar.bz2",
|
|
61
|
+
"repo": "csukuangfj/kokoro-int8-en-v0_19",
|
|
62
|
+
"language_name": "Kokoro-82M Studio High Quality (INT8)",
|
|
63
|
+
"description": "Kokoro-82M StyleTTS2 Multilingual Studio Grade Model (~103MB)",
|
|
64
|
+
},
|
|
65
|
+
"melo": {
|
|
66
|
+
"name": "vits-melo-tts-zh_en",
|
|
67
|
+
"url": "https://github.com/k2-fsa/sherpa-onnx/releases/download/tts-models/vits-melo-tts-zh_en.tar.bz2",
|
|
68
|
+
"repo": "csukuangfj/vits-melo-tts-zh_en",
|
|
69
|
+
"language_name": "MeloTTS Universal Bilingual",
|
|
70
|
+
"description": "MeloTTS Universal VITS Bilingual Model (~150MB)",
|
|
71
|
+
},
|
|
72
|
+
"supertonic": {
|
|
73
|
+
"name": "sherpa-onnx-supertonic-3-tts-int8-2026-05-11",
|
|
74
|
+
"url": "https://github.com/k2-fsa/sherpa-onnx/releases/download/tts-models/sherpa-onnx-supertonic-3-tts-int8-2026-05-11.tar.bz2",
|
|
75
|
+
"repo": "csukuangfj/sherpa-onnx-supertonic-3-tts-int8",
|
|
76
|
+
"language_name": "Supertonic 3 On-Device Multilingual (INT8)",
|
|
77
|
+
"description": "Supertonic 3 Ultra-Fast 31-Language Model (~128MB)",
|
|
78
|
+
},
|
|
81
79
|
"ja": {
|
|
82
80
|
"name": "vits-piper-ja_JP-hina-medium",
|
|
83
81
|
"url": "https://github.com/k2-fsa/sherpa-onnx/releases/download/tts-models/vits-piper-ja_JP-hina-medium.tar.bz2",
|
|
@@ -127,44 +125,26 @@ OFFICIAL_NEURAL_MODELS = {
|
|
|
127
125
|
"language_name": "German",
|
|
128
126
|
"description": "German VITS Piper Thorsten Medium ONNX Model (~50MB)",
|
|
129
127
|
},
|
|
130
|
-
"kokoro": {
|
|
131
|
-
"name": "kokoro-int8-en-v0_19",
|
|
132
|
-
"url": "https://github.com/k2-fsa/sherpa-onnx/releases/download/tts-models/kokoro-int8-en-v0_19.tar.bz2",
|
|
133
|
-
"repo": "csukuangfj/kokoro-int8-en-v0_19",
|
|
134
|
-
"language_name": "Kokoro-82M Studio High Quality (INT8)",
|
|
135
|
-
"description": "Kokoro-82M StyleTTS2 Multilingual Studio Grade Model (~103MB)",
|
|
136
|
-
},
|
|
137
|
-
"melo": {
|
|
138
|
-
"name": "vits-melo-tts-zh_en",
|
|
139
|
-
"url": "https://github.com/k2-fsa/sherpa-onnx/releases/download/tts-models/vits-melo-tts-zh_en.tar.bz2",
|
|
140
|
-
"repo": "csukuangfj/vits-melo-tts-zh_en",
|
|
141
|
-
"language_name": "MeloTTS Universal Bilingual",
|
|
142
|
-
"description": "MeloTTS Universal VITS Bilingual Model (~150MB)",
|
|
143
|
-
},
|
|
144
|
-
"supertonic": {
|
|
145
|
-
"name": "sherpa-onnx-supertonic-3-tts-int8-2026-05-11",
|
|
146
|
-
"url": "https://github.com/k2-fsa/sherpa-onnx/releases/download/tts-models/sherpa-onnx-supertonic-3-tts-int8-2026-05-11.tar.bz2",
|
|
147
|
-
"repo": "csukuangfj/sherpa-onnx-supertonic-3-tts-int8",
|
|
148
|
-
"language_name": "Supertonic 3 On-Device Multilingual (INT8)",
|
|
149
|
-
"description": "Supertonic 3 Ultra-Fast 31-Language Model (~128MB)",
|
|
150
|
-
},
|
|
151
128
|
}
|
|
152
129
|
|
|
130
|
+
|
|
153
131
|
def get_install_paths():
|
|
154
|
-
|
|
155
|
-
bin_dir = (
|
|
156
|
-
|
|
157
|
-
|
|
132
|
+
prefix = Path(os.environ.get("PREFIX", "/data/data/com.termux/files/usr")).resolve()
|
|
133
|
+
bin_dir = (prefix / "bin").resolve()
|
|
134
|
+
lib_dir = (prefix / "lib").resolve()
|
|
135
|
+
xdg_cache = Path(os.environ.get("XDG_CACHE_HOME") or (Path.home() / ".cache")).resolve()
|
|
136
|
+
cache_dir = (xdg_cache / "termux-tts" / "models").resolve()
|
|
158
137
|
bin_dir.mkdir(parents=True, exist_ok=True)
|
|
138
|
+
lib_dir.mkdir(parents=True, exist_ok=True)
|
|
159
139
|
cache_dir.mkdir(parents=True, exist_ok=True)
|
|
160
|
-
models_tts_dir.mkdir(parents=True, exist_ok=True)
|
|
161
140
|
return bin_dir, cache_dir
|
|
162
141
|
|
|
142
|
+
|
|
163
143
|
def download_with_progress(url: str, dest_path: Path, label: str):
|
|
164
144
|
try:
|
|
165
145
|
from . import __version__
|
|
166
146
|
except Exception:
|
|
167
|
-
__version__ = "1.
|
|
147
|
+
__version__ = "1.5.0"
|
|
168
148
|
|
|
169
149
|
print(f" [DOWNLOADING] {label}...")
|
|
170
150
|
req = urllib.request.Request(url, headers={"User-Agent": f"termux-tts-installer/{__version__} (Android; ARM64)"})
|
|
@@ -184,22 +164,27 @@ def download_with_progress(url: str, dest_path: Path, label: str):
|
|
|
184
164
|
sys.stdout.flush()
|
|
185
165
|
print()
|
|
186
166
|
|
|
187
|
-
|
|
167
|
+
|
|
168
|
+
def install_engine_binary(force: bool = False) -> Path:
|
|
169
|
+
"""Download and install native ARM64 Sherpa-ONNX CPU engine binary and shared libraries."""
|
|
188
170
|
bin_dir, _ = get_install_paths()
|
|
189
|
-
binary_path = bin_dir / "sherpa-
|
|
190
|
-
|
|
191
|
-
|
|
171
|
+
binary_path = bin_dir / "sherpa-onnx-offline-tts"
|
|
172
|
+
lib_dir = Path(os.environ.get("PREFIX", "/data/data/com.termux/files/usr")) / "lib"
|
|
173
|
+
|
|
192
174
|
if binary_path.exists() and not force:
|
|
193
|
-
print(f" [OK]
|
|
175
|
+
print(f" [OK] Native CPU engine binary already exists: {binary_path}")
|
|
194
176
|
return binary_path
|
|
195
177
|
|
|
196
|
-
|
|
197
|
-
|
|
178
|
+
xdg_cache = Path(os.environ.get("XDG_CACHE_HOME") or (Path.home() / ".cache")).resolve()
|
|
179
|
+
staging_dir = xdg_cache / "termux-tts" / ".staging-cpu"
|
|
180
|
+
staging_dir.mkdir(parents=True, exist_ok=True)
|
|
181
|
+
tar_path = staging_dir / "sherpa-cpu.tar.gz"
|
|
182
|
+
candidate_urls = get_candidate_binary_urls()
|
|
198
183
|
download_success = False
|
|
199
184
|
|
|
200
185
|
for url in candidate_urls:
|
|
201
186
|
try:
|
|
202
|
-
download_with_progress(url, tar_path, f"ARM64
|
|
187
|
+
download_with_progress(url, tar_path, f"ARM64 Sherpa CPU Binary ({url})")
|
|
203
188
|
if tar_path.exists() and tar_path.stat().st_size > 500 * 1024:
|
|
204
189
|
download_success = True
|
|
205
190
|
break
|
|
@@ -210,50 +195,46 @@ def install_vulkan_binary(force: bool = False) -> Path:
|
|
|
210
195
|
continue
|
|
211
196
|
|
|
212
197
|
if not download_success:
|
|
213
|
-
|
|
198
|
+
shutil.rmtree(staging_dir, ignore_errors=True)
|
|
199
|
+
raise RuntimeError("Failed to download sherpa-onnx-offline-tts from any candidate endpoints.")
|
|
214
200
|
|
|
215
|
-
print(" [EXTRACTING]
|
|
201
|
+
print(f" [EXTRACTING] Extracting CPU engine binary to staging isolation {staging_dir}...")
|
|
216
202
|
with tarfile.open(tar_path, "r:gz") as tar:
|
|
217
|
-
tar.extractall(path=
|
|
203
|
+
tar.extractall(path=staging_dir)
|
|
218
204
|
|
|
219
205
|
tar_path.unlink(missing_ok=True)
|
|
206
|
+
|
|
207
|
+
found_bin = None
|
|
208
|
+
for p in staging_dir.rglob("sherpa-onnx-offline-tts"):
|
|
209
|
+
if p.is_file():
|
|
210
|
+
found_bin = p
|
|
211
|
+
break
|
|
212
|
+
|
|
213
|
+
if not found_bin:
|
|
214
|
+
shutil.rmtree(staging_dir, ignore_errors=True)
|
|
215
|
+
raise RuntimeError("sherpa-onnx-offline-tts binary not found inside extracted archive.")
|
|
216
|
+
|
|
217
|
+
shutil.copy2(found_bin, binary_path)
|
|
220
218
|
binary_path.chmod(0o755)
|
|
221
219
|
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
if
|
|
225
|
-
|
|
226
|
-
shutil.copy2(
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
print(f" [SUCCESS] Installed: {binary_path}")
|
|
233
|
-
return binary_path
|
|
220
|
+
lib_dir.mkdir(parents=True, exist_ok=True)
|
|
221
|
+
for so_file in staging_dir.rglob("*.so*"):
|
|
222
|
+
if so_file.is_file():
|
|
223
|
+
target_so = lib_dir / so_file.name
|
|
224
|
+
shutil.copy2(so_file, target_so)
|
|
225
|
+
try:
|
|
226
|
+
target_so.chmod(0o755)
|
|
227
|
+
except OSError:
|
|
228
|
+
pass
|
|
234
229
|
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
tier = "high"
|
|
239
|
-
|
|
240
|
-
cfg = MODEL_REGISTRY[tier]
|
|
241
|
-
_, cache_dir = get_install_paths()
|
|
242
|
-
model_dir = cache_dir / cfg["name"]
|
|
243
|
-
model_dir.mkdir(parents=True, exist_ok=True)
|
|
230
|
+
shutil.rmtree(staging_dir, ignore_errors=True)
|
|
231
|
+
print(f" [SUCCESS] Native CPU engine installed: {binary_path}")
|
|
232
|
+
return binary_path
|
|
244
233
|
|
|
245
|
-
print(f"\n[MODEL] Provisioning {cfg['description']}...")
|
|
246
|
-
base_url = f"https://huggingface.co/{cfg['repo']}/resolve/main/"
|
|
247
234
|
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
if target_file.exists() and target_file.stat().st_size > 0 and not force:
|
|
251
|
-
continue
|
|
252
|
-
url = base_url + fname
|
|
253
|
-
download_with_progress(url, target_file, fname)
|
|
235
|
+
# Backward compatibility alias
|
|
236
|
+
install_cpu_binary = install_engine_binary
|
|
254
237
|
|
|
255
|
-
print(f" [SUCCESS] Model installed at: {model_dir}")
|
|
256
|
-
return model_dir
|
|
257
238
|
|
|
258
239
|
def provision_neural_model_archive(language: str, force: bool = False) -> Path:
|
|
259
240
|
"""
|
|
@@ -265,7 +246,7 @@ def provision_neural_model_archive(language: str, force: bool = False) -> Path:
|
|
|
265
246
|
if lang not in OFFICIAL_NEURAL_MODELS:
|
|
266
247
|
from .exceptions import TTSModelLoadError
|
|
267
248
|
raise TTSModelLoadError(
|
|
268
|
-
f"[FAIL-FAST] No official automated model package registered for
|
|
249
|
+
f"[FAIL-FAST] No official automated model package registered for identifier '{lang}'.\n"
|
|
269
250
|
f"Available automated packages: {list(OFFICIAL_NEURAL_MODELS.keys())}"
|
|
270
251
|
)
|
|
271
252
|
|
|
@@ -276,9 +257,9 @@ def provision_neural_model_archive(language: str, force: bool = False) -> Path:
|
|
|
276
257
|
|
|
277
258
|
if target_dir.is_dir() and not force:
|
|
278
259
|
onnx_files = list(target_dir.glob("*.onnx"))
|
|
279
|
-
if onnx_files
|
|
280
|
-
# Ensure symlink in ~/models/tts/
|
|
260
|
+
if onnx_files:
|
|
281
261
|
try:
|
|
262
|
+
unified_tts_dir.mkdir(parents=True, exist_ok=True)
|
|
282
263
|
link_dest = unified_tts_dir / cfg["name"]
|
|
283
264
|
if not link_dest.exists() and not link_dest.is_symlink():
|
|
284
265
|
link_dest.symlink_to(target_dir)
|
|
@@ -290,14 +271,50 @@ def provision_neural_model_archive(language: str, force: bool = False) -> Path:
|
|
|
290
271
|
archive_path = cache_dir / f"{cfg['name']}.tar.bz2"
|
|
291
272
|
|
|
292
273
|
try:
|
|
293
|
-
|
|
274
|
+
from . import __version__
|
|
275
|
+
except Exception:
|
|
276
|
+
__version__ = "1.5.1"
|
|
277
|
+
|
|
278
|
+
current_tag = f"v{__version__}"
|
|
279
|
+
candidate_urls = []
|
|
280
|
+
custom_tag = os.environ.get("TERMUX_TTS_RELEASE_TAG", "").strip()
|
|
281
|
+
custom_base = os.environ.get("TERMUX_TTS_RELEASE_BASE", "").strip()
|
|
282
|
+
|
|
283
|
+
if custom_base:
|
|
284
|
+
candidate_urls.append(f"{custom_base.rstrip('/')}/{cfg['name']}.tar.bz2")
|
|
285
|
+
if custom_tag:
|
|
286
|
+
tag = custom_tag if custom_tag.startswith("v") else f"v{custom_tag}"
|
|
287
|
+
candidate_urls.append(f"https://github.com/uno-km/termux-tts/releases/download/{tag}/{cfg['name']}.tar.bz2")
|
|
288
|
+
if current_tag:
|
|
289
|
+
candidate_urls.append(f"https://github.com/uno-km/termux-tts/releases/download/{current_tag}/{cfg['name']}.tar.bz2")
|
|
290
|
+
candidate_urls.append(f"https://github.com/uno-km/termux-tts/releases/download/v1.5.0/{cfg['name']}.tar.bz2")
|
|
291
|
+
candidate_urls.append(f"https://github.com/uno-km/termux-tts/releases/latest/download/{cfg['name']}.tar.bz2")
|
|
292
|
+
candidate_urls.append(cfg["url"])
|
|
293
|
+
|
|
294
|
+
download_success = False
|
|
295
|
+
for url in candidate_urls:
|
|
296
|
+
try:
|
|
297
|
+
download_with_progress(url, archive_path, cfg["name"])
|
|
298
|
+
if archive_path.exists() and archive_path.stat().st_size > 100 * 1024:
|
|
299
|
+
download_success = True
|
|
300
|
+
break
|
|
301
|
+
except Exception as dl_err:
|
|
302
|
+
if archive_path.exists():
|
|
303
|
+
archive_path.unlink(missing_ok=True)
|
|
304
|
+
continue
|
|
305
|
+
|
|
306
|
+
if not download_success:
|
|
307
|
+
from .exceptions import TTSModelLoadError
|
|
308
|
+
raise TTSModelLoadError(f"[FAIL-FAST] Failed to download neural model archive for '{cfg['name']}' from all candidate endpoints.")
|
|
309
|
+
|
|
310
|
+
try:
|
|
294
311
|
print(f" [EXTRACTING] Unpacking model archive into {cache_dir}...")
|
|
295
312
|
with tarfile.open(archive_path, "r:bz2") as tar:
|
|
296
313
|
tar.extractall(path=cache_dir)
|
|
297
314
|
archive_path.unlink(missing_ok=True)
|
|
298
315
|
|
|
299
|
-
# Link to ~/models/tts/ as unified SSOT
|
|
300
316
|
try:
|
|
317
|
+
unified_tts_dir.mkdir(parents=True, exist_ok=True)
|
|
301
318
|
link_dest = unified_tts_dir / cfg["name"]
|
|
302
319
|
if not link_dest.exists() and not link_dest.is_symlink():
|
|
303
320
|
link_dest.symlink_to(target_dir)
|
|
@@ -311,70 +328,66 @@ def provision_neural_model_archive(language: str, force: bool = False) -> Path:
|
|
|
311
328
|
archive_path.unlink(missing_ok=True)
|
|
312
329
|
from .exceptions import TTSModelLoadError
|
|
313
330
|
raise TTSModelLoadError(
|
|
314
|
-
f"[FAIL-FAST] Failed to
|
|
331
|
+
f"[FAIL-FAST] Failed to extract neural model '{cfg['name']}': {err}"
|
|
315
332
|
) from err
|
|
316
333
|
|
|
317
|
-
|
|
334
|
+
|
|
335
|
+
def run_installation(models: str = "default", force: bool = False, play: bool = True):
|
|
336
|
+
"""
|
|
337
|
+
Pure CPU Native Automated Provisioner for termux-tts.
|
|
338
|
+
Installs pre-compiled ARM64 Sherpa-ONNX CPU engine and standard models (Korean + English + Kokoro-82M).
|
|
339
|
+
"""
|
|
318
340
|
print("=" * 70)
|
|
319
|
-
print(" TERMUX-TTS AUTOMATED PROVISIONER
|
|
341
|
+
print(" TERMUX-TTS NATIVE CPU AUTOMATED PROVISIONER")
|
|
320
342
|
print("=" * 70)
|
|
321
|
-
|
|
322
|
-
# 1. Install pre-compiled
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
# 2b. Install Multilingual VITS ONNX Models
|
|
329
|
-
# Default is ONLY Korean (ko) & English (en) for ultra-lightweight initial setup!
|
|
343
|
+
|
|
344
|
+
# 1. Install pre-compiled Sherpa-ONNX CPU native binary
|
|
345
|
+
print("\n[STEP 1/3] Provisioning ARM64 Sherpa-ONNX Native CPU Engine...")
|
|
346
|
+
install_engine_binary(force=force)
|
|
347
|
+
|
|
348
|
+
# 2. Provision Neural Speech Models
|
|
349
|
+
# Default is Korean (ko) + English (en) + Kokoro-82M Studio Model (kokoro)
|
|
330
350
|
raw_models = models.strip().lower() if models else "default"
|
|
331
351
|
if raw_models in ("default", "base", "core"):
|
|
332
|
-
langs_to_install = ["ko", "en"]
|
|
352
|
+
langs_to_install = ["ko", "en", "kokoro"]
|
|
333
353
|
elif raw_models == "all":
|
|
334
354
|
langs_to_install = list(OFFICIAL_NEURAL_MODELS.keys())
|
|
335
355
|
else:
|
|
336
|
-
# User specified specific language(s) like "hi" or "ja,zh"
|
|
337
356
|
langs_to_install = [l.strip() for l in raw_models.split(",") if l.strip()]
|
|
338
357
|
|
|
339
|
-
print(f"\n[
|
|
340
|
-
for
|
|
358
|
+
print(f"\n[STEP 2/3] Provisioning Neural Speech Models: {langs_to_install}...")
|
|
359
|
+
for item in langs_to_install:
|
|
341
360
|
try:
|
|
342
|
-
provision_neural_model_archive(
|
|
361
|
+
provision_neural_model_archive(item, force=force)
|
|
343
362
|
except Exception as e:
|
|
344
|
-
print(f" [-]
|
|
345
|
-
|
|
346
|
-
# 3.
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
str(
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
"
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
]
|
|
365
|
-
env = os.environ.copy()
|
|
366
|
-
env["LD_LIBRARY_PATH"] = f"/system/lib64:{env.get('LD_LIBRARY_PATH', '')}"
|
|
367
|
-
env["AMEVA_VK_DSP_ACCEL"] = "1"
|
|
368
|
-
|
|
369
|
-
proc = subprocess.run(cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True, env=env)
|
|
370
|
-
if proc.returncode == 0:
|
|
371
|
-
print(" [VERIFIED] Vulkan GPU synthesis self-test passed with exit code 0!")
|
|
372
|
-
if play and shutil.which("termux-media-player"):
|
|
363
|
+
print(f" [-] Model '{item}' provisioning note: {e}")
|
|
364
|
+
|
|
365
|
+
# 3. On-Device Verification
|
|
366
|
+
print("\n[STEP 3/3] Running On-Device Self-Test...")
|
|
367
|
+
test_wav = Path.home() / "install_test_tts.wav"
|
|
368
|
+
|
|
369
|
+
try:
|
|
370
|
+
from .engine import load
|
|
371
|
+
with load(language="en", device="cpu") as eng:
|
|
372
|
+
eng.synthesize("Hello, Termux CPU speech synthesis is ready.", output=str(test_wav))
|
|
373
|
+
proc_returncode = 0
|
|
374
|
+
proc_stderr = ""
|
|
375
|
+
except Exception as tts_err:
|
|
376
|
+
proc_returncode = 1
|
|
377
|
+
proc_stderr = str(tts_err)
|
|
378
|
+
|
|
379
|
+
if proc_returncode == 0:
|
|
380
|
+
print(" [VERIFIED] On-device native CPU synthesis self-test passed!")
|
|
381
|
+
player = shutil.which("termux-media-player") or shutil.which("play-audio")
|
|
382
|
+
if play and player:
|
|
373
383
|
print(" [PLAYBACK] Playing verification audio through physical speaker...")
|
|
374
|
-
|
|
375
|
-
|
|
384
|
+
if shutil.which("termux-volume"):
|
|
385
|
+
import subprocess
|
|
386
|
+
subprocess.run(["termux-volume", "music", "10"], check=False)
|
|
387
|
+
import subprocess
|
|
388
|
+
subprocess.run([player, str(test_wav)], check=False)
|
|
376
389
|
else:
|
|
377
|
-
print(f" [FAIL-FAST] Self-test returned error: {
|
|
390
|
+
print(f" [FAIL-FAST] Self-test returned error: {proc_stderr}")
|
|
378
391
|
|
|
379
392
|
print("=" * 70)
|
|
380
393
|
print(" INSTALLATION COMPLETE! YOU CAN NOW USE 'termux-tts' DIRECTLY.")
|