- Docs
- Sensors Reference
Reference
Sensors Reference
All 37 built-in sensors, the signal phenomena each claims, the accepted source types, and the peer-dep footprint (three optional – sharp for image decode, face-api.js for faces, @xenova/transformers for scene). Plus createSensorStream for live audio.
How to read this page
The sense phase runs one or many sensors over a NavigateResult whose content is audio, video, or text bytes. Each sensor implements accepts(source) to tell the registry "I can read this," plus sense(source, options) to produce a SenseResult with typed findings.
- One optional peer (
sharp) for image sensors. The audio + text + video sensors are pure TypeScript. The 5 image sensors (v0.9) loadsharplazily on first call and throwMISSING_PEER_DEPif it isn't installed – users who don't touch images don't pay for the native bindings. The DSP primitives (FFT, autocorrelation, envelope detection, Goertzel, Hilbert, Welch, windowing, text-DSP helpers like chi-squared / Kasiski / index-of-coincidence, 2D convolution + FFT) live inpackages/core/src/sense/and every sensor composes from them. That still makes Pluck the only signal-analysis library in the JS ecosystem whose audio + text + video sensors run on Cloudflare Workers, Vercel edge, or any other runtime without native bindings. - Resolution knob. Every sensor respects
SenseOptions.resolution: "fast" | "standard" | "deep"– trade throughput for confidence. pluck.dowse()runs every sensor infastmode and ranks findings by confidence. Use it when you don't yet know what's in a signal.
See Concepts: Sense for the phase mental model and SenseResult shape.
The 37 built-in sensors
All sensors are registered in a deterministic order; pluck.sense(uri, { detect: [...] }) picks the ones whose names match your list.
Spectral analysis
Foundational frequency-domain primitives. Everything else composes from these.
| # | Sensor | What it detects | Output (in features) |
|---|---|---|---|
| 1 | fft | Magnitude spectrum of the signal window | Frequency bins + magnitudes via SenseResult.spectra |
| 2 | spectrogram | Time-frequency map (STFT). Frame count caps via hop widening on long signals | 2D magnitude array via SenseResult.spectra |
| 3 | pitch | Fundamental frequency via autocorrelation + refinement | { pitchHz, confidence } |
| 4 | tempo | BPM via onset detection + autocorrelation | { bpm, beatMs[] } |
| 5 | chromagram | 12-band pitch-class energy (key / chord detection). Floor 55 Hz | { bins: Float32Array[12], topClass: "C"|"C#"|..., confidence } |
| 6 | mfcc | Mel-Frequency Cepstral Coefficients via 26-filter mel bank + DCT-II | { coefficients: Float32Array[13], filterCount: 26 } |
Decoded signals
Decoders that read sub-perceptual data out of audio.
| # | Sensor | What it detects | Output (in decoded[] or features) |
|---|---|---|---|
| 7 | dtmf | Touch-tone dialing (8 Bell-standard frequencies) via Goertzel | { kind: "dtmf", data: "012...", startTime, endTime, confidence } |
| 8 | morse | Morse code via envelope + dit/dah timing | { kind: "morse", data: "SOS", ... } |
| 9 | fsk | Frequency-shift keying. Default Bell 103 (mark=1270, space=1070, baud=300). Optional UART 8N1 → ASCII text. Throws FSK_INVALID_OPTIONS on NaN / negative / mark≡space / above-Nyquist inputs | { bits, bitCount, text?, markHz, spaceHz, baud, confidence } |
| 10 | psk | BPSK demodulation. carrierHz required; default BPSK31 baud. Tolerates ±5 Hz carrier drift. Throws PSK_INVALID_OPTIONS on bad inputs | { bits, bitCount, carrierHz, baud, phaseJitter, confidence } |
| 11 | am-demod | AM radio demodulation | { kind: "am-demod", data: ArrayBuffer, ... } |
| 12 | fm-demod | FM radio demodulation | { kind: "fm-demod", data: ArrayBuffer, ... } |
| 13 | ssb-demod | Single-sideband radio demodulation | { kind: "ssb-demod", data: ArrayBuffer, ... } |
Band presence + diagnostic
Boolean-ish "is there energy in this band" sensors and the "is the mic live?" sanity check.
| # | Sensor | What it detects | Notes |
|---|---|---|---|
| 14 | ultrasonic | Energy above 18 kHz (cross-device tracking beacons) | Powers pluck snitch. |
| 15 | infrasonic | Energy below 20 Hz (earthquake / HVAC / wind rumble) | Rare but load-bearing for forensic work. |
| 16 | noise-floor | RMS + spectral flatness (power-domain). Returns isNoise gate | Fail-fast check before expensive downstream sensors. |
Identity
| # | Sensor | What it detects | Notes |
|---|---|---|---|
| 17 | birdsong | Bird species via trained acoustic fingerprints | Bird-only today. Broader animal coverage lives in the planned animalsong sensor. Community signature registry proposal in IDEAS.md. |
| 18 | rppg | Remote photoplethysmography – pulse rate from video frames | Powers the pluck deepfake liveness check. |
Physiological + periodicity
| # | Sensor | What it detects | Output |
|---|---|---|---|
| 19 | heartbeat | PCG / stethoscope-audio peak detection. Audio only; for heart-rate from video use rppg | { bpm, peakTimes: number[], rhythm: "regular"|"irregular"|"undetected", confidence } |
| 20 | breathing | Respiration rate from very-low-frequency envelope modulation | { bpm, intervals: number[], quality: "good"|"noisy"|"undetected", confidence } |
| 21 | periodicity | General-purpose autocorrelation – fundamental period, harmonic ratio | { periodSamples, periodSeconds, fundamentalHz, harmonicRatio, secondaryRatio, isPeriodic } |
Outlier detection
| # | Sensor | What it detects | Output (in anomalies[]) |
|---|---|---|---|
| 22 | anomaly | Outlier regions in the signal (audio dropouts, clicks, discontinuities) | { kind, start, end, confidence } |
Text domain (cipher + steganography)
Operates on text/* sources (plain text, JSON, XML, YAML, .txt / .md / .csv / .log URLs). All four enforce a 1 MB pre-DSP input cap (TEXT_TOO_LARGE) and honour AbortSignal.
| # | Sensor | What it detects | Output (in features) |
|---|---|---|---|
| 23 | cipher-classify | Fingerprints unknown ciphertext: Caesar / Vigenère / base64 / base32 / hex / URL-encoded / JWT | { family, confidence, metrics: { indexOfCoincidence, bigramEntropy, length }, rationale } |
| 24 | cipher-crack-caesar | Brute-forces all 26 Caesar shifts; chi-squared against English letter frequencies | { shift, plaintext, score, confidence, candidates: CaesarCandidate[] } |
| 25 | cipher-crack-vigenere | Two-stage Vigenère attack – Kasiski key-length + per-column Caesar crack | { key, keyLength, plaintext, score, confidence, candidates: VigenereKeyCandidate[] } |
| 26 | steganography-text | Trailing-whitespace payloads, 14 zero-width code points + the full Unicode tag block (U+E0000-U+E007F – "ASCII smuggling" prompt-injection), homoglyph substitution (Cyrillic / Greek / Armenian / Latin-extended) | { detected, hits: StegoTextHit[], summary: { trailingWhitespace, zeroWidth, homoglyph }, whitespacePayload?, cleaned } |
Every StegoTextHit.offset indexes the original text so operators can surgically strip the payload.
Image domain (forensics + screen-recording detection)
Operates on image/* sources (PNG / JPEG / WebP / TIFF / GIF / BMP via the optional sharp peer). Decodes go through a two-phase cap: sharp.metadata() is checked against MAX_IMAGE_PIXELS (8192²) BEFORE .raw().toBuffer(), defending against image-bomb inputs. All five sensors honour AbortSignal inside their Sobel / FFT inner loops (poll every 64 rows).
| # | Sensor | What it detects | Output (in features) |
|---|---|---|---|
| 27 | ela | Error-Level Analysis – re-compresses at JPEG q=90, diffs per-pixel luminance | { tamperingScore, meanError, p99Error, maxError, width, height, quality } |
| 28 | heatmap | Generic Sobel-magnitude energy map, binned into an 8×8 grid | { grid, rows, cols, maxCell, meanEnergy, width, height } |
| 29 | moire | Periodic-frequency detection via 2D FFT with low-frequency mask + aspect-preserving resize | { detected, peakToMeanRatio, peak, periodPixels, confidence } |
| 30 | flicker | Horizontal-banding detection from AC-light captured by rolling-shutter cameras | { detected, dominantCycles, bandPeriodPx, peakToMeanRatio, confidence } |
| 31 | rolling-shutter | Consistent slant on vertical edges – CMOS shutter signature for deepfake / composite detection | { detected, meanSlantDeg, slantStddevDeg, edgeCount, confidence } |
CV domain (ML-backed + aerial / thermal)
The v0.10 CV sensors wrap optional face-api.js / @xenova/transformers peers, lazy-loaded so consumers who don't run them don't pay for the ~40 MB install. Every sensor that runs Sobel / CCL pre-resizes to a 1024-longest-edge analysis cap; detection boxes + reported dimensions are scaled back to the ORIGINAL image space so consumer overlays always align.
| # | Sensor | What it detects | Output (in features) |
|---|---|---|---|
| 32 | faces | Face bounding boxes + 68-point landmarks via face-api.js. Single-frame liveness heuristic cross-references Sobel edge density against face-api's confidence | { count, faces: DetectedFace[], maxLiveness, width, height } |
| 33 | scene | Image classification via @xenova/transformers (default Xenova/vit-base-patch16-224; overridable via SceneSensorOptions.model) | { topLabel, topScore, predictions: ScenePrediction[], width, height } |
| 34 | ocr-text-regions | Pre-OCR text-region detection – Sobel horizontal-gradient + morphological closing + CCL → ranked bounding boxes (no peer) | { regions: TextRegion[], imageWidth, imageHeight } |
| 35 | animalsong | Bioacoustic ID beyond birds: frogs, crickets, cicadas, bats (ultrasonic – skipped below Nyquist), mammal calls (audio sensor) | { matches: AnimalsongMatch[], topMatch } |
| 36 | thermal | IR / FLIR hotspot detection – p95-threshold segmentation + stddev-based dynamic-range gate | { hotspots: Hotspot[], meanIntensity, hotspotThreshold, width, height } |
| 37 | ground-anomaly | Satellite / aerial two-mode: single-image NDVI surrogate OR two-frame change-detection via SenseOptions.reference | { mode, anomalyFraction, blobs: GroundBlob[], width, height } |
The image foundation module (toLuminance, applyKernel, sobelMagnitude, fft2d, connectedComponents, sourceToImage, resizeImage, resizeToAnalysisMax, recompressJpeg, MAX_IMAGE_PIXELS, MAX_FFT_PADDED_PIXELS, CV_ANALYSIS_MAX_DIM) is exported for consumers building custom image sensors – defineSensor({ name, accepts, sense }) composes as naturally as it does for audio / text.
Live streaming – createSensorStream
For mic / SDR / SIP / live-tail sources, the streaming primitive runs the same sensors over an incoming ReadableStream<Float32Array> and emits per-window events.
import { createSensorStream } from "@sizls/pluck";
const stream = createSensorStream(micAudio, {
sampleRate: 44_100,
detect: ["fft", "heartbeat", "breathing"],
windowSize: 4096, // default
hop: 2048, // default = 50% overlap
signal: abortSignal, // optional; aborts both directions
});
for await (const event of stream) {
// { time: number, feature: SenseFeature, result: Partial<SenseFeatures> }
}
Memory-bounded (rolling buffer; ~40 KB peak regardless of stream length), backpressured (highWaterMark: 1 queuing strategy), abort-aware. Non-audio sensors like rppg are silently skipped on audio streams.
Planned sensors (not yet shipped)
PlannedSenseFeature is a tag union in the type layer; calling pluck.sense(uri, { detect: ["watermark"] }) today throws NO_SENSOR. Check availability before calling:
pluck.sensors.whichHandles("dtmf"); // "dtmf"
pluck.sensors.whichHandles("flicker"); // "flicker" (v0.9 – now ships)
pluck.sensors.whichHandles("watermark"); // undefined – planned, not shipped
Tracked: watermark (content-ID style spectral watermarks), voiceprint (speaker identity), engine (RPM / cylinder fingerprint), seismic (earthquake precursors), animalsong (broader-than-birds bioacoustic coverage), steganography-image (LSB channel extraction – v0.9 ships ELA which covers the tampering half of the image-stego diagnostic surface; the pure LSB decoder is still planned).
Custom sensors
import { createPluck, defineSensor } from "@sizls/pluck";
import { goertzel } from "@sizls/pluck/dsp"; // re-exported DSP primitive
const lisnr = defineSensor({
name: "lisnr",
accepts: (source) => source.contentType.startsWith("audio/"),
sense(source, options) {
// Proprietary Lisnr ultrasonic beacon decoder
const { sampleRate, samples } = readAudioSource(source);
const decoded = decodeLisnrFrames(samples, sampleRate);
return {
features: { lisnr: decoded },
confidence: decoded.length > 0 ? 0.9 : 0.0,
decoded: decoded.map((d) => ({
kind: "lisnr",
data: d.payload,
startTime: d.t0,
endTime: d.t1,
confidence: d.snr,
})),
};
},
});
const pluck = createPluck({ sensors: [lisnr] });
await pluck.sense("./store-visit.wav", { detect: ["lisnr"] });
Custom sensors prepend to the registry. The community signature registry proposal (@sizls/signatures-*) in IDEAS makes the pattern a shipping primitive – each npm package declares patterns the main sense API picks up at registration.
pluck.dowse() – zero-config scan
When you don't know what's in a signal, dowse is the one-liner:
import { pluck } from "@sizls/pluck";
const scan = await pluck.dowse("./mystery.wav");
console.log(scan.topFinding?.summary);
// → 'Decoded dtmf: "0123456789"'
for (const finding of scan.findings) {
console.log(`${finding.sensor} ${finding.confidence.toFixed(2)} ${finding.summary}`);
}
dowse runs every shipped sensor at resolution: "fast", returns findings sorted by confidence descending, and aliases findings[0] as topFinding. See Concepts: Sense → pluck.dowse().
Introspection
import { createPluck } from "@sizls/pluck";
const pluck = createPluck();
pluck.sensors.list();
// ["fft", "spectrogram", "dtmf", "pitch", "tempo", ...]
pluck.sensors.whichHandles("dtmf");
// "dtmf"
pluck.sensors.find("dtmf", audioSource);
// Sensor – narrows by name AND accepts() probe
What's next
- Concepts: Sense – phase model,
SenseResult, DSP primitive catalog. - Recipe: Snitch Privacy – composing
ultrasonic+fft+spectrograminto a forensic privacy audit. - IDEAS backlog – Sense Receipts,
pluck.senseDiff, the Community Signature Registry.