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 {
|
} 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 url = `https://generativelanguage.googleapis.com/v1beta/models/${model}:generateContent?key=${apiKey}`;
|
||||||
|
|
||||||
const payload = {
|
const payload = {
|
||||||
contents: [
|
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',
|
responseMimeType: 'application/json',
|
||||||
responseSchema: {
|
responseSchema: {
|
||||||
type: 'ARRAY',
|
type: 'ARRAY',
|
||||||
|
|
@ -1515,79 +1520,75 @@ Apenas retorne o JSON estruturado contendo a lista de questões no seguinte form
|
||||||
required: ['text', 'options', 'correctOptionIndex']
|
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
|
const response = await fetch(url, {
|
||||||
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, {
|
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: { 'Content-Type': 'application/json' },
|
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) {
|
// Tentativa 3 (Último recurso): Se ainda falhar, tentamos gemini-1.5-flash sem Schema diretamente
|
||||||
const errorText = errText || await response.text();
|
if (!resObj || !resObj.response.ok) {
|
||||||
throw new Error(`Gemini API retornou erro: ${response.status} - ${errorText}`);
|
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;
|
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;
|
||||||
|
|
||||||
|
// Tentativa 1: Modelo selecionado
|
||||||
|
if (modelUsed.startsWith('imagen-')) {
|
||||||
try {
|
try {
|
||||||
console.log(`[Gemini Imagen] Tentando gerar com o modelo selecionado: ${modelUsed}`);
|
console.log(`[Gemini Imagen] Tentando gerar com o modelo Imagen 3: ${modelUsed}`);
|
||||||
base64ImageBytes = await tentarGerarGemini(modelUsed);
|
base64ImageBytes = await tentarGerarImagen3(modelUsed, prompt);
|
||||||
} catch (err) {
|
} 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') {
|
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');
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue