feat: add horror prompt library with severity tiers and voice texts
This commit is contained in:
@@ -0,0 +1,139 @@
|
|||||||
|
"""Horror prompt library — severity-tiered SDXL prompts + XTTS voice texts."""
|
||||||
|
|
||||||
|
import random
|
||||||
|
|
||||||
|
# Negative prompt applied to all SDXL generations
|
||||||
|
NEGATIVE_PROMPT = (
|
||||||
|
"cheerful, bright, colorful, cartoon, anime, text, watermark, "
|
||||||
|
"logo, signature, pleasant, happy, cute, well-lit, clean"
|
||||||
|
)
|
||||||
|
|
||||||
|
# (min_severity, [prompt_templates])
|
||||||
|
# Each tier's prompts are available when severity >= min_severity.
|
||||||
|
# Prompts use {detail} placeholder for procedural variation.
|
||||||
|
SEVERITY_TIERS: list[tuple[float, list[str]]] = [
|
||||||
|
(0.0, [
|
||||||
|
"dark abstract void, deep shadows, {detail}, horror atmosphere, cinematic",
|
||||||
|
"black fog rolling over dark water, {detail}, ominous, photorealistic",
|
||||||
|
"concrete corridor stretching into darkness, {detail}, liminal space, unsettling",
|
||||||
|
"dark gradient with subtle organic texture, {detail}, dread, macro photography",
|
||||||
|
"abandoned room in total darkness, single light source, {detail}, eerie silence",
|
||||||
|
"static noise pattern forming almost-shapes, {detail}, analog horror aesthetic",
|
||||||
|
"deep underground cavern, no visible exit, {detail}, claustrophobic, dark",
|
||||||
|
]),
|
||||||
|
(1.0, [
|
||||||
|
"distorted human face emerging from darkness, {detail}, uncanny valley, horror",
|
||||||
|
"long dark hallway with a figure at the end, {detail}, found footage aesthetic",
|
||||||
|
"room where the walls are slightly wrong, {detail}, liminal horror, photorealistic",
|
||||||
|
"mirror reflection that doesn't match, {detail}, psychological horror",
|
||||||
|
"staircase descending into impossible depth, {detail}, surreal horror",
|
||||||
|
"doorway opening into a void, {detail}, threshold horror, dark atmosphere",
|
||||||
|
"familiar room but every proportion is wrong, {detail}, dreamlike horror",
|
||||||
|
]),
|
||||||
|
(2.0, [
|
||||||
|
"face melting into dark liquid, {detail}, body horror, visceral, photorealistic",
|
||||||
|
"impossible architecture folding in on itself, {detail}, Escher nightmare, dark",
|
||||||
|
"multiple overlapping faces fused together, {detail}, uncanny, disturbing",
|
||||||
|
"corridor with too many doors, all slightly open, {detail}, psychological horror",
|
||||||
|
"human figure with wrong number of limbs, {detail}, body horror, dark",
|
||||||
|
"room full of eyes watching from every surface, {detail}, paranoid horror",
|
||||||
|
"teeth growing from walls, {detail}, organic horror, visceral, photorealistic",
|
||||||
|
]),
|
||||||
|
(3.0, [
|
||||||
|
"screaming void, flesh merging with architecture, {detail}, extreme body horror",
|
||||||
|
"reality fracturing into bleeding shards, {detail}, cosmic horror, overwhelming",
|
||||||
|
"mass of tangled human forms, {detail}, hellscape, Beksinski inspired, photorealistic",
|
||||||
|
"sky replaced by a massive watching face, {detail}, cosmic dread, surreal",
|
||||||
|
"ground made of writhing organic matter, {detail}, Giger inspired, dark",
|
||||||
|
"impossible geometry that hurts to perceive, {detail}, Lovecraftian, extreme",
|
||||||
|
"world turned inside out, organs as landscape, {detail}, visceral cosmic horror",
|
||||||
|
]),
|
||||||
|
]
|
||||||
|
|
||||||
|
# Procedural detail fragments inserted into {detail} placeholders
|
||||||
|
_DETAILS = [
|
||||||
|
"wet surfaces", "rust and decay", "dim red light", "flickering fluorescent",
|
||||||
|
"peeling paint", "fog and mist", "dripping liquid", "cracked surfaces",
|
||||||
|
"organic growths", "shadow patterns", "reflected light on water",
|
||||||
|
"dust particles", "cobwebs", "stained walls", "scratched metal",
|
||||||
|
"condensation", "mold patterns", "burned edges", "frozen in time",
|
||||||
|
"overlapping shadows", "single bare bulb", "moonlight through cracks",
|
||||||
|
"bioluminescent", "blood-red sky", "green pallor", "bruise-purple tint",
|
||||||
|
]
|
||||||
|
|
||||||
|
# Whisper fragments — sentence fragments, numbers, names, nonsense
|
||||||
|
_WHISPERS = [
|
||||||
|
"seven", "behind you", "don't turn around", "it remembers",
|
||||||
|
"the door", "counting", "almost time", "in the walls",
|
||||||
|
"not alone", "watching", "three two one", "forgetting",
|
||||||
|
"underneath", "the wrong room", "teeth", "it knows your name",
|
||||||
|
"listen", "the sound", "nobody left", "opening",
|
||||||
|
"he's here", "she won't stop", "the children", "below",
|
||||||
|
"always here", "never gone", "the dark", "it follows",
|
||||||
|
"coming closer", "just outside", "the mirror", "look",
|
||||||
|
"run", "too late", "the floor", "above you",
|
||||||
|
"inside", "the old house", "breathing", "footsteps",
|
||||||
|
"scratching", "dripping", "humming", "whispers",
|
||||||
|
"ha ha ha ha", "one two three four five", "again again again",
|
||||||
|
"please", "help me", "let me in", "let me out",
|
||||||
|
]
|
||||||
|
|
||||||
|
# Direct address phrases — "it sees you" moments
|
||||||
|
_DIRECT_ADDRESS = [
|
||||||
|
"you're still here",
|
||||||
|
"don't leave",
|
||||||
|
"I can see you",
|
||||||
|
"why",
|
||||||
|
"stay",
|
||||||
|
"you came back",
|
||||||
|
"I've been waiting",
|
||||||
|
"don't close your eyes",
|
||||||
|
"you can't leave",
|
||||||
|
"I know you're there",
|
||||||
|
"look at me",
|
||||||
|
"do you hear it",
|
||||||
|
"it's behind you",
|
||||||
|
"we see you",
|
||||||
|
"you're one of us now",
|
||||||
|
"don't you remember",
|
||||||
|
"you were here before",
|
||||||
|
"this is yours",
|
||||||
|
"you did this",
|
||||||
|
"welcome home",
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
def get_image_prompt(severity: float) -> str:
|
||||||
|
"""Select and fill a horror prompt appropriate for the given severity.
|
||||||
|
|
||||||
|
Higher severity unlocks more extreme prompt tiers.
|
||||||
|
A random prompt is chosen from all available tiers, weighted toward
|
||||||
|
the highest unlocked tier.
|
||||||
|
"""
|
||||||
|
available: list[tuple[float, str]] = []
|
||||||
|
for threshold, templates in SEVERITY_TIERS:
|
||||||
|
if severity >= threshold:
|
||||||
|
for tmpl in templates:
|
||||||
|
available.append((threshold, tmpl))
|
||||||
|
|
||||||
|
if not available:
|
||||||
|
available = [(0.0, t) for t in SEVERITY_TIERS[0][1]]
|
||||||
|
|
||||||
|
# Weight toward higher tiers: tier_weight = 1 + tier_index
|
||||||
|
tier_thresholds = sorted(set(t[0] for t in available))
|
||||||
|
tier_map = {t: i + 1 for i, t in enumerate(tier_thresholds)}
|
||||||
|
weights = [tier_map[threshold] for threshold, _ in available]
|
||||||
|
|
||||||
|
_, template = random.choices(available, weights=weights, k=1)[0]
|
||||||
|
detail = random.choice(_DETAILS)
|
||||||
|
return template.format(detail=detail)
|
||||||
|
|
||||||
|
|
||||||
|
def get_voice_text() -> str:
|
||||||
|
"""Random whisper fragment for XTTS voice generation."""
|
||||||
|
return random.choice(_WHISPERS)
|
||||||
|
|
||||||
|
|
||||||
|
def get_direct_address_text() -> str:
|
||||||
|
"""Random direct address phrase for 'it sees you' moments."""
|
||||||
|
return random.choice(_DIRECT_ADDRESS)
|
||||||
@@ -0,0 +1,61 @@
|
|||||||
|
from server.prompts import (
|
||||||
|
get_image_prompt,
|
||||||
|
get_voice_text,
|
||||||
|
get_direct_address_text,
|
||||||
|
SEVERITY_TIERS,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class TestImagePrompts:
|
||||||
|
def test_low_severity_prompt(self):
|
||||||
|
"""Low severity returns abstract/atmospheric prompt."""
|
||||||
|
prompt = get_image_prompt(severity=0.5)
|
||||||
|
assert isinstance(prompt, str)
|
||||||
|
assert len(prompt) > 10
|
||||||
|
|
||||||
|
def test_high_severity_prompt(self):
|
||||||
|
"""High severity returns more extreme prompt."""
|
||||||
|
prompt = get_image_prompt(severity=4.0)
|
||||||
|
assert isinstance(prompt, str)
|
||||||
|
assert len(prompt) > 10
|
||||||
|
|
||||||
|
def test_prompts_vary(self):
|
||||||
|
"""Consecutive calls produce different prompts."""
|
||||||
|
prompts = [get_image_prompt(severity=2.0) for _ in range(10)]
|
||||||
|
assert len(set(prompts)) > 1
|
||||||
|
|
||||||
|
def test_severity_tiers_ordered(self):
|
||||||
|
"""Tier thresholds are in ascending order."""
|
||||||
|
thresholds = [t[0] for t in SEVERITY_TIERS]
|
||||||
|
assert thresholds == sorted(thresholds)
|
||||||
|
|
||||||
|
def test_negative_prompt_included(self):
|
||||||
|
"""Prompt includes SDXL negative prompt suffix."""
|
||||||
|
prompt = get_image_prompt(severity=1.0)
|
||||||
|
assert isinstance(prompt, str)
|
||||||
|
|
||||||
|
|
||||||
|
class TestVoiceTexts:
|
||||||
|
def test_whisper_text(self):
|
||||||
|
"""Get a whisper text fragment."""
|
||||||
|
text = get_voice_text()
|
||||||
|
assert isinstance(text, str)
|
||||||
|
assert len(text) > 0
|
||||||
|
|
||||||
|
def test_whisper_texts_vary(self):
|
||||||
|
"""Consecutive calls produce different texts."""
|
||||||
|
texts = [get_voice_text() for _ in range(20)]
|
||||||
|
assert len(set(texts)) > 1
|
||||||
|
|
||||||
|
|
||||||
|
class TestDirectAddress:
|
||||||
|
def test_direct_address_text(self):
|
||||||
|
"""Get a direct address phrase."""
|
||||||
|
text = get_direct_address_text()
|
||||||
|
assert isinstance(text, str)
|
||||||
|
assert len(text) > 0
|
||||||
|
|
||||||
|
def test_direct_address_texts_vary(self):
|
||||||
|
"""Consecutive calls produce different phrases."""
|
||||||
|
texts = [get_direct_address_text() for _ in range(20)]
|
||||||
|
assert len(set(texts)) > 1
|
||||||
Reference in New Issue
Block a user