feat: route MycroZine image generation through RunPod proxy
- Updated generatePageImage to use zine.jeffemmett.com API - Removed direct Gemini API calls (were geo-blocked in EU) - Now uses RunPod US-based proxy for reliable image generation - Fixed TypeScript types for API responses 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
parent
8cf0bad804
commit
7bfc6ff576
|
|
@ -0,0 +1 @@
|
||||||
|
if('serviceWorker' in navigator) navigator.serviceWorker.register('/dev-sw.js?dev-sw', { scope: '/', type: 'classic' })
|
||||||
|
|
@ -0,0 +1,114 @@
|
||||||
|
/**
|
||||||
|
* Copyright 2018 Google Inc. All Rights Reserved.
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
// If the loader is already loaded, just stop.
|
||||||
|
if (!self.define) {
|
||||||
|
let registry = {};
|
||||||
|
|
||||||
|
// Used for `eval` and `importScripts` where we can't get script URL by other means.
|
||||||
|
// In both cases, it's safe to use a global var because those functions are synchronous.
|
||||||
|
let nextDefineUri;
|
||||||
|
|
||||||
|
const singleRequire = (uri, parentUri) => {
|
||||||
|
uri = new URL(uri + ".js", parentUri).href;
|
||||||
|
return registry[uri] || (
|
||||||
|
|
||||||
|
new Promise(resolve => {
|
||||||
|
if ("document" in self) {
|
||||||
|
const script = document.createElement("script");
|
||||||
|
script.src = uri;
|
||||||
|
script.onload = resolve;
|
||||||
|
document.head.appendChild(script);
|
||||||
|
} else {
|
||||||
|
nextDefineUri = uri;
|
||||||
|
importScripts(uri);
|
||||||
|
resolve();
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
.then(() => {
|
||||||
|
let promise = registry[uri];
|
||||||
|
if (!promise) {
|
||||||
|
throw new Error(`Module ${uri} didn’t register its module`);
|
||||||
|
}
|
||||||
|
return promise;
|
||||||
|
})
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
self.define = (depsNames, factory) => {
|
||||||
|
const uri = nextDefineUri || ("document" in self ? document.currentScript.src : "") || location.href;
|
||||||
|
if (registry[uri]) {
|
||||||
|
// Module is already loading or loaded.
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
let exports = {};
|
||||||
|
const require = depUri => singleRequire(depUri, uri);
|
||||||
|
const specialDeps = {
|
||||||
|
module: { uri },
|
||||||
|
exports,
|
||||||
|
require
|
||||||
|
};
|
||||||
|
registry[uri] = Promise.all(depsNames.map(
|
||||||
|
depName => specialDeps[depName] || require(depName)
|
||||||
|
)).then(deps => {
|
||||||
|
factory(...deps);
|
||||||
|
return exports;
|
||||||
|
});
|
||||||
|
};
|
||||||
|
}
|
||||||
|
define(['./workbox-52f2a342'], (function (workbox) { 'use strict';
|
||||||
|
|
||||||
|
self.skipWaiting();
|
||||||
|
workbox.clientsClaim();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The precacheAndRoute() method efficiently caches and responds to
|
||||||
|
* requests for URLs in the manifest.
|
||||||
|
* See https://goo.gl/S9QRab
|
||||||
|
*/
|
||||||
|
workbox.precacheAndRoute([{
|
||||||
|
"url": "registerSW.js",
|
||||||
|
"revision": "3ca0b8505b4bec776b69afdba2768812"
|
||||||
|
}, {
|
||||||
|
"url": "index.html",
|
||||||
|
"revision": "0.8vbqrp48ceg"
|
||||||
|
}], {});
|
||||||
|
workbox.cleanupOutdatedCaches();
|
||||||
|
workbox.registerRoute(new workbox.NavigationRoute(workbox.createHandlerBoundToURL("index.html"), {
|
||||||
|
allowlist: [/^\/$/]
|
||||||
|
}));
|
||||||
|
workbox.registerRoute(/^https?:\/\/.*\/api\/.*/i, new workbox.NetworkFirst({
|
||||||
|
"cacheName": "api-cache",
|
||||||
|
"networkTimeoutSeconds": 10,
|
||||||
|
plugins: [new workbox.ExpirationPlugin({
|
||||||
|
maxEntries: 100,
|
||||||
|
maxAgeSeconds: 86400
|
||||||
|
})]
|
||||||
|
}), 'GET');
|
||||||
|
workbox.registerRoute(/^https:\/\/fonts\.googleapis\.com\/.*/i, new workbox.CacheFirst({
|
||||||
|
"cacheName": "google-fonts-cache",
|
||||||
|
plugins: [new workbox.ExpirationPlugin({
|
||||||
|
maxEntries: 10,
|
||||||
|
maxAgeSeconds: 31536000
|
||||||
|
})]
|
||||||
|
}), 'GET');
|
||||||
|
workbox.registerRoute(/^https:\/\/fonts\.gstatic\.com\/.*/i, new workbox.CacheFirst({
|
||||||
|
"cacheName": "gstatic-fonts-cache",
|
||||||
|
plugins: [new workbox.ExpirationPlugin({
|
||||||
|
maxEntries: 10,
|
||||||
|
maxAgeSeconds: 31536000
|
||||||
|
})]
|
||||||
|
}), 'GET');
|
||||||
|
|
||||||
|
}));
|
||||||
File diff suppressed because it is too large
Load Diff
|
|
@ -11,7 +11,7 @@ import React, { useState, useRef, useEffect } from "react"
|
||||||
import { StandardizedToolWrapper } from "@/components/StandardizedToolWrapper"
|
import { StandardizedToolWrapper } from "@/components/StandardizedToolWrapper"
|
||||||
import { usePinnedToView } from "@/hooks/usePinnedToView"
|
import { usePinnedToView } from "@/hooks/usePinnedToView"
|
||||||
import { useMaximize } from "@/hooks/useMaximize"
|
import { useMaximize } from "@/hooks/useMaximize"
|
||||||
import { getGeminiConfig } from "@/lib/clientConfig"
|
// Note: Image generation now uses zine.jeffemmett.com API which proxies through RunPod
|
||||||
|
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
// Types
|
// Types
|
||||||
|
|
@ -1059,156 +1059,58 @@ Tone: ${toneDesc}
|
||||||
High contrast black and white with neon green accent highlights. Xerox texture, DIY cut-and-paste collage aesthetic, rough edges, rebellious feel.`
|
High contrast black and white with neon green accent highlights. Xerox texture, DIY cut-and-paste collage aesthetic, rough edges, rebellious feel.`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
interface GenerateImageResponse {
|
||||||
|
success: boolean
|
||||||
|
imageData?: string
|
||||||
|
mimeType?: string
|
||||||
|
error?: string
|
||||||
|
}
|
||||||
|
|
||||||
async function generatePageImage(prompt: string, pageNumber: number): Promise<string> {
|
async function generatePageImage(prompt: string, pageNumber: number): Promise<string> {
|
||||||
console.log(`🍄 Generating page ${pageNumber} with Gemini Nano Banana Pro...`)
|
console.log(`🍄 Generating page ${pageNumber} via RunPod proxy...`)
|
||||||
console.log(`📝 Prompt preview:`, prompt.substring(0, 100) + '...')
|
console.log(`📝 Prompt preview:`, prompt.substring(0, 100) + '...')
|
||||||
|
|
||||||
const geminiConfig = getGeminiConfig()
|
// Use the mycro-zine API which proxies through RunPod (US-based, bypasses geo-restrictions)
|
||||||
|
const ZINE_API_URL = 'https://zine.jeffemmett.com/api/generate-image'
|
||||||
|
|
||||||
if (!geminiConfig) {
|
|
||||||
console.warn('⚠️ No Gemini API key configured, using placeholder')
|
|
||||||
return `https://via.placeholder.com/825x1275/1a1a1a/00ff00?text=Page+${pageNumber}+%28No+API+Key%29`
|
|
||||||
}
|
|
||||||
|
|
||||||
const { apiKey } = geminiConfig
|
|
||||||
|
|
||||||
// Try Nano Banana Pro first (gemini-2.0-flash-exp-image-generation)
|
|
||||||
try {
|
try {
|
||||||
const imageUrl = await generateWithNanoBananaPro(prompt, apiKey)
|
const response = await fetch(ZINE_API_URL, {
|
||||||
if (imageUrl) {
|
|
||||||
console.log(`✅ Page ${pageNumber} generated with Nano Banana Pro`)
|
|
||||||
return imageUrl
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
console.error(`❌ Nano Banana Pro failed for page ${pageNumber}:`, error)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Fallback to Gemini 2.0 Flash experimental
|
|
||||||
try {
|
|
||||||
const imageUrl = await generateWithGemini2Flash(prompt, apiKey)
|
|
||||||
if (imageUrl) {
|
|
||||||
console.log(`✅ Page ${pageNumber} generated with Gemini 2.0 Flash`)
|
|
||||||
return imageUrl
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
console.error(`❌ Gemini 2.0 Flash failed for page ${pageNumber}:`, error)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Final fallback: placeholder
|
|
||||||
console.warn(`⚠️ All generation methods failed for page ${pageNumber}, using placeholder`)
|
|
||||||
return `https://via.placeholder.com/825x1275/1a1a1a/00ff00?text=Page+${pageNumber}+%28Generation+Failed%29`
|
|
||||||
}
|
|
||||||
|
|
||||||
// Types for Gemini API response
|
|
||||||
interface GeminiPart {
|
|
||||||
text?: string
|
|
||||||
inlineData?: {
|
|
||||||
mimeType: string
|
|
||||||
data: string
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
interface GeminiCandidate {
|
|
||||||
content?: {
|
|
||||||
parts?: GeminiPart[]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
interface GeminiResponse {
|
|
||||||
candidates?: GeminiCandidate[]
|
|
||||||
}
|
|
||||||
|
|
||||||
// Nano Banana Pro - highest quality, excellent text rendering
|
|
||||||
async function generateWithNanoBananaPro(prompt: string, apiKey: string): Promise<string | null> {
|
|
||||||
const response = await fetch(
|
|
||||||
`https://generativelanguage.googleapis.com/v1beta/models/gemini-2.0-flash-exp-image-generation:generateContent?key=${apiKey}`,
|
|
||||||
{
|
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: {
|
headers: {
|
||||||
'Content-Type': 'application/json',
|
'Content-Type': 'application/json',
|
||||||
},
|
},
|
||||||
body: JSON.stringify({
|
body: JSON.stringify({ prompt }),
|
||||||
contents: [
|
})
|
||||||
{
|
|
||||||
parts: [
|
if (!response.ok) {
|
||||||
{
|
let errorMessage = `HTTP ${response.status}`
|
||||||
text: prompt,
|
try {
|
||||||
},
|
const errorData = await response.json() as { error?: string }
|
||||||
],
|
if (errorData.error) errorMessage = errorData.error
|
||||||
},
|
} catch {
|
||||||
],
|
// Ignore JSON parse errors
|
||||||
generationConfig: {
|
}
|
||||||
responseModalities: ['IMAGE'],
|
console.error(`❌ API error for page ${pageNumber}:`, response.status, errorMessage)
|
||||||
},
|
throw new Error(errorMessage)
|
||||||
}),
|
|
||||||
}
|
}
|
||||||
)
|
|
||||||
|
|
||||||
if (!response.ok) {
|
const data: GenerateImageResponse = await response.json()
|
||||||
const errorText = await response.text()
|
|
||||||
console.error('Nano Banana Pro API error:', response.status, errorText)
|
|
||||||
return null
|
|
||||||
}
|
|
||||||
|
|
||||||
const data: GeminiResponse = await response.json()
|
if (data.success && data.imageData) {
|
||||||
|
console.log(`✅ Page ${pageNumber} generated via RunPod proxy`)
|
||||||
// Extract image from response
|
|
||||||
const parts = data.candidates?.[0]?.content?.parts || []
|
|
||||||
for (const part of parts) {
|
|
||||||
if (part.inlineData?.mimeType?.startsWith('image/')) {
|
|
||||||
// Convert base64 to data URL
|
// Convert base64 to data URL
|
||||||
return `data:${part.inlineData.mimeType};base64,${part.inlineData.data}`
|
return `data:${data.mimeType || 'image/png'};base64,${data.imageData}`
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
return null
|
throw new Error('No image data in response')
|
||||||
|
} catch (error) {
|
||||||
|
console.error(`❌ Generation failed for page ${pageNumber}:`, error)
|
||||||
|
// Fallback to placeholder
|
||||||
|
return `https://via.placeholder.com/825x1275/1a1a1a/00ff00?text=Page+${pageNumber}+%28Generation+Failed%29`
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Gemini 2.0 Flash experimental fallback
|
// Removed direct Gemini API functions - now using zine.jeffemmett.com proxy
|
||||||
async function generateWithGemini2Flash(prompt: string, apiKey: string): Promise<string | null> {
|
|
||||||
const response = await fetch(
|
|
||||||
`https://generativelanguage.googleapis.com/v1beta/models/gemini-2.0-flash-exp:generateContent?key=${apiKey}`,
|
|
||||||
{
|
|
||||||
method: 'POST',
|
|
||||||
headers: {
|
|
||||||
'Content-Type': 'application/json',
|
|
||||||
},
|
|
||||||
body: JSON.stringify({
|
|
||||||
contents: [
|
|
||||||
{
|
|
||||||
parts: [
|
|
||||||
{
|
|
||||||
text: `Generate an image: ${prompt}`,
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
],
|
|
||||||
generationConfig: {
|
|
||||||
responseModalities: ['IMAGE', 'TEXT'],
|
|
||||||
responseMimeType: 'image/png',
|
|
||||||
},
|
|
||||||
}),
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
if (!response.ok) {
|
|
||||||
const errorText = await response.text()
|
|
||||||
console.error('Gemini 2.0 Flash error:', response.status, errorText)
|
|
||||||
return null
|
|
||||||
}
|
|
||||||
|
|
||||||
const data: GeminiResponse = await response.json()
|
|
||||||
|
|
||||||
// Extract image from response
|
|
||||||
const parts = data.candidates?.[0]?.content?.parts || []
|
|
||||||
for (const part of parts) {
|
|
||||||
if (part.inlineData?.mimeType?.startsWith('image/')) {
|
|
||||||
return `data:${part.inlineData.mimeType};base64,${part.inlineData.data}`
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return null
|
|
||||||
}
|
|
||||||
|
|
||||||
async function spawnPageOnCanvas(
|
async function spawnPageOnCanvas(
|
||||||
editor: any,
|
editor: any,
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue