Como ya os había comentado, en mi suscripción de Azure tengo tanto modelos desplegados en Azure AI Foundry como funciones de Azure Functions.
Hay algunas ventajas muy importantes respecto a Foundry, y básicamente es el precio y el control de todo el proceso. Puedo desplegar funciones que no alucinen (o por lo menos no temer a que lo hagan) y el precio, por lo general es mucho más bajo.
Por ejemplo, pensé en la posibilidad de crear un proceso que hiciese lo siguiente:
tengo comentarios de personas de mi empresa imaginaria desde el 1 de enero hasta mayo. En total, 550 comentarios de 550 personas distintas, realizados en fechas diferentes.
Pues bien, quiero visualizar esa información como si se tratase de un electrocardiograma, una forma diferente de ver e interpretar los datos.
¿Qué os parece?
A mí me encanta: es como tomar el pulso al corazón de la empresa ❤️
Para ello utilicé Azure Functions.
Podría haber utilizado Sora, pero habría sido mucho más caro y, además, cada ejecución generaría una imagen diferente.
Y en este caso lo que buscaba era control total sobre lo que voy a generar, asegurando coherencia, reproducibilidad y estabilidad en el resultado.
Por lo tanto, vamos como siempre con la función que vamos a colocar en VSC antes de desplegarla en Azure:
import io
from datetime import datetime
from collections import defaultdict
import azure.functions as func
import matplotlib.pyplot as plt
def main(req: func.HttpRequest) -> func.HttpResponse:
try:
# Leemos el cuerpo de la petición
body = req.get_json()
items = body.get("items", [])
if not items:
return func.HttpResponse("No hay datos", status_code=400)
ancho = body.get("ancho", 1200)
alto = body.get("alto", 300)
# Procesamos los datos y capturamos el dominio real del sentimiento
agrupado = defaultdict(list)
fechas = []
sentimientos_reales = []
for it in items:
try:
fecha = datetime.fromisoformat(it["FechaEvento"])
sentimiento = float(it["SentimientoScore"])
sentimientos_reales.append(sentimiento)
semana = fecha.strftime("%Y-%U")
agrupado[semana].append(sentimiento)
fechas.append(fecha)
except:
continue
if not sentimientos_reales:
return func.HttpResponse("Datos inválidos", status_code=400)
semanas = sorted(agrupado.keys())
medias = []
volumenes = []
for s in semanas:
valores = agrupado[s]
medias.append(sum(valores) / len(valores))
volumenes.append(len(valores))
# Calculamos el rango temporal
fecha_inicio = min(fechas).strftime("%d %b %Y")
fecha_fin = max(fechas).strftime("%d %b %Y")
# Construimos el pulso tipo ECG
x = []
y = []
t = 0.0
max_vol = max(volumenes)
for i, media in enumerate(medias):
intensidad = volumenes[i] / max_vol
pico = media * (1 + intensidad)
latido = [
(0.00, media * 0.2),
(0.08, media * 0.3),
(0.12, media * -0.2),
(0.15, pico),
(0.18, media * -0.4),
(0.28, media * 0.2),
(0.45, media * 0.2),
]
for dx, dy in latido:
x.append(t + dx)
y.append(dy)
t += 0.6
# Definimos el eje Y a partir del dominio real del modelo
y_model_min = min(sentimientos_reales)
y_model_max = max(sentimientos_reales)
margen_sup = (y_model_max - y_model_min) * 0.10
ylim_inf = y_model_min
ylim_sup = y_model_max + margen_sup
ticks_y = [
round(y_model_min, 2),
round((y_model_min + 0) / 2, 2),
0.0,
round(y_model_max / 2, 2),
round(y_model_max, 2)
]
# Dibujamos la gráfica
fig, ax = plt.subplots(figsize=(ancho / 100, alto / 100))
ax.set_facecolor("#fffafa")
ax.minorticks_on()
ax.grid(which="minor", color="#e6bcbc", linewidth=0.4)
ax.grid(which="major", color="#c08080", linewidth=0.8)
ax.plot(x, y, color="#b00000", linewidth=2)
ax.axhline(0, linestyle="--", linewidth=0.6, alpha=0.6)
ax.set_ylim(ylim_inf, ylim_sup)
ax.set_yticks(ticks_y)
ax.tick_params(axis="y", labelsize=8)
ax.set_xticks([])
fig.suptitle("Pulso emocional de la organización", fontsize=14, y=0.97)
fig.text(0.01, 0.92, f"{fecha_inicio} → {fecha_fin}", fontsize=9)
ax.text(0.01, 0.88, "↑ Más positivo", transform=ax.transAxes,
fontsize=9, color="green")
ax.text(0.01, 0.08, "↓ Más negativo", transform=ax.transAxes,
fontsize=9, color="red")
ax.text(0.99, 0.02, "← Pasado Presente →",
transform=ax.transAxes,
fontsize=8, ha="right")
ax.spines["top"].set_visible(False)
ax.spines["right"].set_visible(False)
ax.spines["bottom"].set_visible(False)
# Exportamos PNG
buffer = io.BytesIO()
plt.savefig(buffer, format="png", dpi=100)
plt.close()
buffer.seek(0)
return func.HttpResponse(
body=buffer.getvalue(),
mimetype="image/png",
status_code=200
)
except Exception as e:
return func.HttpResponse(
f"Error interno: {str(e)}",
status_code=500
)
Desplegada ya Azure:

Luego cargamos los datos en nuestra lista de SharePoint (o el medio que sea):

Y el siguiente paso es crear un flujo para que lea los datos de SharePoint con las respuestas de los 550 empleados y lo pase a la función desplegada en Azure y recupere nuestro electro:


La función de salida es esta, debéis añadirla como una expresión:
concat(
'data:image/png;base64,',
base64(body('HTTP'))
)
y finalmente, tenemos que incluir en nuestro power apps, en un botón:
Set(
imgPulso;
FLUJOPULSO.Run().img
)
Y en el control de imagen, imgPulso
El resultado:

