# Pipeline — ingesta y render

> Este archivo es la **receta operativa**. Si estás en Cowork, sigue el §0 QUICKSTART tal cual,
> en orden, sin improvisar. La mayoría de los "errores" de chats nuevos vienen de saltarse un paso
> o de intentar `curl`/Playwright (bloqueados). Aquí está la vía que SÍ funciona.

---

## 0. QUICKSTART COWORK (sandbox Linux) — la ruta que no falla

Cowork corre en un sandbox Linux con red MUY restringida. Reglas de oro:

- ❌ **NO uses `curl`/`wget`** para nada de internet (bloqueado, da código 000).
- ❌ **NO uses `pip install playwright` ni `playwright install`** (su CDN está bloqueado).
- ❌ **NO uses `npx puppeteer`** ni CDNs de Chrome (bloqueados).
- ✅ El **registry de npm SÍ funciona** → el navegador viene de `@sparticuz/chromium`.
- ✅ **`web_fetch` SÍ funciona** para leer el HTML del listing (EasyBroker/Pincali/WordPress).
- ✅ Las **fotos se bajan con el MCP de Chrome** (fetch + descarga como blob), NO con curl.

### Paso 0.1 — Instalar el motor de render (1 vez por sesión)

El sandbox se reinicia entre sesiones y borra `/tmp`. Si `render.js` no encuentra el módulo,
reinstala. Comando idempotente (córrelo siempre al empezar; si ya está, no hace nada):

```bash
mkdir -p /tmp/render && cd /tmp/render && \
  [ -d node_modules/@sparticuz/chromium ] || npm i @sparticuz/chromium puppeteer-core --no-fund --no-audit
```

A partir de aquí, para renderizar SIEMPRE se antepone `NODE_PATH=/tmp/render/node_modules`.

### Paso 0.2 — Leer el listing con web_fetch

```
web_fetch(<URL del listing>)
```

Funciona con EasyBroker (`/mx/listing/...` y `/agent/properties/...`), Pincali y WordPress.
Devuelve HTML renderizado con TODO: título, precio, recámaras, baños, m², ID, y las URLs de las
fotos (`assets.easybroker.com/property_images/<propId>/<imgId>/EB-XXXX.jpg`). Extrae los datos de
ahí — no necesitas la página en vivo.

### Paso 0.3 — Bajar las fotos con el MCP de Chrome (solo si la receta lleva fotos)

Las recetas de POST llevan fotos reales; las de **slides-para-video sobre negro NO** (ver §6) —
en ese caso sáltate este paso.

1. Carga las tools de Chrome vía ToolSearch (una sola llamada):
   `select:mcp__claude-in-chrome__tabs_context_mcp,mcp__claude-in-chrome__navigate,mcp__claude-in-chrome__javascript_tool,mcp__claude-in-chrome__tabs_create_mcp`
2. `tabs_context_mcp{createIfEmpty:true}` → toma el `tabId`.
3. `navigate` ese tab a la 1ª foto (`https://assets.easybroker.com/property_images/.../EB-XXXX.jpg`)
   para quedar en el dominio correcto (evita CORS).
4. Con `javascript_tool` corre un fetch→blob→descarga por cada foto. Devuelve el `size` en bytes de
   cada una (lo necesitas para el paso 5). Ejemplo (ajusta ids/ext del listing):

```js
const p=6039185, f="EB-WE9185", e="jpeg";      // propId, prefijo, extensión
const ids=[106722429,106723380,106723254,106723259,106723665,106723809]; // imgIds elegidos
const sizes={};
for(let i=0;i<ids.length;i++){
  const r=await fetch(`https://assets.easybroker.com/property_images/${p}/${ids[i]}/${f}.${e}`);
  const b=await r.blob(); const name=`prop-${String(i+1).padStart(2,'0')}.jpg`; sizes[name]=b.size;
  const u=URL.createObjectURL(b); const a=document.createElement('a');
  a.href=u; a.download=name; document.body.appendChild(a); a.click(); a.remove();
  URL.revokeObjectURL(u); await new Promise(r=>setTimeout(r,400));
}
sizes
```

5. **GOTCHA — Chrome renombra las descargas a `.tmp` con nombre aleatorio.** No llegan con el nombre
   que pusiste. Recupéralas mapeando por tamaño en bytes (el `sizes` del paso 4):

```bash
DEST="<carpeta destino de la propiedad>"; mkdir -p "$DEST"
cd /sessions/<session>/mnt/Downloads   # carpeta Downloads montada
declare -A M=( [170629]="prop-01.jpg" [206303]="prop-02.jpg" )   # {bytes:nombre} del paso 4
for tmp in *.tmp; do s=$(stat -c%s "$tmp"); [ -n "${M[$s]}" ] && cp "$tmp" "$DEST/${M[$s]}"; done
```

6. **GOTCHA — "Permitir descargas múltiples".** Chrome bloquea descargas en lote. La primera vez el
   usuario verá un aviso en la barra de direcciones; pídele que dé **"Permitir"** y reintenta el JS.
   Si tiene activado "Preguntar dónde guardar cada archivo", saldrá un diálogo por foto: sugiérele
   desactivarlo (Configuración → Descargas) para que la próxima tanda fluya sola.

### Paso 0.4 — Rellenar plantilla y renderizar

Ver §2 (contrato de datos), §3 (render). Usa **Python con heredoc CITADO** (`<<'PYEOF'`) para armar
el HTML — ver el GOTCHA del `$` en §5, es el error #1 de chats nuevos.

---

## 1. Ingesta en Claude Code (Windows de Med) — solo si NO es Cowork

En la máquina de Med sí hay `curl`. Descargar HTML y fotos con User-Agent de navegador:

```bash
curl -sL -A "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 Chrome/124.0 Safari/537.36" "<URL>" -o listing.html
curl -sL -A "Mozilla/5.0" "<URL_FOTO>" -o "assets/<propiedad>/foto-01.jpg"
```

Datos en el HTML: precio `$X,XXX,XXX MXN`, specs "N recámaras / N baños / N estacionamientos /
N m² de construcción / N m² de terreno", ID `EB-XXXXX`. **La 1ª foto casi nunca es la fachada** —
revisa a ojo y elige la mejor toma exterior como hero.

---

## 2. Contrato de datos

Cada propiedad se resume en estos campos (se inyectan en los `{{PLACEHOLDERS}}` de la plantilla):

```
MICRO_LABEL / KICKER   ej. "Casa en venta · A estrenar"
TITULO                 titular corto; palabra acento en <em>…</em> (itálica dorada)
DESCRIPCION            1 línea
PRECIO                 "$10,800,000"   (ver GOTCHA del $ en §5)
MONEDA                 "MXN"
UBICACION              "Zibatá · El Marqués"
RECAMARAS / BANOS / M2_CONST / M2_TERRENO / AUTOS
TELEFONO               "442 846 5028"
WEB                    "nodoinmobiliario.com.mx"
HERO_IMG               ruta ABSOLUTA a la mejor foto exterior
IMG_1..IMG_3           interiores para miniaturas (rutas absolutas)
LOGO                   ruta absoluta al logo (brand/logo-transp.png)
HERO_POS               object-position ej. "center 55%"
BADGE / ID_ANUNCIO / PRECIO_LABEL / FEAT_1..3 / TAG_HOT   según la receta
```

Al renderizar desde una carpeta distinta a `plantillas/`, cambia las rutas relativas del HTML
(`../brand/tokens.css`, `../assets/...`) por rutas **absolutas** antes de renderizar.

---

## 3. Render (HTML → PNG)

Formatos post: feed `1080×1350` · cuadrado `1080×1080` · story `1080×1920`. Video/slides:
`1920×1080` (16:9) o `1080×1920` (9:16). Todo sale a 2× (feed → 2160×2700), listo para subir.

### 3a. Cowork — Chromium vía npm (la vía normal)

```bash
NODE_PATH=/tmp/render/node_modules node <skill>/scripts/render.js \
  <archivo.html> <salida.png> <ancho> <alto>
# ej: ... render.js post.html "$DEST/post.png" 1080 1350
```

`render.js` inyecta las fuentes offline del skill (`assets/fonts/`: Playfair, Inter, Montserrat,
Tabler icons), espera `document.fonts.ready` y dispara a 2×. No necesita red ni `--virtual-time-budget`.
Si da `Cannot find module`, reinstala (Paso 0.1). Si el sandbox se reinició a media sesión, el
`/tmp/render` desaparece — reinstálalo y sigue.

### 3b. Claude Code / Windows — Edge headless

```bash
"/c/Program Files (x86)/Microsoft/Edge/Application/msedge.exe" --headless=new --disable-gpu \
  --hide-scrollbars --force-device-scale-factor=2 --window-size=1080,1350 \
  --virtual-time-budget=25000 \
  --screenshot="posts/<propiedad>-<receta>.png" "file:///<ruta-absoluta>/<archivo>.html"
```

Subir `--virtual-time-budget` (~25s) para que carguen los webfonts vía CDN.

### 3c. QA obligatorio

Después de renderizar, **Read el PNG** y revísalo (ver checklist `capa-creativa.md` §9): ¿precio y
teléfono correctos y completos? ¿íconos cargaron (no cuadros vacíos)? ¿texto legible? ¿balance sin
huecos muertos? Corrige y re-renderiza ANTES de entregar. Nunca entregues sin haber mirado el PNG.

---

## 4. Encuadre inteligente

Antes de dar por bueno el hero, analiza el "aire" de la foto (`capa-creativa.md` §6). Fachada con
mucho cielo → `HERO_POS: center 55–70%` para subir la casa. Verifica en el render que la fachada se
ve completa y el titular no cae sobre ella.

---

## 5. Gotchas conocidos (leer antes de armar HTML)

- **⚠️ El `$` en bash heredoc (error #1 de chats nuevos).** Si armas el HTML con
  `cat > x.py <<PYEOF` (heredoc SIN comillas), bash expande `$11`, `$D`, etc. como variables y te
  come parte del precio: `$11,380,000` se vuelve `1,380,000`. **SIEMPRE cita el delimitador:**
  `cat > x.py <<'PYEOF'` (comillas simples). O escribe el `.py` con la tool Write en vez de heredoc.
  Alternativa segura para el precio: usa la entidad HTML `&#36;` en lugar de `$`.
- **Playwright/curl/npx bloqueados** en Cowork. El navegador SOLO por `@sparticuz/chromium` (Paso 0.1).
- **Chrome renombra descargas a `.tmp`** → mapea por tamaño en bytes (Paso 0.3.5).
- **Descargas múltiples** requieren permiso del usuario en Chrome (Paso 0.3.6).
- **Logo:** la clienta de Nodo prefiere el logo sobre fondo/pastilla **oscura**, nunca sobre blanco.
  Usa `logo-transp.png` (dorado con texto blanco, lee perfecto sobre slate/negro). Si el logo del
  cliente es oscuro, avísale antes de ponerlo sobre oscuro.
- **EasyBroker 1ª foto:** casi nunca es la fachada; revisa y elige a mano.
- **Rutas:** al renderizar fuera de `plantillas/`, convierte `../brand/...` y `../assets/...` a rutas
  absolutas en el HTML.
- **Texto sobre foto:** siempre con scrim; blanco puro sobre cielo claro no se lee.

---

## 6. Variante: slides sobre negro para VIDEO (modo Pantalla / Screen)

Caso de uso frecuente de Med: gráficos para **superponer en video** con modo de mezcla
"Pantalla/Screen", que vuelve el negro transparente. NO llevan fotos.

- **Fondo `#000000` puro**, sin degradados ni sombras oscuras (dejarían halos al mezclar).
- **Todo el contenido en tonos claros:** texto blanco/crema, acento dorado `#F59E0B`, íconos claros.
  Nada oscuro que se pierda al volver transparente el negro.
- **3 slides por propiedad:** (1) hook = título + ubicación; (2) características = rec/baños/m²;
  (3) precio + CTA + logo.
- **Formato:** preguntar SIEMPRE 16:9 (1920×1080) vs 9:16 (1080×1920) — depende del video. En 9:16
  usar lista vertical para las características (ícono · dato · etiqueta); en 16:9 fila horizontal.
- Se arman como HTML suelto (fondo `#000`, fuentes de `assets/fonts/`) y se renderizan con el mismo
  `render.js`. El logo dorado sobre negro se conserva perfecto al mezclar.
- Guardar en la carpeta de video que indique el usuario, con sufijo de formato (`-16x9` / `-9x16`).
