harraf / app.py
tarfandoon's picture
Upload 2 files
f3872a5 verified
Raw History Blame Contribute Delete
30.2 kB
"""Harraf on Hugging Face: a public page, a private model.
The weights and the model's code are in a private model repository; this Space fetches them when it starts,
with a read token kept as the Space secret HARRAF_TOKEN (the repository's name: the variable HARRAF_REPO). Nothing
of the model is in this Space's files. As in Harraf Chat, the model alone answers: nothing is looked up or
rewritten here, and the exact text given to the model is shown under each answer.
Locally: HARRAF_LOCAL=<folder with model.pt, tokenizer.json, morph.json and farsitree/> python app.py
"""
import json
import math
import os
import sys
import tempfile
import threading
import unicodedata
try: # ZeroGPU: «spaces» first; a GPU is lent while a @GPU function runs
import spaces
GPU = spaces.GPU
except Exception: # anywhere else: no-op
spaces = None
def GPU(fn=None, **_):
return fn if fn is not None else (lambda f: f)
import gradio as gr
import torch
REPO = (os.environ.get("HARRAF_REPO") or "tarfandoon/harraf").strip()
FA_DIGITS = str.maketrans("0123456789", "۰۱۲۳۴۵۶۷۸۹")
ABOUT = """
### حراف چیست؟
حراف یک مدل زبانی فارسی است که **علی جهانی** در سال ۱۴۰۵ از صفر ساخته است؛ از توکنایزر تا معماری و آموزش، همه برای
زبان فارسی. هر جوابی که اینجا می‌بینید نوشته‌ی خود مدل است و هیچ برنامه‌ای آن را تغییر نمی‌دهد.
حراف مدل کوچکی است و ممکن است اشتباه کند؛ جواب‌هایش، به‌خصوص عددها و تاریخ‌ها، را از یک منبع معتبر هم بررسی کنید.
* وب‌سایت: [jahaniwww.com](https://jahaniwww.com)
* لینکدین: [ajahani](https://www.linkedin.com/in/ajahani/)
* تلگرام: [@Satreyek](https://t.me/Satreyek)
* گیت‌هاب: [alipyth](https://github.com/alipyth)
* یوتیوب: [ترفندون](https://youtube.com/@tarfandoon)
"""
GUIDE = """
### چند نکته برای گفت‌وگوی بهتر با حراف
**پرسش را کوتاه و روشن بنویسید.** «قله‌ی دماوند در کدام استان است؟» بهتر از یک پاراگراف طولانی جواب می‌گیرد.
اگر جواب کامل‌تر می‌خواهید، بگویید: «درباره‌ی حافظ مفصل توضیح بده» یا بعد از جواب بنویسید «بیشتر بگو».
**دما (تنوع جواب‌ها)**
* **۰ تا ۰٫۳:** جواب دقیق‌تر و ثابت؛ برای پرسش‌های علمی، عددها، ریاضی و قرآن.
* **۰٫۵ تا ۰٫۸:** جواب طبیعی‌تر و متنوع‌تر؛ برای گفت‌وگو، داستان و متن‌های آزاد.
* **بیشتر از ۱:** خلاق ولی پراشتباه؛ برای آزمایش.
**پیام‌های قبلی:** وقتی این گزینه روشن است، حراف گفت‌وگوی قبلی را می‌بیند؛ مثلاً اسمتان را به یاد می‌آورد یا «ارتفاعش چقدره؟»
را درباره‌ی همان کوهی می‌فهمد که پرسیده بودید. وقتی موضوع را عوض می‌کنید، «گفت‌وگوی تازه» را بزنید.
**گزینه‌ها:** برای پرسش چندگزینه‌ای، هر گزینه را در یک خط بنویسید؛ حراف شماره‌ی گزینه‌ی درست را می‌گوید.
**متن:** اگر متنی دارید و می‌خواهید حراف از روی آن جواب بدهد، آن را در بخش «متن» بگذارید؛ اگر جواب در متن نباشد، باید بگوید
«در متن نیامده است».
**ریاضی:** حراف حساب را قدم‌به‌قدم می‌نویسد؛ مسئله را به زبان ساده بنویسید، مثل «مریم ۴۷ کتاب داشت و ۱۹ تا را داد. چند تا ماند؟».
**جای خالی:** در تب «جای خالی» کلمه‌ی گم‌شده را با ___ مشخص کنید؛ حراف چند کلمه با احتمالشان پیشنهاد می‌دهد.
**اصلاح متن:** حراف غلط‌های املایی را درست می‌کند و کلمه‌هایی را که برایش غیرعادی‌اند رنگی می‌کند. رنگی بودن یعنی «به این
کلمه نگاه کنید»، نه لزوماً «غلط است».
**بردار متن:** برای مقایسه‌ی تقریبی معنای چند متن یا ساختن ابزار جست‌وجو و دسته‌بندی ساده. حراف برای این کار آموزش
خاصی ندیده، پس شباهت‌ها تقریبی‌اند.
**کیبورد انگلیسی:** اگر فارسی را با کیبورد انگلیسی تایپ کرده باشید (مثل sghl)، حراف معمولاً می‌فهمد منظورتان چه بوده.
**محدودیت‌ها:** حراف در دانسته‌های عمومی، درک متن، ریاضی ساده و قرآن بهتر است و در نوشتن آزاد و مشاوره ضعیف‌تر. گاهی چیزی
می‌سازد که درست نیست؛ برای کارهای مهم به جوابش تکیه نکنید.
"""
HERO = """
<div class="hero">
<div class="hero-title">حرّاف</div>
<div class="hero-sub">مدل زبانی فارسی، ساخته‌شده از صفر برای زبان فارسی</div>
<div class="badges"><span>گفت‌وگو</span><span>جای خالی</span><span>کلمه‌ی بعدی</span><span>اصلاح متن</span><span>بردار متن</span></div>
</div>
"""
CSS = """
.gradio-container { direction: rtl; max-width: 980px !important; margin: auto; }
textarea, input, .prose, .message, .md { direction: rtl; text-align: right; }
.hero { text-align: center; padding: 28px 16px 20px; border-radius: 18px; margin-bottom: 8px; color: #fff;
background: linear-gradient(135deg, #3730a3 0%, #4f46e5 45%, #0ea5e9 100%); }
.hero, .hero * { color: #ffffff !important; }
.hero-title { font-size: 2.4em; font-weight: 800; letter-spacing: 1px; }
.hero-sub { opacity: .92; margin-top: 4px; }
.badges { margin-top: 14px; display: flex; flex-wrap: wrap; gap: 8px; justify-content: center; }
.badges span { background: rgba(255,255,255,.16); border: 1px solid rgba(255,255,255,.3); padding: 3px 12px;
border-radius: 999px; font-size: .85em; }
.prompt textarea { font-size: 0.82em; opacity: 0.75; }
table, td, th, td *, th * { font-family: Vazirmatn, Tahoma, sans-serif !important; letter-spacing: normal !important; }
footer { visibility: hidden; }
"""
QUESTIONS = ["قله‌ی دماوند در کدام استان است؟", "درباره‌ی حافظ مفصل توضیح بده.", "سوره‌ی یوسف چند آیه دارد؟",
"مریم ۴۷ کتاب داشت و ۱۹ تا را به دوستش داد. چند کتاب برایش ماند؟", "یه سوال از من بپرس",
"کی تو رو ساخته؟", "sghl o,fd?", "سوره‌ی دوم قرآن چیست؟"]
WITH_OPTIONS = [["پایتخت ایران کدام شهر است؟", "اصفهان\nتهران\nشیراز\nتبریز"],
["حاصل ۲۵ ضرب در ۴ چند است؟", "۸۰\n۹۰\n۱۰۰\n۱۲۰"]]
PASSAGE = [["کوه سبلان با ارتفاع ۴۸۱۱ متر، سومین قله‌ی بلند ایران است و در استان اردبیل قرار دارد. دریاچه‌ی کوچکی در "
"دهانه‌ی آتشفشانی آن تشکیل شده است.", "سبلان در کدام استان است؟"]]
GAPS = ["حافظ شاعر بزرگ ___ است.", "پایتخت ایران شهر ___ است.", "من هر روز صبح ___ می‌خورم.",
"قرآن ۱۱۴ ___ دارد.", "بلندترین کوه ایران ___ است."]
EMBED_LINES = "تهران پایتخت ایران است.\nپایتخت ایران تهران است.\nحافظ شاعری شیرازی بود.\nدیوان حافظ پر از غزل است.\nامروز هوا بارانی است."
SENTENCES = ["من سبح زود به مدرسح رفتم و درص خواندم.", "امروز حوا خیلی خوب است و ما به پارک رفطیم.",
"این کطاب را دیروز از کطابخانه گرفتم."]
def fetch():
"""The folder with the model and its code: HARRAF_LOCAL, or the private repository."""
if os.environ.get("HARRAF_LOCAL"):
return os.environ["HARRAF_LOCAL"]
from huggingface_hub import snapshot_download
token = (os.environ.get("HARRAF_TOKEN") or os.environ.get("HF_TOKEN") or "").strip()
seen = sorted(k for k in os.environ if "HARRAF" in k.upper() or "TOKEN" in k.upper()) # names only, never values
print(f"model repository: {REPO}; settings found: {seen or 'none'}", flush=True)
if not token:
raise RuntimeError("no token: add the secret HARRAF_TOKEN (Settings -> Variables and secrets -> New secret)")
# into the Hub's cache, outside the app's folder (nothing the page serves), and blocked in launch() as well
return snapshot_download(REPO, token=token)
def cpus():
"""The cores this container may use: a Space's cgroup limit (2 on free CPU), not the host's count that
os.cpu_count() gives; too many threads on 2 cores make every step crawl."""
try:
quota, period = open("/sys/fs/cgroup/cpu.max").read().split()
if quota != "max":
return max(1, int(int(quota) / int(period)))
except Exception:
pass
try:
return max(1, len(os.sched_getaffinity(0)))
except Exception:
return max(1, min(os.cpu_count() or 1, 4))
class Harraf:
def __init__(self, folder):
sys.path.insert(0, folder)
from tokenizers import Tokenizer
from farsitree.data import EOS, normalize
from farsitree.harraf2 import build_model
from farsitree.morph import Segmenter
self.normalize, self.Segmenter = normalize, Segmenter
torch.set_num_threads(cpus())
zero = spaces is not None and os.environ.get("SPACES_ZERO_GPU") # ZeroGPU: put on «cuda» at start
self.dev = "cuda" if zero or torch.cuda.is_available() else "cpu"
ck = torch.load(os.path.join(folder, "model.pt"), map_location="cpu")
model = build_model(dict(ck["config"]))
model.load_state_dict({k: v.float() if v.is_floating_point() else v for k, v in ck["model"].items()})
self.model = model.to(self.dev).eval()
self.tok = Tokenizer.from_file(os.path.join(folder, "tokenizer.json"))
self.seg = Segmenter.load(os.path.join(folder, "morph.json"))
self.eos = self.tok.token_to_id(EOS)
self.lock = threading.Lock()
print(f"Harraf: {model.num_params() / 1e6:.0f}M parameters on {self.dev}, {cpus()} cores", flush=True)
def encode(self, text):
return self.tok.encode(self.seg(self.normalize(text))).ids
def decode(self, ids):
text = self.Segmenter.join(self.tok.decode(ids)).replace("+", "").replace("�", "")
return "".join(c for c in text if unicodedata.category(c) != "Cc")
@torch.no_grad()
def logits(self, ids):
return self.model(torch.tensor([ids[-self.model.cfg.max_seq_len:]], device=self.dev))["logits"][0, -1].float()
@staticmethod
def pick(logits, temperature, top_p=0.9):
"""Greedy at temperature 0; else sampled from the likeliest tokens that together make top_p."""
if temperature <= 0:
return int(logits.argmax())
p = torch.softmax(logits / temperature, -1)
sp, si = torch.sort(p, descending=True)
keep = torch.cumsum(sp, 0) - sp < top_p
sp = sp * keep
return int(si[torch.multinomial(sp / sp.sum(), 1)])
@torch.no_grad()
def write(self, prompt, n=160, temperature=0.0):
"""The answer, word by word (a generator): up to the end-of-text token, «پرسش:» or n tokens."""
n = max(8, min(int(n), self.model.cfg.max_seq_len // 2))
ids = [self.eos] + self.encode(prompt)
ids = ids[-(self.model.cfg.max_seq_len - n):]
start = len(ids)
with self.lock:
for i in range(n):
logits = self.logits(ids)
t = self.pick(logits, temperature)
if t == self.eos:
break
ids.append(t)
text = self.decode(ids[start:])
if "پرسش:" in text:
break
if i % 3 == 2:
yield text.strip()
yield self.decode(ids[start:]).split("پرسش:")[0].strip()
@torch.no_grad()
def next_tokens(self, text, k=10):
ids = [self.eos] + (self.encode(text) if text.strip() else [])
with self.lock:
p = torch.softmax(self.logits(ids), -1)
top = torch.topk(p, k)
rows = []
for prob, i in zip(top.values.tolist(), top.indices.tolist()):
piece = "⏎ پایان متن" if i == self.eos else self.tok.decode([i]).replace("�", "")
rows.append([piece.replace("+", "ـ").strip() or "␣", f"{100 * prob:.1f}٪".translate(FA_DIGITS)])
return rows
class Tools:
"""What else the model can be asked, with no other program: how likely it finds a text, what fits a blank,
and a vector for a text (the mean of its last hidden states)."""
REFERENCE = ["امروز هوا آفتابی است.", "قیمت نان دوباره بالا رفت.", "حافظ در شیراز به دنیا آمد.", "تیم ملی فوتبال برد.",
"آب در صد درجه می‌جوشد.", "کتاب را روی میز گذاشتم.", "این رستوران غذای خوبی دارد.", "دانشگاه تهران قدیمی است.",
"او هر روز ورزش می‌کند.", "قطار ساعت هشت حرکت می‌کند.", "باران تمام شب بارید.", "رایانه‌ام خراب شده است."]
def __init__(self, h):
self.h = h
self.center = None # made the first time a vector is asked for, not at start
def _center(self):
if self.center is None:
self.center = False # while it is made, vectors are taken plain
self.center = torch.stack([self.vector(t) for t in self.REFERENCE]).mean(0) # what all texts share
return self.center
@torch.no_grad()
def logprob(self, text):
"""The model's log-probability of text (after the start token), and the number of its tokens."""
ids = [self.h.eos] + self.h.encode(text)
ids = ids[-self.h.model.cfg.max_seq_len:]
with self.h.lock:
logits = self.h.model(torch.tensor([ids], device=self.h.dev))["logits"][0, :-1].float()
lp = torch.log_softmax(logits, -1).gather(1, torch.tensor(ids[1:], device=self.h.dev)[:, None])
return float(lp.sum()), len(ids) - 1
def words(self, text, limit=60):
"""(word, how likely the model found it there) for each word: low means unexpected here."""
ws = text.split()[:limit]
out, prev = [], 0.0
for i in range(len(ws)):
lp, _ = self.logprob(" ".join(ws[:i + 1]))
out.append((ws[i], math.exp(lp - prev)))
prev = lp
return out
@torch.no_grad()
def fill(self, text, k=16, show=6):
"""Words for «___» in text, by how likely the whole text is with each (the model reads both sides)."""
left, _, right = text.partition("___")
ids = [self.h.eos] + self.h.encode(left.rstrip()) if left.strip() else [self.h.eos]
with self.h.lock:
p = torch.softmax(self.h.logits(ids), -1)
cands = []
for t in torch.topk(p, k).indices.tolist():
if t == self.h.eos:
continue
seq = [t]
with self.h.lock:
for _ in range(3): # a word may be more than one token
nxt = int(self.h.logits(ids + seq).argmax())
if nxt == self.h.eos or " " in self.h.decode(seq + [nxt]).strip():
break
seq.append(nxt)
w = self.h.decode(seq).strip().split(" ")[0].strip("،.؟!:")
if w and w not in cands:
cands.append(w)
scored = []
for w in cands:
full = (left.rstrip() + " " + w + " " + right.lstrip()).strip()
scored.append((w, self.logprob(full)[0]))
if not scored:
return []
top = max(x for _, x in scored)
z = sum(math.exp(x - top) for _, x in scored)
return sorted(((w, math.exp(x - top) / z) for w, x in scored), key=lambda x: -x[1])[:show]
@torch.no_grad()
def vector(self, text):
ids = [self.h.eos] + self.h.encode(text)
ids = ids[-self.h.model.cfg.max_seq_len:]
with self.h.lock:
hid = self.h.model(torch.tensor([ids], device=self.h.dev), return_hidden=True)["hidden"][0, 1:].float()
v = hid.mean(0)
v = v / v.norm()
center = self._center() if self.center is not False else None
if center is not None and center is not False: # without what every text has in common, the rest stands out
v = v - center
v = v / v.norm()
return v
def with_options(question, options):
"""«… گزینه‌ها: ۱) … ۲) …» when choices are given."""
q = question.strip()
opts = [o.strip() for o in (options or "").split("\n") if o.strip()]
if opts:
q += " گزینه‌ها: " + " ".join(f"{str(i).translate(FA_DIGITS)}) {o}" for i, o in enumerate(opts, 1))
return q
def frame(history, question, options):
"""The text given to the model, in the frame it was taught on."""
parts = [f"پرسش: {q} پاسخ: {a}" for q, a in history]
return " ".join(parts + [f"پرسش: {with_options(question, options)} پاسخ:"])
H = T = None
ERROR = None
MODEL_DIR = None
try:
MODEL_DIR = fetch()
H = Harraf(MODEL_DIR)
T = Tools(H)
except Exception as e: # the page still opens and says why
ERROR = f"مدل بارگذاری نشد: {type(e).__name__}: {str(e)[:300]}"
print(ERROR, flush=True)
@GPU(duration=120)
def ask(question, options, passage, keep, chat, pairs, n, temperature):
question, options, passage = question or "", options or "", passage or "" # Gradio 6 sends None for an empty box
chat, pairs = chat or [], pairs or []
if not question.strip():
yield chat, pairs, "", ""
return
if H is None:
yield chat + [{"role": "user", "content": question}, {"role": "assistant", "content": ERROR}], pairs, "", ""
return
if passage.strip(): # answering from a text: the frame «<text> پرسش: … پاسخ:»
prompt = f"{' '.join(passage.split())} پرسش: {with_options(question, options)} پاسخ:"
else:
prompt = frame(pairs if keep else [], question, options)
shown = question + ("\n" + options.strip() if options.strip() else "")
chat = chat + [{"role": "user", "content": shown}, {"role": "assistant", "content": "…"}]
answer = ""
for answer in H.write(prompt, int(n), float(temperature)):
chat[-1]["content"] = answer or "…"
yield chat, pairs, "", prompt
yield chat, pairs + [(with_options(question, options), answer)], "", prompt
def fix(sentence, n, temperature):
if H is None:
yield ERROR, ""
return
prompt = f"پرسش: این جمله را از نظر نگارشی اصلاح کن: {sentence.strip()} پاسخ:"
for answer in H.write(prompt, int(n), float(temperature)):
yield answer, prompt
def md_table(head, rows):
return "| " + " | ".join(head) + " |\n|" + "---|" * len(head) + "\n" + "\n".join("| " + " | ".join(r) + " |" for r in rows)
@GPU
def blank(text):
text = text or ""
if T is None:
return ERROR, ""
if "___" not in text:
return "جای خالی را با ___ (سه خط زیر) مشخص کنید.", ""
got = T.fill(text)
best = text.replace("___", f"«{got[0][0]}»", 1) if got else ""
return md_table(["کلمه", "احتمال"], [[w, f"{100 * p:.0f}٪".translate(FA_DIGITS)] for w, p in got]), best
@GPU(duration=120)
def check(text, n, temperature):
"""The model's fix, and each word marked by how unexpected the model found it."""
text = text or ""
if H is None:
yield ERROR, [], ""
return
got = T.words(text)
surprise = [-math.log(max(p, 1e-12)) for _, p in got]
mean = sum(surprise) / max(1, len(surprise))
std = (sum((x - mean) ** 2 for x in surprise) / max(1, len(surprise))) ** 0.5
marks = []
for (w, p), x in zip(got, surprise): # much more surprising than the rest of this text
label = "خیلی غیرعادی" if p < 1e-5 and x > mean + 2 * std else "غیرعادی" if p < 1e-3 and x > mean + std else None
marks += [(w, label), (" ", None)]
prompt = f"پرسش: این جمله را از نظر نگارشی اصلاح کن: {text.strip()} پاسخ:"
for answer in H.write(prompt, int(n), float(temperature)):
yield answer, marks, prompt
@GPU
def embed(lines_text):
lines_text = lines_text or ""
if T is None:
return ERROR, None, ""
lines = [l.strip() for l in lines_text.split("\n") if l.strip()][:30]
if len(lines) < 1:
return "", None, ""
vecs = torch.stack([T.vector(l) for l in lines])
sim = (vecs @ vecs.T).tolist()
head = [f"{i + 1}".translate(FA_DIGITS) for i in range(len(lines))]
table = [[head[i] + ") " + lines[i][:40]] + [f"{sim[i][j]:.2f}".translate(FA_DIGITS) for j in range(len(lines))]
for i in range(len(lines))]
pairs = sorted(((sim[i][j], i, j) for i in range(len(lines)) for j in range(i + 1, len(lines))), reverse=True)[:5]
near = "\n".join(f"* {s:.2f}: «{lines[i][:60]}» و «{lines[j][:60]}»".translate(FA_DIGITS) for s, i, j in pairs)
path = os.path.join(tempfile.mkdtemp(), "harraf_vectors.json")
json.dump([{"text": l, "vector": [round(x, 5) for x in v.tolist()]} for l, v in zip(lines, vecs)],
open(path, "w", encoding="utf-8"), ensure_ascii=False)
md = "| متن | " + " | ".join(head) + " |\n|" + "---|" * (len(head) + 1) + "\n"
md += "\n".join("| " + " | ".join(row) + " |" for row in table)
return md, path, ("**نزدیک‌ترین‌ها**\n" + near) if near else ""
@GPU
def live(text):
return md_table(["تکه‌ی بعدی", "احتمال"], H.next_tokens(text or "")) if H is not None else ERROR
GRADIO_6 = int(gr.__version__.split(".")[0]) >= 6
def make(cls, **kw):
"""A Gradio component with only the arguments this Gradio version takes (5 and 6 differ: «type» of Chatbot)."""
import inspect
names = inspect.signature(cls).parameters
return cls(**{k: v for k, v in kw.items() if k in names})
THEME = gr.themes.Soft(primary_hue="indigo", secondary_hue="sky",
font=[gr.themes.GoogleFont("Vazirmatn"), "Tahoma", "sans-serif"])
with (gr.Blocks(title="حراف — مدل زبانی فارسی") if GRADIO_6 else
gr.Blocks(css=CSS, title="حراف — مدل زبانی فارسی", theme=THEME)) as demo:
gr.HTML(HERO)
with gr.Tab("گفت‌وگو"):
chat = make(gr.Chatbot, type="messages", height=440, rtl=True, show_label=False,
placeholder="سلام! پرسشتان را بنویسید یا یکی از نمونه‌های پایین را امتحان کنید.")
pairs = gr.State([])
question = gr.Textbox(show_label=False, placeholder="پرسشتان را بنویسید و Enter بزنید…", rtl=True)
with gr.Row():
send = gr.Button("بپرس", variant="primary", scale=3)
new = gr.Button("گفت‌وگوی تازه", scale=1)
with gr.Accordion("گزینه‌ها (برای پرسش چندگزینه‌ای؛ هر گزینه در یک خط)", open=False):
options = gr.Textbox(lines=4, show_label=False, rtl=True)
with gr.Accordion("متن (برای جواب دادن از روی یک متن)", open=False):
passage = gr.Textbox(lines=5, show_label=False, rtl=True, placeholder="متن را اینجا بگذارید و درباره‌اش بپرسید.")
with gr.Accordion("تنظیمات", open=False):
with gr.Row():
temperature = gr.Slider(0, 1.5, value=0.3, step=0.05, label="دما (تنوع جواب؛ راهنما را ببینید)")
n = gr.Slider(32, 400, value=200, step=8, label="بیشترین طول جواب (توکن)")
keep = gr.Checkbox(value=True, label="پیام‌های قبلی را هم به حراف نشان بده")
gr.Examples(QUESTIONS, inputs=question, label="نمونه‌ها")
gr.Examples(WITH_OPTIONS, inputs=[question, options], label="نمونه با گزینه")
gr.Examples(PASSAGE, inputs=[passage, question], label="نمونه با متن")
prompt = gr.Textbox(label="متنی که به حراف داده شد", interactive=False, elem_classes="prompt", rtl=True)
inputs = [question, options, passage, keep, chat, pairs, n, temperature]
send.click(ask, inputs, [chat, pairs, question, prompt])
question.submit(ask, inputs, [chat, pairs, question, prompt])
new.click(lambda: ([], [], ""), None, [chat, pairs, prompt])
with gr.Tab("جای خالی"):
gr.Markdown("جای کلمه‌ی گم‌شده را با **___** (سه خط زیر) مشخص کنید. حراف هر دو طرف جمله را می‌خواند و "
"کلمه‌هایی را که به نظرش آنجا می‌نشینند، با احتمالشان پیشنهاد می‌دهد.", rtl=True)
gap = gr.Textbox(label="متن با جای خالی", rtl=True, placeholder="مثلاً: حافظ شاعر بزرگ ___ است.")
gap_go = gr.Button("پر کن", variant="primary")
with gr.Row():
gap_table = gr.Markdown(rtl=True)
gap_best = gr.Textbox(label="با محتمل‌ترین کلمه", interactive=False, rtl=True)
gr.Examples(GAPS, inputs=gap, label="نمونه‌ها")
gap_go.click(blank, gap, [gap_table, gap_best])
gap.submit(blank, gap, [gap_table, gap_best])
with gr.Tab("کلمه‌ی بعدی"):
gr.Markdown("بنویسید تا ببینید حراف فکر می‌کند کدام تکه‌ها بعد از متن شما می‌آیند، و با چه احتمالی.", rtl=True)
text = gr.Textbox(label="متن", rtl=True, placeholder="مثلاً: بلندترین کوه ایران")
table = gr.Markdown(rtl=True)
text.change(live, text, table)
with gr.Tab("اصلاح متن"):
gr.Markdown("حراف غلط‌های املایی را درست می‌کند. زیر آن، هر کلمه‌ای که برای حراف **غیرعادی** است رنگی می‌شود: "
"شاید غلط تایپی، کلمه‌ی نابه‌جا یا جمله‌بندی ناآشنا باشد.", rtl=True)
sentence = gr.Textbox(label="متن", lines=3, rtl=True)
go = gr.Button("بررسی و اصلاح", variant="primary")
fixed = gr.Textbox(label="اصلاح حراف", interactive=False, rtl=True)
marks = make(gr.HighlightedText, label="کلمه‌های غیرعادی از نگاه حراف", rtl=True, combine_adjacent=False,
show_inline_category=False, show_legend=True,
color_map={"غیرعادی": "#f59e0b", "خیلی غیرعادی": "#ef4444"})
with gr.Accordion("تنظیمات", open=False):
with gr.Row():
t2 = gr.Slider(0, 1.5, value=0, step=0.05, label="دما")
n2 = gr.Slider(32, 300, value=160, step=8, label="بیشترین طول جواب (توکن)")
gr.Examples(SENTENCES, inputs=sentence, label="نمونه‌ها")
fix_prompt = gr.Textbox(label="متنی که به حراف داده شد", interactive=False, elem_classes="prompt", rtl=True)
go.click(check, [sentence, n2, t2], [fixed, marks, fix_prompt])
with gr.Tab("بردار متن"):
gr.Markdown("هر خط یک متن. حراف برای هر متن یک بردار (embedding) از درون خودش می‌سازد و شباهت متن‌ها را با هم "
"می‌سنجد (۱ یعنی خیلی شبیه). بردارها را هم می‌توانید دانلود کنید.", rtl=True)
lines = gr.Textbox(label="متن‌ها (هر خط یکی، تا ۳۰ خط)", lines=6, rtl=True)
emb_go = gr.Button("بساز و مقایسه کن", variant="primary")
sims = gr.Markdown(rtl=True)
near = gr.Markdown(rtl=True)
vec_file = gr.File(label="بردارها (JSON)")
gr.Examples([EMBED_LINES], inputs=lines, label="نمونه")
emb_go.click(embed, lines, [sims, vec_file, near])
with gr.Tab("راهنما"):
gr.Markdown(GUIDE, rtl=True)
with gr.Tab("درباره‌ی حراف"):
gr.Markdown(ABOUT, rtl=True)
def _only(fn, **kw):
"""fn's own arguments of kw (Gradio 5 and 6 differ)."""
import inspect
names = inspect.signature(fn).parameters
return {k: v for k, v in kw.items() if k in names}
if __name__ == "__main__":
demo.queue(**_only(demo.queue, default_concurrency_limit=1, api_open=False))
hidden = [MODEL_DIR] if MODEL_DIR else []
demo.launch(**_only(demo.launch, css=CSS, theme=THEME, show_api=False, blocked_paths=hidden) if GRADIO_6 else
_only(demo.launch, show_api=False, blocked_paths=hidden))