ブログ一覧へ戻る

Cycle 2 で API → DB のデータパイプラインが開通した。カメラが捉えた映像はAIで推論され、PostgreSQL に永続化される。

しかし、それだけでは「見守り」にならない。異常を検知したら、即座にオーナーに伝える。その「出口」が今回の Sprint Cycle 3 だ。

LINE Messaging API によるプッシュ通知、Supabase Realtime によるダッシュボードのライブ更新、アラート管理の5つの新エンドポイント。AI が「見ている」だけでなく「知らせる」存在になった。

1. アーキテクチャ -- 検知から通知までのフルパイプライン

Cycle 3 の核心は、AI推論の結果をアラートとして評価し、複数の出口に同時配信するパイプラインだ。

Cycle 3 アラートパイプライン

CameraRTSP / フレーム送信
AI Inference人物検出 / 行動分析
Alert Evaluation閾値判定 / ルール評価
Supabase DBalerts テーブル
LINE PushMessaging API
DashboardRealtime subscription

ポイントはAlert Evaluationレイヤーの挿入だ。AI推論結果をそのまま通知するのではなく、閾値・ルールで評価してから永続化する。ノイズを排除し、本当に対応が必要なイベントだけがオーナーに届く。

2. アラートの3つの型

🚨

侵入検知

intrusion

営業時間外に人物を検出。深夜の不法侵入、閉店後の不審者を即座に通知。最も緊急度が高い。

👥

混雑検知

crowding

設定した人数閾値を超過。レジ待ち列の増加、イベント時の密集を検知して対応を促す。

異常検知

unusual_activity

通常パターンから外れた行動を検知。長時間の滞留、特定エリアへの不正アクセスなど。

3. 実装の詳細

crates/api/src/line.rs

LINE Messaging API クライアント

LINE公式アカウントからユーザーにプッシュ通知を送る HTTP クライアント。チャネルトークン設定時のみ初期化。

pub struct LineClient {
    http: reqwest::Client,
    channel_token: String,
}

impl LineClient {
    pub async fn push_alert_message(
        &self, user_id: &str, alert: &AlertPayload
    ) -> Result<(), LineError> {
        let (label, _color) = match alert.alert_type.as_str() {
            "intrusion"        => ("侵入検知", "#e74c3c"),
            "crowding"         => ("混雑検知", "#d97706"),
            "unusual_activity" => ("異常検知", "#9333ea"),
            _                  => ("アラート", "#4f46e5"),
        };
        let text = format!(
            "[{}] {}\n場所: {}\n時刻: {}",
            label, alert.message, alert.camera_name, alert.detected_at
        );
        self.push_message(user_id, &text).await
    }
}
crates/api/src/alerts.rs

アラート評価・永続化・クエリ

AI推論結果からアラートを評価し、DBに永続化し、ダッシュボードから参照するためのクエリ関数群。

pub fn evaluate_alert(
    inference: &InferenceResult,
    config: &StoreConfig,
) -> Option<AlertPayload> {
    // 営業時間外 + 人物検出 = 侵入検知
    if !config.is_open_now() && inference.people_count > 0 {
        return Some(AlertPayload {
            alert_type: "intrusion".into(),
            severity: "critical".into(),
            message: format!("営業時間外に{}名を検出", inference.people_count),
            ..
        });
    }
    // 人数が閾値超過 = 混雑検知
    if inference.people_count > config.crowding_threshold {
        return Some(AlertPayload {
            alert_type: "crowding".into(), severity: "warning".into(),
            message: format!("{}名検出 (閾値: {}名)",
                inference.people_count, config.crowding_threshold), ..
        });
    }
    // 異常スコア超過 = 異常検知
    if inference.anomaly_score > config.anomaly_threshold {
        return Some(AlertPayload {
            alert_type: "unusual_activity".into(), severity: "warning".into(),
            message: "通常と異なる行動パターンを検出".into(), ..
        });
    }
    None
}

evaluate_alert は純粋関数。副作用を持たず、推論結果と店舗設定だけで判定する。テスト容易性とルール拡張性を両立する設計だ。

Dashboard (web/dashboard)

ダッシュボード -- リアルタイムアラート + LINE連携

// Supabase Realtime -- アラートのライブ購読
const channel = supabase
  .channel('alerts-realtime')
  .on('postgres_changes',
    { event: 'INSERT', schema: 'public', table: 'alerts',
      filter: `store_id=eq.${storeId}` },
    (payload) => {
      alerts.value.unshift(payload.new);
      unreadCount.value += 1;
      showToast({ type: payload.new.alert_type, message: payload.new.message });
    }
  ).subscribe();

4. LINE通知メッセージの例

LINE -- ミセバンAI 公式アカウント
🚨 侵入検知アラート
店舗サンプル珈琲店 渋谷店 カメラ裏口カメラ #03 検知内容営業時間外に2名を検出しました 時刻2026-02-23 02:34:17 JST
👥 混雑検知アラート
検知内容18名を検出 (閾値: 15名) カメラレジ前カメラ #01 時刻2026-02-23 12:15:42 JST
⚠ 異常検知アラート
検知内容通常と異なる行動パターンを検出 カメラ倉庫カメラ #05 時刻2026-02-23 19:48:03 JST

5. Before / After -- デモからライブへ

項目Cycle 2 (Before)Cycle 3 (After)
アラート表示ハードコードされたデモデータSupabase alerts テーブルからリアルクエリ
リアルタイム更新なし (ページリロード必要)Supabase Realtime (postgres_changes)
通知手段なしLINE プッシュ通知 + ブラウザトースト
アラート管理なし既読/未読管理、未読バッジ、一括既読
AI → 通知推論結果はDBに保存されるだけ推論 → 評価 → 永続化 → LINE通知

最大の変化は「受動的な監視」から「能動的な通知」への転換だ。日本の小売店オーナーの97%がLINEを利用している。ダッシュボードを開かなくても、異常はLINEで届く。

6. 新規APIエンドポイント

MethodPath説明認証
GET/api/v1/stores/me/alerts自店舗のアラート一覧 (新しい順)JWT
GET/api/v1/stores/me/alerts/unread未読アラート数JWT
PATCH/api/v1/alerts/:alert_id/read指定アラートを既読化 (所有権検証)JWT
POST/api/v1/stores/me/alerts/read-all全アラート一括既読JWT
POST/api/v1/webhooks/lineLINE Webhook受信 (署名検証)Public

Cycle 2 の7エンドポイントと合わせ、合計12エンドポイントPATCH /alerts/:alert_id/read は JWT の sub で所有権を検証し、他店舗のアラートには403を返す。

7. フレーム受信フローの拡張

/// POST /api/v1/frames -- Cycle 3 拡張版
async fn receive_frame(
    State(state): State<AppState>,
    AuthUser(user_id): AuthUser,
    Json(payload): Json<FramePayload>,
) -> Result<Json<FrameResponse>, ApiError> {
    let inference = run_inference(&payload).await?;           // 1. AI推論
    insert_visitor_count(&state.pool, ..).await.ok();         // 2. DB永続化

    // --- Cycle 3 で追加 ---
    let config = get_store_config(&state.pool, user_id).await?;
    if let Some(alert) = evaluate_alert(&inference, &config) {
        insert_alert(&state.pool, &alert).await.ok();        // 3. アラート永続化
        if let Some(ref line) = state.line_client {            // 4. LINE通知
            if let Some(uid) = config.line_user_id {
                line.push_alert_message(&uid, &alert).await.ok();
            }
        }
    }
    Ok(Json(FrameResponse { inference, alert: alert.is_some() }))
}

.ok() で失敗を握りつぶしている点に注目。通知は best-effort、推論は must-succeed。LINE障害でもカメラ分析は止まらない。

8. 技術的判断

なぜこの設計にしたか

9. 成果と数字

5
新規エンドポイント
~650
追加コード
8
ファイル作成/更新
~1.5時間
実装時間

作成・更新したファイル

  1. crates/api/src/line.rs -- LINE Messaging API クライアント (89行)
  2. crates/api/src/alerts.rs -- アラート評価・クエリ関数群 (156行)
  3. crates/api/src/main.rs -- 5エンドポイント追加、LINE初期化 (+120行)
  4. crates/api/Cargo.toml -- reqwest 依存追加
  5. web/dashboard/alerts.js -- Supabase Realtime + アラートUI (142行)
  6. web/dashboard/line-link.js -- LINE連携モーダル (68行)
  7. web/dashboard/index.html -- 未読バッジ・トースト追加
  8. supabase/migrations/003_alerts.sql -- alerts テーブル + RLS ポリシー (45行)

Cycle 2 でデータパイプラインを通したからこそ、Cycle 3 は「出口を増やすだけ」で済んだ。基盤の設計が正しかった証拠だ。

10. 次のサイクルで何をするか

Cycle 1
ダッシュボード・DB・認証完了 -- UI、6テーブル、Supabase Auth
Cycle 2
Rust API × Supabase (sqlx + JWT)完了 -- 7エンドポイント、データパイプライン
Cycle 3
LINE通知Bot + リアルタイムアラート完了 -- 本記事の内容
Cycle 4
Edge推論 or Stripe決済YOLOv8n on-device でクラウド依存排除、または Stripe で課金フロー構築
Cycle 5
クローズドベータテスト実店舗での動作検証。実データによるチューニング

Cycle 4 は2つの候補がある。Edge推論 (YOLOv8n on-device) はクラウドへのフレーム送信を不要にし、レイテンシとコストを劇的に削減する。Stripe決済統合 はフリー / スターター / プロの課金フローを構築し、事業としての収益化を可能にする。どちらを先にするかは、ベータ候補の店舗オーナーからのフィードバック次第だ。

カメラが「見て」、AIが「判断して」、LINEが「知らせる」。3つのサイクルで、ミセバンAIは本当の意味で「見守り」を始めた。

AIが見守り、LINEが知らせる。次世代の店舗セキュリティを体験しませんか?

ミセバンAIは、スプリントサイクルで高速に進化中。
既存の防犯カメラがAI店長に変わる、小規模店舗のための次世代AI分析。