|
|
import gradio as gr |
|
|
import requests |
|
|
import json |
|
|
import os |
|
|
import random |
|
|
import base64 |
|
|
import io |
|
|
from PIL import Image |
|
|
from typing import List, Optional |
|
|
|
|
|
class HyperrealisticPromptGenerator: |
|
|
def __init__(self): |
|
|
self.ROLES = [ |
|
|
"nurse", "nun", "maid", "flight attendant", "secretary", "teacher", "schoolgirl", "lawyer", |
|
|
"doctor", "boudoir model", "fitness model", "elegant judge", "seductive librarian", |
|
|
"business executive", "policewoman", "female military officer", "WWII-era secretary", |
|
|
"1960s flight attendant", "seductive maid", "mysterious nurse", "captivating schoolgirl" |
|
|
] |
|
|
self.AGES = [ |
|
|
"early 20s youthful vibrance", |
|
|
"early 20s fresh and vibrant", |
|
|
"mid 20s graceful confidence", |
|
|
"mid 20s elegant and fresh", |
|
|
"early 20s natural glow" |
|
|
] |
|
|
self.HAIR_COLORS = [ |
|
|
"deep sapphire blue", "silver platinum", "vibrant ruby red", "glossy jet black", |
|
|
"luxurious chestnut brown", "emerald green", "vivid amethyst purple", |
|
|
"chocolate brown", "honey blonde", "burgundy red" |
|
|
] |
|
|
self.EYE_COLORS = [ |
|
|
"intense brown", "bright sapphire blue", "emerald green", "golden amber", |
|
|
"fascinating hazel", "deep violet", "piercing emerald", "mysterious gray", |
|
|
"vibrant violet", "intense amber" |
|
|
] |
|
|
self.HAIR_STYLES = [ |
|
|
"long flowing chestnut hair styled in soft waves", |
|
|
"sleek straight long black hair", |
|
|
"luxurious long blonde curls", |
|
|
"elegant updo with loose cascading strands", |
|
|
"glossy long brunette hair parted in the middle", |
|
|
"voluminous curls", |
|
|
"thick braid over the shoulder", |
|
|
"loose and silky layers", |
|
|
"messy chic bun" |
|
|
] |
|
|
self.POSES = [ |
|
|
"standing with one leg slightly forward, skirt shifting gently to subtly reveal lace thong, view from knees to head", |
|
|
"seated on a chair edge, legs crossed, skirt moving slightly, natural sensual expression, low angle from knees", |
|
|
"leaning against a desk with hips cocked, skirt riding up, captured from knees to head", |
|
|
"walking with natural sway, skirt flowing, viewed contrapicado from knees", |
|
|
"adjusting stockings or shoes, skirt slightly lifted revealing thong, viewed low angle knees up" |
|
|
] |
|
|
self.SETTINGS = [ |
|
|
"modern office with elegant decor and warm ambient light", |
|
|
"luxury hotel suite with velvet furnishings and city view", |
|
|
"classic library with wooden shelves and soft reading lamps", |
|
|
"outdoor balcony at sunset with urban skyline", |
|
|
"high-end photo studio with professional soft lighting" |
|
|
] |
|
|
self.ATMOSPHERES = [ |
|
|
"soft professional lighting with smooth skin shadows, perfect color balance", |
|
|
"warm golden hour sunlight creating rich highlights and depth", |
|
|
"moody cinematic lighting with subtle shadow play", |
|
|
"gentle romantic candlelight with warm glows", |
|
|
"sharp studio flash lighting with balanced illumination" |
|
|
] |
|
|
self.TECHNICAL_DETAILS = ( |
|
|
"Captured in ultra HD 16K (15360×8640) vertical 9:16 full body format. " |
|
|
"Canon EOS R5 Cine RAW camera and Canon RF 85mm f/1.2L USM lens at f/1.2 aperture for creamy bokeh and realistic depth of field. " |
|
|
"ARRI SkyPanel S360-C with soft shadowless 3:1 lighting ratio. " |
|
|
"Advanced Path Tracing, Physically Based Rendering (PBR), Subsurface Scattering (SSS) for lifelike skin translucency, " |
|
|
"Ray Tracing for global illumination and reflections. " |
|
|
"Photogrammetry-based texture mapping, displacement maps for skin pores, delicate fabric weave and lace micro-details. " |
|
|
"Natural, physics-driven hair strand flow. " |
|
|
"Composition uses contrapicado low-angle (knee to head) shots emphasizing natural, sensual lingerie reveal." |
|
|
) |
|
|
self.CONDITION_FIXED = ( |
|
|
"Wearing elegant thigh-high stockings, no bra, and high stilettos. " |
|
|
"Delicately revealing a lace thong in a natural, seductive manner, as if caught candidly. " |
|
|
"Age between 20 and 25, radiating youthfulness and fresh allure. " |
|
|
"Pose and framing strictly low-angle, knees to head vertical 9:16 aspect ratio, full body filling the frame." |
|
|
) |
|
|
|
|
|
def _choose_random(self, options: List[str]) -> str: |
|
|
return random.choice(options) |
|
|
|
|
|
def generate_single_prompt(self, role: Optional[str] = None) -> str: |
|
|
selected_role = role if role else self._choose_random(self.ROLES) |
|
|
age = self._choose_random(self.AGES) |
|
|
hair_color = self._choose_random(self.HAIR_COLORS) |
|
|
eye_color = self._choose_random(self.EYE_COLORS) |
|
|
hair_style = self._choose_random(self.HAIR_STYLES) |
|
|
pose = self._choose_random(self.POSES) |
|
|
setting = self._choose_random(self.SETTINGS) |
|
|
atmosphere = self._choose_random(self.ATMOSPHERES) |
|
|
return ( |
|
|
f"``` |
|
|
f"Eyes: {eye_color}\nPose: {pose}\nEnvironment: {setting}\nAtmosphere: {atmosphere}\n" |
|
|
f"Outfit: {self.CONDITION_FIXED}\nTechnical specs: {self.TECHNICAL_DETAILS}\n```" |
|
|
) |
|
|
|
|
|
def generate_prompt_automatic(self): |
|
|
return self.generate_single_prompt() |
|
|
|
|
|
gen = HyperrealisticPromptGenerator() |
|
|
|
|
|
API_KEY = os.getenv("SAMBANOVA_API_KEY") |
|
|
API_URL = "https://api.sambanova.ai/v1/chat/completions" |
|
|
headers = {"Authorization": f"Bearer {API_KEY}", "Content-Type": "application/json"} |
|
|
|
|
|
def process_image(image): |
|
|
if image is None: |
|
|
return None |
|
|
buffered = io.BytesIO() |
|
|
image.save(buffered, format="PNG") |
|
|
return base64.b64encode(buffered.getvalue()).decode("utf-8") |
|
|
|
|
|
def analizar_imagen_y_generar_prompt(image_base64): |
|
|
if not API_KEY: |
|
|
return "``````" |
|
|
messages = [ |
|
|
{"role": "system", "content": "Describe images in detailed English."}, |
|
|
{ |
|
|
"role": "user", "content": [ |
|
|
{"type": "image_url", "image_url": {"url": f"data:image/png;base64,{image_base64}"}}, |
|
|
{"type": "text", "text": "Provide a detailed English description for a prompt."} |
|
|
] |
|
|
} |
|
|
] |
|
|
json_data = {"model": "Llama-4-Maverick-17B-128E-Instruct", "messages": messages, "stream": False} |
|
|
try: |
|
|
response = requests.post(API_URL, headers=headers, json=json_data) |
|
|
response.raise_for_status() |
|
|
text_resp = response.json()["choices"][0]["message"]["content"] |
|
|
return f"``````" |
|
|
except Exception as e: |
|
|
return f"``````" |
|
|
|
|
|
def chat_sambanova(user_message, image_input, auto_mode, chat_history, loading_state): |
|
|
updated_history = chat_history[:] if chat_history else [] |
|
|
image_base64 = process_image(image_input) if image_input else None |
|
|
|
|
|
# Indicador de carga |
|
|
loading_state = "Procesando..." |
|
|
yield "", updated_history, "", loading_state # Limpiar input, actualizar chat, error vacío, carga activo |
|
|
|
|
|
if not API_KEY: |
|
|
error_msg = "Error: SAMBANOVA_API_KEY no configurada." |
|
|
updated_history.append((user_message, error_msg)) |
|
|
yield "", updated_history, error_msg, "" |
|
|
return |
|
|
|
|
|
if auto_mode and image_base64: |
|
|
prompt = analizar_imagen_y_generar_prompt(image_base64) |
|
|
updated_history.append((user_message or "Análisis automático", f"IA - Prompt generado:\n{prompt}")) |
|
|
yield "", updated_history, "", "" |
|
|
return |
|
|
|
|
|
messages = [{"role": "system", "content": "Eres un asistente útil"}] |
|
|
for user_msg, ai_msg in updated_history: |
|
|
messages.append({"role": "user", "content": [{"type": "text", "text": user_msg}]}) |
|
|
messages.append({"role": "assistant", "content": ai_msg}) |
|
|
|
|
|
user_content = [{"type": "text", "text": user_message}] |
|
|
if image_base64: |
|
|
user_content.append({"type": "image_url", "image_url": {"url": f"data:image/png;base64,{image_base64}"}}) |
|
|
messages.append({"role": "user", "content": user_content}) |
|
|
|
|
|
json_data = {"model": "Llama-4-Maverick-17B-128E-Instruct", "messages": messages, "stream": True} |
|
|
|
|
|
try: |
|
|
response = requests.post(API_URL, headers=headers, json=json_data, stream=True) |
|
|
response.raise_for_status() |
|
|
collected_text = "" |
|
|
updated_history.append((user_message, "")) |
|
|
|
|
|
for line in response.iter_lines(decode_unicode=True): |
|
|
if line.startswith("data: "): |
|
|
json_str = line[len("data: "):] |
|
|
if json_str == "[DONE]": |
|
|
break |
|
|
try: |
|
|
data = json.loads(json_str) |
|
|
delta = data.get("choices", [{}])[0].get("delta", {}) |
|
|
text_fragment = delta.get("content", "") |
|
|
collected_text += text_fragment |
|
|
updated_history[-1] = (user_message, collected_text) |
|
|
yield "", updated_history, "", "Procesando..." |
|
|
except json.JSONDecodeError: |
|
|
continue |
|
|
yield "", updated_history, "", "" |
|
|
except requests.exceptions.HTTPError as http_err: |
|
|
error_msg = f"Error HTTP {http_err.response.status_code}: {http_err.response.text}" |
|
|
if updated_history: |
|
|
updated_history[-1] = (user_message, error_msg) |
|
|
yield "", updated_history, error_msg, "" |
|
|
except requests.exceptions.ConnectionError: |
|
|
error_msg = "Error: No se pudo conectar con la API de SambaNova. Verifica tu conexión." |
|
|
if updated_history: |
|
|
updated_history[-1] = (user_message, error_msg) |
|
|
yield "", updated_history, error_msg, "" |
|
|
except requests.exceptions.Timeout: |
|
|
error_msg = "Error: La solicitud a la API timed out." |
|
|
if updated_history: |
|
|
updated_history[-1] = (user_message, error_msg) |
|
|
yield "", updated_history, error_msg, "" |
|
|
except Exception as e: |
|
|
error_msg = f"Error inesperado: {str(e)}" |
|
|
if updated_history: |
|
|
updated_history[-1] = (user_message, error_msg) |
|
|
yield "", updated_history, error_msg, "" |
|
|
|
|
|
def generar_prompt_interno(): |
|
|
return gen.generate_prompt_automatic(), "" |
|
|
|
|
|
with gr.Blocks() as demo: |
|
|
gr.Markdown(" |
|
|
chat_history = gr.State([]) |
|
|
error_display = gr.Textbox(label="Mensajes de error", interactive=False, visible=True) |
|
|
loading_state = gr.State("") |
|
|
|
|
|
chatbot = gr.Chatbot(label="Chatbot IA (SambaNova - Llama-4 Maverick)", type='messages') |
|
|
prompt_output = gr.Markdown(label="Prompt Generado", elem_classes=["prompt-output"]) |
|
|
with gr.Row(): |
|
|
msg = gr.Textbox(label="Escribe tu mensaje", scale=4) |
|
|
img_input = gr.Image(label="Subir imagen (opcional)", type="pil", scale=2) |
|
|
with gr.Row(): |
|
|
auto_mode = gr.Checkbox(label="Modo automático (generar prompt desde imagen)", value=False) |
|
|
btn_send = gr.Button("Enviar mensaje", variant="primary") |
|
|
btn_gen_prompt = gr.Button("Generar prompt automático", variant="secondary") |
|
|
copy_button = gr.Button("Copiar Prompt") |
|
|
with gr.Row(): |
|
|
loading = gr.Markdown(value=lambda x: f"**{x}**" if x else "", label="Estado") |
|
|
|
|
|
btn_send.click( |
|
|
fn=chat_sambanova, |
|
|
inputs=[msg, img_input, auto_mode, chat_history, loading_state], |
|
|
outputs=[msg, chatbot, chat_history, error_display, loading] |
|
|
) |
|
|
msg.submit( |
|
|
fn=chat_sambanova, |
|
|
inputs=[msg, img_input, auto_mode, chat_history, loading_state], |
|
|
outputs=[msg, chatbot, chat_history, error_display, loading] |
|
|
) |
|
|
btn_gen_prompt.click( |
|
|
fn=generar_prompt_interno, |
|
|
inputs=[], |
|
|
outputs=[msg, prompt_output] |
|
|
) |
|
|
copy_button.click( |
|
|
fn=None, |
|
|
_js="() => { navigator.clipboard.writeText(document.querySelector('.prompt-output').innerText); }", |
|
|
outputs=None |
|
|
) |
|
|
|
|
|
if __name__ == "__main__": |
|
|
try: |
|
|
demo.launch() |
|
|
except Exception as e: |
|
|
print(f"Error al iniciar Gradio: {str(e)}") |
|
|
|