Simon Clematide
Add Annotate & Export tab and public API endpoints (/annotate, /render, /models)
ad9385f
Raw History Blame Contribute Delete
2.94 kB
"""
Server-side export of rendered dependency trees.
The trees are drawn by conllu.js and brat in a browser. To return the same
SVG/PNG files as the UI download buttons, this module loads the visualizer
document in headless Chromium (Playwright) and calls its export functions.
"""
import base64
import subprocess
import sys
import threading
from typing import Dict
from visualizer.template import build_visualizer_document
RENDER_TIMEOUT_MS = 45000
_browser_lock = threading.Lock()
_browser_installed = False
class TreeExportError(RuntimeError):
pass
def _ensure_chromium() -> None:
"""Installs Playwright's Chromium once per process if it is missing."""
global _browser_installed
if _browser_installed:
return
subprocess.run(
[sys.executable, "-m", "playwright", "install", "chromium"],
check=True, capture_output=True,
)
_browser_installed = True
def export_tree_images(conllu_text: str, title: str = "", png_scale: int = 2) -> Dict[str, bytes]:
"""
Renders CoNLL-U in headless Chromium and returns {"svg": bytes, "png": bytes}.
Calls are serialized: Playwright's sync API is not shared across threads.
"""
if not conllu_text or not conllu_text.strip():
raise TreeExportError("Empty CoNLL-U input.")
from playwright.sync_api import sync_playwright, Error as PlaywrightError
document = build_visualizer_document(conllu_text, title=title)
with _browser_lock:
try:
_ensure_chromium()
with sync_playwright() as playwright:
browser = playwright.chromium.launch()
try:
page = browser.new_page(viewport={"width": 1600, "height": 900})
page.set_content(document, wait_until="load")
page.wait_for_function(
"() => ['ready', 'error'].includes(window.visualizerDiagnostics?.stage)",
timeout=RENDER_TIMEOUT_MS,
)
diagnostics = page.evaluate(
"() => ({stage: visualizerDiagnostics.stage, error: visualizerDiagnostics.error || ''})"
)
if diagnostics["stage"] != "ready":
raise TreeExportError(diagnostics["error"] or "Visualization failed.")
svg = page.evaluate("() => getPreparedSVG().xml")
png_url = page.evaluate("scale => exportPNGDataURL(scale)", png_scale)
finally:
browser.close()
except PlaywrightError as error:
raise TreeExportError(f"Headless rendering failed: {error}") from error
except subprocess.CalledProcessError as error:
raise TreeExportError("Could not install the headless browser.") from error
return {
"svg": svg.encode("utf-8"),
"png": base64.b64decode(png_url.split(",", 1)[1]),
}