git.iko.soy/rowtype-yoga/purescript-basic-pitch
on main help sign in
Add BasicPitch.Cdn: self-contained streaming module with NO static @spotify/basic-pitch import. Loads basic-pitch + tfjs from a CDN at runtime (dynamic import the bundler ignores — lets Turbopack avoid bundling tfjs), and evaluateFrames returns the raw frame-activation matrix for streaming peak-pick detection.
commit9e297e02900cb5ed8523108327239e601fdd15da authorMark <mark.eibes@gmail.com> dateSun 31 May 2026, 15:19 (4 months ago) parent9d9d8c7
2 files changed+91
unifiedsplit
src/BasicPitch/Cdn.js+39 src/BasicPitch/Cdn.purs+52
src/BasicPitch/Cdn.jsnew file+39
@@ -0,0 +1,39 @@
1+// Runtime-CDN basic-pitch loader. Deliberately has NO static import of
2+// @spotify/basic-pitch or @tensorflow/tfjs — they're fetched with a dynamic
3+// import() the bundler is told to ignore. This is what lets a client bundler
4+// (e.g. Turbopack) avoid choking on tfjs's megabytes; the browser fetches the
5+// pre-bundled ESM directly. The model is the consumer's to cache (load once).
6+
7+const cdnImport = (url) => import(/* webpackIgnore: true */ /* @vite-ignore */ url);
8+
9+let backendLabel = "?";
10+
11+export const loadModelImpl = (sources, modelPath) =>
12+ (async () => {
13+ const bp = await cdnImport(sources.basicPitch);
14+ try {
15+ const tf = await cdnImport(sources.tfjs);
16+ await tf.ready();
17+ backendLabel = tf.getBackend();
18+ } catch (e) {
19+ backendLabel = "?";
20+ }
21+ const model = new bp.BasicPitch(modelPath);
22+ await model.model;
23+ return model;
24+ })();
25+
26+export const backend = () => backendLabel;
27+
28+export const evaluateFramesImpl = (model, samples) =>
29+ new Promise((resolve, reject) => {
30+ const frames = [];
31+ model
32+ .evaluateModel(
33+ Float32Array.from(samples),
34+ (f) => { for (const row of f) frames.push(Array.from(row)); },
35+ () => {},
36+ )
37+ .then(() => resolve(frames))
38+ .catch(reject);
39+ });
src/BasicPitch/Cdn.pursnew file+52
@@ -0,0 +1,52 @@
1+module BasicPitch.Cdn
2+ ( CdnModel
3+ , CdnSources
4+ , esmSources
5+ , loadModel
6+ , evaluateFrames
7+ , backend
8+ ) where
9+
10+import Prelude
11+
12+import Effect (Effect)
13+import Effect.Aff (Aff)
14+import Effect.Uncurried (EffectFn2, runEffectFn2)
15+import Promise (Promise)
16+import Promise.Aff (toAffE)
17+
18+-- | A basic-pitch model loaded from a CDN at runtime — see `loadModel`. Distinct
19+-- | from `BasicPitch.Model` so importing this module pulls in NO static
20+-- | `@spotify/basic-pitch` dependency (it would otherwise be bundled).
21+foreign import data CdnModel :: Type
22+
23+-- | The ES-module URLs to import basic-pitch and TensorFlow.js from at runtime.
24+type CdnSources = { basicPitch :: String, tfjs :: String }
25+
26+-- | Sensible defaults: esm.sh-hosted basic-pitch 1.0.1 and tfjs 4.22.0.
27+esmSources :: CdnSources
28+esmSources =
29+ { basicPitch: "https://esm.sh/@spotify/basic-pitch@1.0.1"
30+ , tfjs: "https://esm.sh/@tensorflow/tfjs@4.22.0"
31+ }
32+
33+-- | Load basic-pitch + tfjs from a CDN at runtime (a dynamic `import()` the bundler
34+-- | leaves alone — useful when a bundler chokes trying to bundle tfjs into the
35+-- | client), then load the model from `modelPath` (a URL to its `model.json`).
36+-- | Prefers the WebGL backend. The result is cached weights ready for inference.
37+loadModel :: CdnSources -> String -> Aff CdnModel
38+loadModel sources modelPath = toAffE (runEffectFn2 loadModelImpl sources modelPath)
39+
40+foreign import loadModelImpl :: EffectFn2 CdnSources String (Promise CdnModel)
41+
42+-- | Transcribe one buffer of mono samples (at 22050 Hz) to its frame-activation
43+-- | matrix — rows of 88 pitch activations (MIDI 21–108), ~11.6 ms per row. For
44+-- | streaming peak-pick detection on short windows, rather than full note events.
45+evaluateFrames :: CdnModel -> Array Number -> Aff (Array (Array Number))
46+evaluateFrames model samples = toAffE (runEffectFn2 evaluateFramesImpl model samples)
47+
48+foreign import evaluateFramesImpl :: EffectFn2 CdnModel (Array Number) (Promise (Array (Array Number)))
49+
50+-- | The TensorFlow.js backend the last-loaded model is running on (e.g. `"webgl"`),
51+-- | or `"?"` if it couldn't be determined — handy for a debug readout.
52+foreign import backend :: Effect String