# app.py — جسر: خادم موحّد على Hugging Face Space (ZeroGPU) # يجمع ثلاث خدمات في مكان واحد: # /tts → توليد كلام بصوت مستنسخ (Chatterbox Multilingual) # /translate → ترجمة نصية (API خارجي: MyMemory/Microsoft/Google) # /stt → تفريغ صوتي (Whisper) # # ⚠️ يعمل داخل نسخة (Duplicate) من الـ Space الرسمي: # ResembleAI/Chatterbox-Multilingual-TTS-V3 (كود Chatterbox في src/) import os import sys sys.path.insert(0, os.path.join(os.path.dirname(os.path.abspath(__file__)), "src")) import spaces # مطلوب لـ ZeroGPU import torch import torchaudio import gradio as gr # ============================================================ # اللغات المدعومة # ============================================================ LANGS = { "العربية (ar)": "ar", "Türkçe (tr)": "tr", "English (en)": "en", "हिन्दी (hi)": "hi", "Español (es)": "es", "Deutsch (de)": "de", "Français (fr)": "fr", } LANG_CODES = set(LANGS.values()) DEFAULT_VOICE = "default_voice.wav" # ============================================================ # 1) نموذج الكلام — Chatterbox (يُحمّل مرة واحدة) # ============================================================ _tts_model = None def get_tts_model(): global _tts_model if _tts_model is None: from chatterbox.mtl_tts import ChatterboxMultilingualTTS _tts_model = ChatterboxMultilingualTTS.from_pretrained(device="cuda") return _tts_model # (الترجمة الآن عبر API خارجي - انظر دالة translate) # ============================================================ # 3) نموذج التفريغ — Whisper # ============================================================ _asr = None def get_asr(): global _asr if _asr is None: import whisper # جرّب أسماء نموذج turbo بالترتيب (تختلف بين إصدارات whisper) # نبدأ بالأخف لسرعة التحميل وتجنّب انقطاع ZeroGPU، ثم الأثقل candidates = ["small", "medium", "large-v3-turbo", "turbo"] last_err = None for name in candidates: try: _asr = whisper.load_model(name, device="cuda") print(f"✅ Whisper loaded: {name}") break except Exception as e: last_err = e print(f"⚠️ تعذّر تحميل {name}: {e}") if _asr is None: raise RuntimeError(f"فشل تحميل أي نموذج Whisper: {last_err}") return _asr # ============================================================ # الدوال المعروضة كـ API # ============================================================ @spaces.GPU(duration=120) def tts(text: str, language: str, exaggeration: float = 0.5, cfg_weight: float = 0.5, voice_ref=None): """توليد كلام بصوت مستنسخ. يعيد مسار WAV.""" text = (text or "").strip() if not text: raise gr.Error("أدخل نصاً أولاً") if len(text) > 1000: raise gr.Error("الحد الأقصى 1000 حرف") lang = LANGS.get(language, language) if lang not in LANG_CODES: raise gr.Error(f"لغة غير مدعومة: {language}") ref = voice_ref if voice_ref else DEFAULT_VOICE if not os.path.exists(ref): raise gr.Error("ملف الصوت المرجعي default_voice.wav غير موجود — ارفعه للـ Space") model = get_tts_model() wav = model.generate( text, language_id=lang, audio_prompt_path=ref, exaggeration=float(exaggeration), cfg_weight=float(cfg_weight), ) out_path = "/tmp/speech.wav" torchaudio.save(out_path, wav, model.sr) return out_path def translate(text: str, source: str, target: str): """ترجمة نص عبر API خارجي (بلا GPU - سريع وموثوق). الافتراضي: MyMemory (مجاني، بلا مفتاح - للتجربة). للإنتاج: اضبط متغير البيئة على أحد هذين للجودة الأعلى: - MS_TRANSLATOR_KEY (Microsoft Translator - 2 مليون حرف/شهر مجاناً) - GOOGLE_TRANSLATE_KEY (Google Cloud Translation) """ import urllib.parse import urllib.request import json as _json text = (text or "").strip() if not text: return "" tgt = LANGS.get(target, target) src = LANGS.get(source, source) if tgt not in LANG_CODES: raise gr.Error(f"لغة هدف غير مدعومة: {target}") if src == tgt: return text # 1) Microsoft Translator (إن وُجد المفتاح) - الأفضل للإنتاج ms_key = os.environ.get("MS_TRANSLATOR_KEY", "") if ms_key: try: region = os.environ.get("MS_TRANSLATOR_REGION", "global") url = ("https://api.cognitive.microsofttranslator.com/translate" f"?api-version=3.0&from={src}&to={tgt}") body = _json.dumps([{"Text": text}]).encode("utf-8") req = urllib.request.Request(url, data=body, headers={ "Ocp-Apim-Subscription-Key": ms_key, "Ocp-Apim-Subscription-Region": region, "Content-Type": "application/json", }) with urllib.request.urlopen(req, timeout=15) as r: data = _json.loads(r.read()) return data[0]["translations"][0]["text"].strip() except Exception as e: print(f"MS Translator error: {e}", flush=True) # 2) Google Cloud Translation (إن وُجد المفتاح) g_key = os.environ.get("GOOGLE_TRANSLATE_KEY", "") if g_key: try: url = f"https://translation.googleapis.com/language/translate/v2?key={g_key}" body = urllib.parse.urlencode({ "q": text, "source": src, "target": tgt, "format": "text", }).encode("utf-8") with urllib.request.urlopen(url, data=body, timeout=15) as r: data = _json.loads(r.read()) return data["data"]["translations"][0]["translatedText"].strip() except Exception as e: print(f"Google Translate error: {e}", flush=True) # 3) MyMemory (مجاني بلا مفتاح - افتراضي للتجربة) try: q = urllib.parse.quote(text) url = (f"https://api.mymemory.translated.net/get" f"?q={q}&langpair={src}|{tgt}") with urllib.request.urlopen(url, timeout=15) as r: data = _json.loads(r.read()) result = data.get("responseData", {}).get("translatedText", "") if result: return result.strip() raise RuntimeError("لا توجد نتيجة من MyMemory") except Exception as e: import traceback traceback.print_exc() raise gr.Error(f"فشل الترجمة: {type(e).__name__}: {e}") @spaces.GPU(duration=120) def stt(audio_path: str, language: str = ""): """تفريغ صوتي عبر Whisper. يعيد النص.""" if not audio_path or not os.path.exists(audio_path): return "" lang = LANGS.get(language, language) if language else None try: asr = get_asr() kwargs = {} if lang and lang in LANG_CODES: kwargs["language"] = lang result = asr.transcribe(audio_path, **kwargs) return (result.get("text") or "").strip() except Exception as e: # أظهر الخطأ الحقيقي بدل رسالة عامة raise gr.Error(f"فشل التفريغ: {type(e).__name__}: {e}") # ============================================================ # واجهة Gradio + نقاط API # ============================================================ with gr.Blocks(title="جسر — خادم موحّد") as demo: gr.Markdown("# 🌉 جسر — ترجمة + استنساخ صوتي (7 لغات)") with gr.Tab("🔊 توليد كلام"): with gr.Row(): with gr.Column(): text_in = gr.Textbox(label="النص", lines=4, placeholder="اكتب النص هنا...") lang_in = gr.Dropdown(choices=list(LANGS.keys()), value="العربية (ar)", label="اللغة") with gr.Accordion("إعدادات متقدمة", open=False): exag = gr.Slider(0.25, 1.0, value=0.5, step=0.05, label="قوة التعبير") cfg = gr.Slider(0.2, 0.8, value=0.5, step=0.05, label="التطابق مع الصوت المرجعي") voice_in = gr.Audio(label="صوت مرجعي مخصص (اختياري)", type="filepath") btn = gr.Button("🔊 توليد", variant="primary") with gr.Column(): audio_out = gr.Audio(label="الناتج", type="filepath", autoplay=True) btn.click(tts, inputs=[text_in, lang_in, exag, cfg, voice_in], outputs=audio_out, api_name="tts") with gr.Tab("🌍 ترجمة"): with gr.Row(): with gr.Column(): mt_text = gr.Textbox(label="النص", lines=3) mt_src = gr.Dropdown(choices=list(LANGS.keys()), value="العربية (ar)", label="من") mt_tgt = gr.Dropdown(choices=list(LANGS.keys()), value="English (en)", label="إلى") mt_btn = gr.Button("ترجم", variant="primary") mt_out = gr.Textbox(label="الترجمة", lines=3) mt_btn.click(translate, inputs=[mt_text, mt_src, mt_tgt], outputs=mt_out, api_name="translate") with gr.Tab("🎤 تفريغ صوتي"): with gr.Row(): with gr.Column(): stt_audio = gr.Audio(label="ملف صوتي", type="filepath") stt_lang = gr.Dropdown(choices=[""] + list(LANGS.keys()), value="", label="اللغة (اختياري)") stt_btn = gr.Button("فرّغ", variant="primary") stt_out = gr.Textbox(label="النص", lines=3) stt_btn.click(stt, inputs=[stt_audio, stt_lang], outputs=stt_out, api_name="stt") demo.queue(max_size=20).launch(ssr_mode=False)