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.
Files changed (43) hide show
  1. package/LICENSE +17 -0
  2. package/README.md +31 -0
  3. package/README.pypi.md +47 -0
  4. package/bin/cli.js +33 -0
  5. package/binding_node/index.js +117 -0
  6. package/binding_node/test_node.js +40 -0
  7. package/doc.config.yaml +98 -0
  8. package/docs/benchmarks.md +12 -0
  9. package/docs/guide.md +552 -0
  10. package/docs/tts_guide.md +552 -0
  11. package/dsp_test.wav +0 -0
  12. package/expressive_demo.wav +0 -0
  13. package/g2p_test.wav +0 -0
  14. package/index.js +5 -0
  15. package/install.sh +53 -0
  16. package/package.json +31 -0
  17. package/pyproject.toml +43 -0
  18. package/setup.py +38 -0
  19. package/termux_tts/__init__.py +47 -0
  20. package/termux_tts/adapter.py +99 -0
  21. package/termux_tts/audio.py +75 -0
  22. package/termux_tts/cli.py +82 -0
  23. package/termux_tts/control/__init__.py +4 -0
  24. package/termux_tts/control/component.py +242 -0
  25. package/termux_tts/control/errors.py +52 -0
  26. package/termux_tts/control/instances.py +137 -0
  27. package/termux_tts/control/models.py +151 -0
  28. package/termux_tts/control/status.py +13 -0
  29. package/termux_tts/engine.py +157 -0
  30. package/termux_tts/engine_dsp.py +332 -0
  31. package/termux_tts/engine_native.py +104 -0
  32. package/termux_tts/engine_onnx.py +199 -0
  33. package/termux_tts/exceptions.py +28 -0
  34. package/termux_tts/g2p_korean.py +242 -0
  35. package/termux_tts/tokenizer.py +184 -0
  36. package/termux_tts/vulkan_probe.py +25 -0
  37. package/test_cli.wav +0 -0
  38. package/tests/test_expressive_presets.py +29 -0
  39. package/tests/test_g2p_korean.py +73 -0
  40. package/tests/test_granular_tts.py +141 -0
  41. package/tests/test_native_engine.py +38 -0
  42. package/tests/test_onnx_engine.py +48 -0
  43. package/tests/test_vulkan_routing.py +52 -0
@@ -0,0 +1,141 @@
1
+ """
2
+ Production-Grade Invariant & Acoustic Safety Test Suite for termux-tts.
3
+ Validates G2P correctness, Number Normalization, Acoustic Signal Energy, Filter Stability, and Security Bounds.
4
+ """
5
+
6
+ import os
7
+ import time
8
+ import pytest
9
+ import numpy as np
10
+
11
+ import termux_tts as tts
12
+ from termux_tts.tokenizer import PhoneticTokenizer, decompose_hangul
13
+ from termux_tts.audio import AudioBuffer
14
+ from termux_tts.engine import TTSEngine, load
15
+ from termux_tts.engine_dsp import apply_biquad_resonator
16
+ from termux_tts.exceptions import (
17
+ TTSError,
18
+ TTSModelLoadError,
19
+ TTSInferenceError,
20
+ TTSAudioEncodingError,
21
+ TTSLanguageNotSupportedError,
22
+ )
23
+
24
+ # ==============================================================================
25
+ # 1. Phonetic Tokenization & Number Normalization Invariants
26
+ # ==============================================================================
27
+
28
+ def test_hangul_jamo_decomposition():
29
+ jamos = decompose_hangul("한")
30
+ assert jamos == ["ㅎ", "ㅏ", "ㄴ"]
31
+ jamos_no_jong = decompose_hangul("가")
32
+ assert jamos_no_jong == ["ㄱ", "ㅏ"]
33
+
34
+ def test_korean_number_normalization():
35
+ from termux_tts.tokenizer import normalize_numbers_korean
36
+ tok = PhoneticTokenizer(language="ko")
37
+ # 1. Place-value Sino-Korean string conversions
38
+ assert normalize_numbers_korean("100") == "백"
39
+ assert normalize_numbers_korean("1234") == "천이백삼십사"
40
+ assert normalize_numbers_korean("10000") == "만"
41
+ assert normalize_numbers_korean("2026년 9월 1일") == "이천이십육년 구월 일일"
42
+
43
+ # 2. G2P-applied phonetic normalization
44
+ phonetic_1234 = tok.normalize_text("1234")
45
+ assert "처니" in phonetic_1234 # Liaison: 천+이 -> 처니
46
+ assert "백" in phonetic_1234
47
+
48
+ tokens = tok.tokenize("2026년 9월 1일")
49
+ assert len(tokens) > 10
50
+ assert all(isinstance(t, int) for t in tokens)
51
+
52
+ def test_english_number_and_word_tokenization():
53
+ tok = PhoneticTokenizer(language="en")
54
+ norm_text = tok.normalize_text("System 123 active")
55
+ assert "one" in norm_text
56
+ assert "two" in norm_text
57
+ assert "three" in norm_text
58
+ tokens = tok.tokenize("System 123 active")
59
+ assert len(tokens) > 10
60
+
61
+ def test_unsupported_language_guard():
62
+ with pytest.raises(TTSLanguageNotSupportedError):
63
+ PhoneticTokenizer(language="fr_unsupported")
64
+
65
+ # ==============================================================================
66
+ # 2. Audio Buffer & Acoustic Signal Invariants
67
+ # ==============================================================================
68
+
69
+ def test_audio_buffer_normalization():
70
+ raw = np.array([2.5, -3.0, 1.0, 0.5], dtype=np.float32)
71
+ buf = AudioBuffer(raw, sample_rate=22050)
72
+ assert np.max(np.abs(buf.samples)) <= 1.0
73
+ assert not np.isnan(buf.samples).any()
74
+ assert not np.isinf(buf.samples).any()
75
+
76
+ def test_riff_wav_byte_encoding():
77
+ raw = np.sin(np.linspace(0, 2 * np.pi * 440, 2205, dtype=np.float32))
78
+ buf = AudioBuffer(raw, sample_rate=22050)
79
+ wav_bytes = buf.to_wav_bytes()
80
+ assert wav_bytes.startswith(b"RIFF")
81
+ assert b"WAVE" in wav_bytes
82
+ assert len(wav_bytes) > 2205 * 2
83
+
84
+ def test_biquad_resonator_stability():
85
+ """Verify Biquad resonator does not explode or produce NaNs."""
86
+ impulse = np.zeros(1000, dtype=np.float32)
87
+ impulse[0] = 1.0
88
+ filtered = apply_biquad_resonator(impulse, f_res=800.0, bandwidth=80.0, sr=22050)
89
+ assert not np.isnan(filtered).any()
90
+ assert not np.isinf(filtered).any()
91
+ assert np.max(np.abs(filtered)) < 5.0
92
+
93
+ # ==============================================================================
94
+ # 3. Acoustic Synthesis & Fail-Fast Integrity
95
+ # ==============================================================================
96
+
97
+ def test_neural_synthesis_korean_acoustics():
98
+ with load(language="ko") as engine:
99
+ res = engine.synthesize("안녕하세요, 텀묵스 음향 합성 무결성 검증입니다.")
100
+ assert res.duration_sec > 0.5
101
+ assert res.sample_rate == 22050
102
+ assert res.rtf < 0.5
103
+ assert len(res.wav_bytes) > 1000
104
+ # Acoustic energy check: verify non-silent valid speech signal
105
+ rms = np.sqrt(np.mean(res.audio_buffer.samples ** 2))
106
+ assert rms > 0.005, f"Audio signal is virtually silent (RMS={rms})"
107
+
108
+ def test_neural_synthesis_english_acoustics():
109
+ with load(language="en") as engine:
110
+ res = engine.synthesize("Hello world, this is termux speech synthesis.")
111
+ assert res.duration_sec > 0.5
112
+ rms = np.sqrt(np.mean(res.audio_buffer.samples ** 2))
113
+ assert rms > 0.005
114
+
115
+ def test_zero_fallback_error_guards():
116
+ with load(language="ko") as engine:
117
+ with pytest.raises(TTSInferenceError):
118
+ engine.synthesize("")
119
+ with pytest.raises(TTSInferenceError):
120
+ engine.synthesize(" ")
121
+ with pytest.raises(TTSInferenceError):
122
+ engine.synthesize("Hello", speed=-1.0)
123
+ with pytest.raises(TTSInferenceError):
124
+ engine.synthesize("Hello", speed=5.0)
125
+
126
+ # ==============================================================================
127
+ # 4. Lifecycle & Performance Scaling
128
+ # ==============================================================================
129
+
130
+ def test_raii_context_manager_lifecycle():
131
+ with load(language="ko") as engine:
132
+ assert not engine._is_closed
133
+ assert engine._is_closed
134
+ with pytest.raises(TTSInferenceError):
135
+ engine.synthesize("Valid text")
136
+
137
+ def test_speed_scaling_contract():
138
+ with load(language="ko") as engine:
139
+ res_normal = engine.synthesize("속도 테스트 문장입니다.", speed=1.0)
140
+ res_fast = engine.synthesize("속도 테스트 문장입니다.", speed=2.0)
141
+ assert res_fast.duration_sec < res_normal.duration_sec
@@ -0,0 +1,38 @@
1
+ """
2
+ Unit Tests for Option B (Android Native System Voice Engine Bridge).
3
+ """
4
+
5
+ import pytest
6
+ import termux_tts as tts
7
+ from termux_tts.engine_native import NativeAndroidEngine
8
+ from termux_tts.exceptions import TTSInferenceError
9
+
10
+ def test_native_engine_initialization():
11
+ engine = NativeAndroidEngine(language="ko", pitch=1.0, rate=1.0, stream="MUSIC")
12
+ assert engine.language == "ko"
13
+ assert engine.stream == "MUSIC"
14
+
15
+ def test_native_engine_speak_empty_guard():
16
+ engine = NativeAndroidEngine(language="ko")
17
+ with pytest.raises(TTSInferenceError):
18
+ engine.speak("")
19
+ with pytest.raises(TTSInferenceError):
20
+ engine.speak(" ")
21
+
22
+ def test_native_engine_speak_execution():
23
+ with tts.load(engine="native", language="ko") as engine:
24
+ if not engine.binary:
25
+ # 1. Non-Android host must fail-fast with strict TTSInferenceError (Zero-Fallback)
26
+ with pytest.raises(TTSInferenceError) as excinfo:
27
+ engine.speak("테스트 발화입니다.")
28
+ assert "FAIL-FAST" in str(excinfo.value)
29
+ print(f"\n[PASS NATIVE FAIL-FAST] Correctly rejected: {excinfo.value}")
30
+ else:
31
+ # 2. Genuine Android Termux system execution
32
+ res = engine.speak("테스트 발화입니다.")
33
+ assert res.engine_name == "Android_Native_Voice_Engine"
34
+ assert res.language == "ko"
35
+ assert res.elapsed_ms >= 0.0
36
+ print(f"\n[PASS NATIVE SPEAK] Engine: {res.engine_name} | Elapsed: {res.elapsed_ms:.2f}ms")
37
+
38
+
@@ -0,0 +1,48 @@
1
+ """
2
+ Unit and Integration Tests for Real ONNX Neural Inference Engine & Gateway Routing.
3
+ """
4
+
5
+ import os
6
+ import pytest
7
+ import numpy as np
8
+
9
+ import termux_tts as tts
10
+ from termux_tts.engine_onnx import ONNXNeuralEngine
11
+ from termux_tts.engine_dsp import ParametricDSPEngine
12
+ from termux_tts.exceptions import TTSModelLoadError, TTSInferenceError
13
+
14
+ def test_onnx_engine_missing_model_fail_fast():
15
+ """Verify ONNXNeuralEngine raises TTSModelLoadError when model_path is None or onnxruntime missing."""
16
+ with pytest.raises(TTSModelLoadError) as excinfo:
17
+ ONNXNeuralEngine(model_path=None)
18
+ err_str = str(excinfo.value)
19
+ assert "model_path" in err_str or "onnxruntime" in err_str
20
+
21
+ def test_onnx_engine_nonexistent_file_fail_fast():
22
+ """Verify ONNXNeuralEngine raises TTSModelLoadError when file does not exist or onnxruntime missing."""
23
+ with pytest.raises(TTSModelLoadError) as excinfo:
24
+ ONNXNeuralEngine(model_path="nonexistent_vits_model.onnx")
25
+ err_str = str(excinfo.value)
26
+ assert "does not exist" in err_str or "onnxruntime" in err_str
27
+
28
+ def test_dsp_engine_explicit_execution():
29
+ """Verify ParametricDSPEngine executes with zero model dependencies."""
30
+ with tts.load(engine="dsp", language="ko") as engine:
31
+ assert isinstance(engine.synth_engine, ParametricDSPEngine)
32
+ res = engine.synthesize("DSP 포먼트 엔진 단독 구동 테스트입니다.")
33
+ assert res.duration_sec > 0.3
34
+ assert res.model_name == "parametric-formant-dsp"
35
+
36
+ def test_gateway_auto_routing_to_dsp():
37
+ """Verify TTSEngine defaults to ParametricDSPEngine in auto mode when no model file is given."""
38
+ with tts.load(engine="auto", language="ko") as engine:
39
+ assert isinstance(engine.synth_engine, ParametricDSPEngine)
40
+ res = engine.synthesize("자동 라우팅 테스트입니다.")
41
+ assert res.duration_sec > 0.3
42
+
43
+ def test_gateway_explicit_onnx_mode_requires_model():
44
+ """Verify tts.load(engine='onnx') fails fast if no model file is given."""
45
+ with pytest.raises(TTSModelLoadError):
46
+ with tts.load(engine="onnx", language="ko") as engine:
47
+ engine.synthesize("신경망 테스트")
48
+
@@ -0,0 +1,52 @@
1
+ """
2
+ Strict Dual Routing & Zero-Fallback Fail-Fast Verification for termux-tts.
3
+ Tests:
4
+ 1. Explicit Vulkan mode (--device vulkan) -> Strict Fail-Fast on unavailable systems.
5
+ 2. Auto mode (--device auto) -> Auto-detection and seamless transition to CPU.
6
+ 3. Explicit CPU mode (--device cpu) -> Pure CPU execution.
7
+ """
8
+
9
+ import pytest
10
+ import termux_tts as tts
11
+ from termux_tts.exceptions import VulkanInitializationError, TTSInferenceError
12
+ from termux_tts.engine import load
13
+
14
+ def test_explicit_cpu_mode():
15
+ with load(language="ko", device="cpu") as engine:
16
+ res = engine.synthesize("CPU 전용 모드 테스트입니다.")
17
+ assert "CPU" in res.backend
18
+ assert res.duration_sec > 0.3
19
+ print(f"\n[PASS CPU MODE] Backend: {res.backend}")
20
+
21
+ def test_auto_routing_mode():
22
+ with load(language="ko", device="auto") as engine:
23
+ res = engine.synthesize("자동 라우팅 모드 테스트입니다.")
24
+ assert res.backend in ["VULKAN_GPU", "ARM64_NEON_CPU", "X86_64_AVX2_CPU", "X86_CPU"]
25
+ print(f"\n[PASS AUTO ROUTING] Resolved Backend: {res.backend}")
26
+
27
+ def test_explicit_vulkan_fail_fast_when_disabled(monkeypatch):
28
+ from termux_tts import engine_dsp as dsp_mod
29
+ from termux_tts import engine_onnx as onnx_mod
30
+ from termux_tts import engine as eng_mod
31
+
32
+ class FakeDoctorDisabled:
33
+ is_vulkan_available = False
34
+ def probe_all(self):
35
+ return {"V0_LoaderOpen": "FAIL"}
36
+
37
+ monkeypatch.setattr(dsp_mod, "VulkanDoctor", FakeDoctorDisabled)
38
+ monkeypatch.setattr(onnx_mod, "VulkanDoctor", FakeDoctorDisabled)
39
+ monkeypatch.setattr(eng_mod, "VulkanDoctor", FakeDoctorDisabled)
40
+
41
+ # 1. Explicit Vulkan MUST RAISE VulkanInitializationError (No silent fallback!)
42
+ with pytest.raises(VulkanInitializationError) as exc_info:
43
+ load(language="ko", device="vulkan")
44
+ assert "FAIL-FAST" in str(exc_info.value)
45
+ assert "--device cpu" in str(exc_info.value)
46
+ print(f"\n[PASS FAIL-FAST GUARD] Correctly rejected with: {exc_info.value}")
47
+
48
+ # 2. Auto mode MUST gracefully route to CPU
49
+ with load(language="ko", device="auto") as engine:
50
+ res = engine.synthesize("Vulkan 없을 때 자동 CPU 전환 테스트.")
51
+ assert "CPU" in res.backend
52
+ print(f"\n[PASS AUTO DEGRADE TO CPU] Gracefully resolved: {res.backend}")