Corrigindo salvamento de múltiplos projetos e continuidade de quadrinhos com consistência de personagens
This commit is contained in:
+99
-60
@@ -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 "
|
||||
];
|
||||
|
||||
for (const sugg of suggestionsList) {
|
||||
if (baseText.startsWith(sugg)) {
|
||||
baseText = baseText.replace(sugg, "");
|
||||
break;
|
||||
}
|
||||
// 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, "").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;
|
||||
|
||||
@@ -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 -->
|
||||
|
||||
@@ -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 (
|
||||
|
||||
Reference in New Issue
Block a user