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>
This commit is contained in:
rUv 2026-03-12 20:59:57 -04:00 committed by GitHub
parent 6e03a47867
commit 7c1351fd5d
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
27 changed files with 7428 additions and 1 deletions

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 |
@ -89,8 +89,12 @@ docker run -p 3000:3000 ruvnet/wifi-densepose:latest
<em>Real-time pose skeleton from WiFi CSI signals — no cameras, no wearables</em>
<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

View file

@ -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

View file

@ -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

View file

@ -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 ==="
}

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()

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
ui/pose-fusion.html Normal file
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>WiFi-DensePose — Dual-Modal Pose Estimation</title>
<link rel="stylesheet" href="pose-fusion/css/style.css?v=12">
</head>
<body>
<!-- Header -->
<header class="header">
<div class="header-left">
<div class="logo"><span class="pi">&pi;</span> DensePose</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>
WiFi-DensePose &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/wifi-densepose">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=12"></script>
</body>
</html>

30
ui/pose-fusion/build.sh Normal file
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"

View file

@ -0,0 +1,535 @@
/* WiFi-DensePose 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;
}
.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; }
}

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)})`;
}
}
}

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);
}
}

View file

@ -0,0 +1,357 @@
/**
* 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 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;
};
}
}

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
ui/pose-fusion/js/main.js Normal file
View file

@ -0,0 +1,472 @@
/**
* WiFi-DensePose Dual-Modal Pose Estimation Demo
*
* Main orchestration: video capture CNN embedding CSI processing fusion rendering
*/
import { VideoCapture } from './video-capture.js?v=11';
import { CsiSimulator } from './csi-simulator.js?v=11';
import { CnnEmbedder } from './cnn-embedder.js?v=11';
import { FusionEngine } from './fusion-engine.js?v=11';
import { PoseDecoder } from './pose-decoder.js?v=11';
import { CanvasRenderer } from './canvas-renderer.js?v=11';
// === 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.toFixed(2);
if (rvRefineEl) rvRefineEl.textContent = (rvStats.refinementMag * 1000).toFixed(1) + 'px';
if (rvImpactEl) {
const impact = Math.min(100, rvStats.refinementMag * 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}, rssi=${csiSimulator.rssiDbm.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);

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;
}
}

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;
}
}

View file

@ -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.

View file

@ -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

View file

@ -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"
]
}

View file

@ -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;

View file

@ -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

View file

@ -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;

View file

@ -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"
]
}

View file

@ -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 };