Spaces:
Running on Zero
Running on Zero
Download app.py from tarfandoon/harraf: direct link, hf CLI and curl.
- Browser
- Download file 30.2 kB
-
https://huggingface.co/spaces/tarfandoon/harraf/resolve/main/app.py
- Command line
-
hf download hf://spaces/tarfandoon/harraf/app.py
-
curl -L -o app.py https://huggingface.co/spaces/tarfandoon/harraf/resolve/main/app.py
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") | |
| def logits(self, ids): | |
| return self.model(torch.tensor([ids[-self.model.cfg.max_seq_len:]], device=self.dev))["logits"][0, -1].float() | |
| 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)]) | |
| 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() | |
| 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 | |
| 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 | |
| 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] | |
| 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) | |
| 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) | |
| 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 | |
| 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 | |
| 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 "" | |
| 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)) | |