feat: OCR y Whisper como herramientas opcionales por canal en uMind

Cada canal (Telegram/WhatsApp) de un agente ahora puede activar, de forma
independiente, que los audios entrantes se transcriban con el Whisper ASR
propio y que a las imágenes entrantes se les extraiga texto con el
servicio OCR propio, antes de pasarle el mensaje al agente. Antes esos
mensajes se ignoraban en silencio.

- UmindCanal gana usar_whisper_audio/usar_ocr_imagenes (default off).
- WhatsApp: se descarga el media vía Graph API (resolución de URL + fetch
  con el mismo access_token del canal) y se enruta a Whisper/OCR según type.
- Telegram: se descarga el archivo vía getFile + CDN de archivos del bot,
  mismo enrutamiento para voice/audio/photo.
- Panel: checkboxes en el alta de canal y toggles inline por canal ya
  creado, en la tab Canales del agente.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
Lizandro Guarnizo
2026-08-13 10:35:48 -05:00
co-authored by Claude Sonnet 5
parent d493d6dee6
commit 84506a98e2
10 changed files with 332 additions and 47 deletions
+12 -6
View File
@@ -463,16 +463,19 @@ func GetUmindCanalesHandler(c *fiber.Ctx) error {
out[i] = fiber.Map{
"ID": canal.ID, "agente_id": canal.AgenteID, "tipo": canal.Tipo, "activo": canal.Activo,
"webhook_url": webhookURL, "ultimo_error": canal.UltimoError,
"usar_whisper_audio": canal.UsarWhisperAudio, "usar_ocr_imagenes": canal.UsarOcrImagenes,
}
}
return c.JSON(fiber.Map{"items": out})
}
type umindCanalReq struct {
AgenteID uint `json:"agente_id"`
Tipo string `json:"tipo"`
Credenciales map[string]string `json:"credenciales"`
Activo bool `json:"activo"`
AgenteID uint `json:"agente_id"`
Tipo string `json:"tipo"`
Credenciales map[string]string `json:"credenciales"`
Activo bool `json:"activo"`
UsarWhisperAudio bool `json:"usar_whisper_audio"`
UsarOcrImagenes bool `json:"usar_ocr_imagenes"`
}
func CreateUmindCanalHandler(c *fiber.Ctx) error {
@@ -502,7 +505,10 @@ func CreateUmindCanalHandler(c *fiber.Ctx) error {
if err != nil {
return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{"error": err.Error()})
}
canal := &models.UmindCanal{AgenteID: req.AgenteID, Tipo: req.Tipo, Activo: true, CredencialesEnc: credencialesEnc}
canal := &models.UmindCanal{
AgenteID: req.AgenteID, Tipo: req.Tipo, Activo: true, CredencialesEnc: credencialesEnc,
UsarWhisperAudio: req.UsarWhisperAudio, UsarOcrImagenes: req.UsarOcrImagenes,
}
if err := models.CreateUmindCanal(canal); err != nil {
return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{"error": err.Error()})
}
@@ -525,7 +531,7 @@ func UpdateUmindCanalHandler(c *fiber.Ctx) error {
if err := c.BodyParser(&req); err != nil {
return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "body inválido"})
}
updates := map[string]interface{}{"activo": req.Activo}
updates := map[string]interface{}{"activo": req.Activo, "usar_whisper_audio": req.UsarWhisperAudio, "usar_ocr_imagenes": req.UsarOcrImagenes}
if len(req.Credenciales) > 0 {
enc, err := services.CifrarCredencialesCanal(req.Credenciales)
if err != nil {