Files
AndyTranscribe/app/Events/RecordingTranscriptionUpdated.php
T
ben 187b6b5d12 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.
2026-08-13 15:55:51 +02:00

142 lines
3.8 KiB
PHP

<?php
namespace App\Events;
use App\Models\Recording;
use Illuminate\Broadcasting\InteractsWithSockets;
use Illuminate\Broadcasting\PrivateChannel;
use Illuminate\Contracts\Broadcasting\ShouldBroadcastNow;
use Illuminate\Foundation\Events\Dispatchable;
use Illuminate\Queue\SerializesModels;
class RecordingTranscriptionUpdated implements ShouldBroadcastNow
{
use Dispatchable, InteractsWithSockets, SerializesModels;
/**
* Reverb's default max payload is 10KB. Keep streamed text well under that.
*/
public const MAX_DELTA_BYTES = 8_000;
/**
* Create a new event instance.
*
* @param array<string, mixed>|null $whisperDelta
*/
public function __construct(
public Recording $recording,
public ?string $transcriptDelta = null,
public bool $transcriptReplace = false,
public ?array $whisperDelta = null,
) {}
/**
* Get the channels the event should broadcast on.
*
* @return array<int, PrivateChannel>
*/
public function broadcastOn(): array
{
return [
new PrivateChannel('recording.'.$this->recording->id),
new PrivateChannel('user.'.$this->recording->user_id.'.recordings'),
];
}
public function broadcastAs(): string
{
return 'RecordingTranscriptionUpdated';
}
/**
* @return array<string, mixed>
*/
public function broadcastWith(): array
{
$payload = array_merge(
$this->recording->transcriptionStatusPayload(),
[
'word_count' => $this->recording->word_count,
],
);
unset($payload['transcript'], $payload['whisper']);
$delta = $this->transcriptDelta;
if ($delta !== null && $delta !== '' && strlen($delta) <= self::MAX_DELTA_BYTES) {
$payload['transcript_delta'] = $delta;
$payload['transcript_replace'] = $this->transcriptReplace;
}
$whisper = $this->compactWhisperDelta($this->whisperDelta);
if ($whisper !== null) {
$payload['whisper_delta'] = $whisper;
}
return $payload;
}
/**
* @param array<string, mixed>|null $whisper
* @return array<string, mixed>|null
*/
private function compactWhisperDelta(?array $whisper): ?array
{
if ($whisper === null || $whisper === []) {
return null;
}
// Accumulated segment lists belong in HTTP hydrate, not on Reverb.
unset($whisper['segments']);
if ($whisper === []) {
return null;
}
foreach (['tokens', 'logprobs', 'words'] as $drop) {
$encoded = json_encode($whisper);
if (! is_string($encoded) || strlen($encoded) <= self::MAX_DELTA_BYTES) {
return $whisper;
}
$whisper = $this->dropWhisperField($whisper, $drop);
}
$encoded = json_encode($whisper);
if (! is_string($encoded) || strlen($encoded) > self::MAX_DELTA_BYTES) {
return null;
}
return $whisper;
}
/**
* @param array<string, mixed> $whisper
* @return array<string, mixed>
*/
private function dropWhisperField(array $whisper, string $field): array
{
unset($whisper[$field]);
if (isset($whisper['segment']) && is_array($whisper['segment'])) {
unset($whisper['segment'][$field]);
}
if (isset($whisper['segments']) && is_array($whisper['segments'])) {
$whisper['segments'] = array_map(function (mixed $segment) use ($field): mixed {
if (is_array($segment)) {
unset($segment[$field]);
}
return $segment;
}, $whisper['segments']);
}
return $whisper;
}
}