Files changed (2) hide show
  1. app.py +108 -246
  2. requirements.txt +2 -3
app.py CHANGED
@@ -12,307 +12,169 @@ device = "cuda" if torch.cuda.is_available() else "cpu"
12
  print(f"Usando dispositivo: {device}")
13
 
14
  # --- API Keys ---
15
- # Asegúrate de configurar la variable de entorno TAVILY_API_KEY en tu Space Settings
16
  TAVILY_API_KEY = os.environ.get("TAVILY_API_KEY")
17
  if not TAVILY_API_KEY:
18
- print("Advertencia: Falta TAVILY_API_KEY. La búsqueda no funcionará.")
19
 
20
- try:
21
- tavily_client = TavilyClient(api_key=TAVILY_API_KEY)
22
- except:
23
- tavily_client = None
24
- print("Tavily no disponible.")
25
 
26
- # --- Modelos (con manejo de errores) ---
27
  print("Cargando modelos...")
28
 
29
- try:
30
- # 1. LLM: Flan-T5-Large (más ligero que XL para CPU; cambia a XL si tienes RAM)
31
- model_name = "google/flan-t5-large" # Cambia a "google/flan-t5-xl" si tienes >16GB RAM
32
- tokenizer = AutoTokenizer.from_pretrained(model_name)
33
- llm_model = AutoModelForSeq2SeqLM.from_pretrained(model_name).to(device)
34
- print(f"LLM cargado: {model_name}")
35
- except Exception as e:
36
- print(f"Error cargando LLM: {e}. Usando fallback simple.")
37
- llm_model = None
38
-
39
- try:
40
- # 2. Stable Diffusion
41
- pipe_sd = StableDiffusionPipeline.from_pretrained(
42
- "stabilityai/stable-diffusion-2-1",
43
- torch_dtype=torch.float16 if device == "cuda" else torch.float32,
44
- # safety_checker=None # Comentar si causa warnings
45
- ).to(device)
46
- print("Stable Diffusion cargado.")
47
- except Exception as e:
48
- print(f"Error cargando SD: {e}")
49
- pipe_sd = None
50
-
51
- try:
52
- # 3. BLIP para captioning (con use_fast=True para evitar warning)
53
- blip_processor = BlipProcessor.from_pretrained("Salesforce/blip-image-captioning-base", use_fast=True)
54
- blip_model = BlipForConditionalGeneration.from_pretrained("Salesforce/blip-image-captioning-base").to(device)
55
- print("BLIP cargado.")
56
- except Exception as e:
57
- print(f"Error cargando BLIP: {e}")
58
- blip_processor = blip_model = None
59
-
60
- print("Modelos listos.")
61
 
62
  # --- Historial ---
63
  def guardar_historial(historial, user_id="default"):
64
- # Usar el directorio de trabajo actual si /tmp no es accesible en el entorno
65
- try:
66
- with open(f'history_{user_id}.json', 'w') as f:
67
- json.dump(historial[-20:], f)
68
- except:
69
- pass
70
 
71
  def cargar_historial(user_id="default"):
72
  try:
73
- with open(f'history_{user_id}.json', 'r') as f:
74
  return json.load(f)
75
  except:
76
  return []
77
 
78
- # --- LLM: Generar respuesta ---
79
  def generar_respuesta_llm(prompt):
80
- if llm_model is None:
81
- return f"Fallback: {prompt[:100]}... (LLM no disponible)"
82
- try:
83
- inputs = tokenizer(prompt, return_tensors="pt", truncation=True, max_length=512).to(device)
84
- outputs = llm_model.generate(
85
- **inputs,
86
- max_new_tokens=256,
87
- temperature=0.7,
88
- do_sample=True,
89
- top_p=0.9
90
- )
91
- return tokenizer.decode(outputs[0], skip_special_tokens=True)
92
- except Exception as e:
93
- return f"Error en LLM: {str(e)}"
94
 
95
- # --- Búsqueda ---
96
  def busqueda_tecnica(query):
97
- if tavily_client is None:
98
- return "Búsqueda no disponible (falta API key)."
99
  try:
100
  result = tavily_client.search(query, max_results=3)
101
  return "\n\n".join([f"**{r['title']}**\n{r['content'][:300]}..." for r in result.get('results', [])])
102
- except Exception as e:
103
- return f"Error en búsqueda: {str(e)}"
104
 
105
  # --- Generar imagen ---
