Compare commits

...

10 Commits

Author SHA1 Message Date
github-actions[bot] 8380046a8a chore: update vendor submodules to latest main 2026-03-13 18:16:16 +00:00
ruv a467dfed9f docs: ADR-061 QEMU ESP32-S3 firmware testing platform (9 layers)
Comprehensive QEMU emulation strategy for ESP32-S3 CSI node firmware:
- Layer 1: Mock CSI generator with 10 test scenarios
- Layer 2: QEMU runner + CI workflow with NVS matrix
- Layer 3: Multi-node mesh simulation (TAP networking)
- Layer 4: GDB remote debugging (zero-cost, no JTAG)
- Layer 5: Code coverage (gcov/lcov)
- Layer 6: Fuzz testing (libFuzzer for CSI parser, NVS, WASM)
- Layer 7: NVS provisioning matrix (14 configs)
- Layer 8: Snapshot & replay (<100ms restore)
- Layer 9: Chaos testing (9 fault injection scenarios)

Co-Authored-By: claude-flow <ruv@ruv.net>
2026-03-13 09:02:09 -04:00
rUv d793c1f49f feat(firmware): --channel and --filter-mac provisioning (ADR-060)
- provision.py: add --channel (CSI channel override) and --filter-mac
  (AA:BB:CC:DD:EE:FF format) arguments with validation
- nvs_config: add csi_channel, filter_mac[6], filter_mac_set fields;
  read from NVS on boot
- csi_collector: auto-detect AP channel when no NVS override is set;
  filter CSI frames by source MAC when filter_mac is configured
- ADR-060 documents the design and rationale

Fixes #247, fixes #229
2026-03-13 08:27:08 -04:00
ruv 3457610c9f brand: rename DensePose to RuView in pose-fusion UI
Co-Authored-By: claude-flow <ruv@ruv.net>
2026-03-12 21:55:09 -04:00
ruv e9d5ea3ad3 style: add spacing between tagline and demo links in README
Co-Authored-By: claude-flow <ruv@ruv.net>
2026-03-12 21:47:31 -04:00
ruv 9cefb32815 fix(demo): add radial gradient background to camera prompt overlay
Co-Authored-By: claude-flow <ruv@ruv.net>
2026-03-12 21:38:17 -04:00
ruv a7c74e0c57 fix(demo): guard RuVector pipeline stats against undefined values
Co-Authored-By: claude-flow <ruv@ruv.net>
2026-03-12 21:32:02 -04:00
ruv 98a2b0462c fix(demo): bump import cache busters to v=13 to prevent stale modules
Co-Authored-By: claude-flow <ruv@ruv.net>
2026-03-12 21:25:46 -04:00
ruv e5e3d42ca2 fix(demo): guard toFixed on undefined rssiDbm and handle Blob WebSocket data
- Add null-safe optional chaining for embPoints and rssiDbm in diagnostic log
- Handle Blob data in _handleLiveFrame (convert to ArrayBuffer before processing)
- Bump cache busters to v=13

Co-Authored-By: claude-flow <ruv@ruv.net>
2026-03-12 21:16:29 -04:00
rUv 7c1351fd5d feat(demo): wire all 6 RuVector WASM attention mechanisms into pose fusion
* feat: dual-modal WASM browser pose estimation demo (ADR-058)

Live webcam video + WiFi CSI fusion for real-time pose estimation.
Two parallel CNN pipelines (ruvector-cnn-wasm) with attention-weighted
fusion and dynamic confidence gating. Three modes: Dual, Video-only,
CSI-only. Includes pre-built WASM package (~52KB) for browser deployment.

- ADR-058: Dual-modal architecture design
- ui/pose-fusion.html: Main demo page with dark theme UI
- 7 JS modules: video-capture, csi-simulator, cnn-embedder, fusion-engine,
  pose-decoder, canvas-renderer, main orchestrator
- Pre-built ruvector-cnn-wasm WASM package for browser
- CSI heatmap, embedding space visualization, latency metrics
- WebSocket support for live ESP32 CSI data
- Navigation link added to main dashboard

Co-Authored-By: claude-flow <ruv@ruv.net>

* fix: motion-responsive skeleton + through-wall CSI tracking

- Pose decoder now uses per-cell motion grid to track actual arm/head
  positions — raising arms moves the skeleton's arms, head follows
  lateral movement
- Motion grid (10x8 cells) tracks intensity per body zone: head,
  left/right arm upper/mid, legs
- Through-wall mode: when person exits frame, CSI maintains presence
  with slow decay (~10s) and skeleton drifts in exit direction
- CSI simulator persists sensing after video loss, ghost pose renders
  with decreasing confidence
- Reduced temporal smoothing (0.45) for faster response to movement

Co-Authored-By: claude-flow <ruv@ruv.net>

* fix: video fills available space + correct WASM path resolution

- Remove fixed aspect-ratio and max-height from video panel so it
  fills the available viewport space without scrolling
- Grid uses 1fr row for content area, overflow:hidden on main grid
- Fix WASM path: resolve relative to JS module file using import.meta.url
  instead of hardcoded ./pkg/ which resolved incorrectly on gh-pages
- Responsive: mobile still gets aspect-ratio constraint

Co-Authored-By: claude-flow <ruv@ruv.net>

* feat: live ESP32 CSI pipeline + auto-connect WebSocket

- Add auto-connect to local sensing server WebSocket (ws://localhost:8765)
- Demo shows "Live ESP32" when connected to real CSI data
- Add build_firmware.ps1 for native Windows ESP-IDF builds (no Docker)
- Add read_serial.ps1 for ESP32 serial monitor

Pipeline: ESP32 → UDP:5005 → sensing-server → WS:8765 → browser demo

Co-Authored-By: claude-flow <ruv@ruv.net>

* docs: add ADR-059 live ESP32 CSI pipeline + update README with demo links

- ADR-059: Documents end-to-end ESP32 → sensing server → browser pipeline
- README: Add dual-modal pose fusion demo link, update ADR count to 49
- References issue #245

Co-Authored-By: claude-flow <ruv@ruv.net>

* feat: RSSI visualization, RuVector attention WASM, cache-bust fixes

- Add animated RSSI Signal Strength panel with sparkline history
- Fix RuVector WasmMultiHeadAttention retptr calling convention
- Wire up RuVector Multi-Head + Flash Attention in CNN embedder
- Add ambient temporal drift to CSI simulator for visible heatmap animation
- Fix embedding space projection (sparse projection replaces cancelling sum)
- Add auto-scaling to embedding space renderer
- Add cache busters (?v=4) to all ES module imports to prevent stale caches
- Add diagnostic logging for module version verification
- Add RSSI tracking with quality labels and color-coded dBm display
- Includes ruvector-attention-wasm v2.0.5 browser ESM wrapper

Co-Authored-By: claude-flow <ruv@ruv.net>

* feat: 26-keypoint dexterous pose + full RuVector attention pipeline

Pose Decoder (17 → 26 keypoints):
- Add finger approximations: thumb, index, pinky per hand (6 new)
- Add toe tips: left/right foot index (2 new)
- Add neck keypoint (1 new)
- Hand openness driven by arm motion intensity
- Finger positions computed from wrist-elbow axis angles

CNN Embedder (full RuVector WASM pipeline):
- Stage 1: Multi-Head Attention (global spatial reasoning)
- Stage 2: Hyperbolic Attention (hierarchical body-part tree)
- Stage 3: MoE Attention (3 experts: upper/lower/extremities, top-2)
- Blended 40/30/30 weighting → final embedding projection

Canvas Renderer:
- Magenta finger joints with distinct glow
- Cyan toe tips
- White neck keypoint
- Thinner limb lines for hand/foot connections
- Joint count shown in overlay label

CSI Simulator:
- Skip synthetic person state when live ESP32 connected
- Only simulate CSI data in demo mode (was already correct)

Embedding Space:
- Fixed projection: sparse 8-dim projection replaces cancelling sum
- Auto-scaling normalizes point spread to fill canvas

Cache busters bumped to v=5 on all imports.

Co-Authored-By: claude-flow <ruv@ruv.net>

* fix: centroid-based pose tracking for responsive limb movement

Rewrites pose decoder from intensity-based to position-based tracking:
- Arms now track toward motion centroid in each body zone
- Elbow/wrist positions computed along shoulder→centroid vector
- Legs track toward lower-body zone centroids
- Smoothing reduced from 0.45 to 0.25 for responsiveness
- Zone centroids blend 30% old / 70% new each frame

6 body zones with overlapping coverage:
- Head (top 20%, center cols)
- Left/Right Arm (rows 10-60%, outer cols)
- Torso (rows 15-55%, center cols)
- Left/Right Leg (rows 50-100%, half cols each)

Hand openness now driven by arm spread distance + raise amount.
Cache busters v=6.

Co-Authored-By: claude-flow <ruv@ruv.net>

* fix: remove duplicate lAnkleX/rAnkleX declarations in pose-decoder

Stale code block from old intensity-based tracking was left behind,
re-declaring variables already defined by centroid-based tracking.

Co-Authored-By: claude-flow <ruv@ruv.net>

* feat(demo): wire all 6 RuVector WASM attention mechanisms into pose fusion

- Add WasmLinearAttention and WasmLocalGlobalAttention to browser ESM wrapper
- Add 6 WASM utility functions (batch_normalize, pairwise_distances, etc.)
- Extend CnnEmbedder to 6-stage pipeline: Flash → MHA → Hyperbolic → Linear → MoE → L+G
- Use log-energy softmax blending across all 6 stages
- Wire WASM cosine_similarity and normalize into FusionEngine
- Add RuVector pipeline stats panel to UI (energy, refinement, pose impact)
- Compute embedding-to-joint mapping stats without modifying joint positions
- Center camera prompt with flexbox layout
- Add cache busters v=12

Co-Authored-By: claude-flow <ruv@ruv.net>
2026-03-12 20:59:57 -04:00
34 changed files with 8466 additions and 5 deletions
+6 -2
View File
@@ -75,7 +75,7 @@ docker run -p 3000:3000 ruvnet/wifi-densepose:latest
|----------|-------------|
| [User Guide](docs/user-guide.md) | Step-by-step guide: installation, first run, API usage, hardware setup, training |
| [Build Guide](docs/build-guide.md) | Building from source (Rust and Python) |
| [Architecture Decisions](docs/adr/README.md) | 48 ADRs — why each technical choice was made, organized by domain (hardware, signal processing, ML, platform, infrastructure) |
| [Architecture Decisions](docs/adr/README.md) | 49 ADRs — why each technical choice was made, organized by domain (hardware, signal processing, ML, platform, infrastructure) |
| [Domain Models](docs/ddd/README.md) | 7 DDD models (RuvSense, Signal Processing, Training Pipeline, Hardware Platform, Sensing Server, WiFi-Mat, CHCI) — bounded contexts, aggregates, domain events, and ubiquitous language |
| [Desktop App](rust-port/wifi-densepose-rs/crates/wifi-densepose-desktop/README.md) | **WIP** — Tauri v2 desktop app for node management, OTA updates, WASM deployment, and mesh visualization |
@@ -87,10 +87,14 @@ docker run -p 3000:3000 ruvnet/wifi-densepose:latest
</a>
<br>
<em>Real-time pose skeleton from WiFi CSI signals — no cameras, no wearables</em>
<br>
<br><br>
<a href="https://ruvnet.github.io/RuView/"><strong>▶ Live Observatory Demo</strong></a>
&nbsp;|&nbsp;
<a href="https://ruvnet.github.io/RuView/pose-fusion.html"><strong>▶ Dual-Modal Pose Fusion Demo</strong></a>
> The [server](#-quick-start) is optional for visualization and aggregation — the ESP32 [runs independently](#esp32-s3-hardware-pipeline) for presence detection, vital signs, and fall alerts.
>
> **Live ESP32 pipeline**: Connect an ESP32-S3 node → run the [sensing server](#sensing-server) → open the [pose fusion demo](https://ruvnet.github.io/RuView/pose-fusion.html) for real-time dual-modal pose estimation (webcam + WiFi CSI). See [ADR-059](docs/adr/ADR-059-live-esp32-csi-pipeline.md).
## 🚀 Key Features
@@ -0,0 +1,392 @@
# ADR-058: Dual-Modal WASM Browser Pose Estimation — Live Video + WiFi CSI Fusion
- **Status**: Proposed
- **Date**: 2026-03-12
- **Deciders**: ruv
- **Tags**: wasm, browser, cnn, pose-estimation, ruvector, video, multimodal, fusion
## Context
WiFi-DensePose estimates human poses from WiFi CSI (Channel State Information).
The `ruvector-cnn` crate provides a pure Rust CNN (MobileNet-V3) with WASM bindings.
Both modalities exist independently — what's missing is **fusing live webcam video
with WiFi CSI** in a single browser demo to achieve robust pose estimation that
works even when one modality degrades (occlusion, signal noise, poor lighting).
Existing assets:
1. **`wifi-densepose-wasm`** — CSI signal processing compiled to WASM
2. **`wifi-densepose-sensing-server`** — Axum server streaming live CSI via WebSocket
3. **`ruvector-cnn`** — Pure Rust CNN with MobileNet-V3 backbones, SIMD, contrastive learning
4. **`ruvector-cnn-wasm`** — wasm-bindgen bindings: `WasmCnnEmbedder`, `SimdOps`, `LayerOps`, contrastive losses
5. **`vendor/ruvector/examples/wasm-vanilla/`** — Reference vanilla JS WASM example
Research shows multi-modal fusion (camera + WiFi) significantly outperforms either alone:
- Camera fails under occlusion, poor lighting, privacy constraints
- WiFi CSI fails with signal noise, multipath, low spatial resolution
- Fusion compensates: WiFi provides through-wall coverage, camera provides fine-grained detail
## Decision
Build a **dual-modal browser demo** at `examples/wasm-browser-pose/` that:
1. Captures **live webcam video** via `getUserMedia` API
2. Receives **live WiFi CSI** via WebSocket from the sensing server
3. Processes **both streams** through separate CNN pipelines in `ruvector-cnn-wasm`
4. **Fuses embeddings** with learned attention weights for combined pose estimation
5. Renders **video overlay** with skeleton + WiFi confidence heatmap on Canvas
6. Runs entirely in the browser — all inference client-side via WASM
### Architecture
```
┌──────────────────────────────────────────────────────────────────┐
│ Browser │
│ │
│ ┌────────────┐ ┌────────────────┐ ┌───────────────────┐ │
│ │ getUserMedia│───▶│ Video Frame │───▶│ CNN WASM │ │
│ │ (Webcam) │ │ Capture │ │ (Visual Embedder) │ │
│ └────────────┘ │ 224×224 RGB │ │ → 512-dim │ │
│ └────────────────┘ └────────┬──────────┘ │
│ │ │
│ visual_embedding │
│ │ │
│ ┌──────▼──────┐ │
│ ┌────────────┐ ┌────────────────┐ │ │ │
│ │ WebSocket │───▶│ CSI WASM │ │ Attention │ │
│ │ Client │ │ (densepose- │ │ Fusion │ │
│ │ │ │ wasm) │ │ Module │ │
│ └────────────┘ └───────┬────────┘ │ │ │
│ │ └──────┬──────┘ │
│ ┌───────▼────────┐ │ │
│ │ CNN WASM │ fused_embedding │
│ │ (CSI Embedder) │ │ │
│ │ → 512-dim │ ┌──────▼──────┐ │
│ └───────┬────────┘ │ Pose │ │
│ │ │ Decoder │ │
│ csi_embedding │ → 17 kpts │ │
│ │ └──────┬──────┘ │
│ └──────────────────────┘ │
│ │ │
│ ┌──────────────┐ ┌─────▼──────┐ │
│ │ Video Canvas │◀────────│ Overlay │ │
│ │ + Skeleton │ │ Renderer │ │
│ │ + Heatmap │ └────────────┘ │
│ └──────────────┘ │
│ │
└──────────────────────────────────────────────────────────────────┘
▲ ▲
│ getUserMedia │ WebSocket
│ (camera) │ (ws://host:3030/ws/csi)
│ │
┌────┴────┐ ┌───────┴─────────┐
│ Webcam │ │ Sensing Server │
└─────────┘ └─────────────────┘
```
### Dual Pipeline Design
Two parallel CNN pipelines run on each frame tick (~30 FPS):
| Pipeline | Input | Preprocessing | CNN Config | Output |
|----------|-------|---------------|------------|--------|
| **Visual** | Webcam frame (640×480) | Resize to 224×224 RGB, ImageNet normalize | MobileNet-V3 Small, 512-dim | Visual embedding |
| **CSI** | CSI frame (ADR-018 binary) | Amplitude/phase/delta → 224×224 pseudo-RGB | MobileNet-V3 Small, 512-dim | CSI embedding |
Both use the same `WasmCnnEmbedder` but with separate instances and weight sets.
### Fusion Strategy
**Learned attention-weighted fusion** combines the two 512-dim embeddings:
```javascript
// Attention fusion: learn which modality to trust per-dimension
// α ∈ [0,1]^512 — attention weights (shipped as JSON, trained offline)
// visual_emb, csi_emb ∈ R^512
function fuseEmbeddings(visual_emb, csi_emb, attention_weights) {
const fused = new Float32Array(512);
for (let i = 0; i < 512; i++) {
const α = attention_weights[i];
fused[i] = α * visual_emb[i] + (1 - α) * csi_emb[i];
}
return fused;
}
```
**Dynamic confidence gating** adjusts fusion based on signal quality:
| Condition | Behavior |
|-----------|----------|
| Good video + good CSI | Balanced fusion (α ≈ 0.5) |
| Poor lighting / occlusion | CSI-dominant (α → 0, WiFi takes over) |
| CSI noise / no ESP32 | Video-dominant (α → 1, camera only) |
| Video-only mode (no WiFi) | α = 1.0, pure visual CNN pose estimation |
| CSI-only mode (no camera) | α = 0.0, pure WiFi pose estimation |
Quality detection:
- **Video quality**: Frame brightness variance (dark = low quality), motion blur score
- **CSI quality**: Signal-to-noise ratio from `wifi-densepose-wasm`, coherence gate output
### CSI-to-Image Encoding
CSI data encoded as 3-channel pseudo-image for the CSI CNN pipeline:
| Channel | Data | Normalization |
|---------|------|---------------|
| R | CSI amplitude (subcarrier × time window) | Min-max to [0, 255] |
| G | CSI phase (unwrapped, subcarrier × time window) | Min-max to [0, 255] |
| B | Temporal difference (frame-to-frame Δ amplitude) | Abs, min-max to [0, 255] |
### Video Processing
Webcam frames processed through standard ImageNet pipeline:
```javascript
// Capture frame from video element
const frame = captureVideoFrame(videoElement, 224, 224); // Returns Uint8Array RGB
// ImageNet normalization happens inside WasmCnnEmbedder.extract()
const visual_embedding = visual_embedder.extract(frame, 224, 224);
```
### Pose Keypoint Mapping
17 COCO-format keypoints decoded from the fused 512-dim embedding:
```
0: nose 1: left_eye 2: right_eye
3: left_ear 4: right_ear 5: left_shoulder
6: right_shoulder 7: left_elbow 8: right_elbow
9: left_wrist 10: right_wrist 11: left_hip
12: right_hip 13: left_knee 14: right_knee
15: left_ankle 16: right_ankle
```
Each keypoint decoded as (x, y, confidence) = 51 values from the 512-dim embedding
via a learned linear projection.
### Operating Modes
The demo supports three modes, selectable in the UI:
| Mode | Video | CSI | Fusion | Use Case |
|------|-------|-----|--------|----------|
| **Dual (default)** | ✅ | ✅ | Attention-weighted | Best accuracy, full demo |
| **Video Only** | ✅ | ❌ | α = 1.0 | No ESP32 available, quick demo |
| **CSI Only** | ❌ | ✅ | α = 0.0 | Privacy mode, through-wall sensing |
**Video Only mode works without any hardware** — just a webcam — making the demo
instantly accessible for anyone wanting to try it.
### File Layout
```
examples/wasm-browser-pose/
├── index.html # Single-page app (vanilla JS, no bundler)
├── js/
│ ├── app.js # Main entry, mode selection, orchestration
│ ├── video-capture.js # getUserMedia, frame extraction, quality detection
│ ├── csi-processor.js # WebSocket CSI client, frame parsing, pseudo-image encoding
│ ├── fusion.js # Attention-weighted embedding fusion, confidence gating
│ ├── pose-decoder.js # Fused embedding → 17 keypoints
│ └── canvas-renderer.js # Video overlay, skeleton, CSI heatmap, confidence bars
├── data/
│ ├── visual-weights.json # Visual CNN → embedding projection (placeholder until trained)
│ ├── csi-weights.json # CSI CNN → embedding projection (placeholder until trained)
│ ├── fusion-weights.json # Attention fusion α weights (512 values)
│ └── pose-weights.json # Fused embedding → keypoint projection
├── css/
│ └── style.css # Dark theme UI styling
├── pkg/ # Built WASM packages (gitignored, built by script)
│ ├── wifi_densepose_wasm/
│ └── ruvector_cnn_wasm/
├── build.sh # wasm-pack build script for both packages
└── README.md # Setup and usage instructions
```
### Build Pipeline
```bash
#!/bin/bash
# build.sh — builds both WASM packages into pkg/
set -e
# Build wifi-densepose-wasm (CSI processing)
wasm-pack build ../../rust-port/wifi-densepose-rs/crates/wifi-densepose-wasm \
--target web --out-dir "$(pwd)/pkg/wifi_densepose_wasm" --no-typescript
# Build ruvector-cnn-wasm (CNN inference for both video and CSI)
wasm-pack build ../../vendor/ruvector/crates/ruvector-cnn-wasm \
--target web --out-dir "$(pwd)/pkg/ruvector_cnn_wasm" --no-typescript
echo "Build complete. Serve with: python3 -m http.server 8080"
```
### UI Layout
```
┌─────────────────────────────────────────────────────────┐
│ WiFi-DensePose — Live Dual-Modal Pose Estimation │
│ [Dual Mode ▼] [⚙ Settings] FPS: 28 ◉ Live │
├───────────────────────────┬─────────────────────────────┤
│ │ │
│ ┌───────────────────┐ │ ┌───────────────────┐ │
│ │ │ │ │ │ │
│ │ Video + Skeleton │ │ │ CSI Heatmap │ │
│ │ Overlay │ │ │ (amplitude × │ │
│ │ (main canvas) │ │ │ subcarrier) │ │
│ │ │ │ │ │ │
│ └───────────────────┘ │ └───────────────────┘ │
│ │ │
├───────────────────────────┴─────────────────────────────┤
│ Fusion Confidence: ████████░░ 78% │
│ Video: ██████████ 95% │ CSI: ██████░░░░ 61% │
├─────────────────────────────────────────────────────────┤
│ ┌─────────────────────────────────────────────────┐ │
│ │ Embedding Space (2D projection) │ │
│ │ · · · │ │
│ │ · · · · · · (color = pose cluster) │ │
│ │ · · · · │ │
│ └─────────────────────────────────────────────────┘ │
├─────────────────────────────────────────────────────────┤
│ Latency: Video 12ms │ CSI 8ms │ Fusion 1ms │ Total 21ms│
│ [▶ Record] [📷 Snapshot] [Confidence: ████ 0.6] │
└─────────────────────────────────────────────────────────┘
```
### WASM Module Structure
| Package | Source Crate | Provides | Size (est.) |
|---------|-------------|----------|-------------|
| `wifi_densepose_wasm` | `wifi-densepose-wasm` | CSI frame parsing, signal processing, feature extraction | ~200KB |
| `ruvector_cnn_wasm` | `ruvector-cnn-wasm` | `WasmCnnEmbedder` (×2 instances), `SimdOps`, `LayerOps`, contrastive losses | ~150KB |
Two `WasmCnnEmbedder` instances are created — one for video frames, one for CSI pseudo-images.
They share the same WASM module but have independent state.
### Browser API Requirements
| API | Purpose | Required | Fallback |
|-----|---------|----------|----------|
| `getUserMedia` | Webcam capture | For video mode | CSI-only mode |
| WebAssembly | CNN inference | Yes | None (hard requirement) |
| WASM SIMD128 | Accelerated inference | No | Scalar fallback (~2× slower) |
| WebSocket | CSI data stream | For CSI mode | Video-only mode |
| Canvas 2D | Rendering | Yes | None |
| `requestAnimationFrame` | Render loop | Yes | `setTimeout` fallback |
| ES Modules | Code organization | Yes | None |
Target: Chrome 89+, Firefox 89+, Safari 15+, Edge 89+
### Performance Budget
| Stage | Target Latency | Notes |
|-------|---------------|-------|
| Video frame capture + resize | <3ms | `drawImage` to offscreen canvas |
| Video CNN embedding | <15ms | 224×224 RGB → 512-dim |
| CSI receive + parse | <2ms | Binary WebSocket message |
| CSI pseudo-image encoding | <3ms | Amplitude/phase/delta channels |
| CSI CNN embedding | <15ms | 224×224 pseudo-RGB → 512-dim |
| Attention fusion | <1ms | Element-wise weighted sum |
| Pose decoding | <1ms | Linear projection |
| Canvas overlay render | <3ms | Video + skeleton + heatmap |
| **Total (dual mode)** | **<33ms** | **30 FPS capable** |
| **Total (video only)** | **<22ms** | **45 FPS capable** |
Note: Video and CSI CNN pipelines can run in parallel using Web Workers,
reducing dual-mode latency to ~max(15, 15) + 5 = ~20ms (50 FPS).
### Contrastive Learning Integration
The demo optionally shows real-time contrastive learning in the browser:
- **InfoNCE loss** (`WasmInfoNCELoss`): Compare video vs CSI embeddings for the same pose — trains cross-modal alignment
- **Triplet loss** (`WasmTripletLoss`): Push apart different poses, pull together same pose across modalities
- **SimdOps**: Accelerated dot products for real-time similarity computation
- **Embedding space panel**: Live 2D projection shows video and CSI embeddings converging when viewing the same person
### Relationship to Existing Crates
| Existing Crate | Role in This Demo |
|---------------|-------------------|
| `ruvector-cnn-wasm` | CNN inference for **both** video frames and CSI pseudo-images |
| `wifi-densepose-wasm` | CSI frame parsing and signal processing |
| `wifi-densepose-sensing-server` | WebSocket CSI data source |
| `wifi-densepose-core` | ADR-018 frame format definitions |
| `ruvector-cnn` | Underlying MobileNet-V3, layers, contrastive learning |
No new Rust crates are needed. The example is pure HTML/JS consuming existing WASM packages.
## Consequences
### Positive
- **Instant demo**: Video-only mode works with just a webcam — no ESP32 needed
- **Multi-modal showcase**: Demonstrates camera + WiFi fusion, the core innovation of the project
- **Graceful degradation**: Works with video-only, CSI-only, or both
- **Through-wall capability**: CSI mode shows pose estimation where cameras cannot reach
- **Zero-install**: Anyone with a browser can try it
- **Training data collection**: Can record paired (video, CSI) data for offline model training
- **Reusable**: JS modules embed directly in the Tauri desktop app's webview
### Negative
- **Model weights**: Requires offline-trained weights for visual CNN, CSI CNN, fusion, and pose decoder (~200KB total JSON)
- **WASM size**: Two WASM modules total ~350KB (acceptable)
- **No GPU**: CPU-only WASM inference; adequate at 224×224 but limits resolution scaling
- **Camera privacy**: Video mode requires camera permission (mitigated: CSI-only mode available)
- **Two CNN instances**: Memory footprint doubles vs single-modal (~10MB total, acceptable for desktop browsers)
### Risks
- **Cross-modal alignment**: Video and CSI embeddings must be trained jointly for fusion to work;
without proper training, fusion may be worse than either modality alone
- **Latency on mobile**: Dual CNN on mobile browsers may exceed 33ms; implement automatic quality reduction
- **WebSocket drops**: Network jitter → CSI frame gaps; buffer last 3 frames, interpolate missing data
## Implementation Plan
1. **Phase 1 — Scaffold**: File layout, build.sh, index.html shell, mode selector UI
2. **Phase 2 — Video pipeline**: getUserMedia → frame capture → CNN embedding → basic pose display
3. **Phase 3 — CSI pipeline**: WebSocket client → CSI parsing → pseudo-image → CNN embedding
4. **Phase 4 — Fusion**: Attention-weighted combination, confidence gating, mode switching
5. **Phase 5 — Pose decoder**: Linear projection with placeholder weights → 17 keypoints
6. **Phase 6 — Overlay renderer**: Video canvas with skeleton overlay, CSI heatmap panel
7. **Phase 7 — Training**: Use `wifi-densepose-train` to generate real weights for both CNNs + fusion + decoder
8. **Phase 8 — Contrastive demo**: Embedding space visualization, cross-modal similarity display
9. **Phase 9 — Web Workers**: Move CNN inference to workers for parallel video + CSI processing
10. **Phase 10 — Polish**: Recording, snapshots, adaptive quality, mobile optimization
## Alternatives Considered
### 1. CSI-Only (No Video)
Rejected: Misses the opportunity to show multi-modal fusion and makes the demo less
accessible (requires ESP32 hardware). Video-only mode as a fallback is strictly better.
### 2. Server-Side Video Inference
Rejected: Adds latency, requires webcam stream upload (privacy concern), and defeats
the WASM-first architecture. All inference must be client-side.
### 3. TensorFlow.js for Video, ruvector-cnn-wasm for CSI
Rejected: Would require two different ML frameworks. Using `ruvector-cnn-wasm` for both
keeps a single WASM module, unified embedding space, and simpler fusion.
### 4. Pre-recorded Video Demo
Rejected: Live webcam input is far more compelling for demonstrations.
Pre-recorded mode can be added as a secondary option.
### 5. React/Vue Framework
Rejected: Adds build tooling. Vanilla JS + ES modules keeps the demo self-contained.
## References
- [ADR-018: Binary CSI Frame Format](ADR-018-binary-csi-frame-format.md)
- [ADR-024: Contrastive CSI Embedding / AETHER](ADR-024-contrastive-csi-embedding.md)
- [ADR-055: Integrated Sensing Server](ADR-055-integrated-sensing-server.md)
- `vendor/ruvector/crates/ruvector-cnn/src/lib.rs` — CNN embedder implementation
- `vendor/ruvector/crates/ruvector-cnn-wasm/src/lib.rs` — WASM bindings
- `vendor/ruvector/examples/wasm-vanilla/index.html` — Reference vanilla JS WASM pattern
- Person-in-WiFi: Fine-grained Person Perception using WiFi (ICCV 2019) — camera+WiFi fusion precedent
- WiPose: Multi-Person WiFi Pose Estimation (TMC 2022) — cross-modal embedding approach
@@ -0,0 +1,83 @@
# ADR-059: Live ESP32 CSI Pipeline Integration
## Status
Accepted
## Date
2026-03-12
## Context
ADR-058 established a dual-modal browser demo combining webcam video and WiFi CSI for pose estimation. However, it used simulated CSI data. To demonstrate real-world capability, we need an end-to-end pipeline from physical ESP32 hardware through to the browser visualization.
The ESP32-S3 firmware (`firmware/esp32-csi-node/`) already supports CSI collection and UDP streaming (ADR-018). The sensing server (`wifi-densepose-sensing-server`) already supports UDP ingestion and WebSocket bridging. The missing piece was connecting these components and enabling the browser demo to consume live data.
## Decision
Implement a complete live CSI pipeline:
```
ESP32-S3 (CSI capture) → UDP:5005 → sensing-server (Rust/Axum) → WS:8765 → browser demo
```
### Components
1. **ESP32 Firmware** — Rebuilt with native Windows ESP-IDF v5.4.0 toolchain (no Docker). Configured for target network and PC IP via `sdkconfig`. Helper scripts added:
- `build_firmware.ps1` — Sets up IDF environment, cleans, builds, and flashes
- `read_serial.ps1` — Serial monitor with DTR/RTS reset capability
2. **Sensing Server**`wifi-densepose-sensing-server` started with:
- `--source esp32` — Expect real ESP32 UDP frames
- `--bind-addr 0.0.0.0` — Accept connections from any interface
- `--ui-path <path>` — Serve the demo UI via HTTP
3. **Browser Demo**`main.js` updated to auto-connect to `ws://localhost:8765/ws/sensing` on page load. Falls back to simulated CSI if the WebSocket is unavailable (GitHub Pages).
### Network Configuration
The ESP32 sends UDP packets to a configured target IP. If the PC's IP doesn't match the firmware's compiled target, a secondary IP alias can be added:
```powershell
# PowerShell (Admin)
New-NetIPAddress -IPAddress 192.168.1.100 -PrefixLength 24 -InterfaceAlias "Wi-Fi"
```
### Data Flow
| Stage | Protocol | Format | Rate |
|-------|----------|--------|------|
| ESP32 → Server | UDP | ADR-018 binary frame (magic `0xC5110001`, I/Q pairs) | ~100 Hz |
| Server → Browser | WebSocket | ADR-018 binary frame (forwarded) | ~10 Hz (tick-ms=100) |
| Browser decode | JavaScript | Float32 amplitude/phase arrays | Per frame |
### Build Environment (Windows)
ESP-IDF v5.4.0 on Windows requires:
- IDF_PATH pointing to the ESP-IDF framework
- IDF_TOOLS_PATH pointing to toolchain binaries
- MSYS/MinGW environment variables removed (ESP-IDF rejects them)
- Python venv from ESP-IDF tools for `idf.py` execution
The `build_firmware.ps1` script handles all of this automatically.
## Consequences
### Positive
- First end-to-end demonstration of real WiFi CSI → pose estimation in a browser
- No Docker required for firmware builds on Windows
- Demo gracefully degrades to simulated CSI when no server is available
- Same demo works on GitHub Pages (simulated) and locally (live ESP32)
### Negative
- ESP32 target IP is compiled into firmware; changing it requires a rebuild or NVS override
- Windows firewall may block UDP:5005; user must allow it
- Mixed content restrictions prevent HTTPS pages from connecting to ws:// (local only)
## Related
- [ADR-018](ADR-018-esp32-dev-implementation.md) — ESP32 CSI frame format and UDP streaming
- [ADR-058](ADR-058-ruvector-wasm-browser-pose-example.md) — Dual-modal WASM browser pose demo
- [ADR-039](ADR-039-edge-intelligence-framework.md) — Edge intelligence on ESP32
- Issue [#245](https://github.com/ruvnet/RuView/issues/245) — Tracking issue
@@ -0,0 +1,59 @@
# ADR-060: Provision Channel Override and MAC Address Filtering
- **Status:** Accepted
- **Date:** 2026-03-12
- **Issues:** [#247](https://github.com/ruvnet/RuView/issues/247), [#229](https://github.com/ruvnet/RuView/issues/229)
## Context
Two related provisioning gaps were reported by users:
1. **Channel mismatch (Issue #247):** The CSI collector initializes on the
Kconfig default channel (typically 6), even when the ESP32 connects to an AP
on a different channel (e.g. 11). On managed networks where the user cannot
change the router channel, this makes nodes undiscoverable. The
`provision.py` script has no `--channel` argument.
2. **Missing MAC filter (Issue #229):** The v0.2.0 release notes documented a
`--filter-mac` argument for `provision.py`, but it was never implemented.
The firmware's CSI callback accepts frames from all sources, causing signal
mixing in multi-AP environments.
## Decision
### Channel configuration
- Add `--channel` argument to `provision.py` that writes a `csi_channel` key
(u8) to NVS.
- In `nvs_config.c`, read the `csi_channel` key and override
`channel_list[0]` when present.
- In `csi_collector_init()`, after WiFi connects, auto-detect the AP channel
via `esp_wifi_sta_get_ap_info()` and use it as the default CSI channel when
no NVS override is set. This ensures the CSI collector always matches the
connected AP's channel without requiring manual provisioning.
### MAC address filtering
- Add `--filter-mac` argument to `provision.py` that writes a `filter_mac`
key (6-byte blob) to NVS.
- In `nvs_config.h`, add a `filter_mac[6]` field and `filter_mac_set` flag.
- In `nvs_config.c`, read the `filter_mac` blob from NVS.
- In the CSI callback (`wifi_csi_callback`), if `filter_mac_set` is true,
compare the source MAC from the received frame against the configured MAC
and drop non-matching frames.
### Provisioning flow
```
python provision.py --port COM7 --channel 11
python provision.py --port COM7 --filter-mac "AA:BB:CC:DD:EE:FF"
python provision.py --port COM7 --channel 11 --filter-mac "AA:BB:CC:DD:EE:FF"
```
## Consequences
- Users on managed networks can force the CSI channel to match their AP
- Multi-AP environments can filter CSI to a single source
- Auto-channel detection eliminates the most common misconfiguration
- Backward compatible: existing provisioned nodes without these keys behave
as before (use Kconfig default channel, accept all MACs)
@@ -0,0 +1,864 @@
# ADR-061: QEMU ESP32-S3 Emulation for Firmware Testing & Development
| Field | Value |
|-------------|------------------------------------------------|
| **Status** | Proposed |
| **Date** | 2026-03-13 |
| **Authors** | RuView Team |
| **Relates** | ADR-018 (binary frame), ADR-039 (edge intel), ADR-040 (WASM), ADR-057 (build guard), ADR-060 (channel/MAC filter) |
## Context
The ESP32-S3 CSI node firmware (`firmware/esp32-csi-node/`) has grown to 16 source files spanning:
| Module | File | Testable in QEMU? |
|--------|------|--------------------|
| NVS config load | `nvs_config.c` | Yes — NVS partition in flash image |
| Edge processing (DSP) | `edge_processing.c` | Yes — all math, no HW dependency |
| ADR-018 frame serialization | `csi_collector.c:csi_serialize_frame()` | Yes — pure buffer ops |
| UDP stream sender | `stream_sender.c` | Yes — QEMU has lwIP via SLIRP |
| WASM runtime | `wasm_runtime.c` | Yes — CPU only |
| OTA update | `ota_update.c` | Partial — needs HTTP mock |
| Power management | `power_mgmt.c` | Partial — no real light-sleep |
| Display (OLED) | `display_*.c` | No — I2C hardware |
| WiFi CSI callback | `csi_collector.c:wifi_csi_callback()` | **No** — requires RF PHY |
| Channel hopping | `csi_collector.c:hop_timer_cb()` | **No** — requires `esp_wifi_set_channel()` |
Currently, **every code change requires flashing to physical hardware** on COM7. This creates a bottleneck:
- Build + flash cycle: ~20 seconds
- Serial monitor: manual inspection
- No automated CI (no ESP32-S3 in GitHub Actions runners)
- Contributors without hardware cannot test firmware changes
Espressif maintains an official QEMU fork (`github.com/espressif/qemu`) with ESP32-S3 machine support, including dual-core Xtensa LX7, flash mapping, UART, GPIO, timers, and FreeRTOS.
## Decision
Introduce a **comprehensive QEMU testing platform** for the ESP32-S3 CSI node firmware with nine capability layers:
1. **Mock CSI generator** — compile-time synthetic CSI frame injection
2. **QEMU runner** — automated build, run, and validation
3. **Multi-node mesh simulation** — TDM and aggregation testing across QEMU instances
4. **GDB remote debugging** — zero-cost breakpoint debugging without JTAG
5. **Code coverage** — gcov/lcov integration for path analysis
6. **Fuzz testing** — malformed input resilience for CSI parser, NVS, WASM
7. **NVS provisioning matrix** — exhaustive config combination testing
8. **Snapshot & replay** — sub-100ms state restore for fast iteration
9. **Chaos testing** — fault injection for resilience validation
---
## Layer 1: Mock CSI Generator
### Architecture
```
┌─────────────────────────────────────────────────────┐
│ ESP32-S3 Firmware │
│ │
│ ┌─────────────┐ ┌──────────────────────────────┐ │
│ │ Real WiFi │ │ Mock CSI Generator │ │
│ │ CSI Callback │ OR │ (timer → synthetic frames) │ │
│ │ (HW only) │ │ (QEMU + unit tests) │ │
│ └──────┬───────┘ └──────────┬───────────────────┘ │
│ │ │ │
│ └───────────┬───────────┘ │
│ ▼ │
│ ┌──────────────────────────────────────────────────┐ │
│ │ edge_enqueue_csi() → SPSC ring → DSP Core 1 │ │
│ │ ├── Biquad bandpass (breathing / heart rate) │ │
│ │ ├── Phase unwrapping + Welford stats │ │
│ │ ├── Top-K subcarrier selection │ │
│ │ ├── Presence detection (adaptive threshold) │ │
│ │ ├── Fall detection (phase acceleration) │ │
│ │ └── Multi-person vitals clustering │ │
│ └──────────────────┬───────────────────────────────┘ │
│ ▼ │
│ ┌──────────────────────────────────────────────────┐ │
│ │ csi_serialize_frame() → ADR-018 binary format │ │
│ │ stream_sender_send() → UDP to aggregator │ │
│ │ edge vitals packet → 0xC5110002 (32 bytes) │ │
│ └──────────────────────────────────────────────────┘ │
└─────────────────────────────────────────────────────┘
```
### Mock CSI Generator Design
When `CONFIG_CSI_MOCK_ENABLED=y` (Kconfig option), the build replaces `esp_wifi_set_csi_config()` / `esp_wifi_set_csi_rx_cb()` with a periodic timer that injects synthetic CSI frames:
```c
// mock_csi.c — synthetic CSI frame generator
#define MOCK_CSI_INTERVAL_MS 50 // 20 Hz (matches real CSI rate)
#define MOCK_N_SUBCARRIERS 52 // HT20 mode
#define MOCK_IQ_LEN (MOCK_N_SUBCARRIERS * 2) // I + Q bytes
typedef struct {
uint8_t scenario; // 0=empty, 1=person_static, 2=person_walking, 3=fall
uint32_t frame_count;
float person_x; // Simulated position [0..1]
float person_speed; // Movement speed per frame
uint8_t breathing_phase; // Simulated breathing cycle
} mock_state_t;
// Generates realistic CSI I/Q data:
// - Empty room: Gaussian noise + stable phase (low variance)
// - Static person: Phase shift proportional to distance, breathing modulation
// - Walking person: Progressive phase drift + Doppler-like amplitude change
// - Fall event: Sudden phase acceleration spike
void mock_generate_csi_frame(mock_state_t *state, wifi_csi_info_t *out_info);
```
### Signal Model
The synthetic CSI generator models subcarrier amplitude and phase as:
```
A_k(t) = A_base + A_person * exp(-d_k²/σ²) + noise
φ_k(t) = φ_base + (2π * d / λ) + breathing_mod(t) + noise
where:
k = subcarrier index
d_k = simulated distance effect on subcarrier k
A_person = amplitude perturbation from human body (scenario-dependent)
d = simulated person-to-antenna distance
λ = wavelength at subcarrier frequency
breathing_mod(t) = sin(2π * f_breath * t) * amplitude_breath
noise = Gaussian, σ tuned to match real ESP32-S3 CSI noise floor (~-90 dBm)
```
This model exercises:
- Presence detection (amplitude variance exceeds threshold)
- Breathing rate extraction (periodic phase modulation at 0.1-0.5 Hz)
- Fall detection (sudden phase acceleration exceeding `fall_thresh`)
- Multi-person separation (distinct subcarrier groups with different breathing frequencies)
### Scenarios
| ID | Scenario | Duration | Expected Output |
|----|----------|----------|-----------------|
| 0 | Empty room | 10s | `presence=0`, `motion_energy < thresh` |
| 1 | Static person | 10s | `presence=1`, `breathing_rate ∈ [10,25]`, `fall=0` |
| 2 | Walking person | 10s | `presence=1`, `motion_energy > 0.5`, `fall=0` |
| 3 | Fall event | 5s | `fall=1` flag set, `motion_energy` spike |
| 4 | Multi-person | 15s | `n_persons=2`, independent breathing rates |
| 5 | Channel sweep | 5s | Frames on channels 1, 6, 11 in sequence |
| 6 | MAC filter test | 5s | Frames with wrong MAC are dropped (counter check) |
| 7 | Ring buffer overflow | 3s | 1000 frames in 100ms burst, graceful drop |
| 8 | Boundary RSSI | 5s | RSSI sweeps -127 to 0, no crash |
| 9 | Zero-length frame | 2s | `iq_len=0` frames, serialize returns 0 |
---
## Layer 2: QEMU Runner & CI
### QEMU Runner Script
```bash
#!/bin/bash
# scripts/qemu-esp32s3-test.sh
set -euo pipefail
FIRMWARE_DIR="firmware/esp32-csi-node"
BUILD_DIR="$FIRMWARE_DIR/build"
QEMU_BIN="${QEMU_PATH:-qemu-system-xtensa}"
FLASH_IMAGE="$BUILD_DIR/qemu_flash.bin"
LOG_FILE="$BUILD_DIR/qemu_output.log"
TIMEOUT_SEC="${QEMU_TIMEOUT:-60}"
echo "=== QEMU ESP32-S3 Firmware Test ==="
# 1. Build with mock CSI enabled
echo "[1/4] Building firmware (mock CSI mode)..."
idf.py -C "$FIRMWARE_DIR" \
-D SDKCONFIG_DEFAULTS="sdkconfig.defaults;sdkconfig.qemu" \
build
# 2. Merge binaries into single flash image
echo "[2/4] Creating merged flash image..."
esptool.py --chip esp32s3 merge_bin -o "$FLASH_IMAGE" \
--flash_mode dio --flash_freq 80m --flash_size 8MB \
0x0 "$BUILD_DIR/bootloader/bootloader.bin" \
0x8000 "$BUILD_DIR/partition_table/partition-table.bin" \
0xf000 "$BUILD_DIR/ota_data_initial.bin" \
0x20000 "$BUILD_DIR/esp32-csi-node.bin"
# 3. Optionally inject pre-provisioned NVS partition
if [ -f "$BUILD_DIR/nvs_test.bin" ]; then
echo "[2b] Injecting pre-provisioned NVS partition..."
dd if="$BUILD_DIR/nvs_test.bin" of="$FLASH_IMAGE" \
bs=1 seek=$((0x9000)) conv=notrunc
fi
# 4. Run in QEMU with timeout, capture UART output
echo "[3/4] Running QEMU (timeout: ${TIMEOUT_SEC}s)..."
timeout "$TIMEOUT_SEC" "$QEMU_BIN" \
-machine esp32s3 \
-nographic \
-drive file="$FLASH_IMAGE",if=mtd,format=raw \
-serial mon:stdio \
-no-reboot \
2>&1 | tee "$LOG_FILE" || true
# 5. Validate expected output
echo "[4/4] Validating output..."
python3 scripts/validate_qemu_output.py "$LOG_FILE"
```
### QEMU sdkconfig overlay (`sdkconfig.qemu`)
```
# Enable mock CSI generator (disables real WiFi CSI)
CONFIG_CSI_MOCK_ENABLED=y
# Skip WiFi STA connection (no AP in QEMU)
CONFIG_CSI_MOCK_SKIP_WIFI_CONNECT=y
# Run all scenarios sequentially
CONFIG_CSI_MOCK_SCENARIO=255
# Use loopback for UDP (QEMU SLIRP provides 10.0.2.x network)
CONFIG_CSI_TARGET_IP="10.0.2.2"
# Shorter test durations
CONFIG_CSI_MOCK_SCENARIO_DURATION_MS=5000
# Enable verbose logging for validation
CONFIG_LOG_DEFAULT_LEVEL_INFO=y
CONFIG_CSI_MOCK_LOG_FRAMES=y
```
### Output Validation Script
`scripts/validate_qemu_output.py` parses the UART log and checks:
| Check | Pass Criteria | Severity |
|-------|---------------|----------|
| Boot | `app_main()` called, no panic/assert | FATAL |
| NVS load | `nvs_config:` log line present | FATAL |
| Mock CSI init | `mock_csi: Starting mock CSI generator` | FATAL |
| Frame generation | `mock_csi: Generated N frames` where N > 0 | ERROR |
| Edge pipeline | `edge_processing: DSP task started on Core 1` | ERROR |
| Vitals output | At least one `vitals:` log line with valid BPM | ERROR |
| Presence detection | `presence=1` appears during person scenarios | WARN |
| Fall detection | `fall=1` appears during fall scenario | WARN |
| MAC filter | `csi_collector: MAC filter dropped N frames` where N > 0 | WARN |
| ADR-018 serialize | `csi_collector: Serialized N frames` where N > 0 | ERROR |
| No crash | No `Guru Meditation Error`, no `assert failed`, no `abort()` | FATAL |
| Clean exit | Firmware reaches end of scenario sequence | ERROR |
| Heap OK | No `HEAP_ERROR` or `out of memory` | FATAL |
| Stack OK | No `Stack overflow` detected | FATAL |
Exit codes: `0` = all pass, `1` = WARN only, `2` = ERROR, `3` = FATAL
### CI Workflow
```yaml
# .github/workflows/firmware-qemu.yml
name: Firmware QEMU Tests
on:
push:
paths: ['firmware/**']
pull_request:
paths: ['firmware/**']
jobs:
qemu-test:
runs-on: ubuntu-latest
container:
image: espressif/idf:v5.4
strategy:
matrix:
scenario: [default, nvs-full, nvs-edge-tier0, nvs-tdm-3node]
steps:
- uses: actions/checkout@v4
- name: Install Espressif QEMU
run: |
apt-get update && apt-get install -y libslirp-dev libglib2.0-dev ninja-build
git clone --depth 1 https://github.com/espressif/qemu.git /tmp/qemu
cd /tmp/qemu
./configure --target-list=xtensa-softmmu --enable-slirp
make -j$(nproc)
cp build/qemu-system-xtensa /usr/local/bin/
env:
QEMU_PATH: /usr/local/bin/qemu-system-xtensa
- name: Prepare NVS for scenario
run: |
case "${{ matrix.scenario }}" in
nvs-full)
python firmware/esp32-csi-node/provision.py --dry-run \
--port dummy --ssid "TestWiFi" --password "test1234" \
--target-ip "10.0.2.2" --target-port 5005 \
--channel 6 --filter-mac AA:BB:CC:DD:EE:FF \
--node-id 1 --edge-tier 2
cp nvs_provision.bin firmware/esp32-csi-node/build/nvs_test.bin
;;
nvs-edge-tier0)
python firmware/esp32-csi-node/provision.py --dry-run \
--port dummy --edge-tier 0 --node-id 5
cp nvs_provision.bin firmware/esp32-csi-node/build/nvs_test.bin
;;
nvs-tdm-3node)
python firmware/esp32-csi-node/provision.py --dry-run \
--port dummy --tdm-slot 1 --tdm-total 3 --node-id 1
cp nvs_provision.bin firmware/esp32-csi-node/build/nvs_test.bin
;;
esac
- name: Build firmware (mock CSI mode)
run: |
cd firmware/esp32-csi-node
idf.py -D SDKCONFIG_DEFAULTS="sdkconfig.defaults;sdkconfig.qemu" set-target esp32s3
idf.py build
- name: Run QEMU tests
run: bash scripts/qemu-esp32s3-test.sh
env:
QEMU_PATH: /usr/local/bin/qemu-system-xtensa
QEMU_TIMEOUT: 90
- name: Upload QEMU log
if: always()
uses: actions/upload-artifact@v4
with:
name: qemu-output-${{ matrix.scenario }}
path: firmware/esp32-csi-node/build/qemu_output.log
```
---
## Layer 3: Multi-Node Mesh Simulation
Run multiple QEMU instances with TAP networking to test TDM slot coordination and multi-node aggregation.
### Architecture
```
┌──────────┐ ┌──────────┐ ┌──────────┐
│ QEMU #0 │ │ QEMU #1 │ │ QEMU #2
│ slot=0 │ │ slot=1 │ │ slot=2 │
│ node_id=0│ │ node_id=1│ │ node_id=2│
└────┬─────┘ └────┬─────┘ └────┬─────┘
│ │ │
└──────────┬───┴──────────────┘
┌───────────────┐
│ TAP bridge │
│ (10.0.0.0/24) │
└───────┬───────┘
┌───────────────┐
│ Rust aggregator│
│ (UDP :5005) │
└───────────────┘
```
### Multi-Node Runner
```bash
#!/bin/bash
# scripts/qemu-mesh-test.sh — run 3 QEMU nodes + Rust aggregator
set -euo pipefail
N_NODES=${1:-3}
AGGREGATOR_PORT=5005
BRIDGE="qemu-br0"
# Create bridge
ip link add "$BRIDGE" type bridge
ip addr add 10.0.0.1/24 dev "$BRIDGE"
ip link set "$BRIDGE" up
# Build flash images with per-node NVS
for i in $(seq 0 $((N_NODES - 1))); do
python firmware/esp32-csi-node/provision.py --dry-run \
--port dummy --node-id "$i" --tdm-slot "$i" --tdm-total "$N_NODES" \
--target-ip 10.0.0.1 --target-port "$AGGREGATOR_PORT"
cp nvs_provision.bin "build/nvs_node${i}.bin"
# Inject NVS into per-node flash image
cp build/qemu_flash.bin "build/qemu_flash_node${i}.bin"
dd if="build/nvs_node${i}.bin" of="build/qemu_flash_node${i}.bin" \
bs=1 seek=$((0x9000)) conv=notrunc
done
# Start Rust aggregator in background
cargo run -p wifi-densepose-hardware --bin aggregator -- \
--listen 0.0.0.0:${AGGREGATOR_PORT} \
--expect-nodes "$N_NODES" \
--output build/mesh_test_results.json &
AGGREGATOR_PID=$!
# Launch QEMU nodes
for i in $(seq 0 $((N_NODES - 1))); do
TAP="tap${i}"
ip tuntap add "$TAP" mode tap
ip link set "$TAP" master "$BRIDGE"
ip link set "$TAP" up
qemu-system-xtensa \
-machine esp32s3 \
-nographic \
-drive file="build/qemu_flash_node${i}.bin",if=mtd,format=raw \
-serial file:"build/qemu_node${i}.log" \
-nic tap,ifname="$TAP",script=no,downscript=no \
-no-reboot &
echo "Started QEMU node $i (PID: $!)"
done
# Wait for test duration
sleep 30
# Validate results
kill $AGGREGATOR_PID 2>/dev/null || true
python3 scripts/validate_mesh_test.py build/mesh_test_results.json --nodes "$N_NODES"
```
### Mesh Validation Checks
| Check | Pass Criteria |
|-------|---------------|
| All nodes booted | N distinct `node_id` values in received frames |
| TDM ordering | Slot 0 frames arrive before slot 1 within each TDM cycle |
| No slot collision | No two frames from different nodes with overlapping timestamps within TDM window |
| Frame count balance | Each node contributes ±10% of total frames |
| ADR-018 compliance | All frames have valid magic `0xC5110001` and correct node IDs |
| Vitals per node | Each node produces independent vitals packets |
---
## Layer 4: GDB Remote Debugging
QEMU provides a built-in GDB stub for zero-cost debugging without JTAG hardware.
### Usage
```bash
# Launch QEMU with GDB stub (paused at boot)
qemu-system-xtensa \
-machine esp32s3 \
-nographic \
-drive file=build/qemu_flash.bin,if=mtd,format=raw \
-serial mon:stdio \
-s -S # -s = GDB on :1234, -S = pause at start
# In another terminal: attach GDB
xtensa-esp-elf-gdb build/esp32-csi-node.elf \
-ex "target remote :1234" \
-ex "b edge_processing.c:dsp_task" \
-ex "b csi_collector.c:wifi_csi_callback" \
-ex "b mock_csi.c:mock_generate_csi_frame" \
-ex "watch g_nvs_config.csi_channel" \
-ex "continue"
```
### Key Breakpoint Locations
| Breakpoint | Purpose |
|-----------|---------|
| `edge_processing.c:dsp_task` | DSP consumer loop entry |
| `edge_processing.c:presence_detect` | Threshold comparison |
| `edge_processing.c:fall_detect` | Phase acceleration check |
| `csi_collector.c:wifi_csi_callback` | Frame ingestion (or mock injection point) |
| `csi_collector.c:csi_serialize_frame` | ADR-018 serialization |
| `nvs_config.c:nvs_config_load` | NVS parse logic |
| `wasm_runtime.c:wasm_on_csi` | WASM module dispatch |
| `mock_csi.c:mock_generate_csi_frame` | Synthetic frame generation |
### VS Code Integration
```json
// .vscode/launch.json
{
"version": "0.2.0",
"configurations": [{
"name": "QEMU ESP32-S3 Debug",
"type": "cppdbg",
"request": "launch",
"program": "${workspaceFolder}/firmware/esp32-csi-node/build/esp32-csi-node.elf",
"miDebuggerPath": "xtensa-esp-elf-gdb",
"miDebuggerServerAddress": "localhost:1234",
"setupCommands": [
{ "text": "set remote hardware-breakpoint-limit 2" },
{ "text": "set remote hardware-watchpoint-limit 2" }
]
}]
}
```
---
## Layer 5: Code Coverage (gcov/lcov)
### Build with Coverage
```
# sdkconfig.coverage (overlay)
CONFIG_COMPILER_OPTIMIZATION_NONE=y
CONFIG_GCOV_ENABLE=y
CONFIG_APPTRACE_GCOV_ENABLE=y
```
### Coverage Collection
```bash
# After QEMU run, extract gcov data from flash dump
esptool.py --chip esp32s3 read_flash 0x300000 0x100000 gcov_data.bin
# Or use ESP-IDF's app_trace + gcov integration:
# QEMU + GDB → "monitor gcov dump" → .gcda files
# Generate HTML report
lcov --capture --directory build --output-file coverage.info
lcov --remove coverage.info '*/esp-idf/*' '*/test/*' --output-file coverage_filtered.info
genhtml coverage_filtered.info --output-directory build/coverage_report
```
### Coverage Targets
| Module | Target | Critical Paths |
|--------|--------|---------------|
| `edge_processing.c` | ≥80% | `dsp_task`, `biquad_filter`, `fall_detect`, `multi_person_cluster` |
| `csi_collector.c` | ≥90% | `csi_serialize_frame`, `wifi_csi_callback`, MAC filter branch |
| `nvs_config.c` | ≥95% | Every NVS key read path, default fallback paths |
| `mock_csi.c` | ≥95% | All scenarios, all signal model branches |
| `stream_sender.c` | ≥80% | Init, send, error paths |
| `wasm_runtime.c` | ≥70% | Module load, dispatch, signature verify |
---
## Layer 6: Fuzz Testing
### Fuzz Targets
| Target | Input | Mutation Strategy | Looking For |
|--------|-------|-------------------|-------------|
| `csi_serialize_frame()` | Random `wifi_csi_info_t` | Extreme `len` (0, 65535), NULL `buf`, negative RSSI, channel 255 | Buffer overflow, NULL deref |
| `nvs_config_load()` | Crafted NVS partition binary | Truncated strings, out-of-range u8/u16, missing keys, corrupt headers | Kconfig fallback, no crash |
| `edge_enqueue_csi()` | Rapid-fire 10,000 frames | Vary `iq_len` (0 to `EDGE_MAX_IQ_BYTES+1`), randomize RSSI | Ring overflow, no data corruption |
| `rvf_parser.c` | Malformed RVF network packets | Bad magic, truncated headers, oversized payloads | Parse rejection, no crash |
| `wasm_upload.c` | Corrupt WASM blobs | Invalid magic, oversized modules, bad Ed25519 signatures, truncated | Rejection without crash, no code execution |
| `csi_serialize_frame()` + `edge_enqueue_csi()` | Chained: generate → serialize → enqueue | End-to-end with random data | Pipeline integrity |
### Implementation Approach
```c
// test/fuzz_csi_serialize.c — runs on host (not ESP32)
// Compiled with: clang -fsanitize=fuzzer,address
#include "csi_collector.h"
int LLVMFuzzerTestOneInput(const uint8_t *data, size_t size) {
if (size < sizeof(wifi_csi_info_t)) return 0;
wifi_csi_info_t info;
memcpy(&info, data, sizeof(info));
// Point buf at remaining fuzz data
size_t remaining = size - sizeof(info);
uint8_t iq_buf[2048];
if (remaining > sizeof(iq_buf)) remaining = sizeof(iq_buf);
memcpy(iq_buf, data + sizeof(info), remaining);
info.buf = iq_buf;
info.len = (int)remaining;
uint8_t out[4096];
csi_serialize_frame(&info, out, sizeof(out));
return 0;
}
```
### Fuzz CI Job
```yaml
fuzz-test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Build fuzz targets
run: |
cd firmware/esp32-csi-node/test
clang -fsanitize=fuzzer,address -I../main \
fuzz_csi_serialize.c ../main/csi_collector.c \
-o fuzz_serialize
- name: Run fuzz (5 min per target)
run: |
cd firmware/esp32-csi-node/test
timeout 300 ./fuzz_serialize corpus/ || true
- name: Upload crashes
if: failure()
uses: actions/upload-artifact@v4
with:
name: fuzz-crashes
path: firmware/esp32-csi-node/test/crash-*
```
---
## Layer 7: NVS Provisioning Matrix
### Config Combinations
| Config | NVS Values | Validates |
|--------|-----------|-----------|
| `default` | (empty NVS) | Kconfig fallback paths |
| `wifi-only` | ssid, password | Basic provisioning |
| `full-adr060` | channel=6, filter_mac=AA:BB:CC:DD:EE:FF | Channel override + MAC filter |
| `edge-tier0` | edge_tier=0 | Raw CSI passthrough (no DSP) |
| `edge-tier1` | edge_tier=1, pres_thresh=100, fall_thresh=2000 | Stats-only mode |
| `edge-tier2-custom` | edge_tier=2, vital_win=128, vital_int=500, subk_count=16 | Full vitals with custom params |
| `tdm-3node` | tdm_slot=1, tdm_nodes=3, node_id=1 | TDM mesh timing |
| `wasm-signed` | wasm_max=4, wasm_verify=1, wasm_pubkey=<32 bytes> | WASM with Ed25519 verification |
| `wasm-unsigned` | wasm_max=2, wasm_verify=0 | WASM without signature check |
| `5ghz-channel` | channel=36, filter_mac=... | 5 GHz CSI collection |
| `boundary-max` | target_port=65535, node_id=255, top_k=32, vital_win=256 | Max-range values |
| `boundary-min` | target_port=1, node_id=0, top_k=1, vital_win=32 | Min-range values |
| `power-save` | power_duty=10, edge_tier=0 | Low-power mode |
| `corrupt-nvs` | (manually crafted partial/corrupt partition) | Graceful fallback to defaults |
### Automated Matrix Generation
```python
# scripts/generate_nvs_matrix.py
# Generates all 14 NVS partition binaries for CI matrix
CONFIGS = [
{"name": "default", "args": []},
{"name": "wifi-only", "args": ["--ssid", "Test", "--password", "test1234"]},
{"name": "full-adr060", "args": ["--channel", "6", "--filter-mac", "AA:BB:CC:DD:EE:FF",
"--ssid", "Test", "--password", "test"]},
{"name": "edge-tier0", "args": ["--edge-tier", "0"]},
# ... all 14 configs
]
```
---
## Layer 8: Snapshot & Replay
### QEMU Snapshot Commands
```bash
# Save snapshot after boot + NVS load (skip 3s boot time)
(qemu) savevm post_boot
# Save after WiFi connect + first CSI frame
(qemu) savevm post_connect
# Save after edge pipeline calibration complete (~60s)
(qemu) savevm post_calibration
# Restore any snapshot (< 100ms)
(qemu) loadvm post_connect
```
### Automated Snapshot Pipeline
```bash
# scripts/qemu-snapshot-test.sh
# Phase 1: Create base snapshots (one-time, cached in CI)
qemu-system-xtensa ... -monitor unix:qemu.sock,server,nowait &
sleep 5
echo "savevm post_boot" | socat - UNIX-CONNECT:qemu.sock
sleep 10
echo "savevm post_first_frame" | socat - UNIX-CONNECT:qemu.sock
# Phase 2: Run quick tests from snapshots (< 1s each)
for test in test_presence test_fall test_multi_person; do
echo "loadvm post_first_frame" | socat - UNIX-CONNECT:qemu.sock
echo "cont" | socat - UNIX-CONNECT:qemu.sock
sleep 2 # Run test scenario
# Validate output
done
```
### Performance Impact
| Operation | Without Snapshots | With Snapshots |
|-----------|-------------------|----------------|
| Full boot + NVS + WiFi mock | ~5 seconds | ~5 seconds (first run) |
| Run single scenario | ~5s boot + ~5s test = 10s | ~0.1s restore + ~5s test = 5.1s |
| Run all 10 scenarios | ~100 seconds | ~51 seconds (49% faster) |
| Run 14 NVS configs × 10 scenarios | ~23 minutes | ~12 minutes (48% faster) |
---
## Layer 9: Chaos Testing
### Fault Injection Table
| Fault | Injection Method | Expected Behavior | Severity |
|-------|-----------------|-------------------|----------|
| WiFi disconnect | Timer kills mock WiFi connection after N frames | Reconnect attempt, CSI pauses and resumes | HIGH |
| Ring buffer overflow | Burst 1000 frames in 100ms | Frame drop counter increments, no crash, no data corruption | HIGH |
| NVS corruption | Flash image with partial-write NVS partition | Falls back to Kconfig defaults, logs warning | MEDIUM |
| Stack overflow | Deep recursion in WASM module callback | Watchdog fires, task restarts, no hang | HIGH |
| Heap exhaustion | `malloc` returns NULL after N allocations | Graceful degradation, logs OOM, continues operation | HIGH |
| Timer starvation | Block DSP task for 500ms | Frames dropped from ring, no deadlock, recovers | MEDIUM |
| UDP send failure | SLIRP network down | `stream_sender_send` returns -1, error counter increments | LOW |
| Corrupt CSI frame | Inject frame with invalid magic in I/Q data | Edge pipeline rejects, increments error counter | LOW |
| NVS write during read | Concurrent NVS open for write while config loads | No corruption, NVS handle isolation | MEDIUM |
### Chaos Runner
```bash
# scripts/qemu-chaos-test.sh
# Run with fault injection enabled
qemu-system-xtensa ... \
-monitor unix:qemu.sock,server,nowait &
# Inject faults via GDB or monitor commands
for fault in wifi_kill heap_exhaust ring_flood; do
echo "[CHAOS] Injecting: $fault"
python3 scripts/inject_fault.py --socket qemu.sock --fault "$fault"
sleep 5
python3 scripts/check_health.py --log "$LOG_FILE" --after-fault "$fault"
done
```
---
## Implementation Plan
| Phase | Layer | Deliverables | Effort | Priority |
|-------|-------|-------------|--------|----------|
| **P1** | L1 + L2 | `mock_csi.c`, `mock_csi.h`, `Kconfig.projbuild`, `sdkconfig.qemu`, `qemu-esp32s3-test.sh`, `validate_qemu_output.py`, `firmware-qemu.yml` | 2 days | Critical |
| **P2** | L4 + L5 | GDB launch config, `sdkconfig.coverage`, lcov integration, coverage CI job | 1 day | High |
| **P3** | L7 | `generate_nvs_matrix.py`, 14 NVS configs, CI matrix expansion | 1 day | High |
| **P4** | L6 | `fuzz_csi_serialize.c`, `fuzz_nvs_config.c`, `fuzz_edge_enqueue.c`, fuzz CI job | 2 days | High |
| **P5** | L3 | `qemu-mesh-test.sh`, TAP bridge setup, `validate_mesh_test.py`, Rust aggregator integration | 3 days | High |
| **P6** | L8 | Snapshot pipeline, cached base images in CI | 0.5 day | Medium |
| **P7** | L9 | `inject_fault.py`, `check_health.py`, `qemu-chaos-test.sh`, 9 fault scenarios | 2 days | Medium |
| **P8** | Performance | Instruction counting, DSP cycle profiling, optimization report | 1 day | Low |
**Total**: ~12.5 days across 8 phases
---
## File Layout
```
firmware/esp32-csi-node/
├── main/
│ ├── mock_csi.c # NEW — synthetic CSI frame generator
│ ├── mock_csi.h # NEW — mock API + scenario definitions
│ ├── Kconfig.projbuild # MODIFIED — CONFIG_CSI_MOCK_* options
│ ├── CMakeLists.txt # MODIFIED — conditional mock_csi.c inclusion
│ └── ... (existing files unchanged)
├── test/
│ ├── fuzz_csi_serialize.c # NEW — libFuzzer target for serialization
│ ├── fuzz_nvs_config.c # NEW — libFuzzer target for NVS parsing
│ ├── fuzz_edge_enqueue.c # NEW — libFuzzer target for ring buffer
│ └── corpus/ # NEW — seed inputs for fuzz targets
├── sdkconfig.qemu # NEW — QEMU-specific sdkconfig overlay
├── sdkconfig.coverage # NEW — gcov-enabled sdkconfig overlay
└── ...
scripts/
├── qemu-esp32s3-test.sh # NEW — single-node QEMU runner
├── qemu-mesh-test.sh # NEW — multi-node mesh runner
├── qemu-chaos-test.sh # NEW — chaos/fault injection runner
├── validate_qemu_output.py # NEW — UART log validation
├── validate_mesh_test.py # NEW — mesh test validation
├── generate_nvs_matrix.py # NEW — NVS config matrix generator
├── inject_fault.py # NEW — QEMU fault injection
└── check_health.py # NEW — post-fault health checker
.vscode/
└── launch.json # MODIFIED — add QEMU GDB debug config
.github/workflows/
└── firmware-qemu.yml # NEW — CI workflow with matrix
```
---
## Consequences
### Benefits
1. **No hardware required** — contributors validate firmware changes with QEMU alone
2. **Automated CI** — every PR touching `firmware/` runs 14 NVS configs × 10 scenarios in parallel
3. **10× faster iteration** — snapshot restore in <100ms vs 20s flash cycle
4. **Security hardening** — fuzz testing catches buffer overflows, NULL derefs, and parser bugs before they reach hardware
5. **Mesh validation** — multi-node TDM tested without 3 physical ESP32s
6. **Coverage visibility** — lcov reports show untested edge processing paths
7. **Resilience proof** — chaos tests verify firmware recovers from WiFi drops, OOM, and ring overflow
8. **GDB debugging** — set breakpoints on DSP pipeline without JTAG adapter
9. **Regression detection** — boot failures, NVS parsing errors, and FreeRTOS deadlocks caught in CI
### Limitations
1. **No real WiFi/CSI** — QEMU cannot emulate the ESP32-S3 WiFi radio or CSI extraction hardware
2. **Synthetic CSI fidelity** — mock frames approximate real CSI patterns but don't capture real-world multipath, interference, or antenna characteristics
3. **Timing differences** — QEMU timing is not cycle-accurate; FreeRTOS tick rates may differ from hardware
4. **No peripheral testing** — I2C display, real GPIO, and light-sleep power management cannot be tested
5. **QEMU build requirement** — Espressif's QEMU fork must be built from source (not in Ubuntu packages)
6. **Coverage overhead** — gcov-enabled builds are ~2× slower in QEMU
### What QEMU Testing Covers vs Requires Hardware
| Test Domain | QEMU | Hardware |
|-------------|------|----------|
| Boot + NVS config (14 configs) | Full | Full |
| Edge DSP pipeline (biquad, Welford, top-K) | Full | Full |
| ADR-018 frame serialization | Full | Full |
| Vitals packet generation (0xC5110002) | Full | Full |
| WASM module loading + execution | Full | Full |
| Multi-node TDM mesh (3+ nodes) | Full (TAP) | Full |
| Fuzz testing (CSI parser, NVS) | Full | N/A |
| Code coverage analysis | Full | Partial |
| GDB breakpoint debugging | Full | Full (JTAG) |
| Chaos/fault injection | Full | Manual |
| OTA update flow | Partial (HTTP mock) | Full |
| Real WiFi connection | No | Full |
| Real CSI data quality | No | Full |
| Channel hopping on RF | No | Full |
| MAC filter on real frames | No | Full |
| Power management (light-sleep) | No | Full |
| Display rendering (OLED) | No | Full |
| UDP over real network | No | Full |
---
## Alternatives Considered
### 1. Host-native unit tests (no QEMU)
Extract pure C functions (`csi_serialize_frame`, edge DSP math) and compile/test on host with CMock/Unity. Simpler but doesn't test FreeRTOS integration, NVS, or boot sequence.
**Verdict**: Complementary — do both. Host unit tests for math, QEMU for integration. Fuzz targets (Layer 6) already use host-native compilation.
### 2. Hardware-in-the-loop CI (real ESP32 on runner)
Use a self-hosted GitHub Actions runner with a physical ESP32-S3 attached.
**Verdict**: Valuable but expensive and fragile. QEMU covers ~85% of test cases (up from 70% with all 9 layers). Add HIL later for real CSI validation only.
### 3. Docker-based ESP-IDF build only (no runtime test)
Just verify the firmware compiles in CI without running it.
**Verdict**: Already possible but insufficient — compilation doesn't catch runtime bugs (stack overflow, NVS parsing errors, FreeRTOS deadlocks).
### 4. Renode emulator
Alternative to QEMU with better peripheral modeling for some platforms.
**Verdict**: Renode has ESP32 support but ESP32-S3 support is less mature than Espressif's own QEMU fork. Revisit if Renode adds full S3 support.
---
## References
- [Espressif QEMU fork](https://github.com/espressif/qemu) — official ESP32/S3/C3/H2 support
- [ESP-IDF QEMU guide](https://docs.espressif.com/projects/esp-idf/en/latest/esp32s3/api-guides/tools/qemu.html)
- [libFuzzer documentation](https://llvm.org/docs/LibFuzzer.html) — LLVM-based coverage-guided fuzzing
- [lcov](https://github.com/linux-test-project/lcov) — Linux test coverage visualization
- ADR-018: Binary CSI frame format (magic `0xC5110001`)
- ADR-039: Edge intelligence pipeline (biquad, vitals, fall detection)
- ADR-040: WASM programmable sensing runtime
- ADR-057: Build-time CSI guard (`CONFIG_ESP_WIFI_CSI_ENABLED`)
- ADR-060: Channel override and MAC address filter
@@ -0,0 +1,31 @@
# Remove MSYS environment variables that trigger ESP-IDF's MinGW rejection
Remove-Item env:MSYSTEM -ErrorAction SilentlyContinue
Remove-Item env:MSYSTEM_CARCH -ErrorAction SilentlyContinue
Remove-Item env:MSYSTEM_CHOST -ErrorAction SilentlyContinue
Remove-Item env:MSYSTEM_PREFIX -ErrorAction SilentlyContinue
Remove-Item env:MINGW_CHOST -ErrorAction SilentlyContinue
Remove-Item env:MINGW_PACKAGE_PREFIX -ErrorAction SilentlyContinue
Remove-Item env:MINGW_PREFIX -ErrorAction SilentlyContinue
$env:IDF_PATH = "C:\Users\ruv\esp\v5.4\esp-idf"
$env:IDF_TOOLS_PATH = "C:\Espressif\tools"
$env:IDF_PYTHON_ENV_PATH = "C:\Espressif\tools\python\v5.4\venv"
$env:PATH = "C:\Espressif\tools\xtensa-esp-elf\esp-14.2.0_20241119\xtensa-esp-elf\bin;C:\Espressif\tools\cmake\3.30.2\cmake-3.30.2-windows-x86_64\bin;C:\Espressif\tools\ninja\1.12.1;C:\Espressif\tools\ccache\4.10.2\ccache-4.10.2-windows-x86_64;C:\Espressif\tools\idf-exe\1.0.3;C:\Espressif\tools\python\v5.4\venv\Scripts;$env:PATH"
Set-Location "C:\Users\ruv\Projects\wifi-densepose\firmware\esp32-csi-node"
$python = "$env:IDF_PYTHON_ENV_PATH\Scripts\python.exe"
$idf = "$env:IDF_PATH\tools\idf.py"
Write-Host "=== Cleaning stale build cache ==="
& $python $idf fullclean
Write-Host "=== Building firmware (SSID=ruv.net, target=192.168.1.20:5005) ==="
& $python $idf build
if ($LASTEXITCODE -eq 0) {
Write-Host "=== Build succeeded! Flashing to COM7 ==="
& $python $idf -p COM7 flash
} else {
Write-Host "=== Build failed with exit code $LASTEXITCODE ==="
}
+44 -2
View File
@@ -12,6 +12,7 @@
*/
#include "csi_collector.h"
#include "nvs_config.h"
#include "stream_sender.h"
#include "edge_processing.h"
@@ -21,6 +22,9 @@
#include "esp_timer.h"
#include "sdkconfig.h"
/* ADR-060: Access the global NVS config for MAC filter and channel override. */
extern nvs_config_t g_nvs_config;
/* ADR-057: Build-time guard — fail early if CSI is not enabled in sdkconfig.
* Without this, the firmware compiles but crashes at runtime with:
* "E (xxxx) wifi:CSI not enabled in menuconfig!"
@@ -151,6 +155,14 @@ size_t csi_serialize_frame(const wifi_csi_info_t *info, uint8_t *buf, size_t buf
static void wifi_csi_callback(void *ctx, wifi_csi_info_t *info)
{
(void)ctx;
/* ADR-060: MAC address filtering — drop frames from non-matching sources. */
if (g_nvs_config.filter_mac_set) {
if (memcmp(info->mac, g_nvs_config.filter_mac, 6) != 0) {
return; /* Source MAC doesn't match filter — skip frame. */
}
}
s_cb_count++;
if (s_cb_count <= 3 || (s_cb_count % 100) == 0) {
@@ -203,6 +215,29 @@ static void wifi_promiscuous_cb(void *buf, wifi_promiscuous_pkt_type_t type)
void csi_collector_init(void)
{
/* ADR-060: Determine the CSI channel.
* Priority: 1) NVS override (--channel), 2) connected AP channel, 3) Kconfig default. */
uint8_t csi_channel = (uint8_t)CONFIG_CSI_WIFI_CHANNEL;
if (g_nvs_config.csi_channel > 0) {
/* Explicit NVS override via provision.py --channel */
csi_channel = g_nvs_config.csi_channel;
ESP_LOGI(TAG, "Using NVS channel override: %u", (unsigned)csi_channel);
} else {
/* Auto-detect from connected AP */
wifi_ap_record_t ap_info;
if (esp_wifi_sta_get_ap_info(&ap_info) == ESP_OK && ap_info.primary > 0) {
csi_channel = ap_info.primary;
ESP_LOGI(TAG, "Auto-detected AP channel: %u", (unsigned)csi_channel);
} else {
ESP_LOGW(TAG, "Could not detect AP channel, using Kconfig default: %u",
(unsigned)csi_channel);
}
}
/* Update the hop table's first channel to match. */
s_hop_channels[0] = csi_channel;
/* Enable promiscuous mode — required for reliable CSI callbacks.
* Without this, CSI only fires on frames destined to this station,
* which may be very infrequent on a quiet network. */
@@ -230,8 +265,15 @@ void csi_collector_init(void)
ESP_ERROR_CHECK(esp_wifi_set_csi_rx_cb(wifi_csi_callback, NULL));
ESP_ERROR_CHECK(esp_wifi_set_csi(true));
ESP_LOGI(TAG, "CSI collection initialized (node_id=%d, channel=%d)",
CONFIG_CSI_NODE_ID, CONFIG_CSI_WIFI_CHANNEL);
if (g_nvs_config.filter_mac_set) {
ESP_LOGI(TAG, "MAC filter active: %02x:%02x:%02x:%02x:%02x:%02x",
g_nvs_config.filter_mac[0], g_nvs_config.filter_mac[1],
g_nvs_config.filter_mac[2], g_nvs_config.filter_mac[3],
g_nvs_config.filter_mac[4], g_nvs_config.filter_mac[5]);
}
ESP_LOGI(TAG, "CSI collection initialized (node_id=%d, channel=%u)",
CONFIG_CSI_NODE_ID, (unsigned)csi_channel);
}
/* ---- ADR-029: Channel hopping ---- */
+25
View File
@@ -91,6 +91,11 @@ void nvs_config_load(nvs_config_t *cfg)
cfg->wasm_verify = 0; /* Kconfig disabled signature verification. */
#endif
/* ADR-060: Channel override and MAC filter defaults. */
cfg->csi_channel = 0; /* 0 = auto-detect from connected AP. */
cfg->filter_mac_set = 0;
memset(cfg->filter_mac, 0, 6);
/* Try to override from NVS */
nvs_handle_t handle;
esp_err_t err = nvs_open("csi_cfg", NVS_READONLY, &handle);
@@ -277,6 +282,26 @@ void nvs_config_load(nvs_config_t *cfg)
ESP_LOGW(TAG, "wasm_verify=1 but no wasm_pubkey in NVS — uploads will be rejected");
}
/* ADR-060: CSI channel override. */
uint8_t csi_ch_val;
if (nvs_get_u8(handle, "csi_channel", &csi_ch_val) == ESP_OK) {
if ((csi_ch_val >= 1 && csi_ch_val <= 14) || (csi_ch_val >= 36 && csi_ch_val <= 177)) {
cfg->csi_channel = csi_ch_val;
ESP_LOGI(TAG, "NVS override: csi_channel=%u", (unsigned)cfg->csi_channel);
} else {
ESP_LOGW(TAG, "NVS csi_channel=%u invalid, ignored", (unsigned)csi_ch_val);
}
}
/* ADR-060: MAC address filter (6-byte blob). */
size_t mac_len = 6;
if (nvs_get_blob(handle, "filter_mac", cfg->filter_mac, &mac_len) == ESP_OK && mac_len == 6) {
cfg->filter_mac_set = 1;
ESP_LOGI(TAG, "NVS override: filter_mac=%02x:%02x:%02x:%02x:%02x:%02x",
cfg->filter_mac[0], cfg->filter_mac[1], cfg->filter_mac[2],
cfg->filter_mac[3], cfg->filter_mac[4], cfg->filter_mac[5]);
}
/* Validate tdm_slot_index < tdm_node_count */
if (cfg->tdm_slot_index >= cfg->tdm_node_count) {
ESP_LOGW(TAG, "tdm_slot_index=%u >= tdm_node_count=%u, clamping to 0",
@@ -50,6 +50,11 @@ typedef struct {
uint8_t wasm_verify; /**< Require Ed25519 signature for uploads. */
uint8_t wasm_pubkey[32]; /**< Ed25519 public key for WASM signature. */
uint8_t wasm_pubkey_valid; /**< 1 if pubkey was loaded from NVS. */
/* ADR-060: Channel override and MAC address filtering */
uint8_t csi_channel; /**< Explicit CSI channel override (0 = auto-detect). */
uint8_t filter_mac[6]; /**< MAC address to filter CSI frames. */
uint8_t filter_mac_set; /**< 1 if filter_mac was loaded from NVS. */
} nvs_config_t;
/**
+32
View File
@@ -64,6 +64,13 @@ def build_nvs_csv(args):
writer.writerow(["vital_int", "data", "u16", str(args.vital_int)])
if args.subk_count is not None:
writer.writerow(["subk_count", "data", "u8", str(args.subk_count)])
# ADR-060: Channel override and MAC filter
if args.channel is not None:
writer.writerow(["csi_channel", "data", "u8", str(args.channel)])
if args.filter_mac is not None:
mac_bytes = bytes(int(b, 16) for b in args.filter_mac.split(":"))
# NVS blob: write as hex-encoded string for CSV compatibility
writer.writerow(["filter_mac", "data", "hex2bin", mac_bytes.hex()])
return buf.getvalue()
@@ -165,6 +172,10 @@ def main():
parser.add_argument("--vital-win", type=int, help="Phase history window in frames (default: 300)")
parser.add_argument("--vital-int", type=int, help="Vitals packet interval in ms (default: 1000)")
parser.add_argument("--subk-count", type=int, help="Top-K subcarrier count (default: 32)")
# ADR-060: Channel override and MAC filter
parser.add_argument("--channel", type=int, help="CSI channel (1-14 for 2.4GHz, 36-177 for 5GHz). "
"Overrides auto-detection from connected AP.")
parser.add_argument("--filter-mac", type=str, help="MAC address to filter CSI frames (AA:BB:CC:DD:EE:FF)")
parser.add_argument("--dry-run", action="store_true", help="Generate NVS binary but don't flash")
args = parser.parse_args()
@@ -176,6 +187,7 @@ def main():
args.edge_tier is not None, args.pres_thresh is not None,
args.fall_thresh is not None, args.vital_win is not None,
args.vital_int is not None, args.subk_count is not None,
args.channel is not None, args.filter_mac is not None,
])
if not has_value:
parser.error("At least one config value must be specified")
@@ -186,6 +198,22 @@ def main():
if args.tdm_slot is not None and args.tdm_slot >= args.tdm_total:
parser.error(f"--tdm-slot ({args.tdm_slot}) must be less than --tdm-total ({args.tdm_total})")
# ADR-060: Validate channel and MAC filter
if args.channel is not None:
if not ((1 <= args.channel <= 14) or (36 <= args.channel <= 177)):
parser.error(f"--channel must be 1-14 (2.4GHz) or 36-177 (5GHz), got {args.channel}")
if args.filter_mac is not None:
parts = args.filter_mac.split(":")
if len(parts) != 6:
parser.error(f"--filter-mac must be in AA:BB:CC:DD:EE:FF format, got '{args.filter_mac}'")
try:
for p in parts:
val = int(p, 16)
if val < 0 or val > 255:
raise ValueError
except ValueError:
parser.error(f"--filter-mac contains invalid hex bytes: '{args.filter_mac}'")
print("Building NVS configuration:")
if args.ssid:
print(f" WiFi SSID: {args.ssid}")
@@ -212,6 +240,10 @@ def main():
print(f" Vital Interval:{args.vital_int} ms")
if args.subk_count is not None:
print(f" Top-K Subcarr: {args.subk_count}")
if args.channel is not None:
print(f" CSI Channel: {args.channel}")
if args.filter_mac is not None:
print(f" Filter MAC: {args.filter_mac}")
csv_content = build_nvs_csv(args)
+14
View File
@@ -0,0 +1,14 @@
$p = New-Object System.IO.Ports.SerialPort('COM7', 115200)
$p.ReadTimeout = 5000
$p.Open()
Start-Sleep -Milliseconds 200
for ($i = 0; $i -lt 60; $i++) {
try {
$line = $p.ReadLine()
Write-Host $line
} catch {
break
}
}
$p.Close()
+1
View File
@@ -29,6 +29,7 @@
<button class="nav-tab" data-tab="applications">Applications</button>
<button class="nav-tab" data-tab="sensing">Sensing</button>
<button class="nav-tab" data-tab="training">Training</button>
<a href="pose-fusion.html" class="nav-tab" style="text-decoration:none">Pose Fusion</a>
<a href="observatory.html" class="nav-tab" style="text-decoration:none">Observatory</a>
</nav>
+201
View File
@@ -0,0 +1,201 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>RuView — Dual-Modal Pose Estimation</title>
<link rel="stylesheet" href="pose-fusion/css/style.css?v=13">
</head>
<body>
<!-- Header -->
<header class="header">
<div class="header-left">
<div class="logo"><span class="pi">&pi;</span> RuView</div>
<div class="header-title">Dual-Modal Pose Estimation — Live Video + WiFi CSI Fusion</div>
</div>
<div class="header-right">
<select id="mode-select" class="mode-select">
<option value="dual">Dual Mode (Video + CSI)</option>
<option value="video">Video Only</option>
<option value="csi">CSI Only (WiFi)</option>
</select>
<div class="status-badge">
<span id="status-dot" class="status-dot offline"></span>
<span id="status-label">READY</span>
</div>
<span id="fps-display" class="fps-badge">-- FPS</span>
<a href="index.html" class="back-link">&larr; Dashboard</a>
<a href="observatory.html" class="back-link">Observatory &rarr;</a>
</div>
</header>
<!-- Main Grid -->
<div class="main-grid">
<!-- Video + Skeleton Panel -->
<div class="video-panel">
<video id="webcam" autoplay playsinline muted></video>
<canvas id="skeleton-canvas"></canvas>
<div class="video-overlay-label" id="mode-label">DUAL FUSION</div>
<div id="camera-prompt" class="camera-prompt">
<div class="camera-prompt-label" id="prompt-mode-label">DUAL FUSION</div>
<p>Enable your webcam for live video pose estimation.<br>
Or switch to <strong>CSI Only</strong> mode for WiFi-based sensing.</p>
<button id="start-camera-btn">Enable Camera</button>
</div>
</div>
<!-- Side Panels -->
<div class="side-panels">
<!-- Fusion Confidence -->
<div class="panel">
<div class="panel-title">&#9670; Fusion Confidence</div>
<div class="fusion-bars">
<div class="bar-row">
<span class="bar-label">Video</span>
<div class="bar-track"><div class="bar-fill video" id="video-bar" style="width:0%"></div></div>
<span class="bar-value" id="video-bar-val">0%</span>
</div>
<div class="bar-row">
<span class="bar-label">CSI</span>
<div class="bar-track"><div class="bar-fill csi" id="csi-bar" style="width:0%"></div></div>
<span class="bar-value" id="csi-bar-val">0%</span>
</div>
<div class="bar-row">
<span class="bar-label">Fused</span>
<div class="bar-track"><div class="bar-fill fused" id="fused-bar" style="width:0%"></div></div>
<span class="bar-value" id="fused-bar-val">0%</span>
</div>
</div>
<div style="margin-top:8px; font-size:10px; color:var(--text-label)">
Cross-modal: <span id="cross-modal-sim" style="color:var(--green-glow)">0.000</span>
</div>
</div>
<!-- CSI Heatmap -->
<div class="panel">
<div class="panel-title">&#9670; CSI Amplitude Heatmap</div>
<div class="csi-canvas-wrapper">
<canvas id="csi-canvas" width="320" height="100"></canvas>
</div>
</div>
<!-- RSSI Signal Strength -->
<div class="panel">
<div class="panel-title">&#9670; RSSI Signal Strength</div>
<div class="rssi-row">
<div class="rssi-gauge">
<div class="rssi-bar-track">
<div class="rssi-bar-fill" id="rssi-bar" style="width:0%"></div>
</div>
<div class="rssi-values">
<span class="rssi-dbm" id="rssi-value">-- dBm</span>
<span class="rssi-quality" id="rssi-quality">--</span>
</div>
</div>
<canvas id="rssi-sparkline" width="160" height="32"></canvas>
</div>
</div>
<!-- Embedding Space -->
<div class="panel">
<div class="panel-title">&#9670; Embedding Space (2D Projection)</div>
<div class="embedding-canvas-wrapper">
<canvas id="embedding-canvas" width="320" height="100"></canvas>
</div>
</div>
<!-- RuVector Attention Pipeline -->
<div class="panel">
<div class="panel-title">&#9670; RuVector WASM Attention Pipeline</div>
<div class="rv-pipeline">
<div class="rv-stage" id="rv-flash">Flash</div>
<div class="rv-arrow">&rarr;</div>
<div class="rv-stage" id="rv-mha">MHA</div>
<div class="rv-arrow">&rarr;</div>
<div class="rv-stage" id="rv-hyp">Hyper</div>
<div class="rv-arrow">&rarr;</div>
<div class="rv-stage" id="rv-lin">Linear</div>
<div class="rv-arrow">&rarr;</div>
<div class="rv-stage" id="rv-moe">MoE</div>
<div class="rv-arrow">&rarr;</div>
<div class="rv-stage" id="rv-lg">L+G</div>
</div>
<div class="rv-stats">
<span>Energy: <span id="rv-energy" style="color:var(--green-glow)">--</span></span>
<span>Refinement: <span id="rv-refine" style="color:var(--cyan)">--</span></span>
<span>Pose Impact: <span id="rv-impact" style="color:var(--amber)">--</span></span>
</div>
</div>
<!-- Latency -->
<div class="panel">
<div class="panel-title">&#9670; Pipeline Latency</div>
<div class="latency-grid">
<div class="latency-item">
<div class="latency-value" id="lat-video">--</div>
<div class="latency-label">Video CNN</div>
</div>
<div class="latency-item">
<div class="latency-value" id="lat-csi">--</div>
<div class="latency-label">CSI CNN</div>
</div>
<div class="latency-item">
<div class="latency-value" id="lat-fusion">--</div>
<div class="latency-label">Fusion</div>
</div>
<div class="latency-item">
<div class="latency-value" id="lat-total">--</div>
<div class="latency-label">Total</div>
</div>
</div>
</div>
<!-- Controls -->
<div class="panel">
<div class="panel-title">&#9670; Controls</div>
<div class="controls-row">
<button class="btn" id="pause-btn">⏸ Pause</button>
</div>
<div class="slider-row">
<label>Confidence</label>
<input type="range" id="confidence-slider" min="0" max="1" step="0.05" value="0.3">
<span class="slider-val" id="confidence-value">0.30</span>
</div>
<div style="margin-top:10px">
<div class="panel-title" style="margin-bottom:6px">&#9670; Live CSI Source</div>
<div style="display:flex;gap:6px">
<input type="text" id="ws-url" placeholder="ws://localhost:3030/ws/csi"
style="flex:1;background:rgba(255,255,255,0.05);border:1px solid var(--bg-panel-border);
color:var(--text-primary);padding:5px 8px;border-radius:4px;font-size:11px;
font-family:'JetBrains Mono',monospace">
<button class="btn" id="connect-ws-btn">Connect</button>
</div>
</div>
</div>
</div><!-- /side-panels -->
<!-- Bottom Bar -->
<div class="bottom-bar">
<div>
RuView &middot; Dual-Modal Pose Estimation &middot;
Architecture: Conv2D &rarr; RuVector 6-Stage Attention (Flash+MHA+Hyperbolic+Linear+MoE+L/G) &rarr; Fusion &rarr; 26-Keypoint Pose
</div>
<div>
<a href="https://github.com/ruvnet/RuView">GitHub</a> &middot;
CNN: <span id="cnn-backend">ruvector-cnn (loading…)</span> &middot;
<a href="observatory.html">Observatory</a>
</div>
</div>
</div><!-- /main-grid -->
<script type="module" src="pose-fusion/js/main.js?v=13"></script>
</body>
</html>
+30
View File
@@ -0,0 +1,30 @@
#!/bin/bash
# Build WASM packages for the dual-modal pose estimation demo.
# Requires: wasm-pack (cargo install wasm-pack)
#
# Usage: ./build.sh
#
# Output: pkg/ruvector_cnn_wasm/ — WASM CNN embedder for browser
set -e
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
VENDOR_DIR="$SCRIPT_DIR/../../vendor/ruvector"
OUT_DIR="$SCRIPT_DIR/pkg/ruvector_cnn_wasm"
echo "Building ruvector-cnn-wasm..."
wasm-pack build "$VENDOR_DIR/crates/ruvector-cnn-wasm" \
--target web \
--out-dir "$OUT_DIR" \
--no-typescript
# Remove .gitignore so we can commit the build output for GitHub Pages
rm -f "$OUT_DIR/.gitignore"
echo ""
echo "Build complete!"
echo " WASM: $(du -sh "$OUT_DIR/ruvector_cnn_wasm_bg.wasm" | cut -f1)"
echo " JS: $(du -sh "$OUT_DIR/ruvector_cnn_wasm.js" | cut -f1)"
echo ""
echo "Serve the demo: cd $SCRIPT_DIR/.. && python3 -m http.server 8080"
echo "Open: http://localhost:8080/pose-fusion.html"
+536
View File
@@ -0,0 +1,536 @@
/* RuView Dual-Modal Pose Fusion Demo
Dark theme matching Observatory */
@import url('https://fonts.googleapis.com/css2?family=Inter:wght@300;400;600;700&family=JetBrains+Mono:wght@400;600&display=swap');
:root {
--bg-deep: #080c14;
--bg-panel: rgba(8, 16, 28, 0.92);
--bg-panel-border: rgba(0, 210, 120, 0.25);
--green-glow: #00d878;
--green-bright:#3eff8a;
--green-dim: #0a6b3a;
--amber: #ffb020;
--amber-dim: #a06800;
--blue-signal: #2090ff;
--blue-dim: #0a3060;
--red-alert: #ff3040;
--cyan: #00e5ff;
--text-primary: #e8ece0;
--text-secondary: rgba(232,236,224, 0.55);
--text-label: rgba(232,236,224, 0.35);
--radius: 8px;
}
* { margin: 0; padding: 0; box-sizing: border-box; }
body {
background: var(--bg-deep);
font-family: 'Inter', -apple-system, sans-serif;
color: var(--text-primary);
-webkit-font-smoothing: antialiased;
overflow-x: hidden;
min-height: 100vh;
}
/* === Header === */
.header {
display: flex;
align-items: center;
justify-content: space-between;
padding: 16px 24px;
border-bottom: 1px solid var(--bg-panel-border);
background: var(--bg-panel);
backdrop-filter: blur(12px);
}
.header-left {
display: flex;
align-items: center;
gap: 16px;
}
.logo {
font-weight: 700;
font-size: 24px;
color: var(--green-glow);
}
.logo .pi { font-style: normal; }
.header-title {
font-size: 14px;
color: var(--text-secondary);
font-weight: 300;
}
.header-right {
display: flex;
align-items: center;
gap: 16px;
}
.mode-select {
background: rgba(0,210,120,0.1);
border: 1px solid var(--bg-panel-border);
color: var(--text-primary);
padding: 6px 12px;
border-radius: var(--radius);
font-family: inherit;
font-size: 13px;
cursor: pointer;
}
.mode-select option { background: #0c1420; }
.status-badge {
display: flex;
align-items: center;
gap: 6px;
font-family: 'JetBrains Mono', monospace;
font-size: 12px;
padding: 4px 10px;
border-radius: 12px;
background: rgba(0,210,120,0.1);
border: 1px solid var(--bg-panel-border);
}
.status-dot {
width: 8px; height: 8px;
border-radius: 50%;
background: var(--green-glow);
box-shadow: 0 0 8px var(--green-glow);
animation: pulse-dot 2s ease infinite;
}
.status-dot.offline { background: #555; box-shadow: none; animation: none; }
.status-dot.warning { background: var(--amber); box-shadow: 0 0 8px var(--amber); }
@keyframes pulse-dot {
0%, 100% { opacity: 1; }
50% { opacity: 0.5; }
}
.fps-badge {
font-family: 'JetBrains Mono', monospace;
font-size: 12px;
color: var(--green-glow);
}
.back-link {
color: var(--text-secondary);
text-decoration: none;
font-size: 13px;
transition: color 0.2s;
}
.back-link:hover { color: var(--green-glow); }
/* === Main Layout === */
.main-grid {
display: grid;
grid-template-columns: 1fr 360px;
grid-template-rows: 1fr auto;
gap: 16px;
padding: 16px 24px;
height: calc(100vh - 72px);
overflow: hidden;
}
.video-panel {
grid-row: 1;
}
.side-panels {
grid-row: 1;
}
/* === Video Panel === */
.video-panel {
position: relative;
background: #000;
border-radius: var(--radius);
border: 1px solid var(--bg-panel-border);
overflow: hidden;
min-height: 0;
}
.video-panel video {
width: 100%;
height: 100%;
object-fit: cover;
transform: scaleX(-1);
}
.video-panel canvas {
position: absolute;
top: 0; left: 0;
width: 100%;
height: 100%;
transform: scaleX(-1);
}
.video-overlay-label {
position: absolute;
top: 12px; left: 12px;
font-family: 'JetBrains Mono', monospace;
font-size: 11px;
padding: 4px 8px;
background: rgba(0,0,0,0.7);
border-radius: 4px;
color: var(--green-glow);
z-index: 5;
transform: scaleX(-1);
}
.camera-prompt {
position: absolute;
top: 0; left: 0; right: 0; bottom: 0;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
text-align: center;
color: var(--text-secondary);
padding: 24px;
z-index: 6;
background: radial-gradient(ellipse at center, rgba(0,210,120,0.08) 0%, rgba(8,12,20,0.95) 70%);
}
.camera-prompt button {
margin-top: 16px;
padding: 10px 24px;
background: var(--green-glow);
color: #000;
border: none;
border-radius: var(--radius);
font-family: inherit;
font-weight: 600;
font-size: 14px;
cursor: pointer;
transition: background 0.2s;
}
.camera-prompt button:hover { background: var(--green-bright); }
.camera-prompt-label {
font-family: 'JetBrains Mono', monospace;
font-size: 14px;
font-weight: 600;
letter-spacing: 2px;
color: var(--green-glow);
text-shadow: 0 0 12px rgba(0,216,120,0.4);
margin-bottom: 12px;
}
/* === Side Panels === */
.side-panels {
display: flex;
flex-direction: column;
gap: 8px;
overflow-y: auto;
min-height: 0;
max-height: 100%;
scrollbar-width: thin;
scrollbar-color: var(--green-dim) transparent;
}
.panel {
background: var(--bg-panel);
border: 1px solid var(--bg-panel-border);
border-radius: var(--radius);
padding: 10px 14px;
flex-shrink: 0;
}
.panel-title {
font-size: 11px;
text-transform: uppercase;
letter-spacing: 1.2px;
color: var(--text-label);
margin-bottom: 10px;
display: flex;
align-items: center;
gap: 6px;
}
/* === CSI Heatmap === */
.csi-canvas-wrapper {
position: relative;
border-radius: 4px;
overflow: hidden;
background: #000;
}
.csi-canvas-wrapper canvas {
width: 100%;
display: block;
}
/* === Fusion Bars === */
.fusion-bars {
display: flex;
flex-direction: column;
gap: 8px;
}
.bar-row {
display: flex;
align-items: center;
gap: 8px;
}
.bar-label {
font-family: 'JetBrains Mono', monospace;
font-size: 11px;
color: var(--text-secondary);
width: 55px;
text-align: right;
}
.bar-track {
flex: 1;
height: 6px;
background: rgba(255,255,255,0.06);
border-radius: 3px;
overflow: hidden;
}
.bar-fill {
height: 100%;
border-radius: 3px;
transition: width 0.3s ease;
}
.bar-fill.video { background: var(--cyan); }
.bar-fill.csi { background: var(--amber); }
.bar-fill.fused { background: var(--green-glow); box-shadow: 0 0 8px var(--green-glow); }
.bar-value {
font-family: 'JetBrains Mono', monospace;
font-size: 11px;
color: var(--text-primary);
width: 36px;
}
/* === Embedding Space === */
.embedding-canvas-wrapper {
position: relative;
background: #000;
border-radius: 4px;
overflow: hidden;
}
.embedding-canvas-wrapper canvas {
width: 100%;
display: block;
}
/* === RuVector Pipeline === */
.rv-pipeline {
display: flex;
align-items: center;
gap: 2px;
margin-bottom: 8px;
flex-wrap: wrap;
}
.rv-stage {
font-family: 'JetBrains Mono', monospace;
font-size: 10px;
padding: 3px 6px;
border-radius: 3px;
background: rgba(0,210,120,0.12);
border: 1px solid rgba(0,210,120,0.3);
color: var(--green-glow);
transition: all 0.3s;
}
.rv-stage.active {
background: rgba(0,210,120,0.25);
box-shadow: 0 0 6px rgba(0,210,120,0.3);
}
.rv-arrow {
font-size: 10px;
color: var(--text-label);
}
.rv-stats {
display: flex;
gap: 12px;
font-family: 'JetBrains Mono', monospace;
font-size: 10px;
color: var(--text-secondary);
}
/* === Latency Panel === */
.latency-grid {
display: grid;
grid-template-columns: repeat(4, 1fr);
gap: 6px;
}
.latency-item {
text-align: center;
padding: 6px 0;
}
.latency-value {
font-family: 'JetBrains Mono', monospace;
font-size: 16px;
font-weight: 600;
color: var(--green-glow);
}
.latency-label {
font-size: 10px;
color: var(--text-label);
margin-top: 2px;
}
/* === Controls === */
.controls-row {
display: flex;
gap: 8px;
flex-wrap: wrap;
}
.btn {
padding: 6px 14px;
border: 1px solid var(--bg-panel-border);
background: rgba(0,210,120,0.08);
color: var(--text-primary);
border-radius: var(--radius);
font-family: inherit;
font-size: 12px;
cursor: pointer;
transition: all 0.2s;
}
.btn:hover { background: rgba(0,210,120,0.2); }
.btn.active { background: var(--green-glow); color: #000; font-weight: 600; }
.slider-row {
display: flex;
align-items: center;
gap: 8px;
margin-top: 8px;
}
.slider-row label {
font-size: 11px;
color: var(--text-secondary);
white-space: nowrap;
}
.slider-row input[type=range] {
flex: 1;
accent-color: var(--green-glow);
}
.slider-row .slider-val {
font-family: 'JetBrains Mono', monospace;
font-size: 11px;
width: 32px;
color: var(--green-glow);
}
/* === Bottom Bar === */
.bottom-bar {
grid-column: 1 / -1;
display: flex;
align-items: center;
justify-content: space-between;
padding: 10px 16px;
background: var(--bg-panel);
border: 1px solid var(--bg-panel-border);
border-radius: var(--radius);
font-family: 'JetBrains Mono', monospace;
font-size: 11px;
color: var(--text-secondary);
}
.bottom-bar a {
color: var(--green-glow);
text-decoration: none;
}
/* === RSSI Signal Strength === */
.rssi-row {
display: flex;
align-items: center;
gap: 12px;
}
.rssi-gauge { flex: 1; }
.rssi-bar-track {
height: 8px;
background: rgba(255,255,255,0.06);
border-radius: 4px;
overflow: hidden;
position: relative;
}
.rssi-bar-fill {
height: 100%;
border-radius: 4px;
background: linear-gradient(90deg, var(--red-alert), var(--amber), var(--green-glow));
transition: width 0.4s ease;
position: relative;
box-shadow: 0 0 6px rgba(0,210,120,0.3);
}
.rssi-bar-fill::after {
content: '';
position: absolute;
top: 0; left: 0; right: 0; bottom: 0;
background: linear-gradient(90deg, transparent 0%, rgba(255,255,255,0.2) 50%, transparent 100%);
animation: rssi-shimmer 2s ease-in-out infinite;
}
@keyframes rssi-shimmer {
0% { transform: translateX(-100%); }
100% { transform: translateX(100%); }
}
.rssi-values {
display: flex;
justify-content: space-between;
margin-top: 4px;
}
.rssi-dbm {
font-family: 'JetBrains Mono', monospace;
font-size: 14px;
font-weight: 600;
color: var(--green-glow);
}
.rssi-quality {
font-family: 'JetBrains Mono', monospace;
font-size: 11px;
color: var(--text-secondary);
text-transform: uppercase;
}
#rssi-sparkline {
flex-shrink: 0;
border-radius: 4px;
background: rgba(0,0,0,0.3);
}
/* === Skeleton colors === */
.skeleton-joint { fill: var(--green-glow); }
.skeleton-limb { stroke: var(--green-bright); }
.skeleton-joint-csi { fill: var(--amber); }
.skeleton-limb-csi { stroke: var(--amber); }
/* === Responsive === */
@media (max-width: 900px) {
.main-grid {
grid-template-columns: 1fr;
height: auto;
overflow: auto;
}
.video-panel { aspect-ratio: 16/9; max-height: 50vh; }
.side-panels { max-height: none; overflow: visible; }
}
+307
View File
@@ -0,0 +1,307 @@
/**
* CanvasRenderer Renders skeleton overlay on video, CSI heatmap,
* embedding space visualization, and fusion confidence bars.
*/
import { SKELETON_CONNECTIONS } from './pose-decoder.js';
export class CanvasRenderer {
constructor() {
this.colors = {
joint: '#00d878',
jointGlow: 'rgba(0, 216, 120, 0.4)',
limb: '#3eff8a',
limbGlow: 'rgba(62, 255, 138, 0.15)',
csiJoint: '#ffb020',
csiLimb: '#ffc850',
fused: '#00e5ff',
confidence: 'rgba(255,255,255,0.3)',
videoEmb: '#00e5ff',
csiEmb: '#ffb020',
fusedEmb: '#00d878',
};
}
/**
* Draw skeleton overlay on the video canvas
* @param {CanvasRenderingContext2D} ctx
* @param {Array<{x,y,confidence}>} keypoints - Normalized [0,1] coordinates
* @param {number} width - Canvas width
* @param {number} height - Canvas height
* @param {object} opts
*/
drawSkeleton(ctx, keypoints, width, height, opts = {}) {
const minConf = opts.minConfidence || 0.3;
const color = opts.color || 'green';
const jointColor = color === 'amber' ? this.colors.csiJoint : this.colors.joint;
const limbColor = color === 'amber' ? this.colors.csiLimb : this.colors.limb;
const glowColor = color === 'amber' ? 'rgba(255,176,32,0.4)' : this.colors.jointGlow;
// Extended keypoint styling
const fingerColor = '#ff6ef0'; // Magenta for finger tips
const fingerGlow = 'rgba(255,110,240,0.4)';
const fingerLimb = 'rgba(255,110,240,0.5)';
const toeColor = '#6ef0ff'; // Cyan for toes
const neckColor = '#ffffff'; // White for neck
ctx.clearRect(0, 0, width, height);
if (!keypoints || keypoints.length === 0) return;
// Draw limbs first (behind joints)
ctx.lineCap = 'round';
for (const [i, j] of SKELETON_CONNECTIONS) {
const kpA = keypoints[i];
const kpB = keypoints[j];
if (!kpA || !kpB || kpA.confidence < minConf || kpB.confidence < minConf) continue;
const ax = kpA.x * width, ay = kpA.y * height;
const bx = kpB.x * width, by = kpB.y * height;
const avgConf = (kpA.confidence + kpB.confidence) / 2;
// Is this a hand/finger connection? (indices 17-22)
const isFingerLink = i >= 17 && i <= 22 || j >= 17 && j <= 22;
const isToeLink = i >= 23 && i <= 24 || j >= 23 && j <= 24;
// Glow
ctx.strokeStyle = isFingerLink ? fingerLimb : this.colors.limbGlow;
ctx.lineWidth = isFingerLink ? 4 : 8;
ctx.globalAlpha = avgConf * (isFingerLink ? 0.3 : 0.4);
ctx.beginPath();
ctx.moveTo(ax, ay);
ctx.lineTo(bx, by);
ctx.stroke();
// Main line
ctx.strokeStyle = isFingerLink ? fingerColor : isToeLink ? toeColor : limbColor;
ctx.lineWidth = isFingerLink || isToeLink ? 1.5 : 2.5;
ctx.globalAlpha = avgConf;
ctx.beginPath();
ctx.moveTo(ax, ay);
ctx.lineTo(bx, by);
ctx.stroke();
}
// Draw joints
ctx.globalAlpha = 1;
for (let idx = 0; idx < keypoints.length; idx++) {
const kp = keypoints[idx];
if (!kp || kp.confidence < minConf) continue;
const x = kp.x * width;
const y = kp.y * height;
const isFinger = idx >= 17 && idx <= 22;
const isToe = idx >= 23 && idx <= 24;
const isNeck = idx === 25;
const r = isFinger ? 2 + kp.confidence * 2 : isToe ? 2 : 3 + kp.confidence * 3;
const jColor = isFinger ? fingerColor : isToe ? toeColor : isNeck ? neckColor : jointColor;
const gColor = isFinger ? fingerGlow : glowColor;
// Glow
ctx.beginPath();
ctx.arc(x, y, r + (isFinger ? 3 : 4), 0, Math.PI * 2);
ctx.fillStyle = gColor;
ctx.globalAlpha = kp.confidence * (isFinger ? 0.5 : 0.6);
ctx.fill();
// Joint dot
ctx.beginPath();
ctx.arc(x, y, r, 0, Math.PI * 2);
ctx.fillStyle = jColor;
ctx.globalAlpha = kp.confidence;
ctx.fill();
// White center (body joints only)
if (!isFinger && !isToe) {
ctx.beginPath();
ctx.arc(x, y, r * 0.4, 0, Math.PI * 2);
ctx.fillStyle = '#fff';
ctx.globalAlpha = kp.confidence * 0.8;
ctx.fill();
}
}
ctx.globalAlpha = 1;
// Confidence label + keypoint count
if (opts.label) {
const visCount = keypoints.filter(kp => kp && kp.confidence >= minConf).length;
ctx.font = '11px "JetBrains Mono", monospace';
ctx.fillStyle = jointColor;
ctx.globalAlpha = 0.8;
ctx.fillText(`${opts.label} · ${visCount} joints`, 8, height - 8);
ctx.globalAlpha = 1;
}
}
/**
* Draw CSI amplitude heatmap
* @param {CanvasRenderingContext2D} ctx
* @param {{ data: Float32Array, width: number, height: number }} heatmap
* @param {number} canvasW
* @param {number} canvasH
*/
drawCsiHeatmap(ctx, heatmap, canvasW, canvasH) {
ctx.clearRect(0, 0, canvasW, canvasH);
if (!heatmap || !heatmap.data || heatmap.height < 2) {
ctx.fillStyle = '#0a0e18';
ctx.fillRect(0, 0, canvasW, canvasH);
ctx.font = '11px "JetBrains Mono", monospace';
ctx.fillStyle = 'rgba(255,255,255,0.3)';
ctx.fillText('Waiting for CSI data...', 8, canvasH / 2);
return;
}
const { data, width: dw, height: dh } = heatmap;
const cellW = canvasW / dw;
const cellH = canvasH / dh;
for (let y = 0; y < dh; y++) {
for (let x = 0; x < dw; x++) {
const val = Math.min(1, Math.max(0, data[y * dw + x]));
ctx.fillStyle = this._heatmapColor(val);
ctx.fillRect(x * cellW, y * cellH, cellW + 0.5, cellH + 0.5);
}
}
// Axis labels
ctx.font = '9px "JetBrains Mono", monospace';
ctx.fillStyle = 'rgba(255,255,255,0.4)';
ctx.fillText('Subcarrier →', 4, canvasH - 4);
ctx.save();
ctx.translate(canvasW - 4, canvasH - 4);
ctx.rotate(-Math.PI / 2);
ctx.fillText('Time ↑', 0, 0);
ctx.restore();
}
/**
* Draw embedding space 2D projection
* @param {CanvasRenderingContext2D} ctx
* @param {{ video: Array, csi: Array, fused: Array }} points
* @param {number} w
* @param {number} h
*/
drawEmbeddingSpace(ctx, points, w, h) {
ctx.fillStyle = '#050810';
ctx.fillRect(0, 0, w, h);
// Grid
ctx.strokeStyle = 'rgba(255,255,255,0.05)';
ctx.lineWidth = 0.5;
for (let i = 0; i <= 4; i++) {
const x = (i / 4) * w;
ctx.beginPath(); ctx.moveTo(x, 0); ctx.lineTo(x, h); ctx.stroke();
const y = (i / 4) * h;
ctx.beginPath(); ctx.moveTo(0, y); ctx.lineTo(w, y); ctx.stroke();
}
// Axes
ctx.strokeStyle = 'rgba(255,255,255,0.1)';
ctx.lineWidth = 1;
ctx.beginPath(); ctx.moveTo(w / 2, 0); ctx.lineTo(w / 2, h); ctx.stroke();
ctx.beginPath(); ctx.moveTo(0, h / 2); ctx.lineTo(w, h / 2); ctx.stroke();
// Auto-scale: find max extent across all point sets
let maxExtent = 0.01;
for (const pts of [points.video, points.csi, points.fused]) {
if (!pts) continue;
for (const p of pts) {
if (!p) continue;
maxExtent = Math.max(maxExtent, Math.abs(p[0]), Math.abs(p[1]));
}
}
const scale = 0.42 / maxExtent; // Fill ~84% of half-width
const drawPoints = (pts, color, size) => {
if (!pts || pts.length === 0) return;
const len = pts.length;
// Draw trail line connecting recent points
if (len >= 2) {
ctx.beginPath();
let started = false;
for (let i = 0; i < len; i++) {
const p = pts[i];
if (!p) continue;
const px = w / 2 + p[0] * scale * w;
const py = h / 2 + p[1] * scale * h;
if (px < -10 || px > w + 10 || py < -10 || py > h + 10) continue;
if (!started) { ctx.moveTo(px, py); started = true; }
else ctx.lineTo(px, py);
}
ctx.strokeStyle = color;
ctx.globalAlpha = 0.2;
ctx.lineWidth = 1;
ctx.stroke();
}
// Draw dots with glow on newest
for (let i = 0; i < len; i++) {
const p = pts[i];
if (!p) continue;
const age = 1 - (i / len) * 0.7;
const px = w / 2 + p[0] * scale * w;
const py = h / 2 + p[1] * scale * h;
if (px < -10 || px > w + 10 || py < -10 || py > h + 10) continue;
// Glow on newest point
if (i === len - 1) {
ctx.beginPath();
ctx.arc(px, py, size + 4, 0, Math.PI * 2);
ctx.fillStyle = color;
ctx.globalAlpha = 0.3;
ctx.fill();
}
ctx.beginPath();
ctx.arc(px, py, i === len - 1 ? size + 1 : size, 0, Math.PI * 2);
ctx.fillStyle = color;
ctx.globalAlpha = age * 0.8;
ctx.fill();
}
};
drawPoints(points.video, this.colors.videoEmb, 3);
drawPoints(points.csi, this.colors.csiEmb, 3);
drawPoints(points.fused, this.colors.fusedEmb, 4);
ctx.globalAlpha = 1;
// Legend
ctx.font = '9px "JetBrains Mono", monospace';
const legends = [
{ color: this.colors.videoEmb, label: 'Video' },
{ color: this.colors.csiEmb, label: 'CSI' },
{ color: this.colors.fusedEmb, label: 'Fused' },
];
legends.forEach((l, i) => {
const ly = 12 + i * 14;
ctx.fillStyle = l.color;
ctx.beginPath();
ctx.arc(10, ly - 3, 3, 0, Math.PI * 2);
ctx.fill();
ctx.fillStyle = 'rgba(255,255,255,0.5)';
ctx.fillText(l.label, 18, ly);
});
}
_heatmapColor(val) {
// Dark blue → cyan → green → yellow → red
if (val < 0.25) {
const t = val / 0.25;
return `rgb(${Math.floor(t * 20)}, ${Math.floor(20 + t * 60)}, ${Math.floor(60 + t * 100)})`;
} else if (val < 0.5) {
const t = (val - 0.25) / 0.25;
return `rgb(${Math.floor(20 + t * 20)}, ${Math.floor(80 + t * 100)}, ${Math.floor(160 - t * 60)})`;
} else if (val < 0.75) {
const t = (val - 0.5) / 0.25;
return `rgb(${Math.floor(40 + t * 180)}, ${Math.floor(180 + t * 75)}, ${Math.floor(100 - t * 80)})`;
} else {
const t = (val - 0.75) / 0.25;
return `rgb(${Math.floor(220 + t * 35)}, ${Math.floor(255 - t * 120)}, ${Math.floor(20 - t * 20)})`;
}
}
}
+443
View File
@@ -0,0 +1,443 @@
/**
* CNN Embedder RuVector Attention-powered feature extractor.
*
* Uses the real ruvector-attention-wasm WASM module for Multi-Head Attention
* and Flash Attention on CSI/video data. Falls back to a JS Conv2D pipeline
* when WASM is not available.
*
* Pipeline: Conv2D BatchNorm ReLU Pool RuVector Attention Project L2 Normalize
* Two instances are created: one for video frames, one for CSI pseudo-images.
*/
// Seeded PRNG for deterministic weight initialization
function mulberry32(seed) {
return function() {
let t = (seed += 0x6D2B79F5);
t = Math.imul(t ^ (t >>> 15), t | 1);
t ^= t + Math.imul(t ^ (t >>> 7), t | 61);
return ((t ^ (t >>> 14)) >>> 0) / 4294967296;
};
}
export class CnnEmbedder {
/**
* @param {object} opts
* @param {number} opts.inputSize - Square input dimension (default 56 for speed)
* @param {number} opts.embeddingDim - Output embedding dimension (default 128)
* @param {boolean} opts.normalize - L2 normalize output
* @param {number} opts.seed - PRNG seed for weight init
*/
constructor(opts = {}) {
this.inputSize = opts.inputSize || 56;
this.embeddingDim = opts.embeddingDim || 128;
this.normalize = opts.normalize !== false;
this.wasmEmbedder = null;
this.rvAttention = null; // RuVector Multi-Head Attention (WASM)
this.rvFlash = null; // RuVector Flash Attention (WASM)
this.rvHyperbolic = null; // RuVector Hyperbolic Attention (hierarchical body)
this.rvMoE = null; // RuVector Mixture-of-Experts (body-region routing)
this.rvLinear = null; // RuVector Linear Attention (O(n) fast hand refinement)
this.rvLocalGlobal = null; // RuVector Local-Global Attention (detail + context)
this.rvModule = null; // RuVector WASM module reference
this.useRuVector = false;
// Initialize weights with deterministic PRNG
const rng = mulberry32(opts.seed || 42);
const randRange = (lo, hi) => lo + rng() * (hi - lo);
// Conv 3x3: 3 input channels → 16 output channels
this.convWeights = new Float32Array(3 * 3 * 3 * 16);
for (let i = 0; i < this.convWeights.length; i++) {
this.convWeights[i] = randRange(-0.15, 0.15);
}
// BatchNorm params (16 channels)
this.bnGamma = new Float32Array(16).fill(1.0);
this.bnBeta = new Float32Array(16).fill(0.0);
this.bnMean = new Float32Array(16).fill(0.0);
this.bnVar = new Float32Array(16).fill(1.0);
// Projection: 16 → embeddingDim (used when RuVector not available)
this.projWeights = new Float32Array(16 * this.embeddingDim);
for (let i = 0; i < this.projWeights.length; i++) {
this.projWeights[i] = randRange(-0.1, 0.1);
}
// Attention projection: attention_dim → embeddingDim
this.attnProjWeights = new Float32Array(16 * this.embeddingDim);
for (let i = 0; i < this.attnProjWeights.length; i++) {
this.attnProjWeights[i] = randRange(-0.08, 0.08);
}
}
/**
* Try to load RuVector attention WASM, then fall back to ruvector-cnn-wasm
* @param {string} wasmPath - Path to the WASM package directory
*/
async tryLoadWasm(wasmPath) {
// First try: RuVector Attention WASM (the real thing — browser ESM build)
try {
const attnBase = new URL('../pkg/ruvector-attention/ruvector_attention_browser.js', import.meta.url).href;
const mod = await import(attnBase);
await mod.default(); // async WASM init via fetch
mod.init();
// Create all 6 attention mechanisms
this.rvAttention = new mod.WasmMultiHeadAttention(16, 4);
this.rvFlash = new mod.WasmFlashAttention(16, 8);
this.rvHyperbolic = new mod.WasmHyperbolicAttention(16, -1.0);
this.rvMoE = new mod.WasmMoEAttention(16, 3, 2);
this.rvLinear = new mod.WasmLinearAttention(16, 16);
this.rvLocalGlobal = new mod.WasmLocalGlobalAttention(16, 4, 2);
this.rvModule = mod;
this.useRuVector = true;
// Log available mechanisms
const mechs = mod.available_mechanisms();
console.log(`[CNN] RuVector WASM v${mod.version()} — all 6 attention mechanisms active`, mechs);
return true;
} catch (e) {
console.log('[CNN] RuVector Attention WASM not available:', e.message);
}
// Second try: ruvector-cnn-wasm (legacy path)
try {
const mod = await import(`${wasmPath}/ruvector_cnn_wasm.js`);
await mod.default();
const config = new mod.EmbedderConfig();
config.input_size = this.inputSize;
config.embedding_dim = this.embeddingDim;
config.normalize = this.normalize;
this.wasmEmbedder = new mod.WasmCnnEmbedder(config);
console.log('[CNN] WASM CNN embedder loaded successfully');
return true;
} catch (e) {
console.log('[CNN] WASM CNN not available, using JS fallback:', e.message);
return false;
}
}
/**
* Extract embedding from RGB image data
* @param {Uint8Array} rgbData - RGB pixel data (H*W*3)
* @param {number} width
* @param {number} height
* @returns {Float32Array} embedding vector
*/
extract(rgbData, width, height) {
if (this.wasmEmbedder) {
try {
const result = this.wasmEmbedder.extract(rgbData, width, height);
return new Float32Array(result);
} catch (_) { /* fallback to JS */ }
}
return this._extractJS(rgbData, width, height);
}
_extractJS(rgbData, width, height) {
// 1. Resize to inputSize × inputSize if needed
const sz = this.inputSize;
let input;
if (width === sz && height === sz) {
input = new Float32Array(rgbData.length);
for (let i = 0; i < rgbData.length; i++) input[i] = rgbData[i] / 255.0;
} else {
input = this._resize(rgbData, width, height, sz, sz);
}
// 2. ImageNet normalization
const mean = [0.485, 0.456, 0.406];
const std = [0.229, 0.224, 0.225];
const pixels = sz * sz;
for (let i = 0; i < pixels; i++) {
input[i * 3] = (input[i * 3] - mean[0]) / std[0];
input[i * 3 + 1] = (input[i * 3 + 1] - mean[1]) / std[1];
input[i * 3 + 2] = (input[i * 3 + 2] - mean[2]) / std[2];
}
// 3. Conv2D 3x3 (3 → 16 channels)
const convOut = this._conv2d3x3(input, sz, sz, 3, 16);
// 4. BatchNorm
this._batchNorm(convOut, 16);
// 5. ReLU
for (let i = 0; i < convOut.length; i++) {
if (convOut[i] < 0) convOut[i] = 0;
}
// 6. Global average pooling → spatial tokens (each 16-dim)
const outH = sz - 2, outW = sz - 2;
const spatial = outH * outW;
// 7. RuVector Attention (if loaded) — apply attention over spatial tokens
if (this.useRuVector && this.rvAttention) {
return this._extractWithAttention(convOut, spatial, 16);
}
// Fallback: simple global average pool + linear projection
const pooled = new Float32Array(16);
for (let i = 0; i < spatial; i++) {
for (let c = 0; c < 16; c++) {
pooled[c] += convOut[i * 16 + c];
}
}
for (let c = 0; c < 16; c++) pooled[c] /= spatial;
// Linear projection → embeddingDim
const emb = new Float32Array(this.embeddingDim);
for (let o = 0; o < this.embeddingDim; o++) {
let sum = 0;
for (let i = 0; i < 16; i++) {
sum += pooled[i] * this.projWeights[i * this.embeddingDim + o];
}
emb[o] = sum;
}
// L2 normalize
if (this.normalize) {
let norm = 0;
for (let i = 0; i < emb.length; i++) norm += emb[i] * emb[i];
norm = Math.sqrt(norm);
if (norm > 1e-8) {
for (let i = 0; i < emb.length; i++) emb[i] /= norm;
}
}
return emb;
}
/**
* Full 6-stage RuVector WASM attention pipeline:
* 1. Flash Attention (efficient O(n) pre-screening of spatial tokens)
* 2. Multi-Head Attention (global spatial reasoning)
* 3. Hyperbolic Attention (hierarchical body-part structure, Poincaré ball)
* 4. Linear Attention (O(n) refinement for fine detail hands/extremities)
* 5. MoE Attention (body-region specialized expert routing)
* 6. Local-Global Attention (local detail + global context fusion)
* Weighted blend + batch_normalize + project + L2 normalize
*/
_extractWithAttention(convOut, numTokens, channels) {
const mod = this.rvModule;
// Subsample spatial tokens for attention (max 64 for speed)
const maxTokens = 64;
const step = numTokens > maxTokens ? Math.floor(numTokens / maxTokens) : 1;
const tokens = [];
for (let i = 0; i < numTokens && tokens.length < maxTokens; i += step) {
const token = new Float32Array(channels);
for (let c = 0; c < channels; c++) {
token[c] = convOut[i * channels + c];
}
tokens.push(token);
}
const numQueries = Math.min(4, tokens.length);
const queryStride = Math.floor(tokens.length / numQueries);
// === Stage 1: Flash Attention (efficient pre-screening) ===
const flashOut = new Float32Array(channels);
try {
// Flash attention with block size 8 for efficient O(n) screening
const result = this.rvFlash.compute(tokens[0], tokens, tokens);
for (let c = 0; c < channels; c++) flashOut[c] = result[c];
} catch (_) {
flashOut.set(tokens[0]);
}
// === Stage 2: Multi-Head Attention (global spatial reasoning) ===
const mhaOut = new Float32Array(channels);
for (let q = 0; q < numQueries; q++) {
const queryToken = tokens[q * queryStride];
try {
const result = this.rvAttention.compute(queryToken, tokens, tokens);
for (let c = 0; c < channels; c++) mhaOut[c] += result[c] / numQueries;
} catch (_) {
for (let c = 0; c < channels; c++) mhaOut[c] += queryToken[c] / numQueries;
}
}
// === Stage 3: Hyperbolic Attention (hierarchical body structure) ===
const hyOut = new Float32Array(channels);
try {
const result = this.rvHyperbolic.compute(mhaOut, tokens, tokens);
for (let c = 0; c < channels; c++) hyOut[c] = result[c];
} catch (_) {
hyOut.set(mhaOut);
}
// === Stage 4: Linear Attention (O(n) fast refinement for extremities) ===
const linOut = new Float32Array(channels);
try {
const result = this.rvLinear.compute(hyOut, tokens, tokens);
for (let c = 0; c < channels; c++) linOut[c] = result[c];
} catch (_) {
linOut.set(hyOut);
}
// === Stage 5: MoE Attention (body-region expert routing) ===
const moeOut = new Float32Array(channels);
try {
const result = this.rvMoE.compute(linOut, tokens, tokens);
for (let c = 0; c < channels; c++) moeOut[c] = result[c];
} catch (_) {
moeOut.set(linOut);
}
// === Stage 6: Local-Global Attention (detail + context) ===
const lgOut = new Float32Array(channels);
try {
const result = this.rvLocalGlobal.compute(moeOut, tokens, tokens);
for (let c = 0; c < channels; c++) lgOut[c] = result[c];
} catch (_) {
lgOut.set(moeOut);
}
// === Blend all 6 outputs ===
// Use WASM softmax on log-energy scores for dynamic stage weighting
const blended = new Float32Array(channels);
const stages = [flashOut, mhaOut, hyOut, linOut, moeOut, lgOut];
// Use log-energy to prevent exp() overflow in softmax
const logEnergies = new Float32Array(6);
for (let s = 0; s < 6; s++) {
const e = this._energy(stages[s]);
logEnergies[s] = e > 1e-10 ? Math.log(e) : -20;
}
try { mod.softmax(logEnergies); } catch (_) {
let max = -Infinity;
for (let i = 0; i < 6; i++) max = Math.max(max, logEnergies[i]);
let sum = 0;
for (let i = 0; i < 6; i++) { logEnergies[i] = Math.exp(logEnergies[i] - max); sum += logEnergies[i]; }
for (let i = 0; i < 6; i++) logEnergies[i] /= sum;
}
for (let c = 0; c < channels; c++) {
for (let s = 0; s < 6; s++) {
blended[c] += logEnergies[s] * stages[s][c];
}
}
// Batch normalize only when we have enough diversity (skip for single vectors)
// Single-vector batch norm collapses to zeros, killing embedding space
let normed = blended;
// Project to embeddingDim
const emb = new Float32Array(this.embeddingDim);
for (let o = 0; o < this.embeddingDim; o++) {
let sum = 0;
for (let i = 0; i < channels; i++) {
sum += normed[i] * this.attnProjWeights[i * this.embeddingDim + o];
}
emb[o] = sum;
}
// L2 normalize using RuVector WASM
if (this.normalize) {
try { mod.normalize(emb); } catch (_) {
let norm = 0;
for (let i = 0; i < emb.length; i++) norm += emb[i] * emb[i];
norm = Math.sqrt(norm);
if (norm > 1e-8) for (let i = 0; i < emb.length; i++) emb[i] /= norm;
}
}
return emb;
}
/** Compute vector energy (L2 norm squared) for attention weighting */
_energy(vec) {
let e = 0;
for (let i = 0; i < vec.length; i++) e += vec[i] * vec[i];
return e;
}
_conv2d3x3(input, H, W, Cin, Cout) {
const outH = H - 2, outW = W - 2;
const output = new Float32Array(outH * outW * Cout);
for (let y = 0; y < outH; y++) {
for (let x = 0; x < outW; x++) {
for (let co = 0; co < Cout; co++) {
let sum = 0;
for (let ky = 0; ky < 3; ky++) {
for (let kx = 0; kx < 3; kx++) {
for (let ci = 0; ci < Cin; ci++) {
const px = ((y + ky) * W + (x + kx)) * Cin + ci;
const wt = (((ky * 3 + kx) * Cin) + ci) * Cout + co;
sum += input[px] * this.convWeights[wt];
}
}
}
output[(y * outW + x) * Cout + co] = sum;
}
}
}
return output;
}
_batchNorm(data, channels) {
const spatial = data.length / channels;
for (let i = 0; i < spatial; i++) {
for (let c = 0; c < channels; c++) {
const idx = i * channels + c;
data[idx] = this.bnGamma[c] * (data[idx] - this.bnMean[c]) / Math.sqrt(this.bnVar[c] + 1e-5) + this.bnBeta[c];
}
}
}
_resize(rgbData, srcW, srcH, dstW, dstH) {
const output = new Float32Array(dstW * dstH * 3);
const xRatio = srcW / dstW;
const yRatio = srcH / dstH;
for (let y = 0; y < dstH; y++) {
for (let x = 0; x < dstW; x++) {
const sx = Math.min(Math.floor(x * xRatio), srcW - 1);
const sy = Math.min(Math.floor(y * yRatio), srcH - 1);
const srcIdx = (sy * srcW + sx) * 3;
const dstIdx = (y * dstW + x) * 3;
output[dstIdx] = rgbData[srcIdx] / 255.0;
output[dstIdx + 1] = rgbData[srcIdx + 1] / 255.0;
output[dstIdx + 2] = rgbData[srcIdx + 2] / 255.0;
}
}
return output;
}
/** Cosine similarity using WASM when available, JS fallback */
cosineSim(a, b) {
if (this.rvModule) {
try { return this.rvModule.cosine_similarity(a, b); } catch (_) { /* fallback */ }
}
return CnnEmbedder.cosineSimilarity(a, b);
}
/** L2 norm using WASM when available */
l2Norm(vec) {
if (this.rvModule) {
try { return this.rvModule.l2_norm(vec); } catch (_) { /* fallback */ }
}
let norm = 0;
for (let i = 0; i < vec.length; i++) norm += vec[i] * vec[i];
return Math.sqrt(norm);
}
/** Pairwise distance matrix using WASM (for skeleton validation) */
pairwiseDistances(vectors) {
if (this.rvModule) {
try { return this.rvModule.pairwise_distances(vectors); } catch (_) { /* fallback */ }
}
return null;
}
/** Static JS fallback for cosine similarity */
static cosineSimilarity(a, b) {
let dot = 0, normA = 0, normB = 0;
for (let i = 0; i < a.length; i++) {
dot += a[i] * b[i];
normA += a[i] * a[i];
normB += b[i] * b[i];
}
normA = Math.sqrt(normA);
normB = Math.sqrt(normB);
if (normA < 1e-8 || normB < 1e-8) return 0;
return dot / (normA * normB);
}
}
+363
View File
@@ -0,0 +1,363 @@
/**
* CSI Simulator Generates realistic WiFi Channel State Information data.
*
* In live mode, connects to the sensing server via WebSocket.
* In demo mode, generates synthetic CSI that correlates with detected motion.
*
* Outputs: 3-channel pseudo-image (amplitude, phase, temporal diff)
* matching the ADR-018 frame format expectations.
*/
export class CsiSimulator {
static VERSION = 'v4-drift'; // Cache-bust verification
constructor(opts = {}) {
this.subcarriers = opts.subcarriers || 52; // 802.11n HT20
this.timeWindow = opts.timeWindow || 56; // frames in sliding window
this.mode = 'demo'; // 'demo' | 'live'
this.ws = null;
// Circular buffer for CSI frames
this.amplitudeBuffer = [];
this.phaseBuffer = [];
this.frameCount = 0;
// Noise parameters
this._rng = this._mulberry32(opts.seed || 7);
this._noiseState = new Float32Array(this.subcarriers);
this._baseAmplitude = new Float32Array(this.subcarriers);
this._basePhase = new Float32Array(this.subcarriers);
// Initialize base CSI profile (empty room)
for (let i = 0; i < this.subcarriers; i++) {
this._baseAmplitude[i] = 0.5 + 0.3 * Math.sin(i * 0.12);
this._basePhase[i] = (i / this.subcarriers) * Math.PI * 2;
}
// RSSI tracking
this.rssiDbm = -70; // default mid-range
this._rssiTarget = -70;
// Person influence (updated from video motion)
this.personPresence = 0;
this.personX = 0.5;
this.personY = 0.5;
this.personMotion = 0;
}
/**
* Connect to live sensing server WebSocket
* @param {string} url - WebSocket URL (e.g. ws://localhost:3030/ws/csi)
*/
async connectLive(url) {
return new Promise((resolve) => {
try {
this.ws = new WebSocket(url);
this.ws.binaryType = 'arraybuffer';
this.ws.onmessage = (evt) => this._handleLiveFrame(evt.data);
this.ws.onopen = () => { this.mode = 'live'; resolve(true); };
this.ws.onerror = () => resolve(false);
this.ws.onclose = () => { this.mode = 'demo'; };
// Timeout after 3s
setTimeout(() => { if (this.mode !== 'live') resolve(false); }, 3000);
} catch {
resolve(false);
}
});
}
disconnect() {
if (this.ws) { this.ws.close(); this.ws = null; }
this.mode = 'demo';
}
get isLive() { return this.mode === 'live'; }
/**
* Update person state from video detection (for correlated demo data).
* When person exits frame, CSI maintains presence with slow decay
* (simulating through-wall sensing capability).
*/
updatePersonState(presence, x, y, motion) {
// Don't override real CSI sensing with synthetic video-derived state
if (this.mode === 'live') return;
if (presence > 0.1) {
// Person detected in video — update CSI state directly
this.personPresence = presence;
this.personX = x;
this.personY = y;
this.personMotion = motion;
this._lastSeenTime = performance.now();
this._lastSeenX = x;
this._lastSeenY = y;
} else if (this._lastSeenTime) {
// Person NOT in video — CSI "through-wall" persistence
const elapsed = (performance.now() - this._lastSeenTime) / 1000;
// CSI can sense through walls for ~10 seconds with decaying confidence
const decayRate = 0.15; // Lose ~15% per second
this.personPresence = Math.max(0, 1.0 - elapsed * decayRate);
// Position slowly drifts (person walking behind wall)
this.personX = this._lastSeenX;
this.personY = this._lastSeenY;
this.personMotion = Math.max(0, motion * 0.5 + this.personPresence * 0.2);
if (this.personPresence < 0.05) {
this._lastSeenTime = null;
}
} else {
this.personPresence = 0;
this.personMotion = 0;
}
}
/**
* Generate next CSI frame (demo mode) or return latest live frame
* @param {number} elapsed - Time in seconds
* @returns {{ amplitude: Float32Array, phase: Float32Array, snr: number }}
*/
nextFrame(elapsed) {
const amp = new Float32Array(this.subcarriers);
const phase = new Float32Array(this.subcarriers);
if (this.mode === 'live' && this._liveAmplitude) {
amp.set(this._liveAmplitude);
phase.set(this._livePhase);
} else {
this._generateDemoFrame(amp, phase, elapsed);
}
// Push to circular buffer
this.amplitudeBuffer.push(new Float32Array(amp));
this.phaseBuffer.push(new Float32Array(phase));
if (this.amplitudeBuffer.length > this.timeWindow) {
this.amplitudeBuffer.shift();
this.phaseBuffer.shift();
}
// RSSI: smooth toward target (demo mode generates synthetic RSSI)
if (this.mode === 'demo') {
// Simulate RSSI based on person presence and slow drift
this._rssiTarget = -55 - 25 * (1 - this.personPresence) + Math.sin(elapsed * 0.3) * 3;
}
this.rssiDbm += (this._rssiTarget - this.rssiDbm) * 0.1;
// SNR estimate
let signalPower = 0, noisePower = 0;
for (let i = 0; i < this.subcarriers; i++) {
signalPower += amp[i] * amp[i];
noisePower += this._noiseState[i] * this._noiseState[i];
}
const snr = noisePower > 0 ? 10 * Math.log10(signalPower / noisePower) : 30;
this.frameCount++;
return { amplitude: amp, phase, snr: Math.max(0, Math.min(40, snr)) };
}
/**
* Build 3-channel pseudo-image for CNN input
* @param {number} targetSize - Output image dimension (square)
* @returns {Uint8Array} RGB data (targetSize * targetSize * 3)
*/
buildPseudoImage(targetSize = 56) {
const buf = this.amplitudeBuffer;
const pBuf = this.phaseBuffer;
const frames = buf.length;
if (frames < 2) {
return new Uint8Array(targetSize * targetSize * 3);
}
const rgb = new Uint8Array(targetSize * targetSize * 3);
for (let y = 0; y < targetSize; y++) {
const fi = Math.min(Math.floor(y / targetSize * frames), frames - 1);
for (let x = 0; x < targetSize; x++) {
const si = Math.min(Math.floor(x / targetSize * this.subcarriers), this.subcarriers - 1);
const idx = (y * targetSize + x) * 3;
// R: Amplitude (normalized to 0-255)
const ampVal = buf[fi][si];
rgb[idx] = Math.min(255, Math.max(0, Math.floor(ampVal * 255)));
// G: Phase (wrapped to 0-255)
const phaseVal = (pBuf[fi][si] % (2 * Math.PI) + 2 * Math.PI) % (2 * Math.PI);
rgb[idx + 1] = Math.floor(phaseVal / (2 * Math.PI) * 255);
// B: Temporal difference
if (fi > 0) {
const diff = Math.abs(buf[fi][si] - buf[fi - 1][si]);
rgb[idx + 2] = Math.min(255, Math.floor(diff * 500));
}
}
}
return rgb;
}
/**
* Get heatmap data for visualization
* @returns {{ data: Float32Array, width: number, height: number }}
*/
getHeatmapData() {
const frames = this.amplitudeBuffer.length;
const w = this.subcarriers;
const h = Math.min(frames, this.timeWindow);
const data = new Float32Array(w * h);
for (let y = 0; y < h; y++) {
const fi = frames - h + y;
if (fi >= 0 && fi < frames) {
for (let x = 0; x < w; x++) {
data[y * w + x] = this.amplitudeBuffer[fi][x];
}
}
}
return { data, width: w, height: h };
}
// === Private ===
_generateDemoFrame(amp, phase, elapsed) {
const rng = this._rng;
const presence = this.personPresence;
const motion = this.personMotion;
const px = this.personX;
for (let i = 0; i < this.subcarriers; i++) {
// Base CSI profile (frequency-selective channel)
let a = this._baseAmplitude[i];
let p = this._basePhase[i] + elapsed * 0.05;
// Environmental noise (correlated across subcarriers)
this._noiseState[i] = 0.95 * this._noiseState[i] + 0.05 * (rng() * 2 - 1) * 0.03;
a += this._noiseState[i];
// Ambient temporal drift (multipath fading even in empty room)
a += 0.06 * Math.sin(elapsed * 0.7 + i * 0.25)
+ 0.04 * Math.sin(elapsed * 1.3 - i * 0.18)
+ 0.03 * Math.cos(elapsed * 2.1 + i * 0.4);
// Person-induced CSI perturbation
if (presence > 0.1) {
// Subcarrier-dependent body reflection (Fresnel zone model)
const freqOffset = (i - this.subcarriers * px) / (this.subcarriers * 0.3);
const bodyReflection = presence * 0.25 * Math.exp(-freqOffset * freqOffset);
// Motion causes amplitude fluctuation
const motionEffect = motion * 0.15 * Math.sin(elapsed * 3.5 + i * 0.3);
// Breathing modulation (0.2-0.3 Hz)
const breathing = presence * 0.02 * Math.sin(elapsed * 1.5 + i * 0.05);
a += bodyReflection + motionEffect + breathing;
p += presence * 0.4 * Math.sin(elapsed * 2.1 + i * 0.15);
}
amp[i] = Math.max(0, Math.min(1, a));
phase[i] = p;
}
}
_handleLiveFrame(data) {
// Handle JSON text frames from the sensing server
if (typeof data === 'string') {
try {
const msg = JSON.parse(data);
this._handleJsonFrame(msg);
} catch (_) { /* ignore malformed JSON */ }
return;
}
// Handle Blob data (convert to ArrayBuffer and re-process)
if (data instanceof Blob) {
data.arrayBuffer().then(ab => this._handleLiveFrame(ab)).catch(() => {});
return;
}
// Handle binary ArrayBuffer frames (ADR-018 format)
if (!(data instanceof ArrayBuffer)) return;
const view = new DataView(data);
// Check ADR-018 magic: 0xC5110001
if (data.byteLength < 20) return;
const magic = view.getUint32(0, true);
if (magic !== 0xC5110001) return;
const numSub = Math.min(view.getUint16(8, true), this.subcarriers);
this._liveAmplitude = new Float32Array(this.subcarriers);
this._livePhase = new Float32Array(this.subcarriers);
const headerSize = 20;
for (let i = 0; i < numSub && (headerSize + i * 4 + 3) < data.byteLength; i++) {
const real = view.getInt16(headerSize + i * 4, true);
const imag = view.getInt16(headerSize + i * 4 + 2, true);
this._liveAmplitude[i] = Math.sqrt(real * real + imag * imag) / 2048;
this._livePhase[i] = Math.atan2(imag, real);
}
}
_handleJsonFrame(msg) {
// Sensing server sends: { type: "sensing_update", nodes: [{ amplitude: [...], subcarrier_count }], classification, features }
this._liveAmplitude = new Float32Array(this.subcarriers);
this._livePhase = new Float32Array(this.subcarriers);
// Extract amplitude from sensing_update node data
const node = (msg.nodes && msg.nodes[0]) || msg;
const ampArr = node.amplitude || msg.amplitude;
if (ampArr && Array.isArray(ampArr)) {
const n = Math.min(ampArr.length, this.subcarriers);
// Server sends raw amplitude (already magnitude), normalize to 0-1
let maxAmp = 0;
for (let i = 0; i < n; i++) maxAmp = Math.max(maxAmp, Math.abs(ampArr[i]));
const scale = maxAmp > 0 ? 1.0 / maxAmp : 1.0;
for (let i = 0; i < n; i++) {
this._liveAmplitude[i] = Math.abs(ampArr[i]) * scale;
}
}
// Phase from node (if available)
const phaseArr = node.phase || msg.phase;
if (phaseArr && Array.isArray(phaseArr)) {
const n = Math.min(phaseArr.length, this.subcarriers);
for (let i = 0; i < n; i++) this._livePhase[i] = phaseArr[i];
} else if (ampArr) {
// Synthesize phase from amplitude variation (Hilbert-like estimate)
for (let i = 1; i < this.subcarriers; i++) {
this._livePhase[i] = this._livePhase[i - 1] + (this._liveAmplitude[i] - this._liveAmplitude[i - 1]) * Math.PI;
}
}
// Handle raw I/Q pairs
const iq = node.iq || msg.iq;
if (iq && Array.isArray(iq)) {
const n = Math.min(iq.length / 2, this.subcarriers);
for (let i = 0; i < n; i++) {
const real = iq[i * 2], imag = iq[i * 2 + 1];
this._liveAmplitude[i] = Math.sqrt(real * real + imag * imag) / 2048;
this._livePhase[i] = Math.atan2(imag, real);
}
}
// Extract RSSI from node data
if (typeof node.rssi_dbm === 'number') {
this._rssiTarget = node.rssi_dbm;
} else if (msg.features && typeof msg.features.mean_rssi === 'number') {
this._rssiTarget = msg.features.mean_rssi;
}
// Update presence from server classification
const cls = msg.classification;
if (cls) {
if (typeof cls.confidence === 'number') {
this.personPresence = cls.presence ? cls.confidence : 0;
}
}
}
_mulberry32(seed) {
return function() {
let t = (seed += 0x6D2B79F5);
t = Math.imul(t ^ (t >>> 15), t | 1);
t ^= t + Math.imul(t ^ (t >>> 7), t | 61);
return ((t ^ (t >>> 14)) >>> 0) / 4294967296;
};
}
}
+183
View File
@@ -0,0 +1,183 @@
/**
* FusionEngine Attention-weighted dual-modal embedding fusion.
*
* Combines visual (camera) and CSI (WiFi) embeddings with dynamic
* confidence gating based on signal quality.
*/
export class FusionEngine {
/**
* @param {number} embeddingDim
* @param {object} opts
* @param {object} opts.wasmModule - RuVector WASM module for cosine_similarity etc.
*/
constructor(embeddingDim = 128, opts = {}) {
this.embeddingDim = embeddingDim;
this.wasmModule = opts.wasmModule || null;
// Learnable attention weights (initialized to balanced 0.5)
this.attentionWeights = new Float32Array(embeddingDim).fill(0.5);
// Dynamic modality confidence [0, 1]
this.videoConfidence = 1.0;
this.csiConfidence = 0.0;
this.fusedConfidence = 0.5;
// Smoothing for confidence transitions
this._smoothAlpha = 0.85;
// Embedding history for visualization
this.recentVideoEmbeddings = [];
this.recentCsiEmbeddings = [];
this.recentFusedEmbeddings = [];
this.maxHistory = 50;
}
/** Set the WASM module reference (called after WASM loads) */
setWasmModule(mod) { this.wasmModule = mod; }
/**
* Update quality-based confidence scores
* @param {number} videoBrightness - [0,1] video brightness quality
* @param {number} videoMotion - [0,1] motion detected
* @param {number} csiSnr - CSI signal-to-noise ratio in dB
* @param {boolean} csiActive - Whether CSI source is connected
*/
updateConfidence(videoBrightness, videoMotion, csiSnr, csiActive) {
// Video confidence: drops with low brightness, boosted by motion
let vc = 0;
if (videoBrightness > 0.05) {
vc = Math.min(1, videoBrightness * 1.5) * 0.7 + Math.min(1, videoMotion * 3) * 0.3;
}
// CSI confidence: based on SNR and connection status
let cc = 0;
if (csiActive) {
cc = Math.min(1, csiSnr / 25); // 25dB = full confidence
}
// Smooth transitions
this.videoConfidence = this._smoothAlpha * this.videoConfidence + (1 - this._smoothAlpha) * vc;
this.csiConfidence = this._smoothAlpha * this.csiConfidence + (1 - this._smoothAlpha) * cc;
// Fused confidence is the max of either (fusion can only help)
this.fusedConfidence = Math.min(1, Math.sqrt(
this.videoConfidence * this.videoConfidence + this.csiConfidence * this.csiConfidence
));
}
/**
* Fuse video and CSI embeddings
* @param {Float32Array|null} videoEmb - Visual embedding (or null if video-off)
* @param {Float32Array|null} csiEmb - CSI embedding (or null if CSI-off)
* @param {string} mode - 'dual' | 'video' | 'csi'
* @returns {Float32Array} Fused embedding
*/
fuse(videoEmb, csiEmb, mode = 'dual') {
const dim = this.embeddingDim;
const fused = new Float32Array(dim);
if (mode === 'video' || !csiEmb) {
if (videoEmb) fused.set(videoEmb);
this._recordEmbedding(videoEmb, null, fused);
return fused;
}
if (mode === 'csi' || !videoEmb) {
if (csiEmb) fused.set(csiEmb);
this._recordEmbedding(null, csiEmb, fused);
return fused;
}
// Dual mode: attention-weighted fusion with confidence gating
const totalConf = this.videoConfidence + this.csiConfidence;
const videoWeight = totalConf > 0 ? this.videoConfidence / totalConf : 0.5;
for (let i = 0; i < dim; i++) {
const alpha = this.attentionWeights[i] * videoWeight +
(1 - this.attentionWeights[i]) * (1 - videoWeight);
fused[i] = alpha * videoEmb[i] + (1 - alpha) * csiEmb[i];
}
// Re-normalize using WASM when available
if (this.wasmModule) {
try { this.wasmModule.normalize(fused); } catch (_) { this._jsNormalize(fused); }
} else {
this._jsNormalize(fused);
}
this._recordEmbedding(videoEmb, csiEmb, fused);
return fused;
}
/**
* Get embedding pairs for 2D visualization (PCA projection)
* @returns {{ video: Array, csi: Array, fused: Array }}
*/
getEmbeddingPoints() {
// Sparse random projection: pick a few dimensions with fixed coefficients
// to get visible 2D spread (avoids cancellation from summing all 128 dims)
const project = (emb) => {
if (!emb || emb.length < 4) return null;
// Use 8 sparse dimensions with predetermined signs (seeded, not random)
const dim = emb.length;
const x = emb[0] * 3.2 - emb[3] * 2.8 + emb[7] * 2.1 - emb[12] * 1.9
+ (dim > 30 ? emb[29] * 1.5 - emb[31] * 1.3 : 0)
+ (dim > 60 ? emb[55] * 1.1 - emb[60] * 0.9 : 0);
const y = emb[1] * 3.0 - emb[5] * 2.5 + emb[9] * 2.3 - emb[15] * 1.7
+ (dim > 40 ? emb[37] * 1.4 - emb[42] * 1.2 : 0)
+ (dim > 80 ? emb[73] * 1.0 - emb[80] * 0.8 : 0);
return [x, y];
};
return {
video: this.recentVideoEmbeddings.map(project).filter(Boolean),
csi: this.recentCsiEmbeddings.map(project).filter(Boolean),
fused: this.recentFusedEmbeddings.map(project).filter(Boolean)
};
}
/**
* Cross-modal similarity score
* @returns {number} Cosine similarity between latest video and CSI embeddings
*/
getCrossModalSimilarity() {
const v = this.recentVideoEmbeddings[this.recentVideoEmbeddings.length - 1];
const c = this.recentCsiEmbeddings[this.recentCsiEmbeddings.length - 1];
if (!v || !c) return 0;
// Use WASM cosine_similarity when available
if (this.wasmModule) {
try { return this.wasmModule.cosine_similarity(v, c); } catch (_) { /* fallback */ }
}
let dot = 0, na = 0, nb = 0;
for (let i = 0; i < v.length; i++) {
dot += v[i] * c[i];
na += v[i] * v[i];
nb += c[i] * c[i];
}
na = Math.sqrt(na); nb = Math.sqrt(nb);
return (na > 1e-8 && nb > 1e-8) ? dot / (na * nb) : 0;
}
_jsNormalize(vec) {
let norm = 0;
for (let i = 0; i < vec.length; i++) norm += vec[i] * vec[i];
norm = Math.sqrt(norm);
if (norm > 1e-8) for (let i = 0; i < vec.length; i++) vec[i] /= norm;
}
_recordEmbedding(video, csi, fused) {
if (video) {
this.recentVideoEmbeddings.push(new Float32Array(video));
if (this.recentVideoEmbeddings.length > this.maxHistory) this.recentVideoEmbeddings.shift();
}
if (csi) {
this.recentCsiEmbeddings.push(new Float32Array(csi));
if (this.recentCsiEmbeddings.length > this.maxHistory) this.recentCsiEmbeddings.shift();
}
this.recentFusedEmbeddings.push(new Float32Array(fused));
if (this.recentFusedEmbeddings.length > this.maxHistory) this.recentFusedEmbeddings.shift();
}
}
+472
View File
@@ -0,0 +1,472 @@
/**
* RuView Dual-Modal Pose Estimation Demo
*
* Main orchestration: video capture CNN embedding CSI processing fusion rendering
*/
import { VideoCapture } from './video-capture.js?v=13';
import { CsiSimulator } from './csi-simulator.js?v=13';
import { CnnEmbedder } from './cnn-embedder.js?v=13';
import { FusionEngine } from './fusion-engine.js?v=13';
import { PoseDecoder } from './pose-decoder.js?v=13';
import { CanvasRenderer } from './canvas-renderer.js?v=13';
// === State ===
let mode = 'dual'; // 'dual' | 'video' | 'csi'
let isRunning = false;
let isPaused = false;
let startTime = 0;
let frameCount = 0;
let fps = 0;
let lastFpsTime = 0;
let confidenceThreshold = 0.3;
// Latency tracking
const latency = { video: 0, csi: 0, fusion: 0, total: 0 };
// === Components ===
const videoCapture = new VideoCapture(document.getElementById('webcam'));
const csiSimulator = new CsiSimulator({ subcarriers: 52, timeWindow: 56 });
const visualCnn = new CnnEmbedder({ inputSize: 56, embeddingDim: 128, seed: 42 });
const csiCnn = new CnnEmbedder({ inputSize: 56, embeddingDim: 128, seed: 137 });
const fusionEngine = new FusionEngine(128);
const poseDecoder = new PoseDecoder(128);
const renderer = new CanvasRenderer();
// === Canvas Elements ===
const skeletonCanvas = document.getElementById('skeleton-canvas');
const skeletonCtx = skeletonCanvas.getContext('2d');
const csiCanvas = document.getElementById('csi-canvas');
const csiCtx = csiCanvas.getContext('2d');
const embeddingCanvas = document.getElementById('embedding-canvas');
const embeddingCtx = embeddingCanvas.getContext('2d');
// === UI Elements ===
const modeSelect = document.getElementById('mode-select');
const statusDot = document.getElementById('status-dot');
const statusLabel = document.getElementById('status-label');
const fpsDisplay = document.getElementById('fps-display');
const cameraPrompt = document.getElementById('camera-prompt');
const startCameraBtn = document.getElementById('start-camera-btn');
const pauseBtn = document.getElementById('pause-btn');
const confSlider = document.getElementById('confidence-slider');
const confValue = document.getElementById('confidence-value');
const wsUrlInput = document.getElementById('ws-url');
const connectWsBtn = document.getElementById('connect-ws-btn');
// Fusion bar elements
const videoBar = document.getElementById('video-bar');
const csiBar = document.getElementById('csi-bar');
const fusedBar = document.getElementById('fused-bar');
const videoBarVal = document.getElementById('video-bar-val');
const csiBarVal = document.getElementById('csi-bar-val');
const fusedBarVal = document.getElementById('fused-bar-val');
// Latency elements
const latVideoEl = document.getElementById('lat-video');
const latCsiEl = document.getElementById('lat-csi');
const latFusionEl = document.getElementById('lat-fusion');
const latTotalEl = document.getElementById('lat-total');
// Cross-modal similarity
const crossModalEl = document.getElementById('cross-modal-sim');
// RSSI elements
const rssiBarEl = document.getElementById('rssi-bar');
const rssiValueEl = document.getElementById('rssi-value');
const rssiQualityEl = document.getElementById('rssi-quality');
const rssiSparkCanvas = document.getElementById('rssi-sparkline');
const rssiSparkCtx = rssiSparkCanvas ? rssiSparkCanvas.getContext('2d') : null;
const rssiHistory = [];
const RSSI_HISTORY_MAX = 80;
// === Initialize ===
function init() {
console.log(`[PoseFusion] init() v4 — CsiSimulator=${CsiSimulator.VERSION || 'OLD'}, starting...`);
resizeCanvases();
console.log(`[PoseFusion] canvases: skeleton=${skeletonCanvas.width}x${skeletonCanvas.height}, csi=${csiCanvas.width}x${csiCanvas.height}, emb=${embeddingCanvas.width}x${embeddingCanvas.height}`);
window.addEventListener('resize', resizeCanvases);
// Mode change
modeSelect.addEventListener('change', (e) => {
mode = e.target.value;
updateModeUI();
});
// Camera start
startCameraBtn.addEventListener('click', startCamera);
// Pause
pauseBtn.addEventListener('click', () => {
isPaused = !isPaused;
pauseBtn.textContent = isPaused ? '▶ Resume' : '⏸ Pause';
pauseBtn.classList.toggle('active', isPaused);
});
// Confidence slider
confSlider.addEventListener('input', (e) => {
confidenceThreshold = parseFloat(e.target.value);
confValue.textContent = confidenceThreshold.toFixed(2);
});
// WebSocket connect
connectWsBtn.addEventListener('click', async () => {
const url = wsUrlInput.value.trim();
if (!url) return;
connectWsBtn.textContent = 'Connecting...';
const ok = await csiSimulator.connectLive(url);
connectWsBtn.textContent = ok ? '✓ Connected' : 'Connect';
if (ok) {
connectWsBtn.classList.add('active');
}
});
// Try to load RuVector Attention WASM embedders (non-blocking)
const wasmBase = new URL('../pkg/ruvector-attention', import.meta.url).href;
visualCnn.tryLoadWasm(wasmBase).then((ok) => {
// Share the WASM module with FusionEngine for cosine_similarity, normalize, etc.
if (visualCnn.rvModule) fusionEngine.setWasmModule(visualCnn.rvModule);
// Update footer backend label
const backendEl = document.getElementById('cnn-backend');
if (backendEl) {
backendEl.textContent = ok && visualCnn.useRuVector
? `RuVector WASM v${visualCnn.rvModule.version()} — 6 attention mechanisms`
: 'ruvector-cnn (JS fallback)';
}
});
csiCnn.tryLoadWasm(wasmBase);
// Auto-connect to local sensing server WebSocket if available
const defaultWsUrl = 'ws://localhost:8765/ws/sensing';
if (wsUrlInput) wsUrlInput.value = defaultWsUrl;
csiSimulator.connectLive(defaultWsUrl).then(ok => {
if (ok && connectWsBtn) {
connectWsBtn.textContent = '✓ Live ESP32';
connectWsBtn.classList.add('active');
statusLabel.textContent = 'LIVE CSI';
statusDot.classList.remove('offline');
}
});
// Auto-start camera for video/dual modes
updateModeUI();
startTime = performance.now() / 1000;
isRunning = true;
requestAnimationFrame(mainLoop);
}
async function startCamera() {
cameraPrompt.style.display = 'none';
const ok = await videoCapture.start();
if (ok) {
statusDot.classList.remove('offline');
statusLabel.textContent = 'LIVE';
resizeCanvases();
} else {
cameraPrompt.style.display = 'flex';
cameraPrompt.querySelector('p').textContent = 'Camera access denied. Try CSI-only mode.';
}
}
function updateModeUI() {
const needsVideo = mode !== 'csi';
// Show/hide camera prompt
if (needsVideo && !videoCapture.isActive) {
cameraPrompt.style.display = 'flex';
} else {
cameraPrompt.style.display = 'none';
}
// Update mode label in both the overlay and the camera prompt
const labelMap = { dual: 'DUAL FUSION', video: 'VIDEO ONLY', csi: 'CSI ONLY' };
const modeLabel = document.getElementById('mode-label');
const promptLabel = document.getElementById('prompt-mode-label');
if (modeLabel) modeLabel.textContent = labelMap[mode] || mode;
if (promptLabel) promptLabel.textContent = labelMap[mode] || mode;
}
function resizeCanvases() {
const videoPanel = document.querySelector('.video-panel');
if (videoPanel) {
const rect = videoPanel.getBoundingClientRect();
skeletonCanvas.width = rect.width;
skeletonCanvas.height = rect.height;
}
// CSI canvas (min 200px width)
csiCanvas.width = Math.max(200, csiCanvas.parentElement.clientWidth);
csiCanvas.height = 120;
// Embedding canvas (min 200px width)
embeddingCanvas.width = Math.max(200, embeddingCanvas.parentElement.clientWidth);
embeddingCanvas.height = 140;
}
// === Main Loop ===
let _loopErrorShown = false;
let _diagDone = false;
function mainLoop(timestamp) {
if (!isRunning) return;
requestAnimationFrame(mainLoop);
if (isPaused) return;
try {
const elapsed = performance.now() / 1000 - startTime;
const totalStart = performance.now();
// --- Video Pipeline ---
let videoEmb = null;
let motionRegion = null;
if (mode !== 'csi' && videoCapture.isActive) {
const t0 = performance.now();
const frame = videoCapture.captureFrame(56, 56);
if (frame) {
videoEmb = visualCnn.extract(frame.rgb, frame.width, frame.height);
motionRegion = videoCapture.detectMotionRegion(56, 56);
// Feed motion to CSI simulator for correlated demo data
// When detected=false, CSI simulator handles through-wall persistence
csiSimulator.updatePersonState(
motionRegion.detected ? 1.0 : 0,
motionRegion.detected ? motionRegion.x + motionRegion.w / 2 : 0.5,
motionRegion.detected ? motionRegion.y + motionRegion.h / 2 : 0.5,
frame.motion
);
fusionEngine.updateConfidence(
frame.brightness, frame.motion,
0, csiSimulator.isLive || mode === 'dual'
);
}
latency.video = performance.now() - t0;
}
// --- CSI Pipeline ---
let csiEmb = null;
if (mode !== 'video') {
const t0 = performance.now();
const csiFrame = csiSimulator.nextFrame(elapsed);
const pseudoImage = csiSimulator.buildPseudoImage(56);
csiEmb = csiCnn.extract(pseudoImage, 56, 56);
fusionEngine.updateConfidence(
videoCapture.brightnessScore,
videoCapture.motionScore,
csiFrame.snr,
true
);
// Draw CSI heatmap
const heatmap = csiSimulator.getHeatmapData();
renderer.drawCsiHeatmap(csiCtx, heatmap, csiCanvas.width, csiCanvas.height);
latency.csi = performance.now() - t0;
}
// --- Fusion ---
const t0f = performance.now();
const fusedEmb = fusionEngine.fuse(videoEmb, csiEmb, mode);
latency.fusion = performance.now() - t0f;
// --- Pose Decode ---
// For CSI-only mode, generate a synthetic motion region from CSI energy
if (mode === 'csi' && (!motionRegion || !motionRegion.detected)) {
const csiPresence = csiSimulator.personPresence;
if (csiPresence > 0.1) {
motionRegion = {
detected: true,
x: 0.25, y: 0.15, w: 0.5, h: 0.7,
coverage: csiPresence,
motionGrid: null,
gridCols: 10,
gridRows: 8
};
}
}
// CSI state for through-wall tracking
const csiState = {
csiPresence: csiSimulator.personPresence,
isLive: csiSimulator.isLive
};
const keypoints = poseDecoder.decode(fusedEmb, motionRegion, elapsed, csiState);
// --- Render Skeleton ---
const labelMap = { dual: 'DUAL FUSION', video: 'VIDEO ONLY', csi: 'CSI ONLY' };
renderer.drawSkeleton(skeletonCtx, keypoints, skeletonCanvas.width, skeletonCanvas.height, {
minConfidence: confidenceThreshold,
color: mode === 'csi' ? 'amber' : 'green',
label: labelMap[mode]
});
// --- Render Embedding Space ---
const embPoints = fusionEngine.getEmbeddingPoints();
renderer.drawEmbeddingSpace(embeddingCtx, embPoints, embeddingCanvas.width, embeddingCanvas.height);
// --- Update UI ---
latency.total = performance.now() - totalStart;
// FPS
frameCount++;
if (timestamp - lastFpsTime > 500) {
fps = Math.round(frameCount * 1000 / (timestamp - lastFpsTime));
lastFpsTime = timestamp;
frameCount = 0;
fpsDisplay.textContent = `${fps} FPS`;
}
// Fusion bars
const vc = fusionEngine.videoConfidence;
const cc = fusionEngine.csiConfidence;
const fc = fusionEngine.fusedConfidence;
videoBar.style.width = `${vc * 100}%`;
csiBar.style.width = `${cc * 100}%`;
fusedBar.style.width = `${fc * 100}%`;
videoBarVal.textContent = `${Math.round(vc * 100)}%`;
csiBarVal.textContent = `${Math.round(cc * 100)}%`;
fusedBarVal.textContent = `${Math.round(fc * 100)}%`;
// Latency
latVideoEl.textContent = `${latency.video.toFixed(1)}ms`;
latCsiEl.textContent = `${latency.csi.toFixed(1)}ms`;
latFusionEl.textContent = `${latency.fusion.toFixed(1)}ms`;
latTotalEl.textContent = `${latency.total.toFixed(1)}ms`;
// Cross-modal similarity
const sim = fusionEngine.getCrossModalSimilarity();
crossModalEl.textContent = sim.toFixed(3);
// RuVector attention pipeline stats
const rvStats = poseDecoder.attentionStats;
const rvEnergyEl = document.getElementById('rv-energy');
const rvRefineEl = document.getElementById('rv-refine');
const rvImpactEl = document.getElementById('rv-impact');
if (rvEnergyEl) rvEnergyEl.textContent = (rvStats.energy || 0).toFixed(2);
if (rvRefineEl) rvRefineEl.textContent = ((rvStats.refinementMag || 0) * 1000).toFixed(1) + 'px';
if (rvImpactEl) {
const impact = Math.min(100, (rvStats.refinementMag || 0) * 5000);
rvImpactEl.textContent = impact.toFixed(0) + '%';
}
// Pulse the pipeline stages when active
if (visualCnn.useRuVector && rvStats.energy > 0.1) {
document.querySelectorAll('.rv-stage').forEach(el => el.classList.add('active'));
}
// RSSI update
updateRssi(csiSimulator.rssiDbm);
// One-time diagnostic
if (!_diagDone) {
_diagDone = true;
console.log(`[PoseFusion] frame 1 OK — mode=${mode}, csi.bufLen=${csiSimulator.amplitudeBuffer.length}, embPts=${embPoints?.fused?.length ?? 0}, rssi=${(csiSimulator.rssiDbm ?? -99).toFixed(1)}`);
}
} catch (err) {
if (!_loopErrorShown) {
_loopErrorShown = true;
console.error('[MainLoop]', err);
// Show error visually on page
const errDiv = document.createElement('div');
errDiv.style.cssText = 'position:fixed;bottom:60px;left:24px;right:24px;background:rgba(255,48,64,0.95);color:#fff;padding:12px 16px;border-radius:8px;font:12px/1.4 "JetBrains Mono",monospace;z-index:9999;max-height:120px;overflow:auto';
errDiv.textContent = `[MainLoop Error] ${err.message}\n${err.stack?.split('\n').slice(0,3).join('\n')}`;
document.body.appendChild(errDiv);
}
}
}
// === RSSI Visualization ===
function updateRssi(dbm) {
if (!rssiBarEl) return;
// Clamp to typical WiFi range: -100 (worst) to -30 (best)
const clamped = Math.max(-100, Math.min(-30, dbm));
const pct = ((clamped + 100) / 70) * 100; // 0-100%
rssiBarEl.style.width = `${pct}%`;
rssiValueEl.textContent = `${Math.round(clamped)} dBm`;
// Quality label
let quality;
if (clamped > -50) quality = 'Excellent';
else if (clamped > -60) quality = 'Good';
else if (clamped > -70) quality = 'Fair';
else if (clamped > -80) quality = 'Weak';
else quality = 'Poor';
rssiQualityEl.textContent = quality;
// Color the dBm value based on quality
if (clamped > -60) rssiValueEl.style.color = 'var(--green-glow)';
else if (clamped > -75) rssiValueEl.style.color = 'var(--amber)';
else rssiValueEl.style.color = 'var(--red-alert)';
// Sparkline history
rssiHistory.push(clamped);
if (rssiHistory.length > RSSI_HISTORY_MAX) rssiHistory.shift();
drawRssiSparkline();
}
function drawRssiSparkline() {
if (!rssiSparkCtx || rssiHistory.length < 2) return;
const w = rssiSparkCanvas.width;
const h = rssiSparkCanvas.height;
const ctx = rssiSparkCtx;
ctx.clearRect(0, 0, w, h);
// Draw signal strength line
const len = rssiHistory.length;
const step = w / (RSSI_HISTORY_MAX - 1);
// Gradient fill under line
const grad = ctx.createLinearGradient(0, 0, 0, h);
grad.addColorStop(0, 'rgba(0,210,120,0.3)');
grad.addColorStop(1, 'rgba(0,210,120,0)');
ctx.beginPath();
for (let i = 0; i < len; i++) {
const x = (RSSI_HISTORY_MAX - len + i) * step;
const y = h - ((rssiHistory[i] + 100) / 70) * h;
if (i === 0) ctx.moveTo(x, y);
else ctx.lineTo(x, y);
}
// Fill area
const lastX = (RSSI_HISTORY_MAX - 1) * step;
const firstX = (RSSI_HISTORY_MAX - len) * step;
ctx.lineTo(lastX, h);
ctx.lineTo(firstX, h);
ctx.closePath();
ctx.fillStyle = grad;
ctx.fill();
// Draw line on top
ctx.beginPath();
for (let i = 0; i < len; i++) {
const x = (RSSI_HISTORY_MAX - len + i) * step;
const y = h - ((rssiHistory[i] + 100) / 70) * h;
if (i === 0) ctx.moveTo(x, y);
else ctx.lineTo(x, y);
}
ctx.strokeStyle = '#00d878';
ctx.lineWidth = 1.5;
ctx.stroke();
// Pulsing dot at latest value
const latestX = lastX;
const latestY = h - ((rssiHistory[len - 1] + 100) / 70) * h;
const pulse = 0.5 + 0.5 * Math.sin(performance.now() / 300);
ctx.beginPath();
ctx.arc(latestX, latestY, 2 + pulse, 0, Math.PI * 2);
ctx.fillStyle = '#00d878';
ctx.fill();
ctx.beginPath();
ctx.arc(latestX, latestY, 4 + pulse * 2, 0, Math.PI * 2);
ctx.strokeStyle = `rgba(0,216,120,${0.3 + pulse * 0.3})`;
ctx.lineWidth = 1;
ctx.stroke();
}
// Boot
document.addEventListener('DOMContentLoaded', init);
+553
View File
@@ -0,0 +1,553 @@
/**
* PoseDecoder Maps motion detection grid 17 COCO keypoints.
*
* Uses per-cell motion intensity to track actual body part positions:
* - Head: top-center motion cluster
* - Shoulders/Elbows/Wrists: lateral motion in upper body zone
* - Hips/Knees/Ankles: lower body motion distribution
*
* When person exits frame, CSI data continues tracking (through-wall mode).
*/
// Extended keypoint definitions: 17 COCO + 9 hand/fingertip approximations = 26 total
export const KEYPOINT_NAMES = [
'nose', 'left_eye', 'right_eye', 'left_ear', 'right_ear',
'left_shoulder', 'right_shoulder', 'left_elbow', 'right_elbow',
'left_wrist', 'right_wrist', 'left_hip', 'right_hip',
'left_knee', 'right_knee', 'left_ankle', 'right_ankle',
// Extended: hand keypoints (17-25)
'left_thumb', 'left_index', 'left_pinky', // 17, 18, 19
'right_thumb', 'right_index', 'right_pinky', // 20, 21, 22
'left_foot_index', 'right_foot_index', // 23, 24 (toe tips)
'neck', // 25 (mid-shoulder)
];
// Skeleton connections (pairs of keypoint indices)
export const SKELETON_CONNECTIONS = [
[0, 1], [0, 2], [1, 3], [2, 4], // Head
[0, 25], // Nose → neck
[25, 5], [25, 6], // Neck → shoulders
[5, 7], [7, 9], // Left arm
[6, 8], [8, 10], // Right arm
[5, 11], [6, 12], // Torso
[11, 12], // Hips
[11, 13], [13, 15], // Left leg
[12, 14], [14, 16], // Right leg
// Hand connections
[9, 17], [9, 18], [9, 19], // Left wrist → fingers
[10, 20], [10, 21], [10, 22], // Right wrist → fingers
// Foot connections
[15, 23], [16, 24], // Ankles → toes
];
// Standard body proportions (relative to body height)
const PROPORTIONS = {
headToShoulder: 0.15,
shoulderWidth: 0.25,
shoulderToElbow: 0.18,
elbowToWrist: 0.16,
shoulderToHip: 0.30,
hipWidth: 0.18,
hipToKnee: 0.24,
kneeToAnkle: 0.24,
eyeSpacing: 0.04,
earSpacing: 0.07,
// Hand proportions
wristToFinger: 0.09,
fingerSpread: 0.04,
thumbAngle: 0.6, // radians from wrist-elbow axis
// Foot proportions
ankleToToe: 0.06,
};
export class PoseDecoder {
constructor(embeddingDim = 128) {
this.embeddingDim = embeddingDim;
this.smoothedKeypoints = null;
this.smoothingFactor = 0.25; // Low = responsive to real movement
this._time = 0;
// Through-wall tracking state
this._lastBodyState = null;
this._ghostState = null;
this._ghostConfidence = 0;
this._ghostVelocity = { x: 0, y: 0 };
// Zone centroid tracking (normalized 0-1 positions)
this._headCx = 0.5;
this._headCy = 0.15;
this._leftArmCx = 0.3;
this._leftArmCy = 0.35;
this._rightArmCx = 0.7;
this._rightArmCy = 0.35;
this._leftLegCx = 0.4;
this._leftLegCy = 0.8;
this._rightLegCx = 0.6;
this._rightLegCy = 0.8;
this._torsoCx = 0.5;
this._torsoCy = 0.45;
// RuVector embedding → joint mapping
// Each joint gets 2 consecutive embedding dimensions (dx, dy offset)
// and 1 dimension for confidence modulation. 26 joints × 3 = 78 dims used from 128.
// Remaining 50 dims encode global pose features (body scale, rotation, lean).
this._jointEmbMap = this._buildJointEmbeddingMap(embeddingDim);
// Attention contribution tracking (for UI overlay)
this.attentionStats = { energy: 0, maxDim: 0, refinementMag: 0 };
}
/**
* Build the mapping from embedding dimensions to joint refinement signals.
* This maps the RuVector attention output to anatomically meaningful joint offsets.
*/
_buildJointEmbeddingMap(dim) {
const map = [];
// 26 joints × 3 dims each (dx, dy, confidence_mod) = 78 dims
for (let j = 0; j < 26; j++) {
const base = j * 3;
if (base + 2 < dim) {
map.push({ dxDim: base, dyDim: base + 1, confDim: base + 2 });
} else {
map.push({ dxDim: j % dim, dyDim: (j + 1) % dim, confDim: (j + 2) % dim });
}
}
// Global pose features from dims 78-127
return {
joints: map,
scaleDim: Math.min(78, dim - 1), // body scale factor
rotDim: Math.min(79, dim - 1), // body rotation
leanXDim: Math.min(80, dim - 1), // lateral lean
leanYDim: Math.min(81, dim - 1), // forward/back lean
};
}
/**
* Decode motion data into 17 keypoints
* @param {Float32Array} embedding - Fused embedding vector
* @param {{ detected, x, y, w, h, motionGrid, gridCols, gridRows, motionCx, motionCy, exitDirection }} motionRegion
* @param {number} elapsed - Time in seconds
* @param {{ csiPresence: number }} csiState - CSI sensing state for through-wall
* @returns {Array<{x: number, y: number, confidence: number, name: string}>}
*/
decode(embedding, motionRegion, elapsed, csiState = {}) {
this._time = elapsed;
const hasMotion = motionRegion && motionRegion.detected;
const hasCsi = csiState && csiState.csiPresence > 0.1;
if (hasMotion) {
// Active tracking from video motion grid
this._ghostConfidence = 0;
const rawKeypoints = this._trackFromMotionGrid(motionRegion, embedding, elapsed);
this._lastBodyState = { keypoints: rawKeypoints.map(kp => ({...kp})), time: elapsed };
// Track exit velocity
if (motionRegion.exitDirection) {
const speed = 0.008;
this._ghostVelocity = {
x: motionRegion.exitDirection === 'left' ? -speed : motionRegion.exitDirection === 'right' ? speed : 0,
y: motionRegion.exitDirection === 'up' ? -speed : motionRegion.exitDirection === 'down' ? speed : 0
};
}
// Apply temporal smoothing
if (this.smoothedKeypoints && this.smoothedKeypoints.length === rawKeypoints.length) {
const alpha = this.smoothingFactor;
for (let i = 0; i < rawKeypoints.length; i++) {
rawKeypoints[i].x = alpha * this.smoothedKeypoints[i].x + (1 - alpha) * rawKeypoints[i].x;
rawKeypoints[i].y = alpha * this.smoothedKeypoints[i].y + (1 - alpha) * rawKeypoints[i].y;
}
}
this.smoothedKeypoints = rawKeypoints;
return rawKeypoints;
} else if (this._lastBodyState && (hasCsi || this._ghostConfidence > 0.05)) {
// Through-wall mode: person left frame but CSI still senses them
return this._trackThroughWall(elapsed, csiState);
} else if (this.smoothedKeypoints) {
// Fade out
const faded = this.smoothedKeypoints.map(kp => ({
...kp,
confidence: kp.confidence * 0.88
})).filter(kp => kp.confidence > 0.05);
if (faded.length === 0) this.smoothedKeypoints = null;
else this.smoothedKeypoints = faded;
return faded;
}
return [];
}
/**
* Track body parts from the motion grid.
* Finds the centroid of motion in each body zone and positions joints there.
*/
_trackFromMotionGrid(region, embedding, elapsed) {
const grid = region.motionGrid;
const cols = region.gridCols || 10;
const rows = region.gridRows || 8;
// Body bounding box (in normalized 0-1 coords)
const bx = region.x, by = region.y, bw = region.w, bh = region.h;
const cx = bx + bw / 2;
const cy = by + bh / 2;
const bodyH = Math.max(bh, 0.3);
const bodyW = Math.max(bw, 0.15);
// Find motion centroids per body zone from the grid
if (grid) {
const zones = this._findZoneCentroids(grid, cols, rows, bx, by, bw, bh);
// Smooth with low alpha for responsiveness
const a = 0.3; // 30% old, 70% new → responsive
this._headCx = a * this._headCx + (1 - a) * zones.head.x;
this._headCy = a * this._headCy + (1 - a) * zones.head.y;
this._leftArmCx = a * this._leftArmCx + (1 - a) * zones.leftArm.x;
this._leftArmCy = a * this._leftArmCy + (1 - a) * zones.leftArm.y;
this._rightArmCx= a * this._rightArmCx+ (1 - a) * zones.rightArm.x;
this._rightArmCy= a * this._rightArmCy+ (1 - a) * zones.rightArm.y;
this._leftLegCx = a * this._leftLegCx + (1 - a) * zones.leftLeg.x;
this._leftLegCy = a * this._leftLegCy + (1 - a) * zones.leftLeg.y;
this._rightLegCx= a * this._rightLegCx+ (1 - a) * zones.rightLeg.x;
this._rightLegCy= a * this._rightLegCy+ (1 - a) * zones.rightLeg.y;
this._torsoCx = a * this._torsoCx + (1 - a) * zones.torso.x;
this._torsoCy = a * this._torsoCy + (1 - a) * zones.torso.y;
}
const P = PROPORTIONS;
// Breathing (subtle)
const breathe = Math.sin(elapsed * 1.5) * 0.002;
// === Position joints using tracked centroids ===
// HEAD: tracked centroid (top zone)
const headX = this._headCx;
const headY = this._headCy;
// TORSO center drives shoulder/hip
const torsoX = this._torsoCx;
const shoulderY = this._torsoCy - bodyH * 0.08 + breathe;
const halfW = P.shoulderWidth * bodyH / 2;
const hipHalfW = P.hipWidth * bodyH / 2;
const hipY = shoulderY + P.shoulderToHip * bodyH;
// ARMS: elbow + wrist driven toward arm zone centroids
// Left arm: shoulder is fixed, elbow/wrist pulled toward left arm centroid
const lShX = torsoX - halfW;
const lShY = shoulderY;
// Vector from shoulder toward arm centroid
const lArmDx = this._leftArmCx - lShX;
const lArmDy = this._leftArmCy - lShY;
const lArmDist = Math.sqrt(lArmDx * lArmDx + lArmDy * lArmDy) || 0.01;
const lArmNx = lArmDx / lArmDist;
const lArmNy = lArmDy / lArmDist;
// Elbow at shoulderToElbow distance along that direction
const elbowLen = P.shoulderToElbow * bodyH;
const lElbowX = lShX + lArmNx * elbowLen;
const lElbowY = lShY + lArmNy * elbowLen;
// Wrist continues further
const wristLen = P.elbowToWrist * bodyH;
const lWristX = lElbowX + lArmNx * wristLen;
const lWristY = lElbowY + lArmNy * wristLen;
// Right arm: same approach
const rShX = torsoX + halfW;
const rShY = shoulderY;
const rArmDx = this._rightArmCx - rShX;
const rArmDy = this._rightArmCy - rShY;
const rArmDist = Math.sqrt(rArmDx * rArmDx + rArmDy * rArmDy) || 0.01;
const rArmNx = rArmDx / rArmDist;
const rArmNy = rArmDy / rArmDist;
const rElbowX = rShX + rArmNx * elbowLen;
const rElbowY = rShY + rArmNy * elbowLen;
const rWristX = rElbowX + rArmNx * wristLen;
const rWristY = rElbowY + rArmNy * wristLen;
// LEGS: knees/ankles pulled toward leg zone centroids
const lHipX = torsoX - hipHalfW;
const rHipX = torsoX + hipHalfW;
const lLegDx = this._leftLegCx - lHipX;
const lLegDy = Math.max(0.05, this._leftLegCy - hipY); // always downward
const lLegDist = Math.sqrt(lLegDx * lLegDx + lLegDy * lLegDy) || 0.01;
const lLegNx = lLegDx / lLegDist;
const lLegNy = lLegDy / lLegDist;
const kneeLen = P.hipToKnee * bodyH;
const ankleLen = P.kneeToAnkle * bodyH;
const lKneeX = lHipX + lLegNx * kneeLen;
const lKneeY = hipY + lLegNy * kneeLen;
const lAnkleX = lKneeX + lLegNx * ankleLen;
const lAnkleY = lKneeY + lLegNy * ankleLen;
const rLegDx = this._rightLegCx - rHipX;
const rLegDy = Math.max(0.05, this._rightLegCy - hipY);
const rLegDist = Math.sqrt(rLegDx * rLegDx + rLegDy * rLegDy) || 0.01;
const rLegNx = rLegDx / rLegDist;
const rLegNy = rLegDy / rLegDist;
const rKneeX = rHipX + rLegNx * kneeLen;
const rKneeY = hipY + rLegNy * kneeLen;
const rAnkleX = rKneeX + rLegNx * ankleLen;
const rAnkleY = rKneeY + rLegNy * ankleLen;
// Arm raise amount (for hand openness)
const leftArmRaise = Math.max(0, Math.min(1, (shoulderY - this._leftArmCy) / (bodyH * 0.3)));
const rightArmRaise = Math.max(0, Math.min(1, (shoulderY - this._rightArmCy) / (bodyH * 0.3)));
// Compute hand finger positions from wrist-elbow axis
const lHandAngle = Math.atan2(lWristY - lElbowY, lWristX - lElbowX);
const rHandAngle = Math.atan2(rWristY - rElbowY, rWristX - rElbowX);
const fingerLen = P.wristToFinger * bodyH;
const fingerSpr = P.fingerSpread * bodyH;
// Hand openness driven by arm raise + arm lateral spread
const lArmSpread = Math.abs(this._leftArmCx - (bx + bw * 0.3)) / (bw * 0.3);
const rArmSpread = Math.abs(this._rightArmCx - (bx + bw * 0.7)) / (bw * 0.3);
const lHandOpen = Math.min(1, leftArmRaise * 0.5 + lArmSpread * 0.5);
const rHandOpen = Math.min(1, rightArmRaise * 0.5 + rArmSpread * 0.5);
const keypoints = [
// 0: nose
{ x: headX, y: headY + 0.01, confidence: 0.92 },
// 1: left_eye
{ x: headX - P.eyeSpacing * bodyH, y: headY - 0.005, confidence: 0.88 },
// 2: right_eye
{ x: headX + P.eyeSpacing * bodyH, y: headY - 0.005, confidence: 0.88 },
// 3: left_ear
{ x: headX - P.earSpacing * bodyH, y: headY + 0.005, confidence: 0.72 },
// 4: right_ear
{ x: headX + P.earSpacing * bodyH, y: headY + 0.005, confidence: 0.72 },
// 5: left_shoulder
{ x: lShX, y: lShY, confidence: 0.94 },
// 6: right_shoulder
{ x: rShX, y: rShY, confidence: 0.94 },
// 7: left_elbow
{ x: lElbowX, y: lElbowY, confidence: 0.87 },
// 8: right_elbow
{ x: rElbowX, y: rElbowY, confidence: 0.87 },
// 9: left_wrist
{ x: lWristX, y: lWristY, confidence: 0.82 },
// 10: right_wrist
{ x: rWristX, y: rWristY, confidence: 0.82 },
// 11: left_hip
{ x: lHipX, y: hipY, confidence: 0.91 },
// 12: right_hip
{ x: rHipX, y: hipY, confidence: 0.91 },
// 13: left_knee
{ x: lKneeX, y: lKneeY, confidence: 0.88 },
// 14: right_knee
{ x: rKneeX, y: rKneeY, confidence: 0.88 },
// 15: left_ankle
{ x: lAnkleX, y: lAnkleY, confidence: 0.83 },
// 16: right_ankle
{ x: rAnkleX, y: rAnkleY, confidence: 0.83 },
// === Extended keypoints (17-25) ===
// 17: left_thumb — offset at thumb angle from wrist-elbow axis
{ x: lWristX + fingerLen * Math.cos(lHandAngle + P.thumbAngle) * (0.6 + lHandOpen * 0.4),
y: lWristY + fingerLen * Math.sin(lHandAngle + P.thumbAngle) * (0.6 + lHandOpen * 0.4),
confidence: 0.68 * (0.5 + lHandOpen * 0.5) },
// 18: left_index — extends along wrist-elbow axis
{ x: lWristX + fingerLen * Math.cos(lHandAngle) + fingerSpr * lHandOpen * Math.cos(lHandAngle + 0.3),
y: lWristY + fingerLen * Math.sin(lHandAngle) + fingerSpr * lHandOpen * Math.sin(lHandAngle + 0.3),
confidence: 0.72 * (0.5 + lHandOpen * 0.5) },
// 19: left_pinky — offset opposite thumb
{ x: lWristX + fingerLen * 0.85 * Math.cos(lHandAngle - P.thumbAngle * 0.7),
y: lWristY + fingerLen * 0.85 * Math.sin(lHandAngle - P.thumbAngle * 0.7),
confidence: 0.60 * (0.5 + lHandOpen * 0.5) },
// 20: right_thumb
{ x: rWristX + fingerLen * Math.cos(rHandAngle - P.thumbAngle) * (0.6 + rHandOpen * 0.4),
y: rWristY + fingerLen * Math.sin(rHandAngle - P.thumbAngle) * (0.6 + rHandOpen * 0.4),
confidence: 0.68 * (0.5 + rHandOpen * 0.5) },
// 21: right_index
{ x: rWristX + fingerLen * Math.cos(rHandAngle) + fingerSpr * rHandOpen * Math.cos(rHandAngle - 0.3),
y: rWristY + fingerLen * Math.sin(rHandAngle) + fingerSpr * rHandOpen * Math.sin(rHandAngle - 0.3),
confidence: 0.72 * (0.5 + rHandOpen * 0.5) },
// 22: right_pinky
{ x: rWristX + fingerLen * 0.85 * Math.cos(rHandAngle + P.thumbAngle * 0.7),
y: rWristY + fingerLen * 0.85 * Math.sin(rHandAngle + P.thumbAngle * 0.7),
confidence: 0.60 * (0.5 + rHandOpen * 0.5) },
// 23: left_foot_index (toe tip) — extends forward from ankle
{ x: lAnkleX + P.ankleToToe * bodyH * 0.5,
y: lAnkleY + P.ankleToToe * bodyH * 0.3,
confidence: 0.65 },
// 24: right_foot_index
{ x: rAnkleX + P.ankleToToe * bodyH * 0.5,
y: rAnkleY + P.ankleToToe * bodyH * 0.3,
confidence: 0.65 },
// 25: neck (midpoint between shoulders, slightly above)
{ x: (lShX + rShX) / 2, y: shoulderY - P.headToShoulder * bodyH * 0.35, confidence: 0.93 },
];
for (let i = 0; i < keypoints.length; i++) {
keypoints[i].name = KEYPOINT_NAMES[i];
}
// === RuVector Attention Embedding Refinement ===
// Compute attention stats for the UI pipeline display, but only apply
// positional refinement when a trained model is loaded (random-weight
// embeddings carry no meaningful spatial signal and distort the skeleton).
if (embedding && embedding.length >= 26 * 3) {
this._computeEmbeddingStats(keypoints, embedding, bodyH);
}
return keypoints;
}
/**
* Apply RuVector attention embedding to refine joint positions and confidence.
*
* The 128-dim fused embedding is decoded as:
* - Dims 0-77: Per-joint (dx, dy, confidence_mod) × 26 joints
* - Dims 78-81: Global pose parameters (scale, rotation, lean)
* - Dims 82-127: Reserved for cross-modal fusion features
*
* The attention mechanism determines HOW MUCH each spatial region contributes
* to each joint's refinement. Multi-Head captures global relationships,
* Hyperbolic captures hierarchical (torsolimbhand) dependencies,
* MoE routes different body regions to specialized experts,
* Linear provides fast extremity refinement, Local-Global balances detail/context.
*/
/**
* Compute embedding statistics for UI display without modifying joint positions.
* The 6-stage attention pipeline stats are shown in the RuVector panel.
* Position refinement is disabled until a trained model replaces random weights.
*/
_computeEmbeddingStats(keypoints, emb, bodyH) {
const map = this._jointEmbMap;
const tc = (v) => Math.tanh(Number(v) || 0);
// Embedding energy (L2 norm of the used dims)
let energy = 0;
for (let i = 0; i < Math.min(emb.length, 82); i++) {
energy += emb[i] * emb[i];
}
energy = Math.sqrt(energy);
// Simulated per-joint refinement magnitude (what WOULD be applied)
const scale = bodyH * 0.015;
let totalRefinement = 0;
let maxDimVal = 0;
for (let j = 0; j < Math.min(keypoints.length, 26); j++) {
const jmap = map.joints[j];
if (!jmap) continue;
const dx = tc(emb[jmap.dxDim]) * scale;
const dy = tc(emb[jmap.dyDim]) * scale;
totalRefinement += Math.sqrt(dx * dx + dy * dy);
maxDimVal = Math.max(maxDimVal, Math.abs(tc(emb[jmap.dxDim])), Math.abs(tc(emb[jmap.dyDim])));
}
this.attentionStats.energy = energy;
this.attentionStats.maxDim = maxDimVal;
this.attentionStats.refinementMag = totalRefinement / 26;
}
/**
* Find weighted motion centroids for each body zone.
* Divides the bounding box into 6 zones: head, left arm, right arm, torso, left leg, right leg.
* Returns the (x,y) centroid of motion intensity for each zone.
*/
_findZoneCentroids(grid, cols, rows, bx, by, bw, bh) {
// Zone definitions (in grid-relative fractions)
const zones = {
head: { rMin: 0, rMax: 0.2, cMin: 0.25, cMax: 0.75, wx: 0, wy: 0, wt: 0 },
leftArm: { rMin: 0.1, rMax: 0.6, cMin: 0, cMax: 0.35, wx: 0, wy: 0, wt: 0 },
rightArm: { rMin: 0.1, rMax: 0.6, cMin: 0.65, cMax: 1.0, wx: 0, wy: 0, wt: 0 },
torso: { rMin: 0.15, rMax: 0.55, cMin: 0.3, cMax: 0.7, wx: 0, wy: 0, wt: 0 },
leftLeg: { rMin: 0.5, rMax: 1.0, cMin: 0.1, cMax: 0.5, wx: 0, wy: 0, wt: 0 },
rightLeg: { rMin: 0.5, rMax: 1.0, cMin: 0.5, cMax: 0.9, wx: 0, wy: 0, wt: 0 },
};
// Accumulate weighted centroids per zone
for (let r = 0; r < rows; r++) {
const ry = r / rows; // 0-1 within grid
for (let c = 0; c < cols; c++) {
const cx_g = c / cols; // 0-1 within grid
const val = grid[r][c];
if (val < 0.005) continue; // skip near-zero motion
// Map grid position to body-space coordinates (0-1)
const worldX = bx + cx_g * bw;
const worldY = by + ry * bh;
// Assign to matching zones (a cell can contribute to multiple overlapping zones)
for (const z of Object.values(zones)) {
if (ry >= z.rMin && ry < z.rMax && cx_g >= z.cMin && cx_g < z.cMax) {
z.wx += worldX * val;
z.wy += worldY * val;
z.wt += val;
}
}
}
}
// Compute centroids with fallback defaults
const centroid = (z, defX, defY) => ({
x: z.wt > 0.01 ? z.wx / z.wt : defX,
y: z.wt > 0.01 ? z.wy / z.wt : defY,
weight: z.wt
});
const midX = bx + bw / 2;
const midY = by + bh / 2;
return {
head: centroid(zones.head, midX, by + bh * 0.1),
leftArm: centroid(zones.leftArm, bx + bw * 0.2, midY - bh * 0.05),
rightArm: centroid(zones.rightArm, bx + bw * 0.8, midY - bh * 0.05),
torso: centroid(zones.torso, midX, midY),
leftLeg: centroid(zones.leftLeg, bx + bw * 0.35,by + bh * 0.75),
rightLeg: centroid(zones.rightLeg, bx + bw * 0.65,by + bh * 0.75),
};
}
/**
* Through-wall tracking: continue showing pose via CSI when person left video frame.
* The skeleton drifts in the exit direction with decreasing confidence.
*/
_trackThroughWall(elapsed, csiState) {
if (!this._lastBodyState) return [];
const dt = elapsed - this._lastBodyState.time;
const csiPresence = csiState.csiPresence || 0;
// Initialize ghost on first call
if (this._ghostConfidence <= 0.05) {
this._ghostConfidence = 0.8;
this._ghostState = this._lastBodyState.keypoints.map(kp => ({...kp}));
}
// Ghost confidence decays, but CSI presence sustains it
const csiBoost = Math.min(0.7, csiPresence * 0.8);
this._ghostConfidence = Math.max(0.05, this._ghostConfidence * 0.995 - 0.001 + csiBoost * 0.002);
// Drift the ghost in exit direction
const vx = this._ghostVelocity.x;
const vy = this._ghostVelocity.y;
// Breathing continues via CSI
const breathe = Math.sin(elapsed * 1.5) * 0.003 * csiPresence;
const keypoints = this._ghostState.map((kp, i) => {
return {
x: kp.x + vx * dt * 0.3,
y: kp.y + vy * dt * 0.3 + (i >= 5 && i <= 6 ? breathe : 0),
confidence: kp.confidence * this._ghostConfidence * (0.5 + csiPresence * 0.5),
name: kp.name
};
});
// Slow down drift over time
this._ghostVelocity.x *= 0.998;
this._ghostVelocity.y *= 0.998;
this.smoothedKeypoints = keypoints;
return keypoints;
}
}
+235
View File
@@ -0,0 +1,235 @@
/**
* VideoCapture getUserMedia webcam capture with frame extraction.
* Provides quality metrics (brightness, motion) for fusion confidence gating.
*/
export class VideoCapture {
constructor(videoElement) {
this.video = videoElement;
this.stream = null;
this.offscreen = document.createElement('canvas');
this.offCtx = this.offscreen.getContext('2d', { willReadFrequently: true });
this.prevFrame = null;
this.motionScore = 0;
this.brightnessScore = 0;
}
async start(constraints = {}) {
const defaultConstraints = {
video: {
width: { ideal: 640 },
height: { ideal: 480 },
facingMode: 'user',
frameRate: { ideal: 30 }
},
audio: false
};
try {
this.stream = await navigator.mediaDevices.getUserMedia(
Object.keys(constraints).length ? constraints : defaultConstraints
);
this.video.srcObject = this.stream;
await this.video.play();
this.offscreen.width = this.video.videoWidth;
this.offscreen.height = this.video.videoHeight;
return true;
} catch (err) {
console.error('[Video] Camera access failed:', err.message);
return false;
}
}
stop() {
if (this.stream) {
this.stream.getTracks().forEach(t => t.stop());
this.stream = null;
}
this.video.srcObject = null;
}
get isActive() {
return this.stream !== null && this.video.readyState >= 2;
}
get width() { return this.video.videoWidth || 640; }
get height() { return this.video.videoHeight || 480; }
/**
* Capture current frame as RGB Uint8Array + compute quality metrics.
* @param {number} targetW - Target width for CNN input
* @param {number} targetH - Target height for CNN input
* @returns {{ rgb: Uint8Array, width: number, height: number, motion: number, brightness: number }}
*/
captureFrame(targetW = 56, targetH = 56) {
if (!this.isActive) return null;
// Draw to offscreen at target resolution
this.offscreen.width = targetW;
this.offscreen.height = targetH;
this.offCtx.drawImage(this.video, 0, 0, targetW, targetH);
const imageData = this.offCtx.getImageData(0, 0, targetW, targetH);
const rgba = imageData.data;
// Convert RGBA → RGB
const pixels = targetW * targetH;
const rgb = new Uint8Array(pixels * 3);
let brightnessSum = 0;
let motionSum = 0;
for (let i = 0; i < pixels; i++) {
const r = rgba[i * 4];
const g = rgba[i * 4 + 1];
const b = rgba[i * 4 + 2];
rgb[i * 3] = r;
rgb[i * 3 + 1] = g;
rgb[i * 3 + 2] = b;
// Luminance for brightness
const lum = 0.299 * r + 0.587 * g + 0.114 * b;
brightnessSum += lum;
// Motion: diff from previous frame
if (this.prevFrame) {
const pr = this.prevFrame[i * 3];
const pg = this.prevFrame[i * 3 + 1];
const pb = this.prevFrame[i * 3 + 2];
motionSum += Math.abs(r - pr) + Math.abs(g - pg) + Math.abs(b - pb);
}
}
this.brightnessScore = brightnessSum / (pixels * 255);
this.motionScore = this.prevFrame ? Math.min(1, motionSum / (pixels * 100)) : 0;
this.prevFrame = new Uint8Array(rgb);
return {
rgb,
width: targetW,
height: targetH,
motion: this.motionScore,
brightness: this.brightnessScore
};
}
/**
* Capture full-resolution RGBA for overlay rendering
* @returns {ImageData|null}
*/
captureFullFrame() {
if (!this.isActive) return null;
this.offscreen.width = this.width;
this.offscreen.height = this.height;
this.offCtx.drawImage(this.video, 0, 0);
return this.offCtx.getImageData(0, 0, this.width, this.height);
}
/**
* Detect motion region + detailed motion grid for body-part tracking.
* Returns bounding box + a grid showing WHERE motion is concentrated.
* @returns {{ x, y, w, h, detected: boolean, motionGrid: number[][], gridCols: number, gridRows: number, exitDirection: string|null }}
*/
detectMotionRegion(targetW = 56, targetH = 56) {
if (!this.isActive || !this.prevFrame) return { detected: false, motionGrid: null };
this.offscreen.width = targetW;
this.offscreen.height = targetH;
this.offCtx.drawImage(this.video, 0, 0, targetW, targetH);
const rgba = this.offCtx.getImageData(0, 0, targetW, targetH).data;
let minX = targetW, minY = targetH, maxX = 0, maxY = 0;
let motionPixels = 0;
const threshold = 25;
// Motion grid: divide frame into cells and track motion intensity per cell
const gridCols = 10;
const gridRows = 8;
const cellW = targetW / gridCols;
const cellH = targetH / gridRows;
const motionGrid = Array.from({ length: gridRows }, () => new Float32Array(gridCols));
const cellPixels = cellW * cellH;
// Also track motion centroid weighted by intensity
let motionCxSum = 0, motionCySum = 0, motionWeightSum = 0;
for (let y = 0; y < targetH; y++) {
for (let x = 0; x < targetW; x++) {
const i = y * targetW + x;
const r = rgba[i * 4], g = rgba[i * 4 + 1], b = rgba[i * 4 + 2];
const pr = this.prevFrame[i * 3], pg = this.prevFrame[i * 3 + 1], pb = this.prevFrame[i * 3 + 2];
const diff = Math.abs(r - pr) + Math.abs(g - pg) + Math.abs(b - pb);
if (diff > threshold * 3) {
motionPixels++;
if (x < minX) minX = x;
if (y < minY) minY = y;
if (x > maxX) maxX = x;
if (y > maxY) maxY = y;
}
// Accumulate per-cell motion intensity
const gc = Math.min(Math.floor(x / cellW), gridCols - 1);
const gr = Math.min(Math.floor(y / cellH), gridRows - 1);
const intensity = diff / (3 * 255); // Normalize 0-1
motionGrid[gr][gc] += intensity / cellPixels;
// Weighted centroid
if (diff > threshold) {
motionCxSum += x * diff;
motionCySum += y * diff;
motionWeightSum += diff;
}
}
}
const detected = motionPixels > (targetW * targetH * 0.02);
// Motion centroid (normalized 0-1)
const motionCx = motionWeightSum > 0 ? motionCxSum / (motionWeightSum * targetW) : 0.5;
const motionCy = motionWeightSum > 0 ? motionCySum / (motionWeightSum * targetH) : 0.5;
// Detect exit direction: if centroid is near edges
let exitDirection = null;
if (detected && motionCx < 0.1) exitDirection = 'left';
else if (detected && motionCx > 0.9) exitDirection = 'right';
else if (detected && motionCy < 0.1) exitDirection = 'up';
else if (detected && motionCy > 0.9) exitDirection = 'down';
// Track last known position for through-wall persistence
if (detected) {
this._lastDetected = {
x: minX / targetW,
y: minY / targetH,
w: (maxX - minX) / targetW,
h: (maxY - minY) / targetH,
cx: motionCx,
cy: motionCy,
exitDirection,
time: performance.now()
};
}
return {
detected,
x: minX / targetW,
y: minY / targetH,
w: (maxX - minX) / targetW,
h: (maxY - minY) / targetH,
coverage: motionPixels / (targetW * targetH),
motionGrid,
gridCols,
gridRows,
motionCx,
motionCy,
exitDirection
};
}
/**
* Get the last known detection info (for through-wall persistence)
*/
get lastDetection() {
return this._lastDetected || null;
}
}
@@ -0,0 +1,21 @@
MIT License
Copyright (c) 2025 rUv
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
@@ -0,0 +1,220 @@
# ruvector-attention-wasm
WebAssembly bindings for the ruvector-attention package, providing high-performance attention mechanisms for browser and Node.js environments.
## Features
- **Multiple Attention Mechanisms**:
- Scaled Dot-Product Attention
- Multi-Head Attention
- Hyperbolic Attention (for hierarchical data)
- Linear Attention (Performer-style)
- Flash Attention (memory-efficient)
- Local-Global Attention
- Mixture of Experts (MoE) Attention
- **CGT Sheaf Attention** (coherence-gated via Prime-Radiant)
- **Training Utilities**:
- InfoNCE contrastive loss
- Adam optimizer
- AdamW optimizer (with decoupled weight decay)
- Learning rate scheduler (warmup + cosine decay)
- **TypeScript Support**: Full type definitions and modern API
## Installation
```bash
npm install ruvector-attention-wasm
```
## Usage
### TypeScript/JavaScript
```typescript
import { initialize, MultiHeadAttention, utils } from 'ruvector-attention-wasm';
// Initialize WASM module
await initialize();
// Create multi-head attention
const attention = new MultiHeadAttention({ dim: 64, numHeads: 8 });
// Prepare inputs
const query = new Float32Array(64);
const keys = [new Float32Array(64), new Float32Array(64)];
const values = [new Float32Array(64), new Float32Array(64)];
// Compute attention
const output = attention.compute(query, keys, values);
// Use utilities
const similarity = utils.cosineSimilarity(query, keys[0]);
```
### Advanced Examples
#### Hyperbolic Attention
```typescript
import { HyperbolicAttention } from 'ruvector-attention-wasm';
const hyperbolic = new HyperbolicAttention({
dim: 128,
curvature: 1.0
});
const output = hyperbolic.compute(query, keys, values);
```
#### MoE Attention with Expert Stats
```typescript
import { MoEAttention } from 'ruvector-attention-wasm';
const moe = new MoEAttention({
dim: 64,
numExperts: 4,
topK: 2
});
const output = moe.compute(query, keys, values);
// Get expert utilization
const stats = moe.getExpertStats();
console.log('Load balance:', stats.loadBalance);
```
#### Training with InfoNCE Loss
```typescript
import { InfoNCELoss, Adam } from 'ruvector-attention-wasm';
const loss = new InfoNCELoss(0.07);
const optimizer = new Adam(paramCount, {
learningRate: 0.001,
beta1: 0.9,
beta2: 0.999,
});
// Training loop
const lossValue = loss.compute(anchor, positive, negatives);
optimizer.step(params, gradients);
```
#### Learning Rate Scheduling
```typescript
import { LRScheduler, AdamW } from 'ruvector-attention-wasm';
const scheduler = new LRScheduler({
initialLR: 0.001,
warmupSteps: 1000,
totalSteps: 10000,
});
const optimizer = new AdamW(paramCount, {
learningRate: scheduler.getLR(),
weightDecay: 0.01,
});
// Training loop
for (let step = 0; step < 10000; step++) {
optimizer.learningRate = scheduler.getLR();
optimizer.step(params, gradients);
scheduler.step();
}
```
## Building from Source
### Prerequisites
- Rust 1.70+
- wasm-pack
### Build Commands
```bash
# Build for web (ES modules)
wasm-pack build --target web --out-dir pkg
# Build for Node.js
wasm-pack build --target nodejs --out-dir pkg-node
# Build for bundlers (webpack, vite, etc.)
wasm-pack build --target bundler --out-dir pkg-bundler
# Run tests
wasm-pack test --headless --firefox
```
## API Reference
### Attention Mechanisms
- `MultiHeadAttention` - Standard multi-head attention
- `HyperbolicAttention` - Attention in hyperbolic space
- `LinearAttention` - Linear complexity attention (Performer)
- `FlashAttention` - Memory-efficient attention
- `LocalGlobalAttention` - Combined local and global attention
- `MoEAttention` - Mixture of Experts attention
- `CGTSheafAttention` - Coherence-gated via Prime-Radiant energy
- `scaledDotAttention()` - Functional API for basic attention
### CGT Sheaf Attention (Prime-Radiant Integration)
The CGT (Coherence-Gated Transformer) Sheaf Attention mechanism uses Prime-Radiant's sheaf Laplacian energy to gate attention based on mathematical consistency:
```typescript
import { CGTSheafAttention } from 'ruvector-attention-wasm';
const cgtAttention = new CGTSheafAttention({
dim: 128,
numHeads: 8,
coherenceThreshold: 0.3, // Block if energy > threshold
});
// Attention is gated by coherence energy
const result = cgtAttention.compute(query, keys, values);
console.log('Coherence energy:', result.energy);
console.log('Is coherent:', result.isCoherent);
```
**Key features:**
- Energy-weighted attention: Lower coherence energy → higher attention
- Automatic hallucination detection via residual analysis
- GPU-accelerated with wgpu WGSL shaders (vec4 optimized)
- SIMD fallback (AVX-512/AVX2/NEON)
### Training
- `InfoNCELoss` - Contrastive loss function
- `Adam` - Adam optimizer
- `AdamW` - AdamW optimizer with weight decay
- `LRScheduler` - Learning rate scheduler
### Utilities
- `utils.cosineSimilarity()` - Cosine similarity between vectors
- `utils.l2Norm()` - L2 norm of a vector
- `utils.normalize()` - Normalize vector to unit length
- `utils.softmax()` - Apply softmax transformation
- `utils.attentionWeights()` - Compute attention weights from scores
- `utils.batchNormalize()` - Batch normalization
- `utils.randomOrthogonalMatrix()` - Generate random orthogonal matrix
- `utils.pairwiseDistances()` - Compute pairwise distances
## Performance
The WASM bindings provide near-native performance for attention computations:
- Optimized with `opt-level = "s"` and LTO
- SIMD acceleration where available
- Efficient memory management
- Zero-copy data transfer where possible
## License
MIT OR Apache-2.0
@@ -0,0 +1,28 @@
{
"name": "ruvector-attention-wasm",
"collaborators": [
"Ruvector Team"
],
"description": "High-performance WebAssembly attention mechanisms: Multi-Head, Flash, Hyperbolic, MoE, CGT Sheaf Attention with GPU acceleration for transformers and LLMs",
"version": "2.0.5",
"license": "MIT",
"repository": {
"type": "git",
"url": "https://github.com/ruvnet/ruvector"
},
"files": [
"ruvector_attention_wasm_bg.wasm",
"ruvector_attention_wasm.js",
"ruvector_attention_wasm.d.ts"
],
"main": "ruvector_attention_wasm.js",
"homepage": "https://ruv.io/ruvector",
"types": "ruvector_attention_wasm.d.ts",
"keywords": [
"wasm",
"attention",
"transformer",
"flash-attention",
"llm"
]
}
@@ -0,0 +1,642 @@
/**
* Browser ESM wrapper for ruvector-attention-wasm v2.0.5
*
* The upstream pkg/ was built with wasm-pack --target nodejs (CJS + fs.readFileSync).
* This wrapper loads the same WASM binary via fetch() for browser use.
*
* Usage:
* import initWasm, { WasmMultiHeadAttention, ... } from './ruvector_attention_browser.js';
* await initWasm();
* const attn = new WasmMultiHeadAttention(dim, heads);
*/
let _wasm;
let _initialized = false;
// The entire CJS module runs inside this IIFE to avoid polluting global scope.
// We capture all exports in _mod.
const _mod = {};
(function(exports, wasm_getter) {
// ── wasm-bindgen heap management ──────────────────────────────────
const heap = new Array(128).fill(undefined);
heap.push(undefined, null, true, false);
let heap_next = heap.length;
function addHeapObject(obj) {
if (heap_next === heap.length) heap.push(heap.length + 1);
const idx = heap_next;
heap_next = heap[idx];
heap[idx] = obj;
return idx;
}
function getObject(idx) { return heap[idx]; }
function dropObject(idx) {
if (idx < 132) return;
heap[idx] = heap_next;
heap_next = idx;
}
function takeObject(idx) {
const ret = getObject(idx);
dropObject(idx);
return ret;
}
function isLikeNone(x) { return x === undefined || x === null; }
// ── Memory views ──────────────────────────────────────────────────
let cachedDataViewMemory0 = null;
let cachedUint8ArrayMemory0 = null;
let cachedFloat32ArrayMemory0 = null;
function wasm() { return wasm_getter(); }
function getDataViewMemory0() {
if (cachedDataViewMemory0 === null || cachedDataViewMemory0.buffer !== wasm().memory.buffer)
cachedDataViewMemory0 = new DataView(wasm().memory.buffer);
return cachedDataViewMemory0;
}
function getUint8ArrayMemory0() {
if (cachedUint8ArrayMemory0 === null || cachedUint8ArrayMemory0.buffer !== wasm().memory.buffer)
cachedUint8ArrayMemory0 = new Uint8Array(wasm().memory.buffer);
return cachedUint8ArrayMemory0;
}
function getFloat32ArrayMemory0() {
if (cachedFloat32ArrayMemory0 === null || cachedFloat32ArrayMemory0.buffer !== wasm().memory.buffer)
cachedFloat32ArrayMemory0 = new Float32Array(wasm().memory.buffer);
return cachedFloat32ArrayMemory0;
}
function getArrayF32FromWasm0(ptr, len) {
ptr = ptr >>> 0;
return getFloat32ArrayMemory0().subarray(ptr / 4, ptr / 4 + len);
}
function getArrayU8FromWasm0(ptr, len) {
ptr = ptr >>> 0;
return getUint8ArrayMemory0().subarray(ptr, ptr + len);
}
let WASM_VECTOR_LEN = 0;
function passArrayF32ToWasm0(arg, malloc) {
const ptr = malloc(arg.length * 4, 4) >>> 0;
getFloat32ArrayMemory0().set(arg, ptr / 4);
WASM_VECTOR_LEN = arg.length;
return ptr;
}
const cachedTextEncoder = new TextEncoder();
const cachedTextDecoder = new TextDecoder('utf-8', { ignoreBOM: true, fatal: true });
cachedTextDecoder.decode();
function getStringFromWasm0(ptr, len) {
ptr = ptr >>> 0;
return cachedTextDecoder.decode(getUint8ArrayMemory0().subarray(ptr, ptr + len));
}
function passStringToWasm0(arg, malloc, realloc) {
const buf = cachedTextEncoder.encode(arg);
const ptr = malloc(buf.length, 1) >>> 0;
getUint8ArrayMemory0().subarray(ptr, ptr + buf.length).set(buf);
WASM_VECTOR_LEN = buf.length;
return ptr;
}
function debugString(val) {
const type = typeof val;
if (type == 'number' || type == 'boolean' || val == null) return `${val}`;
if (type == 'string') return `"${val}"`;
if (type == 'symbol') return val.description ? `Symbol(${val.description})` : 'Symbol';
if (type == 'function') return 'Function';
if (Array.isArray(val)) return `[${val.map(debugString).join(', ')}]`;
try {
const keys = Object.keys(val);
return `{${keys.map(k => `${k}: ${debugString(val[k])}`).join(', ')}}`;
} catch (_) { return Object.prototype.toString.call(val); }
}
function handleError(f, args) {
try { return f.apply(this, args); }
catch (e) { wasm().__wbindgen_export3(addHeapObject(e)); }
}
// ── FinalizationRegistry ──────────────────────────────────────────
const FR = typeof FinalizationRegistry !== 'undefined'
? FinalizationRegistry
: class { register() {} unregister() {} };
const WasmMultiHeadAttentionFinalization = new FR(ptr => wasm().__wbg_wasmmultiheadattention_free(ptr >>> 0, 1));
const WasmFlashAttentionFinalization = new FR(ptr => wasm().__wbg_wasmflashattention_free(ptr >>> 0, 1));
const WasmHyperbolicAttentionFinalization = new FR(ptr => wasm().__wbg_wasmhyperbolicattention_free(ptr >>> 0, 1));
const WasmMoEAttentionFinalization = new FR(ptr => wasm().__wbg_wasmmoeattention_free(ptr >>> 0, 1));
const WasmLinearAttentionFinalization = new FR(ptr => wasm().__wbg_wasmlinearattention_free(ptr >>> 0, 1));
const WasmLocalGlobalAttentionFinalization = new FR(ptr => wasm().__wbg_wasmlocalglobalattention_free(ptr >>> 0, 1));
// ── Classes ───────────────────────────────────────────────────────
class WasmMultiHeadAttention {
constructor(dim, num_heads) {
const retptr = wasm().__wbindgen_add_to_stack_pointer(-16);
try {
wasm().wasmmultiheadattention_new(retptr, dim, num_heads);
var r0 = getDataViewMemory0().getInt32(retptr + 0, true);
var r1 = getDataViewMemory0().getInt32(retptr + 4, true);
var r2 = getDataViewMemory0().getInt32(retptr + 8, true);
if (r2) throw takeObject(r1);
this.__wbg_ptr = r0 >>> 0;
WasmMultiHeadAttentionFinalization.register(this, this.__wbg_ptr, this);
} finally {
wasm().__wbindgen_add_to_stack_pointer(16);
}
}
free() {
const ptr = this.__wbg_ptr; this.__wbg_ptr = 0;
WasmMultiHeadAttentionFinalization.unregister(this);
wasm().__wbg_wasmmultiheadattention_free(ptr, 0);
}
get dim() { return wasm().wasmmultiheadattention_dim(this.__wbg_ptr); }
get num_heads() { return wasm().wasmmultiheadattention_num_heads(this.__wbg_ptr); }
compute(query, keys, values) {
const retptr = wasm().__wbindgen_add_to_stack_pointer(-16);
try {
const ptr0 = passArrayF32ToWasm0(query, wasm().__wbindgen_export);
const len0 = WASM_VECTOR_LEN;
wasm().wasmmultiheadattention_compute(retptr, this.__wbg_ptr, ptr0, len0, addHeapObject(keys), addHeapObject(values));
var r0 = getDataViewMemory0().getInt32(retptr + 0, true);
var r1 = getDataViewMemory0().getInt32(retptr + 4, true);
var r2 = getDataViewMemory0().getInt32(retptr + 8, true);
var r3 = getDataViewMemory0().getInt32(retptr + 12, true);
if (r3) throw takeObject(r2);
var v1 = getArrayF32FromWasm0(r0, r1).slice();
wasm().__wbindgen_export4(r0, r1 * 4, 4);
return v1;
} finally {
wasm().__wbindgen_add_to_stack_pointer(16);
}
}
}
class WasmFlashAttention {
constructor(dim, block_size) {
const ret = wasm().wasmflashattention_new(dim, block_size);
this.__wbg_ptr = ret >>> 0;
WasmFlashAttentionFinalization.register(this, this.__wbg_ptr, this);
}
free() {
const ptr = this.__wbg_ptr; this.__wbg_ptr = 0;
WasmFlashAttentionFinalization.unregister(this);
wasm().__wbg_wasmflashattention_free(ptr, 0);
}
compute(query, keys, values) {
const retptr = wasm().__wbindgen_add_to_stack_pointer(-16);
try {
const ptr0 = passArrayF32ToWasm0(query, wasm().__wbindgen_export);
const len0 = WASM_VECTOR_LEN;
wasm().wasmflashattention_compute(retptr, this.__wbg_ptr, ptr0, len0, addHeapObject(keys), addHeapObject(values));
var r0 = getDataViewMemory0().getInt32(retptr + 0, true);
var r1 = getDataViewMemory0().getInt32(retptr + 4, true);
var r2 = getDataViewMemory0().getInt32(retptr + 8, true);
var r3 = getDataViewMemory0().getInt32(retptr + 12, true);
if (r3) throw takeObject(r2);
var v1 = getArrayF32FromWasm0(r0, r1).slice();
wasm().__wbindgen_export4(r0, r1 * 4, 4);
return v1;
} finally {
wasm().__wbindgen_add_to_stack_pointer(16);
}
}
}
class WasmHyperbolicAttention {
constructor(dim, curvature) {
const ret = wasm().wasmhyperbolicattention_new(dim, curvature);
this.__wbg_ptr = ret >>> 0;
WasmHyperbolicAttentionFinalization.register(this, this.__wbg_ptr, this);
}
free() {
const ptr = this.__wbg_ptr; this.__wbg_ptr = 0;
WasmHyperbolicAttentionFinalization.unregister(this);
wasm().__wbg_wasmhyperbolicattention_free(ptr, 0);
}
get curvature() { return wasm().wasmhyperbolicattention_curvature(this.__wbg_ptr); }
compute(query, keys, values) {
const retptr = wasm().__wbindgen_add_to_stack_pointer(-16);
try {
const ptr0 = passArrayF32ToWasm0(query, wasm().__wbindgen_export);
const len0 = WASM_VECTOR_LEN;
wasm().wasmhyperbolicattention_compute(retptr, this.__wbg_ptr, ptr0, len0, addHeapObject(keys), addHeapObject(values));
var r0 = getDataViewMemory0().getInt32(retptr + 0, true);
var r1 = getDataViewMemory0().getInt32(retptr + 4, true);
var r2 = getDataViewMemory0().getInt32(retptr + 8, true);
var r3 = getDataViewMemory0().getInt32(retptr + 12, true);
if (r3) throw takeObject(r2);
var v1 = getArrayF32FromWasm0(r0, r1).slice();
wasm().__wbindgen_export4(r0, r1 * 4, 4);
return v1;
} finally {
wasm().__wbindgen_add_to_stack_pointer(16);
}
}
}
class WasmMoEAttention {
constructor(dim, num_experts, top_k) {
const ret = wasm().wasmmoeattention_new(dim, num_experts, top_k);
this.__wbg_ptr = ret >>> 0;
WasmMoEAttentionFinalization.register(this, this.__wbg_ptr, this);
}
free() {
const ptr = this.__wbg_ptr; this.__wbg_ptr = 0;
WasmMoEAttentionFinalization.unregister(this);
wasm().__wbg_wasmmoeattention_free(ptr, 0);
}
compute(query, keys, values) {
const retptr = wasm().__wbindgen_add_to_stack_pointer(-16);
try {
const ptr0 = passArrayF32ToWasm0(query, wasm().__wbindgen_export);
const len0 = WASM_VECTOR_LEN;
wasm().wasmmoeattention_compute(retptr, this.__wbg_ptr, ptr0, len0, addHeapObject(keys), addHeapObject(values));
var r0 = getDataViewMemory0().getInt32(retptr + 0, true);
var r1 = getDataViewMemory0().getInt32(retptr + 4, true);
var r2 = getDataViewMemory0().getInt32(retptr + 8, true);
var r3 = getDataViewMemory0().getInt32(retptr + 12, true);
if (r3) throw takeObject(r2);
var v1 = getArrayF32FromWasm0(r0, r1).slice();
wasm().__wbindgen_export4(r0, r1 * 4, 4);
return v1;
} finally {
wasm().__wbindgen_add_to_stack_pointer(16);
}
}
}
class WasmLinearAttention {
constructor(dim, num_features) {
const ret = wasm().wasmlinearattention_new(dim, num_features || dim);
this.__wbg_ptr = ret >>> 0;
WasmLinearAttentionFinalization.register(this, this.__wbg_ptr, this);
}
free() {
const ptr = this.__wbg_ptr; this.__wbg_ptr = 0;
WasmLinearAttentionFinalization.unregister(this);
wasm().__wbg_wasmlinearattention_free(ptr, 0);
}
compute(query, keys, values) {
const retptr = wasm().__wbindgen_add_to_stack_pointer(-16);
try {
const ptr0 = passArrayF32ToWasm0(query, wasm().__wbindgen_export);
const len0 = WASM_VECTOR_LEN;
wasm().wasmlinearattention_compute(retptr, this.__wbg_ptr, ptr0, len0, addHeapObject(keys), addHeapObject(values));
var r0 = getDataViewMemory0().getInt32(retptr + 0, true);
var r1 = getDataViewMemory0().getInt32(retptr + 4, true);
var r2 = getDataViewMemory0().getInt32(retptr + 8, true);
var r3 = getDataViewMemory0().getInt32(retptr + 12, true);
if (r3) throw takeObject(r2);
var v1 = getArrayF32FromWasm0(r0, r1).slice();
wasm().__wbindgen_export4(r0, r1 * 4, 4);
return v1;
} finally {
wasm().__wbindgen_add_to_stack_pointer(16);
}
}
}
class WasmLocalGlobalAttention {
constructor(dim, local_window, global_tokens) {
const ret = wasm().wasmlocalglobalattention_new(dim, local_window || 4, global_tokens || 2);
this.__wbg_ptr = ret >>> 0;
WasmLocalGlobalAttentionFinalization.register(this, this.__wbg_ptr, this);
}
free() {
const ptr = this.__wbg_ptr; this.__wbg_ptr = 0;
WasmLocalGlobalAttentionFinalization.unregister(this);
wasm().__wbg_wasmlocalglobalattention_free(ptr, 0);
}
compute(query, keys, values) {
const retptr = wasm().__wbindgen_add_to_stack_pointer(-16);
try {
const ptr0 = passArrayF32ToWasm0(query, wasm().__wbindgen_export);
const len0 = WASM_VECTOR_LEN;
wasm().wasmlocalglobalattention_compute(retptr, this.__wbg_ptr, ptr0, len0, addHeapObject(keys), addHeapObject(values));
var r0 = getDataViewMemory0().getInt32(retptr + 0, true);
var r1 = getDataViewMemory0().getInt32(retptr + 4, true);
var r2 = getDataViewMemory0().getInt32(retptr + 8, true);
var r3 = getDataViewMemory0().getInt32(retptr + 12, true);
if (r3) throw takeObject(r2);
var v1 = getArrayF32FromWasm0(r0, r1).slice();
wasm().__wbindgen_export4(r0, r1 * 4, 4);
return v1;
} finally {
wasm().__wbindgen_add_to_stack_pointer(16);
}
}
}
// ── Standalone functions ──────────────────────────────────────────
function cosine_similarity(a, b) {
const retptr = wasm().__wbindgen_add_to_stack_pointer(-16);
try {
const ptr0 = passArrayF32ToWasm0(a, wasm().__wbindgen_export);
const len0 = WASM_VECTOR_LEN;
const ptr1 = passArrayF32ToWasm0(b, wasm().__wbindgen_export);
const len1 = WASM_VECTOR_LEN;
wasm().cosine_similarity(retptr, ptr0, len0, ptr1, len1);
var r0 = getDataViewMemory0().getFloat64(retptr + 0, true);
var r1 = getDataViewMemory0().getInt32(retptr + 8, true);
var r2 = getDataViewMemory0().getInt32(retptr + 12, true);
if (r2) throw takeObject(r1);
return r0;
} finally {
wasm().__wbindgen_add_to_stack_pointer(16);
}
}
function normalize(vec) {
const ptr0 = passArrayF32ToWasm0(vec, wasm().__wbindgen_export);
const len0 = WASM_VECTOR_LEN;
wasm().normalize(ptr0, len0, addHeapObject(vec));
}
function l2_norm(vec) {
const retptr = wasm().__wbindgen_add_to_stack_pointer(-16);
try {
const ptr0 = passArrayF32ToWasm0(vec, wasm().__wbindgen_export);
const len0 = WASM_VECTOR_LEN;
wasm().l2_norm(retptr, ptr0, len0);
var r0 = getDataViewMemory0().getFloat64(retptr + 0, true);
var r1 = getDataViewMemory0().getInt32(retptr + 8, true);
var r2 = getDataViewMemory0().getInt32(retptr + 12, true);
if (r2) throw takeObject(r1);
return r0;
} finally {
wasm().__wbindgen_add_to_stack_pointer(16);
}
}
function softmax(vec) {
const ptr0 = passArrayF32ToWasm0(vec, wasm().__wbindgen_export);
const len0 = WASM_VECTOR_LEN;
wasm().softmax(ptr0, len0, addHeapObject(vec));
}
function batch_normalize(vectors, epsilon) {
const retptr = wasm().__wbindgen_add_to_stack_pointer(-16);
try {
wasm().batch_normalize(retptr, addHeapObject(vectors), isLikeNone(epsilon) ? 0x100000001 : Math.fround(epsilon));
var r0 = getDataViewMemory0().getInt32(retptr + 0, true);
var r1 = getDataViewMemory0().getInt32(retptr + 4, true);
var r2 = getDataViewMemory0().getInt32(retptr + 8, true);
var r3 = getDataViewMemory0().getInt32(retptr + 12, true);
if (r3) throw takeObject(r2);
var v1 = getArrayF32FromWasm0(r0, r1).slice();
wasm().__wbindgen_export4(r0, r1 * 4, 4);
return v1;
} finally {
wasm().__wbindgen_add_to_stack_pointer(16);
}
}
function pairwise_distances(vectors) {
const retptr = wasm().__wbindgen_add_to_stack_pointer(-16);
try {
wasm().pairwise_distances(retptr, addHeapObject(vectors));
var r0 = getDataViewMemory0().getInt32(retptr + 0, true);
var r1 = getDataViewMemory0().getInt32(retptr + 4, true);
var r2 = getDataViewMemory0().getInt32(retptr + 8, true);
var r3 = getDataViewMemory0().getInt32(retptr + 12, true);
if (r3) throw takeObject(r2);
var v1 = getArrayF32FromWasm0(r0, r1).slice();
wasm().__wbindgen_export4(r0, r1 * 4, 4);
return v1;
} finally {
wasm().__wbindgen_add_to_stack_pointer(16);
}
}
function scaled_dot_attention(query, keys, values, scale) {
const retptr = wasm().__wbindgen_add_to_stack_pointer(-16);
try {
const ptr0 = passArrayF32ToWasm0(query, wasm().__wbindgen_export);
const len0 = WASM_VECTOR_LEN;
wasm().scaled_dot_attention(retptr, ptr0, len0, addHeapObject(keys), addHeapObject(values), isLikeNone(scale) ? 0x100000001 : Math.fround(scale));
var r0 = getDataViewMemory0().getInt32(retptr + 0, true);
var r1 = getDataViewMemory0().getInt32(retptr + 4, true);
var r2 = getDataViewMemory0().getInt32(retptr + 8, true);
var r3 = getDataViewMemory0().getInt32(retptr + 12, true);
if (r3) throw takeObject(r2);
var v1 = getArrayF32FromWasm0(r0, r1).slice();
wasm().__wbindgen_export4(r0, r1 * 4, 4);
return v1;
} finally {
wasm().__wbindgen_add_to_stack_pointer(16);
}
}
function attention_weights(scores, temperature) {
const ptr0 = passArrayF32ToWasm0(scores, wasm().__wbindgen_export);
const len0 = WASM_VECTOR_LEN;
wasm().attention_weights(ptr0, len0, addHeapObject(scores), isLikeNone(temperature) ? 0x100000001 : Math.fround(temperature));
}
function available_mechanisms() {
const ret = wasm().available_mechanisms();
return takeObject(ret);
}
function random_orthogonal_matrix(dim) {
const retptr = wasm().__wbindgen_add_to_stack_pointer(-16);
try {
wasm().random_orthogonal_matrix(retptr, dim);
var r0 = getDataViewMemory0().getInt32(retptr + 0, true);
var r1 = getDataViewMemory0().getInt32(retptr + 4, true);
var v1 = getArrayF32FromWasm0(r0, r1).slice();
wasm().__wbindgen_export4(r0, r1 * 4, 4);
return v1;
} finally {
wasm().__wbindgen_add_to_stack_pointer(16);
}
}
function rv_init() { wasm().init(); }
function rv_version() {
let d0, d1;
const retptr = wasm().__wbindgen_add_to_stack_pointer(-16);
try {
wasm().version(retptr);
d0 = getDataViewMemory0().getInt32(retptr + 0, true);
d1 = getDataViewMemory0().getInt32(retptr + 4, true);
return getStringFromWasm0(d0, d1);
} finally {
wasm().__wbindgen_add_to_stack_pointer(16);
if (d0 !== undefined) wasm().__wbindgen_export4(d0, d1, 1);
}
}
// ── Collect exports ───────────────────────────────────────────────
exports.WasmMultiHeadAttention = WasmMultiHeadAttention;
exports.WasmFlashAttention = WasmFlashAttention;
exports.WasmHyperbolicAttention = WasmHyperbolicAttention;
exports.WasmMoEAttention = WasmMoEAttention;
exports.WasmLinearAttention = WasmLinearAttention;
exports.WasmLocalGlobalAttention = WasmLocalGlobalAttention;
exports.cosine_similarity = cosine_similarity;
exports.normalize = normalize;
exports.l2_norm = l2_norm;
exports.softmax = softmax;
exports.batch_normalize = batch_normalize;
exports.pairwise_distances = pairwise_distances;
exports.scaled_dot_attention = scaled_dot_attention;
exports.attention_weights = attention_weights;
exports.available_mechanisms = available_mechanisms;
exports.random_orthogonal_matrix = random_orthogonal_matrix;
exports.init = rv_init;
exports.version = rv_version;
// ── Build WASM import object ──────────────────────────────────────
exports.__wbg_get_imports = function() {
const import0 = {
__proto__: null,
__wbg_Error_4577686b3a6d9b3a: (arg0, arg1) => addHeapObject(Error(getStringFromWasm0(arg0, arg1))),
__wbg_String_8564e559799eccda: (arg0, arg1) => {
const ret = String(getObject(arg1));
const ptr1 = passStringToWasm0(ret, wasm().__wbindgen_export, wasm().__wbindgen_export2);
const len1 = WASM_VECTOR_LEN;
getDataViewMemory0().setInt32(arg0 + 4, len1, true);
getDataViewMemory0().setInt32(arg0, ptr1, true);
},
__wbg___wbindgen_boolean_get_18c4ed9422296fff: (arg0) => {
const v = getObject(arg0);
const ret = typeof v === 'boolean' ? v : undefined;
return isLikeNone(ret) ? 0xFFFFFF : ret ? 1 : 0;
},
__wbg___wbindgen_copy_to_typed_array_5294f8e46aecc086: (arg0, arg1, arg2) => {
new Uint8Array(getObject(arg2).buffer, getObject(arg2).byteOffset, getObject(arg2).byteLength).set(getArrayU8FromWasm0(arg0, arg1));
},
__wbg___wbindgen_debug_string_ddde1867f49c2442: (arg0, arg1) => {
const ret = debugString(getObject(arg1));
const ptr1 = passStringToWasm0(ret, wasm().__wbindgen_export, wasm().__wbindgen_export2);
const len1 = WASM_VECTOR_LEN;
getDataViewMemory0().setInt32(arg0 + 4, len1, true);
getDataViewMemory0().setInt32(arg0, ptr1, true);
},
__wbg___wbindgen_is_function_d633e708baf0d146: (arg0) => typeof getObject(arg0) === 'function',
__wbg___wbindgen_is_object_4b3de556756ee8a8: (arg0) => {
const val = getObject(arg0);
return typeof val === 'object' && val !== null;
},
__wbg___wbindgen_jsval_loose_eq_1562ceb9af84e990: (arg0, arg1) => getObject(arg0) == getObject(arg1),
__wbg___wbindgen_number_get_5854912275df1894: (arg0, arg1) => {
const obj = getObject(arg1);
const ret = typeof obj === 'number' ? obj : undefined;
getDataViewMemory0().setFloat64(arg0 + 8, isLikeNone(ret) ? 0 : ret, true);
getDataViewMemory0().setInt32(arg0, !isLikeNone(ret), true);
},
__wbg___wbindgen_string_get_3e5751597f39a112: (arg0, arg1) => {
const obj = getObject(arg1);
const ret = typeof obj === 'string' ? obj : undefined;
var ptr1 = isLikeNone(ret) ? 0 : passStringToWasm0(ret, wasm().__wbindgen_export, wasm().__wbindgen_export2);
var len1 = WASM_VECTOR_LEN;
getDataViewMemory0().setInt32(arg0 + 4, len1, true);
getDataViewMemory0().setInt32(arg0, ptr1, true);
},
__wbg___wbindgen_throw_39bc967c0e5a9b58: (arg0, arg1) => { throw new Error(getStringFromWasm0(arg0, arg1)); },
__wbg_call_73af281463ec8b58: function() { return handleError(function(arg0, arg1) {
return addHeapObject(getObject(arg0).call(getObject(arg1)));
}, arguments); },
__wbg_done_5aad55ec6b1954b1: (arg0) => getObject(arg0).done,
__wbg_error_a6fa202b58aa1cd3: (arg0, arg1) => {
try { console.error(getStringFromWasm0(arg0, arg1)); }
finally { wasm().__wbindgen_export4(arg0, arg1, 1); }
},
__wbg_error_ad28debb48b5c6bb: (arg0) => console.error(getObject(arg0)),
__wbg_get_4920fefd3451364b: function() { return handleError(function(arg0, arg1) {
return addHeapObject(Reflect.get(getObject(arg0), getObject(arg1)));
}, arguments); },
__wbg_get_unchecked_3d0f4b91c8eca4f0: (arg0, arg1) => addHeapObject(getObject(arg0)[arg1 >>> 0]),
__wbg_instanceof_ArrayBuffer_15859862b80b732d: (arg0) => {
try { return getObject(arg0) instanceof ArrayBuffer; } catch (_) { return false; }
},
__wbg_instanceof_Uint8Array_2240b7046ac16f05: (arg0) => {
try { return getObject(arg0) instanceof Uint8Array; } catch (_) { return false; }
},
__wbg_isArray_fad08a0d12828686: (arg0) => Array.isArray(getObject(arg0)),
__wbg_iterator_fc7ad8d33bab9e26: () => addHeapObject(Symbol.iterator),
__wbg_length_5855c1f289dfffc1: (arg0) => getObject(arg0).length,
__wbg_length_a31e05262e09b7f8: (arg0) => getObject(arg0).length,
__wbg_log_3c5e4b64af29e724: (arg0) => console.log(getObject(arg0)),
__wbg_new_09959f7b4c92c246: (arg0) => addHeapObject(new Uint8Array(getObject(arg0))),
__wbg_new_227d7c05414eb861: () => addHeapObject(new Error()),
__wbg_new_cbee8c0d5c479eac: () => addHeapObject(new Array()),
__wbg_next_a5fe6f328f7affc2: (arg0) => addHeapObject(getObject(arg0).next),
__wbg_next_e592122bb4ed4c67: function() { return handleError(function(arg0) {
return addHeapObject(getObject(arg0).next());
}, arguments); },
__wbg_prototypesetcall_f034d444741426c3: (arg0, arg1, arg2) => {
Uint8Array.prototype.set.call(getArrayU8FromWasm0(arg0, arg1), getObject(arg2));
},
__wbg_random_2b7bed8995d680fb: () => Math.random(),
__wbg_set_4c81cfb5dc3a333c: (arg0, arg1, arg2) => { getObject(arg0)[arg1 >>> 0] = takeObject(arg2); },
__wbg_stack_3b0d974bbf31e44f: (arg0, arg1) => {
const ret = getObject(arg1).stack;
const ptr1 = passStringToWasm0(ret, wasm().__wbindgen_export, wasm().__wbindgen_export2);
const len1 = WASM_VECTOR_LEN;
getDataViewMemory0().setInt32(arg0 + 4, len1, true);
getDataViewMemory0().setInt32(arg0, ptr1, true);
},
__wbg_value_667dcb90597486a6: (arg0) => addHeapObject(getObject(arg0).value),
__wbindgen_cast_0000000000000001: (arg0, arg1) => addHeapObject(getStringFromWasm0(arg0, arg1)),
__wbindgen_object_drop_ref: (arg0) => takeObject(arg0),
};
return { __proto__: null, "./ruvector_attention_wasm_bg.js": import0 };
};
})(_mod, () => _wasm);
// ── Async WASM init (fetch-based for browsers) ───────────────────
export default async function initWasm() {
if (_initialized) return;
const wasmUrl = new URL('ruvector_attention_wasm_bg.wasm', import.meta.url);
const imports = _mod.__wbg_get_imports();
let result;
if (typeof WebAssembly.instantiateStreaming === 'function') {
try {
result = await WebAssembly.instantiateStreaming(fetch(wasmUrl), imports);
} catch (e) {
// Fallback if streaming fails (e.g. wrong MIME type)
const bytes = await (await fetch(wasmUrl)).arrayBuffer();
result = await WebAssembly.instantiate(bytes, imports);
}
} else {
const bytes = await (await fetch(wasmUrl)).arrayBuffer();
result = await WebAssembly.instantiate(bytes, imports);
}
_wasm = result.instance.exports;
_wasm.__wbindgen_start();
_initialized = true;
}
// ── ESM re-exports ────────────────────────────────────────────────
// Attention mechanism classes
export const WasmMultiHeadAttention = _mod.WasmMultiHeadAttention;
export const WasmFlashAttention = _mod.WasmFlashAttention;
export const WasmHyperbolicAttention = _mod.WasmHyperbolicAttention;
export const WasmMoEAttention = _mod.WasmMoEAttention;
export const WasmLinearAttention = _mod.WasmLinearAttention;
export const WasmLocalGlobalAttention = _mod.WasmLocalGlobalAttention;
// Utility functions
export const cosine_similarity = _mod.cosine_similarity;
export const normalize = _mod.normalize;
export const l2_norm = _mod.l2_norm;
export const softmax = _mod.softmax;
export const batch_normalize = _mod.batch_normalize;
export const pairwise_distances = _mod.pairwise_distances;
export const scaled_dot_attention = _mod.scaled_dot_attention;
export const attention_weights = _mod.attention_weights;
export const random_orthogonal_matrix = _mod.random_orthogonal_matrix;
export const available_mechanisms = _mod.available_mechanisms;
// Lifecycle
export const init = _mod.init;
export const version = _mod.version;
@@ -0,0 +1,359 @@
/* tslint:disable */
/* eslint-disable */
/**
* Adam optimizer
*/
export class WasmAdam {
free(): void;
[Symbol.dispose](): void;
/**
* Create a new Adam optimizer
*
* # Arguments
* * `param_count` - Number of parameters
* * `learning_rate` - Learning rate
*/
constructor(param_count: number, learning_rate: number);
/**
* Reset optimizer state
*/
reset(): void;
/**
* Perform optimization step
*
* # Arguments
* * `params` - Current parameter values (will be updated in-place)
* * `gradients` - Gradient values
*/
step(params: Float32Array, gradients: Float32Array): void;
/**
* Get current learning rate
*/
learning_rate: number;
}
/**
* AdamW optimizer (Adam with decoupled weight decay)
*/
export class WasmAdamW {
free(): void;
[Symbol.dispose](): void;
/**
* Create a new AdamW optimizer
*
* # Arguments
* * `param_count` - Number of parameters
* * `learning_rate` - Learning rate
* * `weight_decay` - Weight decay coefficient
*/
constructor(param_count: number, learning_rate: number, weight_decay: number);
/**
* Reset optimizer state
*/
reset(): void;
/**
* Perform optimization step with weight decay
*/
step(params: Float32Array, gradients: Float32Array): void;
/**
* Get current learning rate
*/
learning_rate: number;
/**
* Get weight decay
*/
readonly weight_decay: number;
}
/**
* Flash attention mechanism
*/
export class WasmFlashAttention {
free(): void;
[Symbol.dispose](): void;
/**
* Compute flash attention
*/
compute(query: Float32Array, keys: any, values: any): Float32Array;
/**
* Create a new flash attention instance
*
* # Arguments
* * `dim` - Embedding dimension
* * `block_size` - Block size for tiling
*/
constructor(dim: number, block_size: number);
}
/**
* Hyperbolic attention mechanism
*/
export class WasmHyperbolicAttention {
free(): void;
[Symbol.dispose](): void;
/**
* Compute hyperbolic attention
*/
compute(query: Float32Array, keys: any, values: any): Float32Array;
/**
* Create a new hyperbolic attention instance
*
* # Arguments
* * `dim` - Embedding dimension
* * `curvature` - Hyperbolic curvature parameter
*/
constructor(dim: number, curvature: number);
/**
* Get the curvature
*/
readonly curvature: number;
}
/**
* InfoNCE contrastive loss for training
*/
export class WasmInfoNCELoss {
free(): void;
[Symbol.dispose](): void;
/**
* Compute InfoNCE loss
*
* # Arguments
* * `anchor` - Anchor embedding
* * `positive` - Positive example embedding
* * `negatives` - Array of negative example embeddings
*/
compute(anchor: Float32Array, positive: Float32Array, negatives: any): number;
/**
* Create a new InfoNCE loss instance
*
* # Arguments
* * `temperature` - Temperature parameter for softmax
*/
constructor(temperature: number);
}
/**
* Learning rate scheduler
*/
export class WasmLRScheduler {
free(): void;
[Symbol.dispose](): void;
/**
* Get learning rate for current step
*/
get_lr(): number;
/**
* Create a new learning rate scheduler with warmup and cosine decay
*
* # Arguments
* * `initial_lr` - Initial learning rate
* * `warmup_steps` - Number of warmup steps
* * `total_steps` - Total training steps
*/
constructor(initial_lr: number, warmup_steps: number, total_steps: number);
/**
* Reset scheduler
*/
reset(): void;
/**
* Advance to next step
*/
step(): void;
}
/**
* Linear attention (Performer-style)
*/
export class WasmLinearAttention {
free(): void;
[Symbol.dispose](): void;
/**
* Compute linear attention
*/
compute(query: Float32Array, keys: any, values: any): Float32Array;
/**
* Create a new linear attention instance
*
* # Arguments
* * `dim` - Embedding dimension
* * `num_features` - Number of random features
*/
constructor(dim: number, num_features: number);
}
/**
* Local-global attention mechanism
*/
export class WasmLocalGlobalAttention {
free(): void;
[Symbol.dispose](): void;
/**
* Compute local-global attention
*/
compute(query: Float32Array, keys: any, values: any): Float32Array;
/**
* Create a new local-global attention instance
*
* # Arguments
* * `dim` - Embedding dimension
* * `local_window` - Size of local attention window
* * `global_tokens` - Number of global attention tokens
*/
constructor(dim: number, local_window: number, global_tokens: number);
}
/**
* Mixture of Experts (MoE) attention
*/
export class WasmMoEAttention {
free(): void;
[Symbol.dispose](): void;
/**
* Compute MoE attention
*/
compute(query: Float32Array, keys: any, values: any): Float32Array;
/**
* Create a new MoE attention instance
*
* # Arguments
* * `dim` - Embedding dimension
* * `num_experts` - Number of expert attention mechanisms
* * `top_k` - Number of experts to use per query
*/
constructor(dim: number, num_experts: number, top_k: number);
}
/**
* Multi-head attention mechanism
*/
export class WasmMultiHeadAttention {
free(): void;
[Symbol.dispose](): void;
/**
* Compute multi-head attention
*/
compute(query: Float32Array, keys: any, values: any): Float32Array;
/**
* Create a new multi-head attention instance
*
* # Arguments
* * `dim` - Embedding dimension
* * `num_heads` - Number of attention heads
*/
constructor(dim: number, num_heads: number);
/**
* Get the dimension
*/
readonly dim: number;
/**
* Get the number of heads
*/
readonly num_heads: number;
}
/**
* SGD optimizer with momentum
*/
export class WasmSGD {
free(): void;
[Symbol.dispose](): void;
/**
* Create a new SGD optimizer
*
* # Arguments
* * `param_count` - Number of parameters
* * `learning_rate` - Learning rate
* * `momentum` - Momentum coefficient (default: 0)
*/
constructor(param_count: number, learning_rate: number, momentum?: number | null);
/**
* Reset optimizer state
*/
reset(): void;
/**
* Perform optimization step
*/
step(params: Float32Array, gradients: Float32Array): void;
/**
* Get current learning rate
*/
learning_rate: number;
}
/**
* Compute attention weights from scores
*/
export function attention_weights(scores: Float32Array, temperature?: number | null): void;
/**
* Get information about available attention mechanisms
*/
export function available_mechanisms(): any;
/**
* Batch normalize vectors
*/
export function batch_normalize(vectors: any, epsilon?: number | null): Float32Array;
/**
* Compute cosine similarity between two vectors
*/
export function cosine_similarity(a: Float32Array, b: Float32Array): number;
/**
* Initialize the WASM module with panic hook
*/
export function init(): void;
/**
* Compute L2 norm of a vector
*/
export function l2_norm(vec: Float32Array): number;
/**
* Log a message to the browser console
*/
export function log(message: string): void;
/**
* Log an error to the browser console
*/
export function log_error(message: string): void;
/**
* Normalize a vector to unit length
*/
export function normalize(vec: Float32Array): void;
/**
* Compute pairwise distances between vectors
*/
export function pairwise_distances(vectors: any): Float32Array;
/**
* Generate random orthogonal matrix (for initialization)
*/
export function random_orthogonal_matrix(dim: number): Float32Array;
/**
* Compute scaled dot-product attention
*
* # Arguments
* * `query` - Query vector as Float32Array
* * `keys` - Array of key vectors
* * `values` - Array of value vectors
* * `scale` - Optional scaling factor (defaults to 1/sqrt(dim))
*/
export function scaled_dot_attention(query: Float32Array, keys: any, values: any, scale?: number | null): Float32Array;
/**
* Compute softmax of a vector
*/
export function softmax(vec: Float32Array): void;
/**
* Get the version of the ruvector-attention-wasm crate
*/
export function version(): string;
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,71 @@
/* tslint:disable */
/* eslint-disable */
export const memory: WebAssembly.Memory;
export const __wbg_wasmadam_free: (a: number, b: number) => void;
export const __wbg_wasmadamw_free: (a: number, b: number) => void;
export const __wbg_wasmflashattention_free: (a: number, b: number) => void;
export const __wbg_wasmhyperbolicattention_free: (a: number, b: number) => void;
export const __wbg_wasminfonceloss_free: (a: number, b: number) => void;
export const __wbg_wasmlinearattention_free: (a: number, b: number) => void;
export const __wbg_wasmmoeattention_free: (a: number, b: number) => void;
export const __wbg_wasmmultiheadattention_free: (a: number, b: number) => void;
export const __wbg_wasmsgd_free: (a: number, b: number) => void;
export const attention_weights: (a: number, b: number, c: number, d: number) => void;
export const available_mechanisms: () => number;
export const batch_normalize: (a: number, b: number, c: number) => void;
export const cosine_similarity: (a: number, b: number, c: number, d: number, e: number) => void;
export const l2_norm: (a: number, b: number) => number;
export const log: (a: number, b: number) => void;
export const log_error: (a: number, b: number) => void;
export const normalize: (a: number, b: number, c: number, d: number) => void;
export const pairwise_distances: (a: number, b: number) => void;
export const random_orthogonal_matrix: (a: number, b: number) => void;
export const scaled_dot_attention: (a: number, b: number, c: number, d: number, e: number, f: number) => void;
export const softmax: (a: number, b: number, c: number) => void;
export const version: (a: number) => void;
export const wasmadam_learning_rate: (a: number) => number;
export const wasmadam_new: (a: number, b: number) => number;
export const wasmadam_reset: (a: number) => void;
export const wasmadam_set_learning_rate: (a: number, b: number) => void;
export const wasmadam_step: (a: number, b: number, c: number, d: number, e: number, f: number) => void;
export const wasmadamw_new: (a: number, b: number, c: number) => number;
export const wasmadamw_reset: (a: number) => void;
export const wasmadamw_step: (a: number, b: number, c: number, d: number, e: number, f: number) => void;
export const wasmadamw_weight_decay: (a: number) => number;
export const wasmflashattention_compute: (a: number, b: number, c: number, d: number, e: number, f: number) => void;
export const wasmflashattention_new: (a: number, b: number) => number;
export const wasmhyperbolicattention_compute: (a: number, b: number, c: number, d: number, e: number, f: number) => void;
export const wasmhyperbolicattention_curvature: (a: number) => number;
export const wasmhyperbolicattention_new: (a: number, b: number) => number;
export const wasminfonceloss_compute: (a: number, b: number, c: number, d: number, e: number, f: number, g: number) => void;
export const wasminfonceloss_new: (a: number) => number;
export const wasmlinearattention_compute: (a: number, b: number, c: number, d: number, e: number, f: number) => void;
export const wasmlinearattention_new: (a: number, b: number) => number;
export const wasmlocalglobalattention_compute: (a: number, b: number, c: number, d: number, e: number, f: number) => void;
export const wasmlocalglobalattention_new: (a: number, b: number, c: number) => number;
export const wasmlrscheduler_get_lr: (a: number) => number;
export const wasmlrscheduler_new: (a: number, b: number, c: number) => number;
export const wasmlrscheduler_reset: (a: number) => void;
export const wasmlrscheduler_step: (a: number) => void;
export const wasmmoeattention_compute: (a: number, b: number, c: number, d: number, e: number, f: number) => void;
export const wasmmoeattention_new: (a: number, b: number, c: number) => number;
export const wasmmultiheadattention_compute: (a: number, b: number, c: number, d: number, e: number, f: number) => void;
export const wasmmultiheadattention_dim: (a: number) => number;
export const wasmmultiheadattention_new: (a: number, b: number, c: number) => void;
export const wasmmultiheadattention_num_heads: (a: number) => number;
export const wasmsgd_learning_rate: (a: number) => number;
export const wasmsgd_new: (a: number, b: number, c: number) => number;
export const wasmsgd_reset: (a: number) => void;
export const wasmsgd_set_learning_rate: (a: number, b: number) => void;
export const wasmsgd_step: (a: number, b: number, c: number, d: number, e: number, f: number) => void;
export const init: () => void;
export const wasmadamw_set_learning_rate: (a: number, b: number) => void;
export const wasmadamw_learning_rate: (a: number) => number;
export const __wbg_wasmlocalglobalattention_free: (a: number, b: number) => void;
export const __wbg_wasmlrscheduler_free: (a: number, b: number) => void;
export const __wbindgen_export: (a: number, b: number) => number;
export const __wbindgen_export2: (a: number, b: number, c: number, d: number) => number;
export const __wbindgen_export3: (a: number) => void;
export const __wbindgen_export4: (a: number, b: number, c: number) => void;
export const __wbindgen_add_to_stack_pointer: (a: number) => number;
export const __wbindgen_start: () => void;
@@ -0,0 +1,26 @@
{
"name": "ruvector-cnn-wasm",
"type": "module",
"description": "WASM bindings for ruvector-cnn - CNN feature extraction for image embeddings",
"version": "0.1.0",
"license": "MIT OR Apache-2.0",
"repository": {
"type": "git",
"url": "https://github.com/ruvnet/ruvector"
},
"files": [
"ruvector_cnn_wasm_bg.wasm",
"ruvector_cnn_wasm.js"
],
"main": "ruvector_cnn_wasm.js",
"sideEffects": [
"./snippets/*"
],
"keywords": [
"cnn",
"embeddings",
"wasm",
"simd",
"machine-learning"
]
}
@@ -0,0 +1,802 @@
/**
* Configuration for CNN embedder
*/
export class EmbedderConfig {
__destroy_into_raw() {
const ptr = this.__wbg_ptr;
this.__wbg_ptr = 0;
EmbedderConfigFinalization.unregister(this);
return ptr;
}
free() {
const ptr = this.__destroy_into_raw();
wasm.__wbg_embedderconfig_free(ptr, 0);
}
constructor() {
const ret = wasm.embedderconfig_new();
this.__wbg_ptr = ret >>> 0;
EmbedderConfigFinalization.register(this, this.__wbg_ptr, this);
return this;
}
/**
* Output embedding dimension
* @returns {number}
*/
get embedding_dim() {
const ret = wasm.__wbg_get_embedderconfig_embedding_dim(this.__wbg_ptr);
return ret >>> 0;
}
/**
* Input image size (square)
* @returns {number}
*/
get input_size() {
const ret = wasm.__wbg_get_embedderconfig_input_size(this.__wbg_ptr);
return ret >>> 0;
}
/**
* Whether to L2 normalize embeddings
* @returns {boolean}
*/
get normalize() {
const ret = wasm.__wbg_get_embedderconfig_normalize(this.__wbg_ptr);
return ret !== 0;
}
/**
* Output embedding dimension
* @param {number} arg0
*/
set embedding_dim(arg0) {
wasm.__wbg_set_embedderconfig_embedding_dim(this.__wbg_ptr, arg0);
}
/**
* Input image size (square)
* @param {number} arg0
*/
set input_size(arg0) {
wasm.__wbg_set_embedderconfig_input_size(this.__wbg_ptr, arg0);
}
/**
* Whether to L2 normalize embeddings
* @param {boolean} arg0
*/
set normalize(arg0) {
wasm.__wbg_set_embedderconfig_normalize(this.__wbg_ptr, arg0);
}
}
if (Symbol.dispose) EmbedderConfig.prototype[Symbol.dispose] = EmbedderConfig.prototype.free;
/**
* Layer operations for building custom networks
*/
export class LayerOps {
__destroy_into_raw() {
const ptr = this.__wbg_ptr;
this.__wbg_ptr = 0;
LayerOpsFinalization.unregister(this);
return ptr;
}
free() {
const ptr = this.__destroy_into_raw();
wasm.__wbg_layerops_free(ptr, 0);
}
/**
* Apply batch normalization (returns new array)
* @param {Float32Array} input
* @param {Float32Array} gamma
* @param {Float32Array} beta
* @param {Float32Array} mean
* @param {Float32Array} _var
* @param {number} epsilon
* @returns {Float32Array}
*/
static batch_norm(input, gamma, beta, mean, _var, epsilon) {
try {
const retptr = wasm.__wbindgen_add_to_stack_pointer(-16);
const ptr0 = passArrayF32ToWasm0(input, wasm.__wbindgen_export2);
const len0 = WASM_VECTOR_LEN;
const ptr1 = passArrayF32ToWasm0(gamma, wasm.__wbindgen_export2);
const len1 = WASM_VECTOR_LEN;
const ptr2 = passArrayF32ToWasm0(beta, wasm.__wbindgen_export2);
const len2 = WASM_VECTOR_LEN;
const ptr3 = passArrayF32ToWasm0(mean, wasm.__wbindgen_export2);
const len3 = WASM_VECTOR_LEN;
const ptr4 = passArrayF32ToWasm0(_var, wasm.__wbindgen_export2);
const len4 = WASM_VECTOR_LEN;
wasm.layerops_batch_norm(retptr, ptr0, len0, ptr1, len1, ptr2, len2, ptr3, len3, ptr4, len4, epsilon);
var r0 = getDataViewMemory0().getInt32(retptr + 4 * 0, true);
var r1 = getDataViewMemory0().getInt32(retptr + 4 * 1, true);
var v6 = getArrayF32FromWasm0(r0, r1).slice();
wasm.__wbindgen_export(r0, r1 * 4, 4);
return v6;
} finally {
wasm.__wbindgen_add_to_stack_pointer(16);
}
}
/**
* Apply global average pooling
* Returns one value per channel
* @param {Float32Array} input
* @param {number} height
* @param {number} width
* @param {number} channels
* @returns {Float32Array}
*/
static global_avg_pool(input, height, width, channels) {
try {
const retptr = wasm.__wbindgen_add_to_stack_pointer(-16);
const ptr0 = passArrayF32ToWasm0(input, wasm.__wbindgen_export2);
const len0 = WASM_VECTOR_LEN;
wasm.layerops_global_avg_pool(retptr, ptr0, len0, height, width, channels);
var r0 = getDataViewMemory0().getInt32(retptr + 4 * 0, true);
var r1 = getDataViewMemory0().getInt32(retptr + 4 * 1, true);
var v2 = getArrayF32FromWasm0(r0, r1).slice();
wasm.__wbindgen_export(r0, r1 * 4, 4);
return v2;
} finally {
wasm.__wbindgen_add_to_stack_pointer(16);
}
}
}
if (Symbol.dispose) LayerOps.prototype[Symbol.dispose] = LayerOps.prototype.free;
/**
* SIMD-optimized operations
*/
export class SimdOps {
__destroy_into_raw() {
const ptr = this.__wbg_ptr;
this.__wbg_ptr = 0;
SimdOpsFinalization.unregister(this);
return ptr;
}
free() {
const ptr = this.__destroy_into_raw();
wasm.__wbg_simdops_free(ptr, 0);
}
/**
* Dot product of two vectors
* @param {Float32Array} a
* @param {Float32Array} b
* @returns {number}
*/
static dot_product(a, b) {
const ptr0 = passArrayF32ToWasm0(a, wasm.__wbindgen_export2);
const len0 = WASM_VECTOR_LEN;
const ptr1 = passArrayF32ToWasm0(b, wasm.__wbindgen_export2);
const len1 = WASM_VECTOR_LEN;
const ret = wasm.simdops_dot_product(ptr0, len0, ptr1, len1);
return ret;
}
/**
* L2 normalize a vector (returns new array)
* @param {Float32Array} data
* @returns {Float32Array}
*/
static l2_normalize(data) {
try {
const retptr = wasm.__wbindgen_add_to_stack_pointer(-16);
const ptr0 = passArrayF32ToWasm0(data, wasm.__wbindgen_export2);
const len0 = WASM_VECTOR_LEN;
wasm.simdops_l2_normalize(retptr, ptr0, len0);
var r0 = getDataViewMemory0().getInt32(retptr + 4 * 0, true);
var r1 = getDataViewMemory0().getInt32(retptr + 4 * 1, true);
var v2 = getArrayF32FromWasm0(r0, r1).slice();
wasm.__wbindgen_export(r0, r1 * 4, 4);
return v2;
} finally {
wasm.__wbindgen_add_to_stack_pointer(16);
}
}
/**
* ReLU activation (returns new array)
* @param {Float32Array} data
* @returns {Float32Array}
*/
static relu(data) {
try {
const retptr = wasm.__wbindgen_add_to_stack_pointer(-16);
const ptr0 = passArrayF32ToWasm0(data, wasm.__wbindgen_export2);
const len0 = WASM_VECTOR_LEN;
wasm.simdops_relu(retptr, ptr0, len0);
var r0 = getDataViewMemory0().getInt32(retptr + 4 * 0, true);
var r1 = getDataViewMemory0().getInt32(retptr + 4 * 1, true);
var v2 = getArrayF32FromWasm0(r0, r1).slice();
wasm.__wbindgen_export(r0, r1 * 4, 4);
return v2;
} finally {
wasm.__wbindgen_add_to_stack_pointer(16);
}
}
/**
* ReLU6 activation (returns new array)
* @param {Float32Array} data
* @returns {Float32Array}
*/
static relu6(data) {
try {
const retptr = wasm.__wbindgen_add_to_stack_pointer(-16);
const ptr0 = passArrayF32ToWasm0(data, wasm.__wbindgen_export2);
const len0 = WASM_VECTOR_LEN;
wasm.simdops_relu6(retptr, ptr0, len0);
var r0 = getDataViewMemory0().getInt32(retptr + 4 * 0, true);
var r1 = getDataViewMemory0().getInt32(retptr + 4 * 1, true);
var v2 = getArrayF32FromWasm0(r0, r1).slice();
wasm.__wbindgen_export(r0, r1 * 4, 4);
return v2;
} finally {
wasm.__wbindgen_add_to_stack_pointer(16);
}
}
}
if (Symbol.dispose) SimdOps.prototype[Symbol.dispose] = SimdOps.prototype.free;
/**
* WASM CNN Embedder for image feature extraction
*/
export class WasmCnnEmbedder {
__destroy_into_raw() {
const ptr = this.__wbg_ptr;
this.__wbg_ptr = 0;
WasmCnnEmbedderFinalization.unregister(this);
return ptr;
}
free() {
const ptr = this.__destroy_into_raw();
wasm.__wbg_wasmcnnembedder_free(ptr, 0);
}
/**
* Compute cosine similarity between two embeddings
* @param {Float32Array} a
* @param {Float32Array} b
* @returns {number}
*/
cosine_similarity(a, b) {
try {
const retptr = wasm.__wbindgen_add_to_stack_pointer(-16);
const ptr0 = passArrayF32ToWasm0(a, wasm.__wbindgen_export2);
const len0 = WASM_VECTOR_LEN;
const ptr1 = passArrayF32ToWasm0(b, wasm.__wbindgen_export2);
const len1 = WASM_VECTOR_LEN;
wasm.wasmcnnembedder_cosine_similarity(retptr, this.__wbg_ptr, ptr0, len0, ptr1, len1);
var r0 = getDataViewMemory0().getFloat32(retptr + 4 * 0, true);
var r1 = getDataViewMemory0().getInt32(retptr + 4 * 1, true);
var r2 = getDataViewMemory0().getInt32(retptr + 4 * 2, true);
if (r2) {
throw takeObject(r1);
}
return r0;
} finally {
wasm.__wbindgen_add_to_stack_pointer(16);
}
}
/**
* Get the embedding dimension
* @returns {number}
*/
get embedding_dim() {
const ret = wasm.wasmcnnembedder_embedding_dim(this.__wbg_ptr);
return ret >>> 0;
}
/**
* Extract embedding from image data (RGB format, row-major)
* @param {Uint8Array} image_data
* @param {number} width
* @param {number} height
* @returns {Float32Array}
*/
extract(image_data, width, height) {
try {
const retptr = wasm.__wbindgen_add_to_stack_pointer(-16);
const ptr0 = passArray8ToWasm0(image_data, wasm.__wbindgen_export2);
const len0 = WASM_VECTOR_LEN;
wasm.wasmcnnembedder_extract(retptr, this.__wbg_ptr, ptr0, len0, width, height);
var r0 = getDataViewMemory0().getInt32(retptr + 4 * 0, true);
var r1 = getDataViewMemory0().getInt32(retptr + 4 * 1, true);
var r2 = getDataViewMemory0().getInt32(retptr + 4 * 2, true);
var r3 = getDataViewMemory0().getInt32(retptr + 4 * 3, true);
if (r3) {
throw takeObject(r2);
}
var v2 = getArrayF32FromWasm0(r0, r1).slice();
wasm.__wbindgen_export(r0, r1 * 4, 4);
return v2;
} finally {
wasm.__wbindgen_add_to_stack_pointer(16);
}
}
/**
* Create a new CNN embedder
* @param {EmbedderConfig | null} [config]
*/
constructor(config) {
try {
const retptr = wasm.__wbindgen_add_to_stack_pointer(-16);
let ptr0 = 0;
if (!isLikeNone(config)) {
_assertClass(config, EmbedderConfig);
ptr0 = config.__destroy_into_raw();
}
wasm.wasmcnnembedder_new(retptr, ptr0);
var r0 = getDataViewMemory0().getInt32(retptr + 4 * 0, true);
var r1 = getDataViewMemory0().getInt32(retptr + 4 * 1, true);
var r2 = getDataViewMemory0().getInt32(retptr + 4 * 2, true);
if (r2) {
throw takeObject(r1);
}
this.__wbg_ptr = r0 >>> 0;
WasmCnnEmbedderFinalization.register(this, this.__wbg_ptr, this);
return this;
} finally {
wasm.__wbindgen_add_to_stack_pointer(16);
}
}
}
if (Symbol.dispose) WasmCnnEmbedder.prototype[Symbol.dispose] = WasmCnnEmbedder.prototype.free;
/**
* InfoNCE loss for contrastive learning (SimCLR style)
*/
export class WasmInfoNCELoss {
__destroy_into_raw() {
const ptr = this.__wbg_ptr;
this.__wbg_ptr = 0;
WasmInfoNCELossFinalization.unregister(this);
return ptr;
}
free() {
const ptr = this.__destroy_into_raw();
wasm.__wbg_wasminfonceloss_free(ptr, 0);
}
/**
* Compute loss for a batch of embedding pairs
* embeddings: [2N, D] flattened where (i, i+N) are positive pairs
* @param {Float32Array} embeddings
* @param {number} batch_size
* @param {number} dim
* @returns {number}
*/
forward(embeddings, batch_size, dim) {
try {
const retptr = wasm.__wbindgen_add_to_stack_pointer(-16);
const ptr0 = passArrayF32ToWasm0(embeddings, wasm.__wbindgen_export2);
const len0 = WASM_VECTOR_LEN;
wasm.wasminfonceloss_forward(retptr, this.__wbg_ptr, ptr0, len0, batch_size, dim);
var r0 = getDataViewMemory0().getFloat32(retptr + 4 * 0, true);
var r1 = getDataViewMemory0().getInt32(retptr + 4 * 1, true);
var r2 = getDataViewMemory0().getInt32(retptr + 4 * 2, true);
if (r2) {
throw takeObject(r1);
}
return r0;
} finally {
wasm.__wbindgen_add_to_stack_pointer(16);
}
}
/**
* Create new InfoNCE loss with temperature parameter
* @param {number} temperature
*/
constructor(temperature) {
const ret = wasm.wasminfonceloss_new(temperature);
this.__wbg_ptr = ret >>> 0;
WasmInfoNCELossFinalization.register(this, this.__wbg_ptr, this);
return this;
}
/**
* Get the temperature parameter
* @returns {number}
*/
get temperature() {
const ret = wasm.wasminfonceloss_temperature(this.__wbg_ptr);
return ret;
}
}
if (Symbol.dispose) WasmInfoNCELoss.prototype[Symbol.dispose] = WasmInfoNCELoss.prototype.free;
/**
* Triplet loss for metric learning
*/
export class WasmTripletLoss {
__destroy_into_raw() {
const ptr = this.__wbg_ptr;
this.__wbg_ptr = 0;
WasmTripletLossFinalization.unregister(this);
return ptr;
}
free() {
const ptr = this.__destroy_into_raw();
wasm.__wbg_wasmtripletloss_free(ptr, 0);
}
/**
* Compute loss for a batch of triplets
* @param {Float32Array} anchors
* @param {Float32Array} positives
* @param {Float32Array} negatives
* @param {number} dim
* @returns {number}
*/
forward(anchors, positives, negatives, dim) {
try {
const retptr = wasm.__wbindgen_add_to_stack_pointer(-16);
const ptr0 = passArrayF32ToWasm0(anchors, wasm.__wbindgen_export2);
const len0 = WASM_VECTOR_LEN;
const ptr1 = passArrayF32ToWasm0(positives, wasm.__wbindgen_export2);
const len1 = WASM_VECTOR_LEN;
const ptr2 = passArrayF32ToWasm0(negatives, wasm.__wbindgen_export2);
const len2 = WASM_VECTOR_LEN;
wasm.wasmtripletloss_forward(retptr, this.__wbg_ptr, ptr0, len0, ptr1, len1, ptr2, len2, dim);
var r0 = getDataViewMemory0().getFloat32(retptr + 4 * 0, true);
var r1 = getDataViewMemory0().getInt32(retptr + 4 * 1, true);
var r2 = getDataViewMemory0().getInt32(retptr + 4 * 2, true);
if (r2) {
throw takeObject(r1);
}
return r0;
} finally {
wasm.__wbindgen_add_to_stack_pointer(16);
}
}
/**
* Compute loss for a single triplet
* @param {Float32Array} anchor
* @param {Float32Array} positive
* @param {Float32Array} negative
* @returns {number}
*/
forward_single(anchor, positive, negative) {
try {
const retptr = wasm.__wbindgen_add_to_stack_pointer(-16);
const ptr0 = passArrayF32ToWasm0(anchor, wasm.__wbindgen_export2);
const len0 = WASM_VECTOR_LEN;
const ptr1 = passArrayF32ToWasm0(positive, wasm.__wbindgen_export2);
const len1 = WASM_VECTOR_LEN;
const ptr2 = passArrayF32ToWasm0(negative, wasm.__wbindgen_export2);
const len2 = WASM_VECTOR_LEN;
wasm.wasmtripletloss_forward_single(retptr, this.__wbg_ptr, ptr0, len0, ptr1, len1, ptr2, len2);
var r0 = getDataViewMemory0().getFloat32(retptr + 4 * 0, true);
var r1 = getDataViewMemory0().getInt32(retptr + 4 * 1, true);
var r2 = getDataViewMemory0().getInt32(retptr + 4 * 2, true);
if (r2) {
throw takeObject(r1);
}
return r0;
} finally {
wasm.__wbindgen_add_to_stack_pointer(16);
}
}
/**
* Get the margin parameter
* @returns {number}
*/
get margin() {
const ret = wasm.wasmtripletloss_margin(this.__wbg_ptr);
return ret;
}
/**
* Create new triplet loss with margin
* @param {number} margin
*/
constructor(margin) {
const ret = wasm.wasmtripletloss_new(margin);
this.__wbg_ptr = ret >>> 0;
WasmTripletLossFinalization.register(this, this.__wbg_ptr, this);
return this;
}
}
if (Symbol.dispose) WasmTripletLoss.prototype[Symbol.dispose] = WasmTripletLoss.prototype.free;
/**
* Initialize panic hook for better error messages
*/
export function init() {
wasm.init();
}
function __wbg_get_imports() {
const import0 = {
__proto__: null,
__wbg___wbindgen_throw_39bc967c0e5a9b58: function(arg0, arg1) {
throw new Error(getStringFromWasm0(arg0, arg1));
},
__wbg_error_a6fa202b58aa1cd3: function(arg0, arg1) {
let deferred0_0;
let deferred0_1;
try {
deferred0_0 = arg0;
deferred0_1 = arg1;
console.error(getStringFromWasm0(arg0, arg1));
} finally {
wasm.__wbindgen_export(deferred0_0, deferred0_1, 1);
}
},
__wbg_new_227d7c05414eb861: function() {
const ret = new Error();
return addHeapObject(ret);
},
__wbg_stack_3b0d974bbf31e44f: function(arg0, arg1) {
const ret = getObject(arg1).stack;
const ptr1 = passStringToWasm0(ret, wasm.__wbindgen_export2, wasm.__wbindgen_export3);
const len1 = WASM_VECTOR_LEN;
getDataViewMemory0().setInt32(arg0 + 4 * 1, len1, true);
getDataViewMemory0().setInt32(arg0 + 4 * 0, ptr1, true);
},
__wbindgen_cast_0000000000000001: function(arg0, arg1) {
// Cast intrinsic for `Ref(String) -> Externref`.
const ret = getStringFromWasm0(arg0, arg1);
return addHeapObject(ret);
},
__wbindgen_object_drop_ref: function(arg0) {
takeObject(arg0);
},
};
return {
__proto__: null,
"./ruvector_cnn_wasm_bg.js": import0,
};
}
const EmbedderConfigFinalization = (typeof FinalizationRegistry === 'undefined')
? { register: () => {}, unregister: () => {} }
: new FinalizationRegistry(ptr => wasm.__wbg_embedderconfig_free(ptr >>> 0, 1));
const LayerOpsFinalization = (typeof FinalizationRegistry === 'undefined')
? { register: () => {}, unregister: () => {} }
: new FinalizationRegistry(ptr => wasm.__wbg_layerops_free(ptr >>> 0, 1));
const SimdOpsFinalization = (typeof FinalizationRegistry === 'undefined')
? { register: () => {}, unregister: () => {} }
: new FinalizationRegistry(ptr => wasm.__wbg_simdops_free(ptr >>> 0, 1));
const WasmCnnEmbedderFinalization = (typeof FinalizationRegistry === 'undefined')
? { register: () => {}, unregister: () => {} }
: new FinalizationRegistry(ptr => wasm.__wbg_wasmcnnembedder_free(ptr >>> 0, 1));
const WasmInfoNCELossFinalization = (typeof FinalizationRegistry === 'undefined')
? { register: () => {}, unregister: () => {} }
: new FinalizationRegistry(ptr => wasm.__wbg_wasminfonceloss_free(ptr >>> 0, 1));
const WasmTripletLossFinalization = (typeof FinalizationRegistry === 'undefined')
? { register: () => {}, unregister: () => {} }
: new FinalizationRegistry(ptr => wasm.__wbg_wasmtripletloss_free(ptr >>> 0, 1));
function addHeapObject(obj) {
if (heap_next === heap.length) heap.push(heap.length + 1);
const idx = heap_next;
heap_next = heap[idx];
heap[idx] = obj;
return idx;
}
function _assertClass(instance, klass) {
if (!(instance instanceof klass)) {
throw new Error(`expected instance of ${klass.name}`);
}
}
function dropObject(idx) {
if (idx < 1028) return;
heap[idx] = heap_next;
heap_next = idx;
}
function getArrayF32FromWasm0(ptr, len) {
ptr = ptr >>> 0;
return getFloat32ArrayMemory0().subarray(ptr / 4, ptr / 4 + len);
}
let cachedDataViewMemory0 = null;
function getDataViewMemory0() {
if (cachedDataViewMemory0 === null || cachedDataViewMemory0.buffer.detached === true || (cachedDataViewMemory0.buffer.detached === undefined && cachedDataViewMemory0.buffer !== wasm.memory.buffer)) {
cachedDataViewMemory0 = new DataView(wasm.memory.buffer);
}
return cachedDataViewMemory0;
}
let cachedFloat32ArrayMemory0 = null;
function getFloat32ArrayMemory0() {
if (cachedFloat32ArrayMemory0 === null || cachedFloat32ArrayMemory0.byteLength === 0) {
cachedFloat32ArrayMemory0 = new Float32Array(wasm.memory.buffer);
}
return cachedFloat32ArrayMemory0;
}
function getStringFromWasm0(ptr, len) {
ptr = ptr >>> 0;
return decodeText(ptr, len);
}
let cachedUint8ArrayMemory0 = null;
function getUint8ArrayMemory0() {
if (cachedUint8ArrayMemory0 === null || cachedUint8ArrayMemory0.byteLength === 0) {
cachedUint8ArrayMemory0 = new Uint8Array(wasm.memory.buffer);
}
return cachedUint8ArrayMemory0;
}
function getObject(idx) { return heap[idx]; }
let heap = new Array(1024).fill(undefined);
heap.push(undefined, null, true, false);
let heap_next = heap.length;
function isLikeNone(x) {
return x === undefined || x === null;
}
function passArray8ToWasm0(arg, malloc) {
const ptr = malloc(arg.length * 1, 1) >>> 0;
getUint8ArrayMemory0().set(arg, ptr / 1);
WASM_VECTOR_LEN = arg.length;
return ptr;
}
function passArrayF32ToWasm0(arg, malloc) {
const ptr = malloc(arg.length * 4, 4) >>> 0;
getFloat32ArrayMemory0().set(arg, ptr / 4);
WASM_VECTOR_LEN = arg.length;
return ptr;
}
function passStringToWasm0(arg, malloc, realloc) {
if (realloc === undefined) {
const buf = cachedTextEncoder.encode(arg);
const ptr = malloc(buf.length, 1) >>> 0;
getUint8ArrayMemory0().subarray(ptr, ptr + buf.length).set(buf);
WASM_VECTOR_LEN = buf.length;
return ptr;
}
let len = arg.length;
let ptr = malloc(len, 1) >>> 0;
const mem = getUint8ArrayMemory0();
let offset = 0;
for (; offset < len; offset++) {
const code = arg.charCodeAt(offset);
if (code > 0x7F) break;
mem[ptr + offset] = code;
}
if (offset !== len) {
if (offset !== 0) {
arg = arg.slice(offset);
}
ptr = realloc(ptr, len, len = offset + arg.length * 3, 1) >>> 0;
const view = getUint8ArrayMemory0().subarray(ptr + offset, ptr + len);
const ret = cachedTextEncoder.encodeInto(arg, view);
offset += ret.written;
ptr = realloc(ptr, len, offset, 1) >>> 0;
}
WASM_VECTOR_LEN = offset;
return ptr;
}
function takeObject(idx) {
const ret = getObject(idx);
dropObject(idx);
return ret;
}
let cachedTextDecoder = new TextDecoder('utf-8', { ignoreBOM: true, fatal: true });
cachedTextDecoder.decode();
const MAX_SAFARI_DECODE_BYTES = 2146435072;
let numBytesDecoded = 0;
function decodeText(ptr, len) {
numBytesDecoded += len;
if (numBytesDecoded >= MAX_SAFARI_DECODE_BYTES) {
cachedTextDecoder = new TextDecoder('utf-8', { ignoreBOM: true, fatal: true });
cachedTextDecoder.decode();
numBytesDecoded = len;
}
return cachedTextDecoder.decode(getUint8ArrayMemory0().subarray(ptr, ptr + len));
}
const cachedTextEncoder = new TextEncoder();
if (!('encodeInto' in cachedTextEncoder)) {
cachedTextEncoder.encodeInto = function (arg, view) {
const buf = cachedTextEncoder.encode(arg);
view.set(buf);
return {
read: arg.length,
written: buf.length
};
};
}
let WASM_VECTOR_LEN = 0;
let wasmModule, wasm;
function __wbg_finalize_init(instance, module) {
wasm = instance.exports;
wasmModule = module;
cachedDataViewMemory0 = null;
cachedFloat32ArrayMemory0 = null;
cachedUint8ArrayMemory0 = null;
wasm.__wbindgen_start();
return wasm;
}
async function __wbg_load(module, imports) {
if (typeof Response === 'function' && module instanceof Response) {
if (typeof WebAssembly.instantiateStreaming === 'function') {
try {
return await WebAssembly.instantiateStreaming(module, imports);
} catch (e) {
const validResponse = module.ok && expectedResponseType(module.type);
if (validResponse && module.headers.get('Content-Type') !== 'application/wasm') {
console.warn("`WebAssembly.instantiateStreaming` failed because your server does not serve Wasm with `application/wasm` MIME type. Falling back to `WebAssembly.instantiate` which is slower. Original error:\n", e);
} else { throw e; }
}
}
const bytes = await module.arrayBuffer();
return await WebAssembly.instantiate(bytes, imports);
} else {
const instance = await WebAssembly.instantiate(module, imports);
if (instance instanceof WebAssembly.Instance) {
return { instance, module };
} else {
return instance;
}
}
function expectedResponseType(type) {
switch (type) {
case 'basic': case 'cors': case 'default': return true;
}
return false;
}
}
function initSync(module) {
if (wasm !== undefined) return wasm;
if (module !== undefined) {
if (Object.getPrototypeOf(module) === Object.prototype) {
({module} = module)
} else {
console.warn('using deprecated parameters for `initSync()`; pass a single object instead')
}
}
const imports = __wbg_get_imports();
if (!(module instanceof WebAssembly.Module)) {
module = new WebAssembly.Module(module);
}
const instance = new WebAssembly.Instance(module, imports);
return __wbg_finalize_init(instance, module);
}
async function __wbg_init(module_or_path) {
if (wasm !== undefined) return wasm;
if (module_or_path !== undefined) {
if (Object.getPrototypeOf(module_or_path) === Object.prototype) {
({module_or_path} = module_or_path)
} else {
console.warn('using deprecated parameters for the initialization function; pass a single object instead')
}
}
if (module_or_path === undefined) {
module_or_path = new URL('ruvector_cnn_wasm_bg.wasm', import.meta.url);
}
const imports = __wbg_get_imports();
if (typeof module_or_path === 'string' || (typeof Request === 'function' && module_or_path instanceof Request) || (typeof URL === 'function' && module_or_path instanceof URL)) {
module_or_path = fetch(module_or_path);
}
const { instance, module } = await __wbg_load(await module_or_path, imports);
return __wbg_finalize_init(instance, module);
}
export { initSync, __wbg_init as default };