@octoseq/mir 0.1.0-main.0d2814e
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/dist/chunk-DUWYCAVG.js +1525 -0
- package/dist/chunk-DUWYCAVG.js.map +1 -0
- package/dist/index.d.ts +450 -0
- package/dist/index.js +1234 -0
- package/dist/index.js.map +1 -0
- package/dist/runMir-CSIBwNZ3.d.ts +84 -0
- package/dist/runner/runMir.d.ts +2 -0
- package/dist/runner/runMir.js +3 -0
- package/dist/runner/runMir.js.map +1 -0
- package/dist/runner/workerProtocol.d.ts +169 -0
- package/dist/runner/workerProtocol.js +11 -0
- package/dist/runner/workerProtocol.js.map +1 -0
- package/dist/types-BE3py4fZ.d.ts +83 -0
- package/package.json +55 -0
- package/src/dsp/fft.ts +22 -0
- package/src/dsp/fftBackend.ts +53 -0
- package/src/dsp/fftBackendFftjs.ts +60 -0
- package/src/dsp/hpss.ts +152 -0
- package/src/dsp/hpssGpu.ts +101 -0
- package/src/dsp/mel.ts +219 -0
- package/src/dsp/mfcc.ts +119 -0
- package/src/dsp/onset.ts +205 -0
- package/src/dsp/peakPick.ts +112 -0
- package/src/dsp/spectral.ts +95 -0
- package/src/dsp/spectrogram.ts +176 -0
- package/src/gpu/README.md +34 -0
- package/src/gpu/context.ts +44 -0
- package/src/gpu/helpers.ts +87 -0
- package/src/gpu/hpssMasks.ts +116 -0
- package/src/gpu/kernels/hpssMasks.wgsl.ts +137 -0
- package/src/gpu/kernels/melProject.wgsl.ts +48 -0
- package/src/gpu/kernels/onsetEnvelope.wgsl.ts +56 -0
- package/src/gpu/melProject.ts +98 -0
- package/src/gpu/onsetEnvelope.ts +81 -0
- package/src/gpu/webgpu.d.ts +176 -0
- package/src/index.ts +121 -0
- package/src/runner/runMir.ts +431 -0
- package/src/runner/workerProtocol.ts +189 -0
- package/src/search/featureVectorV1.ts +123 -0
- package/src/search/fingerprintV1.ts +230 -0
- package/src/search/refinedModelV1.ts +321 -0
- package/src/search/searchTrackV1.ts +206 -0
- package/src/search/searchTrackV1Guided.ts +863 -0
- package/src/search/similarity.ts +98 -0
- package/src/types.ts +105 -0
- package/src/util/display.ts +80 -0
- package/src/util/normalise.ts +58 -0
- package/src/util/stats.ts +25 -0
|
@@ -0,0 +1,863 @@
|
|
|
1
|
+
import type { MelSpectrogram } from "../dsp/mel";
|
|
2
|
+
import type { Features2D } from "../dsp/mfcc";
|
|
3
|
+
import { peakPick } from "../dsp/peakPick";
|
|
4
|
+
|
|
5
|
+
import type { MirSearchCandidate, MirSearchOptionsV1 } from "./searchTrackV1";
|
|
6
|
+
import { makeFeatureVectorLayoutV1 } from "./featureVectorV1";
|
|
7
|
+
import {
|
|
8
|
+
buildPrototypeModelV1,
|
|
9
|
+
logitContributionsByGroupV1,
|
|
10
|
+
scoreWithModelV1,
|
|
11
|
+
trainLogisticModelV1,
|
|
12
|
+
type MirLogitContributionsByGroupV1,
|
|
13
|
+
type MirRefinedModelExplainV1,
|
|
14
|
+
type MirRefinedModelKindV1,
|
|
15
|
+
} from "./refinedModelV1";
|
|
16
|
+
|
|
17
|
+
export type MirRefinementCandidateLabelV1 = {
|
|
18
|
+
t0: number;
|
|
19
|
+
t1: number;
|
|
20
|
+
status: "accepted" | "rejected";
|
|
21
|
+
source: "auto" | "manual";
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
export type MirSearchGuidedOptionsV1 = MirSearchOptionsV1 & {
|
|
25
|
+
/**
|
|
26
|
+
* Local contrast features: foreground (query-length) vs surrounding background.
|
|
27
|
+
* Enabled by default because it improves discrimination in dense mixes.
|
|
28
|
+
*/
|
|
29
|
+
localContrast?: {
|
|
30
|
+
enabled?: boolean;
|
|
31
|
+
/** Background duration multiplier relative to the foreground. Default 3. */
|
|
32
|
+
backgroundScale?: number;
|
|
33
|
+
};
|
|
34
|
+
refinement?: {
|
|
35
|
+
enabled?: boolean;
|
|
36
|
+
/**
|
|
37
|
+
* Human labels (accepted/rejected). Unreviewed candidates should not be sent.
|
|
38
|
+
*/
|
|
39
|
+
labels?: MirRefinementCandidateLabelV1[];
|
|
40
|
+
/** Optional: include the query as an extra positive exemplar once enough positives exist. */
|
|
41
|
+
includeQueryAsPositive?: boolean;
|
|
42
|
+
};
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
export type MirSearchCurveKindV1 = "similarity" | "confidence";
|
|
46
|
+
|
|
47
|
+
export type MirGuidedCandidateExplainV1 = {
|
|
48
|
+
/** Only present for logistic models; values are in logit space (sum + bias = total logit). */
|
|
49
|
+
groupLogit?: MirLogitContributionsByGroupV1;
|
|
50
|
+
};
|
|
51
|
+
|
|
52
|
+
export type MirSearchCandidateV1Guided = MirSearchCandidate & {
|
|
53
|
+
explain?: MirGuidedCandidateExplainV1;
|
|
54
|
+
};
|
|
55
|
+
|
|
56
|
+
export type MirSearchResultV1Guided = {
|
|
57
|
+
times: Float32Array; // window start times
|
|
58
|
+
scores: Float32Array; // [0,1] similarity or confidence
|
|
59
|
+
candidates: MirSearchCandidateV1Guided[];
|
|
60
|
+
curveKind: MirSearchCurveKindV1;
|
|
61
|
+
model: MirRefinedModelExplainV1;
|
|
62
|
+
meta: {
|
|
63
|
+
/** Feature prep time (legacy name retained for UI compatibility). */
|
|
64
|
+
fingerprintMs: number;
|
|
65
|
+
scanMs: number;
|
|
66
|
+
modelMs: number;
|
|
67
|
+
totalMs: number;
|
|
68
|
+
windowSec: number;
|
|
69
|
+
hopSec: number;
|
|
70
|
+
skippedWindows: number;
|
|
71
|
+
scannedWindows: number;
|
|
72
|
+
};
|
|
73
|
+
};
|
|
74
|
+
|
|
75
|
+
function nowMs(): number {
|
|
76
|
+
return typeof performance !== "undefined" ? performance.now() : Date.now();
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
function clamp01(x: number): number {
|
|
80
|
+
if (x <= 0) return 0;
|
|
81
|
+
if (x >= 1) return 1;
|
|
82
|
+
return x;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
function zScoreInPlace(out: Float32Array, mean: Float32Array, invStd: Float32Array): void {
|
|
86
|
+
const n = Math.min(out.length, mean.length, invStd.length);
|
|
87
|
+
for (let j = 0; j < n; j++) out[j] = ((out[j] ?? 0) - (mean[j] ?? 0)) * (invStd[j] ?? 1);
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
function decideModelKind(params: {
|
|
91
|
+
enabled: boolean;
|
|
92
|
+
labels: MirRefinementCandidateLabelV1[];
|
|
93
|
+
}): { kind: MirRefinedModelKindV1; positives: MirRefinementCandidateLabelV1[]; negatives: MirRefinementCandidateLabelV1[] } {
|
|
94
|
+
if (!params.enabled) return { kind: "baseline", positives: [], negatives: [] };
|
|
95
|
+
|
|
96
|
+
const positives = params.labels.filter((l) => l.status === "accepted");
|
|
97
|
+
const negatives = params.labels.filter((l) => l.status === "rejected");
|
|
98
|
+
|
|
99
|
+
// Training rule: fewer than 2 positive examples => skip training.
|
|
100
|
+
if (positives.length < 2) return { kind: "baseline", positives, negatives };
|
|
101
|
+
if (negatives.length === 0) return { kind: "prototype", positives, negatives };
|
|
102
|
+
return { kind: "logistic", positives, negatives };
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
function advanceStartIndex(times: Float32Array, start: number, t0: number): number {
|
|
106
|
+
let i = start;
|
|
107
|
+
while (i < times.length && (times[i] ?? 0) < t0) i++;
|
|
108
|
+
return i;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
function advanceEndIndex(times: Float32Array, endExclusive: number, t1: number): number {
|
|
112
|
+
let i = endExclusive;
|
|
113
|
+
while (i < times.length && (times[i] ?? 0) <= t1) i++;
|
|
114
|
+
return i;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
function cosineSimilarity01ByBlocks(
|
|
118
|
+
query: Float32Array,
|
|
119
|
+
window: Float32Array,
|
|
120
|
+
layout: ReturnType<typeof makeFeatureVectorLayoutV1>,
|
|
121
|
+
weights: { mel?: number; transient?: number; mfcc?: number } | undefined
|
|
122
|
+
): number {
|
|
123
|
+
const wMel = weights?.mel ?? 1;
|
|
124
|
+
const wTrans = weights?.transient ?? 1;
|
|
125
|
+
const wMfcc = weights?.mfcc ?? 1;
|
|
126
|
+
|
|
127
|
+
const addBlock = (offset: number, length: number, weight: number, acc: { dot: number; aa: number; bb: number }) => {
|
|
128
|
+
if (length <= 0 || weight === 0) return;
|
|
129
|
+
|
|
130
|
+
const ww = weight * weight;
|
|
131
|
+
const eps = 1e-12;
|
|
132
|
+
|
|
133
|
+
// Compute dot + norms for this block in one pass; then normalise to match
|
|
134
|
+
// fingerprintToVectorV1(): L2-normalise each block independently, then weight.
|
|
135
|
+
let dotRaw = 0;
|
|
136
|
+
let qSumSq = 0;
|
|
137
|
+
let xSumSq = 0;
|
|
138
|
+
const end = Math.min(query.length, window.length, offset + length);
|
|
139
|
+
for (let i = offset; i < end; i++) {
|
|
140
|
+
const q = query[i] ?? 0;
|
|
141
|
+
const x = window[i] ?? 0;
|
|
142
|
+
dotRaw += q * x;
|
|
143
|
+
qSumSq += q * q;
|
|
144
|
+
xSumSq += x * x;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
const qNorm = Math.sqrt(qSumSq);
|
|
148
|
+
const xNorm = Math.sqrt(xSumSq);
|
|
149
|
+
|
|
150
|
+
if (qNorm > eps) acc.aa += ww;
|
|
151
|
+
if (xNorm > eps) acc.bb += ww;
|
|
152
|
+
if (!(qNorm > eps && xNorm > eps)) return;
|
|
153
|
+
|
|
154
|
+
acc.dot += ww * (dotRaw / (qNorm * xNorm));
|
|
155
|
+
};
|
|
156
|
+
|
|
157
|
+
const acc = { dot: 0, aa: 0, bb: 0 };
|
|
158
|
+
|
|
159
|
+
// Foreground blocks
|
|
160
|
+
addBlock(layout.melMeanFg.offset, layout.melMeanFg.length + layout.melVarianceFg.length, wMel, acc);
|
|
161
|
+
addBlock(layout.onsetFg.offset, layout.onsetFg.length, wTrans, acc);
|
|
162
|
+
if (layout.mfccMeanFg && layout.mfccVarianceFg) {
|
|
163
|
+
addBlock(layout.mfccMeanFg.offset, layout.mfccMeanFg.length + layout.mfccVarianceFg.length, wMfcc, acc);
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
// Contrast blocks (if present)
|
|
167
|
+
if (layout.melContrast) addBlock(layout.melContrast.offset, layout.melContrast.length, wMel, acc);
|
|
168
|
+
if (layout.onsetContrast) addBlock(layout.onsetContrast.offset, layout.onsetContrast.length, wTrans, acc);
|
|
169
|
+
if (layout.mfccMeanContrast && layout.mfccVarianceContrast) {
|
|
170
|
+
addBlock(layout.mfccMeanContrast.offset, layout.mfccMeanContrast.length + layout.mfccVarianceContrast.length, wMfcc, acc);
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
const denom = Math.sqrt(acc.aa) * Math.sqrt(acc.bb);
|
|
174
|
+
if (denom <= 0) return 0;
|
|
175
|
+
|
|
176
|
+
const cos = acc.dot / denom;
|
|
177
|
+
const clamped = Math.max(-1, Math.min(1, cos));
|
|
178
|
+
return (clamped + 1) / 2;
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
type SparseMaxQuery = {
|
|
182
|
+
query: (start: number, endExclusive: number) => number;
|
|
183
|
+
};
|
|
184
|
+
|
|
185
|
+
function buildSparseTableMax(values: Float32Array, isCancelled?: () => boolean): SparseMaxQuery {
|
|
186
|
+
const n = values.length;
|
|
187
|
+
const log = new Uint8Array(n + 1);
|
|
188
|
+
for (let i = 2; i <= n; i++) log[i] = ((log[i >>> 1] ?? 0) + 1) as number;
|
|
189
|
+
const maxK = log[n] ?? 0;
|
|
190
|
+
|
|
191
|
+
const table: Float32Array[] = [];
|
|
192
|
+
table[0] = values;
|
|
193
|
+
|
|
194
|
+
for (let k = 1; k <= maxK; k++) {
|
|
195
|
+
const span = 1 << k;
|
|
196
|
+
const half = span >>> 1;
|
|
197
|
+
const prev = table[k - 1]!;
|
|
198
|
+
const len = Math.max(0, n - span + 1);
|
|
199
|
+
const cur = new Float32Array(len);
|
|
200
|
+
for (let i = 0; i < len; i++) {
|
|
201
|
+
if ((i & 2047) === 0 && isCancelled?.()) throw new Error("@octoseq/mir: cancelled");
|
|
202
|
+
const a = prev[i] ?? 0;
|
|
203
|
+
const b = prev[i + half] ?? 0;
|
|
204
|
+
cur[i] = a > b ? a : b;
|
|
205
|
+
}
|
|
206
|
+
table[k] = cur;
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
return {
|
|
210
|
+
query: (start: number, endExclusive: number) => {
|
|
211
|
+
const l = Math.max(0, start | 0);
|
|
212
|
+
const r = Math.min(n, endExclusive | 0);
|
|
213
|
+
const len = r - l;
|
|
214
|
+
if (len <= 0) return -Infinity;
|
|
215
|
+
const k = log[len] ?? 0;
|
|
216
|
+
const span = 1 << k;
|
|
217
|
+
const row = table[k]!;
|
|
218
|
+
const a = row[l] ?? -Infinity;
|
|
219
|
+
const b = row[r - span] ?? -Infinity;
|
|
220
|
+
return a > b ? a : b;
|
|
221
|
+
},
|
|
222
|
+
};
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
function computeBackgroundWindow(params: {
|
|
226
|
+
fgStartSec: number;
|
|
227
|
+
fgEndSec: number;
|
|
228
|
+
trackDurationSec: number;
|
|
229
|
+
backgroundScale: number;
|
|
230
|
+
}): { bgStartSec: number; bgEndSec: number } {
|
|
231
|
+
const fgStartSec = Math.min(params.fgStartSec, params.fgEndSec);
|
|
232
|
+
const fgEndSec = Math.max(params.fgStartSec, params.fgEndSec);
|
|
233
|
+
const fgDur = Math.max(1e-6, fgEndSec - fgStartSec);
|
|
234
|
+
|
|
235
|
+
const desired = Math.max(fgDur, fgDur * Math.max(1, params.backgroundScale));
|
|
236
|
+
const maxDur = Math.max(fgDur, params.trackDurationSec);
|
|
237
|
+
const dur = Math.min(desired, maxDur);
|
|
238
|
+
|
|
239
|
+
const center = (fgStartSec + fgEndSec) / 2;
|
|
240
|
+
let bgStart = center - dur / 2;
|
|
241
|
+
let bgEnd = bgStart + dur;
|
|
242
|
+
|
|
243
|
+
// Preserve duration when possible by shifting the window instead of shrinking it.
|
|
244
|
+
if (bgStart < 0) {
|
|
245
|
+
bgStart = 0;
|
|
246
|
+
bgEnd = Math.min(params.trackDurationSec, dur);
|
|
247
|
+
}
|
|
248
|
+
if (bgEnd > params.trackDurationSec) {
|
|
249
|
+
bgEnd = params.trackDurationSec;
|
|
250
|
+
bgStart = Math.max(0, bgEnd - dur);
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
// Defensive: ensure the window contains the foreground.
|
|
254
|
+
bgStart = Math.min(bgStart, fgStartSec);
|
|
255
|
+
bgEnd = Math.max(bgEnd, fgEndSec);
|
|
256
|
+
|
|
257
|
+
return { bgStartSec: bgStart, bgEndSec: bgEnd };
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
class SlidingMoments {
|
|
261
|
+
private start = 0;
|
|
262
|
+
private end = 0;
|
|
263
|
+
readonly sum: Float64Array;
|
|
264
|
+
readonly sumSq: Float64Array;
|
|
265
|
+
|
|
266
|
+
constructor(
|
|
267
|
+
private readonly dim: number,
|
|
268
|
+
private readonly addFrame: (frame: number, sum: Float64Array, sumSq: Float64Array) => void,
|
|
269
|
+
private readonly removeFrame: (frame: number, sum: Float64Array, sumSq: Float64Array) => void
|
|
270
|
+
) {
|
|
271
|
+
this.sum = new Float64Array(dim);
|
|
272
|
+
this.sumSq = new Float64Array(dim);
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
update(newStart: number, newEnd: number) {
|
|
276
|
+
const s = Math.max(0, newStart | 0);
|
|
277
|
+
const e = Math.max(s, newEnd | 0);
|
|
278
|
+
|
|
279
|
+
while (this.end < e) {
|
|
280
|
+
this.addFrame(this.end, this.sum, this.sumSq);
|
|
281
|
+
this.end++;
|
|
282
|
+
}
|
|
283
|
+
while (this.start < s) {
|
|
284
|
+
this.removeFrame(this.start, this.sum, this.sumSq);
|
|
285
|
+
this.start++;
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
// This class assumes monotonic windows (start/end only move forward).
|
|
289
|
+
// If that assumption is violated, reset deterministically.
|
|
290
|
+
if (this.start > s || this.end > e) {
|
|
291
|
+
this.sum.fill(0);
|
|
292
|
+
this.sumSq.fill(0);
|
|
293
|
+
this.start = s;
|
|
294
|
+
this.end = s;
|
|
295
|
+
while (this.end < e) {
|
|
296
|
+
this.addFrame(this.end, this.sum, this.sumSq);
|
|
297
|
+
this.end++;
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
export async function searchTrackV1Guided(params: {
|
|
304
|
+
queryRegion: { t0: number; t1: number };
|
|
305
|
+
|
|
306
|
+
mel: MelSpectrogram;
|
|
307
|
+
onsetEnvelope: { times: Float32Array; values: Float32Array };
|
|
308
|
+
mfcc?: Features2D;
|
|
309
|
+
|
|
310
|
+
options?: MirSearchGuidedOptionsV1;
|
|
311
|
+
}): Promise<MirSearchResultV1Guided> {
|
|
312
|
+
const tStart = nowMs();
|
|
313
|
+
|
|
314
|
+
const options = params.options ?? {};
|
|
315
|
+
const hopSec = Math.max(0.005, options.hopSec ?? 0.03);
|
|
316
|
+
const threshold = clamp01(options.threshold ?? 0.75);
|
|
317
|
+
const localContrastEnabled = options.localContrast?.enabled ?? true;
|
|
318
|
+
const backgroundScale = Math.max(1, options.localContrast?.backgroundScale ?? 3);
|
|
319
|
+
|
|
320
|
+
const qt0 = Math.min(params.queryRegion.t0, params.queryRegion.t1);
|
|
321
|
+
const qt1 = Math.max(params.queryRegion.t0, params.queryRegion.t1);
|
|
322
|
+
const windowSec = Math.max(1e-3, qt1 - qt0);
|
|
323
|
+
|
|
324
|
+
const minSpacingSec = Math.max(0, options.minCandidateSpacingSec ?? windowSec * 0.8);
|
|
325
|
+
|
|
326
|
+
const refinementEnabled = !!options.refinement?.enabled;
|
|
327
|
+
const refinementLabels = options.refinement?.labels ?? [];
|
|
328
|
+
const includeQueryAsPositive = options.refinement?.includeQueryAsPositive ?? true;
|
|
329
|
+
|
|
330
|
+
const modelDecision = decideModelKind({ enabled: refinementEnabled, labels: refinementLabels });
|
|
331
|
+
const baselineExplain: MirRefinedModelExplainV1 = refinementEnabled
|
|
332
|
+
? {
|
|
333
|
+
kind: "baseline",
|
|
334
|
+
positives: modelDecision.positives.length,
|
|
335
|
+
negatives: modelDecision.negatives.length,
|
|
336
|
+
}
|
|
337
|
+
: { kind: "baseline", positives: 0, negatives: 0 };
|
|
338
|
+
|
|
339
|
+
const tPrep0 = nowMs();
|
|
340
|
+
|
|
341
|
+
const timesFrames = params.mel.times;
|
|
342
|
+
const nFrames = timesFrames.length;
|
|
343
|
+
|
|
344
|
+
const trackDuration = Math.max(
|
|
345
|
+
nFrames ? (timesFrames[nFrames - 1] ?? 0) : 0,
|
|
346
|
+
params.onsetEnvelope.times.length ? (params.onsetEnvelope.times[params.onsetEnvelope.times.length - 1] ?? 0) : 0
|
|
347
|
+
);
|
|
348
|
+
|
|
349
|
+
const nWindows = Math.max(0, Math.floor((trackDuration - windowSec) / hopSec) + 1);
|
|
350
|
+
const times = new Float32Array(nWindows);
|
|
351
|
+
const scores = new Float32Array(nWindows);
|
|
352
|
+
|
|
353
|
+
if (nWindows === 0) {
|
|
354
|
+
const totalMs = nowMs() - tStart;
|
|
355
|
+
return {
|
|
356
|
+
times,
|
|
357
|
+
scores,
|
|
358
|
+
candidates: [],
|
|
359
|
+
curveKind: "similarity",
|
|
360
|
+
model: baselineExplain,
|
|
361
|
+
meta: {
|
|
362
|
+
fingerprintMs: 0,
|
|
363
|
+
scanMs: 0,
|
|
364
|
+
modelMs: 0,
|
|
365
|
+
totalMs,
|
|
366
|
+
windowSec,
|
|
367
|
+
hopSec,
|
|
368
|
+
skippedWindows: 0,
|
|
369
|
+
scannedWindows: 0,
|
|
370
|
+
},
|
|
371
|
+
};
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
const skipOverlap = options.skipWindowOverlap;
|
|
375
|
+
const shouldSkip = (t0: number, t1: number) => {
|
|
376
|
+
if (!skipOverlap) return false;
|
|
377
|
+
const s0 = skipOverlap.t0;
|
|
378
|
+
const s1 = skipOverlap.t1;
|
|
379
|
+
return t0 < s1 && t1 > s0;
|
|
380
|
+
};
|
|
381
|
+
|
|
382
|
+
const melDim = params.mel.melBands[0]?.length ?? 0;
|
|
383
|
+
const mfccFullDim = params.mfcc?.values[0]?.length ?? 0;
|
|
384
|
+
const mfccDim = Math.max(0, Math.min(12, mfccFullDim - 1)); // coeffs 1..12
|
|
385
|
+
|
|
386
|
+
const layout = makeFeatureVectorLayoutV1({ melDim, mfccDim, includeContrast: localContrastEnabled });
|
|
387
|
+
|
|
388
|
+
// --- precompute per-frame normalisation scales (L2) for mel/mfcc blocks
|
|
389
|
+
const melScale = new Float32Array(nFrames);
|
|
390
|
+
const melBands = params.mel.melBands;
|
|
391
|
+
for (let t = 0; t < nFrames; t++) {
|
|
392
|
+
if ((t & 2047) === 0 && options.isCancelled?.()) throw new Error("@octoseq/mir: cancelled");
|
|
393
|
+
const row = melBands[t];
|
|
394
|
+
if (!row) {
|
|
395
|
+
melScale[t] = 1;
|
|
396
|
+
continue;
|
|
397
|
+
}
|
|
398
|
+
let sumSq = 0;
|
|
399
|
+
for (let i = 0; i < melDim; i++) {
|
|
400
|
+
const x = row[i] ?? 0;
|
|
401
|
+
sumSq += x * x;
|
|
402
|
+
}
|
|
403
|
+
const n = Math.sqrt(sumSq);
|
|
404
|
+
melScale[t] = n > 1e-12 ? (1 / n) : 1;
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
const mfccScale = mfccDim > 0 ? new Float32Array(nFrames) : null;
|
|
408
|
+
const mfccFrames = params.mfcc?.values ?? null;
|
|
409
|
+
if (mfccScale && mfccFrames) {
|
|
410
|
+
for (let t = 0; t < nFrames; t++) {
|
|
411
|
+
if ((t & 2047) === 0 && options.isCancelled?.()) throw new Error("@octoseq/mir: cancelled");
|
|
412
|
+
const row = mfccFrames[t];
|
|
413
|
+
if (!row) {
|
|
414
|
+
mfccScale[t] = 1;
|
|
415
|
+
continue;
|
|
416
|
+
}
|
|
417
|
+
let sumSq = 0;
|
|
418
|
+
for (let i = 0; i < mfccDim; i++) {
|
|
419
|
+
const x = row[i + 1] ?? 0;
|
|
420
|
+
sumSq += x * x;
|
|
421
|
+
}
|
|
422
|
+
const n = Math.sqrt(sumSq);
|
|
423
|
+
mfccScale[t] = n > 1e-12 ? (1 / n) : 1;
|
|
424
|
+
}
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
// --- onset helpers (prefix sums + range max + peak counts)
|
|
428
|
+
const onsetValues = new Float32Array(nFrames);
|
|
429
|
+
const onsetSrc = params.onsetEnvelope.values;
|
|
430
|
+
for (let i = 0; i < nFrames; i++) onsetValues[i] = onsetSrc[i] ?? 0;
|
|
431
|
+
|
|
432
|
+
const onsetPrefix = new Float64Array(nFrames + 1);
|
|
433
|
+
onsetPrefix[0] = 0;
|
|
434
|
+
for (let i = 0; i < nFrames; i++) {
|
|
435
|
+
if ((i & 4095) === 0 && options.isCancelled?.()) throw new Error("@octoseq/mir: cancelled");
|
|
436
|
+
onsetPrefix[i + 1] = (onsetPrefix[i] ?? 0) + (onsetValues[i] ?? 0);
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
const onsetMax = buildSparseTableMax(onsetValues, options.isCancelled);
|
|
440
|
+
|
|
441
|
+
const onsetPeaks = peakPick(timesFrames, onsetValues, {
|
|
442
|
+
minIntervalSec: options.queryPeakPick?.minIntervalSec,
|
|
443
|
+
threshold: options.queryPeakPick?.threshold,
|
|
444
|
+
adaptive: options.queryPeakPick?.adaptiveFactor
|
|
445
|
+
? { method: "meanStd", factor: options.queryPeakPick.adaptiveFactor }
|
|
446
|
+
: undefined,
|
|
447
|
+
strict: true,
|
|
448
|
+
});
|
|
449
|
+
|
|
450
|
+
const isPeak = new Uint8Array(nFrames);
|
|
451
|
+
for (const p of onsetPeaks) {
|
|
452
|
+
const idx = p.index | 0;
|
|
453
|
+
if (idx >= 0 && idx < nFrames) isPeak[idx] = 1;
|
|
454
|
+
}
|
|
455
|
+
const peakPrefix = new Uint32Array(nFrames + 1);
|
|
456
|
+
for (let i = 0; i < nFrames; i++) peakPrefix[i + 1] = (peakPrefix[i] ?? 0) + (isPeak[i] ?? 0);
|
|
457
|
+
|
|
458
|
+
const fingerprintMs = nowMs() - tPrep0;
|
|
459
|
+
|
|
460
|
+
const addMelFrame = (frame: number, sum: Float64Array, sumSq: Float64Array) => {
|
|
461
|
+
const row = melBands[frame];
|
|
462
|
+
const s = melScale[frame] ?? 1;
|
|
463
|
+
for (let i = 0; i < melDim; i++) {
|
|
464
|
+
const x = (row?.[i] ?? 0) * s;
|
|
465
|
+
sum[i] = (sum[i] ?? 0) + x;
|
|
466
|
+
sumSq[i] = (sumSq[i] ?? 0) + x * x;
|
|
467
|
+
}
|
|
468
|
+
};
|
|
469
|
+
const removeMelFrame = (frame: number, sum: Float64Array, sumSq: Float64Array) => {
|
|
470
|
+
const row = melBands[frame];
|
|
471
|
+
const s = melScale[frame] ?? 1;
|
|
472
|
+
for (let i = 0; i < melDim; i++) {
|
|
473
|
+
const x = (row?.[i] ?? 0) * s;
|
|
474
|
+
sum[i] = (sum[i] ?? 0) - x;
|
|
475
|
+
sumSq[i] = (sumSq[i] ?? 0) - x * x;
|
|
476
|
+
}
|
|
477
|
+
};
|
|
478
|
+
|
|
479
|
+
const addMfccFrame = (frame: number, sum: Float64Array, sumSq: Float64Array) => {
|
|
480
|
+
const row = mfccFrames?.[frame];
|
|
481
|
+
const s = mfccScale?.[frame] ?? 1;
|
|
482
|
+
for (let i = 0; i < mfccDim; i++) {
|
|
483
|
+
const x = (row?.[i + 1] ?? 0) * s;
|
|
484
|
+
sum[i] = (sum[i] ?? 0) + x;
|
|
485
|
+
sumSq[i] = (sumSq[i] ?? 0) + x * x;
|
|
486
|
+
}
|
|
487
|
+
};
|
|
488
|
+
const removeMfccFrame = (frame: number, sum: Float64Array, sumSq: Float64Array) => {
|
|
489
|
+
const row = mfccFrames?.[frame];
|
|
490
|
+
const s = mfccScale?.[frame] ?? 1;
|
|
491
|
+
for (let i = 0; i < mfccDim; i++) {
|
|
492
|
+
const x = (row?.[i + 1] ?? 0) * s;
|
|
493
|
+
sum[i] = (sum[i] ?? 0) - x;
|
|
494
|
+
sumSq[i] = (sumSq[i] ?? 0) - x * x;
|
|
495
|
+
}
|
|
496
|
+
};
|
|
497
|
+
|
|
498
|
+
const melFg = new SlidingMoments(melDim, addMelFrame, removeMelFrame);
|
|
499
|
+
const melBg = new SlidingMoments(melDim, addMelFrame, removeMelFrame);
|
|
500
|
+
const mfccFg = mfccDim > 0 ? new SlidingMoments(mfccDim, addMfccFrame, removeMfccFrame) : null;
|
|
501
|
+
const mfccBg = mfccDim > 0 ? new SlidingMoments(mfccDim, addMfccFrame, removeMfccFrame) : null;
|
|
502
|
+
|
|
503
|
+
const writeVectorFromState = (opts: {
|
|
504
|
+
fgStartIdx: number;
|
|
505
|
+
fgEndIdx: number;
|
|
506
|
+
bgStartIdx: number;
|
|
507
|
+
bgEndIdx: number;
|
|
508
|
+
fgStartSec: number;
|
|
509
|
+
fgEndSec: number;
|
|
510
|
+
bgStartSec: number;
|
|
511
|
+
bgEndSec: number;
|
|
512
|
+
out: Float32Array;
|
|
513
|
+
}) => {
|
|
514
|
+
const out = opts.out;
|
|
515
|
+
out.fill(0);
|
|
516
|
+
|
|
517
|
+
const fgCount = Math.max(0, opts.fgEndIdx - opts.fgStartIdx);
|
|
518
|
+
const bgCount = Math.max(0, opts.bgEndIdx - opts.bgStartIdx);
|
|
519
|
+
const bgExCount = Math.max(0, bgCount - fgCount);
|
|
520
|
+
|
|
521
|
+
// --- mel foreground
|
|
522
|
+
for (let i = 0; i < melDim; i++) {
|
|
523
|
+
const sum = melFg.sum[i] ?? 0;
|
|
524
|
+
const sumSq = melFg.sumSq[i] ?? 0;
|
|
525
|
+
const mean = fgCount > 0 ? sum / fgCount : 0;
|
|
526
|
+
const variance = fgCount > 0 ? Math.max(0, sumSq / fgCount - mean * mean) : 0;
|
|
527
|
+
out[layout.melMeanFg.offset + i] = mean;
|
|
528
|
+
out[layout.melVarianceFg.offset + i] = variance;
|
|
529
|
+
|
|
530
|
+
if (layout.melContrast) {
|
|
531
|
+
const bgSum = melBg.sum[i] ?? 0;
|
|
532
|
+
const bgMeanEx = bgExCount > 0 ? (bgSum - sum) / bgExCount : mean;
|
|
533
|
+
out[layout.melContrast.offset + i] = mean - bgMeanEx;
|
|
534
|
+
}
|
|
535
|
+
}
|
|
536
|
+
|
|
537
|
+
// --- onset foreground
|
|
538
|
+
const fgOnsetSum = (onsetPrefix[opts.fgEndIdx] ?? 0) - (onsetPrefix[opts.fgStartIdx] ?? 0);
|
|
539
|
+
const fgOnsetMean = fgCount > 0 ? fgOnsetSum / fgCount : 0;
|
|
540
|
+
const fgOnsetMaxRaw = onsetMax.query(opts.fgStartIdx, opts.fgEndIdx);
|
|
541
|
+
const fgOnsetMax = Number.isFinite(fgOnsetMaxRaw) && fgOnsetMaxRaw !== -Infinity ? fgOnsetMaxRaw : 0;
|
|
542
|
+
const fgPeaks = (peakPrefix[opts.fgEndIdx] ?? 0) - (peakPrefix[opts.fgStartIdx] ?? 0);
|
|
543
|
+
const fgDur = Math.max(1e-6, opts.fgEndSec - opts.fgStartSec);
|
|
544
|
+
const fgPeakDensity = fgPeaks / fgDur;
|
|
545
|
+
|
|
546
|
+
out[layout.onsetFg.offset + 0] = fgOnsetMean;
|
|
547
|
+
out[layout.onsetFg.offset + 1] = fgOnsetMax;
|
|
548
|
+
out[layout.onsetFg.offset + 2] = fgPeakDensity;
|
|
549
|
+
|
|
550
|
+
if (layout.onsetContrast) {
|
|
551
|
+
const bgOnsetSum = (onsetPrefix[opts.bgEndIdx] ?? 0) - (onsetPrefix[opts.bgStartIdx] ?? 0);
|
|
552
|
+
const bgOnsetMeanEx = bgExCount > 0 ? (bgOnsetSum - fgOnsetSum) / bgExCount : fgOnsetMean;
|
|
553
|
+
|
|
554
|
+
const leftMax = onsetMax.query(opts.bgStartIdx, opts.fgStartIdx);
|
|
555
|
+
const rightMax = onsetMax.query(opts.fgEndIdx, opts.bgEndIdx);
|
|
556
|
+
const bgOnsetMaxEx = Math.max(
|
|
557
|
+
Number.isFinite(leftMax) && leftMax !== -Infinity ? leftMax : -Infinity,
|
|
558
|
+
Number.isFinite(rightMax) && rightMax !== -Infinity ? rightMax : -Infinity
|
|
559
|
+
);
|
|
560
|
+
const bgOnsetMaxExSafe = bgOnsetMaxEx === -Infinity ? fgOnsetMax : bgOnsetMaxEx;
|
|
561
|
+
|
|
562
|
+
const bgPeaks = (peakPrefix[opts.bgEndIdx] ?? 0) - (peakPrefix[opts.bgStartIdx] ?? 0);
|
|
563
|
+
const bgPeaksEx = Math.max(0, bgPeaks - fgPeaks);
|
|
564
|
+
const bgExDur = Math.max(1e-6, (opts.bgEndSec - opts.bgStartSec) - fgDur);
|
|
565
|
+
const bgPeakDensityEx = bgPeaksEx / bgExDur;
|
|
566
|
+
|
|
567
|
+
out[layout.onsetContrast.offset + 0] = fgOnsetMean - bgOnsetMeanEx;
|
|
568
|
+
out[layout.onsetContrast.offset + 1] = fgOnsetMax - bgOnsetMaxExSafe;
|
|
569
|
+
out[layout.onsetContrast.offset + 2] = fgPeakDensity - bgPeakDensityEx;
|
|
570
|
+
}
|
|
571
|
+
|
|
572
|
+
// --- mfcc (optional)
|
|
573
|
+
if (mfccDim > 0 && mfccFg && mfccBg && layout.mfccMeanFg && layout.mfccVarianceFg) {
|
|
574
|
+
for (let i = 0; i < mfccDim; i++) {
|
|
575
|
+
const sum = mfccFg.sum[i] ?? 0;
|
|
576
|
+
const sumSq = mfccFg.sumSq[i] ?? 0;
|
|
577
|
+
const mean = fgCount > 0 ? sum / fgCount : 0;
|
|
578
|
+
const variance = fgCount > 0 ? Math.max(0, sumSq / fgCount - mean * mean) : 0;
|
|
579
|
+
out[layout.mfccMeanFg.offset + i] = mean;
|
|
580
|
+
out[layout.mfccVarianceFg.offset + i] = variance;
|
|
581
|
+
|
|
582
|
+
if (layout.mfccMeanContrast && layout.mfccVarianceContrast) {
|
|
583
|
+
const bgSum = mfccBg.sum[i] ?? 0;
|
|
584
|
+
const bgSumSq = mfccBg.sumSq[i] ?? 0;
|
|
585
|
+
const bgMeanEx = bgExCount > 0 ? (bgSum - sum) / bgExCount : mean;
|
|
586
|
+
const bgVarEx = bgExCount > 0 ? Math.max(0, (bgSumSq - sumSq) / bgExCount - bgMeanEx * bgMeanEx) : variance;
|
|
587
|
+
out[layout.mfccMeanContrast.offset + i] = mean - bgMeanEx;
|
|
588
|
+
out[layout.mfccVarianceContrast.offset + i] = variance - bgVarEx;
|
|
589
|
+
}
|
|
590
|
+
}
|
|
591
|
+
}
|
|
592
|
+
};
|
|
593
|
+
|
|
594
|
+
const computeVectorForInterval = (t0: number, t1: number, out: Float32Array) => {
|
|
595
|
+
const fgStartSec = Math.min(t0, t1);
|
|
596
|
+
const fgEndSec = Math.max(t0, t1);
|
|
597
|
+
const { bgStartSec, bgEndSec } = computeBackgroundWindow({
|
|
598
|
+
fgStartSec,
|
|
599
|
+
fgEndSec,
|
|
600
|
+
trackDurationSec: trackDuration,
|
|
601
|
+
backgroundScale,
|
|
602
|
+
});
|
|
603
|
+
|
|
604
|
+
const fgStartIdx = advanceStartIndex(timesFrames, 0, fgStartSec);
|
|
605
|
+
const fgEndIdx = advanceEndIndex(timesFrames, fgStartIdx, fgEndSec);
|
|
606
|
+
const bgStartIdx = advanceStartIndex(timesFrames, 0, bgStartSec);
|
|
607
|
+
const bgEndIdx = advanceEndIndex(timesFrames, bgStartIdx, bgEndSec);
|
|
608
|
+
|
|
609
|
+
// Populate SlidingMoments deterministically for this one-off interval.
|
|
610
|
+
melFg.update(fgStartIdx, fgEndIdx);
|
|
611
|
+
melBg.update(bgStartIdx, bgEndIdx);
|
|
612
|
+
mfccFg?.update(fgStartIdx, fgEndIdx);
|
|
613
|
+
mfccBg?.update(bgStartIdx, bgEndIdx);
|
|
614
|
+
|
|
615
|
+
writeVectorFromState({
|
|
616
|
+
fgStartIdx,
|
|
617
|
+
fgEndIdx,
|
|
618
|
+
bgStartIdx,
|
|
619
|
+
bgEndIdx,
|
|
620
|
+
fgStartSec,
|
|
621
|
+
fgEndSec,
|
|
622
|
+
bgStartSec,
|
|
623
|
+
bgEndSec,
|
|
624
|
+
out,
|
|
625
|
+
});
|
|
626
|
+
};
|
|
627
|
+
|
|
628
|
+
// Query vector for baseline similarity + as optional positive anchor for refinement.
|
|
629
|
+
const queryVec = new Float32Array(layout.dim);
|
|
630
|
+
computeVectorForInterval(qt0, qt1, queryVec);
|
|
631
|
+
|
|
632
|
+
// --- scanning helpers (sliding window indices)
|
|
633
|
+
const resetSlidingState = () => {
|
|
634
|
+
melFg.update(0, 0);
|
|
635
|
+
melBg.update(0, 0);
|
|
636
|
+
mfccFg?.update(0, 0);
|
|
637
|
+
mfccBg?.update(0, 0);
|
|
638
|
+
};
|
|
639
|
+
|
|
640
|
+
const buildWindowVectorsPass = (onWindow: (w: number, t0: number, t1: number, bg: { start: number; end: number }, vec: Float32Array) => void) => {
|
|
641
|
+
resetSlidingState();
|
|
642
|
+
|
|
643
|
+
let fgStartIdx = 0;
|
|
644
|
+
let fgEndIdx = 0;
|
|
645
|
+
let bgStartIdx = 0;
|
|
646
|
+
let bgEndIdx = 0;
|
|
647
|
+
|
|
648
|
+
const vec = new Float32Array(layout.dim);
|
|
649
|
+
|
|
650
|
+
for (let w = 0; w < nWindows; w++) {
|
|
651
|
+
if ((w & 255) === 0 && options.isCancelled?.()) throw new Error("@octoseq/mir: cancelled");
|
|
652
|
+
|
|
653
|
+
const t0 = w * hopSec;
|
|
654
|
+
const t1 = t0 + windowSec;
|
|
655
|
+
times[w] = t0;
|
|
656
|
+
|
|
657
|
+
fgStartIdx = advanceStartIndex(timesFrames, fgStartIdx, t0);
|
|
658
|
+
fgEndIdx = advanceEndIndex(timesFrames, fgEndIdx, t1);
|
|
659
|
+
|
|
660
|
+
const { bgStartSec, bgEndSec } = computeBackgroundWindow({
|
|
661
|
+
fgStartSec: t0,
|
|
662
|
+
fgEndSec: t1,
|
|
663
|
+
trackDurationSec: trackDuration,
|
|
664
|
+
backgroundScale,
|
|
665
|
+
});
|
|
666
|
+
bgStartIdx = advanceStartIndex(timesFrames, bgStartIdx, bgStartSec);
|
|
667
|
+
bgEndIdx = advanceEndIndex(timesFrames, bgEndIdx, bgEndSec);
|
|
668
|
+
|
|
669
|
+
melFg.update(fgStartIdx, fgEndIdx);
|
|
670
|
+
melBg.update(bgStartIdx, bgEndIdx);
|
|
671
|
+
mfccFg?.update(fgStartIdx, fgEndIdx);
|
|
672
|
+
mfccBg?.update(bgStartIdx, bgEndIdx);
|
|
673
|
+
|
|
674
|
+
writeVectorFromState({
|
|
675
|
+
fgStartIdx,
|
|
676
|
+
fgEndIdx,
|
|
677
|
+
bgStartIdx,
|
|
678
|
+
bgEndIdx,
|
|
679
|
+
fgStartSec: t0,
|
|
680
|
+
fgEndSec: t1,
|
|
681
|
+
bgStartSec,
|
|
682
|
+
bgEndSec,
|
|
683
|
+
out: vec,
|
|
684
|
+
});
|
|
685
|
+
|
|
686
|
+
onWindow(w, t0, t1, { start: bgStartSec, end: bgEndSec }, vec);
|
|
687
|
+
}
|
|
688
|
+
};
|
|
689
|
+
|
|
690
|
+
let skippedWindows = 0;
|
|
691
|
+
let scannedWindows = 0;
|
|
692
|
+
|
|
693
|
+
const scanStartMs = nowMs();
|
|
694
|
+
|
|
695
|
+
let curveKind: MirSearchCurveKindV1 = "similarity";
|
|
696
|
+
let modelExplain: MirRefinedModelExplainV1 = baselineExplain;
|
|
697
|
+
let modelMs = 0;
|
|
698
|
+
let trainedModel: ReturnType<typeof trainLogisticModelV1> | ReturnType<typeof buildPrototypeModelV1> | null = null;
|
|
699
|
+
let zMean: Float32Array | null = null;
|
|
700
|
+
let zInvStd: Float32Array | null = null;
|
|
701
|
+
|
|
702
|
+
const runBaselineSimilarityScan = () => {
|
|
703
|
+
// We write into scores[w] directly; use the same index `w` inside the callback to avoid rounding.
|
|
704
|
+
skippedWindows = 0;
|
|
705
|
+
scannedWindows = 0;
|
|
706
|
+
buildWindowVectorsPass((w, t0, t1, _bg, vec) => {
|
|
707
|
+
if (shouldSkip(t0, t1)) {
|
|
708
|
+
scores[w] = 0;
|
|
709
|
+
skippedWindows++;
|
|
710
|
+
return;
|
|
711
|
+
}
|
|
712
|
+
scannedWindows++;
|
|
713
|
+
scores[w] = cosineSimilarity01ByBlocks(queryVec, vec, layout, options.weights);
|
|
714
|
+
});
|
|
715
|
+
};
|
|
716
|
+
|
|
717
|
+
if (modelDecision.kind === "baseline") {
|
|
718
|
+
runBaselineSimilarityScan();
|
|
719
|
+
} else {
|
|
720
|
+
const tModel0 = nowMs();
|
|
721
|
+
curveKind = "confidence";
|
|
722
|
+
|
|
723
|
+
try {
|
|
724
|
+
// Pass 1: accumulate z-score params across all windows (per-track, per-search, ephemeral).
|
|
725
|
+
const dim = layout.dim;
|
|
726
|
+
const sum = new Float64Array(dim);
|
|
727
|
+
const sumSq = new Float64Array(dim);
|
|
728
|
+
|
|
729
|
+
buildWindowVectorsPass((_w, _t0, _t1, _bg, vec) => {
|
|
730
|
+
for (let j = 0; j < dim; j++) {
|
|
731
|
+
const x = vec[j] ?? 0;
|
|
732
|
+
sum[j] = (sum[j] ?? 0) + x;
|
|
733
|
+
sumSq[j] = (sumSq[j] ?? 0) + x * x;
|
|
734
|
+
}
|
|
735
|
+
});
|
|
736
|
+
|
|
737
|
+
const mean = new Float32Array(dim);
|
|
738
|
+
const invStd = new Float32Array(dim);
|
|
739
|
+
const n = Math.max(1, nWindows);
|
|
740
|
+
for (let j = 0; j < dim; j++) {
|
|
741
|
+
const mu = (sum[j] ?? 0) / n;
|
|
742
|
+
const ex2 = (sumSq[j] ?? 0) / n;
|
|
743
|
+
const v = Math.max(0, ex2 - mu * mu);
|
|
744
|
+
const std = Math.sqrt(v);
|
|
745
|
+
mean[j] = mu;
|
|
746
|
+
invStd[j] = std > 1e-6 ? 1 / std : 1;
|
|
747
|
+
}
|
|
748
|
+
|
|
749
|
+
zMean = mean;
|
|
750
|
+
zInvStd = invStd;
|
|
751
|
+
|
|
752
|
+
// Build exemplar vectors.
|
|
753
|
+
const positives: Float32Array[] = [];
|
|
754
|
+
const negatives: Float32Array[] = [];
|
|
755
|
+
|
|
756
|
+
const makeExample = (t0: number, t1: number): Float32Array => {
|
|
757
|
+
const v = new Float32Array(dim);
|
|
758
|
+
computeVectorForInterval(t0, t1, v);
|
|
759
|
+
zScoreInPlace(v, mean, invStd);
|
|
760
|
+
return v;
|
|
761
|
+
};
|
|
762
|
+
|
|
763
|
+
for (const l of modelDecision.positives) positives.push(makeExample(l.t0, l.t1));
|
|
764
|
+
for (const l of modelDecision.negatives) negatives.push(makeExample(l.t0, l.t1));
|
|
765
|
+
|
|
766
|
+
// Optional anchor: only include query once we already meet the ">=2 positives" rule.
|
|
767
|
+
if (includeQueryAsPositive) {
|
|
768
|
+
const q = new Float32Array(dim);
|
|
769
|
+
q.set(queryVec);
|
|
770
|
+
zScoreInPlace(q, mean, invStd);
|
|
771
|
+
positives.push(q);
|
|
772
|
+
}
|
|
773
|
+
|
|
774
|
+
// Train model (deterministic, tiny).
|
|
775
|
+
trainedModel =
|
|
776
|
+
modelDecision.kind === "logistic"
|
|
777
|
+
? trainLogisticModelV1({ positives, negatives, layout })
|
|
778
|
+
: buildPrototypeModelV1({ positives, layout });
|
|
779
|
+
modelExplain = trainedModel.explain;
|
|
780
|
+
|
|
781
|
+
// Pass 2: score windows using the classifier.
|
|
782
|
+
skippedWindows = 0;
|
|
783
|
+
scannedWindows = 0;
|
|
784
|
+
buildWindowVectorsPass((w, t0, t1, _bg, vec) => {
|
|
785
|
+
if (shouldSkip(t0, t1)) {
|
|
786
|
+
scores[w] = 0;
|
|
787
|
+
skippedWindows++;
|
|
788
|
+
return;
|
|
789
|
+
}
|
|
790
|
+
scannedWindows++;
|
|
791
|
+
zScoreInPlace(vec, mean, invStd);
|
|
792
|
+
scores[w] = scoreWithModelV1(trainedModel!, vec);
|
|
793
|
+
});
|
|
794
|
+
} catch (e) {
|
|
795
|
+
// Respect cooperative cancellation semantics.
|
|
796
|
+
if (e instanceof Error && e.message === "@octoseq/mir: cancelled") throw e;
|
|
797
|
+
|
|
798
|
+
// Robustness rule: never crash search due to refinement; degrade gracefully.
|
|
799
|
+
// If refinement fails, fall back to baseline similarity (with contrast features if enabled).
|
|
800
|
+
curveKind = "similarity";
|
|
801
|
+
modelExplain = baselineExplain;
|
|
802
|
+
trainedModel = null;
|
|
803
|
+
zMean = null;
|
|
804
|
+
zInvStd = null;
|
|
805
|
+
runBaselineSimilarityScan();
|
|
806
|
+
} finally {
|
|
807
|
+
modelMs = nowMs() - tModel0;
|
|
808
|
+
}
|
|
809
|
+
}
|
|
810
|
+
|
|
811
|
+
const scanMs = nowMs() - scanStartMs;
|
|
812
|
+
|
|
813
|
+
// --- candidate detection on curve
|
|
814
|
+
const events = peakPick(times, scores, {
|
|
815
|
+
threshold,
|
|
816
|
+
minIntervalSec: minSpacingSec,
|
|
817
|
+
strict: options.candidatePeakPick?.strict ?? true,
|
|
818
|
+
});
|
|
819
|
+
|
|
820
|
+
const candidates: MirSearchCandidateV1Guided[] = events.map((e) => {
|
|
821
|
+
const windowStartSec = e.time;
|
|
822
|
+
const windowEndSec = windowStartSec + windowSec;
|
|
823
|
+
return {
|
|
824
|
+
timeSec: e.time,
|
|
825
|
+
score: e.strength,
|
|
826
|
+
windowStartSec,
|
|
827
|
+
windowEndSec,
|
|
828
|
+
};
|
|
829
|
+
});
|
|
830
|
+
|
|
831
|
+
// Explainability: per-candidate group contributions (logistic only).
|
|
832
|
+
if (trainedModel?.kind === "logistic" && zMean && zInvStd) {
|
|
833
|
+
const tmp = new Float32Array(layout.dim);
|
|
834
|
+
for (const c of candidates) {
|
|
835
|
+
tmp.fill(0);
|
|
836
|
+
computeVectorForInterval(c.windowStartSec, c.windowEndSec, tmp);
|
|
837
|
+
zScoreInPlace(tmp, zMean, zInvStd);
|
|
838
|
+
c.explain = {
|
|
839
|
+
groupLogit: logitContributionsByGroupV1(trainedModel.w, trainedModel.b, tmp, layout),
|
|
840
|
+
};
|
|
841
|
+
}
|
|
842
|
+
}
|
|
843
|
+
|
|
844
|
+
const totalMs = nowMs() - tStart;
|
|
845
|
+
|
|
846
|
+
return {
|
|
847
|
+
times,
|
|
848
|
+
scores,
|
|
849
|
+
candidates,
|
|
850
|
+
curveKind,
|
|
851
|
+
model: modelExplain,
|
|
852
|
+
meta: {
|
|
853
|
+
fingerprintMs,
|
|
854
|
+
scanMs,
|
|
855
|
+
modelMs,
|
|
856
|
+
totalMs,
|
|
857
|
+
windowSec,
|
|
858
|
+
hopSec,
|
|
859
|
+
skippedWindows,
|
|
860
|
+
scannedWindows,
|
|
861
|
+
},
|
|
862
|
+
};
|
|
863
|
+
}
|