import os import re import warnings import gradio as gr from gradio_client import Client import torch from transformers import AutoModelForCausalLM, AutoTokenizer # ======================== كتم التحذيرات ======================== warnings.filterwarnings("ignore") # ======================== الإعدادات ======================== DEVICE = "cpu" WHISPER_MODEL = "openai/whisper-base" MODEL_ID = "Qwen/Qwen2.5-0.5B-Instruct" RADAR_SPACE_URL = "Asem75/Aiocr_Radar" # 🔐 يجب ضبط نفس القيمة بالضبط في إعدادات مساحة الرادار (Settings → # Variables and secrets → INTERNAL_API_KEY) وفي إعدادات هذه المساحة أيضاً RADAR_API_KEY = os.environ.get("INTERNAL_API_KEY", "").strip() print("⏳ تحميل النموذج...") _llm_tokenizer = AutoTokenizer.from_pretrained(MODEL_ID) _llm_model = AutoModelForCausalLM.from_pretrained( MODEL_ID, torch_dtype=torch.float32, device_map="cpu", low_cpu_mem_usage=True ) print("✅ النموذج جاهز") # ======================== Whisper ======================== _whisper_pipe = None def load_whisper(): global _whisper_pipe if _whisper_pipe is not None: return _whisper_pipe from transformers import pipeline _whisper_pipe = pipeline("automatic-speech-recognition", model=WHISPER_MODEL, device=DEVICE, chunk_length_s=30) return _whisper_pipe # ======================== الرادار ======================== # ⚠️ إصلاح هام: نقطة API "/synth_arabic" على مساحة الرادار تستقبل # بالضبط معاملين فقط: (text, speaker_name) — وهي مصمّمة خصيصاً لهذا # الاستخدام الخارجي. الكود القديم هنا كان يستدعيها بـ7 معاملات تطابق # توقيع دالة الواجهة الداخلية القديمة (synth_arabic) وليس نقطة الـ API # الفعلية، فكان هذا الاستدعاء يفشل دائماً (يُمسَك بصمت داخل except). # # ⚠️ إصلاح ثانٍ: "ar-JO-IsmaeelNeural" و"ar-AE-AminaNeural" غير # موجودين أصلاً في قائمة أصوات مايكروسوفت الرسمية (الأردن: Sana/Taim # فقط، الإمارات: Fatima/Hamdan فقط؛ Amina هي صوت جزائري ar-DZ تحديداً) — # استخدامهما يسبب NoAudioReceived. استبدلتهما بأصوات حقيقية موثّقة. def call_radar_api(text, subject, detected_emotion): try: client = Client(RADAR_SPACE_URL) word_count = len(text.split()) if word_count <= 35: speaker_name = "ar-EG-SalmaNeural" else: voice_map = { "🧮 الرياضيات والعلوم": "ar-JO-TaimNeural", "🕌 اللغة العربية والتربية الإسلامية": "ar-SA-HamedNeural", "🔤 اللغة الإنجليزية": "en-US-JennyNeural", } speaker_name = voice_map.get(subject, "ar-JO-TaimNeural") audio_url = client.predict( text, speaker_name, RADAR_API_KEY, api_name="/synth_arabic" ) if audio_url and isinstance(audio_url, (list, tuple)): audio_url = audio_url[0] return str(audio_url).strip() if audio_url else None except Exception as e: print(f"⚠️ خطأ الرادار: {e}") return None # ======================== دالة المحادثة ======================== def teacher_chat(audio_mic, subject, chat_history_state): """ التاريخ يُحفظ في gr.State كقائمة قواميس نظيفة بالضبط: [{"role": "user"/"assistant", "content": "..."}, ...] """ if audio_mic is None: return chat_history_state, None, "🎤 الرجاء تسجيل السؤال أولاً" # 1. تحويل الصوت لنص try: whisper = load_whisper() result = whisper(audio_mic, generate_kwargs={"language": "arabic"}) user_text = result["text"].strip() except Exception as e: return chat_history_state, None, f"⚠️ خطأ: {e}" if not user_text: return chat_history_state, None, "❌ لم يتم التعرف على كلام" print(f"🗣️ الطالب: {user_text}") # 2. بناء messages system_prompt = ( "أنت الأستاذ عبود، معلم خبير. " "أجب بإيجاز (30 كلمة كحد أقصى). " "لا تعط الإجابة كاملة، قسمها لخطوات. " "اطرح سؤالاً في النهاية. " "أضف [حالة: حماس] أو [حالة: هدوء]." ) messages = [{"role": "system", "content": system_prompt}] for msg in chat_history_state: messages.append({"role": msg["role"], "content": msg["content"]}) messages.append({"role": "user", "content": user_text}) # 3. توليد الرد try: text_prompt = _llm_tokenizer.apply_chat_template(messages, tokenize=False, add_generation_prompt=True) model_inputs = _llm_tokenizer([text_prompt], return_tensors="pt").to(DEVICE) with torch.no_grad(): generated_ids = _llm_model.generate( **model_inputs, max_new_tokens=80, temperature=0.4, do_sample=True, pad_token_id=_llm_tokenizer.eos_token_id, use_cache=True ) generated_ids = [output_ids[len(input_ids):] for input_ids, output_ids in zip(model_inputs.input_ids, generated_ids)] full_response = _llm_tokenizer.batch_decode(generated_ids, skip_special_tokens=True)[0].strip() emotion_match = re.search(r'\[حالة:\s*(\w+)\]', full_response) detected_emotion = emotion_match.group(1) if emotion_match else "هدوء" cleaned_response = re.sub(r'\[حالة:\s*\w+\]', '', full_response).strip() if not cleaned_response.endswith(('.', '؟', '!')): cleaned_response += '.' print(f"🤖 الأستاذ: {cleaned_response}") except Exception as e: return chat_history_state, None, f"⚠️ خطأ النموذج: {e}" # 4. تحديث التاريخ في الـ state (قائمة قواميس نظيفة دائماً) chat_history_state = chat_history_state + [ {"role": "user", "content": user_text}, {"role": "assistant", "content": cleaned_response}, ] # 5. توليد الصوت audio_path = call_radar_api(cleaned_response, subject, detected_emotion) status = f"✅ ({detected_emotion})" if audio_path else "⚠️ فشل الصوت" return chat_history_state, audio_path, status # ======================== الواجهة ======================== with gr.Blocks(title="الأستاذ عبود", theme=gr.themes.Soft()) as demo: gr.Markdown("# 🎓 المنصة التعليمية - الأستاذ عبود") chat_history_state = gr.State([]) with gr.Row(): with gr.Column(scale=2): subject_dropdown = gr.Dropdown( choices=["🧮 الرياضيات والعلوم", "🕌 اللغة العربية والتربية الإسلامية", "🔤 اللغة الإنجليزية"], value="🧮 الرياضيات والعلوم", label="📚 المادة" ) # ⚠️ في Gradio 6.3.0+ صيغة messages هي الوحيدة الموجودة أصلاً، # وتمت إزالة معامل type نهائياً (لا حاجة له ولا يُقبل كوسيط) chatbot_display = gr.Chatbot(label="الحوار", height=400) mic_input = gr.Audio(label="🎤 سؤالك", type="filepath", sources=["microphone"]) send_btn = gr.Button("🚀 إرسال", variant="primary") with gr.Column(scale=1): audio_output = gr.Audio(label="🔊 الرد", type="filepath", autoplay=True) status_output = gr.Textbox(label="الحالة") # ⚠️ الإصلاح الجذري الفعلي: لا حاجة لأي تحويل لصيغة tuples القديمة # ([user_msg, asst_msg]) — هذا بالضبط كان يكسر Gradio 6. التاريخ # القادم من teacher_chat هو بالفعل بصيغة messages الصحيحة تماماً # (قائمة قواميس role/content)، فنمرره مباشرة لكل من الـ State # وعنصر العرض chatbot_display بدون أي تعديل. def process_and_display(audio_mic, subject, history_state): new_history, audio, status = teacher_chat(audio_mic, subject, history_state) return new_history, new_history, audio, status, None send_btn.click( fn=process_and_display, inputs=[mic_input, subject_dropdown, chat_history_state], outputs=[chat_history_state, chatbot_display, audio_output, status_output, mic_input] ) if __name__ == "__main__": demo.launch(server_name="0.0.0.0", server_port=7860)