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 }