arabnamer-demo / app.py
Sayedyousef's picture
Deploy Gradio demo
56ed239 verified
Raw
History Blame Contribute Delete
9.56 kB
"""Gradio demo for arabnamer — live on Hugging Face Spaces.
Three tabs:
1. Transliterate — English name -> Arabic name (XGBoost / rules / hybrid engines)
2. Similarity — two Arabic strings -> lenient similarity score
3. Batch — paste many English names -> CSV-style output
Runs fully offline inside the Space container. No external API calls.
"""
from __future__ import annotations
import csv
import io
import gradio as gr
from arabnamer import Transliterator, similarity
# Lazy singletons — load once, reuse for all requests
_XGB = Transliterator(engine="model", threshold=85)
_RULES = Transliterator(engine="rules", threshold=85)
_HYBRID = Transliterator(engine="hybrid", threshold=85)
def _get_engine(name: str) -> Transliterator:
return {"model (XGBoost)": _XGB, "rules (deterministic)": _RULES, "hybrid": _HYBRID}[name]
def translit_single(name_en: str, engine: str, reference: str | None, threshold: int) -> tuple[str, str, str]:
"""Transliterate a single English name.
Returns: (arabic, score_display, details_markdown)
"""
if not name_en or not name_en.strip():
return "", "—", "Enter an English name above."
t = _get_engine(engine)
t.threshold = threshold
ref = reference.strip() if reference and reference.strip() else None
r = t.translit(name_en, reference=ref)
if ref:
score_display = f"{r.score:.1f} / 100" + (" ✅ accepted" if r.accepted else " ❌ below threshold")
else:
score_display = "— (no reference supplied)"
details = f"""**Engine used:** `{r.engine}`
**Input:** `{r.input}`
**Predicted Arabic:** `{r.arabic}`
**Reference:** {f'`{r.reference}`' if r.reference else '_not provided_'}
{'**Score:** ' + str(r.score) + ' (threshold ' + str(threshold) + ')' if ref else ''}
"""
return r.arabic, score_display, details
def similarity_pair(a: str, b: str, threshold: int) -> tuple[str, str, str]:
"""Score Arabic-to-Arabic similarity with the lenient normalizer."""
if not a or not b:
return "—", "—", "Enter two Arabic strings above."
passed, score = similarity(a, b, threshold=threshold)
verdict = "✅ match" if passed else "❌ not a match (below threshold)"
# Normalized forms (for debugging / transparency)
from arabnamer.scoring import normalize_arabic
na, nb = normalize_arabic(a), normalize_arabic(b)
details = f"""**Input A:** `{a}`
**Input A (normalized):** `{na}`
**Input B:** `{b}`
**Input B (normalized):** `{nb}`
**Score:** {score} / 100 (threshold: {threshold})
"""
return verdict, f"{score} / 100", details
def batch_transliterate(input_text: str, engine: str) -> tuple[str, str]:
"""Run a list of English names through the selected engine.
Input: one name per line.
Output: markdown table + CSV string.
"""
if not input_text or not input_text.strip():
return "Paste English names above (one per line).", ""
names = [line.strip() for line in input_text.splitlines() if line.strip()]
t = _get_engine(engine)
rows = [t.translit(n) for n in names]
# Markdown table
md_lines = ["| English | Arabic | Engine |", "|---|---|---|"]
for r in rows:
md_lines.append(f"| `{r.input}` | `{r.arabic}` | `{r.engine}` |")
md = "\n".join(md_lines)
# CSV string
buf = io.StringIO()
w = csv.writer(buf)
w.writerow(["name_en", "name_ar", "engine"])
for r in rows:
w.writerow([r.input, r.arabic, r.engine])
return md, buf.getvalue()
# ---------------------------------------------------------------------------
# UI
# ---------------------------------------------------------------------------
with gr.Blocks(title="arabnamer — Arabic name transliteration & similarity") as demo:
gr.Markdown(
"""
# arabnamer — Arabic name transliteration & similarity
**Offline** English → Arabic name transliteration and Arabic-to-Arabic fuzzy matching.
No LLM, no external API, names never leave this Space. Bundled with a 38 MB pruned
XGBoost model trained on 22,798 English-Arabic name pairs.
**Install on your own machine:**
```bash
pip install arabnamer
```
🔗 [GitHub](https://github.com/sayedyousef/arabnamer) ·
🔗 [PyPI](https://pypi.org/project/arabnamer/) ·
🔗 [Model](https://huggingface.co/Sayedyousef/arabnamer-xgboost) ·
🔗 [Dataset](https://huggingface.co/datasets/Sayedyousef/arabic-name-pairs)
"""
)
with gr.Tab("1. Transliterate"):
gr.Markdown("### English → Arabic")
with gr.Row():
with gr.Column():
name_in = gr.Textbox(
label="English name",
placeholder="Mohammed Ali",
lines=1,
)
engine_pick = gr.Radio(
["model (XGBoost)", "rules (deterministic)", "hybrid"],
value="model (XGBoost)",
label="Engine",
)
ref_in = gr.Textbox(
label="Reference Arabic (optional — enables scoring)",
placeholder="محمد علي",
lines=1,
)
thresh_t = gr.Slider(
minimum=0, maximum=100, value=85, step=1,
label="Pass threshold (lenient score)",
)
btn_t = gr.Button("Transliterate", variant="primary")
with gr.Column():
ar_out = gr.Textbox(label="Predicted Arabic", lines=1)
score_out = gr.Textbox(label="Score (vs reference)", lines=1)
details_out = gr.Markdown()
btn_t.click(
fn=translit_single,
inputs=[name_in, engine_pick, ref_in, thresh_t],
outputs=[ar_out, score_out, details_out],
)
gr.Examples(
examples=[
["Mohammed Ali", "model (XGBoost)", "محمد علي", 85],
["Omar Hassan", "hybrid", "عمر حسن", 85],
["Fatima Mansour", "model (XGBoost)", "فاطمة منصور", 85],
["Samir Khalil", "rules (deterministic)", "", 85],
["Layla Al Saleh", "hybrid", "ليلى الصالح", 85],
],
inputs=[name_in, engine_pick, ref_in, thresh_t],
)
with gr.Tab("2. Similarity"):
gr.Markdown("### Arabic ↔ Arabic fuzzy similarity")
gr.Markdown(
"Scoring is lenient — tashkeel stripped, hamza/taa-marbuta/alef-maksura unified, "
"then `max(fuzz.ratio, fuzz.partial_ratio)` via rapidfuzz."
)
with gr.Row():
with gr.Column():
a_in = gr.Textbox(label="Arabic string A", placeholder="أحمد حسن", lines=1)
b_in = gr.Textbox(label="Arabic string B", placeholder="احمد حسن", lines=1)
thresh_s = gr.Slider(
minimum=0, maximum=100, value=85, step=1,
label="Pass threshold",
)
btn_s = gr.Button("Compare", variant="primary")
with gr.Column():
verdict_out = gr.Textbox(label="Result", lines=1)
sim_score_out = gr.Textbox(label="Score", lines=1)
sim_details_out = gr.Markdown()
btn_s.click(
fn=similarity_pair,
inputs=[a_in, b_in, thresh_s],
outputs=[verdict_out, sim_score_out, sim_details_out],
)
gr.Examples(
examples=[
["أحمد حسن", "احمد حسن", 85],
["مروة فرج", "مروه فرج", 85],
["محمد علي", "محمد علي", 85],
["أدهم ساولي", "أدهم الصولي", 85],
],
inputs=[a_in, b_in, thresh_s],
)
with gr.Tab("3. Batch"):
gr.Markdown("### Batch transliteration")
gr.Markdown("Paste one English name per line. Output is a markdown table + downloadable CSV.")
with gr.Row():
with gr.Column():
batch_in = gr.Textbox(
label="English names (one per line)",
placeholder="Mohammed Ali\nAhmad Hassan\nFatima Mansour",
lines=10,
)
batch_engine = gr.Radio(
["model (XGBoost)", "rules (deterministic)", "hybrid"],
value="model (XGBoost)",
label="Engine",
)
btn_b = gr.Button("Transliterate batch", variant="primary")
with gr.Column():
batch_md = gr.Markdown()
batch_csv = gr.Textbox(
label="CSV output (copy / paste)",
lines=10,
)
btn_b.click(
fn=batch_transliterate,
inputs=[batch_in, batch_engine],
outputs=[batch_md, batch_csv],
)
gr.Markdown(
"""
---
**About:** arabnamer is an open-source Python library extracted from an MSc-thesis
project on Arabic name handling. The model, dataset, and training code are all public
and reproducible. Built for KYC / compliance / on-premise entity resolution where
names cannot be sent to cloud APIs.
**License:** code MIT · dataset + model weights CC-BY-4.0.
Maintained by [Elsayed Yousef](mailto:elsayed.yousef@gmail.com) ·
[Commercial support available](mailto:elsayed.yousef@gmail.com).
"""
)
if __name__ == "__main__":
demo.launch()