196 lines
7.0 KiB
Python
196 lines
7.0 KiB
Python
"""LRC-generation and LRC-embedding pipelines."""
|
|
from __future__ import annotations
|
|
|
|
import shutil
|
|
import tempfile
|
|
import threading
|
|
from pathlib import Path
|
|
from typing import Callable, Optional
|
|
|
|
from .config import PipelineConfig
|
|
from .convert import convert_to_wav
|
|
from .lrc import embed_lrc, is_lrc, read_embedded_lyrics, segments_to_lrc
|
|
from .metadata import read_metadata
|
|
from .pipeline import FileResult, _save_unrecognized, collect_audio_files
|
|
from .transcribe import transcribe_aligned
|
|
|
|
|
|
LogFn = Callable[[str], None]
|
|
ProgressFn = Callable[[int, int, str], None]
|
|
|
|
|
|
def _pick_audio_source(
|
|
f: Path, output_dir: Path, config: PipelineConfig, work_dir: Path, log: LogFn,
|
|
) -> Path:
|
|
mode = (config.lrc.audio_source or "auto").lower()
|
|
vocals = output_dir / f.stem / "vocals.wav"
|
|
convert = output_dir / f.stem / "convert.wav"
|
|
if mode == "vocals":
|
|
return vocals
|
|
if mode == "convert":
|
|
return convert
|
|
if mode == "input":
|
|
return f
|
|
if vocals.exists():
|
|
return vocals
|
|
if convert.exists():
|
|
return convert
|
|
target = work_dir / "input_16k.wav"
|
|
log(f" [convert] {f.name} -> 16k mono WAV (для align)")
|
|
convert_to_wav(
|
|
f, target, sample_rate=16000, channels=1,
|
|
ffmpeg_path=config.convert.ffmpeg_path,
|
|
)
|
|
return target
|
|
|
|
|
|
def run_lrc_pipeline(
|
|
config: PipelineConfig,
|
|
log: LogFn = print,
|
|
on_progress: Optional[ProgressFn] = None,
|
|
stop_event: Optional[threading.Event] = None,
|
|
) -> list[FileResult]:
|
|
input_dir = Path(config.input_dir)
|
|
if not input_dir.is_dir():
|
|
raise FileNotFoundError(f"Input directory not found: {input_dir}")
|
|
output_dir = Path(config.output_dir)
|
|
output_dir.mkdir(parents=True, exist_ok=True)
|
|
|
|
files = collect_audio_files(input_dir, config.recursive)
|
|
if not files:
|
|
log("Аудиофайлы не найдены.")
|
|
return []
|
|
|
|
lang = config.lrc.language or config.transcribe.language
|
|
log(f"LRC: {len(files)} файлов | язык={lang or 'auto'} | "
|
|
f"источник={config.lrc.audio_source}")
|
|
|
|
results: list[FileResult] = []
|
|
total = len(files)
|
|
|
|
for idx, f in enumerate(files, 1):
|
|
if stop_event is not None and stop_event.is_set():
|
|
log("⏹ Отменено")
|
|
break
|
|
if on_progress:
|
|
on_progress(idx - 1, total, "lrc")
|
|
log(f"\n▶ [{idx}/{total}] {f.name}")
|
|
|
|
track_out = output_dir / f.stem
|
|
track_out.mkdir(parents=True, exist_ok=True)
|
|
lrc_path = track_out / "text.lrc"
|
|
error: Optional[str] = None
|
|
|
|
tmp = Path(tempfile.mkdtemp(prefix=f"at_lrc_{f.stem}_"))
|
|
try:
|
|
embedded = read_embedded_lyrics(f) if config.lyrics.use_embedded_lyrics else None
|
|
if embedded and is_lrc(embedded):
|
|
lrc_path.write_text(embedded, encoding="utf-8")
|
|
log(" ✓ text.lrc (взят из тегов аудио)")
|
|
results.append(FileResult(f, None, track_out, None, "embedded+lrc"))
|
|
continue
|
|
|
|
audio_path = _pick_audio_source(f, output_dir, config, tmp, log)
|
|
if not audio_path.exists():
|
|
raise FileNotFoundError(
|
|
f"Источник аудио не найден: {audio_path}. "
|
|
"Включите conversion/separation в основном пайплайне "
|
|
"или выберите audio_source=input."
|
|
)
|
|
log(f" [align] {audio_path.name}")
|
|
|
|
segments, detected = transcribe_aligned(
|
|
audio_path,
|
|
model=config.transcribe.model,
|
|
language=lang,
|
|
device=config.lrc.device,
|
|
compute_type=config.transcribe.compute_type,
|
|
batch_size=config.transcribe.batch_size,
|
|
align_model=config.lrc.align_model,
|
|
local_files_only=config.transcribe.local_files_only,
|
|
)
|
|
|
|
lrc_content = segments_to_lrc(segments)
|
|
lrc_path.write_text(lrc_content, encoding="utf-8")
|
|
log(f" ✓ text.lrc ({len(segments)} строк, язык: {detected})")
|
|
|
|
# NEW: save to JSONL for LRCLIB submission
|
|
_save_unrecognized(
|
|
config, f, lrc_content, source="lrc",
|
|
metadata=read_metadata(f),
|
|
)
|
|
results.append(FileResult(f, None, track_out, None, "lrc"))
|
|
except Exception as e: # noqa: BLE001
|
|
error = f"{type(e).__name__}: {e}"
|
|
log(f" ✗ Ошибка: {error}")
|
|
results.append(FileResult(f, None, None, error, "lrc"))
|
|
finally:
|
|
shutil.rmtree(tmp, ignore_errors=True)
|
|
if on_progress:
|
|
on_progress(idx, total, "lrc")
|
|
|
|
return results
|
|
|
|
|
|
def run_embed_pipeline(
|
|
config: PipelineConfig,
|
|
log: LogFn = print,
|
|
on_progress: Optional[ProgressFn] = None,
|
|
stop_event: Optional[threading.Event] = None,
|
|
) -> list[FileResult]:
|
|
input_dir = Path(config.input_dir)
|
|
if not input_dir.is_dir():
|
|
raise FileNotFoundError(f"Input directory not found: {input_dir}")
|
|
output_dir = Path(config.output_dir)
|
|
output_dir.mkdir(parents=True, exist_ok=True)
|
|
|
|
files = collect_audio_files(input_dir, config.recursive)
|
|
if not files:
|
|
log("Аудиофайлы не найдены.")
|
|
return []
|
|
|
|
log(f"Embed LRC: {len(files)} файлов | "
|
|
f"режим={'in-place' if config.lrc.embed_in_place else 'копия в output'}")
|
|
|
|
results: list[FileResult] = []
|
|
total = len(files)
|
|
|
|
for idx, f in enumerate(files, 1):
|
|
if stop_event is not None and stop_event.is_set():
|
|
log("⏹ Отменено")
|
|
break
|
|
if on_progress:
|
|
on_progress(idx - 1, total, "embed")
|
|
|
|
lrc_path = output_dir / f.stem / "text.lrc"
|
|
error: Optional[str] = None
|
|
|
|
try:
|
|
if not lrc_path.exists():
|
|
log(f" ⊘ {f.name}: text.lrc отсутствует, пропуск")
|
|
results.append(FileResult(f, None, None, "no LRC", "embed"))
|
|
continue
|
|
|
|
lrc_content = lrc_path.read_text(encoding="utf-8")
|
|
|
|
if config.lrc.embed_in_place:
|
|
target = f
|
|
else:
|
|
target = output_dir / f.stem / f.name
|
|
target.parent.mkdir(parents=True, exist_ok=True)
|
|
if not target.exists() or config.overwrite:
|
|
shutil.copy2(f, target)
|
|
|
|
embed_lrc(target, lrc_content)
|
|
log(f" ✓ {f.name} → {target if config.lrc.embed_in_place else target.name}")
|
|
results.append(FileResult(f, None, target.parent, None, "embed"))
|
|
except Exception as e: # noqa: BLE001
|
|
error = f"{type(e).__name__}: {e}"
|
|
log(f" ✗ {f.name}: {error}")
|
|
results.append(FileResult(f, None, None, error, "embed"))
|
|
finally:
|
|
if on_progress:
|
|
on_progress(idx, total, "embed")
|
|
|
|
return results
|