やってみたいけどまだやっていない
飲食店向け顔認証会員システム — 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 Detection | Google製、 軽量、 リアルタイム可 |
| 顔認証 | face_recognition (dlib + FaceNet) | OSS定番、 精度98-99%、 Python1行で使える |
| ベクトルDB | Faiss or pgvector | 100名なら全件比較でも数ms |
| API | FastAPI (既存 mikyun-portal) | 2026-05-14 で maguro Docker 化済、 既存認証を流用 |
| 会員DB | Docker MariaDB (既存) + 専用テーブル | maguro compose-db-1 の mikyun_cloud スキーマを拡張 |
| リアルタイム通知 | WebSocket + LINE Bot | 店員iPad へ即時表示・お客様 LINE 通知 |
| 店員UI | Web (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-2 | hello_face.py で自分の顔が認識される | Webカメラに映って名前表示 |
| Week 1 | maguro mikyun-portal に face エンドポイント追加 | Postman で /api/face/match が動く |
| Week 2 | Raspberry 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 |
| MediaPipe | developers.google.com/mediapipe |
| Faiss (Facebook AI) | github.com/facebookresearch/faiss |
| Logitech C920 | www.logitech.com/ja-jp/products/webcams/c920-pro-hd-webcam.html |
| Raspberry Pi 5 | www.raspberrypi.com/products/raspberry-pi-5/ |
| 個人情報保護委員会ガイドライン | www.ppc.go.jp/personalinfo/ |
| 関連案件: ドンマンジョーネ | /home/bakusoku/don-mangione/ |
再開時のチェックポイント
再開時、 まずこれを読む:
- このドキュメントを冒頭から読み直す (30分)
- ドンマンジョーネ提案書 (donmangione_proposal_v2.pptx) の P24-26 を見直す
/home/claude/face-project/ディレクトリのコード雛形を確認- maguro mikyun-portal の現状を確認、 新エンドポイント追加箇所を決定
- Webカメラを購入していなければ買う
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. 内部資料 / 棚上げ承認: みーきゅん
「面白そうだからやってみたい」が、 すべての始まり。