Asem75 commited on
Commit
b9eb636
·
verified ·
1 Parent(s): 11053ab

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +354 -1
app.py CHANGED
@@ -1 +1,354 @@
1
- eduassistant75@gmail.com
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
+
12
+ # ======================== كتم التحذيرات ========================
13
+ 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
+
45
+ # ======================== Whisper ========================
46
+ _whisper_pipe = None
47
+
48
+ def load_whisper():
49
+ global _whisper_pipe
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)
65
+ word_count = len(text.split())
66
+
67
+ if word_count <= 35:
68
+ speaker_name = "ar-EG-SalmaNeural"
69
+ else:
70
+ voice_map = {
71
+ "🧮 الرياضيات والعلوم": "ar-JO-TaimNeural",
72
+ "🕌 اللغة العربية والتربية الإسلامية": "ar-SA-HamedNeural",
73
+ "🔤 اللغة الإنجليزية": "en-US-JennyNeural",
74
+ }
75
+ speaker_name = voice_map.get(subject, "ar-JO-TaimNeural")
76
+
77
+ audio_url = client.predict(
78
+ text, speaker_name, RADAR_API_KEY,
79
+ api_name="/synth_arabic"
80
+ )
81
+
82
+ if audio_url and isinstance(audio_url, (list, tuple)):
83
+ audio_url = audio_url[0]
84
+ return str(audio_url).strip() if audio_url else None
85
+ except Exception as e:
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
+
176
+ # 1. تحويل الصوت لنص
177
+ try:
178
+ whisper = load_whisper()
179
+ result = whisper(audio_mic, generate_kwargs={"language": "arabic"})
180
+ user_text = result["text"].strip()
181
+ except Exception as e:
182
+ return chat_history_state, None, f"⚠️ خطأ: {e}"
183
+
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 "هدوء"
251
+ cleaned_response = re.sub(r'\[حالة:\s*\w+\]', '', full_response).strip()
252
+
253
+ if not cleaned_response.endswith(('.', '؟', '!')):
254
+ cleaned_response += '.'
255
+
256
+ print(f"🤖 الأستاذ: {cleaned_response}")
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("# 🎓 المنصة التعليمية - الأستاذ عبود")
277
+
278
+ chat_history_state = gr.State([])
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
+
323
+ with gr.Column(scale=1):
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
+
353
+ if __name__ == "__main__":
354
+ demo.launch(server_name="0.0.0.0", server_port=7860)