EsfandTTS / app.py
trinitytf's picture
Upload 6 files
be09fe1 verified
Raw
History Blame Contribute Delete
2.23 kB
import json
import os
import gradio as gr
import numpy as np
import spaces
import torch
from qwen_tts import Qwen3TTSModel
# 0.6B matches the voices' origin; switch to Qwen/Qwen3-TTS-12Hz-1.7B-Base for
# higher quality at ~2.5x the GPU time per clip.
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()