v1.0.4 - init version

This commit is contained in:
19 changed files with 2049 additions and 0 deletions

238
handlers/tts.go Normal file
View File

@@ -0,0 +1,238 @@
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
}
}