yaoqifa commited on
Commit
c13e4f1
·
verified ·
1 Parent(s): c578bfa

Upload app.py

Browse files
Files changed (1) hide show
  1. app.py +77 -62
app.py CHANGED
@@ -6,7 +6,7 @@ import asyncio
6
  import time
7
  import gc
8
  from datetime import datetime
9
- from collections import deque # 引入双端队列
10
  from PIL import Image
11
  from io import BytesIO
12
  from openai import AsyncOpenAI
@@ -21,28 +21,25 @@ import uvicorn
21
  # ==========================================
22
  CONCURRENCY_LIMITER = asyncio.Semaphore(3)
23
 
24
- # --- 🚀 动态违禁词缓存 (核心修改) ---
25
- # maxlen=200: 自动保持最新的200个,旧的自动丢弃
26
  DYNAMIC_BLOCK_CACHE = deque(maxlen=200)
27
 
28
- # 硬编码的基础高危词 (保留最基本的底线,防止API挂了时裸奔)
29
  SENSITIVE_WORDS = [
30
  "nsfw", "nude", "naked", "sex", "porn", "hentai",
31
  "裸", "色情", "全裸", "blood", "kill", "murder",
32
  "血腥", "尸体", "毒品", "杀"
33
  ]
34
 
35
- AR_BASES = {
36
- "1:1 (SDXL标准 1024)": (1024, 1024),
37
- "16:9 (SDXL标准 768p)": (1344, 768),
38
- "16:9 (1080p Full HD)": (1920, 1080),
39
- "9:16 (手机竖屏)": (768, 1344),
40
- "4:3 (标准)": (1152, 896),
41
- "21:9 (宽屏)": (1536, 640)
 
 
42
  }
43
- SCALE_MULTIPLIERS = {"1k": 1.0, "2k": 1.5, "4k": 2.0}
44
 
45
- # IP 限流记录
46
  IP_REQUEST_HISTORY = {}
47
 
48
  # ==========================================
@@ -68,25 +65,14 @@ def check_ip_rate_limit(ip: str, limit: int = 5, window: int = 60):
68
  return False
69
 
70
  def contains_local_sensitive_words(text: str):
71
- """
72
- 检查本地缓存
73
- 1. 检查最近被百度封杀的200个Prompt (精确匹配)
74
- 2. 检查硬编码的关键词 (模糊匹配)
75
- """
76
- # 1. 检查动态缓存 (O(n) 遍历,但n最大200,极快)
77
- if text in DYNAMIC_BLOCK_CACHE:
78
- return True, "最近违规记录 (已缓存)"
79
-
80
- # 2. 检查静态关键词
81
  text_lower = text.lower()
82
  for word in SENSITIVE_WORDS:
83
- if word and word.lower() in text_lower:
84
- return True, word
85
-
86
  return False, None
87
 
88
  # ==========================================
89
- # 3. 百度审核 & 生成
90
  # ==========================================
91
 
92
  _BAIDU_TOKEN_CACHE = {"token": None, "expires_at": 0}
@@ -104,8 +90,7 @@ async def get_baidu_access_token():
104
  try:
105
  async with httpx.AsyncClient(timeout=10) as client:
106
  resp = await client.post(url, params=params)
107
- data = resp.json()
108
- token = data.get("access_token")
109
  if token:
110
  _BAIDU_TOKEN_CACHE["token"] = token
111
  _BAIDU_TOKEN_CACHE["expires_at"] = now + 1728000
@@ -124,16 +109,12 @@ async def check_baidu_text_censor(text):
124
  async with httpx.AsyncClient(timeout=10) as client:
125
  resp = await client.post(url, data=data, headers=headers)
126
  result = resp.json()
127
-
128
  if result.get('conclusionType') == 2:
129
  msg = [i.get('msg') for i in result.get('data', [])]
130
  reason = ",".join(msg) if msg else "违规"
131
-
132
- # 🚀 发现新词:存入内存队列
133
  if text not in DYNAMIC_BLOCK_CACHE:
134
  DYNAMIC_BLOCK_CACHE.append(text)
135
- print(f"🔒 [Cache] 缓存违规Prompt ({len(DYNAMIC_BLOCK_CACHE)}/200): {text[:15]}...")
136
-
137
  return True, reason
138
  elif result.get('conclusionType') == 3: return True, "疑似违规"
139
  return False, None
@@ -145,60 +126,96 @@ class GenerateRequest(BaseModel):
145
  prompt: str
146
  ratio: str
147
  scale: str
148
- custom_size: str
149
- format: str
150
 
151
- def process_image_in_memory(image_bytes: bytes, target_format: str) -> str:
152
  try:
153
  with Image.open(BytesIO(image_bytes)) as img:
154
  out = BytesIO()
 
 
 
 
 
 
 
 
 
 
 
 
 
 
155
  fmt = "JPEG" if target_format == "jpg" else "PNG"
156
- img = img.convert("RGB") if target_format == "jpg" else img
157
- img.save(out, format=fmt, quality=95)
158
  return base64.b64encode(out.getvalue()).decode('utf-8')
159
- except Exception as e: raise Exception(f"IMG Error: {e}")
160
- finally: gc.collect()
 
 
161
 
162
  async def download_image_as_bytes(url):
163
  print(f"📥 下载: {url}")
164
- async with httpx.AsyncClient(timeout=60) as client:
165
  r = await client.get(url)
166
  if r.status_code == 200: return r.content
167
  raise Exception(f"HTTP {r.status_code}")
168
 
169
  async def core_generate(req: GenerateRequest):
170
- fs = "1024x1024"
171
- if req.ratio == "custom": fs = req.custom_size.strip()
172
- else:
173
- w, h = AR_BASES.get(req.ratio, (1024, 1024))
174
- m = SCALE_MULTIPLIERS.get(req.scale, 1.0)
175
- fs = f"{int(w*m)}x{int(h*m)}"
176
 
177
- fp = req.prompt
 
 
 
 
 
 
 
 
 
 
178
  if "gemini" in req.model.lower():
179
- suffix = f"--resolution {fs}" if req.ratio=="custom" else f"--ar {req.ratio.split()[0]} --resolution {fs}"
180
- fp = f"{req.prompt} {suffix}"
181
-
 
 
 
 
 
182
  client = AsyncOpenAI(api_key=req.api_key, base_url="https://api.cloudapp.ink/v1", max_retries=0, timeout=60.0)
183
-
184
  img_b = None
 
185
  if "gemini" in req.model.lower():
186
  print(f"🟣 Gemini: {req.model}")
187
- res = await client.chat.completions.create(model=req.model, messages=[{"role":"user","content":fp}], extra_body={"modalities":["image","text"]})
 
 
 
 
188
  c = res.choices[0].message.content
189
  u = re.search(r"!\[.*?\]\((https?://[^\)]+)\)", c) or re.search(r"(https?://\S+\.(?:png|jpg|jpeg|webp))", c)
190
  if u: img_b = await download_image_as_bytes(u.group(1))
191
  elif "base64," in c: img_b = base64.b64decode(c.split("base64,")[1].split(")")[0].strip())
192
- else: raise Exception("No Image")
193
  else:
194
- print(f"🔵 Standard: {req.model}")
195
- res = await client.images.generate(model=req.model, prompt=fp, n=1, size=fs, response_format="b64_json")
 
 
196
  d = res.data[0]
197
  if getattr(d,'b64_json',None): img_b = base64.b64decode(d.b64_json)
198
  elif hasattr(d,'url'): img_b = await download_image_as_bytes(d.url)
199
  else: raise Exception("No Data")
200
 
201
- return process_image_in_memory(img_b, req.format)
202
 
203
  # ==========================================
204
  # 4. FastAPI App
@@ -215,19 +232,17 @@ def health(): return {"status": "ok"}
215
  @app.post("/api/generate")
216
  async def generate_api(req: GenerateRequest, request: Request):
217
  ip = get_real_ip(request)
218
- print(f"\n🚀 [{datetime.now().strftime('%H:%M:%S')}] IP: {ip} | Prompt: {req.prompt[:20]}...")
219
 
220
- if check_ip_rate_limit(ip, 5, 60):
221
  return JSONResponse({"status": "error", "message": "请求过快"}, 429)
222
 
223
  is_loc, word = contains_local_sensitive_words(req.prompt)
224
  if is_loc:
225
- print(f"🚫 [Local] 拦截: {word}")
226
  return JSONResponse({"status": "error", "message": f"违规内容: {word}"}, 200)
227
 
228
  is_bd, reason = await check_baidu_text_censor(req.prompt)
229
  if is_bd:
230
- print(f"🚫 [Baidu] 拦截: {reason}")
231
  return JSONResponse({"status": "error", "message": f"审核未通过: {reason}"}, 200)
232
 
233
  async with CONCURRENCY_LIMITER:
 
6
  import time
7
  import gc
8
  from datetime import datetime
9
+ from collections import deque
10
  from PIL import Image
