アーキテクチャ概要

ミセバンAIは「店舗向けAI番人」として、来客分析・異常検知・売上予測を単一バイナリで提供するプロダクトだ。技術的な特徴は、Rust monorepoの4クレート構成と、ONNX Runtimeによるエッジ推論の両立にある。

4クレート構成

設計方針は「関心の分離」と「ターゲット別ビルド」の両立。店舗のRaspberry Pi、macOSデスクトップ、クラウドAPIが同一コードベースから生成される。

# Cargo.toml (workspace root)
[workspace]
members = [
    "crates/miseban-core",    # ドメインロジック + ONNXモデル管理
    "crates/miseban-api",     # axum HTTP API + WebSocket
    "crates/miseban-edge",    # Raspberry Pi / エッジデバイス向け
    "crates/miseban-desktop", # macOS / Windows GUI (Tauri)
]

[workspace.dependencies]
axum = "0.7"
ort = "2.0"              # ONNX Runtime bindings
tokio = { version = "1", features = ["full"] }
rusqlite = { version = "0.31", features = ["bundled"] }
serde = { version = "1", features = ["derive"] }

miseban-coreがすべての共通ロジックを持ち、他の3クレートは薄いアダプタ層として機能する。これにより、エッジ向けビルドではGUI依存を完全に排除し、バイナリサイズを12MBに抑えている。

ONNXモデルダウンロードとキャッシュ

モデルファイル(YOLOv8n: 6.3MB、カスタム異常検知: 2.1MB)は初回起動時にダウンロードし、$XDG_DATA_HOME/miseban/models/にキャッシュする。SHA256チェックサムで整合性を検証し、破損時は再ダウンロードが走る。

// crates/miseban-core/src/model_manager.rs
pub async fn ensure_model(spec: &ModelSpec) -> Result<PathBuf> {
    let cache_dir = dirs::data_dir()
        .unwrap_or_else(|| PathBuf::from("."))
        .join("miseban/models");
    fs::create_dir_all(&cache_dir).await?;

    let model_path = cache_dir.join(&spec.filename);
    if model_path.exists() {
        let hash = sha256_file(&model_path).await?;
        if hash == spec.expected_sha256 {
            return Ok(model_path);
        }
        tracing::warn!("Model checksum mismatch, re-downloading");
    }

    // ストリーミングダウンロード(メモリに全載せしない)
    let response = reqwest::get(&spec.url).await?;
    let mut file = tokio::fs::File::create(&model_path).await?;
    let mut stream = response.bytes_stream();
    while let Some(chunk) = stream.next().await {
        tokio::io::copy(&mut chunk?.as_ref(), &mut file).await?;
    }
    Ok(model_path)
}

ORT (ort crate) のセッション生成は重い処理のため、Arc<Session>で共有し、リクエストごとの再生成を回避している。推論はspawn_blockingでtokioランタイムをブロックしない設計とした。

Fly.io Postgres との接続

クラウド側のデータストアにはFly.io Managed Postgresを採用。接続プールはdeadpool-postgresで管理し、Fly internal DNS(miseban-db.internal:5432)経由でWireGuardトンネル越しに接続する。マイグレーションはrefineryで管理。

エッジデバイスはローカルSQLiteに書き込み、5分間隔でクラウドへ差分同期する。競合解決はLWW(Last Writer Wins)で、タイムスタンプカラムを全テーブルに持たせている。

Raspberry Pi クロスコンパイル

Raspberry Pi 4(aarch64)向けビルドはcrossを使う。ONNX Runtimeの動的ライブラリはコンテナ内でビルドされるため、ホスト環境の汚染がない。

# Raspberry Pi 4 (aarch64) 向けクロスコンパイル
cross build --manifest-path crates/miseban-edge/Cargo.toml \
  --release --target aarch64-unknown-linux-gnu

# ARMv7 (Raspberry Pi 3) 向け
cross build --manifest-path crates/miseban-edge/Cargo.toml \
  --release --target armv7-unknown-linux-gnueabihf

# バイナリサイズ確認(stripで12MB → 8.7MB)
aarch64-linux-gnu-strip target/aarch64-unknown-linux-gnu/release/miseban-edge
ls -lh target/aarch64-unknown-linux-gnu/release/miseban-edge

注意点として、ORT 2.0はARMv7でNEON SIMDを使うが、一部の古いPi 3ではクラッシュする。ORT_USE_NEON=0環境変数でフォールバック可能だ。

macOS コード署名と公証

macOSデスクトップ版はTauriでビルドし、Apple Notarization APIで公証を通す。CI/CDはGitHub Actionsで自動化済み。

署名にはDeveloper ID Application証明書が必要で、Keychain Accessからp12エクスポートしたものをGitHub Secretsに格納。codesign --deep --forceで全フレームワークに署名後、xcrun notarytool submitでAppleに送信する。公証完了まで通常2-5分、xcrun stapler stapleでチケットを埋め込んで配布する。

パフォーマンス実測値

指標Raspberry Pi 4M1 MacFly.io (shared-cpu-1x)
YOLOv8n推論180ms/frame12ms/frame45ms/frame
API レイテンシ (p99)23ms
メモリ使用量87MB120MB95MB
コールドスタート1.2s0.4s0.8s

エッジ推論180msは30fpsには届かないが、店舗の来客カウントには5fps程度で十分であり、実用上問題ない。

まとめ

4クレート分離により「同一ロジック、異なるターゲット」を実現し、ONNXモデルのエッジ配信でクラウド依存を最小化した。Raspberry Piで動くAIが月額0円で店舗を見守る――これがミセバンAIの技術的な核心だ。