Legan Studio
Все статьи
~ 16 мин чтения

Multimodal Telegram-бот: фото, видео, документы

Как обрабатывать в Telegram-боте фото, видео, PDF и Excel: загрузка через getFile, vision-модели, OCR и архитектура очередей под тяжёлые форматы.

  • Telegram
  • AI
  • разработка

Сильный 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 сам ресайзит, теряет качество
Фото «как файл»document20 МБОригинал без сжатия
Видеоvideo20 МБ download / 50 МБ uploadmp4, есть duration, width, height
Кружокvideo_note20 МБВсегда квадрат, до 60 секунд
Голосvoice20 МБOGG/OPUS, есть duration
Аудиоaudio20 МБЕсть performer, title
Документdocument20 МБЛюбой mime-type

Архитектура

Многим хочется обрабатывать всё прямо в обработчике апдейта. Это плохая идея: видео распознаётся секунды, OCR PDF — десятки секунд, vision-модель отвечает 3–8 секунд. Webhook должен ответить за 60 секунд, иначе Telegram считает доставку неуспешной и начинает ретраить апдейт.

Рабочий шаблон:

  1. В обработчике сразу подтверждаете получение, отправляете «обрабатываю...».
  2. Кладёте задачу в очередь (Celery, RQ, NATS, BullMQ, dramatiq).
  3. Воркер скачивает файл, прогоняет через нужный движок, складывает результат.
  4. Бот отправляет ответ в чат через 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% к счёту.

Какую модель брать

МодельVisionVoiceВидеоСильные стороныГде живёт
GPT-4oдада (нативно)кадрыУниверсальный, voice-realtimeOpenAI / 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генерацияПолный контроль, LoRASelf-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, который умеет нативно. Универсальный пайплайн:

  1. Извлечь дорожку через ffmpeg.
  2. Прогнать аудио через Whisper — получить текст с timestamp'ами.
  3. Брать ключевые кадры (1 кадр в N секунд или через scenedetect) и пропускать через vision-модель.
  4. Собрать 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.