ghassan f98e5415a3 Add lyrics pipeline, playlist views, admin toggles, and player polish
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>
2026-05-31 22:01:47 +03:00

62 lines
2.2 KiB
PHP

<?php
namespace App\Console\Commands;
use App\Jobs\GenerateLyricsJob;
use App\Models\Video;
use App\Services\NasSyncService;
use Illuminate\Console\Command;
/**
* Backfill synced lyrics for existing songs. New uploads generate automatically;
* this covers the catalogue that predates the feature.
*
* php artisan lyrics:generate 163 # one video (primary + every track)
* php artisan lyrics:generate --all # every music video missing lyrics
* php artisan lyrics:generate --all --force # regenerate even if a file exists
*/
class GenerateLyrics extends Command
{
protected $signature = 'lyrics:generate {video? : Video id} {--all : All music videos} {--force : Regenerate even when lyrics already exist}';
protected $description = 'Generate word-level synced lyrics for songs (dispatched to the video-processing queue)';
public function handle(NasSyncService $nas): int
{
$force = (bool) $this->option('force');
if ($videoId = $this->argument('video')) {
$video = Video::find($videoId);
if (! $video) {
$this->error("Video #{$videoId} not found.");
return self::FAILURE;
}
$videos = collect([$video]);
} elseif ($this->option('all')) {
$videos = Video::where('type', 'music')->get();
} else {
$this->error('Pass a video id or --all.');
return self::FAILURE;
}
$dispatched = 0;
foreach ($videos as $video) {
$video->loadMissing('audioTracks');
if ($force || ! is_array($nas->getLyrics($video, null))) {
GenerateLyricsJob::dispatch($video->id, null)->onConnection('database');
$dispatched++;
}
foreach ($video->audioTracks as $track) {
if ($force || ! is_array($nas->getLyrics($video, $track))) {
GenerateLyricsJob::dispatch($video->id, $track->id)->onConnection('database');
$dispatched++;
}
}
$this->line("Queued lyrics for #{$video->id}{$video->title}");
}
$this->info("Dispatched {$dispatched} lyrics job(s) to the video-processing queue.");
return self::SUCCESS;
}
}