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