Spaces:
Running
Running
| from __future__ import annotations | |
| import hashlib | |
| import html | |
| import json | |
| import os | |
| import re | |
| from functools import lru_cache | |
| from typing import Any | |
| APP_TITLE = "Trollsona" | |
| APP_SUBTITLE = "Summon the little menace living behind your respectable personality." | |
| TRACK_NAME = "An Adventure in Thousand Token Wood" | |
| DEFAULT_MODEL_ID = "RthItalia/nano_compact_3b_qkvfp16" | |
| DEFAULT_FALLBACK_MODEL_ID = "Qwen/Qwen2.5-0.5B-Instruct" | |
| MAX_PROFILE_CHARS = 700 | |
| MAX_NAME_CHARS = 36 | |
| def parse_bool_env(name: str, default: bool) -> bool: | |
| raw_value = os.getenv(name) | |
| if raw_value is None: | |
| return default | |
| normalized = raw_value.strip().lower() | |
| if normalized in {"1", "true", "yes", "on"}: | |
| return True | |
| if normalized in {"0", "false", "no", "off"}: | |
| return False | |
| return default | |
| def parse_int_env(name: str, default: int, min_value: int, max_value: int) -> int: | |
| raw_value = os.getenv(name) | |
| if raw_value is None: | |
| return default | |
| try: | |
| value = int(raw_value) | |
| except ValueError: | |
| return default | |
| return max(min_value, min(max_value, value)) | |
| MODEL_ID = os.getenv("TROLLSONA_MODEL_ID", DEFAULT_MODEL_ID) | |
| FALLBACK_MODEL_ID = os.getenv("TROLLSONA_FALLBACK_MODEL_ID", DEFAULT_FALLBACK_MODEL_ID) | |
| MODEL_ENABLED = parse_bool_env("TROLLSONA_ENABLE_MODEL", default=False) | |
| MAX_NEW_TOKENS = parse_int_env("TROLLSONA_MAX_NEW_TOKENS", 200, 32, 512) | |
| PERSONA_STYLES = { | |
| "Back-Alley Oracle": { | |
| "flavor": "candlelit prophecy from a very suspicious side street", | |
| "noun_pool": ["Candle", "Omen", "Alley", "Brass", "Whisper", "Ledger"], | |
| }, | |
| "Basement Prince": { | |
| "flavor": "royal delusion wrapped in dust, snacks, and old cables", | |
| "noun_pool": ["Basement", "Velvet", "Outlet", "Throne", "Snack", "Static"], | |
| }, | |
| "Forest Heckler": { | |
| "flavor": "mossy woodland sarcasm with a pocket full of bad advice", | |
| "noun_pool": ["Moss", "Root", "Twig", "Bog", "Fern", "Stump"], | |
| }, | |
| "Union Goblin": { | |
| "flavor": "petty workplace grievance with ceremonial clipboard energy", | |
| "noun_pool": ["Clause", "Mug", "Breakroom", "Badge", "Staple", "Shift"], | |
| }, | |
| "Dungeon Intern": { | |
| "flavor": "overworked dungeon bureaucracy and unpaid dramatic labor", | |
| "noun_pool": ["Ledger", "Torch", "Mop", "Key", "Goblet", "Trapdoor"], | |
| }, | |
| "Mall Witch": { | |
| "flavor": "food-court divination with lip gloss and thunder", | |
| "noun_pool": ["Kiosk", "Charm", "Receipt", "Fountain", "Mascara", "Pretzel"], | |
| }, | |
| "Parking Lot Philosopher": { | |
| "flavor": "deep truths delivered beside a dented shopping cart", | |
| "noun_pool": ["Asphalt", "Cart", "Neon", "Cone", "Puddle", "Keychain"], | |
| }, | |
| "Saint of Bad Decisions": { | |
| "flavor": "holy nonsense for people who turn errands into lore", | |
| "noun_pool": ["Halo", "Candle", "Excuse", "Relic", "Errand", "Confetti"], | |
| }, | |
| "Meme Caporegime": { | |
| "flavor": "old-neighborhood swagger filtered through cursed screenshots", | |
| "noun_pool": ["Pixel", "Prophecy", "Caption", "Scroll", "Vibe", "Echo"], | |
| }, | |
| } | |
| SPICE_LABELS = { | |
| 1: "tiny pinch", | |
| 2: "polite sting", | |
| 3: "back-room heckle", | |
| 4: "crispy little judgment", | |
| 5: "full dossier incident", | |
| } | |
| BLOCKED_PATTERNS = [ | |
| r"\bkill yourself\b", | |
| r"\bkys\b", | |
| r"\bself[- ]?harm\b", | |
| r"\bsuicide\b", | |
| r"\bhate\b", | |
| r"\bidiot\b", | |
| r"\bstupid\b", | |
| r"\bmoron\b", | |
| r"\bdumb\b", | |
| r"\bloser\b", | |
| r"\bugly\b", | |
| r"\bworthless\b", | |
| r"\bsubhuman\b", | |
| r"\bslur\b", | |
| r"\bterrorist\b", | |
| r"\bsexual\b", | |
| r"\bexplicit\b", | |
| r"\bprotected class\b", | |
| ] | |
| PROTECTED_TARGETING_PATTERNS = [ | |
| r"\bbecause of your race\b", | |
| r"\bbecause of your religion\b", | |
| r"\bbecause of your gender\b", | |
| r"\bbecause of your sexuality\b", | |
| r"\bbecause of your disability\b", | |
| r"\bbecause of your nationality\b", | |
| r"\bbecause of your ethnicity\b", | |
| ] | |
| SAFE_REPLY = ( | |
| "The dossier hissed, smoked, and refused to punch down. " | |
| "Final harmless verdict: your chaos has excellent posture and a suspicious little hat." | |
| ) | |
| SAFE_ADVICE = "Make the next useful move before you decorate the excuse." | |
| PRESET_DOSSIERS = [ | |
| { | |
| "button": "Mira - coffee-built UI oracle", | |
| "values": ( | |
| "Mira", | |
| "I overbuild side projects, drink too much coffee, and love weird UI.", | |
| "Back-Alley Oracle", | |
| 3, | |
| True, | |
| ), | |
| }, | |
| { | |
| "button": "Alex - label-system dungeon clerk", | |
| "values": ( | |
| "Alex", | |
| "I start productivity systems and then reorganize the labels forever.", | |
| "Dungeon Intern", | |
| 4, | |
| True, | |
| ), | |
| }, | |
| { | |
| "button": "Sam - tiny-game screenshot boss", | |
| "values": ( | |
| "Sam", | |
| "I make tiny games, forget lunch, and name variables like ancient spells.", | |
| "Meme Caporegime", | |
| 2, | |
| False, | |
| ), | |
| }, | |
| ] | |
| def stable_int(*parts: str) -> int: | |
| payload = "||".join(parts).encode("utf-8", errors="ignore") | |
| return int(hashlib.sha256(payload).hexdigest()[:12], 16) | |
| def clean_text(value: Any, max_chars: int) -> str: | |
| text = "" if value is None else str(value) | |
| text = re.sub(r"\s+", " ", text).strip() | |
| return text[:max_chars] | |
| def clamp_spice(value: Any) -> int: | |
| try: | |
| spice = int(value) | |
| except (TypeError, ValueError): | |
| spice = 3 | |
| return max(1, min(5, spice)) | |
| def compute_cringe_score(profile: str, persona: str, spice: int) -> int: | |
| base = stable_int(profile.lower(), persona.lower(), str(spice)) % 61 | |
| return max(0, min(100, 22 + base + (spice * 3))) | |
| def cringe_label(score: int) -> str: | |
| if score < 35: | |
| return "barely haunted" | |
| if score < 60: | |
| return "noticeably cursed" | |
| if score < 82: | |
| return "dossier-grade cringe" | |
| return "full goblin canon event" | |
| def build_prompt( | |
| user_name: str, | |
| profile: str, | |
| persona: str, | |
| spice: int, | |
| include_advice: bool, | |
| score: int, | |
| ) -> str: | |
| style = PERSONA_STYLES.get(persona, PERSONA_STYLES["Forest Heckler"]) | |
| advice_rule = "Include one practical useful_advice sentence." if include_advice else ( | |
| "Set useful_advice to a short note that advice was disabled." | |
| ) | |
| return f""" | |
| You are Trollsona, a theatrical troll alter-ego generator. | |
| Track: {TRACK_NAME}. | |
| Your job is to transform the user's self-description into a funny, slightly grotesque, | |
| whimsical troll persona. Make it feel like a stained-paper character dossier that was | |
| dictated by a back-alley fortune teller, stamped by a petty clerk, and lightly heckled | |
| by an italo-american cousin who has opinions but not cruelty. | |
| Return only valid minified JSON with these fields: | |
| trollsona_name, troll_reply, useful_advice, cringe_score, cringe_score_label. | |
| Objective: | |
| - Make the result absurd, memorable, specific, and theatrical. | |
| - Make trollsona_name sound like a summoned character, not a username. | |
| - Keep it roasty, not hateful. | |
| - Keep the humor sharp but warm: playful sting, never humiliation. | |
| Style rules: | |
| - Write in vivid, punchy English. | |
| - Use occasional light italo-american flavor, but sparingly. | |
| - Good flavor examples: "listen, paisan", "madone", "capisce". | |
| - Do not overuse slang or turn the voice into a caricature. | |
| - Use grotesque but charming imagery: candle wax, receipts, tiny crowns, haunted binders, | |
| dented carts, snack dust, side quests, suspicious paperwork. | |
| - No generic roast bot voice. | |
| - No generic assistant copy, no filler, no disclaimers, no moralizing. | |
| - troll_reply must be the strongest comedic line, 1-3 short sentences max. | |
| - useful_advice must contain one real insight in 1 sentence max. | |
| Humor boundaries: | |
| - Roast only habits, vibe, overthinking, productivity rituals, startup energy, | |
| internet behavior, wording, or harmless personal lore. | |
| - Never attack protected characteristics or identity. | |
| - Never insult appearance, race, ethnicity, religion, disability, nationality, | |
| gender, sexuality, trauma, mental health, or protected traits. | |
| - Never include threats, self-harm, sexual content, profanity, or slurs. | |
| - Never punch down. | |
| User name: {user_name or "Anonymous traveler"} | |
| User profile: {profile or "No profile supplied."} | |
| Persona: {persona} | |
| Persona flavor: {style["flavor"]} | |
| Spice level: {spice}/5 ({SPICE_LABELS[spice]}) | |
| Use this exact deterministic cringe_score: {score} | |
| Use this matching cringe_score_label: {cringe_label(score)} | |
| {advice_rule} | |
| """.strip() | |
| def is_safe_text(text: str) -> bool: | |
| normalized = text.lower() | |
| for pattern in BLOCKED_PATTERNS + PROTECTED_TARGETING_PATTERNS: | |
| if re.search(pattern, normalized): | |
| return False | |
| return True | |
| def fallback_trollsona( | |
| user_name: str, | |
| profile: str, | |
| persona: str, | |
| spice: int, | |
| include_advice: bool, | |
| reason: str, | |
| ) -> dict[str, Any]: | |
| style = PERSONA_STYLES.get(persona, PERSONA_STYLES["Forest Heckler"]) | |
| seed = stable_int(user_name.lower(), profile.lower(), persona.lower(), str(spice)) | |
| adjectives = ["Velvet", "Candle", "Ashen", "Brass", "Crooked", "Sainted", "Static"] | |
| titles = [ | |
| "Overthinker in Residence", | |
| "Snack Baron of Almost", | |
| "Dossier Clerk", | |
| "Chaos Notary", | |
| "Sidequest Duke", | |
| "Patron Saint of Later", | |
| ] | |
| noun = style["noun_pool"][seed % len(style["noun_pool"])] | |
| adjective = adjectives[(seed // 7) % len(adjectives)] | |
| title = titles[(seed // 13) % len(titles)] | |
| safe_name = re.sub(r"[^A-Za-z0-9 ]+", "", user_name).strip()[:MAX_NAME_CHARS] | |
| name_prefix = safe_name.title() if safe_name else adjective | |
| trollsona_name = f"{name_prefix} {noun}-{title}" | |
| roast_templates = [ | |
| "Listen, paisan: your vibe is a candlelit side quest that opened twelve tabs, found a tiny crown, and called it destiny.", | |
| "Your aura says main character, but your calendar is dressed like a haunted binder asking for rent.", | |
| "You are one dramatic cape away from turning a normal errand into a village ordinance.", | |
| "Your brain is a basement tavern where every idea demands a theme song, a snack bowl, and a separate invoice.", | |
| "Madone, you carry the confidence of a bridge troll charging tolls in vibes and loose receipts.", | |
| "You alphabetize chaos, misplace the alphabet, then file a complaint with the moon.", | |
| ] | |
| advice_templates = [ | |
| "Pick one task, make it smaller, and finish that version before you rename the kingdom.", | |
| "Write the next concrete step in one sentence, then do only that step. Capisce?", | |
| "Keep the weird idea, but give it a deadline and a visible done state.", | |
| "Trade one dramatic plan for one shipped artifact before the candles burn out.", | |
| "Use the chaos as seasoning, not as project management.", | |
| ] | |
| score = compute_cringe_score(profile, persona, spice) | |
| reply = roast_templates[(seed // 17 + spice) % len(roast_templates)] | |
| advice = advice_templates[(seed // 23 + spice) % len(advice_templates)] | |
| if not include_advice: | |
| advice = "Truth withheld. The dossier clerk stamps the page and looks away." | |
| return { | |
| "trollsona_name": trollsona_name, | |
| "troll_reply": reply, | |
| "useful_advice": advice, | |
| "cringe_score": score, | |
| "cringe_score_label": cringe_label(score), | |
| "include_advice": include_advice, | |
| "runtime": f"model_id={MODEL_ID}; fallback_model_id={FALLBACK_MODEL_ID}; model_enabled={MODEL_ENABLED}", | |
| "source": "deterministic_fallback", | |
| "fallback_reason": reason, | |
| } | |
| def load_model() -> tuple[Any | None, Any | None, str, str]: | |
| if not MODEL_ENABLED: | |
| return ( | |
| None, | |
| None, | |
| "model disabled by TROLLSONA_ENABLE_MODEL", | |
| f"model_id={MODEL_ID}; fallback_model_id={FALLBACK_MODEL_ID}; device=disabled", | |
| ) | |
| try: | |
| import torch | |
| from transformers import AutoModelForCausalLM, AutoTokenizer | |
| except Exception as exc: | |
| return ( | |
| None, | |
| None, | |
| f"model dependencies unavailable: {type(exc).__name__}: {exc}", | |
| f"model_id={MODEL_ID}; fallback_model_id={FALLBACK_MODEL_ID}; device=unavailable", | |
| ) | |
| failures: list[str] = [] | |
| def load_tokenizer(candidate_id: str) -> Any: | |
| tokenizer = AutoTokenizer.from_pretrained( | |
| candidate_id, | |
| use_fast=True, | |
| trust_remote_code=True, | |
| ) | |
| if tokenizer.pad_token_id is None and tokenizer.eos_token is not None: | |
| tokenizer.pad_token = tokenizer.eos_token | |
| return tokenizer | |
| def load_cuda_model(candidate_id: str) -> Any: | |
| load_attempts = [ | |
| { | |
| "trust_remote_code": True, | |
| "device_map": "cuda", | |
| "dtype": torch.float16, | |
| "low_cpu_mem_usage": True, | |
| }, | |
| { | |
| "trust_remote_code": True, | |
| "device_map": "cuda", | |
| "torch_dtype": torch.float16, | |
| "low_cpu_mem_usage": True, | |
| }, | |
| { | |
| "trust_remote_code": True, | |
| "torch_dtype": torch.float16, | |
| "low_cpu_mem_usage": True, | |
| }, | |
| ] | |
| last_error: Exception | None = None | |
| for kwargs in load_attempts: | |
| try: | |
| model = AutoModelForCausalLM.from_pretrained(candidate_id, **kwargs) | |
| if "device_map" not in kwargs: | |
| model = model.to("cuda") | |
| return model | |
| except Exception as exc: | |
| last_error = exc | |
| if last_error is not None: | |
| raise last_error | |
| raise RuntimeError("CUDA model load failed without exception") | |
| def load_cpu_model(candidate_id: str) -> Any: | |
| try: | |
| return AutoModelForCausalLM.from_pretrained( | |
| candidate_id, | |
| trust_remote_code=True, | |
| low_cpu_mem_usage=True, | |
| ) | |
| except TypeError: | |
| return AutoModelForCausalLM.from_pretrained(candidate_id, trust_remote_code=True) | |
| candidates = [ | |
| {"role": "primary", "model_id": MODEL_ID, "requires_cuda": True}, | |
| {"role": "fallback_model", "model_id": FALLBACK_MODEL_ID, "requires_cuda": False}, | |
| ] | |
| seen_model_ids: set[str] = set() | |
| for candidate in candidates: | |
| candidate_id = str(candidate["model_id"]).strip() | |
| if not candidate_id or candidate_id in seen_model_ids: | |
| continue | |
| seen_model_ids.add(candidate_id) | |
| role = str(candidate["role"]) | |
| requires_cuda = bool(candidate["requires_cuda"]) | |
| if requires_cuda and not torch.cuda.is_available(): | |
| failures.append(f"{role} {candidate_id}: CUDA unavailable") | |
| continue | |
| try: | |
| tokenizer = load_tokenizer(candidate_id) | |
| if torch.cuda.is_available(): | |
| model = load_cuda_model(candidate_id) | |
| device = "cuda" | |
| else: | |
| model = load_cpu_model(candidate_id) | |
| device = "cpu" | |
| model.eval() | |
| torch.manual_seed(0) | |
| if torch.cuda.is_available(): | |
| torch.cuda.manual_seed_all(0) | |
| fallback_note = "; ".join(failures) | |
| status = "model loaded" if not fallback_note else f"model loaded after fallback: {fallback_note}" | |
| runtime = ( | |
| f"model_id={candidate_id}; role={role}; device={device}; " | |
| f"cuda_available={torch.cuda.is_available()}" | |
| ) | |
| return tokenizer, model, status, runtime | |
| except Exception as exc: | |
| failures.append(f"{role} {candidate_id}: {type(exc).__name__}: {exc}") | |
| failure_text = " | ".join(failures) if failures else "no model candidates configured" | |
| runtime = ( | |
| f"model_id={MODEL_ID}; fallback_model_id={FALLBACK_MODEL_ID}; " | |
| f"cuda_available={torch.cuda.is_available()}" | |
| ) | |
| return None, None, f"model load failed: {failure_text}", runtime | |
| def format_generation_prompt(tokenizer: Any, prompt: str) -> str: | |
| try: | |
| if getattr(tokenizer, "chat_template", None): | |
| return tokenizer.apply_chat_template( | |
| [{"role": "user", "content": prompt}], | |
| tokenize=False, | |
| add_generation_prompt=True, | |
| ) | |
| except Exception: | |
| return prompt | |
| return prompt | |
| def generation_temperature(spice: int) -> float: | |
| return round(0.48 + (clamp_spice(spice) * 0.08), 2) | |
| def model_device(model: Any) -> Any: | |
| target_device = getattr(model, "device", None) | |
| if target_device is not None and str(target_device) != "meta": | |
| return target_device | |
| try: | |
| return next(model.parameters()).device | |
| except Exception: | |
| return None | |
| def generate_with_model(prompt: str, spice: int) -> tuple[str | None, str, str]: | |
| tokenizer, model, status, runtime = load_model() | |
| if tokenizer is None or model is None: | |
| return None, status, runtime | |
| try: | |
| import torch | |
| model_prompt = format_generation_prompt(tokenizer, prompt) | |
| inputs = tokenizer(model_prompt, return_tensors="pt", truncation=True, max_length=1536) | |
| target_device = model_device(model) | |
| if target_device is not None: | |
| inputs = {key: value.to(target_device) for key, value in inputs.items()} | |
| seed = stable_int(prompt, str(spice), runtime) % (2**31) | |
| torch.manual_seed(seed) | |
| if hasattr(torch, "cuda") and torch.cuda.is_available(): | |
| torch.cuda.manual_seed_all(seed) | |
| with torch.no_grad(): | |
| output_ids = model.generate( | |
| **inputs, | |
| max_new_tokens=MAX_NEW_TOKENS, | |
| do_sample=True, | |
| temperature=generation_temperature(spice), | |
| num_beams=1, | |
| repetition_penalty=1.1, | |
| pad_token_id=tokenizer.eos_token_id, | |
| ) | |
| prompt_len = inputs["input_ids"].shape[-1] | |
| generated_ids = output_ids[0][prompt_len:] | |
| return tokenizer.decode(generated_ids, skip_special_tokens=True).strip(), status, runtime | |
| except Exception as exc: | |
| return None, f"model generation failed: {type(exc).__name__}: {exc}", runtime | |
| def parse_loose_model_fields(raw_text: str) -> dict[str, str]: | |
| fields: dict[str, str] = {} | |
| for field in ["trollsona_name", "troll_reply", "useful_advice", "cringe_score_label"]: | |
| pattern = rf'"{field}"\s*:\s*"((?:\\.|[^"\\])*)' | |
| match = re.search(pattern, raw_text or "", flags=re.DOTALL) | |
| if not match: | |
| continue | |
| try: | |
| value = json.loads(f'"{match.group(1)}"') | |
| except json.JSONDecodeError: | |
| value = match.group(1) | |
| fields[field] = str(value) | |
| return fields | |
| def coerce_model_result( | |
| parsed: dict[str, Any], | |
| fallback: dict[str, Any], | |
| score: int, | |
| include_advice: bool, | |
| fallback_reason: str, | |
| runtime: str, | |
| ) -> dict[str, Any] | None: | |
| result = dict(fallback) | |
| field_limits = { | |
| "trollsona_name": 80, | |
| "troll_reply": 360, | |
| "useful_advice": 280, | |
| "cringe_score_label": 80, | |
| } | |
| used_fields: list[str] = [] | |
| missing_fields: list[str] = [] | |
| for field, limit in field_limits.items(): | |
| value = clean_text(parsed.get(field), limit) | |
| if value and is_safe_text(value): | |
| result[field] = value | |
| used_fields.append(field) | |
| else: | |
| missing_fields.append(field) | |
| if not used_fields: | |
| return None | |
| result["cringe_score"] = score | |
| result["include_advice"] = include_advice | |
| result["source"] = "transformers_model" | |
| result["runtime"] = runtime | |
| if missing_fields: | |
| partial_reason = f"model output partial; fallback filled: {', '.join(missing_fields)}" | |
| result["fallback_reason"] = ( | |
| f"{fallback_reason}; {partial_reason}" if fallback_reason else partial_reason | |
| ) | |
| else: | |
| result["fallback_reason"] = fallback_reason | |
| return result | |
| def parse_model_output( | |
| raw_text: str, | |
| fallback: dict[str, Any], | |
| score: int, | |
| include_advice: bool, | |
| fallback_reason: str, | |
| runtime: str, | |
| ) -> dict[str, Any] | None: | |
| decoder = json.JSONDecoder() | |
| parsed = None | |
| for match in re.finditer(r"\{", raw_text or ""): | |
| try: | |
| candidate, _ = decoder.raw_decode(raw_text[match.start() :]) | |
| except json.JSONDecodeError: | |
| continue | |
| if isinstance(candidate, dict): | |
| parsed = candidate | |
| break | |
| if parsed is None: | |
| parsed = parse_loose_model_fields(raw_text) | |
| return coerce_model_result(parsed, fallback, score, include_advice, fallback_reason, runtime) | |
| def repair_model_output( | |
| raw_text: str, | |
| fallback: dict[str, Any], | |
| fallback_reason: str, | |
| runtime: str, | |
| ) -> dict[str, Any] | None: | |
| repaired_reply = clean_text(raw_text, 360) | |
| repaired_reply = re.sub(r"^```(?:json)?|```$", "", repaired_reply).strip() | |
| if not repaired_reply or repaired_reply.startswith("{"): | |
| return None | |
| if not is_safe_text(repaired_reply): | |
| return None | |
| result = dict(fallback) | |
| result["troll_reply"] = repaired_reply | |
| result["source"] = "transformers_model_repaired" | |
| result["runtime"] = runtime | |
| repair_reason = "model output was not valid JSON and was repaired" | |
| result["fallback_reason"] = f"{fallback_reason}; {repair_reason}" if fallback_reason else repair_reason | |
| return result | |
| def safety_guard(result: dict[str, Any], fallback: dict[str, Any]) -> dict[str, Any]: | |
| fields = [ | |
| result.get("trollsona_name", ""), | |
| result.get("troll_reply", ""), | |
| result.get("useful_advice", ""), | |
| result.get("cringe_score_label", ""), | |
| ] | |
| if not all(is_safe_text(str(field)) for field in fields): | |
| guarded = dict(fallback) | |
| guarded["troll_reply"] = SAFE_REPLY | |
| guarded["useful_advice"] = SAFE_ADVICE | |
| guarded["fallback_reason"] = "safety guard replaced unsafe output" | |
| return guarded | |
| return result | |
| def render_card(result: dict[str, Any]) -> str: | |
| esc = {key: html.escape(str(value)) for key, value in result.items()} | |
| score = max(0, min(100, int(result.get("cringe_score", 0)))) | |
| useful_advice = clean_text(result.get("useful_advice", ""), 280) | |
| show_advice = bool(result.get("include_advice", True)) and bool(useful_advice) | |
| advice_tile = ( | |
| f""" | |
| <div class="trollsona-tile"> | |
| <div class="trollsona-label">A USEFUL SLAP</div> | |
| <div class="trollsona-value">{html.escape(useful_advice)}</div> | |
| </div> | |
| """.rstrip() | |
| if show_advice | |
| else "" | |
| ) | |
| grid_class = "trollsona-grid" if show_advice else "trollsona-grid trollsona-grid-single" | |
| return f""" | |
| <div class="trollsona-card"> | |
| <div class="dossier-kicker">THE SUMMONED MENACE</div> | |
| <h2>{esc["trollsona_name"]}</h2> | |
| <div class="trollsona-mainline">{esc["troll_reply"]}</div> | |
| <div class="{grid_class}"> | |
| {advice_tile} | |
| <div class="trollsona-tile"> | |
| <div class="trollsona-label">GOBLIN METER</div> | |
| <div class="meter-shell" aria-label="Goblin meter {score} out of 100"> | |
| <div class="meter-fill" style="width: {score}%"></div> | |
| </div> | |
| <div class="trollsona-value">{score}/100 - {esc["cringe_score_label"]}</div> | |
| </div> | |
| </div> | |
| </div> | |
| """.strip() | |
| def render_cursed_paperwork(result: dict[str, Any]) -> str: | |
| source = clean_text(result.get("source", "unknown"), 80) | |
| runtime = clean_text(result.get("runtime", "runtime unavailable"), 260) | |
| fallback_reason = clean_text(result.get("fallback_reason", ""), 180) | |
| if not fallback_reason: | |
| fallback_reason = "No fallback note." | |
| return ( | |
| f"**Source:** `{source}` \n" | |
| f"**Runtime:** `{runtime}` \n" | |
| f"**Fallback note:** {fallback_reason}" | |
| ) | |
| def render_empty_card() -> str: | |
| return """ | |
| <div class="empty-dossier"> | |
| <div class="dossier-kicker">The dossier is sealed</div> | |
| <h2>No menace has signed the paperwork yet.</h2> | |
| <p>Feed the booth a little lore, pick a resident menace, and pull the handle.</p> | |
| </div> | |
| """.strip() | |
| def load_preset(index: int) -> tuple[str, str, str, int, bool]: | |
| return PRESET_DOSSIERS[index]["values"] | |
| def generate_trollsona( | |
| user_name: str, | |
| profile: str, | |
| persona: str, | |
| spice: int, | |
| include_advice: bool, | |
| ) -> tuple[str, dict[str, Any], str]: | |
| user_name = clean_text(user_name, MAX_NAME_CHARS) | |
| profile = clean_text(profile, MAX_PROFILE_CHARS) | |
| persona = persona if persona in PERSONA_STYLES else "Forest Heckler" | |
| spice = clamp_spice(spice) | |
| include_advice = bool(include_advice) | |
| fallback = fallback_trollsona( | |
| user_name=user_name, | |
| profile=profile, | |
| persona=persona, | |
| spice=spice, | |
| include_advice=include_advice, | |
| reason="model unavailable or output invalid", | |
| ) | |
| score = compute_cringe_score(profile, persona, spice) | |
| prompt = build_prompt(user_name, profile, persona, spice, include_advice, score) | |
| raw_text, model_status, runtime = generate_with_model(prompt, spice) | |
| model_fallback_reason = "" if model_status == "model loaded" else model_status | |
| result = None | |
| if raw_text: | |
| result = parse_model_output( | |
| raw_text=raw_text, | |
| fallback=fallback, | |
| score=score, | |
| include_advice=include_advice, | |
| fallback_reason=model_fallback_reason, | |
| runtime=runtime, | |
| ) | |
| if result is None: | |
| result = repair_model_output(raw_text, fallback, model_fallback_reason, runtime) | |
| if result is None: | |
| result = dict(fallback) | |
| result["fallback_reason"] = model_status | |
| result["runtime"] = runtime | |
| result = safety_guard(result, fallback) | |
| return render_card(result), result, render_cursed_paperwork(result) | |
| def build_demo() -> Any: | |
| import gradio as gr | |
| css = "" | |
| css_path = os.path.join(os.path.dirname(__file__), "assets", "style.css") | |
| if os.path.exists(css_path): | |
| with open(css_path, "r", encoding="utf-8") as handle: | |
| css = handle.read() | |
| with gr.Blocks(title=APP_TITLE, css=css) as demo: | |
| gr.HTML( | |
| f""" | |
| <section class="ritual-hero"> | |
| <div class="hero-mark">Trollsona</div> | |
| <h1>{APP_TITLE}</h1> | |
| <p>{APP_SUBTITLE}</p> | |
| <div class="badge-row"> | |
| <span>Build Small Hackathon</span> | |
| <span>Small model</span> | |
| <span>Safe grotesque humor</span> | |
| <span>{TRACK_NAME}</span> | |
| </div> | |
| </section> | |
| """.strip() | |
| ) | |
| with gr.Row(elem_classes=["ritual-layout"]): | |
| with gr.Column(scale=1, elem_classes=["summoning-panel"]): | |
| gr.HTML('<div class="panel-heading">The summoning booth</div>') | |
| user_name = gr.Textbox( | |
| label="What do they call you?", | |
| placeholder="Mira", | |
| max_lines=1, | |
| ) | |
| profile = gr.Textbox( | |
| label="Confess your little lore", | |
| placeholder="I overbuild side projects, drink too much coffee, and love weird UI.", | |
| lines=5, | |
| max_lines=7, | |
| ) | |
| persona = gr.Dropdown( | |
| label="Pick your resident menace", | |
| choices=list(PERSONA_STYLES.keys()), | |
| value="Back-Alley Oracle", | |
| ) | |
| spice = gr.Slider( | |
| label="How hard should it sting?", | |
| minimum=1, | |
| maximum=5, | |
| value=3, | |
| step=1, | |
| ) | |
| include_advice = gr.Checkbox(label="Slip in one useful truth", value=True) | |
| generate_button = gr.Button("Summon Trollsona", variant="primary") | |
| with gr.Column(scale=1, elem_classes=["dossier-stage"]): | |
| card_output = gr.HTML(value=render_empty_card()) | |
| debug_state = gr.State() | |
| with gr.Accordion("See the cursed paperwork", open=False): | |
| debug_output = gr.Markdown( | |
| value=( | |
| "**Source:** `not summoned` \n" | |
| "**Runtime:** `not summoned` \n" | |
| "**Fallback note:** The dossier clerk is still asleep." | |
| ) | |
| ) | |
| generate_button.click( | |
| fn=generate_trollsona, | |
| inputs=[user_name, profile, persona, spice, include_advice], | |
| outputs=[card_output, debug_state, debug_output], | |
| ) | |
| with gr.Accordion("Stolen dossiers", open=False): | |
| gr.HTML('<div class="preset-note">Tap a stolen dossier to pre-fill the booth.</div>') | |
| with gr.Row(elem_classes=["preset-row"]): | |
| for preset_index, preset in enumerate(PRESET_DOSSIERS): | |
| preset_button = gr.Button( | |
| preset["button"], | |
| variant="secondary", | |
| elem_classes=["preset-card"], | |
| ) | |
| preset_button.click( | |
| fn=lambda index=preset_index: load_preset(index), | |
| inputs=[], | |
| outputs=[user_name, profile, persona, spice, include_advice], | |
| ) | |
| return demo | |
| demo = None if parse_bool_env("TROLLSONA_SKIP_UI_BUILD", default=False) else build_demo() | |
| if __name__ == "__main__": | |
| (demo or build_demo()).launch() | |