@sorisdk/matcher 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.
package/README.md CHANGED
@@ -55,6 +55,38 @@ const session = new MatcherSession({
55
55
  });
56
56
  ```
57
57
 
58
+ ## Continuous recognition and reset
59
+
60
+ Every query returns its matching result, including repeated material matches.
61
+ `match`, `bestMatch`, and their variant methods share one event observer:
62
+ continuous A → A emits one `match`, while A → B → A emits three. Material
63
+ identity includes the fingerprint type. There is no material cooldown.
64
+
65
+ Every successful observation resets the consecutive-miss counter and renews
66
+ the run, even when its public event is suppressed. Two consecutive no-match
67
+ results end the run and emit one `nomatch`; a single transient miss does not.
68
+ A gap **greater than 30 seconds since the last successful observation** also
69
+ ends continuity, including periods with no queries. This boundary is evaluated
70
+ on the next query: a success emits a new `match` immediately, or a no-match
71
+ emits `nomatch` without waiting for a second miss. The exported
72
+ `MATCH_CONTINUITY_GAP_MS` is shared with Web activity reporting.
73
+
74
+ Call `session.resetRecognitionState()` at an explicit input/capture boundary.
75
+ It synchronously clears event suppression and invalidates pending queries from
76
+ the previous run, retaining the loaded database. Invalidated queries return
77
+ `null` (best-match methods) or `[]` (multi-match methods) without match/no-match
78
+ events. Successful pack replacement and entry mutations also reset recognition
79
+ state; a failed pack load retains the previous state and entries.
80
+
81
+ The low-level generated WASM matcher always returns successful observations;
82
+ event deduplication belongs to `MatcherSession`. Its existing
83
+ `resetDistinctMatchState()` export remains available for compatibility.
84
+
85
+ For source regression tests, build the package first with
86
+ `corepack pnpm --filter @sorisdk/matcher build`, then run
87
+ `corepack pnpm --filter @sorisdk/matcher test`. Tests include production WASM
88
+ instantiated with the TypeScript session, using deterministic fingerprints.
89
+
58
90
  ## Pack loading
59
91
 
60
92
  `loadPack(...)` accepts:
@@ -89,6 +121,44 @@ per-entry limits remain independently enforced.
89
121
 
90
122
  If you want campaign events and browser recognition flow on top of matching, use `@sorisdk/web-audio`.
91
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
+
92
162
  ## Audio-marker detection limits
93
163
 
94
164
  `MatcherSession.detectAudioMarker(...)` preserves its existing signature and
@@ -117,7 +187,9 @@ const detection = await session.detectAudioMarkerWithLimits(
117
187
  ```
118
188
 
119
189
  The generated WASM module exposes the corresponding
120
- `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
121
193
  `Uint8Array` length before allocating a Rust-owned copy. Raising these budgets
122
194
  permits greater memory and CPU use; it does not protect an untrusted host after
123
195
  the caller opts into the larger envelope.
@@ -35,11 +35,21 @@ export class WasmMatcherEngine {
35
35
  matchQuery(database: WasmMatcherDatabase, query: Uint8Array, config?: any | null): any;
36
36
  matchQueryWithVariants(database: WasmMatcherDatabase, query: Uint8Array, pitch_query: any, config?: any | null, variant_config?: any | null): any;
37
37
  constructor();
38
+ /**
39
+ * Compatibility no-op: continuous-run deduplication belongs to MatcherSession.
40
+ * The binding returns every successful observation and retains no run state.
41
+ */
38
42
  resetDistinctMatchState(): void;
39
43
  }
40
44
 
41
45
  export function detectAudioMarker(pcm16_le: Uint8Array, sample_rate: number, config?: any | null): any;
42
46
 
47
+ /**
48
+ * Explicit v1 policy; existing marker exports remain on the legacy policy.
49
+ * Result includes raw detection, normalized diagnostics and shared eligibility.
50
+ */
51
+ export function detectAudioMarkerLevelRobust(pcm16_le: Uint8Array, sample_rate: number, config?: any | null, limits?: any | null): any;
52
+
43
53
  export function detectAudioMarkerWithLimits(pcm16_le: Uint8Array, sample_rate: number, config?: any | null, limits?: any | null): any;
44
54
 
45
55
  export function detectPatternMark(pcm16_le: Uint8Array, sample_rate: number, config?: any | null): any;
@@ -5,5 +5,5 @@ import { __wbg_set_wasm } from "./core_bg.js";
5
5
  __wbg_set_wasm(wasm);
6
6
  wasm.__wbindgen_start();
7
7
  export {
8
- WasmMatcherDatabase, WasmMatcherEngine, detectAudioMarker, detectAudioMarkerWithLimits, detectPatternMark, detectPatternMarkWithLimits, estimatePitchRatio, estimatePitchRatioAgainstReference, estimatePitchRatioAgainstSpectralFeatures, extractPitchReference, extractSpectralShiftReference, init_matcher_wasm
8
+ WasmMatcherDatabase, WasmMatcherEngine, detectAudioMarker, detectAudioMarkerLevelRobust, detectAudioMarkerWithLimits, detectPatternMark, detectPatternMarkWithLimits, estimatePitchRatio, estimatePitchRatioAgainstReference, estimatePitchRatioAgainstSpectralFeatures, extractPitchReference, extractSpectralShiftReference, init_matcher_wasm
9
9
  } from "./core_bg.js";
@@ -359,6 +359,10 @@ export class WasmMatcherEngine {
359
359
  WasmMatcherEngineFinalization.register(this, this.__wbg_ptr, this);
360
360
  return this;
361
361
  }
362
+ /**
363
+ * Compatibility no-op: continuous-run deduplication belongs to MatcherSession.
364
+ * The binding returns every successful observation and retains no run state.
365
+ */
362
366
  resetDistinctMatchState() {
363
367
  wasm.wasmmatcherengine_resetDistinctMatchState(this.__wbg_ptr);
364
368
  }
@@ -379,6 +383,23 @@ export function detectAudioMarker(pcm16_le, sample_rate, config) {
379
383
  return takeFromExternrefTable0(ret[0]);
380
384
  }
381
385
 
386
+ /**
387
+ * Explicit v1 policy; existing marker exports remain on the legacy policy.
388
+ * Result includes raw detection, normalized diagnostics and shared eligibility.
389
+ * @param {Uint8Array} pcm16_le
390
+ * @param {number} sample_rate
391
+ * @param {any | null} [config]
392
+ * @param {any | null} [limits]
393
+ * @returns {any}
394
+ */
395
+ export function detectAudioMarkerLevelRobust(pcm16_le, sample_rate, config, limits) {
396
+ const ret = wasm.detectAudioMarkerLevelRobust(pcm16_le, sample_rate, isLikeNone(config) ? 0 : addToExternrefTable0(config), isLikeNone(limits) ? 0 : addToExternrefTable0(limits));
397
+ if (ret[2]) {
398
+ throw takeFromExternrefTable0(ret[1]);
399
+ }
400
+ return takeFromExternrefTable0(ret[0]);
401
+ }
402
+
382
403
  /**
383
404
  * @param {Uint8Array} pcm16_le
384
405
  * @param {number} sample_rate
@@ -599,6 +620,10 @@ export function __wbg_call_14b169f759b26747() { return handleError(function (arg
599
620
  const ret = arg0.call(arg1);
600
621
  return ret;
601
622
  }, arguments); }
623
+ export function __wbg_call_a24592a6f349a97e() { return handleError(function (arg0, arg1, arg2) {
624
+ const ret = arg0.call(arg1, arg2);
625
+ return ret;
626
+ }, arguments); }
602
627
  export function __wbg_done_9158f7cc8751ba32(arg0) {
603
628
  const ret = arg0.done;
604
629
  return ret;
@@ -618,10 +643,22 @@ export function __wbg_error_a6fa202b58aa1cd3(arg0, arg1) {
618
643
  wasm.__wbindgen_free(deferred0_0, deferred0_1, 1);
619
644
  }
620
645
  }
646
+ export function __wbg_getOwnPropertyDescriptor_131bd582a45a6f5d(arg0, arg1) {
647
+ const ret = Object.getOwnPropertyDescriptor(arg0, arg1);
648
+ return ret;
649
+ }
650
+ export function __wbg_getPrototypeOf_a56da9261bbd1e5b(arg0) {
651
+ const ret = Object.getPrototypeOf(arg0);
652
+ return ret;
653
+ }
621
654
  export function __wbg_get_1affdbdd5573b16a() { return handleError(function (arg0, arg1) {
622
655
  const ret = Reflect.get(arg0, arg1);
623
656
  return ret;
624
657
  }, arguments); }
658
+ export function __wbg_get_6011fa3a58f61074() { return handleError(function (arg0, arg1) {
659
+ const ret = Reflect.get(arg0, arg1);
660
+ return ret;
661
+ }, arguments); }
625
662
  export function __wbg_get_8360291721e2339f(arg0, arg1) {
626
663
  const ret = arg0[arg1 >>> 0];
627
664
  return ret;
@@ -716,10 +753,6 @@ export function __wbg_next_7646edaa39458ef7(arg0) {
716
753
  const ret = arg0.next;
717
754
  return ret;
718
755
  }
719
- export function __wbg_now_a9b7df1cbee90986() {
720
- const ret = Date.now();
721
- return ret;
722
- }
723
756
  export function __wbg_prototypesetcall_a6b02eb00b0f4ce2(arg0, arg1, arg2) {
724
757
  Uint8Array.prototype.set.call(getArrayU8FromWasm0(arg0, arg1), arg2);
725
758
  }
@@ -740,6 +773,26 @@ export function __wbg_stack_3b0d974bbf31e44f(arg0, arg1) {
740
773
  getDataViewMemory0().setInt32(arg0 + 4 * 1, len1, true);
741
774
  getDataViewMemory0().setInt32(arg0 + 4 * 0, ptr1, true);
742
775
  }
776
+ export function __wbg_static_accessor_GLOBAL_8cfadc87a297ca02() {
777
+ const ret = typeof global === 'undefined' ? null : global;
778
+ return isLikeNone(ret) ? 0 : addToExternrefTable0(ret);
779
+ }
780
+ export function __wbg_static_accessor_GLOBAL_THIS_602256ae5c8f42cf() {
781
+ const ret = typeof globalThis === 'undefined' ? null : globalThis;
782
+ return isLikeNone(ret) ? 0 : addToExternrefTable0(ret);
783
+ }
784
+ export function __wbg_static_accessor_SELF_e445c1c7484aecc3() {
785
+ const ret = typeof self === 'undefined' ? null : self;
786
+ return isLikeNone(ret) ? 0 : addToExternrefTable0(ret);
787
+ }
788
+ export function __wbg_static_accessor_WINDOW_f20e8576ef1e0f17() {
789
+ const ret = typeof window === 'undefined' ? null : window;
790
+ return isLikeNone(ret) ? 0 : addToExternrefTable0(ret);
791
+ }
792
+ export function __wbg_toStringTag_d6b1716c86d4892d() {
793
+ const ret = Symbol.toStringTag;
794
+ return ret;
795
+ }
743
796
  export function __wbg_value_ee3a06f4579184fa(arg0) {
744
797
  const ret = arg0.value;
745
798
  return ret;
@@ -755,11 +808,16 @@ export function __wbindgen_cast_0000000000000002(arg0) {
755
808
  return ret;
756
809
  }
757
810
  export function __wbindgen_cast_0000000000000003(arg0, arg1) {
811
+ // Cast intrinsic for `Ref(Slice(U8)) -> NamedExternref("Uint8Array")`.
812
+ const ret = getArrayU8FromWasm0(arg0, arg1);
813
+ return ret;
814
+ }
815
+ export function __wbindgen_cast_0000000000000004(arg0, arg1) {
758
816
  // Cast intrinsic for `Ref(String) -> Externref`.
759
817
  const ret = getStringFromWasm0(arg0, arg1);
760
818
  return ret;
761
819
  }
762
- export function __wbindgen_cast_0000000000000004(arg0) {
820
+ export function __wbindgen_cast_0000000000000005(arg0) {
763
821
  // Cast intrinsic for `U64 -> Externref`.
764
822
  const ret = BigInt.asUintN(64, arg0);
765
823
  return ret;
Binary file
@@ -4,6 +4,7 @@ export const memory: WebAssembly.Memory;
4
4
  export const __wbg_wasmmatcherdatabase_free: (a: number, b: number) => void;
5
5
  export const __wbg_wasmmatcherengine_free: (a: number, b: number) => void;
6
6
  export const detectAudioMarker: (a: any, b: number, c: number) => [number, number, number];
7
+ export const detectAudioMarkerLevelRobust: (a: any, b: number, c: number, d: number) => [number, number, number];
7
8
  export const detectAudioMarkerWithLimits: (a: any, b: number, c: number, d: number) => [number, number, number];
8
9
  export const detectPatternMarkWithLimits: (a: any, b: number, c: number, d: number) => [number, number, number];
9
10
  export const estimatePitchRatio: (a: number, b: number, c: number) => [number, number, number];
@@ -27,7 +28,6 @@ export const wasmmatcherengine_bestMatch: (a: number, b: number, c: number, d: n
27
28
  export const wasmmatcherengine_bestMatchWithVariants: (a: number, b: number, c: number, d: number, e: any, f: number, g: number) => [number, number, number];
28
29
  export const wasmmatcherengine_detectAudioMarker: (a: number, b: any, c: number, d: number) => [number, number, number];
29
30
  export const wasmmatcherengine_detectAudioMarkerWithLimits: (a: number, b: any, c: number, d: number, e: number) => [number, number, number];
30
- export const wasmmatcherengine_detectPatternMark: (a: number, b: any, c: number, d: number) => [number, number, number];
31
31
  export const wasmmatcherengine_detectPatternMarkWithLimits: (a: number, b: any, c: number, d: number, e: number) => [number, number, number];
32
32
  export const wasmmatcherengine_estimatePitchRatio: (a: number, b: number, c: number, d: number) => [number, number, number];
33
33
  export const wasmmatcherengine_estimatePitchRatioAgainstReference: (a: number, b: number, c: number, d: number, e: number, f: number, g: number) => [number, number, number];
@@ -36,10 +36,11 @@ export const wasmmatcherengine_extractPitchReference: (a: number, b: number, c:
36
36
  export const wasmmatcherengine_extractSpectralShiftReference: (a: number, b: number, c: number, d: number) => [number, number, number];
37
37
  export const wasmmatcherengine_matchQuery: (a: number, b: number, c: number, d: number, e: number) => [number, number, number];
38
38
  export const wasmmatcherengine_matchQueryWithVariants: (a: number, b: number, c: number, d: number, e: any, f: number, g: number) => [number, number, number];
39
- export const wasmmatcherengine_new: () => number;
40
39
  export const wasmmatcherengine_resetDistinctMatchState: (a: number) => void;
40
+ export const wasmmatcherengine_new: () => number;
41
41
  export const init_matcher_wasm: () => void;
42
42
  export const detectPatternMark: (a: any, b: number, c: number) => [number, number, number];
43
+ export const wasmmatcherengine_detectPatternMark: (a: number, b: any, c: number, d: number) => [number, number, number];
43
44
  export const __wbindgen_malloc: (a: number, b: number) => number;
44
45
  export const __wbindgen_realloc: (a: number, b: number, c: number, d: number) => number;
45
46
  export const __wbindgen_exn_store: (a: number) => void;
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;
@@ -230,6 +257,7 @@ declare class MatcherSession {
230
257
  private matcher;
231
258
  private destroyed;
232
259
  private packLoadTail;
260
+ private recognitionGeneration;
233
261
  private readonly distinctMatchDeduper;
234
262
  constructor(options?: MatcherSessionOptions);
235
263
  on<T extends MatcherSessionEventName>(eventName: T, listener: MatcherSessionListener<T>): this;
@@ -250,11 +278,16 @@ declare class MatcherSession {
250
278
  extractPitchReference(pcm16Le: Uint8Array, sampleRate: number): Promise<PitchReferenceEstimate>;
251
279
  estimatePitchRatioAgainstReference(pcm16Le: Uint8Array, sampleRate: number, referencePitchHz: number, referenceConfidence?: number | null): Promise<PitchRatioEstimate>;
252
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. */
253
282
  detectAudioMarker(pcm16Le: Uint8Array, sampleRate: number, config?: AudioMarkerConfig): Promise<AudioMarkerDetection>;
254
283
  /** Detect an audio marker with explicit limits for trusted offline input. */
255
284
  detectAudioMarkerWithLimits(pcm16Le: Uint8Array, sampleRate: number, config: AudioMarkerConfig | undefined, limits: AudioMarkerLimits): Promise<AudioMarkerDetection>;
256
285
  matchWithVariants(query: Uint8Array, pitchQuery: VariantMatchQuery, config?: MatchConfig, variantConfig?: VariantMatchConfig): Promise<VariantMatch[]>;
257
286
  bestMatchWithVariants(query: Uint8Array, pitchQuery: VariantMatchQuery, config?: MatchConfig, variantConfig?: VariantMatchConfig): Promise<VariantMatch | null>;
287
+ /** End the current recognition run without removing loaded entries.
288
+ * Pending queries from the previous run cannot update event suppression state.
289
+ */
290
+ resetRecognitionState(): void;
258
291
  destroy(): void;
259
292
  private bootstrap;
260
293
  private ensureLiveReady;
@@ -268,4 +301,7 @@ declare function __setMatcherWasmModuleForTests(moduleLike: unknown): void;
268
301
  declare function __setMatcherEmbeddedPackForTests(bytes: Uint8Array): void;
269
302
  declare function __resetMatcherWasmForTests(): void;
270
303
 
271
- 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, 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 };
304
+ /** Maximum gap between successful observations in one continuous material run. */
305
+ declare const MATCH_CONTINUITY_GAP_MS = 30000;
306
+
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
@@ -327,28 +327,31 @@ var AUDIO_MARKER_WORK_OVERFLOW_REASON = "detector_work_overflow";
327
327
 
328
328
  // src/match-deduper.ts
329
329
  var DEFAULT_NO_MATCH_CONFIRMATION_COUNT = 2;
330
+ var MATCH_CONTINUITY_GAP_MS = 3e4;
330
331
  function getMatchIdentity(match) {
331
332
  return `${match.name}:${match.afpType}`;
332
333
  }
333
334
  var DistinctMatchDeduper = class {
334
335
  lastIdentity = null;
335
336
  pendingNoMatchCount = 0;
336
- observe(best) {
337
+ lastObservedAt = null;
338
+ observe(best, now = Date.now()) {
339
+ const expired = this.lastObservedAt !== null && now - this.lastObservedAt > MATCH_CONTINUITY_GAP_MS;
337
340
  if (!best) {
338
341
  if (this.lastIdentity === null) {
339
342
  return { kind: "repeat", best: null };
340
343
  }
341
344
  this.pendingNoMatchCount += 1;
342
- if (this.pendingNoMatchCount < DEFAULT_NO_MATCH_CONFIRMATION_COUNT) {
345
+ if (!expired && this.pendingNoMatchCount < DEFAULT_NO_MATCH_CONFIRMATION_COUNT) {
343
346
  return { kind: "repeat", best: null };
344
347
  }
345
- this.lastIdentity = null;
346
- this.pendingNoMatchCount = 0;
348
+ this.reset();
347
349
  return { kind: "nomatch" };
348
350
  }
349
351
  const nextIdentity = getMatchIdentity(best);
350
352
  this.pendingNoMatchCount = 0;
351
- if (this.lastIdentity === nextIdentity) {
353
+ this.lastObservedAt = now;
354
+ if (!expired && this.lastIdentity === nextIdentity) {
352
355
  return { kind: "repeat", best };
353
356
  }
354
357
  this.lastIdentity = nextIdentity;
@@ -357,6 +360,7 @@ var DistinctMatchDeduper = class {
357
360
  reset() {
358
361
  this.lastIdentity = null;
359
362
  this.pendingNoMatchCount = 0;
363
+ this.lastObservedAt = null;
360
364
  }
361
365
  };
362
366
 
@@ -711,6 +715,19 @@ function normalizeAudioMarkerDetection(raw) {
711
715
  runnerUpAccuracy: normalizeOptionalNumber(record.runnerUpAccuracy, record.runner_up_accuracy)
712
716
  };
713
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
+ }
714
731
  function isMapLike(raw) {
715
732
  return raw instanceof Map || Object.prototype.toString.call(raw) === "[object Map]" && typeof raw.entries === "function";
716
733
  }
@@ -753,11 +770,6 @@ function normalizeResults(rawResults) {
753
770
  function normalizeVariantMatches(rawResults) {
754
771
  return rawResults.filter((item) => typeof item === "object" && item !== null).map((item) => normalizeVariantMatch(item));
755
772
  }
756
- function isInternallyDuplicatedBestMatch(result) {
757
- return Boolean(
758
- result && typeof result === "object" && result[INTERNAL_DUPLICATE_MATCH_FLAG] === true
759
- );
760
- }
761
773
  function resolveFactory(moduleLike, names) {
762
774
  const fn = getCallable(moduleLike, names);
763
775
  return fn ? fn.bind(moduleLike) : null;
@@ -783,6 +795,10 @@ function createBindingsFromModule(moduleLike) {
783
795
  "estimatePitchRatioAgainstReferenceFeatures",
784
796
  "estimate_pitch_ratio_against_reference_features"
785
797
  ]);
798
+ const detectAudioMarkerLevelRobustFn = resolveFactory(mod, [
799
+ "detectAudioMarkerLevelRobust",
800
+ "detect_audio_marker_level_robust"
801
+ ]);
786
802
  const detectAudioMarkerFn = resolveFactory(mod, [
787
803
  "detectAudioMarker",
788
804
  "detect_audio_marker",
@@ -927,6 +943,26 @@ function createBindingsFromModule(moduleLike) {
927
943
  destroy?.call(matcher);
928
944
  }
929
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
+ },
930
966
  detectAudioMarkerWithLimits(pcm16Le, sampleRate, config, limits) {
931
967
  if (detectAudioMarkerWithLimitsFn) {
932
968
  return normalizeAudioMarkerDetection(
@@ -971,6 +1007,42 @@ async function getBindings(options) {
971
1007
  function toUint8ArrayCopy(input) {
972
1008
  return new Uint8Array(input);
973
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
+ }
974
1046
  function resolveAudioMarkerLimits(limits) {
975
1047
  const resolved = {
976
1048
  maxPcmBytes: limits?.maxPcmBytes ?? DEFAULT_MAX_AUDIO_MARKER_PCM_BYTES,
@@ -986,13 +1058,17 @@ function resolveAudioMarkerLimits(limits) {
986
1058
  return resolved;
987
1059
  }
988
1060
  function audioMarkerInputFailureReason(pcm16Le, sampleRate, limits) {
989
- if (pcm16Le.byteLength > limits.maxPcmBytes) {
1061
+ const byteLength = markerByteLength(pcm16Le);
1062
+ if (byteLength > limits.maxPcmBytes) {
990
1063
  return AUDIO_MARKER_INPUT_BUDGET_EXCEEDED_REASON;
991
1064
  }
992
- if (pcm16Le.byteLength % 2 !== 0) {
1065
+ if (byteLength % 2 !== 0) {
993
1066
  return "invalid_pcm16_byte_length";
994
1067
  }
995
- 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;
996
1072
  if (sampleCount > limits.maxSamples) {
997
1073
  return AUDIO_MARKER_INPUT_BUDGET_EXCEEDED_REASON;
998
1074
  }
@@ -1050,6 +1126,7 @@ var MatcherSession = class {
1050
1126
  matcher = null;
1051
1127
  destroyed = false;
1052
1128
  packLoadTail = Promise.resolve();
1129
+ recognitionGeneration = 0;
1053
1130
  distinctMatchDeduper = new DistinctMatchDeduper();
1054
1131
  constructor(options) {
1055
1132
  this.fetcher = options?.fetch;
@@ -1102,8 +1179,7 @@ var MatcherSession = class {
1102
1179
  }
1103
1180
  this.database = stagedDatabase;
1104
1181
  previousDatabase.destroy();
1105
- this.distinctMatchDeduper.reset();
1106
- this.matcher?.resetDistinctMatchState?.();
1182
+ this.resetRecognitionState();
1107
1183
  this.events.emit("packload", {
1108
1184
  sourceType: normalized.sourceType,
1109
1185
  byteLength: normalized.bytes.byteLength
@@ -1116,8 +1192,7 @@ var MatcherSession = class {
1116
1192
  try {
1117
1193
  await this.ensureLiveReady();
1118
1194
  this.databaseOrThrow().addEntry(name, afpType, toUint8ArrayCopy(fingerprint));
1119
- this.distinctMatchDeduper.reset();
1120
- this.matcher?.resetDistinctMatchState?.();
1195
+ this.resetRecognitionState();
1121
1196
  } catch (error) {
1122
1197
  handleError(this.events, "addEntry", error);
1123
1198
  }
@@ -1131,8 +1206,7 @@ var MatcherSession = class {
1131
1206
  toUint8ArrayCopy(fingerprint),
1132
1207
  metadata
1133
1208
  );
1134
- this.distinctMatchDeduper.reset();
1135
- this.matcher?.resetDistinctMatchState?.();
1209
+ this.resetRecognitionState();
1136
1210
  } catch (error) {
1137
1211
  handleError(this.events, "addOrReplace", error);
1138
1212
  }
@@ -1141,8 +1215,7 @@ var MatcherSession = class {
1141
1215
  try {
1142
1216
  await this.ensureLiveReady();
1143
1217
  this.databaseOrThrow().setEntryVariantMetadata(name, encodedMetadata);
1144
- this.distinctMatchDeduper.reset();
1145
- this.matcher?.resetDistinctMatchState?.();
1218
+ this.resetRecognitionState();
1146
1219
  } catch (error) {
1147
1220
  handleError(this.events, "setEntryVariantMetadata", error);
1148
1221
  }
@@ -1151,8 +1224,7 @@ var MatcherSession = class {
1151
1224
  try {
1152
1225
  await this.ensureLiveReady();
1153
1226
  this.databaseOrThrow().removeEntry(name);
1154
- this.distinctMatchDeduper.reset();
1155
- this.matcher?.resetDistinctMatchState?.();
1227
+ this.resetRecognitionState();
1156
1228
  } catch (error) {
1157
1229
  handleError(this.events, "removeEntry", error);
1158
1230
  }
@@ -1161,8 +1233,7 @@ var MatcherSession = class {
1161
1233
  try {
1162
1234
  await this.ensureLiveReady();
1163
1235
  this.databaseOrThrow().clear();
1164
- this.distinctMatchDeduper.reset();
1165
- this.matcher?.resetDistinctMatchState?.();
1236
+ this.resetRecognitionState();
1166
1237
  } catch (error) {
1167
1238
  handleError(this.events, "clear", error);
1168
1239
  }
@@ -1176,27 +1247,38 @@ var MatcherSession = class {
1176
1247
  }
1177
1248
  }
1178
1249
  async match(query, config) {
1250
+ const generation = this.recognitionGeneration;
1179
1251
  try {
1180
1252
  await this.ensureLiveReady();
1253
+ if (generation !== this.recognitionGeneration) {
1254
+ return [];
1255
+ }
1181
1256
  const safeQuery = toUint8ArrayCopy(query);
1182
1257
  this.events.emit("querystart", { queryByteLength: safeQuery.byteLength });
1258
+ if (generation !== this.recognitionGeneration || this.destroyed) {
1259
+ return [];
1260
+ }
1183
1261
  const results = this.matcherOrThrow().findMatches(this.databaseOrThrow(), safeQuery, config);
1184
- this.emitDistinctMatchEvent(results[0] ?? null, safeQuery.byteLength, results);
1262
+ this.emitDistinctMatchEvent(generation, results[0] ?? null, safeQuery.byteLength, results);
1185
1263
  return results;
1186
1264
  } catch (error) {
1187
1265
  handleError(this.events, "match", error);
1188
1266
  }
1189
1267
  }
1190
1268
  async bestMatch(query, config) {
1269
+ const generation = this.recognitionGeneration;
1191
1270
  try {
1192
1271
  await this.ensureLiveReady();
1272
+ if (generation !== this.recognitionGeneration) {
1273
+ return null;
1274
+ }
1193
1275
  const safeQuery = toUint8ArrayCopy(query);
1194
1276
  this.events.emit("querystart", { queryByteLength: safeQuery.byteLength });
1195
- const best = this.matcherOrThrow().bestMatch(this.databaseOrThrow(), safeQuery, config);
1196
- if (isInternallyDuplicatedBestMatch(best)) {
1197
- return best;
1277
+ if (generation !== this.recognitionGeneration || this.destroyed) {
1278
+ return null;
1198
1279
  }
1199
- this.emitDistinctMatchEvent(best, safeQuery.byteLength, best ? [best] : []);
1280
+ const best = this.matcherOrThrow().bestMatch(this.databaseOrThrow(), safeQuery, config);
1281
+ this.emitDistinctMatchEvent(generation, best, safeQuery.byteLength, best ? [best] : []);
1200
1282
  return best;
1201
1283
  } catch (error) {
1202
1284
  handleError(this.events, "bestMatch", error);
@@ -1288,15 +1370,24 @@ var MatcherSession = class {
1288
1370
  handleError(this.events, "estimatePitchRatioAgainstReferenceFeatures", error);
1289
1371
  }
1290
1372
  }
1373
+ /** Uses level-robust detection by default, preserving explicit legacy thresholds. */
1291
1374
  async detectAudioMarker(pcm16Le, sampleRate, config) {
1292
1375
  try {
1293
1376
  await this.ensureLiveReady();
1377
+ const robust = usesLevelRobust(config);
1294
1378
  const limits = resolveAudioMarkerLimits();
1295
1379
  const failureReason = audioMarkerInputFailureReason(pcm16Le, sampleRate, limits);
1296
1380
  if (failureReason) {
1297
- 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);
1298
1390
  }
1299
- const safePcm = toUint8ArrayCopy(pcm16Le);
1300
1391
  const matcher = this.matcherOrThrow();
1301
1392
  if (matcher.detectAudioMarker) {
1302
1393
  return matcher.detectAudioMarker(safePcm, sampleRate, config);
@@ -1313,6 +1404,7 @@ var MatcherSession = class {
1313
1404
  async detectAudioMarkerWithLimits(pcm16Le, sampleRate, config, limits) {
1314
1405
  try {
1315
1406
  await this.ensureLiveReady();
1407
+ const robust = usesLevelRobust(config);
1316
1408
  const resolvedLimits = resolveAudioMarkerLimits(limits);
1317
1409
  const failureReason = audioMarkerInputFailureReason(
1318
1410
  pcm16Le,
@@ -1320,9 +1412,16 @@ var MatcherSession = class {
1320
1412
  resolvedLimits
1321
1413
  );
1322
1414
  if (failureReason) {
1323
- 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);
1324
1424
  }
1325
- const safePcm = toUint8ArrayCopy(pcm16Le);
1326
1425
  const matcher = this.matcherOrThrow();
1327
1426
  if (matcher.detectAudioMarkerWithLimits) {
1328
1427
  return matcher.detectAudioMarkerWithLimits(
@@ -1346,10 +1445,17 @@ var MatcherSession = class {
1346
1445
  }
1347
1446
  }
1348
1447
  async matchWithVariants(query, pitchQuery, config, variantConfig) {
1448
+ const generation = this.recognitionGeneration;
1349
1449
  try {
1350
1450
  await this.ensureLiveReady();
1451
+ if (generation !== this.recognitionGeneration) {
1452
+ return [];
1453
+ }
1351
1454
  const safeQuery = toUint8ArrayCopy(query);
1352
1455
  this.events.emit("querystart", { queryByteLength: safeQuery.byteLength });
1456
+ if (generation !== this.recognitionGeneration || this.destroyed) {
1457
+ return [];
1458
+ }
1353
1459
  const results = this.matcherOrThrow().findMatchesWithVariants(
1354
1460
  this.databaseOrThrow(),
1355
1461
  safeQuery,
@@ -1358,6 +1464,7 @@ var MatcherSession = class {
1358
1464
  variantConfig
1359
1465
  );
1360
1466
  this.emitDistinctMatchEvent(
1467
+ generation,
1361
1468
  results[0]?.matchResult ?? null,
1362
1469
  safeQuery.byteLength,
1363
1470
  results.map((result) => result.matchResult)
@@ -1368,10 +1475,17 @@ var MatcherSession = class {
1368
1475
  }
1369
1476
  }
1370
1477
  async bestMatchWithVariants(query, pitchQuery, config, variantConfig) {
1478
+ const generation = this.recognitionGeneration;
1371
1479
  try {
1372
1480
  await this.ensureLiveReady();
1481
+ if (generation !== this.recognitionGeneration) {
1482
+ return null;
1483
+ }
1373
1484
  const safeQuery = toUint8ArrayCopy(query);
1374
1485
  this.events.emit("querystart", { queryByteLength: safeQuery.byteLength });
1486
+ if (generation !== this.recognitionGeneration || this.destroyed) {
1487
+ return null;
1488
+ }
1375
1489
  const best = this.matcherOrThrow().bestMatchWithVariants(
1376
1490
  this.databaseOrThrow(),
1377
1491
  safeQuery,
@@ -1380,6 +1494,7 @@ var MatcherSession = class {
1380
1494
  variantConfig
1381
1495
  );
1382
1496
  this.emitDistinctMatchEvent(
1497
+ generation,
1383
1498
  best?.matchResult ?? null,
1384
1499
  safeQuery.byteLength,
1385
1500
  best ? [best.matchResult] : []
@@ -1389,10 +1504,19 @@ var MatcherSession = class {
1389
1504
  handleError(this.events, "bestMatchWithVariants", error);
1390
1505
  }
1391
1506
  }
1507
+ /** End the current recognition run without removing loaded entries.
1508
+ * Pending queries from the previous run cannot update event suppression state.
1509
+ */
1510
+ resetRecognitionState() {
1511
+ this.recognitionGeneration += 1;
1512
+ this.distinctMatchDeduper.reset();
1513
+ this.matcher?.resetDistinctMatchState?.();
1514
+ }
1392
1515
  destroy() {
1393
1516
  if (this.destroyed) {
1394
1517
  return;
1395
1518
  }
1519
+ this.resetRecognitionState();
1396
1520
  this.destroyed = true;
1397
1521
  this.matcher?.destroy();
1398
1522
  this.database?.destroy();
@@ -1456,7 +1580,10 @@ var MatcherSession = class {
1456
1580
  }
1457
1581
  return this.database;
1458
1582
  }
1459
- emitDistinctMatchEvent(best, queryByteLength, results) {
1583
+ emitDistinctMatchEvent(generation, best, queryByteLength, results) {
1584
+ if (generation !== this.recognitionGeneration || this.destroyed) {
1585
+ return;
1586
+ }
1460
1587
  const observation = this.distinctMatchDeduper.observe(best);
1461
1588
  if (observation.kind === "match") {
1462
1589
  this.events.emit("match", { results, best: observation.best });
@@ -1489,6 +1616,7 @@ export {
1489
1616
  DEFAULT_MAX_AUDIO_MARKER_SAMPLES,
1490
1617
  DEFAULT_MAX_AUDIO_MARKER_WORK_UNITS,
1491
1618
  DEFAULT_MAX_PACK_BYTES,
1619
+ MATCH_CONTINUITY_GAP_MS,
1492
1620
  MatcherSession,
1493
1621
  __resetMatcherWasmForTests,
1494
1622
  __setMatcherEmbeddedPackForTests,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sorisdk/matcher",
3
- "version": "0.6.8",
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",