Skip to content

Repository files navigation

████████╗ ██████╗   █████╗  ███╗   ██╗ ███████╗  ██████╗ ██████╗  ██╗ ██████╗  ███████╗
╚══██╔══╝ ██╔══██╗ ██╔══██╗ ████╗  ██║ ██╔════╝ ██╔════╝ ██╔══██╗ ██║ ██╔══██╗ ██╔════╝
   ██║    ██████╔╝ ███████║ ██╔██╗ ██║ ███████╗ ██║      ██████╔╝ ██║ ██████╔╝ █████╗
   ██║    ██╔══██╗ ██╔══██║ ██║╚██╗██║ ╚════██║ ██║      ██╔══██╗ ██║ ██╔══██╗ ██╔══╝
   ██║    ██║  ██╗ ██║  ██║ ██║ ╚████║ ███████║ ╚██████╗ ██║  ██╗ ██║ ██████╔╝ ███████╗
   ╚═╝    ╚═╝  ╚═╝ ╚═╝  ╚═╝ ╚═╝  ╚═══╝ ╚══════╝  ╚═════╝ ╚═╝  ╚═╝ ╚═╝ ╚═════╝  ╚══════╝
  Local TTS — text file to MP3, two engines, one command (or a GUI)

Python Platform Build License Status


What It Is

transcribe.py reads a text file, sends it to a locally running TTS inference server, and saves the result as an MP3. Optionally plays it back immediately.

transcribe-ui.py is a tkinter GUI that wraps both engines — start/stop the server, type or load text, pick a voice, generate, play, and save, all from one window.

Two engines are supported — both run on port 8029, so only one can be active at a time:

Engine Model Port Strength
qwen (UI default) Qwen3-TTS-12Hz-1.7B-CustomVoice 8029 Multiple voices, natural language style
fish (CLI default) Fish Audio S2 Pro F16 GGUF (s2.cpp) 8029 High quality, tone/emotion tags

Both servers run fully offline. No API keys, no cloud calls.


Features

  • GUI (transcribe-ui.py) — start/stop server, load text, pick voice, generate, play, save
  • CLI (transcribe.py) — single command: text file in, MP3 out, elapsed time printed
  • Fish Audio tone tags — inline [pause], [whisper], [excited], etc.
  • --tone flag to set a global tone for the whole file
  • Qwen3-TTS voice selection (Aiden, Ryan, Vivian, and more)
  • --instruct for natural language style control on Qwen
  • Auto-plays the result after saving
  • CUDA-accelerated on NVIDIA GPUs (RTX 3000+, tested on RTX 5080)

Getting Started

Prerequisites

  • NVIDIA GPU with CUDA 12.6+ drivers
  • Windows 10/11
  • Python 3.14 (system Python)
  • ffmpeg in PATH (e.g. winget install Gyan.FFmpeg)

Fish Audio S2 Pro (one-time setup)

# Run as Administrator
.\setup_fish2tts.ps1

Installs CMake/Git/FFmpeg, clones + builds s2.cpp, then downloads s2-pro-f16.gguf (~9.9 GB) from HuggingFace automatically.

Qwen3-TTS (one-time setup)

# Run as Administrator (requires setup_fish2tts.ps1 to have run first — needs FFmpeg)
.\setup_qwen3tts.ps1

Creates a Python 3.14 venv (qwen3tts_venv/), installs qwen-tts + PyTorch CUDA, downloads the model (~4.5 GB).

Install transcribe.py dependencies

pip install -r requirements.txt

Usage

GUI (recommended)

python transcribe-ui.py
  1. Select Qwen3-TTS or Fish Audio engine
  2. Click ▶ Start Server — a server window opens; wait for the port indicator to turn UP
  3. Type or load a .txt file
  4. Pick a voice / tone and click ▶ Generate
  5. Click ▶ Play to listen, or 💾 Save As... to export

CLI

Start the server first (keep it running in its own terminal):

.\start_fish2tts.ps1     # Fish Audio
.\start_qwen3tts.ps1     # Qwen3-TTS

Then run:

# Fish Audio — CLI default
python transcribe.py input/speech.txt
python transcribe.py input/speech.txt --tone "professional broadcast tone"
python transcribe.py input/speech.txt --output output/my_clip.mp3

# Qwen3-TTS
python transcribe.py input/speech.txt --engine qwen
python transcribe.py input/speech.txt --engine qwen --voice Ryan
python transcribe.py input/speech.txt --engine qwen --instruct "speak slowly and warmly"

Fish Audio Tone Tags

Embed inline in speech.txt for per-phrase control:

[professional broadcast tone] Welcome to tonight's broadcast.
[pause] And now, your host. [excited] Let's get started!

Pass --tone TAG to apply one tag to the entire text.

Note: The quantized s2-pro-f16.gguf only reliably recognises [Male] / [Female] prefixes. Full tone tags (e.g. [excited]) are silently treated as male by this model size.

Supported tags: [pause] [emphasis] [laughing] [inhale] [chuckle] [tsk] [singing] [excited] [laughing tone] [interrupting] [chuckling] [excited tone] [volume up] [echo] [angry] [low volume] [sigh] [low voice] [whisper] [screaming] [shouting] [loud] [surprised] [short pause] [exhale] [delight] [panting] [audience laughter] [with strong accent] [volume down] [clearing throat] [sad] [moaning] [shocked] [whisper in small voice] [professional broadcast tone] [pitch up]

Qwen3-TTS Voices

Aiden Dylan Eric Ono Anna Ryan Serena Sohee Uncle Fu Vivian

Default voice: Aiden. Use --voice NAME on the CLI or the Voice dropdown in the UI.

For style control use --instruct:

