хранилище переехало с PocketBase на SQLite со своим каталогом файлов
- база своя: два пула, захват одним UPDATE ... RETURNING, шаги схемы на goose под файловым замком, одна миграция начальной схемы вместо семи прежних - транспорт переписан на net/http: свои слои, свой ограничитель частоты, отдача файла с проверкой владельца; панель /_/ и пространство /api/ исчезли - по находкам ревью: журнал не пишет путь под корнем приложения, ключ бюджета читается справа налево, узнавание известного идёт читающим пулом
This commit is contained in:
@@ -11,10 +11,11 @@
|
||||
|
||||
Сервис расшифровки аудио в текст. Принимает запись одним входом — HTTP API, —
|
||||
конвертирует её `ffmpeg` в ogg, отдаёт на отложенное распознавание Yandex
|
||||
SpeechKit и отдаёт текст тому, кто запись загрузил, по опросу готовности.
|
||||
Состояние записей, метаданные и сами файлы лежат во встроенной PocketBase, и она
|
||||
же даёт владельцу панель администратора. Вход Telegram убран 2026-08-14 —
|
||||
временно, до задачи, которая свяжет чат с учётной записью.
|
||||
SpeechKit и отдаёт текст тому, кто запись загрузил, карточкой записи.
|
||||
Состояние записей и метаданные лежат в SQLite, файлы записей — своим каталогом
|
||||
рядом с базой. Панели администратора у сервиса нет: встроенное хранилище,
|
||||
дававшее её, убрано 2026-08-22. Вход Telegram убран 2026-08-14 — временно, до
|
||||
задачи, которая свяжет чат с учётной записью.
|
||||
|
||||
Чего **не** делает: сам речь не распознаёт и своих моделей не держит, текст
|
||||
руками не правит и в форматы документов не экспортирует, учётных записей не
|
||||
@@ -25,8 +26,10 @@ SpeechKit и отдаёт текст тому, кто запись загруз
|
||||
|
||||
## Стек
|
||||
|
||||
Go 1.26 (сборке CGO не нужен; детектору гонок в гейте — нужен), встроенная PocketBase — хранилище, файлы записей и
|
||||
панель администратора, — `aws-sdk-go-v2` для Object
|
||||
Go 1.26 (сборке CGO не нужен; детектору гонок в гейте — нужен), SQLite через
|
||||
`modernc.org/sqlite` — база, — шаги схемы библиотекой `pressly/goose/v3`,
|
||||
маршруты и слои на `net/http`, файлы записей своим каталогом,
|
||||
`aws-sdk-go-v2` для Object
|
||||
Storage, gRPC-клиент Yandex SpeechKit v3, Prometheus, `slog`. Приложение — Vue 3
|
||||
с роутером пятой версии и сборкой Vite; собранное вшито в бинарник, проверяют
|
||||
его Biome и юнит-тесты Vue. Сборка —
|
||||
@@ -60,7 +63,8 @@ Taskfile, образ — Docker, выкладка — Ansible из `pet-project-
|
||||
Изъятия у инварианта нет. Оно было — секрет клиента OIDC жил ещё и в
|
||||
настройках коллекции пользователей хранилища, — и снято 2026-08-22 вместе с
|
||||
самим секретом: вход переехал на доверенный заголовок, обменивать код стало не
|
||||
на что. Чтение файла базы больше не равносильно чтению секрета.
|
||||
на что. Чтение файла базы больше не равносильно чтению секрета. Секретов в
|
||||
базе не осталось вовсе: пароль владельца от панели ушёл вместе с панелью.
|
||||
- **Содержимое записи остаётся приватным.** Текст расшифровки, имя файла
|
||||
пользователя и его сообщение в лог не пишутся — только длина и
|
||||
идентификаторы. Нарушение необратимо: строки уже уехали в журнал контейнера.
|
||||
@@ -85,20 +89,31 @@ Taskfile, образ — Docker, выкладка — Ansible из `pet-project-
|
||||
логируется, не считается в метрику и не поднимает уровень. Нарушение даёт
|
||||
запись раз в секунду на каждый воркер. **major**
|
||||
- **Миграция, уехавшая на сервер, не переписывается.** Изменение — только новым
|
||||
файлом шага. Необратимо: хранилище считает применённое по имени файла.
|
||||
файлом шага. Необратимо: учёт применённого ведёт сама база.
|
||||
**critical**
|
||||
- **Имя файла в хранилище задаёт сервис, а в журнал не идёт.** Умолчание
|
||||
PocketBase строит имя из имени, данного отправителем, — оно не применяется.
|
||||
Само имя — последняя часть ссылки `/api/files/...`, поэтому в журнал пишется
|
||||
*Снятие было, разовое:* 2026-08-22 решением владельца весь каталог шагов
|
||||
встроенного хранилища удалён и заменён одним шагом начальной схемы. Причина —
|
||||
стройка: на сервере данных нет, сервис остановлен, выкладка идёт с чистого
|
||||
листа, а новая база ведёт учёт применённого своей таблицей, которой отметки
|
||||
прежнего каталога не годятся вовсе. Граница названа: снятие кончилось этим
|
||||
изменением, и шаг начальной схемы подпадает под инвариант как всякий прежний.
|
||||
- **Имя файла на диске задаёт сервис, а в журнал не идёт.** Ни имя файла, ни имя
|
||||
подкаталога записи не строятся из имени, данного отправителем: подкаталог зовётся
|
||||
идентификатором записи, файл — идентификатором с расширением. В журнал пишется
|
||||
расширение, а не имя: строка журнала иначе стала бы бессрочным ключом к чужой
|
||||
записи. **critical**
|
||||
- **Колонки записи правятся в двух местах** пакета хранилища —
|
||||
`applyOwnedByPipeline` вместе с `applyToRecord` и `recordToAudioRecord`, —
|
||||
плюс шаг схемы. Компилятор не видит ни одного: колонка, забытая в одном из
|
||||
них, теряется молча — запись сохранится без поля либо приедет с нулевым.
|
||||
- **Колонки записи правятся в трёх местах** пакета хранилища —
|
||||
`writeOwnedByPipeline` вместе с `writeRecord`, `readRecordColumns` и
|
||||
`rowToAudioRecord`, — плюс шаг схемы. Компилятор не видит ни одного: колонка,
|
||||
забытая в одном из них, теряется молча — запись сохранится без поля, приедет с
|
||||
нулевым либо доедет до сущности пустой, и ближайшее сохранение запишет этот
|
||||
ноль поверх сохранённого.
|
||||
Мест было четыре, пока захват перечислял колонки поимённо; теперь он
|
||||
возвращает идентификатор и признак своего захвата, и перечень перестал расти
|
||||
с моделью. Сверку держат правила `internal/archrules`. **major**
|
||||
с моделью. Отображение при этом идёт **по имени колонки**: именованные
|
||||
параметры запроса и место назначения, найденное по имени, — позиционный список
|
||||
дал бы сдвиг на одно поле, который компилируется молча. Сверку держат правила
|
||||
`internal/archrules`. **major**
|
||||
- **Рубеж объявляется одним дескриптором** — `internal/entity/stage.go`. Из него
|
||||
выводятся выбор шага, отбор захвата, срок протухания захвата и предел простоя;
|
||||
перечислять рубежи порознь в каждом потребителе нельзя. Рубеж, забытый в
|
||||
@@ -108,15 +123,16 @@ Taskfile, образ — Docker, выкладка — Ansible из `pet-project-
|
||||
- **Результат пишет только держатель захвата, и держатель узнаётся значением.**
|
||||
Признак захвата уникален для каждого захвата, и запись результата условна по
|
||||
нему, а не по занятости записи. Шаг, чей захват за время работы достался
|
||||
другому — по протуханию срока или после того, как человек снял признак
|
||||
остановки в панели, — завершается без записи результата. Условие
|
||||
другому — по протуханию срока или после того, как человек вернул запись в
|
||||
работу подкомандой оснастки, — завершается без записи результата. Условие
|
||||
по непустоте признака пропустило бы обоих: два воркера писали бы в одну запись
|
||||
по очереди, портя её результат. **major**
|
||||
- **У записи есть владелец, и колонка пустого значения не принимает.** Ничья
|
||||
запись не заводится ничем — ни приёмом, ни конвейером, ни рукой в панели, — и
|
||||
держит это схема хранилища, а не договорённость. Пока обязательность жила в
|
||||
одном приёме, ничью запись заводили в панели, она уходила в конвейер, стоила
|
||||
денег на распознавание и не доставалась потом никому. Правило со стороны
|
||||
запись не заводится ничем — ни приёмом, ни конвейером, ни запросом к базе, — и
|
||||
держит это схема, а не договорённость: колонка объявлена внешним ключом на
|
||||
учётную запись и обязательна. Пока обязательность жила в одном приёме, ничью
|
||||
запись заводили руками мимо него, она уходила в конвейер, стоила денег на
|
||||
распознавание и не доставалась потом никому. Правило со стороны
|
||||
спрашивающего при этом остаётся: пустой владелец не совпадает ни с одной
|
||||
записью, потому что схема запрещает **заводить** ничью, а это правило —
|
||||
**спрашивать** ничьим именем. **major**
|
||||
@@ -137,6 +153,7 @@ gofmt -l .
|
||||
golangci-lint run
|
||||
go run ./cmd/transcriber -c config.toml # флаг -c или --config, по умолчанию config.toml
|
||||
go run ./cmd/devtools proxy # подставной прокси: ставит заголовок входа локально
|
||||
go run ./cmd/devtools resume -c config.toml <id> # вернуть остановленную запись в работу
|
||||
task front # приложение: зависимости, Biome, юнит-тесты, сборка
|
||||
task image # docker-образ; тег и раскладка — docs/architecture.md
|
||||
task gate # весь набор проверок разом
|
||||
@@ -247,8 +264,8 @@ Node на машину **не ставится**: шаг сборки прило
|
||||
## Запреты
|
||||
|
||||
- **Боевой каталог данных не трогать.** `data/` на сервере целиком: под ним и
|
||||
база (`data/data.db`), и записи живых людей
|
||||
(`data/storage/<коллекция>/<запись>/`). На стройке под ним пусто и сервис
|
||||
база (`data/transcriber.db`), и записи живых людей
|
||||
(`data/records/<запись>/`). На стройке под ним пусто и сервис
|
||||
остановлен — запрет от этого не снимается: каталог принадлежит серверу, и
|
||||
выкладка с чистого листа наполнит его снова. Локальный каталог данных — свой,
|
||||
его ронять и пересоздавать можно свободно.
|
||||
|
||||
Reference in New Issue
Block a user