Spaces:
Paused
Paused
Download app.py from Rasheed/parrot-api: direct link, hf CLI and curl.
- Browser
- Download file 49.4 kB
-
https://huggingface.co/spaces/Rasheed/parrot-api/resolve/main/app.py
- Command line
-
hf download hf://spaces/Rasheed/parrot-api/app.py
-
curl -L -o app.py https://huggingface.co/spaces/Rasheed/parrot-api/resolve/main/app.py
49.4 kB
| import base64 | |
| import hashlib | |
| import hmac | |
| import secrets | |
| import sqlite3 | |
| import os | |
| import re | |
| import statistics | |
| import tempfile | |
| import threading | |
| import time | |
| from pathlib import Path | |
| import gradio as gr | |
| import spaces | |
| import torch | |
| from docx import Document | |
| from transformers import AutoModelForSeq2SeqLM, AutoTokenizer | |
| # --------------------------------------------------------------------------- | |
| # Brand (colours sampled from the CS Software Solutions logo) | |
| # --------------------------------------------------------------------------- | |
| BRAND = { | |
| "navy": "#07228B", | |
| "purple": "#47148B", | |
| "magenta": "#E92E7F", | |
| "red": "#E12B41", | |
| "orange": "#FE5828", | |
| "yellow": "#F5C518", | |
| "green": "#50BF49", | |
| "sky": "#50B4EF", | |
| "grey": "#4A4A4A", | |
| } | |
| RAINBOW = ( | |
| f"linear-gradient(90deg, {BRAND['purple']}, {BRAND['navy']}, {BRAND['sky']}, " | |
| f"{BRAND['green']}, {BRAND['yellow']}, {BRAND['orange']}, {BRAND['red']}, {BRAND['magenta']})" | |
| ) | |
| APP_NAME = "CS Academic Paraphraser" | |
| SITE_URL = "https://cssoftwaresolutions.in" | |
| LOGO_PATH = Path(__file__).parent / "logo.png" | |
| LOGO_SRC = "" | |
| FAVICON_PATH = Path(__file__).parent / "favicon.png" | |
| FAVICON_SRC = "" | |
| if FAVICON_PATH.exists(): | |
| FAVICON_SRC = "data:image/png;base64," + base64.b64encode(FAVICON_PATH.read_bytes()).decode() | |
| if LOGO_PATH.exists(): | |
| LOGO_SRC = "data:image/png;base64," + base64.b64encode(LOGO_PATH.read_bytes()).decode() | |
| # --------------------------------------------------------------------------- | |
| # Model (kept in CPU RAM while idle; moved to GPU only inside @spaces.GPU) | |
| # --------------------------------------------------------------------------- | |
| MODEL_ID = os.getenv("PARROT_MODEL", "prithivida/parrot_paraphraser_on_T5") | |
| tokenizer = AutoTokenizer.from_pretrained(MODEL_ID) | |
| model = AutoModelForSeq2SeqLM.from_pretrained(MODEL_ID).eval() | |
| model_lock = threading.Lock() | |
| GPU_CHUNK = 48 # sentences per ZeroGPU allocation (short reservations) | |
| CPU_CHUNK = 16 # sentences per CPU step (keeps progress updating) | |
| MODES = ["Auto (GPU, falls back to CPU)", "CPU only (no GPU quota)"] | |
| MAX_PASTE_WORDS = 6000 # keeps a single paste request within quota | |
| # --------------------------------------------------------------------------- | |
| # Text helpers | |
| # --------------------------------------------------------------------------- | |
| def split_sentences(text): | |
| parts = re.split(r"(?<=[.!?])\s+(?=[A-Z0-9\"'(\[])", text.strip()) | |
| return [p.strip() for p in parts if p.strip()] | |
| def should_process(text, min_words): | |
| text = (text or "").strip() | |
| if len(text.split()) < int(min_words): | |
| return False | |
| if re.fullmatch(r"https?://\S+|www\.\S+|\S+@\S+\.\S+", text): | |
| return False | |
| alpha = sum(ch.isalpha() for ch in text) | |
| if alpha < max(5, int(len(text) * 0.35)): | |
| return False | |
| return True | |
| def words(text): | |
| return re.findall(r"[A-Za-z0-9']+", (text or "").lower()) | |
| # --------------------------------------------------------------------------- | |
| # Tentative indicators (local heuristics — NOT Turnitin / iThenticate / GPTZero) | |
| # --------------------------------------------------------------------------- | |
| def _ngrams(tokens, n): | |
| return {tuple(tokens[i:i + n]) for i in range(len(tokens) - n + 1)} | |
| def similarity_to_source(original, revised): | |
| """Share of the revised text's word n-grams that also occur in the original.""" | |
| a, b = words(original), words(revised) | |
| if len(b) < 4 or len(a) < 4: | |
| return 0.0 | |
| scores, weights = [], [] | |
| for n, w in ((2, 0.2), (3, 0.35), (4, 0.45)): | |
| gb = _ngrams(b, n) | |
| if not gb: | |
| continue | |
| scores.append(len(gb & _ngrams(a, n)) / len(gb)) | |
| weights.append(w) | |
| if not weights: | |
| return 0.0 | |
| return 100.0 * sum(s * w for s, w in zip(scores, weights)) / sum(weights) | |
| AI_MARKERS = [ | |
| "delve", "delves", "delving", "furthermore", "moreover", "additionally", | |
| "in conclusion", "it is important to note", "it is worth noting", "plays a crucial role", | |
| "plays a pivotal role", "pivotal", "crucial", "comprehensive", "leverage", "leveraging", | |
| "seamless", "seamlessly", "robust", "landscape", "underscore", "underscores", | |
| "notably", "in today's", "fostering", "foster", "multifaceted", "intricate", | |
| "realm", "tapestry", "navigate", "navigating", "holistic", "paramount", | |
| "a testament to", "shed light", "sheds light", "ever-evolving", "cutting-edge", | |
| "in the realm of", "overall,", "ultimately", "enhance", "enhancing", "streamline", | |
| "valuable insights", "key role", "vital role", "significantly", | |
| ] | |
| def ai_likelihood(text): | |
| """Stylometric estimate (0–100) of machine-like writing.""" | |
| toks = words(text) | |
| if len(toks) < 30: | |
| return None | |
| sents = split_sentences(text) or [text] | |
| lengths = [len(words(s)) for s in sents if words(s)] | |
| # 1. Burstiness: humans vary sentence length more than LLMs. | |
| if len(lengths) >= 3: | |
| cv = statistics.pstdev(lengths) / max(1.0, statistics.mean(lengths)) | |
| burst = min(1.0, max(0.0, (0.60 - cv) / 0.45)) | |
| else: | |
| burst = 0.5 | |
| # 2. Density of stock "LLM" vocabulary per 100 words. | |
| low = " " + " ".join(toks) + " " | |
| raw = (text or "").lower() | |
| hits = sum(raw.count(m) if " " in m or "," in m else low.count(f" {m} ") for m in AI_MARKERS) | |
| marker = min(1.0, (hits / len(toks) * 100) / 2.5) | |
| # 3. Uniformity: mid-length sentences, repeated openers, low lexical variety. | |
| mean_len = statistics.mean(lengths) | |
| band = 1.0 if 16 <= mean_len <= 28 else 0.4 | |
| openers = [words(s)[0] for s in sents if words(s)] | |
| repeat = 1 - len(set(openers)) / len(openers) if openers else 0 | |
| window = toks[:400] | |
| ttr = len(set(window)) / len(window) | |
| variety = min(1.0, max(0.0, (0.62 - ttr) / 0.25)) | |
| uniform = 0.45 * band + 0.3 * min(1.0, repeat * 2) + 0.25 * variety | |
| score = 100 * (0.40 * burst + 0.35 * marker + 0.25 * uniform) | |
| return float(max(3.0, min(97.0, score))) | |
| def words_changed(original, revised): | |
| a, b = words(original), words(revised) | |
| if not b: | |
| return 0.0 | |
| return 100.0 * (1 - len(set(b) & set(a)) / len(set(b))) | |
| def _level(value, invert=False): | |
| if value is None: | |
| return "#9aa0a6", "n/a" | |
| v = 100 - value if invert else value | |
| if v < 30: | |
| return BRAND["green"], "Low" | |
| if v < 55: | |
| return BRAND["orange"], "Moderate" | |
| return BRAND["red"], "High" | |
| def _gauge(title, value, caption, invert=False, fixed_color=None): | |
| color, label = _level(value, invert) | |
| if fixed_color: | |
| color = fixed_color | |
| label = "Rewrite depth" | |
| pct = 0 if value is None else round(value) | |
| shown = "—" if value is None else f"{pct}%" | |
| return f""" | |
| <div class="cs-gauge"> | |
| <div class="cs-ring" style="--p:{pct};--c:{color}"> | |
| <div class="cs-ring-inner"><span>{shown}</span></div> | |
| </div> | |
| <div class="cs-gauge-title">{title}</div> | |
| <div class="cs-chip" style="background:{color}1f;color:{color}">{label}</div> | |
| <div class="cs-gauge-cap">{caption}</div> | |
| </div>""" | |
| def score_panel(original, revised): | |
| sim = similarity_to_source(original, revised) | |
| ai_in = ai_likelihood(original) | |
| ai_out = ai_likelihood(revised) | |
| changed = words_changed(original, revised) | |
| return f""" | |
| <div class="cs-scores"> | |
| {_gauge("Similarity to source", sim, "Word n-gram overlap with your original")} | |
| {_gauge("AI indicator · input", ai_in, "Stylometric estimate of the original")} | |
| {_gauge("AI indicator · output", ai_out, "Stylometric estimate after paraphrasing")} | |
| {_gauge("Vocabulary changed", changed, "Share of new wording", fixed_color=BRAND["navy"])} | |
| </div> | |
| <div class="cs-disclaimer"> | |
| <b>Tentative indicators only.</b> Similarity is measured against your own original text, | |
| not against internet or journal databases, and the AI indicator is a stylometric heuristic. | |
| Results will differ from Turnitin, iThenticate or other detectors. Always review the | |
| output for meaning, technical terms and citations before use. | |
| </div>""" | |
| EMPTY_SCORES = """ | |
| <div class="cs-scores cs-empty"> | |
| <div>Scores appear here after paraphrasing — similarity to source, AI indicators and rewrite depth.</div> | |
| </div>""" | |
| # --------------------------------------------------------------------------- | |
| # DOCX structure helpers | |
| # --------------------------------------------------------------------------- | |
| def iter_table_paragraphs(table): | |
| for row in table.rows: | |
| for cell in row.cells: | |
| for paragraph in cell.paragraphs: | |
| yield paragraph | |
| for nested_table in cell.tables: | |
| yield from iter_table_paragraphs(nested_table) | |
| def collect_paragraphs(doc): | |
| seen = set() | |
| def unique(paragraphs): | |
| for paragraph in paragraphs: | |
| key = id(paragraph._p) | |
| if key not in seen: | |
| seen.add(key) | |
| yield paragraph | |
| yield from unique(doc.paragraphs) | |
| for table in doc.tables: | |
| yield from unique(iter_table_paragraphs(table)) | |
| for section in doc.sections: | |
| for part in ( | |
| section.header, section.footer, | |
| section.first_page_header, section.first_page_footer, | |
| section.even_page_header, section.even_page_footer, | |
| ): | |
| yield from unique(part.paragraphs) | |
| for table in part.tables: | |
| yield from unique(iter_table_paragraphs(table)) | |
| def _is_protected(run): | |
| return bool( | |
| run._r.xpath(".//w:drawing") | |
| or run._r.xpath(".//w:object") | |
| or run._r.xpath(".//w:fldChar") | |
| ) | |
| def replace_text_preserving_paragraph(paragraph, new_text): | |
| runs = list(paragraph.runs) | |
| if not runs: | |
| paragraph.add_run(new_text) | |
| return | |
| target = next((r for r in runs if not _is_protected(r)), None) | |
| if target is None: | |
| target = paragraph.add_run() | |
| target.text = new_text | |
| for run in runs: | |
| if run is not target and not _is_protected(run): | |
| run.text = "" | |
| # --------------------------------------------------------------------------- | |
| # Paraphrasing | |
| # --------------------------------------------------------------------------- | |
| def _generate(texts, temperature, device, batch_size): | |
| temperature = max(0.3, min(float(temperature), 1.5)) | |
| outputs = [] | |
| with torch.inference_mode(): | |
| for start in range(0, len(texts), batch_size): | |
| chunk = texts[start:start + batch_size] | |
| encoded = tokenizer( | |
| ["paraphrase: " + t for t in chunk], | |
| return_tensors="pt", padding=True, truncation=True, max_length=256, | |
| ) | |
| encoded = {k: v.to(device) for k, v in encoded.items()} | |
| generated = model.generate( | |
| **encoded, | |
| do_sample=True, | |
| temperature=temperature, | |
| top_p=0.92, | |
| num_return_sequences=1, | |
| max_new_tokens=180, | |
| no_repeat_ngram_size=3, | |
| ) | |
| decoded = tokenizer.batch_decode(generated.detach().cpu(), skip_special_tokens=True) | |
| outputs.extend(c.strip() if c.strip() else s for s, c in zip(chunk, decoded)) | |
| del encoded, generated | |
| return outputs | |
| def _gpu_duration(texts, temperature): | |
| # Reserve only what this chunk needs, so visitors' daily ZeroGPU quota lasts longer. | |
| return int(min(90, 12 + 0.3 * len(texts or []))) | |
| def paraphrase_batch_gpu(texts, temperature): | |
| if not texts: | |
| return [] | |
| if not torch.cuda.is_available(): | |
| raise RuntimeError("GPU allocation did not expose CUDA.") | |
| with model_lock: | |
| model.to("cuda") | |
| try: | |
| return _generate(texts, temperature, "cuda", batch_size=16) | |
| finally: | |
| model.to("cpu") | |
| torch.cuda.empty_cache() | |
| def paraphrase_batch_cpu(texts, temperature): | |
| if not texts: | |
| return [] | |
| with model_lock: | |
| model.to("cpu") | |
| return _generate(texts, temperature, "cpu", batch_size=4) | |
| def _is_gpu_unavailable(err): | |
| msg = str(err).lower() | |
| return any(k in msg for k in ("quota", "zerogpu", "gpu task aborted", "cuda", "no gpu", "queue is full")) | |
| def paraphrase_paragraphs(texts, temperature, progress=None, lo=0.15, hi=0.85, mode="Auto"): | |
| """Returns (paragraphs, engine_note).""" | |
| groups, flat = [], [] | |
| for text in texts: | |
| sents = split_sentences(text) or [text] | |
| start = len(flat) | |
| flat.extend(sents) | |
| groups.append((start, len(flat))) | |
| use_gpu = not str(mode).lower().startswith("cpu") | |
| fell_back = False | |
| rewritten = [] | |
| total = max(1, len(flat)) | |
| step = GPU_CHUNK if use_gpu else CPU_CHUNK | |
| i = 0 | |
| while i < len(flat): | |
| chunk = flat[i:i + step] | |
| engine = "GPU" if use_gpu else "CPU" | |
| if progress is not None: | |
| progress(lo + (hi - lo) * i / total, | |
| desc=f"Paraphrasing sentences {i + 1}–{i + len(chunk)} of {total} ({engine})") | |
| if use_gpu: | |
| try: | |
| rewritten.extend(paraphrase_batch_gpu(chunk, temperature)) | |
| except Exception as err: | |
| if not _is_gpu_unavailable(err): | |
| raise | |
| use_gpu, fell_back, step = False, True, CPU_CHUNK | |
| continue # retry the same chunk on CPU | |
| else: | |
| rewritten.extend(paraphrase_batch_cpu(chunk, temperature)) | |
| i += len(chunk) | |
| if fell_back: | |
| note = "⚠️ GPU quota was unavailable, so the remaining text was processed on CPU (slower, same quality)." | |
| elif not str(mode).lower().startswith("cpu"): | |
| note = "⚡ Processed on GPU." | |
| else: | |
| note = "🖥️ Processed on CPU." | |
| return [" ".join(rewritten[s:e]) for s, e in groups], note | |
| def _preview(pairs, limit=10): | |
| blocks = [] | |
| for i, (before, after) in enumerate(pairs[:limit], 1): | |
| blocks.append( | |
| f"#### Change {i}\n**Original:** {before}\n\n**Paraphrased:** {after}" | |
| ) | |
| return "\n\n---\n\n".join(blocks) if blocks else "No textual changes were generated." | |
| # --------------------------------------------------------------------------- | |
| # Handlers | |
| # --------------------------------------------------------------------------- | |
| def _docx_core(file_path, temperature, min_words, mode, progress): | |
| if not file_path: | |
| raise gr.Error("Please upload a .docx file.") | |
| source_path = Path(file_path) | |
| if source_path.suffix.lower() != ".docx": | |
| raise gr.Error("Only .docx Word documents are supported.") | |
| progress(0.05, desc="Opening Word document") | |
| doc = Document(str(source_path)) | |
| targets, originals = [], [] | |
| for paragraph in collect_paragraphs(doc): | |
| text = paragraph.text | |
| if should_process(text, min_words): | |
| targets.append(paragraph) | |
| originals.append(text) | |
| if not originals: | |
| raise gr.Error("No eligible prose paragraphs were found in the document.") | |
| revised, engine_note = paraphrase_paragraphs(originals, temperature, progress, mode=mode) | |
| progress(0.9, desc="Injecting revised text into the original layout") | |
| changed, pairs, final_after = 0, [], [] | |
| for paragraph, before, after in zip(targets, originals, revised): | |
| after = (after or "").strip() | |
| if after and after != before.strip(): | |
| replace_text_preserving_paragraph(paragraph, after) | |
| changed += 1 | |
| pairs.append((before, after)) | |
| final_after.append(after) | |
| else: | |
| final_after.append(before) | |
| output_path = _save_output(doc, source_path.stem) | |
| progress(1.0, desc="Completed") | |
| summary = ( | |
| f"✅ **Completed** — {changed} of {len(originals)} eligible paragraphs rewritten. " | |
| f"Layout, styles, tables, images and headers/footers were kept in place.\n\n{engine_note}" | |
| ) | |
| scores = score_panel("\n".join(originals), "\n".join(final_after)) | |
| return output_path, download_button(output_path), summary, scores, _preview(pairs) | |
| def _text_core(text, temperature, min_words, mode, progress): | |
| text = (text or "").strip() | |
| if not text: | |
| raise gr.Error("Please paste some text to paraphrase.") | |
| n_words = len(text.split()) | |
| if n_words > MAX_PASTE_WORDS: | |
| raise gr.Error( | |
| f"Pasted text has {n_words} words. Please keep it under {MAX_PASTE_WORDS} words, " | |
| "or upload it as a .docx instead." | |
| ) | |
| progress(0.05, desc="Reading pasted text") | |
| paragraphs = [p.strip() for p in re.split(r"\n\s*\n|\r?\n", text)] | |
| idx = [i for i, p in enumerate(paragraphs) if should_process(p, min_words)] | |
| if not idx: | |
| raise gr.Error(f"No paragraph has at least {int(min_words)} words of prose. Lower the minimum and try again.") | |
| revised, engine_note = paraphrase_paragraphs([paragraphs[i] for i in idx], temperature, progress, mode=mode) | |
| result = list(paragraphs) | |
| pairs = [] | |
| for i, after in zip(idx, revised): | |
| after = (after or "").strip() | |
| if after and after != paragraphs[i]: | |
| result[i] = after | |
| pairs.append((paragraphs[i], after)) | |
| output_text = "\n\n".join(p for p in result if p) | |
| progress(0.95, desc="Preparing download") | |
| doc = Document() | |
| for p in result: | |
| if p: | |
| doc.add_paragraph(p) | |
| output_path = _save_output(doc, "pasted_text") | |
| progress(1.0, desc="Completed") | |
| summary = f"✅ **Completed** — {len(pairs)} of {len(idx)} eligible paragraphs rewritten ({n_words} words in).\n\n{engine_note}" | |
| scores = score_panel( | |
| "\n".join(paragraphs[i] for i in idx), | |
| "\n".join(result[i] for i in idx), | |
| ) | |
| return output_text, output_path, download_button(output_path), summary, scores | |
| # --------------------------------------------------------------------------- | |
| # Accounts (email + password, PBKDF2-hashed, SQLite) | |
| # Stored in /data when persistent storage is enabled; otherwise next to app.py. | |
| # --------------------------------------------------------------------------- | |
| _DATA_DIR = Path("/data") if Path("/data").is_dir() and os.access("/data", os.W_OK) else Path(__file__).parent | |
| USERS_DB = Path(os.getenv("USERS_DB", str(_DATA_DIR / "users.db"))) | |
| _EMAIL_RE = re.compile(r"^[^@\s]+@[^@\s]+\.[^@\s]+$") | |
| def _db(): | |
| con = sqlite3.connect(str(USERS_DB), timeout=10) | |
| con.execute( | |
| "CREATE TABLE IF NOT EXISTS users (email TEXT PRIMARY KEY, name TEXT, salt TEXT, " | |
| "pw TEXT, created TEXT DEFAULT CURRENT_TIMESTAMP, last_login TEXT, runs INTEGER DEFAULT 0)" | |
| ) | |
| cols = {r[1] for r in con.execute("PRAGMA table_info(users)")} | |
| if "active" not in cols: | |
| con.execute("ALTER TABLE users ADD COLUMN active INTEGER DEFAULT 1") | |
| return con | |
| # Admins: comma-separated emails in the ADMIN_EMAILS secret (Space settings → Variables and secrets). | |
| ADMIN_EMAILS = {e.strip().lower() for e in os.getenv("ADMIN_EMAILS", "").split(",") if e.strip()} | |
| def is_admin(user): | |
| return bool(user and user.get("email", "").lower() in ADMIN_EMAILS) | |
| def _hash(password, salt): | |
| return hashlib.pbkdf2_hmac("sha256", password.encode(), bytes.fromhex(salt), 200_000).hex() | |
| def create_user(name, email, password, confirm): | |
| name, email = (name or "").strip(), (email or "").strip().lower() | |
| if not name: | |
| return None, "Please enter your name." | |
| if not _EMAIL_RE.match(email): | |
| return None, "Please enter a valid email address." | |
| if len(password or "") < 8: | |
| return None, "Password must be at least 8 characters." | |
| if password != confirm: | |
| return None, "Passwords do not match." | |
| salt = secrets.token_hex(16) | |
| with _db() as con: | |
| if con.execute("SELECT 1 FROM users WHERE email=?", (email,)).fetchone(): | |
| return None, "An account with this email already exists. Please log in." | |
| con.execute("INSERT INTO users (email, name, salt, pw, last_login) VALUES (?,?,?,?,CURRENT_TIMESTAMP)", | |
| (email, name, salt, _hash(password, salt))) | |
| return {"email": email, "name": name}, None | |
| def check_user(email, password): | |
| email = (email or "").strip().lower() | |
| with _db() as con: | |
| row = con.execute("SELECT name, salt, pw, active FROM users WHERE email=?", (email,)).fetchone() | |
| if not row or not hmac.compare_digest(row[2], _hash(password or "", row[1])): | |
| return None, "Incorrect email or password." | |
| if row[3] == 0: | |
| return None, "This account has been disabled. Please contact CS Software Solutions." | |
| con.execute("UPDATE users SET last_login=CURRENT_TIMESTAMP WHERE email=?", (email,)) | |
| return {"email": email, "name": row[0]}, None | |
| def _count_run(user): | |
| try: | |
| with _db() as con: | |
| con.execute("UPDATE users SET runs = runs + 1 WHERE email=?", (user["email"],)) | |
| except Exception: | |
| pass | |
| def _require(user): | |
| if not user or not user.get("email"): | |
| raise gr.Error("Please log in to use the paraphraser.") | |
| with _db() as con: | |
| row = con.execute("SELECT active FROM users WHERE email=?", (user["email"],)).fetchone() | |
| if not row or row[0] == 0: | |
| raise gr.Error("This account has been disabled.") | |
| def _msg(text, ok=False): | |
| cls = "cs-ok" if ok else "cs-bad" | |
| return f'<div class="cs-authmsg {cls}">{text}</div>' if text else "" | |
| def _welcome(user): | |
| return (f'<div class="cs-userbar"><span>👋 Signed in as <b>{user["name"]}</b> ' | |
| f'<small>({user["email"]})</small></span></div>') | |
| def _signed_in(user): | |
| admin = is_admin(user) | |
| return (user, gr.update(visible=False), gr.update(visible=True), _welcome(user), "", | |
| gr.update(visible=admin), admin_stats(user) if admin else "", admin_table(user) if admin else None) | |
| def do_login(email, password): | |
| user, err = check_user(email, password) | |
| if err: | |
| return None, gr.update(), gr.update(), "", _msg(err), gr.update(visible=False), "", None | |
| return _signed_in(user) | |
| def do_signup(name, email, password, confirm): | |
| user, err = create_user(name, email, password, confirm) | |
| if err: | |
| return None, gr.update(), gr.update(), "", _msg(err), gr.update(visible=False), "", None | |
| return _signed_in(user) | |
| def do_logout(): | |
| return (None, gr.update(visible=True), gr.update(visible=False), "", | |
| _msg("You have been logged out.", ok=True), gr.update(visible=False), "", None) | |
| # --------------------------------------------------------------------------- | |
| # Admin panel | |
| # --------------------------------------------------------------------------- | |
| ADMIN_COLS = ["Name", "Email", "Joined", "Last login", "Runs", "Status", "Role"] | |
| def _need_admin(user): | |
| if not is_admin(user): | |
| raise gr.Error("Admin access only.") | |
| def admin_stats(user): | |
| _need_admin(user) | |
| with _db() as con: | |
| total = con.execute("SELECT COUNT(*) FROM users").fetchone()[0] | |
| active = con.execute("SELECT COUNT(*) FROM users WHERE active=1").fetchone()[0] | |
| runs = con.execute("SELECT COALESCE(SUM(runs),0) FROM users").fetchone()[0] | |
| new7 = con.execute("SELECT COUNT(*) FROM users WHERE created >= datetime('now','-7 days')").fetchone()[0] | |
| seen7 = con.execute("SELECT COUNT(*) FROM users WHERE last_login >= datetime('now','-7 days')").fetchone()[0] | |
| tiles = [("Total users", total), ("Active accounts", active), ("Total runs", runs), | |
| ("New (7 days)", new7), ("Logged in (7 days)", seen7)] | |
| cells = "".join(f'<div class="cs-stat"><div class="n">{v:,}</div><div class="l">{k}</div></div>' for k, v in tiles) | |
| return f'<div class="cs-stats">{cells}</div>' | |
| def admin_table(user, query=""): | |
| _need_admin(user) | |
| q = f"%{(query or '').strip().lower()}%" | |
| with _db() as con: | |
| rows = con.execute( | |
| "SELECT name, email, created, last_login, runs, active FROM users " | |
| "WHERE lower(name) LIKE ? OR lower(email) LIKE ? ORDER BY created DESC", (q, q)).fetchall() | |
| return [[n, e, (c or "")[:16], (l or "")[:16], r, "Active" if a else "Disabled", | |
| "Admin" if e in ADMIN_EMAILS else "User"] for n, e, c, l, r, a in rows] | |
| def _admin_refresh(user, query, note, ok=True): | |
| return admin_stats(user), admin_table(user, query), _msg(note, ok=ok) | |
| def admin_search(user, query): | |
| _need_admin(user) | |
| return admin_table(user, query) | |
| def admin_set_active(user, email, query, active): | |
| _need_admin(user) | |
| email = (email or "").strip().lower() | |
| if not email: | |
| return _admin_refresh(user, query, "Enter the user's email first.", ok=False) | |
| if email in ADMIN_EMAILS and not active: | |
| return _admin_refresh(user, query, "Admin accounts can't be disabled here.", ok=False) | |
| with _db() as con: | |
| n = con.execute("UPDATE users SET active=? WHERE email=?", (1 if active else 0, email)).rowcount | |
| if not n: | |
| return _admin_refresh(user, query, f"No user found with {email}.", ok=False) | |
| return _admin_refresh(user, query, f"{email} is now {'active' if active else 'disabled'}.") | |
| def admin_reset_pw(user, email, new_pw, query): | |
| _need_admin(user) | |
| email = (email or "").strip().lower() | |
| if len(new_pw or "") < 8: | |
| return _admin_refresh(user, query, "New password must be at least 8 characters.", ok=False) | |
| salt = secrets.token_hex(16) | |
| with _db() as con: | |
| n = con.execute("UPDATE users SET salt=?, pw=? WHERE email=?", (salt, _hash(new_pw, salt), email)).rowcount | |
| if not n: | |
| return _admin_refresh(user, query, f"No user found with {email}.", ok=False) | |
| return _admin_refresh(user, query, f"Password reset for {email}. Share it with them securely.") | |
| def admin_delete(user, email, confirm, query): | |
| _need_admin(user) | |
| email = (email or "").strip().lower() | |
| if (confirm or "").strip().lower() != email or not email: | |
| return _admin_refresh(user, query, "Type the same email in the confirm box to delete.", ok=False) | |
| if email in ADMIN_EMAILS: | |
| return _admin_refresh(user, query, "Admin accounts can't be deleted here.", ok=False) | |
| with _db() as con: | |
| n = con.execute("DELETE FROM users WHERE email=?", (email,)).rowcount | |
| if not n: | |
| return _admin_refresh(user, query, f"No user found with {email}.", ok=False) | |
| return _admin_refresh(user, query, f"Deleted {email}.") | |
| def admin_export(user): | |
| _need_admin(user) | |
| import csv | |
| folder = Path(tempfile.mkdtemp(dir=OUT_DIR)) | |
| path = folder / "users.csv" | |
| with open(path, "w", newline="", encoding="utf-8") as f: | |
| w = csv.writer(f) | |
| w.writerow(ADMIN_COLS) | |
| w.writerows(admin_table(user)) | |
| return str(path) | |
| # --------------------------------------------------------------------------- | |
| # Single branded progress panel (replaces Gradio's per-component loaders) | |
| # --------------------------------------------------------------------------- | |
| class _Reporter: | |
| def __init__(self): | |
| self.frac, self.desc, self.start = 0.0, "Starting…", time.time() | |
| def __call__(self, frac, desc=None): | |
| self.frac = max(self.frac, min(1.0, float(frac))) | |
| if desc: | |
| self.desc = desc | |
| def _md2html(md): | |
| html = re.sub(r"\*\*(.+?)\*\*", r"<b>\1</b>", md) | |
| return html.replace("\n\n", "<br><br>").replace("\n", "<br>") | |
| def status_idle(msg): | |
| return f'<div class="cs-status cs-idle">{_md2html(msg)}</div>' | |
| def status_done(msg): | |
| return f'<div class="cs-status cs-done">{_md2html(msg)}</div>' | |
| def status_error(msg): | |
| return f'<div class="cs-status cs-err">⚠️ {msg}</div>' | |
| def status_running(rep): | |
| pct = round(rep.frac * 100) | |
| secs = int(time.time() - rep.start) | |
| return f""" | |
| <div class="cs-status cs-run"> | |
| <div class="cs-spin-logo"></div> | |
| <div class="cs-st-text">{rep.desc}</div> | |
| <div class="cs-bar"><div style="width:{pct}%"></div></div> | |
| <div class="cs-st-meta">{pct}% · {secs}s</div> | |
| </div>""" | |
| def _run_live(core, args, n_outputs, status_index): | |
| """Run `core` in a thread and stream only the status panel until it finishes.""" | |
| rep = _Reporter() | |
| box = {} | |
| def work(): | |
| try: | |
| box["result"] = core(*args, rep) | |
| except Exception as err: # re-raised in the request thread | |
| box["error"] = err | |
| t = threading.Thread(target=work, daemon=True) | |
| t.start() | |
| keep = [gr.update()] * n_outputs | |
| while t.is_alive(): | |
| frame = list(keep) | |
| frame[status_index] = status_running(rep) | |
| yield tuple(frame) | |
| t.join(timeout=0.6) | |
| if "error" in box: | |
| err = box["error"] | |
| frame = list(keep) | |
| frame[status_index] = status_error(str(err) or "Something went wrong.") | |
| yield tuple(frame) | |
| if isinstance(err, gr.Error): | |
| raise err | |
| raise gr.Error(f"Processing failed: {err}") | |
| yield box["result"] | |
| def process_docx(file_path, temperature, min_words, mode=MODES[0], user=None): | |
| _require(user) | |
| _count_run(user) | |
| if not file_path: | |
| raise gr.Error("Please upload a .docx file.") | |
| for frame in _run_live(_docx_core, (file_path, temperature, min_words, mode), 5, 2): | |
| if isinstance(frame[2], str) and frame[2].lstrip().startswith("✅"): | |
| frame = (frame[0], frame[1], status_done(frame[2]), frame[3], frame[4]) | |
| yield frame | |
| def process_text(text, temperature, min_words, mode=MODES[0], user=None): | |
| _require(user) | |
| _count_run(user) | |
| if not (text or "").strip(): | |
| raise gr.Error("Please paste some text to paraphrase.") | |
| for frame in _run_live(_text_core, (text, temperature, min_words, mode), 5, 3): | |
| if isinstance(frame[3], str) and frame[3].lstrip().startswith("✅"): | |
| frame = (frame[0], frame[1], frame[2], status_done(frame[3]), frame[4]) | |
| yield frame | |
| OUT_DIR = Path(tempfile.gettempdir()) / "cs_outputs" | |
| OUT_DIR.mkdir(parents=True, exist_ok=True) | |
| def _save_output(doc, stem): | |
| """Save to a unique folder with a clean, readable file name.""" | |
| safe = re.sub(r"[^A-Za-z0-9_-]+", "_", stem).strip("_") or "document" | |
| folder = Path(tempfile.mkdtemp(dir=OUT_DIR)) | |
| path = folder / f"{safe}_paraphrased.docx" | |
| doc.save(str(path)) | |
| return str(path) | |
| def download_button(path): | |
| """Direct download link (works even if the file widget is blocked).""" | |
| if not path or not Path(path).exists(): | |
| return "" | |
| data = base64.b64encode(Path(path).read_bytes()).decode() | |
| mime = "application/vnd.openxmlformats-officedocument.wordprocessingml.document" | |
| name = Path(path).name | |
| return (f'<a class="cs-dl" download="{name}" href="data:{mime};base64,{data}">' | |
| f'⬇️ Download {name}</a>') | |
| def count_words(text): | |
| n = len((text or "").split()) | |
| color = BRAND["red"] if n > MAX_PASTE_WORDS else BRAND["grey"] | |
| return f"<div class='cs-count' style='color:{color}'>{n:,} / {MAX_PASTE_WORDS:,} words</div>" | |
| # --------------------------------------------------------------------------- | |
| # UI | |
| # --------------------------------------------------------------------------- | |
| CSS = f""" | |
| :root {{ | |
| --cs-navy:{BRAND['navy']}; --cs-purple:{BRAND['purple']}; --cs-magenta:{BRAND['magenta']}; | |
| --cs-sky:{BRAND['sky']}; --cs-rainbow:{RAINBOW}; | |
| }} | |
| .gradio-container {{ max-width: 1180px !important; margin: 0 auto !important; | |
| font-family: 'Poppins', ui-sans-serif, system-ui, sans-serif !important; }} | |
| body, .gradio-container {{ background: #f6f7fb !important; }} | |
| .cs-header {{ display:flex; align-items:center; gap:22px; padding:22px 26px; border-radius:20px; | |
| background:#fff; box-shadow:0 8px 30px rgba(7,34,139,.08); position:relative; overflow:hidden; }} | |
| .cs-header::after {{ content:""; position:absolute; left:0; right:0; bottom:0; height:5px; background:var(--cs-rainbow); }} | |
| .cs-header img {{ width:92px; height:92px; border-radius:50%; flex:none; | |
| box-shadow:0 4px 14px rgba(0,0,0,.12); }} | |
| .cs-header h1 {{ margin:0; font-size:30px; font-weight:800; color:var(--cs-navy); letter-spacing:-.3px; line-height:1.15; }} | |
| .cs-header h1 span {{ color:var(--cs-magenta) !important; }} | |
| .cs-header p {{ margin:6px 0 0; color:#555; font-size:14.5px; }} | |
| .cs-tag {{ margin-top:8px; font-size:11.5px; letter-spacing:3px; color:#444; font-weight:600; }} | |
| .cs-tag b {{ color:var(--cs-magenta); }} .cs-tag i {{ color:var(--cs-navy); font-style:normal; }} | |
| @media (max-width:640px) {{ .cs-header {{ flex-direction:column; text-align:center; }} .cs-header h1 {{ font-size:24px; }} }} | |
| .cs-card {{ background:#fff !important; border-radius:18px !important; padding:18px !important; | |
| box-shadow:0 6px 24px rgba(7,34,139,.06) !important; border:1px solid #eceef6 !important; }} | |
| button.primary, .cs-go {{ background:linear-gradient(90deg,var(--cs-navy),var(--cs-purple) 60%,var(--cs-magenta)) !important; | |
| color:#fff !important; border:none !important; font-weight:700 !important; font-size:16px !important; | |
| border-radius:12px !important; box-shadow:0 6px 18px rgba(71,20,139,.28) !important; }} | |
| button.primary:hover {{ filter:brightness(1.08); transform:translateY(-1px); }} | |
| .tabs button[role="tab"] {{ font-weight:600 !important; font-size:15px !important; }} | |
| .tabs button[role="tab"].selected {{ color:var(--cs-navy) !important; border-color:var(--cs-magenta) !important; }} | |
| .cs-scores {{ display:grid; grid-template-columns:repeat(4,1fr); gap:14px; }} | |
| @media (max-width:820px) {{ .cs-scores {{ grid-template-columns:repeat(2,1fr); }} }} | |
| .cs-scores.cs-empty {{ display:block; text-align:center; color:#888; padding:26px; border:2px dashed #e3e6f2; border-radius:16px; }} | |
| .cs-gauge {{ background:#fff; border:1px solid #eceef6; border-radius:16px; padding:16px 10px; text-align:center; | |
| box-shadow:0 4px 16px rgba(7,34,139,.05); }} | |
| .cs-ring {{ width:104px; height:104px; margin:0 auto 10px; border-radius:50%; | |
| background:conic-gradient(var(--c) calc(var(--p)*1%), #edf0f7 0); display:grid; place-items:center; }} | |
| .cs-ring-inner {{ width:80px; height:80px; border-radius:50%; background:#fff; display:grid; place-items:center; }} | |
| .cs-ring-inner span {{ font-size:22px; font-weight:800; color:#222; }} | |
| .cs-gauge-title {{ font-weight:700; color:var(--cs-navy); font-size:14px; }} | |
| .cs-chip {{ display:inline-block; margin:6px 0; padding:2px 12px; border-radius:99px; font-size:12px; font-weight:700; }} | |
| .cs-gauge-cap {{ font-size:11.5px; color:#777; line-height:1.35; }} | |
| .cs-disclaimer {{ margin-top:12px; font-size:12px; color:#666; background:#fff8ec; border-left:4px solid {BRAND['orange']}; | |
| padding:10px 14px; border-radius:10px; }} | |
| .cs-count {{ text-align:right; font-size:12px; margin-top:-6px; }} | |
| .cs-footer {{ text-align:center; margin-top:18px; padding:16px; color:#666; font-size:13px; }} | |
| .cs-footer .bar {{ height:3px; width:220px; margin:0 auto 12px; border-radius:3px; background:var(--cs-rainbow); }} | |
| .cs-footer a {{ color:var(--cs-navy); font-weight:700; text-decoration:none; }} | |
| footer {{ display:none !important; }} | |
| /* Custom single progress panel; Gradio's own loaders are switched off. */ | |
| .cs-status {{ padding:16px 18px; border-radius:14px; font-size:14.5px; line-height:1.5; }} | |
| .cs-idle {{ color:#666; }} | |
| .cs-done {{ background:#f1fbf1; border-left:4px solid {BRAND['green']}; color:#234; }} | |
| .cs-err {{ background:#fff3f3; border-left:4px solid {BRAND['red']}; color:#8a1c1c; }} | |
| .cs-run {{ text-align:center; background:#fff; border:1px solid #eceef6; }} | |
| .cs-spin-logo {{ width:64px; height:64px; margin:4px auto 12px; | |
| background:url("{FAVICON_SRC}") center/contain no-repeat; animation:cs-spin 1.4s linear infinite; }} | |
| .cs-st-text {{ font-weight:600; color:var(--cs-navy); margin-bottom:10px; }} | |
| .cs-bar {{ height:10px; border-radius:99px; background:#edf0f7; overflow:hidden; }} | |
| .cs-bar > div {{ height:100%; border-radius:99px; background:var(--cs-rainbow); transition:width .5s ease; }} | |
| .cs-st-meta {{ margin-top:8px; font-size:12px; color:#777; }} | |
| @keyframes cs-spin {{ to {{ transform:rotate(360deg); }} }} | |
| #huggingface-space-header, .show-api {{ display:none !important; }} | |
| .cs-dl {{ display:block; text-align:center; margin-top:10px; padding:12px 16px; border-radius:12px; font-weight:700; | |
| color:#fff !important; text-decoration:none !important; | |
| background:linear-gradient(90deg,var(--cs-navy),var(--cs-purple) 60%,var(--cs-magenta)); }} | |
| .cs-dl:hover {{ filter:brightness(1.08); }} | |
| .cs-settings {{ background:#f8f9fd !important; border:1px solid #eceef6 !important; border-radius:14px !important; | |
| padding:12px 14px !important; margin:10px 0 !important; }} | |
| .cs-settings-title {{ font-weight:700; color:var(--cs-navy); font-size:15px; margin:0 0 4px; }} | |
| .cs-stats {{ display:grid; grid-template-columns:repeat(5,1fr); gap:12px; }} | |
| @media (max-width:820px) {{ .cs-stats {{ grid-template-columns:repeat(2,1fr); }} }} | |
| .cs-stat {{ background:#fff; border:1px solid #eceef6; border-radius:14px; padding:14px; text-align:center; | |
| box-shadow:0 4px 16px rgba(7,34,139,.05); }} | |
| .cs-stat .n {{ font-size:26px; font-weight:800; color:var(--cs-navy); }} | |
| .cs-stat .l {{ font-size:12px; color:#777; margin-top:2px; }} | |
| .cs-auth {{ max-width:460px; margin:26px auto 0 !important; }} | |
| .cs-auth-title {{ text-align:center; font-weight:800; font-size:22px; color:var(--cs-navy); margin:4px 0 2px; }} | |
| .cs-auth-sub {{ text-align:center; color:#666; font-size:13.5px; margin-bottom:10px; }} | |
| .cs-authmsg {{ padding:10px 14px; border-radius:10px; font-size:13.5px; margin-top:6px; }} | |
| .cs-ok {{ background:#f1fbf1; color:#1e5b1e; border-left:4px solid {BRAND['green']}; }} | |
| .cs-bad {{ background:#fff3f3; color:#8a1c1c; border-left:4px solid {BRAND['red']}; }} | |
| .cs-userbar {{ display:flex; align-items:center; padding:10px 4px; color:#444; font-size:14px; }} | |
| .cs-userbar small {{ color:#888; }} | |
| .cs-logout {{ max-width:130px !important; margin-left:auto !important; }} | |
| """ | |
| FORCE_LIGHT = """ | |
| () => { | |
| const url = new URL(window.location); | |
| if (url.searchParams.get('__theme') !== 'light') { | |
| url.searchParams.set('__theme', 'light'); | |
| window.location.href = url.href; | |
| return; | |
| } | |
| // Hide the platform's floating Space badge (it can be injected after load). | |
| const hide = () => { | |
| const h = document.getElementById('huggingface-space-header'); | |
| if (h) h.style.setProperty('display', 'none', 'important'); | |
| }; | |
| hide(); | |
| new MutationObserver(hide).observe(document.documentElement, { childList: true, subtree: true }); | |
| // Brand favicon (replaces the platform's default tab icon). | |
| const ICON = "__FAVICON__"; | |
| if (ICON) { | |
| document.querySelectorAll("link[rel*='icon']").forEach(l => l.remove()); | |
| const link = document.createElement('link'); | |
| link.rel = 'icon'; link.type = 'image/png'; link.href = ICON; | |
| document.head.appendChild(link); | |
| } | |
| } | |
| """ | |
| FORCE_LIGHT = FORCE_LIGHT.replace("__FAVICON__", FAVICON_SRC) | |
| logo_html = f'<img src="{LOGO_SRC}" alt="CS Software Solutions logo">' if LOGO_SRC else "" | |
| HEADER = f""" | |
| <div class="cs-header"> | |
| {logo_html} | |
| <div> | |
| <h1>{APP_NAME.replace("Paraphraser", "<span>Paraphraser</span>")}</h1> | |
| <p>Rewrite academic prose while keeping your Word layout intact — upload a <b>.docx</b> or paste text, | |
| then review tentative similarity and AI-style indicators.</p> | |
| <div class="cs-tag">INNOVATE <b>•</b> DEVELOP <i>•</i> DELIVER</div> | |
| </div> | |
| </div> | |
| """ | |
| FOOTER = f""" | |
| <div class="cs-footer"> | |
| <div class="bar"></div> | |
| © CS Software Solutions · Tri-Crest Press | <a href="{SITE_URL}" target="_blank">cssoftwaresolutions.in</a><br> | |
| Paraphrased output should be checked for accuracy and properly cited before submission. | |
| </div> | |
| """ | |
| theme = gr.themes.Soft( | |
| primary_hue="indigo", | |
| secondary_hue="pink", | |
| neutral_hue="slate", | |
| font=[gr.themes.GoogleFont("Poppins"), "ui-sans-serif", "sans-serif"], | |
| radius_size="lg", | |
| ) | |
| with gr.Blocks(title=f"{APP_NAME} · CS Software Solutions", theme=theme, css=CSS, js=FORCE_LIGHT) as demo: | |
| user_state = gr.State(None) | |
| gr.HTML(HEADER) | |
| # ======================= Login / Sign up ======================= | |
| with gr.Column(visible=True, elem_classes="cs-auth") as auth_view: | |
| with gr.Column(elem_classes="cs-card"): | |
| gr.HTML('<div class="cs-auth-title">Welcome</div>' | |
| '<div class="cs-auth-sub">Log in or create a free account to start paraphrasing.</div>') | |
| with gr.Tabs(): | |
| with gr.Tab("🔐 Log in"): | |
| li_email = gr.Textbox(label="Email", placeholder="you@university.edu") | |
| li_pw = gr.Textbox(label="Password", type="password") | |
| li_btn = gr.Button("Log in", variant="primary", size="lg") | |
| with gr.Tab("📝 Sign up"): | |
| su_name = gr.Textbox(label="Full name") | |
| su_email = gr.Textbox(label="Email", placeholder="you@university.edu") | |
| su_pw = gr.Textbox(label="Password", type="password", info="At least 8 characters") | |
| su_pw2 = gr.Textbox(label="Confirm password", type="password") | |
| su_btn = gr.Button("Create account", variant="primary", size="lg") | |
| auth_msg = gr.HTML() | |
| # ======================= Main app (after login) ======================= | |
| with gr.Column(visible=False) as app_view: | |
| with gr.Row(equal_height=True): | |
| user_bar = gr.HTML() | |
| logout_btn = gr.Button("Log out", size="sm", elem_classes="cs-logout") | |
| with gr.Tabs(): | |
| # ---------------- Upload DOCX ---------------- | |
| with gr.Tab("📄 Upload Word (.docx)"): | |
| with gr.Row(equal_height=False): | |
| with gr.Column(scale=5, elem_classes="cs-card"): | |
| input_file = gr.File(label="Word document (.docx)", file_types=[".docx"], type="filepath") | |
| with gr.Group(elem_classes="cs-settings"): | |
| gr.HTML('<div class="cs-settings-title">⚙️ Settings</div>') | |
| d_temp = gr.Slider(0.3, 1.5, value=0.9, step=0.1, label="Paraphrase variation", | |
| info="Higher = bolder rewording, lower = closer to the original") | |
| d_min = gr.Slider(5, 30, value=8, step=1, label="Minimum words per paragraph", | |
| info="Shorter lines (headings, captions) are left untouched") | |
| d_mode = gr.Radio(MODES, value=MODES[0], label="Processing", | |
| info="GPU is fast but limited by a daily usage quota; CPU is slower but always available") | |
| d_btn = gr.Button("✨ Paraphrase document", variant="primary", size="lg") | |
| gr.Markdown( | |
| "<small>Keeps sections, margins, styles, tables, images, numbering and headers/footers. " | |
| "Mixed bold/italic inside one paragraph takes the first run's formatting.</small>" | |
| ) | |
| with gr.Column(scale=5, elem_classes="cs-card"): | |
| d_status = gr.HTML(status_idle("Upload a document and press **Paraphrase document**.")) | |
| d_out = gr.File(label="Download paraphrased .docx", interactive=False) | |
| d_dl = gr.HTML() | |
| d_scores = gr.HTML(EMPTY_SCORES) | |
| with gr.Accordion("Preview of changes (first 10)", open=False): | |
| d_preview = gr.Markdown() | |
| # ---------------- Paste text ---------------- | |
| with gr.Tab("✍️ Paste text"): | |
| with gr.Row(equal_height=True): | |
| with gr.Column(scale=1, elem_classes="cs-card"): | |
| p_in = gr.Textbox(label="Your text", lines=16, max_lines=30, | |
| placeholder="Paste your paragraphs here. Separate paragraphs with a blank line.") | |
| p_count = gr.HTML(count_words("")) | |
| with gr.Column(scale=1, elem_classes="cs-card"): | |
| p_out = gr.Textbox(label="Paraphrased text", lines=16, max_lines=30, | |
| show_copy_button=True, interactive=False) | |
| with gr.Row(): | |
| with gr.Column(scale=5): | |
| with gr.Group(elem_classes="cs-settings"): | |
| gr.HTML('<div class="cs-settings-title">⚙️ Settings</div>') | |
| p_temp = gr.Slider(0.3, 1.5, value=0.9, step=0.1, label="Paraphrase variation", | |
| info="Higher = bolder rewording, lower = closer to the original") | |
| p_min = gr.Slider(3, 30, value=6, step=1, label="Minimum words per paragraph") | |
| p_mode = gr.Radio(MODES, value=MODES[0], label="Processing") | |
| p_btn = gr.Button("✨ Paraphrase text", variant="primary", size="lg") | |
| with gr.Column(scale=5): | |
| p_status = gr.HTML(status_idle("Paste text and press **Paraphrase text**.")) | |
| p_file = gr.File(label="Download as .docx", interactive=False) | |
| p_dl = gr.HTML() | |
| p_scores = gr.HTML(EMPTY_SCORES) | |
| # ---------------- Admin (admins only) ---------------- | |
| with gr.Tab("🛡️ Admin", visible=False) as admin_tab: | |
| ad_stats = gr.HTML() | |
| with gr.Row(): | |
| ad_query = gr.Textbox(label="Search users", placeholder="Name or email", scale=4) | |
| ad_refresh = gr.Button("🔄 Refresh", scale=1) | |
| ad_export = gr.Button("⬇️ Export CSV", scale=1) | |
| ad_table = gr.Dataframe(headers=ADMIN_COLS, interactive=False, wrap=True) | |
| ad_csv = gr.File(label="Users export", interactive=False) | |
| with gr.Row(equal_height=False): | |
| with gr.Column(elem_classes="cs-card"): | |
| gr.HTML('<div class="cs-settings-title">Enable / disable account</div>') | |
| ad_email = gr.Textbox(label="User email") | |
| with gr.Row(): | |
| ad_disable = gr.Button("🚫 Disable") | |
| ad_enable = gr.Button("✅ Enable") | |
| with gr.Column(elem_classes="cs-card"): | |
| gr.HTML('<div class="cs-settings-title">Reset password</div>') | |
| ad_pw_email = gr.Textbox(label="User email") | |
| ad_new_pw = gr.Textbox(label="New password", type="password") | |
| ad_reset = gr.Button("🔑 Reset password") | |
| with gr.Column(elem_classes="cs-card"): | |
| gr.HTML('<div class="cs-settings-title">Delete account</div>') | |
| ad_del_email = gr.Textbox(label="User email") | |
| ad_del_confirm = gr.Textbox(label="Type the email again to confirm") | |
| ad_delete = gr.Button("🗑️ Delete user", variant="stop") | |
| ad_msg = gr.HTML() | |
| gr.HTML(FOOTER) | |
| # ---------------- Events ---------------- | |
| auth_outputs = [user_state, auth_view, app_view, user_bar, auth_msg, admin_tab, ad_stats, ad_table] | |
| ad_out = [ad_stats, ad_table, ad_msg] | |
| hid = dict(show_progress="hidden", api_name=False) | |
| ad_refresh.click(lambda u, q: _admin_refresh(u, q, "Refreshed."), [user_state, ad_query], ad_out, **hid) | |
| ad_query.submit(admin_search, [user_state, ad_query], ad_table, **hid) | |
| ad_disable.click(lambda u, e, q: admin_set_active(u, e, q, False), [user_state, ad_email, ad_query], ad_out, **hid) | |
| ad_enable.click(lambda u, e, q: admin_set_active(u, e, q, True), [user_state, ad_email, ad_query], ad_out, **hid) | |
| ad_reset.click(admin_reset_pw, [user_state, ad_pw_email, ad_new_pw, ad_query], ad_out, **hid) | |
| ad_delete.click(admin_delete, [user_state, ad_del_email, ad_del_confirm, ad_query], ad_out, **hid) | |
| ad_export.click(admin_export, [user_state], ad_csv, **hid) | |
| li_btn.click(do_login, [li_email, li_pw], auth_outputs, show_progress="hidden", api_name=False) | |
| li_pw.submit(do_login, [li_email, li_pw], auth_outputs, show_progress="hidden", api_name=False) | |
| su_btn.click(do_signup, [su_name, su_email, su_pw, su_pw2], auth_outputs, show_progress="hidden", api_name=False) | |
| logout_btn.click(do_logout, None, auth_outputs, show_progress="hidden", api_name=False) | |
| d_btn.click( | |
| process_docx, | |
| inputs=[input_file, d_temp, d_min, d_mode, user_state], | |
| outputs=[d_out, d_dl, d_status, d_scores, d_preview], | |
| api_name=False, | |
| show_progress="hidden", | |
| ) | |
| p_in.change(count_words, inputs=p_in, outputs=p_count, show_progress="hidden", queue=False, api_name=False) | |
| p_btn.click( | |
| process_text, | |
| inputs=[p_in, p_temp, p_min, p_mode, user_state], | |
| outputs=[p_out, p_file, p_dl, p_status, p_scores], | |
| api_name=False, | |
| show_progress="hidden", | |
| ) | |
| demo.queue(default_concurrency_limit=2, max_size=30) | |
| if __name__ == "__main__": | |
| demo.launch( | |
| show_api=False, | |
| allowed_paths=[str(OUT_DIR)], | |
| favicon_path=str(FAVICON_PATH) if FAVICON_PATH.exists() else None, | |
| ) | |