Persist accumulated verbose_json on the recording and broadcast only transcript/whisper deltas so the show page can render language, segments, word timestamps, and confidence while a run is in progress.
215 lines
7.7 KiB
PHP
215 lines
7.7 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_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']);
|
|
}
|
|
}
|