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))