Asem75's picture
Update app.py
86a5e03 verified
Raw
History Blame
8.99 kB
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)