softchart / SC2_INTEGRATION.md
JacobLinCool's picture
BarScript: serve the fitted (non-uniform) bar timeline by default; report grid source, spread and fit quality in the UI
7cc74a6 verified
|
Raw
History Blame Contribute Delete
24.2 kB

A newer version of the Gradio SDK is available: 6.21.0

Upgrade

BarScript in the Space — integration and verification

BarScript (run sc2_c1_v13_s1234_best, 9.1 M parameters) is selectable in the Space as BarScript (preview), alongside the seven published 1.x models, which are unchanged. Ura is now a fifth difficulty on every model.

BarScript is the release name; sc2_c1_v13_s1234 is the run that produced this checkpoint, and it stays that way everywhere it is recorded — in the package directory name, the manifest, and every md5 and sha in the provenance chain. Renaming those would break the trail back to the training run.

BarScript is an experimental preview, published to the private repo JacobLinCool/softchart-barscript. It has measured defects that are shown in the interface and repeated in full below. Read Known limitations before drawing any conclusion from a chart it produces.


Running it locally

1. Environment

Python 3.11, from spaces/:

python3.11 -m venv .venv && source .venv/bin/activate
pip install -r requirements.txt

On Apple Silicon the pinned torch==2.13.0+cpu has no wheel (that build is published for Linux and Windows only). Install the platform build instead and take the rest of the file as-is:

pip install torch==2.13.0
pip install "numpy<2.1" scipy "librosa>=0.10" "numba>=0.60" soundfile \
            matplotlib pillow gradio==6.20.0 gradio_client==2.5.0 \
            "pydantic>=2.10,<3" "huggingface_hub>=1.2,<2.0" safetensors

Everything below was verified on this stack (torch 2.13.0, gradio 6.20.0, numpy 2.0.2, librosa 0.11.0), CPU only.

2. Point the Space at the BarScript package

sc2_loader.resolve_package_dir() looks in one order, most explicit first, and reports which leg won as describe()["package_source"]:

# source package_source when
1 $SC2_PACKAGE_DIR env whenever it is set
2 spaces/models/sc2_c1_v13_s1234_best/, then the build artifact in the development worktree local a package is on disk
3 the Hub repo, via snapshot_download hub neither of the above

$SC2_PACKAGE_DIR that holds no package raises rather than falling through: naming a directory is a request for that directory, and quietly downloading a different copy instead would hide the typo.

export SC2_PACKAGE_DIR=/path/to/sc2_c1_v13_s1234_best   # optional

Running from this checkout, leg 2 finds the build artifact at .codex/worktrees/v18-8h/artifacts/sc2/sc2_c1_v13_s1234_best/ and nothing is downloaded.

The Hub repo is private, so leg 3 needs a token. Set HF_TOKEN, or run hf auth login; the loader also accepts $HUGGING_FACE_HUB_TOKEN and the token cache. With no token it fails with a message naming the credential and the three ways to supply one, rather than reporting a missing model — the two have different fixes. SC2_NO_HUB=1 disables leg 3 entirely, which is how a deployment proves it is not silently depending on a download.

Only the seven files of the release package are fetched (PACKAGE_FILES is passed to snapshot_download as an allow-list), and the snapshot is checked against the release_manifest.json that travels with it before it is served — those bytes crossed a network, and the manifest records the sha256 of each file as built. Local packages are not re-hashed; that check is for the transport.

Whichever leg wins, the package directory is the whole source of truth from that point on: the eight vendored serving modules are checked by md5 against config.json:code.module_md5, the token table is cross-checked, the schema is validated, and the Space's log-mel front end is matched to the package's contract. A downloaded package and a local one are served identically or not at all.

If no leg produces a package, the BarScript entry does not appear in the model menu and the seven published models work as usual. Nothing fails.

3. Start the Space

cd spaces
python app.py                      # http://localhost:7860

Check it came up, and that BarScript is being offered:

curl -s localhost:7860/health
# {"status":"ok","device":"cpu","models_loaded":false}

curl -s localhost:7860/api/capabilities | python -m json.tool

/api/capabilities is the source of the model menu, the experimental labelling, and the limitation text. The BarScript entry carries "experimental": true and all eight model-card limitations; the frontend renders them verbatim and cannot paraphrase them.