106
  def generacion_imagenes(prompt):
107
- if pipe_sd is None:
108
- return None, "Stable Diffusion no disponible."
109
  try:
110
- # Aseguramos que la etiqueta 'BATUTO-ART' est�� en el prompt para la firma
111
- if 'BATUTO-ART' not in prompt:
112
- prompt = f"{prompt}, simple liquid gold marker tag BATUTO-ART in top left corner"
113
-
114
  image = pipe_sd(prompt, num_inference_steps=20).images[0]
115
- # Guardar en el directorio de trabajo para Hugging Face
116
- path = "generated_img.png"
117
  image.save(path)
118
- return Image.open(path), None
119
  except Exception as e:
120
- return None, f"Error: {str(e)}"
121
 
122
  # --- Analizar imagen ---
123
  def analizar_imagen(image):
124
- if blip_processor is None or blip_model is None:
125
- return "Análisis no disponible."
126
- try:
127
- inputs = blip_processor(images=image, return_tensors="pt").to(device)
128
- out = blip_model.generate(**inputs, max_new_tokens=50)
129
- return blip_processor.decode(out[0], skip_special_tokens=True)
130
- except Exception as e:
131
- return f"Error: {str(e)}"
132
 
133
  # --- Pipeline principal ---
134
  def pipeline(texto, imagen, history):
135
  if history is None:
136
  history = cargar_historial()
137
 
138
- # Preparar el historial para el chatbot (Gradio lo maneja como lista de listas)
139
- chatbot_history = []
140
-
141
- # El pipeline principal ahora regresa los mensajes para el chatbot
142
- if not texto and not imagen:
143
- return "", None, chatbot_history, None, "", history # No hacer nada si está vacío
144
-
145
- # Si hay historial, lo cargamos al formato de Gradio para el Chatbot
146
- if history:
147
- for user, bot in history:
148
- # Gradio Chatbot espera [user_msg, bot_msg]
149
- chatbot_history.append([user, bot])
150
-
151
  response_text = ""
152
  response_img = None
153
  response_caption = ""
154
- error_msg = ""
155
- user_msg_for_history = texto or ""
156
-
157
- try:
158
- # --- 1. Procesar texto ---
159
- if texto:
160
- texto = texto.strip()
161
- user_msg_for_history = texto # Guardamos el mensaje original del usuario
162
-
163
- # Búsqueda
164
- if texto.lower().startswith("buscar:"):
165
- query = texto[7:].strip()
166
- response_text = f"**Búsqueda:** {query}\n\n"
167
- response_text += busqueda_tecnica(query)
168
-
169
- # Generar imagen
170
- elif texto.lower().startswith("imagen:") or texto.lower().startswith("dibuja:"):
171
- prompt = texto.lower().replace("imagen:", "").replace("dibuja:", "").strip()
172
- response_text = f"¡Órale, qué buena idea! Ya te estoy generando la imagen para: **{prompt}**"
173
- response_img, err = generacion_imagenes(prompt)
174
- if err:
175
- response_text += f"\n\n¡Aguas! Error al dibujar: {err}"
176
-
177
- # Chat normal
178
- else:
179
- contexto = "Eres BATUTO_INFINITY, un asistente creativo y técnico. Responde útil y directamente.\n\n"
180
- if history:
181
- contexto += "Historial reciente:\n"
182
- for user, bot in history[-3:]:
183
- contexto += f"Usuario: {user}\nAsistente: {bot}\n"
184
- contexto += f"Usuario: {texto}\nAsistente:"
185
-
186
- response_text = generar_respuesta_llm(contexto)
187
-
188
- # --- 2. Procesar imagen (Análisis y Generación de Variante) ---
189
- if imagen:
190
- response_caption = analizar_imagen(imagen)
191
-
192
- # Si solo se sube imagen, el texto de respuesta principal es el análisis
193
- if not texto:
194
- user_msg_for_history = "Análisis de imagen subida."
195
- response_text = f"**Análisis de la Imagen:**\n\n{response_caption}"
196
-
197
- # Siempre se intenta generar una variante si hay BLIP disponible
198
- response_img_variant, err = generacion_imagenes(response_caption)
199
- if response_img_variant:
200
- response_img = response_img_variant # Sobreescribe la imagen generada por texto (si existe)
201
- if err:
202
- # Solo agregamos el error si no hay una imagen de texto previa
203
- if not texto or not response_img:
204
- response_text += f"\n\n**Advertencia:** No se pudo generar la variante visual: {err}"
205
-
206
- # --- 3. Guardar historial y actualizar chatbot ---
207
- if user_msg_for_history or response_text:
208
- # Solo guardamos si hubo interacción válida
209
- history.append((user_msg_for_history, response_text))
210
- guardar_historial(history)
211
-
212
- # Actualizar el formato de Gradio Chatbot para la salida
213
- final_chatbot_output = []
214
- if history:
215
- for user, bot in history:
216
- final_chatbot_output.append([user, bot])
217
-
218
-
219
- except Exception as e:
220
- response_text = f"Error general en el pipeline: {str(e)}"
221
- final_chatbot_output.append([user_msg_for_history, response_text]) # Agregar error al chat
222
-
223
- # Devolvemos: mensaje vacío, imagen input vacía, el estado del chatbot, la imagen output, el caption, el estado del historial
224
- return "", None, final_chatbot_output, response_img, response_caption, history
225
-
226
- # --- Interfaz Gradio (TEMA Y LAYOUT MODIFICADO) ---
227
- try:
228
- # Tema dark compatible con Gradio 4.x+ para un look pro
229
- theme = gr.themes.Base(
230
- primary_hue="sky", # Cambié a sky para un look más techie
231
- secondary_hue="gray",
232
- neutral_hue="slate",
233
- font=[gr.themes.GoogleFont("Space Mono")]
234
- ).set(
235
- body_background_fill="linear-gradient(135deg, #1e1e1e 0%, #2d2d2d 100%)",
236
- block_background_fill="linear-gradient(135deg, #2d2d2d 0%, #1e1e1e 100%)",
237
- button_secondary_background_fill="#3b3b3b",
238
- button_secondary_background_fill_hover="#4a4a4a"
239
- )
240
- except:
241
- theme = None
242
- print("Usando tema por defecto.")
243
 
