diff --git a/public/app.js b/public/app.js
index ed89a4b..729f1a7 100644
--- a/public/app.js
+++ b/public/app.js
@@ -4889,6 +4889,7 @@ Se algum dado não for mencionado, deixe a string vazia ou false para boolean.`
}
fabricaQuadrinhosModal.style.display = 'none';
if (comicsVideoPlayer) comicsVideoPlayer.pause();
+ if (typeof stopComicsMusicPreview === 'function') stopComicsMusicPreview();
});
}
@@ -5033,8 +5034,14 @@ Se algum dado não for mencionado, deixe a string vazia ou false para boolean.`
const totalPanels = scriptData.panels.length;
let completedCount = 0;
+ const CUTE_COMIC_STYLE_SUFFIX = 'adorable cute 3D animated style, Pixar and modern claymation aesthetic, friendly and innocent character design, soft rounded features, big expressive friendly eyes, smooth textures, vibrant warm comforting color palette, whimsical storytelling, sweet and gentle atmosphere, perfect for toddlers and preschoolers (ages 2 to 6), clean studio lighting, 8k render';
+
const generateSinglePanel = async (panel) => {
- const fullPrompt = `${panel.image_prompt}, 3D Pixar Disney style, digital children book illustration, vibrant colors, masterpiece, cinematic lighting, ${selectedComicsRatio === '16:9' ? '16:9 aspect ratio' : '4:3 aspect ratio'}`;
+ let fullPrompt = panel.image_prompt || '';
+ if (!fullPrompt.toLowerCase().includes('claymation') && !fullPrompt.toLowerCase().includes('pixar')) {
+ fullPrompt = `${fullPrompt}, ${CUTE_COMIC_STYLE_SUFFIX}`;
+ }
+ fullPrompt = `${fullPrompt}, ${selectedComicsRatio === '16:9' ? '16:9 aspect ratio' : '4:3 aspect ratio'}`;
let attempts = 0;
let frameData = null;
@@ -5307,7 +5314,12 @@ Se algum dado não for mencionado, deixe a string vazia ou false para boolean.`
}
}
- const fullPrompt = `${promptToUse}, 3D Pixar Disney style, digital children book illustration, vibrant colors, masterpiece, cinematic lighting, ${selectedComicsRatio === '16:9' ? '16:9 aspect ratio' : '4:3 aspect ratio'}`;
+ const CUTE_COMIC_STYLE_SUFFIX = 'adorable cute 3D animated style, Pixar and modern claymation aesthetic, friendly and innocent character design, soft rounded features, big expressive friendly eyes, smooth textures, vibrant warm comforting color palette, whimsical storytelling, sweet and gentle atmosphere, perfect for toddlers and preschoolers (ages 2 to 6), clean studio lighting, 8k render';
+ let fullPrompt = promptToUse || '';
+ if (!fullPrompt.toLowerCase().includes('claymation') && !fullPrompt.toLowerCase().includes('pixar')) {
+ fullPrompt = `${fullPrompt}, ${CUTE_COMIC_STYLE_SUFFIX}`;
+ }
+ fullPrompt = `${fullPrompt}, ${selectedComicsRatio === '16:9' ? '16:9 aspect ratio' : '4:3 aspect ratio'}`;
const resp = await fetch('/api/comics/generate-frame', {
method: 'POST',
@@ -5707,6 +5719,7 @@ Se algum dado não for mencionado, deixe a string vazia ou false para boolean.`
btnCompileComicsVideo.addEventListener('click', async () => {
if (generatedPanelsData.length === 0) return;
+ stopComicsMusicPreview();
btnCompileComicsVideo.disabled = true;
comicsVideoLoader.style.display = 'flex';
comicsVideoResult.style.display = 'none';
@@ -5719,8 +5732,8 @@ Se algum dado não for mencionado, deixe a string vazia ou false para boolean.`
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
frames: generatedPanelsData,
- musicSelection: comicsVideoMusic.value,
- frameDuration: comicsVideoDuration.value,
+ musicSelection: comicsVideoMusic ? comicsVideoMusic.value : 'alegre',
+ frameDuration: comicsVideoDuration ? comicsVideoDuration.value : 5,
narrationVoice: voice,
titulo: comicsResultTitle?.textContent || 'História em Quadrinhos'
})
@@ -5749,6 +5762,72 @@ Se algum dado não for mencionado, deixe a string vazia ou false para boolean.`
});
}
+ // --- PREVIEW INTERATIVO DE TRILHAS SONORAS (PLAY/STOP) ---
+ let comicsPreviewAudio = null;
+ const btnPreviewComicsMusic = document.getElementById('btnPreviewComicsMusic');
+ const comicsMusicPreviewIcon = document.getElementById('comicsMusicPreviewIcon');
+ const comicsMusicPreviewText = document.getElementById('comicsMusicPreviewText');
+ const comicsMusicPlayingTag = document.getElementById('comicsMusicPlayingTag');
+
+ function stopComicsMusicPreview() {
+ if (comicsPreviewAudio) {
+ comicsPreviewAudio.pause();
+ comicsPreviewAudio.currentTime = 0;
+ comicsPreviewAudio = null;
+ }
+ if (comicsMusicPreviewIcon) comicsMusicPreviewIcon.textContent = '▶️';
+ if (comicsMusicPreviewText) comicsMusicPreviewText.textContent = 'Ouvir';
+ if (comicsMusicPlayingTag) comicsMusicPlayingTag.style.display = 'none';
+ if (btnPreviewComicsMusic) {
+ btnPreviewComicsMusic.style.background = 'rgba(219, 39, 119, 0.1)';
+ btnPreviewComicsMusic.style.borderColor = 'var(--brand-pink)';
+ btnPreviewComicsMusic.style.color = 'var(--brand-pink)';
+ }
+ }
+
+ if (btnPreviewComicsMusic && comicsVideoMusic) {
+ btnPreviewComicsMusic.addEventListener('click', () => {
+ const selectedTrack = comicsVideoMusic.value;
+ if (selectedTrack === 'sem_musica') {
+ showCustomAlert('Sem Trilha', 'A opção "Sem Música de Fundo" está selecionada.');
+ return;
+ }
+
+ if (comicsPreviewAudio && !comicsPreviewAudio.paused) {
+ stopComicsMusicPreview();
+ return;
+ }
+
+ stopComicsMusicPreview();
+ const audioUrl = `/assets/audio/${selectedTrack}.mp3`;
+ comicsPreviewAudio = new Audio(audioUrl);
+ comicsPreviewAudio.volume = 0.55;
+
+ comicsPreviewAudio.play().then(() => {
+ if (comicsMusicPreviewIcon) comicsMusicPreviewIcon.textContent = '⏹️';
+ if (comicsMusicPreviewText) comicsMusicPreviewText.textContent = 'Parar';
+ if (comicsMusicPlayingTag) comicsMusicPlayingTag.style.display = 'inline';
+ if (btnPreviewComicsMusic) {
+ btnPreviewComicsMusic.style.background = 'rgba(16, 185, 129, 0.15)';
+ btnPreviewComicsMusic.style.borderColor = '#10b981';
+ btnPreviewComicsMusic.style.color = '#10b981';
+ }
+ }).catch(err => {
+ console.warn('Erro ao reproduzir prévia de áudio:', err.message);
+ stopComicsMusicPreview();
+ showCustomAlert('Áudio', 'Não foi possível carregar a prévia desta trilha.');
+ });
+
+ comicsPreviewAudio.onended = () => {
+ stopComicsMusicPreview();
+ };
+ });
+
+ comicsVideoMusic.addEventListener('change', () => {
+ stopComicsMusicPreview();
+ });
+ }
+
// ============================================================
// VIDEOMIND (TRANSCRIÇÃO E ANÁLISE DE VÍDEO PEDAGÓGICO)
// ============================================================
diff --git a/public/assets/audio/amizade.mp3 b/public/assets/audio/amizade.mp3
new file mode 100644
index 0000000..b80d7fa
Binary files /dev/null and b/public/assets/audio/amizade.mp3 differ
diff --git a/public/assets/audio/animais.mp3 b/public/assets/audio/animais.mp3
new file mode 100644
index 0000000..4554e72
Binary files /dev/null and b/public/assets/audio/animais.mp3 differ
diff --git a/public/assets/audio/bebes.mp3 b/public/assets/audio/bebes.mp3
new file mode 100644
index 0000000..b80d7fa
Binary files /dev/null and b/public/assets/audio/bebes.mp3 differ
diff --git a/public/assets/audio/brinquedos.mp3 b/public/assets/audio/brinquedos.mp3
new file mode 100644
index 0000000..99c6d42
Binary files /dev/null and b/public/assets/audio/brinquedos.mp3 differ
diff --git a/public/assets/audio/carrossel.mp3 b/public/assets/audio/carrossel.mp3
new file mode 100644
index 0000000..99c6d42
Binary files /dev/null and b/public/assets/audio/carrossel.mp3 differ
diff --git a/public/assets/audio/castelo.mp3 b/public/assets/audio/castelo.mp3
new file mode 100644
index 0000000..925b7a1
Binary files /dev/null and b/public/assets/audio/castelo.mp3 differ
diff --git a/public/assets/audio/chuva.mp3 b/public/assets/audio/chuva.mp3
new file mode 100644
index 0000000..3f5a01c
Binary files /dev/null and b/public/assets/audio/chuva.mp3 differ
diff --git a/public/assets/audio/circo.mp3 b/public/assets/audio/circo.mp3
new file mode 100644
index 0000000..266d6a2
Binary files /dev/null and b/public/assets/audio/circo.mp3 differ
diff --git a/public/assets/audio/curiosa.mp3 b/public/assets/audio/curiosa.mp3
new file mode 100644
index 0000000..71f4942
Binary files /dev/null and b/public/assets/audio/curiosa.mp3 differ
diff --git a/public/assets/audio/espaco.mp3 b/public/assets/audio/espaco.mp3
new file mode 100644
index 0000000..023eccd
Binary files /dev/null and b/public/assets/audio/espaco.mp3 differ
diff --git a/public/assets/audio/festa.mp3 b/public/assets/audio/festa.mp3
new file mode 100644
index 0000000..99c6d42
Binary files /dev/null and b/public/assets/audio/festa.mp3 differ
diff --git a/public/assets/audio/herois.mp3 b/public/assets/audio/herois.mp3
new file mode 100644
index 0000000..22001b5
Binary files /dev/null and b/public/assets/audio/herois.mp3 differ
diff --git a/public/assets/audio/jardim.mp3 b/public/assets/audio/jardim.mp3
new file mode 100644
index 0000000..255abfa
Binary files /dev/null and b/public/assets/audio/jardim.mp3 differ
diff --git a/public/assets/audio/magica.mp3 b/public/assets/audio/magica.mp3
new file mode 100644
index 0000000..3f5a01c
Binary files /dev/null and b/public/assets/audio/magica.mp3 differ
diff --git a/public/assets/audio/mar.mp3 b/public/assets/audio/mar.mp3
new file mode 100644
index 0000000..255abfa
Binary files /dev/null and b/public/assets/audio/mar.mp3 differ
diff --git a/public/assets/audio/natureza.mp3 b/public/assets/audio/natureza.mp3
new file mode 100644
index 0000000..255abfa
Binary files /dev/null and b/public/assets/audio/natureza.mp3 differ
diff --git a/public/assets/audio/ninar.mp3 b/public/assets/audio/ninar.mp3
new file mode 100644
index 0000000..b80d7fa
Binary files /dev/null and b/public/assets/audio/ninar.mp3 differ
diff --git a/public/assets/audio/passeio.mp3 b/public/assets/audio/passeio.mp3
new file mode 100644
index 0000000..b0282f7
Binary files /dev/null and b/public/assets/audio/passeio.mp3 differ
diff --git a/public/assets/audio/pipoca.mp3 b/public/assets/audio/pipoca.mp3
new file mode 100644
index 0000000..266d6a2
Binary files /dev/null and b/public/assets/audio/pipoca.mp3 differ
diff --git a/public/index.html b/public/index.html
index 17e2fb7..64cf65e 100644
--- a/public/index.html
+++ b/public/index.html
@@ -2073,13 +2073,52 @@
-
-
+
+
+
+
+
diff --git a/scripts/create_soundtracks.js b/scripts/create_soundtracks.js
new file mode 100644
index 0000000..e982a36
--- /dev/null
+++ b/scripts/create_soundtracks.js
@@ -0,0 +1,63 @@
+const fs = require('fs');
+const path = require('path');
+const { execSync } = require('child_process');
+
+const audioDir = path.join(__dirname, '..', 'public', 'assets', 'audio');
+if (!fs.existsSync(audioDir)) {
+ fs.mkdirSync(audioDir, { recursive: true });
+}
+
+// 22 Estilos Musicais Infantis Detalhados
+const tracks = [
+ { id: 'alegre', name: '🎸 Alegre e Brincalhona' },
+ { id: 'aventura', name: '🎮 Aventura na Floresta' },
+ { id: 'calma', name: '🌙 Calma e Relaxante' },
+ { id: 'magica', name: '✨ Terra da Fantasia & Magia', freq1: 523.25, freq2: 659.25, freq3: 783.99, type: 'magic' },
+ { id: 'circo', name: '🎪 Circo Encantado & Palhacinhos', freq1: 440, freq2: 554.37, freq3: 659.25, type: 'circus' },
+ { id: 'brinquedos', name: '🧸 Fábrica de Brinquedos', freq1: 392, freq2: 493.88, freq3: 587.33, type: 'toy' },
+ { id: 'jardim', name: '🌸 Jardim das Borboletas', freq1: 349.23, freq2: 440, freq3: 523.25, type: 'gentle' },
+ { id: 'animais', name: '🐾 Fazendinha dos Bichinhos', freq1: 293.66, freq2: 369.99, freq3: 440, type: 'playful' },
+ { id: 'ninar', name: '⭐ Estrelinha Brilhante (Ninar)', freq1: 261.63, freq2: 329.63, freq3: 392, type: 'lullaby' },
+ { id: 'passeio', name: '🚲 Passeio no Parque', freq1: 329.63, freq2: 415.30, freq3: 493.88, type: 'upbeat' },
+ { id: 'festa', name: '🎈 Dia de Festa & Balões', freq1: 392, freq2: 493.88, freq3: 587.33, type: 'party' },
+ { id: 'curiosa', name: '🔍 Pequenos Detetives', freq1: 220, freq2: 277.18, freq3: 329.63, type: 'curious' },
+ { id: 'amizade', name: '🤝 Abraço de Amigo & Carinho', freq1: 261.63, freq2: 329.63, freq3: 392, type: 'warm' },
+ { id: 'castelo', name: '🏰 Castelo dos Contos de Fadas', freq1: 440, freq2: 523.25, freq3: 659.25, type: 'royal' },
+ { id: 'mar', name: '🐬 Ondinhas do Mar & Golfinhos', freq1: 349.23, freq2: 440, freq3: 523.25, type: 'ocean' },
+ { id: 'espaco', name: '🚀 Viagem ao Espaço Sideral', freq1: 293.66, freq2: 349.23, freq3: 440, type: 'space' },
+ { id: 'chuva', name: '🌧️ Gotinhas de Chuva Dançantes', freq1: 523.25, freq2: 659.25, freq3: 783.99, type: 'rain' },
+ { id: 'pipoca', name: '🍿 Pula Pula Pipoquinha', freq1: 440, freq2: 554.37, freq3: 659.25, type: 'bouncy' },
+ { id: 'herois', name: '🦸 Mini Super-Heróis', freq1: 329.63, freq2: 392, freq3: 493.88, type: 'heroic' },
+ { id: 'carrossel', name: '🎠 Carrossel Encantado', freq1: 392, freq2: 493.88, freq3: 587.33, type: 'waltz' },
+ { id: 'natureza', name: '🍃 Sons Doces da Natureza', freq1: 349.23, freq2: 440, freq3: 523.25, type: 'nature' },
+ { id: 'bebes', name: '🍼 Melodia Suave do Berço', freq1: 261.63, freq2: 329.63, freq3: 392, type: 'baby' }
+];
+
+console.log('Verificando trilhas sonoras...');
+
+for (const t of tracks) {
+ const filePath = path.join(audioDir, `${t.id}.mp3`);
+ if (!fs.existsSync(filePath) || fs.statSync(filePath).size < 1000) {
+ console.log(`Gerando trilha temática sintetizada: ${t.name} (${t.id}.mp3)...`);
+ try {
+ const f1 = t.freq1 || 440;
+ const f2 = t.freq2 || 554.37;
+ const f3 = t.freq3 || 659.25;
+ // Sintetizar uma harmonia suave de 45 segundos usando ondas senoidais filtradas com vibrato e decay suave (estilo caixinha de música / synth pad infantil)
+ const cmd = `ffmpeg -y -f lavfi -i "sine=frequency=${f1}:duration=45[a1];sine=frequency=${f2}:duration=45[a2];sine=frequency=${f3}:duration=45[a3];sine=frequency=${f1*2}:duration=45[a4];[a1][a2][a3][a4]amix=inputs=4:weights=0.35 0.25 0.25 0.15,lowpass=f=2200,tremolo=f=3:d=0.2,chorus=0.7:0.9:55:0.4:0.25:2,volume=0.4" -c:a libmp3lame -b:a 128k "${filePath}"`;
+ execSync(cmd, { stdio: 'ignore' });
+ console.log(`✅ Trilha criada: ${t.id}.mp3`);
+ } catch (err) {
+ console.warn(`Aviso ao sintetizar ${t.id}:`, err.message);
+ // Se falhar a síntese, copia alegre.mp3 como fallback
+ const alegrePath = path.join(audioDir, 'alegre.mp3');
+ if (fs.existsSync(alegrePath)) {
+ fs.copyFileSync(alegrePath, filePath);
+ }
+ }
+ } else {
+ console.log(`Trilha existente: ${t.id}.mp3 (${(fs.statSync(filePath).size / (1024*1024)).toFixed(2)} MB)`);
+ }
+}
+
+console.log('Trilhas sonoras prontas com sucesso!');
diff --git a/server.js b/server.js
index dcddf93..6d75a86 100644
--- a/server.js
+++ b/server.js
@@ -493,7 +493,11 @@ app.delete('/api/music/:id', requireAuth, async (req, res) => {
// ROTAS DO ESTÚDIO: FÁBRICA DE QUADRINHOS
// ============================================================
-// 1. Gerar o roteiro (script) dos quadrinhos usando MiniMax-M3
+// Constantes do Estilo Visual Infantil (Meigo e Fofo - 2 a 6 anos)
+const CUTE_COMIC_STYLE_ANCHOR = "adorable cute 3D animated style, Pixar and modern claymation aesthetic, friendly and innocent character design, soft rounded features, big expressive friendly eyes, smooth textures, vibrant warm comforting color palette, whimsical storytelling, sweet and gentle atmosphere, perfect for toddlers and preschoolers (ages 2 to 6), clean studio lighting, 8k render";
+const CUTE_COMIC_NEGATIVE_GUARDRAIL = "scary, horror, terrifying, creepy, ugly, grotesque, monster, sharp teeth, blood, dark gritty realism, sharp hyperrealistic skin, deformed, aggressive, mature, photorealistic";
+
+// 1. Gerar o roteiro (script) dos quadrinhos usando MiniMax com Pipeline de Consistência
app.post('/api/comics/generate-script', requireAuth, async (req, res) => {
const {
tema,
@@ -512,58 +516,84 @@ app.post('/api/comics/generate-script', requireAuth, async (req, res) => {
}
try {
- let prompt = '';
const isContinuation = existingPanels && Array.isArray(existingPanels) && existingPanels.length > 0 && continuationType && continuationType !== 'new';
const startPanelNum = (existingPanels && Array.isArray(existingPanels)) ? existingPanels.length + 1 : 1;
+ const systemPrompt = `Você é o Diretor de Arte Master e Roteirista do pipeline "ComicCharacterAndSceneConsistencyPipeline", especializado em histórias em quadrinhos pedagógicas e lúdicas para crianças pequenas de 2 a 6 anos (Maternal e Educação Infantil).
+
+SUA MISSÃO MANDATÓRIA:
+Garantir CONSISTÊNCIA VISUAL RIGOROSA E ABSOLUTA dos personagens e cenários ao longo de TODOS os quadros da história, mantendo sempre uma estética doce, meiga e encantadora.
+
+ESTILO VISUAL OBRIGATÓRIO (MEIGO E FOFO - IDADE 2 A 6 ANOS):
+- Estilo: Adorável animação 3D estilo Pixar misturado com modelagem macia claymation / Toy Art de vinil colecionável.
+- Design de Personagens: Rosto redondo muito fofo (proporções chibi/infantis), bochechas fofas e rosadas, olhos grandes e expressivos cheios de afeto e curiosidade, traços suaves e sem cantos pontiagudos.
+- REGRA DE OURO ANTI-TERROR / ANTI-GROTESCO: Crianças pequenas de 2 a 6 anos não podem ver figuras aterrorizantes! Vilões ou antagonistas (como bruxas, lobos, monstros ou animais selvagens) NUNCA devem ser assustadores, feios, com dentes afiados ou ogros. Eles devem ser SEMPRE estilizados como bonequinhos fofos, cômicos ou personagens inofensivos de conto de fadas infantil (ex: bruxinha fofa de chapéu pontudo com fivela e vestes roxas bonitas; lobinho simpático e atrapalhado).
+- Iluminação e Cores: Cores vibrantes, quentes, acolhedoras e mágicas, iluminação suave de estúdio, atmosfera segura e reconfortante.
+
+PIPELINE DE 3 FASES PARA GERAÇÃO:
+Fase 1 - Extração do DNA Visual (Visual DNA):
+- Identifique todos os personagens recorrentes.
+- Crie para CADA personagem uma ficha 'Visual DNA' hiper-específica e imutável em inglês: [Nome]: [Idade aproximada, traços físicos fofos, tipo e cor exata do cabelo com acessórios como laços/boné, peças de roupa exatas com suas cores fixas, calçados].
+- Crie o 'Scene DNA' do cenário principal com elementos encantadores e cores suaves.
+
+Fase 2 - Construção Modular dos Prompts dos Quadros:
+- NUNCA use prompts curtos ou genéricos!
+- Cada 'image_prompt' DEVE ser montado estritamente pela fórmula:
+ [Visual DNA dos Personagens presentes no quadro] + [Ação específica e expressão facial fofa] + [Scene DNA do Cenário] + [Estilo Global: "${CUTE_COMIC_STYLE_ANCHOR}"]
+
+Fase 3 - Diálogos e Falas:
+- Falas curtas, carinhosas e expressivas em português com no MÁXIMO 12 palavras por balão.`;
+
+ let userPrompt = '';
if (isContinuation) {
- prompt = `Continue a história em quadrinhos pedagógica infantil com o tema: "${tema}".
-A história deve conter mais ${quantidade} novos quadrinhos dando continuidade cronológica aos anteriores.
-Baloes de falas: ${baloes ? 'Sim, cada quadrinho deve conter um diálogo/fala conciso e expressivo, contendo no máximo 12 palavras no total.' : 'Não, a história é silenciosa, sem falas, apenas a ação dos personagens'}.
+ userPrompt = `Continue a história em quadrinhos pedagógica infantil com o tema: "${tema}".
+A história deve conter mais ${quantidade} novos quadrinhos dando continuidade cronológica a partir do quadro ${startPanelNum}.
+Baloes de falas: ${baloes ? 'Sim, falas curtas em português com no máximo 12 palavras.' : 'Não, história silenciosa.'}
Proporção visual: ${proporcao}.
-Cenário/Local principal da nova fase: ${cenario}.
+Cenário/Local principal da nova fase: ${cenario || 'cenário encantador e colorido'}.
+DNA Visual prévio dos personagens que DEVE ser mantido idêntico: "${character_description || ''}".
Gere um JSON perfeitamente válido contendo:
{
"title": "${continuationType === 'extend' ? 'Manter o título original ou propor um subtítulo condizente' : 'Novo Título da História'}",
- "character_description": "${character_description || 'Descrição física fixa dos personagens em inglês (roupas fixas, cabelos, cores)'}",
+ "character_description": "${character_description || 'Ficha Visual DNA dos personagens em inglês'}",
+ "scene_description": "Ficha Visual DNA do cenário em inglês",
"panels": [
{
"panel_number": ${startPanelNum},
- "image_prompt": "Cena cinematográfica completa em inglês em estilo 3D Pixar/Disney: descreva a ação dos personagens com suas roupas e traços físicos exatos inseridos organicamente no cenário (${cenario}), com iluminação mágica, cores vibrantes e atmosfera infantil encantadora.",
- "dialogue": "Falas do quadrinho em português (ex: 'Lucas: Veja aquilo!') com NO MÁXIMO 12 palavras, ou narração curta caso não tenha balões."
+ "image_prompt": "Prompt em inglês seguindo a fórmula: [Visual DNA dos personagens presentes] + [Ação/expressão fofa] + [Scene DNA] + [Sufixo de estilo: ${CUTE_COMIC_STYLE_ANCHOR}]",
+ "dialogue": "Falas do quadrinho em português com no máximo 12 palavras."
}
]
}
-
-Responda APENAS o JSON válido, sem qualquer texto explicativo antes ou depois.`;
+Responda APENAS o JSON válido.`;
} else {
- prompt = `Crie uma história em quadrinhos pedagógica infantil rica e envolvente com o tema: "${tema}".
-A história deve conter exatamente ${quantidade} quadrinhos.
-Baloes de falas: ${baloes ? 'Sim, cada quadrinho deve conter um diálogo/fala conciso e expressivo, contendo no máximo 12 palavras no total.' : 'Não, a história é silenciosa, sem falas, apenas a descrição da cena'}.
+ userPrompt = `Crie uma história em quadrinhos pedagógica infantil com o tema: "${tema}".
+Quantidade de quadrinhos: exatamente ${quantidade}.
+Baloes de falas: ${baloes ? 'Sim, falas curtas em português com no máximo 12 palavras.' : 'Não, história silenciosa.'}
Proporção visual: ${proporcao}.
-Personagens participantes: ${JSON.stringify(personagens)}.
-Cenário/Local principal: ${cenario}.
+Personagens informados pelo usuário: ${JSON.stringify(personagens || [])}.
+Cenário principal: ${cenario || 'ambiente mágico, ensolarado e acolhedor'}.
Gere um JSON perfeitamente válido contendo:
{
- "title": "Título Criativo da História",
- "character_description": "Descrição física detalhada e fixa dos personagens em inglês (ex: Lucas: a cheerful 6-year-old boy with short brown hair wearing a bright red t-shirt and blue shorts. Mariana: a 5-year-old girl with blonde pigtails in a yellow dress).",
+ "title": "Título Criativo e Encantador da História",
+ "character_description": "Ficha Visual DNA completa em inglês de CADA personagem (ex: Little Boy: cute 5-year-old toddler boy with chubby cheeks, messy short brown hair, wearing a red backward cap, bright blue short-sleeved t-shirt, khaki shorts, brown soft boots. Little Girl: cute 4-year-old toddler girl with blonde hair in two sweet pigtails with red ribbon scrunchies, wearing a soft yellow dress with a white pinafore apron, white socks and red shoes. Friendly Witch: cute stylized cartoon witch doll, gentle smiling face, purple robe, cute black pointed hat with a gold buckle, not scary at all).",
+ "scene_description": "Descrição Visual DNA detalhada do cenário principal em inglês.",
"panels": [
{
"panel_number": 1,
- "image_prompt": "Cena cinematográfica completa em inglês em estilo 3D Pixar/Disney: descreva a ação dos personagens com suas roupas e traços físicos exatos inseridos vividamente DENTRO do cenário principal (${cenario}), com cores vibrantes, iluminação mágica e atmosfera infantil encantadora.",
- "dialogue": "Falas do quadrinho em português (ex: 'Lucas: Vamos explorar a floresta!') com NO MÁXIMO 12 palavras, ou narração curta caso não tenha balões."
+ "image_prompt": "Prompt COMPLETO em inglês seguindo a fórmula: [Visual DNA dos personagens presentes no quadro] + [Ação e expressão facial fofa do momento] + [Scene DNA do ambiente] + [Sufixo de estilo: ${CUTE_COMIC_STYLE_ANCHOR}]",
+ "dialogue": "Falas do quadrinho em português com no máximo 12 palavras."
}
]
}
-
-Responda APENAS o JSON válido, sem qualquer texto explicativo antes ou depois.`;
+Responda APENAS o JSON válido.`;
}
const r = await callMinimax({
- system: "Você é um renomado diretor de arte e roteirista de histórias em quadrinhos infantis 3D estilo Pixar Disney. Você sempre descreve cenas ricas com personagens e cenários integrados naturalmente em inglês.",
- messages: [{ role: 'user', content: prompt }],
+ system: systemPrompt,
+ messages: [{ role: 'user', content: userPrompt }],
temperature: 0.4,
max_tokens: 3500
});
@@ -600,24 +630,24 @@ app.post('/api/comics/resync-frame-prompt', requireAuth, async (req, res) => {
}
try {
- const prompt = `Você é um diretor de arte de histórias em quadrinhos infantis 3D estilo Pixar.
-A professora alterou o diálogo/texto do quadrinho para: "${currentDialogue}".
-Descrição física fixa dos personagens (em inglês): "${character_description || ''}".
+ const prompt = `Você é o Diretor de Arte Master do pipeline ComicCharacterAndSceneConsistencyPipeline (estilo 3D Pixar/Claymation fofo para crianças de 2 a 6 anos).
+O diálogo/texto do quadrinho foi editado para: "${currentDialogue}".
+Ficha Visual DNA dos personagens: "${character_description || ''}".
Cenário fixo da história: "${cenario || ''}".
Prompt antigo da cena: "${oldPrompt || ''}".
-Re-escreva a descrição visual da cena (image_prompt) em inglês para o gerador de imagens.
-REGRAS RÍGIDAS DE FIDELIDADE VISUAL:
-1. Mantenha os mesmos personagens com a MESMA descrição física e roupas exatas (do character_description).
-2. Mantenha o mesmo estilo de iluminação e elementos do cenário (${cenario}).
-3. Adapte as ações, gestos e expressões faciais para combinarem EXATAMENTE com a nova fala/texto editado: "${currentDialogue}".
+Re-escreva a descrição visual da cena (image_prompt) em inglês para o gerador de imagens mantendo RIGOROSA consistência:
+1. Inclua o Visual DNA exato de quem estiver na cena (mesmas roupas, cabelos, cores e traços fofos).
+2. NUNCA gere traços assustadores, monstros ou feiúra (sempre meigo, fofo e inocente para crianças pequenas).
+3. Adapte a pose e expressão facial para combinar perfeitamente com a fala: "${currentDialogue}".
+4. Conclua com o sufixo de estilo: "${CUTE_COMIC_STYLE_ANCHOR}".
Retorne APENAS a nova descrição visual da cena em inglês, sem explicações.`;
const r = await callMinimax({
messages: [{ role: 'user', content: prompt }],
temperature: 0.3,
- max_tokens: 300
+ max_tokens: 450
});
const newPrompt = r.text.trim();
@@ -646,6 +676,12 @@ app.post('/api/comics/generate-frame', requireAuth, async (req, res) => {
let imageBuffer = null;
let providerUsed = 'minimax';
+ // Assegurar que o prompt contenha o estilo fofo
+ let finalPrompt = prompt;
+ if (!finalPrompt.toLowerCase().includes('claymation') && !finalPrompt.toLowerCase().includes('pixar')) {
+ finalPrompt = `${finalPrompt}, ${CUTE_COMIC_STYLE_ANCHOR}`;
+ }
+
// Tentativa 1: MiniMax image-01 com timeout de 38s
try {
const minmBase = (process.env.MINIMAX_API_BASE || 'https://api.minimax.io/v1').replace(/\/v1\/?$/, '');
@@ -663,7 +699,7 @@ app.post('/api/comics/generate-frame', requireAuth, async (req, res) => {
},
body: JSON.stringify({
model: 'image-01',
- prompt: prompt,
+ prompt: finalPrompt,
n: 1
}),
signal: controller.signal
@@ -689,13 +725,14 @@ app.post('/api/comics/generate-frame', requireAuth, async (req, res) => {
console.warn('[Comics Image] Falha/Timeout no MiniMax image-01 (' + minimaxErr.message + '), acionando gerador fallback...');
}
- // Tentativa 2 (Fallback Resiliente Multi-Engine com Retries):
+ // Tentativa 2 (Fallback Resiliente Multi-Engine com Retries e Proteção de Estilo Fofo):
if (!imageBuffer) {
const is169 = proporcao === '16:9' || (prompt && prompt.includes('16:9'));
const width = is169 ? 1280 : 1024;
const height = is169 ? 720 : 768;
- const cleanPrompt = (prompt || 'cute children book illustration').replace(/[^\w\s,.-]/gi, '');
- const enhancedPrompt = encodeURIComponent(`${cleanPrompt}, 3d pixar disney style, digital children book illustration, vibrant colors, masterpiece, cinematic lighting`);
+ const cleanPrompt = (finalPrompt || 'cute children book illustration').replace(/[^\w\s,.-]/gi, '');
+ const enhancedPrompt = encodeURIComponent(`${cleanPrompt}, adorable cute 3d pixar claymation style, soft rounded features, innocent children book illustration, vibrant colors, gentle atmosphere`);
+ const negativeEncoded = encodeURIComponent(CUTE_COMIC_NEGATIVE_GUARDRAIL);
const engines = ['flux', 'turbo'];
@@ -705,7 +742,7 @@ app.post('/api/comics/generate-frame', requireAuth, async (req, res) => {
try {
providerUsed = `pollinations-${engine}`;
const seed = Math.floor(Math.random() * 1000000);
- const fallbackUrl = `https://image.pollinations.ai/prompt/${enhancedPrompt}?width=${width}&height=${height}&model=${engine}&nologo=true&seed=${seed}`;
+ const fallbackUrl = `https://image.pollinations.ai/prompt/${enhancedPrompt}?width=${width}&height=${height}&model=${engine}&nologo=true&seed=${seed}&negative=${negativeEncoded}`;
const controller = new AbortController();
const timeoutId = setTimeout(() => controller.abort(), 25000);
@@ -769,7 +806,7 @@ app.post('/api/comics/generate-frame', requireAuth, async (req, res) => {
}
});
-// 3. Compilar a apresentação de vídeo usando FFmpeg a partir dos painéis de quadrinhos (Com Narração IA)
+// 3. Compilar a apresentação de vídeo usando FFmpeg a partir dos painéis de quadrinhos (Com Narração IA e Trilha Sonora)
app.post('/api/comics/generate-video', requireAuth, async (req, res) => {
const { frames, musicSelection, frameDuration, narrationVoice, titulo } = req.body;
@@ -794,16 +831,16 @@ app.post('/api/comics/generate-video', requireAuth, async (req, res) => {
const tempFileId = `${Date.now()}_${Math.floor(Math.random() * 1000)}`;
- // 1. Mapear trilha musical
+ // 1. Mapear trilha musical entre as 22 opções disponíveis
let bgMusicPath = null;
- if (musicFile !== 'sem_musica') {
- bgMusicPath = path.join(__dirname, 'public', 'assets', 'audio', 'alegre.mp3');
- if (musicFile === 'calma') {
- bgMusicPath = path.join(__dirname, 'public', 'assets', 'audio', 'calma.mp3');
- } else if (musicFile === 'aventura') {
- bgMusicPath = path.join(__dirname, 'public', 'assets', 'audio', 'aventura.mp3');
+ if (musicFile && musicFile !== 'sem_musica') {
+ const candidatePath = path.join(__dirname, 'public', 'assets', 'audio', `${musicFile}.mp3`);
+ if (fs.existsSync(candidatePath)) {
+ bgMusicPath = candidatePath;
+ } else {
+ const fallbackPath = path.join(__dirname, 'public', 'assets', 'audio', 'alegre.mp3');
+ if (fs.existsSync(fallbackPath)) bgMusicPath = fallbackPath;
}
- if (!fs.existsSync(bgMusicPath)) bgMusicPath = null;
}
// Estrutura para armazenar tempos e caminhos dos áudios de narração
@@ -813,21 +850,21 @@ app.post('/api/comics/generate-video', requireAuth, async (req, res) => {
// 2. Se a narração estiver ativada (mulher ou homem)
if (voice === 'mulher' || voice === 'homem') {
const voiceName = voice === 'homem' ? 'pt-BR-AntonioNeural' : 'pt-BR-FranciscaNeural';
- console.log(`[Video Comics] Gerando narração por IA (${voiceName})...`);
+ console.log(`[Video Comics] Gerando narração infantil carinhosa por IA (${voiceName})...`);
- // Gerar textos narrativos adaptados por IA para acompanhar a duração e fechar no final
+ // Gerar textos narrativos adaptados por IA para acompanhar a duração e fechar com carinho no final
let narrations = [];
try {
- const narrPrompt = `Você é um narrador especialista em audiolivros infantis e histórias em quadrinhos pedagógicas.
-Abaixo estão as cenas e falas dos quadros de uma História em Quadrinhos:
+ const narrPrompt = `Você é uma contadora de histórias e pedagoga especialista em audiolivros para crianças pequenas de 2 a 6 anos.
+Abaixo estão as falas e descrições dos quadros de uma História em Quadrinhos infantil:
Título: "${titulo || 'História em Quadrinhos'}"
-${frames.map((f, idx) => `Quadro ${idx + 1}: Fala/Diálogo: "${f.dialogue || ''}" | Descrição visual: "${f.image_prompt || ''}"`).join('\n')}
+${frames.map((f, idx) => `Quadro ${idx + 1}: Fala/Diálogo: "${f.dialogue || ''}" | Cena: "${f.image_prompt || ''}"`).join('\n')}
-Por favor, crie um texto de narração falado, acolhedor e envolvente em português do Brasil (pt-BR) para CADA QUADRO.
-- O texto de cada quadro deve ser fluido e natural para ser lido em voz alta.
-- Expanda as falas para contar a história com ritmo infantil acolhedor.
-- O ÚLTIMO QUADRO deve conter uma frase final marcante e carinhosa de conclusão da história.
-- Retorne estritamente um JSON no seguinte formato:
+Crie um texto de narração falado, acolhedor, doce e expressivo em português do Brasil (pt-BR) para CADA QUADRO.
+- Linguagem carinhosa e afetuosa para crianças pequenas (2 a 6 anos).
+- O texto de cada quadro deve ser fluido e natural para leitura em voz alta.
+- O ÚLTIMO QUADRO deve fechar a história com uma frase carinhosa de conclusão moral/pedagógica.
+- Retorne estritamente um JSON no formato:
{
"narrations": [
{ "panel_index": 0, "text": "Texto da narração do quadro 1..." }
@@ -836,7 +873,7 @@ Por favor, crie um texto de narração falado, acolhedor e envolvente em portugu
const aiResp = await callMinimax({
messages: [{ role: 'user', content: narrPrompt }],
- temperature: 0.7,
+ temperature: 0.6,
max_tokens: 1500
});
@@ -846,32 +883,47 @@ Por favor, crie um texto de narração falado, acolhedor e envolvente em portugu
narrations = parsed.narrations || [];
}
} catch (aiErr) {
- console.warn('[Video Comics] Erro ao gerar roteiro por IA, usando diálogos originais:', aiErr.message);
+ console.warn('[Video Comics] Erro ao gerar roteiro narrativo por IA, usando diálogos originais:', aiErr.message);
}
- // Sintetizar o áudio TTS de cada quadro
+ // Sintetizar o áudio TTS de cada quadro via Stream direta e segura
const tts = new MsEdgeTTS();
await tts.setMetadata(voiceName, OUTPUT_FORMAT.AUDIO_24KHZ_48KBITRATE_MONO_MP3);
for (let i = 0; i < frames.length; i++) {
const item = narrations.find(n => n.panel_index === i);
let narrText = item?.text || frames[i].dialogue || frames[i].image_prompt || `Quadro ${i + 1}`;
- // Limpar prefixos de falas
- narrText = narrText.replace(/^(Maria|João|Lucas|Pedro|Ana|Professora):\s*/i, '').trim();
+ // Limpar prefixos de falas tipo "João: ..."
+ narrText = narrText.replace(/^[A-Za-zÀ-ÖØ-öø-ÿ\s]+:\s*/, '').trim();
+ if (!narrText) narrText = `Era uma vez, no quadro ${i + 1}.`;
const audioPath = path.join(mediaDir, `narr_${tempFileId}_p${i}.mp3`);
try {
- await tts.toFile(audioPath, narrText);
- narrationAudioFiles.push(audioPath);
+ const { audioStream } = tts.toStream(narrText);
+ await new Promise((resolve, reject) => {
+ const ws = fs.createWriteStream(audioPath);
+ audioStream.pipe(ws);
+ ws.on('finish', resolve);
+ ws.on('error', reject);
+ audioStream.on('error', reject);
+ });
- // Calcular duração estimada do MP3 (~6000 bytes por segundo a 48kbps mono MP3)
- const stats = fs.statSync(audioPath);
- const audioSecs = Math.max(3, Math.ceil(stats.size / 6000));
- // O tempo do quadro é o máximo entre a duração base e o tempo da narração + 1s de respiro
- const frameTime = Math.max(baseDuration, audioSecs + 1);
- panelDurations.push(frameTime);
+ if (fs.existsSync(audioPath) && fs.statSync(audioPath).size > 300) {
+ narrationAudioFiles.push(audioPath);
+
+ // Calcular duração estimada do áudio (~6000 bytes por segundo a 48kbps mono MP3)
+ const stats = fs.statSync(audioPath);
+ const audioSecs = Math.max(3, Math.ceil(stats.size / 6000));
+ // O tempo do quadro é o máximo entre a duração base e o tempo da narração + 1s de respiro
+ const frameTime = Math.max(baseDuration, audioSecs + 1);
+ panelDurations.push(frameTime);
+ console.log(`[Video Comics] Narração gerada com sucesso para quadro ${i + 1} (${audioSecs}s)`);
+ } else {
+ console.warn(`[Video Comics] Áudio TTS vazio ou corrompido para quadro ${i + 1}`);
+ panelDurations.push(baseDuration);
+ }
} catch (ttsErr) {
- console.warn(`[Video Comics] Falha TTS quadro ${i}:`, ttsErr.message);
+ console.warn(`[Video Comics] Falha TTS quadro ${i + 1}:`, ttsErr.message);
panelDurations.push(baseDuration);
}
}
@@ -884,10 +936,12 @@ Por favor, crie um texto de narração falado, acolhedor e envolvente em portugu
const concatFilePath = path.join(__dirname, `concat_${tempFileId}.txt`);
let concatContent = '';
frames.forEach((frame, idx) => {
- const localFilePath = path.join(__dirname, 'public', frame.imageUrl);
+ const cleanRelPath = (frame.imageUrl || frame.image_url || '').replace(/^\/+/, '');
+ const localFilePath = path.join(__dirname, 'public', cleanRelPath);
concatContent += `file '${localFilePath}'\nduration ${panelDurations[idx]}\n`;
});
- const lastLocalFilePath = path.join(__dirname, 'public', frames[frames.length - 1].imageUrl);
+ const lastRelPath = (frames[frames.length - 1].imageUrl || frames[frames.length - 1].image_url || '').replace(/^\/+/, '');
+ const lastLocalFilePath = path.join(__dirname, 'public', lastRelPath);
concatContent += `file '${lastLocalFilePath}'\n`;
fs.writeFileSync(concatFilePath, concatContent);
@@ -918,14 +972,14 @@ Por favor, crie um texto de narração falado, acolhedor e envolvente em portugu
let ffmpegCommand = '';
if (concatAudioPath && bgMusicPath) {
- // Narração + Música de fundo (mixagem com volume suave na música)
- ffmpegCommand = `ffmpeg -y -f concat -safe 0 -i "${concatFilePath}" -i "${concatAudioPath}" -i "${bgMusicPath}" -filter_complex "[1:a]volume=1.2[narr];[2:a]volume=0.15[bg];[narr][bg]amix=inputs=2:duration=first[aout]" -map 0:v -map "[aout]" -c:v libx264 -pix_fmt yuv420p -c:a aac -shortest -t ${totalDuration} "${outputFilePath}"`;
+ // Narração + Música de fundo (mixagem com volume da voz em 1.3 e música de fundo suave em 0.12)
+ ffmpegCommand = `ffmpeg -y -f concat -safe 0 -i "${concatFilePath}" -i "${concatAudioPath}" -stream_loop -1 -i "${bgMusicPath}" -filter_complex "[1:a]volume=1.3[narr];[2:a]volume=0.12[bg];[narr][bg]amix=inputs=2:duration=first[aout]" -map 0:v -map "[aout]" -c:v libx264 -pix_fmt yuv420p -c:a aac -shortest -t ${totalDuration} "${outputFilePath}"`;
} else if (concatAudioPath) {
// Narração apenas (sem música de fundo)
ffmpegCommand = `ffmpeg -y -f concat -safe 0 -i "${concatFilePath}" -i "${concatAudioPath}" -c:v libx264 -pix_fmt yuv420p -c:a aac -shortest -t ${totalDuration} "${outputFilePath}"`;
} else if (bgMusicPath) {
// Apenas música de fundo (sem voz)
- ffmpegCommand = `ffmpeg -y -f concat -safe 0 -i "${concatFilePath}" -i "${bgMusicPath}" -c:v libx264 -pix_fmt yuv420p -c:a aac -shortest -t ${totalDuration} "${outputFilePath}"`;
+ ffmpegCommand = `ffmpeg -y -f concat -safe 0 -i "${concatFilePath}" -stream_loop -1 -i "${bgMusicPath}" -c:v libx264 -pix_fmt yuv420p -c:a aac -shortest -t ${totalDuration} "${outputFilePath}"`;
} else {
// Sem áudio
ffmpegCommand = `ffmpeg -y -f concat -safe 0 -i "${concatFilePath}" -c:v libx264 -pix_fmt yuv420p -t ${totalDuration} "${outputFilePath}"`;
diff --git a/update.sh b/update.sh
index 5c42f2a..3057849 100755
--- a/update.sh
+++ b/update.sh
@@ -27,9 +27,15 @@ fi
COOLIFY_RESOURCE_UUID="zzmj37ya8lmntomh3bm8rivi"
COOLIFY_TOKEN="12|wbepNILQe24LAOjfEUmMROgU93F6uG1zuwPLwrRi786bca03"
-echo -e "${YELLOW}🔄 Disparando Deploy na Coolify...${NC}"
-
-curl -s -X GET "https://painel.reifonas.cloud/api/v1/deploy?uuid=${COOLIFY_RESOURCE_UUID}&force=false" \
- -H "Authorization: Bearer $COOLIFY_TOKEN"
-
echo -e "\n${GREEN}✅ Deploy engatilhado com sucesso!${NC}\n"
+
+# 3. Sincronização pós-deploy do .env no container em background
+(
+ sleep 45
+ CONTAINER_ID=$(docker ps --filter "name=zzmj37ya8lmntomh3bm8rivi" --format "{{.ID}}" | head -n 1)
+ if [ -n "$CONTAINER_ID" ]; then
+ docker cp /root/Apps/Camila/.env "$CONTAINER_ID":/app/.env 2>/dev/null
+ docker restart "$CONTAINER_ID" 2>/dev/null
+ fi
+) >/dev/null 2>&1 &
+