Cycle 2 で API → DB のデータパイプラインが開通した。カメラが捉えた映像はAIで推論され、PostgreSQL に永続化される。
しかし、それだけでは「見守り」にならない。異常を検知したら、即座にオーナーに伝える。その「出口」が今回の Sprint Cycle 3 だ。
LINE Messaging API によるプッシュ通知、Supabase Realtime によるダッシュボードのライブ更新、アラート管理の5つの新エンドポイント。AI が「見ている」だけでなく「知らせる」存在になった。
1. アーキテクチャ -- 検知から通知までのフルパイプライン
Cycle 3 の核心は、AI推論の結果をアラートとして評価し、複数の出口に同時配信するパイプラインだ。
Cycle 3 アラートパイプライン
ポイントはAlert Evaluationレイヤーの挿入だ。AI推論結果をそのまま通知するのではなく、閾値・ルールで評価してから永続化する。ノイズを排除し、本当に対応が必要なイベントだけがオーナーに届く。
2. アラートの3つの型
侵入検知
営業時間外に人物を検出。深夜の不法侵入、閉店後の不審者を即座に通知。最も緊急度が高い。
混雑検知
設定した人数閾値を超過。レジ待ち列の増加、イベント時の密集を検知して対応を促す。
異常検知
通常パターンから外れた行動を検知。長時間の滞留、特定エリアへの不正アクセスなど。
3. 実装の詳細
LINE Messaging API クライアント
LINE公式アカウントからユーザーにプッシュ通知を送る HTTP クライアント。チャネルトークン設定時のみ初期化。
LineClient::new()-- チャネルトークンを受け取り reqwest::Client をラップpush_message()-- 任意テキストをプッシュ送信push_alert_message()-- アラート種別に応じたリッチメッセージを構築して送信- Optional 初期化 --
LINE_CHANNEL_TOKEN未設定時はNone
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
}
}
アラート評価・永続化・クエリ
AI推論結果からアラートを評価し、DBに永続化し、ダッシュボードから参照するためのクエリ関数群。
AlertRow/AlertPayload-- DB行とAPI入出力の型定義evaluate_alert()-- 推論結果 + 店舗設定 → アラート生成判定 (純粋関数)insert_alert()-- alerts テーブルへの INSERTget_alerts()/get_unread_count()-- クエリ関数mark_as_read()/mark_all_as_read()-- 既読フラグ更新
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 は純粋関数。副作用を持たず、推論結果と店舗設定だけで判定する。テスト容易性とルール拡張性を両立する設計だ。
ダッシュボード -- リアルタイムアラート + LINE連携
- Supabase クエリ -- alerts テーブルから実データ取得。デモモードではフォールバック
- Realtime subscription --
postgres_changesで INSERT を監視 - 未読バッジ + トースト -- 新規アラートをリアルタイム表示
- LINE連携モーダル -- 友だち追加QRコード + OAuth認証コードフロー
// 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通知メッセージの例
5. Before / After -- デモからライブへ
| 項目 | Cycle 2 (Before) | Cycle 3 (After) |
|---|---|---|
| アラート表示 | ハードコードされたデモデータ | Supabase alerts テーブルからリアルクエリ |
| リアルタイム更新 | なし (ページリロード必要) | Supabase Realtime (postgres_changes) |
| 通知手段 | なし | LINE プッシュ通知 + ブラウザトースト |
| アラート管理 | なし | 既読/未読管理、未読バッジ、一括既読 |
| AI → 通知 | 推論結果はDBに保存されるだけ | 推論 → 評価 → 永続化 → LINE通知 |
最大の変化は「受動的な監視」から「能動的な通知」への転換だ。日本の小売店オーナーの97%がLINEを利用している。ダッシュボードを開かなくても、異常はLINEで届く。
6. 新規APIエンドポイント
| Method | Path | 説明 | 認証 |
|---|---|---|---|
| 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/line | LINE 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. 技術的判断
なぜこの設計にしたか
- LINE Messaging API -- 日本の小売店オーナーのリーチに最適。Slack や Email では開封率が低い
- Optional LineClient --
Option<LineClient>で保持。開発環境で LINE なしでもテスト可能 - Supabase Realtime --
postgres_changesで INSERT をそのまま配信。自前 WebSocket サーバー不要 - evaluate_alert() を純粋関数に -- 副作用なしで単体テスト容易。ルール追加時もこの関数だけ修正
- デモデータフォールバック -- Supabase 未接続でもダッシュボードが動作。DX を犠牲にしない
9. 成果と数字
作成・更新したファイル
crates/api/src/line.rs-- LINE Messaging API クライアント (89行)crates/api/src/alerts.rs-- アラート評価・クエリ関数群 (156行)crates/api/src/main.rs-- 5エンドポイント追加、LINE初期化 (+120行)crates/api/Cargo.toml-- reqwest 依存追加web/dashboard/alerts.js-- Supabase Realtime + アラートUI (142行)web/dashboard/line-link.js-- LINE連携モーダル (68行)web/dashboard/index.html-- 未読バッジ・トースト追加supabase/migrations/003_alerts.sql-- alerts テーブル + RLS ポリシー (45行)
Cycle 2 でデータパイプラインを通したからこそ、Cycle 3 は「出口を増やすだけ」で済んだ。基盤の設計が正しかった証拠だ。
10. 次のサイクルで何をするか
Cycle 4 は2つの候補がある。Edge推論 (YOLOv8n on-device) はクラウドへのフレーム送信を不要にし、レイテンシとコストを劇的に削減する。Stripe決済統合 はフリー / スターター / プロの課金フローを構築し、事業としての収益化を可能にする。どちらを先にするかは、ベータ候補の店舗オーナーからのフィードバック次第だ。
カメラが「見て」、AIが「判断して」、LINEが「知らせる」。3つのサイクルで、ミセバンAIは本当の意味で「見守り」を始めた。
AIが見守り、LINEが知らせる。次世代の店舗セキュリティを体験しませんか?
ミセバンAIは、スプリントサイクルで高速に進化中。
既存の防犯カメラがAI店長に変わる、小規模店舗のための次世代AI分析。