Correcao de sintaxe no app.js e aprimoramento da geracao e visualizacao de ilustracoes em Inventando Historias
This commit is contained in:
+10
-1
@@ -7610,6 +7610,8 @@ window.toggleCustomAudio = (btn) => {
|
|||||||
turmaId: musicaTurmaSelect?.value || null,
|
turmaId: musicaTurmaSelect?.value || null,
|
||||||
gerarAudio: true
|
gerarAudio: true
|
||||||
})
|
})
|
||||||
|
});
|
||||||
|
|
||||||
if (!res.ok) {
|
if (!res.ok) {
|
||||||
const errMsg = await safeExtractError(res, 'Erro ao compor música');
|
const errMsg = await safeExtractError(res, 'Erro ao compor música');
|
||||||
throw new Error(errMsg);
|
throw new Error(errMsg);
|
||||||
@@ -8117,7 +8119,14 @@ window.toggleCustomAudio = (btn) => {
|
|||||||
|
|
||||||
// Imagem da cena
|
// Imagem da cena
|
||||||
if (cena.imageUrl) {
|
if (cena.imageUrl) {
|
||||||
if (historiaCenaImg) historiaCenaImg.src = cena.imageUrl;
|
if (historiaCenaImg) {
|
||||||
|
historiaCenaImg.src = cena.imageUrl;
|
||||||
|
historiaCenaImg.onclick = () => window.open(cena.imageUrl, '_blank');
|
||||||
|
}
|
||||||
|
const historiaCenaOpenBtn = document.getElementById('historiaCenaOpenBtn');
|
||||||
|
const historiaCenaDownloadBtn = document.getElementById('historiaCenaDownloadBtn');
|
||||||
|
if (historiaCenaOpenBtn) historiaCenaOpenBtn.href = cena.imageUrl;
|
||||||
|
if (historiaCenaDownloadBtn) historiaCenaDownloadBtn.href = cena.imageUrl;
|
||||||
if (historiaCenaImageContainer) historiaCenaImageContainer.style.display = 'block';
|
if (historiaCenaImageContainer) historiaCenaImageContainer.style.display = 'block';
|
||||||
if (btnGenerateSceneImage) btnGenerateSceneImage.innerHTML = '🔄 Regenerar Ilustração';
|
if (btnGenerateSceneImage) btnGenerateSceneImage.innerHTML = '🔄 Regenerar Ilustração';
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
+6
-2
@@ -1439,8 +1439,12 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Container da Imagem da Cena -->
|
<!-- Container da Imagem da Cena -->
|
||||||
<div id="historiaCenaImageContainer" style="display: none; width: 100%; border-radius: 8px; overflow: hidden; border: 1px solid var(--border-light); max-height: 200px; text-align: center; background: rgba(0,0,0,0.2);">
|
<div id="historiaCenaImageContainer" style="display: none; width: 100%; border-radius: 10px; overflow: hidden; border: 1px solid var(--border-light); text-align: center; background: rgba(0,0,0,0.3); margin: 6px 0;">
|
||||||
<img id="historiaCenaImg" src="" alt="Ilustração da Cena" style="max-height: 200px; width: auto; object-fit: contain;">
|
<img id="historiaCenaImg" src="" alt="Ilustração da Cena" style="max-height: 280px; width: 100%; object-fit: contain; cursor: pointer; border-radius: 8px;" title="Clique para abrir imagem em tela cheia">
|
||||||
|
<div style="display: flex; justify-content: center; gap: 8px; padding: 6px; background: rgba(0,0,0,0.4);">
|
||||||
|
<a id="historiaCenaOpenBtn" href="#" target="_blank" class="btn-music-option" style="padding: 4px 10px; font-size: 0.75rem; margin: 0; text-decoration: none; color: #60a5fa; border-color: rgba(59,130,246,0.3);">🔍 Ver em Tela Cheia</a>
|
||||||
|
<a id="historiaCenaDownloadBtn" href="#" download="ilustracao_cena.jpg" class="btn-music-option" style="padding: 4px 10px; font-size: 0.75rem; margin: 0; text-decoration: none; color: #34d399; border-color: rgba(52,211,153,0.3);">📥 Baixar Ilustração</a>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Texto da Cena -->
|
<!-- Texto da Cena -->
|
||||||
|
|||||||
@@ -3076,84 +3076,111 @@ app.post('/api/historias/scene-image', requireAuth, async (req, res) => {
|
|||||||
return res.status(400).json({ error: 'O prompt da cena é obrigatório.' });
|
return res.status(400).json({ error: 'O prompt da cena é obrigatório.' });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const fs = require('fs');
|
||||||
|
const path = require('path');
|
||||||
|
const mediaDir = path.join(__dirname, 'public', 'generated-media');
|
||||||
|
if (!fs.existsSync(mediaDir)) {
|
||||||
|
fs.mkdirSync(mediaDir, { recursive: true });
|
||||||
|
}
|
||||||
|
|
||||||
|
let imageBuffer = null;
|
||||||
|
let providerUsed = 'minimax';
|
||||||
|
|
||||||
|
const cleanPrompt = (prompt || 'cute children book illustration').replace(/[^\w\s,.-]/gi, '');
|
||||||
|
const cuteStyle = 'adorable cute 3D Pixar claymation style, innocent friendly children book illustration, soft rounded features, big expressive eyes, warm comforting lighting, vibrant colors, 4k digital art';
|
||||||
|
const fullPrompt = `${cleanPrompt}, ${estiloVisual || 'Livro Infantil'}, ${cuteStyle}`;
|
||||||
|
const negativeGuardrail = 'scary, horror, terrifying, creepy, ugly, grotesque, monster, sharp teeth, blood, dark gritty realism, deformed, aggressive, photorealistic';
|
||||||
|
|
||||||
|
// 1. Tentar MiniMax image-01 com timeout
|
||||||
try {
|
try {
|
||||||
console.log(`[Histórias] Gerando ilustração para cena ${cenaNumero || 1}...`);
|
const minmBase = (process.env.MINIMAX_API_BASE || 'https://api.minimax.io/v1').replace(/\/v1\/?$/, '');
|
||||||
const enhancedPrompt = `${prompt}, ${estiloVisual || 'children book colorful illustration'}, vibrant lighting, cute warm character design, 4k master artwork`;
|
const apiKey = process.env.MINIMAX_API_KEY;
|
||||||
|
|
||||||
// 1. Tentar MiniMax image-01
|
if (apiKey) {
|
||||||
if (process.env.MINIMAX_API_KEY) {
|
const controller = new AbortController();
|
||||||
try {
|
const timeoutId = setTimeout(() => controller.abort(), 25000);
|
||||||
const minmBase = (process.env.MINIMAX_API_BASE || 'https://api.minimax.io/v1').replace(/\/v1\/?$/, '');
|
|
||||||
const imgResp = await fetch(`${minmBase}/v1/image_generation`, {
|
|
||||||
method: 'POST',
|
|
||||||
headers: {
|
|
||||||
'Authorization': `Bearer ${process.env.MINIMAX_API_KEY}`,
|
|
||||||
'Content-Type': 'application/json'
|
|
||||||
},
|
|
||||||
body: JSON.stringify({
|
|
||||||
model: 'image-01',
|
|
||||||
prompt: enhancedPrompt,
|
|
||||||
aspect_ratio: '4:3',
|
|
||||||
response_format: 'base64'
|
|
||||||
})
|
|
||||||
});
|
|
||||||
|
|
||||||
if (imgResp.ok) {
|
const genResp = await fetch(`${minmBase}/v1/image_generation`, {
|
||||||
const imgData = await imgResp.json();
|
method: 'POST',
|
||||||
const base64Data = imgData.data?.image_base64 || imgData.data?.images?.[0];
|
headers: {
|
||||||
if (base64Data) {
|
'Authorization': `Bearer ${apiKey}`,
|
||||||
const fs = require('fs');
|
'Content-Type': 'application/json'
|
||||||
const path = require('path');
|
},
|
||||||
const mediaDir = path.join(__dirname, 'public', 'generated-media');
|
body: JSON.stringify({
|
||||||
if (!fs.existsSync(mediaDir)) fs.mkdirSync(mediaDir, { recursive: true });
|
model: 'image-01',
|
||||||
const fileName = `cena_${Date.now()}.png`;
|
prompt: fullPrompt,
|
||||||
const filePath = path.join(mediaDir, fileName);
|
n: 1
|
||||||
const buffer = Buffer.from(base64Data, 'base64');
|
}),
|
||||||
fs.writeFileSync(filePath, buffer);
|
signal: controller.signal
|
||||||
await backupMediaFile(filePath, buffer);
|
});
|
||||||
return res.json({ success: true, imageUrl: `/generated-media/${fileName}` });
|
clearTimeout(timeoutId);
|
||||||
|
|
||||||
|
if (genResp.ok) {
|
||||||
|
const genData = await genResp.json();
|
||||||
|
const imageUrl = genData.data?.image_urls?.[0];
|
||||||
|
if (imageUrl) {
|
||||||
|
const imgFetch = await fetch(imageUrl);
|
||||||
|
if (imgFetch.ok) {
|
||||||
|
imageBuffer = Buffer.from(await imgFetch.arrayBuffer());
|
||||||
|
console.log(`[Histórias Image] Imagem gerada com sucesso via MiniMax para cena ${cenaNumero || 1}`);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} catch (minErr) {
|
|
||||||
console.warn('[Histórias] Falha MiniMax imagem:', minErr.message);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
} catch (minErr) {
|
||||||
|
console.warn('[Histórias Image] MiniMax falhou ou timeout (' + minErr.message + '), acionando fallback...');
|
||||||
|
}
|
||||||
|
|
||||||
// 2. Fallback OpenRouter FLUX/SD
|
// 2. Fallback Multi-Engine Pollinations (Flux / Turbo)
|
||||||
if (process.env.OPENROUTER_API_KEY) {
|
if (!imageBuffer) {
|
||||||
try {
|
const enhancedPrompt = encodeURIComponent(fullPrompt);
|
||||||
const r = await fetch('https://openrouter.ai/api/v1/chat/completions', {
|
const negativeEncoded = encodeURIComponent(negativeGuardrail);
|
||||||
method: 'POST',
|
const engines = ['flux', 'turbo'];
|
||||||
headers: {
|
|
||||||
'Authorization': `Bearer ${process.env.OPENROUTER_API_KEY}`,
|
for (const engine of engines) {
|
||||||
'Content-Type': 'application/json'
|
if (imageBuffer) break;
|
||||||
},
|
for (let attempt = 1; attempt <= 2; attempt++) {
|
||||||
body: JSON.stringify({
|
try {
|
||||||
model: 'black-forest-labs/flux-1-schnell',
|
providerUsed = `pollinations-${engine}`;
|
||||||
messages: [{ role: 'user', content: enhancedPrompt }]
|
const seed = Math.floor(Math.random() * 1000000);
|
||||||
})
|
const fallbackUrl = `https://image.pollinations.ai/prompt/${enhancedPrompt}?width=1024&height=768&model=${engine}&nologo=true&seed=${seed}&negative=${negativeEncoded}`;
|
||||||
});
|
|
||||||
if (r.ok) {
|
const controller = new AbortController();
|
||||||
const data = await r.json();
|
const timeoutId = setTimeout(() => controller.abort(), 20000);
|
||||||
const imgUrl = data.choices?.[0]?.message?.content;
|
|
||||||
if (imgUrl && imgUrl.startsWith('http')) {
|
const fbResp = await fetch(fallbackUrl, { signal: controller.signal });
|
||||||
return res.json({ success: true, imageUrl: imgUrl });
|
clearTimeout(timeoutId);
|
||||||
|
|
||||||
|
if (fbResp.ok) {
|
||||||
|
imageBuffer = Buffer.from(await fbResp.arrayBuffer());
|
||||||
|
console.log(`[Histórias Image] Fallback ${providerUsed} gerou imagem com sucesso (tentativa ${attempt})`);
|
||||||
|
break;
|
||||||
}
|
}
|
||||||
|
} catch (fbErr) {
|
||||||
|
console.warn(`[Histórias Image] Fallback ${engine} erro:`, fbErr.message);
|
||||||
}
|
}
|
||||||
} catch (orErr) {
|
|
||||||
console.warn('[Histórias] Falha OpenRouter imagem:', orErr.message);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Fallback Imagem Ilustrada Placeholder SVG
|
if (!imageBuffer) {
|
||||||
res.json({
|
return res.status(500).json({ error: 'Não foi possível gerar a ilustração no momento. Tente novamente.' });
|
||||||
success: true,
|
}
|
||||||
imageUrl: 'https://images.unsplash.com/photo-1512820790803-83ca734da794?w=600&auto=format&fit=crop&q=80'
|
|
||||||
|
const fileName = `cena_${Date.now()}_${Math.floor(Math.random() * 1000)}.jpg`;
|
||||||
|
const filePath = path.join(mediaDir, fileName);
|
||||||
|
|
||||||
|
try {
|
||||||
|
fs.writeFileSync(filePath, imageBuffer);
|
||||||
|
backupMediaFile(filePath, imageBuffer).catch(() => {});
|
||||||
|
res.json({
|
||||||
|
success: true,
|
||||||
|
imageUrl: `/generated-media/${fileName}`,
|
||||||
|
provider: providerUsed
|
||||||
});
|
});
|
||||||
|
} catch (saveErr) {
|
||||||
} catch (err) {
|
console.error('Erro ao salvar imagem da cena:', saveErr);
|
||||||
console.error('[Histórias] Erro ao gerar imagem da cena:', err);
|
res.status(500).json({ error: 'Erro ao salvar imagem gerada.' });
|
||||||
res.status(500).json({ error: 'Erro ao gerar imagem da cena: ' + err.message });
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user