lyrics --all-languages flag to opt out of the ru filter

This commit is contained in:
Andrew Pimenov 2026-08-16 19:50:35 +03:00
parent 2344889418
commit 1c0f6928bf
4 changed files with 62 additions and 18 deletions

View file

@ -71,16 +71,19 @@ def cmd_lyrics(args: argparse.Namespace) -> None:
if not tracks_cache: if not tracks_cache:
raise SystemExit("Кэш треков пуст. Сначала запустите: python -m sort_ym fetch") raise SystemExit("Кэш треков пуст. Сначала запустите: python -m sort_ym fetch")
# Тот же вызов, что в cmd_report: инкрементальный, на заполненном кэше почти no-op, lang_cache: dict[str, str | None] = {}
# но без него RU-гейт опирался бы только на алфавитную эвристику. if not args.all_languages:
ids_needing_lang = [str(t["id"]) for t in tracks_cache.values() if t["lyrics_available"]] # Тот же вызов, что в cmd_report: инкрементальный, на заполненном кэше почти no-op,
lang_cache = language.fetch_api_languages(client, ids_needing_lang, cfg.cache_dir, cfg.lyrics_request_delay) # но без него RU-гейт опирался бы только на алфавитную эвристику.
ids_needing_lang = [str(t["id"]) for t in tracks_cache.values() if t["lyrics_available"]]
lang_cache = language.fetch_api_languages(client, ids_needing_lang, cfg.cache_dir, cfg.lyrics_request_delay)
ids = lyrics.ru_lyric_track_ids(tracks_cache, lang_cache) ids = lyrics.ru_lyric_track_ids(tracks_cache, lang_cache, all_languages=args.all_languages)
if args.limit is not None: if args.limit is not None:
ids = ids[: args.limit] ids = ids[: args.limit]
print(f"RU-треков с текстом: {len(ids)}") label = "треков" if args.all_languages else "RU-треков"
print(f"{label} с текстом: {len(ids)}")
lyrics.fetch_lyrics_text(client, ids, cfg.cache_dir, cfg.lyrics_request_delay) lyrics.fetch_lyrics_text(client, ids, cfg.cache_dir, cfg.lyrics_request_delay)
@ -263,6 +266,11 @@ def main(argv: list[str] | None = None) -> None:
p_lyrics = sub.add_parser("lyrics", help="загрузить тексты песен RU-треков в cache/lyrics_text.json") p_lyrics = sub.add_parser("lyrics", help="загрузить тексты песен RU-треков в cache/lyrics_text.json")
p_lyrics.add_argument("--limit", type=int, default=None, help="ограничить количество треков (для пробного запуска)") p_lyrics.add_argument("--limit", type=int, default=None, help="ограничить количество треков (для пробного запуска)")
p_lyrics.add_argument(
"--all-languages",
action="store_true",
help="не фильтровать по языку - загрузить тексты всех треков с лирикой, а не только RU",
)
p_analyze = sub.add_parser( p_analyze = sub.add_parser(
"analyze", "analyze",

View file

@ -13,13 +13,19 @@ from .ymclient import with_retries
LYRICS_TEXT_CACHE_FILE = "lyrics_text.json" LYRICS_TEXT_CACHE_FILE = "lyrics_text.json"
def ru_lyric_track_ids(tracks_cache: dict[str, dict], lang_cache: dict[str, str | None]) -> list[str]: def ru_lyric_track_ids(
"""Числовые id RU-треков, у которых вообще есть текст. tracks_cache: dict[str, dict],
lang_cache: dict[str, str | None],
all_languages: bool = False,
) -> list[str]:
"""Числовые id треков, у которых вообще есть текст.
Гейт двойной: lyrics_available (иначе track_supplement заведомо вернёт запись без lyrics) По умолчанию - только RU: на незнакомом языке текст песни несёт для языковой модели меньше
и RU - анализ INT-лирики вне скоупа фичи. Дедупликация нужна потому, что один и тот же сигнала, чем разбор трека, который слушатель понимает дословно. all_languages=True снимает
трек может быть лайкнут с разных альбомов: ключи tracks_cache при этом разные, а числовой языковой фильтр, оставляя только lyrics_available (иначе track_supplement заведомо вернёт
id (и, соответственно, ключ кэша текстов) - один. запись без lyrics). Дедупликация нужна потому, что один и тот же трек может быть лайкнут с
разных альбомов: ключи tracks_cache при этом разные, а числовой id (и, соответственно, ключ
кэша текстов) - один.
""" """
ids: list[str] = [] ids: list[str] = []
seen: set[str] = set() seen: set[str] = set()
@ -29,10 +35,12 @@ def ru_lyric_track_ids(tracks_cache: dict[str, dict], lang_cache: dict[str, str
tid = str(t["id"]) tid = str(t["id"])
if tid in seen: if tid in seen:
continue continue
lang = language.detect_language(t["title"], t["artists"], t["genre_raw"], lang_cache.get(tid)) if not all_languages:
if lang == "RU": lang = language.detect_language(t["title"], t["artists"], t["genre_raw"], lang_cache.get(tid))
seen.add(tid) if lang != "RU":
ids.append(tid) continue
seen.add(tid)
ids.append(tid)
return ids return ids

View file

@ -20,11 +20,22 @@ def test_apply_without_yes_fails_regardless_of_source():
def test_lyrics_command_is_wired(monkeypatch): def test_lyrics_command_is_wired(monkeypatch):
called = {} called = {}
monkeypatch.setattr(cli, "cmd_lyrics", lambda args: called.update(limit=args.limit)) monkeypatch.setattr(
cli, "cmd_lyrics", lambda args: called.update(limit=args.limit, all_languages=args.all_languages)
)
cli.main(["lyrics", "--limit", "5"]) cli.main(["lyrics", "--limit", "5"])
assert called == {"limit": 5} assert called == {"limit": 5, "all_languages": False}
def test_lyrics_command_all_languages_flag(monkeypatch):
called = {}
monkeypatch.setattr(cli, "cmd_lyrics", lambda args: called.update(all_languages=args.all_languages))
cli.main(["lyrics", "--all-languages"])
assert called == {"all_languages": True}
def test_analyze_command_is_wired(monkeypatch): def test_analyze_command_is_wired(monkeypatch):

View file

@ -55,6 +55,23 @@ def test_ru_lyric_track_ids_deduplicates_same_numeric_id():
assert lyrics.ru_lyric_track_ids(tracks_cache, lang_cache={}) == ["100"] assert lyrics.ru_lyric_track_ids(tracks_cache, lang_cache={}) == ["100"]
def test_ru_lyric_track_ids_all_languages_includes_int():
tracks_cache = {
"100:10": track(100, title="Тоска", artists=["Гражданская Оборона"]),
"200:10": track(200, title="Come as You Are", artists=["Nirvana"]),
}
ids = lyrics.ru_lyric_track_ids(tracks_cache, lang_cache={}, all_languages=True)
assert ids == ["100", "200"]
def test_ru_lyric_track_ids_all_languages_still_gates_on_lyrics_available():
tracks_cache = {"100:10": track(100, lyrics_available=False)}
assert lyrics.ru_lyric_track_ids(tracks_cache, lang_cache={}, all_languages=True) == []
class FakeLyrics: class FakeLyrics:
def __init__(self, full_lyrics): def __init__(self, full_lyrics):
self.full_lyrics = full_lyrics self.full_lyrics = full_lyrics