File size: 6,061 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
"""
๐ŸŽญ Story DNA Analyzer โ€” app.py
================================
Gradio web UI โ€” deploy directly to HuggingFace Spaces.
Run locally:  python app.py
"""

import gradio as gr
import json
from model import StoryDNAAnalyzer, StoryDNA

# Load model once at startup
analyzer = StoryDNAAnalyzer()

GRADIO_MAJOR = int(gr.__version__.split(".", 1)[0])
APP_THEME = gr.themes.Soft(primary_hue="purple", neutral_hue="slate")
APP_CSS = """
    .title { text-align: center; }
    .subtitle { text-align: center; color: #888; }
    footer { display: none !important; }
"""

EXAMPLES = [
    ["A young orphan discovers he is a wizard and is accepted into a magical school, "
     "where he makes lifelong friends, faces a dark wizard who killed his parents, "
     "and learns that love is the most powerful magic of all."],

    ["Detective Sarah Lund receives a call at 2 AM. A senator's daughter is missing. "
     "As she digs deeper, the trail leads to the highest levels of government. "
     "Everyone is lying. The killer is someone she trusts."],

    ["In 2157, humanity's last colony ship drifts through space after Earth's destruction. "
     "When the AI pilot starts making strange decisions, engineer Mara must choose between "
     "following orders and saving 4,000 sleeping passengers from a fate worse than death."],

    ["Two rival bakers in a small Italian village have hated each other for forty years. "
     "When their grandchildren fall in love, old secrets surface โ€” and so do feelings "
     "they buried long ago. The village holds its breath."],

    ["The kingdom was built on a lie. When seventeen-year-old Asha uncovers the truth "
     "beneath the palace โ€” that the king sacrifices a child every decade to maintain power โ€” "
     "she must decide how far she will go to burn it all down."],
]


def format_results(dna: StoryDNA) -> tuple:
    """Convert StoryDNA into nicely formatted Gradio outputs."""

    def bar(score: float) -> str:
        filled = int(score * 10)
        return "โ–ˆ" * filled + "โ–‘" * (10 - filled)

    # โ”€โ”€ Genre โ”€โ”€
    genre_md = "### ๐Ÿ“š Genre\n"
    for label, score in sorted(dna.genre.items(), key=lambda x: -x[1])[:4]:
        genre_md += f"`{bar(score)}` **{score:.0%}** โ€” {label}\n\n"

    # โ”€โ”€ Tropes โ”€โ”€
    tropes_md = "### ๐Ÿงฌ Top Narrative Tropes\n"
    for t in dna.top_tropes[:5]:
        tropes_md += f"`{bar(t['score'])}` **{t['score']:.0%}** โ€” {t['label']}\n\n"

    # โ”€โ”€ Mood โ”€โ”€
    mood_md = "### ๐ŸŽจ Story Mood\n"
    for label, score in sorted(dna.mood.items(), key=lambda x: -x[1])[:3]:
        mood_md += f"`{bar(score)}` **{score:.0%}** โ€” {label}\n\n"

    # โ”€โ”€ Audience โ”€โ”€
    audience_md = "### ๐Ÿ‘ฅ Target Audience\n"
    for label, score in sorted(dna.audience.items(), key=lambda x: -x[1])[:3]:
        audience_md += f"`{bar(score)}` **{score:.0%}** โ€” {label}\n\n"

    # โ”€โ”€ Villain โ”€โ”€
    villain_md = "### ๐Ÿ˜ˆ Villain Archetype\n"
    for label, score in sorted(dna.villain.items(), key=lambda x: -x[1])[:3]:
        villain_md += f"`{bar(score)}` **{score:.0%}** โ€” {label}\n\n"

    # โ”€โ”€ Ending prediction โ”€โ”€
    ending_md = "### ๐Ÿ”ฎ Predicted Ending\n"
    for label, score in sorted(dna.predicted_ending.items(), key=lambda x: -x[1]):
        ending_md += f"`{bar(score)}` **{score:.0%}** โ€” {label}\n\n"

    return genre_md, tropes_md, mood_md, audience_md, villain_md, ending_md


def analyze(text: str):
    if not text or len(text.strip()) < 20:
        empty = "*Please enter a story description (at least 20 characters).*"
        return empty, empty, empty, empty, empty, empty

    try:
        dna = analyzer.analyze(text)
        return format_results(dna)
    except Exception as e:
        err = f"โŒ Error: {str(e)}"
        return err, err, err, err, err, err


# โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
# GRADIO UI
# โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€

blocks_kwargs = {"title": "๐ŸŽญ Story DNA Analyzer"}
if GRADIO_MAJOR < 6:
    blocks_kwargs.update(theme=APP_THEME, css=APP_CSS)

with gr.Blocks(**blocks_kwargs) as demo:

    gr.Markdown(
        "# ๐ŸŽญ Story DNA Analyzer\n"
        "### Paste any story snippet, book blurb, or movie description โ€” get its full narrative DNA.\n"
        "*Powered by HuggingFace zero-shot NLI ยท No training required*",
        elem_classes=["title"]
    )

    with gr.Row():
        with gr.Column(scale=2):
            text_input = gr.Textbox(
                label="๐Ÿ“– Story Text",
                placeholder="Paste any story, book blurb, movie synopsis, or description here...",
                lines=7,
                max_lines=20,
            )
            analyze_btn = gr.Button("๐Ÿ”ฌ Analyze Story DNA", variant="primary", size="lg")
            gr.Examples(examples=EXAMPLES, inputs=text_input, label="๐Ÿ“ Try these examples")

        with gr.Column(scale=3):
            with gr.Row():
                genre_out   = gr.Markdown(label="Genre")
                tropes_out  = gr.Markdown(label="Tropes")
            with gr.Row():
                mood_out    = gr.Markdown(label="Mood")
                audience_out= gr.Markdown(label="Audience")
            with gr.Row():
                villain_out = gr.Markdown(label="Villain")
                ending_out  = gr.Markdown(label="Ending")

    analyze_btn.click(
        fn=analyze,
        inputs=text_input,
        outputs=[genre_out, tropes_out, mood_out, audience_out, villain_out, ending_out],
    )

    gr.Markdown(
        "---\n"
        "**Model:** `cross-encoder/nli-MiniLM2-L6-H768` via zero-shot classification  \n"
        "**No fine-tuning needed** โ€” works on any story in any language."
    )

if __name__ == "__main__":
    launch_kwargs = {"share": False}
    if GRADIO_MAJOR >= 6:
        launch_kwargs.update(theme=APP_THEME, css=APP_CSS)
    demo.launch(**launch_kwargs)