Enhanced transcription usability.
This commit is contained in:
+4
-1
@@ -67,7 +67,10 @@ Re-run it after any change to `src/lib/live.ts`, `src/lib/mic.ts`,
|
||||
## 3. Stop & result
|
||||
|
||||
1. Click **Stop** mid-sentence. The button shows "Finishing…" while the last
|
||||
segment gets its final pass, then the normal **Transcript** panel appears:
|
||||
segment gets its final pass — if several segments are still queued (slow
|
||||
model), the transcript heading counts them down ("Finishing up…
|
||||
transcribing N remaining segments", D-024) — then the normal
|
||||
**Transcript** panel appears:
|
||||
full text, Copy button, and a Timestamps section whose segment times are
|
||||
**absolute** (a sentence spoken ~10 s in must not be timestamped 0:00).
|
||||
2. The browser's mic indicator must turn off immediately on Stop.
|
||||
|
||||
+85
-1
@@ -144,7 +144,13 @@ function fakeEngine() {
|
||||
advance(ms: number) {
|
||||
now += ms;
|
||||
},
|
||||
make(options: { tentativeIntervalMs?: number; minTentativeSeconds?: number } = {}) {
|
||||
make(
|
||||
options: {
|
||||
tentativeIntervalMs?: number;
|
||||
minTentativeSeconds?: number;
|
||||
segmenter?: ConstructorParameters<typeof LiveSegmenter>[0];
|
||||
} = {},
|
||||
) {
|
||||
return new LiveTranscriber({
|
||||
transcribe: (audio) =>
|
||||
new Promise<LiveTranscribeResult>((resolve, reject) => {
|
||||
@@ -293,6 +299,84 @@ describe("LiveTranscriber", () => {
|
||||
expect(result.text).toBe("provisional recovered");
|
||||
});
|
||||
|
||||
test("queued final passes are batched into one merged pass", async () => {
|
||||
const engine = fakeEngine();
|
||||
const live = engine.make({ minTentativeSeconds: 9999 }); // finals only
|
||||
|
||||
// Segment 1 commits and its final pass starts (single flight). While it
|
||||
// is in flight, segments 2 and 3 commit and queue behind it.
|
||||
live.push(speech(1));
|
||||
live.push(silence(1.5));
|
||||
expect(engine.calls).toHaveLength(1);
|
||||
live.push(speech(1));
|
||||
live.push(silence(1.5));
|
||||
live.push(speech(1));
|
||||
live.push(silence(1.5));
|
||||
expect(engine.last().pendingFinalPasses).toBe(3);
|
||||
expect(engine.calls).toHaveLength(1); // still just the in-flight pass
|
||||
|
||||
engine.calls[0]!.resolve({ text: " one.", chunks: [] });
|
||||
await tick();
|
||||
|
||||
// The queue drains as ONE merged pass carrying both segments' audio
|
||||
// (each ≈ pre-roll + 1 s speech + 0.75 s commit silence).
|
||||
expect(engine.calls).toHaveLength(2);
|
||||
const merged = engine.calls[1]!;
|
||||
expect(merged.audio.length).toBeGreaterThanOrEqual(3.4 * RATE);
|
||||
expect(engine.last().pendingFinalPasses).toBe(2);
|
||||
|
||||
// Chunk timestamps map piecewise back to absolute session time: segment 2
|
||||
// starts after segment 1 (> 1.5 s into the session), and a chunk in the
|
||||
// second half of the merged audio lands even later, staying monotone.
|
||||
const secondHalf = merged.audio.length / 2 / RATE;
|
||||
merged.resolve({
|
||||
text: " two. three.",
|
||||
chunks: [
|
||||
{ text: " two.", timestamp: [0, 0.8] },
|
||||
{ text: " three.", timestamp: [secondHalf, secondHalf + 0.8] },
|
||||
],
|
||||
});
|
||||
await tick();
|
||||
expect(engine.last().committedText).toBe("one. two. three.");
|
||||
expect(engine.last().pendingFinalPasses).toBe(0);
|
||||
const [first, second] = engine.last().chunks;
|
||||
expect(first!.timestamp[0]).toBeGreaterThan(1.5);
|
||||
expect(second!.timestamp[0]).toBeGreaterThan(first!.timestamp[1]!);
|
||||
|
||||
const result = await live.stop();
|
||||
expect(result.text).toBe("one. two. three.");
|
||||
});
|
||||
|
||||
test("merged passes stay under the segment length cap", async () => {
|
||||
const engine = fakeEngine();
|
||||
// Tiny cap: segments are ~2 s each, so two of them exceed 3 s and must
|
||||
// NOT merge into one pass.
|
||||
const live = engine.make({
|
||||
minTentativeSeconds: 9999,
|
||||
segmenter: { maxSegmentSeconds: 3 },
|
||||
});
|
||||
|
||||
live.push(speech(1));
|
||||
live.push(silence(1.5)); // segment 1 commits → pass 1 in flight
|
||||
live.push(speech(1));
|
||||
live.push(silence(1.5)); // segment 2 queues
|
||||
live.push(speech(1));
|
||||
live.push(silence(1.5)); // segment 3 queues
|
||||
expect(engine.calls).toHaveLength(1);
|
||||
|
||||
engine.calls[0]!.resolve({ text: " one.", chunks: [] });
|
||||
await tick();
|
||||
expect(engine.calls).toHaveLength(2); // segment 2 alone (cap reached)
|
||||
expect(engine.calls[1]!.audio.length).toBeLessThanOrEqual(3 * RATE);
|
||||
|
||||
engine.calls[1]!.resolve({ text: " two.", chunks: [] });
|
||||
await tick();
|
||||
expect(engine.calls).toHaveLength(3);
|
||||
engine.calls[2]!.resolve({ text: " three.", chunks: [] });
|
||||
const result = await live.stop();
|
||||
expect(result.text).toBe("one. two. three.");
|
||||
});
|
||||
|
||||
test("a tentative result for an already-committed segment is discarded", async () => {
|
||||
const engine = fakeEngine();
|
||||
const live = engine.make();
|
||||
|
||||
Reference in New Issue
Block a user