Spaces:
Build error
Build error
| <html lang="pt-BR"> | |
| <head> | |
| <meta charset="UTF-8"> | |
| <link rel="icon" type="image/x-icon" href="/favicon.ico" /> | |
| <meta name="viewport" content="width=device-width, initial-scale=1.0"> | |
| <title>LoRA Trainer - Treinamento de Personagens Consistentes</title> | |
| <style> | |
| * { | |
| margin: 0; | |
| padding: 0; | |
| box-sizing: border-box; | |
| } | |
| body { | |
| font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif; | |
| background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); | |
| min-height: 100vh; | |
| padding: 20px; | |
| } | |
| .container { | |
| max-width: 1200px; | |
| margin: 0 auto; | |
| background: white; | |
| border-radius: 15px; | |
| box-shadow: 0 20px 40px rgba(0,0,0,0.1); | |
| overflow: hidden; | |
| } | |
| .header { | |
| background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); | |
| color: white; | |
| padding: 30px; | |
| text-align: center; | |
| } | |
| .header h1 { | |
| font-size: 2.5em; | |
| margin-bottom: 10px; | |
| } | |
| .header p { | |
| font-size: 1.2em; | |
| opacity: 0.9; | |
| } | |
| .content { | |
| padding: 40px; | |
| } | |
| .section { | |
| margin-bottom: 40px; | |
| padding: 30px; | |
| border: 2px solid #f0f0f0; | |
| border-radius: 10px; | |
| background: #fafafa; | |
| } | |
| .section h2 { | |
| color: #333; | |
| margin-bottom: 20px; | |
| font-size: 1.8em; | |
| border-bottom: 3px solid #667eea; | |
| padding-bottom: 10px; | |
| } | |
| .form-group { | |
| margin-bottom: 20px; | |
| } | |
| label { | |
| display: block; | |
| margin-bottom: 8px; | |
| font-weight: 600; | |
| color: #555; | |
| } | |
| input[type="text"], input[type="number"], input[type="file"], select, textarea { | |
| width: 100%; | |
| padding: 12px; | |
| border: 2px solid #ddd; | |
| border-radius: 8px; | |
| font-size: 16px; | |
| transition: border-color 0.3s; | |
| } | |
| input[type="text"]:focus, input[type="number"]:focus, input[type="file"]:focus, select:focus, textarea:focus { | |
| outline: none; | |
| border-color: #667eea; | |
| } | |
| .file-upload-area { | |
| border: 3px dashed #ddd; | |
| border-radius: 10px; | |
| padding: 40px; | |
| text-align: center; | |
| background: white; | |
| transition: all 0.3s; | |
| cursor: pointer; | |
| } | |
| .file-upload-area:hover { | |
| border-color: #667eea; | |
| background: #f8f9ff; | |
| } | |
| .file-upload-area.dragover { | |
| border-color: #667eea; | |
| background: #f0f4ff; | |
| } | |
| .upload-icon { | |
| font-size: 3em; | |
| color: #667eea; | |
| margin-bottom: 15px; | |
| } | |
| .btn { | |
| background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); | |
| color: white; | |
| border: none; | |
| padding: 15px 30px; | |
| border-radius: 8px; | |
| font-size: 16px; | |
| font-weight: 600; | |
| cursor: pointer; | |
| transition: all 0.3s; | |
| margin-right: 10px; | |
| margin-bottom: 10px; | |
| } | |
| .btn:hover { | |
| transform: translateY(-2px); | |
| box-shadow: 0 10px 20px rgba(102, 126, 234, 0.3); | |
| } | |
| .btn:disabled { | |
| background: #ccc; | |
| cursor: not-allowed; | |
| transform: none; | |
| box-shadow: none; | |
| } | |
| .btn-secondary { | |
| background: #6c757d; | |
| } | |
| .btn-success { | |
| background: #28a745; | |
| } | |
| .btn-danger { | |
| background: #dc3545; | |
| } | |
| .progress-container { | |
| background: #f0f0f0; | |
| border-radius: 10px; | |
| padding: 20px; | |
| margin: 20px 0; | |
| display: none; | |
| } | |
| .progress-bar { | |
| width: 100%; | |
| height: 20px; | |
| background: #e0e0e0; | |
| border-radius: 10px; | |
| overflow: hidden; | |
| margin-bottom: 10px; | |
| } | |
| .progress-fill { | |
| height: 100%; | |
| background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); | |
| width: 0%; | |
| transition: width 0.3s; | |
| } | |
| .log-container { | |
| background: #1e1e1e; | |
| color: #00ff00; | |
| padding: 20px; | |
| border-radius: 10px; | |
| font-family: 'Courier New', monospace; | |
| max-height: 300px; | |
| overflow-y: auto; | |
| margin: 20px 0; | |
| display: none; | |
| } | |
| .image-preview { | |
| display: flex; | |
| flex-wrap: wrap; | |
| gap: 15px; | |
| margin-top: 20px; | |
| } | |
| .image-item { | |
| position: relative; | |
| border-radius: 10px; | |
| overflow: hidden; | |
| box-shadow: 0 5px 15px rgba(0,0,0,0.1); | |
| } | |
| .image-item img { | |
| width: 150px; | |
| height: 150px; | |
| object-fit: cover; | |
| } | |
| .image-item .remove-btn { | |
| position: absolute; | |
| top: 5px; | |
| right: 5px; | |
| background: #dc3545; | |
| color: white; | |
| border: none; | |
| border-radius: 50%; | |
| width: 25px; | |
| height: 25px; | |
| cursor: pointer; | |
| font-size: 12px; | |
| } | |
| .parameters-grid { | |
| display: grid; | |
| grid-template-columns: repeat(auto-fit, minmax(300px, 1fr)); | |
| gap: 20px; | |
| } | |
| .result-section { | |
| background: #e8f5e8; | |
| border: 2px solid #28a745; | |
| border-radius: 10px; | |
| padding: 20px; | |
| margin-top: 20px; | |
| display: none; | |
| } | |
| .download-links { | |
| display: flex; | |
| flex-wrap: wrap; | |
| gap: 10px; | |
| margin-top: 15px; | |
| } | |
| .alert { | |
| padding: 15px; | |
| border-radius: 8px; | |
| margin: 15px 0; | |
| display: none; | |
| } | |
| .alert-success { | |
| background: #d4edda; | |
| border: 1px solid #c3e6cb; | |
| color: #155724; | |
| } | |
| .alert-error { | |
| background: #f8d7da; | |
| border: 1px solid #f5c6cb; | |
| color: #721c24; | |
| } | |
| .alert-warning { | |
| background: #fff3cd; | |
| border: 1px solid #ffeaa7; | |
| color: #856404; | |
| } | |
| @media (max-width: 768px) { | |
| .container { | |
| margin: 10px; | |
| border-radius: 10px; | |
| } | |
| .content { | |
| padding: 20px; | |
| } | |
| .parameters-grid { | |
| grid-template-columns: 1fr; | |
| } | |
| } | |
| </style> | |
| </head> | |
| <body> | |
| <div class="container"> | |
| <div class="header"> | |
| <h1>🎨 LoRA Trainer</h1> | |
| <p>Ferramenta para Treinamento de Personagens Consistentes</p> | |
| </div> | |
| <div class="content"> | |
| <!-- Seção de Upload de Imagens --> | |
| <div class="section"> | |
| <h2>📸 Upload de Imagens</h2> | |
| <p style="margin-bottom: 20px; color: #666;"> | |
| Faça upload de 5-20 imagens do personagem que deseja treinar. | |
| Recomenda-se imagens de alta qualidade (512x512 ou superior) com o personagem em diferentes poses e ângulos. | |
| </p> | |
| <div class="file-upload-area" id="uploadArea"> | |
| <div class="upload-icon">📁</div> | |
| <h3>Arraste e solte suas imagens aqui</h3> | |
| <p>ou clique para selecionar arquivos</p> | |
| <input type="file" id="imageFiles" multiple accept="image/*" style="display: none;"> | |
| </div> | |
| <div class="image-preview" id="imagePreview"></div> | |
| <div class="alert alert-warning" id="imageAlert"> | |
| <strong>Atenção:</strong> Você precisa fazer upload de pelo menos 5 imagens para treinar o LoRA. | |
| </div> | |
| </div> | |
| <!-- Seção de Configurações --> | |
| <div class="section"> | |
| <h2>⚙️ Configurações de Treinamento</h2> | |
| <div class="parameters-grid"> | |
| <div class="form-group"> | |
| <label for="characterName">Nome do Personagem:</label> | |
| <input type="text" id="characterName" placeholder="Ex: meu_personagem" required> | |
| <small style="color: #666;">Use apenas letras, números e underscore</small> | |
| </div> | |
| <div class="form-group"> | |
| <label for="triggerWord">Palavra-chave (Trigger):</label> | |
| <input type="text" id="triggerWord" placeholder="Ex: ohwx person" required> | |
| <small style="color: #666;">Palavra que ativará o personagem nas gerações</small> | |
| </div> | |
| <div class="form-group"> | |
| <label for="resolution">Resolução de Treinamento:</label> | |
| <select id="resolution"> | |
| <option value="512">512x512 (Recomendado)</option> | |
| <option value="768">768x768 (Melhor qualidade)</option> | |
| <option value="1024">1024x1024 (Alta qualidade)</option> | |
| </select> | |
| </div> | |
| <div class="form-group"> | |
| <label for="learningRate">Taxa de Aprendizado:</label> | |
| <select id="learningRate"> | |
| <option value="1e-4">1e-4 (Padrão)</option> | |
| <option value="5e-5">5e-5 (Conservador)</option> | |
| <option value="2e-4">2e-4 (Agressivo)</option> | |
| </select> | |
| </div> | |
| <div class="form-group"> | |
| <label for="rank">Rank do LoRA:</label> | |
| <select id="rank"> | |
| <option value="16">16 (Padrão)</option> | |
| <option value="32">32 (Mais detalhes)</option> | |
| <option value="64">64 (Máxima qualidade)</option> | |
| </select> | |
| </div> | |
| <div class="form-group"> | |
| <label for="epochs">Número de Épocas:</label> | |
| <select id="epochs"> | |
| <option value="10">10 (Rápido)</option> | |
| <option value="20">20 (Padrão)</option> | |
| <option value="30">30 (Detalhado)</option> | |
| </select> | |
| </div> | |
| </div> | |
| <div class="form-group"> | |
| <label for="description">Descrição do Personagem (Opcional):</label> | |
| <textarea id="description" rows="3" placeholder="Descreva as características do personagem..."></textarea> | |
| </div> | |
| </div> | |
| <!-- Seção de Treinamento --> | |
| <div class="section"> | |
| <h2>🚀 Iniciar Treinamento</h2> | |
| <button class="btn" id="startTraining" onclick="startTraining()"> | |
| Iniciar Treinamento LoRA | |
| </button> | |
| <button class="btn btn-secondary" id="stopTraining" onclick="stopTraining()" style="display: none;"> | |
| Parar Treinamento | |
| </button> | |
| <div class="progress-container" id="progressContainer"> | |
| <h3>Progresso do Treinamento</h3> | |
| <div class="progress-bar"> | |
| <div class="progress-fill" id="progressFill"></div> | |
| </div> | |
| <p id="progressText">Preparando treinamento...</p> | |
| </div> | |
| <div class="log-container" id="logContainer"> | |
| <div id="trainingLogs"></div> | |
| </div> | |
| <div class="alert alert-success" id="successAlert"> | |
| <strong>Sucesso!</strong> O treinamento foi concluído com sucesso. | |
| </div> | |
| <div class="alert alert-error" id="errorAlert"> | |
| <strong>Erro!</strong> <span id="errorMessage"></span> | |
| </div> | |
| </div> | |
| <!-- Seção de Resultados --> | |
| <div class="section result-section" id="resultSection"> | |
| <h2>✅ Treinamento Concluído</h2> | |
| <p>Seu modelo LoRA foi treinado com sucesso! Baixe os arquivos abaixo:</p> | |
| <div class="download-links" id="downloadLinks"> | |
| <!-- Links de download serão inseridos aqui --> | |
| </div> | |
| <div style="margin-top: 20px; padding: 15px; background: #f8f9fa; border-radius: 8px;"> | |
| <h4>Como usar seu LoRA:</h4> | |
| <ol> | |
| <li>Baixe o arquivo <code>pytorch_lora_weights.safetensors</code></li> | |
| <li>Coloque o arquivo na pasta de LoRAs do seu software (ComfyUI, Automatic1111, etc.)</li> | |
| <li>Use a palavra-chave "<span id="resultTriggerWord"></span>" em seus prompts</li> | |
| <li>Ajuste o peso do LoRA entre 0.7 e 1.0 para melhores resultados</li> | |
| </ol> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| <script> | |
| let uploadedImages = []; | |
| let trainingInProgress = false; | |
| let trainingInterval = null; | |
| // Configuração da área de upload | |
| const uploadArea = document.getElementById('uploadArea'); | |
| const fileInput = document.getElementById('imageFiles'); | |
| const imagePreview = document.getElementById('imagePreview'); | |
| uploadArea.addEventListener('click', () => fileInput.click()); | |
| uploadArea.addEventListener('dragover', handleDragOver); | |
| uploadArea.addEventListener('drop', handleDrop); | |
| fileInput.addEventListener('change', handleFileSelect); | |
| function handleDragOver(e) { | |
| e.preventDefault(); | |
| uploadArea.classList.add('dragover'); | |
| } | |
| function handleDrop(e) { | |
| e.preventDefault(); | |
| uploadArea.classList.remove('dragover'); | |
| const files = Array.from(e.dataTransfer.files); | |
| processFiles(files); | |
| } | |
| function handleFileSelect(e) { | |
| const files = Array.from(e.target.files); | |
| processFiles(files); | |
| } | |
| function processFiles(files) { | |
| const imageFiles = files.filter(file => file.type.startsWith('image/')); | |
| imageFiles.forEach(file => { | |
| if (uploadedImages.length < 20) { | |
| const reader = new FileReader(); | |
| reader.onload = (e) => { | |
| uploadedImages.push({ | |
| file: file, | |
| url: e.target.result, | |
| name: file.name | |
| }); | |
| updateImagePreview(); | |
| updateImageAlert(); | |
| }; | |
| reader.readAsDataURL(file); | |
| } | |
| }); | |
| } | |
| function updateImagePreview() { | |
| imagePreview.innerHTML = ''; | |
| uploadedImages.forEach((image, index) => { | |
| const imageItem = document.createElement('div'); | |
| imageItem.className = 'image-item'; | |
| imageItem.innerHTML = ` | |
| <img src="${image.url}" alt="${image.name}"> | |
| <button class="remove-btn" onclick="removeImage(${index})">×</button> | |
| `; | |
| imagePreview.appendChild(imageItem); | |
| }); | |
| } | |
| function removeImage(index) { | |
| uploadedImages.splice(index, 1); | |
| updateImagePreview(); | |
| updateImageAlert(); | |
| } | |
| function updateImageAlert() { | |
| const alert = document.getElementById('imageAlert'); | |
| if (uploadedImages.length < 5) { | |
| alert.style.display = 'block'; | |
| alert.className = 'alert alert-warning'; | |
| alert.innerHTML = `<strong>Atenção:</strong> Você precisa fazer upload de pelo menos 5 imagens. Atualmente: ${uploadedImages.length} imagens.`; | |
| } else if (uploadedImages.length >= 5) { | |
| alert.style.display = 'block'; | |
| alert.className = 'alert alert-success'; | |
| alert.innerHTML = `<strong>Perfeito!</strong> ${uploadedImages.length} imagens carregadas. Pronto para treinar!`; | |
| } | |
| } | |
| async function startTraining() { | |
| // Validações | |
| if (uploadedImages.length < 5) { | |
| showError('Você precisa fazer upload de pelo menos 5 imagens.'); | |
| return; | |
| } | |
| const characterName = document.getElementById('characterName').value.trim(); | |
| const triggerWord = document.getElementById('triggerWord').value.trim(); | |
| if (!characterName) { | |
| showError('Por favor, insira o nome do personagem.'); | |
| return; | |
| } | |
| if (!triggerWord) { | |
| showError('Por favor, insira a palavra-chave (trigger).'); | |
| return; | |
| } | |
| // Preparar dados para envio | |
| const formData = new FormData(); | |
| uploadedImages.forEach((image, index) => { | |
| formData.append('images', image.file); | |
| }); | |
| formData.append('character_name', characterName); | |
| formData.append('trigger_word', triggerWord); | |
| formData.append('resolution', document.getElementById('resolution').value); | |
| formData.append('learning_rate', document.getElementById('learningRate').value); | |
| formData.append('rank', document.getElementById('rank').value); | |
| formData.append('epochs', document.getElementById('epochs').value); | |
| formData.append('description', document.getElementById('description').value); | |
| // Iniciar treinamento | |
| trainingInProgress = true; | |
| updateTrainingUI(); | |
| try { | |
| const response = await fetch('/api/train', { | |
| method: 'POST', | |
| body: formData | |
| }); | |
| if (!response.ok) { | |
| throw new Error(`Erro HTTP: ${response.status}`); | |
| } | |
| const result = await response.json(); | |
| if (result.success) { | |
| startProgressMonitoring(result.training_id); | |
| } else { | |
| throw new Error(result.message || 'Erro desconhecido'); | |
| } | |
| } catch (error) { | |
| showError(`Erro ao iniciar treinamento: ${error.message}`); | |
| trainingInProgress = false; | |
| updateTrainingUI(); | |
| } | |
| } | |
| function startProgressMonitoring(trainingId) { | |
| trainingInterval = setInterval(async () => { | |
| try { | |
| const response = await fetch(`/api/training-status/${trainingId}`); | |
| const status = await response.json(); | |
| updateProgress(status.progress, status.message); | |
| updateLogs(status.logs); | |
| if (status.completed) { | |
| clearInterval(trainingInterval); | |
| trainingInProgress = false; | |
| updateTrainingUI(); | |
| showTrainingComplete(status.download_links, status.trigger_word); | |
| } else if (status.error) { | |
| clearInterval(trainingInterval); | |
| trainingInProgress = false; | |
| updateTrainingUI(); | |
| showError(status.error); | |
| } | |
| } catch (error) { | |
| console.error('Erro ao verificar status:', error); | |
| } | |
| }, 2000); | |
| } | |
| function stopTraining() { | |
| if (trainingInterval) { | |
| clearInterval(trainingInterval); | |
| } | |
| trainingInProgress = false; | |
| updateTrainingUI(); | |
| showError('Treinamento interrompido pelo usuário.'); | |
| } | |
| function updateTrainingUI() { | |
| const startBtn = document.getElementById('startTraining'); | |
| const stopBtn = document.getElementById('stopTraining'); | |
| const progressContainer = document.getElementById('progressContainer'); | |
| const logContainer = document.getElementById('logContainer'); | |
| if (trainingInProgress) { | |
| startBtn.style.display = 'none'; | |
| stopBtn.style.display = 'inline-block'; | |
| progressContainer.style.display = 'block'; | |
| logContainer.style.display = 'block'; | |
| } else { | |
| startBtn.style.display = 'inline-block'; | |
| stopBtn.style.display = 'none'; | |
| if (!document.getElementById('resultSection').style.display) { | |
| progressContainer.style.display = 'none'; | |
| logContainer.style.display = 'none'; | |
| } | |
| } | |
| } | |
| function updateProgress(progress, message) { | |
| const progressFill = document.getElementById('progressFill'); | |
| const progressText = document.getElementById('progressText'); | |
| progressFill.style.width = `${progress}%`; | |
| progressText.textContent = message || `Progresso: ${progress}%`; | |
| } | |
| function updateLogs(logs) { | |
| const logContainer = document.getElementById('trainingLogs'); | |
| if (logs && logs.length > 0) { | |
| logContainer.innerHTML = logs.join('\n'); | |
| logContainer.scrollTop = logContainer.scrollHeight; | |
| } | |
| } | |
| function showTrainingComplete(downloadLinks, triggerWord) { | |
| const resultSection = document.getElementById('resultSection'); | |
| const downloadLinksContainer = document.getElementById('downloadLinks'); | |
| const resultTriggerWord = document.getElementById('resultTriggerWord'); | |
| resultSection.style.display = 'block'; | |
| resultTriggerWord.textContent = triggerWord; | |
| downloadLinksContainer.innerHTML = ''; | |
| downloadLinks.forEach(link => { | |
| const btn = document.createElement('a'); | |
| btn.href = link.url; | |
| btn.className = 'btn btn-success'; | |
| btn.textContent = `📥 ${link.name}`; | |
| btn.download = link.name; | |
| downloadLinksContainer.appendChild(btn); | |
| }); | |
| document.getElementById('successAlert').style.display = 'block'; | |
| // Scroll para a seção de resultados | |
| resultSection.scrollIntoView({ behavior: 'smooth' }); | |
| } | |
| function showError(message) { | |
| const errorAlert = document.getElementById('errorAlert'); | |
| const errorMessage = document.getElementById('errorMessage'); | |
| errorMessage.textContent = message; | |
| errorAlert.style.display = 'block'; | |
| setTimeout(() => { | |
| errorAlert.style.display = 'none'; | |
| }, 5000); | |
| } | |
| // Inicialização | |
| updateImageAlert(); | |
| </script> | |
| </body> | |
| </html> | |