Files

5.8 KiB
Raw Permalink Blame History

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:

    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:

    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):

    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.