11
  from io import BytesIO
12
  from openai import AsyncOpenAI
 
21
  # ==========================================
22
  CONCURRENCY_LIMITER = asyncio.Semaphore(3)
23
 
 
 
24
  DYNAMIC_BLOCK_CACHE = deque(maxlen=200)
25
 
 
26
  SENSITIVE_WORDS = [
27
  "nsfw", "nude", "naked", "sex", "porn", "hentai",
28
  "裸", "色情", "全裸", "blood", "kill", "murder",
29
  "血腥", "尸体", "毒品", "杀"
30
  ]
31
 
32
+ # 精确的枚举表
33
+ RESOLUTION_MAP = {
34
+ "1:1": {"1k": (1024, 1024), "2k": (2048, 2048), "4k": (4096, 4096)},
35
+ "16:9": {"1k": (1280, 720), "2k": (2560, 1440), "4k": (3840, 2160)},
36
+ "9:16": {"1k": (720, 1280), "2k": (1440, 2560), "4k": (2160, 3840)},
37
+ "4:3": {"1k": (1152, 864), "2k": (2048, 1536), "4k": (2880, 2160)},
38
+ "3:4": {"1k": (864, 1152), "2k": (1536, 2048), "4k": (2160, 2880)},
39
+ "21:9": {"1k": (1680, 720), "2k": (3360, 1440), "4k": (5040, 2160)},
40
+ "2:3": {"1k": (852, 1280), "2k": (1704, 2560), "4k": (2440, 3660)}
41
  }
 
42
 
 
43
  IP_REQUEST_HISTORY = {}
44
 
45
  # ==========================================
 
65
  return False
66
 
67
  def contains_local_sensitive_words(text: str):
68
+ if text in DYNAMIC_BLOCK_CACHE: return True, text
 
 
 
 
 
 
 
 
 
69
  text_lower = text.lower()
70
  for word in SENSITIVE_WORDS:
71
+ if word and word.lower() in text_lower: return True, word
 
 
72
  return False, None
73
 
74
  # ==========================================
75
+ # 3. 百度审核 & 生成逻辑
76
  # ==========================================
77
 
78
  _BAIDU_TOKEN_CACHE = {"token": None, "expires_at": 0}
 
90
  try:
91
  async with httpx.AsyncClient(timeout=10) as client:
92
  resp = await client.post(url, params=params)
93
+ token = resp.json().get("access_token")
 
94
  if token:
95
  _BAIDU_TOKEN_CACHE["token"] = token
96
  _BAIDU_TOKEN_CACHE["expires_at"] = now + 1728000
 
109
  async with httpx.AsyncClient(timeout=10) as client:
110
  resp = await client.post(url, data=data, headers=headers)
111
  result = resp.json()
 
112
  if result.get('conclusionType') == 2:
113
  msg = [i.get('msg') for i in result.get('data', [])]
114
  reason = ",".join(msg) if msg else "违规"
 
 
115
  if text not in DYNAMIC_BLOCK_CACHE:
116
  DYNAMIC_BLOCK_CACHE.append(text)
117
+ print(f"🔒 [Cache] 新增违规: {text[:10]}...")
 
118
  return True, reason
119
  elif result.get('conclusionType') == 3: return True, "疑似违规"
120
  return False, None
 
126
  prompt: str
127
  ratio: str
128
  scale: str
129
+ format: str
130
+ # ⚠️ custom_size 已移除
131
 
132
+ def process_image_in_memory(image_bytes: bytes, target_format: str, target_size: tuple = None) -> str:
133
  try:
134
  with Image.open(BytesIO(image_bytes)) as img:
135
  out = BytesIO()
136
+ final_img = img
137
+ status_msg = f"[原图直出] 尺寸: {img.size}"
138
+
139
+ if target_size:
140
+ w_diff = abs(img.size[0] - target_size[0])
141
+ h_diff = abs(img.size[1] - target_size[1])
142
+ if w_diff > 50 or h_diff > 50:
143
+ print(f"⚠️ [Resize] {img.size} -> {target_size}")
144
+ final_img = img.resize(target_size, Image.Resampling.LANCZOS)
145
+ status_msg = f"[触发放大] -> {target_size}"
146
+ else:
147
+ status_msg = f"[原图直出] 符合预期"
148
+ print(f"✅ {status_msg}")
149
+
150
  fmt = "JPEG" if target_format == "jpg" else "PNG"
151
+ final_img = final_img.convert("RGB") if target_format == "jpg" else final_img
152
+ final_img.save(out, format=fmt, quality=95)
153
  return base64.b64encode(out.getvalue()).decode('utf-8')
