document lyrics and analyze commands

This commit is contained in:
Andrew Pimenov 2026-08-16 19:49:12 +03:00
parent d8f4e16f14
commit 2344889418
4 changed files with 157 additions and 6 deletions

View file

@ -21,6 +21,10 @@ sort_ym/
genres.py — the real Yandex genre tree; classify_track — resolves track genre and artist genres.py — the real Yandex genre tree; classify_track — resolves track genre and artist
genres into a single sub-genre (root_id); ROOT_BUCKET — sub-genre -> 11 top-level buckets genres into a single sub-genre (root_id); ROOT_BUCKET — sub-genre -> 11 top-level buckets
language.py — hybrid language detection (lyrics API / genre hint / alphabet) language.py — hybrid language detection (lyrics API / genre hint / alphabet)
lyrics.py — text lyrics of RU-tracks: track_supplement().lyrics.full_lyrics ->
cache/lyrics_text.json (incrementally, atomic-write)
analyze.py — lyric analysis via local Ollama (/api/chat, think + JSON Schema) ->
cache/lyrics_analysis.json (written after each track)
classify.py — (sub-genre | top-level bucket, language) -> playlist name; with_label — source classify.py — (sub-genre | top-level bucket, language) -> playlist name; with_label — source
label suffix for apply --source --label label suffix for apply --source --label
report.py — two-pass report row building, writes out/report.csv, summaries report.py — two-pass report row building, writes out/report.csv, summaries
@ -34,12 +38,15 @@ sort_ym/
tests/ tests/
test_genres.py — classify_track, dominant, bucket_for, genre tree test_genres.py — classify_track, dominant, bucket_for, genre tree
test_language.py — language detection test_language.py — language detection
test_lyrics.py — lyrics text download, cache structure, resumability
test_analyze.py — local Ollama integration, JSON Schema parsing, cache atomicity
test_report.py — two-pass collapsing of small sub-genre groups test_report.py — two-pass collapsing of small sub-genre groups
test_fetch.py, test_apply.py — track/artist cache, apply idempotency test_fetch.py, test_apply.py — track/artist cache, apply idempotency
test_digest.py — artist/album aggregation, long-tail collapsing, digest size bounds test_digest.py — artist/album aggregation, long-tail collapsing, digest size bounds
test_source.py — playlist URL parsing, matching client.tracks() response by id, handling test_source.py — playlist URL parsing, matching client.tracks() response by id, handling
private/inaccessible playlists private/inaccessible playlists
test_cli.py — --source/--label flag validation before any network access test_cli.py, test_config.py — --source/--label flag validation before any network access;
config.toml reading including [ollama] block
cache/ — API cache (in .gitignore) cache/ — API cache (in .gitignore)
out/ — reports (in .gitignore) out/ — reports (in .gitignore)
@ -59,6 +66,9 @@ report → genres.load_or_fetch_catalog() → cache/genre_catalog.json
apply --yes → users_playlists_create / users_playlists_insert_track apply --yes → users_playlists_create / users_playlists_insert_track
digest → genres.load_catalog() + language.load_lang_cache() (cache only, no network) digest → genres.load_catalog() + language.load_lang_cache() (cache only, no network)
→ build_rows() → out/digest.md → build_rows() → out/digest.md
lyrics → ru_lyric_track_ids() → client.track_supplement() → cache/lyrics_text.json
analyze → cache/lyrics_text.json → Ollama /api/chat → cache/lyrics_analysis.json
digest → + cache/lyrics_analysis.json → out/digest.md (aggregate block) + out/lyrics_digest.md
report/apply/digest --source <url> → source.parse_playlist_url() → (user_id, kind) report/apply/digest --source <url> → source.parse_playlist_url() → (user_id, kind)
→ client.users_playlists(kind, user_id=...) → TrackShort[] → client.tracks(batch) → client.users_playlists(kind, user_id=...) → TrackShort[] → client.tracks(batch)
@ -107,6 +117,36 @@ report/apply/digest --source <url> → source.parse_playlist_url() → (user_id,
3. An alphabet heuristic on the title/artist (Cyrillic vs Latin, `unicodedata`). 3. An alphabet heuristic on the title/artist (Cyrillic vs Latin, `unicodedata`).
4. Otherwise — `UNKNOWN` → a separate "Undetermined" playlist. 4. Otherwise — `UNKNOWN` → a separate "Undetermined" playlist.
- **Lyric analysis (`lyrics.py`, `analyze.py`)**: only Russian-language tracks are analyzed. The
reason is signal validity, not cost — on a foreign-language track the meaning arrives through
the music and the vocal delivery, not through parsed words, so an LLM reading the literal text
would add a signal the listener never perceives. Text fetching and analysis are **two separate
commands** on purpose: the fetch is network-bound against the deprecated `track_supplement`,
the analysis is a multi-hour local GPU job, and a prompt or model change must be re-runnable
without touching Yandex again. `analyze` and `digest` construct no `Client` at all.
The Ollama call (`/api/chat`) combines `think: true` with a full JSON Schema in `format`:
reasoning goes to a separate `message.thinking` field (never cached) while only the final
answer is grammar-constrained, so the model reasons freely and the result still parses.
`num_predict: -1` — a token cap would truncate mid-JSON and turn a slow answer into an invalid
one; the bound is the request timeout instead. `temperature: 0.65`, not 0: at 0 the narrative
fields flatten out.
`themes` are free-form but pattern-constrained to English snake_case
(`^[a-z][a-z0-9_]{2,29}$`) — in the pilot, an unconstrained field drifted between Russian and
English inside a single run, making the values impossible to aggregate; the pattern makes
Cyrillic mechanically unrepresentable at decode time. They are English for the same reason
genre slugs and `ROOT_BUCKET` keys are: they are aggregation keys. `mood` is a closed enum that
deliberately excludes irony/sarcasm — that axis is `stance`, and mixing the two produced
compound values like `"aggressive/triumphant"`.
Resumability: the cache is written atomically after **every single track** (not batched by 20
as in `language.py`) — inference costs tens of seconds, so a crash must cost at most one track.
An entry is reused only when `(model, prompt_version, lyrics_hash)` all match, so a corrected
lyrics text or a bumped prompt invalidates exactly what it should. A timeout or a broken
response is stored as an `error` marker and the batch continues; error entries are never
considered fresh, so the next run retries precisely those tracks.
- **Track identifiers**: a track has a composite `track_id` (`"id:album_id"`, used as the cache key - **Track identifiers**: a track has a composite `track_id` (`"id:album_id"`, used as the cache key
and to check against tracks already in a playlist) and separate `id`/`album_id` (needed as and to check against tracks already in a playlist) and separate `id`/`album_id` (needed as
separate parameters for `track_supplement`, `tracks_lyrics`, and `users_playlists_insert_track`). separate parameters for `track_supplement`, `tracks_lyrics`, and `users_playlists_insert_track`).
@ -178,6 +218,12 @@ report/apply/digest --source <url> → source.parse_playlist_url() → (user_id,
`report --extra`-only purpose as `added_at` above. `report --extra`-only purpose as `added_at` above.
- `cache/genre_catalog.json` — a flat snapshot of Yandex's genre tree (id -> title, root_id). - `cache/genre_catalog.json` — a flat snapshot of Yandex's genre tree (id -> title, root_id).
- `cache/lyrics_lang.json` — lyrics language per track (id -> language code | null). - `cache/lyrics_lang.json` — lyrics language per track (id -> language code | null).
- `cache/lyrics_text.json` — lyrics text per track (numeric id -> text | null, RU tracks only).
- `cache/lyrics_analysis.json` — per-track analysis record: `{track_id: {model, prompt_version,
lyrics_hash, mood, themes, emotional_arc, pov, tone, description, error?}}`.
- `out/lyrics_digest.md` — full per-track reading grouped by primary mood.
- `config.toml` — includes `[ollama]` block with `host`, `model`, `prompt_version`, `timeout`,
`keep_alive`.
- `out/report.csv` — a preview of the playlist breakdown before `apply` (including the - `out/report.csv` — a preview of the playlist breakdown before `apply` (including the
`fine_bucket` column — the sub-genre before any collapsing into a top-level bucket). `fine_bucket` column — the sub-genre before any collapsing into a top-level bucket).
`report --order playlist` keeps source track order instead of sorting by target playlist; `report --order playlist` keeps source track order instead of sorting by target playlist;

View file

@ -21,6 +21,10 @@ sort_ym/
genres.py — реальное дерево жанров Яндекса; classify_track — сведение жанра трека и жанров genres.py — реальное дерево жанров Яндекса; classify_track — сведение жанра трека и жанров
артиста в один под-жанр (root_id); ROOT_BUCKET — под-жанр -> 11 крупных корзин артиста в один под-жанр (root_id); ROOT_BUCKET — под-жанр -> 11 крупных корзин
language.py — гибридная детекция языка (API текста / жанр-подсказка / алфавит) language.py — гибридная детекция языка (API текста / жанр-подсказка / алфавит)
lyrics.py — тексты песен RU-треков: track_supplement().lyrics.full_lyrics ->
cache/lyrics_text.json (инкрементально, atomic-write)
analyze.py — разбор текстов локальной Ollama (/api/chat, think + JSON Schema) ->
cache/lyrics_analysis.json (запись после каждого трека)
classify.py — (под-жанр | крупная корзина, язык) -> имя плейлиста; with_label — суффикс метки classify.py — (под-жанр | крупная корзина, язык) -> имя плейлиста; with_label — суффикс метки
источника для apply --source --label источника для apply --source --label
report.py — двухпроходная сборка строк отчёта, запись out/report.csv, сводки report.py — двухпроходная сборка строк отчёта, запись out/report.csv, сводки
@ -34,13 +38,16 @@ sort_ym/
tests/ tests/
test_genres.py — classify_track, dominant, bucket_for, дерево жанров test_genres.py — classify_track, dominant, bucket_for, дерево жанров
test_language.py — детекция языка test_language.py — детекция языка
test_lyrics.py — загрузка текстов песен, структура кэша, резюмируемость
test_analyze.py — интеграция локальной Ollama, разбор JSON Schema, атомарность кэша
test_report.py — двухпроходное схлопывание мелких под-жанровых групп test_report.py — двухпроходное схлопывание мелких под-жанровых групп
test_fetch.py, test_apply.py — кэш треков/артистов, идемпотентность применения test_fetch.py, test_apply.py — кэш треков/артистов, идемпотентность применения
test_digest.py — агрегация по исполнителям и альбомам, схлопывание длинного хвоста, test_digest.py — агрегация по исполнителям и альбомам, схлопывание длинного хвоста,
границы размера дайджеста границы размера дайджеста
test_source.py — разбор ссылки на плейлист, сопоставление ответа client.tracks() по id, test_source.py — разбор ссылки на плейлист, сопоставление ответа client.tracks() по id,
обработка приватных/недоступных плейлистов обработка приватных/недоступных плейлистов
test_cli.py — валидация флагов --source/--label до обращения к сети test_cli.py, test_config.py — валидация флагов --source/--label до обращения к сети;
чтение config.toml включая блок [ollama]
cache/ — кэш API (в .gitignore) cache/ — кэш API (в .gitignore)
out/ — отчёты (в .gitignore) out/ — отчёты (в .gitignore)
@ -60,6 +67,9 @@ report → genres.load_or_fetch_catalog() → cache/genre_catalog.json
apply --yes → users_playlists_create / users_playlists_insert_track apply --yes → users_playlists_create / users_playlists_insert_track
digest → genres.load_catalog() + language.load_lang_cache() (только кэш, сеть не используется) digest → genres.load_catalog() + language.load_lang_cache() (только кэш, сеть не используется)
→ build_rows() → out/digest.md → build_rows() → out/digest.md
lyrics → ru_lyric_track_ids() → client.track_supplement() → cache/lyrics_text.json
analyze → cache/lyrics_text.json → Ollama /api/chat → cache/lyrics_analysis.json
digest → + cache/lyrics_analysis.json → out/digest.md (агрегат) + out/lyrics_digest.md
report/apply/digest --source <url> → source.parse_playlist_url() → (user_id, kind) report/apply/digest --source <url> → source.parse_playlist_url() → (user_id, kind)
→ client.users_playlists(kind, user_id=...) → TrackShort[] → client.tracks(batch) → client.users_playlists(kind, user_id=...) → TrackShort[] → client.tracks(batch)
@ -108,6 +118,35 @@ report/apply/digest --source <url> → source.parse_playlist_url() → (user_id,
3. Эвристика по алфавиту названия/исполнителя (кириллица vs латиница, `unicodedata`). 3. Эвристика по алфавиту названия/исполнителя (кириллица vs латиница, `unicodedata`).
4. Иначе — `UNKNOWN` → отдельный плейлист «Не определено». 4. Иначе — `UNKNOWN` → отдельный плейлист «Не определено».
- **Разбор текстов (`lyrics.py`, `analyze.py`)**: разбираются только русскоязычные треки.
Причина — валидность сигнала, не стоимость: на иноязычном треке смысл передаётся музыкой и
подачей вокала, а не разобранными словами, поэтому LLM, читающая буквальный текст, добавит
сигнал, который слушатель при прослушивании не считывает. Загрузка текста и анализ — это
**две отдельные команды** намеренно: загрузка привязана к сети и устаревшему `track_supplement`,
анализ — это многочасовая работа локального GPU, а смена prompt или модели должна быть
переиспользуема без нового обращения к Яндексу. `analyze` и `digest` вообще не создают `Client`.
Вызов Ollama (`/api/chat`) комбинирует `think: true` с полной JSON Schema в `format`:
рассуждение идёт в отдельное поле `message.thinking` (никогда не кэшируется), а финальный ответ
ограничивается грамматикой, так что модель рассуждает свободно, а результат всё равно парсится.
`num_predict: -1` — ограничение по токенам обрежет JSON и превратит медленный ответ в невалидный;
ограничение вместо этого — таймаут запроса. `temperature: 0.65`, не 0: при 0 нарративные поля
становятся одноообразными.
`themes` свободного формата, но ограничены паттерном англ. snake_case (`^[a-z][a-z0-9_]{2,29}$`) —
в пилоте неограниченное поле дрейфовало между русским и английским в одном прогоне, что сделало
значения невозможными для агрегации; паттерн делает кириллицу механически невозможной при разборе.
Они англ. по той же причине, что и слаги жанров, и `ROOT_BUCKET`: это ключи агрегации. `mood` —
закрытый enum, намеренно исключающий иронию/сарказм — это ось `stance`, и смешивание двух осей
дало составные значения типа `"aggressive/triumphant"`.
Резюмируемость: кэш пишется атомарно после **каждого трека** (не батчами по 20, как в
`language.py`) — инференс занимает десятки секунд, так что крах должен стоить максимум один трек.
Запись переиспользуется, только если совпадают `(model, prompt_version, lyrics_hash)`, поэтому
исправленный текст или bumped prompt инвалидирует ровно то, что надо. Таймаут или повреждённый
ответ сохраняются как `error` marker, прогон продолжается; error-записи никогда не считаются
свежими, так что следующий прогон повторит точно эти треки.
- **Идентификаторы треков**: у трека есть composite `track_id` (`"id:album_id"`, используется как - **Идентификаторы треков**: у трека есть composite `track_id` (`"id:album_id"`, используется как
ключ кэша и для сверки с уже добавленными треками в плейлисте) и отдельные `id`/`album_id` ключ кэша и для сверки с уже добавленными треками в плейлисте) и отдельные `id`/`album_id`
(нужны как раздельные параметры для `track_supplement`, `tracks_lyrics` и (нужны как раздельные параметры для `track_supplement`, `tracks_lyrics` и
@ -177,6 +216,12 @@ report/apply/digest --source <url> → source.parse_playlist_url() → (user_id,
и `added_at` выше, только для `report --extra`. и `added_at` выше, только для `report --extra`.
- `cache/genre_catalog.json` — плоский снимок дерева жанров Яндекса (id -> title, root_id). - `cache/genre_catalog.json` — плоский снимок дерева жанров Яндекса (id -> title, root_id).
- `cache/lyrics_lang.json` — язык текста песни по треку (id -> language code | null). - `cache/lyrics_lang.json` — язык текста песни по треку (id -> language code | null).
- `cache/lyrics_text.json` — текст песни по треку (numeric id -> текст | null, только RU-треки).
- `cache/lyrics_analysis.json` — запись анализа на трек: `{track_id: {model, prompt_version,
lyrics_hash, mood, themes, emotional_arc, pov, tone, description, error?}}`.
- `out/lyrics_digest.md` — полный построчный разбор, сгруппированный по основному настроению.
- `config.toml` — включает блок `[ollama]` с полями `host`, `model`, `prompt_version`, `timeout`,
`keep_alive`.
- `out/report.csv` — предпросмотр распределения по плейлистам перед `apply` (включая колонку - `out/report.csv` — предпросмотр распределения по плейлистам перед `apply` (включая колонку
`fine_bucket` — под-жанр до возможного схлопывания в крупную корзину). `report --order playlist` `fine_bucket` — под-жанр до возможного схлопывания в крупную корзину). `report --order playlist`
сохраняет исходный порядок треков вместо сортировки по целевому плейлисту; отдельные флаги сохраняет исходный порядок треков вместо сортировки по целевому плейлисту; отдельные флаги

View file

@ -42,6 +42,8 @@ For development (tests):
.venv\Scripts\python -m sort_ym apply --limit 20 --yes # dry run: create playlists and add the first 20 tracks .venv\Scripts\python -m sort_ym apply --limit 20 --yes # dry run: create playlists and add the first 20 tracks
.venv\Scripts\python -m sort_ym apply --yes # create playlists and add all tracks (changes the account) .venv\Scripts\python -m sort_ym apply --yes # create playlists and add all tracks (changes the account)
.venv\Scripts\python -m sort_ym digest # library summary (top artists, genres) to out/digest.md - to paste into an LLM chat; no network used .venv\Scripts\python -m sort_ym digest # library summary (top artists, genres) to out/digest.md - to paste into an LLM chat; no network used
.venv\Scripts\python -m sort_ym lyrics # download the lyrics text of Russian-language tracks (cache/lyrics_text.json); does not change the account
.venv\Scripts\python -m sort_ym analyze # analyze those lyrics with a local Ollama model (cache/lyrics_analysis.json); no internet, runs for hours, resumable
``` ```
On repeated runs `fetch` only downloads new tracks, and `apply` does not duplicate tracks already added. On repeated runs `fetch` only downloads new tracks, and `apply` does not duplicate tracks already added.
@ -80,11 +82,13 @@ To start over from scratch (re-fetch everything), delete the `cache/` folder:
Remove-Item -Recurse -Force cache Remove-Item -Recurse -Force cache
``` ```
The cache consists of four files: The cache consists of six files:
- `cache/tracks.json` — liked tracks and their metadata (including artist ids); - `cache/tracks.json` — liked tracks and their metadata (including artist ids);
- `cache/artist_genres.json` — genre tags for each artist (an artist can have several); - `cache/artist_genres.json` — genre tags for each artist (an artist can have several);
- `cache/genre_catalog.json` — a snapshot of Yandex's genre tree as of the last run; - `cache/genre_catalog.json` — a snapshot of Yandex's genre tree as of the last run;
- `cache/lyrics_lang.json` — detected lyrics language per track. - `cache/lyrics_lang.json` — detected lyrics language per track;
- `cache/lyrics_text.json` — lyrics text of Russian-language tracks;
- `cache/lyrics_analysis.json` — per-track lyric analysis from the local model.
Deleting `cache/` makes `fetch` and `report` re-fetch everything from scratch on the next run. Deleting `cache/` makes `fetch` and `report` re-fetch everything from scratch on the next run.
@ -159,6 +163,32 @@ Digest saved: out/digest.md
Example `out/digest.md` (abridged): Example `out/digest.md` (abridged):
## Lyric analysis (Russian tracks)
`lyrics` downloads the lyrics text, `analyze` reads it with a local model and writes a per-track
profile: mood, themes, emotional arc, point of view, tone, plus a short description of what the
song is about and why it might resonate.
```
.venv\Scripts\python -m sort_ym lyrics # lyrics text of Russian-language tracks
.venv\Scripts\python -m sort_ym analyze # analysis via local Ollama, hours of local compute
.venv\Scripts\python -m sort_ym digest # out/lyrics_digest.md + an aggregate block in out/digest.md
```
Only Russian-language tracks are analyzed. On a foreign-language track the meaning is carried by
the music and the vocal delivery rather than by parsed words, so a literal reading of the text
would add a signal the listener never actually perceives.
Requirements: Ollama running locally with the model named in `config.toml` under `[ollama]`
(`ollama list` to check). No internet is used — `analyze` never talks to Yandex.
Expect roughly 40 seconds per track — about three hours for a ~266-track library. The run is
interruptible: results are written after every single track, so Ctrl+C costs at most one track,
and re-running continues where it stopped. Tracks that timed out are retried on the next run.
Raising `prompt_version` in `config.toml` invalidates the whole cache and re-analyzes everything.
Example `out/digest.md` (abridged):
``` ```
## Genres (top-level buckets) ## Genres (top-level buckets)
- Indie & Alternative — 680 (34%): INT 571 / RU 108 - Indie & Alternative — 680 (34%): INT 571 / RU 108

View file

@ -42,6 +42,8 @@ python -m venv .venv
.venv\Scripts\python -m sort_ym apply --limit 20 --yes # пробный запуск: создать плейлисты и добавить в них первые 20 треков .venv\Scripts\python -m sort_ym apply --limit 20 --yes # пробный запуск: создать плейлисты и добавить в них первые 20 треков
.venv\Scripts\python -m sort_ym apply --yes # создать плейлисты и добавить в них все треки (меняет аккаунт) .venv\Scripts\python -m sort_ym apply --yes # создать плейлисты и добавить в них все треки (меняет аккаунт)
.venv\Scripts\python -m sort_ym digest # сводка библиотеки (топ-исполнители, жанры) в out/digest.md - чтобы вставить в чат с LLM; сеть не используется .venv\Scripts\python -m sort_ym digest # сводка библиотеки (топ-исполнители, жанры) в out/digest.md - чтобы вставить в чат с LLM; сеть не используется
.venv\Scripts\python -m sort_ym lyrics # загрузить тексты песен русскоязычных треков (cache/lyrics_text.json), аккаунт не меняется
.venv\Scripts\python -m sort_ym analyze # разобрать эти тексты локальной моделью Ollama (cache/lyrics_analysis.json), без интернета, идёт часами, продолжается после обрыва
``` ```
При повторных запусках `fetch` докачивает только новые треки, а `apply` не дублирует уже добавленные треки. При повторных запусках `fetch` докачивает только новые треки, а `apply` не дублирует уже добавленные треки.
@ -80,11 +82,13 @@ python -m venv .venv
Remove-Item -Recurse -Force cache Remove-Item -Recurse -Force cache
``` ```
Кэш состоит из четырёх файлов: Кэш состоит из шести файлов:
- `cache/tracks.json` — список лайкнутых треков и их метаданные (включая id артистов); - `cache/tracks.json` — список лайкнутых треков и их метаданные (включая id артистов);
- `cache/artist_genres.json` — жанровые теги каждого артиста (может быть несколько на артиста); - `cache/artist_genres.json` — жанровые теги каждого артиста (может быть несколько на артиста);
- `cache/genre_catalog.json` — снимок дерева жанров Яндекса на момент последнего запуска; - `cache/genre_catalog.json` — снимок дерева жанров Яндекса на момент последнего запуска;
- `cache/lyrics_lang.json` — определённые языки текстов для каждого трека. - `cache/lyrics_lang.json` — определённые языки текстов для каждого трека;
- `cache/lyrics_text.json` — тексты песен русскоязычных треков;
- `cache/lyrics_analysis.json` — разбор текста каждого трека локальной моделью.
Удаление `cache/` заставит `fetch` и `report` переполучить всё с нуля при следующем запуске. Удаление `cache/` заставит `fetch` и `report` переполучить всё с нуля при следующем запуске.
@ -160,6 +164,32 @@ $ python -m sort_ym digest
Пример `out/digest.md` (сокращённо): Пример `out/digest.md` (сокращённо):
## Разбор текстов (русскоязычные треки)
`lyrics` загружает текст песни, `analyze` читает его локальной моделью и пишет профиль каждого
трека: настроение, темы, эмоциональную арку, ракурс, тон, плюс короткое описание того, о чём
песня и чем она может цеплять.
```
.venv\Scripts\python -m sort_ym lyrics # тексты песен русскоязычных треков
.venv\Scripts\python -m sort_ym analyze # разбор локальной Ollama, часы локальных вычислений
.venv\Scripts\python -m sort_ym digest # out/lyrics_digest.md + агрегат в out/digest.md
```
Разбираются только русскоязычные треки. На иноязычном треке смысл передаётся музыкой и подачей
вокала, а не разобранными словами, поэтому буквальное чтение текста добавило бы сигнал, которого
слушатель при прослушивании не считывает.
Что нужно: запущенная локально Ollama с моделью из `config.toml`, блок `[ollama]` (проверить —
`ollama list`). Интернет не используется: `analyze` не обращается к Яндексу вообще.
Скорость — примерно 40 секунд на трек, около трёх часов на ~266 треков. Прогон прерываемый:
результат пишется после каждого трека, Ctrl+C стоит максимум один трек, повторный запуск
продолжает с места обрыва. Треки, упавшие по таймауту, повторяются при следующем запуске.
Увеличение `prompt_version` в `config.toml` обесценивает весь кэш и запускает разбор заново.
Пример `out/digest.md` (сокращённо):
``` ```
## Жанры (крупные корзины) ## Жанры (крупные корзины)
- Инди и альтернатива — 680 (34%): INT 571 / RU 108 - Инди и альтернатива — 680 (34%): INT 571 / RU 108