--instruct "speak slowly and warmly"
--instruct "excited, enthusiastic delivery"
--instruct "calm, professional broadcast tone"

Architecture

transcribe-ui.py          tkinter GUI (Qwen3 default engine)
├── Engine radio + Start/Stop server (PowerShell scripts)
├── Port 8029 health poll (daemon thread, 2s interval)
├── Voice/Instruct panel (switches per engine)
├── Text area + Load File
└── Generate → Play / Save As

transcribe.py             CLI (Fish default engine)
├── build_parser()        argparse — all flags
├── normalize_tone()      strips/re-wraps [brackets] for Fish tone tags
├── _find_ffmpeg()        locates ffmpeg in PATH or known install locations
├── wav_to_mp3()          shells out to ffmpeg for WAV → MP3
├── run_fish()            POST /generate  multipart form → WAV bytes → MP3
├── run_qwen()            POST /v1/audio/speech  JSON → MP3 bytes directly
└── main()                resolves server URL from .env, dispatches, saves, plays

qwen3tts_server.py        FastAPI wrapper around qwen-tts (Python 3.14 venv)
                          Endpoint: POST /v1/audio/speech (OpenAI TTS compatible)
                          Rope-deltas state reset between requests (fixes 2nd-gen 500)
                          ffmpeg WAV→MP3 runs off the event loop via asyncio.to_thread

s2.cpp server             External binary (s2.cpp/build/Release/s2.exe)
                          Endpoint: POST /generate (multipart form data)
                          Started via start_fish2tts.ps1

API Contracts

Fish Audio — port 8029

POST /generate
  Body: multipart/form-data
    text   = "<text with optional [tone tags]>"
    params = '{"max_new_tokens":1024,"temperature":0.58,"top_p":0.88,"top_k":40,"codec_follow_backend":true}'
  Response: WAV audio bytes

Qwen3-TTS — port 8029

POST /v1/audio/speech
  Body: application/json
    { "input": "...", "voice": "aiden", "response_format": "mp3", "instruct": "..." }
  Response: MP3 audio bytes
GET /v1/health → {"status":"ok","model":"...","gpu":"..."}

Both engines share port 8029. Only one server can run at a time.


Environment Variables (.env)

Variable Default Purpose
FISH_SPEECH_SERVER http://localhost:8029 s2.cpp server URL
FISH_SPEECH_API_KEY (empty) Bearer token if s2.cpp started with --api-key
QWEN_TTS_SERVER http://localhost:8029 Qwen3-TTS server URL
QWEN_TTS_API_KEY (empty) Bearer token (unused currently)
FISH_TONES [Male],[Female] Tone options shown in the UI
QWEN_VOICES Aiden,Dylan,Eric,... Voice options shown in the UI

Directory Layout

Transcribe/
├── transcribe.py           # TTS CLI (both engines)
├── transcribe-ui.py        # tkinter GUI (both engines)
├── qwen3tts_server.py      # Qwen3-TTS FastAPI server
├── setup_fish2tts.ps1      # One-time Fish Audio setup (run as admin)
├── setup_qwen3tts.ps1      # One-time Qwen3-TTS setup (run as admin)
├── start_fish2tts.ps1      # Fish Audio server launcher (port 8029)
├── start_qwen3tts.ps1      # Qwen3-TTS server launcher (port 8029)
├── requirements.txt        # System Python deps (transcribe.py + transcribe-ui.py)
├── .env                    # Server URLs, API keys, UI voice options
├── .gitignore
├── input/speech.txt        # Input text
├── output/                 # Generated MP3s (git-ignored)
├── models/
│   ├── s2-pro-f16.gguf     # Fish Audio GGUF (~9.9 GB) — git-ignored
│   ├── tokenizer.json
│   └── qwen3tts/           # Qwen3-TTS model weights (~4.5 GB) — git-ignored
├── qwen3tts_venv/          # Python 3.14 venv for Qwen3-TTS — git-ignored
└── s2.cpp/                 # Fish Audio C++ engine (cloned by setup) — git-ignored
    └── build/Release/s2.exe

Notes

  • Python versions: Both transcribe.py and qwen3tts_server.py run on Python 3.14. numba 0.65+ and librosa 0.11+ both support 3.14; Python 3.12 is no longer needed.
  • Default engines: The GUI (transcribe-ui.py) defaults to Qwen3-TTS. The CLI (transcribe.py) defaults to Fish Audio.
  • Port sharing: Both engines use port 8029. Only one server can run at a time — stop the current one before starting the other.
  • Codec GPU: --codec-follow-backend is set by default. This forces the audio codec onto the same CUDA device as the transformer. Without it, codec runs on CPU and synthesis stalls.
  • Fish GGUF model: Hosted at tommyho510/s2-pro-gguf on HuggingFace. Downloaded automatically by setup_fish2tts.ps1. Licensed CC BY-NC-SA 4.0 — based on Fish Audio S2 Pro.
  • Fish GGUF gender-only: The quantized s2-pro-f16.gguf only reliably recognises [Male] / [Female] tone prefixes. Full tone tags are silently treated as male.
  • Model size: The Fish GGUF contains both transformer and codec weights in one file — no separate codec download needed.
  • 503 responses: s2.cpp handles one request at a time. If the server returns 503, a synthesis is in progress — wait and retry.
  • Qwen second-generation fix: The server resets rope_deltas state before each request to prevent a shape-mismatch error that caused HTTP 500 on the second call.
  • playsound file locking: The UI writes each generation to a unique timestamped file (output/ui_<ms>.mp3) to avoid Windows MCI handle conflicts when playsound holds the previous file open.

Support

If you find this useful, consider buying me a coffee!

Donate via PayPal

About

Local TTS — text file to MP3, Fish and Qwen3 engines, one command GUI

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages