CyberFox commited on
Commit
ffbc075
·
0 Parent(s):

feat: show DBpedia links in entity cards

Browse files
Files changed (3) hide show
  1. README.md +27 -0
  2. app.py +534 -0
  3. requirements.txt +1 -0
README.md ADDED
@@ -0,0 +1,27 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ ---
2
+ title: Content Analysis v3
3
+ emoji: 🔬
4
+ colorFrom: indigo
5
+ colorTo: purple
6
+ sdk: gradio
7
+ sdk_version: 5.34.0
8
+ app_file: app.py
9
+ pinned: true
10
+ license: apache-2.0
11
+ python_version: "3.11"
12
+ short_description: Multilingual NER + NED Entity Linking (EN/IT/FR/DE/ES)
13
+ env:
14
+ - GRADIO_SSR_MODE=false
15
+ ---
16
+
17
+ # Entity Extraction & Linking [FREE TOOL]
18
+
19
+ Elevate your website's performance with our AI-powered entity extraction and disambiguation tool.
20
+
21
+ Ready to turbocharge your SEO efforts and see your organic search performance soar? Look no further than our next-generation AI-powered entity linker — now with multilingual support across **five languages**!
22
+
23
+ With our intelligent solution powered by **GLiNER** and **BGE-M3**, you can analyze your content — whether raw text or any URL — and instantly identify all relevant entities within it. Each entity is automatically linked to its corresponding **Wikidata entry**, complete with descriptions and confidence scores.
24
+
25
+ This powerful combination helps search engines like Google better understand your content and rank it higher in search results. But that's not all! With precise entity disambiguation, our tool distinguishes between entities that share the same name, ensuring accurate knowledge graph connections that amplify your SEO benefits.
26
+
27
+ **Supported languages:** English 🇬🇧 · Italian 🇮🇹 · French 🇫🇷 · German 🇩🇪 · Spanish 🇪🇸
app.py ADDED
@@ -0,0 +1,534 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Content Analysis v3 — Hugging Face Space Demo.
2
+
3
+ Interactive multilingual entity analysis with highlighted text
4
+ and entity cards. Supports text input and URL extraction.
5
+
6
+ Requires a WordLift API key for authentication.
7
+ """
8
+
9
+ import os
10
+ os.environ["GRADIO_SSR_MODE"] = "false"
11
+
12
+ import gradio as gr
13
+ import requests
14
+
15
+ # ---------------------------------------------------------------------------
16
+ # Configuration
17
+ # ---------------------------------------------------------------------------
18
+ API_BASE = "https://wordlift-lab--content-analysis-v3-web-app.modal.run"
19
+
20
+ # Entity type → color mapping (for highlighting)
21
+ TYPE_COLORS = {
22
+ "Person": "#6366f1", # Indigo
23
+ "Organization": "#0ea5e9", # Sky blue
24
+ "Company": "#0ea5e9",
25
+ "City": "#10b981", # Emerald
26
+ "Country": "#10b981",
27
+ "Place": "#10b981",
28
+ "Location": "#10b981",
29
+ "Brand": "#f59e0b", # Amber
30
+ "Product": "#f59e0b",
31
+ "Date": "#8b5cf6", # Violet
32
+ "Event": "#ec4899", # Pink
33
+ "Movie": "#f43f5e", # Rose
34
+ "Book": "#f43f5e",
35
+ "Song": "#f43f5e",
36
+ "CreativeWork": "#f43f5e",
37
+ "MedicalCondition": "#14b8a6", # Teal
38
+ "Drug": "#14b8a6",
39
+ "SportsTeam": "#0ea5e9",
40
+ "EducationalOrganization": "#0ea5e9",
41
+ }
42
+
43
+ DEFAULT_COLOR = "#64748b" # Slate
44
+
45
+ # ---------------------------------------------------------------------------
46
+ # API calls
47
+ # ---------------------------------------------------------------------------
48
+ def analyze_text_api(text: str, api_key: str, language=None, confidence: float = 0.5) -> dict:
49
+ """Call the Content Analysis v3 text analysis API."""
50
+ payload = {"text": text, "confidence": confidence}
51
+ if language and language != "auto":
52
+ payload["language"] = language
53
+ headers = {"Authorization": f"Key {api_key}"}
54
+ try:
55
+ resp = requests.post(f"{API_BASE}/analyze/text", json=payload, headers=headers, timeout=120)
56
+ resp.raise_for_status()
57
+ return resp.json()
58
+ except requests.exceptions.HTTPError as e:
59
+ if e.response is not None and e.response.status_code == 401:
60
+ return {"error": "Invalid WordLift API key. Get yours at https://wordlift.io"}
61
+ return {"error": str(e), "entities": []}
62
+ except requests.exceptions.RequestException as e:
63
+ return {"error": str(e), "entities": []}
64
+
65
+
66
+ def analyze_url_api(url: str, api_key: str, language=None, confidence: float = 0.5) -> dict:
67
+ """Call the Content Analysis v3 URL analysis API (extraction happens server-side)."""
68
+ payload = {"url": url, "confidence": confidence}
69
+ if language and language != "auto":
70
+ payload["language"] = language
71
+ headers = {"Authorization": f"Key {api_key}"}
72
+ try:
73
+ resp = requests.post(f"{API_BASE}/analyze/url", json=payload, headers=headers, timeout=120)
74
+ resp.raise_for_status()
75
+ return resp.json()
76
+ except requests.exceptions.HTTPError as e:
77
+ if e.response is not None and e.response.status_code == 401:
78
+ return {"error": "Invalid WordLift API key. Get yours at https://wordlift.io"}
79
+ return {"error": str(e), "entities": []}
80
+ except requests.exceptions.RequestException as e:
81
+ return {"error": str(e), "entities": []}
82
+
83
+
84
+ # ---------------------------------------------------------------------------
85
+ # Rendering
86
+ # ---------------------------------------------------------------------------
87
+ def build_highlighted_html(text: str, entities: list[dict]) -> str:
88
+ """Build HTML with highlighted entity spans and hover tooltips."""
89
+ if not entities:
90
+ return f'<div class="analyzed-text">{_escape(text)}</div>'
91
+
92
+ # Sort entities by start position (reverse for safe insertion)
93
+ sorted_ents = sorted(entities, key=lambda e: e["start"])
94
+
95
+ # Build segments
96
+ segments = []
97
+ last_end = 0
98
+
99
+ for ent in sorted_ents:
100
+ start = ent["start"]
101
+ end = ent["end"]
102
+
103
+ # Skip overlapping entities
104
+ if start < last_end:
105
+ continue
106
+
107
+ # Text before entity
108
+ if start > last_end:
109
+ segments.append(_escape(text[last_end:start]))
110
+
111
+ # Entity span with tooltip
112
+ color = TYPE_COLORS.get(ent["label"], DEFAULT_COLOR)
113
+ score = ent.get("score", 0)
114
+ entity_id = ent.get("entity_id", "")
115
+ entity_label = ent.get("entity_label", "")
116
+ entity_desc = ent.get("entity_description", "")
117
+ disambig = ent.get("disambiguation_score")
118
+
119
+ tooltip_parts = [f"Type: {ent['label']}", f"NER Score: {score:.2f}"]
120
+ if entity_id:
121
+ tooltip_parts.append(f"Entity: {entity_id}")
122
+ if entity_label:
123
+ tooltip_parts.append(f"Label: {entity_label}")
124
+ if disambig is not None:
125
+ tooltip_parts.append(f"Disambiguation: {disambig:.2f}")
126
+ tooltip = " | ".join(tooltip_parts)
127
+
128
+ entity_text = _escape(text[start:end])
129
+
130
+ # NED linked entity gets a special badge
131
+ ned_badge = ""
132
+ if entity_id:
133
+ ned_badge = (
134
+ f'<span style="font-size: 0.55em; font-weight: 700; '
135
+ f'color: #7c3aed; vertical-align: super; margin-left: 1px;">✓</span>'
136
+ )
137
+
138
+ segments.append(
139
+ f'<mark class="entity-highlight" style="background-color: {color}22; '
140
+ f'border-bottom: 2px solid {color}; color: inherit; padding: 2px 4px; '
141
+ f'border-radius: 3px; cursor: pointer;" title="{_escape(tooltip)}">'
142
+ f'{entity_text}'
143
+ f'<span class="entity-label" style="font-size: 0.65em; font-weight: 600; '
144
+ f'color: {color}; vertical-align: super; margin-left: 2px;">{ent["label"]}</span>'
145
+ f'{ned_badge}'
146
+ f'</mark>'
147
+ )
148
+ last_end = end
149
+
150
+ # Remaining text
151
+ if last_end < len(text):
152
+ segments.append(_escape(text[last_end:]))
153
+
154
+ return f'<div class="analyzed-text" style="font-size: 1.05em; line-height: 1.8; padding: 16px;">{"".join(segments)}</div>'
155
+
156
+
157
+ def build_entity_cards_html(entities: list[dict]) -> str:
158
+ """Build HTML entity cards showing details for each detected entity."""
159
+ if not entities:
160
+ return '<p style="color: #64748b; text-align: center; padding: 2em;">No entities detected.</p>'
161
+
162
+ # Deduplicate by text
163
+ seen = set()
164
+ unique = []
165
+ for ent in entities:
166
+ key = ent.get("text", "")
167
+ if key not in seen:
168
+ seen.add(key)
169
+ unique.append(ent)
170
+
171
+ cards_html = []
172
+ for ent in unique:
173
+ color = TYPE_COLORS.get(ent["label"], DEFAULT_COLOR)
174
+ score = ent.get("score", 0)
175
+ entity_id = ent.get("entity_id")
176
+ entity_label = ent.get("entity_label", "")
177
+ entity_desc = ent.get("entity_description", "")
178
+ disambig = ent.get("disambiguation_score")
179
+
180
+ # Badge
181
+ badge = (
182
+ f'<span style="display: inline-block; background: {color}; color: white; '
183
+ f'font-size: 0.7em; font-weight: 600; padding: 2px 8px; border-radius: 12px; '
184
+ f'letter-spacing: 0.5px; text-transform: uppercase;">{ent["label"]}</span>'
185
+ )
186
+
187
+ # NED status badge
188
+ ned_status = ""
189
+ if entity_id:
190
+ ned_status = (
191
+ '<span style="display: inline-block; background: #7c3aed; color: white; '
192
+ 'font-size: 0.65em; font-weight: 600; padding: 2px 6px; border-radius: 12px; '
193
+ 'margin-left: 4px;">NED ✓</span>'
194
+ )
195
+ else:
196
+ ned_status = (
197
+ '<span style="display: inline-block; background: #e2e8f0; color: #64748b; '
198
+ 'font-size: 0.65em; font-weight: 600; padding: 2px 6px; border-radius: 12px; '
199
+ 'margin-left: 4px;">NER only</span>'
200
+ )
201
+
202
+ # QID + DBpedia links
203
+ qid_html = ""
204
+ if entity_id:
205
+ dbpedia_uri = ent.get("dbpedia_uri", "")
206
+ dbpedia_link = ""
207
+ if dbpedia_uri:
208
+ dbpedia_link = (
209
+ f' <a href="{dbpedia_uri}" target="_blank" '
210
+ f'style="color: #64748b; text-decoration: none; font-size: 0.85em; '
211
+ f'font-weight: 500;">📚 DBpedia</a>'
212
+ )
213
+ qid_html = (
214
+ f'<a href="https://www.wikidata.org/wiki/{entity_id}" target="_blank" '
215
+ f'style="color: {color}; text-decoration: none; font-size: 0.85em; '
216
+ f'font-weight: 500;">🔗 {entity_id}</a>'
217
+ f'{dbpedia_link}'
218
+ )
219
+
220
+ # Scores bar
221
+ score_bar = _score_bar("NER", score, color)
222
+ disambig_bar = ""
223
+ if disambig is not None:
224
+ disambig_bar = _score_bar("NED", disambig, "#8b5cf6")
225
+
226
+ # Description
227
+ desc_html = ""
228
+ if entity_desc:
229
+ desc_html = f'<p style="color: #64748b; font-size: 0.85em; margin: 6px 0 0 0; line-height: 1.4;">{_escape(entity_desc)}</p>'
230
+
231
+ # Entity label (canonical from KB)
232
+ label_html = ""
233
+ if entity_label and entity_label != ent.get("text", ""):
234
+ label_html = f'<p style="color: #64748b; font-size: 0.8em; margin: 2px 0;">aka: {_escape(entity_label)}</p>'
235
+
236
+ card = f'''
237
+ <div style="background: #f8fafc; border: 1px solid #e2e8f0; border-left: 3px solid {color};
238
+ border-radius: 8px; padding: 14px 16px; margin-bottom: 8px;">
239
+ <div style="display: flex; align-items: center; justify-content: space-between; margin-bottom: 6px;">
240
+ <div>
241
+ <span style="font-size: 1.05em; font-weight: 600; color: #1e293b;">{_escape(ent.get("text", ""))}</span>
242
+ {badge}
243
+ {ned_status}
244
+ </div>
245
+ {qid_html}
246
+ </div>
247
+ {label_html}
248
+ {desc_html}
249
+ <div style="margin-top: 8px;">
250
+ {score_bar}
251
+ {disambig_bar}
252
+ </div>
253
+ </div>
254
+ '''
255
+ cards_html.append(card)
256
+
257
+ return f'<div style="max-height: 500px; overflow-y: auto;">{"".join(cards_html)}</div>'
258
+
259
+
260
+ def build_stats_html(result: dict) -> str:
261
+ """Build summary stats HTML."""
262
+ entities = result.get("entities", [])
263
+ lang = result.get("language", "—")
264
+ time_ms = result.get("processing_time_ms", 0)
265
+ version = result.get("pipeline_version", "—")
266
+
267
+ # Count NER-only vs NED-linked
268
+ ned_count = sum(1 for e in entities if e.get("entity_id"))
269
+ ner_only_count = len(entities) - ned_count
270
+
271
+ # Type distribution
272
+ type_counts = {}
273
+ for ent in entities:
274
+ t = ent.get("label", "Unknown")
275
+ type_counts[t] = type_counts.get(t, 0) + 1
276
+
277
+ type_badges = " ".join(
278
+ f'<span style="background: {TYPE_COLORS.get(t, DEFAULT_COLOR)}33; color: {TYPE_COLORS.get(t, DEFAULT_COLOR)}; '
279
+ f'padding: 3px 10px; border-radius: 12px; font-size: 0.8em; font-weight: 500;">'
280
+ f'{t}: {c}</span>'
281
+ for t, c in sorted(type_counts.items(), key=lambda x: -x[1])
282
+ )
283
+
284
+ lang_flags = {"en": "🇬🇧", "it": "🇮🇹", "fr": "🇫🇷", "de": "🇩🇪", "es": "🇪🇸"}
285
+ flag = lang_flags.get(lang, "🌐")
286
+
287
+ return f'''
288
+ <div style="display: flex; gap: 16px; flex-wrap: wrap; padding: 8px 0;">
289
+ <div style="background: #f8fafc; border: 1px solid #e2e8f0; border-radius: 8px; padding: 10px 16px; flex: 1; min-width: 100px; text-align: center;">
290
+ <div style="font-size: 1.5em; font-weight: 700; color: #4f46e5;">{len(entities)}</div>
291
+ <div style="font-size: 0.75em; color: #64748b; text-transform: uppercase; letter-spacing: 1px;">Entities</div>
292
+ </div>
293
+ <div style="background: #f8fafc; border: 1px solid #e2e8f0; border-radius: 8px; padding: 10px 16px; flex: 1; min-width: 100px; text-align: center;">
294
+ <div style="font-size: 1.5em; font-weight: 700; color: #7c3aed;">{ned_count}</div>
295
+ <div style="font-size: 0.75em; color: #64748b; text-transform: uppercase; letter-spacing: 1px;">NED Linked</div>
296
+ </div>
297
+ <div style="background: #f8fafc; border: 1px solid #e2e8f0; border-radius: 8px; padding: 10px 16px; flex: 1; min-width: 100px; text-align: center;">
298
+ <div style="font-size: 1.5em; font-weight: 700; color: #059669;">{flag} {lang.upper()}</div>
299
+ <div style="font-size: 0.75em; color: #64748b; text-transform: uppercase; letter-spacing: 1px;">Language</div>
300
+ </div>
301
+ <div style="background: #f8fafc; border: 1px solid #e2e8f0; border-radius: 8px; padding: 10px 16px; flex: 1; min-width: 100px; text-align: center;">
302
+ <div style="font-size: 1.5em; font-weight: 700; color: #d97706;">{time_ms:.0f}ms</div>
303
+ <div style="font-size: 0.75em; color: #64748b; text-transform: uppercase; letter-spacing: 1px;">Latency</div>
304
+ </div>
305
+ <div style="background: #f8fafc; border: 1px solid #e2e8f0; border-radius: 8px; padding: 10px 16px; flex: 1; min-width: 100px; text-align: center;">
306
+ <div style="font-size: 1.1em; font-weight: 600; color: #7c3aed;">{version}</div>
307
+ <div style="font-size: 0.75em; color: #64748b; text-transform: uppercase; letter-spacing: 1px;">Pipeline</div>
308
+ </div>
309
+ </div>
310
+ <div style="padding: 6px 0;">{type_badges}</div>
311
+ '''
312
+
313
+
314
+ def _score_bar(label: str, score: float, color: str) -> str:
315
+ """Render a mini score bar."""
316
+ pct = max(0, min(100, score * 100))
317
+ return (
318
+ f'<div style="display: flex; align-items: center; gap: 8px; margin: 3px 0;">'
319
+ f'<span style="font-size: 0.7em; color: #64748b; width: 28px; text-align: right;">{label}</span>'
320
+ f'<div style="flex: 1; background: #e2e8f0; border-radius: 4px; height: 6px; overflow: hidden;">'
321
+ f'<div style="width: {pct}%; background: {color}; height: 100%; border-radius: 4px;"></div>'
322
+ f'</div>'
323
+ f'<span style="font-size: 0.75em; color: #475569; width: 40px;">{score:.2f}</span>'
324
+ f'</div>'
325
+ )
326
+
327
+
328
+ def _escape(s: str) -> str:
329
+ """HTML-escape a string."""
330
+ return s.replace("&", "&amp;").replace("<", "&lt;").replace(">", "&gt;").replace('"', "&quot;")
331
+
332
+
333
+ # ---------------------------------------------------------------------------
334
+ # Gradio handlers
335
+ # ---------------------------------------------------------------------------
336
+ def analyze_text_handler(text: str, language: str, confidence: float):
337
+ """Handle text analysis."""
338
+ api_key = os.environ.get("WL_KEY", "").strip()
339
+ if not api_key:
340
+ err = "<p style='color:#f43f5e;text-align:center;'>⚠️ WL_KEY secret not configured.</p>"
341
+ return err, err, ""
342
+
343
+ if not text or not text.strip():
344
+ msg = "<p style='color:#94a3b8;text-align:center;'>Enter text to analyze.</p>"
345
+ return msg, msg, ""
346
+
347
+ try:
348
+ lang = language if language != "Auto-detect" else None
349
+ result = analyze_text_api(text, api_key, lang, confidence)
350
+
351
+ if "error" in result:
352
+ err_html = f"<p style='color: #f43f5e;'>API Error: {result['error']}</p>"
353
+ return err_html, err_html, ""
354
+
355
+ entities = result.get("entities", [])
356
+ stats = build_stats_html(result)
357
+ highlighted = build_highlighted_html(text, entities)
358
+ cards = build_entity_cards_html(entities)
359
+
360
+ return stats, highlighted, cards
361
+ except Exception as e:
362
+ err_html = f"<p style='color: #f43f5e;'>Error: {str(e)}</p>"
363
+ return err_html, err_html, ""
364
+
365
+
366
+ def analyze_url_handler(url: str, language: str, confidence: float):
367
+ """Handle URL analysis."""
368
+ api_key = os.environ.get("WL_KEY", "").strip()
369
+ if not api_key:
370
+ err = "<p style='color:#f43f5e;text-align:center;'>⚠️ WL_KEY secret not configured.</p>"
371
+ return err, err, "", ""
372
+
373
+ if not url or not url.strip():
374
+ msg = "<p style='color:#94a3b8;text-align:center;'>Enter a URL to analyze.</p>"
375
+ return msg, msg, "", ""
376
+
377
+ try:
378
+ lang = language if language != "Auto-detect" else None
379
+ result = analyze_url_api(url, api_key, lang, confidence)
380
+
381
+ if "error" in result:
382
+ err_html = f"<p style='color: #f43f5e;'>API Error: {result['error']}</p>"
383
+ return err_html, err_html, "", ""
384
+
385
+ entities = result.get("entities", [])
386
+ text = result.get("extracted_text", result.get("text", ""))
387
+ stats = build_stats_html(result)
388
+ highlighted = build_highlighted_html(text[:5000], entities)
389
+ cards = build_entity_cards_html(entities)
390
+
391
+ return stats, highlighted, cards, text[:2000]
392
+ except Exception as e:
393
+ err_html = f"<p style='color: #f43f5e;'>Error: {str(e)}</p>"
394
+ return err_html, err_html, "", ""
395
+
396
+
397
+ # ---------------------------------------------------------------------------
398
+ # Gradio UI
399
+ # ---------------------------------------------------------------------------
400
+ CUSTOM_CSS = """
401
+ .gradio-container {
402
+ max-width: 1200px !important;
403
+ font-family: 'Inter', -apple-system, BlinkMacSystemFont, sans-serif !important;
404
+ }
405
+ .analyzed-text {
406
+ font-family: 'Inter', -apple-system, BlinkMacSystemFont, sans-serif;
407
+ }
408
+ .entity-highlight:hover {
409
+ filter: brightness(0.95);
410
+ }
411
+ footer { display: none !important; }
412
+ """
413
+
414
+ DESCRIPTION = """
415
+ <div style="text-align: center; padding: 8px 0;">
416
+ <p style="color: #475569; font-size: 0.95em; margin: 0;">
417
+ Multilingual Named Entity Recognition &amp; Disambiguation powered by
418
+ <strong style="color: #4f46e5;">GLiNER</strong> +
419
+ <strong style="color: #7c3aed;">BGE-M3</strong> •
420
+ Supports <strong>EN</strong> 🇬🇧 <strong>IT</strong> 🇮🇹 <strong>FR</strong> 🇫🇷 <strong>DE</strong> 🇩🇪 <strong>ES</strong> 🇪🇸
421
+ </p>
422
+ <p style="color: #64748b; font-size: 0.8em; margin-top: 4px;">
423
+ <strong style="color: #4f46e5;">NER</strong> detects entity mentions •
424
+ <strong style="color: #7c3aed;">NED</strong> links them to Wikidata •
425
+ Entities with ✓ are disambiguated
426
+ </p>
427
+ </div>
428
+ """
429
+
430
+ EXAMPLES_TEXT = [
431
+ ["Elon Musk founded SpaceX in 2002 and serves as CEO of Tesla in Austin, Texas.", "Auto-detect", 0.5],
432
+ ["Il presidente Sergio Mattarella ha visitato il Quirinale a Roma con il primo ministro.", "Auto-detect", 0.5],
433
+ ["Emmanuel Macron a rencontré Angela Merkel à l'Élysée à Paris pour discuter du Brexit.", "Auto-detect", 0.5],
434
+ ["Die Europäische Zentralbank in Frankfurt hat neue geldpolitische Maßnahmen angekündigt.", "Auto-detect", 0.5],
435
+ ["Lionel Messi firmó un contrato con el Inter Miami en los Estados Unidos.", "Auto-detect", 0.5],
436
+ ]
437
+
438
+ EXAMPLES_URL = [
439
+ ["https://en.wikipedia.org/wiki/OpenAI", "Auto-detect", 0.5],
440
+ ["https://it.wikipedia.org/wiki/Roma", "Auto-detect", 0.5],
441
+ ]
442
+
443
+
444
+ with gr.Blocks(
445
+ title="Content Analysis v3 — WordLift",
446
+ css=CUSTOM_CSS,
447
+ theme=gr.themes.Soft(
448
+ primary_hue="indigo",
449
+ secondary_hue="slate",
450
+ neutral_hue="slate",
451
+ font=("Inter", "system-ui", "sans-serif"),
452
+ ),
453
+ ) as demo:
454
+
455
+ with gr.Tabs():
456
+ # ---- TEXT TAB ----
457
+ with gr.TabItem("📝 Text Analysis", id="text_tab"):
458
+ with gr.Row():
459
+ with gr.Column(scale=3):
460
+ text_input = gr.Textbox(
461
+ label="Input Text",
462
+ placeholder="Enter text to analyze for entities...",
463
+ lines=5,
464
+ max_lines=15,
465
+ )
466
+ with gr.Column(scale=1):
467
+ lang_dropdown = gr.Dropdown(
468
+ choices=["Auto-detect", "en", "it", "fr", "de", "es"],
469
+ value="Auto-detect",
470
+ label="Language",
471
+ )
472
+ confidence_slider = gr.Slider(
473
+ minimum=0.1, maximum=1.0, value=0.5, step=0.05,
474
+ label="Confidence Threshold",
475
+ )
476
+ text_btn = gr.Button("🔍 Analyze", variant="primary", size="lg")
477
+
478
+ stats_output = gr.HTML(label="Summary")
479
+ highlighted_output = gr.HTML(label="Highlighted Text")
480
+ cards_output = gr.HTML(label="Entity Cards")
481
+
482
+ text_btn.click(
483
+ fn=analyze_text_handler,
484
+ inputs=[text_input, lang_dropdown, confidence_slider],
485
+ outputs=[stats_output, highlighted_output, cards_output],
486
+ )
487
+
488
+ gr.Examples(
489
+ examples=EXAMPLES_TEXT,
490
+ inputs=[text_input, lang_dropdown, confidence_slider],
491
+ label="🌍 Try these multilingual examples",
492
+ )
493
+
494
+ # ---- URL TAB ----
495
+ with gr.TabItem("🔗 URL Analysis", id="url_tab"):
496
+ with gr.Row():
497
+ with gr.Column(scale=3):
498
+ url_input = gr.Textbox(
499
+ label="URL",
500
+ placeholder="https://en.wikipedia.org/wiki/...",
501
+ lines=1,
502
+ )
503
+ with gr.Column(scale=1):
504
+ url_lang = gr.Dropdown(
505
+ choices=["Auto-detect", "en", "it", "fr", "de", "es"],
506
+ value="Auto-detect",
507
+ label="Language",
508
+ )
509
+ url_confidence = gr.Slider(
510
+ minimum=0.1, maximum=1.0, value=0.5, step=0.05,
511
+ label="Confidence Threshold",
512
+ )
513
+ url_btn = gr.Button("🔍 Analyze URL", variant="primary", size="lg")
514
+
515
+ url_stats = gr.HTML(label="Summary")
516
+ url_highlighted = gr.HTML(label="Highlighted Text (first 5000 chars)")
517
+ url_cards = gr.HTML(label="Entity Cards")
518
+ url_extracted = gr.Textbox(label="Extracted Text (preview)", lines=5, interactive=False)
519
+
520
+ url_btn.click(
521
+ fn=analyze_url_handler,
522
+ inputs=[url_input, url_lang, url_confidence],
523
+ outputs=[url_stats, url_highlighted, url_cards, url_extracted],
524
+ )
525
+
526
+ gr.Examples(
527
+ examples=EXAMPLES_URL,
528
+ inputs=[url_input, url_lang, url_confidence],
529
+ label="🔗 Try these URLs",
530
+ )
531
+
532
+
533
+ if __name__ == "__main__":
534
+ demo.launch(ssr_mode=False)
requirements.txt ADDED
@@ -0,0 +1 @@
 
 
1
+ # No extra dependencies needed — Gradio (from sdk_version) and requests are pre-installed