import gradio as gr from transformers import pipeline import re import json # ── Same model as Session 1's Silly Phrase Finder ── classifier = pipeline( "zero-shot-classification", model="valhalla/distilbart-mnli-12-3", ) # ── Four analytical lenses ── LENSES = { "Tone": [ "dramatic and intense", "humorous and playful", "melancholic and sad", "suspenseful and tense", "warm and affectionate", "dry and matter-of-fact", ], "Formality": [ "academic and scholarly", "casual and conversational", "poetic and lyrical", "journalistic and reportorial", ], "Energy": [ "fast-paced and urgent", "slow and contemplative", "building tension", "calm and steady", ], "Genre Feel": [ "literary fiction", "thriller or mystery", "romance", "comedy", "memoir or personal essay", "news report", ], } # Short display names (strip the "and ..." qualifiers) def short_label(label): return label.split(" and ")[0].split(" or ")[0].strip() # ── Sentence splitter ── def split_sentences(text): sentences = [ s.strip() for s in re.split(r'(?<=[.!?])\s+', text) if len(s.strip()) > 15 ] return sentences[:8] # cap for free-CPU performance # ── Main analysis function ── def analyze_passage(text): if not text or not text.strip(): return placeholder_html("Paste a passage above to begin analysis.") sentences = split_sentences(text) if len(sentences) < 2: return placeholder_html( "Please paste a longer passage — at least a few sentences." ) # 1) Passage-level analysis through every lens passage_scores = {} for lens_name, labels in LENSES.items(): result = classifier(text[:512], candidate_labels=labels) passage_scores[lens_name] = { label: score for label, score in zip(result["labels"], result["scores"]) } # 2) Sentence-level analysis through the Tone lens tone_labels = LENSES["Tone"] sentence_data = [] for sentence in sentences: result = classifier(sentence, candidate_labels=tone_labels) sentence_data.append( { "text": sentence, "tone": result["labels"][0], "score": result["scores"][0], } ) return build_dashboard_html(passage_scores, sentence_data) # ── HTML builder ── TONE_COLORS = { "dramatic and intense": "#e74c3c", "humorous and playful": "#f39c12", "melancholic and sad": "#3498db", "suspenseful and tense": "#9b59b6", "warm and affectionate": "#e91e63", "dry and matter-of-fact": "#78909c", } def placeholder_html(msg): return ( f'

{msg}

' ) def build_dashboard_html(passage_scores, sentence_data): # ── Lens summary cards ── lens_icons = {"Tone": "🎭", "Formality": "📐", "Energy": "⚡", "Genre Feel": "📚"} cards = "" for lens_name, scores in passage_scores.items(): top_label = max(scores, key=scores.get) top_score = scores[top_label] icon = lens_icons.get(lens_name, "") cards += f"""
{icon}
{lens_name}
{short_label(top_label)}
{top_score:.0%} confidence
""" # ── Sentence rows ── sentence_rows = "" for i, sd in enumerate(sentence_data): color = TONE_COLORS.get(sd["tone"], "#78909c") pct = sd["score"] * 100 sentence_rows += f"""
{i + 1}
{sd['text']}
{short_label(sd['tone'])}
{sd['score']:.0%}
""" # ── Radar chart data ── # Prepare all four lenses for a tabbed radar radar_json = json.dumps( { lens: { "labels": [short_label(l) for l in scores.keys()], "values": [round(v * 100, 1) for v in scores.values()], } for lens, scores in passage_scores.items() } ) html = f"""

Passage Analysis Dashboard

Four analytical lenses — one zero-shot model — no task-specific training

{cards}

Passage Profile

Sentence-by-Sentence Tone

{sentence_rows}
Powered by the same model as the Silly Phrase Finder: valhalla/distilbart-mnli-12-3
Nobody trained it on tone, formality, energy, or genre. It figures it out from language alone.
""" return html # ── Example passages ── EXAMPLES = [ [ "The old house stood at the end of the lane, its windows dark as closed eyes. " "Nobody had lived there since the winter of 1987, when Mrs. Bellweather vanished " "during the first snowfall. Children crossed the street to avoid it. Dogs pulled " "at their leashes. Even the mailman, who feared nothing, left packages at the gate " "and walked briskly away. But tonight, for the first time in decades, a light " "flickered behind the upstairs curtain." ], [ "The committee has reviewed the quarterly earnings and finds them satisfactory. " "Revenue increased by twelve percent over the previous quarter. However, operating " "costs in the Northeast division remain above target. We recommend a full audit of " "vendor contracts before the next fiscal year. The board will convene on Tuesday to " "discuss the findings." ], [ "She laughed so hard the milk came out of her nose, which made everyone else laugh " "even harder. Uncle Roberto tried to keep a straight face but lost it when the dog " "jumped onto the table and stole an entire chicken leg. Grandma just shook her head " "and muttered something about heathens. It was, by all accounts, a perfectly normal " "Sunday dinner." ], ] # ── Gradio app ── with gr.Blocks( title="Multi-Lens Text Analyzer", theme=gr.themes.Soft(), css=""" .gradio-container { max-width: 980px !important; } #go-btn { background: linear-gradient(135deg, #667eea, #764ba2) !important; color: white !important; font-weight: 600 !important; font-size: 1.05em !important; min-height: 44px !important; } """, ) as demo: gr.Markdown( "## Multi-Lens Text Analyzer\n" "Paste any passage and watch a single zero-shot model analyze it through " "four different lenses — tone, formality, energy, and genre feel.\n\n" "*Uses the same model and the same approach as the Silly Phrase Finder — " "just with a richer interface and more ambitious questions.*" ) with gr.Row(): text_input = gr.Textbox( lines=5, placeholder="Paste a paragraph or passage here…", label="Your Passage", scale=5, ) analyze_btn = gr.Button( "Analyze ✦", elem_id="go-btn", scale=1, size="lg" ) output_html = gr.HTML(label="Analysis Dashboard") gr.Examples(examples=EXAMPLES, inputs=text_input, label="Try a Passage") analyze_btn.click( fn=analyze_passage, inputs=text_input, outputs=output_html ) text_input.submit( fn=analyze_passage, inputs=text_input, outputs=output_html ) demo.launch()