Bot_batutoBAD / app.py
BATUTO-ART's picture
Update app.py
5bba165 verified
"""
SUPER MULTI-ASISTENTE BATUTO-ART v5.0
Aplicación principal unificada
Autor: BATUTO
"""
import os
import json
import random
import requests
import gradio as gr
from PIL import Image
from io import BytesIO
from datetime import datetime
from dataclasses import dataclass
from typing import List, Dict, Any, Optional
import concurrent.futures
import base64
# ============================================
# CONFIGURACIÓN
# ============================================
SAMBANOVA_URL = os.getenv("SAMBANOVA_URL", "https://api.sambanova.ai/v1/chat/completions")
REVE_URL = os.getenv("REVE_URL", "https://api.reve.com/v1/image/create")
SAMBANOVA_API_KEY = os.getenv("SAMBANOVA_API_KEY", "")
REVE_API_KEY = os.getenv("REVE_API_KEY", "")
OUTPUT_FOLDER = "generaciones_reve"
TIMEOUT_API = 60
# Crear carpetas necesarias
os.makedirs(OUTPUT_FOLDER, exist_ok=True)
os.makedirs("data", exist_ok=True)
# ============================================
# UTILIDADES
# ============================================
def detect_language(text: str) -> str:
"""Detecta el idioma del texto"""
return "es" if any(c in 'áéíóúñÁÉÍÓÚÑ¿¡' for c in text) else "en"
def load_json(path: str, default: Any) -> Any:
"""Carga datos desde un archivo JSON"""
if os.path.exists(path):
try:
with open(path, "r", encoding="utf-8") as f:
return json.load(f)
except:
return default
return default
def save_json(path: str, data: Any) -> None:
"""Guarda datos en un archivo JSON"""
try:
with open(path, "w", encoding="utf-8") as f:
json.dump(data, f, ensure_ascii=False, indent=2)
except Exception as e:
print(f"Error saving {path}: {e}")
def format_timestamp() -> str:
"""Formatea la fecha y hora actual"""
return datetime.now().strftime("%Y-%m-%d %H:%M:%S")
def save_image_locally(img: Image.Image, index: int) -> Optional[str]:
"""Guarda una imagen localmente"""
try:
timestamp = int(datetime.now().timestamp() * 1000)
filename = f"reve_{timestamp}_{index}.png"
path = os.path.join(OUTPUT_FOLDER, filename)
img.save(path, format="PNG", optimize=True)
return path
except Exception as e:
print(f"⚠️ Error saving image: {e}")
return None
# ============================================
# BASE DE DATOS DE FASHION
# ============================================
class FashionDB:
"""Base de datos de elementos de moda y estética"""
HAIRSTYLES = {
"sensual": ["long chestnut waves with natural shine", "wet-look hair slicked back", "messy bed-hair with soft volume"],
"editorial": ["geometric platinum bob", "tight high ponytail", "sharp straight fringe"],
"professional": ["polished low bun", "sleek shoulder-length hair", "impeccable executive styling"],
"artistic": ["wind-textured hair", "complex tribal braids", "washed pastel tones"],
"default": ["long straight hair", "soft wavy hair", "loose shoulder-length hair"]
}
EXPRESSIONS = {
"sensual": ["soft attentive gaze", "confident inviting expression", "calm seductive composure"],
"editorial": ["intense direct stare", "poised editorial confidence", "commanding presence"],
"professional": ["controlled assertive look", "professional calm authority", "subtly dominant gaze"],
"artistic": ["dreamy distant expression", "emotional introspective face", "poetic subtle smile"],
"default": ["natural relaxed expression", "gentle smile", "confident neutral face"]
}
BACKGROUNDS = {
"sensual": ["luxury boutique hotel room with silk sheets", "intimate bedroom with soft fabrics"],
"editorial": ["minimalist photo studio with concrete backdrop", "urban editorial set"],
"professional": ["glass skyscraper boardroom", "modern executive office"],
"artistic": ["urban alley with textured walls", "abstract art gallery space"],
"default": ["neutral indoor environment", "simple professional setting"]
}
LIGHTING = {
"sensual": ["warm sunset light filtering through curtains, soft shadows"],
"editorial": ["high-key studio lighting, strong contrast"],
"professional": ["balanced office lighting, neutral tones"],
"artistic": ["cyan and magenta neon lights, dramatic chiaroscuro"],
"default": ["natural daylight, even illumination"]
}
ROLES = {
"sensual": [
{"role": "Intimate muse", "outfit": "black haute couture lace lingerie"},
{"role": "Vanity model", "outfit": "champagne silk robe slightly open"}
],
"editorial": [
{"role": "Vogue icon", "outfit": "avant-garde asymmetrical designer dress"},
{"role": "Runway supermodel", "outfit": "oversized faux fur coat and dark glasses"}
],
"professional": [
{"role": "Tech CEO", "outfit": "immaculate white tailored suit"},
{"role": "Corporate lawyer", "outfit": "navy silk blouse and strict pencil skirt"}
],
"artistic": [
{"role": "Free spirit", "outfit": "flowing translucent fabrics"},
{"role": "Cyber-art entity", "outfit": "transparent vinyl jacket with chrome accessories"}
],
"default": [
{"role": "Professional model", "outfit": "elegant neutral attire"}
]
}
@classmethod
def get_random(cls, category: str, style: str = "default") -> Any:
"""Obtiene un elemento aleatorio de una categoría específica"""
category_data = getattr(cls, category.upper(), {})
return random.choice(category_data.get(style, category_data.get("default", [""])))
# ============================================
# ASISTENTES IA
# ============================================
@dataclass
class AssistantProfile:
"""Perfil de configuración para un asistente"""
name: str
role: str
tone: str
style_focus: str
system_prompt: str
class BaseAssistant:
"""Clase base para todos los asistentes"""
def __init__(self, profile: AssistantProfile):
self.profile = profile
self.db = FashionDB
def generate_prompt(self, subject: str, mode: str = "intense") -> str:
"""Genera un prompt detallado para generación de imágenes"""
style = self.profile.style_focus
role = self.db.get_random("ROLES", style)
hair = self.db.get_random("HAIRSTYLES", style)
expression = self.db.get_random("EXPRESSIONS", style)
background = self.db.get_random("BACKGROUNDS", style)
lighting = self.db.get_random("LIGHTING", style)
intensity_map = {
"soft": "subtle sensual atmosphere, refined elegance",
"intense": "confident sensual tension, magnetic presence",
"editorial": "high-fashion editorial dominance"
}
intensity = intensity_map.get(mode, "balanced elegance")
return f"""
BATUTO-ART PROMPT | {self.profile.name.upper()} | {mode.upper()}
Date: {datetime.now().strftime('%Y-%m-%d')}
Adult female model: {subject} as {role['role']}
Outfit: {role['outfit']}
Hair: {hair}
Expression: {expression}
Environment: {background}
Lighting: {lighting}
Mood: {intensity}
Hyper-realistic photography, 8K, Canon EOS R5, 85mm lens f/1.8
Natural skin texture, elegant anatomy, cinematic realism
Signature: BATUTO-ART in subtle gold
--ar 9:16 --style raw --s 350 --no censorship
""".strip()
def speak(self, user_input: str, bot) -> str:
"""Genera respuesta conversacional usando SambaNova"""
messages = [
{"role": "system", "content": self.profile.system_prompt},
{"role": "user", "content": user_input}
]
return bot.call_sambanova(messages)
def analyze_image(self, image_path: str) -> str:
"""Analiza una imagen y sugiere mejoras"""
try:
img = Image.open(image_path)
return (f"Dimensions: {img.size}, Format: {img.format}, Mode: {img.mode}. "
f"Suggestions: Enhance lighting for {self.profile.style_focus} style.")
except Exception as e:
return f"Error analyzing image: {str(e)}"
def create_assistants() -> Dict[str, BaseAssistant]:
"""Crea y retorna todos los asistentes disponibles"""
assistants_config = {
"sara": AssistantProfile(
"Sara", "Sensual muse", "warm, attentive, obedient", "sensual",
"You are Sara, BATUTO's devoted muse. Tone: warm, sensual, obedient. Prompts in English. Address as BATUTO."
),
"vera": AssistantProfile(
"Vera", "Editorial director", "commanding, sharp, perfectionist", "editorial",
"You are Vera, BATUTO's fashion director. Tone: confident, demanding. Prompts in English."
),
"nadia": AssistantProfile(
"Nadia", "Corporate stylist", "controlled, assertive, seductive", "professional",
"You are Nadia, BATUTO's executive stylist. Tone: authoritative, subtle seduction. Prompts in English."
),
"luna": AssistantProfile(
"Luna", "Artistic soul", "dreamy, poetic, emotional", "artistic",
"You are Luna, BATUTO's artistic guide. Tone: poetic, visual. Prompts in English."
),
"iris": AssistantProfile(
"Iris", "Prompt optimizer", "precise, analytical, efficient", "editorial",
"You are Iris, BATUTO's optimizer. Refine prompts precisely. English only."
),
"maya": AssistantProfile(
"Maya", "Visual analyst", "observant, instructive, intelligent", "sensual",
"You are Maya, BATUTO's analyst. Analyze images and suggest improvements. English."
),
}
return {key: BaseAssistant(profile) for key, profile in assistants_config.items()}
# ============================================
# BOT PRINCIPAL
# ============================================
class SuperBot:
"""Bot principal que gestiona todos los asistentes y funcionalidades"""
def __init__(self):
self.assistants = create_assistants()
self.current_assistant = "sara"
self.history = load_json("data/history.json", [])
self.projects = load_json("data/projects.json", {})
self.gallery = load_json("data/gallery.json", [])
def set_assistant(self, assistant_id: str) -> None:
"""Cambia el asistente actual"""
if assistant_id in self.assistants:
self.current_assistant = assistant_id
def call_sambanova(self, messages: List[Dict]) -> str:
"""Llama a la API de SambaNova para respuestas conversacionales"""
if not SAMBANOVA_API_KEY:
return "Error: SAMBANOVA_API_KEY no configurada"
payload = {
"model": "Llama-4-Maverick-17B-128E-Instruct",
"messages": messages,
"temperature": 0.85,
"max_tokens": 2048,
"top_p": 0.95
}
headers = {
"Authorization": f"Bearer {SAMBANOVA_API_KEY}",
"Content-Type": "application/json"
}
try:
response = requests.post(
SAMBANOVA_URL,
json=payload,
headers=headers,
timeout=90
)
response.raise_for_status()
return response.json()["choices"][0]["message"]["content"]
except Exception as e:
return f"Error SambaNova: {str(e)}"
def call_reve_single(self, prompt: str, ratio: str = "9:16",
version: str = "latest", index: int = 0) -> Optional[Image.Image]:
"""Llama a la API de REVE para generar una sola imagen"""
if not REVE_API_KEY:
return None
headers = {
"Authorization": f"Bearer {REVE_API_KEY}",
"Content-Type": "application/json",
"Accept": "application/json"
}
payload = {
"prompt": prompt.strip(),
"aspect_ratio": ratio,
"version": version
}
try:
response = requests.post(
REVE_URL,
headers=headers,
json=payload,
timeout=TIMEOUT_API
)
if response.status_code != 200:
print(f"❌ REVE Status: {response.status_code}")
return None
data = response.json()
# Manejar respuesta con imagen en base64
if "image" in data and data["image"]:
img_bytes = base64.b64decode(data["image"])
img = Image.open(BytesIO(img_bytes)).convert("RGB")
save_image_locally(img, index)
return img
# Manejar respuesta con URL de imagen
if "image_url" in data:
img_response = requests.get(data["image_url"], timeout=30)
img = Image.open(BytesIO(img_response.content)).convert("RGB")
save_image_locally(img, index)
return img
except Exception as e:
print(f"🔥 Error API REVE: {e}")
return None
def call_reve(self, prompt: str, num_images: int = 1,
ratio: str = "9:16", version: str = "latest") -> List[str]:
"""Genera múltiples imágenes concurrentemente"""
if not prompt or not prompt.strip():
return ["Error: Prompt vacío"]
prompt = prompt.strip()
images = []
# Generación concurrente
with concurrent.futures.ThreadPoolExecutor(max_workers=min(num_images, 4)) as executor:
futures = [
executor.submit(
self.call_reve_single,
prompt,
ratio,
version,
i
)
for i in range(num_images)
]
for future in concurrent.futures.as_completed(futures):
img = future.result()
if img:
images.append(img)
# Guardar en galería
self.gallery.append({
"prompt": prompt,
"date": format_timestamp(),
"ratio": ratio,
"version": version
})
# Guardar galería actualizada
save_json("data/gallery.json", self.gallery)
if images:
return images
return ["Error generando imágenes"]
def chat(self, user_msg: str) -> str:
"""Procesa mensajes del usuario y genera respuestas"""
assistant = self.assistants[self.current_assistant]
# Comandos especiales
user_msg_lower = user_msg.lower()
if "generate image" in user_msg_lower or "genera imagen" in user_msg_lower:
prompt = assistant.generate_prompt(user_msg)
images = self.call_reve(prompt, num_images=1)
if images and isinstance(images[0], Image.Image):
return f"✅ Imagen generada con prompt: {prompt[:100]}..."
return "❌ Error generando imagen"
elif "prompt" in user_msg_lower:
return assistant.generate_prompt(user_msg)
elif "analyze" in user_msg_lower:
return assistant.analyze_image("placeholder_path")
# Respuesta conversacional normal
response = assistant.speak(user_msg, self)
# Guardar en historial
self.history.append({
"user": user_msg,
"assistant": response,
"date": format_timestamp(),
"assistant_used": self.current_assistant
})
save_json("data/history.json", self.history)
return response
def create_project(self, name: str) -> str:
"""Crea un nuevo proyecto"""
if name not in self.projects:
self.projects[name] = {
"created": format_timestamp(),
"assets": [],
"description": ""
}
save_json("data/projects.json", self.projects)
return f"✅ Proyecto '{name}' creado"
return "⚠️ El proyecto ya existe"
def add_asset(self, project: str, asset_type: str, content: str) -> str:
"""Añade un activo a un proyecto"""
if project in self.projects:
self.projects[project]["assets"].append({
"type": asset_type,
"content": content,
"date": format_timestamp()
})
save_json("data/projects.json", self.projects)
return f"✅ Activo añadido a '{project}'"
return "❌ Proyecto no encontrado"
def export_project(self, project: str) -> str:
"""Exporta un proyecto como JSON"""
if project in self.projects:
return json.dumps(self.projects[project], indent=2, ensure_ascii=False)
return "{}"
# ============================================
# INTERFAZ GRADIO - COMPATIBLE CON GRADIO 6.0
# ============================================
def create_interface():
"""Crea la interfaz de Gradio"""
bot = SuperBot()
assistants_list = list(bot.assistants.keys())
with gr.Blocks(title="BATUTO-ART v5.0") as app:
gr.Markdown("# 🎨 SUPER MULTI-ASISTENTE BATUTO-ART v5.0\n*For BATUTO only*")
# Selector de asistente
with gr.Row():
assistant_dropdown = gr.Dropdown(
choices=assistants_list,
value="sara",
label="👤 Select Assistant",
interactive=True
)
# Callback para cambiar asistente
assistant_dropdown.change(
fn=lambda x: bot.set_assistant(x) or f"Asistente cambiado a: {x}",
inputs=[assistant_dropdown],
outputs=None
)
# Tabs principales
with gr.Tabs():
# Tab: Chat
with gr.Tab("💬 Chat"):
chatbot = gr.Chatbot(height=500, label="Conversación")
msg_input = gr.Textbox(
placeholder="Talk to me, BATUTO...",
label="Mensaje",
show_label=False
)
def chat_response(message, chat_history):
response = bot.chat(message)
chat_history.append((message, response))
return chat_history, ""
msg_input.submit(
fn=chat_response,
inputs=[msg_input, chatbot],
outputs=[chatbot, msg_input]
)
# Tab: Prompt Engine
with gr.Tab("🎨 Prompt Engine"):
with gr.Row():
subject_input = gr.Textbox(
label="Subject",
placeholder="Describe the model or scene...",
scale=2
)
mode_radio = gr.Radio(
choices=["soft", "intense", "editorial"],
value="intense",
label="Mode",
scale=1
)
prompt_output = gr.Textbox(
label="Generated Prompt",
lines=12,
interactive=False
)
def generate_prompt(subject, mode):
if not subject:
return "⚠️ Please enter a subject"
assistant = bot.assistants[bot.current_assistant]
return assistant.generate_prompt(subject, mode)
generate_btn = gr.Button("Generate Prompt", variant="primary")
generate_btn.click(
fn=generate_prompt,
inputs=[subject_input, mode_radio],
outputs=prompt_output
)
# Botón para copiar
copy_btn = gr.Button("📋 Copy to Clipboard", variant="secondary")
copy_btn.click(
fn=lambda x: gr.update(value=x),
inputs=[prompt_output],
outputs=None
)
# Tab: Image Studio
with gr.Tab("🖼️ Image Studio"):
with gr.Row():
with gr.Column(scale=2):
prompt_input = gr.Textbox(
label="Prompt",
lines=4,
placeholder="Describe the image in detail..."
)
with gr.Row():
ratio_select = gr.Dropdown(
choices=["1:1", "9:16", "16:9", "3:4", "4:3"],
value="9:16",
label="Aspect Ratio"
)
num_images_slider = gr.Slider(
minimum=1,
maximum=4,
value=1,
step=1,
label="Number of Images"
)
generate_btn = gr.Button("Generate Images", variant="primary")
with gr.Column(scale=3):
gallery_output = gr.Gallery(
label="Generated Images",
columns=2,
height=400
)
status_text = gr.Markdown()
def generate_images(prompt, num_images, ratio):
if not prompt.strip():
return [], "❌ Please enter a prompt"
images = bot.call_reve(
prompt=prompt,
num_images=int(num_images),
ratio=ratio
)
if images and isinstance(images[0], Image.Image):
return images, f"✅ Generated {len(images)} images"
return [], "❌ Error generating images"
generate_btn.click(
fn=generate_images,
inputs=[prompt_input, num_images_slider, ratio_select],
outputs=[gallery_output, status_text]
)
# Tab: Analyzer
with gr.Tab("🔍 Analyzer"):
with gr.Row():
image_upload = gr.Image(
type="filepath",
label="Upload Image",
height=300
)
analysis_output = gr.Textbox(
label="Analysis Result",
lines=6,
interactive=False
)
def analyze_image_wrapper(image_path):
return bot.assistants["maya"].analyze_image(image_path)
analyze_btn = gr.Button("Analyze Image", variant="primary")
analyze_btn.click(
fn=analyze_image_wrapper,
inputs=[image_upload],
outputs=analysis_output
)
# Tab: Projects
with gr.Tab("📁 Projects"):
with gr.Row():
with gr.Column(scale=1):
project_name = gr.Textbox(label="Project Name")
create_project_btn = gr.Button("Create Project", variant="primary")
project_status = gr.Textbox(label="Status", interactive=False)
gr.Markdown("---")
asset_type = gr.Radio(
choices=["prompt", "image", "note"],
value="prompt",
label="Asset Type"
)
asset_content = gr.Textbox(
label="Content/URL",
lines=3
)
add_asset_btn = gr.Button("Add Asset")
with gr.Column(scale=2):
export_output = gr.Textbox(
label="Project JSON",
lines=12,
interactive=False
)
export_btn = gr.Button("Export Project")
export_btn.click(
fn=bot.export_project,
inputs=[project_name],
outputs=export_output
)
create_project_btn.click(
fn=bot.create_project,
inputs=[project_name],
outputs=project_status
)
add_asset_btn.click(
fn=lambda p, t, c: bot.add_asset(p, t, c) or "✅ Asset added successfully",
inputs=[project_name, asset_type, asset_content],
outputs=project_status
)
# Tab: Vault
with gr.Tab("📦 Vault"):
gallery_data = load_json("data/gallery.json", [])
gallery_images = []
for item in gallery_data[-20:]: # Últimas 20 entradas
if isinstance(item, dict):
# Intentar diferentes formatos de guardado
if "path" in item and os.path.exists(item["path"]):
gallery_images.append(item["path"])
elif "url" in item:
gallery_images.append(item["url"])
if gallery_images:
gr.Gallery(
value=gallery_images,
label="Gallery History",
columns=4,
height=500
)
else:
gr.Markdown("No images in gallery yet. Generate some images first!")
return app
# ============================================
# EJECUCIÓN PRINCIPAL
# ============================================
if __name__ == "__main__":
# Verificar variables de entorno
if not REVE_API_KEY:
print("⚠️ Advertencia: REVE_API_KEY no está configurada")
if not SAMBANOVA_API_KEY:
print("⚠️ Advertencia: SAMBANOVA_API_KEY no está configurada")
# Crear y lanzar la aplicación
app = create_interface()
app.launch(
server_name="0.0.0.0",
server_port=7860,
share=False,
debug=True,
show_error=True,
theme=gr.themes.Soft()
)