GenreGoblin / app.py
KrishnaGarg's picture
Keep style memory genre-pure
9a897e0 verified
Raw
History Blame Contribute Delete
72.7 kB
"""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} &nbsp; / &nbsp; CHAOS {result.chaos_score}
&nbsp; | &nbsp; 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" &nbsp; 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,
)