|
| 1 | +# AudioMuse-AI — Deep Import Chain Analysis & Refactoring Guide |
| 2 | + |
| 3 | +> Generated 2026-06-12 — full static analysis of all ~60 Python source files in the project. |
| 4 | +> Ranking is by **total transitive project modules pulled in** when the file is loaded |
| 5 | +> (i.e. "A imports B which imports C which imports D…"). |
| 6 | +
|
| 7 | +--- |
| 8 | + |
| 9 | +## 📊 Top 10 Most Import-Heavy Files |
| 10 | + |
| 11 | +| # | Code File | Direct Internal Imports | Transitive (~) | Impacted Functionality | Suggestion | |
| 12 | +|---|-----------|------------------------|---------------------|------------------------|------------| |
| 13 | +| **1** | `tasks/analysis.py` | **11** | **~22** | **Audio analysis orchestrator**: ONNX embedding inference, mood prediction, CLAP text-search index, lyrics embedding index, SemGrove merged index, artist GMM index, Voyager HNSW index, media server track/album fetching, CUDA memory cleanup. Touches every ML and data subsystem. | Split into `analysis_orchestrator.py` (RQ entry points + task status) and `analysis_pipeline.py` (worker logic). Move the 5 index-builder imports (`clap_text_search`, `lyrics_manager`, `sem_grove_manager`, `artist_gmm_manager`, `voyager_manager`) to **function-level lazy imports** inside each `build_and_store_*_task()` function, since they are only needed during index builds, not at module load. | |
| 14 | +| **2** | `app_setup.py` | **5** | **~20** | **Setup wizard**: media server config (all 5 types), auth fields (JWT, API token, user/password), lyrics API config (2 providers), advanced/enum field dropdowns, hidden fields. The line `from app import app, setup_manager` pulls in **all 18 modules** that `app.py` imports — the entire Flask app, all blueprints, auth, provider migration, error handling, Redis, Swagger. | Change `from app import app, setup_manager` → `from flask_app import app` + `from tasks.setup_manager import SetupManager`. The `flask_app` singleton already exists for exactly this purpose. Move `check_setup_needed` into a tiny `app_setup_guard.py` that has zero internal imports, so `app.py` can import it without cascading. | |
| 15 | +| **3** | `app.py` | **8** | **~18** | **Main Flask entry point**: all ~20 blueprint registrations, authentication init, provider migration, error handling, RQ/Redis connection, Swagger docs, JWT secret resolution, admin seed, proxy fix. | Defer blueprint registration to a `register_blueprints(app)` function called inside `create_app()`. Lazy-import `app_auth` and `app_provider_migration` inside their respective `init_app()` calls rather than at module top. Move the `from rq.job import Job, JobStatus` and `from redis import Redis` imports to `taskqueue.py` where they already belong. | |
| 16 | +| **4** | `tasks/clustering.py` | **5** | **~17** | **Playlist clustering pipeline**: KMeans/DBSCAN/GMM/Spectral algorithms, AI-powered playlist naming (via `tasks/ai/api` → 4 LLM providers), media server playlist CRUD (create/delete), GPU-accelerated clustering (RAPIDS cuML), post-processing (duplicate filtering, minimum size filtering, top-N diversity selection). | Already well-refactored into helper/postprocessing modules. Move the 40+ config constant imports into a `ClusteringConfig` dataclass passed as a single parameter. Lazy-import `mediaserver` functions inside the playlist-creation closures rather than at module level. The `from .clustering_helper import (...)` pulls `sklearn`, `commons`, `ai/api`, `ai/prompts`, and `clustering_gpu` — consider lazy-loading the GPU module since it's conditional. | |
| 17 | +| **5** | `tasks/song_alchemy.py` | **4** (+2 lazy) | **~14** | **Song Alchemy blending engine**: vector centroid math (weighted blend of song/artist vectors), PCA dimensionality reduction, LDA genre discriminant projection, UMAP 2D projection, artist GMM component weighting, Voyager nearest-neighbor fallback, per-artist cap enforcement. | Extract the 4 projection functions (`_project_with_umap`, `_project_to_2d`, `_project_aligned_add_sub`, `_project_with_discriminant`) into a standalone `tasks/alchemy_projections.py`. This lets `app_map.py` import projections without pulling the full alchemy → voyager → mediaserver chain. Convert the 2 lazy imports (`artist_gmm_manager`, `app_helper_artist`) to **dependency injection** — accept callables as parameters. | |
| 18 | +| **6** | `app_map.py` | **2** (+1 lazy) | **~13** | **2D/3D music map visualization**: UMAP projection cache, genre discriminant coloring, JSON/GZip compressed responses at 25/50/75/100% resolutions, mood centroid overlay, per-cluster statistics. | Move the `try: from tasks.song_alchemy import _project_with_umap, ...` block to import from `tasks/alchemy_projections.py` instead (after creating it per suggestion #5). This single change cuts the transitive chain from ~13 to ~4, because `song_alchemy` → `voyager_manager` → `mediaserver` → 5 providers is the deepest part of the cascade. | |
| 19 | +| **7** | `app_alchemy.py` | **2** | **~12** | **Song Alchemy web UI**: blend page HTML, artist autocomplete API (`/api/search_artists`), alchemy search endpoint. The cascade comes entirely from `from tasks.song_alchemy import song_alchemy`. | This blueprint is already minimal (2 imports). The fix is upstream: after refactoring #5 and #6, the transitive count drops from ~12 to ~3 automatically. No direct changes needed here. | |
| 20 | +| **8** | `tasks/mediaserver/__init__.py` | **5** | **~10** | **Media server dispatcher (HUB)**: unified API facade over 5 provider backends. Imports ~80 functions (16 per provider × 5) at module level, unconditionally loading Jellyfin, Navidrome, Lyrion, MPD, and Emby code — even though only ONE provider is active at runtime. Imported by 6+ other modules. | Implement **lazy provider loading**: replace the 5 `from .jellyfin import (...)` / `from .navidrome import (...)` / etc. blocks with a `_provider = None` + `_get_provider()` function that imports only the module matching `config.MEDIASERVER_TYPE` on first call. This cuts module-load cost by 80% and removes 4 unnecessary HTTP session initializations. The public API surface (function names) stays identical. | |
| 21 | +| **9** | `app_sonic_fingerprint.py` | **4** | **~10** | **Sonic Fingerprint feature**: generates a "taste profile" vector from the user's most-played tracks (weighted by recency), then finds similar songs via Voyager nearest-neighbor. Also resolves Emby/Jellyfin user credentials, fetches top stratified genres. | The double import of the mediaserver chain is the issue: `from tasks.sonic_fingerprint_manager import ...` already pulls `mediaserver` (+5 providers) internally, and then `from tasks.mediaserver import resolve_emby_jellyfin_user` pulls it again directly. Move the `resolve_emby_jellyfin_user` call into `sonic_fingerprint_manager` so the blueprint only imports one module. The `top_stratified_genre` import from `app_helper` is fine — it's lightweight. | |
| 22 | +| **10** | `app_voyager.py` | **3** | **~9** | **Voyager similarity search UI**: song/artist similarity lookups, mood-centroid-based search, radius-bounded nearest-neighbor walk, duplicate elimination, playlist creation from similarity results. Loads the `mood_centroids_real_080_clap.json` file (~1MB) at module level. | The `from tasks.voyager_manager import (...)` pulls `mediaserver` → 5 providers. Since `voyager_manager` only needs `mediaserver` for `create_instant_playlist` (one function), move that import inside the single function that uses it. Also, defer `_load_mood_centroids_for_similarity()` to first API call rather than module import — the 1MB JSON parse adds unnecessary startup latency for every worker process. | |
| 23 | + |
| 24 | +--- |
| 25 | + |
| 26 | +## 🔗 The Three Deepest Import Chains |
| 27 | + |
| 28 | +### Chain 1 — The Alchemy Cascade (Depth 5, ~14 modules) |
| 29 | + |
| 30 | +``` |
| 31 | +app_alchemy.py |
| 32 | + └─ tasks/song_alchemy.py |
| 33 | + ├─ tasks/voyager_manager.py |
| 34 | + │ └─ tasks/mediaserver/__init__.py |
| 35 | + │ ├─ tasks/mediaserver/jellyfin.py → .http, .helper, config |
| 36 | + │ ├─ tasks/mediaserver/navidrome.py → .http, .helper, config |
| 37 | + │ ├─ tasks/mediaserver/lyrion.py → .http, .helper, config |
| 38 | + │ ├─ tasks/mediaserver/mpd.py → .http, config |
| 39 | + │ └─ tasks/mediaserver/emby.py → .http, .helper, config |
| 40 | + ├─ app_helper.py → database.py, taskqueue.py, config, tz_helper |
| 41 | + └─ app_helper_artist.py → app_helper, memory_utils |
| 42 | +``` |
| 43 | + |
| 44 | +### Chain 2 — The Setup Cascade (Depth 4, ~20 modules) |
| 45 | + |
| 46 | +``` |
| 47 | +app_setup.py |
| 48 | + └─ from app import app, setup_manager ← PULLS ALL OF: |
| 49 | + app.py |
| 50 | + ├─ flask_app.py |
| 51 | + ├─ app_helper.py → database, taskqueue, config, tz_helper |
| 52 | + ├─ app_auth.py → flask, config, secrets |
| 53 | + ├─ app_provider_migration.py → app_helper, mediaserver/helper |
| 54 | + ├─ error/__init__.py → error_manager → error_dictionary |
| 55 | + ├─ config.py |
| 56 | + └─ tasks/setup_manager.py → config, psycopg2, argon2 |
| 57 | +``` |
| 58 | + |
| 59 | +### Chain 3 — The Clustering Cascade (Depth 4, ~17 modules) |
| 60 | + |
| 61 | +``` |
| 62 | +app_clustering.py |
| 63 | + └─ tasks/clustering.py |
| 64 | + ├─ tasks/clustering_helper.py |
| 65 | + │ ├─ sklearn (KMeans, DBSCAN, Spectral, GMM, PCA, metrics) |
| 66 | + │ ├─ tasks/commons.py → config |
| 67 | + │ ├─ tasks/ai/api.py → providers/{openai,ollama,gemini,mistral} |
| 68 | + │ ├─ tasks/ai/prompts.py → config |
| 69 | + │ └─ tasks/clustering_gpu.py → RAPIDS cuML (conditional) |
| 70 | + ├─ tasks/clustering_postprocessing.py → scipy, psycopg2 |
| 71 | + ├─ tasks/mediaserver/__init__.py → 5 providers |
| 72 | + └─ error/ → error_dictionary |
| 73 | +``` |
| 74 | + |
| 75 | +--- |
| 76 | + |
| 77 | +## 🏗️ Import Architecture Layers |
| 78 | + |
| 79 | +### Layer 0 — Foundation (Zero Internal Imports) |
| 80 | +`config.py`, `error/error_dictionary.py`, `app_logging.py`, `tz_helper.py`, `flask_app.py`, `tasks/mediaserver/http.py`, `tasks/mediaserver/helper.py`, `tasks/memory_utils.py`, `tasks/commons.py`, `tasks/playlist_ordering.py`, `tasks/radius_walk_helper.py`, `tasks/provider_migration_matcher.py` |
| 81 | + |
| 82 | +### Layer 1 — Web & Entry Points |
| 83 | +`app.py`, `rq_worker.py`, `rq_worker_high_priority.py`, `rq_janitor.py`, `restart_manager.py`, `restart_listener.py`, and all 20 `app_*.py` blueprint files |
| 84 | + |
| 85 | +### Layer 2 — Business Logic (THE HUB LAYER) |
| 86 | +`app_helper.py` (imported by 15+ files), `tasks/voyager_manager.py` (imported by 8+ files), `tasks/mediaserver/__init__.py` (imported by 6+ files), `tasks/analysis.py`, `tasks/clustering.py`, `error/error_manager.py` |
| 87 | + |
| 88 | +### Layer 3+ — Specialized Handlers |
| 89 | +`tasks/song_alchemy.py`, `tasks/clustering_helper.py`, `tasks/clustering_postprocessing.py`, `tasks/analysis_helper.py`, `tasks/path_manager.py`, `tasks/radio_manager.py`, `tasks/ai/tool_impl.py`, `tasks/ai/planner.py`, `tasks/ai/tools.py` |
| 90 | + |
| 91 | +--- |
| 92 | + |
| 93 | +## 🎯 Quick Wins (Low Effort, High Impact) |
| 94 | + |
| 95 | +| # | Change | Files Touched | Effort | Impact | |
| 96 | +|---|--------|--------------|--------|--------| |
| 97 | +| 1 | **Lazy provider loading in mediaserver dispatcher** — Replace 5 unconditional `from .jellyfin import (...)` blocks with a `_get_provider()` function that imports only the active provider at first call. | `tasks/mediaserver/__init__.py` | 🟢 Small | Cuts ~80 import lines, skips 4 unnecessary provider module loads, saves HTTP session init | |
| 98 | +| 2 | **Move `create_instant_playlist` import to function body** — In `tasks/voyager_manager.py`, move `from .mediaserver import create_instant_playlist` inside the single function that uses it. | `tasks/voyager_manager.py` | 🟢 Small | Breaks voyager→mediaserver→5 providers chain for all other callers of voyager_manager | |
| 99 | +| 3 | **Use `flask_app` singleton in setup** — Change `from app import app` → `from flask_app import app` in `app_setup.py`. | `app_setup.py` | 🟢 Small | Breaks the app_setup→app→everything cascade; `flask_app` was created exactly for this | |
| 100 | +| 4 | **Extract shared projection functions** — Create `tasks/alchemy_projections.py` containing the 4 UMAP/PCA/LDA functions. Both `song_alchemy.py` and `app_map.py` import from it. | New file + `tasks/song_alchemy.py` + `app_map.py` | 🟡 Medium | Breaks map→alchemy→voyager→mediaserver chain; eliminates the lazy try/except import pattern | |
| 101 | + |
| 102 | +--- |
| 103 | + |
| 104 | +## 📋 Summary of Root Causes |
| 105 | + |
| 106 | +| Root Cause | Files Affected | Severity | |
| 107 | +|------------|---------------|----------| |
| 108 | +| **Module-level import of all 5 media server providers** | `tasks/mediaserver/__init__.py` | 🔴 High — 5 HTTP session setups, 80+ function imports, only 1 provider active | |
| 109 | +| **Blueprint imports the heavy task module directly** | `app_alchemy.py`→`song_alchemy`, `app_map.py`→`song_alchemy`, `app_voyager.py`→`voyager_manager`, `app_sonic_fingerprint.py`→`sonic_fingerprint_manager` | 🟠 Medium — web layer shouldn't pull ML inference dependencies | |
| 110 | +| **`from app import app` used instead of `from flask_app import app`** | `app_setup.py` | 🔴 High — defeats the purpose of the `flask_app` singleton designed to break cycles | |
| 111 | +| **11 direct subsystem imports in one file** | `tasks/analysis.py` | 🔴 High — the orchestrator has module-level knowledge of every subsystem | |
| 112 | +| **`app_helper.py` handles 5 unrelated responsibilities** | `app_helper.py` (imported by 15+ files) | 🟠 Medium — DB, Redis, task status, song features, map projections, SSRF guard all in one file | |
| 113 | +| **40+ config constants imported at module top** | `tasks/clustering.py`, `app_clustering.py`, `app_cron.py` | 🟡 Low — noisy but config has zero internal deps so cost is minimal | |
| 114 | + |
| 115 | +--- |
| 116 | + |
| 117 | +## ✅ Strengths (What's Already Good) |
| 118 | + |
| 119 | +- **No circular imports** — Clean architecture with `flask_app.py` breaking the potential `app` ↔ `tasks` cycle |
| 120 | +- **Lazy imports used strategically** — `app_helper.py`, `tasks/ai/tool_impl.py`, and `tasks/song_alchemy.py` defer heavy imports to function level |
| 121 | +- **Config is read-only** — `config.py` never imports from business logic; safe to import anywhere |
| 122 | +- **Error handling centralized** — All errors flow through `error/error_manager.py` → `error/error_dictionary.py` |
| 123 | +- **Media server abstraction** — Single dispatcher (`tasks/mediaserver/__init__.py`) prevents vendor lock-in, though it needs lazy loading |
| 124 | +- **`flask_app` singleton** — Already exists to break import cycles; just needs to be used consistently |
| 125 | +- **Provider migration isolated** — `tasks/provider_probe.py` deliberately never reads `config.py` globals; takes credentials as parameters |
| 126 | +- **MCP tools use lazy imports** — `tasks/ai/tool_impl.py` imports heavy subsystems (voyager, artist_gmm, song_alchemy) at function level only |
0 commit comments