Spaces:
Running
Running
| """ | |
| ๐ญ 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 | |
| # โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ | |
| 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)) | |