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 <cursoragent@cursor.com>
This commit is contained in:
4Ventures
2026-10-13 17:03:40 +02:00
parent c8608f36f5
commit 1cf8e7c1bd
12 changed files with 243 additions and 35 deletions

View File

@@ -0,0 +1,76 @@
/**
* Aspect ratios for Meta static ads.
* Primary design is always 4:5; every estatico request must also adapt to Stories (9:16)
* and landscape feed / right column (1.91:1).
*/
export const STATIC_PRIMARY_ASPECT = '4:5' as const;
export const STATIC_AD_ASPECT_RATIOS = ['4:5', '9:16', '1.91:1'] as const;
export type StaticAdAspectRatio = (typeof STATIC_AD_ASPECT_RATIOS)[number];
export type StaticAdPlacement = {
aspect_ratio: StaticAdAspectRatio;
role: 'feed' | 'stories' | 'landscape';
size: string;
/** Where n8n should store the final URL on the creative */
meta_key: 'asset_url' | 'stories_asset_url' | 'landscape_asset_url';
canva_template_key?: string;
};
export const STATIC_AD_PLACEMENTS: StaticAdPlacement[] = [
{
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',
},
];
export function isStaticAdFormat(format?: string | null): boolean {
const f = String(format || 'estatico').toLowerCase().trim();
return f === 'estatico' || f === 'image' || f === 'static';
}
/** Fields to merge into generation_request.brief for static ads. */
export function staticAdBriefAspectFields(format?: string | null): {
aspect_ratio?: string;
aspect_ratios?: string[];
required_placements?: StaticAdPlacement[];
} {
if (!isStaticAdFormat(format)) {
return {};
}
return {
aspect_ratio: STATIC_PRIMARY_ASPECT,
aspect_ratios: [...STATIC_AD_ASPECT_RATIOS],
required_placements: STATIC_AD_PLACEMENTS,
};
}
/** Extra payload for creative.created / recompose so WF5 composes all placements. */
export function staticAdWebhookAspectFields(format?: string | null) {
if (!isStaticAdFormat(format)) {
return {};
}
return {
aspect_ratio: STATIC_PRIMARY_ASPECT,
aspect_ratios: [...STATIC_AD_ASPECT_RATIOS],
required_placements: STATIC_AD_PLACEMENTS,
};
}

View File

@@ -110,7 +110,17 @@ export interface GenerationRequestBrief {
reason: GenerationRequestReason;
source_creative_id?: string | null;
format: 'estatico' | 'carrusel' | 'video';
/** Primary design ratio (always 4:5 for estático). */
aspect_ratio?: string;
/** All placements to produce for estático: 4:5, 9:16, 1.91:1. */
aspect_ratios?: string[];
required_placements?: Array<{
aspect_ratio: string;
role: string;
size: string;
meta_key: string;
canva_template_key?: string;
}>;
variants_requested: number;
avoid_angles?: string[];
campaign_name?: string | null;

View File

@@ -14,6 +14,10 @@ import {
buildPartialRegenBriefNotes,
type CreativeRegenSelection,
} from './creatives-regen';
import {
staticAdBriefAspectFields,
staticAdWebhookAspectFields,
} from './creatives-aspect-ratios';
export interface ActionActor {
role: 'agency' | 'client' | string;
@@ -346,14 +350,15 @@ export async function rejectCreative(
const landingUrl =
prodEntry?.landing_url || creative.landing_url || tenant?.default_landing_url || null;
const format = creative.format || 'estatico';
const briefPayload = {
client_id: tenantId,
tenant_id: tenantId,
client_slug: tenant?.slug || 'bps',
reason: 'manual',
source_creative_id: sourceId,
format: creative.format || 'estatico',
aspect_ratio: '4:5',
format,
...staticAdBriefAspectFields(format),
variants_requested: options?.variantsRequested || 1,
avoid_angles: avoidAngles,
campaign_name: campaignName,
@@ -639,14 +644,15 @@ export async function requestCreativePartialV2(
.filter(Boolean)
.join('\n\n');
const format = creative.format || 'estatico';
const briefPayload = {
client_id: tenantId,
tenant_id: tenantId,
client_slug: tenant?.slug || 'bps',
reason: 'manual',
source_creative_id: creative.id,
format: creative.format || 'estatico',
aspect_ratio: '4:5',
format,
...staticAdBriefAspectFields(format),
variants_requested: 1,
avoid_angles: plan.regenerate.image && creative.angle ? [creative.angle] : [],
campaign_name: campaignName,
@@ -824,7 +830,7 @@ export async function recomposeCreativeInCanva(
visibility: 'agency',
});
// Disparar WF5 pasando la imagen limpia original (raw_image_url) como asset_url para Canva
// Disparar WF5: componer 4:5 + adaptar 9:16 y 1.91:1
dispatchN8nWebhookAsync('creative.created', {
creative: {
...updated,
@@ -833,6 +839,7 @@ export async function recomposeCreativeInCanva(
meta: existingMeta,
client: clientInfo,
},
...staticAdWebhookAspectFields(updated.format),
});
return updated;