Spaces:
Sleeping
Sleeping
| 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'<p style="color:#999;text-align:center;padding:48px 0;' | |
| f'font-family:system-ui;font-size:1.05em;">{msg}</p>' | |
| ) | |
| 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""" | |
| <div class="lens-card"> | |
| <div class="lens-icon">{icon}</div> | |
| <div class="lens-title">{lens_name}</div> | |
| <div class="lens-result">{short_label(top_label)}</div> | |
| <div class="lens-score">{top_score:.0%} confidence</div> | |
| </div>""" | |
| # ── 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""" | |
| <div class="s-row" style="animation-delay:{i * 0.12}s"> | |
| <div class="s-num" style="background:{color}">{i + 1}</div> | |
| <div class="s-body"> | |
| <div class="s-text">{sd['text']}</div> | |
| <div class="s-meta"> | |
| <span class="s-badge" style="background:{color}">{short_label(sd['tone'])}</span> | |
| <div class="bar-bg"><div class="bar-fill" style="width:{pct}%;background:{color}"></div></div> | |
| <span class="s-pct">{sd['score']:.0%}</span> | |
| </div> | |
| </div> | |
| </div>""" | |
| # ── 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""" | |
| <style> | |
| /* ── Reset & base ── */ | |
| .mlta *,.mlta *::before,.mlta *::after{{box-sizing:border-box;margin:0;padding:0}} | |
| .mlta{{ | |
| font-family:'Segoe UI',system-ui,-apple-system,sans-serif; | |
| max-width:920px;margin:0 auto;color:#1a1a2e; | |
| }} | |
| /* ── Header ── */ | |
| .mlta-header{{ | |
| text-align:center;padding:24px 16px 16px; | |
| border-bottom:2px solid #e8e8f0;margin-bottom:24px; | |
| }} | |
| .mlta-header h2{{font-size:1.5em;font-weight:800; | |
| background:linear-gradient(135deg,#667eea,#764ba2); | |
| -webkit-background-clip:text;-webkit-text-fill-color:transparent; | |
| background-clip:text;margin-bottom:4px; | |
| }} | |
| .mlta-header p{{color:#888;font-size:0.88em;}} | |
| /* ── Lens cards ── */ | |
| .lens-grid{{display:grid;grid-template-columns:repeat(4,1fr);gap:12px;margin-bottom:28px;}} | |
| .lens-card{{ | |
| background:#fff;border:1px solid #e8e8f0;border-radius:14px; | |
| padding:18px 12px;text-align:center; | |
| transition:transform .2s,box-shadow .2s; | |
| }} | |
| .lens-card:hover{{transform:translateY(-3px);box-shadow:0 6px 18px rgba(102,126,234,.12);}} | |
| .lens-icon{{font-size:1.5em;margin-bottom:6px;}} | |
| .lens-title{{font-size:.7em;text-transform:uppercase;letter-spacing:1.2px;color:#999;font-weight:700;margin-bottom:6px;}} | |
| .lens-result{{font-size:1.05em;font-weight:700;color:#16213e;margin-bottom:2px;text-transform:capitalize;}} | |
| .lens-score{{font-size:.78em;color:#667eea;font-weight:600;}} | |
| /* ── Two-column layout ── */ | |
| .two-col{{display:grid;grid-template-columns:1fr 1fr;gap:24px;margin-bottom:20px;}} | |
| /* ── Radar section ── */ | |
| .radar-sec{{background:#fafafe;border-radius:14px;border:1px solid #e8e8f0;padding:20px;}} | |
| .radar-sec h3{{font-size:.95em;color:#16213e;margin-bottom:4px;}} | |
| .radar-tabs{{display:flex;gap:6px;margin-bottom:14px;flex-wrap:wrap;}} | |
| .radar-tab{{ | |
| font-size:.72em;padding:4px 10px;border-radius:8px;border:1px solid #ddd; | |
| background:#fff;cursor:pointer;font-weight:600;color:#666; | |
| transition:all .2s; | |
| }} | |
| .radar-tab.active{{background:linear-gradient(135deg,#667eea,#764ba2);color:#fff;border-color:transparent;}} | |
| .radar-canvas-wrap{{position:relative;width:100%;aspect-ratio:1;}} | |
| .radar-canvas-wrap canvas{{position:absolute;top:0;left:0;width:100%!important;height:100%!important;}} | |
| /* ── Sentence section ── */ | |
| .sent-sec{{background:#fafafe;border-radius:14px;border:1px solid #e8e8f0;padding:20px;overflow-y:auto;max-height:420px;}} | |
| .sent-sec h3{{font-size:.95em;color:#16213e;margin-bottom:14px;}} | |
| .s-row{{ | |
| display:flex;gap:10px;padding:10px 0;border-bottom:1px solid #f0f0f5; | |
| opacity:0;animation:fadeIn .45s ease forwards; | |
| }} | |
| .s-row:last-child{{border-bottom:none;}} | |
| @keyframes fadeIn{{from{{opacity:0;transform:translateX(-8px)}}to{{opacity:1;transform:translateX(0)}}}} | |
| .s-num{{ | |
| width:26px;height:26px;border-radius:50%;color:#fff; | |
| display:flex;align-items:center;justify-content:center; | |
| font-size:.72em;font-weight:700;flex-shrink:0;margin-top:2px; | |
| }} | |
| .s-body{{flex:1;min-width:0;}} | |
| .s-text{{font-size:.83em;line-height:1.45;color:#333;margin-bottom:5px;}} | |
| .s-meta{{display:flex;align-items:center;gap:8px;}} | |
| .s-badge{{font-size:.68em;color:#fff;padding:2px 9px;border-radius:10px;font-weight:600;white-space:nowrap;text-transform:capitalize;}} | |
| .bar-bg{{flex:1;height:4px;background:#e8e8f0;border-radius:2px;overflow:hidden;}} | |
| .bar-fill{{height:100%;border-radius:2px;transition:width .7s ease;}} | |
| .s-pct{{font-size:.73em;color:#999;font-weight:600;min-width:32px;text-align:right;}} | |
| /* ── Footer note ── */ | |
| .mlta-foot{{ | |
| text-align:center;font-size:.76em;color:#aaa; | |
| padding:16px 0 4px;border-top:1px solid #e8e8f0;margin-top:20px;line-height:1.6; | |
| }} | |
| .mlta-foot code{{background:#f0f0f5;padding:1px 6px;border-radius:4px;font-size:.95em;}} | |
| /* ── Responsive ── */ | |
| @media(max-width:720px){{ | |
| .lens-grid{{grid-template-columns:repeat(2,1fr);}} | |
| .two-col{{grid-template-columns:1fr;}} | |
| }} | |
| </style> | |
| <div class="mlta"> | |
| <div class="mlta-header"> | |
| <h2>Passage Analysis Dashboard</h2> | |
| <p>Four analytical lenses — one zero-shot model — no task-specific training</p> | |
| </div> | |
| <div class="lens-grid">{cards}</div> | |
| <div class="two-col"> | |
| <div class="radar-sec"> | |
| <h3>Passage Profile</h3> | |
| <div class="radar-tabs" id="radar-tabs"></div> | |
| <div class="radar-canvas-wrap"><canvas id="radarChart"></canvas></div> | |
| </div> | |
| <div class="sent-sec"> | |
| <h3>Sentence-by-Sentence Tone</h3> | |
| {sentence_rows} | |
| </div> | |
| </div> | |
| <div class="mlta-foot"> | |
| Powered by the same model as the Silly Phrase Finder: | |
| <code>valhalla/distilbart-mnli-12-3</code><br> | |
| Nobody trained it on tone, formality, energy, or genre. | |
| It figures it out from language alone. | |
| </div> | |
| </div> | |
| <script src="https://cdnjs.cloudflare.com/ajax/libs/Chart.js/4.4.1/chart.umd.min.js"></script> | |
| <script> | |
| (function(){{ | |
| const R={radar_json}; | |
| const lenses=Object.keys(R); | |
| const colors=[ | |
| ['rgba(102,126,234,0.75)','rgba(102,126,234,0.08)'], | |
| ['rgba(118,75,162,0.75)','rgba(118,75,162,0.08)'], | |
| ['rgba(233,30,99,0.75)','rgba(233,30,99,0.08)'], | |
| ['rgba(0,188,212,0.75)','rgba(0,188,212,0.08)'], | |
| ]; | |
| const tabsEl=document.getElementById('radar-tabs'); | |
| const ctx=document.getElementById('radarChart'); | |
| if(!ctx||!tabsEl) return; | |
| let chart=null; | |
| function render(idx){{ | |
| const d=R[lenses[idx]]; | |
| if(chart) chart.destroy(); | |
| chart=new Chart(ctx,{{ | |
| type:'radar', | |
| data:{{ | |
| labels:d.labels.map(l=>l.charAt(0).toUpperCase()+l.slice(1)), | |
| datasets:[{{ | |
| label:lenses[idx], | |
| data:d.values, | |
| borderColor:colors[idx][0], | |
| backgroundColor:colors[idx][1], | |
| borderWidth:2.5, | |
| pointBackgroundColor:colors[idx][0], | |
| pointRadius:4, | |
| pointHoverRadius:6, | |
| }}] | |
| }}, | |
| options:{{ | |
| responsive:true,maintainAspectRatio:true, | |
| plugins:{{legend:{{display:false}}}}, | |
| scales:{{r:{{ | |
| beginAtZero:true,max:100, | |
| ticks:{{stepSize:25,font:{{size:9}},backdropColor:'transparent'}}, | |
| pointLabels:{{font:{{size:10,weight:'600'}},color:'#555'}}, | |
| grid:{{color:'rgba(0,0,0,0.05)'}}, | |
| angleLines:{{color:'rgba(0,0,0,0.05)'}}, | |
| }}}}, | |
| animation:{{duration:800,easing:'easeOutQuart'}}, | |
| }} | |
| }}); | |
| document.querySelectorAll('.radar-tab').forEach((t,i)=>{{ | |
| t.classList.toggle('active',i===idx); | |
| }}); | |
| }} | |
| lenses.forEach((name,i)=>{{ | |
| const btn=document.createElement('span'); | |
| btn.textContent=name; | |
| btn.className='radar-tab'+(i===0?' active':''); | |
| btn.onclick=()=>render(i); | |
| tabsEl.appendChild(btn); | |
| }}); | |
| render(0); | |
| }})(); | |
| </script> | |
| """ | |
| 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() | |