244
- with gr.Blocks(title="BATUTO_INFINITY", theme=theme) as iface:
245
- # Título más moderno
246
- gr.Markdown("## 🤖 BATUTO_INFINITY: Asistente Pro\n**Chat, Web, Imágenes y Visión Multi-modal**")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
247
 
248
  with gr.Row():
249
- # --- COLUMNA 1: CHATBOT (Principal) ---
250
- with gr.Column(scale=3):
251
- chatbot = gr.Chatbot(
252
- height=550, # Un poco más alto
253
- label="Conversación con BATUTO",
254
- layout="panel" # Estilo más limpio de Gradio 4
255
- )
256
-
257
- # --- COLUMNA 2: ENTRADAS Y SALIDAS (Controles) ---
258
  with gr.Column(scale=2):
259
- gr.Markdown("### 💬 Instrucciones y Preguntas")
260
- texto_input = gr.Textbox(
261
- placeholder="Escribe: 'buscar: IA', 'imagen: un gato astronauta', o tu pregunta normal...",
262
- label="Instrucción / Pregunta",
263
- # Ocultamos la etiqueta grande, el placeholder es suficiente
264
- )
265
-
266
- gr.Markdown("---") # Separador para ordenar
267
-
268
- gr.Markdown("### 🖼️ Entrada y Salida Visual")
269
- with gr.Row():
270
- imagen_input = gr.Image(
271
- label="Sube imagen para analizar",
272
- type="pil",
273
- height=200, # Más compacto
274
- image_mode="L",
275
- show_label=True
276
- )
277
- output_img = gr.Image(
278
- label="Imagen Generada",
279
- height=200
280
- )
281
-
282
- output_caption = gr.Textbox(
283
- label="Caption / Descripción de Imagen",
284
- interactive=False, # Que el usuario no lo edite
285
- lines=2 # Más compacto
286
- )
287
-
288
- # Estado para manejar el historial (lista de tuplas [(user, bot), ...])
289
- state = gr.State([])
290
 
291
- # Submit
292
- def submit_fn(texto, imagen, state):
293
- # La función devuelve 6 valores, alineados con los outputs
294
- out_texto_input, out_imagen_input, out_chatbot, out_img, out_cap, new_state = pipeline(texto, imagen, state)
295
-
296
- # Gradio ahora necesita que el chatbot sea la salida directa
297
- return out_texto_input, out_imagen_input, out_chatbot, out_img, out_cap, new_state
298
 
 
299
  texto_input.submit(
300
- submit_fn,
301
  inputs=[texto_input, imagen_input, state],
302
- outputs=[texto_input, imagen_input, chatbot, output_img, output_caption, state]
 
 
303
  )
