Spaces:
Sleeping
Sleeping
| 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) | |