Сервис принимает запись камеры кассового узла (.mov) и за время до 5 минут возвращает вердикт violation / no_violation, список нарушений с типом, секундой, описанием, 1–3 кадрами-доказательствами (JPEG base64) и ссылкой на норму законодательства РК. Вся обработка идёт на собственном GPU-сервере: видео, кадры и описания никуда не передаются, внешние AI API не используются.
- Подробные результаты всех экспериментов: docs/RESULTS.md
- История работы и решений: CHANGELOG.md
- Код сервиса и развёртывание: final_service/README.md, final_service/deploy/README_DEPLOY.md
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)
- Кадры. Выбираются по всему видео: 2 кадра/с для роликов короче 2 минут, 1 кадр/с для длинных (не больше 250 кадров), 640 px по длинной стороне. На каждом кадре — метка времени
[t=12.0 с], по ней модель называет секунду. - Один запрос по всему видео. Модель видит весь сценарий целиком (вход, работа с клиентом, сейф, уход) и отвечает строгим JSON: сценарий, хронология событий, состояния ключевых объектов в ключевые моменты (дверь после входа, сейф после отхода, ключ при уходе, экран ноутбука, содержимое стола, документ клиента), список нарушений из закрытого перечня.
- Голосование. 5 независимых прогонов; видео — нарушение, если так ответило большинство; тип — самый частый, секунда — медиана.
- Проверки согласованности. Код сверяет нарушение с собственной хронологией модели: «компьютер не заблокирован» снимается, если модель сама написала, что ноутбук закрыт/заблокирован; «дверь открыта» не засчитывается при выходе кассира, если после него никто не входит; «приём пищи» — только если рядом есть наличные.
- Точечные проверки состояния. Один фактический вопрос по кадрам в полном разрешении: что на экране ноутбука в момент, когда кассир встаёт (таблица / заставка блокировки / тёмный / крышка закрыта); что осталось на столе после ухода; плотно ли закрыта дверца сейфа после начала пересчёта.
- Нормы. Тип нарушения связывается с пунктом нормативного акта из
rules.json(дословная цитата и ссылка на adilet.zan.kz). Если прямой нормы нет, ответ прямо это говорит и помечает случай для ручной проверки. Типы вне вердикта уходят вobservations. - Надёжность. Любая ошибка модели, сети или разбора JSON даёт статус
failed, а не «нарушений нет». Время считается от загрузки; если до лимита 5 минут мало времени — выполняется один прогон вместо пяти.
| Метод | Путь | Назначение |
|---|---|---|
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 вместе с конфигурацией и точным системным промптом прогона.