shiertier commited on
Commit
23498c5
·
verified ·
1 Parent(s): 715d5b8

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +339 -283
app.py CHANGED
@@ -1,67 +1,66 @@
1
- import gradio as gr
2
- import os
3
- import random
4
- import httpx
5
  import asyncio
6
  import gc
7
- import weakref
 
 
8
  import psutil
 
9
  import threading
10
  import time
 
11
  from dataclasses import dataclass, field
12
- from typing import Any, Optional
13
- import contextlib
14
- import logging
15
-
16
- # 配置日志
17
- logging.basicConfig(
18
- level=logging.INFO,
19
- format="%(asctime)s - %(name)s - %(levelname)s - %(message)s",
20
- handlers=[
21
- logging.StreamHandler(),
22
- logging.FileHandler("app.log", mode="a", encoding="utf-8"),
23
- ],
24
- )
25
- logger = logging.getLogger(__name__)
26
 
27
- # 内存监控配置
28
- MEMORY_CHECK_INTERVAL = 300 # 5分钟检查一次内存
29
- MEMORY_THRESHOLD_PERCENT = 80 # 内存使用百分比阈值
30
 
31
- # 常量定义
32
- HTTP_STATUS_CENSORED = 451
33
  HTTP_STATUS_OK = 200
 
 
 
34
  MAX_SEED = 2147483647 # (2**31 - 1)
35
  MAX_IMAGE_SIZE = 2048
36
- MIN_IMAGE_SIZE = 256 # Smallest dimension for SDXL like models often 512, but API might support smaller. Adjusted to API's limits.
37
-
38
- # 调试模式
39
  DEBUG_MODE = os.environ.get("DEBUG_MODE", "false").lower() == "true"
40
 
41
- # 模型配置映射
 
 
 
 
 
 
 
 
 
 
42
  MODEL_CONFIGS = {
43
- # "ep3": "ep3.pth",
44
- # "ep3latest": "ep3latest.pth",
45
  "ep6": "0622.pth",
46
  }
47
 
48
- # 连接池配置
49
  HTTP_LIMITS = httpx.Limits(
50
  max_keepalive_connections=5, max_connections=10, keepalive_expiry=30.0
51
  )
52
-
53
  HTTP_TIMEOUT = httpx.Timeout(connect=30.0, read=300.0, write=30.0, pool=10.0)
54
 
55
-
56
- def validate_dimensions(width: int, height: int) -> tuple[int, int]:
57
- """验证并调整图片尺寸"""
58
- width = max(MIN_IMAGE_SIZE, min(int(width), MAX_IMAGE_SIZE))
59
- height = max(MIN_IMAGE_SIZE, min(int(height), MAX_IMAGE_SIZE))
60
- width = (width // 32) * 32
61
- height = (height // 32) * 32
62
- return width, height
 
 
63
 
64
 
 
65
  @dataclass
66
  class LuminaConfig:
67
  """Lumina模型配置"""
@@ -79,16 +78,111 @@ class ImageGenerationConfig:
79
  width: int = 1024
80
  height: int = 1024
81
  seed: int | None = None
82
- use_polish: bool = False # This wasn't exposed in UI, assuming false
83
  is_lumina: bool = True
84
  lumina_config: LuminaConfig = field(default_factory=LuminaConfig)
85
 
86
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
87
  class ImageClient:
88
  """图像生成客户端"""
89
 
90
  def __init__(self) -> None:
91
- self.x_token = os.environ.get("API_TOKEN", "")
92
  if not self.x_token:
93
  raise ValueError("环境变量中未设置API_TOKEN")
94
 
@@ -100,18 +194,16 @@ class ImageClient:
100
  self.polling_interval = 3.0
101
  self.default_headers = {
102
  "Content-Type": "application/json",
103
- "x-platform": "nieta-app/web", # Or a generic identifier if preferred
104
  "X-Token": self.x_token,
105
  }
106
 
107
- # 预创建HTTP客户端配置,但不保持持久连接
108
  self._client_config = {
109
  "limits": HTTP_LIMITS,
110
  "timeout": HTTP_TIMEOUT,
111
  "headers": self.default_headers,
112
  }
113
 
114
- # 用于跟踪活跃任务的弱引用集合
115
  self._active_tasks = weakref.WeakSet()
116
 
117
  def _prepare_prompt_data(
@@ -153,7 +245,7 @@ class ImageClient:
153
  "seed": config.seed,
154
  "meta": {"entrance": "PICTURE,PURE"},
155
  "context_model_series": None,
156
- "negative_freetext": "", # Negative handled in rawPrompt
157
  "advanced_translator": config.use_polish,
158
  }
159
  if config.is_lumina:
@@ -171,7 +263,6 @@ class ImageClient:
171
  async def _poll_task_status(self, task_uuid: str) -> dict[str, Any]:
172
  """轮询任务状态 - 优化内存使用和连接管理"""
173
  status_url = self.lumina_task_status_url.format(task_uuid=task_uuid)
174
-
175
  poll_timeout = httpx.Timeout(connect=10.0, read=30.0, write=10.0, pool=5.0)
176
 
177
  try:
@@ -181,7 +272,6 @@ class ImageClient:
181
  for attempt in range(self.max_polling_attempts):
182
  try:
183
  response = await client.get(status_url)
184
-
185
  if response.status_code != HTTP_STATUS_OK:
186
  logger.warning(f"轮询失败 - 状态码: {response.status_code}")
187
  return {
@@ -189,7 +279,6 @@ class ImageClient:
189
  "error": f"获取任务状态失败: {response.status_code} - {response.text[:200]}",
190
  }
191
 
192
- # 解析JSON响应
193
  try:
194
  result = response.json()
195
  except Exception as e:
@@ -200,7 +289,6 @@ class ImageClient:
200
  }
201
 
202
  task_status = result.get("task_status")
203
-
204
  if task_status == "SUCCESS":
205
  artifacts = result.get("artifacts", [])
206
  if artifacts and len(artifacts) > 0:
@@ -222,20 +310,18 @@ class ImageClient:
222
  return {"success": False, "error": "任务超时"}
223
 
224
  await asyncio.sleep(self.polling_interval)
225
-
226
- # 每10次轮询执行一次垃圾回收以防止内存累积
227
  if attempt % 10 == 0 and attempt > 0:
228
  gc.collect()
229
  logger.debug(f"轮询第{attempt}次,执行内存清理")
230
 
231
  except asyncio.TimeoutError:
232
  logger.warning(f"轮询超时 - 尝试 {attempt}")
233
- if attempt >= 3: # 连续3次超时后退出
234
  return {
235
  "success": False,
236
  "error": "连接超时,请检查网络状况",
237
  }
238
- await asyncio.sleep(self.polling_interval * 2) # 延长等待时间
239
  continue
240
  except Exception as e:
241
  logger.warning(f"轮询异常 - 尝试 {attempt}: {str(e)}")
@@ -265,23 +351,21 @@ class ImageClient:
265
  cfg: float,
266
  steps: int,
267
  model_name: str = "ep6",
 
268
  ) -> tuple[str | None, str | None]:
269
  """生成图片"""
270
  try:
271
- # 获取模型路径
272
  model_path = MODEL_CONFIGS.get(model_name, MODEL_CONFIGS["ep6"])
273
-
274
- # 准备配置
275
  config = ImageGenerationConfig(
276
  prompts=self._prepare_prompt_data(prompt, negative_prompt),
277
  width=width,
278
  height=height,
279
  seed=seed,
 
280
  is_lumina=True,
281
  lumina_config=LuminaConfig(model_name=model_path, cfg=cfg, step=steps),
282
  )
283
 
284
- # 发送生成请求
285
  async with httpx.AsyncClient(**self._client_config) as client:
286
  payload = self._build_payload(config)
287
  if DEBUG_MODE:
@@ -302,18 +386,14 @@ class ImageClient:
302
 
303
  if response.status_code == HTTP_STATUS_CENSORED:
304
  return None, "内容不合规"
305
-
306
- # 处理并发限制错误
307
  if response.status_code == 433:
308
  return None, "⏳ 服务器正忙,同时生成的图片数量已达上限,请稍后重试"
309
-
310
  if response.status_code != HTTP_STATUS_OK:
311
  return (
312
  None,
313
  f"API请求失败: {response.status_code} - {response.text[:200]}",
314
  )
315
 
316
- # API直接返回UUID字符串(根据model_studio的实现)
317
  content = response.text.strip()
318
  task_uuid = content.replace('"', "")
319
 
@@ -323,7 +403,6 @@ class ImageClient:
323
  if not task_uuid:
324
  return None, f"未获取到任务ID,API响应: {response.text[:200]}"
325
 
326
- # 轮询任务状态
327
  result = await self._poll_task_status(task_uuid)
328
  if result["success"]:
329
  return result["image_url"], None
@@ -334,13 +413,11 @@ class ImageClient:
334
  logger.error(f"生成图片异常: {str(e)}")
335
  return None, f"生成图片时发生错误: {str(e)}"
336
  finally:
337
- # 确保资源清理
338
  gc.collect()
339
 
340
  def cleanup(self):
341
  """清理资源"""
342
  try:
343
- # 清理活跃任务
344
  self._active_tasks.clear()
345
  gc.collect()
346
  logger.info("ImageClient资源清理完成")
@@ -348,24 +425,6 @@ class ImageClient:
348
  logger.error(f"资源清理异常: {str(e)}")
349
 
350
 
351
- # 创建图片生成客户端实例
352
- image_client: Optional[ImageClient] = None
353
-
354
-
355
- def get_image_client() -> ImageClient:
356
- """获取图片生成客户端实例 - 单例模式"""
357
- global image_client
358
- if image_client is None:
359
- try:
360
- image_client = ImageClient()
361
- logger.info("ImageClient初始化成功")
362
- except Exception as e:
363
- logger.error(f"ImageClient初始化失败: {e}")
364
- raise
365
- return image_client
366
-
367
-
368
- # 内存监控
369
  class MemoryMonitor:
370
  """内存监控器"""
371
 
@@ -394,21 +453,15 @@ class MemoryMonitor:
394
  """内存监控循环"""
395
  while self.running:
396
  try:
397
- # 获取当前进程内存使用情况
398
  process = psutil.Process()
399
  memory_info = process.memory_info()
400
  memory_mb = memory_info.rss / 1024 / 1024
401
-
402
- # 获取系统总内存并计算使用百分比
403
- system_memory = psutil.virtual_memory()
404
  memory_percent = process.memory_percent()
405
 
406
- # 记录内存使用情况(基于百分比而非固定阈值)
407
  if memory_percent > MEMORY_THRESHOLD_PERCENT:
408
  logger.warning(
409
  f"内存使用量过高: {memory_mb:.2f} MB ({memory_percent:.1f}%)"
410
  )
411
- # 执行垃圾回收
412
  gc.collect()
413
  logger.info("执行垃圾回收")
414
  else:
@@ -422,73 +475,45 @@ class MemoryMonitor:
422
  time.sleep(MEMORY_CHECK_INTERVAL)
423
 
424
 
425
- # 全局内存监控实例
426
- memory_monitor = MemoryMonitor()
 
 
 
 
 
 
 
 
 
 
427
 
428
 
429
- # 优雅关闭处理
430
  async def cleanup_resources():
431
  """清理应用资源"""
432
- global image_client, memory_monitor
433
-
434
  try:
435
- # 停止内存监控
436
  memory_monitor.stop_monitoring()
437
-
438
- # 清理图片客户端
439
  if image_client is not None:
440
  image_client.cleanup()
441
  image_client = None
442
-
443
- # 强制垃圾回收
444
  gc.collect()
445
-
446
- # 记录最终内存使用情况
447
  try:
448
  process = psutil.Process()
449
  memory_mb = process.memory_info().rss / 1024 / 1024
450
  logger.info(f"清理后内存使用: {memory_mb:.2f} MB")
451
  except Exception:
452
  pass
453
-
454
  logger.info("应用资源清理完成")
455
  except Exception as e:
456
  logger.error(f"资源清理异常: {str(e)}")
457
 
458
 
