- PVSM.RU - https://www.pvsm.ru -

Telegram-бот, который умеет слушать: разработка на grammY

Telegram-бот, который умеет слушать: разработка на grammY - 1

Представьте: собеседник отправляет голосовое сообщение на пять минут, а вы не можете отвлечься и прослушать все от начала до конца? Что делать? Максим, ведущий канала RED Group, подошел к вопросу творчески и показал, как на базе grammY и SpeechService в NestJS разработать бота, который будет слушать и структурировать по таймкодам голосовые сообщения.

Инструкция будет полезна новичкам, которые только погружаются в работу с Telegram Bot API с помощью JavaScript. Кроме того, в конце материала мы разберем, как задеплоить готового бота на сервер [1], чтобы он работал вне зависимости от локальной машины. Подробности под катом!

Инициализация проекта


Для начала переходим в терминал и пишем команду:

nest new test_voice_bot

Далее выбираете пакетный менеджер или оставляете тот, что по умолчанию.

Telegram-бот, который умеет слушать: разработка на grammY - 2

После создания проекта откройте его в удобном редакторе коде — например, в VSCode.

Telegram-бот, который умеет слушать: разработка на grammY - 3


Разработка бота

Регистрация API

1. Открываем Telegram и в поиске вбиваем @BotFather [2]. Отправляем ему команду /newbot.

2. Вписываем уникальный username и получаем ключ — токен для управления ботом. Далее вы можете кастомизировать его профиль: установить имя, аватарку и описание. Как это делать — можно понять, если ознакомитесь с доступным в чате списком команд.

3. Далее необходимо получить доступ к сервисам OpenAI. Для этого переходим на официальную страницу [3] и регистрируемся. Процесс подробно описан в документации [4].

4. Создаем в корне проекта файл .env с переменными окружения, куда вписываем токен от Telegram-бота и ключ от API-сервисов OpenAI:

Telegram-бот, который умеет слушать: разработка на grammY - 4

5. Открываем терминал и устанавливаем необходимые зависимости:

bun add @grammyjs/nestjs grammy axios @nestjs/config

bun add -D @types/express

Первая команда устанавливает основные библиотеки для работы с Telegram-ботами и конфигурацией. Среди них:

  • @grammyjs/nestjs — интеграция библиотеки Grammy (для создания Telegram-ботов) с фреймворком NestJS;
  • grammy — основная библиотека для работы с Telegram Bot API;
  • axios — популярная библиотека для выполнения HTTP-запросов;
  • @nestjs/config — модуль для управления конфигурациями в приложениях на NestJS.

Вторая — добавляет типы для Express.js как зависимость для разработки. Это нужно для поддержки TypeScript и автодополнения при работе с Express.

Создание базовых вспомогательных файлов

src/constants.ts

В данном файле будут размещены пути до внешних API.

export const TELEGRAM_API = 'https://api.telegram.org'
export const OPENAI_API = 'https://api.openai.com/v1'

src/prompts/timestamp.prompts.ts

Далее нам необходимо подготовить шаблон промтов, которые будет использовать Telegram-бот для расшифровки поступающих голосовых сообщений через сервисы OpenAI.

/**
* System-промпт: описывает поведение и правила для модели
*/
export const TIMESTAMP_SYSTEM_PROMPT = `
Ты — ассистент, который составляет тайм-коды к голосовым сообщениям.
У тебя есть расшифровка текста, разбитая на временные блоки.
Твоя задача — выбрать из каждого блока ОДНУ ключевую идею (если она есть)
и указать её с точным тайм-кодом начала блока.

Правила:
- Не выдумывай тем, которых не было в тексте.
- Не объединяй идеи из разных блоков.
- Не используй больше 10 пунктов.
- Не добавляй "Заключение", "Финал", если этого не было в речи.
- Сохраняй реальный тайминг — не позже времени блока.
- Пропускай блок, если в нём нет ничего важного.

Формат:
00:00 - Введение
00:35 - Почему важно планировать день
01:10 - Проблема прокрастинации
`

/**
* Генерирует пользовательский промпт на основе подготовленного текста
*/
export const buildTimestampUserPrompt = (preparedText: string): string => `
Вот текст, расшифрованный из голосового сообщения. Каждый блок соответствует примерно 30-40 секундам речи.
Для каждого блока выдели ключевую идею (если она есть), строго по времени начала блока.

Текст:
${preparedText}
`

В файлах далее при вставке могут возникать ошибки, так как мы еще не создали самые ключевые файлы и они их просто не найдут.

src/telegram/telegram.module.ts

Здесь мы подключаем библиотеки для работы с Telegram и для доступа к .env.

import { NestjsGrammyModule } from '@grammyjs/nestjs'
import { Module } from '@nestjs/common'
import { ConfigModule, ConfigService } from '@nestjs/config'
import { AIService } from 'src/services/ai.service'
import { SpeechService } from 'src/services/speech.service'
import { TelegramUpdate } from './telegram.update'

@Module({
	imports: [
		ConfigModule,
		NestjsGrammyModule.forRootAsync({
			imports: [ConfigModule],
			inject: [ConfigService],
			useFactory: async (configService: ConfigService) => ({
				token: configService.get<string>('TELEGRAM_BOT_TOKEN')
			})
		})
	],
	providers: [TelegramUpdate, SpeechService, AIService]
})
export class TelegramModule {}

src/app.module.ts

Теперь отредактируем текущий корневой файл. В нем мы просто подключаем дочерние модули. Среди них есть и те, что мы ранее создали.

import { Module } from '@nestjs/common'
import { ConfigModule } from '@nestjs/config'
import { TelegramModule } from './telegram/telegram.module'

@Module({
	imports: [ConfigModule.forRoot(), TelegramModule]
})
export class AppModule {}

Реализация распознавания аудио и речи

Создадим новый файл src/services/speech.service.ts — он будет содержать SpeechService в NestJS. Если коротко, то он делает всю сложную работу за нас и умеет расшифровывать голосовые сообщения из Telegram с помощью OpenAI Whisper. Для этого сервис:

  1. загружает голосовой файл с Telegram по переданному пути;
  2. создает FormData с этим файлом для API OpenAI;
  3. отправляет запрос на whisper-1 модель OpenAI для транскрибации;
  4. возвращает полученный текст.

Все ключи (TELEGRAM_BOT_TOKEN, OPENAI_API_KEY) берутся из переменных окружения через ConfigService (файла .env).

import { Injectable } from '@nestjs/common'
import { ConfigService } from '@nestjs/config'
import axios from 'axios'
import * as FormData from 'form-data'
import { OPENAI_API, TELEGRAM_API } from '../constants'

@Injectable()
export class SpeechService {
	private readonly botToken: string
	private readonly openaiApiKey: string

	constructor(private readonly configService: ConfigService) {
		this.botToken = this.configService.get<string>('TELEGRAM_BOT_TOKEN')
		this.openaiApiKey = this.configService.get<string>('OPENAI_API_KEY')
	}

	async transcribeVoice(filePath: string): Promise<string> {
		const fileUrl = `${TELEGRAM_API}/file/bot${this.botToken}/${filePath}`
		const fileResponse = await axios.get(fileUrl, { responseType: 'stream' })

		const formData = new FormData()
		formData.append('file', fileResponse.data, { filename: 'audio.ogg' })
		formData.append('model', 'whisper-1')

		const response = await axios.post<{ text: string }>(
			`${OPENAI_API}/audio/transcriptions`,
			formData,
			{
				headers: {
					Authorization: `Bearer ${this.openaiApiKey}`,
					...formData.getHeaders()
				}
			}
		)

		return response.data.text
	}
}

Генерация таймкодов

Далее создадим новый файл src/services/ai.service.ts — он будет содержать сервис AIService из NestJS, который с помощью OpenAI GPT-4o генерирует улучшенные таймкоды по тексту и длительности аудио. И снова: все уже сделано за нас!

Если коротко, сервис:

  1. делит текст на десять равных блоков по словам и времени;
  2. формирует вид [MM:SS] текст для каждого блока;
  3. отправляет эти блоки в GPT с системным промтом;
  4. получает улучшенные таймкоды от модели;
  5. считает примерную стоимость запроса (по токенам);
  6. возвращает результат и стоимость.

import { Injectable } from '@nestjs/common'
import { ConfigService } from '@nestjs/config'
import axios from 'axios'
import { OPENAI_API } from '../constants'
import {
	TIMESTAMP_SYSTEM_PROMPT,
	buildTimestampUserPrompt
} from '../prompts/timestamp.prompts'

interface IOpenAIResponse {
	choices: {
		message: {
			content: string
		}
	}[]
	usage: {
		prompt_tokens: number
		completion_tokens: number
	}
}

@Injectable()
export class AIService {
	private readonly openaiApiKey: string

	constructor(private readonly configService: ConfigService) {
		this.openaiApiKey = this.configService.get<string>('OPENAI_API_KEY')
	}

	async generateTimestamps(
		text: string,
		audioDurationSec: number
	): Promise<{ timestamps: string; cost: string }> {
		const maxSegments = 10 // Максимум логических блоков для разметки

		// Разбиваем весь текст на отдельные слова
		const words = text.split(/s+/)

		// Сколько слов и секунд должно быть в каждом блоке
		// Округление вверх — чтобы не потерять слова: 9.6 → 10
		const wordsPerSegment = Math.ceil(words.length / maxSegments)
		// Округление вниз — чтобы не «вывалиться» за длину аудио: 96.5 → 96
		const secondsPerSegment = Math.floor(audioDurationSec / maxSegments)

		// Собираем массив временных блоков текста
		const segments: { time: string; content: string }[] = []

		for (let i = 0; i < maxSegments; i++) {
			// Вычисляем метку времени начала блока
			const fromSec = i * secondsPerSegment
			// padStart - Форматирует числа вида 5 → 05
			// Чтобы мы всегда получали формат MM:SS, а не M:S.
			const fromMin = String(Math.floor(fromSec / 60)).padStart(2, '0')
			const fromSecRest = String(fromSec % 60).padStart(2, '0')
			const time = `${fromMin}:${fromSecRest}`

			// Вырезаем часть текста, относящуюся к текущему блоку
			const start = i * wordsPerSegment
			const end = start + wordsPerSegment
			const content = words.slice(start, end).join(' ')

			// Добавляем только непустые блоки
			if (content.trim()) {
				segments.push({ time, content })
			}
		}

		// Объединяем блоки в формат вида: [00:00] текст
		const preparedText = segments
			.map(s => `[${s.time}] ${s.content}`)
			.join('nn')

		// Готовим сообщения для GPT: правила и ввод
		const systemMessage = TIMESTAMP_SYSTEM_PROMPT
		const userMessage = buildTimestampUserPrompt(preparedText)

		// Отправляем запрос в OpenAI Chat API
		const response = await axios.post<IOpenAIResponse>(
			`${OPENAI_API}/chat/completions`,
			{
				model: 'gpt-4o-mini',
				messages: [
					{ role: 'system', content: systemMessage }, // задаёт поведение
					{ role: 'user', content: userMessage } // передаёт текст с блоками
				],
				temperature: 0.3, // Насколько "свободно" думает модель (0 — строго, 1 — креативно)
				max_tokens: 300 // Ограничиваем объем ответа, чтобы не получить слишком длинный список
			},
			{
				headers: {
					Authorization: `Bearer ${this.openaiApiKey}`
				}
			}
		)

		// Извлекаем ответ GPT
		const result = response.data.choices[0].message.content

		// Статистика использования токенов (нужна для расчёта стоимости)
		const usage = response.data.usage

		// Подсчет примерной стоимости запроса
		const inputCost = (usage.prompt_tokens / 1_000_000) * 0.15
		const outputCost = (usage.completion_tokens / 1_000_000) * 0.6
		const total = inputCost + outputCost

		const costText = `💸 Стоимость генерации: ~$${total.toFixed(4)}`

		return {
			timestamps: result, // Сами тайм-коды
			cost: costText // Стоимость генерации
		}
	}
}

Разработка основных функций

Заведем новый файл src/telegram/telegram.update.ts — в нем будет реализован Telegram-обработчик TelegramUpdate на базе @grammyjs/nestjs. Он ожидает и принимает голосовые сообщения. После — отвечает с расшифровкой и таймкодами. А именно:

  1. Обрабатывает команду /start и голосовые сообщения от пользователя в Telegram.
  2. Показывает красивый прогресс обработки (обновляется каждые 2–3 секунды).
  3. Получает аудио из Telegram и расшифровывает его через SpeechService.
  4. Отправляет текст в AIService для генерации тайм-кодов.
  5. Возвращает результат и стоимость генерации пользователю.

import { Ctx, InjectBot, On, Start, Update } from '@grammyjs/nestjs'
import { Injectable } from '@nestjs/common'
import { ConfigService } from '@nestjs/config'
import { Api, Bot, Context } from 'grammy'
import { AIService } from '../services/ai.service'
import { SpeechService } from '../services/speech.service'

@Update() // Этот декоратор указывает, что класс слушает события от Telegram
@Injectable()
export class TelegramUpdate {
	private readonly botToken: string

	constructor(
		@InjectBot() private readonly bot: Bot<Context>, // Внедрение Telegram-бота
		private readonly speechService: SpeechService, // Сервис для расшифровки речи
		private readonly aiService: AIService, // Сервис для генерации тайм-кодов
		private readonly configService: ConfigService
	) {
		this.botToken = this.configService.get<string>('TELEGRAM_BOT_TOKEN')
	}

	@Start() // Обрабатывает команду /start
	async onStart(@Ctx() ctx: Context): Promise<void> {
		await ctx.reply(
			'👋 Привет! Отправь мне голосовое сообщение, и я расставлю тайм-коды.'
		)
	}

	@On('message:voice') // Обработка голосовых сообщений
	async onVoiceMessage(@Ctx() ctx: Context): Promise<void> {
		let progressMessageId: number | undefined
		let interval: NodeJS.Timeout | undefined
		let percent = 10 // Начальный процент прогресса

		try {
			const voice = ctx.msg.voice
			const duration = voice.duration

			// Получаем путь к файлу голосового сообщения
			const file = await ctx.getFile()

			// Показываем длительность голосового
			await ctx.reply(`🎤 Длина голосового сообщения: ${duration} сек.`)

			// Отправляем первое сообщение с прогрессом
			const progressMsg = await ctx.reply(this.renderProgress(percent))
			progressMessageId = progressMsg.message_id

			// ⏱ Эмулируем "оживший" прогресс — обновляем каждые 2 секунды
			interval = setInterval(
				async () => {
					if (percent < 90) {
						percent += 5
						await this.updateProgress(
							ctx.api,
							ctx.chat.id,
							progressMessageId,
							percent
						)
					}
				},
				duration > 300 ? 3000 : 2000
			)

			// Расшифровываем речь с помощью Whisper
			const transcription = await this.speechService.transcribeVoice(
				file.file_path
			)

			// Отправляем текст в OpenAI и получаем тайм-коды + стоимость
			const { timestamps, cost } = await this.aiService.generateTimestamps(
				transcription,
				duration
			)

			// Останавливаем обновление прогресса
			clearInterval(interval)
			await this.updateProgress(ctx.api, ctx.chat.id, progressMessageId, 100)

			// Отправляем результат
			await ctx.reply(
				`⏳ Тайм-коды:nn${timestamps}nn<i>🤖 Таймы генерирует нейросеть, через наш бот</i>`,
				{
					parse_mode: 'HTML'
				}
			)
			await ctx.reply(cost)
		} catch (error) {
			clearInterval(interval) // Останавливаем прогресс даже при ошибке
			console.error('Ошибка при обработке голосового:', error.message)
			await ctx.reply('⚠️ Ошибка при обработке голосового сообщения.')
		}
	}

	// Обновление прогресса (редактирует предыдущее сообщение)
	private async updateProgress(
		api: Api,
		chatId: number,
		messageId: number,
		percent: number
	) {
		await api.editMessageText(chatId, messageId, this.renderProgress(percent))
	}

	// Отрисовка прогресс-бара с заданным процентом
	private renderProgress(percent: number): string {
		const totalBlocks = 10 // Всего 10 ячеек в прогресс-баре
		const blockChar = '▒' // Символ, обозначающий "заполненную" ячейку прогресса

		// Вычисляем количество заполненных блоков на шкале
		const filledBlocks = Math.max(1, Math.round((percent / 100) * totalBlocks))

		/**
		 * 👉 Math.round(...) — округляет до ближайшего целого (например, 3.6 → 4)
		 * 👉 (percent / 100) * totalBlocks — переводим процент в количество блоков
		 * 👉 Math.max(1, ...) — гарантируем, что хотя бы 1 блок всегда будет показан (даже при 0%)
		 */

		const emptyBlocks = totalBlocks - filledBlocks // Остальные блоки считаем как "пустые"

		/**
		 * 👉 String.prototype.repeat(n) — повторяет символ n раз
		 * Пример: '▒'.repeat(4) = '▒▒▒▒'
		 * Таким образом формируем заполненную и пустую часть визуального прогресса
		 */

		// Собираем строку вида: 🔄 Прогресс: [▒▒▒▒░░░░░░] 40%
		return `🔄 Прогресс: [${blockChar.repeat(filledBlocks)}${'░'.repeat(emptyBlocks)}] ${percent}%`
	}
}

Отлично! Бота уже можно протестировать, запустив его одной из следующих команд:

bun start:dev

npm run start:dev

Чтобы бот работал круглосуточно и без перебоев, его необходимо развернуть на сервере. Один из удобных вариантов — облачная платформа Selectel [1], которая предоставляет виртуальные серверы в понятной панели управления.

Загрузка кода в репозиторий


Код готов, его можно перенести на облачный сервер с помощью утилиты scp, но более удобный вариант — загрузить через Git-репозиторий. Вы можете выбрать любую платформу, принципы везде одни и те же. Но мы коротко разберем процесс на примере самого популярного варианта — GitHub. Подробные инструкции — в отдельной статье [5].

1. Зарегистрируйтесь на платформе GitHub.

2. Нажмите на кнопку New, чтобы создать новый репозиторий [6].

3. Назовите репозиторий удобным образом. Рекомендуем поставить галочку напротив Add on README file, чтобы репозиторий не был пустым и вы сразу получили доступ к его файловой системе. Нажмите кнопку Create repository.

4. Мышкой перетащите файлы своего бота в область репозитория — и все загрузится в облако GitHub. Обратите внимание, что .env переносить нельзя — в нем записаны ключи для работы с API вашего бота.

Подготовка сервера

Создание сервера в панели управления

1. Первым шагом нужно зарегистрироваться в панели управления Selectel [7].

2. Далее в верхнем меню переходим во вкладку Продукты → Облачные серверы. Нажимаем на кнопку Создать сервер.

Telegram-бот, который умеет слушать: разработка на grammY - 5

3. Теперь нужно настроить конфигурацию. Регион, пул и название указывайте, исходя из своих предпочтений. Остальные параметры рекомендуем устанавливать следующим образом.

  • 1 vCPU и 2 GB RAM — этого достаточно для стабильной работы NestJS-приложения и обработки голосовых сообщений в фоне. Больше не нужно, так как бот не выполняет ресурсоемких задач постоянно.
  • 10 GB SSD — минимально необходимый объем для хранения ОС, логов, временных аудиофайлов и кэша.
  • Ubuntu 24 — этот дистрибутив легко настраивается и отлично совместим с большинством библиотек.
  • Публичный IP — нужен, чтобы Telegram мог отправлять запросы на наш сервер. Без него бот не будет получать сообщения.

4. Поскольку в качестве ОС мы выбрали Ubuntu 24, для подключения к серверу обязательно использовать SSH-ключ. О том, как его сгенерировать, можно узнать в инструкции [8].

5. Нажимаем на кнопку Создать сервер и ждем пару минут, пока статус не сменится на ACTIVE.

Telegram-бот, который умеет слушать: разработка на grammY - 6

6. Далее подключаемся к серверу по SSH. Для этого открываем терминал и вводим следующую команду:

ssh root@<ip-адрес>

Базовая настройка Ubuntu

1. После подключения к серверу, необходимо обновить список доступных пакетов и их версий из репозиториев, указанных в конфигурации системы. Это позволяет системе знать о последних версиях программ и обновлениях.

apt update && apt upgrade -y

2. Далее установим необходимые пакеты и Node.js:

apt install -y curl git unzip htop nano nginx

curl -L <a href="https://raw.githubusercontent.com/tj/n/master/bin/n">https://raw.githubusercontent.com/tj/n/master/bin/n</a> -o /usr/local/bin/n

chmod +x /usr/local/bin/n

3. Ставим Node.js latest version. Если вы не используете Docker, нужно установить такую же версию, как на вашем компьютере. Иначе могут возникнуть конфликты.

n lts

node -v npm -v

4. Устанавливаем Bun. Напомним, что это современный быстрый менеджер пакетов, инструмент для разработки и выполнения JavaScript- и TypeScript-проектов.

curl -fsSL https://bun.sh/install | bash
export BUN_INSTALL="$HOME/.bun”
export PATH="$BUN_INSTALL/bin:$PATH”

5. Также однозначно понадобится установить PM2 — это популярный менеджер процессов для Node.js-приложений. Он помогает обеспечить стабильную работу серверных приложений и упрощает их администрирование.

npm install -g pm2

На этом этапе все готово и установлено для деплоя.

Деплой бота на готовый сервер


1. Начнем с клонирования нашего GitHub-репозитория. Это довольно легко сделать, если сам репозиторий публичный. Достаточно ввести одну команду:

git clone <ссылка на репозиторий>

Если репозиторий приватный, понадобится ввести данные от своего аккаунта или предварительно добавить в профиль на GitHub ранее сгенерированный SSH-ключ. Процесс подробно описан в документации [9].

Telegram-бот, который умеет слушать: разработка на grammY - 7

2. Следующим шагом устанавливаем зависимости с помощью Bun:

bun install --production

3. Создаем в корне проекта файл .env и прописываем в нем полученные ранее TELEGRAM_BOT_TOKEN и OPENAI_API_KEY.

nano .env

4. Собираем проект с помощью Bun:

bun run build

5. Проект можно запустить. Но перед этим важно остановить бота на локальной машине, если он работает.

pm2 start dist/main.js --name="telegram-bot”

Telegram-бот, который умеет слушать: разработка на grammY - 8

6. Включаем автозапуск бота. Это полезно, например, на случай перезагрузки сервера.

pm2 save

Готово! Теперь наш бот круглосуточно работает на сервере. В любой момент можно отправить голосовое сообщение и получить расшифровку с таймкодами.

Заключение


В этой инструкции мы разобрали, как создать простого Telegram-бота с использованием API OpenAI и NestJS в качестве стека. А еще — научились деплоить проекты на облачный сервер и настраивать автозапуск на случай перезагрузки сервера.

Материал объемный. Если вам проще изучить тему в формате видео, смотрите выпуск на канале REG Group [10].

Автор: Seleditor

Источник [11]


Сайт-источник PVSM.RU: https://www.pvsm.ru

Путь до страницы источника: https://www.pvsm.ru/linux/424362

Ссылки в тексте:

[1] на сервер: https://selectel.ru/services/cloud/servers/?utm_source=habr.com&utm_medium=referral&utm_campaign=cloud_article_telegramvoicebot_030725_content

[2] @BotFather: https://t.me/BotFather

[3] на официальную страницу: https://openai.com/index/openai-api/

[4] в документации: https://help.openai.com/en/articles/4936850-where-do-i-find-my-openai-api-key

[5] в отдельной статье: https://selectel.ru/blog/git-github-review/

[6] создать новый репозиторий: https://github.com/new

[7] в панели управления Selectel: https://my.selectel.ru/l

[8] в инструкции: https://selectel.ru/blog/tutorials/how-to-generate-ssh/

[9] в документации: https://docs.github.com/ru/repositories/creating-and-managing-repositories/cloning-a-repository

[10] выпуск на канале REG Group: https://www.youtube.com/watch?v=pwxaL1rqQ7Y

[11] Источник: https://habr.com/ru/companies/selectel/articles/914138/?utm_source=habrahabr&utm_medium=rss&utm_campaign=914138