---
name: political-tracker-card
description: Generate political tracker/scorecard red cards (AIPAC Tracker style) using GPT Image 2 or Nano Banana via OpenRouter. Magazine-cover aesthetic, dark red backgrounds, silhouettes, large dollar amounts, editorial typography. For social media political graphics exposing lobby connections, campaign financing, and public contracts.
triggers:
  - "political tracker"
  - "tracker card"
  - "red card"
  - "AIPAC Tracker style"
  - "lobby card"
  - "political scorecard"
  - "generate political graphic"
  - "lobby tracker graphic"
  - "image generation"
  - "GPT Image 2"
  - "Nano Banana"
  - "political infographic card"
requirements:
  env_vars:
    - OPENROUTER_API_KEY
  system_packages:
    - curl
    - python3
    - ffmpeg
    - python3-pil
    - python3-numpy
    - python3-qrcode
---

# Political Tracker Card Generation

Generate red/green political tracker cards in AIPAC Tracker style using AI image generation APIs via OpenRouter.

## Models Available

| Model | OpenRouter ID | Best For | Quality |
|---|---|---|---|
| **GPT Image 2** (preferred) | `openai/gpt-5.4-image-2` | Editorial, photorealistic, magazine covers, text-heavy layouts | Best |
| Nano Banana Pro | `google/gemini-3-pro-image-preview` | Complex layouts, 2K/4K, identity preservation | Excellent |
| Nano Banana 2 | `google/gemini-3.1-flash-image-preview` | Fast iteration, drafts | Very Good |
| Nano Banana (v1) | `google/gemini-2.5-flash-image` | Quick edits, basic cards | Good |

**Default: GPT Image 2** — photorealistic, best text rendering, magazine-cover quality.

## Prompting Best Practices (from OpenAI docs)

1. **Structure**: artifact type → background/scene → subject → layout → text in quotes → constraints
2. **Text**: put exact text in double quotes, specify role (HEADLINE, SUBHEAD, LABEL, FOOTER)
3. **Quality**: use `"high"` for text-heavy cards
4. **Constraints**: "no watermark, no cartoon, no extra text, no border"
5. **For photorealism**: use words like "photorealistic", "editorial photography", "magazine cover", "Sports Illustrated style"

## Template: Red Card Generation Script

```bash
OPENROUTER_KEY=$(grep OPENROUTER_API_KEY ~/.hermes/.env | cut -d= -f2)

curl -s --max-time 300 "https://openrouter.ai/api/v1/chat/completions" \
  -H "Authorization: Bearer $OPENROUTER_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "model": "openai/gpt-5.4-image-2",
    "modalities": ["image", "text"],
    "image_config": {"aspect_ratio": "1:1", "image_size": "1K"},
    "messages": [{
      "role": "user",
      "content": "Sports magazine cover style political tracker card. Photorealistic, glossy editorial photography, dramatic ESPN Magazine / Sports Illustrated aesthetic. Dark crimson red background with volumetric smoke, cinematic god rays, high-contrast. Left: grayscale silhouette with rim lighting. Right: giant glossy white dollar amount. HEADLINE: [NAME]. SUBHEAD: [POSITION]. NUMBER: [AMOUNT]. LABEL: [LOBBY NAME] TOTAL. Bottom: magazine cover lines with stats. Barcode, date stamp. Photorealistic editorial magazine cover."
    }]
  }' -o /tmp/card.json

python3 -c "
import json, base64
with open('/tmp/card.json') as f:
    d = json.load(f)
msg = d.get('choices',[{}])[0].get('message',{})
imgs = msg.get('images',[])
if imgs:
    url = imgs[0]['image_url']['url']
    data = url.split(',')[1]
    with open('/tmp/tracker_card.png','wb') as f:
        f.write(base64.b64decode(data))
    print('Saved: /tmp/tracker_card.png')
else:
    print(f'Error: {json.dumps(d.get(\"error\",{}), indent=2)[:300]}')
"
```

