/** * 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, platform?: string | null ): { aspect_ratio: string; aspect_ratios: string[]; required_placements: RrssPlacement[]; } { const isLinkedIn = String(platform || '') .toLowerCase() .includes('linkedin'); 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 (isLinkedIn) { const p: RrssPlacement = { aspect_ratio: '1:1', role: 'linkedin', size: '1080x1080', meta_key: 'asset_url', canva_template_key: 'linkedin', }; return { aspect_ratio: '1:1', aspect_ratios: ['1:1'], 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: 'post', }; return { aspect_ratio: '4:5', aspect_ratios: ['4:5'], required_placements: [p], }; } // post feed (IG/FB) const p: RrssPlacement = { aspect_ratio: '4:5', role: 'feed', size: '1080x1350', meta_key: 'asset_url', canva_template_key: 'post', }; 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; platform?: 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; /** Resolved Canva Brand Template IDs for RRSS (nested ads/rrss or flat). */ canvaTemplates?: Record | null; }; export function buildRrssGenerationBrief(input: RrssGenerationBriefInput) { const cal = input.calendarPiece || {}; const platform = input.platform || (typeof cal.platform === 'string' ? cal.platform : null) || null; const aspects = rrssBriefAspectFields(input.format, platform); return { channel: 'rrss' as const, tenant_id: input.tenantId, client_id: input.tenantId, client_slug: input.clientSlug, client_name: input.clientName, canva_templates: input.canvaTemplates || {}, reason: 'calendario' as const, source_creative_id: null, source_pipeline_id: input.pipelineId, calendar_piece_id: input.calendarPieceId || cal.id || null, format: input.format, platform, 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(); }