From a4646c09309bd6e25eeb6ac6bbe1209406a97afa Mon Sep 17 00:00:00 2001 From: Anton Vakhrushev Date: Mon, 10 Aug 2026 20:24:30 +0300 Subject: [PATCH] =?UTF-8?q?=D0=98=D0=BD=D1=81=D1=82=D1=80=D1=83=D0=BA?= =?UTF-8?q?=D1=86=D0=B8=D0=B8=20=D0=B4=D0=BB=D1=8F=20Claude,=20=D0=BA?= =?UTF-8?q?=D0=BE=D0=BD=D1=84=D0=B8=D0=B3=20=D0=BB=D0=B8=D0=BD=D1=82=D0=B5?= =?UTF-8?q?=D1=80=D0=B0,=20=D0=B0=D0=BA=D1=82=D1=83=D0=B0=D0=BB=D0=B8?= =?UTF-8?q?=D0=B7=D0=B0=D1=86=D0=B8=D1=8F=20README?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit CLAUDE.md описывает конвейер задач, подвохи конфига и правило про четыре списка колонок в репозитории sqlite. .golangci.yml включает стандартный набор плюс errorlint; осознанные непроверенные вызовы вынесены в исключения. README приведён к коду: эндпоинты /api/audio и /api/status/:id, состояния задач, структура internal/, фактические колонки таблиц. --- .golangci.yml | 26 +++++++++ CLAUDE.md | 99 ++++++++++++++++++++++++++++++++++ README.md | 145 +++++++++++++++++++++++++++++++++++--------------- 3 files changed, 226 insertions(+), 44 deletions(-) create mode 100644 .golangci.yml create mode 100644 CLAUDE.md diff --git a/.golangci.yml b/.golangci.yml new file mode 100644 index 0000000..3758e77 --- /dev/null +++ b/.golangci.yml @@ -0,0 +1,26 @@ +version: "2" + +linters: + default: standard + enable: + - errorlint + settings: + errcheck: + exclude-functions: + # Закрытие через defer и лучшая-попытка уборки файла — осознанно без проверки + - (io.Closer).Close + - (*database/sql.DB).Close + - (*os.File).Close + - (io.ReadCloser).Close + - os.Remove + # Метод сам логирует ошибку отправки, вызывающему она не нужна + - (*git.vakhrushev.me/av/transcriber/internal/controller/tg.TelegramController).send + exclusions: + rules: + - path: _test\.go + linters: + - errcheck + +formatters: + enable: + - gofmt diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 0000000..758bea4 --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,99 @@ +# CLAUDE.md + +This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository. + +Проект и общение по нему ведём по-русски. + +## Что это + +Сервис расшифровки аудио. Два входа — Telegram-бот и HTTP API, один общий конвейер +обработки. Распознавание асинхронное, через Yandex SpeechKit; файл едет в Yandex +Object Storage, оттуда его забирает SpeechKit. + +## Команды + +```bash +go build ./... # нужен CGO: mattn/go-sqlite3 +go test ./... +go vet ./... +gofmt -l . +golangci-lint run +go run . -c config.toml # флаг -c или --config, по умолчанию config.toml +task image # docker-образ, тег из $BUILD_ID (по умолчанию dev) +``` + +`golangci-lint run` на чистом `master` даёт 4 замечания в существующем коде (два +непроверенных `Close`, два сравнения ошибок через приведение типа вместо +`errors.As`). Их пока не чинили — новые замечания отличай от этих. + +Деплой запускается не отсюда, а из `pet-project-server`: `inv pl -- transcriber`. +Плейбук сам зовёт `task image` по контракту роли `app_image`, образ едет на сервер +через `docker save`/`load`, реестр не участвует. + +`lefthook` на pre-commit гоняет `gitleaks git --staged`. + +## Конфиг + +`config.toml` в gitignore, образец — `config.dist.toml`. Разбирается в +`internal/config`; значения по умолчанию заданы в `defaultConfig()`, файл их +перекрывает. Правишь поле — правь оба места. + +Два подвоха: + +- Ключ белого списка пользователей Telegram называется `users_while_list` (опечатка + в теге toml), лежит в секции `[server]`, и в `config.dist.toml` его нет вообще. + Без него бот отвечает отказом всем. +- Список сверяется с `update.Message.From.String()` из tgbotapi — это `@username` + либо имя с фамилией, а не числовой id. + +## Конвейер задач + +Состояния `TranscribeJob`: `created` → `converted` → `transcribe` → `done` / `failed` +(константы в `internal/entity/job.go`). + +Три воркера в `main.go` крутят по одному шагу каждый, опрашивая базу раз в секунду: +`conversion_worker` (created → converted, ffmpeg в ogg), `transcribe_worker` +(converted → transcribe, заливка в S3 и старт распознавания), `check_worker` +(transcribe → done/failed, опрос операции Yandex). + +Задача захватывается через `FindAndAcquire`: один `UPDATE` проставляет +`acquisition_id`, дальше строка читается по нему. Задача с истёкшим `acquire_time` +достаётся заново — час для конвертации и распознавания, сутки для проверки. +`delay_time` откладывает следующую попытку. + +`contract.NoopJobError` — не ошибка, а «задач в этом состоянии нет». Воркер такой +случай не логирует и не считает в метрику. Возвращая ошибку из шага конвейера, не +теряй эту семантику. + +Ошибку, после которой задачу нет смысла повторять, оформляй через `failJob`: он +переводит задачу в `failed` и сам шлёт человеку понятный текст в Telegram. Возврат +обычной ошибки оставляет задачу в текущем состоянии на повтор. + +## Слои + +`internal/entity` — модели, `internal/contract` — интерфейсы и типы ошибок, +`internal/service` — конвейер, `internal/controller/{http,tg,worker}` — входы, +`internal/adapter/*` — реализации contract (ffmpeg, yandex, telegram, sqlite). +Сервис зависит только от интерфейсов `contract`; конкретные адаптеры собираются +в `main.go`. + +## База + +Миграции goose в `migrations/`, вшиты в бинарник через `//go:embed migrations/*.sql` +в `main.go` и накатываются при старте. Новый файл достаточно положить в каталог, +регистрировать нигде не надо. Диалект — `sqlite3`. + +Добавляя колонку, правь четыре места: миграцию, структуру в `internal/entity`, +и в `internal/adapter/repo/sqlite` — списки колонок в `Create`, `Save`, `GetByID` и +`FindAndAcquire`. Списки продублированы, компилятор расхождение не поймает. + +## Конвенции + +- Коммиты прямо в `master`, без веток и PR. +- В сообщении коммита не ставить трейлер `Co-Authored-By`. +- Логи — `log/slog`, структурные пары ключ-значение, логгер прокидывается + конструктором. `log.Printf` в `internal/controller/http/transcribe.go` — остаток, + на него не равняться. +- Текст, который увидит пользователь Telegram, — по-русски. Логи и комментарии в + коде — как в соседних файлах. +- Метрики Prometheus объявляются в `internal/metrics` с префиксом `transcriber_`. diff --git a/README.md b/README.md index 0315f0e..ef1f35d 100644 --- a/README.md +++ b/README.md @@ -1,22 +1,26 @@ # Transcriber Service -Сервис для расшифровки аудиозаписей с REST API. +Сервис расшифровки аудиозаписей. Два входа — Telegram-бот и HTTP API. ## Возможности -- Загрузка аудиофайлов любого формата -- Автоматическая генерация UUID для файлов -- Сохранение файлов на диск +- Приём аудио из Telegram: голосовые сообщения, аудиофайлы и документы с аудио +- Приём аудиофайлов через HTTP API +- Конвертация в ogg через ffmpeg +- Распознавание речи через Yandex SpeechKit - Отслеживание статуса задач расшифровки -- SQLite база данных для хранения метаданных +- SQLite для хранения метаданных, метрики Prometheus ## Технологии - **Веб-фреймворк**: gin-gonic/gin +- **Telegram**: go-telegram-bot-api +- **Распознавание**: Yandex SpeechKit + Yandex Object Storage (S3) +- **Конвертация**: ffmpeg - **SQL Builder**: doug-martin/goqu - **Миграции БД**: pressly/goose - **База данных**: SQLite -- **UUID**: google/uuid +- **Метрики**: prometheus/client_golang ## Установка и запуск @@ -25,12 +29,30 @@ ```bash go mod tidy ``` -3. Запустите приложение: +3. Скопируйте образец конфига и заполните его: ```bash - go run main.go + cp config.dist.toml config.toml + ``` +4. Запустите приложение: + ```bash + go run . -c config.toml ``` -Сервер запустится на порту 8080. +Сервер запустится на порту из `[server] port`, по умолчанию 8080. Нужен +установленный `ffmpeg`. + +### Белый список Telegram + +Бот отвечает только тем, кто перечислен в `[server] users_while_list`. В +`config.dist.toml` этого ключа нет — добавьте его сами: + +```toml +[server] +users_while_list = ["@username"] +``` + +Значения сверяются со строкой автора сообщения из Telegram (`@username` либо имя +с фамилией), а не с числовым id. ## Деплой @@ -46,9 +68,9 @@ inv pl -- transcriber ## API Endpoints -### POST /api/transcribe +### POST /api/audio -Загружает аудиофайл и создает задачу на расшифровку. +Загружает аудиофайл и создает задачу на расшифровку. Отвечает `201 Created`. **Параметры:** - `audio` (form-data) - аудиофайл для расшифровки @@ -56,7 +78,7 @@ inv pl -- transcriber **Пример запроса:** ```bash curl -X POST \ - http://localhost:8080/api/transcribe \ + http://localhost:8080/api/audio \ -F "audio=@/path/to/your/audio.mp3" ``` @@ -64,31 +86,34 @@ curl -X POST \ ```json { "job_id": "550e8400-e29b-41d4-a716-446655440000", - "file_id": "6ba7b810-9dad-11d1-80b4-00c04fd430c8", - "status": "pending" + "status": "created" } ``` -### GET /api/transcribe/:id +### GET /api/status/:id -Получает статус задачи расшифровки по ID. +Получает статус задачи расшифровки по ID. Поле `transcription_text` появляется, +когда задача перешла в `done`. **Пример запроса:** ```bash -curl http://localhost:8080/api/transcribe/550e8400-e29b-41d4-a716-446655440000 +curl http://localhost:8080/api/status/550e8400-e29b-41d4-a716-446655440000 ``` **Ответ:** ```json { - "id": "550e8400-e29b-41d4-a716-446655440000", - "status": "pending", - "file_id": "6ba7b810-9dad-11d1-80b4-00c04fd430c8", + "job_id": "550e8400-e29b-41d4-a716-446655440000", + "status": "done", "created_at": "2024-01-01T12:00:00Z", - "updated_at": "2024-01-01T12:00:00Z" + "transcription_text": "расшифрованный текст" } ``` +### GET /metrics + +Метрики Prometheus с префиксом `transcriber_`. + ### GET /health Проверка работоспособности сервиса. @@ -101,51 +126,72 @@ curl http://localhost:8080/api/transcribe/550e8400-e29b-41d4-a716-446655440000 } ``` -## Статусы задач +## Состояния задач -- `pending` - задача создана, ожидает обработки -- `processing` - задача выполняется -- `completed` - задача завершена успешно +- `created` - задача создана, файл сохранён, ждёт конвертации +- `converted` - файл сконвертирован в ogg, ждёт отправки на распознавание +- `transcribe` - распознавание запущено в Yandex SpeechKit, ждём результата +- `done` - задача завершена успешно - `failed` - задача завершена с ошибкой +Задачи двигают три фоновых воркера: `conversion_worker`, `transcribe_worker` и +`check_worker`. Каждый опрашивает базу раз в секунду и делает один шаг. + ## Структура проекта ``` transcriber/ -├── main.go # Точка входа приложения -├── go.mod # Зависимости Go -├── models/ -│ └── models.go # Модели данных -├── database/ -│ └── database.go # Слой работы с БД -├── handlers/ -│ └── transcribe.go # HTTP обработчики -├── migrations/ -│ ├── 001_create_files_table.sql -│ └── 002_create_transcribe_jobs_table.sql +├── main.go # Точка входа: конфиг, миграции, сборка зависимостей, запуск +├── internal/ +│ ├── entity/ # Модели: задача, файл, результат распознавания +│ ├── contract/ # Интерфейсы адаптеров и репозиториев, типы ошибок +│ ├── config/ # Разбор config.toml +│ ├── metrics/ # Метрики Prometheus +│ ├── service/ # Конвейер расшифровки +│ ├── controller/ +│ │ ├── http/ # HTTP-обработчики +│ │ ├── tg/ # Telegram-бот +│ │ └── worker/ # Фоновые воркеры +│ └── adapter/ +│ ├── converter/ffmpeg/ # Конвертация аудио +│ ├── metaviewer/ffmpeg/ # Длительность аудио +│ ├── recognizer/yandex/ # SpeechKit + Object Storage +│ ├── telegram/ # Отправка сообщений +│ └── repo/sqlite/ # Репозитории +├── migrations/ # Миграции goose, вшиты в бинарник через go:embed └── data/ - ├── files/ # Директория для сохранения файлов - └── transcriber.db # SQLite база данных (создается автоматически) + ├── files/ # Директория для сохранения файлов + └── transcriber.db # SQLite база данных (создается автоматически) ``` ## База данных ### Таблица `files` - `id` (TEXT) - UUID файла -- `type` (TEXT) - MIME-тип файла +- `storage` (TEXT) - где лежит файл: `local` или `s3` +- `file_name` (TEXT) - имя файла в хранилище - `size` (INTEGER) - размер файла в байтах - `created_at` (DATETIME) - время создания ### Таблица `transcribe_jobs` - `id` (TEXT) - UUID задачи -- `status` (TEXT) - статус задачи -- `file_id` (TEXT) - ссылка на файл -- `created_at` (DATETIME) - время создания -- `updated_at` (DATETIME) - время последнего обновления +- `state` (TEXT) - состояние задачи +- `source` (TEXT) - откуда пришла задача: `api`, `telegram` или `unknown` +- `file_id` (TEXT) - ссылка на текущий файл задачи +- `delay_time` (DATETIME) - не брать задачу раньше этого времени +- `acquisition_id`, `acquire_time` - захват задачи воркером +- `recognition_op_id` (TEXT) - ID операции распознавания в Yandex Cloud +- `transcription_text` (TEXT) - результат распознавания +- `is_error` (BOOLEAN), `error_text` (TEXT) - признак и текст ошибки +- `tg_chat_id`, `tg_reply_message_id` - куда отправить результат в Telegram +- `created_at`, `updated_at` (DATETIME) - времена создания и обновления ## Разработка -Для добавления новых миграций используйте goose: +Миграции накатываются автоматически при старте сервиса — они вшиты в бинарник +через `//go:embed`. Достаточно положить новый файл в `migrations/`. + +Создать файл миграции и накатить или откатить её вручную: ```bash # Создание новой миграции @@ -156,3 +202,14 @@ goose -dir migrations sqlite3 data/transcriber.db up # Откат миграций goose -dir migrations sqlite3 data/transcriber.db down +``` + +Проверки перед коммитом: + +```bash +go build ./... +go vet ./... +gofmt -l . +go test ./... +golangci-lint run +```