Spaces:
Running
Running
| from __future__ import annotations | |
| import logging | |
| import os | |
| from pathlib import Path | |
| from typing import TYPE_CHECKING, Any | |
| from dotenv import load_dotenv | |
| if TYPE_CHECKING: | |
| # Only loaded by type checkers; the runtime import is deferred to | |
| # ``_detect_device``/``get_default_dtype`` so dashboard-only deployments | |
| # (e.g. the HF Space) can import this module without the heavy ML stack. | |
| import torch | |
| # MPS has incomplete op coverage; this lets unsupported ops fall back to CPU | |
| # instead of raising NotImplementedError mid-inference. | |
| os.environ.setdefault("PYTORCH_ENABLE_MPS_FALLBACK", "1") | |
| PACKAGE_ROOT: Path = Path(__file__).resolve().parent | |
| PROJECT_ROOT: Path = PACKAGE_ROOT.parent | |
| load_dotenv(PROJECT_ROOT / ".env", override=False) | |
| PIPELINE_VERSION: str = "0.0.1" | |
| DATA_DIR: Path = Path(os.getenv("SYNC_PILOT_DATA_DIR", PROJECT_ROOT / "data")) | |
| AUDIO_DIR: Path = DATA_DIR / "audio" | |
| CACHE_DIR: Path = Path(os.getenv("SYNC_PILOT_CACHE_DIR", DATA_DIR / "cache")) | |
| OUTPUT_DIR: Path = Path(os.getenv("SYNC_PILOT_OUTPUT_DIR", DATA_DIR / "outputs")) | |
| EMBEDDINGS_DIR: Path = DATA_DIR / "embeddings" | |
| LOGS_DIR: Path = DATA_DIR / "logs" | |
| HF_CACHE_DIR: Path = Path(os.getenv("HF_HOME", Path.home() / ".cache" / "huggingface")) | |
| KB_PATH: Path = PROJECT_ROOT / "kb" / "music_models.json" | |
| HF_TOKEN: str | None = os.getenv("HF_TOKEN") or None | |
| OPENROUTER_API_KEY: str | None = os.getenv("OPENROUTER_API_KEY") or None | |
| # Discogs personal access token. When set, ``groundtruth/sources.fetch_discogs`` | |
| # uses the authenticated v2 API (60 req/min) instead of webpage scraping | |
| # (which Discogs 403s for generic User-Agents). When unset, Discogs coverage | |
| # is silently skipped — a one-time warning is logged. | |
| DISCOGS_TOKEN: str | None = os.getenv("DISCOGS_TOKEN") or None | |
| # LLM config — used by sync_pilot.description to synthesize per-track prose | |
| # from the structured tag layers. Default is DeepSeek (cheap, fluent EN, copes | |
| # with Turkish loanwords). Override LLM_MODEL to A/B with anthropic/claude-* | |
| # or openai/gpt-* variants without code changes. Pricing (USD per 1M tokens) | |
| # is model-dependent; we record per-call usage in the batch summary so cost | |
| # can be re-estimated if the rate sheet drifts. | |
| LLM_BASE_URL: str = os.getenv("LLM_BASE_URL", "https://openrouter.ai/api/v1") | |
| LLM_MODEL: str = os.getenv("LLM_MODEL", "deepseek/deepseek-chat") | |
| # Content-safety classification defaults to a Sonnet-class Claude: in the | |
| # 2026-06 A/B it caught both true-explicit tracks (050, 070) while deepseek-chat | |
| # missed the profanity track 050, at comparable cost/latency. Theme extraction | |
| # and all other LLM calls stay on LLM_MODEL (deepseek-chat) — Claude | |
| # under-generated themes (empty on several tracks) and R1 was too slow. | |
| CONTENT_SAFETY_LLM_MODEL: str = os.getenv( | |
| "SYNC_PILOT_CONTENT_SAFETY_MODEL", "anthropic/claude-sonnet-4.6" | |
| ) | |
| # Live Sync rerank is the user-facing, judgment-heavy, Turkish-brief step — the | |
| # one place a frontier model most visibly beats deepseek-chat (ranking quality + | |
| # native structured output, no ```json-fence fragility). Kept env-overridable so | |
| # it can be A/B'd against deepseek without code changes. | |
| SYNC_RERANK_LLM_MODEL: str = os.getenv( | |
| # Haiku for the live rerank: ~2-3x snappier than Sonnet at comparable ranking | |
| # quality, cheaper, higher rate limits — the right trade for a shared sync demo | |
| # where the LLM call is ~95% of the latency. Override the env to A/B vs Sonnet. | |
| "SYNC_PILOT_SYNC_RERANK_MODEL", "anthropic/claude-haiku-4.5" | |
| ) | |
| # Embedding model for Live Sync retrieval ONLY. Decoupled from the lyla text | |
| # classifier (which stays on multilingual-e5-base, the encoder its heads were | |
| # trained on). e5-large is a drop-in upgrade — same query:/passage: prefix | |
| # scheme, 1024d. Build (scripts/build_sync_index) and query (sync_recommend) | |
| # read this together, so the index and query stay dimension-consistent. | |
| SYNC_EMBED_MODEL: str = os.getenv( | |
| "SYNC_PILOT_SYNC_EMBED_MODEL", "intfloat/multilingual-e5-large" | |
| ) | |
| # Track descriptions are user-facing Turkish prose that must respect the GT | |
| # grounding (the specialist flagged deepseek mislabelling genre — "rock şarkısına | |
| # halk türküsü" — and producing prose off that). A frontier model follows the | |
| # grounding block far more reliably. Env-overridable to A/B against deepseek. | |
| DESCRIPTION_LLM_MODEL: str = os.getenv( | |
| "SYNC_PILOT_DESCRIPTION_MODEL", "anthropic/claude-sonnet-4.6" | |
| ) | |
| # Sync brief (the per-track placement recommendation shown in the showcase) and | |
| # the sync search keywords are user-facing judgment tasks — same rationale as | |
| # descriptions, so they move to the frontier model too. Theme extraction stays on | |
| # LLM_MODEL (deepseek): Claude under-generated themes (documented 2026-06). | |
| SYNC_BRIEF_LLM_MODEL: str = os.getenv("SYNC_PILOT_SYNC_BRIEF_MODEL", "anthropic/claude-sonnet-4.6") | |
| KEYWORDS_LLM_MODEL: str = os.getenv("SYNC_PILOT_KEYWORDS_MODEL", "anthropic/claude-sonnet-4.6") | |
| LLM_TEMPERATURE: float = float(os.getenv("LLM_TEMPERATURE", "0.4")) | |
| # DeepSeek-chat current OpenRouter list price (May 2026): roughly $0.27/1M in, | |
| # $1.10/1M out. Treat these as defaults for the cost estimate column in the | |
| # batch summary; bump from the env if the user is benching a different model. | |
| LLM_INPUT_PRICE_PER_M: float = float(os.getenv("LLM_INPUT_PRICE_PER_M", "0.27")) | |
| LLM_OUTPUT_PRICE_PER_M: float = float(os.getenv("LLM_OUTPUT_PRICE_PER_M", "1.10")) | |
| def _detect_device() -> "torch.device": | |
| import torch | |
| override = os.getenv("SYNC_PILOT_DEVICE", "").strip().lower() | |
| if override in {"mps", "cuda", "cpu"}: | |
| return torch.device(override) | |
| if torch.backends.mps.is_available(): | |
| return torch.device("mps") | |
| if torch.cuda.is_available(): | |
| return torch.device("cuda") | |
| return torch.device("cpu") | |
| # ``DEVICE`` is computed lazily so importing this module never triggers a | |
| # ``torch`` import. Pipeline code goes through ``get_device()``; the cached | |
| # value is reused for the rest of the process lifetime. | |
| _DEVICE: "torch.device | None" = None | |
| def get_device() -> "torch.device": | |
| global _DEVICE | |
| if _DEVICE is None: | |
| _DEVICE = _detect_device() | |
| return _DEVICE | |
| def get_default_dtype() -> "torch.dtype": | |
| import torch | |
| return _dtype_by_device().get(get_device().type, torch.float32) | |
| def _dtype_by_device() -> dict[str, "torch.dtype"]: | |
| import torch | |
| # SYNC_PILOT_FORCE_FP32 forces float32 on every device. Used on the live | |
| # backend's CUDA GPU so (a) inference matches the MPS-developed pipeline | |
| # bit-for-bit-ish (reproducibility) and (b) we avoid fp16-input vs fp32-weight | |
| # mismatches — MAEST loads its weights in fp32, so casting inputs to fp16 | |
| # (the cuda default below) raised "Input type (Half) and bias type (float)". | |
| if os.getenv("SYNC_PILOT_FORCE_FP32") == "1": | |
| return {"mps": torch.float32, "cuda": torch.float32, "cpu": torch.float32} | |
| # bfloat16 is unsupported on MPS — keep dtype map device-aware. | |
| return { | |
| "mps": torch.float32, | |
| "cuda": torch.float16, | |
| "cpu": torch.float32, | |
| } | |
| def __getattr__(name: str) -> Any: | |
| """Module-level lazy attribute access for the heavy-ML constants. | |
| Pipeline modules (``tagging.py``, ``clap_tagging.py``, ``transcription.py``) | |
| still reference ``config.DEVICE`` and ``config.DTYPE_BY_DEVICE`` directly. | |
| Defining them at module scope would re-pull ``torch`` on every import, | |
| which is what we moved away from so dashboard-only deployments stay | |
| light. PEP 562 ``__getattr__`` lets us keep the names available while | |
| deferring the ``torch`` import to first access. | |
| """ | |
| if name == "DEVICE": | |
| return get_device() | |
| if name == "DTYPE_BY_DEVICE": | |
| return _dtype_by_device() | |
| raise AttributeError(f"module {__name__!r} has no attribute {name!r}") | |
| # Source of truth for model selection is sync_pilot/kb/music_models.json. | |
| # When the music-models-scout updates the KB, mirror the winning hf_id here. | |
| MODELS: dict[str, dict[str, Any]] = { | |
| "tagging": { | |
| "hf_id": "mtg-upf/discogs-maest-30s-pw-129e", | |
| "library": "transformers", | |
| "trust_remote_code": True, | |
| "sample_rate_hz": 16000, | |
| "segment_seconds": 30, | |
| "license": "cc-by-nc-sa-4.0", | |
| "license_class": "non-commercial-research", | |
| }, | |
| "zero_shot_tagging": { | |
| # KB lists "laion/larger_clap_music" but that checkpoint ships with a | |
| # broken logit_scale (~1.0 instead of trained ~14-38), which collapses | |
| # softmax to ~uniform across prompts and makes zero-shot scoring | |
| # useless. We use "laion/clap-htsat-unfused" instead — also music- | |
| # trained (HTSAT audio backbone, MusicCaps-style captions), Apache-2.0, | |
| # 48 kHz, and produces well-calibrated zero-shot logits in the | |
| # transformers ClapModel API. Verified empirically on 038_dert_olur | |
| # (Anadolu rock track) where it cleanly separated Anatolian rock from | |
| # heavy metal and Turkish folk prompts. | |
| "hf_id": "laion/clap-htsat-unfused", | |
| "library": "transformers", | |
| "trust_remote_code": False, | |
| "sample_rate_hz": 48000, | |
| "license": "apache-2.0", | |
| "license_class": "permissive-commercial-ok", | |
| }, | |
| "lyrics_asr": { | |
| # Whisper-large-v3-turbo: modern Apache-2.0 OpenAI checkpoint, ~5x | |
| # faster than large-v3 with negligible quality loss on high-resource | |
| # languages (Turkish is well-covered). ~800 MB on disk. Override the | |
| # hf_id with SYNC_PILOT_LYRICS_ASR_MODEL to A/B against | |
| # openai/whisper-large-v3 (slower, slightly more accurate) or | |
| # openai/whisper-medium (smaller, faster). chunk_length_s=30 matches | |
| # Whisper's native receptive field; stride_length_s=5 gives the | |
| # transformers ASR pipeline overlap to stitch long-form outputs | |
| # without repeating phrases at chunk boundaries. | |
| "hf_id": os.getenv( | |
| "SYNC_PILOT_LYRICS_ASR_MODEL", "openai/whisper-large-v3-turbo" | |
| ), | |
| "library": "transformers", | |
| "trust_remote_code": False, | |
| "sample_rate_hz": 16000, | |
| "chunk_length_s": 30, | |
| "stride_length_s": 5, | |
| "language": "turkish", | |
| "task": "transcribe", | |
| "license": "apache-2.0", | |
| "license_class": "permissive-commercial-ok", | |
| }, | |
| "embeddings": { | |
| # TODO(music-models-scout): MERT-v1-95M / 330M candidate | |
| "hf_id": None, | |
| }, | |
| "structural_segmentation": { | |
| # TODO(music-models-scout) | |
| "hf_id": None, | |
| }, | |
| "audio_captioning": { | |
| # TODO(music-models-scout) | |
| "hf_id": None, | |
| }, | |
| "stem_separation": { | |
| # TODO(music-models-scout) — Phase 2 | |
| "hf_id": None, | |
| }, | |
| } | |
| # Canonical sample rate for normalized ingest. Stage modules resample from this | |
| # to their model-specific rate (see MODELS[stage]["sample_rate_hz"]). | |
| INGEST_SAMPLE_RATE_HZ: int = 44100 | |
| INGEST_CHANNELS: int = 1 | |
| def ensure_dirs() -> None: | |
| for d in (AUDIO_DIR, CACHE_DIR, OUTPUT_DIR, EMBEDDINGS_DIR, LOGS_DIR): | |
| d.mkdir(parents=True, exist_ok=True) | |
| def stage_cache_dir(stage: str) -> Path: | |
| p = CACHE_DIR / stage | |
| p.mkdir(parents=True, exist_ok=True) | |
| return p | |
| def setup_logging(level: int = logging.INFO) -> None: | |
| logging.basicConfig( | |
| level=level, | |
| format="%(asctime)s %(levelname)s %(name)s: %(message)s", | |
| datefmt="%Y-%m-%dT%H:%M:%S", | |
| ) | |