Spaces:
Running
Running
shahad altamimi Claude Opus 4.8 commited on
Commit ·
90badd7
1
Parent(s): 49f9fdf
تعديل أسماء وترتيب النماذج، حذف Reranking، إصلاح إعادة تعيين التقييم
Browse files- reverse-dictionary/backend/app/database.py +2 -6
- reverse-dictionary/backend/app/models_loader.py +0 -17
- reverse-dictionary/backend/app/reranker.py +0 -49
- reverse-dictionary/backend/app/search.py +4 -27
- reverse-dictionary/backend/config.py +3 -13
- reverse-dictionary/frontend/streamlit_app.py +16 -19
- reverse-dictionary/supabase_schema.sql +0 -1
reverse-dictionary/backend/app/database.py
CHANGED
|
@@ -23,7 +23,7 @@ from pathlib import Path
|
|
| 23 |
import pandas as pd
|
| 24 |
import streamlit as st
|
| 25 |
from sqlalchemy import (
|
| 26 |
-
|
| 27 |
create_engine, delete, func, insert, select, update,
|
| 28 |
)
|
| 29 |
|
|
@@ -51,7 +51,6 @@ ratings = Table(
|
|
| 51 |
Column("word", Text),
|
| 52 |
Column("similarity", Float),
|
| 53 |
Column("verdict", Text), # "صح" أو "خطأ"
|
| 54 |
-
Column("rerank", Boolean), # هل كان Reranking مفعّلًا؟
|
| 55 |
Column("evaluator_name", Text),
|
| 56 |
Column("evaluator_email", Text),
|
| 57 |
)
|
|
@@ -66,7 +65,6 @@ _AR_COLUMNS = {
|
|
| 66 |
"verdict": "التقييم",
|
| 67 |
"evaluator_name": "المقيّم",
|
| 68 |
"evaluator_email": "الإيميل",
|
| 69 |
-
"rerank": "إعادة الترتيب",
|
| 70 |
}
|
| 71 |
|
| 72 |
|
|
@@ -154,8 +152,7 @@ def upsert_evaluator(name: str, email: str) -> None:
|
|
| 154 |
# 💾 حفظ تقييم استعلام كامل (كل النماذج وكلماتها دفعة واحدة)
|
| 155 |
# ------------------------------------------------------------
|
| 156 |
def save_evaluation(query: str, per_model: list[dict],
|
| 157 |
-
evaluator_name: str, evaluator_email: str
|
| 158 |
-
rerank: bool) -> int:
|
| 159 |
"""per_model = [{"model": str, "items": [{"lemma","score","chosen"}, ...]}, ...].
|
| 160 |
يرجّع عدد الصفوف المحفوظة."""
|
| 161 |
ts = datetime.now() # وقت واحد لكل صفوف هذا الحفظ (لتماسك التجميع)
|
|
@@ -169,7 +166,6 @@ def save_evaluation(query: str, per_model: list[dict],
|
|
| 169 |
"word": item["lemma"],
|
| 170 |
"similarity": round(float(item["score"]), 3),
|
| 171 |
"verdict": "صح" if item["chosen"] else "خطأ",
|
| 172 |
-
"rerank": bool(rerank),
|
| 173 |
"evaluator_name": evaluator_name,
|
| 174 |
"evaluator_email": evaluator_email,
|
| 175 |
})
|
|
|
|
| 23 |
import pandas as pd
|
| 24 |
import streamlit as st
|
| 25 |
from sqlalchemy import (
|
| 26 |
+
Column, DateTime, Float, Integer, MetaData, Table, Text,
|
| 27 |
create_engine, delete, func, insert, select, update,
|
| 28 |
)
|
| 29 |
|
|
|
|
| 51 |
Column("word", Text),
|
| 52 |
Column("similarity", Float),
|
| 53 |
Column("verdict", Text), # "صح" أو "خطأ"
|
|
|
|
| 54 |
Column("evaluator_name", Text),
|
| 55 |
Column("evaluator_email", Text),
|
| 56 |
)
|
|
|
|
| 65 |
"verdict": "التقييم",
|
| 66 |
"evaluator_name": "المقيّم",
|
| 67 |
"evaluator_email": "الإيميل",
|
|
|
|
| 68 |
}
|
| 69 |
|
| 70 |
|
|
|
|
| 152 |
# 💾 حفظ تقييم استعلام كامل (كل النماذج وكلماتها دفعة واحدة)
|
| 153 |
# ------------------------------------------------------------
|
| 154 |
def save_evaluation(query: str, per_model: list[dict],
|
| 155 |
+
evaluator_name: str, evaluator_email: str) -> int:
|
|
|
|
| 156 |
"""per_model = [{"model": str, "items": [{"lemma","score","chosen"}, ...]}, ...].
|
| 157 |
يرجّع عدد الصفوف المحفوظة."""
|
| 158 |
ts = datetime.now() # وقت واحد لكل صفوف هذا الحفظ (لتماسك التجميع)
|
|
|
|
| 166 |
"word": item["lemma"],
|
| 167 |
"similarity": round(float(item["score"]), 3),
|
| 168 |
"verdict": "صح" if item["chosen"] else "خطأ",
|
|
|
|
| 169 |
"evaluator_name": evaluator_name,
|
| 170 |
"evaluator_email": evaluator_email,
|
| 171 |
})
|
reverse-dictionary/backend/app/models_loader.py
CHANGED
|
@@ -26,7 +26,6 @@ class ResourceManager:
|
|
| 26 |
self.df: pd.DataFrame | None = None
|
| 27 |
self.embeddings: dict[str, np.ndarray] = {} # model_key -> مصفوفة embeddings
|
| 28 |
self.models: dict[str, SentenceTransformer] = {} # model_key -> النموذج المحمّل
|
| 29 |
-
self.reranker = None # نموذج الـ Cross-Encoder (يُحمّل كسولًا عند أول استخدام)
|
| 30 |
|
| 31 |
# --------------------------------------------------------
|
| 32 |
# 📂 تحميل البيانات المنظّفة
|
|
@@ -67,22 +66,6 @@ class ResourceManager:
|
|
| 67 |
print(f"✅ تم تحميل: {model_key}")
|
| 68 |
return self.models[model_key]
|
| 69 |
|
| 70 |
-
# --------------------------------------------------------
|
| 71 |
-
# 🔁 تحميل نموذج إعادة الترتيب (Cross-Encoder) كسولًا
|
| 72 |
-
# --------------------------------------------------------
|
| 73 |
-
def get_reranker(self):
|
| 74 |
-
"""يحمّل الـ reranker مرة واحدة عند أول بحث بـ rerank=True.
|
| 75 |
-
|
| 76 |
-
لا نضمّه في warmup لأنه اختياري وثقيل؛ فنؤجّل تحميله حتى
|
| 77 |
-
يُطلب فعلًا (قد يأخذ أول طلب reranking بعض الوقت).
|
| 78 |
-
"""
|
| 79 |
-
if self.reranker is None:
|
| 80 |
-
from app.reranker import load_reranker
|
| 81 |
-
print("⏳ تحميل نموذج إعادة الترتيب (Reranker) ...")
|
| 82 |
-
self.reranker = load_reranker()
|
| 83 |
-
print("✅ تم تحميل الـ Reranker")
|
| 84 |
-
return self.reranker
|
| 85 |
-
|
| 86 |
# --------------------------------------------------------
|
| 87 |
# 🔥 تسخين: تحميل كل النماذج والـ embeddings مسبقًا
|
| 88 |
# --------------------------------------------------------
|
|
|
|
| 26 |
self.df: pd.DataFrame | None = None
|
| 27 |
self.embeddings: dict[str, np.ndarray] = {} # model_key -> مصفوفة embeddings
|
| 28 |
self.models: dict[str, SentenceTransformer] = {} # model_key -> النموذج المحمّل
|
|
|
|
| 29 |
|
| 30 |
# --------------------------------------------------------
|
| 31 |
# 📂 تحميل البيانات المنظّفة
|
|
|
|
| 66 |
print(f"✅ تم تحميل: {model_key}")
|
| 67 |
return self.models[model_key]
|
| 68 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 69 |
# --------------------------------------------------------
|
| 70 |
# 🔥 تسخين: تحميل كل النماذج والـ embeddings مسبقًا
|
| 71 |
# --------------------------------------------------------
|
reverse-dictionary/backend/app/reranker.py
DELETED
|
@@ -1,49 +0,0 @@
|
|
| 1 |
-
# ============================================================
|
| 2 |
-
# 📄 الملف: app/reranker.py
|
| 3 |
-
# 🎯 الغرض: مرحلة إعادة الترتيب (Reranking) باستخدام Cross-Encoder.
|
| 4 |
-
#
|
| 5 |
-
# البحث الدلالي (search.py) يجيب أفضل N مرشّح بسرعة عبر تشابه
|
| 6 |
-
# الجيب التمام (bi-encoder + cosine). لكن الـ bi-encoder يرمّز
|
| 7 |
-
# الاستعلام والتعريف كلٌّ على حدة، فلا "يقارنهما مباشرة".
|
| 8 |
-
#
|
| 9 |
-
# الـ Cross-Encoder أدق: يأخذ الزوج (الاستعلام، التعريف) معًا
|
| 10 |
-
# ويعطي درجة تطابق واحدة. نستخدمه لإعادة ترتيب المرشّحين القلائل
|
| 11 |
-
# (مثلًا 40) بدل تطبيقه على كل البيانات (لأنه أبطأ بكثير).
|
| 12 |
-
#
|
| 13 |
-
# النموذج المستخدم: BAAI/bge-reranker-v2-m3 (config.RERANKER_MODEL).
|
| 14 |
-
# يُحمّل كسولًا داخل ResourceManager (models_loader.py).
|
| 15 |
-
# ============================================================
|
| 16 |
-
|
| 17 |
-
from sentence_transformers import CrossEncoder
|
| 18 |
-
|
| 19 |
-
import config
|
| 20 |
-
|
| 21 |
-
|
| 22 |
-
def load_reranker() -> CrossEncoder:
|
| 23 |
-
"""يحمّل نموذج الـ Cross-Encoder (يُستدعى مرة واحدة كسولًا)."""
|
| 24 |
-
return CrossEncoder(config.RERANKER_MODEL, device=config.DEVICE)
|
| 25 |
-
|
| 26 |
-
|
| 27 |
-
def rerank(model: CrossEncoder, query: str, candidates: list[dict]) -> list[dict]:
|
| 28 |
-
"""
|
| 29 |
-
يعيد ترتيب قائمة المرشّحين تنازليًا حسب درجة (الاستعلام، التعريف).
|
| 30 |
-
|
| 31 |
-
المعاملات:
|
| 32 |
-
model : نموذج الـ Cross-Encoder المحمّل.
|
| 33 |
-
query : نص التعريف المُدخل من المستخدم.
|
| 34 |
-
candidates : قائمة نتائج (كل عنصر فيه "definition") من المرحلة الأولى.
|
| 35 |
-
|
| 36 |
-
يرجّع نفس العناصر لكن مرتّبة حسب درجة إعادة الترتيب (الأعلى أولًا).
|
| 37 |
-
"""
|
| 38 |
-
if not candidates:
|
| 39 |
-
return candidates
|
| 40 |
-
|
| 41 |
-
# نكوّن أزواج (الاستعلام، التعريف) — وهي ما يقارنه الـ Cross-Encoder
|
| 42 |
-
pairs = [[query, c.get("definition") or c.get("lemma") or ""] for c in candidates]
|
| 43 |
-
|
| 44 |
-
# درجة لكل زوج (كلما زادت، زاد التطابق)
|
| 45 |
-
scores = model.predict(pairs)
|
| 46 |
-
|
| 47 |
-
# ترتيب الفهارس تنازليًا حسب الدرجة
|
| 48 |
-
order = sorted(range(len(candidates)), key=lambda i: float(scores[i]), reverse=True)
|
| 49 |
-
return [candidates[i] for i in order]
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
reverse-dictionary/backend/app/search.py
CHANGED
|
@@ -16,7 +16,6 @@ import pandas as pd
|
|
| 16 |
from sklearn.metrics.pairwise import cosine_similarity
|
| 17 |
|
| 18 |
import config
|
| 19 |
-
from app import reranker
|
| 20 |
from app.models_loader import resources
|
| 21 |
|
| 22 |
|
|
@@ -40,7 +39,7 @@ def _build_item(row, score: float) -> dict:
|
|
| 40 |
|
| 41 |
|
| 42 |
def search_one_model(query: str, model_cfg: dict, threshold: float,
|
| 43 |
-
top_k: int | None = None
|
| 44 |
"""
|
| 45 |
يبحث في نموذج واحد ويرجّع قائمة نتائج مرتّبة تنازليًا حسب التشابه.
|
| 46 |
|
|
@@ -50,8 +49,6 @@ def search_one_model(query: str, model_cfg: dict, threshold: float,
|
|
| 50 |
threshold : أقل قيمة تشابه مقبولة.
|
| 51 |
top_k : حدّ أقصى للنتائج يتجاوز قيمة config (اختياري). إن لم
|
| 52 |
يُمرَّر، يُستخدم model_cfg["top_k"].
|
| 53 |
-
rerank : لو True نطبّق مرحلة ثانية تعيد ترتيب أفضل المرشّحين
|
| 54 |
-
عبر Cross-Encoder. لو False (الافتراضي) السلوك كما هو.
|
| 55 |
"""
|
| 56 |
df = resources.get_dataframe()
|
| 57 |
doc_embeds = resources.get_embeddings(model_cfg["key"])
|
|
@@ -70,27 +67,7 @@ def search_one_model(query: str, model_cfg: dict, threshold: float,
|
|
| 70 |
# top_k الممرّر يتجاوز قيمة config (وإلا نستخدم قيمة النموذج)
|
| 71 |
top_k = top_k if top_k is not None else model_cfg["top_k"]
|
| 72 |
|
| 73 |
-
#
|
| 74 |
-
# 🔁 مع إعادة الترتيب: مرحلتان
|
| 75 |
-
# (1) نجمع أفضل RERANK_CANDIDATES مرشّح بالكوساين.
|
| 76 |
-
# (2) نعيد ترتيبهم بالـ Cross-Encoder، ثم نطبّق top_k.
|
| 77 |
-
# ====================================================
|
| 78 |
-
if rerank:
|
| 79 |
-
candidates = []
|
| 80 |
-
for i in idx_sorted:
|
| 81 |
-
score = float(sims[i])
|
| 82 |
-
if score < threshold:
|
| 83 |
-
break # القائمة مرتّبة تنازليًا
|
| 84 |
-
candidates.append(_build_item(df.iloc[i], score))
|
| 85 |
-
if len(candidates) >= config.RERANK_CANDIDATES:
|
| 86 |
-
break
|
| 87 |
-
|
| 88 |
-
reranked = reranker.rerank(resources.get_reranker(), query, candidates)
|
| 89 |
-
return reranked[:top_k] if top_k is not None else reranked
|
| 90 |
-
|
| 91 |
-
# ====================================================
|
| 92 |
-
# بدون إعادة الترتيب: السلوك الأصلي تمامًا (كوساين + threshold + top_k)
|
| 93 |
-
# ====================================================
|
| 94 |
results = []
|
| 95 |
for i in idx_sorted:
|
| 96 |
score = float(sims[i])
|
|
@@ -102,11 +79,11 @@ def search_one_model(query: str, model_cfg: dict, threshold: float,
|
|
| 102 |
return results
|
| 103 |
|
| 104 |
|
| 105 |
-
def search_all_models(query: str, threshold: float
|
| 106 |
"""يبحث في النماذج الثلاثة ويرجّع نتائج كل واحد منفصلة."""
|
| 107 |
all_results = []
|
| 108 |
for index, model_cfg in enumerate(config.MODELS):
|
| 109 |
-
items = search_one_model(query, model_cfg, threshold
|
| 110 |
all_results.append({
|
| 111 |
"model_index": index,
|
| 112 |
"model_key": model_cfg["key"],
|
|
|
|
| 16 |
from sklearn.metrics.pairwise import cosine_similarity
|
| 17 |
|
| 18 |
import config
|
|
|
|
| 19 |
from app.models_loader import resources
|
| 20 |
|
| 21 |
|
|
|
|
| 39 |
|
| 40 |
|
| 41 |
def search_one_model(query: str, model_cfg: dict, threshold: float,
|
| 42 |
+
top_k: int | None = None) -> list[dict]:
|
| 43 |
"""
|
| 44 |
يبحث في نموذج واحد ويرجّع قائمة نتائج مرتّبة تنازليًا حسب التشابه.
|
| 45 |
|
|
|
|
| 49 |
threshold : أقل قيمة تشابه مقبولة.
|
| 50 |
top_k : حدّ أقصى للنتائج يتجاوز قيمة config (اختياري). إن لم
|
| 51 |
يُمرَّر، يُستخدم model_cfg["top_k"].
|
|
|
|
|
|
|
| 52 |
"""
|
| 53 |
df = resources.get_dataframe()
|
| 54 |
doc_embeds = resources.get_embeddings(model_cfg["key"])
|
|
|
|
| 67 |
# top_k الممرّر يتجاوز قيمة config (وإلا نستخدم قيمة النموذج)
|
| 68 |
top_k = top_k if top_k is not None else model_cfg["top_k"]
|
| 69 |
|
| 70 |
+
# كوساين + threshold + top_k
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 71 |
results = []
|
| 72 |
for i in idx_sorted:
|
| 73 |
score = float(sims[i])
|
|
|
|
| 79 |
return results
|
| 80 |
|
| 81 |
|
| 82 |
+
def search_all_models(query: str, threshold: float) -> list[dict]:
|
| 83 |
"""يبحث في النماذج الثلاثة ويرجّع نتائج كل واحد منفصلة."""
|
| 84 |
all_results = []
|
| 85 |
for index, model_cfg in enumerate(config.MODELS):
|
| 86 |
+
items = search_one_model(query, model_cfg, threshold)
|
| 87 |
all_results.append({
|
| 88 |
"model_index": index,
|
| 89 |
"model_key": model_cfg["key"],
|
reverse-dictionary/backend/config.py
CHANGED
|
@@ -60,21 +60,21 @@ else:
|
|
| 60 |
MODELS = [
|
| 61 |
{
|
| 62 |
"key": "model_1_microsoft",
|
| 63 |
-
"display_name": "نموذج
|
| 64 |
"hf_id": "intfloat/multilingual-e5-large",
|
| 65 |
"use_prefix": True,
|
| 66 |
"top_k": 10,
|
| 67 |
},
|
| 68 |
{
|
| 69 |
"key": "model_2_researchers",
|
| 70 |
-
"display_name": "نموذج
|
| 71 |
"hf_id": "Omartificial-Intelligence-Space/Arabic-Triplet-Matryoshka-V2",
|
| 72 |
"use_prefix": False,
|
| 73 |
"top_k": None,
|
| 74 |
},
|
| 75 |
{
|
| 76 |
"key": "model_3_ksaa",
|
| 77 |
-
"display_name": "نموذج
|
| 78 |
"hf_id": MODEL_3_PATH,
|
| 79 |
"use_prefix": False,
|
| 80 |
"top_k": None,
|
|
@@ -87,16 +87,6 @@ MODELS = [
|
|
| 87 |
DEFAULT_THRESHOLD = 0.70 # حد التشابه الافتراضي
|
| 88 |
BATCH_SIZE = 64 # حجم الدفعة عند بناء الـ embeddings
|
| 89 |
|
| 90 |
-
# ------------------------------------------------------------
|
| 91 |
-
# 🔁 إعدادات إعادة الترتيب (Reranking) — مرحلة ثانية اختيارية
|
| 92 |
-
# ------------------------------------------------------------
|
| 93 |
-
# نموذج Cross-Encoder يعيد ترتيب أفضل المرشّحين حسب درجة (الاستعلام، التعريف).
|
| 94 |
-
# يُحمّل كسولًا عند أول استخدام فقط (انظر models_loader.py).
|
| 95 |
-
RERANKER_MODEL = os.getenv("RERANKER_MODEL", "BAAI/bge-reranker-v2-m3")
|
| 96 |
-
|
| 97 |
-
# عدد المرشّحين اللي نجيبهم بالكوساين قبل إعادة الترتيب (المرحلة الأولى).
|
| 98 |
-
RERANK_CANDIDATES = 40
|
| 99 |
-
|
| 100 |
# الأعمدة المستخدمة في بناء النص المُدمج للـ embedding
|
| 101 |
TEXT_COLUMNS = ["lemma", "definition", "example"]
|
| 102 |
|
|
|
|
| 60 |
MODELS = [
|
| 61 |
{
|
| 62 |
"key": "model_1_microsoft",
|
| 63 |
+
"display_name": "النموذج الأول",
|
| 64 |
"hf_id": "intfloat/multilingual-e5-large",
|
| 65 |
"use_prefix": True,
|
| 66 |
"top_k": 10,
|
| 67 |
},
|
| 68 |
{
|
| 69 |
"key": "model_2_researchers",
|
| 70 |
+
"display_name": "النموذج الثاني",
|
| 71 |
"hf_id": "Omartificial-Intelligence-Space/Arabic-Triplet-Matryoshka-V2",
|
| 72 |
"use_prefix": False,
|
| 73 |
"top_k": None,
|
| 74 |
},
|
| 75 |
{
|
| 76 |
"key": "model_3_ksaa",
|
| 77 |
+
"display_name": "النموذج الثالث",
|
| 78 |
"hf_id": MODEL_3_PATH,
|
| 79 |
"use_prefix": False,
|
| 80 |
"top_k": None,
|
|
|
|
| 87 |
DEFAULT_THRESHOLD = 0.70 # حد التشابه الافتراضي
|
| 88 |
BATCH_SIZE = 64 # حجم الدفعة عند بناء الـ embeddings
|
| 89 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 90 |
# الأعمدة المستخدمة في بناء النص المُدمج للـ embedding
|
| 91 |
TEXT_COLUMNS = ["lemma", "definition", "example"]
|
| 92 |
|
reverse-dictionary/frontend/streamlit_app.py
CHANGED
|
@@ -153,10 +153,9 @@ st.markdown("""
|
|
| 153 |
# نجيب أفضل TOP_N كلمات دائمًا، بدون حدّ تشابه (threshold = -1).
|
| 154 |
# ============================================================
|
| 155 |
@st.cache_data(show_spinner=False)
|
| 156 |
-
def cached_search(query: str, model_key: str
|
| 157 |
model_cfg = next(m for m in config.MODELS if m["key"] == model_key)
|
| 158 |
-
return search_one_model(query, model_cfg, threshold=-1.0,
|
| 159 |
-
top_k=TOP_N, rerank=rerank)
|
| 160 |
|
| 161 |
|
| 162 |
# ============================================================
|
|
@@ -225,23 +224,22 @@ with c_input:
|
|
| 225 |
query = st.text_input("البحث", placeholder="اكتب التعريف هنا...",
|
| 226 |
key="search_box", label_visibility="collapsed")
|
| 227 |
|
| 228 |
-
#
|
| 229 |
-
|
| 230 |
-
help="يعيد ترتيب أفضل المرشّحين عبر نموذج Cross-Encoder "
|
| 231 |
-
"حسب درجة (الاستعلام، التعريف). أول تفعيل قد يأخذ لحظات "
|
| 232 |
-
"لتحميل النموذج.")
|
| 233 |
-
|
| 234 |
-
# عند تغيّر الاستعلام، نمسح اختيارات المربّعات السابقة حتى لا تختلط
|
| 235 |
if st.session_state.get("_last_query") != query:
|
| 236 |
-
for k in list(st.session_state.keys()):
|
| 237 |
-
if k.startswith("chk_"):
|
| 238 |
-
del st.session_state[k]
|
| 239 |
st.session_state["_last_query"] = query
|
|
|
|
|
|
|
|
|
|
| 240 |
|
| 241 |
has_query = bool(query.strip())
|
| 242 |
cols = st.columns(3)
|
| 243 |
|
| 244 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 245 |
with col:
|
| 246 |
model_cfg = config.MODELS[i]
|
| 247 |
display_name = model_cfg["display_name"]
|
|
@@ -255,7 +253,7 @@ for i, col in enumerate(cols):
|
|
| 255 |
continue
|
| 256 |
|
| 257 |
with st.spinner(f"⏳ {display_name}..."):
|
| 258 |
-
results = cached_search(query.strip(), model_cfg["key"]
|
| 259 |
|
| 260 |
if not results:
|
| 261 |
st.markdown("<p class='empty-note'>لا توجد نتائج.</p>",
|
|
@@ -267,7 +265,7 @@ for i, col in enumerate(cols):
|
|
| 267 |
# مربّع الاختيار يسار | الكلمة والتعريف يمين
|
| 268 |
c_chk, c_txt = st.columns([1, 5], vertical_alignment="center")
|
| 269 |
with c_chk:
|
| 270 |
-
st.checkbox("صح", key=f"chk_{i}_{j}",
|
| 271 |
label_visibility="collapsed")
|
| 272 |
with c_txt:
|
| 273 |
pos_html = (f"<span class='word-pos'>{r['pos']}</span> "
|
|
@@ -290,11 +288,11 @@ with save_col:
|
|
| 290 |
per_model = []
|
| 291 |
for i in range(3):
|
| 292 |
model_cfg = config.MODELS[i]
|
| 293 |
-
results = cached_search(query.strip(), model_cfg["key"]
|
| 294 |
items = [{
|
| 295 |
"lemma": r["lemma"],
|
| 296 |
"score": r["score"],
|
| 297 |
-
"chosen": st.session_state.get(f"chk_{i}_{j}", False),
|
| 298 |
} for j, r in enumerate(results)]
|
| 299 |
if items:
|
| 300 |
per_model.append({"model": model_cfg["display_name"], "items": items})
|
|
@@ -305,7 +303,6 @@ with save_col:
|
|
| 305 |
query.strip(), per_model,
|
| 306 |
evaluator_name=evaluator["name"],
|
| 307 |
evaluator_email=evaluator["email"],
|
| 308 |
-
rerank=rerank,
|
| 309 |
)
|
| 310 |
st.toast(f"✅ حُفظ التقييم ({saved} كلمة)", icon="💾")
|
| 311 |
except Exception as e:
|
|
|
|
| 153 |
# نجيب أفضل TOP_N كلمات دائمًا، بدون حدّ تشابه (threshold = -1).
|
| 154 |
# ============================================================
|
| 155 |
@st.cache_data(show_spinner=False)
|
| 156 |
+
def cached_search(query: str, model_key: str):
|
| 157 |
model_cfg = next(m for m in config.MODELS if m["key"] == model_key)
|
| 158 |
+
return search_one_model(query, model_cfg, threshold=-1.0, top_k=TOP_N)
|
|
|
|
| 159 |
|
| 160 |
|
| 161 |
# ============================================================
|
|
|
|
| 224 |
query = st.text_input("البحث", placeholder="اكتب التعريف هنا...",
|
| 225 |
key="search_box", label_visibility="collapsed")
|
| 226 |
|
| 227 |
+
# عند إدخال استعلام جديد: نزيد "جيل التقييم" فتتغيّر مفاتيح المربّعات
|
| 228 |
+
# (chk_{gen}_..) فتُنشأ مربّعات جديدة فارغة — إعادة تعيين كاملة وموثوقة.
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 229 |
if st.session_state.get("_last_query") != query:
|
|
|
|
|
|
|
|
|
|
| 230 |
st.session_state["_last_query"] = query
|
| 231 |
+
st.session_state["_eval_gen"] = st.session_state.get("_eval_gen", 0) + 1
|
| 232 |
+
|
| 233 |
+
gen = st.session_state.get("_eval_gen", 0)
|
| 234 |
|
| 235 |
has_query = bool(query.strip())
|
| 236 |
cols = st.columns(3)
|
| 237 |
|
| 238 |
+
# ترتيب العرض من اليمين لليسار: يمين=النموذج الأول، وسط=الثاني، يسار=الثالث.
|
| 239 |
+
# (أعمدة Streamlit يسار→يمين، فنعكس الإسناد ونُبقي فهرس النموذج الحقيقي i.)
|
| 240 |
+
DISPLAY_ORDER = [2, 1, 0]
|
| 241 |
+
|
| 242 |
+
for col, i in zip(cols, DISPLAY_ORDER):
|
| 243 |
with col:
|
| 244 |
model_cfg = config.MODELS[i]
|
| 245 |
display_name = model_cfg["display_name"]
|
|
|
|
| 253 |
continue
|
| 254 |
|
| 255 |
with st.spinner(f"⏳ {display_name}..."):
|
| 256 |
+
results = cached_search(query.strip(), model_cfg["key"])
|
| 257 |
|
| 258 |
if not results:
|
| 259 |
st.markdown("<p class='empty-note'>لا توجد نتائج.</p>",
|
|
|
|
| 265 |
# مربّع الاختيار يسار | الكلمة والتعريف يمين
|
| 266 |
c_chk, c_txt = st.columns([1, 5], vertical_alignment="center")
|
| 267 |
with c_chk:
|
| 268 |
+
st.checkbox("صح", key=f"chk_{gen}_{i}_{j}",
|
| 269 |
label_visibility="collapsed")
|
| 270 |
with c_txt:
|
| 271 |
pos_html = (f"<span class='word-pos'>{r['pos']}</span> "
|
|
|
|
| 288 |
per_model = []
|
| 289 |
for i in range(3):
|
| 290 |
model_cfg = config.MODELS[i]
|
| 291 |
+
results = cached_search(query.strip(), model_cfg["key"])
|
| 292 |
items = [{
|
| 293 |
"lemma": r["lemma"],
|
| 294 |
"score": r["score"],
|
| 295 |
+
"chosen": st.session_state.get(f"chk_{gen}_{i}_{j}", False),
|
| 296 |
} for j, r in enumerate(results)]
|
| 297 |
if items:
|
| 298 |
per_model.append({"model": model_cfg["display_name"], "items": items})
|
|
|
|
| 303 |
query.strip(), per_model,
|
| 304 |
evaluator_name=evaluator["name"],
|
| 305 |
evaluator_email=evaluator["email"],
|
|
|
|
| 306 |
)
|
| 307 |
st.toast(f"✅ حُفظ التقييم ({saved} كلمة)", icon="💾")
|
| 308 |
except Exception as e:
|
reverse-dictionary/supabase_schema.sql
CHANGED
|
@@ -24,7 +24,6 @@ CREATE TABLE IF NOT EXISTS ratings (
|
|
| 24 |
word TEXT, -- الكلمة المقترحة
|
| 25 |
similarity REAL, -- نسبة التشابه
|
| 26 |
verdict TEXT, -- "صح" أو "خطأ"
|
| 27 |
-
rerank BOOLEAN, -- هل كان Reranking مفعّلًا؟
|
| 28 |
evaluator_name TEXT,
|
| 29 |
evaluator_email TEXT
|
| 30 |
);
|
|
|
|
| 24 |
word TEXT, -- الكلمة المقترحة
|
| 25 |
similarity REAL, -- نسبة التشابه
|
| 26 |
verdict TEXT, -- "صح" أو "خطأ"
|
|
|
|
| 27 |
evaluator_name TEXT,
|
| 28 |
evaluator_email TEXT
|
| 29 |
);
|