Lyrics pipeline (Whisper + Demucs + description alignment):
- New GenerateLyricsJob runs WhisperX with VAD filtering and forced word
alignment, writes per-track JSON to NAS.
- New DecorateLyricsJob calls the active LLM provider to bake one to
several emojis into each line (heavy decoration prompt).
- LyricsDescriptionParser strips heading content, section markers, and
emoji-decoration from a song's description while preserving every
language verbatim.
- correct_whisper_with_description aligner: strong-match anchors only,
vocal-region-aware gap-fill so missing verses land on actual singing.
- Owner UI for generate/regenerate/edit/delete in the player gear.
Admin pages:
- /admin/lyrics toggles for VAD, vocal gap-fill, Demucs, master
- /admin/gpu extracted GPU section, encoder picker, FFmpeg path
- /admin/backup extracted users-and-settings export/import
- /admin/settings now AI/LLM only with provider list and Test button
- /admin/nas-storage hosts NAS settings, repair, disable flow, browser
- Shared partials/settings-styles for a uniform look across pages.
Playlist view tracking:
- Migration adds playlists.view_count and playlist_views dedup table.
- Playlist::bumpViewIfNew increments per device with a one-hour window.
- Tracked from /playlists/{id}, /playlists/share/{token}, /ps/{token},
and /videos/{id}?playlist={token}. Dispatched after-response so it
never blocks the page render.
- Loading a playlist on the video page now runs one query instead of
the four the old getNextVideo/getPreviousVideo path triggered.
- View counts shown on every playlist card and the playlist hero.
Player polish:
- Floating mini-player is draggable, persists its position in
localStorage, clamps to viewport on resize.
- Mini disabled entirely on mobile (less than 768px).
- New gear-menu Mini Player toggle (persists in localStorage) lets the
user disable both scroll-activation and SPA-nav-activation.
- Close button keeps media playing when used on the player's own page.
- SPA navigator now swaps a #page-scripts container so per-page JS
(channel tabs, etc.) gets re-executed after content swaps.
Storage layout:
- Runtime data moved from /storage/* to /data/* and gitignored.
- /ml/venv, /ml/cache, /ml/__pycache__ excluded.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
48 lines
1.2 KiB
PHP
48 lines
1.2 KiB
PHP
<?php
|
|
|
|
namespace App\Models;
|
|
|
|
use Illuminate\Database\Eloquent\Model;
|
|
|
|
class VideoSlide extends Model
|
|
{
|
|
protected $fillable = ['video_id', 'audio_track_id', 'filename', 'position'];
|
|
|
|
public function video()
|
|
{
|
|
return $this->belongsTo(Video::class);
|
|
}
|
|
|
|
public function audioTrack()
|
|
{
|
|
return $this->belongsTo(VideoAudioTrack::class, 'audio_track_id');
|
|
}
|
|
|
|
public function getUrlAttribute(): string
|
|
{
|
|
return route('media.thumbnail', $this->filename);
|
|
}
|
|
|
|
/**
|
|
* Absolute path to the slide image on local disk.
|
|
* Old format: storage/app/public/thumbnails/{filename}
|
|
* New format: storage/app/{relative-path} (filename contains a slash)
|
|
*/
|
|
public function localPath(): string
|
|
{
|
|
return str_contains($this->filename, '/')
|
|
? storage_path('app/' . $this->filename)
|
|
: storage_path('app/public/thumbnails/' . $this->filename);
|
|
}
|
|
|
|
/**
|
|
* Storage::delete()-compatible key for this slide file.
|
|
*/
|
|
public function storageKey(): string
|
|
{
|
|
return str_contains($this->filename, '/')
|
|
? $this->filename
|
|
: 'public/thumbnails/' . $this->filename;
|
|
}
|
|
}
|