やってみたいけどまだやっていない

飲食店向け顔認証会員システム — Project OmoteSan (棚上げ中)

// PROJECT OMOTESAN / 初版 2026-04-26 / 最終更新: 2026-05-14 (インフラ反映)

棚上げ中 個店でも高級ホテルのコンシェルジュレベルの「私のこと覚えてくれてる感」を月¥30,000で実現するシステム。 ドンマンジョーネ案件で実証 → 千種区SaaS → 全国SaaS の段階展開構想。 着手トリガー: いつでも。

プロジェクトの目的 (一行)
VISION: 個店でも、 高級ホテルのコンシェルジュレベルの「私のこと覚えてくれてる感」を、 月¥30,000で実現する。
// 背景
  • ドンマンジョーネ案件で「どんまーパス」サブスクを設計中 (2026-04)
  • 「顔認証で前回注文を覚えて『いつものハイボール濃いめでいいですか?』と聞ける店」発想
  • 「人間の店員が言うから価値がある」「AIアバターは却下」「顔認証は会員カード代わり + ポイント連動」と方針確定
  • ドンマンジョーネ実装が成功すれば、 そのまま個店向け SaaS 事業として展開可能
  • 本ドキュメントは、 後日プロジェクトを着手する際の起動ファイルとして保管
3つの選択肢 (着手時に決定)
選択肢規模初期収益見込リスク
A. 単店専用 — ドンマンジョーネのみ1店舗¥10,000店から¥600,000 + 月¥30,000最小
B. 地域SaaS — 千種区・名古屋市内 10-50店10-50店¥500,000月¥300,000-¥1,500,000中
C. 全国SaaS化 — OmoteSan 独立事業化100-1,000店¥3M-¥10M年¥3.5億〜大

推奨開始順序: A → ドンマンジョーネで実証 → 千種区他店から問い合わせ来たらB → さらに伸びたらC。 本ドキュメントは「A の実装手順」を中心に、 後で B/C に昇格する設計を意識。

技術スタック (決定済)
レイヤ採用技術理由
顔検出MediaPipe Face DetectionGoogle製、 軽量、 リアルタイム可
顔認証face_recognition (dlib + FaceNet)OSS定番、 精度98-99%、 Python1行で使える
ベクトルDBFaiss or pgvector100名なら全件比較でも数ms
APIFastAPI (既存 mikyun-portal)2026-05-14 で maguro Docker 化済、 既存認証を流用
会員DBDocker MariaDB (既存) + 専用テーブルmaguro compose-db-1 の mikyun_cloud スキーマを拡張
リアルタイム通知WebSocket + LINE Bot店員iPad へ即時表示・お客様 LINE 通知
店員UIWeb (HTML + Tailwind)iPad の Safari で開くだけ、 アプリ不要
カメラLogitech C920 (USB)¥10,000、 Linux ドライバ標準対応
サーバー店舗用 Raspberry Pi 5 + katuocloud (maguro)店舗ローカル処理 + クラウド同期
注意: 顔認証エンジンは店舗ローカルの Raspberry Pi 5 で動かすのが正解 (プライバシー + 低レイテンシ)。 katuocloud (maguro) には顔特徴量ベクトル (数値) と会員IDのみ同期。 顔写真自体は店舗外に出さない。
アーキテクチャ図 (2026-05-14 インフラ反映版)
┌────────────── 店舗 (ドンマンジョーネ等) ──────────────┐ │ │ │ [ Logitech C920 ] │ │ │ USB │ │ ▼ │ │ [ Raspberry Pi 5 ] エッジ処理 │ │ ├ MediaPipe (顔検出) │ │ ├ face_recognition (特徴量抽出) │ │ ├ Faiss (ローカル会員DBで比較) │ │ └ WebSocket送信 │ │ │ WireGuard (wg-clients VPN, 10.200.0.0/16) │ │ ▼ │ │ [ 店員 iPad ] Web画面 │ │ 「3番テーブル: 山田様、 前回ラグー、 │ │ ハイボール濃いめ。 本日のオススメ: │ │ アクアパッツァ ¥1,680」 │ │ │ └────────────────────────────────────────────────────────────┘ │ │ WireGuard VPN (UDP 54024 → RTX1300 NAT) ▼ ┌──── katuocloud / maguro (運営インフラ) ────┐ │ │ │ [ FastAPI / mikyun-portal:1.1 ] │ │ ├ /api/face/register │ │ ├ /api/face/match │ │ ├ /api/points/award │ │ └ /api/order/upsell-suggest │ │ │ │ [ Docker MariaDB: mikyun_cloud ] │ │ ├ members (既存) │ │ ├ face_embeddings (新規) │ │ ├ visit_log (新規) │ │ ├ order_history (新規) │ │ └ points_ledger (新規) │ │ │ │ [ LINE Messaging API ] │ │ お客様への自動通知 │ │ │ └──────────────────────────────────────────────┘
DB スキーマ設計 (追加分のみ)
-- 既存 members テーブルへの拡張
ALTER TABLE members
  ADD COLUMN face_consent_at         DATETIME NULL,   -- 顔認証同意日時
  ADD COLUMN face_consent_revoked_at DATETIME NULL,   -- 撤回日時
  ADD COLUMN total_points            INT NOT NULL DEFAULT 0,
  ADD COLUMN available_points        INT NOT NULL DEFAULT 0;

-- 顔特徴量ベクトル (128次元 × float32 = 512バイト)
CREATE TABLE face_embeddings (
  id           BIGINT AUTO_INCREMENT PRIMARY KEY,
  member_id    BIGINT NOT NULL,
  embedding    BLOB NOT NULL,
  reference_image_hash CHAR(64),
  enrolled_at  DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
  is_active    TINYINT(1) NOT NULL DEFAULT 1,
  FOREIGN KEY (member_id) REFERENCES members(id) ON DELETE CASCADE,
  INDEX idx_member (member_id, is_active)
);

-- 来店ログ
CREATE TABLE visit_log (
  id            BIGINT AUTO_INCREMENT PRIMARY KEY,
  member_id     BIGINT NOT NULL,
  store_id      VARCHAR(32) NOT NULL,
  visited_at    DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
  recognition_method ENUM('face','qr','manual') DEFAULT 'face',
  recognition_confidence FLOAT,
  FOREIGN KEY (member_id) REFERENCES members(id),
  INDEX idx_member_time (member_id, visited_at)
);

-- 注文履歴
CREATE TABLE order_history (
  id            BIGINT AUTO_INCREMENT PRIMARY KEY,
  visit_id      BIGINT NOT NULL,
  member_id     BIGINT NOT NULL,
  item_name     VARCHAR(128) NOT NULL,
  item_category VARCHAR(32),
  price_yen     INT NOT NULL,
  notes         TEXT,
  ordered_at    DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
  FOREIGN KEY (visit_id) REFERENCES visit_log(id) ON DELETE CASCADE,
  INDEX idx_member (member_id, ordered_at)
);

-- ポイント台帳 (増減すべて記録)
CREATE TABLE points_ledger (
  id           BIGINT AUTO_INCREMENT PRIMARY KEY,
  member_id    BIGINT NOT NULL,
  points_delta INT NOT NULL,
  reason       ENUM('visit','redeem','expire','adjust'),
  related_visit_id BIGINT NULL,
  description  VARCHAR(255),
  occurred_at  DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
  FOREIGN KEY (member_id) REFERENCES members(id),
  INDEX idx_member_time (member_id, occurred_at)
);
FastAPI エンドポイント設計
メソッドパス用途認証
POST/api/face/enroll会員の顔を新規登録HTTP Basic (管理者)
POST/api/face/match顔ベクトル送信 → 最も近い会員を返す店舗 API Key
GET/api/members/{id}/profile来店時の会員情報 + 過去注文 + 好み店舗 API Key
POST/api/visits来店記録 + ポイント自動付与店舗 API Key
POST/api/orders注文記録 (ポイント付与の根拠)店舗 API Key
POST/api/points/redeemポイント引換店舗 API Key
GET/api/upsell/{member_id}過去注文ベースのアップセル提案店舗 API Key
DELETE/api/face/forget/{member_id}顔データの完全削除 (退会・撤回)HTTP Basic + 本人確認
認証設計: 店舗側 Raspberry Pi から maguro mikyun-portal へのアクセスは WireGuard wg-clients (10.200.0.0/16) 内での API Key 認証。 管理画面への外部アクセスは既存の /ui_admin の HTTP Basic 認証を流用。
コードスケルトン (Python・抜粋)
// 1. 顔登録エンドポイント
# app/routers/face.py
from fastapi import APIRouter, UploadFile, Depends, HTTPException
import face_recognition
import numpy as np
from .deps import get_db, require_admin

router = APIRouter(prefix="/api/face", tags=["face"])

@router.post("/enroll")
async def enroll_face(member_id: int, image: UploadFile,
                      db = Depends(get_db), _admin = Depends(require_admin)):
    # 1. 同意確認
    member = db.execute(
        "SELECT face_consent_at FROM members WHERE id = %s",
        (member_id,)).fetchone()
    if not member or not member['face_consent_at']:
        raise HTTPException(403, "face_consent_required")

    # 2. 画像から特徴量抽出
    img = face_recognition.load_image_file(image.file)
    encodings = face_recognition.face_encodings(img)
    if len(encodings) != 1:
        raise HTTPException(400, "face_must_be_exactly_one")

    embedding = encodings[0].astype(np.float32).tobytes()

    # 3. DB保存
    db.execute(
        "INSERT INTO face_embeddings (member_id, embedding) VALUES (%s, %s)",
        (member_id, embedding))
    db.commit()
    return {"status": "enrolled", "member_id": member_id}
// 2. 顔認識エンドポイント (店舗からの問い合わせ)
@router.post("/match")
async def match_face(embedding_b64: str,
                     db = Depends(get_db),
                     _api_key = Depends(require_store_key)):
    target = np.frombuffer(base64.b64decode(embedding_b64), dtype=np.float32)

    rows = db.execute("""
        SELECT fe.member_id, fe.embedding, m.name, m.available_points
        FROM face_embeddings fe
        JOIN members m ON m.id = fe.member_id
        WHERE fe.is_active = 1 AND m.face_consent_revoked_at IS NULL
    """).fetchall()

    best_match, best_distance = None, 1.0
    for row in rows:
        emb = np.frombuffer(row['embedding'], dtype=np.float32)
        distance = np.linalg.norm(target - emb)
        if distance < best_distance:
            best_distance, best_match = distance, row

    if not best_match or best_distance >= 0.5:
        return {"matched": False}

    return {
        "matched": True,
        "member_id": best_match['member_id'],
        "name": best_match['name'],
        "confidence": float(1 - best_distance),
        "available_points": best_match['available_points'],
    }
// 3. 来店記録 + 自動ポイント付与
# app/routers/visits.py
POINT_RULES = {980: 10, 1280: 15, 1480: 20, 1680: 25}

@router.post("/api/visits")
async def log_visit(member_id: int, store_id: str, confidence: float,
                    method: str = "face", db = Depends(get_db)):
    cursor = db.execute("""
        INSERT INTO visit_log (member_id, store_id, recognition_method, recognition_confidence)
        VALUES (%s, %s, %s, %s)
    """, (member_id, store_id, method, confidence))
    visit_id = cursor.lastrowid

    asyncio.create_task(notify_line(member_id, "おかえりなさい!本日もお疲れさまです🍝"))
    return {"visit_id": visit_id}

@router.post("/api/orders")
async def log_order(visit_id: int, item_name: str, price_yen: int,
                    notes: str = None, db = Depends(get_db)):
    member_id = db.execute(
        "SELECT member_id FROM visit_log WHERE id = %s",
        (visit_id,)).fetchone()['member_id']
    db.execute("""
        INSERT INTO order_history (visit_id, member_id, item_name, price_yen, notes)
        VALUES (%s, %s, %s, %s, %s)
    """, (visit_id, member_id, item_name, price_yen, notes))

    points = POINT_RULES.get(price_yen, price_yen // 100)
    db.execute("""
        INSERT INTO points_ledger (member_id, points_delta, reason, related_visit_id, description)
        VALUES (%s, %s, 'visit', %s, %s)
    """, (member_id, points, visit_id, f"order: {item_name}"))
    db.execute("""
        UPDATE members
        SET total_points = total_points + %s, available_points = available_points + %s
        WHERE id = %s
    """, (points, points, member_id))
    db.commit()
    return {"points_awarded": points}
店舗側 Raspberry Pi コード (要旨)
# store_client/main.py - 店舗側で常駐するプロセス
import cv2, face_recognition, requests, base64, time
import websocket

# katuocloud maguro mikyun-portal へ wg-clients VPN 経由でアクセス
API_BASE = "http://10.200.0.1:9000"   # WG経由の portal 内部IP
STORE_ID = "donmangione-ikeshita"
API_KEY = "<store-api-key>"

def main():
    cap = cv2.VideoCapture(0)
    last_seen = {}

    while True:
        ret, frame = cap.read()
        rgb = cv2.cvtColor(frame, cv2.COLOR_BGR2RGB)
        encodings = face_recognition.face_encodings(rgb)

        for emb in encodings:
            r = requests.post(
                f"{API_BASE}/api/face/match",
                json={"embedding_b64": base64.b64encode(emb.tobytes()).decode()},
                headers={"X-API-Key": API_KEY},
            ).json()

            if r.get("matched"):
                mid = r["member_id"]
                if mid in last_seen and time.time() - last_seen[mid] < 300:
                    continue
                last_seen[mid] = time.time()

                requests.post(f"{API_BASE}/api/visits", json={
                    "member_id": mid, "store_id": STORE_ID,
                    "confidence": r["confidence"], "method": "face",
                }, headers={"X-API-Key": API_KEY})

                send_to_ipad({
                    "event": "member_arrived",
                    "name": r["name"], "member_id": mid,
                    "available_points": r["available_points"],
                })
        time.sleep(0.5)

if __name__ == "__main__":
    main()
店員 iPad UI (HTML雛形)
<!DOCTYPE html>
<html><head>
  <meta charset="utf-8">
  <title>ドンマンジョーネ 来店アシスト</title>
  <script src="https://cdn.tailwindcss.com"></script>
</head>
<body class="bg-gray-900 text-white p-8">
  <h1 class="text-3xl mb-6">来店ピックアップ</h1>
  <div id="arrivals" class="space-y-4"></div>
  <script>
    const ws = new WebSocket('wss://10.200.0.1/ws/store/donmangione-ikeshita');
    const list = document.getElementById('arrivals');

    ws.onmessage = async (ev) => {
      const msg = JSON.parse(ev.data);
      if (msg.event === 'member_arrived') {
        const r = await fetch(`/api/upsell/${msg.member_id}`).then(r => r.json());
        const card = document.createElement('div');
        card.className = 'bg-white text-gray-900 p-6 rounded-lg shadow-xl';
        card.innerHTML = `
          <div class="text-2xl font-bold mb-2">${msg.name} 様 ご来店</div>
          <div class="text-sm text-gray-600 mb-4">会員ポイント: ${msg.available_points}pt</div>
          <div class="border-l-4 border-red-500 pl-3">
            <div class="font-bold text-red-700 mb-1">接客スクリプト</div>
            ${r.suggestions.map(s => `<div>・${s}</div>`).join('')}
          </div>`;
        list.prepend(card);
        setTimeout(() => card.remove(), 300000);
      }
    };
  </script>
</body></html>
買い物リスト (着手日に Amazon でポチる)
  • ✓ 必須 Logitech C920 HD Pro Webcam (¥10,000)
  • ✓ 必須 Raspberry Pi 5 8GB (¥18,000)
  • ✓ 必須 Pi 5 用 Active Cooler (¥1,500)
  • ✓ 必須 Pi 5 用 microSD 64GB (¥1,500)
  • ✓ 必須 Pi 5 用電源 27W USB-C PD (¥2,000)
  • ! あると便利: カメラ三脚 or マウントアーム (¥1,500)
  • ! あると便利: LEDリングライト (¥2,500)
  • ! 店員側: iPad (既存転用OK、 なければ中古¥30,000〜)
合計: 最小構成 約¥35,000、 フル構成 約¥70,000。 ドンマンジョーネ導入時は Phase 4 のオプション工事費に組込可。
プライバシー・法令対応チェックリスト
  • ✓ 入会時の同意フォーム作成 (顔認証使用について明示)
  • ✓ 同意取消フローの実装 (DELETE /api/face/forget/{id})
  • ✓ 店内に「顔認証システム稼働中」掲示
  • ✓ プライバシーポリシーに顔データ取扱を明記
  • ✓ 顔特徴量の暗号化 (DB 保存時に AES-256)
  • ✓ 顔写真原本は保存しない (特徴量抽出後即削除)
  • ✓ 国外送信なし (店舗ローカル処理 + 国内 katuocloud のみ)
  • ✓ 退会時の即削除 + ログ残す
  • ! 個人情報保護委員会への届け出要否確認 (規模次第)
  • ! 顧問弁護士に利用規約レビュー依頼
マイルストーン (着手後)
期間マイルストーン確認方法
Day 1-2hello_face.py で自分の顔が認識されるWebカメラに映って名前表示
Week 1maguro mikyun-portal に face エンドポイント追加Postman で /api/face/match が動く
Week 2Raspberry Pi にカメラ常駐プロセスカメラ → Pi → maguro フロー成功
Week 3店員 iPad UI 完成WebSocket 通知が iPad に表示
Week 4ポイント連動完成注文記録 → ポイント加算 → LINE 通知
Week 5ベータテスト (自分+家族)10名規模で精度98%以上
Week 6ドンマンジョーネ実装開始店舗オーナー合意あり
事業化を考え始める分岐ポイント (KPI)
継続判断 (A→B昇格)事業化判断 (B→C昇格)
ドンマンジョーネで6ヶ月安定運用
会員ポイント実使用率 50% 以上
追加注文発生率 20% 以上
他店から問い合わせ 3件以上
地域10店舗で運用実績
月次解約率 5% 以下
1店舗あたり月¥30,000 の継続課金が成立
LINE 通知のクリック率 20% 以上
アイデアメモ (実装時に検討)
  • マスク対応: マスク有り画像も学習させる (精度 85% 目標)
  • 双子・親子の誤認: 閾値調整 + 複数枚学習で対応
  • 季節提案: 体調別メニューと連動「冬は温かい料理を多めに」
  • 誕生日サプライズ: 誕生日来店検知 → シェフへ即通知
  • 「お久しぶり LINE」自動化: 14日来店なし → 自動で LINE
  • 予約システム連動: 予約時刻+顔認証で「予約のお客様、 ご到着です」
  • 同伴者識別: 会員+家族2名識別 → 家族割引適用
  • 客層分析ダッシュボード: 年齢・性別の傾向 (個人特定なし統計)
  • SaaS化前提のマルチテナント設計: store_id を UUID で切る
関連リンク
用途リンク
face_recognition (Python)github.com/ageitgey/face_recognition
MediaPipedevelopers.google.com/mediapipe
Faiss (Facebook AI)github.com/facebookresearch/faiss
Logitech C920www.logitech.com/ja-jp/products/webcams/c920-pro-hd-webcam.html
Raspberry Pi 5www.raspberrypi.com/products/raspberry-pi-5/
個人情報保護委員会ガイドラインwww.ppc.go.jp/personalinfo/
関連案件: ドンマンジョーネ/home/bakusoku/don-mangione/
再開時のチェックポイント
再開時、 まずこれを読む:
  1. このドキュメントを冒頭から読み直す (30分)
  2. ドンマンジョーネ提案書 (donmangione_proposal_v2.pptx) の P24-26 を見直す
  3. /home/claude/face-project/ ディレクトリのコード雛形を確認
  4. maguro mikyun-portal の現状を確認、 新エンドポイント追加箇所を決定
  5. Webカメラを購入していなければ買う
  6. hello_face.py から開始
再開条件の目安:
  • ドンマンジョーネ提案がオーナーに通った時
  • 千種区他店から「うちもやりたい」と連絡来た時
  • katuocloud 本体が安定運用に入った時 (2026-05-14 で達成済)
  • 単純に「やる気が湧いた」時 (これが一番大事)
クイックスタート (着手日にやること)
# 1. 環境構築 (maguro 開発環境)
cd ~/projects
mkdir omotesan && cd omotesan
python -m venv .venv && source .venv/bin/activate
pip install face_recognition opencv-python fastapi uvicorn

# 2. Hello Face を試す
python hello_face.py
# → Webカメラに自分の顔が映って名前が表示されたら成功

# 3. maguro mikyun-portal へのデプロイ準備
# /opt/mikyun-portal/app/routers/face.py に
# 本ドキュメントのスケルトンを配置 (Docker image 1.2 へ)

本ドキュメント最終更新: 2026-05-14 / Mikyun Web Lab. 内部資料 / 棚上げ承認: みーきゅん

「面白そうだからやってみたい」が、 すべての始まり。