Пересмотр очереди, выводы из текста, наблюдаемость

Разведка job-queue-choice — очередь написана вручную: захват не
транзакционен, повторов и счётчика попыток нет, очереди мёртвых задач
нет. Стоит перед сменой хранилища, чтобы не переписывать захват дважды.

Цель text-insights: заголовок, пересказ и темы внешним сервисом с
OpenAI-совместимым интерфейсом. Граница паспорта сдвинута — «понимание
сказанного» было записано как то, чем проект не является; за границей
остались ответы на вопросы по записи и поиск по смыслу.

Цель service-observability в сопровождении и разведка opentelemetry-fit:
/metrics остаётся и развивается, способ решает замер.
This commit is contained in:
av
2026-08-10 21:46:08 +03:00
parent d21da8575c
commit bb973b0a68
9 changed files with 155 additions and 4 deletions
+13
View File
@@ -138,3 +138,16 @@
сервис определяет содержимое сам, то ли часть записей теряется на этом.
- **Видео.** Дорожку из видеофайла бот принимает по MIME-типу `video/`, но
конвертер этот случай не проверялся.
- **Очередь.** Принцип «очередь таблицей» и захват `FindAndAcquire` написаны
вручную: захват не транзакционен, повторов с нарастающей паузой нет, число
попыток не считается, очереди мёртвых задач нет. Пересматривается разведкой
`job-queue-choice` — раньше смены хранилища, чтобы не переписывать захват
дважды.
- **Наблюдаемость.** `/metrics` остаётся и развивается. Чем — дописывать
счётчики через `client_golang` или перейти на OpenTelemetry с трассировкой —
решает разведка `opentelemetry-fit`. Коллектор был бы процессом, которого в
выкладке сегодня нет.
- **Выводы из текста.** Заголовок, пересказ и темы решено считать внешним
сервисом с OpenAI-совместимым интерфейсом. Появляется пятая внешняя
зависимость, платная, и текст расшифровки начинает уходить ещё на одну
сторону — сдвиг периметра [security.md](security.md).
+6 -4
View File
@@ -38,10 +38,12 @@
и поиском по прошлым записям сервис не становится. Сколько запись лежит на
диске после обработки — вопрос срока хранения, а его нет вовсе:
[database.md](database.md), «Представление данных».
- **Понимание сказанного.** Пересказ, выжимка, ответы на вопросы по записи, поиск
по смыслу — за границей: мы отдаём текст, а не выводы из него.
- **Собственное распознавание.** Модель не обучаем и не держим у себя, речь
распознаёт внешний сервис.
- **Разговор о записи.** Ответы на вопросы по содержанию и поиск по смыслу — за
границей. Заголовок, пересказ и темы **внутри** границы: она сдвинута
2026-08-10, и до того запись читалась «мы отдаём текст, а не выводы из него».
Направление — цель [text-insights](../tasks/items/text-insights.md).
- **Собственные модели.** Не обучаем и не держим у себя ни модель распознавания,
ни языковую модель: и речь, и выводы из текста считает внешний сервис.
- **Управление учётными записями.** Пользователей заводит и проверяет внешний
провайдер, свою регистрацию и свои пароли не делаем.
- **Живая расшифровка.** Работаем с готовой записью, поток в реальном времени не
+1
View File
@@ -113,6 +113,7 @@
- изменение, трогающее конвейер задач целиком: состояние, воркер, шаг сервиса и
колонку разом;
- замена хранилища или переход на PocketBase — любой её кусок;
- смена модели очереди: захват, повторы и воркеры разом;
- каркас приложения: сборка фронтенда, раздача статики и шаг гейта разом;
- изменение, трогающее оба входа сразу — Telegram и HTTP.
+2
View File
@@ -23,6 +23,7 @@
- [🐞 Починить тесты http-обработчика, ни разу не бывшие зелёными](items/http-handler-tests-never-green.md) — go test ./... падает на master: тесты требуют файла, которого нет в репозитории, и ждут 201 от ffprobe, которому скормили строку.
- [🧹 Сравнивать доменные ошибки через errors.As](items/errors-as-instead-of-typecast.md) — NoopJobError и JobNotFoundError проверяются приведением типа: первая же обёртка %w между слоями сломает проверку молча.
- [🧹 Задать таймауты обращениям к внешним сервисам](items/external-call-timeouts.md) — Ни у Telegram, ни у Object Storage, ни у SpeechKit нет таймаута: молчащий собеседник держит шаг конвейера до истечения часового захвата.
- [🔬 Очередь задач: своя таблица или готовая библиотека](items/job-queue-choice.md) — Очередь написана вручную: захват двумя запросами без транзакции, протухание временем, опрос раз в секунду вхолостую тремя воркерами.
- [🧹 Перевести хранилище на встроенный PocketBase](items/pocketbase-storage.md) — Хранилище, учётные записи и веб-панель нужны все три, и SQLite с goqu не даёт ни второго, ни третьего.
- [✨ Пускать в приложение только после входа через OIDC](items/oidc-login.md) — HTTP API открыт наружу без аутентификации: любой из интернета заводит задачи за наши деньги и читает чужие расшифровки по идентификатору.
- [✨ Привязать запись к владельцу и отдавать только свои](items/record-ownership.md) — У задачи и файла нет владельца, поэтому знание UUID задачи и есть право её читать.
@@ -34,4 +35,5 @@
- [✨ Сделать экран списка своих записей и чтения текста](items/records-list-screen.md) — Расшифровка сегодня доходит одним сообщением и теряется в переписке; вернуться к ней через неделю нечем.
- [✨ Сделать приложение устанавливаемым на телефон](items/installable-pwa.md) — Приложение, живущее вкладкой браузера, теряется среди прочих: ярлыка на экране у него нет.
- [✨ Отправлять готовый текст через apprise и ntfy](items/ntfy-delivery.md) — Пользователь веба узнаёт о готовности только опросом с открытого экрана.
- [🔬 Стоит ли брать OpenTelemetry вместо голого Prometheus](items/opentelemetry-fit.md) — Метрик одиннадцать штук на пять счётчиков, трассировки нет вовсе: путь одной записи по конвейеру собирается только чтением логов глазами.
- [🔬 Потолки SpeechKit по длине записи и по формату](items/speechkit-limits.md) — Потолок длины записи и перечень принимаемых форматов неизвестны, а цель про долгие записи без них не начинается.
+3
View File
@@ -30,9 +30,12 @@
- [🎯 Принимается запись любого формата, включая дорожку из видео](items/any-audio-source.md) — Конвертер вызывается одной командой ffmpeg, проверенной на голосовых Telegram; что он берёт помимо них, никто не мерил.
- [🎯 Запись длиной в несколько часов доходит до текста](items/long-recordings.md) — Потолок не замерен ни на одном звене: Telegram не отдаёт больше 20 МиБ, а границы модели deferred-general неизвестны.
- [🎯 Приложение показывает, о чём запись, не читая её целиком](items/text-insights.md) — Расшифровка часового разговора — это стена текста: найти в списке нужную запись и вспомнить, о чём она, сегодня нечем.
## Сопровождение
- [🎯 Состояние сервиса видно без чтения логов](items/service-observability.md) — Отказ замечает пользователь, а не владелец: оповещения нет, а путь записи по конвейеру собирается глазами по логам контейнера.
## Готово
- 2025-08-14 `telegram-transcription` — Голосовое сообщение из Telegram возвращается текстом. Основной вход сервиса: бот принимает голосовое, аудиофайл и документ с аудио и отвечает расшифровкой.
+46
View File
@@ -0,0 +1,46 @@
# 🔬 Очередь задач: своя таблица или готовая библиотека
- **Тип:** research
- **Категория:** Очередь
- **Зачем:** Очередь написана вручную: захват двумя запросами без транзакции, протухание временем, опрос раз в секунду вхолостую тремя воркерами.
Сегодняшняя очередь — принцип «очередь таблицей» из
[architecture.md](../../docs/architecture.md) плюс `FindAndAcquire` в
репозитории. Работает она на единицах записей в день, но написана целиком
своими руками, и вот чего в ней нет:
- **захват не транзакционен** — `UPDATE` проставляет `acquisition_id`, отдельный
`SELECT` читает строку по нему; сегодня это безопасно только потому, что три
воркера читают три разных состояния и одну строку не делят;
- **повторов с нарастающей паузой нет** — задача либо повторяется каждую
секунду, либо ждёт истечения захвата целый час;
- **число попыток не считается** — задача, падающая всегда, падает вечно;
- **опрос вхолостую** — три воркера дёргают базу раз в секунду независимо от
того, есть ли работа;
- **очереди мёртвых задач нет** — `is_error = 1` исключает запись из выборки
навсегда и молча.
Разведка идёт **перед** `pocketbase-storage`: смена хранилища перепишет захват
задачи в любом случае, и переписывать его дважды незачем.
## Вопрос
Берём ли готовую очередь на Go поверх той же базы, и если да — какую, или
оставляем свою таблицу, дописав к ней повторы, счётчик попыток и очередь мёртвых
задач.
## Куда ляжет ответ
`docs/research/job-queue.md` — сравнение кандидатов с числами: зависимости,
какая база нужна, переживает ли перезапуск, есть ли повторы и очередь мёртвых
задач. Решение и причина отказа от остальных — в `docs/adr/`: принцип «очередь
таблицей» записан в архитектуре, и его пересмотр обратной правкой не
откатывается. Следом правится раздел «Принципы» в
[architecture.md](../../docs/architecture.md).
## Рамки
Внешнего брокера (Redis, RabbitMQ, NATS) не рассматриваем: он добавляет к
выкладке процесс, которого там нет, ради нагрузки в единицы записей в день.
Кандидаты — библиотеки, работающие поверх той же встроенной базы. Ответ обязан
учесть, что хранилище скоро сменится на PocketBase.
+34
View File
@@ -0,0 +1,34 @@
# 🔬 Стоит ли брать OpenTelemetry вместо голого Prometheus
- **Тип:** research
- **Категория:** Очередь
- **Зачем:** Метрик одиннадцать штук на пять счётчиков, трассировки нет вовсе: путь одной записи по конвейеру собирается только чтением логов глазами.
- **Теги:** goal:service-observability
Эндпоинт `/metrics` остаётся и развивается — это решено. Вопрос в том, чем его
развивать: дописывать счётчики в `internal/metrics` напрямую через
`client_golang` или перевести на OpenTelemetry и получить заодно трассировку.
Сегодня у записи есть путь длиной в минуты через четыре внешних сервиса и три
воркера, и связать его звенья можно только по `job_id` в логах. Трассировка
отвечает на это прямо, но приносит коллектор — процесс, которого в выкладке нет.
## Вопрос
Даёт ли OpenTelemetry на этом проекте больше, чем стоит: коллектор в выкладке,
переписанные метрики и словарь имён, — или дешевле остаться на `client_golang` и
дописать недостающие счётчики.
## Куда ляжет ответ
`docs/research/opentelemetry.md` — с числами: сколько зависимостей приносит,
что появляется в выкладке, сколько метрик переписывается. Решение и причина
отказа — в `docs/adr/`, если берём: переход на другой словарь метрик ломает
собранные ряды и обратной правкой не откатывается.
## Рамки
Метрики Prometheus и путь `/metrics` из ответа не исчезают ни при каком исходе:
OpenTelemetry рассматривается как источник тех же метрик, а не как замена
эндпоинта. Своего хранилища метрик и своих панелей не поднимаем — это отдельная
работа.
+25
View File
@@ -0,0 +1,25 @@
# 🎯 Состояние сервиса видно без чтения логов
- **Тип:** goal
- **Секция:** Сопровождение
- **Зачем:** Отказ замечает пользователь, а не владелец: оповещения нет, а путь записи по конвейеру собирается глазами по логам контейнера.
Наблюдаем **мы**, а не пользователь сервиса, — поэтому цель стоит в
сопровождении, а не среди возможностей приложения. Пользовательская половина
той же темы живёт отдельно: о готовности своей записи человек узнаёт по цели
[ready-notification](ready-notification.md).
Эндпоинт `/metrics` остаётся и развивается. Чем именно развивать — голым
`client_golang` или OpenTelemetry с трассировкой — решает разведка
`opentelemetry-fit`.
## Завершение
1. По метрикам видно, что конвейер встал: задача висит в состоянии дольше
обычного, и это отличимо от «работы нет».
2. Каждый внешний сервис имеет метрику вызовов, отказов и длительности —
сегодня их нет ни у одного.
3. Путь одной записи по конвейеру собирается запросом, а не чтением логов
глазами.
4. Владелец узнаёт об отказе сам, а не от пользователя.
5. Стоимость обращений к платным сервисам видна числом.
+25
View File
@@ -0,0 +1,25 @@
# 🎯 Приложение показывает, о чём запись, не читая её целиком
- **Тип:** goal
- **Секция:** Направления
- **Зачем:** Расшифровка часового разговора — это стена текста: найти в списке нужную запись и вспомнить, о чём она, сегодня нечем.
К расшифровке добавляется короткий заголовок, пересказ и темы. Считает их
внешний сервис с OpenAI-совместимым интерфейсом — своей модели не держим, как не
держим и модели распознавания.
**Это сдвиг границы паспорта.** До 2026-08-10 «понимание сказанного» было
записано как то, чем проект не является: «мы отдаём текст, а не выводы из него».
Граница сдвинута сознательно, и в паспорте от неё остался более узкий запрет —
ответов на вопросы по записи и поиска по смыслу не делаем.
## Завершение
1. У готовой записи есть заголовок в одну строку, и он виден в списке вместо
первых слов расшифровки.
2. У записи есть пересказ, который читается быстрее самой расшифровки.
3. У записи есть темы, и по ним список отбирается.
4. Отказ или молчание внешнего сервиса не роняют задачу: расшифровка доходит до
человека без заголовка и пересказа.
5. Стоимость обращения к сервису видна метрикой: сколько записей обработано и
сколько это стоило по числу токенов.