Files
go-speech/handlers/tts.go
2025-11-25 15:08:04 +06:00

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