| """GenreGoblin: a whimsical, resilient message transformation Gradio app.""" |
|
|
| from __future__ import annotations |
|
|
| import hashlib |
| import html |
| import json |
| import os |
| import re |
| import tempfile |
| from dataclasses import asdict, dataclass |
| from functools import lru_cache, partial |
| from typing import Any |
|
|
| import gradio as gr |
| import requests |
|
|
| try: |
| from dotenv import load_dotenv |
|
|
| load_dotenv() |
| except ImportError: |
| pass |
|
|
|
|
| APP_TITLE = "GenreGoblin" |
| MODEL_ID = os.getenv("MINICPM_MODEL_ID", "openbmb/MiniCPM3-4B") |
| VOICE_MODEL_ID = os.getenv("VOXCPM_MODEL_ID", "openbmb/VoxCPM2") |
| GENRES = [ |
| "Shakespeare Villain", |
| "Corporate Apology", |
| "Indian Parent WhatsApp", |
| "Anime Power-Up Speech", |
| "Pirate Warning", |
| "Haunted Railway Announcement", |
| "Courtroom Confession", |
| "Medieval Royal Decree", |
| "Film Noir Detective", |
| "Startup Founder Pitch", |
| "Bureaucratic Government Notice", |
| "Overdramatic Fantasy Quest", |
| ] |
| USE_CASES = [ |
| "WhatsApp", |
| "Email", |
| "Tweet/Post", |
| "Apology", |
| "Invitation", |
| "Excuse", |
| "Flirt but safe/non-explicit", |
| "Reminder", |
| "Complaint", |
| "Announcement", |
| ] |
|
|
| USE_CASE_RULES = { |
| "WhatsApp": "Write 1-3 compact chat-style lines. No subject line, greeting, or sign-off.", |
| "Email": "Write a concise email body with a calm greeting and clean close. Keep the requested action explicit.", |
| "Tweet/Post": "Keep the full version under 280 characters. Make the opening immediately punchy and shareable.", |
| "Apology": "State the apology plainly, own the specific action, and avoid performative excuses or invented repairs.", |
| "Invitation": "Sound welcoming. Preserve only dates, times, places, and plans that the user actually supplied.", |
| "Excuse": "Give an honest update without inventing causes, emergencies, promises, or evidence.", |
| "Flirt but safe/non-explicit": "Be playful, warm, non-explicit, and pressure-free. Never imply entitlement or persistence.", |
| "Reminder": "Put the requested action early and keep the message easy to act on.", |
| "Complaint": "Name the concrete issue, its effect, and the requested resolution without insults or threats.", |
| "Announcement": "Lead with the news, then add genre flavor. The practical information must remain unmistakable.", |
| } |
|
|
| COMEDY_ENGINES = { |
| "Shakespeare Villain": "Elegant menace aimed at the inconvenience, never the recipient; use one absurdly grand consequence.", |
| "Corporate Apology": "Weaponized professionalism: disproportionate process language around a very ordinary mistake.", |
| "Indian Parent WhatsApp": "Affection, practical care, and gentle guilt in a family group-chat rhythm; avoid accent caricature.", |
| "Anime Power-Up Speech": "Escalating resolve, a dramatic callback, and one hilariously mundane source of power.", |
| "Pirate Warning": "Nautical authority plus one shipboard inconvenience treated as a matter of destiny.", |
| "Haunted Railway Announcement": "Polite transit bureaucracy interrupted by one precise supernatural detail.", |
| "Courtroom Confession": "Legal formality, mounting evidence, and a deadpan verdict about an everyday action.", |
| "Medieval Royal Decree": "Ceremonial authority applied to a tiny modern task, with one ridiculous royal institution.", |
| "Film Noir Detective": "Dry first-person suspicion, weathered imagery, and a mundane object with an alibi.", |
| "Startup Founder Pitch": "Founder certainty, category language, and one knowingly excessive scale claim without inventing facts.", |
| "Bureaucratic Government Notice": "Procedural density and fake administrative gravity around one obvious instruction.", |
| "Overdramatic Fantasy Quest": "Mythic stakes, a reluctant fellowship, and one ordinary object promoted to sacred artifact.", |
| } |
|
|
| VOICE_DIRECTIONS = { |
| "Shakespeare Villain": "A commanding theatrical stage voice, low and velvety, precise diction, restrained menace, slow dramatic pauses.", |
| "Corporate Apology": "A polished corporate spokesperson, composed and sincere, medium pace, subtle tension beneath perfect professionalism.", |
| "Indian Parent WhatsApp": "A warm, caring adult family voice, practical and affectionate, brisk conversational pace, gentle exasperation, never caricatured.", |
| "Anime Power-Up Speech": "An energetic heroic young adult voice, rising intensity, crisp declarations, emotionally triumphant, fast but intelligible.", |
| "Pirate Warning": "A weathered seafaring storyteller, hearty and playful, rolling rhythm, bold projection, comic authority.", |
| "Haunted Railway Announcement": "A calm vintage station announcer, formal and measured, faintly eerie, cool tone, unsettling pauses.", |
| "Courtroom Confession": "A serious courtroom advocate, controlled and resonant, deliberate pacing, mounting dramatic conviction.", |
| "Medieval Royal Decree": "A stately royal herald, resonant ceremonial delivery, clear projection, dignified and slightly pompous.", |
| "Film Noir Detective": "A tired noir narrator, smoky and dry, low voice, unhurried pace, understated sarcasm.", |
| "Startup Founder Pitch": "A charismatic startup founder, bright and confident, quick controlled pace, persuasive energy, lightly self-aware.", |
| "Bureaucratic Government Notice": "A neutral official announcer, extremely precise, dry and formal, steady pace, accidental comic seriousness.", |
| "Overdramatic Fantasy Quest": "An epic fantasy narrator, rich and resonant, sweeping emotion, deliberate build, heroic warmth.", |
| } |
|
|
| @dataclass |
| class GenreProfile: |
| vocabulary: list[str] |
| rhythm: str |
| signature_phrase: str |
| emotional_temperature: str |
| forbidden_words: list[str] |
|
|
|
|
| @dataclass |
| class RewriteResult: |
| genre_dna: GenreProfile |
| final_message: str |
| short_version: str |
| sendability_score: int |
| chaos_score: int |
| probably_safe_to_send: str |
| goblin_commentary: str |
| agent_trace: list[dict[str, str]] |
|
|
|
|
| GENRE_PROFILES: dict[str, GenreProfile] = { |
| "Shakespeare Villain": GenreProfile( |
| ["hark", "fate", "mortal", "scheme", "reckoning"], |
| "Measured declarations that swell into a theatrical finish.", |
| "Mark me well", |
| "Velvet menace at candlelight", |
| ["lol", "synergy", "circle back"], |
| ), |
| "Corporate Apology": GenreProfile( |
| ["regret", "oversight", "stakeholders", "moving forward", "commitment"], |
| "Polished clauses, careful accountability, tidy next steps.", |
| "We take this seriously", |
| "Calm concern in a glass meeting room", |
| ["oopsie", "villain", "whatever"], |
| ), |
| "Indian Parent WhatsApp": GenreProfile( |
| ["beta", "please", "message me", "on time", "okay"], |
| "Warm practical reminder, one emotional nudge, decisive ending.", |
| "Just saying for your own good", |
| "Affectionate urgency with read receipts", |
| ["stakeholder", "hereby", "blood oath"], |
| ), |
| "Anime Power-Up Speech": GenreProfile( |
| ["limit", "spirit", "destiny", "power", "final form"], |
| "Short declaration, rising repetition, explosive resolve.", |
| "This is not even my final form", |
| "Lightning-struck determination", |
| ["quarterly", "pursuant", "mildly"], |
| ), |
| "Pirate Warning": GenreProfile( |
| ["ahoy", "matey", "deck", "tide", "captain"], |
| "Blunt nautical command followed by a salty flourish.", |
| "Mark the tide", |
| "Stormy but cheerfully theatrical", |
| ["synergy", "therefore", "calendar invite"], |
| ), |
| "Haunted Railway Announcement": GenreProfile( |
| ["passengers", "platform", "midnight", "carriage", "final stop"], |
| "Formal announcement interrupted by eerie, specific unease.", |
| "Mind the gap between worlds", |
| "Cold fluorescent dread", |
| ["sunshine", "casual", "road trip"], |
| ), |
| "Courtroom Confession": GenreProfile( |
| ["record", "evidence", "verdict", "testimony", "objection"], |
| "A sober admission that builds toward one decisive fact.", |
| "Let the record show", |
| "Guilty composure under bright lights", |
| ["vibes", "perhaps-ish", "pirate"], |
| ), |
| "Medieval Royal Decree": GenreProfile( |
| ["decree", "realm", "subjects", "crown", "herald"], |
| "Ceremonial proclamation with a clear command.", |
| "By order of the crown", |
| "Gold-trimmed authority", |
| ["startup", "hashtag", "low-key"], |
| ), |
| "Film Noir Detective": GenreProfile( |
| ["case", "city", "shadow", "alibi", "rain"], |
| "Clipped observation, moody turn, dry conclusion.", |
| "The case was simple", |
| "Rain-soaked suspicion", |
| ["sparkle", "quest", "henceforth"], |
| ), |
| "Startup Founder Pitch": GenreProfile( |
| ["friction", "unlock", "scale", "users", "opportunity"], |
| "Problem, insight, solution, confident close.", |
| "Here is the unlock", |
| "Caffeinated conviction", |
| ["decree", "haunting", "maybe someday"], |
| ), |
| "Bureaucratic Government Notice": GenreProfile( |
| ["hereby", "pursuant", "notice", "compliance", "effective"], |
| "Dense formality wrapped around one simple instruction.", |
| "Please be advised", |
| "Beige procedural gravity", |
| ["awesome", "destiny", "matey"], |
| ), |
| "Overdramatic Fantasy Quest": GenreProfile( |
| ["quest", "realm", "prophecy", "fellowship", "ancient"], |
| "A humble task inflated into a world-altering expedition.", |
| "So the quest begins", |
| "Torchlit heroic urgency", |
| ["email thread", "KPI", "routine"], |
| ), |
| } |
|
|
| GENRE_MARKERS = { |
| genre: tuple(profile.vocabulary + [profile.signature_phrase.lower()]) |
| for genre, profile in GENRE_PROFILES.items() |
| } |
|
|
|
|
| EXAMPLES = [ |
| ("I'm running late", "Haunted Railway Announcement", "WhatsApp", 3), |
| ("Can you send the file?", "Shakespeare Villain", "Email", 3), |
| ("Sorry I forgot to reply", "Corporate Apology", "Apology", 2), |
| ("Please clean the kitchen", "Indian Parent WhatsApp", "Reminder", 3), |
| ("I need an extension", "Courtroom Confession", "Email", 3), |
| ("Party at my place tonight", "Anime Power-Up Speech", "Invitation", 4), |
| ] |
|
|
|
|
| HARMFUL_PATTERNS = [ |
| r"\b(?:kill|murder|shoot|stab|bomb)\s+(?:you|him|her|them|people)\b", |
| r"\b(?:steal|phish|scam)\b.*\b(?:password|account|money|credentials)\b", |
| r"\bpretend to be\b.*\b(?:bank|police|government|support agent)\b", |
| r"\bsexual\b.*\b(?:minor|child|underage)\b", |
| ] |
|
|
|
|
| SYSTEM_PROMPT = """You are GenreGoblin, an elite comedy rewriter using a compact model. |
| Rewrite the plain message in the requested genre. Preserve the exact practical meaning. |
| Do not add threats, abuse, invented facts, promises, or new requests. Keep it copyable. |
| The humor must come from the genre's logic, not random adjectives, generic hype, or references |
| to being an AI. Use one vivid, specific comic image and a strong final beat. Intensity 1 is |
| subtle; 5 is absurd but clear. Return only one valid JSON object with exactly two string keys: |
| "final_message" and "short_version". No markdown and no commentary.""" |
|
|
|
|
| CSS = """ |
| :root { |
| --ink: #f4ead4; |
| --muted: #b9ad98; |
| --parchment: #2a211d; |
| --parchment-2: #352921; |
| --night: #100c16; |
| --green: #b9ff5a; |
| --green-deep: #709d28; |
| --purple: #8d5cff; |
| --gold: #ddb867; |
| --danger: #ff7a8a; |
| } |
| |
| .gradio-container { |
| background: |
| radial-gradient(circle at 8% 0%, rgba(141, 92, 255, .22), transparent 30rem), |
| radial-gradient(circle at 92% 12%, rgba(185, 255, 90, .12), transparent 26rem), |
| linear-gradient(145deg, #0d0a12 0%, #17111d 55%, #100d0d 100%) !important; |
| color: var(--ink) !important; |
| font-family: Inter, ui-sans-serif, system-ui, sans-serif !important; |
| } |
| |
| .gradio-container .main { |
| max-width: 1180px !important; |
| } |
| |
| .gg-hero { |
| position: relative; |
| overflow: hidden; |
| padding: 1.35rem 1.55rem; |
| margin: .35rem 0 .8rem; |
| border: 1px solid rgba(221, 184, 103, .48); |
| border-radius: 18px; |
| background: linear-gradient(135deg, rgba(53,41,33,.96), rgba(26,19,31,.94)); |
| box-shadow: 0 22px 70px rgba(0,0,0,.38), inset 0 1px rgba(255,255,255,.06); |
| } |
| |
| .gg-hero:after { |
| content: "GG"; |
| position: absolute; |
| right: 2rem; |
| top: -.55rem; |
| color: rgba(185,255,90,.08); |
| font-family: Georgia, serif; |
| font-size: 6.5rem; |
| font-weight: 900; |
| transform: rotate(-7deg); |
| } |
| |
| .gg-kicker { |
| color: var(--green); |
| font-size: .76rem; |
| font-weight: 800; |
| letter-spacing: .19em; |
| text-transform: uppercase; |
| } |
| |
| .gg-hero h1 { |
| color: #fff7e4; |
| font-family: Georgia, "Times New Roman", serif; |
| font-size: clamp(2rem, 4vw, 3.35rem); |
| line-height: 1; |
| letter-spacing: -.045em; |
| margin: .55rem 0 .8rem; |
| max-width: 920px; |
| } |
| |
| .gg-hero h1 span { color: var(--green); text-shadow: 0 0 28px rgba(185,255,90,.2); } |
| .gg-hero p { color: #cfc0aa; font-size: .94rem; max-width: 760px; margin: 0; } |
| |
| .gg-badges { display: flex; gap: .45rem; flex-wrap: wrap; margin-top: .85rem; } |
| .gg-badge { |
| padding: .35rem .65rem; |
| border: 1px solid rgba(221,184,103,.35); |
| border-radius: 99px; |
| background: rgba(10,8,14,.32); |
| color: #e2d4b9; |
| font-size: .75rem; |
| } |
| .gg-badge.live { border-color: rgba(185,255,90,.5); color: var(--green); } |
| |
| .gg-card, .gg-output-card { |
| border: 1px solid rgba(221,184,103,.25) !important; |
| border-radius: 16px !important; |
| background: linear-gradient(155deg, rgba(47,36,31,.93), rgba(28,21,32,.95)) !important; |
| box-shadow: 0 14px 35px rgba(0,0,0,.24) !important; |
| padding: .85rem !important; |
| } |
| |
| .gg-section-title { |
| color: var(--gold); |
| font-family: Georgia, serif; |
| font-size: 1.05rem; |
| margin: .05rem 0 .55rem; |
| } |
| |
| .gradio-container label span, .gradio-container .label-wrap span { |
| color: #dfd1b9 !important; |
| } |
| |
| .gradio-container textarea, |
| .gradio-container input, |
| .gradio-container [data-testid="textbox"] { |
| color: #fff6e8 !important; |
| } |
| |
| .gg-primary { |
| background: linear-gradient(135deg, #a9ee4b, #d8ff8f) !important; |
| color: #172008 !important; |
| border: 0 !important; |
| font-weight: 900 !important; |
| box-shadow: 0 8px 22px rgba(134,201,48,.24) !important; |
| } |
| |
| .gg-secondary { |
| border-color: rgba(141,92,255,.65) !important; |
| color: #e1d6ff !important; |
| background: rgba(87,53,142,.25) !important; |
| } |
| |
| .gg-example { |
| min-width: 0 !important; |
| font-size: .78rem !important; |
| border-color: rgba(221,184,103,.26) !important; |
| background: rgba(20,15,25,.62) !important; |
| color: #d7cab3 !important; |
| } |
| |
| .gg-result-head { |
| padding: .62rem .8rem; |
| border-left: 4px solid var(--green); |
| border-radius: 10px; |
| background: rgba(185,255,90,.07); |
| color: #e9ffd0; |
| } |
| |
| .gg-result-head strong { color: var(--green); } |
| .gg-commentary { |
| margin: .55rem 0; |
| padding: .62rem .8rem; |
| border: 1px dashed rgba(141,92,255,.5); |
| border-radius: 12px; |
| color: #d8c9f3; |
| background: rgba(84,50,133,.12); |
| } |
| |
| .gg-meters { display: grid; grid-template-columns: repeat(3, 1fr); gap: .7rem; margin: .8rem 0; } |
| .gg-meter, .gg-safe { |
| border: 1px solid rgba(221,184,103,.22); |
| border-radius: 12px; |
| background: rgba(12,9,16,.5); |
| padding: .75rem; |
| } |
| .gg-meter-top { display:flex; justify-content:space-between; color:#d9ccb7; font-size:.8rem; } |
| .gg-meter-top b { color:#fff5df; font-size:1rem; } |
| .gg-track { height: 8px; border-radius: 9px; background:#1b1720; margin-top:.55rem; overflow:hidden; } |
| .gg-fill { height:100%; border-radius:9px; background:linear-gradient(90deg,var(--green-deep),var(--green)); } |
| .gg-fill.chaos { background:linear-gradient(90deg,#6540aa,#b98cff); } |
| .gg-safe { display:flex; flex-direction:column; justify-content:center; } |
| .gg-safe span { color:#a99d89; font-size:.72rem; text-transform:uppercase; letter-spacing:.08em; } |
| .gg-safe b { color:var(--green); font-size:1.2rem; text-transform:uppercase; } |
| .gg-safe.maybe b { color:var(--gold); } |
| .gg-safe.no b { color:var(--danger); } |
| |
| .gg-dna-grid { display:grid; grid-template-columns:repeat(2,1fr); gap:.65rem; } |
| .gg-dna-item { padding:.75rem; border-radius:11px; background:rgba(11,8,15,.45); border:1px solid rgba(221,184,103,.18); } |
| .gg-dna-item span { display:block; color:#9f927e; font-size:.69rem; text-transform:uppercase; letter-spacing:.08em; margin-bottom:.3rem; } |
| .gg-dna-item b, .gg-dna-item div { color:#eee1c9; } |
| .gg-dna-item.wide { grid-column:1/-1; } |
| .gg-trace-step { display:grid; grid-template-columns:30px 1fr; gap:.7rem; padding:.62rem 0; border-bottom:1px solid rgba(221,184,103,.12); } |
| .gg-trace-step:last-child { border-bottom:0; } |
| .gg-trace-num { width:28px; height:28px; border-radius:50%; display:grid; place-items:center; background:rgba(185,255,90,.12); color:var(--green); font-weight:800; } |
| .gg-trace-copy b { display:block; color:#f2e5cf; font-size:.86rem; } |
| .gg-trace-copy span { color:#a99c88; font-size:.78rem; } |
| |
| .gg-share { |
| padding:1.25rem; |
| border:1px solid rgba(221,184,103,.4); |
| border-radius:16px; |
| background:linear-gradient(145deg,#241b1b,#1b1524); |
| } |
| .gg-share-kicker { color:var(--green); text-transform:uppercase; letter-spacing:.12em; font-size:.7rem; font-weight:800; } |
| .gg-share h3 { color:#fff1d4; font-family:Georgia,serif; margin:.45rem 0; } |
| .gg-share .original { color:#9f927f; font-style:italic; } |
| .gg-share .output { color:#f7ead3; font-size:1.05rem; padding:.85rem 0; } |
| .gg-share-foot { color:#b89cf4; font-size:.75rem; } |
| |
| .gg-history-item { padding:.75rem 0; border-bottom:1px solid rgba(221,184,103,.12); } |
| .gg-history-item:last-child { border-bottom:0; } |
| .gg-history-item span { color:var(--green); font-size:.72rem; text-transform:uppercase; } |
| .gg-history-item p { color:#dfd1ba; margin:.25rem 0 0; } |
| .gg-empty { color:#938673; padding:1rem 0; } |
| |
| .gg-voice-status { |
| color: #d8c9f3; |
| font-size: .78rem; |
| padding: .3rem .1rem .1rem; |
| } |
| .gg-voice-status b { color: var(--green); } |
| .gg-voice-status.muted { color: #8f8372; } |
| .gg-action-row button { min-width: 0 !important; } |
| .gg-details { margin-top: .35rem !important; } |
| .gg-details > .wrap { border-color: rgba(221,184,103,.2) !important; } |
| .gg-output-card textarea { font-size: 1rem !important; line-height: 1.55 !important; } |
| .gg-compact-audio { margin-top: .25rem !important; } |
| .gg-compact-audio audio { height: 38px !important; } |
| |
| .gg-note { color:#978b79; font-size:.75rem; text-align:center; padding:1rem 0 2rem; } |
| |
| @media (max-width: 720px) { |
| .gg-hero { padding:1.5rem; } |
| .gg-meters, .gg-dna-grid { grid-template-columns:1fr; } |
| .gg-dna-item.wide { grid-column:auto; } |
| } |
| """ |
|
|
|
|
| def _stable_pick(options: list[str], seed: str) -> str: |
| digest = hashlib.sha256(seed.encode("utf-8")).hexdigest() |
| return options[int(digest[:8], 16) % len(options)] |
|
|
|
|
| def _clean_message(message: str) -> str: |
| return re.sub(r"\s+", " ", (message or "").strip())[:1600] |
|
|
|
|
| def _sentence(message: str) -> str: |
| clean = _clean_message(message) |
| if not clean: |
| return "" |
| return clean if clean[-1] in ".!?" else clean + "." |
|
|
|
|
| def _is_unsafe(message: str) -> bool: |
| lower = message.lower() |
| return any(re.search(pattern, lower) for pattern in HARMFUL_PATTERNS) |
|
|
|
|
| def _custom_profile(custom_genre: str) -> GenreProfile: |
| label = _clean_message(custom_genre) or "Mysterious Storyteller" |
| words = [word.lower() for word in re.findall(r"[A-Za-z][A-Za-z'-]+", label)[:3]] |
| vocabulary = (words + ["flourish", "dramatic", "remarkable"])[:5] |
| return GenreProfile( |
| vocabulary=vocabulary, |
| rhythm=f"Cadence inspired by {label}, with a clear practical core.", |
| signature_phrase=f"In true {label} fashion", |
| emotional_temperature="Custom-tailored theatrical energy", |
| forbidden_words=["generic", "boring", "meaningless"], |
| ) |
|
|
|
|
| def get_profile(genre: str, custom_genre: str = "") -> tuple[str, GenreProfile]: |
| if custom_genre.strip(): |
| custom = _clean_message(custom_genre) |
| return custom, _custom_profile(custom) |
| return genre, GENRE_PROFILES.get(genre, GENRE_PROFILES["Shakespeare Villain"]) |
|
|
|
|
| def _safe_alternative(message: str, genre: str) -> tuple[str, str]: |
| safe_core = ( |
| "I am upset, but I do not want this conversation to become threatening. " |
| "Let us pause and resolve this safely." |
| ) |
| if genre == "Shakespeare Villain": |
| final = "Mark me well: I am angered, yet no threat shall cross my lips. Let us pause and settle this with reason." |
| elif genre == "Corporate Apology": |
| final = "For clarity, emotions are running high. I am stepping back so we can continue this conversation safely and constructively." |
| else: |
| final = safe_core |
| return final, "I am upset. Let us pause and resolve this safely." |
|
|
|
|
| def _trim_at_word(text: str, limit: int) -> str: |
| if len(text) <= limit: |
| return text |
| clipped = text[: max(1, limit - 1)].rsplit(" ", 1)[0].rstrip(" ,;:") |
| return clipped + "…" |
|
|
|
|
| def _fit_use_case( |
| final: str, |
| short: str, |
| use_case: str, |
| message: str, |
| intensity: int, |
| ) -> tuple[str, str]: |
| """Keep deterministic output useful even when the model backend is unavailable.""" |
| source = message.lower() |
| if use_case == "WhatsApp": |
| final = _trim_at_word(final, 520) |
| short = _trim_at_word(short, 220) |
| elif use_case == "Email": |
| final = f"Hello,\n\n{final}\n\nThank you." |
| elif use_case == "Tweet/Post": |
| final = _trim_at_word(final, 278) |
| short = _trim_at_word(short, 220) |
| elif use_case == "Apology" and not re.search(r"\b(?:sorry|apolog|regret)\b", final.lower()): |
| final = f"I owe you a clear apology. {final}" |
| short = f"I am sorry. {short}" |
| elif use_case == "Invitation" and not re.search(r"\b(?:invite|join|come|party)\b", source): |
| final = f"You are warmly invited. {final}" |
| elif use_case == "Excuse": |
| final = f"The honest update is this: {final}" |
| elif use_case == "Flirt but safe/non-explicit": |
| final = f"No pressure, only a little theatrical charm: {final}" |
| short = f"No pressure: {short}" |
| elif use_case == "Complaint": |
| final = f"For clarity, here is the issue: {final}" |
|
|
| if intensity <= 2 and use_case in {"WhatsApp", "Email", "Reminder", "Apology"}: |
| final = _trim_at_word(final, 560) |
| return final, short |
|
|
|
|
| def _fallback_stylize( |
| message: str, |
| genre: str, |
| intensity: int, |
| profile: GenreProfile, |
| use_case: str, |
| ) -> tuple[str, str]: |
| text = _sentence(message) |
| bare = text.rstrip(".!?") |
| seed = f"{message}|{genre}|{intensity}" |
| intensity = max(1, min(5, int(intensity))) |
|
|
| templates: dict[str, list[str]] = { |
| "Shakespeare Villain": [ |
| "A modest word, if thou wilt: {text}", |
| "Mark me well, good mortal: {text} I await thy answer.", |
| "Hark. The matter before us is plain: {text} Let fate record what follows.", |
| "Attend me, for the candles gutter at this truth: {text} Defy it, and the very curtains shall judge thee.", |
| "Silence in the hall! I have consulted fate, thunder, and one deeply suspicious raven: {text} Let every trembling bell proclaim it.", |
| ], |
| "Corporate Apology": [ |
| "A quick note: {text} Thank you for your understanding.", |
| "We want to acknowledge the situation: {text} We appreciate your patience as we address it.", |
| "We recognize that expectations were not fully met. Specifically, {bare}. We take this seriously and are acting accordingly.", |
| "Following a comprehensive review of the incident, we can confirm the core issue: {bare}. We sincerely regret the disruption and remain committed to a better path forward.", |
| "After convening an unnecessarily large cross-functional task force, we acknowledge the following critical development: {bare}. We take full accountability and have activated the ceremonial roadmap of continuous improvement.", |
| ], |
| "Indian Parent WhatsApp": [ |
| "Beta, small thing: {text} Please.", |
| "Beta, reminder: {text} Message me when done, okay?", |
| "Beta please listen once: {text} I am saying this for your own good. Reply when you see this.", |
| "Beta, I have reminded you with love, patience, and now capital letters: {text} Please do it and send one message. That is all.", |
| "BETA. Entire household committee has reviewed the matter: {text} Do it, eat something, and message me. I will know if you only send a thumbs-up.", |
| ], |
| "Anime Power-Up Speech": [ |
| "Listen. {text} I know we can do this.", |
| "{text} This is the moment we push past our limits!", |
| "I have waited through every setback for this moment. {text} My resolve will not break!", |
| "The world said it could not be done. My calendar said otherwise. {text} Now witness the power of absolute determination!", |
| "For every unread message, every missed deadline, every dramatic flashback: {text} I summon my final form, ADMINISTRATIVE RESOLVE!", |
| ], |
| "Pirate Warning": [ |
| "Ahoy, matey: {text}", |
| "Hear this, matey: {text} Mark the tide.", |
| "All hands take heed! {text} Best not make the captain ask twice.", |
| "By salt, storm, and the last decent biscuit aboard: {text} Heed the warning before the tide turns.", |
| "AHOY! The sea has spoken, the compass is spinning, and the parrot has filed a formal complaint: {text} Act before we hoist the consequences!", |
| ], |
| "Haunted Railway Announcement": [ |
| "Attention, passengers: {text}", |
| "Attention, passengers. {text} Thank you for your cooperation.", |
| "Attention, passengers on Platform 3: {text} Please remain calm and do not acknowledge the empty carriage.", |
| "This is the final announcement for all living passengers: {text} The delayed service will arrive shortly, although the station clock disagrees.", |
| "Passengers are advised that the midnight service has remembered your name. {text} Mind the gap between worlds, and under no circumstances follow the conductor with no reflection.", |
| ], |
| "Courtroom Confession": [ |
| "Let the record show: {text}", |
| "Your Honor, I submit the following fact: {text}", |
| "I have reviewed the evidence, and the truth is unavoidable: {text} I rest my case.", |
| "Your Honor, despite counsel's objections and the suspicious silence of the jury, I must confess: {text} Let the record reflect my devastating honesty.", |
| "The evidence is overwhelming, the gallery is gasping, and the court stenographer has run out of ink: {text} I accept the verdict of history.", |
| ], |
| "Medieval Royal Decree": [ |
| "By order of the crown: {text}", |
| "Hear ye, good people: {text} Let it be done.", |
| "By royal decree, sealed beneath the watchful crown: {text} All loyal subjects shall take heed.", |
| "Let trumpets sound across the realm! The crown hereby commands: {text} Delay shall be considered deeply unfashionable.", |
| "By the ancient seal, seven trumpets, and one exhausted royal herald: {text} So it is decreed across every hill, hall, and group chat.", |
| ], |
| "Film Noir Detective": [ |
| "The case was simple: {text}", |
| "The city had questions. I had one fact: {text}", |
| "Rain worked the windows while the truth sat under a tired lamp: {text} Case closed, if anything in this town ever closes.", |
| "It was the kind of night that made deadlines nervous. Then the message walked in: {text} I knew trouble had excellent timing.", |
| "The city was soaked, my coffee had an alibi, and every shadow owed me money. One fact remained: {text} That was when the saxophone started judging us.", |
| ], |
| "Startup Founder Pitch": [ |
| "Here is the key update: {text}", |
| "We found the friction point: {bare}. Here is the unlock.", |
| "Users need clarity, and the opportunity is obvious: {text} This is how we move faster.", |
| "We are reimagining the category around one urgent insight: {bare}. The market is ready; now we execute.", |
| "After twelve coffees and a whiteboard that now qualifies as modern art, we found the billion-dollar unlock: {bare}. This scales from one group chat to humanity.", |
| ], |
| "Bureaucratic Government Notice": [ |
| "Please be advised: {text}", |
| "Official notice: {text} Your cooperation is appreciated.", |
| "Pursuant to the relevant common-sense provisions, please be advised that {bare}. Compliance is requested at the earliest convenience.", |
| "This notice serves as formal confirmation that {bare}. Acknowledgment is mandatory; triplicate paperwork is spiritually encouraged.", |
| "Under Section 47(B) of the Extremely Obvious Matters Act, all concerned parties are hereby notified that {bare}. Failure to comply may result in an additional notice explaining this notice.", |
| ], |
| "Overdramatic Fantasy Quest": [ |
| "A small quest awaits: {text}", |
| "Gather your courage, for the task is clear: {text}", |
| "The map is drawn and the fellowship assembled. Our quest: {text} May fortune favor the prepared.", |
| "Across the realm, bells announce a duty too great to ignore: {text} Take up the quest before the final torch fades.", |
| "The prophecy has awakened, the mountains are humming, and an ancient wizard has misplaced the instructions: {text} Go now, for the fate of the realm and possibly dinner depends upon it!", |
| ], |
| } |
|
|
| if genre in templates: |
| final = templates[genre][intensity - 1].format(text=text, bare=bare) |
| else: |
| generic = [ |
| "In a {genre} voice: {text}", |
| "{signature}: {text}", |
| "{signature}. Let every word carry the style of {genre}: {text}", |
| "The scene belongs entirely to {genre}. With appropriate ceremony: {text}", |
| "Reality pauses, the spotlight arrives, and {genre} takes command: {text} Thus concludes the spectacle.", |
| ] |
| final = generic[intensity - 1].format( |
| genre=genre, |
| text=text, |
| signature=profile.signature_phrase, |
| ) |
|
|
| short_options = { |
| "Shakespeare Villain": f"Mark me well: {text}", |
| "Corporate Apology": f"We acknowledge this: {text}", |
| "Indian Parent WhatsApp": f"Beta, {text} Message me.", |
| "Anime Power-Up Speech": f"{text} We push past our limits!", |
| "Pirate Warning": f"Ahoy: {text}", |
| "Haunted Railway Announcement": f"Attention, passengers: {text}", |
| "Courtroom Confession": f"Let the record show: {text}", |
| "Medieval Royal Decree": f"By royal decree: {text}", |
| "Film Noir Detective": f"The case was simple: {text}", |
| "Startup Founder Pitch": f"Here is the unlock: {text}", |
| "Bureaucratic Government Notice": f"Please be advised: {text}", |
| "Overdramatic Fantasy Quest": f"Your quest: {text}", |
| } |
| short = short_options.get(genre, f"{profile.signature_phrase}: {text}") |
| markers = [item.lower() for item in profile.vocabulary + [profile.signature_phrase]] |
| if not any(marker in final.lower() for marker in markers): |
| final = f"{profile.signature_phrase}: {final}" |
| if short.strip().lower() == final.strip().lower(): |
| short = f"{profile.vocabulary[0].title()}: {text}" |
| flourish = _stable_pick( |
| ["The ink is still smoking.", "Usable, but with suspiciously good lighting.", "The drama has been responsibly contained."], |
| seed, |
| ) |
| short += "" if intensity < 5 else f" {flourish}" |
| return _fit_use_case(final, short, use_case, message, intensity) |
|
|
|
|
| def _scores(intensity: int, use_case: str, unsafe: bool) -> tuple[int, int, str]: |
| sendability = 102 - (intensity * 10) |
| chaos = 5 + (intensity * 18) |
| if use_case in {"Email", "Apology", "Complaint"}: |
| sendability -= max(0, intensity - 2) * 4 |
| if use_case in {"Tweet/Post", "Invitation"}: |
| sendability += 3 |
| chaos += 4 |
| if unsafe: |
| return 20, max(chaos, 76), "no" |
| sendability = max(18, min(98, sendability)) |
| chaos = max(8, min(99, chaos)) |
| safe = "yes" if sendability >= 70 else "maybe" if sendability >= 45 else "no" |
| return sendability, chaos, safe |
|
|
|
|
| def _trace(message: str, genre: str, intensity: int, use_case: str, safe: str) -> list[dict[str, str]]: |
| intent = _clean_message(message) |
| if len(intent) > 88: |
| intent = intent[:85] + "..." |
| return [ |
| {"step": "Read plain intent", "note": f'Identified the core request: "{intent}"'}, |
| {"step": "Apply genre lens", "note": f"Loaded the vocabulary, rhythm, and signature moves for {genre}."}, |
| {"step": "Preserve meaning", "note": "Kept the original action and facts intact; added style, not new commitments."}, |
| {"step": "Adjust intensity", "note": f"Set theatrical pressure to {intensity}/5 for a {use_case.lower()} context."}, |
| {"step": "Sendability check", "note": f"Safety and practical-use verdict: {safe.upper()}."}, |
| {"step": "Final goblin polish", "note": "Tightened the cadence, protected readability, and added one memorable flourish."}, |
| ] |
|
|
|
|
| def deterministic_rewrite( |
| message: str, |
| genre: str, |
| custom_genre: str, |
| intensity: int, |
| use_case: str, |
| ) -> RewriteResult: |
| resolved_genre, profile = get_profile(genre, custom_genre) |
| unsafe = _is_unsafe(message) |
| if unsafe: |
| final, short = _safe_alternative(message, resolved_genre) |
| else: |
| final, short = _fallback_stylize( |
| message, |
| resolved_genre, |
| intensity, |
| profile, |
| use_case, |
| ) |
| sendability, chaos, safe = _scores(intensity, use_case, unsafe) |
| comment = ( |
| "The dangerous part was removed; the message now sets a firm, non-threatening boundary." |
| if unsafe |
| else _stable_pick( |
| [ |
| "Meaning preserved. Dignity was offered a chair but declined.", |
| "A practical message wearing extremely impractical sleeves.", |
| "Theatrical enough to remember, coherent enough to send.", |
| "The style is loud; the actual request remains mercifully clear.", |
| ], |
| f"{message}|{resolved_genre}|comment", |
| ) |
| ) |
| return RewriteResult( |
| genre_dna=profile, |
| final_message=final, |
| short_version=short, |
| sendability_score=sendability, |
| chaos_score=chaos, |
| probably_safe_to_send=safe, |
| goblin_commentary=comment, |
| agent_trace=_trace(message, resolved_genre, intensity, use_case, safe), |
| ) |
|
|
|
|
| def build_model_prompt( |
| message: str, |
| genre: str, |
| profile: GenreProfile, |
| intensity: int, |
| use_case: str, |
| history: list[dict[str, Any]] | None = None, |
| ) -> str: |
| history = [ |
| item |
| for item in (history or []) |
| if item.get("genre") == genre |
| ][-3:] |
| style_memory = [ |
| {"plain": item.get("original", ""), "stylized": item.get("final", "")} |
| for item in history |
| ] |
| intensity_guide = { |
| "1": "subtle and highly usable", |
| "2": "stylized but sendable", |
| "3": "dramatic", |
| "4": "ridiculous but coherent", |
| "5": "wildly theatrical but still understandable", |
| }[str(intensity)] |
| memory_text = "None" |
| if style_memory: |
| memory_text = " | ".join( |
| f"{item['plain']} -> {item['stylized']}" for item in style_memory |
| ) |
| safe_draft, safe_short = _fallback_stylize( |
| message, |
| genre, |
| intensity, |
| profile, |
| use_case, |
| ) |
| meaning_words = [ |
| word |
| for word in re.findall(r"[A-Za-z]{3,}", message.lower()) |
| if word |
| not in { |
| "the", |
| "and", |
| "but", |
| "can", |
| "could", |
| "would", |
| "you", |
| "your", |
| "please", |
| "this", |
| "that", |
| "with", |
| "from", |
| "have", |
| "need", |
| } |
| ] |
| required_words = ", ".join(meaning_words) or "the original practical meaning" |
| use_case_rule = USE_CASE_RULES.get( |
| use_case, |
| "Keep the message concise, practical, and suitable for the selected context.", |
| ) |
| comedy_engine = COMEDY_ENGINES.get( |
| genre, |
| f"Commit fully to {genre} while keeping the concrete request easy to find.", |
| ) |
| return f"""{SYSTEM_PROMPT} |
| |
| Plain message: {message} |
| Genre: {genre} |
| Use-case: {use_case} |
| Use-case contract: {use_case_rule} |
| Intensity: {intensity}/5 ({intensity_guide}) |
| Vocabulary hints: {", ".join(profile.vocabulary)} |
| Rhythm: {profile.rhythm} |
| Signature phrase: {profile.signature_phrase} |
| Comedy engine: {comedy_engine} |
| Avoid these words: {", ".join(profile.forbidden_words)} |
| Recent style examples: {memory_text} |
| Meaning words that must remain explicit: {required_words} |
| |
| Safe full draft to polish: {safe_draft} |
| Safe short draft to polish: {safe_short} |
| |
| Edit the safe drafts into genuinely sharp genre comedy. Do not replace their meaning with a |
| generic speech. Keep the concrete action explicit in both fields. Use at least one vocabulary |
| hint or the signature phrase naturally. Follow the use-case contract exactly. Prefer one |
| specific comic image over several vague dramatic adjectives. End on the funniest clean beat. |
| |
| Required output: |
| {{"final_message":"the full rewrite","short_version":"a shorter rewrite"}}""" |
|
|
|
|
| def _extract_api_text(payload: Any) -> str: |
| if isinstance(payload, str): |
| return payload |
| if isinstance(payload, list) and payload: |
| return _extract_api_text(payload[0]) |
| if not isinstance(payload, dict): |
| raise ValueError("Inference response did not contain text.") |
| generated_text = payload.get("generated_text") |
| if isinstance(generated_text, str): |
| return generated_text |
| if isinstance(generated_text, list) and generated_text: |
| last_message = generated_text[-1] |
| if isinstance(last_message, dict) and isinstance(last_message.get("content"), str): |
| return last_message["content"] |
| if isinstance(payload.get("text"), str): |
| return payload["text"] |
| choices = payload.get("choices") |
| if isinstance(choices, list) and choices: |
| first = choices[0] |
| if isinstance(first, dict): |
| message = first.get("message") |
| if isinstance(message, dict) and isinstance(message.get("content"), str): |
| return message["content"] |
| if isinstance(first.get("text"), str): |
| return first["text"] |
| for key in ("data", "output", "response"): |
| if key in payload: |
| try: |
| return _extract_api_text(payload[key]) |
| except ValueError: |
| continue |
| raise ValueError("Inference response did not contain generated text.") |
|
|
|
|
| def _hosted_generate(prompt: str, max_new_tokens: int) -> str: |
| use_modal = os.getenv("USE_MODAL", "").lower() in {"1", "true", "yes"} |
| url = os.getenv("MODAL_API_URL") if use_modal else None |
| url = url or os.getenv("MINICPM_API_URL") |
| if not url: |
| raise RuntimeError("No hosted MiniCPM endpoint configured.") |
|
|
| key = os.getenv("MINICPM_API_KEY") or os.getenv("HF_TOKEN") |
| headers = {"Content-Type": "application/json"} |
| if key: |
| headers["Authorization"] = f"Bearer {key}" |
|
|
| api_format = os.getenv("MINICPM_API_FORMAT", "").lower() |
| if "chat/completions" in url or api_format == "openai": |
| payload = { |
| "model": MODEL_ID, |
| "messages": [{"role": "user", "content": prompt}], |
| "max_tokens": max_new_tokens, |
| "temperature": 0.55, |
| } |
| else: |
| payload = { |
| "inputs": prompt, |
| "parameters": { |
| "max_new_tokens": max_new_tokens, |
| "temperature": 0.55, |
| "do_sample": True, |
| "return_full_text": False, |
| }, |
| } |
| response = requests.post(url, headers=headers, json=payload, timeout=90) |
| response.raise_for_status() |
| return _extract_api_text(response.json()) |
|
|
|
|
| @lru_cache(maxsize=1) |
| def _local_pipeline() -> Any: |
| from transformers import pipeline |
|
|
| token = os.getenv("HF_TOKEN") or None |
| return pipeline( |
| "text-generation", |
| model=MODEL_ID, |
| token=token, |
| device_map="auto", |
| trust_remote_code=True, |
| ) |
|
|
|
|
| def _local_generate(prompt: str, max_new_tokens: int) -> str: |
| generator = _local_pipeline() |
| output = generator( |
| [{"role": "user", "content": prompt}], |
| max_new_tokens=max_new_tokens, |
| do_sample=True, |
| temperature=0.55, |
| return_full_text=False, |
| ) |
| return _extract_api_text(output) |
|
|
|
|
| def generate_with_model(prompt: str, max_new_tokens: int = 350) -> str: |
| """Generate with hosted/Modal MiniCPM or an explicitly enabled local model.""" |
| has_hosted = bool(os.getenv("MINICPM_API_URL")) or ( |
| os.getenv("USE_MODAL", "").lower() in {"1", "true", "yes"} and bool(os.getenv("MODAL_API_URL")) |
| ) |
| if has_hosted: |
| return _hosted_generate(prompt, max_new_tokens) |
| if os.getenv("ENABLE_LOCAL_MODEL", "").lower() in {"1", "true", "yes"}: |
| return _local_generate(prompt, max_new_tokens) |
| raise RuntimeError("No model backend enabled.") |
|
|
|
|
| def safe_parse_json(raw: str) -> dict[str, Any] | None: |
| if not raw: |
| return None |
| candidates = [raw.strip()] |
| fenced = re.sub(r"^```(?:json)?\s*|\s*```$", "", raw.strip(), flags=re.IGNORECASE) |
| candidates.append(fenced) |
| start, end = raw.find("{"), raw.rfind("}") |
| if start >= 0 and end > start: |
| candidates.append(raw[start : end + 1]) |
| for candidate in candidates: |
| try: |
| parsed = json.loads(candidate) |
| if isinstance(parsed, dict): |
| return parsed |
| except (json.JSONDecodeError, TypeError): |
| continue |
| salvaged: dict[str, str] = {} |
| for key in ("final_message", "short_version"): |
| match = re.search( |
| rf'"{key}"\s*:\s*"((?:\\.|[^"\\])*)"', |
| raw, |
| flags=re.DOTALL, |
| ) |
| if match: |
| try: |
| salvaged[key] = json.loads(f'"{match.group(1)}"') |
| except json.JSONDecodeError: |
| salvaged[key] = match.group(1) |
| return salvaged or None |
|
|
|
|
| def _model_output_is_usable( |
| message: str, |
| result: RewriteResult, |
| genre: str, |
| profile: GenreProfile, |
| use_case: str, |
| ) -> bool: |
| output = result.final_message.lower() |
| short = result.short_version.lower() |
| if len(output) < 4 or len(output) > 2800: |
| return False |
| if short in {"a shorter rewrite", "shorter rewrite", "short version"}: |
| return False |
| source_words = { |
| word |
| for word in re.findall(r"[a-z]{3,}", message.lower()) |
| if word |
| not in { |
| "the", |
| "and", |
| "but", |
| "can", |
| "could", |
| "would", |
| "you", |
| "your", |
| "please", |
| "this", |
| "that", |
| "with", |
| "from", |
| "have", |
| "need", |
| } |
| } |
| if source_words and not any(word in output for word in source_words): |
| return False |
| genre_markers = [ |
| marker.lower() |
| for marker in profile.vocabulary + [profile.signature_phrase] |
| if len(marker.strip()) >= 3 |
| ] |
| if genre in GENRE_PROFILES and not any(marker in output for marker in genre_markers): |
| return False |
| if use_case == "Tweet/Post" and len(result.final_message) > 300: |
| return False |
| if use_case == "WhatsApp" and len(result.final_message) > 700: |
| return False |
| if use_case == "Apology" and not re.search( |
| r"\b(?:sorry|apolog|regret|forgive)\b", |
| output, |
| ): |
| return False |
| if use_case == "Flirt but safe/non-explicit" and any( |
| term in output for term in ("explicit", "naked", "bedroom", "won't take no") |
| ): |
| return False |
| if not _is_unsafe(message): |
| unsafe_flourishes = { |
| "kill", |
| "murder", |
| "fatal", |
| "blood", |
| "die", |
| "death", |
| "destroy you", |
| "vengeance", |
| } |
| if any(term in output for term in unsafe_flourishes): |
| return False |
| if not re.search(r"\b(?:will|guarantee|promise|ensure)\b", message.lower()): |
| invented_promises = { |
| "we will", |
| "i will ensure", |
| "we guarantee", |
| "i promise", |
| "we assure", |
| "assure you", |
| } |
| if any(term in output for term in invented_promises): |
| return False |
| return True |
|
|
|
|
| def _clamp_score(value: Any, fallback: int) -> int: |
| try: |
| return max(0, min(100, int(float(value)))) |
| except (TypeError, ValueError): |
| return fallback |
|
|
|
|
| def normalize_model_result( |
| data: dict[str, Any], |
| fallback: RewriteResult, |
| genre: str, |
| message: str, |
| intensity: int, |
| use_case: str, |
| ) -> RewriteResult: |
| dna = data.get("genre_dna") if isinstance(data.get("genre_dna"), dict) else {} |
| base_dna = fallback.genre_dna |
|
|
| def text_value(value: Any, default: str, limit: int = 2800) -> str: |
| clean = _clean_message(str(value)) if value is not None else "" |
| return clean[:limit] or default |
|
|
| def list_value(value: Any, default: list[str]) -> list[str]: |
| if not isinstance(value, list): |
| return default |
| cleaned = [text_value(item, "", 80) for item in value] |
| return [item for item in cleaned if item][:8] or default |
|
|
| profile = GenreProfile( |
| vocabulary=list_value(dna.get("vocabulary"), base_dna.vocabulary), |
| rhythm=text_value(dna.get("rhythm"), base_dna.rhythm, 220), |
| signature_phrase=text_value(dna.get("signature_phrase"), base_dna.signature_phrase, 120), |
| emotional_temperature=text_value( |
| dna.get("emotional_temperature"), |
| base_dna.emotional_temperature, |
| 160, |
| ), |
| forbidden_words=list_value(dna.get("forbidden_words"), base_dna.forbidden_words), |
| ) |
| safe = str(data.get("probably_safe_to_send", fallback.probably_safe_to_send)).lower() |
| safe = safe if safe in {"yes", "maybe", "no"} else fallback.probably_safe_to_send |
| raw_trace = data.get("agent_trace") |
| trace: list[dict[str, str]] = [] |
| if isinstance(raw_trace, list): |
| for item in raw_trace[:8]: |
| if isinstance(item, dict): |
| step = text_value(item.get("step"), "", 80) |
| note = text_value(item.get("note"), "", 300) |
| if step and note: |
| trace.append({"step": step, "note": note}) |
| if len(trace) < 4: |
| trace = _trace(message, genre, intensity, use_case, safe) |
|
|
| return RewriteResult( |
| genre_dna=profile, |
| final_message=text_value(data.get("final_message"), fallback.final_message), |
| short_version=text_value(data.get("short_version"), fallback.short_version, 900), |
| sendability_score=_clamp_score(data.get("sendability_score"), fallback.sendability_score), |
| chaos_score=_clamp_score(data.get("chaos_score"), fallback.chaos_score), |
| probably_safe_to_send=safe, |
| goblin_commentary=text_value(data.get("goblin_commentary"), fallback.goblin_commentary, 320), |
| agent_trace=trace, |
| ) |
|
|
|
|
| class GoblinRewriteAgent: |
| """Small autonomous loop: draft, parse, validate, repair once, then recover.""" |
|
|
| def __init__( |
| self, |
| message: str, |
| genre: str, |
| profile: GenreProfile, |
| intensity: int, |
| use_case: str, |
| history: list[dict[str, Any]] | None, |
| fallback: RewriteResult, |
| ) -> None: |
| self.message = message |
| self.genre = genre |
| self.profile = profile |
| self.intensity = intensity |
| self.use_case = use_case |
| self.history = history |
| self.fallback = fallback |
|
|
| def _mode(self) -> str: |
| use_modal = os.getenv("USE_MODAL", "").lower() in {"1", "true", "yes"} |
| if use_modal: |
| return "MiniCPM3-4B via Modal" |
| if os.getenv("ENABLE_LOCAL_MODEL", "").lower() in {"1", "true", "yes"}: |
| return "MiniCPM3-4B local" |
| return "MiniCPM3-4B hosted" |
|
|
| def run(self) -> tuple[RewriteResult, str]: |
| prompt = build_model_prompt( |
| self.message, |
| self.genre, |
| self.profile, |
| self.intensity, |
| self.use_case, |
| self.history, |
| ) |
| failure = "model draft did not pass validation" |
|
|
| for attempt in range(1, 3): |
| try: |
| raw = generate_with_model(prompt) |
| except Exception: |
| return self.fallback, "Fallback recovered from unavailable model" |
|
|
| parsed = safe_parse_json(raw) |
| if not parsed: |
| failure = "model returned malformed JSON" |
| else: |
| result = normalize_model_result( |
| parsed, |
| self.fallback, |
| self.genre, |
| self.message, |
| self.intensity, |
| self.use_case, |
| ) |
| if _model_output_is_usable( |
| self.message, |
| result, |
| self.genre, |
| self.profile, |
| self.use_case, |
| ): |
| result.agent_trace[-1]["note"] = ( |
| f"Accepted the MiniCPM draft after {attempt} validation pass" |
| f"{'es' if attempt > 1 else ''}." |
| ) |
| return result, self._mode() |
| failure = "draft drifted off-intent or introduced unsafe claims" |
|
|
| prompt += f""" |
| |
| REPAIR PASS {attempt}: The previous draft was rejected because {failure}. |
| Return a corrected JSON object now. Copy the practical action from the safe draft explicitly, |
| avoid invented promises or facts, visibly use the {self.genre} vocabulary and comedy engine, |
| obey the {self.use_case} format contract, and include both final_message and short_version.""" |
|
|
| return self.fallback, f"Fallback after agent repair loop: {failure}" |
|
|
|
|
| def rewrite_engine( |
| message: str, |
| genre: str, |
| custom_genre: str, |
| intensity: int, |
| use_case: str, |
| history: list[dict[str, Any]] | None = None, |
| ) -> tuple[RewriteResult, str, str]: |
| fallback = deterministic_rewrite(message, genre, custom_genre, intensity, use_case) |
| resolved_genre, profile = get_profile(genre, custom_genre) |
| use_modal = os.getenv("USE_MODAL", "").lower() in {"1", "true", "yes"} |
| has_backend = ( |
| bool(os.getenv("MINICPM_API_URL")) |
| or (use_modal and bool(os.getenv("MODAL_API_URL"))) |
| or os.getenv("ENABLE_LOCAL_MODEL", "").lower() in {"1", "true", "yes"} |
| ) |
| if not has_backend: |
| return fallback, "Deterministic demo engine", resolved_genre |
|
|
| agent = GoblinRewriteAgent( |
| message, |
| resolved_genre, |
| profile, |
| intensity, |
| use_case, |
| history, |
| fallback, |
| ) |
| result, mode = agent.run() |
| return result, mode, resolved_genre |
|
|
|
|
| def _esc(value: Any) -> str: |
| return html.escape(str(value), quote=True) |
|
|
|
|
| def render_metrics(result: RewriteResult) -> str: |
| safe = result.probably_safe_to_send.lower() |
| return f""" |
| <div class="gg-meters"> |
| <div class="gg-meter"> |
| <div class="gg-meter-top"><span>Sendability</span><b>{result.sendability_score}</b></div> |
| <div class="gg-track"><div class="gg-fill" style="width:{result.sendability_score}%"></div></div> |
| </div> |
| <div class="gg-meter"> |
| <div class="gg-meter-top"><span>Chaos</span><b>{result.chaos_score}</b></div> |
| <div class="gg-track"><div class="gg-fill chaos" style="width:{result.chaos_score}%"></div></div> |
| </div> |
| <div class="gg-safe {_esc(safe)}"><span>Probably safe to send?</span><b>{_esc(safe)}</b></div> |
| </div> |
| """ |
|
|
|
|
| def render_dna(profile: GenreProfile) -> str: |
| vocabulary = " / ".join(_esc(item) for item in profile.vocabulary) |
| forbidden = " / ".join(_esc(item) for item in profile.forbidden_words) |
| return f""" |
| <div class="gg-dna-grid"> |
| <div class="gg-dna-item"><span>Vocabulary</span><div>{vocabulary}</div></div> |
| <div class="gg-dna-item"><span>Signature phrase</span><b>{_esc(profile.signature_phrase)}</b></div> |
| <div class="gg-dna-item"><span>Emotional temperature</span><div>{_esc(profile.emotional_temperature)}</div></div> |
| <div class="gg-dna-item"><span>Forbidden words</span><div>{forbidden}</div></div> |
| <div class="gg-dna-item wide"><span>Rhythm</span><div>{_esc(profile.rhythm)}</div></div> |
| </div> |
| """ |
|
|
|
|
| def render_trace(trace: list[dict[str, str]]) -> str: |
| rows = [] |
| for index, item in enumerate(trace, 1): |
| rows.append( |
| f"""<div class="gg-trace-step"> |
| <div class="gg-trace-num">{index}</div> |
| <div class="gg-trace-copy"><b>{_esc(item.get("step", ""))}</b> |
| <span>{_esc(item.get("note", ""))}</span></div> |
| </div>""" |
| ) |
| return "".join(rows) |
|
|
|
|
| def render_share_card(original: str, genre: str, result: RewriteResult) -> str: |
| return f""" |
| <div class="gg-share"> |
| <div class="gg-share-kicker">Message transformation</div> |
| <h3>{_esc(genre)}</h3> |
| <div class="original">Original: {_esc(original)}</div> |
| <div class="output">{_esc(result.final_message)}</div> |
| <div class="gg-share-foot"> |
| SENDABILITY {result.sendability_score} / CHAOS {result.chaos_score} |
| | Made with GenreGoblin |
| </div> |
| </div> |
| """ |
|
|
|
|
| def build_social_text(original: str, genre: str, result: RewriteResult) -> str: |
| return ( |
| f'PLAIN: "{original}"\n\n' |
| f"GENRE: {genre}\n\n" |
| f"{result.final_message}\n\n" |
| f"Sendability {result.sendability_score}/100 | Chaos {result.chaos_score}/100\n" |
| "Made with GenreGoblin" |
| ) |
|
|
|
|
| def render_history(history: list[dict[str, Any]]) -> str: |
| if not history: |
| return '<div class="gg-empty">Your in-session style chronicle will appear here.</div>' |
| rows = [] |
| for item in reversed(history[-6:]): |
| rows.append( |
| f"""<div class="gg-history-item"> |
| <span>{_esc(item.get("genre", ""))} / {_esc(item.get("use_case", ""))}</span> |
| <p>{_esc(item.get("final", ""))}</p> |
| </div>""" |
| ) |
| return "".join(rows) |
|
|
|
|
| def _empty_state() -> dict[str, Any]: |
| return {"history": [], "settings": {}, "genre_dna": {}} |
|
|
|
|
| def _package_outputs( |
| original: str, |
| resolved_genre: str, |
| result: RewriteResult, |
| mode: str, |
| state: dict[str, Any], |
| ) -> tuple[Any, ...]: |
| status = ( |
| f'<div class="gg-result-head"><strong>{_esc(resolved_genre)}</strong>' |
| f" forged with {_esc(mode)}</div>" |
| ) |
| commentary = f'<div class="gg-commentary"><b>Goblin commentary:</b> {_esc(result.goblin_commentary)}</div>' |
| return ( |
| result.final_message, |
| result.short_version, |
| render_metrics(result), |
| render_dna(result.genre_dna), |
| render_trace(result.agent_trace), |
| render_share_card(original, resolved_genre, result), |
| build_social_text(original, resolved_genre, result), |
| commentary, |
| status, |
| render_history(state["history"]), |
| state, |
| "", |
| None, |
| '<div class="gg-voice-status muted">Generate a genre-matched studio voice when the text is ready.</div>', |
| ) |
|
|
|
|
| def forge_message( |
| message: str, |
| genre: str, |
| custom_genre: str, |
| intensity: int, |
| use_case: str, |
| state: dict[str, Any] | None, |
| ) -> tuple[Any, ...]: |
| message = _clean_message(message) |
| if not message: |
| raise gr.Error("Enter a plain message first.") |
| state = state if isinstance(state, dict) else _empty_state() |
| history = state.get("history", []) |
| result, mode, resolved_genre = rewrite_engine( |
| message, |
| genre, |
| custom_genre, |
| int(intensity), |
| use_case, |
| history, |
| ) |
| entry = { |
| "original": message, |
| "final": result.final_message, |
| "short": result.short_version, |
| "genre": resolved_genre, |
| "use_case": use_case, |
| "intensity": int(intensity), |
| } |
| state = { |
| "history": (history + [entry])[-12:], |
| "settings": { |
| "genre": genre, |
| "resolved_genre": resolved_genre, |
| "custom_genre": custom_genre, |
| "intensity": int(intensity), |
| "use_case": use_case, |
| }, |
| "genre_dna": asdict(result.genre_dna), |
| } |
| return _package_outputs(message, resolved_genre, result, mode, state) |
|
|
|
|
| def continue_same_voice( |
| followup: str, |
| state: dict[str, Any] | None, |
| current_genre: str, |
| current_custom: str, |
| current_intensity: int, |
| current_use_case: str, |
| ) -> tuple[Any, ...]: |
| followup = _clean_message(followup) |
| if not followup: |
| raise gr.Error("Enter the next plain message first.") |
| state = state if isinstance(state, dict) else _empty_state() |
| settings = state.get("settings") or { |
| "genre": current_genre, |
| "custom_genre": current_custom, |
| "intensity": current_intensity, |
| "use_case": current_use_case, |
| } |
| return forge_message( |
| followup, |
| settings.get("genre", current_genre), |
| settings.get("custom_genre", current_custom), |
| int(settings.get("intensity", current_intensity)), |
| settings.get("use_case", current_use_case), |
| state, |
| ) |
|
|
|
|
| def load_example(example: tuple[str, str, str, int]) -> tuple[str, str, str, int, str]: |
| message, genre, use_case, intensity = example |
| return message, genre, use_case, intensity, "" |
|
|
|
|
| def reset_session() -> tuple[Any, ...]: |
| return ( |
| "", |
| GENRES[0], |
| "", |
| 3, |
| USE_CASES[0], |
| "", |
| "", |
| "", |
| "", |
| '<div class="gg-empty">Choose a genre and forge a message to reveal its DNA.</div>', |
| '<div class="gg-empty">The six-step trace appears after a rewrite.</div>', |
| '<div class="gg-empty">Your share card will appear here.</div>', |
| "", |
| "", |
| '<div class="gg-result-head">The forge is waiting for a plain message.</div>', |
| render_history([]), |
| _empty_state(), |
| None, |
| '<div class="gg-voice-status muted">Generate a genre-matched studio voice when the text is ready.</div>', |
| ) |
|
|
|
|
| def current_backend_label() -> str: |
| text_ready = ( |
| os.getenv("USE_MODAL", "").lower() in {"1", "true", "yes"} |
| and os.getenv("MODAL_API_URL") |
| ) |
| voice_ready = bool(os.getenv("VOXCPM_API_URL")) |
| if text_ready and voice_ready: |
| return "MiniCPM + VoxCPM2 on Modal" |
| if text_ready: |
| return "Modal + MiniCPM3-4B ready" |
| if os.getenv("MINICPM_API_URL"): |
| return "Hosted MiniCPM3-4B ready" |
| if os.getenv("ENABLE_LOCAL_MODEL", "").lower() in {"1", "true", "yes"}: |
| return "Local MiniCPM3-4B enabled" |
| return "Instant demo fallback ready" |
|
|
|
|
| def voice_direction_for_genre(genre: str, custom_genre: str = "") -> str: |
| if custom_genre.strip(): |
| custom = re.sub(r"[()()]", "", _clean_message(custom_genre))[:120] |
| return ( |
| f"A distinctive theatrical narrator suited to {custom}, expressive but natural, " |
| "clear diction, emotionally precise, and playful without impersonating a real person." |
| ) |
| return VOICE_DIRECTIONS.get( |
| genre, |
| "A versatile theatrical narrator, expressive and witty, clear diction, natural pacing.", |
| ) |
|
|
|
|
| def generate_genre_voice( |
| text: str, |
| genre: str, |
| custom_genre: str, |
| state: dict[str, Any] | None, |
| ) -> tuple[str | None, str]: |
| text = _clean_message(text) |
| if not text: |
| raise gr.Error("Forge a message before generating its voice.") |
| url = os.getenv("VOXCPM_API_URL", "").strip() |
| if not url: |
| raise gr.Error("VoxCPM2 is not configured. Use Instant preview for browser speech.") |
|
|
| state = state if isinstance(state, dict) else _empty_state() |
| settings = state.get("settings") or {} |
| resolved_genre = settings.get("resolved_genre") or genre |
| resolved_custom = settings.get("custom_genre") or custom_genre |
| direction = voice_direction_for_genre(resolved_genre, resolved_custom) |
| payload = { |
| "text": text[:700], |
| "voice_description": direction, |
| "cfg_value": 2.0, |
| "inference_timesteps": 8, |
| } |
| try: |
| response = requests.post(url, json=payload, timeout=240) |
| response.raise_for_status() |
| if "audio" not in response.headers.get("content-type", "").lower(): |
| raise ValueError("Voice endpoint did not return audio.") |
| if len(response.content) < 2048: |
| raise ValueError("Voice endpoint returned an incomplete audio file.") |
| except Exception as exc: |
| raise gr.Error( |
| "VoxCPM2 voice generation is temporarily unavailable. " |
| "The Instant preview button still works." |
| ) from exc |
|
|
| with tempfile.NamedTemporaryFile( |
| prefix="genregoblin-", |
| suffix=".wav", |
| delete=False, |
| ) as audio_file: |
| audio_file.write(response.content) |
| audio_path = audio_file.name |
| status = ( |
| f'<div class="gg-voice-status"><b>{_esc(resolved_genre)} voice</b>' |
| f" generated by {_esc(VOICE_MODEL_ID)} on Modal.</div>" |
| ) |
| return audio_path, status |
|
|
|
|
| theme = gr.themes.Base( |
| primary_hue=gr.themes.colors.lime, |
| secondary_hue=gr.themes.colors.purple, |
| neutral_hue=gr.themes.colors.stone, |
| ).set( |
| body_background_fill="#100c16", |
| block_background_fill="#241c20", |
| block_border_color="#594836", |
| input_background_fill="#17121b", |
| button_secondary_background_fill="#2b2034", |
| ) |
|
|
|
|
| with gr.Blocks(title=APP_TITLE, fill_width=True) as demo: |
| session_state = gr.State(_empty_state()) |
|
|
| gr.HTML( |
| f""" |
| <section class="gg-hero"> |
| <div class="gg-kicker">The magical message forge</div> |
| <h1>Plain text goes in.<br><span>Theater comes out.</span></h1> |
| <p>Rewrite ordinary messages as villains, apologies, power-up speeches, |
| haunted announcements, royal decrees, and stranger things.</p> |
| <div class="gg-badges"> |
| <span class="gg-badge live">{_esc(current_backend_label())}</span> |
| <span class="gg-badge">4B text + 2B genre voices</span> |
| </div> |
| </section> |
| """ |
| ) |
|
|
| with gr.Row(equal_height=False): |
| with gr.Column(scale=5, elem_classes=["gg-card"]): |
| gr.HTML('<div class="gg-section-title">1. Feed the forge</div>') |
| plain_input = gr.Textbox( |
| label="Plain message", |
| placeholder="e.g. Sorry I forgot to reply", |
| lines=4, |
| max_lines=7, |
| ) |
| genre = gr.Dropdown( |
| choices=GENRES, |
| value=GENRES[0], |
| label="Genre / persona", |
| ) |
| intensity = gr.Slider( |
| 1, |
| 5, |
| value=3, |
| step=1, |
| label="Intensity: subtle to spectacular", |
| ) |
| with gr.Accordion("Message context and custom genre", open=False): |
| use_case = gr.Dropdown( |
| choices=USE_CASES, |
| value=USE_CASES[0], |
| label="Use-case", |
| ) |
| custom_genre = gr.Textbox( |
| label="Custom genre (optional, overrides the menu)", |
| placeholder="like a sleep-deprived wizard", |
| lines=1, |
| ) |
| forge_button = gr.Button( |
| "Forge the message", |
| variant="primary", |
| size="lg", |
| elem_classes=["gg-primary"], |
| ) |
|
|
| with gr.Accordion("Try a one-click scene", open=False): |
| with gr.Row(): |
| example_buttons = [ |
| gr.Button(item[0], size="sm", elem_classes=["gg-example"]) |
| for item in EXAMPLES[:3] |
| ] |
| with gr.Row(): |
| example_buttons += [ |
| gr.Button(item[0], size="sm", elem_classes=["gg-example"]) |
| for item in EXAMPLES[3:] |
| ] |
|
|
| with gr.Accordion("Continue in the same voice", open=False): |
| followup = gr.Textbox( |
| label="Next plain message", |
| placeholder="The next plain message...", |
| lines=2, |
| ) |
| continue_button = gr.Button( |
| "Continue same voice", |
| elem_classes=["gg-secondary"], |
| ) |
|
|
| reset_button = gr.Button("Clear the desk", size="sm") |
|
|
| with gr.Column(scale=7, elem_classes=["gg-output-card"]): |
| gr.HTML('<div class="gg-section-title">2. Receive dramatic correspondence</div>') |
| result_status = gr.HTML( |
| '<div class="gg-result-head">The forge is waiting for a plain message.</div>' |
| ) |
| final_message = gr.Textbox( |
| label="Final stylized message", |
| lines=6, |
| interactive=False, |
| ) |
| with gr.Row(elem_classes=["gg-action-row"]): |
| copy_final = gr.Button("Copy final", size="sm") |
| speak_final = gr.Button("Instant preview", size="sm") |
| generate_voice_button = gr.Button( |
| "Generate genre voice", |
| size="sm", |
| variant="primary", |
| ) |
| voice_audio = gr.Audio( |
| label="VoxCPM2 genre voice", |
| type="filepath", |
| format="wav", |
| interactive=False, |
| elem_classes=["gg-compact-audio"], |
| ) |
| voice_status = gr.HTML( |
| '<div class="gg-voice-status muted">Generate a genre-matched studio voice when the text is ready.</div>' |
| ) |
| metrics = gr.HTML() |
| commentary = gr.HTML() |
|
|
| with gr.Accordion("Short version", open=False): |
| short_version = gr.Textbox( |
| label="Copy-ready short version", |
| lines=3, |
| interactive=False, |
| ) |
| copy_short = gr.Button("Copy short version", size="sm") |
|
|
| with gr.Accordion( |
| "Behind the rewrite: DNA, agent trace, sharing, history", |
| open=False, |
| elem_classes=["gg-details"], |
| ): |
| with gr.Tabs(): |
| with gr.Tab("Genre DNA"): |
| dna_panel = gr.HTML( |
| '<div class="gg-empty">Forge a message to reveal its DNA.</div>' |
| ) |
| with gr.Tab("Agent Trace"): |
| trace_panel = gr.HTML( |
| '<div class="gg-empty">The six-step trace appears after a rewrite.</div>' |
| ) |
| with gr.Tab("Share"): |
| share_card = gr.HTML( |
| '<div class="gg-empty">Your share card will appear here.</div>' |
| ) |
| social_text = gr.Textbox( |
| label="Copyable social card text", |
| lines=6, |
| interactive=False, |
| ) |
| copy_social = gr.Button("Copy social card text", size="sm") |
| with gr.Tab("History"): |
| history_panel = gr.HTML(render_history([])) |
|
|
| gr.HTML( |
| '<div class="gg-note">Creative writing assistant only. Sendability is a playful estimate, not a guarantee.</div>' |
| ) |
|
|
| generation_outputs = [ |
| final_message, |
| short_version, |
| metrics, |
| dna_panel, |
| trace_panel, |
| share_card, |
| social_text, |
| commentary, |
| result_status, |
| history_panel, |
| session_state, |
| followup, |
| voice_audio, |
| voice_status, |
| ] |
| forge_button.click( |
| fn=forge_message, |
| inputs=[plain_input, genre, custom_genre, intensity, use_case, session_state], |
| outputs=generation_outputs, |
| show_progress="minimal", |
| ) |
| plain_input.submit( |
| fn=forge_message, |
| inputs=[plain_input, genre, custom_genre, intensity, use_case, session_state], |
| outputs=generation_outputs, |
| show_progress="minimal", |
| ) |
| continue_button.click( |
| fn=continue_same_voice, |
| inputs=[followup, session_state, genre, custom_genre, intensity, use_case], |
| outputs=generation_outputs, |
| show_progress="minimal", |
| ) |
| followup.submit( |
| fn=continue_same_voice, |
| inputs=[followup, session_state, genre, custom_genre, intensity, use_case], |
| outputs=generation_outputs, |
| show_progress="minimal", |
| ) |
|
|
| for button, example in zip(example_buttons, EXAMPLES): |
| button.click( |
| fn=partial(load_example, example), |
| inputs=None, |
| outputs=[plain_input, genre, use_case, intensity, custom_genre], |
| ).then( |
| fn=forge_message, |
| inputs=[plain_input, genre, custom_genre, intensity, use_case, session_state], |
| outputs=generation_outputs, |
| show_progress="minimal", |
| ) |
|
|
| copy_final.click( |
| fn=None, |
| inputs=[final_message], |
| outputs=None, |
| js="(text) => { if (text) navigator.clipboard.writeText(text); }", |
| ) |
| copy_short.click( |
| fn=None, |
| inputs=[short_version], |
| outputs=None, |
| js="(text) => { if (text) navigator.clipboard.writeText(text); }", |
| ) |
| copy_social.click( |
| fn=None, |
| inputs=[social_text], |
| outputs=None, |
| js="(text) => { if (text) navigator.clipboard.writeText(text); }", |
| ) |
| speak_final.click( |
| fn=None, |
| inputs=[final_message, genre], |
| outputs=None, |
| js="""(text, genre) => { |
| if (!text || !window.speechSynthesis) return; |
| window.speechSynthesis.cancel(); |
| const utterance = new SpeechSynthesisUtterance(text); |
| const voices = { |
| "Shakespeare Villain": [0.82, 0.72], |
| "Corporate Apology": [0.95, 0.98], |
| "Indian Parent WhatsApp": [1.03, 1.06], |
| "Anime Power-Up Speech": [1.12, 1.18], |
| "Pirate Warning": [0.88, 0.76], |
| "Haunted Railway Announcement": [0.78, 0.68], |
| "Courtroom Confession": [0.86, 0.82], |
| "Medieval Royal Decree": [0.80, 0.78], |
| "Film Noir Detective": [0.76, 0.65], |
| "Startup Founder Pitch": [1.10, 1.05], |
| "Bureaucratic Government Notice": [0.90, 0.88], |
| "Overdramatic Fantasy Quest": [0.83, 0.85] |
| }; |
| const settings = voices[genre] || [0.94, 0.92]; |
| utterance.rate = settings[0]; |
| utterance.pitch = settings[1]; |
| window.speechSynthesis.speak(utterance); |
| }""", |
| ) |
| generate_voice_button.click( |
| fn=generate_genre_voice, |
| inputs=[final_message, genre, custom_genre, session_state], |
| outputs=[voice_audio, voice_status], |
| show_progress="full", |
| ) |
| reset_button.click( |
| fn=reset_session, |
| inputs=None, |
| outputs=[ |
| plain_input, |
| genre, |
| custom_genre, |
| intensity, |
| use_case, |
| followup, |
| final_message, |
| short_version, |
| metrics, |
| dna_panel, |
| trace_panel, |
| share_card, |
| social_text, |
| commentary, |
| result_status, |
| history_panel, |
| session_state, |
| voice_audio, |
| voice_status, |
| ], |
| ) |
|
|
|
|
| if __name__ == "__main__": |
| demo.queue(default_concurrency_limit=4).launch( |
| server_name=os.getenv("GRADIO_SERVER_NAME", "0.0.0.0"), |
| server_port=int(os.getenv("PORT", "7860")), |
| show_error=True, |
| theme=theme, |
| css=CSS, |
| ) |
|
|