Corrigindo salvamento de múltiplos projetos e continuidade de quadrinhos com consistência de personagens

This commit is contained in:
2026-06-03 17:54:59 +00:00
parent 7fd500eec1
commit 133bf185e9
3 changed files with 127 additions and 70 deletions
+99 -60
View File
@@ -2097,56 +2097,47 @@ const initApp = () => {
const mode = btn.dataset.mode; const mode = btn.dataset.mode;
const text = userInput.value.trim(); const text = userInput.value.trim();
// Se for modo POETRY ou TEXT, ou se o input estiver vazio, preenchemos o input com a sugestão correspondente sem enviar let suggestion = "";
const isTextOrPoetry = (mode === 'POETRY' || mode === 'TEXT'); let baseText = text;
if (!text || isTextOrPoetry) { // Se o texto atual já for uma das sugestões padrão, limpamos para não duplicar
let suggestion = ""; const suggestionsList = [
let baseText = text; "Crie uma música pedagógica alegre e animada sobre ",
"Crie um roteiro detalhado para um vídeo educativo sobre ",
// Se o texto atual já for uma das sugestões padrão, limpamos para não duplicar "Crie uma ilustração pedagógica com cores vibrantes mostrando ",
const suggestionsList = [ "Escreva uma poesia infantil rimada e educativa sobre ",
"Crie uma música pedagógica alegre e animada sobre ", "Escreva uma história educativa infantil curta sobre "
"Crie um roteiro detalhado para um vídeo educativo sobre ", ];
"Crie uma ilustração pedagógica com cores vibrantes mostrando ",
"Escreva uma poesia infantil rimada e educativa sobre ", for (const sugg of suggestionsList) {
"Escreva uma história educativa infantil curta sobre " if (baseText.startsWith(sugg)) {
]; baseText = baseText.replace(sugg, "").trim();
break;
for (const sugg of suggestionsList) {
if (baseText.startsWith(sugg)) {
baseText = baseText.replace(sugg, "");
break;
}
} }
if (mode === 'AUDIO') {
suggestion = "Crie uma música pedagógica alegre e animada sobre ";
} else if (mode === 'VIDEO') {
suggestion = "Crie um roteiro detalhado para um vídeo educativo sobre ";
} else if (mode === 'IMAGE') {
suggestion = "Crie uma ilustração pedagógica com cores vibrantes mostrando ";
} else if (mode === 'POETRY') {
suggestion = "Escreva uma poesia infantil rimada e educativa sobre ";
} else if (mode === 'TEXT') {
suggestion = "Escreva uma história educativa infantil curta sobre ";
}
userInput.value = suggestion + baseText;
userInput.focus();
// Mover cursor para o final do texto
userInput.selectionStart = userInput.selectionEnd = userInput.value.length;
// Ajustar altura do textarea para acomodar a sugestão
userInput.style.height = 'auto';
userInput.style.height = userInput.scrollHeight + 'px';
btnSend.disabled = false;
return;
} }
// Se houver texto e for outro modo (AUDIO, VIDEO, IMAGE), envia imediatamente if (mode === 'AUDIO') {
handleSendMessage(text, false, null, mode); suggestion = "Crie uma música pedagógica alegre e animada sobre ";
} else if (mode === 'VIDEO') {
suggestion = "Crie um roteiro detalhado para um vídeo educativo sobre ";
} else if (mode === 'IMAGE') {
suggestion = "Crie uma ilustração pedagógica com cores vibrantes mostrando ";
} else if (mode === 'POETRY') {
suggestion = "Escreva uma poesia infantil rimada e educativa sobre ";
} else if (mode === 'TEXT') {
suggestion = "Escreva uma história educativa infantil curta sobre ";
}
userInput.value = suggestion + baseText;
userInput.focus();
// Mover cursor para o final do texto
userInput.selectionStart = userInput.selectionEnd = userInput.value.length;
// Ajustar altura do textarea para acomodar a sugestão
userInput.style.height = 'auto';
userInput.style.height = userInput.scrollHeight + 'px';
if (btnSend) btnSend.disabled = false;
}); });
}); });
@@ -3515,6 +3506,7 @@ const initApp = () => {
const btnSaveComicsProject = document.getElementById('btnSaveComicsProject'); const btnSaveComicsProject = document.getElementById('btnSaveComicsProject');
let currentComicsProjectId = null; let currentComicsProjectId = null;
let currentComicsCharDescEnglish = '';
let selectedComicsQty = 5; let selectedComicsQty = 5;
let selectedComicsBubbles = true; let selectedComicsBubbles = true;
let selectedComicsRatio = '16:9'; let selectedComicsRatio = '16:9';
@@ -3617,6 +3609,13 @@ const initApp = () => {
// --- GERENCIADOR DE PROJETOS DE HQ --- // --- GERENCIADOR DE PROJETOS DE HQ ---
const startNewComicsProject = () => { const startNewComicsProject = () => {
currentComicsProjectId = null; currentComicsProjectId = null;
currentComicsCharDescEnglish = '';
const modeContainer = document.getElementById('comicsGenerationModeContainer');
if (modeContainer) modeContainer.style.display = 'none';
const radioNew = document.querySelector('input[name="comicsGenMode"][value="new_story"]');
if (radioNew) radioNew.checked = true;
if (comicsProjectSelect) comicsProjectSelect.value = ''; if (comicsProjectSelect) comicsProjectSelect.value = '';
if (comicsProjectTitleInput) comicsProjectTitleInput.value = ''; if (comicsProjectTitleInput) comicsProjectTitleInput.value = '';
if (btnDeleteComicsProject) btnDeleteComicsProject.style.display = 'none'; if (btnDeleteComicsProject) btnDeleteComicsProject.style.display = 'none';
@@ -3699,6 +3698,11 @@ const initApp = () => {
const { project, panels } = await resp.json(); const { project, panels } = await resp.json();
currentComicsProjectId = project.id; currentComicsProjectId = project.id;
currentComicsCharDescEnglish = project.character_description_english || '';
const modeContainer = document.getElementById('comicsGenerationModeContainer');
if (modeContainer) modeContainer.style.display = 'flex';
if (comicsProjectTitleInput) comicsProjectTitleInput.value = project.titulo || ''; if (comicsProjectTitleInput) comicsProjectTitleInput.value = project.titulo || '';
comicsTemaInput.value = project.tema || ''; comicsTemaInput.value = project.tema || '';
@@ -3852,9 +3856,10 @@ const initApp = () => {
cenario, cenario,
proporcao: selectedComicsRatio, proporcao: selectedComicsRatio,
character_description_global: JSON.stringify(characters), character_description_global: JSON.stringify(characters),
character_description_english: currentComicsCharDescEnglish || '',
panels: generatedPanelsData.map(p => ({ panels: generatedPanelsData.map(p => ({
panel_number: p.panel_number, panel_number: p.panel_number,
image_url: p.imageUrl, image_url: p.imageUrl || p.image_url || '',
image_prompt: p.image_prompt || '', image_prompt: p.image_prompt || '',
dialogue: p.dialogue || '' dialogue: p.dialogue || ''
})) }))
@@ -4009,17 +4014,32 @@ const initApp = () => {
try { try {
// 1. Gerar Roteiro // 1. Gerar Roteiro
const genMode = currentComicsProjectId ? (document.querySelector('input[name="comicsGenMode"]:checked')?.value || 'new_story') : 'new_story';
const requestBody = {
tema,
quantidade: selectedComicsQty,
baloes: selectedComicsBubbles,
proporcao: selectedComicsRatio,
personagens,
cenario
};
if (currentComicsProjectId) {
requestBody.continuationType = genMode;
requestBody.character_description = currentComicsCharDescEnglish;
requestBody.existingPanels = generatedPanelsData.map(p => ({
panel_number: p.panel_number,
image_url: p.imageUrl || p.image_url || '',
image_prompt: p.image_prompt || '',
dialogue: p.dialogue || ''
}));
}
const scriptResp = await fetch('/api/comics/generate-script', { const scriptResp = await fetch('/api/comics/generate-script', {
method: 'POST', method: 'POST',
headers: { 'Content-Type': 'application/json' }, headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ body: JSON.stringify(requestBody)
tema,
quantidade: selectedComicsQty,
baloes: selectedComicsBubbles,
proporcao: selectedComicsRatio,
personagens,
cenario
})
}); });
if (!scriptResp.ok) { if (!scriptResp.ok) {
@@ -4030,8 +4050,14 @@ const initApp = () => {
const scriptData = await scriptResp.json(); const scriptData = await scriptResp.json();
comicsLoaderProgress.style.width = '15%'; comicsLoaderProgress.style.width = '15%';
if (scriptData.character_description) {
currentComicsCharDescEnglish = scriptData.character_description;
}
// 2. Gerar cada quadrinho sequencialmente // 2. Gerar cada quadrinho sequencialmente
generatedPanelsData = []; if (genMode !== 'extend') {
generatedPanelsData = [];
}
const totalPanels = scriptData.panels.length; const totalPanels = scriptData.panels.length;
for (let i = 0; i < totalPanels; i++) { for (let i = 0; i < totalPanels; i++) {
@@ -4058,12 +4084,13 @@ const initApp = () => {
generatedPanelsData.push({ generatedPanelsData.push({
panel_number: panel.panel_number, panel_number: panel.panel_number,
imageUrl: frameData.imageUrl, imageUrl: frameData.imageUrl,
dialogue: panel.dialogue || '' dialogue: panel.dialogue || '',
image_prompt: panel.image_prompt || ''
}); });
} }
// Renderizar quadrinhos na tela // Renderizar quadrinhos na tela (todos, incluindo os anteriores caso tenha sido extensão)
comicsResultTitle.textContent = scriptData.title || 'Fábrica de Quadrinhos Camila'; comicsResultTitle.textContent = scriptData.title || comicsResultTitle.textContent || 'Fábrica de Quadrinhos Camila';
comicsGridContainer.innerHTML = ''; comicsGridContainer.innerHTML = '';
generatedPanelsData.forEach(panel => { generatedPanelsData.forEach(panel => {
@@ -4075,7 +4102,7 @@ const initApp = () => {
const img = document.createElement('img'); const img = document.createElement('img');
img.className = 'comic-panel-image'; img.className = 'comic-panel-image';
img.src = panel.imageUrl; img.src = panel.imageUrl || panel.image_url;
img.alt = `Quadro ${panel.panel_number}`; img.alt = `Quadro ${panel.panel_number}`;
imgContainer.appendChild(img); imgContainer.appendChild(img);
@@ -4106,6 +4133,18 @@ const initApp = () => {
comicsGridContainer.appendChild(panelCard); comicsGridContainer.appendChild(panelCard);
}); });
// Se for uma nova história derivada de um projeto antigo, limpamos o ID para virar um novo projeto independente ao salvar
if (genMode === 'new_story' && currentComicsProjectId) {
currentComicsProjectId = null;
if (comicsProjectSelect) comicsProjectSelect.value = '';
const modeContainer = document.getElementById('comicsGenerationModeContainer');
if (modeContainer) modeContainer.style.display = 'none';
if (comicsProjectTitleInput) {
comicsProjectTitleInput.value = scriptData.title || (comicsProjectTitleInput.value + ' (Derivado)');
}
if (btnDeleteComicsProject) btnDeleteComicsProject.style.display = 'none';
}
comicsGenerateLoader.style.display = 'none'; comicsGenerateLoader.style.display = 'none';
comicsResultArea.style.display = 'flex'; comicsResultArea.style.display = 'flex';
btnGenerateComics.disabled = false; btnGenerateComics.disabled = false;
+15
View File
@@ -761,6 +761,21 @@
<input type="text" id="comicsProjectTitleInput" class="obs-input" placeholder="Título do Projeto (Ex: Lucas e Mariana no Parque)" style="flex: 1; margin: 0; padding: 8px 12px; font-size: 0.9rem;"> <input type="text" id="comicsProjectTitleInput" class="obs-input" placeholder="Título do Projeto (Ex: Lucas e Mariana no Parque)" style="flex: 1; margin: 0; padding: 8px 12px; font-size: 0.9rem;">
<button type="button" id="btnSaveComicsProject" class="btn-music-option" style="padding: 8px 14px; font-size: 0.82rem; border-color: #10a37f; color: white; background: #10a37f; margin: 0; white-space: nowrap; font-weight: 600;">💾 Salvar</button> <button type="button" id="btnSaveComicsProject" class="btn-music-option" style="padding: 8px 14px; font-size: 0.82rem; border-color: #10a37f; color: white; background: #10a37f; margin: 0; white-space: nowrap; font-weight: 600;">💾 Salvar</button>
</div> </div>
<!-- MODO DE CONTINUIDADE (Apenas quando um projeto é carregado) -->
<div id="comicsGenerationModeContainer" style="display: none; border-top: 1px dashed var(--border-light); padding-top: 10px; flex-direction: column; gap: 8px;">
<label style="color: var(--text-secondary); font-size: 0.82rem; font-weight: 600;">AÇÃO PARA A PRÓXIMA GERAÇÃO:</label>
<div style="display: flex; flex-direction: column; gap: 8px; background: rgba(0,0,0,0.15); padding: 10px; border-radius: 8px;">
<label style="display: flex; align-items: center; gap: 8px; font-size: 0.85rem; cursor: pointer; color: var(--text-primary);">
<input type="radio" name="comicsGenMode" value="new_story" checked style="cursor: pointer;">
<span>🆕 <strong>Criar Nova História baseada neste Projeto</strong> (Começa do Quadro 1 com os mesmos personagens, salvando como outro projeto)</span>
</label>
<label style="display: flex; align-items: center; gap: 8px; font-size: 0.85rem; cursor: pointer; color: var(--text-primary);">
<input type="radio" name="comicsGenMode" value="extend" style="cursor: pointer;">
<span>➡️ <strong>Continuar a História Existente (Estender)</strong> (Cria mais 5 quadros sequenciais, dando sequência a esta história)</span>
</label>
</div>
</div>
</div> </div>
<!-- TEMA --> <!-- TEMA -->
+13 -10
View File
@@ -365,7 +365,7 @@ Gere um JSON perfeitamente válido contendo:
"panels": [ "panels": [
{ {
"panel_number": ${startPanelNum}, "panel_number": ${startPanelNum},
"image_prompt": "Prompt visual detalhado em inglês para o gerador de imagens da MiniMax descrevendo a cena deste quadrinho. IMPORTANTE: Use enquadramento baixo/afastado (wide angle, medium shot, characters seen from a distance, low framing). O prompt DEVE conter a instrução explícita em inglês de deixar o terço superior (cerca de 30% do topo da imagem) totalmente livre de personagens (cabeças, etc) ou elementos cruciais da cena, mantendo-o como espaço vazio/fundo limpo (ex: 'lower framing, characters positioned in the middle-to-lower section, leaving the top 30% of the image as empty space / simple clean background for text overlay'), para que os balões de diálogo caibam por cima sem ocultar os personagens. O estilo deve ser de ilustração de livro infantil de alta qualidade, 3D Pixar, traço limpo. Você DEVE incluir e respeitar estritamente a descrição visual física de cada personagem participante neste quadrinho conforme definido nos dados do projeto existente acima, para evitar qualquer alucinação de roupas, cores ou visual físico.", "image_prompt": "Prompt visual detalhado em inglês para o gerador de imagens da MiniMax descrevendo a cena deste quadrinho. IMPORTANTE PARA CONSISTÊNCIA: Substitua TODOS os nomes de personagens por sua descrição física minuciosa em inglês (obtida de character_description) em cada quadrinho que aparecerem (ex: em vez de apenas escrever 'Lucas', escreva a descrição detalhada dele com roupas e cabelos específicos, etc). IMPORTANTE PARA TEXTO: Use enquadramento baixo/afastado (wide angle, medium shot, characters seen from a distance, low framing). O prompt DEVE conter a instrução explícita em inglês de deixar o terço superior (cerca de 30% do topo da imagem) totalmente livre de personagens ou elementos cruciais da cena, mantendo-o como espaço vazio/fundo limpo (ex: 'lower framing, characters positioned in the middle-to-lower section, leaving the top 30% of the image as empty space / simple clean background for text overlay'), para os balões de diálogo. O estilo deve ser de ilustração de livro infantil de alta qualidade, 3D Pixar, traço limpo.",
"dialogue": "Falas do quadrinho em português (ex: 'Lucas: Veja aquilo!') com NO MÁXIMO 10 palavras, ou narração curta caso não tenha balões." "dialogue": "Falas do quadrinho em português (ex: 'Lucas: Veja aquilo!') com NO MÁXIMO 10 palavras, ou narração curta caso não tenha balões."
}, },
... ...
@@ -388,7 +388,7 @@ Gere um JSON perfeitamente válido contendo:
"panels": [ "panels": [
{ {
"panel_number": 1, "panel_number": 1,
"image_prompt": "Prompt visual detalhado em inglês para o gerador de imagens da MiniMax descrevendo a cena deste quadrinho. IMPORTANTE: Use enquadramento baixo/afastado (wide angle, medium shot, characters seen from a distance, low framing). O prompt DEVE conter a instrução explícita em inglês de deixar o terço superior (cerca de 30% do topo da imagem) totalmente livre de personagens (cabeças, etc) ou elementos cruciais da cena, mantendo-o como espaço vazio/fundo limpo (ex: 'lower framing, characters positioned in the middle-to-lower section, leaving the top 30% of the image as empty space / simple clean background for text overlay'), para que os balões de diálogo caibam por cima sem ocultar os personagens. Ex: 'Wide angle shot, Lucas and Mariana standing in a colorful sunny park, lower framing, characters positioned in the middle-to-lower section, leaving the top 30% of the image as empty sky for text overlay. ...'. Deve incluir a descrição física constante de cada personagem participante neste quadrinho. O estilo deve ser de ilustração de livro infantil de alta qualidade, 3D Pixar, traço limpo.", "image_prompt": "Prompt visual detalhado em inglês para o gerador de imagens da MiniMax descrevendo a cena deste quadrinho. IMPORTANTE PARA CONSISTÊNCIA: Substitua TODOS os nomes de personagens por sua descrição física minuciosa em inglês (do character_description) em cada quadrinho que aparecerem (ex: não escreva apenas 'Mariana', escreva a descrição dela detalhando cabelo e roupas de forma fixa e idêntica em todos os quadrinhos). IMPORTANTE PARA TEXTO: Use enquadramento baixo/afastado (wide angle, medium shot, characters seen from a distance, low framing) e instrua em inglês a deixar o terço superior (30% do topo da imagem) totalmente livre de personagens ou elementos cruciais (ex: 'lower framing, characters positioned in the middle-to-lower section, leaving the top 30% of the image as empty space / simple clean background for text overlay'), para os balões de diálogo. O estilo deve ser de ilustração de livro infantil de alta qualidade, 3D Pixar, traço limpo.",
"dialogue": "Falas do quadrinho em português (ex: 'Lucas: Veja aquilo!') com NO MÁXIMO 10 palavras, ou narração curta caso não tenha balões." "dialogue": "Falas do quadrinho em português (ex: 'Lucas: Veja aquilo!') com NO MÁXIMO 10 palavras, ou narração curta caso não tenha balões."
}, },
... ...
@@ -568,7 +568,7 @@ app.get('/api/comics/projects', requireAuth, async (req, res) => {
const usuarioId = req.userId || '00000000-0000-0000-0000-000000000000'; const usuarioId = req.userId || '00000000-0000-0000-0000-000000000000';
try { try {
const result = await dbPool.query( const result = await dbPool.query(
'SELECT id, titulo, tema, cenario, proporcao, character_description_global, created_at, updated_at FROM escola.projetos_comics WHERE usuario_id = $1 ORDER BY updated_at DESC;', 'SELECT id, titulo, tema, cenario, proporcao, character_description_global, character_description_english, created_at, updated_at FROM escola.projetos_comics WHERE usuario_id = $1 ORDER BY updated_at DESC;',
[usuarioId] [usuarioId]
); );
res.json(result.rows); res.json(result.rows);
@@ -609,7 +609,7 @@ app.get('/api/comics/projects/:id', requireAuth, async (req, res) => {
// 3. Salvar / Atualizar projeto // 3. Salvar / Atualizar projeto
app.post('/api/comics/projects', requireAuth, async (req, res) => { app.post('/api/comics/projects', requireAuth, async (req, res) => {
const usuarioId = req.userId || '00000000-0000-0000-0000-000000000000'; const usuarioId = req.userId || '00000000-0000-0000-0000-000000000000';
const { id, titulo, tema, cenario, proporcao, character_description_global, panels } = req.body; const { id, titulo, tema, cenario, proporcao, character_description_global, character_description_english, panels } = req.body;
if (!titulo) { if (!titulo) {
return res.status(400).json({ error: 'O título do projeto é obrigatório.' }); return res.status(400).json({ error: 'O título do projeto é obrigatório.' });
@@ -633,17 +633,17 @@ app.post('/api/comics/projects', requireAuth, async (req, res) => {
// Atualizar metadados // Atualizar metadados
await client.query( await client.query(
`UPDATE escola.projetos_comics `UPDATE escola.projetos_comics
SET titulo = $1, tema = $2, cenario = $3, proporcao = $4, character_description_global = $5, updated_at = NOW() SET titulo = $1, tema = $2, cenario = $3, proporcao = $4, character_description_global = $5, character_description_english = $6, updated_at = NOW()
WHERE id = $6;`, WHERE id = $7;`,
[titulo, tema, cenario, proporcao, character_description_global, projetoId] [titulo, tema, cenario, proporcao, character_description_global, character_description_english, projetoId]
); );
} else { } else {
// Criar novo projeto // Criar novo projeto
const insertProj = await client.query( const insertProj = await client.query(
`INSERT INTO escola.projetos_comics (usuario_id, titulo, tema, cenario, proporcao, character_description_global) `INSERT INTO escola.projetos_comics (usuario_id, titulo, tema, cenario, proporcao, character_description_global, character_description_english)
VALUES ($1, $2, $3, $4, $5, $6) VALUES ($1, $2, $3, $4, $5, $6, $7)
RETURNING id;`, RETURNING id;`,
[usuarioId, titulo, tema, cenario, proporcao, character_description_global] [usuarioId, titulo, tema, cenario, proporcao, character_description_global, character_description_english]
); );
projetoId = insertProj.rows[0].id; projetoId = insertProj.rows[0].id;
} }
@@ -872,6 +872,9 @@ async function initDatabase() {
); );
`); `);
// Garantir que a coluna character_description_english exista na tabela projetos_comics
await dbPool.query(`ALTER TABLE escola.projetos_comics ADD COLUMN IF NOT EXISTS character_description_english TEXT;`);
// 0.1. Garantir que a tabela de modelos exista e tenha dados padrão // 0.1. Garantir que a tabela de modelos exista e tenha dados padrão
await dbPool.query(` await dbPool.query(`
CREATE TABLE IF NOT EXISTS escola.modelos ( CREATE TABLE IF NOT EXISTS escola.modelos (