Files
transcriber/internal/controller/http/transcribe.go

367 lines
10 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

package http
import (
"fmt"
"io"
"net/http"
"os"
"path/filepath"
"time"
"git.vakhrushev.me/av/transcriber/internal/entity"
"git.vakhrushev.me/av/transcriber/internal/repo"
"git.vakhrushev.me/av/transcriber/internal/repo/ffmpeg"
"git.vakhrushev.me/av/transcriber/internal/service/s3"
"git.vakhrushev.me/av/transcriber/internal/service/speechkit"
"github.com/gin-gonic/gin"
"github.com/google/uuid"
)
type TranscribeHandler struct {
jobRepo repo.TranscriptJobRepository
fileRepo repo.FileRepository
}
func NewTranscribeHandler(jobRepo repo.TranscriptJobRepository, fileRepo repo.FileRepository) *TranscribeHandler {
return &TranscribeHandler{jobRepo: jobRepo, fileRepo: fileRepo}
}
type CreateTranscribeJobResponse struct {
JobID string `json:"job_id"`
State string `json:"status"`
}
type GetTranscribeJobResponse struct {
JobID string `json:"job_id"`
State string `json:"status"`
CreatedAt time.Time `json:"created_at"`
TranscriptionText *string `json:"transcription_text,omitempty"`
}
func (h *TranscribeHandler) CreateTranscribeJob(c *gin.Context) {
// Получаем файл из формы
file, header, err := c.Request.FormFile("audio")
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "No audio file provided"})
return
}
defer file.Close()
// Генерируем UUID для файла
fileId := uuid.New().String()
// Определяем расширение файла
ext := filepath.Ext(header.Filename)
if ext == "" {
ext = ".audio" // fallback если расширение не определено
}
// Создаем путь для сохранения файла
fileName := fmt.Sprintf("%s%s", fileId, ext)
filePath := filepath.Join("data", "files", fileName)
// Создаем файл на диске
dst, err := os.Create(filePath)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to create file"})
return
}
defer dst.Close()
// Копируем содержимое загруженного файла
size, err := io.Copy(dst, file)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to save file"})
return
}
// Создаем запись в таблице files
fileRecord := &entity.File{
Id: fileId,
Storage: entity.StorageLocal,
FileName: fileName,
Size: size,
CreatedAt: time.Now(),
}
if err := h.fileRepo.Create(fileRecord); err != nil {
// Удаляем файл если не удалось создать запись в БД
os.Remove(filePath)
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to save file record"})
return
}
// Создаем запись в таблице transcribe_jobs
jobId := uuid.New().String()
job := &entity.TranscribeJob{
Id: jobId,
State: entity.StateCreated,
FileID: &fileId,
IsError: false,
CreatedAt: time.Now(),
}
if err := h.jobRepo.Create(job); err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to create transcribe job"})
return
}
// Возвращаем успешный ответ
response := CreateTranscribeJobResponse{
JobID: job.Id,
State: job.State,
}
c.JSON(http.StatusCreated, response)
}
func (h *TranscribeHandler) GetTranscribeJobStatus(c *gin.Context) {
jobID := c.Param("id")
job, err := h.jobRepo.GetByID(jobID)
if err != nil {
c.JSON(http.StatusNotFound, gin.H{"error": "Job not found"})
return
}
c.JSON(http.StatusOK, GetTranscribeJobResponse{
JobID: job.Id,
State: job.State,
CreatedAt: job.CreatedAt,
TranscriptionText: job.TranscriptionText,
})
}
func (h *TranscribeHandler) RunConversionJob(c *gin.Context) {
acquisitionId := uuid.NewString()
rottingTime := time.Now().Add(-1 * time.Hour)
job, err := h.jobRepo.FindAndAcquire(entity.StateCreated, acquisitionId, rottingTime)
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
srcFile, err := h.fileRepo.GetByID(*job.FileID)
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
srcFilePath := filepath.Join("data", "files", srcFile.FileName)
destFileId := uuid.New().String()
destFileName := fmt.Sprintf("%s%s", destFileId, ".ogg")
destFilePath := filepath.Join("data", "files", destFileName)
conv := ffmpeg.NewFileConverter()
err = conv.Convert(srcFilePath, destFilePath)
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
stat, err := os.Stat(destFilePath)
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
// Создаем запись в таблице files
destFileRecord := &entity.File{
Id: destFileId,
Storage: entity.StorageLocal,
FileName: destFileName,
Size: stat.Size(),
CreatedAt: time.Now(),
}
job.FileID = &destFileId
job.MoveToState(entity.StateConverted)
err = h.fileRepo.Create(destFileRecord)
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
err = h.jobRepo.Save(job)
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
c.Status(http.StatusOK)
}
func (h *TranscribeHandler) RunUploadJob(c *gin.Context) {
acquisitionId := uuid.NewString()
rottingTime := time.Now().Add(-1 * time.Hour)
job, err := h.jobRepo.FindAndAcquire(entity.StateConverted, acquisitionId, rottingTime)
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
fileRecord, err := h.fileRepo.GetByID(*job.FileID)
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
filePath := filepath.Join("data", "files", fileRecord.FileName)
destFileId := uuid.New().String()
destFileRecord := &entity.File{
Id: destFileId,
Storage: entity.StorageS3,
FileName: fileRecord.FileName,
Size: fileRecord.Size,
CreatedAt: time.Now(),
}
// Создаем S3 сервис
s3Service, err := s3.NewS3Service()
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to initialize S3 service: " + err.Error()})
return
}
// Загружаем файл на S3
err = s3Service.UploadFile(filePath, destFileRecord.FileName)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to upload file to S3: " + err.Error()})
return
}
job.FileID = &destFileId
job.MoveToState(entity.StateUploaded)
// Сохраняем информацию о загрузке файла на S3
err = h.fileRepo.Create(destFileRecord)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to update file record: " + err.Error()})
return
}
// Обновляем состояние задачи
err = h.jobRepo.Save(job)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to update job state: " + err.Error()})
return
}
c.Status(http.StatusOK)
}
func (h *TranscribeHandler) RunRecognitionJob(c *gin.Context) {
acquisitionId := uuid.NewString()
rottingTime := time.Now().Add(-1 * time.Hour)
job, err := h.jobRepo.FindAndAcquire(entity.StateUploaded, acquisitionId, rottingTime)
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
fileRecord, err := h.fileRepo.GetByID(*job.FileID)
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
// Создаем SpeechKit сервис
speechKitService, err := speechkit.NewSpeechKitService()
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to initialize SpeechKit service: " + err.Error()})
return
}
// Формируем S3 URI для файла
bucketName := os.Getenv("S3_BUCKET_NAME")
s3URI := fmt.Sprintf("https://storage.yandexcloud.net/%s/%s", bucketName, fileRecord.FileName)
// Запускаем асинхронное распознавание
operationID, err := speechKitService.RecognizeFileFromS3(s3URI)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to start recognition: " + err.Error()})
return
}
// Обновляем задачу с ID операции распознавания
job.RecognitionOpID = &operationID
job.MoveToState(entity.StateTranscribe)
err = h.jobRepo.Save(job)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to update job: " + err.Error()})
return
}
c.Status(http.StatusOK)
}
func (h *TranscribeHandler) RunRecognitionCheckJob(c *gin.Context) {
acquisitionId := uuid.NewString()
rottingTime := time.Now().Add(-1 * time.Hour)
job, err := h.jobRepo.FindAndAcquire(entity.StateTranscribe, acquisitionId, rottingTime)
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
if job.RecognitionOpID == nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "No recognition operation ID found"})
return
}
// Создаем SpeechKit сервис
speechKitService, err := speechkit.NewSpeechKitService()
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to initialize SpeechKit service: " + err.Error()})
return
}
// Проверяем статус операции
operation, err := speechKitService.CheckOperationStatus(*job.RecognitionOpID)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to check operation status: " + err.Error()})
return
}
if !operation.Done {
// Операция еще не завершена, переводим в состояние ожидания
err = h.jobRepo.Save(job)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to update job: " + err.Error()})
return
}
c.Status(http.StatusOK)
return
}
// Операция завершена, получаем результат
responses, err := speechKitService.GetRecognitionResult(*job.RecognitionOpID)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to get recognition result: " + err.Error()})
return
}
// Извлекаем текст из результатов
transcriptionText := speechkit.ExtractTranscriptionText(responses)
// Обновляем задачу с результатом
job.TranscriptionText = &transcriptionText
job.MoveToState(entity.StateDone)
err = h.jobRepo.Save(job)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to update job: " + err.Error()})
return
}
c.Status(http.StatusOK)
}