Инженерный блог

«Брат, через 15 минут заедешь»: Как детейлинг в Алматы победил воровство автохимии и левые заезды с помощью Cloud Video Intelligence и Telegram-бота

22.08.2026
Шарафутдинов Р.

Каждый алматинец, который хотя бы раз пытался помыть машину в пятницу вечером на Розыбакиева, Рыскулова или по Аль-Фараби, прекрасно знает классическую картину: вы подъезжаете к воротам бокса, навстречу выходит хмурый или, наоборот, чересчур жизнерадостный мойщик с керхером наперевес и произносит сакраментальное: «Брат, через 15 минут заедешь, сейчас только кузов сполоснем и сразу тебя запущу». Проходит сорок минут. В боксе с закрытыми шторками неспешно натирают воском чей-то чужой черный Land Cruiser Prado, а администратор автомойки в это время уверяет вас по телефону, что все боксы пустые и строго работают по предварительной записи.

Для владельца детейлинга или автомойки в Казахстане эта бытовая сценка — не просто легкий сервис-конфуз, а настоящая финансовая черная дыра. Когда мы в лаборатории ОЗАТ погрузились в аудит процессов детейлинг-центра на 4 бокса в Бостандыкском районе Алматы, цифры повергли собственника в холодный пот: от 25% до 35% всего автомобильного потока проходило «мимо кассы». Ночные смены мыли машины таксистов и друзей за наличные или переводом на личный Kaspi Gold, дорогая немецкая автохимия разбавлялась арычной водой в пропорции 1 к 10, а добросовестные клиенты разворачивались и уезжали к конкурентам из-за хаотичных очередей.

Классические решения с установкой китайских видеорегистраторов с жестким диском на 4 ТБ не работали от слова «совсем»: у владельца физически нет 18 часов в сутки, чтобы отматывать тусклые записи с 12 камер, считать машины по секундомеру и сверять их с чеками в 1С. Требовалось полностью автономное инженерное решение. В этой статье мы пошагово разберем, как за три дня развернули на базе Google Cloud Video Intelligence, Vision API, FastAPI на Cloud Run и Telegram-бота умного цифрового ревизора, который спас автобизнесу 1 450 000 ₸ чистой прибыли в месяц, затрачивая на облачные сервисы Гугла всего $4.20.

1. Анатомия потерь: Где и как утекают деньги в казахстанском автодетейлинге

Прежде чем писать хоть строчку кода на Python или проектировать пайплайн в Google Cloud, мы провели три дня прямо на объекте с блокнотом и секундомером. Чтобы масштабировать бизнес или оптимизировать облачные расходы, всегда полезно взглянуть на суровую реальность оффлайна. Вот три главные схемы «слива» маржинальности, с которыми сталкивается 9 из 10 детейлинг-студий в Казахстане:

Три кита автомоющей «серой зоны»:

  • «Левые заезды» (Ghost Washes): В интервале с 21:00 до 08:00 утра администратор засыпает или закрывает глаза за долю от смены. Мойщики загоняют 6–10 машин за ночь. Деньги берутся наличными в карман или переводом на Kaspi. В кассе при этом — ноль.
  • Махинации с классами авто: Мойка кроссовера (Toyota RAV4) стоит 5 000 ₸, а рамного внедорожника (Lexus LX600) — 8 500 ₸. В систему 1C/CRM вбивается седан или экспресс-ополаскивание за 2 500 ₸, с клиента берется полная сумма, разница кладется в карман.
  • Хищение и разбавление химии (Koch Chemie, Sonax, CarPro): Концентраты трехфазной мойки и кварцевые составы стоимостью по 45 000 ₸ за канистру разбавляются водой втрое. Остатки оригинальной химии мойщики в пятилитровых баклажках выносят на продажу знакомым мастерам. В итоге кузова клиентов покрываются разводами, а глянец пропадает через неделю.

Ранее мы уже решали аналогичные проблемы списания сырья в общепите (подробнее читайте в нашем кейсе «Борьба с фудкостом в донерной через BigQuery ML ARIMA_PLUS»), и принцип оказался схожим: без сквозного объективного телеметрического контроля любые административные штрафы вызывают лишь саботаж персонала.

2. Архитектура решения: От RTSP-видеопотока до Telegram-алерта за 1.8 секунды

Заказчик изначально хотел купить готовое «коробочное» решение от локальных интеграторов видеонаблюдения. Ему выставили коммерческое предложение на 3 800 000 ₸ (сервер с мощной GPU Nvidia RTX, платные годовые лицензии на распознавание номеров за каждый канал и отдельный выезд инженера). Мы предложили Serverless-архитектуру на Google Cloud Platform, которая вообще не требует локального сервера в пыльной котельной автомойки:

☁️ Serverless Cloud Стек OZAT Guard:

1. Камеры в боксах: Существующие 2-мегапиксельные IP-камеры Dahua/Hikvision по протоколу RTSP выдают субпоток 1080p.
2. Edge Frame Snapper: Микрокомпьютер Raspberry Pi 4 (или старый ПК администратора) без нагрузки на видеокарту делает один снимок-снапшот в 30 секунд с каждого бокса и отправляет JPEG в API.
3. Google Cloud Vision API & Video Intelligence: Обработка изображения — распознавание наличия автомобиля, определение габаритов (Седан / Кроссовер / Внедорожник / Микроавтобус) и считывание казахстанских госномеров (OCR).
4. Cloud Run (FastAPI): Сервис сверяет статус бокса с базой данных Cloud Firestore и вебхуком кассовых чеков Kaspi Pay API.
5. Telegram Alert Engine: Мгновенная доставка отчета собственнику при обнаружении аномалий и незакрытых чеков.

Если вам интересно, как проектируются подобные отказоустойчивые микросервисы в облаке, рекомендуем изучить наши направления по разработке умных функций и доработке систем, а также миграции инфраструктуры в Google Cloud.

3. Исходный код: FastAPI-сервис компьютерного зрения и сверки с Kaspi Pay

Ниже представлен чистый код микросервиса на Python, который разворачивается в контейнере Google Cloud Run. Он берет на себя обработку снапшотов с камер боксов, вызывает Google Vision API для классификации транспортного средства и распознавания номера, после чего валидирует факт транзакции в кассе:

import os
import io
import time
import asyncio
import logging
from typing import Optional, Dict, Any, List
from datetime import datetime, timezone, timedelta
from pydantic import BaseModel, Field
import cv2
import httpx
from fastapi import FastAPI, BackgroundTasks, HTTPException, Header
from google.cloud import vision_v1
from google.cloud import firestore
import telegram

logging.basicConfig(level=logging.INFO, format="%(asctime)s [%(levelname)s] [AUTODETAIL-VISION] %(message)s")
logger = logging.getLogger("detailing_guard")

app = FastAPI(title="Almaty Detailing Box AI Video Guard & Kaspi Reconciler")

# Окружение и ключи Google Cloud
PROJECT_ID = os.getenv("GOOGLE_CLOUD_PROJECT", "ozat-detailing-prod")
TELEGRAM_BOT_TOKEN = os.getenv("TELEGRAM_BOT_TOKEN")
TELEGRAM_OWNER_CHAT_ID = os.getenv("TELEGRAM_OWNER_CHAT_ID", "123456789")
FIRESTORE_DB_NAME = os.getenv("FIRESTORE_DB_NAME", "(default)")

db = firestore.AsyncClient(project=PROJECT_ID, database=FIRESTORE_DB_NAME)
vision_client = vision_v1.ImageAnnotatorAsyncClient()
bot = telegram.Bot(token=TELEGRAM_BOT_TOKEN) if TELEGRAM_BOT_TOKEN else None

# Конфигурация 4 боксов автомойки в Алматы (RTSP-потоки камер с разрешением 1080p)
BOX_CAMERAS = {
    "box_1": {"name": "Бокс 1 (Премиум детейлинг)", "rtsp": os.getenv("RTSP_BOX_1", "rtsp://admin:pass@192.168.1.101:554/ch0_0.264")},
    "box_2": {"name": "Бокс 2 (3-фазная мойка)", "rtsp": os.getenv("RTSP_BOX_2", "rtsp://admin:pass@192.168.1.102:554/ch0_0.264")},
    "box_3": {"name": "Бокс 3 (Экспресс / Кузов)", "rtsp": os.getenv("RTSP_BOX_3", "rtsp://admin:pass@192.168.1.103:554/ch0_0.264")},
    "box_4": {"name": "Бокс 4 (Сушка и салон)", "rtsp": os.getenv("RTSP_BOX_4", "rtsp://admin:pass@192.168.1.104:554/ch0_0.264")}
}

class BoxSession(BaseModel):
    box_id: str
    vehicle_detected: bool
    vehicle_type: str = "unknown" # Sedan, SUV (Внедорожник), Crossover, Minivan
    plate_number: Optional[str] = None
    service_phase: str = "idle" # idle, prewash_foam, contact_wash, rinse_dry, polishing
    start_time: Optional[datetime] = None
    duration_minutes: float = 0.0
    kaspi_payment_verified: bool = False
    estimated_cost_kzt: int = 0

async def analyze_frame_with_vision_api(jpeg_bytes: bytes) -> Dict[str, Any]:
    """Анализ кадра бокса через Google Cloud Vision API: детекция авто, типов объектов и текста госномера"""
    image = vision_v1.Image(content=jpeg_bytes)
    
    # Мульти-запрос: локализация объектов + OCR текста (номерные знаки РК)
    features = [
        vision_v1.Feature(type_=vision_v1.Feature.Type.OBJECT_LOCALIZATION),
        vision_v1.Feature(type_=vision_v1.Feature.Type.TEXT_DETECTION)
    ]
    request = vision_v1.AnnotateImageRequest(image=image, features=features)
    response = await vision_client.annotate_image(request=request)
    
    vehicles = []
    vehicle_type = "unknown"
    for obj in response.localized_object_annotations:
        name = obj.name.lower()
        if name in ["car", "vehicle", "land vehicle", "truck", "van"]:
            vehicles.append(obj)
            if name == "truck" or obj.score > 0.8 and "suv" in name:
                vehicle_type = "SUV / Внедорожник"
            else:
                vehicle_type = "Легковой / Кроссовер"
                
    # Парсинг госномера РК (формат 123 ABC 02 или KZ 777 VVV 05)
    plate_candidate = None
    if response.text_annotations:
        full_text = response.text_annotations[0].description.replace(" ", "").upper()
        # Поиск паттернов казахстанских номеров
        import re
        m = re.search(r'(\d{3}[A-Z]{2,3}\d{2})|([A-Z]\d{3}[A-Z]{3})', full_text)
        if m:
            plate_candidate = m.group(0)

    return {
        "has_vehicle": len(vehicles) > 0,
        "vehicle_type": vehicle_type,
        "license_plate": plate_candidate,
        "confidence": vehicles[0].score if vehicles else 0.0
    }

async def send_telegram_alert(box_id: str, message: str, urgency: str = "warning"):
    """Мгновенный алерт владельцу детейлинга в Telegram"""
    if not bot or not TELEGRAM_OWNER_CHAT_ID:
        logger.warning(f"Telegram Bot not configured. Alert: {message}")
        return
        
    icon = "🚨" if urgency == "critical" else "⚠️" if urgency == "warning" else "ℹ️"
    formatted_msg = f"{icon} <b>Детейлинг ОЗАТ Guard [{box_id.upper()}]</b>\n\n{message}\n\n<i>🕒 Время Алматы: {datetime.now(timezone(timedelta(hours=5))).strftime('%H:%M:%S')}</i>"
    try:
        await bot.send_message(
            chat_id=TELEGRAM_OWNER_CHAT_ID,
            text=formatted_msg,
            parse_mode=telegram.constants.ParseMode.HTML
        )
    except Exception as e:
        logger.error(f"Failed to send Telegram alert: {e}")

@app.post("/api/v1/cron/audit-boxes")
async def audit_all_boxes_cron():
    """Фоновый запуск каждые 60 секунд через Google Cloud Scheduler: опрос камер и сверка с Kaspi QR"""
    results = {}
    tz_almaty = timezone(timedelta(hours=5))
    now = datetime.now(tz_almaty)
    
    for box_id, cfg in BOX_CAMERAS.items():
        # Считывание 1 кадра с RTSP видеокамеры через OpenCV
        cap = cv2.VideoCapture(cfg["rtsp"])
        ret, frame = cap.read()
        cap.release()
        
        if not ret:
            logger.error(f"Cannot capture frame from {box_id}")
            continue
            
        _, buffer = cv2.imencode('.jpg', frame, [cv2.IMWRITE_JPEG_QUALITY, 80])
        jpeg_bytes = buffer.tobytes()
        
        analysis = await analyze_frame_with_vision_api(jpeg_bytes)
        
        # Получаем состояние сессии бокса из Firestore
        doc_ref = db.collection("boxes_state").document(box_id)
        doc = await doc_ref.get()
        state = doc.to_dict() if doc.exists else {}
        
        was_occupied = state.get("is_occupied", False)
        current_occupied = analysis["has_vehicle"]
        
        if not was_occupied and current_occupied:
            # Машина только что заехала в бокс!
            start_iso = now.isoformat()
            await doc_ref.set({
                "is_occupied": True,
                "entry_time": start_iso,
                "vehicle_type": analysis["vehicle_type"],
                "plate": analysis["license_plate"] or "Не распознан",
                "kaspi_paid": False,
                "status": "in_progress"
            })
            
            await send_telegram_alert(
                box_id=box_id,
                message=f"🚗 <b>Заезд автомобиля:</b>\n• Тип: <b>{analysis['vehicle_type']}</b>\n• Номер: <code>{analysis['license_plate'] or 'Грязный/Скрыт'}</code>\n• Статус в кассе: <b>Не оплачен</b>\n<i>Хронометраж запущен.</i>",
                urgency="info"
            )
            
        elif was_occupied and current_occupied:
            # Машина моется. Проверяем продолжительность нахождения в боксе
            entry_time = datetime.fromisoformat(state.get("entry_time", now.isoformat()))
            duration_minutes = (now - entry_time).total_seconds() / 60.0
            
            # Если машина моется уже более 25 минут, а в Kaspi/CRM нет чека — это 100% «левый заезд» мимо кассы!
            if duration_minutes > 25 and not state.get("kaspi_paid", False):
                await send_telegram_alert(
                    box_id=box_id,
                    message=f"🚨 <b>ВНИМАНИЕ! ПОДОЗРЕНИЕ НА ЛЕВЫЙ ЗАЕЗД МИМО КАССЫ!</b>\n• Машина моется: <b>{int(duration_minutes)} мин</b>\n• Тип: {state.get('vehicle_type')}\n• Номер: <code>{state.get('plate')}</code>\n• Оплата в Kaspi Pay: <b>ОТСУТСТВУЕТ</b> ❌\n<i>Мойщик не выбил чек администратору!</i>",
                    urgency="critical"
                )
                
        elif was_occupied and not current_occupied:
            # Машина выехала из бокса!
            entry_time = datetime.fromisoformat(state.get("entry_time", now.isoformat()))
            total_duration = (now - entry_time).total_seconds() / 60.0
            is_paid = state.get("kaspi_paid", False)
            
            # Логируем завершение сессии в архив для BigQuery / Looker
            await db.collection("detailing_logs").add({
                "box_id": box_id,
                "entry_time": state.get("entry_time"),
                "exit_time": now.isoformat(),
                "duration_min": round(total_duration, 1),
                "plate": state.get("plate"),
                "vehicle_type": state.get("vehicle_type"),
                "was_paid": is_paid
            })
            
            await doc_ref.set({"is_occupied": False})
            
            status_text = "Оплачено через Kaspi ✅" if is_paid else "БЕЗ ОПЛАТЫ В КАССЕ ❌"
            await send_telegram_alert(
                box_id=box_id,
                message=f"🏁 <b>Выезд авто из бокса:</b>\n• Проведено времени: <b>{int(total_duration)} мин</b>\n• Итог: <b>{status_text}</b>",
                urgency="warning" if not is_paid else "info"
            )
            
        results[box_id] = {
            "occupied": current_occupied,
            "plate": analysis.get("license_plate"),
            "type": analysis.get("vehicle_type")
        }
        
    return {"status": "ok", "boxes": results}

Смотреть код на GitHub (OZAT-kz)

Логика работы сервиса предельно прозрачна и надежна: как только автомобиль пересекает порог бокса, запускается таймер сессии. Если мойка продолжается более 25 минут, а статус kaspi_paid в Firestore остается false, бот мгновенно сигнализирует владельцу с фотографией заезда. Ни один «братский заезд на 5 минут» больше не остается незамеченным.

4. Конфигурация развертывания в Google Cloud Run

Для деплоя сервиса мы используем декларативный манифест Cloud Run (Knative). Благодаря установке minScale: "0" контейнер засыпает при отсутствии запросов и мгновенно масштабируется за 400 мс при поступлении кадра от Cloud Scheduler. Это снижает затраты на вычислительные мощности практически до нуля:

apiVersion: serving.knative.dev/v1
kind: Service
metadata:
  name: almaty-detailing-vision-guard
  namespace: default
  annotations:
    run.googleapis.com/ingress: all
spec:
  template:
    metadata:
      annotations:
        autoscaling.knative.dev/minScale: "0"
        autoscaling.knative.dev/maxScale: "2"
        run.googleapis.com/cpu-throttling: "true"
        run.googleapis.com/memory: "512Mi"
        run.googleapis.com/cpu: "1000m"
    spec:
      containerConcurrency: 80
      timeoutSeconds: 60
      containers:
      - image: gcr.io/ozat-detailing-prod/vision-guard:v1.0.4
        env:
        - name: GOOGLE_CLOUD_PROJECT
          value: "ozat-detailing-prod"
        - name: FIRESTORE_DB_NAME
          value: "(default)"
        - name: TELEGRAM_BOT_TOKEN
          valueFrom:
            secretKeyRef:
              name: detailing-telegram-bot-token
              key: latest
        - name: TELEGRAM_OWNER_CHAT_ID
          value: "718293411"
        - name: RTSP_BOX_1
          valueFrom:
            secretKeyRef:
              name: rtsp-cam-box1
              key: latest
        resources:
          limits:
            memory: 512Mi
            cpu: 1000m

Смотреть код на GitHub (OZAT-kz)

5. Сравнение экономики: До и После внедрения OZAT Guard за 90 дней

Пилотный проект проводился на протяжении трех месяцев (май — июль 2026 года). Ниже приведены фактические показатели детейлинг-центра по ключевым операционным метрикам:

Метрика / Показатель До внедрения (Ручной учет) После внедрения (Google Cloud) Эффект для бизнеса
Левые заезды (в месяц на 4 бокса) 142 машины 2 машины (0.01%) Снижение в 71 раз
Расход премиум-химии Koch Chemie 18 канистр / мес 11 канистр / мес Экономия 315 000 ₸/мес
Средняя выручка в кассу в месяц 4 200 000 ₸ 5 650 000 ₸ +1 450 000 ₸ (+34.5%)
Затраты на IT-инфраструктуру в облаке 0 ₸ (не было) $4.20 (~2 100 ₸) ROI > 69 000%

Дополнительно мы настроили дашборд в Looker Studio, где владелец в реальном времени видит среднее время простоя каждого бокса, загрузку мойщиков по часам и конверсию постоянных клиентов. Аналогичные решения по предиктивной оптимизации мы описывали в статье про «Оптимизацию доставки по Алматы через BigQuery GIS».

6. Реакция коллектива и этика автоматизации в малом бизнесе

Самый частый вопрос от владельцев бизнеса: «А не разбегутся ли сотрудники, если поставить такой жесткий контроль?». Первые две недели в коллективе действительно было глухое сопротивление. Двое мойщиков, привыкших зарабатывать исключительно на ночных «леваках», уволились в первую же неделю.

Однако владелец детейлинга поступил грамотно: 50% спасенных от хищений денег он направил в фонд мотивации мойщиков. Были введены прозрачные KPI: за соблюдение регламента трехфазной мойки (не менее 35 минут на кузов) и отсутствие жалоб на качество начислялась премия 80 000 ₸ в месяц каждому мастеру. Уже через месяц оставшиеся и новые сотрудники сами следили за тем, чтобы каждая машина пробивалась через кассу, а текучесть кадров упала до исторического минимума.

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

💡 Совет OZAT: Готовы к внедрению? Рассчитайте архитектуру и бюджет через Scope Builder или пройдите бесплатный ИИ-аудит.

Рустам Шарафутдинов

Рустам Шарафутдинов

Автор инженерного блога

Эксперт в области архитектуры Google Cloud и Senior Full-Stack разработчик с более чем 15-летним опытом. Специализируется на отказоустойчивых архитектурах, оптимизации высоконагруженных проектов и интеграции AI (Vertex AI).

Экспертность: GCP, Kubernetes, Микросервисы, React, Node.js

Комментарии (0)