v74: откат WAV — WebM для Groq, trimSilence в памяти для локального
This commit is contained in:
Vendored
+11
-42
@@ -257,7 +257,7 @@ async function transcribe(blob) {
|
|||||||
const tStart = performance.now();
|
const tStart = performance.now();
|
||||||
|
|
||||||
const form = new FormData();
|
const form = new FormData();
|
||||||
form.append('file', blob, blob.type === 'audio/wav' ? 'audio.wav' : 'audio.webm');
|
form.append('file', blob, 'audio.webm');
|
||||||
form.append('model', 'whisper-large-v3');
|
form.append('model', 'whisper-large-v3');
|
||||||
form.append('language', 'it');
|
form.append('language', 'it');
|
||||||
|
|
||||||
@@ -274,7 +274,6 @@ async function transcribe(blob) {
|
|||||||
return data.error ? { text: '', error: data.error } : { text: data.text || '', error: null };
|
return data.error ? { text: '', error: data.error } : { text: data.text || '', error: null };
|
||||||
} catch(e) {
|
} catch(e) {
|
||||||
console.log('[HTTP] ERR', e);
|
console.log('[HTTP] ERR', e);
|
||||||
alert('Ошибка транскрипции: ' + (e.message || e));
|
|
||||||
return { text: '', error: 'network' };
|
return { text: '', error: 'network' };
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -365,15 +364,11 @@ async function startRecording() {
|
|||||||
const opts = mimeType ? { mimeType } : {};
|
const opts = mimeType ? { mimeType } : {};
|
||||||
mediaRecorder = new MediaRecorder(stream, opts);
|
mediaRecorder = new MediaRecorder(stream, opts);
|
||||||
mediaRecorder.ondataavailable = e => { if (e.data.size > 0) audioChunks.push(e.data); };
|
mediaRecorder.ondataavailable = e => { if (e.data.size > 0) audioChunks.push(e.data); };
|
||||||
mediaRecorder.onstop = async () => {
|
mediaRecorder.onstop = () => {
|
||||||
stopMicMeter();
|
stopMicMeter();
|
||||||
recordDuration = (Date.now() - recordStartTime) / 1000;
|
recordDuration = (Date.now() - recordStartTime) / 1000;
|
||||||
let blob = new Blob(audioChunks, { type: mimeType || 'audio/webm' });
|
userAudioBlob = new Blob(audioChunks, { type: mimeType || 'audio/webm' });
|
||||||
stream.getTracks().forEach(t => t.stop());
|
stream.getTracks().forEach(t => t.stop());
|
||||||
// Обрезаем тишину
|
|
||||||
const trimmed = await trimSilence(blob);
|
|
||||||
if (trimmed && trimmed.size > 0) blob = trimmed;
|
|
||||||
userAudioBlob = blob;
|
|
||||||
enable('btnCompare', true);
|
enable('btnCompare', true);
|
||||||
enable('btnStereo', true);
|
enable('btnStereo', true);
|
||||||
enable('btnPlayUser', true);
|
enable('btnPlayUser', true);
|
||||||
@@ -558,7 +553,7 @@ function pitchFrame(data, start, len, sr) {
|
|||||||
return bestR > 0.30 ? sr / bestLag : 0;
|
return bestR > 0.30 ? sr / bestLag : 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
// ---------- Обрезка тишины (щелчок в начале, тишина в конце) ----------
|
// ---------- Обрезка тишины — возвращает {buffer, startSample, endSample} ----------
|
||||||
async function trimSilence(blob) {
|
async function trimSilence(blob) {
|
||||||
try {
|
try {
|
||||||
const ctx = new (window.OfflineAudioContext || window.webkitOfflineAudioContext)(1, 1, 44100);
|
const ctx = new (window.OfflineAudioContext || window.webkitOfflineAudioContext)(1, 1, 44100);
|
||||||
@@ -568,7 +563,6 @@ async function trimSilence(blob) {
|
|||||||
const sr = buf.sampleRate;
|
const sr = buf.sampleRate;
|
||||||
const len = data.length;
|
const len = data.length;
|
||||||
|
|
||||||
// RMS по окнам 10мс
|
|
||||||
const win = Math.floor(sr * 0.01);
|
const win = Math.floor(sr * 0.01);
|
||||||
const rms = [];
|
const rms = [];
|
||||||
let maxRms = 0;
|
let maxRms = 0;
|
||||||
@@ -579,17 +573,14 @@ async function trimSilence(blob) {
|
|||||||
rms.push(v);
|
rms.push(v);
|
||||||
if (v > maxRms) maxRms = v;
|
if (v > maxRms) maxRms = v;
|
||||||
}
|
}
|
||||||
if (maxRms < 0.001) return blob; // тишина
|
if (maxRms < 0.001) return null;
|
||||||
|
|
||||||
const threshold = maxRms * 0.02; // 2% от пика
|
const threshold = maxRms * 0.02;
|
||||||
|
|
||||||
// Начало — первый кадр выше порога
|
|
||||||
let startIdx = 0;
|
let startIdx = 0;
|
||||||
for (let i = 0; i < rms.length; i++) {
|
for (let i = 0; i < rms.length; i++) {
|
||||||
if (rms[i] > threshold) { startIdx = Math.max(0, i - 2); break; }
|
if (rms[i] > threshold) { startIdx = Math.max(0, i - 2); break; }
|
||||||
}
|
}
|
||||||
|
|
||||||
// Конец — последний кадр выше порога
|
|
||||||
let endIdx = rms.length - 1;
|
let endIdx = rms.length - 1;
|
||||||
for (let i = rms.length - 1; i >= 0; i--) {
|
for (let i = rms.length - 1; i >= 0; i--) {
|
||||||
if (rms[i] > threshold) { endIdx = Math.min(rms.length - 1, i + 2); break; }
|
if (rms[i] > threshold) { endIdx = Math.min(rms.length - 1, i + 2); break; }
|
||||||
@@ -597,33 +588,11 @@ async function trimSilence(blob) {
|
|||||||
|
|
||||||
const startSample = startIdx * win;
|
const startSample = startIdx * win;
|
||||||
const endSample = Math.min((endIdx + 1) * win, len);
|
const endSample = Math.min((endIdx + 1) * win, len);
|
||||||
if (endSample - startSample < sr * 0.08) return blob; // слишком коротко
|
if (endSample - startSample < sr * 0.08) return null;
|
||||||
|
|
||||||
// WAV: 44-byte header + PCM16
|
const trimmedBuf = new (window.OfflineAudioContext || window.webkitOfflineAudioContext)(1, endSample - startSample, sr);
|
||||||
const trimmed = data.slice(startSample, endSample);
|
return { buffer: trimmedBuf, data: data.slice(startSample, endSample), sr, startSample, endSample };
|
||||||
const numSamples = trimmed.length;
|
} catch(e) { return null; }
|
||||||
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; }
|
|
||||||
}
|
}
|
||||||
|
|
||||||
</script>
|
</script>
|
||||||
@@ -766,7 +735,7 @@ async function doCompare(blob, originalText, duration) {
|
|||||||
<script>
|
<script>
|
||||||
// ---------- Глобальные переменные и инициализация ----------
|
// ---------- Глобальные переменные и инициализация ----------
|
||||||
const GROQ_API_KEY = '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';
|
const GROQ_API_KEY = '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';
|
||||||
const VERSION = 'v73';
|
const VERSION = 'v74';
|
||||||
|
|
||||||
let mediaRecorder, audioChunks = [], userAudioBlob = null, ttsText = '';
|
let mediaRecorder, audioChunks = [], userAudioBlob = null, ttsText = '';
|
||||||
let isRecording = false, isAnalyzing = false, micAnalyser = null, micAnimId = null;
|
let isRecording = false, isAnalyzing = false, micAnalyser = null, micAnimId = null;
|
||||||
|
|||||||
+9
-39
@@ -62,15 +62,11 @@ async function startRecording() {
|
|||||||
const opts = mimeType ? { mimeType } : {};
|
const opts = mimeType ? { mimeType } : {};
|
||||||
mediaRecorder = new MediaRecorder(stream, opts);
|
mediaRecorder = new MediaRecorder(stream, opts);
|
||||||
mediaRecorder.ondataavailable = e => { if (e.data.size > 0) audioChunks.push(e.data); };
|
mediaRecorder.ondataavailable = e => { if (e.data.size > 0) audioChunks.push(e.data); };
|
||||||
mediaRecorder.onstop = async () => {
|
mediaRecorder.onstop = () => {
|
||||||
stopMicMeter();
|
stopMicMeter();
|
||||||
recordDuration = (Date.now() - recordStartTime) / 1000;
|
recordDuration = (Date.now() - recordStartTime) / 1000;
|
||||||
let blob = new Blob(audioChunks, { type: mimeType || 'audio/webm' });
|
userAudioBlob = new Blob(audioChunks, { type: mimeType || 'audio/webm' });
|
||||||
stream.getTracks().forEach(t => t.stop());
|
stream.getTracks().forEach(t => t.stop());
|
||||||
// Обрезаем тишину
|
|
||||||
const trimmed = await trimSilence(blob);
|
|
||||||
if (trimmed && trimmed.size > 0) blob = trimmed;
|
|
||||||
userAudioBlob = blob;
|
|
||||||
enable('btnCompare', true);
|
enable('btnCompare', true);
|
||||||
enable('btnStereo', true);
|
enable('btnStereo', true);
|
||||||
enable('btnPlayUser', true);
|
enable('btnPlayUser', true);
|
||||||
@@ -255,7 +251,7 @@ function pitchFrame(data, start, len, sr) {
|
|||||||
return bestR > 0.30 ? sr / bestLag : 0;
|
return bestR > 0.30 ? sr / bestLag : 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
// ---------- Обрезка тишины (щелчок в начале, тишина в конце) ----------
|
// ---------- Обрезка тишины — возвращает {buffer, startSample, endSample} ----------
|
||||||
async function trimSilence(blob) {
|
async function trimSilence(blob) {
|
||||||
try {
|
try {
|
||||||
const ctx = new (window.OfflineAudioContext || window.webkitOfflineAudioContext)(1, 1, 44100);
|
const ctx = new (window.OfflineAudioContext || window.webkitOfflineAudioContext)(1, 1, 44100);
|
||||||
@@ -265,7 +261,6 @@ async function trimSilence(blob) {
|
|||||||
const sr = buf.sampleRate;
|
const sr = buf.sampleRate;
|
||||||
const len = data.length;
|
const len = data.length;
|
||||||
|
|
||||||
// RMS по окнам 10мс
|
|
||||||
const win = Math.floor(sr * 0.01);
|
const win = Math.floor(sr * 0.01);
|
||||||
const rms = [];
|
const rms = [];
|
||||||
let maxRms = 0;
|
let maxRms = 0;
|
||||||
@@ -276,17 +271,14 @@ async function trimSilence(blob) {
|
|||||||
rms.push(v);
|
rms.push(v);
|
||||||
if (v > maxRms) maxRms = v;
|
if (v > maxRms) maxRms = v;
|
||||||
}
|
}
|
||||||
if (maxRms < 0.001) return blob; // тишина
|
if (maxRms < 0.001) return null;
|
||||||
|
|
||||||
const threshold = maxRms * 0.02; // 2% от пика
|
const threshold = maxRms * 0.02;
|
||||||
|
|
||||||
// Начало — первый кадр выше порога
|
|
||||||
let startIdx = 0;
|
let startIdx = 0;
|
||||||
for (let i = 0; i < rms.length; i++) {
|
for (let i = 0; i < rms.length; i++) {
|
||||||
if (rms[i] > threshold) { startIdx = Math.max(0, i - 2); break; }
|
if (rms[i] > threshold) { startIdx = Math.max(0, i - 2); break; }
|
||||||
}
|
}
|
||||||
|
|
||||||
// Конец — последний кадр выше порога
|
|
||||||
let endIdx = rms.length - 1;
|
let endIdx = rms.length - 1;
|
||||||
for (let i = rms.length - 1; i >= 0; i--) {
|
for (let i = rms.length - 1; i >= 0; i--) {
|
||||||
if (rms[i] > threshold) { endIdx = Math.min(rms.length - 1, i + 2); break; }
|
if (rms[i] > threshold) { endIdx = Math.min(rms.length - 1, i + 2); break; }
|
||||||
@@ -294,31 +286,9 @@ async function trimSilence(blob) {
|
|||||||
|
|
||||||
const startSample = startIdx * win;
|
const startSample = startIdx * win;
|
||||||
const endSample = Math.min((endIdx + 1) * win, len);
|
const endSample = Math.min((endIdx + 1) * win, len);
|
||||||
if (endSample - startSample < sr * 0.08) return blob; // слишком коротко
|
if (endSample - startSample < sr * 0.08) return null;
|
||||||
|
|
||||||
// WAV: 44-byte header + PCM16
|
const trimmedBuf = new (window.OfflineAudioContext || window.webkitOfflineAudioContext)(1, endSample - startSample, sr);
|
||||||
const trimmed = data.slice(startSample, endSample);
|
return { buffer: trimmedBuf, data: data.slice(startSample, endSample), sr, startSample, endSample };
|
||||||
const numSamples = trimmed.length;
|
} catch(e) { return null; }
|
||||||
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; }
|
|
||||||
}
|
}
|
||||||
|
|||||||
+1
-1
@@ -1,6 +1,6 @@
|
|||||||
// ---------- Глобальные переменные и инициализация ----------
|
// ---------- Глобальные переменные и инициализация ----------
|
||||||
const GROQ_API_KEY = '';
|
const GROQ_API_KEY = '';
|
||||||
const VERSION = 'v73';
|
const VERSION = 'v74';
|
||||||
|
|
||||||
let mediaRecorder, audioChunks = [], userAudioBlob = null, ttsText = '';
|
let mediaRecorder, audioChunks = [], userAudioBlob = null, ttsText = '';
|
||||||
let isRecording = false, isAnalyzing = false, micAnalyser = null, micAnimId = null;
|
let isRecording = false, isAnalyzing = false, micAnalyser = null, micAnimId = null;
|
||||||
|
|||||||
+1
-2
@@ -5,7 +5,7 @@ async function transcribe(blob) {
|
|||||||
const tStart = performance.now();
|
const tStart = performance.now();
|
||||||
|
|
||||||
const form = new FormData();
|
const form = new FormData();
|
||||||
form.append('file', blob, blob.type === 'audio/wav' ? 'audio.wav' : 'audio.webm');
|
form.append('file', blob, 'audio.webm');
|
||||||
form.append('model', 'whisper-large-v3');
|
form.append('model', 'whisper-large-v3');
|
||||||
form.append('language', 'it');
|
form.append('language', 'it');
|
||||||
|
|
||||||
@@ -22,7 +22,6 @@ async function transcribe(blob) {
|
|||||||
return data.error ? { text: '', error: data.error } : { text: data.text || '', error: null };
|
return data.error ? { text: '', error: data.error } : { text: data.text || '', error: null };
|
||||||
} catch(e) {
|
} catch(e) {
|
||||||
console.log('[HTTP] ERR', e);
|
console.log('[HTTP] ERR', e);
|
||||||
alert('Ошибка транскрипции: ' + (e.message || e));
|
|
||||||
return { text: '', error: 'network' };
|
return { text: '', error: 'network' };
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user