Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
89 changes: 89 additions & 0 deletions src/lib/transcription/TokenStreamTranscriber.test.ts
Original file line number Diff line number Diff line change
@@ -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);
});
});
93 changes: 41 additions & 52 deletions src/lib/transcription/TokenStreamTranscriber.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
};
}

Expand Down Expand Up @@ -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;
Expand All @@ -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;
}

/**
Expand Down