Bot de Telegram de transcripción

Crea un bot de Telegram que transcriba mensajes de audio y vídeo en más de 90 idiomas con TypeScript, Deno y Supabase Edge Functions.

Guía práctica · Parte de que ya has completado la guía de inicio rápido de Voz a Texto y cuentas con un token de bot de Telegram y una cuenta de Supabase.

Introducción

En este tutorial aprenderás a crear un bot de Telegram que transcribe mensajes de audio y vídeo en más de 90 idiomas con TypeScript y el modelo Scribe de ElevenLabs a través de la API de voz a texto.

Requisitos

Configuración

Registra un bot de Telegram

Usa BotFather para crear un nuevo bot de Telegram. Ejecuta el comando /newbot y sigue las instrucciones para crear un bot. Al final, recibirás el token secreto de tu bot. Guárdalo de forma segura para el siguiente paso.

BotFather

Crea un proyecto de Supabase localmente

Después de instalar la CLI de Supabase, ejecuta el siguiente comando para crear un nuevo proyecto de Supabase localmente:

supabase init

Crea una tabla de base de datos para registrar los resultados de transcripción

A continuación, crea una tabla de base de datos para registrar los resultados de transcripción:

supabase migrations new init

Esto creará un nuevo archivo de migración en el directorio supabase/migrations. Abre el archivo y añade el siguiente SQL:

supabase/migrations/init.sql
CREATE TABLE IF NOT EXISTS transcription_logs (
id BIGSERIAL PRIMARY KEY,
file_type VARCHAR NOT NULL,
duration INTEGER NOT NULL,
chat_id BIGINT NOT NULL,
message_id BIGINT NOT NULL,
username VARCHAR,
transcript TEXT,
language_code VARCHAR,
created_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP,
error TEXT
);
ALTER TABLE transcription_logs ENABLE ROW LEVEL SECURITY;

Crea una Edge Function de Supabase para gestionar las solicitudes de webhook de Telegram

A continuación, crea una Edge Function para gestionar las solicitudes de webhook de Telegram:

supabase functions new scribe-bot

Si usas VS Code o Cursor, selecciona y cuando la CLI te pregunte “Generate VS Code settings for Deno? [y/N]”.

Configura las variables de entorno

En el directorio supabase/functions, crea un archivo .env y añade las siguientes variables:

supabase/functions/.env
# Find / create an API key at https://elevenlabs.io/app/settings/api-keys
ELEVENLABS_API_KEY=your_api_key
# The bot token you received from the BotFather.
TELEGRAM_BOT_TOKEN=your_bot_token
# A random secret chosen by you to secure the function.
FUNCTION_SECRET=random_secret

Dependencias

El proyecto usa varias dependencias:

  • El framework de código abierto grammY para gestionar las solicitudes de webhook de Telegram.
  • La biblioteca @supabase/supabase-js para interactuar con la base de datos de Supabase.
  • El SDK de JavaScript de ElevenLabs para interactuar con la API de voz a texto.

Como las Edge Functions de Supabase usan el runtime de Deno, no necesitas instalar las dependencias; puedes importarlas mediante el prefijo npm:.

Programa el bot de Telegram

En el archivo scribe-bot/index.ts que acabas de crear, añade el siguiente código:

supabase/functions/scribe-bot/index.ts
import { Bot, webhookCallback } from "https://deno.land/x/grammy@v1.34.0/mod.ts";
import "jsr:@supabase/functions-js/edge-runtime.d.ts";
import { createClient } from "jsr:@supabase/supabase-js@2";
import { ElevenLabsClient } from "npm:elevenlabs@1.50.5";
console.log(`Function "elevenlabs-scribe-bot" up and running!`);
const elevenlabs = new ElevenLabsClient({
apiKey: Deno.env.get("ELEVENLABS_API_KEY") || "",
});
const supabase = createClient(
Deno.env.get("SUPABASE_URL") || "",
Deno.env.get("SUPABASE_SERVICE_ROLE_KEY") || ""
);
async function scribe({
fileURL,
fileType,
duration,
chatId,
messageId,
username,
}: {
fileURL: string;
fileType: string;
duration: number;
chatId: number;
messageId: number;
username: string;
}) {
let transcript: string | null = null;
let languageCode: string | null = null;
let errorMsg: string | null = null;
try {
const sourceFileArrayBuffer = await fetch(fileURL).then((res) => res.arrayBuffer());
const sourceBlob = new Blob([sourceFileArrayBuffer], {
type: fileType,
});
const scribeResult = await elevenlabs.speechToText.convert({
file: sourceBlob,
model_id: "scribe_v2",
tag_audio_events: false,
});
transcript = scribeResult.text;
languageCode = scribeResult.language_code;
// Reply to the user with the transcript
await bot.api.sendMessage(chatId, transcript, {
reply_parameters: { message_id: messageId },
});
} catch (error) {
errorMsg = error.message;
console.log(errorMsg);
await bot.api.sendMessage(chatId, "Sorry, there was an error. Please try again.", {
reply_parameters: { message_id: messageId },
});
}
// Write log to Supabase.
const logLine = {
file_type: fileType,
duration,
chat_id: chatId,
message_id: messageId,
username,
language_code: languageCode,
error: errorMsg,
};
console.log({ logLine });
await supabase.from("transcription_logs").insert({ ...logLine, transcript });
}
const telegramBotToken = Deno.env.get("TELEGRAM_BOT_TOKEN");
const bot = new Bot(telegramBotToken || "");
const startMessage = `Welcome to the ElevenLabs Scribe Bot\\! I can transcribe speech in 90\\+ languages with super high accuracy\\!
\nTry it out by sending or forwarding me a voice message, video, or audio file\\!
\n[Learn more about Scribe](https://elevenlabs.io/speech-to-text) or [build your own bot](https://elevenlabs.io/developers/guides/cookbooks/speech-to-text/telegram-bot)\\!
`;
bot.command("start", (ctx) => ctx.reply(startMessage.trim(), { parse_mode: "MarkdownV2" }));
bot.on([":voice", ":audio", ":video"], async (ctx) => {
try {
const file = await ctx.getFile();
const fileURL = `https://api.telegram.org/file/bot${telegramBotToken}/${file.file_path}`;
const fileMeta = ctx.message?.video ?? ctx.message?.voice ?? ctx.message?.audio;
if (!fileMeta) {
return ctx.reply("No video|audio|voice metadata found. Please try again.");
}
// Run the transcription in the background.
EdgeRuntime.waitUntil(
scribe({
fileURL,
fileType: fileMeta.mime_type!,
duration: fileMeta.duration,
chatId: ctx.chat.id,
messageId: ctx.message?.message_id!,
username: ctx.from?.username || "",
})
);
// Reply to the user immediately to let them know we received their file.
return ctx.reply("Received. Scribing...");
} catch (error) {
console.error(error);
return ctx.reply(
"Sorry, there was an error getting the file. Please try again with a smaller file!"
);
}
});
const handleUpdate = webhookCallback(bot, "std/http");
Deno.serve(async (req) => {
try {
const url = new URL(req.url);
if (url.searchParams.get("secret") !== Deno.env.get("FUNCTION_SECRET")) {
return new Response("not allowed", { status: 405 });
}
return await handleUpdate(req);
} catch (err) {
console.error(err);
}
});

Análisis detallado del código

Hay algunos aspectos importantes que conviene destacar sobre el código. Vamos a repasarlo paso a paso.

1

Gestión de la solicitud entrante

Para gestionar la solicitud entrante, usa el controlador Deno.serve. El controlador comprueba si la solicitud tiene el secreto correcto y, después, la pasa a la función handleUpdate.

const handleUpdate = webhookCallback(bot, 'std/http');
Deno.serve(async (req) => {
try {
const url = new URL(req.url);
if (url.searchParams.get('secret') !== Deno.env.get('FUNCTION_SECRET')) {
return new Response('not allowed', { status: 405 });
}
return await handleUpdate(req);
} catch (err) {
console.error(err);
}
});
2

Gestiona mensajes de voz, audio y vídeo

El framework grammY ofrece una forma práctica de filtrar tipos de mensajes específicos. En este caso, el bot escucha mensajes de voz, audio y vídeo.

Mediante el contexto de la solicitud, el bot extrae los metadatos del archivo y usa EdgeRuntime.waitUntil de las tareas en segundo plano de Supabase para ejecutar la transcripción en segundo plano.

Así puedes ofrecer una respuesta inmediata al usuario y gestionar la transcripción del archivo en segundo plano.

bot.on([':voice', ':audio', ':video'], async (ctx) => {
try {
const file = await ctx.getFile();
const fileURL = `https://api.telegram.org/file/bot${telegramBotToken}/${file.file_path}`;
const fileMeta = ctx.message?.video ?? ctx.message?.voice ?? ctx.message?.audio;
if (!fileMeta) {
return ctx.reply('No video|audio|voice metadata found. Please try again.');
}
// Run the transcription in the background.
EdgeRuntime.waitUntil(
scribe({
fileURL,
fileType: fileMeta.mime_type!,
duration: fileMeta.duration,
chatId: ctx.chat.id,
messageId: ctx.message?.message_id!,
username: ctx.from?.username || '',
})
);
// Reply to the user immediately to let them know we received their file.
return ctx.reply('Received. Scribing...');
} catch (error) {
console.error(error);
return ctx.reply(
'Sorry, there was an error getting the file. Please try again with a smaller file!'
);
}
});
3

