mirror of
https://github.com/Horizontal42/Sort_YM.git
synced 2026-11-11 02:12:00 +03:00
lyrics aggregate block in digest.md
This commit is contained in:
parent
74fcb6736d
commit
8eae62fb63
2 changed files with 115 additions and 0 deletions
|
|
@ -9,11 +9,14 @@ from . import classify, genres, report
|
|||
|
||||
DIGEST_FILE = "digest.md"
|
||||
DIGEST_SOURCE_FILE = "digest_source.md"
|
||||
LYRICS_DIGEST_FILE = "lyrics_digest.md"
|
||||
|
||||
DECADE_UNKNOWN = "unknown"
|
||||
DECADE_MIN_YEAR = 1900
|
||||
DECADE_MAX_YEAR = 2100
|
||||
DECADE_TOP_GENRES = 3 # константа вёрстки (сколько жанров показывать на десятилетие), не config-knob
|
||||
LYRICS_TOP_MOODS = 6
|
||||
LYRICS_TOP_THEMES = 8
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
|
|
@ -257,6 +260,60 @@ def _pct(n: int, total: int) -> int:
|
|||
return round(100 * n / total) if total else 0
|
||||
|
||||
|
||||
def analyzed_entries(analysis: dict[str, dict]) -> list[dict]:
|
||||
"""Только успешные записи - таймауты и битые ответы хранятся в том же кэше с полем error."""
|
||||
return [e for e in analysis.values() if "error" not in e]
|
||||
|
||||
|
||||
def _counted(counter: Counter, key: str, limit: int | None = None) -> list[dict]:
|
||||
items = sorted(counter.items(), key=lambda kv: (-kv[1], kv[0]))
|
||||
if limit is not None:
|
||||
items = items[:limit]
|
||||
return [{key: name, "tracks": n} for name, n in items]
|
||||
|
||||
|
||||
def mood_stats(analysis: dict[str, dict]) -> list[dict]:
|
||||
counter: Counter[str] = Counter()
|
||||
for entry in analyzed_entries(analysis):
|
||||
counter.update(entry.get("mood", []))
|
||||
return _counted(counter, "mood")
|
||||
|
||||
|
||||
def theme_stats(analysis: dict[str, dict], limit: int) -> list[dict]:
|
||||
counter: Counter[str] = Counter()
|
||||
for entry in analyzed_entries(analysis):
|
||||
counter.update(entry.get("themes", []))
|
||||
return _counted(counter, "theme", limit)
|
||||
|
||||
|
||||
def lyrics_block(analysis: dict[str, dict]) -> list[str]:
|
||||
"""Компактный агрегат разбора лирики для digest.md.
|
||||
|
||||
Сам разбор (266 нарративных блоков) живёт в out/lyrics_digest.md - digest.md существует
|
||||
ради фиксированного потолка ~150 строк, и класть их сюда нельзя.
|
||||
"""
|
||||
entries = analyzed_entries(analysis)
|
||||
if not entries:
|
||||
return []
|
||||
|
||||
moods = ", ".join(f"{m['mood']} {m['tracks']}" for m in mood_stats(analysis)[:LYRICS_TOP_MOODS])
|
||||
themes = ", ".join(f"{t['theme']} {t['tracks']}" for t in theme_stats(analysis, LYRICS_TOP_THEMES))
|
||||
stances = Counter(e["stance"] for e in entries if "stance" in e)
|
||||
povs = Counter(e["pov"] for e in entries if "pov" in e)
|
||||
stance_part = " / ".join(f"{name} {n}" for name, n in sorted(stances.items(), key=lambda kv: (-kv[1], kv[0])))
|
||||
pov_part = " / ".join(f"{name} {n}" for name, n in sorted(povs.items(), key=lambda kv: (-kv[1], kv[0])))
|
||||
|
||||
return [
|
||||
"## Лирика (RU-подмножество)",
|
||||
f"Разбор текстов {len(entries)} RU-треков локальной моделью; подробнее — out/{LYRICS_DIGEST_FILE}.",
|
||||
f"- Настроения: {moods}",
|
||||
f"- Темы: {themes}",
|
||||
f"- Тон: {stance_part}",
|
||||
f"- Ракурс: {pov_part}",
|
||||
"",
|
||||
]
|
||||
|
||||
|
||||
def render_digest(
|
||||
rows: list[dict],
|
||||
tracks_cache: dict[str, dict],
|
||||
|
|
@ -264,6 +321,7 @@ def render_digest(
|
|||
top_artists: int,
|
||||
top_albums: int,
|
||||
wording: Wording = DEFAULT_WORDING,
|
||||
analysis: dict[str, dict] | None = None,
|
||||
) -> str:
|
||||
total = len(rows)
|
||||
unique_artists = {name for t in tracks_cache.values() for name in t["artists"]}
|
||||
|
|
@ -320,6 +378,8 @@ def render_digest(
|
|||
lines.append(f"- {label}: {n} ({_pct(n, total)}%)")
|
||||
lines.append("")
|
||||
|
||||
lines.extend(lyrics_block(analysis or {}))
|
||||
|
||||
lines.append("## Десятилетия")
|
||||
lines.append("В скобках — доля от библиотеки; далее — три самые частые крупные корзины десятилетия.")
|
||||
for d in decade_rows:
|
||||
|
|
|
|||
|
|
@ -275,3 +275,58 @@ def test_digest_line_count_stays_bounded_on_large_library():
|
|||
text = digest.render_digest(rows, tracks_cache, {}, top_artists=40, top_albums=15)
|
||||
|
||||
assert len(text.splitlines()) < 250
|
||||
|
||||
|
||||
ANALYSIS = {
|
||||
"100": {"mood": ["melancholy", "longing"], "themes": ["city", "memory"], "stance": "sincere", "pov": "confessional"},
|
||||
"200": {"mood": ["melancholy"], "themes": ["city"], "stance": "ironic", "pov": "narrative"},
|
||||
"300": {"error": "TimeoutError: модель зависла"},
|
||||
}
|
||||
|
||||
|
||||
def test_mood_stats_counts_every_mood_of_a_track():
|
||||
assert digest.mood_stats(ANALYSIS) == [
|
||||
{"mood": "melancholy", "tracks": 2},
|
||||
{"mood": "longing", "tracks": 1},
|
||||
]
|
||||
|
||||
|
||||
def test_theme_stats_limits_and_orders():
|
||||
assert digest.theme_stats(ANALYSIS, limit=1) == [{"theme": "city", "tracks": 2}]
|
||||
|
||||
|
||||
def test_analyzed_entries_excludes_error_markers():
|
||||
assert len(digest.analyzed_entries(ANALYSIS)) == 2
|
||||
|
||||
|
||||
def test_lyrics_block_is_compact():
|
||||
lines = digest.lyrics_block(ANALYSIS)
|
||||
|
||||
assert lines[0] == "## Лирика (RU-подмножество)"
|
||||
assert len(lines) <= 7, "агрегат в digest.md должен оставаться компактным"
|
||||
assert any("melancholy 2" in line for line in lines)
|
||||
assert any("city 2" in line for line in lines)
|
||||
|
||||
|
||||
def test_lyrics_block_empty_without_analysis():
|
||||
assert digest.lyrics_block({}) == []
|
||||
assert digest.lyrics_block({"300": {"error": "TimeoutError: ..."}}) == []
|
||||
|
||||
|
||||
def test_render_digest_without_analysis_has_no_lyrics_section():
|
||||
# Обратная совместимость: без analysis дайджест прежний, как в test_default_wording_matches_likes_output.
|
||||
tracks_cache = {"100:1000": _track(100, "Тоска", ["Артист"], "rock")}
|
||||
rows = _rows(tracks_cache)
|
||||
|
||||
text = digest.render_digest(rows, tracks_cache, {}, top_artists=10, top_albums=10)
|
||||
|
||||
assert "## Лирика" not in text
|
||||
|
||||
|
||||
def test_render_digest_with_analysis_includes_lyrics_section():
|
||||
tracks_cache = {"100:1000": _track(100, "Тоска", ["Артист"], "rock")}
|
||||
rows = _rows(tracks_cache)
|
||||
|
||||
text = digest.render_digest(rows, tracks_cache, {}, top_artists=10, top_albums=10, analysis=ANALYSIS)
|
||||
|
||||
assert "## Лирика (RU-подмножество)" in text
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue