From eb70433ae1cfeaac10d5b1edc729544f9d696ed9 Mon Sep 17 00:00:00 2001 From: Sidney Date: Fri, 17 Jul 2026 22:36:52 -0300 Subject: [PATCH] fix: altera geracao de imagem do gemini para generateContent nativo com responseModalities --- manager/server.selfhosted.js | 128 +++++++++++++++++++++++------------ 1 file changed, 84 insertions(+), 44 deletions(-) diff --git a/manager/server.selfhosted.js b/manager/server.selfhosted.js index e883274..93e94bc 100644 --- a/manager/server.selfhosted.js +++ b/manager/server.selfhosted.js @@ -1661,62 +1661,102 @@ app.post('/api/ai/generate-image', async (req, res) => { imageUrl = resJson.data?.[0]?.url; } } else { - // Usar Google Gemini Imagen via OpenAI Compatibility Layer + // Usar Google Gemini Imagen via API Nativa generateContent com responseModalities const geminiImageModel = aiConfig.geminiImageModel || 'gemini-2.5-flash-image'; - const imagenUrl = `https://generativelanguage.googleapis.com/v1beta/openai/images/generations?key=${apiKey}`; - const payload = { - prompt: `Ilustração educativa flat vector sobre: ${prompt}`, - n: 1, - size: '1024x1024', - model: geminiImageModel + + const tentarGerarGemini = async (modelName) => { + const url = `https://generativelanguage.googleapis.com/v1beta/models/${modelName}:generateContent?key=${apiKey}`; + const payload = { + contents: [ + { + parts: [ + { + text: `Gere uma ilustração educativa, estilo flat vector minimalista, limpa e com fundo neutro sobre o tema: ${prompt}` + } + ] + } + ], + generationConfig: { + responseModalities: ["TEXT", "IMAGE"] + } + }; + + const response = await fetch(url, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify(payload) + }); + + if (!response.ok) { + const errText = await response.text(); + throw new Error(`Erro HTTP ${response.status}: ${errText}`); + } + + const resJson = await response.json(); + + let base64 = null; + if (resJson.candidates?.[0]?.content?.parts) { + for (const part of resJson.candidates[0].content.parts) { + if (part.inlineData && part.inlineData.data) { + base64 = part.inlineData.data; + break; + } + } + } + return base64; }; - let response = await fetch(imagenUrl, { - method: 'POST', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify(payload) - }); + let base64ImageBytes = null; + let modelUsed = geminiImageModel; - // Se der 404, tentar com o modelo gemini-2.5-flash-image de fallback - if (!response.ok && response.status === 404 && geminiImageModel !== 'gemini-2.5-flash-image') { - console.warn(`[Gemini Imagen] Modelo ${geminiImageModel} retornou 404, tentando gemini-2.5-flash-image de fallback...`); - payload.model = 'gemini-2.5-flash-image'; - response = await fetch(imagenUrl, { - method: 'POST', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify(payload) - }); + try { + console.log(`[Gemini Imagen] Tentando gerar com o modelo selecionado: ${modelUsed}`); + base64ImageBytes = await tentarGerarGemini(modelUsed); + } catch (err) { + console.warn(`[Gemini Imagen] Falha no modelo principal ${modelUsed}:`, err.message); } - // Se der 404 novamente, tentar com gemini-3.1-flash-image - if (!response.ok && response.status === 404 && payload.model !== 'gemini-3.1-flash-image') { - console.warn(`[Gemini Imagen] Tentando gemini-3.1-flash-image de fallback...`); - payload.model = 'gemini-3.1-flash-image'; - response = await fetch(imagenUrl, { - method: 'POST', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify(payload) - }); + // Fallback 1: gemini-2.5-flash-image + if (!base64ImageBytes && modelUsed !== 'gemini-2.5-flash-image') { + try { + modelUsed = 'gemini-2.5-flash-image'; + console.log(`[Gemini Imagen] Tentando fallback para: ${modelUsed}`); + base64ImageBytes = await tentarGerarGemini(modelUsed); + } catch (err) { + console.warn(`[Gemini Imagen] Falha no fallback ${modelUsed}:`, err.message); + } } - // Fallback final para imagen-3.0-generate-002 se as flash image falharem - if (!response.ok && response.status === 404 && payload.model !== 'imagen-3.0-generate-002') { - console.warn(`[Gemini Imagen] Tentando imagen-3.0-generate-002 de fallback final...`); - payload.model = 'imagen-3.0-generate-002'; - response = await fetch(imagenUrl, { - method: 'POST', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify(payload) - }); + // Fallback 2: gemini-3.1-flash-image + if (!base64ImageBytes && modelUsed !== 'gemini-3.1-flash-image') { + try { + modelUsed = 'gemini-3.1-flash-image'; + console.log(`[Gemini Imagen] Tentando fallback para: ${modelUsed}`); + base64ImageBytes = await tentarGerarGemini(modelUsed); + } catch (err) { + console.warn(`[Gemini Imagen] Falha no fallback ${modelUsed}:`, err.message); + } } - if (!response.ok) { - const errText = await response.text(); - throw new Error(`Gemini Imagen retornou erro: ${response.status} - ${errText}`); + // Fallback 3: gemini-2.0-flash-exp (experimental) + if (!base64ImageBytes && modelUsed !== 'gemini-2.0-flash-exp') { + try { + modelUsed = 'gemini-2.0-flash-exp'; + console.log(`[Gemini Imagen] Tentando fallback para: ${modelUsed}`); + base64ImageBytes = await tentarGerarGemini(modelUsed); + } catch (err) { + console.warn(`[Gemini Imagen] Falha no fallback ${modelUsed}:`, err.message); + } } - const resJson = await response.json(); - imageUrl = resJson.data?.[0]?.url; + if (!base64ImageBytes) { + throw new Error('Não foi possível gerar a imagem em nenhum dos modelos Gemini disponíveis. Verifique sua chave de API ou se a cota foi atingida.'); + } + + const imageBuffer = Buffer.from(base64ImageBytes, 'base64'); + const { uploadExamImage } = await import('./services/storage.js'); + const savedUrl = await uploadExamImage(imageBuffer, 'image/png'); + return res.json({ url: savedUrl }); } if (!imageUrl) {