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
@@ -15,9 +15,16 @@ type umindTgChat struct {
ID int64 `json:"id"`
}
type umindTgFileRef struct {
FileID string `json:"file_id"`
}
type umindTgMessage struct {
Chat umindTgChat `json:"chat"`
Text string `json:"text"`
Chat umindTgChat `json:"chat"`
Text string `json:"text"`
Voice *umindTgFileRef `json:"voice"`
Audio *umindTgFileRef `json:"audio"`
Photo []umindTgFileRef `json:"photo"`
}
type umindTgUpdate struct {
@@ -39,12 +46,23 @@ func UmindTelegramWebhook(c *fiber.Ctx) error {
if err := c.BodyParser(&update); err != nil || update.Message == nil {
return c.SendStatus(fiber.StatusOK)
}
texto := strings.TrimSpace(update.Message.Text)
if texto == "" {
return c.SendStatus(fiber.StatusOK)
}
msg := update.Message
if err := services.ProcesarMensajeTelegramUmind(canal, update.Message.Chat.ID, texto); err != nil {
switch {
case msg.Voice != nil && msg.Voice.FileID != "":
err = services.ProcesarMediaTelegramUmind(canal, msg.Chat.ID, msg.Voice.FileID, "audio")
case msg.Audio != nil && msg.Audio.FileID != "":
err = services.ProcesarMediaTelegramUmind(canal, msg.Chat.ID, msg.Audio.FileID, "audio")
case len(msg.Photo) > 0:
err = services.ProcesarMediaTelegramUmind(canal, msg.Chat.ID, msg.Photo[len(msg.Photo)-1].FileID, "image")
default:
texto := strings.TrimSpace(msg.Text)
if texto == "" {
return c.SendStatus(fiber.StatusOK)
}
err = services.ProcesarMensajeTelegramUmind(canal, msg.Chat.ID, texto)
}
if err != nil {
log.Printf("[UMIND_TELEGRAM] canal %d: %v", canal.ID, err)
models.RegistrarEventoUmind(canal.AgenteID, "error", "canal_telegram", "Error procesando un mensaje de Telegram", err.Error())
}
@@ -53,12 +71,18 @@ func UmindTelegramWebhook(c *fiber.Ctx) error {
// ─── WhatsApp ────────────────────────────────────────────────────────────────
type umindWaMediaRef struct {
ID string `json:"id"`
}
type umindWaMessage struct {
From string `json:"from"`
Type string `json:"type"`
Text struct {
Body string `json:"body"`
} `json:"text"`
Image umindWaMediaRef `json:"image"`
Audio umindWaMediaRef `json:"audio"`
}
type umindWaValue struct {
@@ -130,10 +154,28 @@ func UmindWhatsAppWebhook(c *fiber.Ctx) error {
for _, entry := range payload.Entry {
for _, change := range entry.Changes {
for _, msg := range change.Value.Messages {
if msg.Type != "text" || strings.TrimSpace(msg.Text.Body) == "" {
var err error
switch msg.Type {
case "text":
texto := strings.TrimSpace(msg.Text.Body)
if texto == "" {
continue
}
err = services.ProcesarMensajeWhatsAppUmind(canal, msg.From, texto)
case "audio":
if msg.Audio.ID == "" {
continue
}
err = services.ProcesarMediaWhatsAppUmind(canal, msg.From, msg.Audio.ID, "audio")
case "image":
if msg.Image.ID == "" {
continue
}
err = services.ProcesarMediaWhatsAppUmind(canal, msg.From, msg.Image.ID, "image")
default:
continue
}
if err := services.ProcesarMensajeWhatsAppUmind(canal, msg.From, strings.TrimSpace(msg.Text.Body)); err != nil {
if err != nil {
log.Printf("[UMIND_WHATSAPP] canal %d: %v", canal.ID, err)
models.RegistrarEventoUmind(canal.AgenteID, "error", "canal_whatsapp", "Error procesando un mensaje de WhatsApp", err.Error())
}