The published models are downloaded from the Hub on first use. With them already in the local cache you can run fully offline:

HF_HUB_OFFLINE=1 python app.py

4. Generate a chart

In the browser: upload audio, pick BarScript (preview), pick a difficulty (including Ura), generate. Two controls behave differently on BarScript:

  • Beat grid must stay on (or enter the BPM in Advanced settings). BarScript builds its bars from a downbeat and cannot place them without one; the route fails with an explanatory message rather than guessing a phase.
  • Structure / AI planner switches are ignored. BarScript takes no plan token, so the result reports Not used — BarScript takes no plan instead of a plan source.

The bar timeline

Two different things are called "the grid" and they have to be kept apart.

what it is who decides it
bar-local lattice how finely a note may sit inside one bar — the shipped /16 the release package (SHIP_GRID_DENOM), reported as Chart lattice
bar timeline where each barline falls in the audio the caller, reported as Bar grid / Tempo map

Only the second can express a tempo change, and until now the Space could not. sc2_loader.SoftChart2.generate synthesized the timeline from one scalar BPM, bar_sec = (240/bpm) * num/den tiled from the first downbeat, which is uniform by construction. It now takes either bpm= (that same uniform tiling) or grid= (a supplied timeline used verbatim) — exactly one, never both.

softchart/barscript_grid.py builds the supplied timeline from the beat fit the Space already computes. grid.fit_grid_piecewise returns downbeats that are not evenly spaced when it splices in a local tempo, so the fix is to pass those times through as the bar edges instead of re-tiling from fit["bpm"]. Both the decoder and the frozen TJA writer read every time out of measure_edgeswrite_tja_slots recomputes 60 * quarter_beats[m] / (edges[m+1] - edges[m]) per bar and emits #BPMCHANGE whenever it moves — so nothing else had to change, and neither softchart/sc2/ nor the shipping tree was touched.

The route is chosen by the fit's own gate, not by a new threshold:

timeline grid_source
fit passes fit["ok"] the fitted downbeats, verbatim piecewise
fit fails the gate, or none uniform tiling from one BPM uniform_fallback

A manual BPM needs no special case: fit_grid_fixed_bpm trusts the user's period and estimates only the phase, so its downbeats are evenly spaced and the same route yields a uniform timeline at the requested tempo.

On top of fit["ok"] the builder applies structural checks only — a bar must imply 30–400 BPM (a strict superset of what the estimator can emit), the fit must cover the audio within four extrapolated bars, and a splice joint shorter than one 400 BPM bar is merged within a 5 % budget. Every repair is counted and reported.

How well it works, measured

60 held-out songs from the test split, full length, through the deployed beat path (v1.7 beat head + fit_grid_piecewise); ground truth for "does this song change tempo" is the authored trace.

trace says served non-uniform served uniform (piecewise route) fell back to one BPM
changes tempo (n=30) 8 (27 %) 14 (47 %) 8 (27 %)
constant (n=30) 8 (27 %) 16 (53 %) 6 (20 %)

So 16 songs were served a non-uniform timeline and 8 of them do not change tempo — at this sample size the verdict is a coin flip. Nine of the 16 span a factor of ≥ 1.9 between longest and shortest bar, six of those landing within 1 % of exactly ×2: the signature of a barline the estimator split in two, not of a tempo change. Six of the nine wide-spread grids are on trace-constant songs.

Two consequences, both deliberate:

  1. The interface says follows the estimated barlines, gives the spread as a number, and states that a split barline and a real tempo change are not told apart. It does not say the tempo change was captured.
  2. No new gate was added to suppress the wide-spread cases. The bound that would catch them has to be relative to the local bar period — the absolute 30–400 BPM bound cannot, since a 360 BPM bar beside 180 BPM bars is inside it — and specifying and validating such a rule is a design change, not a deployment one. Inventing a threshold here and shipping it unvalidated would be worse than shipping the measurement.

