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 <[email protected]>
This commit is contained in:
1 parent
73527f3781
commit
f98e5415a3
60 files changed
+7920
-2675
No files matched your search
@@ -0,0 +1,31 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
public function up(): void
|
||||
{
|
||||
Schema::table('video_slides', function (Blueprint $table) {
|
||||
// NULL = song-wide (legacy / shared). Non-null = owned by that audio track.
|
||||
// On track delete, slides become song-wide rather than disappear.
|
||||
$table->foreignId('audio_track_id')
|
||||
->nullable()
|
||||
->after('video_id')
|
||||
->constrained('video_audio_tracks')
|
||||
->nullOnDelete();
|
||||
|
||||
$table->index(['video_id', 'audio_track_id']);
|
||||
});
|
||||
}
|
||||
|
||||
public function down(): void
|
||||
{
|
||||
Schema::table('video_slides', function (Blueprint $table) {
|
||||
$table->dropIndex(['video_id', 'audio_track_id']);
|
||||
$table->dropConstrainedForeignId('audio_track_id');
|
||||
});
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,49 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
/**
|
||||
* Add playlist view tracking — mirrors the video_views pattern.
|
||||
*
|
||||
* • playlists.view_count — denormalised total for fast card rendering
|
||||
* • playlist_views table — one row per (playlist, viewer-id) so a refresh
|
||||
* within the dedup window doesn't double-count
|
||||
*/
|
||||
public function up(): void
|
||||
{
|
||||
Schema::table('playlists', function (Blueprint $table) {
|
||||
$table->unsignedBigInteger('view_count')->default(0)->after('share_token');
|
||||
});
|
||||
|
||||
Schema::create('playlist_views', function (Blueprint $table) {
|
||||
$table->id();
|
||||
$table->unsignedBigInteger('playlist_id');
|
||||
$table->unsignedBigInteger('user_id')->nullable();
|
||||
$table->string('device_id', 64)->nullable();
|
||||
$table->string('device_hash', 64)->nullable();
|
||||
$table->string('ip_address', 64)->nullable();
|
||||
$table->string('country', 8)->nullable();
|
||||
$table->string('country_name', 64)->nullable();
|
||||
$table->string('user_agent', 512)->nullable();
|
||||
$table->timestamp('viewed_at');
|
||||
|
||||
$table->foreign('playlist_id')->references('id')->on('playlists')->onDelete('cascade');
|
||||
$table->index(['playlist_id', 'viewed_at']);
|
||||
$table->index(['playlist_id', 'user_id', 'viewed_at']);
|
||||
$table->index(['playlist_id', 'device_id', 'viewed_at']);
|
||||
$table->index(['playlist_id', 'device_hash', 'viewed_at']);
|
||||
});
|
||||
}
|
||||
|
||||
public function down(): void
|
||||
{
|
||||
Schema::dropIfExists('playlist_views');
|
||||
Schema::table('playlists', function (Blueprint $table) {
|
||||
$table->dropColumn('view_count');
|
||||
});
|
||||
}
|
||||
};
|
||||
Reference in new issue
Block a user