Spaces:
Sleeping
Sleeping
Update templates/index.html
Browse files- templates/index.html +166 -5
templates/index.html
CHANGED
|
@@ -701,8 +701,39 @@
|
|
| 701 |
}
|
| 702 |
|
| 703 |
// --- FUNÇÕES DE ÁUDIO ---
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 704 |
async function fetchAudioBlob(text) {
|
| 705 |
const selectedTld = voiceSelector.value;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 706 |
const response = await fetch('/tts-proxy', {
|
| 707 |
method: 'POST',
|
| 708 |
headers: { 'Content-Type': 'application/json' },
|
|
@@ -712,7 +743,22 @@
|
|
| 712 |
const err = await response.json();
|
| 713 |
throw new Error(err.error || `HTTP error! status: ${response.status}`);
|
| 714 |
}
|
| 715 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 716 |
}
|
| 717 |
|
| 718 |
async function playAudio(text, event) {
|
|
@@ -742,14 +788,101 @@
|
|
| 742 |
const charCount = document.getElementById('charCount');
|
| 743 |
const playText = document.getElementById('playText');
|
| 744 |
const playSpinner = document.getElementById('playSpinner');
|
|
|
|
|
|
|
|
|
|
| 745 |
textEditor.addEventListener('input', () => {
|
| 746 |
const len = textEditor.innerText.length;
|
|
|
|
|
|
|
|
|
|
| 747 |
charCount.textContent = `${len}/10000`;
|
| 748 |
playButton.disabled = len === 0;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 749 |
});
|
|
|
|
|
|
|
| 750 |
textEditor.addEventListener('mouseup', () => {
|
| 751 |
createCardButton.disabled = !window.getSelection().toString().trim();
|
|
|
|
| 752 |
});
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 753 |
async function handlePlay() {
|
| 754 |
const text = textEditor.innerText.trim();
|
| 755 |
if (!text) {
|
|
@@ -757,27 +890,55 @@
|
|
| 757 |
return;
|
| 758 |
}
|
| 759 |
|
| 760 |
-
if (text.length >
|
| 761 |
showToast('Text is too long for audio generation. Please use shorter text.', 'warning');
|
| 762 |
return;
|
| 763 |
}
|
| 764 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 765 |
setButtonLoading(playButton, true);
|
| 766 |
playText.textContent = 'Generating...';
|
| 767 |
playSpinner.classList.remove('hidden');
|
| 768 |
|
| 769 |
try {
|
| 770 |
const audioBlob = await fetchAudioBlob(text);
|
| 771 |
-
|
| 772 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 773 |
audioPlayer.classList.remove('hidden');
|
| 774 |
|
|
|
|
| 775 |
audioPlayer.onplay = () => showToast('Audio ready! Playing...', 'success', 1000);
|
| 776 |
-
audioPlayer.onended = () =>
|
|
|
|
|
|
|
|
|
|
| 777 |
audioPlayer.onerror = () => showToast('Failed to play audio', 'error');
|
| 778 |
|
| 779 |
await audioPlayer.play();
|
|
|
|
| 780 |
} catch (error) {
|
|
|
|
| 781 |
showToast('Failed to generate audio. Please check your connection and try again.', 'error');
|
| 782 |
} finally {
|
| 783 |
setButtonLoading(playButton, false);
|
|
|
|
| 701 |
}
|
| 702 |
|
| 703 |
// --- FUNÇÕES DE ÁUDIO ---
|
| 704 |
+
|
| 705 |
+
// Cache para armazenar áudios gerados
|
| 706 |
+
const audioCache = new Map();
|
| 707 |
+
|
| 708 |
+
// Função para gerar uma chave de cache baseada no texto e voice
|
| 709 |
+
function getAudioCacheKey(text, tld) {
|
| 710 |
+
return `${text.substring(0, 100)}_${tld}_${text.length}`;
|
| 711 |
+
}
|
| 712 |
+
|
| 713 |
+
// Função para limpar cache de áudio (útil quando muda o texto significativamente)
|
| 714 |
+
function clearAudioCache() {
|
| 715 |
+
audioCache.forEach(blob => {
|
| 716 |
+
if (blob && blob.url) {
|
| 717 |
+
URL.revokeObjectURL(blob.url);
|
| 718 |
+
}
|
| 719 |
+
});
|
| 720 |
+
audioCache.clear();
|
| 721 |
+
if (currentAudioUrl) {
|
| 722 |
+
URL.revokeObjectURL(currentAudioUrl);
|
| 723 |
+
currentAudioUrl = null;
|
| 724 |
+
}
|
| 725 |
+
console.log('Audio cache cleared');
|
| 726 |
+
}
|
| 727 |
+
|
| 728 |
async function fetchAudioBlob(text) {
|
| 729 |
const selectedTld = voiceSelector.value;
|
| 730 |
+
const cacheKey = getAudioCacheKey(text, selectedTld);
|
| 731 |
+
|
| 732 |
+
// Verificar se já temos esse áudio no cache
|
| 733 |
+
if (audioCache.has(cacheKey)) {
|
| 734 |
+
return audioCache.get(cacheKey);
|
| 735 |
+
}
|
| 736 |
+
|
| 737 |
const response = await fetch('/tts-proxy', {
|
| 738 |
method: 'POST',
|
| 739 |
headers: { 'Content-Type': 'application/json' },
|
|
|
|
| 743 |
const err = await response.json();
|
| 744 |
throw new Error(err.error || `HTTP error! status: ${response.status}`);
|
| 745 |
}
|
| 746 |
+
|
| 747 |
+
const blob = await response.blob();
|
| 748 |
+
|
| 749 |
+
// Armazenar no cache (limitar a 10 áudios para não usar muita memória)
|
| 750 |
+
if (audioCache.size >= 10) {
|
| 751 |
+
const firstKey = audioCache.keys().next().value;
|
| 752 |
+
// Revogar URL do áudio mais antigo
|
| 753 |
+
const oldBlob = audioCache.get(firstKey);
|
| 754 |
+
if (oldBlob && oldBlob.url) {
|
| 755 |
+
URL.revokeObjectURL(oldBlob.url);
|
| 756 |
+
}
|
| 757 |
+
audioCache.delete(firstKey);
|
| 758 |
+
}
|
| 759 |
+
|
| 760 |
+
audioCache.set(cacheKey, blob);
|
| 761 |
+
return blob;
|
| 762 |
}
|
| 763 |
|
| 764 |
async function playAudio(text, event) {
|
|
|
|
| 788 |
const charCount = document.getElementById('charCount');
|
| 789 |
const playText = document.getElementById('playText');
|
| 790 |
const playSpinner = document.getElementById('playSpinner');
|
| 791 |
+
|
| 792 |
+
let lastTextForCache = '';
|
| 793 |
+
|
| 794 |
textEditor.addEventListener('input', () => {
|
| 795 |
const len = textEditor.innerText.length;
|
| 796 |
+
const currentText = textEditor.innerText.trim();
|
| 797 |
+
|
| 798 |
+
// Atualizar contador
|
| 799 |
charCount.textContent = `${len}/10000`;
|
| 800 |
playButton.disabled = len === 0;
|
| 801 |
+
|
| 802 |
+
// Limpar cache se o texto mudou significativamente (mais de 50 caracteres de diferença)
|
| 803 |
+
if (Math.abs(currentText.length - lastTextForCache.length) > 50 ||
|
| 804 |
+
(currentText.length > 100 && currentText.substring(0, 100) !== lastTextForCache.substring(0, 100))) {
|
| 805 |
+
if (currentAudioUrl) {
|
| 806 |
+
audioPlayer.src = '';
|
| 807 |
+
audioPlayer.classList.add('hidden');
|
| 808 |
+
}
|
| 809 |
+
lastTextForCache = currentText;
|
| 810 |
+
}
|
| 811 |
+
|
| 812 |
+
// Mudança de cor baseada no limite
|
| 813 |
+
if (len > 10000) {
|
| 814 |
+
charCount.classList.add('text-red-500');
|
| 815 |
+
charCount.classList.remove('text-gray-500');
|
| 816 |
+
} else if (len > 8000) {
|
| 817 |
+
charCount.classList.remove('text-red-500', 'text-gray-500');
|
| 818 |
+
charCount.classList.add('text-yellow-500');
|
| 819 |
+
} else {
|
| 820 |
+
charCount.classList.remove('text-red-500', 'text-yellow-500');
|
| 821 |
+
charCount.classList.add('text-gray-500');
|
| 822 |
+
}
|
| 823 |
});
|
| 824 |
+
|
| 825 |
+
// Controle de seleção de texto para flashcards e áudio
|
| 826 |
textEditor.addEventListener('mouseup', () => {
|
| 827 |
createCardButton.disabled = !window.getSelection().toString().trim();
|
| 828 |
+
handleTextSelection();
|
| 829 |
});
|
| 830 |
+
|
| 831 |
+
textEditor.addEventListener('keyup', () => {
|
| 832 |
+
handleTextSelection();
|
| 833 |
+
});
|
| 834 |
+
|
| 835 |
+
function handleTextSelection() {
|
| 836 |
+
const selectedText = window.getSelection().toString().trim();
|
| 837 |
+
const playSelectionBtn = document.getElementById('playSelectionBtn');
|
| 838 |
+
|
| 839 |
+
if (selectedText && selectedText.length > 0) {
|
| 840 |
+
if (!playSelectionBtn) {
|
| 841 |
+
// Criar botão de tocar seleção se não existir
|
| 842 |
+
const selectionBtn = document.createElement('button');
|
| 843 |
+
selectionBtn.id = 'playSelectionBtn';
|
| 844 |
+
selectionBtn.className = 'btn bg-blue-500 hover:bg-blue-600 text-white ml-2';
|
| 845 |
+
selectionBtn.innerHTML = '🎵 Play Selection';
|
| 846 |
+
selectionBtn.onclick = () => playSelectedText();
|
| 847 |
+
|
| 848 |
+
// Inserir próximo ao botão Play principal
|
| 849 |
+
playButton.parentNode.insertBefore(selectionBtn, playButton.nextSibling);
|
| 850 |
+
}
|
| 851 |
+
playSelectionBtn.style.display = 'inline-block';
|
| 852 |
+
playSelectionBtn.disabled = false;
|
| 853 |
+
} else if (playSelectionBtn) {
|
| 854 |
+
playSelectionBtn.style.display = 'none';
|
| 855 |
+
}
|
| 856 |
+
}
|
| 857 |
+
|
| 858 |
+
async function playSelectedText() {
|
| 859 |
+
const selectedText = window.getSelection().toString().trim();
|
| 860 |
+
if (!selectedText) {
|
| 861 |
+
showToast('Please select some text first', 'warning');
|
| 862 |
+
return;
|
| 863 |
+
}
|
| 864 |
+
|
| 865 |
+
if (selectedText.length > 1000) {
|
| 866 |
+
showToast('Selected text is too long. Please select less than 1000 characters.', 'warning');
|
| 867 |
+
return;
|
| 868 |
+
}
|
| 869 |
+
|
| 870 |
+
const selectionBtn = document.getElementById('playSelectionBtn');
|
| 871 |
+
setButtonLoading(selectionBtn, true, '🎵 Play Selection');
|
| 872 |
+
|
| 873 |
+
try {
|
| 874 |
+
await playAudio(selectedText);
|
| 875 |
+
showToast(`Playing selected text (${selectedText.length} chars)`, 'success', 2000);
|
| 876 |
+
} catch (error) {
|
| 877 |
+
showToast('Failed to play selected text', 'error');
|
| 878 |
+
} finally {
|
| 879 |
+
setButtonLoading(selectionBtn, false, '🎵 Play Selection');
|
| 880 |
+
}
|
| 881 |
+
}
|
| 882 |
+
|
| 883 |
+
// Variável para armazenar o URL atual do áudio
|
| 884 |
+
let currentAudioUrl = null;
|
| 885 |
+
|
| 886 |
async function handlePlay() {
|
| 887 |
const text = textEditor.innerText.trim();
|
| 888 |
if (!text) {
|
|
|
|
| 890 |
return;
|
| 891 |
}
|
| 892 |
|
| 893 |
+
if (text.length > 10000) { // Aumentado de 5000 para 10000
|
| 894 |
showToast('Text is too long for audio generation. Please use shorter text.', 'warning');
|
| 895 |
return;
|
| 896 |
}
|
| 897 |
|
| 898 |
+
const selectedTld = voiceSelector.value;
|
| 899 |
+
const cacheKey = getAudioCacheKey(text, selectedTld);
|
| 900 |
+
|
| 901 |
+
// Se já temos um áudio carregado para este texto, apenas reproduzir
|
| 902 |
+
if (currentAudioUrl && audioPlayer.src && getAudioCacheKey(audioPlayer.dataset.lastText || '', audioPlayer.dataset.lastTld || '') === cacheKey) {
|
| 903 |
+
try {
|
| 904 |
+
await audioPlayer.play();
|
| 905 |
+
showToast('Playing cached audio...', 'success', 1000);
|
| 906 |
+
return;
|
| 907 |
+
} catch (error) {
|
| 908 |
+
console.log('Cached audio failed, regenerating...');
|
| 909 |
+
}
|
| 910 |
+
}
|
| 911 |
+
|
| 912 |
setButtonLoading(playButton, true);
|
| 913 |
playText.textContent = 'Generating...';
|
| 914 |
playSpinner.classList.remove('hidden');
|
| 915 |
|
| 916 |
try {
|
| 917 |
const audioBlob = await fetchAudioBlob(text);
|
| 918 |
+
|
| 919 |
+
// Limpar URL anterior se existir
|
| 920 |
+
if (currentAudioUrl) {
|
| 921 |
+
URL.revokeObjectURL(currentAudioUrl);
|
| 922 |
+
}
|
| 923 |
+
|
| 924 |
+
currentAudioUrl = URL.createObjectURL(audioBlob);
|
| 925 |
+
audioPlayer.src = currentAudioUrl;
|
| 926 |
+
audioPlayer.dataset.lastText = text;
|
| 927 |
+
audioPlayer.dataset.lastTld = selectedTld;
|
| 928 |
audioPlayer.classList.remove('hidden');
|
| 929 |
|
| 930 |
+
// Remover listener anterior para evitar duplicatas
|
| 931 |
audioPlayer.onplay = () => showToast('Audio ready! Playing...', 'success', 1000);
|
| 932 |
+
audioPlayer.onended = () => {
|
| 933 |
+
// NÃO revogar o URL aqui, manter para permitir replay
|
| 934 |
+
showToast('Audio finished', 'info', 1000);
|
| 935 |
+
};
|
| 936 |
audioPlayer.onerror = () => showToast('Failed to play audio', 'error');
|
| 937 |
|
| 938 |
await audioPlayer.play();
|
| 939 |
+
showToast(`Audio generated successfully! (${text.length} characters)`, 'success', 2000);
|
| 940 |
} catch (error) {
|
| 941 |
+
console.error('Audio generation error:', error);
|
| 942 |
showToast('Failed to generate audio. Please check your connection and try again.', 'error');
|
| 943 |
} finally {
|
| 944 |
setButtonLoading(playButton, false);
|