Transcripción con la API de ElevenLabs

Por último, en el proceso en segundo plano, el bot usa el SDK de JavaScript de ElevenLabs para transcribir el archivo. Cuando se completa la transcripción, el bot responde al usuario con la transcripción y escribe una entrada de registro en la base de datos de Supabase con supabase-js.

const elevenlabs = new ElevenLabsClient({
apiKey: Deno.env.get('ELEVENLABS_API_KEY') || '',
});
const supabase = createClient(
Deno.env.get('SUPABASE_URL') || '',
Deno.env.get('SUPABASE_SERVICE_ROLE_KEY') || ''
);
async function scribe({
fileURL,
fileType,
duration,
chatId,
messageId,
username,
}: {
fileURL: string;
fileType: string;
duration: number;
chatId: number;
messageId: number;
username: string;
}) {
let transcript: string | null = null;
let languageCode: string | null = null;
let errorMsg: string | null = null;
try {
const sourceFileArrayBuffer = await fetch(fileURL).then((res) => res.arrayBuffer());
const sourceBlob = new Blob([sourceFileArrayBuffer], {
type: fileType,
});
const scribeResult = await elevenlabs.speechToText.convert({
file: sourceBlob,
model_id: 'scribe_v2',
tag_audio_events: false,
});
transcript = scribeResult.text;
languageCode = scribeResult.language_code;
// Reply to the user with the transcript
await bot.api.sendMessage(chatId, transcript, {
reply_parameters: { message_id: messageId },
});
} catch (error) {
errorMsg = error.message;
console.log(errorMsg);
await bot.api.sendMessage(chatId, 'Sorry, there was an error. Please try again.', {
reply_parameters: { message_id: messageId },
});
}
// Write log to Supabase.
const logLine = {
file_type: fileType,
duration,
chat_id: chatId,
message_id: messageId,
username,
language_code: languageCode,
error: errorMsg,
};
console.log({ logLine });
await supabase.from('transcription_logs').insert({ ...logLine, transcript });
}

Despliega en Supabase

Si aún no lo has hecho, crea una cuenta de Supabase en database.new y vincula el proyecto local a tu cuenta de Supabase:

supabase link

Aplica las migraciones de la base de datos

Ejecuta el siguiente comando para aplicar las migraciones de la base de datos desde el directorio supabase/migrations:

supabase db push

Ve al editor de tablas en tu panel de Supabase; deberías ver una tabla transcription_logs vacía.

Tabla vacía

Por último, ejecuta el siguiente comando para desplegar la Edge Function:

supabase functions deploy --no-verify-jwt scribe-bot

Ve a la vista de Edge Functions en tu panel de Supabase; deberías ver la función scribe-bot desplegada. Anota la URL de la función, ya que la necesitarás más adelante. Debería tener un aspecto similar a https://<project-ref>.functions.supabase.co/scribe-bot.

Edge Function desplegada

Configura el webhook

Configura la URL de webhook de tu bot como https://<PROJECT_REFERENCE>.functions.supabase.co/telegram-bot (sustituye <...> por los valores correspondientes). Para ello, solo tienes que realizar una solicitud GET a la siguiente URL (por ejemplo, en tu navegador):

https://api.telegram.org/bot<TELEGRAM_BOT_TOKEN>/setWebhook?url=https://<PROJECT_REFERENCE>.supabase.co/functions/v1/scribe-bot?secret=<FUNCTION_SECRET>

Ten en cuenta que FUNCTION_SECRET es el secreto que configuraste en tu archivo .env.

Configurar webhook

Configura los secretos de la función

Ahora que ya tienes todos los secretos configurados localmente, puedes ejecutar el siguiente comando para configurar los secretos en tu proyecto de Supabase:

supabase secrets set --env-file supabase/functions/.env

Prueba el bot

Por último, puedes probar el bot enviándole un mensaje de voz, un archivo de audio o un vídeo.

Probar el bot

Cuando veas la transcripción como respuesta, vuelve al editor de tablas de tu panel de Supabase; deberías ver una nueva fila en tu tabla transcription_logs.

Nueva fila en la tabla

Próximos pasos