fix: vozes
All checks were successful
Deploy ASP.NET MVC to OCI / build-and-deploy (push) Successful in 17m26s
All checks were successful
Deploy ASP.NET MVC to OCI / build-and-deploy (push) Successful in 17m26s
This commit is contained in:
parent
f8f052428f
commit
00804702e0
@ -5,7 +5,7 @@
|
|||||||
|
|
||||||
<div class="text-center mb-5">
|
<div class="text-center mb-5">
|
||||||
<h1 class="display-4">@ViewData["Title"]</h1>
|
<h1 class="display-4">@ViewData["Title"]</h1>
|
||||||
<p class="lead">Converta qualquer texto em fala usando vozes neurais de alta qualidade.</p>
|
<p class="lead">Converta qualquer texto em áudio para compartilhar no WhatsApp ou baixar.</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="row justify-content-center">
|
<div class="row justify-content-center">
|
||||||
@ -13,47 +13,71 @@
|
|||||||
<div class="card shadow-custom p-4">
|
<div class="card shadow-custom p-4">
|
||||||
<div class="mb-4">
|
<div class="mb-4">
|
||||||
<label for="textInput" class="form-label h5">Digite ou cole seu texto</label>
|
<label for="textInput" class="form-label h5">Digite ou cole seu texto</label>
|
||||||
<textarea class="form-control" id="textInput" rows="6" placeholder="Escreva aqui o que você deseja que seja lido..."></textarea>
|
<textarea class="form-control" id="textInput" rows="6" placeholder="Escreva aqui o que você deseja converter em áudio..." maxlength="5000"></textarea>
|
||||||
|
<div class="form-text text-end"><span id="charCount">0</span>/5000 caracteres</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="row mb-4">
|
<div class="row mb-4">
|
||||||
<div class="col-md-6">
|
<div class="col-md-4">
|
||||||
<label for="voiceSelect" class="form-label">Escolher Voz</label>
|
<label for="voiceSelect" class="form-label">Idioma</label>
|
||||||
<select id="voiceSelect" class="form-select"></select>
|
<select id="voiceSelect" class="form-select">
|
||||||
|
<option value="pt-br" selected>Português (Brasil)</option>
|
||||||
|
<option value="pt-pt">Português (Portugal)</option>
|
||||||
|
<option value="es">Espanhol</option>
|
||||||
|
<option value="en-us">Inglês (EUA)</option>
|
||||||
|
<option value="en-gb">Inglês (UK)</option>
|
||||||
|
</select>
|
||||||
</div>
|
</div>
|
||||||
<div class="col-md-3">
|
<div class="col-md-4">
|
||||||
<label for="rate" class="form-label">Velocidade</label>
|
<label for="rate" class="form-label">Velocidade: <span id="rateValue">1.0x</span></label>
|
||||||
<input type="range" class="form-range" min="0.5" max="2" step="0.1" id="rate" value="1">
|
<input type="range" class="form-range" min="0.5" max="1.5" step="0.1" id="rate" value="1">
|
||||||
</div>
|
</div>
|
||||||
<div class="col-md-3">
|
<div class="col-md-4">
|
||||||
<label for="pitch" class="form-label">Tom</label>
|
<label for="voiceGender" class="form-label">Voz</label>
|
||||||
<input type="range" class="form-range" min="0" max="2" step="0.1" id="pitch" value="1">
|
<select id="voiceGender" class="form-select">
|
||||||
|
<option value="f" selected>Feminina</option>
|
||||||
|
<option value="m">Masculina</option>
|
||||||
|
</select>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div class="d-grid">
|
||||||
|
<button type="button" class="btn btn-primary btn-lg" id="btnGenerate" onclick="generateAudio()">
|
||||||
|
<i class="bi bi-soundwave me-2"></i>Gerar Áudio
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Indicador de progresso -->
|
||||||
|
<div id="progressContainer" class="mt-4" style="display: none;">
|
||||||
|
<div class="text-center mb-2">
|
||||||
|
<div class="spinner-border text-primary" role="status">
|
||||||
|
<span class="visually-hidden">Gerando...</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<p class="text-center text-muted mb-0">Gerando áudio, aguarde...</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Player e botões de ação -->
|
||||||
|
<div id="resultContainer" class="mt-4" style="display: none;">
|
||||||
|
<div class="bg-light rounded p-3">
|
||||||
|
<label class="form-label fw-bold mb-2"><i class="bi bi-music-note-beamed me-2"></i>Áudio gerado:</label>
|
||||||
|
<audio id="audioPlayer" controls class="w-100 mb-3"></audio>
|
||||||
|
|
||||||
<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" id="btnSpeak" onclick="speak()">
|
<button type="button" class="btn btn-success btn-lg" id="btnShare" onclick="shareAudio()">
|
||||||
<i class="bi bi-play-fill me-2"></i>Ouvir
|
<i class="bi bi-share me-2" id="iconShare"></i><span id="btnShareText">Compartilhar</span>
|
||||||
</button>
|
</button>
|
||||||
<button type="button" class="btn btn-outline-danger btn-lg" id="btnStop" onclick="stop()" style="display: none;">
|
<button type="button" class="btn btn-outline-primary btn-lg" id="btnDownload" onclick="downloadAudio()">
|
||||||
<i class="bi bi-stop-fill me-2"></i>Parar
|
<i class="bi bi-download me-2"></i>Baixar
|
||||||
</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>
|
</button>
|
||||||
</div>
|
</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>
|
</div>
|
||||||
<small class="text-muted">Gerando áudio...</small>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="mt-4 alert alert-info">
|
<div class="mt-4 alert alert-info">
|
||||||
<i class="bi bi-info-circle me-2"></i>
|
<i class="bi bi-info-circle me-2"></i>
|
||||||
Esta ferramenta usa as vozes instaladas no seu dispositivo. No Android e Windows, você encontrará opções de vozes neurais muito naturais.
|
O áudio é gerado em formato OGG, compatível com WhatsApp e a maioria dos aplicativos de mensagem.
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -61,159 +85,34 @@
|
|||||||
@section Scripts {
|
@section Scripts {
|
||||||
<script>
|
<script>
|
||||||
// Variáveis globais
|
// Variáveis globais
|
||||||
const synth = window.speechSynthesis;
|
let audioBase64 = null;
|
||||||
let voices = [];
|
let audioBlob = null;
|
||||||
|
|
||||||
// Função para popular lista de vozes
|
|
||||||
function populateVoiceList() {
|
|
||||||
const voiceSelect = document.getElementById('voiceSelect');
|
|
||||||
if (!voiceSelect) return;
|
|
||||||
|
|
||||||
voices = synth.getVoices().sort(function (a, b) {
|
|
||||||
const aname = a.name.toUpperCase();
|
|
||||||
const bname = b.name.toUpperCase();
|
|
||||||
if (aname < bname) return -1;
|
|
||||||
else if (aname > bname) return 1;
|
|
||||||
else return 0;
|
|
||||||
});
|
|
||||||
|
|
||||||
voiceSelect.innerHTML = '';
|
|
||||||
let ptBrIndex = -1;
|
|
||||||
let ptIndex = -1;
|
|
||||||
|
|
||||||
for (let i = 0; i < voices.length; i++) {
|
|
||||||
const option = document.createElement('option');
|
|
||||||
option.textContent = voices[i].name + ' (' + voices[i].lang + ')';
|
|
||||||
|
|
||||||
if (voices[i].default) {
|
|
||||||
option.textContent += ' -- PADRÃO';
|
|
||||||
}
|
|
||||||
|
|
||||||
option.setAttribute('data-lang', voices[i].lang);
|
|
||||||
option.setAttribute('data-name', voices[i].name);
|
|
||||||
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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 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();
|
|
||||||
if (speechSynthesis.onvoiceschanged !== undefined) {
|
|
||||||
speechSynthesis.onvoiceschanged = populateVoiceList;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Função global para falar - acessível pelo onclick
|
|
||||||
window.speak = function() {
|
|
||||||
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;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (synth.speaking) {
|
|
||||||
console.log('Já está falando, cancelando...');
|
|
||||||
synth.cancel();
|
|
||||||
}
|
|
||||||
|
|
||||||
const text = textInput.value.trim();
|
|
||||||
if (text === '') {
|
|
||||||
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++) {
|
|
||||||
if (voices[i].name === selectedVoiceName) {
|
|
||||||
utterThis.voice = voices[i];
|
|
||||||
console.log('Voz selecionada:', voices[i].name);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
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);
|
const isMobile = /Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i.test(navigator.userAgent);
|
||||||
|
|
||||||
// Atualizar texto do botão para dispositivos móveis
|
// Atualizar contador de caracteres
|
||||||
if (isMobile && navigator.canShare) {
|
document.getElementById('textInput').addEventListener('input', function() {
|
||||||
document.getElementById('iconDownload').className = 'bi bi-share me-2';
|
document.getElementById('charCount').textContent = this.value.length;
|
||||||
document.getElementById('btnDownloadText').textContent = 'Compartilhar';
|
});
|
||||||
|
|
||||||
|
// Atualizar label de velocidade
|
||||||
|
document.getElementById('rate').addEventListener('input', function() {
|
||||||
|
document.getElementById('rateValue').textContent = this.value + 'x';
|
||||||
|
});
|
||||||
|
|
||||||
|
// Em mobile, trocar texto do botão compartilhar
|
||||||
|
if (!isMobile || !navigator.canShare) {
|
||||||
|
document.getElementById('btnShare').style.display = 'none';
|
||||||
}
|
}
|
||||||
|
|
||||||
// Função global para baixar/compartilhar áudio OGG
|
// Gerar áudio
|
||||||
window.downloadAudio = async function() {
|
window.generateAudio = async function() {
|
||||||
const textInput = document.getElementById('textInput');
|
const textInput = document.getElementById('textInput');
|
||||||
const voiceSelect = document.getElementById('voiceSelect');
|
const voiceSelect = document.getElementById('voiceSelect');
|
||||||
|
const voiceGender = document.getElementById('voiceGender');
|
||||||
const rate = document.getElementById('rate');
|
const rate = document.getElementById('rate');
|
||||||
const pitch = document.getElementById('pitch');
|
const btnGenerate = document.getElementById('btnGenerate');
|
||||||
const btnDownload = document.getElementById('btnDownload');
|
const progressContainer = document.getElementById('progressContainer');
|
||||||
const downloadProgress = document.getElementById('downloadProgress');
|
const resultContainer = document.getElementById('resultContainer');
|
||||||
|
|
||||||
const text = textInput.value.trim();
|
const text = textInput.value.trim();
|
||||||
if (text === '') {
|
if (text === '') {
|
||||||
@ -221,15 +120,10 @@
|
|||||||
return;
|
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
|
// Mostrar progresso
|
||||||
btnDownload.disabled = true;
|
btnGenerate.disabled = true;
|
||||||
downloadProgress.style.display = 'block';
|
progressContainer.style.display = 'block';
|
||||||
|
resultContainer.style.display = 'none';
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const response = await fetch('/api/tts/generate', {
|
const response = await fetch('/api/tts/generate', {
|
||||||
@ -239,9 +133,9 @@
|
|||||||
},
|
},
|
||||||
body: JSON.stringify({
|
body: JSON.stringify({
|
||||||
text: text,
|
text: text,
|
||||||
language: lang,
|
language: voiceSelect.value,
|
||||||
rate: parseFloat(rate.value),
|
rate: parseFloat(rate.value),
|
||||||
pitch: parseFloat(pitch.value)
|
gender: voiceGender.value
|
||||||
})
|
})
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -250,30 +144,70 @@
|
|||||||
throw new Error(error || 'Erro ao gerar áudio');
|
throw new Error(error || 'Erro ao gerar áudio');
|
||||||
}
|
}
|
||||||
|
|
||||||
const blob = await response.blob();
|
const data = await response.json();
|
||||||
|
audioBase64 = data.audio;
|
||||||
|
|
||||||
// Se for mobile e suportar Web Share API, usar compartilhamento nativo
|
// Converter base64 para blob
|
||||||
if (isMobile && navigator.canShare) {
|
const byteCharacters = atob(audioBase64);
|
||||||
const file = new File([blob], 'audio.ogg', { type: 'audio/ogg' });
|
const byteNumbers = new Array(byteCharacters.length);
|
||||||
|
for (let i = 0; i < byteCharacters.length; i++) {
|
||||||
|
byteNumbers[i] = byteCharacters.charCodeAt(i);
|
||||||
|
}
|
||||||
|
const byteArray = new Uint8Array(byteNumbers);
|
||||||
|
audioBlob = new Blob([byteArray], { type: 'audio/ogg' });
|
||||||
|
|
||||||
|
// Configurar player
|
||||||
|
const audioPlayer = document.getElementById('audioPlayer');
|
||||||
|
audioPlayer.src = 'data:audio/ogg;base64,' + audioBase64;
|
||||||
|
|
||||||
|
// Mostrar resultado
|
||||||
|
resultContainer.style.display = 'block';
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Erro:', error);
|
||||||
|
alert('Erro ao gerar áudio: ' + error.message);
|
||||||
|
} finally {
|
||||||
|
btnGenerate.disabled = false;
|
||||||
|
progressContainer.style.display = 'none';
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Compartilhar áudio (mobile)
|
||||||
|
window.shareAudio = async function() {
|
||||||
|
if (!audioBlob) {
|
||||||
|
alert('Gere o áudio primeiro.');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
if (navigator.canShare({ files: [file] })) {
|
|
||||||
try {
|
try {
|
||||||
|
const file = new File([audioBlob], 'audio.ogg', { type: 'audio/ogg' });
|
||||||
|
|
||||||
|
if (navigator.canShare && navigator.canShare({ files: [file] })) {
|
||||||
await navigator.share({
|
await navigator.share({
|
||||||
files: [file],
|
files: [file],
|
||||||
title: 'Áudio gerado',
|
title: 'Áudio',
|
||||||
text: 'Áudio gerado pelo Convert-It'
|
text: ''
|
||||||
});
|
});
|
||||||
return; // Compartilhamento bem sucedido
|
} else {
|
||||||
} catch (shareError) {
|
// Fallback para download
|
||||||
if (shareError.name !== 'AbortError') {
|
downloadAudio();
|
||||||
console.log('Compartilhamento cancelado ou falhou, usando download');
|
}
|
||||||
}
|
} catch (error) {
|
||||||
|
if (error.name !== 'AbortError') {
|
||||||
|
console.error('Erro ao compartilhar:', error);
|
||||||
|
downloadAudio();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Baixar áudio
|
||||||
|
window.downloadAudio = function() {
|
||||||
|
if (!audioBlob) {
|
||||||
|
alert('Gere o áudio primeiro.');
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Fallback: download tradicional
|
const url = window.URL.createObjectURL(audioBlob);
|
||||||
const url = window.URL.createObjectURL(blob);
|
|
||||||
const a = document.createElement('a');
|
const a = document.createElement('a');
|
||||||
a.href = url;
|
a.href = url;
|
||||||
a.download = 'audio.ogg';
|
a.download = 'audio.ogg';
|
||||||
@ -281,14 +215,6 @@
|
|||||||
a.click();
|
a.click();
|
||||||
document.body.removeChild(a);
|
document.body.removeChild(a);
|
||||||
window.URL.revokeObjectURL(url);
|
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!');
|
console.log('Script de Text-to-Speech carregado com sucesso!');
|
||||||
|
|||||||
@ -32,17 +32,24 @@ namespace Convert_It_Online.Controllers
|
|||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
_logger.LogInformation("Gerando áudio TTS: {Length} caracteres, idioma: {Lang}",
|
_logger.LogInformation("Gerando áudio TTS: {Length} caracteres, idioma: {Lang}, gênero: {Gender}",
|
||||||
request.Text.Length, request.Language);
|
request.Text.Length, request.Language, request.Gender);
|
||||||
|
|
||||||
var audioBytes = await _ttsService.GenerateAudioAsync(
|
var audioBytes = await _ttsService.GenerateAudioAsync(
|
||||||
request.Text,
|
request.Text,
|
||||||
request.Language ?? "pt-BR",
|
request.Language ?? "pt-br",
|
||||||
request.Rate ?? 1.0f,
|
request.Rate ?? 1.0f,
|
||||||
request.Pitch ?? 1.0f
|
request.Gender ?? "f"
|
||||||
);
|
);
|
||||||
|
|
||||||
return File(audioBytes, "audio/ogg", "audio.ogg");
|
// Retornar como JSON com base64
|
||||||
|
var base64Audio = Convert.ToBase64String(audioBytes);
|
||||||
|
return Ok(new TtsResponse
|
||||||
|
{
|
||||||
|
Audio = base64Audio,
|
||||||
|
Format = "ogg",
|
||||||
|
Size = audioBytes.Length
|
||||||
|
});
|
||||||
}
|
}
|
||||||
catch (InvalidOperationException ex)
|
catch (InvalidOperationException ex)
|
||||||
{
|
{
|
||||||
@ -62,6 +69,13 @@ namespace Convert_It_Online.Controllers
|
|||||||
public string Text { get; set; } = string.Empty;
|
public string Text { get; set; } = string.Empty;
|
||||||
public string? Language { get; set; }
|
public string? Language { get; set; }
|
||||||
public float? Rate { get; set; }
|
public float? Rate { get; set; }
|
||||||
public float? Pitch { get; set; }
|
public string? Gender { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
public class TtsResponse
|
||||||
|
{
|
||||||
|
public string Audio { get; set; } = string.Empty;
|
||||||
|
public string Format { get; set; } = "ogg";
|
||||||
|
public int Size { get; set; }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -4,6 +4,6 @@ namespace Convert_It_Online.Services
|
|||||||
{
|
{
|
||||||
public interface ITextToSpeechService
|
public interface ITextToSpeechService
|
||||||
{
|
{
|
||||||
Task<byte[]> GenerateAudioAsync(string text, string language = "pt-BR", float rate = 1.0f, float pitch = 1.0f);
|
Task<byte[]> GenerateAudioAsync(string text, string language = "pt-br", float rate = 1.0f, string gender = "f");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -83,7 +83,7 @@ namespace Convert_It_Online.Services
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task<byte[]> GenerateAudioAsync(string text, string language = "pt-BR", float rate = 1.0f, float pitch = 1.0f)
|
public async Task<byte[]> GenerateAudioAsync(string text, string language = "pt-br", float rate = 1.0f, string gender = "f")
|
||||||
{
|
{
|
||||||
if (_espeakPath == null)
|
if (_espeakPath == null)
|
||||||
throw new InvalidOperationException("espeak-ng não está instalado no servidor.");
|
throw new InvalidOperationException("espeak-ng não está instalado no servidor.");
|
||||||
@ -96,21 +96,20 @@ namespace Convert_It_Online.Services
|
|||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
// Mapear código de idioma para espeak
|
// Mapear código de idioma para espeak (com gênero)
|
||||||
var espeakLang = MapLanguageCode(language);
|
var espeakVoice = MapVoice(language, gender);
|
||||||
|
|
||||||
// Calcular velocidade (espeak usa words per minute, padrão ~175)
|
// Calcular velocidade (espeak usa words per minute, padrão ~175)
|
||||||
var wordsPerMinute = (int)(175 * rate);
|
// Usar uma faixa mais conservadora para melhor inteligibilidade
|
||||||
if (wordsPerMinute < 80) wordsPerMinute = 80;
|
var wordsPerMinute = (int)(150 * rate);
|
||||||
if (wordsPerMinute > 450) wordsPerMinute = 450;
|
if (wordsPerMinute < 100) wordsPerMinute = 100;
|
||||||
|
if (wordsPerMinute > 200) wordsPerMinute = 200;
|
||||||
|
|
||||||
// Calcular pitch (espeak usa 0-99, padrão 50)
|
// Pitch fixo para melhor qualidade
|
||||||
var espeakPitch = (int)(50 * pitch);
|
var espeakPitch = 50;
|
||||||
if (espeakPitch < 0) espeakPitch = 0;
|
|
||||||
if (espeakPitch > 99) espeakPitch = 99;
|
|
||||||
|
|
||||||
// Gerar WAV com espeak-ng
|
// Gerar WAV com espeak-ng
|
||||||
var espeakArgs = $"-v {espeakLang} -s {wordsPerMinute} -p {espeakPitch} -w \"{tempWavPath}\" \"{EscapeText(text)}\"";
|
var espeakArgs = $"-v {espeakVoice} -s {wordsPerMinute} -p {espeakPitch} -w \"{tempWavPath}\" \"{EscapeText(text)}\"";
|
||||||
|
|
||||||
_logger.LogInformation("Executando espeak-ng: {Args}", espeakArgs);
|
_logger.LogInformation("Executando espeak-ng: {Args}", espeakArgs);
|
||||||
|
|
||||||
@ -181,19 +180,24 @@ namespace Convert_It_Online.Services
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private string MapLanguageCode(string language)
|
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
|
// Mapear códigos de idioma para vozes espeak-ng
|
||||||
return language.ToLower() switch
|
var baseLang = language.ToLower() switch
|
||||||
{
|
{
|
||||||
"pt-br" => "pt-br",
|
"pt-br" => "pt-br",
|
||||||
"pt" => "pt",
|
"pt" or "pt-pt" => "pt-pt",
|
||||||
"pt-pt" => "pt-pt",
|
|
||||||
"es" or "es-mx" or "es-cl" or "es-py" or "es-es" => "es",
|
"es" or "es-mx" or "es-cl" or "es-py" or "es-es" => "es",
|
||||||
"en" or "en-us" => "en-us",
|
"en" or "en-us" => "en-us",
|
||||||
"en-gb" => "en-gb",
|
"en-gb" => "en-gb",
|
||||||
_ => "pt-br" // Padrão
|
_ => "pt-br"
|
||||||
};
|
};
|
||||||
|
|
||||||
|
return baseLang + genderSuffix;
|
||||||
}
|
}
|
||||||
|
|
||||||
private string EscapeText(string text)
|
private string EscapeText(string text)
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user