Делаем самые лучшие фото для документов
- среда, 29 октября 2025 г. в 00:00:04
Привет, Хабр!
Делали ли вы электронную визу в Индию? А, может, в Южную Корею? Или подавались на лотерею Green Card в США? Если да, то вы точно знаете, что для заявки на все эти документы надо прикрепить фотографию определённого размерас целым набором требований...
А такое ну просто необходимо автоматизировать!
И, как можно догадаться, сайтов для автоматизации фотографий на документы просто куча. Только вот есть одна проблема: все эти сайты хотят много денег - от 5 до 12 долларов за приведение фото к нужным требованиям.
Терпеть такое я, конечно же, не стал и написал своего бота в Телеграме, который делает это всё бесплатно.

@photovisa_bot - бесплатный бот для генерации фото на документы.
Сидел я одним зимним вечером на диване. А моя девушка планировала поезду с подругами в Китай и в это время заполняла анкету на визу (да-да, тогда еще нужны были визы!).
Когда она всё заполнила, я подошёл поинтересоваться, как все прошло. И вдруг обнаружил, что для подачи заявки она с подругами купила создание фото на визу за 400 рублей. Тогда я подумал про себя: "А отличный бизнес можно сделать - удаляем задний фон, выравниваем лицо на фото - и ко мне потекут горы денег. Богатство и слава! АХАХАХАХАХ!" И благополучно забыл об этом.
И вот, спустя почти год, мне на работе стало скучно, и я решил, что пора приниматься за дело (естественно, в рабочее время). Получилось сделать бота быстро и хорошо, поэтому решил оставить его бесплатным для всех.

Делаем мы приложение для себя, поэтому самое главное - чтобы его было приятно разрабатывать. Поэтому в выборе архитектуры я полагался только на свои специфичные вкусы.
Я люблю Телеграм-ботов - их просто и быстро делать, поэтому будем делать свой собственный Телеграм-миниапп для обработки фото на визы.

Чтобы сделать Телеграм-миниапп, у нас будут следующие компоненты:
Обычный сервер на питоне, который:
обрабатывает сообщения от пользователя в чате
дает ему ссылку на фронтенд
принимает от фронтенда запрос на обработку фото и кладет его в очередь
когда фото готово, отправляет его пользователю в чат
А вот это у нас будет сайт на React!
Если давать юзеру интерфейс через чат Телеграма, то это неудобно (как из огромного списка стран для документов найти нужную?) и не очень красиво (хочется показать приятный лоадер, пока генерируем фото). А миниаппы в Телеграме позволяют нам открыть свой сайт и избавиться от всех этих проблем.
Этот сайт будет:
давать пользователю возможность выбрать страну, выбрать тип документа и загрузить фото
показывать красивый лоадер, пока мы генерируем для пользователя фото
Это будет простенький сервис на питоне, который:
принимает фото из очереди
обрабатывает его и передает нашему Bot Server'у
Что ж, архитектура спланирована, пора приступать к реализации.
У каждой страны и у каждого документа есть свои требования к:
размерам (в пикселях, мм или дюймах)
расположению лица на фото (какой размер лицо занимает по высоте, размер отступа от верха фото до макушки и т.д.)
размеру файла (у некоторых документов в Китае максимальный размер 40кб)
цвету заднего фона (некоторые просят голубой фон вместо белого)
Так как же нам их получить? Все просто - украсть с других сайтов. Вместо тысячи слов вот вам ссылка на репозиторий со скриптами для скраппинга и нормализации данных с одного известного сайта для виз + готовые JSON-файлы.
Для обработки фото нам понадобится удаление заднего фона, поиск лица и его ключевых точек на картинке.
Удаление заднего фона - задача очень известная и решений опубликовано куча. Но не все они нам подойдут - тот же rembg довольно старый и плохо работает с волосами (а у нас их планируется много). Я решил взять свежую модельку ZhengPeng7/BiRefNet для поиска маски заднего фона.

Моделька довольно большая и на CPU работает медленно, поэтому пришлось взять тачку с RTX 3060 чтобы работало быстрее. А вот так эту модельку можно использовать в коде:
import torch
import numpy as np
from PIL import Image
from transformers import AutoModelForImageSegmentation
from torchvision import transforms as T
device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
print(f"Using device: {device}")
print("Loading BiRefNet model...")
model = AutoModelForImageSegmentation.from_pretrained("ZhengPeng7/BiRefNet", trust_remote_code=True)
model.to(device)
model.eval()
preprocess = T.Compose([
T.Resize((1024, 1024)),
T.ToTensor(),
T.Normalize([0.485, 0.456, 0.406], [0.229, 0.224, 0.225]),
])
def remove_background(pil_image: Image.Image) -> np.ndarray:
image = pil_image.convert("RGB")
original_size = image.size
input_tensor = preprocess(image).unsqueeze(0).to(device)
with torch.no_grad():
predictions = model(input_tensor)[-1].sigmoid().cpu()
pred = predictions[0].squeeze()
mask_pil = T.ToPILImage()(pred)
mask_resized = mask_pil.resize(original_size, Image.BILINEAR)
mask_array = np.array(mask_resized, dtype=np.uint8)
image_array = np.array(image)
rgba_array = np.dstack((image_array, mask_array))
return rgba_array
Для поиска лица и ключевых точек мы возьмем dlib. Он старенький, но в этой задаче нам ювелирная точность не понадобится.
Будем использовать его так:
import cv2
import dlib
cnn_detector = dlib.cnn_face_detection_model_v1("mmod_human_face_detector.dat")
predictor = dlib.shape_predictor("shape_predictor_68_face_landmarks.dat")
def detect_face_and_landmarks(img):
gray = cv2.cvtColor(img, cv2.COLOR_RGB2GRAY)
detections = cnn_detector(img, 1)
if len(detections) == 0:
return None, None
face_rect = detections[0].rect
landmarks = predictor(gray, face_rect)
return face_rect, landmarks

Как только готовы удаление заднего фона и поиск ключевых точек лица, остальное сделать тривиально: нужно добавить еще получение данных из очереди, расположить лицо по требованиям документа и отправить результат на наш сервер. Чтобы не перегружать вас, дорогой читатель, оставлю это все за кадром.
Мы сделали worker, но чтобы отпралять туда запросы нам нужно обрабатывать запросы от пользователей в чате Телеграма. Давайте напишем бота!
Для начала сделаем простого бота, который будет отображать нам кнопку для открытия миниаппа прямо в чате!
import os
from telegram import Update, InlineKeyboardButton, InlineKeyboardMarkup, WebAppInfo
from telegram.ext import Application, CommandHandler, ContextTypes
BOT_TOKEN = os.getenv("TELEGRAM_BOT_TOKEN")
# Адрес нашего будущего миниаппа
WEB_URL = os.getenv("WEBAPP_URL", "https://127.0.0.1:3000/")
async def start(update: Update, context: ContextTypes.DEFAULT_TYPE):
keyboard = InlineKeyboardMarkup([
[InlineKeyboardButton("Открыть miniapp", web_app=WebAppInfo(url=WEB_URL))],
])
if update.message:
await update.message.reply_text(
"Нажмите ниже, чтобы открыть miniapp",
reply_markup=keyboard,
)
def main():
app = Application.builder().token(BOT_TOKEN).build()
app.add_handler(CommandHandler("start", start))
app.run_polling(allowed_updates=Update.ALL_TYPES)
Отлично - что-то готово! Теперь добавим функцию для авторизации пользователя, когда он будет слать нам запросы из миниаппа на бэкенд. Телеграм отправляет внутрь миниаппа специальный payload, который миниаппу надо будет отправлять нам на бекенд. Я написал функцию для валидации этого payload на бекенде и положил ее сюда под спойлер (она довольно магическая):
import hashlib
import hmac
import time
def _parse_auth_data(raw: str):
data = {}
for pair in raw.split("&"):
if not pair or "=" not in pair:
continue
k, v = pair.split("=", 1)
data[unquote_plus(k)] = unquote_plus(v)
return data
def _compute_secret_key(bot_token: str):
return hmac.new(key=b"WebAppData", msg=bot_token.encode(), digestmod=hashlib.sha256).digest()
def _validate_and_extract_auth(raw: str, bot_token: str, max_age_seconds: int = 3000):
data = _parse_auth_data(raw)
if "hash" not in data:
raise ValueError("hash field missing")
if "auth_date" not in data:
raise ValueError("auth_date missing")
try:
auth_date = int(data.get("auth_date", "0"))
except ValueError:
raise ValueError("auth_date invalid")
now = int(time.time())
if now - auth_date > max_age_seconds:
raise ValueError("auth data too old")
received_hash = data.pop("hash")
data_check_pairs = [f"{k}={data[k]}" for k in sorted(data.keys())]
data_check_string = "\n".join(data_check_pairs)
secret_key = _compute_secret_key(bot_token)
calculated_hash = hmac.new(secret_key, data_check_string.encode(), hashlib.sha256).hexdigest()
if calculated_hash != received_hash:
raise ValueError("hash mismatch")
return data
Теперь добавим несколько роутов FastAPI:
один - чтобы миниапп мог отправить запрос на генерацию фото
второй - чтобы при получении ответа от worker'а мы отправили сообщение пользователю в чат
import io
import json
from typing import Optional
from telegram import Bot
from fastapi import FastAPI, File, Form, UploadFile, Header, HTTPException, status
from fastapi.responses import JSONResponse
from helpers import _validate_and_extract_auth
app = FastAPI(title="Test TG FastAPI")
@app.post("/request_photo_generation")
async def request_photo_generation(
Authorization: Optional[str] = Header(default=None),
doc_id: str = Form(...),
photo: UploadFile = File(...),
):
try:
auth_data = _validate_and_extract_auth(Authorization, BOT_TOKEN)
except ValueError as e:
raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED, detail=f"Invalid auth data: {e}")
user_obj = json.loads(auth_data["user"])
user_id = int(user_obj.get("id"))
# а затем пишем эти данные в очередь для worker'а
whatewer_queue_you_want_to_use.put({
"user_id": user_id,
"doc_id": doc_id,
"photo": await photo.read(),
"filename": photo.filename,
})
return JSONResponse(status_code=200, content={
"status": "queued",
"user_id": user_id,
"doc_id": doc_id,
"filename": photo.filename,
})
@app.post("/receive_photo_result")
async def receive_photo_result(
user_id: int = Form(...),
photo: UploadFile = File(...),
caption: str = Form(default=""),
):
content = await photo.read()
bio = io.BytesIO(content)
bio.name = photo.filename or "photo.jpg"
await BOT.send_document(chat_id=user_id, document=bio, caption=caption)
return {"status": "sent", "user_id": user_id}
И вот таким образом наш бекенд уже готов принимать картинку на вход и отправлять пользователю готовое фото на документ.
Для frontend'а мы делаем все максимально просто - берем create-react-app и копируем дизайн текстов и кнопок как у бота @BotFather. И все, наш фронтенд готов! (Правда, тут вообще ничего интересного не было...)

Для получения данных авторизации используем готовую библиотечку от Телеграма:
import { retrieveRawInitData } from '@telegram-apps/sdk';
const initDataRaw = retrieveRawInitData();
// А потом вставляем эти данные в заголовок Authorization при запросе /request_photo_generation
Единственное, с чем могут возникнуть сложности - это локальная отладка миниаппа на десктопном сайте телеграма. Он работает по HTTPS, а это значит что все сайты, которые будут открыты с помощью iframe, тоже должны его поддерживать.
Чтобы наше приложение можно было открыть локально по https, давайте сгенерируем приватный ключик и соответствующий сертификат:
openssl req -x509 -nodes -newkey rsa:2048 \
-keyout localhost-key.pem -out localhost.pem \
-days 825 -subj "/CN=localhost" \
-addext "subjectAltName=DNS:localhost,IP:127.0.0.1"
Добавим их в параметры запуска нашего приложения:
# package.json
{
...
"scripts": {
"start": "HTTPS=true SSL_CRT_FILE=localhost.pem SSL_KEY_FILE=localhost-key.pem react-scripts start",
...
}
}
И конечно же добавим сертификат в Хром. Переходим по ссылке chrome://certificate-manager/localcerts/usercerts и в Trusted Certificates добавляем наш новосозданный localhost.pem.
Вуаля! Теперь наш локально запущенный миниапп можно открыть на сайте Телеграма.
Наши три компонента системы готовы, и если мы их успешно задеплоили, то приложение должно работать без проблем. Так ведь?
Если вы, как и я, выбрали для деплоя фронтенда AWS, то у вас возникнут некоторые сложности... У ребят из России, когда они пользовались мобильным интернетом от МТС, фронтенд моего миниаппа просто не открывался (получается МТС блочат весь трафик на AWS?).
У меня как раз был сервер в Германии на хостинге hosting-russia.ru, к которому Роскомнадзор, похоже, относится более лояльно. Сделаем его прокси-сервером! Добавим следующие строки в конфиг NGINX:
server {
listen 443 ssl;
server_name yourdomain.com;
# SSL сертификаты
ssl_certificate /etc/ssl/certs/yourdomain.crt;
ssl_certificate_key /etc/ssl/private/yourdomain.key;
ssl_protocols TLSv1.2 TLSv1.3;
ssl_ciphers HIGH:!aNULL:!MD5;
location / {
proxy_pass http://any_address_on_aws;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
}
}
Теперь Роскомнадзор нам не страшен, и пользователи из России спокойно смогут сгенерировать себе любые фотографии для виз!
Посмотреть результат и сгенерировать себе фото на документы можно в @photovisa_bot - абсолютно бесплатно и без регистрации, для хабровчан и остальных.

Спасибо, что дочитали статью до конца! Думаю, теперь у вас не возникнет никаких проблем с созданием фото себе на визу :)