stream = new WhisperTranscriptionStream; } public function test_extracts_sse_data_payloads_from_buffer(): void { $buffer = "data: {\"type\":\"transcript.text.delta\",\"delta\":\"Hello\"}\n\npartial"; $payloads = $this->stream->extractPayloads($buffer); $this->assertSame(['{"type":"transcript.text.delta","delta":"Hello"}'], $payloads); $this->assertSame('partial', $buffer); } public function test_parses_openai_delta_and_done_events(): void { $delta = $this->stream->parseEvent('{"type":"transcript.text.delta","delta":"Hello "}'); $done = $this->stream->parseEvent('{"type":"transcript.text.done","text":"Hello world"}'); $this->assertSame('Hello ', $delta['append']); $this->assertFalse($delta['done']); $this->assertSame('Hello world', $done['replace']); $this->assertTrue($done['done']); } public function test_parses_legacy_segment_events(): void { $event = $this->stream->parseEvent('{"text":"First segment","start":0,"end":12.5}'); $this->assertTrue($event['legacy']); $this->assertSame('First segment', $event['append']); $this->assertSame(12.5, $event['end']); $this->assertSame('First segment', $event['whisper']['segment']['text']); $this->assertSame(12.5, $event['whisper']['segment']['end']); } public function test_parses_typed_segment_events_as_legacy_chunks(): void { $event = $this->stream->parseEvent(json_encode([ 'type' => 'segment', 'start' => 0.0, 'end' => 2.4, 'text' => 'Hello, how are you?', 'avg_logprob' => -0.12, 'words' => [ ['word' => 'Hello', 'start' => 0.0, 'end' => 0.5, 'probability' => 0.99], ], ])); $this->assertTrue($event['legacy']); $this->assertSame('Hello, how are you?', $event['append']); $this->assertSame(2.4, $event['end']); $this->assertSame(-0.12, $event['whisper']['segment']['avg_logprob']); $this->assertSame('Hello', $event['whisper']['segment']['words'][0]['word']); } public function test_parses_verbose_segment_words_language_and_logprobs(): void { $json = json_encode([ 'language' => 'en', 'duration' => 8.5, 'text' => 'Hello world', 'id' => 0, 'start' => 0.0, 'end' => 1.6, 'tokens' => [50364, 2425, 1002], 'avg_logprob' => -0.21, 'compression_ratio' => 1.15, 'no_speech_prob' => 0.01, 'temperature' => 0.0, 'words' => [ ['word' => 'Hello', 'start' => 0.0, 'end' => 0.6, 'probability' => 0.94], ['word' => 'world', 'start' => 0.7, 'end' => 1.5, 'probability' => 0.88], ], ]); $event = $this->stream->parseEvent($json); $this->assertSame('en', $event['whisper']['language']); $this->assertSame(8.5, $event['whisper']['duration']); $this->assertSame([50364, 2425, 1002], $event['whisper']['segment']['tokens']); $this->assertSame(-0.21, $event['whisper']['segment']['avg_logprob']); $this->assertCount(2, $event['whisper']['segment']['words']); $this->assertSame('Hello', $event['whisper']['segment']['words'][0]['word']); } public function test_parses_openai_delta_logprobs_without_treating_them_as_a_segment(): void { $event = $this->stream->parseEvent(json_encode([ 'type' => 'transcript.text.delta', 'delta' => 'Hel', 'logprobs' => [ ['token' => 'Hel', 'logprob' => -0.05], ], ])); $this->assertSame('Hel', $event['append']); $this->assertArrayNotHasKey('segment', $event['whisper']); $this->assertSame('Hel', $event['whisper']['logprobs'][0]['token']); $this->assertSame(-0.05, $event['whisper']['logprobs'][0]['logprob']); } public function test_applies_delta_then_done_replace(): void { $delta = $this->stream->parseEvent('{"type":"transcript.text.delta","delta":"Hel"}'); $text = $this->stream->applyEvent('', $delta); $delta2 = $this->stream->parseEvent('{"type":"transcript.text.delta","delta":"lo"}'); $text = $this->stream->applyEvent($text, $delta2); $done = $this->stream->parseEvent('{"type":"transcript.text.done","text":"Hello world"}'); $text = $this->stream->applyEvent($text, $done); $this->assertSame('Hello world', $text); } public function test_parses_verbose_json_segments_array(): void { $event = $this->stream->parseEvent(json_encode([ 'task' => 'transcribe', 'language' => 'en', 'duration' => 4.2, 'text' => 'Hello world', 'segments' => [ [ 'id' => 0, 'start' => 0.0, 'end' => 4.2, 'text' => ' Hello world', 'avg_logprob' => -0.3, 'tokens' => [1, 2], 'words' => [ ['word' => ' Hello', 'start' => 0.0, 'end' => 0.5, 'probability' => 0.9], ], ], ], ])); $this->assertSame('Hello world', $event['replace']); $this->assertSame(4.2, $event['end']); $this->assertSame('en', $event['whisper']['language']); $this->assertCount(1, $event['whisper']['segments']); $this->assertSame(-0.3, $event['whisper']['segments'][0]['avg_logprob']); } public function test_joins_legacy_segments_with_spaces(): void { $first = $this->stream->parseEvent('{"text":"Hello from","end":10}'); $second = $this->stream->parseEvent('{"text":"whisper.","end":20}'); $text = $this->stream->applyEvent('', $first); $text = $this->stream->applyEvent($text, $second); $this->assertSame('Hello from whisper.', $text); } public function test_ignores_done_sentinel_and_non_json(): void { $buffer = "data: [DONE]\n\ndata: not-json\n\n"; $payloads = $this->stream->extractPayloads($buffer); $this->assertSame(['not-json'], $payloads); $this->assertNull($this->stream->parseEvent('not-json')); } public function test_flush_buffer_emits_trailing_frame(): void { $buffer = 'data: {"text":"Last"}'; $payloads = $this->stream->flushBuffer($buffer); $this->assertSame(['{"text":"Last"}'], $payloads); $this->assertSame('', $buffer); } public function test_openai_delta_uses_word_end_as_audio_position(): void { $event = $this->stream->parseEvent(json_encode([ 'type' => 'transcript.text.delta', 'delta' => 'Hello', 'words' => [ ['word' => 'Hello', 'start' => 0.0, 'end' => 1.6, 'probability' => 0.9], ], ])); $this->assertSame(1.6, $event['end']); $this->assertSame('Hello', $event['whisper']['words'][0]['word']); } public function test_file_duration_is_not_used_as_audio_position(): void { $event = $this->stream->parseEvent(json_encode([ 'type' => 'transcript.text.delta', 'delta' => 'Hello', 'duration' => 40.0, ])); $this->assertNull($event['end']); $this->assertSame(40.0, $event['whisper']['duration']); } }