|
1 | | -"""Render /og.png, the 1200x630 social card for agentrust-io.com, with headless Chrome. |
| 1 | +"""Render AgenTrust social cards (1200x630 PNG) with headless Chrome. |
2 | 2 |
|
3 | | - python tools/render-og-card.py # writes og.png |
| 3 | + python tools/render-og-card.py # agentrust-io.com, writes og.png |
| 4 | + python tools/render-og-card.py wcm --out PATH # wcm.agentrust-io.com card |
4 | 5 | CHROME=/path/to/chrome python tools/render-og-card.py |
5 | 6 |
|
6 | | -The card is HTML so its words can change with an edit, not a lost design file. |
7 | | -It lives here rather than as a page because every *.html in the repository is a |
8 | | -published page to check-site.py and the sitemap. Keep the words in step with the |
9 | | -homepage hero and with the og:image:alt text the pages use; colours are the brand |
10 | | -navy and red from brand/BRAND.md. |
| 7 | +Every card is the same HTML template with different words, so the family stays |
| 8 | +consistent and the next wording change is an edit and one command, not a lost |
| 9 | +design file. Cards for the docs hosts are committed in their own repositories |
| 10 | +(the WCM card lives at docs/assets/og.png in weight-custody-manifest), so pass |
| 11 | +--out for those. |
| 12 | +
|
| 13 | +The template lives here rather than as a page because every *.html in this |
| 14 | +repository is a published page to check-site.py and the sitemap. Keep the words |
| 15 | +in step with each site's hero and its og:image:alt; colours are the brand navy |
| 16 | +and red from brand/BRAND.md. |
11 | 17 |
|
12 | 18 | No --check: a PNG rendered on another machine differs in font hinting, so commit |
13 | 19 | the rendered file and review it by eye. |
14 | 20 | """ |
| 21 | +import argparse |
15 | 22 | import os |
16 | 23 | from pathlib import Path |
17 | 24 | import shutil |
|
20 | 27 | import tempfile |
21 | 28 |
|
22 | 29 | ROOT = Path(__file__).resolve().parents[1] |
23 | | - |
24 | | -EYEBROW = 'Open specifications for verifiable AI' |
25 | | -TITLE = 'AgenTrust' |
26 | | -LEAD = 'Prove what your AI ran, and what it did.' |
27 | 30 | CHAIN = ['Weights', 'Agent', 'Actions', 'Evidence'] |
28 | | -DOMAIN = 'agentrust-io.com' |
| 31 | + |
| 32 | +CARDS = { |
| 33 | + 'hub': { |
| 34 | + 'out': ROOT / 'og.png', |
| 35 | + 'eyebrow': 'Open specifications for verifiable AI', |
| 36 | + 'title': 'AgenTrust', |
| 37 | + 'lead': 'Prove what your AI ran, and what it did.', |
| 38 | + 'step': None, |
| 39 | + 'domain': 'agentrust-io.com', |
| 40 | + }, |
| 41 | + 'wcm': { |
| 42 | + 'out': None, |
| 43 | + 'eyebrow': 'An AgenTrust open specification', |
| 44 | + 'title': 'Weight Custody Manifest', |
| 45 | + 'lead': 'Release model weights only to an attested runtime.', |
| 46 | + 'step': 1, |
| 47 | + 'domain': 'wcm.agentrust-io.com', |
| 48 | + }, |
| 49 | +} |
29 | 50 |
|
30 | 51 | CARD = """<!DOCTYPE html> |
31 | 52 | <html lang="en"> |
|
36 | 57 | body {{ background: #15294B; color: #FFFEFA; position: relative; font-family: Inter, "Segoe UI", ui-sans-serif, sans-serif; }} |
37 | 58 | .wrap {{ position: absolute; left: 90px; right: 90px; top: 84px; }} |
38 | 59 | .eyebrow {{ font-size: 25px; font-weight: 700; letter-spacing: .06em; text-transform: uppercase; color: #9FB3D1; }} |
39 | | - h1 {{ font-size: 118px; font-weight: 700; margin: 26px 0 0; letter-spacing: -.01em; line-height: 1; }} |
| 60 | + h1 {{ font-size: {title_size}px; font-weight: 700; margin: 26px 0 0; letter-spacing: -.01em; line-height: 1; }} |
40 | 61 | .bar {{ width: 120px; height: 9px; background: #B91C1C; margin: 38px 0 34px; }} |
41 | 62 | .lead {{ font-size: 44px; line-height: 1.2; color: #E9E4D8; margin: 0; }} |
42 | 63 | .chain {{ margin-top: 30px; font-size: 27px; color: #9FB3D1; letter-spacing: .01em; }} |
43 | 64 | .chain b {{ color: #FFFEFA; font-weight: 600; }} |
| 65 | + .chain .on {{ color: #FFFEFA; border-bottom: 3px solid #B91C1C; padding-bottom: 2px; }} |
44 | 66 | .domain {{ position: absolute; left: 90px; bottom: 46px; font-size: 27px; font-weight: 700; color: #9FB3D1; }} |
45 | 67 | .foot {{ position: absolute; left: 0; right: 0; bottom: 0; height: 14px; background: #B91C1C; }} |
46 | 68 | </style> |
@@ -73,17 +95,37 @@ def chrome(): |
73 | 95 | raise SystemExit('Chrome not found. Set CHROME to its path.') |
74 | 96 |
|
75 | 97 |
|
| 98 | +def chain_html(step): |
| 99 | + parts = [] |
| 100 | + for i, name in enumerate(CHAIN, 1): |
| 101 | + item = f'<b>{i:02d}</b> {name}' |
| 102 | + parts.append(f'<span class="on">{item}</span>' if i == step else item) |
| 103 | + return ' · '.join(parts) |
| 104 | + |
| 105 | + |
76 | 106 | def main(): |
77 | | - chain = ' · '.join(f'<b>{i:02d}</b> {step}' for i, step in enumerate(CHAIN, 1)) |
78 | | - html = CARD.format(eyebrow=EYEBROW, title=TITLE, lead=LEAD, chain=chain, domain=DOMAIN) |
| 107 | + parser = argparse.ArgumentParser(description=__doc__.splitlines()[0]) |
| 108 | + parser.add_argument('card', nargs='?', default='hub', choices=sorted(CARDS)) |
| 109 | + parser.add_argument('--out', type=Path, help='where to write the PNG (required for docs-host cards)') |
| 110 | + args = parser.parse_args() |
| 111 | + |
| 112 | + card = CARDS[args.card] |
| 113 | + out = args.out or card['out'] |
| 114 | + if out is None: |
| 115 | + raise SystemExit(f'the {args.card} card is committed in its own repository: pass --out') |
| 116 | + html = CARD.format( |
| 117 | + eyebrow=card['eyebrow'], title=card['title'], lead=card['lead'], domain=card['domain'], |
| 118 | + chain=chain_html(card['step']), |
| 119 | + # One line at 1020px wide: long names step down rather than wrap. |
| 120 | + title_size=118 if len(card['title']) <= 12 else 84, |
| 121 | + ) |
79 | 122 | with tempfile.TemporaryDirectory() as tmp: |
80 | | - card = Path(tmp) / 'og-card.html' |
81 | | - card.write_text(html, encoding='utf-8') |
82 | | - out = ROOT / 'og.png' |
| 123 | + page = Path(tmp) / 'og-card.html' |
| 124 | + page.write_text(html, encoding='utf-8') |
83 | 125 | subprocess.run([chrome(), '--headless=new', '--disable-gpu', '--hide-scrollbars', |
84 | 126 | '--force-device-scale-factor=1', '--window-size=1200,630', |
85 | | - f'--screenshot={out}', card.as_uri()], check=True, capture_output=True) |
86 | | - print(f'wrote {out.relative_to(ROOT)}') |
| 127 | + f'--screenshot={Path(out).resolve()}', page.as_uri()], check=True, capture_output=True) |
| 128 | + print(f'wrote {out}') |
87 | 129 |
|
88 | 130 |
|
89 | 131 | if __name__ == '__main__': |
|
0 commit comments