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
+45 -2
View File
@@ -182,6 +182,7 @@ async function copiarWidget() {
function canalVacio() {
return {
tipo: 'telegram', bot_token: '', phone_number_id: '', access_token: '', app_secret: '', verify_token: '',
usar_whisper_audio: false, usar_ocr_imagenes: false,
}
}
@@ -206,7 +207,10 @@ async function guardarCanal() {
verify_token: canalForm.value.verify_token,
}
try {
await api.post('/app/umind/canales', { agente_id: agenteIdNum.value, tipo: canalForm.value.tipo, credenciales, activo: true })
await api.post('/app/umind/canales', {
agente_id: agenteIdNum.value, tipo: canalForm.value.tipo, credenciales, activo: true,
usar_whisper_audio: canalForm.value.usar_whisper_audio, usar_ocr_imagenes: canalForm.value.usar_ocr_imagenes,
})
showCanalForm.value = false
await cargarCanales()
} catch (e) {
@@ -215,7 +219,26 @@ async function guardarCanal() {
}
async function toggleCanal(c) {
await api.put(`/app/umind/canales/${c.ID}`, { activo: !c.activo, credenciales: {} })
await api.put(`/app/umind/canales/${c.ID}`, {
activo: !c.activo, credenciales: {},
usar_whisper_audio: c.usar_whisper_audio, usar_ocr_imagenes: c.usar_ocr_imagenes,
})
await cargarCanales()
}
async function toggleCanalWhisper(c) {
await api.put(`/app/umind/canales/${c.ID}`, {
activo: c.activo, credenciales: {},
usar_whisper_audio: !c.usar_whisper_audio, usar_ocr_imagenes: c.usar_ocr_imagenes,
})
await cargarCanales()
}
async function toggleCanalOcr(c) {
await api.put(`/app/umind/canales/${c.ID}`, {
activo: c.activo, credenciales: {},
usar_whisper_audio: c.usar_whisper_audio, usar_ocr_imagenes: !c.usar_ocr_imagenes,
})
await cargarCanales()
}
@@ -497,6 +520,16 @@ onMounted(async () => {
<button class="text-red-500 hover:text-red-700" @click="eliminarCanal(c)">Eliminar</button>
</div>
</div>
<div class="flex gap-4 mt-2 text-xs">
<label class="flex items-center gap-1.5 text-gray-600 dark:text-gray-300 cursor-pointer">
<input type="checkbox" :checked="c.usar_whisper_audio" @change="toggleCanalWhisper(c)" class="rounded border-gray-300 dark:border-gray-700 text-brand focus:ring-brand" />
Transcribir audios (Whisper)
</label>
<label class="flex items-center gap-1.5 text-gray-600 dark:text-gray-300 cursor-pointer">
<input type="checkbox" :checked="c.usar_ocr_imagenes" @change="toggleCanalOcr(c)" class="rounded border-gray-300 dark:border-gray-700 text-brand focus:ring-brand" />
Leer texto de imágenes (OCR)
</label>
</div>
<p class="text-xs text-gray-500 dark:text-gray-400 mt-1 break-all">
Webhook: <code class="bg-gray-100 dark:bg-gray-800 px-1 rounded">{{ c.webhook_url }}</code>
</p>
@@ -542,6 +575,16 @@ onMounted(async () => {
<input v-model="canalForm.verify_token" required class="w-full border border-gray-300 dark:border-gray-700 bg-white dark:bg-gray-800 text-gray-800 dark:text-gray-100 rounded-lg px-3 py-2 text-sm" />
</div>
</template>
<div class="flex flex-col gap-2 pt-1">
<label class="flex items-center gap-2 text-sm text-gray-600 dark:text-gray-300 cursor-pointer">
<input type="checkbox" v-model="canalForm.usar_whisper_audio" class="rounded border-gray-300 dark:border-gray-700 text-brand focus:ring-brand" />
Transcribir audios con Whisper
</label>
<label class="flex items-center gap-2 text-sm text-gray-600 dark:text-gray-300 cursor-pointer">
<input type="checkbox" v-model="canalForm.usar_ocr_imagenes" class="rounded border-gray-300 dark:border-gray-700 text-brand focus:ring-brand" />
Leer texto de imágenes con OCR
</label>
</div>
<div class="flex justify-end gap-2 pt-2">
<button type="button" class="px-4 py-2 text-sm text-gray-500 dark:text-gray-400" @click="showCanalForm = false">Cancelar</button>
<button type="submit" class="bg-brand hover:bg-brand-dark text-white text-sm font-medium px-4 py-2 rounded-lg">Guardar</button>