Fix screen-share tile recovery and make AI summaries use full transcripts.
Deploy Ladill Meet / deploy (push) Successful in 1m17s
Deploy Ladill Meet / deploy (push) Successful in 1m17s
Defer summarization until recordings finish, merge chat with deduplicated live captions, and clear the screen-share overlay so participant cards return after sharing ends. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -2,29 +2,43 @@
|
||||
|
||||
namespace App\Services\Meet;
|
||||
|
||||
use App\Jobs\GenerateAiSummary;
|
||||
use App\Models\Recording;
|
||||
use App\Models\Session;
|
||||
use App\Models\Transcript;
|
||||
use App\Jobs\GenerateAiSummary;
|
||||
|
||||
class TranscriptService
|
||||
{
|
||||
public function ensureForSession(Session $session, ?Recording $recording = null): Transcript
|
||||
public function getOrCreateForSession(Session $session, ?Recording $recording = null): Transcript
|
||||
{
|
||||
$existing = $session->transcripts()->where('status', '!=', 'failed')->latest()->first();
|
||||
if ($existing) {
|
||||
if ($recording && ! $existing->recording_id) {
|
||||
$existing->update(['recording_id' => $recording->id]);
|
||||
}
|
||||
|
||||
return $existing;
|
||||
}
|
||||
|
||||
$transcript = Transcript::create([
|
||||
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),
|
||||
]);
|
||||
}
|
||||
|
||||
$this->populateFromChat($transcript, $session);
|
||||
/** @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);
|
||||
|
||||
@@ -38,9 +52,11 @@ class TranscriptService
|
||||
'speaker' => $speaker,
|
||||
'text' => trim($text),
|
||||
'at' => now()->toIso8601String(),
|
||||
'source' => 'caption',
|
||||
];
|
||||
|
||||
$content = collect($segments)->map(fn ($s) => ($s['speaker'] ?? 'Speaker').': '.($s['text'] ?? ''))->implode("\n");
|
||||
$segments = $this->compactCaptionSegments($segments);
|
||||
$content = $this->segmentsToContent($segments);
|
||||
|
||||
$transcript->update([
|
||||
'segments' => $segments,
|
||||
@@ -51,21 +67,128 @@ class TranscriptService
|
||||
return $transcript;
|
||||
}
|
||||
|
||||
protected function populateFromChat(Transcript $transcript, Session $session): void
|
||||
public function refreshContent(Transcript $transcript, Session $session): void
|
||||
{
|
||||
$messages = $session->messages()->orderBy('created_at')->get();
|
||||
$segments = $messages->map(fn ($m) => [
|
||||
'speaker' => $m->sender_name,
|
||||
'text' => $m->body,
|
||||
'at' => $m->created_at->toIso8601String(),
|
||||
])->all();
|
||||
$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();
|
||||
|
||||
$content = $messages->map(fn ($m) => $m->sender_name.': '.$m->body)->implode("\n");
|
||||
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 ?: 'No transcript content available.',
|
||||
'content' => $content !== '' ? $content : 'No transcript content available.',
|
||||
'status' => 'ready',
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<int, array<string, mixed>> $segments
|
||||
* @return array<int, array<string, mixed>>
|
||||
*/
|
||||
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<int, array<string, mixed>> $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<int, array<string, mixed>> $segments
|
||||
*/
|
||||
protected function segmentsToContent(array $segments): string
|
||||
{
|
||||
return collect($segments)
|
||||
->map(fn ($segment) => ($segment['speaker'] ?? 'Speaker').': '.($segment['text'] ?? ''))
|
||||
->implode("\n");
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user