All checks were successful
Deploy ASP.NET MVC to OCI / build-and-deploy (push) Successful in 17m26s
214 lines
7.7 KiB
C#
214 lines
7.7 KiB
C#
using System;
|
|
using System.Diagnostics;
|
|
using System.IO;
|
|
using System.Runtime.InteropServices;
|
|
using System.Threading.Tasks;
|
|
using Microsoft.Extensions.Logging;
|
|
|
|
namespace Convert_It_Online.Services
|
|
{
|
|
public class TextToSpeechService : ITextToSpeechService
|
|
{
|
|
private readonly ILogger<TextToSpeechService> _logger;
|
|
private readonly string? _espeakPath;
|
|
private readonly string? _ffmpegPath;
|
|
|
|
public TextToSpeechService(ILogger<TextToSpeechService> logger)
|
|
{
|
|
_logger = logger;
|
|
_espeakPath = FindExecutable("espeak-ng");
|
|
_ffmpegPath = FindExecutable("ffmpeg");
|
|
|
|
if (_espeakPath != null)
|
|
_logger.LogInformation("espeak-ng encontrado em: {Path}", _espeakPath);
|
|
else
|
|
_logger.LogWarning("espeak-ng não encontrado. TTS pode não funcionar.");
|
|
|
|
if (_ffmpegPath != null)
|
|
_logger.LogInformation("FFmpeg encontrado em: {Path}", _ffmpegPath);
|
|
else
|
|
_logger.LogWarning("FFmpeg não encontrado. Conversão para OGG pode não funcionar.");
|
|
}
|
|
|
|
private string? FindExecutable(string name)
|
|
{
|
|
var isWindows = RuntimeInformation.IsOSPlatform(OSPlatform.Windows);
|
|
var executable = isWindows ? $"{name}.exe" : name;
|
|
|
|
// Caminhos comuns
|
|
string[] searchPaths;
|
|
if (isWindows)
|
|
{
|
|
searchPaths = new[]
|
|
{
|
|
Path.Combine(AppContext.BaseDirectory, name),
|
|
@"C:\Program Files\eSpeak NG",
|
|
@"C:\Program Files (x86)\eSpeak NG",
|
|
@"C:\Apps\espeak-ng",
|
|
@"C:\Apps\ffmpeg\bin",
|
|
@"C:\ffmpeg\bin",
|
|
@"C:\Program Files\ffmpeg\bin"
|
|
};
|
|
}
|
|
else
|
|
{
|
|
searchPaths = new[]
|
|
{
|
|
"/usr/bin",
|
|
"/usr/local/bin",
|
|
"/opt/homebrew/bin"
|
|
};
|
|
}
|
|
|
|
foreach (var path in searchPaths)
|
|
{
|
|
var fullPath = Path.Combine(path, executable);
|
|
if (File.Exists(fullPath))
|
|
return fullPath;
|
|
}
|
|
|
|
// Tentar encontrar via PATH
|
|
var pathEnv = Environment.GetEnvironmentVariable("PATH");
|
|
if (!string.IsNullOrEmpty(pathEnv))
|
|
{
|
|
var separator = isWindows ? ';' : ':';
|
|
foreach (var path in pathEnv.Split(separator))
|
|
{
|
|
var fullPath = Path.Combine(path.Trim(), executable);
|
|
if (File.Exists(fullPath))
|
|
return fullPath;
|
|
}
|
|
}
|
|
|
|
return null;
|
|
}
|
|
|
|
public async Task<byte[]> GenerateAudioAsync(string text, string language = "pt-br", float rate = 1.0f, string gender = "f")
|
|
{
|
|
if (_espeakPath == null)
|
|
throw new InvalidOperationException("espeak-ng não está instalado no servidor.");
|
|
|
|
if (_ffmpegPath == null)
|
|
throw new InvalidOperationException("FFmpeg não está instalado no servidor.");
|
|
|
|
var tempWavPath = Path.Combine(Path.GetTempPath(), $"{Guid.NewGuid()}.wav");
|
|
var tempOggPath = Path.Combine(Path.GetTempPath(), $"{Guid.NewGuid()}.ogg");
|
|
|
|
try
|
|
{
|
|
// Mapear código de idioma para espeak (com gênero)
|
|
var espeakVoice = MapVoice(language, gender);
|
|
|
|
// Calcular velocidade (espeak usa words per minute, padrão ~175)
|
|
// Usar uma faixa mais conservadora para melhor inteligibilidade
|
|
var wordsPerMinute = (int)(150 * rate);
|
|
if (wordsPerMinute < 100) wordsPerMinute = 100;
|
|
if (wordsPerMinute > 200) wordsPerMinute = 200;
|
|
|
|
// Pitch fixo para melhor qualidade
|
|
var espeakPitch = 50;
|
|
|
|
// Gerar WAV com espeak-ng
|
|
var espeakArgs = $"-v {espeakVoice} -s {wordsPerMinute} -p {espeakPitch} -w \"{tempWavPath}\" \"{EscapeText(text)}\"";
|
|
|
|
_logger.LogInformation("Executando espeak-ng: {Args}", espeakArgs);
|
|
|
|
var espeakProcess = new Process
|
|
{
|
|
StartInfo = new ProcessStartInfo
|
|
{
|
|
FileName = _espeakPath,
|
|
Arguments = espeakArgs,
|
|
RedirectStandardOutput = true,
|
|
RedirectStandardError = true,
|
|
UseShellExecute = false,
|
|
CreateNoWindow = true
|
|
}
|
|
};
|
|
|
|
espeakProcess.Start();
|
|
await espeakProcess.WaitForExitAsync();
|
|
|
|
if (espeakProcess.ExitCode != 0)
|
|
{
|
|
var error = await espeakProcess.StandardError.ReadToEndAsync();
|
|
throw new Exception($"espeak-ng falhou: {error}");
|
|
}
|
|
|
|
if (!File.Exists(tempWavPath))
|
|
throw new Exception("espeak-ng não gerou o arquivo WAV.");
|
|
|
|
// Converter WAV para OGG com FFmpeg
|
|
var ffmpegArgs = $"-i \"{tempWavPath}\" -c:a libvorbis -q:a 4 -y \"{tempOggPath}\"";
|
|
|
|
_logger.LogInformation("Executando FFmpeg: {Args}", ffmpegArgs);
|
|
|
|
var ffmpegProcess = new Process
|
|
{
|
|
StartInfo = new ProcessStartInfo
|
|
{
|
|
FileName = _ffmpegPath,
|
|
Arguments = ffmpegArgs,
|
|
RedirectStandardOutput = true,
|
|
RedirectStandardError = true,
|
|
UseShellExecute = false,
|
|
CreateNoWindow = true
|
|
}
|
|
};
|
|
|
|
ffmpegProcess.Start();
|
|
await ffmpegProcess.WaitForExitAsync();
|
|
|
|
if (ffmpegProcess.ExitCode != 0)
|
|
{
|
|
var error = await ffmpegProcess.StandardError.ReadToEndAsync();
|
|
throw new Exception($"FFmpeg falhou: {error}");
|
|
}
|
|
|
|
if (!File.Exists(tempOggPath))
|
|
throw new Exception("FFmpeg não gerou o arquivo OGG.");
|
|
|
|
return await File.ReadAllBytesAsync(tempOggPath);
|
|
}
|
|
finally
|
|
{
|
|
// Limpar arquivos temporários
|
|
if (File.Exists(tempWavPath))
|
|
File.Delete(tempWavPath);
|
|
if (File.Exists(tempOggPath))
|
|
File.Delete(tempOggPath);
|
|
}
|
|
}
|
|
|
|
private string MapVoice(string language, string gender)
|
|
{
|
|
// espeak-ng usa sufixo +f1, +f2, etc para vozes femininas
|
|
// e +m1, +m2, etc para vozes masculinas
|
|
var genderSuffix = gender?.ToLower() == "m" ? "+m3" : "+f2";
|
|
|
|
// Mapear códigos de idioma para vozes espeak-ng
|
|
var baseLang = language.ToLower() switch
|
|
{
|
|
"pt-br" => "pt-br",
|
|
"pt" or "pt-pt" => "pt-pt",
|
|
"es" or "es-mx" or "es-cl" or "es-py" or "es-es" => "es",
|
|
"en" or "en-us" => "en-us",
|
|
"en-gb" => "en-gb",
|
|
_ => "pt-br"
|
|
};
|
|
|
|
return baseLang + genderSuffix;
|
|
}
|
|
|
|
private string EscapeText(string text)
|
|
{
|
|
// Escapar aspas e caracteres especiais para linha de comando
|
|
return text
|
|
.Replace("\\", "\\\\")
|
|
.Replace("\"", "\\\"")
|
|
.Replace("\n", " ")
|
|
.Replace("\r", "");
|
|
}
|
|
}
|
|
}
|