Stream Whisper verbose metadata live without overflowing Reverb.
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.
This commit is contained in:
@@ -204,6 +204,35 @@ class RecordingUploadTest extends TestCase
|
||||
->assertJsonPath('driver_label', 'Local (faster-whisper)');
|
||||
}
|
||||
|
||||
public function test_transcription_status_endpoint_includes_whisper_snapshot(): void
|
||||
{
|
||||
$recording = Recording::query()->create([
|
||||
'user_id' => $this->user->id,
|
||||
'title' => 'Verbose status',
|
||||
'original_filename' => 'live.mp3',
|
||||
'file_path' => 'recordings/live.mp3',
|
||||
'file_size_bytes' => 100,
|
||||
'transcription_status' => 'processing',
|
||||
'transcription_progress' => 'Transcribing locally…',
|
||||
'transcription_percent' => 40,
|
||||
'transcription_driver' => 'local',
|
||||
'transcription_started_at' => now(),
|
||||
'transcription_verbose' => [
|
||||
'language' => 'en',
|
||||
'duration' => 8.5,
|
||||
'segments' => [
|
||||
['text' => 'Hello', 'start' => 0.0, 'end' => 1.0],
|
||||
],
|
||||
],
|
||||
]);
|
||||
|
||||
$this->getJson(route('recordings.transcription-status', $recording))
|
||||
->assertOk()
|
||||
->assertJsonPath('whisper.language', 'en')
|
||||
->assertJsonPath('whisper.duration', 8.5)
|
||||
->assertJsonPath('whisper.segments.0.text', 'Hello');
|
||||
}
|
||||
|
||||
public function test_transcription_service_calls_local_whisper_http(): void
|
||||
{
|
||||
Storage::fake('local');
|
||||
@@ -232,7 +261,10 @@ class RecordingUploadTest extends TestCase
|
||||
|
||||
return str_contains($request->url(), '/audio/transcriptions')
|
||||
&& str_contains($body, 'name="stream"')
|
||||
&& str_contains($body, 'true');
|
||||
&& str_contains($body, 'true')
|
||||
&& str_contains($body, 'verbose_json')
|
||||
&& str_contains($body, 'timestamp_granularities[]')
|
||||
&& str_contains($body, 'word');
|
||||
});
|
||||
}
|
||||
|
||||
@@ -241,9 +273,9 @@ class RecordingUploadTest extends TestCase
|
||||
Storage::fake('local');
|
||||
Storage::disk('local')->put('recordings/long.mp3', 'fake-audio-bytes');
|
||||
|
||||
$sse = "data: {\"type\":\"transcript.text.delta\",\"delta\":\"Hello \"}\n\n"
|
||||
."data: {\"type\":\"transcript.text.delta\",\"delta\":\"from whisper.\"}\n\n"
|
||||
."data: {\"type\":\"transcript.text.done\",\"text\":\"Hello from whisper.\"}\n\n";
|
||||
$sse = "data: {\"type\":\"transcript.text.delta\",\"delta\":\"Hello \",\"end\":10}\n\n"
|
||||
."data: {\"type\":\"transcript.text.delta\",\"delta\":\"from whisper.\",\"end\":30}\n\n"
|
||||
."data: {\"type\":\"transcript.text.done\",\"text\":\"Hello from whisper.\",\"end\":40}\n\n";
|
||||
|
||||
Http::fake([
|
||||
'*' => Http::response($sse, 200, ['Content-Type' => 'text/event-stream']),
|
||||
@@ -275,7 +307,10 @@ class RecordingUploadTest extends TestCase
|
||||
$this->assertSame('Hello from whisper.', $text);
|
||||
$this->assertNotEmpty(array_filter($partials, fn (array $row): bool => $row['partial'] === 'Hello '));
|
||||
$this->assertSame('Hello from whisper.', $partials[array_key_last($partials)]['partial'] ?? $text);
|
||||
$this->assertGreaterThanOrEqual(50, max(array_column($partials, 'percent')));
|
||||
$this->assertContains(0, array_column($partials, 'percent'));
|
||||
$this->assertContains(25, array_column($partials, 'percent'));
|
||||
$this->assertContains(99, array_column($partials, 'percent'));
|
||||
$this->assertLessThan(100, max(array_column($partials, 'percent')));
|
||||
}
|
||||
|
||||
public function test_transcription_service_streams_legacy_segments_with_timestamp_percent(): void
|
||||
@@ -320,6 +355,157 @@ class RecordingUploadTest extends TestCase
|
||||
$this->assertContains(99, $percents);
|
||||
}
|
||||
|
||||
public function test_transcription_service_streams_verbose_segment_metadata(): void
|
||||
{
|
||||
Storage::fake('local');
|
||||
Storage::disk('local')->put('recordings/long.mp3', 'fake-audio-bytes');
|
||||
|
||||
$segment = [
|
||||
'id' => 0,
|
||||
'start' => 0.0,
|
||||
'end' => 20.0,
|
||||
'text' => 'Hello from whisper.',
|
||||
'tokens' => [50364, 2425],
|
||||
'avg_logprob' => -0.18,
|
||||
'compression_ratio' => 1.2,
|
||||
'no_speech_prob' => 0.02,
|
||||
'language' => 'en',
|
||||
'duration' => 40.0,
|
||||
'words' => [
|
||||
['word' => 'Hello', 'start' => 0.0, 'end' => 0.5, 'probability' => 0.96],
|
||||
],
|
||||
];
|
||||
|
||||
$sse = 'data: '.json_encode($segment)."\n\n";
|
||||
|
||||
Http::fake([
|
||||
'*' => Http::response($sse, 200, ['Content-Type' => 'text/event-stream']),
|
||||
]);
|
||||
|
||||
$recording = Recording::query()->create([
|
||||
'user_id' => $this->user->id,
|
||||
'title' => 'Verbose stream',
|
||||
'original_filename' => 'long.mp3',
|
||||
'file_path' => 'recordings/long.mp3',
|
||||
'file_size_bytes' => 100,
|
||||
'duration_seconds' => 40,
|
||||
'transcription_status' => 'processing',
|
||||
'transcription_driver' => 'local',
|
||||
]);
|
||||
|
||||
$whispers = [];
|
||||
|
||||
$text = app(TranscriptionService::class)->transcribe(
|
||||
$recording,
|
||||
function (string $message, int $percent, ?string $partial = null, ?array $whisper = null) use (&$whispers): void {
|
||||
if ($whisper !== null) {
|
||||
$whispers[] = $whisper;
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
$this->assertSame('Hello from whisper.', $text);
|
||||
$this->assertNotEmpty($whispers);
|
||||
$this->assertSame('en', $whispers[0]['language']);
|
||||
$this->assertSame(-0.18, $whispers[0]['segment']['avg_logprob']);
|
||||
$this->assertSame('Hello', $whispers[0]['segment']['words'][0]['word']);
|
||||
}
|
||||
|
||||
public function test_transcription_service_does_not_invent_percent_when_events_lack_timestamps(): void
|
||||
{
|
||||
Storage::fake('local');
|
||||
Storage::disk('local')->put('recordings/long.mp3', 'fake-audio-bytes');
|
||||
|
||||
$this->freezeTime();
|
||||
|
||||
$sse = "data: {\"type\":\"transcript.text.delta\",\"delta\":\"Hello \"}\n\n"
|
||||
."data: {\"type\":\"transcript.text.delta\",\"delta\":\"from whisper.\"}\n\n";
|
||||
|
||||
Http::fake([
|
||||
'*' => Http::response($sse, 200, ['Content-Type' => 'text/event-stream']),
|
||||
]);
|
||||
|
||||
$recording = Recording::query()->create([
|
||||
'user_id' => $this->user->id,
|
||||
'title' => 'No timestamp percent',
|
||||
'original_filename' => 'long.mp3',
|
||||
'file_path' => 'recordings/long.mp3',
|
||||
'file_size_bytes' => 100,
|
||||
'duration_seconds' => 40,
|
||||
'transcription_status' => 'processing',
|
||||
'transcription_driver' => 'local',
|
||||
'transcription_started_at' => now()->subSeconds(20),
|
||||
]);
|
||||
|
||||
$partials = [];
|
||||
|
||||
$text = app(TranscriptionService::class)->transcribe(
|
||||
$recording,
|
||||
function (string $message, int $percent, ?string $partial = null) use (&$partials): void {
|
||||
$partials[] = [
|
||||
'percent' => $percent,
|
||||
'partial' => $partial,
|
||||
];
|
||||
},
|
||||
);
|
||||
|
||||
$this->assertSame('Hello from whisper.', $text);
|
||||
$this->assertContains('Hello ', array_column($partials, 'partial'));
|
||||
$this->assertContains('Hello from whisper.', array_column($partials, 'partial'));
|
||||
$this->assertSame([0], array_values(array_unique(array_column($partials, 'percent'))));
|
||||
}
|
||||
|
||||
public function test_transcription_service_uses_word_timestamps_for_percent(): void
|
||||
{
|
||||
Storage::fake('local');
|
||||
Storage::disk('local')->put('recordings/long.mp3', 'fake-audio-bytes');
|
||||
|
||||
$sse = 'data: '.json_encode([
|
||||
'type' => 'transcript.text.delta',
|
||||
'delta' => 'Hello ',
|
||||
'words' => [
|
||||
['word' => 'Hello', 'start' => 0.0, 'end' => 10.0],
|
||||
],
|
||||
])."\n\n"
|
||||
.'data: '.json_encode([
|
||||
'type' => 'transcript.text.delta',
|
||||
'delta' => 'from whisper.',
|
||||
'words' => [
|
||||
['word' => 'from', 'start' => 10.0, 'end' => 20.0],
|
||||
['word' => 'whisper.', 'start' => 20.0, 'end' => 30.0],
|
||||
],
|
||||
])."\n\n";
|
||||
|
||||
Http::fake([
|
||||
'*' => Http::response($sse, 200, ['Content-Type' => 'text/event-stream']),
|
||||
]);
|
||||
|
||||
$recording = Recording::query()->create([
|
||||
'user_id' => $this->user->id,
|
||||
'title' => 'Word timestamps',
|
||||
'original_filename' => 'long.mp3',
|
||||
'file_path' => 'recordings/long.mp3',
|
||||
'file_size_bytes' => 100,
|
||||
'duration_seconds' => 40,
|
||||
'transcription_status' => 'processing',
|
||||
'transcription_driver' => 'local',
|
||||
]);
|
||||
|
||||
$percents = [];
|
||||
|
||||
$text = app(TranscriptionService::class)->transcribe(
|
||||
$recording,
|
||||
function (string $message, int $percent, ?string $partial = null) use (&$percents): void {
|
||||
$percents[] = $percent;
|
||||
},
|
||||
);
|
||||
|
||||
$this->assertSame('Hello from whisper.', $text);
|
||||
$this->assertContains(25, $percents);
|
||||
$this->assertContains(75, $percents);
|
||||
$this->assertLessThan(100, max($percents));
|
||||
}
|
||||
|
||||
public function test_report_progress_persists_partial_transcript_without_completing(): void
|
||||
{
|
||||
$recording = Recording::query()->create([
|
||||
|
||||
@@ -125,12 +125,37 @@ class IndexTest extends TestCase
|
||||
|
||||
Livewire::test(Index::class)
|
||||
->assertSee('wire:poll', false)
|
||||
->assertDontSee('echo-private', false)
|
||||
->assertSee('Transcribing')
|
||||
->assertSee('40%')
|
||||
->assertDontSee('Transcribing locally…')
|
||||
->assertSeeHtml('bg-amber-400');
|
||||
}
|
||||
|
||||
public function test_processing_recordings_do_not_show_zero_percent(): void
|
||||
{
|
||||
$user = User::factory()->create();
|
||||
$this->actingAs($user);
|
||||
|
||||
Recording::query()->create([
|
||||
'user_id' => $user->id,
|
||||
'title' => 'Just started',
|
||||
'original_filename' => 'fresh.mp3',
|
||||
'file_path' => 'recordings/fresh.mp3',
|
||||
'file_size_bytes' => 100,
|
||||
'transcription_status' => 'processing',
|
||||
'transcription_progress' => 'Transcribing locally…',
|
||||
'transcription_percent' => 0,
|
||||
'transcription_driver' => 'local',
|
||||
'transcription_started_at' => now(),
|
||||
]);
|
||||
|
||||
Livewire::test(Index::class)
|
||||
->assertSee('Just started')
|
||||
->assertSee('Transcribing')
|
||||
->assertDontSee('0%');
|
||||
}
|
||||
|
||||
public function test_queued_recordings_do_not_show_fake_percent(): void
|
||||
{
|
||||
$user = User::factory()->create();
|
||||
|
||||
@@ -60,7 +60,7 @@ class ShowTest extends TestCase
|
||||
$this->assertDatabaseMissing('recordings', ['id' => $recording->id]);
|
||||
}
|
||||
|
||||
public function test_show_polls_while_transcription_is_active(): void
|
||||
public function test_show_polls_while_transcription_is_queued(): void
|
||||
{
|
||||
$user = User::factory()->create();
|
||||
$this->actingAs($user);
|
||||
@@ -71,7 +71,7 @@ class ShowTest extends TestCase
|
||||
'original_filename' => 'active.mp3',
|
||||
'file_path' => 'recordings/active.mp3',
|
||||
'file_size_bytes' => 100,
|
||||
'transcription_status' => 'processing',
|
||||
'transcription_status' => 'pending',
|
||||
'transcription_progress' => 'Queued — waiting to start…',
|
||||
'transcription_percent' => null,
|
||||
'transcription_driver' => 'local',
|
||||
@@ -80,9 +80,33 @@ class ShowTest extends TestCase
|
||||
|
||||
Livewire::test(Show::class, ['recording' => $recording])
|
||||
->assertSee('wire:poll', false)
|
||||
->assertDontSee('echo-private', false)
|
||||
->assertSee('Queued — waiting to start…');
|
||||
}
|
||||
|
||||
public function test_show_does_not_poll_while_transcription_is_processing(): void
|
||||
{
|
||||
$user = User::factory()->create();
|
||||
$this->actingAs($user);
|
||||
|
||||
$recording = Recording::query()->create([
|
||||
'user_id' => $user->id,
|
||||
'title' => 'Live stream',
|
||||
'original_filename' => 'active.mp3',
|
||||
'file_path' => 'recordings/active.mp3',
|
||||
'file_size_bytes' => 100,
|
||||
'transcription_status' => 'processing',
|
||||
'transcription_progress' => 'Transcribing locally…',
|
||||
'transcription_percent' => 20,
|
||||
'transcription_driver' => 'local',
|
||||
'transcription_started_at' => now(),
|
||||
]);
|
||||
|
||||
Livewire::test(Show::class, ['recording' => $recording])
|
||||
->assertDontSee('wire:poll', false)
|
||||
->assertSee('userId', false);
|
||||
}
|
||||
|
||||
public function test_show_page_includes_partial_transcript_while_processing(): void
|
||||
{
|
||||
$user = User::factory()->create();
|
||||
@@ -107,6 +131,44 @@ class ShowTest extends TestCase
|
||||
->assertSee('status.transcript', false);
|
||||
}
|
||||
|
||||
public function test_show_page_includes_live_whisper_segment_bindings(): void
|
||||
{
|
||||
$user = User::factory()->create();
|
||||
$this->actingAs($user);
|
||||
|
||||
$recording = Recording::query()->create([
|
||||
'user_id' => $user->id,
|
||||
'title' => 'Verbose live',
|
||||
'original_filename' => 'active.mp3',
|
||||
'file_path' => 'recordings/active.mp3',
|
||||
'file_size_bytes' => 100,
|
||||
'transcription_status' => 'processing',
|
||||
'transcription_driver' => 'local',
|
||||
'transcription_started_at' => now(),
|
||||
'transcript' => 'Hello',
|
||||
'transcription_verbose' => [
|
||||
'language' => 'en',
|
||||
'duration' => 4.0,
|
||||
'segments' => [
|
||||
[
|
||||
'text' => 'Hello',
|
||||
'start' => 0.0,
|
||||
'end' => 1.0,
|
||||
'avg_logprob' => -0.1,
|
||||
'words' => [
|
||||
['word' => 'Hello', 'start' => 0.0, 'end' => 1.0, 'probability' => 0.9],
|
||||
],
|
||||
],
|
||||
],
|
||||
],
|
||||
]);
|
||||
|
||||
Livewire::test(Show::class, ['recording' => $recording])
|
||||
->assertSee('whisper.segments', false)
|
||||
->assertSee('whisper.language', false)
|
||||
->assertSee('Hello');
|
||||
}
|
||||
|
||||
public function test_show_does_not_poll_when_transcription_is_idle(): void
|
||||
{
|
||||
$user = User::factory()->create();
|
||||
@@ -126,7 +188,7 @@ class ShowTest extends TestCase
|
||||
->assertDontSee('wire:poll', false);
|
||||
}
|
||||
|
||||
public function test_show_refreshes_recording_on_transcription_broadcast(): void
|
||||
public function test_show_poll_refresh_picks_up_processing_status(): void
|
||||
{
|
||||
$user = User::factory()->create();
|
||||
$this->actingAs($user);
|
||||
@@ -152,12 +214,68 @@ class ShowTest extends TestCase
|
||||
]);
|
||||
|
||||
$component
|
||||
->call('onTranscriptionUpdated', [
|
||||
'id' => $recording->id,
|
||||
'status' => 'processing',
|
||||
])
|
||||
->call('$refresh')
|
||||
->assertSet('recording.transcription_status', 'processing')
|
||||
->assertSet('recording.transcription_percent', 40)
|
||||
->assertSee('Transcribing locally…');
|
||||
}
|
||||
|
||||
public function test_show_poll_refresh_picks_up_completed_transcript(): void
|
||||
{
|
||||
$user = User::factory()->create();
|
||||
$this->actingAs($user);
|
||||
|
||||
$recording = Recording::query()->create([
|
||||
'user_id' => $user->id,
|
||||
'title' => 'Almost done',
|
||||
'original_filename' => 'live.mp3',
|
||||
'file_path' => 'recordings/live.mp3',
|
||||
'file_size_bytes' => 100,
|
||||
'transcription_status' => 'processing',
|
||||
'transcription_progress' => 'Transcribing locally…',
|
||||
'transcription_percent' => 90,
|
||||
'transcription_driver' => 'local',
|
||||
'transcription_started_at' => now(),
|
||||
'transcript' => 'Partial',
|
||||
]);
|
||||
|
||||
$component = Livewire::test(Show::class, ['recording' => $recording]);
|
||||
|
||||
$recording->update([
|
||||
'transcription_status' => 'done',
|
||||
'transcription_percent' => 100,
|
||||
'transcript' => 'Finished live transcript',
|
||||
'transcribed_at' => now(),
|
||||
]);
|
||||
|
||||
$component
|
||||
->call('$refresh')
|
||||
->assertSet('recording.transcription_status', 'done')
|
||||
->assertSet('recording.transcription_percent', 100)
|
||||
->assertSee('Finished live transcript');
|
||||
}
|
||||
|
||||
public function test_show_alpine_root_key_does_not_include_percent(): void
|
||||
{
|
||||
$user = User::factory()->create();
|
||||
$this->actingAs($user);
|
||||
|
||||
$recording = Recording::query()->create([
|
||||
'user_id' => $user->id,
|
||||
'title' => 'Stable key',
|
||||
'original_filename' => 'live.mp3',
|
||||
'file_path' => 'recordings/live.mp3',
|
||||
'file_size_bytes' => 100,
|
||||
'transcription_status' => 'processing',
|
||||
'transcription_progress' => 'Transcribing locally…',
|
||||
'transcription_percent' => 62,
|
||||
'transcription_driver' => 'local',
|
||||
'transcription_started_at' => now(),
|
||||
]);
|
||||
|
||||
Livewire::test(Show::class, ['recording' => $recording])
|
||||
->assertSee('wire:ignore.self', false)
|
||||
->assertSee('transcription-ui-'.$recording->id.'-processing-', false)
|
||||
->assertDontSee('transcription-ui-'.$recording->id.'-processing-62', false);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -160,4 +160,192 @@ class TranscriptionBroadcastTest extends TestCase
|
||||
$this->assertSame('private-recording.'.$recording->id, $event->broadcastOn()[0]->name);
|
||||
$this->assertSame('private-user.'.$this->user->id.'.recordings', $event->broadcastOn()[1]->name);
|
||||
}
|
||||
|
||||
public function test_broadcast_payload_omits_transcript_text(): void
|
||||
{
|
||||
$recording = Recording::query()->create([
|
||||
'user_id' => $this->user->id,
|
||||
'title' => 'Long text',
|
||||
'original_filename' => 'long.mp3',
|
||||
'file_path' => 'recordings/long.mp3',
|
||||
'file_size_bytes' => 100,
|
||||
'transcription_status' => 'processing',
|
||||
'transcription_percent' => 70,
|
||||
'transcript' => str_repeat('word ', 5000),
|
||||
]);
|
||||
|
||||
$payload = (new RecordingTranscriptionUpdated($recording))->broadcastWith();
|
||||
|
||||
$this->assertArrayNotHasKey('transcript', $payload);
|
||||
$this->assertArrayNotHasKey('whisper', $payload);
|
||||
$this->assertTrue($payload['has_transcript']);
|
||||
$this->assertSame(70, $payload['percent']);
|
||||
$this->assertLessThan(10_000, strlen((string) json_encode($payload)));
|
||||
}
|
||||
|
||||
public function test_report_progress_broadcasts_transcript_deltas_for_append(): void
|
||||
{
|
||||
Event::fake([RecordingTranscriptionUpdated::class]);
|
||||
|
||||
$recording = Recording::query()->create([
|
||||
'user_id' => $this->user->id,
|
||||
'title' => 'Deltas',
|
||||
'original_filename' => 'delta.mp3',
|
||||
'file_path' => 'recordings/delta.mp3',
|
||||
'file_size_bytes' => 100,
|
||||
'transcription_status' => 'processing',
|
||||
'transcription_driver' => 'local',
|
||||
'transcription_started_at' => now(),
|
||||
'transcript' => 'Previous finished transcript',
|
||||
]);
|
||||
|
||||
$recording->reportProgress('Transcribing locally…', 55, partialTranscript: 'Hello ');
|
||||
|
||||
Event::assertDispatched(RecordingTranscriptionUpdated::class, function (RecordingTranscriptionUpdated $event): bool {
|
||||
$payload = $event->broadcastWith();
|
||||
|
||||
return ($payload['transcript_delta'] ?? null) === 'Hello '
|
||||
&& ($payload['transcript_replace'] ?? false) === true
|
||||
&& ! array_key_exists('transcript', $payload);
|
||||
});
|
||||
|
||||
$recording->refresh()->reportProgress('Transcribing locally…', 60, partialTranscript: 'Hello from whisper.');
|
||||
|
||||
Event::assertDispatched(RecordingTranscriptionUpdated::class, function (RecordingTranscriptionUpdated $event): bool {
|
||||
$payload = $event->broadcastWith();
|
||||
|
||||
return ($payload['transcript_delta'] ?? null) === 'from whisper.'
|
||||
&& ($payload['transcript_replace'] ?? true) === false;
|
||||
});
|
||||
}
|
||||
|
||||
public function test_report_progress_broadcasts_whisper_delta_without_full_verbose_snapshot(): void
|
||||
{
|
||||
Event::fake([RecordingTranscriptionUpdated::class]);
|
||||
|
||||
$recording = Recording::query()->create([
|
||||
'user_id' => $this->user->id,
|
||||
'title' => 'Verbose live',
|
||||
'original_filename' => 'delta.mp3',
|
||||
'file_path' => 'recordings/delta.mp3',
|
||||
'file_size_bytes' => 100,
|
||||
'transcription_status' => 'processing',
|
||||
'transcription_driver' => 'local',
|
||||
'transcription_started_at' => now(),
|
||||
]);
|
||||
|
||||
$recording->reportProgress('Transcribing locally…', 40, partialTranscript: 'Hello', whisperDelta: [
|
||||
'language' => 'en',
|
||||
'duration' => 12.5,
|
||||
'segment' => [
|
||||
'id' => 0,
|
||||
'start' => 0.0,
|
||||
'end' => 1.2,
|
||||
'text' => 'Hello',
|
||||
'avg_logprob' => -0.2,
|
||||
'words' => [
|
||||
['word' => 'Hello', 'start' => 0.0, 'end' => 1.2, 'probability' => 0.91],
|
||||
],
|
||||
],
|
||||
]);
|
||||
|
||||
$recording->refresh();
|
||||
$this->assertSame('en', $recording->transcription_verbose['language']);
|
||||
$this->assertCount(1, $recording->transcription_verbose['segments']);
|
||||
|
||||
Event::assertDispatched(RecordingTranscriptionUpdated::class, function (RecordingTranscriptionUpdated $event): bool {
|
||||
$payload = $event->broadcastWith();
|
||||
|
||||
return ($payload['whisper_delta']['language'] ?? null) === 'en'
|
||||
&& ($payload['whisper_delta']['segment']['text'] ?? null) === 'Hello'
|
||||
&& ! array_key_exists('whisper', $payload)
|
||||
&& ! array_key_exists('transcript', $payload)
|
||||
&& ! array_key_exists('segments', $payload['whisper_delta'] ?? []);
|
||||
});
|
||||
}
|
||||
|
||||
public function test_full_whisper_segments_snapshot_is_persisted_but_not_broadcast(): void
|
||||
{
|
||||
Event::fake([RecordingTranscriptionUpdated::class]);
|
||||
|
||||
$recording = Recording::query()->create([
|
||||
'user_id' => $this->user->id,
|
||||
'title' => 'Snapshot',
|
||||
'original_filename' => 'snap.mp3',
|
||||
'file_path' => 'recordings/snap.mp3',
|
||||
'file_size_bytes' => 100,
|
||||
'transcription_status' => 'processing',
|
||||
'transcription_driver' => 'local',
|
||||
'transcription_started_at' => now(),
|
||||
]);
|
||||
|
||||
$recording->reportProgress('Transcribing locally…', 90, partialTranscript: 'Hello world', whisperDelta: [
|
||||
'language' => 'en',
|
||||
'duration' => 4.2,
|
||||
'segments' => [
|
||||
['id' => 0, 'text' => 'Hello', 'start' => 0.0, 'end' => 1.0],
|
||||
['id' => 1, 'text' => 'world', 'start' => 1.0, 'end' => 2.0],
|
||||
],
|
||||
]);
|
||||
|
||||
$recording->refresh();
|
||||
$this->assertCount(2, $recording->transcription_verbose['segments']);
|
||||
|
||||
Event::assertDispatched(RecordingTranscriptionUpdated::class, function (RecordingTranscriptionUpdated $event): bool {
|
||||
$payload = $event->broadcastWith();
|
||||
|
||||
return ($payload['whisper_delta']['language'] ?? null) === 'en'
|
||||
&& ($payload['whisper_delta']['duration'] ?? null) === 4.2
|
||||
&& ! array_key_exists('segments', $payload['whisper_delta'] ?? [])
|
||||
&& ! array_key_exists('whisper', $payload);
|
||||
});
|
||||
}
|
||||
|
||||
public function test_report_progress_replaces_verbose_segments_when_snapshot_arrives(): void
|
||||
{
|
||||
Event::fake([RecordingTranscriptionUpdated::class]);
|
||||
|
||||
$recording = Recording::query()->create([
|
||||
'user_id' => $this->user->id,
|
||||
'title' => 'Replace segments',
|
||||
'original_filename' => 'replace.mp3',
|
||||
'file_path' => 'recordings/replace.mp3',
|
||||
'file_size_bytes' => 100,
|
||||
'transcription_status' => 'processing',
|
||||
'transcription_driver' => 'local',
|
||||
'transcription_started_at' => now(),
|
||||
]);
|
||||
|
||||
$recording->reportProgress('Transcribing locally…', 40, whisperDelta: [
|
||||
'segment' => ['id' => 0, 'text' => 'Hello', 'start' => 0.0, 'end' => 1.0],
|
||||
]);
|
||||
$recording->refresh()->reportProgress('Transcribing locally…', 99, whisperDelta: [
|
||||
'language' => 'en',
|
||||
'segments' => [
|
||||
['id' => 0, 'text' => 'Hello', 'start' => 0.0, 'end' => 1.0, 'avg_logprob' => -0.1],
|
||||
],
|
||||
]);
|
||||
|
||||
$this->assertCount(1, $recording->refresh()->transcription_verbose['segments']);
|
||||
$this->assertSame(-0.1, $recording->transcription_verbose['segments'][0]['avg_logprob']);
|
||||
}
|
||||
|
||||
public function test_oversized_transcript_delta_is_not_broadcast(): void
|
||||
{
|
||||
$recording = Recording::query()->create([
|
||||
'user_id' => $this->user->id,
|
||||
'title' => 'Huge delta',
|
||||
'original_filename' => 'huge.mp3',
|
||||
'file_path' => 'recordings/huge.mp3',
|
||||
'file_size_bytes' => 100,
|
||||
'transcription_status' => 'processing',
|
||||
]);
|
||||
|
||||
$huge = str_repeat('a', RecordingTranscriptionUpdated::MAX_DELTA_BYTES + 1);
|
||||
$payload = (new RecordingTranscriptionUpdated($recording, $huge, true))->broadcastWith();
|
||||
|
||||
$this->assertArrayNotHasKey('transcript_delta', $payload);
|
||||
$this->assertArrayNotHasKey('transcript', $payload);
|
||||
$this->assertLessThan(10_000, strlen((string) json_encode($payload)));
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user