|
|
|
|
|
""" |
|
|
Análisis de Value Betting con Interfaz Gradio |
|
|
""" |
|
|
|
|
|
import pandas as pd |
|
|
import numpy as np |
|
|
from scipy.stats import poisson |
|
|
from datetime import datetime |
|
|
from openai import OpenAI |
|
|
import gradio as gr |
|
|
import os |
|
|
|
|
|
from reportlab.lib.pagesizes import A4 |
|
|
from reportlab.platypus import SimpleDocTemplate, Paragraph, Spacer, Table, TableStyle |
|
|
from reportlab.lib import colors |
|
|
from reportlab.lib.styles import getSampleStyleSheet, ParagraphStyle |
|
|
from reportlab.lib.enums import TA_CENTER, TA_LEFT |
|
|
|
|
|
|
|
|
df = None |
|
|
|
|
|
def cargar_datos(archivo_csv): |
|
|
"""Carga los datos del CSV""" |
|
|
global df |
|
|
try: |
|
|
if archivo_csv is None: |
|
|
return "❌ Por favor, sube un archivo CSV", None |
|
|
|
|
|
df = pd.read_csv(archivo_csv.name) |
|
|
|
|
|
|
|
|
equipos_local = df["HomeTeam"].unique().tolist() |
|
|
equipos_visitante = df["AwayTeam"].unique().tolist() |
|
|
todos_equipos = sorted(list(set(equipos_local + equipos_visitante))) |
|
|
|
|
|
return f"✅ Datos cargados correctamente. {len(df)} partidos encontrados.", gr.update(choices=todos_equipos), gr.update(choices=todos_equipos) |
|
|
except Exception as e: |
|
|
return f"❌ Error al cargar datos: {str(e)}", None, None |
|
|
|
|
|
def moda(series): |
|
|
m = series.mode() |
|
|
return m.iloc[0] if not m.empty else np.nan |
|
|
|
|
|
def estadisticas_equipo(local, visitante): |
|
|
categorias = { |
|
|
"Goles FT Local": ("FTHG", df["HomeTeam"] == local), |
|
|
"Goles FT Visitante": ("FTAG", df["AwayTeam"] == visitante), |
|
|
"Goles 1T Local": ("HTHG", df["HomeTeam"] == local), |
|
|
"Goles 1T Visitante": ("HTAG", df["AwayTeam"] == visitante), |
|
|
"Remates Local": ("HS", df["HomeTeam"] == local), |
|
|
"Remates Visitante": ("AS", df["AwayTeam"] == visitante), |
|
|
"Remates a puerta Local": ("HST", df["HomeTeam"] == local), |
|
|
"Remates a puerta Visitante": ("AST", df["AwayTeam"] == visitante), |
|
|
"Corners Local": ("HC", df["HomeTeam"] == local), |
|
|
"Corners Visitante": ("AC", df["AwayTeam"] == visitante), |
|
|
} |
|
|
|
|
|
filas = [] |
|
|
for nombre, (col, filtro) in categorias.items(): |
|
|
s = df.loc[filtro, col] |
|
|
media = s.mean() |
|
|
mediana = s.median() |
|
|
mod = moda(s) |
|
|
|
|
|
if abs(media - mod) < 0.3: |
|
|
vol = "Estable" |
|
|
factor = 1.00 |
|
|
elif media > mod: |
|
|
vol = "Picos altos" |
|
|
factor = 0.95 |
|
|
else: |
|
|
vol = "Bajones" |
|
|
factor = 0.97 |
|
|
|
|
|
filas.append([ |
|
|
nombre, round(media,2), round(mediana,2), mod, vol, factor |
|
|
]) |
|
|
|
|
|
return filas |
|
|
|
|
|
def lambdas_goles(local, visitante): |
|
|
lg_h, lg_a = df["FTHG"].mean(), df["FTAG"].mean() |
|
|
df_l = df[df["HomeTeam"] == local] |
|
|
df_v = df[df["AwayTeam"] == visitante] |
|
|
|
|
|
atk_l = df_l["FTHG"].mean() / lg_h |
|
|
def_l = df_l["FTAG"].mean() / lg_a |
|
|
atk_v = df_v["FTAG"].mean() / lg_a |
|
|
def_v = df_v["FTHG"].mean() / lg_h |
|
|
|
|
|
λl = lg_h * atk_l * def_v |
|
|
λv = lg_a * atk_v * def_l |
|
|
|
|
|
return { |
|
|
"FT": (λl, λv), |
|
|
"1T": (λl*0.45, λv*0.45), |
|
|
"2T": (λl*0.55, λv*0.55) |
|
|
} |
|
|
|
|
|
def probs_goles(lh, la, maxg=6): |
|
|
p = { |
|
|
"1":0,"X":0,"2":0, |
|
|
"Over2.5":0,"Under2.5":0, |
|
|
"BTTS_SI":0,"BTTS_NO":0 |
|
|
} |
|
|
|
|
|
for i in range(maxg+1): |
|
|
for j in range(maxg+1): |
|
|
pr = poisson.pmf(i,lh)*poisson.pmf(j,la) |
|
|
if i>j: p["1"]+=pr |
|
|
if i==j: p["X"]+=pr |
|
|
if i<j: p["2"]+=pr |
|
|
if i+j>2: p["Over2.5"]+=pr |
|
|
if i+j<=2: p["Under2.5"]+=pr |
|
|
if i>0 and j>0: p["BTTS_SI"]+=pr |
|
|
if i==0 or j==0: p["BTTS_NO"]+=pr |
|
|
|
|
|
return {k:round(v,4) for k,v in p.items()} |
|
|
|
|
|
def edge(p, c): |
|
|
return round(p - 1/c,4) |
|
|
|
|
|
def kelly(p, c): |
|
|
b = c - 1 |
|
|
return round(max(0, (b*p - (1-p))/b),4) |
|
|
|
|
|
def web_info(local, visitante, api_key): |
|
|
"""Obtiene información web usando OpenAI""" |
|
|
if not api_key: |
|
|
return "⚠️ API Key de OpenAI no proporcionada. Análisis web omitido." |
|
|
|
|
|
try: |
|
|
client = OpenAI(api_key=api_key) |
|
|
|
|
|
prompt = f""" |
|
|
Actúa como un experto analista de datos de fútbol y periodista deportivo. |
|
|
Realiza una búsqueda web exhaustiva y un scraping de noticias de última hora para el partido: {local} vs {visitante}. |
|
|
|
|
|
Genera un informe detallado que incluya los siguientes apartados: |
|
|
|
|
|
Contexto y Cuotas: |
|
|
- Busca las cuotas actuales de 1X2 en casas de apuestas principales (ej. Bet365, William Hill o similares). |
|
|
Si no hay cuotas claras o disponibles, indícalo explícitamente y explica el motivo. |
|
|
|
|
|
Estado de las Plantillas (Actualizado al día de hoy): |
|
|
- Lesiones: Lista de jugadores confirmados como baja y los que están en duda. |
|
|
- Sanciones: Jugadores que no pueden jugar por acumulación de tarjetas. |
|
|
|
|
|
Análisis Táctico y de Rendimiento: |
|
|
- Rotaciones: Indica si alguno de los equipos viene de jugar competición europea. |
|
|
- Fatiga: Analiza el calendario reciente. |
|
|
|
|
|
Formato: Sé conciso y prioriza la información de las últimas 24-48 horas. |
|
|
""" |
|
|
|
|
|
r = client.responses.create( |
|
|
model="gpt-4.1", |
|
|
tools=[{"type":"web_search"}], |
|
|
input=prompt, |
|
|
temperature=0, |
|
|
max_output_tokens=400 |
|
|
) |
|
|
return r.output_text |
|
|
except Exception as e: |
|
|
return f"⚠️ Error al obtener información web: {str(e)}" |
|
|
|
|
|
def estimar_cuotas(probs, margen=0.07): |
|
|
return {k: round(1/(v*(1-margen)),2) for k,v in probs.items()} |
|
|
|
|
|
def ia_final(local, visitante, estadisticas, probs, cuotas, contexto, api_key): |
|
|
"""Análisis final con IA""" |
|
|
if not api_key: |
|
|
return "⚠️ API Key de OpenAI no proporcionada. Análisis de IA omitido." |
|
|
|
|
|
try: |
|
|
client = OpenAI(api_key=api_key) |
|
|
|
|
|
prompt = f""" |
|
|
Actúa como un experto analista estadístico de fútbol y apostador profesional |
|
|
especializado en Value Betting. |
|
|
|
|
|
ESTADÍSTICAS (Media / Mediana / Moda): |
|
|
{estadisticas} |
|
|
|
|
|
PROBABILIDADES PROYECTADAS (modelo matemático): |
|
|
{probs} |
|
|
|
|
|
CUOTAS DE MERCADO: |
|
|
{cuotas} |
|
|
|
|
|
CONTEXTO RECIENTE (web): |
|
|
{contexto} |
|
|
|
|
|
Tareas: |
|
|
1. Contrastar modelo matemático vs realidad reciente |
|
|
2. Analizar CADA categoría (goles, BTTS, corners, remates, remates a puerta, goles por tiempo) |
|
|
3. Identificar value betting real |
|
|
4. Advertir riesgos por volatilidad |
|
|
5. Recomendar máximo 2 apuestas |
|
|
""" |
|
|
|
|
|
r = client.responses.create( |
|
|
model="gpt-4.1", |
|
|
input=prompt, |
|
|
temperature=0.2, |
|
|
max_output_tokens=20000 |
|
|
) |
|
|
return r.output_text |
|
|
except Exception as e: |
|
|
return f"⚠️ Error en análisis de IA: {str(e)}" |
|
|
|
|
|
def analizar_partido(local, visitante, api_key, incluir_analisis_web): |
|
|
"""Función principal que analiza el partido y retorna resultados""" |
|
|
global df |
|
|
|
|
|
if df is None: |
|
|
return "❌ Por favor, primero carga los datos CSV", None, None, None, None |
|
|
|
|
|
if not local or not visitante: |
|
|
return "❌ Por favor, selecciona ambos equipos", None, None, None, None |
|
|
|
|
|
try: |
|
|
|
|
|
stats = estadisticas_equipo(local, visitante) |
|
|
lambdas = lambdas_goles(local, visitante) |
|
|
probs = probs_goles(*lambdas["FT"]) |
|
|
cuotas = estimar_cuotas(probs) |
|
|
|
|
|
|
|
|
contexto = "" |
|
|
if incluir_analisis_web: |
|
|
contexto = web_info(local, visitante, api_key) |
|
|
else: |
|
|
contexto = "Análisis web no solicitado." |
|
|
|
|
|
|
|
|
analisis_ia = "" |
|
|
if incluir_analisis_web: |
|
|
analisis_ia = ia_final(local, visitante, stats, probs, cuotas, contexto, api_key) |
|
|
else: |
|
|
analisis_ia = "Análisis de IA no solicitado." |
|
|
|
|
|
|
|
|
valores = [] |
|
|
volatilidades = [] |
|
|
|
|
|
for fila in stats: |
|
|
_, media, _, moda, vol, _ = fila |
|
|
if not np.isnan(media) and not np.isnan(moda): |
|
|
valores.append(abs(media - moda)) |
|
|
volatilidades.append(vol) |
|
|
|
|
|
penalizacion_vol = volatilidades.count("Picos altos") * 8 |
|
|
dispersion = np.mean(valores) if valores else 0 |
|
|
|
|
|
score_confianza = max( |
|
|
0, |
|
|
min(100, int(100 - penalizacion_vol - dispersion * 15)) |
|
|
) |
|
|
|
|
|
|
|
|
resumen = f""" |
|
|
### 📊 Score de Confianza: {score_confianza}/100 |
|
|
|
|
|
Este score sintetiza la consistencia estadística del partido. |
|
|
Un valor alto indica mayor fiabilidad del modelo matemático. |
|
|
""" |
|
|
|
|
|
|
|
|
tabla_stats = pd.DataFrame(stats, columns=["Categoría", "Media", "Mediana", "Moda", "Volatilidad", "Factor"]) |
|
|
|
|
|
|
|
|
tabla_valor_data = [] |
|
|
for k in probs: |
|
|
tabla_valor_data.append({ |
|
|
"Mercado": k, |
|
|
"Prob. Proy.": f"{probs[k]*100:.1f}%", |
|
|
"Cuota": cuotas[k], |
|
|
"Edge": f"{edge(probs[k], cuotas[k])*100:.2f}%", |
|
|
"Kelly": f"{kelly(probs[k], cuotas[k])*100:.1f}%" |
|
|
}) |
|
|
tabla_valor = pd.DataFrame(tabla_valor_data) |
|
|
|
|
|
|
|
|
advertencias = [] |
|
|
for f in stats: |
|
|
if f[4] == "Picos altos": |
|
|
advertencias.append(f"⚠️ {f[0]} presenta picos inflados (riesgo de sobreestimación).") |
|
|
if f[4] == "Bajones": |
|
|
advertencias.append(f"⚠️ {f[0]} muestra tendencia a bajones (riesgo de unders).") |
|
|
|
|
|
advertencias_texto = "\n".join(advertencias) if advertencias else "✅ No se detectaron advertencias significativas." |
|
|
|
|
|
|
|
|
resultado_contexto = f"### 🌐 Contexto del Partido\n\n{contexto}" |
|
|
resultado_analisis = f"### 🤖 Análisis de IA\n\n{analisis_ia}" |
|
|
|
|
|
return resumen, tabla_stats, tabla_valor, advertencias_texto, resultado_contexto, resultado_analisis |
|
|
|
|
|
except Exception as e: |
|
|
return f"❌ Error en el análisis: {str(e)}", None, None, None, None, None |
|
|
|
|
|
def generar_pdf(local, visitante, api_key, incluir_analisis_web): |
|
|
"""Genera el PDF del reporte""" |
|
|
global df |
|
|
|
|
|
if df is None: |
|
|
return None, "❌ Por favor, primero carga los datos CSV" |
|
|
|
|
|
if not local or not visitante: |
|
|
return None, "❌ Por favor, selecciona ambos equipos" |
|
|
|
|
|
try: |
|
|
|
|
|
stats = estadisticas_equipo(local, visitante) |
|
|
lambdas = lambdas_goles(local, visitante) |
|
|
probs = probs_goles(*lambdas["FT"]) |
|
|
cuotas = estimar_cuotas(probs) |
|
|
|
|
|
|
|
|
contexto = "" |
|
|
if incluir_analisis_web: |
|
|
contexto = web_info(local, visitante, api_key) |
|
|
else: |
|
|
contexto = "Análisis web no solicitado." |
|
|
|
|
|
|
|
|
analisis_ia = "" |
|
|
if incluir_analisis_web: |
|
|
analisis_ia = ia_final(local, visitante, stats, probs, cuotas, contexto, api_key) |
|
|
else: |
|
|
analisis_ia = "Análisis de IA no solicitado." |
|
|
|
|
|
|
|
|
valores = [] |
|
|
volatilidades = [] |
|
|
|
|
|
for fila in stats: |
|
|
_, media, _, moda, vol, _ = fila |
|
|
if not np.isnan(media) and not np.isnan(moda): |
|
|
valores.append(abs(media - moda)) |
|
|
volatilidades.append(vol) |
|
|
|
|
|
penalizacion_vol = volatilidades.count("Picos altos") * 8 |
|
|
dispersion = np.mean(valores) if valores else 0 |
|
|
|
|
|
score_confianza = max( |
|
|
0, |
|
|
min(100, int(100 - penalizacion_vol - dispersion * 15)) |
|
|
) |
|
|
|
|
|
|
|
|
filename = f"/tmp/Reporte_{local}_vs_{visitante}.pdf".replace(" ", "_") |
|
|
doc = SimpleDocTemplate(filename, pagesize=A4) |
|
|
styles = getSampleStyleSheet() |
|
|
story = [] |
|
|
|
|
|
|
|
|
story.append(Paragraph("Reporte Profesional de Value Betting", styles["Title"])) |
|
|
story.append(Paragraph(f"{local} vs {visitante}", styles["Heading2"])) |
|
|
story.append(Spacer(1, 12)) |
|
|
|
|
|
|
|
|
resumen = f""" |
|
|
<b>Score de Confianza:</b> {score_confianza}/100<br/><br/> |
|
|
Este score sintetiza la consistencia estadística del partido, penalizando |
|
|
alta volatilidad (media ≠ moda) y dispersiones elevadas. |
|
|
Un valor alto indica mayor fiabilidad del modelo matemático. |
|
|
""" |
|
|
story.append(Paragraph("Resumen Ejecutivo", styles["Heading2"])) |
|
|
story.append(Paragraph(resumen, styles["Normal"])) |
|
|
story.append(Spacer(1, 12)) |
|
|
|
|
|
|
|
|
story.append(Paragraph("Estadísticas Descriptivas (Media / Mediana / Moda)", styles["Heading2"])) |
|
|
|
|
|
tabla_stats = [["Categoría", "Media", "Mediana", "Moda", "Volatilidad", "Factor"]] |
|
|
for f in stats: |
|
|
tabla_stats.append(f) |
|
|
|
|
|
story.append( |
|
|
Table( |
|
|
tabla_stats, |
|
|
style=[ |
|
|
("GRID", (0,0), (-1,-1), 0.5, colors.grey), |
|
|
("BACKGROUND", (0,0), (-1,0), colors.lightgrey), |
|
|
("ALIGN", (1,1), (-1,-1), "CENTER"), |
|
|
], |
|
|
repeatRows=1 |
|
|
) |
|
|
) |
|
|
story.append(Spacer(1, 12)) |
|
|
|
|
|
|
|
|
story.append(Paragraph("Probabilidades, Valor y Kelly Criterion", styles["Heading2"])) |
|
|
|
|
|
tabla_valor = [["Mercado", "Prob. Proy.", "Cuota", "Edge", "Kelly"]] |
|
|
for k in probs: |
|
|
tabla_valor.append([ |
|
|
k, |
|
|
f"{probs[k]*100:.1f}%", |
|
|
cuotas[k], |
|
|
f"{edge(probs[k], cuotas[k])*100:.2f}%", |
|
|
f"{kelly(probs[k], cuotas[k])*100:.1f}%" |
|
|
]) |
|
|
|
|
|
story.append( |
|
|
Table( |
|
|
tabla_valor, |
|
|
style=[ |
|
|
("GRID", (0,0), (-1,-1), 0.5, colors.grey), |
|
|
("BACKGROUND", (0,0), (-1,0), colors.lightgrey), |
|
|
("ALIGN", (1,1), (-1,-1), "CENTER"), |
|
|
], |
|
|
repeatRows=1 |
|
|
) |
|
|
) |
|
|
story.append(Spacer(1, 12)) |
|
|
|
|
|
|
|
|
advertencias = [] |
|
|
for f in stats: |
|
|
if f[4] == "Picos altos": |
|
|
advertencias.append(f"• {f[0]} presenta picos inflados (riesgo de sobreestimación).") |
|
|
if f[4] == "Bajones": |
|
|
advertencias.append(f"• {f[0]} muestra tendencia a bajones (riesgo de unders).") |
|
|
|
|
|
if advertencias: |
|
|
story.append(Paragraph("Advertencias de Riesgo Estadístico", styles["Heading2"])) |
|
|
story.append(Paragraph("<br/>".join(advertencias), styles["Normal"])) |
|
|
story.append(Spacer(1, 12)) |
|
|
|
|
|
|
|
|
if incluir_analisis_web: |
|
|
story.append(Paragraph("Contraste IA: Modelo Matemático vs Contexto Real", styles["Heading2"])) |
|
|
story.append(Paragraph(analisis_ia.replace("\n", "<br/>"), styles["Normal"])) |
|
|
|
|
|
|
|
|
story.append(Spacer(1, 24)) |
|
|
story.append( |
|
|
Paragraph( |
|
|
f"Reporte generado automáticamente • {datetime.now().strftime('%Y-%m-%d %H:%M')}", |
|
|
styles["Italic"] |
|
|
) |
|
|
) |
|
|
|
|
|
doc.build(story) |
|
|
|
|
|
return filename, f"✅ PDF generado exitosamente: {local} vs {visitante}" |
|
|
|
|
|
except Exception as e: |
|
|
return None, f"❌ Error al generar PDF: {str(e)}" |
|
|
|
|
|
|
|
|
with gr.Blocks(title="⚽ Análisis de Value Betting", theme=gr.themes.Soft()) as demo: |
|
|
gr.Markdown(""" |
|
|
# ⚽ Sistema de Análisis de Value Betting |
|
|
### Análisis estadístico profesional para apuestas deportivas |
|
|
|
|
|
**Instrucciones:** |
|
|
1. Sube tu archivo CSV con datos históricos |
|
|
2. Selecciona los equipos a analizar |
|
|
3. (Opcional) Ingresa tu API Key de OpenAI para análisis web avanzado |
|
|
4. Genera el análisis o descarga el reporte en PDF |
|
|
""") |
|
|
|
|
|
with gr.Row(): |
|
|
with gr.Column(scale=1): |
|
|
gr.Markdown("### 📁 Carga de Datos") |
|
|
archivo_csv = gr.File(label="Archivo CSV de partidos", file_types=[".csv"]) |
|
|
btn_cargar = gr.Button("📊 Cargar Datos", variant="primary") |
|
|
estado_carga = gr.Textbox(label="Estado", interactive=False) |
|
|
|
|
|
gr.Markdown("### ⚙️ Configuración") |
|
|
equipo_local = gr.Dropdown(label="🏠 Equipo Local", choices=[], interactive=True) |
|
|
equipo_visitante = gr.Dropdown(label="✈️ Equipo Visitante", choices=[], interactive=True) |
|
|
|
|
|
api_key = gr.Textbox( |
|
|
label="🔑 OpenAI API Key (opcional)", |
|
|
type="password", |
|
|
placeholder="sk-...", |
|
|
info="Necesaria solo para análisis web y de IA" |
|
|
) |
|
|
|
|
|
incluir_web = gr.Checkbox( |
|
|
label="Incluir análisis web y de IA", |
|
|
value=False, |
|
|
info="Requiere API Key de OpenAI" |
|
|
) |
|
|
|
|
|
with gr.Row(): |
|
|
btn_analizar = gr.Button("📈 Analizar Partido", variant="primary", size="lg") |
|
|
btn_pdf = gr.Button("📄 Generar PDF", variant="secondary", size="lg") |
|
|
|
|
|
with gr.Column(scale=2): |
|
|
gr.Markdown("### 📊 Resultados del Análisis") |
|
|
|
|
|
with gr.Tab("📈 Resumen"): |
|
|
resumen_output = gr.Markdown() |
|
|
advertencias_output = gr.Textbox(label="⚠️ Advertencias de Riesgo", lines=5) |
|
|
|
|
|
with gr.Tab("📊 Estadísticas"): |
|
|
tabla_stats_output = gr.Dataframe(label="Estadísticas Descriptivas") |
|
|
|
|
|
with gr.Tab("💰 Probabilidades y Valor"): |
|
|
tabla_valor_output = gr.Dataframe(label="Mercados de Apuestas") |
|
|
|
|
|
with gr.Tab("🌐 Contexto Web"): |
|
|
contexto_output = gr.Markdown() |
|
|
|
|
|
with gr.Tab("🤖 Análisis IA"): |
|
|
analisis_output = gr.Markdown() |
|
|
|
|
|
with gr.Tab("📄 PDF"): |
|
|
pdf_output = gr.File(label="Descargar Reporte PDF") |
|
|
pdf_estado = gr.Textbox(label="Estado del PDF", interactive=False) |
|
|
|
|
|
|
|
|
btn_cargar.click( |
|
|
fn=cargar_datos, |
|
|
inputs=[archivo_csv], |
|
|
outputs=[estado_carga, equipo_local, equipo_visitante] |
|
|
) |
|
|
|
|
|
btn_analizar.click( |
|
|
fn=analizar_partido, |
|
|
inputs=[equipo_local, equipo_visitante, api_key, incluir_web], |
|
|
outputs=[ |
|
|
resumen_output, |
|
|
tabla_stats_output, |
|
|
tabla_valor_output, |
|
|
advertencias_output, |
|
|
contexto_output, |
|
|
analisis_output |
|
|
] |
|
|
) |
|
|
|
|
|
btn_pdf.click( |
|
|
fn=generar_pdf, |
|
|
inputs=[equipo_local, equipo_visitante, api_key, incluir_web], |
|
|
outputs=[pdf_output, pdf_estado] |
|
|
) |
|
|
|
|
|
gr.Markdown(""" |
|
|
--- |
|
|
### 📖 Sobre el Sistema |
|
|
|
|
|
Este sistema utiliza: |
|
|
- **Distribución de Poisson** para modelar probabilidades de goles |
|
|
- **Kelly Criterion** para gestión óptima del bankroll |
|
|
- **Edge Calculation** para identificar value betting |
|
|
- **Análisis de volatilidad** mediante media, mediana y moda |
|
|
- **OpenAI GPT-4** (opcional) para análisis contextual en tiempo real |
|
|
""") |
|
|
|
|
|
|
|
|
if __name__ == "__main__": |
|
|
demo.launch(share=True) |