fix: features de audio
All checks were successful
Deploy ASP.NET MVC to OCI / build-and-deploy (push) Successful in 22m1s

This commit is contained in:
Ricardo Carneiro 2026-01-26 09:37:47 -03:00
parent 665d36d81c
commit f8f052428f
9 changed files with 615 additions and 31 deletions

View File

@ -20,7 +20,9 @@
"Bash(dotnet build:*)", "Bash(dotnet build:*)",
"Bash(rm:*)", "Bash(rm:*)",
"Bash(dotnet add package:*)", "Bash(dotnet add package:*)",
"Bash(dir:*)" "Bash(dir:*)",
"Bash(where:*)",
"Bash(winget install:*)"
], ],
"deny": [], "deny": [],
"ask": [] "ask": []

View File

@ -1,6 +1,7 @@
@{ @{
ViewData["Title"] = "Áudio para Texto (Transcrição)"; ViewData["Title"] = "Áudio para Texto (Transcrição)";
var culture = ViewContext.RouteData.Values["culture"] as string ?? "pt-BR"; var culture = ViewContext.RouteData.Values["culture"] as string ?? "pt-BR";
Layout = "_Layout";
} }
<div class="text-center mb-5"> <div class="text-center mb-5">

View File

@ -1,5 +1,6 @@
@{ @{
ViewData["Title"] = "Texto para Áudio (Voz)"; ViewData["Title"] = "Texto para Áudio (Voz)";
Layout = "_Layout";
} }
<div class="text-center mb-5"> <div class="text-center mb-5">
@ -31,12 +32,22 @@
</div> </div>
<div class="d-grid gap-2 d-md-flex justify-content-md-center"> <div class="d-grid gap-2 d-md-flex justify-content-md-center">
<button type="button" class="btn btn-primary btn-lg px-5" onclick="speak()"> <button type="button" class="btn btn-primary btn-lg px-5" id="btnSpeak" onclick="speak()">
<i class="bi bi-play-fill me-2"></i>Ouvir <i class="bi bi-play-fill me-2"></i>Ouvir
</button> </button>
<button type="button" class="btn btn-outline-danger btn-lg" onclick="stop()"> <button type="button" class="btn btn-outline-danger btn-lg" id="btnStop" onclick="stop()" style="display: none;">
<i class="bi bi-stop-fill me-2"></i>Parar <i class="bi bi-stop-fill me-2"></i>Parar
</button> </button>
<button type="button" class="btn btn-success btn-lg" id="btnDownload" onclick="downloadAudio()">
<i class="bi bi-download me-2" id="iconDownload"></i><span id="btnDownloadText">Baixar OGG</span>
</button>
</div>
<div id="downloadProgress" class="mt-3" style="display: none;">
<div class="progress">
<div class="progress-bar progress-bar-striped progress-bar-animated" role="progressbar" style="width: 100%"></div>
</div>
<small class="text-muted">Gerando áudio...</small>
</div> </div>
</div> </div>
@ -49,15 +60,15 @@
@section Scripts { @section Scripts {
<script> <script>
// Variáveis globais
const synth = window.speechSynthesis; const synth = window.speechSynthesis;
const voiceSelect = document.querySelector('#voiceSelect');
const textInput = document.querySelector('#textInput');
const rate = document.querySelector('#rate');
const pitch = document.querySelector('#pitch');
let voices = []; let voices = [];
// Função para popular lista de vozes
function populateVoiceList() { function populateVoiceList() {
const voiceSelect = document.getElementById('voiceSelect');
if (!voiceSelect) return;
voices = synth.getVoices().sort(function (a, b) { voices = synth.getVoices().sort(function (a, b) {
const aname = a.name.toUpperCase(); const aname = a.name.toUpperCase();
const bname = b.name.toUpperCase(); const bname = b.name.toUpperCase();
@ -66,8 +77,9 @@
else return 0; else return 0;
}); });
const selectedIndex = voiceSelect.selectedIndex < 0 ? 0 : voiceSelect.selectedIndex;
voiceSelect.innerHTML = ''; voiceSelect.innerHTML = '';
let ptBrIndex = -1;
let ptIndex = -1;
for (let i = 0; i < voices.length; i++) { for (let i = 0; i < voices.length; i++) {
const option = document.createElement('option'); const option = document.createElement('option');
@ -80,43 +92,205 @@
option.setAttribute('data-lang', voices[i].lang); option.setAttribute('data-lang', voices[i].lang);
option.setAttribute('data-name', voices[i].name); option.setAttribute('data-name', voices[i].name);
voiceSelect.appendChild(option); voiceSelect.appendChild(option);
// Procurar voz pt-BR (prioridade) ou pt (fallback)
if (ptBrIndex === -1 && voices[i].lang.toLowerCase() === 'pt-br') {
ptBrIndex = i;
} else if (ptIndex === -1 && voices[i].lang.toLowerCase().startsWith('pt')) {
ptIndex = i;
}
} }
voiceSelect.selectedIndex = selectedIndex;
// Selecionar voz pt-BR por padrão, ou pt, ou primeira disponível
if (ptBrIndex !== -1) {
voiceSelect.selectedIndex = ptBrIndex;
console.log('Voz pt-BR selecionada por padrão:', voices[ptBrIndex].name);
} else if (ptIndex !== -1) {
voiceSelect.selectedIndex = ptIndex;
console.log('Voz pt selecionada por padrão:', voices[ptIndex].name);
} else {
voiceSelect.selectedIndex = 0;
}
console.log('Vozes carregadas:', voices.length);
} }
// Inicializar vozes
populateVoiceList(); populateVoiceList();
if (speechSynthesis.onvoiceschanged !== undefined) { if (speechSynthesis.onvoiceschanged !== undefined) {
speechSynthesis.onvoiceschanged = populateVoiceList; speechSynthesis.onvoiceschanged = populateVoiceList;
} }
function speak() { // Função global para falar - acessível pelo onclick
if (synth.speaking) { window.speak = function() {
console.error('speechSynthesis.speaking'); console.log('Função speak() chamada');
const textInput = document.getElementById('textInput');
const voiceSelect = document.getElementById('voiceSelect');
const rate = document.getElementById('rate');
const pitch = document.getElementById('pitch');
if (!textInput || !voiceSelect) {
console.error('Elementos não encontrados');
return; return;
} }
if (textInput.value !== '') {
const utterThis = new SpeechSynthesisUtterance(textInput.value); if (synth.speaking) {
utterThis.onend = function (event) { console.log('Já está falando, cancelando...');
console.log('SpeechSynthesisUtterance.onend'); synth.cancel();
} }
utterThis.onerror = function (event) {
console.error('SpeechSynthesisUtterance.onerror'); const text = textInput.value.trim();
} if (text === '') {
const selectedOption = voiceSelect.selectedOptions[0].getAttribute('data-name'); alert('Por favor, digite algum texto para ser lido.');
return;
}
console.log('Texto a falar:', text);
const utterThis = new SpeechSynthesisUtterance(text);
utterThis.onstart = function(event) {
console.log('Iniciou a fala');
document.getElementById('btnStop').style.display = 'inline-block';
document.getElementById('btnSpeak').disabled = true;
};
utterThis.onend = function(event) {
console.log('Terminou a fala');
document.getElementById('btnStop').style.display = 'none';
document.getElementById('btnSpeak').disabled = false;
};
utterThis.onerror = function(event) {
console.error('Erro na fala:', event.error);
document.getElementById('btnStop').style.display = 'none';
document.getElementById('btnSpeak').disabled = false;
};
// Selecionar voz
if (voiceSelect.selectedOptions.length > 0) {
const selectedVoiceName = voiceSelect.selectedOptions[0].getAttribute('data-name');
for (let i = 0; i < voices.length; i++) { for (let i = 0; i < voices.length; i++) {
if (voices[i].name === selectedOption) { if (voices[i].name === selectedVoiceName) {
utterThis.voice = voices[i]; utterThis.voice = voices[i];
console.log('Voz selecionada:', voices[i].name);
break; break;
} }
} }
utterThis.pitch = pitch.value;
utterThis.rate = rate.value;
synth.speak(utterThis);
} }
utterThis.pitch = parseFloat(pitch.value);
utterThis.rate = parseFloat(rate.value);
console.log('Pitch:', utterThis.pitch, 'Rate:', utterThis.rate);
// Falar
synth.speak(utterThis);
};
// Função global para parar
window.stop = function() {
console.log('Função stop() chamada');
synth.cancel();
document.getElementById('btnStop').style.display = 'none';
document.getElementById('btnSpeak').disabled = false;
};
// Detectar se é dispositivo móvel
const isMobile = /Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i.test(navigator.userAgent);
// Atualizar texto do botão para dispositivos móveis
if (isMobile && navigator.canShare) {
document.getElementById('iconDownload').className = 'bi bi-share me-2';
document.getElementById('btnDownloadText').textContent = 'Compartilhar';
} }
function stop() { // Função global para baixar/compartilhar áudio OGG
synth.cancel(); window.downloadAudio = async function() {
} const textInput = document.getElementById('textInput');
const voiceSelect = document.getElementById('voiceSelect');
const rate = document.getElementById('rate');
const pitch = document.getElementById('pitch');
const btnDownload = document.getElementById('btnDownload');
const downloadProgress = document.getElementById('downloadProgress');
const text = textInput.value.trim();
if (text === '') {
alert('Por favor, digite algum texto para gerar o áudio.');
return;
}
// Obter idioma da voz selecionada
let lang = 'pt-BR';
if (voiceSelect.selectedOptions.length > 0) {
lang = voiceSelect.selectedOptions[0].getAttribute('data-lang') || 'pt-BR';
}
// Mostrar progresso
btnDownload.disabled = true;
downloadProgress.style.display = 'block';
try {
const response = await fetch('/api/tts/generate', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({
text: text,
language: lang,
rate: parseFloat(rate.value),
pitch: parseFloat(pitch.value)
})
});
if (!response.ok) {
const error = await response.text();
throw new Error(error || 'Erro ao gerar áudio');
}
const blob = await response.blob();
// Se for mobile e suportar Web Share API, usar compartilhamento nativo
if (isMobile && navigator.canShare) {
const file = new File([blob], 'audio.ogg', { type: 'audio/ogg' });
if (navigator.canShare({ files: [file] })) {
try {
await navigator.share({
files: [file],
title: 'Áudio gerado',
text: 'Áudio gerado pelo Convert-It'
});
return; // Compartilhamento bem sucedido
} catch (shareError) {
if (shareError.name !== 'AbortError') {
console.log('Compartilhamento cancelado ou falhou, usando download');
}
}
}
}
// Fallback: download tradicional
const url = window.URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = 'audio.ogg';
document.body.appendChild(a);
a.click();
document.body.removeChild(a);
window.URL.revokeObjectURL(url);
} catch (error) {
console.error('Erro:', error);
alert('Erro ao gerar áudio: ' + error.message);
} finally {
btnDownload.disabled = false;
downloadProgress.style.display = 'none';
}
};
console.log('Script de Text-to-Speech carregado com sucesso!');
</script> </script>
} }

