update shazam and liblrc services. adding text synchronizations

This commit is contained in:
jadssss
2026-10-24 08:30:35 +03:00
parent 1e8e5e4433
commit 78636699dc
4 changed files with 2617 additions and 27 deletions

2456
combined_files.txt Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -19,6 +19,7 @@ import json
import shutil import shutil
import tempfile import tempfile
import threading import threading
import re
from dataclasses import dataclass, field from dataclasses import dataclass, field
from pathlib import Path from pathlib import Path
from typing import Callable, Optional from typing import Callable, Optional
@@ -212,17 +213,40 @@ def _try_post_asr_match(
config: PipelineConfig, config: PipelineConfig,
log: LogFn, log: LogFn,
) -> Optional[str]: ) -> Optional[str]:
"""Try to find the actual lyrics via LRCLIB using recognized text. Returns if not text:
the lyrics text on a confident match, else None."""
if not text or len(text.split()) < 3:
return None return None
head = _text_head(text, 8) # Filter out common ASR watermarks/credits
log(f" LRCLIB по тексту: {head!r}...") ignore_phrases = ["субтитры", "редактор", "перевод", "сделал", "озвучка"]
lines = []
for line in text.splitlines():
line = line.strip()
if not line:
continue
if any(p in line.lower() for p in ignore_phrases):
continue
lines.append(line)
if not lines:
return None
# Take the first line with at least 3 words, strip punctuation
query = ""
for line in lines:
words = line.split()
if len(words) >= 3:
clean_words = [re.sub(r'[^\w\s]', '', w) for w in words]
query = " ".join(clean_words[:6])
break
if not query:
return None
log(f" LRCLIB по тексту: {query!r}...")
try: try:
candidates = search_by_text( candidates = search_by_text(
head, limit=5, query, limit=5,
user_agent=config.lyrics.user_agent, user_agent=config.lyrics.user_agent,
timeout=config.lyrics.timeout, timeout=config.lyrics.timeout,
) )
@@ -597,6 +621,7 @@ def _run_staged(
isolate_stem=config.separate.isolate_stem, isolate_stem=config.separate.isolate_stem,
unblend_path=config.separate.unblend_path, unblend_path=config.separate.unblend_path,
custom_kernels=config.separate.custom_kernels, custom_kernels=config.separate.custom_kernels,
progress_callback=lambda p: on_progress(p, len(todo), "separate") if on_progress else None,
) )
for f in todo: for f in todo:
src = state[f]["audio_path"] src = state[f]["audio_path"]

View File

