# Plano Técnico — Smart Router Multimodal para Camila AI **App:** `/root/Apps/Camila` **Data:** 2026-05-26 **Objetivo:** Roteamento automático de LLMs (OpenRouter + MiniMax) baseado na intenção do prompt da Camila --- ## 1. Arquitetura Atual (Linha Base) ``` Camila → frontend (app.js) → POST /api/chat → OpenRouter (GPT-4.1-nano) → SSE stream → frontend ``` **Arquivos relevantes:** - `server.js` — Express, 362 linhas, rotas: `/api/chat`, `/api/login`, `/api/logout`, `/api/status` - `.env` — `OPENROUTER_API_KEY`, `OPENROUTER_MODEL`, `ACCESS_PASSWORD` - `public/app.js` — SPA, streaming SSE, renderiza markdown - `public/index.html` — UI principal **Dependências atuais:** `express`, `cookie-parser`, `dotenv`, `msedge-tts` --- ## 2. Arquitetura Proposta (Smart Router) ``` Camila digita prompt │ ▼ ┌───────────────────┐ │ INTENT DETECTOR │ ← analisa prompt → classifica tipo │ (app.js/frontend)│ └────────┬──────────┘ │ ┌────┴──────────────────────┐ │ Classificação do Prompt │ ├──────────────────────────┤ │ [texto] → OpenRouter │ │ [imagem] → MiniMax │ │ [áudio/música]→ MiniMax │ │ [vídeo] → MiniMax │ │ [análise img]→ MiniMax │ └───────────────────────────┘ │ ┌────┴────────┐ │ │ ▼ ▼ OpenRouter MiniMax API (SSE stream) (sync/async) │ │ └──────┬──────┘ ▼ Response Unificada (mesmo visual no chat) ``` --- ## 3. Detalhamento por Etapa ### 3.1 Intent Detector (Frontend — `app.js`) **O que faz:** Analisa o prompt da Camila antes de enviar e classifica em uma intenção. **Keywords de classificação:** | Tipo | Keywords (pt-BR) | Provider | |------|-----------------|----------| | `texto` | (nenhuma das abaixo) | OpenRouter | | `imagem_gerar` | "gere uma imagem", "crie uma imagem", "desenhe", "gere figure", "crie figura", "imagem de", "foto de" | MiniMax Image | | `áudio/música` | "gere uma música", "crie uma música", "cante", "melodia", "som para", "trilha" | MiniMax Audio | | `vídeo` | "gere um vídeo", "crie um vídeo", "video de", "animação de" | MiniMax Video | | `análise_img` | (imagem anexada + pergunta sobre ela) | MiniMax Vision | **Regex simplificado (implementação):** ```javascript const detectIntent = (text, hasAttachedImages) => { const lower = text.toLowerCase(); if (hasAttachedImages) return 'análise_img'; if (/gere uma imagem|crie uma imagem|desenhe|gere figura|crie figura|imagem de|foto de/i.test(lower)) return 'imagem_gerar'; if (/gere uma música|crie uma música|melodia|trilha|som para|cante/i.test(lower)) return 'áudio'; if (/gere um vídeo|crie um vídeo|video de|animação de/i.test(lower)) return 'vídeo'; return 'texto'; }; ``` **Prompt Rewriting (para geração de mídia):** Quando o tipo ≠ `texto`, o app reescreve o pedido para o formato otimizado do MiniMax: ```javascript const rewritePrompt = (text, intent) => { // Para imagem: enrich com detalhes visuais if (intent === 'imagem_gerar') { return ` Gere uma imagem: ${text}. Estilo: ilustração infantil educativo, cores vibrantes, pedagogico.`; } // Para música: enrich com estilo if (intent === 'audio') { return `Crie uma música: ${text}. Estilo: infantil, educativo, cantado por voz feminina.`; } return text; }; ``` --- ### 3.2 Backend — Novas Rotas (`server.js`) **3.2.1 Nova rota: `/api/generate` (unificada)** Substitui ou complementa `/api/chat`. Aceita campo `type` adicional. ```javascript // .env precisa adicionar: MINIMAX_API_KEY=seu_token_minimax MINIMAX_API_BASE=https://api.minimax.chat/v1 // confirmar endpoint real ``` **Rotas novas:** | Endpoint | Método | Provider | Tipo Retorno | |----------|--------|----------|--------------| | `/api/chat` | POST | OpenRouter | SSE (mantém) | | `/api/image` | POST | MiniMax Image | JSON `{url, prompt}` | | `/api/audio` | POST | MiniMax Audio/T2A | JSON `{url, duration}` | | `/api/video` | POST | MiniMax Video | JSON `{url, duration, status}` | | `/api/analyze-image` | POST | MiniMax Vision | SSE (como chat) | **Exemplo de implementação — `/api/image`:** ```javascript app.post('/api/image', requireAuth, async (req, res) => { const { prompt, originalPrompt } = req.body; try { const response = await fetch(`${process.env.MINIMAX_API_BASE}/images/generations`, { method: 'POST', headers: { 'Authorization': `Bearer ${process.env.MINIMAX_API_KEY}`, 'Content-Type': 'application/json' }, body: JSON.stringify({ model: 'minimax-image-01', prompt: prompt, num_images: 1, width: 1024, height: 1024 }) }); const data = await response.json(); if (!response.ok) { return res.status(response.status).json({ error: 'Erro ao gerar imagem' }); } // Formato de resposta padronizado res.json({ type: 'image', url: data.data?.[0]?.url, prompt: originalPrompt, provider: 'minimax' }); } catch (error) { console.error('Erro /api/image:', error); res.status(500).json({ error: 'Erro interno ao gerar imagem' }); } }); ``` **Exemplo de implementação — `/api/audio` (text-to-audio/MiniMax T2A):** ```javascript app.post('/api/audio', requireAuth, async (req, res) => { const { text, voice } = req.body; try { const response = await fetch(`${process.env.MINIMAX_API_BASE}/t2a_v2`, { method: 'POST', headers: { 'Authorization': `Bearer ${process.env.MINIMAX_API_KEY}`, 'Content-Type': 'application/json' }, body: JSON.stringify({ model: 'speech-01', text: text, voice_setting: { voice_id: voice || 'female-pt-BR-001' }, output_format: 'mp3' }) }); // Audio возвращается como base64 ou URL const data = await response.json(); res.json({ type: 'audio', url: data.data?.audio_url, duration: data.data?.duration, provider: 'minimax' }); } catch (error) { console.error('Erro /api/audio:', error); res.status(500).json({ error: 'Erro interno ao gerar áudio' }); } }); ``` **Exemplo de implementação — `/api/video` (MiniMax S2V?):** ```javascript app.post('/api/video', requireAuth, async (req, res) => { const { prompt, duration } = req.body; // Video geralmente é async (job-based) // Poll status endpoint até completar try { 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: prompt, duration: duration || 10 // segundos }) }); const job = await response.json(); // Retornar job_id para o frontend trackear res.json({ type: 'video', job_id: job.job_id, status: 'processing', provider: 'minimax' }); } catch (error) { console.error('Erro /api/video:', error); res.status(500).json({ error: 'Erro interno ao gerar vídeo' }); } }); ``` **3.2.2 Status endpoint atualizado (`/api/status`):** ```javascript 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() }); }); ``` --- ### 3.3 Frontend — `app.js` (Modificações) **Mudanças no fluxo de `handleSendMessage`:** ```javascript const handleSendMessage = async (text, skipUserAppend = false, files = null) => { // 1. Detectar intenção const hasImages = attachedFiles.some(f => f.type.startsWith('image/')); const intent = detectIntent(text, hasImages); // 2. Se não é texto, desviar para endpoint específico if (intent !== 'texto') { return handleMediaGeneration(text, intent, attachedFiles); } // 3. Fluxo normal de texto (mantém SSE streaming) // ... (código existente) }; const handleMediaGeneration = async (text, intent, files) => { // Mostrar estado de loading no chat const loadingRow = appendMediaLoadingMessage(intent); // 3.1 Prompt rewriting const rewrittenPrompt = rewritePrompt(text, intent); // 3.2 Chamar endpoint correto let endpoint = '/api/image'; let payload = { prompt: rewrittenPrompt, originalPrompt: text }; if (intent === 'áudio') { endpoint = '/api/audio'; payload = { text, voice: 'pt-BR-female' }; } else if (intent === 'vídeo') { endpoint = '/api/video'; payload = { prompt: rewrittenPrompt, duration: 10 }; } try { const response = await fetch(endpoint, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(payload) }); const data = await response.json(); // 3.3 Renderizar resposta replaceLoadingWithMediaMessage(loadingRow, intent, data); } catch (error) { showMediaError(loadingRow, intent); } }; ``` **UI de mensagem para mídia gerada:** ```javascript const appendMediaMessage = (type, data) => { // Template unificado let mediaHtml = ''; if (type === 'imagem_gerar') { mediaHtml = `
Imagem gerada

${escapeHtml(data.originalPrompt)}

`; } else if (type === 'áudio') { mediaHtml = `

${escapeHtml(data.originalPrompt)}

`; } else if (type === 'vídeo') { mediaHtml = `

${escapeHtml(data.originalPrompt)}

`; } // ... injetar no DOM }; ``` **CSS novo para mensagens de mídia** (adicionar em `style.css`): ```css .media-message { margin: 12px 0; border-radius: 12px; overflow: hidden; } .media-message img.generated-image { max-width: 100%; border-radius: 12px; border: 1px solid var(--border-light); } .media-message video, .media-message audio { max-width: 100%; border-radius: 12px; } .media-caption { font-size: 13px; color: var(--text-muted); margin-top: 8px !important; font-style: italic; } ``` --- ### 4. `.env` — Variáveis Novas ```bash # Existentes PORT=3000 ACCESS_PASSWORD=puglindo26 OPENROUTER_API_KEY=sk-or-...13d0 OPENROUTER_MODEL=openai/gpt-4.1-nano SESSION_SECRET=camila-secret-key-321123 # Novas MINIMAX_API_KEY=seu_token_minimax_aqui MINIMAX_API_BASE=https://api.minimax.chat/v1 ``` --- ### 5. Ordem de Implementação Sugerida ``` FASE 1 — Backend mínimo (1-2h) ├── Adicionar MINIMAX_API_KEY ao .env ├── Criar rota /api/image (geração de imagem) ├── Criar rota /api/audio (text-to-audio) ├── Atualizar /api/status com flag minimax_available └── Testar rotas com curl FASE 2 — Frontend mínimo (1-2h) ├── Implementar detectIntent() em app.js ├── Implementar handleMediaGeneration() ├── Criar template de mensagem de imagem no chat ├── Adicionar CSS para .media-message └── Testar fluxo completo: digitar → gerar → ver imagem FASE 3 — Geração de música/vídeo (2-3h) ├── Criar /api/audio (se MiniMax tem TTS/Música) ├── Criar /api/video (se MiniMax suporta) ├── Prompt rewriting refinado └── UI para áudio/vídeo (player nativo HTML5) FASE 4 — Análise de imagem (vision) (2h) ├── Detectar imagem anexada + pergunta ├── /api/analyze-image → MiniMax Vision ├── Streaming SSE da análise como texto └── Kemily "enxerga" a imagem e comenta FASE 5 — Polish (1-2h) ├── Loading states animados para mídia ├── Error handling robusto ├── Fallback: se MiniMax falhar, pedir ao GPT que descreva o que geraria └── Documentação do sistema ``` **Tempo total estimado:** 8-12 horas (distribuído) --- ### 6. Checklist de Verificação - [ ] `MINIMAX_API_KEY` adicionada ao `.env` - [ ] Intent detector funcionando (regex testado) - [ ] `/api/image` responde com URL de imagem - [ ] `/api/audio` responde com URL de áudio (se disponível) - [ ] `/api/video` responde com URL de vídeo ou job_id (se disponível) - [ ] Frontend exibe imagem gerada no chat - [ ] Frontend exibe player de áudio/vídeo - [ ] Loading state visível durante geração - [ ] Error state graceful se provider falhar - [ ] Kemily responde em texto quando mídia é gerada ("Gerei essa imagem para você!") --- ### 7. Riscos e Mitigações | Risco | Probabilidade | Mitigação | |-------|--------------|-----------| | MiniMax API não tem endpoint de música | Média | Usar só imagem/vídeo; música como "将来" | | CORS errors no frontend | Baixa | Backend faz proxy, nunca chama API direto | | Imagem gerada tarde (async) | Média | Polling com loading spinner + status | | Prompt rewriting gera prompt ruim | Média | Manter simples; GPT-4.1-nano reescreve melhor | | Custo miniMax exceder límite | Baixa | Rate limit por usuário no backend | --- ### 8. Referências da API MiniMax **Precisa confirmar endpoints oficiais.** Até a implementação, consultar: - Portal desenvolvedor: https://www.minimaxi.com/developer - Dashboard da conta MiniMax para ver módulos ativos (geração de imagem, áudio, vídeo) - A API key atual do Marcos está no painel MiniMax dele **Headers comuns MiniMax:** ``` Authorization: Bearer {MINIMAX_API_KEY} Content-Type: application/json ``` --- *Plano gerado por Hermes — 2026-05-26*