termux-tts 1.4.3 → 1.4.4
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/.github/workflows/release.yml +98 -98
- package/CHANGELOG.md +69 -57
- package/LICENSE +17 -17
- package/README.md +138 -441
- package/README.pypi.md +54 -441
- package/bin/cli.js +33 -33
- package/doc.config.yaml +596 -532
- package/docs/benchmarks.md +11 -11
- package/index.js +5 -5
- package/install.sh +53 -53
- package/package.json +57 -57
- package/pyproject.toml +81 -81
- package/setup.py +38 -38
- package/termux_tts/__init__.py +74 -64
- package/termux_tts/audio.py +6 -3
- package/termux_tts/cli.py +27 -10
- package/termux_tts/control/component.py +398 -398
- package/termux_tts/engine.py +73 -7
- package/termux_tts/engine_multilingual.py +374 -0
- package/termux_tts/engine_native.py +104 -104
- package/termux_tts/engine_sherpa.py +2 -1
- package/termux_tts/engine_sherpa_capi.py +491 -0
- package/termux_tts/exceptions.py +28 -28
- package/termux_tts/hardware.py +34 -1
- package/termux_tts/installer.py +154 -12
- package/termux_tts/script_classifier.py +253 -0
- package/termux_tts/tokenizer.py +186 -184
package/termux_tts/audio.py
CHANGED
|
@@ -5,8 +5,9 @@ Handles 16-bit Linear PCM formatting with soft-clipping protection.
|
|
|
5
5
|
|
|
6
6
|
import io
|
|
7
7
|
import wave
|
|
8
|
-
|
|
8
|
+
from pathlib import Path
|
|
9
9
|
from typing import Union
|
|
10
|
+
import numpy as np
|
|
10
11
|
from .exceptions import TTSAudioEncodingError
|
|
11
12
|
|
|
12
13
|
class AudioBuffer:
|
|
@@ -89,10 +90,12 @@ class AudioBuffer:
|
|
|
89
90
|
|
|
90
91
|
def save(self, filepath: str) -> str:
|
|
91
92
|
"""Save the audio buffer to a WAV file on disk."""
|
|
93
|
+
target = Path(filepath).expanduser().resolve()
|
|
92
94
|
wav_data = self.to_wav_bytes()
|
|
93
95
|
try:
|
|
94
|
-
|
|
96
|
+
target.parent.mkdir(parents=True, exist_ok=True)
|
|
97
|
+
with open(target, "wb") as f:
|
|
95
98
|
f.write(wav_data)
|
|
96
|
-
return
|
|
99
|
+
return str(target)
|
|
97
100
|
except Exception as e:
|
|
98
101
|
raise TTSAudioEncodingError(f"Failed to save WAV to '{filepath}': {e}") from e
|
package/termux_tts/cli.py
CHANGED
|
@@ -11,6 +11,12 @@ import argparse
|
|
|
11
11
|
from .engine import load, doctor
|
|
12
12
|
|
|
13
13
|
def main():
|
|
14
|
+
# Route to default subcommand 'synth' if no recognized subcommand is given
|
|
15
|
+
KNOWN_COMMANDS = {"synth", "speak", "doctor", "install", "component", "model", "instance"}
|
|
16
|
+
raw_args = sys.argv[1:]
|
|
17
|
+
if raw_args and raw_args[0] not in KNOWN_COMMANDS and raw_args[0] not in ("-h", "--help", "-v", "--version"):
|
|
18
|
+
sys.argv.insert(1, "synth")
|
|
19
|
+
|
|
14
20
|
parser = argparse.ArgumentParser(
|
|
15
21
|
prog="termux-tts",
|
|
16
22
|
description="Termux Neural & Native Text-to-Speech Engine"
|
|
@@ -19,13 +25,14 @@ def main():
|
|
|
19
25
|
|
|
20
26
|
# 1. Synth (4-Tier Speech Synthesis)
|
|
21
27
|
synth_parser = subparsers.add_parser("synth", help="Synthesize text to audio WAV file (Synth, Neural, Expressive)")
|
|
22
|
-
synth_parser.add_argument("
|
|
28
|
+
synth_parser.add_argument("text_pos", nargs="*", default=[], help="Input text to synthesize (positional)")
|
|
29
|
+
synth_parser.add_argument("-t", "--text", default=None, help="Input text to synthesize (flag)")
|
|
23
30
|
synth_parser.add_argument("-o", "--output", default="output.wav", help="Output WAV filepath")
|
|
24
|
-
synth_parser.add_argument("-l", "--lang", default="
|
|
31
|
+
synth_parser.add_argument("-l", "--lang", default="auto", help="Language code (auto=Multi-language auto switch, ko/kor=Korean only, en/eng=English only, ja/jpn=Japanese only)")
|
|
25
32
|
synth_parser.add_argument(
|
|
26
33
|
"-e", "--engine", default="auto",
|
|
27
|
-
choices=["auto", "vulkan", "ncnn", "gpu", "synth", "dsp", "native", "neural", "onnx", "expressive"],
|
|
28
|
-
help="Synthesis engine tier (vulkan=GPU NCNN, synth=0MB DSP, native=Android voice, neural=VITS C++, expressive=emotional)"
|
|
34
|
+
choices=["auto", "vulkan", "ncnn", "gpu", "synth", "dsp", "native", "neural", "onnx", "expressive", "multilingual", "hybrid"],
|
|
35
|
+
help="Synthesis engine tier (auto=Smart Routing, vulkan=GPU NCNN, synth=0MB DSP, native=Android voice, neural=VITS C++, expressive=emotional, multilingual=Cross-language)"
|
|
29
36
|
)
|
|
30
37
|
synth_parser.add_argument("-m", "--model", default=None, help="Path to model file or directory")
|
|
31
38
|
synth_parser.add_argument("-p", "--preset", default="balanced", choices=["fast", "balanced", "expressive", "ultra"])
|
|
@@ -52,6 +59,10 @@ def main():
|
|
|
52
59
|
# 4. Install (One-Click Automated Provisioner)
|
|
53
60
|
install_parser = subparsers.add_parser("install", help="1-Click download and provision precompiled Vulkan binary & VITS studio models")
|
|
54
61
|
install_parser.add_argument("--tier", default="high", choices=["high", "medium"], help="Model resolution tier (high=57MB Studio FP16, medium=25MB Fast)")
|
|
62
|
+
install_parser.add_argument(
|
|
63
|
+
"--models", default="default",
|
|
64
|
+
help="Language model packages to provision: default (Korean & English only), or specific code: hi, ja, zh, ru, es, fr, de, or 'all'"
|
|
65
|
+
)
|
|
55
66
|
install_parser.add_argument("--force", action="store_true", help="Force overwrite existing binary and model assets")
|
|
56
67
|
install_parser.add_argument("--no-play", action="store_true", help="Skip playback verification during self-test")
|
|
57
68
|
|
|
@@ -75,6 +86,12 @@ def main():
|
|
|
75
86
|
subprocess.run(["termux-volume", "music", str(args.volume)], check=False)
|
|
76
87
|
|
|
77
88
|
if args.command == "synth":
|
|
89
|
+
pos_text = " ".join(args.text_pos).strip() if getattr(args, "text_pos", None) else None
|
|
90
|
+
target_text = args.text or (pos_text if pos_text else None)
|
|
91
|
+
if not target_text:
|
|
92
|
+
synth_parser.error("the following arguments are required: text (as positional arguments or -t/--text)")
|
|
93
|
+
|
|
94
|
+
out_path = os.path.expanduser(args.output) if args.output else None
|
|
78
95
|
with load(
|
|
79
96
|
model=args.model,
|
|
80
97
|
language=args.lang,
|
|
@@ -84,20 +101,20 @@ def main():
|
|
|
84
101
|
engine=args.engine,
|
|
85
102
|
tier=getattr(args, "tier", None),
|
|
86
103
|
) as engine:
|
|
87
|
-
res = engine.synthesize(
|
|
104
|
+
res = engine.synthesize(target_text, output=out_path, speed=args.speed, language=args.lang)
|
|
88
105
|
backend_name = getattr(res, "backend", "UNKNOWN")
|
|
89
106
|
model_name = getattr(res, "model_name", "model")
|
|
90
107
|
dur = getattr(res, "duration_sec", 0.0)
|
|
91
108
|
elapsed = getattr(res, "elapsed_ms", 0.0)
|
|
92
109
|
rtf = getattr(res, "rtf", 0.0)
|
|
93
|
-
print(f"[SUCCESS] Synthesized via {backend_name} ({model_name}) -> {args.output}")
|
|
110
|
+
print(f"[SUCCESS] Synthesized via {backend_name} ({model_name}) -> {out_path or args.output}")
|
|
94
111
|
print(f" Duration: {dur:.2f}s | Elapsed: {elapsed:.1f}ms | RTF: {rtf:.4f}x")
|
|
95
112
|
|
|
96
|
-
if args.play and
|
|
113
|
+
if args.play and out_path and os.path.exists(out_path):
|
|
97
114
|
if shutil.which("termux-media-player"):
|
|
98
|
-
subprocess.run(["termux-media-player", "play",
|
|
115
|
+
subprocess.run(["termux-media-player", "play", out_path], check=False)
|
|
99
116
|
elif shutil.which("play-audio"):
|
|
100
|
-
subprocess.run(["play-audio",
|
|
117
|
+
subprocess.run(["play-audio", out_path], check=False)
|
|
101
118
|
|
|
102
119
|
elif args.command == "speak":
|
|
103
120
|
with load(language=args.lang) as engine:
|
|
@@ -115,7 +132,7 @@ def main():
|
|
|
115
132
|
|
|
116
133
|
elif args.command == "install":
|
|
117
134
|
from .installer import run_installation
|
|
118
|
-
run_installation(tier=args.tier, force=args.force, play=not args.no_play)
|
|
135
|
+
run_installation(tier=args.tier, models=getattr(args, "models", "all"), force=args.force, play=not args.no_play)
|
|
119
136
|
|
|
120
137
|
elif args.command in ("component", "model", "instance") and _protocol_available:
|
|
121
138
|
from ameva_component.cli_support import dispatch_protocol
|