[eric] voice: whisper's non-speech captions no longer inject as dictation (ENG-167)

This commit is contained in:
ciregenz
2026-08-05 18:07:23 -07:00
parent 9046dfbaa6
commit d9813de3cd
3 changed files with 30 additions and 4 deletions
+9 -3
View File
@@ -16,6 +16,12 @@ const SAMPLE_RATE = 16000;
const SILENT_KEEP_BYTES = SAMPLE_RATE * 2 * 2;
const SILENT_TRIM_BYTES = SAMPLE_RATE * 2 * 10;
// Whisper captions non-speech in brackets/parens ("[ Background sounds ]", "(laughs)"); strip them so
// neither the live preview nor a committed phrase ever carries a caption instead of dictation.
function stripSoundCaptions(text) {
return String(text || '').replace(/\[[^\]]*\]|\([^)]*\)|\*[^*]*\*|(?:^|\s)>>\s?/g, ' ').replace(/\s+/g, ' ').trim();
}
function wavFromPcm16(pcm) {
const buf = Buffer.alloc(44 + pcm.length);
buf.write('RIFF', 0); buf.writeUInt32LE(36 + pcm.length, 4); buf.write('WAVE', 8);
@@ -68,7 +74,7 @@ function createStreamingSession({ resourceDir, userDataDir, onPartial, previewIn
inflight = decodePcm(pcm)
.then((text) => {
if (closedDown || epoch !== segEpoch) return; // the segment closed mid-decode; its final wins
tentative = text;
tentative = stripSoundCaptions(text);
emit();
})
.catch(() => { skipNext = true; })
@@ -94,7 +100,7 @@ function createStreamingSession({ resourceDir, userDataDir, onPartial, previewIn
commitChain = commitChain.then(async () => {
if (inflight) await inflight;
try {
const text = await decodePcm(pcm);
const text = stripSoundCaptions(await decodePcm(pcm));
if (text) committed.push(text);
} catch (_) {
degraded = true; // a lost phrase final means the caller must fall back to the full-clip decode
@@ -137,4 +143,4 @@ function createStreamingSession({ resourceDir, userDataDir, onPartial, previewIn
};
}
module.exports = { createStreamingSession, wavFromPcm16 };
module.exports = { createStreamingSession, wavFromPcm16, stripSoundCaptions };
+19 -1
View File
@@ -2,7 +2,7 @@ const { test } = require('node:test');
const assert = require('node:assert');
const { createStreamSegmenter } = require('./streamSegmenter');
const whisperService = require('./whisperService');
const { createStreamingSession, wavFromPcm16 } = require('./streamingSession');
const { createStreamingSession, wavFromPcm16, stripSoundCaptions } = require('./streamingSession');
const RATE = 16000;
@@ -91,6 +91,24 @@ test('session: a failed segment decode marks the result degraded', async () => {
assert.strictEqual(out.degraded, true);
});
test('stripSoundCaptions: captions and speaker marks go, words stay', () => {
assert.strictEqual(stripSoundCaptions('[ Background sounds ]'), '');
assert.strictEqual(stripSoundCaptions('[ Silence ] >> Hello world. [ Silence ]'), 'Hello world.');
assert.strictEqual(stripSoundCaptions('(laughs) okay *music* done'), 'okay done');
assert.strictEqual(stripSoundCaptions('plain dictated text'), 'plain dictated text');
});
test('session: a caption-only decode never becomes a committed phrase', async () => {
const restore = stubTranscribe(async () => '[ Background sounds ]');
const s = createStreamingSession({ resourceDir: '', userDataDir: '', onPartial: () => {}, previewIntervalMs: 3600000 });
s.pushChunk(asBuffer(tone(400)));
const out = await s.stop();
restore();
assert.strictEqual(out.ok, true);
assert.strictEqual(out.text, '');
assert.strictEqual(out.degraded, false);
});
test('session: chunks after cancel are dropped and stop reports stopped', async () => {
const restore = stubTranscribe(async () => 'never');
const s = createStreamingSession({ resourceDir: '', userDataDir: '', onPartial: () => {}, previewIntervalMs: 3600000 });
@@ -200,6 +200,8 @@ export function useVoiceDictation() {
res = await window.openswarm?.voiceTranscribe?.(wav);
}
}
// Whisper captions non-speech in brackets/parens ("[ Background sounds ]", "(laughs)") and marks speaker turns with ">>"; those are annotations, not dictation.
if (res?.ok && res.text) res = { ok: true, text: res.text.replace(/\[[^\]]*\]|\([^)]*\)|\*[^*]*\*|(?:^|\s)>>\s?/g, ' ').replace(/\s+/g, ' ').trim() };
// A transcript with no letter or digit in it is a hallucination artifact (the lone comma), not dictation.
if (res?.ok && res.text && !/[\p{L}\p{N}]/u.test(res.text)) res = { ok: true, text: '' };
if (res?.ok && res.text) {