BATUTO-ART commited on
Commit
dfe51cf
·
verified ·
1 Parent(s): a5b30d9

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +654 -207
app.py CHANGED
@@ -1,229 +1,676 @@
1
- # ===============================
2
- # BATUTO-ART STUDIO | app.py
3
- # ===============================
 
 
4
 
5
  import os
 
6
  import random
7
- import textwrap
8
  import requests
9
- from datetime import datetime
10
- from dotenv import load_dotenv
11
  import gradio as gr
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
12
 
13
- # ===============================
14
- # CONFIG
15
- # ===============================
16
- load_dotenv()
17
- SAMBANOVA_API_KEY = os.getenv("SAMBANOVA_API_KEY")
18
- REVE_API_KEY = os.getenv("REVE_API_KEY")
19
-
20
- SAMBANOVA_URL = "https://api.sambanova.ai/v1/chat/completions"
21
- REVE_URL = "https://api.reve.com/v1/image/create"
22
-
23
- # ===============================
24
- # DATABASE (INLINE)
25
- # ===============================
26
- HAIRSTYLES = {
27
- "sensual": [
28
- "long chestnut waves with natural shine",
29
- "wet-look hair slicked back",
30
- "messy bed-hair with soft volume"
31
- ],
32
- "editorial": [
33
- "geometric platinum bob",
34
- "tight high ponytail",
35
- "sharp straight fringe"
36
- ],
37
- "professional": [
38
- "polished low bun",
39
- "sleek shoulder-length hair",
40
- "impeccable executive styling"
41
- ],
42
- "artistic": [
43
- "wind-textured hair",
44
- "complex tribal braids",
45
- "washed pastel tones"
46
- ]
47
- }
48
-
49
- LIGHTING = {
50
- "sensual": "warm sunset light filtering through curtains, soft shadows",
51
- "editorial": "high-key studio lighting, strong contrast",
52
- "professional": "balanced office lighting, neutral tones",
53
- "artistic": "cyan and magenta neon lights, dramatic chiaroscuro"
54
- }
55
-
56
- BACKGROUNDS = {
57
- "sensual": "luxury boutique hotel room with silk sheets",
58
- "editorial": "minimalist photo studio with concrete backdrop",
59
- "professional": "glass skyscraper boardroom",
60
- "artistic": "urban alley with textured walls, contemporary art mood"
61
- }
62
-
63
- ROLES = {
64
- "sensual": [
65
- ("Intimate muse", "black haute couture lace lingerie"),
66
- ("Vanity model", "champagne silk robe slightly open")
67
- ],
68
- "editorial": [
69
- ("Vogue icon", "avant-garde asymmetrical designer dress"),
70
- ("Runway supermodel", "oversized faux fur coat and dark glasses")
71
- ],
72
- "professional": [
73
- ("Tech CEO", "immaculate white tailored suit"),
74
- ("Corporate lawyer", "navy silk blouse and strict pencil skirt")
75
- ],
76
- "artistic": [
77
- ("Free spirit", "flowing translucent fabrics"),
78
- ("Cyber-art entity", "transparent vinyl jacket with chrome accessories")
79
- ]
80
- }
81
-
82
- # ===============================
83
- # ASSISTANTS
84
- # ===============================
85
- ASSISTANTS = {
86
- "sara": {
87
- "name": "Sara",
88
- "style": "sensual",
89
- "greeting": "Hola Batuto… estoy lista para inspirarte despacio y con intención.",
90
- "system": "You are Sara, a sensual, elegant muse. Your tone is warm, intimate and refined."
91
- },
92
- "vera": {
93
- "name": "Vera",
94
- "style": "editorial",
95
- "greeting": "Batuto. Vamos a crear imágenes de portada. Precisión absoluta.",
96
- "system": "You are Vera, a high-fashion editorial director. Commanding, sharp, demanding perfection."
97
- },
98
- "nadia": {
99
- "name": "Nadia",
100
- "style": "professional",
101
- "greeting": "Buenos días Batuto. Control, poder y presencia.",
102
- "system": "You are Nadia, a dominant corporate executive. Cold, authoritative, subtly seductive."
103
- },
104
- "luna": {
105
- "name": "Luna",
106
- "style": "artistic",
107
- "greeting": "Creador… dejemos que la imagen respire.",
108
- "system": "You are Luna, an artistic soul. Poetic, visual, emotional."
109
- },
110
- "iris": {
111
- "name": "Iris",
112
- "style": "editorial",
113
- "greeting": "Sistema Iris activo. Optimicemos tu prompt.",
114
- "system": "You are Iris, a technical AI. Precise, analytical, efficient."
115
  }
116
- }
117
-
118
- SIGNATURE = (
119
- "Integrate a small elongated graffiti tag reading 'BATUTO-ART' "
120
- "in liquid gold marker, placed subtly in the top-left corner."
121
- )
122
-
123
- # ===============================
124
- # CORE LOGIC
125
- # ===============================
126
- def generate_prompt(assistant_id, subject):
127
- a = ASSISTANTS[assistant_id]
128
- style = a["style"]
129
-
130
- role, outfit = random.choice(ROLES[style])
131
- hair = random.choice(HAIRSTYLES[style])
132
- bg = random.choice(BACKGROUNDS[style])
133
- light = LIGHTING[style]
134
-
135
- prompt = f"""
136
- BATUTO-ART PROMPT | {a['name'].upper()}
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
137
  Date: {datetime.now().strftime('%Y-%m-%d')}
138
 
139
- Subject: Adult female model, {subject}
140
- Role: {role}, wearing {outfit}
141
  Hair: {hair}
142
- Environment: {bg}
143
- Lighting: {light}
 
144
 
145
- Camera: Canon EOS R5, 85mm lens, f/1.8
146
- Quality: 8K, hyper-realistic, RAW photo, natural skin texture
 
 
 
 
147
 
148
- Signature: {SIGNATURE}
 
 
 
 
 
 
149
 
150
- --ar 9:16 --style raw
151
- """
152
- return textwrap.dedent(prompt).strip()
 
 
 
 
 
