Deploy / build (push) Successful in 17s
Every post now carries one unique local-ComfyUI/Ideogram 4 base frame, composited into EN + ZH share cards, a banner and list thumbs. - the shipped prompts_catalog held only 21 distinct metaphors for 72 posts (16 shared one visual), so all 72 were rewritten as unique object+state metaphors and 10 frames re-rolled after OCR found pseudo-text rendered at legible size - render_from_art.mjs emits JPEG by default (558 KB PNG -> 84 KB mozjpeg q85); the ZH 1200x630 card did not exist before this batch - 288 frontmatter fields migrated; the 144 v1 PNGs are gone - PostList's thumb helper keys off the base slug (a ZH card is zh-<slug>.jpg while the thumb is <slug>-800.webp, which 404'd the whole ZH listing) - [...slug].astro's <picture> regex accepts .jpg - the repo now carries the art inputs (metaphors.json, captions/<slug>.json, refreshed prompts_catalog.json) so a frame is reproducible from git alone
148 lines
9.5 KiB
Python
148 lines
9.5 KiB
Python
#!/usr/bin/env python3
|
|
"""build_art_meta.py — regenerate scripts/og-gen/art-meta.json's per-slug entries.
|
|
|
|
`evidence` is the short factual line printed inside the card's evidence bar. It is
|
|
editorial text: every line below quotes a number, log line, file path or command
|
|
that exists in that post's body — nothing is derived from the title, and nothing is
|
|
invented. The renderer refuses to run for a slug that has no entry, so this file is
|
|
also the checklist for the 72-post rollout.
|
|
|
|
The two hand-accepted sample entries keep their explicit `art` paths; every other
|
|
slug uses the renderer's default `<art-dir>/<slug>.png`, so they need no `art` key.
|
|
|
|
Usage: python scripts/og-gen/build_art_meta.py # rewrite art-meta.json
|
|
python scripts/og-gen/build_art_meta.py --check # report coverage only
|
|
"""
|
|
from __future__ import annotations
|
|
|
|
import argparse
|
|
import json
|
|
from pathlib import Path
|
|
|
|
ROOT = Path(__file__).resolve().parents[2]
|
|
META = ROOT / "scripts" / "og-gen" / "art-meta.json"
|
|
|
|
ABOUT = (
|
|
"Inputs for scripts/og-gen/render_from_art.mjs ('one art, four targets'). One entry per post: "
|
|
"`art` = the zero-text base frame (default <art-dir>/<slug>.png), `evidence` = the short factual line "
|
|
"shown in the card's evidence bar (REQUIRED, written by hand — never derived), optional `ogTitle_en` / "
|
|
"`ogTitle_zh` = shortened display titles for the card only (consultant question 1, option C: the article "
|
|
"keeps its real title), optional `tags` = override the three badge tags. Cards render from the post's own "
|
|
"frontmatter (title/description/category) unless an ogTitle override is present."
|
|
)
|
|
|
|
# slug -> evidence line. Each fact is taken from that post's own body.
|
|
EVIDENCE: dict[str, str] = {
|
|
"adding-a-domain-to-someone-elses-mail-server": "4 DNS records · 554 antispam reject",
|
|
"adding-english-mode-to-a-chinese-only-web-app": "no browser API · sub_filter EN mode",
|
|
"ai-furniture-compositing-with-flux-kontext": "kontext multi-image · tainted canvas fix",
|
|
"authentik-css-greater-than-bug": "1 escaped '>' · CSS silently dropped",
|
|
"authentik-forward-auth-gate-wasnt-live": "host 302 · public 200 · 2 ingress layers",
|
|
"authentik-major-upgrade-gotchas": "2025.8 → 2026.8 · auth_flow mixup",
|
|
"automating-cyberpanel-without-the-ui": "v2 JSON API gone · session + CSRF",
|
|
"best-ai-video-generators-2026": "free vs paid · local LTX-2.3 test",
|
|
"calibrating-intel-quick-sync-hevc-quality": "ICQ 28-30 cliff · fast == medium",
|
|
"chainlink-vrf-v2-lottery-contract": "VRF v2.5 · mock-funding bug",
|
|
"docker-reported-exit-0-the-oom-killer-disagreed": "exit 0 · dmesg: 6x cgroup OOM kills",
|
|
"does-hevc-actually-shrink-your-files": "NVENC cq27 bigger · x265 -10-22%",
|
|
"embedding-fonts-so-a-pdf-looks-the-same-everywhere": "4 families · 8 faces · 2 renderers",
|
|
"every-player-stuttered-the-file-was-fine": "5818/5818 intervals · 0 duplicate PTS",
|
|
"five-step-ai-roadmap-what-i-actually-run": "whisper + mem0 in production · step 4 skipped",
|
|
"fully-on-chain-svg-nfts": "base64 SVG in tokenURI() · mood flip on-chain",
|
|
"hardening-a-tor-onion-service": "bind 0.0.0.0:80 denied · onion v3",
|
|
"hello-world": "~140-container homelab · self-hosted",
|
|
"how-i-built-the-digikedai-telegram-bot": "Telegram + WhatsApp · LiteLLM gateway",
|
|
"how-i-host-this-blog": "git push · act_runner · nginx",
|
|
"how-i-made-my-own-songs-with-suno-ai": "7 songs · lyrics + prompts",
|
|
"how-i-vetted-20-vps-providers-with-parallel-subagents": "20 providers · 3 hours · subagents",
|
|
"how-to-verify-a-hosting-provider-before-you-buy": "rdap.org domain age · AUP read",
|
|
"i-let-an-agent-manage-my-shopee-vouchers": "mode=propose gate · RM140 voucher unapproved",
|
|
"it-reported-success-nothing-had-changed": "12.5 MB of 403 MB · rename never landed",
|
|
"jpa-version-field-lost-update": "@Version · stale PUT → 409",
|
|
"loop-engineering-without-a-coding-agent": "11 cron jobs · 8 zero-AI loops",
|
|
"migrating-codeigniter-iis-to-openlitespeed": "17,000-line CI4 · env() fatal",
|
|
"my-backup-was-using-someone-elses-api-quota": "1 TB restic seed · shared rclone quota",
|
|
"n8n-v1-to-v2-upgrade-gotchas": "1.x → 2.40.1 · 7 silent breaks",
|
|
"nocodb-sso-is-a-licensed-feature": "OIDC env accepted · first login crashes",
|
|
"one-hostname-public-tracker-sso-dashboard": "1 host · tracker public, dashboard SSO",
|
|
"one-prometheus-for-unraid-synology-and-a-vps": "3 hosts · 1 prometheus · 0 false panels",
|
|
"passbolt-hang-three-failure-modes": "null fingerprint · 3 failure modes",
|
|
"patching-workbench-26-for-mariadb": "26.7.0 TypeError · 3 patches",
|
|
"putting-a-front-door-on-an-app-you-cant-modify": "5879-byte SPA · 3 injected lines",
|
|
"read-only-nocodb-dashboard-for-a-remote-database": "SNAT source IP · 0 rows copied",
|
|
"reading-a-containers-own-api-docs": "4 docker commands · image is the spec",
|
|
"replacing-rdpguard-with-ipban": "RDPGuard 7.8.7 → IPBan 4.1.0 · 12 IPs",
|
|
"running-production-infrastructure-solo": "162 containers · 3 hosts · 78 compose stacks",
|
|
"running-tts-as-a-service-with-token-sidecars": "tokens expire 10 min / 1 h · sidecars",
|
|
"scraping-bot-walled-marketplace-warm-browser-session": "warm CDP session · split waits",
|
|
"self-healing-digital-goods-entitlements": "5 workflows · NocoDB + n8n + AList",
|
|
"self-hosted-speech-to-text-api": "whisper.cpp large-v3 · ~5x typing",
|
|
"self-hosting-mem0-memory-stack": "mem0 + LiteLLM + pgvector · :20015",
|
|
"shipping-an-ai-photo-editor-as-a-wordpress-plugin": "22 versions · fal.ai async queue",
|
|
"smartctl-exit-code-32-skips-the-disks-that-matter": "rc=32 is not failure · skips bad disks",
|
|
"space-reclamation-wasnt-stuck-it-was-waiting": "121 GiB returned · scheduled 09:29",
|
|
"stop-writing-agent-prompts-for-deterministic-work": "29 straight failures · 1 string compare",
|
|
"syncing-a-self-improving-ai-agent-across-machines": "git-backed profile · manual sync",
|
|
"synology-api-401-with-the-correct-password": "401 twice · 2 causes · 3 commands",
|
|
"synology-spreadsheet-api-is-a-container": "1515 APIs · 4 traps",
|
|
"that-dying-ssd-was-just-a-bad-sata-cable": "WRITE FPDMA timeout · 1 SATA cable",
|
|
"the-cause-was-trim-not-the-ssds": "corruption_errs 27 / 31 · queued TRIM",
|
|
"the-nocodb-attachment-that-wouldnt-update": "300 rows · 1 lookahead regex miss",
|
|
"the-upstream-was-deleted-then-came-back-rewritten": "repo 404 · 233 commits rewrite",
|
|
"two-ways-a-javascript-file-stops-working-and-looks-fine": "shadowed name · let below call",
|
|
"unraid-stop-array-hangs-on-swapfile": "8 GB swapfile pins /mnt/cache",
|
|
"upgrading-codeigniter-46-to-47": "4.6 → 4.7 · 2 unlisted fatals",
|
|
"using-chinese-llm-apis-from-malaysia": "GLM-5.3 · DeepSeek V4 · RMB top-up",
|
|
"verifying-a-pdf-report-page-by-page": "19 pages · every deviation < 2 pt",
|
|
"vetting-an-open-source-dependency-before-you-bet-on-it": "6 checks · grep beats the README",
|
|
"when-smart-says-healthy-but-your-raid-is-corrupting-data": "SMART healthy · checksum 0x8941f998",
|
|
"why-500000-lines-of-commit-churn-only-left-230000-in-production": "518,000 added · 235,000 kept · 5 repos",
|
|
"why-chrome-forgets-its-tabs-in-a-container": "RestoreOnStartup=1 · 70-line keeper",
|
|
"why-i-still-bought-a-local-gpu": "stronger frontier · local for retries + batch",
|
|
"why-my-on-chain-nft-art-changed-on-windows": "autocrlf CRLF · base64 differs",
|
|
"why-telegram-bot-notifications-die": "400 on sendPhoto · getMe fine · IPv6-only DNS",
|
|
"why-your-cdp-clicks-silently-fail": "scroll-behavior:smooth · rect expired",
|
|
"why-your-grafana-dashboard-shows-no-data": "41 panels No data · variable filtered itself",
|
|
"why-your-headless-browser-cant-scrape-everything": "Goofish blocks · Carousell serves",
|
|
"your-disk-full-alert-is-lying": "3% free = 500 GB · alert fires wrong way",
|
|
}
|
|
|
|
# The two consultant samples keep their explicit frame paths.
|
|
ART_OVERRIDES = {
|
|
"running-production-infrastructure-solo": "C:/AI/ideogram4/out/sample_prod_169_00001_.png",
|
|
"docker-reported-exit-0-the-oom-killer-disagreed": "C:/AI/ideogram4/out/sample_oom_169_00001_.png",
|
|
}
|
|
|
|
|
|
def main() -> None:
|
|
ap = argparse.ArgumentParser()
|
|
ap.add_argument("--check", action="store_true")
|
|
args = ap.parse_args()
|
|
|
|
posts = {p.stem for p in (ROOT / "src" / "content" / "posts").glob("*.md")}
|
|
missing = posts - set(EVIDENCE)
|
|
extra = set(EVIDENCE) - posts
|
|
print(f"posts {len(posts)} · evidence {len(EVIDENCE)} · missing {sorted(missing)} · extra {sorted(extra)}")
|
|
if missing or extra:
|
|
raise SystemExit("coverage mismatch — fix the map before rendering")
|
|
if args.check:
|
|
dupes = [e for e in EVIDENCE.values() if list(EVIDENCE.values()).count(e) > 1]
|
|
print(f"duplicate evidence lines: {sorted(set(dupes)) or 'none'}")
|
|
print(f"longest line: {max(len(e) for e in EVIDENCE.values())} chars")
|
|
return
|
|
|
|
out: dict = {"_about": ABOUT, "_status": f"{len(EVIDENCE)} of {len(posts)} posts have a hand-written evidence line; "
|
|
"base art is generated from C:/AI/ideogram4/captions/<slug>.json"}
|
|
for slug in sorted(EVIDENCE):
|
|
entry: dict = {}
|
|
if slug in ART_OVERRIDES:
|
|
entry["art"] = ART_OVERRIDES[slug]
|
|
entry["evidence"] = EVIDENCE[slug]
|
|
out[slug] = entry
|
|
META.write_text(json.dumps(out, ensure_ascii=False, indent=1), encoding="utf-8")
|
|
print(f"wrote {META} ({len(EVIDENCE)} entries)")
|
|
|
|
|
|
if __name__ == "__main__":
|
|
main()
|