termux-tts 1.4.2 → 1.4.3

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.
@@ -1,104 +1,104 @@
1
- """
2
- Android OS System Native TTS Engine Bridge (Option B).
3
- Directly communicates with Samsung / Google Voice Engine via Termux IPC.
4
- Provides authentic human speech output with zero download overhead.
5
- """
6
-
7
- import os
8
- import time
9
- import subprocess
10
- import shutil
11
- from dataclasses import dataclass
12
- from typing import Optional
13
-
14
- from .exceptions import TTSInferenceError
15
-
16
- @dataclass
17
- class NativeResult:
18
- text: str
19
- output_path: Optional[str]
20
- language: str
21
- pitch: float
22
- rate: float
23
- elapsed_ms: float
24
- engine_name: str
25
-
26
- class NativeAndroidEngine:
27
- """Android System Native TTS Engine Bridge (Samsung / Google Voice)."""
28
-
29
- def __init__(
30
- self,
31
- language: str = "ko",
32
- pitch: float = 1.0,
33
- rate: float = 1.0,
34
- stream: str = "MUSIC"
35
- ):
36
- self.language = language
37
- self.pitch = pitch
38
- self.rate = rate
39
- self.stream = stream
40
- self.binary = self._find_binary()
41
-
42
- def _find_binary(self) -> Optional[str]:
43
- bin_path = shutil.which("termux-tts-speak")
44
- if bin_path and os.access(bin_path, os.X_OK):
45
- return bin_path
46
- default_p = "/data/data/com.termux/files/usr/bin/termux-tts-speak"
47
- if os.path.exists(default_p) and os.access(default_p, os.X_OK):
48
- return default_p
49
- return None
50
-
51
- def speak(self, text: str, stream: Optional[str] = None) -> NativeResult:
52
- """Speak text directly through physical Android device speakers via termux-tts-speak IPC."""
53
- if not text or not text.strip():
54
- raise TTSInferenceError("Input text cannot be empty or whitespace only.")
55
-
56
- if not self.binary:
57
- raise TTSInferenceError(
58
- "[FAIL-FAST] 'termux-tts-speak' binary not found on this system. "
59
- "NativeAndroidEngine requires an Android Termux environment with 'termux-api' installed (run 'pkg install termux-api'). "
60
- "If running on non-Android Linux/macOS/Windows, please use '--engine dsp' or '--engine onnx'."
61
- )
62
-
63
- t0 = time.perf_counter()
64
- target_stream = stream or self.stream
65
- cmd = [
66
- self.binary,
67
- "-l", self.language,
68
- "-p", str(self.pitch),
69
- "-r", str(self.rate),
70
- "-s", target_stream,
71
- text
72
- ]
73
-
74
- try:
75
- res = subprocess.run(cmd, capture_output=True, text=True, timeout=30)
76
- if res.returncode != 0:
77
- err_msg = res.stderr.strip() or f"Process exited with code {res.returncode}"
78
- raise TTSInferenceError(f"Native Android TTS engine execution failed: {err_msg}")
79
- except subprocess.TimeoutExpired as e:
80
- raise TTSInferenceError(f"Native Android TTS execution timed out (30s): {e}") from e
81
- except Exception as e:
82
- raise TTSInferenceError(f"Failed to execute native Android TTS: {e}") from e
83
-
84
- elapsed_ms = (time.perf_counter() - t0) * 1000.0
85
- return NativeResult(
86
- text=text,
87
- output_path=None,
88
- language=self.language,
89
- pitch=self.pitch,
90
- rate=self.rate,
91
- elapsed_ms=elapsed_ms,
92
- engine_name="Android_Native_Voice_Engine"
93
- )
94
-
95
- def close(self) -> None:
96
- pass
97
-
98
- def __enter__(self):
99
- return self
100
-
101
- def __exit__(self, exc_type, exc_val, exc_tb):
102
- self.close()
103
-
104
-
1
+ """
2
+ Android OS System Native TTS Engine Bridge (Option B).
3
+ Directly communicates with Samsung / Google Voice Engine via Termux IPC.
4
+ Provides authentic human speech output with zero download overhead.
5
+ """
6
+
7
+ import os
8
+ import time
9
+ import subprocess
10
+ import shutil
11
+ from dataclasses import dataclass
12
+ from typing import Optional
13
+
14
+ from .exceptions import TTSInferenceError
15
+
16
+ @dataclass
17
+ class NativeResult:
18
+ text: str
19
+ output_path: Optional[str]
20
+ language: str
21
+ pitch: float
22
+ rate: float
23
+ elapsed_ms: float
24
+ engine_name: str
25
+
26
+ class NativeAndroidEngine:
27
+ """Android System Native TTS Engine Bridge (Samsung / Google Voice)."""
28
+
29
+ def __init__(
30
+ self,
31
+ language: str = "ko",
32
+ pitch: float = 1.0,
33
+ rate: float = 1.0,
34
+ stream: str = "MUSIC"
35
+ ):
36
+ self.language = language
37
+ self.pitch = pitch
38
+ self.rate = rate
39
+ self.stream = stream
40
+ self.binary = self._find_binary()
41
+
42
+ def _find_binary(self) -> Optional[str]:
43
+ bin_path = shutil.which("termux-tts-speak")
44
+ if bin_path and os.access(bin_path, os.X_OK):
45
+ return bin_path
46
+ default_p = "/data/data/com.termux/files/usr/bin/termux-tts-speak"
47
+ if os.path.exists(default_p) and os.access(default_p, os.X_OK):
48
+ return default_p
49
+ return None
50
+
51
+ def speak(self, text: str, stream: Optional[str] = None) -> NativeResult:
52
+ """Speak text directly through physical Android device speakers via termux-tts-speak IPC."""
53
+ if not text or not text.strip():
54
+ raise TTSInferenceError("Input text cannot be empty or whitespace only.")
55
+
56
+ if not self.binary:
57
+ raise TTSInferenceError(
58
+ "[FAIL-FAST] 'termux-tts-speak' binary not found on this system. "
59
+ "NativeAndroidEngine requires an Android Termux environment with 'termux-api' installed (run 'pkg install termux-api'). "
60
+ "If running on non-Android Linux/macOS/Windows, please use '--engine dsp' or '--engine onnx'."
61
+ )
62
+
63
+ t0 = time.perf_counter()
64
+ target_stream = stream or self.stream
65
+ cmd = [
66
+ self.binary,
67
+ "-l", self.language,
68
+ "-p", str(self.pitch),
69
+ "-r", str(self.rate),
70
+ "-s", target_stream,
71
+ text
72
+ ]
73
+
74
+ try:
75
+ res = subprocess.run(cmd, capture_output=True, text=True, timeout=30)
76
+ if res.returncode != 0:
77
+ err_msg = res.stderr.strip() or f"Process exited with code {res.returncode}"
78
+ raise TTSInferenceError(f"Native Android TTS engine execution failed: {err_msg}")
79
+ except subprocess.TimeoutExpired as e:
80
+ raise TTSInferenceError(f"Native Android TTS execution timed out (30s): {e}") from e
81
+ except Exception as e:
82
+ raise TTSInferenceError(f"Failed to execute native Android TTS: {e}") from e
83
+
84
+ elapsed_ms = (time.perf_counter() - t0) * 1000.0
85
+ return NativeResult(
86
+ text=text,
87
+ output_path=None,
88
+ language=self.language,
89
+ pitch=self.pitch,
90
+ rate=self.rate,
91
+ elapsed_ms=elapsed_ms,
92
+ engine_name="Android_Native_Voice_Engine"
93
+ )
94
+
95
+ def close(self) -> None:
96
+ pass
97
+
98
+ def __enter__(self):
99
+ return self
100
+
101
+ def __exit__(self, exc_type, exc_val, exc_tb):
102
+ self.close()
103
+
104
+
@@ -1,28 +1,28 @@
1
- """
2
- Domain Specific Exceptions for termux-tts (Strict Fail-Fast Protocol).
3
- Adheres to AOSF-ENG-STD-2026-V1 No-Fallback Governance.
4
- """
5
-
6
- class TTSError(Exception):
7
- """Base exception for all termux-tts domain errors."""
8
- pass
9
-
10
- class TTSModelLoadError(TTSError):
11
- """Raised when the neural TTS model file cannot be loaded or is corrupted."""
12
- pass
13
-
14
- class TTSInferenceError(TTSError):
15
- """Raised when tensor forward pass or audio synthesis fails."""
16
- pass
17
-
18
- class VulkanInitializationError(TTSInferenceError):
19
- """Raised when Vulkan GPU is explicitly requested but unavailable (Strict Fail-Fast)."""
20
- pass
21
-
22
- class TTSAudioEncodingError(TTSError):
23
- """Raised when raw PCM cannot be encoded to standard WAV."""
24
- pass
25
-
26
- class TTSLanguageNotSupportedError(TTSError):
27
- """Raised when the requested language is not supported by the current tokenizer."""
28
- pass
1
+ """
2
+ Domain Specific Exceptions for termux-tts (Strict Fail-Fast Protocol).
3
+ Adheres to AOSF-ENG-STD-2026-V1 No-Fallback Governance.
4
+ """
5
+
6
+ class TTSError(Exception):
7
+ """Base exception for all termux-tts domain errors."""
8
+ pass
9
+
10
+ class TTSModelLoadError(TTSError):
11
+ """Raised when the neural TTS model file cannot be loaded or is corrupted."""
12
+ pass
13
+
14
+ class TTSInferenceError(TTSError):
15
+ """Raised when tensor forward pass or audio synthesis fails."""
16
+ pass
17
+
18
+ class VulkanInitializationError(TTSInferenceError):
19
+ """Raised when Vulkan GPU is explicitly requested but unavailable (Strict Fail-Fast)."""
20
+ pass
21
+
22
+ class TTSAudioEncodingError(TTSError):
23
+ """Raised when raw PCM cannot be encoded to standard WAV."""
24
+ pass
25
+
26
+ class TTSLanguageNotSupportedError(TTSError):
27
+ """Raised when the requested language is not supported by the current tokenizer."""
28
+ pass
@@ -9,10 +9,37 @@ import urllib.request
9
9
  import shutil
