@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 +41 -1
- package/dist/generated/core.d.ts +2 -0
- package/dist/generated/core_bg.js +32 -0
- package/dist/generated/core_bg.wasm +0 -0
- package/dist/generated/core_bg.wasm.d.ts +3 -1
- package/dist/index.d.ts +52 -1
- package/dist/index.js +195 -7
- package/package.json +1 -1
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
|
|
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/generated/core.d.ts
CHANGED
|
@@ -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
|
-
|
|
1135
|
+
const byteLength = markerByteLength(pcm16Le);
|
|
1136
|
+
if (byteLength > limits.maxPcmBytes) {
|
|
989
1137
|
return AUDIO_MARKER_INPUT_BUDGET_EXCEEDED_REASON;
|
|
990
1138
|
}
|
|
991
|
-
if (
|
|
1139
|
+
if (byteLength % 2 !== 0) {
|
|
992
1140
|
return "invalid_pcm16_byte_length";
|
|
993
1141
|
}
|
|
994
|
-
|
|
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
|
|
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
|
|
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(
|