206 lines
7.9 KiB
Go
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
|
|
}
|