304
 
305
- # Ejemplos (lo dejamos igual)
306
  gr.Examples(
307
  examples=[
308
- ["buscar: avances en IA 2025"],
309
- ["imagen: un dragón cyberpunk"],
310
- ["¿Cómo funciona un LLM?"],
311
  ],
312
  inputs=texto_input
313
  )
314
 
315
  if __name__ == "__main__":
316
- # Usamos share=True para generar un enlace temporal, útil en entornos como Colab o si quieres compartirlo
317
- iface.launch(share=True)
318
-
 
12
  print(f"Usando dispositivo: {device}")
13
 
14
  # --- API Keys ---
 
15
  TAVILY_API_KEY = os.environ.get("TAVILY_API_KEY")
16
  if not TAVILY_API_KEY:
17
+ raise ValueError("Falta TAVILY_API_KEY en variables de entorno")
18
 
19
+ tavily_client = TavilyClient(api_key=TAVILY_API_KEY)
 
 
 
 
20
 
21
+ # --- Modelos ---
22
  print("Cargando modelos...")
23
 
24
+ # 1. LLM: Flan-T5-XL (gratis, potente, funciona en Spaces)
25
+ tokenizer = AutoTokenizer.from_pretrained("google/flan-t5-xl")
26
+ llm_model = AutoModelForSeq2SeqLM.from_pretrained("google/flan-t5-xl").to(device)
27
+
28
+ # 2. Stable Diffusion
29
+ pipe_sd = StableDiffusionPipeline.from_pretrained(
30
+ "stabilityai/stable-diffusion-2-1",
31
+ torch_dtype=torch.float16 if device == "cuda" else torch.float32,
32
+ safety_checker=None
33
+ ).to(device)
34
+
35
+ # 3. BLIP para captioning
36
+ blip_processor = BlipProcessor.from_pretrained("Salesforce/blip-image-captioning-base")
37
+ blip_model = BlipForConditionalGeneration.from_pretrained("Salesforce/blip-image-captioning-base").to(device)
38
+
39
+ print("Modelos cargados.")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
40
 
41
  # --- Historial ---
42
  def guardar_historial(historial, user_id="default"):
43
+ with open(f'/tmp/history_{user_id}.json', 'w') as f:
44
+ json.dump(historial[-20:], f) # Guardar solo últimos 20
 
 
 
 
45
 
46
  def cargar_historial(user_id="default"):
47
  try:
48
+ with open(f'/tmp/history_{user_id}.json', 'r') as f:
49
  return json.load(f)
50
  except:
51
  return []
52
 
53
+ # --- LLM: Generar respuesta inteligente ---
54
  def generar_respuesta_llm(prompt):
55
+ inputs = tokenizer(prompt, return_tensors="pt", truncation=True, max_length=512).to(device)
56
+ outputs = llm_model.generate(
57
+ **inputs,
58
+ max_new_tokens=256,
59
+ temperature=0.7,
60
+ do_sample=True,
61
+ top_p=0.9
62
+ )
63
+ return tokenizer.decode(outputs[0], skip_special_tokens=True)
 
 
 
 
 
64
 
65
+ # --- Búsqueda Tavily ---
66
  def busqueda_tecnica(query):
 
 
67
  try:
68
  result = tavily_client.search(query, max_results=3)
69
  return "\n\n".join([f"**{r['title']}**\n{r['content'][:300]}..." for r in result.get('results', [])])
70
+ except:
71
+ return "Error en búsqueda. Revisa tu API key."
72
 
73
  # --- Generar imagen ---
74
  def generacion_imagenes(prompt):
 
 
75
  try:
 
 
 
 
76
  image = pipe_sd(prompt, num_inference_steps=20).images[0]
77
+ path = "/tmp/generated_img.png"
 
78
  image.save(path)
79
+ return Image.open(path)
80
  except Exception as e:
81
+ return None, f"Error generando imagen: {str(e)}"
82
 
83
  # --- Analizar imagen ---
84
  def analizar_imagen(image):
85
+ inputs = blip_processor(images=image, return_tensors="pt").to(device)
86
+ out = blip_model.generate(**inputs, max_new_tokens=50)
87
+ return blip_processor.decode(out[0], skip_special_tokens=True)
 
 
 
 
 
88
 
