File size: 13,354 Bytes
7c8a29e
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
import os
import uuid
import json
import threading
import subprocess
import time
from datetime import datetime
from flask import Blueprint, request, jsonify, send_file
from werkzeug.utils import secure_filename
import shutil
from src.lora_trainer import create_lora_trainer, validate_training_config

lora_bp = Blueprint('lora', __name__)

# Configurações
UPLOAD_FOLDER = '/tmp/lora_uploads'
TRAINING_FOLDER = '/tmp/lora_training'
RESULTS_FOLDER = '/tmp/lora_results'
ALLOWED_EXTENSIONS = {'png', 'jpg', 'jpeg', 'webp', 'bmp'}

# Armazenamento em memória para status de treinamento
training_status = {}

def allowed_file(filename):
    return '.' in filename and filename.rsplit('.', 1)[1].lower() in ALLOWED_EXTENSIONS

def ensure_directories():
    """Garante que os diretórios necessários existam"""
    for folder in [UPLOAD_FOLDER, TRAINING_FOLDER, RESULTS_FOLDER]:
        os.makedirs(folder, exist_ok=True)

def run_training_process(training_id, config):
    """Executa o processo de treinamento LoRA em thread separada"""
    try:
        # Atualizar status
        training_status[training_id]['status'] = 'preparing'
        training_status[training_id]['progress'] = 10
        training_status[training_id]['message'] = 'Preparando ambiente de treinamento...'
        
        # Criar diretório de treinamento
        training_dir = os.path.join(TRAINING_FOLDER, training_id)
        os.makedirs(training_dir, exist_ok=True)
        
        # Copiar imagens para diretório de treinamento
        images_dir = os.path.join(training_dir, 'images')
        os.makedirs(images_dir, exist_ok=True)
        
        upload_dir = os.path.join(UPLOAD_FOLDER, training_id)
        for filename in os.listdir(upload_dir):
            if allowed_file(filename):
                src = os.path.join(upload_dir, filename)
                dst = os.path.join(images_dir, filename)
                shutil.copy2(src, dst)
        
        training_status[training_id]['progress'] = 20
        training_status[training_id]['message'] = 'Preparando dataset...'
        
        # Configurar paths para treinamento
        output_dir = os.path.join(RESULTS_FOLDER, training_id)
        os.makedirs(output_dir, exist_ok=True)
        
        config['images_dir'] = images_dir
        config['output_dir'] = output_dir
        
        # Validar configuração
        is_valid, validation_message = validate_training_config(config)
        if not is_valid:
            raise Exception(f"Configuração inválida: {validation_message}")
        
        training_status[training_id]['progress'] = 30
        training_status[training_id]['message'] = 'Iniciando treinamento LoRA...'
        
        # Callback para atualizar progresso
        def progress_callback(progress, message):
            training_status[training_id]['progress'] = max(30, min(90, progress))
            training_status[training_id]['message'] = message
            training_status[training_id]['logs'] = training_status[training_id].get('logs', []) + [message]
        
        # Criar e executar trainer
        trainer = create_lora_trainer(config)
        
        # Executar treinamento
        trainer.train(progress_callback=progress_callback)
        
        # Atualizar logs
        training_status[training_id]['logs'] = trainer.training_logs
        
        training_status[training_id]['status'] = 'completed'
        training_status[training_id]['progress'] = 100
        training_status[training_id]['message'] = 'Treinamento concluído com sucesso!'
        training_status[training_id]['completed'] = True
        
        # Criar arquivos adicionais
        create_additional_files(training_id, config, output_dir)
        
        # Criar links de download
        download_links = []
        for filename in os.listdir(output_dir):
            if os.path.isfile(os.path.join(output_dir, filename)):
                download_links.append({
                    'name': filename,
                    'url': f'/api/download/{training_id}/{filename}'
                })
        
        training_status[training_id]['download_links'] = download_links
        training_status[training_id]['trigger_word'] = config['trigger_word']
        
    except Exception as e:
        training_status[training_id]['status'] = 'error'
        training_status[training_id]['error'] = str(e)
        training_status[training_id]['message'] = f'Erro durante treinamento: {str(e)}'

def create_additional_files(training_id, config, output_dir):
    """Cria arquivos adicionais de resultado"""
    
    # Criar README com instruções detalhadas
    readme_content = f'''# LoRA: {config['character_name']}

## Informações do Treinamento
- **Personagem**: {config['character_name']}
- **Trigger Word**: {config['trigger_word']}
- **Resolução**: {config['resolution']}x{config['resolution']}
- **Rank**: {config['rank']}
- **Learning Rate**: {config['learning_rate']}
- **Épocas**: {config['epochs']}
- **Data de Treinamento**: {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}

## Como Usar

### ComfyUI
1. Coloque o arquivo `pytorch_lora_weights.safetensors` na pasta `models/loras/` do ComfyUI
2. No workflow, adicione um nó "Load LoRA"
3. Selecione o arquivo LoRA
4. Use a trigger word "{config['trigger_word']}" em seus prompts
5. Ajuste o peso entre 0.7 e 1.0

### Automatic1111
1. Coloque o arquivo `pytorch_lora_weights.safetensors` na pasta `models/Lora/`
2. Use a sintaxe `<lora:pytorch_lora_weights:0.8>` no prompt
3. Inclua a trigger word "{config['trigger_word']}" no prompt

### SeaArt
1. Faça upload do arquivo LoRA na plataforma
2. Selecione o LoRA em suas gerações
3. Use a trigger word "{config['trigger_word']}" no prompt

## Exemplos de Prompts
- "{config['trigger_word']}, portrait, high quality"
- "{config['trigger_word']}, full body, standing, detailed"
- "{config['trigger_word']}, close-up, beautiful lighting"
- "{config['trigger_word']}, anime style, colorful"
- "{config['trigger_word']}, realistic, photographic"

## Dicas de Uso
- Use peso entre 0.7-1.0 para melhores resultados
- Combine com outros LoRAs para estilos específicos
- Experimente diferentes CFG scales (7-12)
- Para resultados mais consistentes, use a trigger word no início do prompt
- Ajuste o peso do LoRA conforme necessário para cada geração

## Compatibilidade
Este LoRA é compatível com:
- Stable Diffusion 1.5
- ComfyUI
- Automatic1111 WebUI
- SeaArt
- Fooocus
- InvokeAI
- E outras ferramentas que suportam LoRA

## Suporte
Para dúvidas ou problemas, consulte a documentação da ferramenta utilizada.
'''
    
    readme_file = os.path.join(output_dir, 'README.md')
    with open(readme_file, 'w', encoding='utf-8') as f:
        f.write(readme_content)
    
    # Criar arquivo de metadados
    metadata = {
        "model_name": config['character_name'],
        "trigger_word": config['trigger_word'],
        "base_model": "runwayml/stable-diffusion-v1-5",
        "training_config": config,
        "created_at": datetime.now().isoformat(),
        "version": "1.0",
        "type": "character_lora",
        "tags": ["character", "lora", "consistent", config['character_name']],
        "description": f"LoRA treinado para o personagem {config['character_name']} usando a trigger word '{config['trigger_word']}'",
        "usage_instructions": {
            "trigger_word": config['trigger_word'],
            "recommended_weight": "0.7-1.0",
            "compatible_models": ["SD1.5"],
            "example_prompts": [
                f"{config['trigger_word']}, portrait, high quality",
                f"{config['trigger_word']}, full body, detailed",
                f"{config['trigger_word']}, close-up, beautiful lighting"
            ]
        }
    }
    
    metadata_file = os.path.join(output_dir, 'metadata.json')
    with open(metadata_file, 'w', encoding='utf-8') as f:
        json.dump(metadata, f, indent=2, ensure_ascii=False)

@lora_bp.route('/train', methods=['POST'])
def start_training():
    """Inicia o treinamento LoRA"""
    try:
        ensure_directories()
        
        # Gerar ID único para o treinamento
        training_id = str(uuid.uuid4())
        
        # Verificar se há imagens
        if 'images' not in request.files:
            return jsonify({'success': False, 'message': 'Nenhuma imagem foi enviada'}), 400
        
        files = request.files.getlist('images')
        if len(files) < 5:
            return jsonify({'success': False, 'message': 'Mínimo de 5 imagens necessárias'}), 400
        
        # Criar diretório para upload
        upload_dir = os.path.join(UPLOAD_FOLDER, training_id)
        os.makedirs(upload_dir, exist_ok=True)
        
        # Salvar imagens
        saved_files = []
        for file in files:
            if file and allowed_file(file.filename):
                filename = secure_filename(file.filename)
                filepath = os.path.join(upload_dir, filename)
                file.save(filepath)
                saved_files.append(filename)
        
        if len(saved_files) < 5:
            return jsonify({'success': False, 'message': 'Pelo menos 5 imagens válidas são necessárias'}), 400
        
        # Obter configurações
        config = {
            'character_name': request.form.get('character_name', '').strip(),
            'trigger_word': request.form.get('trigger_word', '').strip(),
            'resolution': request.form.get('resolution', '512'),
            'learning_rate': request.form.get('learning_rate', '1e-4'),
            'rank': request.form.get('rank', '16'),
            'epochs': request.form.get('epochs', '20'),
            'description': request.form.get('description', '').strip(),
            'images': saved_files,
            'training_id': training_id,
            'created_at': datetime.now().isoformat()
        }
        
        # Validar configurações obrigatórias
        if not config['character_name']:
            return jsonify({'success': False, 'message': 'Nome do personagem é obrigatório'}), 400
        
        if not config['trigger_word']:
            return jsonify({'success': False, 'message': 'Trigger word é obrigatória'}), 400
        
        # Inicializar status do treinamento
        training_status[training_id] = {
            'status': 'starting',
            'progress': 0,
            'message': 'Iniciando treinamento...',
            'logs': [],
            'completed': False,
            'error': None,
            'config': config
        }
        
        # Iniciar treinamento em thread separada
        training_thread = threading.Thread(
            target=run_training_process,
            args=(training_id, config)
        )
        training_thread.daemon = True
        training_thread.start()
        
        return jsonify({
            'success': True,
            'training_id': training_id,
            'message': 'Treinamento iniciado com sucesso'
        })
        
    except Exception as e:
        return jsonify({'success': False, 'message': f'Erro interno: {str(e)}'}), 500

@lora_bp.route('/training-status/<training_id>', methods=['GET'])
def get_training_status(training_id):
    """Retorna o status do treinamento"""
    if training_id not in training_status:
        return jsonify({'error': 'Treinamento não encontrado'}), 404
    
    return jsonify(training_status[training_id])

@lora_bp.route('/download/<training_id>/<filename>', methods=['GET'])
def download_file(training_id, filename):
    """Download de arquivos de resultado"""
    try:
        result_dir = os.path.join(RESULTS_FOLDER, training_id)
        file_path = os.path.join(result_dir, filename)
        
        if not os.path.exists(file_path):
            return jsonify({'error': 'Arquivo não encontrado'}), 404
        
        return send_file(file_path, as_attachment=True, download_name=filename)
        
    except Exception as e:
        return jsonify({'error': f'Erro ao baixar arquivo: {str(e)}'}), 500

@lora_bp.route('/trainings', methods=['GET'])
def list_trainings():
    """Lista todos os treinamentos"""
    trainings = []
    for training_id, status in training_status.items():
        trainings.append({
            'id': training_id,
            'character_name': status.get('config', {}).get('character_name', 'Desconhecido'),
            'status': status.get('status', 'unknown'),
            'progress': status.get('progress', 0),
            'created_at': status.get('config', {}).get('created_at', '')
        })
    
    return jsonify({'trainings': trainings})

@lora_bp.route('/delete-training/<training_id>', methods=['DELETE'])
def delete_training(training_id):
    """Remove um treinamento e seus arquivos"""
    try:
        # Remover do status
        if training_id in training_status:
            del training_status[training_id]
        
        # Remover diretórios
        for base_dir in [UPLOAD_FOLDER, TRAINING_FOLDER, RESULTS_FOLDER]:
            training_dir = os.path.join(base_dir, training_id)
            if os.path.exists(training_dir):
                shutil.rmtree(training_dir)
        
        return jsonify({'success': True, 'message': 'Treinamento removido com sucesso'})
        
    except Exception as e:
        return jsonify({'success': False, 'message': f'Erro ao remover treinamento: {str(e)}'}), 500