Spaces:
Sleeping
A newer version of the Gradio SDK is available: 6.21.0
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 planinstead 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_edges —
write_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:
- 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. - 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.traceinto 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
A deployed Space needs an
HF_TOKENsecret, 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_loaderfetched all seven files fromJacobLinCool/softchart-barscript, passed the md5 and vocabulary gates, and charted a held-out song throughapp.generate_chartin 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.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.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.
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.
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.
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.