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