Spaces:
Sleeping
Sleeping
File size: 6,727 Bytes
f7ae335 58255ff f7ae335 2015b99 f7ae335 2015b99 58255ff f7ae335 58255ff 2015b99 f7ae335 58255ff f7ae335 2015b99 0c5a164 2015b99 f7ae335 ccc6835 f7ae335 ccc6835 f7ae335 58255ff ccc6835 58255ff f7ae335 58255ff ccc6835 f7ae335 58255ff ccc6835 f7ae335 2015b99 f7ae335 59ca944 f7ae335 59ca944 441171e 2015b99 ccc6835 f7ae335 2015b99 f7ae335 ccc6835 441171e 59ca944 2015b99 ccc6835 59ca944 2015b99 f7ae335 2015b99 59ca944 441171e 2015b99 59ca944 2015b99 441171e | 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 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 | import os
import base64
import tempfile
import gradio as gr
import requests
from hume import HumeClient
from PIL import Image
import whisper
# --- Configuración ---
SAMBA_API_KEY = os.getenv("SAMBA_API_KEY")
HUME_API_KEY = os.getenv("HUME_API_KEY")
if not SAMBA_API_KEY:
raise ValueError("Falta la variable de entorno: SAMBA_API_KEY")
if not HUME_API_KEY:
raise ValueError("Falta la variable de entorno: HUME_API_KEY")
VOICE_ID_DEFAULT = "085fdec7-b201-4a58-b65b-4d321f7abd85"
MODEL_NAME = "Llama-4-Maverick-17B-128E-Instruct"
SAMBA_API_URL = "https://api.sambanova.ai/v1/chat/completions"
# --- Inicializar clientes ---
hume = HumeClient(api_key=HUME_API_KEY)
whisper_model = whisper.load_model("base")
# --- Funciones auxiliares ---
def imagen_pil_a_base64(pil_img):
with tempfile.NamedTemporaryFile(delete=False, suffix=".jpeg") as tmp:
pil_img.save(tmp, format="JPEG")
tmp.seek(0)
img_bytes = tmp.read()
return base64.b64encode(img_bytes).decode("utf-8")
def audio_a_texto(audio_path):
if not audio_path:
return ""
try:
result = whisper_model.transcribe(audio_path, language="es")
return result["text"].strip()
except Exception as e:
print("Error en Whisper STT:", e)
return ""
def generar_respuesta(texto, imagen_pil):
if not texto or not texto.strip():
if imagen_pil is not None:
prompt_usuario = "Describe this image in English in one short paragraph. Only output the description, no other text."
contenido = [{"type": "text", "text": prompt_usuario}]
else:
return "Por favor, escribe un mensaje o envía una imagen."
else:
contenido = [{"type": "text", "text": texto}]
if imagen_pil is not None:
img_b64 = imagen_pil_a_base64(imagen_pil)
contenido.append({
"type": "image_url",
"image_url": {"url": f"data:image/jpeg;base64,{img_b64}"}
})
headers = {
"Authorization": f"Bearer {SAMBA_API_KEY}",
"Content-Type": "application/json"
}
payload = {
"model": MODEL_NAME,
"messages": [{"role": "user", "content": contenido}],
"temperature": 0.1 if (imagen_pil is not None and (not texto or "describe" in texto.lower())) else 0.3,
"top_p": 0.9
}
try:
response = requests.post(SAMBA_API_URL, headers=headers, json=payload, timeout=120)
response.raise_for_status()
return response.json()["choices"][0]["message"]["content"].strip()
except Exception as e:
print("Error en SambaNova API:", e)
return "Lo siento, no pude procesar la solicitud."
def generar_audio(texto, voz_id=VOICE_ID_DEFAULT):
if not texto or not texto.strip():
return None
try:
respuesta = hume.tts.speak(text=texto, voice=voz_id)
audio_bytes = respuesta.audio
if not audio_bytes:
return None
tmp = tempfile.NamedTemporaryFile(delete=False, suffix=".wav")
tmp.write(audio_bytes)
tmp.close()
return tmp.name
except Exception as e:
print("Error en Hume TTS:", e)
return None
def manejar_entrada(texto_input, audio_input, imagen_pil, historial, voz_id):
if historial is None:
historial = []
if texto_input and texto_input.strip():
texto_usuario = texto_input.strip()
elif audio_input:
texto_usuario = audio_a_texto(audio_input)
if not texto_usuario:
return historial, None, "No entendí lo que dijiste. ¿Puedes repetirlo?"
else:
texto_usuario = ""
respuesta = generar_respuesta(texto_usuario, imagen_pil)
historial.append({"role": "user", "content": texto_usuario or "(envió una imagen)"})
historial.append({"role": "assistant", "content": respuesta})
audio_respuesta = generar_audio(respuesta, voz_id)
return historial, audio_respuesta, respuesta
def limpiar_chat():
return [], None, ""
# --- Interfaz Gradio ---
with gr.Blocks(title="Batuto AI: Texto/Voz + Imagen → Voz", theme=gr.themes.Soft()) as demo:
gr.Markdown("# 🧠 Batuto AI: Texto o Voz + Imagen → Respuesta en Voz")
gr.Markdown("Escribe o habla. Sube una imagen. Siempre respondo con voz empática.")
# Agregar script JS para botones de copiar dentro del chat
gr.HTML("""
<script>
const observer = new MutationObserver(() => {
document.querySelectorAll('.message.bot').forEach(msg => {
if (!msg.querySelector('.copy-btn')) {
const button = document.createElement('button');
button.textContent = '📋 Copiar';
button.className = 'copy-btn';
button.style.cssText = 'float:right; margin-left:10px; cursor:pointer; background:#eee; border:none; border-radius:4px; padding:2px 6px;';
button.onclick = () => {
navigator.clipboard.writeText(msg.querySelector('p').innerText);
button.textContent = '✅ Copiado';
setTimeout(()=>button.textContent='📋 Copiar',1500);
};
msg.querySelector('p').appendChild(button);
}
});
});
observer.observe(document.body, { childList: true, subtree: true });
</script>
""")
chat = gr.Chatbot(label="Conversación", height=400, type="messages", render_markdown=True)
with gr.Row():
texto_in = gr.Textbox(label="📝 Escribe tu mensaje", lines=1, placeholder="Ej: Describe this image in English")
audio_in = gr.Audio(sources=["microphone"], type="filepath", label="🎤 O habla aquí")
imagen_in = gr.Image(label="📸 Imagen opcional", type="pil")
with gr.Row():
voz_sel = gr.Dropdown(
label="Voz de respuesta",
choices=[
("Femenina cálida", "085fdec7-b201-4a58-b65b-4d321f7abd85"),
("Masculina serena", "5c7d2e6a-5d3f-4b3a-8a3d-2e6a5d3f4b3a"),
("Neutra", "9a8b7c6d-5e4f-3a2b-1c0d-9e8f7a6b5c4d")
],
value=VOICE_ID_DEFAULT
)
enviar_btn = gr.Button("Enviar")
limpiar_btn = gr.Button("Limpiar")
salida_texto = gr.Textbox(label="Respuesta", interactive=False, show_copy_button=True)
salida_audio = gr.Audio(label="Audio", autoplay=True, interactive=False)
enviar_btn.click(
manejar_entrada,
inputs=[texto_in, audio_in, imagen_in, chat, voz_sel],
outputs=[chat, salida_audio, salida_texto]
)
texto_in.submit(
manejar_entrada,
inputs=[texto_in, audio_in, imagen_in, chat, voz_sel],
outputs=[chat, salida_audio, salida_texto]
)
limpiar_btn.click(limpiar_chat, outputs=[chat, salida_audio, salida_texto])
demo.launch()
|