publisher: themed calendar, NIP-01 nostr adapter, gated brand images
ci / lint-test-leaks (pull_request) Canceled after 0s
ci / lint-test-leaks (pull_request) Canceled after 0s
- calendar: one draft per theme/channel (idempotent), rotated schedule - human gate: approve() audited; AUTONOMOUS_MODE has no effect here by design - nostr (primary): in-repo NIP-01 client (D4) — canonical serialization, sha256 id, BIP-340 schnorr sign via coincurve with zero aux randomness (deterministic), pure-python reference verification before publish, per-relay publish with isolated errors, event id stored as post_ref - facebook/instagram adapters: exist but refuse to run unless explicitly enabled via env (default deployment never touches Meta) - images: Pillow brand templates (bh gold / hc orange), 1080x1350, template-driven typeset design (not AI-generated media) - failure isolation: config errors audited, item stays approved tests: 12 new (event roundtrip+tamper, determinism, key validation, calendar idempotence, approve gate, nostr happy path, future schedule, clean config failure, disabled meta adapters, image render) — 41 total
This commit is contained in:
@@ -0,0 +1,72 @@
|
||||
"""Brand image templates (Pillow). 1080x1350 portrait, two brand styles.
|
||||
|
||||
Deterministic, template-driven graphics — deliberately not AI-generated media
|
||||
(DECISIONS: nothing here trips AI-media disclosure requirements; it is typeset
|
||||
design). Fonts: Pillow's bundled default (scalable), so no system font deps.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import os
|
||||
|
||||
from PIL import Image, ImageDraw, ImageFont
|
||||
|
||||
WIDTH, HEIGHT = 1080, 1350
|
||||
|
||||
_BRANDS = {
|
||||
# bitcoinhotels — warm gold on near-black
|
||||
"bh": {"bg": (20, 18, 16), "accent": (201, 168, 76), "text": (242, 236, 224), "muted": (150, 140, 120)},
|
||||
# hardcap — bitcoin orange on cool black
|
||||
"hc": {"bg": (13, 13, 16), "accent": (247, 147, 26), "text": (250, 250, 250), "muted": (120, 120, 130)},
|
||||
}
|
||||
|
||||
|
||||
def _font(size: int, bold: bool = False):
|
||||
# Pillow >=10: load_default(size=...) returns a scalable bundled font
|
||||
return ImageFont.load_default(size=size)
|
||||
|
||||
|
||||
def _vgrad(top: tuple, bottom: tuple) -> Image.Image:
|
||||
"""Simple two-color vertical gradient."""
|
||||
img = Image.new("RGB", (WIDTH, HEIGHT))
|
||||
for y in range(HEIGHT):
|
||||
t = y / (HEIGHT - 1)
|
||||
row = tuple(int(top[i] + (bottom[i] - top[i]) * t) for i in range(3))
|
||||
ImageDraw.Draw(img).line([(0, y), (WIDTH, y)], fill=row)
|
||||
return img
|
||||
|
||||
|
||||
def render_brand_image(
|
||||
*,
|
||||
brand: str = "bh",
|
||||
kicker: str | None,
|
||||
lines: list[str],
|
||||
tagline: list[str] | None = None,
|
||||
url: str | None = None,
|
||||
out_path: str,
|
||||
) -> str:
|
||||
"""Render one branded poster. Returns out_path. Raises on unknown brand."""
|
||||
if brand not in _BRANDS:
|
||||
raise ValueError(f"unknown brand {brand!r} (have: {sorted(_BRANDS)})")
|
||||
palette = _BRANDS[brand]
|
||||
img = _vgrad(palette["bg"], tuple(max(0, c - 8) for c in palette["bg"]))
|
||||
draw = ImageDraw.Draw(img)
|
||||
|
||||
x, y = 90, 240
|
||||
if kicker:
|
||||
draw.text((x, y), kicker, font=_font(40), fill=palette["muted"])
|
||||
y += 110
|
||||
for line in lines:
|
||||
draw.text((x, y), line, font=_font(96, bold=True), fill=palette["text"])
|
||||
y += 118
|
||||
y += 24
|
||||
for t in tagline or []:
|
||||
draw.text((x, y), t, font=_font(38), fill=palette["muted"])
|
||||
y += 58
|
||||
draw.rectangle([x, y + 30, x + 470, y + 33], fill=palette["accent"])
|
||||
if url:
|
||||
draw.text((x, y + 110), url, font=_font(44), fill=palette["accent"])
|
||||
|
||||
os.makedirs(os.path.dirname(out_path) or ".", exist_ok=True)
|
||||
img.save(out_path, "JPEG", quality=92)
|
||||
return out_path
|
||||
Reference in New Issue
Block a user