Как стать автором
Обновить

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

Уровень сложностиСредний
Время на прочтение5 мин
Количество просмотров852

Дисклеймер

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

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

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

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

На картинке выше, я привел схему, которая появилась у меня в голове, когда я только начинал, и эта схема очень сильно напоминает 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. Всё так классно работает, но как это связать...

Рефакторинг

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

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

Вывод

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

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

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

Теги:
Хабы:
+2
Комментарии4

Публикации

Работа

Data Scientist
46 вакансий
Go разработчик
86 вакансий

Ближайшие события