File size: 10,016 Bytes
fc8e69c
 
1e1df5b
 
 
 
 
 
 
 
fc8e69c
 
 
1e1df5b
fc8e69c
 
 
1e1df5b
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
fc8e69c
3f4e88d
1e1df5b
 
3f4e88d
1e1df5b
 
3f4e88d
 
 
 
1e1df5b
 
 
 
 
 
 
 
 
3f4e88d
 
1e1df5b
 
 
fc8e69c
 
 
30a0766
fc8e69c
 
 
1e1df5b
fc8e69c
1e1df5b
 
 
 
e1676e0
fc8e69c
1e1df5b
e1676e0
fc8e69c
 
1e1df5b
fc8e69c
1e1df5b
 
4300251
b5aeef7
4300251
1e1df5b
 
 
 
fc8e69c
1e1df5b
fc8e69c
1e1df5b
 
 
e1676e0
 
 
 
1e1df5b
e1676e0
fc8e69c
e1676e0
 
1e1df5b
 
e1676e0
fc8e69c
 
 
 
 
30a0766
fc8e69c
 
 
1e1df5b
fc8e69c
1e1df5b
 
fc8e69c
1e1df5b
e1676e0
fc8e69c
1e1df5b
e1676e0
fc8e69c
1e1df5b
e1676e0
fc8e69c
 
 
1e1df5b
e1676e0
fc8e69c
 
1e1df5b
4300251
 
 
 
1e1df5b
 
 
 
 
fc8e69c
1e1df5b
fc8e69c
1e1df5b
 
e1676e0
 
 
 
 
fc8e69c
1e1df5b
 
 
 
fc8e69c
1e1df5b
 
dff87bb
 
 
1e1df5b
dff87bb
 
 
 
 
1e1df5b
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
632bee9
1e1df5b
 
 
 
 
632bee9
1e1df5b
 
 
 
 
 
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
#!/usr/bin/env python3
"""
VINCIE Service UI (Gradio) — Multi-GPU Pool Manager

- Importa e utiliza o singleton 'vince_pool_manager_singleton'.
- A instância do manager é criada uma única vez no primeiro import, mantendo
  os modelos "quentes" em todas as GPUs disponíveis.
- A UI do Gradio despacha as tarefas para o pool manager, que as distribui
  automaticamente entre os workers da GPU.
- UI minimalista: galeria e vídeo por aba, com opções avançadas.
"""

import os
import gradio as gr
from pathlib import Path
from typing import List, Tuple, Optional

# ==============================================================================
# <<< PONTO CENTRAL DA INTEGRAÇÃO >>>
# Importamos o singleton do nosso novo pool manager.
# A inicialização pesada (download, carregamento de modelos para 4 GPUs)
# acontece automaticamente dentro deste módulo na primeira vez que ele é importado.
# ==============================================================================
try:
    from services.vince_pool_manager import vince_pool_manager_singleton as server
except Exception as e:
    print(f"ERRO FATAL: Não foi possível importar o VincePoolManager. A aplicação não pode iniciar.")
    print(f"Detalhe do erro: {e}")
    # Se o import falhar, a aplicação não tem como funcionar.
    # Lançamos um erro para que os logs mostrem claramente o problema.
    raise RuntimeError("Falha na inicialização do VincePoolManager.") from e

# Verificação para garantir que o singleton foi criado com sucesso.
if server is None:
    raise RuntimeError("O VincePoolManager não foi inicializado corretamente. Verifique os logs de erro.")


# --- Funções Utilitárias ---

def _list_media(out_dir: Path, max_images: int = 24) -> Tuple[List[str], Optional[str]]:
    """Busca os arquivos de imagem e vídeo mais recentes no diretório de saída."""
    # Esta função não precisa de alterações.
    img_globs = ("*.png", "*.jpg", "*.jpeg", "*.webp")
    images: List[Path] = []
    for pat in img_globs:
        images += list(out_dir.rglob(pat))
    
    # Ordena por data de modificação para pegar os mais recentes
    images = sorted(images, key=lambda p: p.stat().st_mtime, reverse=True) if images else []
    
    image_paths = [str(p) for p in images[:max_images]]
    
    videos = sorted(out_dir.rglob("*.mp4"), key=lambda p: p.stat().st_mtime, reverse=True)
    video_path = str(videos[0]) if videos else None
    
    return image_paths, video_path


# --- Funções de Callback da UI (Handlers) ---

def ui_multi_turn(
    input_image: Optional[str],
    turns_text: Optional[str],
    steps_input: int,
    cfg_scale_input: float,
    aspect_ratio_input: str,
    resolution_input: int,
    progress=gr.Progress(track_tqdm=True)
):
    """Callback para a aba 'Multi-turn Editing'."""
    progress(0.1, desc="Validando entradas...")
    if not input_image or not Path(input_image).exists():
        gr.Warning("Arquivo de imagem de entrada ausente ou inválido.")
        return [], None
    if not turns_text or not turns_text.strip():
        gr.Warning("As instruções (turns) estão vazias.")
        return [], None

    turns = [ln.strip() for ln in turns_text.splitlines() if ln.strip()]
    
    try:
        progress(0.5, desc="Enviando tarefa para o pool de GPUs. Aguardando a inferência...")
        # A chamada para o servidor agora é limpa e delega todo o trabalho pesado.
        out_dir = server.generate_multi_turn(
            input_image=input_image,
            turns=turns,
            cfg_scale=float(cfg_scale_input),
            aspect_ratio=str(aspect_ratio_input),
            resolution=int(resolution_input),
            steps=int(steps_input),
        )
        progress(0.9, desc="Inferência concluída. Buscando resultados...")
    except Exception as e:
        print(f"[UI][multi_turn] Erro durante a inferência: {e}")
        # gr.Error exibe uma notificação de erro clara para o usuário.
        gr.Error(f"Erro na Geração: {e}")
        return [], None

    out_path = Path(out_dir)
    if not out_path.exists():
        gr.Warning(f"O diretório de saída '{out_path}' não foi encontrado.")
        return [], None

    imgs, vid = _list_media(out_path)
    if not imgs and not vid:
        gr.Warning(f"Nenhum arquivo de mídia encontrado no diretório de saída.")
    
    return imgs, vid

def ui_multi_concept(
    files: Optional[List[str]],
    descs_text: Optional[str],
    final_prompt: Optional[str],
    steps_input: int,
    cfg_scale_input: float,
    aspect_ratio_input: str,
    resolution_input: int,
    progress=gr.Progress(track_tqdm=True)
):
    """Callback para a aba 'Multi-concept Composition'."""
    progress(0.1, desc="Validando entradas...")
    if not files:
        gr.Warning("Nenhum arquivo de imagem de conceito fornecido.")
        return [], None
    if not descs_text or not descs_text.strip():
        gr.Warning("As descrições dos conceitos estão vazias.")
        return [], None
    if not final_prompt or not final_prompt.strip():
        gr.Warning("O prompt final está vazio.")
        return [], None

    descs = [ln.strip() for ln in descs_text.splitlines() if ln.strip()]
    if len(descs) != len(files):
        gr.Warning(f"O número de descrições ({len(descs)}) não corresponde ao número de imagens ({len(files)}).")
        return [], None

    try:
        progress(0.5, desc="Enviando tarefa para o pool de GPUs. Aguardando a inferência...")
        out_dir = server.generate_multi_concept(
            concept_images=files,
            concept_prompts=descs,
            final_prompt=final_prompt,
            cfg_scale=float(cfg_scale_input),
            aspect_ratio=str(aspect_ratio_input),
            resolution=int(resolution_input),
            steps=int(steps_input),
            pad_placeholder=False, # Este parâmetro pode ser exposto na UI se necessário
        )
        progress(0.9, desc="Inferência concluída. Buscando resultados...")
    except Exception as e:
        print(f"[UI][multi_concept] Erro durante a inferência: {e}")
        gr.Error(f"Erro na Geração: {e}")
        return [], None

    out_path = Path(out_dir)
    imgs, vid = _list_media(out_path)
    return imgs, vid

# --- Definição da Interface Gráfica com Gradio ---
with gr.Blocks(title="VINCIE (Multi-GPU)") as demo:
    gr.Markdown("# 🎨 VINCIE — Edição e Composição (Multi-GPU)")
    gr.Markdown("Esta interface utiliza um pool de GPUs para processar as solicitações de forma rápida e paralela.")

    # Opções Avançadas são definidas uma vez e reutilizadas nas abas
    with gr.Accordion("Opções Avançadas (Comum a todas as abas)", open=False):
        steps_input = gr.Slider(label="Passos de Inferência", minimum=10, maximum=100, step=1, value=50)
        cfg_scale_input = gr.Slider(label="Escala de Orientação (CFG)", minimum=1.0, maximum=15.0, step=0.5, value=7.5)
        aspect_ratio_input = gr.Dropdown(
            label="Proporção da Imagem (Aspect Ratio)",
            choices=["keep_ratio", "1:1", "16:9", "9:16", "4:3", "3:4"],
            value="keep_ratio",
        )
        resolution_input = gr.Slider(label="Resolução (lado menor)", minimum=256, maximum=1024, step=64, value=512)
    
    with gr.Tabs():
        # Aba 1 — Multi-turn Editing
        with gr.TabItem("Edição Sequencial (Multi-turn)"):
            with gr.Row():
                with gr.Column(scale=1):
                    img_in_1 = gr.Image(type="filepath", label="Imagem de Entrada")
                    turns_in_1 = gr.Textbox(lines=8, label="Instruções de Edição (uma por linha)")
                    run_btn_1 = gr.Button("Gerar Edição", variant="primary")
                with gr.Column(scale=2):
                    gallery_out_1 = gr.Gallery(label="Imagens Geradas", columns=4, height="auto")
                    video_out_1 = gr.Video(label="Vídeo Gerado (se aplicável)")
            
            run_btn_1.click(
                fn=ui_multi_turn,
                inputs=[img_in_1, turns_in_1, steps_input, cfg_scale_input, aspect_ratio_input, resolution_input],
                outputs=[gallery_out_1, video_out_1],
            )

        # Aba 2 — Multi-concept Composition
        with gr.TabItem("Composição de Conceitos (Multi-concept)"):
            with gr.Row():
                with gr.Column(scale=1):
                    files_in_2 = gr.File(file_count="multiple", type="filepath", label="Imagens de Conceito")
                    descs_in_2 = gr.Textbox(lines=8, label="Descrições (uma por linha, na mesma ordem das imagens)")
                    final_prompt_in_2 = gr.Textbox(lines=2, label="Prompt Final da Composição")
                    run_btn_2 = gr.Button("Gerar Composição", variant="primary")
                with gr.Column(scale=2):
                    gallery_out_2 = gr.Gallery(label="Imagens Geradas", columns=4, height="auto")
                    video_out_2 = gr.Video(label="Vídeo Gerado (se aplicável)")
            
            run_btn_2.click(
                fn=ui_multi_concept,
                inputs=[files_in_2, descs_in_2, final_prompt_in_2, steps_input, cfg_scale_input, aspect_ratio_input, resolution_input],
                outputs=[gallery_out_2, video_out_2],
            )

# --- Ponto de Entrada da Aplicação ---
if __name__ == "__main__":
    # Busca as configurações do servidor a partir de variáveis de ambiente, com valores padrão.
    server_name = os.getenv("GRADIO_SERVER_NAME", "0.0.0.0")
    server_port = int(os.getenv("GRADIO_SERVER_PORT", "7860"))

    print(f"Iniciando a interface Gradio em http://{server_name}:{server_port}")
    demo.launch(
        server_name=server_name,
        server_port=server_port,
        # allowed_paths é importante para que o Gradio possa servir os arquivos de resultado
        allowed_paths=["/app/outputs", "/app/ckpt"], 
        show_error=True, # Exibe tracebacks de erro detalhados no navegador (bom para depuração)
    )