Spaces:
Running
Running
File size: 11,947 Bytes
6ccb752 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 | """
๐ญ Story DNA Analyzer โ model.py
=================================
Uses HuggingFace zero-shot NLI to dissect any story text into:
โข Genre (8 categories)
โข Narrative Tropes (12 tropes)
โข Mood (6 tones)
โข Target Audience (4 groups)
โข Villain Archetype (6 types)
โข Story Outcome Prediction (3 endings)
Model: cross-encoder/nli-MiniLM2-L6-H768 (~90 MB, fast CPU inference)
"""
import sys
from transformers import pipeline
from dataclasses import dataclass, field
from typing import Dict, List
import json
if hasattr(sys.stdout, "reconfigure"):
sys.stdout.reconfigure(encoding="utf-8", errors="replace")
# โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
# LABEL DEFINITIONS
# โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
GENRES = [
"epic fantasy with magic and mythical creatures",
"science fiction with technology and space",
"psychological horror and dread",
"slow-burn romance and emotional longing",
"crime thriller and detective mystery",
"historical drama set in the past",
"dark comedy and satirical humor",
"coming-of-age and personal growth",
]
TROPES = [
"the chosen one who must save the world",
"enemies who fall in love",
"a betrayal by a trusted ally",
"a hero's journey of self-discovery",
"found family replacing blood family",
"a dark secret hidden for years",
"the mentor who dies to motivate the hero",
"redemption arc for a former villain",
"love triangle causing conflict",
"an unreliable narrator hiding the truth",
"a dystopian society being overthrown",
"time travel or parallel universe twist",
]
MOODS = [
"dark, gritty and hopeless",
"warm, cozy and optimistic",
"tense, suspenseful and unpredictable",
"whimsical, playful and magical",
"melancholic, bittersweet and nostalgic",
"intense, passionate and dramatic",
]
AUDIENCES = [
"young children under age 10",
"teenagers and young adults",
"mature adults dealing with complex themes",
"general audiences of all ages",
]
VILLAIN_ARCHETYPES = [
"a power-hungry tyrant who craves control",
"a misunderstood anti-hero with tragic backstory",
"a charismatic manipulator hiding in plain sight",
"an ancient evil force or monster",
"a corrupt institution or faceless system",
"no clear villain โ internal struggle is the enemy",
]
ENDINGS = [
"a triumphant happy ending where good wins",
"a tragic ending with sacrifice and loss",
"an ambiguous open ending left to interpretation",
]
# โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
# PRETTY LABEL MAPPING
# โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
GENRE_DISPLAY = {
"epic fantasy with magic and mythical creatures": "๐ง Epic Fantasy",
"science fiction with technology and space": "๐ Science Fiction",
"psychological horror and dread": "๐๏ธ Psychological Horror",
"slow-burn romance and emotional longing": "๐ Romance",
"crime thriller and detective mystery": "๐ Crime Thriller",
"historical drama set in the past": "๐๏ธ Historical Drama",
"dark comedy and satirical humor": "๐ญ Dark Comedy",
"coming-of-age and personal growth": "๐ฑ Coming-of-Age",
}
TROPE_DISPLAY = {
"the chosen one who must save the world": "โก The Chosen One",
"enemies who fall in love": "๐ Enemies to Lovers",
"a betrayal by a trusted ally": "๐ก๏ธ The Big Betrayal",
"a hero's journey of self-discovery": "๐บ๏ธ Hero's Journey",
"found family replacing blood family": "๐ค Found Family",
"a dark secret hidden for years": "๐ Hidden Dark Secret",
"the mentor who dies to motivate the hero": "๐ Dead Mentor",
"redemption arc for a former villain": "๐
Redemption Arc",
"love triangle causing conflict": "๐ Love Triangle",
"an unreliable narrator hiding the truth": "๐ญ Unreliable Narrator",
"a dystopian society being overthrown": "โ Revolution Arc",
"time travel or parallel universe twist": "๐ Time/Dimension Twist",
}
MOOD_DISPLAY = {
"dark, gritty and hopeless": "๐ค Dark & Gritty",
"warm, cozy and optimistic": "โ๏ธ Warm & Hopeful",
"tense, suspenseful and unpredictable": "โก Tense & Suspenseful",
"whimsical, playful and magical": "โจ Whimsical & Magical",
"melancholic, bittersweet and nostalgic": "๐ง๏ธ Melancholic",
"intense, passionate and dramatic": "๐ฅ Passionate & Dramatic",
}
AUDIENCE_DISPLAY = {
"young children under age 10": "๐ง Children (Under 10)",
"teenagers and young adults": "๐ง Young Adult (13โ25)",
"mature adults dealing with complex themes": "๐ง Adult (18+)",
"general audiences of all ages": "๐จโ๐ฉโ๐งโ๐ฆ All Ages",
}
VILLAIN_DISPLAY = {
"a power-hungry tyrant who craves control": "๐ The Tyrant",
"a misunderstood anti-hero with tragic backstory": "๐ฅ The Tragic Anti-Hero",
"a charismatic manipulator hiding in plain sight": "๐ญ The Hidden Manipulator",
"an ancient evil force or monster": "๐ Ancient Evil",
"a corrupt institution or faceless system": "๐๏ธ The System",
"no clear villain โ internal struggle is the enemy": "๐ช Internal Conflict",
}
ENDING_DISPLAY = {
"a triumphant happy ending where good wins": "๐ Happy Ending",
"a tragic ending with sacrifice and loss": "๐ง Tragic Ending",
"an ambiguous open ending left to interpretation": "โ Ambiguous Ending",
}
# โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
# RESULT DATACLASS
# โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
@dataclass
class StoryDNA:
genre: Dict[str, float] = field(default_factory=dict)
top_tropes: List[Dict] = field(default_factory=list)
mood: Dict[str, float] = field(default_factory=dict)
audience: Dict[str, float] = field(default_factory=dict)
villain: Dict[str, float] = field(default_factory=dict)
predicted_ending:Dict[str, float] = field(default_factory=dict)
def top(self, d: dict, n=1):
return sorted(d.items(), key=lambda x: -x[1])[:n]
def summary(self) -> str:
lines = ["=" * 52, "๐ญ STORY DNA ANALYSIS", "=" * 52]
lines.append(f"\n๐ GENRE")
for label, score in self.top(self.genre, 3):
bar = "โ" * int(score * 20)
lines.append(f" {label:<30} {bar} {score:.0%}")
lines.append(f"\n๐งฌ TOP NARRATIVE TROPES")
for t in self.top_tropes[:4]:
lines.append(f" {t['label']:<30} {t['score']:.0%}")
lines.append(f"\n๐จ MOOD")
for label, score in self.top(self.mood, 2):
lines.append(f" {label:<30} {score:.0%}")
lines.append(f"\n๐ฅ TARGET AUDIENCE")
for label, score in self.top(self.audience, 2):
lines.append(f" {label:<30} {score:.0%}")
lines.append(f"\n๐ VILLAIN ARCHETYPE")
for label, score in self.top(self.villain, 2):
lines.append(f" {label:<30} {score:.0%}")
lines.append(f"\n๐ฎ PREDICTED ENDING")
for label, score in self.top(self.predicted_ending, 2):
lines.append(f" {label:<30} {score:.0%}")
lines.append("=" * 52)
return "\n".join(lines)
def to_dict(self) -> dict:
return {
"genre": self.genre,
"top_tropes": self.top_tropes,
"mood": self.mood,
"audience": self.audience,
"villain": self.villain,
"predicted_ending": self.predicted_ending,
}
# โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
# ANALYZER CLASS
# โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
class StoryDNAAnalyzer:
"""
Zero-shot story analyzer using HuggingFace NLI.
No training needed โ works out of the box.
"""
MODEL_ID = "cross-encoder/nli-MiniLM2-L6-H768"
def __init__(self, device: int = -1):
print(f"โณ Loading model: {self.MODEL_ID} ...")
self.clf = pipeline(
"zero-shot-classification",
model=self.MODEL_ID,
device=device, # -1 = CPU, 0 = first GPU
)
print("โ
Model ready!\n")
def _classify(self, text: str, labels: list, display_map: dict) -> dict:
out = self.clf(text, candidate_labels=labels, multi_label=False)
return {
display_map[label]: round(score, 4)
for label, score in zip(out["labels"], out["scores"])
}
def _classify_multi(self, text: str, labels: list, display_map: dict) -> list:
out = self.clf(text, candidate_labels=labels, multi_label=True)
results = [
{"label": display_map[label], "score": round(score, 4)}
for label, score in zip(out["labels"], out["scores"])
]
return sorted(results, key=lambda x: -x["score"])
def analyze(self, text: str) -> StoryDNA:
if len(text.strip()) < 20:
raise ValueError("Please provide at least 20 characters of story text.")
print("๐ฌ Analyzing Story DNA...")
dna = StoryDNA(
genre = self._classify(text, GENRES, GENRE_DISPLAY),
top_tropes = self._classify_multi(text, TROPES, TROPE_DISPLAY),
mood = self._classify(text, MOODS, MOOD_DISPLAY),
audience = self._classify(text, AUDIENCES, AUDIENCE_DISPLAY),
villain = self._classify(text, VILLAIN_ARCHETYPES,VILLAIN_DISPLAY),
predicted_ending = self._classify(text, ENDINGS, ENDING_DISPLAY),
)
return dna
# โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
# QUICK TEST
# โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
if __name__ == "__main__":
analyzer = StoryDNAAnalyzer()
sample = """
In a crumbling empire ruled by an immortal king who feeds on fear,
a young blind girl discovers she can see the true faces of the dead.
Hunted by the king's shadow-wolves, she escapes into the forbidden forest
where a disgraced general with blood on his hands offers her an uneasy alliance.
Together they must unlock the secret buried beneath the throne โ
a truth that will either free the kingdom or doom it forever.
"""
result = analyzer.analyze(sample)
print(result.summary())
print("\n๐ฆ JSON output:")
print(json.dumps(result.to_dict(), indent=2))
|