Asem75 commited on
Commit
86a5e03
·
verified ·
1 Parent(s): b9eb636

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +55 -217
app.py CHANGED
@@ -1,11 +1,8 @@
1
  import os
2
  import re
3
- import json
4
- import time
5
  import warnings
6
  import gradio as gr
7
  from gradio_client import Client
8
- from huggingface_hub import HfApi, hf_hub_download
9
  import torch
10
  from transformers import AutoModelForCausalLM, AutoTokenizer
11
 
@@ -14,31 +11,17 @@ warnings.filterwarnings("ignore")
14
 
15
  # ======================== الإعدادات ========================
16
  DEVICE = "cpu"
17
- WHISPER_MODEL = "openai/whisper-base" # تأكدت من السجلات أن Whisper ليس السبب، فلم أغيّره
18
- MODEL_ID = "Qwen/Qwen2.5-3B-Instruct"
19
  RADAR_SPACE_URL = "Asem75/Aiocr_Radar"
20
- VAULT_REPO_ID = "Asem75/aiocr_asistant"
21
-
22
- # ⚠️ سقف طول محتوى الدرس المُحقَن في الـ System Prompt. النصوص الطويلة جداً
23
- # كانت تطغى على السؤال الحي للطالب وتُشتّت النموذج عن الإجابة المباشرة.
24
- MAX_LESSON_CHARS = 1200
25
-
26
  RADAR_API_KEY = os.environ.get("INTERNAL_API_KEY", "").strip()
27
- HF_TOKEN = os.environ.get("HF_TOKEN", "").strip()
28
-
29
- print(f"✅ الخزنة: {VAULT_REPO_ID} | الرادار: {RADAR_SPACE_URL}")
30
- if not HF_TOKEN:
31
- print("⚠️ تنبيه: HF_TOKEN غير مضبوط في أسرار هذه المساحة — حفظ ملف الطالب وإنشاء مجلدات الخزنة لن يعملا إطلاقاً.")
32
-
33
- _hf_api = HfApi(token=HF_TOKEN) if HF_TOKEN else HfApi()
34
 
35
  print("⏳ تحميل النموذج...")
36
  _llm_tokenizer = AutoTokenizer.from_pretrained(MODEL_ID)
37
  _llm_model = AutoModelForCausalLM.from_pretrained(
38
- MODEL_ID,
39
- torch_dtype=torch.float32,
40
- device_map="cpu",
41
- low_cpu_mem_usage=True
42
  )
43
  print("✅ النموذج جاهز")
44
 
@@ -50,15 +33,21 @@ def load_whisper():
50
  if _whisper_pipe is not None:
51
  return _whisper_pipe
52
  from transformers import pipeline
53
- _whisper_pipe = pipeline(
54
- "automatic-speech-recognition",
55
- model=WHISPER_MODEL,
56
- device=DEVICE,
57
- chunk_length_s=30
58
- )
59
  return _whisper_pipe
60
 
61
- # ======================== الرادار - لم يتم لمسه ========================
 
 
 
 
 
 
 
 
 
 
 
62
  def call_radar_api(text, subject, detected_emotion):
63
  try:
64
  client = Client(RADAR_SPACE_URL)
@@ -86,90 +75,13 @@ def call_radar_api(text, subject, detected_emotion):
86
  print(f"⚠️ خطأ الرادار: {e}")
87
  return None
88
 