Variable meter is not supported at all. fit_grid reports a single meter, so measure_num/measure_den are constant and meter_source is recorded as fit_estimate. The grid dict and the TJA writer can both express a per-bar #MEASURE, but nothing in this stack estimates one from audio; writing one would be fabrication. Recomputing the corpus from trace over 1 134 songs / 4 449 charts, 29.0 % of charts change tempo and 52.4 % change meter — the earlier combined figure of 62.3 % overstated what this module reaches by about a factor of two, and the Space copy of the module says so in its docstring.


Tests

Three suites, all CPU, no network required (the third reads published models from the local HuggingFace cache; the Hub leg of the second is mocked).

cd spaces
python scripts/test_sc2_vendor.py     # vendored BarScript tree matches the package
python scripts/test_app_wiring.py     # routing, resolution order, ura, limitations
HF_HUB_OFFLINE=1 python scripts/test_space_e2e.py   # real weights, real music

Current status: all green — 6 vendor checks, 148 wiring checks, 380 end-to-end checks (6 songs × 5 difficulties, plus the published-model replay).

30 of the wiring checks are new with the bar timeline: that the loader is handed a supplied grid and not a scalar BPM, that the served bar edges are bitwise the fitted downbeats, that a two-tempo fit comes out with two bar lengths at the right tempi, that a constant song and a manual BPM both come out uniform, that a failed quality gate falls back and says so with its numbers, that the loader refuses an ambiguous bpm=+grid= call and a grid whose lattice disagrees with grid_denom, and — through the real decoder and the frozen TJA writer — that a two-tempo timeline reaches the file as a header BPM:120 plus #BPMCHANGE 150.

15 of the wiring checks cover package resolution: that $SC2_PACKAGE_DIR outranks a local copy and the Hub, that an unsatisfiable $SC2_PACKAGE_DIR raises instead of falling through, that a local copy outranks the Hub, that the Hub is reached only when neither is present, that SC2_NO_HUB disables it, that a missing token is reported as a credentials problem before any request is made, that the request carries the resolved token and an allow-list of exactly the package files, and that a downloaded package is refused when a vendored module md5, the schema, the vocabulary or the manifest disagrees. The Hub is mocked throughout — the subject is the precedence rule, and a suite that downloaded 18 MB to assert one would be testing the network instead. The real download is exercised separately.

test_space_e2e.py is the only one that decodes real music. It drives app.generate_chart itself — the same generator the frontend consumes — and inspects the artifacts it produced. Options:

python scripts/test_space_e2e.py --songs 2 --seconds 30   # quicker
python scripts/test_space_e2e.py --skip-legacy            # BarScript only
python scripts/test_space_e2e.py --strict-ladder          # fail if ladder not monotone
python scripts/test_space_e2e.py --audio-dir ../other     # your own audio
python scripts/test_space_e2e.py --json report.json

Fixtures come from the held-out test split of JacobLinCool/taiko-1000-parsed if it is in the local HuggingFace cache: real music the models never trained on, shipping authored charts for all five courses, which gives the difficulty ladder a reference. Nothing is downloaded; with no cache and no --audio-dir the suite reports that and exits.


What the end-to-end run shows

Six held-out songs (120–210 BPM), 60 s excerpts, five difficulties, greedy decoding — 30 real BarScript decodes, median 3.3 s each on CPU.

Every chart, on every difficulty, satisfies:

  • re-parses with softchart.sc2.trace into exactly one course section;
  • COURSE: header is the literal the corpus uses, and resolves back to the course requested;
  • bars are all 96 digits, and the bar count follows tempo and excerpt length to within 1.5 bars — the lattice is anchored, not drifting;
  • every note sits on the shipped /16 lattice (0 off-lattice notes in 30 charts);
  • drumroll spans are balanced and correctly ordered — no unclosed and no nested spans anywhere;
  • the note and span counts shown in the UI equal the counts recovered by re-parsing the file.

On these six songs the bar timeline is uniform either way: four fits pass their gate and produce evenly spaced barlines, two miss it and fall back, so the served grids are identical to what shipped before. The excerpts are 60 s taken from 20 % into each song, which is why the fits are worse here than on full songs. The non-uniform path is measured separately, on full-length audio, above.

Ura is harder than Oni — but by much less than authored charts

Note counts per course, same song. Generated rows cover the 60 s excerpt; authored rows cover the whole chart, so only the shape of each row — the step from one course to the next — is comparable between them, not the absolute numbers. (Duration-normalised rates follow in the next table.)

song easy normal hard oni ura
Doppelgangers 88 146 219 300 351
(authored) 136 253 368 634 929
美しく忙しきドナウ 89 141 218 317 348
(authored) 174 247 449 682 1038
零の交響曲 79 178 293 341 375
(authored) 163 214 434 648 893
雨とペトラ 92 152 291 369 402
(authored) 117 198 335 464 591
スプラトゥーン2 メドレー 82 132 219 336 373
(authored) 105 144 245 441 757
さんぽ 75 137 218 311 316
(authored) 87 107 249 371 816

Ura came out denser than Oni on 6 of 6 songs, and the whole ladder easy < normal < hard < oni < ura was monotone on 6 of 6. So ura is wired correctly and does produce a harder chart.

The size of the step is the weak part. Generated oni→ura is 1.02x–1.17x (median 1.10x); authored oni→ura on the same six songs is 1.27x–2.20x (median 1.49x). The gap is widest exactly where the authors stretched furthest: on さんぽ they went 371→816 notes (2.20x) while the model went 311→316 (1.02x). This is the 208-chart training deficit showing up, and it is the reason --strict-ladder is off by default: monotonicity here is a model result to be reported, not a wiring property to be assumed. Do not read "monotone on 6/6" as evidence that the difficulty family is nested — the model card measures that separately (§2.1) and finds it is not.

Median notes-per-second, generated vs authored across the six songs:

easy normal hard oni ura
generated 1.45 2.43 3.69 5.46 6.10
authored 1.08 1.76 2.91 4.56 6.70

BarScript is denser than authored on the four lower courses and sparser than authored on ura — the one course where it should be densest is the one where it falls short.

Note spacing

The model card reports 102 sub-40 ms gaps across 29 charts (§2.9). None of the 30 charts here contains one; the tightest gap measured was 76.9 ms.

This is a property of the deployment grid, not evidence that the defect is gone. On a uniform /16 lattice the minimum possible spacing is 15 / BPM seconds, so a sub-40 ms gap is arithmetically impossible below 375 BPM. These songs run 120–210 BPM. The model card's figure was measured on authored grids with finer denominators. On very fast songs the defect can return.

The seven published models are unchanged

