"""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'
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'''⚠️ 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