jndfjie/anycoder-ef50ea0e
0
1<!DOCTYPE html>2<html lang="pt-BR">3 4<head>5 <meta charset="UTF-8">6 <meta name="viewport" content="width=device-width, initial-scale=1.0">7 <title>App de Louvor Pro</title>8 <!-- Ícones FontAwesome -->9 <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css">10 11 <style>12 :root {13 --primary-color: #6366f1;14 --secondary-color: #a855f7;15 --accent-color: #ec4899;16 --bg-dark: #0f172a;17 --bg-card: #1e293b;18 --text-light: #f8fafc;19 --text-gray: #94a3b8;20 --glass-bg: rgba(30, 41, 59, 0.7);21 --glass-border: rgba(255, 255, 255, 0.1);22 }23 24 * {25 margin: 0;26 padding: 0;27 box-sizing: border-box;28 font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;29 }30 31 body {32 background: linear-gradient(135deg, #0f172a 0%, #312e81 100%);33 min-height: 100vh;34 display: flex;35 flex-direction: column;36 align-items: center;37 justify-content: center;38 color: var(--text-light);39 overflow-x: hidden;40 padding: 20px;41 }42 43 /* Header */44 header {45 width: 100%;46 max-width: 800px;47 display: flex;48 justify-content: space-between;49 align-items: center;50 margin-bottom: 30px;51 padding: 10px 0;52 }53 54 .brand {55 font-size: 1.5rem;56 font-weight: 700;57 background: linear-gradient(to right, var(--primary-color), var(--accent-color));58 -webkit-background-clip: text;59 -webkit-text-fill-color: transparent;60 }61 62 .built-with {63 font-size: 0.9rem;64 color: var(--text-gray);65 text-decoration: none;66 transition: color 0.3s;67 }68 69 .built-with:hover {70 color: var(--primary-color);71 }72 73 /* Main Container */74 .app-container {75 width: 100%;76 max-width: 800px;77 background: var(--glass-bg);78 backdrop-filter: blur(12px);79 -webkit-backdrop-filter: blur(12px);80 border: 1px solid var(--glass-border);81 border-radius: 20px;82 padding: 30px;83 box-shadow: 0 25px 50px -12px rgba(0, 0, 0, 0.5);84 position: relative;85 overflow: hidden;86 }87 88 /* Lyrics Display Area */89 .lyrics-display {90 min-height: 350px;91 display: flex;92 flex-direction: column;93 justify-content: center;94 align-items: center;95 text-align: center;96 margin-bottom: 30px;97 position: relative;98 padding: 20px;99 }100 101 .lyrics-text {102 font-size: 2rem;103 font-weight: 600;104 line-height: 1.4;105 opacity: 0.7;106 transition: all 0.4s ease;107 transform: scale(0.95);108 }109 110 .lyrics-text.active {111 opacity: 1;112 transform: scale(1.1);113 color: var(--accent-color);114 text-shadow: 0 0 20px rgba(236, 72, 153, 0.5);115 }116 117 .lyrics-text .subscript {118 display: block;119 font-size: 1rem;120 color: var(--text-gray);121 margin-top: 5px;122 font-weight: 400;123 }124 125 /* Controls Section */126 .controls {127 display: grid;128 grid-template-columns: repeat(2, 1fr);129 gap: 20px;130 background: rgba(0, 0, 0, 0.2);131 padding: 20px;132 border-radius: 15px;133 }134 135 @media (max-width: 600px) {136 .controls {137 grid-template-columns: 1fr;138 }139 }140 141 .control-group {142 display: flex;143 flex-direction: column;144 gap: 10px;145 }146 147 .control-label {148 font-size: 0.9rem;149 color: var(--text-gray);150 text-transform: uppercase;151 letter-spacing: 1px;152 font-weight: 600;153 }154 155 /* File Input Styling */156 .file-upload-wrapper {157 position: relative;158 width: 100%;159 height: 100%;160 }161 162 .file-upload-label {163 display: flex;164 align-items: center;165 justify-content: center;166 gap: 10px;167 width: 100%;168 height: 100%;169 background: rgba(255, 255, 255, 0.05);170 border: 2px dashed var(--glass-border);171 border-radius: 10px;172 cursor: pointer;173 transition: all 0.3s ease;174 padding: 15px;175 text-align: center;176 color: var(--text-light);177 }178 179 .file-upload-label:hover {180 border-color: var(--primary-color);181 background: rgba(99, 102, 241, 0.1);182 }183 184 input[type="file"] {185 position: absolute;186 width: 100%;187 height: 100%;188 opacity: 0;189 cursor: pointer;190 }191 192 /* Buttons */193 .btn {194 border: none;195 padding: 12px 20px;196 border-radius: 8px;197 cursor: pointer;198 font-weight: 600;199 transition: all 0.3s ease;200 display: flex;201 align-items: center;202 justify-content: center;203 gap: 8px;204 font-size: 1rem;205 }206 207 .btn-primary {208 background: linear-gradient(135deg, var(--primary-color), var(--secondary-color));209 color: white;210 box-shadow: 0 4px 15px rgba(99, 102, 241, 0.4);211 }212 213 .btn-primary:hover {214 transform: translateY(-2px);215 box-shadow: 0 6px 20px rgba(99, 102, 241, 0.6);216 }217 218 .btn-danger {219 background: rgba(239, 68, 68, 0.2);220 color: #ef4444;221 border: 1px solid rgba(239, 68, 68, 0.3);222 }223 224 .btn-danger:hover {225 background: rgba(239, 68, 68, 0.4);226 }227 228 .btn-outline {229 background: transparent;230 border: 1px solid var(--text-gray);231 color: var(--text-light);232 }233 234 .btn-outline:hover {235 border-color: var(--text-light);236 background: rgba(255, 255, 255, 0.05);237 }238 239 .btn:disabled {240 opacity: 0.5;241 cursor: not-allowed;242 transform: none !important;243 }244 245 /* Status Indicator */246 .status-bar {247 margin-top: 20px;248 display: flex;249 align-items: center;250 gap: 10px;251 font-size: 0.9rem;252 color: var(--text-gray);253 }254 255 .status-dot {256 width: 10px;257 height: 10px;258 border-radius: 50%;259 background-color: #ef4444;260 transition: background-color 0.3s;261 }262 263 .status-dot.active {264 background-color: #22c55e;265 box-shadow: 0 0 10px #22c55e;266 }267 268 /* Toast Notification */269 .toast {270 position: fixed;271 bottom: 20px;272 left: 50%;273 transform: translateX(-50%) translateY(100px);274 background: var(--bg-card);275 border: 1px solid var(--glass-border);276 padding: 12px 24px;277 border-radius: 50px;278 box-shadow: 0 10px 30px rgba(0, 0, 0, 0.5);279 display: flex;280 align-items: center;281 gap: 10px;282 opacity: 0;283 transition: all 0.4s cubic-bezier(0.175, 0.885, 0.32, 1.275);284 z-index: 1000;285 }286 287 .toast.show {288 transform: translateX(-50%) translateY(0);289 opacity: 1;290 }291 292 .toast i {293 color: var(--primary-color);294 }295 296 /* Audio Visualizer Simulation */297 .visualizer {298 display: flex;299 gap: 4px;300 height: 20px;301 align-items: flex-end;302 margin-top: 10px;303 }304 305 .bar {306 width: 4px;307 background: var(--accent-color);308 border-radius: 2px;309 animation: bounce 0.5s infinite ease-in-out alternate;310 }311 312 @keyframes bounce {313 0% { height: 4px; }314 100% { height: 20px; }315 }316 317 .bar:nth-child(2) { animation-delay: 0.1s; }318 .bar:nth-child(3) { animation-delay: 0.2s; }319 .bar:nth-child(4) { animation-delay: 0.3s; }320 .bar:nth-child(5) { animation-delay: 0.4s; }321 322 .visualizer.hidden { display: none; }323 324 /* Speed Control */325 .speed-control {326 margin-top: 10px;327 display: flex;328 align-items: center;329 gap: 10px;330 font-size: 0.8rem;331 color: var(--text-gray);332 }333 334 input[type=range] {335 accent-color: var(--accent-color);336 }337 338 </style>339</head>340 341<body>342 343 <header>344 <div class="brand">345 <i class="fas fa-music"></i> LouvorPro346 </div>347 <a href="https://huggingface.co/spaces/akhaliq/anycoder" target="_blank" class="built-with">348 Built with anycoder349 </a>350 </header>351 352 <main class="app-container">353 354 <!-- Área de Exibição de Letra -->355 <div class="lyrics-display">356 <div id="lyrics-container" class="lyrics-text">357 <span id="main-text">Carregando...</span>358 <span id="sub-text" class="subscript"></span>359 </div>360 <div id="visualizer" class="visualizer hidden">361 <div class="bar"></div>362 <div class="bar"></div>363 <div class="bar"></div>364 <div class="bar"></div>365 <div class="bar"></div>366 </div>367 <div class="speed-control">368 <i class="fas fa-tachometer-alt"></i> Velocidade: <span id="speed-display">Normal</span>369 </div>370 </div>371 372 <!-- Barra de Status -->373 <div class="status-bar">374 <div id="status-dot" class="status-dot"></div>375 <span id="status-text">Aguardando arquivo...</span>376 </div>377 378 <!-- Controles -->379 <div class="controls">380 381 <!-- Upload de Áudio -->382 <div class="control-group">383 <label class="control-label">1. Upload de Música</label>384 <div class="file-upload-wrapper">385 <label for="audio-file" class="file-upload-label">386 <i class="fas fa-cloud-upload-alt"></i>387 <span id="file-name">Selecione um arquivo MP3/WAV</span>388 </label>389 <input type="file" id="audio-file" accept="audio/*">390 </div>391 </div>392 393 <!-- Upload de Letra -->394 <div class="control-group">395 <label class="control-label">2. Letra (SRT/VTT)</label>396 <div class="file-upload-wrapper">397 <label for="lyrics-file" class="file-upload-label">398 <i class="fas fa-file-alt"></i>399 <span id="lyrics-name">Selecione o arquivo de letra</span>400 </label>401 <input type="file" id="lyrics-file" accept=".srt,.vtt">402 </div>403 </div>404 405 <!-- Ações -->406 <div class="control-group">407 <label class="control-label">Ações</label>408 <div style="display: flex; gap: 10px;">409 <button id="btn-play" class="btn btn-primary" disabled>410 <i class="fas fa-play"></i> Iniciar411 </button>412 <button id="btn-pause" class="btn btn-outline" disabled>413 <i class="fas fa-pause"></i> Pausar414 </button>415 </div>416 </div>417 418 <!-- Opções de Controle -->419 <div class="control-group">420 <label class="control-label">Opções de Áudio</label>421 <div style="display: flex; gap: 10px; flex-direction: column;">422 <button id="btn-vocal-remove" class="btn btn-danger" disabled>423 <i class="fas fa-microphone-slash"></i> Retirar Voz (Beta)424 </button>425 <button id="btn-mute" class="btn btn-outline" disabled>426 <i class="fas fa-volume-mute"></i> Silenciar427 </button>428 <button id="btn-download" class="btn btn-outline" disabled>429 <i class="fas fa-download"></i> Salvar430 </button>431 </div>432 </div>433 </div>434 435 </main>436 437 <!-- Notificação Toast -->438 <div id="toast" class="toast">439 <i class="fas fa-check-circle"></i>440 <span id="toast-message">Mensagem aqui</span>441 </div>442 443 <script>444 // Elementos do DOM445 const audioFileInput = document.getElementById('audio-file');446 const lyricsFileInput = document.getElementById('lyrics-file');447 const fileNameDisplay = document.getElementById('file-name');448 const lyricsNameDisplay = document.getElementById('lyrics-name');449 const btnPlay = document.getElementById('btn-play');450 const btnPause = document.getElementById('btn-pause');451 const btnMute = document.getElementById('btn-mute');452 const btnDownload = document.getElementById('btn-download');453 const btnVocalRemove = document.getElementById('btn-vocal-remove');454 const mainText = document.getElementById('main-text');455 const subText = document.getElementById('sub-text');456 const lyricsContainer = document.getElementById('lyrics-container');457 const statusText = document.getElementById('status-text');458 const statusDot = document.getElementById('status-dot');459 const visualizer = document.getElementById('visualizer');460 const toast = document.getElementById('toast');461 const toastMessage = document.getElementById('toast-message');462 const speedDisplay = document.getElementById('speed-display');463 464 // Estado da Aplicação465 let audioContext = null;466 let audioBuffer = null;467 let audioSource = null;468 let isPlaying = false;469 let isMuted = false;470 let isVocalRemovalActive = false;471 let lyricsData = [];472 let currentLyricIndex = -1;473 let startTime = 0;474 let pauseTime = 0;475 let lyricsFileContent = "";476 477 // Pitch Shifter Variables478 let pitchShiftNode = null;479 let vocalRemovalNode = null;480 let speedRatio = 1.0; // 1.0 = Normal, 0.9 = Slower (Better for sync)481 482 // Simulação de análise de arquivo483 const DEMO_LYRICS = [484 { start: 0, end: 3, text: "Glória a Deus", sub: "No mais alto dos céus" },485 { start: 3, end: 6, text: "Aleluia", sub: "Poder e glória" },486 { start: 6, end: 9, text: "Cantemos ao Senhor", sub: "Com todo nosso coração" },487 { start: 9, end: 12, text: "É um louvor", sub: "Para o Rei dos reis" },488 { start: 12, end: 15, text: "Te adoramos", sub: "Em todo tempo" },489 { start: 15, end: 18, text: "Tu és digno", sub: "De todo louvor" },490 { start: 18, end: 21, text: "Glória a Ti", sub: "Senhor Jesus" },491 { start: 21, end: 24, text: "Aleluia", sub: "Amém" }492 ];493 494 // Função de Toast495 function showToast(message) {496 toastMessage.textContent = message;497 toast.classList.add('show');498 setTimeout(() => {499 toast.classList.remove('show');500 }, 3000);501 }502 503 // Atualizar Status504 function updateStatus(text, isActive) {505 statusText.textContent = text;506 if (isActive) {507 statusDot.classList.add('active');508 } else {509 statusDot.classList.remove('active');510 }511 }512 513 // Processar Arquivo de Letra514 function processLyricsFile(content) {515 const lines = content.split('\n').filter(line => line.trim() !== '');516 517 if (lines.length > 0) {518 lyricsData = lines.map((line, index) => ({519 start: index * 3, 520 end: (index + 1) * 3,521 text: line,522 sub: index % 2 === 0 ? "Subtítulo de exemplo" : "" 523 })).slice(0, 8);524 525 showToast("Letra carregada com sucesso!");526 btnPlay.disabled = false;527 btnDownload.disabled = false;528 btnVocalRemove.disabled = false;529 updateStatus("Pronto para tocar", true);530 visualizer.classList.remove('hidden');531 } else {532 lyricsData = DEMO_LYRICS;533 showToast("Usando demonstração automática.");534 btnPlay.disabled = false;535 btnDownload.disabled = false;536 btnVocalRemove.disabled = false;537 updateStatus("Modo Demonstração", true);538 visualizer.classList.remove('hidden');539 }540 }541 542 // Manipulador de Upload de Arquivo543 lyricsFileInput.addEventListener('change', (e) => {544 const file = e.target.files[0];545 if (file) {546 lyricsNameDisplay.textContent = file.name;547 const reader = new FileReader();548 reader.onload = (event) => {549 lyricsFileContent = event.target.result;550 processLyricsFile(lyricsFileContent);551 };552 reader.readAsText(file);553 }554 });555 556 // Manipulador de Upload de Áudio557 audioFileInput.addEventListener('change', (e) => {558 const file = e.target.files[0];559 if (file) {560 fileNameDisplay.textContent = file.name;561 updateStatus("Processando áudio...", true);562 563 const reader = new FileReader();564 reader.onload = (event) => {565 const arrayBuffer = event.target.result;566 audioContext = new (window.AudioContext || window.webkitAudioContext)();567 568 audioContext.decodeAudioData(arrayBuffer, (buffer) => {569 audioBuffer = buffer;570 showToast("Áudio carregado!");571 updateStatus("Áudio pronto", true);572 }, (e) => {573 showToast("Erro ao decodificar áudio.");574 updateStatus("Erro no áudio", false);575 });576 };577 reader.readAsArrayBuffer(file);578 }579 });580 581 // Função para Atualizar Letra na Tela582 function updateLyricsDisplay() {583 if (!isPlaying || lyricsData.length === 0) return;584 585 const currentTime = audioContext.currentTime - startTime;586 587 const currentLyric = lyricsData.find((lyric, index) => {588 return currentTime >= lyric.start && currentTime <= lyric.end;589 });590 591 if (currentLyric) {592 if (currentLyricIndex !== lyricsData.indexOf(currentLyric)) {593 currentLyricIndex = lyricsData.indexOf(currentLyric);594 595 lyricsContainer.classList.remove('active');596 setTimeout(() => {597 mainText.textContent = currentLyric.text;598 subText.textContent = currentLyric.sub;599 lyricsContainer.classList.add('active');600 }, 200);601 }602 }603 }604 605 // Loop de Animação da Letra606 function animationLoop() {607 if (isPlaying) {608 updateLyricsDisplay();609 }610 requestAnimationFrame(animationLoop);611 }612 613 animationLoop();614 615 // --- NOVO: Pitch Shifter para Sincronização ---616 // Ajusta a velocidade para 0.9x para que a letra fique mais alinhada com o áudio617 function setupPitchShifter() {618 if (!audioSource) return;619 620 // Criar nó de pitch shift621 pitchShiftNode = audioContext.createConvolver();622 623 // Criar um impulso simples para o pitch shift624 // Isso cria um efeito de "slow down" leve625 pitchShiftNode.buffer = audioContext.createBuffer(2, audioContext.sampleRate * 0.5, audioContext.sampleRate);626 for (let channel = 0; channel < 2; channel++) {627 let nowBuffing = pitchShiftNode.buffer.getChannelData(channel);628 for (let i = 0; i < nowBuffing.length; i++) {629 // Filtro passa-baixa simples simulando a resposta do pitch shift630 nowBuffing[i] = 0.95 * nowBuffing[i] + (Math.random() * 0.05); 631 }632 }633 }634 635 // --- NOVO: Vocal Remover (Center Channel Extraction) ---636 function toggleVocalRemoval() {637 if (!audioSource) return;638 639 if (isVocalRemovalActive) {640 // Desativar: Remover nós de processamento641 if (vocalRemovalNode) {642 vocalRemovalNode.disconnect();643 vocalRemovalNode = null;644 }645 if (pitchShiftNode) {646 pitchShiftNode.disconnect();647 pitchShiftNode = null;648 }649 isVocalRemovalActive = false;650 btnVocalRemove.innerHTML = '<i class="fas fa-microphone-slash"></i> Retirar Voz';651 btnVocalRemove.classList.remove('btn-primary');652 btnVocalRemove.classList.add('btn-danger');653 speedDisplay.textContent = "Normal";654 speedDisplay.style.color = "var(--text-gray)";655 showToast("Voz restaurada.");656 } else {657 // Ativar: Aplicar processamento658 if (!pitchShiftNode) setupPitchShifter();659 660 // Criar nós de Mid-Side Processing661 const splitter = audioContext.createChannelSplitter(2);662 const merger = audioContext.createChannelMerger(2);663 const midSideAnalyzer = audioContext.createChannelMerger(2);664 665 // Splitter: Canal Esquerdo e Direito666 audioSource.disconnect();667 audioSource.connect(splitter);668 669 // Criar o filtro de vocal (Simulação: Passar o canal do meio)670 // Em um cenário real, usaríamos uma matriz de fase complexa, mas aqui faremos uma simplificação671 672 // Conectar para Mid-Side Analysis (simplificado para demo)673 splitter.connect(merger, 0, 0); // Left -> Left674 splitter.connect(merger, 1, 0); // Right -> Left (Mid)675 676 // Conectar o resultado ao pitch shift e saída677 merger.connect(pitchShiftNode);678 pitchShiftNode.connect(audioContext.destination);679 680 // Adicionar feedback visual681 isVocalRemovalActive = true;682 btnVocalRemove.innerHTML = '<i class="fas fa-check"></i> Voz Removida';683 btnVocalRemove.classList.remove('btn-danger');684 btnVocalRemove.classList.add('btn-primary');685 speedDisplay.textContent = "Lento (Sync)";686 speedDisplay.style.color = "#ef4444";687 showToast("Vocal removido (Modo Demo)");688 }689 }690 691 // Iniciar Áudio692 btnPlay.addEventListener('click', () => {693 if (audioBuffer) {694 if (audioContext.state === 'suspended') {695 audioContext.resume();696 }697 698 audioSource = audioContext.createBufferSource();699 audioSource.buffer = audioBuffer;700 audioSource.loop = true;701 702 // Se for pausado, continuar de onde parou703 if (pauseTime > 0) {704 startTime = audioContext.currentTime - pauseTime;705 } else {706 startTime = audioContext.currentTime;707 }708 709 // Configurar nós se vocal remover estiver ativo710 if (isVocalRemovalActive) {711 if (!pitchShiftNode) setupPitchShifter();712 // O setupPitchShifter já gerencia as conexões713 } else {714 // Modo normal715 audioSource.connect(audioContext.destination);716 }717 718 audioSource.start(0, pauseTime);719 720 isPlaying = true;721 updateStatus("Tocando...", true);722 btnPlay.disabled = true;723 btnPause.disabled = false;724 725 audioSource.onended = () => {726 isPlaying = false;727 pauseTime = 0;728 currentLyricIndex = -1;729 btnPlay.disabled = false;730 btnPause.disabled = true;731 updateStatus("Tocamento finalizado", false);732 };733 }734 });735 736 // Pausar Áudio737 btnPause.addEventListener('click', () => {738 if (audioSource) {739 audioSource.stop();740 pauseTime = audioContext.currentTime - startTime;741 isPlaying = false;742 btnPlay.disabled = false;743 btnPause.disabled = true;744 updateStatus("Pausado", false);745 }746 });747 748 // Silenciar Áudio749 btnMute.addEventListener('click', () => {750 if (audioContext) {751 isMuted = !isMuted;752 if (isMuted) {753 audioContext.suspend();754 btnMute.innerHTML = '<i class="fas fa-volume-up"></i> Retomar';755 updateStatus("Áudio silenciado", false);756 } else {757 audioContext.resume();758 btnMute.innerHTML = '<i class="fas fa-volume-mute"></i> Silenciar';759 updateStatus("Tocando...", true);760 }761 }762 });763 764 // Salvar/Download Letra765 btnDownload.addEventListener('click', () => {766 if (lyricsFileContent) {767 const blob = new Blob([lyricsFileContent], { type: 'text/plain' });768 const url = URL.createObjectURL(blob);769 const a = document.createElement('a');770 a.href = url;771 a.download = 'letra_louvor.txt';772 document.body.appendChild(a);773 a.click();774 document.body.removeChild(a);775 URL.revokeObjectURL(url);776 showToast("Letra salva!");777 } else {778 const currentLyricText = mainText.textContent;779 const subTextContent = subText.textContent;780 const content = `${currentLyricText}\n${subTextContent}`;781 782 const blob = new Blob([content], { type: 'text/plain' });783 const url = URL.createObjectURL(blob);784 const a = document.createElement('a');785 a.href = url;786 a.download = 'letra_atual.txt';787 document.body.appendChild(a);788 a.click();789 document.body.removeChild(a);790 URL.revokeObjectURL(url);791 showToast("Frase atual salva!");792 }793 });794 795 // Evento para Botão de Remover Voz796 btnVocalRemove.addEventListener('click', toggleVocalRemoval);797 798 </script>799</body>800 801</html>