Spaces:
Running on Zero
Running on Zero
| from __future__ import annotations | |
| from io import BytesIO | |
| import math | |
| import re | |
| from typing import Any | |
| from PIL import Image, ImageDraw, ImageFont | |
| CANVAS_WIDTH = 1200 | |
| CANVAS_HEIGHT = 675 | |
| INK = (37, 22, 14) | |
| INK_SOFT = (107, 78, 53) | |
| PARCHMENT_LIGHT = (234, 215, 167) | |
| PARCHMENT_MID = (212, 180, 118) | |
| PARCHMENT_DARK = (185, 138, 76) | |
| OXBLOOD = (141, 45, 38) | |
| LEAF = (47, 122, 73) | |
| GOLD = (182, 138, 18) | |
| CREAM = (255, 240, 181) | |
| def artifact_png_filename(artifact: dict[str, Any]) -> str: | |
| slug = re.sub(r"[^a-z0-9]+", "-", str(artifact.get("title") or "unwritten-page").lower()) | |
| slug = slug.strip("-")[:60] or "unwritten-page" | |
| return f"{slug}.png" | |
| def render_artifact_png(artifact: dict[str, Any]) -> bytes: | |
| image = Image.new("RGB", (CANVAS_WIDTH, CANVAS_HEIGHT), PARCHMENT_LIGHT) | |
| draw = ImageDraw.Draw(image) | |
| _draw_parchment(draw) | |
| seal = artifact.get("seal") if isinstance(artifact.get("seal"), dict) else {} | |
| verdict = str(artifact.get("verdict") or seal.get("verdict") or "UNWRITTEN") | |
| overall = _number(artifact.get("overall") or seal.get("overall"), default=0) | |
| title_font = _font(58) | |
| body_font = _font(28) | |
| label_font = _font(20) | |
| small_font = _font(15) | |
| stamp_font = _font(27) | |
| score_font = _font(58) | |
| map_label_font = _font(18) | |
| _wrap_text( | |
| draw, | |
| str(artifact.get("title") or "Unwritten Page"), | |
| (78, 94), | |
| 760, | |
| 66, | |
| title_font, | |
| INK, | |
| ) | |
| _wrap_text(draw, str(artifact.get("caption") or ""), (82, 235), 720, 36, body_font, INK_SOFT) | |
| echoes = seal.get("echoes") if isinstance(seal.get("echoes"), list) else [] | |
| _draw_citations(draw, echoes, (742, 335), 330, small_font) | |
| _draw_stamp(draw, (930, 205), verdict, overall, stamp_font, score_font) | |
| _draw_score_bars(draw, seal, verdict, (82, 418), label_font) | |
| wood_map = artifact.get("wood_map") if isinstance(artifact.get("wood_map"), dict) else {} | |
| _draw_wood_map(draw, wood_map, (742, 465), (330, 105), verdict, map_label_font, small_font) | |
| buffer = BytesIO() | |
| image.save(buffer, format="PNG", optimize=True) | |
| return buffer.getvalue() | |
| def _draw_parchment(draw: ImageDraw.ImageDraw) -> None: | |
| for y in range(CANVAS_HEIGHT): | |
| t = y / max(1, CANVAS_HEIGHT - 1) | |
| left = _mix(PARCHMENT_LIGHT, PARCHMENT_MID, min(1, t * 1.6)) | |
| right = _mix(PARCHMENT_MID, PARCHMENT_DARK, t) | |
| for x in range(CANVAS_WIDTH): | |
| u = x / max(1, CANVAS_WIDTH - 1) | |
| draw.point((x, y), fill=_mix(left, right, u)) | |
| for i in range(360): | |
| x = (i * 73) % CANVAS_WIDTH | |
| y = (i * 37) % CANVAS_HEIGHT | |
| color = (59, 33, 15) | |
| draw.rectangle((x, y, x + 2 + (i % 7), y), fill=_blend(color, 0.16, PARCHMENT_MID)) | |
| draw.rectangle((28, 28, CANVAS_WIDTH - 28, CANVAS_HEIGHT - 28), outline=(72, 39, 18), width=16) | |
| def _draw_stamp( | |
| draw: ImageDraw.ImageDraw, | |
| center: tuple[int, int], | |
| verdict: str, | |
| overall: float, | |
| stamp_font: ImageFont.ImageFont, | |
| score_font: ImageFont.ImageFont, | |
| ) -> None: | |
| color = GOLD if verdict.startswith("UNWRITTEN") else OXBLOOD | |
| x, y = center | |
| radius = 104 | |
| draw.ellipse((x - radius, y - radius, x + radius, y + radius), fill=color) | |
| _wrap_text(draw, verdict, (x - 86, y - 30), 172, 32, stamp_font, CREAM, align="center") | |
| text = f"{overall:.1f}" | |
| box = draw.textbbox((0, 0), text, font=score_font) | |
| draw.text((x - (box[2] - box[0]) / 2, y + 4), text, font=score_font, fill=CREAM) | |
| def _draw_score_bars( | |
| draw: ImageDraw.ImageDraw, | |
| seal: dict[str, Any], | |
| verdict: str, | |
| origin: tuple[int, int], | |
| font: ImageFont.ImageFont, | |
| ) -> None: | |
| rows = [ | |
| ("Originality", seal.get("originality")), | |
| ("Delight", seal.get("delight")), | |
| ("AI Need", seal.get("ai_necessity")), | |
| ("Feasible", seal.get("feasibility")), | |
| ("Goal Fit", seal.get("goal_fit")), | |
| ] | |
| fill = LEAF if verdict.startswith("UNWRITTEN") else OXBLOOD | |
| x, y = origin | |
| for index, (label, value) in enumerate(rows): | |
| row_y = y + index * 34 | |
| score = _number(value, default=0) | |
| draw.text((x, row_y - 18), label, font=font, fill=INK_SOFT) | |
| draw.rectangle((240, row_y - 17, 560, row_y - 1), fill=(177, 148, 111)) | |
| draw.rectangle((240, row_y - 17, 240 + int(32 * score), row_y - 1), fill=fill) | |
| draw.text((582, row_y - 20), str(int(score)), font=font, fill=INK) | |
| def _draw_wood_map( | |
| draw: ImageDraw.ImageDraw, | |
| map_data: dict[str, Any], | |
| origin: tuple[int, int], | |
| size: tuple[int, int], | |
| verdict: str, | |
| label_font: ImageFont.ImageFont, | |
| small_font: ImageFont.ImageFont, | |
| ) -> None: | |
| dots = map_data.get("dots") if isinstance(map_data.get("dots"), list) else [] | |
| if not dots: | |
| return | |
| x, y = origin | |
| width, height = size | |
| draw.rounded_rectangle( | |
| (x, y, x + width, y + height), | |
| radius=8, | |
| fill=(231, 205, 149), | |
| outline=(80, 47, 22), | |
| width=2, | |
| ) | |
| draw.text((x, y - 26), "IDEA MAP", font=label_font, fill=INK_SOFT) | |
| for dot in dots: | |
| if not isinstance(dot, dict): | |
| continue | |
| px = x + int(width * _bounded_percent(dot.get("x")) / 100) | |
| py = y + int(height * _bounded_percent(dot.get("y")) / 100) | |
| radius = max(3, min(10, int(_number(dot.get("radius"), default=4)))) | |
| kind = str(dot.get("kind") or "inked") | |
| if kind == "idea": | |
| color = LEAF if verdict.startswith("UNWRITTEN") else OXBLOOD | |
| outline = CREAM | |
| elif kind == "echo": | |
| color = OXBLOOD | |
| outline = CREAM | |
| else: | |
| color = (120, 88, 58) | |
| outline = color | |
| draw.ellipse( | |
| (px - radius, py - radius, px + radius, py + radius), | |
| fill=color, | |
| outline=outline, | |
| width=2, | |
| ) | |
| caption = _ellipsize(str(map_data.get("caption") or ""), 78) | |
| _wrap_text(draw, caption, (x, y + height + 12), width, 18, small_font, INK_SOFT) | |
| def _draw_citations( | |
| draw: ImageDraw.ImageDraw, | |
| echoes: list[Any], | |
| origin: tuple[int, int], | |
| max_width: int, | |
| font: ImageFont.ImageFont, | |
| ) -> None: | |
| if not echoes: | |
| return | |
| x, y = origin | |
| draw.text((x, y - 20), "CLOSEST PAGES", font=_font(18), fill=INK_SOFT) | |
| for index, echo in enumerate(echoes[:3]): | |
| if not isinstance(echo, dict): | |
| continue | |
| project = echo.get("project") if isinstance(echo.get("project"), dict) else {} | |
| page = echo.get("page_number") or "?" | |
| title = project.get("title") or project.get("id") or "Untitled" | |
| label = f"Page {page}: {title}" | |
| _wrap_text(draw, label, (x, y + 8 + index * 30), max_width, 18, font, INK_SOFT) | |
| def _wrap_text( | |
| draw: ImageDraw.ImageDraw, | |
| text: str, | |
| origin: tuple[int, int], | |
| max_width: int, | |
| line_height: int, | |
| font: ImageFont.ImageFont, | |
| fill: tuple[int, int, int], | |
| align: str = "left", | |
| ) -> int: | |
| words = str(text).split() | |
| if not words: | |
| return origin[1] | |
| x, y = origin | |
| line = "" | |
| for word in words: | |
| next_line = f"{line} {word}".strip() | |
| if _text_width(draw, next_line, font) > max_width and line: | |
| _draw_line(draw, line, x, y, max_width, font, fill, align) | |
| line = word | |
| y += line_height | |
| else: | |
| line = next_line | |
| if line: | |
| _draw_line(draw, line, x, y, max_width, font, fill, align) | |
| y += line_height | |
| return y | |
| def _draw_line( | |
| draw: ImageDraw.ImageDraw, | |
| text: str, | |
| x: int, | |
| y: int, | |
| max_width: int, | |
| font: ImageFont.ImageFont, | |
| fill: tuple[int, int, int], | |
| align: str, | |
| ) -> None: | |
| if align == "center": | |
| x = x + (max_width - _text_width(draw, text, font)) // 2 | |
| draw.text((x, y), text, font=font, fill=fill) | |
| def _text_width(draw: ImageDraw.ImageDraw, text: str, font: ImageFont.ImageFont) -> int: | |
| box = draw.textbbox((0, 0), text, font=font) | |
| return box[2] - box[0] | |
| def _font(size: int) -> ImageFont.ImageFont: | |
| return ImageFont.load_default(size=size) | |
| def _ellipsize(value: str, limit: int) -> str: | |
| text = " ".join(value.split()) | |
| if len(text) <= limit: | |
| return text | |
| return text[: max(0, limit - 1)].rstrip() + "..." | |
| def _number(value: Any, default: float) -> float: | |
| try: | |
| number = float(value) | |
| except (TypeError, ValueError): | |
| return default | |
| return number if math.isfinite(number) else default | |
| def _bounded_percent(value: Any) -> float: | |
| return max(4, min(96, _number(value, default=50))) | |
| def _mix(a: tuple[int, int, int], b: tuple[int, int, int], t: float) -> tuple[int, int, int]: | |
| t = max(0, min(1, t)) | |
| return tuple(round(a[index] * (1 - t) + b[index] * t) for index in range(3)) | |
| def _blend( | |
| foreground: tuple[int, int, int], | |
| alpha: float, | |
| background: tuple[int, int, int], | |
| ) -> tuple[int, int, int]: | |
| return _mix(background, foreground, alpha) | |