| 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, |
| } |
|
|
|
|
| @lru_cache(maxsize=1) |
| 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() |
|
|