@sorisdk/matcher 0.6.9 → 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.
package/README.md CHANGED
@@ -121,6 +121,44 @@ per-entry limits remain independently enforced.
121
121
 
122
122
  If you want campaign events and browser recognition flow on top of matching, use `@sorisdk/web-audio`.
123
123
 
124
+ ## Audio-marker default policy (0.6.10)
125
+
126
+ `session.detectAudioMarker(pcm16Le, actualSampleRate)` and
127
+ `detectAudioMarkerWithLimits` now select the shared level-robust v1 policy.
128
+ An empty config or waveform/search-only config also uses this default. No PCM
129
+ amplification is applied: `score`, `syncScore`, `margin`, and runner-up scores
130
+ remain in raw input-amplitude units. `confidence` uses the v1 calibration.
131
+
132
+ Results keep the existing flat detection shape and add `levelPolicy`,
133
+ `diagnostics`, `candidateEligible`, and `materialFallbackGuardEligible`.
134
+ Eligibility flags come directly from the shared DSP. They do not authorize a
135
+ marker hit: only a non-null accepted `code` is eligible for activity enrichment.
136
+ The Web path does not apply extra legacy raw-score gates or implement native
137
+ SDK temporal voting/material-fallback heuristics. Early JS input rejections
138
+ include false eligibility flags and may omit DSP diagnostics.
139
+
140
+ For compatibility, explicitly setting any of `minSyncScore`, `minScore`,
141
+ `minMargin`, `minRatio`, `minConfidence`, or `minVotes` retains legacy behavior.
142
+ Choose `levelPolicy` explicitly to override this compatibility rule:
143
+
144
+ ```ts
145
+ await session.detectAudioMarker(pcm, rate, { levelPolicy: "legacy" });
146
+ await session.detectAudioMarker(pcm, rate, {
147
+ levelPolicy: "levelRobust", minVotes: 2
148
+ });
149
+ ```
150
+
151
+ With explicit robust policy, shared v1 amplitude gates replace `minSyncScore`,
152
+ `minScore`, and `minMargin`; waveform/search/codebook/vote parameters and stricter
153
+ ratio/confidence requirements retain the shared Rust semantics. The generated
154
+ low-level `detectAudioMarker` / `detectPatternMark` exports and their bounded
155
+ variants remain legacy APIs; `detectAudioMarkerLevelRobust` returns the shared
156
+ nested envelope. Rust, C, Python, and native Node legacy APIs are unchanged.
157
+
158
+ Upgrade the JS package and its generated WASM together. A custom loader lacking
159
+ the robust export fails with an upgrade error on the default path; it never
160
+ silently downgrades. An explicit legacy config remains supported with old WASM.
161
+
124
162
  ## Audio-marker detection limits
125
163
 
126
164
  `MatcherSession.detectAudioMarker(...)` preserves its existing signature and
