1. Adicione uma opção de escolher em tempo real um...
Criado em: 18 de maio de 2025
Criado em: 18 de maio de 2025
|
que é onde o audio está« Voltar 2 s (←)
está bloqueado, não funciona.html<!DOCTYPE html> <html lang="pt-BR"> <head> <meta charset="utf-8"> <title>Ajustar Legenda Pro v2</title> <style> body{font-family:Arial,Helvetica,sans-serif;margin:20px;} #controls{margin-bottom:1rem;} button{margin-right:.5rem;padding:.4rem .8rem;font-size:14px;} #wordList{display:flex;flex-wrap:wrap;gap:4px;} .word{padding:2px 4px;border-radius:4px;cursor:pointer;border:1px solid transparent;} .word.pending{background:#eee;} .word.current{border:1px solid #000;} .word.recorded{background:#4caf50;color:#fff;} #output{width:100%;height:120px;margin-top:1rem;} </style> </head> <body> <h1>Ajustar Legenda Pro v2</h1> <div id="controls"> <label>SRT: <input type="file" id="srtFile" accept=".srt,.txt"></label> <label>Áudio: <input type="file" id="audioFile" accept="audio/*"></label> <button id="startBtn" disabled>INICIAR</button> <button id="undoBtn" disabled>Desfazer (Z)</button> <button id="backBtn" disabled>« Voltar 2 s (←)</button> <span id="progress"></span> </div> <div id="wordList"></div> <textarea id="output" placeholder="Linhas geradas..." readonly></textarea> <button id="copyBtn" disabled>Copiar</button> <button id="downloadBtn" disabled>Baixar .srt</button> <audio id="player"></audio> <script> // --- utilidades --- function parseSrt(text){ const lines=text.split(/\r?\n/).filter(Boolean); return lines.map(l=>{ const [time,word]=l.split('#'); const [m,s,ds]=time.split(':').map(Number); return {originalTime:m*60+s+ds/10,word}; }); } function formatTime(sec){ const m=Math.floor(sec/60); const s=Math.floor(sec%60); const ds=Math.round((sec-m*60-s)*10); return `${m}:${s.toString().padStart(2,'0')}:${ds}`; } const srtInput=document.getElementById('srtFile'); const audioInput=document.getElementById('audioFile'); const startBtn=document.getElementById('startBtn'); const undoBtn=document.getElementById('undoBtn'); const backBtn=document.getElementById('backBtn'); const copyBtn=document.getElementById('copyBtn'); const downloadBtn=document.getElementById('downloadBtn'); const player=document.getElementById('player'); const wordListEl=document.getElementById('wordList'); const output=document.getElementById('output'); const progress=document.getElementById('progress'); let words=[]; let currentIdx=0; // --- carregamento de arquivos --- srtInput.addEventListener('change',readSrt); audioInput.addEventListener('change',handleAudio); function readSrt(){ if(!srtInput.files.length) return; const reader=new FileReader(); reader.onload=e=>{ words=parseSrt(e.target.result); renderList(); updateProgress(); if(audioInput.files.length) startBtn.disabled=false; }; reader.readAsText(srtInput.files[0],'utf-8'); } function handleAudio(){ if(!audioInput.files.length) return; player.src=URL.createObjectURL(audioInput.files[0]); if(words.length) startBtn.disabled=false; } // --- renderização --- function renderList(){ wordListEl.innerHTML=''; words.forEach((w,i)=>{ const span=document.createElement('span'); span.textContent=w.word; span.className='word pending'; span.dataset.index=i; span.addEventListener('click',()=>{ const t=w.recordedTime??w.originalTime; player.currentTime=t; player.play(); }); wordListEl.appendChild(span); }); } function markCurrentWord(){ wordListEl.querySelectorAll('.word').forEach(el=>el.classList.remove('current')); const current=wordListEl.querySelector(`.word[data-index="${currentIdx}"]`); if(current) current.classList.add('current'); } function updateProgress(){ const done=words.filter(w=>w.recordedTime!==undefined).length; progress.textContent=`${done} / ${words.length}`; undoBtn.disabled=done===0; copyBtn.disabled=downloadBtn.disabled=done===0; } // --- gravação e comandos --- startBtn.addEventListener('click',()=>{ currentIdx=0; player.currentTime=0; player.play(); markCurrentWord(); }); function exportSrt(){ return words.filter(w=>w.recordedTime!==undefined) .map(w=>`${formatTime(w.recordedTime)}#${w.word}`) .join('\n'); } function undoLast(){ const last=words.map(w=>w.recordedTime!==undefined).lastIndexOf(true); if(last<0) return; delete words[last].recordedTime; const span=wordListEl.querySelector(`.word[data-index="${last}"]`); span.classList.remove('recorded'); span.classList.add('pending'); currentIdx=last; markCurrentWord(); updateProgress(); output.value=exportSrt(); } document.addEventListener('keydown',e=>{ if(e.code==='Space'){ e.preventDefault(); const now=player.currentTime; if(currentIdx>=words.length) return; const item=words[currentIdx]; item.recordedTime=now; const span=wordListEl.querySelector(`.word[data-index="${currentIdx}"]`); span.classList.remove('pending'); span.classList.add('recorded'); currentIdx=Math.min(currentIdx+1,words.length); markCurrentWord(); updateProgress(); if(currentIdx===words.length){ player.pause(); output.value=exportSrt(); } }else if(e.key.toLowerCase()==='z'){ undoLast(); }else if(e.key==='ArrowLeft'){ player.currentTime=Math.max(0,player.currentTime-2); } }); undoBtn.addEventListener('click',undoLast); backBtn.addEventListener('click',()=>{player.currentTime=Math.max(0,player.currentTime-2);}); copyBtn.addEventListener('click',()=>{navigator.clipboard.writeText(output.value);}); downloadBtn.addEventListener('click',()=>{ const blob=new Blob([output.value],{type:'text/plain'}); const url=URL.createObjectURL(blob); const a=document.createElement('a'); a.href=url; a.download='ajustada.srt'; document.body.appendChild(a); a.click(); document.body.removeChild(a); URL.revokeObjectURL(url); }); player.addEventListener('ended',()=>{output.value=exportSrt();}); </script> </body> </html>
Segue o código completo já com as três melhorias solicitadas:
playbackRate
do áudio e mostra o valor atual.<canvas>
exibe a forma de onda do arquivo de áudio; um marcador vertical “|” acompanha a posição atual de reprodução em tempo real.html<!DOCTYPE html> <html lang="pt-BR"> <head> <meta charset="utf-8"> <title>Ajustar Legenda Pro v2</title> <style> body{font-family:Arial,Helvetica,sans-serif;margin:20px;} #controls{margin-bottom:1rem;display:flex;flex-wrap:wrap;gap:.5rem;align-items:center;} button{padding:.4rem .8rem;font-size:14px;cursor:pointer;} input[type="range"]{vertical-align:middle;} #wordList{display:flex;flex-wrap:wrap;gap:4px;} .word{padding:2px 4px;border-radius:4px;cursor:pointer;border:1px solid transparent;} .word.pending{background:#eee;} .word.current{border:1px solid #000;} .word.recorded{background:#4caf50;color:#fff;} #output{width:100%;height:120px;margin-top:1rem;} #waveform{width:100%;height:100px;border:1px solid #ccc;margin-top:1rem;} </style> </head> <body> <h1>Ajustar Legenda Pro v2</h1> <!-- CONTROLES --> <div id="controls"> <label>SRT: <input type="file" id="srtFile" accept=".srt,.txt"></label> <label>Áudio: <input type="file" id="audioFile" accept="audio/*"></label> <button id="startBtn" disabled>INICIAR</button> <button id="undoBtn" disabled>Desfazer (Z)</button> <button id="backBtn" disabled>« Voltar 2 s (←)</button> <span id="progress"></span> <!-- SLIDER DE VELOCIDADE --> <label style="margin-left:1rem;">Velocidade: <input type="range" id="speedSlider" min="0.5" max="2" value="1" step="0.1"> <span id="speedValue">1.0×</span> </label> </div> <!-- FORMA DE ONDA --> <canvas id="waveform"></canvas> <!-- LISTA DE PALAVRAS / SAÍDA --> <div id="wordList"></div> <textarea id="output" placeholder="Linhas geradas..." readonly></textarea> <button id="copyBtn" disabled>Copiar</button> <button id="downloadBtn" disabled>Baixar .srt</button> <audio id="player"></audio> <script> // ---------- utilidades ---------- function parseSrt(text){ const lines=text.split(/\r?\n/).filter(Boolean); return lines.map(l=>{ const [time,word]=l.split('#'); const [m,s,ds]=time.split(':').map(Number); return {originalTime:m*60+s+ds/10,word}; }); } function formatTime(sec){ const m=Math.floor(sec/60); const s=Math.floor(sec%60); const ds=Math.round((sec-m*60-s)*10); return `${m}:${s.toString().padStart(2,'0')}:${ds}`; } // ---------- elementos ---------- const srtInput = document.getElementById('srtFile'); const audioInput= document.getElementById('audioFile'); const startBtn = document.getElementById('startBtn'); const undoBtn = document.getElementById('undoBtn'); const backBtn = document.getElementById('backBtn'); const copyBtn = document.getElementById('copyBtn'); const downloadBtn= document.getElementById('downloadBtn'); const player = document.getElementById('player'); const wordListEl= document.getElementById('wordList'); const output = document.getElementById('output'); const progress = document.getElementById('progress'); const speedSlider= document.getElementById('speedSlider'); const speedValue = document.getElementById('speedValue'); const waveformCanvas = document.getElementById('waveform'); const wfCtx = waveformCanvas.getContext('2d'); let words=[],currentIdx=0; let audioBuffer=null; // para forma de onda let wfAnimationId=null; // p/ cancelar quando necessário // ---------- carregamento de arquivos ---------- srtInput.addEventListener('change',readSrt); audioInput.addEventListener('change',handleAudio); function readSrt(){ if(!srtInput.files.length) return; const reader=new FileReader(); reader.onload=e=>{ words=parseSrt(e.target.result); renderList(); updateProgress(); if(audioInput.files.length) startBtn.disabled=false; }; reader.readAsText(srtInput.files[0],'utf-8'); } function handleAudio(){ if(!audioInput.files.length) return; const file=audioInput.files[0]; player.src=URL.createObjectURL(file); backBtn.disabled=false; // agora habilita if(words.length) startBtn.disabled=false; // decodificar para desenhar forma de onda const reader=new FileReader(); reader.onload=async e=>{ const arrayBuf=e.target.result; try{ const audioCtx=new (window.AudioContext||window.webkitAudioContext)(); audioBuffer=await audioCtx.decodeAudioData(arrayBuf); drawWaveform(); // desenha uma vez }catch(err){console.error(err);} }; reader.readAsArrayBuffer(file); } // ---------- forma de onda ---------- function drawWaveform(){ if(!audioBuffer) return; // garantir o tamanho do canvas waveformCanvas.width=waveformCanvas.clientWidth; waveformCanvas.height=waveformCanvas.clientHeight; const {width,height}=waveformCanvas; wfCtx.clearRect(0,0,width,height); wfCtx.fillStyle='#ddd'; // usa dados do primeiro canal const data=audioBuffer.getChannelData(0); const samplesPerPixel=Math.floor(data.length/width); for(let x=0; x<width; x++){ // encontra pico para esse pixel let sum=0; for(let i=0;i<samplesPerPixel;i++){ const idx=x*samplesPerPixel+i; sum+=Math.abs(data[idx]); } const peak=sum/samplesPerPixel; const y=peak*height; wfCtx.fillRect(x,(height-y)/2,1,y); } } // marcador de posição “|” function drawPlayhead(){ const {width,height}=waveformCanvas; const ratio=player.currentTime/player.duration||0; const x=ratio*width; // limpa canvas e desenha waveform + playhead drawWaveform(); wfCtx.strokeStyle='#ff0000'; wfCtx.beginPath(); wfCtx.moveTo(x,0); wfCtx.lineTo(x,height); wfCtx.stroke(); } // atualiza playhead em cada frame enquanto toca function startPlayheadLoop(){ cancelAnimationFrame(wfAnimationId); const loop=()=>{ if(!player.paused) drawPlayhead(); wfAnimationId=requestAnimationFrame(loop); }; loop(); } player.addEventListener('play',startPlayheadLoop); player.addEventListener('pause',()=>cancelAnimationFrame(wfAnimationId)); player.addEventListener('seeked',drawPlayhead); player.addEventListener('loadedmetadata',drawWaveform); // ---------- renderização de palavras ---------- function renderList(){ wordListEl.innerHTML=''; words.forEach((w,i)=>{ const span=document.createElement('span'); span.textContent=w.word; span.className='word pending'; span.dataset.index=i; span.addEventListener('click',()=>{ const t=w.recordedTime??w.originalTime; player.currentTime=t; player.play(); }); wordListEl.appendChild(span); }); } function markCurrentWord(){ wordListEl.querySelectorAll('.word').forEach(el=>el.classList.remove('current')); const current=wordListEl.querySelector(`.word[data-index="${currentIdx}"]`); if(current) current.classList.add('current'); } function updateProgress(){ const done=words.filter(w=>w.recordedTime!==undefined).length; progress.textContent=`${done} / ${words.length}`; const hasDone=done>0; undoBtn.disabled=!hasDone; copyBtn.disabled=downloadBtn.disabled=!hasDone; } // ---------- gravação e comandos ---------- startBtn.addEventListener('click',()=>{ currentIdx=0; player.currentTime=0; player.play(); markCurrentWord(); }); function exportSrt(){ return words.filter(w=>w.recordedTime!==undefined) .map(w=>`${formatTime(w.recordedTime)}#${w.word}`) .join('\n'); } function undoLast(){ const last=words.map(w=>w.recordedTime!==undefined).lastIndexOf(true); if(last<0) return; delete words[last].recordedTime; const span=wordListEl.querySelector(`.word[data-index="${last}"]`); span.classList.remove('recorded'); span.classList.add('pending'); currentIdx=last; markCurrentWord(); updateProgress(); output.value=exportSrt(); } document.addEventListener('keydown',e=>{ if(e.code==='Space'){ e.preventDefault(); const now=player.currentTime; if(currentIdx>=words.length) return; const item=words[currentIdx]; item.recordedTime=now; const span=wordListEl.querySelector(`.word[data-index="${currentIdx}"]`); span.classList.remove('pending'); span.classList.add('recorded'); currentIdx=Math.min(currentIdx+1,words.length); markCurrentWord(); updateProgress(); if(currentIdx===words.length){ player.pause(); output.value=exportSrt(); } }else if(e.key.toLowerCase()==='z'){ undoLast(); }else if(e.key==='ArrowLeft'){ player.currentTime=Math.max(0,player.currentTime-2); } }); undoBtn.addEventListener('click',undoLast); backBtn.addEventListener('click',()=>{player.currentTime=Math.max(0,player.currentTime-2);}); copyBtn.addEventListener('click',()=>{navigator.clipboard.writeText(output.value);}); downloadBtn.addEventListener('click',()=>{ const blob=new Blob([output.value],{type:'text/plain'}); const url=URL.createObjectURL(blob); const a=document.createElement('a'); a.href=url; a.download='ajustada.srt'; document.body.appendChild(a); a.click(); document.body.removeChild(a); URL.revokeObjectURL(url); }); player.addEventListener('ended',()=>{output.value=exportSrt();}); // ---------- velocidade ---------- function updatePlaybackRate(){ const rate=parseFloat(speedSlider.value); player.playbackRate=rate; speedValue.textContent=`${rate.toFixed(1)}×`; } speedSlider.addEventListener('input',updatePlaybackRate); updatePlaybackRate(); // inicial </script> </body> </html>
Como testar
.srt
simples com formato min:seg:dec#palavra
.