Files
transcriber/docs/architecture.md
T
av 4d1c2bf44c Канон документов, каталог задач и OpenSpec
docs/ по канону 12: паспорт с целью проекта, архитектура сегодняшнего
устройства, схема хранилища, модель угроз, конвенции кода, журнал ревью.
Конвенции перенесены из jellybit; места, где код им не следует, помечены
строкой «Расхождение» как объявленный долг.

tasks/ с роадмапом: две достигнутые цели, две запланированные (веб и
многопользовательский режим), два направления (все форматы, долгие
записи) и пять задач в беклоге.

openspec/config.yaml — маршрутизатор с адресами документов, спек пока нет.

CLAUDE.md переписан по форме канона: инварианты с severity, семантика
гейта, запреты с путями. Taskfile получил task gate.
2026-08-10 21:19:07 +03:00

12 KiB
Raw Blame History

Архитектура

Обзор: как сложено и где что работает. Поведение системы здесь не описывается — нормативно оно живёт в openspec/specs/. Места, где оно всё-таки описано, помечены маркером долга и переезжают туда первой же задачей, которая их трогает.

Документ описывает сегодняшнее устройство. Куда проект идёт — в passport.md и в tasks/ROADMAP.md; что из этого ещё не решено — в разделе «Открытые вопросы».

Спеки ещё не заведены: capability ни одной, поведение живёт только в коде. Первая задача, которая трогает поведение, заводит спеку — до тех пор у темы requirements нормативного документа нет.

Принципы

  • Один процесс. Бот, HTTP-сервер и фоновые воркеры живут в одном бинарнике и делят одну базу. Отдельного воркер-процесса нет намеренно.
  • Очередь таблицей. Состояние задачи лежит в SQLite, воркер забирает работу запросом с захватом. Внешний брокер не заводим: нагрузка — единицы записей в день (оценка владельца, не замер: research/ пуст).
  • Шаг конвейера идемпотентен по повтору. Задача, брошенная на середине, достаётся снова по истечении срока захвата и проходит шаг заново.
  • Ядро зависит от интерфейсов. internal/service знает только internal/contract; ffmpeg, Yandex, Telegram и SQLite подставляются в main.go.

Компоненты

Каждый — строкой со ссылкой на capability, а не пересказом её требований.

Компонент Где Что делает
Telegram-бот internal/controller/tg Принимает голосовые, аудиофайлы и документы с аудио, скачивает их, заводит задачу
HTTP API internal/controller/http Приём файла и опрос статуса задачи
Воркеры internal/controller/worker Крутят по одному шагу конвейера, опрашивая базу
Сервис расшифровки internal/service Конвейер: приём, конвертация, распознавание, отдача результата
Конвертер и метаданные internal/adapter/{converter,metaviewer}/ffmpeg ffmpeg в ogg/vorbis, ffprobe для длительности
Распознаватель internal/adapter/recognizer/yandex Заливка в Object Storage и отложенное распознавание SpeechKit
Отправитель Telegram internal/adapter/telegram Отправка текста, деление длинного по словам
Репозитории internal/adapter/repo/sqlite Задачи и файлы, запросы через goqu

Конвейер: createdconvertedtranscribedone либо failed. Три воркера двигают по одному переходу, каждый опрашивает базу раз в секунду.

Внешние границы и форматы

  • Telegram Bot API. Вход — обновления длинным опросом, выход — сообщения. Файл скачивается по ссылке file.Link(token) обычным http.Get. Telegram не отдаёт файлы больше 20 МиБ — это потолок приёма из бота.
  • Yandex Object Storage. S3-совместимый, клиент aws-sdk-go-v2 с UsePathStyle. Ключ объекта — имя файла, то есть UUID с расширением.
  • Yandex SpeechKit v3. gRPC, stt.api.cloud.yandex.net:443, модель deferred-general, авторизация заголовком Api-Key. Распознавание асинхронное: запрос возвращает идентификатор операции, готовность опрашивается через operation.api.cloud.yandex.net:443, текст читается потоком.
  • ffmpeg и ffprobe. Внешние процессы, ищутся в PATH.

