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

192 lines
8.6 KiB
PHP

@extends('admin.layout')
@section('title', 'Lyrics Pipeline')
@section('page_title', 'Lyrics Pipeline')
@section('extra_styles')
@include('admin.partials.settings-styles')
@endsection
@section('content')
<div class="adm-page-header">
<h1 class="adm-page-title"><i class="bi bi-music-note-list"></i> Lyrics Pipeline</h1>
</div>
@if(session('success'))
<div class="adm-alert adm-alert-success" style="margin-bottom:20px;">
<i class="bi bi-check-circle-fill"></i>
<span>{{ session('success') }}</span>
<button class="adm-alert-close" type="button"><i class="bi bi-x"></i></button>
</div>
@endif
<form method="POST" action="{{ route('admin.settings.update') }}" id="lyricsForm">
@csrf
<input type="hidden" name="lyrics_section" value="1">
{{-- ── Master switch ─────────────────────────────────────────── --}}
<div class="settings-section">
<div class="settings-section-header">
<i class="bi bi-toggle-on"></i>
Master
@if($settings['lyrics_enabled'] === 'true')
<span class="chip chip-green" style="margin-left:6px;"><span class="chip-dot"></span> Enabled</span>
@else
<span class="chip chip-red" style="margin-left:6px;"><span class="chip-dot"></span> Disabled</span>
@endif
</div>
<div class="settings-section-body">
<div class="setting-row">
<div class="setting-label">
<strong>Lyrics generation</strong>
<small>
Master switch for the whole feature. When OFF, the Generate / Regenerate
button in the player gear is hidden and the API endpoint refuses new jobs.
Existing lyrics keep displaying only NEW generation is blocked.
</small>
</div>
<div class="setting-control">
<label class="toggle-wrap">
<span class="toggle-switch">
<input type="checkbox" name="lyrics_enabled" value="true" {{ $settings['lyrics_enabled'] === 'true' ? 'checked' : '' }}>
<span class="toggle-track"></span>
<span class="toggle-thumb"></span>
</span>
<span class="toggle-label">Allow new lyrics jobs</span>
</label>
</div>
</div>
</div>
</div>
{{-- ── Pipeline steps ────────────────────────────────────────── --}}
<div class="settings-section">
<div class="settings-section-header">
<i class="bi bi-diagram-3"></i>
Pipeline steps
</div>
<div class="settings-section-body">
<div class="setting-row">
<div class="setting-label">
<strong>Description-based correction</strong>
<small>
Use the song description as the source of truth for lyric text. Whisper provides
timing anchors; the description provides the exact words. When OFF, the player
shows Whisper's raw transcription (may contain misheard words and miss verses).
</small>
</div>
<div class="setting-control">
<label class="toggle-wrap">
<span class="toggle-switch">
<input type="checkbox" name="lyrics_use_description" value="true" {{ $settings['lyrics_use_description'] === 'true' ? 'checked' : '' }}>
<span class="toggle-track"></span>
<span class="toggle-thumb"></span>
</span>
<span class="toggle-label">Use description as ground truth</span>
</label>
</div>
</div>
<div class="setting-row">
<div class="setting-label">
<strong>Voice activity filter (VAD)</strong>
<small>
Runs Silero VAD before Whisper to skip pure-instrumental sections. Without this,
Whisper invents lyrics over intro / outro / breakdown music. When OFF, the entire
audio is transcribed — useful as a fallback if VAD is dropping legitimate quiet vocals.
</small>
</div>
<div class="setting-control">
<label class="toggle-wrap">
<span class="toggle-switch">
<input type="checkbox" name="lyrics_vad_enabled" value="true" {{ $settings['lyrics_vad_enabled'] === 'true' ? 'checked' : '' }}>
<span class="toggle-track"></span>
<span class="toggle-thumb"></span>
</span>
<span class="toggle-label">Drop instrumentals via VAD</span>
</label>
</div>
</div>
<div class="setting-row">
<div class="setting-label">
<strong>Vocal-region gap-filling</strong>
<small>
For description lines Whisper missed, snap them to moments where vocals are
active (detected by VAD) instead of distributing evenly across time. Keeps
gap-filled lyrics on actual singing. When OFF, missing lines distribute uniformly
between Whisper anchors (faster but can drift over instrumental beats).
</small>
</div>
<div class="setting-control">
<label class="toggle-wrap">
<span class="toggle-switch">
<input type="checkbox" name="lyrics_vocal_region_gapfill" value="true" {{ $settings['lyrics_vocal_region_gapfill'] === 'true' ? 'checked' : '' }}>
<span class="toggle-track"></span>
<span class="toggle-thumb"></span>
</span>
<span class="toggle-label">Snap missing lines to vocal regions</span>
</label>
</div>
</div>
<div class="setting-row">
<div class="setting-label">
<strong>Vocal isolation (Demucs)</strong>
<small>
Two-stem source separation (vocals vs. instruments) before Whisper. Improves
transcription on noisy mixes but adds ~30s GPU time and has caused a CUDA-handoff
deadlock that hangs the pipeline at 50%. Currently auto-skipped when a description
is present (timing-only mode). Enable to also use Demucs when no description is given.
</small>
</div>
<div class="setting-control">
<label class="toggle-wrap">
<span class="toggle-switch">
<input type="checkbox" name="lyrics_demucs_enabled" value="true" {{ $settings['lyrics_demucs_enabled'] === 'true' ? 'checked' : '' }}>
<span class="toggle-track"></span>
<span class="toggle-thumb"></span>
</span>
<span class="toggle-label">Use Demucs when no description</span>
</label>
</div>
</div>
<div class="setting-row">
<div class="setting-label">
<strong>LLM emoji decoration</strong>
<small>
After lyrics are saved, run the active LLM provider to bake one or more emojis
into each line. Without this, the player still decorates lines using a built-in
keyword-to-emoji map (less varied). Provider is configured under
<a href="{{ route('admin.settings') }}" style="color:var(--brand)">AI / LLM Settings</a>.
</small>
</div>
<div class="setting-control">
<label class="toggle-wrap">
<span class="toggle-switch">
<input type="checkbox" name="lyrics_llm_decorate" value="true" {{ $settings['lyrics_llm_decorate'] === 'true' ? 'checked' : '' }}>
<span class="toggle-track"></span>
<span class="toggle-thumb"></span>
</span>
<span class="toggle-label">Bake emojis via LLM</span>
</label>
</div>
</div>
</div>
</div>
<div class="save-bar">
<a href="{{ route('admin.dashboard') }}" class="adm-btn">Cancel</a>
<button type="submit" class="adm-btn adm-btn-primary">
<i class="bi bi-floppy"></i> Save Lyrics Settings
</button>
</div>
</form>
@endsection