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.
@@ -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
- import numpy as np
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
- with open(filepath, "wb") as f:
96
+ target.parent.mkdir(parents=True, exist_ok=True)
97
+ with open(target, "wb") as f:
95
98
  f.write(wav_data)
96
- return filepath
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("-t", "--text", required=True, help="Input text to synthesize")
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="ko", help="Language code (ko, en)")
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(args.text, output=args.output, speed=args.speed)
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 args.output and os.path.exists(args.output):
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", args.output], check=False)
115
+ subprocess.run(["termux-media-player", "play", out_path], check=False)
99
116
  elif shutil.which("play-audio"):
100
- subprocess.run(["play-audio", args.output], check=False)
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