Сильный AI-бот сегодня — это не только текст. Фото товара, скрин ошибки, PDF с прайсом, кружочек с вопросом, видео из магазина — всё это валидный вход. Telegram отдаёт каждый тип отдельным полем апдейта, и архитектура бота должна понимать, какой формат пришёл и что с ним делать. Разберём, как устроен мультимодальный пайплайн от приёма файла до ответа vision-модели, какие модели брать, где ломается продакшн и как остаться в рамках 152-ФЗ при работе с фото лиц.
Что такое multimodal LLM
Раньше «понимать картинки» и «понимать текст» делали разные модели: vision-энкодер (CLIP, ViT) выдавал эмбеддинги, отдельный LLM работал с текстом, а соединял их адаптер. Сейчас всё чаще встречается архитектура единой модели: GPT-4o, Claude 3.5/4 Sonnet, Gemini 2.0 принимают на вход картинку, текст, аудио — и выдают текст в одном проходе. Это называют natively multimodal: модель училась на смешанных данных, не на склейке двух обученных по отдельности.
Для бота это означает простую вещь — можно передать [{type: "image_url", ...}, {type: "text", text: "опиши"}] и не строить отдельный пайплайн под зрение. Но композиция (классификатор → LLM, OCR → LLM, ASR → LLM) до сих пор живёт там, где важна цена, скорость или точность под конкретную задачу.
Use cases мультимодальных Telegram-ботов
Где это реально используется:
- Распознавание документов — пользователь шлёт фото паспорта/водительского, бот вытаскивает ФИО, серию, номер, дату рождения в структурированный JSON.
- Парсинг чека — фото чека из магазина → позиции, цены, итог. Удобно для трекеров расходов и corporate expense.
- OCR ценников и этикеток — снять полку в супермаркете, вытащить состав, аллергены, КБЖУ.
- Описание изображения для accessibility — alt-text для слабовидящих, описание мема глухим.
- Изучение языков — фото меню в Бангкоке → перевод и расшифровка иероглифов.
- Фото симптомов в мед-боте — сыпь, родинка, цвет языка; обязательно с дисклеймером «не заменяет врача».
- Fashion-подбор — фото вещи → похожие из каталога, рекомендации сочетаний.
- Контроль работ — рабочий шлёт фото монтажа, бот сверяет с чек-листом.
- Скрин ошибки разработчика — пользователь кидает screenshot из IDE, бот объясняет stack trace.
Каждый из этих сценариев — отдельный пайплайн с своим набором моделей и правил приватности.
Что приходит из Telegram
В апдейте Message могут быть:
photo— массивPhotoSizeс разными разрешениями. Берите последний элемент — это самый большой.document— любой файл: PDF, DOCX, XLSX, ZIP, изображения отправленные «как файл» (без сжатия).video— настоящее видео с превью.video_note— кружок до 1 минуты, MPEG-4, разрешение 240×240 или 384×384.audio— музыкальный файл (MP3, M4A) с метаданными.voice— голосовое OGG/OPUS, моно 48 kHz.animation— GIF или MP4 без звука.sticker— WebP или TGS (анимированный Lottie), VideoSticker — WebM.
Скачивание одинаковое: getFile(file_id) возвращает file_path, по которому можно тянуть до 20 МБ напрямую через https://api.telegram.org/file/bot<TOKEN>/<file_path>. Файлы больше 20 МБ нужно получать через MTProto-клиент (Telethon, gramjs, TDLib) — Bot API их физически не отдаёт.
| Тип | Поле в Message | Лимит Bot API | Особенности |
|---|---|---|---|
| Фото (сжатое) | photo[] | 20 МБ | Telegram сам ресайзит, теряет качество |
| Фото «как файл» | document | 20 МБ | Оригинал без сжатия |
| Видео | video | 20 МБ download / 50 МБ upload | mp4, есть duration, width, height |
| Кружок | video_note | 20 МБ | Всегда квадрат, до 60 секунд |
| Голос | voice | 20 МБ | OGG/OPUS, есть duration |
| Аудио | audio | 20 МБ | Есть performer, title |
| Документ | document | 20 МБ | Любой mime-type |
Архитектура
Многим хочется обрабатывать всё прямо в обработчике апдейта. Это плохая идея: видео распознаётся секунды, OCR PDF — десятки секунд, vision-модель отвечает 3–8 секунд. Webhook должен ответить за 60 секунд, иначе Telegram считает доставку неуспешной и начинает ретраить апдейт.
Рабочий шаблон:
- В обработчике сразу подтверждаете получение, отправляете «обрабатываю...».
- Кладёте задачу в очередь (Celery, RQ, NATS, BullMQ, dramatiq).
- Воркер скачивает файл, прогоняет через нужный движок, складывает результат.
- Бот отправляет ответ в чат через
sendMessageили редактирует placeholder.
Для долгих операций показывайте chat.sendChatAction("typing") или upload_photo — индикатор «печатает» / «отправляет фото» успокаивает пользователя. Action живёт 5 секунд, поэтому в долгом таске — повторяйте каждые 4 секунды в отдельной корутине.
Скачивание и preprocessing фото
Базовый handler на aiogram:
from aiogram import F, Router
from aiogram.types import Message
import io
from PIL import Image, ImageOps
router = Router()
@router.message(F.photo)
async def on_photo(msg: Message):
placeholder = await msg.answer("Анализирую фото...")
# Берём самый большой PhotoSize
photo = msg.photo[-1]
file = await msg.bot.get_file(photo.file_id)
buf = io.BytesIO()
await msg.bot.download_file(file.file_path, destination=buf)
buf.seek(0)
img = Image.open(buf)
# EXIF orientation: телефон часто пишет «повёрнут на 90», пиксели не повёрнуты
img = ImageOps.exif_transpose(img)
# Ресайз: GPT-4o берёт 512x512 для low / 768x2000 для high — больше не имеет смысла
img.thumbnail((1024, 1024), Image.Resampling.LANCZOS)
out = io.BytesIO()
img.convert("RGB").save(out, format="JPEG", quality=85)
payload = out.getvalue()
# Дальше — отправка в очередь / vision-модель
await process_in_queue(msg.from_user.id, payload, placeholder.message_id)
Два важных момента: exif_transpose спасает от перевёрнутых снимков с iPhone (модель видит то же, что юзер), а ресайз до 1024×1024 экономит токены — vision-модели тарифицируют входные изображения в условных «тайлах», и на 4K-снимке тайлов в 16 раз больше, чем нужно.
Запрос в vision-модель
Стандартный формат для GPT-4o / Claude / Gemini — picture as base64 data URL:
import base64
import httpx
def to_data_url(jpeg_bytes: bytes) -> str:
b64 = base64.b64encode(jpeg_bytes).decode()
return f"data:image/jpeg;base64,{b64}"
async def describe_with_gpt4o(jpeg: bytes, prompt: str) -> str:
body = {
"model": "gpt-4o",
"messages": [{
"role": "user",
"content": [
{"type": "text", "text": prompt},
{
"type": "image_url",
"image_url": {
"url": to_data_url(jpeg),
"detail": "high", # low|high|auto
},
},
],
}],
"max_tokens": 800,
}
async with httpx.AsyncClient(timeout=60) as client:
r = await client.post(
"https://api.openai.com/v1/chat/completions",
headers={"Authorization": f"Bearer {OPENAI_KEY}"},
json=body,
)
r.raise_for_status()
return r.json()["choices"][0]["message"]["content"]
Параметр detail управляет ценой и точностью: low — фиксированные 85 токенов, годится для «что на фото в общих чертах»; high — десятки тайлов по 170 токенов, нужен для чтения текста и мелких деталей. Не ставьте high по умолчанию — это +50–80% к счёту.
Какую модель брать
| Модель | Vision | Voice | Видео | Сильные стороны | Где живёт |
|---|---|---|---|---|---|
| GPT-4o | да | да (нативно) | кадры | Универсальный, voice-realtime | OpenAI / Azure |
| Claude 3.5/4 Sonnet | да | нет | кадры | Лучшее чтение длинных документов | Anthropic / Bedrock / Vertex |
| Gemini 2.0 Flash | да | да | да (нативно) | Дёшев, ест видео целиком до 1 часа | Google AI / Vertex |
| GigaChat-Vision | да | нет | нет | РФ-резидентность данных | Sber Cloud |
| YandexGPT-Vision | да | нет | нет | РФ-резидентность, дешевле | Yandex Cloud |
| DALL-E 3 | генерация | — | — | Качество fashion / постеры | OpenAI |
| FLUX.1 (dev/schnell) | генерация | — | — | Open weights, фотореализм | Replicate / self-hosted |
| Stable Diffusion XL | генерация | — | — | Полный контроль, LoRA | Self-hosted |
| YandexART | генерация | — | — | РФ-резидентность | Yandex Cloud |
Для документов и чтения мелкого текста Claude Sonnet и GPT-4o идут ноздря в ноздрю; Gemini уверенно лидирует там, где нужно скормить целое видео или 1000-страничный PDF — у него native multimodal context до 2M токенов.
OCR на документах
Vision-модель отлично читает фото, но для PDF на 200 страниц она дорого и долго. Специализированные OCR:
- Tesseract — открытая, бесплатная, прилично знает русский, плох на сложной вёрстке.
- PaddleOCR — точнее на таблицах и многоколонке.
- Yandex Vision OCR / Cloud Vision API — облако, оплата по страницам, точность выше открытых движков.
- DocLayout-YOLO + Donut/TrOCR — для строго структурированных документов (паспорт, СНИЛС).
- GPT-4o — быстрее всех в реализации, но дороже Yandex Vision раз в 5–10 на странице.
PDF делятся на «текстовые» (реальный текст внутри) и «сканы» (картинки страниц). Сначала пробуем pdfplumber или pypdf — если получили нормальный текст, OCR не нужен. Если страницы — картинки, гоним через pdf2image в PNG и далее OCR.
import pytesseract
from pdf2image import convert_from_bytes
def ocr_pdf(pdf_bytes: bytes, lang: str = "rus+eng") -> list[str]:
images = convert_from_bytes(pdf_bytes, dpi=300)
pages = []
for img in images:
txt = pytesseract.image_to_string(img, lang=lang)
pages.append(txt)
return pages
DPI 300 — компромисс между точностью и скоростью; ниже 200 Tesseract начинает путать «и/н», «о/0».
Структурированный output
Когда нужно превратить чек в JSON — пользуйтесь structured outputs модели, не парсингом регулярками:
from pydantic import BaseModel, Field
from openai import OpenAI
class ReceiptItem(BaseModel):
name: str
qty: float
price: float
sum: float
class Receipt(BaseModel):
shop: str | None
date: str | None = Field(description="ISO 8601")
items: list[ReceiptItem]
total: float
client = OpenAI()
parsed = client.beta.chat.completions.parse(
model="gpt-4o",
messages=[
{"role": "system", "content": "Распознай чек, верни JSON."},
{"role": "user", "content": [
{"type": "image_url", "image_url": {"url": data_url, "detail": "high"}},
]},
],
response_format=Receipt,
)
receipt: Receipt = parsed.choices[0].message.parsed
Pydantic-схема превращается в JSON Schema, OpenAI/Anthropic форсят модель не отклоняться от формата. Это резко снижает количество «модель вернула markdown вместо JSON» багов.
Excel и табличные данные
XLSX/CSV открываются через openpyxl, pandas или polars. Тут важнее не парсинг, а интерфейс: пользователь редко присылает идеально оформленную таблицу. Сначала бот должен показать «я нашёл колонки A, B, C — это правильно?», и только после подтверждения запускать импорт. Иначе клиент получит криво загруженные 5000 строк и удалит бота.
Для умного маппинга «непонятная шапка → ваш канонический формат» хорошо работает LLM: даём первую строку и описание целевых полей, получаем mapping {«Артикул товара»: «sku», «Цена руб»: «price»}.
Голос: voice → text → ответ
Voice приходит в OGG/OPUS. Whisper и большинство ASR жуёт это нативно, но иногда нужен ресемпл в WAV 16 kHz mono:
import subprocess
def ogg_to_wav(ogg_bytes: bytes) -> bytes:
proc = subprocess.run(
["ffmpeg", "-i", "pipe:0", "-ar", "16000", "-ac", "1",
"-f", "wav", "pipe:1"],
input=ogg_bytes, capture_output=True, check=True,
)
return proc.stdout
Дальше Whisper (OpenAI API или локальный faster-whisper) → текст → LLM → опционально TTS обратно в voice. Подробнее — в отдельной статье про voice-ботов.
Видео
Чистое распознавание видео целиком — дорого, кроме Gemini, который умеет нативно. Универсальный пайплайн:
- Извлечь дорожку через ffmpeg.
- Прогнать аудио через Whisper — получить текст с timestamp'ами.
- Брать ключевые кадры (1 кадр в N секунд или через
scenedetect) и пропускать через vision-модель. - Собрать summary: «на 0:12 — кассир, на 0:34 — стеллаж с молочкой, аудио — конфликт».
import subprocess, os, tempfile
def extract_frames(video_bytes: bytes, every_seconds: int = 2) -> list[bytes]:
with tempfile.TemporaryDirectory() as td:
src = os.path.join(td, "in.mp4")
with open(src, "wb") as f:
f.write(video_bytes)
out_pattern = os.path.join(td, "frame_%04d.jpg")
subprocess.run([
"ffmpeg", "-i", src,
"-vf", f"fps=1/{every_seconds}",
"-q:v", "3",
out_pattern,
], check=True, capture_output=True)
frames = []
for name in sorted(os.listdir(td)):
if name.startswith("frame_"):
with open(os.path.join(td, name), "rb") as f:
frames.append(f.read())
return frames
Если задача — просто транскрипт с timestamp'ами, достаточно Whisper и subtitle-формата (SRT/VTT). Live-перевод видео потока — отдельная история через WebRTC и streaming-ASR, в Bot API не реализуема без обвеса.
Генерация изображений в ответе
Пользователь пишет «нарисуй кота-астронавта в стиле акварели» — бот зовёт DALL-E/FLUX и отдаёт картинку:
import httpx
from aiogram.types import BufferedInputFile
async def generate_and_send(msg, prompt: str):
async with httpx.AsyncClient(timeout=120) as client:
r = await client.post(
"https://api.openai.com/v1/images/generations",
headers={"Authorization": f"Bearer {OPENAI_KEY}"},
json={
"model": "dall-e-3",
"prompt": prompt,
"size": "1024x1024",
"quality": "standard",
"response_format": "b64_json",
"n": 1,
},
)
r.raise_for_status()
b64 = r.json()["data"][0]["b64_json"]
import base64
png = base64.b64decode(b64)
await msg.answer_photo(
BufferedInputFile(png, filename="generated.png"),
caption=f"Промпт: {prompt[:200]}",
)
FLUX через Replicate возвращает URL — можно либо переслать ссылку Telegram'у через answer_photo(url), либо скачать и переотправить (надёжнее: внешние URL временные).
Размеры и лимиты
- Bot API отдаёт через
getFileфайлы до 20 МБ, заливает черезsendDocumentдо 50 МБ. - Для больших файлов нужен MTProto-клиент (Telethon, gramjs, TDLib) — лимит 2 ГБ, для Premium-пользователей — 4 ГБ.
- Внутри Mini App файлы стоит грузить через свой бэкенд по подписанной ссылке, не через Bot API.
- Telegram сжимает фото при отправке без флажка «как файл». Если важно качество (паспорт, чертёж) — просите присылать «как файл» (
document). file_pathв ответеgetFileживёт около часа — не кешируйте надолго, повторно вызовитеgetFileпоfile_id.
Многошаговые сценарии
Часто пользователь шлёт несколько файлов подряд: фото-фото-фото-«проанализируй». Бот должен буферизовать вход. Простое решение — таймер на 1.5 секунды после последнего сообщения. Если за этот период пришёл новый медиафайл, продлеваем таймер; когда тишина — обрабатываем пакет целиком. Telegram уже группирует медиа при отправке (media_group_id), используйте это поле для очевидной группировки.
Обработка ошибок
Vision и OCR любят падать на краевых случаях. Минимум, который должен возвращать ваш пайплайн:
file_too_large— для файлов > 20 МБ от Bot API. Подсказать «пришлите фрагмент».unsupported_format— для экзотических документов. Список поддерживаемых явно в подсказке.low_quality— фото размыто, текст нечитаем. Можно автоматически дёрнуть яркость через PIL и повторить.no_face_detected— для биометрических сценариев. Просим переснять при дневном свете.nsfw_detected— модель отказалась обрабатывать. Логируем, не показываем содержимое в чат.model_timeout— vision-модель не ответила за 30 секунд. Retry с экспоненциальной задержкой, не больше 2 попыток.
Сообщения для пользователя — на русском, без технических кодов. В логах — полный traceback и file_id для разбирательств.
Privacy и 152-ФЗ
Фото лиц = биометрические персональные данные (152-ФЗ ст. 11). Это значит:
- Отдельное письменное согласие (галочка в чате с явной формулировкой не катит — нужна электронная подпись или очное согласие; для большинства Telegram-ботов биометрия = красная зона, обходите её).
- Уведомление РКН до начала обработки (форма с указанием категорий и целей).
- Нельзя хранить дольше, чем нужно для конкретной операции.
- Для трансграничной передачи (а Telegram + OpenAI = трансграничная) — отдельное уведомление.
Практический совет: если задача не требует биометрии (фото чека, фото товара) — не работайте с фото лиц вовсе. Если требует (KYC) — стройте отдельный контур с YandexGPT-Vision/GigaChat в РФ-облаке, не отправляйте лица в OpenAI/Anthropic.
Чек-лист по любому фото:
- Удалить EXIF (там GPS, модель камеры, иногда серийник).
- Не сохранять оригинал в S3 без необходимости — обработали, выкинули.
- В логах — только
file_id, не сами байты. - Прозрачное согласие в
/start: что мы делаем с фото, кому передаём, сколько храним.
Стоимость
Vision дороже текста в 5–30 раз за «сообщение». Главные рычаги экономии:
- Ресайз и качество JPEG — 1024×1024 q85 обычно покрывает любую задачу OCR.
- Параметр
detail: low— для классификации «это документ или фото» хватает. - Кеш по hash файла — пользователь часто шлёт одно и то же. SHA-256 от байтов + результат → Redis.
- Каскад моделей — сначала дешёвый классификатор/CLIP, потом GPT-4o только на «интересных» фото.
- Локальные модели для рутины — YOLO для object detection, EasyOCR для номеров — 0 за вызов.
Для видео самая большая статья — кадры. Семплируйте редко (1 кадр в 5–10 секунд), отправляйте grid из 4 кадров одним запросом — vision-модели понимают коллажи.
Итого
Multimodal-бот — это набор отдельных пайплайнов под каждый тип файла, объединённых очередью задач. Не пытайтесь обработать видео в обработчике апдейта, не лейте каждый PDF в LLM, выделяйте OCR и распознавание в отдельные воркеры. Чем уже задача, тем дешевле и точнее можно её решить специализированной моделью; LLM-vision держите для свободных формулировок и сложных кейсов. Особое внимание — фото лиц: 152-ФЗ строг, лучше проектировать архитектуру так, чтобы биометрии вовсе не было, либо держать её в РФ-облаке. Кеш по хэшу, ресайз до 1024 и параметр detail: low экономят 50–70% бюджета на vision без потери качества для большинства задач.
Частые вопросы
Какие типы файлов может принимать Telegram-бот?
В апдейте Message могут быть: photo — массив PhotoSize с разными разрешениями (берите последний, самый большой); document — любой файл (PDF, DOCX, XLSX, ZIP, изображения «как файл»); video — настоящее видео с превью; video_note — кружок до 1 минуты MPEG-4 240×240 или 384×384; audio — музыкальный файл MP3/M4A с метаданными; voice — голосовое OGG/OPUS моно 48 kHz; animation — GIF или MP4 без звука; sticker — WebP, TGS (Lottie) или WebM. Скачивание одинаковое: getFile(file_id) возвращает file_path, по которому можно тянуть до 20 МБ напрямую. Файлы больше 20 МБ нужно получать через MTProto-клиент (Telethon, gramjs, TDLib).
Какие vision-модели подходят для Telegram-бота в 2026?
GPT-4o — универсальный, нативный voice, средняя цена. Claude 3.5/4 Sonnet — лучшее чтение длинных документов и таблиц. Gemini 2.0 Flash — самый дешёвый, нативно ест видео до часа и контекст до 2M токенов. Из российских — GigaChat-Vision и YandexGPT-Vision: данные не покидают РФ, что критично для 152-ФЗ. Для генерации изображений: DALL-E 3, FLUX.1 (open weights), Stable Diffusion XL, YandexART. Для документов и чтения мелкого текста Claude Sonnet и GPT-4o идут ноздря в ноздрю. Где нужно скармливать целое видео или 1000-страничный PDF — Gemini лидирует за счёт огромного контекста.
Как обрабатывать тяжёлые файлы в Telegram-боте без таймаута?
Многим хочется обрабатывать всё прямо в обработчике апдейта. Это плохая идея: видео распознаётся секунды, OCR PDF — десятки секунд, vision-модель отвечает 3–8 секунд. Webhook должен ответить за 60 секунд, иначе Telegram считает доставку неуспешной и ретраит. Рабочий шаблон: в обработчике сразу подтверждаете получение, отправляете «обрабатываю»; кладёте задачу в очередь (Celery, RQ, NATS, BullMQ, dramatiq); воркер скачивает файл, прогоняет через нужный движок; бот отправляет ответ через sendMessage или редактирует placeholder. Для долгих операций — sendChatAction("typing") или upload_photo каждые 4 секунды.
Как сделать OCR документов в Telegram-боте?
Tesseract — открытая, бесплатная, прилично знает русский. PaddleOCR — точнее на таблицах и многоколонке. Yandex Vision OCR / Cloud Vision API — облако, оплата по страницам, точность выше открытых движков. DocLayout-YOLO + Donut/TrOCR — для строго структурированных документов (паспорт, СНИЛС). GPT-4o — быстрее в реализации, но дороже Yandex Vision раз в 5–10 на странице. PDF делятся на «текстовые» (реальный текст внутри) и «сканы» (картинки). Сначала пробуем pdfplumber или pypdf — если получили нормальный текст, OCR не нужен. Если страницы — картинки, гоним через pdf2image в PNG (DPI 300) и далее OCR.
Как обрабатывать видео в Telegram-боте?
Чистое распознавание видео целиком — дорого, кроме Gemini (нативный multimodal). Универсальный пайплайн: извлечь аудиодорожку через ffmpeg; прогнать через Whisper для текста с timestamp; брать ключевые кадры (1 кадр в N секунд или через scenedetect) и пропускать через vision-модель; собрать summary. Если задача — просто транскрипт, достаточно Whisper и SRT/VTT. Live-перевод видео потока — отдельная история через WebRTC и streaming-ASR, в Bot API не реализуема без обвеса. Семплируйте кадры редко (1 в 5–10 секунд), отправляйте grid из 4 кадров одним запросом — vision-модели понимают коллажи и счёт сильно меньше.
Как соблюсти 152-ФЗ при работе с фото в Telegram-боте?
Фото лиц = биометрические персональные данные (152-ФЗ ст. 11). Нужны: отдельное письменное согласие (галочка в чате не катит — нужна ЭП или очно); уведомление РКН до начала обработки; ограниченный срок хранения; отдельное уведомление по трансграничной передаче если используете OpenAI/Anthropic. Практический совет: если задача не требует биометрии (фото чека, товара) — не работайте с фото лиц вовсе. Если требует (KYC) — стройте отдельный контур с YandexGPT-Vision или GigaChat в РФ-облаке. Чек-лист по любому фото: удалить EXIF (там GPS); не сохранять оригинал в S3 без нужды; в логах — только file_id; прозрачное согласие в /start.
Как сэкономить на vision-моделях в Telegram-боте?
Vision дороже текста в 5–30 раз. Главные рычаги: ресайз до 1024×1024 и JPEG q85 — для большинства задач OCR хватает; параметр detail: low (вместо high) — фиксированные 85 токенов, годится для классификации; кеш по SHA-256 хешу байтов файла в Redis — пользователи часто шлют одно и то же; каскад моделей — сначала дешёвый CLIP-классификатор «это чек/паспорт/прочее», потом GPT-4o только на нужных; локальные модели для рутины — YOLO для object detection, EasyOCR для номеров — 0 за вызов. Для видео — семплирование кадров и grid-склейка перед отправкой в vision.