Update app.py
Browse files
app.py
CHANGED
|
@@ -11,22 +11,60 @@ from typing import Optional
|
|
| 11 |
torch.set_num_threads(int(os.environ.get("TORCH_THREADS", "2")))
|
| 12 |
|
| 13 |
HERE = os.path.dirname(os.path.abspath(__file__))
|
|
|
|
|
|
|
| 14 |
SECRET = os.environ.get("TTS_SECRET_KEY", "") # khali he to koi key nahi lagegi
|
|
|
|
| 15 |
MODEL_REPO = "saryps-labs/pocket-tts-hindi"
|
| 16 |
LOCAL_DIR = "/home/user/hindi_model"
|
| 17 |
-
VOICE_FILE = os.path.join(HERE, "voice.wav") # apni (permission wali) awaaz ka 10-20 sec sample
|
| 18 |
FALLBACK_VOICE = "hf://kyutai/tts-voices/alba-mackenna/casual.wav"
|
| 19 |
MAX_CHARS = int(os.environ.get("MAX_CHARS", "6000"))
|
| 20 |
GAP_SECONDS = 0.22
|
| 21 |
|
| 22 |
model = None
|
| 23 |
-
voice_state = None
|
| 24 |
SR = 24000
|
| 25 |
state_msg = "loading"
|
| 26 |
last_error = ""
|
| 27 |
gen_lock = threading.Lock()
|
|
|
|
| 28 |
|
| 29 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 30 |
def get_config_path() -> str:
|
| 31 |
cfg = os.path.join(LOCAL_DIR, "config.yaml")
|
| 32 |
if not os.path.exists(cfg):
|
|
@@ -43,13 +81,13 @@ def try_load(TTSModel, arg):
|
|
| 43 |
|
| 44 |
|
| 45 |
def load_everything():
|
| 46 |
-
global model,
|
| 47 |
try:
|
| 48 |
from pocket_tts import TTSModel
|
| 49 |
cfg = get_config_path()
|
| 50 |
print("Config:", cfg)
|
| 51 |
m, err = None, None
|
| 52 |
-
for arg in (Path(cfg), cfg):
|
| 53 |
try:
|
| 54 |
m = try_load(TTSModel, arg)
|
| 55 |
break
|
|
@@ -58,11 +96,10 @@ def load_everything():
|
|
| 58 |
traceback.print_exc()
|
| 59 |
if m is None:
|
| 60 |
raise err
|
| 61 |
-
voice_path = VOICE_FILE if os.path.exists(VOICE_FILE) else FALLBACK_VOICE
|
| 62 |
-
print("Voice prompt:", voice_path)
|
| 63 |
-
voice_state = m.get_state_for_audio_prompt(voice_path)
|
| 64 |
-
SR = int(getattr(m, "sample_rate", 24000))
|
| 65 |
model = m
|
|
|
|
|
|
|
|
|
|
| 66 |
state_msg = "ready"
|
| 67 |
print("Pocket TTS Hindi ready, sample rate", SR)
|
| 68 |
except Exception as e:
|
|
@@ -85,7 +122,7 @@ app.add_middleware(
|
|
| 85 |
|
| 86 |
def authorized(request: Request) -> bool:
|
| 87 |
if not SECRET:
|
| 88 |
-
return True
|
| 89 |
cands = [
|
| 90 |
request.headers.get("x-secret-key", ""),
|
| 91 |
request.query_params.get("key", ""),
|
|
@@ -96,8 +133,8 @@ def authorized(request: Request) -> bool:
|
|
| 96 |
return any(hmac.compare_digest(c.encode(), SECRET.encode()) for c in cands if c)
|
| 97 |
|
| 98 |
|
|
|
|
| 99 |
def split_clean(text: str):
|
| 100 |
-
"""Is model ko punctuation nahi chahiye: vakyon me todo, sirf Devanagari + digits rakho."""
|
| 101 |
text = text.replace("\r", "\n")
|
| 102 |
parts = re.split(r"[\u0964\u0965\.\?\!\n;:]+", text)
|
| 103 |
out = []
|
|
@@ -116,12 +153,12 @@ def split_clean(text: str):
|
|
| 116 |
return out
|
| 117 |
|
| 118 |
|
| 119 |
-
def iter_audio(text: str):
|
| 120 |
if hasattr(model, "generate_audio_stream"):
|
| 121 |
-
for a in model.generate_audio_stream(
|
| 122 |
yield a
|
| 123 |
else:
|
| 124 |
-
yield model.generate_audio(
|
| 125 |
|
| 126 |
|
| 127 |
def to_pcm16(a) -> bytes:
|
|
@@ -130,12 +167,12 @@ def to_pcm16(a) -> bytes:
|
|
| 130 |
return (arr * 32767.0).astype("<i2").tobytes()
|
| 131 |
|
| 132 |
|
| 133 |
-
def pcm_stream(sentences):
|
| 134 |
silence = np.zeros(int(GAP_SECONDS * SR), dtype="<i2").tobytes()
|
| 135 |
with gen_lock: # ek samay me ek hi generation
|
| 136 |
for s in sentences:
|
| 137 |
try:
|
| 138 |
-
for a in iter_audio(s):
|
| 139 |
yield to_pcm16(a)
|
| 140 |
yield silence
|
| 141 |
except Exception:
|
|
@@ -143,9 +180,10 @@ def pcm_stream(sentences):
|
|
| 143 |
traceback.print_exc()
|
| 144 |
|
| 145 |
|
|
|
|
| 146 |
class TTSReq(BaseModel):
|
| 147 |
text: str
|
| 148 |
-
voice: Optional[str] = None
|
| 149 |
speed: Optional[float] = None
|
| 150 |
|
| 151 |
|
|
@@ -159,6 +197,15 @@ def health():
|
|
| 159 |
return {"state": state_msg, "error": last_error}
|
| 160 |
|
| 161 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 162 |
@app.post("/tts")
|
| 163 |
def tts(req: TTSReq, request: Request):
|
| 164 |
if not authorized(request):
|
|
@@ -172,8 +219,15 @@ def tts(req: TTSReq, request: Request):
|
|
| 172 |
sentences = split_clean(req.text)
|
| 173 |
if not sentences:
|
| 174 |
return JSONResponse({"error": "text me Hindi (देवनागरी) shabd nahi mile"}, status_code=400)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 175 |
return StreamingResponse(
|
| 176 |
-
pcm_stream(sentences),
|
| 177 |
media_type="application/octet-stream",
|
| 178 |
headers={"X-Sample-Rate": str(SR), "Cache-Control": "no-store"},
|
| 179 |
)
|
|
|
|
| 11 |
torch.set_num_threads(int(os.environ.get("TORCH_THREADS", "2")))
|
| 12 |
|
| 13 |
HERE = os.path.dirname(os.path.abspath(__file__))
|
| 14 |
+
VOICES_DIR = os.path.join(HERE, "voices")
|
| 15 |
+
AUDIO_EXTS = (".wav", ".mp3", ".flac", ".ogg")
|
| 16 |
SECRET = os.environ.get("TTS_SECRET_KEY", "") # khali he to koi key nahi lagegi
|
| 17 |
+
DEFAULT_VOICE = os.environ.get("DEFAULT_VOICE", "") # chahe to default awaaz ka naam yaha daalo
|
| 18 |
MODEL_REPO = "saryps-labs/pocket-tts-hindi"
|
| 19 |
LOCAL_DIR = "/home/user/hindi_model"
|
|
|
|
| 20 |
FALLBACK_VOICE = "hf://kyutai/tts-voices/alba-mackenna/casual.wav"
|
| 21 |
MAX_CHARS = int(os.environ.get("MAX_CHARS", "6000"))
|
| 22 |
GAP_SECONDS = 0.22
|
| 23 |
|
| 24 |
model = None
|
|
|
|
| 25 |
SR = 24000
|
| 26 |
state_msg = "loading"
|
| 27 |
last_error = ""
|
| 28 |
gen_lock = threading.Lock()
|
| 29 |
+
voice_cache = {}
|
| 30 |
|
| 31 |
|
| 32 |
+
# ---------------- voices ----------------
|
| 33 |
+
def discover_voices() -> dict:
|
| 34 |
+
"""voices/ folder aur root me padi saari audio files -> {naam: path}"""
|
| 35 |
+
found = {}
|
| 36 |
+
for folder in (VOICES_DIR, HERE):
|
| 37 |
+
if not os.path.isdir(folder):
|
| 38 |
+
continue
|
| 39 |
+
for fn in sorted(os.listdir(folder)):
|
| 40 |
+
if fn.lower().endswith(AUDIO_EXTS):
|
| 41 |
+
base = os.path.splitext(fn)[0]
|
| 42 |
+
name, i = base, 2
|
| 43 |
+
while name in found:
|
| 44 |
+
name = f"{base}_{i}"
|
| 45 |
+
i += 1
|
| 46 |
+
found[name] = os.path.join(folder, fn)
|
| 47 |
+
return found
|
| 48 |
+
|
| 49 |
+
|
| 50 |
+
def resolve_voice(name: Optional[str]):
|
| 51 |
+
voices = discover_voices()
|
| 52 |
+
if not voices:
|
| 53 |
+
return "default", FALLBACK_VOICE
|
| 54 |
+
if not name or name not in voices:
|
| 55 |
+
name = DEFAULT_VOICE if DEFAULT_VOICE in voices else next(iter(voices))
|
| 56 |
+
return name, voices[name]
|
| 57 |
+
|
| 58 |
+
|
| 59 |
+
def get_voice_state(path: str):
|
| 60 |
+
key = path if path.startswith("hf://") else f"{path}:{os.path.getmtime(path)}"
|
| 61 |
+
if key not in voice_cache:
|
| 62 |
+
print("Voice process ho rahi he:", path)
|
| 63 |
+
voice_cache[key] = model.get_state_for_audio_prompt(path)
|
| 64 |
+
return voice_cache[key]
|
| 65 |
+
|
| 66 |
+
|
| 67 |
+
# ---------------- model ----------------
|
| 68 |
def get_config_path() -> str:
|
| 69 |
cfg = os.path.join(LOCAL_DIR, "config.yaml")
|
| 70 |
if not os.path.exists(cfg):
|
|
|
|
| 81 |
|
| 82 |
|
| 83 |
def load_everything():
|
| 84 |
+
global model, SR, state_msg, last_error
|
| 85 |
try:
|
| 86 |
from pocket_tts import TTSModel
|
| 87 |
cfg = get_config_path()
|
| 88 |
print("Config:", cfg)
|
| 89 |
m, err = None, None
|
| 90 |
+
for arg in (Path(cfg), cfg):
|
| 91 |
try:
|
| 92 |
m = try_load(TTSModel, arg)
|
| 93 |
break
|
|
|
|
| 96 |
traceback.print_exc()
|
| 97 |
if m is None:
|
| 98 |
raise err
|
|
|
|
|
|
|
|
|
|
|
|
|
| 99 |
model = m
|
| 100 |
+
SR = int(getattr(m, "sample_rate", 24000))
|
| 101 |
+
_, path = resolve_voice(DEFAULT_VOICE)
|
| 102 |
+
get_voice_state(path) # default awaaz pehle se taiyar
|
| 103 |
state_msg = "ready"
|
| 104 |
print("Pocket TTS Hindi ready, sample rate", SR)
|
| 105 |
except Exception as e:
|
|
|
|
| 122 |
|
| 123 |
def authorized(request: Request) -> bool:
|
| 124 |
if not SECRET:
|
| 125 |
+
return True
|
| 126 |
cands = [
|
| 127 |
request.headers.get("x-secret-key", ""),
|
| 128 |
request.query_params.get("key", ""),
|
|
|
|
| 133 |
return any(hmac.compare_digest(c.encode(), SECRET.encode()) for c in cands if c)
|
| 134 |
|
| 135 |
|
| 136 |
+
# ---------------- text / audio ----------------
|
| 137 |
def split_clean(text: str):
|
|
|
|
| 138 |
text = text.replace("\r", "\n")
|
| 139 |
parts = re.split(r"[\u0964\u0965\.\?\!\n;:]+", text)
|
| 140 |
out = []
|
|
|
|
| 153 |
return out
|
| 154 |
|
| 155 |
|
| 156 |
+
def iter_audio(text: str, vstate):
|
| 157 |
if hasattr(model, "generate_audio_stream"):
|
| 158 |
+
for a in model.generate_audio_stream(vstate, text):
|
| 159 |
yield a
|
| 160 |
else:
|
| 161 |
+
yield model.generate_audio(vstate, text)
|
| 162 |
|
| 163 |
|
| 164 |
def to_pcm16(a) -> bytes:
|
|
|
|
| 167 |
return (arr * 32767.0).astype("<i2").tobytes()
|
| 168 |
|
| 169 |
|
| 170 |
+
def pcm_stream(sentences, vstate):
|
| 171 |
silence = np.zeros(int(GAP_SECONDS * SR), dtype="<i2").tobytes()
|
| 172 |
with gen_lock: # ek samay me ek hi generation
|
| 173 |
for s in sentences:
|
| 174 |
try:
|
| 175 |
+
for a in iter_audio(s, vstate):
|
| 176 |
yield to_pcm16(a)
|
| 177 |
yield silence
|
| 178 |
except Exception:
|
|
|
|
| 180 |
traceback.print_exc()
|
| 181 |
|
| 182 |
|
| 183 |
+
# ---------------- API ----------------
|
| 184 |
class TTSReq(BaseModel):
|
| 185 |
text: str
|
| 186 |
+
voice: Optional[str] = None
|
| 187 |
speed: Optional[float] = None
|
| 188 |
|
| 189 |
|
|
|
|
| 197 |
return {"state": state_msg, "error": last_error}
|
| 198 |
|
| 199 |
|
| 200 |
+
@app.get("/voices")
|
| 201 |
+
def voices(request: Request):
|
| 202 |
+
if not authorized(request):
|
| 203 |
+
return JSONResponse({"error": "unauthorized"}, status_code=401)
|
| 204 |
+
names = list(discover_voices().keys()) or ["default"]
|
| 205 |
+
default, _ = resolve_voice(DEFAULT_VOICE)
|
| 206 |
+
return {"voices": names, "default": default}
|
| 207 |
+
|
| 208 |
+
|
| 209 |
@app.post("/tts")
|
| 210 |
def tts(req: TTSReq, request: Request):
|
| 211 |
if not authorized(request):
|
|
|
|
| 219 |
sentences = split_clean(req.text)
|
| 220 |
if not sentences:
|
| 221 |
return JSONResponse({"error": "text me Hindi (देवनागरी) shabd nahi mile"}, status_code=400)
|
| 222 |
+
name, path = resolve_voice(req.voice)
|
| 223 |
+
try:
|
| 224 |
+
with gen_lock: # nayi awaaz pehli baar process hogi (kuch second lagenge)
|
| 225 |
+
vstate = get_voice_state(path)
|
| 226 |
+
except Exception as e:
|
| 227 |
+
traceback.print_exc()
|
| 228 |
+
return JSONResponse({"error": f"awaaz '{name}' load nahi hui: {e}"}, status_code=500)
|
| 229 |
return StreamingResponse(
|
| 230 |
+
pcm_stream(sentences, vstate),
|
| 231 |
media_type="application/octet-stream",
|
| 232 |
headers={"X-Sample-Rate": str(SR), "Cache-Control": "no-store"},
|
| 233 |
)
|