@sorisdk/matcher 0.6.9 → 0.6.11

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.
@@ -22,6 +22,7 @@ export class WasmMatcherEngine {
22
22
  free(): void;
23
23
  [Symbol.dispose](): void;
24
24
  bestMatch(database: WasmMatcherDatabase, query: Uint8Array, config?: any | null): any;
25
+ bestMatchChecked(database: WasmMatcherDatabase, query: Uint8Array, config?: any | null): any;
25
26
  bestMatchWithVariants(database: WasmMatcherDatabase, query: Uint8Array, pitch_query: any, config?: any | null, variant_config?: any | null): any;
26
27
  detectAudioMarker(pcm16_le: Uint8Array, sample_rate: number, config?: any | null): any;
27
28
  detectAudioMarkerWithLimits(pcm16_le: Uint8Array, sample_rate: number, config?: any | null, limits?: any | null): any;
@@ -33,6 +34,7 @@ export class WasmMatcherEngine {
33
34
  extractPitchReference(pcm16_le: Uint8Array, sample_rate: number): any;
34
35
  extractSpectralShiftReference(pcm16_le: Uint8Array, sample_rate: number): any;
35
36
  matchQuery(database: WasmMatcherDatabase, query: Uint8Array, config?: any | null): any;
37
+ matchQueryChecked(database: WasmMatcherDatabase, query: Uint8Array, config?: any | null): any;
36
38
  matchQueryWithVariants(database: WasmMatcherDatabase, query: Uint8Array, pitch_query: any, config?: any | null, variant_config?: any | null): any;
37
39
  constructor();
38
40
  /**
@@ -170,6 +170,22 @@ export class WasmMatcherEngine {
170
170
  }
171
171
  return takeFromExternrefTable0(ret[0]);
172
172
  }
173
+ /**
174
+ * @param {WasmMatcherDatabase} database
175
+ * @param {Uint8Array} query
176
+ * @param {any | null} [config]
177
+ * @returns {any}
178
+ */
179
+ bestMatchChecked(database, query, config) {
180
+ _assertClass(database, WasmMatcherDatabase);
181
+ const ptr0 = passArray8ToWasm0(query, wasm.__wbindgen_malloc);
182
+ const len0 = WASM_VECTOR_LEN;
183
+ const ret = wasm.wasmmatcherengine_bestMatchChecked(this.__wbg_ptr, database.__wbg_ptr, ptr0, len0, isLikeNone(config) ? 0 : addToExternrefTable0(config));
184
+ if (ret[2]) {
185
+ throw takeFromExternrefTable0(ret[1]);
186
+ }
187
+ return takeFromExternrefTable0(ret[0]);
188
+ }
173
189
  /**
174
190
  * @param {WasmMatcherDatabase} database
175
191
  * @param {Uint8Array} query
@@ -335,6 +351,22 @@ export class WasmMatcherEngine {
335
351
  }
336
352
  return takeFromExternrefTable0(ret[0]);
337
353
  }
354
+ /**
355
+ * @param {WasmMatcherDatabase} database
356
+ * @param {Uint8Array} query
357
+ * @param {any | null} [config]
358
+ * @returns {any}
359
+ */
360
+ matchQueryChecked(database, query, config) {
361
+ _assertClass(database, WasmMatcherDatabase);
362
+ const ptr0 = passArray8ToWasm0(query, wasm.__wbindgen_malloc);
363
+ const len0 = WASM_VECTOR_LEN;
364
+ const ret = wasm.wasmmatcherengine_matchQueryChecked(this.__wbg_ptr, database.__wbg_ptr, ptr0, len0, isLikeNone(config) ? 0 : addToExternrefTable0(config));
365
+ if (ret[2]) {
366
+ throw takeFromExternrefTable0(ret[1]);
367
+ }
368
+ return takeFromExternrefTable0(ret[0]);
369
+ }
338
370
  /**
339
371
  * @param {WasmMatcherDatabase} database
340
372
  * @param {Uint8Array} query
Binary file
@@ -25,6 +25,7 @@ export const wasmmatcherdatabase_new: () => number;
25
25
  export const wasmmatcherdatabase_removeEntry: (a: number, b: number, c: number) => [number, number];
26
26
  export const wasmmatcherdatabase_setEntryVariantMetadata: (a: number, b: number, c: number, d: number, e: number) => [number, number];
27
27
  export const wasmmatcherengine_bestMatch: (a: number, b: number, c: number, d: number, e: number) => [number, number, number];
28
+ export const wasmmatcherengine_bestMatchChecked: (a: number, b: number, c: number, d: number, e: number) => [number, number, number];
28
29
  export const wasmmatcherengine_bestMatchWithVariants: (a: number, b: number, c: number, d: number, e: any, f: number, g: number) => [number, number, number];
29
30
  export const wasmmatcherengine_detectAudioMarker: (a: number, b: any, c: number, d: number) => [number, number, number];
30
31
  export const wasmmatcherengine_detectAudioMarkerWithLimits: (a: number, b: any, c: number, d: number, e: number) => [number, number, number];
@@ -35,9 +36,10 @@ export const wasmmatcherengine_estimatePitchRatioAgainstSpectralFeatures: (a: nu
35
36
  export const wasmmatcherengine_extractPitchReference: (a: number, b: number, c: number, d: number) => [number, number, number];
36
37
  export const wasmmatcherengine_extractSpectralShiftReference: (a: number, b: number, c: number, d: number) => [number, number, number];
37
38
  export const wasmmatcherengine_matchQuery: (a: number, b: number, c: number, d: number, e: number) => [number, number, number];
39
+ export const wasmmatcherengine_matchQueryChecked: (a: number, b: number, c: number, d: number, e: number) => [number, number, number];
38
40
  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_resetDistinctMatchState: (a: number) => void;
40
41
  export const wasmmatcherengine_new: () => number;
42
+ export const wasmmatcherengine_resetDistinctMatchState: (a: number) => void;
41
43
  export const init_matcher_wasm: () => void;
42
44
  export const detectPatternMark: (a: any, b: number, c: number) => [number, number, number];
43
45
  export const wasmmatcherengine_detectPatternMark: (a: number, b: any, c: number, d: number) => [number, number, number];
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;
@@ -115,6 +142,24 @@ interface MatchResult {
115
142
  durationMillis?: number;
116
143
  afpType: AudioFingerprintType;
117
144
  }
145
+ type TimingCoordinateSpace = "entry_local" | "segment_local" | "unavailable";
146
+ type TimingUnavailableReason = "rejected_metadata" | "invalidated_metadata" | "incompatible_geometry" | "ambiguous_clock" | "out_of_range" | "window_origin_underflow" | "inconsistent_windows";
147
+ interface MatchTiming {
148
+ coordinateSpace: TimingCoordinateSpace;
149
+ /** Null when timing normalization failed. */
150
+ positionMillis: number | null;
151
+ durationMillis: number | null;
152
+ segmentIndex: number | null;
153
+ reason: TimingUnavailableReason | null;
154
+ }
155
+ interface CheckedMatchResult {
156
+ name: string;
157
+ score: number;
158
+ afpType: AudioFingerprintType;
159
+ /** Diagnostic entry offset only; consumers should use timing. */
160
+ rawPositionMillis: number;
161
+ timing: MatchTiming;
162
+ }
118
163
  interface VariantMatch {
119
164
  matchResult: MatchResult;
120
165
  variant: VariantDecision;
@@ -246,11 +291,17 @@ declare class MatcherSession {
246
291
  clear(): Promise<void>;
247
292
  getEntryMetadata(name: string): Promise<EntryMetadata | null>;
248
293
  match(query: Uint8Array, config?: MatchConfig): Promise<MatchResult[]>;
294
+ /**
295
+ * Read one observation with explicit timing coordinates. This direct accessor
296
+ * does not emit legacy match events or update recognition deduplication state.
297
+ */
298
+ bestMatchChecked(query: Uint8Array, config?: MatchConfig): Promise<CheckedMatchResult | null>;
249
299
  bestMatch(query: Uint8Array, config?: MatchConfig): Promise<MatchResult | null>;
250
300
  estimatePitchRatio(pcm16Le: Uint8Array, sampleRate: number): Promise<PitchRatioEstimate>;
251
301
  extractPitchReference(pcm16Le: Uint8Array, sampleRate: number): Promise<PitchReferenceEstimate>;
252
302
  estimatePitchRatioAgainstReference(pcm16Le: Uint8Array, sampleRate: number, referencePitchHz: number, referenceConfidence?: number | null): Promise<PitchRatioEstimate>;
253
303
  estimatePitchRatioAgainstReferenceFeatures(pcm16Le: Uint8Array, sampleRate: number, referenceFeatures: string, referencePositionMillis: number, referenceConfidence?: number | null): Promise<PitchRatioEstimate>;
304
+ /** Uses level-robust detection by default, preserving explicit legacy thresholds. */
254
305
  detectAudioMarker(pcm16Le: Uint8Array, sampleRate: number, config?: AudioMarkerConfig): Promise<AudioMarkerDetection>;
255
306
  /** Detect an audio marker with explicit limits for trusted offline input. */
256
307
  detectAudioMarkerWithLimits(pcm16Le: Uint8Array, sampleRate: number, config: AudioMarkerConfig | undefined, limits: AudioMarkerLimits): Promise<AudioMarkerDetection>;
@@ -276,4 +327,4 @@ declare function __resetMatcherWasmForTests(): void;
276
327
  /** Maximum gap between successful observations in one continuous material run. */
277
328
  declare const MATCH_CONTINUITY_GAP_MS = 30000;
278
329
 
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 };
330
+ 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, type CheckedMatchResult, 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 MatchTiming, type MatcherEphemeralAuthOptions, type MatcherEphemeralAuthResult, type MatcherEphemeralKeyExchangeMetadata, type MatcherEphemeralKeyExchangeOptions, MatcherSession, type MatcherSessionEventMap, type MatcherSessionEventName, type MatcherSessionListener, type MatcherSessionOptions, type PackSource, type PitchRatioEstimate, type PitchReferenceEstimate, type TimingCoordinateSpace, type TimingUnavailableReason, type VariantDecision, type VariantDecisionStatus, type VariantMatch, type VariantMatchConfig, type VariantMatchQuery, __resetMatcherWasmForTests, __setMatcherEmbeddedPackForTests, __setMatcherWasmModuleForTests, initMatcher };
package/dist/index.js CHANGED
@@ -478,6 +478,28 @@ var WrappedDatabase = class {
478
478
  var WrappedMatcher = class {
479
479
  constructor(inner) {
480
480
  this.inner = inner;
481
+ const findMatchesCheckedFn = getCallable(this.inner, [
482
+ "findMatchesChecked",
483
+ "find_matches_checked",
484
+ "matchQueryChecked",
485
+ "match_query_checked"
486
+ ]);
487
+ if (findMatchesCheckedFn) {
488
+ this.findMatchesChecked = (database, query, config) => {
489
+ const dbRecord = typeof database.raw === "function" ? database.raw() : database;
490
+ const raw = findMatchesCheckedFn.call(this.inner, dbRecord, query, config);
491
+ if (!Array.isArray(raw)) throw new Error("Invalid checked match results");
492
+ return raw.map(normalizeCheckedResult);
493
+ };
494
+ }
495
+ const bestMatchCheckedFn = getCallable(this.inner, ["bestMatchChecked", "best_match_checked"]);
496
+ if (bestMatchCheckedFn) {
497
+ this.bestMatchChecked = (database, query, config) => {
498
+ const dbRecord = typeof database.raw === "function" ? database.raw() : database;
499
+ const raw = bestMatchCheckedFn.call(this.inner, dbRecord, query, config);
500
+ return raw == null ? null : normalizeCheckedResult(raw);
501
+ };
502
+ }
481
503
  const estimatePitchRatioFn = getCallable(this.inner, [
482
504
  "estimatePitchRatio",
483
505
  "estimate_pitch_ratio"
@@ -555,6 +577,8 @@ var WrappedMatcher = class {
555
577
  }
556
578
  }
557
579
  inner;
580
+ findMatchesChecked;
581
+ bestMatchChecked;
558
582
  estimatePitchRatio;
559
583
  extractPitchReference;
560
584
  estimatePitchRatioAgainstReference;
@@ -645,6 +669,56 @@ function normalizeResult(raw) {
645
669
  }
646
670
  return result;
647
671
  }
672
+ var TIMING_UNAVAILABLE_REASONS = /* @__PURE__ */ new Set([
673
+ "rejected_metadata",
674
+ "invalidated_metadata",
675
+ "incompatible_geometry",
676
+ "ambiguous_clock",
677
+ "out_of_range",
678
+ "window_origin_underflow",
679
+ "inconsistent_windows"
680
+ ]);
681
+ function normalizeCheckedResult(raw) {
682
+ const invalid = () => new Error("Invalid checked match result");
683
+ if (!raw || typeof raw !== "object") {
684
+ throw invalid();
685
+ }
686
+ const record = raw;
687
+ if (!record.timing || typeof record.timing !== "object") {
688
+ throw invalid();
689
+ }
690
+ const source = record.timing;
691
+ const integer = (camel, snake) => {
692
+ const value = camel ?? snake;
693
+ if (value == null) return null;
694
+ if (typeof value !== "number" || !Number.isSafeInteger(value) || value < 0) {
695
+ throw invalid();
696
+ }
697
+ return value;
698
+ };
699
+ const coordinateSpace = source.coordinateSpace ?? source.coordinate_space;
700
+ const timing = {
701
+ coordinateSpace,
702
+ positionMillis: integer(source.positionMillis, source.position_millis),
703
+ durationMillis: integer(source.durationMillis, source.duration_millis),
704
+ segmentIndex: integer(source.segmentIndex, source.segment_index),
705
+ reason: source.reason ?? null
706
+ };
707
+ const available = timing.reason === null && timing.positionMillis !== null;
708
+ const validTiming = coordinateSpace === "unavailable" ? timing.positionMillis === null && timing.durationMillis === null && timing.segmentIndex === null && TIMING_UNAVAILABLE_REASONS.has(timing.reason) : coordinateSpace === "entry_local" ? available && timing.durationMillis === null && timing.segmentIndex === null : coordinateSpace === "segment_local" && available && timing.durationMillis !== null && timing.segmentIndex !== null && timing.positionMillis < timing.durationMillis;
709
+ const rawPositionMillis = integer(record.rawPositionMillis, record.raw_position_millis);
710
+ const afpType = record.afpType ?? record.afp_type;
711
+ if (!validTiming || typeof record.name !== "string" || record.name.length === 0 || typeof record.score !== "number" || !Number.isFinite(record.score) || record.score < 0 || ![1, 2, 4].includes(afpType) || rawPositionMillis === null) {
712
+ throw invalid();
713
+ }
714
+ return {
715
+ name: record.name,
716
+ score: record.score,
717
+ afpType,
718
+ rawPositionMillis,
719
+ timing
720
+ };
721
+ }
648
722
  function normalizeVariantMatch(raw) {
649
723
  const rawMatch = raw.matchResult ?? raw.match;
650
724
  const rawVariant = raw.variant ?? {};
@@ -715,6 +789,19 @@ function normalizeAudioMarkerDetection(raw) {
715
789
  runnerUpAccuracy: normalizeOptionalNumber(record.runnerUpAccuracy, record.runner_up_accuracy)
716
790
  };
717
791
  }
792
+ function normalizeLevelRobustDetection(raw) {
793
+ const envelope = raw;
794
+ if (!envelope || typeof envelope.detection !== "object" || !envelope.detection || typeof envelope.diagnostics !== "object" || !envelope.diagnostics || typeof envelope.candidateEligible !== "boolean" || typeof envelope.materialFallbackGuardEligible !== "boolean") {
795
+ throw new Error("Invalid level-robust marker result envelope");
796
+ }
797
+ return {
798
+ ...normalizeAudioMarkerDetection(envelope.detection),
799
+ levelPolicy: "levelRobust",
800
+ diagnostics: { ...envelope.diagnostics },
801
+ candidateEligible: envelope.candidateEligible,
802
+ materialFallbackGuardEligible: envelope.materialFallbackGuardEligible
803
+ };
804
+ }
718
805
  function isMapLike(raw) {
719
806
  return raw instanceof Map || Object.prototype.toString.call(raw) === "[object Map]" && typeof raw.entries === "function";
720
807
  }
@@ -782,6 +869,10 @@ function createBindingsFromModule(moduleLike) {
782
869
  "estimatePitchRatioAgainstReferenceFeatures",
783
870
  "estimate_pitch_ratio_against_reference_features"
784
871
  ]);
872
+ const detectAudioMarkerLevelRobustFn = resolveFactory(mod, [
873
+ "detectAudioMarkerLevelRobust",
874
+ "detect_audio_marker_level_robust"
875
+ ]);
785
876
  const detectAudioMarkerFn = resolveFactory(mod, [
786
877
  "detectAudioMarker",
787
878
  "detect_audio_marker",
@@ -926,6 +1017,26 @@ function createBindingsFromModule(moduleLike) {
926
1017
  destroy?.call(matcher);
927
1018
  }
928
1019
  },
1020
+ detectAudioMarkerLevelRobust(pcm16Le, sampleRate, config, limits) {
1021
+ if (detectAudioMarkerLevelRobustFn) {
1022
+ return normalizeLevelRobustDetection(
1023
+ detectAudioMarkerLevelRobustFn(pcm16Le, sampleRate, config, limits)
1024
+ );
1025
+ }
1026
+ const matcher = createMatcherFactory ? createMatcherFactory() : new MatcherCtor();
1027
+ try {
1028
+ const fn = getCallable(matcher, [
1029
+ "detectAudioMarkerLevelRobust",
1030
+ "detect_audio_marker_level_robust"
1031
+ ]);
1032
+ if (!fn) {
1033
+ throw new Error("WASM matcher is missing detectAudioMarkerLevelRobust; upgrade the WASM assets or explicitly select levelPolicy: legacy");
1034
+ }
1035
+ return normalizeLevelRobustDetection(fn.call(matcher, pcm16Le, sampleRate, config, limits));
1036
+ } finally {
1037
+ getCallable(matcher, ["destroy", "free"])?.call(matcher);
1038
+ }
1039
+ },
929
1040
  detectAudioMarkerWithLimits(pcm16Le, sampleRate, config, limits) {
930
1041
  if (detectAudioMarkerWithLimitsFn) {
931
1042
  return normalizeAudioMarkerDetection(
@@ -970,6 +1081,42 @@ async function getBindings(options) {
970
1081
  function toUint8ArrayCopy(input) {
971
1082
  return new Uint8Array(input);
972
1083
  }
1084
+ var typedArrayPrototype = Object.getPrototypeOf(Uint8Array.prototype);
1085
+ var typedArrayByteLength = Object.getOwnPropertyDescriptor(typedArrayPrototype, "byteLength").get;
1086
+ var typedArrayTag = Object.getOwnPropertyDescriptor(typedArrayPrototype, Symbol.toStringTag).get;
1087
+ var typedArraySet = Uint8Array.prototype.set;
1088
+ function markerByteLength(input) {
1089
+ if (typedArrayTag.call(input) !== "Uint8Array") {
1090
+ throw new TypeError("Audio marker PCM must be a Uint8Array");
1091
+ }
1092
+ return typedArrayByteLength.call(input);
1093
+ }
1094
+ function copyMarkerPcm(input) {
1095
+ const copy = new Uint8Array(markerByteLength(input));
1096
+ typedArraySet.call(copy, input);
1097
+ return copy;
1098
+ }
1099
+ function usesLevelRobust(config) {
1100
+ if (config?.levelPolicy !== void 0) {
1101
+ if (config.levelPolicy !== "levelRobust" && config.levelPolicy !== "legacy") {
1102
+ throw new RangeError("Invalid audio marker levelPolicy");
1103
+ }
1104
+ return config.levelPolicy === "levelRobust";
1105
+ }
1106
+ return ![
1107
+ config?.minSyncScore,
1108
+ config?.minScore,
1109
+ config?.minMargin,
1110
+ config?.minRatio,
1111
+ config?.minConfidence,
1112
+ config?.minVotes
1113
+ ].some((value) => value !== void 0);
1114
+ }
1115
+ function detectorConfig(config) {
1116
+ if (!config || config.levelPolicy === void 0) return config;
1117
+ const { levelPolicy: _policy, ...rest } = config;
1118
+ return rest;
1119
+ }
973
1120
  function resolveAudioMarkerLimits(limits) {
974
1121
  const resolved = {
975
1122
  maxPcmBytes: limits?.maxPcmBytes ?? DEFAULT_MAX_AUDIO_MARKER_PCM_BYTES,
@@ -985,13 +1132,17 @@ function resolveAudioMarkerLimits(limits) {
985
1132
  return resolved;
986
1133
  }
987
1134
  function audioMarkerInputFailureReason(pcm16Le, sampleRate, limits) {
988
- if (pcm16Le.byteLength > limits.maxPcmBytes) {
1135
+ const byteLength = markerByteLength(pcm16Le);
1136
+ if (byteLength > limits.maxPcmBytes) {
989
1137
  return AUDIO_MARKER_INPUT_BUDGET_EXCEEDED_REASON;
990
1138
  }
991
- if (pcm16Le.byteLength % 2 !== 0) {
1139
+ if (byteLength % 2 !== 0) {
992
1140
  return "invalid_pcm16_byte_length";
993
1141
  }
994
- const sampleCount = pcm16Le.byteLength / 2;
1142
+ if (!Number.isInteger(sampleRate) || sampleRate <= 0 || sampleRate > 4294967295) {
1143
+ return "invalid_sample_rate";
1144
+ }
1145
+ const sampleCount = byteLength / 2;
995
1146
  if (sampleCount > limits.maxSamples) {
996
1147
  return AUDIO_MARKER_INPUT_BUDGET_EXCEEDED_REASON;
997
1148
  }
@@ -1188,6 +1339,26 @@ var MatcherSession = class {
1188
1339
  handleError(this.events, "match", error);
1189
1340
  }
1190
1341
  }
1342
+ /**
1343
+ * Read one observation with explicit timing coordinates. This direct accessor
1344
+ * does not emit legacy match events or update recognition deduplication state.
1345
+ */
1346
+ async bestMatchChecked(query, config) {
1347
+ const generation = this.recognitionGeneration;
1348
+ try {
1349
+ await this.ensureLiveReady();
1350
+ if (generation !== this.recognitionGeneration) return null;
1351
+ const safeQuery = toUint8ArrayCopy(query);
1352
+ const matcher = this.matcherOrThrow();
1353
+ if (!matcher.bestMatchChecked) {
1354
+ throw new Error("WASM matcher is missing bestMatchChecked; upgrade the WASM assets");
1355
+ }
1356
+ if (generation !== this.recognitionGeneration || this.destroyed) return null;
1357
+ return matcher.bestMatchChecked(this.databaseOrThrow(), safeQuery, config);
1358
+ } catch (error) {
1359
+ handleError(this.events, "bestMatchChecked", error);
1360
+ }
1361
+ }
1191
1362
  async bestMatch(query, config) {
1192
1363
  const generation = this.recognitionGeneration;
1193
1364
  try {
@@ -1293,15 +1464,24 @@ var MatcherSession = class {
1293
1464
  handleError(this.events, "estimatePitchRatioAgainstReferenceFeatures", error);
1294
1465
  }
1295
1466
  }
1467
+ /** Uses level-robust detection by default, preserving explicit legacy thresholds. */
1296
1468
  async detectAudioMarker(pcm16Le, sampleRate, config) {
1297
1469
  try {
1298
1470
  await this.ensureLiveReady();
1471
+ const robust = usesLevelRobust(config);
1299
1472
  const limits = resolveAudioMarkerLimits();
1300
1473
  const failureReason = audioMarkerInputFailureReason(pcm16Le, sampleRate, limits);
1301
1474
  if (failureReason) {
1302
- return absentAudioMarkerDetection(config, failureReason);
1475
+ return {
1476
+ ...absentAudioMarkerDetection(config, failureReason),
1477
+ ...robust ? { levelPolicy: "levelRobust", candidateEligible: false, materialFallbackGuardEligible: false } : {}
1478
+ };
1479
+ }
1480
+ const safePcm = copyMarkerPcm(pcm16Le);
1481
+ config = detectorConfig(config);
1482
+ if (robust) {
1483
+ return this.bindings.detectAudioMarkerLevelRobust(safePcm, sampleRate, config);
1303
1484
  }
1304
- const safePcm = toUint8ArrayCopy(pcm16Le);
1305
1485
  const matcher = this.matcherOrThrow();
1306
1486
  if (matcher.detectAudioMarker) {
1307
1487
  return matcher.detectAudioMarker(safePcm, sampleRate, config);
@@ -1318,6 +1498,7 @@ var MatcherSession = class {
1318
1498
  async detectAudioMarkerWithLimits(pcm16Le, sampleRate, config, limits) {
1319
1499
  try {
1320
1500
  await this.ensureLiveReady();
1501
+ const robust = usesLevelRobust(config);
1321
1502
  const resolvedLimits = resolveAudioMarkerLimits(limits);
1322
1503
  const failureReason = audioMarkerInputFailureReason(
1323
1504
  pcm16Le,
@@ -1325,9 +1506,16 @@ var MatcherSession = class {
1325
1506
  resolvedLimits
1326
1507
  );
1327
1508
  if (failureReason) {
1328
- return absentAudioMarkerDetection(config, failureReason);
1509
+ return {
1510
+ ...absentAudioMarkerDetection(config, failureReason),
1511
+ ...robust ? { levelPolicy: "levelRobust", candidateEligible: false, materialFallbackGuardEligible: false } : {}
1512
+ };
1513
+ }
1514
+ const safePcm = copyMarkerPcm(pcm16Le);
1515
+ config = detectorConfig(config);
1516
+ if (robust) {
1517
+ return this.bindings.detectAudioMarkerLevelRobust(safePcm, sampleRate, config, resolvedLimits);
1329
1518
  }
1330
- const safePcm = toUint8ArrayCopy(pcm16Le);
1331
1519
  const matcher = this.matcherOrThrow();
1332
1520
  if (matcher.detectAudioMarkerWithLimits) {
1333
1521
  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.11",
4
4
  "description": "Browser WASM SDK for audio fingerprint matching",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",