Files

108 lines
5.8 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# Custom card: generate `public/og.jpg`
For prompt-craft, composition, and blind read-back verification, follow the
`imagine` and `game-asset-core` skills — this file owns the **card-specific**
contract only (size, lockup, wiring).
1. **Set the canvas with `aspect_ratio: "16:9"`.** The call looks like
`{ "prompt": "…", "aspect_ratio": "16:9" }` — ratio words in the prompt do
**not** set the canvas. At 2mp, 16:9 renders 1792×1008, so the normalize
below cover-crops to 1200×630 trimming only ~3% vertically, which a
centered title survives. A narrower canvas is what kills titles: from 3:2
the same crop takes **~21% vertically**, straight through the lockup.
Paths:
- **Default — one call with `aspect_ratio: "16:9"`:** `imagine_text_to_image`
with the art + baked title. **Check the output dimensions** via
`read_file` / Pillow on the returned `file_path`; if the ratio missed, reframe or use
the API path.
- **Reframe if needed:** pass the prior `file_path` into `imagine_image_to_image`
with **`aspect_ratio: "16:9"`** and a prompt like "extend the scenery
left and right into a wider frame; keep the title lettering and
central subject exactly as they are".
- **Optional — true 2:1 via the xAI Images API:** `POST
https://api.x.ai/v1/images/generations` with `"aspect_ratio": "2:1"`
and `response_format: "b64_json"` using the injected `XAI_API_KEY`
(see the `xai-api` skill). 2mp 2:1 is 1984×992; normalize then trims
only ~2.4% per side and nothing vertical.
Build the prompt from the app's theme, palette, and characters. If the
app already has a key generated asset (hero sprite, title scene), pass
its `file_path` into `imagine_image_to_image` so the card matches in-game art —
same 16:9 + check-the-output rule applies.
**Last resort only** (no `imagine_text_to_image` and no xAI Images API): stay
on whatever canvas you have and keep the entire title block inside the
**middle half** of the frame height, with the crop-clipping check in
step 6 as the gate.
2. **Bake the title in like a game cover.** Store-page covers (Stardew Valley,
Cuphead) lead with a short stylized logo-type title. Put the exact app name
in quotes in the prompt; 13 strong words; optional short tagline under the
title in smaller lettering (exact words in quotes).
- **Stack multi-word titles** into a two-line lockup ("SKY" over "STRIKE").
- **Center the block both ways** with generous margins — avoid "upper third"
/ percentage placement (models hug the edge).
- **Bound the width**: lettering spans roughly half to two-thirds of the
frame, never border to border.
- **Keep comfortable margins anyway.** From a 16:9 canvas the normalize
below trims only ~3% vertically; from a 2:1 API canvas it trims
nothing vertical and ~2.4% per side. The crop turns destructive when
the canvas comes back off-ratio — an edit that pinned to its input's
ratio, or a model miss — so **check the raw canvas dimensions before
cropping**, and re-ratio first if it isn't ~16:9 or 2:1.
3. **Verify glyphs *and* layout on read-back** (see `imagine` / `game-asset-core`
for the blind-describe loop). On a garble or layout miss, **regenerate with a
corrected prompt** — never try to move a logo with `imagine_image_to_image` (frame
translation / seams). After two failed attempts, ship the card **artwork-only**
(titleless).
**Intentional exception vs `imagine`'s "rebuild text with code" rule:** the
share card is a single static PNG; there is no reliable in-sandbox path to
composite crisp code-drawn lettering onto generative art for this asset, so
a clean titleless card is the correct fallback after two glyph failures.
4. **Normalize to exactly 1200×630 JPEG** with the baked-in ffmpeg (cover-crop —
from 16:9 this shaves ~3% top/bottom; from 2:1 ~2.4% per side and
nothing vertical). **JPEG, not PNG**: the card is
photographic generative art, and a PNG of it lands at 12 MB — heavy
enough that link scrapers (X card previews included) time out or skip the
image, so the card silently fails to unfurl. JPEG at this quality is
~150300 KB with no visible loss at unfurl size:
```sh
ffmpeg -y -i card-raw.jpg \
-vf "scale=1200:630:force_original_aspect_ratio=increase,crop=1200:630" \
-q:v 4 /workspace/.grok/og.jpg.tmp
node scripts/write-atomic.mjs /workspace/.grok/og.jpg.tmp public/og.jpg
```
5. **Tell the injector the card is custom** — set `"card": "custom"` in
`src/lib/og/site.json`, handed over the same way, and keep `public/og.jpg`:
```sh
node scripts/write-atomic.mjs /workspace/.grok/site.json.tmp src/lib/og/site.json
```
Bake also infers custom
from the file if the flag is missing, but brand-check still requires the
field. The injector emits the absolute `https://${host}/og.jpg` URL. Do not
add `og:image` to `__root.tsx`.
6. **Verify before finishing** (Pillow is installed; `ffprobe` is **not**):
```sh
python3 -c "
from PIL import Image; import os
im = Image.open('public/og.jpg')
kb = os.path.getsize('public/og.jpg') // 1024
print(im.size, f'{kb} KB')"
# expect: (1200, 630) and under 600 KB (keeps X and other scrapers
# reliable; target <= 300 KB — if over, bump -q:v up a step and re-encode)
```
**Read back the final `public/og.jpg`, not the pre-crop raw** — the crop
is where clipping happens. This is a **hard gate, not an impression**:
if any title glyph touches a frame edge or is visibly cut, the card is
**rejected** — do not ship it, whatever else is right about it. Fix the
ratio (step 1) or regenerate with the width bound restated; a shipped
decapitated title is worse than the placeholder. Then confirm the card
reads like *this* app at thumbnail size — clear subject, correctly
spelled title (if any), comfortable margins.