Spaces:
Sleeping
Sleeping
File size: 7,870 Bytes
1894d18 e049a51 aecf044 e049a51 1894d18 e049a51 6cae1ea f109235 1894d18 6cae1ea f109235 1894d18 f109235 1894d18 6cae1ea f109235 e049a51 f109235 6cae1ea e049a51 6cae1ea f109235 6cae1ea f109235 e049a51 f109235 e049a51 f109235 e049a51 f109235 e049a51 f109235 e049a51 6cae1ea 1894d18 6cae1ea e049a51 f109235 6cae1ea 1894d18 f109235 e049a51 f109235 6cae1ea e049a51 6cae1ea f109235 6cae1ea f109235 6cae1ea e049a51 6cae1ea f109235 6cae1ea f109235 6cae1ea e049a51 f109235 6cae1ea f109235 6cae1ea f109235 6cae1ea | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 | import os
# Must be set before torch's OpenMP/MKL thread pools initialize on first use.
# This is the most reliable way to make sure BLAS/OpenMP actually uses all
# cores instead of a conservative default.
def _detect_cpu_threads() -> int:
# Respect a value the platform/container already set -- on cgroup-limited
# containers (like HF Spaces) this is often the *correct* real quota,
# whereas os.cpu_count() reports the host machine's full core count and
# will cause thread oversubscription if trusted blindly.
for var in ("OMP_NUM_THREADS", "MKL_NUM_THREADS"):
val = os.environ.get(var)
if val and val.isdigit() and int(val) > 0:
return int(val)
# sched_getaffinity reflects CPU-affinity/cgroup restrictions more
# accurately than os.cpu_count() on Linux; fall back to cpu_count if
# unavailable (e.g. non-Linux).
try:
return len(os.sched_getaffinity(0))
except AttributeError:
return os.cpu_count() or 4
_CPU_THREADS = str(_detect_cpu_threads())
os.environ.setdefault("OMP_NUM_THREADS", _CPU_THREADS)
os.environ.setdefault("MKL_NUM_THREADS", _CPU_THREADS)
import tempfile
import time
import traceback
from typing import Optional
import soundfile as sf
import torch
from fastapi import FastAPI, HTTPException, Query
from fastapi.responses import FileResponse
from starlette.background import BackgroundTask
from qwen_tts import Qwen3TTSModel
app = FastAPI()
MODEL = None
SUPPORTED_SPEAKERS = []
SUPPORTED_LANGUAGES = []
MODEL_NAME = "Qwen/Qwen3-TTS-12Hz-1.7B-CustomVoice"
DEFAULT_SPEAKER = "Ryan" # English male voice; pick any from SUPPORTED_SPEAKERS
DEFAULT_LANGUAGE = "English" # or "Auto" to let the model detect it
# bf16 has no native hardware acceleration on most x86 CPUs (it gets
# emulated via upcasting), which is often slower than plain fp32. fp32 is
# the reliable baseline for generic CPU inference.
CPU_DTYPE = torch.float32
@app.on_event("startup")
def startup_event():
global MODEL, SUPPORTED_SPEAKERS, SUPPORTED_LANGUAGES
try:
print("=" * 80)
print("Starting application...")
print(f"PyTorch version: {torch.__version__}")
cuda_available = torch.cuda.is_available()
print(f"CUDA available: {cuda_available}")
if not cuda_available:
n_threads = int(_CPU_THREADS)
# Intra-op parallelism (parallelizes ops like matmul/conv internally).
torch.set_num_threads(n_threads)
# Inter-op parallelism (runs independent ops concurrently). Can
# only be set once, before any parallel work starts, so this
# must stay early and wrapped defensively.
try:
torch.set_num_interop_threads(max(1, n_threads // 2))
except RuntimeError as e:
print(f"Could not set interop threads (already initialized): {e}")
# Denormal floats are handled by a slow FP path on most CPUs;
# flushing them to zero avoids random latency spikes during
# generation. Negligible effect on audio quality.
torch.set_flush_denormal(True)
print(f"No GPU detected. CPU threads: {n_threads} "
f"(OMP_NUM_THREADS={os.environ.get('OMP_NUM_THREADS')})")
print(f"Loading Qwen3-TTS model: {MODEL_NAME}")
load_start = time.perf_counter()
device_map = "cuda:0" if cuda_available else "cpu"
dtype = torch.bfloat16 if cuda_available else CPU_DTYPE
load_kwargs = dict(device_map=device_map, dtype=dtype)
if cuda_available:
try:
MODEL = Qwen3TTSModel.from_pretrained(
MODEL_NAME,
attn_implementation="flash_attention_2",
**load_kwargs,
)
except Exception as flash_err:
print(f"flash_attention_2 unavailable ({flash_err}); "
f"falling back to default attention implementation.")
MODEL = Qwen3TTSModel.from_pretrained(MODEL_NAME, **load_kwargs)
else:
MODEL = Qwen3TTSModel.from_pretrained(MODEL_NAME, **load_kwargs)
SUPPORTED_SPEAKERS = MODEL.get_supported_speakers()
SUPPORTED_LANGUAGES = MODEL.get_supported_languages()
load_elapsed = time.perf_counter() - load_start
print(f"Supported speakers: {SUPPORTED_SPEAKERS}")
print(f"Supported languages: {SUPPORTED_LANGUAGES}")
print(f"Model loaded successfully in {load_elapsed:.1f}s.")
print("=" * 80)
except Exception as exc:
print(traceback.format_exc())
MODEL = None
print(f"Startup failed: {exc}")
@app.get("/")
def health():
return {
"status": "running",
"model_loaded": MODEL is not None,
"cpu_threads": _CPU_THREADS,
"supported_speakers": SUPPORTED_SPEAKERS,
"supported_languages": SUPPORTED_LANGUAGES,
}
@app.get("/tts")
def generate(
text: str,
speaker: str = Query(DEFAULT_SPEAKER, description="Voice to use, e.g. Ryan, Vivian, Aiden"),
language: str = Query(DEFAULT_LANGUAGE, description="Target language, or 'Auto' to detect"),
instruct: Optional[str] = Query(
None, description="Optional natural-language style instruction, e.g. 'Speak happily'"
),
max_new_tokens: Optional[int] = Query(
None,
description="Optional cap on generated audio tokens. Lower = faster on CPU, "
"but can truncate longer sentences. Omit to use the model default.",
),
):
output_file = None
try:
print("=" * 80)
print(f"Incoming text: {text}")
print(f"speaker={speaker} language={language} instruct={instruct!r} "
f"max_new_tokens={max_new_tokens}")
if MODEL is None:
raise HTTPException(status_code=500, detail="Model was not loaded.")
if SUPPORTED_SPEAKERS and speaker not in SUPPORTED_SPEAKERS:
raise HTTPException(
status_code=400,
detail=f"Unsupported speaker '{speaker}'. Choose from: {SUPPORTED_SPEAKERS}",
)
generate_kwargs = {}
if max_new_tokens is not None:
generate_kwargs["max_new_tokens"] = max_new_tokens
gen_start = time.perf_counter()
# inference_mode disables autograd bookkeeping entirely (faster and
# lower memory than no_grad) -- pure inference, no training ever
# happens on this path.
with torch.inference_mode():
wavs, sample_rate = MODEL.generate_custom_voice(
text=text,
language=language,
speaker=speaker,
instruct=instruct or "",
**generate_kwargs,
)
gen_elapsed = time.perf_counter() - gen_start
print(f"Generation took {gen_elapsed:.2f}s")
output_file = tempfile.NamedTemporaryFile(suffix=".wav", delete=False).name
sf.write(output_file, wavs[0], sample_rate)
size = os.path.getsize(output_file)
print(f"Output file: {output_file}")
print(f"File size: {size} bytes")
if size == 0:
raise RuntimeError("Generated WAV file is empty.")
print("=" * 80)
return FileResponse(
output_file,
media_type="audio/wav",
filename="speech.wav",
headers={"X-Generation-Seconds": f"{gen_elapsed:.2f}"},
# Clean up the temp file once the response has been sent.
background=BackgroundTask(lambda: os.remove(output_file) if os.path.exists(output_file) else None),
)
except HTTPException:
raise
except Exception:
print(traceback.format_exc())
if output_file and os.path.exists(output_file):
os.remove(output_file)
raise |