@sorisdk/web-audio 0.6.3 → 0.6.5

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.
package/README.md CHANGED
@@ -22,7 +22,7 @@ configuration, or Node-based asset server is required:
22
22
  ```html
23
23
  <script type="module">
24
24
  import { AudioRecognizer } from
25
- "https://cdn.iplateia.com/web/sorisdk/v0.6.3/sori-web-audio.mjs";
25
+ "https://cdn.iplateia.com/web/sorisdk/v0.6.5/sori-web-audio.mjs";
26
26
 
27
27
  const recognizer = new AudioRecognizer({
28
28
  appId: "YOUR_APP_ID",
@@ -49,7 +49,7 @@ configuration, or Node-based asset server is required:
49
49
  ```
50
50
 
51
51
  Static URL imports are valid inside `<script type="module">`. Dynamic
52
- `await import("https://cdn.iplateia.com/web/sorisdk/v0.6.3/sori-web-audio.mjs")`
52
+ `await import("https://cdn.iplateia.com/web/sorisdk/v0.6.5/sori-web-audio.mjs")`
53
53
  is an optional alternative when the SDK should be loaded conditionally.
54
54
 
55
55
  Pin an exact version in production. Do not construct a mutable `latest` URL.
@@ -93,7 +93,7 @@ An import map can give the standalone URL the npm package name:
93
93
  <script type="importmap">
94
94
  {
95
95
  "imports": {
96
- "@sorisdk/web-audio": "https://cdn.iplateia.com/web/sorisdk/v0.6.3/sori-web-audio.mjs"
96
+ "@sorisdk/web-audio": "https://cdn.iplateia.com/web/sorisdk/v0.6.5/sori-web-audio.mjs"
97
97
  }
98
98
  }
99
99
  </script>
@@ -142,7 +142,7 @@ const recognizer = new AudioRecognizer({
142
142
  });
143
143
 
144
144
  recognizer.on("campaign", (event) => {
145
- console.log(event.campaign);
145
+ console.log(event.activityId, event.campaign);
146
146
  });
147
147
 
148
148
  recognizer.on("error", ({ error }) => {
@@ -159,6 +159,119 @@ await recognizer.stop();
159
159
  await recognizer.destroy();
160
160
  ```
161
161
 
162
+ ## Activity refinement
163
+
164
+ The default `AudioRecognizer` activity reporter represents one fingerprint
165
+ recognition and its later audio-marker enrichment as one server activity:
166
+
167
+ 1. A marker-free match with an active marker-scan request is created with
168
+ `POST /api/activity/` and top-level `refinement_expected: true`.
169
+ 2. The returned `activity_id` is retained for 30 seconds.
170
+ 3. If the same recognition gains `audioMarker.code`, the SDK sends
171
+ `PUT /api/activity/{activity_id}` with the same material and
172
+ `trait.marker`.
173
+ 4. The refined `campaign` event keeps the same non-empty `activityId`, so an
174
+ application can replace the earlier campaign row deterministically.
175
+
176
+ A marker present on the first match is included in the POST and does not cause
177
+ a PUT or include `refinement_expected`. Recognition without an active marker
178
+ scan, plus legacy/custom POST-only reporters, also omit the opt-in flag.
179
+ Marker-state `audiomarker` events remain independently observable; they do not
180
+ mean that server activity refinement completed.
181
+
182
+ Network and 5xx PUT failures are retried at most twice within the original
183
+ 30-second deadline. Server responses 404, 409, and 410 are terminal. An
184
+ expired or failed refinement is dropped and never becomes a second POST.
185
+ Stopping, clearing, destroying, or replacing the authenticated capture clears
186
+ pending refinement identity.
187
+
188
+ Custom `activityReporter` configurations remain POST-only by default. Enable
189
+ refinement explicitly and, if needed, provide operation-specific transport or
190
+ mapping hooks:
191
+
192
+ ```ts
193
+ const recognizer = new AudioRecognizer({
194
+ appId: "YOUR_APP_ID",
195
+ ephemeralKey: fetchEphemeralKeyFromYourServer,
196
+ activityReporter: {
197
+ endpoint: "https://example.com/activity/",
198
+ refinement: {
199
+ endpoint: (activityId) =>
200
+ `https://example.com/activity/${encodeURIComponent(activityId)}`,
201
+ maxAttempts: 2,
202
+ shouldRetry: ({ status }) => status === null || status >= 500
203
+ }
204
+ }
205
+ });
206
+ ```
207
+
208
+ Existing `mapMatchToRequest` and `mapResponseToCampaign` hooks continue to
209
+ handle POST. Additive refinement hooks can override the PUT payload, response
210
+ mapping, endpoint, headers, fetcher, and retry policy. The stable terminal
211
+ statuses 404, 409, and 410 are never retried even if a custom policy returns
212
+ `true`.
213
+
214
+ ## Session identifiers
215
+
216
+ `AudioRecognizer` creates one pseudonymous session identifier per application
217
+ and stores it in `localStorage` under
218
+ `sorisdk:web-audio:session:<appId>`. The value persists until the origin's
219
+ storage is cleared or the session manager's `clear()` method is called. An
220
+ upgrade does not rotate an existing non-empty value, including identifiers
221
+ created by older SDK versions.
222
+
223
+ New identifiers use `crypto.randomUUID()`. Browsers without `randomUUID()` use
224
+ `crypto.getRandomValues()` to create an RFC 4122 UUID v4. If neither secure API
225
+ is available, session creation fails instead of falling back to predictable
226
+ randomness. A host for such an environment must provide a cryptographically
227
+ secure generator explicitly:
228
+
229
+ ```ts
230
+ import {
231
+ AudioRecognizer,
232
+ createLocalStorageSessionManager
233
+ } from "@sorisdk/web-audio";
234
+
235
+ const sessionManager = createLocalStorageSessionManager({
236
+ key: "sorisdk:web-audio:session:YOUR_APP_ID",
237
+ generateSessionId: () => secureSessionIdFromYourRuntime()
238
+ });
239
+
240
+ const recognizer = new AudioRecognizer({
241
+ appId: "YOUR_APP_ID",
242
+ ephemeralKey: fetchEphemeralKeyFromYourServer,
243
+ sessionManager
244
+ });
245
+
246
+ // Stop recognition before intentionally rotating the identifier.
247
+ await recognizer.destroy();
248
+ await sessionManager.clear?.();
249
+ ```
250
+
251
+ Treat the identifier as persistent pseudonymous data: do not include personal
252
+ information in a custom value, and do not log or expose it unnecessarily.
253
+ Clearing it breaks device and activity continuity; the next authentication
254
+ creates a new server-side device identity.
255
+
256
+ ### SORI service contract
257
+
258
+ The current SORI service uses the identifier at these boundaries:
259
+
260
+ | Boundary | Use of `sessionId` | Security contract |
261
+ | --- | --- | --- |
262
+ | Authentication request | Debounces repeated requests and upserts an account-scoped device record | A valid application ID plus secret or ephemeral key is still required |
263
+ | Session token | Stored as the signed device claim | Possession of the raw identifier does not create or validate a token |
264
+ | Recognition activity | Attributes impressions, clicks, campaign links, and campaign webhooks to a device | Authorization comes from the signed token, not the identifier |
265
+ | Monitoring | Binds stored health and transition data to the token's device claim | A submitted device ID must match the signed claim |
266
+ | Authentication webhook | Populates the webhook `device_id` | The value is correlation data, not a webhook credential |
267
+ | Quota and throttling | Selects the authentication debounce bucket only | It is not a standalone billing or usage quota key |
268
+ | AudioPack caching | Not used as a cache partition | The browser cache remains application-scoped |
269
+ | Replay protection | No use | The stable identifier provides neither freshness nor replay protection |
270
+
271
+ Authorization, quota enforcement, cache isolation, and replay controls must
272
+ remain bound to authenticated server-side state rather than possession or
273
+ unpredictability of `sessionId`.
274
+
162
275
  ## Advanced wasm loading overrides
163
276
 
164
277
  If you need to pin explicit generated modules, use the nested `wasm` options:
package/dist/index.d.ts CHANGED
@@ -62,6 +62,11 @@ interface AudioMarkerEvent {
62
62
  interface MaterialActivityRequestPayload {
63
63
  type: "material";
64
64
  material_id: string;
65
+ refinement_expected?: true;
66
+ trait?: {
67
+ marker?: string;
68
+ [key: string]: unknown;
69
+ };
65
70
  metadata?: unknown;
66
71
  }
67
72
  interface MicrophoneMatcherCampaignEvent {
@@ -70,6 +75,27 @@ interface MicrophoneMatcherCampaignEvent {
70
75
  activityId: string | null;
71
76
  raw: Record<string, unknown>;
72
77
  }
78
+ interface ActivityRefinementRequestContext {
79
+ operation: "refine";
80
+ activityId: string;
81
+ attempt: number;
82
+ deadline: number;
83
+ }
84
+ interface ActivityRefinementFailureContext extends ActivityRefinementRequestContext {
85
+ error: Error;
86
+ status: number | null;
87
+ detail: string | null;
88
+ }
89
+ interface MicrophoneMatcherActivityRefinementOptions {
90
+ endpoint?: string | URL | ((activityId: string) => string | URL | Promise<string | URL>);
91
+ fetch?: typeof fetch;
92
+ headers?: HeadersInit | (() => HeadersInit | Promise<HeadersInit>);
93
+ mapMatchToRequest?: (match: DetectedMatch, context: ActivityRefinementRequestContext) => MaterialActivityRequestPayload | null | Promise<MaterialActivityRequestPayload | null>;
94
+ mapResponseToCampaign?: (response: Record<string, unknown>, match: DetectedMatch, context: ActivityRefinementRequestContext) => MicrophoneMatcherCampaignEvent | null | Promise<MicrophoneMatcherCampaignEvent | null>;
95
+ shouldRetry?: (context: ActivityRefinementFailureContext) => boolean | Promise<boolean>;
96
+ maxAttempts?: number;
97
+ now?: () => number;
98
+ }
73
99
  interface MicrophoneMatcherActivityReporterOptions {
74
100
  endpoint: string | URL;
75
101
  token: string | (() => string | Promise<string>);
@@ -77,6 +103,7 @@ interface MicrophoneMatcherActivityReporterOptions {
77
103
  headers?: HeadersInit | (() => HeadersInit | Promise<HeadersInit>);
78
104
  mapMatchToRequest?: (match: DetectedMatch) => MaterialActivityRequestPayload | null | Promise<MaterialActivityRequestPayload | null>;
79
105
  mapResponseToCampaign?: (response: Record<string, unknown>, match: DetectedMatch) => MicrophoneMatcherCampaignEvent | null | Promise<MicrophoneMatcherCampaignEvent | null>;
106
+ refinement?: false | MicrophoneMatcherActivityRefinementOptions;
80
107
  }
81
108
  interface CreateMicrophoneMatcherWasmOptions {
82
109
  afpgen?: InitAfpgenOptions;
@@ -182,6 +209,8 @@ declare class AudioRecognizer {
182
209
  private setAuthResult;
183
210
  }
184
211
 
212
+ declare const ACTIVITY_REFINEMENT_WINDOW_MS = 30000;
213
+
185
214
  declare class MicrophoneMatcher {
186
215
  private readonly events;
187
216
  private readonly matcherSession;
@@ -193,6 +222,7 @@ declare class MicrophoneMatcher {
193
222
  private readonly captureSampleRate;
194
223
  private readonly audioGraphFactory;
195
224
  private readonly packSource;
225
+ private readonly activityRefinement;
196
226
  private readonly options;
197
227
  private extractor;
198
228
  private graphController;
@@ -211,6 +241,7 @@ declare class MicrophoneMatcher {
211
241
  private latestAudioMarker;
212
242
  private latestAudioMarkerDetection;
213
243
  private audioMarkerRequestId;
244
+ private activityRecognitionId;
214
245
  private hopSize;
215
246
  private diagnostics;
216
247
  constructor(options: CreateMicrophoneMatcherOptions);
@@ -260,4 +291,4 @@ declare function requestMicrophoneStream(mediaDevices: MediaDevices, mediaConstr
260
291
  preferredSampleRate?: number;
261
292
  }): Promise<MediaStream>;
262
293
 
263
- export { type AudioMarkerEvent, AudioRecognizer, type AudioRecognizerAuthOptions, type AudioRecognizerOptions, type AuthenticateAndLoadAudioPackOptions, type AuthenticateAndLoadAudioPackResult, type BrowserAudioPackLoader, type BrowserAudioPackState, type BrowserAudioPackStateStore, type BrowserSessionManager, type CreateLocalStorageAudioPackStoreOptions, type CreateLocalStorageSessionManagerOptions, type CreateMicrophoneMatcherOptions, type CreateMicrophoneMatcherWasmOptions, DEFAULT_BROWSER_AUDIOPACK_VERSION, DEFAULT_SORI_API_ENDPOINT, type DetectedMatch, type MaterialActivityRequestPayload, MicrophoneMatcher, type MicrophoneMatcherActivityReporterOptions, type MicrophoneMatcherCampaignEvent, type MicrophoneMatcherEventMap, type MicrophoneMatcherEventName, type MicrophoneMatcherListener, authenticateAndLoadAudioPack, createLocalStorageAudioPackStore, createLocalStorageSessionManager, createMicrophoneMatcher, requestMicrophoneStream };
294
+ export { ACTIVITY_REFINEMENT_WINDOW_MS, type ActivityRefinementFailureContext, type ActivityRefinementRequestContext, type AudioMarkerEvent, AudioRecognizer, type AudioRecognizerAuthOptions, type AudioRecognizerOptions, type AuthenticateAndLoadAudioPackOptions, type AuthenticateAndLoadAudioPackResult, type BrowserAudioPackLoader, type BrowserAudioPackState, type BrowserAudioPackStateStore, type BrowserSessionManager, type CreateLocalStorageAudioPackStoreOptions, type CreateLocalStorageSessionManagerOptions, type CreateMicrophoneMatcherOptions, type CreateMicrophoneMatcherWasmOptions, DEFAULT_BROWSER_AUDIOPACK_VERSION, DEFAULT_SORI_API_ENDPOINT, type DetectedMatch, type MaterialActivityRequestPayload, MicrophoneMatcher, type MicrophoneMatcherActivityRefinementOptions, type MicrophoneMatcherActivityReporterOptions, type MicrophoneMatcherCampaignEvent, type MicrophoneMatcherEventMap, type MicrophoneMatcherEventName, type MicrophoneMatcherListener, authenticateAndLoadAudioPack, createLocalStorageAudioPackStore, createLocalStorageSessionManager, createMicrophoneMatcher, requestMicrophoneStream };
package/dist/index.js CHANGED
@@ -201,6 +201,7 @@ var WrappedFingerprintMatchWindow = class {
201
201
  constructor(inner) {
202
202
  this.inner = inner;
203
203
  }
204
+ inner;
204
205
  appendFingerprint(bytes) {
205
206
  const fn = getCallable(this.inner, ["appendFingerprint", "append_fingerprint"]);
206
207
  if (!fn) {
@@ -313,6 +314,22 @@ var FingerprintMatchWindow = class {
313
314
  };
314
315
 
315
316
  // src/activity.ts
317
+ var ActivityReportingError = class extends Error {
318
+ status;
319
+ detail;
320
+ constructor(message, status = null, detail = null) {
321
+ super(message);
322
+ this.name = "ActivityReportingError";
323
+ this.status = status;
324
+ this.detail = detail;
325
+ }
326
+ };
327
+ var ActivityReportingCancelledError = class extends Error {
328
+ constructor() {
329
+ super("Activity reporting was cancelled before transport");
330
+ this.name = "ActivityReportingCancelledError";
331
+ }
332
+ };
316
333
  function isRecord2(value) {
317
334
  return typeof value === "object" && value !== null;
318
335
  }
@@ -332,11 +349,15 @@ async function resolveToken(token) {
332
349
  }
333
350
  return resolved;
334
351
  }
335
- async function resolveRequestPayload(reporter, match) {
336
- const payload = reporter.mapMatchToRequest ? await reporter.mapMatchToRequest(match) : {
352
+ function defaultRequestPayload(match) {
353
+ const marker = match.audioMarker?.code;
354
+ return {
337
355
  type: "material",
338
- material_id: match.name
356
+ material_id: match.name,
357
+ ...typeof marker === "string" && marker.length > 0 ? { trait: { marker } } : {}
339
358
  };
359
+ }
360
+ function validateRequestPayload(payload) {
340
361
  if (payload === null) {
341
362
  return null;
342
363
  }
@@ -345,6 +366,30 @@ async function resolveRequestPayload(reporter, match) {
345
366
  }
346
367
  return payload;
347
368
  }
369
+ async function resolveCreateRequestPayload(reporter, match) {
370
+ const payload = validateRequestPayload(
371
+ reporter.mapMatchToRequest ? await reporter.mapMatchToRequest(match) : defaultRequestPayload(match)
372
+ );
373
+ if (!payload) {
374
+ return null;
375
+ }
376
+ const { refinement_expected: _ignored, ...requestPayload } = payload;
377
+ return requestPayload;
378
+ }
379
+ async function resolveRefineRequestPayload(reporter, match, context) {
380
+ const refinement = reporter.refinement;
381
+ if (!refinement) {
382
+ return null;
383
+ }
384
+ const payload = validateRequestPayload(
385
+ refinement.mapMatchToRequest ? await refinement.mapMatchToRequest(match, context) : reporter.mapMatchToRequest ? await reporter.mapMatchToRequest(match) : defaultRequestPayload(match)
386
+ );
387
+ if (!payload) {
388
+ return null;
389
+ }
390
+ const { refinement_expected: _ignored, ...requestPayload } = payload;
391
+ return requestPayload;
392
+ }
348
393
  function defaultCampaignMapper(payload, match) {
349
394
  const campaign = payload.campaign;
350
395
  if (!isRecord2(campaign)) {
@@ -357,42 +402,462 @@ function defaultCampaignMapper(payload, match) {
357
402
  raw: payload
358
403
  };
359
404
  }
360
- async function reportMatchedMaterialActivity(reporter, match) {
361
- const payload = await resolveRequestPayload(reporter, match);
362
- if (!payload) {
363
- return null;
405
+ async function resolveHeaders(reporter, refinement = false) {
406
+ const refinementHeaders = reporter.refinement && reporter.refinement.headers;
407
+ const baseHeaders = reporter.headers ? typeof reporter.headers === "function" ? await reporter.headers() : reporter.headers : void 0;
408
+ const headers = new Headers(baseHeaders);
409
+ if (refinement && refinementHeaders !== void 0) {
410
+ const overrides = new Headers(
411
+ typeof refinementHeaders === "function" ? await refinementHeaders() : refinementHeaders
412
+ );
413
+ overrides.forEach((value, name) => headers.set(name, value));
364
414
  }
365
- const headers = new Headers(
366
- reporter.headers ? typeof reporter.headers === "function" ? await reporter.headers() : reporter.headers : void 0
367
- );
368
415
  headers.set("authorization", `Bearer ${await resolveToken(reporter.token)}`);
369
416
  headers.set("content-type", "application/json");
417
+ return headers;
418
+ }
419
+ async function parseActivityResponse(response, operation) {
420
+ const responseText = await response.text();
421
+ let data = null;
422
+ if (responseText.trim().length > 0) {
423
+ try {
424
+ data = JSON.parse(responseText);
425
+ } catch {
426
+ throw new ActivityReportingError(
427
+ `Activity ${operation} failed: invalid JSON response`,
428
+ response.status
429
+ );
430
+ }
431
+ }
432
+ const succeeded = operation === "refine" ? response.status === 200 : response.ok;
433
+ if (!succeeded) {
434
+ const detail = isRecord2(data) && typeof data.detail === "string" ? data.detail : null;
435
+ throw new ActivityReportingError(
436
+ `Activity ${operation} failed: HTTP ${response.status}${detail ? ` (${detail})` : ""}`,
437
+ response.status,
438
+ detail
439
+ );
440
+ }
441
+ if (data === null) {
442
+ return null;
443
+ }
444
+ if (!isRecord2(data)) {
445
+ throw new ActivityReportingError(
446
+ `Activity ${operation} failed: expected an object response`,
447
+ response.status
448
+ );
449
+ }
450
+ return data;
451
+ }
452
+ function responseActivityId(data, campaign) {
453
+ if (data && typeof data.activity_id === "string" && data.activity_id.length > 0) {
454
+ return data.activity_id;
455
+ }
456
+ return campaign?.activityId && campaign.activityId.length > 0 ? campaign.activityId : null;
457
+ }
458
+ async function mapCreateResponse(reporter, data, match, refinementExpected) {
459
+ if (!data) {
460
+ return { activityId: null, campaign: null, refinementExpected };
461
+ }
462
+ const campaign = reporter.mapResponseToCampaign ? await reporter.mapResponseToCampaign(data, match) : defaultCampaignMapper(data, match);
463
+ return { activityId: responseActivityId(data, campaign), campaign, refinementExpected };
464
+ }
465
+ async function resolveRefinementEndpoint(reporter, activityId) {
466
+ const configured = reporter.refinement && reporter.refinement.endpoint;
467
+ if (typeof configured === "function") {
468
+ return configured(activityId);
469
+ }
470
+ const prefix = String(configured ?? reporter.endpoint);
471
+ return `${prefix.endsWith("/") ? prefix : `${prefix}/`}${encodeURIComponent(activityId)}`;
472
+ }
473
+ async function createMatchedMaterialActivity(reporter, match, refinementExpected = false) {
474
+ const payload = await resolveCreateRequestPayload(reporter, match);
475
+ if (!payload) {
476
+ return { activityId: null, campaign: null, refinementExpected: false };
477
+ }
478
+ const headers = await resolveHeaders(reporter);
479
+ const actualMarker = match.audioMarker?.code;
480
+ const payloadMarker = payload.trait?.marker;
481
+ const hasMarker = typeof actualMarker === "string" && actualMarker.length > 0 || typeof payloadMarker === "string" && payloadMarker.length > 0;
482
+ const lifecycleOpen = typeof refinementExpected === "function" ? refinementExpected() : refinementExpected;
483
+ const requestRefinementExpected = Boolean(reporter.refinement && lifecycleOpen && !hasMarker);
484
+ const requestPayload = requestRefinementExpected ? { ...payload, refinement_expected: true } : payload;
370
485
  const response = await resolveFetch(reporter.fetch)(reporter.endpoint, {
371
486
  method: "POST",
372
487
  headers,
488
+ body: JSON.stringify(requestPayload)
489
+ });
490
+ return mapCreateResponse(
491
+ reporter,
492
+ await parseActivityResponse(response, "create"),
493
+ match,
494
+ requestRefinementExpected
495
+ );
496
+ }
497
+ async function refineMatchedMaterialActivity(reporter, match, context, isActive = () => true) {
498
+ const refinement = reporter.refinement;
499
+ if (!refinement) {
500
+ return { activityId: null, campaign: null };
501
+ }
502
+ const payload = await resolveRefineRequestPayload(reporter, match, context);
503
+ if (!payload) {
504
+ return { activityId: null, campaign: null };
505
+ }
506
+ const endpoint = await resolveRefinementEndpoint(reporter, context.activityId);
507
+ const headers = await resolveHeaders(reporter, true);
508
+ if (!isActive()) {
509
+ throw new ActivityReportingCancelledError();
510
+ }
511
+ const response = await resolveFetch(refinement.fetch ?? reporter.fetch)(endpoint, {
512
+ method: "PUT",
513
+ headers,
373
514
  body: JSON.stringify(payload)
374
515
  });
375
- if (!response.ok) {
376
- throw new Error(`Activity reporting failed: HTTP ${response.status}`);
516
+ const data = await parseActivityResponse(response, "refine");
517
+ if (!data) {
518
+ throw new ActivityReportingError("Activity refine failed: empty response", response.status);
519
+ }
520
+ const mappedCampaign = refinement.mapResponseToCampaign ? await refinement.mapResponseToCampaign(data, match, context) : reporter.mapResponseToCampaign ? await reporter.mapResponseToCampaign(data, match) : defaultCampaignMapper(data, match);
521
+ const activityId = responseActivityId(data, mappedCampaign);
522
+ if (activityId !== context.activityId) {
523
+ throw new ActivityReportingError(
524
+ "Activity refine failed: response activity_id does not match the requested activity",
525
+ response.status
526
+ );
377
527
  }
378
- const responseText = await response.text();
379
- if (responseText.trim().length === 0) {
380
- return null;
528
+ const campaign = mappedCampaign ? { ...mappedCampaign, activityId: context.activityId } : null;
529
+ return { activityId, campaign };
530
+ }
531
+
532
+ // src/activity-refinement.ts
533
+ var ACTIVITY_REFINEMENT_WINDOW_MS = 3e4;
534
+ var DEFAULT_MAX_REFINEMENT_ATTEMPTS = 2;
535
+ var MAX_REFINEMENT_ATTEMPTS = 10;
536
+ function normalizeError(error) {
537
+ return error instanceof Error ? error : new Error(String(error));
538
+ }
539
+ function statusAndDetail(error) {
540
+ return error instanceof ActivityReportingError ? { status: error.status, detail: error.detail } : { status: null, detail: null };
541
+ }
542
+ function defaultShouldRetry(context) {
543
+ return context.status === null || context.status >= 500 && context.status <= 599;
544
+ }
545
+ function isStableTerminalStatus(status) {
546
+ return status === 404 || status === 409 || status === 410;
547
+ }
548
+ var ActivityRefinementCoordinator = class {
549
+ reporter;
550
+ callbacks;
551
+ lifecycles = /* @__PURE__ */ new Map();
552
+ requestLifecycles = /* @__PURE__ */ new Map();
553
+ pendingMarkers = /* @__PURE__ */ new Map();
554
+ registeredRequests = /* @__PURE__ */ new Set();
555
+ settledRequests = /* @__PURE__ */ new Set();
556
+ activeLifecycle = null;
557
+ generation = 0;
558
+ constructor(reporter, callbacks) {
559
+ this.reporter = reporter;
560
+ this.callbacks = callbacks;
561
+ }
562
+ registerRequest(requestId) {
563
+ if (this.reporter.refinement) {
564
+ this.registeredRequests.add(requestId);
565
+ }
381
566
  }
382
- let data;
383
- try {
384
- data = JSON.parse(responseText);
385
- } catch {
386
- throw new Error("Activity reporting failed: invalid JSON response");
567
+ report(recognitionId, match, captureGeneration, requestId = null) {
568
+ const coordinatorGeneration = this.generation;
569
+ if (this.activeLifecycle && this.activeLifecycle.recognitionId !== recognitionId) {
570
+ this.deleteLifecycle(this.activeLifecycle);
571
+ }
572
+ const hasMarker = typeof match.audioMarker?.code === "string" && match.audioMarker.code.length > 0;
573
+ const refinement = this.reporter.refinement;
574
+ const requestCanRefine = requestId !== null && !this.settledRequests.has(requestId);
575
+ const pendingMarker = requestId === null ? void 0 : this.pendingMarkers.get(requestId);
576
+ if (requestId !== null) {
577
+ this.pendingMarkers.delete(requestId);
578
+ this.registeredRequests.delete(requestId);
579
+ this.settledRequests.delete(requestId);
580
+ }
581
+ const refinementExpected = Boolean(refinement && !hasMarker && requestCanRefine);
582
+ let lifecycle = null;
583
+ if (refinementExpected && refinement) {
584
+ const now = refinement.now?.() ?? Date.now();
585
+ lifecycle = {
586
+ recognitionId,
587
+ captureGeneration,
588
+ match,
589
+ deadline: now + ACTIVITY_REFINEMENT_WINDOW_MS,
590
+ activityId: null,
591
+ markerMatch: null,
592
+ markerCode: null,
593
+ timer: null,
594
+ createSettled: false,
595
+ refinementStarted: false
596
+ };
597
+ const createdLifecycle = lifecycle;
598
+ lifecycle.timer = setTimeout(() => {
599
+ this.deleteLifecycle(createdLifecycle);
600
+ }, ACTIVITY_REFINEMENT_WINDOW_MS);
601
+ this.lifecycles.set(recognitionId, lifecycle);
602
+ if (requestId !== null) {
603
+ this.requestLifecycles.set(requestId, lifecycle);
604
+ }
605
+ this.activeLifecycle = lifecycle;
606
+ if (pendingMarker) {
607
+ this.refine(requestId ?? recognitionId, pendingMarker, captureGeneration);
608
+ }
609
+ }
610
+ void createMatchedMaterialActivity(
611
+ this.reporter,
612
+ match,
613
+ () => Boolean(lifecycle && this.isRefinementActive(lifecycle))
614
+ ).then(
615
+ (result) => {
616
+ if (coordinatorGeneration === this.generation && result.campaign && this.callbacks.isCurrentCapture(captureGeneration)) {
617
+ this.callbacks.emitCampaign(result.campaign);
618
+ }
619
+ if (coordinatorGeneration !== this.generation || !lifecycle || this.lifecycles.get(recognitionId) !== lifecycle) {
620
+ return;
621
+ }
622
+ if (!result.refinementExpected) {
623
+ this.deleteLifecycle(lifecycle);
624
+ return;
625
+ }
626
+ lifecycle.createSettled = true;
627
+ lifecycle.activityId = result.activityId;
628
+ if (lifecycle.markerMatch) {
629
+ if (lifecycle.activityId) {
630
+ this.startRefinement(lifecycle);
631
+ } else {
632
+ this.failMissingActivityId(lifecycle);
633
+ }
634
+ }
635
+ },
636
+ (error) => {
637
+ if (lifecycle) {
638
+ this.deleteLifecycle(lifecycle);
639
+ }
640
+ if (coordinatorGeneration === this.generation && this.callbacks.isCurrentCapture(captureGeneration)) {
641
+ this.callbacks.emitError("activity", error);
642
+ }
643
+ }
644
+ );
387
645
  }
388
- if (!isRecord2(data)) {
389
- throw new Error("Activity reporting failed: expected an object response");
646
+ associate(requestId, match, captureGeneration) {
647
+ const lifecycle = this.activeLifecycle;
648
+ this.registeredRequests.delete(requestId);
649
+ if (!lifecycle || !match || lifecycle.captureGeneration !== captureGeneration || lifecycle.match.name !== match.name || lifecycle.match.afpType !== match.afpType || !this.isActive(lifecycle)) {
650
+ return false;
651
+ }
652
+ this.requestLifecycles.set(requestId, lifecycle);
653
+ const pendingMarker = this.pendingMarkers.get(requestId);
654
+ if (pendingMarker) {
655
+ this.pendingMarkers.delete(requestId);
656
+ this.refine(requestId, pendingMarker, captureGeneration);
657
+ }
658
+ return true;
390
659
  }
391
- if (reporter.mapResponseToCampaign) {
392
- return reporter.mapResponseToCampaign(data, match);
660
+ discardRequest(requestId) {
661
+ this.registeredRequests.delete(requestId);
662
+ this.pendingMarkers.delete(requestId);
663
+ this.requestLifecycles.delete(requestId);
664
+ this.settledRequests.delete(requestId);
665
+ }
666
+ settleRequest(requestId) {
667
+ const awaitingAssociation = this.registeredRequests.delete(requestId);
668
+ this.pendingMarkers.delete(requestId);
669
+ const lifecycle = this.requestLifecycles.get(requestId);
670
+ if (lifecycle) {
671
+ this.deleteLifecycle(lifecycle);
672
+ return;
673
+ }
674
+ if (awaitingAssociation) {
675
+ this.settledRequests.add(requestId);
676
+ }
393
677
  }
394
- return defaultCampaignMapper(data, match);
395
- }
678
+ clearActive(captureGeneration) {
679
+ if (this.activeLifecycle?.captureGeneration === captureGeneration) {
680
+ this.deleteLifecycle(this.activeLifecycle);
681
+ }
682
+ }
683
+ refine(recognitionId, marker, captureGeneration) {
684
+ if (!this.reporter.refinement) {
685
+ return;
686
+ }
687
+ const lifecycle = this.requestLifecycles.get(recognitionId);
688
+ const markerCode = marker.code;
689
+ if (typeof markerCode !== "string" || markerCode.length === 0) {
690
+ this.settleRequest(recognitionId);
691
+ return;
692
+ }
693
+ if (!lifecycle || lifecycle.captureGeneration !== captureGeneration || lifecycle.markerCode !== null || !this.callbacks.isCurrentCapture(captureGeneration)) {
694
+ if (!lifecycle && this.registeredRequests.has(recognitionId) && typeof markerCode === "string" && markerCode.length > 0 && this.callbacks.isCurrentCapture(captureGeneration)) {
695
+ this.pendingMarkers.set(recognitionId, marker);
696
+ }
697
+ return;
698
+ }
699
+ const now = this.reporter.refinement?.now?.() ?? Date.now();
700
+ if (now > lifecycle.deadline) {
701
+ this.deleteLifecycle(lifecycle);
702
+ return;
703
+ }
704
+ lifecycle.markerCode = markerCode;
705
+ lifecycle.markerMatch = { ...lifecycle.match, audioMarker: marker };
706
+ if (lifecycle.activityId) {
707
+ this.startRefinement(lifecycle);
708
+ } else if (lifecycle.createSettled) {
709
+ this.failMissingActivityId(lifecycle);
710
+ }
711
+ }
712
+ reset() {
713
+ this.generation += 1;
714
+ for (const lifecycle of this.lifecycles.values()) {
715
+ if (lifecycle.timer) {
716
+ clearTimeout(lifecycle.timer);
717
+ }
718
+ }
719
+ this.lifecycles.clear();
720
+ this.requestLifecycles.clear();
721
+ this.pendingMarkers.clear();
722
+ this.registeredRequests.clear();
723
+ this.settledRequests.clear();
724
+ this.activeLifecycle = null;
725
+ }
726
+ startRefinement(lifecycle) {
727
+ if (lifecycle.refinementStarted || !lifecycle.activityId || !lifecycle.markerMatch) {
728
+ return;
729
+ }
730
+ lifecycle.refinementStarted = true;
731
+ void this.runRefinement(lifecycle);
732
+ }
733
+ failMissingActivityId(lifecycle) {
734
+ if (!this.isRefinementActive(lifecycle)) {
735
+ return;
736
+ }
737
+ this.deleteLifecycle(lifecycle);
738
+ this.callbacks.emitError(
739
+ "activity-refinement",
740
+ new Error("Activity refinement requires a non-empty activity_id from the create response")
741
+ );
742
+ }
743
+ async runRefinement(lifecycle) {
744
+ const refinement = this.reporter.refinement;
745
+ if (!refinement || !lifecycle.activityId || !lifecycle.markerMatch) {
746
+ this.deleteLifecycle(lifecycle);
747
+ return;
748
+ }
749
+ const maxAttempts = Math.min(
750
+ MAX_REFINEMENT_ATTEMPTS,
751
+ Math.max(
752
+ 1,
753
+ Number.isSafeInteger(refinement.maxAttempts) ? refinement.maxAttempts ?? DEFAULT_MAX_REFINEMENT_ATTEMPTS : DEFAULT_MAX_REFINEMENT_ATTEMPTS
754
+ )
755
+ );
756
+ for (let attempt = 1; attempt <= maxAttempts; attempt += 1) {
757
+ if (!this.isActive(lifecycle)) {
758
+ return;
759
+ }
760
+ const now = refinement.now?.() ?? Date.now();
761
+ if (now > lifecycle.deadline) {
762
+ this.deleteLifecycle(lifecycle);
763
+ return;
764
+ }
765
+ try {
766
+ const result = await refineMatchedMaterialActivity(
767
+ this.reporter,
768
+ lifecycle.markerMatch,
769
+ {
770
+ operation: "refine",
771
+ activityId: lifecycle.activityId,
772
+ attempt,
773
+ deadline: lifecycle.deadline
774
+ },
775
+ () => this.isRefinementActive(lifecycle)
776
+ );
777
+ if (!this.isRefinementActive(lifecycle)) {
778
+ return;
779
+ }
780
+ this.deleteLifecycle(lifecycle);
781
+ if (result.campaign) {
782
+ this.callbacks.emitCampaign(result.campaign);
783
+ }
784
+ return;
785
+ } catch (caught) {
786
+ if (caught instanceof ActivityReportingCancelledError) {
787
+ return;
788
+ }
789
+ if (!this.isRefinementActive(lifecycle)) {
790
+ return;
791
+ }
792
+ const error = normalizeError(caught);
793
+ const { status, detail } = statusAndDetail(error);
794
+ const failureContext = {
795
+ operation: "refine",
796
+ activityId: lifecycle.activityId,
797
+ attempt,
798
+ deadline: lifecycle.deadline,
799
+ error,
800
+ status,
801
+ detail
802
+ };
803
+ let retry = false;
804
+ try {
805
+ retry = !isStableTerminalStatus(status) && attempt < maxAttempts && (refinement.now?.() ?? Date.now()) <= lifecycle.deadline && (refinement.shouldRetry ? await refinement.shouldRetry(failureContext) : defaultShouldRetry(failureContext));
806
+ } catch (policyError) {
807
+ if (!this.isRefinementActive(lifecycle)) {
808
+ return;
809
+ }
810
+ this.deleteLifecycle(lifecycle);
811
+ this.callbacks.emitError("activity-refinement", policyError);
812
+ return;
813
+ }
814
+ if (!this.isRefinementActive(lifecycle)) {
815
+ return;
816
+ }
817
+ if (retry) {
818
+ continue;
819
+ }
820
+ this.deleteLifecycle(lifecycle);
821
+ this.callbacks.emitError("activity-refinement", error);
822
+ return;
823
+ }
824
+ }
825
+ }
826
+ isActive(lifecycle) {
827
+ return this.lifecycles.get(lifecycle.recognitionId) === lifecycle && this.callbacks.isCurrentCapture(lifecycle.captureGeneration);
828
+ }
829
+ isRefinementActive(lifecycle) {
830
+ if (!this.isActive(lifecycle)) {
831
+ return false;
832
+ }
833
+ const refinement = this.reporter.refinement;
834
+ const now = refinement && refinement.now ? refinement.now() : Date.now();
835
+ if (now > lifecycle.deadline) {
836
+ this.deleteLifecycle(lifecycle);
837
+ return false;
838
+ }
839
+ return true;
840
+ }
841
+ deleteLifecycle(lifecycle) {
842
+ if (lifecycle.timer) {
843
+ clearTimeout(lifecycle.timer);
844
+ lifecycle.timer = null;
845
+ }
846
+ if (this.lifecycles.get(lifecycle.recognitionId) === lifecycle) {
847
+ this.lifecycles.delete(lifecycle.recognitionId);
848
+ }
849
+ for (const [requestId, requestLifecycle] of this.requestLifecycles) {
850
+ if (requestLifecycle === lifecycle) {
851
+ this.requestLifecycles.delete(requestId);
852
+ this.pendingMarkers.delete(requestId);
853
+ this.registeredRequests.delete(requestId);
854
+ }
855
+ }
856
+ if (this.activeLifecycle === lifecycle) {
857
+ this.activeLifecycle = null;
858
+ }
859
+ }
860
+ };
396
861
 
397
862
  // src/microphone.ts
398
863
  var WORKLET_PROCESSOR_NAME = "sori-microphone-capture";
@@ -517,11 +982,11 @@ var defaultAudioGraphFactory = async (options) => {
517
982
  };
518
983
 
519
984
  // src/microphone-matcher.ts
520
- function normalizeError(error) {
985
+ function normalizeError2(error) {
521
986
  return error instanceof Error ? error : new Error(String(error));
522
987
  }
523
988
  function emitError(emitter, phase, error) {
524
- const normalized = normalizeError(error);
989
+ const normalized = normalizeError2(error);
525
990
  emitter.emit("error", { phase, error: normalized });
526
991
  return normalized;
527
992
  }
@@ -539,6 +1004,7 @@ var MicrophoneMatcher = class {
539
1004
  captureSampleRate;
540
1005
  audioGraphFactory;
541
1006
  packSource;
1007
+ activityRefinement;
542
1008
  options;
543
1009
  extractor = null;
544
1010
  graphController = null;
@@ -557,6 +1023,7 @@ var MicrophoneMatcher = class {
557
1023
  latestAudioMarker = null;
558
1024
  latestAudioMarkerDetection = null;
559
1025
  audioMarkerRequestId = 0;
1026
+ activityRecognitionId = 0;
560
1027
  hopSize = 0;
561
1028
  diagnostics = {
562
1029
  sampleCallbacks: 0,
@@ -578,6 +1045,11 @@ var MicrophoneMatcher = class {
578
1045
  this.suspendContextOnStop = options.suspendContextOnStop ?? true;
579
1046
  this.audioGraphFactory = defaultAudioGraphFactory;
580
1047
  this.packSource = options.packSource;
1048
+ this.activityRefinement = options.activityReporter ? new ActivityRefinementCoordinator(options.activityReporter, {
1049
+ isCurrentCapture: (captureGeneration) => captureGeneration === this.captureGeneration && this.running,
1050
+ emitCampaign: (campaign) => this.events.emit("campaign", campaign),
1051
+ emitError: (phase, error) => emitError(this.events, phase, error)
1052
+ }) : null;
581
1053
  }
582
1054
  on(eventName, listener) {
583
1055
  this.events.on(eventName, listener);
@@ -661,6 +1133,7 @@ var MicrophoneMatcher = class {
661
1133
  try {
662
1134
  this.ensureNotDestroyed();
663
1135
  await this.prepare();
1136
+ this.activityRefinement?.reset();
664
1137
  this.matchWindowOrThrow().reset();
665
1138
  await this.matcherSession.clear();
666
1139
  } catch (error) {
@@ -798,6 +1271,8 @@ var MicrophoneMatcher = class {
798
1271
  this.diagnostics.readyQueries += 1;
799
1272
  let matchedBest = null;
800
1273
  let shouldEmitNoMatch = false;
1274
+ let audioMarkerRequestId = null;
1275
+ let associatedActivity = false;
801
1276
  const handleMatch = (event) => {
802
1277
  if (!event.best) {
803
1278
  return;
@@ -814,8 +1289,23 @@ var MicrophoneMatcher = class {
814
1289
  try {
815
1290
  this.diagnostics.matchRequests += 1;
816
1291
  const bestMatchPromise = this.matcherSession.bestMatch(query, this.matchConfig());
817
- const audioMarkerRequestId = this.beginAudioMarkerDetection(captureGeneration);
818
- await bestMatchPromise;
1292
+ audioMarkerRequestId = this.beginAudioMarkerDetection(captureGeneration);
1293
+ let observedBest;
1294
+ try {
1295
+ observedBest = await bestMatchPromise;
1296
+ } catch (error) {
1297
+ if (audioMarkerRequestId !== null) {
1298
+ this.activityRefinement?.discardRequest(audioMarkerRequestId);
1299
+ }
1300
+ throw error;
1301
+ }
1302
+ if (audioMarkerRequestId !== null) {
1303
+ associatedActivity = this.activityRefinement?.associate(
1304
+ audioMarkerRequestId,
1305
+ observedBest,
1306
+ captureGeneration
1307
+ ) ?? false;
1308
+ }
819
1309
  const distinctBest = this.withAudioMarkerForRequest(matchedBest, audioMarkerRequestId);
820
1310
  matchedBest = distinctBest;
821
1311
  } finally {
@@ -827,9 +1317,21 @@ var MicrophoneMatcher = class {
827
1317
  }
828
1318
  if (matchedBest) {
829
1319
  this.events.emit("match", { best: matchedBest });
830
- void this.reportCampaign(matchedBest, captureGeneration);
1320
+ this.activityRecognitionId += 1;
1321
+ this.reportCampaign(
1322
+ this.activityRecognitionId,
1323
+ matchedBest,
1324
+ captureGeneration,
1325
+ audioMarkerRequestId
1326
+ );
831
1327
  } else if (shouldEmitNoMatch) {
1328
+ if (audioMarkerRequestId !== null && !associatedActivity) {
1329
+ this.activityRefinement?.discardRequest(audioMarkerRequestId);
1330
+ }
1331
+ this.activityRefinement?.clearActive(captureGeneration);
832
1332
  this.events.emit("nomatch", {});
1333
+ } else if (audioMarkerRequestId !== null && !associatedActivity) {
1334
+ this.activityRefinement?.discardRequest(audioMarkerRequestId);
833
1335
  }
834
1336
  } while (this.pendingMatch || this.matchWindowOrThrow().hasReadyQuery());
835
1337
  } catch (error) {
@@ -849,6 +1351,7 @@ var MicrophoneMatcher = class {
849
1351
  this.latestAudioMarker = null;
850
1352
  this.latestAudioMarkerDetection = null;
851
1353
  this.audioMarkerRequestId += 1;
1354
+ this.activityRefinement?.reset();
852
1355
  this.matchWindowOrThrow().reset();
853
1356
  await this.extractor?.reset();
854
1357
  }
@@ -866,6 +1369,7 @@ var MicrophoneMatcher = class {
866
1369
  }
867
1370
  const requestId = this.audioMarkerRequestId + 1;
868
1371
  this.audioMarkerRequestId = requestId;
1372
+ this.activityRefinement?.registerRequest(requestId);
869
1373
  const markerPcm = float32ToPcm16Le(this.markerPcmBuffer);
870
1374
  void this.detectAudioMarkerForRequest(captureGeneration, requestId, markerPcm);
871
1375
  return requestId;
@@ -881,8 +1385,10 @@ var MicrophoneMatcher = class {
881
1385
  }
882
1386
  this.latestAudioMarkerDetection = { requestId, detection };
883
1387
  this.emitAudioMarkerIfChanged(detection);
1388
+ this.activityRefinement?.refine(requestId, detection, captureGeneration);
884
1389
  return detection;
885
1390
  } catch (error) {
1391
+ this.activityRefinement?.settleRequest(requestId);
886
1392
  emitError(this.events, "audiomarker", error);
887
1393
  return null;
888
1394
  }
@@ -975,19 +1481,17 @@ var MicrophoneMatcher = class {
975
1481
  }
976
1482
  return this.matchWindow;
977
1483
  }
978
- async reportCampaign(match, captureGeneration) {
1484
+ reportCampaign(recognitionId, match, captureGeneration, audioMarkerRequestId) {
979
1485
  const reporter = this.options.activityReporter;
980
1486
  if (!reporter) {
981
1487
  return;
982
1488
  }
983
- try {
984
- const campaign = await reportMatchedMaterialActivity(reporter, match);
985
- if (campaign && captureGeneration === this.captureGeneration && this.running) {
986
- this.events.emit("campaign", campaign);
987
- }
988
- } catch (error) {
989
- emitError(this.events, "activity", error);
990
- }
1489
+ this.activityRefinement?.report(
1490
+ recognitionId,
1491
+ match,
1492
+ captureGeneration,
1493
+ audioMarkerRequestId
1494
+ );
991
1495
  }
992
1496
  };
993
1497
  function appendFloat32(left, right) {
@@ -1025,11 +1529,31 @@ function resolveStorage2(storage) {
1025
1529
  }
1026
1530
  return globalThis.localStorage;
1027
1531
  }
1532
+ function formatUuidV4(bytes) {
1533
+ bytes[6] = bytes[6] & 15 | 64;
1534
+ bytes[8] = bytes[8] & 63 | 128;
1535
+ const hex = Array.from(bytes, (byte) => byte.toString(16).padStart(2, "0")).join("");
1536
+ return [
1537
+ hex.slice(0, 8),
1538
+ hex.slice(8, 12),
1539
+ hex.slice(12, 16),
1540
+ hex.slice(16, 20),
1541
+ hex.slice(20)
1542
+ ].join("-");
1543
+ }
1028
1544
  function defaultGenerateSessionId() {
1029
- if (typeof globalThis.crypto !== "undefined" && typeof globalThis.crypto.randomUUID === "function") {
1030
- return globalThis.crypto.randomUUID();
1545
+ const crypto = globalThis.crypto;
1546
+ if (typeof crypto !== "undefined") {
1547
+ if (typeof crypto.randomUUID === "function") {
1548
+ return crypto.randomUUID();
1549
+ }
1550
+ if (typeof crypto.getRandomValues === "function") {
1551
+ return formatUuidV4(crypto.getRandomValues(new Uint8Array(16)));
1552
+ }
1031
1553
  }
1032
- return `session-${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 10)}`;
1554
+ throw new Error(
1555
+ "Secure random number generation is unavailable; provide a cryptographically secure `generateSessionId`"
1556
+ );
1033
1557
  }
1034
1558
  function createLocalStorageSessionManager(options) {
1035
1559
  const storage = resolveStorage2(options.storage);
@@ -1122,7 +1646,8 @@ function resolveRecognizerOptions(options) {
1122
1646
  const activityReporter = activityEndpoint === void 0 ? void 0 : {
1123
1647
  ...customActivityReporter,
1124
1648
  endpoint: activityEndpoint,
1125
- fetch: customActivityReporter?.fetch ?? sharedFetch
1649
+ fetch: customActivityReporter?.fetch ?? sharedFetch,
1650
+ refinement: customActivityReporter ? customActivityReporter.refinement : {}
1126
1651
  };
1127
1652
  return {
1128
1653
  ...options,
@@ -1295,6 +1820,7 @@ var AudioRecognizer = class {
1295
1820
  // src/index.ts
1296
1821
  import { AudioFingerprintType as AudioFingerprintType3 } from "@sorisdk/matcher";
1297
1822
  export {
1823
+ ACTIVITY_REFINEMENT_WINDOW_MS,
1298
1824
  AudioFingerprintType3 as AudioFingerprintType,
1299
1825
  AudioRecognizer,
1300
1826
  DEFAULT_BROWSER_AUDIOPACK_VERSION,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sorisdk/web-audio",
3
- "version": "0.6.3",
3
+ "version": "0.6.5",
4
4
  "description": "Web SDK for browser-based audio recognition with SORI API",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
@@ -34,13 +34,13 @@
34
34
  "license": "SEE LICENSE IN LICENSE.md",
35
35
  "homepage": "https://docs.soriapi.com/ko/integration/web",
36
36
  "dependencies": {
37
- "@sorisdk/matcher": "0.6.3",
38
- "@sorisdk/afpgen": "0.6.3"
37
+ "@sorisdk/afpgen": "0.6.5",
38
+ "@sorisdk/matcher": "0.6.5"
39
39
  },
40
40
  "devDependencies": {
41
41
  "tsup": "^8.5.1",
42
42
  "typescript": "^5.8.3",
43
- "vitest": "^3.2.4"
43
+ "vitest": "^3.2.6"
44
44
  },
45
45
  "scripts": {
46
46
  "build:wasm": "node ../../scripts/build-wasm-package.mjs web-audio",