Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
39 changes: 39 additions & 0 deletions .github/workflows/test.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
---
name: test

"on":
pull_request:
workflow_dispatch:

concurrency:
group: "${{ github.workflow }}-${{ github.ref }}"
cancel-in-progress: ${{ startsWith(github.ref, 'refs/pull/') }}

jobs:
rust:
runs-on: ubuntu-latest
timeout-minutes: 25
steps:
- name: Checkout
uses: actions/checkout@v4

- name: Install mise
uses: taiki-e/install-action@v2
with:
tool: mise

- name: Install mise tools
run: |
mise settings add idiomatic_version_file_enable_tools "[]"
mise settings experimental=true
mise settings set cargo.binstall true
mise use -g cargo-binstall
mise install
env:
GITHUB_TOKEN: ${{ github.token }}

- name: Build WASM modules
run: mise run build-modules

- name: Run tests
run: mise run test
18 changes: 10 additions & 8 deletions .mise.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
[tools]
action-validator = "latest"
cargo-binstall = "latest"
"cargo:aube" = "latest"
"cargo:taplo-cli" = "latest"
"cargo:wasm-pack" = "latest"
"chromedriver" = "146"
Expand All @@ -15,14 +16,18 @@ editorconfig-checker = "latest"
gemini-cli = "latest"
"github:block/goose" = "latest"
"github:wasm-bindgen/wasm-bindgen" = "0.2.114"
"npm:onnxruntime-web" = "latest"
ollama = "latest"
osv-scanner = "latest"
pipx = "latest"
"pipx:cmake" = "latest"
protoc = "latest"
rclone = "latest"
ruff = "latest"
rust = { version = "latest", components = "clippy" }
rust = [
{ version = "latest", components = "clippy", targets = "wasm32-unknown-unknown" },
{ version = "nightly", components = "rust-src,rustfmt", targets = "wasm32-unknown-unknown" },
]
typos = "latest"
uv = "latest"
yq = "latest"
Expand All @@ -33,6 +38,7 @@ ec = "ec"
editorconfig-check = "ec"
ruff-check = "ruff check services/ws-modules/"
ruff-fmt = "ruff format services/ws-modules/"
test = "cargo test --workspace"
zig-check = "zig fmt --check services/ws-modules/"
zig-fmt = "zig fmt services/ws-modules/"

Expand All @@ -57,9 +63,6 @@ run = "dprint fmt"
depends = ["cargo-clippy-fix", "cargo-fmt", "dart-fmt", "dotnet-fmt", "dprint-fmt", "taplo-fmt", "zig-fmt"]
description = "Run repository formatters"

[tasks.install-nightly]
run = "cargo +nightly fmt --version >/dev/null 2>&1 || rustup toolchain install nightly --component rustfmt"

[tasks.dart-check]
run = "dart analyze services/ws-modules/dart-comm1/"

Expand All @@ -75,10 +78,10 @@ depends = [
"dotnet-check",
"dprint-check",
"editorconfig-check",
"generated-scenarios-check",
"osv-scanner",
"taplo-check",
"typos",
"verification-check",
"zig-check",
]
description = "Run repository checks"
Expand All @@ -87,11 +90,9 @@ description = "Run repository checks"
run = "cargo check --workspace"

[tasks.cargo-fmt]
depends = ["install-nightly"]
run = "cargo +nightly fmt"

[tasks.cargo-fmt-check]
depends = ["install-nightly"]
run = "cargo +nightly fmt -- --check"

[tasks.cargo-clippy]
Expand All @@ -114,10 +115,11 @@ depends = ["cargo-check"]
run = "osv-scanner --lockfile Cargo.lock"

[tasks.regenerate-verification]
alias = "regen-verification"
description = "Regenerate checked-in verification output files"
run = "cargo run -p et-cli -- regen-verification"

[tasks.generated-scenarios-check]
[tasks.verification-check]
depends = ["regenerate-verification"]
description = "Fail if generated scenario output files are stale"
run = "git diff --exit-code -- verification"
Expand Down
40 changes: 27 additions & 13 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

## mise

