The music curation engine exists to solve a real problem for working DJs: the raw track-to-listen ratio on Beatport or Traxsource is roughly 50:1. For every track worth playing, you audition dozens that aren’t even close. OpenSwarm’s curation pipeline compresses that down to approximately 5:1 — not by replacing your ears, but by doing the first filter pass automatically, surfacing the ~30 best candidates from a new-release scan so you spend your limited review time on tracks that already match your taste. You still pick the final ten. The engine just does the grunt work.Documentation Index
Fetch the complete documentation index at: https://mintlify.com/UnkleFunk/HouseMusicSwarm-/llms.txt
Use this file to discover all available pages before exploring further.
The pipeline
Discovery — fetch new release candidates
discovery.py polls Beatport genre and label pages for new tracks and normalises them into a Track dataclass. It fetches Beatport’s embedded __NEXT_DATA__ JSON directly from genre pages (deep-house, house, tech-house, minimal-deep-tech, afro-house, organic-house-downtempo) and from a configurable set of target label pages defined in label_ids.json. Traxsource currently sits behind Cloudflare bot protection that blocks both plain requests and headless browsers, so Beatport is the sole automated source today. Raw candidates are written to raw_candidates.json.discovery.py runs without an API key — it reads Beatport’s public genre and label pages directly. Set --source labels (default) to scan your target labels only, --source genres for genre-wide sweeps, or --source both for a full pass. Traxsource remains a manual-check platform for now.Scoring — rank candidates against your taste profile
scoring.py loads profile.yaml (your hand-tuned baseline) and profile.learned.yaml (auto-generated overrides from feedback), then scores each candidate track across eight weighted dimensions. The result is a ScoreBreakdown with a total out of 100 and a per-feature explanation. Tracks that trigger hard rejections — a blocked label, a BPM outside the reject band — are filtered out immediately. The top 30 scoreable tracks are saved to ranked_top_30.json.Review — approve candidates in the local Flask UI
review_ui.py starts a local Flask app at http://localhost:5678. Each candidate card shows the track title, artist, label, BPM, key, the scorer’s total, and a collapsible per-feature breakdown. A link opens the track directly on Beatport for a 30-second preview. Tap 👍 Yes, 🤔 Maybe, or 👎 No — optionally tag a reason like “too tech” or “wrong vibe”. Every decision is written to feedback.sqlite and feeds the learning loop. Access the review UI from your phone on the same network or via Tailscale.Publish — write the chart to your website
publish.py reads the ten most-recent yes decisions from feedback.sqlite and assembles official Beatport iframe embeds (and Traxsource styled-link cards) into a <!-- CHARTS:START --> … <!-- CHARTS:END --> block inside website/index.html. The charts CSS is appended to style.css if not already present. The file is written but not auto-pushed — review the diff with git diff website/, then commit and push (or hand the push off to Claude Code). GitHub Actions deploys to unklefunk.music on every push to main.Explainable scoring
Every scored track produces a human-readable breakdown so you can see exactly why something ranked where it did — and tune the profile or add feedback if it’s wrong. Here’s a real example from the test suite inscoring.py:
profile.yaml directly, or give feedback that will auto-adjust the weights.
Score dimensions
Eight dimensions make up the 100-point total. Weights are defined inprofile.yaml and sum to 100.
BPM (20%)
The strongest fast filter. Full points (100) for the bullseye range of 120–123. Drops to 75 in the acceptable band (118–125), 45 in the tolerated band (116–127), and 0 outside the reject band (<112 or >130). A zero here short-circuits scoring and marks the track rejected.
Label (25%)
The biggest coarse filter. Tier 1 labels score 100, Tier 2 labels score 65, everything else gets a fallback of 30. Tracks on the reject list (e.g. Spinnin’ Records) are eliminated before any other dimension runs.
Artist (20%)
Seed scores from
profile.yaml give known artists a starting position (e.g. Kerri Chandler: 95, Dantiez Saunderson: 88). Unknown artists start neutral at 50. The learning loop shifts these up or down based on your yes/no feedback.Sonic (15%)
Positive descriptors like
warm, analog, chicago house, soulful, and paradise garage add to the score via a sigmoid-squashed sum. Negative descriptors like edm, big room, and festival tech subtract. Partial tag matches count.Duration (5%)
DJ tools in the 6:40–9:00 range (400–540 seconds) score 100. Tracks under 5 minutes are likely radio edits and score 20. Over 12 minutes triggers a “check for filler” flag. The acceptable window is 5:40–11:00.
Vocal (5%)
Soulful lead, spoken word, adlibs, and sample-based vocals are rewarded (95). Vocal chops, diva runs, and guest-rapper features are penalized (25). Instrumentals score a neutral 60.
Key (5%)
Minor keys score higher by default. Favoured Camelot positions (4A–11A) score 100; other minor keys score 80. Major keys take a 0.7 penalty unless the soulful sonic descriptor also fires. Tracks with no key metadata score a neutral 50.
Recency (5%)
Tracks released within the last 90 days score 100, within 180 days score 70, older tracks score 30. Reissues on heritage labels (Salsoul, Nu Groove, Trax) get a partial credit bonus even if the original recording is decades old.
The learning loop
Every decision you make in the review UI writes a row tofeedback.sqlite — track metadata, your verdict (yes / no / maybe), and any reason tags you picked. Every 25 decisions, the engine retrains:
- Artist scores shift up on yes, down on no — bounded at ±15% per event so no single track can wildly derail an established score.
- Label tier confidence adjusts — a Tier 2 label with eight yes-votes earns promotion to Tier 1.
- Sonic descriptor weights update based on which tags consistently predict yes versus no.
profile.learned.yaml. Your hand-tuned profile.yaml stays clean as the baseline. To reset the learning and start fresh, delete profile.learned.yaml — the scorer will fall back entirely to profile.yaml on the next run.
Configuring the taste profile
music_curation/profile.yaml is the hand-tuned source of truth. The scorer reloads it on every run — no restart required. Every block is a knob:
discovery.py run.
Discovery access
discovery.py reads Beatport’s public pages directly — no API key, no per-request cost. For a more targeted pass, populate label_ids.json with the Beatport IDs of your priority labels, then run with --source labels. For a genre-wide sweep, use --source genres. To do both in one pass: --source both.
Historical discovery options (reference only)
Historical discovery options (reference only)
- Option A — SearchAPI.io key: Uses
SEARCH_API_KEYto find Beatport URLs via search, then fetches each page for metadata. Works today but costs ~40 paid requests per pass and search snippets don’t carry release dates reliably. - Option B — Beatport partner API: Cleaner structured data, but access is gated and typically restricted to Beatport retail partners. Not a practical path for independent producers.
- Option C — Traxsource + Beatport RSS/label pages: Free, but Traxsource now sits behind Cloudflare bot protection. Beatport label pages are the reliable free path — now the default in
discovery.py.
Traxsource is not currently automatable. Its site serves a JS challenge page to both plain HTTP requests and headless Playwright browsers (verified). Traxsource remains a manual-check platform —
discovery.py is Beatport-only until a reliable path to Traxsource data emerges.