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.
142 lines
3.8 KiB
PHP
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;
|
|
}
|
|
}
|