Over 2026-10-21/22, camelot_from_youtube (a Tk app that had grown 25 analysis scripts) and CyborgDJ (a renderer holding forks of them) became two libraries, two apps and one shared data store: dj-score measures, dj-render renders, camelot is the UI and CyborgDJ the controller, all reading the
musiclibrary. No result changed along the way, and that was checked every time, not assumed. This is the dated specimen of Well-Structured Python at Scale: its §4 recipe, run on a real codebase.
Links: Well-Structured Python at Scale (the thesis this tests), dj-score, dj-render, CyborgDJ, Camelot From YouTube, CyborgDJ as Controller (the MCP design this enables), DJ Blend Tool Test Plan (the beat-grid bench), Oracles Are Objective Functions, The Three-Layer Method
| Before | After |
|---|---|
| camelot: UI + 25 scripts, each CLI with its own loader and grid; track data and audio in the repo | dj-score (analysis: grid, key, events, spectra, collision, Rekordbox export), layered and import-linted; camelot = the UI on top |
| CyborgDJ: renderer + forks of camelot scripts + a stale copy of its analysis + 28 specs, renders, transcripts | dj-render (spec → audio; may not import librosa or dj-score); CyborgDJ = the controller (CLI + scripts) |
| tracks scattered across repos; derived files beside the audio | music/: originals; analysis/<track>/; CyborgProjects/<project>/ (specs, renders, notes); local only |
| 4 camelot envs (two on Python 3.9), ~10 GB | 2 envs (UI; one GPU/ML env with a committed recipe), −4.9 GB |
The ledger with every verdict is dj-score’s MIGRATION.md; CyborgDJ’s AUDIT.md holds its half. Public repos now
hold code only. Retired tools sit in local, gitignored archive/ folders, and git history keeps their code.
1. Convert, don’t rewrite, and prove it against the original running. Every module moved with an oracle test that runs camelot’s own code (imported from its git history at a pinned revision, so archiving can’t break it) on the same input and requires identical output: JSON, cache files, chart pixels, a replayed UI session (43 observations), a printed report. For the renderer the oracle is byte-identical WAVs from four transitions, re-run after every structural step. “Same output” is cheap to check and settles arguments that review cannot.
2. An oracle must be shown able to fail. Green suites lied in five different ways, each found only by looking:
.pyc (same size, same second);The fixes, all mechanical: plant a mutation and watch it fail; assert coverage; raise on empty inputs; fail rather than skip; reproduce the failure condition deliberately (Tk disabled) and revert the fix to see it break again.
3. Split along dependency lines, not topics. “Music” was one topic but two dependency families: analysis (librosa) and rendering (Rubber Band, scipy, no librosa). Chris: “the dependency issues are key here.” The seam was already visible in the code: the renderer needs grid results (tempo, downbeat, bar frames), never analysis code. An import contract makes the seam permanent, and a planted violation proves the contract bites.
4. One resolver for shared state. The music library’s layout is known in exactly one place (dj-score’s
adapters/library; dj-render repeats three lines rather than depend on it), and it is read from disk: a folder
with analysis/ is a library. So the UI, the CLIs, the renderer and the tests needed no configuration when Chris
moved the data, and audio outside a library still works the old way.
5. Archive, don’t purge; and data stays out of public view. Nothing was deleted. Tracked files were copied and
compared byte for byte before removal, untracked ones moved. Some “stale copies” turned out to be the only copy:
CyborgDJ’s key_analysis.json files were the only key analysis for four tracks, and a different tool’s output
than the same-named file in the library.
6. Accumulated state is contaminated, so check it against a lower artifact.
Each was caught by going one level down: the stems, drum onsets, Rekordbox’s native analyses. The new comb grid reproduced the September bench to 1e-9 s on all 14 tracks.
7. Baselines before claims. Every Breeder label sits on a phrase boundary, so “fire every 8 bars” scores 6/10.
Against that baseline, camelot’s micro “fill detector” was a phrase-position prior, and the full_drop event to
harvest existed only in a docstring. Nothing was left to extract.
8. On dense tracks a locked grid beats a detector, and the ear is the final judge. The measured-tempo Foss render was “so close that it is very hard to tell, but B does sound a little cleaner”: faded kicks turn mistiming into a warble, not double beats. A fixed tempo assumes the beat through the gaps where detectors lose it. Switching the renderer’s grid therefore waits on listening, and on an objective verify step.
9. Verify claims about the environment too. “ffmpeg is blocked” was carried for two days; it was only
ffprobe. Application Control allows specific builds: PyMuPDF loads when pinned to the allowed binary. Merging
two GPU envs was accepted only after their Demucs stems matched to 63–77 dB below each stem.
10. The corrections were part of the method. Several confident statements were wrong and were retracted with
evidence: the label grid being “exactly right”; ffmpeg; align_to_master.py as a live second consumer; the
audit’s plan to move loader I/O into dj-score, which contradicted its own dependency finding. Each surfaced
because the next step re-checked the last one.
snap_cues_to_bars folds into export;first_beat_sample overrides become unnecessary
across the Preach set, or does the ear still disagree somewhere?beat_this-style learning like the downbeat?