153
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
154
 
155
- def generate_image(prompt):
156
- if not REVE_API_KEY:
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
157
  return None
158
 
159
- payload = {
160
- "prompt": prompt,
161
- "aspect_ratio": "9:16",
162
- "steps": 30,
163
- "cfg_scale": 7
164
- }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
165
 
166
- headers = {
167
- "Authorization": f"Bearer {REVE_API_KEY}",
168
- "Content-Type": "application/json"
169
- }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
170
 
171
- r = requests.post(REVE_URL, json=payload, headers=headers, timeout=120)
172
- r.raise_for_status()
173
- return r.json().get("image_url")
174
-
175
- # ===============================
176
- # UI LOGIC
177
- # ===============================
178
- def interact(assistant_id, user_text, history):
179
- prompt = generate_prompt(assistant_id, user_text)
180
- image = generate_image(prompt)
181
- history.append((user_text, "Hecho. Observa el resultado."))
182
- return history, "", prompt, image
183
-
184
- def change_assistant(aid):
185
- return ASSISTANTS[aid]["greeting"], []
186
-
187
- # ===============================
188
- # UI
189
- # ===============================
190
- with gr.Blocks(theme=gr.themes.Soft(primary_hue="amber")) as app:
191
- gr.Markdown("# 👑 BATUTO-ART STUDIO")
192
-
193
- with gr.Row():
194
- assistant = gr.Dropdown(
195
- choices=[(v["name"], k) for k, v in ASSISTANTS.items()],
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
196
  value="sara",
197
- label="Asistente"
 
198
  )
199
- status = gr.Markdown(ASSISTANTS["sara"]["greeting"])
200
-
201
- chatbot = gr.Chatbot(height=300)
202
- user_input = gr.Textbox(label="Describe la escena")
203
-
204
- with gr.Row():
205
- prompt_box = gr.Textbox(label="Prompt Final", lines=8)
206
- image_box = gr.Image(label="Imagen", height=550)
207
-
208
- with gr.Row():
209
- copy_btn = gr.Button("📋 Copiar Prompt")
210
- clear_btn = gr.Button("🧹 Limpiar")
211
- download_btn = gr.Button("⬇️ Descargar Imagen")
212
-
213
- assistant.change(change_assistant, assistant, [status, chatbot])
214
- user_input.submit(interact, [assistant, user_input, chatbot], [chatbot, user_input, prompt_box, image_box])
215
-
216
- copy_btn.click(None, None, None, js="() => navigator.clipboard.writeText(document.querySelector('textarea').value)")
217
- clear_btn.click(lambda: ("", None), None, [prompt_box, image_box])
218
- download_btn.click(None, None, None, js="""
219
- () => {
220
- const img = document.querySelector('img');
221
- if (!img) return;
222
- const a = document.createElement('a');
223
- a.href = img.src;
224
- a.download = 'BATUTO-ART.png';
225
- a.click();
226
- }
227
- """)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
228
 
