В 3 ночи мой бот уверенно перепутал «колики» и «голод». Я всё равно его выпустил — и вот почему

В 3 ночи мой бот уверенно перепутал «колики» и «голод». Я всё равно его выпустил — и вот почему

Я сделал Telegram-бота, который по короткому фрагменту детского плача (в MVP беру 5 секунд) пытается угадать причину из 6 классов и быстро вернуть ответ. Это не диагностика: бот может ошибаться, а шум и контекст ломают предсказание сильнее, чем хочется.

Что именно предсказываем

Сейчас бот возвращает один наиболее вероятный класс:

  • Боль в животике
  • Колики
  • Дискомфорт
  • Усталость
  • Голод
  • Страх

Честная оговорка: в реальности причины могут накладываться, а некоторые классы пересекаются по смыслу (например, «колики» vs «боль в животике»), поэтому это классификация по разметке, а не “понимание ребёнка”.

Архитектура без воркера: очередь в Postgres + cron

Я не держу постоянно работающий воркер. Задачи на обработку копятся в Postgres‑очереди, а по расписанию их разгребает cron прямо в базе: достаёт пачку задач, отправляет их в Flask-инференс, сохраняет результат и помечает задачу выполненной.

Telegram -> бот | кладём задачу {voice_url, request_id, ...} v Supabase Postgres - очередь pgmq: cry_jobs - таблицы: requests / predictions / errors ^ | pg_cron по расписанию: read batch -> HTTP -> write result | Flask inference API - скачать голосовое - препроцессинг (5s, mel) - CNN predict - ответ + запись результата

Supabase Cron работает на расширении pg_cron: расписание хранится в cron.job, а история прогонов — в cron.job_run_details. Очередь — это pgmq: чтение сообщений задаёт visibility timeout (пока задача “в работе” она невидима), а после успеха сообщение нужно удалить (pgmq.delete) или архивировать (pgmq.archive), иначе оно вернётся в очередь после истечения окна.

Как я разгребаю очередь (вот что реально важно)

Суть тут не в “красивом SQL”, а в трёх правилах, без которых ты быстро утонешь:

  • Пачка: cron‑задача читает ограниченное число сообщений через pgmq.read(queue, vt, qty) и обрабатывает их за один запуск, чтобы не упираться в таймауты и не выстрелить себе в ногу.
  • Повторы: если инференс/сеть упали и вы не сделали delete/archive, сообщение снова станет видимым после vt — значит, дубликаты будут.
  • Идемпотентность: результат пишется по уникальному request_id (UPSERT/“записать один раз”), иначе повторная доставка начнёт плодить мусор и ломать статистику.

Если в системе есть только одна вещь “как у взрослых” — пусть это будет идемпотентность.

Где Supabase, а где модель

Supabase у меня — это Postgres и два расширения вокруг фоновых задач: очередь (pgmq) и планировщик (pg_cron). Модель живёт отдельно в Flask: сервис принимает ссылку на голосовое, скачивает её, делает препроцессинг и отдаёт вероятности по 6 классам, а я сохраняю результат в базу и отвечаю пользователю.

ML: baseline mel + CNN (почему так)

Я сделал максимально прямолинейный baseline, чтобы быстро выйти в “работает end‑to‑end”:

  • Аудио привожу к SAMPLE_RATE = 22050, беру фиксированное окно DURATION = 5 секунд (короткое дополняю нулями).
  • Строю mel‑спектрограмму (N_MELS=128), перевожу в dB через librosa.power_to_db(..., ref=np.max), и привожу временную ось к MAX_TIME=200.
  • Дальше CNN: три блока Conv+Pool (32/64/128) → Dense(128) + Dropout(0.5) → Softmax.

5 секунд я выбрал как компромисс между UX и стабильностью входа: проще стандартизировать форму тензора и быстрее отвечать пользователю.

Данные и ответственность

Сейчас я не храню аудио пользователей: файл нужен только на время обработки и ответа. Дальше я хочу улучшать модель, но сбор аудио для дообучения возможен только как отдельный opt‑in с понятными сроками хранения и удалением по запросу — иначе доверия не будет.

И ещё: если у ребёнка есть тревожные симптомы (температура, вялость, отказ от еды и т.п.), бот не должен быть точкой принятия решения — это надо проговаривать в тексте прямо, иначе вы выглядите безответственно.

Где это ломается (и что буду чинить первым)

  • Шум/эхо и агрессивная обработка микрофоном телефона меняют спектрограмму сильнее, чем кажется.
  • В реальности “причина” может быть не одна, но модель всегда выбирает один класс.
  • Пересечение классов делает ошибки неизбежными — поэтому нужен режим “не уверен” и предложение перезаписать в тишине.

Вопрос к читателям (выберите A/B/C)

Мне нужен совет по двум решениям — выберите вариант в комментариях:

  1. Дообучение и данные:
  • A) Только opt‑in + пользователь выбирает “что было на самом деле” (дороже по UX, но лучшая разметка).
  • B) Только opt‑in + кнопки “угадал/не угадал” (хуже разметка, выше конверсия).
  • C) Не собирать аудио вообще, улучшать только препроцессинг и правила отказа.
  1. Что первым даст прирост качества:
  • A) VAD/проверка качества входа (шум, клиппинг, доля плача) + “не уверен”.
  • B) Аугментации под шум/микрофоны.
  • C) Менять модель (CRNN/Transformer) раньше, чем чинить данные.

Если вы делали аудио‑ML в проде: где я гарантированно наступлю на грабли?

Если хотите смотреть, как это всё развивается (и где оно снова развалится), я веду Telegram‑канал про разработку/продукт и этот проект: t.me/debug_leg. Туда же кидаю мелкие апдейты

Начать дискуссию