From 1cf8e7c1bd2af12fee53fdebb2bbc82038376051 Mon Sep 17 00:00:00 2001 From: 4Ventures <4ventures@MacBook-Air-de-4Ventures.local> Date: Tue, 13 Oct 2026 17:03:40 +0200 Subject: [PATCH] Require 9:16 and 1.91:1 adaptations alongside every static 4:5 ad creative. Generation briefs and Canva webhooks now declare all three Meta placements so WF3/WF5 deliver feed, Stories, and landscape from one Freepik base. Co-authored-by: Cursor --- docs/API_INTEGRACIONES.md | 8 +- skills/formats/estatico-4-5.md | 42 +++++++--- skills/formats/estatico.md | 42 +++++++--- src/app/api/generation-requests/route.ts | 6 +- .../api/integrations/creatives/[id]/route.ts | 15 ++++ src/app/api/integrations/creatives/route.ts | 7 ++ .../api/integrations/fatigue/mark/route.ts | 9 ++- src/components/AgencyCreativesView.tsx | 22 ++++-- src/components/ClientCreativeStylePanel.tsx | 24 +++++- src/lib/creatives-aspect-ratios.ts | 76 +++++++++++++++++++ src/lib/creatives-types.ts | 10 +++ src/lib/creatives-workflow.ts | 17 +++-- 12 files changed, 243 insertions(+), 35 deletions(-) create mode 100644 src/lib/creatives-aspect-ratios.ts diff --git a/docs/API_INTEGRACIONES.md b/docs/API_INTEGRACIONES.md index 1e3a102..11f1501 100644 --- a/docs/API_INTEGRACIONES.md +++ b/docs/API_INTEGRACIONES.md @@ -179,7 +179,13 @@ Cambia el estado de la creatividad a `fatigada`, guarda el evento de auditoría "variants_requested": 2, "avoid_angles": ["objecion_precio"], "format": "estatico", - "aspect_ratio": "4:5" + "aspect_ratio": "4:5", + "aspect_ratios": ["4:5", "9:16", "1.91:1"], + "required_placements": [ + { "aspect_ratio": "4:5", "role": "feed", "size": "1080x1350", "meta_key": "asset_url" }, + { "aspect_ratio": "9:16", "role": "stories", "size": "1080x1920", "meta_key": "stories_asset_url", "canva_template_key": "stories_9_16" }, + { "aspect_ratio": "1.91:1", "role": "landscape", "size": "1200x628", "meta_key": "landscape_asset_url", "canva_template_key": "landscape_1_91" } + ] } } } diff --git a/skills/formats/estatico-4-5.md b/skills/formats/estatico-4-5.md index 84e9eb4..6ca1b40 100644 --- a/skills/formats/estatico-4-5.md +++ b/skills/formats/estatico-4-5.md @@ -1,6 +1,7 @@ -# Skill de formato · Estático para Meta Ads (4:5) +# Skill de formato · Estático para Meta Ads (4:5 + 9:16 + 1.91:1) -> Se aplica junto a la skill del cliente. Define la estructura de un anuncio estático de imagen para feed de Instagram y Facebook. Siempre 4:5 (1080×1350). +> Se aplica junto a la skill del cliente. Define un anuncio estático de imagen para Meta. +> **Diseño base siempre 4:5 (1080×1350).** Cada variante debe además adaptarse a **9:16** (Stories/Reels, 1080×1920) y **1.91:1** (feed landscape / right column, 1200×628). ## Qué entregas por cada variante @@ -14,15 +15,34 @@ Un objeto JSON con exactamente estos campos: "headline": "título del anuncio en Meta, máx. 40 caracteres", "primary_text": "texto principal del anuncio, 2-4 frases, máx. 300 caracteres, la primera frase es el gancho", "cta": "uno de: 'Más información', 'Solicitar presupuesto', 'Reservar', 'Contactar', 'Registrarte'", - "image_prompt": "prompt en inglés para el generador de imagen, SIN texto ni logo, describiendo escena, personas, luz, encuadre y paleta", + "image_prompt": "prompt en inglés para el generador de imagen, SIN texto ni logo, describiendo escena, personas, luz, encuadre vertical 4:5 y paleta", "layout": "una de: 'foto-con-franja' (foto + franja de color con titular), 'tipografico' (solo color y texto), 'dato-grande' (una cifra enorme + contexto)", - "composition_notes": "instrucciones de montaje para diseño en 3-5 líneas: dónde va la franja o el texto, qué palabra va en color de acento, posición del logo, qué recortar. Es lo que ve el diseñador en la tarjeta.", + "composition_notes": "instrucciones de montaje 4:5 en 3-5 líneas: dónde va la franja o el texto, acento, logo, qué recortar", + "composition_notes_by_aspect": { + "4:5": "montaje feed vertical (misma idea que composition_notes)", + "9:16": "cómo adaptar a Stories/Reels: qué recortar, dónde subir el hook, zona segura superior/inferior de UI", + "1.91:1": "cómo adaptar a landscape: recorte horizontal, hook más corto si hace falta, franja lateral o inferior" + }, "rationale": "una frase: por qué este ángulo puede relevar a la fatigada" } ``` Y fuera de las variantes, en `notes`, el diagnóstico de la fatigada en 2-3 frases (qué métrica se movió, si es mensaje o saturación) y cualquier dato que faltara para decidir mejor. +## Entrega multi-formato (obligatorio) + +Por cada variante, el pipeline genera **tres piezas finales** a partir de la misma foto Freepik + hook/support: + +| Ratio | Uso Meta | Tamaño | Campo de salida | +|-------|----------|--------|-----------------| +| **4:5** | Feed IG/FB (principal) | 1080×1350 | `asset_url` | +| **9:16** | Stories / Reels / Advantage+ vertical | 1080×1920 | `meta.stories_asset_url` | +| **1.91:1** | Feed landscape / right column | 1200×628 | `meta.landscape_asset_url` | + +- Una sola generación de imagen Freepik en **4:5** con aire para titular. +- Canva (WF5) compone el 4:5 y **adapta** tipografía/recortes a 9:16 y 1.91:1 (mismas plantillas de marca o las keys `stories_9_16` / `landscape_1_91`). +- No inventes tres fotos distintas: es la misma creatividad en tres placements. + ## Reglas de ángulo - Cada variante ataca un **ángulo distinto** entre sí y distinto del de la creatividad fatigada (viene en `avoid_angles`). @@ -36,6 +56,7 @@ Y fuera de las variantes, en `notes`, el diagnóstico de la fatigada en 2-3 fras - Sin signos de exclamación. Sin mayúsculas completas. - Se lee en 1,5 segundos en móvil con el pulgar en movimiento. - Prohibido empezar por "¿Sabías que", "Descubre", "Imagina". +- En **1.91:1** el hook puede acortarse a 5–6 palabras si no cabe con buena jerarquía. ## Reglas de copy (primary_text) @@ -47,25 +68,28 @@ Y fuera de las variantes, en `notes`, el diagnóstico de la fatigada en 2-3 fras ## Reglas de imagen (image_prompt) - Siempre en inglés, 40-80 palabras. -- Describe: sujeto, acción, entorno, luz, encuadre (vertical 4:5, espacio libre arriba o abajo para texto), paleta en términos de la skill del cliente. +- Describe: sujeto, acción, entorno, luz, encuadre (**vertical 4:5**, espacio libre arriba o abajo para texto), paleta en términos de la skill del cliente. - **La escena tiene que contar el servicio sin texto.** Incluye siempre un objeto o gesto que identifique la actividad (lo que diga la skill del cliente) y deja claros los roles si hay dos personas. Evita escenas ambiguas que puedan leerse como otra cosa: firma de papeles, entrevista, reunión de banco. - Pide explícitamente el aire para el titular con una fracción: `the upper third of the frame is empty wall or sky`. "Negative space" a secas no basta. - Prefiere una persona a dos cuando el mensaje es individual; nunca más de dos. - Termina siempre con: `no text, no logos, no watermarks, photorealistic, vertical 4:5 composition with clear negative space for a headline`. - Nunca pidas personas reconocibles, marcas de terceros, ni niños. -- Una generación por variante. No pidas variaciones. +- Una generación por variante. No pidas variaciones ni tres prompts distintos por ratio. ## Reglas de layout -- Zona segura: 8 % de margen en todos los lados. Texto nunca en el 15 % superior ni inferior (lo tapan la interfaz y el CTA de Meta). +- Zona segura 4:5: 8 % de margen en todos los lados. Texto nunca en el 15 % superior ni inferior (lo tapan la interfaz y el CTA de Meta). +- 9:16: deja aire extra arriba/abajo para UI de Stories; el hook puede subir hacia el tercio superior seguro. +- 1.91:1: evita texto en los extremos laterales; prioriza franja inferior o bloque lateral corto. - Jerarquía: hook ≥ 2× el tamaño del support. - Contraste mínimo 4,5:1 entre texto y fondo. -- El logo ocupa como máximo el 12 % del ancho. +- El logo ocupa como máximo el 12 % del ancho (en landscape, ≤ 10 %). ## Autocheck antes de entregar - [ ] ¿Las variantes tienen ángulos distintos entre sí y del `avoid_angles`? - [ ] ¿Cada hook tiene ≤ 8 palabras? - [ ] ¿El primary_text cabe en 300 caracteres? -- [ ] ¿El image_prompt no pide texto ni logo? +- [ ] ¿El image_prompt no pide texto ni logo y es 4:5? +- [ ] ¿Hay `composition_notes_by_aspect` para 4:5, 9:16 y 1.91:1? - [ ] ¿Se respeta la lista de prohibiciones de la skill del cliente? diff --git a/skills/formats/estatico.md b/skills/formats/estatico.md index 84e9eb4..6ca1b40 100644 --- a/skills/formats/estatico.md +++ b/skills/formats/estatico.md @@ -1,6 +1,7 @@ -# Skill de formato · Estático para Meta Ads (4:5) +# Skill de formato · Estático para Meta Ads (4:5 + 9:16 + 1.91:1) -> Se aplica junto a la skill del cliente. Define la estructura de un anuncio estático de imagen para feed de Instagram y Facebook. Siempre 4:5 (1080×1350). +> Se aplica junto a la skill del cliente. Define un anuncio estático de imagen para Meta. +> **Diseño base siempre 4:5 (1080×1350).** Cada variante debe además adaptarse a **9:16** (Stories/Reels, 1080×1920) y **1.91:1** (feed landscape / right column, 1200×628). ## Qué entregas por cada variante @@ -14,15 +15,34 @@ Un objeto JSON con exactamente estos campos: "headline": "título del anuncio en Meta, máx. 40 caracteres", "primary_text": "texto principal del anuncio, 2-4 frases, máx. 300 caracteres, la primera frase es el gancho", "cta": "uno de: 'Más información', 'Solicitar presupuesto', 'Reservar', 'Contactar', 'Registrarte'", - "image_prompt": "prompt en inglés para el generador de imagen, SIN texto ni logo, describiendo escena, personas, luz, encuadre y paleta", + "image_prompt": "prompt en inglés para el generador de imagen, SIN texto ni logo, describiendo escena, personas, luz, encuadre vertical 4:5 y paleta", "layout": "una de: 'foto-con-franja' (foto + franja de color con titular), 'tipografico' (solo color y texto), 'dato-grande' (una cifra enorme + contexto)", - "composition_notes": "instrucciones de montaje para diseño en 3-5 líneas: dónde va la franja o el texto, qué palabra va en color de acento, posición del logo, qué recortar. Es lo que ve el diseñador en la tarjeta.", + "composition_notes": "instrucciones de montaje 4:5 en 3-5 líneas: dónde va la franja o el texto, acento, logo, qué recortar", + "composition_notes_by_aspect": { + "4:5": "montaje feed vertical (misma idea que composition_notes)", + "9:16": "cómo adaptar a Stories/Reels: qué recortar, dónde subir el hook, zona segura superior/inferior de UI", + "1.91:1": "cómo adaptar a landscape: recorte horizontal, hook más corto si hace falta, franja lateral o inferior" + }, "rationale": "una frase: por qué este ángulo puede relevar a la fatigada" } ``` Y fuera de las variantes, en `notes`, el diagnóstico de la fatigada en 2-3 frases (qué métrica se movió, si es mensaje o saturación) y cualquier dato que faltara para decidir mejor. +## Entrega multi-formato (obligatorio) + +Por cada variante, el pipeline genera **tres piezas finales** a partir de la misma foto Freepik + hook/support: + +| Ratio | Uso Meta | Tamaño | Campo de salida | +|-------|----------|--------|-----------------| +| **4:5** | Feed IG/FB (principal) | 1080×1350 | `asset_url` | +| **9:16** | Stories / Reels / Advantage+ vertical | 1080×1920 | `meta.stories_asset_url` | +| **1.91:1** | Feed landscape / right column | 1200×628 | `meta.landscape_asset_url` | + +- Una sola generación de imagen Freepik en **4:5** con aire para titular. +- Canva (WF5) compone el 4:5 y **adapta** tipografía/recortes a 9:16 y 1.91:1 (mismas plantillas de marca o las keys `stories_9_16` / `landscape_1_91`). +- No inventes tres fotos distintas: es la misma creatividad en tres placements. + ## Reglas de ángulo - Cada variante ataca un **ángulo distinto** entre sí y distinto del de la creatividad fatigada (viene en `avoid_angles`). @@ -36,6 +56,7 @@ Y fuera de las variantes, en `notes`, el diagnóstico de la fatigada en 2-3 fras - Sin signos de exclamación. Sin mayúsculas completas. - Se lee en 1,5 segundos en móvil con el pulgar en movimiento. - Prohibido empezar por "¿Sabías que", "Descubre", "Imagina". +- En **1.91:1** el hook puede acortarse a 5–6 palabras si no cabe con buena jerarquía. ## Reglas de copy (primary_text) @@ -47,25 +68,28 @@ Y fuera de las variantes, en `notes`, el diagnóstico de la fatigada en 2-3 fras ## Reglas de imagen (image_prompt) - Siempre en inglés, 40-80 palabras. -- Describe: sujeto, acción, entorno, luz, encuadre (vertical 4:5, espacio libre arriba o abajo para texto), paleta en términos de la skill del cliente. +- Describe: sujeto, acción, entorno, luz, encuadre (**vertical 4:5**, espacio libre arriba o abajo para texto), paleta en términos de la skill del cliente. - **La escena tiene que contar el servicio sin texto.** Incluye siempre un objeto o gesto que identifique la actividad (lo que diga la skill del cliente) y deja claros los roles si hay dos personas. Evita escenas ambiguas que puedan leerse como otra cosa: firma de papeles, entrevista, reunión de banco. - Pide explícitamente el aire para el titular con una fracción: `the upper third of the frame is empty wall or sky`. "Negative space" a secas no basta. - Prefiere una persona a dos cuando el mensaje es individual; nunca más de dos. - Termina siempre con: `no text, no logos, no watermarks, photorealistic, vertical 4:5 composition with clear negative space for a headline`. - Nunca pidas personas reconocibles, marcas de terceros, ni niños. -- Una generación por variante. No pidas variaciones. +- Una generación por variante. No pidas variaciones ni tres prompts distintos por ratio. ## Reglas de layout -- Zona segura: 8 % de margen en todos los lados. Texto nunca en el 15 % superior ni inferior (lo tapan la interfaz y el CTA de Meta). +- Zona segura 4:5: 8 % de margen en todos los lados. Texto nunca en el 15 % superior ni inferior (lo tapan la interfaz y el CTA de Meta). +- 9:16: deja aire extra arriba/abajo para UI de Stories; el hook puede subir hacia el tercio superior seguro. +- 1.91:1: evita texto en los extremos laterales; prioriza franja inferior o bloque lateral corto. - Jerarquía: hook ≥ 2× el tamaño del support. - Contraste mínimo 4,5:1 entre texto y fondo. -- El logo ocupa como máximo el 12 % del ancho. +- El logo ocupa como máximo el 12 % del ancho (en landscape, ≤ 10 %). ## Autocheck antes de entregar - [ ] ¿Las variantes tienen ángulos distintos entre sí y del `avoid_angles`? - [ ] ¿Cada hook tiene ≤ 8 palabras? - [ ] ¿El primary_text cabe en 300 caracteres? -- [ ] ¿El image_prompt no pide texto ni logo? +- [ ] ¿El image_prompt no pide texto ni logo y es 4:5? +- [ ] ¿Hay `composition_notes_by_aspect` para 4:5, 9:16 y 1.91:1? - [ ] ¿Se respeta la lista de prohibiciones de la skill del cliente? diff --git a/src/app/api/generation-requests/route.ts b/src/app/api/generation-requests/route.ts index 4ef8384..a059dd0 100644 --- a/src/app/api/generation-requests/route.ts +++ b/src/app/api/generation-requests/route.ts @@ -3,6 +3,7 @@ import { createClient } from '@/lib/supabase/server'; import { isAgencyRole, userRole } from '@/lib/agency'; import { resolveActiveTenant } from '@/lib/tenant-context'; import { dispatchN8nWebhookAsync } from '@/lib/creatives-webhooks'; +import { staticAdBriefAspectFields } from '@/lib/creatives-aspect-ratios'; import { createClient as createAdminClient } from '@supabase/supabase-js'; function getAdmin() { @@ -144,13 +145,16 @@ export async function POST(req: NextRequest) { const avoidAngles = body.avoid_angles || (creativeAngle ? [creativeAngle] : []); + const format = body.format || 'estatico'; + const brief = { tenant_id: tenantId, client_id: tenantId, client_slug: tenant?.slug || 'cliente', reason: body.reason || 'manual', source_creative_id: body.source_creative_id || null, - format: body.format || 'estatico', + format, + ...staticAdBriefAspectFields(format), variants_requested: Number(body.variants_requested || 3), campaign_name: campaignName, adset_id: adsetId, diff --git a/src/app/api/integrations/creatives/[id]/route.ts b/src/app/api/integrations/creatives/[id]/route.ts index 209ad98..1754224 100644 --- a/src/app/api/integrations/creatives/[id]/route.ts +++ b/src/app/api/integrations/creatives/[id]/route.ts @@ -127,16 +127,31 @@ export async function PATCH( (existingMeta as any).raw_image_url || (!creative.asset_final && creative.asset_url ? creative.asset_url : undefined); + const placementMeta: Record = {}; + if (typeof body.stories_asset_url === 'string' && body.stories_asset_url) { + placementMeta.stories_asset_url = body.stories_asset_url; + } + if (typeof body.landscape_asset_url === 'string' && body.landscape_asset_url) { + placementMeta.landscape_asset_url = body.landscape_asset_url; + } + if (body.meta !== undefined && typeof body.meta === 'object' && body.meta !== null) { updates.meta = { ...existingMeta, ...(preservedRawUrl ? { raw_image_url: preservedRawUrl } : {}), ...body.meta, + ...placementMeta, }; } else if (preservedRawUrl && body.asset_final) { updates.meta = { ...existingMeta, raw_image_url: preservedRawUrl, + ...placementMeta, + }; + } else if (Object.keys(placementMeta).length > 0) { + updates.meta = { + ...existingMeta, + ...placementMeta, }; } diff --git a/src/app/api/integrations/creatives/route.ts b/src/app/api/integrations/creatives/route.ts index f87b69e..aed93d4 100644 --- a/src/app/api/integrations/creatives/route.ts +++ b/src/app/api/integrations/creatives/route.ts @@ -1,6 +1,7 @@ import { NextRequest, NextResponse } from 'next/server'; import { authenticateIntegrationRequest } from '@/lib/integrations-auth'; import { dispatchN8nWebhookAsync } from '@/lib/creatives-webhooks'; +import { staticAdWebhookAspectFields } from '@/lib/creatives-aspect-ratios'; import { normalizeMetaAdCopyInput } from '@/lib/meta-ad-copy'; import { createClient } from '@supabase/supabase-js'; @@ -66,6 +67,11 @@ export async function POST(req: NextRequest) { if (c.composition_notes) { meta.composition_notes = c.composition_notes; } + if (c.composition_notes_by_aspect) { + meta.composition_notes_by_aspect = c.composition_notes_by_aspect; + } + if (c.stories_asset_url) meta.stories_asset_url = c.stories_asset_url; + if (c.landscape_asset_url) meta.landscape_asset_url = c.landscape_asset_url; if (c.asset_url && !c.asset_final && !meta.raw_image_url) { meta.raw_image_url = c.asset_url; } @@ -154,6 +160,7 @@ export async function POST(req: NextRequest) { meta: c.meta || {}, client: clientInfo, }, + ...staticAdWebhookAspectFields(c.format), }); } } diff --git a/src/app/api/integrations/fatigue/mark/route.ts b/src/app/api/integrations/fatigue/mark/route.ts index 6999e29..0b5805f 100644 --- a/src/app/api/integrations/fatigue/mark/route.ts +++ b/src/app/api/integrations/fatigue/mark/route.ts @@ -1,6 +1,7 @@ import { NextRequest, NextResponse } from 'next/server'; import { authenticateIntegrationRequest } from '@/lib/integrations-auth'; import { dispatchN8nWebhookAsync } from '@/lib/creatives-webhooks'; +import { staticAdBriefAspectFields } from '@/lib/creatives-aspect-ratios'; import { createClient } from '@supabase/supabase-js'; export async function POST(req: NextRequest) { @@ -109,14 +110,18 @@ export async function POST(req: NextRequest) { const deadline = new Date(); deadline.setDate(deadline.getDate() + 1); // 24 horas después + const format = + creative.format === 'video' || creative.format === 'carrusel' + ? creative.format + : 'estatico'; const brief = { client_id: tenantId, tenant_id: tenantId, client_slug: clientSlug, reason: 'fatiga', source_creative_id: creativeId, - format: creative.format === 'video' || creative.format === 'carrusel' ? creative.format : 'estatico', - aspect_ratio: '4:5', + format, + ...staticAdBriefAspectFields(format), variants_requested: 2, avoid_angles: avoidAngles, campaign_name: campaignName, diff --git a/src/components/AgencyCreativesView.tsx b/src/components/AgencyCreativesView.tsx index 93571f5..432ba4b 100644 --- a/src/components/AgencyCreativesView.tsx +++ b/src/components/AgencyCreativesView.tsx @@ -791,8 +791,8 @@ export default function AgencyCreativesView({ ) : null} - {/* Badge Formato + 9:16 Stories */} -
+ {/* Badge Formato + placements 9:16 / 1.91:1 */} +
{c.meta?.stories_asset_url ? ( - 9:16 Stories ↗ + 9:16 ↗ + + ) : null} + {c.meta?.landscape_asset_url ? ( + e.stopPropagation()} + title="Abrir versión 1.91:1 landscape" + className="rounded-md bg-sky-600/90 hover:bg-sky-700 backdrop-blur-xs px-1.5 py-0.5 text-[10px] font-semibold text-white" + > + 1.91:1 ↗ ) : null} @@ -1421,7 +1433,7 @@ export default function AgencyCreativesView({ Historial y Cola de Relevos de Claude (WF3)

- Peticiones procesadas por Claude Opus 5-5 y Freepik Mystic a 4:5. + Peticiones estáticas: Freepik 4:5 + Canva adapta a 9:16 y 1.91:1.