tasks: целевая картина пересобрана — три цели, тринадцать задач, сдвинутые границы
- паспорт: сервис объявлен архивом с бессрочным хранением записей и текстов, машинная вычитка расшифровки внутри границ, приложение — основной вход; добавлены две границы: не файловое хранилище общего назначения и не биллинг; - заведены цели upload-reliability, user-settings, usage-stats и тринадцать задач; очередь пересобрана — сперва починки, затем разведки о хранилище, затем доступ и владелец, и только потом экраны; - архитектура: четыре новых открытых вопроса — приём большого файла, учёт расхода, срок хранения, потолок шести часов.
This commit is contained in:
@@ -0,0 +1,37 @@
|
||||
# ✨ Сделать страницу статистики для владельца
|
||||
|
||||
- **Тип:** feature
|
||||
- **Категория:** Очередь
|
||||
- **Зачем:** Собранный учёт читается только запросом к базе руками: ни страницы, ни признака владельца в приложении нет.
|
||||
- **Теги:** goal:usage-stats
|
||||
|
||||
Двигает пункты 1, 2 и 3 «Завершения» цели: расход по каждому пользователю виден
|
||||
на странице, и открывается она только владельцу сервиса.
|
||||
|
||||
## Затрагивает
|
||||
|
||||
- новый экран приложения: таблица пользователей с объёмом, минутами и расходом,
|
||||
накопительно и за период;
|
||||
- эндпоинт сводки потребления с отбором по периоду;
|
||||
- признак владельца сервиса: откуда он берётся — из группы OIDC или из
|
||||
конфигурации;
|
||||
- `docs/security.md` — второй уровень доступа помимо «свой или чужой».
|
||||
|
||||
## Критерии приёмки
|
||||
|
||||
- Владелец видит на странице строку по каждому пользователю с объёмом, минутами
|
||||
и расходом. Оракул — тест экрана на подставном API: три пользователя, три
|
||||
строки, числа совпадают с ответом.
|
||||
- Обычный пользователь получает отказ и на странице, и на эндпоинте сводки, а не
|
||||
пустую таблицу. Оракул — тест эндпоинта под обычной учётной записью: `403`.
|
||||
- Отбор по периоду меняет числа. Оракул — тест на выборке за месяц и за всё
|
||||
время.
|
||||
- Страница не показывает ни имён файлов, ни текстов записей — только
|
||||
идентификаторы и числа. Оракул — тест ответа сводки: полей с именем и текстом
|
||||
в нём нет.
|
||||
|
||||
## Рамки
|
||||
|
||||
Берётся после `usage-accounting` — до неё показывать нечего. Управления
|
||||
пользователями на странице не делаем: заводит и отключает их Authelia. Цен и
|
||||
пересчёта в деньги здесь нет.
|
||||
@@ -0,0 +1,41 @@
|
||||
# ✨ Пускать скрипты в API по личным токенам
|
||||
|
||||
- **Тип:** feature
|
||||
- **Категория:** Очередь
|
||||
- **Зачем:** Вход через OIDC закрывает API целиком, а скрипту браузерная сессия недоступна: автоматизировать загрузку станет нечем.
|
||||
- **Теги:** goal:multi-user
|
||||
|
||||
Двигает пункты 1 и 6 «Завершения» цели: запрос без токена не проходит (пункт 1),
|
||||
а скрипт ходит в API по токену, выпущенному пользователем, и видит ровно его
|
||||
записи (пункт 6).
|
||||
|
||||
Токен принадлежит учётной записи и даёт ровно её права: записи, заведённые по
|
||||
токену, видны владельцу в приложении, и наоборот.
|
||||
|
||||
## Затрагивает
|
||||
|
||||
- заголовок авторизации у всех эндпоинтов `/api/`;
|
||||
- таблица токенов: владелец, имя, отпечаток, время выпуска и последнего
|
||||
обращения, и её миграция;
|
||||
- эндпоинты выпуска, перечня и отзыва токена;
|
||||
- экран настроек — место, где токен выпускают и отзывают;
|
||||
- `docs/security.md` — второй способ представиться и хранение отпечатка;
|
||||
- `README.md` — пример вызова API скриптом.
|
||||
|
||||
## Критерии приёмки
|
||||
|
||||
- Запрос с годным токеном заводит задачу от имени его владельца. Оракул — тест
|
||||
API: задача в репозитории с владельцем токена.
|
||||
- Запрос без токена и с отозванным токеном получает `401` и задачи не заводит.
|
||||
Оракул — тест на трёх случаях: нет заголовка, чужая строка, отозванный токен.
|
||||
- Полное значение токена показывается один раз при выпуске, в базе лежит только
|
||||
отпечаток. Оракул — тест: повторное чтение токена отдаёт имя и отпечаток,
|
||||
значение отсутствует, плюс поиск значения по логу пуст.
|
||||
- Токен не попадает ни в журнал, ни в текст ошибки. Оракул — тест приёма с
|
||||
токеном: в перехваченном журнале значения нет.
|
||||
|
||||
## Рамки
|
||||
|
||||
Учётные записи по-прежнему заводит Authelia — свою регистрацию не делаем.
|
||||
Сроков жизни и областей действия у токена не заводим: он даёт права владельца
|
||||
целиком. Берётся после `oidc-login`: до неё представляться некому.
|
||||
@@ -0,0 +1,29 @@
|
||||
# 🔬 Загрузка большого файла частями
|
||||
|
||||
- **Тип:** research
|
||||
- **Категория:** Очередь
|
||||
- **Зачем:** Гигабайтный файл едет одним запросом, и обрыв на девяноста процентах начинает его заново.
|
||||
|
||||
Шестичасовая диктофонная запись и видео из семейного архива весят гигабайты, а
|
||||
уходят с телефона по сотовой сети. Сегодня приём читает форму целиком и держит
|
||||
её в памяти до предела `router.MaxMultipartMemory`; что делать с обрывом,
|
||||
неизвестно.
|
||||
|
||||
Решено начать с одного запроса, а докачку разобрать отдельно — это она.
|
||||
|
||||
## Вопрос
|
||||
|
||||
Чем чинить обрыв загрузки большого файла: готовым протоколом докачки, своей
|
||||
нарезкой на части поверх обычной формы или пределом размера с отказом, и во что
|
||||
каждый вариант обходится на стороне приложения, сервера и обратного прокси.
|
||||
|
||||
## Куда ляжет ответ
|
||||
|
||||
`docs/research/upload.md` — вариантами с ценой каждого. Выбор оформляется
|
||||
решением в `docs/adr/`, потому что меняет публичный контракт приёма.
|
||||
|
||||
## Рамки
|
||||
|
||||
Ответ учитывает обратный прокси перед сервисом: его предел размера тела и
|
||||
таймаут — часть цены. Хеш-сумма на стороне приложения разбирается здесь же:
|
||||
она решает, можно ли пропустить загрузку целиком.
|
||||
@@ -0,0 +1,41 @@
|
||||
# ✨ Узнавать уже загруженный файл по хеш-сумме
|
||||
|
||||
- **Тип:** feature
|
||||
- **Категория:** Очередь
|
||||
- **Зачем:** Один и тот же файл, отправленный дважды, распознаётся дважды и оплачивается дважды: приём не смотрит на содержимое вовсе.
|
||||
- **Теги:** goal:upload-reliability
|
||||
|
||||
Двигает пункт 1 «Завершения» цели: повторная отправка того же файла возвращает
|
||||
прежнюю запись вместо второй задачи.
|
||||
|
||||
Совпадение ищется **в пределах одного пользователя**: чужая расшифровка по
|
||||
совпадению хеш-суммы не отдаётся и о её существовании отправитель не узнаёт.
|
||||
|
||||
## Затрагивает
|
||||
|
||||
- `TranscribeService.createTranscribeJob` — единая точка приёма, через неё идут
|
||||
оба входа;
|
||||
- таблица `files`: колонка хеш-суммы, её миграция и индекс по паре «владелец,
|
||||
хеш-сумма»;
|
||||
- контракт `POST /api/audio`: ответ на повторный файл;
|
||||
- ответ бота на повторно присланное голосовое;
|
||||
- `docs/database.md` — представление данных.
|
||||
|
||||
## Критерии приёмки
|
||||
|
||||
- Повторная отправка того же файла тем же пользователем возвращает
|
||||
идентификатор прежней задачи, второй задачи в базе не появляется. Оракул —
|
||||
тест приёма: два вызова одним содержимым, в репозитории одна задача.
|
||||
- Тот же файл от другого пользователя заводит свою задачу и своё распознавание.
|
||||
Оракул — тест приёма с двумя владельцами: две задачи, тексты не разделяются.
|
||||
- Повторный файл не остаётся вторым экземпляром в каталоге хранения. Оракул —
|
||||
тест: после второго вызова в каталоге один файл.
|
||||
- Незавершённая задача тоже узнаётся: повторная отправка отдаёт её состояние, а
|
||||
не заводит соседнюю. Оракул — тест на задаче в состоянии `created`.
|
||||
|
||||
## Рамки
|
||||
|
||||
Владелец записи приходит из `record-ownership` — до неё дедупликация опирается
|
||||
на того владельца, который уже есть. Хеш-сумма считается на сервере: подсчёт на
|
||||
стороне приложения относится к `chunked-upload-choice`. Колонка добавляется во
|
||||
всех четырёх местах репозитория SQLite (инвариант `CLAUDE.md`).
|
||||
@@ -0,0 +1,41 @@
|
||||
# ✨ Слать готовый текст на почту из учётной записи
|
||||
|
||||
- **Тип:** feature
|
||||
- **Категория:** Очередь
|
||||
- **Зачем:** Адрес почты приходит вместе с входом через OIDC, но почтового отправителя в сервисе нет.
|
||||
- **Теги:** goal:ready-notification
|
||||
|
||||
Двигает пункты 1, 2 и 3 «Завершения» цели: готовый текст и отказ доходят
|
||||
письмом, а адрес берётся у учётной записи, а не из общего конфига.
|
||||
|
||||
Почта — второй канал рядом с тем, что заводит `ntfy-delivery`; выбор канала
|
||||
остаётся в той же единой точке, что и сейчас.
|
||||
|
||||
## Затрагивает
|
||||
|
||||
- реализация интерфейса отправителя уведомления — почтовый адаптер;
|
||||
- адрес почты из данных учётной записи OIDC и его хранение;
|
||||
- выбор канала по настройке пользователя в `completeJob` и `failJob`;
|
||||
- секция конфигурации: сервер отправки почты, учётные данные, адрес отправителя;
|
||||
- `docs/architecture.md` — внешняя зависимость и чем она отказывает;
|
||||
- `docs/security.md` — текст расшифровки уходит на почтовый сервер.
|
||||
|
||||
## Критерии приёмки
|
||||
|
||||
- Задача, дошедшая до `done` у пользователя с выбранной почтой, отправляет одно
|
||||
письмо на адрес владельца. Оракул — тест с подставным отправителем: ровно один
|
||||
вызов, адрес владельца, текст задачи в теле.
|
||||
- Недоступный почтовый сервер не мешает задаче завершиться. Оракул — тест с
|
||||
отправителем, возвращающим ошибку: задача в `done`, в журнале одна запись
|
||||
уровня `WARN`.
|
||||
- Отказ задачи доходит письмом тем же человекочитаемым текстом, что видит
|
||||
пользователь Telegram. Оракул — тест на ветке `failJob`.
|
||||
- Пароль почтового сервера не попадает ни в журнал, ни в текст письма. Оракул —
|
||||
тест отправки с перехваченным журналом.
|
||||
|
||||
## Рамки
|
||||
|
||||
Берётся после `ntfy-delivery` — интерфейс отправителя заводит она — и после
|
||||
`settings-screen`, где канал выбирают. Своего почтового сервера не поднимаем.
|
||||
Длинный текст в письме не делится на части: предел сообщения есть у Telegram, а
|
||||
не у почты.
|
||||
@@ -0,0 +1,38 @@
|
||||
# ✨ Отдавать вычитанный текст рядом с сырым
|
||||
|
||||
- **Тип:** feature
|
||||
- **Категория:** Очередь
|
||||
- **Зачем:** Сырая расшифровка идёт без знаков препинания, с повторами и словами-паразитами: читать её подряд тяжело, а другого уровня текста нет.
|
||||
- **Теги:** goal:text-insights
|
||||
|
||||
Двигает пункт «Завершения» цели про литературный текст: у записи появляется
|
||||
второй уровень — тот же разговор, вычитанный до читаемого вида.
|
||||
|
||||
Вычитку считает та же внешняя модель, что заголовок и темы. Сырой текст
|
||||
остаётся и не переписывается: уровни лежат рядом, а не поверх друг друга.
|
||||
|
||||
## Затрагивает
|
||||
|
||||
- колонка литературного текста у задачи и её миграция;
|
||||
- контракт чтения записи: поле рядом с сырым текстом;
|
||||
- шаг выводов из текста: ещё одно обращение к модели;
|
||||
- экран чтения записи: переключение между уровнями;
|
||||
- ответ бота: какой из уровней уходит в Telegram.
|
||||
|
||||
## Критерии приёмки
|
||||
|
||||
- У дошедшей до `done` записи есть литературный текст, а сырой остался
|
||||
нетронутым. Оракул — тест конвейера на подставной модели: оба поля заполнены,
|
||||
сырое совпадает с ответом распознавания.
|
||||
- Отказ модели на вычитке не роняет задачу и не портит сырой текст. Оракул —
|
||||
тест с моделью, возвращающей ошибку: задача в `done`, сырой текст на месте.
|
||||
- Выключенный в настройках уровень не запрашивается и остаётся пустым. Оракул —
|
||||
тест с выключенной вычиткой: обращений за ней нет.
|
||||
- Экран показывает оба уровня с переключением между ними. Оракул — тест экрана
|
||||
на подставном API.
|
||||
|
||||
## Рамки
|
||||
|
||||
Ручной правки текста человеком не делаем — это граница паспорта: сервис отдаёт
|
||||
машинную вычитку, а редактором не становится. Берётся после
|
||||
`llm-insights-adapter`: клиент модели заводит она.
|
||||
@@ -0,0 +1,47 @@
|
||||
# ✨ Считать заголовок, темы и пересказ внешней моделью
|
||||
|
||||
- **Тип:** feature
|
||||
- **Категория:** Очередь
|
||||
- **Зачем:** Расшифровка доходит стеной текста: ни заголовка, ни тем, ни пересказа сервис не считает, и клиента языковой модели в нём нет.
|
||||
- **Теги:** goal:text-insights
|
||||
|
||||
Двигает пункты 1, 3, 4 и 5 «Завершения» цели: у готовой записи появляются
|
||||
заголовок (1), пересказ (3) и темы (4), а отказ и молчание модели не роняют
|
||||
задачу (5).
|
||||
|
||||
Здесь появляется пятая внешняя зависимость — языковая модель с
|
||||
OpenAI-совместимым интерфейсом за шлюзом bifrost, — и текст расшифровки уходит
|
||||
ещё на одну сторону.
|
||||
|
||||
## Затрагивает
|
||||
|
||||
- `internal/contract` — интерфейс расчёта выводов из текста;
|
||||
- новый адаптер OpenAI-совместимого клиента и подставной адаптер для тестов;
|
||||
- конвейер: шаг после `transcribe`, его состояние и место в цепочке;
|
||||
- колонки заголовка, тем и пересказа у задачи и их миграция;
|
||||
- контракт чтения записи: новые поля ответа;
|
||||
- секция конфигурации: адрес шлюза, имя модели, ключ;
|
||||
- `docs/architecture.md` — внешняя зависимость и чем она отказывает;
|
||||
- `docs/security.md` — текст расшифровки уходит на внешний сервис.
|
||||
|
||||
## Критерии приёмки
|
||||
|
||||
- Дошедшая до `done` запись несёт заголовок в одну строку, список тем и пересказ
|
||||
в два-три предложения. Оракул — тест конвейера на подставной модели: три поля
|
||||
заполнены, заголовок без переносов строки.
|
||||
- Отказ и молчание модели не роняют задачу: текст доходит до человека без
|
||||
выводов. Оракул — тест с моделью, возвращающей ошибку и таймаут: задача в
|
||||
`done`, текст на месте, в журнале одна запись уровня `WARN`.
|
||||
- Уровень, выключенный в настройках пользователя, у модели не запрашивается, а
|
||||
настройка берётся та, что действовала на приёме записи. Оракул — тест с
|
||||
выключенными темами и тест с настройкой, изменённой после приёма: вызовов за
|
||||
темами нет в обоих случаях.
|
||||
- Ключ и адрес шлюза не попадают ни в журнал, ни в ответ. Оракул — тест шага с
|
||||
перехваченным журналом.
|
||||
|
||||
## Рамки
|
||||
|
||||
Литературный текст сюда не входит — это `literary-text-level`. Своей модели не
|
||||
держим и промптов в код не зашиваем сверх необходимого. Прогон на боевом ключе
|
||||
ради проверки запрещён: подставной адаптер. Обращение к модели платное, и
|
||||
объём расхода учитывает `usage-accounting`.
|
||||
@@ -0,0 +1,47 @@
|
||||
# ✨ Резать длинную запись на фрагменты и продолжать с места остановки
|
||||
|
||||
- **Тип:** feature
|
||||
- **Категория:** Очередь
|
||||
- **Зачем:** Шаг конвейера повторяется целиком: перезапуск на пятом часу шестичасовой записи начинает распознавание заново и оплачивает его второй раз.
|
||||
- **Теги:** goal:long-recordings
|
||||
|
||||
Двигает пункты 3, 5 и 6 «Завершения» цели: запись в пределах потолка доходит до
|
||||
текста целиком, долгая задача не занимает воркер на часы, а перезапуск на
|
||||
середине продолжает работу с первого неотмеченного фрагмента.
|
||||
|
||||
Запись делится на фрагменты, каждый распознаётся отдельно, готовый фрагмент
|
||||
отмечается в базе. После перезапуска работа продолжается с первого неотмеченного, а
|
||||
текст собирается из фрагментов по порядку.
|
||||
|
||||
## Затрагивает
|
||||
|
||||
- принцип «шаг конвейера идемпотентен по повтору» из `docs/architecture.md`:
|
||||
шаг перестаёт быть неделимым;
|
||||
- таблица фрагментов задачи: порядковый номер, границы по времени, состояние,
|
||||
текст — и её миграция;
|
||||
- состояния конвейера `created` → `converted` → `transcribe` → `done`;
|
||||
- конвертер: деление записи на фрагменты по времени;
|
||||
- адаптер SpeechKit: обращение на фрагмент вместо обращения на запись;
|
||||
- раскладка `data/files`: файлы фрагментов рядом с исходным.
|
||||
|
||||
## Критерии приёмки
|
||||
|
||||
- Запись длиной шесть часов доходит до текста, и текст собран из фрагментов по
|
||||
порядку без пропусков и повторов на стыках. Оракул — тест конвейера на
|
||||
подставном распознавателе, отдающем свой текст на каждый фрагмент.
|
||||
- Остановка процесса на середине не теряет распознанные фрагменты: после
|
||||
перезапуска обращений по ним нет. Оракул — тест: половина фрагментов
|
||||
отмечена, повторный проход шага зовёт распознаватель только по остатку.
|
||||
- Отказ на одном фрагменте не отменяет готовые: задача остаётся пригодной к
|
||||
повтору либо уходит в `failed` с сообщением пользователю. Оракул — тест с
|
||||
распознавателем, отказывающим на третьем фрагменте.
|
||||
- Долгая задача не останавливает короткие: голосовое на десять секунд проходит
|
||||
конвейер, пока шестичасовая запись в работе. Оракул — тест захвата: воркер
|
||||
берёт вторую задачу, пока первая держит фрагмент.
|
||||
|
||||
## Рамки
|
||||
|
||||
Длину фрагмента назначает ответ разведки `speechkit-limits` — до неё задача не
|
||||
берётся. Раскладка `data/files` меняется, а это необратимо: формат согласуется с
|
||||
человеком. Прогон на боевых ключах ради проверки запрещён — подставной
|
||||
распознаватель.
|
||||
@@ -1,13 +1,16 @@
|
||||
# 🎯 Запись длиной в несколько часов доходит до текста
|
||||
# 🎯 Запись длиной до шести часов доходит до текста
|
||||
|
||||
- **Тип:** goal
|
||||
- **Секция:** Направления
|
||||
- **Зачем:** Потолок не замерен ни на одном звене: Telegram не отдаёт больше 20 МиБ, а границы модели deferred-general неизвестны.
|
||||
- **Зачем:** Потолок не замерен ни на одном звене: Telegram не отдаёт больше 20 МиБ, границы модели deferred-general неизвестны, а перезапуск на середине начинает распознавание заново.
|
||||
- **Теги:** decomposed
|
||||
|
||||
Лекция, созвон и интервью целиком превращаются в текст. Сегодня неизвестно даже,
|
||||
на каком звене такая запись отваливается, — цель начинается с замера, а не с
|
||||
переделки.
|
||||
Лекция, созвон, интервью и диктофонная запись из семейного архива целиком
|
||||
превращаются в текст. Сегодня неизвестно даже, на каком звене такая запись
|
||||
отваливается, — цель начинается с замера, а не с переделки.
|
||||
|
||||
Расчётный потолок — **шесть часов**: он взят с запасом под диктофонные записи и
|
||||
дорожки из видео, и замер проверяет, каким звеном он ограничен на самом деле.
|
||||
|
||||
## Завершение
|
||||
|
||||
@@ -21,3 +24,5 @@
|
||||
Telegram, где предел сообщения — 4000 символов.
|
||||
5. Долгая задача не блокирует короткие: запись на три часа не останавливает
|
||||
конвейер для голосового на десять секунд.
|
||||
6. Перезапуск сервиса на середине долгой расшифровки не начинает её заново:
|
||||
работа продолжается с места остановки.
|
||||
|
||||
@@ -0,0 +1,39 @@
|
||||
# ✨ Принимать до десяти файлов одной загрузкой
|
||||
|
||||
- **Тип:** feature
|
||||
- **Категория:** Очередь
|
||||
- **Зачем:** Приём берёт один файл в запросе, а с телефона выбирают пачку сразу: десять записей значат десять заходов на экран загрузки.
|
||||
- **Теги:** goal:upload-reliability
|
||||
|
||||
Двигает пункт 2 «Завершения» цели: пачка файлов уходит одной загрузкой, и отказ
|
||||
одного не отменяет остальные.
|
||||
|
||||
## Затрагивает
|
||||
|
||||
- контракт `POST /api/audio`: несколько файлов в одной форме и ответ списком;
|
||||
- `TranscribeService.createTranscribeJob` — заведение нескольких задач одним
|
||||
запросом;
|
||||
- экран загрузки: выбор нескольких файлов и показ их состояний;
|
||||
- предел числа файлов и предел размера запроса на стороне сервера;
|
||||
- `docs/database.md` — предел числа файлов числом.
|
||||
|
||||
## Критерии приёмки
|
||||
|
||||
- Десять файлов одной формой заводят десять задач, и ответ отдаёт
|
||||
идентификатор каждой. Оракул — тест приёма: в ответе десять записей, в
|
||||
репозитории десять задач.
|
||||
- Негодный файл в пачке отклоняется поимённо, а годные соседи заводятся. Оракул
|
||||
— тест на пачке из годного и негодного: одна задача заведена, второй элемент
|
||||
ответа несёт причину отказа.
|
||||
- Одиннадцатый файл отклоняется до чтения содержимого, с названным пределом.
|
||||
Оракул — тест на пачке из одиннадцати: задач не заведено, в теле ответа
|
||||
предел числом.
|
||||
- Прежний вызов с одним файлом работает по-старому. Оракул — существующие тесты
|
||||
приёма по HTTP.
|
||||
|
||||
## Рамки
|
||||
|
||||
Публичный контракт `POST /api/audio` меняется, а это необратимо — форма ответа
|
||||
согласуется с человеком. Загрузка частями сюда не входит: это
|
||||
`chunked-upload-choice`. Приём из Telegram не трогается — бот присылает файлы по
|
||||
одному.
|
||||
@@ -19,3 +19,5 @@
|
||||
ботом, видны ему же в браузере.
|
||||
5. Белый список Telegram перестаёт быть отдельным механизмом: право писать боту
|
||||
выводится из учётной записи.
|
||||
6. Скрипт ходит в API по токену, выпущенному пользователем, и видит ровно его
|
||||
записи.
|
||||
|
||||
@@ -5,9 +5,10 @@
|
||||
- **Зачем:** Пользователь веба узнаёт о готовности только опросом с открытого экрана.
|
||||
- **Теги:** goal:ready-notification
|
||||
|
||||
Двигает все пять пунктов «Завершения» цели: готовый текст и отказ доходят до
|
||||
пользователя веба без открытого приложения, адрес канала свой у каждого, а отказ
|
||||
канала задачу не роняет.
|
||||
Двигает пункты 1, 2, 4 и 5 «Завершения» цели: готовый текст и отказ доходят до
|
||||
пользователя веба без открытого приложения, отказ канала задачу не роняет, а
|
||||
пользователь Telegram получает ответ по-прежнему ботом. Выбор канала самим
|
||||
пользователем (пункт 3) заводит `settings-screen`.
|
||||
|
||||
Сегодня `completeJob` и `failJob` отвечают только источнику `telegram`;
|
||||
источник `api` не получает ничего. Здесь появляется второй способ доставки, и
|
||||
@@ -19,7 +20,8 @@
|
||||
- `internal/service`, `completeJob` и `failJob` — выбор канала по источнику
|
||||
задачи;
|
||||
- новый адаптер поверх apprise либо прямого HTTP к ntfy;
|
||||
- адрес канала у учётной записи: колонка и её миграция, экран настройки;
|
||||
- адрес канала у учётной записи: колонка и её миграция (экран настройки заводит
|
||||
`settings-screen`);
|
||||
- секция конфигурации: адрес сервера ntfy, способ вызова apprise;
|
||||
- `docs/architecture.md` — новая внешняя зависимость и чем она отказывает;
|
||||
- `docs/security.md` — текст расшифровки уходит на внешний сервис;
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
- **Тип:** fix
|
||||
- **Категория:** Очередь
|
||||
- **Зачем:** Отказ чтения метаданных и отказ записи на диск оставляют файл в каталоге хранения без задачи и без учёта: сопоставить его не с чем, удалять приходится руками.
|
||||
- **Теги:** review-2026-08-11
|
||||
- **Теги:** review-2026-08-11, goal:upload-reliability
|
||||
|
||||
Приём пишет файл на диск, потом спрашивает у источника метаданных длительность,
|
||||
потом заводит запись в учёте и задачу. Уборка при отказе есть **только на
|
||||
|
||||
@@ -0,0 +1,29 @@
|
||||
# 🔬 Админка PocketBase: данные, пользователи и файлы
|
||||
|
||||
- **Тип:** research
|
||||
- **Категория:** Очередь
|
||||
- **Зачем:** Переход на PocketBase решён ради его панели администратора, а что она даёт по данным, пользователям и файлам на диске — не проверено.
|
||||
|
||||
Задача `pocketbase-storage` обоснована тремя доводами, и два из них ушли: учётные
|
||||
записи заводит Authelia, а страницу статистики делает само приложение. Остался
|
||||
довод про панель администратора — и он не проверен: что она показывает, правит
|
||||
ли записи, видит ли пользователей, пришедших по OIDC, и что умеет с файлами на
|
||||
диске.
|
||||
|
||||
## Вопрос
|
||||
|
||||
Что панель администратора PocketBase даёт по трём частям — правка записей задач
|
||||
и файлов, список пользователей при входе через внешнего провайдера, работа с
|
||||
файлами в хранилище, — и достаточно ли этого, чтобы держать перевод хранилища в
|
||||
планах.
|
||||
|
||||
## Куда ляжет ответ
|
||||
|
||||
`docs/research/pocketbase.md` — перечнем по каждой части, с версией PocketBase, на
|
||||
которой смотрели. Итог правит «зачем» задачи `pocketbase-storage` либо закрывает
|
||||
её через `REJECTED.md`.
|
||||
|
||||
## Рамки
|
||||
|
||||
Смотрится на пустой локальной базе, боевые данные не участвуют. Ответ не решает,
|
||||
чем становится очередь задач: это разведка `job-queue-choice`.
|
||||
@@ -2,13 +2,18 @@
|
||||
|
||||
- **Тип:** chore
|
||||
- **Категория:** Очередь
|
||||
- **Зачем:** Хранилище, учётные записи и веб-панель нужны все три, и SQLite с goqu не даёт ни второго, ни третьего.
|
||||
- **Зачем:** Из трёх доводов за перевод остался один: учётные записи ушли к Authelia, страницу статистики рисует само приложение, а панель администратора не проверена.
|
||||
|
||||
PocketBase встраивается библиотекой в тот же бинарник и приносит хранилище,
|
||||
учётные записи и панель администратора разом. Наблюдаемое поведение сервиса
|
||||
после перевода не меняется: те же два входа, тот же конвейер, тот же текст на
|
||||
выходе.
|
||||
|
||||
Из трёх доводов 2026-08-11 осталось полтора: учётные записи заводит Authelia по
|
||||
OIDC, страницу статистики рисует само приложение. Довод про панель
|
||||
администратора проверяет разведка `pocketbase-admin-fit`, и её ответ решает,
|
||||
берётся эта задача или уходит в `REJECTED.md`.
|
||||
|
||||
Данные не переносим — база заводится с чистого листа, и это решение принято
|
||||
сознательно.
|
||||
|
||||
|
||||
@@ -8,8 +8,10 @@
|
||||
когда текст готов. Пользователь Telegram это уже имеет: бот отвечает сам.
|
||||
Пользователь веба — нет.
|
||||
|
||||
Доставку берём внешнюю: apprise как отправитель, ntfy как канал. Web Push с
|
||||
VAPID и собственным хранением подписок за целью **не стоит** — это отдельная
|
||||
Каналов два: почта, адрес которой приходит вместе с входом через OIDC, и
|
||||
Telegram для того, кто связал свою учётную запись с ботом. Доставку берём
|
||||
внешнюю: apprise как отправитель, ntfy как один из каналов. Web Push с VAPID и
|
||||
собственным хранением подписок за целью **не стоит** — это отдельная
|
||||
инфраструктура ради того же результата.
|
||||
|
||||
## Завершение
|
||||
@@ -18,8 +20,8 @@ VAPID и собственным хранением подписок за цел
|
||||
приложения.
|
||||
2. Отказ задачи доходит тем же путём и тем же человекочитаемым текстом, что
|
||||
видит пользователь Telegram.
|
||||
3. Адрес канала уведомлений задаёт пользователь, а не общий конфиг: у каждого
|
||||
свой.
|
||||
3. Канал и адрес уведомлений задаёт пользователь, а не общий конфиг: у каждого
|
||||
свой, и от уведомлений можно отказаться.
|
||||
4. Недоступность канала уведомлений не роняет задачу и не мешает ей завершиться:
|
||||
текст остаётся в приложении.
|
||||
5. Пользователь Telegram получает ответ по-прежнему ботом, а не вторым каналом.
|
||||
|
||||
@@ -0,0 +1,41 @@
|
||||
# ✨ Сделать экран настроек и хранить настройки по пользователю
|
||||
|
||||
- **Тип:** feature
|
||||
- **Категория:** Очередь
|
||||
- **Зачем:** Настроек у пользователя нет вовсе: уровни текста и канал уведомлений задаются общим конфигом сервиса.
|
||||
- **Теги:** goal:user-settings
|
||||
|
||||
Двигает пункты 1, 2 и 3 «Завершения» цели: у каждого пользователя свои
|
||||
переключатели уровней текста и свой канал уведомлений, и они переживают
|
||||
повторный вход.
|
||||
|
||||
Здесь появляется дом настроек — таблица и эндпоинты; применяют их задачи
|
||||
уровней текста и доставки уведомлений.
|
||||
|
||||
## Затрагивает
|
||||
|
||||
- таблица настроек пользователя: переключатели уровней текста, канал и адрес
|
||||
уведомлений, и её миграция;
|
||||
- эндпоинты чтения и записи своих настроек;
|
||||
- новый экран приложения;
|
||||
- значения по умолчанию для пользователя, у которого настроек ещё нет;
|
||||
- `docs/database.md` — представление данных и значения по умолчанию.
|
||||
|
||||
## Критерии приёмки
|
||||
|
||||
- Изменённая настройка видна после повторного входа и не меняется у соседа.
|
||||
Оракул — тест эндпоинта на двух учётных записях: запись первой, чтение обеими.
|
||||
- Пользователь без сохранённых настроек читает значения по умолчанию, а не
|
||||
пустой ответ. Оракул — тест чтения для новой учётной записи.
|
||||
- Чужие настройки не читаются и не пишутся по идентификатору. Оракул — тест
|
||||
запроса к настройкам другого владельца: `404` либо `403`, содержимое не
|
||||
отдаётся.
|
||||
- Экран показывает переключатели уровней и поле канала, а сохранение доходит до
|
||||
эндпоинта. Оракул — тест экрана на подставном API.
|
||||
|
||||
## Рамки
|
||||
|
||||
Применением настроек к конвейеру занимаются `llm-insights-adapter`,
|
||||
`literary-text-level` и `ntfy-delivery` — здесь только дом настроек и экран.
|
||||
Выпуск токенов API живёт на этом же экране, но заводит его `api-tokens`.
|
||||
Берётся после `oidc-login`: до неё настройки не к кому привязать.
|
||||
@@ -4,22 +4,32 @@
|
||||
- **Секция:** Направления
|
||||
- **Зачем:** Расшифровка часового разговора — это стена текста: найти в списке нужную запись и вспомнить, о чём она, сегодня нечем.
|
||||
|
||||
К расшифровке добавляется короткий заголовок, пересказ и темы. Считает их
|
||||
внешний сервис с OpenAI-совместимым интерфейсом — своей модели не держим, как не
|
||||
держим и модели распознавания.
|
||||
Текст записи выдаётся уровнями: сырая расшифровка, вычитанный литературный
|
||||
текст, заголовок в одну строку, темы-теги и пересказ в два-три предложения.
|
||||
Считает их внешний сервис с OpenAI-совместимым интерфейсом за шлюзом bifrost —
|
||||
своей модели не держим, как не держим и модели распознавания.
|
||||
|
||||
**Это сдвиг границы паспорта.** До 2026-08-10 «понимание сказанного» было
|
||||
записано как то, чем проект не является: «мы отдаём текст, а не выводы из него».
|
||||
Граница сдвинута сознательно, и в паспорте от неё остался более узкий запрет —
|
||||
ответов на вопросы по записи и поиска по смыслу не делаем.
|
||||
Каждый уровень сверх сырого пользователь выключает у себя — это цель
|
||||
[user-settings](user-settings.md).
|
||||
|
||||
**Это сдвиг границы паспорта, и сдвигали её дважды.** До 2026-08-10 «понимание
|
||||
сказанного» было записано как то, чем проект не является: «мы отдаём текст, а не
|
||||
выводы из него». 2026-08-11 к выводам добавился литературный текст — машинная
|
||||
вычитка расшифровки, прежде запрещённая строкой про редактор.
|
||||
|
||||
От обоих запретов остались более узкие: ответов на вопросы по записи и поиска по
|
||||
смыслу не делаем, правку текста руками человеку не даём.
|
||||
|
||||
## Завершение
|
||||
|
||||
1. У готовой записи есть заголовок в одну строку, и он виден в списке вместо
|
||||
первых слов расшифровки.
|
||||
2. У записи есть пересказ, который читается быстрее самой расшифровки.
|
||||
3. У записи есть темы, и по ним список отбирается.
|
||||
4. Отказ или молчание внешнего сервиса не роняют задачу: расшифровка доходит до
|
||||
человека без заголовка и пересказа.
|
||||
5. Стоимость обращения к сервису видна метрикой: сколько записей обработано и
|
||||
2. Рядом с сырой расшифровкой лежит вычитанный текст: со знаками препинания, без
|
||||
повторов и слов-паразитов.
|
||||
3. У записи есть пересказ в два-три предложения, который читается быстрее самой
|
||||
расшифровки.
|
||||
4. У записи есть темы, и по ним список отбирается.
|
||||
5. Отказ или молчание внешнего сервиса не роняют задачу: расшифровка доходит до
|
||||
человека без выводов из неё.
|
||||
6. Стоимость обращения к сервису видна метрикой: сколько записей обработано и
|
||||
сколько это стоило по числу токенов.
|
||||
|
||||
@@ -0,0 +1,34 @@
|
||||
# ✨ Показывать ход загрузки записи на экране
|
||||
|
||||
- **Тип:** feature
|
||||
- **Категория:** Очередь
|
||||
- **Зачем:** Гигабайтный файл уходит на сервер молча: до ответа сервера экран не отличает идущую загрузку от зависшей.
|
||||
- **Теги:** goal:upload-reliability
|
||||
|
||||
Двигает пункт 3 «Завершения» цели: ход загрузки виден числом, а не одним
|
||||
ожиданием.
|
||||
|
||||
Загрузка шестичасовой записи по сотовой сети идёт минутами. Пока сервер не
|
||||
ответил, экран показывает долю отправленного и позволяет её отменить.
|
||||
|
||||
## Затрагивает
|
||||
|
||||
- экран загрузки: доля отправленного по каждому файлу пачки и отмена;
|
||||
- отправка формы с отслеживанием хода вместо простого ожидания ответа;
|
||||
- поведение при обрыве связи: что видит человек и что предлагается дальше.
|
||||
|
||||
## Критерии приёмки
|
||||
|
||||
- Доля отправленного растёт по ходу загрузки и доходит до конца. Оракул — тест
|
||||
экрана на подставной отправке, отдающей события хода: показанные значения
|
||||
растут и заканчиваются на сотне.
|
||||
- Отмена прекращает загрузку, и задача на сервере не заводится. Оракул — тест:
|
||||
после отмены запрос прерван, вызовов приёма нет.
|
||||
- Обрыв связи показывается человекочитаемым текстом с предложением повторить, а
|
||||
не молчанием. Оракул — тест на отправке, завершающейся ошибкой сети.
|
||||
|
||||
## Рамки
|
||||
|
||||
Докачки с места обрыва здесь нет — повтор начинает загрузку заново, а протокол
|
||||
докачки разбирает `chunked-upload-choice`. Берётся после
|
||||
`upload-and-status-screen`: экран заводит она.
|
||||
@@ -0,0 +1,24 @@
|
||||
# 🎯 Загрузка большого файла доходит до сервиса и не повторяется впустую
|
||||
|
||||
- **Тип:** goal
|
||||
- **Секция:** Запланировано
|
||||
- **Зачем:** Приём рассчитан на голосовое в пару мегабайт: обрыв на середине гигабайтного файла начинает загрузку заново, а один и тот же файл распознаётся повторно за наши деньги.
|
||||
|
||||
Человек отдаёт диктофонную запись или видео из семейного архива с телефона, по
|
||||
сотовой сети, и загрузка либо доходит, либо честно говорит, что не дошла.
|
||||
Отданное однажды второй раз не грузится и второй раз не распознаётся.
|
||||
|
||||
Дедупликация ищет совпадение **в пределах одного пользователя**: чужая
|
||||
расшифровка не достаётся по совпадению хеш-суммы, даже когда файл тот же.
|
||||
|
||||
Загрузка частями за целью пока **не стоит**: сначала один запрос, а протокол
|
||||
докачки разбирает разведка.
|
||||
|
||||
## Завершение
|
||||
|
||||
1. Файл, уже загруженный этим пользователем, узнаётся по хеш-сумме: сервис
|
||||
возвращает прежнюю запись и не заводит вторую задачу.
|
||||
2. До десяти файлов уходят одной загрузкой, и отказ одного не отменяет
|
||||
остальные.
|
||||
3. Ход загрузки виден на экране числом, а не одним ожиданием.
|
||||
4. Оборванная загрузка не оставляет ни файла в хранилище, ни задачи в очереди.
|
||||
@@ -0,0 +1,47 @@
|
||||
# ✨ Считать объём, минуты и расход по каждому пользователю
|
||||
|
||||
- **Тип:** feature
|
||||
- **Категория:** Очередь
|
||||
- **Зачем:** Ни объём, ни длительность, ни обращения к платным сервисам никуда не записываются: восстановить расход задним числом не из чего.
|
||||
- **Теги:** goal:usage-stats
|
||||
|
||||
Двигает пункты 1, 2 и 4 «Завершения» цели: по каждому пользователю копятся
|
||||
объём, минуты и расход на внешние сервисы, и повтор шага не удваивает счёт.
|
||||
|
||||
Учёт ведётся записями о потреблении, а не счётчиком в строке пользователя:
|
||||
счётчик, увеличенный дважды при повторе шага, обратно не отматывается.
|
||||
|
||||
## Затрагивает
|
||||
|
||||
- таблица записей потребления: владелец, задача, вид ресурса, величина, время —
|
||||
и её миграция;
|
||||
- `TranscribeService.createTranscribeJob` — размер файла и длительность записи
|
||||
на приёме;
|
||||
- `TranscribeService.completeJob` и `failJob` — запись потребления после
|
||||
успешного шага и после отказа;
|
||||
- адаптер SpeechKit и адаптер языковой модели: откуда берутся минуты и токены;
|
||||
- `internal/metrics` — счётчики того же расхода без разбивки по людям;
|
||||
- `docs/database.md` — представление данных.
|
||||
|
||||
## Критерии приёмки
|
||||
|
||||
- Дошедшая до `done` запись оставляет строки потребления: объём файла,
|
||||
длительность в минутах, минуты распознавания и токены языковой модели. Оракул
|
||||
— тест конвейера на подставных адаптерах: четыре строки с владельцем задачи.
|
||||
- Повторный проход шага после перезапуска не удваивает потребление. Оракул — тест:
|
||||
шаг выполнен дважды, сумма по задаче не изменилась.
|
||||
- Отказавшая задача сохраняет то, что уже потрачено, а не обнуляет счёт. Оракул
|
||||
— тест на ветке `failJob` после успешной конвертации.
|
||||
- Величина расхода не смешивает пользователей: выборка по владельцу отдаёт
|
||||
только его строки. Оракул — тест на двух владельцах.
|
||||
|
||||
## Рамки
|
||||
|
||||
Снаружи эта задача не видна сама по себе: ни экрана, ни эндпоинта она не
|
||||
заводит — их строит `admin-stats-screen`. Тип оставлен `feature` сознательно,
|
||||
как шаг цели.
|
||||
|
||||
Потолков и отказов по исчерпании квоты не заводим — цель показывает, а не
|
||||
ограничивает. Пересчёт расхода в деньги здесь не делается: копятся минуты и
|
||||
токены, цена прайс-листа живёт вне сервиса. Данные о потреблении содержат
|
||||
идентификаторы, но не текст записи и не имя файла (инвариант приватности).
|
||||
@@ -0,0 +1,24 @@
|
||||
# 🎯 Владелец видит, кто сколько загрузил и во что это обошлось
|
||||
|
||||
- **Тип:** goal
|
||||
- **Секция:** Сопровождение
|
||||
- **Зачем:** Распознавание и языковая модель оплачиваются по факту, а счёт приходит одной суммой: кто её набрал, из сервиса не выясняется.
|
||||
|
||||
Владелец открывает страницу и видит по каждому пользователю объём загруженного,
|
||||
длительность записей в минутах и расход на внешние сервисы. Приглашая человека,
|
||||
он понимает, во что это обойдётся.
|
||||
|
||||
Цель **показывает, но не ограничивает**: потолков и отказов по исчерпании квоты
|
||||
здесь нет — перебравшего останавливает разговор или отзыв доступа в Authelia.
|
||||
|
||||
Секция — сопровождение, потому что наблюдает владелец сервиса, а не его
|
||||
пользователь.
|
||||
|
||||
## Завершение
|
||||
|
||||
1. По каждому пользователю видны объём загруженных файлов и длительность записей
|
||||
в минутах, накопительно и за период.
|
||||
2. Видно, во что обошлись внешние сервисы: минуты распознавания и число токенов
|
||||
языковой модели.
|
||||
3. Страница открывается только владельцу, обычному пользователю она недоступна.
|
||||
4. Учёт переживает перезапуск и не считает одну запись дважды при повторе шага.
|
||||
@@ -0,0 +1,21 @@
|
||||
# 🎯 Пользователь настраивает, что сервис делает с его записями
|
||||
|
||||
- **Тип:** goal
|
||||
- **Секция:** Запланировано
|
||||
- **Зачем:** Уровни текста считает платная модель, а уведомления приходят одним общим способом: отказаться от лишнего и выбрать свой канал пользователю нечем.
|
||||
|
||||
У каждого своя мера: одному нужна только сырая расшифровка, другому — все пять
|
||||
уровней текста. Настройки принадлежат человеку, а не общему конфигу сервиса, и
|
||||
переживают выход и повторный вход.
|
||||
|
||||
Выключенный уровень **не считается вовсе**: настройка экономит деньги, а не
|
||||
прячет готовое.
|
||||
|
||||
## Завершение
|
||||
|
||||
1. Каждый уровень текста сверх сырого включается и выключается отдельно, и
|
||||
выключенный не запрашивается у языковой модели.
|
||||
2. Канал уведомлений выбирает сам пользователь, а не общий конфиг.
|
||||
3. Настройки переживают выход и повторный вход, и у каждого пользователя свои.
|
||||
4. Запись, заведённая до правки настроек, обрабатывается по той настройке,
|
||||
которая действовала на приёме.
|
||||
@@ -9,6 +9,9 @@
|
||||
открывается с ярлыка, как обычное приложение. Конвейер обработки при этом
|
||||
остаётся прежним — меняется вход и способ показать результат.
|
||||
|
||||
**Приложение — основной вход сервиса**, бот остаётся дополнением для голосовых
|
||||
сообщений. Экраны рисуются сначала под телефон, потом под широкий экран.
|
||||
|
||||
Границы взяты уже: запись звука в самом приложении и работа без сети за целью
|
||||
**не стоят** — файл выбирают в системном диалоге, а без сети приложение
|
||||
показывает, что связи нет.
|
||||
|
||||
Reference in New Issue
Block a user