import { describe, expect, it } from 'bun:test'; import { toConversation } from './conversation'; import type { WhisperSegment } from './transcribe'; import type { SpeakerTurn } from './worker'; const seg = (text: string, start: number, end: number, extra: Partial = {}): WhisperSegment => ({ text, start, end, ...extra, }); const turn = (start: number, end: number, speaker: string): SpeakerTurn => ({ start, end, speaker }); describe('toConversation', () => { it('attributes each segment to the speaker whose turns cover it most', () => { const out = toConversation( [seg(' How are you?', 0, 2), seg(' Fine, thanks.', 2.5, 4)], [turn(0, 2.2, 'SPEAKER_00'), turn(2.3, 4.5, 'SPEAKER_01')], ); expect(out.text).toBe('Speaker 1: How are you?\n\nSpeaker 2: Fine, thanks.'); expect(out.speakerCount).toBe(2); }); it('numbers speakers by first appearance, not by pyannote label order', () => { // SPEAKER_01 talks first — they are "Speaker 1" in the transcript. const out = toConversation( [seg(' Me first.', 0, 2), seg(' Me second.', 3, 5)], [turn(0, 2, 'SPEAKER_01'), turn(3, 5, 'SPEAKER_00')], ); expect(out.text).toBe('Speaker 1: Me first.\n\nSpeaker 2: Me second.'); }); it('joins consecutive segments by the same speaker into one block', () => { const out = toConversation( [seg(' First sentence.', 0, 2), seg(' Second sentence.', 2.2, 4), seg(' A reply.', 5, 6)], [turn(0, 4.5, 'SPEAKER_00'), turn(4.8, 6.5, 'SPEAKER_01')], ); expect(out.text).toBe('Speaker 1: First sentence. Second sentence.\n\nSpeaker 2: A reply.'); }); // Whisper and pyannote disagree about boundaries all the time; a segment neither turn covers // must stay with whoever was already talking, not open a phantom block. it('keeps an uncovered segment with the previous speaker', () => { const out = toConversation( [seg(' Covered.', 0, 2), seg(' In the gap.', 2.5, 3), seg(' Reply.', 4, 6)], [turn(0, 2.2, 'SPEAKER_00'), turn(3.8, 6, 'SPEAKER_01')], ); expect(out.text).toBe('Speaker 1: Covered. In the gap.\n\nSpeaker 2: Reply.'); }); it('attributes a transcript that starts before the first turn to the first speaker', () => { const out = toConversation([seg(' Early words.', 0, 1)], [turn(1.5, 3, 'SPEAKER_00')]); expect(out.text).toBe('Speaker 1: Early words.'); expect(out.speakerCount).toBe(1); }); it('drops hallucinated-silence segments like the solo path does', () => { const out = toConversation( [seg(' Real.', 0, 2), seg(' Thanks for watching!', 2.1, 3, { no_speech_prob: 0.95, avg_logprob: -1.8 })], [turn(0, 3, 'SPEAKER_00')], ); expect(out.text).toBe('Speaker 1: Real.'); }); it('attaches stranded punctuation without a space, inside a block', () => { const out = toConversation( [seg(' See you tomorrow', 0, 2), seg(' .', 2.1, 2.2)], [turn(0, 3, 'SPEAKER_00')], ); expect(out.text).toBe('Speaker 1: See you tomorrow.'); }); it('reports one speaker for a monologue so the caller can fall back to paragraphs', () => { const out = toConversation( [seg(' Just me talking.', 0, 2)], [turn(0, 2, 'SPEAKER_00'), turn(3, 4, 'SPEAKER_00')], ); expect(out.speakerCount).toBe(1); }); });