""" ๐ŸŽญ 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))