## Image-to-image from an approved/reference card

Use this when the user attaches or points to an approved visual direction and complains the generated cards are too flat/generic. Send the reference image as `image_url` content to GPT Image 2 and explicitly say it is the **primary visual reference**. This preserves the high-impact composition better than trying to recreate it from text alone.

```python
import os, base64, pathlib, requests
src = pathlib.Path('/home/ubuntu/.hermes/image_cache/<attached>.jpg')
img_b64 = base64.b64encode(src.read_bytes()).decode()
prompt = '''Use the attached image as the PRIMARY VISUAL REFERENCE. Create a new 4:5 vertical hyperrealistic political investigative poster/card. Preserve the same punch: red storm sky, electric lightning, Buenos Aires Obelisco, relevant flag/background symbolism, dollars, contract papers, central suited politician, glossy magazine-cover lighting. Editorial typography, not flat infographic, not Canva, not cartoon. Use only the exact text/claims provided.'''
payload = {
  'model': 'openai/gpt-5.4-image-2',
  'modalities': ['image','text'],
  'image_config': {'aspect_ratio':'4:5','image_size':'1K'},
  'messages': [{'role':'user','content':[
    {'type':'text','text': prompt},
    {'type':'image_url','image_url': {'url':'data:image/jpeg;base64,' + img_b64}},
  ]}]
}
r = requests.post('https://openrouter.ai/api/v1/chat/completions',
  headers={'Authorization':'Bearer '+os.environ['OPENROUTER_API_KEY'], 'Content-Type':'application/json'},
  json=payload, timeout=300)
r.raise_for_status()
url = r.json()['choices'][0]['message']['images'][0]['image_url']['url']
pathlib.Path('/tmp/card.png').write_bytes(base64.b64decode(url.split(',',1)[1]))
```

Immediately run `vision_analyze` on the result for: hyperrealism, preservation of reference elements, text legibility, and serious composition problems. If the user approved a specific visual metaphor set (Obelisco + flag + money + contract), do not replace it with generic silhouettes.

## Batch generation for a whole politician set

Use this when the user asks for cards/assets for a project containing multiple politicians, or pushes back with "¿y los demás?". Do **not** stop after one hero card unless the user explicitly requested a single person. Generate the complete set in one pass and verify every deployed URL.

1. **Fetch the factual source data first** (never invent bullets):
   ```python
   import requests, json
   h = {'apikey': SUPABASE_ANON, 'Authorization': 'Bearer ' + SUPABASE_ANON}
   url = f'{SUPABASE_URL}/rest/v1/politicians?select=*,connections(*,lobby_groups(*),evidence(*))&order=name'
   data = requests.get(url, headers=h, timeout=60).json()
   ```
2. **For each politician**, build the GPT Image 2 prompt from DB fields: `name`, `position`, `status`, `total_lobby_ars`, `total_lobby_usd`, and 4-6 compact connection descriptions. If `total_lobby_ars/usd` are zero, label as `CONEXIONES REGISTRADAS` rather than inventing a money total.
3. **Use the approved image as style reference** for every generated card. Keep the shared visual language but vary context subtly by role (diplomacy, security, city government, etc.).
4. **Do not trust AI-generated QR codes.** Ask the model to leave space for QR, then overlay a real QR locally with `python3-qrcode` after generation:
   ```python
   import qrcode
   from PIL import Image, ImageDraw
   im = Image.open(card_path).convert('RGB')
   qr = qrcode.QRCode(error_correction=qrcode.constants.ERROR_CORRECT_H, box_size=10, border=2)
   qr.add_data(politician['qr_url']); qr.make(fit=True)
   q = qr.make_image(fill_color='black', back_color='white').convert('RGB')
   # paste bottom-right on white backing; label ESCANEAR
   ```
   Optionally verify with `zbarimg` when installed.
5. **Generate motion assets for each card** using `scripts/make_impact_animation.py`. Prefer MP4 for production and GIF only as preview/fallback; vertical GIFs are often 7-12MB+ even after palette compression.
6. **Deploy and verify all outputs**, not just the first:
   ```bash
   npm run build
   vercel --prod --yes
   python3 - <<'PY'
   import requests
   for url in urls:
       r = requests.head(url, allow_redirects=True, timeout=30)
       print(r.status_code, r.headers.get('content-type'), r.headers.get('content-length'), url)
       assert r.status_code == 200
   PY
   ```

Pitfall: GPT Image 2 calls are slow and sometimes exceed a 10-minute batch command. Make the generator idempotent: skip already-created images with a sane size, then rerun the script to continue failed/timed-out politicians.

## Green zero-connection cards

Use this when adding politicians with no source-backed cooperative Israel-lobby connections. Keep the framing precise: **0 documented money / 0 cooperative connections found**, not an absolute metaphysical claim. Good labels:

- `SIN PLATA / SIN CONEXIONES COOPERATIVAS`
- `0 CONTRATOS DOCUMENTADOS`
- `0 VIAJES A ISRAEL ENCONTRADOS`
- `0 REUNIONES COOPERATIVAS ENCONTRADAS`
- `0 PROCUREMENT / SEGURIDAD`
- For adversarial public mentions, say `MENCIONES DAIA/OSA: ADVERSARIALES` rather than counting them as connections.

Visual direction: same expensive magazine-cover realism as red cards, but emerald green/black palette, audit/check marks, verified documents, `$0` as the giant number. Still overlay a real QR locally; do not trust model-generated QR.

For Lobby Tracker Argentina, as of May 2026, the first validated green contrast candidates are:
- Myriam Bregman — DB id `69be50fb-489f-4dc8-a148-b7dc178ffc5a`, slug `myriam-bregman`, image `/cards/generated/myriam-bregman-green-gpt-image2-v1.png`, motion `/media/myriam-bregman-impact-short.mp4`.
- Nicolás del Caño — DB id `63976f74-2d23-4c4b-8434-76cc0d551315`, slug `nicolas-del-cano`, image `/cards/generated/nicolas-del-cano-green-gpt-image2-v1.png`, motion `/media/nicolas-del-cano-impact-short.mp4`.

## Fallback: Nano Banana 2

```bash
# Same curl structure, different model:
"model": "google/gemini-3.1-flash-image-preview"
# Prompts should be shorter, less text-heavy
```

## AIPAC Tracker Design Spec

**Red cards** (exposing pro-lobby politicians):
- Grayscale candidate photo/silhouette on LEFT (45%)
- Dark crimson/smoky red background filling ENTIRE card
- GIANT white dollar amount on RIGHT (55%) — the "glowing number"
- Label: "[LOBBY NAME] TOTAL" in uppercase
- Bullet points with specific funding sources
- Footer: "TRACKER — [COUNTRY] 🇦🇷"

**Green cards** (endorsed/clean candidates):
- Same layout but green background
- Shows $0 or minimal lobby funding
- "REJECTS [LOBBY] CASH" label

## GPT Image 2 Sports Magazine Villain Style (May 2026)

For maximum visual impact, use the sports signing poster aesthetic (Lautaro Martinez Inter Milan style). GPT Image 2 excels here. Key elements:

- **HIGH ANGLE** perspective — camera above, looking down (they're beneath us)
- **ELECTRIC RED LIGHTNING** bolts across dark crimson background
- **VILLAIN ENERGY** — grayscale silhouette, red rim lighting, no eye contact, caught-in-the-act
- **SPORTS POSTER TYPOGRAPHY** — massive bold names, glossy white stat numbers
- **4:5 vertical** aspect ratio (best for Twitter/Instagram)
- **Glossy magazine texture**, cinematic god rays, volumetric fog

Prompt template:
```
Sports magazine cover political tracker card, 4:5 vertical.
HIGH ANGLE looking DOWN — ominous villain energy.
Deep blood-red background with ELECTRIC RED LIGHTNING,
cinematic fog, glossy magazine texture.
Silhouette of suited politician, grayscale, high angle, red rim-lit.
Bold white sports fonts. Top: "LOBBY TRACKER ARGENTINA 🇦🇷"
Massive: "[LAST NAME]". Above: "[FIRST NAME]"
Giant glossy: "$[AMOUNT]+". Label: "[LOBBY] TOTAL"
Bottom lines: "[FACT 1] | [FACT 2] | [FACT 3]"
Photorealistic editorial sports magazine cover. Barcode. Date stamp.
```

## Collectible Card Format (May 2026 iteration)

After user feedback, evolved from book-cover to **collectible trading card** style:
- **NO borders or book frames** — clean card edges with subtle glow
- **QR code** (bottom right) with "ESCANEAR PARA VER CONEXIONES" — links to full connections website at lobby-tracker URL
- **6+ stats bullet points** — not just 3, include all relevant facts
- **Both ARS and USD** — always convert using historical blue dollar rates (see table below)
- **Footer URL**: website domain
- **Key lesson**: Do NOT over-control the prompt. V6 failed because too many constraints killed GPT Image 2's creative visual metaphors (obelisk, Israeli flag, money stacks, contract papers). The V5 prompt let GPT be creative and it delivered excellence. Only tweak ONE thing at a time (e.g. spacing).

## jq Extraction Trick (faster than Python for large responses)

```bash
# For 2MB+ responses, jq is faster than Python's json module:
jq -r '.choices[0].message.images[0].image_url.url' /tmp/card.json | cut -d, -f2 | base64 -d > /tmp/output.png
```

## Currency Conversion: Argentina Blue Dollar

ALWAYS convert ARS amounts to USD using the historical "dólar blue" rate for the transaction date.

| Year | Blue Dollar Range | Midpoint |
|---|---|---|
| 2021 | $141 - $208 | ~$180 ARS/USD |
| 2022 | $200 - $346 | ~$280 ARS/USD |
| June 2022 | ~$240 | ~$240 ARS/USD |
| 2023 | $346 - $1,025 | ~$600 ARS/USD |

Format: `ARS $2.400.000.000+` / `≈ USD $8.000.000+`
Sources: cotizacion-dolar.com.ar, bloomberglinea.com

## Card Evolution History

This documents the iterative feedback loop — useful for future card generation to avoid repeated mistakes:

1. **V1 (Nano Banana Pro)**: Generic dark infographic, 4-square grid → **REJECTED**. "No se parece en NADA a AIPAC Tracker."
2. **V2 (Nano Banana 2)**: Red card, silhouette, dollar amount → **MISSED (wasn't delivered)**. Too flat, lacked punch.
3. **V3 (GPT Image 2)**: Red card with stats, barcode → **REJECTED**. "Le falta ese punch típico de imagen2."
4. **V4 (GPT Image 2)**: Sports magazine cover, cinematic, silhouette, high angle → **APPROACH CORRECT**. User said "Va queriendo. La posición del personaje y su forma es excelente."
5. **V5 (GPT Image 2)**: Collectible card, no borders, QR, ARS+USD, 6 stats, villain energy, high angle, electric lightning, obelisk + Israeli flag visual metaphors → **FINAL DESIGN — APPROVED**. User said \"Excelente. Me encanta. Podes manejar un poco mejor el posicionamiento de las cosas.\"
6. **V6 (GPT Image 2)**: Attempted to fix text spacing but OVER-CONTROLLED the prompt → **REJECTED**. Lost creative visual elements (obelisk, flag, money). User: \"Donde esta el obelisco la bandera de Israel qué estaba en la otra imagen, el contrato y los dolares, le quedaba muy bien! La cagaste con la última imagen.\"
7. **V7 (GPT Image 2)**: Restored original V5 prompt, only added spacing + \"let the model be creative with visual metaphors\" → **FINAL APPROVED.** User: \"Hermoso. Ya estamos con el diseño. Dejalo fijo así.\"

Key learnings across iterations:
- GPT Image 2 >> Nano Banana for this use case
- Magazine sports cover aesthetic > flat infographic
- HIGH ANGLE villain perspective resonated strongly
- Always include BOTH ARS and USD
- QR code > barcode (functional purpose)
- Collectible trading card > book layout
- 6+ detailed stats > 3 bullet points
- **CRITICAL: Do NOT over-control the prompt.** V6 was worse because too many layout constraints killed GPT's creativity. Let the model add visual metaphors (obelisk, flag, money, papers). Only change ONE thing per iteration.

## Animating Tracker Cards into GIF/MP4

Use this sub-workflow when the user asks to animate an existing political tracker card, make an impact GIF/video for a detail page/social post, or mimic the mood of a reference short without relying on the original video file.

### Preferred input order

1. **Use the approved source image** if present (the exact card/poster the user liked).
2. If the exact image is missing, recover it from likely paths before asking:
   - `~/.hermes/image_cache/`
   - `/tmp/` (`nano_card.png`, `gpt2_*.json`, `tracker_card.png`, etc.)
   - session logs with `session_search` or direct `~/.hermes/sessions/*` search for `MEDIA:`, `data:image`, `gpt2_v`, `Hermoso`, `obelisco`, `bandera`.
3. If still missing, use a project screenshot or current site card as a temporary demo only, and tell the user the exact image will produce a better result.

### When YouTube/Shorts download is blocked

YouTube Shorts often fail from Oracle/datacenter IPs with `Sign in to confirm you’re not a bot`, even with fresh `yt-dlp`. Try the standard fast path first:

```bash
python3 -m venv /tmp/ytdlpvenv
/tmp/ytdlpvenv/bin/pip install -q -U yt-dlp
/tmp/ytdlpvenv/bin/yt-dlp --no-playlist --force-ipv4 \
  --extractor-args 'youtube:player_client=web_creator,ios' \
  -f 'bv*[height<=720]+ba/b[height<=720]/best' \
  -o '/tmp/reference_short.%(ext)s' 'https://youtube.com/shorts/VIDEO_ID'
```

If blocked, do not stall. First use YouTube oEmbed to recover basic metadata/thumbnail/title for style clues without downloading the video:

```bash
curl -sS 'https://www.youtube.com/oembed?url=https://www.youtube.com/shorts/VIDEO_ID&format=json'
```

Then treat the short as a **style reference** and recreate the effect procedurally: jump cuts/glitch slices, red/blue chromatic offsets, sudden zooms, scanlines, flicker, red impact flashes, grain/VHS noise, and short-form pacing. For a CapCut/terror edit, a square or vertical card with punch zooms and 1-frame dark/red flashes can work even without the original video.

### Procedural GIF/MP4 recipe

A reliable no-GPU path:

1. Ensure tools:
   ```bash
   python3 -m venv /tmp/gifvenv
   /tmp/gifvenv/bin/pip install -q pillow numpy imageio
   command -v ffmpeg
   ```
   If the active Hermes Python lacks `pip` or imports still fail, use the system Python path instead:
   ```bash
   sudo apt-get update
   sudo apt-get install -y python3-pil python3-numpy
   /usr/bin/python3 - <<'PY'
   import PIL, numpy
   print('ok')
   PY
   ```
2. Create frames with Pillow/NumPy:
   - vertical `540x960` or `720x1280`
   - darkened/zoomed source card or screenshot background
   - red radial rays behind the politician/card
   - glowing symbolic elements (flag/contract/money) as overlays
   - card/person rising from bottom-center using ease-out cubic motion
   - 1-3 frame glitch cuts with horizontal roll and RGB channel offsets
   - scanlines + vignette for short-video tension
3. Encode both GIF and MP4:
   ```bash
   # GIF via Pillow for portability; keep <= ~8MB when possible
   # MP4 is much smaller and better for web/Telegram
   ffmpeg -y -framerate 15 -i /tmp/frames/frame_%03d.png \
     -vf 'format=yuv420p' -movflags +faststart public/larreta-impact-demo.mp4
   ```
   Or use the reusable helper script bundled with this skill:
   ```bash
   /usr/bin/python3 ~/.hermes/skills/political-tracker-card/scripts/make_impact_animation.py \
     --input ~/.hermes/image_cache/<approved-card>.jpg \
     --out-dir public/media \
     --slug larreta-impact-short
   ```
   Validated defaults: `720x1280`, `15fps`, `4.2s`, beat punches at ~0.55/1.35/2.45/3.35s, red flashes, RGB split, horizontal glitch bars, scanlines, vignette, and grain. Prefer MP4 for production/detail pages; GIF is heavier and mostly for chat preview/fallback.
4. Verify a mid-frame/contact sheet with `vision_analyze` before sending:

### OpenRouter video / Veo 3.1 motion-poster workflow

Use this when the user asks to make a higher-impact video from a static tracker card using OpenRouter video models (e.g. `google/veo-3.1`) or wants a YouTube Shorts/Reels-style motion poster.

1. **Survey available video models and pricing before submitting jobs**:
   ```bash
   set -a; . ~/.hermes/.env; set +a
   /usr/bin/python3 - <<'PY'
   import os, requests
   j=requests.get('https://openrouter.ai/api/v1/videos/models',
     headers={'Authorization':'Bearer '+os.environ['OPENROUTER_API_KEY']}, timeout=30).json()
   for m in j['data']:
     if 'veo' in m['id'] or 'google' in m['id']:
       print(m['id'], m.get('supported_resolutions'), m.get('supported_aspect_ratios'),
             m.get('supported_durations'), m.get('pricing_skus'))
   PY
   ```
   As of May 2026, `google/veo-3.1` supports `/api/v1/videos` with `aspect_ratio`, `duration`, `resolution`, `generate_audio`, `frame_images`, and polling via `polling_url`. `google/veo-3.1-fast` and `google/veo-3.1-lite` are cheaper iteration options.

2. **For image-to-video attempts**, publish the source card under `public/` first so OpenRouter can fetch it:
   ```bash
   cp ~/.hermes/image_cache/<approved-card>.jpg public/media/source-card.jpg
   vercel --prod --yes
   ```

3. **Submit the job and poll**:
   ```python
   import os, json, requests, pathlib, time
   prompt = pathlib.Path('assets/video-ref/veo_prompt.txt').read_text()
   payload = {
     'model': 'google/veo-3.1',
     'prompt': prompt,
     'aspect_ratio': '9:16',
     'duration': 8,
     'resolution': '720p',
     'generate_audio': False,
     'frame_images': [{
       'type': 'image_url',
       'frame_type': 'first_frame',
       'image_url': {'url': 'https://<site>/media/source-card.jpg'}
     }]
   }
   r = requests.post('https://openrouter.ai/api/v1/videos',
     headers={'Authorization':'Bearer '+os.environ['OPENROUTER_API_KEY'], 'Content-Type':'application/json'},
     json=payload, timeout=60)
   r.raise_for_status()
   polling_url = r.json()['polling_url']
   # GET polling_url with the same Authorization header until status is completed/failed.
   # Download returned content URLs WITH Authorization header; unsigned_urls may still require auth.
   ```

4. **If Veo/Vertex filters image-to-video with real politicians or sensitive poster text**, do not burn repeated full-price retries. Use a two-layer approach:
   - Generate a neutral 9:16 red/black investigative **motion background** with no real names, no readable political text, no real people, no claims.
   - Composite the approved card/poster locally on top with Pillow/ffmpeg, preserving the exact source image and adding zoom pulses, shake, scanlines, RGB split, glitch bars, and subtitles.
   - This keeps the factual poster unchanged while using Veo only for cinematic motion texture.

   Safe background prompt pattern:
   ```text
   Create an 8-second vertical 9:16 motion-poster video for a dark investigative documentary teaser. Red-and-black editorial thriller, polished YouTube Shorts/CapCut-style glitch edit. A stylized graphic poster fills the frame: a stern adult male figure in a dark suit is centered in shadow, surrounded by abstract documents, money-like paper shapes, a city monument silhouette, and a dark flag-like graphic background. Bold white headline blocks are abstract poster typography. Do not render readable real names, numbers, logos, political claims, country names, or QR codes. Slow cinematic push-in, three hard zoom-punch beats, red lightning flicker, parallax, VHS grain, scanlines, chromatic aberration, horizontal glitch bars, micro camera shake, red flash frames, final one-second hold for loop. No gore, no weapons, no physical harm, no real people, no readable political text.
   ```

5. **Composite locally**:
   - Decode Veo MP4 frames with `ffmpeg -i veo-bg.mp4 /tmp/bg/frame_%04d.png`.
   - Place the approved poster/card centered over the background; for vertical videos, keep the card around `690x690` on a `720x1280` canvas.
   - Use slow push-in plus beat pulses around `1.5s`, `4.0s`, `6.2s`.
   - Add subtitle/CTA only if desired; avoid adding any new factual claims.
   - Export MP4 for web and a smaller GIF for previews.

6. **Verify**:
   - Create a contact sheet from 3-4 frames and run `vision_analyze` for cropping/readability.
   - `ffprobe` the MP4 and `curl` deployed asset URLs.

### OpenRouter/content-policy fallback

Image generation/editing/video prompts using real politicians + ominous language + national/religious symbols may return `PROHIBITED_CONTENT`, Vertex usage-guideline errors, or “completed with no output (content may have been filtered)”. If that happens:

- Do **not** keep retrying the same prompt.
- For static image generation/editing: use the already-approved/static card as the source and animate locally.
- For Veo/video: generate a neutral motion background with no real names/claims/readable political text, then composite the approved card locally.
- Keep the visual editorial, not defamatory: labels should reflect the project’s documented tracker framing, while evidence claims remain on the site/DB.

### Integration note

For a Next.js project, save assets under `public/` so they can be referenced as `/larreta-impact-demo.gif` or `/larreta-impact-demo.mp4`. Prefer MP4 in the page for size/performance, with GIF for chat previews.

## Pitfalls

- **GPT Image 2 timeout**: use `--max-time 300` minimum; the model renders slowly (sometimes 60-120s)
- **Base64 decoding**: output is `data:image/png;base64,...` — split on comma, decode part [1]
- **Large responses**: A 2.3MB response JSON takes ~10s to base64 decode — use heredoc Python, not one-liner
- **Nano Banana timeout**: even slower than GPT Image 2; reduce prompt complexity
- **Text hallucination**: ALWAYS verify generated text with vision_analyze before sending to user
- **OpenRouter routing**: GPT Image 2 requires OpenAI API key configured in OpenRouter; Nano Banana works with Google AI keys
- **Empty curl responses**: If curl times out, check file size (0 bytes = curl failed, retry)
- **Exact approved image missing**: search caches/session logs first; if unavailable, create a demo from a site screenshot but clearly ask for the exact image for final quality.
- **GIF size**: vertical GIFs get large quickly; also export MP4 because it is usually 10-50x smaller.
- **Hermes venv may not have pip/Pillow**: if `python3 -m pip` says `No module named pip` or Pillow imports fail, install `python3-pil python3-numpy` with apt and run generation scripts with `/usr/bin/python3`.
- **Font path drift**: `DejaVuSansCondensed-Bold.ttf` may not exist; search `/usr/share/fonts/**/*.ttf` and fall back to `/usr/share/fonts/truetype/dejavu/DejaVuSans-Bold.ttf`.
- **Short download blocked**: use oEmbed metadata for title/thumbnail/context, then recreate the style locally rather than waiting on yt-dlp/CDP.
