Correcao de sintaxe no app.js e aprimoramento da geracao e visualizacao de ilustracoes em Inventando Historias

This commit is contained in:
2026-09-01 02:33:04 +00:00
parent ee1cd3bbe5
commit 12283f29f2
3 changed files with 107 additions and 67 deletions
+10 -1
View File
@@ -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
View File
@@ -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 -->
+91 -64
View File
@@ -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 });
} }
}); });