10
10
  from pathlib import Path
11
11
 
12
- VULKAN_BINARY_RELEASE_URL = (
13
- "https://github.com/uno-km/termux-sherpa-ncnn/releases/download/"
14
- "v1.0.0-vulkan/sherpa-ncnn-offline-tts-vulkan-arm64.tar.gz"
15
- )
12
+ def get_candidate_vulkan_binary_urls():
13
+ """Generate dynamic candidate endpoints for Vulkan binary provisioner."""
14
+ try:
15
+ from . import __version__
16
+ except Exception:
17
+ __version__ = "1.4.3"
18
+
19
+ urls = []
20
+ custom_tag = os.environ.get("TERMUX_TTS_RELEASE_TAG", "").strip()
21
+ custom_base = os.environ.get("TERMUX_TTS_RELEASE_BASE", "").strip()
22
+
23
+ if custom_base:
24
+ urls.append(f"{custom_base.rstrip('/')}/sherpa-ncnn-offline-tts-vulkan-arm64.tar.gz")
25
+ if custom_tag:
26
+ 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-ncnn-offline-tts-vulkan-arm64.tar.gz")
28
+
29
+ # 1. termux-tts current version SSOT
30
+ current_tag = f"v{__version__}"
31
+ urls.append(f"https://github.com/uno-km/termux-tts/releases/download/{current_tag}/sherpa-ncnn-offline-tts-vulkan-arm64.tar.gz")
32
+
33
+ # 2. termux-tts latest release
34
+ urls.append("https://github.com/uno-km/termux-tts/releases/latest/download/sherpa-ncnn-offline-tts-vulkan-arm64.tar.gz")
35
+
36
+ # 3. ameva-runtime unified SSOT ecosystem release fallback
37
+ urls.append("https://github.com/uno-km/ameva-runtime/releases/latest/download/sherpa-ncnn-offline-tts-vulkan-arm64.tar.gz")
38
+
39
+ # 4. Companion release fallback
40
+ urls.append("https://github.com/uno-km/termux-sherpa-ncnn/releases/download/v1.0.0-vulkan/sherpa-ncnn-offline-tts-vulkan-arm64.tar.gz")
41
+
42
+ return urls
16
43
 
