"""Content Analysis v3 — Hugging Face Space Demo. Interactive multilingual entity analysis with highlighted text and entity cards. Supports text input and URL extraction. Requires a WordLift API key for authentication. """ import os os.environ["GRADIO_SSR_MODE"] = "false" import gradio as gr import requests # --------------------------------------------------------------------------- # Configuration # --------------------------------------------------------------------------- API_BASE = "https://wordlift-lab--content-analysis-v3-web-app.modal.run" # Entity type → color mapping (for highlighting) TYPE_COLORS = { "Person": "#6366f1", # Indigo "Organization": "#0ea5e9", # Sky blue "Company": "#0ea5e9", "City": "#10b981", # Emerald "Country": "#10b981", "Place": "#10b981", "Location": "#10b981", "Brand": "#f59e0b", # Amber "Product": "#f59e0b", "Date": "#8b5cf6", # Violet "Event": "#ec4899", # Pink "Movie": "#f43f5e", # Rose "Book": "#f43f5e", "Song": "#f43f5e", "CreativeWork": "#f43f5e", "MedicalCondition": "#14b8a6", # Teal "Drug": "#14b8a6", "SportsTeam": "#0ea5e9", "EducationalOrganization": "#0ea5e9", } DEFAULT_COLOR = "#64748b" # Slate # --------------------------------------------------------------------------- # API calls # --------------------------------------------------------------------------- def analyze_text_api(text: str, api_key: str, language=None, confidence: float = 0.5) -> dict: """Call the Content Analysis v3 text analysis API.""" payload = {"text": text, "confidence": confidence} if language and language != "auto": payload["language"] = language headers = {"Authorization": f"Key {api_key}"} try: resp = requests.post(f"{API_BASE}/analyze/text", json=payload, headers=headers, timeout=120) resp.raise_for_status() return resp.json() except requests.exceptions.HTTPError as e: if e.response is not None and e.response.status_code == 401: return {"error": "Invalid WordLift API key. Get yours at https://wordlift.io"} return {"error": str(e), "entities": []} except requests.exceptions.RequestException as e: return {"error": str(e), "entities": []} def analyze_url_api(url: str, api_key: str, language=None, confidence: float = 0.5) -> dict: """Call the Content Analysis v3 URL analysis API (extraction happens server-side).""" payload = {"url": url, "confidence": confidence} if language and language != "auto": payload["language"] = language headers = {"Authorization": f"Key {api_key}"} try: resp = requests.post(f"{API_BASE}/analyze/url", json=payload, headers=headers, timeout=120) resp.raise_for_status() return resp.json() except requests.exceptions.HTTPError as e: if e.response is not None and e.response.status_code == 401: return {"error": "Invalid WordLift API key. Get yours at https://wordlift.io"} return {"error": str(e), "entities": []} except requests.exceptions.RequestException as e: return {"error": str(e), "entities": []} # --------------------------------------------------------------------------- # Rendering # --------------------------------------------------------------------------- def build_highlighted_html(text: str, entities: list[dict]) -> str: """Build HTML with highlighted entity spans and hover tooltips.""" if not entities: return f'
{_escape(text)}
' # Sort entities by start position (reverse for safe insertion) sorted_ents = sorted(entities, key=lambda e: e["start"]) # Build segments segments = [] last_end = 0 for ent in sorted_ents: start = ent["start"] end = ent["end"] # Skip overlapping entities if start < last_end: continue # Text before entity if start > last_end: segments.append(_escape(text[last_end:start])) # Entity span with tooltip color = TYPE_COLORS.get(ent["label"], DEFAULT_COLOR) score = ent.get("score", 0) entity_id = ent.get("entity_id", "") entity_label = ent.get("entity_label", "") entity_desc = ent.get("entity_description", "") disambig = ent.get("disambiguation_score") tooltip_parts = [f"Type: {ent['label']}", f"NER Score: {score:.2f}"] if entity_id: tooltip_parts.append(f"Entity: {entity_id}") if entity_label: tooltip_parts.append(f"Label: {entity_label}") if disambig is not None: tooltip_parts.append(f"Disambiguation: {disambig:.2f}") tooltip = " | ".join(tooltip_parts) entity_text = _escape(text[start:end]) # NED linked entity gets a special badge ned_badge = "" if entity_id: ned_badge = ( f'' ) segments.append( f'' f'{entity_text}' f'{ent["label"]}' f'{ned_badge}' f'' ) last_end = end # Remaining text if last_end < len(text): segments.append(_escape(text[last_end:])) return f'
{"".join(segments)}
' def build_entity_cards_html(entities: list[dict]) -> str: """Build HTML entity cards showing details for each detected entity.""" if not entities: return '

No entities detected.

' # Deduplicate by text seen = set() unique = [] for ent in entities: key = ent.get("text", "") if key not in seen: seen.add(key) unique.append(ent) cards_html = [] for ent in unique: color = TYPE_COLORS.get(ent["label"], DEFAULT_COLOR) score = ent.get("score", 0) entity_id = ent.get("entity_id") entity_label = ent.get("entity_label", "") entity_desc = ent.get("entity_description", "") disambig = ent.get("disambiguation_score") # Badge badge = ( f'{ent["label"]}' ) # NED status badge ned_status = "" if entity_id: ned_status = ( 'NED ✓' ) else: ned_status = ( 'NER only' ) # QID + DBpedia links qid_html = "" if entity_id: dbpedia_uri = ent.get("dbpedia_uri", "") dbpedia_link = "" if dbpedia_uri: dbpedia_link = ( f' 📚 DBpedia' ) qid_html = ( f'🔗 {entity_id}' f'{dbpedia_link}' ) # Scores bar score_bar = _score_bar("NER", score, color) disambig_bar = "" if disambig is not None: disambig_bar = _score_bar("NED", disambig, "#8b5cf6") # Description desc_html = "" if entity_desc: desc_html = f'

{_escape(entity_desc)}

' # Entity label (canonical from KB) label_html = "" if entity_label and entity_label != ent.get("text", ""): label_html = f'

aka: {_escape(entity_label)}

' card = f'''
{_escape(ent.get("text", ""))} {badge} {ned_status}
{qid_html}
{label_html} {desc_html}
{score_bar} {disambig_bar}
''' cards_html.append(card) return f'
{"".join(cards_html)}
' def build_stats_html(result: dict) -> str: """Build summary stats HTML.""" entities = result.get("entities", []) lang = result.get("language", "—") time_ms = result.get("processing_time_ms", 0) version = result.get("pipeline_version", "—") # Count NER-only vs NED-linked ned_count = sum(1 for e in entities if e.get("entity_id")) ner_only_count = len(entities) - ned_count # Type distribution type_counts = {} for ent in entities: t = ent.get("label", "Unknown") type_counts[t] = type_counts.get(t, 0) + 1 type_badges = " ".join( f'' f'{t}: {c}' for t, c in sorted(type_counts.items(), key=lambda x: -x[1]) ) lang_flags = {"en": "🇬🇧", "it": "🇮🇹", "fr": "🇫🇷", "de": "🇩🇪", "es": "🇪🇸"} flag = lang_flags.get(lang, "🌐") return f'''
{len(entities)}
Entities
{ned_count}
NED Linked
{flag} {lang.upper()}
Language
{time_ms:.0f}ms
Latency
{version}
Pipeline
{type_badges}
''' def _score_bar(label: str, score: float, color: str) -> str: """Render a mini score bar.""" pct = max(0, min(100, score * 100)) return ( f'
' f'{label}' f'
' f'
' f'
' f'{score:.2f}' f'
' ) def _escape(s: str) -> str: """HTML-escape a string.""" return s.replace("&", "&").replace("<", "<").replace(">", ">").replace('"', """) # --------------------------------------------------------------------------- # Gradio handlers # --------------------------------------------------------------------------- def analyze_text_handler(text: str, language: str, confidence: float): """Handle text analysis.""" api_key = os.environ.get("WL_KEY", "").strip() if not api_key: err = "

⚠️ WL_KEY secret not configured.

" return err, err, "" if not text or not text.strip(): msg = "

Enter text to analyze.

" return msg, msg, "" try: lang = language if language != "Auto-detect" else None result = analyze_text_api(text, api_key, lang, confidence) if "error" in result: err_html = f"

API Error: {result['error']}

" return err_html, err_html, "" entities = result.get("entities", []) stats = build_stats_html(result) highlighted = build_highlighted_html(text, entities) cards = build_entity_cards_html(entities) return stats, highlighted, cards except Exception as e: err_html = f"

Error: {str(e)}

" return err_html, err_html, "" def analyze_url_handler(url: str, language: str, confidence: float): """Handle URL analysis.""" api_key = os.environ.get("WL_KEY", "").strip() if not api_key: err = "

⚠️ WL_KEY secret not configured.

" return err, err, "", "" if not url or not url.strip(): msg = "

Enter a URL to analyze.

" return msg, msg, "", "" try: lang = language if language != "Auto-detect" else None result = analyze_url_api(url, api_key, lang, confidence) if "error" in result: err_html = f"

API Error: {result['error']}

" return err_html, err_html, "", "" entities = result.get("entities", []) text = result.get("extracted_text", result.get("text", "")) stats = build_stats_html(result) highlighted = build_highlighted_html(text[:5000], entities) cards = build_entity_cards_html(entities) return stats, highlighted, cards, text[:2000] except Exception as e: err_html = f"

Error: {str(e)}

" return err_html, err_html, "", "" # --------------------------------------------------------------------------- # Gradio UI # --------------------------------------------------------------------------- CUSTOM_CSS = """ .gradio-container { max-width: 1200px !important; font-family: 'Inter', -apple-system, BlinkMacSystemFont, sans-serif !important; } .analyzed-text { font-family: 'Inter', -apple-system, BlinkMacSystemFont, sans-serif; } .entity-highlight:hover { filter: brightness(0.95); } footer { display: none !important; } """ DESCRIPTION = """

Multilingual Named Entity Recognition & Disambiguation powered by GLiNER + BGE-M3 • Supports EN 🇬🇧 IT 🇮🇹 FR 🇫🇷 DE 🇩🇪 ES 🇪🇸

NER detects entity mentions • NED links them to Wikidata • Entities with ✓ are disambiguated

""" EXAMPLES_TEXT = [ ["Elon Musk founded SpaceX in 2002 and serves as CEO of Tesla in Austin, Texas.", "Auto-detect", 0.5], ["Il presidente Sergio Mattarella ha visitato il Quirinale a Roma con il primo ministro.", "Auto-detect", 0.5], ["Emmanuel Macron a rencontré Angela Merkel à l'Élysée à Paris pour discuter du Brexit.", "Auto-detect", 0.5], ["Die Europäische Zentralbank in Frankfurt hat neue geldpolitische Maßnahmen angekündigt.", "Auto-detect", 0.5], ["Lionel Messi firmó un contrato con el Inter Miami en los Estados Unidos.", "Auto-detect", 0.5], ] EXAMPLES_URL = [ ["https://en.wikipedia.org/wiki/OpenAI", "Auto-detect", 0.5], ["https://it.wikipedia.org/wiki/Roma", "Auto-detect", 0.5], ] with gr.Blocks( title="Content Analysis v3 — WordLift", css=CUSTOM_CSS, theme=gr.themes.Soft( primary_hue="indigo", secondary_hue="slate", neutral_hue="slate", font=("Inter", "system-ui", "sans-serif"), ), ) as demo: # Force light theme regardless of user's system preference demo.load(None, js="() => { document.querySelector('body').classList.remove('dark'); document.documentElement.style.colorScheme = 'light'; }") with gr.Tabs(): # ---- TEXT TAB ---- with gr.TabItem("📝 Text Analysis", id="text_tab"): with gr.Row(): with gr.Column(scale=3): text_input = gr.Textbox( label="Input Text", placeholder="Enter text to analyze for entities...", lines=5, max_lines=15, ) with gr.Column(scale=1): lang_dropdown = gr.Dropdown( choices=["Auto-detect", "en", "it", "fr", "de", "es"], value="Auto-detect", label="Language", ) confidence_slider = gr.Slider( minimum=0.1, maximum=1.0, value=0.5, step=0.05, label="Confidence Threshold", ) text_btn = gr.Button("🔍 Analyze", variant="primary", size="lg") stats_output = gr.HTML(label="Summary") highlighted_output = gr.HTML(label="Highlighted Text") cards_output = gr.HTML(label="Entity Cards") text_btn.click( fn=analyze_text_handler, inputs=[text_input, lang_dropdown, confidence_slider], outputs=[stats_output, highlighted_output, cards_output], ) gr.Examples( examples=EXAMPLES_TEXT, inputs=[text_input, lang_dropdown, confidence_slider], label="🌍 Try these multilingual examples", ) # ---- URL TAB ---- with gr.TabItem("🔗 URL Analysis", id="url_tab"): with gr.Row(): with gr.Column(scale=3): url_input = gr.Textbox( label="URL", placeholder="https://en.wikipedia.org/wiki/...", lines=1, ) with gr.Column(scale=1): url_lang = gr.Dropdown( choices=["Auto-detect", "en", "it", "fr", "de", "es"], value="Auto-detect", label="Language", ) url_confidence = gr.Slider( minimum=0.1, maximum=1.0, value=0.5, step=0.05, label="Confidence Threshold", ) url_btn = gr.Button("🔍 Analyze URL", variant="primary", size="lg") url_stats = gr.HTML(label="Summary") url_highlighted = gr.HTML(label="Highlighted Text (first 5000 chars)") url_cards = gr.HTML(label="Entity Cards") url_extracted = gr.Textbox(label="Extracted Text (preview)", lines=5, interactive=False) url_btn.click( fn=analyze_url_handler, inputs=[url_input, url_lang, url_confidence], outputs=[url_stats, url_highlighted, url_cards, url_extracted], ) gr.Examples( examples=EXAMPLES_URL, inputs=[url_input, url_lang, url_confidence], label="🔗 Try these URLs", ) if __name__ == "__main__": demo.launch(ssr_mode=False)