From bb973b0a68923c1c6d1574b9818a89069ebf73a7 Mon Sep 17 00:00:00 2001 From: Anton Vakhrushev Date: Mon, 10 Aug 2026 21:46:08 +0300 Subject: [PATCH] =?UTF-8?q?=D0=9F=D0=B5=D1=80=D0=B5=D1=81=D0=BC=D0=BE?= =?UTF-8?q?=D1=82=D1=80=20=D0=BE=D1=87=D0=B5=D1=80=D0=B5=D0=B4=D0=B8,=20?= =?UTF-8?q?=D0=B2=D1=8B=D0=B2=D0=BE=D0=B4=D1=8B=20=D0=B8=D0=B7=20=D1=82?= =?UTF-8?q?=D0=B5=D0=BA=D1=81=D1=82=D0=B0,=20=D0=BD=D0=B0=D0=B1=D0=BB?= =?UTF-8?q?=D1=8E=D0=B4=D0=B0=D0=B5=D0=BC=D0=BE=D1=81=D1=82=D1=8C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Разведка job-queue-choice — очередь написана вручную: захват не транзакционен, повторов и счётчика попыток нет, очереди мёртвых задач нет. Стоит перед сменой хранилища, чтобы не переписывать захват дважды. Цель text-insights: заголовок, пересказ и темы внешним сервисом с OpenAI-совместимым интерфейсом. Граница паспорта сдвинута — «понимание сказанного» было записано как то, чем проект не является; за границей остались ответы на вопросы по записи и поиск по смыслу. Цель service-observability в сопровождении и разведка opentelemetry-fit: /metrics остаётся и развивается, способ решает замер. --- docs/architecture.md | 13 ++++++++ docs/passport.md | 10 +++--- docs/review.md | 1 + tasks/BACKLOG.md | 2 ++ tasks/ROADMAP.md | 3 ++ tasks/items/job-queue-choice.md | 46 ++++++++++++++++++++++++++++ tasks/items/opentelemetry-fit.md | 34 ++++++++++++++++++++ tasks/items/service-observability.md | 25 +++++++++++++++ tasks/items/text-insights.md | 25 +++++++++++++++ 9 files changed, 155 insertions(+), 4 deletions(-) create mode 100644 tasks/items/job-queue-choice.md create mode 100644 tasks/items/opentelemetry-fit.md create mode 100644 tasks/items/service-observability.md create mode 100644 tasks/items/text-insights.md diff --git a/docs/architecture.md b/docs/architecture.md index 2d84bc0..aad1987 100644 --- a/docs/architecture.md +++ b/docs/architecture.md @@ -138,3 +138,16 @@ сервис определяет содержимое сам, то ли часть записей теряется на этом. - **Видео.** Дорожку из видеофайла бот принимает по MIME-типу `video/`, но конвертер этот случай не проверялся. +- **Очередь.** Принцип «очередь таблицей» и захват `FindAndAcquire` написаны + вручную: захват не транзакционен, повторов с нарастающей паузой нет, число + попыток не считается, очереди мёртвых задач нет. Пересматривается разведкой + `job-queue-choice` — раньше смены хранилища, чтобы не переписывать захват + дважды. +- **Наблюдаемость.** `/metrics` остаётся и развивается. Чем — дописывать + счётчики через `client_golang` или перейти на OpenTelemetry с трассировкой — + решает разведка `opentelemetry-fit`. Коллектор был бы процессом, которого в + выкладке сегодня нет. +- **Выводы из текста.** Заголовок, пересказ и темы решено считать внешним + сервисом с OpenAI-совместимым интерфейсом. Появляется пятая внешняя + зависимость, платная, и текст расшифровки начинает уходить ещё на одну + сторону — сдвиг периметра [security.md](security.md). diff --git a/docs/passport.md b/docs/passport.md index 11556ff..b06d6cc 100644 --- a/docs/passport.md +++ b/docs/passport.md @@ -38,10 +38,12 @@ и поиском по прошлым записям сервис не становится. Сколько запись лежит на диске после обработки — вопрос срока хранения, а его нет вовсе: [database.md](database.md), «Представление данных». -- **Понимание сказанного.** Пересказ, выжимка, ответы на вопросы по записи, поиск - по смыслу — за границей: мы отдаём текст, а не выводы из него. -- **Собственное распознавание.** Модель не обучаем и не держим у себя, речь - распознаёт внешний сервис. +- **Разговор о записи.** Ответы на вопросы по содержанию и поиск по смыслу — за + границей. Заголовок, пересказ и темы **внутри** границы: она сдвинута + 2026-08-10, и до того запись читалась «мы отдаём текст, а не выводы из него». + Направление — цель [text-insights](../tasks/items/text-insights.md). +- **Собственные модели.** Не обучаем и не держим у себя ни модель распознавания, + ни языковую модель: и речь, и выводы из текста считает внешний сервис. - **Управление учётными записями.** Пользователей заводит и проверяет внешний провайдер, свою регистрацию и свои пароли не делаем. - **Живая расшифровка.** Работаем с готовой записью, поток в реальном времени не diff --git a/docs/review.md b/docs/review.md index 629ec9a..bfbe415 100644 --- a/docs/review.md +++ b/docs/review.md @@ -113,6 +113,7 @@ - изменение, трогающее конвейер задач целиком: состояние, воркер, шаг сервиса и колонку разом; - замена хранилища или переход на PocketBase — любой её кусок; +- смена модели очереди: захват, повторы и воркеры разом; - каркас приложения: сборка фронтенда, раздача статики и шаг гейта разом; - изменение, трогающее оба входа сразу — Telegram и HTTP. diff --git a/tasks/BACKLOG.md b/tasks/BACKLOG.md index c15933e..cd3f342 100644 --- a/tasks/BACKLOG.md +++ b/tasks/BACKLOG.md @@ -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) — Потолок длины записи и перечень принимаемых форматов неизвестны, а цель про долгие записи без них не начинается. diff --git a/tasks/ROADMAP.md b/tasks/ROADMAP.md index ff1cd50..6089af8 100644 --- a/tasks/ROADMAP.md +++ b/tasks/ROADMAP.md @@ -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 возвращается текстом. Основной вход сервиса: бот принимает голосовое, аудиофайл и документ с аудио и отвечает расшифровкой. diff --git a/tasks/items/job-queue-choice.md b/tasks/items/job-queue-choice.md new file mode 100644 index 0000000..f6e2f6c --- /dev/null +++ b/tasks/items/job-queue-choice.md @@ -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. diff --git a/tasks/items/opentelemetry-fit.md b/tasks/items/opentelemetry-fit.md new file mode 100644 index 0000000..79b7a99 --- /dev/null +++ b/tasks/items/opentelemetry-fit.md @@ -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 рассматривается как источник тех же метрик, а не как замена +эндпоинта. Своего хранилища метрик и своих панелей не поднимаем — это отдельная +работа. diff --git a/tasks/items/service-observability.md b/tasks/items/service-observability.md new file mode 100644 index 0000000..e432af2 --- /dev/null +++ b/tasks/items/service-observability.md @@ -0,0 +1,25 @@ +# 🎯 Состояние сервиса видно без чтения логов + +- **Тип:** goal +- **Секция:** Сопровождение +- **Зачем:** Отказ замечает пользователь, а не владелец: оповещения нет, а путь записи по конвейеру собирается глазами по логам контейнера. + +Наблюдаем **мы**, а не пользователь сервиса, — поэтому цель стоит в +сопровождении, а не среди возможностей приложения. Пользовательская половина +той же темы живёт отдельно: о готовности своей записи человек узнаёт по цели +[ready-notification](ready-notification.md). + +Эндпоинт `/metrics` остаётся и развивается. Чем именно развивать — голым +`client_golang` или OpenTelemetry с трассировкой — решает разведка +`opentelemetry-fit`. + +## Завершение + +1. По метрикам видно, что конвейер встал: задача висит в состоянии дольше + обычного, и это отличимо от «работы нет». +2. Каждый внешний сервис имеет метрику вызовов, отказов и длительности — + сегодня их нет ни у одного. +3. Путь одной записи по конвейеру собирается запросом, а не чтением логов + глазами. +4. Владелец узнаёт об отказе сам, а не от пользователя. +5. Стоимость обращений к платным сервисам видна числом. diff --git a/tasks/items/text-insights.md b/tasks/items/text-insights.md new file mode 100644 index 0000000..1ec8d71 --- /dev/null +++ b/tasks/items/text-insights.md @@ -0,0 +1,25 @@ +# 🎯 Приложение показывает, о чём запись, не читая её целиком + +- **Тип:** goal +- **Секция:** Направления +- **Зачем:** Расшифровка часового разговора — это стена текста: найти в списке нужную запись и вспомнить, о чём она, сегодня нечем. + +К расшифровке добавляется короткий заголовок, пересказ и темы. Считает их +внешний сервис с OpenAI-совместимым интерфейсом — своей модели не держим, как не +держим и модели распознавания. + +**Это сдвиг границы паспорта.** До 2026-08-10 «понимание сказанного» было +записано как то, чем проект не является: «мы отдаём текст, а не выводы из него». +Граница сдвинута сознательно, и в паспорте от неё остался более узкий запрет — +ответов на вопросы по записи и поиска по смыслу не делаем. + +## Завершение + +1. У готовой записи есть заголовок в одну строку, и он виден в списке вместо + первых слов расшифровки. +2. У записи есть пересказ, который читается быстрее самой расшифровки. +3. У записи есть темы, и по ним список отбирается. +4. Отказ или молчание внешнего сервиса не роняют задачу: расшифровка доходит до + человека без заголовка и пересказа. +5. Стоимость обращения к сервису видна метрикой: сколько записей обработано и + сколько это стоило по числу токенов.