Files
navidrome-music-sync/app/templates/index.html
T
Antigravity 4ed8e0c71a feat(radio): replace robot TTS with Piper Neural voices & add instant voice preview
- Integrate local Piper ONNX neural TTS engine (Dmitri, Irina, Denis, Ruslan)
- Add GET /api/radio/preview-voice endpoint with cached pre-rendered samples
- Add '▶ Прослушать голос' button in Web UI for 1-click preview before generating
- Add automated filter to strip LLM refusals and fall back to clean radio phrasing
2026-10-12 16:58:59 +03:00

266 lines
13 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<!DOCTYPE html>
<html lang="ru">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Navidrome Music Sync</title>
<link rel="stylesheet" href="static/style.css">
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap" rel="stylesheet">
</head>
<body>
<div class="app-container">
<header class="header">
<div class="logo-group">
<span class="logo-icon">🎵</span>
<div>
<h1>Navidrome Music Sync</h1>
<p class="subtitle">Поиск, импорт и синхронизация музыки в Navidrome</p>
</div>
</div>
<div class="status-badges">
<span class="badge badge-success" id="yandexStatus">Яндекс Музыка: Активна</span>
<span class="badge badge-info" id="tracksCount">Загружено: 0</span>
{% if user %}
<span class="badge badge-user">👤 {{ user }}</span>
<a href="/logout" class="btn-logout" title="Выйти из системы">Выйти 🚪</a>
{% endif %}
</div>
</header>
<main class="main-content">
<!-- Search & Input Card -->
<section class="card input-card">
<h2>🔍 Поиск и импорт музыки</h2>
<p class="card-desc">Введите название трека/артиста (например: <b>Booker - Время колокольчиков</b>) или вставьте прямую ссылку (Яндекс, Spotify, Apple, YouTube):</p>
<form id="downloadForm" class="download-form">
<div class="input-wrapper">
<input type="text" id="urlInput" placeholder="Название трека или https://..." required autocomplete="off">
<button type="submit" id="submitBtn" class="btn btn-primary">Найти / Загрузить</button>
</div>
</form>
<div class="quick-actions">
<span>Быстрые действия:</span>
<button id="syncLikesBtn" class="btn btn-secondary">
❤️ Синхронизировать «Мне нравится» (Яндекс)
</button>
</div>
</section>
<!-- Search Results Card (Hidden by default) -->
<section class="card search-card" id="searchCard" style="display: none;">
<div class="search-header">
<h2>🎯 Результаты поиска: <span id="searchQueryLabel"></span></h2>
<button id="closeSearchBtn" class="btn-close" title="Закрыть поиск">&times;</button>
</div>
<div id="searchLoading" class="search-loading" style="display: none;">
<div class="spinner"></div> Ищем на Spotify, Яндекс Музыке, SoundCloud, YouTube и Apple Music...
</div>
<!-- Search Tracks -->
<div class="search-section" id="searchTracksSection">
<h3>🎵 Найденные треки</h3>
<div class="search-tracks-list" id="searchTracksList"></div>
</div>
<!-- Search Albums -->
<div class="search-section" id="searchAlbumsSection" style="margin-top: 1.5rem;">
<h3>💿 Альбомы</h3>
<div class="search-albums-grid" id="searchAlbumsList"></div>
</div>
</section>
<!-- Smart Wave Card -->
<section class="card wave-card">
<div class="wave-header">
<div class="wave-header-title">
<h2>🌊 Моя Волна <span class="badge badge-wave">Smart Wave V2</span> <span class="badge badge-llm">🤖 Qwen 2.5</span></h2>
<p class="wave-subtitle">
Персональный контекстный поток рекомендаций. Нейросетевой реранкинг по времени суток и настроению с авто-ротацией в Navidrome.
</p>
</div>
<div class="wave-controls">
<button id="waveGenerateBtn" class="btn btn-primary">⚡ Обновить Волну</button>
<button id="waveRotateBtn" class="btn btn-outline" title="Перенести лайкнутые треки в постоянную библиотеку и очистить старые">🔄 Ротация</button>
</div>
</div>
<!-- Mood & Context Selector Row -->
<div class="wave-mood-row">
<div class="wave-mood-group">
<label for="waveMoodSelect" class="wave-mood-label">Режим настроения:</label>
<select id="waveMoodSelect" class="wave-mood-select">
<option value="auto" selected>🕒 Авто (по времени суток)</option>
<option value="energy">⚡ Энергия & Спорт</option>
<option value="chill">☕ Спокойствие & Чилл</option>
<option value="dark">🌙 Ночной андеграунд</option>
</select>
</div>
<div class="wave-context-badge-wrap">
<span id="waveContextLabel" class="badge badge-context">Определение контекста...</span>
</div>
</div>
<div class="wave-stats-row">
<div class="wave-stat">
<span class="wave-stat-label">Буфер ротации:</span>
<strong id="waveCountLabel">0 / 25 треков</strong>
</div>
<div class="wave-stat">
<span class="wave-stat-label">Объём на диске:</span>
<strong id="waveSizeLabel">0 МБ</strong>
</div>
<div class="wave-stat">
<span class="wave-stat-label">Срок хранения:</span>
<strong>7 дней</strong>
</div>
<div class="wave-stat">
<span class="wave-stat-label">Умный плейлист:</span>
<strong><a href="https://music.qpid.pro" target="_blank">Моя Волна.nsp ↗</a></strong>
</div>
</div>
<div id="waveAnchorsContainer" class="wave-anchors" style="margin-top: 0.8rem; display: none;">
<span style="font-size: 0.8rem; color: var(--text-secondary);">Якорные исполнители вкуса:</span>
<div id="waveAnchorsList" class="wave-anchors-tags"></div>
</div>
<!-- Wave Tracks List -->
<div class="wave-tracks-container" style="margin-top: 1rem;">
<div class="wave-tracks-header">
<h3>🎵 Треки сейчас в потоке Волны</h3>
<span id="waveLoadingText" style="display: none; color: var(--accent-color); font-size: 0.82rem;">
<span class="spinner-sm"></span> Идёт поиск и загрузка рекомендаций...
</span>
</div>
<div id="waveTracksList" class="wave-tracks-list">
<p class="empty-state">Волна пуста. Нажмите «Обновить Волну» для генерации первого потока.</p>
</div>
</div>
</section>
<!-- AI Radio Card (Phase 3) -->
<section class="card radio-card">
<div class="radio-header">
<div class="radio-header-title">
<h2>🎙️ AI Радио <span class="badge badge-radio">AI DJ Эфир</span> <span class="badge badge-llm">🤖 Qwen 2.5 + Piper Neural</span></h2>
<p class="radio-subtitle">
Персональный радиоэфир: AI-ведущий объявляет погоду, время суток и делает динамичные подводки перед песнями в отдельном плейлисте «AI Радио».
</p>
</div>
<div class="radio-controls">
<button id="radioGenerateBtn" class="btn btn-primary" style="background: linear-gradient(135deg, #f59e0b 0%, #d97706 100%);">🎙️ Сгенерировать радиоэфир</button>
</div>
</div>
<!-- Radio Host Voice & Context Row -->
<div class="radio-settings-row">
<div class="radio-voice-group" style="flex-wrap: wrap;">
<label for="radioVoiceSelect" class="radio-voice-label">Голос ведущего:</label>
<select id="radioVoiceSelect" class="radio-voice-select">
<option value="ru_RU-dmitri-medium" selected>🎙️ Дмитрий (Энергичный DJ)</option>
<option value="ru_RU-irina-medium">🎙️ Ирина (Естественный женский)</option>
<option value="ru_RU-denis-medium">🎙️ Денис (Глубокий баритон)</option>
<option value="ru_RU-ruslan-medium">🎙️ Руслан (Спокойный ведущий)</option>
<option value="gtts_ru">🎙️ Google (Стандартный робот)</option>
</select>
<button id="radioVoicePreviewBtn" type="button" class="btn-sm btn-play-preview" title="Послушать сэмпл выбранного голоса">
▶ Прослушать голос
</button>
</div>
<div class="radio-status-badge-wrap">
<span id="radioStatusLabel" class="badge badge-context">Эфир готов к запуску</span>
</div>
</div>
<div class="radio-stats-row">
<div class="radio-stat">
<span class="radio-stat-label">Реплик ведущего:</span>
<strong id="radioClipsCountLabel">0 записей</strong>
</div>
<div class="radio-stat">
<span class="radio-stat-label">Эфирный плейлист:</span>
<strong><a href="https://music.qpid.pro" target="_blank">AI Радио.m3u8 ↗</a></strong>
</div>
<div class="radio-stat">
<span class="radio-stat-label">Чистая музыка (без ведущего):</span>
<strong><a href="https://music.qpid.pro" target="_blank">Моя Волна.nsp ↗</a></strong>
</div>
</div>
<div id="radioSegmentsContainer" class="radio-segments-container" style="margin-top: 1rem; display: none;">
<h3 style="font-size: 0.95rem; margin-bottom: 0.4rem;">📻 Реплики ведущего в текущем радиоэфире:</h3>
<div id="radioSegmentsList" class="radio-segments-list"></div>
</div>
</section>
<!-- Active Tasks -->
<section class="card tasks-card">
<h2>⚡ Очередь задач</h2>
<div id="tasksList" class="tasks-list">
<p class="empty-state">Нет активных загрузок</p>
</div>
</section>
<!-- History -->
<section class="card history-card">
<h2>📜 Недавно загруженные треки</h2>
<div class="table-container">
<table class="tracks-table">
<thead>
<tr>
<th>Сервис</th>
<th>Исполнитель</th>
<th>Трек</th>
<th>Альбом</th>
<th>Дата</th>
<th>Действия</th>
</tr>
</thead>
<tbody id="historyBody">
<tr>
<td colspan="6" class="empty-state">Пока нет загрузок</td>
</tr>
</tbody>
</table>
</div>
</section>
</main>
<footer class="footer">
<p>Navidrome Music Sync • Интегрировано с <a href="https://music.qpid.pro" target="_blank">music.qpid.pro</a></p>
</footer>
</div>
<!-- Floating Audio Preview Player -->
<div id="audioPlayerBar" class="audio-player-bar" style="display: none;">
<div class="player-track-info">
<img id="playerCover" class="player-cover" src="" alt="Cover" />
<div class="player-meta">
<div class="player-title-row">
<span id="playerTitle" class="player-title"></span>
<span id="playerExplicitBadge" class="badge"></span>
</div>
<span id="playerArtist" class="player-artist"></span>
</div>
</div>
<div class="player-controls-container">
<audio id="globalAudioPlayer" controls preload="metadata"></audio>
</div>
<div class="player-actions">
<button id="playerImportBtn" class="btn-sm btn-import">📥 В Navidrome</button>
<button id="playerDownloadBtn" class="btn-sm btn-direct-dl">💾 Скачать</button>
<button id="playerCloseBtn" class="btn-player-close" title="Закрыть плеер">&times;</button>
</div>
</div>
<script src="static/app.js"></script>
</body>
</html>