Edge Functions

Transcription Telegram Bot

Build a Telegram bot that transcribes audio and video messages in 99 languages using TypeScript with Deno in Supabase Edge Functions.


Introduction

In this tutorial you will learn how to build a Telegram bot that transcribes audio and video messages in 99 languages using TypeScript and the ElevenLabs Scribe model via the speech to text API.

To check out what the end result will look like, you can test out the t.me/ElevenLabsScribeBot

Requirements

Setup

Register a Telegram bot

Use the BotFather to create a new Telegram bot. Run the /newbot command and follow the instructions to create a new bot. At the end, you will receive your secret bot token. Note it down securely for the next step.

BotFather

Create a Supabase project locally

After installing the Supabase CLI, run the following command to create a new Supabase project locally:


_10
supabase init

Create a database table to log the transcription results

Next, create a new database table to log the transcription results:


_10
supabase migrations new init

This will create a new migration file in the supabase/migrations directory. Open the file and add the following SQL:

supabase/migrations/init.sql

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

Create a Supabase Edge Function to handle Telegram webhook requests

Next, create a new Edge Function to handle Telegram webhook requests:


_10
supabase functions new scribe-bot

If you're using VS Code or Cursor, select y when the CLI prompts "Generate VS Code settings for Deno? [y/N]"!

Set up the environment variables

Within the supabase/functions directory, create a new .env file and add the following variables:

supabase/functions/.env

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

Dependencies

The project uses a couple of dependencies:

Since Supabase Edge Function uses the Deno runtime, you don't need to install the dependencies, rather you can import them via the npm: prefix.

Code the Telegram bot

In your newly created scribe-bot/index.ts file, add the following code:

supabase/functions/scribe-bot/index.ts

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

Deploy to Supabase

If you haven't already, create a new Supabase account at database.new and link the local project to your Supabase account:


_10
supabase link

Apply the database migrations

Run the following command to apply the database migrations from the supabase/migrations directory:


_10
supabase db push

Navigate to the table editor in your Supabase dashboard and you should see and empty transcription_logs table.

Empty table

Lastly, run the following command to deploy the Edge Function:


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

Navigate to the Edge Functions view in your Supabase dashboard and you should see the scribe-bot function deployed. Make a note of the function URL as you'll need it later, it should look something like https://<project-ref>.functions.supabase.co/scribe-bot.

Edge Function deployed

Set up the webhook

Set your bot's webhook URL to https://<PROJECT_REFERENCE>.functions.supabase.co/telegram-bot (Replacing <...> with respective values). In order to do that, run a GET request to the following URL (in your browser, for example):


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

Note that the FUNCTION_SECRET is the secret you set in your .env file.

Set webhook

Set the function secrets

Now that you have all your secrets set locally, you can run the following command to set the secrets in your Supabase project:


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

Test the bot

Finally you can test the bot by sending it a voice message, audio or video file.

Test the bot

After you see the transcript as a reply, navigate back to your table editor in the Supabase dashboard and you should see a new row in your transcription_logs table.

New row in table