Download app.py from simon-clmtd/universal-dependencies-visualizer: direct link, hf CLI and curl.
- Browser
- Download file 52.8 kB
-
https://huggingface.co/spaces/simon-clmtd/universal-dependencies-visualizer/resolve/main/app.py
- Command line
-
hf download hf://spaces/simon-clmtd/universal-dependencies-visualizer/app.py
-
curl -L -o app.py https://huggingface.co/spaces/simon-clmtd/universal-dependencies-visualizer/resolve/main/app.py
52.8 kB
| """ | |
| Universal Dependencies Parser & Visualizer | |
| Gradio Web Application following a calm, pedagogical design. | |
| Tabs: | |
| Tabs are ordered About, Parse, Compare, Annotate & Export, Treebank Guide; About opens by default. | |
| Annotate & Export also defines the public API endpoints (see API_MD). | |
| """ | |
| import html | |
| import inspect | |
| import json | |
| import os | |
| import tempfile | |
| from typing import List, Optional | |
| import gradio as gr | |
| import pandas as pd | |
| ENABLE_SCREENCAST = os.getenv("ENABLE_SCREENCAST", "").strip().lower() in {"1", "true", "yes", "on"} | |
| ScreenRecorder = None | |
| if ENABLE_SCREENCAST: | |
| try: | |
| from gradio_screenrecorder import ScreenRecorder | |
| except ImportError: | |
| ScreenRecorder = None | |
| ENABLE_SCREENCAST = False | |
| from core.udpipe_client import UDPipeClient | |
| from core.conllu_utils import conllu_to_table, compare_trees, extract_tree_signature, BoundedParseCache | |
| from core.treebanks_data import TREEBANK_CATALOG, AVAILABLE_MODELS | |
| from core.annotate import annotate_text, model_ids | |
| from core.tree_export import TreeExportError, export_tree_images | |
| from visualizer.template import render_conllu_visualizer_html, render_side_by_side_visualizers_html | |
| # Initialize client | |
| client = UDPipeClient() | |
| # Load curated examples | |
| EXAMPLES_FILE = os.path.join(os.path.dirname(__file__), "examples.json") | |
| with open(EXAMPLES_FILE, "r", encoding="utf-8") as f: | |
| CURATED_EXAMPLES = json.load(f) | |
| # Stylesheet for calm academic styling with Source Sans Pro | |
| CUSTOM_HEAD = """<link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=Source+Sans+Pro:ital,wght@0,300;0,400;0,600;0,700;1,400&display=swap"> | |
| <script> | |
| window.switchAppTab = function(name, action) { | |
| try { | |
| const tabs = Array.from(document.querySelectorAll('button[role="tab"]')); | |
| const target = tabs.find(function(t) { | |
| const txt = (t.textContent || '').trim().toLowerCase(); | |
| const search = name.trim().toLowerCase(); | |
| return txt === search || txt.startsWith(search); | |
| }); | |
| if (target) { | |
| target.click(); | |
| window.scrollTo({ top: 0, behavior: 'smooth' }); | |
| setTimeout(window.updateShareUrl, 200); | |
| // Execute tab action (e.g. trigger parsing, open help) | |
| setTimeout(function() { | |
| const panel = Array.from(document.querySelectorAll('[role="tabpanel"]')) | |
| .find(function(el) { return el.getBoundingClientRect().height > 0; }); | |
| if (!panel) return; | |
| if (name.toLowerCase().startsWith('parse')) { | |
| const submitBtn = panel.querySelector('button.primary, button[variant="primary"]'); | |
| if (submitBtn) submitBtn.click(); | |
| if (action === 'help') { | |
| var pollCount = 0; | |
| var checkHelp = setInterval(function() { | |
| pollCount++; | |
| var iframe = panel.querySelector('iframe[id^="vis-"]'); | |
| if (iframe && iframe.contentDocument) { | |
| var helpElem = iframe.contentDocument.querySelector('.ud-help'); | |
| if (helpElem) { | |
| helpElem.open = true; | |
| helpElem.scrollIntoView({ behavior: 'smooth', block: 'start' }); | |
| iframe.scrollIntoView({ behavior: 'smooth', block: 'center' }); | |
| clearInterval(checkHelp); | |
| return; | |
| } | |
| } | |
| if (pollCount > 50) clearInterval(checkHelp); | |
| }, 200); | |
| } | |
| } else if (name.toLowerCase().startsWith('compare')) { | |
| const submitBtn = panel.querySelector('button.primary, button[variant="primary"]'); | |
| if (submitBtn) submitBtn.click(); | |
| } | |
| }, 100); | |
| } | |
| } catch (e) { | |
| console.error('Error switching tab:', e); | |
| } | |
| }; | |
| window.updateShareUrl = function() { | |
| try { | |
| const activeTabBtn = document.querySelector('button[role="tab"][aria-selected="true"]'); | |
| if (!activeTabBtn) return; | |
| const activeTabText = activeTabBtn.textContent.trim().toLowerCase(); | |
| const params = new URLSearchParams(); | |
| if (activeTabText.startsWith('parse')) { | |
| params.set('tab', 'parse'); | |
| const panel = Array.from(document.querySelectorAll('[role="tabpanel"]')) | |
| .find(el => el.getBoundingClientRect().height > 0); | |
| if (panel) { | |
| const sent = panel.querySelector('.sentence-input input, .sentence-input textarea'); | |
| if (sent && sent.value.trim() && sent.value.trim() !== 'They buy and sell books.') { | |
| params.set('sentence', sent.value.trim()); | |
| } | |
| const model = panel.querySelector('input[aria-label*="UDPipe parser"]'); | |
| if (model && model.value.trim()) { | |
| params.set('model', model.value.trim()); | |
| } | |
| } | |
| } else if (activeTabText.startsWith('compare')) { | |
| params.set('tab', 'compare'); | |
| const panel = Array.from(document.querySelectorAll('[role="tabpanel"]')) | |
| .find(el => el.getBoundingClientRect().height > 0); | |
| if (panel) { | |
| const sent = panel.querySelector('.sentence-input input, .sentence-input textarea'); | |
| if (sent && sent.value.trim() && sent.value.trim() !== 'They buy and sell books.') { | |
| params.set('sentence', sent.value.trim()); | |
| } | |
| const m1 = panel.querySelector('input[aria-label*="First UDPipe parser"]'); | |
| if (m1 && m1.value.trim()) params.set('model', m1.value.trim()); | |
| const m2 = panel.querySelector('input[aria-label*="Second UDPipe parser"]'); | |
| if (m2 && m2.value.trim()) params.set('model2', m2.value.trim()); | |
| } | |
| } else if (activeTabText.startsWith('annotate')) { | |
| params.set('tab', 'export'); | |
| } else if (activeTabText.startsWith('treebank')) { | |
| params.set('tab', 'guide'); | |
| } else { | |
| // About tab: clean root url | |
| } | |
| const newSearch = params.toString() ? '?' + params.toString() : ''; | |
| const newUrl = window.location.pathname + newSearch; | |
| if (window.location.search !== newSearch) { | |
| window.history.replaceState(null, '', newUrl); | |
| } | |
| } catch (e) { | |
| // ignore | |
| } | |
| }; | |
| document.addEventListener('click', function(e) { | |
| const link = e.target.closest('a[href^="#tab-"]'); | |
| if (link) { | |
| e.preventDefault(); | |
| const hash = link.getAttribute('href'); | |
| if (hash === '#tab-parse-help') { | |
| if (window.switchAppTab) window.switchAppTab('Parse', 'help'); | |
| } else if (hash === '#tab-parse') { | |
| if (window.switchAppTab) window.switchAppTab('Parse', 'parse'); | |
| } else if (hash === '#tab-compare') { | |
| if (window.switchAppTab) window.switchAppTab('Compare', 'compare'); | |
| } else { | |
| const tabMap = { | |
| '#tab-export': 'Annotate & Export', | |
| '#tab-guide': 'Treebank Guide', | |
| '#tab-about': 'About' | |
| }; | |
| const targetName = tabMap[hash]; | |
| if (targetName && window.switchAppTab) { | |
| window.switchAppTab(targetName); | |
| } | |
| } | |
| } else { | |
| setTimeout(window.updateShareUrl, 250); | |
| } | |
| }); | |
| document.addEventListener('change', function(e) { | |
| setTimeout(window.updateShareUrl, 250); | |
| }); | |
| </script>""" | |
| CUSTOM_CSS = """ | |
| /* Calm academic aesthetic with Source Sans Pro */ | |
| html, body, .gradio-container, button, input, select, textarea, .prose { | |
| font-family: 'Source Sans Pro', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif !important; | |
| } | |
| .gradio-container { | |
| width: 100% !important; | |
| max-width: none !important; | |
| padding: 0 !important; | |
| margin: 0 !important; | |
| box-sizing: border-box; | |
| } | |
| .gradio-container main.fillable { | |
| width: 100% !important; | |
| max-width: none !important; | |
| padding: 0 !important; | |
| margin: 0 !important; | |
| box-sizing: border-box; | |
| } | |
| .wrap { | |
| padding-left: 16px !important; | |
| padding-right: 16px !important; | |
| box-sizing: border-box !important; | |
| } | |
| /* Gradio uses input for one line and textarea for multiline textboxes. | |
| Keep the sentence prominent in either form: 24px is intentional. */ | |
| .sentence-input input, .sentence-input textarea { | |
| font-size: 24px !important; | |
| line-height: 1.5 !important; | |
| } | |
| .gradio-container { | |
| background-color: var(--background-fill-primary) !important; | |
| } | |
| .overview-card { | |
| background-color: var(--background-fill-secondary); | |
| border: 1px solid var(--border-color-primary); | |
| border-radius: 6px; | |
| padding: 16px; | |
| } | |
| .diff-alert { | |
| padding: 10px 14px; | |
| border-radius: 6px; | |
| margin-bottom: 10px; | |
| font-size: 13px; | |
| } | |
| .diff-alert-same { | |
| background-color: var(--color-accent-soft, #ecfdf5); | |
| border: 1px solid #a7f3d0; | |
| color: #065f46; | |
| } | |
| .diff-alert-diff { | |
| background-color: #fef2f2; | |
| border: 1px solid #fecaca; | |
| color: #991b1b; | |
| } | |
| .dark .diff-alert-same { | |
| background-color: rgba(6, 95, 70, 0.25); | |
| border: 1px solid #065f46; | |
| color: #6ee7b7; | |
| } | |
| .dark .diff-alert-diff { | |
| background-color: rgba(153, 27, 27, 0.25); | |
| border: 1px solid #991b1b; | |
| color: #fca5a5; | |
| } | |
| /* Promotional feature grid ("Kacheln") */ | |
| .promo-hero { | |
| background: linear-gradient(135deg, #eff6ff 0%, #f5f3ff 50%, #fdf2f8 100%); | |
| border: 1px solid #e0e7ff; | |
| border-radius: 12px; | |
| padding: 28px 24px; | |
| margin-bottom: 24px; | |
| box-shadow: 0 2px 8px rgba(99, 102, 241, 0.05); | |
| } | |
| .promo-hero-badge { | |
| display: inline-flex; | |
| align-items: center; | |
| gap: 6px; | |
| background: #ffffff; | |
| border: 1px solid #c7d2fe; | |
| color: #4338ca; | |
| font-size: 12px; | |
| font-weight: 700; | |
| text-transform: uppercase; | |
| letter-spacing: 0.05em; | |
| padding: 4px 12px; | |
| border-radius: 9999px; | |
| margin-bottom: 12px; | |
| } | |
| .promo-hero-title { | |
| font-size: 30px !important; | |
| font-weight: 800 !important; | |
| line-height: 1.25 !important; | |
| color: #1e1b4b !important; | |
| margin: 0 0 10px 0 !important; | |
| } | |
| .promo-hero-desc { | |
| font-size: 16px; | |
| line-height: 1.6; | |
| color: #1f2937; | |
| max-width: 820px; | |
| margin-bottom: 0; | |
| } | |
| .promo-grid { | |
| display: grid; | |
| grid-template-columns: repeat(auto-fit, minmax(290px, 1fr)); | |
| gap: 18px; | |
| margin-bottom: 28px; | |
| } | |
| .promo-card { | |
| display: flex !important; | |
| flex-direction: column !important; | |
| justify-content: space-between !important; | |
| text-decoration: none !important; | |
| color: inherit !important; | |
| border-radius: 10px; | |
| padding: 22px; | |
| box-shadow: 0 1px 3px rgba(0, 0, 0, 0.04); | |
| transition: background-color 0.18s ease, border-color 0.18s ease, box-shadow 0.18s ease; | |
| cursor: pointer; | |
| } | |
| .promo-card:hover { | |
| box-shadow: 0 2px 8px rgba(0, 0, 0, 0.06); | |
| } | |
| /* Individual card color accents */ | |
| .promo-card-blue { | |
| background: linear-gradient(180deg, #ffffff 0%, #f0f7ff 100%); | |
| border: 1px solid #bfdbfe; | |
| border-top: 4px solid #2563eb; | |
| } | |
| .promo-card-blue:hover { | |
| background: linear-gradient(180deg, #ffffff 0%, #e0effe 100%); | |
| border-color: #93c5fd; | |
| border-top-color: #1d4ed8; | |
| } | |
| .promo-card-amber { | |
| background: linear-gradient(180deg, #ffffff 0%, #fffbeb 100%); | |
| border: 1px solid #fde68a; | |
| border-top: 4px solid #d97706; | |
| } | |
| .promo-card-amber:hover { | |
| background: linear-gradient(180deg, #ffffff 0%, #fef3c7 100%); | |
| border-color: #fcd34d; | |
| border-top-color: #b45309; | |
| } | |
| .promo-card-purple { | |
| background: linear-gradient(180deg, #ffffff 0%, #faf5ff 100%); | |
| border: 1px solid #e9d5ff; | |
| border-top: 4px solid #7c3aed; | |
| } | |
| .promo-card-purple:hover { | |
| background: linear-gradient(180deg, #ffffff 0%, #f3e8ff 100%); | |
| border-color: #d8b4fe; | |
| border-top-color: #6d28d9; | |
| } | |
| .promo-card-teal { | |
| background: linear-gradient(180deg, #ffffff 0%, #f0fdfa 100%); | |
| border: 1px solid #99f6e4; | |
| border-top: 4px solid #0d9488; | |
| } | |
| .promo-card-teal:hover { | |
| background: linear-gradient(180deg, #ffffff 0%, #ccfbf1 100%); | |
| border-color: #5eead4; | |
| border-top-color: #0f766e; | |
| } | |
| .promo-card-rose { | |
| background: linear-gradient(180deg, #ffffff 0%, #fff1f2 100%); | |
| border: 1px solid #fecdd3; | |
| border-top: 4px solid #e11d48; | |
| } | |
| .promo-card-rose:hover { | |
| background: linear-gradient(180deg, #ffffff 0%, #ffe4e6 100%); | |
| border-color: #fda4af; | |
| border-top-color: #be123c; | |
| } | |
| .promo-card-emerald { | |
| background: linear-gradient(180deg, #ffffff 0%, #f0fdf4 100%); | |
| border: 1px solid #a7f3d0; | |
| border-top: 4px solid #059669; | |
| } | |
| .promo-card-emerald:hover { | |
| background: linear-gradient(180deg, #ffffff 0%, #d1fae5 100%); | |
| border-color: #6ee7b7; | |
| border-top-color: #047857; | |
| } | |
| .promo-tag { | |
| display: inline-block; | |
| font-size: 11px; | |
| font-weight: 700; | |
| letter-spacing: 0.06em; | |
| text-transform: uppercase; | |
| padding: 4px 10px; | |
| border-radius: 6px; | |
| margin-bottom: 12px; | |
| width: fit-content; | |
| } | |
| .pt-blue { background: #dbeafe; color: #1e40af; border: 1px solid #93c5fd; } | |
| .pt-amber { background: #fef3c7; color: #92400e; border: 1px solid #fcd34d; } | |
| .pt-purple { background: #ede9fe; color: #5b21b6; border: 1px solid #d8b4fe; } | |
| .pt-teal { background: #ccfbf1; color: #115e59; border: 1px solid #5eead4; } | |
| .pt-rose { background: #ffe4e6; color: #9f1239; border: 1px solid #fda4af; } | |
| .pt-emerald { background: #d1fae5; color: #065f46; border: 1px solid #6ee7b7; } | |
| .promo-title { | |
| font-size: 19px; | |
| font-weight: 700; | |
| margin: 0 0 6px 0; | |
| display: flex; | |
| align-items: center; | |
| gap: 8px; | |
| } | |
| .promo-card-blue .promo-title { color: #1e3a8a; } | |
| .promo-card-amber .promo-title { color: #78350f; } | |
| .promo-card-purple .promo-title { color: #4c1d95; } | |
| .promo-card-teal .promo-title { color: #134e4a; } | |
| .promo-card-rose .promo-title { color: #881337; } | |
| .promo-card-emerald .promo-title { color: #064e3b; } | |
| .promo-subtitle-hint { | |
| display: inline-block; | |
| font-size: 12px; | |
| font-weight: 600; | |
| color: #b45309; | |
| margin-bottom: 8px; | |
| } | |
| .promo-desc { | |
| font-size: 14.5px; | |
| line-height: 1.6; | |
| color: #111827; | |
| margin-bottom: 18px; | |
| flex-grow: 1; | |
| } | |
| .promo-btn { | |
| display: inline-flex; | |
| align-items: center; | |
| justify-content: center; | |
| gap: 6px; | |
| padding: 8px 16px; | |
| border-radius: 6px; | |
| font-size: 13px; | |
| font-weight: 600; | |
| color: #ffffff !important; | |
| text-decoration: none !important; | |
| transition: all 0.15s ease; | |
| width: fit-content; | |
| box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1); | |
| } | |
| .pb-blue { background: #2563eb; } | |
| .pb-blue:hover { background: #1d4ed8; } | |
| .pb-amber { background: #d97706; } | |
| .pb-amber:hover { background: #b45309; } | |
| .pb-purple { background: #7c3aed; } | |
| .pb-purple:hover { background: #6d28d9; } | |
| .pb-teal { background: #0d9488; } | |
| .pb-teal:hover { background: #0f766e; } | |
| .pb-rose { background: #e11d48; } | |
| .pb-rose:hover { background: #be123c; } | |
| .pb-emerald { background: #059669; } | |
| .pb-emerald:hover { background: #047857; } | |
| .promo-workflow-banner { | |
| background: #f8fafc; | |
| border: 1px solid #cbd5e1; | |
| border-radius: 10px; | |
| padding: 20px 24px; | |
| box-shadow: 0 1px 4px rgba(0, 0, 0, 0.04); | |
| text-align: center; | |
| } | |
| .workflow-steps { | |
| display: flex; | |
| flex-wrap: wrap; | |
| align-items: center; | |
| justify-content: center; | |
| gap: 10px 14px; | |
| margin-bottom: 12px; | |
| } | |
| .workflow-step { | |
| background: #ffffff; | |
| border: 1px solid #cbd5e1; | |
| border-radius: 6px; | |
| padding: 6px 14px; | |
| font-size: 13.5px; | |
| font-weight: 700; | |
| color: #1e3a8a; | |
| box-shadow: 0 1px 2px rgba(0, 0, 0, 0.04); | |
| } | |
| .workflow-arrow { | |
| color: #64748b; | |
| font-weight: bold; | |
| font-size: 14px; | |
| } | |
| .workflow-note { | |
| font-size: 14px; | |
| color: #475569; | |
| font-style: italic; | |
| margin: 0; | |
| } | |
| /* Dark mode enhancements */ | |
| .dark .promo-hero { | |
| background: linear-gradient(135deg, #1e1b4b 0%, #0f172a 100%); | |
| border-color: #312e81; | |
| } | |
| .dark .promo-hero-badge { | |
| background: #1e1b4b; | |
| border-color: #4338ca; | |
| color: #c7d2fe; | |
| } | |
| .dark .promo-hero-title { | |
| color: #e0e7ff !important; | |
| } | |
| .dark .promo-hero-desc { | |
| color: #cbd5e1; | |
| } | |
| .dark .promo-card-blue { background: linear-gradient(180deg, #1e293b 0%, #0f172a 100%); border-color: #1e3a8a; } | |
| .dark .promo-card-blue:hover { background: linear-gradient(180deg, #24344d 0%, #131d2e 100%); border-color: #3b82f6; } | |
| .dark .promo-card-amber { background: linear-gradient(180deg, #1e293b 0%, #0f172a 100%); border-color: #78350f; } | |
| .dark .promo-card-amber:hover { background: linear-gradient(180deg, #2e261f 0%, #1a1612 100%); border-color: #f59e0b; } | |
| .dark .promo-card-purple { background: linear-gradient(180deg, #1e293b 0%, #0f172a 100%); border-color: #4c1d95; } | |
| .dark .promo-card-purple:hover { background: linear-gradient(180deg, #29203d 0%, #171224 100%); border-color: #a855f7; } | |
| .dark .promo-card-teal { background: linear-gradient(180deg, #1e293b 0%, #0f172a 100%); border-color: #134e4a; } | |
| .dark .promo-card-teal:hover { background: linear-gradient(180deg, #182f2f 0%, #0d1e1e 100%); border-color: #14b8a6; } | |
| .dark .promo-card-rose { background: linear-gradient(180deg, #1e293b 0%, #0f172a 100%); border-color: #881337; } | |
| .dark .promo-card-rose:hover { background: linear-gradient(180deg, #331922 0%, #1e0d13 100%); border-color: #f43f5e; } | |
| .dark .promo-card-emerald { background: linear-gradient(180deg, #1e293b 0%, #0f172a 100%); border-color: #064e3b; } | |
| .dark .promo-card-emerald:hover { background: linear-gradient(180deg, #162e24 0%, #0d1e17 100%); border-color: #10b981; } | |
| .dark .promo-title { color: #f8fafc !important; } | |
| .dark .promo-subtitle-hint { color: #fde68a !important; } | |
| .dark .promo-desc { color: #e2e8f0 !important; } | |
| .dark .promo-workflow-banner { | |
| background: #1e293b; | |
| border-color: #334155; | |
| } | |
| .dark .workflow-step { | |
| background: #0f172a; | |
| border-color: #334155; | |
| color: #93c5fd; | |
| } | |
| .dark .workflow-arrow { | |
| color: #94a3b8; | |
| } | |
| .dark .workflow-note { | |
| color: #cbd5e1; | |
| } | |
| """ | |
| OVERVIEW_MD = """ | |
| <div class="promo-hero"> | |
| <span class="promo-hero-badge">✨ Universal Dependencies & UDPipe</span> | |
| <h1 class="promo-hero-title">Universal Dependencies Parser & Visualizer</h1> | |
| <div class="promo-hero-desc"> | |
| <p style="margin: 0 0 10px 0;">Explore grammatical structure in English and German sentences using Universal Dependencies. This teaching tool combines neural dependency parsing, interactive tree visualizations, grammatical documentation, and treebank examples.</p> | |
| <p style="margin: 0;">Inspect the analyses, compare different interpretations, and evaluate the grammatical decisions made by automatic parsers.</p> | |
| </div> | |
| </div> | |
| <div class="promo-grid"> | |
| <a href="#tab-parse" class="promo-card promo-card-blue"> | |
| <div> | |
| <span class="promo-tag pt-blue">DEPENDENCY PARSING</span> | |
| <div class="promo-title">⚡ Parse English & German</div> | |
| <div class="promo-desc"> | |
| Enter a sentence or select an example. UDPipe analyzes the words and displays their part-of-speech tags, lemmas, morphological features, and dependency relations. Inspect the resulting tree to see how words are connected. | |
| </div> | |
| </div> | |
| <span class="promo-btn pb-blue">Open the Parse tab →</span> | |
| </a> | |
| <a href="#tab-parse" class="promo-card promo-card-amber"> | |
| <div> | |
| <span class="promo-tag pt-amber">INTERACTIVE EXPLORATION</span> | |
| <div class="promo-title">🎯 Explore Heads & Dependencies</div> | |
| <div class="promo-subtitle-hint">Try the "Head Hunt" interaction</div> | |
| <div class="promo-desc"> | |
| Hover over a word to highlight its syntactic head. Hover over a POS tag or dependency arc to focus on relevant grammatical relationships while the rest of the tree fades. Follow dependency chains to explore sentence structure. | |
| </div> | |
| </div> | |
| <span class="promo-btn pb-amber">Try in the Parse tab →</span> | |
| </a> | |
| <a href="#tab-parse-help" class="promo-card promo-card-purple"> | |
| <div> | |
| <span class="promo-tag pt-purple">GRAMMATICAL REFERENCE</span> | |
| <div class="promo-title">📖 Look Up UD Categories</div> | |
| <div class="promo-desc"> | |
| Look up unfamiliar part-of-speech tags, dependency relations, and morphological features. The contextual Help panel links categories in the analysis to their definitions and examples in the official Universal Dependencies documentation. | |
| </div> | |
| </div> | |
| <span class="promo-btn pb-purple">Open Help below the tree →</span> | |
| </a> | |
| <a href="#tab-parse" class="promo-card promo-card-teal"> | |
| <div> | |
| <span class="promo-tag pt-teal">CORPUS EXPLORATION</span> | |
| <div class="promo-title">🔍 Examine Treebank Examples</div> | |
| <div class="promo-desc"> | |
| Click a word in the dependency tree to search for examples in an annotated corpus using Grew. Examine how words and constructions are analyzed in a treebank, and compare those examples with the parser’s output. | |
| </div> | |
| </div> | |
| <span class="promo-btn pb-teal">Click a word to search in Grew →</span> | |
| </a> | |
| <a href="#tab-compare" class="promo-card promo-card-rose"> | |
| <div> | |
| <span class="promo-tag pt-rose">MODEL COMPARISON</span> | |
| <div class="promo-title">⚖️ Compare Parser Analyses</div> | |
| <div class="promo-desc"> | |
| Analyze the same sentence with two different treebank-based models. A side-by-side view highlights differences in part-of-speech tags and dependency attachments. Discuss whether disagreements reflect ambiguity, annotation conventions, or possible parser errors. | |
| </div> | |
| </div> | |
| <span class="promo-btn pb-rose">Open the Compare tab →</span> | |
| </a> | |
| <a href="#tab-export" class="promo-card promo-card-emerald"> | |
| <div> | |
| <span class="promo-tag pt-emerald">DATA & EXPORT</span> | |
| <div class="promo-title">💾 Export & Process Analyses</div> | |
| <div class="promo-desc"> | |
| Download dependency trees as SVG or PNG graphics, or access the underlying CoNLL-U representation. The batch interface and API support processing multiple sentences and using the analyses in other applications. | |
| </div> | |
| </div> | |
| <span class="promo-btn pb-emerald">Open Annotate & Export →</span> | |
| </a> | |
| </div> | |
| <div class="promo-workflow-banner"> | |
| <div class="workflow-steps"> | |
| <span class="workflow-step">Parse</span> | |
| <span class="workflow-arrow">→</span> | |
| <span class="workflow-step">Inspect</span> | |
| <span class="workflow-arrow">→</span> | |
| <span class="workflow-step">Consult UD</span> | |
| <span class="workflow-arrow">→</span> | |
| <span class="workflow-step">Explore examples</span> | |
| <span class="workflow-arrow">→</span> | |
| <span class="workflow-step">Compare</span> | |
| <span class="workflow-arrow">→</span> | |
| <span class="workflow-step">Export</span> | |
| </div> | |
| <p class="workflow-note">Remember: A dependency tree is an analysis of a sentence, not necessarily the only possible analysis.</p> | |
| </div> | |
| """ | |
| CONFORMANCE_DOC_MD = """ | |
| **Parser and model identification**: Parser names in this application show | |
| UDPipe and the model ID used in the raw CoNLL-U output, including the training | |
| treebank and UD release (for example, `UDPipe · english-ewt-ud-2.17-251125`). | |
| **Training size** is the number of syntactic words in the training split, not | |
| the size of the whole corpus. Smaller training sets provide fewer examples | |
| and can increase errors on uncommon constructions; size alone does not | |
| predict correctness on your sentence. | |
| **Quality and conformance** concern how consistently annotations follow the | |
| universal and language-specific UD guidelines. Automated validation checks | |
| format and many constraints, but cannot verify every linguistic decision. | |
| Manual review and conversion history are useful evidence, not a compliance | |
| score. These sources do not establish a comparable conformance ranking. | |
| In **Compare**, inspect disagreements against the sentence and the | |
| [UD syntax guidelines](https://universaldependencies.org/u/overview/syntax.html) | |
| ([German](https://universaldependencies.org/de/), | |
| [English](https://universaldependencies.org/en/)). | |
| The table describes annotation methods, rather than assigning quality labels. | |
| Sizes refer to **UD 2.17**, the training release of the app's configured models. | |
| Treebank names link to their documentation; training counts link to the | |
| release-specific statistics. | |
| | Treebank | Training words | Annotation quality / conformance evidence | Why included | | |
| | :--- | ---: | :--- | :--- | | |
| """ + "\n".join( | |
| f"| [{row['Language']}-{row['Treebank']}]({row['SourceURL']}) " | |
| f"| [{row['TrainingWords']}]({row['StatsURL']}) " | |
| f"| {row['Conformance']} | {row['BestFor']} |" | |
| for row in TREEBANK_CATALOG | |
| ) + """ | |
| [UD validation: what is checked](https://universaldependencies.org/contributing/validation.html) | |
| · [UDPipe model training and evaluation](https://ufal.mff.cuni.cz/udpipe/2/models) | |
| """ | |
| def run_single_or_compare( | |
| lang: str, | |
| sentence: str, | |
| primary_model_name: str, | |
| compare_mode: bool, | |
| secondary_model_name: str, | |
| include_morphology: bool = False, | |
| cache: Optional[BoundedParseCache] = None | |
| ): | |
| """ | |
| Executes parsing for one sentence. | |
| If compare_mode is active, queries both models and diffs POS tags + dependencies. | |
| """ | |
| sentence = sentence.strip() | |
| if not sentence: | |
| return ( | |
| "<div style='color: #ef4444;'>Please enter a sentence to parse.</div>", | |
| pd.DataFrame(), | |
| "", | |
| "", | |
| gr.update(visible=False), | |
| pd.DataFrame(), | |
| ) | |
| # Restrict strictly to one sentence (first line / sentence split) | |
| if "\n" in sentence: | |
| sentence = sentence.split("\n")[0].strip() | |
| models_dict = AVAILABLE_MODELS.get(lang, AVAILABLE_MODELS["English"]) | |
| model_id_a = models_dict.get(primary_model_name, list(models_dict.values())[0]) | |
| strip_morphology = not include_morphology | |
| res_a = client.parse_sentence(sentence, model_id_a, strip_morphology=strip_morphology, cache=cache) | |
| error_a = res_a.get("error") | |
| if not error_a and not extract_tree_signature(res_a.get("conllu", "")): | |
| error_a = "The primary model returned no usable parse tokens." | |
| if error_a: | |
| err_msg = f"<div style='color: #ef4444;'>Error: {html.escape(str(error_a))}</div>" | |
| return err_msg, pd.DataFrame(), "", "", gr.update(visible=False), pd.DataFrame() | |
| conllu_a = res_a["conllu"] | |
| table_a = conllu_to_table(conllu_a) | |
| if not compare_mode: | |
| # Single Tree Mode | |
| vis_html = render_conllu_visualizer_html(conllu_a, vis_id="vis-primary", title=primary_model_name, show_help=True, model_id=model_id_a) | |
| status_html = "" | |
| return ( | |
| vis_html, | |
| table_a, | |
| conllu_a, | |
| status_html, | |
| gr.update(visible=False), | |
| pd.DataFrame(), | |
| ) | |
| elif primary_model_name == secondary_model_name: | |
| # Same model selected twice in comparison mode | |
| vis_html = render_conllu_visualizer_html(conllu_a, vis_id="vis-primary", title=primary_model_name, model_id=model_id_a) | |
| m_name_esc = html.escape(primary_model_name) | |
| status_html = ( | |
| f"<div class='diff-alert diff-alert-same'>✓ <strong>Same UDPipe Parser Selected:</strong> " | |
| f"Both parsers are set to <strong>{m_name_esc}</strong> ({len(table_a)} tokens). " | |
| f"Select a different second parser to inspect differences.</div>" | |
| ) | |
| return ( | |
| vis_html, | |
| table_a, | |
| conllu_a, | |
| status_html, | |
| gr.update(visible=False), | |
| pd.DataFrame(), | |
| ) | |
| else: | |
| # Comparison Mode: Query Secondary Model | |
| model_id_b = models_dict.get(secondary_model_name, list(models_dict.values())[0]) | |
| res_b = client.parse_sentence(sentence, model_id_b, strip_morphology=strip_morphology, cache=cache) | |
| conllu_b = res_b.get("conllu", "") | |
| error_b = res_b.get("error") | |
| if not error_b and not extract_tree_signature(conllu_b): | |
| error_b = "The secondary model returned no usable parse tokens." | |
| if error_b: | |
| vis_html = render_conllu_visualizer_html( | |
| conllu_a, vis_id="vis-primary", title=primary_model_name, model_id=model_id_a | |
| ) | |
| status_html = ( | |
| "<div class='diff-alert diff-alert-diff'><strong>Comparison unavailable:</strong> " | |
| f"{html.escape(secondary_model_name)}: {html.escape(str(error_b))}</div>" | |
| ) | |
| return vis_html, table_a, conllu_a, status_html, gr.update(visible=False), pd.DataFrame() | |
| # Diff trees | |
| diff_info = compare_trees(conllu_a, primary_model_name, conllu_b, secondary_model_name) | |
| vis_html = render_side_by_side_visualizers_html( | |
| conllu_a, primary_model_name, | |
| conllu_b, secondary_model_name, | |
| model_id_a=model_id_a, model_id_b=model_id_b | |
| ) | |
| raw_conllu_combined = f"# === {primary_model_name} ===\n{conllu_a}\n\n# === {secondary_model_name} ===\n{conllu_b}" | |
| if diff_info["identical"]: | |
| status_html = f"<div class='diff-alert diff-alert-same'>✓ <strong>Identical Trees:</strong> Both UDPipe parsers produced matching POS tags and syntactic dependency arcs.</div>" | |
| diff_visible = False | |
| else: | |
| diff_text = html.escape(diff_info["diff_summary"]) | |
| model_a_escaped = html.escape(primary_model_name) | |
| model_b_escaped = html.escape(secondary_model_name) | |
| diff_text = diff_text.replace(f"**{model_a_escaped}**", f"<strong>{model_a_escaped}</strong>").replace(f"**{model_b_escaped}**", f"<strong>{model_b_escaped}</strong>") | |
| status_html = f"<div class='diff-alert diff-alert-diff'>⚡ <strong>Discrepancies Found:</strong> {diff_text}</div>" | |
| diff_visible = True | |
| return ( | |
| vis_html, | |
| table_a, | |
| raw_conllu_combined, | |
| status_html, | |
| gr.update(visible=diff_visible), | |
| diff_info["diff_table"], | |
| ) | |
| def _describe_example(example): | |
| description = example["phenomenon"] | |
| if example.get("source_url"): | |
| description += ( | |
| f"\n\nSource: [{example['source_corpus']} {example['source_version']}, " | |
| f"sentence {example['sent_id']}]({example['source_url']}) " | |
| f"· [{example['source_license']}](https://creativecommons.org/licenses/by-sa/3.0/)" | |
| ) | |
| return description | |
| def _get_example_choices(lang): | |
| return [(ex["text"], i) for i, ex in enumerate(CURATED_EXAMPLES) if ex["language"] == lang] | |
| def _get_first_example_index_for_lang(lang: str) -> int: | |
| for i, ex in enumerate(CURATED_EXAMPLES): | |
| if ex["language"] == lang: | |
| return i | |
| return 0 | |
| def resolve_model_label(lang: str, query: str) -> Optional[str]: | |
| """Resolves a model query (ID, label, or substring) to a display label.""" | |
| if not query: | |
| return None | |
| models = list(AVAILABLE_MODELS.get(lang, {}).keys()) | |
| for m in models: | |
| if m.lower() == query.lower(): | |
| return m | |
| for m in models: | |
| mid = AVAILABLE_MODELS[lang][m] | |
| if mid.lower() == query.lower(): | |
| return m | |
| for m in models: | |
| mid = AVAILABLE_MODELS[lang][m] | |
| if query.lower() in mid.lower() or query.lower() in m.lower(): | |
| return m | |
| return None | |
| def build_parser(compare=False): | |
| """Build independent, sentence-first controls for each parsing mode.""" | |
| sentence = gr.Textbox(label="Sentence", value="They buy and sell books.", | |
| placeholder="Enter an English or German sentence...", | |
| lines=1, max_lines=1, elem_classes=["sentence-input"]) | |
| with gr.Row(): | |
| language = gr.Dropdown(label="Language", choices=["English", "German"], value="English", scale=1) | |
| primary = gr.Dropdown(label="UDPipe parser" if not compare else "First UDPipe parser", | |
| choices=list(AVAILABLE_MODELS["English"]), | |
| value=list(AVAILABLE_MODELS["English"])[0], scale=2) | |
| secondary = gr.Dropdown(label="Second UDPipe parser", choices=list(AVAILABLE_MODELS["English"]), | |
| value=list(AVAILABLE_MODELS["English"])[1], visible=compare, scale=2) | |
| with gr.Column(scale=1): | |
| with gr.Row(): | |
| submit = gr.Button("Compare" if compare else "Parse", variant="primary", scale=3) | |
| share_btn = gr.Button("🔗 Share", size="sm", scale=2, min_width=75) | |
| include_morphology = gr.Checkbox(label="Include morphology (FEATS)", value=False) | |
| with gr.Accordion("Examples", open=False): | |
| selected_example = gr.State(0) | |
| with gr.Row(): | |
| german_examples = gr.Dropdown(label="German Examples", | |
| choices=_get_example_choices("German"), | |
| value=None, scale=1, min_width=240, allow_custom_value=False) | |
| english_examples = gr.Dropdown(label="English Examples", | |
| choices=_get_example_choices("English"), | |
| value=0, scale=1, min_width=240, allow_custom_value=False) | |
| phenomenon = gr.Markdown(_describe_example(CURATED_EXAMPLES[0])) | |
| status = gr.HTML(visible=compare) | |
| visualization = gr.HTML() | |
| with gr.Group(visible=False) as differences: | |
| gr.Markdown("### Differences") | |
| diff_table = gr.DataFrame(interactive=False) | |
| with gr.Accordion("Annotations", open=False): | |
| table = gr.DataFrame(headers=["ID", "Form", "Lemma", "UPOS", "FEATS", "Head", "Deprel"], interactive=False) | |
| with gr.Accordion("CoNLL-U", open=False): | |
| raw = gr.Code(label="CoNLL-U", language="markdown", interactive=False) | |
| def on_language_input(new_lang): | |
| models = list(AVAILABLE_MODELS[new_lang]) | |
| return ( | |
| gr.update(choices=models, value=models[0]), | |
| gr.update(choices=models, value=models[min(1, len(models)-1)]), | |
| ) | |
| def on_sentence_change(): | |
| return ( | |
| None, | |
| gr.update(value=None), | |
| gr.update(value=None), | |
| "", | |
| ) | |
| def on_example_input(new_ex_idx, current_lang, current_primary, current_secondary, morph, cache): | |
| if new_ex_idx is None: | |
| return tuple(gr.update() for _ in range(14)) | |
| ex = CURATED_EXAMPLES[int(new_ex_idx)] | |
| ex_lang = ex["language"] | |
| if ex_lang != current_lang: | |
| models = list(AVAILABLE_MODELS[ex_lang]) | |
| primary_name = models[0] | |
| secondary_name = models[min(1, len(models)-1)] | |
| model_updates = ( | |
| gr.update(choices=models, value=primary_name), | |
| gr.update(choices=models, value=secondary_name), | |
| ) | |
| else: | |
| primary_name = current_primary | |
| secondary_name = current_secondary | |
| model_updates = (gr.update(), gr.update()) | |
| vis_html, table_df, raw_conllu, status_html, diff_group, diff_df = run_single_or_compare( | |
| lang=ex_lang, | |
| sentence=ex["text"], | |
| primary_model_name=primary_name, | |
| compare_mode=compare, | |
| secondary_model_name=secondary_name, | |
| include_morphology=morph, | |
| cache=cache, | |
| ) | |
| return ( | |
| ex_lang, | |
| *model_updates, | |
| _describe_example(ex), | |
| ex["text"], | |
| int(new_ex_idx), | |
| gr.update(value=int(new_ex_idx) if ex_lang == "German" else None), | |
| gr.update(value=int(new_ex_idx) if ex_lang == "English" else None), | |
| vis_html, | |
| table_df, | |
| raw_conllu, | |
| status_html, | |
| diff_group, | |
| diff_df, | |
| ) | |
| language.input( | |
| on_language_input, | |
| [language], | |
| [primary, secondary], | |
| api_name=False, | |
| ) | |
| sentence.input( | |
| on_sentence_change, | |
| None, | |
| [selected_example, german_examples, english_examples, phenomenon], | |
| api_name=False, | |
| ) | |
| # An instance, not the class: Gradio deep-copies it per session, and a | |
| # callable default would register a public load endpoint. | |
| # An instance, not the class: Gradio deep-copies State per session, and a | |
| # callable default would publish a load endpoint in the API. | |
| session_cache = gr.State(BoundedParseCache()) | |
| for example_dropdown in (german_examples, english_examples): | |
| example_dropdown.input( | |
| on_example_input, | |
| [example_dropdown, language, primary, secondary, include_morphology, session_cache], | |
| [language, primary, secondary, phenomenon, sentence, selected_example, | |
| german_examples, english_examples, visualization, table, raw, status, differences, diff_table], | |
| api_name=False, | |
| ) | |
| inputs = [language, sentence, primary, gr.State(compare), secondary, include_morphology, session_cache] | |
| outputs = [visualization, table, raw, status, differences, diff_table] | |
| submit.click(run_single_or_compare, inputs, outputs, api_name=False) | |
| sentence.submit(run_single_or_compare, inputs, outputs, api_name=False) | |
| share_btn.click( | |
| fn=None, | |
| inputs=[], | |
| outputs=[], | |
| js="""() => { | |
| if (window.updateShareUrl) window.updateShareUrl(); | |
| const url = window.location.href; | |
| navigator.clipboard.writeText(url).then(() => { | |
| const btn = document.activeElement; | |
| if (btn) { | |
| const oldText = btn.textContent; | |
| btn.textContent = '✓ Copied!'; | |
| setTimeout(() => { btn.textContent = oldText; }, 2000); | |
| } | |
| }).catch(() => { | |
| prompt('Copy shareable link:', url); | |
| }); | |
| return []; | |
| }""", | |
| api_name=False, | |
| ) | |
| return { | |
| "sentence": sentence, | |
| "language": language, | |
| "primary": primary, | |
| "secondary": secondary, | |
| "include_morphology": include_morphology, | |
| "session_cache": session_cache, | |
| "visualization": visualization, | |
| "table": table, | |
| "raw": raw, | |
| "status": status, | |
| "differences": differences, | |
| "diff_table": diff_table, | |
| } | |
| EXPORT_FORMATS = ["CoNLL-U", "SVG", "PNG"] | |
| API_MD = """ | |
| Each button below is also an API endpoint. From Python: | |
| ```python | |
| from gradio_client import Client | |
| client = Client("simon-clmtd/universal-dependencies-visualizer") | |
| conllu, files = client.predict( | |
| "Bestimmt hat sie die Lösung bestimmt.", "german-hdt-ud-2.17-251125", | |
| False, ["CoNLL-U", "SVG", "PNG"], api_name="/annotate") | |
| files = client.predict(conllu, ["SVG", "PNG"], api_name="/render") | |
| client.predict(api_name="/models") | |
| ``` | |
| - `/annotate(text, model, include_morphology, formats)`: parses each non-empty line | |
| (at most 50) and returns the CoNLL-U text and the requested files. | |
| - `/render(conllu, formats)`: draws your own or corrected CoNLL-U as SVG and PNG, | |
| exactly like the tree download buttons. | |
| - `/models()`: lists the model IDs. | |
| Images take a few seconds per request because a browser draws each tree. | |
| """ | |
| def _write_exports(conllu: str, formats: List[str], stem: str = "ud-analysis") -> List[str]: | |
| """Writes the requested CoNLL-U/SVG/PNG files to a fresh temporary directory.""" | |
| folder = tempfile.mkdtemp(prefix="ud-export-") | |
| paths = [] | |
| if "CoNLL-U" in formats: | |
| path = os.path.join(folder, stem + ".conllu") | |
| with open(path, "w", encoding="utf-8") as handle: | |
| handle.write(conllu) | |
| paths.append(path) | |
| image_formats = [name for name in ("SVG", "PNG") if name in formats] | |
| if image_formats: | |
| try: | |
| images = export_tree_images(conllu) | |
| except TreeExportError as error: | |
| raise gr.Error(str(error)) | |
| for name in image_formats: | |
| path = os.path.join(folder, f"{stem}.{name.lower()}") | |
| with open(path, "wb") as handle: | |
| handle.write(images[name.lower()]) | |
| paths.append(path) | |
| return paths | |
| def annotate_and_export(text: str, model: str, include_morphology: bool, formats: List[str]): | |
| """Parses each line of text with one UDPipe model and exports the analysis.""" | |
| try: | |
| result = annotate_text(text, model, include_morphology=include_morphology, client=client) | |
| except (ValueError, RuntimeError) as error: | |
| raise gr.Error(str(error)) | |
| return result["conllu"], _write_exports(result["conllu"], formats or []) | |
| def render_conllu(conllu: str, formats: List[str]): | |
| """Draws supplied CoNLL-U as SVG/PNG (and returns it as a file if requested).""" | |
| if not conllu or not conllu.strip(): | |
| raise gr.Error("Paste CoNLL-U to render.") | |
| return _write_exports(conllu if conllu.endswith("\n") else conllu + "\n", formats or []) | |
| def list_models() -> List[str]: | |
| """Returns the selectable UDPipe model IDs.""" | |
| return model_ids() | |
| def build_exporter(): | |
| """Batch annotation and file export; its events are the public API.""" | |
| gr.Markdown( | |
| "Annotate one sentence per line, or paste CoNLL-U (for example a corrected " | |
| "analysis) and render it. Download the CoNLL-U, SVG, and PNG files below." | |
| ) | |
| with gr.Row(): | |
| with gr.Column(): | |
| text = gr.Textbox(label="Sentences (one per line)", lines=6, | |
| value="Bestimmt hat sie die Lösung bestimmt.") | |
| with gr.Row(): | |
| model = gr.Dropdown(label="UDPipe model", choices=model_ids(), | |
| value="german-hdt-ud-2.17-251125", scale=2) | |
| include_morphology = gr.Checkbox(label="Include morphology (FEATS)", value=False, scale=1) | |
| formats = gr.CheckboxGroup(EXPORT_FORMATS, value=EXPORT_FORMATS, label="Files") | |
| annotate = gr.Button("Annotate", variant="primary") | |
| with gr.Column(): | |
| conllu = gr.Code(label="CoNLL-U (editable)", language="markdown", interactive=True, lines=12) | |
| render_formats = gr.CheckboxGroup(EXPORT_FORMATS, value=["SVG", "PNG"], label="Files") | |
| render = gr.Button("Render CoNLL-U") | |
| files = gr.File(label="Downloads", file_count="multiple", interactive=False) | |
| with gr.Accordion("Use as an API", open=False): | |
| gr.Markdown(API_MD) | |
| annotate.click(annotate_and_export, [text, model, include_morphology, formats], [conllu, files], | |
| api_name="annotate", concurrency_limit=2) | |
| render.click(render_conllu, [conllu, render_formats], [files], | |
| api_name="render", concurrency_limit=2) | |
| gr.api(list_models, api_name="models") | |
| _blocks_init_params = inspect.signature(gr.Blocks.__init__).parameters | |
| _blocks_kwargs = {"title": "Universal Dependencies Visualizer"} | |
| _launch_kwargs = {} | |
| if "css" in _blocks_init_params: | |
| _blocks_kwargs.update(theme=gr.themes.Base(), css=CUSTOM_CSS, head=CUSTOM_HEAD) | |
| else: | |
| _launch_kwargs.update(theme=gr.themes.Base(), css=CUSTOM_CSS, head=CUSTOM_HEAD) | |
| with gr.Blocks(**_blocks_kwargs) as demo: | |
| if ENABLE_SCREENCAST and ScreenRecorder is not None: | |
| with gr.Accordion("Record screencast (experimental)", open=False, elem_id="screencast-panel"): | |
| gr.Markdown( | |
| "Record your exploration with microphone narration. Choose this browser " | |
| "tab or a window in the screen-sharing dialog, then explore Parse or Compare. " | |
| "Stop recording, then click Download WebM. The video is saved directly " | |
| "from your browser." | |
| ) | |
| screen_recorder = ScreenRecorder( | |
| label="Screencast", audio_enabled=True, webcam_overlay=False, | |
| recording_format="webm", max_duration=600, | |
| ) | |
| download_recording = gr.Button("Download WebM", size="sm") | |
| # 0.0.1 does not bridge completion events to Gradio or upload FileData. | |
| # Save its browser-local preview instead of relying on that broken path. | |
| download_recording.click( | |
| fn=None, inputs=[], outputs=[], api_name=False, js="""() => { | |
| const video = Array.from(document.querySelectorAll('#screencast-panel .recording-preview video')) | |
| .find(video => video.src.startsWith('blob:')); | |
| if (!video) { | |
| alert('Stop a recording before downloading it.'); | |
| return []; | |
| } | |
| const link = document.createElement('a'); | |
| link.href = video.src; | |
| link.download = 'ud-exploration.webm'; | |
| document.body.appendChild(link); | |
| link.click(); | |
| link.remove(); | |
| return []; | |
| }""" | |
| ) | |
| with gr.Tabs(selected="tab_overview") as tabs: | |
| with gr.Tab("About", id="tab_overview"): | |
| gr.HTML(OVERVIEW_MD) | |
| with gr.Tab("Parse", id="tab_parser"): | |
| parse_ctrls = build_parser() | |
| with gr.Tab("Compare", id="tab_compare"): | |
| compare_ctrls = build_parser(compare=True) | |
| with gr.Tab("Annotate & Export", id="tab_export"): | |
| build_exporter() | |
| with gr.Tab("Treebank Guide", id="tab_guide"): | |
| with gr.Accordion("Training size, quality and conformance", open=False): | |
| # Remove duplicate heading since accordion provides the label | |
| doc_lines = CONFORMANCE_DOC_MD.strip().splitlines() | |
| if doc_lines and doc_lines[0].startswith("### "): | |
| conformance_content = "\n".join(doc_lines[1:]).strip() | |
| else: | |
| conformance_content = CONFORMANCE_DOC_MD | |
| gr.Markdown(conformance_content) | |
| with gr.Accordion("Explore corpora with Grew", open=False): | |
| gr.Markdown( | |
| "Go beyond a single parse: search annotated corpus sentences, " | |
| "look for recurring patterns, and find counterexamples to your expectations. " | |
| "Use Grew's built-in dependency-relation search examples to get started. " | |
| "Inspect several matches and explain what their trees have in common.\n\n" | |
| "| Treebank on Grew | Try exploring |\n" | |
| "| --- | --- |\n" | |
| + "\n".join( | |
| f"| [{row['Language']}-{row['Treebank']}]({row['GrewURL']}) " | |
| f"| {row['ExplorePrompt']} |" | |
| for row in TREEBANK_CATALOG | |
| ) | |
| ) | |
| with gr.Accordion("Full treebank catalog", open=False): | |
| catalog_rows = [ | |
| [ | |
| row["Language"], | |
| row["Treebank"], | |
| row["Words"], | |
| row["TrainingWords"], | |
| row["Sentences"], | |
| row["Genres"], | |
| row["BestFor"], | |
| ] | |
| for row in TREEBANK_CATALOG | |
| ] | |
| catalog_df = gr.DataFrame( | |
| value=catalog_rows, | |
| headers=["Language", "Treebank", "Total words (UD 2.17)", "Training words", "Sentences", "Text sources", "Role in this app"], | |
| interactive=False, | |
| ) | |
| def load_from_request(request: gr.Request): | |
| params = request.query_params if request else {} | |
| if not params: | |
| return tuple(gr.update() for _ in range(22)) | |
| tab_param = params.get("tab", "").strip().lower() | |
| sentence_param = params.get("sentence", "").strip() | |
| model_param = params.get("model", "").strip() | |
| model2_param = params.get("model2", "").strip() | |
| lang_param = params.get("lang", "").strip() | |
| morph = params.get("morph", "").strip().lower() in {"1", "true", "yes"} | |
| if tab_param in {"compare", "tab_compare"} or model2_param: | |
| target_tab = "tab_compare" | |
| elif tab_param in {"parse", "tab_parser"} or sentence_param: | |
| target_tab = "tab_parser" | |
| elif tab_param in {"export", "annotate", "tab_export"}: | |
| target_tab = "tab_export" | |
| elif tab_param in {"guide", "treebank", "tab_guide"}: | |
| target_tab = "tab_guide" | |
| elif tab_param in {"about", "overview", "tab_overview"}: | |
| target_tab = "tab_overview" | |
| else: | |
| target_tab = "tab_overview" | |
| if lang_param and lang_param.capitalize() in AVAILABLE_MODELS: | |
| lang = lang_param.capitalize() | |
| elif "german" in model_param.lower() or "german" in model2_param.lower(): | |
| lang = "German" | |
| else: | |
| lang = "English" | |
| models_list = list(AVAILABLE_MODELS[lang].keys()) | |
| p_model = resolve_model_label(lang, model_param) or models_list[0] | |
| p_model2 = resolve_model_label(lang, model2_param) or (models_list[1] if len(models_list) > 1 else models_list[0]) | |
| p_sent_up = sentence_param if sentence_param else gr.update() | |
| p_lang_up = gr.update(choices=["English", "German"], value=lang) | |
| p_model_up = gr.update(choices=models_list, value=p_model) | |
| p_morph_up = gr.update(value=morph) | |
| p_vis = gr.update() | |
| p_table = gr.update() | |
| p_raw = gr.update() | |
| p_status = gr.update() | |
| p_diff_group = gr.update() | |
| p_diff_table = gr.update() | |
| if target_tab == "tab_parser" and sentence_param: | |
| v, t, r, s, dg, dt = run_single_or_compare( | |
| lang=lang, | |
| sentence=sentence_param, | |
| primary_model_name=p_model, | |
| compare_mode=False, | |
| secondary_model_name="", | |
| include_morphology=morph, | |
| cache=parse_ctrls["session_cache"].value, | |
| ) | |
| p_vis, p_table, p_raw, p_status, p_diff_group, p_diff_table = v, t, r, s, dg, dt | |
| c_sent_up = sentence_param if sentence_param else gr.update() | |
| c_lang_up = gr.update(choices=["English", "German"], value=lang) | |
| c_model1_up = gr.update(choices=models_list, value=p_model) | |
| c_model2_up = gr.update(choices=models_list, value=p_model2) | |
| c_morph_up = gr.update(value=morph) | |
| c_vis = gr.update() | |
| c_table = gr.update() | |
| c_raw = gr.update() | |
| c_status = gr.update() | |
| c_diff_group = gr.update() | |
| c_diff_table = gr.update() | |
| if target_tab == "tab_compare" and sentence_param: | |
| v, t, r, s, dg, dt = run_single_or_compare( | |
| lang=lang, | |
| sentence=sentence_param, | |
| primary_model_name=p_model, | |
| compare_mode=True, | |
| secondary_model_name=p_model2, | |
| include_morphology=morph, | |
| cache=compare_ctrls["session_cache"].value, | |
| ) | |
| c_vis, c_table, c_raw, c_status, c_diff_group, c_diff_table = v, t, r, s, dg, dt | |
| return ( | |
| gr.Tabs(selected=target_tab), | |
| p_sent_up, p_lang_up, p_model_up, p_morph_up, | |
| p_vis, p_table, p_raw, p_status, p_diff_group, p_diff_table, | |
| c_sent_up, c_lang_up, c_model1_up, c_model2_up, c_morph_up, | |
| c_vis, c_table, c_raw, c_status, c_diff_group, c_diff_table, | |
| ) | |
| load_outputs = [ | |
| tabs, | |
| parse_ctrls["sentence"], parse_ctrls["language"], parse_ctrls["primary"], parse_ctrls["include_morphology"], | |
| parse_ctrls["visualization"], parse_ctrls["table"], parse_ctrls["raw"], parse_ctrls["status"], | |
| parse_ctrls["differences"], parse_ctrls["diff_table"], | |
| compare_ctrls["sentence"], compare_ctrls["language"], compare_ctrls["primary"], compare_ctrls["secondary"], | |
| compare_ctrls["include_morphology"], | |
| compare_ctrls["visualization"], compare_ctrls["table"], compare_ctrls["raw"], compare_ctrls["status"], | |
| compare_ctrls["differences"], compare_ctrls["diff_table"], | |
| ] | |
| demo.load(load_from_request, inputs=None, outputs=load_outputs, api_name=False) | |
| if __name__ == "__main__": | |
| demo.launch(**_launch_kwargs) | |