Asem75's picture
Update app.py
16eb743 verified
Raw
History Blame Contribute Delete
45 kB
import os
import re
import json
import time
import gc
import warnings
import asyncio
import threading
import edge_tts
import gradio as gr
from gradio_client import Client
from huggingface_hub import HfApi, hf_hub_download
import torch
from PIL import Image
import sympy as sp
from transformers import AutoModelForCausalLM, AutoTokenizer, TrOCRProcessor, VisionEncoderDecoderModel
warnings.filterwarnings("ignore")
# โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•
# ุงู„ุฅุนุฏุงุฏุงุช ูˆุงู„ุฑูˆุงุจุท ุจูŠู† ุงู„ู…ุณุงุญุงุช
# โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•
CONTROLLER_SPACE_URL = "Asem75/My_teacher_controller"
MEDIA_SPACE_URL = "Asem75/My_teacher_enemation"
VAULT_REPO_ID = "Asem75/aiocr_asistant"
MODEL_ID = "Qwen/Qwen2.5-3B-Instruct"
WHISPER_MODEL = "openai/whisper-medium"
OCR_MODEL_ID = "RayR1/trocr-base-arabic-handwritten" # ุชุตุญูŠุญ: ุฑุงุฌุน ู…ู„ู Chat ู„ู„ุชูุตูŠู„
DEVICE = "cpu"
MAX_LESSON_CHARS = 1200
RADAR_API_KEY = os.environ.get("INTERNAL_API_KEY", "").strip()
HF_TOKEN = os.environ.get("HF_TOKEN", "").strip()
if not HF_TOKEN:
print("โš ๏ธ ุชู†ุจูŠู‡: HF_TOKEN ุบูŠุฑ ู…ุถุจูˆุท โ€” ุญูุธ ู…ู„ู ุงู„ุทุงู„ุจ ู‚ุฏ ูŠูุดู„.")
_hf_api = HfApi(token=HF_TOKEN) if HF_TOKEN else HfApi()
# โš ๏ธ ุชุญู…ูŠู„ ูƒุณูˆู„ ู„ูƒูˆูŠู† ุจู†ุงุกู‹ ุนู„ู‰ ุทู„ุจูƒ: ู„ุง ูŠูุญู…ูŽู‘ู„ ุนู†ุฏ ุฅู‚ู„ุงุน ุงู„ุณูŠุฑูุฑุŒ ุจู„
# ูู‚ุท ุนู†ุฏ ุฃูˆู„ ุงุณุชุฎุฏุงู… ูุนู„ูŠ (ุฃูˆู„ ุฏุฎูˆู„ ู„ุบุฑูุฉ ุงู„ุตู ูˆุฅุฑุณุงู„ ุณุคุงู„) โ€” ู„ุชุฎููŠู
# ุงู„ุถุบุท ุนู„ู‰ ุงู„ู…ุณุงุญุฉ ุนู†ุฏ ุงู„ุฅู‚ู„ุงุน ูˆุนู†ุฏ ุงู„ุชุตูุญ ุจุฏูˆู† ู…ุญุงุฏุซุฉ ูุนู„ูŠุฉ.
_llm_tokenizer, _llm_model = None, None
_qwen_lock = threading.Lock()
def load_qwen_lazy():
global _llm_tokenizer, _llm_model
if _llm_model is not None:
return _llm_tokenizer, _llm_model
with _qwen_lock:
if _llm_model is not None:
return _llm_tokenizer, _llm_model
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("โœ… ูƒูˆูŠู† ุฌุงู‡ุฒ")
return _llm_tokenizer, _llm_model
# ู‚ุงุฆู…ุฉ ุฃุตูˆุงุช ู…ุงูŠูƒุฑูˆุณูˆูุช ุงู„ุนุฑุจูŠุฉ ุงู„ู…ูˆุซูŽู‘ู‚ุฉ (ู†ูุณ ุงู„ู‚ุงุฆู…ุฉ ุงู„ู…ุนุชู…ุฏุฉ ููŠ ุงู„ุฑุงุฏุงุฑ)
MICROSOFT_VOICES = [
"ar-EG-SalmaNeural", "ar-EG-ShakirNeural", "ar-JO-SanaNeural", "ar-JO-TaimNeural",
"ar-SA-ZariyahNeural", "ar-SA-HamedNeural", "ar-AE-FatimaNeural", "ar-AE-HamdanNeural",
"ar-IQ-RanaNeural", "ar-IQ-BasselNeural", "ar-LB-LaylaNeural", "ar-LB-RamiNeural",
"ar-SY-AmanyNeural", "ar-SY-LaithNeural", "ar-MA-MounaNeural", "ar-MA-JamalNeural",
"ar-TN-ReemNeural", "ar-TN-HediNeural", "ar-DZ-AminaNeural", "ar-DZ-IsmaelNeural",
"ar-LY-ImanNeural", "ar-LY-OmarNeural", "ar-KW-NouraNeural", "ar-KW-FahedNeural",
"ar-QA-AmalNeural", "ar-QA-MoazNeural", "ar-OM-AyshaNeural", "ar-OM-AbdullahNeural",
"ar-BH-LailaNeural", "ar-BH-AliNeural", "ar-YE-MaryamNeural", "ar-YE-SalehNeural",
]
VOICE_ROLES = ["ู…ุญุงูˆุฑ", "ุดุฑุญ", "ุฃุณุฆู„ุฉ"]
DEFAULT_TEACHER_PREFS = [
{"name": "ุงู„ู…ุนู„ู… ุงู„ุฃูˆู„", "ู…ุญุงูˆุฑ": "ar-JO-TaimNeural", "ุดุฑุญ": "ar-SA-HamedNeural", "ุฃุณุฆู„ุฉ": "ar-EG-ShakirNeural"},
{"name": "ุงู„ู…ุนู„ู…ุฉ ุงู„ุซุงู†ูŠุฉ", "ู…ุญุงูˆุฑ": "ar-EG-SalmaNeural", "ุดุฑุญ": "ar-AE-FatimaNeural", "ุฃุณุฆู„ุฉ": "ar-LB-LaylaNeural"},
{"name": "ุงู„ู…ุนู„ู… ุงู„ุซุงู„ุซ", "ู…ุญุงูˆุฑ": "ar-SA-HamedNeural", "ุดุฑุญ": "ar-JO-TaimNeural", "ุฃุณุฆู„ุฉ": "ar-IQ-BasselNeural"},
]
THEME_CSS = {
"๐ŸŒž ู†ู‡ุงุฑูŠ": "",
"๐ŸŒ™ ู„ูŠู„ูŠ": "body, .gradio-container { background-color:#0b0f19 !important; color:#f3f4f6 !important; } .gr-button { background:#1e293b !important; color:#f3f4f6 !important; }",
"๐ŸŽจ ู…ู„ูˆู‘ู†": "body, .gradio-container { background: linear-gradient(135deg,#fef3c7,#dbeafe) !important; } .gr-button { background: linear-gradient(135deg,#f59e0b,#3b82f6) !important; color:white !important; }",
}
def apply_theme(theme_name):
css = THEME_CSS.get(theme_name, "")
return f"<style>{css}</style>"
# โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•
# ๐Ÿ”— ุงู„ุฑุจุท ุจู€ Controller (Phi-4-mini โ€” ุฃุณุชุงุฐ ุนู„ู…ูŠุŒ ู†ุต ูู‚ุทุŒ ู„ุง ุตูˆุช ุฅุทู„ุงู‚ุงู‹)
# โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•
def ask_science_teacher_via_controller(text, task_type="teach", history=None):
try:
client = Client(CONTROLLER_SPACE_URL)
return client.predict(text, task_type, history or [], api_name="/ask_science_teacher")
except Exception as e:
print(f"โš ๏ธ ุฎุทุฃ ุงู„ูƒู†ุชุฑูˆู„ุฑ: {e}")
return None
def deliver_via_qwen(original_question, phi_content, history):
"""Phi ู„ุง ูŠุชุญุฏุซ ู…ุน ุงู„ุทุงู„ุจ ู…ุจุงุดุฑุฉ ุฃุจุฏุงู‹ โ€” ูŠู…ุฑ ุนุจุฑ ูƒูˆูŠู† ุฏุงุฆู…ุงู‹.
โš ๏ธ ู†ุจู†ูŠ ุงู„ุณุฌู„ ูŠุฏูˆูŠุงู‹ ุจุณุคุงู„ ุงู„ุทุงู„ุจ ุงู„ุญู‚ูŠู‚ูŠ (ู„ุง ู†ุต ุงู„ุชูˆุฌูŠู‡ ุงู„ุฏุงุฎู„ูŠ)."""
delivery_prompt = (
f"ุฒู…ูŠู„ูƒ ุงู„ู…ุนู„ู… ุงู„ู…ุชุฎุตุต ุจุงู„ุนู„ูˆู… (Phi) ุฃุนุทุงูƒ ู‡ุฐุง ุงู„ู…ุญุชูˆู‰ ู„ุณุคุงู„ ุงู„ุทุงู„ุจ \"{original_question}\":\n"
f"{phi_content}\n\nุฃุนุฏ ุตูŠุงุบุชู‡ ุจุฃุณู„ูˆุจูƒ ุงู„ูˆุฏูˆุฏ ู…ุจุงุดุฑุฉ ู„ู„ุทุงู„ุจ (30 ูƒู„ู…ุฉ ูƒุญุฏ ุฃู‚ุตู‰ุŒ ุฎุทูˆุงุชุŒ ุณุคุงู„ ุฎุชุงู…ูŠ)."
)
response, _ = ask_teacher(delivery_prompt, history)
new_history = history + [{"role": "user", "content": original_question}, {"role": "assistant", "content": response}]
return response, new_history
def ask_teacher(prompt, history):
tokenizer, model = load_qwen_lazy()
history = history or []
messages = [{"role": "system", "content": "ุฃู†ุช ูƒูˆูŠู†ุŒ ุงู„ุนู‚ู„ ุงู„ู…ุฏุจุฑ ู„ู…ู†ุตุฉ ุชุนู„ูŠู…ูŠุฉ ุนุฑุจูŠุฉ. ุชุณุงุนุฏ ููŠ ุตูŠุงุบุฉ ู…ุญุชูˆู‰ ุชุนู„ูŠู…ูŠ ู‚ุตูŠุฑ ูˆูˆุงุถุญ."}]
messages.extend(history)
messages.append({"role": "user", "content": prompt})
text_prompt = tokenizer.apply_chat_template(messages, tokenize=False, add_generation_prompt=True)
inputs = tokenizer([text_prompt], return_tensors="pt").to(DEVICE)
with torch.no_grad():
out_ids = model.generate(**inputs, max_new_tokens=200, temperature=0.5, do_sample=True, repetition_penalty=1.15, pad_token_id=tokenizer.eos_token_id)
out_ids = [o[len(i):] for i, o in zip(inputs.input_ids, out_ids)]
response = tokenizer.batch_decode(out_ids, skip_special_tokens=True)[0].strip()
gc.collect() # ุชุฎููŠู ุฐุฑูˆุฉ ุงู„ุฐุงูƒุฑุฉ ุจุนุฏ ูƒู„ ุชูˆู„ูŠุฏ
new_history = history + [{"role": "user", "content": prompt}, {"role": "assistant", "content": response}]
return response, new_history
def generate_image_via_media(prompt):
try:
client = Client(MEDIA_SPACE_URL)
return client.predict(prompt, RADAR_API_KEY, api_name="/generate_image")
except Exception as e:
print(f"โš ๏ธ ุฎุทุฃ ู…ุณุงุญุฉ ุงู„ูˆุณุงุฆุท: {e}")
return None
# โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•
# ๐ŸŽ™๏ธ ุฎุท ู…ุนุงู„ุฌุฉ ุงู„ุตูˆุช ุงู„ู…ุชู‚ุฏู… (4 ู…ุฑุงุญู„) โ€” ูƒู„ ู…ุฑุญู„ุฉ ู…ุญู…ูŠุฉ ุจุดูƒู„ ู…ุณุชู‚ู„
# โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•
SKIP_HEAVY_AUDIO_FILTERS = os.environ.get("SKIP_HEAVY_AUDIO_FILTERS", "").strip().lower() in ("1", "true", "yes")
if SKIP_HEAVY_AUDIO_FILTERS:
print("๐Ÿงช ูˆุถุน ุงู„ุชุดุฎูŠุต: ุชุฎุทู‘ูŠ DeepFilterNet ูˆMetricGAN+ (ู…ูุนู‘ู„ ุนุจุฑ SKIP_HEAVY_AUDIO_FILTERS)")
def normalize_audio_pydub(input_path):
try:
from pydub import AudioSegment, effects
sound = AudioSegment.from_file(input_path)
normalized = effects.normalize(sound)
output_path = "/tmp/step1_normalized.wav"
normalized.export(output_path, format="wav")
return output_path
except Exception as e:
print(f"โš ๏ธ ูุดู„ุช ุฎุทูˆุฉ ุชุทุจูŠุน ุงู„ุตูˆุช: {e}")
return input_path
_df_model, _df_state = None, None
def denoise_with_deepfilternet(input_path):
global _df_model, _df_state
try:
from df.enhance import enhance as df_enhance, init_df, load_audio as df_load_audio, save_audio as df_save_audio
if _df_model is None:
print("โณ ุชุญู…ูŠู„ DeepFilterNet...")
_df_model, _df_state, _ = init_df()
audio, _ = df_load_audio(input_path, sr=_df_state.sr())
enhanced = df_enhance(_df_model, _df_state, audio)
output_path = "/tmp/step2_denoised.wav"
df_save_audio(output_path, enhanced, _df_state.sr())
return output_path
except Exception as e:
print(f"โš ๏ธ ูุดู„ุช ุฎุทูˆุฉ ุชุตููŠุฉ ุงู„ุถูˆุถุงุก: {e}")
return input_path
_metricgan_model = None
def isolate_speaker_metricgan(input_path):
global _metricgan_model
try:
import torchaudio
from speechbrain.inference.enhancement import SpectralMaskEnhancement
if _metricgan_model is None:
print("โณ ุชุญู…ูŠู„ MetricGAN+...")
_metricgan_model = SpectralMaskEnhancement.from_hparams(
source="speechbrain/metricgan-plus-voicebank", savedir="/tmp/pretrained_metricgan",
)
noisy = _metricgan_model.load_audio(input_path).unsqueeze(0)
enhanced = _metricgan_model.enhance_batch(noisy, lengths=torch.tensor([1.0]))
output_path = "/tmp/step3_isolated.wav"
torchaudio.save(output_path, enhanced.cpu(), 16000)
return output_path
except Exception as e:
print(f"โš ๏ธ ูุดู„ุช ุฎุทูˆุฉ ุนุฒู„ ุงู„ู…ุชุญุฏุซ: {e}")
return input_path
def preprocess_student_audio(raw_audio_path):
t0 = time.time()
step1 = normalize_audio_pydub(raw_audio_path)
print(f"โฑ๏ธ ุชุทุจูŠุน ุงู„ุตูˆุช (pydub): {time.time() - t0:.1f}s")
if SKIP_HEAVY_AUDIO_FILTERS:
return step1
# โš ๏ธ ุจู†ุงุกู‹ ุนู„ู‰ ุทู„ุจูƒ: ุชุฎุทู‘ูŠ DeepFilterNet ู†ู‡ุงุฆูŠุงู‹ ู…ู† ุงู„ู…ุณุงุฑ ุงู„ูุนู„ูŠ ุญุงู„ูŠุงู‹
# (ุงู„ุฏุงู„ุฉ denoise_with_deepfilternet ุชุจู‚ู‰ ููŠ ุงู„ูƒูˆุฏ ู„ุงุณุชุฎุฏุงู…ู‡ุง ู„ุงุญู‚ุงู‹
# ู„ูˆ ุงุญุชุฌุชู‡ุง ู…ุณุชู‚ุจู„ุงู‹ุŒ ูู‚ุท ู„ุง ุชูุณุชุฏุนู‰ ุงู„ุขู†) โ€” ูู‚ุท ูู„ุชุฑ ุงู„ุนุฒู„ (MetricGAN+).
t2 = time.time()
step3 = isolate_speaker_metricgan(step1)
print(f"โฑ๏ธ ุนุฒู„ ุงู„ู…ุชุญุฏุซ (MetricGAN+): {time.time() - t2:.1f}s")
return step3
_whisper_pipe = None
def load_whisper():
global _whisper_pipe
if _whisper_pipe is not None:
return _whisper_pipe
from transformers import pipeline
print("โณ ุชุญู…ูŠู„ Whisper-medium...")
_whisper_pipe = pipeline("automatic-speech-recognition", model=WHISPER_MODEL, device=DEVICE, chunk_length_s=30)
return _whisper_pipe
# โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•
# ุงู„ุฑุงุฏุงุฑ โ€” ุงู„ุตูˆุช ูู‚ุท ู…ู† ู‡ู†ุงุŒ ุฏุงุฆู…ุงู‹
# โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•
# โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•
# ๐Ÿงช Edge-TTS ู…ุญู„ูŠ ู…ุจุงุดุฑ ุฏุงุฎู„ ู‡ุฐู‡ ุงู„ู…ุณุงุญุฉ โ€” ุจุฏู„ ุงู„ุงุชุตุงู„ ุจุงู„ุฑุงุฏุงุฑุŒ ูƒุชุฌุฑุจุฉ
# ู„ุชุดุฎูŠุต/ุญู„ ู…ุดูƒู„ุฉ ุงู„ุชุนู„ูŠู‚ ุงู„ุทูˆูŠู„ (ูƒุงู†ุช ุชุธู‡ุฑ "processing 186s" ุจุฏูˆู† ู†ุชูŠุฌุฉ).
# ๐Ÿ”‡ ุงู„ุฑุงุฏุงุฑ ุญูุฐู ู†ู‡ุงุฆูŠุงู‹ ู…ู† ู‡ุฐุง ุงู„ุชุทุจูŠู‚ ุจู†ุงุกู‹ ุนู„ู‰ ุทู„ุจูƒ โ€” ู„ุง ูŠูˆุฌุฏ ุฃูŠ
# ุงุชุตุงู„ ุจู‡ ุนู„ู‰ ุงู„ุฅุทู„ุงู‚ ุจุนุฏ ุงู„ุขู†. ุงู„ุตูˆุช 100% ู…ู† Edge-TTS ุงู„ู…ุญู„ูŠ ูู‚ุท.
# โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•
# โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•
# ๐Ÿ”ง ุฅุตู„ุงุญ ุฌุฐุฑูŠ: ุญู„ู‚ุฉ asyncio ุงู„ุฏุงุฆู…ุฉ ููŠ ุฎูŠุท ู…ู†ูุตู„ (_start_edge_loop)
# ูƒุงู†ุช ุงู„ุณุจุจ ุงู„ูุนู„ูŠ ูˆุงู„ู…ุณุชู…ุฑ ู„ุฎุทุฃ "ValueError: Invalid file descriptor: -1"
# โ€” ูˆู‚ุฏ ุชุฃูƒุฏู†ุง ุฃู†ู‡ ุธู‡ุฑ ุญุชู‰ ููŠ ุงู„ู…ุณุงุญุฉ ุงู„ู‚ุฏูŠู…ุฉ (ุงู„ุฑุงุฏุงุฑ) ุจู†ูุณ ุงู„ู†ู…ุทุŒ ูู‡ูˆ
# ุนูŠุจ ููŠ ุงู„ู†ู…ุท ู†ูุณู‡ ู„ุง ููŠ ู…ุณุงุญุฉ ู…ุนูŠู‘ู†ุฉ. ุงู„ุญู„: asyncio.run() ู„ูƒู„ ุงุณุชุฏุนุงุก โ€”
# ุชู†ุดุฆ ุญู„ู‚ุฉ ุฌุฏูŠุฏุฉ ูˆุชูู†ุธู‘ูู‡ุง ุจุงู„ูƒุงู…ู„ (ุฅู„ุบุงุก ุงู„ู…ู‡ุงู… + ุฅุบู„ุงู‚ async generators
# + ุฅุบู„ุงู‚ ุงู„ุญู„ู‚ุฉ) ููŠ ูƒู„ ู…ุฑุฉุŒ ูู„ุง ูŠุจู‚ู‰ ุฃูŠ ูƒุงุฆู† ุญู„ู‚ุฉ ู…ุนู„ู‘ู‚ ูŠูุณุจู‘ุจ ู‡ุฐุง ุงู„ุฎุทุฃ
# ุนู†ุฏ ุฌู…ุน ุงู„ู‚ู…ุงู…ุฉ ู„ุงุญู‚ุงู‹.
# โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•
def call_edge_tts_local(text, voice):
filepath = f"/tmp/edge_{int(time.time() * 1000)}.mp3"
async def _task():
communicate = edge_tts.Communicate(text.strip(), voice)
await communicate.save(filepath)
try:
asyncio.run(_task())
return filepath
except Exception as e:
print(f"โš ๏ธ ุฎุทุฃ Edge-TTS ุงู„ู…ุญู„ูŠ: {e}")
return None
# โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•
# ุงู„ุฎุฒู†ุฉ: ุงู„ุฏุฑูˆุณ + ู…ู„ู ุงู„ุทุงู„ุจ
# โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•
def clean_subject_for_path(subject):
"""ูŠุญุฐู ุงู„ุฅูŠู…ูˆุฌูŠ ุงู„ุจุงุฏุฆ ู…ู† ุงุณู… ุงู„ู…ุงุฏุฉ (๐Ÿงฎ ุงู„ุฑูŠุงุถูŠุงุช โ†’ ุงู„ุฑูŠุงุถูŠุงุช) ู„ู…ุทุงุจู‚ุฉ
ุฃุณู…ุงุก ุงู„ู…ุฌู„ุฏุงุช ุงู„ุญู‚ูŠู‚ูŠุฉ ููŠ ุงู„ุฎุฒู†ุฉุŒ ุงู„ุชูŠ ู„ุง ุชุญุชูˆูŠ ุฅูŠู…ูˆุฌูŠ."""
if not subject:
return subject
parts = subject.strip().split(" ", 1)
return parts[-1].strip() if len(parts) > 1 else subject.strip()
def list_lessons_in_vault(grade, semester, subject):
try:
# โš ๏ธ ุงู„ุชุฑุชูŠุจ ุงู„ุตุญูŠุญ ุงู„ู…ุคูƒูŽู‘ุฏ ู…ู† ุงู„ุฎุฒู†ุฉ ุงู„ูุนู„ูŠุฉ: ุงู„ู…ุงุฏุฉ/ุงู„ูุตู„/ุงู„ุตู
clean_subject = clean_subject_for_path(subject)
folder_path = f"Curriculum_Core/{clean_subject}/{semester}/{grade}/"
all_files = _hf_api.list_repo_files(repo_id=VAULT_REPO_ID, repo_type="dataset")
lesson_files = [f for f in all_files if f.startswith(folder_path) and f.lower().endswith(('.txt', '.md', '.json'))]
choices = []
for f in sorted(lesson_files):
display_name = re.sub(r'\.(txt|md|json)$', '', os.path.basename(f), flags=re.IGNORECASE)
choices.append((display_name, f))
return choices
except Exception as e:
print(f"โš ๏ธ ูุดู„ ุณุฑุฏ ุงู„ุฏุฑูˆุณ: {e}")
return []
def load_lesson_content(lesson_path):
if not lesson_path:
return None
try:
local_path = hf_hub_download(repo_id=VAULT_REPO_ID, filename=lesson_path, repo_type="dataset")
with open(local_path, "r", encoding="utf-8") as f:
content = f.read()
return content[:MAX_LESSON_CHARS] if len(content) > MAX_LESSON_CHARS else content
except Exception as e:
print(f"โš ๏ธ ูุดู„ ุชุญู…ูŠู„ ุงู„ุฏุฑุณ: {e}")
return None
def save_student_profile(grade, semester, subject, lesson_path, user_name, track=None):
if not HF_TOKEN:
return
try:
profile = {
"user_name": user_name, "grade": grade, "semester": semester,
"subject": subject, "lesson": lesson_path, "track": track,
"last_updated": time.strftime("%Y-%m-%d %H:%M:%S")
}
_hf_api.upload_file(
path_or_fileobj=json.dumps(profile, ensure_ascii=False, indent=2).encode("utf-8"),
path_in_repo=f"Student_Hub/User_{user_name}/profile.json",
repo_id=VAULT_REPO_ID, repo_type="dataset", token=HF_TOKEN
)
except Exception as e:
print(f"โš ๏ธ ุฎุทุฃ ููŠ ุงู„ุญูุธ: {e}")
def upload_lesson_to_vault(file_obj, lesson_number, lesson_title, subject, semester, grade):
if not HF_TOKEN:
return "โš ๏ธ ู„ุง ูŠูˆุฌุฏ HF_TOKEN ููŠ ุฃุณุฑุงุฑ ู‡ุฐู‡ ุงู„ู…ุณุงุญุฉุŒ ู„ุง ูŠู…ูƒู† ุงู„ุฑูุน."
if file_obj is None:
return "โš ๏ธ ุงุฎุชุฑ ู…ู„ูุงู‹ ุฃูˆู„ุงู‹."
match = re.search(r'\d+', str(lesson_number or ""))
if not match:
return "โš ๏ธ ุฃุฏุฎู„ ุฑู‚ู… ุงู„ุฏุฑุณ (ู…ุซู„: 1)."
try:
clean_subject = clean_subject_for_path(subject)
num = int(match.group())
title = (lesson_title or "").strip() or "ุจุฏูˆู†_ุนู†ูˆุงู†"
ext = os.path.splitext(file_obj.name)[1] or ".txt"
path_in_repo = f"Curriculum_Core/{clean_subject}/{semester}/{grade}/{num:02d}_{title}{ext}"
_hf_api.upload_file(
path_or_fileobj=file_obj.name, path_in_repo=path_in_repo,
repo_id=VAULT_REPO_ID, repo_type="dataset", token=HF_TOKEN,
)
return f"โœ… ุชู… ุฑูุน ุงู„ุฏุฑุณ ุฅู„ู‰: {path_in_repo}"
except Exception as e:
return f"โš ๏ธ ูุดู„ ุงู„ุฑูุน: {e}"
# โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•
# ุงู„ุตููˆู/ุงู„ูุตูˆู„/ุงู„ู…ูˆุงุฏ
# โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•
GRADES = [
"ุงู„ุฑูˆุถุฉ", "ุงู„ุตู ุงู„ุฃูˆู„", "ุงู„ุตู ุงู„ุซุงู†ูŠ", "ุงู„ุตู ุงู„ุซุงู„ุซ", "ุงู„ุตู ุงู„ุฑุงุจุน",
"ุงู„ุตู ุงู„ุฎุงู…ุณ", "ุงู„ุตู ุงู„ุณุงุฏุณ", "ุงู„ุตู ุงู„ุณุงุจุน", "ุงู„ุตู ุงู„ุซุงู…ู†", "ุงู„ุตู ุงู„ุชุงุณุน",
"ุงู„ุตู ุงู„ุนุงุดุฑ", "ุงู„ุตู ุงู„ุญุงุฏูŠ ุนุดุฑ", "ุงู„ุตู ุงู„ุซุงู†ูŠ ุนุดุฑ (ุงู„ุชูˆุฌูŠู‡ูŠ)"
]
SEMESTERS = ["ุงู„ูุตู„ ุงู„ุฃูˆู„", "ุงู„ูุตู„ ุงู„ุซุงู†ูŠ"]
DEFAULT_SUBJECTS = ["๐Ÿงฎ ุงู„ุฑูŠุงุถูŠุงุช", "๐Ÿ”ฌ ุงู„ุนู„ูˆู…", "๐Ÿ“— ุงู„ู„ุบุฉ ุงู„ุนุฑุจูŠุฉ", "๐Ÿ•Œ ุงู„ุชุฑุจูŠุฉ ุงู„ุฅุณู„ุงู…ูŠุฉ", "๐Ÿ”ค ุงู„ู„ุบุฉ ุงู„ุฅู†ุฌู„ูŠุฒูŠุฉ"]
# โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•
# ุฏุงู„ุฉ ุงู„ู…ุญุงุฏุซุฉ ุงู„ุฑุฆูŠุณูŠุฉ ู„ุบุฑูุฉ ุงู„ุตู
# โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•
def teacher_chat(audio_mic, direct_text, subject, grade, semester, lesson_path, track, student_name,
chat_history_state, teacher_prefs, active_idx):
t_start = time.time()
if direct_text and direct_text.strip():
user_text = direct_text.strip()
else:
if audio_mic is None:
return chat_history_state, None, "๐ŸŽค ุณุฌู‘ู„ ุณุคุงู„ูƒ ุฃูˆ ุงุณุชุฎุฏู… ุฃุญุฏ ุฃุฏูˆุงุชูŠ"
try:
t0 = time.time()
processed_audio = preprocess_student_audio(audio_mic)
whisper = load_whisper()
result = whisper(processed_audio, generate_kwargs={"language": "arabic"})
user_text = result["text"].strip()
print(f"โฑ๏ธ ุงู„ู…ุฑุญู„ุฉ ุงู„ุตูˆุชูŠุฉ ูƒุงู…ู„ุฉ (ุชุทุจูŠุน+ูู„ุงุชุฑ+Whisper): {time.time() - t0:.1f}s")
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}")
lesson_content = load_lesson_content(lesson_path)
is_science = subject in ("๐Ÿงฎ ุงู„ุฑูŠุงุถูŠุงุช", "๐Ÿ”ฌ ุงู„ุนู„ูˆู…")
if is_science:
t0 = time.time()
phi_content = ask_science_teacher_via_controller(user_text, "teach", chat_history_state)
print(f"โฑ๏ธ ุงุณุชุฏุนุงุก Controller/Phi: {time.time() - t0:.1f}s")
t0 = time.time()
if phi_content:
cleaned_response, new_history = deliver_via_qwen(user_text, phi_content, chat_history_state)
else:
cleaned_response, new_history = ask_teacher(user_text, chat_history_state)
print(f"โฑ๏ธ ุชูˆู„ูŠุฏ ูƒูˆูŠู† (ู…ุณุงุฑ ุนู„ู…ูŠ): {time.time() - t0:.1f}s")
else:
t0 = time.time()
student_name_clean = (student_name or "ุงู„ุทุงู„ุจ").strip()
context_str = f"ู…ุงุฏุฉ {subject} - {semester}" + (f" - ุชุฎุตุต {track}" if track else "")
system_prompt = (
f"ุฃู†ุช ุงู„ุฃุณุชุงุฐ ุนุจูˆุฏุŒ ู…ุนู„ู… ุฎุจูŠุฑ ูˆุฏูˆุฏ ุฌุฏุงู‹. ุชุชุญุฏุซ ุงู„ุขู† ู…ุน ุทุงู„ุจูƒ {student_name_clean}. ุงู„ุณูŠุงู‚: {context_str}.\n"
f"- ุฎุงุทุจ {student_name_clean} ุจุงุณู…ู‡ ู…ู† ูˆู‚ุช ู„ุขุฎุฑ ุจุดูƒู„ ุทุจูŠุนูŠ.\n"
"- ุฅุฐุง ูƒุงู†ุช ุชุญูŠุฉุŒ ุฑุฏ ุจุชุญูŠุฉ ูˆุฏูˆุฏุฉ ู‚ุตูŠุฑุฉ ูู‚ุท.\n"
"- ุฅุฐุง ูƒุงู† ุณุคุงู„ุงู‹ ุชุนู„ูŠู…ูŠุงู‹: ุฃุฌุจ ุจุฅูŠุฌุงุฒ (30 ูƒู„ู…ุฉ)ุŒ ุฎุทูˆุงุชุŒ ุณุคุงู„ ุฎุชุงู…ูŠ.\n"
"- ุฅุฐุง ูˆุฑุฏุช ู…ุนุงุฏู„ุฉ LaTeX ุจูŠู† $$ุŒ ูุณู‘ุฑู‡ุง ุจูˆุถูˆุญ."
)
if lesson_content:
system_prompt += f"\n\n๐Ÿ“– ุงู„ุฏุฑุณ ุงู„ู…ุฑุฌุนูŠ (ู…ุณุงุนุฏ ูู‚ุท):\n```\n{lesson_content}\n```"
messages = [{"role": "system", "content": system_prompt}] + chat_history_state + [{"role": "user", "content": user_text}]
tokenizer, model = load_qwen_lazy()
text_prompt = tokenizer.apply_chat_template(messages, tokenize=False, add_generation_prompt=True)
inputs = tokenizer([text_prompt], return_tensors="pt").to(DEVICE)
with torch.no_grad():
out_ids = model.generate(**inputs, max_new_tokens=200, temperature=0.4, do_sample=True, repetition_penalty=1.15, pad_token_id=tokenizer.eos_token_id)
out_ids = [o[len(i):] for i, o in zip(inputs.input_ids, out_ids)]
cleaned_response = tokenizer.batch_decode(out_ids, skip_special_tokens=True)[0].strip()
gc.collect() # ุชุฎููŠู ุฐุฑูˆุฉ ุงู„ุฐุงูƒุฑุฉ ุจุนุฏ ูƒู„ ุชูˆู„ูŠุฏ
if not cleaned_response.endswith(('.', 'ุŸ', '!')):
cleaned_response += '.'
new_history = chat_history_state + [{"role": "user", "content": user_text}, {"role": "assistant", "content": cleaned_response}]
print(f"โฑ๏ธ ุชูˆู„ูŠุฏ ูƒูˆูŠู† (ู…ุณุงุฑ ุฃุฏุจูŠ/ู„ุบุงุช): {time.time() - t0:.1f}s")
save_student_profile(grade, semester, subject, lesson_path, student_name, track)
# ๐ŸŽ™๏ธ ุตูˆุช "ุงู„ู…ุญุงูˆุฑ" ู„ู„ู…ุญุงุฏุซุฉ ุงู„ุนุงุฏูŠุฉ ุญุงู„ูŠุงู‹ (ุฃุตูˆุงุช ุงู„ุดุฑุญ/ุงู„ุฃุณุฆู„ุฉ
# ุณุชููุนูŽู‘ู„ ุชู„ู‚ุงุฆูŠุงู‹ ุนู†ุฏ ุจู†ุงุก ู…ู†ุทู‚ ุงู„ุญุตุฉ ุงู„ู…ู‚ุณู‘ู…ุฉ ู„ุงุญู‚ุงู‹)
try:
voice = teacher_prefs[active_idx]["ู…ุญุงูˆุฑ"]
except Exception:
voice = "ar-JO-TaimNeural"
t0 = time.time()
audio_path = call_edge_tts_local(cleaned_response, voice) # ๐Ÿงช ู…ุญู„ูŠ ู…ุจุงุดุฑ ุจุฏู„ ุงู„ุฑุงุฏุงุฑ (ู„ู„ุชุฌุฑุจุฉ)
print(f"โฑ๏ธ ุชูˆู„ูŠุฏ ุงู„ุตูˆุช (Edge-TTS ู…ุญู„ูŠ): {time.time() - t0:.1f}s")
print(f"โฑ๏ธ ุงู„ูˆู‚ุช ุงู„ุฅุฌู…ุงู„ูŠ ู„ู„ุฏูˆุฑุฉ ูƒุงู…ู„ุฉ: {time.time() - t_start:.1f}s")
status = "โœ… ุชู… ุงู„ุฑุฏ" if audio_path else "โš ๏ธ ูุดู„ ุงู„ุตูˆุช"
return new_history, audio_path, status
# โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•
# ุฃุฏูˆุงุชูŠ: ุขู„ุฉ ุญุงุณุจุฉ + ูƒุงู…ูŠุฑุง OCR + ู„ูˆุญุฉ ู…ูุงุชูŠุญ ุจุฏูŠู„ุฉ
# โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•
SYMPY_LOCALS = {"sin": sp.sin, "cos": sp.cos, "tan": sp.tan, "sqrt": sp.sqrt, "pi": sp.pi}
CALC_BUTTONS = [
[("7", "7"), ("8", "8"), ("9", "9"), ("รท", "/"), ("โˆš", "sqrt(")],
[("4", "4"), ("5", "5"), ("6", "6"), ("ร—", "*"), ("^", "**")],
[("1", "1"), ("2", "2"), ("3", "3"), ("-", "-"), ("ฯ€", "pi")],
[("0", "0"), (".", "."), ("(", "("), (")", ")"), ("+", "+")],
[("sin", "sin("), ("cos", "cos("), ("tan", "tan("), ("C", "__CLEAR__"), ("โŒซ", "__BACK__")],
]
def calc_append(current, token):
return (current or "") + token
def calc_clear():
return ""
def calc_backspace(current):
return (current or "")[:-1]
def calc_evaluate(expr):
if not expr or not expr.strip():
return expr, "โš ๏ธ ุฃุฏุฎู„ ุนุจุงุฑุฉ ุฃูˆู„ุงู‹"
try:
return str(sp.sympify(expr, locals=SYMPY_LOCALS).evalf()), "โœ… ุชู… ุงู„ุญุณุงุจ"
except Exception as e:
return expr, f"โš ๏ธ ุชุนุฐุฑ ุงู„ุญุณุงุจ: {e}"
def calc_to_latex_question(expr):
if not expr or not expr.strip():
return ""
try:
return f"ุงุดุฑุญ ู„ูŠ ุฎุทูˆุงุช ุญู„ ู‡ุฐู‡ ุงู„ู…ุณุฃู„ุฉ: $$ {sp.latex(sp.sympify(expr, locals=SYMPY_LOCALS))} $$"
except Exception as e:
return f"ุงุดุฑุญ ู„ูŠ ุฎุทูˆุงุช ุญู„ ู‡ุฐู‡ ุงู„ู…ุณุฃู„ุฉ: {expr}"
_ocr_processor, _ocr_model = None, None
def load_ocr():
global _ocr_processor, _ocr_model
if _ocr_model is not None:
return _ocr_processor, _ocr_model
print("โณ ุชุญู…ูŠู„ ู†ู…ูˆุฐุฌ ู‚ุฑุงุกุฉ ุงู„ุตูˆุฑ ุงู„ุนุฑุจูŠ (TrOCR)...")
_ocr_processor = TrOCRProcessor.from_pretrained(OCR_MODEL_ID)
_ocr_model = VisionEncoderDecoderModel.from_pretrained(OCR_MODEL_ID)
return _ocr_processor, _ocr_model
def read_image_text(image_path):
if not image_path:
return "", "โš ๏ธ ู„ู… ูŠุชู… ุฑูุน ุตูˆุฑุฉ"
try:
processor, model = load_ocr()
image = Image.open(image_path).convert("RGB")
pixel_values = processor(images=image, return_tensors="pt").pixel_values
generated_ids = model.generate(pixel_values, max_new_tokens=200)
text = processor.batch_decode(generated_ids, skip_special_tokens=True)[0]
return text, "โœ… ุชู… ุงุณุชุฎุฑุงุฌ ุงู„ู†ุต"
except Exception as e:
return "", f"โš ๏ธ ุฎุทุฃ ููŠ ู‚ุฑุงุกุฉ ุงู„ุตูˆุฑุฉ: {e}"
# โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•
# ุงู„ูˆุงุฌู‡ุฉ
# โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•
with gr.Blocks(title="My Teacher Lesson", theme=gr.themes.Soft()) as demo:
theme_html = gr.HTML(apply_theme("๐ŸŒž ู†ู‡ุงุฑูŠ"))
gr.Markdown("# ๐ŸŽ“ ุงู„ู…ู†ุตุฉ ุงู„ุชุนู„ูŠู…ูŠุฉ ุงู„ุฐูƒูŠุฉ ุงู„ู…ูˆุญุฏุฉ")
teacher_prefs_state = gr.State([dict(t) for t in DEFAULT_TEACHER_PREFS])
active_teacher_index_state = gr.State(0)
chat_history_state = gr.State([])
subject_choices_state = gr.State(list(DEFAULT_SUBJECTS))
with gr.Tabs():
# โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
# ุชุจูˆูŠุจ 1: ุบุฑูุฉ ุงู„ุตู (ู…ุฏู…ุฌุฉ ุจุงู„ูƒุงู…ู„ ู…ู† ู…ู„ู Chat)
# โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
with gr.Tab("๐Ÿซ ุบุฑูุฉ ุงู„ุตู"):
with gr.Row():
with gr.Column(scale=2):
with gr.Row():
student_name_input = gr.Textbox(label="๐Ÿ‘ค ุงุณู… ุงู„ุทุงู„ุจ", value="Divid")
grade_dropdown = gr.Dropdown(choices=GRADES, value="ุงู„ุตู ุงู„ุนุงุดุฑ", label="๐Ÿ“† ุงู„ุตู")
with gr.Row():
semester_dropdown = gr.Dropdown(choices=SEMESTERS, value="ุงู„ูุตู„ ุงู„ุฃูˆู„", label="๐Ÿ“… ุงู„ูุตู„")
track_dropdown = gr.Dropdown(choices=[], value=None, label="๐ŸŽฏ ุงู„ุชุฎุตุต (ุชูˆุฌูŠู‡ูŠ)", visible=False, allow_custom_value=True)
with gr.Row(visible=False) as track_row:
new_track_input = gr.Textbox(label="โž• ุชุฎุตุต ุฌุฏูŠุฏ")
add_track_btn = gr.Button("๐Ÿ’พ ุญูุธ", size="sm")
with gr.Row():
subject_dropdown = gr.Dropdown(choices=DEFAULT_SUBJECTS, value=DEFAULT_SUBJECTS[0], label="๐Ÿ“š ุงู„ู…ุงุฏุฉ")
add_subject_btn = gr.Button("โž• ุฅุถุงูุฉ ู…ูˆุงุฏ ุฃุฎุฑู‰", size="sm")
with gr.Row(visible=False) as new_subject_row:
new_subject_input = gr.Textbox(label="โœ๏ธ ุงุณู… ุงู„ู…ุงุฏุฉ ุงู„ุฌุฏูŠุฏุฉ")
confirm_subject_btn = gr.Button("๐Ÿ’พ ุญูุธ", size="sm")
with gr.Row():
lesson_dropdown = gr.Dropdown(choices=[], label="๐Ÿ“– ุงู„ุฏุฑุณ (ู…ู† ุงู„ุฎุฒู†ุฉ)")
refresh_lessons_btn = gr.Button("๐Ÿ”„", size="sm")
with gr.Accordion("๐Ÿ“ค ุฑูุน ุฏุฑุณ ุฌุฏูŠุฏ ุฅู„ู‰ ุงู„ุฎุฒู†ุฉ", open=False):
upload_lesson_number = gr.Textbox(label="ุฑู‚ู… ุงู„ุฏุฑุณ", placeholder="ู…ุซุงู„: 1")
upload_lesson_title = gr.Textbox(label="ุนู†ูˆุงู† ุงู„ุฏุฑุณ", placeholder="ู…ุซุงู„: ุงู„ู‚ูŠู…ุฉ ุงู„ู…ู†ุฒู„ูŠุฉ ู„ู„ุฑู‚ู…")
upload_lesson_file = gr.File(label="ุงุฎุชุฑ ู…ู„ู ุงู„ุฏุฑุณ ู…ู† ุฌู‡ุงุฒูƒ")
upload_lesson_btn = gr.Button("๐Ÿ’พ ุญูุธ ููŠ ุงู„ุฎุฒู†ุฉ", variant="primary")
upload_lesson_status = gr.Textbox(label="ุญุงู„ุฉ ุงู„ุฑูุน", 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.Accordion("๐Ÿ› ๏ธ ุฃุฏูˆุงุชูŠ", open=False):
with gr.Tab("๐Ÿงฎ ุงู„ุขู„ุฉ ุงู„ุญุงุณุจุฉ ุงู„ุนู„ู…ูŠุฉ"):
calc_display = gr.Textbox(label="ุงู„ุนุจุงุฑุฉ", value="")
for row in CALC_BUTTONS:
with gr.Row():
for label, token in row:
b = gr.Button(label, size="sm")
if token == "__CLEAR__":
b.click(fn=calc_clear, outputs=[calc_display], api_name=False)
elif token == "__BACK__":
b.click(fn=calc_backspace, inputs=[calc_display], outputs=[calc_display], api_name=False)
else:
b.click(fn=lambda cur, t=token: calc_append(cur, t), inputs=[calc_display], outputs=[calc_display], api_name=False)
with gr.Row():
calc_eq_btn = gr.Button("๐ŸŸฐ ุญุณุงุจ", size="sm")
calc_send_btn = gr.Button("๐Ÿ“ค ุญูˆู‘ู„ ู„ู€ LaTeX ูˆุฃุฑุณู„", size="sm")
calc_status = gr.Textbox(label="ุงู„ุญุงู„ุฉ", interactive=False)
calc_pending_question = gr.Textbox(visible=False)
calc_eq_btn.click(fn=calc_evaluate, inputs=[calc_display], outputs=[calc_display, calc_status], api_name=False)
with gr.Tab("๐Ÿ“ท ุงู„ูƒุงู…ูŠุฑุง ูˆู‚ุฑุงุกุฉ ุงู„ุตูˆุฑ"):
camera_input = gr.Image(label="ุตูˆู‘ุฑ/ุงุฑูุน ูˆุฑู‚ุฉ ุงู„ุนู…ู„", type="filepath", sources=["upload", "webcam"])
ocr_btn = gr.Button("๐Ÿ” ุงู‚ุฑุฃ ุงู„ู†ุต")
ocr_text_out = gr.Textbox(label="ุงู„ู†ุต ุงู„ู…ุณุชุฎุฑุฌ", lines=3)
ocr_status = gr.Textbox(label="ุงู„ุญุงู„ุฉ", interactive=False)
ocr_send_btn = gr.Button("โžก๏ธ ุฃุฑุณู„ ู„ู„ุฃุณุชุงุฐ", variant="primary")
ocr_btn.click(fn=read_image_text, inputs=[camera_input], outputs=[ocr_text_out, ocr_status], api_name=False)
with gr.Tab("โŒจ๏ธ ู„ูˆุญุฉ ุงู„ู…ูุงุชูŠุญ ุงู„ุจุฏูŠู„ุฉ"):
emergency_text_in = gr.Textbox(label="ุงูƒุชุจ ุณุคุงู„ูƒ ู‡ู†ุง", lines=3)
emergency_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="ุงู„ุญุงู„ุฉ")
# โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
# ุชุจูˆูŠุจ 2: ุงู„ุฅุนุฏุงุฏุงุช ูˆุงู„ุชูุถูŠู„ุงุช
# โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
with gr.Tab("โš™๏ธ ุงู„ุฅุนุฏุงุฏุงุช ูˆุงู„ุชูุถูŠู„ุงุช"):
gr.Markdown("### ๐ŸŽจ ู…ุธู‡ุฑ ุงู„ู…ู†ุตุฉ")
theme_radio = gr.Radio(choices=list(THEME_CSS.keys()), value="๐ŸŒž ู†ู‡ุงุฑูŠ", label="ุงุฎุชุฑ ุงู„ู…ุธู‡ุฑ")
gr.Markdown("---\n### ๐Ÿ‘จโ€๐Ÿซ ุนุฏุฏ ุงู„ู…ุนู„ู…ูŠู† ูˆุฃุตูˆุงุช ุงู„ุฃุฏูˆุงุฑ")
teacher_count_radio = gr.Radio(choices=["2", "3"], value="2", label="ุนุฏุฏ ุงู„ู…ุนู„ู…ูŠู† ุงู„ู…ุชุงุญูŠู†")
active_teacher_radio = gr.Radio(choices=["ุงู„ู…ุนู„ู… ุงู„ุฃูˆู„", "ุงู„ู…ุนู„ู…ุฉ ุงู„ุซุงู†ูŠุฉ", "ุงู„ู…ุนู„ู… ุงู„ุซุงู„ุซ"], value="ุงู„ู…ุนู„ู… ุงู„ุฃูˆู„", label="ุงู„ู…ุนู„ู… ุงู„ู†ุดุท ุญุงู„ูŠุงู‹ ููŠ ุบุฑูุฉ ุงู„ุตู")
teacher_setting_rows = []
for idx in range(3):
with gr.Row(visible=(idx < 2)) as row:
name_box = gr.Textbox(label=f"ุงุณู… ุงู„ู…ุนู„ู… {idx + 1}", value=DEFAULT_TEACHER_PREFS[idx]["name"])
voice_dd_map = {}
for role in VOICE_ROLES:
voice_dd_map[role] = gr.Dropdown(choices=MICROSOFT_VOICES, value=DEFAULT_TEACHER_PREFS[idx][role], label=f"ุตูˆุช {role}")
teacher_setting_rows.append((row, name_box, voice_dd_map))
save_settings_btn = gr.Button("๐Ÿ’พ ุญูุธ ุงู„ุชูุถูŠู„ุงุช", variant="primary")
settings_status = gr.Textbox(label="ุญุงู„ุฉ ุงู„ุญูุธ", interactive=False)
def toggle_teacher_count(count):
n = int(count)
return [gr.update(visible=(i < n)) for i in range(3)]
teacher_count_radio.change(
fn=toggle_teacher_count, inputs=[teacher_count_radio],
outputs=[r[0] for r in teacher_setting_rows], api_name=False
)
def save_all_settings(*args):
# args = name1, v1_ู…ุญุงูˆุฑ, v1_ุดุฑุญ, v1_ุฃุณุฆู„ุฉ, name2, ..., name3, ...
prefs = []
i = 0
names_voices = list(args)
for t_idx in range(3):
name = names_voices[i]; i += 1
roles = {}
for role in VOICE_ROLES:
roles[role] = names_voices[i]; i += 1
prefs.append({"name": name, **roles})
return prefs, "โœ… ุชู… ุญูุธ ุงู„ุชูุถูŠู„ุงุช"
all_setting_inputs = []
for row, name_box, voice_dd_map in teacher_setting_rows:
all_setting_inputs.append(name_box)
for role in VOICE_ROLES:
all_setting_inputs.append(voice_dd_map[role])
save_settings_btn.click(
fn=save_all_settings, inputs=all_setting_inputs,
outputs=[teacher_prefs_state, settings_status], api_name=False
)
def set_active_teacher(choice):
mapping = {"ุงู„ู…ุนู„ู… ุงู„ุฃูˆู„": 0, "ุงู„ู…ุนู„ู…ุฉ ุงู„ุซุงู†ูŠุฉ": 1, "ุงู„ู…ุนู„ู… ุงู„ุซุงู„ุซ": 2}
return mapping.get(choice, 0)
active_teacher_radio.change(fn=set_active_teacher, inputs=[active_teacher_radio], outputs=[active_teacher_index_state], api_name=False)
theme_radio.change(fn=apply_theme, inputs=[theme_radio], outputs=[theme_html], api_name=False)
# โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
# ุงู„ุชุจูˆูŠุจุงุช 3-6: ุญุฌุฒ ู…ูƒุงู† โ€” ุณู†ุจู†ูŠู‡ุง ูˆุงุญุฏุฉ ุชู„ูˆ ุงู„ุฃุฎุฑู‰
# โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
with gr.Tab("๐Ÿ—‚๏ธ ุฎุทุท ุงู„ุฏุฑูˆุณ ูˆุงู„ุชู‚ุณูŠู…"):
gr.Markdown("๐Ÿšง **ู‚ูŠุฏ ุงู„ุชุทูˆูŠุฑ.** ุณูŠูุจู†ู‰ ู„ุงุญู‚ุงู‹ ุจุงู„ุชุนุงูˆู† ู…ุน ูƒูˆูŠู† ูˆPhi.")
with gr.Tab("๐Ÿ“Š ุงู„ู…ุณุชูˆูŠุงุช ูˆุชุญู„ูŠู„ ุงู„ูุฌูˆุฉ ุงู„ุชุนู„ูŠู…ูŠุฉ"):
gr.Markdown("๐Ÿšง **ู‚ูŠุฏ ุงู„ุชุทูˆูŠุฑ.**")
with gr.Tab("๐Ÿ’ฌ ุนู„ุงู‚ุฉ ุงู„ุทุงู„ุจ ูˆุงู„ุฃุณุชุงุฐ"):
gr.Markdown("๐Ÿšง **ู‚ูŠุฏ ุงู„ุชุทูˆูŠุฑ.**")
with gr.Tab("๐Ÿ–ผ๏ธ ุฃุฑุดูŠู ูˆุฅุฏุงุฑุฉ ุงู„ูˆุณุงุฆุท"):
gr.Markdown("๐Ÿšง **ู‚ูŠุฏ ุงู„ุชุทูˆูŠุฑ.**")
# โ”€โ”€โ”€โ”€ ุฑุจุท ุงู„ุฃุญุฏุงุซ: ุบุฑูุฉ ุงู„ุตู โ”€โ”€โ”€โ”€
def toggle_track(grade):
if "ุงู„ุชูˆุฌูŠู‡ูŠ" in grade:
return gr.update(visible=True), gr.update(visible=True), gr.update(visible=True)
return gr.update(visible=False, value=None), gr.update(visible=False), gr.update(visible=False)
grade_dropdown.change(fn=toggle_track, inputs=[grade_dropdown], outputs=[track_dropdown, track_row, add_track_btn], api_name=False)
add_subject_btn.click(fn=lambda: gr.update(visible=True), outputs=[new_subject_row], api_name=False)
def confirm_new_subject(new_subject, choices):
new_subject = (new_subject or "").strip()
if not new_subject:
return gr.update(), choices, gr.update(visible=False), ""
if new_subject not in choices:
choices = choices + [new_subject]
return gr.update(choices=choices, value=new_subject), choices, gr.update(visible=False), ""
confirm_subject_btn.click(fn=confirm_new_subject, inputs=[new_subject_input, subject_choices_state],
outputs=[subject_dropdown, subject_choices_state, new_subject_row, new_subject_input], api_name=False)
def refresh_lessons(grade, semester, subject):
lessons = list_lessons_in_vault(grade, semester, subject)
return gr.update(choices=lessons, value=(lessons[0][1] if lessons else None))
grade_dropdown.change(fn=refresh_lessons, inputs=[grade_dropdown, semester_dropdown, subject_dropdown], outputs=[lesson_dropdown], api_name=False)
semester_dropdown.change(fn=refresh_lessons, inputs=[grade_dropdown, semester_dropdown, subject_dropdown], outputs=[lesson_dropdown], api_name=False)
subject_dropdown.change(fn=refresh_lessons, inputs=[grade_dropdown, semester_dropdown, subject_dropdown], outputs=[lesson_dropdown], api_name=False)
refresh_lessons_btn.click(fn=refresh_lessons, inputs=[grade_dropdown, semester_dropdown, subject_dropdown], outputs=[lesson_dropdown], api_name=False)
demo.load(fn=refresh_lessons, inputs=[grade_dropdown, semester_dropdown, subject_dropdown], outputs=[lesson_dropdown])
upload_lesson_btn.click(
fn=upload_lesson_to_vault,
inputs=[upload_lesson_file, upload_lesson_number, upload_lesson_title, subject_dropdown, semester_dropdown, grade_dropdown],
outputs=[upload_lesson_status],
api_name=False,
).then(
fn=refresh_lessons, inputs=[grade_dropdown, semester_dropdown, subject_dropdown], outputs=[lesson_dropdown], api_name=False
)
def sanitize_history_for_chatbot(history):
"""๐Ÿ›ก๏ธ ุชู†ุธูŠู ุฏูุงุนูŠ ู†ู‡ุงุฆูŠ ูˆุตุงุฑู…: ุฃูŠุงู‹ ูƒุงู† ู…ุตุฏุฑ ุงู„ุนุทู„ (PhiุŒ ูƒูˆูŠู†ุŒ
ุชุณู„ุณู„ ุบูŠุฑ ู…ุชูˆู‚ุน)ุŒ ู‡ุฐุง ูŠุถู…ู† ุฃู† ูƒู„ ุนู†ุตุฑ ูŠุตู„ ู„ู€ Chatbot ู‡ูˆ dict
ู†ุธูŠู 100% ุจู…ูุชุงุญูŠ role/content ูู‚ุท ูˆู‚ูŠู… ู†ุตูŠุฉ ุตุงููŠุฉ โ€” ูŠู…ู†ุน ุชูƒุฑุงุฑ
ุฎุทุฃ 'Data incompatible with messages format' ู†ู‡ุงุฆูŠุงู‹."""
clean = []
for msg in (history or []):
if isinstance(msg, dict) and "role" in msg and "content" in msg:
role = str(msg["role"]).strip()
content = str(msg["content"]).strip()
if role and content:
clean.append({"role": role, "content": content})
return clean
def process_and_display(audio_mic, direct_text, subject, grade, semester, lesson_path, track, student_name,
history_state, teacher_prefs, active_idx):
new_history, audio, status = teacher_chat(audio_mic, direct_text, subject, grade, semester, lesson_path,
track, student_name, history_state, teacher_prefs, active_idx)
clean_history = sanitize_history_for_chatbot(new_history)
return clean_history, clean_history, audio, status, None, ""
common_inputs_tail = [subject_dropdown, grade_dropdown, semester_dropdown, lesson_dropdown, track_dropdown,
student_name_input, chat_history_state, teacher_prefs_state, active_teacher_index_state]
send_btn.click(
fn=process_and_display,
inputs=[mic_input, calc_pending_question] + common_inputs_tail,
outputs=[chat_history_state, chatbot_display, audio_output, status_output, mic_input, calc_pending_question],
api_name=False,
)
calc_send_btn.click(fn=calc_to_latex_question, inputs=[calc_display], outputs=[calc_pending_question], api_name=False).then(
fn=process_and_display,
inputs=[mic_input, calc_pending_question] + common_inputs_tail,
outputs=[chat_history_state, chatbot_display, audio_output, status_output, mic_input, calc_pending_question],
api_name=False,
)
ocr_send_btn.click(
fn=process_and_display,
inputs=[mic_input, ocr_text_out] + common_inputs_tail,
outputs=[chat_history_state, chatbot_display, audio_output, status_output, mic_input, ocr_text_out],
api_name=False,
)
emergency_send_btn.click(
fn=process_and_display,
inputs=[mic_input, emergency_text_in] + common_inputs_tail,
outputs=[chat_history_state, chatbot_display, audio_output, status_output, mic_input, emergency_text_in],
api_name=False,
)
if __name__ == "__main__":
demo.launch(server_name="0.0.0.0", server_port=7860)