package yandex import ( "context" "fmt" "io" "time" "git.vakhrushev.me/av/transcriber/internal/entity" ) // ProviderName — имя провайдера, под которым сохраняется попытка распознавания. // По нему видно, чем считана запись, когда провайдеров станет больше одного. const ProviderName = "yandex-speechkit" type YandexAudioRecognizerConfig struct { // s3 Region string AccessKey string SecretKey string BucketName string Endpoint string // speech kit ApiKey string FolderID string } type YandexAudioRecognizerService struct { s3Sevice *yandexS3Service sttService *speechKitService } func NewYandexAudioRecognizerService(cfg YandexAudioRecognizerConfig) (*YandexAudioRecognizerService, error) { s3, err := newYandexS3Service(s3Config{ Region: cfg.Region, AccessKey: cfg.AccessKey, SecretKey: cfg.SecretKey, BucketName: cfg.BucketName, Endpoint: cfg.Endpoint, }) if err != nil { return nil, err } stt, err := newSpeechKitService(speechKitConfig{ ApiKey: cfg.ApiKey, FolderID: cfg.FolderID, }) if err != nil { return nil, err } return &YandexAudioRecognizerService{ s3Sevice: s3, sttService: stt, }, nil } func (s *YandexAudioRecognizerService) Close() error { return s.sttService.Close() } func (s *YandexAudioRecognizerService) Provider() string { return ProviderName } func (s *YandexAudioRecognizerService) Model() string { return RecognitionModel } // startRecognitionTimeout — сколько ждём принятия операции, когда нас уже // остановили. Число меньше жёсткого предела остановки: иначе процесс убьют // прежде, чем ответ дойдёт, и защита ничего не даст. const startRecognitionTimeout = 10 * time.Second // Upload кладёт аудио туда, откуда провайдер его прочитает. // // Отменяется штатно: заливка дорога по времени, а повтор её бесплатен — объект // ложится под тем же ключом. func (s *YandexAudioRecognizerService) Upload(ctx context.Context, file io.Reader, objectKey string) (string, error) { if err := s.s3Sevice.uploadFile(ctx, file, objectKey); err != nil { return "", err } return s.s3Sevice.fileUrl(objectKey), nil } // ObjectExists отвечает, лежит ли объект нужного размера. // // Сверка идёт по присутствию и длине, а не по отпечатку содержимого: признак // целостности у составного объекта не равен отпечатку, и сверка хешем дала бы // расхождение на всякой большой записи. func (s *YandexAudioRecognizerService) ObjectExists(ctx context.Context, objectKey string, size int64) (bool, error) { return s.s3Sevice.objectExists(ctx, objectKey, size) } // Submit заводит операцию распознавания. Оплачивается наружу, поэтому от отмены // защищён: окно короткое и дорогое — SpeechKit может операцию принять и начать // считать деньги, а ответ до нас не доедет, и повтор оплатит ту же запись второй // раз. Свой предел вызову оставлен, чтобы остановка не ждала вечно. func (s *YandexAudioRecognizerService) Submit(ctx context.Context, sourceURI string) (string, error) { startCtx, cancel := protectFromCancel(ctx, startRecognitionTimeout) defer cancel() return s.sttService.recognizeFileFromS3(startCtx, sourceURI) } // protectFromCancel отвязывает вызов от отмены родителя, оставляя ему значения // родителя и собственный предел по времени. Употребляется там, где обрыв стоит // дороже ожидания: у платной операции, чей результат нельзя переспросить. func protectFromCancel(ctx context.Context, timeout time.Duration) (context.Context, context.CancelFunc) { return context.WithTimeout(context.WithoutCancel(ctx), timeout) } // Fetch забирает готовый результат и отдаёт его доменным: реплики со временем, // плоский текст и байты ответа на хранение. Формата провайдера наружу не выходит // ничего — ни один шаг конвейера не знает, каким потоком тот отвечает. func (s *YandexAudioRecognizerService) Fetch(ctx context.Context, operationID string) (*entity.RecognitionOutcome, error) { return s.sttService.fetchRecognition(ctx, operationID) } // Parse строит доменный результат из **сохранённого** ответа, не обращаясь к // провайдеру. По нему архив пересчитывается без единого рубля. func (s *YandexAudioRecognizerService) Parse(raw []byte) (*entity.RecognitionOutcome, error) { responses, err := decodeResponses(raw) if err != nil { return nil, err } outcome := outcomeFromResponses(responses) outcome.Raw = raw return outcome, nil } func (s *YandexAudioRecognizerService) CheckStatus(ctx context.Context, operationID string) (*entity.RecognitionResult, error) { operation, err := s.sttService.checkOperationStatus(ctx, operationID) if err != nil { return nil, err } if !operation.Done { return entity.NewInProgressResult(), nil } if opErr := operation.GetError(); opErr != nil { errorText := fmt.Sprintf("operation failed: code %d, message: %s", opErr.Code, opErr.Message) return entity.NewFailedResult(errorText), nil } return entity.NewCompletedResult(), nil }