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
+98 -59
View File
@@ -2097,56 +2097,47 @@ const initApp = () => {
const mode = btn.dataset.mode;
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
const isTextOrPoetry = (mode === 'POETRY' || mode === 'TEXT');
let suggestion = "";
let baseText = text;
if (!text || isTextOrPoetry) {
let suggestion = "";
let baseText = text;
// Se o texto atual já for uma das sugestões padrão, limpamos para não duplicar
const suggestionsList = [
"Crie uma música pedagógica alegre e animada 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 ",
"Escreva uma história educativa infantil curta sobre "
];
// Se o texto atual já for uma das sugestões padrão, limpamos para não duplicar
const suggestionsList = [
"Crie uma música pedagógica alegre e animada 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 ",
"Escreva uma história educativa infantil curta sobre "
];
for (const sugg of suggestionsList) {
if (baseText.startsWith(sugg)) {
baseText = baseText.replace(sugg, "");
break;
}
for (const sugg of suggestionsList) {
if (baseText.startsWith(sugg)) {
baseText = baseText.replace(sugg, "").trim();
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
handleSendMessage(text, false, null, mode);
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';
if (btnSend) btnSend.disabled = false;
});
});
@@ -3515,6 +3506,7 @@ const initApp = () => {
const btnSaveComicsProject = document.getElementById('btnSaveComicsProject');
let currentComicsProjectId = null;
let currentComicsCharDescEnglish = '';
let selectedComicsQty = 5;
let selectedComicsBubbles = true;
let selectedComicsRatio = '16:9';
@@ -3617,6 +3609,13 @@ const initApp = () => {
// --- GERENCIADOR DE PROJETOS DE HQ ---
const startNewComicsProject = () => {
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 (comicsProjectTitleInput) comicsProjectTitleInput.value = '';
if (btnDeleteComicsProject) btnDeleteComicsProject.style.display = 'none';
@@ -3699,6 +3698,11 @@ const initApp = () => {
const { project, panels } = await resp.json();
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 || '';
comicsTemaInput.value = project.tema || '';
@@ -3852,9 +3856,10 @@ const initApp = () => {
cenario,
proporcao: selectedComicsRatio,
character_description_global: JSON.stringify(characters),
character_description_english: currentComicsCharDescEnglish || '',
panels: generatedPanelsData.map(p => ({
panel_number: p.panel_number,
image_url: p.imageUrl,
image_url: p.imageUrl || p.image_url || '',
image_prompt: p.image_prompt || '',
dialogue: p.dialogue || ''
}))
@@ -4009,17 +4014,32 @@ const initApp = () => {
try {
// 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', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
tema,
quantidade: selectedComicsQty,
baloes: selectedComicsBubbles,
proporcao: selectedComicsRatio,
personagens,
cenario
})
body: JSON.stringify(requestBody)
});
if (!scriptResp.ok) {
@@ -4030,8 +4050,14 @@ const initApp = () => {
const scriptData = await scriptResp.json();
comicsLoaderProgress.style.width = '15%';
if (scriptData.character_description) {
currentComicsCharDescEnglish = scriptData.character_description;
}
// 2. Gerar cada quadrinho sequencialmente
generatedPanelsData = [];
if (genMode !== 'extend') {
generatedPanelsData = [];
}
const totalPanels = scriptData.panels.length;
for (let i = 0; i < totalPanels; i++) {
@@ -4058,12 +4084,13 @@ const initApp = () => {
generatedPanelsData.push({
panel_number: panel.panel_number,
imageUrl: frameData.imageUrl,
dialogue: panel.dialogue || ''
dialogue: panel.dialogue || '',
image_prompt: panel.image_prompt || ''
});
}
// Renderizar quadrinhos na tela
comicsResultTitle.textContent = scriptData.title || 'Fábrica de Quadrinhos Camila';
// Renderizar quadrinhos na tela (todos, incluindo os anteriores caso tenha sido extensão)
comicsResultTitle.textContent = scriptData.title || comicsResultTitle.textContent || 'Fábrica de Quadrinhos Camila';
comicsGridContainer.innerHTML = '';
generatedPanelsData.forEach(panel => {
@@ -4075,7 +4102,7 @@ const initApp = () => {
const img = document.createElement('img');
img.className = 'comic-panel-image';
img.src = panel.imageUrl;
img.src = panel.imageUrl || panel.image_url;
img.alt = `Quadro ${panel.panel_number}`;
imgContainer.appendChild(img);
@@ -4106,6 +4133,18 @@ const initApp = () => {
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';
comicsResultArea.style.display = 'flex';
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;">
<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>
<!-- 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>
<!-- TEMA -->
+13 -10
View File
@@ -365,7 +365,7 @@ Gere um JSON perfeitamente válido contendo:
"panels": [
{
"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."
},
...
@@ -388,7 +388,7 @@ Gere um JSON perfeitamente válido contendo:
"panels": [
{
"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."
},
...
@@ -568,7 +568,7 @@ app.get('/api/comics/projects', requireAuth, async (req, res) => {
const usuarioId = req.userId || '00000000-0000-0000-0000-000000000000';
try {
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]
);
res.json(result.rows);
@@ -609,7 +609,7 @@ app.get('/api/comics/projects/:id', requireAuth, async (req, res) => {
// 3. Salvar / Atualizar projeto
app.post('/api/comics/projects', requireAuth, async (req, res) => {
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) {
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
await client.query(
`UPDATE escola.projetos_comics
SET titulo = $1, tema = $2, cenario = $3, proporcao = $4, character_description_global = $5, updated_at = NOW()
WHERE id = $6;`,
[titulo, tema, cenario, proporcao, character_description_global, projetoId]
SET titulo = $1, tema = $2, cenario = $3, proporcao = $4, character_description_global = $5, character_description_english = $6, updated_at = NOW()
WHERE id = $7;`,
[titulo, tema, cenario, proporcao, character_description_global, character_description_english, projetoId]
);
} else {
// Criar novo projeto
const insertProj = await client.query(
`INSERT INTO escola.projetos_comics (usuario_id, titulo, tema, cenario, proporcao, character_description_global)
VALUES ($1, $2, $3, $4, $5, $6)
`INSERT INTO escola.projetos_comics (usuario_id, titulo, tema, cenario, proporcao, character_description_global, character_description_english)
VALUES ($1, $2, $3, $4, $5, $6, $7)
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;
}
@@ -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
await dbPool.query(`
CREATE TABLE IF NOT EXISTS escola.modelos (