@sorisdk/web-audio 0.6.7 → 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/README.md CHANGED
@@ -13,6 +13,36 @@ Public integration guide: https://docs.soriapi.com/ko/integration/web
13
13
  - An ephemeral key issued by your server
14
14
  - Microphone permission granted by the user
15
15
 
16
+ ## Unreleased: Console-resolved audio marker events
17
+
18
+ The public `audiomarker` event now contains only the marker identity managed in
19
+ SORI Console. The SDK emits it after a successful activity POST or refinement
20
+ PUT returns both a non-blank `audio_marker_id` and `audio_marker_name`:
21
+
22
+ ```ts
23
+ recognizer.on("audiomarker", ({ marker }) => {
24
+ console.log(marker.id, marker.name);
25
+ });
26
+ ```
27
+
28
+ The event is independent from campaign mapping and can be emitted when the
29
+ activity response has no campaign. Repeated responses within one continuous
30
+ material segment emit once; an intervening material, no-match, capture reset,
31
+ or expired continuity gap starts a new segment that may emit again.
32
+
33
+ Local WASM detection and labels such as `code_000` remain private inputs to the
34
+ activity request. They are no longer present on public `audiomarker` or `match`
35
+ events, `campaign.match`, the exported `DetectedMatch`, or the
36
+ `@sorisdk/web-audio` type exports. Custom request and campaign mappers receive
37
+ the public codebook-free match; the SDK adds its private `trait.marker` only to
38
+ the outgoing activity payload after request mapping.
39
+
40
+ This deliberately replaces the 0.6.7-and-earlier event shape. Migrate from
41
+ `event.marker` as a nullable string and `event.detection` as a raw detector
42
+ result to the non-null `event.marker.id` and `event.marker.name` fields above.
43
+ There is no legacy fallback: disabled activity reporting, failed transport, or
44
+ a response with a missing or malformed id/name pair emits no public marker.
45
+
16
46
  ## Standalone CDN
17
47
 
18
48
  For a browser application that does not use npm or a bundler, import the
@@ -22,7 +52,7 @@ configuration, or Node-based asset server is required:
22
52
  ```html
23
53
  <script type="module">
24
54
  import { AudioRecognizer } from
25
- "https://cdn.iplateia.com/web/sorisdk/v0.6.7/sori-web-audio.mjs";
55
+ "https://cdn.iplateia.com/web/sorisdk/v0.6.8/sori-web-audio.mjs";
26
56
 
27
57
  const recognizer = new AudioRecognizer({
28
58
  appId: "YOUR_APP_ID",
@@ -49,7 +79,7 @@ configuration, or Node-based asset server is required:
49
79
  ```
50
80
 
51
81
  Static URL imports are valid inside `<script type="module">`. Dynamic
52
- `await import("https://cdn.iplateia.com/web/sorisdk/v0.6.7/sori-web-audio.mjs")`
82
+ `await import("https://cdn.iplateia.com/web/sorisdk/v0.6.8/sori-web-audio.mjs")`
53
83
  is an optional alternative when the SDK should be loaded conditionally.
54
84
 
55
85
  Pin an exact version in production. Do not construct a mutable `latest` URL.
@@ -119,7 +149,7 @@ An import map can give the standalone URL the npm package name:
119
149
  <script type="importmap">
120
150
  {
121
151
  "imports": {
122
- "@sorisdk/web-audio": "https://cdn.iplateia.com/web/sorisdk/v0.6.7/sori-web-audio.mjs"
152
+ "@sorisdk/web-audio": "https://cdn.iplateia.com/web/sorisdk/v0.6.8/sori-web-audio.mjs"
123
153
  }
124
154
  }
125
155
  </script>
@@ -171,6 +201,10 @@ recognizer.on("campaign", (event) => {
171
201
  console.log(event.activityId, event.campaign);
172
202
  });
173
203
 
204
+ recognizer.on("audiomarker", ({ marker }) => {
205
+ console.log(marker.id, marker.name);
206
+ });
207
+
174
208
  recognizer.on("error", ({ error }) => {
175
209
  console.error(error);
176
210
  });
@@ -200,10 +234,12 @@ server activity:
200
234
  3. A marker-free match with an active marker-scan request also includes
201
235
  top-level `refinement_expected: true`.
202
236
  4. The returned `activity_id` is retained for 30 seconds.
203
- 5. If the same recognition gains `audioMarker.code`, the SDK sends
237
+ 5. If the same recognition gains a private local marker detection, the SDK sends
204
238
  `PUT /api/activity/{activity_id}` with the same material and
205
239
  `trait.marker`.
206
- 6. The refined `campaign` event keeps the same non-empty `activityId`, so an
240
+ 6. A valid response marker id/name pair emits one public structured
241
+ `audiomarker` event, even when the response has no campaign.
242
+ 7. The refined `campaign` event keeps the same non-empty `activityId`, so an
207
243
  application can replace the earlier campaign row deterministically.
208
244
 
209
245
  A consecutive match for the same material and fingerprint type renews a local
@@ -228,10 +264,10 @@ refine the earlier A. Marker evidence arriving after a segment is sealed is
228
264
  ignored for that segment; create or PUT transport that already started may
229
265
  finish under the existing capture, deadline, and cancellation guards.
230
266
 
231
- Ordinary fingerprint `match` and marker-state `audiomarker` events remain
232
- independently observable; coalescing only changes activity mutation and
233
- campaign delivery. An `audiomarker` event alone does not mean that server
234
- activity refinement completed.
267
+ Ordinary fingerprint `match` events remain independently observable and never
268
+ contain the private detector result. Public `audiomarker` delivery is sourced
269
+ only from a completed activity response. It is independent from campaign
270
+ availability but does prove that the server resolved the marker id and name.
235
271
 
236
272
  Recognition timing belongs only to the segment's first material POST. Later
237
273
  same-material observations update only the internal `lastObservedAt` lease;
@@ -273,7 +309,10 @@ Existing `mapMatchToRequest` and `mapResponseToCampaign` hooks continue to
273
309
  handle POST. Additive refinement hooks can override the PUT payload, response
274
310
  mapping, endpoint, headers, fetcher, and retry policy. The stable terminal
275
311
  statuses 404, 409, and 410 are never retried even if a custom policy returns
276
- `true`.
312
+ `true`. Match arguments passed to these hooks omit private detector state. When
313
+ local detection succeeds, the SDK injects its private marker into the outgoing
314
+ `trait.marker` after request mapping, while resolved public identity is parsed
315
+ directly from the unmodified activity response before campaign delivery.
277
316
 
278
317
  `recognized_at` and `position` are optional public request properties for
279
318
  source compatibility, but they are SDK-owned transport values. After a custom
@@ -404,7 +443,8 @@ fingerprint chunks and the existing match-window cadence are unchanged.
404
443
 
405
444
  1. Prepare your `appId` and ephemeral key flow.
406
445
  2. Create an `AudioRecognizer`.
407
- 3. Subscribe to the events you need, such as `campaign`, `match`, and `error`.
446
+ 3. Subscribe to the events you need, such as `campaign`, `audiomarker`, `match`,
447
+ and `error`.
408
448
  4. Call `start()` to begin recognition.
409
449
  5. Call `stop()` or `destroy()` when leaving the page or stopping capture.
410
450
 
Binary file
package/dist/index.d.ts CHANGED
@@ -1,5 +1,5 @@
1
- import { MatcherEphemeralAuthOptions, InitMatcherOptions, MatcherEphemeralAuthResult, AudioMarkerDetection, PackSource, MatchResult, MatcherSessionOptions, AudioFingerprintType } from '@sorisdk/matcher';
2
- export { AudioFingerprintType, AudioMarkerConfig, AudioMarkerDetection, PackSource } from '@sorisdk/matcher';
1
+ import { MatcherEphemeralAuthOptions, InitMatcherOptions, MatcherEphemeralAuthResult, PackSource, MatchResult, MatcherSessionOptions, AudioFingerprintType } from '@sorisdk/matcher';
2
+ export { AudioFingerprintType, AudioMarkerConfig, PackSource } from '@sorisdk/matcher';
3
3
  import { InitAfpgenOptions } from '@sorisdk/afpgen';
4
4
 
5
5
  declare const DEFAULT_BROWSER_AUDIOPACK_VERSION = "20220401000000";
@@ -53,11 +53,14 @@ interface InitMatchWindowWasmOptions {
53
53
  }
54
54
 
55
55
  interface DetectedMatch extends Pick<MatchResult, "afpType" | "durationMillis" | "name" | "position" | "score"> {
56
- audioMarker?: AudioMarkerDetection | null;
56
+ }
57
+ /** SORI Console-managed identity resolved from a successful activity response. */
58
+ interface AudioMarkerIdentity {
59
+ readonly id: string;
60
+ readonly name: string;
57
61
  }
58
62
  interface AudioMarkerEvent {
59
- marker: string | null;
60
- detection: AudioMarkerDetection;
63
+ readonly marker: AudioMarkerIdentity;
61
64
  }
62
65
  interface MaterialActivityRequestPayload {
63
66
  type: "material";
@@ -243,7 +246,6 @@ declare class MicrophoneMatcher {
243
246
  private markerPcmBuffer;
244
247
  private resampleBuffer;
245
248
  private resampleOffset;
246
- private latestAudioMarker;
247
249
  private latestAudioMarkerDetection;
248
250
  private audioMarkerRequestId;
249
251
  private activityRecognitionId;
@@ -281,7 +283,6 @@ declare class MicrophoneMatcher {
281
283
  private detectAudioMarkerForRequest;
282
284
  private matchConfig;
283
285
  private withAudioMarkerForRequest;
284
- private emitAudioMarkerIfChanged;
285
286
  private normalizeInputSamples;
286
287
  private resampleToTargetRate;
287
288
  private cleanupAfterStartFailure;
@@ -296,4 +297,4 @@ declare function requestMicrophoneStream(mediaDevices: MediaDevices, mediaConstr
296
297
  preferredSampleRate?: number;
297
298
  }): Promise<MediaStream>;
298
299
 
299
- 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, MAX_API_ENDPOINT_LENGTH, type MaterialActivityRequestPayload, MicrophoneMatcher, type MicrophoneMatcherActivityRefinementOptions, type MicrophoneMatcherActivityReporterOptions, type MicrophoneMatcherCampaignEvent, type MicrophoneMatcherEventMap, type MicrophoneMatcherEventName, type MicrophoneMatcherListener, authenticateAndLoadAudioPack, createLocalStorageAudioPackStore, createLocalStorageSessionManager, createMicrophoneMatcher, requestMicrophoneStream };
300
+ export { ACTIVITY_REFINEMENT_WINDOW_MS, type ActivityRefinementFailureContext, type ActivityRefinementRequestContext, type AudioMarkerEvent, type AudioMarkerIdentity, 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, MAX_API_ENDPOINT_LENGTH, 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
@@ -354,6 +354,12 @@ var FingerprintMatchWindow = class {
354
354
  }
355
355
  };
356
356
 
357
+ // src/detected-match.ts
358
+ function toPublicDetectedMatch(match) {
359
+ const { audioMarker: _internalAudioMarker, ...publicMatch } = match;
360
+ return publicMatch;
361
+ }
362
+
357
363
  // src/activity.ts
358
364
  var ActivityReportingError = class extends Error {
359
365
  status;
@@ -417,7 +423,7 @@ function validateRequestPayload(payload) {
417
423
  }
418
424
  async function resolveCreateRequestPayload(reporter, match) {
419
425
  const payload = validateRequestPayload(
420
- reporter.mapMatchToRequest ? await reporter.mapMatchToRequest(match) : defaultRequestPayload(match)
426
+ reporter.mapMatchToRequest ? await reporter.mapMatchToRequest(toPublicDetectedMatch(match)) : defaultRequestPayload(match)
421
427
  );
422
428
  if (!payload) {
423
429
  return null;
@@ -428,7 +434,7 @@ async function resolveCreateRequestPayload(reporter, match) {
428
434
  refinement_expected: _ignoredRefinementExpected,
429
435
  ...requestPayload
430
436
  } = payload;
431
- return requestPayload;
437
+ return withInternalAudioMarker(requestPayload, match);
432
438
  }
433
439
  async function resolveRefineRequestPayload(reporter, match, context) {
434
440
  const refinement = reporter.refinement;
@@ -436,7 +442,7 @@ async function resolveRefineRequestPayload(reporter, match, context) {
436
442
  return null;
437
443
  }
438
444
  const payload = validateRequestPayload(
439
- 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)
440
446
  );
441
447
  if (!payload) {
442
448
  return null;
@@ -447,7 +453,20 @@ async function resolveRefineRequestPayload(reporter, match, context) {
447
453
  refinement_expected: _ignoredRefinementExpected,
448
454
  ...requestPayload
449
455
  } = payload;
450
- return requestPayload;
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
+ };
451
470
  }
452
471
  function defaultCampaignMapper(payload, match) {
453
472
  const campaign = payload.campaign;
@@ -455,12 +474,23 @@ function defaultCampaignMapper(payload, match) {
455
474
  return null;
456
475
  }
457
476
  return {
458
- match,
477
+ match: toPublicDetectedMatch(match),
459
478
  campaign,
460
479
  activityId: typeof payload.activity_id === "string" ? payload.activity_id : null,
461
480
  raw: payload
462
481
  };
463
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
+ }
464
494
  async function resolveHeaders(reporter, refinement = false) {
465
495
  const refinementHeaders = reporter.refinement && reporter.refinement.headers;
466
496
  const baseHeaders = reporter.headers ? typeof reporter.headers === "function" ? await reporter.headers() : reporter.headers : void 0;
@@ -518,17 +548,28 @@ async function mapCreateResponse(reporter, data, match, refinementExpected) {
518
548
  if (!data) {
519
549
  return {
520
550
  activityId: null,
551
+ audioMarker: null,
521
552
  campaign: null,
522
553
  creationAttempted: true,
523
554
  refinementExpected
524
555
  };
525
556
  }
526
- const campaign = reporter.mapResponseToCampaign ? await reporter.mapResponseToCampaign(data, match) : defaultCampaignMapper(data, match);
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 };
565
+ }
527
566
  return {
528
567
  activityId: responseActivityId(data, campaign),
568
+ audioMarker,
529
569
  campaign,
530
570
  creationAttempted: true,
531
- refinementExpected
571
+ refinementExpected,
572
+ ...campaignMappingFailure ? { campaignMappingFailure } : {}
532
573
  };
533
574
  }
534
575
  async function resolveRefinementEndpoint(reporter, activityId) {
@@ -544,6 +585,7 @@ async function createMatchedMaterialActivity(reporter, match, refinementExpected
544
585
  if (!payload) {
545
586
  return {
546
587
  activityId: null,
588
+ audioMarker: null,
547
589
  campaign: null,
548
590
  creationAttempted: false,
549
591
  refinementExpected: false
@@ -576,11 +618,11 @@ async function createMatchedMaterialActivity(reporter, match, refinementExpected
576
618
  async function refineMatchedMaterialActivity(reporter, match, context, isActive = () => true) {
577
619
  const refinement = reporter.refinement;
578
620
  if (!refinement) {
579
- return { activityId: null, campaign: null };
621
+ return { activityId: null, audioMarker: null, campaign: null };
580
622
  }
581
623
  const payload = await resolveRefineRequestPayload(reporter, match, context);
582
624
  if (!payload) {
583
- return { activityId: null, campaign: null };
625
+ return { activityId: null, audioMarker: null, campaign: null };
584
626
  }
585
627
  const endpoint = await resolveRefinementEndpoint(reporter, context.activityId);
586
628
  const headers = await resolveHeaders(reporter, true);
@@ -596,8 +638,16 @@ async function refineMatchedMaterialActivity(reporter, match, context, isActive
596
638
  if (!data) {
597
639
  throw new ActivityReportingError("Activity refine failed: empty response", response.status);
598
640
  }
599
- const mappedCampaign = refinement.mapResponseToCampaign ? await refinement.mapResponseToCampaign(data, match, context) : reporter.mapResponseToCampaign ? await reporter.mapResponseToCampaign(data, match) : defaultCampaignMapper(data, match);
600
- 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);
601
651
  if (activityId !== context.activityId) {
602
652
  throw new ActivityReportingError(
603
653
  "Activity refine failed: response activity_id does not match the requested activity",
@@ -605,7 +655,12 @@ async function refineMatchedMaterialActivity(reporter, match, context, isActive
605
655
  );
606
656
  }
607
657
  const campaign = mappedCampaign ? { ...mappedCampaign, activityId: context.activityId } : null;
608
- return { activityId, campaign };
658
+ return {
659
+ activityId,
660
+ audioMarker,
661
+ campaign,
662
+ ...campaignMappingFailure ? { campaignMappingFailure } : {}
663
+ };
609
664
  }
610
665
 
611
666
  // src/activity-refinement.ts
@@ -634,6 +689,7 @@ var ActivityRefinementCoordinator = class {
634
689
  registeredRequests = /* @__PURE__ */ new Set();
635
690
  settledRequests = /* @__PURE__ */ new Set();
636
691
  activeLifecycle = null;
692
+ activeMarkerSegment = null;
637
693
  generation = 0;
638
694
  constructor(reporter, callbacks) {
639
695
  this.reporter = reporter;
@@ -646,6 +702,7 @@ var ActivityRefinementCoordinator = class {
646
702
  }
647
703
  report(recognitionId, match, captureGeneration, requestId = null) {
648
704
  const coordinatorGeneration = this.generation;
705
+ const markerSegment = this.resolveMarkerSegment(match, captureGeneration);
649
706
  const hasMarker = typeof match.audioMarker?.code === "string" && match.audioMarker.code.length > 0;
650
707
  const refinement = this.reporter.refinement;
651
708
  const requestCanRefine = requestId !== null && !this.settledRequests.has(requestId);
@@ -690,6 +747,7 @@ var ActivityRefinementCoordinator = class {
690
747
  activityId: null,
691
748
  markerMatch: hasMarker ? match : null,
692
749
  markerCode: hasMarker ? match.audioMarker?.code ?? null : null,
750
+ markerSegment,
693
751
  refinementTimer: null,
694
752
  continuityTimer: null,
695
753
  createSettled: false,
@@ -722,9 +780,15 @@ var ActivityRefinementCoordinator = class {
722
780
  recognitionTiming
723
781
  ).then(
724
782
  (result) => {
783
+ if (coordinatorGeneration === this.generation && result.audioMarker && this.callbacks.isCurrentCapture(captureGeneration)) {
784
+ this.emitResolvedAudioMarker(markerSegment, result.audioMarker);
785
+ }
725
786
  if (coordinatorGeneration === this.generation && result.campaign && this.callbacks.isCurrentCapture(captureGeneration)) {
726
787
  this.callbacks.emitCampaign(result.campaign);
727
788
  }
789
+ if (coordinatorGeneration === this.generation && result.campaignMappingFailure && this.callbacks.isCurrentCapture(captureGeneration)) {
790
+ this.callbacks.emitError("activity", result.campaignMappingFailure.error);
791
+ }
728
792
  if (coordinatorGeneration !== this.generation || !lifecycle || this.lifecycles.get(recognitionId) !== lifecycle) {
729
793
  return;
730
794
  }
@@ -803,6 +867,9 @@ var ActivityRefinementCoordinator = class {
803
867
  if (this.activeLifecycle?.captureGeneration === captureGeneration) {
804
868
  this.sealLifecycle(this.activeLifecycle);
805
869
  }
870
+ if (this.activeMarkerSegment?.captureGeneration === captureGeneration) {
871
+ this.activeMarkerSegment = null;
872
+ }
806
873
  }
807
874
  refine(recognitionId, marker, captureGeneration) {
808
875
  if (!this.reporter.refinement) {
@@ -841,6 +908,7 @@ var ActivityRefinementCoordinator = class {
841
908
  this.registeredRequests.clear();
842
909
  this.settledRequests.clear();
843
910
  this.activeLifecycle = null;
911
+ this.activeMarkerSegment = null;
844
912
  }
845
913
  startRefinement(lifecycle) {
846
914
  if (lifecycle.refinementStarted || lifecycle.refinementClosed || !lifecycle.activityId || !lifecycle.markerMatch) {
@@ -901,9 +969,18 @@ var ActivityRefinementCoordinator = class {
901
969
  return;
902
970
  }
903
971
  this.closeRefinement(lifecycle);
972
+ if (result.audioMarker) {
973
+ this.emitResolvedAudioMarker(lifecycle.markerSegment, result.audioMarker);
974
+ }
904
975
  if (result.campaign) {
905
976
  this.callbacks.emitCampaign(result.campaign);
906
977
  }
978
+ if (result.campaignMappingFailure) {
979
+ this.callbacks.emitError(
980
+ "activity-refinement",
981
+ result.campaignMappingFailure.error
982
+ );
983
+ }
907
984
  return;
908
985
  } catch (caught) {
909
986
  if (caught instanceof ActivityReportingCancelledError) {
@@ -979,6 +1056,30 @@ var ActivityRefinementCoordinator = class {
979
1056
  isContinuousMatch(lifecycle, match, captureGeneration, now) {
980
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;
981
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
+ }
982
1083
  renewContinuity(lifecycle, now) {
983
1084
  lifecycle.lastObservedAt = now;
984
1085
  this.scheduleContinuityExpiry(lifecycle);
@@ -1225,7 +1326,6 @@ var MicrophoneMatcher = class {
1225
1326
  markerPcmBuffer = new Float32Array(0);
1226
1327
  resampleBuffer = new Float32Array(0);
1227
1328
  resampleOffset = 0;
1228
- latestAudioMarker = null;
1229
1329
  latestAudioMarkerDetection = null;
1230
1330
  audioMarkerRequestId = 0;
1231
1331
  activityRecognitionId = 0;
@@ -1252,6 +1352,7 @@ var MicrophoneMatcher = class {
1252
1352
  this.packSource = options.packSource;
1253
1353
  this.activityRefinement = options.activityReporter ? new ActivityRefinementCoordinator(options.activityReporter, {
1254
1354
  isCurrentCapture: (captureGeneration) => captureGeneration === this.captureGeneration && this.running,
1355
+ emitAudioMarker: (event) => this.events.emit("audiomarker", event),
1255
1356
  emitCampaign: (campaign) => this.events.emit("campaign", campaign),
1256
1357
  emitError: (phase, error) => emitError(this.events, phase, error)
1257
1358
  }) : null;
@@ -1521,7 +1622,7 @@ var MicrophoneMatcher = class {
1521
1622
  return;
1522
1623
  }
1523
1624
  if (matchedBest) {
1524
- this.events.emit("match", { best: matchedBest });
1625
+ this.events.emit("match", { best: toPublicDetectedMatch(matchedBest) });
1525
1626
  this.activityRecognitionId += 1;
1526
1627
  this.reportCampaign(
1527
1628
  this.activityRecognitionId,
@@ -1553,7 +1654,6 @@ var MicrophoneMatcher = class {
1553
1654
  this.markerPcmBuffer = new Float32Array(0);
1554
1655
  this.resampleBuffer = new Float32Array(0);
1555
1656
  this.resampleOffset = 0;
1556
- this.latestAudioMarker = null;
1557
1657
  this.latestAudioMarkerDetection = null;
1558
1658
  this.audioMarkerRequestId += 1;
1559
1659
  this.activityRefinement?.reset();
@@ -1589,7 +1689,6 @@ var MicrophoneMatcher = class {
1589
1689
  return null;
1590
1690
  }
1591
1691
  this.latestAudioMarkerDetection = { requestId, detection };
1592
- this.emitAudioMarkerIfChanged(detection);
1593
1692
  this.activityRefinement?.refine(requestId, detection, captureGeneration);
1594
1693
  return detection;
1595
1694
  } catch (error) {
@@ -1616,14 +1715,6 @@ var MicrophoneMatcher = class {
1616
1715
  audioMarker: this.latestAudioMarkerDetection.detection
1617
1716
  };
1618
1717
  }
1619
- emitAudioMarkerIfChanged(detection) {
1620
- const marker = detection.code ?? null;
1621
- if (marker === this.latestAudioMarker) {
1622
- return;
1623
- }
1624
- this.latestAudioMarker = marker;
1625
- this.events.emit("audiomarker", { marker, detection });
1626
- }
1627
1718
  normalizeInputSamples(samples) {
1628
1719
  if (this.audioContext.sampleRate === TARGET_SAMPLE_RATE) {
1629
1720
  return samples;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sorisdk/web-audio",
3
- "version": "0.6.7",
3
+ "version": "0.6.8",
4
4
  "description": "Web SDK for browser-based audio recognition with SORI API",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
@@ -34,8 +34,8 @@
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.7",
38
- "@sorisdk/afpgen": "0.6.7"
37
+ "@sorisdk/afpgen": "0.6.8",
38
+ "@sorisdk/matcher": "0.6.8"
39
39
  },
40
40
  "devDependencies": {
41
41
  "tsup": "^8.5.1",