From 81b3625fdfecb9f7cb3fbd73321ff5c53810215f Mon Sep 17 00:00:00 2001 From: let5sne Date: Fri, 28 Nov 2025 17:46:23 +0000 Subject: [PATCH] =?UTF-8?q?=E2=9C=A8=20=E6=B7=BB=E5=8A=A0=E5=8E=BB?= =?UTF-8?q?=E6=B0=B4=E5=8D=B0API=E6=9C=8D=E5=8A=A1=20-=20MVP=E7=89=88?= =?UTF-8?q?=E6=9C=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 新增功能: - 精简的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 --- API_CLIENT_EXAMPLES.md | 776 +++++++++++++++++++++++++++++++++++++++++ API_SERVICE_GUIDE.md | 526 ++++++++++++++++++++++++++++ API_SERVICE_README.md | 303 ++++++++++++++++ api_service_mvp.py | 369 ++++++++++++++++++++ docker-compose.mvp.yml | 81 +++++ docker/APIDockerfile | 51 +++ nginx/nginx.conf | 133 +++++++ 7 files changed, 2239 insertions(+) create mode 100644 API_CLIENT_EXAMPLES.md create mode 100644 API_SERVICE_GUIDE.md create mode 100644 API_SERVICE_README.md create mode 100644 api_service_mvp.py create mode 100644 docker-compose.mvp.yml create mode 100644 docker/APIDockerfile create mode 100644 nginx/nginx.conf diff --git a/API_CLIENT_EXAMPLES.md b/API_CLIENT_EXAMPLES.md new file mode 100644 index 0000000..36d9aa3 --- /dev/null +++ b/API_CLIENT_EXAMPLES.md @@ -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 +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 diff --git a/API_SERVICE_GUIDE.md b/API_SERVICE_GUIDE.md new file mode 100644 index 0000000..4142c39 --- /dev/null +++ b/API_SERVICE_GUIDE.md @@ -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单机** | 个人/小团队
月< 10万张 | • 部署简单
• 成本低
• 维护容易 | • 无法扩展
• 单点故障
• 性能有限 | ¥300-500 | +| **Docker Compose多容器** | 小型商业
月10-50万张 | • 支持多实例
• 负载均衡
• 成本可控 | • 手动扩展
• 监控有限
• 高可用差 | ¥1000-3000 | +| **Kubernetes** | 中大型商业
月50万张+ | • 自动扩展
• 高可用
• 完善监控
• 多云部署 | • 复杂度高
• 学习成本
• 初期成本高 | ¥5000-20000+ | +| **Serverless (Lambda/云函数)** | 不规则流量
峰谷明显 | • 按用付费
• 无需运维
• 无限扩展 | • 冷启动慢
• GPU支持差
• 单次限制 | 按用量计费 | + +--- + +## 💰 成本与扩展性分析 + +### 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) diff --git a/API_SERVICE_README.md b/API_SERVICE_README.md new file mode 100644 index 0000000..95c26cb --- /dev/null +++ b/API_SERVICE_README.md @@ -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文档! diff --git a/api_service_mvp.py b/api_service_mvp.py new file mode 100644 index 0000000..61fac8d --- /dev/null +++ b/api_service_mvp.py @@ -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() diff --git a/docker-compose.mvp.yml b/docker-compose.mvp.yml new file mode 100644 index 0000000..9fce3e4 --- /dev/null +++ b/docker-compose.mvp.yml @@ -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 diff --git a/docker/APIDockerfile b/docker/APIDockerfile new file mode 100644 index 0000000..81ab35b --- /dev/null +++ b/docker/APIDockerfile @@ -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"] diff --git a/nginx/nginx.conf b/nginx/nginx.conf new file mode 100644 index 0000000..ecd64df --- /dev/null +++ b/nginx/nginx.conf @@ -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."}'; + } + } +}