Кейс · сентябрь 2026 · Самара

Ночной клуб хотел знать, сколько людей реально проходит за смену

Считали по кассе и на глаз: в пятницу «много», во вторник «мало». Я собрал систему на потолочных камерах, которые там уже висели. Ночью она сама перегоняет запись на домашний компьютер, распознаёт людей и к утру кладёт отчёт по смене. 14 смен подряд без единого ручного запуска. Дальше про то, как это устроено, сколько стоило и где система до сих пор врёт.

Из этой системы я сделал продукт для клубов и баров: okovision.ru ↗

Ролик продукта «Цифровое ОКО», 80 секунд.
3машины в конвейере: клуб, сервер, дом
14смен подряд разобрано без вмешательства
~1,5 ГБвидео за ночь, две камеры
0 ₽на железо: только то, что уже было

Задача и рамки

Владельцу нужна одна цифра: сколько человек было за ночь. Купить её негде. Камеры в клубе обычные, потолочные, их вешали для охраны, про аналитику тогда никто не думал. Считать людей надо было на том, что есть.

Как устроен конвейер

Ночью пишется смена, утром она сама доезжает до видеокарты и превращается в отчёт. Человек в цепочке не нужен.

Схема конвейера: клуб, сервер, домашний ПК

Что делает движок с кадром

PythonYOLOv8ByteTrackinsightfaceOpenCVCUDASQLitePowerShellbashffmpeg / ffprobeopenpyxl

Панель

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

Вкладка «Отчёты»: список смен с размером файла и датой разбора
Отчёты по сменам. Каждая ночь — отдельный xlsx: сводка, почасовая разбивка, гости, роли. По колонке «Изменён» видно, когда смена реально разобралась: утром следующего дня.
Вкладка «Система»: диски и агенты трёх машин
Диски и агенты трёх машин в одном месте. Зелёная галочка значит, что агент отвечает. Кнопками можно прогнать последнюю ночь руками и вычистить всё старше суток.

Ночь как график

Сколько людей в зале, считаю нарастающим итогом входов и выходов с шагом пять минут. По кривой видно, как зал наполняется и когда начинает пустеть. На графике пятница 19 сентября 2026 года, пик 296 человек около трёх ночи.

Кривая присутствия в зале за ночь 19 сентября
человек в зале, шаг 5 минут источник: журнал событий движка за ночь 19.09.2026

Что ломалось после запуска

Модель распознавания я взял готовую. Основная работа началась, когда всё вроде бы «заработало»: конвейер тёк, терял смены и молча жёг видеокарту. Ниже шесть таких находок, на каждую ушёл отдельный разбор.

01 · идемпотентность

Смена качалась по два раза

Отметку «эту ночь уже забрали» клали внутрь очереди, а очередь чистит другая машина. Отметка исчезала, и смена ехала снова. Перенёс отметку в отдельную папку вне очереди, которую никто не чистит.

02 · целостность

Докачка «дописывала» в дыры

Файл качается кусками параллельно, а докачка считала прогресс по размеру файла. При записи со смещением размер становится финальным сразу, а середина ещё пустая: получался целый по размеру и битый по содержимому файл. Теперь на каждый кусок пишется строка в журнале, и файл не считается готовым, пока журнал не закрыт.

03 · ворота

Битый файл ронял разбор каждое утро

Один повреждённый файл вставал в очередь и валил цепочку сутки за сутками. Добавил проверку файла до постановки в очередь и потолок попыток: после второго провала смена помечается и не долбится бесконечно.

04 · сон

Таймаут не сработал: компьютер спал

Ограничение времени висело на ожидании процесса, и вместе со спящим компьютером «уснул» счётчик. Разбор шёл 401 минуту при лимите 180. Переписал на настенные часы: опрос раз в десять секунд, решение по реальному времени.

05 · сторож

Зависший разбор грел карту впустую

Процесс жив, вывода нет, и так пять часов сорок минут работы в никуда. Сделал сторож тишины: пока шаг пишет в лог, он считается живым. Замолчал на 20 минут, значит убиваем и перезапускаем.

06 · срыв GPU

