Spaces:
Sleeping
Sleeping
BarScript: serve the fitted (non-uniform) bar timeline by default; report grid source, spread and fit quality in the UI
7cc74a6 verified | # 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`](https://huggingface.co/JacobLinCool/softchart-barscript). | |
| It has measured defects that are shown in the interface and repeated in full | |
| below. Read [Known limitations](#known-limitations-of-barscript) before drawing | |
| any conclusion from a chart it produces. | |
| --- | |
| ## Running it locally | |
| ### 1. Environment | |
| Python 3.11, from `spaces/`: | |
| ```bash | |
| 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: | |
| ```bash | |
| 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. | |
| ```bash | |
| 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 | |
| ```bash | |
| cd spaces | |
| python app.py # http://localhost:7860 | |
| ``` | |
| Check it came up, and that BarScript is being offered: | |
| ```bash | |
| 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: | |
| ```bash | |
| 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_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: | |
| 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). | |
| ```bash | |
| 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: | |
| ```bash | |
| 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](#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. | |