|
| 1 | +from __future__ import annotations |
| 2 | + |
| 3 | +from pathlib import Path |
| 4 | +from PIL import Image, ImageDraw, ImageFont, ImageFilter |
| 5 | + |
| 6 | + |
| 7 | +ROOT = Path(__file__).resolve().parent |
| 8 | +BACKGROUND = ROOT / "generated-backgrounds" |
| 9 | +FINAL = ROOT / "final" |
| 10 | +BRAND = ROOT.parent / "brand" |
| 11 | + |
| 12 | +FONT_REGULAR = "/System/Library/Fonts/Supplemental/Arial.ttf" |
| 13 | +FONT_BOLD = "/System/Library/Fonts/Supplemental/Arial Bold.ttf" |
| 14 | + |
| 15 | +INK = (236, 239, 225, 255) |
| 16 | +MUTED = (171, 176, 162, 255) |
| 17 | +OLIVE = (174, 207, 119, 255) |
| 18 | +BLUE = (141, 196, 212, 255) |
| 19 | +GOLD = (216, 193, 103, 255) |
| 20 | +LINE = (216, 228, 188, 90) |
| 21 | +DARK = (11, 14, 12, 220) |
| 22 | + |
| 23 | + |
| 24 | +def font(size: int, bold: bool = False) -> ImageFont.FreeTypeFont: |
| 25 | + return ImageFont.truetype(FONT_BOLD if bold else FONT_REGULAR, size=size) |
| 26 | + |
| 27 | + |
| 28 | +def load_bg(name: str) -> Image.Image: |
| 29 | + return Image.open(BACKGROUND / name / "openai-gpt-image-2" / "image.png").convert("RGBA") |
| 30 | + |
| 31 | + |
| 32 | +def fit_logo(size: int) -> Image.Image: |
| 33 | + logo = Image.open(BRAND / "ob1-beanie-mark-cream.png").convert("RGBA") |
| 34 | + logo.thumbnail((size, size), Image.Resampling.LANCZOS) |
| 35 | + return logo |
| 36 | + |
| 37 | + |
| 38 | +def overlay_gradient(img: Image.Image, opacity: int = 150) -> Image.Image: |
| 39 | + w, h = img.size |
| 40 | + layer = Image.new("RGBA", (w, h), (0, 0, 0, 0)) |
| 41 | + px = layer.load() |
| 42 | + for x in range(w): |
| 43 | + for y in range(h): |
| 44 | + left = 1 - (x / max(w - 1, 1)) |
| 45 | + bottom = y / max(h - 1, 1) |
| 46 | + alpha = int(opacity * (0.35 + 0.65 * left) + 42 * bottom) |
| 47 | + px[x, y] = (6, 8, 7, min(alpha, 235)) |
| 48 | + return Image.alpha_composite(img, layer) |
| 49 | + |
| 50 | + |
| 51 | +def add_microtype(img: Image.Image, text: str = "NBJ OB1") -> Image.Image: |
| 52 | + w, h = img.size |
| 53 | + layer = Image.new("RGBA", (w, h), (0, 0, 0, 0)) |
| 54 | + draw = ImageDraw.Draw(layer) |
| 55 | + f = font(12, bold=True) |
| 56 | + for y in range(-20, h + 40, 86): |
| 57 | + for x in range(-20, w + 180, 182): |
| 58 | + draw.text((x, y), text, font=f, fill=(230, 240, 205, 18)) |
| 59 | + layer = layer.filter(ImageFilter.GaussianBlur(0.15)) |
| 60 | + return Image.alpha_composite(img, layer) |
| 61 | + |
| 62 | + |
| 63 | +def label(draw: ImageDraw.ImageDraw, xy: tuple[int, int], text: str, fill=OLIVE) -> None: |
| 64 | + x, y = xy |
| 65 | + f = font(22, bold=True) |
| 66 | + pad_x, pad_y = 14, 8 |
| 67 | + box = draw.textbbox((x, y), text, font=f) |
| 68 | + draw.rectangle( |
| 69 | + (x - pad_x, y - pad_y, box[2] + pad_x, box[3] + pad_y), |
| 70 | + outline=(fill[0], fill[1], fill[2], 105), |
| 71 | + fill=(20, 24, 20, 125), |
| 72 | + width=1, |
| 73 | + ) |
| 74 | + draw.text((x, y), text, font=f, fill=fill) |
| 75 | + |
| 76 | + |
| 77 | +def draw_footer(draw: ImageDraw.ImageDraw, w: int, h: int) -> None: |
| 78 | + footer = "Built by Nate B. Jones / OB1 | substack.com/@natesnewsletter | natebjones.com" |
| 79 | + draw.text((48, h - 56), footer, font=font(20), fill=(216, 222, 199, 205)) |
| 80 | + |
| 81 | + |
| 82 | +def draw_logo_lockup(img: Image.Image, draw: ImageDraw.ImageDraw, x: int, y: int, logo_size: int = 74) -> None: |
| 83 | + logo = fit_logo(logo_size) |
| 84 | + img.alpha_composite(logo, (x, y)) |
| 85 | + draw.text((x + logo_size + 18, y + 8), "NBJ / OB1", font=font(24, bold=True), fill=INK) |
| 86 | + draw.text((x + logo_size + 18, y + 40), "AGENT MEMORY", font=font(16, bold=True), fill=(OLIVE[0], OLIVE[1], OLIVE[2], 220)) |
| 87 | + |
| 88 | + |
| 89 | +def save(img: Image.Image, name: str) -> None: |
| 90 | + FINAL.mkdir(parents=True, exist_ok=True) |
| 91 | + img.convert("RGB").save(FINAL / name, quality=95, optimize=True) |
| 92 | + |
| 93 | + |
| 94 | +def hero() -> None: |
| 95 | + img = overlay_gradient(load_bg("hero-16x9"), 165) |
| 96 | + img = add_microtype(img) |
| 97 | + draw = ImageDraw.Draw(img) |
| 98 | + draw_logo_lockup(img, draw, 64, 58, 82) |
| 99 | + draw.text((64, 205), "NBJ OB1", font=font(44, bold=True), fill=OLIVE) |
| 100 | + draw.text((64, 260), "Agent Memory", font=font(92, bold=True), fill=INK) |
| 101 | + draw.text((64, 356), "for OpenClaw", font=font(58, bold=True), fill=(209, 218, 194, 250)) |
| 102 | + draw.text((68, 455), "Recall before the task. Write back after. Inspect everything.", font=font(31), fill=(218, 224, 204, 230)) |
| 103 | + label(draw, (70, 545), "ClawHub plugin + skill") |
| 104 | + label(draw, (342, 545), "provenance-aware") |
| 105 | + label(draw, (592, 545), "human review") |
| 106 | + draw_footer(draw, *img.size) |
| 107 | + save(img, "nbj-ob1-agent-memory-hero-16x9.png") |
| 108 | + |
| 109 | + |
| 110 | +def banner() -> None: |
| 111 | + img = overlay_gradient(load_bg("clawhub-banner"), 145) |
| 112 | + img = add_microtype(img) |
| 113 | + draw = ImageDraw.Draw(img) |
| 114 | + draw_logo_lockup(img, draw, 44, 42, 68) |
| 115 | + draw.text((44, 166), "NBJ OB1 Agent Memory", font=font(58, bold=True), fill=INK) |
| 116 | + draw.text((48, 235), "OpenClaw plugin + skill for governed recall and write-back.", font=font(29), fill=(219, 224, 204, 232)) |
| 117 | + label(draw, (50, 320), "@natebjones/ob1-agent-memory") |
| 118 | + label(draw, (475, 320), "nbj-ob1-agent-memory-openclaw", fill=BLUE) |
| 119 | + draw.text((48, 446), "Follow Nate: substack.com/@natesnewsletter | natebjones.com", font=font(20), fill=(216, 222, 199, 205)) |
| 120 | + save(img, "nbj-ob1-agent-memory-clawhub-banner.png") |
| 121 | + |
| 122 | + |
| 123 | +def square() -> None: |
| 124 | + img = overlay_gradient(load_bg("social-square"), 165) |
| 125 | + img = add_microtype(img) |
| 126 | + draw = ImageDraw.Draw(img) |
| 127 | + draw_logo_lockup(img, draw, 62, 58, 76) |
| 128 | + draw.text((64, 228), "Agents need", font=font(58, bold=True), fill=INK) |
| 129 | + draw.text((64, 292), "memory they", font=font(58, bold=True), fill=INK) |
| 130 | + draw.text((64, 356), "can trust.", font=font(74, bold=True), fill=OLIVE) |
| 131 | + draw.text((68, 478), "NBJ OB1 Agent Memory gives OpenClaw", font=font(29), fill=(222, 227, 207, 232)) |
| 132 | + draw.text((68, 520), "scoped recall, write-back, review queues,", font=font(29), fill=(222, 227, 207, 232)) |
| 133 | + draw.text((68, 562), "and recall traces.", font=font(29), fill=(222, 227, 207, 232)) |
| 134 | + label(draw, (72, 660), "evidence is not instruction") |
| 135 | + label(draw, (72, 724), "inspectable by default", fill=BLUE) |
| 136 | + draw.text((68, 930), "Nate B. Jones / OB1", font=font(21), fill=(216, 222, 199, 210)) |
| 137 | + draw.text((68, 958), "substack.com/@natesnewsletter | natebjones.com", font=font(19), fill=(216, 222, 199, 190)) |
| 138 | + save(img, "nbj-ob1-agent-memory-social-square.png") |
| 139 | + |
| 140 | + |
| 141 | +def loop_card() -> None: |
| 142 | + img = overlay_gradient(load_bg("loop-card"), 135) |
| 143 | + img = add_microtype(img) |
| 144 | + draw = ImageDraw.Draw(img) |
| 145 | + draw_logo_lockup(img, draw, 58, 48, 72) |
| 146 | + draw.text((58, 160), "The governed agent memory loop", font=font(58, bold=True), fill=INK) |
| 147 | + draw.text((62, 232), "OpenClaw acts. OB1 remembers what is useful, sourced, and reviewable.", font=font(30), fill=(218, 224, 204, 230)) |
| 148 | + nodes = [ |
| 149 | + ((90, 420), "1", "Recall", "Scoped context\n+ use policy", OLIVE), |
| 150 | + ((455, 420), "2", "Work", "Agent task\nacross models", BLUE), |
| 151 | + ((820, 420), "3", "Write back", "Compact operational\nmemory", GOLD), |
| 152 | + ((1185, 420), "4", "Review", "Confirm, edit,\nor reject", OLIVE), |
| 153 | + ] |
| 154 | + for idx, ((x, y), num, title, body, color) in enumerate(nodes): |
| 155 | + draw.rounded_rectangle((x, y, x + 270, y + 220), radius=8, fill=(18, 22, 19, 190), outline=(color[0], color[1], color[2], 155), width=2) |
| 156 | + draw.text((x + 24, y + 24), num, font=font(28, bold=True), fill=color) |
| 157 | + draw.text((x + 24, y + 72), title, font=font(35, bold=True), fill=INK) |
| 158 | + draw.multiline_text((x + 24, y + 124), body, font=font(21), fill=(216, 222, 199, 215), spacing=8) |
| 159 | + if idx < len(nodes) - 1: |
| 160 | + draw.line((x + 290, y + 110, x + 345, y + 110), fill=LINE, width=3) |
| 161 | + draw.polygon([(x + 345, y + 110), (x + 329, y + 101), (x + 329, y + 119)], fill=LINE) |
| 162 | + draw_footer(draw, *img.size) |
| 163 | + save(img, "nbj-ob1-agent-memory-loop-card.png") |
| 164 | + |
| 165 | + |
| 166 | +if __name__ == "__main__": |
| 167 | + hero() |
| 168 | + banner() |
| 169 | + square() |
| 170 | + loop_card() |
0 commit comments