Мое автопротоколирование, начало создания полноценного сервиса

от автора

Дисклеймер

Всем привет! В данной статье я поделюсь своим опытом написания сервиса. Я не являюсь опытным или профессиональным разработчиком, я пишу свой проект и мои решения могут быть не самыми оптимальными. Эта статья состоит в основном из ошибок, которые я совершил. Мой путь не является правильным и потому — судите «строго».

Создание структуры проекта

Первое, что приходит в голову при создании своего проекта, это как правильно организовать работу всего приложения, что с чем должно взаимодействовать, и как вообще это должно работать. Поэтому первым делом я решил накидать какую-то структуру проекта.

Первая схема приложения

Первая схема приложения

На картинке выше, я привел схему, которая появилась у меня в голове, когда я только начинал, и эта схема очень сильно напоминает MVC паттерн (По крайней мере мне так кажется). Что из этого выходит. Весь сервис можно разделить на 3 большие части:

  • Frontend — часть, которую видят пользователи, и где они могут загружать свои аудио/видео файлы

  • Controller — предоставляет api для frontend, чтобы можно было получать данные из бд и хранилище, писать в них и ставить задачи для бэка и получать некий callback, для обновления задач

  • Backend — выполняет сложные задачи, диаризации, транскрибации и т.д.

Давайте быстро пробежимся по тому, что это за сервис. Автопротоколирование — это некоторое приложение в которое ты можешь загрузить свою видео/аудио-запись, а на выходе получишь протокол и транскрипцию этого файла.

Так как проект домашний, для базы данных выбрал Postgres, для хранилища файлов — minio. Накидал docker-compose, запустил, всё супер, работает.

Почему я решил работу с базой оставить контроллеру а не бэку. Всё просто, задачи, которые обрабатывает бэк — крайне ресурсоёмкие, и в моём случае, распараллелить их не удается (один whisper кушает 6-8ГБ ОЗУ, а если брать нормальную модельку, то там минимум гигов 40). В совокупности, если бы бэку еще нужно было общаться с базой и отвечать на запросы фронта, это стоило бы ооооочень дорого.)

Вот тут я понял, что бэк я в любом случаю оставлю на Python, потому что там уже есть удобные библиотеки для работы с разными моделями, на чем писать контроллер и фронт.

Выбор языков для проекта

Для контроллера я выбрал то, что давно хотел начать изучать — Go, что касается фронта, на рандоме тыкнул в React с TypeScript, что в целом оказалось не плохо (Если бы я, конечно, умел писать фронт нормально)

Выбор сделан, работа закипела, но только по контроллеру и бэкенду.

Сделал простую структуру для конфигурации бэка и контроллера, и запихнул конфигурацию в yaml

Контроллер:

pg:   host: 127.0.0.1   port: 5432   user: virus   password: postgres   database: db  server:   host: 0.0.0.0   port: 8080  s3:   endpoint: http://127.0.0.1:9000   access_key: virus   secret_key: password   bucket: bucket   region: ru-central1   minio: true

Бэк:

tokens:   hf_token: ${HF_TOKEN}  ollama:   url: localhost   model: llama3.3   num_context: 5000

Ну и сделал им структуры. (Не знаю, стоит ли их приводить, всегда можно глянуть на гитхабе, тут, пожалуй, будет кода минимум)

Структура баз данных

Как видно из конфигурации, у меня есть подключение к базе данных (у бэкэнда она тоже была, но я уже не найду). Ну и раз мне нужна база, сразу же сделал структуру базы.

База контроллера

База контроллера
База бэкэнда

База бэкэнда

Идея была тупой простой, у меня есть контроллер, который записывает всё необходимое в базу данных. Все таблицы задач (diarize, convert, transcribe, report) связаны с таблицей с Совещаниями, плюс еще несколько вспомогательных таблиц. Как это должно было работать:

  • Приходит пользователь и загружает свою аудио/видео-запись

  • Контроллер обрабатывает и кладет в S3 файл, а в бд добавляет запись

  • Пользователь нажимает кнопочку

  • Контроллер достает данные по совещанию и по порядку ставит задачи в бэк

  • Бэк получает задачу и записывает её в свою базу, как задачу в очереди

  • Параллельно с этим бэк читает поставленные задачи из бд, и отдает ответ контроллеру по API

  • Контроллер пишет полученные результаты задачи и ставит новую по необходимости

  • Вы великолепны

Вроде бы всё идеально. Позже до меня дойдет, что я делаю какой-то бред, что я реализую очередь через дополнительную базу данных, но это где-то в будущем.)

