Moves platform STT out into a plugin that owns the whole whisper.cpp contract: sidecar, config (a JSON file under DATA_PATH/transcribe/, validated against the server before storing), a thin proxy router, a skill so an agent knows the contract without being told, and a script-mode file-browser task (not agentic — no model turn per audio file). sidecar/transcribe.ts and its test moved unchanged from src/servers/api/stt/ — every constant in it has a reason attached and none of it was touched. The platform-side removal (routes, dead useAudioRecording path, Settings screen, locale keys) lands in a separate, uncommitted change in the platform repo for review. Not yet reachable from the file browser: the platform does not load tasks from a plugin's directory yet (skills/affordances/notifications do). Ships anyway per instructions. Officer-Session: 656f5c85-e49a-4f1f-b9f3-042a04b4d043
79 lines
3.1 KiB
TypeScript
79 lines
3.1 KiB
TypeScript
import { describe, expect, it } from 'bun:test';
|
|
import { toParagraphs } from './transcribe';
|
|
import type { WhisperSegment } from './transcribe';
|
|
|
|
// Whisper's own shape: leading space on every segment, times in seconds.
|
|
const seg = (text: string, start: number, end: number, extra: Partial<WhisperSegment> = {}): WhisperSegment => ({
|
|
text,
|
|
start,
|
|
end,
|
|
...extra,
|
|
});
|
|
|
|
describe('toParagraphs', () => {
|
|
it('joins segments spoken without a real pause into one paragraph', () => {
|
|
const out = toParagraphs([seg(' I went to the shop.', 0, 2), seg(' It was closed.', 2.2, 4)]);
|
|
expect(out).toBe('I went to the shop. It was closed.');
|
|
});
|
|
|
|
it('starts a new paragraph after a long silence', () => {
|
|
const out = toParagraphs([seg(' First thought.', 0, 2), seg(' Second thought.', 5, 7)]);
|
|
expect(out).toBe('First thought.\n\nSecond thought.');
|
|
});
|
|
|
|
it('does not break on a breath-length pause', () => {
|
|
const out = toParagraphs([seg(' Hang on,', 0, 1), seg(' let me think.', 1.5, 3)]);
|
|
expect(out).toBe('Hang on, let me think.');
|
|
});
|
|
|
|
// The lone "." lines in the old response_format=text output came from segments like this.
|
|
it('attaches stranded punctuation without a space in front of it', () => {
|
|
const out = toParagraphs([seg(' See you tomorrow morning', 0, 2), seg(' .', 2.1, 2.2)]);
|
|
expect(out).toBe('See you tomorrow morning.');
|
|
});
|
|
|
|
it('drops segments that are probably silence decoded with low confidence', () => {
|
|
const out = toParagraphs([
|
|
seg(' Real speech here.', 0, 2),
|
|
seg(' Thanks for watching!', 2.1, 3, { no_speech_prob: 0.95, avg_logprob: -1.8 }),
|
|
seg(' More real speech.', 3.1, 5),
|
|
]);
|
|
expect(out).toBe('Real speech here. More real speech.');
|
|
});
|
|
|
|
it('keeps a confident segment even when it looks like silence', () => {
|
|
const out = toParagraphs([seg(' Definitely said.', 0, 2, { no_speech_prob: 0.95, avg_logprob: -0.2 })]);
|
|
expect(out).toBe('Definitely said.');
|
|
});
|
|
|
|
it('keeps segments that report neither probability', () => {
|
|
const out = toParagraphs([seg(' No metadata at all.', 0, 2)]);
|
|
expect(out).toBe('No metadata at all.');
|
|
});
|
|
|
|
it('measures the pause from the last kept segment, not the dropped one', () => {
|
|
const out = toParagraphs([
|
|
seg(' Before.', 0, 2),
|
|
seg(' Hallucinated.', 2.1, 9, { no_speech_prob: 0.9, avg_logprob: -2 }),
|
|
seg(' After.', 9.1, 11),
|
|
]);
|
|
// The gap that matters is 2 -> 9.1, so these are separate thoughts.
|
|
expect(out).toBe('Before.\n\nAfter.');
|
|
});
|
|
|
|
it('collapses internal whitespace and ignores empty segments', () => {
|
|
const out = toParagraphs([seg(' Too many\n spaces. ', 0, 2), seg(' ', 2.1, 2.2)]);
|
|
expect(out).toBe('Too many spaces.');
|
|
});
|
|
|
|
it('returns an empty string when every segment is filtered out', () => {
|
|
expect(toParagraphs([])).toBe('');
|
|
expect(toParagraphs([seg(' ', 0, 1)])).toBe('');
|
|
});
|
|
|
|
it('falls back to no break when segments carry no timings', () => {
|
|
const out = toParagraphs([{ text: ' One.' }, { text: ' Two.' }]);
|
|
expect(out).toBe('One. Two.');
|
|
});
|
|
});
|