yaoqifa commited on
Commit
d321018
·
verified ·
1 Parent(s): 1561f27

Upload app.py

Browse files
Files changed (1) hide show
  1. app.py +310 -212
app.py CHANGED
@@ -1,212 +1,310 @@
1
- import httpx
2
- import re
3
- import base64
4
- import os
5
- import asyncio
6
- import time
7
- import gc # 引入垃圾回收
8
- from datetime import datetime
9
- from PIL import Image
10
- from io import BytesIO
11
- from openai import AsyncOpenAI
12
- from fastapi import FastAPI, HTTPException, Request
13
- from fastapi.staticfiles import StaticFiles
14
- from fastapi.responses import FileResponse, JSONResponse
15
- from pydantic import BaseModel
16
- import uvicorn
17
-
18
- # ==========================================
19
- # 1. 配置与工具
20
- # ==========================================
21
-
22
- # --- 关键修改:全局并发限制 ---
23
- # 限制同时进行处理的请求数量为 3。
24
- # 这样即使有 100 个人同时点,同时也只有 3 个人在占用内存处理图片,其他人排队。
25
- # 既保护了内存,又不会报错。
26
- CONCURRENCY_LIMITER = asyncio.Semaphore(5)
27
-
28
- AR_BASES = {
29
- "1:1 (SDXL标准 1024)": (1024, 1024),
30
- "16:9 (SDXL标准 768p)": (1344, 768),
31
- "16:9 (1080p Full HD)": (1920, 1080),
32
- "9:16 (手机竖屏)": (768, 1344),
33
- "4:3 (标准)": (1152, 896),
34
- "21:9 (宽屏)": (1536, 640)
35
- }
36
-
37
- SCALE_MULTIPLIERS = {
38
- "1k": 1.0,
39
- "2k": 1.5,
40
- "4k": 2.0
41
- }
42
-
43
- class GenerateRequest(BaseModel):
44
- api_key: str
45
- model: str
46
- prompt: str
47
- ratio: str
48
- scale: str
49
- custom_size: str
50
- format: str
51
-
52
- def process_image_in_memory(image_bytes: bytes, target_format: str) -> str:
53
- try:
54
- # 使用上下文管理器打开,确保及时关闭
55
- with Image.open(BytesIO(image_bytes)) as img:
56
- output_buffer = BytesIO()
57
-
58
- if target_format == "jpg":
59
- img = img.convert("RGB")
60
- img.save(output_buffer, format="JPEG", quality=95)
61
- else:
62
- img.save(output_buffer, format="PNG")
63
-
64
- result = base64.b64encode(output_buffer.getvalue()).decode('utf-8')
65
-
66
- # 显式关闭 buffer
67
- output_buffer.close()
68
- return result
69
- except Exception as e:
70
- raise Exception(f"格式转换失败: {str(e)}")
71
- finally:
72
- # 强制垃圾回收,防止大图片占用内存不释放
73
- gc.collect()
74
-
75
- async def download_image_as_bytes(url):
76
- print(f"📥 [System] 正在下载图片: {url}")
77
- # 增加超时时间,防止大图下载断开
78
- async with httpx.AsyncClient(timeout=60) as client:
79
- response = await client.get(url)
80
- if response.status_code == 200:
81
- return response.content
82
- else:
83
- raise Exception(f"下载失败 HTTP {response.status_code}")
84
-
85
- # ==========================================
86
- # 2. 核心生成逻辑 (异步)
87
- # ==========================================
88
-
89
- async def core_generate(req: GenerateRequest):
90
- # 1. 计算分辨率
91
- final_size = "1024x1024"
92
- ratio_str = "1:1"
93
-
94
- if req.ratio == "custom":
95
- final_size = req.custom_size.strip()
96
- else:
97
- base_w, base_h = AR_BASES.get(req.ratio, (1024, 1024))
98
- multiplier = SCALE_MULTIPLIERS.get(req.scale, 1.0)
99
- final_size = f"{int(base_w * multiplier)}x{int(base_h * multiplier)}"
100
- ratio_str = req.ratio.split(" ")[0]
101
-
102
- # 2. 处理 Prompt
103
- final_prompt = req.prompt
104
- if "gemini" in req.model.lower():
105
- if req.ratio == "custom":
106
- final_prompt = f"{req.prompt} --resolution {final_size}"
107
- else:
108
- final_prompt = f"{req.prompt} --ar {ratio_str} --resolution {final_size}"
109
-
110
- # 3. 初始化客户端
111
- client = AsyncOpenAI(
112
- api_key=req.api_key,
113
- base_url="https://api.cloudapp.ink/v1",
114
- max_retries=0
115
- )
116
-
117
- image_bytes = None
118
-
119
- # 4. 执行请求
120
- if "gemini" in req.model.lower():
121
- # --- Gemini Logic ---
122
- print(f"🟣 [Chat模式] 请求 Gemini: {req.model}")
123
- response = await client.chat.completions.create(
124
- model=req.model,
125
- messages=[{"role": "user", "content": final_prompt}],
126
- extra_body={"modalities": ["image", "text"]}
127
- )
128
- content = response.choices[0].message.content
129
-
130
- # 提取链接或 Base64
131
- url_match = re.search(r"!\[.*?\]\((https?://[^\)]+)\)", content)
132
- if not url_match:
133
- url_match = re.search(r"(https?://\S+\.(?:png|jpg|jpeg|webp))", content)
134
-
135
- if url_match:
136
- image_bytes = await download_image_as_bytes(url_match.group(1))
137
- elif "base64," in content:
138
- print(f"🟣 [Chat模式] 检测到 Base64 流")
139
- b64_str = content.split("base64,")[1].split(")")[0].strip()
140
- image_bytes = base64.b64decode(b64_str)
141
- else:
142
- raise Exception("未能识别图片内容")
143
-
144
- else:
145
- # --- Nano Banana (Standard) Logic ---
146
- print(f"🔵 [绘图模式] 请求 Standard: {req.model} | 分辨率: {final_size}")
147
- response = await client.images.generate(
148
- model=req.model,
149
- prompt=final_prompt,
150
- n=1,
151
- size=final_size,
152
- response_format="b64_json"
153
- )
154
- print(f"🔵 [绘图模式] 请求成功")
155
-
156
- data = response.data[0]
157
- if getattr(data, 'b64_json', None):
158
- image_bytes = base64.b64decode(data.b64_json)
159
- elif hasattr(data, 'url'):
160
- image_bytes = await download_image_as_bytes(data.url)
161
- else:
162
- raise Exception("API 未返回有效图片数据")
163
-
164
- # 5. 格式转换
165
- return process_image_in_memory(image_bytes, req.format)
166
-
167
-
168
- # ==========================================
169
- # 3. FastAPI App
170
- # ==========================================
171
- app = FastAPI()
172
-
173
- app.mount("/static", StaticFiles(directory="static"), name="static")
174
-
175
- @app.get("/")
176
- async def read_index():
177
- return FileResponse('static/index.html')
178
-
179
- @app.get("/health")
180
- def health():
181
- return {"status": "ok"}
182
-
183
- @app.post("/api/generate")
184
- async def generate_api(req: GenerateRequest, request: Request):
185
- client_host = request.client.host
186
- timestamp = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
187
-
188
- print("\n" + "="*60)
189
- print(f"🚀 [New Request] {timestamp} from {client_host}")
190
- print(f"📌 模型: {req.model} | 规格: {req.ratio} ({req.scale})")
191
- print("-" * 60)
192
-
193
- # --- 关键修改:使用 Semaphore 进行限流 ---
194
- # 只有拿到“通行证”的请求才能进入下面的代码块执行
195
- # 没拿到的会在这一行等待,直到别人执行完释放内存
196
- async with CONCURRENCY_LIMITER:
197
- try:
198
- start_time = time.time()
199
- base64_img = await core_generate(req)
200
- duration = time.time() - start_time
201
- print(f"✅ [Success] 耗时: {duration:.2f}s")
202
-
203
- return {"status": "success", "image_base64": base64_img}
204
- except Exception as e:
205
- print(f"❌ [Failed] 失败: {str(e)}")
206
- return JSONResponse(
207
- content={"status": "error", "message": str(e)},
208
- status_code=200
209
- )
210
- finally:
211
- # 每次请求结束后,强制清理一次内存
212
- gc.collect()
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import httpx
2
+ import re
3
+ import base64
4
+ import os
5
+ import asyncio
6
+ import time
7
+ import gc
8
+ from datetime import datetime
9
+ from PIL import Image
10
+ from io import BytesIO
11
+ from openai import AsyncOpenAI
12
+ from fastapi import FastAPI, HTTPException, Request
13
+ from fastapi.staticfiles import StaticFiles
14
+ from fastapi.responses import FileResponse, JSONResponse
15
+ from pydantic import BaseModel
16
+ import uvicorn
17
+
18
+ # ==========================================
19
+ # 1. 配置区域
20
+ # ==========================================
21
+
22
+ # --- 百度内容审核配置 (从环境变量获取) ---
23
+ # 如果环境变量没配置,默认为空字符串,相关功能会自动关闭
24
+ BAIDU_API_KEY = os.getenv("BAIDU_API_KEY", "")
25
+ BAIDU_SECRET_KEY = os.getenv("BAIDU_SECRET_KEY", "")
26
+
27
+ # --- 并发限制 ---
28
+ CONCURRENCY_LIMITER = asyncio.Semaphore(3)
29
+
30
+ # --- 基础配置 ---
31
+ AR_BASES = {
32
+ "1:1 (SDXL标准 1024)": (1024, 1024),
33
+ "16:9 (SDXL标准 768p)": (1344, 768),
34
+ "16:9 (1080p Full HD)": (1920, 1080),
35
+ "9:16 (手机竖屏)": (768, 1344),
36
+ "4:3 (标准)": (1152, 896),
37
+ "21:9 (宽屏)": (1536, 640)
38
+ }
39
+
40
+ SCALE_MULTIPLIERS = {
41
+ "1k": 1.0, "2k": 1.5, "4k": 2.0
42
+ }
43
+
44
+ # --- 本地敏感词 (第一道防线) ---
45
+ SENSITIVE_WORDS = [
46
+ "nsfw", "nude", "naked", "sex", "porn", "hentai",
47
+ "裸", "色情", "全裸", "blood", "kill", "murder",
48
+ "血腥", "尸体", "毒品"
49
+ ]
50
+
51
+ # ==========================================
52
+ # 2. 工具函数 (百度审核 & 图像处理)
53
+ # ==========================================
54
+
55
+ # 全局缓存 Access Token
56
+ _BAIDU_TOKEN_CACHE = {"token": None, "expires_at": 0}
57
+
58
+ async def get_baidu_access_token():
59
+ """获取百度 Access Token"""
60
+ # 如果环境变量没填,直接返回 None,跳过百度审核
61
+ if not BAIDU_API_KEY or not BAIDU_SECRET_KEY:
62
+ return None
63
+
64
+ now = time.time()
65
+ if _BAIDU_TOKEN_CACHE["token"] and now < _BAIDU_TOKEN_CACHE["expires_at"]:
66
+ return _BAIDU_TOKEN_CACHE["token"]
67
+
68
+ url = "https://aip.baidubce.com/oauth/2.0/token"
69
+ params = {
70
+ "grant_type": "client_credentials",
71
+ "client_id": BAIDU_API_KEY,
72
+ "client_secret": BAIDU_SECRET_KEY
73
+ }
74
+
75
+ try:
76
+ async with httpx.AsyncClient(timeout=10) as client:
77
+ resp = await client.post(url, params=params)
78
+ data = resp.json()
79
+ token = data.get("access_token")
80
+ if token:
81
+ _BAIDU_TOKEN_CACHE["token"] = token
82
+ _BAIDU_TOKEN_CACHE["expires_at"] = now + (20 * 24 * 3600)
83
+ return token
84
+ else:
85
+ print(f"⚠️ 百度 Token 获取失败: {data}")
86
+ return None
87
+ except Exception as e:
88
+ print(f"⚠️ 百度 Token 网络错误: {e}")
89
+ return None
90
+
91
+ async def check_baidu_text_censor(text):
92
+ """调用百度 API 检查文本"""
93
+ token = await get_baidu_access_token()
94
+ if not token:
95
+ # 如果获取不到 token (未配置环境变量或接口挂了),默认放行
96
+ return False, None
97
+
98
+ url = f"https://aip.baidubce.com/rest/2.0/solution/v1/text_censor/v2/user_defined?access_token={token}"
99
+ data = {'text': text}
100
+
101
+ try:
102
+ async with httpx.AsyncClient(timeout=10) as client:
103
+ resp = await client.post(url, data=data)
104
+ result = resp.json()
105
+
106
+ # conclusionType: 1.合规 2.不合规 3.疑似 4.审核失败
107
+ conclusion_type = result.get('conclusionType')
108
+
109
+ if conclusion_type == 2:
110
+ msg_list = []
111
+ if 'data' in result:
112
+ for item in result['data']:
113
+ msg_list.append(item.get('msg', '违规内容'))
114
+ reason = ",".join(msg_list) if msg_list else "内容不合规"
115
+ return True, reason
116
+ elif conclusion_type == 3:
117
+ return True, "内容疑似违规"
118
+
119
+ return False, None
120
+
121
+ except Exception as e:
122
+ print(f"⚠️ 百度审核接口异常: {e}")
123
+ return False, None
124
+
125
+ def contains_local_sensitive_words(text: str):
126
+ text_lower = text.lower()
127
+ for word in SENSITIVE_WORDS:
128
+ if word.lower() in text_lower:
129
+ return True, word
130
+ return False, None
131
+
132
+ class GenerateRequest(BaseModel):
133
+ api_key: str
134
+ model: str
135
+ prompt: str
136
+ ratio: str
137
+ scale: str
138
+ custom_size: str
139
+ format: str
140
+
141
+ def process_image_in_memory(image_bytes: bytes, target_format: str) -> str:
142
+ try:
143
+ with Image.open(BytesIO(image_bytes)) as img:
144
+ output_buffer = BytesIO()
145
+ if target_format == "jpg":
146
+ img = img.convert("RGB")
147
+ img.save(output_buffer, format="JPEG", quality=95)
148
+ else:
149
+ img.save(output_buffer, format="PNG")
150
+ return base64.b64encode(output_buffer.getvalue()).decode('utf-8')
151
+ except Exception as e:
152
+ raise Exception(f"格式转换失败: {str(e)}")
153
+ finally:
154
+ gc.collect()
155
+
156
+ async def download_image_as_bytes(url):
157
+ print(f"📥 [System] 正在下载图片: {url}")
158
+ async with httpx.AsyncClient(timeout=60) as client:
159
+ response = await client.get(url)
160
+ if response.status_code == 200:
161
+ return response.content
162
+ else:
163
+ raise Exception(f"下载失败 HTTP {response.status_code}")
164
+
165
+ # ==========================================
166
+ # 3. 核心生成逻辑
167
+ # ==========================================
168
+
169
+ async def core_generate(req: GenerateRequest):
170
+ final_size = "1024x1024"
171
+ ratio_str = "1:1"
172
+ if req.ratio == "custom":
173
+ final_size = req.custom_size.strip()
174
+ else:
175
+ base_w, base_h = AR_BASES.get(req.ratio, (1024, 1024))
176
+ multiplier = SCALE_MULTIPLIERS.get(req.scale, 1.0)
177
+ final_size = f"{int(base_w * multiplier)}x{int(base_h * multiplier)}"
178
+ ratio_str = req.ratio.split(" ")[0]
179
+
180
+ final_prompt = req.prompt
181
+ if "gemini" in req.model.lower():
182
+ if req.ratio == "custom":
183
+ final_prompt = f"{req.prompt} --resolution {final_size}"
184
+ else:
185
+ final_prompt = f"{req.prompt} --ar {ratio_str} --resolution {final_size}"
186
+
187
+ client = AsyncOpenAI(
188
+ api_key=req.api_key,
189
+ base_url="https://api.cloudapp.ink/v1",
190
+ max_retries=0,
191
+ timeout=60.0
192
+ )
193
+
194
+ image_bytes = None
195
+
196
+ if "gemini" in req.model.lower():
197
+ print(f"🟣 [Chat模式] 请求 Gemini: {req.model}")
198
+ response = await client.chat.completions.create(
199
+ model=req.model,
200
+ messages=[{"role": "user", "content": final_prompt}],
201
+ extra_body={"modalities": ["image", "text"]}
202
+ )
203
+ content = response.choices[0].message.content
204
+
205
+ url_match = re.search(r"!\[.*?\]\((https?://[^\)]+)\)", content)
206
+ if not url_match:
207
+ url_match = re.search(r"(https?://\S+\.(?:png|jpg|jpeg|webp))", content)
208
+
209
+ if url_match:
210
+ image_bytes = await download_image_as_bytes(url_match.group(1))
211
+ elif "base64," in content:
212
+ print(f"🟣 [Chat模式] 检测到 Base64 流")
213
+ b64_str = content.split("base64,")[1].split(")")[0].strip()
214
+ image_bytes = base64.b64decode(b64_str)
215
+ else:
216
+ raise Exception("未能识别图片内容")
217
+
218
+ else:
219
+ print(f"🔵 [绘图模式] 请求 Standard: {req.model} | 分辨率: {final_size}")
220
+ response = await client.images.generate(
221
+ model=req.model,
222
+ prompt=final_prompt,
223
+ n=1,
224
+ size=final_size,
225
+ response_format="b64_json"
226
+ )
227
+ print(f"🔵 [绘图模式] 请求成功")
228
+
229
+ data = response.data[0]
230
+ if getattr(data, 'b64_json', None):
231
+ image_bytes = base64.b64decode(data.b64_json)
232
+ elif hasattr(data, 'url'):
233
+ image_bytes = await download_image_as_bytes(data.url)
234
+ else:
235
+ raise Exception("API 未返回有效图片数据")
236
+
237
+ return process_image_in_memory(image_bytes, req.format)
238
+
239
+ # ==========================================
240
+ # 4. FastAPI App
241
+ # ==========================================
242
+ app = FastAPI()
243
+
244
+ app.mount("/static", StaticFiles(directory="static"), name="static")
245
+
246
+ @app.get("/")
247
+ async def read_index():
248
+ return FileResponse('static/index.html')
249
+
250
+ @app.get("/health")
251
+ def health():
252
+ return {"status": "ok"}
253
+
254
+ @app.post("/api/generate")
255
+ async def generate_api(req: GenerateRequest, request: Request):
256
+ client_host = request.client.host
257
+ timestamp = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
258
+
259
+ print("\n" + "="*60)
260
+ print(f"🚀 [Request] {timestamp} | IP: {client_host}")
261
+ print(f"📌 Model: {req.model}")
262
+ print(f"💡 Prompt: {req.prompt}")
263
+
264
+ # 1. 本地敏感词检查
265
+ is_local_sensitive, word = contains_local_sensitive_words(req.prompt)
266
+ if is_local_sensitive:
267
+ print(f"🚫 [Blocked-Local] 命中本地词库: {word}")
268
+ return JSONResponse(
269
+ content={
270
+ "status": "error",
271
+ "message": f"提示词包含违禁词汇 ({word}),请求被拒绝。"
272
+ },
273
+ status_code=200
274
+ )
275
+
276
+ # 2. 百度 API 检查 (动态读取环境变量)
277
+ is_baidu_sensitive, reason = await check_baidu_text_censor(req.prompt)
278
+ if is_baidu_sensitive:
279
+ print(f"🚫 [Blocked-Baidu] 百度审核拦截: {reason}")
280
+ return JSONResponse(
281
+ content={
282
+ "status": "error",
283
+ "message": f"提示词未通过智能安全审核: {reason}"
284
+ },
285
+ status_code=200
286
+ )
287
+
288
+ print("-" * 60)
289
+
290
+ # 3. 正常生成
291
+ async with CONCURRENCY_LIMITER:
292
+ try:
293
+ start_time = time.time()
294
+ base64_img = await core_generate(req)
295
+
296
+ duration = time.time() - start_time
297
+ print(f"✅ [Success] 耗时: {duration:.2f}s")
298
+ return {"status": "success", "image_base64": base64_img}
299
+
300
+ except Exception as e:
301
+ print(f"❌ [Failed] 错误: {str(e)}")
302
+ return JSONResponse(
303
+ content={"status": "error", "message": str(e)},
304
+ status_code=200
305
+ )
306
+ finally:
307
+ gc.collect()
308
+
309
+ if __name__ == "__main__":
310
+ uvicorn.run(app, host="0.0.0.0", port=7860)