Advertisement
Guest User

tts_box.py

a guest
May 29th, 2025
45
0
Never
Not a member of Pastebin yet? Sign Up, it unlocks many cool features!
Python 8.03 KB | Source Code | 0 0
  1. # Simple GUI to let you paste text and have it read aloud, or save as file, w/ voice dropdown.
  2. # Requires Python ≥3.8 (tested on 3.12)
  3. # OS dependencies: libasound2t64 ffmpeg libsndfile1
  4. # pip install -U edge-tts pydub aiohttp
  5. """
  6. TTS Box — single‑file neural TTS (Edge‑TTS)
  7. ──────────────────────────────────────────
  8. • Voice dropdown (default **en‑GB‑RyanNeural**)
  9. • **Read** → synthesize → encode → play via ffplay
  10. • **Save WAV** / **Save MP3** (compact 32 kHz 48 kbps mono)
  11. • Progress bar + status text show stage (Synthesizing ▸ Encoding ▸ Playing/Saving)
  12. • Save dialogs default to Desktop if the folder exists, else Home
  13. """
  14.  
  15. import tkinter as tk
  16. from tkinter import filedialog, messagebox, ttk
  17. from tkinter.scrolledtext import ScrolledText
  18. import threading
  19. import asyncio
  20. import tempfile
  21. import subprocess
  22. import os
  23. import signal
  24.  
  25. import edge_tts  # type: ignore
  26. from pydub import AudioSegment
  27. from aiohttp.client_exceptions import ClientConnectionError
  28.  
  29. # ---------- Paths ----------
  30. HOME = os.path.expanduser("~")
  31. DESKTOP_DIR = os.path.join(HOME, "Desktop") if os.path.isdir(os.path.join(HOME, "Desktop")) else HOME
  32.  
  33. # ---------- Voices ----------
  34. VOICE_OPTIONS = [
  35.     "en-US-JennyNeural",
  36.     "en-US-GuyNeural",
  37.     "en-US-AriaNeural",
  38.     "en-US-DavisNeural",
  39.     "en-GB-SoniaNeural",
  40.     "en-GB-RyanNeural",
  41. ]
  42. VOICE_DEFAULT = "en-GB-RyanNeural"
  43. RATE_DEFAULT = "+0%"
  44.  
  45. # ---------- Encoding params ----------
  46. SMALL_MP3_PARAMS = {
  47.     "format": "mp3",
  48.     "bitrate": "48k",
  49.     "parameters": ["-ar", "32000", "-ac", "1"],
  50. }
  51.  
  52.  
  53. class ReaderApp:
  54.     def __init__(self, root: tk.Tk):
  55.         self.root = root
  56.         self.root.title("TTS Box")
  57.         self.root.protocol("WM_DELETE_WINDOW", self.on_close)
  58.  
  59.         # Text input
  60.         self.text_box = ScrolledText(root, wrap=tk.WORD, width=80, height=18)
  61.         self.text_box.pack(fill=tk.BOTH, expand=True, padx=10, pady=10)
  62.  
  63.         # Controls
  64.         ctrl = tk.Frame(root)
  65.         ctrl.pack(pady=(0, 10))
  66.         tk.Label(ctrl, text="Voice:").pack(side=tk.LEFT)
  67.         self.voice_var = tk.StringVar(value=VOICE_DEFAULT)
  68.         tk.OptionMenu(ctrl, self.voice_var, *VOICE_OPTIONS).pack(side=tk.LEFT, padx=5)
  69.  
  70.         self.read_btn = tk.Button(ctrl, text="Read", command=self.start_read)
  71.         self.read_btn.pack(side=tk.LEFT, padx=5)
  72.         self.stop_btn = tk.Button(ctrl, text="Stop", command=self.stop_read, state=tk.DISABLED)
  73.         self.stop_btn.pack(side=tk.LEFT, padx=5)
  74.         self.save_wav_btn = tk.Button(ctrl, text="Save WAV", command=lambda: self.save_audio("wav"))
  75.         self.save_wav_btn.pack(side=tk.LEFT, padx=5)
  76.         self.save_mp3_btn = tk.Button(ctrl, text="Save MP3", command=lambda: self.save_audio("mp3"))
  77.         self.save_mp3_btn.pack(side=tk.LEFT, padx=5)
  78.  
  79.         # Status + progress
  80.         self.status_var = tk.StringVar(value="Idle")
  81.         ttk.Label(root, textvariable=self.status_var).pack(fill=tk.X, padx=10)
  82.         self.progress = ttk.Progressbar(root, mode="indeterminate")
  83.         self.progress.pack(fill=tk.X, padx=10, pady=(0, 10))
  84.  
  85.         # Internal
  86.         self.proc: subprocess.Popen | None = None
  87.         self.worker: threading.Thread | None = None
  88.  
  89.     # ---------- Progress helpers ----------
  90.     def _progress_start(self, msg: str):
  91.         self.status_var.set(msg)
  92.         self.progress.start(10)
  93.  
  94.     def _progress_update(self, msg: str):
  95.         self.status_var.set(msg)
  96.  
  97.     def _progress_stop(self):
  98.         self.progress.stop()
  99.         self.progress["value"] = 0
  100.         self.status_var.set("Idle")
  101.  
  102.     # ---------- UI actions ----------
  103.     def start_read(self):
  104.         if self.worker and self.worker.is_alive():
  105.             return
  106.         text = self._get_text()
  107.         if not text:
  108.             return
  109.         self.read_btn.config(state=tk.DISABLED)
  110.         self.stop_btn.config(state=tk.NORMAL)
  111.         self._progress_start("Synthesizing…")
  112.         self.worker = threading.Thread(target=self._read_flow, args=(text,), daemon=True)
  113.         self.worker.start()
  114.  
  115.     def stop_read(self):
  116.         if self.proc and self.proc.poll() is None:
  117.             try:
  118.                 self.proc.send_signal(signal.SIGINT)
  119.             except Exception:
  120.                 self.proc.kill()
  121.         self._reset_buttons()
  122.  
  123.     def save_audio(self, fmt: str):
  124.         text = self._get_text()
  125.         if not text:
  126.             return
  127.         ext = ".wav" if fmt == "wav" else ".mp3"
  128.         filetypes = [(fmt.upper() + " audio", f"*{ext}")]
  129.         dst = filedialog.asksaveasfilename(initialdir=DESKTOP_DIR, defaultextension=ext, filetypes=filetypes)
  130.         if not dst:
  131.             return
  132.         self._progress_start("Synthesizing…")
  133.         if fmt == "wav":
  134.             threading.Thread(target=self._save_wav, args=(text, dst), daemon=True).start()
  135.         else:
  136.             threading.Thread(target=self._save_mp3, args=(text, dst), daemon=True).start()
  137.  
  138.     def on_close(self):
  139.         self.stop_read()
  140.         self.root.destroy()
  141.  
  142.     # ---------- Workflows ----------
  143.     def _read_flow(self, text: str):
  144.         try:
  145.             with tempfile.TemporaryDirectory() as tmpdir:
  146.                 mp3_tmp = os.path.join(tmpdir, "speech.mp3")
  147.                 wav_tmp = os.path.join(tmpdir, "speech.wav")
  148.                 asyncio.run(self._synth(text, mp3_tmp))
  149.                 self.root.after(0, lambda: self._progress_update("Encoding…"))
  150.                 AudioSegment.from_file(mp3_tmp).export(wav_tmp, format="wav")
  151.                 self.root.after(0, lambda: self._progress_update("Playing…"))
  152.                 self.proc = subprocess.Popen(["ffplay", "-nodisp", "-autoexit", "-loglevel", "quiet", wav_tmp])
  153.                 self.proc.wait()
  154.         finally:
  155.             self.root.after(0, self._reset_buttons)
  156.  
  157.     def _save_wav(self, text: str, dst: str):
  158.         try:
  159.             with tempfile.NamedTemporaryFile(delete=False, suffix=".mp3") as tmp:
  160.                 mp3_tmp = tmp.name
  161.             asyncio.run(self._synth(text, mp3_tmp))
  162.             self.root.after(0, lambda: self._progress_update("Encoding…"))
  163.             AudioSegment.from_file(mp3_tmp).export(dst, format="wav")
  164.             os.remove(mp3_tmp)
  165.             self.root.after(0, lambda: messagebox.showinfo("Saved", f"WAV saved to {dst}"))
  166.         except Exception as e:
  167.             self.root.after(0, lambda: messagebox.showerror("Error", str(e)))
  168.         finally:
  169.             self.root.after(0, self._progress_stop)
  170.  
  171.     def _save_mp3(self, text: str, dst: str):
  172.         try:
  173.             with tempfile.NamedTemporaryFile(delete=False, suffix=".mp3") as tmp:
  174.                 mp3_tmp = tmp.name
  175.             asyncio.run(self._synth(text, mp3_tmp))
  176.             self.root.after(0, lambda: self._progress_update("Encoding…"))
  177.             seg = AudioSegment.from_file(mp3_tmp)
  178.             seg.export(dst, **SMALL_MP3_PARAMS)
  179.             os.remove(mp3_tmp)
  180.             self.root.after(0, lambda: messagebox.showinfo("Saved", f"MP3 saved to {dst}"))
  181.         except Exception as e:
  182.             self.root.after(0, lambda: messagebox.showerror("Error", str(e)))
  183.         finally:
  184.             self.root.after(0, self._progress_stop)
  185.  
  186.     # ---------- TTS ----------
  187.     async def _synth(self, text: str, mp3_out: str):
  188.         try:
  189.             comm = edge_tts.Communicate(text=text, voice=self.voice_var.get(), rate=RATE_DEFAULT)
  190.             await comm.save(mp3_out)
  191.         except ClientConnectionError:
  192.             pass
  193.  
  194.     # ---------- Utils ----------
  195.     def _get_text(self) -> str:
  196.         txt = self.text_box.get("1.0", tk.END).strip()
  197.         if not txt:
  198.             messagebox.showwarning("No text", "Please enter text to speak.")
  199.         return txt
  200.  
  201.     def _reset_buttons(self):
  202.         self.read_btn.config(state=tk.NORMAL)
  203.         self.stop_btn.config(state=tk.DISABLED)
  204.         self._progress_stop()
  205.  
  206.  
  207. def main():
  208.     root = tk.Tk()
  209.     ReaderApp(root)
  210.     root.mainloop()
  211.  
  212.  
  213. if __name__ == "__main__":
  214.     main()
Tags: tts
Advertisement
Add Comment
Please, Sign In to add comment
Advertisement