89
- # ======================== الخزنة - البحث عن الدرس ========================
90
- def lesson_to_file_prefix(lesson_str):
91
- match = re.search(r'\d+', lesson_str)
92
- return f"{int(match.group()):02d}" if match else "01"
93
-
94
- def find_lesson_in_vault(grade, semester, subject, lesson):
95
- try:
96
- lesson_prefix = lesson_to_file_prefix(lesson)
97
- folder_path = f"Curriculum_Core/{grade}/{semester}/{subject}"
98
-
99
- all_files = _hf_api.list_repo_files(repo_id=VAULT_REPO_ID, repo_type="dataset")
100
- target_prefix = f"{folder_path}/{lesson_prefix}_"
101
- matching_files = [f for f in all_files if f.startswith(target_prefix) and f.endswith(('.txt', '.md', '.json'))]
102
-
103
- if not matching_files:
104
- # ⚠️ سجل تشخيصي: يطبع المسار الذي بحث عنه فعلياً بالضبط، حتى تتأكد
105
- # مباشرة من مطابقته للهيكل الحقيقي في الخزنة (أهم سطر لتشخيص
106
- # مشكلة "الإجابة في وادٍ آخر" إذا كان السبب درساً مُحقَناً خاطئاً)
107
- print(f"⚠️ لم يتم ��لعثور على درس مطابق للمسار: {target_prefix}")
108
- return None
109
-
110
- lesson_file = matching_files[0]
111
- print(f"📂 تم العثور على الدرس ومطابقته: {lesson_file}")
112
-
113
- local_path = hf_hub_download(repo_id=VAULT_REPO_ID, filename=lesson_file, repo_type="dataset")
114
- with open(local_path, "r", encoding="utf-8") as f:
115
- content = f.read()
116
-
117
- if len(content) > MAX_LESSON_CHARS:
118
- print(f"✂️ محتوى الدرس ({len(content)} حرف) أطول من السقف، سيتم اقتطاعه إلى {MAX_LESSON_CHARS} حرف")
119
- content = content[:MAX_LESSON_CHARS]
120
-
121
- print(f"✅ تم تحميل الدرس ({len(content)} حرف بعد أي اقتطاع)")
122
- return content
123
- except Exception as e:
124
- print(f"⚠️ خطأ في البحث عن الدرس: {e}")
125
- return None
126
-
127
- def save_student_profile(grade, semester, subject, lesson, user_name, track=None):
128
- if not HF_TOKEN:
129
- print("⚠️ تخطّي حفظ ملف الطالب: لا يوجد HF_TOKEN في أسرار هذه المساحة.")
130
- return
131
- try:
132
- curriculum_path = f"Curriculum_Core/{grade}/{semester}/{subject}/"
133
- try:
134
- _hf_api.upload_file(
135
- path_or_fileobj=b"",
136
- path_in_repo=f"{curriculum_path}.gitkeep",
137
- repo_id=VAULT_REPO_ID, repo_type="dataset", token=HF_TOKEN
138
- )
139
- except Exception as e_gitkeep:
140
- print(f"⚠️ فشل إنشاء .gitkeep في {curriculum_path}: {e_gitkeep}")
141
-
142
- student_path = f"Student_Hub/User_{user_name}/"
143
- profile = {
144
- "user_name": user_name, "grade": grade, "semester": semester,
145
- "subject": subject, "lesson": lesson, "track": track,
146
- "last_updated": time.strftime("%Y-%m-%d %H:%M:%S")
147
- }
148
-
149
- _hf_api.upload_file(
150
- path_or_fileobj=json.dumps(profile, ensure_ascii=False, indent=2).encode("utf-8"),
151
- path_in_repo=f"{student_path}profile.json",
152
- repo_id=VAULT_REPO_ID, repo_type="dataset", token=HF_TOKEN
153
- )
154
- print(f"👤 تم حفظ: {student_path}profile.json")
155
- except Exception as e:
156
- print(f"⚠️ خطأ في الحفظ: {e}")
157
-
158
- # ======================== المواد والصفوف ========================
159
- GRADES = [
160
- "الروضة", "الصف الأول", "الصف الثاني", "الصف الثالث",
161
- "الصف الرابع", "الصف الخامس", "الصف السادس", "الصف السابع",
162
- "الصف الثامن", "الصف التاسع", "الصف العاشر", "الصف الحادي عشر",
163
- "الصف الثاني عشر (التوجيهي)"
164
- ]
165
-
166
- SEMESTERS = ["الفصل الأول", "الفصل الثاني"]
167
-
168
- def get_lesson_numbers():
169
- return [f"الدرس {i}" for i in range(1, 31)]
170
 
171
  # ======================== دالة المحادثة ========================
172
- def teacher_chat(audio_mic, subject, grade, semester, lesson, track, student_name, chat_history_state):
 
 
 
 
173
  if audio_mic is None:
174
  return chat_history_state, None, "🎤 الرجاء تسجيل السؤال أولاً"
175
 