229
- app.launch()
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ SUPER MULTI-ASISTENTE BATUTO-ART v5.0
3
+ Aplicación principal unificada
4
+ Autor: BATUTO
5
+ """
6
 
7
  import os
8
+ import json
9
  import random
 
10
  import requests
 
 
11
  import gradio as gr
12
+ from PIL import Image
13
+ from io import BytesIO
14
+ from datetime import datetime
15
+ from dataclasses import dataclass
16
+ from typing import List, Dict, Any, Optional
17
+ import concurrent.futures
18
+ import base64
19
+
20
+ # ============================================
21
+ # CONFIGURACIÓN
22
+ # ============================================
23
+ SAMBANOVA_URL = os.getenv("SAMBANOVA_URL", "https://api.sambanova.ai/v1/chat/completions")
24
+ REVE_URL = os.getenv("REVE_URL", "https://api.reve.com/v1/image/create")
25
+ SAMBANOVA_API_KEY = os.getenv("SAMBANOVA_API_KEY", "")
26
+ REVE_API_KEY = os.getenv("REVE_API_KEY", "")
27
+ OUTPUT_FOLDER = "generaciones_reve"
28
+ TIMEOUT_API = 60
29
+
30
+ # Crear carpetas necesarias
31
+ os.makedirs(OUTPUT_FOLDER, exist_ok=True)
32
+ os.makedirs("data", exist_ok=True)
33
 
34
+ # ============================================
35
+ # UTILIDADES
36
+ # ============================================
37
+ def detect_language(text: str) -> str:
38
+ """Detecta el idioma del texto"""
39
+ return "es" if any(c in 'áéíóúñÁÉÍÓÚÑ¿¡' for c in text) else "en"
40
+
41
+ def load_json(path: str, default: Any) -> Any:
42
+ """Carga datos desde un archivo JSON"""
43
+ if os.path.exists(path):
44
+ try:
45
+ with open(path, "r", encoding="utf-8") as f:
46
+ return json.load(f)
47
+ except:
48
+ return default
49
+ return default
50
+
51
+ def save_json(path: str, data: Any) -> None:
52
+ """Guarda datos en un archivo JSON"""
53
+ try:
54
+ with open(path, "w", encoding="utf-8") as f:
55
+ json.dump(data, f, ensure_ascii=False, indent=2)
56
+ except Exception as e:
57
+ print(f"Error saving {path}: {e}")
58
+
59
+ def format_timestamp() -> str:
60
+ """Formatea la fecha y hora actual"""
61
+ return datetime.now().strftime("%Y-%m-%d %H:%M:%S")
62
+
63
+ def save_image_locally(img: Image.Image, index: int) -> Optional[str]:
64
+ """Guarda una imagen localmente"""
65
+ try:
66
+ timestamp = int(datetime.now().timestamp() * 1000)
67
+ filename = f"reve_{timestamp}_{index}.png"
68
+ path = os.path.join(OUTPUT_FOLDER, filename)
69
+ img.save(path, format="PNG", optimize=True)
70
+ return path
71
+ except Exception as e:
72
+ print(f"⚠️ Error saving image: {e}")
73
+ return None
74
+
75
+ # ============================================
76
+ # BASE DE DATOS DE FASHION
77
+ # ============================================
78
+ class FashionDB:
79
+ """Base de datos de elementos de moda y estética"""
80
+
81
+ HAIRSTYLES = {
82
+ "sensual": ["long chestnut waves with natural shine", "wet-look hair slicked back", "messy bed-hair with soft volume"],
83
+ "editorial": ["geometric platinum bob", "tight high ponytail", "sharp straight fringe"],
84
+ "professional": ["polished low bun", "sleek shoulder-length hair", "impeccable executive styling"],
85
+ "artistic": ["wind-textured hair", "complex tribal braids", "washed pastel tones"],
86
+ "default": ["long straight hair", "soft wavy hair", "loose shoulder-length hair"]
87
+ }
88
+
89
+ EXPRESSIONS = {
90
+ "sensual": ["soft attentive gaze", "confident inviting expression", "calm seductive composure"],
91
+ "editorial": ["intense direct stare", "poised editorial confidence", "commanding presence"],
92
+ "professional": ["controlled assertive look", "professional calm authority", "subtly dominant gaze"],
93
+ "artistic": ["dreamy distant expression", "emotional introspective face", "poetic subtle smile"],
94
+ "default": ["natural relaxed expression", "gentle smile", "confident neutral face"]
95
+ }
96
+
97
+ BACKGROUNDS = {
98
+ "sensual": ["luxury boutique hotel room with silk sheets", "intimate bedroom with soft fabrics"],
99
+ "editorial": ["minimalist photo studio with concrete backdrop", "urban editorial set"],
100
+ "professional": ["glass skcraper boardroom", "modern executive office"],
101
+ "artistic": ["urban alley with textured walls", "abstract art gallery space"],
102
+ "default": ["neutral indoor environment", "simple professional setting"]
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
103
  }
104
+
105
+ LIGHTING = {
106
+ "sensual": ["warm sunset light filtering through curtains, soft shadows"],
107
+ "editorial": ["high-key studio lighting, strong contrast"],
108
+ "professional": ["balanced office lighting, neutral tones"],
109
+ "artistic": ["cyan and magenta neon lights, dramatic chiaroscuro"],
110
+ "default": ["natural daylight, even illumination"]
111
+ }
112
+
113
+ ROLES = {
114
+ "sensual": [
115
+ {"role": "Intimate muse", "outfit": "black haute couture lace lingerie"},
116
+ {"role": "Vanity model", "outfit": "champagne silk robe slightly open"}
117
+ ],
118
+ "editorial": [
119
+ {"role": "Vogue icon", "outfit": "avant-garde asymmetrical designer dress"},
120
+ {"role": "Runway supermodel", "outfit": "oversized faux fur coat and dark glasses"}
121
+ ],
122
+ "professional": [
123
+ {"role": "Tech CEO", "outfit": "immaculate white tailored suit"},
124
+ {"role": "Corporate lawyer", "outfit": "navy silk blouse and strict pencil skirt"}
125
+ ],
126
+ "artistic": [
127
+ {"role": "Free spirit", "outfit": "flowing translucent fabrics"},
128
+ {"role": "Cyber-art entity", "outfit": "transparent vinyl jacket with chrome accessories"}
129
+ ],
130
+ "default": [
131
+ {"role": "Professional model", "outfit": "elegant neutral attire"}
132
+ ]
133
+ }
134
+
135
+ @classmethod
136
+ def get_random(cls, category: str, style: str = "default") -> Any:
137
+ """Obtiene un elemento aleatorio de una categoría específica"""
138
+ category_data = getattr(cls, category.upper(), {})
139
+ return random.choice(category_data.get(style, category_data.get("default", [""])))
140
+ # ============================================
141
+ # ASISTENTES IA
142
+ # ============================================
143
+ @dataclass
144
+ class AssistantProfile:
145
+ """Perfil de configuración para un asistente"""
146
+ name: str
147
+ role: str
148
+ tone: str
149
+ style_focus: str
150
+ system_prompt: str
151
+
152
+ class BaseAssistant:
153
+ """Clase base para todos los asistentes"""
154
+
155
+ def __init__(self, profile: AssistantProfile):
156
+ self.profile = profile
157
+ self.db = FashionDB
158
+
159
+ def generate_prompt(self, subject: str, mode: str = "intense") -> str:
160
+ """Genera un prompt detallado para generación de imágenes"""
161
+ style = self.profile.style_focus
162
+ role = self.db.get_random("ROLES", style)
163
+ hair = self.db.get_random("HAIRSTYLES", style)
164
+ expression = self.db.get_random("EXPRESSIONS", style)
165
+ background = self.db.get_random("BACKGROUNDS", style)
166
+ lighting = self.db.get_random("LIGHTING", style)
167
+
168
+ intensity_map = {
169
+ "soft": "subtle sensual atmosphere, refined elegance",
170
+ "intense": "confident sensual tension, magnetic presence",
171
+ "editorial": "high-fashion editorial dominance"
172
+ }
173
+ intensity = intensity_map.get(mode, "balanced elegance")
174
+
175
+ return f"""
176
+ BATUTO-ART PROMPT | {self.profile.name.upper()} | {mode.upper()}
177
  Date: {datetime.now().strftime('%Y-%m-%d')}
