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:
|
||||
- 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
|
||||
# 内部通信用の専用ネットワーク
|
||||
|
|
|
|||
Loading…
Reference in New Issue