Interactive explorer for compressibility of quantized filtered Gaussian time series
26 changed files+4211−0
.gitignoreadded+2−0View file
@@ -0,0 +1,2 @@
1+node_modules
2+dist
README.mdadded+50−0View file
@@ -0,0 +1,50 @@
1+# timeseries-compressibility
2+
3+Interactive exploration of how compressible quantized time series are.
4+
5+The generating model is: i.i.d. Gaussian noise (std σ, measured in quantization
6+steps) → FIR filter → optional additive uniform dither on [-½, ½) → round to
7+integers. The app shows the filter (convolution kernel and frequency response,
8+with cutoffs in Hz against a chosen sample rate), an endlessly scrolling view of
9+the generated integer signal, and the measured compression of a 120,000-sample
10+block under nine methods — zlib, zstd, and an rANS entropy coder, each raw,
11+delta-coded, and LPC-residual-coded — as bits per sample and as ratio against
12+raw int16 storage.
13+
14+Alongside the measurements it plots the theoretical bits/sample: the entropy
15+rate of the stationary filtered Gaussian process quantized at unit step, in the
16+high-resolution limit,
17+
18+```
19+R = ½ log₂(2πe) + ∫₀^½ log₂ S(f) df, S(f) = σ² |H(f)|²
20+```
21+
22+LPC + ANS should approach R — and does, where the formula is valid. Where the
23+filter's stopband pushes S(f) below one step² (see the dashed threshold on the
24+response plot), the formula under-predicts and can go negative; making that
25+breakdown visible is part of the point. The math section is a stub for the full
26+derivation.
27+
28+## Run it
29+
30+```sh
31+npm install
32+npm run dev
33+```
34+
35+## Layout
36+
37+```
38+src/model/ the pipeline (seeded Gaussian stream, FIR presets, dither,
39+ rounding) and the entropy-rate integral
40+src/compress/ lossless codecs run in the browser: zlib (fflate), zstd (wasm),
41+ ans.ts (a bit-identical port of simple_ans), and FLAC-style
42+ integer LPC; borrowed from entropy-quantized-linear-transform
43+src/worker/ the codecs run off the main thread on a debounced parameter set
44+src/components/ controls, filter plots, scrolling canvas, compression chart
45+```
46+
47+Every reported size round-trips through the decoder and includes whatever the
48+decoder needs (ANS symbol table, LPC coefficients). The compression block and
49+the scrolling display are fed by the same `Pipeline`, so what is compressed is
50+what is shown.
index.htmladded+12−0View file
@@ -0,0 +1,12 @@
1+<!doctype html>
2+<html lang="en">
3+ <head>
4+ <meta charset="UTF-8" />
5+ <meta name="viewport" content="width=device-width, initial-scale=1.0" />
6+ <title>Time-series compressibility</title>
7+ </head>
8+ <body>
9+ <div id="root"></div>
10+ <script type="module" src="/src/main.tsx"></script>
11+ </body>
12+</html>
package-lock.jsonadded+1920−0View file
@@ -0,0 +1,1920 @@
1+{
2+ "name": "timeseries-compressibility",
3+ "version": "0.0.0",
4+ "lockfileVersion": 3,
5+ "requires": true,
6+ "packages": {
7+ "": {
8+ "name": "timeseries-compressibility",
9+ "version": "0.0.0",
10+ "dependencies": {
11+ "@bokuweb/zstd-wasm": "^0.0.27",
12+ "fflate": "^0.8.3",
13+ "katex": "^0.16.25",
14+ "react": "^19.2.7",
15+ "react-dom": "^19.2.7"
16+ },
17+ "devDependencies": {
18+ "@types/node": "^26.1.1",
19+ "@types/react": "^19.2.17",
20+ "@types/react-dom": "^19.2.3",
21+ "@vitejs/plugin-react": "^5.1.1",
22+ "typescript": "~5.9.3",
23+ "vite": "^7.2.4"
24+ }
25+ },
26+ "node_modules/@babel/code-frame": {
27+ "version": "7.29.7",
28+ "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.29.7.tgz",
29+ "integrity": "sha512-Aup7aUOfpbAUg2ROOJN6Iw5f9DMBlzu0mIkm/malLQFN/YQgO48wCj0Kxa3sEHJvPVFg7siR+qRInwXd2qhQKw==",
30+ "dev": true,
31+ "license": "MIT",
32+ "dependencies": {
33+ "@babel/helper-validator-identifier": "^7.29.7",
34+ "js-tokens": "^4.0.0",
35+ "picocolors": "^1.1.1"
36+ },
37+ "engines": {
38+ "node": ">=6.9.0"
39+ }
40+ },
41+ "node_modules/@babel/compat-data": {
42+ "version": "7.29.7",
43+ "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.29.7.tgz",
44+ "integrity": "sha512-locTkQyKvwIEgBzVrn8693ebc97F2U8ZHjbXwDXJ5Fn2TCpNwTlKcaKLkdHop5c/icOFE7qt7Q9JC5hnKNa6Gg==",
45+ "dev": true,
46+ "license": "MIT",
47+ "engines": {
48+ "node": ">=6.9.0"
49+ }
50+ },
51+ "node_modules/@babel/core": {
52+ "version": "7.29.7",
53+ "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.29.7.tgz",
54+ "integrity": "sha512-RgHBCvtjbOK2gXSNBNIkNoEc9qoVEtau3hj8gEqKQuL3HZAibKarWFEI3Lfm6EYKkLalOh8eSrj9b+ch9H/VBA==",
55+ "dev": true,
56+ "license": "MIT",
57+ "dependencies": {
58+ "@babel/code-frame": "^7.29.7",
59+ "@babel/generator": "^7.29.7",
60+ "@babel/helper-compilation-targets": "^7.29.7",
61+ "@babel/helper-module-transforms": "^7.29.7",
62+ "@babel/helpers": "^7.29.7",
63+ "@babel/parser": "^7.29.7",
64+ "@babel/template": "^7.29.7",
65+ "@babel/traverse": "^7.29.7",
66+ "@babel/types": "^7.29.7",
67+ "@jridgewell/remapping": "^2.3.5",
68+ "convert-source-map": "^2.0.0",
69+ "debug": "^4.1.0",
70+ "gensync": "^1.0.0-beta.2",
71+ "json5": "^2.2.3",
72+ "semver": "^6.3.1"
73+ },
74+ "engines": {
75+ "node": ">=6.9.0"
76+ },
77+ "funding": {
78+ "type": "opencollective",
79+ "url": "https://opencollective.com/babel"
80+ }
81+ },
82+ "node_modules/@babel/generator": {
83+ "version": "7.29.7",
84+ "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.29.7.tgz",
85+ "integrity": "sha512-DkXD5OJQaAQIdZ1bt3UZdEnHAn9Imd3IVBdX03UFe+ony9Ojw5pzr9YVKGDY1jt+Gcn/FnGkNf8r+Vj5NOJWtQ==",
86+ "dev": true,
87+ "license": "MIT",
88+ "dependencies": {
89+ "@babel/parser": "^7.29.7",
90+ "@babel/types": "^7.29.7",
91+ "@jridgewell/gen-mapping": "^0.3.12",
92+ "@jridgewell/trace-mapping": "^0.3.28",
93+ "jsesc": "^3.0.2"
94+ },
95+ "engines": {
96+ "node": ">=6.9.0"
97+ }
98+ },
99+ "node_modules/@babel/helper-compilation-targets": {
100+ "version": "7.29.7",
101+ "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.29.7.tgz",
102+ "integrity": "sha512-wem6WaBj4NaVYVdNhLPPVacES6ZJ+KBBfSkTMD3YZxbP3rm3Di85tJU5ljaUNhaOynt+Aj0xruhYuzQBt8n71g==",
103+ "dev": true,
104+ "license": "MIT",
105+ "dependencies": {
106+ "@babel/compat-data": "^7.29.7",
107+ "@babel/helper-validator-option": "^7.29.7",
108+ "browserslist": "^4.24.0",
109+ "lru-cache": "^5.1.1",
110+ "semver": "^6.3.1"
111+ },
112+ "engines": {
113+ "node": ">=6.9.0"
114+ }
115+ },
116+ "node_modules/@babel/helper-globals": {
117+ "version": "7.29.7",
118+ "resolved": "https://registry.npmjs.org/@babel/helper-globals/-/helper-globals-7.29.7.tgz",
119+ "integrity": "sha512-3nQVUAtvkKH9zahfWgw96Jc/uFOmjACE1kQz82E2lqWmHBgjzbNlsC22nuQTfahmWeQtTq5nQ/4Nnd2A1wj4zA==",
120+ "dev": true,
121+ "license": "MIT",
122+ "engines": {
123+ "node": ">=6.9.0"
124+ }
125+ },
126+ "node_modules/@babel/helper-module-imports": {
127+ "version": "7.29.7",
128+ "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.29.7.tgz",
129+ "integrity": "sha512-ejHwrQQYcm9xnTivShn2IDOlIzInN34AXskvq9QicvCtEzq1Vzclu/tKF8Jq1Cg8JG2GL6/EmjgsCT7lXepE3g==",
130+ "dev": true,
131+ "license": "MIT",
132+ "dependencies": {
133+ "@babel/traverse": "^7.29.7",
134+ "@babel/types": "^7.29.7"
135+ },
136+ "engines": {
137+ "node": ">=6.9.0"
138+ }
139+ },
140+ "node_modules/@babel/helper-module-transforms": {
141+ "version": "7.29.7",
142+ "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.29.7.tgz",
143+ "integrity": "sha512-UPUVSyXbOh627KiCIGQSgwWzGeBKLkaJ9PJEdrngIwMSzxLR4jS4+f1f1jb7VzBbg8nFLaYotvVPFCTqdrmTAg==",
144+ "dev": true,
145+ "license": "MIT",
146+ "dependencies": {
147+ "@babel/helper-module-imports": "^7.29.7",
148+ "@babel/helper-validator-identifier": "^7.29.7",
149+ "@babel/traverse": "^7.29.7"
150+ },
151+ "engines": {
152+ "node": ">=6.9.0"
153+ },
154+ "peerDependencies": {
155+ "@babel/core": "^7.0.0"
156+ }
157+ },
158+ "node_modules/@babel/helper-plugin-utils": {
159+ "version": "7.29.7",
160+ "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.29.7.tgz",
161+ "integrity": "sha512-G7sHYigPY17oO5SYWnfD/0MTBwVR781S/JI643e/JhUYgVgWE/61SoW3NH9KWUKyKq5LVh3npif99Wkt6j86Jw==",
162+ "dev": true,
163+ "license": "MIT",
164+ "engines": {
165+ "node": ">=6.9.0"
166+ }
167+ },
168+ "node_modules/@babel/helper-string-parser": {
169+ "version": "7.29.7",
170+ "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.29.7.tgz",
171+ "integrity": "sha512-Pb5ijPrZ89GDH8223L4UP8i6QApWxs04RbPQJTeWDV0/keR2E36MeKnyr6LYmUUvqRRI+Iv87SuF1W6ErINzYw==",
172+ "dev": true,
173+ "license": "MIT",
174+ "engines": {
175+ "node": ">=6.9.0"
176+ }
177+ },
178+ "node_modules/@babel/helper-validator-identifier": {
179+ "version": "7.29.7",
180+ "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.29.7.tgz",
181+ "integrity": "sha512-qehxGkRj55h/ff8EMaJ+cYhyaKlHIxqYDn682wQD7RNp9UujOQsHog2uS0r2vzr4pW+sXf90NeeayjcNaX3fFg==",
182+ "dev": true,
183+ "license": "MIT",
184+ "engines": {
185+ "node": ">=6.9.0"
186+ }
187+ },
188+ "node_modules/@babel/helper-validator-option": {
189+ "version": "7.29.7",
190+ "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.29.7.tgz",
191+ "integrity": "sha512-N9ZErrD+yW5geCDtBqnOoxmR8+tNKiGuxKlDpuJxfsqpa2dFcexaziGAE/qoHLiDDreVNMupxGmSoNlyvsA3gw==",
192+ "dev": true,
193+ "license": "MIT",
194+ "engines": {
195+ "node": ">=6.9.0"
196+ }
197+ },
198+ "node_modules/@babel/helpers": {
199+ "version": "7.29.7",
200+ "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.29.7.tgz",
201+ "integrity": "sha512-1k2lAGRMfHTcwuNYcCNUmaUffmQv8KWMfh2iJUUeRlwlwH4FdNG7mfPI10NPfLHJFThE4Tyr4mv7kTNZOiPuBg==",
202+ "dev": true,
203+ "license": "MIT",
204+ "dependencies": {
205+ "@babel/template": "^7.29.7",
206+ "@babel/types": "^7.29.7"
207+ },
208+ "engines": {
209+ "node": ">=6.9.0"
210+ }
211+ },
212+ "node_modules/@babel/parser": {
213+ "version": "7.29.7",
214+ "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.29.7.tgz",
215+ "integrity": "sha512-hnORnjP/1P/zFEndoeX+n+t1RwWRJiJpM/jO7FW32Kn9r5+sJB2JWOdYo4L6k78j15eCwY3Gm/7364B1EMwtNg==",
216+ "dev": true,
217+ "license": "MIT",
218+ "dependencies": {
219+ "@babel/types": "^7.29.7"
220+ },
221+ "bin": {
222+ "parser": "bin/babel-parser.js"
223+ },
224+ "engines": {
225+ "node": ">=6.0.0"
226+ }
227+ },
228+ "node_modules/@babel/plugin-transform-react-jsx-self": {
229+ "version": "7.29.7",
230+ "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-self/-/plugin-transform-react-jsx-self-7.29.7.tgz",
231+ "integrity": "sha512-TL0hMc9xzy86VD31nUiwzd5otRAcyEPcsegCxolO0PvcXuH1v0kECe/UIznYFihpkvU5wg/jk4v0TTEFfm53fw==",
232+ "dev": true,
233+ "license": "MIT",
234+ "dependencies": {
235+ "@babel/helper-plugin-utils": "^7.29.7"
236+ },
237+ "engines": {
238+ "node": ">=6.9.0"
239+ },
240+ "peerDependencies": {
241+ "@babel/core": "^7.0.0-0"
242+ }
243+ },
244+ "node_modules/@babel/plugin-transform-react-jsx-source": {
245+ "version": "7.29.7",
246+ "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-source/-/plugin-transform-react-jsx-source-7.29.7.tgz",
247+ "integrity": "sha512-06IyK09H3wi4cGbhDBwp5gUGo0IKtnYa8tyTiephirPCK6fbobVGiXMMI5zLQ4aKEYP3wZ3ArU44o+8KMrSG/Q==",
248+ "dev": true,
249+ "license": "MIT",
250+ "dependencies": {
251+ "@babel/helper-plugin-utils": "^7.29.7"
252+ },
253+ "engines": {
254+ "node": ">=6.9.0"
255+ },
256+ "peerDependencies": {
257+ "@babel/core": "^7.0.0-0"
258+ }
259+ },
260+ "node_modules/@babel/template": {
261+ "version": "7.29.7",
262+ "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.29.7.tgz",
263+ "integrity": "sha512-puq+Gf35oI24FeN11LkoUQFqv9uwNeWpxXZi/Ji3rRIoKAzKnxRaZ+Gkj0vKS9ZCiTESfng1N9LyOyXvo+m+Gg==",
264+ "dev": true,
265+ "license": "MIT",
266+ "dependencies": {
267+ "@babel/code-frame": "^7.29.7",
268+ "@babel/parser": "^7.29.7",
269+ "@babel/types": "^7.29.7"
270+ },
271+ "engines": {
272+ "node": ">=6.9.0"
273+ }
274+ },
275+ "node_modules/@babel/traverse": {
276+ "version": "7.29.7",
277+ "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.29.7.tgz",
278+ "integrity": "sha512-EhlfNQtZ+NK22w5BM61ciuiq1m58ed33Wr1Xan//ZRTy6hgjnwyCffRYwzsGXdASJSUJ1guZILsErh1eQcl+zw==",
279+ "dev": true,
280+ "license": "MIT",
281+ "dependencies": {
282+ "@babel/code-frame": "^7.29.7",
283+ "@babel/generator": "^7.29.7",
284+ "@babel/helper-globals": "^7.29.7",
285+ "@babel/parser": "^7.29.7",
286+ "@babel/template": "^7.29.7",
287+ "@babel/types": "^7.29.7",
288+ "debug": "^4.3.1"
289+ },
290+ "engines": {
291+ "node": ">=6.9.0"
292+ }
293+ },
294+ "node_modules/@babel/types": {
295+ "version": "7.29.7",
296+ "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.29.7.tgz",
297+ "integrity": "sha512-4zBIxpPzowiZpusoFkyGVwakdRJUyuH5PxQ/PrqghfdFWWasvnCdPfQXHrenDai+gyLARulZjZowCOj6fjT4pA==",
298+ "dev": true,
299+ "license": "MIT",
300+ "dependencies": {
301+ "@babel/helper-string-parser": "^7.29.7",
302+ "@babel/helper-validator-identifier": "^7.29.7"
303+ },
304+ "engines": {
305+ "node": ">=6.9.0"
306+ }
307+ },
308+ "node_modules/@bokuweb/zstd-wasm": {
309+ "version": "0.0.27",
310+ "resolved": "https://registry.npmjs.org/@bokuweb/zstd-wasm/-/zstd-wasm-0.0.27.tgz",
311+ "integrity": "sha512-GDm2uOTK3ESjnYmSeLQifJnBsRCWajKLvN32D2ZcQaaCIJI/Hse9s74f7APXjHit95S10UImsRGkTsbwHmrtmg==",
312+ "license": "MIT"
313+ },
314+ "node_modules/@esbuild/aix-ppc64": {
315+ "version": "0.28.1",
316+ "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.28.1.tgz",
317+ "integrity": "sha512-Svl7tq8k/08+p6CXPpRjQ1fKX+1odH/BQbb48fV6fj3CWHhsoIOoY87w1oHXm0qEpkIK3ZfVgp0hed3XBXzXMQ==",
318+ "cpu": [
319+ "ppc64"
320+ ],
321+ "dev": true,
322+ "license": "MIT",
323+ "optional": true,
324+ "os": [
325+ "aix"
326+ ],
327+ "engines": {
328+ "node": ">=18"
329+ }
330+ },
331+ "node_modules/@esbuild/android-arm": {
332+ "version": "0.28.1",
333+ "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.28.1.tgz",
334+ "integrity": "sha512-0k2F129Xdio1TdJfzJ8sy1Q47vUD2NnwdhiAf7drUN1EBTfPf4hsFCtmMgu/6m8JSzsBrlmVjudMBQqOfG8usQ==",
335+ "cpu": [
336+ "arm"
337+ ],
338+ "dev": true,
339+ "license": "MIT",
340+ "optional": true,
341+ "os": [
342+ "android"
343+ ],
344+ "engines": {
345+ "node": ">=18"
346+ }
347+ },
348+ "node_modules/@esbuild/android-arm64": {
349+ "version": "0.28.1",
350+ "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.28.1.tgz",
351+ "integrity": "sha512-34EGEbCIAgosYz6goLcopX6Mo7NyGv9tfwEM2/7Ce2VcVRk568iSvniGWcUXIy7wEDR1wzolcxcriFVrWYcwBg==",
352+ "cpu": [
353+ "arm64"
354+ ],
355+ "dev": true,
356+ "license": "MIT",
357+ "optional": true,
358+ "os": [
359+ "android"
360+ ],
361+ "engines": {
362+ "node": ">=18"
363+ }
364+ },
365+ "node_modules/@esbuild/android-x64": {
366+ "version": "0.28.1",
367+ "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.28.1.tgz",
368+ "integrity": "sha512-dbwY7ltSMDWsRatcRpCnES4F+im88OCUgGZjy52shC7GqHRE/cYlxNbB4Z4UpJswpcc4Qxd2oE/ufM0p61IKng==",
369+ "cpu": [
370+ "x64"
371+ ],
372+ "dev": true,
373+ "license": "MIT",
374+ "optional": true,
375+ "os": [
376+ "android"
377+ ],
378+ "engines": {
379+ "node": ">=18"
380+ }
381+ },
382+ "node_modules/@esbuild/darwin-arm64": {
383+ "version": "0.28.1",
384+ "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.28.1.tgz",
385+ "integrity": "sha512-TZbWkQY7kvTAXbXUT7uVACR5cMHsDiSz9z7ZKAX/RTq/WJEk3QyRr0wZpNhBDX+/0CtdqUIJlOiodQcta6tY3Q==",
386+ "cpu": [
387+ "arm64"
388+ ],
389+ "dev": true,
390+ "license": "MIT",
391+ "optional": true,
392+ "os": [
393+ "darwin"
394+ ],
395+ "engines": {
396+ "node": ">=18"
397+ }
398+ },
399+ "node_modules/@esbuild/darwin-x64": {
400+ "version": "0.28.1",
401+ "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.28.1.tgz",
402+ "integrity": "sha512-zfdzgK9ACBNZLI/CyHTOx81SyNbM6YXn7rxSgX97VjyiPl9W1i4Ka4fgKECEoFCKGpvBj5qArWIGgQjOwkgskQ==",
403+ "cpu": [
404+ "x64"
405+ ],
406+ "dev": true,
407+ "license": "MIT",
408+ "optional": true,
409+ "os": [
410+ "darwin"
411+ ],
412+ "engines": {
413+ "node": ">=18"
414+ }
415+ },
416+ "node_modules/@esbuild/freebsd-arm64": {
417+ "version": "0.28.1",
418+ "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.28.1.tgz",
419+ "integrity": "sha512-wG2EA8ENdEI0qhkSZMjfqrdY+ziCYCPMmtZjjIwOmXFjmyzEHn+UUxk5of+SYsjtfs3VpnlC7QLzSI5hY/rOAw==",
420+ "cpu": [
421+ "arm64"
422+ ],
423+ "dev": true,
424+ "license": "MIT",
425+ "optional": true,
426+ "os": [
427+ "freebsd"
428+ ],
429+ "engines": {
430+ "node": ">=18"
431+ }
432+ },
433+ "node_modules/@esbuild/freebsd-x64": {
434+ "version": "0.28.1",
435+ "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.28.1.tgz",
436+ "integrity": "sha512-i7dZ9vQgnvSCzi/rYCXNgtF/U+eKZNJBzu3eTQbRgHnM7tNSizLOkRFAl3qzVc/Op/u5YkHHa4pf/3DOYHthLQ==",
437+ "cpu": [
438+ "x64"
439+ ],
440+ "dev": true,
441+ "license": "MIT",
442+ "optional": true,
443+ "os": [
444+ "freebsd"
445+ ],
446+ "engines": {
447+ "node": ">=18"
448+ }
449+ },
450+ "node_modules/@esbuild/linux-arm": {
451+ "version": "0.28.1",
452+ "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.28.1.tgz",
453+ "integrity": "sha512-qVXBOHQS+d5Y722GwJzJUtOLlX7km3CraOaGormF1pDtPd2C/l1SHRPgjLunLGe51Sh5YYWKMFDyV4SxgMQYTQ==",
454+ "cpu": [
455+ "arm"
456+ ],
457+ "dev": true,
458+ "license": "MIT",
459+ "optional": true,
460+ "os": [
461+ "linux"
462+ ],
463+ "engines": {
464+ "node": ">=18"
465+ }
466+ },
467+ "node_modules/@esbuild/linux-arm64": {
468+ "version": "0.28.1",
469+ "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.28.1.tgz",
470+ "integrity": "sha512-yHs+0uc8+nvEAfAfxrWQKK5peSNzBc4PegcMO0EJ2hT71uA7vB8Ihg2e77R2P7SG5uYjPbHlLLmve4LLLRCf0g==",
471+ "cpu": [
472+ "arm64"
473+ ],
474+ "dev": true,
475+ "license": "MIT",
476+ "optional": true,
477+ "os": [
478+ "linux"
479+ ],
480+ "engines": {
481+ "node": ">=18"
482+ }
483+ },
484+ "node_modules/@esbuild/linux-ia32": {
485+ "version": "0.28.1",
486+ "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.28.1.tgz",
487+ "integrity": "sha512-d1z4ZuP0ajrfz/FhGT4vv278rX8KnPPJx8i5+AtK7TYbx9Le9F1hyzurZpkEyjkGa9dUGhQow4C1NmeGvqxN2w==",
488+ "cpu": [
489+ "ia32"
490+ ],
491+ "dev": true,
492+ "license": "MIT",
493+ "optional": true,
494+ "os": [
495+ "linux"
496+ ],
497+ "engines": {
498+ "node": ">=18"
499+ }
500+ },
501+ "node_modules/@esbuild/linux-loong64": {
502+ "version": "0.28.1",
503+ "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.28.1.tgz",
504+ "integrity": "sha512-M5sRjUVZrkm1OAPR3dlOYzNmN+loZKGVi1VUQGrwuqLcbR6qeAz+famMhjASeH3YVKvZz+zT1jlh/keC3Rj/lg==",
505+ "cpu": [
506+ "loong64"
507+ ],
508+ "dev": true,
509+ "license": "MIT",
510+ "optional": true,
511+ "os": [
512+ "linux"
513+ ],
514+ "engines": {
515+ "node": ">=18"
516+ }
517+ },
518+ "node_modules/@esbuild/linux-mips64el": {
519+ "version": "0.28.1",
520+ "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.28.1.tgz",
521+ "integrity": "sha512-mRObBZeHh2OxcBFPWE/FjylkRgZdYuiTR3vaTozquCGOH14iP9oN4x4Ge81CoIDYQrXmIxpFumJBu5MtZpnQJQ==",
522+ "cpu": [
523+ "mips64el"
524+ ],
525+ "dev": true,
526+ "license": "MIT",
527+ "optional": true,
528+ "os": [
529+ "linux"
530+ ],
531+ "engines": {
532+ "node": ">=18"
533+ }
534+ },
535+ "node_modules/@esbuild/linux-ppc64": {
536+ "version": "0.28.1",
537+ "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.28.1.tgz",
538+ "integrity": "sha512-slScBsMAb3GFDcdrCgLwZtPYRoH2H/youv10QiZyRjmsP48fznoveWytSgCI/R0ZcUgpc0ZhIUEx6LHts8yrfQ==",
539+ "cpu": [
540+ "ppc64"
541+ ],
542+ "dev": true,
543+ "license": "MIT",
544+ "optional": true,
545+ "os": [
546+ "linux"
547+ ],
548+ "engines": {
549+ "node": ">=18"
550+ }
551+ },
552+ "node_modules/@esbuild/linux-riscv64": {
553+ "version": "0.28.1",
554+ "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.28.1.tgz",
555+ "integrity": "sha512-kw0owk1o0GFETUJyW0jc0G4Yzs0BHZn0JDZ8JRT088vjJYX777BAs1fDGxAC+q831qOs2DTC96mNsG2opdfyyQ==",
556+ "cpu": [
557+ "riscv64"
558+ ],
559+ "dev": true,
560+ "license": "MIT",
561+ "optional": true,
562+ "os": [
563+ "linux"
564+ ],
565+ "engines": {
566+ "node": ">=18"
567+ }
568+ },
569+ "node_modules/@esbuild/linux-s390x": {
570+ "version": "0.28.1",
571+ "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.28.1.tgz",
572+ "integrity": "sha512-/lAIjX8aYFRByhh6L5rYtPEDRqa9de/4V/juOXcta5frjvzXO4/sqEtyytse0g3zZFuWu5cDN0MkLz2qRDD2Ag==",
573+ "cpu": [
574+ "s390x"
575+ ],
576+ "dev": true,
577+ "license": "MIT",
578+ "optional": true,
579+ "os": [
580+ "linux"
581+ ],
582+ "engines": {
583+ "node": ">=18"
584+ }
585+ },
586+ "node_modules/@esbuild/linux-x64": {
587+ "version": "0.28.1",
588+ "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.28.1.tgz",
589+ "integrity": "sha512-u/anNYF2mmVOEDwLtnQ1wOr3EZ9sTNGLWrsYGYwHWzGA3Si84IOkHXlbWTD1NB+9/1lcnweYKO54uhxZydNzfA==",
590+ "cpu": [
591+ "x64"
592+ ],
593+ "dev": true,
594+ "license": "MIT",
595+ "optional": true,
596+ "os": [
597+ "linux"
598+ ],
599+ "engines": {
600+ "node": ">=18"
601+ }
602+ },
603+ "node_modules/@esbuild/netbsd-arm64": {
604+ "version": "0.28.1",
605+ "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.28.1.tgz",
606+ "integrity": "sha512-oks0DYbLwWMmaakTsCb+zL4E+aHRVLom9IJZOAthMQEPiQmydXHkziYEsGYRx0uNV/IjEKGAV941JzH02pflqw==",
607+ "cpu": [
608+ "arm64"
609+ ],
610+ "dev": true,
611+ "license": "MIT",
612+ "optional": true,
613+ "os": [
614+ "netbsd"
615+ ],
616+ "engines": {
617+ "node": ">=18"
618+ }
619+ },
620+ "node_modules/@esbuild/netbsd-x64": {
621+ "version": "0.28.1",
622+ "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.28.1.tgz",
623+ "integrity": "sha512-aeL6lAnN89Hz43Mlh1G8ARasbuoYvSITDEx0tHh5b7jJnHcssqgjy9Yx430GDpmCa6OyrKoS0aNRjKundRizGg==",
624+ "cpu": [
625+ "x64"
626+ ],
627+ "dev": true,
628+ "license": "MIT",
629+ "optional": true,
630+ "os": [
631+ "netbsd"
632+ ],
633+ "engines": {
634+ "node": ">=18"
635+ }
636+ },
637+ "node_modules/@esbuild/openbsd-arm64": {
638+ "version": "0.28.1",
639+ "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.28.1.tgz",
640+ "integrity": "sha512-MEFJe5C3R8pwXdZ5Y21oo6m7ePiS0d9pWucn99O/wvyJZChoIQKrQDxKrGeW8F5+T0okTHesAmDeiHDTIq0V/Q==",
641+ "cpu": [
642+ "arm64"
643+ ],
644+ "dev": true,
645+ "license": "MIT",
646+ "optional": true,
647+ "os": [
648+ "openbsd"
649+ ],
650+ "engines": {
651+ "node": ">=18"
652+ }
653+ },
654+ "node_modules/@esbuild/openbsd-x64": {
655+ "version": "0.28.1",
656+ "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.28.1.tgz",
657+ "integrity": "sha512-i/ZLIOafE0Z8cI/XANJAixoJL/uRAoS2xOA3rb0xN+KK0K177cMAsQYkzHtBrtMXAKuAc7HGgcWiZ/sRC1Nxgw==",
658+ "cpu": [
659+ "x64"
660+ ],
661+ "dev": true,
662+ "license": "MIT",
663+ "optional": true,
664+ "os": [
665+ "openbsd"
666+ ],
667+ "engines": {
668+ "node": ">=18"
669+ }
670+ },
671+ "node_modules/@esbuild/openharmony-arm64": {
672+ "version": "0.28.1",
673+ "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.28.1.tgz",
674+ "integrity": "sha512-ge+Z7EXFNt2BO1oAMsVpiQ8EwndV9i1xXerAeTIK7AtPs3bKFXQM7nlRxDSIUIMeueR1CNXxqztLzdNeReKBJg==",
675+ "cpu": [
676+ "arm64"
677+ ],
678+ "dev": true,
679+ "license": "MIT",
680+ "optional": true,
681+ "os": [
682+ "openharmony"
683+ ],
684+ "engines": {
685+ "node": ">=18"
686+ }
687+ },
688+ "node_modules/@esbuild/sunos-x64": {
689+ "version": "0.28.1",
690+ "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.28.1.tgz",
691+ "integrity": "sha512-BEjgtECkL3vY+SaSQ6nzVfiALUeFxpawyp8Jmf5PtYhf1Ug40N1h/hxlhts+f1FvSvarEigdxS3BlSMI2PJLcQ==",
692+ "cpu": [
693+ "x64"
694+ ],
695+ "dev": true,
696+ "license": "MIT",
697+ "optional": true,
698+ "os": [
699+ "sunos"
700+ ],
701+ "engines": {
702+ "node": ">=18"
703+ }
704+ },
705+ "node_modules/@esbuild/win32-arm64": {
706+ "version": "0.28.1",
707+ "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.28.1.tgz",
708+ "integrity": "sha512-lCv9eK/H6ZJWbE7bh2nw54CZ9M2nupBxJcTsdk/QQnWkdSjKGuxmmH8/GWrlT1eMmZfn4dGcCjRte397WqfQXA==",
709+ "cpu": [
710+ "arm64"
711+ ],
712+ "dev": true,
713+ "license": "MIT",
714+ "optional": true,
715+ "os": [
716+ "win32"
717+ ],
718+ "engines": {
719+ "node": ">=18"
720+ }
721+ },
722+ "node_modules/@esbuild/win32-ia32": {
723+ "version": "0.28.1",
724+ "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.28.1.tgz",
725+ "integrity": "sha512-zvb/mB2bSCoJOpoCBgYKKpX6YM6mJBlBUVUtVj41DlZJVEB6/0CKlRYxP5wWl1C1ILiCoAU5wZZ4q1P3qeS6Eg==",
726+ "cpu": [
727+ "ia32"
728+ ],
729+ "dev": true,
730+ "license": "MIT",
731+ "optional": true,
732+ "os": [
733+ "win32"
734+ ],
735+ "engines": {
736+ "node": ">=18"
737+ }
738+ },
739+ "node_modules/@esbuild/win32-x64": {
740+ "version": "0.28.1",
741+ "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.28.1.tgz",
742+ "integrity": "sha512-bm4Mowrv+GXMlpWX++EcXw/iLyd1o3+bJkC2DkWXYVvgZCqD/bSj9ctZeAMC3cIxgjRVR2Dufaiu4YPxr5gW1A==",
743+ "cpu": [
744+ "x64"
745+ ],
746+ "dev": true,
747+ "license": "MIT",
748+ "optional": true,
749+ "os": [
750+ "win32"
751+ ],
752+ "engines": {
753+ "node": ">=18"
754+ }
755+ },
756+ "node_modules/@jridgewell/gen-mapping": {
757+ "version": "0.3.13",
758+ "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz",
759+ "integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==",
760+ "dev": true,
761+ "license": "MIT",
762+ "dependencies": {
763+ "@jridgewell/sourcemap-codec": "^1.5.0",
764+ "@jridgewell/trace-mapping": "^0.3.24"
765+ }
766+ },
767+ "node_modules/@jridgewell/remapping": {
768+ "version": "2.3.5",
769+ "resolved": "https://registry.npmjs.org/@jridgewell/remapping/-/remapping-2.3.5.tgz",
770+ "integrity": "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==",
771+ "dev": true,
772+ "license": "MIT",
773+ "dependencies": {
774+ "@jridgewell/gen-mapping": "^0.3.5",
775+ "@jridgewell/trace-mapping": "^0.3.24"
776+ }
777+ },
778+ "node_modules/@jridgewell/resolve-uri": {
779+ "version": "3.1.2",
780+ "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz",
781+ "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==",
782+ "dev": true,
783+ "license": "MIT",
784+ "engines": {
785+ "node": ">=6.0.0"
786+ }
787+ },
788+ "node_modules/@jridgewell/sourcemap-codec": {
789+ "version": "1.5.5",
790+ "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz",
791+ "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==",
792+ "dev": true,
793+ "license": "MIT"
794+ },
795+ "node_modules/@jridgewell/trace-mapping": {
796+ "version": "0.3.31",
797+ "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz",
798+ "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==",
799+ "dev": true,
800+ "license": "MIT",
801+ "dependencies": {
802+ "@jridgewell/resolve-uri": "^3.1.0",
803+ "@jridgewell/sourcemap-codec": "^1.4.14"
804+ }
805+ },
806+ "node_modules/@rolldown/pluginutils": {
807+ "version": "1.0.0-rc.3",
808+ "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-rc.3.tgz",
809+ "integrity": "sha512-eybk3TjzzzV97Dlj5c+XrBFW57eTNhzod66y9HrBlzJ6NsCrWCp/2kaPS3K9wJmurBC0Tdw4yPjXKZqlznim3Q==",
810+ "dev": true,
811+ "license": "MIT"
812+ },
813+ "node_modules/@rollup/rollup-android-arm-eabi": {
814+ "version": "4.62.3",
815+ "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.62.3.tgz",
816+ "integrity": "sha512-c0wdcekXtQvvn5Tsrk/+op/gUArrbWaFduBnTLP2l1cKLSQs4diMWjJw3m6A0DdzT8dAAX95KpkJ3qynCePbmw==",
817+ "cpu": [
818+ "arm"
819+ ],
820+ "dev": true,
821+ "license": "MIT",
822+ "optional": true,
823+ "os": [
824+ "android"
825+ ]
826+ },
827+ "node_modules/@rollup/rollup-android-arm64": {
828+ "version": "4.62.3",
829+ "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.62.3.tgz",
830+ "integrity": "sha512-3YjElDdWN+qXAFbJ/CzPV+0wspLqh54k/I6GfdYtEJRqg7buSgc1yPM3B+93j1M4neobtkATHZTmxK2AMVGfnA==",
831+ "cpu": [
832+ "arm64"
833+ ],
834+ "dev": true,
835+ "license": "MIT",
836+ "optional": true,
837+ "os": [
838+ "android"
839+ ]
840+ },
841+ "node_modules/@rollup/rollup-darwin-arm64": {
842+ "version": "4.62.3",
843+ "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.62.3.tgz",
844+ "integrity": "sha512-Pch2pFNOxxz1hTjypIdPyRTR6riiwRl84+VcN9djS680fw+Co1nAJINrdpqp7KV0NvyuU8ilZXZCjd7ykJl1GQ==",
845+ "cpu": [
846+ "arm64"
847+ ],
848+ "dev": true,
849+ "license": "MIT",
850+ "optional": true,
851+ "os": [
852+ "darwin"
853+ ]
854+ },
855+ "node_modules/@rollup/rollup-darwin-x64": {
856+ "version": "4.62.3",
857+ "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.62.3.tgz",
858+ "integrity": "sha512-LEuncFUHFiF8t4yZVZvvZA1wk0pjAscRnsrn1EfTEmN4HXotBi2YtcnLRyaK6UbuczW7xZS5ES+81Rdz8Z0T6g==",
859+ "cpu": [
860+ "x64"
861+ ],
862+ "dev": true,
863+ "license": "MIT",
864+ "optional": true,
865+ "os": [
866+ "darwin"
867+ ]
868+ },
869+ "node_modules/@rollup/rollup-freebsd-arm64": {
870+ "version": "4.62.3",
871+ "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.62.3.tgz",
872+ "integrity": "sha512-zvBUvsQUpOWALdDsk6qbS8bXf2VxmPisuudNDrY7x0p0jBdsoZl8HsHczIOgkQiZldmcacMKtBzpoGVNeIe2bQ==",
873+ "cpu": [
874+ "arm64"
875+ ],
876+ "dev": true,
877+ "license": "MIT",
878+ "optional": true,
879+ "os": [
880+ "freebsd"
881+ ]
882+ },
883+ "node_modules/@rollup/rollup-freebsd-x64": {
884+ "version": "4.62.3",
885+ "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.62.3.tgz",
886+ "integrity": "sha512-C2KmNrcSem/AMg984H/dev+si0lieQGdXdR/lYGJnuumXnFb9Y7QdiI62obFdLlxRYLBv4P0eUVIDbD4c1vVvw==",
887+ "cpu": [
888+ "x64"
889+ ],
890+ "dev": true,
891+ "license": "MIT",
892+ "optional": true,
893+ "os": [
894+ "freebsd"
895+ ]
896+ },
897+ "node_modules/@rollup/rollup-linux-arm-gnueabihf": {
898+ "version": "4.62.3",
899+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.62.3.tgz",
900+ "integrity": "sha512-ggXnsTAEzNQx74XpunRsiZ9aBZDsI7XIa0hm2nzR9f4WzH5/f/d73ZSDaC5ejJ8YLY4NW+V3wr0tjOaeCq8hqA==",
901+ "cpu": [
902+ "arm"
903+ ],
904+ "dev": true,
905+ "libc": [
906+ "glibc"
907+ ],
908+ "license": "MIT",
909+ "optional": true,
910+ "os": [
911+ "linux"
912+ ]
913+ },
914+ "node_modules/@rollup/rollup-linux-arm-musleabihf": {
915+ "version": "4.62.3",
916+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.62.3.tgz",
917+ "integrity": "sha512-2vng+FlzNUhKZxtej3IUqJgbZoQk2M/dwQM20+ULV0R/E/8tr9/P6uEf2iiGIk4HL0zMKh5Jry7mUHdUOvyGgA==",
918+ "cpu": [
919+ "arm"
920+ ],
921+ "dev": true,
922+ "libc": [
923+ "musl"
924+ ],
925+ "license": "MIT",
926+ "optional": true,
927+ "os": [
928+ "linux"
929+ ]
930+ },
931+ "node_modules/@rollup/rollup-linux-arm64-gnu": {
932+ "version": "4.62.3",
933+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.62.3.tgz",
934+ "integrity": "sha512-LLLFZKt4/Nraf9rxDkhiU8QVgLF4WmCkfr0L4fj0fPfIZFBib0DeiFk1hhaYKd03LFAFJcxHslhDFlNJLylf5Q==",
935+ "cpu": [
936+ "arm64"
937+ ],
938+ "dev": true,
939+ "libc": [
940+ "glibc"
941+ ],
942+ "license": "MIT",
943+ "optional": true,
944+ "os": [
945+ "linux"
946+ ]
947+ },
948+ "node_modules/@rollup/rollup-linux-arm64-musl": {
949+ "version": "4.62.3",
950+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.62.3.tgz",
951+ "integrity": "sha512-WJkdQCvS9sWNOUBJZfQRKpZGFBztRzcowI+nndmflKgU4XY+3a420FgTOSKTsVqJbnzSxeT4vaJalpOaPo2YCQ==",
952+ "cpu": [
953+ "arm64"
954+ ],
955+ "dev": true,
956+ "libc": [
957+ "musl"
958+ ],
959+ "license": "MIT",
960+ "optional": true,
961+ "os": [
962+ "linux"
963+ ]
964+ },
965+ "node_modules/@rollup/rollup-linux-loong64-gnu": {
966+ "version": "4.62.3",
967+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.62.3.tgz",
968+ "integrity": "sha512-PwHXCCS2n64/1Ot6rP1YEYA02MGYBcQlr8CSZZyrUG2O7NH6NklYmvr9v3Jy+5e/eDeNchc/ukmKJi9LuflMIQ==",
969+ "cpu": [
970+ "loong64"
971+ ],
972+ "dev": true,
973+ "libc": [
974+ "glibc"
975+ ],
976+ "license": "MIT",
977+ "optional": true,
978+ "os": [
979+ "linux"
980+ ]
981+ },
982+ "node_modules/@rollup/rollup-linux-loong64-musl": {
983+ "version": "4.62.3",
984+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-musl/-/rollup-linux-loong64-musl-4.62.3.tgz",
985+ "integrity": "sha512-vUjxINQu3RC8NZS3ykk1gN65gIz8pAopOq2HXuZhiIxHdx7TFvDG+jgrdSgInu1Eza4/Rfi2VzZgyIgEH4WOaw==",
986+ "cpu": [
987+ "loong64"
988+ ],
989+ "dev": true,
990+ "libc": [
991+ "musl"
992+ ],
993+ "license": "MIT",
994+ "optional": true,
995+ "os": [
996+ "linux"
997+ ]
998+ },
999+ "node_modules/@rollup/rollup-linux-ppc64-gnu": {
1000+ "version": "4.62.3",
1001+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.62.3.tgz",
1002+ "integrity": "sha512-wzko4aJ13+0G3kGnviCg5gnXFKd40izKsrf2uOw12US4XqprkDrmwOpeW14aSNa37V8bfPcz5Fkob6LZ3BAPmA==",
1003+ "cpu": [
1004+ "ppc64"
1005+ ],
1006+ "dev": true,
1007+ "libc": [
1008+ "glibc"
1009+ ],
1010+ "license": "MIT",
1011+ "optional": true,
1012+ "os": [
1013+ "linux"
1014+ ]
1015+ },
1016+ "node_modules/@rollup/rollup-linux-ppc64-musl": {
1017+ "version": "4.62.3",
1018+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-musl/-/rollup-linux-ppc64-musl-4.62.3.tgz",
1019+ "integrity": "sha512-8120ue0JUMSwy11stlwnfdX3pPd+WZYGCDBwEHWtIHi6pOpZmsEF5QKB7a/UN+XFdqvobxz98kv8RTqikyCEBw==",
1020+ "cpu": [
1021+ "ppc64"
1022+ ],
1023+ "dev": true,
1024+ "libc": [
1025+ "musl"
1026+ ],
1027+ "license": "MIT",
1028+ "optional": true,
1029+ "os": [
1030+ "linux"
1031+ ]
1032+ },
1033+ "node_modules/@rollup/rollup-linux-riscv64-gnu": {
1034+ "version": "4.62.3",
1035+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.62.3.tgz",
1036+ "integrity": "sha512-XLFHnR3tXMjbOCh2vtVJHmxt+995uJsTERQyseFDRA0xxMxyTZPLa3OIUlyFaO4mF/Lu0FjmWHCuPXJT1n/IOg==",
1037+ "cpu": [
1038+ "riscv64"
1039+ ],
1040+ "dev": true,
1041+ "libc": [
1042+ "glibc"
1043+ ],
1044+ "license": "MIT",
1045+ "optional": true,
1046+ "os": [
1047+ "linux"
1048+ ]
1049+ },
1050+ "node_modules/@rollup/rollup-linux-riscv64-musl": {
1051+ "version": "4.62.3",
1052+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.62.3.tgz",
1053+ "integrity": "sha512-se6yXvNGMIl0f+RQzyh7XAmia8/9kplQx424wnG2w0C1oi6XgO6Y8otKhdXFHbHs88Ihavzmvh1NWjuovE76BQ==",
1054+ "cpu": [
1055+ "riscv64"
1056+ ],
1057+ "dev": true,
1058+ "libc": [
1059+ "musl"
1060+ ],
1061+ "license": "MIT",
1062+ "optional": true,
1063+ "os": [
1064+ "linux"
1065+ ]
1066+ },
1067+ "node_modules/@rollup/rollup-linux-s390x-gnu": {
1068+ "version": "4.62.3",
1069+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.62.3.tgz",
1070+ "integrity": "sha512-gNoxRefktVIiGflpONuxWWXZAzIQG++z9qHO3xKwk4WdDMuQja3JHGfE1u0i3PfPDyvhypdk+WrgIJqLhGG7sg==",
1071+ "cpu": [
1072+ "s390x"
1073+ ],
1074+ "dev": true,
1075+ "libc": [
1076+ "glibc"
1077+ ],
1078+ "license": "MIT",
1079+ "optional": true,
1080+ "os": [
1081+ "linux"
1082+ ]
1083+ },
1084+ "node_modules/@rollup/rollup-linux-x64-gnu": {
1085+ "version": "4.62.3",
1086+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.62.3.tgz",
1087+ "integrity": "sha512-V4KtWtQfAFMU7+9/A/VDps/VI8CHd3cYz0L8sgJzz8qK7eY7wI4ruFD82UYIYvW9Z4DtlTfhQcsl4XyPHW5uSg==",
1088+ "cpu": [
1089+ "x64"
1090+ ],
1091+ "dev": true,
1092+ "libc": [
1093+ "glibc"
1094+ ],
1095+ "license": "MIT",
1096+ "optional": true,
1097+ "os": [
1098+ "linux"
1099+ ]
1100+ },
1101+ "node_modules/@rollup/rollup-linux-x64-musl": {
1102+ "version": "4.62.3",
1103+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.62.3.tgz",
1104+ "integrity": "sha512-LBx9LYXvj2CBkMkjLdNAWLwH0MLMin7do2VcVo9kVPibGLkY0BQQut2fv7NVqkXqZ/CrAu9LqDHVV1xHCMpCPw==",
1105+ "cpu": [
1106+ "x64"
1107+ ],
1108+ "dev": true,
1109+ "libc": [
1110+ "musl"
1111+ ],
1112+ "license": "MIT",
1113+ "optional": true,
1114+ "os": [
1115+ "linux"
1116+ ]
1117+ },
1118+ "node_modules/@rollup/rollup-openbsd-x64": {
1119+ "version": "4.62.3",
1120+ "resolved": "https://registry.npmjs.org/@rollup/rollup-openbsd-x64/-/rollup-openbsd-x64-4.62.3.tgz",
1121+ "integrity": "sha512-ABVf3Q0RCu7NcyCCOZQI0pJ3GuSdfSl8EXcy88QtdceIMIoCUdfhsJChZ64L9zVM2aJHjde1Bhn5uqSRcX9ySA==",
1122+ "cpu": [
1123+ "x64"
1124+ ],
1125+ "dev": true,
1126+ "license": "MIT",
1127+ "optional": true,
1128+ "os": [
1129+ "openbsd"
1130+ ]
1131+ },
1132+ "node_modules/@rollup/rollup-openharmony-arm64": {
1133+ "version": "4.62.3",
1134+ "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.62.3.tgz",
1135+ "integrity": "sha512-+2Cy/ldweGBLlPIKsQLF8U5N44a0KDdbrk1rAjHOM9M2K+kGdIVjHLmmrZIcx+9Ny3ke/1JomCsDI1ocb11+sg==",
1136+ "cpu": [
1137+ "arm64"
1138+ ],
1139+ "dev": true,
1140+ "license": "MIT",
1141+ "optional": true,
1142+ "os": [
1143+ "openharmony"
1144+ ]
1145+ },
1146+ "node_modules/@rollup/rollup-win32-arm64-msvc": {
1147+ "version": "4.62.3",
1148+ "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.62.3.tgz",
1149+ "integrity": "sha512-dtZvzc8BedpSaFNy75x6uiWwAGTH+aZHDtdrqP6qk+WcLJrfti6sGje1ZJ9UxyzDLF23d/mV+PaMwuC0hL7UVA==",
1150+ "cpu": [
1151+ "arm64"
1152+ ],
1153+ "dev": true,
1154+ "license": "MIT",
1155+ "optional": true,
1156+ "os": [
1157+ "win32"
1158+ ]
1159+ },
1160+ "node_modules/@rollup/rollup-win32-ia32-msvc": {
1161+ "version": "4.62.3",
1162+ "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.62.3.tgz",
1163+ "integrity": "sha512-Rj8Ra4noo+aYy7sKBggCx0407mws34kAb1ySyWuq5DAtFBQdkSwnsjCgPrhPe9cvgBKZIukpE+CVHvORCS93kQ==",
1164+ "cpu": [
1165+ "ia32"
1166+ ],
1167+ "dev": true,
1168+ "license": "MIT",
1169+ "optional": true,
1170+ "os": [
1171+ "win32"
1172+ ]
1173+ },
1174+ "node_modules/@rollup/rollup-win32-x64-gnu": {
1175+ "version": "4.62.3",
1176+ "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.62.3.tgz",
1177+ "integrity": "sha512-vp7N084ew/odXn2gi/mzm9mUkQu9l6AiN6dt4IeUM2Uvm9o+cVmP+YkqbMOteLbiGgqBBlJZjIMYVCfOOIVbVQ==",
1178+ "cpu": [
1179+ "x64"
1180+ ],
1181+ "dev": true,
1182+ "license": "MIT",
1183+ "optional": true,
1184+ "os": [
1185+ "win32"
1186+ ]
1187+ },
1188+ "node_modules/@rollup/rollup-win32-x64-msvc": {
1189+ "version": "4.62.3",
1190+ "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.62.3.tgz",
1191+ "integrity": "sha512-MOG/3gTOn4Fwf574RVOaY61I5o6P90legkFADiTyn1hyjNydT+cerU2rLUwPdZkKKyJ+iT+K9p7WXK4LM1Ka6g==",
1192+ "cpu": [
1193+ "x64"
1194+ ],
1195+ "dev": true,
1196+ "license": "MIT",
1197+ "optional": true,
1198+ "os": [
1199+ "win32"
1200+ ]
1201+ },
1202+ "node_modules/@types/babel__core": {
1203+ "version": "7.20.5",
1204+ "resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.5.tgz",
1205+ "integrity": "sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA==",
1206+ "dev": true,
1207+ "license": "MIT",
1208+ "dependencies": {
1209+ "@babel/parser": "^7.20.7",
1210+ "@babel/types": "^7.20.7",
1211+ "@types/babel__generator": "*",
1212+ "@types/babel__template": "*",
1213+ "@types/babel__traverse": "*"
1214+ }
1215+ },
1216+ "node_modules/@types/babel__generator": {
1217+ "version": "7.27.0",
1218+ "resolved": "https://registry.npmjs.org/@types/babel__generator/-/babel__generator-7.27.0.tgz",
1219+ "integrity": "sha512-ufFd2Xi92OAVPYsy+P4n7/U7e68fex0+Ee8gSG9KX7eo084CWiQ4sdxktvdl0bOPupXtVJPY19zk6EwWqUQ8lg==",
1220+ "dev": true,
1221+ "license": "MIT",
1222+ "dependencies": {
1223+ "@babel/types": "^7.0.0"
1224+ }
1225+ },
1226+ "node_modules/@types/babel__template": {
1227+ "version": "7.4.4",
1228+ "resolved": "https://registry.npmjs.org/@types/babel__template/-/babel__template-7.4.4.tgz",
1229+ "integrity": "sha512-h/NUaSyG5EyxBIp8YRxo4RMe2/qQgvyowRwVMzhYhBCONbW8PUsg4lkFMrhgZhUe5z3L3MiLDuvyJ/CaPa2A8A==",
1230+ "dev": true,
1231+ "license": "MIT",
1232+ "dependencies": {
1233+ "@babel/parser": "^7.1.0",
1234+ "@babel/types": "^7.0.0"
1235+ }
1236+ },
1237+ "node_modules/@types/babel__traverse": {
1238+ "version": "7.28.0",
1239+ "resolved": "https://registry.npmjs.org/@types/babel__traverse/-/babel__traverse-7.28.0.tgz",
1240+ "integrity": "sha512-8PvcXf70gTDZBgt9ptxJ8elBeBjcLOAcOtoO/mPJjtji1+CdGbHgm77om1GrsPxsiE+uXIpNSK64UYaIwQXd4Q==",
1241+ "dev": true,
1242+ "license": "MIT",
1243+ "dependencies": {
1244+ "@babel/types": "^7.28.2"
1245+ }
1246+ },
1247+ "node_modules/@types/estree": {
1248+ "version": "1.0.9",
1249+ "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.9.tgz",
1250+ "integrity": "sha512-GhdPgy1el4/ImP05X05Uw4cw2/M93BCUmnEvWZNStlCzEKME4Fkk+YpoA5OiHNQmoS7Cafb8Xa3Pya8m1Qrzeg==",
1251+ "dev": true,
1252+ "license": "MIT"
1253+ },
1254+ "node_modules/@types/node": {
1255+ "version": "26.1.2",
1256+ "resolved": "https://registry.npmjs.org/@types/node/-/node-26.1.2.tgz",
1257+ "integrity": "sha512-Vu4a5UFA9rIIFJ7rB/Vaafh9lrCQszopTCx6KjFboXTGQbPNasehVR5TEiithSDGyd1DEiUByggTZsg8jukeIg==",
1258+ "dev": true,
1259+ "license": "MIT",
1260+ "dependencies": {
1261+ "undici-types": "~8.3.0"
1262+ }
1263+ },
1264+ "node_modules/@types/react": {
1265+ "version": "19.2.17",
1266+ "resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.17.tgz",
1267+ "integrity": "sha512-MXfmqaVPEVgkBT/aY0aGCkRWWtByiYQXo3xdQ8r5RzuFrPiRn8Gar2tQdXSUQ2GKV3bkXckek89V8wQBY2Q/Aw==",
1268+ "dev": true,
1269+ "license": "MIT",
1270+ "dependencies": {
1271+ "csstype": "^3.2.2"
1272+ }
1273+ },
1274+ "node_modules/@types/react-dom": {
1275+ "version": "19.2.3",
1276+ "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-19.2.3.tgz",
1277+ "integrity": "sha512-jp2L/eY6fn+KgVVQAOqYItbF0VY/YApe5Mz2F0aykSO8gx31bYCZyvSeYxCHKvzHG5eZjc+zyaS5BrBWya2+kQ==",
1278+ "dev": true,
1279+ "license": "MIT",
1280+ "peerDependencies": {
1281+ "@types/react": "^19.2.0"
1282+ }
1283+ },
1284+ "node_modules/@vitejs/plugin-react": {
1285+ "version": "5.2.0",
1286+ "resolved": "https://registry.npmjs.org/@vitejs/plugin-react/-/plugin-react-5.2.0.tgz",
1287+ "integrity": "sha512-YmKkfhOAi3wsB1PhJq5Scj3GXMn3WvtQ/JC0xoopuHoXSdmtdStOpFrYaT1kie2YgFBcIe64ROzMYRjCrYOdYw==",
1288+ "dev": true,
1289+ "license": "MIT",
1290+ "dependencies": {
1291+ "@babel/core": "^7.29.0",
1292+ "@babel/plugin-transform-react-jsx-self": "^7.27.1",
1293+ "@babel/plugin-transform-react-jsx-source": "^7.27.1",
1294+ "@rolldown/pluginutils": "1.0.0-rc.3",
1295+ "@types/babel__core": "^7.20.5",
1296+ "react-refresh": "^0.18.0"
1297+ },
1298+ "engines": {
1299+ "node": "^20.19.0 || >=22.12.0"
1300+ },
1301+ "peerDependencies": {
1302+ "vite": "^4.2.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0"
1303+ }
1304+ },
1305+ "node_modules/baseline-browser-mapping": {
1306+ "version": "2.11.6",
1307+ "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.11.6.tgz",
1308+ "integrity": "sha512-69D/imtToCsIcAl8WBS2YaRwA4jO/j0HhU+hELqMEu9f54MoUtI6+XH5mrKU8rEFNEk/Ui1I2MK4/JkWacclGw==",
1309+ "dev": true,
1310+ "license": "Apache-2.0",
1311+ "bin": {
1312+ "baseline-browser-mapping": "dist/cli.cjs"
1313+ },
1314+ "engines": {
1315+ "node": ">=6.0.0"
1316+ }
1317+ },
1318+ "node_modules/browserslist": {
1319+ "version": "4.28.7",
1320+ "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.28.7.tgz",
1321+ "integrity": "sha512-JxV13hNrFxqjOc8alRbq9dK1MM79NEXYpma2B2J4wAtpWS5zIEIKqWPGCl7N4o7Uc7B7itylh7SuDujATRyyTw==",
1322+ "dev": true,
1323+ "funding": [
1324+ {
1325+ "type": "opencollective",
1326+ "url": "https://opencollective.com/browserslist"
1327+ },
1328+ {
1329+ "type": "tidelift",
1330+ "url": "https://tidelift.com/funding/github/npm/browserslist"
1331+ },
1332+ {
1333+ "type": "github",
1334+ "url": "https://github.com/sponsors/ai"
1335+ }
1336+ ],
1337+ "license": "MIT",
1338+ "dependencies": {
1339+ "baseline-browser-mapping": "^2.10.44",
1340+ "caniuse-lite": "^1.0.30001806",
1341+ "electron-to-chromium": "^1.5.393",
1342+ "node-releases": "^2.0.51",
1343+ "update-browserslist-db": "^1.2.3"
1344+ },
1345+ "bin": {
1346+ "browserslist": "cli.js"
1347+ },
1348+ "engines": {
1349+ "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7"
1350+ }
1351+ },
1352+ "node_modules/caniuse-lite": {
1353+ "version": "1.0.30001806",
1354+ "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001806.tgz",
1355+ "integrity": "sha512-72Cuvd95zbSYPKq6Fhg8eDJRlzgWDf7/mtoZv6Qe/DYNCEBdNxoA3+rZAU2ZhGCpZlns3EssFavaZomckT5Uuw==",
1356+ "dev": true,
1357+ "funding": [
1358+ {
1359+ "type": "opencollective",
1360+ "url": "https://opencollective.com/browserslist"
1361+ },
1362+ {
1363+ "type": "tidelift",
1364+ "url": "https://tidelift.com/funding/github/npm/caniuse-lite"
1365+ },
1366+ {
1367+ "type": "github",
1368+ "url": "https://github.com/sponsors/ai"
1369+ }
1370+ ],
1371+ "license": "CC-BY-4.0"
1372+ },
1373+ "node_modules/commander": {
1374+ "version": "8.3.0",
1375+ "resolved": "https://registry.npmjs.org/commander/-/commander-8.3.0.tgz",
1376+ "integrity": "sha512-OkTL9umf+He2DZkUq8f8J9of7yL6RJKI24dVITBmNfZBmri9zYZQrKkuXiKhyfPSu8tUhnVBB1iKXevvnlR4Ww==",
1377+ "license": "MIT",
1378+ "engines": {
1379+ "node": ">= 12"
1380+ }
1381+ },
1382+ "node_modules/convert-source-map": {
1383+ "version": "2.0.0",
1384+ "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz",
1385+ "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==",
1386+ "dev": true,
1387+ "license": "MIT"
1388+ },
1389+ "node_modules/csstype": {
1390+ "version": "3.2.3",
1391+ "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz",
1392+ "integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==",
1393+ "dev": true,
1394+ "license": "MIT"
1395+ },
1396+ "node_modules/debug": {
1397+ "version": "4.4.3",
1398+ "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz",
1399+ "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==",
1400+ "dev": true,
1401+ "license": "MIT",
1402+ "dependencies": {
1403+ "ms": "^2.1.3"
1404+ },
1405+ "engines": {
1406+ "node": ">=6.0"
1407+ },
1408+ "peerDependenciesMeta": {
1409+ "supports-color": {
1410+ "optional": true
1411+ }
1412+ }
1413+ },
1414+ "node_modules/electron-to-chromium": {
1415+ "version": "1.5.398",
1416+ "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.398.tgz",
1417+ "integrity": "sha512-AsvhAxopJGh6museTDMIjn6JpDYOfgu4RLlygomt87MUwBUqTfd/1EiPtx10/LZE8xpTvkP2E9Gafq7lkLtodQ==",
1418+ "dev": true,
1419+ "license": "ISC"
1420+ },
1421+ "node_modules/esbuild": {
1422+ "version": "0.28.1",
1423+ "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.28.1.tgz",
1424+ "integrity": "sha512-HrJrvZv5ayxBzPfwphOoNzkzOIIlifzk0KJrGK2c8R4+LKpMtpYLQeUdjnwjWv/LZlkH2laZk+4w78pi99D4Vw==",
1425+ "dev": true,
1426+ "hasInstallScript": true,
1427+ "license": "MIT",
1428+ "bin": {
1429+ "esbuild": "bin/esbuild"
1430+ },
1431+ "engines": {
1432+ "node": ">=18"
1433+ },
1434+ "optionalDependencies": {
1435+ "@esbuild/aix-ppc64": "0.28.1",
1436+ "@esbuild/android-arm": "0.28.1",
1437+ "@esbuild/android-arm64": "0.28.1",
1438+ "@esbuild/android-x64": "0.28.1",
1439+ "@esbuild/darwin-arm64": "0.28.1",
1440+ "@esbuild/darwin-x64": "0.28.1",
1441+ "@esbuild/freebsd-arm64": "0.28.1",
1442+ "@esbuild/freebsd-x64": "0.28.1",
1443+ "@esbuild/linux-arm": "0.28.1",
1444+ "@esbuild/linux-arm64": "0.28.1",
1445+ "@esbuild/linux-ia32": "0.28.1",
1446+ "@esbuild/linux-loong64": "0.28.1",
1447+ "@esbuild/linux-mips64el": "0.28.1",
1448+ "@esbuild/linux-ppc64": "0.28.1",
1449+ "@esbuild/linux-riscv64": "0.28.1",
1450+ "@esbuild/linux-s390x": "0.28.1",
1451+ "@esbuild/linux-x64": "0.28.1",
1452+ "@esbuild/netbsd-arm64": "0.28.1",
1453+ "@esbuild/netbsd-x64": "0.28.1",
1454+ "@esbuild/openbsd-arm64": "0.28.1",
1455+ "@esbuild/openbsd-x64": "0.28.1",
1456+ "@esbuild/openharmony-arm64": "0.28.1",
1457+ "@esbuild/sunos-x64": "0.28.1",
1458+ "@esbuild/win32-arm64": "0.28.1",
1459+ "@esbuild/win32-ia32": "0.28.1",
1460+ "@esbuild/win32-x64": "0.28.1"
1461+ }
1462+ },
1463+ "node_modules/escalade": {
1464+ "version": "3.2.0",
1465+ "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz",
1466+ "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==",
1467+ "dev": true,
1468+ "license": "MIT",
1469+ "engines": {
1470+ "node": ">=6"
1471+ }
1472+ },
1473+ "node_modules/fdir": {
1474+ "version": "6.5.0",
1475+ "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz",
1476+ "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==",
1477+ "dev": true,
1478+ "license": "MIT",
1479+ "engines": {
1480+ "node": ">=12.0.0"
1481+ },
1482+ "peerDependencies": {
1483+ "picomatch": "^3 || ^4"
1484+ },
1485+ "peerDependenciesMeta": {
1486+ "picomatch": {
1487+ "optional": true
1488+ }
1489+ }
1490+ },
1491+ "node_modules/fflate": {
1492+ "version": "0.8.3",
1493+ "resolved": "https://registry.npmjs.org/fflate/-/fflate-0.8.3.tgz",
1494+ "integrity": "sha512-tbZNuJrLwGUp3zshBtdy4W+ORxZuIh8a5ilyIEQDC5rY1f3U20JMry0Ll3WBzU58EZKsEuJFXhb5gwv8CsPvgA==",
1495+ "license": "MIT"
1496+ },
1497+ "node_modules/fsevents": {
1498+ "version": "2.3.3",
1499+ "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz",
1500+ "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==",
1501+ "dev": true,
1502+ "hasInstallScript": true,
1503+ "license": "MIT",
1504+ "optional": true,
1505+ "os": [
1506+ "darwin"
1507+ ],
1508+ "engines": {
1509+ "node": "^8.16.0 || ^10.6.0 || >=11.0.0"
1510+ }
1511+ },
1512+ "node_modules/gensync": {
1513+ "version": "1.0.0-beta.2",
1514+ "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz",
1515+ "integrity": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==",
1516+ "dev": true,
1517+ "license": "MIT",
1518+ "engines": {
1519+ "node": ">=6.9.0"
1520+ }
1521+ },
1522+ "node_modules/js-tokens": {
1523+ "version": "4.0.0",
1524+ "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz",
1525+ "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==",
1526+ "dev": true,
1527+ "license": "MIT"
1528+ },
1529+ "node_modules/jsesc": {
1530+ "version": "3.1.0",
1531+ "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz",
1532+ "integrity": "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==",
1533+ "dev": true,
1534+ "license": "MIT",
1535+ "bin": {
1536+ "jsesc": "bin/jsesc"
1537+ },
1538+ "engines": {
1539+ "node": ">=6"
1540+ }
1541+ },
1542+ "node_modules/json5": {
1543+ "version": "2.2.3",
1544+ "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz",
1545+ "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==",
1546+ "dev": true,
1547+ "license": "MIT",
1548+ "bin": {
1549+ "json5": "lib/cli.js"
1550+ },
1551+ "engines": {
1552+ "node": ">=6"
1553+ }
1554+ },
1555+ "node_modules/katex": {
1556+ "version": "0.16.47",
1557+ "resolved": "https://registry.npmjs.org/katex/-/katex-0.16.47.tgz",
1558+ "integrity": "sha512-Eeo8Ys1doU1z+x8AZsPpQu+p/QcZBI5PeOo7QGQdy2x2m0MU/hYagBbGOmXwr5KVbEfVuWv9LpnQWeehogurjg==",
1559+ "funding": [
1560+ "https://opencollective.com/katex",
1561+ "https://github.com/sponsors/katex"
1562+ ],
1563+ "license": "MIT",
1564+ "dependencies": {
1565+ "commander": "^8.3.0"
1566+ },
1567+ "bin": {
1568+ "katex": "cli.js"
1569+ }
1570+ },
1571+ "node_modules/lru-cache": {
1572+ "version": "5.1.1",
1573+ "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz",
1574+ "integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==",
1575+ "dev": true,
1576+ "license": "ISC",
1577+ "dependencies": {
1578+ "yallist": "^3.0.2"
1579+ }
1580+ },
1581+ "node_modules/ms": {
1582+ "version": "2.1.3",
1583+ "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
1584+ "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",
1585+ "dev": true,
1586+ "license": "MIT"
1587+ },
1588+ "node_modules/nanoid": {
1589+ "version": "3.3.16",
1590+ "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.16.tgz",
1591+ "integrity": "sha512-bzlKTyNJ7+LdGIIwy8ijFpIqEQIvafahV7eYykJ8Cvh42EdJeODoJ6gUJXpQJvej1BddH8OqTXZNE/KfbWAu8Q==",
1592+ "dev": true,
1593+ "funding": [
1594+ {
1595+ "type": "github",
1596+ "url": "https://github.com/sponsors/ai"
1597+ }
1598+ ],
1599+ "license": "MIT",
1600+ "bin": {
1601+ "nanoid": "bin/nanoid.cjs"
1602+ },
1603+ "engines": {
1604+ "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1"
1605+ }
1606+ },
1607+ "node_modules/node-releases": {
1608+ "version": "2.0.51",
1609+ "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.51.tgz",
1610+ "integrity": "sha512-wRNIrw4DmVLKQlbgOMdkMx27Wrpzes2hh5Jtbi2bjPd+4wJstWIqP5A+lscnqbm0xxmT5Bpg8Lec5ItEBwx6BQ==",
1611+ "dev": true,
1612+ "license": "MIT",
1613+ "engines": {
1614+ "node": ">=18"
1615+ }
1616+ },
1617+ "node_modules/picocolors": {
1618+ "version": "1.1.1",
1619+ "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz",
1620+ "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==",
1621+ "dev": true,
1622+ "license": "ISC"
1623+ },
1624+ "node_modules/picomatch": {
1625+ "version": "4.0.5",
1626+ "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.5.tgz",
1627+ "integrity": "sha512-RvwwcruNjI1ncT5xRakeyS9Lf8lcItv34KD+aif+VH9kduAyfYBipGh12274xtenIPZ119/R9BdTBa8gAwSh0A==",
1628+ "dev": true,
1629+ "license": "MIT",
1630+ "engines": {
1631+ "node": ">=12"
1632+ },
1633+ "funding": {
1634+ "url": "https://github.com/sponsors/jonschlinkert"
1635+ }
1636+ },
1637+ "node_modules/postcss": {
1638+ "version": "8.5.25",
1639+ "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.25.tgz",
1640+ "integrity": "sha512-DTPx3RWSSnWyzLxQnlH0rJP+EW5ekl16ZU4/psbIhA0e53kJfdgaN5vKM+xP7yJtXVu+nfdVFmlgFDEKAe4Pyw==",
1641+ "dev": true,
1642+ "funding": [
1643+ {
1644+ "type": "opencollective",
1645+ "url": "https://opencollective.com/postcss/"
1646+ },
1647+ {
1648+ "type": "tidelift",
1649+ "url": "https://tidelift.com/funding/github/npm/postcss"
1650+ },
1651+ {
1652+ "type": "github",
1653+ "url": "https://github.com/sponsors/ai"
1654+ }
1655+ ],
1656+ "license": "MIT",
1657+ "dependencies": {
1658+ "nanoid": "^3.3.16",
1659+ "picocolors": "^1.1.1",
1660+ "source-map-js": "^1.2.1"
1661+ },
1662+ "engines": {
1663+ "node": "^10 || ^12 || >=14"
1664+ }
1665+ },
1666+ "node_modules/react": {
1667+ "version": "19.2.8",
1668+ "resolved": "https://registry.npmjs.org/react/-/react-19.2.8.tgz",
1669+ "integrity": "sha512-PWaYA1L/q9u2u7xYQi+Y3L3Yfnie7XyLeaJICV1MGD6LprsBxcAqGjYyr0eY3p+QdsA+x/Irkt4Qif8D63+Sbw==",
1670+ "license": "MIT",
1671+ "engines": {
1672+ "node": ">=0.10.0"
1673+ }
1674+ },
1675+ "node_modules/react-dom": {
1676+ "version": "19.2.8",
1677+ "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.2.8.tgz",
1678+ "integrity": "sha512-rVprimfGBG3DR+Tq0IQG2DT5PxKth1WIGDmj5yPmlzr4YBe7uyE+Du4oVqTDXZSHGGGXRtTJEGSSePyQCMBglQ==",
1679+ "license": "MIT",
1680+ "dependencies": {
1681+ "scheduler": "^0.27.0"
1682+ },
1683+ "peerDependencies": {
1684+ "react": "^19.2.8"
1685+ }
1686+ },
1687+ "node_modules/react-refresh": {
1688+ "version": "0.18.0",
1689+ "resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.18.0.tgz",
1690+ "integrity": "sha512-QgT5//D3jfjJb6Gsjxv0Slpj23ip+HtOpnNgnb2S5zU3CB26G/IDPGoy4RJB42wzFE46DRsstbW6tKHoKbhAxw==",
1691+ "dev": true,
1692+ "license": "MIT",
1693+ "engines": {
1694+ "node": ">=0.10.0"
1695+ }
1696+ },
1697+ "node_modules/rollup": {
1698+ "version": "4.62.3",
1699+ "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.62.3.tgz",
1700+ "integrity": "sha512-Gu0c0iH9FzgX1L1t7ByIbbS3Vmdz+6KHm/EsqmmC71gUQ82yvZRkTK6XzrFObSka91WUVdynqp6nsfilzr5k6Q==",
1701+ "dev": true,
1702+ "license": "MIT",
1703+ "dependencies": {
1704+ "@types/estree": "1.0.9"
1705+ },
1706+ "bin": {
1707+ "rollup": "dist/bin/rollup"
1708+ },
1709+ "engines": {
1710+ "node": ">=18.0.0",
1711+ "npm": ">=8.0.0"
1712+ },
1713+ "optionalDependencies": {
1714+ "@rollup/rollup-android-arm-eabi": "4.62.3",
1715+ "@rollup/rollup-android-arm64": "4.62.3",
1716+ "@rollup/rollup-darwin-arm64": "4.62.3",
1717+ "@rollup/rollup-darwin-x64": "4.62.3",
1718+ "@rollup/rollup-freebsd-arm64": "4.62.3",
1719+ "@rollup/rollup-freebsd-x64": "4.62.3",
1720+ "@rollup/rollup-linux-arm-gnueabihf": "4.62.3",
1721+ "@rollup/rollup-linux-arm-musleabihf": "4.62.3",
1722+ "@rollup/rollup-linux-arm64-gnu": "4.62.3",
1723+ "@rollup/rollup-linux-arm64-musl": "4.62.3",
1724+ "@rollup/rollup-linux-loong64-gnu": "4.62.3",
1725+ "@rollup/rollup-linux-loong64-musl": "4.62.3",
1726+ "@rollup/rollup-linux-ppc64-gnu": "4.62.3",
1727+ "@rollup/rollup-linux-ppc64-musl": "4.62.3",
1728+ "@rollup/rollup-linux-riscv64-gnu": "4.62.3",
1729+ "@rollup/rollup-linux-riscv64-musl": "4.62.3",
1730+ "@rollup/rollup-linux-s390x-gnu": "4.62.3",
1731+ "@rollup/rollup-linux-x64-gnu": "4.62.3",
1732+ "@rollup/rollup-linux-x64-musl": "4.62.3",
1733+ "@rollup/rollup-openbsd-x64": "4.62.3",
1734+ "@rollup/rollup-openharmony-arm64": "4.62.3",
1735+ "@rollup/rollup-win32-arm64-msvc": "4.62.3",
1736+ "@rollup/rollup-win32-ia32-msvc": "4.62.3",
1737+ "@rollup/rollup-win32-x64-gnu": "4.62.3",
1738+ "@rollup/rollup-win32-x64-msvc": "4.62.3",
1739+ "fsevents": "~2.3.2"
1740+ }
1741+ },
1742+ "node_modules/scheduler": {
1743+ "version": "0.27.0",
1744+ "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.27.0.tgz",
1745+ "integrity": "sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q==",
1746+ "license": "MIT"
1747+ },
1748+ "node_modules/semver": {
1749+ "version": "6.3.1",
1750+ "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz",
1751+ "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==",
1752+ "dev": true,
1753+ "license": "ISC",
1754+ "bin": {
1755+ "semver": "bin/semver.js"
1756+ }
1757+ },
1758+ "node_modules/source-map-js": {
1759+ "version": "1.2.1",
1760+ "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz",
1761+ "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==",
1762+ "dev": true,
1763+ "license": "BSD-3-Clause",
1764+ "engines": {
1765+ "node": ">=0.10.0"
1766+ }
1767+ },
1768+ "node_modules/tinyglobby": {
1769+ "version": "0.2.17",
1770+ "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.17.tgz",
1771+ "integrity": "sha512-wXR/dYpcqKmfWpEdZjiKJOwCNFndD0DMnrW/cYjVGttEkBfVgcLFHoNrlj47mjOVic9yyNu65alsgF4NQyTa2g==",
1772+ "dev": true,
1773+ "license": "MIT",
1774+ "dependencies": {
1775+ "fdir": "^6.5.0",
1776+ "picomatch": "^4.0.4"
1777+ },
1778+ "engines": {
1779+ "node": ">=12.0.0"
1780+ },
1781+ "funding": {
1782+ "url": "https://github.com/sponsors/SuperchupuDev"
1783+ }
1784+ },
1785+ "node_modules/typescript": {
1786+ "version": "5.9.3",
1787+ "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz",
1788+ "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==",
1789+ "dev": true,
1790+ "license": "Apache-2.0",
1791+ "bin": {
1792+ "tsc": "bin/tsc",
1793+ "tsserver": "bin/tsserver"
1794+ },
1795+ "engines": {
1796+ "node": ">=14.17"
1797+ }
1798+ },
1799+ "node_modules/undici-types": {
1800+ "version": "8.3.0",
1801+ "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-8.3.0.tgz",
1802+ "integrity": "sha512-j375ScV60dom+YkPFIfTLcOiPxkN/buHz5GobjLhixFuANaNs3C9l4GmrWqejgXWJ7BbJcFYpTEUkS1Ge8bpZQ==",
1803+ "dev": true,
1804+ "license": "MIT"
1805+ },
1806+ "node_modules/update-browserslist-db": {
1807+ "version": "1.2.3",
1808+ "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.2.3.tgz",
1809+ "integrity": "sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w==",
1810+ "dev": true,
1811+ "funding": [
1812+ {
1813+ "type": "opencollective",
1814+ "url": "https://opencollective.com/browserslist"
1815+ },
1816+ {
1817+ "type": "tidelift",
1818+ "url": "https://tidelift.com/funding/github/npm/browserslist"
1819+ },
1820+ {
1821+ "type": "github",
1822+ "url": "https://github.com/sponsors/ai"
1823+ }
1824+ ],
1825+ "license": "MIT",
1826+ "dependencies": {
1827+ "escalade": "^3.2.0",
1828+ "picocolors": "^1.1.1"
1829+ },
1830+ "bin": {
1831+ "update-browserslist-db": "cli.js"
1832+ },
1833+ "peerDependencies": {
1834+ "browserslist": ">= 4.21.0"
1835+ }
1836+ },
1837+ "node_modules/vite": {
1838+ "version": "7.3.6",
1839+ "resolved": "https://registry.npmjs.org/vite/-/vite-7.3.6.tgz",
1840+ "integrity": "sha512-4XP60spRGjSZFf1qYH+dJIkK2znL3zQfl9KkOV9MkkRR/3Dls0dxaBsQPTloEc5BLXWPL9vsOxopxyKoMmDueg==",
1841+ "dev": true,
1842+ "license": "MIT",
1843+ "dependencies": {
1844+ "esbuild": "^0.27.0 || ^0.28.0",
1845+ "fdir": "^6.5.0",
1846+ "picomatch": "^4.0.3",
1847+ "postcss": "^8.5.6",
1848+ "rollup": "^4.43.0",
1849+ "tinyglobby": "^0.2.15"
1850+ },
1851+ "bin": {
1852+ "vite": "bin/vite.js"
1853+ },
1854+ "engines": {
1855+ "node": "^20.19.0 || >=22.12.0"
1856+ },
1857+ "funding": {
1858+ "url": "https://github.com/vitejs/vite?sponsor=1"
1859+ },
1860+ "optionalDependencies": {
1861+ "fsevents": "~2.3.3"
1862+ },
1863+ "peerDependencies": {
1864+ "@types/node": "^20.19.0 || >=22.12.0",
1865+ "jiti": ">=1.21.0",
1866+ "less": "^4.0.0",
1867+ "lightningcss": "^1.21.0",
1868+ "sass": "^1.70.0",
1869+ "sass-embedded": "^1.70.0",
1870+ "stylus": ">=0.54.8",
1871+ "sugarss": "^5.0.0",
1872+ "terser": "^5.16.0",
1873+ "tsx": "^4.8.1",
1874+ "yaml": "^2.4.2"
1875+ },
1876+ "peerDependenciesMeta": {
1877+ "@types/node": {
1878+ "optional": true
1879+ },
1880+ "jiti": {
1881+ "optional": true
1882+ },
1883+ "less": {
1884+ "optional": true
1885+ },
1886+ "lightningcss": {
1887+ "optional": true
1888+ },
1889+ "sass": {
1890+ "optional": true
1891+ },
1892+ "sass-embedded": {
1893+ "optional": true
1894+ },
1895+ "stylus": {
1896+ "optional": true
1897+ },
1898+ "sugarss": {
1899+ "optional": true
1900+ },
1901+ "terser": {
1902+ "optional": true
1903+ },
1904+ "tsx": {
1905+ "optional": true
1906+ },
1907+ "yaml": {
1908+ "optional": true
1909+ }
1910+ }
1911+ },
1912+ "node_modules/yallist": {
1913+ "version": "3.1.1",
1914+ "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz",
1915+ "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==",
1916+ "dev": true,
1917+ "license": "ISC"
1918+ }
1919+ }
1920+}
package.jsonadded+27−0View file
@@ -0,0 +1,27 @@
1+{
2+ "name": "timeseries-compressibility",
3+ "private": true,
4+ "version": "0.0.0",
5+ "type": "module",
6+ "scripts": {
7+ "dev": "vite",
8+ "build": "tsc -b && vite build",
9+ "preview": "vite preview",
10+ "typecheck": "tsc -b"
11+ },
12+ "dependencies": {
13+ "@bokuweb/zstd-wasm": "^0.0.27",
14+ "fflate": "^0.8.3",
15+ "katex": "^0.16.25",
16+ "react": "^19.2.7",
17+ "react-dom": "^19.2.7"
18+ },
19+ "devDependencies": {
20+ "@types/node": "^26.1.1",
21+ "@types/react": "^19.2.17",
22+ "@types/react-dom": "^19.2.3",
23+ "@vitejs/plugin-react": "^5.1.1",
24+ "typescript": "~5.9.3",
25+ "vite": "^7.2.4"
26+ }
27+}
src/App.tsxadded+177−0View file
@@ -0,0 +1,177 @@
1+import { useEffect, useMemo, useRef, useState } from 'react'
2+import Controls from './components/Controls'
3+import FilterViz from './components/FilterViz'
4+import ScrollingView from './components/ScrollingView'
5+import CompressionChart from './components/CompressionChart'
6+import MathSection from './components/MathSection'
7+import { DEFAULT_SPEC, designKernel, kernelNorm } from './model/filters'
8+import { entropyRateBits } from './model/theory'
9+import type { CodecResult } from './compress/codecs'
10+import type { CompressRequest, CompressResponse } from './worker/compressWorker'
11+
12+const BLOCK_SIZE = 120000
13+const BLOCK_SEED = 20260729
14+
15+interface CompressionState {
16+ results: CodecResult[]
17+ empiricalStd: number
18+ computing: boolean
19+ error: string | null
20+}
21+
22+/** The nine codec sizes, measured in a worker on a debounced parameter set. */
23+function useCompression(kernel: Float64Array, sigma: number, dither: boolean): CompressionState {
24+ const [state, setState] = useState<CompressionState>({
25+ results: [],
26+ empiricalStd: 0,
27+ computing: true,
28+ error: null,
29+ })
30+ const workerRef = useRef<Worker | null>(null)
31+ const idRef = useRef(0)
32+
33+ useEffect(() => {
34+ const worker = new Worker(new URL('./worker/compressWorker.ts', import.meta.url), {
35+ type: 'module',
36+ })
37+ worker.onmessage = (e: MessageEvent<CompressResponse>) => {
38+ if (e.data.id !== idRef.current) return
39+ setState({
40+ results: e.data.error ? [] : e.data.results,
41+ empiricalStd: e.data.empiricalStd,
42+ computing: false,
43+ error: e.data.error ?? null,
44+ })
45+ }
46+ workerRef.current = worker
47+ return () => {
48+ worker.terminate()
49+ workerRef.current = null
50+ }
51+ }, [])
52+
53+ useEffect(() => {
54+ setState(s => ({ ...s, computing: true }))
55+ const id = ++idRef.current
56+ const timer = setTimeout(() => {
57+ const request: CompressRequest = {
58+ id,
59+ kernel,
60+ sigma,
61+ dither,
62+ blockSize: BLOCK_SIZE,
63+ seed: BLOCK_SEED,
64+ }
65+ workerRef.current?.postMessage(request)
66+ }, 250)
67+ return () => clearTimeout(timer)
68+ }, [kernel, sigma, dither])
69+
70+ return state
71+}
72+
73+export default function App() {
74+ const [sigma, setSigma] = useState(5)
75+ const [sampleRateHz, setSampleRateHz] = useState(30000)
76+ const [spec, setSpec] = useState(DEFAULT_SPEC)
77+ const [dither, setDither] = useState(false)
78+
79+ const kernel = useMemo(() => designKernel(spec, sampleRateHz), [spec, sampleRateHz])
80+ const sigmaY = useMemo(() => {
81+ const filtered = sigma * kernelNorm(kernel)
82+ return dither ? Math.sqrt(filtered * filtered + 1 / 12) : filtered
83+ }, [kernel, sigma, dither])
84+ const theoryBits = useMemo(() => entropyRateBits(kernel, sigma), [kernel, sigma])
85+ const compression = useCompression(kernel, sigma, dither)
86+
87+ return (
88+ <div className="app">
89+ <header className="app-header">
90+ <h1>Time-series compressibility</h1>
91+ <p>
92+ Gaussian noise → FIR filter → optional dither → round to integers. How well can the
93+ integer stream be losslessly compressed, and does the spectral entropy-rate formula
94+ predict the limit?
95+ </p>
96+ </header>
97+
98+ <section className="card">
99+ <h2>Model</h2>
100+ <Controls
101+ sigma={sigma}
102+ setSigma={setSigma}
103+ sampleRateHz={sampleRateHz}
104+ setSampleRateHz={setSampleRateHz}
105+ spec={spec}
106+ setSpec={setSpec}
107+ dither={dither}
108+ setDither={setDither}
109+ />
110+ </section>
111+
112+ <section className="card">
113+ <h2>Filter</h2>
114+ <FilterViz kernel={kernel} sampleRateHz={sampleRateHz} sigma={sigma} />
115+ </section>
116+
117+ <section className="card">
118+ <h2>Quantized signal z</h2>
119+ <ScrollingView kernel={kernel} sigma={sigma} dither={dither} sigmaY={sigmaY} />
120+ <p className="card-note">
121+ Continuously generated from the model; sample-and-hold rendering, so the integer
122+ staircase appears as σ approaches the quantization step.
123+ </p>
124+ </section>
125+
126+ <section className="card">
127+ <h2>Compression</h2>
128+ <div className="stat-row">
129+ <div className="stat">
130+ <span className="label">predicted std of z</span>
131+ <span className="value">
132+ {sigmaY.toFixed(2)} <small>steps</small>
133+ </span>
134+ </div>
135+ <div className="stat">
136+ <span className="label">measured std of z</span>
137+ <span className="value">
138+ {compression.results.length > 0 ? compression.empiricalStd.toFixed(2) : '…'}{' '}
139+ <small>steps</small>
140+ </span>
141+ </div>
142+ <div className="stat">
143+ <span className="label">entropy rate R (theory)</span>
144+ <span className="value">
145+ {theoryBits.toFixed(2)} <small>bits/sample</small>
146+ </span>
147+ </div>
148+ <div className="stat">
149+ <span className="label">implied best ratio</span>
150+ <span className="value">{theoryBits > 0 ? `${(16 / theoryBits).toFixed(2)}×` : '—'}</span>
151+ </div>
152+ </div>
153+ {compression.error ? (
154+ <p className="card-note">Compression failed: {compression.error}</p>
155+ ) : (
156+ <CompressionChart
157+ results={compression.results}
158+ theoryBits={theoryBits}
159+ computing={compression.computing}
160+ />
161+ )}
162+ <p className="card-note">
163+ Measured on a {BLOCK_SIZE.toLocaleString()}-sample block from the same model; sizes
164+ include everything a decoder needs (ANS symbol table, LPC coefficients). Baseline is
165+ raw int16 (16 bits/sample). The dashed line is the high-resolution entropy rate R — it
166+ ignores dither and is unreliable where S(f) falls below one step² (see the response
167+ plot).
168+ </p>
169+ </section>
170+
171+ <section className="card">
172+ <h2>The math</h2>
173+ <MathSection />
174+ </section>
175+ </div>
176+ )
177+}
src/app.cssadded+381−0View file
@@ -0,0 +1,381 @@
1+/* Theme roles from the reference dataviz palette; dark mode is its own
2+ selected set of steps, not an automatic flip. */
3+:root {
4+ color-scheme: light;
5+ --page: #f9f9f7;
6+ --surface: #fcfcfb;
7+ --ink: #0b0b0b;
8+ --ink-2: #52514e;
9+ --muted: #898781;
10+ --grid: #e1e0d9;
11+ --baseline: #c3c2b7;
12+ --border: rgba(11, 11, 11, 0.1);
13+ --series-1: #2a78d6;
14+ --series-2: #eb6834;
15+ --series-3: #1baf7a;
16+}
17+@media (prefers-color-scheme: dark) {
18+ :root {
19+ color-scheme: dark;
20+ --page: #0d0d0d;
21+ --surface: #1a1a19;
22+ --ink: #ffffff;
23+ --ink-2: #c3c2b7;
24+ --muted: #898781;
25+ --grid: #2c2c2a;
26+ --baseline: #383835;
27+ --border: rgba(255, 255, 255, 0.1);
28+ --series-1: #3987e5;
29+ --series-2: #d95926;
30+ --series-3: #199e70;
31+ }
32+}
33+
34+* {
35+ box-sizing: border-box;
36+}
37+
38+body {
39+ margin: 0;
40+ background: var(--page);
41+ color: var(--ink);
42+ font-family: system-ui, -apple-system, 'Segoe UI', sans-serif;
43+ font-size: 14px;
44+ line-height: 1.5;
45+}
46+
47+.app {
48+ max-width: 1080px;
49+ margin: 0 auto;
50+ padding: 24px 20px 64px;
51+ display: flex;
52+ flex-direction: column;
53+ gap: 16px;
54+}
55+
56+.app-header h1 {
57+ font-size: 22px;
58+ font-weight: 650;
59+ margin: 0;
60+}
61+
62+.app-header p {
63+ margin: 4px 0 0;
64+ color: var(--ink-2);
65+ max-width: 72ch;
66+}
67+
68+.card {
69+ background: var(--surface);
70+ border: 1px solid var(--border);
71+ border-radius: 10px;
72+ padding: 16px 20px;
73+}
74+
75+.card > h2 {
76+ font-size: 13px;
77+ font-weight: 650;
78+ text-transform: uppercase;
79+ letter-spacing: 0.05em;
80+ color: var(--ink-2);
81+ margin: 0 0 12px;
82+}
83+
84+.card-note {
85+ color: var(--muted);
86+ font-size: 12px;
87+ margin: 10px 0 0;
88+}
89+
90+/* ---- controls ---- */
91+
92+.controls {
93+ display: flex;
94+ flex-wrap: wrap;
95+ gap: 16px 28px;
96+ align-items: flex-end;
97+}
98+
99+.control {
100+ display: flex;
101+ flex-direction: column;
102+ gap: 4px;
103+ min-width: 150px;
104+}
105+
106+.control > label {
107+ font-size: 12px;
108+ color: var(--ink-2);
109+}
110+
111+.control .value {
112+ color: var(--ink);
113+ font-weight: 600;
114+}
115+
116+.control input[type='range'] {
117+ accent-color: var(--series-1);
118+ width: 170px;
119+ margin: 0;
120+}
121+
122+.control select,
123+.control input[type='number'] {
124+ background: var(--surface);
125+ color: var(--ink);
126+ border: 1px solid var(--baseline);
127+ border-radius: 6px;
128+ padding: 4px 8px;
129+ font: inherit;
130+ width: 170px;
131+}
132+
133+.control-toggle {
134+ flex-direction: row;
135+ align-items: center;
136+ gap: 8px;
137+ min-width: 0;
138+ padding-bottom: 6px;
139+}
140+
141+.control-toggle input {
142+ accent-color: var(--series-1);
143+ width: 16px;
144+ height: 16px;
145+ margin: 0;
146+}
147+
148+.control-toggle label {
149+ font-size: 13px;
150+ color: var(--ink);
151+}
152+
153+/* ---- plots ---- */
154+
155+.filter-panels {
156+ display: grid;
157+ grid-template-columns: 1fr 1fr;
158+ gap: 20px;
159+}
160+
161+@media (max-width: 760px) {
162+ .filter-panels {
163+ grid-template-columns: 1fr;
164+ }
165+}
166+
167+.panel h3 {
168+ font-size: 12px;
169+ font-weight: 600;
170+ color: var(--ink-2);
171+ margin: 0 0 6px;
172+}
173+
174+.panel {
175+ position: relative;
176+}
177+
178+.panel svg {
179+ display: block;
180+ width: 100%;
181+ height: auto;
182+}
183+
184+.scroll-canvas {
185+ display: block;
186+ width: 100%;
187+ height: 240px;
188+}
189+
190+.viz-tooltip {
191+ position: absolute;
192+ pointer-events: none;
193+ background: var(--surface);
194+ border: 1px solid var(--border);
195+ border-radius: 6px;
196+ padding: 4px 8px;
197+ font-size: 12px;
198+ box-shadow: 0 2px 8px rgba(0, 0, 0, 0.15);
199+ white-space: nowrap;
200+ z-index: 10;
201+}
202+
203+.viz-tooltip .tip-value {
204+ font-weight: 650;
205+ color: var(--ink);
206+}
207+
208+.viz-tooltip .tip-label {
209+ color: var(--ink-2);
210+}
211+
212+/* ---- stat tiles ---- */
213+
214+.stat-row {
215+ display: flex;
216+ flex-wrap: wrap;
217+ gap: 12px 32px;
218+ margin-bottom: 14px;
219+}
220+
221+.stat {
222+ display: flex;
223+ flex-direction: column;
224+}
225+
226+.stat .label {
227+ font-size: 12px;
228+ color: var(--ink-2);
229+}
230+
231+.stat .value {
232+ font-size: 20px;
233+ font-weight: 600;
234+}
235+
236+.stat .value small {
237+ font-size: 12px;
238+ font-weight: 400;
239+ color: var(--muted);
240+}
241+
242+/* ---- compression chart ---- */
243+
244+.chart-header {
245+ display: flex;
246+ align-items: center;
247+ justify-content: space-between;
248+ gap: 12px;
249+ flex-wrap: wrap;
250+ margin-bottom: 6px;
251+}
252+
253+.metric-hint {
254+ margin-left: 10px;
255+ font-size: 12px;
256+ color: var(--muted);
257+}
258+
259+.segmented {
260+ display: inline-flex;
261+ border: 1px solid var(--baseline);
262+ border-radius: 7px;
263+ overflow: hidden;
264+}
265+
266+.segmented button {
267+ border: none;
268+ background: transparent;
269+ color: var(--ink-2);
270+ font: inherit;
271+ font-size: 12px;
272+ padding: 4px 10px;
273+ cursor: pointer;
274+}
275+
276+.segmented button.active {
277+ background: var(--series-1);
278+ color: #fff;
279+}
280+
281+.legend {
282+ display: flex;
283+ gap: 14px;
284+ font-size: 12px;
285+ color: var(--ink-2);
286+}
287+
288+.legend .swatch {
289+ display: inline-block;
290+ width: 10px;
291+ height: 10px;
292+ border-radius: 2px;
293+ margin-right: 5px;
294+ vertical-align: -1px;
295+}
296+
297+.chart-body {
298+ position: relative;
299+ transition: opacity 0.15s;
300+}
301+
302+.chart-body.computing {
303+ opacity: 0.55;
304+}
305+
306+.computing-badge {
307+ position: absolute;
308+ top: -2px;
309+ right: 0;
310+ font-size: 12px;
311+ color: var(--muted);
312+}
313+
314+.bar-row-label {
315+ fill: var(--ink);
316+ font-size: 12px;
317+}
318+
319+.bar-group-label {
320+ fill: var(--muted);
321+ font-size: 11px;
322+ text-transform: uppercase;
323+ letter-spacing: 0.06em;
324+}
325+
326+.bar-value {
327+ fill: var(--ink-2);
328+ font-size: 11.5px;
329+ font-variant-numeric: tabular-nums;
330+}
331+
332+.axis-tick {
333+ fill: var(--muted);
334+ font-size: 11px;
335+ font-variant-numeric: tabular-nums;
336+}
337+
338+.chart-table {
339+ margin-top: 10px;
340+ color: var(--ink-2);
341+ font-size: 12.5px;
342+}
343+
344+.chart-table summary {
345+ cursor: pointer;
346+ color: var(--muted);
347+}
348+
349+.chart-table table {
350+ margin-top: 8px;
351+ border-collapse: collapse;
352+ font-variant-numeric: tabular-nums;
353+}
354+
355+.chart-table th,
356+.chart-table td {
357+ text-align: right;
358+ padding: 3px 14px 3px 0;
359+ border-bottom: 1px solid var(--grid);
360+}
361+
362+.chart-table th:first-child,
363+.chart-table td:first-child {
364+ text-align: left;
365+}
366+
367+.chart-table th {
368+ color: var(--muted);
369+ font-weight: 500;
370+}
371+
372+/* ---- math ---- */
373+
374+.math-section p {
375+ color: var(--ink-2);
376+ max-width: 76ch;
377+}
378+
379+.math-section .katex-display {
380+ margin: 12px 0;
381+}
src/components/CompressionChart.tsxadded+221−0View file
@@ -0,0 +1,221 @@
1+import { useRef, useState } from 'react'
2+import type { CodecResult } from '../compress/codecs'
3+import { useWidth } from './useWidth'
4+
5+const GROUPS = ['no prefilter', 'delta', `LPC`]
6+const CODER_NAMES = ['zlib', 'zstd', 'ANS']
7+const CODER_VARS = ['var(--series-1)', 'var(--series-2)', 'var(--series-3)']
8+
9+const LABEL_W = 96
10+const RIGHT_PAD = 64
11+const AXIS_H = 26
12+const GROUP_H = 22
13+const ROW_H = 26
14+const BAR_H = 16
15+
16+type Metric = 'bits' | 'ratio'
17+
18+interface Tip {
19+ x: number
20+ y: number
21+ result: CodecResult
22+}
23+
24+function axisTicks(max: number): number[] {
25+ const step = max > 24 ? 8 : max > 12 ? 4 : max > 6 ? 2 : max > 3 ? 1 : 0.5
26+ const out: number[] = []
27+ for (let v = 0; v <= max + 1e-9; v += step) out.push(v)
28+ return out
29+}
30+
31+/** A bar whose data-end is rounded (4px) while the baseline end stays square. */
32+function barPath(x0: number, y: number, len: number, h: number): string {
33+ const r = Math.min(4, len)
34+ return `M${x0},${y} h${len - r} a${r},${r} 0 0 1 ${r},${r} v${h - 2 * r} a${r},${r} 0 0 1 ${-r},${r} h${-(len - r)} z`
35+}
36+
37+export default function CompressionChart(props: {
38+ results: CodecResult[]
39+ theoryBits: number
40+ computing: boolean
41+}) {
42+ const ref = useRef<HTMLDivElement>(null)
43+ const width = useWidth(ref, 720)
44+ const [metric, setMetric] = useState<Metric>('bits')
45+ const [tip, setTip] = useState<Tip | null>(null)
46+ const [hovered, setHovered] = useState<number | null>(null)
47+
48+ const { results, theoryBits } = props
49+ if (results.length === 0) {
50+ return <p className="card-note">Computing compression on the first block…</p>
51+ }
52+
53+ const value = (r: CodecResult) => (metric === 'bits' ? r.bitsPerSample : r.ratio)
54+ const theoryValue = metric === 'bits' ? theoryBits : theoryBits > 0 ? 16 / theoryBits : NaN
55+ const theoryVisible = Number.isFinite(theoryValue) && theoryValue > 0
56+ const xMax =
57+ metric === 'bits'
58+ ? Math.max(16, ...results.map(value), theoryVisible ? theoryValue : 0) * 1.02
59+ : Math.max(...results.map(value), theoryVisible ? theoryValue : 0) * 1.1
60+
61+ const plotW = width - LABEL_W - RIGHT_PAD
62+ const height = AXIS_H + GROUPS.length * (GROUP_H + 3 * ROW_H) + 6
63+ const xOf = (v: number) => LABEL_W + (v / xMax) * plotW
64+ const rowY = (i: number) => AXIS_H + Math.floor(i / 3) * (GROUP_H + 3 * ROW_H) + GROUP_H + (i % 3) * ROW_H
65+
66+ const fmt = (r: CodecResult) =>
67+ metric === 'bits' ? r.bitsPerSample.toFixed(2) : `${r.ratio.toFixed(2)}×`
68+
69+ const onBarMove = (e: React.PointerEvent, r: CodecResult) => {
70+ const box = ref.current!.getBoundingClientRect()
71+ setTip({ x: e.clientX - box.left, y: e.clientY - box.top, result: r })
72+ }
73+
74+ const theoryX = theoryVisible ? xOf(theoryValue) : 0
75+ const theoryLabel =
76+ metric === 'bits' ? `entropy rate R = ${theoryBits.toFixed(2)}` : `R ⇒ ${(16 / theoryBits).toFixed(2)}×`
77+
78+ return (
79+ <div>
80+ <div className="chart-header">
81+ <div>
82+ <div className="segmented" role="group" aria-label="metric">
83+ <button className={metric === 'bits' ? 'active' : ''} onClick={() => setMetric('bits')}>
84+ bits / sample
85+ </button>
86+ <button className={metric === 'ratio' ? 'active' : ''} onClick={() => setMetric('ratio')}>
87+ compression ratio
88+ </button>
89+ </div>
90+ <span className="metric-hint">
91+ {metric === 'bits' ? 'lower is better' : 'vs int16 — higher is better'}
92+ </span>
93+ </div>
94+ <div className="legend">
95+ {CODER_NAMES.map((name, i) => (
96+ <span key={name}>
97+ <span className="swatch" style={{ background: CODER_VARS[i] }} />
98+ {name}
99+ </span>
100+ ))}
101+ </div>
102+ </div>
103+ <div className={`chart-body${props.computing ? ' computing' : ''}`} ref={ref}>
104+ {props.computing && <span className="computing-badge">computing…</span>}
105+ <svg width={width} height={height}>
106+ {axisTicks(xMax).map(v => (
107+ <g key={v}>
108+ <line x1={xOf(v)} x2={xOf(v)} y1={AXIS_H - 6} y2={height - 4} stroke="var(--grid)" strokeWidth={1} />
109+ <text x={xOf(v)} y={AXIS_H - 10} textAnchor="middle" className="axis-tick">
110+ {+v.toFixed(1)}
111+ </text>
112+ </g>
113+ ))}
114+ <line x1={xOf(0)} x2={xOf(0)} y1={AXIS_H - 6} y2={height - 4} stroke="var(--baseline)" strokeWidth={1} />
115+ {GROUPS.map((g, gi) => (
116+ <text key={g} x={0} y={AXIS_H + gi * (GROUP_H + 3 * ROW_H) + 15} className="bar-group-label">
117+ {g}
118+ </text>
119+ ))}
120+ {results.map((r, i) => {
121+ const y = rowY(i)
122+ const len = Math.max(1, (value(r) / xMax) * plotW)
123+ const label = fmt(r)
124+ return (
125+ <g key={r.codec} opacity={hovered === null || hovered === i ? 1 : 0.45}>
126+ <text x={8} y={y + BAR_H / 2 + 4} className="bar-row-label">
127+ {CODER_NAMES[i % 3]}
128+ </text>
129+ <path d={barPath(xOf(0), y, len, BAR_H)} fill={CODER_VARS[i % 3]} />
130+ <text x={xOf(0) + len + 6} y={y + BAR_H / 2 + 4} className="bar-value">
131+ {label}
132+ </text>
133+ <rect
134+ x={0}
135+ y={y - (ROW_H - BAR_H) / 2}
136+ width={width}
137+ height={ROW_H}
138+ fill="transparent"
139+ onPointerMove={e => {
140+ setHovered(i)
141+ onBarMove(e, r)
142+ }}
143+ onPointerLeave={() => {
144+ setHovered(null)
145+ setTip(null)
146+ }}
147+ />
148+ </g>
149+ )
150+ })}
151+ {theoryVisible && (
152+ <g>
153+ <line
154+ x1={theoryX}
155+ x2={theoryX}
156+ y1={AXIS_H - 2}
157+ y2={height - 4}
158+ stroke="var(--ink-2)"
159+ strokeWidth={1.5}
160+ strokeDasharray="5 4"
161+ />
162+ <text
163+ x={theoryX + (theoryX > width - 150 ? -6 : 6)}
164+ y={AXIS_H + 10}
165+ textAnchor={theoryX > width - 150 ? 'end' : 'start'}
166+ className="bar-value"
167+ fill="var(--ink)"
168+ >
169+ {theoryLabel}
170+ </text>
171+ </g>
172+ )}
173+ </svg>
174+ {tip && (
175+ <div className="viz-tooltip" style={{ left: tip.x + 14, top: tip.y - 8 }}>
176+ <div>
177+ <span className="tip-value">
178+ {tip.result.bitsPerSample.toFixed(3)} bits/sample · {tip.result.ratio.toFixed(2)}×
179+ </span>{' '}
180+ <span className="tip-label">{tip.result.codec}</span>
181+ </div>
182+ <div className="tip-label">
183+ {tip.result.bytes.toLocaleString()} bytes · {tip.result.note}
184+ </div>
185+ </div>
186+ )}
187+ </div>
188+ <details className="chart-table">
189+ <summary>Table view</summary>
190+ <table>
191+ <thead>
192+ <tr>
193+ <th>method</th>
194+ <th>bytes</th>
195+ <th>bits/sample</th>
196+ <th>ratio vs int16</th>
197+ </tr>
198+ </thead>
199+ <tbody>
200+ {results.map(r => (
201+ <tr key={r.codec}>
202+ <td>{r.codec}</td>
203+ <td>{r.bytes.toLocaleString()}</td>
204+ <td>{r.bitsPerSample.toFixed(3)}</td>
205+ <td>{r.ratio.toFixed(3)}</td>
206+ </tr>
207+ ))}
208+ {theoryBits > 0 && (
209+ <tr>
210+ <td>theory: entropy rate R</td>
211+ <td>—</td>
212+ <td>{theoryBits.toFixed(3)}</td>
213+ <td>{(16 / theoryBits).toFixed(3)}</td>
214+ </tr>
215+ )}
216+ </tbody>
217+ </table>
218+ </details>
219+ </div>
220+ )
221+}
src/components/Controls.tsxadded+143−0View file
@@ -0,0 +1,143 @@
1+import { FAMILY_LABELS, type FilterFamily, type FilterSpec } from '../model/filters'
2+
3+export interface ControlsProps {
4+ sigma: number
5+ setSigma: (v: number) => void
6+ sampleRateHz: number
7+ setSampleRateHz: (v: number) => void
8+ spec: FilterSpec
9+ setSpec: (v: FilterSpec) => void
10+ dither: boolean
11+ setDither: (v: boolean) => void
12+}
13+
14+function formatHz(hz: number): string {
15+ return hz >= 1000 ? `${(hz / 1000).toFixed(hz >= 10000 ? 0 : 1)} kHz` : `${Math.round(hz)} Hz`
16+}
17+
18+/** A slider that is linear in log10 of its value. */
19+function LogSlider(props: {
20+ label: string
21+ value: number
22+ min: number
23+ max: number
24+ display: string
25+ onChange: (v: number) => void
26+}) {
27+ const lo = Math.log10(props.min)
28+ const hi = Math.log10(props.max)
29+ return (
30+ <div className="control">
31+ <label>
32+ {props.label} <span className="value">{props.display}</span>
33+ </label>
34+ <input
35+ type="range"
36+ min={lo}
37+ max={hi}
38+ step={(hi - lo) / 400}
39+ value={Math.log10(props.value)}
40+ onChange={e => props.onChange(10 ** Number(e.target.value))}
41+ />
42+ </div>
43+ )
44+}
45+
46+export default function Controls(p: ControlsProps) {
47+ const { spec } = p
48+ const nyquist = p.sampleRateHz / 2
49+ const sinc = spec.family === 'lowpass' || spec.family === 'bandpass'
50+ return (
51+ <div className="controls">
52+ <LogSlider
53+ label="σ (quantization steps)"
54+ value={p.sigma}
55+ min={0.1}
56+ max={100}
57+ display={p.sigma.toPrecision(3)}
58+ onChange={v => p.setSigma(Number(v.toPrecision(3)))}
59+ />
60+ <div className="control">
61+ <label>filter</label>
62+ <select
63+ value={spec.family}
64+ onChange={e => p.setSpec({ ...spec, family: e.target.value as FilterFamily })}
65+ >
66+ {(Object.keys(FAMILY_LABELS) as FilterFamily[]).map(f => (
67+ <option key={f} value={f}>
68+ {FAMILY_LABELS[f]}
69+ </option>
70+ ))}
71+ </select>
72+ </div>
73+ {spec.family === 'bandpass' && (
74+ <LogSlider
75+ label="low cutoff"
76+ value={spec.lowHz}
77+ min={10}
78+ max={nyquist * 0.9}
79+ display={formatHz(spec.lowHz)}
80+ onChange={v => p.setSpec({ ...spec, lowHz: Math.round(v) })}
81+ />
82+ )}
83+ {sinc && (
84+ <LogSlider
85+ label={spec.family === 'bandpass' ? 'high cutoff' : 'cutoff'}
86+ value={spec.highHz}
87+ min={20}
88+ max={nyquist * 0.98}
89+ display={formatHz(spec.highHz)}
90+ onChange={v => p.setSpec({ ...spec, highHz: Math.round(v) })}
91+ />
92+ )}
93+ {sinc && (
94+ <div className="control">
95+ <label>
96+ kernel length <span className="value">{spec.taps} taps</span>
97+ </label>
98+ <input
99+ type="range"
100+ min={15}
101+ max={201}
102+ step={2}
103+ value={spec.taps}
104+ onChange={e => p.setSpec({ ...spec, taps: Number(e.target.value) })}
105+ />
106+ </div>
107+ )}
108+ {spec.family === 'movingAverage' && (
109+ <div className="control">
110+ <label>
111+ width <span className="value">{spec.width} samples</span>
112+ </label>
113+ <input
114+ type="range"
115+ min={2}
116+ max={64}
117+ value={spec.width}
118+ onChange={e => p.setSpec({ ...spec, width: Number(e.target.value) })}
119+ />
120+ </div>
121+ )}
122+ <div className="control">
123+ <label>sample rate</label>
124+ <select value={p.sampleRateHz} onChange={e => p.setSampleRateHz(Number(e.target.value))}>
125+ {[1000, 8000, 16000, 30000, 44100, 96000].map(r => (
126+ <option key={r} value={r}>
127+ {formatHz(r)}
128+ </option>
129+ ))}
130+ </select>
131+ </div>
132+ <div className="control control-toggle">
133+ <input
134+ id="dither"
135+ type="checkbox"
136+ checked={p.dither}
137+ onChange={e => p.setDither(e.target.checked)}
138+ />
139+ <label htmlFor="dither">dither (uniform ±½ before rounding)</label>
140+ </div>
141+ </div>
142+ )
143+}
src/components/FilterViz.tsxadded+221−0View file
@@ -0,0 +1,221 @@
1+import { useMemo, useRef, useState } from 'react'
2+import { magnitudeResponse } from '../model/filters'
3+import { useWidth } from './useWidth'
4+
5+const MARGIN = { left: 44, right: 12, top: 10, bottom: 26 }
6+const HEIGHT = 190
7+const DB_FLOOR = -100
8+const RESPONSE_POINTS = 512
9+
10+interface Tip {
11+ x: number
12+ y: number
13+ value: string
14+ label: string
15+}
16+
17+function Tooltip({ tip }: { tip: Tip | null }) {
18+ if (!tip) return null
19+ return (
20+ <div className="viz-tooltip" style={{ left: tip.x + 12, top: tip.y - 10 }}>
21+ <span className="tip-value">{tip.value}</span> <span className="tip-label">{tip.label}</span>
22+ </div>
23+ )
24+}
25+
26+/** ~n round tick values covering [lo, hi]. */
27+function ticks(lo: number, hi: number, n: number): number[] {
28+ const span = hi - lo
29+ if (!(span > 0)) return [lo]
30+ const raw = span / n
31+ const mag = 10 ** Math.floor(Math.log10(raw))
32+ const step = [1, 2, 2.5, 5, 10].map(m => m * mag).find(s => span / s <= n) ?? raw
33+ const out: number[] = []
34+ for (let v = Math.ceil(lo / step) * step; v <= hi + 1e-9; v += step) out.push(v)
35+ return out
36+}
37+
38+function formatHz(hz: number): string {
39+ return hz >= 1000 ? `${+(hz / 1000).toFixed(1)}k` : `${Math.round(hz)}`
40+}
41+
42+/** The convolution kernel, tap value against time. */
43+function KernelPanel({ kernel, sampleRateHz }: { kernel: Float64Array; sampleRateHz: number }) {
44+ const ref = useRef<HTMLDivElement>(null)
45+ const width = useWidth(ref)
46+ const [tip, setTip] = useState<Tip | null>(null)
47+
48+ const L = kernel.length
49+ const mid = (L - 1) / 2
50+ const msAt = (i: number) => ((i - mid) / sampleRateHz) * 1000
51+ // A one-tap kernel still needs a nonzero span to draw on.
52+ const tSpan = L > 1 ? msAt(L - 1) - msAt(0) : 1000 / sampleRateHz
53+ const t0 = msAt(0) - (L === 1 ? tSpan / 2 : 0)
54+ const plotW = width - MARGIN.left - MARGIN.right
55+ const plotH = HEIGHT - MARGIN.top - MARGIN.bottom
56+ let yMin = 0
57+ let yMax = 0
58+ for (const v of kernel) {
59+ yMin = Math.min(yMin, v)
60+ yMax = Math.max(yMax, v)
61+ }
62+ const pad = (yMax - yMin || 1) * 0.1
63+ yMin -= pad
64+ yMax += pad
65+ const xOf = (i: number) => MARGIN.left + ((msAt(i) - t0) / tSpan) * plotW
66+ const yOf = (v: number) => MARGIN.top + ((yMax - v) / (yMax - yMin)) * plotH
67+
68+ const path = Array.from(kernel, (v, i) => `${i ? 'L' : 'M'}${xOf(i).toFixed(1)},${yOf(v).toFixed(1)}`).join('')
69+
70+ const onMove = (e: React.PointerEvent<SVGSVGElement>) => {
71+ const box = e.currentTarget.getBoundingClientRect()
72+ const px = e.clientX - box.left
73+ const i = L === 1 ? 0 : Math.max(0, Math.min(L - 1, Math.round(((px - MARGIN.left) / plotW) * (L - 1))))
74+ setTip({
75+ x: px,
76+ y: e.clientY - box.top,
77+ value: kernel[i].toPrecision(3),
78+ label: `tap ${i} · ${msAt(i).toFixed(2)} ms`,
79+ })
80+ }
81+
82+ return (
83+ <div className="panel" ref={ref}>
84+ <h3>Convolution kernel h</h3>
85+ <svg width={width} height={HEIGHT} onPointerMove={onMove} onPointerLeave={() => setTip(null)}>
86+ <line
87+ x1={MARGIN.left}
88+ x2={width - MARGIN.right}
89+ y1={yOf(0)}
90+ y2={yOf(0)}
91+ stroke="var(--baseline)"
92+ strokeWidth={1}
93+ />
94+ {ticks(t0, t0 + tSpan, 5)
95+ .filter(t => MARGIN.left + ((t - t0) / tSpan) * plotW < width - MARGIN.right - 34)
96+ .map(t => (
97+ <text key={t} x={MARGIN.left + ((t - t0) / tSpan) * plotW} y={HEIGHT - 8} textAnchor="middle" className="axis-tick">
98+ {+t.toFixed(2)}
99+ </text>
100+ ))}
101+ <text x={width - MARGIN.right} y={HEIGHT - 8} textAnchor="end" className="axis-tick">
102+ ms
103+ </text>
104+ {ticks(yMin, yMax, 3).map(v => (
105+ <text key={v} x={MARGIN.left - 6} y={yOf(v) + 4} textAnchor="end" className="axis-tick">
106+ {+v.toPrecision(2)}
107+ </text>
108+ ))}
109+ <path d={path} fill="none" stroke="var(--series-1)" strokeWidth={2} strokeLinejoin="round" strokeLinecap="round" />
110+ {L <= 31 &&
111+ Array.from(kernel, (v, i) => (
112+ <circle key={i} cx={xOf(i)} cy={yOf(v)} r={4} fill="var(--series-1)" stroke="var(--surface)" strokeWidth={2} />
113+ ))}
114+ </svg>
115+ <Tooltip tip={tip} />
116+ </div>
117+ )
118+}
119+
120+/** |H(f)| in dB up to Nyquist, with the S(f) = 1 step² threshold. */
121+function ResponsePanel({
122+ kernel,
123+ sampleRateHz,
124+ sigma,
125+}: {
126+ kernel: Float64Array
127+ sampleRateHz: number
128+ sigma: number
129+}) {
130+ const ref = useRef<HTMLDivElement>(null)
131+ const width = useWidth(ref)
132+ const [tip, setTip] = useState<Tip | null>(null)
133+
134+ const db = useMemo(() => {
135+ const mag = magnitudeResponse(kernel, RESPONSE_POINTS)
136+ return Array.from(mag, m => Math.max(DB_FLOOR, 20 * Math.log10(Math.max(m, 1e-12))))
137+ }, [kernel])
138+
139+ const nyquist = sampleRateHz / 2
140+ const plotW = width - MARGIN.left - MARGIN.right
141+ const plotH = HEIGHT - MARGIN.top - MARGIN.bottom
142+ const dbMax = Math.max(5, Math.ceil(Math.max(...db) / 5) * 5)
143+ const yOf = (v: number) => MARGIN.top + ((dbMax - v) / (dbMax - DB_FLOOR)) * plotH
144+ const xOf = (i: number) => MARGIN.left + (i / (RESPONSE_POINTS - 1)) * plotW
145+
146+ const path = db.map((v, i) => `${i ? 'L' : 'M'}${xOf(i).toFixed(1)},${yOf(v).toFixed(1)}`).join('')
147+
148+ // S(f) = σ²|H|² = 1 (one step² of spectral power) sits at |H| = 1/σ. Below
149+ // this line the high-resolution formula is on thin ice.
150+ const stepDb = -20 * Math.log10(sigma)
151+ const showStep = stepDb < dbMax && stepDb > DB_FLOOR
152+
153+ const onMove = (e: React.PointerEvent<SVGSVGElement>) => {
154+ const box = e.currentTarget.getBoundingClientRect()
155+ const px = e.clientX - box.left
156+ const i = Math.max(0, Math.min(RESPONSE_POINTS - 1, Math.round(((px - MARGIN.left) / plotW) * (RESPONSE_POINTS - 1))))
157+ setTip({
158+ x: px,
159+ y: e.clientY - box.top,
160+ value: `${db[i].toFixed(1)} dB`,
161+ label: `at ${formatHz((i / (RESPONSE_POINTS - 1)) * nyquist)} Hz`,
162+ })
163+ }
164+
165+ return (
166+ <div className="panel" ref={ref}>
167+ <h3>Frequency response |H(f)|</h3>
168+ <svg width={width} height={HEIGHT} onPointerMove={onMove} onPointerLeave={() => setTip(null)}>
169+ {[0, -20, -40, -60, -80, -100].map(v => (
170+ <g key={v}>
171+ <line x1={MARGIN.left} x2={width - MARGIN.right} y1={yOf(v)} y2={yOf(v)} stroke="var(--grid)" strokeWidth={1} />
172+ <text x={MARGIN.left - 6} y={yOf(v) + 4} textAnchor="end" className="axis-tick">
173+ {v}
174+ </text>
175+ </g>
176+ ))}
177+ <text x={MARGIN.left - 6} y={MARGIN.top - 1} textAnchor="end" className="axis-tick">
178+ dB
179+ </text>
180+ {ticks(0, nyquist, 5)
181+ // Leave the right corner to the unit label.
182+ .filter(f => MARGIN.left + (f / nyquist) * plotW < width - MARGIN.right - 34)
183+ .map(f => (
184+ <text key={f} x={MARGIN.left + (f / nyquist) * plotW} y={HEIGHT - 8} textAnchor="middle" className="axis-tick">
185+ {formatHz(f)}
186+ </text>
187+ ))}
188+ <text x={width - MARGIN.right} y={HEIGHT - 8} textAnchor="end" className="axis-tick">
189+ Hz
190+ </text>
191+ {showStep && (
192+ <g>
193+ <line
194+ x1={MARGIN.left}
195+ x2={width - MARGIN.right}
196+ y1={yOf(stepDb)}
197+ y2={yOf(stepDb)}
198+ stroke="var(--muted)"
199+ strokeWidth={1}
200+ strokeDasharray="4 3"
201+ />
202+ <text x={width - MARGIN.right} y={yOf(stepDb) - 4} textAnchor="end" className="axis-tick">
203+ S(f) = 1 step²
204+ </text>
205+ </g>
206+ )}
207+ <path d={path} fill="none" stroke="var(--series-1)" strokeWidth={2} strokeLinejoin="round" strokeLinecap="round" />
208+ </svg>
209+ <Tooltip tip={tip} />
210+ </div>
211+ )
212+}
213+
214+export default function FilterViz(props: { kernel: Float64Array; sampleRateHz: number; sigma: number }) {
215+ return (
216+ <div className="filter-panels">
217+ <KernelPanel kernel={props.kernel} sampleRateHz={props.sampleRateHz} />
218+ <ResponsePanel kernel={props.kernel} sampleRateHz={props.sampleRateHz} sigma={props.sigma} />
219+ </div>
220+ )
221+}
src/components/MathSection.tsxadded+38−0View file
@@ -0,0 +1,38 @@
1+import { useMemo } from 'react'
2+import katex from 'katex'
3+
4+function Display({ tex }: { tex: string }) {
5+ const html = useMemo(() => katex.renderToString(tex, { displayMode: true, throwOnError: false }), [tex])
6+ return <div dangerouslySetInnerHTML={{ __html: html }} />
7+}
8+
9+/**
10+ * Stub for the full derivation. It states the model and the formula the app
11+ * plots; the reasoning connecting them is to come.
12+ */
13+export default function MathSection() {
14+ return (
15+ <div className="math-section">
16+ <p>
17+ The generating model: i.i.d. Gaussian noise, a FIR filter h, optional uniform dither, and
18+ rounding to the integer quantization grid (the step is the unit, so σ is measured in
19+ steps):
20+ </p>
21+ <Display tex="x_n \sim \mathcal{N}(0,\sigma^2)\ \text{i.i.d.}, \qquad y = h * x, \qquad z_n = \operatorname{round}(y_n + d_n), \quad d_n \sim \mathcal{U}[-\tfrac12,\tfrac12)\ \text{or}\ 0" />
22+ <p>
23+ The dashed reference line is the entropy rate of the stationary Gaussian process y,
24+ quantized at unit step, in the fine-quantization (high-resolution) limit — the ideal
25+ lossless rate in bits per sample:
26+ </p>
27+ <Display tex="R \;=\; \tfrac12\log_2(2\pi e)\;+\;\int_0^{1/2} \log_2 S(f)\,df, \qquad S(f) = \sigma^2\,|H(f)|^2" />
28+ <p>
29+ with f in cycles per sample. With no filter this reduces to ½ log₂(2πe σ²). The formula
30+ holds when S(f) is well above one step² across the band; where the response dips toward or
31+ below the quantization step — deep stopbands, small σ — the true entropy rate is larger
32+ than R (and R can even go negative), and no fixed-order predictor fully whitens the
33+ process. Quantifying that gap, the effect of dither, and why LPC + ANS is the right
34+ yardstick is the subject of the full derivation, still to be written.
35+ </p>
36+ </div>
37+ )
38+}
src/components/ScrollingView.tsxadded+146−0View file
@@ -0,0 +1,146 @@
1+import { useEffect, useRef } from 'react'
2+import { Pipeline } from '../model/pipeline'
3+
4+/** Display samples generated per second; px per sample is fixed below. */
5+const RATE = 220
6+const PX_PER_SAMPLE = 2
7+const RING_SIZE = 8192
8+
9+/** A nice round gridline step ≤ span/2. */
10+function niceStep(span: number): number {
11+ const raw = span / 2
12+ const mag = 10 ** Math.floor(Math.log10(raw))
13+ for (const m of [5, 2.5, 2, 1]) if (m * mag <= raw) return m * mag
14+ return mag
15+}
16+
17+/**
18+ * The endlessly generated quantized signal z, drawn sample-and-hold so the
19+ * integer staircase is visible once σ is small. Rendering is a canvas ring
20+ * buffer fed by the same Pipeline the compression worker uses.
21+ */
22+export default function ScrollingView(props: {
23+ kernel: Float64Array
24+ sigma: number
25+ dither: boolean
26+ /** Predicted std of the quantized signal, for a stable y-scale. */
27+ sigmaY: number
28+}) {
29+ const canvasRef = useRef<HTMLCanvasElement>(null)
30+ const seedRef = useRef(1)
31+
32+ useEffect(() => {
33+ const canvas = canvasRef.current
34+ if (!canvas) return
35+ const ctx = canvas.getContext('2d')
36+ if (!ctx) return
37+
38+ const pipeline = new Pipeline(props.kernel, props.sigma, props.dither, seedRef.current++)
39+ const ring = new Float32Array(RING_SIZE)
40+ let head = 0
41+ let filled = 0
42+ const push = (samples: Int16Array) => {
43+ for (let i = 0; i < samples.length; i++) {
44+ ring[head] = samples[i]
45+ head = (head + 1) % RING_SIZE
46+ }
47+ filled = Math.min(RING_SIZE, filled + samples.length)
48+ }
49+
50+ // Start with a full screen of history so the view is never empty.
51+ push(pipeline.next(2048))
52+
53+ const scale = Math.max(4 * props.sigmaY, 3.5)
54+ const gridStep = niceStep(scale)
55+
56+ let styles = getComputedStyle(canvas)
57+ const scheme = window.matchMedia('(prefers-color-scheme: dark)')
58+ const refreshStyles = () => {
59+ styles = getComputedStyle(canvas)
60+ }
61+ scheme.addEventListener('change', refreshStyles)
62+
63+ let raf = 0
64+ let last = performance.now()
65+ let carry = 0
66+
67+ const draw = (now: number) => {
68+ raf = requestAnimationFrame(draw)
69+ const dt = Math.min(0.25, (now - last) / 1000)
70+ last = now
71+ carry += dt * RATE
72+ const n = Math.floor(carry)
73+ carry -= n
74+ if (n > 0) push(pipeline.next(n))
75+
76+ const dpr = window.devicePixelRatio || 1
77+ const w = canvas.clientWidth
78+ const h = canvas.clientHeight
79+ if (w === 0 || h === 0) return
80+ if (canvas.width !== Math.round(w * dpr) || canvas.height !== Math.round(h * dpr)) {
81+ canvas.width = Math.round(w * dpr)
82+ canvas.height = Math.round(h * dpr)
83+ }
84+ ctx.setTransform(dpr, 0, 0, dpr, 0, 0)
85+
86+ const surface = styles.getPropertyValue('--surface')
87+ ctx.fillStyle = surface
88+ ctx.fillRect(0, 0, w, h)
89+
90+ const yOf = (v: number) => h / 2 - (v / scale) * (h / 2 - 12)
91+
92+ ctx.strokeStyle = styles.getPropertyValue('--grid')
93+ ctx.lineWidth = 1
94+ ctx.fillStyle = styles.getPropertyValue('--muted')
95+ ctx.font = '11px system-ui, sans-serif'
96+ ctx.textAlign = 'left'
97+ for (let g = -2; g <= 2; g++) {
98+ const v = g * gridStep
99+ if (Math.abs(v) > scale) continue
100+ const y = Math.round(yOf(v)) + 0.5
101+ ctx.beginPath()
102+ ctx.moveTo(0, y)
103+ ctx.lineTo(w, y)
104+ if (g !== 0) ctx.stroke()
105+ // A surface-colored halo keeps the label readable over the trace.
106+ ctx.strokeStyle = surface
107+ ctx.lineWidth = 3
108+ const label = `${v > 0 ? '+' : ''}${+v.toPrecision(3)}`
109+ ctx.strokeText(label, 6, y - 4)
110+ ctx.fillText(label, 6, y - 4)
111+ ctx.strokeStyle = styles.getPropertyValue('--grid')
112+ ctx.lineWidth = 1
113+ }
114+ const zeroY = Math.round(yOf(0)) + 0.5
115+ ctx.strokeStyle = styles.getPropertyValue('--baseline')
116+ ctx.beginPath()
117+ ctx.moveTo(0, zeroY)
118+ ctx.lineTo(w, zeroY)
119+ ctx.stroke()
120+
121+ const visible = Math.min(filled, Math.floor(w / PX_PER_SAMPLE))
122+ ctx.strokeStyle = styles.getPropertyValue('--series-1')
123+ ctx.lineWidth = 2
124+ ctx.lineJoin = 'round'
125+ ctx.beginPath()
126+ for (let i = 0; i < visible; i++) {
127+ const idx = (head - visible + i + RING_SIZE) % RING_SIZE
128+ const x = w - (visible - i) * PX_PER_SAMPLE
129+ const y = yOf(ring[idx])
130+ // Sample-and-hold: horizontal run at each value, vertical jump between.
131+ if (i === 0) ctx.moveTo(x, y)
132+ else ctx.lineTo(x, y)
133+ ctx.lineTo(x + PX_PER_SAMPLE, y)
134+ }
135+ ctx.stroke()
136+ }
137+ raf = requestAnimationFrame(draw)
138+
139+ return () => {
140+ cancelAnimationFrame(raf)
141+ scheme.removeEventListener('change', refreshStyles)
142+ }
143+ }, [props.kernel, props.sigma, props.dither, props.sigmaY])
144+
145+ return <canvas ref={canvasRef} className="scroll-canvas" />
146+}
src/components/useWidth.tsadded+16−0View file
@@ -0,0 +1,16 @@
1+import { useLayoutEffect, useState, type RefObject } from 'react'
2+
3+/** The rendered width of a container, tracked through resizes. */
4+export function useWidth(ref: RefObject<HTMLElement | null>, fallback = 480): number {
5+ const [width, setWidth] = useState(fallback)
6+ useLayoutEffect(() => {
7+ const el = ref.current
8+ if (!el) return
9+ const update = () => setWidth(el.clientWidth || fallback)
10+ update()
11+ const ro = new ResizeObserver(update)
12+ ro.observe(el)
13+ return () => ro.disconnect()
14+ }, [ref, fallback])
15+ return width
16+}
src/compress/ans.tsadded+177−0View file
@@ -0,0 +1,177 @@
1+/**
2+ * Asymmetric Numeral Systems (rANS), ported from the pure-Python reference in
3+ * simple_ans (github.com/flatironinstitute/simple_ans,
4+ * `simple_ans/pure_python/py_encode_decode.py`).
5+ *
6+ * The port is deliberately faithful — same 64-bit state, same 32-bit
7+ * renormalisation word, same `choose_symbol_counts`, same automatic choice of
8+ * precision — so its output is byte-for-byte what simple_ans produces, and can
9+ * be checked against it.
10+ *
11+ * The 64-bit state does not fit a JS number, so it is carried as a BigInt.
12+ */
13+
14+const STATE_BITS = 64n
15+const WORD_BITS = 32n
16+const THRESHOLD = 1n << (STATE_BITS - WORD_BITS)
17+const MASK_WORD = (1n << WORD_BITS) - 1n
18+
19+export interface EncodedSignal {
20+ state: bigint
21+ words: Uint32Array
22+ symbolCounts: Uint32Array
23+ symbolValues: Int16Array
24+ signalLength: number
25+ /** Bits used for the quantised symbol distribution; L = 2^precision. */
26+ precision: number
27+}
28+
29+/**
30+ * Turn real-valued proportions into integer counts summing to L, each >= 1,
31+ * by largest remainder. Mirrors `choose_symbol_counts`.
32+ */
33+export function chooseSymbolCounts(proportions: Float64Array, L: number): Uint32Array {
34+ const k = proportions.length
35+ if (k > L) throw new Error('Number of proportions cannot exceed total items to distribute.')
36+
37+ let total = 0
38+ for (const p of proportions) total += p
39+
40+ const counts = new Uint32Array(k).fill(1)
41+ const remainder = L - k
42+ if (remainder > 0) {
43+ const frac = new Float64Array(k)
44+ let floorSum = 0
45+ for (let i = 0; i < k; i++) {
46+ const x = (remainder * proportions[i]) / total
47+ const f = Math.floor(x)
48+ counts[i] += f
49+ floorSum += f
50+ frac[i] = x - f
51+ }
52+ // Hand the leftover to the largest fractional parts.
53+ let leftover = remainder - floorSum
54+ if (leftover > 0) {
55+ const order = Array.from({ length: k }, (_, i) => i).sort((a, b) => frac[b] - frac[a])
56+ for (let i = 0; i < leftover; i++) counts[order[i]] += 1
57+ }
58+ }
59+ return counts
60+}
61+
62+/** Sorted distinct values of the signal, with their counts. */
63+function histogram(signal: Int16Array): { values: Int16Array; counts: Float64Array } {
64+ const map = new Map<number, number>()
65+ for (const v of signal) map.set(v, (map.get(v) ?? 0) + 1)
66+ const values = Int16Array.from([...map.keys()].sort((a, b) => a - b))
67+ const counts = new Float64Array(values.length)
68+ for (let i = 0; i < values.length; i++) counts[i] = map.get(values[i])!
69+ return { values, counts }
70+}
71+
72+function entropyBits(probs: Float64Array, against: Float64Array): number {
73+ let h = 0
74+ for (let i = 0; i < probs.length; i++) {
75+ if (probs[i] > 0) h -= probs[i] * Math.log2(against[i])
76+ }
77+ return h
78+}
79+
80+/**
81+ * Smallest precision whose quantised distribution costs no more than 1/0.98 of
82+ * the true entropy. Mirrors the `precision is None` branch of `py_ans_encode`.
83+ */
84+function choosePrecision(probs: Float64Array, symbolCount: number): number {
85+ const target = entropyBits(probs, probs)
86+ for (let precision = 2; precision < 24; precision++) {
87+ const L = 2 ** precision
88+ if (L < symbolCount) continue
89+ const counts = chooseSymbolCounts(probs, L)
90+ const quantised = Float64Array.from(counts, c => c / L)
91+ if (entropyBits(probs, quantised) <= target / 0.98 || L >= 2 ** 20) return precision
92+ }
93+ return 23
94+}
95+
96+export function ansEncode(signal: Int16Array, precisionOverride?: number): EncodedSignal {
97+ const { values, counts } = histogram(signal)
98+ const n = signal.length
99+ const probs = Float64Array.from(counts, c => c / n)
100+
101+ const precision = precisionOverride ?? choosePrecision(probs, values.length)
102+ const L = 2 ** precision
103+ if (values.length > L) {
104+ throw new Error(`${values.length} distinct symbols exceeds index size ${L}`)
105+ }
106+ const symbolCounts = chooseSymbolCounts(probs, L)
107+
108+ // Cumulative counts, and value -> symbol index.
109+ const cum = new Uint32Array(values.length)
110+ for (let i = 1; i < values.length; i++) cum[i] = cum[i - 1] + symbolCounts[i - 1]
111+ const index = new Map<number, number>()
112+ for (let i = 0; i < values.length; i++) index.set(values[i], i)
113+
114+ const precisionN = BigInt(precision)
115+ const shift = STATE_BITS - precisionN
116+ let state = 0n
117+ const words: number[] = []
118+
119+ for (let i = 0; i < n; i++) {
120+ const s = index.get(signal[i])!
121+ const F = BigInt(symbolCounts[s])
122+ // Renormalise: emit the low word so the state stays under 2^64.
123+ if (state >> shift >= F) {
124+ words.push(Number(state & MASK_WORD))
125+ state >>= WORD_BITS
126+ }
127+ state = ((state / F) << precisionN) | (BigInt(cum[s]) + (state % F))
128+ }
129+
130+ return {
131+ state,
132+ words: Uint32Array.from(words),
133+ symbolCounts,
134+ symbolValues: values,
135+ signalLength: n,
136+ precision,
137+ }
138+}
139+
140+export function ansDecode(e: EncodedSignal): Int16Array {
141+ const k = e.symbolCounts.length
142+ const cum = new Uint32Array(k)
143+ for (let i = 1; i < k; i++) cum[i] = cum[i - 1] + e.symbolCounts[i - 1]
144+
145+ const L = 2 ** e.precision
146+ // quantile -> symbol index, so the decoder does a lookup rather than a scan.
147+ const slot = new Uint32Array(L)
148+ for (let s = 0; s < k; s++) {
149+ for (let j = 0; j < e.symbolCounts[s]; j++) slot[cum[s] + j] = s
150+ }
151+
152+ const precisionN = BigInt(e.precision)
153+ const quantileMask = (1n << precisionN) - 1n
154+ const out = new Int16Array(e.signalLength)
155+ let state = e.state
156+ let stack = e.words.length - 1
157+
158+ for (let i = 0; i < e.signalLength; i++) {
159+ const quantile = Number(state & quantileMask)
160+ const s = slot[quantile]
161+ let previous = (state >> precisionN) * BigInt(e.symbolCounts[s]) + BigInt(quantile - cum[s])
162+ if (previous < THRESHOLD && stack >= 0) {
163+ previous = (previous << WORD_BITS) | BigInt(e.words[stack--])
164+ }
165+ state = previous
166+ out[e.signalLength - i - 1] = e.symbolValues[s]
167+ }
168+ return out
169+}
170+
171+/**
172+ * Bytes an encoded signal occupies: the state, the emitted words, and the
173+ * symbol table that the decoder needs. Matches `EncodedSignal.size()`.
174+ */
175+export function encodedSize(e: EncodedSignal): number {
176+ return 8 + e.words.byteLength + e.symbolCounts.byteLength + e.symbolValues.byteLength + 8
177+}
src/compress/codecs.tsadded+200−0View file
@@ -0,0 +1,200 @@
1+/**
2+ * Lossless codecs run in the browser, on int16 sample data.
3+ *
4+ * The baseline throughout is the uncompressed int16 stream: 2 bytes = 16 bits
5+ * per sample. A ratio is (baseline bytes) / (compressed bytes).
6+ */
7+import { zlibSync } from 'fflate'
8+import { init as zstdInit, compress as zstdCompress } from '@bokuweb/zstd-wasm'
9+import { ansEncode, ansDecode, encodedSize } from './ans'
10+import { fitLpc, lpcResidual, lpcRestore, modelSize } from './lpc'
11+// Bundled as an asset so the page needs no network at run time. The path is
12+// relative rather than a bare specifier because the package's `exports` map
13+// has no entry for the wasm file, so `@bokuweb/zstd-wasm/dist/web/zstd.wasm`
14+// cannot be resolved.
15+import zstdWasmUrl from '../../node_modules/@bokuweb/zstd-wasm/dist/web/zstd.wasm?url'
16+
17+export interface Codec {
18+ name: string
19+ /** Longer description, shown on hover. */
20+ note: string
21+ /** Compressed size in bytes, including any table the decoder needs. */
22+ size: (samples: Int16Array, bytes: Uint8Array) => number
23+}
24+
25+/**
26+ * First differences, in int16 with wraparound — exactly invertible, and the
27+ * standard prefilter for signals whose neighbouring samples are related.
28+ */
29+function delta(samples: Int16Array): Int16Array {
30+ const d = new Int16Array(samples.length)
31+ d[0] = samples[0]
32+ for (let i = 1; i < samples.length; i++) {
33+ d[i] = ((samples[i] - samples[i - 1]) << 16) >> 16
34+ }
35+ return d
36+}
37+
38+function undelta(d: Int16Array): Int16Array {
39+ const s = new Int16Array(d.length)
40+ s[0] = d[0]
41+ for (let i = 1; i < d.length; i++) {
42+ s[i] = ((s[i - 1] + d[i]) << 16) >> 16
43+ }
44+ return s
45+}
46+
47+function asBytes(samples: Int16Array): Uint8Array {
48+ return new Uint8Array(samples.buffer, samples.byteOffset, samples.byteLength)
49+}
50+
51+/**
52+ * Size of an ANS encoding of `coded`, after decoding it, running `inverse` to
53+ * undo whatever prefilter produced it, and checking the result against the
54+ * original samples — so a reported size always belongs to an encoding that
55+ * actually round-trips. `extraBytes` covers anything else the decoder needs,
56+ * such as predictor coefficients.
57+ */
58+function ansSize(
59+ samples: Int16Array,
60+ coded: Int16Array,
61+ inverse: (coded: Int16Array) => Int16Array,
62+ extraBytes = 0,
63+): number {
64+ const encoded = ansEncode(coded)
65+ const decoded = inverse(ansDecode(encoded))
66+ if (decoded.length !== samples.length) throw new Error('ANS round-trip length mismatch')
67+ for (let i = 0; i < samples.length; i++) {
68+ if (decoded[i] !== samples[i]) throw new Error(`ANS round-trip mismatch at ${i}`)
69+ }
70+ return encodedSize(encoded) + extraBytes
71+}
72+
73+/** Predictor order. Going past 32 buys well under a percent on this data. */
74+const LPC_ORDER = 32
75+
76+/** Fit the predictor and take the residual, with the coefficients' cost. */
77+function lpcTransform(samples: Int16Array): {
78+ residual: Int16Array
79+ restore: (residual: Int16Array) => Int16Array
80+ extraBytes: number
81+} {
82+ const model = fitLpc(samples, LPC_ORDER)
83+ if (!model) throw new Error('LPC fit failed')
84+ return {
85+ residual: lpcResidual(samples, model),
86+ restore: residual => lpcRestore(residual, model),
87+ extraBytes: modelSize(model),
88+ }
89+}
90+
91+export const ZLIB: Codec = {
92+ name: 'zlib -9',
93+ note: 'DEFLATE at maximum level — the HDF5 gzip filter',
94+ size: (_s, bytes) => zlibSync(bytes, { level: 9 }).length,
95+}
96+
97+export const ZSTD: Codec = {
98+ name: 'zstd -19',
99+ note: 'Zstandard at level 19 — the Blosc/Zarr default family',
100+ size: (_s, bytes) => zstdCompress(bytes, 19).length,
101+}
102+
103+export const ANS: Codec = {
104+ name: 'ANS',
105+ note: 'rANS entropy coder over the sample histogram, ported from simple_ans; the size includes the symbol table',
106+ size: samples => ansSize(samples, samples, x => x),
107+}
108+
109+export const DELTA_ZLIB: Codec = {
110+ name: 'delta + zlib -9',
111+ note: 'First differences, then DEFLATE',
112+ size: samples => zlibSync(asBytes(delta(samples)), { level: 9 }).length,
113+}
114+
115+export const DELTA_ZSTD: Codec = {
116+ name: 'delta + zstd -19',
117+ note: 'First differences, then Zstandard 19',
118+ size: samples => zstdCompress(asBytes(delta(samples)), 19).length,
119+}
120+
121+export const DELTA_ANS: Codec = {
122+ name: 'delta + ANS',
123+ note: 'First differences, then the rANS entropy coder',
124+ size: samples => ansSize(samples, delta(samples), undelta),
125+}
126+
127+const LPC_NOTE = `Order-${LPC_ORDER} linear prediction with integer coefficients; the size includes the coefficients`
128+
129+export const LPC_ZLIB: Codec = {
130+ name: `LPC(${LPC_ORDER}) + zlib -9`,
131+ note: `${LPC_NOTE}, then DEFLATE`,
132+ size: samples => {
133+ const { residual, extraBytes } = lpcTransform(samples)
134+ return zlibSync(asBytes(residual), { level: 9 }).length + extraBytes
135+ },
136+}
137+
138+export const LPC_ZSTD: Codec = {
139+ name: `LPC(${LPC_ORDER}) + zstd -19`,
140+ note: `${LPC_NOTE}, then Zstandard 19`,
141+ size: samples => {
142+ const { residual, extraBytes } = lpcTransform(samples)
143+ return zstdCompress(asBytes(residual), 19).length + extraBytes
144+ },
145+}
146+
147+export const LPC_ANS: Codec = {
148+ name: `LPC(${LPC_ORDER}) + ANS`,
149+ note: `${LPC_NOTE}, then the rANS entropy coder`,
150+ size: samples => {
151+ const { residual, restore, extraBytes } = lpcTransform(samples)
152+ return ansSize(samples, residual, restore, extraBytes)
153+ },
154+}
155+
156+/** The general-purpose compressors, which know nothing about the data. */
157+export const GENERAL_CODECS: Codec[] = [ZLIB, ZSTD]
158+
159+let ready: Promise<void> | null = null
160+
161+// zstd-wasm publishes the *node* build's types while the bundler resolves the
162+// browser build (its `exports` map has a "browser" condition). Only the browser
163+// build's `init` takes a wasm URL, so the signature has to be restated here.
164+const initWithUrl = zstdInit as unknown as (path?: string) => Promise<void>
165+
166+/** Load the zstd wasm module. Safe to call repeatedly. */
167+export function initCodecs(): Promise<void> {
168+ if (!ready) {
169+ ready = initWithUrl(zstdWasmUrl).catch((err: unknown) => {
170+ ready = null
171+ throw err
172+ })
173+ }
174+ return ready
175+}
176+
177+export interface CodecResult {
178+ codec: string
179+ note: string
180+ bytes: number
181+ ratio: number
182+ bitsPerSample: number
183+}
184+
185+/** Compress one int16 buffer with each of the given codecs. */
186+export function compressAll(buffer: Uint8Array, codecs: Codec[]): CodecResult[] {
187+ // The session's buffer may sit at an odd offset; align before viewing as int16.
188+ const aligned = buffer.byteOffset % 2 === 0 ? buffer : new Uint8Array(buffer)
189+ const samples = new Int16Array(aligned.buffer, aligned.byteOffset, aligned.byteLength / 2)
190+ return codecs.map(codec => {
191+ const bytes = codec.size(samples, aligned)
192+ return {
193+ codec: codec.name,
194+ note: codec.note,
195+ bytes,
196+ ratio: aligned.byteLength / bytes,
197+ bitsPerSample: (8 * bytes) / samples.length,
198+ }
199+ })
200+}
src/compress/lpc.tsadded+125−0View file
@@ -0,0 +1,125 @@
1+/**
2+ * Linear predictive coding, the generalisation of delta coding.
3+ *
4+ * Delta codes `x[n] - x[n-1]`. LPC fits an order-p predictor
5+ *
6+ * xhat[n] = sum_{k=1..p} a_k x[n-k]
7+ *
8+ * to the data and codes the residual `x[n] - xhat[n]` instead. Delta is the
9+ * special case p = 1, a_1 = 1.
10+ *
11+ * For the residual to be *losslessly* invertible the decoder has to reproduce
12+ * `xhat` bit-for-bit, so — as in FLAC and Shorten — the coefficients are
13+ * quantised to integers with a shared right shift and the prediction is done
14+ * in exact integer arithmetic:
15+ *
16+ * xhat[n] = floor( (sum_k q_k x[n-k]) / 2^shift )
17+ *
18+ * `sum_k q_k x[n-k]` reaches about 2^35 for the orders and widths used here,
19+ * which is exact in a double, so no BigInt is needed.
20+ */
21+
22+/** 15-bit signed coefficients, as FLAC's default. */
23+const COEFF_PRECISION = 15
24+
25+export interface LpcModel {
26+ order: number
27+ shift: number
28+ coeffs: Int32Array
29+}
30+
31+/** Autocorrelation r[0..maxLag] of the signal. */
32+function autocorrelation(x: Int16Array, maxLag: number): Float64Array {
33+ const r = new Float64Array(maxLag + 1)
34+ for (let lag = 0; lag <= maxLag; lag++) {
35+ let sum = 0
36+ for (let i = lag; i < x.length; i++) sum += x[i] * x[i - lag]
37+ r[lag] = sum
38+ }
39+ return r
40+}
41+
42+/**
43+ * Levinson-Durbin: the order-p predictor minimising the mean squared
44+ * prediction error, from the autocorrelation. Returns a[1..p] in a[0..p-1].
45+ */
46+function levinson(r: Float64Array, order: number): Float64Array | null {
47+ if (r[0] <= 0) return null
48+ const a = new Float64Array(order)
49+ const tmp = new Float64Array(order)
50+ let error = r[0]
51+
52+ for (let i = 0; i < order; i++) {
53+ let acc = r[i + 1]
54+ for (let j = 0; j < i; j++) acc -= a[j] * r[i - j]
55+ const k = acc / error
56+ if (!Number.isFinite(k)) return null
57+
58+ a[i] = k
59+ for (let j = 0; j < i; j++) tmp[j] = a[j] - k * a[i - 1 - j]
60+ for (let j = 0; j < i; j++) a[j] = tmp[j]
61+
62+ error *= 1 - k * k
63+ if (error <= 0) return null
64+ }
65+ return a
66+}
67+
68+/** Scale the real coefficients onto integers sharing one right shift. */
69+function quantiseCoefficients(a: Float64Array): LpcModel | null {
70+ let maxAbs = 0
71+ for (const v of a) maxAbs = Math.max(maxAbs, Math.abs(v))
72+ if (maxAbs === 0 || !Number.isFinite(maxAbs)) return null
73+
74+ let shift = COEFF_PRECISION - 1 - Math.floor(Math.log2(maxAbs)) - 1
75+ shift = Math.max(0, Math.min(15, shift))
76+
77+ const limit = 2 ** (COEFF_PRECISION - 1)
78+ const coeffs = new Int32Array(a.length)
79+ for (let i = 0; i < a.length; i++) {
80+ coeffs[i] = Math.max(-limit, Math.min(limit - 1, Math.round(a[i] * 2 ** shift)))
81+ }
82+ return { order: a.length, shift, coeffs }
83+}
84+
85+export function fitLpc(x: Int16Array, order: number): LpcModel | null {
86+ const a = levinson(autocorrelation(x, order), order)
87+ return a ? quantiseCoefficients(a) : null
88+}
89+
90+/**
91+ * Prediction residual. The first `order` entries are the samples themselves
92+ * (the warm-up the decoder needs before it can predict). Differences are taken
93+ * in int16 with wraparound, so the transform is exactly invertible.
94+ */
95+export function lpcResidual(x: Int16Array, model: LpcModel): Int16Array {
96+ const { order, shift, coeffs } = model
97+ const scale = 2 ** shift
98+ const e = new Int16Array(x.length)
99+ for (let n = 0; n < Math.min(order, x.length); n++) e[n] = x[n]
100+ for (let n = order; n < x.length; n++) {
101+ let sum = 0
102+ for (let k = 0; k < order; k++) sum += coeffs[k] * x[n - 1 - k]
103+ e[n] = ((x[n] - Math.floor(sum / scale)) << 16) >> 16
104+ }
105+ return e
106+}
107+
108+/** Rebuild the signal from its residual — the exact inverse of `lpcResidual`. */
109+export function lpcRestore(e: Int16Array, model: LpcModel): Int16Array {
110+ const { order, shift, coeffs } = model
111+ const scale = 2 ** shift
112+ const x = new Int16Array(e.length)
113+ for (let n = 0; n < Math.min(order, e.length); n++) x[n] = e[n]
114+ for (let n = order; n < e.length; n++) {
115+ let sum = 0
116+ for (let k = 0; k < order; k++) sum += coeffs[k] * x[n - 1 - k]
117+ x[n] = ((e[n] + Math.floor(sum / scale)) << 16) >> 16
118+ }
119+ return x
120+}
121+
122+/** Bytes the decoder needs for the model: order, shift, and the coefficients. */
123+export function modelSize(model: LpcModel): number {
124+ return 2 + 2 * model.order
125+}
src/main.tsxadded+6−0View file
@@ -0,0 +1,6 @@
1+import { createRoot } from 'react-dom/client'
2+import App from './App'
3+import 'katex/dist/katex.min.css'
4+import './app.css'
5+
6+createRoot(document.getElementById('root')!).render(<App />)
src/model/filters.tsadded+110−0View file
@@ -0,0 +1,110 @@
1+/**
2+ * FIR filter presets and their frequency response.
3+ *
4+ * The filter is always realized as an explicit convolution kernel, so the
5+ * pipeline is exactly x → h*x → (+dither) → round, and the theory can work
6+ * from |H(f)| of the same taps the data actually went through. Cutoffs are
7+ * given in Hz against a user-set sample rate; internally everything is in
8+ * normalized frequency (cycles/sample, Nyquist = 0.5).
9+ */
10+
11+export type FilterFamily = 'none' | 'movingAverage' | 'lowpass' | 'bandpass' | 'firstDifference'
12+
13+export interface FilterSpec {
14+ family: FilterFamily
15+ /** Low band edge, Hz (bandpass only). */
16+ lowHz: number
17+ /** Cutoff / high band edge, Hz (lowpass, bandpass). */
18+ highHz: number
19+ /** Kernel length for the windowed-sinc designs; forced odd. */
20+ taps: number
21+ /** Moving-average width. */
22+ width: number
23+}
24+
25+export const FAMILY_LABELS: Record<FilterFamily, string> = {
26+ none: 'none (white noise)',
27+ movingAverage: 'moving average',
28+ lowpass: 'lowpass',
29+ bandpass: 'bandpass',
30+ firstDifference: 'first difference',
31+}
32+
33+export const DEFAULT_SPEC: FilterSpec = {
34+ family: 'bandpass',
35+ lowHz: 300,
36+ highHz: 6000,
37+ taps: 101,
38+ width: 8,
39+}
40+
41+/** Hamming-windowed sinc lowpass with unit DC gain; fc in cycles/sample. */
42+function windowedSincLowpass(fc: number, taps: number): Float64Array {
43+ const n = taps | 1
44+ const mid = (n - 1) / 2
45+ const h = new Float64Array(n)
46+ let sum = 0
47+ for (let i = 0; i < n; i++) {
48+ const t = i - mid
49+ const sinc = t === 0 ? 2 * fc : Math.sin(2 * Math.PI * fc * t) / (Math.PI * t)
50+ const w = 0.54 - 0.46 * Math.cos((2 * Math.PI * i) / (n - 1))
51+ h[i] = sinc * w
52+ sum += h[i]
53+ }
54+ for (let i = 0; i < n; i++) h[i] /= sum
55+ return h
56+}
57+
58+/** Clamp the spec's band edges into (0, Nyquist) for the given sample rate. */
59+export function clampSpec(spec: FilterSpec, sampleRateHz: number): FilterSpec {
60+ const nyq = sampleRateHz / 2
61+ const highHz = Math.min(Math.max(spec.highHz, 2), nyq * 0.98)
62+ const lowHz = Math.min(Math.max(spec.lowHz, 1), highHz * 0.9)
63+ return { ...spec, highHz, lowHz }
64+}
65+
66+export function designKernel(spec: FilterSpec, sampleRateHz: number): Float64Array {
67+ const s = clampSpec(spec, sampleRateHz)
68+ switch (s.family) {
69+ case 'none':
70+ return new Float64Array([1])
71+ case 'movingAverage': {
72+ const w = Math.max(2, Math.round(s.width))
73+ return new Float64Array(w).fill(1 / w)
74+ }
75+ case 'lowpass':
76+ return windowedSincLowpass(s.highHz / sampleRateHz, s.taps)
77+ case 'bandpass': {
78+ const lo = windowedSincLowpass(s.lowHz / sampleRateHz, s.taps)
79+ const hi = windowedSincLowpass(s.highHz / sampleRateHz, s.taps)
80+ const h = new Float64Array(hi.length)
81+ for (let i = 0; i < h.length; i++) h[i] = hi[i] - lo[i]
82+ return h
83+ }
84+ case 'firstDifference':
85+ return new Float64Array([1, -1])
86+ }
87+}
88+
89+/** ‖h‖₂ — the gain from input σ to the filtered signal's σ_y. */
90+export function kernelNorm(h: Float64Array): number {
91+ let sum = 0
92+ for (const v of h) sum += v * v
93+ return Math.sqrt(sum)
94+}
95+
96+/** |H(f)| at `points` frequencies uniform on [0, 0.5] cycles/sample. */
97+export function magnitudeResponse(h: Float64Array, points: number): Float64Array {
98+ const out = new Float64Array(points)
99+ for (let k = 0; k < points; k++) {
100+ const f = (0.5 * k) / (points - 1)
101+ let re = 0
102+ let im = 0
103+ for (let i = 0; i < h.length; i++) {
104+ re += h[i] * Math.cos(2 * Math.PI * f * i)
105+ im -= h[i] * Math.sin(2 * Math.PI * f * i)
106+ }
107+ out[k] = Math.hypot(re, im)
108+ }
109+ return out
110+}
src/model/pipeline.tsadded+42−0View file
@@ -0,0 +1,42 @@
1+/**
2+ * The generating model: x ~ N(0, σ²) i.i.d. → FIR filter → optional additive
3+ * uniform dither on [-1/2, 1/2) → round to integers (the quantization step is
4+ * the unit, so σ is measured in steps).
5+ *
6+ * A single streaming implementation feeds both the scrolling display and the
7+ * compression block, so what is compressed is exactly what is shown.
8+ */
9+import { GaussianStream } from './random'
10+
11+export class Pipeline {
12+ private rng: GaussianStream
13+ /** Ring of the last kernel-length inputs; index 0 is the newest. */
14+ private history: Float64Array
15+ private pos = 0
16+
17+ constructor(
18+ private kernel: Float64Array,
19+ private sigma: number,
20+ private dither: boolean,
21+ seed: number,
22+ ) {
23+ this.rng = new GaussianStream(seed)
24+ this.history = new Float64Array(kernel.length)
25+ }
26+
27+ /** Generate the next n quantized samples, clamped into int16 range. */
28+ next(n: number): Int16Array {
29+ const { kernel, history } = this
30+ const L = kernel.length
31+ const out = new Int16Array(n)
32+ for (let j = 0; j < n; j++) {
33+ this.pos = (this.pos + L - 1) % L
34+ history[this.pos] = this.sigma * this.rng.normal()
35+ let y = 0
36+ for (let k = 0; k < L; k++) y += kernel[k] * history[(this.pos + k) % L]
37+ if (this.dither) y += this.rng.uniformCentered()
38+ out[j] = Math.max(-32768, Math.min(32767, Math.round(y)))
39+ }
40+ return out
41+ }
42+}
src/model/random.tsadded+43−0View file
@@ -0,0 +1,43 @@
1+/**
2+ * Deterministic Gaussian sample stream.
3+ *
4+ * Seeded so that the compression block is reproducible for a given parameter
5+ * set — the reported sizes then only move when the model moves, not between
6+ * recomputes. mulberry32 for the uniforms, Box–Muller for the normals.
7+ */
8+export class GaussianStream {
9+ private state: number
10+ private spare: number | null = null
11+
12+ constructor(seed: number) {
13+ this.state = seed >>> 0
14+ }
15+
16+ private uniform(): number {
17+ this.state = (this.state + 0x6d2b79f5) >>> 0
18+ let t = this.state
19+ t = Math.imul(t ^ (t >>> 15), t | 1)
20+ t ^= t + Math.imul(t ^ (t >>> 7), t | 61)
21+ return ((t ^ (t >>> 14)) >>> 0) / 4294967296
22+ }
23+
24+ /** Uniform on [-1/2, 1/2), for dither. */
25+ uniformCentered(): number {
26+ return this.uniform() - 0.5
27+ }
28+
29+ /** Standard normal. */
30+ normal(): number {
31+ if (this.spare !== null) {
32+ const v = this.spare
33+ this.spare = null
34+ return v
35+ }
36+ let u = 0
37+ while (u === 0) u = this.uniform()
38+ const r = Math.sqrt(-2 * Math.log(u))
39+ const theta = 2 * Math.PI * this.uniform()
40+ this.spare = r * Math.sin(theta)
41+ return r * Math.cos(theta)
42+ }
43+}
src/model/theory.tsadded+34−0View file
@@ -0,0 +1,34 @@
1+/**
2+ * The theoretical bits/sample: the entropy rate of the stationary Gaussian
3+ * process y = h * x with x ~ N(0, σ²) i.i.d., quantized at unit step. In the
4+ * fine-quantization (high-resolution) regime the discrete entropy rate
5+ * approaches the differential entropy rate (Kolmogorov):
6+ *
7+ * R = ½ log₂(2πe) + ∫₀^{1/2} log₂ S(f) df, S(f) = σ² |H(f)|²
8+ *
9+ * with f in cycles/sample. With no filter this reduces to ½ log₂(2πe σ²).
10+ * The formula ignores dither and degrades where S(f) falls to the order of
11+ * the quantization step or below — exactly the regime the app probes.
12+ */
13+
14+const INTEGRATION_POINTS = 8192
15+
16+export function entropyRateBits(kernel: Float64Array, sigma: number): number {
17+ const L = kernel.length
18+ let integral = 0
19+ for (let k = 0; k < INTEGRATION_POINTS; k++) {
20+ // Midpoint rule keeps f = 0 (where a bandpass H vanishes) off the grid;
21+ // the log singularity at isolated zeros is integrable.
22+ const f = (0.5 * (k + 0.5)) / INTEGRATION_POINTS
23+ let re = 0
24+ let im = 0
25+ for (let i = 0; i < L; i++) {
26+ re += kernel[i] * Math.cos(2 * Math.PI * f * i)
27+ im -= kernel[i] * Math.sin(2 * Math.PI * f * i)
28+ }
29+ const S = sigma * sigma * (re * re + im * im)
30+ integral += Math.log2(Math.max(S, 1e-300))
31+ }
32+ integral *= 0.5 / INTEGRATION_POINTS
33+ return 0.5 * Math.log2(2 * Math.PI * Math.E) + integral
34+}
src/worker/compressWorker.tsadded+61−0View file
@@ -0,0 +1,61 @@
1+/**
2+ * Compression measurements off the main thread, so the scrolling view never
3+ * stutters while zstd -19 or the LPC fit runs. One message in (the model),
4+ * one message out (the nine codec results).
5+ */
6+import { Pipeline } from '../model/pipeline'
7+import {
8+ initCodecs,
9+ compressAll,
10+ ZLIB,
11+ ZSTD,
12+ ANS,
13+ DELTA_ZLIB,
14+ DELTA_ZSTD,
15+ DELTA_ANS,
16+ LPC_ZLIB,
17+ LPC_ZSTD,
18+ LPC_ANS,
19+ type CodecResult,
20+} from '../compress/codecs'
21+
22+export interface CompressRequest {
23+ id: number
24+ kernel: Float64Array
25+ sigma: number
26+ dither: boolean
27+ blockSize: number
28+ seed: number
29+}
30+
31+export interface CompressResponse {
32+ id: number
33+ results: CodecResult[]
34+ /** Empirical std of the quantized block, for display sanity. */
35+ empiricalStd: number
36+ error?: string
37+}
38+
39+const CODECS = [ZLIB, ZSTD, ANS, DELTA_ZLIB, DELTA_ZSTD, DELTA_ANS, LPC_ZLIB, LPC_ZSTD, LPC_ANS]
40+
41+const post = self.postMessage as (message: CompressResponse) => void
42+
43+self.onmessage = async (e: MessageEvent<CompressRequest>) => {
44+ const { id, kernel, sigma, dither, blockSize, seed } = e.data
45+ try {
46+ await initCodecs()
47+ const samples = new Pipeline(kernel, sigma, dither, seed).next(blockSize)
48+ let sum = 0
49+ let sumSq = 0
50+ for (let i = 0; i < samples.length; i++) {
51+ sum += samples[i]
52+ sumSq += samples[i] * samples[i]
53+ }
54+ const mean = sum / samples.length
55+ const empiricalStd = Math.sqrt(Math.max(0, sumSq / samples.length - mean * mean))
56+ const bytes = new Uint8Array(samples.buffer, 0, samples.byteLength)
57+ post({ id, results: compressAll(bytes, CODECS), empiricalStd })
58+ } catch (err) {
59+ post({ id, results: [], empiricalStd: 0, error: String(err) })
60+ }
61+}
tsconfig.app.jsonadded+24−0View file
@@ -0,0 +1,24 @@
1+{
2+ "compilerOptions": {
3+ "tsBuildInfoFile": "./node_modules/.tmp/tsconfig.app.tsbuildinfo",
4+ "target": "ES2022",
5+ "useDefineForClassFields": true,
6+ "lib": ["ES2022", "DOM", "DOM.Iterable"],
7+ "module": "ESNext",
8+ "skipLibCheck": true,
9+
10+ "moduleResolution": "bundler",
11+ "allowImportingTsExtensions": true,
12+ "verbatimModuleSyntax": true,
13+ "moduleDetection": "force",
14+ "noEmit": true,
15+ "jsx": "react-jsx",
16+
17+ "strict": true,
18+ "noUnusedLocals": true,
19+ "noUnusedParameters": true,
20+ "noFallthroughCasesInSwitch": true,
21+ "types": ["vite/client"]
22+ },
23+ "include": ["src"]
24+}
tsconfig.jsonadded+4−0View file
@@ -0,0 +1,4 @@
1+{
2+ "files": [],
3+ "references": [{ "path": "./tsconfig.app.json" }, { "path": "./tsconfig.node.json" }]
4+}
tsconfig.node.jsonadded+22−0View file
@@ -0,0 +1,22 @@
1+{
2+ "compilerOptions": {
3+ "tsBuildInfoFile": "./node_modules/.tmp/tsconfig.node.tsbuildinfo",
4+ "target": "ES2023",
5+ "lib": ["ES2023"],
6+ "module": "ESNext",
7+ "skipLibCheck": true,
8+
9+ "moduleResolution": "bundler",
10+ "allowImportingTsExtensions": true,
11+ "verbatimModuleSyntax": true,
12+ "moduleDetection": "force",
13+ "noEmit": true,
14+
15+ "strict": true,
16+ "noUnusedLocals": true,
17+ "noUnusedParameters": true,
18+ "noFallthroughCasesInSwitch": true,
19+ "types": ["node"]
20+ },
21+ "include": ["vite.config.ts"]
22+}
vite.config.tsadded+9−0View file
@@ -0,0 +1,9 @@
1+import { defineConfig } from 'vite'
2+import react from '@vitejs/plugin-react'
3+
4+// Relative base so the built site works from any path (GitHub Pages project
5+// site, a subdirectory, or the filesystem).
6+export default defineConfig({
7+ base: './',
8+ plugins: [react()],
9+})