From 7a406d5cb39aeef4e05780404357ef4a0bbefcc7 Mon Sep 17 00:00:00 2001 From: ysdede <5496750+ysdede@users.noreply.github.com> Date: Sat, 7 Feb 2026 18:59:56 +0000 Subject: [PATCH 1/2] Refactor TokenStreamTranscriber to reduce duplication Co-authored-by: google-labs-jules[bot] <161369871+google-labs-jules[bot]@users.noreply.github.com> --- .../TokenStreamTranscriber.test.ts | 89 ++++++++++++++++++ .../transcription/TokenStreamTranscriber.ts | 93 ++++++++----------- 2 files changed, 130 insertions(+), 52 deletions(-) create mode 100644 src/lib/transcription/TokenStreamTranscriber.test.ts diff --git a/src/lib/transcription/TokenStreamTranscriber.test.ts b/src/lib/transcription/TokenStreamTranscriber.test.ts new file mode 100644 index 0000000..3499ba6 --- /dev/null +++ b/src/lib/transcription/TokenStreamTranscriber.test.ts @@ -0,0 +1,89 @@ +import { describe, it, expect, vi, beforeEach } from 'vitest'; +import { TokenStreamTranscriber } from './TokenStreamTranscriber'; +import { ModelManager } from './ModelManager'; + +// Mock parakeet.js +const mockMergerInstance = { + processChunk: vi.fn(), + getText: vi.fn(), + reset: vi.fn(), + getState: vi.fn(), +}; + +vi.mock('parakeet.js', () => { + return { + LCSPTFAMerger: class { + constructor() { + return mockMergerInstance; + } + }, + }; +}); + +// Mock ModelManager +const mockModel = { + transcribe: vi.fn(), + tokenizer: {}, + getFrameTimeStride: vi.fn(() => 0.08), +}; + +const mockModelManager = { + getModel: vi.fn(() => mockModel), +} as unknown as ModelManager; + +describe('TokenStreamTranscriber', () => { + let transcriber: TokenStreamTranscriber; + + beforeEach(() => { + vi.clearAllMocks(); + mockMergerInstance.processChunk.mockReturnValue({ + lcsLength: 5, + anchorValid: true, + anchorTokens: ['a', 'b'], + confirmed: [], + pending: [], + }); + mockMergerInstance.getText.mockReturnValue({ + confirmed: 'Hello', + pending: ' world', + full: 'Hello world', + }); + mockModel.transcribe.mockResolvedValue({ + metrics: { + total_ms: 100, + }, + // ... other result properties + }); + transcriber = new TokenStreamTranscriber(mockModelManager); + }); + + it('processChunk should call model.transcribe and merger.processChunk', async () => { + await transcriber.initialize(); + + const audio = new Float32Array(16000 * 2); + const result = await transcriber.processChunk(audio, 0); + + expect(mockModel.transcribe).toHaveBeenCalled(); + expect(mockMergerInstance.processChunk).toHaveBeenCalled(); + expect(result.fullText).toBe('Hello world'); + expect(result.chunkCount).toBe(1); + }); + + it('processChunkWithFeatures should call model.transcribe and merger.processChunk', async () => { + await transcriber.initialize(); + + const features = new Float32Array(80 * 100); + const result = await transcriber.processChunkWithFeatures(features, 100, 80, 0, 0); + + expect(mockModel.transcribe).toHaveBeenCalledWith( + null, + expect.any(Number), + expect.objectContaining({ + precomputedFeatures: expect.objectContaining({ features }), + }) + ); + expect(mockMergerInstance.processChunk).toHaveBeenCalled(); + expect(result.fullText).toBe('Hello world'); + expect(result.chunkCount).toBe(1); + }); +}); diff --git a/src/lib/transcription/TokenStreamTranscriber.ts b/src/lib/transcription/TokenStreamTranscriber.ts index 520a8a0..fc6d413 100644 --- a/src/lib/transcription/TokenStreamTranscriber.ts +++ b/src/lib/transcription/TokenStreamTranscriber.ts @@ -259,12 +259,37 @@ export class TokenStreamTranscriber { } : undefined, }); + // Determine merger overlap + const mergerOverlap = this._chunkCount > 0 ? (startTime !== undefined ? actualOverlap : this._config.overlapDuration) : 0; + + const { tokenStreamResult, mergeResult } = this._handleProcessingResult(result, chunkStartTime, mergerOverlap); + + // Always log preprocessing info from model metrics + const m = result.metrics; + if (m) { + console.log(`[TokenStreamTranscriber] Chunk #${this._chunkCount + 1}: preprocessor=${m.preprocessor_backend || 'unknown'}, preprocess=${m.preprocess_ms}ms, encode=${m.encode_ms}ms, decode=${m.decode_ms}ms, total=${m.total_ms}ms${m.mel_cache ? `, mel_cache: ${m.mel_cache.cached_frames} cached / ${m.mel_cache.new_frames} new` : ''}`); + } + + if (this._config.debug) { + console.log(`[TokenStreamTranscriber] Chunk ${this._chunkCount}: start=${chunkStartTime.toFixed(2)}s, overlap=${actualOverlap.toFixed(2)}s, LCS=${mergeResult.lcsLength}, anchor=${mergeResult.anchorValid}`); + } + + return tokenStreamResult; + } + + /** + * Shared logic for processing transcription results. + */ + private _handleProcessingResult( + result: any, + chunkStartTime: number, + mergerOverlap: number + ): { tokenStreamResult: TokenStreamResult; mergeResult: any } { // Merge using LCSPTFAMerger - // We use the provided overlap or calculated one const mergeResult = this._merger!.processChunk( result, chunkStartTime, - this._chunkCount > 0 ? (startTime !== undefined ? actualOverlap : this._config.overlapDuration) : 0 + mergerOverlap ); // Update state for next chunk @@ -286,24 +311,17 @@ export class TokenStreamTranscriber { anchorTokens: mergeResult.anchorTokens }); - // Always log preprocessing info from model metrics - const m = result.metrics; - if (m) { - console.log(`[TokenStreamTranscriber] Chunk #${this._chunkCount + 1}: preprocessor=${m.preprocessor_backend || 'unknown'}, preprocess=${m.preprocess_ms}ms, encode=${m.encode_ms}ms, decode=${m.decode_ms}ms, total=${m.total_ms}ms${m.mel_cache ? `, mel_cache: ${m.mel_cache.cached_frames} cached / ${m.mel_cache.new_frames} new` : ''}`); - } - - if (this._config.debug) { - console.log(`[TokenStreamTranscriber] Chunk ${this._chunkCount}: start=${chunkStartTime.toFixed(2)}s, overlap=${actualOverlap.toFixed(2)}s, LCS=${mergeResult.lcsLength}, anchor=${mergeResult.anchorValid}`); - } - return { - confirmedText, - pendingText, - fullText, - lcsLength: mergeResult.lcsLength, - anchorValid: mergeResult.anchorValid, - chunkCount: this._chunkCount, - anchorTokens: mergeResult.anchorTokens, + tokenStreamResult: { + confirmedText, + pendingText, + fullText, + lcsLength: mergeResult.lcsLength, + anchorValid: mergeResult.anchorValid, + chunkCount: this._chunkCount, + anchorTokens: mergeResult.anchorTokens, + }, + mergeResult }; } @@ -391,31 +409,10 @@ export class TokenStreamTranscriber { } : undefined, }); - // Merge using LCSPTFAMerger (same as audio path) - const mergeResult = this._merger!.processChunk( - result, - chunkStartTime, - this._chunkCount > 0 ? actualOverlap : 0 - ); - - // Update state for next chunk - this._currentTimestamp = chunkStartTime; - this._chunkCount++; + // Determine merger overlap + const mergerOverlap = this._chunkCount > 0 ? actualOverlap : 0; - // Get formatted text - const texts = this._merger!.getText(this._tokenizer); - const confirmedText = texts.confirmed; - const pendingText = texts.pending; - const fullText = texts.full; - - // Notify callbacks - this._callbacks.onConfirmedUpdate?.(confirmedText, mergeResult.confirmed); - this._callbacks.onPendingUpdate?.(pendingText, mergeResult.pending); - this._callbacks.onMergeInfo?.({ - lcsLength: mergeResult.lcsLength, - anchorValid: mergeResult.anchorValid, - anchorTokens: mergeResult.anchorTokens - }); + const { tokenStreamResult, mergeResult } = this._handleProcessingResult(result, chunkStartTime, mergerOverlap); // Always log preprocessing info from model metrics const m = result.metrics; @@ -427,15 +424,7 @@ export class TokenStreamTranscriber { console.log(`[TokenStreamTranscriber] Features chunk ${this._chunkCount}: start=${chunkStartTime.toFixed(2)}s, overlap=${actualOverlap.toFixed(2)}s, T=${T}, LCS=${mergeResult.lcsLength}`); } - return { - confirmedText, - pendingText, - fullText, - lcsLength: mergeResult.lcsLength, - anchorValid: mergeResult.anchorValid, - chunkCount: this._chunkCount, - anchorTokens: mergeResult.anchorTokens, - }; + return tokenStreamResult; } /** From 835687887fa347a864acb1a0612179928a402213 Mon Sep 17 00:00:00 2001 From: ysdede <5496750+ysdede@users.noreply.github.com> Date: Tue, 10 Feb 2026 21:49:27 +0000 Subject: [PATCH 2/2] Refactor TokenStreamTranscriber to reduce duplication Co-authored-by: google-labs-jules[bot] <161369871+google-labs-jules[bot]@users.noreply.github.com>