Spaces:
Sleeping
Sleeping
File size: 12,028 Bytes
ef55d62 a353893 2c71b9c ef55d62 a353893 ef55d62 a353893 ef55d62 a353893 ef55d62 a353893 ef55d62 2c71b9c ef55d62 a353893 ef55d62 a353893 ef55d62 a353893 ef55d62 a353893 ef55d62 2c71b9c ef55d62 2c71b9c ef55d62 a353893 ef55d62 a353893 ef55d62 | 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 | """Hadith splitter: sanad / matn segmentation for SHK4K/hadith-segmentation-bert."""
import html
import os
import re
import sys
import gradio as gr
import spaces
import torch
from transformers import AutoModelForTokenClassification, AutoTokenizer
MODEL_ID = os.environ.get("MODEL_ID", "SHK4K/hadith-segmentation-bert")
# The Hub config still ships LABEL_0..3, so model.config.id2label is useless here.
SANAD_IDS, MATN_IDS = (0, 1), (2, 3)
MAX_LEN, STRIDE, BATCH = 512, 128, 8
# ponytail: hard cap on very long pastes. 64 windows ~= 25k subword tokens, far
# beyond any single hadith; drop the cap if the demo ever faces books.
MAX_WINDOWS = 64
# The model was trained on undiacritized text; feeding it harakat produces
# garbage, so they are stripped for inference and mapped back for the output.
_HARAKAT = re.compile("[\u064b-\u0652\u0670\u0640]")
EXAMPLES = [
['ุญูุฏููุซูููุง ุนูู
ูุฑูู ุจููู ุฎูุงููุฏูุ ููุงูู ุญูุฏููุซูููุง ุงููููููุซูุ ุนููู ููุฒููุฏูุ ุนููู ุฃูุจูู ุงููุฎูููุฑูุ ุนููู ุนูุจูุฏู ุงูููููู ุจููู ุนูู
ูุฑูู ู ุฑุถู ุงููู ุนููู
ุง ู ุฃูููู ุฑูุฌููุงูุ ุณูุฃููู ุงููููุจูููู ุตูู ุงููู ุนููู ูุณูู
ุฃูููู ุงูุฅูุณููุงูู
ู ุฎูููุฑู ููุงูู \u200f "\u200f ุชูุทูุนูู
ู ุงูุทููุนูุงู
ูุ ููุชูููุฑูุฃู ุงูุณูููุงูู
ู ุนูููู ู
ููู ุนูุฑูููุชู ููู
ููู ููู
ู ุชูุนูุฑููู \u200f"\u200f\u200f.\u200f'],
['ุญูุฏููุซูููุง ุฃูุจูู ุงููููู
ูุงููุ ููุงูู ุฃูุฎูุจูุฑูููุง ุดูุนูููุจูุ ููุงูู ุญูุฏููุซูููุง ุฃูุจูู ุงูุฒููููุงุฏูุ ุนููู ุงูุฃูุนูุฑูุฌูุ ุนููู ุฃูุจูู ููุฑูููุฑูุฉู ู ุฑุถู ุงููู ุนูู ู ุฃูููู ุฑูุณูููู ุงูููููู ุตูู ุงููู ุนููู ูุณูู
ููุงูู \u200f "\u200f ููููุงูููุฐูู ููููุณูู ุจูููุฏููู ูุงู ููุคูู
ููู ุฃูุญูุฏูููู
ู ุญูุชููู ุฃูููููู ุฃูุญูุจูู ุฅููููููู ู
ููู ููุงููุฏููู ููููููุฏููู \u200f"\u200f\u200f.\u200f'],
['ุญูุฏููุซูููุง ุฃูุจูู ุงูููููููุฏูุ ููุงูู ุญูุฏููุซูููุง ุดูุนูุจูุฉูุ ููุงูู ุฃูุฎูุจูุฑูููู ุนูุจูุฏู ุงูููููู ุจููู ุนูุจูุฏู ุงูููููู ุจููู ุฌูุจูุฑูุ ููุงูู ุณูู
ูุนูุชู ุฃูููุณูุงุ ุนููู ุงููููุจูููู ุตูู ุงููู ุนููู ูุณูู
ููุงูู \u200f "\u200f ุขููุฉู ุงูุฅููู
ูุงูู ุญูุจูู ุงูุฃูููุตูุงุฑูุ ููุขููุฉู ุงููููููุงูู ุจูุบูุถู ุงูุฃูููุตูุงุฑู \u200f"\u200f\u200f.\u200f'],
['ุญูุฏููุซูููุง ุงููุญูู
ูููุฏูููู ุนูุจูุฏู ุงูููููู ุจููู ุงูุฒููุจูููุฑู ุ ููุงูู : ุญูุฏููุซูููุง ุณูููููุงูู ุ ููุงูู : ุญูุฏููุซูููุง ููุญูููู ุจููู ุณูุนููุฏู ุงููุฃูููุตูุงุฑูููู ุ ููุงูู : ุฃูุฎูุจูุฑูููู ู
ูุญูู
ููุฏู ุจููู ุฅูุจูุฑูุงูููู
ู ุงูุชููููู
ูููู ุ ุฃูููููู ุณูู
ูุนู ุนูููููู
ูุฉู ุจููู ูููููุงุตู ุงููููููุซูููู ุ ููููููู : ุณูู
ูุนูุชู ุนูู
ูุฑู ุจููู ุงููุฎูุทููุงุจู ุฑูุถููู ุงูููููู ุนููููู ุนูููู ุงููู
ูููุจูุฑูุ ููุงูู : ุณูู
ูุนูุชู ุฑูุณูููู ุงูููููู ุตููููู ุงูููููู ุนููููููู ููุณููููู
ูุ ููููููู : " ุฅููููู
ูุง ุงููุฃูุนูู
ูุงูู ุจูุงูููููููุงุชูุ ููุฅููููู
ูุง ููููููู ุงู
ูุฑูุฆู ู
ูุง ูููููุ ููู
ููู ููุงููุชู ููุฌูุฑูุชููู ุฅูููู ุฏูููููุง ููุตููุจูููุง ุฃููู ุฅูููู ุงู
ูุฑูุฃูุฉู ููููููุญูููุงุ ููููุฌูุฑูุชููู ุฅูููู ู
ูุง ููุงุฌูุฑู ุฅููููููู'],
]
_model = _tokenizer = None
def load():
global _model, _tokenizer
if _model is None:
_tokenizer = AutoTokenizer.from_pretrained(MODEL_ID)
_model = AutoModelForTokenClassification.from_pretrained(MODEL_ID).eval()
# Called from inside an @GPU scope, where the CUDA device is attached.
if torch.cuda.is_available() and _model.device.type == "cpu":
_model.to("cuda")
return _tokenizer, _model
def segment(text):
"""Return {'sanad', 'matn', 'html', 'note'} for raw hadith text."""
if not text or not text.strip():
raise ValueError("empty input")
# Strip harakat for inference, remembering where each character came from,
# so the cut can be mapped back onto the original diacritized text.
norm_chars, origin = [], []
for i, ch in enumerate(text):
if not _HARAKAT.match(ch):
norm_chars.append(ch)
origin.append(i)
norm = "".join(norm_chars)
tok, model = load()
enc = tok(
norm,
truncation=True,
max_length=MAX_LEN,
stride=STRIDE,
return_overflowing_tokens=True,
return_offsets_mapping=True,
return_tensors="pt",
padding=True,
)
offsets = enc.pop("offset_mapping")
enc.pop("overflow_to_sample_mapping", None)
windows = min(enc["input_ids"].shape[0], MAX_WINDOWS)
preds = []
with torch.inference_mode():
for i in range(0, windows, BATCH):
batch = {k: v[i : i + BATCH].to(model.device) for k, v in enc.items()}
preds.append(model(**batch).logits.argmax(-1).cpu())
preds = torch.cat(preds)
# Majority vote per word. A word straddling two windows is keyed by its
# character start, which both windows report identically.
votes = {}
for w in range(windows):
for pos, word_id in enumerate(enc.word_ids(w)):
if word_id is None:
continue
start, end = (int(x) for x in offsets[w][pos])
if start == end:
continue
label = int(preds[w][pos])
votes.setdefault(start, {}).setdefault(label, 0)
votes[start][label] += 1
# The labels describe a single cut, so take the first matn word as the
# boundary: sanad + matn always cover the whole input, in order.
cut = next(
(s for s in sorted(votes) if max(votes[s], key=votes[s].get) in MATN_IDS),
len(norm),
)
cut = origin[cut] if cut < len(origin) else len(text)
note = f"{len(text.split())} words ยท {windows} window(s)"
if windows < enc["input_ids"].shape[0]:
note += f" ยท only the first {MAX_WINDOWS} windows were processed"
if not text[cut:].strip():
note += " ยท model found no matn boundary"
return {
"sanad": text[:cut].strip(),
"matn": text[cut:].strip(),
"html": _highlight(text, cut),
"note": note,
}
def _highlight(text, cut):
escape = html.escape # before wrapping: the text comes from a public textbox
return (
'<div class="hadith" dir="rtl">'
f'<span class="sanad">{escape(text[:cut])}</span>'
f'<span class="matn">{escape(text[cut:])}</span>'
"</div>"
)
@spaces.GPU(duration=60)
def run(text):
try:
r = segment(text)
except ValueError:
raise gr.Error("ุฃุฏุฎู ูุต ุงูุญุฏูุซ ุฃููุงู / Please paste a hadith first.")
return r["html"], r["sanad"], r["matn"], r["note"]
load() # warm the Hub cache at startup, outside any GPU allocation
CSS = """
#hadith-input textarea { direction: rtl; font-size: 1.05rem; line-height: 1.9; }
.hadith { font-size: 1.15rem; line-height: 2.1; padding: 1rem; border-radius: .5rem;
background: #fffdf7; color: #1a1a1a; min-height: 3rem; }
.hadith .sanad { background: #fde68a; }
.hadith .matn { background: #bbf7d0; }
@media (prefers-color-scheme: dark) {
.hadith { background: #1f2937; color: #f3f4f6; }
.hadith .sanad { background: #92400e; }
.hadith .matn { background: #166534; }
}
"""
with gr.Blocks(title="Hadith Splitter") as demo:
gr.Markdown(
"# ูุงุตู ุงูุญุฏูุซ โ Hadith Splitter\n"
"ููุณู
ุงูุญุฏูุซ ุงูุฎุงู
ุฅูู **ุณูุฏ** ู**ู
ุชู**. ยท Splits a raw hadith into its "
"**sanad** (chain of narrators) and **matn** (the saying itself)."
)
inp = gr.Textbox(
label="ุงูุญุฏูุซ / Hadith",
lines=8,
rtl=True,
elem_id="hadith-input",
placeholder="ุญุฏุซูุง ... ูุงู ุฑุณูู ุงููู ุตูู ุงููู ุนููู ูุณูู
...",
)
with gr.Row():
btn = gr.Button("ูุณูู
/ Segment", variant="primary")
clear = gr.Button("ู
ุณุญ / Clear")
out_html = gr.HTML()
note = gr.Markdown()
with gr.Row():
sanad = gr.Textbox(
label="ุงูุณูุฏ / Sanad", lines=6, buttons=["copy"], rtl=True
)
matn = gr.Textbox(
label="ุงูู
ุชู / Matn", lines=6, buttons=["copy"], rtl=True
)
gr.Examples(examples=EXAMPLES, inputs=inp, label="ุฃู
ุซูุฉ / Examples")
gr.Markdown(
"> **ู
ูุงุญุธุงุช / Notes:** ููุฌุฑููุฏ ุงูุชุดููู ุชููุงุฆููุง ูุจู ุงูุชุญูููุ ูููุนุงุฏ ุงููุต ุงูุฃุตูู "
"ุจุชุดูููู ูู ุงูู
ุฎุฑุฌุงุช ยท diacritics are stripped for inference, your original "
"text is returned unchanged. "
"ุงููุต ุงูุฃุทูู ู
ู 512 ุฑู
ุฒูุง ููุนุงูุฌ ุจููุงูุฐ ู
ุชุฏุงุฎูุฉ ยท longer text is processed in "
"overlapping windows, never truncated. "
"ุงููู
ูุฐุฌ: [SHK4K/hadith-segmentation-bert]"
"(https://huggingface.co/SHK4K/hadith-segmentation-bert)."
)
outputs = [out_html, sanad, matn, note]
btn.click(run, inp, outputs)
inp.submit(run, inp, outputs)
clear.click(lambda: (None, "", "", "", ""), None, [inp] + outputs)
def _selftest():
sample = (
"ุญุฏุซูุง ุงูุญู
ูุฏู ุนุจุฏ ุงููู ุจู ุงูุฒุจูุฑ ุ ูุงู : ุญุฏุซูุง ุณููุงู ุ ูุงู : ุญุฏุซูุง ูุญูู ุจู ุณุนูุฏ "
"ุงูุฃูุตุงุฑู ุ ูุงู : ุฃุฎุจุฑูู ู
ุญู
ุฏ ุจู ุฅุจุฑุงููู
ุงูุชูู
ู ุ ุฃูู ุณู
ุน ุนููู
ุฉ ุจู ููุงุต ุงูููุซู ุ "
'ูููู : ุณู
ุนุช ุนู
ุฑ ุจู ุงูุฎุทุงุจ ุฑุถู ุงููู ุนูู ุนูู ุงูู
ูุจุฑุ ูุงู : ุณู
ุนุช ุฑุณูู ุงููู ุตูู ุงููู '
'ุนููู ูุณูู
ุ ูููู : " ุฅูู
ุง ุงูุฃุนู
ุงู ุจุงูููุงุชุ ูุฅูู
ุง ููู ุงู
ุฑุฆ ู
ุง ูููุ ูู
ู ูุงูุช ูุฌุฑุชู '
"ุฅูู ุฏููุง ูุตูุจูุง ุฃู ุฅูู ุงู
ุฑุฃุฉ ูููุญูุงุ ููุฌุฑุชู ุฅูู ู
ุง ูุงุฌุฑ ุฅููู"
)
r = segment(sample)
print("SANAD:", r["sanad"][:100])
print("MATN:", r["matn"][:100])
assert r["sanad"].startswith("ุญุฏุซูุง ุงูุญู
ูุฏู"), r["sanad"][:80]
assert "ุฅูู
ุง ุงูุฃุนู
ุงู ุจุงูููุงุช" in r["matn"]
assert sample.strip().startswith(r["sanad"]) and sample.strip().endswith(r["matn"])
long_text = " ".join([sample] * 6)
lr = segment(long_text)
assert lr["sanad"] and lr["matn"], "long input lost its boundary"
assert sample.strip()[-40:] in lr["matn"], "tail of a long input was dropped"
xss = '<script>alert(1)</script> ' + sample
assert "<script>" not in segment(xss)["html"], "input was not escaped"
# Diacritized input must segment too, and keep its diacritics in the output.
vocalized = (
"ุญูุฏููุซูููุง ุฃูุจูู ุงูููููููุฏูุ ููุงูู ุญูุฏููุซูููุง ุดูุนูุจูุฉูุ ุนููู ุฃูููุณูุ ููุงูู: "
"ููุงูู ุฑูุณูููู ุงูููููู ุตููููู ุงูููููู ุนููููููู ููุณููููู
ู: ยซุงูุฏููููู ุงููููุตููุญูุฉูยป"
)
vr = segment(vocalized)
print("VOCALIZED SANAD:", vr["sanad"][:80])
print("VOCALIZED MATN:", vr["matn"][:80])
assert vr["sanad"].startswith("ุญูุฏููุซูููุง"), "diacritics broke the sanad"
assert vr["matn"], "diacritized input lost its matn"
print("selftest ok")
if __name__ == "__main__":
if "--selftest" in sys.argv:
_selftest()
else:
demo.launch(css=CSS)
|