package handlers import ( "encoding/json" "fmt" "io" "log" "net/http" "os" "go-speech/internal/cache" "go-speech/internal/logger" "go-speech/tts" ) type TTSHandler struct { ttsService *tts.PiperService modelDir string cache *cache.Cache } func NewTTSHandler(piperPath, modelDir, ffmpegPath string, cacheInstance *cache.Cache) *TTSHandler { service := tts.NewPiperService(piperPath, modelDir, ffmpegPath) return &TTSHandler{ ttsService: service, modelDir: modelDir, cache: cacheInstance, } } type TTSRequest struct { Text string `json:"text"` Voice string `json:"voice"` } func (h *TTSHandler) HandleTTS(w http.ResponseWriter, r *http.Request) { logger.Debug("=== Обработка TTS запроса ===") if r.Method != http.MethodPost { logger.Warn("Неподдерживаемый метод: %s", r.Method) http.Error(w, "Метод не поддерживается. Используйте POST", http.StatusMethodNotAllowed) return } // Проверка Content-Type contentType := r.Header.Get("Content-Type") logger.Debug("Content-Type: %s", contentType) if contentType != "application/json" { logger.Warn("Неверный Content-Type: %s", contentType) http.Error(w, "Content-Type должен быть application/json", http.StatusBadRequest) return } // Чтение тела запроса bodyBytes, err := io.ReadAll(r.Body) if err != nil { logger.Error("Ошибка чтения тела запроса: %v", err) http.Error(w, "Ошибка чтения запроса: "+err.Error(), http.StatusBadRequest) return } logger.Debug("Тело запроса: %s", string(bodyBytes)) var req TTSRequest if err := json.Unmarshal(bodyBytes, &req); err != nil { logger.Error("Ошибка парсинга JSON: %v", err) http.Error(w, "Неверный формат JSON: "+err.Error(), http.StatusBadRequest) return } logger.Debug("Распарсенный запрос:") logger.Debug(" Text: %s (длина: %d)", req.Text, len(req.Text)) logger.Debug(" Voice: %s", req.Voice) // Валидация текста if req.Text == "" { http.Error(w, "Поле 'text' не может быть пустым", http.StatusBadRequest) return } if len(req.Text) > 5000 { http.Error(w, "Текст слишком длинный (максимум 5000 символов)", http.StatusBadRequest) return } // Определение голоса (по умолчанию ruslan) voice := req.Voice if voice == "" { voice = "ruslan" logger.Debug("Голос не указан, используется по умолчанию: ruslan") } // Валидация голоса validVoices := map[string]bool{ "ruslan": true, "irina": true, "denis": true, "dmitri": true, } if !validVoices[voice] { logger.Warn("Неверный голос: %s", voice) http.Error(w, "Неверный голос. Доступные: ruslan, irina, denis, dmitri", http.StatusBadRequest) return } logger.Debug("Выбранный голос: %s", voice) // Формируем имя модели для кэша modelName := fmt.Sprintf("ru_RU-%s-medium.onnx", voice) cacheKey := cache.GetCacheKey(modelName, req.Text) logger.Debug("Ключ кэша: %s", cacheKey) // Проверка кэша var audioPath string var tempFileToDelete string // Путь к временному файлу для удаления if h.cache != nil && h.cache.Exists(cacheKey) { logger.Debug("Файл найден в кэше, используем кэш") // Используем путь к файлу в кэше audioPath = h.cache.GetPath(cacheKey) logger.Debug("Используется файл из кэша: %s", audioPath) } else { // Генерация аудио logger.Debug("Файл не найден в кэше, начинаем генерацию...") logger.Debug(" Текст: %s", req.Text) logger.Debug(" Голос: %s", voice) generatedPath, err := h.ttsService.GenerateAudio(req.Text, voice) if err != nil { logger.Error("Ошибка генерации аудио: %v", err) log.Printf("Ошибка генерации аудио: %v", err) http.Error(w, "Ошибка генерации аудио: "+err.Error(), http.StatusInternalServerError) return } audioPath = generatedPath tempFileToDelete = generatedPath // Сохраняем путь для удаления logger.Debug("Аудио успешно сгенерировано: %s", audioPath) // Сохранение в кэш if h.cache != nil { logger.Debug("Сохранение файла в кэш...") if err := h.cache.Put(cacheKey, audioPath); err != nil { logger.Warn("Ошибка сохранения в кэш: %v", err) } else { logger.Debug("Файл успешно сохранен в кэш") } } } // Удаляем временный файл только если он был сгенерирован (не из кэша) if tempFileToDelete != "" { defer func() { if err := os.Remove(tempFileToDelete); err != nil { logger.Warn("Ошибка удаления временного файла %s: %v", tempFileToDelete, err) } else { logger.Debug("Временный файл удален: %s", tempFileToDelete) } }() } // Открытие файла logger.Debug("Открытие аудио файла: %s", audioPath) audioFile, err := os.Open(audioPath) if err != nil { logger.Error("Ошибка открытия аудио файла: %v", err) log.Printf("Ошибка открытия аудио файла: %v", err) http.Error(w, "Ошибка чтения аудио файла", http.StatusInternalServerError) return } defer audioFile.Close() // Получение информации о файле fileInfo, err := audioFile.Stat() if err != nil { logger.Error("Ошибка получения информации о файле: %v", err) log.Printf("Ошибка получения информации о файле: %v", err) http.Error(w, "Ошибка чтения аудио файла", http.StatusInternalServerError) return } logger.Debug("Размер аудио файла: %d байт", fileInfo.Size()) // Установка заголовков ответа logger.Debug("Установка HTTP заголовков ответа") w.Header().Set("Content-Type", "audio/ogg") w.Header().Set("Content-Length", fmt.Sprintf("%d", fileInfo.Size())) w.Header().Set("Content-Disposition", fmt.Sprintf("inline; filename=speech-%s.ogg", voice)) w.Header().Set("Cache-Control", "no-cache") w.WriteHeader(http.StatusOK) logger.Debug("Отправка аудио файла клиенту...") // Отправка файла bytesWritten, err := io.Copy(w, audioFile) if err != nil { logger.Error("Ошибка отправки аудио файла: %v", err) log.Printf("Ошибка отправки аудио файла: %v", err) return } logger.Debug("Аудио файл успешно отправлен: %d байт", bytesWritten) logger.Debug("=== TTS запрос обработан успешно ===") } // HandleHealthz генерирует аудио файл со словом "Окей" и возвращает его func (h *TTSHandler) HandleHealthz(w http.ResponseWriter, r *http.Request) { // Генерация аудио со словом "Окей" (используем голос по умолчанию ruslan) audioPath, err := h.ttsService.GenerateAudio("Окей", "ruslan") if err != nil { log.Printf("Ошибка генерации аудио для healthz: %v", err) http.Error(w, "Ошибка генерации аудио: "+err.Error(), http.StatusInternalServerError) return } defer os.Remove(audioPath) // Удаляем временный файл после отправки // Открытие файла audioFile, err := os.Open(audioPath) if err != nil { log.Printf("Ошибка открытия аудио файла: %v", err) http.Error(w, "Ошибка чтения аудио файла", http.StatusInternalServerError) return } defer audioFile.Close() // Получение информации о файле fileInfo, err := audioFile.Stat() if err != nil { log.Printf("Ошибка получения информации о файле: %v", err) http.Error(w, "Ошибка чтения аудио файла", http.StatusInternalServerError) return } // Установка заголовков ответа w.Header().Set("Content-Type", "audio/ogg") w.Header().Set("Content-Length", fmt.Sprintf("%d", fileInfo.Size())) w.Header().Set("Content-Disposition", "attachment; filename=healthz.ogg") w.WriteHeader(http.StatusOK) // Отправка файла if _, err := io.Copy(w, audioFile); err != nil { log.Printf("Ошибка отправки аудио файла: %v", err) return } }