File size: 9,564 Bytes
f4768fc
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
56ed239
 
 
 
f4768fc
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
56ed239
f4768fc
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
"""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()