Lora-trainer-all / index.html
Allex21's picture
Upload 12 files
7c8a29e verified
<!DOCTYPE html>
<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>