termux-tts 0.1.1 → 1.1.5
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 +18 -0
- package/docs/guide.md +5 -5
- package/docs/tts_guide.md +5 -5
- package/install.sh +3 -3
- package/package.json +2 -2
- package/pyproject.toml +2 -1
- package/setup.py +2 -2
- package/termux_tts/__init__.py +1 -1
- package/termux_tts/adapter.py +139 -36
- package/termux_tts/cli.py +82 -82
- package/termux_tts/control/component.py +113 -11
- package/termux_tts/engine.py +161 -156
- package/termux_tts/engine_dsp.py +332 -332
- package/termux_tts/vulkan_probe.py +32 -8
- package/dsp_test.wav +0 -0
- package/expressive_demo.wav +0 -0
- package/g2p_test.wav +0 -0
- package/test_cli.wav +0 -0
package/CHANGELOG.md
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
All notable changes to this project will be documented in this file.
|
|
4
|
+
|
|
5
|
+
## [1.1.5] - 2026-09-05
|
|
6
|
+
|
|
7
|
+
### Changed
|
|
8
|
+
- Synchronized install.sh hardware diagnostics binding to ameva-runtime.
|
|
9
|
+
- Modernized 12-stage hardware diagnostic bridge and documentation architecture diagrams.
|
|
10
|
+
|
|
11
|
+
---
|
|
12
|
+
|
|
13
|
+
## [1.1.4] - 2026-09-05
|
|
14
|
+
|
|
15
|
+
### Changed
|
|
16
|
+
- Migrated hardware acceleration dependency to unified `ameva-runtime>=2.0.0` and `@ameva/runtime>=2.0.0`.
|
|
17
|
+
- Synchronized Python and npm package versions to v1.1.4.
|
|
18
|
+
|
package/docs/guide.md
CHANGED
|
@@ -31,7 +31,7 @@ flowchart TD
|
|
|
31
31
|
end
|
|
32
32
|
|
|
33
33
|
subgraph Hardware_Probe ["하드웨어 진단 계층"]
|
|
34
|
-
Doctor[VulkanDoctor (vulkan_probe.py)] --> AVR[ameva-
|
|
34
|
+
Doctor[VulkanDoctor (vulkan_probe.py)] --> AVR[ameva-runtime (12-Stage Probe)]
|
|
35
35
|
end
|
|
36
36
|
```
|
|
37
37
|
|
|
@@ -42,7 +42,7 @@ flowchart TD
|
|
|
42
42
|
2. **Option B (안드로이드 시스템 네이티브 음성 브릿지 - `NativeAndroidEngine`)**:
|
|
43
43
|
- 삼성 보이스(Samsung Voice) 및 구글 음성 엔진(Google TTS)과 Termux IPC로 직접 통신하여 zero-download 오버헤드로 즉시 물리 스피커 발화를 수행합니다.
|
|
44
44
|
3. **Vulkan GPU 진단 계층 (`VulkanDoctor`)**:
|
|
45
|
-
- `ameva-
|
|
45
|
+
- `ameva-runtime`의 12단계 자체 검증 엔진(V0~V11)을 바인딩하여 퀄컴 Adreno/ARM Mali GPU의 파이프라인 가용성을 실시간 판별합니다.
|
|
46
46
|
|
|
47
47
|
---
|
|
48
48
|
|
|
@@ -66,7 +66,7 @@ fi
|
|
|
66
66
|
|
|
67
67
|
# [2/4] Python SDK 및 CLI 설치 (Editable/Release Build)
|
|
68
68
|
pip install --upgrade pip setuptools wheel
|
|
69
|
-
pip install ameva-
|
|
69
|
+
pip install ameva-runtime || true
|
|
70
70
|
pip install --no-build-isolation -e .
|
|
71
71
|
|
|
72
72
|
# [3/4] Node.js SDK 및 글로벌 npm CLI 심볼릭 링크
|
|
@@ -81,7 +81,7 @@ termux-tts doctor || true
|
|
|
81
81
|
### 2.2 패키지 빌드 메타데이터 (`setup.py` & `package.json`)
|
|
82
82
|
- **Python Packaging (`setup.py`)**:
|
|
83
83
|
- `console_scripts` 엔트리포인트를 통해 시스템 전역에 `termux-tts` CLI 명령어를 등록합니다.
|
|
84
|
-
- `numpy>=1.20.0` 및 `ameva-
|
|
84
|
+
- `numpy>=1.20.0` 및 `ameva-runtime>=2.0.0`을 표준 종속성으로 선언합니다.
|
|
85
85
|
- **Node.js Packaging (`package.json`)**:
|
|
86
86
|
- `bin/cli.js`를 전역 실행 바이너리로 연결하고, CommonJS 기반 `index.js` 모듈을 제공합니다.
|
|
87
87
|
|
|
@@ -378,7 +378,7 @@ class NativeAndroidEngine:
|
|
|
378
378
|
---
|
|
379
379
|
|
|
380
380
|
### 3.6 하드웨어 진단 프로브 (`termux_tts/vulkan_probe.py`)
|
|
381
|
-
공식 `ameva-
|
|
381
|
+
공식 `ameva-runtime` 패키지를 바인딩하여 12단계 하드웨어 정밀 진단을 구동합니다.
|
|
382
382
|
|
|
383
383
|
- **V0**: `libvulkan.so` 동적 로더 개방 여부
|
|
384
384
|
- **V1~V3**: GPU 인스턴스 및 물리 디바이스(Adreno/Mali) 질의
|
package/docs/tts_guide.md
CHANGED
|
@@ -31,7 +31,7 @@ flowchart TD
|
|
|
31
31
|
end
|
|
32
32
|
|
|
33
33
|
subgraph Hardware_Probe ["하드웨어 진단 계층"]
|
|
34
|
-
Doctor[VulkanDoctor (vulkan_probe.py)] --> AVR[ameva-
|
|
34
|
+
Doctor[VulkanDoctor (vulkan_probe.py)] --> AVR[ameva-runtime (12-Stage Probe)]
|
|
35
35
|
end
|
|
36
36
|
```
|
|
37
37
|
|
|
@@ -42,7 +42,7 @@ flowchart TD
|
|
|
42
42
|
2. **Option B (안드로이드 시스템 네이티브 음성 브릿지 - `NativeAndroidEngine`)**:
|
|
43
43
|
- 삼성 보이스(Samsung Voice) 및 구글 음성 엔진(Google TTS)과 Termux IPC로 직접 통신하여 zero-download 오버헤드로 즉시 물리 스피커 발화를 수행합니다.
|
|
44
44
|
3. **Vulkan GPU 진단 계층 (`VulkanDoctor`)**:
|
|
45
|
-
- `ameva-
|
|
45
|
+
- `ameva-runtime`의 12단계 자체 검증 엔진(V0~V11)을 바인딩하여 퀄컴 Adreno/ARM Mali GPU의 파이프라인 가용성을 실시간 판별합니다.
|
|
46
46
|
|
|
47
47
|
---
|
|
48
48
|
|
|
@@ -66,7 +66,7 @@ fi
|
|
|
66
66
|
|
|
67
67
|
# [2/4] Python SDK 및 CLI 설치 (Editable/Release Build)
|
|
68
68
|
pip install --upgrade pip setuptools wheel
|
|
69
|
-
pip install ameva-
|
|
69
|
+
pip install ameva-runtime || true
|
|
70
70
|
pip install --no-build-isolation -e .
|
|
71
71
|
|
|
72
72
|
# [3/4] Node.js SDK 및 글로벌 npm CLI 심볼릭 링크
|
|
@@ -81,7 +81,7 @@ termux-tts doctor || true
|
|
|
81
81
|
### 2.2 패키지 빌드 메타데이터 (`setup.py` & `package.json`)
|
|
82
82
|
- **Python Packaging (`setup.py`)**:
|
|
83
83
|
- `console_scripts` 엔트리포인트를 통해 시스템 전역에 `termux-tts` CLI 명령어를 등록합니다.
|
|
84
|
-
- `numpy>=1.20.0` 및 `ameva-
|
|
84
|
+
- `numpy>=1.20.0` 및 `ameva-runtime>=2.0.0`을 표준 종속성으로 선언합니다.
|
|
85
85
|
- **Node.js Packaging (`package.json`)**:
|
|
86
86
|
- `bin/cli.js`를 전역 실행 바이너리로 연결하고, CommonJS 기반 `index.js` 모듈을 제공합니다.
|
|
87
87
|
|
|
@@ -378,7 +378,7 @@ class NativeAndroidEngine:
|
|
|
378
378
|
---
|
|
379
379
|
|
|
380
380
|
### 3.6 하드웨어 진단 프로브 (`termux_tts/vulkan_probe.py`)
|
|
381
|
-
공식 `ameva-
|
|
381
|
+
공식 `ameva-runtime` 패키지를 바인딩하여 12단계 하드웨어 정밀 진단을 구동합니다.
|
|
382
382
|
|
|
383
383
|
- **V0**: `libvulkan.so` 동적 로더 개방 여부
|
|
384
384
|
- **V1~V3**: GPU 인스턴스 및 물리 디바이스(Adreno/Mali) 질의
|
package/install.sh
CHANGED
|
@@ -26,10 +26,10 @@ fi
|
|
|
26
26
|
# 2. Python Toolchain & Package Installation (pip)
|
|
27
27
|
echo "[2/4] Installing Python SDK and CLI via pip..."
|
|
28
28
|
pip install --upgrade pip setuptools wheel
|
|
29
|
-
if pip install ameva-
|
|
30
|
-
echo " -> ameva-
|
|
29
|
+
if pip install ameva-runtime 2>/dev/null; then
|
|
30
|
+
echo " -> ameva-runtime hardware diagnostics bound."
|
|
31
31
|
else
|
|
32
|
-
echo " -> ameva-
|
|
32
|
+
echo " -> ameva-runtime optional hardware acceleration bridge skipped."
|
|
33
33
|
fi
|
|
34
34
|
pip install --no-build-isolation -e .
|
|
35
35
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "termux-tts",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "1.1.5",
|
|
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": {
|
|
@@ -23,7 +23,7 @@
|
|
|
23
23
|
"author": "AMEVA Foundation",
|
|
24
24
|
"license": "Apache-2.0",
|
|
25
25
|
"dependencies": {
|
|
26
|
-
"ameva
|
|
26
|
+
"@ameva/runtime": ">=2.0.0"
|
|
27
27
|
},
|
|
28
28
|
"engines": {
|
|
29
29
|
"node": ">=16.0.0"
|
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.1.
|
|
7
|
+
version = "1.1.5"
|
|
8
8
|
description = "On-device Text-to-Speech framework utilizing device resources (DSP Formant Vocoder, ONNX Neural Runtime & Android Native Voice Bridge)"
|
|
9
9
|
readme = "README.pypi.md"
|
|
10
10
|
requires-python = ">=3.10"
|
|
@@ -25,6 +25,7 @@ classifiers = [
|
|
|
25
25
|
]
|
|
26
26
|
dependencies = [
|
|
27
27
|
"numpy>=1.20.0",
|
|
28
|
+
"ameva-component-sdk>=0.1.0,<2.0",
|
|
28
29
|
]
|
|
29
30
|
|
|
30
31
|
[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.1.
|
|
6
|
+
version="1.1.5",
|
|
7
7
|
description="Ultra-Fast On-Device Dual-Engine Text-to-Speech Framework (Parametric Formant Acoustic Synthesizer & Android Native Voice Bridge)",
|
|
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,7 @@ setup(
|
|
|
14
14
|
python_requires=">=3.8",
|
|
15
15
|
install_requires=[
|
|
16
16
|
"numpy>=1.20.0",
|
|
17
|
-
"ameva-
|
|
17
|
+
"ameva-runtime>=2.0.0",
|
|
18
18
|
],
|
|
19
19
|
extras_require={
|
|
20
20
|
"onnx": ["onnxruntime>=1.15.0"],
|
package/termux_tts/__init__.py
CHANGED
package/termux_tts/adapter.py
CHANGED
|
@@ -2,12 +2,21 @@
|
|
|
2
2
|
termux_tts.adapter
|
|
3
3
|
===================
|
|
4
4
|
AMEVA Component Protocol v1 — Orchestrator Adapter (v0.8.1 호환)
|
|
5
|
+
|
|
6
|
+
P0-2: infer() fallback yield _not_supported → raise OperationNotSupported
|
|
7
|
+
P0-4: except Exception → retryable 분류, 원본 오류 코드 보존
|
|
5
8
|
"""
|
|
6
9
|
from __future__ import annotations
|
|
7
10
|
|
|
8
11
|
from typing import Any, AsyncIterator
|
|
9
12
|
|
|
10
13
|
from ameva_component.adapter_base import BaseOrchestratorAdapter
|
|
14
|
+
from ameva_component.exceptions import (
|
|
15
|
+
ComponentError,
|
|
16
|
+
OperationNotSupported,
|
|
17
|
+
redact_details,
|
|
18
|
+
redact_text,
|
|
19
|
+
)
|
|
11
20
|
from termux_tts.control.component import TTSControl
|
|
12
21
|
|
|
13
22
|
|
|
@@ -15,12 +24,25 @@ class TTSOrchestratorAdapter(BaseOrchestratorAdapter):
|
|
|
15
24
|
"""TTS (Text-to-Speech) Orchestrator Adapter.
|
|
16
25
|
|
|
17
26
|
합성은 파일 기반으로 수행됩니다.
|
|
18
|
-
infer()
|
|
27
|
+
infer(): text를 받아 audio 파일 경로를 반환합니다.
|
|
19
28
|
요청 모델과 실행 모델이 다르면 fallback_used=True를 명시합니다.
|
|
20
29
|
"""
|
|
21
30
|
|
|
22
31
|
COMPONENT_ID = "termux-tts"
|
|
23
32
|
|
|
33
|
+
_RETRYABLE_CODES: frozenset[str] = frozenset({
|
|
34
|
+
"REMOTE_TIMEOUT",
|
|
35
|
+
"MODEL_BUSY",
|
|
36
|
+
"TEMPORARY_RESOURCE_UNAVAILABLE",
|
|
37
|
+
})
|
|
38
|
+
_NON_RETRYABLE_CODES: frozenset[str] = frozenset({
|
|
39
|
+
"VOICE_NOT_FOUND",
|
|
40
|
+
"TEXT_TOO_LONG",
|
|
41
|
+
"UNSUPPORTED_LANGUAGE",
|
|
42
|
+
"OUTPUT_PATH_FORBIDDEN",
|
|
43
|
+
"ENCODING_FAILED",
|
|
44
|
+
})
|
|
45
|
+
|
|
24
46
|
def __init__(self, control: TTSControl | None = None) -> None:
|
|
25
47
|
self._control = control or TTSControl()
|
|
26
48
|
|
|
@@ -28,14 +50,14 @@ class TTSOrchestratorAdapter(BaseOrchestratorAdapter):
|
|
|
28
50
|
"""TTS synthesis: text → audio file path.
|
|
29
51
|
|
|
30
52
|
request 키:
|
|
31
|
-
text (str): 합성할 텍스트 (
|
|
53
|
+
text (str): 합성할 텍스트 (필수, 비어있으면 안 됨)
|
|
32
54
|
voice_id (str): 목소리 ID (선택)
|
|
33
55
|
model_id (str): 모델 ID (선택)
|
|
34
|
-
output_path (str): 출력 파일 경로 (
|
|
56
|
+
output_path (str): 출력 파일 경로 (선택)
|
|
35
57
|
|
|
36
58
|
반환 프레임:
|
|
37
59
|
{"type": "audio", "audio_path": str, "final": bool, "fallback_used": bool}
|
|
38
|
-
{"type": "error", "
|
|
60
|
+
{"type": "error", "ok": False, "error": {...}}
|
|
39
61
|
"""
|
|
40
62
|
text = request.get("text", "").strip()
|
|
41
63
|
if not text:
|
|
@@ -52,46 +74,127 @@ class TTSOrchestratorAdapter(BaseOrchestratorAdapter):
|
|
|
52
74
|
}
|
|
53
75
|
return
|
|
54
76
|
|
|
55
|
-
if hasattr(self._control, "synthesize"):
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
"component_id": self.COMPONENT_ID,
|
|
68
|
-
"retryable": True,
|
|
69
|
-
},
|
|
70
|
-
}
|
|
71
|
-
return
|
|
72
|
-
yield {
|
|
73
|
-
"type": "audio",
|
|
74
|
-
"audio_path": audio_path,
|
|
75
|
-
"final": True,
|
|
76
|
-
"fallback_used": result.get("fallback_used", False),
|
|
77
|
-
"requested_voice": result.get("requested_voice"),
|
|
78
|
-
"executed_voice": result.get("executed_voice"),
|
|
79
|
-
"ok": True,
|
|
80
|
-
}
|
|
81
|
-
except Exception as exc:
|
|
77
|
+
if not hasattr(self._control, "synthesize"):
|
|
78
|
+
# P0-2: fallback yield → raise
|
|
79
|
+
raise OperationNotSupported(operation="infer.synthesize", component_id=self.COMPONENT_ID)
|
|
80
|
+
|
|
81
|
+
try:
|
|
82
|
+
result = await self._control.synthesize(request)
|
|
83
|
+
|
|
84
|
+
if not isinstance(result, dict):
|
|
85
|
+
raise ValueError(f"synthesize() must return dict, got {type(result).__name__}")
|
|
86
|
+
|
|
87
|
+
if result.get("ok") is not True:
|
|
88
|
+
err_payload = result.get("error") if isinstance(result.get("error"), dict) else {}
|
|
82
89
|
yield {
|
|
83
90
|
"type": "error",
|
|
84
91
|
"ok": False,
|
|
85
92
|
"error": {
|
|
86
|
-
"code": "
|
|
87
|
-
"message":
|
|
93
|
+
"code": err_payload.get("code", "ADAPTER_RESULT_NOT_SUCCESS"),
|
|
94
|
+
"message": err_payload.get("message", "synthesize() did not return ok=True"),
|
|
88
95
|
"operation": "infer",
|
|
89
96
|
"component_id": self.COMPONENT_ID,
|
|
90
|
-
"retryable":
|
|
97
|
+
"retryable": False,
|
|
98
|
+
"details": {"result_keys": sorted(result.keys())},
|
|
91
99
|
},
|
|
92
100
|
}
|
|
93
|
-
|
|
94
|
-
|
|
101
|
+
return
|
|
102
|
+
|
|
103
|
+
audio_path = result.get("audio_path")
|
|
104
|
+
if not audio_path:
|
|
105
|
+
raise ValueError("synthesize() result missing required field 'audio_path'")
|
|
106
|
+
if not isinstance(audio_path, str):
|
|
107
|
+
raise TypeError(f"synthesize() audio_path must be str, got {type(audio_path).__name__}")
|
|
108
|
+
|
|
109
|
+
yield {
|
|
110
|
+
"type": "audio",
|
|
111
|
+
"audio_path": audio_path,
|
|
112
|
+
"final": True,
|
|
113
|
+
"fallback_used": result.get("fallback_used", False),
|
|
114
|
+
"requested_voice": result.get("requested_voice"),
|
|
115
|
+
"executed_voice": result.get("executed_voice"),
|
|
116
|
+
"ok": True,
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
except ComponentError as component_err:
|
|
120
|
+
# HIGH 1: ComponentError public_message와 redact_details로 외부 노출 보안 격리
|
|
121
|
+
import logging
|
|
122
|
+
logging.getLogger(__name__).exception(
|
|
123
|
+
"TTS component operation failed",
|
|
124
|
+
extra={
|
|
125
|
+
"component_id": self.COMPONENT_ID,
|
|
126
|
+
"operation": "infer",
|
|
127
|
+
"code": getattr(component_err, "code", "COMPONENT_ERROR"),
|
|
128
|
+
},
|
|
129
|
+
)
|
|
130
|
+
if hasattr(component_err, "to_public_dict"):
|
|
131
|
+
err_dict = component_err.to_public_dict()
|
|
132
|
+
else:
|
|
133
|
+
err_dict = {
|
|
134
|
+
"code": getattr(component_err, "code", "COMPONENT_ERROR"),
|
|
135
|
+
"message": redact_text(getattr(component_err, "public_message", "Component operation failed")),
|
|
136
|
+
"retryable": getattr(component_err, "retryable", False),
|
|
137
|
+
"details": redact_details(getattr(component_err, "details", {})),
|
|
138
|
+
}
|
|
139
|
+
yield {
|
|
140
|
+
"type": "error",
|
|
141
|
+
"ok": False,
|
|
142
|
+
"error": {
|
|
143
|
+
**err_dict,
|
|
144
|
+
"operation": "infer",
|
|
145
|
+
"component_id": self.COMPONENT_ID,
|
|
146
|
+
"retryable": self._classify_retryable(
|
|
147
|
+
err_dict.get("code", ""), default=err_dict.get("retryable", False)
|
|
148
|
+
),
|
|
149
|
+
},
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
except (ValueError, TypeError) as contract_err:
|
|
153
|
+
# P0-3/4 & HIGH 1: 계약 위반 (str(contract_err) 직접 노출 금지, 내부 로그 격리)
|
|
154
|
+
import logging
|
|
155
|
+
logging.getLogger(__name__).exception("TTS contract validation error during infer: %s", contract_err)
|
|
156
|
+
yield {
|
|
157
|
+
"type": "error",
|
|
158
|
+
"ok": False,
|
|
159
|
+
"error": {
|
|
160
|
+
"code": "ADAPTER_CONTRACT_ERROR",
|
|
161
|
+
"message": "Adapter contract validation failed",
|
|
162
|
+
"operation": "infer",
|
|
163
|
+
"component_id": self.COMPONENT_ID,
|
|
164
|
+
"retryable": False,
|
|
165
|
+
"details": {
|
|
166
|
+
"cause_type": type(contract_err).__name__,
|
|
167
|
+
"operation": "infer",
|
|
168
|
+
},
|
|
169
|
+
},
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
except Exception as unexpected_err:
|
|
173
|
+
import logging
|
|
174
|
+
logging.getLogger(__name__).exception("TTS adapter unexpected error during infer: %s", unexpected_err)
|
|
175
|
+
code = getattr(unexpected_err, "code", "ADAPTER_INTERNAL_ERROR")
|
|
176
|
+
yield {
|
|
177
|
+
"type": "error",
|
|
178
|
+
"ok": False,
|
|
179
|
+
"error": {
|
|
180
|
+
"code": code if isinstance(code, str) else "ADAPTER_INTERNAL_ERROR",
|
|
181
|
+
"message": "Unexpected adapter failure",
|
|
182
|
+
"operation": "infer",
|
|
183
|
+
"component_id": self.COMPONENT_ID,
|
|
184
|
+
"retryable": False,
|
|
185
|
+
"details": {
|
|
186
|
+
"cause_type": type(unexpected_err).__name__,
|
|
187
|
+
"operation": "infer",
|
|
188
|
+
},
|
|
189
|
+
},
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
def _classify_retryable(self, code: str, *, default: bool = False) -> bool:
|
|
193
|
+
if code in self._RETRYABLE_CODES:
|
|
194
|
+
return True
|
|
195
|
+
if code in self._NON_RETRYABLE_CODES:
|
|
196
|
+
return False
|
|
197
|
+
return default
|
|
95
198
|
|
|
96
199
|
|
|
97
200
|
def create_adapter() -> TTSOrchestratorAdapter:
|
package/termux_tts/cli.py
CHANGED
|
@@ -1,82 +1,82 @@
|
|
|
1
|
-
"""
|
|
2
|
-
Command-Line Interface for termux-tts:
|
|
3
|
-
- termux-tts synth : Option A Deep Learning Neural Synthesis to File
|
|
4
|
-
- termux-tts speak : Option B Android System Native Immediate Voice Output
|
|
5
|
-
- termux-tts doctor: 12-Stage Vulkan GPU Hardware Diagnostics
|
|
6
|
-
"""
|
|
7
|
-
|
|
8
|
-
import argparse
|
|
9
|
-
from .engine import load, doctor
|
|
10
|
-
|
|
11
|
-
def main():
|
|
12
|
-
parser = argparse.ArgumentParser(
|
|
13
|
-
prog="termux-tts",
|
|
14
|
-
description="Termux Neural & Native Text-to-Speech Engine"
|
|
15
|
-
)
|
|
16
|
-
subparsers = parser.add_subparsers(dest="command", help="Sub-commands")
|
|
17
|
-
|
|
18
|
-
# 1. Synth (Option A: DSP Formant or ONNX Neural Vocoder to File)
|
|
19
|
-
synth_parser = subparsers.add_parser("synth", help="Synthesize text to audio WAV file (DSP / ONNX)")
|
|
20
|
-
synth_parser.add_argument("-t", "--text", required=True, help="Input text to synthesize")
|
|
21
|
-
synth_parser.add_argument("-o", "--output", default="output.wav", help="Output WAV filepath")
|
|
22
|
-
synth_parser.add_argument("-l", "--lang", default="ko", help="Language code (ko, en)")
|
|
23
|
-
synth_parser.add_argument("-e", "--engine", default="auto", choices=["auto", "dsp", "onnx"], help="Synthesis engine (dsp=zero-dependency, onnx=deep learning)")
|
|
24
|
-
synth_parser.add_argument("-m", "--model", default=None, help="Path to .onnx model file (required for onnx engine)")
|
|
25
|
-
synth_parser.add_argument("-p", "--preset", default="balanced", choices=["fast", "balanced", "expressive", "ultra"])
|
|
26
|
-
synth_parser.add_argument("-d", "--device", default="auto", choices=["auto", "gpu", "vulkan", "cpu"])
|
|
27
|
-
synth_parser.add_argument("-s", "--speed", type=float, default=1.0, help="Speech speed multiplier (0.5 to 2.0)")
|
|
28
|
-
|
|
29
|
-
# 2. Speak (Option B: Native Samsung/Google System Voice)
|
|
30
|
-
speak_parser = subparsers.add_parser("speak", help="Speak text directly through device speaker (Option B: Native)")
|
|
31
|
-
speak_parser.add_argument("-t", "--text", required=True, help="Input text to speak")
|
|
32
|
-
speak_parser.add_argument("-l", "--lang", default="ko", help="Language code (ko, en)")
|
|
33
|
-
speak_parser.add_argument("-s", "--stream", default="MUSIC", help="Audio stream (MUSIC, NOTIFICATION, ALARM)")
|
|
34
|
-
|
|
35
|
-
# 3. Doctor (Diagnostics)
|
|
36
|
-
subparsers.add_parser("doctor", help="Run 12-stage Vulkan GPU hardware diagnostics")
|
|
37
|
-
|
|
38
|
-
# ── AMEVA Component Protocol v1 ─────────────────────────────────────────
|
|
39
|
-
_protocol_available = False
|
|
40
|
-
try:
|
|
41
|
-
from ameva_component.cli_support import build_protocol_subcommands
|
|
42
|
-
build_protocol_subcommands(subparsers)
|
|
43
|
-
_protocol_available = True
|
|
44
|
-
except ImportError:
|
|
45
|
-
|
|
46
|
-
# ────────────────────────────────────────────────────────────────────────
|
|
47
|
-
|
|
48
|
-
args = parser.parse_args()
|
|
49
|
-
|
|
50
|
-
if args.command == "synth":
|
|
51
|
-
with load(model=args.model, language=args.lang, preset=args.preset, device=args.device, engine=args.engine) as engine:
|
|
52
|
-
res = engine.synthesize(args.text, output=args.output, speed=args.speed)
|
|
53
|
-
print(f"[SUCCESS] Synthesized via {res.backend} ({res.model_name}) -> {args.output}")
|
|
54
|
-
print(f" Duration: {res.duration_sec:.2f}s | Elapsed: {res.elapsed_ms:.1f}ms | RTF: {res.rtf:.4f}x")
|
|
55
|
-
|
|
56
|
-
elif args.command == "speak":
|
|
57
|
-
with load(language=args.lang) as engine:
|
|
58
|
-
res = engine.speak(args.text, stream=args.stream)
|
|
59
|
-
print(f"[SUCCESS] Spoken via {res.engine_name} on stream {args.stream} ({res.elapsed_ms:.1f}ms)")
|
|
60
|
-
|
|
61
|
-
elif args.command == "doctor":
|
|
62
|
-
diag = doctor()
|
|
63
|
-
print("=" * 60)
|
|
64
|
-
print(" TERMUX-TTS 12-STAGE VULKAN HARDWARE DIAGNOSTICS")
|
|
65
|
-
print("=" * 60)
|
|
66
|
-
for k, v in diag.items():
|
|
67
|
-
print(f" - {k:30s}: {v}")
|
|
68
|
-
print("=" * 60)
|
|
69
|
-
|
|
70
|
-
elif args.command in ("component", "model", "instance") and _protocol_available:
|
|
71
|
-
from ameva_component.cli_support import dispatch_protocol
|
|
72
|
-
from termux_tts.control import TTSControl
|
|
73
|
-
dispatch_protocol(args, TTSControl())
|
|
74
|
-
elif args.command in ("component", "model", "instance"):
|
|
75
|
-
print("[ERROR] ameva-component-sdk not installed.", file=sys.stderr)
|
|
76
|
-
sys.exit(1)
|
|
77
|
-
else:
|
|
78
|
-
parser.print_help()
|
|
79
|
-
|
|
80
|
-
if __name__ == "__main__":
|
|
81
|
-
main()
|
|
82
|
-
|
|
1
|
+
"""
|
|
2
|
+
Command-Line Interface for termux-tts:
|
|
3
|
+
- termux-tts synth : Option A Deep Learning Neural Synthesis to File
|
|
4
|
+
- termux-tts speak : Option B Android System Native Immediate Voice Output
|
|
5
|
+
- termux-tts doctor: 12-Stage Vulkan GPU Hardware Diagnostics
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
import argparse
|
|
9
|
+
from .engine import load, doctor
|
|
10
|
+
|
|
11
|
+
def main():
|
|
12
|
+
parser = argparse.ArgumentParser(
|
|
13
|
+
prog="termux-tts",
|
|
14
|
+
description="Termux Neural & Native Text-to-Speech Engine"
|
|
15
|
+
)
|
|
16
|
+
subparsers = parser.add_subparsers(dest="command", help="Sub-commands")
|
|
17
|
+
|
|
18
|
+
# 1. Synth (Option A: DSP Formant or ONNX Neural Vocoder to File)
|
|
19
|
+
synth_parser = subparsers.add_parser("synth", help="Synthesize text to audio WAV file (DSP / ONNX)")
|
|
20
|
+
synth_parser.add_argument("-t", "--text", required=True, help="Input text to synthesize")
|
|
21
|
+
synth_parser.add_argument("-o", "--output", default="output.wav", help="Output WAV filepath")
|
|
22
|
+
synth_parser.add_argument("-l", "--lang", default="ko", help="Language code (ko, en)")
|
|
23
|
+
synth_parser.add_argument("-e", "--engine", default="auto", choices=["auto", "dsp", "onnx"], help="Synthesis engine (dsp=zero-dependency, onnx=deep learning)")
|
|
24
|
+
synth_parser.add_argument("-m", "--model", default=None, help="Path to .onnx model file (required for onnx engine)")
|
|
25
|
+
synth_parser.add_argument("-p", "--preset", default="balanced", choices=["fast", "balanced", "expressive", "ultra"])
|
|
26
|
+
synth_parser.add_argument("-d", "--device", default="auto", choices=["auto", "gpu", "vulkan", "cpu"])
|
|
27
|
+
synth_parser.add_argument("-s", "--speed", type=float, default=1.0, help="Speech speed multiplier (0.5 to 2.0)")
|
|
28
|
+
|
|
29
|
+
# 2. Speak (Option B: Native Samsung/Google System Voice)
|
|
30
|
+
speak_parser = subparsers.add_parser("speak", help="Speak text directly through device speaker (Option B: Native)")
|
|
31
|
+
speak_parser.add_argument("-t", "--text", required=True, help="Input text to speak")
|
|
32
|
+
speak_parser.add_argument("-l", "--lang", default="ko", help="Language code (ko, en)")
|
|
33
|
+
speak_parser.add_argument("-s", "--stream", default="MUSIC", help="Audio stream (MUSIC, NOTIFICATION, ALARM)")
|
|
34
|
+
|
|
35
|
+
# 3. Doctor (Diagnostics)
|
|
36
|
+
subparsers.add_parser("doctor", help="Run 12-stage Vulkan GPU hardware diagnostics")
|
|
37
|
+
|
|
38
|
+
# ── AMEVA Component Protocol v1 ─────────────────────────────────────────
|
|
39
|
+
_protocol_available = False
|
|
40
|
+
try:
|
|
41
|
+
from ameva_component.cli_support import build_protocol_subcommands
|
|
42
|
+
build_protocol_subcommands(subparsers)
|
|
43
|
+
_protocol_available = True
|
|
44
|
+
except ImportError as _proto_err:
|
|
45
|
+
_protocol_available = False
|
|
46
|
+
# ────────────────────────────────────────────────────────────────────────
|
|
47
|
+
|
|
48
|
+
args = parser.parse_args()
|
|
49
|
+
|
|
50
|
+
if args.command == "synth":
|
|
51
|
+
with load(model=args.model, language=args.lang, preset=args.preset, device=args.device, engine=args.engine) as engine:
|
|
52
|
+
res = engine.synthesize(args.text, output=args.output, speed=args.speed)
|
|
53
|
+
print(f"[SUCCESS] Synthesized via {res.backend} ({res.model_name}) -> {args.output}")
|
|
54
|
+
print(f" Duration: {res.duration_sec:.2f}s | Elapsed: {res.elapsed_ms:.1f}ms | RTF: {res.rtf:.4f}x")
|
|
55
|
+
|
|
56
|
+
elif args.command == "speak":
|
|
57
|
+
with load(language=args.lang) as engine:
|
|
58
|
+
res = engine.speak(args.text, stream=args.stream)
|
|
59
|
+
print(f"[SUCCESS] Spoken via {res.engine_name} on stream {args.stream} ({res.elapsed_ms:.1f}ms)")
|
|
60
|
+
|
|
61
|
+
elif args.command == "doctor":
|
|
62
|
+
diag = doctor()
|
|
63
|
+
print("=" * 60)
|
|
64
|
+
print(" TERMUX-TTS 12-STAGE VULKAN HARDWARE DIAGNOSTICS")
|
|
65
|
+
print("=" * 60)
|
|
66
|
+
for k, v in diag.items():
|
|
67
|
+
print(f" - {k:30s}: {v}")
|
|
68
|
+
print("=" * 60)
|
|
69
|
+
|
|
70
|
+
elif args.command in ("component", "model", "instance") and _protocol_available:
|
|
71
|
+
from ameva_component.cli_support import dispatch_protocol
|
|
72
|
+
from termux_tts.control import TTSControl
|
|
73
|
+
dispatch_protocol(args, TTSControl())
|
|
74
|
+
elif args.command in ("component", "model", "instance"):
|
|
75
|
+
print("[ERROR] ameva-component-sdk not installed.", file=sys.stderr)
|
|
76
|
+
sys.exit(1)
|
|
77
|
+
else:
|
|
78
|
+
parser.print_help()
|
|
79
|
+
|
|
80
|
+
if __name__ == "__main__":
|
|
81
|
+
main()
|
|
82
|
+
|