All checks were successful
Deploy ASP.NET MVC to OCI / build-and-deploy (push) Successful in 22m1s
297 lines
12 KiB
Plaintext
297 lines
12 KiB
Plaintext
@{
|
|
ViewData["Title"] = "Texto para Áudio (Voz)";
|
|
Layout = "_Layout";
|
|
}
|
|
|
|
<div class="text-center mb-5">
|
|
<h1 class="display-4">@ViewData["Title"]</h1>
|
|
<p class="lead">Converta qualquer texto em fala usando vozes neurais de alta qualidade.</p>
|
|
</div>
|
|
|
|
<div class="row justify-content-center">
|
|
<div class="col-md-8">
|
|
<div class="card shadow-custom p-4">
|
|
<div class="mb-4">
|
|
<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>
|
|
</div>
|
|
|
|
<div class="row mb-4">
|
|
<div class="col-md-6">
|
|
<label for="voiceSelect" class="form-label">Escolher Voz</label>
|
|
<select id="voiceSelect" class="form-select"></select>
|
|
</div>
|
|
<div class="col-md-3">
|
|
<label for="rate" class="form-label">Velocidade</label>
|
|
<input type="range" class="form-range" min="0.5" max="2" step="0.1" id="rate" value="1">
|
|
</div>
|
|
<div class="col-md-3">
|
|
<label for="pitch" class="form-label">Tom</label>
|
|
<input type="range" class="form-range" min="0" max="2" step="0.1" id="pitch" value="1">
|
|
</div>
|
|
</div>
|
|
|
|
<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()">
|
|
<i class="bi bi-play-fill me-2"></i>Ouvir
|
|
</button>
|
|
<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
|
|
</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 class="mt-4 alert alert-info">
|
|
<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.
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
@section Scripts {
|
|
<script>
|
|
// Variáveis globais
|
|
const synth = window.speechSynthesis;
|
|
let voices = [];
|
|
|
|
// 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);
|
|
|
|
// 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';
|
|
}
|
|
|
|
// Função global para baixar/compartilhar áudio OGG
|
|
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>
|
|
}
|