fix: resolve definitivamente erro 400 de Schema no Gemini e erro de geracao de imagem no AI Studio usando generateImages
Build and Deploy (Gitea) / build-and-deploy (push) Successful in 2m8s Details

This commit is contained in:
Sidney 2026-07-17 22:57:48 -03:00
parent 8d1b246f84
commit 961cbfdbdd
1 changed files with 158 additions and 124 deletions

View File

@ -1477,13 +1477,15 @@ Exemplo de formato esperado:
} else { } else {
// Provedor padrão: Google Gemini // Provedor padrão: Google Gemini
let geminiUrl = `https://generativelanguage.googleapis.com/v1beta/models/${modelName}:generateContent?key=${apiKey}`; const tentarChamadaGemini = async (model, usarSchema) => {
const payload = { const url = `https://generativelanguage.googleapis.com/v1beta/models/${model}:generateContent?key=${apiKey}`;
contents: [
{ const payload = {
parts: [ contents: [
{ {
text: `${userContent} parts: [
{
text: `${userContent}
Apenas retorne o JSON estruturado contendo a lista de questões no seguinte formato de array: Apenas retorne o JSON estruturado contendo a lista de questões no seguinte formato de array:
[ [
{ {
@ -1492,102 +1494,101 @@ Apenas retorne o JSON estruturado contendo a lista de questões no seguinte form
"correctOptionIndex": 0 "correctOptionIndex": 0
} }
]` ]`
} }
] ]
}
],
generationConfig: {
responseMimeType: 'application/json',
responseSchema: {
type: 'ARRAY',
items: {
type: 'OBJECT',
properties: {
text: { type: 'STRING' },
options: {
type: 'ARRAY',
items: { type: 'STRING' },
minItems: 4,
maxItems: 4
},
correctOptionIndex: { type: 'INTEGER' }
},
required: ['text', 'options', 'correctOptionIndex']
} }
} ]
};
if (usarSchema) {
payload.generationConfig = {
responseMimeType: 'application/json',
responseSchema: {
type: 'ARRAY',
items: {
type: 'OBJECT',
properties: {
text: { type: 'STRING' },
options: {
type: 'ARRAY',
items: { type: 'STRING' },
minItems: 4,
maxItems: 4
},
correctOptionIndex: { type: 'INTEGER' }
},
required: ['text', 'options', 'correctOptionIndex']
}
}
};
} }
const response = await fetch(url, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(payload)
});
return { response, modelUsed: model, schemaUsed: usarSchema };
}; };
let response = await fetch(geminiUrl, { let resObj = null;
method: 'POST', let modelTentado = modelName;
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify(payload)
});
// Se der 404, tentar API v1 estável // Tentativa 1: Modelo selecionado com Schema estruturado
if (!response.ok && response.status === 404) { try {
console.warn(`[Gemini] v1beta retornou 404 para o modelo ${modelName}. Tentando endpoint /v1/...`); console.log(`[Gemini] Tentando modelo selecionado com Schema: ${modelTentado}`);
geminiUrl = `https://generativelanguage.googleapis.com/v1/models/${modelName}:generateContent?key=${apiKey}`; resObj = await tentarChamadaGemini(modelTentado, true);
response = await fetch(geminiUrl, {
method: 'POST', // Se der erro 400 de Schema/MimeType, re-tentamos o mesmo modelo sem Schema imediatamente
headers: { if (!resObj.response.ok && resObj.response.status === 400) {
'Content-Type': 'application/json' const errText = await resObj.response.clone().text();
}, if (errText.includes('responseMimeType') || errText.includes('responseSchema')) {
body: JSON.stringify(payload) console.warn(`[Gemini] Modelo ${modelTentado} rejeitou Schema. Re-tentando sem Schema...`);
}); resObj = await tentarChamadaGemini(modelTentado, false);
}
}
} catch (err) {
console.warn('[Gemini] Erro na primeira tentativa:', err.message);
} }
// Se ainda assim der 404 e o modelo for diferente de gemini-1.5-flash, tentar com gemini-1.5-flash de fallback universal // Tentativa 2 (Fallback): Se a tentativa 1 deu 404 (modelo inválido/descontinuado) ou outro erro crítico, tentamos gemini-1.5-flash com Schema
if (!response.ok && response.status === 404 && modelName !== 'gemini-1.5-flash') { if (!resObj || !resObj.response.ok) {
console.warn(`[Gemini] Modelo ${modelName} não encontrado no AI Studio. Tentando com fallback universal gemini-1.5-flash no endpoint v1...`); try {
geminiUrl = `https://generativelanguage.googleapis.com/v1/models/gemini-1.5-flash:generateContent?key=${apiKey}`; modelTentado = 'gemini-1.5-flash';
response = await fetch(geminiUrl, { console.log(`[Gemini] Tentando fallback para gemini-1.5-flash com Schema...`);
method: 'POST', resObj = await tentarChamadaGemini(modelTentado, true);
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify(payload)
});
if (!response.ok && response.status === 404) { if (!resObj.response.ok && resObj.response.status === 400) {
// Última tentativa: gemini-1.5-flash no endpoint v1beta const errText = await resObj.response.clone().text();
console.warn(`[Gemini] Tentando gemini-1.5-flash no endpoint v1beta...`); if (errText.includes('responseMimeType') || errText.includes('responseSchema')) {
geminiUrl = `https://generativelanguage.googleapis.com/v1beta/models/gemini-1.5-flash:generateContent?key=${apiKey}`; console.warn('[Gemini] gemini-1.5-flash rejeitou Schema. Re-tentando sem Schema...');
response = await fetch(geminiUrl, { resObj = await tentarChamadaGemini(modelTentado, false);
method: 'POST', }
headers: { }
'Content-Type': 'application/json' } catch (err) {
}, console.warn('[Gemini] Erro no fallback gemini-1.5-flash:', err.message);
body: JSON.stringify(payload)
});
} }
} }
// Se der erro 400 por conta de Structured Outputs incompatíveis com o modelo selecionado, re-tentamos sem eles // Tentativa 3 (Último recurso): Se ainda falhar, tentamos gemini-1.5-flash sem Schema diretamente
let errText = ''; if (!resObj || !resObj.response.ok) {
if (!response.ok && response.status === 400) { try {
errText = await response.text(); modelTentado = 'gemini-1.5-flash';
if (errText.includes('responseMimeType') || errText.includes('responseSchema')) { console.log(`[Gemini] Tentando fallback final para gemini-1.5-flash sem Schema...`);
console.warn(`[Gemini] Modelo ${modelName} não aceita Structured Outputs. Re-tentando chamada sem responseMimeType/Schema...`); resObj = await tentarChamadaGemini(modelTentado, false);
const simplePayload = { } catch (err) {
contents: payload.contents console.warn('[Gemini] Erro no fallback final:', err.message);
};
response = await fetch(geminiUrl, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(simplePayload)
});
} }
} }
if (!response.ok) { if (!resObj || !resObj.response.ok) {
const errorText = errText || await response.text(); const status = resObj ? resObj.response.status : 500;
throw new Error(`Gemini API retornou erro: ${response.status} - ${errorText}`); const errText = resObj ? await resObj.response.text() : 'Erro de rede ou timeout.';
throw new Error(`Gemini API retornou erro: ${status} - ${errText}`);
} }
const resJson = await response.json(); const resJson = await resObj.response.json();
const textResponse = resJson.candidates?.[0]?.content?.parts?.[0]?.text; const textResponse = resJson.candidates?.[0]?.content?.parts?.[0]?.text;
if (!textResponse) { if (!textResponse) {
throw new Error('Retorno vazio da API do Gemini'); throw new Error('Retorno vazio da API do Gemini');
@ -1690,19 +1691,40 @@ app.post('/api/ai/generate-image', async (req, res) => {
imageUrl = resJson.data?.[0]?.url; imageUrl = resJson.data?.[0]?.url;
} }
} else { } else {
// Usar Google Gemini Imagen via API Nativa generateContent com responseModalities // Usar Google Gemini Imagen via API Nativa
const geminiImageModel = aiConfig.geminiImageModel || 'gemini-2.5-flash-image'; const geminiImageModel = aiConfig.geminiImageModel || 'gemini-2.5-flash-image';
const tentarGerarGemini = async (modelName) => { const tentarGerarImagen3 = async (model, promptText) => {
const url = `https://generativelanguage.googleapis.com/v1beta/models/${modelName}:generateContent?key=${apiKey}`; const url = `https://generativelanguage.googleapis.com/v1beta/models/${model}:generateImages?key=${apiKey}`;
const payload = {
prompt: `Ilustração educativa estilo flat vector minimalista, limpa e com fundo neutro sobre o tema: ${promptText}`,
numberOfImages: 1,
outputMimeType: 'image/jpeg',
aspectRatio: '1:1'
};
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 Imagen 3 HTTP ${response.status}: ${errText}`);
}
const resJson = await response.json();
const base64 = resJson.generatedImages?.[0]?.image?.imageBytes;
return base64;
};
const tentarGerarMultimodal = async (model, promptText) => {
const url = `https://generativelanguage.googleapis.com/v1beta/models/${model}:generateContent?key=${apiKey}`;
const payload = { const payload = {
contents: [ contents: [
{ {
parts: [ parts: [{ text: `Gere uma ilustração educativa, estilo flat vector minimalista, limpa e com fundo neutro sobre o tema: ${promptText}` }]
{
text: `Gere uma ilustração educativa, estilo flat vector minimalista, limpa e com fundo neutro sobre o tema: ${prompt}`
}
]
} }
], ],
generationConfig: { generationConfig: {
@ -1718,11 +1740,10 @@ app.post('/api/ai/generate-image', async (req, res) => {
if (!response.ok) { if (!response.ok) {
const errText = await response.text(); const errText = await response.text();
throw new Error(`Erro HTTP ${response.status}: ${errText}`); throw new Error(`Erro Multimodal HTTP ${response.status}: ${errText}`);
} }
const resJson = await response.json(); const resJson = await response.json();
let base64 = null; let base64 = null;
if (resJson.candidates?.[0]?.content?.parts) { if (resJson.candidates?.[0]?.content?.parts) {
for (const part of resJson.candidates[0].content.parts) { for (const part of resJson.candidates[0].content.parts) {
@ -1736,50 +1757,63 @@ app.post('/api/ai/generate-image', async (req, res) => {
}; };
let base64ImageBytes = null; let base64ImageBytes = null;
let ultimoErro = '';
let modelUsed = geminiImageModel; let modelUsed = geminiImageModel;
try { // Tentativa 1: Modelo selecionado
console.log(`[Gemini Imagen] Tentando gerar com o modelo selecionado: ${modelUsed}`); if (modelUsed.startsWith('imagen-')) {
base64ImageBytes = await tentarGerarGemini(modelUsed); try {
} catch (err) { console.log(`[Gemini Imagen] Tentando gerar com o modelo Imagen 3: ${modelUsed}`);
console.warn(`[Gemini Imagen] Falha no modelo principal ${modelUsed}:`, err.message); base64ImageBytes = await tentarGerarImagen3(modelUsed, prompt);
} catch (err) {
ultimoErro = err.message;
console.warn(`[Gemini Imagen] Falha no Imagen 3 (${modelUsed}):`, ultimoErro);
}
} else {
try {
console.log(`[Gemini Imagen] Tentando gerar com o modelo Multimodal: ${modelUsed}`);
base64ImageBytes = await tentarGerarMultimodal(modelUsed, prompt);
} catch (err) {
ultimoErro = err.message;
console.warn(`[Gemini Imagen] Falha no Multimodal (${modelUsed}):`, ultimoErro);
}
} }
// Fallback 1: gemini-2.5-flash-image // Fallback 1: imagen-3.0-generate-002 estável
if (!base64ImageBytes && modelUsed !== 'imagen-3.0-generate-002') {
try {
console.log('[Gemini Imagen] Tentando fallback para imagen-3.0-generate-002 estável...');
base64ImageBytes = await tentarGerarImagen3('imagen-3.0-generate-002', prompt);
} catch (err) {
ultimoErro = err.message;
console.warn('[Gemini Imagen] Falha no fallback imagen-3.0-generate-002:', ultimoErro);
}
}
// Fallback 2: gemini-2.5-flash-image
if (!base64ImageBytes && modelUsed !== 'gemini-2.5-flash-image') { if (!base64ImageBytes && modelUsed !== 'gemini-2.5-flash-image') {
try { try {
modelUsed = 'gemini-2.5-flash-image'; console.log('[Gemini Imagen] Tentando fallback para gemini-2.5-flash-image...');
console.log(`[Gemini Imagen] Tentando fallback para: ${modelUsed}`); base64ImageBytes = await tentarGerarMultimodal('gemini-2.5-flash-image', prompt);
base64ImageBytes = await tentarGerarGemini(modelUsed);
} catch (err) { } catch (err) {
console.warn(`[Gemini Imagen] Falha no fallback ${modelUsed}:`, err.message); ultimoErro = err.message;
console.warn('[Gemini Imagen] Falha no fallback gemini-2.5-flash-image:', ultimoErro);
} }
} }
// Fallback 2: gemini-3.1-flash-image // Fallback 3: gemini-3.1-flash-image
if (!base64ImageBytes && modelUsed !== 'gemini-3.1-flash-image') { if (!base64ImageBytes && modelUsed !== 'gemini-3.1-flash-image') {
try { try {
modelUsed = 'gemini-3.1-flash-image'; console.log('[Gemini Imagen] Tentando fallback para gemini-3.1-flash-image...');
console.log(`[Gemini Imagen] Tentando fallback para: ${modelUsed}`); base64ImageBytes = await tentarGerarMultimodal('gemini-3.1-flash-image', prompt);
base64ImageBytes = await tentarGerarGemini(modelUsed);
} catch (err) { } catch (err) {
console.warn(`[Gemini Imagen] Falha no fallback ${modelUsed}:`, err.message); ultimoErro = err.message;
} console.warn('[Gemini Imagen] Falha no fallback gemini-3.1-flash-image:', ultimoErro);
}
// 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);
} }
} }
if (!base64ImageBytes) { 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.'); throw new Error(`Não foi possível gerar a imagem em nenhum dos modelos Gemini/Imagen disponíveis. Detalhes do último erro: ${ultimoErro}`);
} }
const imageBuffer = Buffer.from(base64ImageBytes, 'base64'); const imageBuffer = Buffer.from(base64ImageBytes, 'base64');