diff --git a/README.md b/README.md index 23fef57..edd6546 100644 --- a/README.md +++ b/README.md @@ -1,25 +1,35 @@ # Streamify -Local-first self-analytics for your Yandex Music metadata. +Локальная self-analytics платформа для вашей Яндекс Музыки. -Streamify turns a Yandex Music library into a reproducible local lakehouse: raw JSONL metadata, DuckDB/dbt marts, Streamlit dashboard, static summary, JSON snapshot, CSV action queues and GitHub Pages documentation. It stores metadata and derived analytics only. It does not download or store audio. +Streamify превращает музыкальную библиотеку в воспроизводимый локальный lakehouse: сырые JSONL-метаданные, DuckDB/dbt-марты, Streamlit dashboard, Markdown-отчет, JSON snapshot, CSV-очереди действий и GitHub Pages документацию. Проект хранит только метаданные и производные аналитические признаки. Аудио не скачивается, не сохраняется и не воспроизводится. -## Product Value +## Зачем Это Нужно -Streamify answers practical questions about a personal music library: +Streamify отвечает на прикладные вопросы о личной музыкальной библиотеке: -- which artists, tracks and genres dominate the library; -- how taste changes over months and release eras; -- which liked tracks are under-playlisted and worth rediscovering; -- which playlists overlap, stand out or need cleanup; -- how complete and fresh the local data is; -- what future location enrichment would need before map views can be trusted. +- какие артисты, треки и жанры реально доминируют; +- как меняется вкус по месяцам, эпохам релизов и жанровым периодам; +- какие любимые треки недопредставлены в плейлистах и стоят повторного открытия; +- какие плейлисты пересекаются, выделяются или требуют чистки; +- насколько полные, свежие и надежные локальные данные; +- какие данные нужны для будущих карт прослушивания без опасных догадок о геолокации. -The current dashboard is chart-first: `Story`, `Taste Map`, `Atlas`, `Mix Shift`, `Rediscovery`, `Playlists`, `Explorer`, `Actions` and `Data Quality`. +Дашборд построен как продуктовый аналитический интерфейс, а не набор таблиц: `Story`, `Taste Map`, `Atlas`, `Mix Shift`, `Rediscovery`, `Playlists`, `Explorer`, `Actions`, `Data Quality`. -## First Local Run +## Демонстрация Дашборда -Run a deterministic local sample with no credentials: +Скриншоты ниже собираются из локального sample-прогона и не содержат приватных данных аккаунта. + +![Обзор Streamify dashboard](docs/assets/dashboard-story.png) + +![Atlas и визуальные инсайты](docs/assets/dashboard-atlas.png) + +![Actions и очереди рекомендаций](docs/assets/dashboard-actions.png) + +## Быстрый Локальный Запуск + +Запуск на sample-данных без токена: ```bash cp .env.example .env @@ -28,43 +38,45 @@ make acceptance-local make dashboard ``` -Then open the Streamlit URL printed by `make dashboard`. +После этого откройте URL, который напечатает `make dashboard`. -Docker Compose uses the `local` profile, and `.env.example` pins `DBT_THREADS=1` for predictable laptop builds. Make targets load `.env` through `scripts/run_with_dotenv.py`, so secrets are passed through the process environment instead of Make parsing. +Docker Compose использует профиль `local`, а `.env.example` задает `DBT_THREADS=1`, чтобы сборка была предсказуемой на ноутбуке. Make-команды загружают `.env` через `scripts/run_with_dotenv.py`, поэтому секреты передаются через environment, а не парсятся Makefile. -Run against your Yandex Music account: +Запуск на вашей Яндекс Музыке: ```bash cp .env.example .env make token-help -# Put YANDEX_MUSIC_TOKEN into .env. +# Добавьте YANDEX_MUSIC_TOKEN в .env. make acceptance-real make dashboard ``` -## Main Commands +`make acceptance-real` падает, если последний manifest не доказывает `source=yandex_music`. + +## Основные Команды ```bash -make help # command map -make status # safe local readiness/status hints -make token-help # token setup guide, without printing secrets -make ingest # real account metadata ingestion -make ingest-sample # deterministic sample metadata -make raw-contract # raw JSONL/manifest validation -make dbt-build # local DuckDB/dbt marts -make report # markdown summary, JSON snapshot, CSV queues -make snapshot # JSON snapshot only -make recommendations # CSV action queues only -make readiness-real # require latest manifest source=yandex_music +make help # карта команд +make status # безопасная диагностика локального состояния +make token-help # подсказка по токену без печати секретов +make ingest # ingestion метаданных реального аккаунта +make ingest-sample # детерминированные sample-данные +make raw-contract # проверка raw JSONL и manifest +make dbt-build # локальные DuckDB/dbt-марты +make report # Markdown summary, JSON snapshot, CSV queues +make snapshot # только JSON snapshot +make recommendations # только CSV action queues +make readiness-real # требовать source=yandex_music make dashboard-smoke # Streamlit content + HTTP smoke -make pages-site # static GitHub Pages site in public/ -make test # full local quality gate +make pages-site # статический GitHub Pages сайт в public/ +make test # полный локальный quality gate make up-local # Docker Compose local product profile -make compose-smoke-real # Docker Compose smoke against a configured token -make clean-local # remove generated local artifacts +make compose-smoke-real # Docker Compose smoke с настроенным токеном +make clean-local # удалить локально сгенерированные артефакты ``` -## Local Artifacts +## Локальные Артефакты - Raw metadata: `data/raw/yamusic/*.jsonl` - DuckDB warehouse: `data/streamify.duckdb` @@ -73,21 +85,21 @@ make clean-local # remove generated local artifacts - CSV action queues: `data/recommendations/*.csv` - Optional enrichment inputs: `data/enrichment/*.csv` -All generated local artifacts are ignored by git. `.env` is ignored and must not be committed. +Все локально сгенерированные артефакты игнорируются git. `.env` тоже игнорируется и не должен попадать в коммиты. -`make clean-local` removes generated raw data, reports, DuckDB files and dbt `target`/`logs`/`dbt_packages` artifacts without touching `.env`. +`make clean-local` удаляет raw data, отчеты, DuckDB-файлы и dbt `target`/`logs`/`dbt_packages`, но не трогает `.env`. -## Data Architecture +## Архитектура Данных ```text -Yandex Music metadata +Метаданные Яндекс Музыки -> yamusic_ingest raw JSONL -> dbt staging views -> DuckDB marts - -> Streamlit dashboard, reports, snapshots and recommendation queues + -> Streamlit dashboard, reports, snapshots, recommendation queues ``` -Core marts include: +Ключевые марты: - `yamusic_dim_tracks`, `yamusic_dim_artists`, `yamusic_dim_albums`, `yamusic_dim_playlists` - `yamusic_fact_library_events`, `yamusic_fact_playlist_tracks` @@ -95,50 +107,49 @@ Core marts include: - `yamusic_track_signals`, `yamusic_playlist_signals`, `yamusic_playlist_overlap` - `yamusic_library_profile` -See [docs/yamusic_lineage.md](docs/yamusic_lineage.md) for raw-to-dashboard lineage. +Lineage описан в [docs/yamusic_lineage.md](docs/yamusic_lineage.md). -## Dashboard +## Что Показывает Дашборд -The Streamlit dashboard focuses on evidence, not table dumps: +- `Story`: профиль библиотеки, timeline активности и жанровый отпечаток. +- `Taste Map`: гравитация артистов и разнообразие жанров. +- `Atlas`: genre atlas, monthly rhythm, music time travel, playlist subway, playlist DNA и Geo Atlas readiness. +- `Mix Shift`: жанровая heatmap, release-era mix и focus genre mix. +- `Rediscovery`: любимые треки, которые мало представлены в плейлистах. +- `Playlists`: здоровье плейлистов и overlap. +- `Explorer`: фильтруемые карточки треков и точечный поиск. +- `Actions`: следующие действия и downloadable queues. +- `Data Quality`: source, raw counts, checksums и ingestion diagnostics. -- `Story`: profile metrics, activity timeline and genre fingerprint. -- `Taste Map`: artist gravity and genre diversity. -- `Atlas`: genre atlas, monthly rhythm, music time travel, playlist subway, playlist DNA and Geo Atlas readiness. -- `Mix Shift`: genre heatmap, release-era mix and focus genre mix. -- `Rediscovery`: under-playlisted liked tracks and repeat quadrants. -- `Playlists`: playlist health and overlap. -- `Explorer`: filtered track cards and exact lookup. -- `Actions`: next steps and downloadable queues. -- `Data Quality`: source, raw counts, checksums and ingestion diagnostics. +## География И Карты -## Optional Location Enrichment +Метаданные Яндекс Музыки не содержат надежную геолокацию прослушивания. Streamify не делает вид, что регион аккаунта, язык плейлиста, жанр или происхождение артиста равны вашему местоположению. -Yandex Music metadata does not contain reliable listening location. Streamify therefore does not infer where listening happened from account region, playlist language, genre or artist origin. +Будущие карты требуют явных локальных enrichment-файлов в `data/enrichment`: -Future map views require explicit local enrichment files under `data/enrichment`: +- `artist_locations.csv`: места, связанные с артистами; +- `user_location_events.csv`: пользовательская timeline геолокации. -- `artist_locations.csv` for artist-associated places; -- `user_location_events.csv` for user-provided location timelines. - -See [docs/location_enrichment.md](docs/location_enrichment.md) for schemas, source ideas, privacy constraints and timestamp join caveats. +Схемы, источники и privacy-ограничения описаны в [docs/location_enrichment.md](docs/location_enrichment.md). ## GitHub Pages -`make pages-site` builds a polished static product site into `public/`. The Pages workflow builds it from sample metadata with `YANDEX_MUSIC_TOKEN` empty, so public documentation is reproducible and does not depend on a private account. +`make pages-site` собирает современный русскоязычный статический сайт в `public/`. Workflow Pages строит его на sample metadata с пустым `YANDEX_MUSIC_TOKEN`, поэтому публичная документация воспроизводима и не зависит от приватного аккаунта. -The public site includes: +Публичный сайт включает: -- product overview; -- local runbook; -- Atlas and location enrichment guidance; +- продуктовый overview; +- локальный runbook; +- демонстрации dashboard; +- Atlas и location enrichment; - lineage; - acceptance matrix; - release process; -- generated sample summary when available. +- generated sample summary, если он доступен. ## Quality Gates -`make test` runs the local product gate: +`make test` запускает локальный product gate: - repository contract validation; - secret/audio artifact guards; @@ -149,13 +160,11 @@ The public site includes: - Pages build; - Python compile checks; - pytest; -- Docker Compose config and local profile smoke. - -`make acceptance-real` is the real account gate and fails unless the latest manifest proves `source=yandex_music`. +- Docker Compose config и local profile smoke. -## Documentation +## Документация -- [Local runbook](docs/yandex_music_local.md) +- [Локальный runbook](docs/yandex_music_local.md) - [Lineage](docs/yamusic_lineage.md) - [Product acceptance](docs/product_acceptance.md) - [Location enrichment contract](docs/location_enrichment.md) diff --git a/docs/assets/dashboard-actions.png b/docs/assets/dashboard-actions.png new file mode 100644 index 0000000..a409081 Binary files /dev/null and b/docs/assets/dashboard-actions.png differ diff --git a/docs/assets/dashboard-atlas.png b/docs/assets/dashboard-atlas.png new file mode 100644 index 0000000..0607626 Binary files /dev/null and b/docs/assets/dashboard-atlas.png differ diff --git a/docs/assets/dashboard-story.png b/docs/assets/dashboard-story.png new file mode 100644 index 0000000..02695db Binary files /dev/null and b/docs/assets/dashboard-story.png differ diff --git a/scripts/build_pages_site.py b/scripts/build_pages_site.py index 0f43794..e3c7e70 100644 --- a/scripts/build_pages_site.py +++ b/scripts/build_pages_site.py @@ -2,6 +2,7 @@ from __future__ import annotations import re +import shutil from dataclasses import dataclass from datetime import datetime, timezone from html import escape @@ -14,22 +15,53 @@ @dataclass(frozen=True) class Page: title: str - source: Path + source: Path | None output: str summary: str + section: str + + +DASHBOARD_DEMO = """# Демонстрация Дашборда + +Streamify показывает не только таблицы, а продуктовые аналитические срезы: сюжет библиотеки, карту вкуса, atlas-визуализации, rediscovery-очереди, playlist health и data quality. + +![Обзор Streamify dashboard](docs/assets/dashboard-story.png) + +## Визуальные Экраны + +![Atlas и визуальные инсайты](docs/assets/dashboard-atlas.png) + +![Actions и очереди рекомендаций](docs/assets/dashboard-actions.png) + +## Что Проверять + +- `Story`: общая картина библиотеки, активность и жанровый отпечаток. +- `Atlas`: месячный ритм, карта жанров, playlist subway и готовность geo enrichment. +- `Actions`: готовые очереди для rediscovery, чистки плейлистов и экспортов. +- `Data Quality`: источник данных, checksums, raw counts и diagnostics. +""" PAGES = [ - Page("Product Overview", ROOT / "README.md", "index.html", "What Streamify is, why it exists, and how to run it."), - Page("Local Runbook", ROOT / "docs" / "yandex_music_local.md", "runbook.html", "Token-safe local setup and real-account acceptance."), - Page("Atlas + Location", ROOT / "docs" / "location_enrichment.md", "location.html", "Map-ready enrichment contract and privacy guardrails."), - Page("Lineage", ROOT / "docs" / "yamusic_lineage.md", "lineage.html", "Raw, staging, mart and dashboard data flow."), - Page("Acceptance", ROOT / "docs" / "product_acceptance.md", "acceptance.html", "Product requirements mapped to concrete checks."), - Page("Release Process", ROOT / "docs" / "release_process.md", "release.html", "Privacy-safe release and Pages workflow."), - Page("Sample Summary", ROOT / "data" / "streamify_summary.md", "sample-summary.html", "Generated example insights from the sample library."), + Page("Главная", ROOT / "README.md", "index.html", "Что это за продукт, как запустить и какую пользу он дает.", "overview"), + Page("Запуск", ROOT / "docs" / "yandex_music_local.md", "runbook.html", "Токен, локальный запуск, sample path и real-account acceptance.", "runbook"), + Page("Дашборд", None, "dashboard.html", "Скриншоты и ключевые экраны Streamlit-интерфейса.", "dashboard"), + Page("Atlas + Гео", ROOT / "docs" / "location_enrichment.md", "location.html", "Как готовить будущие карты без опасных догадок о местоположении.", "atlas"), + Page("Поток данных", ROOT / "docs" / "yamusic_lineage.md", "lineage.html", "Raw, staging, marts и dashboard data flow.", "lineage"), + Page("Проверка", ROOT / "docs" / "product_acceptance.md", "acceptance.html", "Требования MVP и команды, которые их доказывают.", "quality"), + Page("Релизы", ROOT / "docs" / "release_process.md", "release.html", "Privacy-safe release flow и GitHub Pages workflow.", "release"), + Page("Пример отчета", ROOT / "data" / "streamify_summary.md", "sample-summary.html", "Пример инсайтов, собранный на sample metadata.", "summary"), ] +def page_markdown(page: Page) -> str: + if page.source is None: + return DASHBOARD_DEMO + if page.source.exists(): + return page.source.read_text(encoding="utf-8") + return f"# {page.title}\n\nЗапустите `make report`, чтобы собрать эту страницу на sample metadata." + + def inline_markdown(text: str) -> str: text = escape(text) text = re.sub(r"`([^`]+)`", r"\1", text) @@ -61,6 +93,21 @@ def table_to_html(lines: list[str]) -> str: return "".join(html) +def image_to_html(line: str) -> str | None: + match = re.match(r"^!\[([^\]]*)\]\(([^)]+)\)$", line.strip()) + if not match: + return None + alt, src = match.groups() + if src.startswith("docs/assets/"): + src = src.replace("docs/assets/", "assets/", 1) + return ( + '
' + f'{escape(alt)}' + f'
{inline_markdown(alt)}
' + "
" + ) + + def markdown_to_html(markdown: str) -> str: body: list[str] = [] lines = markdown.splitlines() @@ -103,6 +150,14 @@ def flush_paragraph() -> None: i += 1 continue + image_html = image_to_html(line) + if image_html: + flush_paragraph() + in_list = close_list(body, in_list) + body.append(image_html) + i += 1 + continue + if line.startswith("|") and "|" in line[1:]: flush_paragraph() in_list = close_list(body, in_list) @@ -157,226 +212,363 @@ def nav_html(current_output: str) -> str: return "\n".join(links) +def side_links(current_output: str) -> str: + return "\n".join( + f'' + f'{escape(page.title)}{escape(page.summary)}' + for page in PAGES + ) + + +def hero_visual() -> str: + return """ +
+
+
+
+ Жанровый сдвиг +
+
+
+ Artist gravity + 3.4x +
+
+ Playlist overlap + 0.28 +
+
+ Monthly rhythm + + + + +
+
+
+ """ + + def page_html(page: Page, body: str) -> str: generated = datetime.now(timezone.utc).strftime("%Y-%m-%d %H:%M UTC") - cards = "\n".join( - f'{escape(item.title)}{escape(item.summary)}' - for item in PAGES - ) return f""" - + + {escape(page.title)} | Streamify
-
StreamifyLocal Yandex Music self-analytics
- + + S + StreamifyЛокальная аналитика Яндекс Музыки + +
-
{escape(page.title)}
-

