update shazam and liblrc services. adding text synchronizations
This commit is contained in:
2456
combined_files.txt
Normal file
2456
combined_files.txt
Normal file
File diff suppressed because it is too large
Load Diff
@@ -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"]
|
||||||
@@ -627,8 +652,8 @@ 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,
|
||||||
)
|
)
|
||||||
shutil.move(str(raw), str(target))
|
shutil.move(str(raw), str(target))
|
||||||
state[f]["audio_path"] = target
|
state[f]["audio_path"] = target
|
||||||
log(f" ✓ {f.name}")
|
log(f" ✓ {f.name}")
|
||||||
except Exception as inner: # noqa: BLE001
|
except Exception as inner: # noqa: BLE001
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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":
|
||||||
return _transcribe_whisperx(
|
try:
|
||||||
audio_path, model, language, device, compute_type,
|
return _transcribe_whisperx(
|
||||||
batch_size, local_files_only, vad_filter,
|
audio_path, model, language, device, compute_type,
|
||||||
)
|
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:
|
||||||
|
|||||||
Reference in New Issue
Block a user