Эксплуатация

  • Где работает, что рядом, кто перезапускает: один контейнер на личном сервере, разворачивает и перезапускает Ansible из pet-project-server. Рядом — обратный прокси, который публикует HTTP-порт наружу.

  • Внешние зависимости поимённо и чем каждая отказывает. Столбец «отвечает медленно» читается вместе с тем, что таймаута нет ни у одного обращения наружу — database.md, «Настройки с числовым значением»:

    Зависимость Падает Отвечает медленно Молчит Отдаёт мусор
    Telegram Bot API Бот не стартует, приложение продолжает работу без него Скачивание файла висит бесконечно Длинный опрос пуст, новые задачи не заводятся Файл скачался битым, отказ вылезет на конвертации
    Yandex SpeechKit Шаг возвращает ошибку, задача остаётся на повтор Захват держится час, задача не двигается Операция вечно in progress, повтор каждые 5 секунд Пустой текст — задача завершается заглушкой «на записи нет текста»
    Yandex Object Storage Заливка падает, задача остаётся в converted То же, что падение: висит до конца захвата SpeechKit не прочитает объект и вернёт отказ операции
    ffmpeg, ffprobe Задача уходит в failed с текстом «сбой конвертации файла» Конвейер стоит: вызов синхронный Выходной файл пуст, отказ вылезет на распознавании
    SQLite (файл на диске) Приложение не стартует либо шаг падает на каждом запросе Блокировка записи держит воркеры
    Диск Запись файла падает, задача не заводится
  • Кто заметит отказ и когда: пользователь Telegram — сразу, по молчанию бота или по сообщению об ошибке. Владелец — по метрике transcriber_worker_job_count с меткой error="true" и по логам контейнера. Отдельного оповещения нет.

  • Характер потока: непрерывный, но разреженный. Бот держит длинный опрос, три воркера опрашивают базу раз в секунду вхолостую.

Единые точки проекта

Что Где
Приём аудио и заведение задачи TranscribeService.createTranscribeJob — через него идут оба входа
Захват задачи воркером TranscriptJobRepository.FindAndAcquire
Переход задачи в состояние entity.TranscribeJob.MoveToState — чистит служебные поля прошлого состояния
Завершение и отказ TranscribeService.completeJob и failJob — они же отвечают пользователю
Разбор конфигурации internal/config.LoadConfig
Метрики internal/metrics, префикс имени transcriber_

Единых точек, которых нет и которые ожидались бы: идентификаторы генерируются вызовом uuid.NewString() по месту, время — вызовом time.Now() по месту, отображения доменной ошибки в код HTTP-ответа нет — обработчик решает сам.

Деплой

Образ собирается по контракту роли app_image: task image даёт transcriber:$BUILD_ID, по умолчанию transcriber:dev. Реестр не участвует — образ едет на сервер через docker save/load. Выкладку целиком запускает человек командой inv pl -- transcriber из pet-project-server.

Сборка двухступенчатая, финальный слой — alpine с ca-certificates и ffmpeg, процесс работает под непривилегированным пользователем transcriber.

Открытые вопросы

  • Хранилище. Пробуем PocketBase взамен SQLite с goqu и goose. Не решено, чем становится конвейер задач: таблицей PocketBase с тем же захватом или чем-то другим. Данные не переносим — начинаем с чистого листа.
  • Учётные записи. Вход через OIDC, провайдер — Authelia. Не решено, где живёт сессия и как связываются пользователь Telegram и пользователь веба.
  • Веб-интерфейс. Формы нет вовсе, есть только API. Конвенция веб-UI на htmx описана в conventions/web-ui.md заранее.
  • Долгие записи. Потолок сегодня неизвестен и не замерялся: 20 МиБ на приём из Telegram — точно, ограничения deferred-general по длине — нет.
  • Формат для распознавания. Конвертер отдаёт ogg/vorbis (libvorbis), а SpeechKit получает ContainerAudio_OGG_OPUS. Расхождение не разобрано: то ли сервис определяет содержимое сам, то ли часть записей теряется на этом.
  • Видео. Дорожку из видеофайла бот принимает по MIME-типу video/, но конвертер этот случай не проверялся.