Surface stage, percent, and elapsed time while jobs run, and ship Compose so local confidential transcription can use faster-whisper on port 8090.
160 lines
4.5 KiB
PHP
160 lines
4.5 KiB
PHP
<?php
|
|
|
|
namespace App\Models;
|
|
|
|
use Illuminate\Database\Eloquent\Casts\Attribute;
|
|
use Illuminate\Database\Eloquent\Model;
|
|
use Illuminate\Support\Facades\Storage;
|
|
|
|
class Recording extends Model
|
|
{
|
|
/**
|
|
* @var list<string>
|
|
*/
|
|
protected $fillable = [
|
|
'title',
|
|
'original_filename',
|
|
'file_path',
|
|
'duration_seconds',
|
|
'recorded_at',
|
|
'artist',
|
|
'album',
|
|
'file_size_bytes',
|
|
'transcript',
|
|
'transcription_status',
|
|
'transcription_progress',
|
|
'transcription_percent',
|
|
'transcription_started_at',
|
|
'transcription_error',
|
|
'transcription_driver',
|
|
'ollama_url',
|
|
'transcribed_at',
|
|
];
|
|
|
|
/**
|
|
* @return array<string, string>
|
|
*/
|
|
protected function casts(): array
|
|
{
|
|
return [
|
|
'recorded_at' => 'datetime',
|
|
'transcribed_at' => 'datetime',
|
|
'transcription_started_at' => 'datetime',
|
|
'duration_seconds' => 'integer',
|
|
'file_size_bytes' => 'integer',
|
|
'transcription_percent' => 'integer',
|
|
];
|
|
}
|
|
|
|
/**
|
|
* Human-readable duration (m:ss).
|
|
*/
|
|
protected function durationFormatted(): Attribute
|
|
{
|
|
return Attribute::get(function (): string {
|
|
if ($this->duration_seconds === null) {
|
|
return '—';
|
|
}
|
|
|
|
$minutes = intdiv($this->duration_seconds, 60);
|
|
$seconds = $this->duration_seconds % 60;
|
|
|
|
return sprintf('%d:%02d', $minutes, $seconds);
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Friendly label for the selected transcription engine.
|
|
*/
|
|
protected function transcriptionDriverLabel(): Attribute
|
|
{
|
|
return Attribute::get(function (): ?string {
|
|
return match ($this->transcription_driver) {
|
|
'cloud' => 'Cloud (OpenAI Whisper)',
|
|
'local' => 'Local (faster-whisper)',
|
|
'ollama' => 'Ollama host',
|
|
default => $this->transcription_driver,
|
|
};
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Whether transcription is actively running or queued.
|
|
*/
|
|
public function isTranscribing(): bool
|
|
{
|
|
return in_array($this->transcription_status, ['pending', 'processing'], true);
|
|
}
|
|
|
|
/**
|
|
* Update the live progress fields shown in the UI.
|
|
*/
|
|
public function reportProgress(string $message, int $percent, string $status = 'processing'): void
|
|
{
|
|
$this->forceFill([
|
|
'transcription_status' => $status,
|
|
'transcription_progress' => $message,
|
|
'transcription_percent' => max(0, min(100, $percent)),
|
|
'transcription_error' => null,
|
|
])->save();
|
|
}
|
|
|
|
/**
|
|
* Absolute filesystem path for the stored audio file.
|
|
*/
|
|
public function absolutePath(): string
|
|
{
|
|
return Storage::disk('local')->path($this->file_path);
|
|
}
|
|
|
|
/**
|
|
* Delete the audio file from storage.
|
|
*/
|
|
public function deleteFile(): void
|
|
{
|
|
if ($this->file_path && Storage::disk('local')->exists($this->file_path)) {
|
|
Storage::disk('local')->delete($this->file_path);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Payload for the live status endpoint / Alpine poller.
|
|
*
|
|
* @return array<string, mixed>
|
|
*/
|
|
public function transcriptionStatusPayload(): array
|
|
{
|
|
$startedAt = $this->transcription_started_at;
|
|
$elapsed = $startedAt ? $startedAt->diffInSeconds(now()) : null;
|
|
|
|
return [
|
|
'id' => $this->id,
|
|
'status' => $this->transcription_status,
|
|
'progress' => $this->transcription_progress,
|
|
'percent' => $this->transcription_percent,
|
|
'driver' => $this->transcription_driver,
|
|
'driver_label' => $this->transcription_driver_label,
|
|
'error' => $this->transcription_error,
|
|
'started_at' => $startedAt?->toIso8601String(),
|
|
'elapsed_seconds' => $elapsed,
|
|
'elapsed_human' => $elapsed === null ? null : $this->formatElapsed($elapsed),
|
|
'duration_seconds' => $this->duration_seconds,
|
|
'is_active' => $this->isTranscribing(),
|
|
'has_transcript' => filled($this->transcript),
|
|
'transcribed_at' => $this->transcribed_at?->toIso8601String(),
|
|
];
|
|
}
|
|
|
|
private function formatElapsed(int $seconds): string
|
|
{
|
|
$minutes = intdiv($seconds, 60);
|
|
$remain = $seconds % 60;
|
|
|
|
if ($minutes === 0) {
|
|
return sprintf('%ds', $remain);
|
|
}
|
|
|
|
return sprintf('%dm %02ds', $minutes, $remain);
|
|
}
|
|
}
|