459
- # Example prompts
460
- example_titles = [
461
- "Emotionless figure stares upward in heavy rain, drenched and defeated, set against a monochrome grey background.",
462
- "Glamorous woman in vintage cheongsam on dim staircase, exuding 1930s Shanghai elegance and ethereal light",
463
- "薄明のサイバーパンク都市で、銀灰獣人少女が近未来装備を纏い振り返るクールなアニメイラスト",
464
- "都会の孤独な銀髪女性のアニメ風イラスト",
465
- "仰视视角下,翘腿而坐的西装大佬俯视着你,气场冷峻危险",
466
- "黄昏天台,白裙女子独望天空,惆怅静谧,风中飘扬",
467
- ]
468
- full_prompts = {
469
- example_titles[
470
- 0
471
- ]: "looking_up, rain, heavy_rain, emotionless, high_angle, perspective, face_focus, from_above, from_side, grey_background, empty_eyes, defeated_look, limited_palette, partially_colored, monochrome_background, wet_hair, wet_clothes, messy_hair A figure stands motionless in torrential rain, their face captured from a high-angle perspective with vacant eyes staring upward into the downpour. Sodden strands of hair cling to pale skin, soaked clothing blending into the monochrome grey void surrounding them. The limited color palette isolates faint blue tones in their parted lips against the washed-out background, their expression of utter defeat frozen as raindrops streak vertically through the frame. Sharp focus on their emotionless features contrasts with the blurred chaos of falling water, the composition's oppressive atmosphere amplified by the overhead view's psychological distance.",
472
- example_titles[
473
- 1
474
- ]: "Elegant anime-style illustration of a glamorous woman standing on a dimly lit staircase, bathed in soft, ethereal light. Medium full-body shot. She has short, wavy black hair adorned with ornate golden hairpieces and a delicate feather. Her makeup is refined, with deep red lipstick and sharp eyeliner accentuating her poised expression. She wears a dark green high-collared cheongsam embroidered with a vividly colored crane and landscape motif, blending traditional Chinese elements with a touch of Art Deco. Her black lace gloves, beaded pearl chains draped across her body, and intricate earrings enhance her luxurious, vintage aura. A fur stole rests over her arms, adding texture and sophistication. The setting is subtly blurred, with falling sparkles and filtered light through windowpanes creating a dreamy, nostalgic atmosphere. The composition evokes 1930s Shanghai elegance, rendered in a painterly digital style with high fashion sensibility and dramatic lighting contrast.",
475
- example_titles[
476
- 2
477
- ]: "薄明のサイバーパンク都市を背景にした、ハイブリッドな獣人少女のアニメイラスト。構図はバストアップ〜ウエストまでの斜めアングルで、キャラクターが観覧者にやや振り返るような姿勢をとっている。彼女の顔は精巧な狼のマズルと人間的な表情が融合しており、瞳はグリッチ風の蛍光ブルーに輝くオッドアイ。毛皮は柔らかな銀灰色で、頬や耳の先端には淡い紫のグラデーションが入り、毛並みは丁寧に一本ずつ描写されている。衣装は近未来的なデザインのボディスーツで、胸元には蛍光回路のような文様が走り、左肩には機械義肢風のメタルアーマーが装着されている。耳にはピアスとデータ受信装置が取り付けられており、首には布製のスカーフと識別タグが結ばれている。背景にはネオンに照らされた廃墟ビルとホログラム広告が揺らめき、空にはサイバー・オーロラが流れている。カラーリングはダークトーンに蛍光アクセントが効いたネオ東京風配色(黒、紫、青、サイバーグリーン)。アートスタイルはセルルックアニメ+ハードエッジグラデーション、ディテール豊かで表情と毛の質感に特に重点を置く。キャラクターの雰囲気はクールで孤独だが、どこか繊細な感情を秘めた印象。",
478
- example_titles[
479
- 3
480
- ]: "銀白色の長い髪を持つ若い女性が黒いコートを羽織り、都会の街中に佇む、メランコリックな雰囲気のアニメ風イラスト。背景にはグレースケールで描かれた、顔のない人々の群衆がぼかされて描かれ、彼女だけがシャープな焦点で際立っている。彼女は長いダークカラーのオーバーコートを着用し、物憂げで遠くを見つめるような表情を浮かべている。シーンは現代の都市の街角で設定されており、背景には建築的な要素が描かれている。配色はモノクロームで、黒・白・灰色を基調としたフィルム・ノワール風の雰囲気を醸し出している。アートスタイルはリアルな都市描写とアニメ的キャラクターデザインを融合させており、主人公の細部まで描き込まれた描写と、幽霊のように描かれた背景の人々とのコントラストによって、強い孤独感と疎外感を表現している。どんよりとした曇り空のライティングと都市背景が、重苦しい空気感を一層引き立てている。キャラクターデザインは、暗い服装に映える銀髪により、どこか神秘的または超自然的な要素を感じさせる。プロフェッショナルなデジタルイラストで、映画のような質感と強い感情的トーンを備え、現代社会における孤独や断絶のテーマを描いている。雰囲気としては、心理的または超自然的な物語の一場面を想起させる構成となっている。",
481
- example_titles[
482
- 4
483
- ]: "戏剧感十足的数字插画,描绘一位黑社会风格的长发美男子大佬坐姿翘着二郎腿,从大仰视视角俯视观者,气场强烈。他身穿剪裁锋利的深色西装,带有低调的细条纹图案,外套敞开,内衬深色马甲与整洁的衬衫,显得既优雅又危险。他的长发黑亮顺滑,自肩膀垂落,几缕发丝在环境光中微微反光。他一只手漫不经心地夹着一根燃着的雪茄,袅袅烟雾在昏暗中盘旋上升。他俯视下方,眉头微蹙,唇角紧绷,脸上带有一丝明显的不悦,神情冷峻。仰视视角放大了他身上的压迫感与支配力,整个人散发出沉稳却不可侵犯的气势。另一只手或搭在椅扶或搁在膝上,动作自信从容。背景为昏暗豪华的私人会客室或办公室,可见金属饰条、皮革沙发或昏黄灯光等元素。画面风格精致、电影感强烈,通过明暗对比、表情细节与强光源制造紧张与权威氛围。",
484
- example_titles[
485
- 5
486
- ]: "惆怅氛围的数字插画,描绘一位身穿白裙的女子独自站在天台上,在黄昏时分凝望天空。她的长发与白裙的裙摆在风中轻轻飘扬,画面充满静谧与动感的交织。她仰望着染上橙粉色霞光的天空,脸上露出若有所思、带着忧郁的神情,仿佛沉浸在回忆或思念之中。夕阳的余晖洒落在她身上与天台上,为画面增添一层柔和的金色光晕。她的站姿略带沉重,肩膀微微下垂,双手自然垂落或轻轻拽住裙摆,展现出内敛的情绪张力。天台背景简洁,设有低矮护栏���通风设备等结构,远处是模糊的城市天际线,进一步强化孤独氛围。风吹起她的发丝与裙摆,为静止的场景注入一丝淡淡的动态诗意。画风柔和写意,色彩渐变细腻,注重光影、空气感与人物情绪的融合,营造出宁静而感伤的黄昏场景。",
487
- }
488
-
489
-
490
  async def infer(
491
  prompt_text,
 
492
  seed_val,
493
  randomize_seed_val,
494
  width_val,
@@ -498,11 +523,8 @@ async def infer(
498
  model_name_val,
499
  ):
500
  """推理函数 - 优化内存管理和错误处理"""
501
- client = None
502
  try:
503
  logger.info(f"开始生成图像: {prompt_text[:50]}...")
504
-
505
- # 获取客户端实例
506
  try:
507
  client = get_image_client()
508
  except Exception as e:
@@ -512,21 +534,27 @@ async def infer(
512
  if not prompt_text.strip():
513
  raise gr.Error("提示词不能为空")
514
 
 
 
 
 
 
 
 
 
515
  current_seed = int(seed_val)
516
  if randomize_seed_val:
517
  current_seed = random.randint(0, MAX_SEED)
518
 
519
- # 验证并调整尺寸
520
  width_val, height_val = validate_dimensions(width_val, height_val)
521
 
522
- # 验证其他参数
523
  if not (1.0 <= float(cfg_val) <= 20.0):
524
  raise gr.Error("CFG Scale 必须在 1.0 到 20.0 之间")
525
  if not (1 <= int(steps_val) <= 50):
526
  raise gr.Error("Steps 必须在 1 到 50 之间")
527
 
528
  image_url, error = await client.generate_image(
529
- prompt=prompt_text,
530
  negative_prompt="",
531
  seed=current_seed,
532
  width=width_val,
@@ -534,6 +562,7 @@ async def infer(
534
  cfg=float(cfg_val),
535
  steps=int(steps_val),
536
  model_name=model_name_val,
 
537
  )
538
 
539
  if error:
@@ -541,153 +570,17 @@ async def infer(
541
  raise gr.Error(error)
542
 
543
  logger.info("图像生成成功")
544
- return image_url, current_seed
545
 
546
  except gr.Error:
547
- # 重新抛出Gradio错误
548
  raise
549
  except Exception as e:
550
  logger.error(f"推理过程异常: {str(e)}")
551
  raise gr.Error(f"生成图像时发生意外错误: {str(e)}")
552
  finally:
553
- # 确保内存清理
554
  gc.collect()
555
 
556
 
557
- # Links for HTML header
558
- DISCORD_LINK = os.environ.get(
559
- "DISCORD_LINK", "https://discord.com/invite/AtRtbe9W8w"
560
- ) # Example
561
- APP_INDEX_LINK = os.environ.get("APP_INDEX_LINK", "https://app.nieta.art/") # Example
562
- APP_INDEX_ICON = "https://cdn-avatars.huggingface.co/v1/production/uploads/62be651a1e22ec8427aa7096/XQEUF5niIZXQbiOOxn8rQ.jpeg" # Using HF logo
563
-
564
-
565
- with gr.Blocks(theme=gr.themes.Soft(), title="Lumina Image Playground") as demo:
566
- gr.Markdown("<h1>🎨 Lumina Text-to-Image Playground | Lumina 文本生图工具</h1>")
567
- gr.Markdown(
568
- "Fine-tuned Lumina model specialized for anime/manga style generation! Supports Chinese, English, and Japanese prompts. Model under active development - more exciting features coming soon!\n 🌸 专为二次元风格优化的Lumina模型!支持中文、英文、日文三语提示词,让您的创意无界限!模型持续优化中,敬请期待。"
569
- )
570
-
571
- gr.HTML(f"""
572
- <div style="display: flex; justify-content: flex-start; align-items: center; gap: 15px; margin-bottom: 20px; padding: 10px;">
573
- <a href="{DISCORD_LINK}" target="_blank" style="text-decoration: none; color: #5865F2; font-weight: 500; display: inline-flex; align-items: center; gap: 5px;">
574
- <img src="https://assets-global.website-files.com/6257adef93867e50d84d30e2/636e0a69f118df70ad7828d4_icon_clyde_blurple_RGB.svg" alt="Discord" style="height: 20px;">
575
- Join Discord | 加入Discord
576
- </a>
577
- <a href="{APP_INDEX_LINK}" target="_blank" style="text-decoration: none; color: #333; font-weight: 500; display: inline-flex; align-items: center; gap: 5px;">
578
- <img src="{APP_INDEX_ICON}" alt="App Index" style="height: 20px; border-radius: 3px;">
579
- Nieta Home | 捏Ta主页
580
- </a>
581
- </div>
582
- """)
583
-
584
- with gr.Row(variant="panel"):
585
- with gr.Column(scale=2): # Controls Panel
586
- gr.Markdown("## ⚙️ Generation Controls | 生成控制")
587
- prompt = gr.Textbox(
588
- label="Prompt | 提示词",
589
- lines=5,
590
- placeholder="e.g., A majestic dragon soaring through a cyberpunk city skyline, neon lights reflecting off its scales, intricate details. | 例如:一条威武的巨龙翱翔在赛博朋克城市天际线,霓虹灯映照在它的鳞片上,细节精美。",
591
- info="Describe the image you want to create. | 描述您想要创建的图像。",
592
- )
593
-
594
- with gr.Accordion("🔧 Advanced Settings | 高级设置", open=True):
595
- model_name = gr.Dropdown(
596
- label="Model Version | 模型版本",
597
- choices=list(MODEL_CONFIGS.keys()),
598
- value="ep6",
599
- info="Select the generation model. | 选择生成模型。",
600
- )
601
- with gr.Row():
602
- cfg = gr.Slider(
603
- label="CFG Scale | CFG缩放",
604
- minimum=1.0,
605
- maximum=20.0,
606
- step=0.1,
607
- value=5.5,
608
- info="Guidance strength. Higher values adhere more to prompt. | 引导强度,更高的值更贴近提示词。",
609
- )
610
- steps = gr.Slider(
611
- label="Sampling Steps | 采样步数",
612
- minimum=1,
613
- maximum=50,
614
- step=1,
615
- value=30,
616
- info="Number of steps. More steps can improve quality but take longer. | 步数,更多步数可提高质量但耗时更长。",
617
- )
618
-
619
- with gr.Row():
620
- width = gr.Slider(
621
- label="Width | 宽度",
622
- minimum=MIN_IMAGE_SIZE,
623
- maximum=MAX_IMAGE_SIZE,
624
- step=32,
625
- value=1024,
626
- )
627
- height = gr.Slider(
628
- label="Height | 高度",
629
- minimum=MIN_IMAGE_SIZE,
630
- maximum=MAX_IMAGE_SIZE,
631
- step=32,
632
- value=1024,
633
- )
634
-
635
- with gr.Row():
636
- seed = gr.Slider(
637
- label="Seed | 种子",
638
- minimum=0,
639
- maximum=MAX_SEED,
640
- step=1,
641
- value=random.randint(0, MAX_SEED),
642
- )
643
- randomize_seed = gr.Checkbox(
644
- label="Randomize Seed | 随机种子",
645
- value=True,
646
- info="Use a new random seed for each generation if checked. | 勾选后每次生成使用新的随机种子。",
647
- )
648
-
649
- run_button = gr.Button(
650
- "🚀 Generate Image | 生成图像", variant="primary", scale=0
651
- )
652
-
653
- with gr.Group():
654
- gr.Markdown("### ✨ Example Prompts | 示例提示词")
655
- for i, title in enumerate(example_titles):
656
- btn = gr.Button(title)
657
- btn.click(lambda t=title: full_prompts[t], outputs=[prompt])
658
-
659
- with gr.Column(scale=3): # Output Panel
660
- gr.Markdown("## 🖼️ Generated Image | 生成图像")
661
- result_image = gr.Image(
662
- label="Output Image | 输出图像",
663
- show_label=False,
664
- type="filepath",
665
- height=600, # Max display height
666
- show_download_button=True,
667
- interactive=False,
668
- elem_id="result_image_display", # for potential CSS targeting if needed
669
- )
670
- generated_seed_info = gr.Textbox(
671
- label="Seed Used | 使用的种子",
672
- interactive=False,
673
- placeholder="The seed for the generated image will appear here. | 生成图像所使用的种子值将显示在此处。",
674
- )
675
-
676
- # Event Handlers
677
- inputs_list = [prompt, seed, randomize_seed, width, height, cfg, steps, model_name]
678
- outputs_list = [result_image, generated_seed_info]
679
-
680
- run_button.click(
681
- fn=infer, inputs=inputs_list, outputs=outputs_list, api_name="generate_image"
682
- )
683
- prompt.submit(
684
- fn=infer,
685
- inputs=inputs_list,
686
- outputs=outputs_list,
687
- api_name="generate_image_submit",
688
- )
689
-
690
-
691
  def setup_signal_handlers():
692
  """设置信号处理器以实现优雅关闭"""
693
  import signal
@@ -702,20 +595,178 @@ def setup_signal_handlers():
702
  logger.error(f"清理资源时出错: {str(e)}")
703
  logger.info("应用已安全关闭")
704
 
705
- # 注册信号处理器
706
  signal.signal(signal.SIGINT, cleanup_handler)
707
  signal.signal(signal.SIGTERM, cleanup_handler)
708
-
709
- # 注册退出处理器
710
  atexit.register(cleanup_handler)
711
 
712
 
713
- if __name__ == "__main__":
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
714
  try:
715
  # 设置信号处理器
716
  setup_signal_handlers()
717
 
718
  # 启动内存监控
 
719
  memory_monitor.start_monitoring()
720
 
721
  if DEBUG_MODE:
@@ -749,14 +800,15 @@ if __name__ == "__main__":
749
  pass
750
 
751
  logger.info("启动Gradio应用...")
 
752
  demo.launch(
753
  debug=DEBUG_MODE,
754
  show_error=True,
755
- server_name="0.0.0.0", # 允许外部访问
756
  server_port=7860,
757
  share=False,
758
- max_threads=40, # 限制线程数
759
- favicon_path=None, # 避免额外的文件加载
760
  )
761
 
762
  except KeyboardInterrupt:
@@ -771,3 +823,7 @@ if __name__ == "__main__":
771
  except Exception as e:
772
  logger.error(f"最终清理时出错: {str(e)}")
773
  logger.info("应用已退出")
 
 
 
 
 
1
+ # 导入区域
 
 
 
2
  import asyncio
3
  import gc
4
+ import httpx
5
+ import logging
6
+ import os
7
  import psutil
8
+ import random
9
  import threading
10
  import time
11
+ import weakref
12
  from dataclasses import dataclass, field
13
+ from typing import Any, Optional, Tuple, List, Dict
 
 
 
 
 
 
 
 
 
 
 
 
 
14
 
15
+ import gradio as gr
 
 
16
 
17
+ # 常量定义区域
18
+ # HTTP 状态码
19
  HTTP_STATUS_OK = 200
20
+ HTTP_STATUS_CENSORED = 451
21
+
22
+ # 系统配置
23
  MAX_SEED = 2147483647 # (2**31 - 1)
24
  MAX_IMAGE_SIZE = 2048
25
+ MIN_IMAGE_SIZE = 256
26
+ MEMORY_CHECK_INTERVAL = 300 # 5分钟检查一次内存
27
+ MEMORY_THRESHOLD_PERCENT = 80 # 内存使用百分比阈值
28
  DEBUG_MODE = os.environ.get("DEBUG_MODE", "false").lower() == "true"
29
 
30
+ # API配置
31
+ API_TOKEN = os.environ.get("API_TOKEN", "")
32
+ if not API_TOKEN:
33
+ raise ValueError("环境变量中未设置API_TOKEN")
34
+
35
+ # 外部链接
36
+ DISCORD_LINK = os.environ.get("DISCORD_LINK", "https://discord.com/invite/AtRtbe9W8w")
37
+ APP_INDEX_LINK = os.environ.get("APP_INDEX_LINK", "https://app.nieta.art/")
38
+ APP_INDEX_ICON = "https://cdn-avatars.huggingface.co/v1/production/uploads/62be651a1e22ec8427aa7096/XQEUF5niIZXQbiOOxn8rQ.jpeg"
39
+
40
+ # 模型配置
41
  MODEL_CONFIGS = {
 
 
42
  "ep6": "0622.pth",
43
  }
44
 
45
+ # HTTP 客户端配置
46
  HTTP_LIMITS = httpx.Limits(
47
  max_keepalive_connections=5, max_connections=10, keepalive_expiry=30.0
48
  )
 
49
  HTTP_TIMEOUT = httpx.Timeout(connect=30.0, read=300.0, write=30.0, pool=10.0)
50
 
51
+ # 日志配置
52
+ logging.basicConfig(
53
+ level=logging.INFO,
54
+ format="%(asctime)s - %(name)s - %(levelname)s - %(message)s",
55
+ handlers=[
56
+ logging.StreamHandler(),
57
+ logging.FileHandler("app.log", mode="a", encoding="utf-8"),
58
+ ],
59
+ )
60
+ logger = logging.getLogger(__name__)
61
 
62
 
63
+ # 数据模型定义
64
  @dataclass
65
  class LuminaConfig:
66
  """Lumina模型配置"""
 
78
  width: int = 1024
79
  height: int = 1024
80
  seed: int | None = None
81
+ use_polish: bool = False
82
  is_lumina: bool = True
83
  lumina_config: LuminaConfig = field(default_factory=LuminaConfig)
84
 
85
 
86
+ # 全局变量
87
+ image_client = None
88
+ polish_client = None
89
+
90
+ # 示例提示词
91
+ example_titles = [
92
+ "Emotionless figure stares upward in heavy rain, drenched and defeated, set against a monochrome grey background.",
93
+ "Glamorous woman in vintage cheongsam on dim staircase, exuding 1930s Shanghai elegance and ethereal light",
94
+ "薄明のサイバーパンク都市で、銀灰獣人少女が近未来装備を纏い振り返るクールなアニメイラスト",
95
+ "都会の孤独な銀髪女性のアニメ風イラスト",
96
+ "仰视视角下,翘腿而坐的西装大佬俯视着你,气场冷峻危险",
97
+ "黄昏天台,白裙女子独望天空,惆怅静谧,风中飘扬",
98
+ ]
99
+
100
+ full_prompts = {
101
+ example_titles[
102
+ 0
103
+ ]: "looking_up, rain, heavy_rain, emotionless, high_angle, perspective, face_focus, from_above, from_side, grey_background, empty_eyes, defeated_look, limited_palette, partially_colored, monochrome_background, wet_hair, wet_clothes, messy_hair A figure stands motionless in torrential rain, their face captured from a high-angle perspective with vacant eyes staring upward into the downpour. Sodden strands of hair cling to pale skin, soaked clothing blending into the monochrome grey void surrounding them. The limited color palette isolates faint blue tones in their parted lips against the washed-out background, their expression of utter defeat frozen as raindrops streak vertically through the frame. Sharp focus on their emotionless features contrasts with the blurred chaos of falling water, the composition's oppressive atmosphere amplified by the overhead view's psychological distance.",
104
+ example_titles[
105
+ 1
106
+ ]: "Elegant anime-style illustration of a glamorous woman standing on a dimly lit staircase, bathed in soft, ethereal light. Medium full-body shot. She has short, wavy black hair adorned with ornate golden hairpieces and a delicate feather. Her makeup is refined, with deep red lipstick and sharp eyeliner accentuating her poised expression. She wears a dark green high-collared cheongsam embroidered with a vividly colored crane and landscape motif, blending traditional Chinese elements with a touch of Art Deco. Her black lace gloves, beaded pearl chains draped across her body, and intricate earrings enhance her luxurious, vintage aura. A fur stole rests over her arms, adding texture and sophistication. The setting is subtly blurred, with falling sparkles and filtered light through windowpanes creating a dreamy, nostalgic atmosphere. The composition evokes 1930s Shanghai elegance, rendered in a painterly digital style with high fashion sensibility and dramatic lighting contrast.",
107
+ example_titles[
108
+ 2
109
+ ]: "薄明のサイバーパンク都市を背景にした、ハイブリッドな獣人少女のアニメイラスト。構図はバストアップ〜ウエストまでの斜めアングルで、キャラクターが観覧者にやや振り返るような姿勢をとっている。彼女の顔は精巧な狼のマズルと人間的な表情が融合しており、瞳はグリッチ風の蛍光ブルーに輝くオッドアイ。毛皮は柔らかな銀灰色で、頬や耳の先端には淡い紫のグラデーションが入り、毛並みは丁寧に一本ずつ描写されている。衣装は近未来的なデザインのボディスーツで、胸元には蛍光回路のような文様が走り、左肩には機械義肢風のメタルアーマーが装着されている。耳にはピアスとデータ受信装置が取り付けられており、首には布製のスカーフと識別タグが結ばれている。背景にはネオンに照らされた廃墟ビルとホログラム広告が揺らめき、空にはサイバー・オーロラが流れている。カラーリングはダークトーンに蛍光アクセントが効いたネオ東京風配色(黒、紫、青、サイバーグリーン)。アートスタイルはセルルックアニメ+ハードエッジグラデーション、ディテール豊かで表情と毛の質感に特に重点を置く。キャラクターの雰囲気はクールで孤独だが、どこか繊細な感情を秘めた印象。",
110
+ example_titles[
111
+ 3
112
+ ]: "銀白色の長い髪を持つ若い女性が黒いコートを羽織り、都会の街中に佇む、メランコリックな雰囲気のアニメ風イラスト。背景にはグレースケールで描かれた、顔のない人々の群衆がぼかされて描かれ、彼女だけがシャープな焦点で際立っている。彼女は長いダークカラーのオーバーコートを着用し、物憂げで遠くを見つめるような表情を浮かべている。シーンは現代の都市の街角で設定されており、背景には建築的な要素が描かれている。配色はモノクロームで、黒・白・灰色を基調としたフィルム・ノワール風の雰囲気を醸し出している。アートスタイルはリアルな都市描写とアニメ的キャラクターデザインを融合させており、主人公の細部まで描き込まれた描写と、幽霊のように描かれた背景の人々とのコントラストによって、強い孤独感と疎外感を表現している。どんよりとした曇り空のライティングと都市背景が、重苦しい空気感を一層引き立てている。キャラクターデザインは、暗い服装に映える銀髪により、どこか神秘的または超自然的な要素を感じさせる。プロフェッショナルなデジタルイラストで、映画のような質感と強い感情的トーンを備え、現代社会における孤独や断絶のテーマを描いている。雰囲気としては、心理的または超自然的な物語の一場面を想起させる構成となっている。",
113
+ example_titles[
114
+ 4
115
+ ]: "戏剧感十足的数字插画,描绘一位黑社会风格的长发美男子大佬坐姿翘着二郎腿,从大仰视视角俯视观者,气场强烈。他身穿剪裁锋利的深色西装,带有低调的细条纹图案,外套敞开,内衬深色马甲与整洁的衬衫,显得既优雅又危险。他的长发黑亮顺滑,自肩膀垂落,几缕发丝在环境光中微微反光。他一只手漫不经心地夹着一根燃着的雪茄,袅袅烟雾在昏暗中盘旋上升。他俯视下方,眉头微蹙,唇角紧绷,脸上带有一丝明显的不悦,神情冷峻。仰视视角放大了他身上的压迫感与支配力,整个人散发出沉稳却不可侵犯的气势。另一只手或搭在椅扶或搁在膝上,动作自信从容。背景为昏暗豪华的私人会客室或办公室,可见金属饰条、皮革沙发或昏黄灯光等元素。画面风格精致、电影感强烈,通过明暗对比、表情细节与强光源制造紧张与权威氛围。",
116
+ example_titles[
117
+ 5
118
+ ]: "惆怅氛围的数字插画,描绘一位身穿白裙的女子独自站在天台上,在黄昏时分凝望天空。她的长发与白裙的裙摆在风中轻轻飘扬,画面充满静谧与动感的交织。她仰望着染上橙粉色霞光的天空,脸上露出若有所思、带着忧郁的神情,仿佛沉浸在回忆或思念之中。夕阳的余晖洒落在她身上与天台上,为画面增添一层柔和的金色光晕。她的站姿略带沉重,肩膀微微下垂,双手自然垂落或轻轻拽住裙摆,展现出内敛的情绪张力。天台背景简洁,设有低矮护栏、通风设备等结构,远处是模糊的城市天际线,进一步强化孤独氛围。风吹起她的发丝与裙摆,为静止的场景注入一丝淡淡的动态诗意。画风柔和写意,色彩渐变细腻,注重光影、空气感与人物情绪的融合,营造出宁静而感伤的黄昏场景。",
119
+ }
120
+
121
+
122
+ # 工具函数
123
+ def validate_dimensions(width: int, height: int) -> tuple[int, int]:
124
+ """验证并调整图片尺寸"""
125
+ width = max(MIN_IMAGE_SIZE, min(int(width), MAX_IMAGE_SIZE))
126
+ height = max(MIN_IMAGE_SIZE, min(int(height), MAX_IMAGE_SIZE))
127
+ width = (width // 32) * 32
128
+ height = (height // 32) * 32
129
+ return width, height
130
+
131
+
132
+ class PolishClient:
133
+ """提示词润色客户端"""
134
+
135
+ def __init__(self):
136
+ self.x_token = API_TOKEN
137
+ if not self.x_token:
138
+ raise ValueError("环境变量中未设置API_TOKEN")
139
+
140
+ self.url = "https://api.talesofai.cn/v3/gpt/dify/text-complete"
141
+ self.headers = {
142
+ "x-token": self.x_token,
143
+ "Content-Type": "application/json",
144
+ "x-nieta-app-version": "5.14.0",
145
+ "x-platform": "nieta-app/web",
146
+ }
147
+
148
+ async def polish_text(self, input_text: str) -> str:
149
+ """润色文本"""
150
+ payload = {
151
+ "query": "",
152
+ "response_mode": "blocking",
153
+ "preset_key": "latitude://28|live|running",
154
+ "inputs": {"query": input_text},
155
+ }
156
+
157
+ async with httpx.AsyncClient(timeout=HTTP_TIMEOUT) as client:
158
+ response = await client.post(self.url, headers=self.headers, json=payload)
159
+ if response.status_code == HTTP_STATUS_OK:
160
+ response_data = response.json()
161
+ polished_text = response_data.get("answer", input_text)
162
+ return polished_text.strip()
163
+ else:
164
+ logger.warning(f"润色API调用失败: {response.status_code}")
165
+ return input_text
166
+
167
+
168
+ async def polish_prompt(prompt: str) -> str:
169
+ """提示词润色函数 - 使用外部API润色"""
170
+ global polish_client
171
+ if polish_client is None:
172
+ polish_client = PolishClient()
173
+
174
+ logger.info(f"润色提示词: {prompt[:50]}...")
175
+ polished_prompt = await polish_client.polish_text(prompt)
176
+ logger.info("提示词润色完成")
177
+ return polished_prompt
178
+
179
+
180
+ # 核心类定义
181
  class ImageClient:
182
  """图像生成客户端"""
183
 
184
  def __init__(self) -> None:
185
+ self.x_token = API_TOKEN
186
  if not self.x_token:
187
  raise ValueError("环境变量中未设置API_TOKEN")
188
 
 
194
  self.polling_interval = 3.0
195
  self.default_headers = {
196
  "Content-Type": "application/json",
197
+ "x-platform": "nieta-app/web",
198
  "X-Token": self.x_token,
199
  }
200
 
 
201
  self._client_config = {
202
  "limits": HTTP_LIMITS,
203
  "timeout": HTTP_TIMEOUT,
204
  "headers": self.default_headers,
205
  }
206
 
 
207
  self._active_tasks = weakref.WeakSet()
208
 
209
  def _prepare_prompt_data(
 
245
  "seed": config.seed,
246
  "meta": {"entrance": "PICTURE,PURE"},
247
  "context_model_series": None,
248
+ "negative_freetext": "",
249
  "advanced_translator": config.use_polish,
250
  }
251
  if config.is_lumina:
 
263
  async def _poll_task_status(self, task_uuid: str) -> dict[str, Any]:
264
  """轮询任务状态 - 优化内存使用和连接管理"""
265
  status_url = self.lumina_task_status_url.format(task_uuid=task_uuid)
 
266
  poll_timeout = httpx.Timeout(connect=10.0, read=30.0, write=10.0, pool=5.0)
267
 
268
  try:
 
272
  for attempt in range(self.max_polling_attempts):
273
  try:
274
  response = await client.get(status_url)
 
275
  if response.status_code != HTTP_STATUS_OK:
276
  logger.warning(f"轮询失败 - 状态码: {response.status_code}")
277
  return {
 
279
  "error": f"获取任务状态失败: {response.status_code} - {response.text[:200]}",
280
  }
281
 
 
282
  try:
283
  result = response.json()
284
  except Exception as e:
 
289
  }
290
 
291
  task_status = result.get("task_status")
 
292
  if task_status == "SUCCESS":
293
  artifacts = result.get("artifacts", [])
294
  if artifacts and len(artifacts) > 0:
 
310
  return {"success": False, "error": "任务超时"}
311
 
312
  await asyncio.sleep(self.polling_interval)
 
 
313
  if attempt % 10 == 0 and attempt > 0:
314
  gc.collect()
315
  logger.debug(f"轮询第{attempt}次,执行内存清理")
316
 
317
  except asyncio.TimeoutError:
318
  logger.warning(f"轮询超时 - 尝试 {attempt}")
319
+ if attempt >= 3:
320
  return {
321
  "success": False,
322
  "error": "连接超时,请检查网络状况",
323
  }
324
+ await asyncio.sleep(self.polling_interval * 2)
325
  continue
326
  except Exception as e:
327
  logger.warning(f"轮询异常 - 尝试 {attempt}: {str(e)}")
 
351
  cfg: float,
352
  steps: int,
353
  model_name: str = "ep6",
354
+ use_polish: bool = False,
355
  ) -> tuple[str | None, str | None]:
356
  """生成图片"""
357
  try:
 
358
  model_path = MODEL_CONFIGS.get(model_name, MODEL_CONFIGS["ep6"])
 
 
359
  config = ImageGenerationConfig(
360
  prompts=self._prepare_prompt_data(prompt, negative_prompt),
361
  width=width,
362
  height=height,
363
  seed=seed,
364
+ use_polish=use_polish,
365
  is_lumina=True,
366
  lumina_config=LuminaConfig(model_name=model_path, cfg=cfg, step=steps),
367
  )
368
 
 
369
  async with httpx.AsyncClient(**self._client_config) as client:
370
  payload = self._build_payload(config)
371
  if DEBUG_MODE:
 
386
 
387
  if response.status_code == HTTP_STATUS_CENSORED:
388
  return None, "内容不合规"
 
 
389
  if response.status_code == 433:
390
  return None, "⏳ 服务器正忙,同时生成的图片数量已达上限,请稍后重试"
 
391
  if response.status_code != HTTP_STATUS_OK:
392
  return (
393
  None,
394
  f"API请求失败: {response.status_code} - {response.text[:200]}",
395
  )
396
 
 
397
  content = response.text.strip()
398
  task_uuid = content.replace('"', "")
399
 
 
403
  if not task_uuid:
404
  return None, f"未获取到任务ID,API响应: {response.text[:200]}"
405
 
 
406
  result = await self._poll_task_status(task_uuid)
407
  if result["success"]:
408
  return result["image_url"], None
 
413
  logger.error(f"生成图片异常: {str(e)}")
414
  return None, f"生成图片时发生错误: {str(e)}"
415
  finally:
 
416
  gc.collect()
417
 
418
  def cleanup(self):
419
  """清理资源"""
420
  try:
 
421
  self._active_tasks.clear()
422
  gc.collect()
423
  logger.info("ImageClient资源清理完成")
 
425
  logger.error(f"资源清理异常: {str(e)}")
426
 
427
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
428
  class MemoryMonitor:
429
  """内存监控器"""
430
 
 
453
  """内存监控循环"""
454
  while self.running:
455
  try:
 
456
  process = psutil.Process()
457
  memory_info = process.memory_info()
458
  memory_mb = memory_info.rss / 1024 / 1024
 
 
 
459
  memory_percent = process.memory_percent()
460
 
 
461
  if memory_percent > MEMORY_THRESHOLD_PERCENT:
462
  logger.warning(
463
  f"内存使用量过高: {memory_mb:.2f} MB ({memory_percent:.1f}%)"
464
  )
 
465
  gc.collect()
466
  logger.info("执行垃圾回收")
467
  else:
 
475
  time.sleep(MEMORY_CHECK_INTERVAL)
476
 
477
 
478
+ # 辅助函数
479
+ def get_image_client() -> ImageClient:
480
+ """获取图片生成客户端实例 - 单例模式"""
481
+ global image_client
482
+ if image_client is None:
483
+ try:
484
+ image_client = ImageClient()
485
+ logger.info("ImageClient初始化成功")
486
+ except Exception as e:
487
+ logger.error(f"ImageClient初始化失败: {e}")
488
+ raise
489
+ return image_client
490
 
491
 
 
492
  async def cleanup_resources():
493
  """清理应用资源"""
494
+ global image_client, memory_monitor, polish_client
 
495
  try:
 
496
  memory_monitor.stop_monitoring()
 
 
497
  if image_client is not None:
498
  image_client.cleanup()
499
  image_client = None
500
+ if polish_client is not None:
501
+ polish_client = None
502
  gc.collect()
 
 
503
  try:
504
  process = psutil.Process()
505
  memory_mb = process.memory_info().rss / 1024 / 1024
506
  logger.info(f"清理后内存使用: {memory_mb:.2f} MB")
507
  except Exception:
508
  pass
 
509
  logger.info("应用资源清理完成")
510
  except Exception as e:
511
  logger.error(f"资源清理异常: {str(e)}")
512
 
513
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
514
  async def infer(
515
  prompt_text,
516
+ use_polish_val,
517
  seed_val,
518
  randomize_seed_val,
519
  width_val,
 
523
  model_name_val,
524
  ):
525
  """推理函数 - 优化内存管理和错误处理"""
 
526
  try:
527
  logger.info(f"开始生成图像: {prompt_text[:50]}...")
 
 
528
  try:
529
  client = get_image_client()
530
  except Exception as e:
 
534
  if not prompt_text.strip():
535
  raise gr.Error("提示词不能为空")
536
 
537
+ final_prompt = prompt_text
538
+ if use_polish_val:
539
+ final_prompt = await polish_prompt(prompt_text)
540
+
541
+ # 为生成添加系统前缀,但不在UI显示
542
+ system_prefix = "You are an assistant designed to generate anime images with the highest degree of image-text alignment based on danbooru tags. <Prompt Start>"
543
+ generation_prompt = f"{system_prefix} {final_prompt}"
544
+
545
  current_seed = int(seed_val)
546
  if randomize_seed_val:
547
  current_seed = random.randint(0, MAX_SEED)
548
 
 
549
  width_val, height_val = validate_dimensions(width_val, height_val)
550
 
 
551
  if not (1.0 <= float(cfg_val) <= 20.0):
552
  raise gr.Error("CFG Scale 必须在 1.0 到 20.0 之间")
553
  if not (1 <= int(steps_val) <= 50):
554
  raise gr.Error("Steps 必须在 1 到 50 之间")
555
 
556
  image_url, error = await client.generate_image(
557
+ prompt=generation_prompt, # 使用包含系统前缀的提示词
558
  negative_prompt="",
559
  seed=current_seed,
560
  width=width_val,
 
562
  cfg=float(cfg_val),
563
  steps=int(steps_val),
564
  model_name=model_name_val,
565
+ use_polish=False,
566
  )
567
 
568
  if error:
 
570
  raise gr.Error(error)
571
 
572
  logger.info("图像生成成功")
573
+ return image_url, final_prompt, current_seed # 返回不带系统前缀的用户提示词
574
 
575
  except gr.Error:
 
576
  raise
577
  except Exception as e:
578
  logger.error(f"推理过程异常: {str(e)}")
579
  raise gr.Error(f"生成图像时发生意外错误: {str(e)}")
580
  finally:
 
581
  gc.collect()
582
 
583
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
584
  def setup_signal_handlers():
585
  """设置信号处理器以实现优雅关闭"""
586
  import signal
 
595
  logger.error(f"清理资源时出错: {str(e)}")
596
  logger.info("应用已安全关闭")
597
 
 
598
  signal.signal(signal.SIGINT, cleanup_handler)
599
  signal.signal(signal.SIGTERM, cleanup_handler)
 
 
600
  atexit.register(cleanup_handler)
601
 
602
 
603
+ # UI 构建
604
+ def build_ui():
605
+ """构建Gradio UI"""
606
+ with gr.Blocks(theme=gr.themes.Soft(), title="Lumina Image Playground") as demo:
607
+ gr.Markdown("<h1>🎨 Lumina Text-to-Image Playground | Lumina 文本生图工具</h1>")
608
+ gr.Markdown(
609
+ "Fine-tuned Lumina model specialized for anime/manga style generation! Supports Chinese, English, and Japanese prompts. Model under active development - more exciting features coming soon!\n 🌸 专为二次元风格优化的Lumina模型!支持中文、英文、日文三语提示词,让您的创意无界限!模型持续优化中,敬请期待。"
610
+ )
611
+
612
+ gr.HTML(f"""
613
+ <div style="display: flex; justify-content: flex-start; align-items: center; gap: 15px; margin-bottom: 20px; padding: 10px;">
614
+ <a href="{DISCORD_LINK}" target="_blank" style="text-decoration: none; color: #5865F2; font-weight: 500; display: inline-flex; align-items: center; gap: 5px;">
615
+ <img src="https://assets-global.website-files.com/6257adef93867e50d84d30e2/636e0a69f118df70ad7828d4_icon_clyde_blurple_RGB.svg" alt="Discord" style="height: 20px;">
616
+ Join Discord | 加入Discord
617
+ </a>
618
+ <a href="{APP_INDEX_LINK}" target="_blank" style="text-decoration: none; color: #333; font-weight: 500; display: inline-flex; align-items: center; gap: 5px;">
619
+ <img src="{APP_INDEX_ICON}" alt="App Index" style="height: 20px; border-radius: 3px;">
620
+ Nieta Home | 捏Ta主页
621
+ </a>
622
+ </div>
623
+ """)
624
+
625
+ with gr.Row(variant="panel"):
626
+ with gr.Column(scale=2): # Controls Panel
627
+ gr.Markdown("## ⚙️ Generation Controls | 生成控制")
628
+ prompt = gr.Textbox(
629
+ label="Prompt | 提示词",
630
+ lines=5,
631
+ placeholder="e.g., A majestic dragon soaring through a cyberpunk city skyline, neon lights reflecting off its scales, intricate details. | 例如:一条威武的巨龙翱翔在赛博朋克城市天际线,霓虹灯映照在它的鳞片上,细节精美。",
632
+ info="Describe the image you want to create. | 描述您想要创建的图像。",
633
+ )
634
+
635
+ use_polish = gr.Checkbox(
636
+ label="✨ Auto Polish Prompt | 自动润色提示词",
637
+ value=True,
638
+ info="Automatically optimize and enhance your prompt for better results. | 自动优化和增强您的提示词以获得更好的效果。",
639
+ )
640
+
641
+ run_button = gr.Button(
642
+ "🚀 Generate Image | 生成图像", variant="primary", scale=0
643
+ )
644
+
645
+ with gr.Accordion("🔧 Advanced Settings | 高级设置", open=False):
646
+ model_name = gr.Dropdown(
647
+ label="Model Version | 模型版本",
648
+ choices=list(MODEL_CONFIGS.keys()),
649
+ value="ep6",
650
+ info="Select the generation model. | 选择生成模型。",
651
+ )
652
+ with gr.Row():
653
+ cfg = gr.Slider(
654
+ label="CFG Scale | CFG缩放",
655
+ minimum=1.0,
656
+ maximum=20.0,
657
+ step=0.1,
658
+ value=5.5,
659
+ info="Guidance strength. Higher values adhere more to prompt. | 引导强度,更高的值更贴近提示词。",
660
+ )
661
+ steps = gr.Slider(
662
+ label="Sampling Steps | 采样步数",
663
+ minimum=1,
664
+ maximum=50,
665
+ step=1,
666
+ value=30,
667
+ info="Number of steps. More steps can improve quality but take longer. | 步数,更多步数可提高质量但耗时更长。",
668
+ )
669
+
670
+ with gr.Row():
671
+ width = gr.Slider(
672
+ label="Width | 宽度",
673
+ minimum=MIN_IMAGE_SIZE,
674
+ maximum=MAX_IMAGE_SIZE,
675
+ step=32,
676
+ value=1024,
677
+ )
678
+ height = gr.Slider(
679
+ label="Height | 高度",
680
+ minimum=MIN_IMAGE_SIZE,
681
+ maximum=MAX_IMAGE_SIZE,
682
+ step=32,
683
+ value=1024,
684
+ )
685
+
686
+ with gr.Row():
687
+ seed = gr.Slider(
688
+ label="Seed | 种子",
689
+ minimum=0,
690
+ maximum=MAX_SEED,
691
+ step=1,
692
+ value=random.randint(0, MAX_SEED),
693
+ )
694
+ randomize_seed = gr.Checkbox(
695
+ label="Randomize Seed | 随机种子",
696
+ value=True,
697
+ info="Use a new random seed for each generation if checked. | 勾选后每次生成使用新的随机种子。",
698
+ )
699
+
700
+ with gr.Group():
701
+ gr.Markdown("### ✨ Example Prompts | 示例提示词")
702
+ for i, title in enumerate(example_titles):
703
+ btn = gr.Button(title)
704
+ btn.click(lambda t=title: full_prompts[t], outputs=[prompt])
705
+
706
+ with gr.Column(scale=3): # Output Panel
707
+ gr.Markdown("## 🖼️ Generated Image | 生成图像")
708
+ result_image = gr.Image(
709
+ label="Output Image | 输出图像",
710
+ show_label=False,
711
+ type="filepath",
712
+ height=600,
713
+ show_download_button=True,
714
+ interactive=False,
715
+ elem_id="result_image_display",
716
+ )
717
+ used_prompt_info = gr.Textbox(
718
+ label="Used Prompt | 使用的提示词",
719
+ interactive=False,
720
+ lines=3,
721
+ placeholder="The actual prompt used for generation will appear here. | 生成时实际使用的提示词将显示在此处。",
722
+ )
723
+ generated_seed_info = gr.Textbox(
724
+ label="Seed Used | 使用的种子",
725
+ interactive=False,
726
+ placeholder="The seed for the generated image will appear here. | 生成图像所使用的种子值将显示在此处。",
727
+ )
728
+
729
+ # 事件处理
730
+ inputs_list = [
731
+ prompt,
732
+ use_polish,
733
+ seed,
734
+ randomize_seed,
735
+ width,
736
+ height,
737
+ cfg,
738
+ steps,
739
+ model_name,
740
+ ]
741
+ outputs_list = [result_image, used_prompt_info, generated_seed_info]
742
+
743
+ run_button.click(
744
+ fn=infer,
745
+ inputs=inputs_list,
746
+ outputs=outputs_list,
747
+ api_name="generate_image",
748
+ )
749
+ prompt.submit(
750
+ fn=infer,
751
+ inputs=inputs_list,
752
+ outputs=outputs_list,
753
+ api_name="generate_image_submit",
754
+ )
755
+
756
+ return demo
757
+
758
+
759
+ # 主函数
760
+ def main():
761
+ """主函数"""
762
+ global memory_monitor
763
+
764
  try:
765
  # 设置信号处理器
766
  setup_signal_handlers()
767
 
768
  # 启动内存监控
769
+ memory_monitor = MemoryMonitor()
770
  memory_monitor.start_monitoring()
771
 
772
  if DEBUG_MODE:
 
800
  pass
801
 
802
  logger.info("启动Gradio应用...")
803
+ demo = build_ui()
804
  demo.launch(
805
  debug=DEBUG_MODE,
806
  show_error=True,
807
+ server_name="0.0.0.0",
808
  server_port=7860,
809
  share=False,
810
+ max_threads=40,
811
+ favicon_path=None,
812
  )
813
 
814
  except KeyboardInterrupt:
 
823
  except Exception as e:
824
  logger.error(f"最终清理时出错: {str(e)}")
825
  logger.info("应用已退出")
826
+
827
+
828
+ if __name__ == "__main__":
829
+ main()