Consistencia visual dos quadrinhos (estilo fofo 2 a 6 anos), correcao de narracao em video e 22 trilhas com preview
This commit is contained in:
+83
-4
@@ -4889,6 +4889,7 @@ Se algum dado não for mencionado, deixe a string vazia ou false para boolean.`
|
|||||||
}
|
}
|
||||||
fabricaQuadrinhosModal.style.display = 'none';
|
fabricaQuadrinhosModal.style.display = 'none';
|
||||||
if (comicsVideoPlayer) comicsVideoPlayer.pause();
|
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;
|
const totalPanels = scriptData.panels.length;
|
||||||
let completedCount = 0;
|
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 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 attempts = 0;
|
||||||
let frameData = null;
|
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', {
|
const resp = await fetch('/api/comics/generate-frame', {
|
||||||
method: 'POST',
|
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 () => {
|
btnCompileComicsVideo.addEventListener('click', async () => {
|
||||||
if (generatedPanelsData.length === 0) return;
|
if (generatedPanelsData.length === 0) return;
|
||||||
|
|
||||||
|
stopComicsMusicPreview();
|
||||||
btnCompileComicsVideo.disabled = true;
|
btnCompileComicsVideo.disabled = true;
|
||||||
comicsVideoLoader.style.display = 'flex';
|
comicsVideoLoader.style.display = 'flex';
|
||||||
comicsVideoResult.style.display = 'none';
|
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' },
|
headers: { 'Content-Type': 'application/json' },
|
||||||
body: JSON.stringify({
|
body: JSON.stringify({
|
||||||
frames: generatedPanelsData,
|
frames: generatedPanelsData,
|
||||||
musicSelection: comicsVideoMusic.value,
|
musicSelection: comicsVideoMusic ? comicsVideoMusic.value : 'alegre',
|
||||||
frameDuration: comicsVideoDuration.value,
|
frameDuration: comicsVideoDuration ? comicsVideoDuration.value : 5,
|
||||||
narrationVoice: voice,
|
narrationVoice: voice,
|
||||||
titulo: comicsResultTitle?.textContent || 'História em Quadrinhos'
|
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)
|
// VIDEOMIND (TRANSCRIÇÃO E ANÁLISE DE VÍDEO PEDAGÓGICO)
|
||||||
// ============================================================
|
// ============================================================
|
||||||
|
|||||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
+46
-7
@@ -2073,13 +2073,52 @@
|
|||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
<div class="settings-group" style="gap: 4px;">
|
<div class="settings-group" style="gap: 4px;">
|
||||||
<label style="font-size: 0.72rem; color: var(--text-secondary); font-weight: 600;">🎵 Trilha Sonora de Fundo</label>
|
<label style="font-size: 0.72rem; color: var(--text-secondary); font-weight: 600; display: flex; justify-content: space-between; align-items: center;">
|
||||||
<select id="comicsVideoMusic" class="obs-select" style="padding: 6px 10px; font-size: 0.8rem;">
|
<span>🎵 Trilha Sonora de Fundo</span>
|
||||||
<option value="alegre">🎸 Trilha 1: Alegre e Divertida</option>
|
<span id="comicsMusicPlayingTag" style="font-size: 0.7rem; color: #10a37f; display: none;">🔊 Tocando...</span>
|
||||||
<option value="calma">🌙 Trilha 2: Calma e Relaxante</option>
|
</label>
|
||||||
<option value="aventura">🎮 Trilha 3: Aventura e Mistério</option>
|
<div style="display: flex; gap: 6px; align-items: center;">
|
||||||
<option value="sem_musica">🔇 Sem Música de Fundo</option>
|
<select id="comicsVideoMusic" class="obs-select" style="padding: 6px 8px; font-size: 0.8rem; flex: 1;">
|
||||||
</select>
|
<optgroup label="🎸 Alegres & Brincalhonas">
|
||||||
|
<option value="alegre" selected>🎸 Alegre e Brincalhona</option>
|
||||||
|
<option value="festa">🎈 Dia de Festa & Balões</option>
|
||||||
|
<option value="pipoca">🍿 Pula Pula Pipoquinha</option>
|
||||||
|
<option value="passeio">🚲 Passeio no Parque</option>
|
||||||
|
</optgroup>
|
||||||
|
<optgroup label="🌙 Calmas & Canções de Ninar">
|
||||||
|
<option value="calma">🌙 Calma e Relaxante</option>
|
||||||
|
<option value="ninar">⭐ Estrelinha Brilhante (Ninar)</option>
|
||||||
|
<option value="bebes">🍼 Melodia Suave do Berço</option>
|
||||||
|
<option value="amizade">🤝 Abraço de Amigo & Carinho</option>
|
||||||
|
</optgroup>
|
||||||
|
<optgroup label="✨ Contos de Fadas & Magia">
|
||||||
|
<option value="magica">✨ Terra da Fantasia & Magia</option>
|
||||||
|
<option value="castelo">🏰 Castelo dos Contos de Fadas</option>
|
||||||
|
<option value="carrossel">🎠 Carrossel Encantado</option>
|
||||||
|
<option value="brinquedos">🧸 Fábrica de Brinquedos</option>
|
||||||
|
<option value="circo">🎪 Circo Encantado & Palhacinhos</option>
|
||||||
|
</optgroup>
|
||||||
|
<optgroup label="🎮 Aventuras & Curiosidade">
|
||||||
|
<option value="aventura">🎮 Aventura na Floresta</option>
|
||||||
|
<option value="curiosa">🔍 Pequenos Detetives</option>
|
||||||
|
<option value="herois">🦸 Mini Super-Heróis</option>
|
||||||
|
<option value="espaco">🚀 Viagem ao Espaço Sideral</option>
|
||||||
|
</optgroup>
|
||||||
|
<optgroup label="🍃 Natureza & Bichinhos">
|
||||||
|
<option value="animais">🐾 Fazendinha dos Bichinhos</option>
|
||||||
|
<option value="jardim">🌸 Jardim das Borboletas</option>
|
||||||
|
<option value="mar">🐬 Ondinhas do Mar & Golfinhos</option>
|
||||||
|
<option value="natureza">🍃 Sons Doces da Natureza</option>
|
||||||
|
<option value="chuva">🌧️ Gotinhas de Chuva Dançantes</option>
|
||||||
|
</optgroup>
|
||||||
|
<optgroup label="🔇 Sem Áudio">
|
||||||
|
<option value="sem_musica">🔇 Sem Música de Fundo</option>
|
||||||
|
</optgroup>
|
||||||
|
</select>
|
||||||
|
<button type="button" id="btnPreviewComicsMusic" class="btn-music-option" style="padding: 6px 10px; font-size: 0.8rem; margin: 0; background: rgba(219, 39, 119, 0.1); border-color: var(--brand-pink); color: var(--brand-pink); font-weight: 600; white-space: nowrap; display: flex; align-items: center; gap: 4px;" title="Ouvir prévia desta trilha sonora">
|
||||||
|
<span id="comicsMusicPreviewIcon">▶️</span> <span id="comicsMusicPreviewText">Ouvir</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="settings-group" style="gap: 4px;">
|
<div class="settings-group" style="gap: 4px;">
|
||||||
<label style="font-size: 0.72rem; color: var(--text-secondary); font-weight: 600;">⏱️ Tempo Mínimo / Cena</label>
|
<label style="font-size: 0.72rem; color: var(--text-secondary); font-weight: 600;">⏱️ Tempo Mínimo / Cena</label>
|
||||||
|
|||||||
@@ -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!');
|
||||||
@@ -493,7 +493,11 @@ app.delete('/api/music/:id', requireAuth, async (req, res) => {
|
|||||||
// ROTAS DO ESTÚDIO: FÁBRICA DE QUADRINHOS
|
// 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) => {
|
app.post('/api/comics/generate-script', requireAuth, async (req, res) => {
|
||||||
const {
|
const {
|
||||||
tema,
|
tema,
|
||||||
@@ -512,58 +516,84 @@ app.post('/api/comics/generate-script', requireAuth, async (req, res) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
let prompt = '';
|
|
||||||
const isContinuation = existingPanels && Array.isArray(existingPanels) && existingPanels.length > 0 && continuationType && continuationType !== 'new';
|
const isContinuation = existingPanels && Array.isArray(existingPanels) && existingPanels.length > 0 && continuationType && continuationType !== 'new';
|
||||||
const startPanelNum = (existingPanels && Array.isArray(existingPanels)) ? existingPanels.length + 1 : 1;
|
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) {
|
if (isContinuation) {
|
||||||
prompt = `Continue a história em quadrinhos pedagógica infantil com o tema: "${tema}".
|
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 aos anteriores.
|
A história deve conter mais ${quantidade} novos quadrinhos dando continuidade cronológica a partir do quadro ${startPanelNum}.
|
||||||
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'}.
|
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}.
|
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:
|
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'}",
|
"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": [
|
"panels": [
|
||||||
{
|
{
|
||||||
"panel_number": ${startPanelNum},
|
"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.",
|
"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 (ex: 'Lucas: Veja aquilo!') com NO MÁXIMO 12 palavras, ou narração curta caso não tenha balões."
|
"dialogue": "Falas do quadrinho em português com no máximo 12 palavras."
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
Responda APENAS o JSON válido.`;
|
||||||
Responda APENAS o JSON válido, sem qualquer texto explicativo antes ou depois.`;
|
|
||||||
} else {
|
} else {
|
||||||
prompt = `Crie uma história em quadrinhos pedagógica infantil rica e envolvente com o tema: "${tema}".
|
userPrompt = `Crie uma história em quadrinhos pedagógica infantil com o tema: "${tema}".
|
||||||
A história deve conter exatamente ${quantidade} quadrinhos.
|
Quantidade de quadrinhos: exatamente ${quantidade}.
|
||||||
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'}.
|
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}.
|
Proporção visual: ${proporcao}.
|
||||||
Personagens participantes: ${JSON.stringify(personagens)}.
|
Personagens informados pelo usuário: ${JSON.stringify(personagens || [])}.
|
||||||
Cenário/Local principal: ${cenario}.
|
Cenário principal: ${cenario || 'ambiente mágico, ensolarado e acolhedor'}.
|
||||||
|
|
||||||
Gere um JSON perfeitamente válido contendo:
|
Gere um JSON perfeitamente válido contendo:
|
||||||
{
|
{
|
||||||
"title": "Título Criativo da História",
|
"title": "Título Criativo e Encantador 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).",
|
"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": [
|
"panels": [
|
||||||
{
|
{
|
||||||
"panel_number": 1,
|
"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.",
|
"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 (ex: 'Lucas: Vamos explorar a floresta!') com NO MÁXIMO 12 palavras, ou narração curta caso não tenha balões."
|
"dialogue": "Falas do quadrinho em português com no máximo 12 palavras."
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
Responda APENAS o JSON válido.`;
|
||||||
Responda APENAS o JSON válido, sem qualquer texto explicativo antes ou depois.`;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const r = await callMinimax({
|
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.",
|
system: systemPrompt,
|
||||||
messages: [{ role: 'user', content: prompt }],
|
messages: [{ role: 'user', content: userPrompt }],
|
||||||
temperature: 0.4,
|
temperature: 0.4,
|
||||||
max_tokens: 3500
|
max_tokens: 3500
|
||||||
});
|
});
|
||||||
@@ -600,24 +630,24 @@ app.post('/api/comics/resync-frame-prompt', requireAuth, async (req, res) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const prompt = `Você é um diretor de arte de histórias em quadrinhos infantis 3D estilo Pixar.
|
const prompt = `Você é o Diretor de Arte Master do pipeline ComicCharacterAndSceneConsistencyPipeline (estilo 3D Pixar/Claymation fofo para crianças de 2 a 6 anos).
|
||||||
A professora alterou o diálogo/texto do quadrinho para: "${currentDialogue}".
|
O diálogo/texto do quadrinho foi editado para: "${currentDialogue}".
|
||||||
Descrição física fixa dos personagens (em inglês): "${character_description || ''}".
|
Ficha Visual DNA dos personagens: "${character_description || ''}".
|
||||||
Cenário fixo da história: "${cenario || ''}".
|
Cenário fixo da história: "${cenario || ''}".
|
||||||
Prompt antigo da cena: "${oldPrompt || ''}".
|
Prompt antigo da cena: "${oldPrompt || ''}".
|
||||||
|
|
||||||
Re-escreva a descrição visual da cena (image_prompt) em inglês para o gerador de imagens.
|
Re-escreva a descrição visual da cena (image_prompt) em inglês para o gerador de imagens mantendo RIGOROSA consistência:
|
||||||
REGRAS RÍGIDAS DE FIDELIDADE VISUAL:
|
1. Inclua o Visual DNA exato de quem estiver na cena (mesmas roupas, cabelos, cores e traços fofos).
|
||||||
1. Mantenha os mesmos personagens com a MESMA descrição física e roupas exatas (do character_description).
|
2. NUNCA gere traços assustadores, monstros ou feiúra (sempre meigo, fofo e inocente para crianças pequenas).
|
||||||
2. Mantenha o mesmo estilo de iluminação e elementos do cenário (${cenario}).
|
3. Adapte a pose e expressão facial para combinar perfeitamente com a fala: "${currentDialogue}".
|
||||||
3. Adapte as ações, gestos e expressões faciais para combinarem EXATAMENTE com a nova fala/texto editado: "${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.`;
|
Retorne APENAS a nova descrição visual da cena em inglês, sem explicações.`;
|
||||||
|
|
||||||
const r = await callMinimax({
|
const r = await callMinimax({
|
||||||
messages: [{ role: 'user', content: prompt }],
|
messages: [{ role: 'user', content: prompt }],
|
||||||
temperature: 0.3,
|
temperature: 0.3,
|
||||||
max_tokens: 300
|
max_tokens: 450
|
||||||
});
|
});
|
||||||
|
|
||||||
const newPrompt = r.text.trim();
|
const newPrompt = r.text.trim();
|
||||||
@@ -646,6 +676,12 @@ app.post('/api/comics/generate-frame', requireAuth, async (req, res) => {
|
|||||||
let imageBuffer = null;
|
let imageBuffer = null;
|
||||||
let providerUsed = 'minimax';
|
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
|
// Tentativa 1: MiniMax image-01 com timeout de 38s
|
||||||
try {
|
try {
|
||||||
const minmBase = (process.env.MINIMAX_API_BASE || 'https://api.minimax.io/v1').replace(/\/v1\/?$/, '');
|
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({
|
body: JSON.stringify({
|
||||||
model: 'image-01',
|
model: 'image-01',
|
||||||
prompt: prompt,
|
prompt: finalPrompt,
|
||||||
n: 1
|
n: 1
|
||||||
}),
|
}),
|
||||||
signal: controller.signal
|
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...');
|
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) {
|
if (!imageBuffer) {
|
||||||
const is169 = proporcao === '16:9' || (prompt && prompt.includes('16:9'));
|
const is169 = proporcao === '16:9' || (prompt && prompt.includes('16:9'));
|
||||||
const width = is169 ? 1280 : 1024;
|
const width = is169 ? 1280 : 1024;
|
||||||
const height = is169 ? 720 : 768;
|
const height = is169 ? 720 : 768;
|
||||||
const cleanPrompt = (prompt || 'cute children book illustration').replace(/[^\w\s,.-]/gi, '');
|
const cleanPrompt = (finalPrompt || '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 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'];
|
const engines = ['flux', 'turbo'];
|
||||||
|
|
||||||
@@ -705,7 +742,7 @@ app.post('/api/comics/generate-frame', requireAuth, async (req, res) => {
|
|||||||
try {
|
try {
|
||||||
providerUsed = `pollinations-${engine}`;
|
providerUsed = `pollinations-${engine}`;
|
||||||
const seed = Math.floor(Math.random() * 1000000);
|
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 controller = new AbortController();
|
||||||
const timeoutId = setTimeout(() => controller.abort(), 25000);
|
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) => {
|
app.post('/api/comics/generate-video', requireAuth, async (req, res) => {
|
||||||
const { frames, musicSelection, frameDuration, narrationVoice, titulo } = req.body;
|
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)}`;
|
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;
|
let bgMusicPath = null;
|
||||||
if (musicFile !== 'sem_musica') {
|
if (musicFile && musicFile !== 'sem_musica') {
|
||||||
bgMusicPath = path.join(__dirname, 'public', 'assets', 'audio', 'alegre.mp3');
|
const candidatePath = path.join(__dirname, 'public', 'assets', 'audio', `${musicFile}.mp3`);
|
||||||
if (musicFile === 'calma') {
|
if (fs.existsSync(candidatePath)) {
|
||||||
bgMusicPath = path.join(__dirname, 'public', 'assets', 'audio', 'calma.mp3');
|
bgMusicPath = candidatePath;
|
||||||
} else if (musicFile === 'aventura') {
|
} else {
|
||||||
bgMusicPath = path.join(__dirname, 'public', 'assets', 'audio', 'aventura.mp3');
|
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
|
// 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)
|
// 2. Se a narração estiver ativada (mulher ou homem)
|
||||||
if (voice === 'mulher' || voice === 'homem') {
|
if (voice === 'mulher' || voice === 'homem') {
|
||||||
const voiceName = voice === 'homem' ? 'pt-BR-AntonioNeural' : 'pt-BR-FranciscaNeural';
|
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 = [];
|
let narrations = [];
|
||||||
try {
|
try {
|
||||||
const narrPrompt = `Você é um narrador especialista em audiolivros infantis e histórias em quadrinhos pedagógicas.
|
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 cenas e falas dos quadros de uma História em Quadrinhos:
|
Abaixo estão as falas e descrições dos quadros de uma História em Quadrinhos infantil:
|
||||||
Título: "${titulo || 'História em Quadrinhos'}"
|
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.
|
Crie um texto de narração falado, acolhedor, doce e expressivo 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.
|
- Linguagem carinhosa e afetuosa para crianças pequenas (2 a 6 anos).
|
||||||
- Expanda as falas para contar a história com ritmo infantil acolhedor.
|
- O texto de cada quadro deve ser fluido e natural para leitura em voz alta.
|
||||||
- O ÚLTIMO QUADRO deve conter uma frase final marcante e carinhosa de conclusão da história.
|
- O ÚLTIMO QUADRO deve fechar a história com uma frase carinhosa de conclusão moral/pedagógica.
|
||||||
- Retorne estritamente um JSON no seguinte formato:
|
- Retorne estritamente um JSON no formato:
|
||||||
{
|
{
|
||||||
"narrations": [
|
"narrations": [
|
||||||
{ "panel_index": 0, "text": "Texto da narração do quadro 1..." }
|
{ "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({
|
const aiResp = await callMinimax({
|
||||||
messages: [{ role: 'user', content: narrPrompt }],
|
messages: [{ role: 'user', content: narrPrompt }],
|
||||||
temperature: 0.7,
|
temperature: 0.6,
|
||||||
max_tokens: 1500
|
max_tokens: 1500
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -846,32 +883,47 @@ Por favor, crie um texto de narração falado, acolhedor e envolvente em portugu
|
|||||||
narrations = parsed.narrations || [];
|
narrations = parsed.narrations || [];
|
||||||
}
|
}
|
||||||
} catch (aiErr) {
|
} 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();
|
const tts = new MsEdgeTTS();
|
||||||
await tts.setMetadata(voiceName, OUTPUT_FORMAT.AUDIO_24KHZ_48KBITRATE_MONO_MP3);
|
await tts.setMetadata(voiceName, OUTPUT_FORMAT.AUDIO_24KHZ_48KBITRATE_MONO_MP3);
|
||||||
|
|
||||||
for (let i = 0; i < frames.length; i++) {
|
for (let i = 0; i < frames.length; i++) {
|
||||||
const item = narrations.find(n => n.panel_index === i);
|
const item = narrations.find(n => n.panel_index === i);
|
||||||
let narrText = item?.text || frames[i].dialogue || frames[i].image_prompt || `Quadro ${i + 1}`;
|
let narrText = item?.text || frames[i].dialogue || frames[i].image_prompt || `Quadro ${i + 1}`;
|
||||||
// Limpar prefixos de falas
|
// Limpar prefixos de falas tipo "João: ..."
|
||||||
narrText = narrText.replace(/^(Maria|João|Lucas|Pedro|Ana|Professora):\s*/i, '').trim();
|
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`);
|
const audioPath = path.join(mediaDir, `narr_${tempFileId}_p${i}.mp3`);
|
||||||
try {
|
try {
|
||||||
await tts.toFile(audioPath, narrText);
|
const { audioStream } = tts.toStream(narrText);
|
||||||
narrationAudioFiles.push(audioPath);
|
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)
|
if (fs.existsSync(audioPath) && fs.statSync(audioPath).size > 300) {
|
||||||
const stats = fs.statSync(audioPath);
|
narrationAudioFiles.push(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
|
// Calcular duração estimada do áudio (~6000 bytes por segundo a 48kbps mono MP3)
|
||||||
const frameTime = Math.max(baseDuration, audioSecs + 1);
|
const stats = fs.statSync(audioPath);
|
||||||
panelDurations.push(frameTime);
|
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) {
|
} 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);
|
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`);
|
const concatFilePath = path.join(__dirname, `concat_${tempFileId}.txt`);
|
||||||
let concatContent = '';
|
let concatContent = '';
|
||||||
frames.forEach((frame, idx) => {
|
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`;
|
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`;
|
concatContent += `file '${lastLocalFilePath}'\n`;
|
||||||
fs.writeFileSync(concatFilePath, concatContent);
|
fs.writeFileSync(concatFilePath, concatContent);
|
||||||
|
|
||||||
@@ -918,14 +972,14 @@ Por favor, crie um texto de narração falado, acolhedor e envolvente em portugu
|
|||||||
let ffmpegCommand = '';
|
let ffmpegCommand = '';
|
||||||
|
|
||||||
if (concatAudioPath && bgMusicPath) {
|
if (concatAudioPath && bgMusicPath) {
|
||||||
// Narração + Música de fundo (mixagem com volume suave na música)
|
// 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}" -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}"`;
|
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) {
|
} else if (concatAudioPath) {
|
||||||
// Narração apenas (sem música de fundo)
|
// 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}"`;
|
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) {
|
} else if (bgMusicPath) {
|
||||||
// Apenas música de fundo (sem voz)
|
// 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 {
|
} else {
|
||||||
// Sem áudio
|
// Sem áudio
|
||||||
ffmpegCommand = `ffmpeg -y -f concat -safe 0 -i "${concatFilePath}" -c:v libx264 -pix_fmt yuv420p -t ${totalDuration} "${outputFilePath}"`;
|
ffmpegCommand = `ffmpeg -y -f concat -safe 0 -i "${concatFilePath}" -c:v libx264 -pix_fmt yuv420p -t ${totalDuration} "${outputFilePath}"`;
|
||||||
|
|||||||
@@ -27,9 +27,15 @@ fi
|
|||||||
COOLIFY_RESOURCE_UUID="zzmj37ya8lmntomh3bm8rivi"
|
COOLIFY_RESOURCE_UUID="zzmj37ya8lmntomh3bm8rivi"
|
||||||
COOLIFY_TOKEN="12|wbepNILQe24LAOjfEUmMROgU93F6uG1zuwPLwrRi786bca03"
|
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"
|
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 &
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user