@cef-ai/widget-runtime 0.2.0 → 1.0.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.
@@ -63,37 +63,52 @@ export interface VaultStoreConfig {
63
63
  */
64
64
  credentialLabel?: string;
65
65
  }
66
- export interface VaultUploaderConfig extends VaultStoreConfig {
67
- /** Path prefix inside the bucket. Defaults to `widget-audio/<uuid>`. */
68
- keyPrefix?: string;
69
- /**
70
- * Read URL mode for downstream model fetches.
71
- *
72
- * Default `signed` keeps Vault audio private; the URL is bound to this
73
- * client's own credential (revoking that credential invalidates it).
74
- * `public` writes `x-amz-acl: public-read` and returns the plain object URL;
75
- * use only for controlled dev/test flows or a deliberate public asset policy.
76
- */
77
- readMode?: 'signed' | 'public';
78
- }
79
66
  export type VaultUploadFn = ((wav: ArrayBuffer, index: number) => Promise<string>) & {
80
67
  finalizeUrls: (urls: string[]) => Promise<string[]>;
81
68
  };
82
69
  /**
83
- * Creates a WAV uploader that writes to a DDC S3-compatible vault bucket using
84
- * the proven in-browser flow from the spike:
85
- * 1. Derive signer from embed wallet (ed25519_signRaw).
86
- * 2. GET /auth/info → gateway_public_key.
87
- * 3. Mint delegation token (subject = gateway_public_key).
88
- * 4. POST /auth/credentials with signed headers — always mints a NEW,
89
- * labeled credential for this uploader (cap-400 → reclaim our oldest
90
- * labeled row and retry once).
91
- * 5. Per segment: SHA-256 WAV bytes, PUT via SigV4-signed AwsClient.
92
- *
93
- * S3 credentials are memoized (shared credential core) so the signer is only
94
- * invoked once per uploader instance under normal operation.
70
+ * Structural subset of `vault.scope(name).objects` (`@cef-ai/vault-sdk`,
71
+ * ADR-002) the object uploader needs. Kept local — same rationale as
72
+ * `vault-client.ts`'s `VaultSdkLike` — so this stays unit-testable against a
73
+ * plain fake and decoupled from vault-sdk internals.
74
+ */
75
+ export interface VaultObjectsLike {
76
+ upload(path: string, data: Uint8Array, opts?: {
77
+ contentType?: string;
78
+ }): Promise<{
79
+ vaultPath: string;
80
+ }>;
81
+ presignedUrl(path: string, opts?: {
82
+ ttlSeconds?: number;
83
+ }): Promise<{
84
+ url: string;
85
+ expiresAt: string;
86
+ }>;
87
+ }
88
+ export interface VaultObjectUploaderConfig {
89
+ /** `vault.scope(manifest.scope).objects` — see ADR-002. */
90
+ objects: VaultObjectsLike;
91
+ /** Path prefix inside the scope's object namespace. Defaults to `widget-audio/<uuid>`. */
92
+ keyPrefix?: string;
93
+ /** TTL for the signed read URL handed back to the caller (model-side fetch). Server default if omitted. */
94
+ readTtlSeconds?: number;
95
+ }
96
+ /**
97
+ * Creates a WAV uploader on top of Vault API's object storage (ADR-002),
98
+ * replacing the direct-to-DDC-S3-Gateway SigV4 uploader this function used
99
+ * to be: no delegation token, no gateway credential, no client-side
100
+ * signing — every call is an ordinary authenticated Vault API request, the
101
+ * same auth the rest of the widget runtime already uses for cubby reads and
102
+ * publish. There is no credential to go stale mid-batch — but a chunk's
103
+ * presigned URL, minted right after ITS upload finishes, still has its own
104
+ * TTL clock ticking independent of auth: on a long capture, chunk 0's URL
105
+ * can age past `readTtlSeconds` (server default if unset) before the whole
106
+ * batch is even done uploading, let alone before whatever reads it (ASR)
107
+ * gets to it. So unlike the old uploader's no-op `finalizeUrls`, this one
108
+ * re-mints every chunk's URL right before handoff — every URL's TTL clock
109
+ * starts at the same moment, the end of the batch, not staggered across it.
95
110
  */
96
- export declare function createVaultUploader(cfg: VaultUploaderConfig): VaultUploadFn;
111
+ export declare function createVaultObjectUploader(cfg: VaultObjectUploaderConfig): VaultUploadFn;
97
112
  /** Read/write whole objects at caller-chosen keys in a DDC vault bucket. */
98
113
  export interface VaultObjectStore {
99
114
  /**
@@ -105,12 +120,19 @@ export interface VaultObjectStore {
105
120
  signGet(key: string, ttlSeconds?: number): Promise<string>;
106
121
  }
107
122
  /**
108
- * Whole-file companion to createVaultUploader: PUT an object at a STABLE,
109
- * caller-chosen key and re-sign GET URLs for it on demand. Used to persist a
110
- * durable interview recording (store the key, not the expiring presigned URL)
111
- * and re-sign playback URLs from the candidate modal — same private bucket,
112
- * same wallet, so the grant can always be re-minted. Shares the auth flow with
113
- * the segment uploader via createCredCore.
123
+ * PUT an object at a STABLE, caller-chosen key and re-sign GET URLs for it on
124
+ * demand — direct to the DDC S3 Gateway via SigV4, the same pattern
125
+ * `createVaultObjectUploader` above replaces for WAV chunk uploads (ADR-002:
126
+ * company-memory-bank 04-cef/user-vault/decisions/ADR-002-vault-object-storage.md).
127
+ *
128
+ * **Not yet migrated.** This function has no known consumer inside this
129
+ * repo — it's exported public API with no first-party caller to migrate
130
+ * alongside `createVaultUploader`. Left in place rather than migrated
131
+ * speculatively or deleted blind (removing exported API without knowing its
132
+ * consumers is its own risk). If you're adding a caller, prefer Vault API
133
+ * object storage (`vault.scope(name).objects.upload/presignedUrl`) over this —
134
+ * this function is the same abstraction leak the ADR closes, just not yet
135
+ * closed here.
114
136
  */
115
137
  export declare function createVaultObjectStore(cfg: VaultStoreConfig): VaultObjectStore;
116
138
  export {};
@@ -120,8 +120,10 @@ async function registerCredentials(ctx, delegationToken, label) {
120
120
  }
121
121
  /**
122
122
  * Acquire vault-bucket S3 credentials once and expose a memoized AwsClient.
123
- * Shared by createVaultObjectStore AND createVaultUploader; the in-browser
124
- * auth flow —
123
+ * Used by createVaultObjectStore (the direct-to-DDC-S3-Gateway whole-object
124
+ * store — see its own doc comment for why this SigV4 path still exists
125
+ * post-ADR-002, unlike the WAV chunk uploader above which now goes through
126
+ * Vault API object storage instead). The in-browser auth flow —
125
127
  * 1. GET /auth/info → gateway_public_key.
126
128
  * 2. Mint delegation token (subject = gateway_public_key).
127
129
  * 3. POST /auth/credentials with signed headers — always mints a NEW,
@@ -132,7 +134,12 @@ async function registerCredentials(ctx, delegationToken, label) {
132
134
  * revoke-each-other race anymore.
133
135
  */
134
136
  function createCredCore(cfg) {
135
- const fetchImpl = cfg.fetchImpl ?? globalThis.fetch;
137
+ // Bind to globalThis: browsers (Blink/WebKit) brand-check fetch's receiver
138
+ // against window and throw "Illegal invocation" when it's called detached
139
+ // (`const f = globalThis.fetch; f(...)`). Node's fetch doesn't brand-check and
140
+ // tests inject cfg.fetchImpl, so an unbound default stayed green in CI but
141
+ // broke every real browser upload on the first /auth call.
142
+ const fetchImpl = cfg.fetchImpl ?? globalThis.fetch.bind(globalThis);
136
143
  const awsClientFactory = cfg.awsClientFactory ?? ((creds) => new AwsClient({ accessKeyId: creds.accessKeyId, secretAccessKey: creds.secretAccessKey, region: creds.region, service: 's3', ...(creds.sessionToken ? { sessionToken: creds.sessionToken } : {}) }));
137
144
  const label = cfg.credentialLabel ?? CREDENTIAL_LABEL;
138
145
  const ctx = {
@@ -224,109 +231,48 @@ async function putWithRetry(core, url, body, contentType, onAttempt) {
224
231
  throw new Error(`PUT ${url} → ${res.status} ${await res.text()}`);
225
232
  }
226
233
  /**
227
- * Creates a WAV uploader that writes to a DDC S3-compatible vault bucket using
228
- * the proven in-browser flow from the spike:
229
- * 1. Derive signer from embed wallet (ed25519_signRaw).
230
- * 2. GET /auth/info → gateway_public_key.
231
- * 3. Mint delegation token (subject = gateway_public_key).
232
- * 4. POST /auth/credentials with signed headers — always mints a NEW,
233
- * labeled credential for this uploader (cap-400 → reclaim our oldest
234
- * labeled row and retry once).
235
- * 5. Per segment: SHA-256 WAV bytes, PUT via SigV4-signed AwsClient.
236
- *
237
- * S3 credentials are memoized (shared credential core) so the signer is only
238
- * invoked once per uploader instance under normal operation.
234
+ * Creates a WAV uploader on top of Vault API's object storage (ADR-002),
235
+ * replacing the direct-to-DDC-S3-Gateway SigV4 uploader this function used
236
+ * to be: no delegation token, no gateway credential, no client-side
237
+ * signing — every call is an ordinary authenticated Vault API request, the
238
+ * same auth the rest of the widget runtime already uses for cubby reads and
239
+ * publish. There is no credential to go stale mid-batch — but a chunk's
240
+ * presigned URL, minted right after ITS upload finishes, still has its own
241
+ * TTL clock ticking independent of auth: on a long capture, chunk 0's URL
242
+ * can age past `readTtlSeconds` (server default if unset) before the whole
243
+ * batch is even done uploading, let alone before whatever reads it (ASR)
244
+ * gets to it. So unlike the old uploader's no-op `finalizeUrls`, this one
245
+ * re-mints every chunk's URL right before handoff — every URL's TTL clock
246
+ * starts at the same moment, the end of the batch, not staggered across it.
239
247
  */
240
- export function createVaultUploader(cfg) {
241
- const core = createCredCore(cfg);
242
- // Compute keyPrefix once per instance (not per call).
248
+ export function createVaultObjectUploader(cfg) {
243
249
  const keyPrefix = cfg.keyPrefix ?? `widget-audio/${crypto.randomUUID()}`;
244
- const uploadedObjects = new Map();
245
- // Presign (signed mode) or return the plain public URL. We deliberately do NOT
246
- // read-back-verify each chunk: a credential refresh mid-batch (the PUT-retry
247
- // path below) invalidates URLs presigned with the replaced credential — an
248
- // intermittent SignatureDoesNotMatch on a verify GET that has no business
249
- // failing the upload. `finalizeUrls` re-signs every object with the final
250
- // stable credential after all chunks land, and the ASR stage retries its own
251
- // reads; that is the stable path the pre-refactor uploader used (see
252
- // docs/engineering/known-issues/2026-06-01-widget-audio-private-bucket-asr-403.md).
253
- async function readableUrlForObject(aws, objectUrl) {
254
- if (cfg.readMode === 'public')
255
- return objectUrl;
256
- const signed = await aws.sign(`${objectUrl}?X-Amz-Expires=${core.readTtl}`, { aws: { signQuery: true } });
257
- return signed.url;
258
- }
250
+ const presignOpts = cfg.readTtlSeconds ? { ttlSeconds: cfg.readTtlSeconds } : {};
259
251
  const uploadWav = (async function uploadWav(wav, index) {
260
- const wavBytes = new Uint8Array(wav);
261
- const contentSha256 = await sha256Hex(wavBytes);
262
- // SigV4 region from the bucket (cfg.region), not creds.region.
263
- const region = cfg.region;
264
- const url = `${cfg.gatewayUrl}/${cfg.bucketId}/${keyPrefix}/${index}.wav`;
265
- // Bounded 401/403 retry with backoff. Under the multi-credential gateway a
266
- // 403 no longer means a concurrent session revoked our credential, so the
267
- // first retry RE-USES the already-minted credential (transient gateway /
268
- // propagation hiccups). If the failure persists past attempt 2, refresh the
269
- // credential (the underlying DDC delegation token may have expired); the
270
- // replaced row is revoked by id, so retries never leak rows toward the cap.
271
- const maxAttempts = 4;
272
- let lastStatus = 0;
273
- let lastText = '';
274
- for (let attempt = 1; attempt <= maxAttempts; attempt++) {
275
- const aws = await core.aws();
276
- // NOTE: never log the RETURNED presigned url — it carries a bearer SigV4 sig.
277
- console.log('[wr-up] PUT region=', region, 'bucket=', cfg.bucketId, 'index=', index, 'attempt=', attempt, 'bytes=', wavBytes.length, 'sha=', contentSha256.slice(0, 16));
278
- const putRes = await aws.fetch(url, {
279
- method: 'PUT',
280
- // Send a Blob (an immutable, exact byte sequence) rather than the Uint8Array
281
- // view: the browser transmits EXACTLY the bytes we hashed, so the gateway's
282
- // recomputed SHA256(body) matches the signed X-Amz-Content-Sha256. Passing a
283
- // typed-array view to fetch() can transmit its whole backing ArrayBuffer in
284
- // the browser (not in Node) — that skew was the SignatureDoesNotMatch on
285
- // segmented chunks. The gateway rejects UNSIGNED-PAYLOAD, so we sign the real
286
- // content hash.
287
- body: new Blob([wavBytes], { type: 'audio/wav' }),
288
- headers: {
289
- 'Content-Type': 'audio/wav',
290
- 'X-Amz-Content-Sha256': contentSha256,
291
- ...(cfg.readMode === 'public' ? { 'x-amz-acl': 'public-read' } : {}),
292
- },
293
- });
294
- if (putRes.ok) {
295
- uploadedObjects.set(index, url);
296
- return await readableUrlForObject(aws, url);
297
- }
298
- lastStatus = putRes.status;
299
- lastText = await putRes.text();
300
- if ((putRes.status === 403 || putRes.status === 401) && attempt < maxAttempts) {
301
- if (attempt >= 2)
302
- await core.refresh();
303
- await new Promise((r) => setTimeout(r, 600 * attempt));
304
- continue;
305
- }
306
- break;
307
- }
308
- throw new Error(`PUT ${url} → ${lastStatus} ${lastText}`);
252
+ const path = `${keyPrefix}/${index}.wav`;
253
+ await cfg.objects.upload(path, new Uint8Array(wav), { contentType: 'audio/wav' });
254
+ const signed = await cfg.objects.presignedUrl(path, presignOpts);
255
+ return signed.url;
309
256
  });
310
257
  uploadWav.finalizeUrls = async (urls) => {
311
- if (uploadedObjects.size === 0)
312
- return urls;
313
- const aws = await core.aws();
314
- const entries = [...uploadedObjects.entries()].sort(([a], [b]) => a - b);
315
- const signed = [];
316
- for (const [, objectUrl] of entries) {
317
- signed.push(await readableUrlForObject(aws, objectUrl));
318
- }
319
- return signed;
258
+ return Promise.all(urls.map(async (_url, index) => (await cfg.objects.presignedUrl(`${keyPrefix}/${index}.wav`, presignOpts)).url));
320
259
  };
321
260
  return uploadWav;
322
261
  }
323
262
  /**
324
- * Whole-file companion to createVaultUploader: PUT an object at a STABLE,
325
- * caller-chosen key and re-sign GET URLs for it on demand. Used to persist a
326
- * durable interview recording (store the key, not the expiring presigned URL)
327
- * and re-sign playback URLs from the candidate modal — same private bucket,
328
- * same wallet, so the grant can always be re-minted. Shares the auth flow with
329
- * the segment uploader via createCredCore.
263
+ * PUT an object at a STABLE, caller-chosen key and re-sign GET URLs for it on
264
+ * demand — direct to the DDC S3 Gateway via SigV4, the same pattern
265
+ * `createVaultObjectUploader` above replaces for WAV chunk uploads (ADR-002:
266
+ * company-memory-bank 04-cef/user-vault/decisions/ADR-002-vault-object-storage.md).
267
+ *
268
+ * **Not yet migrated.** This function has no known consumer inside this
269
+ * repo — it's exported public API with no first-party caller to migrate
270
+ * alongside `createVaultUploader`. Left in place rather than migrated
271
+ * speculatively or deleted blind (removing exported API without knowing its
272
+ * consumers is its own risk). If you're adding a caller, prefer Vault API
273
+ * object storage (`vault.scope(name).objects.upload/presignedUrl`) over this —
274
+ * this function is the same abstraction leak the ADR closes, just not yet
275
+ * closed here.
330
276
  */
331
277
  export function createVaultObjectStore(cfg) {
332
278
  const core = createCredCore(cfg);
@@ -37,8 +37,9 @@ export declare function toVaultSdkSigner(pubkey: string, sigType: 'ed25519' | 's
37
37
  */
38
38
  export declare function toClientSdkWallet(pubkey: string, sigType: 'ed25519' | 'sr25519', signFn: (bytes: Uint8Array) => Promise<Uint8Array>): VaultSignedWallet;
39
39
  /**
40
- * Build the signerPair shape that `createVaultUploader` (and `mintDelegationToken`)
41
- * accept, from a bridge pubkey + sign function.
40
+ * Build the signerPair shape that the SigV4-signing DDC S3 Gateway clients
41
+ * (`createVaultObjectStore`, `mintDelegationToken`) accept, from a bridge
42
+ * pubkey + sign function.
42
43
  */
43
44
  export declare function toUploaderSignerPair(pubkey: string, signFn: (bytes: Uint8Array) => Promise<Uint8Array>): {
44
45
  publicKey: Uint8Array;
@@ -5,7 +5,11 @@
5
5
  //
6
6
  // toVaultSdkSigner → @cef-ai/signer `Signer` (type/address/publicKey/isReady/sign)
7
7
  // toClientSdkWallet → @cef-ai/client-sdk VaultSignedWallet
8
- // toUploaderSignerPair → { publicKey: Uint8Array; sign } for createVaultUploader
8
+ // toUploaderSignerPair → { publicKey: Uint8Array; sign } for the SigV4-signing
9
+ // DDC S3 Gateway clients (createVaultObjectStore,
10
+ // mintDelegationToken) — NOT for WAV chunk uploads,
11
+ // which go through createVaultObjectUploader (Vault
12
+ // API object storage, ADR-002) and need no signer.
9
13
  //
10
14
  // All hex values in the client-sdk shape are WITHOUT a 0x prefix, matching
11
15
  // the proven embed-wallet behaviour (see embed-adapter.ts history).
@@ -69,8 +73,9 @@ export function toClientSdkWallet(pubkey, sigType, signFn) {
69
73
  };
70
74
  }
71
75
  /**
72
- * Build the signerPair shape that `createVaultUploader` (and `mintDelegationToken`)
73
- * accept, from a bridge pubkey + sign function.
76
+ * Build the signerPair shape that the SigV4-signing DDC S3 Gateway clients
77
+ * (`createVaultObjectStore`, `mintDelegationToken`) accept, from a bridge
78
+ * pubkey + sign function.
74
79
  */
75
80
  export function toUploaderSignerPair(pubkey, signFn) {
76
81
  return {
package/dist/entry.js CHANGED
@@ -1,11 +1,11 @@
1
1
  // packages/widget-runtime/src/entry.ts
2
2
  import { VaultSDK } from '@cef-ai/vault-sdk';
3
3
  import { createHostBridge } from './host-bridge.js';
4
- import { toVaultSdkSigner, toUploaderSignerPair } from './bridge-wallet.js';
4
+ import { toVaultSdkSigner } from './bridge-wallet.js';
5
5
  import { connectStandaloneWallet } from './standalone-wallet.js';
6
6
  import { createVaultClient } from './vault-client.js';
7
7
  import { createWidgetRuntime } from './runtime.js';
8
- import { captureAndUploadAudio, createVaultUploader } from './audio/capture-upload.js';
8
+ import { captureAndUploadAudio, createVaultObjectUploader } from './audio/capture-upload.js';
9
9
  import { decodeToMono16k } from './audio/segment.js';
10
10
  import { registerCompositePanel } from './kinds/composite.js';
11
11
  /**
@@ -91,16 +91,24 @@ function boot() {
91
91
  // The uploader resolves its identity lazily via the shared (memoized)
92
92
  // resolver on the first upload — it never blocks WidgetRuntime exposure and
93
93
  // reuses the same host-or-wallet signer as the vault client.
94
+ //
95
+ // ADR-002 (company-memory-bank 04-cef/user-vault/decisions/): chunks upload
96
+ // through Vault API object storage, not the DDC S3 Gateway directly — no
97
+ // delegation token, no gateway credential, no client-side SigV4 signing.
98
+ // The `vault.scope(...).objects` handle is real vault-sdk (not the
99
+ // structural VaultClient abstraction createVaultClient wraps), so it's
100
+ // built directly here from the same signer + endpoints as the main vault
101
+ // client above.
94
102
  let mountDeps;
95
103
  if (manifest.kind === 'submit' || manifest.kind === 'conversation' || manifest.kind === 'composite') {
96
- // Memoize: build uploader once, reuse across multiple uploads in same session.
104
+ // Memoize: build the uploader once, reuse across multiple uploads in the
105
+ // same session (mirrors the pre-ADR-002 uploader's memoization).
97
106
  let uploaderP = null;
98
107
  const getUploader = () => {
99
108
  if (uploaderP)
100
109
  return uploaderP;
101
110
  uploaderP = (async () => {
102
111
  const id = await resolveSigningIdentity();
103
- const signerPair = toUploaderSignerPair(id.publicKey, id.sign);
104
112
  const vaultSdkSigner = toVaultSdkSigner(id.publicKey, id.sigType, id.sign);
105
113
  const sdk = new VaultSDK({
106
114
  endpoint: manifest.endpoints.vault,
@@ -110,13 +118,7 @@ function boot() {
110
118
  signer: vaultSdkSigner,
111
119
  });
112
120
  const ensuredVault = await sdk.vault.ensure({ onboard: false });
113
- return createVaultUploader({
114
- signerPair,
115
- sigType: id.sigType,
116
- gatewayUrl: manifest.endpoints.s3GatewayAuthInfo.replace(/\/auth\/info\/?$/, ''),
117
- bucketId: ensuredVault.bucketId,
118
- region: ensuredVault.region,
119
- });
121
+ return createVaultObjectUploader({ objects: ensuredVault.scope(manifest.scope).objects });
120
122
  })().catch((err) => { uploaderP = null; throw err; });
121
123
  return uploaderP;
122
124
  };
package/dist/index.d.ts CHANGED
@@ -13,8 +13,8 @@ export { renderConversation } from './kinds/conversation.js';
13
13
  export { renderComposite, registerCompositePanel, getCompositePanel, type CompositePanelRenderer } from './kinds/composite.js';
14
14
  export * from './render-helpers.js';
15
15
  export type { WidgetKind, WidgetConfig, RecordConfig, ListConfig, DashboardConfig, SubmitConfig, ConversationConfig, CustomPanelConfig, CompositePanel, CompositeConfig } from './types.js';
16
- export { captureAndUploadAudio, createVaultUploader, createVaultObjectStore } from './audio/capture-upload.js';
17
- export type { CaptureUploadInput, CaptureUploadDeps, VaultUploaderConfig, VaultStoreConfig, VaultObjectStore } from './audio/capture-upload.js';
16
+ export { captureAndUploadAudio, createVaultObjectUploader, createVaultObjectStore } from './audio/capture-upload.js';
17
+ export type { CaptureUploadInput, CaptureUploadDeps, VaultObjectUploaderConfig, VaultObjectsLike, VaultStoreConfig, VaultObjectStore } from './audio/capture-upload.js';
18
18
  export { mintDelegationToken, Operation } from './audio/ddc-delegation.js';
19
19
  export type { AsyncSignerPair, MintOpts } from './audio/ddc-delegation.js';
20
20
  export { encodeWav } from './audio/wav.js';
package/dist/index.js CHANGED
@@ -11,7 +11,7 @@ export { renderSubmit } from './kinds/submit.js';
11
11
  export { renderConversation } from './kinds/conversation.js';
12
12
  export { renderComposite, registerCompositePanel, getCompositePanel } from './kinds/composite.js';
13
13
  export * from './render-helpers.js';
14
- export { captureAndUploadAudio, createVaultUploader, createVaultObjectStore } from './audio/capture-upload.js';
14
+ export { captureAndUploadAudio, createVaultObjectUploader, createVaultObjectStore } from './audio/capture-upload.js';
15
15
  export { mintDelegationToken, Operation } from './audio/ddc-delegation.js';
16
16
  export { encodeWav } from './audio/wav.js';
17
17
  export { segmentPcm, decodeToMono16k } from './audio/segment.js';