트랜스크립션 Telegram 봇

Supabase Edge Functions에서 Deno와 TypeScript를 사용해 90개 이상의 언어로 오디오 및 비디오 메시지를 트랜스크립션하는 Telegram 봇을 구축하세요.

사용 방법 가이드 · 음성 텍스트 변환 빠른 시작을 완료했으며 Telegram 봇 토큰과 Supabase 계정이 있다고 가정합니다.

소개

이 튜토리얼에서는 음성 텍스트 변환 API를 통해 TypeScript와 ElevenLabs Scribe 모델을 사용하여 90개 이상의 언어로 오디오 및 비디오 메시지를 텍스트로 변환하는 Telegram 봇을 만드는 방법을 알아봅니다.

요구 사항

설정

Telegram 봇 등록

BotFather를 사용하여 새 Telegram 봇을 만드세요. /newbot 명령을 실행하고 안내에 따라 새 봇을 만듭니다. 완료하면 비밀 봇 토큰을 받게 됩니다. 다음 단계에서 사용할 수 있도록 안전하게 기록해 두세요.

BotFather

로컬에서 Supabase 프로젝트 만들기

Supabase CLI를 설치한 후, 다음 명령을 실행하여 로컬에 새 Supabase 프로젝트를 만드세요.

supabase init

변환 결과를 기록할 데이터베이스 테이블 만들기

다음으로, 변환 결과를 기록할 새 데이터베이스 테이블을 만드세요.

supabase migrations new init

그러면 supabase/migrations 디렉터리에 새 마이그레이션 파일이 생성됩니다. 파일을 열고 다음 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;

Telegram 웹훅 요청을 처리하는 Supabase Edge Function 만들기

다음으로, Telegram 웹훅 요청을 처리할 새 Edge Function을 만드세요.

supabase functions new scribe-bot

VS Code 또는 Cursor를 사용한다면 CLI에서 “Generate VS Code settings for Deno? [y/N]“라고 물을 때 y를 선택하세요.

환경 변수 설정

supabase/functions 디렉터리에서 새 .env 파일을 만들고 다음 변수를 추가하세요.

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

종속성

프로젝트에서는 몇 가지 종속성을 사용합니다.

  • Telegram 웹훅 요청을 처리하기 위한 오픈 소스 grammY Framework
  • Supabase 데이터베이스와 상호작용하기 위한 @supabase/supabase-js 라이브러리
  • 음성 텍스트 변환 API와 상호작용하기 위한 ElevenLabs JavaScript SDK

Supabase Edge Function은 Deno 런타임을 사용하므로 종속성을 설치할 필요 없이 npm: 접두사를 통해 가져올 수 있습니다.

Telegram 봇 코드 작성

새로 만든 scribe-bot/index.ts 파일에 다음 코드를 추가하세요.

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);
}
});

코드 자세히 살펴보기

코드에서 주목할 만한 몇 가지 사항이 있습니다. 하나씩 살펴보겠습니다.

1

수신 요청 처리

수신 요청을 처리하려면 Deno.serve 핸들러를 사용하세요. 핸들러는 요청에 올바른 시크릿이 있는지 확인한 후 요청을 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

음성, 오디오 및 비디오 메시지 처리

grammY 프레임워크는 특정 메시지 유형을 필터링하는 편리한 방법을 제공합니다. 이 경우 봇은 음성, 오디오, 비디오 메시지를 수신합니다.

요청 컨텍스트를 사용하여 봇은 파일 메타데이터를 추출한 다음 Supabase Background Tasks의 EdgeRuntime.waitUntil을 사용해 백그라운드에서 변환을 실행합니다.

이렇게 하면 사용자에게 즉시 응답을 제공하면서 백그라운드에서 파일 변환을 처리할 수 있습니다.

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

ElevenLabs API로 변환

마지막으로 백그라운드 워커에서 봇은 ElevenLabs JavaScript SDK를 사용해 파일을 텍스트로 변환합니다. 변환이 완료되면 봇은 사용자에게 텍스트 변환 결과로 답장하고 supabase-js를 사용해 Supabase 데이터베이스에 로그 항목을 작성합니다.

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 });
}

Supabase에 배포

아직 Supabase 계정이 없다면 database.new에서 새 계정을 만들고 로컬 프로젝트를 Supabase 계정에 연결하세요.

supabase link

데이터베이스 마이그레이션 적용

다음 명령을 실행하여 supabase/migrations 디렉터리의 데이터베이스 마이그레이션을 적용하세요.

supabase db push

Supabase 대시보드의 테이블 편집기로 이동하면 비어 있는 transcription_logs 테이블이 표시됩니다.

빈 테이블

마지막으로 다음 명령을 실행하여 Edge Function을 배포하세요.

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

Supabase 대시보드의 Edge Functions 보기로 이동하면 scribe-bot 함수가 배포된 것을 확인할 수 있습니다. 나중에 필요하므로 함수 URL을 기록해 두세요. https://<project-ref>.functions.supabase.co/scribe-bot와 비슷한 형태입니다.

배포된 Edge Function

웹훅 설정

봇의 웹훅 URL을 https://<PROJECT_REFERENCE>.functions.supabase.co/telegram-bot로 설정하세요(<_..._>를 해당 값으로 대체). 이를 위해 다음 URL에 GET 요청을 실행하면 됩니다(예: 브라우저에서).

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

FUNCTION_SECRET은 .env 파일에서 설정한 시크릿입니다.

웹훅 설정

함수 시크릿 설정

이제 모든 시크릿을 로컬에서 설정했으므로 다음 명령을 실행하여 Supabase 프로젝트에 시크릿을 설정할 수 있습니다.

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

봇 테스트

마지막으로 봇에 음성 메시지, 오디오 또는 비디오 파일을 전송하여 테스트할 수 있습니다.

봇 테스트

답장으로 텍스트 변환 결과가 표시되면 Supabase 대시보드의 테이블 편집기로 돌아가세요. transcription_logs 테이블에 새 행이 표시됩니다.

테이블의 새 행

다음 단계