File size: 7,743 Bytes
8b16063
 
d7cf2e1
 
 
8b16063
b51f255
8b16063
5fb7e04
647f24c
989af85
5fb7e04
f66e7a8
68e1f2f
d7cf2e1
6b73089
8b16063
 
 
d7cf2e1
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
5fb7e04
8b16063
d7cf2e1
 
 
 
5fb7e04
 
d501229
8b16063
647f24c
 
8b16063
d7cf2e1
68e1f2f
 
3a19604
 
989af85
3a19604
 
647f24c
d7cf2e1
647f24c
d7cf2e1
647f24c
 
d7cf2e1
 
 
 
 
647f24c
d7cf2e1
6105e2a
647f24c
d7cf2e1
 
68e1f2f
d7cf2e1
 
 
 
 
5107215
d7cf2e1
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
d501229
647f24c
5fb7e04
91c107d
d7cf2e1
d501229
d7cf2e1
d501229
 
647f24c
 
91c107d
647f24c
 
8b16063
d501229
5fb7e04
 
647f24c
5fb7e04
 
647f24c
5fb7e04
8b16063
647f24c
5fb7e04
 
 
 
d7cf2e1
5fb7e04
647f24c
8b16063
647f24c
99671cb
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
# aduc_framework/managers/llama_scout_manager.py
#
# Versão 21.0.0 (Literal Function Replica)
# Implementa a função `generate_text_from_image` exatamente como no script
# da Meta para garantir 100% de fidelidade.

import yaml
import os
import logging
import torch
from PIL import Image
from typing import List, Callable
# --- IMPORTAÇÕES EXATAS DO SCRIPT DA META ---
from transformers import MllamaForConditionalGeneration, MllamaProcessor
from transformers import AutoTokenizer, AutoModelForCausalLM

from ..tools.hardware_manager import hardware_manager

logger = logging.getLogger(__name__)

# --- A FUNÇÃO ORIGINAL DA META, AGORA DENTRO DO NOSSO ARQUIVO ---
def generate_text_from_image(
    model, processor, image, prompt_text: str,
    temperature: float = 0.6, top_p: float = 0.9, max_new_tokens=2048
):
    """Generate text from image using model"""
    # 1. Garante que a imagem está no formato correto
    processed_image = image.convert("RGB")

    # 2. Constrói a estrutura da conversa
    conversation = [
        {"role": "user", "content": [{"type": "image"}, {"type": "text", "text": prompt_text}]},
    ]

    # 3. Usa o `apply_chat_template` para criar o prompt
    prompt = processor.apply_chat_template(
        conversation, add_generation_prompt=True, tokenize=False
    )

    # 4. O processador combina a imagem e o texto
    inputs = processor(
        processed_image, prompt,
        text_kwargs={"add_special_tokens": False},
        return_tensors="pt"
    ).to(model.device)

    # 5. O modelo gera a resposta
    output = model.generate(
        **inputs,
        temperature=temperature,
        top_p=top_p,
        max_new_tokens=max_new_tokens,
        do_sample=True # Necessário para usar temperature e top_p
    )
    
    # 6. Decodifica e limpa a resposta
    full_response = processor.decode(output[0])
    clean_response = full_response[len(prompt):]

    if clean_response.endswith("<|eot_id|>"):
            clean_response = clean_response[:-len("<|eot_id|>")].strip()

    return clean_response.strip()
# -----------------------------------------------------------------

