@trillboards/edge-sdk 0.2.49 → 0.2.52

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.
Files changed (41) hide show
  1. package/README.md +58 -0
  2. package/dist/AudioOnsetDetector.d.ts +211 -0
  3. package/dist/AudioOnsetDetector.d.ts.map +1 -0
  4. package/dist/AudioOnsetDetector.js +286 -0
  5. package/dist/AudioOnsetDetector.js.map +1 -0
  6. package/dist/CloudSensingManager.d.ts +49 -0
  7. package/dist/CloudSensingManager.d.ts.map +1 -1
  8. package/dist/CloudSensingManager.js +84 -1
  9. package/dist/CloudSensingManager.js.map +1 -1
  10. package/dist/EdgeAgent.d.ts.map +1 -1
  11. package/dist/EdgeAgent.js +20 -2
  12. package/dist/EdgeAgent.js.map +1 -1
  13. package/dist/LiteRtVlmProvider.d.ts +54 -0
  14. package/dist/LiteRtVlmProvider.d.ts.map +1 -1
  15. package/dist/LiteRtVlmProvider.js +132 -0
  16. package/dist/LiteRtVlmProvider.js.map +1 -1
  17. package/dist/SceneChangeDetector.d.ts +27 -1
  18. package/dist/SceneChangeDetector.d.ts.map +1 -1
  19. package/dist/SceneChangeDetector.js +42 -0
  20. package/dist/SceneChangeDetector.js.map +1 -1
  21. package/dist/attachAudioOnset.d.ts +87 -0
  22. package/dist/attachAudioOnset.d.ts.map +1 -0
  23. package/dist/attachAudioOnset.js +87 -0
  24. package/dist/attachAudioOnset.js.map +1 -0
  25. package/dist/config.d.ts +46 -0
  26. package/dist/config.d.ts.map +1 -1
  27. package/dist/config.js.map +1 -1
  28. package/dist/diagnose.d.ts +1 -1
  29. package/dist/diagnose.js +1 -1
  30. package/dist/index.d.ts +5 -0
  31. package/dist/index.d.ts.map +1 -1
  32. package/dist/index.js +16 -1
  33. package/dist/index.js.map +1 -1
  34. package/dist/sensingRunMode.d.ts.map +1 -1
  35. package/dist/sensingRunMode.js +5 -0
  36. package/dist/sensingRunMode.js.map +1 -1
  37. package/dist/wavEncode.d.ts +29 -0
  38. package/dist/wavEncode.d.ts.map +1 -0
  39. package/dist/wavEncode.js +63 -0
  40. package/dist/wavEncode.js.map +1 -0
  41. package/package.json +1 -1
package/README.md CHANGED
@@ -106,6 +106,63 @@ The SDK auto-detects device capabilities and adjusts inference accordingly:
106
106
  - `directml` — Windows GPU acceleration
107
107
  - `cuda` — NVIDIA GPU
108
108
 
109
+ ## Acoustic Sensing (opt-in)
110
+
111
+ The connector can hear as well as see. A microphone catches what a camera has
112
+ nothing to show for — a freezer door left open behind a shelf, glass breaking off
113
+ frame, an alarm in the next room — so an acoustic **onset** is allowed to carry a
114
+ frame that the motion cascade would otherwise have dropped as `no_motion`.
115
+
116
+ **It is OFF by default, and that default is a consent position rather than a
117
+ rollout setting.** Many US states are all-party-consent for audio; the cameras
118
+ this connector attaches to belong to the customer and were very likely installed,
119
+ signed and papered for video. Turning this on has to be somebody's explicit
120
+ decision.
121
+
122
+ ```ts
123
+ new CloudSensingManager({
124
+ loops,
125
+ motionGate: { enabled: true }, // the acoustic gate rides the visual cascade
126
+ audioSensing: {
127
+ enabled: true, // <- explicit; default false
128
+ maxAudioSessionsPerSource: 1, // long-lived audio sessions per recorder
129
+ },
130
+ });
131
+ ```
132
+
133
+ Every failure on this path is non-fatal by construction — a recorder that refuses
134
+ admission, a stream with no audio track, a missing ffmpeg — all leave the camera
135
+ loops untouched. Audio is supplementary and can never stop a feed from producing
136
+ frames.
137
+
138
+ ### Why the gate is DSP and not the on-device model
139
+
140
+ Gemma 4 was measured against this exact runtime before the path was built. It
141
+ **transcribes speech excellently and does not name acoustic events**: on real
142
+ recordings it scored 2/8 on an open prompt and 3/8 forced-choice, where every hit
143
+ was an animate vocalization (dog, baby, applause) and every mechanical / impact /
144
+ tonal event — breaking glass, a door slam, a smoke alarm — missed, defaulting to
145
+ the label `speech` on audio containing no speech.
146
+
147
+ So the two halves are split by what each is actually good at:
148
+
149
+ | Job | Implementation |
150
+ |---|---|
151
+ | "Did something happen?" | `AudioOnsetDetector` — rolling background estimate + threshold |
152
+ | "What was said?" | `LiteRtVlmProvider.transcribeAudio` (Gemma, via litert-lm `input_audio`) |
153
+
154
+ The full measured battery is in the `AudioOnsetDetector` header, and the operator
155
+ view is in [docs/runbooks/audio-sensing-substrate.md](../../../docs/runbooks/audio-sensing-substrate.md) §0.
156
+
157
+ ### New exports
158
+
159
+ | Export | Purpose |
160
+ |---|---|
161
+ | `AudioOnsetDetector`, `rmsDbfs`, `SILENCE_DBFS` | Acoustic onset detection (dBFS — **not** a 0..4 tier) |
162
+ | `attachAudioOnset` | Joins an audio transport to a feed's `MotionGate`; returns `null` when a feed has no audio |
163
+ | `encodeWav16`, `takeMostRecentSeconds` | Float32 PCM → 16-bit mono WAV for the model's `input_audio` |
164
+ | `MotionGateReason: 'audio_onset'` | New cascade verdict — a heard event carried this frame |
165
+
109
166
  ## Sub-Packages
110
167
 
111
168
  This is the umbrella package. For fine-grained control, install sub-packages individually:
@@ -119,6 +176,7 @@ This is the umbrella package. For fine-grained control, install sub-packages ind
119
176
  | [@trillboards/edge-cloud](https://www.npmjs.com/package/@trillboards/edge-cloud) | Gemini Vision scene analysis, contextual targeting |
120
177
  | [@trillboards/edge-platform-linux](https://www.npmjs.com/package/@trillboards/edge-platform-linux) | V4L2 camera, PulseAudio, systemd, kiosk mode |
121
178
  | [@trillboards/edge-platform-windows](https://www.npmjs.com/package/@trillboards/edge-platform-windows) | DirectShow, RTSP IP cameras, WASAPI audio |
179
+ | [@trillboards/edge-platform-rtsp](https://www.npmjs.com/package/@trillboards/edge-platform-rtsp) | RTSP camera frames, RTSP **audio** sessions, macOS microphone |
122
180
 
123
181
  ## MCP Server (AI Agent Integration)
124
182
 
@@ -0,0 +1,211 @@
1
+ /**
2
+ * AudioOnsetDetector — the connector's acoustic equivalent of
3
+ * `SceneChangeDetector`: the CHEAP stage that answers "did anything happen?"
4
+ * so the expensive stage (a VLM inference) is spent only when it might matter.
5
+ *
6
+ * ── WHY THIS IS DSP AND NOT A MODEL. The plan for this feature was to let
7
+ * Gemma 4 name acoustic events directly — it is already on the box, litert-lm
8
+ * accepts `input_audio`, and both encoders ship in our `.litertlm`, so no
9
+ * second model would be needed. That was MEASURED before it was built, and it
10
+ * does not hold. Against `gemma-4-E2B-it.litertlm,gpu`, mono 16 kHz WAV, real
11
+ * recordings, fresh serve, a speech control passing before and after every
12
+ * trial, on an Apple M4 Max — and run TWICE, on litert-lm 0.13.1 (the machine
13
+ * default) and again on an isolated 0.16.0 (the version both provisioners pin,
14
+ * so the one the fleet actually runs). The two agree clip-for-clip:
15
+ *
16
+ * | clip | open prompt ("describe every sound") | forced choice |
17
+ * |---------------|---------------------------------------|---------------|
18
+ * | glass break | "I cannot hear any audio" | `speech` |
19
+ * | glass break 2 | "you haven't provided an audio file" | `speech` |
20
+ * | door slam | "a click or a snap" | `speech` |
21
+ * | smoke alarm | "I cannot hear any audio" | `speech` |
22
+ * | siren | "possibly a scream or a siren" | `dog_barking` |
23
+ * | dog bark | "resemble a bird call" | `dog_barking` |
24
+ * | baby cry | "possibly laughter or screams" | `baby_crying` |
25
+ * | applause | "a single, loud, sharp sound" | `applause` |
26
+ *
27
+ * 2/8 open, 3/8 forced — the SAME 3/8 and the same per-clip labels on both
28
+ * runtimes, siren -> `dog_barking` included, so this is a property of the model
29
+ * and not of a runtime version. The three forced-choice hits are ALL animate
30
+ * vocalizations (dog, baby, human hands); every miss is mechanical, impact or
31
+ * tonal — and those four default to the label `speech` on audio containing no
32
+ * speech at all, because it is a speech model. The result is stable, not noisy:
33
+ * the smoke alarm returned a byte-identical refusal 16/16 times on a fresh
34
+ * serve, and the siren reads as `dog_barking` even as the FIRST request after a
35
+ * restart, so it is a real misclassification and not cross-request context
36
+ * bleed. The audio was genuinely INGESTED in every one of those trials — proven
37
+ * by a mixed control, `glass_then_speech.wav`, whose speech (which is SECOND in
38
+ * the file) transcribes exactly; a miss is therefore a capability result and
39
+ * not a silently dropped payload.
40
+ *
41
+ * The three events the product actually needs — a freezer door, breaking glass,
42
+ * a smoke alarm — are 0/3 in BOTH prompt regimes. So the gate is DSP, and Gemma
43
+ * keeps the half it is genuinely excellent at: it transcribed
44
+ * "There is a fire in the kitchen, everyone get out now" verbatim, 30/30, in
45
+ * 0.63–5.83 s. Speech goes to the model; "something happened" does not.
46
+ *
47
+ * TWO THINGS THOSE TIMINGS ARE NOT. (1) The DELEGATE is unconfirmed: the `,gpu`
48
+ * id was POSTed, but no engine trace was read back, and a warm mldrift cache
49
+ * does not re-emit the `Replacing N … (LITERT_WEBGPU)` line — so a latency range
50
+ * quoted here may span two delegates. The CAPABILITY result does not depend on
51
+ * that; the latencies are indicative only. (2) They are not this feature's
52
+ * running cost. The gate never calls the model at all — see below.
53
+ *
54
+ * ── COST, WHICH IS THE POINT OF PUTTING IT HERE. `litert-lm serve` is a SINGLE
55
+ * conversation and the connector's whole inference budget flows through it. A
56
+ * model-based gate would have spent that budget continuously, at 25 audio
57
+ * tokens per second of audio, on a machine already ceilinged in analyses/hour.
58
+ * This detector spends NONE of it: measured over 1 h of 16 kHz audio pushed in
59
+ * 100 ms blocks (36,000 blocks, one M4 Max core), it costs 50.5 ms of CPU
60
+ * total — 1.40 µs per block, 0.0014% of one core — and zero model tokens. The
61
+ * acoustic channel is therefore free with respect to the serial slot, and only
62
+ * `transcribeAudio` (opt-in, speech only) ever competes for it.
63
+ *
64
+ * ── WHAT IT DOES. Frame the PCM into short hops, take each hop's RMS in dBFS,
65
+ * hold a rolling estimate of the room's background level, and fire when a hop
66
+ * stands `thresholdDb` above that background for at least `minDurationMs`.
67
+ *
68
+ * ── THE ONE NON-OBVIOUS RULE, AND IT IS BORROWED ON PURPOSE. The background
69
+ * must NOT absorb the event that is currently firing. `SceneChangeDetector`
70
+ * carries the visual form of this rule from Frigate (rule 1 in its header: the
71
+ * background only moves after motion has persisted `BACKGROUND_UPDATE_AFTER_FRAMES`
72
+ * frames, otherwise a slowly-moving object is absorbed mid-walk and becomes
73
+ * invisible). The acoustic failure is the same shape and worse, because sound
74
+ * levels move much faster than pixels: a mean that updates every hop is dragged
75
+ * up by the very transient it is supposed to be measuring against, so a long
76
+ * event raises its own floor and the detector goes deaf partway through it.
77
+ * So the background updates freely while the room is quiet, and while a hop is
78
+ * above threshold it updates ONLY once that loudness has PERSISTED past
79
+ * `backgroundAbsorbAfterMs`. A 200 ms door slam never moves the floor; a
80
+ * genuinely louder room (a compressor kicking on, a lunch rush) is absorbed
81
+ * after a few seconds and stops re-firing forever.
82
+ *
83
+ * ── CALIBRATION, for the same reason `SceneChangeDetector` has it. On the
84
+ * first hops there is no trustworthy background, and firing off a floor built
85
+ * from one sample means every connector start costs an inference storm. The
86
+ * detector reports `calibrating` until it has `calibrationFrames` hops and
87
+ * refuses to fire until then. Unlike the visual detector it never RE-ENTERS
88
+ * calibration: there is no acoustic analogue of the dusk IR-cut flip that
89
+ * changes every pixel at once, and a detector that can be talked back into
90
+ * calibration by loud audio is a detector an attacker (or a busy store) can
91
+ * silence.
92
+ *
93
+ * ── REFRACTORY. One physical event is many loud hops. `refractoryMs` collapses
94
+ * a burst into ONE onset so a single dropped tray does not spend the feed's
95
+ * whole inference budget.
96
+ *
97
+ * ── UNITS: THIS CLASS EMITS dBFS AND NOTHING ELSE. dBFS here is a NEGATIVE
98
+ * number relative to full scale (digital silence floors at `SILENCE_DBFS`).
99
+ * It is deliberately NOT a 0..4 loudness tier. The Android DOOH fleet wired a
100
+ * tier-valued device field to a decibel-valued server field, and the decibel
101
+ * column has read as a constant sentinel on 100% of rows ever since — a value
102
+ * that scores perfectly on any fill-rate monitor while measuring nothing, and
103
+ * which pins every venue at "quiet". A tier and a decibel level are two
104
+ * quantities, not a naming difference; docs/runbooks/audio-sensing-substrate.md
105
+ * §3 names both fields and why they must never be aliased. Nothing in this file
106
+ * writes either of them: a caller that wants a tier must map dBFS to one
107
+ * explicitly and own that mapping.
108
+ */
109
+ /** dBFS reported for a digitally-silent hop. Finite so the EMA stays finite. */
110
+ export declare const SILENCE_DBFS = -100;
111
+ export interface AudioOnsetDetectorOptions {
112
+ /** Sample rate of the PCM handed to `push()` (Hz). */
113
+ sampleRate: number;
114
+ /** Analysis hop length (ms). Default 20 — short enough to catch an impact. */
115
+ frameMs?: number;
116
+ /** dB above background that counts as an onset. Default 12. */
117
+ thresholdDb?: number;
118
+ /** Loudness must persist this long to fire (ms). Default 40. */
119
+ minDurationMs?: number;
120
+ /** Minimum gap between two reported onsets (ms). Default 3000. */
121
+ refractoryMs?: number;
122
+ /** Hops required before the background is trusted. Default 50 (=1 s @20 ms). */
123
+ calibrationFrames?: number;
124
+ /** Background EMA blend per quiet hop, 0..1. Default 0.02 (~1 s constant). */
125
+ backgroundAlpha?: number;
126
+ /**
127
+ * Sustained loudness is absorbed into the background after this long (ms).
128
+ * Default 4000. This is the acoustic form of Frigate's
129
+ * `BACKGROUND_UPDATE_AFTER_FRAMES` — see the header.
130
+ */
131
+ backgroundAbsorbAfterMs?: number;
132
+ /**
133
+ * Absolute floor: hops quieter than this never fire, however far above the
134
+ * background they sit. Without it a silent room's background collapses toward
135
+ * `SILENCE_DBFS` and pure dither trips the threshold. Default -55 dBFS.
136
+ */
137
+ minLevelDbfs?: number;
138
+ /** Clock injection for tests. */
139
+ now?: () => number;
140
+ }
141
+ export interface AudioOnsetEvent {
142
+ /** Wall-clock ms at which the onset was reported. */
143
+ atMs: number;
144
+ /** Peak hop level (dBFS) observed during the triggering burst. */
145
+ peakDbfs: number;
146
+ /** Background level (dBFS) the peak was measured against. */
147
+ backgroundDbfs: number;
148
+ /** `peakDbfs - backgroundDbfs`, i.e. how far above the room it stood. */
149
+ excessDb: number;
150
+ /** How long the burst had been above threshold when it fired (ms). */
151
+ durationMs: number;
152
+ }
153
+ export interface AudioOnsetStats {
154
+ framesSeen: number;
155
+ onsets: number;
156
+ calibrating: boolean;
157
+ /** Current rolling background estimate (dBFS), or null while uncalibrated. */
158
+ backgroundDbfs: number | null;
159
+ /** Most recent hop level (dBFS), or null before any audio. */
160
+ lastLevelDbfs: number | null;
161
+ suppressedByRefractory: number;
162
+ }
163
+ /** RMS of a slice, expressed in dBFS (0 = full scale). */
164
+ export declare function rmsDbfs(samples: Float32Array, from: number, to: number): number;
165
+ /**
166
+ * Stateful, one instance per audio feed. PCM must be pushed to the SAME
167
+ * instance in capture order — like `SceneChangeDetector`, the background model
168
+ * is the state.
169
+ */
170
+ export declare class AudioOnsetDetector {
171
+ private readonly sampleRate;
172
+ private readonly frameSamples;
173
+ private readonly frameMs;
174
+ private readonly thresholdDb;
175
+ private readonly minDurationMs;
176
+ private readonly refractoryMs;
177
+ private readonly calibrationFrames;
178
+ private readonly backgroundAlpha;
179
+ private readonly backgroundAbsorbAfterMs;
180
+ private readonly minLevelDbfs;
181
+ private readonly now;
182
+ /** Samples carried across `push()` boundaries so hops are never torn. */
183
+ private residue;
184
+ private background;
185
+ private framesSeen;
186
+ private lastLevelDbfs;
187
+ /** ms of contiguous above-threshold audio accumulated so far. */
188
+ private aboveMs;
189
+ private burstPeakDbfs;
190
+ private burstBackgroundDbfs;
191
+ /** Set once a burst has fired, so it fires ONCE rather than every hop. */
192
+ private burstFired;
193
+ private lastOnsetAtMs;
194
+ private onsets;
195
+ private suppressedByRefractory;
196
+ constructor(options: AudioOnsetDetectorOptions);
197
+ isCalibrating(): boolean;
198
+ getStats(): AudioOnsetStats;
199
+ /**
200
+ * Feed PCM. Returns every onset detected within THIS push (usually none, and
201
+ * at most one per `refractoryMs`). Sample count need not align to a hop — the
202
+ * remainder is carried into the next call so an event straddling a buffer
203
+ * boundary is not silently split.
204
+ */
205
+ push(pcm: Float32Array, nowMs?: number): AudioOnsetEvent[];
206
+ /** Drop all state — used when a feed reconnects and continuity is broken. */
207
+ reset(): void;
208
+ /** One hop. Split out so the state machine is readable in one screen. */
209
+ private consumeFrame;
210
+ }
211
+ //# sourceMappingURL=AudioOnsetDetector.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"AudioOnsetDetector.d.ts","sourceRoot":"","sources":["../src/AudioOnsetDetector.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,286 @@
1
+ "use strict";
2
+ /**
3
+ * AudioOnsetDetector — the connector's acoustic equivalent of
4
+ * `SceneChangeDetector`: the CHEAP stage that answers "did anything happen?"
5
+ * so the expensive stage (a VLM inference) is spent only when it might matter.
6
+ *
7
+ * ── WHY THIS IS DSP AND NOT A MODEL. The plan for this feature was to let
8
+ * Gemma 4 name acoustic events directly — it is already on the box, litert-lm
9
+ * accepts `input_audio`, and both encoders ship in our `.litertlm`, so no
10
+ * second model would be needed. That was MEASURED before it was built, and it
11
+ * does not hold. Against `gemma-4-E2B-it.litertlm,gpu`, mono 16 kHz WAV, real
12
+ * recordings, fresh serve, a speech control passing before and after every
13
+ * trial, on an Apple M4 Max — and run TWICE, on litert-lm 0.13.1 (the machine
14
+ * default) and again on an isolated 0.16.0 (the version both provisioners pin,
15
+ * so the one the fleet actually runs). The two agree clip-for-clip:
16
+ *
17
+ * | clip | open prompt ("describe every sound") | forced choice |
18
+ * |---------------|---------------------------------------|---------------|
19
+ * | glass break | "I cannot hear any audio" | `speech` |
20
+ * | glass break 2 | "you haven't provided an audio file" | `speech` |
21
+ * | door slam | "a click or a snap" | `speech` |
22
+ * | smoke alarm | "I cannot hear any audio" | `speech` |
23
+ * | siren | "possibly a scream or a siren" | `dog_barking` |
24
+ * | dog bark | "resemble a bird call" | `dog_barking` |
25
+ * | baby cry | "possibly laughter or screams" | `baby_crying` |
26
+ * | applause | "a single, loud, sharp sound" | `applause` |
27
+ *
28
+ * 2/8 open, 3/8 forced — the SAME 3/8 and the same per-clip labels on both
29
+ * runtimes, siren -> `dog_barking` included, so this is a property of the model
30
+ * and not of a runtime version. The three forced-choice hits are ALL animate
31
+ * vocalizations (dog, baby, human hands); every miss is mechanical, impact or
32
+ * tonal — and those four default to the label `speech` on audio containing no
33
+ * speech at all, because it is a speech model. The result is stable, not noisy:
34
+ * the smoke alarm returned a byte-identical refusal 16/16 times on a fresh
35
+ * serve, and the siren reads as `dog_barking` even as the FIRST request after a
36
+ * restart, so it is a real misclassification and not cross-request context
37
+ * bleed. The audio was genuinely INGESTED in every one of those trials — proven
38
+ * by a mixed control, `glass_then_speech.wav`, whose speech (which is SECOND in
39
+ * the file) transcribes exactly; a miss is therefore a capability result and
40
+ * not a silently dropped payload.
41
+ *
42
+ * The three events the product actually needs — a freezer door, breaking glass,
43
+ * a smoke alarm — are 0/3 in BOTH prompt regimes. So the gate is DSP, and Gemma
44
+ * keeps the half it is genuinely excellent at: it transcribed
45
+ * "There is a fire in the kitchen, everyone get out now" verbatim, 30/30, in
46
+ * 0.63–5.83 s. Speech goes to the model; "something happened" does not.
47
+ *
48
+ * TWO THINGS THOSE TIMINGS ARE NOT. (1) The DELEGATE is unconfirmed: the `,gpu`
49
+ * id was POSTed, but no engine trace was read back, and a warm mldrift cache
50
+ * does not re-emit the `Replacing N … (LITERT_WEBGPU)` line — so a latency range
51
+ * quoted here may span two delegates. The CAPABILITY result does not depend on
52
+ * that; the latencies are indicative only. (2) They are not this feature's
53
+ * running cost. The gate never calls the model at all — see below.
54
+ *
55
+ * ── COST, WHICH IS THE POINT OF PUTTING IT HERE. `litert-lm serve` is a SINGLE
56
+ * conversation and the connector's whole inference budget flows through it. A
57
+ * model-based gate would have spent that budget continuously, at 25 audio
58
+ * tokens per second of audio, on a machine already ceilinged in analyses/hour.
59
+ * This detector spends NONE of it: measured over 1 h of 16 kHz audio pushed in
60
+ * 100 ms blocks (36,000 blocks, one M4 Max core), it costs 50.5 ms of CPU
61
+ * total — 1.40 µs per block, 0.0014% of one core — and zero model tokens. The
62
+ * acoustic channel is therefore free with respect to the serial slot, and only
63
+ * `transcribeAudio` (opt-in, speech only) ever competes for it.
64
+ *
65
+ * ── WHAT IT DOES. Frame the PCM into short hops, take each hop's RMS in dBFS,
66
+ * hold a rolling estimate of the room's background level, and fire when a hop
67
+ * stands `thresholdDb` above that background for at least `minDurationMs`.
68
+ *
69
+ * ── THE ONE NON-OBVIOUS RULE, AND IT IS BORROWED ON PURPOSE. The background
70
+ * must NOT absorb the event that is currently firing. `SceneChangeDetector`
71
+ * carries the visual form of this rule from Frigate (rule 1 in its header: the
72
+ * background only moves after motion has persisted `BACKGROUND_UPDATE_AFTER_FRAMES`
73
+ * frames, otherwise a slowly-moving object is absorbed mid-walk and becomes
74
+ * invisible). The acoustic failure is the same shape and worse, because sound
75
+ * levels move much faster than pixels: a mean that updates every hop is dragged
76
+ * up by the very transient it is supposed to be measuring against, so a long
77
+ * event raises its own floor and the detector goes deaf partway through it.
78
+ * So the background updates freely while the room is quiet, and while a hop is
79
+ * above threshold it updates ONLY once that loudness has PERSISTED past
80
+ * `backgroundAbsorbAfterMs`. A 200 ms door slam never moves the floor; a
81
+ * genuinely louder room (a compressor kicking on, a lunch rush) is absorbed
82
+ * after a few seconds and stops re-firing forever.
83
+ *
84
+ * ── CALIBRATION, for the same reason `SceneChangeDetector` has it. On the
85
+ * first hops there is no trustworthy background, and firing off a floor built
86
+ * from one sample means every connector start costs an inference storm. The
87
+ * detector reports `calibrating` until it has `calibrationFrames` hops and
88
+ * refuses to fire until then. Unlike the visual detector it never RE-ENTERS
89
+ * calibration: there is no acoustic analogue of the dusk IR-cut flip that
90
+ * changes every pixel at once, and a detector that can be talked back into
91
+ * calibration by loud audio is a detector an attacker (or a busy store) can
92
+ * silence.
93
+ *
94
+ * ── REFRACTORY. One physical event is many loud hops. `refractoryMs` collapses
95
+ * a burst into ONE onset so a single dropped tray does not spend the feed's
96
+ * whole inference budget.
97
+ *
98
+ * ── UNITS: THIS CLASS EMITS dBFS AND NOTHING ELSE. dBFS here is a NEGATIVE
99
+ * number relative to full scale (digital silence floors at `SILENCE_DBFS`).
100
+ * It is deliberately NOT a 0..4 loudness tier. The Android DOOH fleet wired a
101
+ * tier-valued device field to a decibel-valued server field, and the decibel
102
+ * column has read as a constant sentinel on 100% of rows ever since — a value
103
+ * that scores perfectly on any fill-rate monitor while measuring nothing, and
104
+ * which pins every venue at "quiet". A tier and a decibel level are two
105
+ * quantities, not a naming difference; docs/runbooks/audio-sensing-substrate.md
106
+ * §3 names both fields and why they must never be aliased. Nothing in this file
107
+ * writes either of them: a caller that wants a tier must map dBFS to one
108
+ * explicitly and own that mapping.
109
+ */
110
+ Object.defineProperty(exports, "__esModule", { value: true });
111
+ exports.AudioOnsetDetector = exports.SILENCE_DBFS = void 0;
112
+ exports.rmsDbfs = rmsDbfs;
113
+ /** dBFS reported for a digitally-silent hop. Finite so the EMA stays finite. */
114
+ exports.SILENCE_DBFS = -100;
115
+ const DEFAULTS = {
116
+ frameMs: 20,
117
+ thresholdDb: 12,
118
+ minDurationMs: 40,
119
+ refractoryMs: 3000,
120
+ calibrationFrames: 50,
121
+ backgroundAlpha: 0.02,
122
+ backgroundAbsorbAfterMs: 4000,
123
+ minLevelDbfs: -55,
124
+ };
125
+ /** RMS of a slice, expressed in dBFS (0 = full scale). */
126
+ function rmsDbfs(samples, from, to) {
127
+ let sum = 0;
128
+ for (let i = from; i < to; i++) {
129
+ const s = samples[i];
130
+ sum += s * s;
131
+ }
132
+ const n = Math.max(1, to - from);
133
+ const rms = Math.sqrt(sum / n);
134
+ if (!(rms > 0))
135
+ return exports.SILENCE_DBFS;
136
+ return Math.max(exports.SILENCE_DBFS, 20 * Math.log10(rms));
137
+ }
138
+ /**
139
+ * Stateful, one instance per audio feed. PCM must be pushed to the SAME
140
+ * instance in capture order — like `SceneChangeDetector`, the background model
141
+ * is the state.
142
+ */
143
+ class AudioOnsetDetector {
144
+ constructor(options) {
145
+ /** Samples carried across `push()` boundaries so hops are never torn. */
146
+ this.residue = new Float32Array(0);
147
+ this.background = exports.SILENCE_DBFS;
148
+ this.framesSeen = 0;
149
+ this.lastLevelDbfs = null;
150
+ /** ms of contiguous above-threshold audio accumulated so far. */
151
+ this.aboveMs = 0;
152
+ this.burstPeakDbfs = exports.SILENCE_DBFS;
153
+ this.burstBackgroundDbfs = exports.SILENCE_DBFS;
154
+ /** Set once a burst has fired, so it fires ONCE rather than every hop. */
155
+ this.burstFired = false;
156
+ this.lastOnsetAtMs = null;
157
+ this.onsets = 0;
158
+ this.suppressedByRefractory = 0;
159
+ if (!(options.sampleRate > 0)) {
160
+ throw new Error('AudioOnsetDetector requires a positive sampleRate');
161
+ }
162
+ this.sampleRate = options.sampleRate;
163
+ this.frameMs = Math.max(1, Math.trunc(options.frameMs ?? DEFAULTS.frameMs));
164
+ this.frameSamples = Math.max(1, Math.round((this.sampleRate * this.frameMs) / 1000));
165
+ this.thresholdDb = options.thresholdDb ?? DEFAULTS.thresholdDb;
166
+ this.minDurationMs = Math.max(0, options.minDurationMs ?? DEFAULTS.minDurationMs);
167
+ this.refractoryMs = Math.max(0, options.refractoryMs ?? DEFAULTS.refractoryMs);
168
+ this.calibrationFrames = Math.max(1, Math.trunc(options.calibrationFrames ?? DEFAULTS.calibrationFrames));
169
+ this.backgroundAlpha = Math.min(1, Math.max(0, options.backgroundAlpha ?? DEFAULTS.backgroundAlpha));
170
+ this.backgroundAbsorbAfterMs = Math.max(0, options.backgroundAbsorbAfterMs ?? DEFAULTS.backgroundAbsorbAfterMs);
171
+ this.minLevelDbfs = options.minLevelDbfs ?? DEFAULTS.minLevelDbfs;
172
+ this.now = options.now ?? Date.now;
173
+ }
174
+ isCalibrating() {
175
+ return this.framesSeen < this.calibrationFrames;
176
+ }
177
+ getStats() {
178
+ return {
179
+ framesSeen: this.framesSeen,
180
+ onsets: this.onsets,
181
+ calibrating: this.isCalibrating(),
182
+ backgroundDbfs: this.framesSeen === 0 ? null : this.background,
183
+ lastLevelDbfs: this.lastLevelDbfs,
184
+ suppressedByRefractory: this.suppressedByRefractory,
185
+ };
186
+ }
187
+ /**
188
+ * Feed PCM. Returns every onset detected within THIS push (usually none, and
189
+ * at most one per `refractoryMs`). Sample count need not align to a hop — the
190
+ * remainder is carried into the next call so an event straddling a buffer
191
+ * boundary is not silently split.
192
+ */
193
+ push(pcm, nowMs) {
194
+ const now = nowMs ?? this.now();
195
+ const events = [];
196
+ let buf;
197
+ if (this.residue.length === 0) {
198
+ buf = pcm;
199
+ }
200
+ else {
201
+ buf = new Float32Array(this.residue.length + pcm.length);
202
+ buf.set(this.residue, 0);
203
+ buf.set(pcm, this.residue.length);
204
+ }
205
+ let offset = 0;
206
+ while (offset + this.frameSamples <= buf.length) {
207
+ const level = rmsDbfs(buf, offset, offset + this.frameSamples);
208
+ const event = this.consumeFrame(level, now);
209
+ if (event)
210
+ events.push(event);
211
+ offset += this.frameSamples;
212
+ }
213
+ this.residue = offset > 0 ? buf.slice(offset) : buf;
214
+ return events;
215
+ }
216
+ /** Drop all state — used when a feed reconnects and continuity is broken. */
217
+ reset() {
218
+ this.residue = new Float32Array(0);
219
+ this.background = exports.SILENCE_DBFS;
220
+ this.framesSeen = 0;
221
+ this.lastLevelDbfs = null;
222
+ this.aboveMs = 0;
223
+ this.burstPeakDbfs = exports.SILENCE_DBFS;
224
+ this.burstBackgroundDbfs = exports.SILENCE_DBFS;
225
+ this.burstFired = false;
226
+ // `lastOnsetAtMs` deliberately SURVIVES: the refractory window is about the
227
+ // physical room, not about our bookkeeping, and a flapping feed that reset
228
+ // it could re-fire on every reconnect.
229
+ }
230
+ /** One hop. Split out so the state machine is readable in one screen. */
231
+ consumeFrame(level, now) {
232
+ this.lastLevelDbfs = level;
233
+ // Seed from the FIRST real reading rather than from the silence floor, for
234
+ // the reason SceneChangeDetector seeds its contrast ring from the first real
235
+ // reading: a synthetic seed makes the mapping crawl for the whole startup
236
+ // window and manufactures phantom events while it does.
237
+ if (this.framesSeen === 0)
238
+ this.background = level;
239
+ this.framesSeen++;
240
+ const calibrating = this.isCalibrating();
241
+ const excess = level - this.background;
242
+ const isLoud = excess >= this.thresholdDb && level >= this.minLevelDbfs;
243
+ if (!isLoud) {
244
+ // Quiet hop: the background tracks the room freely, and the burst ends.
245
+ this.background += this.backgroundAlpha * (level - this.background);
246
+ this.aboveMs = 0;
247
+ this.burstPeakDbfs = exports.SILENCE_DBFS;
248
+ this.burstFired = false;
249
+ return null;
250
+ }
251
+ // Loud hop. Record the burst, but do NOT let it move the background until it
252
+ // has persisted long enough to be a room change rather than an event.
253
+ if (this.aboveMs === 0)
254
+ this.burstBackgroundDbfs = this.background;
255
+ this.aboveMs += this.frameMs;
256
+ this.burstPeakDbfs = Math.max(this.burstPeakDbfs, level);
257
+ if (this.aboveMs >= this.backgroundAbsorbAfterMs) {
258
+ this.background += this.backgroundAlpha * (level - this.background);
259
+ }
260
+ if (calibrating)
261
+ return null;
262
+ if (this.burstFired)
263
+ return null;
264
+ if (this.aboveMs < this.minDurationMs)
265
+ return null;
266
+ if (this.lastOnsetAtMs !== null && now - this.lastOnsetAtMs < this.refractoryMs) {
267
+ // Still one physical event as far as the product is concerned. Mark the
268
+ // burst fired so it is counted once, not once per hop.
269
+ this.burstFired = true;
270
+ this.suppressedByRefractory++;
271
+ return null;
272
+ }
273
+ this.burstFired = true;
274
+ this.lastOnsetAtMs = now;
275
+ this.onsets++;
276
+ return {
277
+ atMs: now,
278
+ peakDbfs: this.burstPeakDbfs,
279
+ backgroundDbfs: this.burstBackgroundDbfs,
280
+ excessDb: this.burstPeakDbfs - this.burstBackgroundDbfs,
281
+ durationMs: this.aboveMs,
282
+ };
283
+ }
284
+ }
285
+ exports.AudioOnsetDetector = AudioOnsetDetector;
286
+ //# sourceMappingURL=AudioOnsetDetector.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"AudioOnsetDetector.js","sourceRoot":"","sources":["../src/AudioOnsetDetector.ts"],"names":[],"mappings":"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"}