XCarleX commited on
Commit
3a0e4bf
·
verified ·
1 Parent(s): 088bf18

Create vincie_service.py

Browse files
Files changed (1) hide show
  1. vincie_service.py +527 -0
vincie_service.py ADDED
@@ -0,0 +1,527 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/usr/bin/env python3
2
+ """
3
+ VINCIE Service - Multi-turn Image Editing Service
4
+ Serviço completo para VINCIE: Unlocking In-context Image Editing from Video
5
+ """
6
+
7
+ import os
8
+ import sys
9
+ import time
10
+ import uuid
11
+ import json
12
+ import torch
13
+ import gradio as gr
14
+ import argparse
15
+ from pathlib import Path
16
+ from typing import List, Dict, Any, Optional, Tuple
17
+ from datetime import datetime
18
+ from PIL import Image
19
+ import logging
20
+
21
+ # Configure logging
22
+ logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s')
23
+ logger = logging.getLogger(__name__)
24
+
25
+ class VINCIEService:
26
+ """Serviço completo para VINCIE Multi-turn Image Editing"""
27
+
28
+ def __init__(self, model_path: str = "ckpt/VINCIE-3B"):
29
+ self.model_path = model_path
30
+ self.model = None
31
+ self.device = "cuda" if torch.cuda.is_available() else "cpu"
32
+ self.output_dir = Path("outputs")
33
+ self.output_dir.mkdir(exist_ok=True)
34
+
35
+ logger.info(f"🎨 VINCIE Service iniciado")
36
+ logger.info(f" Device: {self.device}")
37
+ logger.info(f" Model path: {self.model_path}")
38
+
39
+ def load_model(self):
40
+ """Carregar modelo VINCIE"""
41
+ try:
42
+ logger.info("📦 Carregando modelo VINCIE...")
43
+
44
+ # Importar módulos VINCIE
45
+ sys.path.append('.')
46
+ from vincie.models import VINCIE
47
+
48
+ # Carregar checkpoint
49
+ self.model = VINCIE.from_pretrained(
50
+ self.model_path,
51
+ torch_dtype=torch.bfloat16,
52
+ device_map="auto"
53
+ )
54
+
55
+ logger.info("✅ Modelo VINCIE carregado com sucesso!")
56
+ return True
57
+
58
+ except Exception as e:
59
+ logger.error(f"❌ Erro ao carregar modelo: {e}")
60
+ return False
61
+
62
+ def multi_turn_editing(
63
+ self,
64
+ input_image: str,
65
+ prompts: List[str],
66
+ output_name: str = None
67
+ ) -> List[str]:
68
+ """
69
+ Multi-turn image editing
70
+
71
+ Args:
72
+ input_image: Caminho da imagem inicial
73
+ prompts: Lista de prompts para edição sequencial
74
+ output_name: Nome base para outputs
75
+
76
+ Returns:
77
+ Lista com caminhos das imagens geradas
78
+ """
79
+ if not self.model:
80
+ if not self.load_model():
81
+ return []
82
+
83
+ try:
84
+ # Preparar nome de saída
85
+ if not output_name:
86
+ output_name = f"multiturn_{int(time.time())}"
87
+
88
+ output_folder = self.output_dir / output_name
89
+ output_folder.mkdir(exist_ok=True)
90
+
91
+ logger.info(f"🎨 Multi-turn editing iniciado:")
92
+ logger.info(f" Input: {input_image}")
93
+ logger.info(f" Turns: {len(prompts)}")
94
+
95
+ # Processar cada turn
96
+ results = []
97
+ current_image = input_image
98
+
99
+ for i, prompt in enumerate(prompts, 1):
100
+ logger.info(f" Turn {i}: {prompt}")
101
+
102
+ # Gerar imagem editada
103
+ result = self._generate_single_edit(current_image, prompt)
104
+
105
+ if result:
106
+ # Salvar resultado
107
+ output_path = output_folder / f"turn_{i:02d}.png"
108
+ result.save(output_path)
109
+ results.append(str(output_path))
110
+
111
+ # Usar resultado como input do próximo turn
112
+ current_image = str(output_path)
113
+
114
+ logger.info(f" ✅ Turn {i} concluído: {output_path}")
115
+ else:
116
+ logger.error(f" ❌ Turn {i} falhou")
117
+ break
118
+
119
+ # Criar GIF animado dos resultados
120
+ if len(results) > 1:
121
+ self._create_editing_animation(results, output_folder / "animation.gif")
122
+
123
+ logger.info(f"✅ Multi-turn editing concluído: {len(results)} imagens")
124
+ return results
125
+
126
+ except Exception as e:
127
+ logger.error(f"❌ Erro no multi-turn editing: {e}")
128
+ return []
129
+
130
+ def multi_concept_composition(
131
+ self,
132
+ concept_images: List[str],
133
+ concept_descriptions: List[str],
134
+ final_prompt: str,
135
+ output_name: str = None
136
+ ) -> Optional[str]:
137
+ """
138
+ Multi-concept composition
139
+
140
+ Args:
141
+ concept_images: Lista de imagens dos conceitos
142
+ concept_descriptions: Descrições de cada conceito
143
+ final_prompt: Prompt final para composição
144
+ output_name: Nome do arquivo de saída
145
+
146
+ Returns:
147
+ Caminho da imagem composta
148
+ """
149
+ if not self.model:
150
+ if not self.load_model():
151
+ return None
152
+
153
+ try:
154
+ if not output_name:
155
+ output_name = f"composition_{int(time.time())}.png"
156
+
157
+ logger.info(f"🎭 Multi-concept composition:")
158
+ logger.info(f" Concepts: {len(concept_images)}")
159
+ logger.info(f" Final prompt: {final_prompt}")
160
+
161
+ # Preparar prompts no formato VINCIE
162
+ prompts = []
163
+ for i, desc in enumerate(concept_descriptions):
164
+ prompts.append(f"<IMG{i}>: {desc}")
165
+
166
+ prompts.append(final_prompt)
167
+
168
+ # Gerar composição
169
+ result = self._generate_composition(concept_images, prompts)
170
+
171
+ if result:
172
+ output_path = self.output_dir / output_name
173
+ result.save(output_path)
174
+
175
+ logger.info(f"✅ Composição criada: {output_path}")
176
+ return str(output_path)
177
+ else:
178
+ logger.error("❌ Falha na geração da composição")
179
+ return None
180
+
181
+ except Exception as e:
182
+ logger.error(f"❌ Erro na composição: {e}")
183
+ return None
184
+
185
+ def story_generation(
186
+ self,
187
+ story_prompts: List[str],
188
+ initial_image: Optional[str] = None,
189
+ output_name: str = None
190
+ ) -> List[str]:
191
+ """
192
+ Story generation através de sequência de imagens
193
+
194
+ Args:
195
+ story_prompts: Lista de prompts da história
196
+ initial_image: Imagem inicial (opcional)
197
+ output_name: Nome base para a história
198
+
199
+ Returns:
200
+ Lista com caminhos das imagens da história
201
+ """
202
+ if not self.model:
203
+ if not self.load_model():
204
+ return []
205
+
206
+ try:
207
+ if not output_name:
208
+ output_name = f"story_{int(time.time())}"
209
+
210
+ story_folder = self.output_dir / output_name
211
+ story_folder.mkdir(exist_ok=True)
212
+
213
+ logger.info(f"📖 Story generation:")
214
+ logger.info(f" Chapters: {len(story_prompts)}")
215
+
216
+ results = []
217
+ current_context = []
218
+
219
+ # Adicionar imagem inicial se fornecida
220
+ if initial_image:
221
+ current_context.append(initial_image)
222
+
223
+ for i, prompt in enumerate(story_prompts, 1):
224
+ logger.info(f" Chapter {i}: {prompt}")
225
+
226
+ # Gerar próxima imagem da história
227
+ result = self._generate_story_frame(current_context, prompt)
228
+
229
+ if result:
230
+ output_path = story_folder / f"chapter_{i:02d}.png"
231
+ result.save(output_path)
232
+ results.append(str(output_path))
233
+
234
+ # Adicionar ao contexto
235
+ current_context.append(str(output_path))
236
+
237
+ logger.info(f" ✅ Chapter {i} gerado: {output_path}")
238
+ else:
239
+ logger.error(f" ❌ Chapter {i} falhou")
240
+
241
+ # Criar storyboard
242
+ if len(results) > 1:
243
+ self._create_storyboard(results, story_folder / "storyboard.png")
244
+
245
+ logger.info(f"✅ História gerada: {len(results)} capítulos")
246
+ return results
247
+
248
+ except Exception as e:
249
+ logger.error(f"❌ Erro na story generation: {e}")
250
+ return []
251
+
252
+ def _generate_single_edit(self, input_image: str, prompt: str) -> Optional[Image.Image]:
253
+ """Gerar uma única edição"""
254
+ try:
255
+ # Implementação específica do VINCIE para edição
256
+ # Usar a API do modelo carregado
257
+ result = self.model.generate(
258
+ input_image=input_image,
259
+ prompt=prompt,
260
+ num_inference_steps=50,
261
+ guidance_scale=7.5,
262
+ height=512,
263
+ width=512
264
+ )
265
+ return result
266
+
267
+ except Exception as e:
268
+ logger.error(f"Erro na edição: {e}")
269
+ return None
270
+
271
+ def _generate_composition(self, images: List[str], prompts: List[str]) -> Optional[Image.Image]:
272
+ """Gerar composição multi-conceito"""
273
+ try:
274
+ result = self.model.generate_composition(
275
+ concept_images=images,
276
+ prompts=prompts,
277
+ num_inference_steps=50,
278
+ guidance_scale=7.5,
279
+ height=768,
280
+ width=768
281
+ )
282
+ return result
283
+
284
+ except Exception as e:
285
+ logger.error(f"Erro na composição: {e}")
286
+ return None
287
+
288
+ def _generate_story_frame(self, context: List[str], prompt: str) -> Optional[Image.Image]:
289
+ """Gerar frame da história"""
290
+ try:
291
+ result = self.model.generate_story_frame(
292
+ context_images=context,
293
+ prompt=prompt,
294
+ num_inference_steps=40,
295
+ guidance_scale=6.0,
296
+ height=512,
297
+ width=768
298
+ )
299
+ return result
300
+
301
+ except Exception as e:
302
+ logger.error(f"Erro no story frame: {e}")
303
+ return None
304
+
305
+ def _create_editing_animation(self, image_paths: List[str], output_path: Path):
306
+ """Criar animação GIF das edições"""
307
+ try:
308
+ images = [Image.open(path) for path in image_paths]
309
+ images[0].save(
310
+ output_path,
311
+ save_all=True,
312
+ append_images=images[1:],
313
+ duration=1000, # 1 segundo por frame
314
+ loop=0
315
+ )
316
+ logger.info(f"📹 Animação criada: {output_path}")
317
+ except Exception as e:
318
+ logger.error(f"Erro na animação: {e}")
319
+
320
+ def _create_storyboard(self, image_paths: List[str], output_path: Path):
321
+ """Criar storyboard das imagens"""
322
+ try:
323
+ images = [Image.open(path) for path in image_paths]
324
+
325
+ # Calcular grid
326
+ cols = min(3, len(images))
327
+ rows = (len(images) + cols - 1) // cols
328
+
329
+ # Tamanho de cada thumbnail
330
+ thumb_width, thumb_height = 256, 256
331
+
332
+ # Criar canvas
333
+ canvas = Image.new(
334
+ 'RGB',
335
+ (cols * thumb_width, rows * thumb_height),
336
+ 'white'
337
+ )
338
+
339
+ # Colar imagens
340
+ for i, img in enumerate(images):
341
+ row = i // cols
342
+ col = i % cols
343
+
344
+ # Redimensionar mantendo aspecto
345
+ img.thumbnail((thumb_width, thumb_height), Image.Resampling.LANCZOS)
346
+
347
+ # Posição no canvas
348
+ x = col * thumb_width + (thumb_width - img.width) // 2
349
+ y = row * thumb_height + (thumb_height - img.height) // 2
350
+
351
+ canvas.paste(img, (x, y))
352
+
353
+ canvas.save(output_path)
354
+ logger.info(f"📋 Storyboard criado: {output_path}")
355
+
356
+ except Exception as e:
357
+ logger.error(f"Erro no storyboard: {e}")
358
+
359
+ def create_gradio_interface(service: VINCIEService):
360
+ """Criar interface Gradio para VINCIE"""
361
+
362
+ def multi_turn_interface(input_image, turns_text, output_name):
363
+ if not input_image or not turns_text:
364
+ return [], "❌ Forneça uma imagem e os prompts"
365
+
366
+ # Parse dos turns (um por linha)
367
+ prompts = [line.strip() for line in turns_text.split('\n') if line.strip()]
368
+
369
+ if not prompts:
370
+ return [], "❌ Nenhum prompt válido fornecido"
371
+
372
+ results = service.multi_turn_editing(input_image, prompts, output_name)
373
+
374
+ if results:
375
+ return results, f"✅ {len(results)} edições geradas com sucesso!"
376
+ else:
377
+ return [], "❌ Falha na geração"
378
+
379
+ def composition_interface(concept_images, descriptions_text, final_prompt, output_name):
380
+ if not concept_images or not descriptions_text or not final_prompt:
381
+ return None, "❌ Forneça imagens, descrições e prompt final"
382
+
383
+ # Parse das descrições
384
+ descriptions = [line.strip() for line in descriptions_text.split('\n') if line.strip()]
385
+
386
+ if len(descriptions) != len(concept_images):
387
+ return None, "❌ Número de descrições deve ser igual ao de imagens"
388
+
389
+ result = service.multi_concept_composition(
390
+ concept_images, descriptions, final_prompt, output_name
391
+ )
392
+
393
+ if result:
394
+ return result, "✅ Composição gerada com sucesso!"
395
+ else:
396
+ return None, "❌ Falha na composição"
397
+
398
+ def story_interface(story_prompts_text, initial_image, output_name):
399
+ if not story_prompts_text:
400
+ return [], "❌ Forneça os prompts da história"
401
+
402
+ # Parse dos prompts da história
403
+ prompts = [line.strip() for line in story_prompts_text.split('\n') if line.strip()]
404
+
405
+ if not prompts:
406
+ return [], "❌ Nenhum prompt válido fornecido"
407
+
408
+ results = service.story_generation(prompts, initial_image, output_name)
409
+
410
+ if results:
411
+ return results, f"✅ História gerada: {len(results)} capítulos!"
412
+ else:
413
+ return [], "❌ Falha na geração da história"
414
+
415
+ # Interface Gradio
416
+ with gr.Blocks(title="VINCIE Service", theme=gr.themes.Soft()) as interface:
417
+ gr.Markdown("""
418
+ # 🎨 VINCIE Multi-turn Image Editing Service
419
+
420
+ **VINCIE**: Unlocking In-context Image Editing from Video
421
+
422
+ Três modos disponíveis:
423
+ - **Multi-turn Editing**: Edite uma imagem em múltiplas etapas
424
+ - **Multi-concept Composition**: Combine conceitos de várias imagens
425
+ - **Story Generation**: Gere uma sequência de imagens contando uma história
426
+ """)
427
+
428
+ with gr.Tabs():
429
+ # Tab 1: Multi-turn Editing
430
+ with gr.TabItem("🔄 Multi-turn Editing"):
431
+ gr.Markdown("### Edição sequencial de uma imagem")
432
+
433
+ with gr.Row():
434
+ with gr.Column(scale=1):
435
+ mt_input_image = gr.Image(
436
+ label="Imagem inicial",
437
+ type="filepath"
438
+ )
439
+ mt_turns = gr.Textbox(
440
+ label="Prompts de edição (um por linha)",
441
+ placeholder="Lower the pineapple beside her face\nAdd a crown to the woman's head\nChange the woman's expression to laughing",
442
+ lines=5
443
+ )
444
+ mt_output_name = gr.Textbox(
445
+ label="Nome da saída (opcional)",
446
+ placeholder="minha_edicao"
447
+ )
448
+ mt_generate_btn = gr.Button("🎨 Gerar Edições", variant="primary")
449
+
450
+ with gr.Column(scale=2):
451
+ mt_output_gallery = gr.Gallery(
452
+ label="Edições geradas",
453
+ show_label=True,
454
+ elem_id="mt_gallery",
455
+ columns=3,
456
+ rows=2,
457
+ height="auto"
458
+ )
459
+ mt_status = gr.Textbox(label="Status", interactive=False)
460
+
461
+ mt_generate_btn.click(
462
+ fn=multi_turn_interface,
463
+ inputs=[mt_input_image, mt_turns, mt_output_name],
464
+ outputs=[mt_output_gallery, mt_status]
465
+ )
466
+
467
+ # Tab 2: Multi-concept Composition
468
+ with gr.TabItem("🎭 Multi-concept Composition"):
469
+ gr.Markdown("### Combine conceitos de múltiplas imagens")
470
+
471
+ with gr.Row():
472
+ with gr.Column(scale=1):
473
+ mc_images = gr.File(
474
+ label="Imagens dos conceitos",
475
+ file_count="multiple",
476
+ file_types=["image"]
477
+ )
478
+ mc_descriptions = gr.Textbox(
479
+ label="Descrições dos conceitos (uma por linha)",
480
+ placeholder="father in casual clothes\nmother with blonde hair\nson with school backpack",
481
+ lines=4
482
+ )
483
+ mc_final_prompt = gr.Textbox(
484
+ label="Prompt final da composição",
485
+ placeholder="A happy family portrait in a sunny park with trees in the background"
486
+ )
487
+ mc_output_name = gr.Textbox(
488
+ label="Nome da saída (opcional)",
489
+ placeholder="composicao_familia"
490
+ )
491
+ mc_generate_btn = gr.Button("🎭 Gerar Composição", variant="primary")
492
+
493
+ with gr.Column(scale=2):
494
+ mc_output = gr.Image(label="Composição gerada")
495
+ mc_status = gr.Textbox(label="Status", interactive=False)
496
+
497
+ mc_generate_btn.click(
498
+ fn=composition_interface,
499
+ inputs=[mc_images, mc_descriptions, mc_final_prompt, mc_output_name],
500
+ outputs=[mc_output, mc_status]
501
+ )
502
+
503
+ # Tab 3: Story Generation
504
+ with gr.TabItem("📖 Story Generation"):
505
+ gr.Markdown("### Gere uma sequência de imagens contando uma história")
506
+
507
+ with gr.Row():
508
+ with gr.Column(scale=1):
509
+ sg_initial = gr.Image(
510
+ label="Imagem inicial (opcional)",
511
+ type="filepath"
512
+ )
513
+ sg_prompts = gr.Textbox(
514
+ label="Prompts da história (um por linha)",
515
+ placeholder="A brave knight starts his journey\nHe encounters a dragon in a cave\nHe befriends the dragon\nThey fly together into the sunset",
516
+ lines=6
517
+ )
518
+ sg_output_name = gr.Textbox(
519
+ label="Nome da história (opcional)",
520
+ placeholder="historia_cavaleiro"
521
+ )
522
+ sg_generate_btn = gr.Button("📖 Gerar História", variant="primary")
523
+
524
+ with gr.Column(scale=2):
525
+ sg_output_gallery = gr.Gallery(
526
+ label="Capítulo
527
+