@trillboards/edge-sdk 0.2.49 → 0.2.53
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 +58 -0
- package/deploy/macos/README.md +18 -3
- package/deploy/macos/apply-update.sh +225 -0
- package/deploy/macos/build-macos-pkg.sh +10 -8
- package/deploy/macos/scripts/postinstall +13 -3
- package/dist/AudioOnsetDetector.d.ts +211 -0
- package/dist/AudioOnsetDetector.d.ts.map +1 -0
- package/dist/AudioOnsetDetector.js +286 -0
- package/dist/AudioOnsetDetector.js.map +1 -0
- package/dist/CloudSensingManager.d.ts +49 -0
- package/dist/CloudSensingManager.d.ts.map +1 -1
- package/dist/CloudSensingManager.js +84 -1
- package/dist/CloudSensingManager.js.map +1 -1
- package/dist/EdgeAgent.d.ts.map +1 -1
- package/dist/EdgeAgent.js +20 -2
- package/dist/EdgeAgent.js.map +1 -1
- package/dist/LiteRtVlmProvider.d.ts +54 -0
- package/dist/LiteRtVlmProvider.d.ts.map +1 -1
- package/dist/LiteRtVlmProvider.js +132 -0
- package/dist/LiteRtVlmProvider.js.map +1 -1
- package/dist/SceneChangeDetector.d.ts +27 -1
- package/dist/SceneChangeDetector.d.ts.map +1 -1
- package/dist/SceneChangeDetector.js +42 -0
- package/dist/SceneChangeDetector.js.map +1 -1
- package/dist/attachAudioOnset.d.ts +87 -0
- package/dist/attachAudioOnset.d.ts.map +1 -0
- package/dist/attachAudioOnset.js +87 -0
- package/dist/attachAudioOnset.js.map +1 -0
- package/dist/config.d.ts +46 -0
- package/dist/config.d.ts.map +1 -1
- package/dist/config.js.map +1 -1
- package/dist/connectorUpdate.d.ts +23 -16
- package/dist/connectorUpdate.d.ts.map +1 -1
- package/dist/connectorUpdate.js +37 -10
- package/dist/connectorUpdate.js.map +1 -1
- package/dist/connectorUpdateApply.d.ts +3 -0
- package/dist/connectorUpdateApply.d.ts.map +1 -1
- package/dist/connectorUpdateApply.js +3 -0
- package/dist/connectorUpdateApply.js.map +1 -1
- package/dist/connectorUpdateApplyMac.d.ts +50 -0
- package/dist/connectorUpdateApplyMac.d.ts.map +1 -0
- package/dist/connectorUpdateApplyMac.js +166 -0
- package/dist/connectorUpdateApplyMac.js.map +1 -0
- package/dist/diagnose.d.ts +1 -1
- package/dist/diagnose.js +1 -1
- package/dist/index.d.ts +5 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +16 -1
- package/dist/index.js.map +1 -1
- package/dist/sensingRunMode.d.ts.map +1 -1
- package/dist/sensingRunMode.js +5 -0
- package/dist/sensingRunMode.js.map +1 -1
- package/dist/wavEncode.d.ts +29 -0
- package/dist/wavEncode.d.ts.map +1 -0
- package/dist/wavEncode.js +63 -0
- package/dist/wavEncode.js.map +1 -0
- 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
|
|
package/deploy/macos/README.md
CHANGED
|
@@ -19,6 +19,7 @@ What lands on disk (home-domain, **no sudo / no admin password**):
|
|
|
19
19
|
edge-sdk), WITHOUT the heavy local-inference stack
|
|
20
20
|
share/com.trillboards.edge.plist LaunchAgent template
|
|
21
21
|
share/ensure-litertlm.sh background local-AI retry supervisor
|
|
22
|
+
share/apply-update.sh signed-pkg updater with rollback
|
|
22
23
|
share/models/gemma-4-E2B-it.litertlm required customer local VLM (removed after import)
|
|
23
24
|
~/Applications/Trillboards CCTV Setup.app native customer setup window (opened automatically)
|
|
24
25
|
~/Library/Application Support/Trillboards/trillboards.config.json (after pair, 0600)
|
|
@@ -31,8 +32,9 @@ bootstrapped automatically on first successful pair. The device token is
|
|
|
31
32
|
delivered **only by pairing** — it is never baked into the `.pkg`.
|
|
32
33
|
|
|
33
34
|
> The customer build includes Gemma and completes its local health check before
|
|
34
|
-
> pairing. Cloud inference requires explicit configuration.
|
|
35
|
-
>
|
|
35
|
+
> pairing. Cloud inference requires explicit configuration. The release lane
|
|
36
|
+
> also builds a signed, notarized `--skip-model` package for automatic upgrades;
|
|
37
|
+
> it is accepted only on an already-provisioned machine whose model is healthy.
|
|
36
38
|
|
|
37
39
|
---
|
|
38
40
|
|
|
@@ -83,13 +85,26 @@ Options:
|
|
|
83
85
|
| `--arch arm64\|x64` | host arch | target architecture |
|
|
84
86
|
| `--node-version vX.Y.Z` | `v22.22.0` | pinned Node runtime to bundle |
|
|
85
87
|
| `--bundle-model` | on | include the pinned ~2.4 GB Gemma model required by customer setup |
|
|
86
|
-
| `--skip-model` | off | build
|
|
88
|
+
| `--skip-model` | off | build the slim code-only upgrade package; it is not a valid first installer |
|
|
87
89
|
| `--offline` | off | skip the official Node download; copy the host `node` |
|
|
88
90
|
| `--sign-installer "Developer ID Installer: …"` | unset | `productsign` the pkg |
|
|
89
91
|
|
|
90
92
|
The build is idempotent (it wipes its staging tree each run) and **smoke-tests
|
|
91
93
|
the staged bundle** (`trillboards-edge version`) before packaging.
|
|
92
94
|
|
|
95
|
+
### Automatic updates
|
|
96
|
+
|
|
97
|
+
Every paired connector checks the macOS version manifest at launch and every
|
|
98
|
+
six hours. Once a release has baked for six hours, the connector downloads the
|
|
99
|
+
content-addressed code-only package while sensing remains online. Before it can
|
|
100
|
+
be staged, macOS must validate the exact Aethercorp Developer ID Installer
|
|
101
|
+
identity, the notarization staple, Gatekeeper acceptance, size, and SHA-256.
|
|
102
|
+
The connector waits for a safe sensing point, exits cleanly, installs the
|
|
103
|
+
package, smoke-tests the new version, and relaunches the LaunchAgent. The old
|
|
104
|
+
tree remains as `~/Library/Trillboards/edge.previous` and is restored
|
|
105
|
+
automatically if installation, smoke testing, or relaunch fails. Existing
|
|
106
|
+
pairing and the local model are preserved; the setup window does not reopen.
|
|
107
|
+
|
|
93
108
|
### Local end-to-end test
|
|
94
109
|
|
|
95
110
|
```bash
|
|
@@ -0,0 +1,225 @@
|
|
|
1
|
+
#!/bin/bash
|
|
2
|
+
#
|
|
3
|
+
# Stage and apply the signed, notarized macOS connector update package.
|
|
4
|
+
# The running connector chooses the safe point; this detached helper owns the
|
|
5
|
+
# download, macOS trust checks, LaunchAgent handoff, swap, smoke test, and
|
|
6
|
+
# `.previous` rollback.
|
|
7
|
+
|
|
8
|
+
set -u
|
|
9
|
+
|
|
10
|
+
LABEL='com.trillboards.edge'
|
|
11
|
+
EXPECTED_SIGNER='Developer ID Installer: Aethercorp Inc (78JQ7882U9)'
|
|
12
|
+
MODE=''
|
|
13
|
+
INSTALL_DIR=''
|
|
14
|
+
UPDATE_URL=''
|
|
15
|
+
EXPECTED_VERSION=''
|
|
16
|
+
CURRENT_VERSION=''
|
|
17
|
+
EXPECTED_SHA256=''
|
|
18
|
+
EXPECTED_SIZE=''
|
|
19
|
+
WAIT_PID=''
|
|
20
|
+
|
|
21
|
+
while [ "$#" -gt 0 ]; do
|
|
22
|
+
case "$1" in
|
|
23
|
+
--stage|--commit) MODE="$1"; shift ;;
|
|
24
|
+
--install-dir) INSTALL_DIR="${2:-}"; shift 2 ;;
|
|
25
|
+
--update-url) UPDATE_URL="${2:-}"; shift 2 ;;
|
|
26
|
+
--expected-version) EXPECTED_VERSION="${2:-}"; shift 2 ;;
|
|
27
|
+
--current-version) CURRENT_VERSION="${2:-}"; shift 2 ;;
|
|
28
|
+
--expected-sha256) EXPECTED_SHA256="${2:-}"; shift 2 ;;
|
|
29
|
+
--expected-size) EXPECTED_SIZE="${2:-}"; shift 2 ;;
|
|
30
|
+
--wait-pid) WAIT_PID="${2:-}"; shift 2 ;;
|
|
31
|
+
*) printf '[apply-update] unknown argument: %s\n' "$1" >&2; exit 2 ;;
|
|
32
|
+
esac
|
|
33
|
+
done
|
|
34
|
+
|
|
35
|
+
log() {
|
|
36
|
+
/usr/bin/logger -t com.trillboards.edge "apply-update: $*" 2>/dev/null || true
|
|
37
|
+
printf '[apply-update] %s\n' "$*"
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
case "$EXPECTED_VERSION" in
|
|
41
|
+
[0-9]*.[0-9]*.[0-9]*) ;;
|
|
42
|
+
*) log 'invalid expected version'; exit 2 ;;
|
|
43
|
+
esac
|
|
44
|
+
|
|
45
|
+
# The updater is intentionally scoped to the one home-domain path the package
|
|
46
|
+
# installs. This makes every later rm/mv target resolved and bounded.
|
|
47
|
+
EXPECTED_INSTALL_DIR="${HOME}/Library/Trillboards/edge"
|
|
48
|
+
if [ "$INSTALL_DIR" != "$EXPECTED_INSTALL_DIR" ]; then
|
|
49
|
+
log "refusing unexpected install root: $INSTALL_DIR"
|
|
50
|
+
exit 2
|
|
51
|
+
fi
|
|
52
|
+
|
|
53
|
+
PARENT_DIR="$(dirname "$INSTALL_DIR")"
|
|
54
|
+
UPDATE_DIR="$PARENT_DIR/.edge-auto-update"
|
|
55
|
+
ARCHIVE="$UPDATE_DIR/connector-update.pkg"
|
|
56
|
+
MARKER="$UPDATE_DIR/staged.json"
|
|
57
|
+
OUTCOME="$UPDATE_DIR/outcome.json"
|
|
58
|
+
LOCK_DIR="$UPDATE_DIR/lock"
|
|
59
|
+
PREVIOUS="$PARENT_DIR/edge.previous"
|
|
60
|
+
PLIST="$HOME/Library/LaunchAgents/$LABEL.plist"
|
|
61
|
+
|
|
62
|
+
mkdir -p "$UPDATE_DIR" || exit 1
|
|
63
|
+
if ! mkdir "$LOCK_DIR" 2>/dev/null; then
|
|
64
|
+
log 'another update transaction is already running'
|
|
65
|
+
exit 0
|
|
66
|
+
fi
|
|
67
|
+
trap 'rmdir "$LOCK_DIR" 2>/dev/null || true' EXIT
|
|
68
|
+
|
|
69
|
+
json_escape() {
|
|
70
|
+
printf '%s' "$1" | /usr/bin/sed 's/\\/\\\\/g; s/"/\\"/g'
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
write_outcome() {
|
|
74
|
+
local result="$1" detail="$2" tmp="$OUTCOME.tmp.$$"
|
|
75
|
+
printf '{"attemptedAt":"%s","result":"%s","detail":"%s"}\n' \
|
|
76
|
+
"$(date -u +%Y-%m-%dT%H:%M:%SZ)" "$result" "$(json_escape "$detail")" > "$tmp" \
|
|
77
|
+
&& mv -f "$tmp" "$OUTCOME"
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
fail_stage() {
|
|
81
|
+
log "$1"
|
|
82
|
+
rm -f "$ARCHIVE.download" "$MARKER.tmp.$$"
|
|
83
|
+
exit 1
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
configure_proxy() {
|
|
87
|
+
if [ -n "${HTTPS_PROXY:-}" ] || [ -n "${https_proxy:-}" ] || [ -n "${ALL_PROXY:-}" ]; then
|
|
88
|
+
return
|
|
89
|
+
fi
|
|
90
|
+
local proxy_conf enabled host port
|
|
91
|
+
proxy_conf="$(/usr/sbin/scutil --proxy 2>/dev/null || true)"
|
|
92
|
+
enabled="$(printf '%s\n' "$proxy_conf" | /usr/bin/awk '/HTTPSEnable/ {print $3}')"
|
|
93
|
+
[ "$enabled" = '1' ] || return
|
|
94
|
+
host="$(printf '%s\n' "$proxy_conf" | /usr/bin/awk '/HTTPSProxy/ {print $3}')"
|
|
95
|
+
port="$(printf '%s\n' "$proxy_conf" | /usr/bin/awk '/HTTPSPort/ {print $3}')"
|
|
96
|
+
[ -n "$host" ] || return
|
|
97
|
+
HTTPS_PROXY="http://$host${port:+:$port}"
|
|
98
|
+
export HTTPS_PROXY HTTP_PROXY="$HTTPS_PROXY"
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
verify_pkg() {
|
|
102
|
+
local pkg="$1" sha="$2" size="$3" actual_size actual_sha sig_out
|
|
103
|
+
[ -f "$pkg" ] || return 1
|
|
104
|
+
case "$size" in *[!0-9]*|'') return 1 ;; esac
|
|
105
|
+
printf '%s' "$sha" | /usr/bin/grep -Eq '^[0-9a-f]{64}$' || return 1
|
|
106
|
+
|
|
107
|
+
actual_size="$(/usr/bin/wc -c < "$pkg" | tr -d ' ')"
|
|
108
|
+
[ "$actual_size" = "$size" ] || { log "payload size mismatch: expected $size, got $actual_size"; return 1; }
|
|
109
|
+
actual_sha="$(/usr/bin/shasum -a 256 "$pkg" | /usr/bin/awk '{print $1}')"
|
|
110
|
+
[ "$actual_sha" = "$sha" ] || { log 'payload SHA-256 mismatch'; return 1; }
|
|
111
|
+
|
|
112
|
+
sig_out="$UPDATE_DIR/signature.txt"
|
|
113
|
+
/usr/sbin/pkgutil --check-signature "$pkg" > "$sig_out" 2>&1 || { log 'package signature is invalid'; return 1; }
|
|
114
|
+
/usr/bin/grep -Fq "$EXPECTED_SIGNER" "$sig_out" || { log 'package signer is not Aethercorp'; return 1; }
|
|
115
|
+
/usr/bin/xcrun stapler validate "$pkg" >/dev/null 2>&1 || { log 'package has no valid notarization staple'; return 1; }
|
|
116
|
+
/usr/sbin/spctl -a -vv --type install "$pkg" >/dev/null 2>&1 || { log 'Gatekeeper rejected the update package'; return 1; }
|
|
117
|
+
return 0
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
if [ "$MODE" = '--stage' ]; then
|
|
121
|
+
case "$UPDATE_URL" in https://*) ;; *) fail_stage 'update URL must use HTTPS' ;; esac
|
|
122
|
+
configure_proxy
|
|
123
|
+
log "downloading connector $EXPECTED_VERSION while sensing stays online"
|
|
124
|
+
/usr/bin/curl --fail --location --silent --show-error --proto '=https' --tlsv1.2 \
|
|
125
|
+
--connect-timeout 30 --max-time 3600 --output "$ARCHIVE.download" "$UPDATE_URL" \
|
|
126
|
+
|| fail_stage 'update download failed'
|
|
127
|
+
verify_pkg "$ARCHIVE.download" "$EXPECTED_SHA256" "$EXPECTED_SIZE" \
|
|
128
|
+
|| fail_stage 'update package verification failed'
|
|
129
|
+
mv -f "$ARCHIVE.download" "$ARCHIVE" || fail_stage 'could not retain verified package'
|
|
130
|
+
marker_tmp="$MARKER.tmp.$$"
|
|
131
|
+
printf '{"version":"%s","currentVersion":"%s","sha256":"%s","sizeBytes":%s}\n' \
|
|
132
|
+
"$EXPECTED_VERSION" "$CURRENT_VERSION" "$EXPECTED_SHA256" "$EXPECTED_SIZE" > "$marker_tmp" \
|
|
133
|
+
|| fail_stage 'could not write staged marker'
|
|
134
|
+
mv -f "$marker_tmp" "$MARKER" || fail_stage 'could not publish staged marker'
|
|
135
|
+
log "connector $EXPECTED_VERSION is staged, signed, notarized, and verified"
|
|
136
|
+
exit 0
|
|
137
|
+
fi
|
|
138
|
+
|
|
139
|
+
if [ "$MODE" != '--commit' ]; then
|
|
140
|
+
log 'exactly one of --stage or --commit is required'
|
|
141
|
+
exit 2
|
|
142
|
+
fi
|
|
143
|
+
|
|
144
|
+
marker_version="$(/usr/bin/plutil -extract version raw "$MARKER" 2>/dev/null || true)"
|
|
145
|
+
marker_sha="$(/usr/bin/plutil -extract sha256 raw "$MARKER" 2>/dev/null || true)"
|
|
146
|
+
marker_size="$(/usr/bin/plutil -extract sizeBytes raw "$MARKER" 2>/dev/null || true)"
|
|
147
|
+
if [ "$marker_version" != "$EXPECTED_VERSION" ]; then
|
|
148
|
+
write_outcome 'rejected' 'staged version did not match the requested version'
|
|
149
|
+
log 'staged version mismatch; refusing update'
|
|
150
|
+
exit 1
|
|
151
|
+
fi
|
|
152
|
+
verify_pkg "$ARCHIVE" "$marker_sha" "$marker_size" || {
|
|
153
|
+
write_outcome 'rejected' 'staged package failed commit-time verification'
|
|
154
|
+
rm -f "$MARKER" "$ARCHIVE"
|
|
155
|
+
exit 1
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
# The connector launches us and then exits cleanly. Do not touch its mapped
|
|
159
|
+
# binaries until that PID is gone; a timeout leaves the current install intact.
|
|
160
|
+
if [ -n "$WAIT_PID" ]; then
|
|
161
|
+
waited=0
|
|
162
|
+
while kill -0 "$WAIT_PID" 2>/dev/null && [ "$waited" -lt 120 ]; do
|
|
163
|
+
sleep 1
|
|
164
|
+
waited=$((waited + 1))
|
|
165
|
+
done
|
|
166
|
+
if kill -0 "$WAIT_PID" 2>/dev/null; then
|
|
167
|
+
write_outcome 'failed' 'connector did not exit before the update deadline'
|
|
168
|
+
log 'connector did not exit; leaving current install untouched'
|
|
169
|
+
exit 1
|
|
170
|
+
fi
|
|
171
|
+
fi
|
|
172
|
+
|
|
173
|
+
restart_agent() {
|
|
174
|
+
[ -f "$PLIST" ] || return 1
|
|
175
|
+
/bin/launchctl bootout "gui/$(id -u)/$LABEL" >/dev/null 2>&1 || true
|
|
176
|
+
local attempt=1
|
|
177
|
+
while [ "$attempt" -le 10 ]; do
|
|
178
|
+
/bin/launchctl bootstrap "gui/$(id -u)" "$PLIST" >/dev/null 2>&1 || true
|
|
179
|
+
if /bin/launchctl print "gui/$(id -u)/$LABEL" >/dev/null 2>&1; then
|
|
180
|
+
/bin/launchctl kickstart -k "gui/$(id -u)/$LABEL" >/dev/null 2>&1 || true
|
|
181
|
+
return 0
|
|
182
|
+
fi
|
|
183
|
+
sleep 1
|
|
184
|
+
attempt=$((attempt + 1))
|
|
185
|
+
done
|
|
186
|
+
return 1
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
rollback() {
|
|
190
|
+
local detail="$1"
|
|
191
|
+
/bin/launchctl bootout "gui/$(id -u)/$LABEL" >/dev/null 2>&1 || true
|
|
192
|
+
rm -rf "$INSTALL_DIR"
|
|
193
|
+
if [ -d "$PREVIOUS" ]; then
|
|
194
|
+
mv "$PREVIOUS" "$INSTALL_DIR" || true
|
|
195
|
+
fi
|
|
196
|
+
if restart_agent; then
|
|
197
|
+
write_outcome 'failed' "$detail; previous version restored"
|
|
198
|
+
else
|
|
199
|
+
write_outcome 'failed' "$detail; previous files restored but the agent did not restart"
|
|
200
|
+
fi
|
|
201
|
+
rm -f "$MARKER" "$ARCHIVE"
|
|
202
|
+
log "$detail; rolled back"
|
|
203
|
+
exit 1
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
log "applying connector $EXPECTED_VERSION"
|
|
207
|
+
/bin/launchctl bootout "gui/$(id -u)/$LABEL" >/dev/null 2>&1 || true
|
|
208
|
+
rm -rf "$PREVIOUS"
|
|
209
|
+
mv "$INSTALL_DIR" "$PREVIOUS" || {
|
|
210
|
+
write_outcome 'failed' 'could not preserve the current install for rollback'
|
|
211
|
+
exit 1
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
/usr/sbin/installer -pkg "$ARCHIVE" -target CurrentUserHomeDirectory >/dev/null 2>&1 \
|
|
215
|
+
|| rollback 'macOS Installer rejected the update'
|
|
216
|
+
|
|
217
|
+
installed_version="$($INSTALL_DIR/bin/trillboards-edge version 2>/dev/null || true)"
|
|
218
|
+
printf '%s' "$installed_version" | /usr/bin/grep -Fq "edge-sdk v$EXPECTED_VERSION" \
|
|
219
|
+
|| rollback 'updated connector failed its version smoke test'
|
|
220
|
+
|
|
221
|
+
restart_agent || rollback 'updated connector installed but its LaunchAgent did not restart'
|
|
222
|
+
write_outcome 'success' "updated from $CURRENT_VERSION to $EXPECTED_VERSION"
|
|
223
|
+
rm -f "$MARKER" "$ARCHIVE" "$UPDATE_DIR/signature.txt"
|
|
224
|
+
log "connector $EXPECTED_VERSION installed and relaunched"
|
|
225
|
+
exit 0
|
|
@@ -31,7 +31,7 @@
|
|
|
31
31
|
# [--sign-app "Developer ID Application: …"]
|
|
32
32
|
# [--sign-installer "Developer ID Installer: …"]
|
|
33
33
|
# [--bundle-model] (default; include the pinned ~2.4 GB VLM)
|
|
34
|
-
# [--skip-model] (
|
|
34
|
+
# [--skip-model] (code-only upgrade; not a first installer)
|
|
35
35
|
# [--offline] (skip the official Node + ffmpeg downloads,
|
|
36
36
|
# copy the host `node`/`ffmpeg` instead — for
|
|
37
37
|
# air-gapped CI)
|
|
@@ -150,8 +150,8 @@ while [ $# -gt 0 ]; do
|
|
|
150
150
|
esac
|
|
151
151
|
done
|
|
152
152
|
|
|
153
|
-
# Offline
|
|
154
|
-
#
|
|
153
|
+
# Offline first-install builds must provide the model cache explicitly. Slim
|
|
154
|
+
# builds are the release lane's code-only upgrade artifact.
|
|
155
155
|
if [ "$OFFLINE" -eq 1 ] && [ "$BUNDLE_MODEL" -eq 1 ] && [ -z "$GEMMA_MODEL_FILE" ]; then
|
|
156
156
|
echo "--offline with the customer model bundle requires GEMMA_MODEL_FILE" >&2
|
|
157
157
|
exit 2
|
|
@@ -398,7 +398,9 @@ cp "$PKG_ROOT/deploy/macos/com.trillboards.edge.plist" "$PAYLOAD/share/com.trill
|
|
|
398
398
|
cp "$PKG_ROOT/deploy/macos/com.trillboards.litertlm.plist" "$PAYLOAD/share/com.trillboards.litertlm.plist"
|
|
399
399
|
cp "$PKG_ROOT/deploy/macos/scripts/setup-litertlm.sh" "$PAYLOAD/share/setup-litertlm.sh"
|
|
400
400
|
cp "$PKG_ROOT/deploy/macos/scripts/ensure-litertlm.sh" "$PAYLOAD/share/ensure-litertlm.sh"
|
|
401
|
-
|
|
401
|
+
cp "$PKG_ROOT/deploy/macos/apply-update.sh" "$PAYLOAD/share/apply-update.sh"
|
|
402
|
+
chmod +x "$PAYLOAD/share/setup-litertlm.sh" "$PAYLOAD/share/ensure-litertlm.sh" \
|
|
403
|
+
"$PAYLOAD/share/apply-update.sh"
|
|
402
404
|
|
|
403
405
|
# ─── 7b. Stage the pre-bundled Gemma model (.litertlm) into the payload ───────
|
|
404
406
|
# Stage the redistributable (Apache-2.0) Gemma 4 E2B LiteRT-LM model and verify
|
|
@@ -423,7 +425,7 @@ if [ "$BUNDLE_MODEL" -eq 1 ]; then
|
|
|
423
425
|
printf '%s %s\n' "$GEMMA_MODEL_SHA256" "$GEMMA_MODEL_ID" > "$PAYLOAD/share/models/$GEMMA_MODEL_ID.sha256"
|
|
424
426
|
log "Staged + SHA-verified bundled Gemma model ($(du -h "$GEMMA_OUT" | cut -f1))"
|
|
425
427
|
else
|
|
426
|
-
warn "Building
|
|
428
|
+
warn "Building the code-only upgrade package without bundled local AI; it requires an already-provisioned Mac."
|
|
427
429
|
fi
|
|
428
430
|
|
|
429
431
|
# ─── 7c. Build the native customer setup application ─────────────────────────
|
|
@@ -603,12 +605,12 @@ else
|
|
|
603
605
|
warn "cert (pass --sign-installer) then notarize (see deploy/macos/README.md)."
|
|
604
606
|
fi
|
|
605
607
|
|
|
606
|
-
# Keep
|
|
607
|
-
#
|
|
608
|
+
# Keep code-only upgrade builds bounded so an unrelated payload regression
|
|
609
|
+
# cannot turn an automatic update into another multi-gigabyte first install.
|
|
608
610
|
PKG_BYTES="$(stat -f%z "$PKG_OUT")"
|
|
609
611
|
MAX_SLIM_PKG_BYTES="${MAX_SLIM_PKG_BYTES:-262144000}"
|
|
610
612
|
if [ "$BUNDLE_MODEL" -eq 0 ] && [ "$PKG_BYTES" -gt "$MAX_SLIM_PKG_BYTES" ]; then
|
|
611
|
-
die "
|
|
613
|
+
die "code-only update package is $PKG_BYTES bytes; maximum is $MAX_SLIM_PKG_BYTES bytes"
|
|
612
614
|
fi
|
|
613
615
|
|
|
614
616
|
# ─── Done ────────────────────────────────────────────────────────────────────
|
|
@@ -8,7 +8,8 @@
|
|
|
8
8
|
# - ensure the runtime dirs exist (Application Support config dir + Logs dir)
|
|
9
9
|
# - make the bundled binaries executable (pkg payloads can lose +x)
|
|
10
10
|
# - install and health-check the bundled local AI model
|
|
11
|
-
# - open camera pairing only after local AI is ready
|
|
11
|
+
# - open camera pairing only after local AI is ready on a first install
|
|
12
|
+
# - keep an already-paired update headless
|
|
12
13
|
# - DO NOT start the LaunchAgent: there is no paired config yet, and a
|
|
13
14
|
# config-less cloud-sense crash-loops. The agent is bootstrapped only after
|
|
14
15
|
# a successful setup (the installed pairing engine does that).
|
|
@@ -42,6 +43,9 @@ fi
|
|
|
42
43
|
EDGE_ROOT="$USER_HOME/Library/Trillboards/edge"
|
|
43
44
|
APP_SUPPORT="$USER_HOME/Library/Application Support/Trillboards"
|
|
44
45
|
LOG_DIR="$USER_HOME/Library/Logs/Trillboards"
|
|
46
|
+
PAIRED_CONFIG="$APP_SUPPORT/trillboards.config.json"
|
|
47
|
+
IS_UPGRADE=0
|
|
48
|
+
[ -s "$PAIRED_CONFIG" ] && IS_UPGRADE=1
|
|
45
49
|
|
|
46
50
|
log() { /usr/bin/logger -t com.trillboards.edge "postinstall: $*" 2>/dev/null || true; printf '[trillboards postinstall] %s\n' "$*"; }
|
|
47
51
|
|
|
@@ -118,7 +122,9 @@ APPS_DIR="$USER_HOME/Applications"
|
|
|
118
122
|
SETUP_APP="$APPS_DIR/Trillboards CCTV Setup.app"
|
|
119
123
|
LEGACY_PAIR_LAUNCHER="$APPS_DIR/Trillboards Pair.command"
|
|
120
124
|
rm -f "$LEGACY_PAIR_LAUNCHER" 2>/dev/null || true
|
|
121
|
-
if [
|
|
125
|
+
if [ "$IS_UPGRADE" -eq 1 ]; then
|
|
126
|
+
log "paired connector upgrade detected; setup remains closed"
|
|
127
|
+
elif [ -d "$SETUP_APP" ] && [ -x "$EDGE_ROOT/bin/trillboards-pair" ]; then
|
|
122
128
|
log "native setup app ready: \"$SETUP_APP\" (opening it)"
|
|
123
129
|
if [ "$(id -u)" = "0" ] && [ -n "$OWNER" ] && [ "$OWNER" != "root" ]; then
|
|
124
130
|
LAUNCH_UID="$(id -u "$OWNER" 2>/dev/null || echo '')"
|
|
@@ -149,5 +155,9 @@ else
|
|
|
149
155
|
log "WARNING: launcher not executable at $EDGE_ROOT/bin/trillboards-edge"
|
|
150
156
|
fi
|
|
151
157
|
|
|
152
|
-
|
|
158
|
+
if [ "$IS_UPGRADE" -eq 1 ]; then
|
|
159
|
+
log "done. Connector update installed; existing pairing preserved."
|
|
160
|
+
else
|
|
161
|
+
log "done. Connector and local AI installed; Trillboards CCTV Setup opened automatically."
|
|
162
|
+
fi
|
|
153
163
|
exit 0
|
|
@@ -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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"}
|