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
Build and Deploy (Gitea) / build-and-deploy (push) Successful in 2m8s
Details
This commit is contained in:
parent
8d1b246f84
commit
961cbfdbdd
|
|
@ -1477,7 +1477,9 @@ Exemplo de formato esperado:
|
|||
|
||||
} else {
|
||||
// Provedor padrão: Google Gemini
|
||||
let geminiUrl = `https://generativelanguage.googleapis.com/v1beta/models/${modelName}:generateContent?key=${apiKey}`;
|
||||
const tentarChamadaGemini = async (model, usarSchema) => {
|
||||
const url = `https://generativelanguage.googleapis.com/v1beta/models/${model}:generateContent?key=${apiKey}`;
|
||||
|
||||
const payload = {
|
||||
contents: [
|
||||
{
|
||||
|
|
@ -1495,8 +1497,11 @@ Apenas retorne o JSON estruturado contendo a lista de questões no seguinte form
|
|||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
generationConfig: {
|
||||
]
|
||||
};
|
||||
|
||||
if (usarSchema) {
|
||||
payload.generationConfig = {
|
||||
responseMimeType: 'application/json',
|
||||
responseSchema: {
|
||||
type: 'ARRAY',
|
||||
|
|
@ -1515,79 +1520,75 @@ Apenas retorne o JSON estruturado contendo a lista de questões no seguinte form
|
|||
required: ['text', 'options', 'correctOptionIndex']
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
let response = await fetch(geminiUrl, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
body: JSON.stringify(payload)
|
||||
});
|
||||
|
||||
// Se der 404, tentar API v1 estável
|
||||
if (!response.ok && response.status === 404) {
|
||||
console.warn(`[Gemini] v1beta retornou 404 para o modelo ${modelName}. Tentando endpoint /v1/...`);
|
||||
geminiUrl = `https://generativelanguage.googleapis.com/v1/models/${modelName}:generateContent?key=${apiKey}`;
|
||||
response = await fetch(geminiUrl, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
body: JSON.stringify(payload)
|
||||
});
|
||||
}
|
||||
|
||||
// Se ainda assim der 404 e o modelo for diferente de gemini-1.5-flash, tentar com gemini-1.5-flash de fallback universal
|
||||
if (!response.ok && response.status === 404 && modelName !== 'gemini-1.5-flash') {
|
||||
console.warn(`[Gemini] Modelo ${modelName} não encontrado no AI Studio. Tentando com fallback universal gemini-1.5-flash no endpoint v1...`);
|
||||
geminiUrl = `https://generativelanguage.googleapis.com/v1/models/gemini-1.5-flash:generateContent?key=${apiKey}`;
|
||||
response = await fetch(geminiUrl, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
body: JSON.stringify(payload)
|
||||
});
|
||||
|
||||
if (!response.ok && response.status === 404) {
|
||||
// Última tentativa: gemini-1.5-flash no endpoint v1beta
|
||||
console.warn(`[Gemini] Tentando gemini-1.5-flash no endpoint v1beta...`);
|
||||
geminiUrl = `https://generativelanguage.googleapis.com/v1beta/models/gemini-1.5-flash:generateContent?key=${apiKey}`;
|
||||
response = await fetch(geminiUrl, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
body: JSON.stringify(payload)
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Se der erro 400 por conta de Structured Outputs incompatíveis com o modelo selecionado, re-tentamos sem eles
|
||||
let errText = '';
|
||||
if (!response.ok && response.status === 400) {
|
||||
errText = await response.text();
|
||||
if (errText.includes('responseMimeType') || errText.includes('responseSchema')) {
|
||||
console.warn(`[Gemini] Modelo ${modelName} não aceita Structured Outputs. Re-tentando chamada sem responseMimeType/Schema...`);
|
||||
const simplePayload = {
|
||||
contents: payload.contents
|
||||
};
|
||||
response = await fetch(geminiUrl, {
|
||||
const response = await fetch(url, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify(simplePayload)
|
||||
body: JSON.stringify(payload)
|
||||
});
|
||||
|
||||
return { response, modelUsed: model, schemaUsed: usarSchema };
|
||||
};
|
||||
|
||||
let resObj = null;
|
||||
let modelTentado = modelName;
|
||||
|
||||
// Tentativa 1: Modelo selecionado com Schema estruturado
|
||||
try {
|
||||
console.log(`[Gemini] Tentando modelo selecionado com Schema: ${modelTentado}`);
|
||||
resObj = await tentarChamadaGemini(modelTentado, true);
|
||||
|
||||
// Se der erro 400 de Schema/MimeType, re-tentamos o mesmo modelo sem Schema imediatamente
|
||||
if (!resObj.response.ok && resObj.response.status === 400) {
|
||||
const errText = await resObj.response.clone().text();
|
||||
if (errText.includes('responseMimeType') || errText.includes('responseSchema')) {
|
||||
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);
|
||||
}
|
||||
|
||||
// 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 (!resObj || !resObj.response.ok) {
|
||||
try {
|
||||
modelTentado = 'gemini-1.5-flash';
|
||||
console.log(`[Gemini] Tentando fallback para gemini-1.5-flash com Schema...`);
|
||||
resObj = await tentarChamadaGemini(modelTentado, true);
|
||||
|
||||
if (!resObj.response.ok && resObj.response.status === 400) {
|
||||
const errText = await resObj.response.clone().text();
|
||||
if (errText.includes('responseMimeType') || errText.includes('responseSchema')) {
|
||||
console.warn('[Gemini] gemini-1.5-flash rejeitou Schema. Re-tentando sem Schema...');
|
||||
resObj = await tentarChamadaGemini(modelTentado, false);
|
||||
}
|
||||
}
|
||||
} catch (err) {
|
||||
console.warn('[Gemini] Erro no fallback gemini-1.5-flash:', err.message);
|
||||
}
|
||||
}
|
||||
|
||||
if (!response.ok) {
|
||||
const errorText = errText || await response.text();
|
||||
throw new Error(`Gemini API retornou erro: ${response.status} - ${errorText}`);
|
||||
// Tentativa 3 (Último recurso): Se ainda falhar, tentamos gemini-1.5-flash sem Schema diretamente
|
||||
if (!resObj || !resObj.response.ok) {
|
||||
try {
|
||||
modelTentado = 'gemini-1.5-flash';
|
||||
console.log(`[Gemini] Tentando fallback final para gemini-1.5-flash sem Schema...`);
|
||||
resObj = await tentarChamadaGemini(modelTentado, false);
|
||||
} catch (err) {
|
||||
console.warn('[Gemini] Erro no fallback final:', err.message);
|
||||
}
|
||||
}
|
||||
|
||||
const resJson = await response.json();
|
||||
if (!resObj || !resObj.response.ok) {
|
||||
const status = resObj ? resObj.response.status : 500;
|
||||
const errText = resObj ? await resObj.response.text() : 'Erro de rede ou timeout.';
|
||||
throw new Error(`Gemini API retornou erro: ${status} - ${errText}`);
|
||||
}
|
||||
|
||||
const resJson = await resObj.response.json();
|
||||
const textResponse = resJson.candidates?.[0]?.content?.parts?.[0]?.text;
|
||||
if (!textResponse) {
|
||||
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;
|
||||
}
|
||||
} 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 tentarGerarGemini = async (modelName) => {
|
||||
const url = `https://generativelanguage.googleapis.com/v1beta/models/${modelName}:generateContent?key=${apiKey}`;
|
||||
const tentarGerarImagen3 = async (model, promptText) => {
|
||||
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 = {
|
||||
contents: [
|
||||
{
|
||||
parts: [
|
||||
{
|
||||
text: `Gere uma ilustração educativa, estilo flat vector minimalista, limpa e com fundo neutro sobre o tema: ${prompt}`
|
||||
}
|
||||
]
|
||||
parts: [{ text: `Gere uma ilustração educativa, estilo flat vector minimalista, limpa e com fundo neutro sobre o tema: ${promptText}` }]
|
||||
}
|
||||
],
|
||||
generationConfig: {
|
||||
|
|
@ -1718,11 +1740,10 @@ app.post('/api/ai/generate-image', async (req, res) => {
|
|||
|
||||
if (!response.ok) {
|
||||
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();
|
||||
|
||||
let base64 = null;
|
||||
if (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 ultimoErro = '';
|
||||
let modelUsed = geminiImageModel;
|
||||
|
||||
// Tentativa 1: Modelo selecionado
|
||||
if (modelUsed.startsWith('imagen-')) {
|
||||
try {
|
||||
console.log(`[Gemini Imagen] Tentando gerar com o modelo selecionado: ${modelUsed}`);
|
||||
base64ImageBytes = await tentarGerarGemini(modelUsed);
|
||||
console.log(`[Gemini Imagen] Tentando gerar com o modelo Imagen 3: ${modelUsed}`);
|
||||
base64ImageBytes = await tentarGerarImagen3(modelUsed, prompt);
|
||||
} catch (err) {
|
||||
console.warn(`[Gemini Imagen] Falha no modelo principal ${modelUsed}:`, err.message);
|
||||
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') {
|
||||
try {
|
||||
modelUsed = 'gemini-2.5-flash-image';
|
||||
console.log(`[Gemini Imagen] Tentando fallback para: ${modelUsed}`);
|
||||
base64ImageBytes = await tentarGerarGemini(modelUsed);
|
||||
console.log('[Gemini Imagen] Tentando fallback para gemini-2.5-flash-image...');
|
||||
base64ImageBytes = await tentarGerarMultimodal('gemini-2.5-flash-image', prompt);
|
||||
} 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') {
|
||||
try {
|
||||
modelUsed = 'gemini-3.1-flash-image';
|
||||
console.log(`[Gemini Imagen] Tentando fallback para: ${modelUsed}`);
|
||||
base64ImageBytes = await tentarGerarGemini(modelUsed);
|
||||
console.log('[Gemini Imagen] Tentando fallback para gemini-3.1-flash-image...');
|
||||
base64ImageBytes = await tentarGerarMultimodal('gemini-3.1-flash-image', prompt);
|
||||
} catch (err) {
|
||||
console.warn(`[Gemini Imagen] Falha no fallback ${modelUsed}:`, err.message);
|
||||
}
|
||||
}
|
||||
|
||||
// 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);
|
||||
ultimoErro = err.message;
|
||||
console.warn('[Gemini Imagen] Falha no fallback gemini-3.1-flash-image:', ultimoErro);
|
||||
}
|
||||
}
|
||||
|
||||
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');
|
||||
|
|
|
|||
Loading…
Reference in New Issue