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:
ben
2026-08-13 15:55:51 +02:00
parent 4c73620458
commit 187b6b5d12
16 changed files with 1646 additions and 189 deletions
@@ -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)));
}
}