#!/usr/bin/env python3 """Sofelia TTS — Palestinian Arabic (speaker Eliaa). CPU Gradio Space.""" import re import gradio as gr import numpy as np import torch from huggingface_hub import hf_hub_download from kokoro import KModel from misaki import espeak from sofelia_frontend import text_to_phonemes REPO = "hamdallah/Sofelia-TTS-82M" SR = 24000 torch.set_num_threads(4) print("Downloading model files…") MODEL_PATH = hf_hub_download(REPO, "kokoro_sofelia_82M.pth") CONFIG_PATH = hf_hub_download(REPO, "config.json") VOICE_PATH = hf_hub_download(REPO, "voices/eliaa.pt") import kokoro as _kokoro_pkg print(f"[diag] kokoro from: {_kokoro_pkg.__file__}", flush=True) print("Loading model on CPU…") MODEL = KModel(repo_id="hexgrad/Kokoro-82M", config=CONFIG_PATH, model=MODEL_PATH).to("cpu").eval() VOICE = torch.load(VOICE_PATH, map_location="cpu", weights_only=True) G2P = espeak.EspeakG2P(language="ar") # ── startup self-test: log phonemes + audio stats so noise vs speech is visible try: import numpy as _np _t = "شو أخبارك؟ كلشي تمام إن شاء الله." _ps = text_to_phonemes(_t, G2P)[:510] print(f"[diag] phonemes: {_ps}", flush=True) with torch.no_grad(): _a = MODEL(_ps, VOICE[len(_ps) - 1], 1.0, return_output=False).cpu().numpy().squeeze() print( f"[diag] selftest rms={float(_np.sqrt((_a**2).mean())):.4f} " f"peak={float(_np.abs(_a).max()):.3f} len={len(_a)/24000:.1f}s " f"(clean ref: rms~0.104 peak~0.70 len~3.6s)", flush=True, ) except Exception as _e: print(f"[diag] selftest failed: {_e}", flush=True) EXAMPLES = [ "مرحبا، أنا إيلياء. كيف بقدر أساعدك اليوم؟", "بدي أروح ع السوق أشتري خضرة وفواكه للبيت.", "يا زلمة وين كنت مبارح؟ دورت عليك وما لقيتك.", "الصبح الساعة سبعة طلعت من البيت على الشغل.", ] def compress_silence(x, thresh=0.012, max_gap=0.22, keep=0.14): """Cap any silent run longer than max_gap down to `keep` seconds. Fixes the ~1s pauses at '.'/',' (model's own silence + chunk-stitch silence).""" sil = np.abs(x) < thresh keep_n, max_n = int(keep * SR), int(max_gap * SR) out, i, n = [], 0, len(x) while i < n: j = i if sil[i]: while j < n and sil[j]: j += 1 out.append(x[i : i + keep_n] if (j - i) > max_n else x[i:j]) else: while j < n and not sil[j]: j += 1 out.append(x[i:j]) i = j return np.concatenate(out) if out else x def synth(text, speed): text = (text or "").strip() if not text: return None chunks = [c.strip() for c in re.split(r"(?<=[.!؟?:])\s+", text) if c.strip()] or [text] pieces = [] for ch in chunks: ps = text_to_phonemes(ch, G2P)[:510] if not ps: continue with torch.no_grad(): audio = MODEL(ps, VOICE[len(ps) - 1], float(speed), return_output=False) pieces.append(audio.cpu().numpy().squeeze().astype(np.float32)) if not pieces: return None full = compress_silence(np.concatenate(pieces)) # return 16-bit PCM, not float — gradio's float-audio path distorts/clips pcm = (np.clip(full, -1.0, 1.0) * 32767.0).astype(np.int16) return (SR, pcm) with gr.Blocks(title="Sofelia TTS — Palestinian Arabic") as demo: gr.Markdown( "# 🗣️ Sofelia TTS 82M — Palestinian Arabic\n" "Natural Palestinian Arabic text-to-speech. Voice: **Eliaa**. Runs on CPU.\n\n" "Type Arabic text (dialect welcome) and press Generate." ) with gr.Row(): with gr.Column(): txt = gr.Textbox(label="النص العربي", rtl=True, lines=4, value=EXAMPLES[0]) speed = gr.Slider(0.7, 1.3, value=1.0, step=0.05, label="Speed / السرعة") btn = gr.Button("🔊 توليد الصوت", variant="primary") out = gr.Audio(label="الصوت", type="numpy") gr.Examples(EXAMPLES, inputs=txt) btn.click(synth, [txt, speed], out) if __name__ == "__main__": demo.launch()