@sorisdk/web-audio 0.6.5 → 0.6.8

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/dist/index.js CHANGED
@@ -181,6 +181,18 @@ function getBrowserMediaDevices() {
181
181
  }
182
182
 
183
183
  // src/match-window.ts
184
+ var FINGERPRINT_LEVEL_BYTE_LENGTH = 8;
185
+ function getTypedArrayByteLengthGetter() {
186
+ const getter = Object.getOwnPropertyDescriptor(
187
+ Object.getPrototypeOf(Uint8Array.prototype),
188
+ "byteLength"
189
+ )?.get;
190
+ if (!getter) {
191
+ throw new Error("Uint8Array byteLength intrinsic is unavailable");
192
+ }
193
+ return getter;
194
+ }
195
+ var typedArrayByteLengthGetter = getTypedArrayByteLengthGetter();
184
196
  var cachedBindings = null;
185
197
  var initPromise = null;
186
198
  var testInjectedWasmModule = null;
@@ -197,18 +209,46 @@ var defaultLoader = async () => {
197
209
  const loaderModule = await import("./default-loader.js");
198
210
  return loaderModule.default();
199
211
  };
212
+ function calculateMaxAppendFingerprintByteLength(options) {
213
+ const hopSize = options.afpgenConfig.hopSize ?? 1e3;
214
+ const levelCount = options.afpgenConfig.levelCount ?? 2;
215
+ const hopDurationMs = hopSize / options.sampleRate * 1e3;
216
+ const hopsInWindow = Math.ceil(options.matchWindowMs / hopDurationMs);
217
+ return Math.max(1, hopsInWindow) * levelCount * FINGERPRINT_LEVEL_BYTE_LENGTH;
218
+ }
219
+ function appendLimitError(byteLength, maxByteLength) {
220
+ return new RangeError(
221
+ `Fingerprint append byteLength ${byteLength} exceeds the per-instance limit of ${maxByteLength} bytes`
222
+ );
223
+ }
224
+ function intrinsicUint8ArrayByteLength(bytes) {
225
+ return typedArrayByteLengthGetter.call(bytes);
226
+ }
200
227
  var WrappedFingerprintMatchWindow = class {
201
- constructor(inner) {
228
+ constructor(inner, options) {
202
229
  this.inner = inner;
230
+ const maxLengthFn = getCallable(this.inner, [
231
+ "maxAppendFingerprintByteLength",
232
+ "max_append_fingerprint_byte_length"
233
+ ]);
234
+ this.maxAppendByteLength = maxLengthFn ? Number(maxLengthFn.call(this.inner)) : calculateMaxAppendFingerprintByteLength(options);
203
235
  }
204
236
  inner;
237
+ maxAppendByteLength;
205
238
  appendFingerprint(bytes) {
239
+ const byteLength = intrinsicUint8ArrayByteLength(bytes);
240
+ if (byteLength > this.maxAppendByteLength) {
241
+ throw appendLimitError(byteLength, this.maxAppendByteLength);
242
+ }
206
243
  const fn = getCallable(this.inner, ["appendFingerprint", "append_fingerprint"]);
207
244
  if (!fn) {
208
245
  throw new Error("web-audio wasm window is missing appendFingerprint");
209
246
  }
210
247
  fn.call(this.inner, bytes);
211
248
  }
249
+ maxAppendFingerprintByteLength() {
250
+ return this.maxAppendByteLength;
251
+ }
212
252
  hasReadyQuery() {
213
253
  const fn = getCallable(this.inner, ["hasReadyQuery", "has_ready_query"]);
214
254
  if (!fn) {
@@ -261,7 +301,8 @@ function createBindingsFromModule(moduleLike) {
261
301
  options.afpgenConfig,
262
302
  options.matchWindowMs,
263
303
  options.matchStrideMs
264
- )
304
+ ),
305
+ options
265
306
  );
266
307
  }
267
308
  };
@@ -313,6 +354,12 @@ var FingerprintMatchWindow = class {
313
354
  }
314
355
  };
315
356
 
357
+ // src/detected-match.ts
358
+ function toPublicDetectedMatch(match) {
359
+ const { audioMarker: _internalAudioMarker, ...publicMatch } = match;
360
+ return publicMatch;
361
+ }
362
+
316
363
  // src/activity.ts
317
364
  var ActivityReportingError = class extends Error {
318
365
  status;
@@ -357,6 +404,14 @@ function defaultRequestPayload(match) {
357
404
  ...typeof marker === "string" && marker.length > 0 ? { trait: { marker } } : {}
358
405
  };
359
406
  }
407
+ function captureActivityRecognitionTiming(match) {
408
+ const recognizedAtMillis = Date.now();
409
+ const position = match.position;
410
+ return Object.freeze({
411
+ recognizedAt: new Date(recognizedAtMillis).toISOString(),
412
+ position
413
+ });
414
+ }
360
415
  function validateRequestPayload(payload) {
361
416
  if (payload === null) {
362
417
  return null;
@@ -368,13 +423,18 @@ function validateRequestPayload(payload) {
368
423
  }
369
424
  async function resolveCreateRequestPayload(reporter, match) {
370
425
  const payload = validateRequestPayload(
371
- reporter.mapMatchToRequest ? await reporter.mapMatchToRequest(match) : defaultRequestPayload(match)
426
+ reporter.mapMatchToRequest ? await reporter.mapMatchToRequest(toPublicDetectedMatch(match)) : defaultRequestPayload(match)
372
427
  );
373
428
  if (!payload) {
374
429
  return null;
375
430
  }
376
- const { refinement_expected: _ignored, ...requestPayload } = payload;
377
- return requestPayload;
431
+ const {
432
+ position: _ignoredPosition,
433
+ recognized_at: _ignoredRecognizedAt,
434
+ refinement_expected: _ignoredRefinementExpected,
435
+ ...requestPayload
436
+ } = payload;
437
+ return withInternalAudioMarker(requestPayload, match);
378
438
  }
379
439
  async function resolveRefineRequestPayload(reporter, match, context) {
380
440
  const refinement = reporter.refinement;
@@ -382,13 +442,31 @@ async function resolveRefineRequestPayload(reporter, match, context) {
382
442
  return null;
383
443
  }
384
444
  const payload = validateRequestPayload(
385
- refinement.mapMatchToRequest ? await refinement.mapMatchToRequest(match, context) : reporter.mapMatchToRequest ? await reporter.mapMatchToRequest(match) : defaultRequestPayload(match)
445
+ refinement.mapMatchToRequest ? await refinement.mapMatchToRequest(toPublicDetectedMatch(match), context) : reporter.mapMatchToRequest ? await reporter.mapMatchToRequest(toPublicDetectedMatch(match)) : defaultRequestPayload(match)
386
446
  );
387
447
  if (!payload) {
388
448
  return null;
389
449
  }
390
- const { refinement_expected: _ignored, ...requestPayload } = payload;
391
- return requestPayload;
450
+ const {
451
+ position: _ignoredPosition,
452
+ recognized_at: _ignoredRecognizedAt,
453
+ refinement_expected: _ignoredRefinementExpected,
454
+ ...requestPayload
455
+ } = payload;
456
+ return withInternalAudioMarker(requestPayload, match);
457
+ }
458
+ function withInternalAudioMarker(payload, match) {
459
+ const marker = match.audioMarker?.code;
460
+ if (typeof marker !== "string" || marker.length === 0) {
461
+ return payload;
462
+ }
463
+ return {
464
+ ...payload,
465
+ trait: {
466
+ ...payload.trait,
467
+ marker
468
+ }
469
+ };
392
470
  }
393
471
  function defaultCampaignMapper(payload, match) {
394
472
  const campaign = payload.campaign;
@@ -396,12 +474,23 @@ function defaultCampaignMapper(payload, match) {
396
474
  return null;
397
475
  }
398
476
  return {
399
- match,
477
+ match: toPublicDetectedMatch(match),
400
478
  campaign,
401
479
  activityId: typeof payload.activity_id === "string" ? payload.activity_id : null,
402
480
  raw: payload
403
481
  };
404
482
  }
483
+ function resolvedAudioMarker(payload) {
484
+ if (!payload) {
485
+ return null;
486
+ }
487
+ const id = payload.audio_marker_id;
488
+ const name = payload.audio_marker_name;
489
+ if (typeof id !== "string" || id.trim().length === 0 || typeof name !== "string" || name.trim().length === 0) {
490
+ return null;
491
+ }
492
+ return Object.freeze({ id, name });
493
+ }
405
494
  async function resolveHeaders(reporter, refinement = false) {
406
495
  const refinementHeaders = reporter.refinement && reporter.refinement.headers;
407
496
  const baseHeaders = reporter.headers ? typeof reporter.headers === "function" ? await reporter.headers() : reporter.headers : void 0;
@@ -457,10 +546,31 @@ function responseActivityId(data, campaign) {
457
546
  }
458
547
  async function mapCreateResponse(reporter, data, match, refinementExpected) {
459
548
  if (!data) {
460
- return { activityId: null, campaign: null, refinementExpected };
549
+ return {
550
+ activityId: null,
551
+ audioMarker: null,
552
+ campaign: null,
553
+ creationAttempted: true,
554
+ refinementExpected
555
+ };
556
+ }
557
+ const audioMarker = resolvedAudioMarker(data);
558
+ let campaign;
559
+ let campaignMappingFailure;
560
+ try {
561
+ campaign = reporter.mapResponseToCampaign ? await reporter.mapResponseToCampaign(data, toPublicDetectedMatch(match)) : defaultCampaignMapper(data, match);
562
+ } catch (error) {
563
+ campaign = null;
564
+ campaignMappingFailure = { error };
461
565
  }
462
- const campaign = reporter.mapResponseToCampaign ? await reporter.mapResponseToCampaign(data, match) : defaultCampaignMapper(data, match);
463
- return { activityId: responseActivityId(data, campaign), campaign, refinementExpected };
566
+ return {
567
+ activityId: responseActivityId(data, campaign),
568
+ audioMarker,
569
+ campaign,
570
+ creationAttempted: true,
571
+ refinementExpected,
572
+ ...campaignMappingFailure ? { campaignMappingFailure } : {}
573
+ };
464
574
  }
465
575
  async function resolveRefinementEndpoint(reporter, activityId) {
466
576
  const configured = reporter.refinement && reporter.refinement.endpoint;
@@ -470,10 +580,16 @@ async function resolveRefinementEndpoint(reporter, activityId) {
470
580
  const prefix = String(configured ?? reporter.endpoint);
471
581
  return `${prefix.endsWith("/") ? prefix : `${prefix}/`}${encodeURIComponent(activityId)}`;
472
582
  }
473
- async function createMatchedMaterialActivity(reporter, match, refinementExpected = false) {
583
+ async function createMatchedMaterialActivity(reporter, match, refinementExpected = false, recognitionTiming = captureActivityRecognitionTiming(match)) {
474
584
  const payload = await resolveCreateRequestPayload(reporter, match);
475
585
  if (!payload) {
476
- return { activityId: null, campaign: null, refinementExpected: false };
586
+ return {
587
+ activityId: null,
588
+ audioMarker: null,
589
+ campaign: null,
590
+ creationAttempted: false,
591
+ refinementExpected: false
592
+ };
477
593
  }
478
594
  const headers = await resolveHeaders(reporter);
479
595
  const actualMarker = match.audioMarker?.code;
@@ -481,7 +597,12 @@ async function createMatchedMaterialActivity(reporter, match, refinementExpected
481
597
  const hasMarker = typeof actualMarker === "string" && actualMarker.length > 0 || typeof payloadMarker === "string" && payloadMarker.length > 0;
482
598
  const lifecycleOpen = typeof refinementExpected === "function" ? refinementExpected() : refinementExpected;
483
599
  const requestRefinementExpected = Boolean(reporter.refinement && lifecycleOpen && !hasMarker);
484
- const requestPayload = requestRefinementExpected ? { ...payload, refinement_expected: true } : payload;
600
+ const requestPayload = {
601
+ ...payload,
602
+ recognized_at: recognitionTiming.recognizedAt,
603
+ position: recognitionTiming.position,
604
+ ...requestRefinementExpected ? { refinement_expected: true } : {}
605
+ };
485
606
  const response = await resolveFetch(reporter.fetch)(reporter.endpoint, {
486
607
  method: "POST",
487
608
  headers,
@@ -497,11 +618,11 @@ async function createMatchedMaterialActivity(reporter, match, refinementExpected
497
618
  async function refineMatchedMaterialActivity(reporter, match, context, isActive = () => true) {
498
619
  const refinement = reporter.refinement;
499
620
  if (!refinement) {
500
- return { activityId: null, campaign: null };
621
+ return { activityId: null, audioMarker: null, campaign: null };
501
622
  }
502
623
  const payload = await resolveRefineRequestPayload(reporter, match, context);
503
624
  if (!payload) {
504
- return { activityId: null, campaign: null };
625
+ return { activityId: null, audioMarker: null, campaign: null };
505
626
  }
506
627
  const endpoint = await resolveRefinementEndpoint(reporter, context.activityId);
507
628
  const headers = await resolveHeaders(reporter, true);
@@ -517,8 +638,16 @@ async function refineMatchedMaterialActivity(reporter, match, context, isActive
517
638
  if (!data) {
518
639
  throw new ActivityReportingError("Activity refine failed: empty response", response.status);
519
640
  }
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);
641
+ const audioMarker = resolvedAudioMarker(data);
642
+ let mappedCampaign;
643
+ let campaignMappingFailure;
644
+ try {
645
+ mappedCampaign = refinement.mapResponseToCampaign ? await refinement.mapResponseToCampaign(data, toPublicDetectedMatch(match), context) : reporter.mapResponseToCampaign ? await reporter.mapResponseToCampaign(data, toPublicDetectedMatch(match)) : defaultCampaignMapper(data, match);
646
+ } catch (error) {
647
+ mappedCampaign = null;
648
+ campaignMappingFailure = { error };
649
+ }
650
+ const activityId = responseActivityId(data, mappedCampaign) ?? (campaignMappingFailure ? context.activityId : null);
522
651
  if (activityId !== context.activityId) {
523
652
  throw new ActivityReportingError(
524
653
  "Activity refine failed: response activity_id does not match the requested activity",
@@ -526,11 +655,17 @@ async function refineMatchedMaterialActivity(reporter, match, context, isActive
526
655
  );
527
656
  }
528
657
  const campaign = mappedCampaign ? { ...mappedCampaign, activityId: context.activityId } : null;
529
- return { activityId, campaign };
658
+ return {
659
+ activityId,
660
+ audioMarker,
661
+ campaign,
662
+ ...campaignMappingFailure ? { campaignMappingFailure } : {}
663
+ };
530
664
  }
531
665
 
532
666
  // src/activity-refinement.ts
533
667
  var ACTIVITY_REFINEMENT_WINDOW_MS = 3e4;
668
+ var ACTIVITY_CONTINUITY_GAP_MS = 3e4;
534
669
  var DEFAULT_MAX_REFINEMENT_ATTEMPTS = 2;
535
670
  var MAX_REFINEMENT_ATTEMPTS = 10;
536
671
  function normalizeError(error) {
@@ -554,6 +689,7 @@ var ActivityRefinementCoordinator = class {
554
689
  registeredRequests = /* @__PURE__ */ new Set();
555
690
  settledRequests = /* @__PURE__ */ new Set();
556
691
  activeLifecycle = null;
692
+ activeMarkerSegment = null;
557
693
  generation = 0;
558
694
  constructor(reporter, callbacks) {
559
695
  this.reporter = reporter;
@@ -566,9 +702,7 @@ var ActivityRefinementCoordinator = class {
566
702
  }
567
703
  report(recognitionId, match, captureGeneration, requestId = null) {
568
704
  const coordinatorGeneration = this.generation;
569
- if (this.activeLifecycle && this.activeLifecycle.recognitionId !== recognitionId) {
570
- this.deleteLifecycle(this.activeLifecycle);
571
- }
705
+ const markerSegment = this.resolveMarkerSegment(match, captureGeneration);
572
706
  const hasMarker = typeof match.audioMarker?.code === "string" && match.audioMarker.code.length > 0;
573
707
  const refinement = this.reporter.refinement;
574
708
  const requestCanRefine = requestId !== null && !this.settledRequests.has(requestId);
@@ -578,53 +712,97 @@ var ActivityRefinementCoordinator = class {
578
712
  this.registeredRequests.delete(requestId);
579
713
  this.settledRequests.delete(requestId);
580
714
  }
715
+ if (refinement && this.activeLifecycle) {
716
+ const now = this.now();
717
+ const activeLifecycle = this.activeLifecycle;
718
+ if (this.isContinuousMatch(activeLifecycle, match, captureGeneration, now)) {
719
+ this.renewContinuity(activeLifecycle, now);
720
+ if (requestId !== null) {
721
+ if (requestCanRefine && this.canAcceptMarker(activeLifecycle)) {
722
+ this.requestLifecycles.set(requestId, activeLifecycle);
723
+ } else {
724
+ this.requestLifecycles.delete(requestId);
725
+ }
726
+ }
727
+ if (pendingMarker) {
728
+ this.claimMarker(activeLifecycle, pendingMarker);
729
+ }
730
+ if (hasMarker && match.audioMarker) {
731
+ this.claimMarker(activeLifecycle, match.audioMarker);
732
+ }
733
+ return;
734
+ }
735
+ this.sealLifecycle(activeLifecycle);
736
+ }
581
737
  const refinementExpected = Boolean(refinement && !hasMarker && requestCanRefine);
582
738
  let lifecycle = null;
583
- if (refinementExpected && refinement) {
584
- const now = refinement.now?.() ?? Date.now();
739
+ if (refinement) {
740
+ const now = this.now();
585
741
  lifecycle = {
586
742
  recognitionId,
587
743
  captureGeneration,
588
744
  match,
589
745
  deadline: now + ACTIVITY_REFINEMENT_WINDOW_MS,
746
+ lastObservedAt: now,
590
747
  activityId: null,
591
- markerMatch: null,
592
- markerCode: null,
593
- timer: null,
748
+ markerMatch: hasMarker ? match : null,
749
+ markerCode: hasMarker ? match.audioMarker?.code ?? null : null,
750
+ markerSegment,
751
+ refinementTimer: null,
752
+ continuityTimer: null,
594
753
  createSettled: false,
595
- refinementStarted: false
754
+ refinementStarted: false,
755
+ refinementInFlight: false,
756
+ refinementClosed: !refinementExpected,
757
+ sealed: false
596
758
  };
597
759
  const createdLifecycle = lifecycle;
598
- lifecycle.timer = setTimeout(() => {
599
- this.deleteLifecycle(createdLifecycle);
600
- }, ACTIVITY_REFINEMENT_WINDOW_MS);
760
+ if (refinementExpected) {
761
+ lifecycle.refinementTimer = setTimeout(() => {
762
+ this.closeRefinement(createdLifecycle);
763
+ }, ACTIVITY_REFINEMENT_WINDOW_MS);
764
+ }
765
+ this.scheduleContinuityExpiry(lifecycle);
601
766
  this.lifecycles.set(recognitionId, lifecycle);
602
- if (requestId !== null) {
767
+ if (requestId !== null && refinementExpected) {
603
768
  this.requestLifecycles.set(requestId, lifecycle);
604
769
  }
605
770
  this.activeLifecycle = lifecycle;
606
- if (pendingMarker) {
607
- this.refine(requestId ?? recognitionId, pendingMarker, captureGeneration);
771
+ if (pendingMarker && refinementExpected) {
772
+ this.claimMarker(lifecycle, pendingMarker);
608
773
  }
609
774
  }
775
+ const recognitionTiming = captureActivityRecognitionTiming(match);
610
776
  void createMatchedMaterialActivity(
611
777
  this.reporter,
612
778
  match,
613
- () => Boolean(lifecycle && this.isRefinementActive(lifecycle))
779
+ () => Boolean(lifecycle && this.isRefinementActive(lifecycle)),
780
+ recognitionTiming
614
781
  ).then(
615
782
  (result) => {
783
+ if (coordinatorGeneration === this.generation && result.audioMarker && this.callbacks.isCurrentCapture(captureGeneration)) {
784
+ this.emitResolvedAudioMarker(markerSegment, result.audioMarker);
785
+ }
616
786
  if (coordinatorGeneration === this.generation && result.campaign && this.callbacks.isCurrentCapture(captureGeneration)) {
617
787
  this.callbacks.emitCampaign(result.campaign);
618
788
  }
789
+ if (coordinatorGeneration === this.generation && result.campaignMappingFailure && this.callbacks.isCurrentCapture(captureGeneration)) {
790
+ this.callbacks.emitError("activity", result.campaignMappingFailure.error);
791
+ }
619
792
  if (coordinatorGeneration !== this.generation || !lifecycle || this.lifecycles.get(recognitionId) !== lifecycle) {
620
793
  return;
621
794
  }
622
- if (!result.refinementExpected) {
795
+ lifecycle.createSettled = true;
796
+ lifecycle.activityId = result.activityId;
797
+ if (!result.creationAttempted) {
623
798
  this.deleteLifecycle(lifecycle);
624
799
  return;
625
800
  }
626
- lifecycle.createSettled = true;
627
- lifecycle.activityId = result.activityId;
801
+ if (!result.refinementExpected) {
802
+ this.closeRefinement(lifecycle);
803
+ this.maybeDeleteSealed(lifecycle);
804
+ return;
805
+ }
628
806
  if (lifecycle.markerMatch) {
629
807
  if (lifecycle.activityId) {
630
808
  this.startRefinement(lifecycle);
@@ -632,9 +810,11 @@ var ActivityRefinementCoordinator = class {
632
810
  this.failMissingActivityId(lifecycle);
633
811
  }
634
812
  }
813
+ this.maybeDeleteSealed(lifecycle);
635
814
  },
636
815
  (error) => {
637
816
  if (lifecycle) {
817
+ lifecycle.createSettled = true;
638
818
  this.deleteLifecycle(lifecycle);
639
819
  }
640
820
  if (coordinatorGeneration === this.generation && this.callbacks.isCurrentCapture(captureGeneration)) {
@@ -646,14 +826,26 @@ var ActivityRefinementCoordinator = class {
646
826
  associate(requestId, match, captureGeneration) {
647
827
  const lifecycle = this.activeLifecycle;
648
828
  this.registeredRequests.delete(requestId);
649
- if (!lifecycle || !match || lifecycle.captureGeneration !== captureGeneration || lifecycle.match.name !== match.name || lifecycle.match.afpType !== match.afpType || !this.isActive(lifecycle)) {
829
+ if (!lifecycle || !match) {
830
+ return false;
831
+ }
832
+ const now = this.now();
833
+ if (!this.isContinuousMatch(lifecycle, match, captureGeneration, now)) {
834
+ this.sealLifecycle(lifecycle);
650
835
  return false;
651
836
  }
652
- this.requestLifecycles.set(requestId, lifecycle);
837
+ this.renewContinuity(lifecycle, now);
653
838
  const pendingMarker = this.pendingMarkers.get(requestId);
654
- if (pendingMarker) {
839
+ if (this.canAcceptMarker(lifecycle)) {
840
+ this.requestLifecycles.set(requestId, lifecycle);
841
+ } else {
842
+ this.requestLifecycles.delete(requestId);
843
+ }
844
+ if (pendingMarker && this.canAcceptMarker(lifecycle)) {
845
+ this.pendingMarkers.delete(requestId);
846
+ this.claimMarker(lifecycle, pendingMarker);
847
+ } else if (pendingMarker) {
655
848
  this.pendingMarkers.delete(requestId);
656
- this.refine(requestId, pendingMarker, captureGeneration);
657
849
  }
658
850
  return true;
659
851
  }
@@ -666,18 +858,17 @@ var ActivityRefinementCoordinator = class {
666
858
  settleRequest(requestId) {
667
859
  const awaitingAssociation = this.registeredRequests.delete(requestId);
668
860
  this.pendingMarkers.delete(requestId);
669
- const lifecycle = this.requestLifecycles.get(requestId);
670
- if (lifecycle) {
671
- this.deleteLifecycle(lifecycle);
672
- return;
673
- }
861
+ this.requestLifecycles.delete(requestId);
674
862
  if (awaitingAssociation) {
675
863
  this.settledRequests.add(requestId);
676
864
  }
677
865
  }
678
866
  clearActive(captureGeneration) {
679
867
  if (this.activeLifecycle?.captureGeneration === captureGeneration) {
680
- this.deleteLifecycle(this.activeLifecycle);
868
+ this.sealLifecycle(this.activeLifecycle);
869
+ }
870
+ if (this.activeMarkerSegment?.captureGeneration === captureGeneration) {
871
+ this.activeMarkerSegment = null;
681
872
  }
682
873
  }
683
874
  refine(recognitionId, marker, captureGeneration) {
@@ -696,24 +887,19 @@ var ActivityRefinementCoordinator = class {
696
887
  }
697
888
  return;
698
889
  }
699
- const now = this.reporter.refinement?.now?.() ?? Date.now();
700
- if (now > lifecycle.deadline) {
701
- this.deleteLifecycle(lifecycle);
890
+ if (!this.canAcceptMarker(lifecycle)) {
702
891
  return;
703
892
  }
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
- }
893
+ this.claimMarker(lifecycle, marker);
711
894
  }
712
895
  reset() {
713
896
  this.generation += 1;
714
897
  for (const lifecycle of this.lifecycles.values()) {
715
- if (lifecycle.timer) {
716
- clearTimeout(lifecycle.timer);
898
+ if (lifecycle.refinementTimer) {
899
+ clearTimeout(lifecycle.refinementTimer);
900
+ }
901
+ if (lifecycle.continuityTimer) {
902
+ clearTimeout(lifecycle.continuityTimer);
717
903
  }
718
904
  }
719
905
  this.lifecycles.clear();
@@ -722,19 +908,24 @@ var ActivityRefinementCoordinator = class {
722
908
  this.registeredRequests.clear();
723
909
  this.settledRequests.clear();
724
910
  this.activeLifecycle = null;
911
+ this.activeMarkerSegment = null;
725
912
  }
726
913
  startRefinement(lifecycle) {
727
- if (lifecycle.refinementStarted || !lifecycle.activityId || !lifecycle.markerMatch) {
914
+ if (lifecycle.refinementStarted || lifecycle.refinementClosed || !lifecycle.activityId || !lifecycle.markerMatch) {
728
915
  return;
729
916
  }
730
917
  lifecycle.refinementStarted = true;
731
- void this.runRefinement(lifecycle);
918
+ lifecycle.refinementInFlight = true;
919
+ void this.runRefinement(lifecycle).finally(() => {
920
+ lifecycle.refinementInFlight = false;
921
+ this.maybeDeleteSealed(lifecycle);
922
+ });
732
923
  }
733
924
  failMissingActivityId(lifecycle) {
734
925
  if (!this.isRefinementActive(lifecycle)) {
735
926
  return;
736
927
  }
737
- this.deleteLifecycle(lifecycle);
928
+ this.closeRefinement(lifecycle);
738
929
  this.callbacks.emitError(
739
930
  "activity-refinement",
740
931
  new Error("Activity refinement requires a non-empty activity_id from the create response")
@@ -743,7 +934,7 @@ var ActivityRefinementCoordinator = class {
743
934
  async runRefinement(lifecycle) {
744
935
  const refinement = this.reporter.refinement;
745
936
  if (!refinement || !lifecycle.activityId || !lifecycle.markerMatch) {
746
- this.deleteLifecycle(lifecycle);
937
+ this.closeRefinement(lifecycle);
747
938
  return;
748
939
  }
749
940
  const maxAttempts = Math.min(
@@ -754,12 +945,12 @@ var ActivityRefinementCoordinator = class {
754
945
  )
755
946
  );
756
947
  for (let attempt = 1; attempt <= maxAttempts; attempt += 1) {
757
- if (!this.isActive(lifecycle)) {
948
+ if (!this.isRefinementActive(lifecycle)) {
758
949
  return;
759
950
  }
760
- const now = refinement.now?.() ?? Date.now();
951
+ const now = this.now();
761
952
  if (now > lifecycle.deadline) {
762
- this.deleteLifecycle(lifecycle);
953
+ this.closeRefinement(lifecycle);
763
954
  return;
764
955
  }
765
956
  try {
@@ -777,10 +968,19 @@ var ActivityRefinementCoordinator = class {
777
968
  if (!this.isRefinementActive(lifecycle)) {
778
969
  return;
779
970
  }
780
- this.deleteLifecycle(lifecycle);
971
+ this.closeRefinement(lifecycle);
972
+ if (result.audioMarker) {
973
+ this.emitResolvedAudioMarker(lifecycle.markerSegment, result.audioMarker);
974
+ }
781
975
  if (result.campaign) {
782
976
  this.callbacks.emitCampaign(result.campaign);
783
977
  }
978
+ if (result.campaignMappingFailure) {
979
+ this.callbacks.emitError(
980
+ "activity-refinement",
981
+ result.campaignMappingFailure.error
982
+ );
983
+ }
784
984
  return;
785
985
  } catch (caught) {
786
986
  if (caught instanceof ActivityReportingCancelledError) {
@@ -807,7 +1007,7 @@ var ActivityRefinementCoordinator = class {
807
1007
  if (!this.isRefinementActive(lifecycle)) {
808
1008
  return;
809
1009
  }
810
- this.deleteLifecycle(lifecycle);
1010
+ this.closeRefinement(lifecycle);
811
1011
  this.callbacks.emitError("activity-refinement", policyError);
812
1012
  return;
813
1013
  }
@@ -817,35 +1017,119 @@ var ActivityRefinementCoordinator = class {
817
1017
  if (retry) {
818
1018
  continue;
819
1019
  }
820
- this.deleteLifecycle(lifecycle);
1020
+ this.closeRefinement(lifecycle);
821
1021
  this.callbacks.emitError("activity-refinement", error);
822
1022
  return;
823
1023
  }
824
1024
  }
825
1025
  }
826
- isActive(lifecycle) {
1026
+ isTransportActive(lifecycle) {
827
1027
  return this.lifecycles.get(lifecycle.recognitionId) === lifecycle && this.callbacks.isCurrentCapture(lifecycle.captureGeneration);
828
1028
  }
829
1029
  isRefinementActive(lifecycle) {
830
- if (!this.isActive(lifecycle)) {
1030
+ if (!this.isTransportActive(lifecycle) || lifecycle.refinementClosed) {
831
1031
  return false;
832
1032
  }
833
- const refinement = this.reporter.refinement;
834
- const now = refinement && refinement.now ? refinement.now() : Date.now();
1033
+ const now = this.now();
835
1034
  if (now > lifecycle.deadline) {
836
- this.deleteLifecycle(lifecycle);
1035
+ this.closeRefinement(lifecycle);
837
1036
  return false;
838
1037
  }
839
1038
  return true;
840
1039
  }
841
- deleteLifecycle(lifecycle) {
842
- if (lifecycle.timer) {
843
- clearTimeout(lifecycle.timer);
844
- lifecycle.timer = null;
1040
+ canAcceptMarker(lifecycle) {
1041
+ return lifecycle.markerCode === null && this.isRefinementActive(lifecycle);
1042
+ }
1043
+ claimMarker(lifecycle, marker) {
1044
+ const markerCode = marker.code;
1045
+ if (typeof markerCode !== "string" || markerCode.length === 0 || !this.canAcceptMarker(lifecycle)) {
1046
+ return;
845
1047
  }
846
- if (this.lifecycles.get(lifecycle.recognitionId) === lifecycle) {
847
- this.lifecycles.delete(lifecycle.recognitionId);
1048
+ lifecycle.markerCode = markerCode;
1049
+ lifecycle.markerMatch = { ...lifecycle.match, audioMarker: marker };
1050
+ if (lifecycle.activityId) {
1051
+ this.startRefinement(lifecycle);
1052
+ } else if (lifecycle.createSettled) {
1053
+ this.failMissingActivityId(lifecycle);
1054
+ }
1055
+ }
1056
+ isContinuousMatch(lifecycle, match, captureGeneration, now) {
1057
+ return !lifecycle.sealed && this.activeLifecycle === lifecycle && lifecycle.captureGeneration === captureGeneration && lifecycle.match.name === match.name && lifecycle.match.afpType === match.afpType && this.isTransportActive(lifecycle) && now - lifecycle.lastObservedAt <= ACTIVITY_CONTINUITY_GAP_MS;
1058
+ }
1059
+ resolveMarkerSegment(match, captureGeneration) {
1060
+ const now = this.now();
1061
+ const active = this.activeMarkerSegment;
1062
+ if (active && active.captureGeneration === captureGeneration && active.materialName === match.name && active.afpType === match.afpType && now - active.lastObservedAt <= ACTIVITY_CONTINUITY_GAP_MS) {
1063
+ active.lastObservedAt = now;
1064
+ return active;
1065
+ }
1066
+ const segment = {
1067
+ captureGeneration,
1068
+ materialName: match.name,
1069
+ afpType: match.afpType,
1070
+ lastObservedAt: now,
1071
+ emitted: false
1072
+ };
1073
+ this.activeMarkerSegment = segment;
1074
+ return segment;
1075
+ }
1076
+ emitResolvedAudioMarker(segment, marker) {
1077
+ if (segment.emitted) {
1078
+ return;
1079
+ }
1080
+ segment.emitted = true;
1081
+ this.callbacks.emitAudioMarker(Object.freeze({ marker }));
1082
+ }
1083
+ renewContinuity(lifecycle, now) {
1084
+ lifecycle.lastObservedAt = now;
1085
+ this.scheduleContinuityExpiry(lifecycle);
1086
+ }
1087
+ scheduleContinuityExpiry(lifecycle) {
1088
+ if (lifecycle.continuityTimer) {
1089
+ clearTimeout(lifecycle.continuityTimer);
1090
+ }
1091
+ const remainingMs = lifecycle.lastObservedAt + ACTIVITY_CONTINUITY_GAP_MS - this.now();
1092
+ lifecycle.continuityTimer = setTimeout(() => {
1093
+ lifecycle.continuityTimer = null;
1094
+ if (this.activeLifecycle !== lifecycle || lifecycle.sealed) {
1095
+ return;
1096
+ }
1097
+ const now = this.now();
1098
+ if (now - lifecycle.lastObservedAt <= ACTIVITY_CONTINUITY_GAP_MS) {
1099
+ this.scheduleContinuityExpiry(lifecycle);
1100
+ return;
1101
+ }
1102
+ this.sealLifecycle(lifecycle);
1103
+ }, Math.max(1, remainingMs + 1));
1104
+ }
1105
+ sealLifecycle(lifecycle) {
1106
+ if (lifecycle.sealed) {
1107
+ return;
848
1108
  }
1109
+ lifecycle.sealed = true;
1110
+ if (lifecycle.continuityTimer) {
1111
+ clearTimeout(lifecycle.continuityTimer);
1112
+ lifecycle.continuityTimer = null;
1113
+ }
1114
+ if (this.activeLifecycle === lifecycle) {
1115
+ this.activeLifecycle = null;
1116
+ }
1117
+ this.removeRequestAssociations(lifecycle);
1118
+ this.maybeDeleteSealed(lifecycle);
1119
+ }
1120
+ closeRefinement(lifecycle) {
1121
+ if (lifecycle.refinementClosed) {
1122
+ return;
1123
+ }
1124
+ lifecycle.refinementClosed = true;
1125
+ if (lifecycle.refinementTimer) {
1126
+ clearTimeout(lifecycle.refinementTimer);
1127
+ lifecycle.refinementTimer = null;
1128
+ }
1129
+ this.removeRequestAssociations(lifecycle);
1130
+ this.maybeDeleteSealed(lifecycle);
1131
+ }
1132
+ removeRequestAssociations(lifecycle) {
849
1133
  for (const [requestId, requestLifecycle] of this.requestLifecycles) {
850
1134
  if (requestLifecycle === lifecycle) {
851
1135
  this.requestLifecycles.delete(requestId);
@@ -853,6 +1137,28 @@ var ActivityRefinementCoordinator = class {
853
1137
  this.registeredRequests.delete(requestId);
854
1138
  }
855
1139
  }
1140
+ }
1141
+ maybeDeleteSealed(lifecycle) {
1142
+ if (lifecycle.sealed && lifecycle.createSettled && !lifecycle.refinementInFlight) {
1143
+ this.deleteLifecycle(lifecycle);
1144
+ }
1145
+ }
1146
+ now() {
1147
+ return this.reporter.refinement && this.reporter.refinement.now ? this.reporter.refinement.now() : Date.now();
1148
+ }
1149
+ deleteLifecycle(lifecycle) {
1150
+ if (lifecycle.refinementTimer) {
1151
+ clearTimeout(lifecycle.refinementTimer);
1152
+ lifecycle.refinementTimer = null;
1153
+ }
1154
+ if (lifecycle.continuityTimer) {
1155
+ clearTimeout(lifecycle.continuityTimer);
1156
+ lifecycle.continuityTimer = null;
1157
+ }
1158
+ if (this.lifecycles.get(lifecycle.recognitionId) === lifecycle) {
1159
+ this.lifecycles.delete(lifecycle.recognitionId);
1160
+ }
1161
+ this.removeRequestAssociations(lifecycle);
856
1162
  if (this.activeLifecycle === lifecycle) {
857
1163
  this.activeLifecycle = null;
858
1164
  }
@@ -1020,7 +1326,6 @@ var MicrophoneMatcher = class {
1020
1326
  markerPcmBuffer = new Float32Array(0);
1021
1327
  resampleBuffer = new Float32Array(0);
1022
1328
  resampleOffset = 0;
1023
- latestAudioMarker = null;
1024
1329
  latestAudioMarkerDetection = null;
1025
1330
  audioMarkerRequestId = 0;
1026
1331
  activityRecognitionId = 0;
@@ -1047,6 +1352,7 @@ var MicrophoneMatcher = class {
1047
1352
  this.packSource = options.packSource;
1048
1353
  this.activityRefinement = options.activityReporter ? new ActivityRefinementCoordinator(options.activityReporter, {
1049
1354
  isCurrentCapture: (captureGeneration) => captureGeneration === this.captureGeneration && this.running,
1355
+ emitAudioMarker: (event) => this.events.emit("audiomarker", event),
1050
1356
  emitCampaign: (campaign) => this.events.emit("campaign", campaign),
1051
1357
  emitError: (phase, error) => emitError(this.events, phase, error)
1052
1358
  }) : null;
@@ -1316,7 +1622,7 @@ var MicrophoneMatcher = class {
1316
1622
  return;
1317
1623
  }
1318
1624
  if (matchedBest) {
1319
- this.events.emit("match", { best: matchedBest });
1625
+ this.events.emit("match", { best: toPublicDetectedMatch(matchedBest) });
1320
1626
  this.activityRecognitionId += 1;
1321
1627
  this.reportCampaign(
1322
1628
  this.activityRecognitionId,
@@ -1348,7 +1654,6 @@ var MicrophoneMatcher = class {
1348
1654
  this.markerPcmBuffer = new Float32Array(0);
1349
1655
  this.resampleBuffer = new Float32Array(0);
1350
1656
  this.resampleOffset = 0;
1351
- this.latestAudioMarker = null;
1352
1657
  this.latestAudioMarkerDetection = null;
1353
1658
  this.audioMarkerRequestId += 1;
1354
1659
  this.activityRefinement?.reset();
@@ -1384,7 +1689,6 @@ var MicrophoneMatcher = class {
1384
1689
  return null;
1385
1690
  }
1386
1691
  this.latestAudioMarkerDetection = { requestId, detection };
1387
- this.emitAudioMarkerIfChanged(detection);
1388
1692
  this.activityRefinement?.refine(requestId, detection, captureGeneration);
1389
1693
  return detection;
1390
1694
  } catch (error) {
@@ -1411,14 +1715,6 @@ var MicrophoneMatcher = class {
1411
1715
  audioMarker: this.latestAudioMarkerDetection.detection
1412
1716
  };
1413
1717
  }
1414
- emitAudioMarkerIfChanged(detection) {
1415
- const marker = detection.code ?? null;
1416
- if (marker === this.latestAudioMarker) {
1417
- return;
1418
- }
1419
- this.latestAudioMarker = marker;
1420
- this.events.emit("audiomarker", { marker, detection });
1421
- }
1422
1718
  normalizeInputSamples(samples) {
1423
1719
  if (this.audioContext.sampleRate === TARGET_SAMPLE_RATE) {
1424
1720
  return samples;
@@ -1576,6 +1872,8 @@ function createLocalStorageSessionManager(options) {
1576
1872
 
1577
1873
  // src/audio-recognizer.ts
1578
1874
  var DEFAULT_SORI_API_ENDPOINT = "https://console.soriapi.com/api";
1875
+ var MAX_API_ENDPOINT_LENGTH = 32 * 1024;
1876
+ var ASCII_SLASH_CODE_UNIT = 47;
1579
1877
  var FORWARDED_EVENTS = [
1580
1878
  "ready",
1581
1879
  "microphoneready",
@@ -1603,12 +1901,24 @@ function ensureNonEmptyString(name, value) {
1603
1901
  }
1604
1902
  return value;
1605
1903
  }
1606
- function normalizeEndpointPrefix(endpoint) {
1607
- const value = String(endpoint).replace(/\/+$/, "");
1904
+ function stringifyAndValidateApiEndpoint(endpoint) {
1905
+ const value = String(endpoint);
1906
+ if (value.length > MAX_API_ENDPOINT_LENGTH) {
1907
+ throw new Error(
1908
+ `AudioRecognizer apiEndpoint exceeds maximum length of ${MAX_API_ENDPOINT_LENGTH} UTF-16 code units`
1909
+ );
1910
+ }
1608
1911
  return value;
1609
1912
  }
1913
+ function normalizeValidatedEndpointPrefix(value) {
1914
+ let end = value.length;
1915
+ while (end > 0 && value.charCodeAt(end - 1) === ASCII_SLASH_CODE_UNIT) {
1916
+ end -= 1;
1917
+ }
1918
+ return end === value.length ? value : value.slice(0, end);
1919
+ }
1610
1920
  function joinEndpoint(prefix, path) {
1611
- return `${normalizeEndpointPrefix(prefix)}${path.startsWith("/") ? path : `/${path}`}`;
1921
+ return `${prefix}${path.startsWith("/") ? path : `/${path}`}`;
1612
1922
  }
1613
1923
  function createEphemeralKeyResolver(endpoint, fetcher, requestInit) {
1614
1924
  return async () => {
@@ -1636,8 +1946,15 @@ function resolveRecognizerOptions(options) {
1636
1946
  const activityReportingDisabled = options.activityReporter === false;
1637
1947
  const customActivityReporter = activityReportingDisabled ? void 0 : options.activityReporter;
1638
1948
  const appId = legacyAuth?.appId ?? options.appId;
1639
- const authEndpoint = legacyAuth?.endpoint ?? options.authEndpoint ?? joinEndpoint(apiEndpoint, "/auth");
1640
- const activityEndpoint = activityReportingDisabled ? void 0 : customActivityReporter?.endpoint ?? options.activityEndpoint ?? joinEndpoint(apiEndpoint, "/activity/");
1949
+ const configuredAuthEndpoint = legacyAuth?.endpoint ?? options.authEndpoint;
1950
+ const configuredActivityEndpoint = activityReportingDisabled ? void 0 : customActivityReporter?.endpoint ?? options.activityEndpoint;
1951
+ const derivesAuthEndpoint = configuredAuthEndpoint == null;
1952
+ const derivesActivityEndpoint = !activityReportingDisabled && configuredActivityEndpoint == null;
1953
+ const derivesApiEndpoint = derivesAuthEndpoint || derivesActivityEndpoint;
1954
+ const validatedApiEndpoint = options.apiEndpoint !== void 0 || derivesApiEndpoint ? stringifyAndValidateApiEndpoint(apiEndpoint) : void 0;
1955
+ const normalizedApiEndpoint = derivesApiEndpoint ? normalizeValidatedEndpointPrefix(validatedApiEndpoint) : void 0;
1956
+ const authEndpoint = configuredAuthEndpoint ?? joinEndpoint(normalizedApiEndpoint, "/auth");
1957
+ const activityEndpoint = activityReportingDisabled ? void 0 : configuredActivityEndpoint ?? joinEndpoint(normalizedApiEndpoint, "/activity/");
1641
1958
  const ephemeralKey = legacyAuth?.ephemeralKey ?? options.ephemeralKey ?? options.ephemeraKey ?? (legacyAuth?.ephemeralKeyEndpoint ?? options.ephemeralKeyEndpoint ? createEphemeralKeyResolver(
1642
1959
  legacyAuth?.ephemeralKeyEndpoint ?? options.ephemeralKeyEndpoint,
1643
1960
  legacyAuth?.ephemeralKeyFetch ?? options.ephemeralKeyFetch ?? legacyAuth?.fetch ?? sharedFetch,
@@ -1825,6 +2142,7 @@ export {
1825
2142
  AudioRecognizer,
1826
2143
  DEFAULT_BROWSER_AUDIOPACK_VERSION,
1827
2144
  DEFAULT_SORI_API_ENDPOINT,
2145
+ MAX_API_ENDPOINT_LENGTH,
1828
2146
  MicrophoneMatcher,
1829
2147
  authenticateAndLoadAudioPack,
1830
2148
  createLocalStorageAudioPackStore,