17
44
  MODEL_REGISTRY = {
18
45
  "high": {
@@ -48,8 +75,13 @@ def get_install_paths():
48
75
  return bin_dir, cache_dir
49
76
 
50
77
  def download_with_progress(url: str, dest_path: Path, label: str):
78
+ try:
79
+ from . import __version__
80
+ except Exception:
81
+ __version__ = "1.4.3"
82
+
51
83
  print(f" [DOWNLOADING] {label}...")
52
- req = urllib.request.Request(url, headers={"User-Agent": "termux-tts-installer/1.3.0"})
84
+ req = urllib.request.Request(url, headers={"User-Agent": f"termux-tts-installer/{__version__} (Android; ARM64)"})
53
85
  with urllib.request.urlopen(req) as resp, open(dest_path, "wb") as out_f:
54
86
  total = int(resp.headers.get("Content-Length", 0))
55
87
  downloaded = 0
@@ -69,13 +101,30 @@ def download_with_progress(url: str, dest_path: Path, label: str):
69
101
  def install_vulkan_binary(force: bool = False) -> Path:
70
102
  bin_dir, _ = get_install_paths()
71
103
  binary_path = bin_dir / "sherpa-ncnn-offline-tts"
104
+ prefix_bin = Path(os.environ.get("PREFIX", "/data/data/com.termux/files/usr")) / "bin"
72
105
 
73
106
  if binary_path.exists() and not force:
74
107
  print(f" [OK] Pre-compiled Vulkan binary already exists: {binary_path}")
75
108
  return binary_path
76
109
 
77
110
  tar_path = bin_dir / "sherpa-vulkan.tar.gz"
78
- download_with_progress(VULKAN_BINARY_RELEASE_URL, tar_path, "ARM64 Vulkan Binary (3.9MB)")
111
+ candidate_urls = get_candidate_vulkan_binary_urls()
112
+ download_success = False
113
+
114
+ for url in candidate_urls:
115
+ try:
116
+ download_with_progress(url, tar_path, f"ARM64 Vulkan Binary ({url})")
117
+ if tar_path.exists() and tar_path.stat().st_size > 500 * 1024:
118
+ download_success = True
119
+ break
120
+ except Exception as dl_err:
121
+ print(f" [-] Candidate URL failed ({url}): {dl_err}")
122
+ if tar_path.exists():
123
+ tar_path.unlink(missing_ok=True)
124
+ continue
125
+
126
+ if not download_success:
127
+ raise RuntimeError("Failed to download sherpa-ncnn-offline-tts from any candidate endpoints.")
79
128
 
80
129
  print(" [EXTRACTING] Installing binary to ~/.local/bin...")
81
130
  with tarfile.open(tar_path, "r:gz") as tar:
@@ -83,6 +132,17 @@ def install_vulkan_binary(force: bool = False) -> Path:
83
132
 
84
133
  tar_path.unlink(missing_ok=True)
85
134
  binary_path.chmod(0o755)
135
+
136
+ # Dual-path installation to PREFIX/bin if writable
137
+ try:
138
+ if prefix_bin.exists() and os.access(prefix_bin, os.W_OK):
139
+ target_prefix_bin = prefix_bin / "sherpa-ncnn-offline-tts"
140
+ shutil.copy2(binary_path, target_prefix_bin)
141
+ target_prefix_bin.chmod(0o755)
142
+ print(f" [SYMLINK/COPY] Linked to {target_prefix_bin}")
143
+ except OSError:
144
+ pass
145
+
86
146
  print(f" [SUCCESS] Installed: {binary_path}")
87
147
  return binary_path
88
148