Please install [`mise`](https://mise.jdx.dev/).
Please install [`mise`](https://mise.jdx.dev/), including the shell integration.
It is needed for all use of this repository.

Configure it with
Expand All @@ -16,14 +16,6 @@ mise settings set cargo.binstall true

Use `mise run fmt` and `mise run check` to run formatters and checkers.

## Run e2e

Run the end-to-end tests using Chrome:

```bash
mise run ws-e2e-chrome
```

## Run ws agent in browser

### Build modules and run the WS server
Expand Down Expand Up @@ -60,14 +52,36 @@ The server logs appear in the Logs section.

The module list is dynamically populated from the modules in [services/ws-modules](services/ws-modules).

Each module must be a directory `pkg` containing a `package.json` that defines a `main` which contains a JavaScript file
Each module must have a `package.json` that defines a `main` which contains a JavaScript file
that can load and run the module.

Under each module in `ws-modules`, the package can be found in a subdirectory `pkg`.

Most of the module are built from Rust using `wasm-pack build --target web`.

The module `pydata1` uses [pyodide](https://pyodide.org/) to run a Python script.
There are also modules written in:

- Python, using [pyodide](https://pyodide.org/)
- .Net C#
- Dart
- Zig, including C code.

## Root module

The default UX in the web-browser is also a loadable module located in
[services/ws-server/static](services/ws-server/static).

A custom UX module can be used by setting the `ws-server` environment variable `MODULES_ROOT`.

## Run e2e

Run the end-to-end tests using Chrome:

```bash
mise run ws-e2e-chrome
```

## Run an example demo scenario using et-cli
Run an example demo scenario using et-cli

```bash
cargo install --path utilities/cli --force
Expand All @@ -93,7 +107,7 @@ To regenerate all checked-in verification outputs from
the matching `verification/*/output/<input-file-stem>` folder:

```bash
et-cli regen-verification
mise run regen-verification
```

## Grant
Expand Down
40 changes: 40 additions & 0 deletions libs/edge-toolkit/src/config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,46 @@ pub fn get_project_root() -> PathBuf {
}
}

/// Returns the default module search paths for ws-server.
///
/// Includes the standard workspace paths and any npm packages installed via mise.
#[must_use]
pub fn default_modules_folders() -> Vec<PathBuf> {
let project_root = get_project_root();
let mut paths = vec![
project_root.join("services/ws-server/static"),
project_root.join("services/ws-wasm-agent"),
project_root.join("data/model-modules"),
project_root.join("services/ws-modules"),
];
if let Some(p) = mise_npm_modules_path("onnxruntime-web") {
paths.push(p);
}
paths
}

/// Returns the install path for a `mise` tool, e.g. `mise where npm:onnxruntime-web`.
#[must_use]
pub fn mise_where(tool: &str) -> Option<PathBuf> {
let output = std::process::Command::new("mise").args(["where", tool]).output().ok()?;
if !output.status.success() {
return None;
}
let s = std::str::from_utf8(&output.stdout).ok()?;
let p = PathBuf::from(s.trim());
p.is_dir().then_some(p)
}

/// Returns the `lib/node_modules` path within a mise npm tool install root.
///
/// This directory contains all npm packages installed by mise and can be used
/// as a modules search path.
#[must_use]
pub fn mise_npm_modules_path(package: &str) -> Option<PathBuf> {
let p = mise_where(&format!("npm:{package}"))?.join("lib/node_modules");
p.is_dir().then_some(p)
}

/// Default port for the otlp http collector.
#[must_use]
const fn default_otlp_collector_port() -> u16 {
Expand Down
2 changes: 1 addition & 1 deletion services/ws-modules/dart-comm1/pkg/et_ws_dart_comm1.js
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ export async function run() {
}
// Dart @JS() interop resolves against globalThis, so expose the wasm-agent
// classes there for the duration of the call.
const { WsClient, WsClientConfig } = await import("/modules/ws-wasm-agent/et_ws_wasm_agent.js");
const { WsClient, WsClientConfig } = await import("/modules/et-ws-wasm-agent/et_ws_wasm_agent.js");
globalThis.WsClient = WsClient;
globalThis.WsClientConfig = WsClientConfig;
try {
Expand Down
2 changes: 1 addition & 1 deletion services/ws-modules/face-detection/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ use wasm_bindgen_futures::{JsFuture, spawn_local};
use web_sys::MediaStreamConstraints;
use web_sys::{CanvasRenderingContext2d, HtmlCanvasElement, HtmlVideoElement, ImageData, MediaStream};

const FACE_MODEL_PATH: &str = "/modules/model-face1/video_cv.onnx";
const FACE_MODEL_PATH: &str = "/modules/et-model-face1/video_cv.onnx";
const FACE_INPUT_WIDTH: usize = 640;
const FACE_INPUT_HEIGHT: usize = 608;
const FACE_INPUT_WIDTH_F64: f64 = FACE_INPUT_WIDTH as f64;
Expand Down
2 changes: 1 addition & 1 deletion services/ws-modules/har1/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ use wasm_bindgen::prelude::*;
use wasm_bindgen_futures::JsFuture;
use web_sys::Event;

const HAR_MODEL_PATH: &str = "/modules/model-har-motion1/har-motion1.onnx";
const HAR_MODEL_PATH: &str = "/modules/et-model-har-motion1/har-motion1.onnx";
const HAR_SEQUENCE_LENGTH: usize = 150;
const HAR_FEATURE_COUNT: usize = 8;
const HAR_FEAT_INPUT_SIZE: usize = 36;
Expand Down
27 changes: 0 additions & 27 deletions services/ws-modules/har1/src/test_har1.rs
Original file line number Diff line number Diff line change
@@ -1,32 +1,5 @@
use super::*;

#[test]
fn softmax_distribution_preserves_order_and_normalizes() {
let logits = vec![2.0, 1.0, 0.1];
let probs = softmax(&logits);

assert_eq!(probs.len(), 3);
let sum: f64 = probs.iter().sum();
assert!((sum - 1.0).abs() < 1e-6);
assert!(probs[0] > probs[1]);
assert!(probs[1] > probs[2]);
}

#[test]
fn softmax_handles_empty_equal_and_large_values() {
assert!(softmax(&[]).is_empty());

let equal = softmax(&[7.0, 7.0, 7.0]);
assert!(equal.iter().all(|value| (*value - (1.0 / 3.0)).abs() < 1e-6));

let large = softmax(&[1000.0, 1001.0, 999.0]);
assert_eq!(large.len(), 3);
assert!(large.iter().all(|value| value.is_finite()));
assert!((large.iter().sum::<f64>() - 1.0).abs() < 1e-6);
assert!(large[1] > large[0]);
assert!(large[0] > large[2]);
}

#[test]
fn gravity_and_rotation_conversions_handle_positive_negative_and_zero() {
assert_eq!(to_g(0.0), 0.0);
Expand Down
2 changes: 1 addition & 1 deletion services/ws-modules/pydata1/pkg/et_ws_pydata1.js
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ export async function run() {
const wsProtocol = window.location.protocol === "https:" ? "wss:" : "ws:";
const wsUrl = `${wsProtocol}//${window.location.host}/ws`;

const { WsClient, WsClientConfig } = await import("/modules/ws-wasm-agent/et_ws_wasm_agent.js");
const { WsClient, WsClientConfig } = await import("/modules/et-ws-wasm-agent/et_ws_wasm_agent.js");
const client = new WsClient(new WsClientConfig(wsUrl));

let responseResolvers = [];
Expand Down
17 changes: 5 additions & 12 deletions services/ws-server/src/config.rs
Original file line number Diff line number Diff line change
@@ -1,19 +1,9 @@
use std::path::PathBuf;

use edge_toolkit::config::OtlpConfig;
use edge_toolkit::config::{OtlpConfig, default_modules_folders};
use serde::Deserialize;
use serde_default::DefaultFromSerde;

/// Default modules directory.
#[must_use]
pub fn default_modules_folders() -> Vec<std::path::PathBuf> {
let project_root = edge_toolkit::config::get_project_root();
vec![
project_root.join("services/ws-wasm-agent"),
project_root.join("data").join("model-modules"),
project_root.join("services").join("ws-modules"),
]
}
use serde_inline_default::serde_inline_default;

/// Default modules directory.
#[must_use]
Expand All @@ -23,10 +13,13 @@ pub fn default_storage_folder() -> std::path::PathBuf {
}

/// Modules config.
#[serde_inline_default]
#[derive(Clone, Debug, DefaultFromSerde, Deserialize)]
pub struct ModulesConfig {
#[serde(default = "default_modules_folders")]
pub paths: Vec<PathBuf>,
#[serde_inline_default(String::from("et-ws-server-static"))]
pub root: String,
}

/// Storage config.
Expand Down
Loading
Loading