Files
AndyTranscribe/tests/Unit/WhisperTranscriptionStreamTest.php
T
ben 3a9cf50529 Keep live Whisper segments on screen without a page refresh.
Treat streamed chunks as appends, stop Livewire from remorphing the Alpine tree, and hydrate from HTTP while a run is active so segment cards appear as they arrive.
2026-08-13 16:35:33 +02:00

251 lines
9.0 KiB
PHP

<?php
namespace Tests\Unit;
use App\Services\WhisperTranscriptionStream;
use PHPUnit\Framework\TestCase;
class WhisperTranscriptionStreamTest extends TestCase
{
private WhisperTranscriptionStream $stream;
protected function setUp(): void
{
parent::setUp();
$this->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_live_segment_with_nested_segments_array_appends_instead_of_replacing(): void
{
$first = $this->stream->parseEvent(json_encode([
'language' => 'en',
'duration' => 6.3,
'id' => 0,
'start' => 254.5,
'end' => 260.8,
'text' => "didn't I wasn't aware",
'segments' => [
['id' => 0, 'start' => 254.5, 'end' => 260.8, 'text' => "didn't I wasn't aware"],
],
]));
$second = $this->stream->parseEvent(json_encode([
'language' => 'en',
'duration' => 6.96,
'id' => 0,
'start' => 260.8,
'end' => 267.4,
'text' => ' other people so I am trying',
'segments' => [
['id' => 0, 'start' => 260.8, 'end' => 267.4, 'text' => ' other people so I am trying'],
],
]));
$this->assertTrue($first['legacy']);
$this->assertNull($first['replace']);
$this->assertArrayNotHasKey('segments', $first['whisper']);
$this->assertSame("didn't I wasn't aware", $first['whisper']['segment']['text']);
$text = $this->stream->applyEvent('', $first);
$text = $this->stream->applyEvent($text, $second);
$this->assertSame("didn't I wasn't aware other people so I am trying", $text);
}
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']);
}
}