@@ -184,67 +96,34 @@ def teacher_chat(audio_mic, subject, grade, semester, lesson, track, student_nam
184
  if not user_text:
185
  return chat_history_state, None, "❌ لم يتم التعرف على كلام"
186
 
187
- print(f"🗣️ {student_name}: {user_text}")
188
 
189
- # 2. البحث عن الدرس في الخزنة (مع سقف طول واقتطاع، انظر الدالة أعلاه)
190
- lesson_content = find_lesson_in_vault(grade, semester, subject, lesson)
191
-
192
- # 3. بناء messages
193
- context_parts = [f"مادة {subject}", f"{lesson}"]
194
- if semester:
195
- context_parts.insert(1, f"الفصل {semester}")
196
- if track:
197
- context_parts.append(f"تخصص {track}")
198
- context_str = " - ".join(context_parts)
199
-
200
- # ⚠️ تعديل جوهري على التوجيه: التزام صارم بصيغة الخطوات/الأسئلة كان
201
- # يجعل النموذج يطبّقها حتى على مجرد تحية، ويُهمل أحياناً السؤال الحي
202
- # لصالح "الالتزام" بمحتوى الدرس المرجعي. الآن: التحية تُعامل بشكل
203
- # طبيعي منفصل، والسؤال الحي له الأولوية القطعية على المرجع.
204
  system_prompt = (
205
- f"أنت الأستاذ عبود، معلم خبير ودود. السياق الحالي: {context_str}.\n"
206
- "- إذا كانت رسالة الطالب مجرد تحية أو سؤال عام (مثل: السلام عليكم، كيف حالك)، "
207
- "رد بتحية طبيعية ودودة وقصيرة فقط، بدون فرض خطوات أو أسئلة تعليمية.\n"
208
- "- إذا كان سؤالاً تعليمياً فعلياً: أجب بإيجاز (30 كلمة كحد أقصى)، "
209
- "لا تعط الإجابة كاملة، قسمها لخطوات، واطرح سؤالاً واحداً في النهاية.\n"
210
- "- السؤال الذي يطرحه الطالب الآن هو الأولوية القطعية دائماً. "
211
- "استخدم محتوى الدرس المرجعي أدناه (إن وُجد) كمصدر مساعد فقط إذا كان "
212
- "مرتبطاً مباشرة بسؤال الطالب — وتجاهله تماماً إذا لم يكن ذا صلة.\n"
213
- "أضف في كل ردودك التعليمية [حالة: حماس] أو [حالة: هدوء] (ليس مطلوباً مع التحية)."
214
  )
215
-
216
- if lesson_content:
217
- system_prompt += f"\n\n📖 محتوى الدرس المرجعي (مساعد فقط، وليس ملزماً):\n```\n{lesson_content}\n```"
218
- print(f"📚 تم حقن محتوى الدرس في الـ System Prompt ({len(lesson_content)} حرف)")
219
- else:
220
- print("ℹ️ لا يوجد محتوى درس مُحقَن لهذا السؤال (لم يُعثر على ملف مطابق، أو لا حاجة له).")
221
-
222
  messages = [{"role": "system", "content": system_prompt}]
223
  for msg in chat_history_state:
224
  messages.append({"role": msg["role"], "content": msg["content"]})
225
  messages.append({"role": "user", "content": user_text})
226
 
227
- # 4. توليد الرد
228
  try:
229
- text_prompt = _llm_tokenizer.apply_chat_template(
230
- messages, tokenize=False, add_generation_prompt=True
231
- )
232
  model_inputs = _llm_tokenizer([text_prompt], return_tensors="pt").to(DEVICE)
233
 
234
  with torch.no_grad():
235
  generated_ids = _llm_model.generate(
236
- **model_inputs, max_new_tokens=200, temperature=0.4,
237
- do_sample=True, repetition_penalty=1.15,
238
- pad_token_id=_llm_tokenizer.eos_token_id, use_cache=True
239
  )
240
 
241
- generated_ids = [
242
- output_ids[len(input_ids):]
243
- for input_ids, output_ids in zip(model_inputs.input_ids, generated_ids)
244
- ]
245
- full_response = _llm_tokenizer.batch_decode(
246
- generated_ids, skip_special_tokens=True
247
- )[0].strip()
248
 
249
  emotion_match = re.search(r'\[حالة:\s*(\w+)\]', full_response)
250
  detected_emotion = emotion_match.group(1) if emotion_match else "هدوء"
@@ -257,20 +136,18 @@ def teacher_chat(audio_mic, subject, grade, semester, lesson, track, student_nam
257
  except Exception as e:
258
  return chat_history_state, None, f"⚠️ خطأ النموذج: {e}"
259
 
260
- # 5. تحديث التاريخ
261
  chat_history_state = chat_history_state + [
262
  {"role": "user", "content": user_text},
263
  {"role": "assistant", "content": cleaned_response},
264
  ]
265
 
266
- # 6. حفظ في الخزنة
267
- save_student_profile(grade, semester, subject, lesson, student_name, track)
268
-
269
- # 7. توليد الصوت
270
  audio_path = call_radar_api(cleaned_response, subject, detected_emotion)
271
  status = f"✅ ({detected_emotion})" if audio_path else "⚠️ فشل الصوت"
272
  return chat_history_state, audio_path, status
273
 
 
274
  # ======================== الواجهة ========================
275
  with gr.Blocks(title="الأستاذ عبود", theme=gr.themes.Soft()) as demo:
276
  gr.Markdown("# 🎓 المنصة التعليمية - الأستاذ عبود")
@@ -279,44 +156,16 @@ with gr.Blocks(title="الأستاذ عبود", theme=gr.themes.Soft()) as demo:
279
 
280
  with gr.Row():
281
  with gr.Column(scale=2):
282
- with gr.Row():
283
- student_name_input = gr.Textbox(label="👤 اسم الطالب", value="Divid")
284
- grade_dropdown = gr.Dropdown(
285
- choices=GRADES, value="الصف العاشر", label="📆 الصف الدراسي"
286
- )
287
-
288
- with gr.Row():
289
- semester_dropdown = gr.Dropdown(
290
- choices=SEMESTERS, value="الفصل الأول", label="📅 الفصل الدراسي"
291
- )
292
- track_dropdown = gr.Dropdown(
293
- choices=[], value=None, label="🎯 التخصص (للتوجيهي فقط)",
294
- visible=False, allow_custom_value=True
295
- )
296
-
297
- with gr.Row(visible=False) as track_row:
298
- new_track_input = gr.Textbox(
299
- label="➕ أضف تخصصاً جديداً", placeholder="مثال: صناعي، زراعي"
300
- )
301
- add_track_btn = gr.Button("💾 حفظ التخصص", variant="secondary", size="sm")
302
-
303
- with gr.Row():
304
- subject_dropdown = gr.Dropdown(
305
- choices=[
306
- "🧮 الرياضيات والعلوم",
307
- "🕌 اللغة العربية والتربية الإسلامية",
308
- "🔤 اللغة الإنجليزية"
309
- ],
310
- value="🧮 الرياضيات والعلوم",
311
- label="📚 المادة"
312
- )
313
- lesson_dropdown = gr.Dropdown(
314
- choices=get_lesson_numbers(),
315
- value="الدرس 1",
316
- label="📖 رقم الدرس"
317
- )
318
 
 
 
319
  chatbot_display = gr.Chatbot(label="الحوار", height=400)
 
320
  mic_input = gr.Audio(label="🎤 سؤالك", type="filepath", sources=["microphone"])
321
  send_btn = gr.Button("🚀 إرسال", variant="primary")
322
 
@@ -324,29 +173,18 @@ with gr.Blocks(title="الأستاذ عبود", theme=gr.themes.Soft()) as demo:
324
  audio_output = gr.Audio(label="🔊 الرد", type="filepath", autoplay=True)
325
  status_output = gr.Textbox(label="الحالة")
326
 
327
- def toggle_track(grade):
328
- if "التوجيهي" in grade:
329
- return gr.update(visible=True), gr.update(visible=True), gr.update(visible=True)
330
- return gr.update(visible=False, value=None), gr.update(visible=False), gr.update(visible=False)
331
-
332
- grade_dropdown.change(
333
- fn=toggle_track,
334
- inputs=[grade_dropdown],
335
- outputs=[track_dropdown, track_row, add_track_btn]
336
- )
337
-
338
- def process_and_display(audio_mic, subject, grade, semester, lesson, track, student_name, history_state):
339
- new_history, audio, status = teacher_chat(
340
- audio_mic, subject, grade, semester, lesson, track, student_name, history_state
341
- )
342
  return new_history, new_history, audio, status, None
343
 
344
  send_btn.click(
345
  fn=process_and_display,
346
- inputs=[
347
- mic_input, subject_dropdown, grade_dropdown, semester_dropdown,
348
- lesson_dropdown, track_dropdown, student_name_input, chat_history_state
349
- ],
350
  outputs=[chat_history_state, chatbot_display, audio_output, status_output, mic_input]
351
  )
352
 
 
1
  import os
2
  import re
 
 
3
  import warnings
4
  import gradio as gr
5
  from gradio_client import Client
 
6
  import torch
7
  from transformers import AutoModelForCausalLM, AutoTokenizer
8
 
 
11
 
12
  # ======================== الإعدادات ========================
13
  DEVICE = "cpu"
14
+ WHISPER_MODEL = "openai/whisper-base"
15
+ MODEL_ID = "Qwen/Qwen2.5-0.5B-Instruct"
16
  RADAR_SPACE_URL = "Asem75/Aiocr_Radar"
17
+ # 🔐 يجب ضبط نفس القيمة بالضبط في إعدادات مساحة الرادار (Settings →
18
+ # Variables and secrets → INTERNAL_API_KEY) وفي إعدادات هذه المساحة أيضاً
 
 
 
 
19
  RADAR_API_KEY = os.environ.get("INTERNAL_API_KEY", "").strip()
 
 
 
 
 
 
 
20
 
21
  print("⏳ تحميل النموذج...")
22
  _llm_tokenizer = AutoTokenizer.from_pretrained(MODEL_ID)
23
  _llm_model = AutoModelForCausalLM.from_pretrained(
24
+ MODEL_ID, torch_dtype=torch.float32, device_map="cpu", low_cpu_mem_usage=True
 
 
 
25
  )
26
  print("✅ النموذج جاهز")
27
 
 
33
  if _whisper_pipe is not None:
34
  return _whisper_pipe
35
  from transformers import pipeline
36
+ _whisper_pipe = pipeline("automatic-speech-recognition", model=WHISPER_MODEL, device=DEVICE, chunk_length_s=30)
 
 
 
 
 
37
  return _whisper_pipe
38
 
39
+
40
+ # ======================== الرادار ========================
41
+ # ⚠️ إصلاح هام: نقطة API "/synth_arabic" على مساحة الرادار تستقبل
42
+ # بالضبط معاملين فقط: (text, speaker_name) — وهي مصمّمة خصيصاً لهذا
43
+ # الاستخدام الخارجي. الكود القديم هنا كان يستدعيها بـ7 معاملات تطابق
44
+ # توقيع دالة الواجهة الداخلية القديمة (synth_arabic) وليس نقطة الـ API
45
+ # الفعلية، فكان هذا الاستدعاء يفشل دائماً (يُمسَك بصمت داخل except).
46
+ #
47
+ # ⚠️ إصلاح ثانٍ: "ar-JO-IsmaeelNeural" و"ar-AE-AminaNeural" غير
48
+ # موجودين أصلاً في قائمة أصوات مايكروسوفت الرسمية (الأردن: Sana/Taim
49
+ # فقط، الإمارات: Fatima/Hamdan فقط؛ Amina هي صوت جزائري ar-DZ تحديداً) —
50
+ # استخدامهما يسبب NoAudioReceived. استبدلتهما بأصوات حقيقية موثّقة.
51
  def call_radar_api(text, subject, detected_emotion):
52
  try:
53
  client = Client(RADAR_SPACE_URL)
 
75
  print(f"⚠️ خطأ الرادار: {e}")
76
  return None
77
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
78
 
79
  # ======================== دالة المحادثة ========================
80
+ def teacher_chat(audio_mic, subject, chat_history_state):
81
+ """
82
+ التاريخ يُحفظ في gr.State كقائمة قواميس نظيفة بالضبط:
83
+ [{"role": "user"/"assistant", "content": "..."}, ...]
84
+ """
85
  if audio_mic is None:
86
  return chat_history_state, None, "🎤 الرجاء تسجيل السؤال أولاً"
87
 
 
96
  if not user_text:
97
  return chat_history_state, None, "❌ لم يتم التعرف على كلام"
98
 
99
+ print(f"🗣️ الطالب: {user_text}")
100
 
101
+ # 2. بناء messages
 
 
 
 
 
 
 
 
 
 
 
 
 
 
102
  system_prompt = (
103
+ "أنت الأستاذ عبود، معلم خبير. "
104
+ "أجب بإيجاز (30 كلمة كحد أقصى). "
105
+ "لا تعط الإجابة كاملة، قسمها لخطوات. "
106
+ طرح سؤالاً في النهاية. "
107
+ "أضف الة: حماس] أو [حالة: هدوء]."
 
 
 
 
108
  )
 
 
 
 
 
 
 
109
  messages = [{"role": "system", "content": system_prompt}]
110
  for msg in chat_history_state:
111
  messages.append({"role": msg["role"], "content": msg["content"]})
112
  messages.append({"role": "user", "content": user_text})
113
 
114
+ # 3. توليد الرد
115
  try:
116
+ text_prompt = _llm_tokenizer.apply_chat_template(messages, tokenize=False, add_generation_prompt=True)
 
 
117
  model_inputs = _llm_tokenizer([text_prompt], return_tensors="pt").to(DEVICE)
118
 
119
  with torch.no_grad():
120
  generated_ids = _llm_model.generate(
121
+ **model_inputs, max_new_tokens=80, temperature=0.4,
122
+ do_sample=True, pad_token_id=_llm_tokenizer.eos_token_id, use_cache=True
 
123
  )
124
 
125
+ generated_ids = [output_ids[len(input_ids):] for input_ids, output_ids in zip(model_inputs.input_ids, generated_ids)]
126
+ full_response = _llm_tokenizer.batch_decode(generated_ids, skip_special_tokens=True)[0].strip()
 
 
 
 
 
127
 
128
  emotion_match = re.search(r'\[حالة:\s*(\w+)\]', full_response)
129
  detected_emotion = emotion_match.group(1) if emotion_match else "هدوء"
 
136
  except Exception as e:
137
  return chat_history_state, None, f"⚠️ خطأ النموذج: {e}"
138
 
139
+ # 4. تحديث التاريخ في الـ state (قائمة قواميس نظيفة دائماً)
140
  chat_history_state = chat_history_state + [
141
  {"role": "user", "content": user_text},
142
  {"role": "assistant", "content": cleaned_response},
143
  ]
144
 
145
+ # 5. توليد الصوت
 
 
 
146
  audio_path = call_radar_api(cleaned_response, subject, detected_emotion)
147
  status = f"✅ ({detected_emotion})" if audio_path else "⚠️ فشل الصوت"
148
  return chat_history_state, audio_path, status
149
 
150
+
151
  # ======================== الواجهة ========================
152
  with gr.Blocks(title="الأستاذ عبود", theme=gr.themes.Soft()) as demo:
153
  gr.Markdown("# 🎓 المنصة التعليمية - الأستاذ عبود")
 
156
 
157
  with gr.Row():
158
  with gr.Column(scale=2):
159
+ subject_dropdown = gr.Dropdown(
160
+ choices=["🧮 الرياضيات والعلوم", "🕌 اللغة العربية والتربية الإسلامية", "🔤 اللغة الإنجليزية"],
161
+ value="🧮 الرياضيات والعلوم",
162
+ label="📚 المادة"
163
+ )
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
164
 
165
+ # ⚠️ في Gradio 6.3.0+ صيغة messages هي الوحيدة الموجودة أصلاً،
166
+ # وتمت إزالة معامل type نهائياً (لا حاجة له ولا يُقبل كوسيط)
167
  chatbot_display = gr.Chatbot(label="الحوار", height=400)
168
+
169
  mic_input = gr.Audio(label="🎤 سؤالك", type="filepath", sources=["microphone"])
170
  send_btn = gr.Button("🚀 إرسال", variant="primary")
171
 
 
173
  audio_output = gr.Audio(label="🔊 الرد", type="filepath", autoplay=True)
174
  status_output = gr.Textbox(label="الحالة")
175
 
176
+ # ⚠️ الإصلاح الجذري الفعلي: لا حاجة لأي تحويل لصيغة tuples القديمة
177
+ # ([user_msg, asst_msg]) — هذا بالضبط كان يكسر Gradio 6. التاريخ
178
+ # القادم من teacher_chat هو بالفعل بصيغة messages الصحيحة تماماً
179
+ # (قائمة قواميس role/content)، فنمرره مباشرة لكل من الـ State
180
+ # وعنصر العرض chatbot_display بدون أي تعديل.
181
+ def process_and_display(audio_mic, subject, history_state):
182
+ new_history, audio, status = teacher_chat(audio_mic, subject, history_state)
 
 
 
 
 
 
 
 
183
  return new_history, new_history, audio, status, None
184
 
185
  send_btn.click(
186
  fn=process_and_display,
187
+ inputs=[mic_input, subject_dropdown, chat_history_state],
 
 
 
188
  outputs=[chat_history_state, chatbot_display, audio_output, status_output, mic_input]
189
  )
190