Add RRSS auto-generation via the Ads Claude/Freepik/Canva pipeline.
Pipeline pieces can be created manually or enqueued with channel=rrss, using brandbook, calendar copy and RRSS history; n8n writes results back to creative_pipeline. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -109,8 +109,14 @@ export interface GenerationRequestBrief {
|
||||
client_slug: string;
|
||||
reason: GenerationRequestReason;
|
||||
source_creative_id?: string | null;
|
||||
format: 'estatico' | 'carrusel' | 'video';
|
||||
/** Primary design ratio (always 4:5 for estático). */
|
||||
/** ads (default) | rrss — RRSS writes back to creative_pipeline */
|
||||
channel?: 'ads' | 'rrss';
|
||||
source_pipeline_id?: string | null;
|
||||
calendar_piece_id?: string | null;
|
||||
format: 'estatico' | 'carrusel' | 'video' | 'post' | 'story' | 'reel';
|
||||
/** Format key for skills/formats/*.md (RRSS may differ from format). */
|
||||
skill_format?: string;
|
||||
/** Primary design ratio (always 4:5 for estático ads). */
|
||||
aspect_ratio?: string;
|
||||
/** All placements to produce for estático: 4:5, 9:16, 1.91:1. */
|
||||
aspect_ratios?: string[];
|
||||
@@ -127,6 +133,13 @@ export interface GenerationRequestBrief {
|
||||
adset_id?: string | null;
|
||||
language?: string;
|
||||
deadline?: string;
|
||||
/** RRSS editorial context (calendar piece, brandbook, history). */
|
||||
calendar_piece?: Record<string, unknown> | null;
|
||||
context?: Record<string, unknown> | null;
|
||||
write_back?: Record<string, unknown> | null;
|
||||
notes?: string | null;
|
||||
title?: string | null;
|
||||
caption?: string | null;
|
||||
}
|
||||
|
||||
export interface GenerationRequestResult {
|
||||
|
||||
190
src/lib/rrss-generation.ts
Normal file
190
src/lib/rrss-generation.ts
Normal file
@@ -0,0 +1,190 @@
|
||||
/**
|
||||
* RRSS auto-generation: reuse Ads generation_requests → n8n (Claude + Freepik + Canva)
|
||||
* with channel=rrss and write-back into creative_pipeline.
|
||||
*/
|
||||
|
||||
export type RrssPipelineFormat = 'post' | 'story' | 'reel' | 'carrusel';
|
||||
|
||||
export type RrssPlacement = {
|
||||
aspect_ratio: string;
|
||||
role: string;
|
||||
size: string;
|
||||
meta_key: string;
|
||||
canva_template_key?: string;
|
||||
};
|
||||
|
||||
/** Placements per RRSS format (not the Ads triple 4:5+9:16+1.91:1). */
|
||||
export function rrssBriefAspectFields(format: RrssPipelineFormat): {
|
||||
aspect_ratio: string;
|
||||
aspect_ratios: string[];
|
||||
required_placements: RrssPlacement[];
|
||||
} {
|
||||
if (format === 'story') {
|
||||
const p: RrssPlacement = {
|
||||
aspect_ratio: '9:16',
|
||||
role: 'stories',
|
||||
size: '1080x1920',
|
||||
meta_key: 'asset_url',
|
||||
canva_template_key: 'stories_9_16',
|
||||
};
|
||||
return { aspect_ratio: '9:16', aspect_ratios: ['9:16'], required_placements: [p] };
|
||||
}
|
||||
if (format === 'reel') {
|
||||
const p: RrssPlacement = {
|
||||
aspect_ratio: '9:16',
|
||||
role: 'reel',
|
||||
size: '1080x1920',
|
||||
meta_key: 'asset_url',
|
||||
canva_template_key: 'stories_9_16',
|
||||
};
|
||||
return { aspect_ratio: '9:16', aspect_ratios: ['9:16'], required_placements: [p] };
|
||||
}
|
||||
if (format === 'carrusel') {
|
||||
const p: RrssPlacement = {
|
||||
aspect_ratio: '4:5',
|
||||
role: 'carousel_slide',
|
||||
size: '1080x1350',
|
||||
meta_key: 'asset_url',
|
||||
canva_template_key: 'foto-con-franja',
|
||||
};
|
||||
return {
|
||||
aspect_ratio: '4:5',
|
||||
aspect_ratios: ['4:5'],
|
||||
required_placements: [p],
|
||||
};
|
||||
}
|
||||
// post feed
|
||||
const p: RrssPlacement = {
|
||||
aspect_ratio: '4:5',
|
||||
role: 'feed',
|
||||
size: '1080x1350',
|
||||
meta_key: 'asset_url',
|
||||
canva_template_key: 'foto-con-franja',
|
||||
};
|
||||
return { aspect_ratio: '4:5', aspect_ratios: ['4:5'], required_placements: [p] };
|
||||
}
|
||||
|
||||
export function isRrssPipelineFormat(f: unknown): f is RrssPipelineFormat {
|
||||
return f === 'post' || f === 'story' || f === 'reel' || f === 'carrusel';
|
||||
}
|
||||
|
||||
export function mapRrssFormatToSkillFormat(format: RrssPipelineFormat): string {
|
||||
return format; // skills/formats/{post,story,reel,carrusel}.md
|
||||
}
|
||||
|
||||
export type RrssCalendarPieceBrief = {
|
||||
id?: string | null;
|
||||
title?: string | null;
|
||||
date?: string | null;
|
||||
time?: string | null;
|
||||
format?: string | null;
|
||||
pillar?: string | null;
|
||||
hook?: string | null;
|
||||
body?: string | null;
|
||||
cta?: string | null;
|
||||
hashtags?: string | null;
|
||||
platform?: string | null;
|
||||
};
|
||||
|
||||
export type RrssGenerationBriefInput = {
|
||||
tenantId: string;
|
||||
clientSlug: string;
|
||||
clientName: string;
|
||||
format: RrssPipelineFormat;
|
||||
pipelineId: string;
|
||||
calendarPieceId?: string | null;
|
||||
title: string;
|
||||
caption?: string | null;
|
||||
notes?: string | null;
|
||||
pillar?: string | null;
|
||||
month?: string | null;
|
||||
scheduledAt?: string | null;
|
||||
calendarPiece?: RrssCalendarPieceBrief | null;
|
||||
/** Recent pipeline captions/titles for style continuity */
|
||||
recentPipeline?: Array<{ title: string; format: string; caption?: string | null }>;
|
||||
/** Brandbook prompt snippet (already compiled or raw fields) */
|
||||
brandbookPrompt?: string | null;
|
||||
/** Recent organic posts snapshot */
|
||||
socialHistory?: unknown;
|
||||
variantsRequested?: number;
|
||||
extraNotes?: string | null;
|
||||
};
|
||||
|
||||
export function buildRrssGenerationBrief(input: RrssGenerationBriefInput) {
|
||||
const aspects = rrssBriefAspectFields(input.format);
|
||||
const cal = input.calendarPiece || {};
|
||||
return {
|
||||
channel: 'rrss' as const,
|
||||
tenant_id: input.tenantId,
|
||||
client_id: input.tenantId,
|
||||
client_slug: input.clientSlug,
|
||||
client_name: input.clientName,
|
||||
reason: 'calendario' as const,
|
||||
source_creative_id: null,
|
||||
source_pipeline_id: input.pipelineId,
|
||||
calendar_piece_id: input.calendarPieceId || cal.id || null,
|
||||
format: input.format,
|
||||
skill_format: mapRrssFormatToSkillFormat(input.format),
|
||||
...aspects,
|
||||
variants_requested: Number(input.variantsRequested || 2),
|
||||
language: 'es',
|
||||
title: input.title,
|
||||
caption: input.caption || null,
|
||||
pillar: input.pillar || cal.pillar || null,
|
||||
month: input.month || null,
|
||||
scheduled_at: input.scheduledAt || null,
|
||||
calendar_piece: {
|
||||
id: input.calendarPieceId || cal.id || null,
|
||||
title: cal.title || input.title,
|
||||
date: cal.date || null,
|
||||
time: cal.time || null,
|
||||
format: cal.format || input.format,
|
||||
pillar: cal.pillar || input.pillar || null,
|
||||
hook: cal.hook || null,
|
||||
body: cal.body || input.caption || null,
|
||||
cta: cal.cta || null,
|
||||
hashtags: cal.hashtags || null,
|
||||
platform: cal.platform || 'instagram',
|
||||
},
|
||||
context: {
|
||||
brandbook_prompt: input.brandbookPrompt || null,
|
||||
recent_pipeline: (input.recentPipeline || []).slice(0, 12),
|
||||
social_history: input.socialHistory || null,
|
||||
},
|
||||
notes:
|
||||
[
|
||||
input.extraNotes,
|
||||
input.notes,
|
||||
'Canal RRSS · producir pieza para creative_pipeline (no Meta Ads).',
|
||||
'Escribir resultado vía POST /api/integrations/creative-pipeline con generation_request_id.',
|
||||
]
|
||||
.filter(Boolean)
|
||||
.join('\n'),
|
||||
write_back: {
|
||||
target: 'creative_pipeline',
|
||||
pipeline_id: input.pipelineId,
|
||||
endpoint: '/api/integrations/creative-pipeline',
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
/** Marker stored in pipeline.notes while auto-generation is pending. */
|
||||
export function generationRequestNoteMarker(grId: string): string {
|
||||
return `[[generation_request:${grId}]]`;
|
||||
}
|
||||
|
||||
export function parseGenerationRequestIdFromNotes(
|
||||
notes: string | null | undefined
|
||||
): string | null {
|
||||
if (!notes) return null;
|
||||
const m = notes.match(/\[\[generation_request:([0-9a-f-]{36})\]\]/i);
|
||||
return m?.[1] || null;
|
||||
}
|
||||
|
||||
export function stripGenerationRequestMarker(notes: string | null | undefined): string {
|
||||
if (!notes) return '';
|
||||
return notes
|
||||
.replace(/\[\[generation_request:[0-9a-f-]{36}\]\]/gi, '')
|
||||
.replace(/\n{3,}/g, '\n\n')
|
||||
.trim();
|
||||
}
|
||||
Reference in New Issue
Block a user