Update app.py
#2
by
shiertier
- opened
app.py
CHANGED
@@ -9,9 +9,9 @@ from typing import Any
|
|
9 |
# 常量定义
|
10 |
HTTP_STATUS_CENSORED = 451
|
11 |
HTTP_STATUS_OK = 200
|
12 |
-
MAX_SEED = 2147483647
|
13 |
MAX_IMAGE_SIZE = 2048
|
14 |
-
MIN_IMAGE_SIZE = 256
|
15 |
|
16 |
# 调试模式
|
17 |
DEBUG_MODE = os.environ.get("DEBUG_MODE", "false").lower() == "true"
|
@@ -24,9 +24,8 @@ MODEL_CONFIGS = {
|
|
24 |
|
25 |
def validate_dimensions(width: int, height: int) -> tuple[int, int]:
|
26 |
"""验证并调整图片尺寸"""
|
27 |
-
|
28 |
-
|
29 |
-
height = max(MIN_IMAGE_SIZE, min(height, MAX_IMAGE_SIZE))
|
30 |
width = (width // 32) * 32
|
31 |
height = (height // 32) * 32
|
32 |
return width, height
|
@@ -45,108 +44,61 @@ class ImageGenerationConfig:
|
|
45 |
width: int = 1024
|
46 |
height: int = 1024
|
47 |
seed: int | None = None
|
48 |
-
use_polish: bool = False
|
49 |
is_lumina: bool = True
|
50 |
lumina_config: LuminaConfig = field(default_factory=LuminaConfig)
|
51 |
|
52 |
class ImageClient:
|
53 |
"""图像生成客户端"""
|
54 |
def __init__(self) -> None:
|
55 |
-
# 使用环境变量中的API_TOKEN
|
56 |
self.x_token = os.environ.get("API_TOKEN", "")
|
57 |
if not self.x_token:
|
58 |
raise ValueError("环境变量中未设置API_TOKEN")
|
59 |
|
60 |
-
# API端点
|
61 |
self.lumina_api_url = "https://ops.api.talesofai.cn/v3/make_image"
|
62 |
self.lumina_task_status_url = "https://ops.api.talesofai.cn/v1/artifact/task/{task_uuid}"
|
63 |
-
|
64 |
-
|
65 |
-
self.max_polling_attempts = 100 # 增加到100次
|
66 |
-
self.polling_interval = 3.0 # 保持3秒间隔
|
67 |
-
# 总超时时间:100 × 3.0 = 300秒 = 5分钟
|
68 |
-
|
69 |
-
# 默认请求头
|
70 |
self.default_headers = {
|
71 |
"Content-Type": "application/json",
|
72 |
-
"x-platform": "nieta-app/web",
|
73 |
"X-Token": self.x_token,
|
74 |
}
|
75 |
|
76 |
def _prepare_prompt_data(self, prompt: str, negative_prompt: str = "") -> list[dict[str, Any]]:
|
77 |
-
"""
|
78 |
-
prompts = [
|
79 |
-
{
|
80 |
-
"type": "freetext",
|
81 |
-
"value": prompt,
|
82 |
-
"weight": 1.0
|
83 |
-
}
|
84 |
-
]
|
85 |
-
|
86 |
if negative_prompt:
|
87 |
-
|
88 |
-
|
89 |
-
|
90 |
-
|
91 |
-
})
|
92 |
-
|
93 |
-
# 添加Lumina元素
|
94 |
-
prompts.append({
|
95 |
-
"type": "elementum",
|
96 |
-
"value": "b5edccfe-46a2-4a14-a8ff-f4d430343805",
|
97 |
-
"uuid": "b5edccfe-46a2-4a14-a8ff-f4d430343805",
|
98 |
-
"weight": 1.0,
|
99 |
-
"name": "lumina1",
|
100 |
"img_url": "https://oss.talesofai.cn/picture_s/1y7f53e6itfn_0.jpeg",
|
101 |
-
"domain": "",
|
102 |
-
"
|
103 |
-
"label": None,
|
104 |
-
"sort_index": 0,
|
105 |
-
"status": "IN_USE",
|
106 |
-
"polymorphi_values": {},
|
107 |
-
"sub_type": None,
|
108 |
})
|
109 |
-
|
110 |
-
return prompts
|
111 |
|
112 |
def _build_payload(self, config: ImageGenerationConfig) -> dict[str, Any]:
|
113 |
-
"""构建API请求载荷"""
|
114 |
payload = {
|
115 |
-
"storyId": "",
|
116 |
-
"
|
117 |
-
"
|
118 |
-
"height": config.height,
|
119 |
-
"rawPrompt": config.prompts,
|
120 |
-
"seed": config.seed,
|
121 |
-
"meta": {"entrance": "PICTURE,PURE"},
|
122 |
-
"context_model_series": None,
|
123 |
-
"negative_freetext": "",
|
124 |
"advanced_translator": config.use_polish,
|
125 |
}
|
126 |
-
|
127 |
if config.is_lumina:
|
128 |
client_args = {}
|
129 |
-
if config.lumina_config.model_name:
|
130 |
-
|
131 |
-
if config.lumina_config.
|
132 |
-
|
133 |
-
if config.lumina_config.step is not None:
|
134 |
-
client_args["steps"] = str(config.lumina_config.step)
|
135 |
-
|
136 |
-
if client_args:
|
137 |
-
payload["client_args"] = client_args
|
138 |
-
|
139 |
return payload
|
140 |
|
141 |
async def _poll_task_status(self, task_uuid: str) -> dict[str, Any]:
|
142 |
-
|
143 |
-
async with httpx.AsyncClient(timeout=30.0) as client:
|
144 |
-
for
|
145 |
-
response = await client.get(
|
146 |
-
|
147 |
-
headers=self.default_headers
|
148 |
-
)
|
149 |
-
|
150 |
if response.status_code != HTTP_STATUS_OK:
|
151 |
return {
|
152 |
"success": False,
|
@@ -161,12 +113,10 @@ class ImageClient:
|
|
161 |
"success": False,
|
162 |
"error": f"任务状态响应解析失败: {response.text[:500]}"
|
163 |
}
|
164 |
-
|
165 |
-
# 使用正确的字段名(根据model_studio的实现)
|
166 |
task_status = result.get("task_status")
|
167 |
-
|
168 |
if task_status == "SUCCESS":
|
169 |
-
# 从artifacts数组中获取图片URL
|
170 |
artifacts = result.get("artifacts", [])
|
171 |
if artifacts and len(artifacts) > 0:
|
172 |
image_url = artifacts[0].get("url")
|
@@ -196,7 +146,6 @@ class ImageClient:
|
|
196 |
}
|
197 |
|
198 |
await asyncio.sleep(self.polling_interval)
|
199 |
-
|
200 |
return {
|
201 |
"success": False,
|
202 |
"error": "⏳ 生图任务超时(5分钟),服务器可能正在处理大量请求,请稍后重试"
|
@@ -207,7 +156,7 @@ class ImageClient:
|
|
207 |
try:
|
208 |
# 获取模型路径
|
209 |
model_path = MODEL_CONFIGS.get(model_name, MODEL_CONFIGS["ep3"])
|
210 |
-
|
211 |
# 准备配置
|
212 |
config = ImageGenerationConfig(
|
213 |
prompts=self._prepare_prompt_data(prompt, negative_prompt),
|
@@ -221,41 +170,40 @@ class ImageClient:
|
|
221 |
step=steps
|
222 |
)
|
223 |
)
|
224 |
-
|
225 |
# 发送生成请求
|
226 |
async with httpx.AsyncClient(timeout=300.0) as client:
|
227 |
payload = self._build_payload(config)
|
228 |
if DEBUG_MODE:
|
229 |
print(f"DEBUG: 发送API请求到 {self.lumina_api_url}")
|
230 |
print(f"DEBUG: 请求载荷: {payload}")
|
231 |
-
|
232 |
response = await client.post(
|
233 |
self.lumina_api_url,
|
234 |
json=payload,
|
235 |
headers=self.default_headers
|
236 |
)
|
237 |
-
|
238 |
if DEBUG_MODE:
|
239 |
print(f"DEBUG: API响应状态码: {response.status_code}")
|
240 |
print(f"DEBUG: API响应内容: {response.text[:1000]}")
|
241 |
|
242 |
if response.status_code == HTTP_STATUS_CENSORED:
|
243 |
return None, "内容不合规"
|
244 |
-
|
245 |
# 处理并发限制错误
|
246 |
if response.status_code == 433:
|
247 |
return None, "⏳ 服务器正忙,同时生成的图片数量已达上限,请稍后重试"
|
248 |
-
|
249 |
if response.status_code != HTTP_STATUS_OK:
|
250 |
return None, f"API请求失败: {response.status_code} - {response.text}"
|
251 |
|
252 |
# API直接返回UUID字符串(根据model_studio的实现)
|
253 |
content = response.text.strip()
|
254 |
task_uuid = content.replace('"', "")
|
255 |
-
|
256 |
if DEBUG_MODE:
|
257 |
print(f"DEBUG: API返回UUID: {task_uuid}")
|
258 |
-
|
259 |
if not task_uuid:
|
260 |
return None, f"未获取到任务ID,API响应: {response.text}"
|
261 |
|
@@ -265,388 +213,152 @@ class ImageClient:
|
|
265 |
return result["image_url"], None
|
266 |
else:
|
267 |
return None, result["error"]
|
268 |
-
|
269 |
except Exception as e:
|
270 |
return None, f"生成图片时发生错误: {str(e)}"
|
271 |
|
272 |
# 创建图片生成客户端实例
|
273 |
-
|
274 |
-
|
|
|
|
|
|
|
|
|
|
|
275 |
example_titles = [
|
276 |
"A stylized female demon with red hair and glitch effects",
|
277 |
-
"A young man relaxes on a hazy urban rooftop",
|
278 |
"A gentle, freckled girl embraces a goat in a meadow"
|
279 |
]
|
280 |
-
|
281 |
-
# 完整提示词映射
|
282 |
full_prompts = {
|
283 |
-
|
284 |
-
|
285 |
-
|
286 |
-
}
|
287 |
-
|
288 |
-
css = """
|
289 |
-
.main-container {
|
290 |
-
max-width: 1400px !important;
|
291 |
-
margin: 0 auto !important;
|
292 |
-
padding: 20px !important;
|
293 |
-
}
|
294 |
-
|
295 |
-
.left-panel {
|
296 |
-
background: linear-gradient(145deg, #f8f9fa, #e9ecef) !important;
|
297 |
-
border-radius: 16px !important;
|
298 |
-
padding: 24px !important;
|
299 |
-
box-shadow: 0 8px 32px rgba(0,0,0,0.1) !important;
|
300 |
-
border: 1px solid rgba(255,255,255,0.2) !important;
|
301 |
}
|
302 |
|
303 |
-
.right-panel {
|
304 |
-
background: linear-gradient(145deg, #ffffff, #f8f9fa) !important;
|
305 |
-
border-radius: 16px !important;
|
306 |
-
padding: 24px !important;
|
307 |
-
box-shadow: 0 8px 32px rgba(0,0,0,0.1) !important;
|
308 |
-
border: 1px solid rgba(255,255,255,0.2) !important;
|
309 |
-
display: flex !important;
|
310 |
-
flex-direction: column !important;
|
311 |
-
align-items: center !important;
|
312 |
-
justify-content: flex-start !important;
|
313 |
-
min-height: 600px !important;
|
314 |
-
}
|
315 |
-
|
316 |
-
#main-prompt textarea {
|
317 |
-
min-height: 180px !important;
|
318 |
-
font-size: 15px !important;
|
319 |
-
line-height: 1.6 !important;
|
320 |
-
padding: 16px !important;
|
321 |
-
border-radius: 12px !important;
|
322 |
-
border: 2px solid #e9ecef !important;
|
323 |
-
transition: all 0.3s ease !important;
|
324 |
-
background: white !important;
|
325 |
-
}
|
326 |
-
|
327 |
-
#main-prompt textarea:focus {
|
328 |
-
border-color: #4f46e5 !important;
|
329 |
-
box-shadow: 0 0 0 3px rgba(79, 70, 229, 0.1) !important;
|
330 |
-
}
|
331 |
-
|
332 |
-
.run-button {
|
333 |
-
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%) !important;
|
334 |
-
border: none !important;
|
335 |
-
border-radius: 12px !important;
|
336 |
-
padding: 12px 32px !important;
|
337 |
-
font-weight: 600 !important;
|
338 |
-
font-size: 16px !important;
|
339 |
-
color: white !important;
|
340 |
-
box-shadow: 0 4px 15px rgba(102, 126, 234, 0.4) !important;
|
341 |
-
transition: all 0.3s ease !important;
|
342 |
-
}
|
343 |
-
|
344 |
-
.run-button:hover {
|
345 |
-
transform: translateY(-2px) !important;
|
346 |
-
box-shadow: 0 6px 20px rgba(102, 126, 234, 0.6) !important;
|
347 |
-
}
|
348 |
-
|
349 |
-
.settings-section {
|
350 |
-
background: white !important;
|
351 |
-
border-radius: 12px !important;
|
352 |
-
padding: 16px !important;
|
353 |
-
margin-top: 12px !important;
|
354 |
-
box-shadow: 0 2px 8px rgba(0,0,0,0.05) !important;
|
355 |
-
border: 1px solid #e9ecef !important;
|
356 |
-
}
|
357 |
-
|
358 |
-
.settings-title {
|
359 |
-
font-size: 18px !important;
|
360 |
-
font-weight: 600 !important;
|
361 |
-
color: #374151 !important;
|
362 |
-
margin-bottom: 12px !important;
|
363 |
-
padding-bottom: 6px !important;
|
364 |
-
border-bottom: 2px solid #e9ecef !important;
|
365 |
-
}
|
366 |
-
|
367 |
-
.right-panel .settings-title {
|
368 |
-
margin-bottom: 8px !important;
|
369 |
-
padding-bottom: 4px !important;
|
370 |
-
font-size: 16px !important;
|
371 |
-
}
|
372 |
-
|
373 |
-
.right-panel .block {
|
374 |
-
min-height: unset !important;
|
375 |
-
height: auto !important;
|
376 |
-
flex: none !important;
|
377 |
-
}
|
378 |
-
|
379 |
-
.right-panel .html-container {
|
380 |
-
padding: 0 !important;
|
381 |
-
margin: 0 !important;
|
382 |
-
}
|
383 |
-
|
384 |
-
.slider-container .wrap {
|
385 |
-
background: #f8f9fa !important;
|
386 |
-
border-radius: 8px !important;
|
387 |
-
padding: 6px !important;
|
388 |
-
margin: 2px 0 !important;
|
389 |
-
}
|
390 |
-
|
391 |
-
.settings-section .block {
|
392 |
-
margin: 4px 0 !important;
|
393 |
-
}
|
394 |
-
|
395 |
-
.settings-section .row {
|
396 |
-
margin: 6px 0 !important;
|
397 |
-
}
|
398 |
-
|
399 |
-
.settings-section .form {
|
400 |
-
gap: 4px !important;
|
401 |
-
}
|
402 |
-
|
403 |
-
.settings-section .html-container {
|
404 |
-
padding: 0 !important;
|
405 |
-
margin: 8px 0 4px 0 !important;
|
406 |
-
}
|
407 |
-
|
408 |
-
.result-image {
|
409 |
-
border-radius: 16px !important;
|
410 |
-
box-shadow: 0 8px 32px rgba(0,0,0,0.15) !important;
|
411 |
-
max-width: 100% !important;
|
412 |
-
height: auto !important;
|
413 |
-
min-height: 400px !important;
|
414 |
-
width: 100% !important;
|
415 |
-
}
|
416 |
-
|
417 |
-
.result-image img {
|
418 |
-
border-radius: 16px !important;
|
419 |
-
object-fit: contain !important;
|
420 |
-
max-width: 100% !important;
|
421 |
-
max-height: 600px !important;
|
422 |
-
width: auto !important;
|
423 |
-
height: auto !important;
|
424 |
-
}
|
425 |
-
|
426 |
-
.examples-container {
|
427 |
-
margin-top: 20px !important;
|
428 |
-
}
|
429 |
-
|
430 |
-
/* 示例按钮样式 - 每个按钮单独一行 */
|
431 |
-
.examples-container button {
|
432 |
-
font-size: 13px !important;
|
433 |
-
padding: 10px 16px !important;
|
434 |
-
border-radius: 10px !important;
|
435 |
-
background: #f8f9fa !important;
|
436 |
-
border: 1px solid #e9ecef !important;
|
437 |
-
color: #495057 !important;
|
438 |
-
transition: all 0.2s ease !important;
|
439 |
-
width: 100% !important;
|
440 |
-
margin-bottom: 8px !important;
|
441 |
-
text-align: left !important;
|
442 |
-
font-weight: 500 !important;
|
443 |
-
}
|
444 |
-
|
445 |
-
.examples-container button:hover {
|
446 |
-
background: #e9ecef !important;
|
447 |
-
border-color: #6c757d !important;
|
448 |
-
transform: translateY(-1px) !important;
|
449 |
-
box-shadow: 0 2px 8px rgba(0,0,0,0.1) !important;
|
450 |
-
}
|
451 |
-
|
452 |
-
.examples-container button:active {
|
453 |
-
transform: translateY(0px) !important;
|
454 |
-
background: #dee2e6 !important;
|
455 |
-
}
|
456 |
-
|
457 |
-
|
458 |
-
|
459 |
-
.title-header {
|
460 |
-
text-align: center !important;
|
461 |
-
margin-bottom: 30px !important;
|
462 |
-
padding: 20px !important;
|
463 |
-
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%) !important;
|
464 |
-
color: white !important;
|
465 |
-
border-radius: 16px !important;
|
466 |
-
box-shadow: 0 4px 15px rgba(102, 126, 234, 0.3) !important;
|
467 |
-
}
|
468 |
-
|
469 |
-
.title-header h1 {
|
470 |
-
font-size: 28px !important;
|
471 |
-
font-weight: 700 !important;
|
472 |
-
margin: 0 !important;
|
473 |
-
text-shadow: 0 2px 4px rgba(0,0,0,0.1) !important;
|
474 |
-
}
|
475 |
-
"""
|
476 |
-
|
477 |
-
|
478 |
-
|
479 |
async def infer(
|
480 |
-
|
481 |
-
|
482 |
-
randomize_seed,
|
483 |
-
width,
|
484 |
-
height,
|
485 |
-
cfg,
|
486 |
-
steps,
|
487 |
-
model_name,
|
488 |
-
progress=gr.Progress(track_tqdm=True),
|
489 |
):
|
490 |
-
if
|
|
|
|
|
491 |
raise gr.Error("提示词不能为空")
|
492 |
|
493 |
-
|
494 |
-
|
|
|
495 |
|
496 |
# 验证并调整尺寸
|
497 |
-
|
498 |
|
499 |
# 验证其他参数
|
500 |
-
if not 1.0 <=
|
501 |
raise gr.Error("CFG Scale 必须在 1.0 到 20.0 之间")
|
502 |
-
if not 1 <=
|
503 |
raise gr.Error("Steps 必须在 1 到 50 之间")
|
504 |
|
505 |
image_url, error = await image_client.generate_image(
|
506 |
-
prompt=
|
507 |
negative_prompt="",
|
508 |
-
seed=
|
509 |
-
width=
|
510 |
-
height=
|
511 |
-
cfg=
|
512 |
-
steps=
|
513 |
-
model_name=
|
514 |
)
|
515 |
|
516 |
if error:
|
517 |
raise gr.Error(error)
|
518 |
|
519 |
-
return image_url,
|
520 |
-
|
521 |
-
|
522 |
-
|
523 |
-
|
524 |
-
|
525 |
-
|
526 |
-
|
527 |
-
|
528 |
-
|
529 |
-
|
530 |
-
|
531 |
-
|
532 |
-
|
533 |
-
|
534 |
-
|
535 |
-
|
536 |
-
|
537 |
-
|
538 |
-
|
539 |
-
|
540 |
-
|
541 |
-
|
542 |
-
|
543 |
-
|
544 |
-
|
545 |
-
|
546 |
-
|
547 |
-
|
548 |
-
|
549 |
-
|
550 |
-
|
551 |
-
|
552 |
-
|
553 |
-
|
554 |
-
|
555 |
-
|
556 |
-
|
557 |
-
|
558 |
-
)
|
559 |
-
randomize_seed = gr.Checkbox(label="Random Seed", value=True)
|
560 |
-
|
561 |
-
# 尺寸设置
|
562 |
-
gr.HTML("<div style='margin: 16px 0 8px 0; font-weight: 600; color: #6B7280;'>📐 Image Dimensions</div>")
|
563 |
-
with gr.Row():
|
564 |
-
width = gr.Slider(
|
565 |
-
label="Width",
|
566 |
-
minimum=256,
|
567 |
-
maximum=MAX_IMAGE_SIZE,
|
568 |
-
step=32,
|
569 |
-
value=1024,
|
570 |
-
elem_classes=["slider-container"]
|
571 |
-
)
|
572 |
-
height = gr.Slider(
|
573 |
-
label="Height",
|
574 |
-
minimum=256,
|
575 |
-
maximum=MAX_IMAGE_SIZE,
|
576 |
-
step=32,
|
577 |
-
value=1024,
|
578 |
-
elem_classes=["slider-container"]
|
579 |
-
)
|
580 |
-
|
581 |
-
# 生成参数
|
582 |
-
gr.HTML("<div style='margin: 16px 0 8px 0; font-weight: 600; color: #6B7280;'>⚙️ Generation Parameters</div>")
|
583 |
-
with gr.Row():
|
584 |
-
cfg = gr.Slider(
|
585 |
-
label="CFG Scale",
|
586 |
-
minimum=1.0,
|
587 |
-
maximum=20.0,
|
588 |
-
step=0.1,
|
589 |
-
value=5.5,
|
590 |
-
elem_classes=["slider-container"]
|
591 |
-
)
|
592 |
-
steps = gr.Slider(
|
593 |
-
label="Steps",
|
594 |
-
minimum=1,
|
595 |
-
maximum=50,
|
596 |
-
step=1,
|
597 |
-
value=30,
|
598 |
-
elem_classes=["slider-container"]
|
599 |
-
)
|
600 |
-
|
601 |
-
# 模型选择
|
602 |
-
model_name = gr.Dropdown(
|
603 |
-
label="🤖 Model Selection",
|
604 |
-
choices=list(MODEL_CONFIGS.keys()),
|
605 |
-
value="ep3"
|
606 |
-
)
|
607 |
-
|
608 |
-
# 示例提示词
|
609 |
-
with gr.Column(elem_classes=["examples-container"]):
|
610 |
-
gr.HTML("<div class='settings-title'>💡 Example Prompts</div>")
|
611 |
-
example_btn1 = gr.Button("A stylized female demon with red hair and glitch effects", size="sm")
|
612 |
-
example_btn2 = gr.Button("A young man relaxes on a hazy urban rooftop", size="sm")
|
613 |
-
example_btn3 = gr.Button("A gentle, freckled girl embraces a goat in a meadow", size="sm")
|
614 |
-
|
615 |
-
# 右侧图像显示面板
|
616 |
-
with gr.Column(scale=1, elem_classes=["right-panel"]):
|
617 |
-
gr.HTML("<div class='settings-title'>🖼️ Generated Image</div>")
|
618 |
-
result = gr.Image(
|
619 |
-
label="Result",
|
620 |
-
show_label=False,
|
621 |
-
elem_classes=["result-image"],
|
622 |
-
height=600,
|
623 |
-
container=True,
|
624 |
-
show_download_button=True,
|
625 |
-
show_share_button=False
|
626 |
)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
627 |
|
628 |
-
|
629 |
-
|
630 |
-
|
631 |
-
|
632 |
-
|
633 |
-
|
634 |
-
randomize_seed,
|
635 |
-
width,
|
636 |
-
height,
|
637 |
-
cfg,
|
638 |
-
steps,
|
639 |
-
model_name,
|
640 |
-
],
|
641 |
-
outputs=[result, seed],
|
642 |
-
)
|
643 |
-
|
644 |
-
# 示例按钮点击事件
|
645 |
-
example_btn1.click(lambda: full_prompts["A stylized female demon with red hair and glitch effects"], outputs=[prompt])
|
646 |
-
example_btn2.click(lambda: full_prompts["A young man relaxes on a hazy urban rooftop"], outputs=[prompt])
|
647 |
-
example_btn3.click(lambda: full_prompts["A gentle, freckled girl embraces a goat in a meadow"], outputs=[prompt])
|
648 |
-
|
649 |
|
650 |
|
651 |
if __name__ == "__main__":
|
652 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
9 |
# 常量定义
|
10 |
HTTP_STATUS_CENSORED = 451
|
11 |
HTTP_STATUS_OK = 200
|
12 |
+
MAX_SEED = 2147483647 # (2**31 - 1)
|
13 |
MAX_IMAGE_SIZE = 2048
|
14 |
+
MIN_IMAGE_SIZE = 256 # Smallest dimension for SDXL like models often 512, but API might support smaller. Adjusted to API's limits.
|
15 |
|
16 |
# 调试模式
|
17 |
DEBUG_MODE = os.environ.get("DEBUG_MODE", "false").lower() == "true"
|
|
|
24 |
|
25 |
def validate_dimensions(width: int, height: int) -> tuple[int, int]:
|
26 |
"""验证并调整图片尺寸"""
|
27 |
+
width = max(MIN_IMAGE_SIZE, min(int(width), MAX_IMAGE_SIZE))
|
28 |
+
height = max(MIN_IMAGE_SIZE, min(int(height), MAX_IMAGE_SIZE))
|
|
|
29 |
width = (width // 32) * 32
|
30 |
height = (height // 32) * 32
|
31 |
return width, height
|
|
|
44 |
width: int = 1024
|
45 |
height: int = 1024
|
46 |
seed: int | None = None
|
47 |
+
use_polish: bool = False # This wasn't exposed in UI, assuming false
|
48 |
is_lumina: bool = True
|
49 |
lumina_config: LuminaConfig = field(default_factory=LuminaConfig)
|
50 |
|
51 |
class ImageClient:
|
52 |
"""图像生成客户端"""
|
53 |
def __init__(self) -> None:
|
|
|
54 |
self.x_token = os.environ.get("API_TOKEN", "")
|
55 |
if not self.x_token:
|
56 |
raise ValueError("环境变量中未设置API_TOKEN")
|
57 |
|
|
|
58 |
self.lumina_api_url = "https://ops.api.talesofai.cn/v3/make_image"
|
59 |
self.lumina_task_status_url = "https://ops.api.talesofai.cn/v1/artifact/task/{task_uuid}"
|
60 |
+
self.max_polling_attempts = 100
|
61 |
+
self.polling_interval = 3.0
|
|
|
|
|
|
|
|
|
|
|
62 |
self.default_headers = {
|
63 |
"Content-Type": "application/json",
|
64 |
+
"x-platform": "nieta-app/web", # Or a generic identifier if preferred
|
65 |
"X-Token": self.x_token,
|
66 |
}
|
67 |
|
68 |
def _prepare_prompt_data(self, prompt: str, negative_prompt: str = "") -> list[dict[str, Any]]:
|
69 |
+
prompts_data = [{"type": "freetext", "value": prompt, "weight": 1.0}]
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
70 |
if negative_prompt:
|
71 |
+
prompts_data.append({"type": "freetext", "value": negative_prompt, "weight": -1.0})
|
72 |
+
prompts_data.append({
|
73 |
+
"type": "elementum", "value": "b5edccfe-46a2-4a14-a8ff-f4d430343805",
|
74 |
+
"uuid": "b5edccfe-46a2-4a14-a8ff-f4d430343805", "weight": 1.0, "name": "lumina1",
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
75 |
"img_url": "https://oss.talesofai.cn/picture_s/1y7f53e6itfn_0.jpeg",
|
76 |
+
"domain": "", "parent": "", "label": None, "sort_index": 0, "status": "IN_USE",
|
77 |
+
"polymorphi_values": {}, "sub_type": None,
|
|
|
|
|
|
|
|
|
|
|
78 |
})
|
79 |
+
return prompts_data
|
|
|
80 |
|
81 |
def _build_payload(self, config: ImageGenerationConfig) -> dict[str, Any]:
|
|
|
82 |
payload = {
|
83 |
+
"storyId": "", "jobType": "universal", "width": config.width, "height": config.height,
|
84 |
+
"rawPrompt": config.prompts, "seed": config.seed, "meta": {"entrance": "PICTURE,PURE"},
|
85 |
+
"context_model_series": None, "negative_freetext": "", # Negative handled in rawPrompt
|
|
|
|
|
|
|
|
|
|
|
|
|
86 |
"advanced_translator": config.use_polish,
|
87 |
}
|
|
|
88 |
if config.is_lumina:
|
89 |
client_args = {}
|
90 |
+
if config.lumina_config.model_name: client_args["ckpt_name"] = config.lumina_config.model_name
|
91 |
+
if config.lumina_config.cfg is not None: client_args["cfg"] = str(config.lumina_config.cfg)
|
92 |
+
if config.lumina_config.step is not None: client_args["steps"] = str(config.lumina_config.step)
|
93 |
+
if client_args: payload["client_args"] = client_args
|
|
|
|
|
|
|
|
|
|
|
|
|
94 |
return payload
|
95 |
|
96 |
async def _poll_task_status(self, task_uuid: str) -> dict[str, Any]:
|
97 |
+
status_url = self.lumina_task_status_url.format(task_uuid=task_uuid)
|
98 |
+
async with httpx.AsyncClient(timeout=30.0) as client: # Timeout for individual poll request
|
99 |
+
for attempt in range(self.max_polling_attempts):
|
100 |
+
response = await client.get(status_url, headers=self.default_headers)
|
101 |
+
|
|
|
|
|
|
|
102 |
if response.status_code != HTTP_STATUS_OK:
|
103 |
return {
|
104 |
"success": False,
|
|
|
113 |
"success": False,
|
114 |
"error": f"任务状态响应解析失败: {response.text[:500]}"
|
115 |
}
|
116 |
+
|
|
|
117 |
task_status = result.get("task_status")
|
118 |
+
|
119 |
if task_status == "SUCCESS":
|
|
|
120 |
artifacts = result.get("artifacts", [])
|
121 |
if artifacts and len(artifacts) > 0:
|
122 |
image_url = artifacts[0].get("url")
|
|
|
146 |
}
|
147 |
|
148 |
await asyncio.sleep(self.polling_interval)
|
|
|
149 |
return {
|
150 |
"success": False,
|
151 |
"error": "⏳ 生图任务超时(5分钟),服务器可能正在处理大量请求,请稍后重试"
|
|
|
156 |
try:
|
157 |
# 获取模型路径
|
158 |
model_path = MODEL_CONFIGS.get(model_name, MODEL_CONFIGS["ep3"])
|
159 |
+
|
160 |
# 准备配置
|
161 |
config = ImageGenerationConfig(
|
162 |
prompts=self._prepare_prompt_data(prompt, negative_prompt),
|
|
|
170 |
step=steps
|
171 |
)
|
172 |
)
|
|
|
173 |
# 发送生成请求
|
174 |
async with httpx.AsyncClient(timeout=300.0) as client:
|
175 |
payload = self._build_payload(config)
|
176 |
if DEBUG_MODE:
|
177 |
print(f"DEBUG: 发送API请求到 {self.lumina_api_url}")
|
178 |
print(f"DEBUG: 请求载荷: {payload}")
|
179 |
+
|
180 |
response = await client.post(
|
181 |
self.lumina_api_url,
|
182 |
json=payload,
|
183 |
headers=self.default_headers
|
184 |
)
|
185 |
+
|
186 |
if DEBUG_MODE:
|
187 |
print(f"DEBUG: API响应状态码: {response.status_code}")
|
188 |
print(f"DEBUG: API响应内容: {response.text[:1000]}")
|
189 |
|
190 |
if response.status_code == HTTP_STATUS_CENSORED:
|
191 |
return None, "内容不合规"
|
192 |
+
|
193 |
# 处理并发限制错误
|
194 |
if response.status_code == 433:
|
195 |
return None, "⏳ 服务器正忙,同时生成的图片数量已达上限,请稍后重试"
|
196 |
+
|
197 |
if response.status_code != HTTP_STATUS_OK:
|
198 |
return None, f"API请求失败: {response.status_code} - {response.text}"
|
199 |
|
200 |
# API直接返回UUID字符串(根据model_studio的实现)
|
201 |
content = response.text.strip()
|
202 |
task_uuid = content.replace('"', "")
|
203 |
+
|
204 |
if DEBUG_MODE:
|
205 |
print(f"DEBUG: API返回UUID: {task_uuid}")
|
206 |
+
|
207 |
if not task_uuid:
|
208 |
return None, f"未获取到任务ID,API响应: {response.text}"
|
209 |
|
|
|
213 |
return result["image_url"], None
|
214 |
else:
|
215 |
return None, result["error"]
|
|
|
216 |
except Exception as e:
|
217 |
return None, f"生成图片时发生错误: {str(e)}"
|
218 |
|
219 |
# 创建图片生成客户端实例
|
220 |
+
try:
|
221 |
+
image_client = ImageClient()
|
222 |
+
except Exception as e:
|
223 |
+
print(f"Failed to initialize ImageClient: {e}")
|
224 |
+
image_client = None
|
225 |
+
|
226 |
+
# Example prompts
|
227 |
example_titles = [
|
228 |
"A stylized female demon with red hair and glitch effects",
|
229 |
+
"A young man relaxes on a hazy urban rooftop",
|
230 |
"A gentle, freckled girl embraces a goat in a meadow"
|
231 |
]
|
|
|
|
|
232 |
full_prompts = {
|
233 |
+
example_titles[0]: "Stylized anime illustration of a female demon or supernatural character with vibrant red hair in twintails/pigtails and glowing purple eyes. Character has black horns and features bandage-like cross markings on face. Subject wears a black sleeveless top and holds a pink bubblegum or candy sphere near mouth. Digital glitch effects create pixelated elements in her hair and around background. Dramatic lighting with stark white/black contrasting background featuring cracks or lightning patterns. Character has gold/yellow accessories including bracelets and hair decorations. Modern anime art style with sharp contrast and vivid colors. Portrait composition showing three-quarter view of character with confident or playful expression. Color palette dominated by reds, blacks, whites, purple and pink accents. Surreal or otherworldly atmosphere enhanced by particle effects and lighting. Professional digital illustration combining traditional anime aesthetics with contemporary glitch art elements. Character design suggests edgy or alternative styling with possible cyberpunk or modern demon girl influences.",
|
234 |
+
example_titles[1]: "Atmospheric anime illustration of young man with messy brown hair on urban rooftop during overcast day. Character wears white dress shirt and dark trousers, leaning back against railing while holding canned drink. Scene set on building rooftop with industrial elements like water tower, power lines, and metal structures visible. Cityscape background shows apartment buildings and urban architecture through soft hazy lighting. Subject has relaxed pose suggesting brief break or moment of contemplation. Color palette uses muted whites, grays, and industrial tones creating realistic urban atmosphere. Art style combines detailed architectural elements with soft, painterly technique. Composition emphasizes vertical lines of city buildings and metal structures. Professional digital artwork capturing slice-of-life moment in urban setting. Scene suggests peaceful solitude amid busy city environment. Lighting creates gentle, overcast mood with subtle shadows and highlights. Character design and setting reflect contemporary Japanese salary-man or office worker aesthetic.",
|
235 |
+
example_titles[2]: "Enchanting anime illustration of a gentle, freckled girl with long, wavy orange hair and elegant ram horns, tenderly embracing a white baby goat in a sunlit meadow. The composition is a close-up, focusing on the upper body and faces of both the girl and the goat, capturing an intimate and heartwarming moment. She wears a vintage-inspired dress with a high collar, puffed sleeves, and a delicate white headband, adorned with golden ribbons and lace details. The sunlight bathes the scene in warm, golden tones, casting soft shadows and creating a dreamy, pastoral atmosphere. The background is filled with lush green grass and scattered white flowers, enhancing the idyllic countryside setting. The art style is painterly and vibrant, with expressive brushwork and a focus on light and texture, evoking a sense of peace, innocence, and connection with nature."
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
236 |
}
|
237 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
238 |
async def infer(
|
239 |
+
prompt_text, seed_val, randomize_seed_val, width_val, height_val,
|
240 |
+
cfg_val, steps_val, model_name_val, progress=gr.Progress(track_tqdm=True)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
241 |
):
|
242 |
+
if image_client is None:
|
243 |
+
raise gr.Error("ImageClient 未正确初始化。请检查应用日志和API_TOKEN配置。")
|
244 |
+
if not prompt_text.strip():
|
245 |
raise gr.Error("提示词不能为空")
|
246 |
|
247 |
+
current_seed = int(seed_val)
|
248 |
+
if randomize_seed_val:
|
249 |
+
current_seed = random.randint(0, MAX_SEED)
|
250 |
|
251 |
# 验证并调整尺寸
|
252 |
+
width_val, height_val = validate_dimensions(width_val, height_val)
|
253 |
|
254 |
# 验证其他参数
|
255 |
+
if not (1.0 <= float(cfg_val) <= 20.0):
|
256 |
raise gr.Error("CFG Scale 必须在 1.0 到 20.0 之间")
|
257 |
+
if not (1 <= int(steps_val) <= 50):
|
258 |
raise gr.Error("Steps 必须在 1 到 50 之间")
|
259 |
|
260 |
image_url, error = await image_client.generate_image(
|
261 |
+
prompt=prompt_text,
|
262 |
negative_prompt="",
|
263 |
+
seed=current_seed,
|
264 |
+
width=width_val,
|
265 |
+
height=height_val,
|
266 |
+
cfg=float(cfg_val),
|
267 |
+
steps=int(steps_val),
|
268 |
+
model_name=model_name_val
|
269 |
)
|
270 |
|
271 |
if error:
|
272 |
raise gr.Error(error)
|
273 |
|
274 |
+
return image_url, current_seed
|
275 |
+
|
276 |
+
|
277 |
+
# Links for HTML header
|
278 |
+
DISCORD_LINK = os.environ.get("DISCORD_LINK", "https://discord.com/invite/AtRtbe9W8w") # Example
|
279 |
+
APP_INDEX_LINK = os.environ.get("APP_INDEX_LINK", "https://neta.art/") # Example
|
280 |
+
APP_INDEX_ICON = "https://cdn-avatars.huggingface.co/v1/production/uploads/62be651a1e22ec8427aa7096/XQEUF5niIZXQbiOOxn8rQ.jpeg" # Using HF logo
|
281 |
+
|
282 |
+
|
283 |
+
with gr.Blocks(theme=gr.themes.Soft(), title="Lumina Image Playground") as demo:
|
284 |
+
gr.Markdown("<h1>🎨 Lumina Text-to-Image Playground | Lumina 文本生图工具</h1>")
|
285 |
+
gr.Markdown("Describe your vision and let the AI bring it to life! Uses an external API for image generation. | 描述您的创意愿景,让AI为您创造精美图像!使用外部API进行图像生成。")
|
286 |
+
|
287 |
+
gr.HTML(f"""
|
288 |
+
<div style="display: flex; justify-content: flex-start; align-items: center; gap: 15px; margin-bottom: 20px; padding: 10px;">
|
289 |
+
<a href="{DISCORD_LINK}" target="_blank" style="text-decoration: none; color: #5865F2; font-weight: 500; display: inline-flex; align-items: center; gap: 5px;">
|
290 |
+
<img src="https://assets-global.website-files.com/6257adef93867e50d84d30e2/636e0a69f118df70ad7828d4_icon_clyde_blurple_RGB.svg" alt="Discord" style="height: 20px;">
|
291 |
+
Join Discord | 加入Discord
|
292 |
+
</a>
|
293 |
+
<a href="{APP_INDEX_LINK}" target="_blank" style="text-decoration: none; color: #333; font-weight: 500; display: inline-flex; align-items: center; gap: 5px;">
|
294 |
+
<img src="{APP_INDEX_ICON}" alt="App Index" style="height: 20px; border-radius: 3px;">
|
295 |
+
Nieta Index | 捏她主页
|
296 |
+
</a>
|
297 |
+
</div>
|
298 |
+
""")
|
299 |
+
|
300 |
+
with gr.Row(variant="panel"):
|
301 |
+
with gr.Column(scale=2): # Controls Panel
|
302 |
+
gr.Markdown("## ⚙️ Generation Controls | 生成控制")
|
303 |
+
prompt = gr.Textbox(
|
304 |
+
label="Prompt | 提示词", lines=5,
|
305 |
+
placeholder="e.g., A majestic dragon soaring through a cyberpunk city skyline, neon lights reflecting off its scales, intricate details. | 例如:一条威武的巨龙翱翔在赛博朋克城市天际线,霓虹灯映照在它的鳞片上,细节精美。",
|
306 |
+
info="Describe the image you want to create. | 描述您想要创建的图像。"
|
307 |
+
)
|
308 |
+
|
309 |
+
with gr.Accordion("🔧 Advanced Settings | 高级设置", open=True):
|
310 |
+
model_name = gr.Dropdown(
|
311 |
+
label="Model Version | 模型版本", choices=list(MODEL_CONFIGS.keys()), value="ep3",
|
312 |
+
info="Select the generation model. | 选择生成模型。"
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
313 |
)
|
314 |
+
with gr.Row():
|
315 |
+
cfg = gr.Slider(label="CFG Scale | CFG缩放", minimum=1.0, maximum=20.0, step=0.1, value=5.5, info="Guidance strength. Higher values adhere more to prompt. | 引导强度,更高的值更贴近提示词。")
|
316 |
+
steps = gr.Slider(label="Sampling Steps | 采样步数", minimum=1, maximum=50, step=1, value=30, info="Number of steps. More steps can improve quality but take longer. | 步数,更多步数可提高质量但耗时更长。")
|
317 |
+
|
318 |
+
with gr.Row():
|
319 |
+
width = gr.Slider(label="Width | 宽度", minimum=MIN_IMAGE_SIZE, maximum=MAX_IMAGE_SIZE, step=32, value=1024)
|
320 |
+
height = gr.Slider(label="Height | 高度", minimum=MIN_IMAGE_SIZE, maximum=MAX_IMAGE_SIZE, step=32, value=1024)
|
321 |
+
|
322 |
+
with gr.Row():
|
323 |
+
seed = gr.Slider(label="Seed | 种子", minimum=0, maximum=MAX_SEED, step=1, value=random.randint(0, MAX_SEED))
|
324 |
+
randomize_seed = gr.Checkbox(label="Randomize Seed | 随机种子", value=True, info="Use a new random seed for each generation if checked. | 勾选后每次生成使用新的随机种子。")
|
325 |
+
|
326 |
+
run_button = gr.Button("🚀 Generate Image | 生成图像", variant="primary", scale=0)
|
327 |
+
|
328 |
+
with gr.Group():
|
329 |
+
gr.Markdown("### ✨ Example Prompts | 示例提示词")
|
330 |
+
for i, title in enumerate(example_titles):
|
331 |
+
btn = gr.Button(title)
|
332 |
+
btn.click(lambda t=title: full_prompts[t], outputs=[prompt])
|
333 |
+
|
334 |
+
|
335 |
+
with gr.Column(scale=3): # Output Panel
|
336 |
+
gr.Markdown("## 🖼️ Generated Image | 生成图像")
|
337 |
+
result_image = gr.Image(
|
338 |
+
label="Output Image | 输出图像", show_label=False, type="filepath",
|
339 |
+
height=600, # Max display height
|
340 |
+
show_download_button=True, interactive=False,
|
341 |
+
elem_id="result_image_display" # for potential CSS targeting if needed
|
342 |
+
)
|
343 |
+
generated_seed_info = gr.Textbox(label="Seed Used | 使用的种子", interactive=False, placeholder="The seed for the generated image will appear here. | 生成图像所使用的种子值将显示在此处。")
|
344 |
|
345 |
+
# Event Handlers
|
346 |
+
inputs_list = [prompt, seed, randomize_seed, width, height, cfg, steps, model_name]
|
347 |
+
outputs_list = [result_image, generated_seed_info]
|
348 |
+
|
349 |
+
run_button.click(fn=infer, inputs=inputs_list, outputs=outputs_list, api_name="generate_image")
|
350 |
+
prompt.submit(fn=infer, inputs=inputs_list, outputs=outputs_list, api_name="generate_image_submit")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
351 |
|
352 |
|
353 |
if __name__ == "__main__":
|
354 |
+
if DEBUG_MODE:
|
355 |
+
print("DEBUG_MODE is enabled.")
|
356 |
+
if not os.environ.get("API_TOKEN"):
|
357 |
+
print("**************************************************************************************")
|
358 |
+
print("WARNING: API_TOKEN environment variable is not set locally.")
|
359 |
+
print("The application will run, but image generation will fail until API_TOKEN is provided.")
|
360 |
+
print("You can set it by running: export API_TOKEN='your_actual_token_here'")
|
361 |
+
print("Or if using a .env file, ensure it's loaded or API_TOKEN is set in your run config.")
|
362 |
+
print("**************************************************************************************")
|
363 |
+
|
364 |
+
demo.launch(debug=DEBUG_MODE, show_error=True)
|