View File

@ -0,0 +1,67 @@
using Microsoft.AspNetCore.Mvc;
using Convert_It_Online.Services;
namespace Convert_It_Online.Controllers
{
[ApiController]
[Route("api/tts")]
public class TtsApiController : ControllerBase
{
private readonly ITextToSpeechService _ttsService;
private readonly ILogger<TtsApiController> _logger;
public TtsApiController(ITextToSpeechService ttsService, ILogger<TtsApiController> logger)
{
_ttsService = ttsService;
_logger = logger;
}
[HttpPost("generate")]
public async Task<IActionResult> Generate([FromBody] TtsRequest request)
{
if (string.IsNullOrWhiteSpace(request.Text))
{
return BadRequest("Texto é obrigatório.");
}
// Limitar tamanho do texto para evitar abusos
if (request.Text.Length > 5000)
{
return BadRequest("Texto muito longo. Máximo de 5000 caracteres.");
}
try
{
_logger.LogInformation("Gerando áudio TTS: {Length} caracteres, idioma: {Lang}",
request.Text.Length, request.Language);
var audioBytes = await _ttsService.GenerateAudioAsync(
request.Text,
request.Language ?? "pt-BR",
request.Rate ?? 1.0f,
request.Pitch ?? 1.0f
);
return File(audioBytes, "audio/ogg", "audio.ogg");
}
catch (InvalidOperationException ex)
{
_logger.LogWarning(ex, "TTS não disponível");
return StatusCode(503, "Serviço de síntese de voz não disponível no momento.");
}
catch (Exception ex)
{
_logger.LogError(ex, "Erro ao gerar áudio TTS");
return StatusCode(500, "Erro ao gerar áudio.");
}
}
}
public class TtsRequest
{
public string Text { get; set; } = string.Empty;
public string? Language { get; set; }
public float? Rate { get; set; }
public float? Pitch { get; set; }
}
}

View File

@ -44,10 +44,11 @@ FROM base AS final
WORKDIR /app WORKDIR /app
COPY --from=publish /app/publish . COPY --from=publish /app/publish .
# Instalar ffmpeg e bibliotecas nativas (rodar como root) # Instalar ffmpeg, espeak-ng e bibliotecas nativas (rodar como root)
USER root USER root
RUN apt-get update && apt-get install -y \ RUN apt-get update && apt-get install -y \
ffmpeg \ ffmpeg \
espeak-ng \
libc6-dev \ libc6-dev \
&& rm -rf /var/lib/apt/lists/* && rm -rf /var/lib/apt/lists/*
USER app USER app

View File

@ -164,6 +164,7 @@ builder.Services.AddLocalization();
builder.Services.AddSingleton<IUrlTranslationService, UrlTranslationService>(); builder.Services.AddSingleton<IUrlTranslationService, UrlTranslationService>();
builder.Services.AddSingleton<IAudioTranscriptionService, AudioTranscriptionService>(); builder.Services.AddSingleton<IAudioTranscriptionService, AudioTranscriptionService>();
builder.Services.AddSingleton<ITextToSpeechService, TextToSpeechService>();
var supportedCultures = new[] { "pt-BR", "es-MX", "es-CL", "es-PY" }; var supportedCultures = new[] { "pt-BR", "es-MX", "es-CL", "es-PY" };
builder.Services.Configure<RequestLocalizationOptions>(options => builder.Services.Configure<RequestLocalizationOptions>(options =>

View File

@ -1,6 +1,8 @@
using System; using System;
using System.Diagnostics;
using System.IO; using System.IO;
using System.Net.Http; using System.Net.Http;
using System.Runtime.InteropServices;
using System.Threading.Tasks; using System.Threading.Tasks;
using Whisper.net; using Whisper.net;
using Whisper.net.Ggml; using Whisper.net.Ggml;
@ -14,6 +16,7 @@ namespace Convert_It_Online.Services
private readonly string _modelPath; private readonly string _modelPath;
private readonly ILogger<AudioTranscriptionService> _logger; private readonly ILogger<AudioTranscriptionService> _logger;
private readonly HttpClient _httpClient; private readonly HttpClient _httpClient;
private static bool _ffmpegConfigured = false;
public AudioTranscriptionService(ILogger<AudioTranscriptionService> logger) public AudioTranscriptionService(ILogger<AudioTranscriptionService> logger)
{ {
@ -27,6 +30,123 @@ namespace Convert_It_Online.Services
{ {
Directory.CreateDirectory(modelsDir!); Directory.CreateDirectory(modelsDir!);
} }
// Configurar FFmpeg uma única vez
ConfigureFFmpeg();
}
private void ConfigureFFmpeg()
{
if (_ffmpegConfigured) return;
try
{
string? ffmpegPath = null;
// 1. Primeiro, verificar variável de ambiente (maior prioridade)
var envPath = Environment.GetEnvironmentVariable("FFMPEG_PATH");
if (!string.IsNullOrEmpty(envPath) && Directory.Exists(envPath))
{
ffmpegPath = envPath;
_logger.LogInformation("FFmpeg configurado via FFMPEG_PATH: {Path}", ffmpegPath);
}
// 2. Se não encontrou, procurar em locais comuns
if (ffmpegPath == null)
{
if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
{
// Windows: procurar em locais comuns
var possiblePaths = new[]
{
Path.Combine(AppContext.BaseDirectory, "ffmpeg"),
@"C:\Apps\ffmpeg\bin",
@"C:\Apps\ffmpeg",
Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.ProgramFiles), "ffmpeg", "bin"),
@"C:\ffmpeg\bin",
@"C:\ffmpeg",
@"C:\Program Files\ffmpeg\bin",
@"C:\tools\ffmpeg\bin",
@"C:\tools\ffmpeg"
};
foreach (var path in possiblePaths)
{
var ffmpegExe = Path.Combine(path, "ffmpeg.exe");
if (File.Exists(ffmpegExe))
{
ffmpegPath = path;
_logger.LogInformation("FFmpeg encontrado em: {Path}", ffmpegPath);
break;
}
}
// Tentar encontrar via PATH
if (ffmpegPath == null)
{
ffmpegPath = FindFFmpegInPath("ffmpeg.exe");
}
}
else
{
// Linux: verificar locais padrão
var possiblePaths = new[]
{
"/usr/bin",
"/usr/local/bin",
"/opt/ffmpeg/bin"
};
foreach (var path in possiblePaths)
{
var ffmpegExe = Path.Combine(path, "ffmpeg");
if (File.Exists(ffmpegExe))
{
ffmpegPath = path;
_logger.LogInformation("FFmpeg encontrado em: {Path}", ffmpegPath);
break;
}
}
}
}
if (!string.IsNullOrEmpty(ffmpegPath))
{
FFmpeg.SetExecutablesPath(ffmpegPath);
_logger.LogInformation("FFmpeg.SetExecutablesPath configurado: {Path}", ffmpegPath);
}
else
{
_logger.LogWarning("FFmpeg não encontrado em caminhos conhecidos. A transcrição de áudio pode falhar.");
}
_ffmpegConfigured = true;
}
catch (Exception ex)
{
_logger.LogError(ex, "Erro ao configurar FFmpeg");
}
}
private string? FindFFmpegInPath(string executable)
{
var pathEnv = Environment.GetEnvironmentVariable("PATH");
if (string.IsNullOrEmpty(pathEnv)) return null;
var separator = RuntimeInformation.IsOSPlatform(OSPlatform.Windows) ? ';' : ':';
var paths = pathEnv.Split(separator);
foreach (var path in paths)
{
var fullPath = Path.Combine(path.Trim(), executable);
if (File.Exists(fullPath))
{
_logger.LogInformation("FFmpeg encontrado no PATH: {Path}", path);
return path;
}
}
return null;
} }
private async Task EnsureModelExistsAsync() private async Task EnsureModelExistsAsync()

View File

@ -0,0 +1,9 @@
using System.Threading.Tasks;
namespace Convert_It_Online.Services
{
public interface ITextToSpeechService
{
Task<byte[]> GenerateAudioAsync(string text, string language = "pt-BR", float rate = 1.0f, float pitch = 1.0f);
}
}

View File

@ -0,0 +1,209 @@
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, float pitch = 1.0f)
{
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
var espeakLang = MapLanguageCode(language);
// Calcular velocidade (espeak usa words per minute, padrão ~175)
var wordsPerMinute = (int)(175 * rate);
if (wordsPerMinute < 80) wordsPerMinute = 80;
if (wordsPerMinute > 450) wordsPerMinute = 450;
// Calcular pitch (espeak usa 0-99, padrão 50)
var espeakPitch = (int)(50 * pitch);
if (espeakPitch < 0) espeakPitch = 0;
if (espeakPitch > 99) espeakPitch = 99;
// Gerar WAV com espeak-ng
var espeakArgs = $"-v {espeakLang} -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 MapLanguageCode(string language)
{
// Mapear códigos de idioma para vozes espeak-ng
return language.ToLower() switch
{
"pt-br" => "pt-br",
"pt" => "pt",
"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" // Padrão
};
}
private string EscapeText(string text)
{
// Escapar aspas e caracteres especiais para linha de comando
return text
.Replace("\\", "\\\\")
.Replace("\"", "\\\"")
.Replace("\n", " ")
.Replace("\r", "");
}
}
}