Драйвер видеокарты ронял процесс без следа

За десять дней 26 срывов драйвера: процесс умирал без трассировки, смена терялась. Разделил падения «по делу» и падения «по железу»: вторые перезапускаются до трёх раз. Одну субботу это и спасло: шаг упал на 78-й минуте и доехал сам.

Как выглядит сторож тишины

# процесс живой, пока пишет в лог; замолчал надолго — значит завис
$beat = [hashtable]::Synchronized(@{ last = Get-Date })
$onData = {
  if ($null -ne $EventArgs.Data) {
    [Console]::Out.WriteLine($EventArgs.Data)
    $Event.MessageData.last = Get-Date
  }
}
Register-ObjectEvent -InputObject $p -EventName OutputDataReceived -Action $onData -MessageData $beat

while (-not $p.HasExited) {
  $now    = Get-Date
  $silent = ($now - $beat.last).TotalMinutes
  if ($silent -gt $STALL_MIN) { $p.Kill(); $rc = 125; break }   # завис
  if ($now -gt $deadline)     { $p.Kill(); $rc = 124; break }   # не уложился
  Start-Sleep -Seconds 10
}

Два кода возврата я развёл нарочно: 125 значит «молчит», 124 значит «не уложился в лимит». Первый перезапускается сам, второй разбираю руками.

Журнал докачки

# каждый доехавший кусок — строка в журнале; размер файла ничего не доказывает
with open(out, "r+b") as f:
    f.seek(offset)
    f.write(chunk)
    f.flush(); os.fsync(f.fileno())
with open(out + ".parts", "a") as j:
    j.write("%d %d\n" % (offset, len(chunk)))

# файл готов, только если сошлись все куски
if missing:
    sys.exit(2)
os.remove(out + ".parts")

Потом я добавил проверку файла сторонним инструментом, и только после неё файл попадает в очередь.

Что сошлось и что врёт

Проще всего проверить счётчик балансом: сколько людей за ночь вошло и сколько вышло. К утру клуб пустеет, значит числа должны сойтись. По журналу событий за 14 ночей подряд:

НочьВошлоВышлоРасхождениеПик в зале
11.09 · пт21991675—644
12.09 · сб20001532—590
13.09 · вс3983470 по минимуму80
15.09 · вт3192710 по минимуму98
17.09 · чт370319−196
19.09 · пт1160998−1296
20.09 · сб517709−245сорван
23.09 · вт363365−345

Расхождение здесь показывает, насколько «в зале» уходило в минус за ночь. Ноль значит, что кривая ни разу не провалилась ниже пустого зала и входы с выходами сошлись. На 13 ночах из 14 провал не больше шести человек.

А пересчёт проходов в людей врёт

Проход и человек не одно и то же. Один гость за ночь выходит покурить четыре раза, охранник мотается туда-сюда всю смену. Сырые 1062 прохода в субботу вовсе не значат 1062 гостя. Пересчёт я сделал через повторность: сколько проходов в среднем приходится на одного опознанного человека. И он, по-честному, не работает:

Вывод, который я записал владельцу прямым текстом: считать проходы система умеет, считать людей — пока нет. Докручивать формулу дальше смысла нет. Лучше поставить у входа одну камеру на уровне лица: тогда опознание вырастет, и пересчёты станут не нужны.

Что случилось в субботу 20 сентября

Это единственная ночь, где счётчик сорвало. С 22:00 до 22:11 система показала до 59 выходов в минуту и увела зал в минус 245 человек, а дальше вся ночь считалась нормально. Модель тут ни при чём. Запись начинается в 22:00, счётчик присутствия стартует с нуля и не знает, что в зале уже есть люди. Все, кто вошёл до начала записи, выходят «в минус», и пик ночи считается от съехавшего нуля. Лечится калибровкой старта по минимуму кривой. На остальных тринадцати ночах минимум и так нулевой, поэтому всплыло это только раз.

Чему этот проект меня научил

Нужен такой же разбор вашей задачи?

Могу собрать конвейер из того, что у вас уже есть, и честно сказать, где он будет врать. Пишите в Telegram, отвечу сегодня.

Telegram @vitaldemort