bankml / tools /makecards.py
Gregory-L's picture
bankML: the whole source (github.com/cryptoAGI/bankml @ 12ae409) and its page, with the bankML persona; the live engine (Dockerfile, hf/start.sh) ready for Docker hardware
28c70af verified
Raw History Blame Contribute Delete
7.95 kB
#!/usr/bin/env python3
# SPDX-License-Identifier: MIT OR Apache-2.0
"""Draw a page's share cards: the images a link preview shows (Open Graph, X/Twitter, LinkedIn, Discord, Telegram,
Slack). Three files, deterministic:
og-<name>.jpg 1200×630, the wide card (og:image, twitter:image with summary_large_image)
og-<name>-square.jpg 1200×1200, for platforms that crop square
<name>-touch-icon.png 180×180, the apple-touch-icon
The look is the DeltaVerse's: a triangulated mesh glowing cyan to violet on the landing's radial dark, the frame's
corner marks, and the DeltaVerse $ (the green of deltaverse.pythai.net/favicon.ico) drawn large as an outlined glyph.
The words are arguments, so a card says only what the page measured.
usage: python3 tools/makecards.py [--out DIR] [--name bankml] [--title bankML]
[--tagline "line one|line two"] [--stat "bit-exact|vs llama.cpp"]... [--foot "text"] [--fonts DIR]
needs: Pillow (pip install pillow) and the Noto Sans fonts (fonts-noto-core on Debian/Ubuntu). Check the result with
tools/seo.py, which also confirms each card's real size matches what the page declares."""
import argparse, math, random
from pathlib import Path
try:
from PIL import Image, ImageDraw, ImageFilter, ImageFont
except ImportError:
raise SystemExit("tools/makecards.py needs Pillow: pip install pillow")
GREEN, CYAN, VIOLET = (51, 190, 103), (34, 211, 238), (157, 78, 221)
DEFAULT_STATS = ["bit-exact|vs llama.cpp", "9.4–10×|ternary kernel", "0|dependencies"]
def mesh(w, h, seed, n):
"""A jittered grid, every cell cut on a diagonal: a triangulation with no long slivers."""
rnd = random.Random(seed)
cols, rows = n, max(3, round(n * h / w))
jit = lambda i, last, span: rnd.uniform(-.38, .38) * span if 0 < i < last else 0
pts = [[(c * w / (cols - 1) + jit(c, cols - 1, w / cols), r * h / (rows - 1) + jit(r, rows - 1, h / rows))
for c in range(cols)] for r in range(rows)]
tris = []
for r in range(rows - 1):
for c in range(cols - 1):
a, b, cc, d = pts[r][c], pts[r][c + 1], pts[r + 1][c], pts[r + 1][c + 1]
tris += [(a, b, d), (a, d, cc)] if (r + c) % 2 else [(a, b, cc), (b, d, cc)]
return tris
class Cards:
def __init__(self, fonts: Path):
self.fonts = fonts
for f in ("NotoSans-Bold.ttf", "NotoSans-Light.ttf", "NotoSans-Regular.ttf", "NotoSans-SemiBold.ttf", "NotoSansMono-Regular.ttf"):
if not (fonts / f).exists():
raise SystemExit(f"{fonts / f} not found: install fonts-noto-core or pass --fonts DIR")
def font(self, name, size):
return ImageFont.truetype(str(self.fonts / name), size)
def ground(self, w, h):
g = Image.new("L", (w, h))
gd = ImageDraw.Draw(g)
cx, cy, R = w * .5, h * .5, math.hypot(w, h) * .55
for i in range(60, 0, -1):
gd.ellipse([cx - R * i / 60, cy - R * i / 60, cx + R * i / 60, cy + R * i / 60], fill=int(22 * (1 - i / 60) ** .7))
img = Image.merge("RGB", (g.point(lambda v: v * .35), g.point(lambda v: v * .45), g.point(lambda v: v)))
m = Image.new("RGBA", (w, h), (0, 0, 0, 0))
md = ImageDraw.Draw(m)
for t in mesh(w, h, 1129, 15 if w > h else 11):
x = sum(p[0] for p in t) / 3 / w
col = tuple(int(CYAN[k] * (1 - x) + VIOLET[k] * x) for k in range(3))
md.polygon(t, outline=col + (70,), width=1)
md.polygon(t, fill=col + (int(10 + 18 * random.Random(int(x * 1e6)).random()),))
glow = m.filter(ImageFilter.GaussianBlur(3))
img.paste(glow, (0, 0), glow)
img.paste(m, (0, 0), m)
veil = Image.new("RGBA", (w, h), (4, 6, 14, 150)) # so the type reads over the mesh
img.paste(veil, (0, 0), veil)
return img
def dollar(self, img, x, y, size):
layer = Image.new("RGBA", img.size, (0, 0, 0, 0))
ImageDraw.Draw(layer).text((x, y), "$", font=self.font("NotoSans-Bold.ttf", size), fill=GREEN + (255,), anchor="mm")
glow = layer.filter(ImageFilter.GaussianBlur(16))
img.paste(glow, (0, 0), glow)
ImageDraw.Draw(img).text((x, y), "$", font=self.font("NotoSans-Bold.ttf", size), fill=(6, 22, 12), anchor="mm",
stroke_width=max(4, size // 40), stroke_fill=GREEN)
def card(self, w, h, title, tagline, stats, foot, path):
img = self.ground(w, h)
d = ImageDraw.Draw(img)
square = h >= w
self.dollar(img, *((w * .5, h * .27) if square else (w * .2, h * .5)), int(h * (.42 if square else .78)))
tx, anchor = (w * .5, "ma") if square else (w * .40, "la")
y = h * .52 if square else h * .17
d.text((tx, y), title, font=self.font("NotoSans-Light.ttf", int(h * (.115 if square else .17))), fill=(255, 255, 255), anchor=anchor)
y += h * (.14 if square else .22)
for line in tagline:
d.text((tx, y), line, font=self.font("NotoSans-Regular.ttf", int(h * (.036 if square else .052))), fill=(214, 226, 236), anchor=anchor)
y += h * (.05 if square else .072)
y += h * .03
colw = (w * .82 if square else w * .55) / max(1, len(stats))
x0 = (w - colw * len(stats)) / 2 if square else tx
for i, (big, small) in enumerate(stats):
x = x0 + colw * i
d.text((x, y), big, font=self.font("NotoSans-SemiBold.ttf", int(h * (.045 if square else .062))), fill=CYAN, anchor="la")
d.text((x, y + h * (.058 if square else .082)), small, font=self.font("NotoSans-Regular.ttf", int(h * (.026 if square else .036))),
fill=(160, 172, 190), anchor="la")
d.text((w / 2, h * .94), foot, font=self.font("NotoSansMono-Regular.ttf", int(h * (.022 if square else .032))), fill=(150, 160, 178), anchor="mm")
for ax, ay, sx, sy in [(14, 14, 1, 1), (w - 14, 14, -1, 1), (14, h - 14, 1, -1), (w - 14, h - 14, -1, -1)]:
d.line([(ax, ay), (ax + 34 * sx, ay)], fill=CYAN, width=2) # the landing's corner marks
d.line([(ax, ay), (ax, ay + 34 * sy)], fill=CYAN, width=2)
img.save(path, quality=90, optimize=True, progressive=True)
print(f"{path} {w}×{h}")
def touch(self, path, size=180):
img = Image.new("RGB", (size, size), (0, 0, 0))
ImageDraw.Draw(img).text((size / 2, size * .53), "$", font=self.font("NotoSans-Bold.ttf", int(size * .92)), fill=(6, 22, 12),
anchor="mm", stroke_width=max(3, size // 36), stroke_fill=GREEN)
img.save(path)
print(f"{path} {size}×{size}")
def main():
ap = argparse.ArgumentParser(description=__doc__.split("\n")[0])
ap.add_argument("--out", default=".", help="directory to write into (default: here)")
ap.add_argument("--name", default="bankml", help="file stem: og-<name>.jpg, og-<name>-square.jpg, <name>-touch-icon.png")
ap.add_argument("--title", default="bankML")
ap.add_argument("--tagline", default="Verified 1-bit and ternary language|models on the CPU you already have", help="lines split by |")
ap.add_argument("--stat", action="append", help='"big|small", repeatable (default: the three measured claims)')
ap.add_argument("--foot", default="deltaverse.pythai.net/bankml · github.com/cryptoAGI/bankml · Rust")
ap.add_argument("--fonts", default="/usr/share/fonts/truetype/noto", help="directory holding the Noto Sans .ttf files")
a = ap.parse_args()
out = Path(a.out)
out.mkdir(parents=True, exist_ok=True)
stats = [tuple((s.split("|", 1) + [""])[:2]) for s in (a.stat or DEFAULT_STATS)]
c = Cards(Path(a.fonts))
tagline = a.tagline.split("|")
c.card(1200, 630, a.title, tagline, stats, a.foot, out / f"og-{a.name}.jpg")
c.card(1200, 1200, a.title, tagline, stats, a.foot, out / f"og-{a.name}-square.jpg")
c.touch(out / f"{a.name}-touch-icon.png")
if __name__ == "__main__":
main()