adding shazam, and LRClib support

This commit is contained in:
jadssss
2026-10-24 06:34:12 +03:00
parent 8ffb778241
commit f702cc87ef
7 changed files with 822 additions and 213 deletions

View File

@@ -8,13 +8,16 @@ from typing import Optional
from PySide6.QtCore import Qt, QUrl
from PySide6.QtGui import QColor, QDesktopServices
from PySide6.QtWidgets import (
QCheckBox, QComboBox, QFileDialog, QFormLayout, QGroupBox,
QCheckBox, QComboBox, QDoubleSpinBox, QFileDialog, QFormLayout, QGroupBox,
QHBoxLayout, QLabel, QLineEdit, QMainWindow, QMessageBox,
QPlainTextEdit, QProgressBar, QPushButton, QSpinBox, QTabWidget,
QVBoxLayout, QWidget,
)
from core.config import ConvertConfig, PipelineConfig, SeparateConfig, TranscribeConfig
from core.config import (
ConvertConfig, LyricsConfig, PipelineConfig, SeparateConfig,
ShazamConfig, TranscribeConfig,
)
from gui.worker import PipelineWorker
@@ -24,6 +27,7 @@ ENGINES = ["whisperx", "gigaam"]
STAGE_LABELS = {
"file": "Файлы",
"identify": "Идентификация",
"convert": "Конвертация",
"separate": "Отделение",
"transcribe": "Транскрибация",
@@ -36,7 +40,6 @@ MODE_LABELS = {
def _detect_device_info() -> tuple[str, str]:
"""Return (display_text, color_hex) describing the compute device."""
try:
import torch
except ImportError:
@@ -57,7 +60,7 @@ class MainWindow(QMainWindow):
def __init__(self) -> None:
super().__init__()
self.setWindowTitle("Song Transcriber")
self.resize(1100, 850)
self.resize(1140, 880)
self._worker: Optional[PipelineWorker] = None
self._build_ui()
self._apply_defaults()
@@ -74,19 +77,16 @@ class MainWindow(QMainWindow):
root.setContentsMargins(18, 16, 18, 16)
root.setSpacing(12)
# --- header row: title + device badge ---
header = QHBoxLayout()
title = QLabel("🎵 Song Transcriber")
title.setObjectName("appTitle")
header.addWidget(title)
header.addStretch(1)
self.device_label = QLabel("")
self.device_label.setObjectName("deviceInfo")
header.addWidget(self.device_label)
root.addLayout(header)
# --- top settings ---
top_box = QGroupBox("Общие настройки")
top_layout = QFormLayout(top_box)
top_layout.setSpacing(10)
@@ -114,19 +114,21 @@ class MainWindow(QMainWindow):
root.addWidget(top_box)
# --- stages ---
self.tabs = QTabWidget()
self.tabs.addTab(self._build_shazam_tab(), "Shazam")
self.tabs.addTab(self._build_lrclib_tab(), "LRCLIB")
self.tabs.addTab(self._build_convert_tab(), "Конвертация")
self.tabs.addTab(self._build_separate_tab(), "Отделение вокала")
self.tabs.addTab(self._build_transcribe_tab(), "Транскрибация")
root.addWidget(self.tabs)
self.shazam_enabled_cb.toggled.connect(self._sync_tab_indicators)
self.lyrics_enabled_cb.toggled.connect(self._sync_tab_indicators)
self.convert_enabled_cb.toggled.connect(self._sync_tab_indicators)
self.separate_enabled_cb.toggled.connect(self._sync_tab_indicators)
self.transcribe_enabled_cb.toggled.connect(self._sync_tab_indicators)
self.engine_cb.currentTextChanged.connect(self._sync_engine_fields)
# --- controls row ---
row = QHBoxLayout()
row.setSpacing(10)
self.start_btn = QPushButton("▶ Запустить")
@@ -136,7 +138,6 @@ class MainWindow(QMainWindow):
self.stop_btn.setObjectName("danger")
self.stop_btn.setEnabled(False)
self.stop_btn.clicked.connect(self._on_stop)
self.open_out_btn = QPushButton("📂 Открыть результат")
self.open_out_btn.clicked.connect(self._open_output_dir)
self.save_preset_btn = QPushButton("💾 Сохранить пресет")
@@ -152,12 +153,10 @@ class MainWindow(QMainWindow):
row.addWidget(self.load_preset_btn)
root.addLayout(row)
# --- progress ---
self.progress = QProgressBar()
self.progress.setFormat("Ожидание")
root.addWidget(self.progress)
# --- log ---
self.log_view = QPlainTextEdit()
self.log_view.setObjectName("log")
self.log_view.setReadOnly(True)
@@ -176,6 +175,64 @@ class MainWindow(QMainWindow):
h.addWidget(btn)
return w
def _build_shazam_tab(self) -> QWidget:
w = QWidget()
form = QFormLayout(w)
form.setContentsMargins(16, 16, 16, 16)
form.setSpacing(10)
self.shazam_enabled_cb = QCheckBox("Включён")
form.addRow("", self.shazam_enabled_cb)
info = QLabel(
"Идентификация трека через Shazam (unofficial API).\n"
"При совпадении сохраняются метаданные и (если возможно) текст из LRCLIB.\n"
"Требует ffmpeg (для конвертации в 16 кГц моно)."
)
info.setWordWrap(True)
info.setStyleSheet("color: #a6adc8;")
form.addRow(info)
return w
def _build_lrclib_tab(self) -> QWidget:
w = QWidget()
form = QFormLayout(w)
form.setContentsMargins(16, 16, 16, 16)
form.setSpacing(10)
self.lyrics_enabled_cb = QCheckBox("Включён")
form.addRow("", self.lyrics_enabled_cb)
self.lyrics_user_agent = QLineEdit()
form.addRow("User-Agent:", self.lyrics_user_agent)
self.lyrics_use_tags_cb = QCheckBox("Искать по тегам (artist + title)")
form.addRow("", self.lyrics_use_tags_cb)
self.lyrics_use_shazam_cb = QCheckBox("Искать по результату Shazam")
form.addRow("", self.lyrics_use_shazam_cb)
self.lyrics_after_asr_cb = QCheckBox("Проверять после ASR (fuzzy-match)")
form.addRow("", self.lyrics_after_asr_cb)
self.lyrics_transcribe_unresolved_cb = QCheckBox(
"Запускать ASR, если Shazam опознал, но текста нет"
)
form.addRow("", self.lyrics_transcribe_unresolved_cb)
self.lyrics_threshold = QDoubleSpinBox()
self.lyrics_threshold.setRange(0.0, 1.0)
self.lyrics_threshold.setSingleStep(0.05)
self.lyrics_threshold.setDecimals(2)
self.lyrics_threshold.setToolTip(
"Насколько должен совпасть распознанный ASR-текст с текстом из LRCLIB,\n"
"чтобы считать это тем же треком. 1.0 = точное совпадение, 0.0 = любое."
)
form.addRow("Порог fuzzy:", self.lyrics_threshold)
return w
def _build_convert_tab(self) -> QWidget:
w = QWidget()
form = QFormLayout(w)
@@ -241,8 +298,8 @@ class MainWindow(QMainWindow):
self.lang_cb.setEditable(True)
self.lang_cb.addItems(LANGUAGES)
self.lang_cb.setToolTip(
"'auto' — WhisperX определяет язык на каждом файле отдельно.\n"
"Это заметно медленнее. Если знаешь язык — выбери его явно."
"'auto' — WhisperX определяет язык на каждом файле.\n"
"Если знаешь язык — выбери его явно, будет быстрее."
)
form.addRow("Язык:", self.lang_cb)
@@ -258,28 +315,19 @@ class MainWindow(QMainWindow):
self.batch_spin.setRange(1, 512)
self.batch_spin.setToolTip(
"Размер батча сегментов ВНУТРИ одного файла (не файлов!).\n"
"Больше ≠ всегда быстрее: упирается в VRAM.\n"
"8–16 для 8 ГБ VRAM, 32–64 для 24 ГБ.\n"
"При 'CUDA out of memory' — уменьшайте."
"8–16 для 8 ГБ VRAM, 32–64 для 24 ГБ."
)
form.addRow("Batch size:", self.batch_spin)
self.gigaam_provider_cb = QComboBox()
self.gigaam_provider_cb.addItems(["auto", "cuda", "cpu"])
form.addRow("GigaAM provider:", self.gigaam_provider_cb)
self.local_files_only_cb = QCheckBox("Не проверять HuggingFace (offline)")
self.local_files_only_cb.setToolTip(
"Если модель уже скачана — включите, чтобы не ждать сетевую проверку.\n"
"При первом запуске (модели нет) оставьте выключенным."
"Если модель уже скачана — включите, чтобы не ждать сетевую проверку."
)
form.addRow("", self.local_files_only_cb)
self.vad_filter_cb = QCheckBox("Использовать VAD (pyannote)")
self.vad_filter_cb.setChecked(True)
self.vad_filter_cb.setToolTip(
"Отключите, если загрузка модели виснет на 'Performing voice activity detection'."
)
form.addRow("", self.vad_filter_cb)
return w
# ---------------- visual sync ----------------
@@ -294,9 +342,11 @@ class MainWindow(QMainWindow):
active = QColor("#89b4fa")
inactive = QColor("#585b70")
bar = self.tabs.tabBar()
bar.setTabTextColor(0, active if self.convert_enabled_cb.isChecked() else inactive)
bar.setTabTextColor(1, active if self.separate_enabled_cb.isChecked() else inactive)
bar.setTabTextColor(2, active if self.transcribe_enabled_cb.isChecked() else inactive)
bar.setTabTextColor(0, active if self.shazam_enabled_cb.isChecked() else inactive)
bar.setTabTextColor(1, active if self.lyrics_enabled_cb.isChecked() else inactive)
bar.setTabTextColor(2, active if self.convert_enabled_cb.isChecked() else inactive)
bar.setTabTextColor(3, active if self.separate_enabled_cb.isChecked() else inactive)
bar.setTabTextColor(4, active if self.transcribe_enabled_cb.isChecked() else inactive)
def _sync_engine_fields(self) -> None:
is_whisperx = self.engine_cb.currentText() == "whisperx"
@@ -320,6 +370,18 @@ class MainWindow(QMainWindow):
if i >= 0:
self.mode_cb.setCurrentIndex(i)
self.shazam_enabled_cb.setChecked(cfg.shazam.enabled)
self.lyrics_enabled_cb.setChecked(cfg.lyrics.enabled)
self.lyrics_user_agent.setText(cfg.lyrics.user_agent)
self.lyrics_use_tags_cb.setChecked(cfg.lyrics.use_tags_first)
self.lyrics_use_shazam_cb.setChecked(cfg.lyrics.use_shazam_result)
self.lyrics_after_asr_cb.setChecked(cfg.lyrics.try_after_asr)
self.lyrics_transcribe_unresolved_cb.setChecked(
cfg.lyrics.transcribe_if_identified_no_lyrics
)
self.lyrics_threshold.setValue(cfg.lyrics.fuzzy_threshold)
self.convert_enabled_cb.setChecked(cfg.convert.enabled)
i = self.convert_sr.findData(cfg.convert.sample_rate)
if i >= 0:
@@ -344,7 +406,6 @@ class MainWindow(QMainWindow):
self.batch_spin.setValue(cfg.transcribe.batch_size)
self.gigaam_provider_cb.setCurrentText(cfg.transcribe.gigaam_provider)
self.local_files_only_cb.setChecked(cfg.transcribe.local_files_only)
self.vad_filter_cb.setChecked(cfg.transcribe.vad_filter)
def _collect_config(self) -> PipelineConfig:
cfg = PipelineConfig()
@@ -355,6 +416,18 @@ class MainWindow(QMainWindow):
cfg.overwrite = self.overwrite_cb.isChecked()
cfg.mode = self.mode_cb.currentData()
cfg.shazam = ShazamConfig(enabled=self.shazam_enabled_cb.isChecked())
cfg.lyrics = LyricsConfig(
enabled=self.lyrics_enabled_cb.isChecked(),
user_agent=self.lyrics_user_agent.text().strip() or "audio-transcriber/1.0",
use_tags_first=self.lyrics_use_tags_cb.isChecked(),
use_shazam_result=self.lyrics_use_shazam_cb.isChecked(),
try_after_asr=self.lyrics_after_asr_cb.isChecked(),
transcribe_if_identified_no_lyrics=self.lyrics_transcribe_unresolved_cb.isChecked(),
fuzzy_threshold=float(self.lyrics_threshold.value()),
)
cfg.convert = ConvertConfig(
enabled=self.convert_enabled_cb.isChecked(),
sample_rate=int(self.convert_sr.currentData()),
@@ -380,9 +453,8 @@ class MainWindow(QMainWindow):
compute_type=None if compute == "auto" else compute,
batch_size=self.batch_spin.value(),
gigaam_provider=self.gigaam_provider_cb.currentText(),
local_files_only=self.local_files_only_cb.isChecked(),
)
cfg.transcribe.local_files_only = self.local_files_only_cb.isChecked()
cfg.transcribe.vad_filter = self.vad_filter_cb.isChecked()
return cfg
# ---------------- handlers ----------------
@@ -425,10 +497,15 @@ class MainWindow(QMainWindow):
if not cfg.input_dir or not Path(cfg.input_dir).is_dir():
QMessageBox.warning(self, "Нет входной папки", "Выберите существующую входную папку.")
return
if not (cfg.convert.enabled or cfg.separate.enabled or cfg.transcribe.enabled):
has_work = any([
cfg.shazam.enabled, cfg.lyrics.enabled,
cfg.convert.enabled, cfg.separate.enabled, cfg.transcribe.enabled,
])
if not has_work:
QMessageBox.warning(self, "Нет этапов", "Включите хотя бы один этап.")
return
if cfg.convert.enabled and not shutil.which(cfg.convert.ffmpeg_path):
needs_ffmpeg = cfg.convert.enabled or cfg.shazam.enabled
if needs_ffmpeg and not shutil.which(cfg.convert.ffmpeg_path):
QMessageBox.warning(self, "ffmpeg не найден",
f"Команда '{cfg.convert.ffmpeg_path}' не найдена в PATH.")
return
@@ -440,8 +517,7 @@ class MainWindow(QMainWindow):
self.log_view.clear()
self._log_device_info()
if cfg.transcribe.enabled and cfg.transcribe.language is None:
self._append_log("⚠ Язык = auto: WhisperX определит язык на каждом файле. "
"Если язык известен — задайте его явно, будет быстрее.")
self._append_log("⚠ Язык = auto: WhisperX определит язык на каждом файле.")
self.progress.setValue(0)
self.progress.setFormat("Запуск…")