| import json |
| import os |
|
|
| import gradio as gr |
| import numpy as np |
| import spaces |
| import torch |
| from qwen_tts import Qwen3TTSModel |
|
|
| |
| |
| MODEL_ID = "Qwen/Qwen3-TTS-12Hz-0.6B-Base" |
| MAX_CHARS = 1500 |
|
|
| VOICES_DIR = os.path.join(os.path.dirname(os.path.abspath(__file__)), "voices") |
| with open(os.path.join(VOICES_DIR, "transcripts.json"), encoding="utf-8") as f: |
| TRANSCRIPTS = json.load(f) |
| VOICES = sorted(TRANSCRIPTS.keys()) |
| LANGUAGES = ["English", "Chinese", "Japanese", "Korean", "German", |
| "French", "Russian", "Portuguese", "Spanish", "Italian"] |
|
|
| model = Qwen3TTSModel.from_pretrained( |
| MODEL_ID, |
| device_map="cuda", |
| dtype=torch.bfloat16, |
| ) |
|
|
| _prompt_cache = {} |
|
|
|
|
| def _get_voice_prompt(voice: str): |
| if voice not in _prompt_cache: |
| _prompt_cache[voice] = model.create_voice_clone_prompt( |
| ref_audio=os.path.join(VOICES_DIR, f"{voice}.wav"), |
| ref_text=TRANSCRIPTS[voice], |
| ) |
| return _prompt_cache[voice] |
|
|
|
|
| @spaces.GPU(duration=90) |
| def tts(text: str, voice: str, language: str): |
| text = (text or "").strip() |
| if not text: |
| raise gr.Error("Enter some text to speak.") |
| if len(text) > MAX_CHARS: |
| raise gr.Error(f"Text too long ({len(text)} chars, max {MAX_CHARS}).") |
| if voice not in TRANSCRIPTS: |
| raise gr.Error(f"Unknown voice '{voice}'. Available: {', '.join(VOICES)}") |
|
|
| wavs, sr = model.generate_voice_clone( |
| text=text, |
| language=language, |
| voice_clone_prompt=_get_voice_prompt(voice), |
| ) |
| audio = np.asarray(wavs[0], dtype=np.float32) |
| return sr, audio |
|
|
|
|
| demo = gr.Interface( |
| fn=tts, |
| inputs=[ |
| gr.Textbox(label="Text", lines=4, placeholder="What should the voice say?"), |
| gr.Dropdown(VOICES, value=VOICES[0], label="Voice"), |
| gr.Dropdown(LANGUAGES, value="English", label="Language"), |
| ], |
| outputs=gr.Audio(label="Generated speech"), |
| title="EsfandTTS — Qwen3-TTS voice clone", |
| description="Cloned voices via Qwen3-TTS 0.6B Base. Also callable as an API (see the 'Use via API' link below).", |
| flagging_mode="never", |
| ) |
|
|
| demo.launch() |
|
|