From 6db1a2f01621a1e17fe2f4939f3ac677279564a5 Mon Sep 17 00:00:00 2001 From: Jeff Emmett Date: Tue, 23 Dec 2025 01:24:55 -0500 Subject: [PATCH] feat: switch to RunPod Stable Diffusion for image generation MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Gemini image generation is geo-blocked in Germany (server location). Use RunPod's Automatic1111 endpoint instead for zine page images. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 --- ...sletter-across-all-myco-themed-websites.md | 24 ++++ docker-compose.yml | 1 + lib/gemini.ts | 118 ++++++++++++------ 3 files changed, 102 insertions(+), 41 deletions(-) create mode 100644 backlog/tasks/task-1 - Integrate-newsletter-across-all-myco-themed-websites.md diff --git a/backlog/tasks/task-1 - Integrate-newsletter-across-all-myco-themed-websites.md b/backlog/tasks/task-1 - Integrate-newsletter-across-all-myco-themed-websites.md new file mode 100644 index 0000000..4895b7d --- /dev/null +++ b/backlog/tasks/task-1 - Integrate-newsletter-across-all-myco-themed-websites.md @@ -0,0 +1,24 @@ +--- +id: task-1 +title: Integrate newsletter across all myco-themed websites +status: To Do +assignee: [] +created_date: '2025-12-04 10:23' +labels: [] +dependencies: [] +priority: medium +--- + +## Description + + +Set up consistent newsletter integration (likely Buttondown or similar) across all myco-themed sites: mycofi.earth, mycopunk.xyz, and any related properties. Ensure unified subscriber list and consistent branding. + + +## Acceptance Criteria + +- [ ] #1 Newsletter service selected and configured +- [ ] #2 mycofi.earth has newsletter signup +- [ ] #3 mycopunk.xyz has newsletter signup +- [ ] #4 Subscriber lists unified or properly segmented + diff --git a/docker-compose.yml b/docker-compose.yml index af9c6fd..a2c9868 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -11,6 +11,7 @@ services: - HOSTNAME=0.0.0.0 - PORT=3000 - GEMINI_API_KEY=${GEMINI_API_KEY} + - RUNPOD_API_KEY=${RUNPOD_API_KEY} volumes: - zine-data:/app/data networks: diff --git a/lib/gemini.ts b/lib/gemini.ts index c6cf4d4..acbb0ac 100644 --- a/lib/gemini.ts +++ b/lib/gemini.ts @@ -14,6 +14,69 @@ function getGenAI(): GoogleGenerativeAI { return _genAI; } +// RunPod Stable Diffusion configuration +const RUNPOD_API_KEY = process.env.RUNPOD_API_KEY; +const RUNPOD_SD_ENDPOINT = "tzf1j3sc3zufsy"; // Automatic1111 endpoint + +interface RunPodResponse { + id: string; + status: "IN_QUEUE" | "IN_PROGRESS" | "COMPLETED" | "FAILED"; + output?: { + images?: string[]; + image?: string; + }; + error?: string; +} + +async function generateImageWithRunPod(prompt: string): Promise { + if (!RUNPOD_API_KEY) { + throw new Error("RUNPOD_API_KEY environment variable is not set"); + } + + const runUrl = `https://api.runpod.ai/v2/${RUNPOD_SD_ENDPOINT}/runsync`; + + // Call RunPod Automatic1111 endpoint + const response = await fetch(runUrl, { + method: "POST", + headers: { + "Content-Type": "application/json", + "Authorization": `Bearer ${RUNPOD_API_KEY}`, + }, + body: JSON.stringify({ + input: { + prompt: prompt, + negative_prompt: "blurry, low quality, distorted text, watermark, signature", + width: 512, + height: 768, // Portrait orientation for zine pages + num_inference_steps: 25, + guidance_scale: 7.5, + sampler_name: "DPM++ 2M Karras", + }, + }), + }); + + if (!response.ok) { + const errorText = await response.text(); + console.error("RunPod API error:", response.status, errorText); + throw new Error(`RunPod API error: ${response.status}`); + } + + const result: RunPodResponse = await response.json(); + + if (result.status === "FAILED") { + throw new Error(`RunPod job failed: ${result.error || "Unknown error"}`); + } + + // Extract base64 image from response + const imageData = result.output?.images?.[0] || result.output?.image; + if (!imageData) { + throw new Error("No image data in RunPod response"); + } + + // RunPod returns base64 without prefix + return `data:image/png;base64,${imageData}`; +} + export interface PageOutline { pageNumber: number; type: string; @@ -128,55 +191,28 @@ export async function generatePageImage( tone: string, feedback?: string ): Promise { - // Use Nano Banana Pro for highest quality image generation - // Model: gemini-2.0-flash-exp-image-generation (supports native image output) - const model = getGenAI().getGenerativeModel({ - model: "gemini-2.0-flash-exp-image-generation", - generationConfig: { - // @ts-expect-error - responseModalities is valid but not in types yet - responseModalities: ["IMAGE"], - }, - }); - + // Use RunPod Stable Diffusion for image generation + // (Gemini image gen is geo-blocked in Germany where the server is located) const styleDesc = STYLE_PROMPTS[style] || STYLE_PROMPTS["mycelial"]; const toneDesc = TONE_PROMPTS[tone] || TONE_PROMPTS["regenerative"]; - let imagePrompt = `Create a single page for a mini-zine (approximately 825x1275 pixels aspect ratio, portrait orientation). - -Page ${pageOutline.pageNumber}: ${pageOutline.title} -Type: ${pageOutline.type} -Key elements: ${pageOutline.keyPoints.join(", ")} - -Visual style: ${styleDesc} -Mood/tone: ${toneDesc} - -Specific requirements: -${pageOutline.imagePrompt} - -The image should be a complete, self-contained page that could be printed. Include any text as part of the design in a ${style} typography style.`; + // Build a Stable Diffusion optimized prompt + let imagePrompt = `${pageOutline.title}, ${pageOutline.keyPoints.join(", ")}, ${styleDesc}, ${toneDesc}, ${pageOutline.imagePrompt}, zine page design, printable art, high quality illustration`; if (feedback) { - imagePrompt += `\n\nUser feedback for refinement: ${feedback}`; + imagePrompt += `, ${feedback}`; + } + + // Truncate prompt if too long (SD has token limits) + if (imagePrompt.length > 500) { + imagePrompt = imagePrompt.substring(0, 500); } try { - const result = await model.generateContent(imagePrompt); - const response = result.response; - - // Extract image from response parts - for (const candidate of response.candidates || []) { - for (const part of candidate.content?.parts || []) { - // @ts-expect-error - inlineData exists on image responses - if (part.inlineData) { - // @ts-expect-error - inlineData has data and mimeType - const { data, mimeType } = part.inlineData; - return `data:${mimeType || "image/png"};base64,${data}`; - } - } - } - - // If no image in response, throw error - throw new Error("No image data in response"); + console.log(`Generating image for page ${pageOutline.pageNumber} with RunPod...`); + const imageDataUrl = await generateImageWithRunPod(imagePrompt); + console.log(`Successfully generated image for page ${pageOutline.pageNumber}`); + return imageDataUrl; } catch (error) { console.error("Image generation error:", error); throw new Error(`Failed to generate page image: ${error instanceof Error ? error.message : "Unknown error"}`);