import streamlit as st import os import requests from io import BytesIO from openai import OpenAI XAI_API_KEY = os.getenv("XAI_API_KEY", "") # El client se inicializa solo si hay key def get_client(): key = XAI_API_KEY or st.session_state.get("api_key", "") if not key: return None return OpenAI(api_key=key, base_url="https://api.x.ai/v1") def get_key(): return XAI_API_KEY or st.session_state.get("api_key", "") st.set_page_config(page_title="Grok Voice Studio", page_icon="πŸ”Š", layout="wide") st.title("πŸ”Š Grok Voice Studio - TTS, Chat y Clonador xAI") st.markdown("**Plataforma completa con voz nativa Grok, chat y clonaciΓ³n de voz**") # Si no hay key en env, pedir al usuario if not XAI_API_KEY: with st.sidebar: st.subheader("πŸ”‘ ConfiguraciΓ³n") user_key = st.text_input("XAI_API_KEY", type="password", placeholder="xai-...") if user_key: st.session_state["api_key"] = user_key st.success("Key configurada") def call_tts(text: str, voice: str = "eve", language: str = "es", speed: float = 1.0): """ xAI TTS β€” intentamos el endpoint oficial. Si no existe todavΓ­a (404/501), devolvemos None y mostramos aviso. """ key = get_key() if not key: return None, "Sin API key" try: # Intentar con el endpoint nativo de xAI r = requests.post( "https://api.x.ai/v1/audio/speech", headers={"Authorization": f"Bearer {key}", "Content-Type": "application/json"}, json={ "model": "grok-tts", # nombre tentativo segΓΊn docs xAI "input": text, "voice": voice, "response_format": "mp3", "speed": speed }, timeout=30 ) if r.status_code == 200: return BytesIO(r.content), None # Fallback: endpoint legacy que usaba el app original r2 = requests.post( "https://api.x.ai/v1/tts", headers={"Authorization": f"Bearer {key}", "Content-Type": "application/json"}, json={"text": text, "voice_id": voice, "language": language, "speed": speed}, timeout=30 ) if r2.status_code == 200: return BytesIO(r2.content), None # Si ninguno funciona, devolver el error real return None, f"TTS no disponible aΓΊn (HTTP {r.status_code}): {r.text[:300]}" except Exception as e: return None, str(e) tab1, tab2, tab3, tab4 = st.tabs(["πŸ“ TTS Directo", "πŸ’¬ Chat + Voz", "πŸ—£οΈ Clonador", "ℹ️ Info API"]) # ─── TAB 1: TTS DIRECTO ───────────────────────────────────────────────────── with tab1: st.subheader("Lector Directo TTS xAI") texto = st.text_area("Texto:", height=180, placeholder="Escribe el texto que quieres escuchar...") c1, c2, c3 = st.columns(3) with c1: voice = st.selectbox("Voz", ["eve", "ara", "rex", "sal", "leo"], index=0) with c2: lang = st.selectbox("Idioma", ["es", "en", "fr", "pt", "it", "auto"], index=0) with c3: speed = st.slider("Velocidad", 0.5, 2.0, 1.0, 0.1) if st.button("πŸŽ™οΈ Generar Audio", type="primary", key="btn_tts"): if not get_key(): st.error("Configura tu XAI_API_KEY primero (sidebar o variable de entorno)") elif texto.strip(): with st.spinner("Procesando con Grok TTS..."): audio_bytes, err = call_tts(texto, voice, lang, speed) if audio_bytes: st.audio(audio_bytes, format="audio/mp3") st.success("βœ… Audio generado") else: st.error(f"Error TTS: {err}") st.info("πŸ’‘ El endpoint TTS de xAI puede no estar disponible en tu plan actual. Consulta https://docs.x.ai") else: st.warning("Escribe algΓΊn texto primero") # ─── TAB 2: CHAT + VOZ ─────────────────────────────────────────────────────── with tab2: st.subheader("Chat Grok con Voz") # Selector de modelo β€” grok-4 puede no estar disponible en todos los planes model_name = st.selectbox("Modelo", ["grok-3", "grok-3-mini", "grok-2-1212"], index=0, key="chat_model") tts_on = st.toggle("πŸ”Š Leer respuestas en voz alta", value=True) if "msgs" not in st.session_state: st.session_state.msgs = [] # BotΓ³n limpiar chat if st.button("πŸ—‘οΈ Limpiar chat", key="clear_chat"): st.session_state.msgs = [] st.rerun() for m in st.session_state.msgs: with st.chat_message(m["role"]): st.write(m["content"]) prompt = st.chat_input("Mensaje...") if prompt: if not get_key(): st.error("Configura tu XAI_API_KEY primero") else: st.session_state.msgs.append({"role": "user", "content": prompt}) with st.chat_message("user"): st.write(prompt) with st.chat_message("assistant"): with st.spinner("Pensando..."): try: c = get_client() resp = c.chat.completions.create( model=model_name, messages=[{"role": m["role"], "content": m["content"]} for m in st.session_state.msgs], temperature=0.7, max_tokens=2048 ) txt = resp.choices[0].message.content st.write(txt) st.session_state.msgs.append({"role": "assistant", "content": txt}) if tts_on: with st.spinner("Generando voz..."): audio_bytes, err = call_tts(txt[:4000]) # lΓ­mite seguro if audio_bytes: st.audio(audio_bytes, format="audio/mp3") else: st.caption(f"⚠️ TTS no disponible: {err}") except Exception as e: st.error(f"Error chat: {e}") # ─── TAB 3: CLONADOR ───────────────────────────────────────────────────────── with tab3: st.subheader("Clonador de Voz xAI") st.info("ℹ️ La clonaciΓ³n de voz requiere acceso al endpoint `/v1/custom-voices` de xAI, actualmente en beta privada.") aud = st.file_uploader("Audio referencia (wav/mp3, mΓ­n. 30s recomendado)", type=["wav", "mp3"]) vname = st.text_input("Nombre para la voz", "MiVoz") if aud and st.button("🧬 Clonar Voz", key="btn_clone"): if not get_key(): st.error("Configura tu XAI_API_KEY primero") else: with st.spinner("Clonando voz... (puede tardar varios segundos)"): try: files = {"file": (aud.name, aud.getvalue(), "audio/wav")} cr = requests.post( "https://api.x.ai/v1/custom-voices", headers={"Authorization": f"Bearer {get_key()}"}, files=files, data={"name": vname, "language": "es"}, timeout=60 ) if cr.status_code == 200: vid = cr.json().get("voice_id") or cr.json().get("id") st.success(f"βœ… Voz creada! ID: `{vid}`") st.session_state["cloned_voice_id"] = vid else: st.error(f"HTTP {cr.status_code}: {cr.text[:400]}") st.info("El endpoint de clonaciΓ³n puede no estar disponible pΓΊblicamente aΓΊn.") except Exception as e: st.error(str(e)) # Probar voz clonada si existe en session if "cloned_voice_id" in st.session_state: st.divider() st.write(f"**Voz activa:** `{st.session_state['cloned_voice_id']}`") test_text = st.text_area("Texto de prueba", "Hola, esta es mi voz clonada con Grok.") if st.button("▢️ Probar voz clonada"): with st.spinner("Sintetizando..."): audio_bytes, err = call_tts(test_text, voice=st.session_state["cloned_voice_id"]) if audio_bytes: st.audio(audio_bytes, format="audio/mp3") else: st.error(err) # ─── TAB 4: INFO API ───────────────────────────────────────────────────────── with tab4: st.subheader("Estado de la API xAI") if st.button("πŸ” Verificar conexiΓ³n API", key="btn_check"): if not get_key(): st.error("Configura tu XAI_API_KEY primero") else: with st.spinner("Verificando..."): try: c = get_client() # Llamada mΓ­nima para verificar autenticaciΓ³n models = c.models.list() model_names = [m.id for m in models.data] st.success("βœ… API key vΓ‘lida") st.write("**Modelos disponibles en tu cuenta:**") for mn in model_names: st.code(mn) except Exception as e: st.error(f"Error: {e}") st.divider() st.markdown(""" ### Endpoints xAI relevantes | Endpoint | Estado | |----------|--------| | `/v1/chat/completions` | βœ… Disponible | | `/v1/models` | βœ… Disponible | | `/v1/audio/speech` | ⚠️ Beta / verificar | | `/v1/tts` | ⚠️ Beta / verificar | | `/v1/custom-voices` | πŸ”’ Beta privada | **Docs:** https://docs.x.ai/api **Console:** https://console.x.ai """) st.caption("Grok Voice Studio β€’ Powered by xAI API")