omni-viewer-core 0.22.0 → 0.24.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (174) hide show
  1. package/README.md +291 -9
  2. package/THIRD_PARTY_NOTICES.md +23 -0
  3. package/dist/assets/audio-engine/spectrogram_kernel.mjs +256 -0
  4. package/dist/assets/audio-engine/spectrogram_worker.mjs +24 -0
  5. package/dist/i18n/catalog.en.js +363 -0
  6. package/dist/i18n/catalog.ja.js +363 -0
  7. package/dist/i18n/catalog.ko.js +363 -0
  8. package/dist/i18n/catalog.zh-cn.js +363 -0
  9. package/dist/i18n/catalog.zh-tw.js +185 -0
  10. package/dist/parsers/cfb.d.ts +21 -0
  11. package/dist/parsers/cfb.js +122 -0
  12. package/dist/parsers/dicom/index.d.ts +6 -0
  13. package/dist/parsers/dicom/index.js +335 -0
  14. package/dist/parsers/dicom/model.d.ts +67 -0
  15. package/dist/parsers/dicom/model.js +1 -0
  16. package/dist/parsers/dicom/pixels.d.ts +9 -0
  17. package/dist/parsers/dicom/pixels.js +82 -0
  18. package/dist/parsers/doc-binary/index.js +2 -76
  19. package/dist/parsers/drawio/index.d.ts +46 -0
  20. package/dist/parsers/drawio/index.js +193 -0
  21. package/dist/parsers/eml/index.d.ts +37 -0
  22. package/dist/parsers/eml/index.js +278 -0
  23. package/dist/parsers/epub/index.d.ts +2 -0
  24. package/dist/parsers/epub/index.js +16 -12
  25. package/dist/parsers/epub/zip.d.ts +2 -1
  26. package/dist/parsers/epub/zip.js +88 -44
  27. package/dist/parsers/file-preview.d.ts +45 -0
  28. package/dist/parsers/file-preview.js +179 -0
  29. package/dist/parsers/gps/index.d.ts +18 -0
  30. package/dist/parsers/gps/index.js +264 -0
  31. package/dist/parsers/har/index.d.ts +1 -7
  32. package/dist/parsers/har/index.js +2 -42
  33. package/dist/parsers/iso8601.d.ts +7 -0
  34. package/dist/parsers/iso8601.js +42 -0
  35. package/dist/parsers/junit/index.d.ts +49 -0
  36. package/dist/parsers/junit/index.js +841 -0
  37. package/dist/parsers/junit/model.d.ts +141 -0
  38. package/dist/parsers/junit/model.js +8 -0
  39. package/dist/parsers/junit/sniff.d.ts +7 -0
  40. package/dist/parsers/junit/sniff.js +28 -0
  41. package/dist/parsers/mcap/errors.d.ts +5 -0
  42. package/dist/parsers/mcap/errors.js +5 -0
  43. package/dist/parsers/mcap/index.d.ts +95 -0
  44. package/dist/parsers/mcap/index.js +321 -0
  45. package/dist/parsers/mcap/self-loading.d.ts +6 -0
  46. package/dist/parsers/mcap/self-loading.js +21 -0
  47. package/dist/parsers/mcap/zstd.d.ts +8 -0
  48. package/dist/parsers/mcap/zstd.js +102 -0
  49. package/dist/parsers/mesh/index.d.ts +28 -0
  50. package/dist/parsers/mesh/index.js +351 -0
  51. package/dist/parsers/mesh/validation.d.ts +11 -0
  52. package/dist/parsers/mesh/validation.js +215 -0
  53. package/dist/parsers/msg/index.d.ts +17 -0
  54. package/dist/parsers/msg/index.js +222 -0
  55. package/dist/parsers/openapi/index.d.ts +24 -0
  56. package/dist/parsers/openapi/index.js +319 -0
  57. package/dist/parsers/openapi/model.d.ts +57 -0
  58. package/dist/parsers/openapi/model.js +1 -0
  59. package/dist/parsers/openapi/nodes.d.ts +19 -0
  60. package/dist/parsers/openapi/nodes.js +214 -0
  61. package/dist/parsers/openapi/self-loading.d.ts +5 -0
  62. package/dist/parsers/openapi/self-loading.js +35 -0
  63. package/dist/parsers/openapi/sniff.d.ts +6 -0
  64. package/dist/parsers/openapi/sniff.js +136 -0
  65. package/dist/parsers/opendocument/index.d.ts +44 -0
  66. package/dist/parsers/opendocument/index.js +355 -0
  67. package/dist/parsers/sarif/index.d.ts +19 -0
  68. package/dist/parsers/sarif/index.js +238 -0
  69. package/dist/parsers/sbom/index.d.ts +19 -0
  70. package/dist/parsers/sbom/index.js +78 -0
  71. package/dist/parsers/text/index.d.ts +17 -0
  72. package/dist/parsers/text/index.js +68 -0
  73. package/dist/parsers/trace/index.d.ts +23 -0
  74. package/dist/parsers/trace/index.js +129 -0
  75. package/dist/parsers/xml/model.d.ts +3 -0
  76. package/dist/parsers/xml/parse.js +5 -1
  77. package/dist/parsers/xml/prologue.d.ts +3 -0
  78. package/dist/parsers/xml/prologue.js +59 -0
  79. package/dist/parsers/yaml/index.js +3 -2
  80. package/dist/parsers/yaml/model.d.ts +2 -1
  81. package/dist/parsers/yaml/self-loading.js +2 -2
  82. package/dist/registry/container.js +4 -0
  83. package/dist/registry/file-preview-sniff.d.ts +5 -0
  84. package/dist/registry/file-preview-sniff.js +112 -0
  85. package/dist/registry/file-previews.d.ts +57 -0
  86. package/dist/registry/file-previews.js +30 -0
  87. package/dist/registry/index.d.ts +20 -4
  88. package/dist/registry/index.js +110 -11
  89. package/dist/registry/probe.js +74 -145
  90. package/dist/registry/sniff.d.ts +6 -1
  91. package/dist/registry/sniff.js +26 -1
  92. package/dist/styles/audio.css +4 -4
  93. package/dist/styles/dicom.css +14 -0
  94. package/dist/styles/eml.css +19 -0
  95. package/dist/styles/file-preview.css +34 -0
  96. package/dist/styles/junit.css +96 -0
  97. package/dist/styles/mcap.css +25 -0
  98. package/dist/styles/mesh.css +15 -0
  99. package/dist/styles/openapi.css +38 -0
  100. package/dist/styles/text.css +14 -0
  101. package/dist/viewers/audio/audio-window.d.ts +16 -0
  102. package/dist/viewers/audio/audio-window.js +141 -49
  103. package/dist/viewers/audio/index.d.ts +9 -0
  104. package/dist/viewers/audio/index.js +60 -16
  105. package/dist/viewers/audio/spectrogram-view.d.ts +40 -3
  106. package/dist/viewers/audio/spectrogram-view.js +247 -38
  107. package/dist/viewers/audio/spectrogram-worker.d.ts +27 -0
  108. package/dist/viewers/audio/spectrogram-worker.js +117 -0
  109. package/dist/viewers/audio/spectrogram.d.ts +0 -6
  110. package/dist/viewers/audio/spectrogram.js +12 -12
  111. package/dist/viewers/audio/styles.js +4 -4
  112. package/dist/viewers/dicom/index.d.ts +18 -0
  113. package/dist/viewers/dicom/index.js +222 -0
  114. package/dist/viewers/dicom/styles.d.ts +1 -0
  115. package/dist/viewers/dicom/styles.js +15 -0
  116. package/dist/viewers/drawio/index.d.ts +15 -0
  117. package/dist/viewers/drawio/index.js +447 -0
  118. package/dist/viewers/eml/index.d.ts +19 -0
  119. package/dist/viewers/eml/index.js +219 -0
  120. package/dist/viewers/eml/styles.d.ts +1 -0
  121. package/dist/viewers/eml/styles.js +20 -0
  122. package/dist/viewers/file-preview-styles.d.ts +1 -0
  123. package/dist/viewers/file-preview-styles.js +35 -0
  124. package/dist/viewers/file-preview.d.ts +36 -0
  125. package/dist/viewers/file-preview.js +248 -0
  126. package/dist/viewers/geojson/index.d.ts +4 -1
  127. package/dist/viewers/geojson/index.js +9 -4
  128. package/dist/viewers/gps/index.d.ts +15 -0
  129. package/dist/viewers/gps/index.js +170 -0
  130. package/dist/viewers/html/index.d.ts +1 -1
  131. package/dist/viewers/html/index.js +7 -2
  132. package/dist/viewers/junit/index.d.ts +31 -0
  133. package/dist/viewers/junit/index.js +840 -0
  134. package/dist/viewers/junit/styles.d.ts +1 -0
  135. package/dist/viewers/junit/styles.js +101 -0
  136. package/dist/viewers/mcap/index.d.ts +30 -0
  137. package/dist/viewers/mcap/index.js +281 -0
  138. package/dist/viewers/mcap/self-loading.d.ts +4 -0
  139. package/dist/viewers/mcap/self-loading.js +16 -0
  140. package/dist/viewers/mcap/styles.d.ts +1 -0
  141. package/dist/viewers/mcap/styles.js +26 -0
  142. package/dist/viewers/mesh/async.d.ts +2 -0
  143. package/dist/viewers/mesh/async.js +19 -0
  144. package/dist/viewers/mesh/index.d.ts +39 -0
  145. package/dist/viewers/mesh/index.js +462 -0
  146. package/dist/viewers/mesh/self-loading.d.ts +6 -0
  147. package/dist/viewers/mesh/self-loading.js +28 -0
  148. package/dist/viewers/mesh/styles.d.ts +2 -0
  149. package/dist/viewers/mesh/styles.js +17 -0
  150. package/dist/viewers/msg/index.d.ts +18 -0
  151. package/dist/viewers/msg/index.js +83 -0
  152. package/dist/viewers/openapi/controller.d.ts +20 -0
  153. package/dist/viewers/openapi/controller.js +74 -0
  154. package/dist/viewers/openapi/index.d.ts +25 -0
  155. package/dist/viewers/openapi/index.js +556 -0
  156. package/dist/viewers/openapi/self-loading.d.ts +5 -0
  157. package/dist/viewers/openapi/self-loading.js +13 -0
  158. package/dist/viewers/openapi/styles.d.ts +2 -0
  159. package/dist/viewers/openapi/styles.js +40 -0
  160. package/dist/viewers/opendocument/index.d.ts +15 -0
  161. package/dist/viewers/opendocument/index.js +122 -0
  162. package/dist/viewers/sarif/index.d.ts +14 -0
  163. package/dist/viewers/sarif/index.js +35 -0
  164. package/dist/viewers/sbom/index.d.ts +14 -0
  165. package/dist/viewers/sbom/index.js +44 -0
  166. package/dist/viewers/text/index.d.ts +19 -0
  167. package/dist/viewers/text/index.js +91 -0
  168. package/dist/viewers/text/styles.d.ts +1 -0
  169. package/dist/viewers/text/styles.js +15 -0
  170. package/dist/viewers/trace/args-preview.d.ts +4 -0
  171. package/dist/viewers/trace/args-preview.js +60 -0
  172. package/dist/viewers/trace/index.d.ts +14 -0
  173. package/dist/viewers/trace/index.js +167 -0
  174. package/package.json +162 -10
package/README.md CHANGED
@@ -12,20 +12,25 @@ models, viewers mount into a DOM element, and everything host-specific
12
12
 
13
13
  ## Supported formats
14
14
 
15
- - **Documents** — PDF, Word (DOCX and legacy DOC), HWP, PowerPoint (PPTX and
15
+ - **Documents** — Plain text (.txt), PDF, Word (DOCX and legacy DOC), HWP, PowerPoint (PPTX and
16
16
  legacy PPT), HTML, Markdown, Jupyter Notebook (.ipynb), EPUB e-books (2 and 3),
17
- LaTeX (structure and math preview, not typesetting)
17
+ LaTeX (structure and math preview, not typesetting), OpenDocument (ODT/ODP)
18
18
  - **Data & spreadsheets** — Excel, CSV/TSV, JSON, JSONL/NDJSON, YAML, TOML, XML,
19
19
  Parquet, Arrow IPC / Feather V2 (.arrow/.feather), Avro, HDF5, MATLAB MAT, NumPy (NPY/NPZ), Safetensors, GGUF, ONNX, TFLite/LiteRT,
20
20
  Keras (.keras and legacy .h5), Core ML (.mlmodel and .mlpackage),
21
21
  OpenVINO IR (.xml + .bin), PyTorch Export (.pt2), ExecuTorch (.pte), Protocol
22
22
  Buffers, ReqIF, SQLite
23
- - **Web & network** — HAR (HTTP Archive) request logs
23
+ - **Email, web & network** — EML and Outlook MSG email messages, HAR (HTTP Archive) request logs,
24
+ OpenAPI / Swagger API documents (JSON/YAML)
25
+ - **Developer tooling** — JUnit XML test reports, SARIF 2.1.0, CycloneDX JSON
26
+ SBOMs, Chrome Trace Event JSON
27
+ - **Medical imaging** — DICOM (.dcm/.dicom), native grayscale/RGB image preview,
28
+ multi-frame navigation, windowing and metadata inspection
24
29
  - **Media & graphics** — audio (waveform/spectrogram), video, images,
25
- Photoshop PSD, fonts (TTF/OTF/WOFF)
30
+ Photoshop PSD, fonts (TTF/OTF/WOFF), 3D meshes (GLB/glTF/STL/OBJ)
26
31
  - **Engineering & automotive** — CAN DBC, AUTOSAR ARXML, ASAM A2L, Vector
27
- ASC/BLF logs, ASAM MDF 4 (MF4), PCAP/PCAPNG, ROS bag, STEP (STP)
28
- - **Diagrams & GIS** — Mermaid, PlantUML, ESRI Shapefile, GeoJSON
32
+ ASC/BLF logs, ASAM MDF 4 (MF4), PCAP/PCAPNG, ROS bag, MCAP (.mcap), STEP (STP)
33
+ - **Diagrams & GIS** — Mermaid, PlantUML, draw.io, ESRI Shapefile, GeoJSON, GPX, KML and KMZ
29
34
  - **Archives** — ZIP-family listing and safe entry preview
30
35
 
31
36
  The HTML viewer handles `.html` and `.htm` files with preview and source tabs.
@@ -34,6 +39,50 @@ navigation removed or blocked; inline styles and embedded data images remain.
34
39
  Files over 8 MiB show a bounded source preview. Very dense markup opens in
35
40
  source view to keep parsing responsive.
36
41
 
42
+ ### Plain text (.txt)
43
+
44
+ `omni-viewer-core/viewers/text` provides a dependency-free, read-only text
45
+ viewer. Whitespace, tabs and line endings are preserved; HTML and Markdown
46
+ remain literal text. Soft line wrapping can be toggled without changing the
47
+ content. UTF-8 is the default; UTF-8 and UTF-16 LE/BE BOMs are recognized.
48
+ For BOM-less UTF-16, pass `parse: { encoding: 'utf-16le' }` (or `'utf-16be'`).
49
+ Legacy encodings such as CP949 are not automatically detected. Malformed
50
+ Unicode bytes are displayed as replacement characters with a visible warning;
51
+ UTF-32 BOMs are rejected.
52
+
53
+ ```ts
54
+ import { mountTextViewer } from 'omni-viewer-core/viewers/text';
55
+
56
+ const handle = await mountTextViewer(
57
+ { fileName: 'notes.txt', data: bytes }, container, hostContext
58
+ );
59
+ // On file switch or tab close:
60
+ handle.dispose();
61
+ ```
62
+
63
+ The DOM-free parser is available at `omni-viewer-core/parsers/text` as
64
+ `parseText(bytes, options)`. The default input limit is 256 MiB and the decoded
65
+ preview budget is 8 MiB (excluding the BOM); larger previews are visibly marked
66
+ as partial. Override them through `limits.maxInputBytes` / `maxPreviewBytes`
67
+ (under `parse.limits` for the viewer). Incomplete characters at a preview
68
+ boundary are omitted without an encoding warning. Decoding limits are
69
+ cooperative; hosts still own file loading and hard CPU/memory bounds.
70
+ Scoped hosts load `omni-viewer-core/styles/text.css`; shadow mode injects it.
71
+
72
+ The registry returns `viewerId: 'text'` for `.txt` (including uppercase `.TXT`).
73
+ Hosts must register that extension and explicitly map `text` to
74
+ `mountTextViewer`; the registry does not load viewers. Other extensions and
75
+ extensionless text retain their existing detection behavior.
76
+
77
+ ### DICOM (.dcm/.dicom)
78
+
79
+ `omni-viewer-core/viewers/dicom` provides a dependency-free native 8/16-bit DICOM
80
+ preview with frame navigation, window center/width, inversion, zoom, and a
81
+ searchable metadata table. Implicit/explicit VR little endian and explicit VR
82
+ big endian are supported. Compressed images keep metadata available and report
83
+ that preview is unavailable. See [DICOM support and limits](examples/dicom/SUPPORT.md)
84
+ and the [offline synthetic example](examples/dicom/README.md).
85
+
37
86
  ## Install
38
87
 
39
88
  ```sh
@@ -74,6 +123,156 @@ Viewers follow the same pattern — a `mount*Viewer(input, container, ctx,
74
123
  deps)` entry per format, plus `self-loading` variants that dynamically import
75
124
  their own dependencies for hosts that don't want to wire them manually.
76
125
 
126
+ ### Read-only file previews
127
+
128
+ | Viewer ID | Parser / mount | Preview |
129
+ | --- | --- | --- |
130
+ | `sarif` | `parseSarif` / `mountSarifViewer` | Result severity, rule, tool and source locations |
131
+ | `gps` | `parseGps` / `mountGpsViewer` | Offline GPX/KML/KMZ map and GPX track statistics |
132
+ | `trace` | `parseTrace` / `mountTraceViewer` | Chrome Trace Event JSON timeline with search and zoom |
133
+ | `sbom` | `parseSbom` / `mountSbomViewer` | CycloneDX components, licenses and direct dependencies |
134
+ | `msg` | `parseMsg` / `mountMsgViewer` | Outlook message headers, text/HTML and file attachments |
135
+ | `drawio` | `parseDrawio` / `mountDrawioViewer` | Compressed or plain diagrams, basic shapes, connectors and layers |
136
+ | `opendocument` | `parseOpenDocument` / `mountOpenDocumentViewer` | ODT structure and basic ODP slide layout |
137
+
138
+ Import each from `omni-viewer-core/parsers/<id>` or
139
+ `omni-viewer-core/viewers/<id>`. Register the extensions and map the viewer IDs
140
+ to their mount functions in the host. Scoped mounts use
141
+ `omni-viewer-core/styles/file-preview.css`; the GPS map also uses
142
+ `omni-viewer-core/styles/geojson.css`. Deflated KMZ, OpenDocument and draw.io
143
+ content uses the runtime’s raw deflate `DecompressionStream` or a bounded
144
+ `inflate` function supplied by the host.
145
+
146
+ When `detectViewer` receives a text sample, it also routes `.json` and
147
+ extensionless SARIF 2.1.0, CycloneDX and Trace Event JSON to `sarif`, `sbom`
148
+ and `trace`, and `.xml` and extensionless namespaced GPX/KML and draw.io XML to
149
+ `gps` and `drawio`. Extensionless KMZ, ODT/ODP and Outlook MSG are recognized by
150
+ `probeContainer`. A `.msg` file must be an OLE compound file, so plain-text
151
+ `.msg` files (ROS messages, Tcl catalogs) keep their previous routing.
152
+
153
+ The GPS map draws geometry without basemap tiles and skips KML network links
154
+ and overlays. The trace preview supports complete, begin/end and instant events.
155
+ The MSG preview reads ordinary attachments and displays a diagnostic for
156
+ embedded messages or RTF-only bodies. draw.io approximates advanced styles
157
+ with basic shapes. OpenDocument previews text structure and basic slide layout.
158
+ Each preview reports records omitted because of resource limits.
159
+
160
+ ### OpenAPI / Swagger (JSON / YAML)
161
+
162
+ Open Swagger 2.0 and OpenAPI 3.0–3.2 specifications with searchable API
163
+ documentation, HTTP method/tag filters, parameter and request/response schemas,
164
+ examples, authentication definitions and an original-source tab.
165
+
166
+ ```ts
167
+ import { mountSelfLoadingOpenApiViewer } from 'omni-viewer-core/viewers/openapi/self-loading';
168
+
169
+ const handle = await mountSelfLoadingOpenApiViewer(
170
+ { fileName: 'openapi.yaml', data: bytes }, container, hostContext
171
+ );
172
+ handle.dispose();
173
+ ```
174
+
175
+ JSON needs no external library. YAML uses the optional `yaml` 2.8.3 peer;
176
+ base parser/viewer entries accept injected `yamlDeps`, and the opt-in
177
+ `/self-loading` entries load it for YAML. The registry refines generic
178
+ JSON/YAML to OpenAPI when file content matches; ordinary files retain their
179
+ existing viewers. Pass complete text plus `{ yamlDeps }` as `detectViewer`'s
180
+ sixth argument for strict YAML detection. Without it, conservative block YAML
181
+ signatures are recognized and validated when mounted.
182
+
183
+ Local references, Swagger definitions and parameter inheritance are supported.
184
+ The viewer runs offline and renders descriptions as text. Exports include
185
+ `parsers/openapi`, `viewers/openapi`, their `/self-loading` entries and
186
+ `styles/openapi.css`. See the [viewer contract](docs/viewers/openapi.md) and
187
+ [runnable examples](examples/openapi/README.md) for integration and limits.
188
+
189
+ ### 3D meshes (GLB / glTF / STL / OBJ)
190
+
191
+ Install `three` (`^0.180.0`) and, for TypeScript projects, `@types/three`
192
+ (`^0.180.0`). The viewer provides orbit/pan/zoom, fit to view, wireframe,
193
+ grid/axes, mesh statistics, glTF materials and animation playback.
194
+
195
+ ```ts
196
+ import { mountSelfLoadingMeshViewer } from 'omni-viewer-core/viewers/mesh/self-loading';
197
+
198
+ const handle = await mountSelfLoadingMeshViewer(
199
+ { fileName: 'model.glb', data: bytes }, container, hostContext
200
+ );
201
+ // On file switch or tab close:
202
+ handle.dispose();
203
+ ```
204
+
205
+ For injected runtimes, use `mountMeshViewer(input, container, ctx, deps, options)`
206
+ from `viewers/mesh`; `loadMeshViewerDeps()` supplies the standard Three.js deps.
207
+ `parsers/mesh` offers DOM-free `parseMesh(bytes, { fileName, limits })`
208
+ preflight without loading Three.js. It validates structure, coordinates, resource
209
+ paths and budgets; scene decoding happens when mounted.
210
+
211
+ Self-contained GLB and embedded glTF need no asset service. For `.gltf` with
212
+ external `.bin` or textures, provide `ctx.documentAssets.resolve(relativePath)`;
213
+ each returned asset's `dispose()` is called when the viewer closes. Input URLs,
214
+ absolute paths and parent traversal are rejected; the core never resolves them
215
+ against the page URL. Hosts must cap external asset sizes as well as the main
216
+ input. OBJ geometry, UVs and normals are supported; MTL libraries use default
217
+ materials with a warning. OBJ line and point primitives are omitted with a
218
+ warning. Compressed glTF requires host-provided decoders via
219
+ `deps.configureGltfLoader`; the core never downloads them. Rendering requires
220
+ WebGL 2. Scoped hosts should load `styles/mesh.css`; shadow mode injects it.
221
+
222
+ glTF preflight also budgets generated accessors, animation arrays, skeletons,
223
+ instancing and morph storage. Generated GPU storage uses conservative estimates
224
+ to cover device-dependent padding before rendering.
225
+
226
+ Run `npm run build`, serve the repo root, and open `examples/mesh/` for a
227
+ standalone example with sample files in all four formats and local file loading.
228
+
229
+ ### MCAP (.mcap)
230
+
231
+ Inspect topics/channels, schemas, sampled messages, metadata, attachments and
232
+ chunks in MCAP v0 recordings. The viewer includes topic filtering, search,
233
+ pagination, text/hex payload inspection and JSON copy. JSON payloads are
234
+ formatted when complete; CDR, Protobuf and other binary payloads are shown as
235
+ raw bytes. Attachments are listed without extracting or executing their data.
236
+
237
+ ```ts
238
+ import { mountSelfLoadingMcapViewer } from 'omni-viewer-core/viewers/mcap/self-loading';
239
+
240
+ const handle = await mountSelfLoadingMcapViewer(
241
+ { fileName: 'robot.mcap', data: bytes }, container, ctx,
242
+ { parse: { previewMessages: 5_000 } }
243
+ );
244
+ // On close: handle.dispose();
245
+ ```
246
+
247
+ Install the existing optional peers `lz4js` and `fzstd` to read compressed
248
+ chunks with the self-loading entry. Uncompressed recordings need no peers.
249
+ The base `parsers/mcap` and `viewers/mcap` entries import no runtime peers:
250
+ `parseMcap(bytes, { decompressLz4?, decompressZstd? }, parseOptions)` and
251
+ `mountMcapViewer(input, container, ctx, deps?, mountOptions?)` accept injected
252
+ bounded decoders (the optional third argument limits decoder history bytes).
253
+ Injected decoders may throw `McapDecompressionLimitError` to report resource
254
+ limits separately from malformed input.
255
+ `parsers/mcap/self-loading` exports `parseSelfLoadingMcap`
256
+ and `loadMcapDeps`. Missing/unknown codecs leave a partial document with any
257
+ available summary statistics and schemas. Incomplete scans display lower-bound
258
+ counts when exact summary counts are unavailable.
259
+
260
+ Defaults: 256 MiB input and cumulative chunk bytes, 1,000,000 records/map entries,
261
+ 5,000 retained messages, 4 KiB per payload/schema preview and an 8 MiB cumulative
262
+ preview budget. Structural encoding identifiers are read independently of the
263
+ preview budget (up to 16 KiB). Zstd frame content sizes and history windows are
264
+ checked before decoding; history uses the configured chunk byte limit.
265
+ Search/copy operate on retained samples; totals include every
266
+ scanned message. Uint64 values remain decimal strings and timestamps display
267
+ exact seconds relative to the recording's epoch. Chunk, data-section and summary
268
+ CRCs are checked when present; attachment payload CRCs are not checked. Limits
269
+ are cooperative; hosts can use a Worker for hard CPU/memory bounds. The viewer
270
+ supports shadow/scoped isolation and `styles/mcap.css`. Detection recognizes
271
+ the `.mcap` extension and binary magic, including extensionless recordings.
272
+ Host adapters must add `mcap` to their explicit viewer import maps.
273
+
274
+ The binary reader follows the [MCAP format specification](https://mcap.dev/spec).
275
+
77
276
  ### Fonts (TTF / OTF / WOFF)
78
277
 
79
278
  Preview local fonts with editable sample text, size and variable axis controls,
@@ -489,6 +688,46 @@ returns `partial` with diagnostics. Invalid JSON and unsupported major versions
489
688
  return typed failures. See [viewer details](examples/notebook/README.md) and the
490
689
  [sample notebook](examples/notebook/analysis.ipynb).
491
690
 
691
+ ### EML email (.eml)
692
+
693
+ The EML viewer displays decoded subjects, sender/recipient fields, dates,
694
+ plain-text and HTML bodies, all message headers, a bounded raw-source preview,
695
+ and attachments. It handles nested MIME messages, Base64/quoted-printable,
696
+ encoded header words, legacy body charsets, and extended attachment filenames.
697
+ Attached messages remain downloadable `.eml` files rather than becoming the
698
+ parent message's body. Extensionless messages are detected conservatively.
699
+
700
+ ```ts
701
+ import { mountEmlViewer } from 'omni-viewer-core/viewers/eml';
702
+ import { createCatalogI18n } from 'omni-viewer-core/i18n';
703
+
704
+ const handle = await mountEmlViewer(
705
+ { fileName: file.name, data: new Uint8Array(await file.arrayBuffer()) },
706
+ container,
707
+ {
708
+ assets: { resolveAssetUrl: async path => path },
709
+ i18n: createCatalogI18n('ko'),
710
+ logger: { log: (level, message) => console.log(level, message) },
711
+ // Optional: save: { saveFile: async (name, bytes, mimeType) => … },
712
+ // Optional: clipboard: { writeText: text => navigator.clipboard.writeText(text) }
713
+ }
714
+ );
715
+ // On file change / unmount:
716
+ handle.dispose();
717
+ ```
718
+
719
+ HTML runs in an opaque sandbox with scripts, navigation, forms, and remote
720
+ resources blocked. Embedded `cid:` raster images render locally. Attachment
721
+ filenames are sanitized; saving goes through the optional host `save` service.
722
+ Copy/save controls are disabled when their host service is absent. No MIME
723
+ dependency is required. Default parsing limits are 32 MiB input/decoded data,
724
+ 1,000 MIME parts, 32 nesting levels, and 4 MiB per body preview. Limits can be
725
+ configured through `parseEml(bytes, options)` from `parsers/eml`.
726
+
727
+ The default mount uses Shadow DOM. Scoped mounting requires
728
+ `omni-viewer-core/styles/eml.css`. Run `npm run build`, serve the repository
729
+ root, and open `examples/eml/` for sample messages and a local file picker.
730
+
492
731
  ### HAR network logs (.har)
493
732
 
494
733
  A `.har` file is the HTTP Archive a browser's network panel (or curl, Charles,
@@ -535,6 +774,46 @@ metadata get their own tabs. Routing is by the `.har` extension; an
535
774
  extensionless archive is recognized by content — a `log` object holding an
536
775
  `entries` array — and claimed before the generic JSON viewer.
537
776
 
777
+ ### JUnit XML test reports
778
+
779
+ A JUnit XML report is what nearly every test runner writes for CI: Gradle's
780
+ `build/test-results/**/TEST-*.xml`, Maven Surefire, `pytest --junitxml`,
781
+ vitest/Jest JUnit reporters, Go and Android tooling. It is an ordinary `.xml`,
782
+ so the generic XML viewer keeps the extension and `detectViewer` routes a file
783
+ to `junit` only when the supplied text sample has a `<testsuites>` root, or a
784
+ `<testsuite>` root carrying counts or a JUnit child element:
785
+
786
+ ```ts
787
+ import { detectViewer } from 'omni-viewer-core/registry';
788
+ import { mountJUnitViewer } from 'omni-viewer-core/viewers/junit';
789
+
790
+ const { viewerId } = detectViewer(file.name, undefined, undefined, undefined, text);
791
+ if (viewerId === 'junit') await mountJUnitViewer({ fileName: file.name, data: bytes }, container, { ...ctx, clipboard });
792
+ ```
793
+
794
+ `parseJUnit` is a contract parser (`ParseOutcome<JUnitDocument>`) built on the
795
+ core XML scanner, so it has no DOM dependency, never loads external entities,
796
+ and recovers a truncated report as `partial`. It reads nested suites, test cases
797
+ with their `failure`/`error`/`skipped` results (and Surefire's flaky/rerun
798
+ attempts), properties, and suite or case `system-out`/`system-err`. Writers
799
+ disagree about the declared totals, so every count is recomputed from the
800
+ cases; declared totals that differ are reported, not trusted. A `failure` or
801
+ `error` placed directly in a suite becomes a synthetic case so a setup error
802
+ shows in the list.
803
+
804
+ The viewer opens on the first failing test with a pass/fail verdict, an
805
+ outcome bar and summary figures (tests, passed, failed, errors, skipped, pass
806
+ rate, duration). The test list is filterable by status, suite subtree, and free
807
+ text over names, classes, and messages, with an opt-in toggle that extends the
808
+ search to stack traces and captured output. It is sortable by status severity,
809
+ name, suite, and time. The detail pane shows each result's message, type, and
810
+ trace, the captured output, and properties, and **Copy failure details** puts a
811
+ plain-text report on the clipboard. A suites tab lists nested suites with
812
+ rolled-up counts, and the source tab shows the report text. Defaults are 64 MiB
813
+ input, 100,000 cases, 64 KiB per trace or output block, and 16 MiB of captured
814
+ output in total. See the [viewer contract](docs/viewers/junit.md) and
815
+ [examples](examples/junit/README.md).
816
+
538
817
  ### EPUB e-books (.epub)
539
818
 
540
819
  EPUB 2.0.1 and EPUB 3.x books open in a reflowable reading pane with the
@@ -582,7 +861,7 @@ stored `mimetype` member.
582
861
  ### XML (.xml and XML dialects)
583
862
 
584
863
  Configuration and integration documents (`pom.xml`, Android manifests,
585
- MSBuild projects, SOAP/WSDL, RSS/Atom, XLIFF, KML/GPX…) open as an
864
+ MSBuild projects, SOAP/WSDL, RSS/Atom, XLIFF…) open as an
586
865
  element/attribute tree with a node inspector and the original source side by
587
866
  side:
588
867
 
@@ -606,8 +885,11 @@ comments and PIs, with next/previous and a matches-only filter. Copy gives the
606
885
  XPath (positions appear only where names repeat), the value, or the node's XML.
607
886
  Both panes are virtualized, and long single-line sources are split into
608
887
  display segments. Defaults are 64 MiB input, 500,000 nodes, and depth 1,000.
609
- ARXML and ReqIF keep their dedicated viewers by extension. An `.xml` whose root
610
- is an OpenVINO IR goes to the OpenVINO viewer when a text sample is provided.
888
+ ARXML and ReqIF keep their dedicated viewers by extension, and `.gpx`/`.kml`
889
+ open in the GPS map. When a text sample is provided, an `.xml` whose root is an
890
+ OpenVINO IR, a JUnit test report, a namespaced GPX/KML document or a draw.io
891
+ `<mxfile>`/`<mxGraphModel>` goes to the OpenVINO, JUnit, GPS or draw.io viewer;
892
+ a GPX/KML root without its namespace stays on this viewer.
611
893
 
612
894
  ### Archive host integration
613
895
 
@@ -2,6 +2,17 @@
2
2
 
3
3
  This distribution includes the following third-party software.
4
4
 
5
+ ## Three.js (optional peer)
6
+
7
+ - Project: Three.js (`three`)
8
+ - Copyright: 2010-2025 three.js authors
9
+ - License: MIT
10
+ - Use: 3D mesh rendering, GLB/glTF/STL/OBJ loaders and orbit controls
11
+ - Source: https://github.com/mrdoob/three.js
12
+
13
+ Three.js is supplied by the consuming host; the core does not redistribute its
14
+ runtime. Retain the upstream MIT license when bundling it in an application.
15
+
5
16
  ## Hugging Face GGUF parser
6
17
 
7
18
  - Project: `@huggingface/gguf` and `@huggingface/tasks`
@@ -55,3 +66,15 @@ rhwp project, used only for tests and never published to the npm package.
55
66
  - License: MIT
56
67
  - Source: https://github.com/edwardkim/rhwp (`rhwp-studio/public/samples/`,
57
68
  `samples/valign_fixtures/`)
69
+
70
+ ## JUnit example reports
71
+
72
+ `examples/junit/jenkins-nested-testsuites.xml` and
73
+ `examples/junit/jenkins-suite-error.xml` are unmodified test fixtures from the
74
+ Jenkins JUnit plugin, used for examples and tests and never published to the
75
+ npm package.
76
+
77
+ - Project: jenkinsci/junit-plugin (commit `04461ad56caf19b05bc16015f23675883df83d43`)
78
+ - Copyright: (c) 2004-2009, Sun Microsystems, Inc., Kohsuke Kawaguchi
79
+ - License: MIT
80
+ - Source: https://github.com/jenkinsci/junit-plugin/tree/04461ad56caf19b05bc16015f23675883df83d43/src/test/resources/hudson/tasks/junit
@@ -0,0 +1,256 @@
1
+ // Spectrogram computation, DOM-free and decoder-free.
2
+ //
3
+ // Ported from `native/audio-engine/audio_engine.c` (`generate_spectrogram`),
4
+ // which in turn replicates the WaveSurfer plugin's pipeline: Hann window →
5
+ // FFT → frequency-scale filter bank → dB → 8-bit colour index. Keeping the
6
+ // same pipeline means the windowed renderer looks like the whole-file one it
7
+ // replaces.
8
+ //
9
+ // The point of doing it here rather than in the plugin is the input: this takes
10
+ // *a window of samples*, so cost follows the viewport instead of the file. A
11
+ // two-hour track costs the same as a ten-second one.
12
+ const sinc = (x) => (x === 0 ? 1 : Math.sin(Math.PI * x) / (Math.PI * x));
13
+ /** Window coefficients. `alpha` is only consulted by blackman and gauss. */
14
+ export function windowCoefficients(size, kind = 'hann', alpha) {
15
+ const out = new Float32Array(size);
16
+ const last = size - 1;
17
+ for (let i = 0; i < size; i++) {
18
+ switch (kind) {
19
+ case 'rectangular':
20
+ out[i] = 1;
21
+ break;
22
+ case 'hann':
23
+ out[i] = 0.5 * (1 - Math.cos((2 * Math.PI * i) / last));
24
+ break;
25
+ case 'hamming':
26
+ out[i] = 0.54 - 0.46 * Math.cos((2 * Math.PI * i) / last);
27
+ break;
28
+ case 'bartlett':
29
+ out[i] = (2 / last) * (last / 2 - Math.abs(i - last / 2));
30
+ break;
31
+ case 'triangular':
32
+ out[i] = (2 / size) * (size / 2 - Math.abs(i - last / 2));
33
+ break;
34
+ case 'bartlettHann':
35
+ out[i] = 0.62 - 0.48 * Math.abs(i / last - 0.5) - 0.38 * Math.cos((2 * Math.PI * i) / last);
36
+ break;
37
+ case 'blackman': {
38
+ const a = alpha ?? 0.16;
39
+ out[i] = (1 - a) / 2
40
+ - 0.5 * Math.cos((2 * Math.PI * i) / last)
41
+ + (a / 2) * Math.cos((4 * Math.PI * i) / last);
42
+ break;
43
+ }
44
+ case 'cosine':
45
+ out[i] = Math.cos((i * Math.PI) / last - Math.PI / 2);
46
+ break;
47
+ case 'gauss': {
48
+ const a = alpha ?? 0.25;
49
+ out[i] = Math.exp(-0.5 * ((i - last / 2) / ((a * last) / 2)) ** 2);
50
+ break;
51
+ }
52
+ case 'lanczoz':
53
+ out[i] = sinc((2 * i) / last - 1);
54
+ break;
55
+ }
56
+ }
57
+ return out;
58
+ }
59
+ const DEFAULT_FFT_SIZE = 1024;
60
+ const DEFAULT_GAIN_DB = 20;
61
+ const DEFAULT_RANGE_DB = 80;
62
+ /** Floor for the log: silence would otherwise be -Infinity dB. */
63
+ const MIN_MAGNITUDE = 1e-12;
64
+ // Scale conversions, matching the plugin's own set.
65
+ const toMel = (hz) => 2595 * Math.log10(1 + hz / 700);
66
+ const fromMel = (mel) => 700 * (10 ** (mel / 2595) - 1);
67
+ const toBark = (hz) => {
68
+ let bark = (26.81 * hz) / (1960 + hz) - 0.53;
69
+ if (bark < 2)
70
+ bark += 0.15 * (2 - bark);
71
+ if (bark > 20.1)
72
+ bark += 0.22 * (bark - 20.1);
73
+ return bark;
74
+ };
75
+ const fromBark = (bark) => {
76
+ let value = bark;
77
+ if (value < 2)
78
+ value = (value - 0.3) / 0.85;
79
+ if (value > 20.1)
80
+ value = (value + 4.422) / 1.22;
81
+ return ((value + 0.53) / (26.28 - value)) * 1960;
82
+ };
83
+ const toLog = (hz) => Math.log10(Math.max(1, hz));
84
+ const fromLog = (value) => 10 ** value;
85
+ const ERB_SCALE = (1000 * Math.LN10) / 107.939;
86
+ const toErb = (hz) => ERB_SCALE * Math.log10(1 + 0.00437 * hz);
87
+ const fromErb = (erb) => (10 ** (erb / ERB_SCALE) - 1) / 0.00437;
88
+ const forward = (scale) => scale === 'mel' ? toMel : scale === 'bark' ? toBark : scale === 'erb' ? toErb
89
+ : scale === 'logarithmic' ? toLog : (hz) => hz;
90
+ const inverse = (scale) => scale === 'mel' ? fromMel : scale === 'bark' ? fromBark : scale === 'erb' ? fromErb
91
+ : scale === 'logarithmic' ? fromLog : (value) => value;
92
+ /**
93
+ * Frequency at a fraction of the height, 0 at the bottom.
94
+ *
95
+ * The axis is only readable with labels on it, and the labels have to follow
96
+ * whichever scale drew the picture — evenly spaced rows are far from evenly
97
+ * spaced hertz on mel or log.
98
+ */
99
+ export function frequencyAtFraction(fraction, scale, minHz, maxHz) {
100
+ const toScale = forward(scale);
101
+ const fromScale = inverse(scale);
102
+ const low = toScale(Math.max(0, minHz));
103
+ const high = toScale(Math.max(minHz, maxHz));
104
+ return fromScale(low + Math.min(1, Math.max(0, fraction)) * (high - low));
105
+ }
106
+ /** In-place radix-2 FFT. Tables are precomputed per size and reused. */
107
+ function createFft(size) {
108
+ if (size < 2 || (size & (size - 1)) !== 0)
109
+ throw new Error(`spectrogram: fftSize must be a power of two, got ${size}`);
110
+ const levels = Math.log2(size);
111
+ const cos = new Float32Array(size / 2);
112
+ const sin = new Float32Array(size / 2);
113
+ for (let i = 0; i < size / 2; i++) {
114
+ cos[i] = Math.cos((2 * Math.PI * i) / size);
115
+ sin[i] = Math.sin((2 * Math.PI * i) / size);
116
+ }
117
+ // Bit-reversal permutation, precomputed so the hot loop stays arithmetic.
118
+ const reversed = new Uint32Array(size);
119
+ for (let i = 0; i < size; i++) {
120
+ let bits = 0;
121
+ for (let bit = 0; bit < levels; bit++)
122
+ bits = (bits << 1) | ((i >>> bit) & 1);
123
+ reversed[i] = bits;
124
+ }
125
+ return (real, imag) => {
126
+ for (let i = 0; i < size; i++) {
127
+ const j = reversed[i];
128
+ if (j > i) {
129
+ let swap = real[i];
130
+ real[i] = real[j];
131
+ real[j] = swap;
132
+ swap = imag[i];
133
+ imag[i] = imag[j];
134
+ imag[j] = swap;
135
+ }
136
+ }
137
+ for (let span = 2; span <= size; span *= 2) {
138
+ const half = span / 2;
139
+ const step = size / span;
140
+ for (let start = 0; start < size; start += span) {
141
+ for (let i = start, k = 0; i < start + half; i++, k += step) {
142
+ const pair = i + half;
143
+ const tre = real[pair] * cos[k] + imag[pair] * sin[k];
144
+ const tim = -real[pair] * sin[k] + imag[pair] * cos[k];
145
+ real[pair] = real[i] - tre;
146
+ imag[pair] = imag[i] - tim;
147
+ real[i] = real[i] + tre;
148
+ imag[i] = imag[i] + tim;
149
+ }
150
+ }
151
+ }
152
+ };
153
+ }
154
+ /** FFT tables per size. A viewport is transformed as many small batches, and
155
+ * rebuilding the trig and bit-reversal tables for each one is pure overhead. */
156
+ const fftCache = new Map();
157
+ function fftFor(size) {
158
+ let fft = fftCache.get(size);
159
+ if (!fft) {
160
+ fft = createFft(size);
161
+ fftCache.set(size, fft);
162
+ }
163
+ return fft;
164
+ }
165
+ /** Where each output row samples the linear FFT bins, with the interpolation
166
+ * weights between the two neighbours. */
167
+ function filterBank(bins, fftSize, sampleRate, scale, minHz, maxHz) {
168
+ const toScale = forward(scale);
169
+ const fromScale = inverse(scale);
170
+ const nyquist = sampleRate / 2;
171
+ const low = toScale(Math.max(0, Math.min(minHz, nyquist)));
172
+ const high = toScale(Math.max(low, Math.min(maxHz, nyquist)));
173
+ const hzPerBin = sampleRate / fftSize;
174
+ const bank = new Array(bins);
175
+ for (let row = 0; row < bins; row++) {
176
+ const centerHz = fromScale(low + (row / bins) * (high - low));
177
+ const exact = centerHz / hzPerBin;
178
+ const lo = Math.floor(exact);
179
+ const frac = exact - lo;
180
+ bank[row] = { lo, weightLo: 1 - frac, weightHi: frac };
181
+ }
182
+ return bank;
183
+ }
184
+ /**
185
+ * Intensities for one window of interleaved-free mono samples.
186
+ *
187
+ * Channels are the caller's business: pass a single channel, or a downmix.
188
+ * Columns run left to right in time, rows low to high in frequency.
189
+ */
190
+ export function computeSpectrogram(samples, sampleRate, options = {}) {
191
+ const fftSize = options.fftSize ?? DEFAULT_FFT_SIZE;
192
+ const hopSize = options.hopSize ?? Math.max(1, Math.floor(fftSize / 4));
193
+ const bins = options.bins ?? fftSize / 2;
194
+ const scale = options.scale ?? 'mel';
195
+ const gainDb = options.gainDb ?? DEFAULT_GAIN_DB;
196
+ const rangeDb = options.rangeDb ?? DEFAULT_RANGE_DB;
197
+ if (hopSize < 1)
198
+ throw new Error('spectrogram: hopSize must be >= 1');
199
+ if (bins < 1)
200
+ throw new Error('spectrogram: bins must be >= 1');
201
+ if (rangeDb <= 0)
202
+ throw new Error('spectrogram: rangeDb must be > 0');
203
+ const fft = fftFor(fftSize);
204
+ const columns = samples.length >= fftSize
205
+ ? Math.floor((samples.length - fftSize) / hopSize) + 1
206
+ : 0;
207
+ const data = new Uint8Array(Math.max(0, columns) * bins);
208
+ if (columns <= 0)
209
+ return { columns: 0, bins, data };
210
+ const window = windowCoefficients(fftSize, options.windowFunc ?? 'hann', options.alpha);
211
+ const real = new Float32Array(fftSize);
212
+ const imag = new Float32Array(fftSize);
213
+ const halfBins = fftSize / 2;
214
+ const magnitudes = new Float32Array(halfBins);
215
+ const bank = filterBank(bins, fftSize, sampleRate, scale, options.frequencyMin ?? 0, options.frequencyMax ?? sampleRate / 2);
216
+ const norm = 2 / fftSize;
217
+ // White point sits `gainDb` below full scale and `rangeDb` more reaches
218
+ // black. This is the WaveSurfer plugin's convention (it maps with a white
219
+ // point of `-gainDB`); taking `gainDb` as an absolute ceiling instead put
220
+ // the floor 40 dB higher and rendered ordinary material almost entirely
221
+ // black.
222
+ const whiteDb = -gainDb;
223
+ const floorDb = whiteDb - rangeDb;
224
+ for (let column = 0; column < columns; column++) {
225
+ const offset = column * hopSize;
226
+ for (let i = 0; i < fftSize; i++) {
227
+ real[i] = samples[offset + i] * window[i];
228
+ imag[i] = 0;
229
+ }
230
+ fft(real, imag);
231
+ for (let bin = 0; bin < halfBins; bin++) {
232
+ const re = real[bin];
233
+ const im = imag[bin];
234
+ magnitudes[bin] = norm * Math.sqrt(re * re + im * im);
235
+ }
236
+ const base = column * bins;
237
+ for (let row = 0; row < bins; row++) {
238
+ const { lo, weightLo, weightHi } = bank[row];
239
+ const hi = lo + 1;
240
+ let magnitude;
241
+ if (lo >= halfBins)
242
+ magnitude = MIN_MAGNITUDE;
243
+ else if (hi >= halfBins)
244
+ magnitude = magnitudes[lo] * weightLo;
245
+ else
246
+ magnitude = magnitudes[lo] * weightLo + magnitudes[hi] * weightHi;
247
+ if (magnitude < MIN_MAGNITUDE)
248
+ magnitude = MIN_MAGNITUDE;
249
+ const db = 20 * Math.log10(magnitude);
250
+ data[base + row] = db <= floorDb ? 0
251
+ : db >= whiteDb ? 255
252
+ : Math.round(((db - floorDb) / rangeDb) * 255);
253
+ }
254
+ }
255
+ return { columns, bins, data };
256
+ }
@@ -0,0 +1,24 @@
1
+ // Worker shell for the spectrogram transform. Deliberately dumb, like
2
+ // audio_engine_worker.mjs: it owns no analysis logic, only the message
3
+ // plumbing, because everything here is outside the TypeScript build and
4
+ // therefore untypechecked and untested.
5
+ //
6
+ // The transform is the compiled `viewers/audio/spectrogram.ts`, shipped beside
7
+ // this file as `spectrogram_kernel.mjs` (see scripts/build-styles.mjs). Its URL
8
+ // arrives with each message rather than being resolved relative to this file:
9
+ // adapters that serve the core from blob URLs (obsidian) have no usable
10
+ // sibling paths.
11
+
12
+ let kernelPromise;
13
+
14
+ self.onmessage = async (event) => {
15
+ const { id, kernelUrl, samples, sampleRate, options } = event.data;
16
+ try {
17
+ kernelPromise ??= import(kernelUrl);
18
+ const { computeSpectrogram } = await kernelPromise;
19
+ const result = computeSpectrogram(samples, sampleRate, options);
20
+ self.postMessage({ id, ok: true, result }, [result.data.buffer]);
21
+ } catch (error) {
22
+ self.postMessage({ id, ok: false, error: String(error?.message ?? error) });
23
+ }
24
+ };