Что делаем дальше, простой клиент для базы в бэке и в контроллере (я выбрал базу pg, так как в целом с ней знаком). Показывать буду на примере Go, но в целом в питоне было сделано так же стандартно.

type DBConfig struct { Host     string `yaml:"host"` Port     int    `yaml:"port"` User     string `yaml:"user"` Password string `yaml:"password"` Database string `yaml:"database"` }  type PGdatabase struct { Config DBConfig Pool   *pgxpool.Pool }  func NewDatabase(ctx context.Context, cfg DBConfig) (*PGdatabase, error) {  connectionInfo := fmt.Sprintf( "postgres://%s:%s@%s:%d/%s?sslmode=disable", cfg.User, cfg.Password, cfg.Host, cfg.Port, cfg.Database)  pool, err := pgxpool.New(ctx, connectionInfo)  if err != nil { return nil, fmt.Errorf("Database connection error: %s", err)  }  if err = pool.Ping(ctx); err != nil { return nil, fmt.Errorf("Database ping error: %s", err) }  return &PGdatabase{ Pool:   pool, Config: cfg, }, nil }  func (d *PGdatabase) CloseConnection() { if d.Pool != nil { d.Pool.Close() } }

То есть я создал структуру конфигурации подключения к базе, проверку, всё ли нормально работает, есть ли база и т.п, и создал свою структуру в которую писал pool и конфигурацию, чтобы не нужно было перечитывать (просто так захотелось, окей).

То же самое проделал на бэке, всё завелось, добавил ручку чтобы можно было покидать запросы, посмотреть, что пишется в базу, что читается. Для запросов к базе использовал sqlc, крайне удобная вещь, где ты просто пишешь свой запрос и получаешь готовый код (спасибо тем людям, которые пишут штуки для генерации кода)

-- name: GetConversations :many SELECT * FROM Conversations;

Привел пример простого запроса, он сам генерирует нужные типы данных, сам запрос на Go и всё что к нему прилагается, но нужно освоить конфиг, чтобы типы были нормальными а не pg.string и т.п. (с этим я тоже не мало пострадал, но 1 раз настроил и забыл)

Ну вот, первый запуск, все стартовало успешно, кинул запрос, записал в базу, прочитал из базы (писал curl’ом). Всё отлично, а что дальше…

Что же делать дальше…

А дальше нужно было делать постановку задач для бэка. И вот тут я понял свои ошибки. Использовать базу данных как очередь — плохо. Когда у тебя две базы данных — а какой смысл. Бэк принимает запросы по API? Вообще ужас. Callback с выполненной задачей по API? Тоже тяжело и бред, задачи могут потеряться. Но в тот момент я не видел этих проблем.

Ручки

Поэтому что я решил сделать? Правильно, сделать CRUD (Create, Read, Update, Delete) ручки для работы с базой. Это было просто, пишешь запрос в sqlc, генерируешь код, пишешь функцию, засовываешь эту функцию в ручку, которую потом дергаешь curl’ом по необходимости.) Сделал ручку для каждой таблички, для каждого значения, для создания строк, для изменения каждого значения.

Классно, супер, но зачем. Задача же стоит сделать общение с бэком. Что делать в этом случае. Правильно, отрефакторить бэк. В итоге переделал бэк, сделал там на fastAPI уже новые ручки, больше ручек. Я мог через swagger поставить разные задачи, закинуть туда файл по ссылке на S3. Всё так классно работает, но как это связать…

Рефакторинг

Я начинаю сам себе закапывать, пытаюсь понять, как сделать лучше, пишу доп функции, пишу генерики, чтобы было меньше повторений кода. Пишу пишу пишу пишу… А до результата ближе не оказываюсь. Переписываю, переписываю, переписываю, и вновь, я ухожу от главного, как поставить задачу в бэк.

В итоге на бессмысленное переписывание я убил неделю времени. Чтобы как-то сдвинуться с места, я решил спросить у друга, что я делаю не так. В ответ получаю, что почти всё. Но самое тупое — очередь через базу данных, бесконечное количество ручек и бесполезное улучшение того, что просто никак не будет использоваться при работе приложения.

Вывод

Для чего я это написал. Я описал то, как я начинал писать своё приложение. Описал решения, которые принимал, куда потратил много времени. Написал то, на какие грабли я наступил, этот этап, именно начала своего сервиса кроме как хождение по граблям — охарактеризовать никак не могу.

Если есть возможность, всегда лучше спросить у более опытных друзей.

Скоро напишу продолжение, про то, как я исправлял ошибки, менял базу, и многое другое.


ссылка на оригинал статьи https://habr.com/ru/articles/911166/


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *