diff --git a/dist/index.html b/dist/index.html index 6aff42b..94e2b20 100644 --- a/dist/index.html +++ b/dist/index.html @@ -257,7 +257,7 @@ async function transcribe(blob) { const tStart = performance.now(); const form = new FormData(); - form.append('file', blob, 'audio.webm'); + form.append('file', blob, blob.type === 'audio/wav' ? 'audio.wav' : 'audio.webm'); form.append('model', 'whisper-large-v3'); form.append('language', 'it'); @@ -365,11 +365,15 @@ async function startRecording() { const opts = mimeType ? { mimeType } : {}; mediaRecorder = new MediaRecorder(stream, opts); mediaRecorder.ondataavailable = e => { if (e.data.size > 0) audioChunks.push(e.data); }; - mediaRecorder.onstop = () => { + mediaRecorder.onstop = async () => { stopMicMeter(); recordDuration = (Date.now() - recordStartTime) / 1000; - userAudioBlob = new Blob(audioChunks, { type: mimeType || 'audio/webm' }); + let blob = new Blob(audioChunks, { type: mimeType || 'audio/webm' }); stream.getTracks().forEach(t => t.stop()); + // Обрезаем тишину + const trimmed = await trimSilence(blob); + if (trimmed && trimmed.size > 0) blob = trimmed; + userAudioBlob = blob; enable('btnCompare', true); enable('btnStereo', true); enable('btnPlayUser', true); @@ -554,6 +558,74 @@ function pitchFrame(data, start, len, sr) { return bestR > 0.30 ? sr / bestLag : 0; } +// ---------- Обрезка тишины (щелчок в начале, тишина в конце) ---------- +async function trimSilence(blob) { + try { + const ctx = new (window.OfflineAudioContext || window.webkitOfflineAudioContext)(1, 1, 44100); + const ab = await blob.arrayBuffer(); + const buf = await ctx.decodeAudioData(ab); + const data = buf.getChannelData(0); + const sr = buf.sampleRate; + const len = data.length; + + // RMS по окнам 10мс + const win = Math.floor(sr * 0.01); + const rms = []; + let maxRms = 0; + for (let i = 0; i < len; i += win) { + let sum = 0, n = 0; + for (let j = i; j < i + win && j < len; j++, n++) sum += data[j] * data[j]; + const v = Math.sqrt(sum / n); + rms.push(v); + if (v > maxRms) maxRms = v; + } + if (maxRms < 0.001) return blob; // тишина + + const threshold = maxRms * 0.02; // 2% от пика + + // Начало — первый кадр выше порога + let startIdx = 0; + for (let i = 0; i < rms.length; i++) { + if (rms[i] > threshold) { startIdx = Math.max(0, i - 2); break; } + } + + // Конец — последний кадр выше порога + let endIdx = rms.length - 1; + for (let i = rms.length - 1; i >= 0; i--) { + if (rms[i] > threshold) { endIdx = Math.min(rms.length - 1, i + 2); break; } + } + + const startSample = startIdx * win; + const endSample = Math.min((endIdx + 1) * win, len); + if (endSample - startSample < sr * 0.08) return blob; // слишком коротко + + // WAV: 44-byte header + PCM16 + const trimmed = data.slice(startSample, endSample); + const numSamples = trimmed.length; + const wav = new ArrayBuffer(44 + numSamples * 2); + const view = new DataView(wav); + const writeStr = (off, s) => { for (let i = 0; i < s.length; i++) view.setUint8(off + i, s.charCodeAt(i)); }; + writeStr(0, 'RIFF'); + view.setUint32(4, 36 + numSamples * 2, true); + writeStr(8, 'WAVE'); + writeStr(12, 'fmt '); + view.setUint32(16, 16, true); + view.setUint16(20, 1, true); + view.setUint16(22, 1, true); + view.setUint32(24, sr, true); + view.setUint32(28, sr * 2, true); + view.setUint16(32, 2, true); + view.setUint16(34, 16, true); + writeStr(36, 'data'); + view.setUint32(40, numSamples * 2, true); + for (let i = 0; i < numSamples; i++) { + const s = Math.max(-1, Math.min(1, trimmed[i])); + view.setInt16(44 + i * 2, s < 0 ? s * 0x8000 : s * 0x7FFF, true); + } + return new Blob([wav], { type: 'audio/wav' }); + } catch(e) { return blob; } +} +