diff --git a/public/app.js b/public/app.js
index 6e06474..9ba8841 100644
--- a/public/app.js
+++ b/public/app.js
@@ -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 {
diff --git a/public/index.html b/public/index.html
index 64cf65e..7fef0a4 100644
--- a/public/index.html
+++ b/public/index.html
@@ -1439,8 +1439,12 @@
-
-
![Ilustra莽茫o da Cena]()
+
+
![Clique para abrir imagem em tela cheia Ilustra莽茫o da Cena]()
+
diff --git a/server.js b/server.js
index cf18e4c..896ba47 100644
--- a/server.js
+++ b/server.js
@@ -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.' });
}
+ 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 {
- 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 minmBase = (process.env.MINIMAX_API_BASE || 'https://api.minimax.io/v1').replace(/\/v1\/?$/, '');
+ const apiKey = process.env.MINIMAX_API_KEY;
- // 1. Tentar MiniMax image-01
- if (process.env.MINIMAX_API_KEY) {
- try {
- 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 (apiKey) {
+ const controller = new AbortController();
+ const timeoutId = setTimeout(() => controller.abort(), 25000);
- 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}` });
+ const genResp = await fetch(`${minmBase}/v1/image_generation`, {
+ method: 'POST',
+ headers: {
+ 'Authorization': `Bearer ${apiKey}`,
+ 'Content-Type': 'application/json'
+ },
+ body: JSON.stringify({
+ model: 'image-01',
+ prompt: fullPrompt,
+ n: 1
+ }),
+ signal: controller.signal
+ });
+ 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
- if (process.env.OPENROUTER_API_KEY) {
- 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 });
+ // 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 {
+ 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
- res.json({
- success: true,
- imageUrl: 'https://images.unsplash.com/photo-1512820790803-83ca734da794?w=600&auto=format&fit=crop&q=80'
+ 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: `/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.' });
}
});