"""Display-only projections. Raw model output is never rewritten.""" import base64 import html import io import json import re import bleach from bs4 import BeautifulSoup from markdown_it import MarkdownIt from PIL import ImageDraw, ImageFont from catalog import ROOT from presentation import TASK_VIEWS, JSON_TASKS from structured_json import strict_json_loads COLORS = ["#0d9488", "#e05a47", "#7659b4", "#b88712", "#2876ba"] RESULT_CSS = (ROOT / "assets/result.css").read_text().replace("__ASSETS__", str(ROOT / "assets")) CROP = re.compile(r"(?:page_(\d+)/)?images/bbox_(\d+)_(\d+)_(\d+)_(\d+)\.jpg") TAGS = {"p", "div", "span", "br", "hr", "h1", "h2", "h3", "h4", "h5", "h6", "b", "strong", "i", "em", "u", "s", "sub", "sup", "ul", "ol", "li", "blockquote", "pre", "code", "table", "thead", "tbody", "tfoot", "tr", "th", "td", "img"} def parse_json(raw): text = raw.strip() fence = re.fullmatch(r"```(?:json)?\s*\n(.*?)\n```", text, re.S | re.I) return strict_json_loads(fence[1] if fence else text) def output_structure_error(task, data): arrays = {"document_structured", "document_layout", "figure_captioning", "word_spotting", "reference_grounding", "crosspage_table"} expected = (dict, list) if task == "kie_schema" else (list,) if task in arrays else (dict,) if not isinstance(data, expected): name = "an object or array" if len(expected) == 2 else "an array" if list in expected else "an object" return f"Expected a JSON {name.removeprefix('an ')} for this task." if task in arrays and not all(isinstance(item, dict) for item in data): return "Expected a JSON array of objects for this task." return None def bbox(value): if isinstance(value, str): match = re.fullmatch(r"\s*[\[(]\s*(\d+)\s*,\s*(\d+)\s*,\s*(\d+)\s*,\s*(\d+)\s*[\])]\s*", value) if not match: raise ValueError("Invalid bounding box string.") value = list(map(int, match.groups())) if not isinstance(value, (list, tuple)) or len(value) != 4 or not all(type(v) is int for v in value): raise ValueError("Invalid bounding box.") x1, y1, x2, y2 = value if not (0 <= x1 < x2 <= 1000 and 0 <= y1 < y2 <= 1000): raise ValueError("Bounding box is out of range or empty.") return value def crop_image(image, value): x1, y1, x2, y2 = bbox(value) w, h = image.size return image.crop((round(x1*w/1000), round(y1*h/1000), round(x2*w/1000), round(y2*h/1000))) def image_uri(image): output = io.BytesIO() image.convert("RGB").save(output, format="JPEG", quality=88) return "data:image/jpeg;base64," + base64.b64encode(output.getvalue()).decode("ascii") def boxed_answer_rule(state, silent): if not state.src.startswith(r"\boxed", state.pos): return False opening = state.pos + len(r"\boxed") while opening < state.posMax and state.src[opening].isspace(): opening += 1 if opening >= state.posMax or state.src[opening] != "{": return False depth, end = 1, opening + 1 # Balance nested TeX groups without consuming escaped braces or code spans. while end < state.posMax: char = state.src[end] if char == "\\": end += 2 continue if char == "{": depth += 1 elif char == "}": depth -= 1 if depth == 0: if not silent: token = state.push("boxed_answer", "", 0) token.content = state.src[opening + 1:end] state.pos = end + 1 return True end += 1 return False def boxed_answer_html(tokens, index, options, env): content = tokens[index].content # Plain-language answers retain spaces and upright text; TeX remains TeX. if re.search(r"[\\{}_^]", content): content = r"\(" + content + r"\)" return '' + html.escape(content) + '' def markdown_html(text, *, boxed_answers=False): # Protect math before Markdown consumes backslashes and underscores. expressions = [] def protect(match): expressions.append(match[0]) return f"OVISMATHPLACEHOLDER{len(expressions)-1}END" text = re.sub(r"\$\$[\s\S]+?\$\$|\\\[[\s\S]+?\\\]|\\\([\s\S]+?\\\)|(?
''' + safe + "" document = document.replace("", "") if not math: document = re.sub(r"