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

206 lines
7.9 KiB
Go

package tts
import (
"fmt"
"os"
"os/exec"
"path/filepath"
"time"
"go-speech/internal/logger"
)
type PiperService struct {
piperPath string
modelDir string
ffmpegPath string
tempDir string
}
func NewPiperService(piperPath, modelDir, ffmpegPath string) *PiperService {
tempDir := filepath.Join(os.TempDir(), "go-speech")
os.MkdirAll(tempDir, 0755)
return &PiperService{
piperPath: piperPath,
modelDir: modelDir,
ffmpegPath: ffmpegPath,
tempDir: tempDir,
}
}
// GenerateAudio генерирует аудио файл из текста и возвращает путь к OGG файлу
func (s *PiperService) GenerateAudio(text string, voice string) (string, error) {
logger.Debug("=== Генерация аудио ===")
logger.Debug("Параметры:")
logger.Debug(" Текст: %s (длина: %d символов)", text, len(text))
logger.Debug(" Голос: %s", voice)
logger.Debug(" Piper путь: %s", s.piperPath)
logger.Debug(" Директория моделей: %s", s.modelDir)
logger.Debug(" FFmpeg путь: %s", s.ffmpegPath)
logger.Debug(" Временная директория: %s", s.tempDir)
// Формируем путь к модели на основе голоса
modelFile := fmt.Sprintf("ru_RU-%s-medium.onnx", voice)
modelPath := filepath.Join(s.modelDir, modelFile)
logger.Debug("Путь к модели: %s", modelPath)
// Создание уникального имени файла
timestamp := time.Now().UnixNano()
wavPath := filepath.Join(s.tempDir, fmt.Sprintf("speech_%d.wav", timestamp))
oggPath := filepath.Join(s.tempDir, fmt.Sprintf("speech_%d.ogg", timestamp))
logger.Debug("Временные файлы:")
logger.Debug(" WAV: %s", wavPath)
logger.Debug(" OGG: %s", oggPath)
// Проверка наличия Piper
logger.Debug("Проверка наличия Piper TTS...")
if _, err := os.Stat(s.piperPath); os.IsNotExist(err) {
logger.Error("Piper TTS не найден по пути: %s", s.piperPath)
return "", fmt.Errorf("Piper TTS не найден по пути: %s", s.piperPath)
}
logger.Debug("Piper TTS найден")
// Проверка наличия модели
logger.Debug("Проверка наличия модели...")
if _, err := os.Stat(modelPath); os.IsNotExist(err) {
logger.Error("Модель не найдена по пути: %s", modelPath)
return "", fmt.Errorf("Модель не найдена по пути: %s", modelPath)
}
logger.Debug("Модель найдена")
// Генерация WAV файла через Piper
logger.Debug("Запуск Piper TTS для генерации WAV...")
cmd := exec.Command(s.piperPath,
"--model", modelPath,
"--output_file", wavPath,
)
logger.Debug("Команда Piper: %s --model %s --output_file %s", s.piperPath, modelPath, wavPath)
// Получение stdin pipe для передачи текста
logger.Debug("Создание stdin pipe...")
stdin, err := cmd.StdinPipe()
if err != nil {
logger.Error("Ошибка создания stdin pipe: %v", err)
return "", fmt.Errorf("ошибка создания stdin pipe: %v", err)
}
// Запуск команды
logger.Debug("Запуск процесса Piper...")
if err := cmd.Start(); err != nil {
logger.Error("Ошибка запуска Piper: %v", err)
return "", fmt.Errorf("ошибка запуска Piper: %v", err)
}
logger.Debug("Процесс Piper запущен (PID: %d)", cmd.Process.Pid)
// Отправка текста в Piper через stdin
logger.Debug("Отправка текста в Piper через stdin...")
textToSend := text + "\n"
if _, err := stdin.Write([]byte(textToSend)); err != nil {
stdin.Close()
cmd.Wait()
logger.Error("Ошибка записи текста в Piper: %v", err)
return "", fmt.Errorf("ошибка записи текста в Piper: %v", err)
}
logger.Debug("Текст отправлен (%d байт)", len(textToSend))
stdin.Close()
// Ожидание завершения Piper
logger.Debug("Ожидание завершения Piper...")
startTime := time.Now()
if err := cmd.Wait(); err != nil {
logger.Error("Ошибка выполнения Piper: %v", err)
return "", fmt.Errorf("ошибка выполнения Piper: %v", err)
}
duration := time.Since(startTime)
logger.Debug("Piper завершился успешно за %v", duration)
// Проверка наличия созданного WAV файла
logger.Debug("Проверка наличия WAV файла...")
fileInfo, err := os.Stat(wavPath)
if os.IsNotExist(err) {
logger.Error("WAV файл не был создан")
return "", fmt.Errorf("WAV файл не был создан")
}
logger.Debug("WAV файл создан: %d байт", fileInfo.Size())
// Конвертация WAV в OGG через ffmpeg
logger.Debug("Начало конвертации WAV в OGG через ffmpeg...")
if err := s.convertToOGG(wavPath, oggPath); err != nil {
logger.Error("Ошибка конвертации в OGG: %v", err)
os.Remove(wavPath) // Удаляем WAV файл при ошибке
return "", fmt.Errorf("ошибка конвертации в OGG: %v", err)
}
// Проверка размера OGG файла
oggInfo, err := os.Stat(oggPath)
if err == nil {
logger.Debug("OGG файл создан: %d байт", oggInfo.Size())
logger.Debug("Сжатие: %.2f%% (WAV: %d байт -> OGG: %d байт)",
float64(fileInfo.Size()-oggInfo.Size())/float64(fileInfo.Size())*100,
fileInfo.Size(), oggInfo.Size())
}
// Удаляем временный WAV файл
logger.Debug("Удаление временного WAV файла...")
if err := os.Remove(wavPath); err != nil {
logger.Warn("Ошибка удаления WAV файла: %v", err)
} else {
logger.Debug("WAV файл удален")
}
logger.Debug("=== Генерация аудио завершена успешно ===")
return oggPath, nil
}
// convertToOGG конвертирует WAV файл в OGG используя ffmpeg
func (s *PiperService) convertToOGG(wavPath, oggPath string) error {
logger.Debug("=== Конвертация WAV в OGG ===")
logger.Debug("Входной файл: %s", wavPath)
logger.Debug("Выходной файл: %s", oggPath)
// Проверка наличия ffmpeg
logger.Debug("Проверка наличия ffmpeg...")
if _, err := os.Stat(s.ffmpegPath); os.IsNotExist(err) {
logger.Error("ffmpeg не найден по пути: %s", s.ffmpegPath)
return fmt.Errorf("ffmpeg не найден по пути: %s", s.ffmpegPath)
}
logger.Debug("ffmpeg найден: %s", s.ffmpegPath)
cmd := exec.Command(s.ffmpegPath,
"-i", wavPath,
"-acodec", "libvorbis",
"-qscale:a", "5",
"-y", // Перезаписать выходной файл если существует
oggPath,
)
logger.Debug("Команда ffmpeg: %s -i %s -acodec libvorbis -qscale:a 5 -y %s",
s.ffmpegPath, wavPath, oggPath)
logger.Debug("Запуск ffmpeg...")
startTime := time.Now()
output, err := cmd.CombinedOutput()
duration := time.Since(startTime)
if err != nil {
logger.Error("Ошибка выполнения ffmpeg: %v", err)
logger.Debug("Вывод ffmpeg: %s", string(output))
return fmt.Errorf("ошибка выполнения ffmpeg: %v, вывод: %s", err, string(output))
}
logger.Debug("ffmpeg завершился успешно за %v", duration)
if len(output) > 0 {
logger.Debug("Вывод ffmpeg: %s", string(output))
}
// Проверка наличия созданного OGG файла
logger.Debug("Проверка наличия OGG файла...")
if _, err := os.Stat(oggPath); os.IsNotExist(err) {
logger.Error("OGG файл не был создан")
return fmt.Errorf("OGG файл не был создан")
}
logger.Debug("OGG файл успешно создан")
logger.Debug("=== Конвертация завершена ===")
return nil
}