Files
AndyTranscribe/app/Models/Recording.php
T
ben e0400aadef Add live transcription progress and Docker Whisper.
Surface stage, percent, and elapsed time while jobs run, and ship Compose so local confidential transcription can use faster-whisper on port 8090.
2026-08-12 14:32:56 +02:00

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);
}
}