✨ 添加去水印API服务 - MVP版本
新增功能: - 精简的API服务实现(api_service_mvp.py) - 专注单一功能:去水印 - 使用LaMa模型 - API Key认证 - 完整的错误处理和日志 - 完整的部署方案 - Docker配置(APIDockerfile) - Docker Compose配置(docker-compose.mvp.yml) - Nginx反向代理配置 - 详尽的文档 - API_SERVICE_GUIDE.md - MVP到商业化完整方案 - API_SERVICE_README.md - 快速开始指南 - API_CLIENT_EXAMPLES.md - 多语言客户端示例(Python/JS/cURL/PHP/Java/Go) 架构特点: - 遵循MVP和KISS原则 - 提供从单机到Kubernetes的扩展路径 - 包含成本分析��收益模型 - 完整的监控和告警方案 🎯 适用场景: - 个人/小团队快速验证产品(月成本¥300-500) - 中小型商业化部署(月成本¥1000-3000) - 大规模生产环境(月成本¥5000+) 🔧 Generated with Claude Code
This commit is contained in:
776
API_CLIENT_EXAMPLES.md
Normal file
776
API_CLIENT_EXAMPLES.md
Normal file
@@ -0,0 +1,776 @@
|
|||||||
|
# IOPaint API 客户端示例
|
||||||
|
|
||||||
|
本文档提供多种编程语言的API调用示例。
|
||||||
|
|
||||||
|
## 目录
|
||||||
|
- [Python](#python)
|
||||||
|
- [JavaScript/Node.js](#javascriptnodejs)
|
||||||
|
- [cURL](#curl)
|
||||||
|
- [PHP](#php)
|
||||||
|
- [Java](#java)
|
||||||
|
- [Go](#go)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 配置信息
|
||||||
|
|
||||||
|
```bash
|
||||||
|
API_URL=http://localhost:8080
|
||||||
|
API_KEY=your_secret_key_change_me
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Python
|
||||||
|
|
||||||
|
### 基础示例
|
||||||
|
|
||||||
|
```python
|
||||||
|
import requests
|
||||||
|
|
||||||
|
def remove_watermark(image_path, mask_path=None, api_key="your_secret_key_change_me"):
|
||||||
|
"""去除图片水印"""
|
||||||
|
url = "http://localhost:8080/api/v1/remove-watermark"
|
||||||
|
headers = {"X-API-Key": api_key}
|
||||||
|
|
||||||
|
files = {
|
||||||
|
"image": open(image_path, "rb")
|
||||||
|
}
|
||||||
|
|
||||||
|
if mask_path:
|
||||||
|
files["mask"] = open(mask_path, "rb")
|
||||||
|
|
||||||
|
response = requests.post(url, headers=headers, files=files)
|
||||||
|
|
||||||
|
if response.status_code == 200:
|
||||||
|
# 保存结果
|
||||||
|
output_path = "result.png"
|
||||||
|
with open(output_path, "wb") as f:
|
||||||
|
f.write(response.content)
|
||||||
|
print(f"✓ 处理成功!结果已保存到: {output_path}")
|
||||||
|
print(f"处理时间: {response.headers.get('X-Processing-Time')}秒")
|
||||||
|
return output_path
|
||||||
|
else:
|
||||||
|
print(f"✗ 处理失败: {response.status_code}")
|
||||||
|
print(response.json())
|
||||||
|
return None
|
||||||
|
|
||||||
|
# 使用示例
|
||||||
|
remove_watermark("input.jpg")
|
||||||
|
```
|
||||||
|
|
||||||
|
### 高级示例(含错误处理和重试)
|
||||||
|
|
||||||
|
```python
|
||||||
|
import requests
|
||||||
|
import time
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
class IOPaintClient:
|
||||||
|
"""IOPaint API客户端"""
|
||||||
|
|
||||||
|
def __init__(self, api_url="http://localhost:8080", api_key=None):
|
||||||
|
self.api_url = api_url.rstrip("/")
|
||||||
|
self.api_key = api_key
|
||||||
|
self.session = requests.Session()
|
||||||
|
self.session.headers.update({"X-API-Key": api_key})
|
||||||
|
|
||||||
|
def health_check(self):
|
||||||
|
"""健康检查"""
|
||||||
|
response = self.session.get(f"{self.api_url}/api/v1/health")
|
||||||
|
return response.json()
|
||||||
|
|
||||||
|
def get_stats(self):
|
||||||
|
"""获取使用统计"""
|
||||||
|
response = self.session.get(f"{self.api_url}/api/v1/stats")
|
||||||
|
return response.json()
|
||||||
|
|
||||||
|
def remove_watermark(
|
||||||
|
self,
|
||||||
|
image_path,
|
||||||
|
mask_path=None,
|
||||||
|
output_path=None,
|
||||||
|
max_retries=3,
|
||||||
|
timeout=120
|
||||||
|
):
|
||||||
|
"""
|
||||||
|
去除图片水印
|
||||||
|
|
||||||
|
参数:
|
||||||
|
image_path: 输入图片路径
|
||||||
|
mask_path: 遮罩图片路径(可选)
|
||||||
|
output_path: 输出路径(可选,默认为input_result.png)
|
||||||
|
max_retries: 最大重试次数
|
||||||
|
timeout: 超时时间(秒)
|
||||||
|
|
||||||
|
返回:
|
||||||
|
成功返回输出路径,失败返回None
|
||||||
|
"""
|
||||||
|
# 准备文件
|
||||||
|
files = {"image": open(image_path, "rb")}
|
||||||
|
if mask_path:
|
||||||
|
files["mask"] = open(mask_path, "rb")
|
||||||
|
|
||||||
|
# 确定输出路径
|
||||||
|
if output_path is None:
|
||||||
|
input_path = Path(image_path)
|
||||||
|
output_path = input_path.parent / f"{input_path.stem}_result.png"
|
||||||
|
|
||||||
|
# 重试逻辑
|
||||||
|
for attempt in range(max_retries):
|
||||||
|
try:
|
||||||
|
response = self.session.post(
|
||||||
|
f"{self.api_url}/api/v1/remove-watermark",
|
||||||
|
files=files,
|
||||||
|
timeout=timeout
|
||||||
|
)
|
||||||
|
|
||||||
|
if response.status_code == 200:
|
||||||
|
# 保存结果
|
||||||
|
with open(output_path, "wb") as f:
|
||||||
|
f.write(response.content)
|
||||||
|
|
||||||
|
print(f"✓ 处理成功!")
|
||||||
|
print(f" 输出: {output_path}")
|
||||||
|
print(f" 处理时间: {response.headers.get('X-Processing-Time')}秒")
|
||||||
|
print(f" 图片尺寸: {response.headers.get('X-Image-Size')}")
|
||||||
|
return str(output_path)
|
||||||
|
|
||||||
|
elif response.status_code == 429:
|
||||||
|
# 限流,等待后重试
|
||||||
|
wait_time = 2 ** attempt
|
||||||
|
print(f"⚠ 请求过于频繁,等待{wait_time}秒后重试...")
|
||||||
|
time.sleep(wait_time)
|
||||||
|
continue
|
||||||
|
|
||||||
|
else:
|
||||||
|
# 其他错误
|
||||||
|
print(f"✗ 处理失败 ({response.status_code})")
|
||||||
|
error_data = response.json()
|
||||||
|
print(f" 错误: {error_data.get('detail', '未知错误')}")
|
||||||
|
return None
|
||||||
|
|
||||||
|
except requests.Timeout:
|
||||||
|
print(f"⚠ 请求超时 (尝试 {attempt + 1}/{max_retries})")
|
||||||
|
if attempt < max_retries - 1:
|
||||||
|
continue
|
||||||
|
else:
|
||||||
|
print("✗ 超过最大重试次数")
|
||||||
|
return None
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
print(f"✗ 发生错误: {e}")
|
||||||
|
return None
|
||||||
|
|
||||||
|
finally:
|
||||||
|
# 关闭文件
|
||||||
|
for f in files.values():
|
||||||
|
if hasattr(f, 'close'):
|
||||||
|
f.close()
|
||||||
|
|
||||||
|
return None
|
||||||
|
|
||||||
|
def batch_process(self, image_dir, output_dir=None, mask_dir=None):
|
||||||
|
"""
|
||||||
|
批量处理图片
|
||||||
|
|
||||||
|
参数:
|
||||||
|
image_dir: 输入图片目录
|
||||||
|
output_dir: 输出目录(可选)
|
||||||
|
mask_dir: 遮罩目录(可选,按文件名匹配)
|
||||||
|
"""
|
||||||
|
image_dir = Path(image_dir)
|
||||||
|
if output_dir:
|
||||||
|
output_dir = Path(output_dir)
|
||||||
|
output_dir.mkdir(exist_ok=True, parents=True)
|
||||||
|
|
||||||
|
# 支持的图片格式
|
||||||
|
image_exts = {".jpg", ".jpeg", ".png", ".webp"}
|
||||||
|
images = [
|
||||||
|
f for f in image_dir.iterdir()
|
||||||
|
if f.suffix.lower() in image_exts
|
||||||
|
]
|
||||||
|
|
||||||
|
print(f"找到 {len(images)} 张图片")
|
||||||
|
|
||||||
|
results = {"success": 0, "failed": 0}
|
||||||
|
for i, image_path in enumerate(images, 1):
|
||||||
|
print(f"\n[{i}/{len(images)}] 处理: {image_path.name}")
|
||||||
|
|
||||||
|
# 查找对应的遮罩
|
||||||
|
mask_path = None
|
||||||
|
if mask_dir:
|
||||||
|
mask_path = Path(mask_dir) / image_path.name
|
||||||
|
if not mask_path.exists():
|
||||||
|
mask_path = None
|
||||||
|
|
||||||
|
# 确定输出路径
|
||||||
|
if output_dir:
|
||||||
|
out_path = output_dir / f"{image_path.stem}_result.png"
|
||||||
|
else:
|
||||||
|
out_path = image_path.parent / f"{image_path.stem}_result.png"
|
||||||
|
|
||||||
|
# 处理
|
||||||
|
result = self.remove_watermark(image_path, mask_path, out_path)
|
||||||
|
if result:
|
||||||
|
results["success"] += 1
|
||||||
|
else:
|
||||||
|
results["failed"] += 1
|
||||||
|
|
||||||
|
# 总结
|
||||||
|
print("\n" + "=" * 60)
|
||||||
|
print(f"批量处理完成!")
|
||||||
|
print(f" 成功: {results['success']}")
|
||||||
|
print(f" 失败: {results['failed']}")
|
||||||
|
print("=" * 60)
|
||||||
|
|
||||||
|
# 使用示例
|
||||||
|
if __name__ == "__main__":
|
||||||
|
# 创建客户端
|
||||||
|
client = IOPaintClient(
|
||||||
|
api_url="http://localhost:8080",
|
||||||
|
api_key="your_secret_key_change_me"
|
||||||
|
)
|
||||||
|
|
||||||
|
# 健康检查
|
||||||
|
print("健康检查:", client.health_check())
|
||||||
|
|
||||||
|
# 单张图片处理
|
||||||
|
client.remove_watermark("test.jpg")
|
||||||
|
|
||||||
|
# 批量处理
|
||||||
|
client.batch_process("./input_images", "./output_images")
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## JavaScript/Node.js
|
||||||
|
|
||||||
|
### 使用 axios
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
const axios = require('axios');
|
||||||
|
const FormData = require('form-data');
|
||||||
|
const fs = require('fs');
|
||||||
|
|
||||||
|
async function removeWatermark(imagePath, maskPath = null, apiKey = 'your_secret_key_change_me') {
|
||||||
|
const url = 'http://localhost:8080/api/v1/remove-watermark';
|
||||||
|
|
||||||
|
const formData = new FormData();
|
||||||
|
formData.append('image', fs.createReadStream(imagePath));
|
||||||
|
|
||||||
|
if (maskPath) {
|
||||||
|
formData.append('mask', fs.createReadStream(maskPath));
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await axios.post(url, formData, {
|
||||||
|
headers: {
|
||||||
|
'X-API-Key': apiKey,
|
||||||
|
...formData.getHeaders()
|
||||||
|
},
|
||||||
|
responseType: 'arraybuffer',
|
||||||
|
timeout: 120000 // 120秒超时
|
||||||
|
});
|
||||||
|
|
||||||
|
// 保存结果
|
||||||
|
const outputPath = 'result.png';
|
||||||
|
fs.writeFileSync(outputPath, response.data);
|
||||||
|
|
||||||
|
console.log('✓ 处理成功!');
|
||||||
|
console.log(` 输出: ${outputPath}`);
|
||||||
|
console.log(` 处理时间: ${response.headers['x-processing-time']}秒`);
|
||||||
|
|
||||||
|
return outputPath;
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
if (error.response) {
|
||||||
|
console.error('✗ 处理失败:', error.response.status);
|
||||||
|
console.error(' 错误:', error.response.data.toString());
|
||||||
|
} else {
|
||||||
|
console.error('✗ 请求失败:', error.message);
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 使用示例
|
||||||
|
removeWatermark('input.jpg');
|
||||||
|
```
|
||||||
|
|
||||||
|
### 完整客户端类
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
const axios = require('axios');
|
||||||
|
const FormData = require('form-data');
|
||||||
|
const fs = require('fs');
|
||||||
|
const path = require('path');
|
||||||
|
|
||||||
|
class IOPaintClient {
|
||||||
|
constructor(apiUrl = 'http://localhost:8080', apiKey = null) {
|
||||||
|
this.apiUrl = apiUrl.replace(/\/$/, '');
|
||||||
|
this.apiKey = apiKey;
|
||||||
|
this.client = axios.create({
|
||||||
|
baseURL: this.apiUrl,
|
||||||
|
headers: {
|
||||||
|
'X-API-Key': apiKey
|
||||||
|
},
|
||||||
|
timeout: 120000
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async healthCheck() {
|
||||||
|
const response = await this.client.get('/api/v1/health');
|
||||||
|
return response.data;
|
||||||
|
}
|
||||||
|
|
||||||
|
async getStats() {
|
||||||
|
const response = await this.client.get('/api/v1/stats');
|
||||||
|
return response.data;
|
||||||
|
}
|
||||||
|
|
||||||
|
async removeWatermark(imagePath, maskPath = null, outputPath = null) {
|
||||||
|
const formData = new FormData();
|
||||||
|
formData.append('image', fs.createReadStream(imagePath));
|
||||||
|
|
||||||
|
if (maskPath) {
|
||||||
|
formData.append('mask', fs.createReadStream(maskPath));
|
||||||
|
}
|
||||||
|
|
||||||
|
// 确定输出路径
|
||||||
|
if (!outputPath) {
|
||||||
|
const parsed = path.parse(imagePath);
|
||||||
|
outputPath = path.join(parsed.dir, `${parsed.name}_result.png`);
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await this.client.post('/api/v1/remove-watermark', formData, {
|
||||||
|
headers: formData.getHeaders(),
|
||||||
|
responseType: 'arraybuffer'
|
||||||
|
});
|
||||||
|
|
||||||
|
fs.writeFileSync(outputPath, response.data);
|
||||||
|
|
||||||
|
console.log('✓ 处理成功!');
|
||||||
|
console.log(` 输出: ${outputPath}`);
|
||||||
|
console.log(` 处理时间: ${response.headers['x-processing-time']}秒`);
|
||||||
|
|
||||||
|
return outputPath;
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
if (error.response) {
|
||||||
|
console.error('✗ 处理失败:', error.response.status);
|
||||||
|
} else {
|
||||||
|
console.error('✗ 请求失败:', error.message);
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 使用示例
|
||||||
|
(async () => {
|
||||||
|
const client = new IOPaintClient('http://localhost:8080', 'your_secret_key_change_me');
|
||||||
|
|
||||||
|
// 健康检查
|
||||||
|
const health = await client.healthCheck();
|
||||||
|
console.log('健康检查:', health);
|
||||||
|
|
||||||
|
// 处理图片
|
||||||
|
await client.removeWatermark('test.jpg');
|
||||||
|
})();
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## cURL
|
||||||
|
|
||||||
|
### 基础使用
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 简单调用
|
||||||
|
curl -X POST http://localhost:8080/api/v1/remove-watermark \
|
||||||
|
-H "X-API-Key: your_secret_key_change_me" \
|
||||||
|
-F "image=@input.jpg" \
|
||||||
|
-o result.png
|
||||||
|
|
||||||
|
# 带遮罩
|
||||||
|
curl -X POST http://localhost:8080/api/v1/remove-watermark \
|
||||||
|
-H "X-API-Key: your_secret_key_change_me" \
|
||||||
|
-F "image=@input.jpg" \
|
||||||
|
-F "mask=@mask.png" \
|
||||||
|
-o result.png
|
||||||
|
|
||||||
|
# 显示详细信息
|
||||||
|
curl -X POST http://localhost:8080/api/v1/remove-watermark \
|
||||||
|
-H "X-API-Key: your_secret_key_change_me" \
|
||||||
|
-F "image=@input.jpg" \
|
||||||
|
-o result.png \
|
||||||
|
-v
|
||||||
|
|
||||||
|
# 健康检查
|
||||||
|
curl http://localhost:8080/api/v1/health
|
||||||
|
```
|
||||||
|
|
||||||
|
### Bash脚本批量处理
|
||||||
|
|
||||||
|
```bash
|
||||||
|
#!/bin/bash
|
||||||
|
|
||||||
|
API_URL="http://localhost:8080/api/v1/remove-watermark"
|
||||||
|
API_KEY="your_secret_key_change_me"
|
||||||
|
INPUT_DIR="./input"
|
||||||
|
OUTPUT_DIR="./output"
|
||||||
|
|
||||||
|
mkdir -p "$OUTPUT_DIR"
|
||||||
|
|
||||||
|
for image in "$INPUT_DIR"/*.{jpg,jpeg,png}; do
|
||||||
|
[ -f "$image" ] || continue
|
||||||
|
|
||||||
|
filename=$(basename "$image")
|
||||||
|
name="${filename%.*}"
|
||||||
|
output="$OUTPUT_DIR/${name}_result.png"
|
||||||
|
|
||||||
|
echo "处理: $filename"
|
||||||
|
|
||||||
|
curl -X POST "$API_URL" \
|
||||||
|
-H "X-API-Key: $API_KEY" \
|
||||||
|
-F "image=@$image" \
|
||||||
|
-o "$output" \
|
||||||
|
-s -w "状态码: %{http_code}, 时间: %{time_total}s\n"
|
||||||
|
done
|
||||||
|
|
||||||
|
echo "批量处理完成!"
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## PHP
|
||||||
|
|
||||||
|
```php
|
||||||
|
<?php
|
||||||
|
|
||||||
|
class IOPaintClient {
|
||||||
|
private $apiUrl;
|
||||||
|
private $apiKey;
|
||||||
|
|
||||||
|
public function __construct($apiUrl = 'http://localhost:8080', $apiKey = null) {
|
||||||
|
$this->apiUrl = rtrim($apiUrl, '/');
|
||||||
|
$this->apiKey = $apiKey;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function healthCheck() {
|
||||||
|
$ch = curl_init($this->apiUrl . '/api/v1/health');
|
||||||
|
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
|
||||||
|
$response = curl_exec($ch);
|
||||||
|
curl_close($ch);
|
||||||
|
return json_decode($response, true);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function removeWatermark($imagePath, $maskPath = null, $outputPath = null) {
|
||||||
|
$url = $this->apiUrl . '/api/v1/remove-watermark';
|
||||||
|
|
||||||
|
// 准备文件
|
||||||
|
$postData = [
|
||||||
|
'image' => new CURLFile($imagePath)
|
||||||
|
];
|
||||||
|
|
||||||
|
if ($maskPath) {
|
||||||
|
$postData['mask'] = new CURLFile($maskPath);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 确定输出路径
|
||||||
|
if (!$outputPath) {
|
||||||
|
$pathInfo = pathinfo($imagePath);
|
||||||
|
$outputPath = $pathInfo['dirname'] . '/' . $pathInfo['filename'] . '_result.png';
|
||||||
|
}
|
||||||
|
|
||||||
|
// 发送请求
|
||||||
|
$ch = curl_init($url);
|
||||||
|
curl_setopt($ch, CURLOPT_POST, true);
|
||||||
|
curl_setopt($ch, CURLOPT_POSTFIELDS, $postData);
|
||||||
|
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
|
||||||
|
curl_setopt($ch, CURLOPT_HTTPHEADER, [
|
||||||
|
'X-API-Key: ' . $this->apiKey
|
||||||
|
]);
|
||||||
|
curl_setopt($ch, CURLOPT_TIMEOUT, 120);
|
||||||
|
|
||||||
|
$response = curl_exec($ch);
|
||||||
|
$httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
|
||||||
|
curl_close($ch);
|
||||||
|
|
||||||
|
if ($httpCode == 200) {
|
||||||
|
file_put_contents($outputPath, $response);
|
||||||
|
echo "✓ 处理成功!输出: $outputPath\n";
|
||||||
|
return $outputPath;
|
||||||
|
} else {
|
||||||
|
echo "✗ 处理失败 (HTTP $httpCode)\n";
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 使用示例
|
||||||
|
$client = new IOPaintClient('http://localhost:8080', 'your_secret_key_change_me');
|
||||||
|
|
||||||
|
// 健康检查
|
||||||
|
print_r($client->healthCheck());
|
||||||
|
|
||||||
|
// 处理图片
|
||||||
|
$client->removeWatermark('test.jpg');
|
||||||
|
?>
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Java
|
||||||
|
|
||||||
|
```java
|
||||||
|
import okhttp3.*;
|
||||||
|
import java.io.*;
|
||||||
|
import java.nio.file.*;
|
||||||
|
|
||||||
|
public class IOPaintClient {
|
||||||
|
private final String apiUrl;
|
||||||
|
private final String apiKey;
|
||||||
|
private final OkHttpClient client;
|
||||||
|
|
||||||
|
public IOPaintClient(String apiUrl, String apiKey) {
|
||||||
|
this.apiUrl = apiUrl.replaceAll("/$", "");
|
||||||
|
this.apiKey = apiKey;
|
||||||
|
this.client = new OkHttpClient.Builder()
|
||||||
|
.connectTimeout(30, TimeUnit.SECONDS)
|
||||||
|
.readTimeout(120, TimeUnit.SECONDS)
|
||||||
|
.build();
|
||||||
|
}
|
||||||
|
|
||||||
|
public String removeWatermark(String imagePath, String maskPath, String outputPath) throws IOException {
|
||||||
|
// 构建请求
|
||||||
|
MultipartBody.Builder builder = new MultipartBody.Builder()
|
||||||
|
.setType(MultipartBody.FORM)
|
||||||
|
.addFormDataPart("image", "image.jpg",
|
||||||
|
RequestBody.create(new File(imagePath), MediaType.parse("image/*")));
|
||||||
|
|
||||||
|
if (maskPath != null) {
|
||||||
|
builder.addFormDataPart("mask", "mask.png",
|
||||||
|
RequestBody.create(new File(maskPath), MediaType.parse("image/*")));
|
||||||
|
}
|
||||||
|
|
||||||
|
RequestBody requestBody = builder.build();
|
||||||
|
|
||||||
|
Request request = new Request.Builder()
|
||||||
|
.url(apiUrl + "/api/v1/remove-watermark")
|
||||||
|
.addHeader("X-API-Key", apiKey)
|
||||||
|
.post(requestBody)
|
||||||
|
.build();
|
||||||
|
|
||||||
|
// 发送请求
|
||||||
|
try (Response response = client.newCall(request).execute()) {
|
||||||
|
if (response.isSuccessful()) {
|
||||||
|
// 保存结果
|
||||||
|
if (outputPath == null) {
|
||||||
|
Path path = Paths.get(imagePath);
|
||||||
|
String name = path.getFileName().toString();
|
||||||
|
name = name.substring(0, name.lastIndexOf('.'));
|
||||||
|
outputPath = path.getParent().resolve(name + "_result.png").toString();
|
||||||
|
}
|
||||||
|
|
||||||
|
try (InputStream is = response.body().byteStream();
|
||||||
|
FileOutputStream fos = new FileOutputStream(outputPath)) {
|
||||||
|
byte[] buffer = new byte[4096];
|
||||||
|
int bytesRead;
|
||||||
|
while ((bytesRead = is.read(buffer)) != -1) {
|
||||||
|
fos.write(buffer, 0, bytesRead);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
System.out.println("✓ 处理成功!输出: " + outputPath);
|
||||||
|
return outputPath;
|
||||||
|
} else {
|
||||||
|
System.err.println("✗ 处理失败: " + response.code());
|
||||||
|
System.err.println(response.body().string());
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void main(String[] args) {
|
||||||
|
IOPaintClient client = new IOPaintClient(
|
||||||
|
"http://localhost:8080",
|
||||||
|
"your_secret_key_change_me"
|
||||||
|
);
|
||||||
|
|
||||||
|
try {
|
||||||
|
client.removeWatermark("test.jpg", null, null);
|
||||||
|
} catch (IOException e) {
|
||||||
|
e.printStackTrace();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Go
|
||||||
|
|
||||||
|
```go
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"mime/multipart"
|
||||||
|
"net/http"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
type IOPaintClient struct {
|
||||||
|
apiURL string
|
||||||
|
apiKey string
|
||||||
|
client *http.Client
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewIOPaintClient(apiURL, apiKey string) *IOPaintClient {
|
||||||
|
return &IOPaintClient{
|
||||||
|
apiURL: apiURL,
|
||||||
|
apiKey: apiKey,
|
||||||
|
client: &http.Client{
|
||||||
|
Timeout: 120 * time.Second,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *IOPaintClient) RemoveWatermark(imagePath, maskPath, outputPath string) error {
|
||||||
|
// 准备multipart请求
|
||||||
|
body := &bytes.Buffer{}
|
||||||
|
writer := multipart.NewWriter(body)
|
||||||
|
|
||||||
|
// 添加图片
|
||||||
|
imageFile, err := os.Open(imagePath)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
defer imageFile.Close()
|
||||||
|
|
||||||
|
imagePart, err := writer.CreateFormFile("image", filepath.Base(imagePath))
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
io.Copy(imagePart, imageFile)
|
||||||
|
|
||||||
|
// 添加遮罩(如果有)
|
||||||
|
if maskPath != "" {
|
||||||
|
maskFile, err := os.Open(maskPath)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
defer maskFile.Close()
|
||||||
|
|
||||||
|
maskPart, err := writer.CreateFormFile("mask", filepath.Base(maskPath))
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
io.Copy(maskPart, maskFile)
|
||||||
|
}
|
||||||
|
|
||||||
|
writer.Close()
|
||||||
|
|
||||||
|
// 创建请求
|
||||||
|
req, err := http.NewRequest("POST", c.apiURL+"/api/v1/remove-watermark", body)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
req.Header.Set("X-API-Key", c.apiKey)
|
||||||
|
req.Header.Set("Content-Type", writer.FormDataContentType())
|
||||||
|
|
||||||
|
// 发送请求
|
||||||
|
resp, err := c.client.Do(req)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
defer resp.Body.Close()
|
||||||
|
|
||||||
|
// 检查响应
|
||||||
|
if resp.StatusCode != 200 {
|
||||||
|
return fmt.Errorf("请求失败: %d", resp.StatusCode)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 确定输出路径
|
||||||
|
if outputPath == "" {
|
||||||
|
ext := filepath.Ext(imagePath)
|
||||||
|
name := imagePath[:len(imagePath)-len(ext)]
|
||||||
|
outputPath = name + "_result.png"
|
||||||
|
}
|
||||||
|
|
||||||
|
// 保存结果
|
||||||
|
outFile, err := os.Create(outputPath)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
defer outFile.Close()
|
||||||
|
|
||||||
|
_, err = io.Copy(outFile, resp.Body)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
fmt.Printf("✓ 处理成功!输出: %s\n", outputPath)
|
||||||
|
fmt.Printf(" 处理时间: %s秒\n", resp.Header.Get("X-Processing-Time"))
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
client := NewIOPaintClient("http://localhost:8080", "your_secret_key_change_me")
|
||||||
|
|
||||||
|
err := client.RemoveWatermark("test.jpg", "", "")
|
||||||
|
if err != nil {
|
||||||
|
fmt.Printf("错误: %v\n", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 错误处理
|
||||||
|
|
||||||
|
### 常见错误代码
|
||||||
|
|
||||||
|
| 状态码 | 错误 | 解决方案 |
|
||||||
|
|--------|------|----------|
|
||||||
|
| 401 | 未授权 | 检查API Key是否正确 |
|
||||||
|
| 400 | 请求错误 | 检查图片格式、大小是否符合要求 |
|
||||||
|
| 429 | 请求过多 | 降低请求频率,稍后重试 |
|
||||||
|
| 500 | 服务器错误 | 检查服务器日志,联系技术支持 |
|
||||||
|
| 503 | 服务不可用 | 服务器可能正在重启,稍后重试 |
|
||||||
|
|
||||||
|
### 限流说明
|
||||||
|
|
||||||
|
默认限流:每秒10个请求,突发20个请求
|
||||||
|
|
||||||
|
如果遇到429错误,建议:
|
||||||
|
1. 使用指数退避重试策略
|
||||||
|
2. 减少并发请求数
|
||||||
|
3. 联系管理员增加配额
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 性能优化建议
|
||||||
|
|
||||||
|
1. **批量处理**:合理控制并发数(建议2-4个并发)
|
||||||
|
2. **图片预处理**:压缩大图片后再上传
|
||||||
|
3. **复用连接**:使用HTTP keep-alive
|
||||||
|
4. **错误重试**:实现指数退避策略
|
||||||
|
5. **超时设置**:根据图片大小设置合理超时时间
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 支持
|
||||||
|
|
||||||
|
如有问题,请访问:
|
||||||
|
- 文档:`http://localhost:8080/docs`
|
||||||
|
- GitHub:https://github.com/let5sne/IOPaint/issues
|
||||||
526
API_SERVICE_GUIDE.md
Normal file
526
API_SERVICE_GUIDE.md
Normal file
@@ -0,0 +1,526 @@
|
|||||||
|
# IOPaint 去水印 API 服务设计方案
|
||||||
|
|
||||||
|
## 📋 目录
|
||||||
|
1. [MVP 最小可行产品](#mvp-最小可行产品)
|
||||||
|
2. [商业化架构设计](#商业化架构设计)
|
||||||
|
3. [部署方案对比](#部署方案对比)
|
||||||
|
4. [成本与扩展性分析](#成本与扩展性分析)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🚀 MVP 最小可行产品
|
||||||
|
|
||||||
|
### 设计原则(KISS)
|
||||||
|
- **单一功能**:只提供去水印API,不包含WebUI
|
||||||
|
- **单一模型**:只使用LaMa模型(快速、低资源)
|
||||||
|
- **简单认证**:API Key认证
|
||||||
|
- **本地存储**:无需对象存储
|
||||||
|
- **单机部署**:Docker Compose即可
|
||||||
|
|
||||||
|
### 核心改造
|
||||||
|
|
||||||
|
#### 1. 精简API服务 (`api_service.py`)
|
||||||
|
```python
|
||||||
|
# 只保留核心功能:
|
||||||
|
# - POST /api/v1/remove-watermark - 去水印接口
|
||||||
|
# - GET /api/v1/health - 健康检查
|
||||||
|
# - GET /api/v1/usage - 使用统计(可选)
|
||||||
|
|
||||||
|
# 移除功能:
|
||||||
|
# - WebUI相关路由
|
||||||
|
# - 多模型支持
|
||||||
|
# - 插件系统
|
||||||
|
# - 文件浏览器
|
||||||
|
# - Socket.IO实时通信
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 2. API接口设计
|
||||||
|
|
||||||
|
**去水印接口**
|
||||||
|
```bash
|
||||||
|
POST /api/v1/remove-watermark
|
||||||
|
Headers:
|
||||||
|
X-API-Key: your_api_key_here
|
||||||
|
Content-Type: multipart/form-data
|
||||||
|
|
||||||
|
Body:
|
||||||
|
image: file (必需) - 原始图片
|
||||||
|
mask: file (可选) - 水印遮罩,不提供则自动检测
|
||||||
|
|
||||||
|
Response:
|
||||||
|
- 200: 返回处理后的图片(image/png)
|
||||||
|
- 401: API Key无效
|
||||||
|
- 400: 参数错误
|
||||||
|
- 500: 处理失败
|
||||||
|
```
|
||||||
|
|
||||||
|
**健康检查**
|
||||||
|
```bash
|
||||||
|
GET /api/v1/health
|
||||||
|
Response: {"status": "ok", "model": "lama"}
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 3. MVP部署架构
|
||||||
|
|
||||||
|
```
|
||||||
|
┌─────────────────────────────────────┐
|
||||||
|
│ Nginx (反向代理) │
|
||||||
|
│ - SSL终止 │
|
||||||
|
│ - 限流 (rate limiting) │
|
||||||
|
│ - 日志记录 │
|
||||||
|
└──────────────┬──────────────────────┘
|
||||||
|
│
|
||||||
|
┌──────────────▼──────────────────────┐
|
||||||
|
│ IOPaint API Service │
|
||||||
|
│ - FastAPI │
|
||||||
|
│ - LaMa模型 │
|
||||||
|
│ - API Key认证 │
|
||||||
|
│ - 本地存储 │
|
||||||
|
└─────────────────────────────────────┘
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 4. MVP Docker配置
|
||||||
|
|
||||||
|
**单容器方案**:适合月处理量 < 10万张
|
||||||
|
```yaml
|
||||||
|
# docker-compose.mvp.yml
|
||||||
|
version: '3.8'
|
||||||
|
services:
|
||||||
|
api:
|
||||||
|
build:
|
||||||
|
context: .
|
||||||
|
dockerfile: docker/APIDockerfile
|
||||||
|
ports:
|
||||||
|
- "8080:8080"
|
||||||
|
environment:
|
||||||
|
- API_KEY=your_secret_key_here
|
||||||
|
- MAX_IMAGE_SIZE=4096
|
||||||
|
- ENABLE_METRICS=true
|
||||||
|
volumes:
|
||||||
|
- ./models:/root/.cache
|
||||||
|
- ./logs:/app/logs
|
||||||
|
restart: unless-stopped
|
||||||
|
deploy:
|
||||||
|
resources:
|
||||||
|
limits:
|
||||||
|
cpus: '2'
|
||||||
|
memory: 4G
|
||||||
|
```
|
||||||
|
|
||||||
|
**成本估算(MVP阶段)**:
|
||||||
|
- **云服务器**:2核4G,约¥200-300/月(阿里云、腾讯云)
|
||||||
|
- **存储**:100GB SSD,约¥50/月
|
||||||
|
- **流量**:100GB/月,约¥50/月
|
||||||
|
- **总计**:约¥300-400/月
|
||||||
|
|
||||||
|
**性能预估**:
|
||||||
|
- 处理速度:约1-2秒/张(1024x1024)
|
||||||
|
- 并发能力:2-4个请求
|
||||||
|
- 月处理量:~5-10万张
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🏢 商业化架构设计
|
||||||
|
|
||||||
|
### 设计原则
|
||||||
|
- **横向扩展**:支持动态增减实例
|
||||||
|
- **高可用**:无单点故障
|
||||||
|
- **异步处理**:支持批量和队列
|
||||||
|
- **监控完善**:实时监控和告警
|
||||||
|
- **成本优化**:按需扩展
|
||||||
|
|
||||||
|
### 商业化架构图
|
||||||
|
|
||||||
|
```
|
||||||
|
┌─────────────────┐
|
||||||
|
│ CDN / CloudFlare │
|
||||||
|
└────────┬────────┘
|
||||||
|
│
|
||||||
|
┌────────▼────────┐
|
||||||
|
│ Load Balancer │ (Nginx/HAProxy/ALB)
|
||||||
|
│ - SSL终止 │
|
||||||
|
│ - 限流 │
|
||||||
|
│ - WAF │
|
||||||
|
└────────┬────────┘
|
||||||
|
│
|
||||||
|
┌───────────────────┼───────────────────┐
|
||||||
|
│ │ │
|
||||||
|
┌────▼────┐ ┌────▼────┐ ┌────▼────┐
|
||||||
|
│API Pod 1│ │API Pod 2│ │API Pod N│
|
||||||
|
│ (GPU) │ │ (GPU) │ │ (GPU) │
|
||||||
|
└────┬────┘ └────┬────┘ └────┬────┘
|
||||||
|
│ │ │
|
||||||
|
└───────────────────┼───────────────────┘
|
||||||
|
│
|
||||||
|
┌───────────────────┼───────────────────┐
|
||||||
|
│ │ │
|
||||||
|
┌────▼────┐ ┌────▼────┐ ┌────▼────┐
|
||||||
|
│ Redis │ │PostgreSQL│ │ S3 │
|
||||||
|
│ (队列) │ │ (元数据) │ │ (存储) │
|
||||||
|
└─────────┘ └──────────┘ └─────────┘
|
||||||
|
│
|
||||||
|
┌────▼────┐
|
||||||
|
│ Celery │
|
||||||
|
│ Worker │
|
||||||
|
└─────────┘
|
||||||
|
│
|
||||||
|
┌────▼────┐
|
||||||
|
│Prometheus│
|
||||||
|
│ Grafana │
|
||||||
|
└─────────┘
|
||||||
|
```
|
||||||
|
|
||||||
|
### 核心组件
|
||||||
|
|
||||||
|
#### 1. API层(Kubernetes部署)
|
||||||
|
|
||||||
|
**api-deployment.yaml**
|
||||||
|
```yaml
|
||||||
|
apiVersion: apps/v1
|
||||||
|
kind: Deployment
|
||||||
|
metadata:
|
||||||
|
name: iopaint-api
|
||||||
|
spec:
|
||||||
|
replicas: 3 # 根据负载自动扩展
|
||||||
|
template:
|
||||||
|
spec:
|
||||||
|
containers:
|
||||||
|
- name: api
|
||||||
|
image: let5sne/iopaint-api:latest
|
||||||
|
resources:
|
||||||
|
requests:
|
||||||
|
memory: "4Gi"
|
||||||
|
cpu: "2"
|
||||||
|
nvidia.com/gpu: 1
|
||||||
|
limits:
|
||||||
|
memory: "8Gi"
|
||||||
|
cpu: "4"
|
||||||
|
nvidia.com/gpu: 1
|
||||||
|
env:
|
||||||
|
- name: REDIS_URL
|
||||||
|
value: "redis://redis:6379"
|
||||||
|
- name: S3_BUCKET
|
||||||
|
value: "iopaint-images"
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 2. 异步任务队列(Redis + Celery)
|
||||||
|
|
||||||
|
**好处**:
|
||||||
|
- 避免API超时
|
||||||
|
- 支持批量处理
|
||||||
|
- 可重试失败任务
|
||||||
|
- 平滑处理流量峰值
|
||||||
|
|
||||||
|
**工作流程**:
|
||||||
|
```
|
||||||
|
1. 用户上传图片 → API返回任务ID
|
||||||
|
2. 图片存入S3 → 任务推入Redis队列
|
||||||
|
3. Celery Worker异步处理
|
||||||
|
4. 处理完成 → 更新数据库 → 触发回调/Webhook
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 3. 数据库设计(PostgreSQL)
|
||||||
|
|
||||||
|
```sql
|
||||||
|
-- 用户表
|
||||||
|
CREATE TABLE users (
|
||||||
|
id SERIAL PRIMARY KEY,
|
||||||
|
api_key VARCHAR(64) UNIQUE NOT NULL,
|
||||||
|
plan VARCHAR(20) NOT NULL, -- free, basic, pro, enterprise
|
||||||
|
quota_monthly INT NOT NULL,
|
||||||
|
quota_used INT DEFAULT 0,
|
||||||
|
created_at TIMESTAMP DEFAULT NOW()
|
||||||
|
);
|
||||||
|
|
||||||
|
-- 任务表
|
||||||
|
CREATE TABLE tasks (
|
||||||
|
id UUID PRIMARY KEY,
|
||||||
|
user_id INT REFERENCES users(id),
|
||||||
|
status VARCHAR(20) NOT NULL, -- pending, processing, completed, failed
|
||||||
|
image_url TEXT NOT NULL,
|
||||||
|
result_url TEXT,
|
||||||
|
created_at TIMESTAMP DEFAULT NOW(),
|
||||||
|
completed_at TIMESTAMP,
|
||||||
|
processing_time_ms INT
|
||||||
|
);
|
||||||
|
|
||||||
|
-- 使用统计表(按日汇总)
|
||||||
|
CREATE TABLE usage_stats (
|
||||||
|
date DATE NOT NULL,
|
||||||
|
user_id INT REFERENCES users(id),
|
||||||
|
requests_count INT DEFAULT 0,
|
||||||
|
success_count INT DEFAULT 0,
|
||||||
|
avg_processing_time_ms INT,
|
||||||
|
PRIMARY KEY (date, user_id)
|
||||||
|
);
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 4. 监控与告警
|
||||||
|
|
||||||
|
**Prometheus指标**:
|
||||||
|
```python
|
||||||
|
# 核心业务指标
|
||||||
|
requests_total = Counter('api_requests_total', 'Total API requests', ['status', 'endpoint'])
|
||||||
|
processing_time = Histogram('image_processing_seconds', 'Image processing time')
|
||||||
|
model_inference_time = Histogram('model_inference_seconds', 'Model inference time')
|
||||||
|
queue_size = Gauge('redis_queue_size', 'Current queue size')
|
||||||
|
gpu_utilization = Gauge('gpu_utilization', 'GPU utilization %')
|
||||||
|
```
|
||||||
|
|
||||||
|
**告警规则**:
|
||||||
|
- API错误率 > 5%
|
||||||
|
- 队列积压 > 1000
|
||||||
|
- GPU利用率 > 90%(持续5分钟)
|
||||||
|
- 响应时间 > 10秒(P95)
|
||||||
|
|
||||||
|
#### 5. 成本优化策略
|
||||||
|
|
||||||
|
**弹性伸缩**:
|
||||||
|
```yaml
|
||||||
|
apiVersion: autoscaling/v2
|
||||||
|
kind: HorizontalPodAutoscaler
|
||||||
|
metadata:
|
||||||
|
name: iopaint-api-hpa
|
||||||
|
spec:
|
||||||
|
scaleTargetRef:
|
||||||
|
apiVersion: apps/v1
|
||||||
|
kind: Deployment
|
||||||
|
name: iopaint-api
|
||||||
|
minReplicas: 2
|
||||||
|
maxReplicas: 10
|
||||||
|
metrics:
|
||||||
|
- type: Resource
|
||||||
|
resource:
|
||||||
|
name: cpu
|
||||||
|
target:
|
||||||
|
type: Utilization
|
||||||
|
averageUtilization: 70
|
||||||
|
- type: Resource
|
||||||
|
resource:
|
||||||
|
name: nvidia.com/gpu
|
||||||
|
target:
|
||||||
|
type: Utilization
|
||||||
|
averageUtilization: 80
|
||||||
|
```
|
||||||
|
|
||||||
|
**Spot实例**:
|
||||||
|
- 使用云厂商Spot/抢占式实例,成本降低60-80%
|
||||||
|
- 配合优先级队列,重要任务用按需实例
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🔄 部署方案对比
|
||||||
|
|
||||||
|
| 方案 | 适用场景 | 优点 | 缺点 | 月成本估算 |
|
||||||
|
|------|---------|------|------|-----------|
|
||||||
|
| **Docker单机** | 个人/小团队<br>月< 10万张 | • 部署简单<br>• 成本低<br>• 维护容易 | • 无法扩展<br>• 单点故障<br>• 性能有限 | ¥300-500 |
|
||||||
|
| **Docker Compose多容器** | 小型商业<br>月10-50万张 | • 支持多实例<br>• 负载均衡<br>• 成本可控 | • 手动扩展<br>• 监控有限<br>• 高可用差 | ¥1000-3000 |
|
||||||
|
| **Kubernetes** | 中大型商业<br>月50万张+ | • 自动扩展<br>• 高可用<br>• 完善监控<br>• 多云部署 | • 复杂度高<br>• 学习成本<br>• 初期成本高 | ¥5000-20000+ |
|
||||||
|
| **Serverless (Lambda/云函数)** | 不规则流量<br>峰谷明显 | • 按用付费<br>• 无需运维<br>• 无限扩展 | • 冷启动慢<br>• GPU支持差<br>• 单次限制 | 按用量计费 |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 💰 成本与扩展性分析
|
||||||
|
|
||||||
|
### MVP阶段(月处理10万张)
|
||||||
|
|
||||||
|
**方案:单机Docker**
|
||||||
|
```
|
||||||
|
硬件:
|
||||||
|
- 云服务器 2核4G(CPU版本):¥200/月
|
||||||
|
或
|
||||||
|
- GPU服务器 4核16G + T4(GPU版本):¥800/月
|
||||||
|
|
||||||
|
存储:
|
||||||
|
- 系统盘 100GB SSD:¥50/月
|
||||||
|
- 模型缓存:~5GB(LaMa)
|
||||||
|
|
||||||
|
带宽:
|
||||||
|
- 假设平均每张图500KB,10万张 = 50GB
|
||||||
|
- 上传 + 下载 = 100GB,约¥60/月
|
||||||
|
|
||||||
|
总计:
|
||||||
|
- CPU版本:约¥310/月
|
||||||
|
- GPU版本:约¥910/月(推荐,处理速度快10倍)
|
||||||
|
```
|
||||||
|
|
||||||
|
### 商业化阶段(月处理100万张)
|
||||||
|
|
||||||
|
**方案:Kubernetes + GPU节点池**
|
||||||
|
```
|
||||||
|
计算资源(3个GPU节点,自动扩展):
|
||||||
|
- 3 x (4核16G + T4 GPU):¥2400/月
|
||||||
|
- 高峰期额外2个节点(Spot实例):¥400/月
|
||||||
|
|
||||||
|
数据库:
|
||||||
|
- PostgreSQL云数据库(2核4G):¥300/月
|
||||||
|
- Redis云实例(2G):¥150/月
|
||||||
|
|
||||||
|
存储:
|
||||||
|
- 对象存储 500GB:¥100/月
|
||||||
|
- 数据库存储 100GB:¥50/月
|
||||||
|
|
||||||
|
CDN + 流量:
|
||||||
|
- CDN加速:¥200/月
|
||||||
|
- 带宽流量(1TB):¥600/月
|
||||||
|
|
||||||
|
监控 + 日志:
|
||||||
|
- 日志服务:¥100/月
|
||||||
|
- 监控告警:¥100/月
|
||||||
|
|
||||||
|
负载均衡:¥100/月
|
||||||
|
|
||||||
|
总计:约¥4500-5000/月
|
||||||
|
```
|
||||||
|
|
||||||
|
**收益模型(参考)**:
|
||||||
|
```
|
||||||
|
定价方案:
|
||||||
|
- Free: 10张/天,免费
|
||||||
|
- Basic: ¥99/月,3000张
|
||||||
|
- Pro: ¥399/月,20000张
|
||||||
|
- Enterprise: ¥1999/月,150000张,优先处理
|
||||||
|
|
||||||
|
假设用户分布:
|
||||||
|
- Free用户:1000人 = 0元(引流)
|
||||||
|
- Basic用户:200人 = ¥19,800
|
||||||
|
- Pro用户:50人 = ¥19,950
|
||||||
|
- Enterprise:10人 = ¥19,990
|
||||||
|
|
||||||
|
月收入:约¥59,740
|
||||||
|
月成本:约¥5,000
|
||||||
|
月利润:约¥54,740
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 📝 推荐实施路线
|
||||||
|
|
||||||
|
### 阶段1:MVP验证(1-2个月)
|
||||||
|
**目标**:验证市场需求,获取前100个付费用户
|
||||||
|
|
||||||
|
**技术栈**:
|
||||||
|
- Docker单机部署
|
||||||
|
- FastAPI + LaMa模型
|
||||||
|
- 简单API Key认证
|
||||||
|
- SQLite本地数据库
|
||||||
|
|
||||||
|
**投入**:
|
||||||
|
- 开发时间:1周
|
||||||
|
- 服务器成本:¥300-500/月
|
||||||
|
- 域名+SSL:¥100/年
|
||||||
|
|
||||||
|
**里程碑**:
|
||||||
|
- [ ] API服务上线
|
||||||
|
- [ ] 文档和示例代码
|
||||||
|
- [ ] 支付集成(微信/支付宝)
|
||||||
|
- [ ] 获取前10个付费用户
|
||||||
|
- [ ] 收集用户反馈
|
||||||
|
|
||||||
|
### 阶段2:产品优化(2-4个月)
|
||||||
|
**目标**:优化体验,扩展到1000付费用户
|
||||||
|
|
||||||
|
**技术栈**:
|
||||||
|
- Docker Compose多容器
|
||||||
|
- PostgreSQL数据库
|
||||||
|
- Redis缓存
|
||||||
|
- 简单监控(Prometheus)
|
||||||
|
|
||||||
|
**投入**:
|
||||||
|
- 开发时间:2周
|
||||||
|
- 服务器成本:¥1000-2000/月
|
||||||
|
|
||||||
|
**里程碑**:
|
||||||
|
- [ ] 批量处理API
|
||||||
|
- [ ] Webhook回调
|
||||||
|
- [ ] 使用Dashboard
|
||||||
|
- [ ] 自动检测水印(可选)
|
||||||
|
- [ ] API SDK(Python/Node.js)
|
||||||
|
|
||||||
|
### 阶段3:规模化(4-6个月)
|
||||||
|
**目标**:支持月百万级处理,稳定盈利
|
||||||
|
|
||||||
|
**技术栈**:
|
||||||
|
- Kubernetes集群
|
||||||
|
- 对象存储
|
||||||
|
- 完整监控体系
|
||||||
|
- 多模型支持(可选)
|
||||||
|
|
||||||
|
**投入**:
|
||||||
|
- 开发时间:4周
|
||||||
|
- 基础设施成本:¥5000-10000/月
|
||||||
|
|
||||||
|
**里程碑**:
|
||||||
|
- [ ] 自动扩展
|
||||||
|
- [ ] 多区域部署
|
||||||
|
- [ ] SLA保证(99.9%)
|
||||||
|
- [ ] 企业级支持
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🎯 关键建议
|
||||||
|
|
||||||
|
### 1. MVP阶段重点
|
||||||
|
✅ **做**:
|
||||||
|
- 专注核心功能(去水印)
|
||||||
|
- 简单可靠的API
|
||||||
|
- 完善的文档和示例
|
||||||
|
- 快速迭代
|
||||||
|
|
||||||
|
❌ **不做**:
|
||||||
|
- 复杂的功能(多模型、插件)
|
||||||
|
- 过度设计的架构
|
||||||
|
- 过早优化性能
|
||||||
|
- WebUI界面
|
||||||
|
|
||||||
|
### 2. Docker vs Kubernetes
|
||||||
|
|
||||||
|
**用Docker如果**:
|
||||||
|
- 月处理量 < 50万张
|
||||||
|
- 团队 < 3人
|
||||||
|
- 预算有限
|
||||||
|
- 流量相对稳定
|
||||||
|
|
||||||
|
**用Kubernetes如果**:
|
||||||
|
- 月处理量 > 50万张
|
||||||
|
- 需要高可用(99.9%+)
|
||||||
|
- 流量波动大
|
||||||
|
- 计划多区域部署
|
||||||
|
|
||||||
|
### 3. 技术债务控制
|
||||||
|
|
||||||
|
**从一开始就做好**:
|
||||||
|
- API版本控制(/api/v1/)
|
||||||
|
- 完善的错误处理和日志
|
||||||
|
- API限流和认证
|
||||||
|
- 数据备份策略
|
||||||
|
|
||||||
|
**可以后续优化**:
|
||||||
|
- 监控系统(先简单后完善)
|
||||||
|
- 自动扩展(先手动后自动)
|
||||||
|
- 多模型支持(先单模型验证)
|
||||||
|
- 高级功能(批量、回调等)
|
||||||
|
|
||||||
|
### 4. 安全建议
|
||||||
|
|
||||||
|
**必须**:
|
||||||
|
- HTTPS强制
|
||||||
|
- API Key认证
|
||||||
|
- 请求限流
|
||||||
|
- 输入验证(文件大小、格式)
|
||||||
|
- 敏感信息加密
|
||||||
|
|
||||||
|
**推荐**:
|
||||||
|
- WAF防护
|
||||||
|
- DDoS防护
|
||||||
|
- 审计日志
|
||||||
|
- 定期安全扫描
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 📚 参考资源
|
||||||
|
|
||||||
|
- [FastAPI最佳实践](https://fastapi.tiangolo.com/tutorial/)
|
||||||
|
- [Kubernetes生产实践](https://kubernetes.io/docs/setup/production-environment/)
|
||||||
|
- [AWS架构最佳实践](https://aws.amazon.com/architecture/well-architected/)
|
||||||
|
- [API设计指南](https://github.com/microsoft/api-guidelines)
|
||||||
303
API_SERVICE_README.md
Normal file
303
API_SERVICE_README.md
Normal file
@@ -0,0 +1,303 @@
|
|||||||
|
# IOPaint 去水印 API 服务
|
||||||
|
|
||||||
|
专注于提供去水印功能的精简API服务,适合商业化部署。
|
||||||
|
|
||||||
|
## 🎯 项目特点
|
||||||
|
|
||||||
|
- **单一职责**:专注去水印功能,移除WebUI和其他复杂功能
|
||||||
|
- **高性能**:使用LaMa模型,1-2秒处理一张1024x1024图片
|
||||||
|
- **易部署**:Docker一键部署,支持CPU和GPU
|
||||||
|
- **低成本**:MVP阶段月成本约¥300-500
|
||||||
|
- **可扩展**:提供完整的商业化架构方案
|
||||||
|
|
||||||
|
## 📚 文档
|
||||||
|
|
||||||
|
- [完整设计方案](./API_SERVICE_GUIDE.md) - MVP到商业化的完整路线图
|
||||||
|
- [客户端示例](./API_CLIENT_EXAMPLES.md) - Python、JavaScript、cURL等多语言调用示例
|
||||||
|
|
||||||
|
## 🚀 快速开始
|
||||||
|
|
||||||
|
### 方式1:Docker Compose部署(推荐)
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 1. 设置API密钥
|
||||||
|
export API_KEY="your_secret_key_here"
|
||||||
|
|
||||||
|
# 2. 启动服务(GPU版本)
|
||||||
|
docker-compose -f docker-compose.mvp.yml up -d
|
||||||
|
|
||||||
|
# 3. 检查服务状态
|
||||||
|
curl http://localhost:8080/api/v1/health
|
||||||
|
|
||||||
|
# 4. 测试去水印
|
||||||
|
curl -X POST http://localhost:8080/api/v1/remove-watermark \
|
||||||
|
-H "X-API-Key: $API_KEY" \
|
||||||
|
-F "image=@test.jpg" \
|
||||||
|
-o result.png
|
||||||
|
```
|
||||||
|
|
||||||
|
### 方式2:直接运行Python脚本
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 1. 安装依赖
|
||||||
|
pip3 install -r requirements.txt
|
||||||
|
pip3 install -e .
|
||||||
|
|
||||||
|
# 2. 设置环境变量
|
||||||
|
export API_KEY="your_secret_key_here"
|
||||||
|
|
||||||
|
# 3. 启动服务
|
||||||
|
python3 api_service_mvp.py
|
||||||
|
|
||||||
|
# 4. 访问 http://localhost:8080/docs 查看API文档
|
||||||
|
```
|
||||||
|
|
||||||
|
## 🔧 配置说明
|
||||||
|
|
||||||
|
### 环境变量
|
||||||
|
|
||||||
|
| 变量名 | 说明 | 默认值 |
|
||||||
|
|--------|------|--------|
|
||||||
|
| `API_KEY` | API访问密钥 | `your_secret_key_change_me` |
|
||||||
|
| `MAX_IMAGE_SIZE` | 最大图片边长(像素) | `4096` |
|
||||||
|
| `ENABLE_METRICS` | 启用统计指标 | `true` |
|
||||||
|
|
||||||
|
### 硬件要求
|
||||||
|
|
||||||
|
**最低配置(CPU版本)**:
|
||||||
|
- CPU: 2核
|
||||||
|
- 内存: 4GB
|
||||||
|
- 磁盘: 20GB
|
||||||
|
- 性能: ~10-15秒/张
|
||||||
|
|
||||||
|
**推荐配置(GPU版本)**:
|
||||||
|
- CPU: 4核
|
||||||
|
- 内存: 8GB
|
||||||
|
- GPU: NVIDIA T4或更好(2GB+ VRAM)
|
||||||
|
- 磁盘: 30GB
|
||||||
|
- 性能: ~1-2秒/张
|
||||||
|
|
||||||
|
## 📖 API文档
|
||||||
|
|
||||||
|
### 核心接口
|
||||||
|
|
||||||
|
#### 1. 去水印接口
|
||||||
|
|
||||||
|
```http
|
||||||
|
POST /api/v1/remove-watermark
|
||||||
|
```
|
||||||
|
|
||||||
|
**请求头**:
|
||||||
|
- `X-API-Key`: API密钥(必需)
|
||||||
|
- `Content-Type`: multipart/form-data
|
||||||
|
|
||||||
|
**请求体**:
|
||||||
|
- `image`: 图片文件(必需)
|
||||||
|
- `mask`: 遮罩图片(可选,白色区域将被修复)
|
||||||
|
|
||||||
|
**响应**:
|
||||||
|
- 成功:返回处理后的PNG图片
|
||||||
|
- 失败:返回JSON错误信息
|
||||||
|
|
||||||
|
**示例**:
|
||||||
|
```bash
|
||||||
|
curl -X POST http://localhost:8080/api/v1/remove-watermark \
|
||||||
|
-H "X-API-Key: your_key" \
|
||||||
|
-F "image=@input.jpg" \
|
||||||
|
-o result.png
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 2. 健康检查
|
||||||
|
|
||||||
|
```http
|
||||||
|
GET /api/v1/health
|
||||||
|
```
|
||||||
|
|
||||||
|
**响应**:
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"status": "healthy",
|
||||||
|
"model": "lama",
|
||||||
|
"device": "cuda",
|
||||||
|
"gpu_available": true
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 3. 使用统计
|
||||||
|
|
||||||
|
```http
|
||||||
|
GET /api/v1/stats
|
||||||
|
```
|
||||||
|
|
||||||
|
**请求头**:
|
||||||
|
- `X-API-Key`: API密钥(必需)
|
||||||
|
|
||||||
|
**响应**:
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"total": 1000,
|
||||||
|
"success": 980,
|
||||||
|
"failed": 20,
|
||||||
|
"avg_processing_time": 1.5
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## 💡 使用示例
|
||||||
|
|
||||||
|
### Python
|
||||||
|
|
||||||
|
```python
|
||||||
|
import requests
|
||||||
|
|
||||||
|
def remove_watermark(image_path, api_key):
|
||||||
|
url = "http://localhost:8080/api/v1/remove-watermark"
|
||||||
|
headers = {"X-API-Key": api_key}
|
||||||
|
files = {"image": open(image_path, "rb")}
|
||||||
|
|
||||||
|
response = requests.post(url, headers=headers, files=files)
|
||||||
|
|
||||||
|
if response.status_code == 200:
|
||||||
|
with open("result.png", "wb") as f:
|
||||||
|
f.write(response.content)
|
||||||
|
print("✓ 处理成功!")
|
||||||
|
else:
|
||||||
|
print(f"✗ 失败: {response.json()}")
|
||||||
|
|
||||||
|
remove_watermark("test.jpg", "your_api_key")
|
||||||
|
```
|
||||||
|
|
||||||
|
更多语言示例请查看 [API_CLIENT_EXAMPLES.md](./API_CLIENT_EXAMPLES.md)
|
||||||
|
|
||||||
|
## 📊 性能基准
|
||||||
|
|
||||||
|
基于NVIDIA T4 GPU测试:
|
||||||
|
|
||||||
|
| 图片尺寸 | 处理时间 | 内存占用 | 每秒处理 |
|
||||||
|
|----------|---------|----------|---------|
|
||||||
|
| 512x512 | ~0.8秒 | ~1.5GB | ~1.25张/秒 |
|
||||||
|
| 1024x1024 | ~1.5秒 | ~2GB | ~0.67张/秒 |
|
||||||
|
| 2048x2048 | ~4秒 | ~3.5GB | ~0.25张/秒 |
|
||||||
|
| 4096x4096 | ~15秒 | ~6GB | ~0.07张/秒 |
|
||||||
|
|
||||||
|
## 🏗️ 架构方案
|
||||||
|
|
||||||
|
### MVP阶段(月处理10万张)
|
||||||
|
- **部署方式**:Docker单机
|
||||||
|
- **成本**:约¥300-500/月
|
||||||
|
- **支持用户**:100-500人
|
||||||
|
|
||||||
|
### 商业化阶段(月处理100万张)
|
||||||
|
- **部署方式**:Kubernetes + GPU节点池
|
||||||
|
- **成本**:约¥5000-10000/月
|
||||||
|
- **支持用户**:5000+人
|
||||||
|
- **特性**:
|
||||||
|
- 自动扩展
|
||||||
|
- 异步队列(Redis + Celery)
|
||||||
|
- 对象存储(S3/OSS)
|
||||||
|
- 完整监控(Prometheus + Grafana)
|
||||||
|
|
||||||
|
详细架构请查看 [API_SERVICE_GUIDE.md](./API_SERVICE_GUIDE.md)
|
||||||
|
|
||||||
|
## 💰 定价建议(参考)
|
||||||
|
|
||||||
|
| 套餐 | 价格 | 额度 | 适用场景 |
|
||||||
|
|------|------|------|---------|
|
||||||
|
| **Free** | ¥0/月 | 10张/天 | 个人测试 |
|
||||||
|
| **Basic** | ¥99/月 | 3000张 | 小型工作室 |
|
||||||
|
| **Pro** | ¥399/月 | 20000张 | 中型企业 |
|
||||||
|
| **Enterprise** | ¥1999/月 | 150000张 | 大型企业 |
|
||||||
|
|
||||||
|
## 🔒 安全建议
|
||||||
|
|
||||||
|
1. **生产环境务必修改默认API密钥**
|
||||||
|
2. **使用HTTPS**(配置Nginx SSL)
|
||||||
|
3. **启用限流**(防止滥用)
|
||||||
|
4. **定期备份数据库**
|
||||||
|
5. **监控异常访问**
|
||||||
|
|
||||||
|
## 🐛 故障排查
|
||||||
|
|
||||||
|
### 常见问题
|
||||||
|
|
||||||
|
**1. API返回401错误**
|
||||||
|
- 检查X-API-Key header是否正确
|
||||||
|
- 确认API_KEY环境变量已设置
|
||||||
|
|
||||||
|
**2. 处理速度慢**
|
||||||
|
- CPU模式:考虑升级到GPU
|
||||||
|
- GPU模式:检查显存是否充足
|
||||||
|
- 检查图片是否过大
|
||||||
|
|
||||||
|
**3. Docker容器无法启动**
|
||||||
|
- GPU版本:确认nvidia-docker已安装
|
||||||
|
- 检查端口8080是否被占用
|
||||||
|
- 查看日志:`docker-compose logs api`
|
||||||
|
|
||||||
|
**4. 返回500错误**
|
||||||
|
- 查看服务日志:`tail -f logs/api_*.log`
|
||||||
|
- 检查磁盘空间是否充足
|
||||||
|
- 确认模型文件已下载
|
||||||
|
|
||||||
|
## 📈 监控指标
|
||||||
|
|
||||||
|
推荐监控以下指标:
|
||||||
|
|
||||||
|
- **业务指标**:
|
||||||
|
- 请求总数
|
||||||
|
- 成功率
|
||||||
|
- 平均处理时间
|
||||||
|
- 队列长度
|
||||||
|
|
||||||
|
- **系统指标**:
|
||||||
|
- CPU使用率
|
||||||
|
- GPU使用率
|
||||||
|
- 内存使用
|
||||||
|
- 磁盘I/O
|
||||||
|
|
||||||
|
- **告警阈值**:
|
||||||
|
- 错误率 > 5%
|
||||||
|
- 响应时间P95 > 10秒
|
||||||
|
- GPU利用率 > 90%(持续5分钟)
|
||||||
|
|
||||||
|
## 🚦 实施路线图
|
||||||
|
|
||||||
|
### 第1周:MVP上线
|
||||||
|
- [ ] 部署API服务
|
||||||
|
- [ ] 编写使用文档
|
||||||
|
- [ ] 集成支付系统
|
||||||
|
- [ ] 获取前10个用户反馈
|
||||||
|
|
||||||
|
### 第2-4周:产品优化
|
||||||
|
- [ ] 优化处理速度
|
||||||
|
- [ ] 添加批量处理API
|
||||||
|
- [ ] 实现Webhook回调
|
||||||
|
- [ ] 创建使用Dashboard
|
||||||
|
|
||||||
|
### 第2-3个月:规模化
|
||||||
|
- [ ] 迁移到Kubernetes
|
||||||
|
- [ ] 添加自动扩展
|
||||||
|
- [ ] 实现异步队列
|
||||||
|
- [ ] 完善监控系统
|
||||||
|
|
||||||
|
## 📞 技术支持
|
||||||
|
|
||||||
|
- **文档**: [API_SERVICE_GUIDE.md](./API_SERVICE_GUIDE.md)
|
||||||
|
- **示例**: [API_CLIENT_EXAMPLES.md](./API_CLIENT_EXAMPLES.md)
|
||||||
|
- **问题反馈**: https://github.com/let5sne/IOPaint/issues
|
||||||
|
- **在线文档**: `http://localhost:8080/docs` (Swagger UI)
|
||||||
|
|
||||||
|
## 📄 许可证
|
||||||
|
|
||||||
|
本项目基于 Apache-2.0 许可证开源。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
**⚡ 立即开始:**
|
||||||
|
```bash
|
||||||
|
git clone https://github.com/let5sne/IOPaint.git
|
||||||
|
cd IOPaint
|
||||||
|
export API_KEY="your_secret_key"
|
||||||
|
docker-compose -f docker-compose.mvp.yml up -d
|
||||||
|
```
|
||||||
|
|
||||||
|
访问 http://localhost:8080/docs 查看完整API文档!
|
||||||
369
api_service_mvp.py
Normal file
369
api_service_mvp.py
Normal file
@@ -0,0 +1,369 @@
|
|||||||
|
"""
|
||||||
|
IOPaint 去水印 API 服务 - MVP版本
|
||||||
|
专注于单一功能:去除图片水印
|
||||||
|
|
||||||
|
遵循KISS原则:
|
||||||
|
- 只支持LaMa模型
|
||||||
|
- 简单的API Key认证
|
||||||
|
- 同步处理(无需队列)
|
||||||
|
- 本地存储
|
||||||
|
"""
|
||||||
|
|
||||||
|
import os
|
||||||
|
import time
|
||||||
|
import hashlib
|
||||||
|
from pathlib import Path
|
||||||
|
from typing import Optional
|
||||||
|
from datetime import datetime
|
||||||
|
|
||||||
|
import torch
|
||||||
|
import uvicorn
|
||||||
|
from fastapi import FastAPI, File, UploadFile, Header, HTTPException, Request
|
||||||
|
from fastapi.responses import Response, JSONResponse
|
||||||
|
from fastapi.middleware.cors import CORSMiddleware
|
||||||
|
from loguru import logger
|
||||||
|
from PIL import Image
|
||||||
|
|
||||||
|
from iopaint.model_manager import ModelManager
|
||||||
|
from iopaint.schema import ApiConfig, InpaintRequest, HDStrategy
|
||||||
|
from iopaint.helper import (
|
||||||
|
decode_base64_to_image,
|
||||||
|
numpy_to_bytes,
|
||||||
|
load_img,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
# ==================== 配置 ====================
|
||||||
|
class Config:
|
||||||
|
"""服务配置"""
|
||||||
|
# API密钥(生产环境应从环境变量读取)
|
||||||
|
API_KEY = os.getenv("API_KEY", "your_secret_key_change_me")
|
||||||
|
|
||||||
|
# 模型配置
|
||||||
|
MODEL_NAME = "lama"
|
||||||
|
DEVICE = "cuda" if torch.cuda.is_available() else "cpu"
|
||||||
|
|
||||||
|
# 限制配置
|
||||||
|
MAX_IMAGE_SIZE = int(os.getenv("MAX_IMAGE_SIZE", "4096")) # 最大边长
|
||||||
|
MAX_FILE_SIZE = 10 * 1024 * 1024 # 10MB
|
||||||
|
|
||||||
|
# 日志配置
|
||||||
|
LOG_DIR = Path("./logs")
|
||||||
|
LOG_DIR.mkdir(exist_ok=True)
|
||||||
|
|
||||||
|
# 指标统计
|
||||||
|
ENABLE_METRICS = os.getenv("ENABLE_METRICS", "true").lower() == "true"
|
||||||
|
|
||||||
|
|
||||||
|
# ==================== 应用初始化 ====================
|
||||||
|
app = FastAPI(
|
||||||
|
title="IOPaint 去水印 API",
|
||||||
|
description="基于LaMa模型的图片去水印API服务",
|
||||||
|
version="1.0.0-MVP",
|
||||||
|
docs_url="/docs", # Swagger文档
|
||||||
|
redoc_url="/redoc", # ReDoc文档
|
||||||
|
)
|
||||||
|
|
||||||
|
# CORS配置
|
||||||
|
app.add_middleware(
|
||||||
|
CORSMiddleware,
|
||||||
|
allow_origins=["*"], # 生产环境应限制具体域名
|
||||||
|
allow_credentials=True,
|
||||||
|
allow_methods=["*"],
|
||||||
|
allow_headers=["*"],
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
# ==================== 全局变量 ====================
|
||||||
|
model_manager: Optional[ModelManager] = None
|
||||||
|
request_stats = {
|
||||||
|
"total": 0,
|
||||||
|
"success": 0,
|
||||||
|
"failed": 0,
|
||||||
|
"total_processing_time": 0.0,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
# ==================== 认证中间件 ====================
|
||||||
|
async def verify_api_key(x_api_key: str = Header(None, alias="X-API-Key")):
|
||||||
|
"""验证API密钥"""
|
||||||
|
if not x_api_key:
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=401,
|
||||||
|
detail="Missing API Key. Please provide X-API-Key header."
|
||||||
|
)
|
||||||
|
|
||||||
|
if x_api_key != Config.API_KEY:
|
||||||
|
logger.warning(f"Invalid API key attempt: {x_api_key[:8]}...")
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=401,
|
||||||
|
detail="Invalid API Key"
|
||||||
|
)
|
||||||
|
|
||||||
|
return x_api_key
|
||||||
|
|
||||||
|
|
||||||
|
# ==================== 启动/关闭事件 ====================
|
||||||
|
@app.on_event("startup")
|
||||||
|
async def startup_event():
|
||||||
|
"""应用启动时加载模型"""
|
||||||
|
global model_manager
|
||||||
|
|
||||||
|
logger.info("=" * 60)
|
||||||
|
logger.info("IOPaint API Service - MVP Version")
|
||||||
|
logger.info("=" * 60)
|
||||||
|
logger.info(f"Device: {Config.DEVICE}")
|
||||||
|
logger.info(f"Model: {Config.MODEL_NAME}")
|
||||||
|
logger.info(f"Max Image Size: {Config.MAX_IMAGE_SIZE}")
|
||||||
|
logger.info(f"API Key: {'*' * 20}{Config.API_KEY[-4:]}")
|
||||||
|
logger.info("=" * 60)
|
||||||
|
|
||||||
|
try:
|
||||||
|
# 初始化模型管理器
|
||||||
|
api_config = ApiConfig(
|
||||||
|
host="0.0.0.0",
|
||||||
|
port=8080,
|
||||||
|
model=Config.MODEL_NAME,
|
||||||
|
device=Config.DEVICE,
|
||||||
|
gui=False,
|
||||||
|
no_gui_auto_close=True,
|
||||||
|
cpu_offload=False,
|
||||||
|
disable_nsfw_checker=True,
|
||||||
|
cpu_textencoder=False,
|
||||||
|
local_files_only=False,
|
||||||
|
)
|
||||||
|
|
||||||
|
model_manager = ModelManager(
|
||||||
|
name=api_config.model,
|
||||||
|
device=torch.device(api_config.device),
|
||||||
|
no_half=False,
|
||||||
|
low_mem=False,
|
||||||
|
cpu_offload=False,
|
||||||
|
disable_nsfw=api_config.disable_nsfw_checker,
|
||||||
|
sd_cpu_textencoder=api_config.cpu_textencoder,
|
||||||
|
local_files_only=api_config.local_files_only,
|
||||||
|
cpu_textencoder=api_config.cpu_textencoder,
|
||||||
|
)
|
||||||
|
|
||||||
|
logger.success(f"✓ Model {Config.MODEL_NAME} loaded successfully on {Config.DEVICE}")
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Failed to load model: {e}")
|
||||||
|
raise
|
||||||
|
|
||||||
|
|
||||||
|
@app.on_event("shutdown")
|
||||||
|
async def shutdown_event():
|
||||||
|
"""应用关闭时的清理工作"""
|
||||||
|
logger.info("Shutting down API service...")
|
||||||
|
|
||||||
|
if Config.ENABLE_METRICS:
|
||||||
|
logger.info("=" * 60)
|
||||||
|
logger.info("Final Statistics:")
|
||||||
|
logger.info(f" Total Requests: {request_stats['total']}")
|
||||||
|
logger.info(f" Successful: {request_stats['success']}")
|
||||||
|
logger.info(f" Failed: {request_stats['failed']}")
|
||||||
|
if request_stats['success'] > 0:
|
||||||
|
avg_time = request_stats['total_processing_time'] / request_stats['success']
|
||||||
|
logger.info(f" Avg Processing Time: {avg_time:.2f}s")
|
||||||
|
logger.info("=" * 60)
|
||||||
|
|
||||||
|
|
||||||
|
# ==================== API路由 ====================
|
||||||
|
|
||||||
|
@app.get("/")
|
||||||
|
async def root():
|
||||||
|
"""根路径"""
|
||||||
|
return {
|
||||||
|
"service": "IOPaint Watermark Removal API",
|
||||||
|
"version": "1.0.0-MVP",
|
||||||
|
"status": "running",
|
||||||
|
"model": Config.MODEL_NAME,
|
||||||
|
"device": Config.DEVICE,
|
||||||
|
"docs": "/docs",
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@app.get("/api/v1/health")
|
||||||
|
async def health_check():
|
||||||
|
"""健康检查"""
|
||||||
|
return {
|
||||||
|
"status": "healthy",
|
||||||
|
"model": Config.MODEL_NAME,
|
||||||
|
"device": Config.DEVICE,
|
||||||
|
"gpu_available": torch.cuda.is_available(),
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@app.get("/api/v1/stats")
|
||||||
|
async def get_stats(api_key: str = Header(None, alias="X-API-Key")):
|
||||||
|
"""获取使用统计(需要API Key)"""
|
||||||
|
await verify_api_key(api_key)
|
||||||
|
|
||||||
|
if not Config.ENABLE_METRICS:
|
||||||
|
raise HTTPException(status_code=404, detail="Metrics disabled")
|
||||||
|
|
||||||
|
stats = request_stats.copy()
|
||||||
|
if stats['success'] > 0:
|
||||||
|
stats['avg_processing_time'] = stats['total_processing_time'] / stats['success']
|
||||||
|
else:
|
||||||
|
stats['avg_processing_time'] = 0
|
||||||
|
|
||||||
|
return stats
|
||||||
|
|
||||||
|
|
||||||
|
@app.post("/api/v1/remove-watermark")
|
||||||
|
async def remove_watermark(
|
||||||
|
request: Request,
|
||||||
|
image: UploadFile = File(..., description="原始图片"),
|
||||||
|
mask: Optional[UploadFile] = File(None, description="水印遮罩(可选)"),
|
||||||
|
api_key: str = Header(None, alias="X-API-Key")
|
||||||
|
):
|
||||||
|
"""
|
||||||
|
去除图片水印
|
||||||
|
|
||||||
|
参数:
|
||||||
|
- image: 原始图片文件(必需)
|
||||||
|
- mask: 水印遮罩图片(可选,黑色区域会被保留,白色区域会被修复)
|
||||||
|
|
||||||
|
返回:
|
||||||
|
- 处理后的图片(PNG格式)
|
||||||
|
"""
|
||||||
|
# 验证API Key
|
||||||
|
await verify_api_key(api_key)
|
||||||
|
|
||||||
|
start_time = time.time()
|
||||||
|
request_stats["total"] += 1
|
||||||
|
|
||||||
|
try:
|
||||||
|
# 1. 读取图片
|
||||||
|
image_bytes = await image.read()
|
||||||
|
if len(image_bytes) > Config.MAX_FILE_SIZE:
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=400,
|
||||||
|
detail=f"Image too large. Max size: {Config.MAX_FILE_SIZE / 1024 / 1024}MB"
|
||||||
|
)
|
||||||
|
|
||||||
|
# 验证图片格式
|
||||||
|
try:
|
||||||
|
pil_image = Image.open(image.file).convert("RGB")
|
||||||
|
except Exception as e:
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=400,
|
||||||
|
detail=f"Invalid image format: {str(e)}"
|
||||||
|
)
|
||||||
|
|
||||||
|
# 检查图片尺寸
|
||||||
|
width, height = pil_image.size
|
||||||
|
if max(width, height) > Config.MAX_IMAGE_SIZE:
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=400,
|
||||||
|
detail=f"Image too large. Max dimension: {Config.MAX_IMAGE_SIZE}px"
|
||||||
|
)
|
||||||
|
|
||||||
|
logger.info(f"Processing image: {width}x{height}")
|
||||||
|
|
||||||
|
# 2. 读取遮罩(如果提供)
|
||||||
|
mask_pil = None
|
||||||
|
if mask:
|
||||||
|
mask_bytes = await mask.read()
|
||||||
|
try:
|
||||||
|
mask_pil = Image.open(mask.file).convert("L")
|
||||||
|
# 确保遮罩尺寸与原图一致
|
||||||
|
if mask_pil.size != pil_image.size:
|
||||||
|
mask_pil = mask_pil.resize(pil_image.size, Image.LANCZOS)
|
||||||
|
except Exception as e:
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=400,
|
||||||
|
detail=f"Invalid mask format: {str(e)}"
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
# 如果没有提供遮罩,创建全白遮罩(修复整张图)
|
||||||
|
logger.info("No mask provided, will process entire image")
|
||||||
|
mask_pil = Image.new("L", pil_image.size, 255)
|
||||||
|
|
||||||
|
# 3. 构建请求配置
|
||||||
|
inpaint_request = InpaintRequest(
|
||||||
|
image="", # 我们直接传PIL对象,不需要base64
|
||||||
|
mask="",
|
||||||
|
hd_strategy=HDStrategy.ORIGINAL,
|
||||||
|
hd_strategy_crop_margin=128,
|
||||||
|
hd_strategy_crop_trigger_size=800,
|
||||||
|
hd_strategy_resize_limit=2048,
|
||||||
|
)
|
||||||
|
|
||||||
|
# 4. 调用模型进行处理
|
||||||
|
logger.info("Running model inference...")
|
||||||
|
inference_start = time.time()
|
||||||
|
|
||||||
|
result_image = model_manager(
|
||||||
|
image=pil_image,
|
||||||
|
mask=mask_pil,
|
||||||
|
config=inpaint_request,
|
||||||
|
)
|
||||||
|
|
||||||
|
inference_time = time.time() - inference_start
|
||||||
|
logger.info(f"Inference completed in {inference_time:.2f}s")
|
||||||
|
|
||||||
|
# 5. 转换结果为字节
|
||||||
|
output_bytes = numpy_to_bytes(
|
||||||
|
result_image,
|
||||||
|
ext="png",
|
||||||
|
)
|
||||||
|
|
||||||
|
# 6. 更新统计
|
||||||
|
processing_time = time.time() - start_time
|
||||||
|
request_stats["success"] += 1
|
||||||
|
request_stats["total_processing_time"] += processing_time
|
||||||
|
|
||||||
|
logger.success(
|
||||||
|
f"✓ Request completed in {processing_time:.2f}s "
|
||||||
|
f"(inference: {inference_time:.2f}s)"
|
||||||
|
)
|
||||||
|
|
||||||
|
# 7. 返回结果
|
||||||
|
return Response(
|
||||||
|
content=output_bytes,
|
||||||
|
media_type="image/png",
|
||||||
|
headers={
|
||||||
|
"X-Processing-Time": f"{processing_time:.3f}",
|
||||||
|
"X-Image-Size": f"{width}x{height}",
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
except HTTPException:
|
||||||
|
request_stats["failed"] += 1
|
||||||
|
raise
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
request_stats["failed"] += 1
|
||||||
|
logger.error(f"Error processing request: {e}")
|
||||||
|
logger.exception(e)
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=500,
|
||||||
|
detail=f"Processing failed: {str(e)}"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
# ==================== 主函数 ====================
|
||||||
|
def main():
|
||||||
|
"""启动服务"""
|
||||||
|
# 配置日志
|
||||||
|
logger.add(
|
||||||
|
Config.LOG_DIR / "api_{time:YYYY-MM-DD}.log",
|
||||||
|
rotation="1 day",
|
||||||
|
retention="7 days",
|
||||||
|
level="INFO",
|
||||||
|
)
|
||||||
|
|
||||||
|
# 启动服务
|
||||||
|
uvicorn.run(
|
||||||
|
app,
|
||||||
|
host="0.0.0.0",
|
||||||
|
port=8080,
|
||||||
|
log_level="info",
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
81
docker-compose.mvp.yml
Normal file
81
docker-compose.mvp.yml
Normal file
@@ -0,0 +1,81 @@
|
|||||||
|
version: '3.8'
|
||||||
|
|
||||||
|
services:
|
||||||
|
# ==================== API服务 ====================
|
||||||
|
api:
|
||||||
|
build:
|
||||||
|
context: .
|
||||||
|
dockerfile: docker/APIDockerfile
|
||||||
|
container_name: iopaint-api
|
||||||
|
restart: unless-stopped
|
||||||
|
|
||||||
|
ports:
|
||||||
|
- "8080:8080"
|
||||||
|
|
||||||
|
environment:
|
||||||
|
# API配置
|
||||||
|
- API_KEY=${API_KEY:-change_me_in_production}
|
||||||
|
- MAX_IMAGE_SIZE=${MAX_IMAGE_SIZE:-4096}
|
||||||
|
- ENABLE_METRICS=true
|
||||||
|
|
||||||
|
# 模型缓存(使用HuggingFace镜像加速下载)
|
||||||
|
- HF_ENDPOINT=https://hf-mirror.com
|
||||||
|
- HF_HOME=/root/.cache
|
||||||
|
|
||||||
|
# PyTorch配置
|
||||||
|
- PYTORCH_CUDA_ALLOC_CONF=max_split_size_mb:512
|
||||||
|
|
||||||
|
volumes:
|
||||||
|
# 模型缓存目录(避免每次重启重新下载模型)
|
||||||
|
- ./models:/root/.cache:rw
|
||||||
|
|
||||||
|
# 日志目录
|
||||||
|
- ./logs:/app/logs:rw
|
||||||
|
|
||||||
|
deploy:
|
||||||
|
resources:
|
||||||
|
limits:
|
||||||
|
cpus: '4'
|
||||||
|
memory: 8G
|
||||||
|
reservations:
|
||||||
|
devices:
|
||||||
|
- driver: nvidia
|
||||||
|
count: 1
|
||||||
|
capabilities: [gpu]
|
||||||
|
|
||||||
|
# 健康检查
|
||||||
|
healthcheck:
|
||||||
|
test: ["CMD", "curl", "-f", "http://localhost:8080/api/v1/health"]
|
||||||
|
interval: 30s
|
||||||
|
timeout: 10s
|
||||||
|
retries: 3
|
||||||
|
start_period: 60s
|
||||||
|
|
||||||
|
# ==================== Nginx反向代理(可选)====================
|
||||||
|
nginx:
|
||||||
|
image: nginx:alpine
|
||||||
|
container_name: iopaint-nginx
|
||||||
|
restart: unless-stopped
|
||||||
|
ports:
|
||||||
|
- "80:80"
|
||||||
|
- "443:443"
|
||||||
|
volumes:
|
||||||
|
- ./nginx/nginx.conf:/etc/nginx/nginx.conf:ro
|
||||||
|
- ./nginx/ssl:/etc/nginx/ssl:ro # SSL证书目录
|
||||||
|
- ./nginx/logs:/var/log/nginx:rw
|
||||||
|
depends_on:
|
||||||
|
- api
|
||||||
|
profiles:
|
||||||
|
- production # 使用 docker-compose --profile production up 启动
|
||||||
|
|
||||||
|
# ==================== 数据卷 ====================
|
||||||
|
volumes:
|
||||||
|
models:
|
||||||
|
driver: local
|
||||||
|
logs:
|
||||||
|
driver: local
|
||||||
|
|
||||||
|
# ==================== 网络 ====================
|
||||||
|
networks:
|
||||||
|
default:
|
||||||
|
name: iopaint-network
|
||||||
51
docker/APIDockerfile
Normal file
51
docker/APIDockerfile
Normal file
@@ -0,0 +1,51 @@
|
|||||||
|
# IOPaint API Service - MVP Dockerfile
|
||||||
|
# 专门用于去水印API服务的精简镜像
|
||||||
|
|
||||||
|
FROM nvidia/cuda:12.1.0-runtime-ubuntu22.04
|
||||||
|
|
||||||
|
ENV DEBIAN_FRONTEND=noninteractive \
|
||||||
|
PYTHONUNBUFFERED=1
|
||||||
|
|
||||||
|
# 安装系统依赖
|
||||||
|
RUN apt-get update && apt-get install -y --no-install-recommends \
|
||||||
|
python3.11 \
|
||||||
|
python3-pip \
|
||||||
|
libsm6 \
|
||||||
|
libxext6 \
|
||||||
|
libxrender1 \
|
||||||
|
libgl1-mesa-glx \
|
||||||
|
ffmpeg \
|
||||||
|
&& rm -rf /var/lib/apt/lists/*
|
||||||
|
|
||||||
|
# 升级pip
|
||||||
|
RUN pip3 install --no-cache-dir --upgrade pip
|
||||||
|
|
||||||
|
# 安装PyTorch(CUDA 12.1)
|
||||||
|
RUN pip3 install --no-cache-dir \
|
||||||
|
torch torchvision --index-url https://download.pytorch.org/whl/cu121
|
||||||
|
|
||||||
|
WORKDIR /app
|
||||||
|
|
||||||
|
# 复制核心文件(只复制必要的)
|
||||||
|
COPY requirements.txt setup.py ./
|
||||||
|
COPY iopaint ./iopaint
|
||||||
|
|
||||||
|
# 安装Python依赖
|
||||||
|
RUN pip3 install --no-cache-dir -r requirements.txt && \
|
||||||
|
pip3 install --no-cache-dir -e .
|
||||||
|
|
||||||
|
# 复制API服务文件
|
||||||
|
COPY api_service_mvp.py ./
|
||||||
|
|
||||||
|
# 创建日志目录
|
||||||
|
RUN mkdir -p /app/logs
|
||||||
|
|
||||||
|
# 暴露端口
|
||||||
|
EXPOSE 8080
|
||||||
|
|
||||||
|
# 健康检查
|
||||||
|
HEALTHCHECK --interval=30s --timeout=10s --start-period=60s --retries=3 \
|
||||||
|
CMD python3 -c "import requests; requests.get('http://localhost:8080/api/v1/health').raise_for_status()" || exit 1
|
||||||
|
|
||||||
|
# 启动命令
|
||||||
|
CMD ["python3", "api_service_mvp.py"]
|
||||||
133
nginx/nginx.conf
Normal file
133
nginx/nginx.conf
Normal file
@@ -0,0 +1,133 @@
|
|||||||
|
# Nginx配置 - IOPaint API服务
|
||||||
|
|
||||||
|
user nginx;
|
||||||
|
worker_processes auto;
|
||||||
|
error_log /var/log/nginx/error.log warn;
|
||||||
|
pid /var/run/nginx.pid;
|
||||||
|
|
||||||
|
events {
|
||||||
|
worker_connections 1024;
|
||||||
|
}
|
||||||
|
|
||||||
|
http {
|
||||||
|
include /etc/nginx/mime.types;
|
||||||
|
default_type application/octet-stream;
|
||||||
|
|
||||||
|
# 日志格式
|
||||||
|
log_format main '$remote_addr - $remote_user [$time_local] "$request" '
|
||||||
|
'$status $body_bytes_sent "$http_referer" '
|
||||||
|
'"$http_user_agent" "$http_x_forwarded_for" '
|
||||||
|
'rt=$request_time uct="$upstream_connect_time" '
|
||||||
|
'uht="$upstream_header_time" urt="$upstream_response_time"';
|
||||||
|
|
||||||
|
access_log /var/log/nginx/access.log main;
|
||||||
|
|
||||||
|
sendfile on;
|
||||||
|
tcp_nopush on;
|
||||||
|
tcp_nodelay on;
|
||||||
|
keepalive_timeout 65;
|
||||||
|
types_hash_max_size 2048;
|
||||||
|
|
||||||
|
# 客户端配置
|
||||||
|
client_max_body_size 20M; # 允许上传最大20MB
|
||||||
|
client_body_timeout 60s;
|
||||||
|
client_header_timeout 60s;
|
||||||
|
|
||||||
|
# Gzip压缩
|
||||||
|
gzip on;
|
||||||
|
gzip_vary on;
|
||||||
|
gzip_proxied any;
|
||||||
|
gzip_comp_level 6;
|
||||||
|
gzip_types text/plain text/css text/xml text/javascript application/json application/javascript application/xml+rss;
|
||||||
|
|
||||||
|
# 限流配置(防止API滥用)
|
||||||
|
limit_req_zone $binary_remote_addr zone=api_limit:10m rate=10r/s;
|
||||||
|
limit_req_status 429;
|
||||||
|
|
||||||
|
# Upstream配置
|
||||||
|
upstream api_backend {
|
||||||
|
server api:8080;
|
||||||
|
keepalive 32;
|
||||||
|
}
|
||||||
|
|
||||||
|
# HTTP服务器(重定向到HTTPS)
|
||||||
|
server {
|
||||||
|
listen 80;
|
||||||
|
server_name _;
|
||||||
|
|
||||||
|
# 健康检查端点(不需要HTTPS)
|
||||||
|
location /api/v1/health {
|
||||||
|
proxy_pass http://api_backend;
|
||||||
|
}
|
||||||
|
|
||||||
|
# 其他请求重定向到HTTPS
|
||||||
|
location / {
|
||||||
|
return 301 https://$host$request_uri;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
# HTTPS服务器
|
||||||
|
server {
|
||||||
|
listen 443 ssl http2;
|
||||||
|
server_name your-domain.com; # 替换为你的域名
|
||||||
|
|
||||||
|
# SSL证书配置
|
||||||
|
ssl_certificate /etc/nginx/ssl/cert.pem;
|
||||||
|
ssl_certificate_key /etc/nginx/ssl/key.pem;
|
||||||
|
ssl_protocols TLSv1.2 TLSv1.3;
|
||||||
|
ssl_ciphers HIGH:!aNULL:!MD5;
|
||||||
|
ssl_prefer_server_ciphers on;
|
||||||
|
|
||||||
|
# API路由
|
||||||
|
location /api/ {
|
||||||
|
# 限流:每秒10个请求,突发20个
|
||||||
|
limit_req zone=api_limit burst=20 nodelay;
|
||||||
|
|
||||||
|
# 代理设置
|
||||||
|
proxy_pass http://api_backend;
|
||||||
|
proxy_http_version 1.1;
|
||||||
|
proxy_set_header Host $host;
|
||||||
|
proxy_set_header X-Real-IP $remote_addr;
|
||||||
|
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||||
|
proxy_set_header X-Forwarded-Proto $scheme;
|
||||||
|
proxy_set_header Connection "";
|
||||||
|
|
||||||
|
# 超时设置(图片处理可能较慢)
|
||||||
|
proxy_connect_timeout 60s;
|
||||||
|
proxy_send_timeout 120s;
|
||||||
|
proxy_read_timeout 120s;
|
||||||
|
|
||||||
|
# 缓冲区设置
|
||||||
|
proxy_buffering off;
|
||||||
|
proxy_request_buffering off;
|
||||||
|
}
|
||||||
|
|
||||||
|
# 文档路由
|
||||||
|
location ~ ^/(docs|redoc|openapi.json) {
|
||||||
|
proxy_pass http://api_backend;
|
||||||
|
proxy_set_header Host $host;
|
||||||
|
proxy_set_header X-Real-IP $remote_addr;
|
||||||
|
}
|
||||||
|
|
||||||
|
# 根路径
|
||||||
|
location / {
|
||||||
|
proxy_pass http://api_backend;
|
||||||
|
proxy_set_header Host $host;
|
||||||
|
}
|
||||||
|
|
||||||
|
# 自定义错误页面
|
||||||
|
error_page 429 /429.html;
|
||||||
|
location = /429.html {
|
||||||
|
internal;
|
||||||
|
default_type application/json;
|
||||||
|
return 429 '{"error": "Too Many Requests", "detail": "Rate limit exceeded. Please try again later."}';
|
||||||
|
}
|
||||||
|
|
||||||
|
error_page 502 503 504 /50x.html;
|
||||||
|
location = /50x.html {
|
||||||
|
internal;
|
||||||
|
default_type application/json;
|
||||||
|
return 503 '{"error": "Service Unavailable", "detail": "The service is temporarily unavailable. Please try again later."}';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user