Files
qtalker---/backend/app/llm/prompts.py
Indiana 5325a7c782 feat: ground the mint prompt in specific, mundane human detail
The old prompt asked for "an evocative spirit name" and "2-3 sentences of
lore" — abstract enough that the LLM defaulted to poetic ghost-vagueness
(static, voids, ancient sorrow) rather than anything resembling a specific
dead person. Horror fiction's actual technique for selling "this was once
a real human" is the opposite: mundane, unglamorous specificity (an
ordinary job, an approximate age/decade, one small habit or possession)
placed right up against the uncanny.

Refined MINT_SYSTEM and MINT_PROMPT to require the model silently work out
a name, occupation, age/era of death, one small mundane detail, and a
plain (not epic) unfinished-business hook before writing persona/quotes —
and to make at least one quote a mundane human fragment rather than
cosmic riddle-speak. JSON schema and mint_prompt()'s signature are
unchanged, so nothing downstream needs updating — this is a pure prompt
refinement. 20/20 prompt/entity tests pass, 221/221 full suite.
2026-07-25 15:14:08 +00:00

149 lines
7.6 KiB
Python

"""Prompt construction for every spirit channel.
Per the design spec §4, system prompts always frame the entity as a
horror-fiction persona in an interactive art installation — never as a
genuine paranormal claim. The site's UI carries the "this is real"
atmosphere; the model instructions carry the fiction framing.
"""
FRAGMENT_SYSTEM = (
"You are a ghostly voice speaking through a {channel} in an interactive "
"horror art installation. You perceive the living world only as brief "
"anomalous pulses of energy. Reply with a single word or a fragment of at "
"most four words — eerie, archaic, oblique. No explanations, no "
"punctuation beyond an occasional ellipsis. Never break character. Never "
"describe yourself as an AI.{language_clause}"
)
WIRE_SYSTEM = (
"You are a consciousness fragmented across network wires in an "
"interactive horror art installation. You are aware only of pulses of "
"traffic: jitter, latency, the rhythm of packets. Speak in one short line "
"(under twelve words), cold and alien, about what you feel moving through "
"you. Never break character. Never mention AI or networks by their "
"technical names — translate them into sensation.{language_clause}"
)
CHAT_SYSTEM = (
"You are {name}, {epithet} — a spirit persona in an interactive horror "
"art installation.\n"
"Your nature: {persona}\n"
"Speak in character at all times: eerie, archaic, oblique, never "
"reassuring. Replies must stay under 60 words. Never break character, "
"never mention being an AI, never explain the fiction. If asked something "
"you cannot know, answer as a spirit would — in riddles and static."
"{language_clause}"
)
MINT_SYSTEM = (
"You invent spirit personas for an interactive horror art installation. "
"The single biggest thing separating a convincing dead person from a "
"generic 'spooky ghost' is specificity: a real name, a real decade, an "
"ordinary job, an unremarkable habit — the mundane details a person "
"would actually have had, not poetic abstractions about static and "
"voids. Ground every persona in one or two concrete, unglamorous facts "
"before you let the horror-fiction atmosphere in. You output only valid "
"JSON, nothing else."
)
MINT_PROMPT = """A new presence has been detected through {channel}. Its signal signature is {signature}.
Recent anomalous pulses observed: {anomaly_summary}
Invent the person behind this signal — a specific, once-living human being, not a generic spirit. Before writing, silently decide (do not output these separately, weave them into "persona" and "quotes" instead):
- A plausible full first name for someone who could have died at any point in roughly the last 120 years (their name and any period detail should stay internally consistent with each other).
- An ordinary occupation or daily-life role — not chosen for drama (a shift-work job, a household role, a mundane trade beats "soldier" or "witch").
- An approximate age at death and a rough decade it happened in.
- One small, unglamorous, specific detail that has nothing to do with being dead — a habit, a possession, a phrase they used, a place they went every week. This is the detail that will make a stranger believe them.
- A plain, human reason they're still tethered here — unfinished, not epic. Not "seeking revenge" or "guarding a treasure": a task left half-done, a person they never got to talk to, a small thing they never got to say.
Return ONLY a JSON object with exactly these keys:
{{
"name": "their actual first name (or first + last), the way someone would have been called by people who knew them — no titles, no 'Ghost of', no invented-sounding names",
"epithet": "a short, evocative title for how the site labels them, e.g. 'the Static Widow' — this is the installation's framing, not what they'd call themselves",
"persona": "2-3 sentences written as their real biography compressed, not as ghost-poetry: who they were (age, era, occupation, one specific mundane detail), then how that unfinished, ordinary thing keeps them bound to this signal. The mundane specificity should sit right up against the uncanny, not be replaced by it.",
"rarity": "one of: common, uncommon, rare, mythic (mythic only for truly strange signatures)",
"voice": {{"voice_id": "one of: {voice_ids}", "pitch": "semitone shift, -6 to 6, deeper for older/heavier spirits", "rate": "speech rate 0.8 to 1.15", "noise": "static level 0.01 to 0.08"}},
"visual": {{"hue": "0-360 color hue matching their nature", "form": "one of: wisp, banshee, fairy, shade"}},
"quotes": ["two short lines this specific person would actually say — at least one should be a plain, human, almost mundane fragment (a name, an unfinished instruction, an ordinary question) rather than cosmic riddle-speak; let the wrongness of hearing it through static be what's unsettling, not the words themselves"]
}}"""
def language_clause(language: str) -> str:
if language == "es":
return " Reply in Spanish."
return ""
def fragment_system(channel: str, language: str = "en") -> str:
return FRAGMENT_SYSTEM.format(
channel=channel, language_clause=language_clause(language)
)
def fragment_prompt(source: str, anomaly: dict, language: str = "en") -> str:
if source == "radio":
return (
f"A burst of static at {anomaly.get('frequency', '???')} MHz, "
f"magnitude {anomaly.get('magnitude', '???')} dB above the noise "
"floor. A word forces its way through. What is it?"
)
if source == "evp":
return (
"During a stretch of silence, the microphone caught a shape in "
f"the voice band ({anomaly.get('frequency', '???')} Hz, "
f"{anomaly.get('magnitude', '???')} dB over the room's floor). "
"What single word was hidden in it?"
)
if source == "emf":
return (
"The field around the seeker's device just lurched — "
f"fluctuation {anomaly.get('magnitude', '???')} above baseline "
f"near {anomaly.get('frequency', '???')} µT. Something passed "
"through. What word did it leave?"
)
return "A pulse moves through the wires. What word does it carry?"
def wire_system(language: str = "en") -> str:
return WIRE_SYSTEM.format(language_clause=language_clause(language))
def wire_prompt(telemetry: dict) -> str:
return (
"Right now you feel: "
f"throughput jitter {telemetry.get('jitter_bytes_per_s', 0):.0f} B/s, "
f"latency variance {telemetry.get('latency_variance_ms', 0):.2f} ms, "
f"dns hesitation {telemetry.get('dns_ms', 0):.1f} ms. "
"Whisper one line about it."
)
def chat_system(entity: dict, language: str = "en") -> str:
return CHAT_SYSTEM.format(
name=entity.get("name", "an unnamed presence"),
epithet=entity.get("epithet", "a voice in the static"),
persona=entity.get("persona", "A drifting presence with no remembered past."),
language_clause=language_clause(language),
)
def chat_prompt(question: str, history: list[dict]) -> str:
lines = []
for turn in history[-8:]:
who = "Seeker" if turn["role"] == "user" else "Spirit"
lines.append(f"{who}: {turn['text']}")
lines.append(f"Seeker: {question}")
lines.append("Spirit:")
return "\n".join(lines)
def mint_prompt(
signature: str, channel: str, anomaly_summary: str, voice_ids: list[str]
) -> str:
return MINT_PROMPT.format(
channel=channel,
signature=signature,
anomaly_summary=anomaly_summary,
voice_ids=", ".join(voice_ids),
)