termux-tts 0.1.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/LICENSE +17 -0
- package/README.md +31 -0
- package/README.pypi.md +47 -0
- package/bin/cli.js +33 -0
- package/binding_node/index.js +117 -0
- package/binding_node/test_node.js +40 -0
- package/doc.config.yaml +98 -0
- package/docs/benchmarks.md +12 -0
- package/docs/guide.md +552 -0
- package/docs/tts_guide.md +552 -0
- package/dsp_test.wav +0 -0
- package/expressive_demo.wav +0 -0
- package/g2p_test.wav +0 -0
- package/index.js +5 -0
- package/install.sh +53 -0
- package/package.json +31 -0
- package/pyproject.toml +43 -0
- package/setup.py +38 -0
- package/termux_tts/__init__.py +47 -0
- package/termux_tts/adapter.py +99 -0
- package/termux_tts/audio.py +75 -0
- package/termux_tts/cli.py +82 -0
- package/termux_tts/control/__init__.py +4 -0
- package/termux_tts/control/component.py +242 -0
- package/termux_tts/control/errors.py +52 -0
- package/termux_tts/control/instances.py +137 -0
- package/termux_tts/control/models.py +151 -0
- package/termux_tts/control/status.py +13 -0
- package/termux_tts/engine.py +157 -0
- package/termux_tts/engine_dsp.py +332 -0
- package/termux_tts/engine_native.py +104 -0
- package/termux_tts/engine_onnx.py +199 -0
- package/termux_tts/exceptions.py +28 -0
- package/termux_tts/g2p_korean.py +242 -0
- package/termux_tts/tokenizer.py +184 -0
- package/termux_tts/vulkan_probe.py +25 -0
- package/test_cli.wav +0 -0
- package/tests/test_expressive_presets.py +29 -0
- package/tests/test_g2p_korean.py +73 -0
- package/tests/test_granular_tts.py +141 -0
- package/tests/test_native_engine.py +38 -0
- package/tests/test_onnx_engine.py +48 -0
- package/tests/test_vulkan_routing.py +52 -0
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
"""
|
|
2
|
+
termux_tts.control.errors
|
|
3
|
+
termux-tts 패키지 고유 오류 정의.
|
|
4
|
+
|
|
5
|
+
공통 AmevaError 하위 클래스로 정의하여 render_from_exception()과 호환됩니다.
|
|
6
|
+
"""
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
from ameva_component.exceptions import AmevaError
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class VoiceNotFound(AmevaError):
|
|
13
|
+
"""요청한 voice_id가 Registry 또는 내장 목록에 없을 때."""
|
|
14
|
+
|
|
15
|
+
code = "VOICE_NOT_FOUND"
|
|
16
|
+
exit_code = 65
|
|
17
|
+
|
|
18
|
+
def __init__(self, voice_id: str, model_id: str | None = None) -> None:
|
|
19
|
+
where = f" in model '{model_id}'" if model_id else ""
|
|
20
|
+
super().__init__(
|
|
21
|
+
f"Voice '{voice_id}' not found{where}",
|
|
22
|
+
details={"voice_id": voice_id, "model_id": model_id},
|
|
23
|
+
)
|
|
24
|
+
self.voice_id = voice_id
|
|
25
|
+
self.model_id = model_id
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
class EngineNotAvailable(AmevaError):
|
|
29
|
+
"""요청한 엔진 백엔드(dsp/onnx/native)가 현재 환경에서 사용 불가능할 때."""
|
|
30
|
+
|
|
31
|
+
code = "ENGINE_NOT_AVAILABLE"
|
|
32
|
+
exit_code = 69
|
|
33
|
+
|
|
34
|
+
def __init__(self, engine_name: str, reason: str) -> None:
|
|
35
|
+
super().__init__(
|
|
36
|
+
f"Engine '{engine_name}' is not available: {reason}",
|
|
37
|
+
details={"engine_name": engine_name, "reason": reason},
|
|
38
|
+
)
|
|
39
|
+
self.engine_name = engine_name
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
class ModelPathRequired(AmevaError):
|
|
43
|
+
"""ONNX 엔진 사용 시 model_path가 없을 때."""
|
|
44
|
+
|
|
45
|
+
code = "MODEL_PATH_REQUIRED"
|
|
46
|
+
exit_code = 64
|
|
47
|
+
|
|
48
|
+
def __init__(self) -> None:
|
|
49
|
+
super().__init__(
|
|
50
|
+
"model_path is required for ONNX engine activation",
|
|
51
|
+
details={"hint": "Pass model_path in the activation request"},
|
|
52
|
+
)
|
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
"""
|
|
2
|
+
termux_tts.control.instances
|
|
3
|
+
InstanceRegistry 연동 — DSP / ONNX / Native 엔진을 각각 Instance로 표현.
|
|
4
|
+
|
|
5
|
+
TTS Instance 정책:
|
|
6
|
+
- DSP 엔진 → instance_id = "tts-dsp"
|
|
7
|
+
- ONNX 엔진 → instance_id = "tts-onnx"
|
|
8
|
+
- Native 엔진 → instance_id = "tts-native"
|
|
9
|
+
- 각 엔진은 in-process ControlMode로 동작 (별도 Worker 프로세스 없음)
|
|
10
|
+
- active_jobs 측정 실패를 0으로 변환하지 않음 → None 반환 후 호출자가 처리
|
|
11
|
+
"""
|
|
12
|
+
from __future__ import annotations
|
|
13
|
+
|
|
14
|
+
import time
|
|
15
|
+
from pathlib import Path
|
|
16
|
+
|
|
17
|
+
from ameva_component import InstanceRegistry
|
|
18
|
+
from ameva_component.instance import ControlMode, InstanceState, InstanceStatus
|
|
19
|
+
|
|
20
|
+
COMPONENT_ID = "termux-tts"
|
|
21
|
+
|
|
22
|
+
# TTS 엔진별 고정 Instance ID
|
|
23
|
+
INSTANCE_DSP = "tts-dsp"
|
|
24
|
+
INSTANCE_ONNX = "tts-onnx"
|
|
25
|
+
INSTANCE_NATIVE = "tts-native"
|
|
26
|
+
|
|
27
|
+
ALL_INSTANCE_IDS = (INSTANCE_DSP, INSTANCE_ONNX, INSTANCE_NATIVE)
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def _make_default_instance(instance_id: str, model_id: str, backend: str) -> InstanceStatus:
|
|
31
|
+
"""엔진별 기본 InstanceStatus를 생성합니다."""
|
|
32
|
+
return InstanceStatus(
|
|
33
|
+
instance_id=instance_id,
|
|
34
|
+
component_id=COMPONENT_ID,
|
|
35
|
+
model_id=model_id,
|
|
36
|
+
state=InstanceState.HOT,
|
|
37
|
+
active_jobs=0,
|
|
38
|
+
queue_depth=0,
|
|
39
|
+
max_concurrency=1,
|
|
40
|
+
backend=backend,
|
|
41
|
+
started_at=time.time(),
|
|
42
|
+
last_heartbeat=time.time(),
|
|
43
|
+
last_error=None,
|
|
44
|
+
control_mode=ControlMode.IN_PROCESS,
|
|
45
|
+
endpoint=None,
|
|
46
|
+
)
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
class TTSInstanceRegistry:
|
|
50
|
+
"""
|
|
51
|
+
InstanceRegistry를 감싸는 TTS 전용 레지스트리.
|
|
52
|
+
|
|
53
|
+
DSP/ONNX/Native 3개 엔진을 각각 독립 Instance로 추적합니다.
|
|
54
|
+
Registry 파일이 없으면 기본 InstanceStatus를 생성하여 등록합니다.
|
|
55
|
+
"""
|
|
56
|
+
|
|
57
|
+
def __init__(self, registry_dir: Path | None = None) -> None:
|
|
58
|
+
self._reg = InstanceRegistry(COMPONENT_ID, registry_dir=registry_dir)
|
|
59
|
+
self._ensure_defaults()
|
|
60
|
+
|
|
61
|
+
# ------------------------------------------------------------------
|
|
62
|
+
# 초기화
|
|
63
|
+
# ------------------------------------------------------------------
|
|
64
|
+
|
|
65
|
+
def _ensure_defaults(self) -> None:
|
|
66
|
+
"""레지스트리에 없는 엔진 Instance를 기본값으로 등록합니다."""
|
|
67
|
+
defaults = {
|
|
68
|
+
INSTANCE_DSP: ("dsp-ko", "dsp-cpu"),
|
|
69
|
+
INSTANCE_ONNX: ("native-system", "onnx-cpu"), # 활성 모델이 없을 때 기본값
|
|
70
|
+
INSTANCE_NATIVE: ("native-system", "android-native"),
|
|
71
|
+
}
|
|
72
|
+
for inst_id, (model_id, backend) in defaults.items():
|
|
73
|
+
if self._reg.get(inst_id) is None:
|
|
74
|
+
self._reg.register(_make_default_instance(inst_id, model_id, backend))
|
|
75
|
+
|
|
76
|
+
# ------------------------------------------------------------------
|
|
77
|
+
# 조회
|
|
78
|
+
# ------------------------------------------------------------------
|
|
79
|
+
|
|
80
|
+
def list_all(self) -> list[dict]:
|
|
81
|
+
"""등록된 모든 Instance를 dict 목록으로 반환합니다."""
|
|
82
|
+
return [
|
|
83
|
+
inst.to_dict()
|
|
84
|
+
for inst in self._reg.list_all()
|
|
85
|
+
]
|
|
86
|
+
|
|
87
|
+
def get(self, instance_id: str) -> InstanceStatus | None:
|
|
88
|
+
"""단일 Instance 상태를 반환합니다. 없으면 None."""
|
|
89
|
+
return self._reg.get(instance_id)
|
|
90
|
+
|
|
91
|
+
def total_active_jobs(self) -> int | None:
|
|
92
|
+
"""
|
|
93
|
+
전체 Instance의 active_jobs 합계를 반환합니다.
|
|
94
|
+
측정 실패 시 None — 0으로 변환 금지.
|
|
95
|
+
"""
|
|
96
|
+
total = 0
|
|
97
|
+
any_read = False
|
|
98
|
+
for inst in self._reg.list_all():
|
|
99
|
+
any_read = True
|
|
100
|
+
total += inst.active_jobs
|
|
101
|
+
return total if any_read else None
|
|
102
|
+
|
|
103
|
+
# ------------------------------------------------------------------
|
|
104
|
+
# 상태 변경
|
|
105
|
+
# ------------------------------------------------------------------
|
|
106
|
+
|
|
107
|
+
def on_model_activated(self, instance_id: str, model_id: str, backend: str) -> None:
|
|
108
|
+
"""모델 활성화 시 Instance model_id와 backend를 갱신합니다."""
|
|
109
|
+
inst = self._reg.get(instance_id)
|
|
110
|
+
if inst is None:
|
|
111
|
+
inst = _make_default_instance(instance_id, model_id, backend)
|
|
112
|
+
inst.model_id = model_id
|
|
113
|
+
inst.backend = backend
|
|
114
|
+
inst.state = InstanceState.HOT
|
|
115
|
+
inst.last_error = None
|
|
116
|
+
inst.last_heartbeat = time.time()
|
|
117
|
+
self._reg.register(inst)
|
|
118
|
+
|
|
119
|
+
def on_job_start(self, instance_id: str) -> None:
|
|
120
|
+
"""Job 시작 시 active_jobs++."""
|
|
121
|
+
self._reg.increment_jobs(instance_id)
|
|
122
|
+
|
|
123
|
+
def on_job_end(self, instance_id: str) -> None:
|
|
124
|
+
"""Job 종료 시 active_jobs-- (음수 방지)."""
|
|
125
|
+
self._reg.decrement_jobs(instance_id)
|
|
126
|
+
|
|
127
|
+
def on_engine_error(self, instance_id: str, error: str) -> None:
|
|
128
|
+
"""엔진 오류 시 Instance 상태를 FAILED로 변경합니다."""
|
|
129
|
+
self._reg.update_state(instance_id, InstanceState.FAILED, last_error=error)
|
|
130
|
+
|
|
131
|
+
def on_heartbeat(self, instance_id: str) -> None:
|
|
132
|
+
"""Heartbeat 갱신."""
|
|
133
|
+
self._reg.update_heartbeat(instance_id)
|
|
134
|
+
|
|
135
|
+
def update_state(self, instance_id: str, state: InstanceState, *, last_error: str | None = None) -> None:
|
|
136
|
+
"""Instance 상태를 직접 변경합니다."""
|
|
137
|
+
self._reg.update_state(instance_id, state, last_error=last_error)
|
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
"""
|
|
2
|
+
termux_tts.control.models
|
|
3
|
+
ModelRegistry 연동 — TTS 모델 + Voice 분리 관리.
|
|
4
|
+
|
|
5
|
+
TTS 특이사항:
|
|
6
|
+
- model_id: "dsp-ko", "piper-ko", "fastspeech-ko" 등 엔진/언어 조합
|
|
7
|
+
- voice_id: "ko-speaker-01", "en-speaker-01" 등 화자 ID (모델에 종속)
|
|
8
|
+
- 내장 엔진(dsp/native)은 파일 없이도 항상 사용 가능 → missing 상태로 표기 금지
|
|
9
|
+
- ONNX 모델은 파일 존재만으로 installed 처리 금지 → Registry 기반
|
|
10
|
+
"""
|
|
11
|
+
from __future__ import annotations
|
|
12
|
+
|
|
13
|
+
import os
|
|
14
|
+
import time
|
|
15
|
+
from pathlib import Path
|
|
16
|
+
from typing import Any
|
|
17
|
+
|
|
18
|
+
from ameva_component import ModelRegistry
|
|
19
|
+
from ameva_component.manifest import ModelState
|
|
20
|
+
|
|
21
|
+
COMPONENT_ID = "termux-tts"
|
|
22
|
+
|
|
23
|
+
# ------------------------------------------------------------------
|
|
24
|
+
# 내장(Built-in) 엔진 정의 — 파일 불필요, 항상 사용 가능
|
|
25
|
+
# ------------------------------------------------------------------
|
|
26
|
+
|
|
27
|
+
# format: {model_id: {description, engine, voices, backend_key}}
|
|
28
|
+
BUILTIN_MODELS: dict[str, dict[str, Any]] = {
|
|
29
|
+
"dsp-ko": {
|
|
30
|
+
"model_id": "dsp-ko",
|
|
31
|
+
"description": "Pure Parametric DSP Formant Synthesizer (Korean, zero-dependency)",
|
|
32
|
+
"engine": "dsp",
|
|
33
|
+
"format": "builtin",
|
|
34
|
+
"language": "ko",
|
|
35
|
+
"voices": [
|
|
36
|
+
{"voice_id": "ko-default", "description": "Default Korean DSP voice", "language": "ko"},
|
|
37
|
+
],
|
|
38
|
+
"state": ModelState.ACTIVE.value, # 내장 엔진은 항상 사용 가능
|
|
39
|
+
},
|
|
40
|
+
"dsp-en": {
|
|
41
|
+
"model_id": "dsp-en",
|
|
42
|
+
"description": "Pure Parametric DSP Formant Synthesizer (English, zero-dependency)",
|
|
43
|
+
"engine": "dsp",
|
|
44
|
+
"format": "builtin",
|
|
45
|
+
"language": "en",
|
|
46
|
+
"voices": [
|
|
47
|
+
{"voice_id": "en-default", "description": "Default English DSP voice", "language": "en"},
|
|
48
|
+
],
|
|
49
|
+
"state": ModelState.ACTIVE.value,
|
|
50
|
+
},
|
|
51
|
+
"native-system": {
|
|
52
|
+
"model_id": "native-system",
|
|
53
|
+
"description": "Android System Native Voice (Samsung/Google TTS bridge)",
|
|
54
|
+
"engine": "native",
|
|
55
|
+
"format": "builtin",
|
|
56
|
+
"language": "ko,en",
|
|
57
|
+
"voices": [
|
|
58
|
+
{"voice_id": "system-default", "description": "Android system TTS voice", "language": "ko,en"},
|
|
59
|
+
],
|
|
60
|
+
"state": ModelState.ACTIVE.value,
|
|
61
|
+
},
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
class TTSModelRegistry:
|
|
66
|
+
"""
|
|
67
|
+
ModelRegistry를 감싸는 TTS 전용 레지스트리.
|
|
68
|
+
|
|
69
|
+
내장 엔진(dsp, native)은 파일 없이도 항상 사용 가능하며,
|
|
70
|
+
ONNX 모델만 ModelRegistry를 통해 상태를 추적합니다.
|
|
71
|
+
"""
|
|
72
|
+
|
|
73
|
+
def __init__(self, registry_dir: Path | None = None) -> None:
|
|
74
|
+
self._reg = ModelRegistry(COMPONENT_ID, registry_dir=registry_dir)
|
|
75
|
+
|
|
76
|
+
# ------------------------------------------------------------------
|
|
77
|
+
# 전체 목록 조회
|
|
78
|
+
# ------------------------------------------------------------------
|
|
79
|
+
|
|
80
|
+
def list_all(self) -> list[dict]:
|
|
81
|
+
"""
|
|
82
|
+
모든 모델 레코드를 반환합니다.
|
|
83
|
+
- 내장 엔진: BUILTIN_MODELS에서 직접 반환
|
|
84
|
+
- ONNX 모델: ModelRegistry에서 실제 상태 조회
|
|
85
|
+
"""
|
|
86
|
+
result: list[dict] = list(BUILTIN_MODELS.values())
|
|
87
|
+
# Registry에서 외부 ONNX 모델 추가
|
|
88
|
+
for rec in self._reg.list_all():
|
|
89
|
+
result.append(self._enrich_onnx_record(rec))
|
|
90
|
+
return result
|
|
91
|
+
|
|
92
|
+
def list_voices_for_model(self, model_id: str) -> list[dict]:
|
|
93
|
+
"""특정 model_id의 voice 목록을 반환합니다."""
|
|
94
|
+
if model_id in BUILTIN_MODELS:
|
|
95
|
+
return BUILTIN_MODELS[model_id].get("voices", [])
|
|
96
|
+
rec = self._reg.get(model_id)
|
|
97
|
+
if rec is None:
|
|
98
|
+
return []
|
|
99
|
+
return rec.get("voices", [])
|
|
100
|
+
|
|
101
|
+
def get(self, model_id: str) -> dict | None:
|
|
102
|
+
"""단일 모델 레코드를 반환합니다. 없으면 None."""
|
|
103
|
+
if model_id in BUILTIN_MODELS:
|
|
104
|
+
return BUILTIN_MODELS[model_id]
|
|
105
|
+
rec = self._reg.get(model_id)
|
|
106
|
+
if rec is None:
|
|
107
|
+
return None
|
|
108
|
+
return self._enrich_onnx_record(rec)
|
|
109
|
+
|
|
110
|
+
def get_state(self, model_id: str) -> ModelState | None:
|
|
111
|
+
"""모델 상태를 반환합니다. 없으면 None."""
|
|
112
|
+
if model_id in BUILTIN_MODELS:
|
|
113
|
+
return ModelState.ACTIVE
|
|
114
|
+
rec = self._reg.get(model_id)
|
|
115
|
+
if rec is None:
|
|
116
|
+
return None
|
|
117
|
+
try:
|
|
118
|
+
return ModelState(rec.get("state", "missing"))
|
|
119
|
+
except ValueError:
|
|
120
|
+
return None
|
|
121
|
+
|
|
122
|
+
# ------------------------------------------------------------------
|
|
123
|
+
# 상태 변경
|
|
124
|
+
# ------------------------------------------------------------------
|
|
125
|
+
|
|
126
|
+
def set_onnx_active(self, model_id: str) -> None:
|
|
127
|
+
"""ONNX 모델을 active 상태로 변경합니다."""
|
|
128
|
+
self._reg.set_state(model_id, ModelState.ACTIVE)
|
|
129
|
+
|
|
130
|
+
def set_onnx_inactive(self, model_id: str) -> None:
|
|
131
|
+
"""ONNX 모델을 inactive 상태로 변경합니다."""
|
|
132
|
+
self._reg.set_state(model_id, ModelState.INACTIVE)
|
|
133
|
+
|
|
134
|
+
def record_onnx_install(self, model_id: str, manifest, verified_at: float) -> None:
|
|
135
|
+
"""ONNX 모델 설치 완료를 기록합니다."""
|
|
136
|
+
self._reg.record_install(model_id, manifest, verified_at)
|
|
137
|
+
|
|
138
|
+
def set_onnx_state(self, model_id: str, state: ModelState, *, last_error: str | None = None) -> None:
|
|
139
|
+
"""ONNX 모델 상태를 직접 갱신합니다."""
|
|
140
|
+
self._reg.set_state(model_id, state, last_error=last_error)
|
|
141
|
+
|
|
142
|
+
# ------------------------------------------------------------------
|
|
143
|
+
# 내부 헬퍼
|
|
144
|
+
# ------------------------------------------------------------------
|
|
145
|
+
|
|
146
|
+
def _enrich_onnx_record(self, rec: dict) -> dict:
|
|
147
|
+
"""ONNX 레코드에 engine/voices 필드가 없으면 기본값을 추가합니다."""
|
|
148
|
+
enriched = dict(rec)
|
|
149
|
+
enriched.setdefault("engine", "onnx")
|
|
150
|
+
enriched.setdefault("voices", [])
|
|
151
|
+
return enriched
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
"""
|
|
2
|
+
termux_tts.control.status — TTS 상태 파일 Writer + Heartbeat
|
|
3
|
+
"""
|
|
4
|
+
from __future__ import annotations
|
|
5
|
+
from typing import Any
|
|
6
|
+
from ameva_component.heartbeat import HeartbeatWriter
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class TTSStatusWriter(HeartbeatWriter):
|
|
10
|
+
"""TTSControl 상태를 10초마다 상태 파일에 원자적으로 기록합니다."""
|
|
11
|
+
|
|
12
|
+
def __init__(self, control: Any) -> None:
|
|
13
|
+
super().__init__(control, name="tts")
|
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Unified 3-Tier Multi-Backend TTS Gateway for termux-tts:
|
|
3
|
+
1. Pure Parametric DSP Formant Synthesizer Engine (ParametricDSPEngine - 0MB Zero-Dependency)
|
|
4
|
+
2. Authentic ONNX Runtime Neural Vocoder Engine (ONNXNeuralEngine - Deep Learning)
|
|
5
|
+
3. Android System Native Samsung/Google Voice Engine Bridge (NativeAndroidEngine)
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
import os
|
|
9
|
+
from typing import Optional, Union, Dict, Any
|
|
10
|
+
|
|
11
|
+
from .exceptions import TTSInferenceError
|
|
12
|
+
from .engine_native import NativeAndroidEngine, NativeResult
|
|
13
|
+
from .engine_dsp import ParametricDSPEngine, DSPResult, QUALITY_PRESETS
|
|
14
|
+
from .engine_onnx import ONNXNeuralEngine, ONNXResult
|
|
15
|
+
|
|
16
|
+
class TTSEngine:
|
|
17
|
+
"""Production Multi-Backend Gateway supporting DSP, ONNX, and Native speech engines."""
|
|
18
|
+
|
|
19
|
+
def __init__(
|
|
20
|
+
self,
|
|
21
|
+
model_path: Optional[str] = None,
|
|
22
|
+
language: str = "ko",
|
|
23
|
+
preset: str = "balanced",
|
|
24
|
+
device: str = "auto",
|
|
25
|
+
sample_rate: Optional[int] = None,
|
|
26
|
+
engine_type: str = "auto"
|
|
27
|
+
):
|
|
28
|
+
self.language = language.lower()
|
|
29
|
+
self.preset = preset.lower()
|
|
30
|
+
self.requested_device = device.lower()
|
|
31
|
+
self.requested_engine_type = engine_type.lower()
|
|
32
|
+
self.model_path = model_path
|
|
33
|
+
self.sample_rate = sample_rate
|
|
34
|
+
self._is_closed = False
|
|
35
|
+
|
|
36
|
+
self.native_engine = NativeAndroidEngine(language=language)
|
|
37
|
+
self.synth_engine = self._resolve_synth_engine()
|
|
38
|
+
|
|
39
|
+
def _resolve_synth_engine(self):
|
|
40
|
+
if self.requested_engine_type == "onnx":
|
|
41
|
+
return ONNXNeuralEngine(
|
|
42
|
+
model_path=self.model_path,
|
|
43
|
+
language=self.language,
|
|
44
|
+
preset=self.preset,
|
|
45
|
+
device=self.requested_device,
|
|
46
|
+
sample_rate=self.sample_rate or 22050
|
|
47
|
+
)
|
|
48
|
+
elif self.requested_engine_type in ("dsp", "formant"):
|
|
49
|
+
return ParametricDSPEngine(
|
|
50
|
+
model_path=self.model_path,
|
|
51
|
+
language=self.language,
|
|
52
|
+
preset=self.preset,
|
|
53
|
+
device=self.requested_device,
|
|
54
|
+
sample_rate=self.sample_rate
|
|
55
|
+
)
|
|
56
|
+
elif self.requested_engine_type == "native":
|
|
57
|
+
return self.native_engine
|
|
58
|
+
elif self.requested_engine_type == "auto":
|
|
59
|
+
if self.model_path and os.path.isfile(self.model_path):
|
|
60
|
+
try:
|
|
61
|
+
return ONNXNeuralEngine(
|
|
62
|
+
model_path=self.model_path,
|
|
63
|
+
language=self.language,
|
|
64
|
+
preset=self.preset,
|
|
65
|
+
device=self.requested_device,
|
|
66
|
+
sample_rate=self.sample_rate or 22050
|
|
67
|
+
)
|
|
68
|
+
except Exception:
|
|
69
|
+
pass
|
|
70
|
+
return ParametricDSPEngine(
|
|
71
|
+
model_path=self.model_path,
|
|
72
|
+
language=self.language,
|
|
73
|
+
preset=self.preset,
|
|
74
|
+
device=self.requested_device,
|
|
75
|
+
sample_rate=self.sample_rate
|
|
76
|
+
)
|
|
77
|
+
else:
|
|
78
|
+
raise TTSInferenceError(
|
|
79
|
+
f"Unknown engine_type '{self.requested_engine_type}'. Available: ['auto', 'dsp', 'onnx', 'native']"
|
|
80
|
+
)
|
|
81
|
+
|
|
82
|
+
@property
|
|
83
|
+
def active_backend(self) -> str:
|
|
84
|
+
return getattr(self.synth_engine, "backend", "NATIVE_SYSTEM")
|
|
85
|
+
|
|
86
|
+
@property
|
|
87
|
+
def model_name(self) -> str:
|
|
88
|
+
return getattr(self.synth_engine, "model_name", "native-system-voice")
|
|
89
|
+
|
|
90
|
+
@property
|
|
91
|
+
def binary(self) -> Optional[str]:
|
|
92
|
+
return getattr(self.native_engine, "binary", None)
|
|
93
|
+
|
|
94
|
+
def speak(self, text: str, stream: Optional[str] = None) -> NativeResult:
|
|
95
|
+
"""Speak text directly through physical Android speaker (Native Engine)."""
|
|
96
|
+
if self._is_closed:
|
|
97
|
+
raise TTSInferenceError("Cannot speak: Engine session is closed.")
|
|
98
|
+
return self.native_engine.speak(text, stream=stream)
|
|
99
|
+
|
|
100
|
+
def synthesize(self, text: str, output: Optional[str] = None, speed: float = 1.0, preset: Optional[str] = None) -> Union[DSPResult, ONNXResult]:
|
|
101
|
+
"""Synthesize text into speech audio buffer / WAV file (DSP or ONNX Engine)."""
|
|
102
|
+
if self._is_closed:
|
|
103
|
+
raise TTSInferenceError("Cannot synthesize: Engine session is closed.")
|
|
104
|
+
return self.synth_engine.synthesize(text, output=output, speed=speed, preset=preset)
|
|
105
|
+
|
|
106
|
+
def close(self) -> None:
|
|
107
|
+
self._is_closed = True
|
|
108
|
+
self.native_engine.close()
|
|
109
|
+
if hasattr(self.synth_engine, "close"):
|
|
110
|
+
self.synth_engine.close()
|
|
111
|
+
|
|
112
|
+
def __enter__(self):
|
|
113
|
+
return self
|
|
114
|
+
|
|
115
|
+
def __exit__(self, exc_type, exc_val, exc_tb):
|
|
116
|
+
self.close()
|
|
117
|
+
|
|
118
|
+
def load(
|
|
119
|
+
model: Optional[str] = None,
|
|
120
|
+
language: str = "ko",
|
|
121
|
+
preset: str = "balanced",
|
|
122
|
+
device: str = "auto",
|
|
123
|
+
sample_rate: Optional[int] = None,
|
|
124
|
+
engine: str = "auto"
|
|
125
|
+
) -> TTSEngine:
|
|
126
|
+
return TTSEngine(
|
|
127
|
+
model_path=model,
|
|
128
|
+
language=language,
|
|
129
|
+
preset=preset,
|
|
130
|
+
device=device,
|
|
131
|
+
sample_rate=sample_rate,
|
|
132
|
+
engine_type=engine
|
|
133
|
+
)
|
|
134
|
+
|
|
135
|
+
def doctor() -> Dict[str, Any]:
|
|
136
|
+
try:
|
|
137
|
+
import ameva_vulkan_runtime as avr
|
|
138
|
+
from ameva_vulkan_runtime.adapters import TtsAdapter
|
|
139
|
+
doc = avr.Doctor()
|
|
140
|
+
rep = doc.run_self_test(verbose=False)
|
|
141
|
+
return {
|
|
142
|
+
"doctor_report": rep,
|
|
143
|
+
"overall_success": getattr(rep, "overall_success", False),
|
|
144
|
+
"passed_stages": getattr(rep, "passed_stages", 0),
|
|
145
|
+
"recommended_backend": getattr(rep, "recommended_backend", "cpu"),
|
|
146
|
+
"status": "DIAGNOSED_VIA_AMEVA"
|
|
147
|
+
}
|
|
148
|
+
except Exception as e:
|
|
149
|
+
return {
|
|
150
|
+
"doctor_report": None,
|
|
151
|
+
"overall_success": False,
|
|
152
|
+
"passed_stages": 0,
|
|
153
|
+
"recommended_backend": "cpu_neon",
|
|
154
|
+
"error": str(e),
|
|
155
|
+
"status": "FALLBACK_CPU"
|
|
156
|
+
}
|
|
157
|
+
|