Same six songs, real Hub weights, v1.7 and v1.5 x {hard, oni} — 24 comparisons of the current app.py against scripts/fixtures/app_baseline.py, the app as it stood before this change (pinned by sha256 a21c5853… to the same source the wiring suite's golden trace was recorded from).

  • 24/24 produced byte-identical TJA text.
  • 24/24 agree on every metric the old app reported.
  • The metrics panel gained three display-only fields — model, difficulty, plan_source — and dropped none. These are the new UI surface (model name, the TJA course literal, plan provenance), not a change in generation.

The wiring suite covers the same question from the other side: 35 mocked scenarios comparing call arguments. This one compares the charts themselves.


Known limitations of BarScript

Verbatim from MODEL_CARD_sc2_c1_v13_s1234.md. All eight are shown in the interface when BarScript is selected.

§0 The champion comparison is inconclusive: the challenger was run at one seed.
§2.1 The difficulty ladder is not nested. Independently decoded courses share 0.658/0.667/0.660 of their notes with the next harder course; authored charts share 0.95–0.99.
§2.9 Notes come out too dense: 102 sub-40 ms gaps across 29 charts (authored: 2 across 1), 19 of them below the series record of 29.41 ms, and up to 17 notes inside one second.
§2.7 One drumroll in seven is a micro-span (14.7 % vs 3.8 % authored).
§2.12 Fine-lattice and tuplet placement is much worse than average: 0.345 hit rate at positions whose denominator is divisible by 3.
§2.8 The deployment grid is a uniform /16 lattice, so triplets are arithmetically impossible — 3 does not divide 16.
§2.4 At the top of the density knob, drumrolls are squeezed out: span yield falls to 33 % of authored. The highest setting is not recommended.
corpus Ura was trained on 208 charts against roughly 1 100 per other course; expect lower quality.

Four operational notes are shown with them: the plan switches do not apply, BarScript needs a beat grid, a tempo change is followed only when the beat fit passes its gate (and the result says which happened), and a time-signature change is never followed.

The Ura warning applies to every model

Ura appears on all eight models, and the 208-chart notice is shown whichever one is selected — the shortage is in the training corpus, not in BarScript.

The song planner was never trained on Ura. scripts/train_planner.py builds its dataset with courses=("easy","normal","hard","oni"), so embedding row 4 never received a gradient; probing the published checkpoint gives a mean predicted density bucket of 1.471 for ura, between normal and hard, when ura is the densest course in the corpus. Ura therefore queries the planner with the Oni course id. This is disclosed before generation and again in the result (AI planner (Oni plan reused for Ura)). Turning the planner off avoids the substitution entirely.


Not done

  1. A deployed Space needs an HF_TOKEN secret, which has not been set. The package is now on the Hub, and the download path is verified: with every on-disk copy hidden, sc2_loader fetched all seven files from JacobLinCool/softchart-barscript, passed the md5 and vocabulary gates, and charted a held-out song through app.generate_chart in 6.0 s on CPU.

    What is missing is the credential on the deployment. The repo is private, so the running Space needs a read token in HF_TOKEN (Settings → Variables and secrets → New secret). Until that exists, a deployed Space reaches leg 3, finds no token, and reports exactly that.

    Two alternatives avoid the secret: copy the package to spaces/models/sc2_c1_v13_s1234_best/ so it travels with the Space (leg 2, 18 MB in the Space repo), or make the model repo public — which publishes the weights and the model card, including its KNOWN LIMITATIONS, and is a separate decision from this one.

  2. The BarScript beat front end is unevaluated. sc2_beat_model() borrows v1.7's beat head to get BPM and downbeats for its lattice. The model card's numbers were measured on authored grids, so no published figure describes BarScript on a detected grid. The end-to-end suite confirms the resulting bar structure is self-consistent, which is not the same as confirming the barlines match the music.

  3. Density bucket 8 for ura is unvalidated on BarScript. The bucket map is measured from the corpus and matches the rest of the repo, but §2.5 says absolute bucket calibration is off and §2.4 warns the top of the knob starves drumrolls. The ladder above shows ura landing below authored density, which is consistent with the knob not delivering what it asks for.

  4. A non-uniform bar timeline is served, but its accuracy is a coin flip. BarScript now decodes onto the fitted barlines, so a tempo change can survive — see The bar timeline. Over 60 held-out songs only 8 of the 16 non-uniform timelines belong to a song that actually changes tempo, and 9 of the 16 carry a ≥ 1.9× bar-length spread that looks like a split barline. The relative bound that would catch those is a design change and is not made here; the interface reports the spread and the ambiguity instead of claiming a tempo change was captured.

  5. No human play-testing. Every claim here is structural or statistical. Whether these charts are good — groove, pattern quality, whether the ura step feels like a step — has not been assessed by a player.

  6. No GPU run. Everything was verified on CPU, which is sufficient for correctness; the remote machine was not needed.


Layout

path role
spaces/softchart/sc2/ vendored BarScript serving code, 8 modules, byte-identical to the frozen shipping tree (md5 checked against the package at every load)
spaces/softchart/sc2_loader.py the only bridge: package loading, integrity checks, serving contract, supplied-grid route, COURSE: header rewrite, limitation text
spaces/softchart/barscript_grid.py builds the supplied bar timeline from a beat fit, so a tempo change reaches the decoder and the TJA
spaces/app.py routing, ura, the BarScript branch, /api/capabilities
spaces/static/ model menu, ura option, limitation panel
spaces/scripts/test_sc2_vendor.py vendored tree matches the package; 1.x untouched
spaces/scripts/test_app_wiring.py routing and published-model regression, under mocks
spaces/scripts/test_space_e2e.py real weights on real music, through the app entry point
spaces/scripts/fixtures/app_baseline.py the pre-change app, the regression baseline

The BarScript modules live in their own subpackage because the two generations have incompatible token tables (VOCAB.size 2071 vs 1810, MAX_TGT 960 vs 1536). All BarScript imports are package-relative, so softchart.sc2.generate resolves from .vocab import VOCAB to softchart.sc2.vocab and can never reach the 1.x table, or the reverse.