@@ -1,4 +1,11 @@
"""Vocal separation via unblend (Mel-Band RoFormer).""" """Vocal separation via unblend (Mel-Band RoFormer).
Two entry points:
- separate_vocals() single file, subprocess per call (sequential mode)
- separate_vocals_batch() many files in ONE subprocess, model loaded once
(staged mode — big speedup on batches)
"""
from __future__ import annotations from __future__ import annotations
import os import os
@@ -6,6 +13,19 @@ import subprocess
from pathlib import Path from pathlib import Path
def _build_env(custom_kernels: bool) -> dict:
env = os.environ.copy()
env["UNBLEND_CUSTOM_KERNELS"] = "1" if custom_kernels else "0"
return env
def _check_result(result: subprocess.CompletedProcess, what: str) -> None:
if result.returncode != 0:
raise RuntimeError(
f"unblend failed ({what}):\n{result.stderr.strip()[:1200]}"
)
def separate_vocals( def separate_vocals(
input_wav: Path, input_wav: Path,
work_dir: Path, work_dir: Path,
@@ -13,8 +33,9 @@ def separate_vocals(
isolate_stem: str = "vocals", isolate_stem: str = "vocals",
unblend_path: str = "unblend", unblend_path: str = "unblend",
custom_kernels: bool = False, custom_kernels: bool = False,
device: str = "cuda",
) -> Path: ) -> Path:
"""Separate the isolated stem from input_wav. Returns path to the stem WAV.""" """Separate one file. Returns path to the isolated stem WAV."""
work_dir.mkdir(parents=True, exist_ok=True) work_dir.mkdir(parents=True, exist_ok=True)
output_pattern = work_dir / "{track}" / "{stem}.wav" output_pattern = work_dir / "{track}" / "{stem}.wav"
cmd = [ cmd = [
@@ -22,15 +43,10 @@ def separate_vocals(
"-m", model, "-m", model,
"-o", str(output_pattern), "-o", str(output_pattern),
"--isolate-stem", isolate_stem, "--isolate-stem", isolate_stem,
"--device", device,
] ]
env = os.environ.copy() result = subprocess.run(cmd, capture_output=True, text=True, env=_build_env(custom_kernels))
env["UNBLEND_CUSTOM_KERNELS"] = "1" if custom_kernels else "0" _check_result(result, input_wav.name)
result = subprocess.run(cmd, capture_output=True, text=True, env=env)
if result.returncode != 0:
raise RuntimeError(
f"unblend failed for {input_wav.name}:\n{result.stderr.strip()[:800]}"
)
candidates = sorted(work_dir.rglob(f"{isolate_stem}.wav")) candidates = sorted(work_dir.rglob(f"{isolate_stem}.wav"))
if not candidates: if not candidates:
@@ -38,3 +54,62 @@ def separate_vocals(
f"unblend finished but stem '{isolate_stem}.wav' was not found in {work_dir}" f"unblend finished but stem '{isolate_stem}.wav' was not found in {work_dir}"
) )
return candidates[0] return candidates[0]
def separate_vocals_batch(
input_wavs: list[Path],
work_root: Path,
model: str = "bs_roformer_anvuew",
isolate_stem: str = "vocals",
unblend_path: str = "unblend",
custom_kernels: bool = False,
device: str = "cuda",
) -> dict[Path, Path]:
"""Separate many files in one unblend invocation.
Input WAVs are expected to live in distinct directories
(e.g. <work_root>/<stem>/convert.wav). The output pattern uses {parent}
so each result lands next to its input as <parent>/vocals.wav.
Returns: {input_path: vocal_path}
"""
if not input_wavs:
return {}
work_root.mkdir(parents=True, exist_ok=True)
output_pattern = work_root / "{parent}" / "{stem}.wav"
cmd = [
unblend_path, "separate",
*[str(p) for p in input_wavs],
"-m", model,
"-o", str(output_pattern),
"--isolate-stem", isolate_stem,
"--device", device,
]
result = subprocess.run(cmd, capture_output=True, text=True, env=_build_env(custom_kernels))
_check_result(result, f"{len(input_wavs)} files")
mapping: dict[Path, Path] = {}
missing: list[Path] = []
for src in input_wavs:
candidate = src.parent / f"{isolate_stem}.wav"
if candidate.exists():
mapping[src] = candidate
else:
missing.append(src)
if missing:
# Fallback: search anywhere under work_root
found = sorted(work_root.rglob(f"{isolate_stem}.wav"))
if len(found) == len(input_wavs):
for src, dst in zip(input_wavs, found):
mapping[src] = dst
else:
names = ", ".join(p.name for p in missing[:3])
raise RuntimeError(
f"unblend finished but outputs missing for {len(missing)} file(s): {names}"
)
return mapping

View File

@@ -1,11 +1,11 @@
"""Speech recognition via WhisperX or GigaAM (onnx-asr). """Speech recognition via WhisperX or GigaAM (onnx-asr).
Models are cached per (name, device, compute_type) so loading happens once Includes aggressive VRAM cleanup to prevent OOM on long batches,
per process, not once per file. Loading is retried a few times to survive and automatic model reload if the CUDA context gets corrupted.
transient HuggingFace / SSL hiccups.
""" """
from __future__ import annotations from __future__ import annotations
import gc
import time import time
from pathlib import Path from pathlib import Path
from typing import Optional from typing import Optional
@@ -26,7 +26,7 @@ def _retry(fn, attempts: int = 3, base_delay: float = 2.0):
for i in range(attempts): for i in range(attempts):
try: try:
return fn() return fn()
except Exception as e: # noqa: BLE001 except Exception as e:
last = e last = e
if i < attempts - 1: if i < attempts - 1:
time.sleep(base_delay * (i + 1)) time.sleep(base_delay * (i + 1))
@@ -93,11 +93,31 @@ def transcribe(
vad_filter: bool = True, vad_filter: bool = True,
) -> str: ) -> str:
device = _resolve_device(device) device = _resolve_device(device)
if engine == "whisperx": if engine == "whisperx":
try:
return _transcribe_whisperx( return _transcribe_whisperx(
audio_path, model, language, device, compute_type, audio_path, model, language, device, compute_type,
batch_size, local_files_only, vad_filter, batch_size, local_files_only, vad_filter,
) )
except Exception as e:
err = str(e).lower()
if "out of memory" in err or "cuda" in err or "device ordinal" in err:
print(f" [ASR] CUDA error: {e}. Clearing model cache and retrying...")
_WhisperXModels._cache.clear()
gc.collect()
try:
import torch
if torch.cuda.is_available():
torch.cuda.empty_cache()
except ImportError:
pass
return _transcribe_whisperx(
audio_path, model, language, device, compute_type,
batch_size, local_files_only, vad_filter,
)
raise
if engine == "gigaam": if engine == "gigaam":
return _transcribe_gigaam(audio_path, gigaam_provider) return _transcribe_gigaam(audio_path, gigaam_provider)
raise ValueError(f"Unknown ASR engine: {engine!r}") raise ValueError(f"Unknown ASR engine: {engine!r}")
@@ -121,7 +141,21 @@ def _transcribe_whisperx(
batch_size=batch_size, batch_size=batch_size,
language=language, language=language,
) )
return " ".join(seg["text"].strip() for seg in result["segments"]).strip()
text = " ".join(seg["text"].strip() for seg in result["segments"]).strip()
# Free VRAM aggressively to prevent fragmentation/OOM on long batches
del audio
del result
gc.collect()
try:
import torch
if torch.cuda.is_available():
torch.cuda.empty_cache()
except ImportError:
pass
return text
def _transcribe_gigaam(audio_path: Path, provider: str) -> str: def _transcribe_gigaam(audio_path: Path, provider: str) -> str: