Advertisement
Not a member of Pastebin yet?
Sign Up,
it unlocks many cool features!
- # Simple GUI to let you paste text and have it read aloud, or save as file, w/ voice dropdown.
- # Requires Python ≥3.8 (tested on 3.12)
- # OS dependencies: libasound2t64 ffmpeg libsndfile1
- # pip install -U edge-tts pydub aiohttp
- """
- TTS Box — single‑file neural TTS (Edge‑TTS)
- ──────────────────────────────────────────
- • Voice dropdown (default **en‑GB‑RyanNeural**)
- • **Read** → synthesize → encode → play via ffplay
- • **Save WAV** / **Save MP3** (compact 32 kHz 48 kbps mono)
- • Progress bar + status text show stage (Synthesizing ▸ Encoding ▸ Playing/Saving)
- • Save dialogs default to Desktop if the folder exists, else Home
- """
- import tkinter as tk
- from tkinter import filedialog, messagebox, ttk
- from tkinter.scrolledtext import ScrolledText
- import threading
- import asyncio
- import tempfile
- import subprocess
- import os
- import signal
- import edge_tts # type: ignore
- from pydub import AudioSegment
- from aiohttp.client_exceptions import ClientConnectionError
- # ---------- Paths ----------
- HOME = os.path.expanduser("~")
- DESKTOP_DIR = os.path.join(HOME, "Desktop") if os.path.isdir(os.path.join(HOME, "Desktop")) else HOME
- # ---------- Voices ----------
- VOICE_OPTIONS = [
- "en-US-JennyNeural",
- "en-US-GuyNeural",
- "en-US-AriaNeural",
- "en-US-DavisNeural",
- "en-GB-SoniaNeural",
- "en-GB-RyanNeural",
- ]
- VOICE_DEFAULT = "en-GB-RyanNeural"
- RATE_DEFAULT = "+0%"
- # ---------- Encoding params ----------
- SMALL_MP3_PARAMS = {
- "format": "mp3",
- "bitrate": "48k",
- "parameters": ["-ar", "32000", "-ac", "1"],
- }
- class ReaderApp:
- def __init__(self, root: tk.Tk):
- self.root = root
- self.root.title("TTS Box")
- self.root.protocol("WM_DELETE_WINDOW", self.on_close)
- # Text input
- self.text_box = ScrolledText(root, wrap=tk.WORD, width=80, height=18)
- self.text_box.pack(fill=tk.BOTH, expand=True, padx=10, pady=10)
- # Controls
- ctrl = tk.Frame(root)
- ctrl.pack(pady=(0, 10))
- tk.Label(ctrl, text="Voice:").pack(side=tk.LEFT)
- self.voice_var = tk.StringVar(value=VOICE_DEFAULT)
- tk.OptionMenu(ctrl, self.voice_var, *VOICE_OPTIONS).pack(side=tk.LEFT, padx=5)
- self.read_btn = tk.Button(ctrl, text="Read", command=self.start_read)
- self.read_btn.pack(side=tk.LEFT, padx=5)
- self.stop_btn = tk.Button(ctrl, text="Stop", command=self.stop_read, state=tk.DISABLED)
- self.stop_btn.pack(side=tk.LEFT, padx=5)
- self.save_wav_btn = tk.Button(ctrl, text="Save WAV", command=lambda: self.save_audio("wav"))
- self.save_wav_btn.pack(side=tk.LEFT, padx=5)
- self.save_mp3_btn = tk.Button(ctrl, text="Save MP3", command=lambda: self.save_audio("mp3"))
- self.save_mp3_btn.pack(side=tk.LEFT, padx=5)
- # Status + progress
- self.status_var = tk.StringVar(value="Idle")
- ttk.Label(root, textvariable=self.status_var).pack(fill=tk.X, padx=10)
- self.progress = ttk.Progressbar(root, mode="indeterminate")
- self.progress.pack(fill=tk.X, padx=10, pady=(0, 10))
- # Internal
- self.proc: subprocess.Popen | None = None
- self.worker: threading.Thread | None = None
- # ---------- Progress helpers ----------
- def _progress_start(self, msg: str):
- self.status_var.set(msg)
- self.progress.start(10)
- def _progress_update(self, msg: str):
- self.status_var.set(msg)
- def _progress_stop(self):
- self.progress.stop()
- self.progress["value"] = 0
- self.status_var.set("Idle")
- # ---------- UI actions ----------
- def start_read(self):
- if self.worker and self.worker.is_alive():
- return
- text = self._get_text()
- if not text:
- return
- self.read_btn.config(state=tk.DISABLED)
- self.stop_btn.config(state=tk.NORMAL)
- self._progress_start("Synthesizing…")
- self.worker = threading.Thread(target=self._read_flow, args=(text,), daemon=True)
- self.worker.start()
- def stop_read(self):
- if self.proc and self.proc.poll() is None:
- try:
- self.proc.send_signal(signal.SIGINT)
- except Exception:
- self.proc.kill()
- self._reset_buttons()
- def save_audio(self, fmt: str):
- text = self._get_text()
- if not text:
- return
- ext = ".wav" if fmt == "wav" else ".mp3"
- filetypes = [(fmt.upper() + " audio", f"*{ext}")]
- dst = filedialog.asksaveasfilename(initialdir=DESKTOP_DIR, defaultextension=ext, filetypes=filetypes)
- if not dst:
- return
- self._progress_start("Synthesizing…")
- if fmt == "wav":
- threading.Thread(target=self._save_wav, args=(text, dst), daemon=True).start()
- else:
- threading.Thread(target=self._save_mp3, args=(text, dst), daemon=True).start()
- def on_close(self):
- self.stop_read()
- self.root.destroy()
- # ---------- Workflows ----------
- def _read_flow(self, text: str):
- try:
- with tempfile.TemporaryDirectory() as tmpdir:
- mp3_tmp = os.path.join(tmpdir, "speech.mp3")
- wav_tmp = os.path.join(tmpdir, "speech.wav")
- asyncio.run(self._synth(text, mp3_tmp))
- self.root.after(0, lambda: self._progress_update("Encoding…"))
- AudioSegment.from_file(mp3_tmp).export(wav_tmp, format="wav")
- self.root.after(0, lambda: self._progress_update("Playing…"))
- self.proc = subprocess.Popen(["ffplay", "-nodisp", "-autoexit", "-loglevel", "quiet", wav_tmp])
- self.proc.wait()
- finally:
- self.root.after(0, self._reset_buttons)
- def _save_wav(self, text: str, dst: str):
- try:
- with tempfile.NamedTemporaryFile(delete=False, suffix=".mp3") as tmp:
- mp3_tmp = tmp.name
- asyncio.run(self._synth(text, mp3_tmp))
- self.root.after(0, lambda: self._progress_update("Encoding…"))
- AudioSegment.from_file(mp3_tmp).export(dst, format="wav")
- os.remove(mp3_tmp)
- self.root.after(0, lambda: messagebox.showinfo("Saved", f"WAV saved to {dst}"))
- except Exception as e:
- self.root.after(0, lambda: messagebox.showerror("Error", str(e)))
- finally:
- self.root.after(0, self._progress_stop)
- def _save_mp3(self, text: str, dst: str):
- try:
- with tempfile.NamedTemporaryFile(delete=False, suffix=".mp3") as tmp:
- mp3_tmp = tmp.name
- asyncio.run(self._synth(text, mp3_tmp))
- self.root.after(0, lambda: self._progress_update("Encoding…"))
- seg = AudioSegment.from_file(mp3_tmp)
- seg.export(dst, **SMALL_MP3_PARAMS)
- os.remove(mp3_tmp)
- self.root.after(0, lambda: messagebox.showinfo("Saved", f"MP3 saved to {dst}"))
- except Exception as e:
- self.root.after(0, lambda: messagebox.showerror("Error", str(e)))
- finally:
- self.root.after(0, self._progress_stop)
- # ---------- TTS ----------
- async def _synth(self, text: str, mp3_out: str):
- try:
- comm = edge_tts.Communicate(text=text, voice=self.voice_var.get(), rate=RATE_DEFAULT)
- await comm.save(mp3_out)
- except ClientConnectionError:
- pass
- # ---------- Utils ----------
- def _get_text(self) -> str:
- txt = self.text_box.get("1.0", tk.END).strip()
- if not txt:
- messagebox.showwarning("No text", "Please enter text to speak.")
- return txt
- def _reset_buttons(self):
- self.read_btn.config(state=tk.NORMAL)
- self.stop_btn.config(state=tk.DISABLED)
- self._progress_stop()
- def main():
- root = tk.Tk()
- ReaderApp(root)
- root.mainloop()
- if __name__ == "__main__":
- main()
Advertisement
Add Comment
Please, Sign In to add comment
Advertisement