@sorisdk/web-audio 0.6.8 → 0.6.10

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (3) hide show
  1. package/README.md +41 -5
  2. package/dist/index.js +58 -38
  3. package/package.json +3 -3
package/README.md CHANGED
@@ -13,6 +13,26 @@ 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
+ ## Level-robust marker policy (0.6.10)
17
+
18
+ Marker recognition remains opt-in with `audiomarker: true`; fingerprint-only
19
+ applications keep their existing capture and matching behavior. When enabled,
20
+ `MicrophoneMatcher` uses the level-robust v1 default through its real
21
+ `MatcherSession` at the actual `AudioContext.sampleRate`. The PCM window,
22
+ material cadence, asynchronous refinement, and continuous-run lifecycle are
23
+ unchanged. Only accepted marker codes enter private activity requests; public
24
+ marker identity still comes from Console's resolved id/name pair.
25
+
26
+ This release follows **user-reported improvement** in Android human testing.
27
+ No quantitative physical-test measurements were attached, and it does not
28
+ complete the original hardware-validation scope of NADA #154.
29
+
30
+ Consume all three 0.6.10 packages together (web-audio publishes exact 0.6.10
31
+ matcher/afpgen dependencies). Flutter's web bridge can keep its Dart API: resolve
32
+ the updated npm dependency/lockfile, regenerate the JS/WASM/model assets, and
33
+ redeploy the consuming application. Publishing npm does not update existing
34
+ bundles. The immutable standalone module URL is shown below.
35
+
16
36
  ## Unreleased: Console-resolved audio marker events
17
37
 
18
38
  The public `audiomarker` event now contains only the marker identity managed in
@@ -52,7 +72,7 @@ configuration, or Node-based asset server is required:
52
72
  ```html
53
73
  <script type="module">
54
74
  import { AudioRecognizer } from
55
- "https://cdn.iplateia.com/web/sorisdk/v0.6.8/sori-web-audio.mjs";
75
+ "https://cdn.iplateia.com/web/sorisdk/v0.6.10/sori-web-audio.mjs";
56
76
 
57
77
  const recognizer = new AudioRecognizer({
58
78
  appId: "YOUR_APP_ID",
@@ -79,7 +99,7 @@ configuration, or Node-based asset server is required:
79
99
  ```
80
100
 
81
101
  Static URL imports are valid inside `<script type="module">`. Dynamic
82
- `await import("https://cdn.iplateia.com/web/sorisdk/v0.6.8/sori-web-audio.mjs")`
102
+ `await import("https://cdn.iplateia.com/web/sorisdk/v0.6.10/sori-web-audio.mjs")`
83
103
  is an optional alternative when the SDK should be loaded conditionally.
84
104
 
85
105
  Pin an exact version in production. Do not construct a mutable `latest` URL.
@@ -149,7 +169,7 @@ An import map can give the standalone URL the npm package name:
149
169
  <script type="importmap">
150
170
  {
151
171
  "imports": {
152
- "@sorisdk/web-audio": "https://cdn.iplateia.com/web/sorisdk/v0.6.8/sori-web-audio.mjs"
172
+ "@sorisdk/web-audio": "https://cdn.iplateia.com/web/sorisdk/v0.6.10/sori-web-audio.mjs"
153
173
  }
154
174
  }
155
175
  </script>
@@ -219,6 +239,22 @@ await recognizer.stop();
219
239
  await recognizer.destroy();
220
240
  ```
221
241
 
242
+ ## Continuous recognition
243
+
244
+ Continuous observations of one material emit one public `match`. An intervening
245
+ material (A → B → A), two consecutive fingerprint no-match results, or a gap
246
+ greater than 30 seconds since the last successful observation starts a new run.
247
+ The next successful A is delivered immediately. A single fingerprint miss or
248
+ marker miss does not split a run. Successful observations renew continuity even
249
+ when no public `match` event is emitted, in both marker modes.
250
+
251
+ The 30-second boundary also covers capture interruptions with no queries; it is
252
+ checked when querying resumes. Two consecutive misses can confirm a boundary
253
+ sooner. `nomatch` is emitted on an observed boundary, rather than by an idle
254
+ timer. Sequential stop/start, failed-start cleanup, and successful database
255
+ replacement or mutation clear recognition and capture state. Pending queries
256
+ from an older capture cannot restore suppression in the new capture.
257
+
222
258
  ## Activity refinement
223
259
 
224
260
  The default `AudioRecognizer` activity reporter represents one ordered,
@@ -242,7 +278,7 @@ server activity:
242
278
  7. The refined `campaign` event keeps the same non-empty `activityId`, so an
243
279
  application can replace the earlier campaign row deterministically.
244
280
 
245
- A consecutive match for the same material and fingerprint type renews a local
281
+ A successful observation for the same material and fingerprint type renews a local
246
282
  30-second continuity lease without another POST or campaign event. That local
247
283
  lease can keep one segment alive beyond the initial server refinement window,
248
284
  but it never extends the original PUT deadline. A marker confirmed anywhere
@@ -264,7 +300,7 @@ refine the earlier A. Marker evidence arriving after a segment is sealed is
264
300
  ignored for that segment; create or PUT transport that already started may
265
301
  finish under the existing capture, deadline, and cancellation guards.
266
302
 
267
- Ordinary fingerprint `match` events remain independently observable and never
303
+ Fingerprint `match` events follow the continuous-run rules above and never
268
304
  contain the private detector result. Public `audiomarker` delivery is sourced
269
305
  only from a completed activity response. It is independent from campaign
270
306
  availability but does prove that the server resolved the marker id and name.
package/dist/index.js CHANGED
@@ -354,6 +354,9 @@ var FingerprintMatchWindow = class {
354
354
  }
355
355
  };
356
356
 
357
+ // src/activity-refinement.ts
358
+ import { MATCH_CONTINUITY_GAP_MS } from "@sorisdk/matcher";
359
+
357
360
  // src/detected-match.ts
358
361
  function toPublicDetectedMatch(match) {
359
362
  const { audioMarker: _internalAudioMarker, ...publicMatch } = match;
@@ -665,7 +668,6 @@ async function refineMatchedMaterialActivity(reporter, match, context, isActive
665
668
 
666
669
  // src/activity-refinement.ts
667
670
  var ACTIVITY_REFINEMENT_WINDOW_MS = 3e4;
668
- var ACTIVITY_CONTINUITY_GAP_MS = 3e4;
669
671
  var DEFAULT_MAX_REFINEMENT_ATTEMPTS = 2;
670
672
  var MAX_REFINEMENT_ATTEMPTS = 10;
671
673
  function normalizeError(error) {
@@ -700,11 +702,14 @@ var ActivityRefinementCoordinator = class {
700
702
  this.registeredRequests.add(requestId);
701
703
  }
702
704
  }
703
- report(recognitionId, match, captureGeneration, requestId = null) {
705
+ report(recognitionId, match, captureGeneration, requestId = null, distinct = true) {
704
706
  const coordinatorGeneration = this.generation;
705
707
  const markerSegment = this.resolveMarkerSegment(match, captureGeneration);
706
708
  const hasMarker = typeof match.audioMarker?.code === "string" && match.audioMarker.code.length > 0;
707
709
  const refinement = this.reporter.refinement;
710
+ if (!refinement && !distinct) {
711
+ return;
712
+ }
708
713
  const requestCanRefine = requestId !== null && !this.settledRequests.has(requestId);
709
714
  const pendingMarker = requestId === null ? void 0 : this.pendingMarkers.get(requestId);
710
715
  if (requestId !== null) {
@@ -1054,12 +1059,12 @@ var ActivityRefinementCoordinator = class {
1054
1059
  }
1055
1060
  }
1056
1061
  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;
1062
+ 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 <= MATCH_CONTINUITY_GAP_MS;
1058
1063
  }
1059
1064
  resolveMarkerSegment(match, captureGeneration) {
1060
1065
  const now = this.now();
1061
1066
  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) {
1067
+ if (active && active.captureGeneration === captureGeneration && active.materialName === match.name && active.afpType === match.afpType && now - active.lastObservedAt <= MATCH_CONTINUITY_GAP_MS) {
1063
1068
  active.lastObservedAt = now;
1064
1069
  return active;
1065
1070
  }
@@ -1088,14 +1093,14 @@ var ActivityRefinementCoordinator = class {
1088
1093
  if (lifecycle.continuityTimer) {
1089
1094
  clearTimeout(lifecycle.continuityTimer);
1090
1095
  }
1091
- const remainingMs = lifecycle.lastObservedAt + ACTIVITY_CONTINUITY_GAP_MS - this.now();
1096
+ const remainingMs = lifecycle.lastObservedAt + MATCH_CONTINUITY_GAP_MS - this.now();
1092
1097
  lifecycle.continuityTimer = setTimeout(() => {
1093
1098
  lifecycle.continuityTimer = null;
1094
1099
  if (this.activeLifecycle !== lifecycle || lifecycle.sealed) {
1095
1100
  return;
1096
1101
  }
1097
1102
  const now = this.now();
1098
- if (now - lifecycle.lastObservedAt <= ACTIVITY_CONTINUITY_GAP_MS) {
1103
+ if (now - lifecycle.lastObservedAt <= MATCH_CONTINUITY_GAP_MS) {
1099
1104
  this.scheduleContinuityExpiry(lifecycle);
1100
1105
  return;
1101
1106
  }
@@ -1381,6 +1386,7 @@ var MicrophoneMatcher = class {
1381
1386
  }
1382
1387
  await this.matcherSession.loadPack(target);
1383
1388
  this.loadedPack = true;
1389
+ await this.resetCaptureState();
1384
1390
  } catch (error) {
1385
1391
  emitAndThrow(this.events, "loadPack", error);
1386
1392
  }
@@ -1422,6 +1428,7 @@ var MicrophoneMatcher = class {
1422
1428
  this.ensureNotDestroyed();
1423
1429
  await this.prepare();
1424
1430
  await this.matcherSession.addEntry(name, afpType, fingerprint);
1431
+ await this.resetCaptureState();
1425
1432
  } catch (error) {
1426
1433
  emitAndThrow(this.events, "addEntry", error);
1427
1434
  }
@@ -1431,6 +1438,7 @@ var MicrophoneMatcher = class {
1431
1438
  this.ensureNotDestroyed();
1432
1439
  await this.prepare();
1433
1440
  await this.matcherSession.addOrReplace(name, afpType, fingerprint);
1441
+ await this.resetCaptureState();
1434
1442
  } catch (error) {
1435
1443
  emitAndThrow(this.events, "addOrReplace", error);
1436
1444
  }
@@ -1439,9 +1447,8 @@ var MicrophoneMatcher = class {
1439
1447
  try {
1440
1448
  this.ensureNotDestroyed();
1441
1449
  await this.prepare();
1442
- this.activityRefinement?.reset();
1443
- this.matchWindowOrThrow().reset();
1444
1450
  await this.matcherSession.clear();
1451
+ await this.resetCaptureState();
1445
1452
  } catch (error) {
1446
1453
  emitAndThrow(this.events, "clear", error);
1447
1454
  }
@@ -1486,9 +1493,9 @@ var MicrophoneMatcher = class {
1486
1493
  return;
1487
1494
  }
1488
1495
  this.running = false;
1496
+ await this.resetCaptureState();
1489
1497
  await this.graphController?.stop();
1490
1498
  this.graphController = null;
1491
- await this.resetCaptureState();
1492
1499
  if (this.stopTracksOnStop) {
1493
1500
  this.stream?.getTracks().forEach((track) => track.stop());
1494
1501
  }
@@ -1578,15 +1585,18 @@ var MicrophoneMatcher = class {
1578
1585
  let matchedBest = null;
1579
1586
  let shouldEmitNoMatch = false;
1580
1587
  let audioMarkerRequestId = null;
1581
- let associatedActivity = false;
1588
+ let observedBest = null;
1582
1589
  const handleMatch = (event) => {
1583
- if (!event.best) {
1590
+ if (!event.best || captureGeneration !== this.captureGeneration || !this.running) {
1584
1591
  return;
1585
1592
  }
1586
1593
  matchedBest = event.best;
1587
1594
  shouldEmitNoMatch = false;
1588
1595
  };
1589
1596
  const handleNoMatch = () => {
1597
+ if (captureGeneration !== this.captureGeneration || !this.running) {
1598
+ return;
1599
+ }
1590
1600
  matchedBest = null;
1591
1601
  shouldEmitNoMatch = true;
1592
1602
  };
@@ -1596,7 +1606,6 @@ var MicrophoneMatcher = class {
1596
1606
  this.diagnostics.matchRequests += 1;
1597
1607
  const bestMatchPromise = this.matcherSession.bestMatch(query, this.matchConfig());
1598
1608
  audioMarkerRequestId = this.beginAudioMarkerDetection(captureGeneration);
1599
- let observedBest;
1600
1609
  try {
1601
1610
  observedBest = await bestMatchPromise;
1602
1611
  } catch (error) {
@@ -1605,15 +1614,10 @@ var MicrophoneMatcher = class {
1605
1614
  }
1606
1615
  throw error;
1607
1616
  }
1608
- if (audioMarkerRequestId !== null) {
1609
- associatedActivity = this.activityRefinement?.associate(
1610
- audioMarkerRequestId,
1611
- observedBest,
1612
- captureGeneration
1613
- ) ?? false;
1617
+ if (captureGeneration !== this.captureGeneration || !this.running) {
1618
+ return;
1614
1619
  }
1615
- const distinctBest = this.withAudioMarkerForRequest(matchedBest, audioMarkerRequestId);
1616
- matchedBest = distinctBest;
1620
+ observedBest = this.withAudioMarkerForRequest(observedBest, audioMarkerRequestId);
1617
1621
  } finally {
1618
1622
  this.matcherSession.off("match", handleMatch);
1619
1623
  this.matcherSession.off("nomatch", handleNoMatch);
@@ -1621,33 +1625,46 @@ var MicrophoneMatcher = class {
1621
1625
  if (captureGeneration !== this.captureGeneration || !this.running) {
1622
1626
  return;
1623
1627
  }
1624
- if (matchedBest) {
1625
- this.events.emit("match", { best: toPublicDetectedMatch(matchedBest) });
1628
+ if (observedBest) {
1629
+ const distinct = matchedBest !== null;
1630
+ if (distinct) {
1631
+ this.activityRefinement?.clearActive(captureGeneration);
1632
+ this.events.emit("match", { best: toPublicDetectedMatch(observedBest) });
1633
+ }
1634
+ if (captureGeneration !== this.captureGeneration || !this.running) {
1635
+ return;
1636
+ }
1626
1637
  this.activityRecognitionId += 1;
1627
1638
  this.reportCampaign(
1628
1639
  this.activityRecognitionId,
1629
- matchedBest,
1640
+ observedBest,
1630
1641
  captureGeneration,
1631
- audioMarkerRequestId
1642
+ audioMarkerRequestId,
1643
+ distinct
1632
1644
  );
1633
- } else if (shouldEmitNoMatch) {
1634
- if (audioMarkerRequestId !== null && !associatedActivity) {
1645
+ } else {
1646
+ if (audioMarkerRequestId !== null) {
1635
1647
  this.activityRefinement?.discardRequest(audioMarkerRequestId);
1636
1648
  }
1637
- this.activityRefinement?.clearActive(captureGeneration);
1638
- this.events.emit("nomatch", {});
1639
- } else if (audioMarkerRequestId !== null && !associatedActivity) {
1640
- this.activityRefinement?.discardRequest(audioMarkerRequestId);
1649
+ if (shouldEmitNoMatch) {
1650
+ this.activityRefinement?.clearActive(captureGeneration);
1651
+ this.events.emit("nomatch", {});
1652
+ }
1641
1653
  }
1642
1654
  } while (this.pendingMatch || this.matchWindowOrThrow().hasReadyQuery());
1643
1655
  } catch (error) {
1644
- emitError(this.events, "match", error);
1656
+ if (captureGeneration === this.captureGeneration && this.running) {
1657
+ emitError(this.events, "match", error);
1658
+ }
1645
1659
  } finally {
1646
- this.matchInFlight = false;
1660
+ if (captureGeneration === this.captureGeneration) {
1661
+ this.matchInFlight = false;
1662
+ }
1647
1663
  }
1648
1664
  }
1649
1665
  async resetCaptureState() {
1650
1666
  this.captureGeneration += 1;
1667
+ this.matcherSession.resetRecognitionState();
1651
1668
  this.pendingMatch = false;
1652
1669
  this.matchInFlight = false;
1653
1670
  this.pcmBuffer = new Float32Array(0);
@@ -1657,7 +1674,7 @@ var MicrophoneMatcher = class {
1657
1674
  this.latestAudioMarkerDetection = null;
1658
1675
  this.audioMarkerRequestId += 1;
1659
1676
  this.activityRefinement?.reset();
1660
- this.matchWindowOrThrow().reset();
1677
+ this.matchWindow?.reset();
1661
1678
  await this.extractor?.reset();
1662
1679
  }
1663
1680
  appendAudioMarkerSamples(samples) {
@@ -1692,8 +1709,10 @@ var MicrophoneMatcher = class {
1692
1709
  this.activityRefinement?.refine(requestId, detection, captureGeneration);
1693
1710
  return detection;
1694
1711
  } catch (error) {
1695
- this.activityRefinement?.settleRequest(requestId);
1696
- emitError(this.events, "audiomarker", error);
1712
+ if (captureGeneration === this.captureGeneration && this.running) {
1713
+ this.activityRefinement?.settleRequest(requestId);
1714
+ emitError(this.events, "audiomarker", error);
1715
+ }
1697
1716
  return null;
1698
1717
  }
1699
1718
  }
@@ -1744,9 +1763,9 @@ var MicrophoneMatcher = class {
1744
1763
  }
1745
1764
  async cleanupAfterStartFailure() {
1746
1765
  this.running = false;
1766
+ await this.resetCaptureState();
1747
1767
  await this.graphController?.stop();
1748
1768
  this.graphController = null;
1749
- await this.resetCaptureState();
1750
1769
  if (this.stopTracksOnStop) {
1751
1770
  this.stream?.getTracks().forEach((track) => track.stop());
1752
1771
  }
@@ -1777,7 +1796,7 @@ var MicrophoneMatcher = class {
1777
1796
  }
1778
1797
  return this.matchWindow;
1779
1798
  }
1780
- reportCampaign(recognitionId, match, captureGeneration, audioMarkerRequestId) {
1799
+ reportCampaign(recognitionId, match, captureGeneration, audioMarkerRequestId, distinct) {
1781
1800
  const reporter = this.options.activityReporter;
1782
1801
  if (!reporter) {
1783
1802
  return;
@@ -1786,7 +1805,8 @@ var MicrophoneMatcher = class {
1786
1805
  recognitionId,
1787
1806
  match,
1788
1807
  captureGeneration,
1789
- audioMarkerRequestId
1808
+ audioMarkerRequestId,
1809
+ distinct
1790
1810
  );
1791
1811
  }
1792
1812
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sorisdk/web-audio",
3
- "version": "0.6.8",
3
+ "version": "0.6.10",
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/afpgen": "0.6.8",
38
- "@sorisdk/matcher": "0.6.8"
37
+ "@sorisdk/afpgen": "0.6.10",
38
+ "@sorisdk/matcher": "0.6.10"
39
39
  },
40
40
  "devDependencies": {
41
41
  "tsup": "^8.5.1",