@sorisdk/web-audio 0.6.8 → 0.6.9
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 +21 -5
- package/dist/index.js +58 -38
- package/package.json +3 -3
package/README.md
CHANGED
|
@@ -52,7 +52,7 @@ configuration, or Node-based asset server is required:
|
|
|
52
52
|
```html
|
|
53
53
|
<script type="module">
|
|
54
54
|
import { AudioRecognizer } from
|
|
55
|
-
"https://cdn.iplateia.com/web/sorisdk/v0.6.
|
|
55
|
+
"https://cdn.iplateia.com/web/sorisdk/v0.6.9/sori-web-audio.mjs";
|
|
56
56
|
|
|
57
57
|
const recognizer = new AudioRecognizer({
|
|
58
58
|
appId: "YOUR_APP_ID",
|
|
@@ -79,7 +79,7 @@ configuration, or Node-based asset server is required:
|
|
|
79
79
|
```
|
|
80
80
|
|
|
81
81
|
Static URL imports are valid inside `<script type="module">`. Dynamic
|
|
82
|
-
`await import("https://cdn.iplateia.com/web/sorisdk/v0.6.
|
|
82
|
+
`await import("https://cdn.iplateia.com/web/sorisdk/v0.6.9/sori-web-audio.mjs")`
|
|
83
83
|
is an optional alternative when the SDK should be loaded conditionally.
|
|
84
84
|
|
|
85
85
|
Pin an exact version in production. Do not construct a mutable `latest` URL.
|
|
@@ -149,7 +149,7 @@ An import map can give the standalone URL the npm package name:
|
|
|
149
149
|
<script type="importmap">
|
|
150
150
|
{
|
|
151
151
|
"imports": {
|
|
152
|
-
"@sorisdk/web-audio": "https://cdn.iplateia.com/web/sorisdk/v0.6.
|
|
152
|
+
"@sorisdk/web-audio": "https://cdn.iplateia.com/web/sorisdk/v0.6.9/sori-web-audio.mjs"
|
|
153
153
|
}
|
|
154
154
|
}
|
|
155
155
|
</script>
|
|
@@ -219,6 +219,22 @@ await recognizer.stop();
|
|
|
219
219
|
await recognizer.destroy();
|
|
220
220
|
```
|
|
221
221
|
|
|
222
|
+
## Continuous recognition
|
|
223
|
+
|
|
224
|
+
Continuous observations of one material emit one public `match`. An intervening
|
|
225
|
+
material (A → B → A), two consecutive fingerprint no-match results, or a gap
|
|
226
|
+
greater than 30 seconds since the last successful observation starts a new run.
|
|
227
|
+
The next successful A is delivered immediately. A single fingerprint miss or
|
|
228
|
+
marker miss does not split a run. Successful observations renew continuity even
|
|
229
|
+
when no public `match` event is emitted, in both marker modes.
|
|
230
|
+
|
|
231
|
+
The 30-second boundary also covers capture interruptions with no queries; it is
|
|
232
|
+
checked when querying resumes. Two consecutive misses can confirm a boundary
|
|
233
|
+
sooner. `nomatch` is emitted on an observed boundary, rather than by an idle
|
|
234
|
+
timer. Sequential stop/start, failed-start cleanup, and successful database
|
|
235
|
+
replacement or mutation clear recognition and capture state. Pending queries
|
|
236
|
+
from an older capture cannot restore suppression in the new capture.
|
|
237
|
+
|
|
222
238
|
## Activity refinement
|
|
223
239
|
|
|
224
240
|
The default `AudioRecognizer` activity reporter represents one ordered,
|
|
@@ -242,7 +258,7 @@ server activity:
|
|
|
242
258
|
7. The refined `campaign` event keeps the same non-empty `activityId`, so an
|
|
243
259
|
application can replace the earlier campaign row deterministically.
|
|
244
260
|
|
|
245
|
-
A
|
|
261
|
+
A successful observation for the same material and fingerprint type renews a local
|
|
246
262
|
30-second continuity lease without another POST or campaign event. That local
|
|
247
263
|
lease can keep one segment alive beyond the initial server refinement window,
|
|
248
264
|
but it never extends the original PUT deadline. A marker confirmed anywhere
|
|
@@ -264,7 +280,7 @@ refine the earlier A. Marker evidence arriving after a segment is sealed is
|
|
|
264
280
|
ignored for that segment; create or PUT transport that already started may
|
|
265
281
|
finish under the existing capture, deadline, and cancellation guards.
|
|
266
282
|
|
|
267
|
-
|
|
283
|
+
Fingerprint `match` events follow the continuous-run rules above and never
|
|
268
284
|
contain the private detector result. Public `audiomarker` delivery is sourced
|
|
269
285
|
only from a completed activity response. It is independent from campaign
|
|
270
286
|
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 <=
|
|
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 <=
|
|
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 +
|
|
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 <=
|
|
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
|
|
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 (
|
|
1609
|
-
|
|
1610
|
-
audioMarkerRequestId,
|
|
1611
|
-
observedBest,
|
|
1612
|
-
captureGeneration
|
|
1613
|
-
) ?? false;
|
|
1617
|
+
if (captureGeneration !== this.captureGeneration || !this.running) {
|
|
1618
|
+
return;
|
|
1614
1619
|
}
|
|
1615
|
-
|
|
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 (
|
|
1625
|
-
|
|
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
|
-
|
|
1640
|
+
observedBest,
|
|
1630
1641
|
captureGeneration,
|
|
1631
|
-
audioMarkerRequestId
|
|
1642
|
+
audioMarkerRequestId,
|
|
1643
|
+
distinct
|
|
1632
1644
|
);
|
|
1633
|
-
} else
|
|
1634
|
-
if (audioMarkerRequestId !== null
|
|
1645
|
+
} else {
|
|
1646
|
+
if (audioMarkerRequestId !== null) {
|
|
1635
1647
|
this.activityRefinement?.discardRequest(audioMarkerRequestId);
|
|
1636
1648
|
}
|
|
1637
|
-
|
|
1638
|
-
|
|
1639
|
-
|
|
1640
|
-
|
|
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
|
-
|
|
1656
|
+
if (captureGeneration === this.captureGeneration && this.running) {
|
|
1657
|
+
emitError(this.events, "match", error);
|
|
1658
|
+
}
|
|
1645
1659
|
} finally {
|
|
1646
|
-
this.
|
|
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.
|
|
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.
|
|
1696
|
-
|
|
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.
|
|
3
|
+
"version": "0.6.9",
|
|
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.
|
|
38
|
-
"@sorisdk/matcher": "0.6.
|
|
37
|
+
"@sorisdk/afpgen": "0.6.9",
|
|
38
|
+
"@sorisdk/matcher": "0.6.9"
|
|
39
39
|
},
|
|
40
40
|
"devDependencies": {
|
|
41
41
|
"tsup": "^8.5.1",
|