Abhishek
Initialize project files and updated hackathon tags
485459a
Raw
History Blame Contribute Delete
8.38 kB
from __future__ import annotations
from dataclasses import asdict
from math import ceil, sqrt
from pathlib import Path
from typing import Sequence
import hashlib
import textwrap
from .prompt_rewriter import RewriteResult, STYLE_LIBRARY
from . import flux_backend
def _require_pillow():
try:
from PIL import Image, ImageDraw, ImageFont
except Exception as exc: # pragma: no cover - only triggered in non-runtime environments
raise RuntimeError(
"Pillow is required for quilt rendering. Run scripts/bootstrap_venv.sh first or install requirements.txt."
) from exc
return Image, ImageDraw, ImageFont
def _digest(result: RewriteResult) -> bytes:
payload = "|".join(
[
result.memory_text,
result.location_tag,
result.style,
result.caption,
result.flux_prompt,
result.selected_model_id,
]
)
return hashlib.sha256(payload.encode("utf-8")).digest()
def _rgb_from_digest(digest: bytes, offset: int, mix: float = 1.0) -> tuple[int, int, int]:
r = int(digest[offset % len(digest)] * mix) % 256
g = int(digest[(offset + 1) % len(digest)] * mix) % 256
b = int(digest[(offset + 2) % len(digest)] * mix) % 256
return (r, g, b)
def _palette(result: RewriteResult) -> dict[str, tuple[int, int, int]]:
digest = _digest(result)
base = _rgb_from_digest(digest, 0)
accent = _rgb_from_digest(digest, 3, 1.3)
soft = _rgb_from_digest(digest, 6, 0.8)
border = _rgb_from_digest(digest, 9, 0.65)
text = (246, 241, 229)
return {"base": base, "accent": accent, "soft": soft, "border": border, "text": text}
def _load_font(ImageFont, size: int):
try:
return ImageFont.truetype("DejaVuSans.ttf", size)
except Exception:
return ImageFont.load_default()
def _draw_stitch_border(draw, size: int, color: tuple[int, int, int]) -> None:
step = 16
for x in range(8, size - 8, step):
draw.rectangle((x, 6, x + 8, 10), fill=color)
draw.rectangle((x, size - 10, x + 8, size - 6), fill=color)
for y in range(8, size - 8, step):
draw.rectangle((6, y, 10, y + 8), fill=color)
draw.rectangle((size - 10, y, size - 6, y + 8), fill=color)
def _draw_motif(draw, result: RewriteResult, palette: dict[str, tuple[int, int, int]], size: int) -> None:
Image, ImageDraw, ImageFont = _require_pillow()
digest = _digest(result)
motif = digest[0] % 4
cx = size // 2
cy = int(size * 0.46)
if motif == 0:
# House silhouette.
roof = [(cx - 120, cy + 20), (cx, cy - 92), (cx + 120, cy + 20)]
draw.polygon(roof, fill=palette["accent"])
draw.rectangle((cx - 92, cy + 20, cx + 92, cy + 132), fill=palette["soft"])
draw.rectangle((cx - 24, cy + 74, cx + 24, cy + 132), fill=palette["text"])
draw.rectangle((cx - 70, cy + 46, cx - 34, cy + 82), fill=palette["text"])
draw.rectangle((cx + 34, cy + 46, cx + 70, cy + 82), fill=palette["text"])
elif motif == 1:
# Tree and bench.
draw.line((cx, cy + 40, cx, cy + 128), fill=palette["accent"], width=14)
for shift in (-70, -28, 26, 68):
draw.ellipse((cx + shift - 42, cy - 60, cx + shift + 42, cy + 24), fill=palette["soft"])
draw.rectangle((cx - 92, cy + 92, cx + 92, cy + 114), fill=palette["border"])
draw.rectangle((cx - 70, cy + 70, cx - 52, cy + 114), fill=palette["border"])
draw.rectangle((cx + 52, cy + 70, cx + 70, cy + 114), fill=palette["border"])
elif motif == 2:
# Window grid.
draw.rounded_rectangle((cx - 120, cy - 86, cx + 120, cy + 90), radius=24, fill=palette["accent"])
draw.rectangle((cx - 102, cy - 68, cx + 102, cy + 72), outline=palette["text"], width=6)
draw.line((cx, cy - 68, cx, cy + 72), fill=palette["text"], width=6)
draw.line((cx - 102, cy + 2, cx + 102, cy + 2), fill=palette["text"], width=6)
else:
# Porch light and street.
draw.ellipse((cx - 76, cy - 86, cx + 76, cy + 62), fill=palette["soft"])
draw.line((cx, cy - 98, cx, cy - 140), fill=palette["border"], width=8)
draw.line((cx - 120, cy + 74, cx + 120, cy + 74), fill=palette["text"], width=10)
draw.arc((cx - 100, cy + 24, cx + 100, cy + 148), start=180, end=360, fill=palette["accent"], width=10)
def _draw_caption_band(image: "object", result: RewriteResult, palette: dict[str, tuple[int, int, int]]) -> None:
Image, ImageDraw, ImageFont = _require_pillow()
size = image.size[0]
draw = ImageDraw.Draw(image)
band_height = 112
band_top = size - band_height
draw.rectangle((0, band_top, size, size), fill=(22, 21, 19, 210))
title_font = _load_font(ImageFont, 24)
body_font = _load_font(ImageFont, 16)
small_font = _load_font(ImageFont, 13)
caption = textwrap.fill(result.caption, width=20)
prompt_excerpt = textwrap.fill(result.flux_prompt, width=38)
draw.text((18, band_top + 10), caption, fill=palette["text"], font=title_font)
draw.text((18, band_top + 48), prompt_excerpt, fill=(233, 228, 218), font=body_font)
draw.text((18, band_top + 89), f"{result.selected_model_id} · {result.prompt_source}", fill=(202, 196, 185), font=small_font)
def _render_tile_flux(result: RewriteResult, size: int = 512) -> "object":
Image, ImageDraw, ImageFont = _require_pillow()
palette = _palette(result)
base, meta = flux_backend.try_generate(result.flux_prompt, size=size)
image = base.convert("RGBA").resize((size, size))
_draw_caption_band(image, result, palette)
image.info["backend"] = str(meta.get("backend", "flux-diffusers"))
if meta.get("adapter_name"):
image.info["adapter_name"] = str(meta["adapter_name"])
if meta.get("model_id"):
image.info["model_id"] = str(meta["model_id"])
if meta.get("generation_stats") is not None:
image.info["generation_stats"] = meta["generation_stats"]
return image
def render_tile(result: RewriteResult, size: int = 512) -> "object":
if not flux_backend.available():
raise RuntimeError(
"P5_FLUX_MODEL_DIR is not set or does not point to a local FLUX checkpoint; "
"deterministic tile fallback has been removed"
)
return _render_tile_flux(result, size=size)
def assemble_quilt(results: Sequence[RewriteResult], columns: int | None = None, tile_size: int = 256) -> "object":
Image, ImageDraw, ImageFont = _require_pillow()
cards = list(results)
if not cards:
raise ValueError("assemble_quilt requires at least one card")
columns = columns or (3 if len(cards) >= 6 else 2 if len(cards) > 1 else 1)
columns = max(1, min(columns, len(cards)))
rows = ceil(len(cards) / columns)
padding = 24
banner_height = 104
tile_span = tile_size + 112
width = padding * 2 + columns * tile_size + (columns - 1) * padding
height = banner_height + padding + rows * tile_span + max(0, rows - 1) * padding
image = Image.new("RGBA", (width, height), (244, 238, 228, 255))
draw = ImageDraw.Draw(image)
header_font = _load_font(ImageFont, 30)
body_font = _load_font(ImageFont, 16)
draw.rounded_rectangle((16, 16, width - 16, banner_height - 12), radius=28, fill=(78, 62, 46, 255))
draw.text((36, 32), "Memory Quilt", fill=(250, 245, 236), font=header_font)
subtitle = f"{len(cards)} stitched contribution(s) · local model renderer"
draw.text((36, 68), subtitle, fill=(239, 226, 205), font=body_font)
for index, result in enumerate(cards):
row = index // columns
col = index % columns
x = padding + col * (tile_size + padding)
y = banner_height + padding + row * (tile_span + padding)
tile = render_tile(result, size=tile_size).convert("RGBA")
image.paste(tile, (x, y), tile)
return image
def save_image(image: "object", path: str | Path) -> Path:
path = Path(path)
path.parent.mkdir(parents=True, exist_ok=True)
image.save(path, format="PNG")
return path
def card_summaries(results: Sequence[RewriteResult]) -> list[dict[str, object]]:
summaries: list[dict[str, object]] = []
for index, result in enumerate(results, start=1):
summary = asdict(result)
summary["index"] = index
summaries.append(summary)
return summaries