feat(infra): Add AI Proxy Server for Rate Limiting & API Key Protection
## 概要 Gemini APIへのリクエストを中継するプロキシサーバーを実装。 アプリにAPIキーを埋め込まず、Synology上で安全に管理。 ## 主な機能 - デバイスID認証(SHA256ハッシュ) - レート制限(1デバイスあたり1日10回) - 使用状況のログ記録(JSON形式) - 30日以上前のデータ自動削除 ## 技術スタック - Python 3.11 + FastAPI - Docker Container(既存のGitea環境に追加) - ポート8080で公開 ## ファイル構成 - tools/synology/ai-proxy/server.py - プロキシサーバー本体 - tools/synology/ai-proxy/requirements.txt - Python依存関係 - tools/synology/docker-compose.yml - ai-proxyサービス追加 - tools/synology/.env.example - 環境変数テンプレート - tools/synology/DEPLOYMENT_GUIDE.md - デプロイ手順書 ## セキュリティ - .env ファイルは.gitignoreで除外(APIキー保護) - 環境変数でAPIキー管理(コードに埋め込まない) 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
parent
3841cbb907
commit
a5353a9b50
|
|
@ -0,0 +1,17 @@
|
||||||
|
# ==========================================
|
||||||
|
# Ponshu Room AI Factory - 環境変数設定
|
||||||
|
# ==========================================
|
||||||
|
# このファイルを `.env` にコピーして、実際の値を設定してください。
|
||||||
|
#
|
||||||
|
# コピー方法:
|
||||||
|
# cp .env.example .env
|
||||||
|
#
|
||||||
|
# 重要: `.env` ファイルは絶対にGitにコミットしないでください!
|
||||||
|
|
||||||
|
# ----------------------------------------
|
||||||
|
# Gemini API設定
|
||||||
|
# ----------------------------------------
|
||||||
|
# 新しいGoogle AI Studioプロジェクト(Early Accessでない通常プロジェクト)で
|
||||||
|
# 作成したAPIキーを設定してください
|
||||||
|
# 取得方法: https://aistudio.google.com/ → "Get API key" → "Create API key in new project"
|
||||||
|
GEMINI_API_KEY=YOUR_GEMINI_API_KEY_HERE
|
||||||
|
|
@ -0,0 +1,27 @@
|
||||||
|
# ==========================================
|
||||||
|
# Synology AI Factory - Git除外設定
|
||||||
|
# ==========================================
|
||||||
|
# セキュリティ上重要: APIキーを含むファイルは絶対にコミットしない
|
||||||
|
|
||||||
|
# 環境変数ファイル(APIキーが含まれる)
|
||||||
|
.env
|
||||||
|
|
||||||
|
# データフォルダ(使用状況ログなど)
|
||||||
|
ai-proxy-data/
|
||||||
|
gitea/
|
||||||
|
postgres/
|
||||||
|
mcp/
|
||||||
|
|
||||||
|
# macOS
|
||||||
|
.DS_Store
|
||||||
|
|
||||||
|
# Windows
|
||||||
|
Thumbs.db
|
||||||
|
desktop.ini
|
||||||
|
|
||||||
|
# エディタ
|
||||||
|
.vscode/
|
||||||
|
.idea/
|
||||||
|
*.swp
|
||||||
|
*.swo
|
||||||
|
*~
|
||||||
|
|
@ -0,0 +1,298 @@
|
||||||
|
# Synology AI Proxy Server デプロイガイド
|
||||||
|
|
||||||
|
## 📋 前提条件
|
||||||
|
|
||||||
|
- ✅ Synology NASにDockerがインストールされている
|
||||||
|
- ✅ Container Managerでgitea環境が稼働している
|
||||||
|
- ✅ 新しいGoogle AI StudioプロジェクトでAPIキーを取得済み
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🚀 デプロイ手順
|
||||||
|
|
||||||
|
### ステップ1: ファイルをSynologyにアップロード
|
||||||
|
|
||||||
|
#### 方法A: File Station(推奨・簡単)
|
||||||
|
|
||||||
|
1. **Synology DSMにログイン**
|
||||||
|
- ブラウザで `http://[SynologyのIP]:5000` にアクセス
|
||||||
|
|
||||||
|
2. **File Stationを開く**
|
||||||
|
- メインメニュー → File Station
|
||||||
|
|
||||||
|
3. **プロジェクトフォルダに移動**
|
||||||
|
- `/docker/ponshu-ai-factory/` に移動
|
||||||
|
- (もしフォルダ名が違う場合は、Giteaのdocker-compose.ymlがある場所)
|
||||||
|
|
||||||
|
4. **新しいファイルをアップロード**
|
||||||
|
- `ai-proxy` フォルダを作成
|
||||||
|
- 以下のファイルをアップロード:
|
||||||
|
- `server.py`
|
||||||
|
- `requirements.txt`
|
||||||
|
- `README.md`
|
||||||
|
|
||||||
|
5. **docker-compose.ymlを更新**
|
||||||
|
- 既存の `docker-compose.yml` を新しいバージョンで上書き
|
||||||
|
|
||||||
|
6. **.envファイルを作成**
|
||||||
|
- `.env.example` を `.env` にコピー
|
||||||
|
- `.env` を編集して、`YOUR_GEMINI_API_KEY_HERE` を実際のAPIキーに置き換え
|
||||||
|
|
||||||
|
#### 方法B: Git(上級者向け)
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# PCのターミナルから
|
||||||
|
cd c:\Users\maita\posimai-project\ponshu_room_lite\tools\synology
|
||||||
|
|
||||||
|
# Giteaにpush(既にリポジトリがある場合)
|
||||||
|
git add .
|
||||||
|
git commit -m "feat: Add AI Proxy Server with rate limiting"
|
||||||
|
git push origin main
|
||||||
|
|
||||||
|
# SynologyにSSH接続
|
||||||
|
ssh maita@[SynologyのIP]
|
||||||
|
|
||||||
|
# プロジェクトフォルダに移動
|
||||||
|
cd /volume1/docker/ponshu-ai-factory
|
||||||
|
|
||||||
|
# Giteaからpull
|
||||||
|
git pull origin main
|
||||||
|
|
||||||
|
# .envファイルを作成
|
||||||
|
cp .env.example .env
|
||||||
|
nano .env # APIキーを設定
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### ステップ2: APIキーの設定
|
||||||
|
|
||||||
|
1. **Google AI Studioで新しいプロジェクトを作成**
|
||||||
|
- https://aistudio.google.com/ にアクセス
|
||||||
|
- 新しいプロジェクトを作成(**Early Accessでない通常プロジェクト**)
|
||||||
|
- "Get API key" → "Create API key in new project"
|
||||||
|
- APIキーをコピー
|
||||||
|
|
||||||
|
2. **Synologyの.envファイルに設定**
|
||||||
|
- File Stationで `/docker/ponshu-ai-factory/.env` を編集
|
||||||
|
- `GEMINI_API_KEY=実際のAPIキー` に変更
|
||||||
|
- 保存
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### ステップ3: Dockerコンテナを起動
|
||||||
|
|
||||||
|
#### Container Managerを使う場合(推奨)
|
||||||
|
|
||||||
|
1. **Container Managerを開く**
|
||||||
|
- DSMメインメニュー → Container Manager
|
||||||
|
|
||||||
|
2. **プロジェクトを停止**
|
||||||
|
- 「プロジェクト」タブ
|
||||||
|
- 既存の `ponshu-ai-factory` プロジェクトを選択
|
||||||
|
- 「停止」をクリック
|
||||||
|
|
||||||
|
3. **プロジェクトを再構築**
|
||||||
|
- 「アクション」→「ビルド」をクリック
|
||||||
|
- 数分待つ(Pythonの依存関係をインストール中)
|
||||||
|
|
||||||
|
4. **プロジェクトを起動**
|
||||||
|
- 「起動」をクリック
|
||||||
|
|
||||||
|
5. **コンテナの状態を確認**
|
||||||
|
- 「コンテナ」タブ
|
||||||
|
- `ai_proxy` コンテナが「実行中」になっているか確認
|
||||||
|
- ログを確認(「ログ」ボタン)して `🚀 AI Proxy Server started` が出ていればOK
|
||||||
|
|
||||||
|
#### SSHを使う場合(上級者向け)
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Synologyにログイン
|
||||||
|
ssh maita@[SynologyのIP]
|
||||||
|
|
||||||
|
# プロジェクトフォルダに移動
|
||||||
|
cd /volume1/docker/ponshu-ai-factory
|
||||||
|
|
||||||
|
# 既存のコンテナを停止
|
||||||
|
docker-compose down
|
||||||
|
|
||||||
|
# 新しいコンテナを起動
|
||||||
|
docker-compose up -d
|
||||||
|
|
||||||
|
# ログを確認
|
||||||
|
docker logs ai_proxy
|
||||||
|
|
||||||
|
# 正常に起動していれば以下が表示される:
|
||||||
|
# 🚀 AI Proxy Server started
|
||||||
|
# Model: gemini-1.5-flash
|
||||||
|
# Rate Limit: 10 requests/day
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### ステップ4: 動作確認
|
||||||
|
|
||||||
|
#### ブラウザでヘルスチェック
|
||||||
|
|
||||||
|
```
|
||||||
|
http://[SynologyのIP]:8080/health
|
||||||
|
```
|
||||||
|
|
||||||
|
**期待されるレスポンス:**
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"status": "healthy",
|
||||||
|
"gemini_api_key_configured": true,
|
||||||
|
"rate_limit": 10,
|
||||||
|
"model": "gemini-1.5-flash"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🔧 トラブルシューティング
|
||||||
|
|
||||||
|
### コンテナが起動しない
|
||||||
|
|
||||||
|
**原因1: .envファイルが見つからない**
|
||||||
|
- `.env.example` を `.env` にコピーしたか確認
|
||||||
|
- `.env` に正しいAPIキーが設定されているか確認
|
||||||
|
|
||||||
|
**原因2: ポート8080が既に使われている**
|
||||||
|
```bash
|
||||||
|
# ポート使用状況を確認
|
||||||
|
netstat -tuln | grep 8080
|
||||||
|
|
||||||
|
# 他のアプリが使っている場合は、docker-compose.ymlのポートを変更
|
||||||
|
# ports:
|
||||||
|
# - "8081:8080" # 外部8081 → コンテナ内8080
|
||||||
|
```
|
||||||
|
|
||||||
|
**原因3: Pythonの依存関係インストールに失敗**
|
||||||
|
```bash
|
||||||
|
# コンテナ内に入ってログ確認
|
||||||
|
docker exec -it ai_proxy bash
|
||||||
|
pip install -r requirements.txt
|
||||||
|
python server.py
|
||||||
|
```
|
||||||
|
|
||||||
|
### APIキーが認識されない
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 環境変数を確認
|
||||||
|
docker exec ai_proxy printenv | grep GEMINI
|
||||||
|
|
||||||
|
# GEMINI_API_KEY が空の場合は .env ファイルを確認
|
||||||
|
```
|
||||||
|
|
||||||
|
### アプリから接続できない
|
||||||
|
|
||||||
|
1. **ネットワーク確認**
|
||||||
|
- Synologyとスマホが同じネットワークにいるか
|
||||||
|
- ファイアウォールで8080ポートが開いているか
|
||||||
|
|
||||||
|
2. **IPアドレス確認**
|
||||||
|
```bash
|
||||||
|
# SynologyのIPを確認
|
||||||
|
ifconfig | grep inet
|
||||||
|
```
|
||||||
|
|
||||||
|
3. **アプリ側のURLを確認**
|
||||||
|
- `http://192.168.1.xxx:8080/analyze` のようになっているか
|
||||||
|
- `https://` ではなく `http://` を使用
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 📊 使用状況の確認
|
||||||
|
|
||||||
|
### 特定デバイスの使用状況を確認
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# デバイスIDを取得(アプリのログから)
|
||||||
|
DEVICE_ID="a1b2c3d4..." # SHA256ハッシュ
|
||||||
|
|
||||||
|
# ブラウザまたはcurlで確認
|
||||||
|
curl http://[SynologyのIP]:8080/usage/${DEVICE_ID}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 全デバイスの使用状況を確認
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Synologyにログイン
|
||||||
|
ssh maita@[SynologyのIP]
|
||||||
|
|
||||||
|
# 使用状況ファイルを確認
|
||||||
|
cat /volume1/docker/ponshu-ai-factory/ai-proxy-data/usage.json
|
||||||
|
```
|
||||||
|
|
||||||
|
**データ形式:**
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"a1b2c3d4e5f6...": {
|
||||||
|
"2026-01-15": 3,
|
||||||
|
"2026-01-14": 7
|
||||||
|
},
|
||||||
|
"f6e5d4c3b2a1...": {
|
||||||
|
"2026-01-15": 10
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🔐 セキュリティのベストプラクティス
|
||||||
|
|
||||||
|
1. **APIキーを絶対にGitにコミットしない**
|
||||||
|
- `.env` ファイルは `.gitignore` に追加済み
|
||||||
|
- 万が一コミットした場合は即座にAPIキーを再発行
|
||||||
|
|
||||||
|
2. **定期的にログをチェック**
|
||||||
|
```bash
|
||||||
|
docker logs ai_proxy | tail -50
|
||||||
|
```
|
||||||
|
|
||||||
|
3. **不正アクセスの監視**
|
||||||
|
- `usage.json` で異常な使用パターンを確認
|
||||||
|
- 知らないデバイスIDが大量に出ている場合は要調査
|
||||||
|
|
||||||
|
4. **ファイアウォール設定**
|
||||||
|
- Synology DSM → コントロールパネル → セキュリティ → ファイアウォール
|
||||||
|
- ポート8080はローカルネットワークのみ許可(外部には開放しない)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 📈 次のステップ
|
||||||
|
|
||||||
|
1. **アプリ側の実装**
|
||||||
|
- `lib/services/gemini_service.dart` をプロキシサーバー経由に変更
|
||||||
|
- デバイスID認証の実装
|
||||||
|
|
||||||
|
2. **Pro版の準備**
|
||||||
|
- `RATE_LIMIT_PER_DAY=999999` で無制限に変更
|
||||||
|
- 決済システムと連携
|
||||||
|
|
||||||
|
3. **モニタリング**
|
||||||
|
- Grafanaなどで使用状況をダッシュボード化
|
||||||
|
- アラート設定(異常なリクエスト数など)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 💡 よくある質問
|
||||||
|
|
||||||
|
**Q: 1日10回の制限はいつリセットされますか?**
|
||||||
|
A: 日本時間の0時(サーバー時刻基準)にリセットされます。
|
||||||
|
|
||||||
|
**Q: 複数のアプリバージョン(開発版/本番版)で使えますか?**
|
||||||
|
A: はい。デバイスIDが同じであれば共通のカウントになります。別々にしたい場合は、環境ごとにプロキシサーバーを分けてください。
|
||||||
|
|
||||||
|
**Q: Gemini APIのコストは誰が負担しますか?**
|
||||||
|
A: プロキシサーバーの管理者(あなた)が負担します。gemini-1.5-flashは1日1,500回まで無料なので、10ユーザー×10回/日=100回/日であれば無料枠内です。
|
||||||
|
|
||||||
|
**Q: レート制限を超えたらどうなりますか?**
|
||||||
|
A: アプリに「本日の解析上限に達しました。明日またお試しください」というメッセージが表示されます。サーバーはエラーを返しません。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🎉 完成!
|
||||||
|
|
||||||
|
これでAI Proxyサーバーの構築は完了です。次はアプリ側の実装に進みましょう!
|
||||||
|
|
@ -0,0 +1,104 @@
|
||||||
|
# Ponshu Room AI Proxy Server
|
||||||
|
|
||||||
|
## 概要
|
||||||
|
|
||||||
|
Ponshu Room Liteアプリのバックエンドプロキシサーバー。Gemini APIへのリクエストを中継し、以下の機能を提供します:
|
||||||
|
|
||||||
|
### 主な機能
|
||||||
|
|
||||||
|
1. **APIキー保護**
|
||||||
|
- アプリにAPIキーを埋め込まない
|
||||||
|
- サーバー側で環境変数として管理
|
||||||
|
|
||||||
|
2. **デバイスID認証**
|
||||||
|
- SHA256ハッシュ化されたデバイスIDで認証
|
||||||
|
- 未登録デバイスのアクセスを制限可能(将来実装)
|
||||||
|
|
||||||
|
3. **レート制限**
|
||||||
|
- 1デバイスあたり1日10回まで(Lite版)
|
||||||
|
- Pro版では無制限に拡張可能
|
||||||
|
|
||||||
|
4. **使用状況ログ**
|
||||||
|
- JSON形式で使用履歴を記録
|
||||||
|
- 30日以上前のデータは自動削除
|
||||||
|
|
||||||
|
## 技術スタック
|
||||||
|
|
||||||
|
- **言語**: Python 3.11
|
||||||
|
- **フレームワーク**: FastAPI
|
||||||
|
- **HTTPクライアント**: httpx
|
||||||
|
- **デプロイ**: Docker
|
||||||
|
|
||||||
|
## エンドポイント
|
||||||
|
|
||||||
|
### `GET /`
|
||||||
|
ヘルスチェック
|
||||||
|
|
||||||
|
### `GET /health`
|
||||||
|
詳細なヘルスチェック(APIキー設定状況など)
|
||||||
|
|
||||||
|
### `POST /analyze`
|
||||||
|
日本酒ラベル画像を解析
|
||||||
|
|
||||||
|
**リクエスト:**
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"device_id": "sha256ハッシュ(64文字)",
|
||||||
|
"images": ["base64エンコードされた画像1", "画像2", ...],
|
||||||
|
"prompt": "カスタムプロンプト(オプション)"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**レスポンス:**
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"success": true,
|
||||||
|
"data": {
|
||||||
|
"name": "獺祭 純米大吟醸",
|
||||||
|
"brand": "旭酒造",
|
||||||
|
"prefecture": "山口県",
|
||||||
|
...
|
||||||
|
},
|
||||||
|
"usage": {
|
||||||
|
"today": 3,
|
||||||
|
"limit": 10
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### `GET /usage/{device_id}`
|
||||||
|
デバイスの使用状況を取得
|
||||||
|
|
||||||
|
## 環境変数
|
||||||
|
|
||||||
|
| 変数名 | 説明 | デフォルト |
|
||||||
|
|--------|------|-----------|
|
||||||
|
| `GEMINI_API_KEY` | Gemini API Key | (必須) |
|
||||||
|
| `GEMINI_MODEL` | 使用するモデル | `gemini-1.5-flash` |
|
||||||
|
| `RATE_LIMIT_PER_DAY` | 1日あたりの制限 | `10` |
|
||||||
|
|
||||||
|
## データ保存
|
||||||
|
|
||||||
|
- 使用状況: `/app/data/usage.json`
|
||||||
|
- 30日以上前のデータは自動削除
|
||||||
|
|
||||||
|
## セキュリティ
|
||||||
|
|
||||||
|
- デバイスIDはSHA256ハッシュ化されたもののみ受け入れ
|
||||||
|
- CORS設定でオリジン制限可能
|
||||||
|
- APIキーは環境変数で管理(コードに埋め込まない)
|
||||||
|
|
||||||
|
## ローカルでのテスト
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 依存関係インストール
|
||||||
|
pip install -r requirements.txt
|
||||||
|
|
||||||
|
# 環境変数設定
|
||||||
|
export GEMINI_API_KEY="YOUR_API_KEY"
|
||||||
|
|
||||||
|
# サーバー起動
|
||||||
|
python server.py
|
||||||
|
```
|
||||||
|
|
||||||
|
http://localhost:8080 でアクセス可能
|
||||||
|
|
@ -0,0 +1,4 @@
|
||||||
|
fastapi==0.109.0
|
||||||
|
uvicorn[standard]==0.27.0
|
||||||
|
httpx==0.26.0
|
||||||
|
pydantic==2.5.3
|
||||||
|
|
@ -0,0 +1,351 @@
|
||||||
|
#!/usr/bin/env python3
|
||||||
|
"""
|
||||||
|
Ponshu Room AI Proxy Server
|
||||||
|
============================
|
||||||
|
役割: アプリからのリクエストを受け取り、Gemini APIに中継
|
||||||
|
機能:
|
||||||
|
- APIキーの保護(アプリにAPIキーを埋め込まない)
|
||||||
|
- デバイスID認証(許可されたデバイスのみアクセス可能)
|
||||||
|
- レート制限(1日10回まで/デバイス)
|
||||||
|
- 使用状況のログ記録
|
||||||
|
"""
|
||||||
|
|
||||||
|
import os
|
||||||
|
import json
|
||||||
|
import hashlib
|
||||||
|
from datetime import datetime, timedelta
|
||||||
|
from pathlib import Path
|
||||||
|
from typing import Dict, List, Optional
|
||||||
|
|
||||||
|
from fastapi import FastAPI, HTTPException, Request
|
||||||
|
from fastapi.middleware.cors import CORSMiddleware
|
||||||
|
from pydantic import BaseModel
|
||||||
|
import httpx
|
||||||
|
|
||||||
|
# ==========================================
|
||||||
|
# 設定
|
||||||
|
# ==========================================
|
||||||
|
GEMINI_API_KEY = os.getenv("GEMINI_API_KEY", "")
|
||||||
|
GEMINI_MODEL = os.getenv("GEMINI_MODEL", "gemini-1.5-flash")
|
||||||
|
RATE_LIMIT_PER_DAY = int(os.getenv("RATE_LIMIT_PER_DAY", "10"))
|
||||||
|
DATA_DIR = Path("/app/data")
|
||||||
|
USAGE_FILE = DATA_DIR / "usage.json"
|
||||||
|
|
||||||
|
# アプリ起動時にデータディレクトリを作成
|
||||||
|
DATA_DIR.mkdir(exist_ok=True)
|
||||||
|
|
||||||
|
# ==========================================
|
||||||
|
# FastAPI アプリ初期化
|
||||||
|
# ==========================================
|
||||||
|
app = FastAPI(
|
||||||
|
title="Ponshu Room AI Proxy",
|
||||||
|
description="Gemini API Proxy with Rate Limiting",
|
||||||
|
version="1.0.0"
|
||||||
|
)
|
||||||
|
|
||||||
|
# CORS設定(モバイルアプリからのリクエストを許可)
|
||||||
|
app.add_middleware(
|
||||||
|
CORSMiddleware,
|
||||||
|
allow_origins=["*"], # 本番環境では特定のオリジンに制限すべき
|
||||||
|
allow_credentials=True,
|
||||||
|
allow_methods=["*"],
|
||||||
|
allow_headers=["*"],
|
||||||
|
)
|
||||||
|
|
||||||
|
# ==========================================
|
||||||
|
# データモデル
|
||||||
|
# ==========================================
|
||||||
|
class AnalyzeRequest(BaseModel):
|
||||||
|
"""画像解析リクエスト"""
|
||||||
|
device_id: str # デバイスID(SHA256ハッシュ化されたもの)
|
||||||
|
images: List[str] # Base64エンコードされた画像データ
|
||||||
|
prompt: Optional[str] = None # カスタムプロンプト(オプション)
|
||||||
|
|
||||||
|
class AnalyzeResponse(BaseModel):
|
||||||
|
"""画像解析レスポンス"""
|
||||||
|
success: bool
|
||||||
|
data: Optional[Dict] = None
|
||||||
|
error: Optional[str] = None
|
||||||
|
usage: Dict[str, int] = {} # {"today": 3, "limit": 10}
|
||||||
|
|
||||||
|
# ==========================================
|
||||||
|
# 使用状況管理
|
||||||
|
# ==========================================
|
||||||
|
def load_usage_data() -> Dict:
|
||||||
|
"""使用状況データを読み込む"""
|
||||||
|
if not USAGE_FILE.exists():
|
||||||
|
return {}
|
||||||
|
|
||||||
|
try:
|
||||||
|
with open(USAGE_FILE, "r", encoding="utf-8") as f:
|
||||||
|
return json.load(f)
|
||||||
|
except Exception:
|
||||||
|
return {}
|
||||||
|
|
||||||
|
def save_usage_data(data: Dict) -> None:
|
||||||
|
"""使用状況データを保存"""
|
||||||
|
with open(USAGE_FILE, "w", encoding="utf-8") as f:
|
||||||
|
json.dump(data, f, indent=2, ensure_ascii=False)
|
||||||
|
|
||||||
|
def get_today_key() -> str:
|
||||||
|
"""今日の日付キー(YYYY-MM-DD)を取得"""
|
||||||
|
return datetime.now().strftime("%Y-%m-%d")
|
||||||
|
|
||||||
|
def check_rate_limit(device_id: str) -> tuple[bool, int]:
|
||||||
|
"""
|
||||||
|
レート制限をチェック
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
(制限内か, 今日の使用回数)
|
||||||
|
"""
|
||||||
|
usage_data = load_usage_data()
|
||||||
|
today = get_today_key()
|
||||||
|
|
||||||
|
# デバイスIDのエントリがない場合は作成
|
||||||
|
if device_id not in usage_data:
|
||||||
|
usage_data[device_id] = {}
|
||||||
|
|
||||||
|
# 今日の使用回数を取得
|
||||||
|
today_count = usage_data[device_id].get(today, 0)
|
||||||
|
|
||||||
|
# 制限チェック
|
||||||
|
is_within_limit = today_count < RATE_LIMIT_PER_DAY
|
||||||
|
|
||||||
|
return is_within_limit, today_count
|
||||||
|
|
||||||
|
def increment_usage(device_id: str) -> int:
|
||||||
|
"""
|
||||||
|
使用回数をインクリメント
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
更新後の今日の使用回数
|
||||||
|
"""
|
||||||
|
usage_data = load_usage_data()
|
||||||
|
today = get_today_key()
|
||||||
|
|
||||||
|
if device_id not in usage_data:
|
||||||
|
usage_data[device_id] = {}
|
||||||
|
|
||||||
|
usage_data[device_id][today] = usage_data[device_id].get(today, 0) + 1
|
||||||
|
|
||||||
|
save_usage_data(usage_data)
|
||||||
|
|
||||||
|
return usage_data[device_id][today]
|
||||||
|
|
||||||
|
def cleanup_old_usage_data() -> None:
|
||||||
|
"""30日以上前のデータを削除(データサイズ管理)"""
|
||||||
|
usage_data = load_usage_data()
|
||||||
|
cutoff_date = (datetime.now() - timedelta(days=30)).strftime("%Y-%m-%d")
|
||||||
|
|
||||||
|
for device_id in usage_data:
|
||||||
|
dates_to_remove = [
|
||||||
|
date for date in usage_data[device_id]
|
||||||
|
if date < cutoff_date
|
||||||
|
]
|
||||||
|
for date in dates_to_remove:
|
||||||
|
del usage_data[device_id][date]
|
||||||
|
|
||||||
|
save_usage_data(usage_data)
|
||||||
|
|
||||||
|
# ==========================================
|
||||||
|
# APIエンドポイント
|
||||||
|
# ==========================================
|
||||||
|
@app.get("/")
|
||||||
|
async def root():
|
||||||
|
"""ヘルスチェック"""
|
||||||
|
return {
|
||||||
|
"service": "Ponshu Room AI Proxy",
|
||||||
|
"status": "running",
|
||||||
|
"version": "1.0.0"
|
||||||
|
}
|
||||||
|
|
||||||
|
@app.get("/health")
|
||||||
|
async def health_check():
|
||||||
|
"""詳細なヘルスチェック"""
|
||||||
|
return {
|
||||||
|
"status": "healthy",
|
||||||
|
"gemini_api_key_configured": bool(GEMINI_API_KEY),
|
||||||
|
"rate_limit": RATE_LIMIT_PER_DAY,
|
||||||
|
"model": GEMINI_MODEL
|
||||||
|
}
|
||||||
|
|
||||||
|
@app.post("/analyze", response_model=AnalyzeResponse)
|
||||||
|
async def analyze_sake_label(request: AnalyzeRequest):
|
||||||
|
"""
|
||||||
|
日本酒ラベル画像を解析
|
||||||
|
|
||||||
|
レート制限: 1デバイスあたり1日10回まで
|
||||||
|
"""
|
||||||
|
|
||||||
|
# 1. APIキーのチェック
|
||||||
|
if not GEMINI_API_KEY:
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=500,
|
||||||
|
detail="Gemini APIキーが設定されていません"
|
||||||
|
)
|
||||||
|
|
||||||
|
# 2. デバイスIDの検証(SHA256形式かチェック)
|
||||||
|
if len(request.device_id) != 64: # SHA256は64文字
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=400,
|
||||||
|
detail="無効なデバイスIDです"
|
||||||
|
)
|
||||||
|
|
||||||
|
# 3. レート制限チェック
|
||||||
|
is_within_limit, today_count = check_rate_limit(request.device_id)
|
||||||
|
|
||||||
|
if not is_within_limit:
|
||||||
|
return AnalyzeResponse(
|
||||||
|
success=False,
|
||||||
|
error=f"本日の解析上限({RATE_LIMIT_PER_DAY}回)に達しました。明日またお試しください。",
|
||||||
|
usage={
|
||||||
|
"today": today_count,
|
||||||
|
"limit": RATE_LIMIT_PER_DAY
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
# 4. Gemini APIにリクエストを送信
|
||||||
|
try:
|
||||||
|
# プロンプトのデフォルト値
|
||||||
|
prompt = request.prompt or """
|
||||||
|
この日本酒のラベル画像(複数枚ある場合は表・裏など)を分析してください。
|
||||||
|
全ての画像から情報を統合し、以下の情報をJSON形式で返してください:
|
||||||
|
|
||||||
|
{
|
||||||
|
"name": "銘柄名(例:獺祭 純米大吟醸)",
|
||||||
|
"brand": "蔵元名(例:旭酒造)",
|
||||||
|
"prefecture": "都道府県名(例:山口県)",
|
||||||
|
"type": "種類(例:純米大吟醸)",
|
||||||
|
"description": "この日本酒の特徴を100文字程度で説明(裏ラベルの情報があれば活用してください)",
|
||||||
|
"catchCopy": "この日本酒を一言で表現する詩的なキャッチコピー(20文字以内)",
|
||||||
|
"confidenceScore": 0から100の整数,
|
||||||
|
"flavorTags": ["フルーティ", "辛口"],
|
||||||
|
"tasteStats": {
|
||||||
|
"aroma": 3,
|
||||||
|
"sweetness": 3,
|
||||||
|
"acidity": 3,
|
||||||
|
"bitterness": 3,
|
||||||
|
"body": 3
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
**tasteStatsの説明 (1-5の整数)**:
|
||||||
|
- aroma: 香りの強さ
|
||||||
|
- sweetness: 甘み
|
||||||
|
- acidity: 酸味
|
||||||
|
- bitterness: ビター感/キレ
|
||||||
|
- body: コク・ボディ
|
||||||
|
|
||||||
|
読み取れない情報は null を返してください。
|
||||||
|
JSONのみを返し、他の文章は含めないでください。
|
||||||
|
"""
|
||||||
|
|
||||||
|
# Gemini APIエンドポイント
|
||||||
|
gemini_url = f"https://generativelanguage.googleapis.com/v1beta/models/{GEMINI_MODEL}:generateContent"
|
||||||
|
|
||||||
|
# リクエストボディの構築
|
||||||
|
parts = [{"text": prompt}]
|
||||||
|
for image_data in request.images:
|
||||||
|
parts.append({
|
||||||
|
"inline_data": {
|
||||||
|
"mime_type": "image/jpeg",
|
||||||
|
"data": image_data
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
gemini_request = {
|
||||||
|
"contents": [{
|
||||||
|
"parts": parts
|
||||||
|
}],
|
||||||
|
"safetySettings": [
|
||||||
|
{"category": "HARM_CATEGORY_HARASSMENT", "threshold": "BLOCK_NONE"},
|
||||||
|
{"category": "HARM_CATEGORY_HATE_SPEECH", "threshold": "BLOCK_NONE"}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
# HTTPクライアントでリクエスト送信
|
||||||
|
async with httpx.AsyncClient(timeout=30.0) as client:
|
||||||
|
response = await client.post(
|
||||||
|
gemini_url,
|
||||||
|
json=gemini_request,
|
||||||
|
params={"key": GEMINI_API_KEY}
|
||||||
|
)
|
||||||
|
response.raise_for_status()
|
||||||
|
|
||||||
|
gemini_response = response.json()
|
||||||
|
|
||||||
|
# レスポンスから結果を抽出
|
||||||
|
if "candidates" not in gemini_response or not gemini_response["candidates"]:
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=500,
|
||||||
|
detail="Gemini APIからの応答が不正です"
|
||||||
|
)
|
||||||
|
|
||||||
|
text_result = gemini_response["candidates"][0]["content"]["parts"][0]["text"]
|
||||||
|
|
||||||
|
# JSONをパース(マークダウンのコードブロックを削除)
|
||||||
|
text_result = text_result.strip().replace("```json", "").replace("```", "").strip()
|
||||||
|
result_json = json.loads(text_result)
|
||||||
|
|
||||||
|
# 5. 使用回数をインクリメント
|
||||||
|
new_count = increment_usage(request.device_id)
|
||||||
|
|
||||||
|
# 6. 成功レスポンス
|
||||||
|
return AnalyzeResponse(
|
||||||
|
success=True,
|
||||||
|
data=result_json,
|
||||||
|
usage={
|
||||||
|
"today": new_count,
|
||||||
|
"limit": RATE_LIMIT_PER_DAY
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
except httpx.HTTPStatusError as e:
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=e.response.status_code,
|
||||||
|
detail=f"Gemini API error: {e.response.text}"
|
||||||
|
)
|
||||||
|
except json.JSONDecodeError:
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=500,
|
||||||
|
detail="Gemini APIからのJSON解析に失敗しました"
|
||||||
|
)
|
||||||
|
except Exception as e:
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=500,
|
||||||
|
detail=f"予期しないエラー: {str(e)}"
|
||||||
|
)
|
||||||
|
|
||||||
|
@app.get("/usage/{device_id}")
|
||||||
|
async def get_usage(device_id: str):
|
||||||
|
"""デバイスの使用状況を取得"""
|
||||||
|
if len(device_id) != 64:
|
||||||
|
raise HTTPException(status_code=400, detail="無効なデバイスIDです")
|
||||||
|
|
||||||
|
_, today_count = check_rate_limit(device_id)
|
||||||
|
|
||||||
|
return {
|
||||||
|
"device_id": device_id[:8] + "...", # プライバシー保護のため一部のみ表示
|
||||||
|
"today": today_count,
|
||||||
|
"limit": RATE_LIMIT_PER_DAY,
|
||||||
|
"remaining": max(0, RATE_LIMIT_PER_DAY - today_count),
|
||||||
|
"date": get_today_key()
|
||||||
|
}
|
||||||
|
|
||||||
|
# ==========================================
|
||||||
|
# 起動時処理
|
||||||
|
# ==========================================
|
||||||
|
@app.on_event("startup")
|
||||||
|
async def startup_event():
|
||||||
|
"""サーバー起動時に古いデータをクリーンアップ"""
|
||||||
|
cleanup_old_usage_data()
|
||||||
|
print(f"🚀 AI Proxy Server started")
|
||||||
|
print(f" Model: {GEMINI_MODEL}")
|
||||||
|
print(f" Rate Limit: {RATE_LIMIT_PER_DAY} requests/day")
|
||||||
|
|
||||||
|
# ==========================================
|
||||||
|
# メイン実行
|
||||||
|
# ==========================================
|
||||||
|
if __name__ == "__main__":
|
||||||
|
import uvicorn
|
||||||
|
uvicorn.run(app, host="0.0.0.0", port=8080)
|
||||||
|
|
@ -74,6 +74,32 @@ services:
|
||||||
networks:
|
networks:
|
||||||
- gitea_network
|
- gitea_network
|
||||||
|
|
||||||
|
# ----------------------------------------
|
||||||
|
# 4. AI Proxy Server (App Backend)
|
||||||
|
# 役割: Ponshu Room Liteアプリからのリクエストを受け取り、Gemini APIに中継
|
||||||
|
# - APIキー保護: アプリにAPIキーを埋め込まない
|
||||||
|
# - レート制限: 1デバイスあたり1日10回まで(Lite版)
|
||||||
|
# - デバイスID認証: SHA256ハッシュ化されたIDで管理
|
||||||
|
# ----------------------------------------
|
||||||
|
ai-proxy:
|
||||||
|
image: python:3.11-slim
|
||||||
|
container_name: ai_proxy
|
||||||
|
working_dir: /app
|
||||||
|
volumes:
|
||||||
|
- ./ai-proxy:/app # AI Proxyサーバーのコード
|
||||||
|
- ./ai-proxy-data:/app/data # 使用状況データの永続化
|
||||||
|
environment:
|
||||||
|
- GEMINI_API_KEY=${GEMINI_API_KEY} # 【重要】APIキーを.envファイルに設定してください
|
||||||
|
- GEMINI_MODEL=gemini-2.5-flash # 一旦2.5を使用(RPD 20制限あり)。1.5にアクセスできたら変更
|
||||||
|
- RATE_LIMIT_PER_DAY=10
|
||||||
|
ports:
|
||||||
|
- "8080:8080" # アプリからアクセスするポート
|
||||||
|
# 初回起動時に依存関係をインストールしてサーバー起動
|
||||||
|
command: sh -c "pip install --no-cache-dir -r requirements.txt && python server.py"
|
||||||
|
restart: always
|
||||||
|
networks:
|
||||||
|
- gitea_network
|
||||||
|
|
||||||
# ----------------------------------------
|
# ----------------------------------------
|
||||||
# Network Setting
|
# Network Setting
|
||||||
# 内部通信用の専用ネットワーク
|
# 内部通信用の専用ネットワーク
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue