Skip to content

About

Resources

Stars

1 star

Watchers

0 watching

Forks

Repository files navigation

Контроль кассовой дисциплины по видео

Сервис принимает запись камеры кассового узла (.mov) и за время до 5 минут возвращает вердикт violation / no_violation, список нарушений с типом, секундой, описанием, 1–3 кадрами-доказательствами (JPEG base64) и ссылкой на норму законодательства РК. Вся обработка идёт на собственном GPU-сервере: видео, кадры и описания никуда не передаются, внешние AI API не используются.

Итог в цифрах

40 уникальных видео открытого датасета (21 с нарушением, 19 чистых), голосование по 5 прогонам.

Конфигурация Точность вердикта Precision Recall Таймкод ±3 с Тип верный Время на видео
Базовая (промпт v1, одна модель) 0.725 0.81 0.62 0.62 0.62 ~8 с на вызов
Промпт v3b 0.825 0.89 0.76 0.56 0.63 ~13 с на вызов
Промпт v3b + проверки ноутбука и стола 0.825–0.85 1.00 0.67–0.71 0.64–0.67 0.80–0.86 +~1 с
Промпт v3b + проверки ноутбука, стола и сейфа (на сервере жюри) 0.875 1.00 0.76 0.69 0.81 +3–5 с

Ни одного ложного обвинения на 19 чистых видео в лучшей конфигурации. На сервере жюри развёрнута последняя строка таблицы (см. v0.9 в CHANGELOG.md). Веб-интерфейс: встроенный (/ на адресе API) и отдельный на Vercel. Промпты дорабатывались на этом же наборе, поэтому цифры — оценка сверху; в промпты попадают только процедурные правила, но не описания конкретных видео.

Как это работает

 .mov ──► кадры всего видео ──► Qwen3.6-35B-A3B (vLLM, локально) ──► JSON: сценарий, хронология,
          2 кадр/с (<120 с)       один запрос со всеми кадрами           состояния объектов, нарушения
          1 кадр/с (≥120 с)        × 5 прогонов, голосование                         │
          640 px, метка [t=X с]                                                         ▼
                                                    проверки согласованности (код) ◄────┘
                                                    точечные проверки состояния (кроп, 1 вопрос)
                                                                     │
                                                                     ▼
                                   вердикт + тип + секунда + описание + кадры + норма (rules.json)
  1. Кадры. Выбираются по всему видео: 2 кадра/с для роликов короче 2 минут, 1 кадр/с для длинных (не больше 250 кадров), 640 px по длинной стороне. На каждом кадре — метка времени [t=12.0 с], по ней модель называет секунду.
  2. Один запрос по всему видео. Модель видит весь сценарий целиком (вход, работа с клиентом, сейф, уход) и отвечает строгим JSON: сценарий, хронология событий, состояния ключевых объектов в ключевые моменты (дверь после входа, сейф после отхода, ключ при уходе, экран ноутбука, содержимое стола, документ клиента), список нарушений из закрытого перечня.
  3. Голосование. 5 независимых прогонов; видео — нарушение, если так ответило большинство; тип — самый частый, секунда — медиана.
  4. Проверки согласованности. Код сверяет нарушение с собственной хронологией модели: «компьютер не заблокирован» снимается, если модель сама написала, что ноутбук закрыт/заблокирован; «дверь открыта» не засчитывается при выходе кассира, если после него никто не входит; «приём пищи» — только если рядом есть наличные.
  5. Точечные проверки состояния. Один фактический вопрос по кадрам в полном разрешении: что на экране ноутбука в момент, когда кассир встаёт (таблица / заставка блокировки / тёмный / крышка закрыта); что осталось на столе после ухода; плотно ли закрыта дверца сейфа после начала пересчёта.
  6. Нормы. Тип нарушения связывается с пунктом нормативного акта из rules.json (дословная цитата и ссылка на adilet.zan.kz). Если прямой нормы нет, ответ прямо это говорит и помечает случай для ручной проверки. Типы вне вердикта уходят в observations.
  7. Надёжность. Любая ошибка модели, сети или разбора JSON даёт статус failed, а не «нарушений нет». Время считается от загрузки; если до лимита 5 минут мало времени — выполняется один прогон вместо пяти.

API

Метод Путь Назначение
POST /api/v1/analyze multipart, поле file (.mov) → {"job_id": "..."}
GET /api/v1/jobs/{job_id} processing → done (с результатом) или failed (с error)
GET /api/v1/health проверка готовности
GET / веб-интерфейс на русском

Авторизация — заголовок X-API-Key.

{
  "verdict": "violation",
  "violations": [{
    "type": "Наличные/ценности оставлены без присмотра",
    "type_id": "cash_unattended",
    "timecode_sec": 9,
    "timecode": "00:09",
    "confidence": 0.9,
    "description": "Наличные/ценности оставлены без присмотра. Наблюдение (00:09): ... Основание: Правила № 231, п. 20 ч. 2: «...».",
    "evidence_frames": [{"timestamp_sec": 9.0, "frame_index": 270, "role": "peak", "image_base64": "..."}],
    "rule_ref": {"doc": "Правила № 231 ...", "clause": "п. 20 ч. 2", "quote": "...", "url": "https://adilet.zan.kz/rus/docs/V1900019680#z112"}
  }],
  "observations": [],
  "scenario": "...",
  "timeline": [{"t": 0.0, "event": "..."}],
  "meta": {"model": "Qwen/Qwen3.6-35B-A3B", "duration_sec": 28.0, "frames_sampled": 56, "samples": 5}
}

Перечень нарушений и нормы

Таксономия rules.json v0.4. Цитаты сверены с текстами актов на adilet.zan.kz.

Код Нарушение Основание В вердикте
cash_unattended Наличные/ценности оставлены без присмотра Правила № 231 п. 20 ч. 2; п. 22 да
seals_docs_unattended Пломбы, штампы, ключи или кассовые документы оставлены без присмотра Правила № 231 п. 80 да
safe_left_open Сейф/хранилище оставлен открытым Правила № 231 п. 20 ч. 2; п. 80 да
door_left_open Дверь кассы/кассового узла оставлена открытой Правила № 14 п. 8 ч. 1; п. 16 пп. 1) да
outsider_in_cash_room Постороннее лицо в кассовом помещении / за стойкой Правила № 14 п. 8 ч. 1; Правила № 231 п. 75; Инструкция АРРФР № 21 п. 16 пп. 4) да
cash_out_of_view Операции с наличными вне зоны видеонаблюдения Правила № 14 п. 16 пп. 6); Правила № 231 п. 20 ч. 1 да
collector_bag_no_check Приём инкассаторской сумки без проверки документа/пломбы Правила № 231 п. 24 ч. 2 пп. 1), 3), 4) да
cash_within_client_reach Наличные в зоне досягаемости клиента Правила № 231 п. 20 ч. 2; п. 22 да
counting_without_client Пересчёт наличных не в присутствии клиента Правила № 231 п. 20 ч. 1 да
issue_without_count Выдача наличных без пересчёта в присутствии клиента Правила № 231 п. 31 пп. 4) да
phone_or_personal_items Смартфон / личные вещи на рабочем месте кассира Правила № 231 п. 12 ч. 1, ч. 2 да
eating_drinking Приём пищи/напитков на рабочем месте кассира Правила № 231 п. 12 (косвенно) да
no_client_id_check Приём наличных без предъявления документа, удостоверяющего личность Правила № 231 п. 19 пп. 1) да
foreign_cash_on_desk Наличные предыдущего клиента на столе при обслуживании следующего Правила № 231 п. 20 ч. 2; п. 21 да
cash_accepted_without_count Наличные приняты/убраны без пересчёта Правила № 231 п. 19 пп. 2) да
suspicious_note_returned Сомнительная банкнота возвращена клиенту без оформления Правила № 231 п. 59 ч. 1; п. 61 да
safe_key_left Ключ оставлен в замке сейфа/ящика Правила № 231 п. 80; п. 20 ч. 2 да
workstation_unlocked Рабочий компьютер оставлен разблокированным Требования ИБ АРРФР № 53 п. 71 пп. 9) (косвенно: рабочая станция без активности пользователя блокируется, разблокировка — только по аутентификационным данным) да
safe_opened_with_client Открытие сейфа в присутствии клиента/постороннего Правила № 231 п. 80 нет, наблюдение
counting_without_controller Пересчёт без контролирующего работника Правила № 231 п. 42; п. 2 пп. 2) нет, наблюдение
other Иное возможное нарушение — да

Акты: Правила № 231 — постановление Правления НБ РК от 29.11.2019 № 231 (рег. № 19680); Правила № 14 — постановление Правления НБ РК от 24.02.2020 № 14 (рег. № 20075); Инструкция АРРФР № 21 — постановление Правления АРРФР от 30.04.2024 № 21 (рег. № 34348); Требования ИБ АРРФР № 53 — постановление Правления АРРФР от 03.04.2026 № 53 (рег. № 38336), заменило постановление НБ РК от 27.03.2018 № 48; Правила № 49 — постановление Правления НБ РК от 04.04.2019 № 49 (рег. № 18545).

Какие модели сравнивали

Все модели — открытые веса, запуск через vLLM 0.31 на 2×A100 80 GB. Голосование по 10 (v1) или 5 (v2, v3b) прогонам, с проверками согласованности.

Модель v1 v2 v3b Таймкод ±3 с (лучший) Тип верный (лучший) Время вызова
Qwen3.6-35B-A3B (MoE, 3B активных) 0.725 0.650 0.825 0.79 0.64 8–13 с
Qwen3.6-27B 0.700 0.600 0.725 0.80 0.87 14–23 с
Qwen3.8-27B 0.600 0.600 — 0.50 0.71 14–22 с
Qwen3.5-9B 0.600 0.525 — 0.70 0.70 18–40 с
Qwen3-VL-32B 0.625 0.600 — 0.44 0.50 50–72 с
Qwen3-VL-30B-A3B 0.525 0.550 0.575 0.44 0.44 11–22 с
Qwen3-VL-8B 0.475 0.525 — 0.33 0.67 40–54 с
Molmo2-8B не запустилась: контекст 32k мал, невалидный JSON 72 с

Выбрана Qwen3.6-35B-A3B: лучшая точность вердикта и самая быстрая (MoE). Qwen3.6-27B точнее называет тип и секунду — гибрид «вердикт 35B + тип и секунда 27B» даёт тип 0.81 и таймкод ±3 с 0.75 при той же точности вердикта (обе модели помещаются на один сервер).

Ключевые решения

Решение Почему Подтверждение
Анализ всего видео одним запросом, а не окнами Нарушение часто — отсутствие шага («не проверил документ», «не пересчитал»), его видно только в контексте всего сценария Окно ±3–5 с не подтверждает пропущенный шаг (X-17 потерян)
2 кадр/с до 2 минут, 1 кадр/с дальше, 640 px Укладывает самое длинное видео (143 с) в контекст и в лимит времени 848 px: 0.60; 3 кадр/с: 0.675 — больше пикселей и кадров даёт новые ложные срабатывания
Строгий JSON с хронологией и состояниями объектов Модель обязана сначала описать факты, потом выводить нарушение; код может проверить ответ на противоречия Проверки согласованности: +5–10 п.п. на большинстве прогонов
Голосование 5 прогонов Ответы при temperature 0.6 нестабильны: многие видео на 2/5–3/5 голосов +5–7 п.п. к точности отдельного вызова
Правила-определения в промпте (v3b) вместо общих инструкций Модель путала выход кассира с открытой дверью при входе, «экран включён» с «не заблокирован» v1 0.725 → v3b 0.825; исправлены X-4, X-29, X-32, X-33
Точечный вопрос о состоянии, а не «есть ли нарушение?» Вопрос «нарушение?» отклонял кандидатов одинаково на чистых и плохих видео; вопрос о факте модель решает надёжно Ноутбук: 8/8 верно на размеченных моментах; precision 1.00
Текстовый судья не используется 15 из 17 ошибок судьи — в описании видеомодели; судья не видит того, чего нет в описании Судья 27B: 0.650 = столько же, сколько без него, +104 с
Сбой → failed, не no_violation Тихое «нарушений нет» при ошибке — худший исход для банка Тесты сервиса
Секунда нарушения — по конвенции разметки Для пропущенного шага — начало действия, где шаг должен был быть; для оставленного — момент, когда кассир отворачивается/встаёт, а не выход за дверь Постоянный сдвиг таймкода проверен и отвергнут: на перекрёстной проверке ухудшает 12 из 19 прогонов

Ограничения (на реальных примерах датасета)

Ограничение Пример
Мелкие объекты сверху при 640 px X-22: ключ в замке сейфа — пропущен почти всеми прогонами, не виден даже на кропе
Модель «дорисовывает» привычный сценарий X-2: придумано предъявление документа; X-22: придумано открытие сейфа, хотя рука лишь лежит на ключе
Короткое событие ~1 с X-20: дверца сейфа прикрыта, но не заперта — 2 из 5 голосов
Редкие составные события X-1: часть купюр возвращена клиенту после пересчёта; X-44: эталонный тип неоднозначен
Стеклянная дверь Вопрос «закрыта ли дверь» по кропу ошибается (X-32, X-33), поэтому проверка двери не используется
Тип при уходе с рабочего места X-13, X-15, X-24, X-26: вердикт верный, но тип часто «компьютер не заблокирован» вместо реального
Устойчивость к другому ракурсу не измерена В датасете один ракурс камеры
Для «компьютер не заблокирован» норма косвенная Требования ИБ АРРФР № 53 п. 71 пп. 9) говорят об автоблокировке рабочей станции; в ответе основание помечено как косвенное

Локальность и безопасность

  • Модель и сервис работают в одном GPU-контейнере; vLLM слушает только 127.0.0.1:8001, наружу открыт лишь порт приложения.
  • Сервис отказывается работать с нелокальным адресом модели (VLM_BASE_URL — только localhost / частная сеть).
  • Веса загружаются один раз на постоянный диск сервера; после этого сервис можно запускать в офлайн-режиме (HF_HUB_OFFLINE=1).
  • Лица не распознаются, звук не анализируется; видео хранится только на диске сервера для выдачи кадров-доказательств.

Инфраструктура

Сначала планировали GPU-серверы в AWS (EC2, us-east-1 / us-west-2): заявки на квоту GPU-инстансов (G и VT) поданы 17.10, но к сроку не одобрены. Поэтому эксперименты и сервис жюри развёрнуты на арендованных GPU-серверах RunPod (2×A100 80 GB, vLLM 0.31). Ранний вариант развёртывания под EC2 сохранён в deploy/. Сервис упакован в один контейнер и переносится в любой GPU-контур без изменений.

Структура репозитория

Путь Что внутри
final_service/ сервис для жюри: FastAPI, очередь задач, пайплайн, UI, rules.json, тесты, скрипт запуска
harness/ стенд оценки: выборка кадров, промпты v1–v3d, проверки согласованности и состояния, оценка по разметке
harness/experiments/ снимки экспериментов: конфигурация, точный промпт, метрики, результаты по видео, TRENDS.csv
harness/analysis_out/ разбор ошибок, карта сложности по типам, анализ таймкодов
docs/RESULTS.md все результаты в таблицах
analysis/ исследование кейса, норм, моделей и совместимости vLLM, аудит датасета
harness/runs/ все прогоны: конфигурация, точный системный промпт, метрики, ответ модели по каждому видео
deploy/ ранний вариант развёртывания (EC2, vLLM)
service/, open/ первая версия сервиса и материалы организаторов

Как воспроизвести оценку

cd harness
# модель поднята в vLLM, адрес в endpoints.json, ключ в .vlm_key
PROMPT_MODULE=prompt_v3b python eval.py --name my-run --repeats 5 --concurrency 8
python eval.py --name my-run --repeats 5 --score-only          # пересчёт метрик из сохранённых ответов
STATE_CHECKS=laptop3,desk python eval_state.py --pass1 my-run  # второй проход: проверки состояния

Каждый вызов модели сохраняется в harness/runs/<прогон>/<видео>/r<k>.json вместе с конфигурацией и точным системным промптом прогона.

About

Resources

Stars

1 star

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages