import gradio as gr import pandas as pd import os import secrets import time from collections import defaultdict from dotenv import load_dotenv from core import analyze_text, validate_api_key, validate_glm_api_key # Cargar variables de entorno desde .env load_dotenv() # DEBUG: Verificar carga de variables print("=" * 50) print("DEBUG - Variables de entorno cargadas:") print(f"GROQ_API_KEY existe: {bool(os.environ.get('GROQ_API_KEY'))}") print(f"pass_owner value: '{os.environ.get('pass_owner')}'") print("=" * 50) # Rate limiting (almacenado en memoria del servidor) login_attempts = defaultdict(list) MAX_ATTEMPTS = 5 LOCKOUT_TIME = 300 # 5 minutos def interface_fn(input_text, api_key, provider): """ Wrapper function for Gradio interface. CRITICAL: Environment Variable Logic - If api_key == "owner_authenticated": use environment variable based on provider - Otherwise: use user-provided key """ # Si es el token del owner, usar la key del entorno según el provider if api_key == "owner_authenticated": if provider == "glm": actual_key = os.environ.get("GLM_API_KEY") else: actual_key = os.environ.get("GROQ_API_KEY") if not actual_key: return ( "", "", pd.DataFrame(), f"⚠️ Error de configuración del servidor ({provider.upper()})", ) else: actual_key = api_key result = analyze_text(input_text, actual_key, provider) # Process errors for DataFrame errors_list = result.get("errors", []) if errors_list: df_data = [ [ e.get("original"), e.get("correction"), e.get("explanation"), e.get("type"), ] for e in errors_list ] else: df_data = [] # Empty dataframe if no errors return ( result.get("corrected_text", ""), result.get("spanish_translation", ""), pd.DataFrame( df_data, columns=["Original", "Corrección", "Explicación", "Tipo"] ), result.get("general_feedback", ""), ) def verify_key(key, request: gr.Request): """ Verifies the input key and implements the owner bypass. SECURE VERSION for Hugging Face Spaces. """ # Rate limiting por IP client_ip = request.client.host if request else "unknown" now = time.time() # Limpiar intentos antiguos login_attempts[client_ip] = [ t for t in login_attempts[client_ip] if now - t < LOCKOUT_TIME ] if len(login_attempts[client_ip]) >= MAX_ATTEMPTS: return ( None, gr.update(visible=True), gr.update(visible=False), "⚠️ Demasiados intentos. Espera 5 minutos.", ) key = key.strip() # Check for bypass con comparación de tiempo constante if key.startswith("pass"): pass_owner = os.environ.get("pass_owner") if pass_owner: expected = "pass" + pass_owner # Usar secrets.compare_digest para evitar timing attacks if len(key) == len(expected) and secrets.compare_digest(key, expected): # ✅ NO retornar la API key real # Usar un token de sesión en su lugar session_token = "owner_authenticated" login_attempts[client_ip] = [] # Reset intentos return ( session_token, gr.update(visible=False), gr.update(visible=True), "", ) else: login_attempts[client_ip].append(now) return ( None, gr.update(visible=True), gr.update(visible=False), "⚠️ Credenciales incorrectas.", ) # Validar formato de API key de usuario (Groq keys empiezan con gsk_) if key and key.startswith("gsk_") and len(key) > 30: if validate_api_key(key): login_attempts[client_ip] = [] return key, gr.update(visible=False), gr.update(visible=True), "" else: login_attempts[client_ip].append(now) return ( None, gr.update(visible=True), gr.update(visible=False), "⚠️ API Key inválida (rechazada por Groq).", ) # GLM key validation (new) if len(key) > 20: if validate_glm_api_key(key): login_attempts[client_ip] = [] return key, gr.update(visible=False), gr.update(visible=True), "" else: login_attempts[client_ip].append(now) return ( None, gr.update(visible=True), gr.update(visible=False), "⚠️ API Key inválida (rechazada por GLM).", ) login_attempts[client_ip].append(now) return ( None, gr.update(visible=True), gr.update(visible=False), "⚠️ API Key inválida.", ) def logout_fn(): """ Clears the API key and returns to the login screen. """ return ( None, gr.update(visible=True), gr.update(visible=False), "Sesión cerrada correctamente.", ) def update_provider(choice): """Map UI choice to internal provider value.""" return "glm" if "GLM" in choice else "groq" custom_css = """ .gradio-container { width: 95% !important; max-width: 95% !important; } #main_view_container { padding: 40px; } """ with gr.Blocks( title="GroqEnglish Assistant", theme=gr.themes.Soft(primary_hue="orange"), css=custom_css, ) as demo: # State to store the authenticated API key api_key_state = gr.State() # State to store selected provider (default: "groq") provider_state = gr.State("groq") # --- Login View --- with gr.Column(visible=True) as login_view: with gr.Row(elem_id="login_container"): with gr.Column(scale=1): pass # Spacer with gr.Column(scale=2): gr.Markdown( """ # 🚀 GroqEnglish ### Asistente Inteligente de Escritura (Inglés L2) Bienvenido. Este asistente utiliza modelos avanzados de Groq para ayudarte a pulir tu inglés. """ ) with gr.Group(): api_key_input = gr.Textbox( label="🔑 Groq API Key", type="password", placeholder="gsk_...", lines=1, info="Tu llave es necesaria para acceder al modelo.", ) login_btn = gr.Button("Ingresar", variant="primary", size="lg") gr.Markdown( """ > **¿No tienes una API Key?** > Consíguela gratis en [Groq Console](https://console.groq.com/keys). """ ) login_msg = gr.Markdown("", visible=True) with gr.Column(scale=1): pass # Spacer # --- Main View --- with gr.Column(visible=False, elem_id="main_view_container") as main_view: # Header with Title and Logout with gr.Row(variant="panel"): with gr.Column(scale=6): gr.Markdown("# 🇬🇧 GroqEnglish Studio") with gr.Column(scale=2, min_width=150): provider_radio = gr.Radio( choices=["Groq Llama 3.3", "GLM-4.7-Flash"], value="Groq Llama 3.3", label="Modelo AI", interactive=True, ) with gr.Column(scale=1, min_width=100): logout_btn = gr.Button("Cerrar Sesión", variant="secondary", size="sm") gr.Markdown( "Transforma tu inglés con correcciones gramaticales, traducciones inversas y explicaciones detalladas." ) with gr.Row(equal_height=True): # Left Column: Input with gr.Column(scale=1): with gr.Group(): gr.Markdown("### ✍️ Tu Texto") input_box = gr.Textbox( show_label=False, placeholder="Escribe aquí tu frase en inglés... (ej. I goes to the park yesterday)", lines=12, elem_id="input_text", ) submit_btn = gr.Button( "✨ Analizar y Mejorar", variant="primary", size="lg" ) # Right Column: Output with gr.Column(scale=1): with gr.Group(): gr.Markdown("### 💎 Resultado Pulido") with gr.Row(): polished_text = gr.Textbox( show_label=False, interactive=False, lines=4, scale=6 ) copy_polished_btn = gr.Button("📋", scale=1, min_width=50) with gr.Group(): gr.Markdown("### 🇪🇸 Verificación (Traducción)") with gr.Row(): spanish_translation = gr.Textbox( show_label=False, interactive=False, lines=3, scale=6 ) copy_translation_btn = gr.Button("📋", scale=1, min_width=50) with gr.Group(): feedback_msg = gr.Markdown( "### 💡 Feedback General\n*(El análisis aparecerá aquí)*" ) # Bottom Section: Detailed Errors with gr.Accordion("🔍 Detalle de Errores y Explicaciones", open=True): error_table = gr.Dataframe( headers=["Original", "Corrección", "Explicación", "Tipo"], label="Tabla de Correcciones", interactive=False, wrap=True, datatype=["str", "str", "str", "str"], ) with gr.Row(): copy_table_btn = gr.Button("📋 Copiar Tabla de Errores", size="sm") copy_table_output = gr.Textbox(visible=False) # Functionality wiring submit_btn.click( fn=interface_fn, inputs=[input_box, api_key_state, provider_state], outputs=[polished_text, spanish_translation, error_table, feedback_msg], ) # Copy functionality copy_polished_btn.click( lambda x: x, inputs=[polished_text], outputs=[polished_text], js="(x) => {navigator.clipboard.writeText(x); return x;}", ) copy_translation_btn.click( lambda x: x, inputs=[spanish_translation], outputs=[spanish_translation], js="(x) => {navigator.clipboard.writeText(x); return x;}", ) def format_table_for_copy(df): if df is None or len(df) == 0: return "" lines = [] for _, row in df.iterrows(): lines.append( f"Original: {row['Original']} | Corrección: {row['Corrección']} | Explicación: {row['Explicación']}" ) return "\n".join(lines) copy_table_btn.click( fn=format_table_for_copy, inputs=[error_table], outputs=[copy_table_output], js="(text) => {navigator.clipboard.writeText(text); return text;}", ) # --- Global Event Wiring --- # Login login_btn.click( fn=verify_key, inputs=[api_key_input], outputs=[api_key_state, login_view, main_view, login_msg], ) api_key_input.submit( fn=verify_key, inputs=[api_key_input], outputs=[api_key_state, login_view, main_view, login_msg], ) # Logout logout_btn.click( fn=logout_fn, inputs=None, outputs=[api_key_state, login_view, main_view, login_msg], ) # Provider selection provider_radio.change( fn=update_provider, inputs=[provider_radio], outputs=[provider_state] ) if __name__ == "__main__": demo.launch()