v1.0.4 - init version
This commit is contained in:
205
tts/piper.go
Normal file
205
tts/piper.go
Normal file
@@ -0,0 +1,205 @@
|
||||
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
|
||||
}
|
||||
Reference in New Issue
Block a user