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,
|
||||
gerarAudio: true
|
||||
})
|
||||
});
|
||||
|
||||
if (!res.ok) {
|
||||
const errMsg = await safeExtractError(res, 'Erro ao compor música');
|
||||
throw new Error(errMsg);
|
||||
@@ -8117,7 +8119,14 @@ window.toggleCustomAudio = (btn) => {
|
||||
|
||||
// Imagem da cena
|
||||
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 (btnGenerateSceneImage) btnGenerateSceneImage.innerHTML = '🔄 Regenerar Ilustração';
|
||||
} else {
|
||||
|
||||
+6
-2
@@ -1439,8 +1439,12 @@
|
||||
</div>
|
||||
|
||||
<!-- 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);">
|
||||
<img id="historiaCenaImg" src="" alt="Ilustração da Cena" style="max-height: 200px; width: auto; object-fit: contain;">
|
||||
<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: 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>
|
||||
|
||||
<!-- 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.' });
|
||||
}
|
||||
|
||||
try {
|
||||
console.log(`[Histórias] Gerando ilustração para cena ${cenaNumero || 1}...`);
|
||||
const enhancedPrompt = `${prompt}, ${estiloVisual || 'children book colorful illustration'}, vibrant lighting, cute warm character design, 4k master artwork`;
|
||||
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 });
|
||||
}
|
||||
|
||||
// 1. Tentar MiniMax image-01
|
||||
if (process.env.MINIMAX_API_KEY) {
|
||||
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 {
|
||||
const minmBase = (process.env.MINIMAX_API_BASE || 'https://api.minimax.io/v1').replace(/\/v1\/?$/, '');
|
||||
const imgResp = await fetch(`${minmBase}/v1/image_generation`, {
|
||||
const apiKey = process.env.MINIMAX_API_KEY;
|
||||
|
||||
if (apiKey) {
|
||||
const controller = new AbortController();
|
||||
const timeoutId = setTimeout(() => controller.abort(), 25000);
|
||||
|
||||
const genResp = await fetch(`${minmBase}/v1/image_generation`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Authorization': `Bearer ${process.env.MINIMAX_API_KEY}`,
|
||||
'Authorization': `Bearer ${apiKey}`,
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
body: JSON.stringify({
|
||||
model: 'image-01',
|
||||
prompt: enhancedPrompt,
|
||||
aspect_ratio: '4:3',
|
||||
response_format: 'base64'
|
||||
})
|
||||
prompt: fullPrompt,
|
||||
n: 1
|
||||
}),
|
||||
signal: controller.signal
|
||||
});
|
||||
clearTimeout(timeoutId);
|
||||
|
||||
if (imgResp.ok) {
|
||||
const imgData = await imgResp.json();
|
||||
const base64Data = imgData.data?.image_base64 || imgData.data?.images?.[0];
|
||||
if (base64Data) {
|
||||
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 });
|
||||
const fileName = `cena_${Date.now()}.png`;
|
||||
const filePath = path.join(mediaDir, fileName);
|
||||
const buffer = Buffer.from(base64Data, 'base64');
|
||||
fs.writeFileSync(filePath, buffer);
|
||||
await backupMediaFile(filePath, buffer);
|
||||
return res.json({ success: true, imageUrl: `/generated-media/${fileName}` });
|
||||
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);
|
||||
}
|
||||
console.warn('[Histórias Image] MiniMax falhou ou timeout (' + minErr.message + '), acionando fallback...');
|
||||
}
|
||||
|
||||
// 2. Fallback OpenRouter FLUX/SD
|
||||
if (process.env.OPENROUTER_API_KEY) {
|
||||
// 2. Fallback Multi-Engine Pollinations (Flux / Turbo)
|
||||
if (!imageBuffer) {
|
||||
const enhancedPrompt = encodeURIComponent(fullPrompt);
|
||||
const negativeEncoded = encodeURIComponent(negativeGuardrail);
|
||||
const engines = ['flux', 'turbo'];
|
||||
|
||||
for (const engine of engines) {
|
||||
if (imageBuffer) break;
|
||||
for (let attempt = 1; attempt <= 2; attempt++) {
|
||||
try {
|
||||
const r = await fetch('https://openrouter.ai/api/v1/chat/completions', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Authorization': `Bearer ${process.env.OPENROUTER_API_KEY}`,
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
body: JSON.stringify({
|
||||
model: 'black-forest-labs/flux-1-schnell',
|
||||
messages: [{ role: 'user', content: enhancedPrompt }]
|
||||
})
|
||||
});
|
||||
if (r.ok) {
|
||||
const data = await r.json();
|
||||
const imgUrl = data.choices?.[0]?.message?.content;
|
||||
if (imgUrl && imgUrl.startsWith('http')) {
|
||||
return res.json({ success: true, imageUrl: imgUrl });
|
||||
providerUsed = `pollinations-${engine}`;
|
||||
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}`;
|
||||
|
||||
const controller = new AbortController();
|
||||
const timeoutId = setTimeout(() => controller.abort(), 20000);
|
||||
|
||||
const fbResp = await fetch(fallbackUrl, { signal: controller.signal });
|
||||
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) {
|
||||
return res.status(500).json({ error: 'Não foi possível gerar a ilustração no momento. Tente novamente.' });
|
||||
}
|
||||
|
||||
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: 'https://images.unsplash.com/photo-1512820790803-83ca734da794?w=600&auto=format&fit=crop&q=80'
|
||||
imageUrl: `/generated-media/${fileName}`,
|
||||
provider: providerUsed
|
||||
});
|
||||
|
||||
} catch (err) {
|
||||
console.error('[Histórias] Erro ao gerar imagem da cena:', err);
|
||||
res.status(500).json({ error: 'Erro ao gerar imagem da cena: ' + err.message });
|
||||
} catch (saveErr) {
|
||||
console.error('Erro ao salvar imagem da cena:', saveErr);
|
||||
res.status(500).json({ error: 'Erro ao salvar imagem gerada.' });
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
Reference in New Issue
Block a user