class LlamaScoutManager:
    """
    Uma casca fina que carrega os modelos e chama a função de inferência
    oficial da Meta.
    """
    def __init__(self, config: dict):
        self.hf_token = os.getenv("HF_TOKEN")
        if not self.hf_token: raise ValueError("HF_TOKEN é necessário.")

        multimodal_id = config['multimodal_model_id']
        helper_id = config['helper_model_id']

        logger.info(f"LLAMA SCOUT (Literal Replica): Carregando Cinegrafista: {multimodal_id}...")
        self.multimodal_processor = MllamaProcessor.from_pretrained(multimodal_id, token=self.hf_token)
        self.multimodal_model = MllamaForConditionalGeneration.from_pretrained(
            multimodal_id,
            torch_dtype=torch.bfloat16,
            use_safetensors=True,
            device_map="auto",
            token=self.hf_token
        )
        logger.info("LLAMA SCOUT (Literal Replica): Cinegrafista (Llama 3.2 Vision) carregado.")

        logger.info(f"LLAMA SCOUT (Literal Replica): Carregando Diretor: {helper_id}...")
        self.helper_tokenizer = AutoTokenizer.from_pretrained(helper_id, token=self.hf_token)
        self.helper_model = AutoModelForCausalLM.from_pretrained(
            helper_id,
            torch_dtype=torch.bfloat16,
            device_map="auto",
            token=self.hf_token,
            attn_implementation="flash_attention_2"
        )
        logger.info("LLAMA SCOUT (Literal Replica): Diretor (Llama 3.1 8B) carregado.")

    @torch.inference_mode()
    def analyze_sequence(self, image_list: List[Image.Image], question: str, progress_callback: Callable = None) -> str:
        if not image_list: return "Nenhuma imagem fornecida."

        # A lógica agora é simples: chamar a função replicada.
        # Nós ainda usamos nossa lógica de "chunking" se necessário.
        if len(image_list) == 1:
            if progress_callback: progress_callback(0.2, f"Analisando 1 imagem com a função oficial...")
            return generate_text_from_image(self.multimodal_model, self.multimodal_processor, image_list[0], question)
        
        # Para múltiplas imagens, ainda precisamos da nossa lógica de orquestração.
        else:
            if progress_callback: progress_callback(0.1, f"Múltiplas imagens detectadas. Analisando em chunks...")
            # A API do modelo funciona melhor com uma imagem, então vamos analisar uma por uma
            # e depois pedir ao nosso Diretor para resumir.
            partial_analyses = []
            for i, image in enumerate(image_list):
                progress = 0.1 + (i / len(image_list)) * 0.8
                if progress_callback: progress_callback(progress, f"Analisando imagem {i+1}/{len(image_list)}...")
                
                # Criamos uma pergunta específica para cada imagem
                chunk_question = f"Esta é a imagem {i+1} de uma sequência. {question}"
                analysis = generate_text_from_image(self.multimodal_model, self.multimodal_processor, image, chunk_question)
                partial_analyses.append(analysis)
            
            return self._summarize_with_helper(partial_analyses, question, progress_callback)

    @torch.inference_mode()
    def _summarize_with_helper(self, partial_texts: List[str], original_question: str, progress_callback: Callable) -> str:
        if progress_callback: progress_callback(0.9, "Síntese com o Diretor 8B (Local)...")
        combined_partials = "\n\n---\n\n".join(f"Análise da Imagem {i+1}:\n{text}" for i, text in enumerate(partial_texts))
        prompt = (f"Você é um diretor de cinema. Sua visão é: '{original_question}'. "
                  f"Seu cinegrafista enviou os seguintes relatórios, um para cada imagem de uma sequência: {combined_partials}. "
                  "Sintetize esses relatórios em uma única resposta final, coesa e poderosa, "
                  "que atenda à sua visão original. Responda diretamente, sem mencionar os relatórios.")
        messages = [{"role": "user", "content": prompt}]
        input_ids = self.helper_tokenizer.apply_chat_template(messages, add_generation_prompt=True, return_tensors="pt").to(self.helper_model.device)
        outputs = self.helper_model.generate(input_ids, max_new_tokens=2048, do_sample=False)
        response = self.helper_tokenizer.decode(outputs[0][len(input_ids[0]):], skip_special_tokens=True)
        return response.strip()

# (Placeholder e instanciação singleton permanecem iguais)
class LlamaScoutPlaceholder:
    def __init__(self, reason: str = "Motivo desconhecido"):
        logger.error(f"LlamaScoutManager não inicializado. Razão: {reason}. Placeholder em uso.")
        self.reason = reason
    def analyze_sequence(self, *args, **kwargs):
        return f"ERRO: Especialista Llama Scout indisponível. Razão: {self.reason}"

try:
    with open("config.yaml", 'r') as f: config = yaml.safe_load(f)
    llama_scout_config = config['specialists'].get('llama_scout')
    if llama_scout_config and llama_scout_config.get('gpus_required', 0) > 0:
        hardware_manager.allocate_gpus('LlamaScout', llama_scout_config['gpus_required'])
        llama_scout_manager_singleton = LlamaScoutManager(config=llama_scout_config)
        logger.info("Especialista de Análise (Original Recipe) pronto.")
    else:
        llama_scout_manager_singleton = LlamaScoutPlaceholder("Não habilitado na config.yaml")
except Exception as e:
    logger.critical(f"Falha CRÍTICA ao inicializar o LlamaScoutManager (Local): {e}", exc_info=True)
    llama_scout_manager_singleton = LlamaScoutPlaceholder(reason=str(e))