transcripts()->where('status', '!=', 'failed')->latest()->first(); if ($existing) { if ($recording && ! $existing->recording_id) { $existing->update(['recording_id' => $recording->id]); } return $existing; } return Transcript::create([ 'owner_ref' => $session->owner_ref, 'session_id' => $session->id, 'recording_id' => $recording?->id, 'status' => 'processing', 'live_captions_enabled' => (bool) $session->room->setting('live_captions', false), ]); } /** @deprecated Use finalizeAndSummarize() when a recording is ready. */ public function ensureForSession(Session $session, ?Recording $recording = null): Transcript { return $this->finalizeAndSummarize($session, $recording); } public function finalizeAndSummarize(Session $session, ?Recording $recording = null): Transcript { $transcript = $this->getOrCreateForSession($session, $recording); $this->refreshContent($transcript, $session); GenerateAiSummary::dispatch($transcript->id); return $transcript->fresh(); } public function appendSegment(Transcript $transcript, string $speaker, string $text): Transcript { $segments = $transcript->segments ?? []; $segments[] = [ 'speaker' => $speaker, 'text' => trim($text), 'at' => now()->toIso8601String(), 'source' => 'caption', ]; $segments = $this->compactCaptionSegments($segments); $content = $this->segmentsToContent($segments); $transcript->update([ 'segments' => $segments, 'content' => $content, 'status' => 'ready', ]); return $transcript; } public function refreshContent(Transcript $transcript, Session $session): void { $segments = $session->messages() ->orderBy('created_at') ->get() ->map(fn ($message) => [ 'speaker' => $message->sender_name, 'text' => $message->body, 'at' => $message->created_at->toIso8601String(), 'source' => 'chat', ]) ->all(); foreach ($transcript->segments ?? [] as $segment) { $source = $segment['source'] ?? null; if ($source === 'chat') { continue; } $text = trim((string) ($segment['text'] ?? '')); if ($text === '') { continue; } $segments[] = array_merge($segment, ['source' => 'caption']); } $segments = $this->compactCaptionSegments($segments); usort($segments, fn (array $a, array $b) => strcmp((string) ($a['at'] ?? ''), (string) ($b['at'] ?? ''))); $content = $this->segmentsToContent($segments); $transcript->update([ 'segments' => $segments, 'content' => $content !== '' ? $content : 'No transcript content available.', 'status' => 'ready', ]); } /** * @param array> $segments * @return array> */ protected function compactCaptionSegments(array $segments): array { $compact = []; foreach ($segments as $segment) { if (($segment['source'] ?? '') === 'chat') { $compact[] = $segment; continue; } $speaker = (string) ($segment['speaker'] ?? 'Speaker'); $text = trim((string) ($segment['text'] ?? '')); if ($text === '') { continue; } $lastCaptionIndex = $this->lastCaptionIndex($compact); if ($lastCaptionIndex !== null && ($compact[$lastCaptionIndex]['speaker'] ?? '') === $speaker) { $lastText = trim((string) ($compact[$lastCaptionIndex]['text'] ?? '')); if ($this->captionTextsEquivalent($lastText, $text)) { continue; } if ($lastText !== '' && $this->captionTextExtends($lastText, $text)) { $compact[$lastCaptionIndex] = $segment; continue; } if ($lastText !== '' && $this->captionTextExtends($text, $lastText)) { continue; } } $compact[] = $segment; } return $compact; } /** * @param array> $segments */ protected function lastCaptionIndex(array $segments): ?int { for ($index = count($segments) - 1; $index >= 0; $index -= 1) { if (($segments[$index]['source'] ?? '') !== 'chat') { return $index; } } return null; } protected function captionTextsEquivalent(string $left, string $right): bool { return mb_strtolower(trim($left)) === mb_strtolower(trim($right)); } protected function captionTextExtends(string $shorter, string $longer): bool { $shorter = mb_strtolower(trim($shorter)); $longer = mb_strtolower(trim($longer)); if ($shorter === '' || $longer === '') { return false; } return str_starts_with($longer, $shorter); } /** * @param array> $segments */ protected function segmentsToContent(array $segments): string { return collect($segments) ->map(fn ($segment) => ($segment['speaker'] ?? 'Speaker').': '.($segment['text'] ?? '')) ->implode("\n"); } }