@@ -149,7 +187,9 @@ const detection = await session.detectAudioMarkerWithLimits(
149
187
  ```
150
188
 
151
189
  The generated WASM module exposes the corresponding
152
- `detectAudioMarkerWithLimits` function and method. It inspects the JavaScript
190
+ `detectAudioMarkerWithLimits` legacy function and method, and the robust
191
+ `detectAudioMarkerLevelRobust(pcm, rate, config, limits)` function. The session
192
+ selects the correct export from the policy above. Each inspects the JavaScript
153
193
  `Uint8Array` length before allocating a Rust-owned copy. Raising these budgets
154
194
  permits greater memory and CPU use; it does not protect an untrusted host after
155
195
  the caller opts into the larger envelope.
package/dist/index.d.ts CHANGED
@@ -20,7 +20,10 @@ declare const DEFAULT_MAX_AUDIO_MARKER_WORK_UNITS = 1000000000;
20
20
  declare const AUDIO_MARKER_INPUT_BUDGET_EXCEEDED_REASON = "pcm_input_budget_exceeded";
21
21
  declare const AUDIO_MARKER_WORK_BUDGET_EXCEEDED_REASON = "detector_work_budget_exceeded";
22
22
  declare const AUDIO_MARKER_WORK_OVERFLOW_REASON = "detector_work_overflow";
23
+ type AudioMarkerLevelPolicy = "levelRobust" | "legacy";
23
24
  interface AudioMarkerConfig {
25
+ /** Defaults to levelRobust; explicit thresholds retain legacy semantics unless overridden. */
26
+ levelPolicy?: AudioMarkerLevelPolicy;
24
27
  bandLowHz?: number;
25
28
  bandHighHz?: number;
26
29
  syncDurationMillis?: number;
@@ -43,7 +46,31 @@ interface AudioMarkerLimits {
43
46
  maxDurationMillis?: number;
44
47
  maxWorkUnits?: number;
45
48
  }
49
+ /** Shared v1 diagnostics; levels are band estimates, not calibrated SPL or SNR. */
50
+ interface AudioMarkerLevelDiagnostics {
51
+ policyVersion: number;
52
+ markerBandDbfs: number;
53
+ lowerNeighborDbfs: number;
54
+ upperNeighborDbfs: number;
55
+ normalizationRms: number;
56
+ effectiveGain: number;
57
+ normalizedSyncScore: number;
58
+ normalizedScore: number;
59
+ normalizedMargin: number;
60
+ policyConfidence: number;
61
+ codeHalfBalance: number | null;
62
+ effectiveMinSyncScore: number;
63
+ effectiveMinScore: number;
64
+ effectiveMinMargin: number;
65
+ qualityReason: string | null;
66
+ }
46
67
  interface AudioMarkerDetection {
68
+ /** Present on level-robust session results; raw score units below are unchanged. */
69
+ levelPolicy?: AudioMarkerLevelPolicy;
70
+ diagnostics?: AudioMarkerLevelDiagnostics;
71
+ /** Shared eligibility only; a candidate is not an accepted marker identity. */
72
+ candidateEligible?: boolean;
73
+ materialFallbackGuardEligible?: boolean;
47
74
  pattern?: string | null;
48
75
  code?: string | null;
49
76
  confidence?: number | null;
@@ -251,6 +278,7 @@ declare class MatcherSession {
251
278
  extractPitchReference(pcm16Le: Uint8Array, sampleRate: number): Promise<PitchReferenceEstimate>;
252
279
  estimatePitchRatioAgainstReference(pcm16Le: Uint8Array, sampleRate: number, referencePitchHz: number, referenceConfidence?: number | null): Promise<PitchRatioEstimate>;
253
280
  estimatePitchRatioAgainstReferenceFeatures(pcm16Le: Uint8Array, sampleRate: number, referenceFeatures: string, referencePositionMillis: number, referenceConfidence?: number | null): Promise<PitchRatioEstimate>;
281
+ /** Uses level-robust detection by default, preserving explicit legacy thresholds. */
254
282
  detectAudioMarker(pcm16Le: Uint8Array, sampleRate: number, config?: AudioMarkerConfig): Promise<AudioMarkerDetection>;
255
283
  /** Detect an audio marker with explicit limits for trusted offline input. */
256
284
  detectAudioMarkerWithLimits(pcm16Le: Uint8Array, sampleRate: number, config: AudioMarkerConfig | undefined, limits: AudioMarkerLimits): Promise<AudioMarkerDetection>;
@@ -276,4 +304,4 @@ declare function __resetMatcherWasmForTests(): void;
276
304
  /** Maximum gap between successful observations in one continuous material run. */
277
305
  declare const MATCH_CONTINUITY_GAP_MS = 30000;
278
306
 
279
- export { AUDIO_MARKER_INPUT_BUDGET_EXCEEDED_REASON, AUDIO_MARKER_WORK_BUDGET_EXCEEDED_REASON, AUDIO_MARKER_WORK_OVERFLOW_REASON, AudioFingerprintType, type AudioMarkerCode, type AudioMarkerConfig, type AudioMarkerDetection, type AudioMarkerLimits, DEFAULT_MAX_AUDIO_MARKER_DURATION_MILLIS, DEFAULT_MAX_AUDIO_MARKER_PCM_BYTES, DEFAULT_MAX_AUDIO_MARKER_SAMPLES, DEFAULT_MAX_AUDIO_MARKER_WORK_UNITS, DEFAULT_MAX_PACK_BYTES, type EntryMetadata, type InitMatcherOptions, type InitMatcherResult, MATCH_CONTINUITY_GAP_MS, type MatchConfig, type MatchResult, type MatcherEphemeralAuthOptions, type MatcherEphemeralAuthResult, type MatcherEphemeralKeyExchangeMetadata, type MatcherEphemeralKeyExchangeOptions, MatcherSession, type MatcherSessionEventMap, type MatcherSessionEventName, type MatcherSessionListener, type MatcherSessionOptions, type PackSource, type PitchRatioEstimate, type PitchReferenceEstimate, type VariantDecision, type VariantDecisionStatus, type VariantMatch, type VariantMatchConfig, type VariantMatchQuery, __resetMatcherWasmForTests, __setMatcherEmbeddedPackForTests, __setMatcherWasmModuleForTests, initMatcher };
307
+ export { AUDIO_MARKER_INPUT_BUDGET_EXCEEDED_REASON, AUDIO_MARKER_WORK_BUDGET_EXCEEDED_REASON, AUDIO_MARKER_WORK_OVERFLOW_REASON, AudioFingerprintType, type AudioMarkerCode, type AudioMarkerConfig, type AudioMarkerDetection, type AudioMarkerLevelDiagnostics, type AudioMarkerLevelPolicy, type AudioMarkerLimits, DEFAULT_MAX_AUDIO_MARKER_DURATION_MILLIS, DEFAULT_MAX_AUDIO_MARKER_PCM_BYTES, DEFAULT_MAX_AUDIO_MARKER_SAMPLES, DEFAULT_MAX_AUDIO_MARKER_WORK_UNITS, DEFAULT_MAX_PACK_BYTES, type EntryMetadata, type InitMatcherOptions, type InitMatcherResult, MATCH_CONTINUITY_GAP_MS, type MatchConfig, type MatchResult, type MatcherEphemeralAuthOptions, type MatcherEphemeralAuthResult, type MatcherEphemeralKeyExchangeMetadata, type MatcherEphemeralKeyExchangeOptions, MatcherSession, type MatcherSessionEventMap, type MatcherSessionEventName, type MatcherSessionListener, type MatcherSessionOptions, type PackSource, type PitchRatioEstimate, type PitchReferenceEstimate, type VariantDecision, type VariantDecisionStatus, type VariantMatch, type VariantMatchConfig, type VariantMatchQuery, __resetMatcherWasmForTests, __setMatcherEmbeddedPackForTests, __setMatcherWasmModuleForTests, initMatcher };
package/dist/index.js CHANGED
@@ -715,6 +715,19 @@ function normalizeAudioMarkerDetection(raw) {
715
715
  runnerUpAccuracy: normalizeOptionalNumber(record.runnerUpAccuracy, record.runner_up_accuracy)
716
716
  };
717
717
  }
718
+ function normalizeLevelRobustDetection(raw) {
719
+ const envelope = raw;
720
+ if (!envelope || typeof envelope.detection !== "object" || !envelope.detection || typeof envelope.diagnostics !== "object" || !envelope.diagnostics || typeof envelope.candidateEligible !== "boolean" || typeof envelope.materialFallbackGuardEligible !== "boolean") {
721
+ throw new Error("Invalid level-robust marker result envelope");
722
+ }
723
+ return {
724
+ ...normalizeAudioMarkerDetection(envelope.detection),
725
+ levelPolicy: "levelRobust",
726
+ diagnostics: { ...envelope.diagnostics },
727
+ candidateEligible: envelope.candidateEligible,
728
+ materialFallbackGuardEligible: envelope.materialFallbackGuardEligible
729
+ };
730
+ }
718
731
  function isMapLike(raw) {
719
732
  return raw instanceof Map || Object.prototype.toString.call(raw) === "[object Map]" && typeof raw.entries === "function";
720
733
  }
@@ -782,6 +795,10 @@ function createBindingsFromModule(moduleLike) {
782
795
  "estimatePitchRatioAgainstReferenceFeatures",
783
796
  "estimate_pitch_ratio_against_reference_features"
784
797
  ]);
798
+ const detectAudioMarkerLevelRobustFn = resolveFactory(mod, [
799
+ "detectAudioMarkerLevelRobust",
800
+ "detect_audio_marker_level_robust"
801
+ ]);
785
802
  const detectAudioMarkerFn = resolveFactory(mod, [
786
803
  "detectAudioMarker",
787
804
  "detect_audio_marker",
@@ -926,6 +943,26 @@ function createBindingsFromModule(moduleLike) {
926
943
  destroy?.call(matcher);
927
944
  }
928
945
  },
946
+ detectAudioMarkerLevelRobust(pcm16Le, sampleRate, config, limits) {
947
+ if (detectAudioMarkerLevelRobustFn) {
948
+ return normalizeLevelRobustDetection(
949
+ detectAudioMarkerLevelRobustFn(pcm16Le, sampleRate, config, limits)
950
+ );
951
+ }
952
+ const matcher = createMatcherFactory ? createMatcherFactory() : new MatcherCtor();
953
+ try {
954
+ const fn = getCallable(matcher, [
955
+ "detectAudioMarkerLevelRobust",
956
+ "detect_audio_marker_level_robust"
957
+ ]);
958
+ if (!fn) {
959
+ throw new Error("WASM matcher is missing detectAudioMarkerLevelRobust; upgrade the WASM assets or explicitly select levelPolicy: legacy");
960
+ }
961
+ return normalizeLevelRobustDetection(fn.call(matcher, pcm16Le, sampleRate, config, limits));
962
+ } finally {
963
+ getCallable(matcher, ["destroy", "free"])?.call(matcher);
964
+ }
965
+ },
929
966
  detectAudioMarkerWithLimits(pcm16Le, sampleRate, config, limits) {
930
967
  if (detectAudioMarkerWithLimitsFn) {
931
968
  return normalizeAudioMarkerDetection(
@@ -970,6 +1007,42 @@ async function getBindings(options) {
970
1007
  function toUint8ArrayCopy(input) {
971
1008
  return new Uint8Array(input);
972
1009
  }
1010
+ var typedArrayPrototype = Object.getPrototypeOf(Uint8Array.prototype);
1011
+ var typedArrayByteLength = Object.getOwnPropertyDescriptor(typedArrayPrototype, "byteLength").get;
1012
+ var typedArrayTag = Object.getOwnPropertyDescriptor(typedArrayPrototype, Symbol.toStringTag).get;
1013
+ var typedArraySet = Uint8Array.prototype.set;
1014
+ function markerByteLength(input) {
1015
+ if (typedArrayTag.call(input) !== "Uint8Array") {
1016
+ throw new TypeError("Audio marker PCM must be a Uint8Array");
1017
+ }
1018
+ return typedArrayByteLength.call(input);
1019
+ }
1020
+ function copyMarkerPcm(input) {
1021
+ const copy = new Uint8Array(markerByteLength(input));
1022
+ typedArraySet.call(copy, input);
1023
+ return copy;
1024
+ }
1025
+ function usesLevelRobust(config) {
1026
+ if (config?.levelPolicy !== void 0) {
1027
+ if (config.levelPolicy !== "levelRobust" && config.levelPolicy !== "legacy") {
1028
+ throw new RangeError("Invalid audio marker levelPolicy");
1029
+ }
1030
+ return config.levelPolicy === "levelRobust";
1031
+ }
1032
+ return ![
1033
+ config?.minSyncScore,
1034
+ config?.minScore,
1035
+ config?.minMargin,
1036
+ config?.minRatio,
1037
+ config?.minConfidence,
1038
+ config?.minVotes
1039
+ ].some((value) => value !== void 0);
1040
+ }
1041
+ function detectorConfig(config) {
1042
+ if (!config || config.levelPolicy === void 0) return config;
1043
+ const { levelPolicy: _policy, ...rest } = config;
1044
+ return rest;
1045
+ }
973
1046
  function resolveAudioMarkerLimits(limits) {
974
1047
  const resolved = {
975
1048
  maxPcmBytes: limits?.maxPcmBytes ?? DEFAULT_MAX_AUDIO_MARKER_PCM_BYTES,
@@ -985,13 +1058,17 @@ function resolveAudioMarkerLimits(limits) {
985
1058
  return resolved;
986
1059
  }
987
1060
  function audioMarkerInputFailureReason(pcm16Le, sampleRate, limits) {
988
- if (pcm16Le.byteLength > limits.maxPcmBytes) {
1061
+ const byteLength = markerByteLength(pcm16Le);
1062
+ if (byteLength > limits.maxPcmBytes) {
989
1063
  return AUDIO_MARKER_INPUT_BUDGET_EXCEEDED_REASON;
990
1064
  }
991
- if (pcm16Le.byteLength % 2 !== 0) {
1065
+ if (byteLength % 2 !== 0) {
992
1066
  return "invalid_pcm16_byte_length";
993
1067
  }
994
- const sampleCount = pcm16Le.byteLength / 2;
1068
+ if (!Number.isInteger(sampleRate) || sampleRate <= 0 || sampleRate > 4294967295) {
1069
+ return "invalid_sample_rate";
1070
+ }
1071
+ const sampleCount = byteLength / 2;
995
1072
  if (sampleCount > limits.maxSamples) {
996
1073
  return AUDIO_MARKER_INPUT_BUDGET_EXCEEDED_REASON;
997
1074
  }
@@ -1293,15 +1370,24 @@ var MatcherSession = class {
1293
1370
  handleError(this.events, "estimatePitchRatioAgainstReferenceFeatures", error);
1294
1371
  }
1295
1372
  }
1373
+ /** Uses level-robust detection by default, preserving explicit legacy thresholds. */
1296
1374
  async detectAudioMarker(pcm16Le, sampleRate, config) {
1297
1375
  try {
1298
1376
  await this.ensureLiveReady();
1377
+ const robust = usesLevelRobust(config);
1299
1378
  const limits = resolveAudioMarkerLimits();
1300
1379
  const failureReason = audioMarkerInputFailureReason(pcm16Le, sampleRate, limits);
1301
1380
  if (failureReason) {
1302
- return absentAudioMarkerDetection(config, failureReason);
1381
+ return {
1382
+ ...absentAudioMarkerDetection(config, failureReason),
1383
+ ...robust ? { levelPolicy: "levelRobust", candidateEligible: false, materialFallbackGuardEligible: false } : {}
1384
+ };
1385
+ }
1386
+ const safePcm = copyMarkerPcm(pcm16Le);
1387
+ config = detectorConfig(config);
1388
+ if (robust) {
1389
+ return this.bindings.detectAudioMarkerLevelRobust(safePcm, sampleRate, config);
1303
1390
  }
1304
- const safePcm = toUint8ArrayCopy(pcm16Le);
1305
1391
  const matcher = this.matcherOrThrow();
1306
1392
  if (matcher.detectAudioMarker) {
1307
1393
  return matcher.detectAudioMarker(safePcm, sampleRate, config);
@@ -1318,6 +1404,7 @@ var MatcherSession = class {
1318
1404
  async detectAudioMarkerWithLimits(pcm16Le, sampleRate, config, limits) {
1319
1405
  try {
1320
1406
  await this.ensureLiveReady();
1407
+ const robust = usesLevelRobust(config);
1321
1408
  const resolvedLimits = resolveAudioMarkerLimits(limits);
1322
1409
  const failureReason = audioMarkerInputFailureReason(
1323
1410
  pcm16Le,
@@ -1325,9 +1412,16 @@ var MatcherSession = class {
1325
1412
  resolvedLimits
1326
1413
  );
1327
1414
  if (failureReason) {
1328
- return absentAudioMarkerDetection(config, failureReason);
1415
+ return {
1416
+ ...absentAudioMarkerDetection(config, failureReason),
1417
+ ...robust ? { levelPolicy: "levelRobust", candidateEligible: false, materialFallbackGuardEligible: false } : {}
1418
+ };
1419
+ }
1420
+ const safePcm = copyMarkerPcm(pcm16Le);
1421
+ config = detectorConfig(config);
1422
+ if (robust) {
1423
+ return this.bindings.detectAudioMarkerLevelRobust(safePcm, sampleRate, config, resolvedLimits);
1329
1424
  }
1330
- const safePcm = toUint8ArrayCopy(pcm16Le);
1331
1425
  const matcher = this.matcherOrThrow();
1332
1426
  if (matcher.detectAudioMarkerWithLimits) {
1333
1427
  return matcher.detectAudioMarkerWithLimits(
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sorisdk/matcher",
3
- "version": "0.6.9",
3
+ "version": "0.6.10",
4
4
  "description": "Browser WASM SDK for audio fingerprint matching",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",