89
  # --- Pipeline principal ---
90
  def pipeline(texto, imagen, history):
91
  if history is None:
92
  history = cargar_historial()
93
 
 
 
 
 
 
 
 
 
 
 
 
 
 
94
  response_text = ""
95
  response_img = None
96
  response_caption = ""
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
97
 
98
+ # --- 1. Procesar texto ---
99
+ if texto:
100
+ texto = texto.strip()
101
+
102
+ # Búsqueda
103
+ if texto.lower().startswith("buscar:"):
104
+ query = texto[7:].strip()
105
+ response_text = f"**Búsqueda:** {query}\n\n"
106
+ response_text += busqueda_tecnica(query)
107
+
108
+ # Generar imagen desde texto
109
+ elif texto.lower().startswith("imagen:") or texto.lower().startswith("dibuja:"):
110
+ prompt = texto[7:].strip() if texto.lower().startswith("imagen:") else texto[7:].strip()
111
+ response_text = f"Generando imagen para: **{prompt}**"
112
+ response_img, error = generacion_imagenes(prompt)
113
+ if error:
114
+ response_text += f"\n\n{error}"
115
+
116
+ # Chat normal con LLM
117
+ else:
118
+ # Construir contexto
119
+ contexto = "Eres BATUTO_INFINITY, un asistente creativo y técnico. Responde útil y directamente.\n\n"
120
+ if history:
121
+ contexto += "Historial reciente:\n"
122
+ for user, bot in history[-3:]:
123
+ contexto += f"Usuario: {user}\nAsistente: {bot}\n"
124
+ contexto += f"Usuario: {texto}\nAsistente:"
125
+
126
+ response_text = generar_respuesta_llm(contexto)
127
+
128
+ # Guardar en historial
129
+ history.append((texto, response_text))
130
+ guardar_historial(history)
131
+
132
+ # --- 2. Procesar imagen ---
133
+ if imagen:
134
+ response_caption = analizar_imagen(imagen)
135
+ response_text += f"\n\n**Análisis de imagen:** {response_caption}"
136
+
137
+ # Opcional: generar nueva imagen a partir del caption
138
+ response_img, _ = generacion_imagenes(response_caption)
139
+
140
+ return response_text, response_img, response_caption, history
141
+
142
+ # --- Interfaz Gradio ---
143
+ with gr.Blocks(title="BATUTO_INFINITY", theme=gr.themes.Dark()) as iface:
144
+ gr.Markdown("# BATUTO_INFINITY\n**Chat + Búsqueda + Imágenes + Análisis**")
145
 
146
  with gr.Row():
 
 
 
 
 
 
 
 
 
147
  with gr.Column(scale=2):
148
+ chatbot = gr.Chatbot(height=500)
149
+ texto_input = gr.Textbox(placeholder="Escribe: 'buscar: IA', 'imagen: un gato astronauta', o pregunta normal...", label="Entrada")
150
+ imagen_input = gr.Image(label="Sube imagen para analizar", type="pil")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
151
 
152
+ with gr.Column(scale=1):
153
+ gr.Markdown("### Salidas")
154
+ output_img = gr.Image(label="Imagen generada")
155
+ output_caption = gr.Textbox(label="Caption")
156
+
157
+ # Estado oculto
158
+ state = gr.State([])
159
 
160
+ # Enviar con botón o enter
161
  texto_input.submit(
162
+ pipeline,
163
  inputs=[texto_input, imagen_input, state],
164
+ outputs=[chatbot, output_img, output_caption, state]
165
+ ).then(
166
+ lambda: ("", None), outputs=[texto_input, imagen_input]
167
  )
168
 
169
+ # Ejemplos
170
  gr.Examples(
171
  examples=[
172
+ ["buscar: avances en fusión nuclear 2025"],
173
+ ["imagen: un dragón cyberpunk volando sobre Tokio"],
174
+ ["¿Cómo funciona Stable Diffusion?"],
175
  ],
176
  inputs=texto_input
177
  )
178
 
179
  if __name__ == "__main__":
180
+ iface.launch()
 
 
requirements.txt CHANGED
@@ -1,7 +1,6 @@
1
- gradio>=4.0
2
  torch
3
  diffusers[torch]
4
  transformers
5
  tavily-python
6
- pillow
7
- accelerate
 
1
+ gradio
2
  torch
3
  diffusers[torch]
4
  transformers
5
  tavily-python
6
+ pillow