Personal music analytics that stays on your machine.

-

{escape(page.summary)} Metadata-only ingestion, DuckDB/dbt marts, Streamlit insights, action queues, and reproducible public docs.

-
-
-
LocalNo account data in public artifacts.
-
TypedRaw contracts, dbt tests, lineage.
-
UsefulRediscovery, overlap, taste shifts.
-
Map-readyOpt-in location enrichment only.
+ +

Личная аналитика Яндекс Музыки на вашем ноутбуке.

+

{escape(page.summary)} Метаданные остаются локально: ingestion, DuckDB/dbt, dashboard, отчеты, action queues и воспроизводимая документация.

+
+ {hero_visual()}
-
- -
{body}
+
+ +
+
+
{body}
+
+
- + """ @@ -384,12 +576,15 @@ def page_html(page: Page, body: str) -> str: def main() -> int: PUBLIC_DIR.mkdir(parents=True, exist_ok=True) + assets_src = ROOT / "docs" / "assets" + assets_dest = PUBLIC_DIR / "assets" + if assets_src.exists(): + assets_dest.mkdir(parents=True, exist_ok=True) + for asset in assets_src.iterdir(): + if asset.is_file(): + shutil.copy2(asset, assets_dest / asset.name) for page in PAGES: - if page.source.exists(): - markdown = page.source.read_text(encoding="utf-8") - else: - markdown = f"# {page.title}\n\nRun `make report` to generate this page from sample metadata." - (PUBLIC_DIR / page.output).write_text(page_html(page, markdown_to_html(markdown)), encoding="utf-8") + (PUBLIC_DIR / page.output).write_text(page_html(page, markdown_to_html(page_markdown(page))), encoding="utf-8") return 0 diff --git a/scripts/validate_yamusic_local.py b/scripts/validate_yamusic_local.py index 2fb34d9..f8b2b25 100644 --- a/scripts/validate_yamusic_local.py +++ b/scripts/validate_yamusic_local.py @@ -130,7 +130,7 @@ def main() -> int: require_markers( "README.md", - ["Yandex Music", "DuckDB", "make help", "make status", "make ingest-sample", "make acceptance-real", "make dashboard", "taste changes", "`local` profile", "DBT_THREADS=1", "scripts/run_with_dotenv.py", "make clean-local", "dbt `target`/`logs`/`dbt_packages`", "make readiness-real", "make up-local", "make compose-smoke-real", "make snapshot", "make recommendations", "make pages-site", "GitHub Pages", "streamify_snapshot.json", "data/recommendations", "Optional Location Enrichment", "Atlas"], + ["Яндекс Музыки", "DuckDB", "make help", "make status", "make ingest-sample", "make acceptance-real", "make dashboard", "как меняется вкус", "`local`", "DBT_THREADS=1", "scripts/run_with_dotenv.py", "make clean-local", "dbt `target`/`logs`/`dbt_packages`", "make readiness-real", "make up-local", "make compose-smoke-real", "make snapshot", "make recommendations", "make pages-site", "GitHub Pages", "streamify_snapshot.json", "data/recommendations", "География И Карты", "Atlas", "docs/assets/dashboard-story.png", "docs/assets/dashboard-atlas.png", "docs/assets/dashboard-actions.png"], ) require_markers( "docs/yandex_music_local.md", @@ -164,7 +164,7 @@ def main() -> int: require_markers(".github/PULL_REQUEST_TEMPLATE.md", ["Product Value", "Data Engineering Impact", "make test", "make acceptance-real"]) require_markers("docs/project_management.md", ["Agent Lanes", "Repo/Build", "Yandex Ingestion", "QA/Integration", "v0.1.0-local-mvp"]) require_markers("docs/release_process.md", ["Release Checklist", "GitHub Pages", "sample metadata", "git tag vX.Y.Z"]) - require_markers("scripts/build_pages_site.py", ["PUBLIC_DIR", "Product Overview", "Atlas + Location", "streamify_summary.md", "location.html", "doc-card", "metric-strip", "inline_markdown", "index.html"]) + require_markers("scripts/build_pages_site.py", ["PUBLIC_DIR", "Главная", "Дашборд", "Atlas + Гео", "streamify_summary.md", "dashboard.html", "location.html", "hero-visual", "side-link", "media-frame", "inline_markdown", "docs/assets/", "assets/", "index.html"]) require_markers("scripts/yamusic_token_help.py", ["TOKEN_HELPER_URL", "supports_device_auth", "token_configured", "make preflight", "make acceptance-real"]) require_markers("scripts/check_no_local_sensitive_artifacts.py", ["FORBIDDEN_TRACKED_PATHS", "data/raw/yamusic", "DuckDB files", "audio artifacts are tracked"]) require_markers("scripts/check_no_audio_artifacts.py", ["AUDIO_EXTENSIONS", "must not store audio files"])