Add Docker stack, Reverb live progress, and duplicate upload detection.
Ship FrankenPHP Compose services with Reverb WebSockets for real-time transcription status, skip re-uploading identical audio via content hash, and format disk usage without requiring intl.
This commit is contained in:
@@ -8,7 +8,10 @@
|
||||
'statusUrl' => route('recordings.transcription-status', $recording),
|
||||
'initial' => $recording->transcriptionStatusPayload(),
|
||||
]))"
|
||||
x-init="start()"
|
||||
x-init="
|
||||
start();
|
||||
return () => destroy();
|
||||
"
|
||||
>
|
||||
<div class="mb-6 flex flex-col gap-4 sm:flex-row sm:items-start sm:justify-between">
|
||||
<div>
|
||||
@@ -173,120 +176,4 @@
|
||||
</p>
|
||||
</section>
|
||||
</div>
|
||||
|
||||
<script defer src="https://cdn.jsdelivr.net/npm/alpinejs@3.x.x/dist/cdn.min.js"></script>
|
||||
<style>[x-cloak]{display:none!important}</style>
|
||||
<script>
|
||||
function transcriptionMonitor({ statusUrl, initial }) {
|
||||
return {
|
||||
statusUrl,
|
||||
status: initial,
|
||||
pollError: null,
|
||||
timer: null,
|
||||
tickTimer: null,
|
||||
|
||||
get badgeClass() {
|
||||
const map = {
|
||||
done: 'bg-teal-50 text-teal-800 ring-teal-600/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 'Restart transcription';
|
||||
}
|
||||
|
||||
return this.status.has_transcript ? 'Re-transcribe' : 'Start transcription';
|
||||
},
|
||||
|
||||
start() {
|
||||
if (this.status.is_active) {
|
||||
this.beginPolling();
|
||||
}
|
||||
},
|
||||
|
||||
beginPolling() {
|
||||
this.stopPolling();
|
||||
this.poll();
|
||||
this.timer = setInterval(() => this.poll(), 1500);
|
||||
this.tickTimer = setInterval(() => this.tickElapsed(), 1000);
|
||||
},
|
||||
|
||||
stopPolling() {
|
||||
if (this.timer) clearInterval(this.timer);
|
||||
if (this.tickTimer) clearInterval(this.tickTimer);
|
||||
this.timer = null;
|
||||
this.tickTimer = null;
|
||||
},
|
||||
|
||||
tickElapsed() {
|
||||
if (!this.status.is_active || this.status.elapsed_seconds == null) return;
|
||||
this.status.elapsed_seconds += 1;
|
||||
this.status.elapsed_human = this.formatElapsed(this.status.elapsed_seconds);
|
||||
},
|
||||
|
||||
async poll() {
|
||||
try {
|
||||
const response = await fetch(this.statusUrl, {
|
||||
headers: { Accept: 'application/json' },
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error('Status request failed (' + response.status + ')');
|
||||
}
|
||||
|
||||
const wasActive = this.status.is_active;
|
||||
this.status = await response.json();
|
||||
this.pollError = null;
|
||||
|
||||
if (this.status.is_active && !this.timer) {
|
||||
this.beginPolling();
|
||||
}
|
||||
|
||||
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) {
|
||||
this.pollError = error.message || 'Could not refresh progress.';
|
||||
}
|
||||
},
|
||||
|
||||
formatElapsed(seconds) {
|
||||
const total = Math.max(0, Math.round(Number(seconds) || 0));
|
||||
const minutes = Math.floor(total / 60);
|
||||
const remain = total % 60;
|
||||
if (minutes === 0) return remain + 's';
|
||||
return minutes + 'm ' + String(remain).padStart(2, '0') + 's';
|
||||
},
|
||||
|
||||
formatDuration(seconds) {
|
||||
const total = Math.max(0, Math.round(Number(seconds) || 0));
|
||||
const minutes = Math.floor(total / 60);
|
||||
const remain = total % 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>
|
||||
@endsection
|
||||
|
||||
Reference in New Issue
Block a user