From 72ad4a459d0c55b435c2a70b217f152d65e104e0 Mon Sep 17 00:00:00 2001 From: Miriad Date: Thu, 5 Mar 2026 07:02:06 +0000 Subject: [PATCH 1/3] feat: add getCookieHeader() to NotebookLM client for authenticated downloads Co-authored-by: research --- lib/services/notebooklm/client.ts | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/lib/services/notebooklm/client.ts b/lib/services/notebooklm/client.ts index 14ab5e83..864105f8 100644 --- a/lib/services/notebooklm/client.ts +++ b/lib/services/notebooklm/client.ts @@ -96,6 +96,14 @@ export class NotebookLMClient { return new NotebookLMClient(auth); } + /** + * Get the raw cookie header string for authenticated fetch calls. + * Useful for downloading auth-gated resources (e.g., infographic images). + */ + getCookieHeader(): string { + return this.auth.cookieHeader; + } + // ------------------------------------------------------------------------- // Internal RPC helper // ------------------------------------------------------------------------- From 53e673a298afab9a5123ffe21cdee8b482661c43 Mon Sep 17 00:00:00 2001 From: Miriad Date: Thu, 5 Mar 2026 07:02:21 +0000 Subject: [PATCH 2/3] feat: add infographics array field to automatedVideo schema (visible, reusable for blog posts) Co-authored-by: research --- sanity/schemas/documents/automatedVideo.ts | 26 ++++++++++++++++++++++ 1 file changed, 26 insertions(+) diff --git a/sanity/schemas/documents/automatedVideo.ts b/sanity/schemas/documents/automatedVideo.ts index e1e07017..a33fc1ec 100644 --- a/sanity/schemas/documents/automatedVideo.ts +++ b/sanity/schemas/documents/automatedVideo.ts @@ -288,6 +288,32 @@ export default defineType({ type: 'url', description: 'Direct URL to the short video (auto-populated from video asset)', }), + defineField({ + name: 'infographics', + title: 'Infographics', + type: 'array', + description: 'Research infographics from NotebookLM — reusable for blog posts and video b-roll', + of: [ + { + type: 'image', + options: { hotspot: true }, + fields: [ + defineField({ + name: 'alt', + title: 'Alt Text', + type: 'string', + description: 'Describe the infographic for accessibility', + }), + defineField({ + name: 'caption', + title: 'Caption', + type: 'string', + description: 'Optional caption for display', + }), + ], + }, + ], + }), defineField({ name: 'renderData', title: 'Render Data', From c285a7ba97693453babea1728886a4fba16936d2 Mon Sep 17 00:00:00 2001 From: Miriad Date: Thu, 5 Mar 2026 07:03:14 +0000 Subject: [PATCH 3/3] feat: download NotebookLM infographics and upload to Sanity assets in check-research - Import writeClient from sanity-write-client - Download PNGs with authenticated fetch using NotebookLM cookies - Upload to Sanity via writeClient.assets.upload() - Store image refs in infographics array field on doc - Keep backward-compat CDN URLs in researchData.infographicUrls - Graceful error handling per infographic (try/catch) Co-authored-by: research --- app/api/cron/check-research/route.ts | 92 +++++++++++++++++++++++----- 1 file changed, 77 insertions(+), 15 deletions(-) diff --git a/app/api/cron/check-research/route.ts b/app/api/cron/check-research/route.ts index 205e7848..b10b4b9a 100644 --- a/app/api/cron/check-research/route.ts +++ b/app/api/cron/check-research/route.ts @@ -10,6 +10,7 @@ import { ArtifactTypeCode, ArtifactStatus } from '@/lib/services/notebooklm/type import { generateWithGemini, stripCodeFences } from '@/lib/gemini'; import { getConfigValue } from '@/lib/config'; import type { ResearchPayload } from '@/lib/services/research'; +import { writeClient } from '@/lib/sanity-write-client'; // --------------------------------------------------------------------------- // Types @@ -351,20 +352,77 @@ async function stepInfographicsGenerating( return { id: doc._id, title: doc.title, step: 'infographics_generating', outcome: 'still_generating' }; } - // Collect infographic URLs from completed artifacts + // Download and upload infographics to Sanity + interface SanityImageRef { + _type: 'image'; + _key: string; + alt?: string; + asset: { _type: 'reference'; _ref: string }; + } + + const infographicRefs: SanityImageRef[] = []; const infographicUrls: string[] = []; - for (const artifactId of artifactIds) { + + for (let i = 0; i < artifactIds.length; i++) { + const artifactId = artifactIds[i]; try { - const url = await nbClient.getInfographicUrl(notebookId, artifactId); - if (url) { - infographicUrls.push(url); + // Step 1: Get the auth-gated URL + const authUrl = await nbClient.getInfographicUrl(notebookId, artifactId); + if (!authUrl) { + console.warn(`[check-research] No URL for artifact ${artifactId}`); + continue; + } + + // Step 2: Download PNG with NotebookLM auth cookies + const cookies = nbClient.getCookieHeader(); + const imageResponse = await fetch(authUrl, { + headers: { Cookie: cookies }, + redirect: 'follow', + }); + + if (!imageResponse.ok) { + console.warn(`[check-research] Failed to download infographic ${artifactId}: ${imageResponse.status}`); + continue; } + + const contentType = imageResponse.headers.get('content-type') || ''; + if (!contentType.includes('image')) { + console.warn(`[check-research] Infographic ${artifactId} returned non-image: ${contentType}`); + continue; + } + + const arrayBuffer = await imageResponse.arrayBuffer(); + const buffer = Buffer.from(arrayBuffer); + console.log(`[check-research] Downloaded infographic ${i + 1}: ${buffer.length} bytes`); + + // Step 3: Upload to Sanity assets + const filename = `infographic-${doc._id}-${i}.png`; + const asset = await writeClient.assets.upload('image', buffer, { + filename, + contentType: 'image/png', + }); + + console.log(`[check-research] Uploaded to Sanity: ${asset._id}`); + + // Step 4: Build image reference for the array field + const artifact = ourArtifacts.find(a => a.id === artifactId); + infographicRefs.push({ + _type: 'image', + _key: artifactId.slice(0, 8), + alt: artifact?.title || `Research infographic ${i + 1}`, + asset: { _type: 'reference', _ref: asset._id }, + }); + + // Also store the Sanity CDN URL for researchData backward compat + const cdnUrl = `https://cdn.sanity.io/images/${process.env.NEXT_PUBLIC_SANITY_PROJECT_ID}/${process.env.NEXT_PUBLIC_SANITY_DATASET}/${asset._id.replace('image-', '').replace('-png', '.png').replace('-jpg', '.jpg')}`; + infographicUrls.push(cdnUrl); + } catch (err) { - console.warn(`[check-research] Failed to get infographic URL for ${artifactId}:`, err instanceof Error ? err.message : err); + console.warn(`[check-research] Failed to process infographic ${artifactId}:`, err instanceof Error ? err.message : err); } } - console.log(`[check-research] Collected ${infographicUrls.length} infographic URLs`); + console.log(`[check-research] Processed ${infographicRefs.length} infographics (${infographicUrls.length} URLs)`); // Parse existing research data and add infographic URLs let researchData: Record = {}; @@ -377,15 +435,19 @@ async function stepInfographicsGenerating( } researchData.infographicUrls = infographicUrls; - await sanity - .patch(doc._id) - .set({ - status: 'enriching', - researchData: JSON.stringify(researchData), - }) - .commit(); + const patchData: Record = { + status: 'enriching', + researchData: JSON.stringify(researchData), + }; + + // Add infographic image refs if we have any + if (infographicRefs.length > 0) { + patchData.infographics = infographicRefs; + } + + await sanity.patch(doc._id).set(patchData).commit(); - console.log(`[check-research] "${doc.title}" → enriching (${infographicUrls.length} infographic URLs)`); + console.log(`[check-research] "${doc.title}" → enriching (${infographicRefs.length} infographics, ${infographicUrls.length} URLs)`); return { id: doc._id, title: doc.title, step: 'infographics_generating', outcome: 'enriching' }; }