XCarleX commited on
Commit
699a61f
·
verified ·
1 Parent(s): 3a0e4bf

Update vincie_service.py

Browse files
Files changed (1) hide show
  1. vincie_service.py +105 -516
vincie_service.py CHANGED
@@ -1,527 +1,116 @@
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
-
 
1
  #!/usr/bin/env python3
 
 
 
 
 
2
  import os
 
 
 
3
  import json
4
+ import shlex
5
+ import subprocess
 
6
  from pathlib import Path
7
+ from typing import List, Optional
8
+ from huggingface_hub import snapshot_download
 
 
9
 
10
+ class VincieService:
11
+ """
12
+ Serviço mínimo que:
13
+ - garante repositório VINCIE clonado e dependências de modelo baixadas
14
+ - chama python main.py configs/generate.yaml com overrides oficiais
15
+ - expõe funções de alto nível para multi-turn editing e multi-concept composition
16
+ """
17
 
18
+ def __init__(self,
19
+ repo_dir: str = "/app/VINCIE",
20
+ ckpt_dir: str = "/app/ckpt/VINCIE-3B",
21
+ python_bin: str = "python"):
22
+ self.repo_dir = Path(repo_dir)
23
+ self.ckpt_dir = Path(ckpt_dir)
24
+ self.python = python_bin
25
+ self.generate_yaml = self.repo_dir / "configs" / "generate.yaml"
26
+ self.assets_dir = self.repo_dir / "assets"
27
+ self.output_root = Path("/app/outputs")
28
+ self.output_root.mkdir(parents=True, exist_ok=True)
29
+
30
+ # ---------- Setup ----------
31
+ def ensure_repo(self, git_url: str = "https://github.com/ByteDance-Seed/VINCIE") -> None:
32
+ if not self.repo_dir.exists():
33
+ subprocess.run(["git", "clone", git_url, str(self.repo_dir)], check=True)
34
+ # opcional: garantir submódulos/updates mínimos
35
+ # subprocess.run(["git", "pull"], cwd=self.repo_dir, check=True)
36
+
37
+ def ensure_model(self, repo_id: str = "ByteDance-Seed/VINCIE-3B") -> None:
38
+ self.ckpt_dir.mkdir(parents=True, exist_ok=True)
39
+ snapshot_download(
40
+ repo_id=repo_id,
41
+ local_dir=str(self.ckpt_dir),
42
+ local_dir_use_symlinks=False,
43
+ resume_download=True
44
+ )
45
+
46
+ def ready(self) -> bool:
47
+ return self.repo_dir.exists() and self.generate_yaml.exists() and self.ckpt_dir.exists()
48
+
49
+ # ---------- Core runner ----------
50
+ def _run_vincie(self, overrides: List[str], work_output: Path) -> None:
51
+ work_output.mkdir(parents=True, exist_ok=True)
52
+ cmd = [
53
+ self.python,
54
+ "main.py",
55
+ str(self.generate_yaml),
56
+ # overrides list (Hydra/YACS-style) como no README
57
+ *overrides,
58
+ f"generation.output.dir={str(work_output)}"
59
+ ]
60
+ # executar dentro do diretório do repo VINCIE
61
+ subprocess.run(cmd, cwd=self.repo_dir, check=True)
62
+
63
+ # ---------- Multi-turn editing ----------
64
+ def multi_turn_edit(self,
65
+ input_image: str,
66
+ turns: List[str],
67
+ out_dir_name: Optional[str] = None) -> Path:
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
68
  """
69
+ Equivalente ao exemplo:
70
+ python main.py configs/generate.yaml \
71
+ generation.positive_prompt.image_path="[...]"
72
+ generation.positive_prompt.prompts="[...]"
73
+ generation.output.dir=...
 
 
 
 
 
74
  """
75
+ out_dir = self.output_root / (out_dir_name or f"multi_turn_{self._slug(input_image)}")
76
+ image_json = json.dumps([str(input_image)])
77
+ prompts_json = json.dumps(turns)
78
+ overrides = [
79
+ f'generation.positive_prompt.image_path={image_json}',
80
+ f'generation.positive_prompt.prompts={prompts_json}',
81
+ f'ckpt.path={str(self.ckpt_dir)}'
82
+ ]
83
+ self._run_vincie(overrides, out_dir)
84
+ return out_dir
85
+
86
+ # ---------- Multi-concept composition ----------
87
+ def multi_concept_compose(self,
88
+ concept_images: List[str],
89
+ concept_prompts: List[str],
90
+ final_prompt: str,
91
+ out_dir_name: Optional[str] = None) -> Path:
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
92
  """
93
+ Modelo de uso inspirado no README:
94
+ - image_path: lista de imagens dos conceitos
95
+ - prompts: lista com p1..pN e o prompt final concatenado
 
 
 
 
 
 
96
  """
97
+ out_dir = self.output_root / (out_dir_name or "multi_concept")
98
+ imgs_json = json.dumps([str(p) for p in concept_images])
99
+ # prompts devem alinhar com <IMG0>, <IMG1>, ... e incluir o prompt final no fim
100
+ prompts_all = concept_prompts + [final_prompt]
101
+ prompts_json = json.dumps(prompts_all)
102
+ overrides = [
103
+ f'generation.positive_prompt.image_path={imgs_json}',
104
+ f'generation.positive_prompt.prompts={prompts_json}',
105
+ f"generation.pad_img_placehoder=False", # segue exemplo público
106
+ f'ckpt.path={str(self.ckpt_dir)}'
107
+ ]
108
+ self._run_vincie(overrides, out_dir)
109
+ return out_dir
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
110
 
111
+ # ---------- Helpers ----------
112
+ @staticmethod
113
+ def _slug(path_or_text: str) -> str:
114
+ base = Path(path_or_text).stem if Path(path_or_text).exists() else path_or_text
115
+ keep = "".join(c if c.isalnum() or c in "-_." else "_" for c in str(base))
116
+ return keep[:64]