154
+ except Exception as e:
155
+ raise Exception(f"Image Error: {e}")
156
+ finally:
157
+ gc.collect()
158
 
159
  async def download_image_as_bytes(url):
160
  print(f"📥 下载: {url}")
161
+ async with httpx.AsyncClient(timeout=180) as client:
162
  r = await client.get(url)
163
  if r.status_code == 200: return r.content
164
  raise Exception(f"HTTP {r.status_code}")
165
 
166
  async def core_generate(req: GenerateRequest):
167
+ # 1. 纯查表逻辑 (不再处理 custom)
168
+ ratio_key = req.ratio.split(' ')[0] # 提取 "16:9"
169
+
170
+ # 默认兜底
171
+ target_w, target_h = 1024, 1024
 
172
 
173
+ if ratio_key in RESOLUTION_MAP:
174
+ if req.scale in RESOLUTION_MAP[ratio_key]:
175
+ target_w, target_h = RESOLUTION_MAP[ratio_key][req.scale]
176
+ else:
177
+ # 如果 scale 不在表里,默认 1k
178
+ target_w, target_h = RESOLUTION_MAP[ratio_key]["1k"]
179
+
180
+ api_size_str = f"{target_w}x{target_h}"
181
+
182
+ # 2. Prompt 处理
183
+ final_prompt = req.prompt
184
  if "gemini" in req.model.lower():
185
+ quality_prompt = "standard quality"
186
+ if req.scale == "4k": quality_prompt = "Extreme High Quality, 4K Resolution"
187
+ elif req.scale == "2k": quality_prompt = "High Quality, 2K Resolution"
188
+
189
+ suffix = f". (Settings: Aspect Ratio {ratio_key}, Quality {quality_prompt}, Target Size {api_size_str})"
190
+ final_prompt = f"{req.prompt} {suffix}"
191
+
192
+ # 3. API 请求
193
  client = AsyncOpenAI(api_key=req.api_key, base_url="https://api.cloudapp.ink/v1", max_retries=0, timeout=60.0)
 
194
  img_b = None
195
+
196
  if "gemini" in req.model.lower():
197
  print(f"🟣 Gemini: {req.model}")
198
+ res = await client.chat.completions.create(
199
+ model=req.model,
200
+ messages=[{"role":"user","content":final_prompt}],
201
+ extra_body={"modalities":["image","text"]}
202
+ )
203
  c = res.choices[0].message.content
204
  u = re.search(r"!\[.*?\]\((https?://[^\)]+)\)", c) or re.search(r"(https?://\S+\.(?:png|jpg|jpeg|webp))", c)
205
  if u: img_b = await download_image_as_bytes(u.group(1))
206
  elif "base64," in c: img_b = base64.b64decode(c.split("base64,")[1].split(")")[0].strip())
207
+ else: raise Exception("No Image Found")
208
  else:
209
+ print(f"🔵 Standard: {req.model} | 请求 {api_size_str}")
210
+ res = await client.images.generate(
211
+ model=req.model, prompt=final_prompt, n=1, size=api_size_str, response_format="b64_json"
212
+ )
213
  d = res.data[0]
214
  if getattr(d,'b64_json',None): img_b = base64.b64decode(d.b64_json)
215
  elif hasattr(d,'url'): img_b = await download_image_as_bytes(d.url)
216
  else: raise Exception("No Data")
217
 
218
+ return process_image_in_memory(img_b, req.format, target_size=(target_w, target_h))
219
 
220
  # ==========================================
221
  # 4. FastAPI App
 
232
  @app.post("/api/generate")
233
  async def generate_api(req: GenerateRequest, request: Request):
234
  ip = get_real_ip(request)
235
+ print(f"\n🚀 [{datetime.now().strftime('%H:%M:%S')}] IP: {ip} | Target: {req.scale} ({req.ratio})")
236
 
237
+ if check_ip_rate_limit(ip, 3, 60):
238
  return JSONResponse({"status": "error", "message": "请求过快"}, 429)
239
 
240
  is_loc, word = contains_local_sensitive_words(req.prompt)
241
  if is_loc:
 
242
  return JSONResponse({"status": "error", "message": f"违规内容: {word}"}, 200)
243
 
244
  is_bd, reason = await check_baidu_text_censor(req.prompt)
245
  if is_bd:
 
246
  return JSONResponse({"status": "error", "message": f"审核未通过: {reason}"}, 200)
247
 
248
  async with CONCURRENCY_LIMITER: