From e4d2a98c76a693c975bc29cd00926b0a3d250ca8 Mon Sep 17 00:00:00 2001 From: Alex Patterson Date: Thu, 12 Mar 2026 22:00:10 -0400 Subject: [PATCH] feat: wire infographic URLs from Sanity through pipeline to Remotion MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Wire infographic URLs from Sanity automatedVideo doc through video-pipeline.ts into Remotion input props.\n\n- getInfographicUrls() reads doc.infographics[] via @sanity/image-url, falls back to researchData.infographicUrls[]\n- Round-robin distribution across scenes\n- Never throws — returns [] so pipeline falls back to Pexels B-roll\n- mapInputProps() passes infographicUrl to each scene for SceneRouter/InfographicScene --- lib/services/remotion.ts | 2 + lib/services/video-pipeline.ts | 71 ++++++++++++++++++++++++++++++++++ 2 files changed, 73 insertions(+) diff --git a/lib/services/remotion.ts b/lib/services/remotion.ts index cbb85af0..df3298f3 100644 --- a/lib/services/remotion.ts +++ b/lib/services/remotion.ts @@ -48,6 +48,7 @@ export interface RenderInput { bRollKeywords?: string[]; sceneNumber?: number; durationEstimate?: number; + infographicUrl?: string; }>; cta: string; }; @@ -117,6 +118,7 @@ function mapInputProps(input: RenderInput): Record { sceneNumber: s.sceneNumber, durationEstimate: s.durationEstimate, bRollUrl: input.bRollUrls[i], + ...(s.infographicUrl ? { infographicUrl: s.infographicUrl } : {}), })), cta: input.script.cta, sponsor: input.sponsor, diff --git a/lib/services/video-pipeline.ts b/lib/services/video-pipeline.ts index 90d6fcee..119c6891 100644 --- a/lib/services/video-pipeline.ts +++ b/lib/services/video-pipeline.ts @@ -11,6 +11,7 @@ import { createClient, type SanityClient } from 'next-sanity'; import { apiVersion, dataset, projectId } from '@/sanity/lib/api'; +import imageUrlBuilder from '@sanity/image-url'; import { generateSpeechFromScript } from '@/lib/services/elevenlabs'; import { generatePerSceneAudio } from '@/lib/services/elevenlabs'; import type { WordTimestamp } from '@/lib/utils/audio-timestamps'; @@ -50,6 +51,12 @@ interface AutomatedVideoDocument { videoUrl?: string; shortUrl?: string; flaggedReason?: string; + infographics?: Array<{ + asset: { _ref: string; _type: string }; + alt?: string; + caption?: string; + }>; + researchData?: string; // JSON string } interface SponsorLeadDocument { @@ -58,6 +65,61 @@ interface SponsorLeadDocument { contactName?: string; } +// --- Infographic URL Extraction --- + +/** + * Extract infographic CDN URLs from the Sanity document. + * + * Priority: + * 1. `doc.infographics[]` — Sanity image assets resolved via @sanity/image-url + * 2. `doc.researchData` — JSON string with `infographicUrls: string[]` (backward compat) + * + * Never throws — returns empty array on failure so the pipeline continues + * with Pexels B-roll fallback. + */ +function getInfographicUrls(doc: AutomatedVideoDocument): string[] { + try { + // Primary: resolve Sanity image assets to CDN URLs + if (doc.infographics?.length) { + const builder = imageUrlBuilder({ projectId, dataset }); + const urls = doc.infographics + .map((img) => { + try { + return builder.image(img.asset).url(); + } catch { + return null; + } + }) + .filter((url): url is string => !!url); + + if (urls.length > 0) { + console.log(`[VIDEO-PIPELINE] Resolved ${urls.length} infographic URL(s) from Sanity image assets`); + return urls; + } + } + + // Fallback: parse researchData JSON for infographicUrls + if (doc.researchData) { + const parsed = JSON.parse(doc.researchData); + if (Array.isArray(parsed?.infographicUrls) && parsed.infographicUrls.length > 0) { + const urls = parsed.infographicUrls.filter( + (u: unknown): u is string => typeof u === 'string' && u.length > 0 + ); + if (urls.length > 0) { + console.log(`[VIDEO-PIPELINE] Resolved ${urls.length} infographic URL(s) from researchData fallback`); + return urls; + } + } + } + } catch (err) { + console.warn( + `[VIDEO-PIPELINE] Failed to extract infographic URLs (non-fatal): ${err instanceof Error ? err.message : String(err)}` + ); + } + + return []; +} + // --- Sanity Write Client --- function getSanityWriteClient(): SanityClient { @@ -132,6 +194,12 @@ export async function processVideoProduction(documentId: string): Promise `[VIDEO-PIPELINE] Script validated: ${script.scenes.length} scenes, hook="${script.hook.substring(0, 50)}..."` ); + // Step 2.5: Extract infographic URLs from Sanity doc + const infographicUrls = getInfographicUrls(doc); + if (infographicUrls.length > 0) { + console.log(`[VIDEO-PIPELINE] ${infographicUrls.length} infographic URL(s) will be distributed across scenes`); + } + // Step 3: Update status to audio_gen console.log(`[VIDEO-PIPELINE] Updating status to "audio_gen"`); await updateStatus(client, documentId, { status: 'audio_gen' }); @@ -243,6 +311,9 @@ export async function processVideoProduction(documentId: string): Promise scenes: script.scenes.map((s, i) => ({ ...s, wordTimestamps: sceneWordTimestamps[i], + ...(infographicUrls.length > 0 + ? { infographicUrl: infographicUrls[i % infographicUrls.length] } + : {}), })), cta: script.cta, },