239 lines
8.8 KiB
Go
239 lines
8.8 KiB
Go
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
|
||
}
|
||
}
|