fix: Meta previews vertical by default; show all Google ad formats

Meta cards use a 9:16 frame (carousel 1:1 then 4:5). Google gallery balances Search/PMax/Display and surfaces PMax multi-format thumbs.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
4Ventures
2026-10-05 18:22:01 +02:00
parent 78bd80079d
commit 3ddf722912
3 changed files with 404 additions and 144 deletions

View File

@@ -105,12 +105,7 @@ export function inferCreativeAspect(
height?: number
): CreativeAspect {
const L = (label || '').toLowerCase();
if (/9\s*:?\s*16|story|reels|vertical|tall|portrait_9/.test(L)) return '9:16';
if (/4\s*:?\s*5|portrait|feed_portrait/.test(L)) return '4:5';
if (/16\s*:?\s*9|1\.91|landscape|horizontal|right_column|video/.test(L)) {
return '16:9';
}
if (/1\s*:?\s*1|square/.test(L)) return '1:1';
// Dimensiones primero: labels tipo "video" no deben forzar landscape.
if (width && height && width > 0 && height > 0) {
const r = width / height;
if (r <= 0.62) return '9:16';
@@ -118,6 +113,12 @@ export function inferCreativeAspect(
if (r >= 1.5) return '16:9';
if (Math.abs(r - 1) < 0.12) return '1:1';
}
if (/9\s*:?\s*16|story|reels|vertical|tall|portrait_9/.test(L)) return '9:16';
if (/4\s*:?\s*5|portrait|feed_portrait/.test(L)) return '4:5';
if (/16\s*:?\s*9|1\.91|landscape|horizontal|right_column|youtube/.test(L)) {
return '16:9';
}
if (/1\s*:?\s*1|square/.test(L)) return '1:1';
return 'unknown';
}
@@ -201,7 +202,8 @@ function extractCreativePack(creative: Record<string, unknown>): Extracted {
if (Array.isArray(link?.child_attachments)) {
(link!.child_attachments as Record<string, unknown>[]).forEach((c, i) => {
pushImg(c.picture || c.image_url, `carousel-${i + 1} 4:5`, 1080, 1350);
// Carrusel: preferimos 1:1; si el asset es más alto, se reclasifica por ratio.
pushImg(c.picture || c.image_url, `carousel-${i + 1} 1:1`, 1080, 1080);
pushHash(c.image_hash);
if (c.name) headlines.push(str(c.name));
if (c.message) bodies.push(str(c.message));
@@ -269,22 +271,56 @@ function extractCreativePack(creative: Record<string, unknown>): Extracted {
};
}
/** Prefer 9:16 story; then 4:5; else first image. */
function pickVerticalPreview(assets: CreativeAsset[], fallback: string): string {
const images = assets.filter((a) => a.kind === 'image' && a.url);
const nine = images.find((a) => a.aspect === '9:16');
if (nine) return nine.url;
const portrait = images.find(
/**
* Meta preview picker:
* - Default: vertical 9:16 (vídeo/imagen), then 4:5
* - Carousel: 1:1, then 4:5
*/
export function pickMetaPreviewAsset(
assets: CreativeAsset[],
mediaType: string,
fallbackUrl = ''
): { url: string; aspect: CreativeAspect } {
const withUrl = assets.filter((a) => a.url);
const aspectOf = (a: CreativeAsset) =>
a.aspect && a.aspect !== 'unknown'
? a.aspect
: inferCreativeAspect(a.label);
if (mediaType === 'carousel') {
const square = withUrl.find((a) => aspectOf(a) === '1:1');
if (square) return { url: square.url, aspect: '1:1' };
const fourFive = withUrl.find((a) => aspectOf(a) === '4:5');
if (fourFive) return { url: fourFive.url, aspect: '4:5' };
const carousel = withUrl.find((a) => /carousel/i.test(a.label || ''));
if (carousel) return { url: carousel.url, aspect: '1:1' };
if (withUrl[0]) return { url: withUrl[0].url, aspect: '1:1' };
return { url: fallbackUrl, aspect: '1:1' };
}
// Prefer video poster/frame labeled 9:16, then any 9:16 image, then 4:5
const videoNine = withUrl.find(
(a) =>
a.aspect === '4:5' ||
/portrait|vertical|9.?16|4.?5|tall|story/i.test(a.label || '')
(a.kind === 'video' || /video|reel|story/i.test(a.label || '')) &&
aspectOf(a) === '9:16'
);
if (portrait) return portrait.url;
const nonSquare = images.find(
(a) => a.label?.includes('thumbnail') || a.label?.includes('video_poster')
if (videoNine) return { url: videoNine.url, aspect: '9:16' };
const nine = withUrl.find((a) => aspectOf(a) === '9:16');
if (nine) return { url: nine.url, aspect: '9:16' };
const fourFive = withUrl.find((a) => aspectOf(a) === '4:5');
if (fourFive) return { url: fourFive.url, aspect: '4:5' };
const thumb = withUrl.find((a) =>
/thumbnail|video_poster|video_feed/i.test(a.label || '')
);
if (nonSquare) return nonSquare.url;
return images[0]?.url || fallback;
if (thumb) return { url: thumb.url, aspect: '9:16' };
if (withUrl[0]) {
return { url: withUrl[0].url, aspect: aspectOf(withUrl[0]) || '9:16' };
}
return { url: fallbackUrl, aspect: '9:16' };
}
async function sleep(ms: number) {
@@ -657,7 +693,8 @@ function cardFromBundle(
const creative = b?.creative || {};
const fallback =
str(creative.image_url) || str(creative.thumbnail_url) || '';
const preview = pickVerticalPreview(assets, fallback);
const mediaType = detectMediaType(creative);
const picked = pickMetaPreviewAsset(assets, mediaType, fallback);
const headlines = pack.headlines;
const bodies = pack.bodies;
const descriptions = pack.descriptions;
@@ -672,9 +709,9 @@ function cardFromBundle(
clicks: agg.clicks,
conversions: agg.conversions,
cpl: agg.conversions > 0 ? agg.spend / agg.conversions : null,
mediaType: detectMediaType(creative),
thumbnailUrl: preview,
imageUrl: preview,
mediaType,
thumbnailUrl: picked.url,
imageUrl: picked.url,
headline: headlines.join('\n'),
primaryText: bodies.join('\n') || descriptions.join('\n'),
headlines,