render lyrics digest grouped by mood

This commit is contained in:
Andrew Pimenov 2026-08-16 19:42:33 +03:00
parent 8eae62fb63
commit d0682f8341
2 changed files with 123 additions and 0 deletions

View file

@ -439,6 +439,56 @@ def render_digest(
return "\n".join(lines).rstrip() + "\n"
def _lyrics_track_names(tracks_cache: dict[str, dict]) -> dict[str, str]:
names: dict[str, str] = {}
for t in tracks_cache.values():
names[str(t["id"])] = f"{', '.join(t['artists'])} — {t['title']}"
return names
def render_lyrics_digest(tracks_cache: dict[str, dict], analysis: dict[str, dict]) -> str:
names = _lyrics_track_names(tracks_cache)
entries = [(tid, e) for tid, e in analysis.items() if "error" not in e]
failed = len(analysis) - len(analyzed_entries(analysis))
lines = [
"# Разбор текстов (RU-треки)",
"",
f"Разобрано треков: {len(entries)}. Группировка по основному настроению; "
"цитата приводится только если сверена с текстом дословно.",
"",
]
by_mood: dict[str, list[tuple[str, dict]]] = {}
for tid, entry in entries:
by_mood.setdefault(entry["mood"][0], []).append((tid, entry))
for mood, items in sorted(by_mood.items(), key=lambda kv: (-len(kv[1]), kv[0])):
lines.append(f"## {mood} ({len(items)})")
lines.append("")
for tid, entry in sorted(items, key=lambda kv: names.get(kv[0], kv[0])):
lines.append(f"### {names.get(tid, f'трек {tid}')}")
lines.append(
f"- Настроение: {', '.join(entry['mood'])}; арка: {entry['emotional_arc']}; "
f"ракурс: {entry['pov']}; тон: {entry['stance']}; речь: {entry['register']}; "
f"конкретность: {entry['concreteness']}"
)
lines.append(f"- Темы: {', '.join(entry['themes'])}")
if entry.get("key_line_verified"):
lines.append(f"- Цитата: «{entry['key_line']}»")
lines.append("")
lines.append(entry["summary"])
lines.append("")
lines.append(f"Чем может цеплять: {entry['resonance']}")
lines.append("")
if failed:
lines.append(f"Не разобрано: {failed} (таймаут или сбой модели, повторный `analyze` попробует снова).")
lines.append("")
return "\n".join(lines).rstrip() + "\n"
def write_digest(text: str, out_dir: Path, filename: str = DIGEST_FILE) -> Path:
out_dir.mkdir(parents=True, exist_ok=True)
out_file = out_dir / filename

View file

@ -330,3 +330,76 @@ def test_render_digest_with_analysis_includes_lyrics_section():
text = digest.render_digest(rows, tracks_cache, {}, top_artists=10, top_albums=10, analysis=ANALYSIS)
assert "## Лирика (RU-подмножество)" in text
LYRICS_TRACKS = {
"100:10": {"id": 100, "album_id": 10, "title": "Тоска", "artists": ["Артист"]},
"200:20": {"id": 200, "album_id": 20, "title": "Город", "artists": ["Другой"]},
}
FULL_ANALYSIS = {
"100": {
"mood": ["melancholy", "longing"],
"themes": ["city", "memory"],
"emotional_arc": "descent",
"pov": "confessional",
"register": "conversational",
"concreteness": "abstract",
"stance": "sincere",
"confidence": 0.9,
"summary": "Герой бродит по городу.",
"resonance": "Узнаваемое чувство одиночества.",
"key_line": "А я иду, шагаю по Москве",
"key_line_verified": True,
},
"200": {
"mood": ["melancholy"],
"themes": ["city"],
"emotional_arc": "static",
"pov": "narrative",
"register": "poetic",
"concreteness": "concrete",
"stance": "ironic",
"confidence": 0.8,
"summary": "Наблюдение за городом.",
"resonance": "Городская меланхолия.",
"key_line": "Пересказ вместо цитаты",
"key_line_verified": False,
},
"300": {"error": "TimeoutError: модель зависла"},
}
def test_render_lyrics_digest_lists_tracks_with_narrative():
text = digest.render_lyrics_digest(LYRICS_TRACKS, FULL_ANALYSIS)
assert "Артист — Тоска" in text
assert "Герой бродит по городу." in text
assert "Узнаваемое чувство одиночества." in text
assert "city, memory" in text
def test_render_lyrics_digest_omits_unverified_key_line():
text = digest.render_lyrics_digest(LYRICS_TRACKS, FULL_ANALYSIS)
assert "А я иду, шагаю по Москве" in text
assert "Пересказ вместо цитаты" not in text, "непроверенная цитата - вероятный пересказ модели"
def test_render_lyrics_digest_reports_failed_tracks_without_rendering_them():
text = digest.render_lyrics_digest(LYRICS_TRACKS, FULL_ANALYSIS)
assert "TimeoutError" not in text
assert "Не разобрано: 1" in text
def test_render_lyrics_digest_groups_by_primary_mood():
text = digest.render_lyrics_digest(LYRICS_TRACKS, FULL_ANALYSIS)
assert "## melancholy (2)" in text
def test_render_lyrics_digest_handles_track_missing_from_cache():
text = digest.render_lyrics_digest({}, {"100": FULL_ANALYSIS["100"]})
assert "трек 100" in text