Fix stuck transcriptions and make transcripts stoppable and searchable.
Raise queue retry_after above the job timeout, recover orphaned runs, allow stop/restart with any engine, and keep finished transcripts searchable in the recordings list.
This commit is contained in:
@@ -78,13 +78,13 @@
|
||||
|
||||
<section class="rounded border border-stone-200 bg-white p-6 shadow-sm">
|
||||
<h2 class="text-sm font-semibold uppercase tracking-wide text-stone-500">Transcribe</h2>
|
||||
<p class="mt-2 text-sm text-stone-600">Choose how to convert this recording to text.</p>
|
||||
<p class="mt-2 text-sm text-stone-600">Choose an engine anytime — starting a new run stops the current one.</p>
|
||||
|
||||
<form method="POST" action="{{ route('recordings.transcribe', $recording) }}" class="mt-4 space-y-4">
|
||||
@csrf
|
||||
|
||||
<label class="flex cursor-pointer gap-3 rounded border border-stone-200 p-3 hover:bg-stone-50">
|
||||
<input type="radio" name="driver" value="cloud" x-model="driver" class="mt-1 text-teal-700 focus:ring-teal-600" :disabled="status.is_active">
|
||||
<input type="radio" name="driver" value="cloud" x-model="driver" class="mt-1 text-teal-700 focus:ring-teal-600">
|
||||
<span>
|
||||
<span class="block text-sm font-medium">Cloud (OpenAI Whisper)</span>
|
||||
<span class="block text-xs text-stone-500">Fast; audio is sent to OpenAI.</span>
|
||||
@@ -92,7 +92,7 @@
|
||||
</label>
|
||||
|
||||
<label class="flex cursor-pointer gap-3 rounded border border-stone-200 p-3 hover:bg-stone-50">
|
||||
<input type="radio" name="driver" value="local" x-model="driver" class="mt-1 text-teal-700 focus:ring-teal-600" :disabled="status.is_active">
|
||||
<input type="radio" name="driver" value="local" x-model="driver" class="mt-1 text-teal-700 focus:ring-teal-600">
|
||||
<span>
|
||||
<span class="block text-sm font-medium">Local — confidential (faster-whisper)</span>
|
||||
<span class="block text-xs text-stone-500">Runs via Docker Compose (<code class="text-[11px]">docker compose up -d whisper</code>) on port 8090.</span>
|
||||
@@ -100,7 +100,7 @@
|
||||
</label>
|
||||
|
||||
<label class="flex cursor-pointer gap-3 rounded border border-stone-200 p-3 hover:bg-stone-50">
|
||||
<input type="radio" name="driver" value="ollama" x-model="driver" class="mt-1 text-teal-700 focus:ring-teal-600" :disabled="status.is_active">
|
||||
<input type="radio" name="driver" value="ollama" x-model="driver" class="mt-1 text-teal-700 focus:ring-teal-600">
|
||||
<span>
|
||||
<span class="block text-sm font-medium">Ollama host</span>
|
||||
<span class="block text-xs text-stone-500">
|
||||
@@ -117,17 +117,33 @@
|
||||
name="ollama_url"
|
||||
value="{{ old('ollama_url', $recording->ollama_url) }}"
|
||||
placeholder="http://192.168.1.50:8000"
|
||||
:disabled="status.is_active"
|
||||
class="w-full rounded border border-stone-300 px-3 py-2 text-sm shadow-sm focus:border-teal-600 focus:outline-none focus:ring-1 focus:ring-teal-600 disabled:opacity-50"
|
||||
class="w-full rounded border border-stone-300 px-3 py-2 text-sm shadow-sm focus:border-teal-600 focus:outline-none focus:ring-1 focus:ring-teal-600"
|
||||
>
|
||||
</div>
|
||||
|
||||
<div class="flex flex-wrap items-center gap-3">
|
||||
<button
|
||||
type="submit"
|
||||
class="rounded bg-teal-700 px-4 py-2 text-sm font-medium text-white hover:bg-teal-800"
|
||||
>
|
||||
<span x-text="startButtonLabel"></span>
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
<form
|
||||
method="POST"
|
||||
action="{{ route('recordings.transcribe.cancel', $recording) }}"
|
||||
x-show="status.is_active"
|
||||
x-cloak
|
||||
class="mt-3"
|
||||
>
|
||||
@csrf
|
||||
<button
|
||||
type="submit"
|
||||
:disabled="status.is_active"
|
||||
class="rounded bg-teal-700 px-4 py-2 text-sm font-medium text-white hover:bg-teal-800 disabled:cursor-not-allowed disabled:opacity-50"
|
||||
class="rounded border border-stone-300 bg-white px-4 py-2 text-sm font-medium text-stone-800 hover:bg-stone-50"
|
||||
>
|
||||
<span x-text="status.has_transcript ? 'Re-transcribe' : 'Start transcription'"></span>
|
||||
Stop transcription
|
||||
</button>
|
||||
</form>
|
||||
</section>
|
||||
@@ -136,16 +152,15 @@
|
||||
<section class="mt-6 rounded border border-stone-200 bg-white p-6 shadow-sm">
|
||||
<div class="flex items-center justify-between gap-4">
|
||||
<h2 class="text-sm font-semibold uppercase tracking-wide text-stone-500">Transcript</h2>
|
||||
@if ($recording->transcript)
|
||||
<button
|
||||
type="button"
|
||||
x-show="!status.is_active && status.has_transcript"
|
||||
onclick="navigator.clipboard.writeText(@js($recording->transcript))"
|
||||
class="text-sm text-teal-700 hover:underline"
|
||||
>
|
||||
Copy
|
||||
</button>
|
||||
@endif
|
||||
<button
|
||||
type="button"
|
||||
x-show="!status.is_active && status.has_transcript"
|
||||
x-cloak
|
||||
@click="navigator.clipboard.writeText(status.transcript || '')"
|
||||
class="text-sm text-teal-700 hover:underline"
|
||||
>
|
||||
Copy
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div x-show="status.is_active" x-cloak class="mt-4 space-y-3 rounded border border-amber-200 bg-amber-50 p-4">
|
||||
@@ -181,26 +196,31 @@
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div x-show="!status.is_active && status.status === 'cancelled'" x-cloak class="mt-4 rounded border border-stone-200 bg-stone-50 p-4 text-sm text-stone-700">
|
||||
Transcription stopped. Choose an engine above to start again.
|
||||
</div>
|
||||
|
||||
<div x-show="!status.is_active && status.status === 'failed'" x-cloak class="mt-4 space-y-2 rounded border border-red-200 bg-red-50 p-4 text-sm text-red-800">
|
||||
<p class="font-medium">Transcription failed</p>
|
||||
<p x-text="status.error || 'Check the logs and try again with another engine.'"></p>
|
||||
</div>
|
||||
|
||||
@if ($recording->transcript)
|
||||
<div x-show="!status.is_active && status.has_transcript">
|
||||
<p class="mt-4 whitespace-pre-wrap text-sm leading-relaxed text-stone-800">{{ $recording->transcript }}</p>
|
||||
@if ($recording->transcribed_at)
|
||||
<p class="mt-4 text-xs text-stone-500">Transcribed {{ $recording->transcribed_at->format('Y-m-d H:i') }}</p>
|
||||
@endif
|
||||
</div>
|
||||
@else
|
||||
<div x-show="!status.is_active && status.has_transcript" x-cloak>
|
||||
<p class="mt-4 whitespace-pre-wrap text-sm leading-relaxed text-stone-800" x-text="status.transcript"></p>
|
||||
<p
|
||||
x-show="!status.is_active && status.status !== 'failed' && !status.has_transcript"
|
||||
class="mt-4 text-sm text-stone-500"
|
||||
>
|
||||
No transcript yet. Choose an engine above to start.
|
||||
</p>
|
||||
@endif
|
||||
class="mt-4 text-xs text-stone-500"
|
||||
x-show="status.transcribed_at"
|
||||
x-text="status.transcribed_at ? ('Transcribed ' + formatTimestamp(status.transcribed_at)) : ''"
|
||||
></p>
|
||||
</div>
|
||||
|
||||
<p
|
||||
x-show="!status.is_active && status.status !== 'failed' && status.status !== 'cancelled' && !status.has_transcript"
|
||||
x-cloak
|
||||
class="mt-4 text-sm text-stone-500"
|
||||
>
|
||||
No transcript yet. Choose an engine above to start.
|
||||
</p>
|
||||
</section>
|
||||
</div>
|
||||
|
||||
@@ -222,11 +242,20 @@
|
||||
processing: 'bg-amber-50 text-amber-800 ring-amber-600/20',
|
||||
pending: 'bg-amber-50 text-amber-800 ring-amber-600/20',
|
||||
failed: 'bg-red-50 text-red-800 ring-red-600/20',
|
||||
cancelled: 'bg-stone-100 text-stone-700 ring-stone-500/20',
|
||||
};
|
||||
|
||||
return map[this.status.status] || 'bg-stone-100 text-stone-700 ring-stone-500/20';
|
||||
},
|
||||
|
||||
get startButtonLabel() {
|
||||
if (this.status.is_active) {
|
||||
return 'Switch engine / restart';
|
||||
}
|
||||
|
||||
return this.status.has_transcript ? 'Re-transcribe' : 'Start transcription';
|
||||
},
|
||||
|
||||
start() {
|
||||
if (this.status.is_active) {
|
||||
this.beginPolling();
|
||||
@@ -273,6 +302,9 @@
|
||||
|
||||
if (wasActive && !this.status.is_active) {
|
||||
this.stopPolling();
|
||||
if (this.status.has_transcript || this.status.status === 'failed' || this.status.status === 'cancelled') {
|
||||
return;
|
||||
}
|
||||
window.location.reload();
|
||||
}
|
||||
} catch (error) {
|
||||
@@ -292,6 +324,17 @@
|
||||
const remain = seconds % 60;
|
||||
return minutes + ':' + String(remain).padStart(2, '0');
|
||||
},
|
||||
|
||||
formatTimestamp(value) {
|
||||
const date = new Date(value);
|
||||
if (Number.isNaN(date.getTime())) return value;
|
||||
const pad = (n) => String(n).padStart(2, '0');
|
||||
return date.getFullYear()
|
||||
+ '-' + pad(date.getMonth() + 1)
|
||||
+ '-' + pad(date.getDate())
|
||||
+ ' ' + pad(date.getHours())
|
||||
+ ':' + pad(date.getMinutes());
|
||||
},
|
||||
};
|
||||
}
|
||||
</script>
|
||||
|
||||
Reference in New Issue
Block a user