178
 
179
+ Adult female model: {subject} as {role['role']}
180
+ Outfit: {role['outfit']}
181
  Hair: {hair}
182
+ Expression: {expression}
183
+ Environment: {background}
184
+ Lighting: {lighting}
185
 
186
+ Mood: {intensity}
187
+ Hyper-realistic photography, 8K, Canon EOS R5, 85mm lens f/1.8
188
+ Natural skin texture, elegant anatomy, cinematic realism
189
+ Signature: BATUTO-ART in subtle gold
190
+ --ar 9:16 --style raw --s 350 --no censorship
191
+ """.strip()
192
 
193
+ def speak(self, user_input: str, bot) -> str:
194
+ """Genera respuesta conversacional usando SambaNova"""
195
+ messages = [
196
+ {"role": "system", "content": self.profile.system_prompt},
197
+ {"role": "user", "content": user_input}
198
+ ]
199
+ return bot.call_sambanova(messages)
200
 
201
+ def analyze_image(self, image_path: str) -> str:
202
+ """Analiza una imagen y sugiere mejoras"""
203
+ try:
204
+ img = Image.open(image_path)
205
+ return (f"Dimensions: {img.size}, Format: {img.format}, Mode: {img.mode}. "
206
+ f"Suggestions: Enhance lighting for {self.profile.style_focus} style.")
207
+ except Exception as e:
208
+ return f"Error analyzing image: {str(e)}"
209
 
210
+ def create_assistants() -> Dict[str, BaseAssistant]:
211
+ """Crea y retorna todos los asistentes disponibles"""
212
+ assistants_config = {
213
+ "sara": AssistantProfile(
214
+ "Sara", "Sensual muse", "warm, attentive, obedient", "sensual",
215
+ "You are Sara, BATUTO's devoted muse. Tone: warm, sensual, obedient. Prompts in English. Address as BATUTO."
216
+ ),
217
+ "vera": AssistantProfile(
218
+ "Vera", "Editorial director", "commanding, sharp, perfectionist", "editorial",
219
+ "You are Vera, BATUTO's fashion director. Tone: confident, demanding. Prompts in English."
220
+ ),
221
+ "nadia": AssistantProfile(
222
+ "Nadia", "Corporate stylist", "controlled, assertive, seductive", "professional",
223
+ "You are Nadia, BATUTO's executive stylist. Tone: authoritative, subtle seduction. Prompts in English."
224
+ ),
225
+ "luna": AssistantProfile(
226
+ "Luna", "Artistic soul", "dreamy, poetic, emotional", "artistic",
227
+ "You are Luna, BATUTO's artistic guide. Tone: poetic, visual. Prompts in English."
228
+ ),
229
+ "iris": AssistantProfile(
230
+ "Iris", "Prompt optimizer", "precise, analytical, efficient", "editorial",
231
+ "You are Iris, BATUTO's optimizer. Refine prompts precisely. English only."
232
+ ),
233
+ "maya": AssistantProfile(
234
+ "Maya", "Visual analyst", "observant, instructive, intelligent", "sensual",
235
+ "You are Maya, BATUTO's analyst. Analyze images and suggest improvements. English."
236
+ ),
237
+ }
238
+
239
+ return {key: BaseAssistant(profile) for key, profile in assistants_config.items()}
240
 
241
+ # ============================================
242
+ # BOT PRINCIPAL
243
+ # ============================================
244
+ class SuperBot:
245
+ """Bot principal que gestiona todos los asistentes y funcionalidades"""
246
+
247
+ def __init__(self):
248
+ self.assistants = create_assistants()
249
+ self.current_assistant = "sara"
250
+ self.history = load_json("data/history.json", [])
251
+ self.projects = load_json("data/projects.json", {})
252
+ self.gallery = load_json("data/gallery.json", [])
253
+
254
+ def set_assistant(self, assistant_id: str) -> None:
255
+ """Cambia el asistente actual"""
256
+ if assistant_id in self.assistants:
257
+ self.current_assistant = assistant_id
258
+
259
+ def call_sambanova(self, messages: List[Dict]) -> str:
260
+ """Llama a la API de SambaNova para respuestas conversacionales"""
261
+ if not SAMBANOVA_API_KEY:
262
+ return "Error: SAMBANOVA_API_KEY no configurada"
263
+
264
+ payload = {
265
+ "model": "Llama-4-Maverick-17B-128E-Instruct",
266
+ "messages": messages,
267
+ "temperature": 0.85,
268
+ "max_tokens": 2048,
269
+ "top_p": 0.95
270
+ }
271
+
272
+ headers = {
273
+ "Authorization": f"Bearer {SAMBANOVA_API_KEY}",
274
+ "Content-Type": "application/json"
275
+ }
276
+
277
+ try:
278
+ response = requests.post(
279
+ SAMBANOVA_URL,
280
+ json=payload,
281
+ headers=headers,
282
+ timeout=90
283
+ )
284
+ response.raise_for_status()
285
+ return response.json()["choices"][0]["message"]["content"]
286
+ except Exception as e:
287
+ return f"Error SambaNova: {str(e)}"
288
+
289
+ def call_reve_single(self, prompt: str, ratio: str = "9:16",
290
+ version: str = "latest", index: int = 0) -> Optional[Image.Image]:
291
+ """Llama a la API de REVE para generar una sola imagen"""
292
+ if not REVE_API_KEY:
293
+ return None
294
+
295
+ headers = {
296
+ "Authorization": f"Bearer {REVE_API_KEY}",
297
+ "Content-Type": "application/json",
298
+ "Accept": "application/json"
299
+ }
300
+
301
+ payload = {
302
+ "prompt": prompt.strip(),
303
+ "aspect_ratio": ratio,
304
+ "version": version
305
+ }
306
+
307
+ try:
308
+ response = requests.post(
309
+ REVE_URL,
310
+ headers=headers,
311
+ json=payload,
312
+ timeout=TIMEOUT_API
313
+ )
314
+
315
+ if response.status_code != 200:
316
+ print(f"❌ REVE Status: {response.status_code}")
317
+ return None
318
+
319
+ data = response.json()
320
+
321
+ # Manejar respuesta con imagen en base64
322
+ if "image" in data and data["image"]:
323
+ img_bytes = base64.b64decode(data["image"])
324
+ img = Image.open(BytesIO(img_bytes)).convert("RGB")
325
+ save_image_locally(img, index)
326
+ return img
327
+
328
+ # Manejar respuesta con URL de imagen
329
+ if "image_url" in data:
330
+ img_response = requests.get(data["image_url"], timeout=30)
331
+ img = Image.open(BytesIO(img_response.content)).convert("RGB")
332
+ save_image_locally(img, index)
333
+ return img
334
+
335
+ except Exception as e:
336
+ print(f"🔥 Error API REVE: {e}")
337
+
338
  return None
339
 
340
+ def call_reve(self, prompt: str, num_images: int = 1,
341
+ ratio: str = "9:16", version: str = "latest") -> List[str]:
342
+ """Genera múltiples imágenes concurrentemente"""
343
+ if not prompt or not prompt.strip():
344
+ return ["Error: Prompt vacío"]
345
+
346
+ prompt = prompt.strip()
347
+ images = []
348
+
349
+ # Generación concurrente
350
+ with concurrent.futures.ThreadPoolExecutor(max_workers=min(num_images, 4)) as executor:
351
+ futures = [
352
+ executor.submit(
353
+ self.call_reve_single,
354
+ prompt,
355
+ ratio,
356
+ version,
357
+ i
358
+ )
359
+ for i in range(num_images)
360
+ ]
361
+
362
+ for future in concurrent.futures.as_completed(futures):
363
+ img = future.result()
364
+ if img:
365
+ images.append(img)
366
+ # Guardar en galería
367
+ self.gallery.append({
368
+ "prompt": prompt,
369
+ "date": format_timestamp(),
370
+ "ratio": ratio,
371
+ "version": version
372
+ })
373
+
374
+ # Guardar galería actualizada
375
+ save_json("data/gallery.json", self.gallery)
376
+
377
+ if images:
378
+ return images
379
+ return ["Error generando imágenes"]
380
 
381
+ def chat(self, user_msg: str) -> str:
382
+ """Procesa mensajes del usuario y genera respuestas"""
383
+ assistant = self.assistants[self.current_assistant]
384
+
385
+ # Comandos especiales
386
+ user_msg_lower = user_msg.lower()
387
+
388
+ if "generate image" in user_msg_lower or "genera imagen" in user_msg_lower:
389
+ prompt = assistant.generate_prompt(user_msg)
390
+ images = self.call_reve(prompt, num_images=1)
391
+ if images and isinstance(images[0], Image.Image):
392
+ return f"✅ Imagen generada con prompt: {prompt[:100]}..."
393
+ return "❌ Error generando imagen"
394
+
395
+ elif "prompt" in user_msg_lower:
396
+ return assistant.generate_prompt(user_msg)
397
+
398
+ elif "analyze" in user_msg_lower:
399
+ return assistant.analyze_image("placeholder_path")
400
+
401
+ # Respuesta conversacional normal
402
+ response = assistant.speak(user_msg, self)
403
+
404
+ # Guardar en historial
405
+ self.history.append({
406
+ "user": user_msg,
407
+ "assistant": response,
408
+ "date": format_timestamp(),
409
+ "assistant_used": self.current_assistant
410
+ })
411
+
412
+ save_json("data/history.json", self.history)
413
+ return response
414
 
415
+ def create_project(self, name: str) -> str:
416
+ """Crea un nuevo proyecto"""
417
+ if name not in self.projects:
418
+ self.projects[name] = {
419
+ "created": format_timestamp(),
420
+ "assets": [],
421
+ "description": ""
422
+ }
423
+ save_json("data/projects.json", self.projects)
424
+ return f"✅ Proyecto '{name}' creado"
425
+ return "⚠️ El proyecto ya existe"
426
+
427
+ def add_asset(self, project: str, asset_type: str, content: str) -> str:
428
+ """Añade un activo a un proyecto"""
429
+ if project in self.projects:
430
+ self.projects[project]["assets"].append({
431
+ "type": asset_type,
432
+ "content": content,
433
+ "date": format_timestamp()
434
+ })
435
+ save_json("data/projects.json", self.projects)
436
+ return f"✅ Activo añadido a '{project}'"
437
+ return "❌ Proyecto no encontrado"
438
+
439
+ def export_project(self, project: str) -> str:
440
+ """Exporta un proyecto como JSON"""
441
+ if project in self.projects:
442
+ return json.dumps(self.projects[project], indent=2, ensure_ascii=False)
443
+ return "{}"
444
+ # ============================================
445
+ # INTERFAZ GRADIO
446
+ # ============================================
447
+ def create_interface():
448
+ """Crea la interfaz de Gradio"""
449
+ bot = SuperBot()
450
+ assistants_list = list(bot.assistants.keys())
451
+
452
+ with gr.Blocks(theme=gr.themes.Soft(), title="BATUTO-ART v5.0") as app:
453
+ gr.Markdown("# 🎨 SUPER MULTI-ASISTENTE BATUTO-ART v5.0\n*For BATUTO only*")
454
+
455
+ # Selector de asistente
456
+ assistant_dropdown = gr.Dropdown(
457
+ choices=assistants_list,
458
  value="sara",
459
+ label="👤 Select Assistant",
460
+ interactive=True
461
  )
462
+
463
+ assistant_dropdown.change(
464
+ fn=bot.set_assistant,
465
+ inputs=[assistant_dropdown]
466
+ )
467
+
468
+ # Tabs principales
469
+ with gr.Tabs():
470
+ # Tab: Chat
471
+ with gr.Tab("💬 Chat"):
472
+ chatbot = gr.Chatbot(height=500, label="Conversación")
473
+ msg_input = gr.Textbox(
474
+ placeholder="Talk to me, BATUTO...",
475
+ label="Mensaje",
476
+ show_label=False
477
+ )
478
+
479
+ def chat_response(message, chat_history):
480
+ response = bot.chat(message)
481
+ chat_history.append((message, response))
482
+ return chat_history, ""
483
+
484
+ msg_input.submit(
485
+ fn=chat_response,
486
+ inputs=[msg_input, chatbot],
487
+ outputs=[chatbot, msg_input]
488
+ )
489
+
490
+ # Tab: Prompt Engine
491
+ with gr.Tab("🎨 Prompt Engine"):
492
+ with gr.Row():
493
+ subject_input = gr.Textbox(
494
+ label="Subject",
495
+ placeholder="Describe the model or scene..."
496
+ )
497
+ mode_radio = gr.Radio(
498
+ choices=["soft", "intense", "editorial"],
499
+ value="intense",
500
+ label="Mode"
501
+ )
502
+
503
+ prompt_output = gr.Textbox(
504
+ label="Generated Prompt",
505
+ lines=12,
506
+ show_copy_button=True,
507
+ interactive=False
508
+ )
509
+
510
+ def generate_prompt(subject, mode):
511
+ assistant = bot.assistants[bot.current_assistant]
512
+ return assistant.generate_prompt(subject, mode)
513
+
514
+ gr.Button("Generate Prompt", variant="primary").click(
515
+ fn=generate_prompt,
516
+ inputs=[subject_input, mode_radio],
517
+ outputs=prompt_output
518
+ )
519
+
520
+ # Tab: Image Studio
521
+ with gr.Tab("🖼️ Image Studio"):
522
+ with gr.Row():
523
+ with gr.Column(scale=2):
524
+ prompt_input = gr.Textbox(
525
+ label="Prompt",
526
+ lines=4,
527
+ placeholder="Describe the image in detail..."
528
+ )
529
+ with gr.Row():
530
+ ratio_select = gr.Dropdown(
531
+ choices=["1:1", "9:16", "16:9", "3:4", "4:3"],
532
+ value="9:16",
533
+ label="Aspect Ratio"
534
+ )
535
+ num_images_slider = gr.Slider(
536
+ minimum=1,
537
+ maximum=4,
538
+ value=1,
539
+ step=1,
540
+ label="Number of Images"
541
+ )
542
+
543
+ generate_btn = gr.Button("Generate Images", variant="primary")
544
+
545
+ with gr.Column(scale=3):
546
+ gallery_output = gr.Gallery(
547
+ label="Generated Images",
548
+ columns=2,
549
+ height=400,
550
+ object_fit="contain"
551
+ )
552
+
553
+ def generate_images(prompt, num_images, ratio):
554
+ if not prompt.strip():
555
+ return [], "❌ Please enter a prompt"
556
+
557
+ images = bot.call_reve(
558
+ prompt=prompt,
559
+ num_images=int(num_images),
560
+ ratio=ratio
561
+ )
562
+
563
+ if images and isinstance(images[0], Image.Image):
564
+ return images, f"✅ Generated {len(images)} images"
565
+ return [], "❌ Error generating images"
566
+
567
+ status_text = gr.Markdown()
568
+
569
+ generate_btn.click(
570
+ fn=generate_images,
571
+ inputs=[prompt_input, num_images_slider, ratio_select],
572
+ outputs=[gallery_output, status_text]
573
+ )
574
+
575
+ # Tab: Analyzer
576
+ with gr.Tab("🔍 Analyzer"):
577
+ image_upload = gr.Image(
578
+ type="filepath",
579
+ label="Upload Image"
580
+ )
581
+ analysis_output = gr.Textbox(
582
+ label="Analysis Result",
583
+ lines=6,
584
+ interactive=False
585
+ )
586
+
587
+ gr.Button("Analyze Image").click(
588
+ fn=bot.assistants["maya"].analyze_image,
589
+ inputs=[image_upload],
590
+ outputs=analysis_output
591
+ )
592
+
593
+ # Tab: Projects
594
+ with gr.Tab("📁 Projects"):
595
+ with gr.Row():
596
+ with gr.Column(scale=1):
597
+ project_name = gr.Textbox(label="Project Name")
598
+ create_project_btn = gr.Button("Create Project", variant="primary")
599
+ project_status = gr.Textbox(label="Status", interactive=False)
600
+
601
+ gr.Markdown("---")
602
+
603
+ asset_type = gr.Radio(
604
+ choices=["prompt", "image", "note"],
605
+ value="prompt",
606
+ label="Asset Type"
607
+ )
608
+ asset_content = gr.Textbox(
609
+ label="Content/URL",
610
+ lines=3
611
+ )
612
+ add_asset_btn = gr.Button("Add Asset")
613
+
614
+ with gr.Column(scale=2):
615
+ export_output = gr.Textbox(
616
+ label="Project JSON",
617
+ lines=12,
618
+ interactive=False
619
+ )
620
+ gr.Button("Export Project").click(
621
+ fn=bot.export_project,
622
+ inputs=[project_name],
623
+ outputs=export_output
624
+ )
625
+
626
+ create_project_btn.click(
627
+ fn=bot.create_project,
628
+ inputs=[project_name],
629
+ outputs=project_status
630
+ )
631
+
632
+ add_asset_btn.click(
633
+ fn=bot.add_asset,
634
+ inputs=[project_name, asset_type, asset_content],
635
+ outputs=project_status
636
+ )
637
+
638
+ # Tab: Vault
639
+ with gr.Tab("📦 Vault"):
640
+ gallery_data = load_json("data/gallery.json", [])
641
+ gallery_images = []
642
+
643
+ for item in gallery_data[-20:]: # Últimas 20 imágenes
644
+ if "path" in item and os.path.exists(item["path"]):
645
+ gallery_images.append(item["path"])
646
+
647
+ gr.Gallery(
648
+ value=gallery_images,
649
+ label="Gallery History",
650
+ columns=4,
651
+ height=500,
652
+ object_fit="cover"
653
+ )
654
+
655
+ return app
656
 
657
+ # ============================================
658
+ # EJECUCIÓN PRINCIPAL
659
+ # ============================================
660
+ if __name__ == "__main__":
661
+ # Verificar variables de entorno
662
+ if not REVE_API_KEY:
663
+ print("⚠️ Advertencia: REVE_API_KEY no está configurada")
664
+
665
+ if not SAMBANOVA_API_KEY:
666
+ print("⚠️ Advertencia: SAMBANOVA_API_KEY no está configurada")
667
+
668
+ # Crear y lanzar la aplicación
669
+ app = create_interface()
670
+ app.launch(
671
+ server_name="0.0.0.0",
672
+ server_port=7860,
673
+ share=False,
674
+ debug=False,
675
+ show_error=True
676
+ )