709 lines
29 KiB
JavaScript
709 lines
29 KiB
JavaScript
require('dotenv').config();
|
||
const express = require('express');
|
||
const cookieParser = require('cookie-parser');
|
||
const path = require('path');
|
||
|
||
const app = express();
|
||
const PORT = process.env.PORT || 3000;
|
||
const ACCESS_PASSWORD = process.env.ACCESS_PASSWORD || 'puglindo26';
|
||
|
||
app.use(express.json());
|
||
app.use(cookieParser(process.env.SESSION_SECRET || 'camila-secret'));
|
||
|
||
// Middleware para verificar autenticação
|
||
const requireAuth = (req, res, next) => {
|
||
const session = req.signedCookies.camila_session || req.cookies.camila_session;
|
||
if (session === 'authenticated') {
|
||
next();
|
||
} else {
|
||
if (req.xhr || req.path.startsWith('/api/')) {
|
||
res.status(401).json({ error: 'Não autorizado' });
|
||
} else {
|
||
res.redirect('/login');
|
||
}
|
||
}
|
||
};
|
||
|
||
// Rota de login (página)
|
||
app.get('/login', (req, res) => {
|
||
const session = req.signedCookies.camila_session || req.cookies.camila_session;
|
||
if (session === 'authenticated') {
|
||
res.redirect('/');
|
||
} else {
|
||
res.sendFile(path.join(__dirname, 'public', 'login.html'));
|
||
}
|
||
});
|
||
|
||
// API de status do modelo
|
||
app.get('/api/status', requireAuth, (req, res) => {
|
||
res.json({
|
||
model: process.env.OPENROUTER_MODEL || 'openai/gpt-4.1-nano',
|
||
provider: 'openrouter',
|
||
minimax_available: !!process.env.MINIMAX_API_KEY,
|
||
timestamp: new Date().toISOString()
|
||
});
|
||
});
|
||
|
||
// API para Login
|
||
app.post('/api/login', (req, res) => {
|
||
const { password } = req.body;
|
||
if (password === ACCESS_PASSWORD) {
|
||
// Define cookie de sessão assinado que dura 30 dias
|
||
res.cookie('camila_session', 'authenticated', {
|
||
signed: true,
|
||
httpOnly: true,
|
||
maxAge: 30 * 24 * 60 * 60 * 1000, // 30 dias
|
||
sameSite: 'lax',
|
||
secure: process.env.NODE_ENV === 'production'
|
||
});
|
||
res.json({ success: true });
|
||
} else {
|
||
res.status(401).json({ error: 'Senha incorreta' });
|
||
}
|
||
});
|
||
|
||
// API para Logout
|
||
app.post('/api/logout', (req, res) => {
|
||
res.clearCookie('camila_session');
|
||
res.json({ success: true });
|
||
});
|
||
|
||
// Proteger arquivos estáticos do diretório public exceto o login
|
||
app.use((req, res, next) => {
|
||
if (req.path === '/login' || req.path === '/login.html' || req.path === '/login.js' || req.path === '/style.css' || req.path.startsWith('/assets/')) {
|
||
return next();
|
||
}
|
||
requireAuth(req, res, next);
|
||
});
|
||
|
||
// Servir arquivos estáticos do diretório public
|
||
app.use(express.static(path.join(__dirname, 'public')));
|
||
|
||
// System prompt da Kemily (injetado em todas as conversas)
|
||
const KEMILY_SYSTEM_PROMPT = {
|
||
role: "system",
|
||
content: `Você é a Kemily, assistente de educação infantil da Profª Camila Martella Gasparini Reifonas.
|
||
|
||
PERFIL DA PROFESSORA:
|
||
- Rede municipal de São Bernardo do Campo, SP
|
||
- EMEB Mariana Neves Interliche
|
||
- Turma: Infantil III – Turma B, Sala 7
|
||
- Faixa etária: crianças de 4-5 anos
|
||
- Atuação em substituição (professora substituta)
|
||
|
||
SUA MISSÃO:
|
||
Ajudar a Camila na criação de relatórios HTPC, planejamentos quinzenais/mensais, resumos de livros infantis, registros de formação, estudos de caso e documentos pedagógicos diversos.
|
||
|
||
═══════════════════════════════════════════════
|
||
ESTILO DE ESCRITA DA CAMILA — REGRAS RIGOROSAS
|
||
═══════════════════════════════════════════════
|
||
|
||
A Camila escreve em estilo FORMAL-PEDAGÓGICO COM CALOR HUMANO. Você DEVE seguir todas estas regras ao produzir textos para ela:
|
||
|
||
1. FRASES LONGAS E ENCADEADAS:
|
||
✅ "O professor media esse conhecimento prévio, estando atento e propondo experiências significativas."
|
||
❌ Não use frases curtas e secas. Nunca fragmente o pensamento.
|
||
|
||
2. VOCABULÁRIO TÉCNICO-PEDAGÓGICO (uso natural, não forçado):
|
||
- protagonismo, intencionalidade educativa, desenvolvimento integral
|
||
- escuta ativa, vivências, saberes prévios, mediação, autorregulação
|
||
- potencialidades, avanços, coherência, propostas
|
||
- campos de experiência, BNCC, etapa do ensino
|
||
|
||
3. EXPRESSÕES CARACTERÍSTICAS (são MARCA, use com naturalidade):
|
||
- "colocando as crianças como protagonistas"
|
||
- "faz parte do mundo letrado"
|
||
- "nos mais diversos contextos/formatos/portadores"
|
||
- "propiciando assim"
|
||
- "a meu entender" / "ao meu ver" / "creio que"
|
||
- "dessa forma" / "nesse sentido" / "para além de"
|
||
- "a partir de" / "por meio de" / "acerca de"
|
||
- "partilhou" / "socializou" (em vez de "compartilhou")
|
||
- "aguçando interesses e curiosidade"
|
||
- "compartilhada" / "coletiva"
|
||
|
||
4. ESTRUTURA DE PARÁGRAFO:
|
||
Afirmação inicial → Desenvolvimento com exemplos/conexões → Transição para próximo ponto
|
||
|
||
5. CENTRALIDADE NA CRIANÇA:
|
||
Sempre volte ao estudante como protagonista do processo ensino-aprendizagem.
|
||
|
||
6. POSICIONAMENTO PESSOAL:
|
||
Use "Considero que...", "Entendo que...", "Creio que...", "Ao meu ver..." para se posicionar.
|
||
|
||
7. O QUE EVITAR:
|
||
- ❌ "de acordo com" → use "conforme", "segundo", "a partir de"
|
||
- ❌ "no dia a dia" → use "cotidianamente", "no cotidiano"
|
||
- ❌ "em relação a" → use "sobre", "acerca de", "a respeito de"
|
||
- ❌ fragmentação em frases curtas
|
||
- ❌ gírias ou linguagem informal
|
||
- ❌ linguagem burocrática vazia
|
||
|
||
═══════════════════════════════════════════════
|
||
EXEMPLOS REAIS DA ESCRITA DA CAMILA
|
||
═══════════════════════════════════════════════
|
||
|
||
EXEMPLO 1 — Relatório HTPC (avaliação anual):
|
||
"O professor como observador, faz escuta ativa da criança, para compreender o que mais chama atenção da turma, leva para a roda de conversa, aponta as curiosidades e assim consegue planejar e executar as práticas pedagógicas, colocando as crianças como protagonistas do processo ensino-aprendizagem. O planejamento se baseia principalmente nos interesses das crianças, trazendo situações que desafiam e aguçam seus interesses e curiosidade."
|
||
|
||
EXEMPLO 2 — Carta de Intenções:
|
||
"Inicio esta carta de intenções para o ano de 2026 com esta citação atribuída à escritora Lya Luft, pois reconheço claramente que a organização e o atendimento estabelecidos na Educação Infantil atuam como fundamentos desta etapa basilar da vida humana, merecendo especial atenção, dos aspectos simples aos mais complexos, e da qualidade deste trabalho dependem as fases posteriores de crescimento, aprendizagem, amadurecimento e desenvolvimento integral de cada criança."
|
||
|
||
EXEMPLO 3 — Planejamento quinzenal:
|
||
"Durante a visita ao mercadinho, pretendo propor que as crianças criem uma salad nutritiva, a partir dos vegetais que encontrarem nos setores de hortifruti, latarias e congelados, verificando o preço desses alimentos, e posteriormente faremos uma listagem verificando quais tipos de vitaminas e sais minerais encontraremos nessa salada e sua funcionalidade para o organismo."
|
||
|
||
EXEMPLO 4 — Registro de formação:
|
||
"Fomos divididas nos seguintes grupos: Grupo 1 — Viviane, Camila, Patrícia e Vanessa: jogos estruturados, conversamos sobre apresentar jogos de chão para ir trabalhando regras gerais com as crianças, para depois utilizar material ATTO e Mind Lab com regras mais estabelecidas, cuidando das peças, utilizar Lego, repertoriar as crianças, deixar explorar o material utilizando peças maiores, deixar as crianças criando por um momento livres e depois contextualizando as montagens com alguma história."
|
||
|
||
═══════════════════════════════════════════════
|
||
CONHECIMENTO ESPECÍFICO DA CAMILA
|
||
═══════════════════════════════════════════════
|
||
|
||
PROJETOS E VIVÊNCIAS DA ESCOLA:
|
||
- Entradas coletivas (com presentations de poesias)
|
||
- Atividades intersalas (jogos de tabuleiro, brincadeiras coletivas, tema)
|
||
- Piquenique literário (contração de histórias pelas crianças do Infantil IV e V)
|
||
- Saraus de poesia
|
||
- Painéis coletivos e murais
|
||
- Dia da família na escola
|
||
- Sábado letivo (apresentações de turmas para famílias)
|
||
- Estudo do meio (ex: Borboletário de Diadema)
|
||
- Mercadinho (sala 4 — trabalho com números, preços, lista)
|
||
- Projeto de leitura (empréstimo de livros da BEI)
|
||
- Show de acrobacias, baile à fantasia, cabelo maluco
|
||
- Teatro de bonecos, cineminha com pipoca
|
||
- Gincana, cataventos
|
||
|
||
MOMENTOS DO HTPC:
|
||
- "Nutrição" (momento inicial com livro/vídeo compartilhado)
|
||
- Frase do dia
|
||
- Momento da brincadeira
|
||
- Pauta da reunião
|
||
- Grupose para discussão
|
||
- Encerramento com registro
|
||
|
||
LIVROS INFANTIS CONHECIDOS:
|
||
- "Shhh! Nós temos um plano" (Chris Haughton)
|
||
- "A Galinha Ruiva" (Byron Barton)
|
||
- "Marcelo, Marmelo, Martelo" (Ruth Rocha)
|
||
- "O Ponto" (Peter H. Reynolds)
|
||
- "O Fabricante de Lápis"
|
||
- "A Primeira Infância" (vídeo)
|
||
- "Como os doces brasileiros ficaram tão doces" (vídeo)
|
||
|
||
FORMADORAS/MENTORAS:
|
||
- Formadora Márcia (formações sobre leitura e escrita)
|
||
- Sirlene (coordenadora)
|
||
- Fernanda (coordenadora)
|
||
- Maria (gestora)
|
||
|
||
METODOLOGIA:
|
||
- Mind Lab (jogos certificados INMETRO: Tik Tak Toy, Hora do Rush, Caixa Mágica, Resta-Um, Quatro-em-linha)
|
||
- Material ATTO (durável,替代 Mind Lab para jogos de chão)
|
||
- Brincadeiras musicais e cantigas de roda
|
||
- Contação de histórias
|
||
- Desenvolvimento fonológico (rimas, ritmo)
|
||
- Desenvolvimento sintático (estruturação de frases)
|
||
- Desenvolvimento semântico (significados e vocabulário)
|
||
- Desenvolvimento pragmático (uso social da linguagem)
|
||
- Professor como escriba
|
||
|
||
BNCC/CAMPOS DE EXPERIÊNCIA:
|
||
- O eu, o outro e o nós
|
||
- Corpo, gestos e movimentos
|
||
- Traços, sons, cores e formas
|
||
- Escuta, fala, pensamento e imaginação
|
||
- Espaços, tempos, quantidades, relações e transformações
|
||
|
||
ESCRITA DA CRIANÇA:
|
||
- Hipótese de escrita silábico-alfabética
|
||
- Hypothesis de escrita alfabética
|
||
|
||
═══════════════════════════════════════════════
|
||
REGRAS DE ATUAÇÃO
|
||
═══════════════════════════════════════════════
|
||
|
||
1. Quando a Camila pedir um documento (relatório, planejamento, etc):
|
||
- Seguir ESTRITAMENTE o estilo de escrita dela
|
||
- Usar frases longas e encadeadas
|
||
- Incluir vocabulário técnico-pedagógico natural
|
||
- Posicionar-se pessoalmente quando apropriado
|
||
- Manter centralidade na criança
|
||
|
||
2. Quando pedir resumo de livro infantil:
|
||
- Identificar temas, personagens, lições
|
||
- Sugerir atividades práticas de sala
|
||
- Conectar com BNCC e campos de experiência
|
||
|
||
3. Quando pedir pesquisa pedagógica:
|
||
- Fundamentar com referências reais
|
||
- Dar exemplos práticos conectando com a realidade da turma
|
||
|
||
4. Para relatórios HTPC, usar estrutura:
|
||
- Planejamento e execução das práticas pedagógicas
|
||
- Coerência entre propostas e desenvolvimento
|
||
- Diversidade de experiências e intencionalidade educativa
|
||
- Avaliação e acompanhamento das aprendizagens
|
||
|
||
5. Nunca inventar nomes de crianças, atividades ou datas. Use apenas o que conhece dos documentos.
|
||
|
||
6. Referências: usar "conforme a BNCC", "segundo a Proposta Curricular de SBC", "a partir do documento orientador".
|
||
|
||
Responda SEMPRE em português do Brasil.`
|
||
|
||
};
|
||
|
||
// Rota de Chat com OpenRouter com Smart Router Multimodal integrado (MiniMax)
|
||
app.post('/api/chat', requireAuth, async (req, res) => {
|
||
const { messages } = req.body;
|
||
|
||
if (!messages || !Array.isArray(messages)) {
|
||
return res.status(400).json({ error: 'Mensagens inválidas' });
|
||
}
|
||
|
||
const lastMessage = messages[messages.length - 1];
|
||
const promptText = lastMessage ? lastMessage.content : '';
|
||
|
||
// 1. Detectar se há imagens anexadas para desviar para OpenRouter multimodal (Vision)
|
||
const hasAttachedImages = promptText.includes('*[Imagem anexada:');
|
||
|
||
let intent = 'TEXT';
|
||
|
||
// Executar classificação apenas se houver prompt de texto e não houver imagens anexadas
|
||
if (!hasAttachedImages && promptText && process.env.MINIMAX_API_KEY) {
|
||
try {
|
||
const systemDetector = "Você é um classificador de intenções de prompt. Classifique a última mensagem do usuário em uma destas quatro categorias exatas: 'TEXT' (para conversas normais, perguntas, redação de relatórios, planejamentos, textos pedagógicos, respostas gerais), 'IMAGE' (se o usuário estiver pedindo explicitamente para gerar, desenhar, criar ou ilustrar uma imagem, figura, ilustração ou foto), 'AUDIO' (se o usuário estiver pedindo explicitamente para gerar uma música, melodia, trilha sonora, cantiga ou som), ou 'VIDEO' (se o usuário estiver pedindo explicitamente para gerar ou criar um vídeo ou animação). Responda APENAS com a palavra da categoria em letras maiúsculas: TEXT, IMAGE, AUDIO, VIDEO.";
|
||
|
||
const detectResponse = await fetch('https://openrouter.ai/api/v1/chat/completions', {
|
||
method: 'POST',
|
||
headers: {
|
||
'Authorization': `Bearer ${process.env.OPENROUTER_API_KEY}`,
|
||
'Content-Type': 'application/json'
|
||
},
|
||
body: JSON.stringify({
|
||
model: process.env.OPENROUTER_MODEL || 'openai/gpt-4.1-nano',
|
||
messages: [
|
||
{ role: 'system', content: systemDetector },
|
||
{ role: 'user', content: promptText }
|
||
],
|
||
max_tokens: 20,
|
||
temperature: 0.0
|
||
})
|
||
});
|
||
|
||
if (detectResponse.ok) {
|
||
const detectData = await detectResponse.json();
|
||
const detectedText = detectData.choices?.[0]?.message?.content?.toUpperCase() || 'TEXT';
|
||
console.log(`[Smart Router] Resposta crua do detector: "${detectedText}"`);
|
||
|
||
if (detectedText.includes('IMAGE')) {
|
||
intent = 'IMAGE';
|
||
} else if (detectedText.includes('AUDIO') || detectedText.includes('MUSIC')) {
|
||
intent = 'AUDIO';
|
||
} else if (detectedText.includes('VIDEO')) {
|
||
intent = 'VIDEO';
|
||
} else {
|
||
intent = 'TEXT';
|
||
}
|
||
console.log(`[Smart Router] Intenção detectada para o prompt "${promptText.substring(0, 40)}...": ${intent}`);
|
||
} else {
|
||
const errText = await detectResponse.text();
|
||
console.error('[Smart Router] Chamada de detecção retornou erro:', errText);
|
||
}
|
||
} catch (err) {
|
||
console.error('Erro no classificador de intenções (usando fallback TEXT):', err);
|
||
}
|
||
}
|
||
|
||
// ==========================================================================
|
||
// FLUXO DE IMAGEM (MiniMax image-01)
|
||
// ==========================================================================
|
||
if (intent === 'IMAGE') {
|
||
res.setHeader('Content-Type', 'text/event-stream');
|
||
res.setHeader('Cache-Control', 'no-cache');
|
||
res.setHeader('Connection', 'keep-alive');
|
||
|
||
res.write(`data: ${JSON.stringify({ type: 'media_status', message: 'Iniciando geração de imagem com a MiniMax...' })}\n\n`);
|
||
|
||
try {
|
||
// Prompt rewriting: pedir à IA para enriquecer o prompt em inglês para a MiniMax
|
||
let enrichedPrompt = promptText;
|
||
try {
|
||
const rewriteResp = await fetch('https://openrouter.ai/api/v1/chat/completions', {
|
||
method: 'POST',
|
||
headers: {
|
||
'Authorization': `Bearer ${process.env.OPENROUTER_API_KEY}`,
|
||
'Content-Type': 'application/json'
|
||
},
|
||
body: JSON.stringify({
|
||
model: process.env.OPENROUTER_MODEL || 'openai/gpt-4.1-nano',
|
||
messages: [
|
||
{ role: 'system', content: "Reescreva o pedido de imagem do usuário em inglês como um prompt altamente detalhado para geradores de imagem (como Midjourney/Stable Diffusion). Adicione estilo de ilustração infantil fofa, vibrante, adequada para educação infantil, traço limpo, cores harmoniosas, sem textos/letras na imagem. Retorne apenas a tradução/prompt em inglês." },
|
||
{ role: 'user', content: promptText }
|
||
],
|
||
max_tokens: 150,
|
||
temperature: 0.7
|
||
})
|
||
});
|
||
if (rewriteResp.ok) {
|
||
const rewriteData = await rewriteResp.json();
|
||
enrichedPrompt = rewriteData.choices?.[0]?.message?.content?.trim() || promptText;
|
||
console.log('[Smart Router] Prompt reescrito para imagem:', enrichedPrompt);
|
||
}
|
||
} catch (err) {
|
||
console.error('Erro ao reescrever prompt de imagem:', err);
|
||
}
|
||
|
||
res.write(`data: ${JSON.stringify({ type: 'media_status', message: 'Gerando ilustração pedagógica...' })}\n\n`);
|
||
|
||
const response = await fetch(`${process.env.MINIMAX_API_BASE}/image_generation`, {
|
||
method: 'POST',
|
||
headers: {
|
||
'Authorization': `Bearer ${process.env.MINIMAX_API_KEY}`,
|
||
'Content-Type': 'application/json'
|
||
},
|
||
body: JSON.stringify({
|
||
model: 'image-01',
|
||
prompt: enrichedPrompt,
|
||
aspect_ratio: '1:1',
|
||
response_format: 'url'
|
||
})
|
||
});
|
||
|
||
if (!response.ok) {
|
||
const errText = await response.text();
|
||
throw new Error(`MiniMax respondeu com erro: ${errText}`);
|
||
}
|
||
|
||
const imgData = await response.json();
|
||
const imageUrl = imgData.data?.image_url?.[0];
|
||
|
||
if (!imageUrl) {
|
||
throw new Error('Nenhuma imagem foi gerada pela API do MiniMax');
|
||
}
|
||
|
||
res.write(`data: ${JSON.stringify({ type: 'image', url: imageUrl, originalPrompt: promptText })}\n\n`);
|
||
res.write('data: [DONE]\n\n');
|
||
return res.end();
|
||
|
||
} catch (error) {
|
||
console.error('Erro na geração de imagem:', error);
|
||
res.write(`data: ${JSON.stringify({ content: `Ocorreu um erro ao gerar a imagem: ${error.message}` })}\n\n`);
|
||
res.write('data: [DONE]\n\n');
|
||
return res.end();
|
||
}
|
||
}
|
||
|
||
// ==========================================================================
|
||
// FLUXO DE ÁUDIO/MÚSICA (MiniMax music-2.6)
|
||
// ==========================================================================
|
||
if (intent === 'AUDIO') {
|
||
res.setHeader('Content-Type', 'text/event-stream');
|
||
res.setHeader('Cache-Control', 'no-cache');
|
||
res.setHeader('Connection', 'keep-alive');
|
||
|
||
res.write(`data: ${JSON.stringify({ type: 'media_status', message: 'Compondo a letra da música infantil...' })}\n\n`);
|
||
|
||
try {
|
||
// Gerar letra curta e estilo musical com OpenRouter
|
||
let lyrics = '';
|
||
let stylePrompt = '';
|
||
try {
|
||
const composerResp = await fetch('https://openrouter.ai/api/v1/chat/completions', {
|
||
method: 'POST',
|
||
headers: {
|
||
'Authorization': `Bearer ${process.env.OPENROUTER_API_KEY}`,
|
||
'Content-Type': 'application/json'
|
||
},
|
||
body: JSON.stringify({
|
||
model: process.env.OPENROUTER_MODEL || 'openai/gpt-4.1-nano',
|
||
messages: [
|
||
{ role: 'system', content: "Crie uma letra de música infantil em português baseada no prompt do usuário. A música deve ser muito curta (apenas 2 estrofes e 1 refrão) usando as tags estruturais [Verse] e [Chorus]. Crie também um estilo de trilha adequado em inglês (ex: 'Acoustic children song, happy, acoustic guitar, soft piano, female vocals'). Retorne obrigatoriamente um JSON no formato: {\"lyrics\": \"[Verse]...\", \"style\": \"style in english\"}" },
|
||
{ role: 'user', content: promptText }
|
||
],
|
||
response_format: { type: 'json_object' }
|
||
})
|
||
});
|
||
|
||
if (composerResp.ok) {
|
||
const compData = await composerResp.json();
|
||
const parsedComp = JSON.parse(compData.choices?.[0]?.message?.content || '{}');
|
||
lyrics = parsedComp.lyrics || `[Verse]\n${promptText}`;
|
||
stylePrompt = parsedComp.style || 'acoustic children song, happy, female vocals';
|
||
console.log('[Smart Router] Letra e estilo compostos:', { stylePrompt, lyrics });
|
||
}
|
||
} catch (err) {
|
||
console.error('Erro ao compor letra de música:', err);
|
||
lyrics = `[Verse]\n${promptText}`;
|
||
stylePrompt = 'children song, acoustic guitar, female vocals';
|
||
}
|
||
|
||
res.write(`data: ${JSON.stringify({ type: 'media_status', message: 'Gerando áudio da música com a MiniMax...' })}\n\n`);
|
||
|
||
const response = await fetch(`${process.env.MINIMAX_API_BASE}/music_generation`, {
|
||
method: 'POST',
|
||
headers: {
|
||
'Authorization': `Bearer ${process.env.MINIMAX_API_KEY}`,
|
||
'Content-Type': 'application/json'
|
||
},
|
||
body: JSON.stringify({
|
||
model: 'music-2.6',
|
||
prompt: stylePrompt,
|
||
lyrics: lyrics,
|
||
is_instrumental: false,
|
||
lyrics_optimizer: true
|
||
})
|
||
});
|
||
|
||
if (!response.ok) {
|
||
const errText = await response.text();
|
||
throw new Error(`MiniMax respondeu com erro na música: ${errText}`);
|
||
}
|
||
|
||
const musicData = await response.json();
|
||
let audioUrl = musicData.data?.audio_url;
|
||
|
||
// Se a API retornar os dados binários codificados em hex
|
||
if (!audioUrl && musicData.data?.audio) {
|
||
const audioBase64 = Buffer.from(musicData.data.audio, 'hex').toString('base64');
|
||
audioUrl = `data:audio/mp3;base64,${audioBase64}`;
|
||
}
|
||
|
||
if (!audioUrl) {
|
||
throw new Error('Nenhum áudio de música foi gerado pela API da MiniMax');
|
||
}
|
||
|
||
res.write(`data: ${JSON.stringify({ type: 'audio', url: audioUrl, originalPrompt: promptText })}\n\n`);
|
||
res.write('data: [DONE]\n\n');
|
||
return res.end();
|
||
|
||
} catch (error) {
|
||
console.error('Erro na geração de música:', error);
|
||
res.write(`data: ${JSON.stringify({ content: `Ocorreu um erro ao compor a música: ${error.message}` })}\n\n`);
|
||
res.write('data: [DONE]\n\n');
|
||
return res.end();
|
||
}
|
||
}
|
||
|
||
// ==========================================================================
|
||
// FLUXO DE VÍDEO (MiniMax video-01 - Assíncrono)
|
||
// ==========================================================================
|
||
if (intent === 'VIDEO') {
|
||
res.setHeader('Content-Type', 'text/event-stream');
|
||
res.setHeader('Cache-Control', 'no-cache');
|
||
res.setHeader('Connection', 'keep-alive');
|
||
|
||
res.write(`data: ${JSON.stringify({ type: 'media_status', message: 'Iniciando geração de vídeo na MiniMax...' })}\n\n`);
|
||
|
||
try {
|
||
// Prompt rewriting em inglês para vídeo
|
||
let enrichedPrompt = promptText;
|
||
try {
|
||
const rewriteResp = await fetch('https://openrouter.ai/api/v1/chat/completions', {
|
||
method: 'POST',
|
||
headers: {
|
||
'Authorization': `Bearer ${process.env.OPENROUTER_API_KEY}`,
|
||
'Content-Type': 'application/json'
|
||
},
|
||
body: JSON.stringify({
|
||
model: process.env.OPENROUTER_MODEL || 'openai/gpt-4.1-nano',
|
||
messages: [
|
||
{ role: 'system', content: "Reescreva o pedido de vídeo do usuário em inglês como um prompt altamente descritivo e fluído para geradores de vídeo. Adicione estilo de animação 3D amigável, cores vibrantes, infantil, movimentos suaves. Retorne apenas o prompt em inglês." },
|
||
{ role: 'user', content: promptText }
|
||
],
|
||
max_tokens: 150,
|
||
temperature: 0.7
|
||
})
|
||
});
|
||
if (rewriteResp.ok) {
|
||
const rewriteData = await rewriteResp.json();
|
||
enrichedPrompt = rewriteData.choices?.[0]?.message?.content?.trim() || promptText;
|
||
console.log('[Smart Router] Prompt reescrito para vídeo:', enrichedPrompt);
|
||
}
|
||
} catch (err) {
|
||
console.error('Erro ao reescrever prompt de vídeo:', err);
|
||
}
|
||
|
||
res.write(`data: ${JSON.stringify({ type: 'media_status', message: 'Criando tarefa de renderização...' })}\n\n`);
|
||
|
||
const response = await fetch(`${process.env.MINIMAX_API_BASE}/video_generation`, {
|
||
method: 'POST',
|
||
headers: {
|
||
'Authorization': `Bearer ${process.env.MINIMAX_API_KEY}`,
|
||
'Content-Type': 'application/json'
|
||
},
|
||
body: JSON.stringify({
|
||
model: 'video-01',
|
||
prompt: enrichedPrompt
|
||
})
|
||
});
|
||
|
||
if (!response.ok) {
|
||
const errText = await response.text();
|
||
throw new Error(`MiniMax respondeu com erro na geração de vídeo: ${errText}`);
|
||
}
|
||
|
||
const jobData = await response.json();
|
||
const taskId = jobData.task_id;
|
||
|
||
if (!taskId) {
|
||
throw new Error('Nenhum ID de tarefa foi retornado pelo MiniMax');
|
||
}
|
||
|
||
// Polling do status do vídeo
|
||
let videoUrl = null;
|
||
let attempts = 0;
|
||
const maxAttempts = 30; // ~90 segundos máximo (3 segundos de intervalo)
|
||
|
||
while (attempts < maxAttempts) {
|
||
res.write(`data: ${JSON.stringify({ type: 'media_status', message: `Processando vídeo... (${attempts * 3}s / 90s)` })}\n\n`);
|
||
|
||
const checkResp = await fetch(`${process.env.MINIMAX_API_BASE}/query/video_generation?task_id=${taskId}`, {
|
||
headers: { 'Authorization': `Bearer ${process.env.MINIMAX_API_KEY}` }
|
||
});
|
||
|
||
if (checkResp.ok) {
|
||
const checkData = await checkResp.json();
|
||
console.log(`[Smart Router] Status do vídeo job ${taskId}:`, checkData.status);
|
||
|
||
if (checkData.status === 'Success') {
|
||
const fileId = checkData.file_id;
|
||
|
||
// Obter link de download real via Files API
|
||
const fileResp = await fetch(`${process.env.MINIMAX_API_BASE}/files/retrieve?file_id=${fileId}`, {
|
||
headers: { 'Authorization': `Bearer ${process.env.MINIMAX_API_KEY}` }
|
||
});
|
||
|
||
if (fileResp.ok) {
|
||
const fileData = await fileResp.json();
|
||
videoUrl = fileData.file?.download_url;
|
||
}
|
||
break;
|
||
} else if (checkData.status === 'Fail') {
|
||
throw new Error('A API da MiniMax falhou em renderizar este vídeo.');
|
||
}
|
||
}
|
||
|
||
await new Promise(r => setTimeout(r, 3000));
|
||
attempts++;
|
||
}
|
||
|
||
if (!videoUrl) {
|
||
throw new Error('Tempo limite de renderização de vídeo excedido');
|
||
}
|
||
|
||
res.write(`data: ${JSON.stringify({ type: 'video', url: videoUrl, originalPrompt: promptText })}\n\n`);
|
||
res.write('data: [DONE]\n\n');
|
||
return res.end();
|
||
|
||
} catch (error) {
|
||
console.error('Erro na geração de vídeo:', error);
|
||
res.write(`data: ${JSON.stringify({ content: `Ocorreu um erro ao gerar o vídeo: ${error.message}` })}\n\n`);
|
||
res.write('data: [DONE]\n\n');
|
||
return res.end();
|
||
}
|
||
}
|
||
|
||
// ==========================================================================
|
||
// FLUXO PADRÃO DE TEXTO (OpenRouter com SSE streaming original)
|
||
// ==========================================================================
|
||
const enrichedMessages = [KEMILY_SYSTEM_PROMPT, ...messages];
|
||
|
||
try {
|
||
const openrouterUrl = 'https://openrouter.ai/api/v1/chat/completions';
|
||
const response = await fetch(openrouterUrl, {
|
||
method: 'POST',
|
||
headers: {
|
||
'Authorization': `Bearer ${process.env.OPENROUTER_API_KEY}`,
|
||
'HTTP-Referer': 'https://camila.reifonas.cloud',
|
||
'X-Title': 'Camila ChatGPT replica',
|
||
'Content-Type': 'application/json'
|
||
},
|
||
body: JSON.stringify({
|
||
model: process.env.OPENROUTER_MODEL,
|
||
messages: enrichedMessages,
|
||
stream: true
|
||
})
|
||
});
|
||
|
||
if (!response.ok) {
|
||
const errorText = await response.text();
|
||
console.error('Erro na API do OpenRouter:', errorText);
|
||
return res.status(response.status).json({ error: 'Erro ao processar requisição na API do OpenRouter' });
|
||
}
|
||
|
||
// Configurar headers para Server-Sent Events (SSE)
|
||
res.setHeader('Content-Type', 'text/event-stream');
|
||
res.setHeader('Cache-Control', 'no-cache');
|
||
res.setHeader('Connection', 'keep-alive');
|
||
|
||
const reader = response.body.getReader();
|
||
const decoder = new TextDecoder('utf-8');
|
||
let buffer = '';
|
||
|
||
while (true) {
|
||
const { done, value } = await reader.read();
|
||
if (done) break;
|
||
|
||
buffer += decoder.decode(value, { stream: true });
|
||
const lines = buffer.split('\n');
|
||
|
||
// Manter a última linha inacabada no buffer
|
||
buffer = lines.pop();
|
||
|
||
for (const line of lines) {
|
||
const cleanLine = line.trim();
|
||
if (cleanLine.startsWith('data: ')) {
|
||
const dataContent = cleanLine.slice(6);
|
||
if (dataContent === '[DONE]') {
|
||
res.write('data: [DONE]\n\n');
|
||
continue;
|
||
}
|
||
try {
|
||
const parsed = JSON.parse(dataContent);
|
||
const content = parsed.choices?.[0]?.delta?.content || '';
|
||
if (content) {
|
||
res.write(`data: ${JSON.stringify({ content })}\n\n`);
|
||
}
|
||
} catch (e) {
|
||
// Ignorar linhas incompletas ou JSON inválido
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
// Enviar qualquer conteúdo restante no buffer
|
||
if (buffer) {
|
||
const cleanLine = buffer.trim();
|
||
if (cleanLine.startsWith('data: ')) {
|
||
const dataContent = cleanLine.slice(6);
|
||
if (dataContent !== '[DONE]') {
|
||
try {
|
||
const parsed = JSON.parse(dataContent);
|
||
const content = parsed.choices?.[0]?.delta?.content || '';
|
||
if (content) {
|
||
res.write(`data: ${JSON.stringify({ content })}\n\n`);
|
||
}
|
||
} catch (e) {}
|
||
}
|
||
}
|
||
}
|
||
|
||
res.write('data: [DONE]\n\n');
|
||
res.end();
|
||
|
||
} catch (error) {
|
||
console.error('Erro na rota de chat:', error);
|
||
res.status(500).json({ error: 'Erro interno no servidor ao chamar a API de IA' });
|
||
}
|
||
});
|
||
|
||
// Rota coringa para redirecionar para index.html
|
||
app.get('*', requireAuth, (req, res) => {
|
||
res.sendFile(path.join(__dirname, 'public', 'index.html'));
|
||
});
|
||
|
||
app.listen(PORT, '0.0.0.0', () => {
|
||
console.log(`Servidor rodando na porta ${PORT}`);
|
||
});
|