Asem75's picture
Update app.py
93db9f9 verified
Raw
History Blame
22.8 kB
import os
import re
import json
import warnings
import gradio as gr
from gradio_client import Client
from huggingface_hub import HfApi
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"
VAULT_REPO_ID = "Asem75/My_teacher_volt"
# التوكنز من البيئة
HF_TOKEN = os.environ.get("HF_TOKEN", "").strip()
RADAR_API_KEY = os.environ.get("INTERNAL_API_KEY", "").strip()
# إنشاء عميل Hub للتعامل مع الخزنة
_hf_api = HfApi(token=HF_TOKEN) if HF_TOKEN else None
# ======================== الملف المرجعي للمواد المخصصة ========================
CUSTOM_SUBJECTS_PATH = "Curriculum_Core/custom_subjects.json"
def load_custom_subjects():
"""تحميل قائمة المواد المخصصة من الخزنة."""
if not _hf_api:
return []
try:
from huggingface_hub import hf_hub_download
path = hf_hub_download(
repo_id=VAULT_REPO_ID,
filename=CUSTOM_SUBJECTS_PATH,
repo_type="dataset",
token=HF_TOKEN
)
with open(path, "r", encoding="utf-8") as f:
data = json.load(f)
return data.get("subjects", [])
except Exception:
return []
def save_custom_subjects(subjects_list):
"""حفظ قائمة المواد المخصصة إلى الخزنة."""
if not _hf_api:
print("⚠️ لا يوجد HF_TOKEN")
return
try:
content = json.dumps(
{"subjects": subjects_list},
ensure_ascii=False,
indent=2
)
_hf_api.upload_file(
path_or_fileobj=content.encode("utf-8"),
path_in_repo=CUSTOM_SUBJECTS_PATH,
repo_id=VAULT_REPO_ID,
repo_type="dataset",
token=HF_TOKEN
)
print(f"✅ تم حفظ المواد المخصصة: {subjects_list}")
except Exception as e:
print(f"⚠️ خطأ في حفظ المواد: {e}")
# ======================== المواد الأساسية ========================
BASE_SUBJECTS = [
"الرياضيات",
"العلوم",
"اللغة العربية",
"اللغة الإنجليزية",
"التربية الإسلامية",
"الفيزياء",
"الكيمياء",
"الأحياء",
"التاريخ",
"الجغرافيا",
"التربية الوطنية",
"الحاسوب",
"الفلسفة",
"الفرنسية"
]
def get_all_subjects():
"""دمج المواد الأساسية مع المخصصة."""
custom = load_custom_subjects()
all_subjects = list(dict.fromkeys(BASE_SUBJECTS + custom))
return all_subjects
# ======================== إدارة التخصصات للتوجيهي ========================
CUSTOM_TRACKS_PATH = "Curriculum_Core/custom_tracks.json"
def load_custom_tracks():
"""تحميل قائمة التخصصات المخصصة للتوجيهي من الخزنة."""
if not _hf_api:
return []
try:
from huggingface_hub import hf_hub_download
path = hf_hub_download(
repo_id=VAULT_REPO_ID,
filename=CUSTOM_TRACKS_PATH,
repo_type="dataset",
token=HF_TOKEN
)
with open(path, "r", encoding="utf-8") as f:
data = json.load(f)
return data.get("tracks", [])
except Exception:
return []
def save_custom_tracks(tracks_list):
"""حفظ قائمة التخصصات المخصصة إلى الخزنة."""
if not _hf_api:
print("⚠️ لا يوجد HF_TOKEN")
return
try:
content = json.dumps(
{"tracks": tracks_list},
ensure_ascii=False,
indent=2
)
_hf_api.upload_file(
path_or_fileobj=content.encode("utf-8"),
path_in_repo=CUSTOM_TRACKS_PATH,
repo_id=VAULT_REPO_ID,
repo_type="dataset",
token=HF_TOKEN
)
print(f"✅ تم حفظ التخصصات المخصصة: {tracks_list}")
except Exception as e:
print(f"⚠️ خطأ في حفظ التخصصات: {e}")
def get_all_tracks():
"""جلب جميع التخصصات (الفارغة افتراضياً)."""
return load_custom_tracks()
# ======================== عدد الدروس ========================
MAX_LESSONS = 30
def get_lesson_numbers():
"""إرجاع قائمة بأرقام الدروس: الدرس 1، الدرس 2، ..."""
return [f"الدرس {i}" for i in range(1, MAX_LESSONS + 1)]
# ======================== تحميل النموذج ========================
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
# ======================== تحويل رقم الدرس إلى صيغة الملف ========================
def lesson_to_file_prefix(lesson_str):
"""
تحويل 'الدرس 1' إلى '01'، 'الدرس 15' إلى '15'.
"""
match = re.search(r'\d+', lesson_str)
if match:
num = int(match.group())
return f"{num:02d}"
return "01"
# ======================== إدارة الخزنة السيادية ========================
def ensure_vault_structure(grade, semester, subject, lesson, user_name, track=None):
"""
بناء المسار الشجري في الخزنة:
- Curriculum_Core/{grade}/{semester}/{subject}/
- البحث عن ملف يبدأ بـ {lesson_prefix}_ داخل المجلد.
- Student_Hub/User_{user_name}/profile.json
"""
if not _hf_api:
print("⚠️ لا يوجد HF_TOKEN للتعامل مع الخزنة")
return
try:
# مسار المناهج مع الفصل الدراسي
curriculum_path = f"Curriculum_Core/{grade}/{semester}/{subject}/"
lesson_prefix = lesson_to_file_prefix(lesson)
print(f"📂 مسار المناهج: {curriculum_path}")
print(f"🔍 البحث عن ملف يبدأ بـ: {lesson_prefix}_")
# إنشاء ملف .gitkeep لحفظ المجلد
_hf_api.upload_file(
path_or_fileobj=b"",
path_in_repo=f"{curriculum_path}.gitkeep",
repo_id=VAULT_REPO_ID,
repo_type="dataset",
token=HF_TOKEN
)
# مسار الطالب
student_path = f"Student_Hub/User_{user_name}/"
profile = {
"user_name": user_name,
"grade": grade,
"semester": semester,
"subject": subject,
"lesson": lesson,
"track": track if track else None,
"last_updated": None
}
_hf_api.upload_file(
path_or_fileobj=json.dumps(
profile,
ensure_ascii=False,
indent=2
).encode("utf-8"),
path_in_repo=f"{student_path}profile.json",
repo_id=VAULT_REPO_ID,
repo_type="dataset",
token=HF_TOKEN
)
print(f"👤 تم تحديث ملف الطالب: {student_path}profile.json")
except Exception as e:
print(f"⚠️ خطأ في الخزنة: {e}")
# ======================== الرادار الصوتي ========================
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",
"التربية الإسلامية": "ar-SA-HamedNeural"
}
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,
student_name,
grade,
semester,
lesson,
track,
chat_history_state
):
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"🗣️ الطالب {student_name}: {user_text}")
# 2. بناء messages
context_parts = [f"مادة {subject}", f"{lesson}"]
if semester:
context_parts.insert(1, f"الفصل {semester}")
if track:
context_parts.append(f"تخصص {track}")
context_str = " - ".join(context_parts)
system_prompt = (
f"أنت الأستاذ عبود، معلم خبير. "
f"السياق الحالي: {context_str}. "
"أجب بإيجاز (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. تحديث الخزنة السحابية مع الفصل الدراسي والتخصص
ensure_vault_structure(
grade=grade,
semester=semester,
subject=subject,
lesson=lesson,
user_name=student_name,
track=track
)
# 5. تحديث التاريخ
chat_history_state = chat_history_state + [
{"role": "user", "content": user_text},
{"role": "assistant", "content": cleaned_response}
]
# 6. توليد الصوت
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
# ======================== الواجهة المطورة ========================
GRADES = [
"الروضة",
"الصف الأول",
"الصف الثاني",
"الصف الثالث",
"الصف الرابع",
"الصف الخامس",
"الصف السادس",
"الصف السابع",
"الصف الثامن",
"الصف التاسع",
"الصف العاشر",
"الصف الحادي عشر",
"الصف الثاني عشر (التوجيهي)"
]
SEMESTERS = ["الفصل الأول", "الفصل الثاني"]
ALL_SUBJECTS = get_all_subjects()
LESSON_NUMBERS = get_lesson_numbers()
ALL_TRACKS = get_all_tracks()
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):
# حقول تتبع الطالب
with gr.Row():
student_name_input = gr.Textbox(
label="👤 اسم الطالب",
value="Divid",
placeholder="أدخل اسم الطالب"
)
grade_dropdown = gr.Dropdown(
choices=GRADES,
value="الصف العاشر",
label="📆 الصف الدراسي"
)
# الفصل الدراسي + التخصص للتوجيهي
with gr.Row():
semester_dropdown = gr.Dropdown(
choices=SEMESTERS,
value="الفصل الأول",
label="📅 الفصل الدراسي"
)
# حقل التخصص - يظهر فقط عند اختيار التوجيهي
track_dropdown = gr.Dropdown(
choices=ALL_TRACKS,
value=None,
label="🎯 التخصص (للتوجيهي فقط)",
visible=False,
allow_custom_value=True
)
# صف إضافة تخصص جديد (يظهر فقط مع التوجيهي)
with gr.Row(visible=False) as track_input_row:
new_track_input = gr.Textbox(
label="➕ أضف تخصصاً جديداً",
placeholder="مثال: صناعي، زراعي، تجاري..."
)
add_track_btn = gr.Button(
"💾 حفظ التخصص",
variant="secondary",
size="sm"
)
with gr.Row():
subject_dropdown = gr.Dropdown(
choices=ALL_SUBJECTS,
value=ALL_SUBJECTS[0] if ALL_SUBJECTS else "الرياضيات",
label="📚 المادة الدراسية",
allow_custom_value=True
)
lesson_dropdown = gr.Dropdown(
choices=LESSON_NUMBERS,
value="الدرس 1",
label="📖 رقم الدرس"
)
# إضافة مادة جديدة
with gr.Row():
new_subject_input = gr.Textbox(
label="➕ أضف مادة جديدة (مخصصة)",
placeholder="اكتب اسم المادة ثم اضغط الزر..."
)
add_subject_btn = gr.Button(
"💾 حفظ المادة في الخزنة",
variant="secondary",
size="sm"
)
# رسالة حالة الخزنة
vault_status = gr.Textbox(
label="📋 حالة الخزنة",
value="🟢 جاهزة",
interactive=False
)
# المحادثة
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):
gr.Markdown("### 📊 لوحة المعلومات")
gr.Markdown(
f"""
| الإعداد | القيمة |
|---------|--------|
| الخزنة | `{VAULT_REPO_ID}` |
| الرادار | `{RADAR_SPACE_URL}` |
| المواد الأساسية | {len(BASE_SUBJECTS)} مادة |
| المواد المخصصة | {len(load_custom_subjects())} مادة |
| الدروس المتاحة | 1 - {MAX_LESSONS} |
"""
)
gr.Markdown("---")
audio_output = gr.Audio(
label="🔊 رد الأستاذ",
type="filepath",
autoplay=True
)
status_output = gr.Textbox(
label="📌 حالة الطلب",
interactive=False
)
# ════════════════════ دوال الأحداث ════════════════════
def toggle_track_fields(grade):
"""إظهار/إخفاء حقول التخصص عند اختيار التوجيهي."""
if "التوجيهي" in grade:
return (
gr.update(visible=True),
gr.update(visible=True),
gr.update(visible=True)
)
else:
return (
gr.update(visible=False, value=None),
gr.update(visible=False),
gr.update(visible=False)
)
grade_dropdown.change(
fn=toggle_track_fields,
inputs=[grade_dropdown],
outputs=[track_dropdown, track_input_row, add_track_btn]
)
def add_custom_subject(new_subject):
"""إضافة مادة جديدة إلى الخزنة وتحديث القائمة المنسدلة."""
if not new_subject or not new_subject.strip():
return gr.update(), "⚠️ الرجاء كتابة اسم المادة أولاً"
new_subject = new_subject.strip()
current_custom = load_custom_subjects()
if new_subject in BASE_SUBJECTS:
return (
gr.update(),
f"⚠️ '{new_subject}' موجودة مسبقاً في المواد الأساسية"
)
if new_subject in current_custom:
return (
gr.update(),
f"⚠️ '{new_subject}' موجودة مسبقاً في المواد المخصصة"
)
current_custom.append(new_subject)
save_custom_subjects(current_custom)
updated_subjects = get_all_subjects()
return (
gr.update(choices=updated_subjects, value=new_subject),
f"✅ تمت إضافة '{new_subject}' إلى الخزنة بنجاح!"
)
add_subject_btn.click(
fn=add_custom_subject,
inputs=[new_subject_input],
outputs=[subject_dropdown, vault_status]
)
def add_custom_track(new_track):
"""إضافة تخصص جديد للتوجيهي."""
if not new_track or not new_track.strip():
return gr.update(), "⚠️ الرجاء كتابة اسم التخصص أولاً"
new_track = new_track.strip()
current_tracks = load_custom_tracks()
if new_track in current_tracks:
return (
gr.update(),
f"⚠️ '{new_track}' موجود مسبقاً في التخصصات"
)
current_tracks.append(new_track)
save_custom_tracks(current_tracks)
updated_tracks = get_all_tracks()
return (
gr.update(choices=updated_tracks, value=new_track),
f"✅ تمت إضافة تخصص '{new_track}' بنجاح!"
)
add_track_btn.click(
fn=add_custom_track,
inputs=[new_track_input],
outputs=[track_dropdown, vault_status]
)
def process_and_display(
audio_mic,
student_name,
grade,
semester,
subject,
lesson,
track,
history_state
):
new_history, audio, status = teacher_chat(
audio_mic=audio_mic,
subject=subject,
student_name=student_name,
grade=grade,
semester=semester,
lesson=lesson,
track=track,
chat_history_state=history_state
)
return (
new_history,
new_history,
audio,
f"🟢 {status}" if audio else f"🔴 {status}",
None
)
send_btn.click(
fn=process_and_display,
inputs=[
mic_input,
student_name_input,
grade_dropdown,
semester_dropdown,
subject_dropdown,
lesson_dropdown,
track_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)