Consume faster-whisper SSE instead of blocking on one JSON response, persist growing text and progress over Reverb, and drop the fake percent heartbeat.
243 lines
10 KiB
PHP
243 lines
10 KiB
PHP
<div
|
|
@if ($recording->isTranscribing())
|
|
wire:poll.2s.visible
|
|
@endif
|
|
>
|
|
<div
|
|
wire:key="transcription-ui-{{ $recording->id }}-{{ $recording->transcription_status }}-{{ $recording->transcription_percent }}-{{ md5((string) $recording->transcription_progress) }}-{{ $recording->transcribed_at?->timestamp }}"
|
|
x-data="transcriptionMonitor(@js([
|
|
'statusUrl' => route('recordings.transcription-status', $recording),
|
|
'initial' => $recording->transcriptionStatusPayload(),
|
|
]))"
|
|
x-init="
|
|
start();
|
|
return () => destroy();
|
|
"
|
|
>
|
|
<div class="mb-6 flex flex-col gap-4 sm:flex-row sm:items-start sm:justify-between">
|
|
<div>
|
|
<flux:link href="{{ route('recordings.index') }}" wire:navigate class="text-sm">← Recordings</flux:link>
|
|
<flux:heading size="xl" class="mt-2">{{ $recording->title }}</flux:heading>
|
|
<div class="mt-2 flex flex-wrap items-center gap-2">
|
|
<x-transcription-status-badge
|
|
:status="$recording->transcription_status"
|
|
:label="$recording->transcriptionStatusLabel()"
|
|
alpine-row="status"
|
|
/>
|
|
<flux:text
|
|
class="text-xs"
|
|
x-show="status.driver_label"
|
|
x-cloak
|
|
x-text="status.driver_label"
|
|
></flux:text>
|
|
</div>
|
|
</div>
|
|
<div class="flex flex-wrap items-center gap-2">
|
|
<flux:button
|
|
type="button"
|
|
variant="primary"
|
|
icon="play"
|
|
@click="$refs.player.paused ? $refs.player.play() : $refs.player.pause()"
|
|
>
|
|
Play
|
|
</flux:button>
|
|
|
|
<flux:modal.trigger name="delete-recording">
|
|
<flux:button type="button" variant="danger">Delete</flux:button>
|
|
</flux:modal.trigger>
|
|
|
|
<flux:modal name="delete-recording" class="max-w-md">
|
|
<div class="space-y-6">
|
|
<div>
|
|
<flux:heading size="lg">Delete recording?</flux:heading>
|
|
<flux:text class="mt-2">
|
|
This permanently removes the recording and its audio file.
|
|
</flux:text>
|
|
</div>
|
|
<div class="flex justify-end gap-2">
|
|
<flux:modal.close>
|
|
<flux:button variant="ghost">Cancel</flux:button>
|
|
</flux:modal.close>
|
|
<flux:button type="button" variant="danger" wire:click="delete">Delete</flux:button>
|
|
</div>
|
|
</div>
|
|
</flux:modal>
|
|
</div>
|
|
</div>
|
|
|
|
<flux:card class="mb-6">
|
|
<flux:heading size="sm" class="uppercase tracking-wide text-zinc-500 dark:text-zinc-400">Audio</flux:heading>
|
|
<audio
|
|
x-ref="player"
|
|
class="mt-4 w-full"
|
|
controls
|
|
preload="metadata"
|
|
src="{{ route('recordings.audio', $recording) }}"
|
|
>
|
|
Your browser does not support audio playback.
|
|
</audio>
|
|
</flux:card>
|
|
|
|
<div class="grid gap-6 lg:grid-cols-2">
|
|
<flux:card>
|
|
<flux:heading size="sm" class="uppercase tracking-wide text-zinc-500 dark:text-zinc-400">Metadata</flux:heading>
|
|
<dl class="mt-4 space-y-3 text-sm">
|
|
<div class="flex justify-between gap-4">
|
|
<dt><flux:text>Original file</flux:text></dt>
|
|
<dd class="text-right font-medium">{{ $recording->original_filename }}</dd>
|
|
</div>
|
|
<div class="flex justify-between gap-4">
|
|
<dt><flux:text>Duration</flux:text></dt>
|
|
<dd class="font-medium">{{ $recording->duration_formatted }}</dd>
|
|
</div>
|
|
<div class="flex justify-between gap-4">
|
|
<dt><flux:text>Artist</flux:text></dt>
|
|
<dd class="font-medium">{{ $recording->artist ?: '—' }}</dd>
|
|
</div>
|
|
<div class="flex justify-between gap-4">
|
|
<dt><flux:text>Album</flux:text></dt>
|
|
<dd class="font-medium">{{ $recording->album ?: '—' }}</dd>
|
|
</div>
|
|
<div class="flex justify-between gap-4">
|
|
<dt><flux:text>Recorded</flux:text></dt>
|
|
<dd class="font-medium">{{ $recording->recorded_at?->format('Y-m-d') ?: '—' }}</dd>
|
|
</div>
|
|
<div class="flex justify-between gap-4">
|
|
<dt><flux:text>Size</flux:text></dt>
|
|
<dd class="font-medium">{{ number_format($recording->file_size_bytes / 1024, 1) }} KB</dd>
|
|
</div>
|
|
<div class="flex justify-between gap-4">
|
|
<dt><flux:text>Uploaded</flux:text></dt>
|
|
<dd class="font-medium">{{ $recording->created_at?->format('Y-m-d H:i') }}</dd>
|
|
</div>
|
|
</dl>
|
|
</flux:card>
|
|
|
|
<flux:card>
|
|
<flux:heading size="sm" class="uppercase tracking-wide text-zinc-500 dark:text-zinc-400">Transcribe</flux:heading>
|
|
<div class="mt-4">
|
|
<flux:button type="button" variant="primary" wire:click="startTranscription">
|
|
<span x-text="startButtonLabel"></span>
|
|
</flux:button>
|
|
</div>
|
|
|
|
<div
|
|
x-show="status.is_active"
|
|
x-cloak
|
|
class="mt-3"
|
|
>
|
|
<flux:button type="button" variant="outline" wire:click="cancelTranscription">
|
|
Stop transcription
|
|
</flux:button>
|
|
</div>
|
|
</flux:card>
|
|
</div>
|
|
|
|
<flux:card class="mt-6">
|
|
<div class="flex items-center justify-between gap-4">
|
|
<flux:heading size="sm" class="uppercase tracking-wide text-zinc-500 dark:text-zinc-400">Transcript</flux:heading>
|
|
<flux:button
|
|
type="button"
|
|
variant="ghost"
|
|
size="sm"
|
|
x-show="!status.is_active && status.has_transcript"
|
|
x-cloak
|
|
@click="navigator.clipboard.writeText(status.transcript || '')"
|
|
>
|
|
Copy
|
|
</flux:button>
|
|
</div>
|
|
|
|
<div x-show="status.is_active" x-cloak class="mt-4">
|
|
<flux:callout variant="warning" icon="arrow-path">
|
|
<flux:callout.heading>
|
|
<span class="inline-flex items-center gap-2">
|
|
<flux:icon.loading
|
|
variant="micro"
|
|
class="size-4"
|
|
x-show="status.status === 'processing'"
|
|
x-cloak
|
|
/>
|
|
<span x-text="status.progress || 'Working…'"></span>
|
|
</span>
|
|
</flux:callout.heading>
|
|
<flux:callout.text>
|
|
<span
|
|
class="tabular-nums"
|
|
x-show="status.status === 'processing' && status.percent != null"
|
|
x-cloak
|
|
>
|
|
<span x-text="status.percent + '%'"></span>
|
|
·
|
|
</span>
|
|
Elapsed <span x-text="status.elapsed_human || '0s'"></span>
|
|
</flux:callout.text>
|
|
</flux:callout>
|
|
|
|
<div class="mt-3" x-show="status.status === 'processing'" x-cloak>
|
|
<flux:progress color="amber" x-bind:value="status.percent || 0" />
|
|
</div>
|
|
|
|
<p
|
|
class="mt-4 whitespace-pre-wrap text-sm leading-relaxed text-zinc-800 dark:text-zinc-100"
|
|
x-show="status.transcript"
|
|
x-cloak
|
|
x-text="status.transcript"
|
|
></p>
|
|
|
|
<ul class="mt-3 space-y-1 text-xs text-zinc-600 dark:text-zinc-400">
|
|
<li>
|
|
Engine:
|
|
<span class="font-medium" x-text="status.driver_label || '—'"></span>
|
|
</li>
|
|
<template x-if="status.duration_seconds">
|
|
<li>
|
|
Audio length:
|
|
<span class="font-medium" x-text="formatDuration(status.duration_seconds)"></span>
|
|
<span class="text-zinc-500">(longer files take longer)</span>
|
|
</li>
|
|
</template>
|
|
<li x-show="pollError" class="text-red-600 dark:text-red-400" x-text="pollError"></li>
|
|
</ul>
|
|
</div>
|
|
|
|
<div x-show="!status.is_active && status.status === 'cancelled'" x-cloak class="mt-4">
|
|
<flux:callout icon="stop-circle">
|
|
<flux:callout.text>
|
|
Transcription stopped. Use the button above to start again.
|
|
</flux:callout.text>
|
|
</flux:callout>
|
|
</div>
|
|
|
|
<div x-show="!status.is_active && status.status === 'failed'" x-cloak class="mt-4">
|
|
<flux:callout variant="danger" icon="exclamation-triangle">
|
|
<flux:callout.heading>Transcription failed</flux:callout.heading>
|
|
<flux:callout.text>
|
|
<span x-text="status.error || 'Check the logs and try again.'"></span>
|
|
</flux:callout.text>
|
|
</flux:callout>
|
|
</div>
|
|
|
|
<div x-show="!status.is_active && status.has_transcript" x-cloak>
|
|
<p class="mt-4 whitespace-pre-wrap text-sm leading-relaxed text-zinc-800 dark:text-zinc-100" x-text="status.transcript"></p>
|
|
<flux:text
|
|
class="mt-4 text-xs"
|
|
x-show="status.transcribed_at"
|
|
x-text="status.transcribed_at
|
|
? ('Transcribed ' + formatTimestamp(status.transcribed_at)
|
|
+ (status.transcription_duration_human ? (' · took ' + status.transcription_duration_human) : ''))
|
|
: ''"
|
|
></flux:text>
|
|
</div>
|
|
|
|
<flux:text
|
|
x-show="!status.is_active && status.status !== 'failed' && status.status !== 'cancelled' && !status.has_transcript"
|
|
x-cloak
|
|
class="mt-4"
|
|
>
|
|
No transcript yet. Transcription starts automatically after upload, or use the button above.
|
|
</flux:text>
|
|
</flux:card>
|
|
</div>
|
|
</div>
|