dx8152 commited on
Commit
c8ffebf
·
verified ·
1 Parent(s): 9618b22

Upload 47 files

Browse files
Files changed (48) hide show
  1. .gitattributes +9 -0
  2. 26-5-10-API-Studio/API/.env +12 -0
  3. 26-5-10-API-Studio/main.py +1764 -0
  4. 26-5-10-API-Studio/packages/annotated_doc-0.0.4-py3-none-any.whl +0 -0
  5. 26-5-10-API-Studio/packages/annotated_types-0.7.0-py3-none-any.whl +0 -0
  6. 26-5-10-API-Studio/packages/anyio-4.13.0-py3-none-any.whl +3 -0
  7. 26-5-10-API-Studio/packages/certifi-2026.4.22-py3-none-any.whl +3 -0
  8. 26-5-10-API-Studio/packages/charset_normalizer-3.4.7-cp314-cp314-win_amd64.whl +3 -0
  9. 26-5-10-API-Studio/packages/click-8.3.3-py3-none-any.whl +3 -0
  10. 26-5-10-API-Studio/packages/colorama-0.4.6-py2.py3-none-any.whl +0 -0
  11. 26-5-10-API-Studio/packages/fastapi-0.136.1-py3-none-any.whl +3 -0
  12. 26-5-10-API-Studio/packages/h11-0.16.0-py3-none-any.whl +0 -0
  13. 26-5-10-API-Studio/packages/httpcore-1.0.9-py3-none-any.whl +0 -0
  14. 26-5-10-API-Studio/packages/httpx-0.28.1-py3-none-any.whl +0 -0
  15. 26-5-10-API-Studio/packages/idna-3.13-py3-none-any.whl +0 -0
  16. 26-5-10-API-Studio/packages/pillow-12.2.0-cp314-cp314-win_amd64.whl +3 -0
  17. 26-5-10-API-Studio/packages/pydantic-2.13.4-py3-none-any.whl +3 -0
  18. 26-5-10-API-Studio/packages/pydantic_core-2.46.4-cp314-cp314-win_amd64.whl +3 -0
  19. 26-5-10-API-Studio/packages/python_multipart-0.0.27-py3-none-any.whl +0 -0
  20. 26-5-10-API-Studio/packages/requests-2.33.1-py3-none-any.whl +0 -0
  21. 26-5-10-API-Studio/packages/starlette-1.0.0-py3-none-any.whl +0 -0
  22. 26-5-10-API-Studio/packages/typing_extensions-4.15.0-py3-none-any.whl +0 -0
  23. 26-5-10-API-Studio/packages/typing_inspection-0.4.2-py3-none-any.whl +0 -0
  24. 26-5-10-API-Studio/packages/urllib3-2.7.0-py3-none-any.whl +3 -0
  25. 26-5-10-API-Studio/packages/uvicorn-0.46.0-py3-none-any.whl +0 -0
  26. 26-5-10-API-Studio/readme.txt +33 -0
  27. 26-5-10-API-Studio/requirements.txt +7 -0
  28. 26-5-10-API-Studio/static/angle.html +1270 -0
  29. 26-5-10-API-Studio/static/canvas.html +0 -0
  30. 26-5-10-API-Studio/static/enhance.html +881 -0
  31. 26-5-10-API-Studio/static/gpt-chat.html +538 -0
  32. 26-5-10-API-Studio/static/index.html +663 -0
  33. 26-5-10-API-Studio/static/klein.html +800 -0
  34. 26-5-10-API-Studio/static/login.html +292 -0
  35. 26-5-10-API-Studio/static/logo.png +0 -0
  36. 26-5-10-API-Studio/static/modelscope.gif +0 -0
  37. 26-5-10-API-Studio/static/online.html +329 -0
  38. 26-5-10-API-Studio/static/theme.css +1591 -0
  39. 26-5-10-API-Studio/static/theme.js +41 -0
  40. 26-5-10-API-Studio/static/zimage.html +586 -0
  41. 26-5-10-API-Studio/workflows/2511.json +264 -0
  42. 26-5-10-API-Studio/workflows/Flux2-Klein.json +525 -0
  43. 26-5-10-API-Studio/workflows/Z-Image-Enhance.json +383 -0
  44. 26-5-10-API-Studio/workflows/Z-Image.json +176 -0
  45. 26-5-10-API-Studio/workflows/upscale.json +90 -0
  46. 26-5-10-API-Studio/启动服务.bat +11 -0
  47. 26-5-10-API-Studio/安装依赖.bat +37 -0
  48. 26-5-10-API-Studio/运行说明.txt +28 -0
.gitattributes CHANGED
@@ -35,3 +35,12 @@ saved_model/**/* filter=lfs diff=lfs merge=lfs -text
35
  *tfevents* filter=lfs diff=lfs merge=lfs -text
36
  static/app_icon.ico filter=lfs diff=lfs merge=lfs -text
37
  runtime/ffmpeg/ffmpeg.exe filter=lfs diff=lfs merge=lfs -text
 
 
 
 
 
 
 
 
 
 
35
  *tfevents* filter=lfs diff=lfs merge=lfs -text
36
  static/app_icon.ico filter=lfs diff=lfs merge=lfs -text
37
  runtime/ffmpeg/ffmpeg.exe filter=lfs diff=lfs merge=lfs -text
38
+ 26-5-10-API-Studio/packages/anyio-4.13.0-py3-none-any.whl filter=lfs diff=lfs merge=lfs -text
39
+ 26-5-10-API-Studio/packages/certifi-2026.4.22-py3-none-any.whl filter=lfs diff=lfs merge=lfs -text
40
+ 26-5-10-API-Studio/packages/charset_normalizer-3.4.7-cp314-cp314-win_amd64.whl filter=lfs diff=lfs merge=lfs -text
41
+ 26-5-10-API-Studio/packages/click-8.3.3-py3-none-any.whl filter=lfs diff=lfs merge=lfs -text
42
+ 26-5-10-API-Studio/packages/fastapi-0.136.1-py3-none-any.whl filter=lfs diff=lfs merge=lfs -text
43
+ 26-5-10-API-Studio/packages/pillow-12.2.0-cp314-cp314-win_amd64.whl filter=lfs diff=lfs merge=lfs -text
44
+ 26-5-10-API-Studio/packages/pydantic_core-2.46.4-cp314-cp314-win_amd64.whl filter=lfs diff=lfs merge=lfs -text
45
+ 26-5-10-API-Studio/packages/pydantic-2.13.4-py3-none-any.whl filter=lfs diff=lfs merge=lfs -text
46
+ 26-5-10-API-Studio/packages/urllib3-2.7.0-py3-none-any.whl filter=lfs diff=lfs merge=lfs -text
26-5-10-API-Studio/API/.env ADDED
@@ -0,0 +1,12 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ COMFLY_BASE_URL=https://ai.comfly.chat
2
+ COMFLY_API_KEY=sk-
3
+ MODELSCOPE_API_KEY=ms-
4
+ COMFYUI_INSTANCES=127.0.0.1:8188,127.0.0.1:4090
5
+
6
+ SYSTEM_PROMPT=You are a helpful assistant.
7
+ MAX_HISTORY_MESSAGES=30
8
+ REQUEST_TIMEOUT=120
9
+ IMAGE_POLL_INTERVAL=2
10
+ CHAT_MODELS=gpt-5.5
11
+ IMAGE_MODELS=gpt-image-2-all,nano-banana-pro-2k
12
+ MODELSCOPE_CHAT_MODELS=Qwen/Qwen3-235B-A22B,MiniMax/MiniMax-M2.7:MiniMax
26-5-10-API-Studio/main.py ADDED
@@ -0,0 +1,1764 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import json
2
+ import uuid
3
+ import base64
4
+ import urllib.request
5
+ import urllib.parse
6
+ import urllib.error
7
+ import os
8
+ import re
9
+ import random
10
+ import time
11
+ import shutil
12
+ import asyncio
13
+ import requests
14
+ from typing import List, Dict, Any, Optional
15
+ from threading import Lock
16
+ import httpx
17
+ from PIL import Image
18
+ from fastapi import FastAPI, HTTPException, WebSocket, WebSocketDisconnect, UploadFile, File, Header, Request
19
+ from fastapi.staticfiles import StaticFiles
20
+ from fastapi.responses import FileResponse, Response, StreamingResponse
21
+ from pydantic import BaseModel, Field
22
+ from fastapi.middleware.cors import CORSMiddleware
23
+
24
+ app = FastAPI()
25
+
26
+ app.add_middleware(
27
+ CORSMiddleware,
28
+ allow_origins=["*"],
29
+ allow_methods=["*"],
30
+ allow_headers=["*"],
31
+ )
32
+
33
+ # --- WebSocket 状态管理器 ---
34
+ class ConnectionManager:
35
+ def __init__(self):
36
+ self.active_connections: List[WebSocket] = []
37
+ self.user_connections: Dict[str, WebSocket] = {}
38
+
39
+ async def connect(self, websocket: WebSocket, client_id: str = None):
40
+ await websocket.accept()
41
+ self.active_connections.append(websocket)
42
+ if client_id:
43
+ self.user_connections[client_id] = websocket
44
+ print(f"WS Connected. Total: {len(self.active_connections)}")
45
+ await self.broadcast_count()
46
+
47
+ async def disconnect(self, websocket: WebSocket, client_id: str = None):
48
+ if websocket in self.active_connections:
49
+ self.active_connections.remove(websocket)
50
+ if client_id and client_id in self.user_connections:
51
+ del self.user_connections[client_id]
52
+ print(f"WS Disconnected. Total: {len(self.active_connections)}")
53
+ await self.broadcast_count()
54
+
55
+ async def broadcast_count(self):
56
+ count = len(self.active_connections)
57
+ data = json.dumps({"type": "stats", "online_count": count})
58
+ for connection in self.active_connections[:]:
59
+ try:
60
+ await connection.send_text(data)
61
+ except Exception as e:
62
+ print(f"Broadcast error: {e}")
63
+ self.active_connections.remove(connection)
64
+
65
+ async def broadcast_new_image(self, image_data: dict):
66
+ data = json.dumps({"type": "new_image", "data": image_data})
67
+ for connection in self.active_connections[:]:
68
+ try:
69
+ await connection.send_text(data)
70
+ except Exception as e:
71
+ print(f"Broadcast image error: {e}")
72
+ self.active_connections.remove(connection)
73
+
74
+ async def send_personal_message(self, message: dict, client_id: str):
75
+ ws = self.user_connections.get(client_id)
76
+ if ws:
77
+ try:
78
+ await ws.send_text(json.dumps(message))
79
+ except Exception as e:
80
+ print(f"Personal message error for {client_id}: {e}")
81
+
82
+ manager = ConnectionManager()
83
+ GLOBAL_LOOP = None
84
+
85
+ @app.on_event("startup")
86
+ async def startup_event():
87
+ global GLOBAL_LOOP
88
+ GLOBAL_LOOP = asyncio.get_running_loop()
89
+
90
+ @app.websocket("/ws/stats")
91
+ async def websocket_endpoint(websocket: WebSocket, client_id: str = None):
92
+ await manager.connect(websocket, client_id)
93
+ try:
94
+ while True:
95
+ data = await websocket.receive_text()
96
+ if data == "ping":
97
+ await websocket.send_text(json.dumps({"type": "pong"}))
98
+ except WebSocketDisconnect:
99
+ await manager.disconnect(websocket, client_id)
100
+ except Exception as e:
101
+ print(f"WS Error: {e}")
102
+ await manager.disconnect(websocket, client_id)
103
+
104
+ # --- 配置区域 ---
105
+
106
+ CLIENT_ID = str(uuid.uuid4())
107
+ BASE_DIR = os.path.dirname(os.path.abspath(__file__))
108
+ WORKFLOW_DIR = os.path.join(BASE_DIR, "workflows")
109
+ WORKFLOW_PATH = os.path.join(WORKFLOW_DIR, "Z-Image.json")
110
+ STATIC_DIR = os.path.join(BASE_DIR, "static")
111
+ OUTPUT_DIR = os.path.join(BASE_DIR, "output")
112
+ HISTORY_FILE = os.path.join(BASE_DIR, "history.json")
113
+ API_ENV_FILE = os.path.join(BASE_DIR, "API", ".env")
114
+ DATA_DIR = os.path.join(BASE_DIR, "data")
115
+ CONVERSATION_DIR = os.path.join(DATA_DIR, "conversations")
116
+ CANVAS_DIR = os.path.join(DATA_DIR, "canvases")
117
+ GLOBAL_CONFIG_FILE = os.path.join(BASE_DIR, "global_config.json")
118
+ CANVAS_TRASH_RETENTION_MS = 30 * 24 * 60 * 60 * 1000
119
+
120
+ QUEUE = []
121
+ QUEUE_LOCK = Lock()
122
+ HISTORY_LOCK = Lock()
123
+ GLOBAL_CONFIG_LOCK = Lock()
124
+ CONVERSATION_LOCK = Lock()
125
+ CANVAS_LOCK = Lock()
126
+ LOAD_LOCK = Lock()
127
+ NEXT_TASK_ID = 1
128
+
129
+ def load_env_file():
130
+ if not os.path.exists(API_ENV_FILE):
131
+ return
132
+ try:
133
+ with open(API_ENV_FILE, 'r', encoding='utf-8-sig') as f:
134
+ for raw_line in f.read().splitlines():
135
+ line = raw_line.strip()
136
+ if not line or line.startswith("#") or "=" not in line:
137
+ continue
138
+ key, value = line.split("=", 1)
139
+ key = key.strip()
140
+ value = value.strip().strip('"').strip("'")
141
+ os.environ.setdefault(key, value)
142
+ except Exception as e:
143
+ print(f"加�� API/.env 失败: {e}")
144
+
145
+ load_env_file()
146
+
147
+ COMFYUI_INSTANCES = [s.strip() for s in os.getenv("COMFYUI_INSTANCES", "127.0.0.1:8188").split(",") if s.strip()]
148
+ COMFYUI_ADDRESS = COMFYUI_INSTANCES[0]
149
+
150
+ AI_BASE_URL = os.getenv("COMFLY_BASE_URL", "https://ai.comfly.chat").rstrip("/")
151
+ AI_API_KEY = os.getenv("COMFLY_API_KEY", "")
152
+ MODELSCOPE_API_KEY = os.getenv("MODELSCOPE_API_KEY", "")
153
+ MODELSCOPE_CHAT_BASE_URL = "https://api-inference.modelscope.cn/v1"
154
+ MODELSCOPE_CHAT_MODELS = [m.strip() for m in os.getenv("MODELSCOPE_CHAT_MODELS", "Qwen/Qwen3-235B-A22B,MiniMax/MiniMax-M2.7:MiniMax").split(",") if m.strip()]
155
+ CHAT_MODEL = os.getenv("CHAT_MODEL", "gpt-4o-mini")
156
+ IMAGE_MODEL = os.getenv("IMAGE_MODEL", "gpt-image-1")
157
+ SYSTEM_PROMPT = os.getenv("SYSTEM_PROMPT", "You are a helpful assistant.")
158
+ MAX_HISTORY_MESSAGES = int(os.getenv("MAX_HISTORY_MESSAGES", "30"))
159
+ AI_REQUEST_TIMEOUT = float(os.getenv("REQUEST_TIMEOUT", "120"))
160
+ IMAGE_POLL_INTERVAL = float(os.getenv("IMAGE_POLL_INTERVAL", "2"))
161
+
162
+ def model_list(env_name, primary, defaults):
163
+ configured = os.getenv(env_name, "")
164
+ configured_values = [item.strip() for item in configured.split(",") if item.strip()]
165
+ values = configured_values or [primary, *defaults]
166
+ deduped = []
167
+ for value in values:
168
+ if value and value not in deduped:
169
+ deduped.append(value)
170
+ return deduped
171
+
172
+ CHAT_MODELS = model_list("CHAT_MODELS", CHAT_MODEL, ["gpt-4o-mini", "gemini-3.1-flash-image-preview-2k"])
173
+ IMAGE_MODELS = model_list("IMAGE_MODELS", IMAGE_MODEL, ["gpt-image-2-all", "nano-banana"])
174
+
175
+ BACKEND_LOCAL_LOAD = {addr: 0 for addr in COMFYUI_INSTANCES}
176
+
177
+ os.makedirs(OUTPUT_DIR, exist_ok=True)
178
+ os.makedirs(STATIC_DIR, exist_ok=True)
179
+ os.makedirs(WORKFLOW_DIR, exist_ok=True)
180
+ os.makedirs(CONVERSATION_DIR, exist_ok=True)
181
+ os.makedirs(CANVAS_DIR, exist_ok=True)
182
+
183
+ app.mount("/static", StaticFiles(directory=STATIC_DIR), name="static")
184
+ app.mount("/output", StaticFiles(directory=OUTPUT_DIR), name="output")
185
+
186
+ # --- Pydantic 模型 ---
187
+
188
+ class GenerateRequest(BaseModel):
189
+ prompt: str = ""
190
+ width: int = 1024
191
+ height: int = 1024
192
+ workflow_json: str = "Z-Image.json"
193
+ params: Dict[str, Any] = {}
194
+ type: str = "zimage"
195
+ client_id: str = ""
196
+ convert_to_jpg: bool = False
197
+
198
+ class DeleteHistoryRequest(BaseModel):
199
+ timestamp: float
200
+
201
+ class TokenRequest(BaseModel):
202
+ token: str
203
+
204
+ class CloudGenRequest(BaseModel):
205
+ prompt: str
206
+ api_key: str = ""
207
+ resolution: str = "1024*1024"
208
+ type: str = "zimage"
209
+ image_urls: List[str] = []
210
+ client_id: Optional[str] = None
211
+
212
+ class CloudPollRequest(BaseModel):
213
+ task_id: str
214
+ api_key: str = ""
215
+ client_id: Optional[str] = None
216
+
217
+ class AIReference(BaseModel):
218
+ url: str = ""
219
+ name: str = ""
220
+
221
+ class OnlineImageRequest(BaseModel):
222
+ prompt: str = Field(min_length=1, max_length=4000)
223
+ model: str = ""
224
+ size: str = "1024x1024"
225
+ quality: str = "auto"
226
+ reference_images: List[AIReference] = []
227
+
228
+ class ChatRequest(BaseModel):
229
+ conversation_id: str = ""
230
+ message: str = Field(min_length=1, max_length=20000)
231
+ model: str = ""
232
+ image_model: str = ""
233
+ mode: str = "chat"
234
+ size: str = "1024x1024"
235
+ quality: str = "auto"
236
+ reference_images: List[AIReference] = []
237
+ provider: str = "comfly"
238
+ ms_model: str = ""
239
+
240
+ class MsGenerateRequest(BaseModel):
241
+ prompt: str
242
+ model: str = "black-forest-labs/FLUX.2-klein-9B"
243
+ image_urls: List[str] = []
244
+ width: int = 0
245
+ height: int = 0
246
+ loras: Optional[Any] = None
247
+ client_id: Optional[str] = None
248
+
249
+ class CanvasLLMRequest(BaseModel):
250
+ message: str = Field(min_length=1, max_length=20000)
251
+ system_prompt: str = "You are a helpful assistant."
252
+ model: str = ""
253
+ messages: List[Dict[str, str]] = []
254
+ provider: str = "comfly"
255
+ ms_model: str = ""
256
+
257
+ class ConversationCreateRequest(BaseModel):
258
+ title: str = "新对话"
259
+
260
+ class CanvasCreateRequest(BaseModel):
261
+ title: str = "未命名画布"
262
+ icon: str = "🧩"
263
+
264
+ class CanvasSaveRequest(BaseModel):
265
+ title: str = "未命名画布"
266
+ icon: str = "🧩"
267
+ nodes: List[Dict[str, Any]] = []
268
+ connections: List[Dict[str, Any]] = []
269
+ viewport: Dict[str, Any] = {}
270
+
271
+ # --- 负载均衡 ---
272
+
273
+ def check_images_exist(backend_addr, images):
274
+ if not images: return True
275
+ for img in images:
276
+ try:
277
+ url = f"http://{backend_addr}/view?filename={urllib.parse.quote(img)}&type=input"
278
+ r = requests.get(url, stream=True, timeout=0.5)
279
+ r.close()
280
+ if r.status_code != 200: return False
281
+ except: return False
282
+ return True
283
+
284
+ def get_best_backend(required_images: List[str] = None):
285
+ best_backend = COMFYUI_INSTANCES[0]
286
+ min_queue_size = float('inf')
287
+ candidates_with_images = []
288
+ candidates_others = []
289
+ backend_stats = {}
290
+
291
+ for addr in COMFYUI_INSTANCES:
292
+ try:
293
+ with urllib.request.urlopen(f"http://{addr}/queue", timeout=1) as response:
294
+ data = json.loads(response.read())
295
+ remote_load = len(data.get('queue_running', [])) + len(data.get('queue_pending', []))
296
+ with LOAD_LOCK:
297
+ local_load = BACKEND_LOCAL_LOAD.get(addr, 0)
298
+ effective_load = max(remote_load, local_load)
299
+ has_images = check_images_exist(addr, required_images)
300
+ backend_stats[addr] = {"load": effective_load, "has_images": has_images}
301
+ if has_images:
302
+ candidates_with_images.append(addr)
303
+ else:
304
+ candidates_others.append(addr)
305
+ except Exception as e:
306
+ print(f"Backend {addr} unreachable: {e}")
307
+ continue
308
+
309
+ target_candidates = candidates_with_images if candidates_with_images else candidates_others
310
+ if not target_candidates:
311
+ if candidates_others:
312
+ target_candidates = candidates_others
313
+ else:
314
+ return COMFYUI_INSTANCES[0]
315
+
316
+ for addr in target_candidates:
317
+ load = backend_stats[addr]["load"]
318
+ if load < min_queue_size:
319
+ min_queue_size = load
320
+ best_backend = addr
321
+
322
+ return best_backend
323
+
324
+ # --- 辅助工具 ---
325
+
326
+ def download_image(comfy_address, comfy_url_path, prefix="studio_"):
327
+ filename = f"{prefix}{uuid.uuid4().hex[:10]}.png"
328
+ local_path = os.path.join(OUTPUT_DIR, filename)
329
+ full_url = f"http://{comfy_address}{comfy_url_path}"
330
+ try:
331
+ with urllib.request.urlopen(full_url) as response, open(local_path, 'wb') as out_file:
332
+ shutil.copyfileobj(response, out_file)
333
+ return f"/output/{filename}"
334
+ except Exception as e:
335
+ print(f"下载图片失败: {e}")
336
+ if comfy_url_path.startswith("/view"):
337
+ return comfy_url_path.replace("/view", "/api/view", 1)
338
+ return full_url
339
+
340
+ def save_to_history(record):
341
+ with HISTORY_LOCK:
342
+ history = []
343
+ if os.path.exists(HISTORY_FILE):
344
+ try:
345
+ with open(HISTORY_FILE, 'r', encoding='utf-8') as f:
346
+ history = json.load(f)
347
+ except: pass
348
+ if "timestamp" not in record:
349
+ record["timestamp"] = time.time()
350
+ history.insert(0, record)
351
+ with open(HISTORY_FILE, 'w', encoding='utf-8') as f:
352
+ json.dump(history[:5000], f, ensure_ascii=False, indent=4)
353
+
354
+ def get_comfy_history(comfy_address, prompt_id):
355
+ try:
356
+ with urllib.request.urlopen(f"http://{comfy_address}/history/{prompt_id}") as response:
357
+ return json.loads(response.read())
358
+ except Exception as e:
359
+ return {}
360
+
361
+ def safe_user_id(user_id, request: Request):
362
+ candidate = (user_id or "").strip()
363
+ if not candidate and request.client:
364
+ candidate = f"ip-{request.client.host}"
365
+ if not candidate:
366
+ candidate = "anonymous"
367
+ candidate = re.sub(r"[^a-zA-Z0-9_.-]", "-", candidate)[:80].strip(".-")
368
+ return candidate or "anonymous"
369
+
370
+ def user_dir(user_id):
371
+ path = os.path.join(CONVERSATION_DIR, user_id)
372
+ os.makedirs(path, exist_ok=True)
373
+ return path
374
+
375
+ def conversation_path(user_id, conversation_id):
376
+ cleaned = re.sub(r"[^a-zA-Z0-9_-]", "", conversation_id or "")
377
+ if not cleaned:
378
+ raise HTTPException(status_code=400, detail="无效的对话 ID")
379
+ return os.path.join(user_dir(user_id), f"{cleaned}.json")
380
+
381
+ def now_ms():
382
+ return int(time.time() * 1000)
383
+
384
+ def save_conversation(user_id, conversation):
385
+ with CONVERSATION_LOCK:
386
+ path = conversation_path(user_id, conversation["id"])
387
+ with open(path, 'w', encoding='utf-8') as f:
388
+ json.dump(conversation, f, ensure_ascii=False, indent=2)
389
+
390
+ def new_conversation(user_id, title="新对话"):
391
+ timestamp = now_ms()
392
+ conversation = {
393
+ "id": uuid.uuid4().hex,
394
+ "title": (title or "新对话")[:80],
395
+ "created_at": timestamp,
396
+ "updated_at": timestamp,
397
+ "messages": [],
398
+ }
399
+ save_conversation(user_id, conversation)
400
+ return conversation
401
+
402
+ def load_conversation(user_id, conversation_id):
403
+ path = conversation_path(user_id, conversation_id)
404
+ if not os.path.exists(path):
405
+ raise HTTPException(status_code=404, detail="对话不存在")
406
+ with open(path, 'r', encoding='utf-8') as f:
407
+ return json.load(f)
408
+
409
+ def list_conversations(user_id):
410
+ records = []
411
+ for filename in os.listdir(user_dir(user_id)):
412
+ if not filename.endswith(".json"):
413
+ continue
414
+ path = os.path.join(user_dir(user_id), filename)
415
+ try:
416
+ with open(path, 'r', encoding='utf-8') as f:
417
+ data = json.load(f)
418
+ except Exception:
419
+ continue
420
+ messages = data.get("messages", [])
421
+ last_message = next((m for m in reversed(messages) if m.get("role") != "system"), None)
422
+ records.append({
423
+ "id": data.get("id"),
424
+ "title": data.get("title", "新对话"),
425
+ "created_at": data.get("created_at", 0),
426
+ "updated_at": data.get("updated_at", 0),
427
+ "last_message": (last_message or {}).get("content", ""),
428
+ })
429
+ return sorted(records, key=lambda item: item["updated_at"], reverse=True)
430
+
431
+ def canvas_path(canvas_id):
432
+ cleaned = re.sub(r"[^a-zA-Z0-9_-]", "", canvas_id or "")
433
+ if not cleaned:
434
+ raise HTTPException(status_code=400, detail="无效的画布 ID")
435
+ return os.path.join(CANVAS_DIR, f"{cleaned}.json")
436
+
437
+ def save_canvas(canvas):
438
+ canvas["updated_at"] = now_ms()
439
+ with CANVAS_LOCK:
440
+ with open(canvas_path(canvas["id"]), 'w', encoding='utf-8') as f:
441
+ json.dump(canvas, f, ensure_ascii=False, indent=2)
442
+
443
+ def new_canvas(title="未命名画布", icon="layers"):
444
+ timestamp = now_ms()
445
+ canvas = {
446
+ "id": uuid.uuid4().hex,
447
+ "title": (title or "未命名画布")[:80],
448
+ "icon": (icon or "🧩")[:4],
449
+ "created_at": timestamp,
450
+ "updated_at": timestamp,
451
+ "nodes": [],
452
+ "connections": [],
453
+ "viewport": {"x": 0, "y": 0, "scale": 1},
454
+ }
455
+ save_canvas(canvas)
456
+ return canvas
457
+
458
+ def load_canvas(canvas_id):
459
+ path = canvas_path(canvas_id)
460
+ if not os.path.exists(path):
461
+ raise HTTPException(status_code=404, detail="画布不存在")
462
+ with open(path, 'r', encoding='utf-8') as f:
463
+ canvas = json.load(f)
464
+ if canvas.get("deleted_at"):
465
+ raise HTTPException(status_code=404, detail="画布已在回收站")
466
+ return canvas
467
+
468
+ def load_canvas_any(canvas_id):
469
+ path = canvas_path(canvas_id)
470
+ if not os.path.exists(path):
471
+ raise HTTPException(status_code=404, detail="画布不存在")
472
+ with open(path, 'r', encoding='utf-8') as f:
473
+ return json.load(f)
474
+
475
+ def canvas_record(data):
476
+ return {
477
+ "id": data.get("id"),
478
+ "title": data.get("title", "未命名画布"),
479
+ "icon": data.get("icon", "🧩"),
480
+ "created_at": data.get("created_at", 0),
481
+ "updated_at": data.get("updated_at", 0),
482
+ "deleted_at": data.get("deleted_at", 0),
483
+ "node_count": len(data.get("nodes", [])),
484
+ }
485
+
486
+ def cleanup_expired_canvas_trash():
487
+ cutoff = now_ms() - CANVAS_TRASH_RETENTION_MS
488
+ with CANVAS_LOCK:
489
+ for filename in os.listdir(CANVAS_DIR):
490
+ if not filename.endswith(".json"):
491
+ continue
492
+ path = os.path.join(CANVAS_DIR, filename)
493
+ try:
494
+ with open(path, 'r', encoding='utf-8') as f:
495
+ data = json.load(f)
496
+ deleted_at = int(data.get("deleted_at") or 0)
497
+ if deleted_at and deleted_at < cutoff:
498
+ os.remove(path)
499
+ except Exception:
500
+ continue
501
+
502
+ def iter_canvas_records(include_deleted=False):
503
+ cleanup_expired_canvas_trash()
504
+ records = []
505
+ for filename in os.listdir(CANVAS_DIR):
506
+ if not filename.endswith(".json"):
507
+ continue
508
+ try:
509
+ with open(os.path.join(CANVAS_DIR, filename), 'r', encoding='utf-8') as f:
510
+ data = json.load(f)
511
+ except Exception:
512
+ continue
513
+ is_deleted = bool(data.get("deleted_at"))
514
+ if include_deleted != is_deleted:
515
+ continue
516
+ records.append(canvas_record(data))
517
+ return records
518
+
519
+ def list_canvases():
520
+ records = iter_canvas_records(include_deleted=False)
521
+ return sorted(records, key=lambda item: item["updated_at"], reverse=True)
522
+
523
+ def list_deleted_canvases():
524
+ records = iter_canvas_records(include_deleted=True)
525
+ return sorted(records, key=lambda item: item["deleted_at"], reverse=True)
526
+
527
+ def display_title(text):
528
+ title = re.sub(r"\s+", " ", text or "").strip()
529
+ return title[:24] or "新对话"
530
+
531
+ def resolve_chat_provider(provider: str, model: str, ms_model: str):
532
+ if provider == "modelscope":
533
+ if not MODELSCOPE_API_KEY:
534
+ raise HTTPException(status_code=400, detail="未配置 MODELSCOPE_API_KEY,请在 API/.env 中填写。")
535
+ base = MODELSCOPE_CHAT_BASE_URL
536
+ hdrs = {"Authorization": f"Bearer {MODELSCOPE_API_KEY}", "Content-Type": "application/json"}
537
+ mdl = selected_model(ms_model or model, MODELSCOPE_CHAT_MODELS[0] if MODELSCOPE_CHAT_MODELS else "MiniMax/MiniMax-M2.7")
538
+ return base, hdrs, mdl
539
+ base = AI_BASE_URL + "/v1"
540
+ hdrs = api_headers()
541
+ mdl = selected_model(model, CHAT_MODEL)
542
+ return base, hdrs, mdl
543
+
544
+ def api_headers(json_body=True):
545
+ if not AI_API_KEY:
546
+ raise HTTPException(status_code=400, detail="未配置 COMFLY_API_KEY,请在 API/.env 中填写。")
547
+ headers = {"Accept": "application/json", "Authorization": f"Bearer {AI_API_KEY}"}
548
+ if json_body:
549
+ headers["Content-Type"] = "application/json"
550
+ return headers
551
+
552
+ def selected_model(requested, fallback):
553
+ model = (requested or fallback).strip()
554
+ if not model:
555
+ raise HTTPException(status_code=400, detail="模型名称不能为空")
556
+ if len(model) > 120 or not re.fullmatch(r"[a-zA-Z0-9_.:/+-]+", model):
557
+ raise HTTPException(status_code=400, detail=f"模型名称不合法:{model}")
558
+ return model
559
+
560
+ def text_from_chat_response(data):
561
+ choices = data.get("choices") or []
562
+ if not choices:
563
+ return ""
564
+ message = choices[0].get("message") or {}
565
+ content = message.get("content", "")
566
+ if isinstance(content, str):
567
+ return content
568
+ if isinstance(content, list):
569
+ parts = []
570
+ for item in content:
571
+ if isinstance(item, dict):
572
+ parts.append(item.get("text") or item.get("content") or "")
573
+ return "\n".join(part for part in parts if part)
574
+ return str(content)
575
+
576
+ def text_delta_from_chat_chunk(data):
577
+ choices = data.get("choices") or []
578
+ if not choices:
579
+ return ""
580
+ delta = choices[0].get("delta") or {}
581
+ content = delta.get("content", "")
582
+ if isinstance(content, str):
583
+ return content
584
+ if isinstance(content, list):
585
+ parts = []
586
+ for item in content:
587
+ if isinstance(item, dict):
588
+ parts.append(item.get("text") or item.get("content") or "")
589
+ return "".join(parts)
590
+ return str(content) if content else ""
591
+
592
+ def sse_event(data):
593
+ return f"data: {json.dumps(data, ensure_ascii=False)}\n\n"
594
+
595
+ def extract_image(data):
596
+ if isinstance(data.get("data"), dict) and isinstance(data["data"].get("data"), dict):
597
+ data = data["data"]["data"]
598
+ images = data.get("data") or []
599
+ if not images:
600
+ raise HTTPException(status_code=502, detail="生图接口没有返回图片数据")
601
+ first = images[0]
602
+ if first.get("url"):
603
+ return {"type": "url", "value": first["url"]}
604
+ if first.get("b64_json"):
605
+ return {"type": "b64", "value": first["b64_json"]}
606
+ raise HTTPException(status_code=502, detail="无法识别生图接口返回格式")
607
+
608
+ def extract_task_id(data):
609
+ if data.get("task_id"):
610
+ return str(data["task_id"])
611
+ if data.get("id") and str(data.get("id", "")).startswith("task"):
612
+ return str(data["id"])
613
+ nested = data.get("data")
614
+ if isinstance(nested, dict):
615
+ return extract_task_id(nested)
616
+ return None
617
+
618
+ async def wait_for_image_task(client, task_id):
619
+ deadline = time.monotonic() + AI_REQUEST_TIMEOUT
620
+ last_payload = {}
621
+ while time.monotonic() < deadline:
622
+ response = await client.get(f"{AI_BASE_URL}/v1/images/tasks/{task_id}", headers=api_headers())
623
+ response.raise_for_status()
624
+ last_payload = response.json()
625
+ task_data = last_payload.get("data") if isinstance(last_payload.get("data"), dict) else last_payload
626
+ status = str(task_data.get("status", "")).upper()
627
+ if status == "SUCCESS":
628
+ return last_payload
629
+ if status == "FAILURE":
630
+ reason = task_data.get("fail_reason") or last_payload.get("message") or "生图任务失败"
631
+ raise HTTPException(status_code=502, detail=f"生图任务失败:{reason}")
632
+ await asyncio.sleep(IMAGE_POLL_INTERVAL)
633
+ raise HTTPException(status_code=504, detail=f"生图任务超时,task_id={task_id}")
634
+
635
+ def output_file_from_url(url):
636
+ if not url or not url.startswith("/output/"):
637
+ return None
638
+ filename = os.path.basename(urllib.parse.unquote(url.split("?", 1)[0]))
639
+ path = os.path.abspath(os.path.join(OUTPUT_DIR, filename))
640
+ output_root = os.path.abspath(OUTPUT_DIR)
641
+ if os.path.commonpath([output_root, path]) != output_root or not os.path.exists(path):
642
+ return None
643
+ return path
644
+
645
+ def content_type_for_path(path):
646
+ ext = os.path.splitext(path)[1].lower()
647
+ if ext in [".jpg", ".jpeg"]:
648
+ return "image/jpeg"
649
+ if ext == ".webp":
650
+ return "image/webp"
651
+ return "image/png"
652
+
653
+ def convert_output_to_jpg(url, quality=88):
654
+ path = output_file_from_url(url)
655
+ if not path:
656
+ return url
657
+ root, ext = os.path.splitext(path)
658
+ if ext.lower() in [".jpg", ".jpeg"]:
659
+ return url
660
+ jpg_path = f"{root}.jpg"
661
+ try:
662
+ with Image.open(path) as img:
663
+ if img.mode in ("RGBA", "LA") or (img.mode == "P" and "transparency" in img.info):
664
+ bg = Image.new("RGB", img.size, (255, 255, 255))
665
+ bg.paste(img.convert("RGBA"), mask=img.convert("RGBA").split()[-1])
666
+ img = bg
667
+ else:
668
+ img = img.convert("RGB")
669
+ img.save(jpg_path, "JPEG", quality=quality, optimize=True)
670
+ return f"/output/{os.path.basename(jpg_path)}"
671
+ except Exception as e:
672
+ print(f"转换 JPG 失败: {e}")
673
+ return url
674
+
675
+ def reference_to_data_url(ref):
676
+ path = output_file_from_url(ref.get("url", ""))
677
+ if not path:
678
+ return ref.get("url", "")
679
+ with open(path, "rb") as f:
680
+ encoded = base64.b64encode(f.read()).decode("ascii")
681
+ return f"data:{content_type_for_path(path)};base64,{encoded}"
682
+
683
+ async def save_ai_image_to_output(image_data, prefix="online_"):
684
+ filename = f"{prefix}{uuid.uuid4().hex[:10]}.png"
685
+ path = os.path.join(OUTPUT_DIR, filename)
686
+ if image_data["type"] == "b64":
687
+ with open(path, "wb") as f:
688
+ f.write(base64.b64decode(image_data["value"]))
689
+ return f"/output/{filename}"
690
+ value = image_data["value"]
691
+ if value.startswith("/output/"):
692
+ return value
693
+ try:
694
+ async with httpx.AsyncClient(timeout=AI_REQUEST_TIMEOUT) as client:
695
+ response = await client.get(value)
696
+ response.raise_for_status()
697
+ content_type = response.headers.get("Content-Type", "")
698
+ if "jpeg" in content_type or "jpg" in content_type:
699
+ filename = filename[:-4] + ".jpg"
700
+ path = os.path.join(OUTPUT_DIR, filename)
701
+ elif "webp" in content_type:
702
+ filename = filename[:-4] + ".webp"
703
+ path = os.path.join(OUTPUT_DIR, filename)
704
+ with open(path, "wb") as f:
705
+ f.write(response.content)
706
+ return f"/output/{filename}"
707
+ except Exception as e:
708
+ print(f"保存上游图片失败: {e}")
709
+ return value
710
+
711
+ async def generate_ai_image(prompt, size, quality, model, reference_images=None):
712
+ refs = [ref for ref in (reference_images or []) if ref.get("url")]
713
+ async with httpx.AsyncClient(timeout=AI_REQUEST_TIMEOUT) as client:
714
+ if refs:
715
+ files = []
716
+ opened = []
717
+ try:
718
+ for ref in refs[:4]:
719
+ path = output_file_from_url(ref.get("url", ""))
720
+ if not path:
721
+ continue
722
+ fh = open(path, "rb")
723
+ opened.append(fh)
724
+ files.append(("image", (os.path.basename(path), fh, content_type_for_path(path))))
725
+ data = {"model": model, "prompt": prompt, "size": size, "quality": quality, "response_format": "url", "n": "1"}
726
+ response = await client.post(f"{AI_BASE_URL}/v1/images/edits", headers=api_headers(json_body=False), data=data, files=files)
727
+ finally:
728
+ for fh in opened:
729
+ fh.close()
730
+ else:
731
+ response = await client.post(
732
+ f"{AI_BASE_URL}/v1/images/generations",
733
+ headers=api_headers(),
734
+ json={"model": model, "prompt": prompt, "size": size, "quality": quality, "response_format": "url", "n": 1},
735
+ )
736
+ response.raise_for_status()
737
+ raw = response.json()
738
+ try:
739
+ return extract_image(raw), raw
740
+ except HTTPException:
741
+ task_id = extract_task_id(raw)
742
+ if not task_id:
743
+ raise
744
+ task_result = await wait_for_image_task(client, task_id)
745
+ return extract_image(task_result), task_result
746
+
747
+ def upstream_message_from_record(item):
748
+ role = item.get("role")
749
+ if role not in {"user", "assistant"} or item.get("type") == "image":
750
+ return None
751
+ refs = item.get("attachments") or []
752
+ if refs and role == "user":
753
+ content = [{"type": "text", "text": item.get("content", "")}]
754
+ for ref in refs[:4]:
755
+ url = reference_to_data_url(ref)
756
+ if url:
757
+ content.append({"type": "image_url", "image_url": {"url": url}})
758
+ return {"role": role, "content": content}
759
+ return {"role": role, "content": item.get("content", "")}
760
+
761
+ # --- 路由接口 ---
762
+
763
+ @app.get("/")
764
+ async def index():
765
+ return FileResponse(os.path.join(STATIC_DIR, "index.html"))
766
+
767
+ @app.get("/api/view")
768
+ def view_image(filename: str, type: str = "input", subfolder: str = ""):
769
+ for addr in COMFYUI_INSTANCES:
770
+ try:
771
+ url = f"http://{addr}/view"
772
+ params = {"filename": filename, "type": type, "subfolder": subfolder}
773
+ r = requests.get(url, params=params, timeout=1)
774
+ if r.status_code == 200:
775
+ return Response(content=r.content, media_type=r.headers.get('Content-Type'))
776
+ except Exception:
777
+ continue
778
+ raise HTTPException(status_code=404, detail="Image not found on any available backend")
779
+
780
+ @app.get("/api/download-output")
781
+ def download_output(url: str, name: str = ""):
782
+ path = output_file_from_url(url)
783
+ if not path:
784
+ raise HTTPException(status_code=404, detail="文件不存在")
785
+ filename = os.path.basename(name) if name else os.path.basename(path)
786
+ return FileResponse(path, media_type=content_type_for_path(path), filename=filename)
787
+
788
+ @app.post("/api/upload")
789
+ async def upload_image(files: List[UploadFile] = File(...)):
790
+ uploaded_files = []
791
+ files_content = []
792
+ for file in files:
793
+ content = await file.read()
794
+ files_content.append((file, content))
795
+
796
+ for file, content in files_content:
797
+ success_count = 0
798
+ last_result = None
799
+ for addr in COMFYUI_INSTANCES:
800
+ try:
801
+ files_data = {'image': (file.filename, content, file.content_type)}
802
+ response = requests.post(f"http://{addr}/upload/image", files=files_data, timeout=5)
803
+ if response.status_code == 200:
804
+ last_result = response.json()
805
+ success_count += 1
806
+ except Exception as e:
807
+ print(f"Upload error for {addr}: {e}")
808
+
809
+ if success_count > 0 and last_result:
810
+ uploaded_files.append({"comfy_name": last_result.get("name", file.filename)})
811
+ else:
812
+ raise HTTPException(status_code=500, detail="Failed to upload to any backend")
813
+
814
+ return {"files": uploaded_files}
815
+
816
+ @app.post("/api/ai/upload")
817
+ async def upload_ai_reference(files: List[UploadFile] = File(...)):
818
+ uploaded = []
819
+ for file in files:
820
+ content = await file.read()
821
+ if not content:
822
+ continue
823
+ ext = os.path.splitext(file.filename or "")[1].lower()
824
+ if ext not in [".png", ".jpg", ".jpeg", ".webp"]:
825
+ content_type = (file.content_type or "").lower()
826
+ ext = ".jpg" if "jpeg" in content_type else ".webp" if "webp" in content_type else ".png"
827
+ filename = f"ai_ref_{uuid.uuid4().hex[:12]}{ext}"
828
+ path = os.path.join(OUTPUT_DIR, filename)
829
+ with open(path, "wb") as f:
830
+ f.write(content)
831
+ uploaded.append({"url": f"/output/{filename}", "name": file.filename or filename})
832
+ return {"files": uploaded}
833
+
834
+ @app.get("/api/config")
835
+ async def ai_config():
836
+ preferred_chat_model = next((m for m in CHAT_MODELS if m == "gpt-5.5"), CHAT_MODELS[0] if CHAT_MODELS else CHAT_MODEL)
837
+ return {
838
+ "base_url": AI_BASE_URL,
839
+ "chat_model": preferred_chat_model,
840
+ "image_model": IMAGE_MODEL,
841
+ "chat_models": CHAT_MODELS,
842
+ "image_models": IMAGE_MODELS,
843
+ "has_api_key": bool(AI_API_KEY),
844
+ "ms_chat_models": MODELSCOPE_CHAT_MODELS,
845
+ "has_ms_key": bool(MODELSCOPE_API_KEY),
846
+ }
847
+
848
+ @app.get("/api/models")
849
+ async def ai_models():
850
+ return {"chat_models": CHAT_MODELS, "image_models": IMAGE_MODELS}
851
+
852
+ # --- ModelScope Token (从 env 读取,不再支持通过 UI 修改) ---
853
+
854
+ @app.get("/api/config/token")
855
+ async def get_global_token():
856
+ # 优先读 env,回退到 global_config.json(兼容旧数据)
857
+ if MODELSCOPE_API_KEY:
858
+ return {"token": MODELSCOPE_API_KEY}
859
+ if os.path.exists(GLOBAL_CONFIG_FILE):
860
+ try:
861
+ with open(GLOBAL_CONFIG_FILE, 'r', encoding='utf-8') as f:
862
+ config = json.load(f)
863
+ return {"token": config.get("modelscope_token", "")}
864
+ except:
865
+ pass
866
+ return {"token": ""}
867
+
868
+ # --- 在线生图 (COMFLY) ---
869
+
870
+ @app.post("/api/online-image")
871
+ async def online_image(payload: OnlineImageRequest):
872
+ model = selected_model(payload.model, IMAGE_MODEL)
873
+ refs = [ref.dict() for ref in payload.reference_images if ref.url]
874
+ try:
875
+ image_data, raw = await generate_ai_image(payload.prompt, payload.size, payload.quality, model, refs)
876
+ local_url = await save_ai_image_to_output(image_data, prefix="online_")
877
+ except httpx.HTTPStatusError as exc:
878
+ raise HTTPException(status_code=exc.response.status_code, detail=f"上游生图接口错误:{exc.response.text}") from exc
879
+ except httpx.HTTPError as exc:
880
+ raise HTTPException(status_code=502, detail=f"请求上游生图接口失败:{exc}") from exc
881
+
882
+ result = {
883
+ "prompt": payload.prompt,
884
+ "images": [local_url],
885
+ "timestamp": time.time(),
886
+ "type": "online",
887
+ "model": model,
888
+ "params": {"model": model, "size": payload.size, "quality": payload.quality, "reference_images": refs},
889
+ "raw_usage": raw.get("usage") if isinstance(raw, dict) else None,
890
+ }
891
+ save_to_history(result)
892
+ if GLOBAL_LOOP:
893
+ asyncio.run_coroutine_threadsafe(manager.broadcast_new_image(result), GLOBAL_LOOP)
894
+ return result
895
+
896
+ # --- Canvas LLM ---
897
+
898
+ @app.post("/api/canvas-llm")
899
+ async def canvas_llm(payload: CanvasLLMRequest):
900
+ chat_base, chat_hdrs, model = resolve_chat_provider(payload.provider, payload.model, payload.ms_model)
901
+ upstream_messages = [{"role": "system", "content": payload.system_prompt or SYSTEM_PROMPT}]
902
+ for item in payload.messages[-MAX_HISTORY_MESSAGES:]:
903
+ role = item.get("role")
904
+ content = item.get("content")
905
+ if role in {"user", "assistant"} and content:
906
+ upstream_messages.append({"role": role, "content": content})
907
+ upstream_messages.append({"role": "user", "content": payload.message})
908
+ try:
909
+ async with httpx.AsyncClient(timeout=AI_REQUEST_TIMEOUT) as client:
910
+ response = await client.post(
911
+ f"{chat_base}/chat/completions",
912
+ headers=chat_hdrs,
913
+ json={"model": model, "messages": upstream_messages},
914
+ )
915
+ response.raise_for_status()
916
+ if not response.content:
917
+ raise HTTPException(status_code=502, detail="上游接口返回了空响应")
918
+ raw = response.json()
919
+ except httpx.HTTPStatusError as exc:
920
+ raise HTTPException(status_code=exc.response.status_code, detail=f"上游接口错误:{exc.response.text}") from exc
921
+ except httpx.HTTPError as exc:
922
+ raise HTTPException(status_code=502, detail=f"请求上游接口失败:{exc}") from exc
923
+ text = text_from_chat_response(raw).strip() or "接口返回了空回复。"
924
+ return {"text": text, "model": model, "raw_usage": raw.get("usage") if isinstance(raw, dict) else None}
925
+
926
+ # --- 对话管理 ---
927
+
928
+ @app.get("/api/conversations")
929
+ async def conversations(request: Request, x_user_id: str = Header(default="")):
930
+ user_id = safe_user_id(x_user_id, request)
931
+ return {"user_id": user_id, "conversations": list_conversations(user_id)}
932
+
933
+ @app.post("/api/conversations")
934
+ async def create_conversation(payload: ConversationCreateRequest, request: Request, x_user_id: str = Header(default="")):
935
+ user_id = safe_user_id(x_user_id, request)
936
+ return {"conversation": new_conversation(user_id, payload.title)}
937
+
938
+ @app.get("/api/conversations/{conversation_id}")
939
+ async def get_conversation(conversation_id: str, request: Request, x_user_id: str = Header(default="")):
940
+ user_id = safe_user_id(x_user_id, request)
941
+ return {"conversation": load_conversation(user_id, conversation_id)}
942
+
943
+ @app.delete("/api/conversations/{conversation_id}")
944
+ async def delete_conversation(conversation_id: str, request: Request, x_user_id: str = Header(default="")):
945
+ user_id = safe_user_id(x_user_id, request)
946
+ path = conversation_path(user_id, conversation_id)
947
+ if os.path.exists(path):
948
+ os.remove(path)
949
+ return {"ok": True}
950
+
951
+ # --- 画布管理 ---
952
+
953
+ @app.get("/api/canvases")
954
+ async def canvases():
955
+ return {"canvases": list_canvases()}
956
+
957
+ @app.get("/api/canvases/trash")
958
+ async def trashed_canvases():
959
+ return {"canvases": list_deleted_canvases(), "retention_days": 30}
960
+
961
+ @app.post("/api/canvases")
962
+ async def create_canvas(payload: CanvasCreateRequest):
963
+ return {"canvas": new_canvas(payload.title, payload.icon)}
964
+
965
+ @app.get("/api/canvases/{canvas_id}")
966
+ async def get_canvas(canvas_id: str):
967
+ return {"canvas": load_canvas(canvas_id)}
968
+
969
+ @app.put("/api/canvases/{canvas_id}")
970
+ async def update_canvas(canvas_id: str, payload: CanvasSaveRequest):
971
+ canvas = load_canvas(canvas_id)
972
+ canvas["title"] = (payload.title or canvas.get("title") or "未命名画布")[:80]
973
+ canvas["icon"] = (payload.icon or canvas.get("icon") or "layers")[:32]
974
+ canvas["nodes"] = payload.nodes
975
+ canvas["connections"] = payload.connections
976
+ canvas["viewport"] = payload.viewport
977
+ save_canvas(canvas)
978
+ return {"canvas": canvas}
979
+
980
+ @app.delete("/api/canvases/{canvas_id}")
981
+ async def delete_canvas(canvas_id: str):
982
+ canvas = load_canvas_any(canvas_id)
983
+ if not canvas.get("deleted_at"):
984
+ canvas["deleted_at"] = now_ms()
985
+ save_canvas(canvas)
986
+ return {"ok": True}
987
+
988
+ @app.post("/api/canvases/{canvas_id}/restore")
989
+ async def restore_canvas(canvas_id: str):
990
+ canvas = load_canvas_any(canvas_id)
991
+ if canvas.get("deleted_at"):
992
+ canvas.pop("deleted_at", None)
993
+ save_canvas(canvas)
994
+ return {"canvas": canvas}
995
+
996
+ @app.delete("/api/canvases/{canvas_id}/purge")
997
+ async def purge_canvas(canvas_id: str):
998
+ path = canvas_path(canvas_id)
999
+ if os.path.exists(path):
1000
+ os.remove(path)
1001
+ return {"ok": True}
1002
+
1003
+ # --- GPT 对话 ---
1004
+
1005
+ @app.post("/api/chat")
1006
+ async def chat(payload: ChatRequest, request: Request, x_user_id: str = Header(default="")):
1007
+ user_id = safe_user_id(x_user_id, request)
1008
+ conversation = (
1009
+ load_conversation(user_id, payload.conversation_id)
1010
+ if payload.conversation_id
1011
+ else new_conversation(user_id, display_title(payload.message))
1012
+ )
1013
+ if not conversation.get("messages"):
1014
+ conversation["title"] = display_title(payload.message)
1015
+
1016
+ refs = [ref.dict() for ref in payload.reference_images if ref.url]
1017
+ user_message = {
1018
+ "id": uuid.uuid4().hex,
1019
+ "role": "user",
1020
+ "content": payload.message,
1021
+ "created_at": now_ms(),
1022
+ "attachments": refs,
1023
+ "mode": payload.mode,
1024
+ }
1025
+ conversation["messages"].append(user_message)
1026
+ conversation["updated_at"] = now_ms()
1027
+ save_conversation(user_id, conversation)
1028
+
1029
+ if payload.mode == "image":
1030
+ model = selected_model(payload.image_model or payload.model, IMAGE_MODEL)
1031
+ try:
1032
+ image_data, raw = await generate_ai_image(payload.message, payload.size, payload.quality, model, refs)
1033
+ local_url = await save_ai_image_to_output(image_data, prefix="chat_")
1034
+ except httpx.HTTPStatusError as exc:
1035
+ raise HTTPException(status_code=exc.response.status_code, detail=f"上游生图接口错误:{exc.response.text}") from exc
1036
+ except httpx.HTTPError as exc:
1037
+ raise HTTPException(status_code=502, detail=f"请求上游生图接口失败:{exc}") from exc
1038
+ assistant_message = {
1039
+ "id": uuid.uuid4().hex,
1040
+ "role": "assistant",
1041
+ "type": "image",
1042
+ "content": payload.message,
1043
+ "image_url": local_url,
1044
+ "created_at": now_ms(),
1045
+ "model": model,
1046
+ "raw_usage": raw.get("usage") if isinstance(raw, dict) else None,
1047
+ }
1048
+ else:
1049
+ chat_base, chat_hdrs, model = resolve_chat_provider(payload.provider, payload.model, payload.ms_model)
1050
+ history = conversation["messages"][-MAX_HISTORY_MESSAGES:]
1051
+ upstream_messages = [{"role": "system", "content": SYSTEM_PROMPT}]
1052
+ for item in history:
1053
+ msg = upstream_message_from_record(item)
1054
+ if msg:
1055
+ upstream_messages.append(msg)
1056
+ try:
1057
+ async with httpx.AsyncClient(timeout=AI_REQUEST_TIMEOUT) as client:
1058
+ response = await client.post(
1059
+ f"{chat_base}/chat/completions",
1060
+ headers=chat_hdrs,
1061
+ json={"model": model, "messages": upstream_messages},
1062
+ )
1063
+ response.raise_for_status()
1064
+ raw = response.json()
1065
+ except httpx.HTTPStatusError as exc:
1066
+ raise HTTPException(status_code=exc.response.status_code, detail=f"上游接口错误:{exc.response.text}") from exc
1067
+ except httpx.HTTPError as exc:
1068
+ raise HTTPException(status_code=502, detail=f"请求上游接口失败:{exc}") from exc
1069
+ assistant_message = {
1070
+ "id": uuid.uuid4().hex,
1071
+ "role": "assistant",
1072
+ "content": text_from_chat_response(raw).strip() or "接口返回了空回复。",
1073
+ "created_at": now_ms(),
1074
+ "model": model,
1075
+ "raw_usage": raw.get("usage") if isinstance(raw, dict) else None,
1076
+ }
1077
+
1078
+ conversation["messages"].append(assistant_message)
1079
+ conversation["updated_at"] = now_ms()
1080
+ save_conversation(user_id, conversation)
1081
+ return {"conversation": conversation, "message": assistant_message}
1082
+
1083
+ @app.post("/api/chat/stream")
1084
+ async def chat_stream(payload: ChatRequest, request: Request, x_user_id: str = Header(default="")):
1085
+ if payload.mode == "image":
1086
+ raise HTTPException(status_code=400, detail="图片模式请使用 /api/chat")
1087
+
1088
+ user_id = safe_user_id(x_user_id, request)
1089
+ conversation = (
1090
+ load_conversation(user_id, payload.conversation_id)
1091
+ if payload.conversation_id
1092
+ else new_conversation(user_id, display_title(payload.message))
1093
+ )
1094
+ if not conversation.get("messages"):
1095
+ conversation["title"] = display_title(payload.message)
1096
+
1097
+ refs = [ref.dict() for ref in payload.reference_images if ref.url]
1098
+ user_message = {
1099
+ "id": uuid.uuid4().hex,
1100
+ "role": "user",
1101
+ "content": payload.message,
1102
+ "created_at": now_ms(),
1103
+ "attachments": refs,
1104
+ "mode": payload.mode,
1105
+ }
1106
+ conversation["messages"].append(user_message)
1107
+ conversation["updated_at"] = now_ms()
1108
+ save_conversation(user_id, conversation)
1109
+
1110
+ chat_base, chat_hdrs, model = resolve_chat_provider(payload.provider, payload.model, payload.ms_model)
1111
+ history = conversation["messages"][-MAX_HISTORY_MESSAGES:]
1112
+ upstream_messages = [{"role": "system", "content": SYSTEM_PROMPT}]
1113
+ for item in history:
1114
+ msg = upstream_message_from_record(item)
1115
+ if msg:
1116
+ upstream_messages.append(msg)
1117
+
1118
+ async def stream():
1119
+ content_parts = []
1120
+ raw_usage = None
1121
+ yield sse_event({"type": "meta", "conversation": conversation})
1122
+ try:
1123
+ async with httpx.AsyncClient(timeout=AI_REQUEST_TIMEOUT) as client:
1124
+ async with client.stream(
1125
+ "POST",
1126
+ f"{chat_base}/chat/completions",
1127
+ headers=chat_hdrs,
1128
+ json={"model": model, "messages": upstream_messages, "stream": True},
1129
+ ) as response:
1130
+ if response.status_code >= 400:
1131
+ detail = await response.aread()
1132
+ yield sse_event({"type": "error", "detail": f"上游接口错误:{detail.decode('utf-8', errors='ignore')}"})
1133
+ return
1134
+ async for line in response.aiter_lines():
1135
+ if not line:
1136
+ continue
1137
+ if line.startswith("data:"):
1138
+ line = line[5:].strip()
1139
+ if line == "[DONE]":
1140
+ break
1141
+ try:
1142
+ chunk = json.loads(line)
1143
+ except json.JSONDecodeError:
1144
+ continue
1145
+ if isinstance(chunk, dict) and chunk.get("usage"):
1146
+ raw_usage = chunk.get("usage")
1147
+ delta = text_delta_from_chat_chunk(chunk)
1148
+ if delta:
1149
+ content_parts.append(delta)
1150
+ yield sse_event({"type": "delta", "delta": delta})
1151
+ except httpx.HTTPError as exc:
1152
+ yield sse_event({"type": "error", "detail": f"请求上游接口失败:{exc}"})
1153
+ return
1154
+
1155
+ assistant_message = {
1156
+ "id": uuid.uuid4().hex,
1157
+ "role": "assistant",
1158
+ "content": "".join(content_parts).strip() or "接口返回了空回复。",
1159
+ "created_at": now_ms(),
1160
+ "model": model,
1161
+ "raw_usage": raw_usage,
1162
+ }
1163
+ conversation["messages"].append(assistant_message)
1164
+ conversation["updated_at"] = now_ms()
1165
+ save_conversation(user_id, conversation)
1166
+ yield sse_event({"type": "done", "conversation": conversation, "message": assistant_message})
1167
+
1168
+ return StreamingResponse(stream(), media_type="text/event-stream")
1169
+
1170
+ # --- 历史记录 ---
1171
+
1172
+ @app.get("/api/history")
1173
+ async def get_history_api(type: str = None):
1174
+ if os.path.exists(HISTORY_FILE):
1175
+ try:
1176
+ with open(HISTORY_FILE, 'r', encoding='utf-8') as f:
1177
+ data = json.load(f)
1178
+ if type:
1179
+ data = [item for item in data if item.get("type", "zimage") == type]
1180
+ data = [item for item in data if item.get("images") and len(item["images"]) > 0]
1181
+
1182
+ def sort_key(item):
1183
+ ts = item.get("timestamp", 0)
1184
+ if isinstance(ts, (int, float)):
1185
+ return float(ts)
1186
+ return 0
1187
+
1188
+ data.sort(key=sort_key, reverse=True)
1189
+ return data
1190
+ except Exception as e:
1191
+ print(f"读取历史文件失败: {e}")
1192
+ return []
1193
+ return []
1194
+
1195
+ @app.get("/api/queue_status")
1196
+ async def get_queue_status(client_id: str):
1197
+ with QUEUE_LOCK:
1198
+ total = len(QUEUE)
1199
+ positions = [i + 1 for i, t in enumerate(QUEUE) if t["client_id"] == client_id]
1200
+ position = positions[0] if positions else 0
1201
+ return {"total": total, "position": position}
1202
+
1203
+ @app.post("/api/history/delete")
1204
+ async def delete_history(req: DeleteHistoryRequest):
1205
+ if not os.path.exists(HISTORY_FILE):
1206
+ return {"success": False, "message": "History file not found"}
1207
+ try:
1208
+ with HISTORY_LOCK:
1209
+ with open(HISTORY_FILE, 'r', encoding='utf-8') as f:
1210
+ history = json.load(f)
1211
+ target_record = None
1212
+ new_history = []
1213
+ for item in history:
1214
+ is_match = False
1215
+ item_ts = item.get("timestamp", 0)
1216
+ if isinstance(req.timestamp, (int, float)) and isinstance(item_ts, (int, float)):
1217
+ if abs(float(item_ts) - float(req.timestamp)) < 0.001:
1218
+ is_match = True
1219
+ elif str(item_ts) == str(req.timestamp):
1220
+ is_match = True
1221
+ if is_match:
1222
+ target_record = item
1223
+ else:
1224
+ new_history.append(item)
1225
+ if target_record:
1226
+ with open(HISTORY_FILE, 'w', encoding='utf-8') as f:
1227
+ json.dump(new_history, f, ensure_ascii=False, indent=4)
1228
+
1229
+ if target_record:
1230
+ for img_url in target_record.get("images", []):
1231
+ if img_url.startswith("/output/"):
1232
+ filename = img_url.split("/")[-1]
1233
+ file_path = os.path.join(OUTPUT_DIR, filename)
1234
+ if os.path.exists(file_path):
1235
+ try:
1236
+ os.remove(file_path)
1237
+ except Exception as e:
1238
+ print(f"Failed to delete file {file_path}: {e}")
1239
+ return {"success": True}
1240
+ else:
1241
+ return {"success": False, "message": "Record not found"}
1242
+ except Exception as e:
1243
+ print(f"Delete history error: {e}")
1244
+ return {"success": False, "message": str(e)}
1245
+
1246
+ # --- ModelScope 角度控制 ---
1247
+
1248
+ @app.post("/api/angle/poll_status")
1249
+ async def poll_angle_cloud(req: CloudPollRequest):
1250
+ base_url = 'https://api-inference.modelscope.cn/'
1251
+ clean_token = (req.api_key or MODELSCOPE_API_KEY).strip()
1252
+ if not clean_token:
1253
+ raise HTTPException(status_code=400, detail="未提供 ModelScope API Key")
1254
+
1255
+ headers = {
1256
+ "Authorization": f"Bearer {clean_token}",
1257
+ "Content-Type": "application/json",
1258
+ "X-ModelScope-Async-Mode": "true"
1259
+ }
1260
+ task_id = req.task_id
1261
+ print(f"Resuming polling for Angle Task: {task_id}")
1262
+
1263
+ try:
1264
+ async with httpx.AsyncClient(timeout=30) as client:
1265
+ for i in range(300):
1266
+ await asyncio.sleep(2)
1267
+ try:
1268
+ result = await client.get(
1269
+ f"{base_url}v1/tasks/{task_id}",
1270
+ headers={**headers, "X-ModelScope-Task-Type": "image_generation"},
1271
+ )
1272
+ data = result.json()
1273
+ status = data.get("task_status")
1274
+
1275
+ if status == "SUCCEED":
1276
+ img_url = data["output_images"][0]
1277
+ local_path = ""
1278
+ try:
1279
+ async with httpx.AsyncClient() as dl_client:
1280
+ img_res = await dl_client.get(img_url)
1281
+ if img_res.status_code == 200:
1282
+ filename = f"cloud_angle_{int(time.time())}.png"
1283
+ file_path = os.path.join(OUTPUT_DIR, filename)
1284
+ with open(file_path, "wb") as f:
1285
+ f.write(img_res.content)
1286
+ local_path = f"/output/{filename}"
1287
+ else:
1288
+ local_path = img_url
1289
+ except Exception:
1290
+ local_path = img_url
1291
+
1292
+ record = {"timestamp": time.time(), "prompt": f"Resumed {task_id}", "images": [local_path], "type": "angle"}
1293
+ save_to_history(record)
1294
+ if req.client_id:
1295
+ await manager.send_personal_message({"type": "cloud_status", "status": "SUCCEED", "task_id": task_id}, req.client_id)
1296
+ return {"url": local_path}
1297
+
1298
+ elif status == "FAILED":
1299
+ if req.client_id:
1300
+ await manager.send_personal_message({"type": "cloud_status", "status": "FAILED", "task_id": task_id}, req.client_id)
1301
+ raise Exception(f"ModelScope task failed: {data}")
1302
+
1303
+ if i % 5 == 0 and req.client_id:
1304
+ await manager.send_personal_message({
1305
+ "type": "cloud_status", "status": f"{status} ({i}/300)",
1306
+ "task_id": task_id, "progress": i, "total": 300
1307
+ }, req.client_id)
1308
+
1309
+ except Exception as loop_e:
1310
+ print(f"Angle polling error: {loop_e}")
1311
+ continue
1312
+
1313
+ if req.client_id:
1314
+ await manager.send_personal_message({"type": "cloud_status", "status": "TIMEOUT", "task_id": task_id}, req.client_id)
1315
+ return {"status": "timeout", "task_id": task_id, "message": "Task still pending"}
1316
+
1317
+ except Exception as e:
1318
+ print(f"Angle polling error: {e}")
1319
+ raise HTTPException(status_code=400, detail=str(e))
1320
+
1321
+ @app.post("/api/angle/generate")
1322
+ async def generate_angle_cloud(req: CloudGenRequest):
1323
+ base_url = 'https://api-inference.modelscope.cn/'
1324
+ clean_token = (req.api_key or MODELSCOPE_API_KEY).strip()
1325
+ if not clean_token:
1326
+ raise HTTPException(status_code=400, detail="未提供 ModelScope API Key")
1327
+
1328
+ headers = {
1329
+ "Authorization": f"Bearer {clean_token}",
1330
+ "Content-Type": "application/json",
1331
+ "X-ModelScope-Async-Mode": "true"
1332
+ }
1333
+ payload = {
1334
+ "model": "Qwen/Qwen-Image-Edit-2511",
1335
+ "prompt": req.prompt.strip(),
1336
+ "image_url": req.image_urls
1337
+ }
1338
+
1339
+ try:
1340
+ async with httpx.AsyncClient(timeout=30) as client:
1341
+ submit_res = await client.post(f"{base_url}v1/images/generations", headers=headers, json=payload)
1342
+ if submit_res.status_code != 200:
1343
+ try:
1344
+ detail = submit_res.json()
1345
+ except:
1346
+ detail = submit_res.text
1347
+ raise HTTPException(status_code=submit_res.status_code, detail=detail)
1348
+
1349
+ task_id = submit_res.json().get("task_id")
1350
+ print(f"Angle Task submitted, ID: {task_id}")
1351
+
1352
+ for i in range(300):
1353
+ await asyncio.sleep(2)
1354
+ try:
1355
+ result = await client.get(
1356
+ f"{base_url}v1/tasks/{task_id}",
1357
+ headers={**headers, "X-ModelScope-Task-Type": "image_generation"},
1358
+ )
1359
+ data = result.json()
1360
+ status = data.get("task_status")
1361
+
1362
+ if status == "SUCCEED":
1363
+ img_url = data["output_images"][0]
1364
+ local_path = ""
1365
+ try:
1366
+ async with httpx.AsyncClient() as dl_client:
1367
+ img_res = await dl_client.get(img_url)
1368
+ if img_res.status_code == 200:
1369
+ filename = f"cloud_angle_{int(time.time())}.png"
1370
+ file_path = os.path.join(OUTPUT_DIR, filename)
1371
+ with open(file_path, "wb") as f:
1372
+ f.write(img_res.content)
1373
+ local_path = f"/output/{filename}"
1374
+ else:
1375
+ local_path = img_url
1376
+ except Exception:
1377
+ local_path = img_url
1378
+
1379
+ record = {"timestamp": time.time(), "prompt": req.prompt, "images": [local_path], "type": "angle"}
1380
+ save_to_history(record)
1381
+ if req.client_id:
1382
+ await manager.send_personal_message({"type": "cloud_status", "status": "SUCCEED", "task_id": task_id}, req.client_id)
1383
+ if GLOBAL_LOOP:
1384
+ asyncio.run_coroutine_threadsafe(manager.broadcast_new_image(record), GLOBAL_LOOP)
1385
+ return {"url": local_path, "task_id": task_id}
1386
+
1387
+ elif status == "FAILED":
1388
+ if req.client_id:
1389
+ await manager.send_personal_message({"type": "cloud_status", "status": "FAILED", "task_id": task_id}, req.client_id)
1390
+ raise Exception(f"ModelScope task failed: {data}")
1391
+
1392
+ if i % 5 == 0 and req.client_id:
1393
+ await manager.send_personal_message({
1394
+ "type": "cloud_status", "status": f"{status} ({i}/300)",
1395
+ "task_id": task_id, "progress": i, "total": 300
1396
+ }, req.client_id)
1397
+
1398
+ except Exception as loop_e:
1399
+ print(f"Angle polling error: {loop_e}")
1400
+ continue
1401
+
1402
+ if req.client_id:
1403
+ await manager.send_personal_message({"type": "cloud_status", "status": "TIMEOUT", "task_id": task_id}, req.client_id)
1404
+ return {"status": "timeout", "task_id": task_id, "message": "Task still pending"}
1405
+
1406
+ except HTTPException:
1407
+ raise
1408
+ except Exception as e:
1409
+ print(f"Angle generation error: {e}")
1410
+ raise HTTPException(status_code=400, detail=str(e))
1411
+
1412
+ # --- ModelScope Z-Image 云端生图 ---
1413
+
1414
+ @app.post("/generate")
1415
+ async def generate_cloud(req: CloudGenRequest):
1416
+ base_url = 'https://api-inference.modelscope.cn/'
1417
+ clean_token = (req.api_key or MODELSCOPE_API_KEY).strip()
1418
+ if not clean_token:
1419
+ raise HTTPException(status_code=400, detail="未提供 ModelScope API Key")
1420
+
1421
+ headers = {
1422
+ "Authorization": f"Bearer {clean_token}",
1423
+ "Content-Type": "application/json",
1424
+ }
1425
+ payload = {
1426
+ "model": "Tongyi-MAI/Z-Image-Turbo",
1427
+ "prompt": req.prompt.strip(),
1428
+ "size": req.resolution,
1429
+ "n": 1
1430
+ }
1431
+
1432
+ try:
1433
+ async with httpx.AsyncClient(timeout=30) as client:
1434
+ submit_res = await client.post(
1435
+ f"{base_url}v1/images/generations",
1436
+ headers={**headers, "X-ModelScope-Async-Mode": "true"},
1437
+ json=payload
1438
+ )
1439
+ if submit_res.status_code != 200:
1440
+ try:
1441
+ detail = submit_res.json()
1442
+ except:
1443
+ detail = submit_res.text
1444
+ raise HTTPException(status_code=submit_res.status_code, detail=detail)
1445
+
1446
+ task_id = submit_res.json().get("task_id")
1447
+ print(f"Z-Image Task submitted, ID: {task_id}")
1448
+
1449
+ for i in range(200):
1450
+ await asyncio.sleep(3)
1451
+ try:
1452
+ result = await client.get(
1453
+ f"{base_url}v1/tasks/{task_id}",
1454
+ headers={**headers, "X-ModelScope-Task-Type": "image_generation"},
1455
+ )
1456
+ data = result.json()
1457
+ status = data.get("task_status")
1458
+
1459
+ if i % 5 == 0:
1460
+ print(f"Task {task_id} status check {i}: {status}")
1461
+
1462
+ if status == "SUCCEED":
1463
+ img_url = data["output_images"][0]
1464
+ local_path = ""
1465
+ try:
1466
+ async with httpx.AsyncClient() as dl_client:
1467
+ img_res = await dl_client.get(img_url)
1468
+ if img_res.status_code == 200:
1469
+ filename = f"cloud_{int(time.time())}.png"
1470
+ file_path = os.path.join(OUTPUT_DIR, filename)
1471
+ with open(file_path, "wb") as f:
1472
+ f.write(img_res.content)
1473
+ local_path = f"/output/{filename}"
1474
+ else:
1475
+ local_path = img_url
1476
+ except Exception as dl_e:
1477
+ print(f"Download error: {dl_e}")
1478
+ local_path = img_url
1479
+
1480
+ record = {"timestamp": time.time(), "prompt": req.prompt, "images": [local_path], "type": "cloud"}
1481
+ save_to_history(record)
1482
+ try:
1483
+ await manager.broadcast_new_image(record)
1484
+ except Exception:
1485
+ pass
1486
+ return {"url": local_path}
1487
+
1488
+ elif status == "FAILED":
1489
+ raise Exception(f"ModelScope task failed: {data}")
1490
+
1491
+ except Exception as loop_e:
1492
+ print(f"Polling error (retrying): {loop_e}")
1493
+ continue
1494
+
1495
+ raise Exception("Cloud generation timeout")
1496
+
1497
+ except HTTPException:
1498
+ raise
1499
+ except Exception as e:
1500
+ print(f"Cloud generation error: {e}")
1501
+ raise HTTPException(status_code=400, detail=str(e))
1502
+
1503
+ # --- ModelScope 通用图片生成(支持图生图) ---
1504
+
1505
+ @app.post("/api/ms/generate")
1506
+ async def ms_generate(req: MsGenerateRequest):
1507
+ base_url = 'https://api-inference.modelscope.cn/'
1508
+ clean_token = MODELSCOPE_API_KEY.strip()
1509
+ if not clean_token:
1510
+ raise HTTPException(status_code=400, detail="未配置 MODELSCOPE_API_KEY,请在 API/.env 中填写。")
1511
+
1512
+ headers = {
1513
+ "Authorization": f"Bearer {clean_token}",
1514
+ "Content-Type": "application/json",
1515
+ "X-ModelScope-Async-Mode": "true"
1516
+ }
1517
+ payload = {
1518
+ "model": req.model,
1519
+ "prompt": req.prompt.strip(),
1520
+ }
1521
+ if req.width and req.height:
1522
+ payload["width"] = req.width
1523
+ payload["height"] = req.height
1524
+ if req.image_urls:
1525
+ payload["image_url"] = req.image_urls
1526
+ if req.loras is not None:
1527
+ payload["loras"] = req.loras
1528
+
1529
+ try:
1530
+ async with httpx.AsyncClient(timeout=30) as client:
1531
+ submit_res = await client.post(
1532
+ f"{base_url}v1/images/generations",
1533
+ headers=headers,
1534
+ json=payload
1535
+ )
1536
+ if submit_res.status_code != 200:
1537
+ try:
1538
+ detail = submit_res.json()
1539
+ except:
1540
+ detail = submit_res.text
1541
+ raise HTTPException(status_code=submit_res.status_code, detail=detail)
1542
+
1543
+ task_id = submit_res.json().get("task_id")
1544
+ print(f"MS Generate Task submitted ({req.model}), ID: {task_id}")
1545
+
1546
+ TERMINAL_FAILED_STATUSES = {"FAILED", "FAIL", "ERROR", "CANCELED", "CANCELLED", "TIMEOUT", "REVOKED"}
1547
+
1548
+ for i in range(300):
1549
+ await asyncio.sleep(2)
1550
+ try:
1551
+ result = await client.get(
1552
+ f"{base_url}v1/tasks/{task_id}",
1553
+ headers={**headers, "X-ModelScope-Task-Type": "image_generation"},
1554
+ )
1555
+ data = result.json()
1556
+ status = data.get("task_status")
1557
+ print(f"MS Task {task_id} poll {i}: status={status}")
1558
+
1559
+ if status == "SUCCEED":
1560
+ img_url = data["output_images"][0]
1561
+ local_path = ""
1562
+ try:
1563
+ async with httpx.AsyncClient() as dl_client:
1564
+ img_res = await dl_client.get(img_url)
1565
+ if img_res.status_code == 200:
1566
+ filename = f"ms_{req.model.replace('/', '_').replace(':', '_')}_{int(time.time())}.png"
1567
+ file_path = os.path.join(OUTPUT_DIR, filename)
1568
+ with open(file_path, "wb") as f:
1569
+ f.write(img_res.content)
1570
+ local_path = f"/output/{filename}"
1571
+ else:
1572
+ local_path = img_url
1573
+ except Exception:
1574
+ local_path = img_url
1575
+
1576
+ record = {
1577
+ "timestamp": time.time(),
1578
+ "prompt": req.prompt,
1579
+ "images": [local_path],
1580
+ "type": "klein",
1581
+ "model": req.model,
1582
+ }
1583
+ save_to_history(record)
1584
+ if GLOBAL_LOOP:
1585
+ asyncio.run_coroutine_threadsafe(manager.broadcast_new_image(record), GLOBAL_LOOP)
1586
+ return {"url": local_path, "task_id": task_id}
1587
+
1588
+ elif status in TERMINAL_FAILED_STATUSES:
1589
+ error_info = data.get("error_info") or data.get("message") or data.get("detail") or str(data)
1590
+ raise HTTPException(status_code=502, detail=f"MS task {status}: {error_info}")
1591
+
1592
+ except HTTPException:
1593
+ raise
1594
+ except Exception as loop_e:
1595
+ print(f"MS polling error: {loop_e}")
1596
+ continue
1597
+
1598
+ raise HTTPException(status_code=504, detail="MS 生图超时")
1599
+
1600
+ except HTTPException:
1601
+ raise
1602
+ except Exception as e:
1603
+ print(f"MS generate error: {e}")
1604
+ raise HTTPException(status_code=400, detail=str(e))
1605
+
1606
+ # --- 本地 ComfyUI 生图 ---
1607
+
1608
+ @app.post("/api/generate")
1609
+ def generate(req: GenerateRequest):
1610
+ global NEXT_TASK_ID
1611
+ current_task = None
1612
+ target_backend = None
1613
+ with QUEUE_LOCK:
1614
+ task_id = NEXT_TASK_ID
1615
+ NEXT_TASK_ID += 1
1616
+ current_task = {"task_id": task_id, "client_id": req.client_id}
1617
+ QUEUE.append(current_task)
1618
+
1619
+ try:
1620
+ required_images = []
1621
+ for node_id, node_inputs in req.params.items():
1622
+ if isinstance(node_inputs, dict) and "image" in node_inputs:
1623
+ image_name = node_inputs["image"]
1624
+ if isinstance(image_name, str) and image_name:
1625
+ required_images.append(image_name)
1626
+
1627
+ target_backend = get_best_backend(required_images)
1628
+ with LOAD_LOCK:
1629
+ BACKEND_LOCAL_LOAD[target_backend] += 1
1630
+
1631
+ for image_name in required_images:
1632
+ need_sync = False
1633
+ try:
1634
+ check_url = f"http://{target_backend}/view?filename={urllib.parse.quote(image_name)}&type=input"
1635
+ resp = requests.get(check_url, stream=True, timeout=0.5)
1636
+ resp.close()
1637
+ if resp.status_code != 200:
1638
+ need_sync = True
1639
+ except:
1640
+ need_sync = True
1641
+
1642
+ if need_sync:
1643
+ image_content = None
1644
+ image_type = "image/png"
1645
+ for addr in COMFYUI_INSTANCES:
1646
+ if addr == target_backend: continue
1647
+ try:
1648
+ src_url = f"http://{addr}/view?filename={urllib.parse.quote(image_name)}&type=input"
1649
+ r = requests.get(src_url, timeout=5)
1650
+ if r.status_code == 200:
1651
+ image_content = r.content
1652
+ image_type = r.headers.get("Content-Type", "image/png")
1653
+ break
1654
+ except: continue
1655
+
1656
+ if image_content:
1657
+ try:
1658
+ files = {'image': (image_name, image_content, image_type)}
1659
+ requests.post(f"http://{target_backend}/upload/image", files=files, timeout=10)
1660
+ except Exception as e:
1661
+ print(f"Sync upload failed: {e}")
1662
+
1663
+ workflow_path = os.path.join(WORKFLOW_DIR, req.workflow_json)
1664
+ if not os.path.exists(workflow_path) and req.workflow_json == "Z-Image.json":
1665
+ workflow_path = WORKFLOW_PATH
1666
+ if not os.path.exists(workflow_path):
1667
+ raise Exception(f"Workflow file not found: {req.workflow_json}")
1668
+
1669
+ with open(workflow_path, 'r', encoding='utf-8') as f:
1670
+ workflow = json.load(f)
1671
+
1672
+ seed = random.randint(1, 10**15)
1673
+
1674
+ if "23" in workflow and req.prompt:
1675
+ workflow["23"]["inputs"]["text"] = req.prompt
1676
+ if "144" in workflow:
1677
+ workflow["144"]["inputs"]["width"] = req.width
1678
+ workflow["144"]["inputs"]["height"] = req.height
1679
+ if "22" in workflow:
1680
+ workflow["22"]["inputs"]["seed"] = seed
1681
+ if "158" in workflow:
1682
+ workflow["158"]["inputs"]["noise_seed"] = seed
1683
+ for node_id in ["146", "181"]:
1684
+ if node_id in workflow and "inputs" in workflow[node_id] and "seed" in workflow[node_id]["inputs"]:
1685
+ workflow[node_id]["inputs"]["seed"] = seed
1686
+ if "184" in workflow and "inputs" in workflow["184"] and "seed" in workflow["184"]["inputs"]:
1687
+ workflow["184"]["inputs"]["seed"] = seed
1688
+ if "172" in workflow and "inputs" in workflow["172"] and "seed" in workflow["172"]["inputs"]:
1689
+ workflow["172"]["inputs"]["seed"] = seed % 4294967295
1690
+ if "14" in workflow and "inputs" in workflow["14"] and "seed" in workflow["14"]["inputs"]:
1691
+ workflow["14"]["inputs"]["seed"] = seed
1692
+
1693
+ for node_id, node_inputs in req.params.items():
1694
+ if node_id in workflow:
1695
+ if "inputs" not in workflow[node_id]:
1696
+ workflow[node_id]["inputs"] = {}
1697
+ for input_name, value in node_inputs.items():
1698
+ workflow[node_id]["inputs"][input_name] = value
1699
+
1700
+ p = {"prompt": workflow, "client_id": CLIENT_ID}
1701
+ data = json.dumps(p).encode('utf-8')
1702
+ try:
1703
+ post_req = urllib.request.Request(f"http://{target_backend}/prompt", data=data)
1704
+ prompt_id = json.loads(urllib.request.urlopen(post_req, timeout=10).read())['prompt_id']
1705
+ except urllib.error.HTTPError as e:
1706
+ error_body = e.read().decode('utf-8')
1707
+ raise Exception(f"HTTP Error {e.code}: {error_body}")
1708
+
1709
+ history_data = None
1710
+ for i in range(300):
1711
+ try:
1712
+ res = get_comfy_history(target_backend, prompt_id)
1713
+ if prompt_id in res:
1714
+ history_data = res[prompt_id]
1715
+ break
1716
+ except Exception:
1717
+ pass
1718
+ time.sleep(1)
1719
+
1720
+ if not history_data:
1721
+ raise Exception("ComfyUI 渲染超时")
1722
+
1723
+ local_urls = []
1724
+ current_timestamp = time.time()
1725
+ if 'outputs' in history_data:
1726
+ for node_id in history_data['outputs']:
1727
+ node_output = history_data['outputs'][node_id]
1728
+ if 'images' in node_output:
1729
+ for img in node_output['images']:
1730
+ comfy_url_path = f"/view?filename={img['filename']}&subfolder={img['subfolder']}&type={img['type']}"
1731
+ prefix = f"{req.type}_{int(current_timestamp)}_"
1732
+ local_path = download_image(target_backend, comfy_url_path, prefix=prefix)
1733
+ if req.convert_to_jpg:
1734
+ local_path = convert_output_to_jpg(local_path)
1735
+ local_urls.append(local_path)
1736
+
1737
+ result = {
1738
+ "prompt": req.prompt if req.prompt else "Detail Enhance",
1739
+ "images": local_urls,
1740
+ "seed": seed,
1741
+ "timestamp": current_timestamp,
1742
+ "type": req.type,
1743
+ "params": req.params
1744
+ }
1745
+ save_to_history(result)
1746
+ if GLOBAL_LOOP:
1747
+ asyncio.run_coroutine_threadsafe(manager.broadcast_new_image(result), GLOBAL_LOOP)
1748
+ return result
1749
+
1750
+ except Exception as e:
1751
+ return {"images": [], "error": str(e)}
1752
+ finally:
1753
+ if target_backend:
1754
+ with LOAD_LOCK:
1755
+ if BACKEND_LOCAL_LOAD.get(target_backend, 0) > 0:
1756
+ BACKEND_LOCAL_LOAD[target_backend] -= 1
1757
+ if current_task:
1758
+ with QUEUE_LOCK:
1759
+ if current_task in QUEUE:
1760
+ QUEUE.remove(current_task)
1761
+
1762
+ if __name__ == "__main__":
1763
+ import uvicorn
1764
+ uvicorn.run(app, host="0.0.0.0", port=3000)
26-5-10-API-Studio/packages/annotated_doc-0.0.4-py3-none-any.whl ADDED
Binary file (5.3 kB). View file
 
26-5-10-API-Studio/packages/annotated_types-0.7.0-py3-none-any.whl ADDED
Binary file (13.6 kB). View file
 
26-5-10-API-Studio/packages/anyio-4.13.0-py3-none-any.whl ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:08b310f9e24a9594186fd75b4f73f4a4152069e3853f1ed8bfbf58369f4ad708
3
+ size 114353
26-5-10-API-Studio/packages/certifi-2026.4.22-py3-none-any.whl ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:3cb2210c8f88ba2318d29b0388d1023c8492ff72ecdde4ebdaddbb13a31b1c4a
3
+ size 135707
26-5-10-API-Studio/packages/charset_normalizer-3.4.7-cp314-cp314-win_amd64.whl ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:92a0a01ead5e668468e952e4238cccd7c537364eb7d851ab144ab6627dbbe12f
3
+ size 159634
26-5-10-API-Studio/packages/click-8.3.3-py3-none-any.whl ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:a2bf429bb3033c89fa4936ffb35d5cb471e3719e1f3c8a7c3fff0b8314305613
3
+ size 110502
26-5-10-API-Studio/packages/colorama-0.4.6-py2.py3-none-any.whl ADDED
Binary file (25.3 kB). View file
 
26-5-10-API-Studio/packages/fastapi-0.136.1-py3-none-any.whl ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:a6e9d7eeada96c93a4d69cb03836b44fa34e2854accb7244a1ece36cd4781c3f
3
+ size 117683
26-5-10-API-Studio/packages/h11-0.16.0-py3-none-any.whl ADDED
Binary file (37.5 kB). View file
 
26-5-10-API-Studio/packages/httpcore-1.0.9-py3-none-any.whl ADDED
Binary file (78.8 kB). View file
 
26-5-10-API-Studio/packages/httpx-0.28.1-py3-none-any.whl ADDED
Binary file (73.5 kB). View file
 
26-5-10-API-Studio/packages/idna-3.13-py3-none-any.whl ADDED
Binary file (68.6 kB). View file
 
26-5-10-API-Studio/packages/pillow-12.2.0-cp314-cp314-win_amd64.whl ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:4e6c62e9d237e9b65fac06857d511e90d8461a32adcc1b9065ea0c0fa3a28150
3
+ size 7217400
26-5-10-API-Studio/packages/pydantic-2.13.4-py3-none-any.whl ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:45a282cde31d808236fd7ea9d919b128653c8b38b393d1c4ab335c62924d9aba
3
+ size 472262
26-5-10-API-Studio/packages/pydantic_core-2.46.4-cp314-cp314-win_amd64.whl ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:811ff8e9c313ab425368bcbb36e5c4ebd7108c2bbf4e4089cfbb0b01eff63fac
3
+ size 2072238
26-5-10-API-Studio/packages/python_multipart-0.0.27-py3-none-any.whl ADDED
Binary file (29.3 kB). View file
 
26-5-10-API-Studio/packages/requests-2.33.1-py3-none-any.whl ADDED
Binary file (64.9 kB). View file
 
26-5-10-API-Studio/packages/starlette-1.0.0-py3-none-any.whl ADDED
Binary file (72.7 kB). View file
 
26-5-10-API-Studio/packages/typing_extensions-4.15.0-py3-none-any.whl ADDED
Binary file (44.6 kB). View file
 
26-5-10-API-Studio/packages/typing_inspection-0.4.2-py3-none-any.whl ADDED
Binary file (14.6 kB). View file
 
26-5-10-API-Studio/packages/urllib3-2.7.0-py3-none-any.whl ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:9fb4c81ebbb1ce9531cce37674bbc6f1360472bc18ca9a553ede278ef7276897
3
+ size 131087
26-5-10-API-Studio/packages/uvicorn-0.46.0-py3-none-any.whl ADDED
Binary file (70.9 kB). View file
 
26-5-10-API-Studio/readme.txt ADDED
@@ -0,0 +1,33 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ 1. API Website Registration:
2
+
3
+ https://ai.comfly.chat/register?aff=HAOj137551
4
+
5
+ 2. Create an API key:
6
+
7
+ https://ai.comfly.chat/token
8
+
9
+ 3. Fill in the key in the .env file of the software's API directory:
10
+
11
+ COMFLY_API_KEY=sk-xxxxx
12
+
13
+ 4. Create a ModelScope API key (for international domains, it's modelscope.ai, so you'll need to modify this part of the code using Codex):
14
+
15
+ https://www.modelscope.cn/my/access/token
16
+
17
+ 5. Fill in the key in the .env file of the API directory:
18
+
19
+ MODELSCOPE_API_KEY=ms-xxxx
20
+
21
+ 6. If calling a local ComfyUI, ensure all workflows in the workflows directory can run normally locally.
22
+
23
+ 7. If the default port for your local ComfyUI is 8188, you don't need to modify this value. Currently, this value reads the graphics cards using ports 8188 and 4090. If you have multiple graphics cards, you can change the port number.
24
+
25
+ COMFYUI_INSTANCES=127.0.0.1:8188,127.0.0.1:4090
26
+
27
+ ----Instructions-----
28
+
29
+ Run "Start Service.bat" directly. If dependencies are missing, run "Install Dependencies.bat".
30
+
31
+ ---Error Troubleshooting---
32
+
33
+ For any errors, you can install Codex Installer.exe. Select this folder and let Codex resolve the runtime issues. Free accounts have a weekly free quota.
26-5-10-API-Studio/requirements.txt ADDED
@@ -0,0 +1,7 @@
 
 
 
 
 
 
 
 
1
+ fastapi
2
+ uvicorn
3
+ requests
4
+ pydantic
5
+ python-multipart
6
+ httpx
7
+ pillow
26-5-10-API-Studio/static/angle.html ADDED
@@ -0,0 +1,1270 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <!DOCTYPE html>
2
+ <html lang="zh-CN">
3
+
4
+ <head>
5
+ <meta charset="UTF-8">
6
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
7
+ <link rel="icon" href="/static/logo.png" type="image/png">
8
+ <title>Angle Control | 视角重塑</title>
9
+ <script src="https://cdn.tailwindcss.com"></script>
10
+ <script src="https://unpkg.com/lucide@latest"></script>
11
+ <script src="/static/theme.js?v=20260509"></script>
12
+ <script type="importmap">
13
+ {
14
+ "imports": {
15
+ "three": "https://unpkg.com/three@0.160.0/build/three.module.js"
16
+ }
17
+ }
18
+ </script>
19
+ <style>
20
+ @import url('https://fonts.googleapis.com/css2?family=Inter:wght@300;400;600;800&family=JetBrains+Mono:wght@400;700&display=swap');
21
+
22
+ :root {
23
+ --accent: #111827;
24
+ --bg: #f9fafb;
25
+ --card: #ffffff;
26
+ --easing: cubic-bezier(0.4, 0, 0.2, 1);
27
+ }
28
+
29
+ /* --- 极简悬浮浅灰滚动条 (无底色/左移) --- */
30
+ *::-webkit-scrollbar {
31
+ width: 10px !important;
32
+ height: 10px !important;
33
+ background: transparent !important;
34
+ }
35
+
36
+ *::-webkit-scrollbar-track {
37
+ background: transparent !important;
38
+ border: none !important;
39
+ }
40
+
41
+ *::-webkit-scrollbar-thumb {
42
+ background-color: #d8d8d8 !important;
43
+ border: 3px solid transparent !important;
44
+ border-right-width: 5px !important;
45
+ /* 增加右侧间距,使滚动条向左位移 */
46
+ background-clip: padding-box !important;
47
+ border-radius: 10px !important;
48
+ }
49
+
50
+ *::-webkit-scrollbar-thumb:hover {
51
+ background-color: #c0c0c0 !important;
52
+ }
53
+
54
+ *::-webkit-scrollbar-corner {
55
+ background: transparent !important;
56
+ }
57
+
58
+ * {
59
+ scrollbar-width: thin !important;
60
+ scrollbar-color: #d8d8d8 transparent !important;
61
+ }
62
+
63
+ body {
64
+ background-color: var(--bg);
65
+ font-family: 'Inter', -apple-system, sans-serif;
66
+ color: var(--accent);
67
+ -webkit-font-smoothing: antialiased;
68
+ }
69
+
70
+ .container-box {
71
+ max-width: 1280px;
72
+ margin: 0 auto;
73
+ padding: 0 40px;
74
+ margin-top: 50px;
75
+ }
76
+
77
+ /* 统一组件风格 */
78
+ .glass-btn {
79
+ background: #111827;
80
+ transition: all 0.3s var(--easing);
81
+ }
82
+
83
+ .glass-btn:hover {
84
+ background: #000;
85
+ transform: translateY(-1px);
86
+ box-shadow: 0 12px 24px rgba(0, 0, 0, 0.1);
87
+ }
88
+
89
+ .glass-btn:active {
90
+ transform: scale(0.98);
91
+ }
92
+
93
+ .upload-item {
94
+ background: var(--card);
95
+ border: 1px dashed #e2e8f0;
96
+ transition: all 0.4s var(--easing);
97
+ }
98
+
99
+ .upload-item:hover {
100
+ border-color: #000;
101
+ background: #fff;
102
+ transform: translateY(-2px);
103
+ }
104
+
105
+ .result-frame {
106
+ background: #ffffff;
107
+ border-radius: 32px;
108
+ border: 1px solid #f1f5f9;
109
+ box-shadow: 0 2px 15px rgba(0, 0, 0, 0.02);
110
+ }
111
+
112
+ .masonry-grid {
113
+ display: grid;
114
+ grid-template-columns: repeat(2, 1fr);
115
+ gap: 1.25rem;
116
+ }
117
+
118
+ @media (min-width: 768px) {
119
+ .masonry-grid {
120
+ grid-template-columns: repeat(4, 1fr);
121
+ }
122
+ }
123
+
124
+ .masonry-item {
125
+ aspect-ratio: 1 / 1;
126
+ background: #fff;
127
+ border: 1px solid #f1f5f9;
128
+ border-radius: 24px;
129
+ overflow: hidden;
130
+ transition: all 0.5s var(--easing);
131
+ position: relative;
132
+ }
133
+
134
+ .masonry-item:hover {
135
+ transform: translateY(-6px);
136
+ box-shadow: 0 20px 40px rgba(0, 0, 0, 0.08);
137
+ }
138
+
139
+ .nano-input {
140
+ background: #ffffff;
141
+ border-radius: 16px;
142
+ transition: all 0.3s ease;
143
+ border: 1px solid #e5e7eb;
144
+ }
145
+
146
+ .nano-input:focus {
147
+ background: #ffffff;
148
+ box-shadow: 0 0 0 2px #000;
149
+ border-color: transparent;
150
+ }
151
+
152
+ @keyframes b-loading {
153
+ 0% {
154
+ transform: scale(1);
155
+ background: #000;
156
+ }
157
+
158
+ 50% {
159
+ transform: scale(1.15);
160
+ background: #444;
161
+ }
162
+
163
+ 100% {
164
+ transform: scale(1);
165
+ background: #000;
166
+ }
167
+ }
168
+
169
+ .loading-box {
170
+ width: 10px;
171
+ height: 10px;
172
+ animation: b-loading 1s infinite var(--easing);
173
+ }
174
+
175
+ /* 复合切换组件样式 - 来自 zimage */
176
+ .mode-switcher {
177
+ position: relative;
178
+ background: #f1f1f1;
179
+ padding: 4px;
180
+ border-radius: 14px;
181
+ display: flex;
182
+ width: 100%;
183
+ }
184
+
185
+ .mode-btn {
186
+ position: relative;
187
+ z-index: 10;
188
+ flex: 1;
189
+ padding: 8px 0;
190
+ text-align: center;
191
+ font-size: 11px;
192
+ font-weight: 800;
193
+ text-transform: uppercase;
194
+ color: #999;
195
+ transition: color 0.3s ease;
196
+ cursor: pointer;
197
+ }
198
+
199
+ .mode-btn.active {
200
+ color: #000;
201
+ }
202
+
203
+ .mode-glider {
204
+ position: absolute;
205
+ height: calc(100% - 8px);
206
+ width: calc(50% - 4px);
207
+ background: #fff;
208
+ border-radius: 11px;
209
+ box-shadow: 0 2px 5px rgba(0, 0, 0, 0.05);
210
+ transition: transform 0.3s var(--easing);
211
+ z-index: 1;
212
+ }
213
+ </style>
214
+ <link rel="stylesheet" href="/static/theme.css?v=20260510-studio-pages-blue-dark10">
215
+ </head>
216
+
217
+ <body class="selection:bg-black selection:text-white">
218
+
219
+ <div class="container-box">
220
+ <header class="flex flex-col md:flex-row justify-between items-end mb-16 gap-6">
221
+ <div class="space-y-1">
222
+ <h1 class="text-4xl font-extrabold tracking-[-0.05em] flex items-center">
223
+ ANGLE CONTROL<span class="text-base mt-3 ml-1">®</span>
224
+ </h1>
225
+ <p class="text-[10px] font-bold uppercase tracking-[0.5em] text-gray-400">Camera & Perspective Control
226
+ </p>
227
+ </div>
228
+ <nav class="flex gap-8 text-[11px] font-bold uppercase tracking-widest text-gray-500">
229
+ <span class="text-black border-b-2 border-black pb-1">Angle</span>
230
+ </nav>
231
+ </header>
232
+
233
+ <main class="space-y-12">
234
+ <!-- Row 1: Upload and 3D Control -->
235
+ <div class="grid grid-cols-1 md:grid-cols-2 gap-10 items-start">
236
+ <section class="group w-full">
237
+ <h3 class="text-[9px] font-black uppercase tracking-[0.3em] mb-5 text-gray-400">01. Input Source
238
+ </h3>
239
+ <div id="dropzone"
240
+ class="upload-item relative overflow-hidden rounded-2xl aspect-[4/3] flex flex-col items-center justify-center cursor-pointer">
241
+ <input type="file" id="fileInput" class="hidden" accept="image/*">
242
+
243
+ <div id="uploadContent" class="text-center space-y-4">
244
+ <div
245
+ class="w-14 h-14 rounded-full border border-gray-200 bg-white flex items-center justify-center mx-auto group-hover:bg-black group-hover:text-white group-hover:border-black transition-all duration-500">
246
+ <i data-lucide="arrow-up" class="w-5 h-5"></i>
247
+ </div>
248
+ <p class="text-[11px] font-bold uppercase tracking-tight">Drop image here</p>
249
+ </div>
250
+
251
+ <img id="previewImg" class="hidden absolute inset-0 w-full h-full object-cover">
252
+
253
+ <div id="changeOverlay"
254
+ class="hidden absolute inset-0 bg-black/10 backdrop-blur-sm items-center justify-center opacity-0 hover:opacity-100 transition-opacity">
255
+ <span
256
+ class="bg-white px-5 py-2 rounded-full text-[10px] font-bold uppercase tracking-widest shadow-2xl">Change</span>
257
+ </div>
258
+ </div>
259
+ </section>
260
+
261
+ <section id="cameraControl" class="space-y-6 w-full">
262
+ <h3 class="text-[9px] font-black uppercase tracking-[0.3em] text-gray-400">02. Camera Control</h3>
263
+ <div
264
+ class="w-full aspect-[4/3] flex flex-col md:flex-row bg-white rounded-2xl border border-gray-100 shadow-sm overflow-hidden">
265
+ <!-- 3D View -->
266
+ <div id="threeContainer" class="relative flex-1 bg-[#222] h-full min-h-0"></div>
267
+
268
+ <!-- Controls -->
269
+ <div
270
+ class="w-full md:w-64 flex-shrink-0 p-5 flex flex-col justify-center gap-4 border-l border-gray-100 bg-white overflow-y-auto">
271
+ <!-- Horizontal -->
272
+ <div class="space-y-3">
273
+ <div
274
+ class="flex justify-between items-center text-[10px] font-bold uppercase tracking-wider text-gray-500">
275
+ <div class="flex items-center gap-2">
276
+ <i data-lucide="move-horizontal" class="w-3 h-3"></i>
277
+ <span>Rotation</span>
278
+ </div>
279
+ <div class="flex items-center gap-1.5">
280
+ <button onclick="resetControl('h')"
281
+ class="text-gray-300 hover:text-black transition-colors p-1" title="Reset">
282
+ <i data-lucide="rotate-ccw" class="w-3 h-3"></i>
283
+ </button>
284
+ <div class="flex items-center bg-gray-100 rounded-md px-2">
285
+ <input type="number" id="val-horizontal" value="0"
286
+ class="w-10 bg-transparent py-1 text-black text-center outline-none border-none p-0 text-[10px] font-bold [appearance:textfield] [&::-webkit-outer-spin-button]:appearance-none [&::-webkit-inner-spin-button]:appearance-none"
287
+ oninput="syncInput('h')">
288
+ <span class="text-gray-400 select-none text-[10px]">°</span>
289
+ </div>
290
+ </div>
291
+ </div>
292
+ <input type="range" id="rotate-h" min="-90" max="90" value="0"
293
+ class="w-full h-1.5 bg-gray-200 rounded-lg appearance-none cursor-pointer accent-black">
294
+ </div>
295
+
296
+ <!-- Vertical -->
297
+ <div class="space-y-3">
298
+ <div
299
+ class="flex justify-between items-center text-[10px] font-bold uppercase tracking-wider text-gray-500">
300
+ <div class="flex items-center gap-2">
301
+ <i data-lucide="move-vertical" class="w-3 h-3"></i>
302
+ <span>Pitch</span>
303
+ </div>
304
+ <div class="flex items-center gap-1.5">
305
+ <button onclick="resetControl('v')"
306
+ class="text-gray-300 hover:text-black transition-colors p-1" title="Reset">
307
+ <i data-lucide="rotate-ccw" class="w-3 h-3"></i>
308
+ </button>
309
+ <div class="flex items-center bg-gray-100 rounded-md px-2">
310
+ <input type="number" id="val-vertical" value="0"
311
+ class="w-10 bg-transparent py-1 text-black text-center outline-none border-none p-0 text-[10px] font-bold [appearance:textfield] [&::-webkit-outer-spin-button]:appearance-none [&::-webkit-inner-spin-button]:appearance-none"
312
+ oninput="syncInput('v')">
313
+ <span class="text-gray-400 select-none text-[10px]">°</span>
314
+ </div>
315
+ </div>
316
+ </div>
317
+ <input type="range" id="rotate-v" min="-90" max="90" value="0"
318
+ class="w-full h-1.5 bg-gray-200 rounded-lg appearance-none cursor-pointer accent-black">
319
+ </div>
320
+
321
+ <!-- Distance -->
322
+ <div class="space-y-3">
323
+ <div
324
+ class="flex justify-between items-center text-[10px] font-bold uppercase tracking-wider text-gray-500">
325
+ <div class="flex items-center gap-2">
326
+ <i data-lucide="zoom-in" class="w-3 h-3"></i>
327
+ <span>Distance</span>
328
+ </div>
329
+ <div class="flex items-center gap-1.5">
330
+ <button onclick="resetControl('d')"
331
+ class="text-gray-300 hover:text-black transition-colors p-1" title="Reset">
332
+ <i data-lucide="rotate-ccw" class="w-3 h-3"></i>
333
+ </button>
334
+ <div class="flex items-center bg-gray-100 rounded-md px-2">
335
+ <input type="number" id="val-distance" value="4.0" step="0.1"
336
+ class="w-10 bg-transparent py-1 text-black text-center outline-none border-none p-0 text-[10px] font-bold [appearance:textfield] [&::-webkit-outer-spin-button]:appearance-none [&::-webkit-inner-spin-button]:appearance-none"
337
+ oninput="syncInput('d')">
338
+ </div>
339
+ </div>
340
+ </div>
341
+ <input type="range" id="distance" min="0.1" max="8" value="4" step="0.1"
342
+ class="w-full h-1.5 bg-gray-200 rounded-lg appearance-none cursor-pointer accent-black">
343
+ </div>
344
+ </div>
345
+ </div>
346
+ </section>
347
+ </div>
348
+
349
+ <!-- Row 2: Parameters & Result -->
350
+ <div class="grid grid-cols-1 md:grid-cols-2 gap-10 items-stretch">
351
+ <section class="flex flex-col space-y-6">
352
+ <h3 class="text-[9px] font-black uppercase tracking-[0.3em] text-gray-400">03. Parameters</h3>
353
+
354
+ <div class="space-y-3 flex-1 flex flex-col">
355
+ <div class="flex items-center gap-2 text-gray-800 ml-1">
356
+ <i data-lucide="text-quote" class="w-3 h-3"></i>
357
+ <span class="text-[10px] font-bold uppercase tracking-widest">Prompt</span>
358
+ </div>
359
+ <textarea id="promptInput"
360
+ class="nano-input w-full flex-1 p-5 text-sm outline-none resize-none placeholder-gray-300"
361
+ placeholder="请通过右侧控制器调整,或输入提示词"></textarea>
362
+ </div>
363
+
364
+ <!-- Engine Switcher -->
365
+ <div class="mode-switcher">
366
+ <div id="modeLocal" class="mode-btn active flex items-center justify-center gap-1.5"
367
+ onclick="switchEngine('local')">
368
+ <i data-lucide="monitor" class="w-3 h-3"></i>
369
+ <span>Local</span>
370
+ </div>
371
+ <div id="modeCloud" class="mode-btn flex items-center justify-center gap-1.5"
372
+ onclick="switchEngine('cloud')">
373
+ <i data-lucide="cloud" class="w-3 h-3"></i>
374
+ <span>ModelScope</span>
375
+ </div>
376
+ <div id="glider" class="mode-glider"></div>
377
+ </div>
378
+
379
+ <button id="genBtn" onclick="handleGenerate()"
380
+ class="glass-btn w-full py-5 text-white rounded-xl font-bold text-[11px] uppercase tracking-[0.4em] flex items-center justify-center gap-3 shadow-xl shadow-black/10 disabled:opacity-50 disabled:cursor-not-allowed">
381
+ <i data-lucide="zap" id="btnIcon" class="w-4 h-4 text-yellow-400"></i>
382
+ <span id="btnText">Generate New Angle</span>
383
+ </button>
384
+ </section>
385
+
386
+ <section class="space-y-6 flex flex-col">
387
+ <h3 class="text-[9px] font-black uppercase tracking-[0.3em] text-gray-400">04. Result Preview</h3>
388
+ <div id="resultBox"
389
+ class="result-frame relative aspect-[4/3] w-full flex items-center justify-center overflow-hidden group">
390
+ <div id="emptyState" class="text-center space-y-4 opacity-20">
391
+ <i data-lucide="camera" class="w-12 h-12 mx-auto stroke-[1px]"></i>
392
+ <p class="text-[10px] font-black tracking-[0.5em] uppercase">Canvas Ready</p>
393
+ </div>
394
+
395
+ <div id="loadingState" class="hidden flex flex-col items-center gap-5 w-full max-w-[80%]">
396
+ <div class="loading-box"></div>
397
+ <p class="text-[10px] font-bold uppercase tracking-[0.4em] animate-pulse">Processing...</p>
398
+ <!-- Progress Bar -->
399
+ <div id="cloud-progress-container" class="hidden w-full mt-4">
400
+ <div class="flex justify-between text-[9px] font-bold text-gray-400 mb-1 uppercase tracking-widest">
401
+ <span id="cloud-status-text">Pending...</span>
402
+ <span id="cloud-percent">0%</span>
403
+ </div>
404
+ <div class="w-full bg-gray-100 rounded-full h-1.5 overflow-hidden">
405
+ <div id="cloud-progress-bar" class="bg-black h-full rounded-full transition-all duration-300" style="width: 0%"></div>
406
+ </div>
407
+ </div>
408
+ </div>
409
+
410
+ <div id="textResult"
411
+ class="hidden w-full h-full p-12 flex flex-col items-center justify-center text-center space-y-8">
412
+ <i data-lucide="terminal" class="w-12 h-12 text-gray-300 mx-auto"></i>
413
+ <div class="space-y-4 max-w-md">
414
+ <p class="text-[10px] font-bold uppercase tracking-[0.5em] text-gray-400">Generated
415
+ Command
416
+ </p>
417
+ <h2 id="generatedText" class="text-2xl font-bold leading-relaxed text-gray-900"></h2>
418
+ </div>
419
+ <button onclick="copyText()"
420
+ class="px-8 py-3 bg-gray-100 hover:bg-black hover:text-white rounded-full text-[10px] font-bold uppercase tracking-widest transition-all flex items-center gap-2">
421
+ <i data-lucide="copy" class="w-3 h-3"></i> Copy
422
+ </button>
423
+ </div>
424
+
425
+ <img id="outputImg"
426
+ class="hidden w-full h-full object-contain p-8 cursor-zoom-in transition-all duration-700 hover:scale-[1.02]"
427
+ onclick="zoomImage()">
428
+
429
+ <a id="downloadBtn" href="#" download
430
+ class="hidden absolute bottom-8 right-8 w-14 h-14 bg-white shadow-2xl rounded-2xl flex items-center justify-center hover:bg-black hover:text-white transition-all duration-500 border border-gray-100">
431
+ <i data-lucide="download" class="w-5 h-5"></i>
432
+ </a>
433
+ </div>
434
+ </section>
435
+ </div>
436
+ </main>
437
+
438
+ <section class="mt-32">
439
+ <div class="flex items-center gap-6 mb-10">
440
+ <h2 class="text-[11px] font-black uppercase tracking-[0.5em]">Archive</h2>
441
+ <div class="h-px flex-1 bg-black/5"></div>
442
+ </div>
443
+ <div id="masonry" class="masonry-grid"></div>
444
+ <div id="loadMoreTrigger"
445
+ class="py-16 text-center opacity-20 text-[10px] font-bold uppercase tracking-widest">
446
+ End of Archive
447
+ </div>
448
+ </section>
449
+ </div>
450
+
451
+ <div id="lightbox" onclick="handleOutsideClick(event)"
452
+ class="hidden fixed inset-0 z-50 bg-white/95 backdrop-blur-3xl flex items-center justify-center p-8">
453
+ <button onclick="closeLightbox()"
454
+ class="absolute top-10 right-10 p-2 hover:rotate-90 transition-transform duration-500">
455
+ <i data-lucide="x" class="w-8 h-8"></i>
456
+ </button>
457
+
458
+ <div class="max-w-6xl w-full h-full flex flex-col items-center justify-center">
459
+ <div class="relative">
460
+ <div id="lightboxRes"
461
+ class="absolute top-4 left-4 bg-black/30 backdrop-blur-md border border-white/20 text-white px-3 py-1.5 rounded-full text-[10px] font-medium tracking-wider opacity-0 transition-opacity duration-300 pointer-events-none">
462
+ </div>
463
+ <img id="lightboxImg" src="" class="hidden max-h-[80vh] rounded-3xl shadow-2xl">
464
+ </div>
465
+ <div class="mt-8">
466
+ <button onclick="downloadLightboxImage()"
467
+ class="px-10 py-4 bg-black text-white rounded-full text-[10px] font-black uppercase tracking-widest flex items-center gap-3 shadow-xl">
468
+ <i data-lucide="save" class="w-4 h-4"></i> Save Master
469
+ </button>
470
+ </div>
471
+ </div>
472
+ </div>
473
+
474
+ <script type="module">
475
+ import * as THREE from 'three';
476
+
477
+ // Initialize WebSocket Listener for Cloud Progress
478
+ window.addEventListener('message', function(event) {
479
+ const data = event.data;
480
+ if (data && data.type === 'cloud_status') {
481
+ updateCloudProgress(data);
482
+ }
483
+ });
484
+
485
+ function updateCloudProgress(data) {
486
+ const container = document.getElementById('cloud-progress-container');
487
+ const statusText = document.getElementById('cloud-status-text');
488
+ const progressBar = document.getElementById('cloud-progress-bar');
489
+ const percentText = document.getElementById('cloud-percent');
490
+
491
+ if (!container || !statusText || !progressBar) return;
492
+
493
+ // Show container if hidden
494
+ if (container.classList.contains('hidden')) {
495
+ container.classList.remove('hidden');
496
+ }
497
+
498
+ // Update UI
499
+ if (data.status) {
500
+ // Simplify status text (remove internal details if needed)
501
+ let displayStatus = data.status;
502
+ if (displayStatus.includes("PENDING")) displayStatus = "Queueing...";
503
+ if (displayStatus.includes("RUNNING")) displayStatus = "Generating...";
504
+ statusText.innerText = displayStatus;
505
+ }
506
+
507
+ if (typeof data.progress !== 'undefined' && typeof data.total !== 'undefined') {
508
+ const percent = Math.min(100, Math.round((data.progress / data.total) * 100));
509
+ progressBar.style.width = `${percent}%`;
510
+ percentText.innerText = `${percent}%`;
511
+ }
512
+ }
513
+
514
+ const container = document.getElementById('threeContainer');
515
+ const scene = new THREE.Scene();
516
+ scene.background = new THREE.Color(0x222222);
517
+
518
+ // Camera setup
519
+ const camera = new THREE.PerspectiveCamera(75, 1, 0.1, 1000);
520
+
521
+ // Renderer setup
522
+ const renderer = new THREE.WebGLRenderer({ antialias: true, alpha: true });
523
+ renderer.setSize(container.clientWidth, container.clientHeight);
524
+ renderer.setPixelRatio(window.devicePixelRatio);
525
+ container.appendChild(renderer.domElement);
526
+
527
+ // Objects
528
+ const geometry = new THREE.PlaneGeometry(3, 3);
529
+ const material = new THREE.MeshStandardMaterial({
530
+ color: 0x444444,
531
+ side: THREE.DoubleSide
532
+ });
533
+ const cube = new THREE.Mesh(geometry, material);
534
+ scene.add(cube);
535
+
536
+ const gridHelper = new THREE.GridHelper(20, 20, 0x444444, 0x333333);
537
+ scene.add(gridHelper);
538
+
539
+ // Lighting
540
+ const ambientLight = new THREE.AmbientLight(0xffffff, 0.6);
541
+ scene.add(ambientLight);
542
+ const pointLight = new THREE.DirectionalLight(0xffffff, 1);
543
+ pointLight.position.set(5, 10, 7);
544
+ scene.add(pointLight);
545
+
546
+ // Camera Logic
547
+ const sliderH = document.getElementById('rotate-h');
548
+ const sliderV = document.getElementById('rotate-v');
549
+ const sliderD = document.getElementById('distance');
550
+ const valH = document.getElementById('val-horizontal');
551
+ const valV = document.getElementById('val-vertical');
552
+ const valD = document.getElementById('val-distance');
553
+
554
+ window.updateCamera = function () {
555
+ const lon = parseFloat(sliderH.value);
556
+ const lat = parseFloat(sliderV.value);
557
+ const dist = parseFloat(sliderD.value);
558
+
559
+ // Sync inputs (avoid overwriting if active)
560
+ if (document.activeElement !== valH) valH.value = lon;
561
+ if (document.activeElement !== valV) valV.value = lat;
562
+ if (document.activeElement !== valD) valD.value = dist.toFixed(1);
563
+
564
+ const phi = THREE.MathUtils.degToRad(90 - lat);
565
+ const theta = THREE.MathUtils.degToRad(lon);
566
+
567
+ camera.position.x = dist * Math.sin(phi) * Math.sin(theta);
568
+ camera.position.y = dist * Math.cos(phi);
569
+ camera.position.z = dist * Math.sin(phi) * Math.cos(theta);
570
+ camera.lookAt(0, 0, 0);
571
+
572
+ // Real-time update prompt
573
+ updatePromptWithAngle(lon, lat, dist);
574
+ }
575
+
576
+ window.syncInput = (type) => {
577
+ if (type === 'h') {
578
+ let v = parseFloat(valH.value);
579
+ if (!isNaN(v)) sliderH.value = v;
580
+ } else if (type === 'v') {
581
+ let v = parseFloat(valV.value);
582
+ if (!isNaN(v)) sliderV.value = v;
583
+ } else if (type === 'd') {
584
+ let v = parseFloat(valD.value);
585
+ if (!isNaN(v)) sliderD.value = v;
586
+ }
587
+ window.updateCamera();
588
+ };
589
+
590
+ window.resetControl = (type) => {
591
+ if (type === 'h') {
592
+ sliderH.value = 0;
593
+ valH.value = 0;
594
+ } else if (type === 'v') {
595
+ sliderV.value = 0;
596
+ valV.value = 0;
597
+ } else if (type === 'd') {
598
+ sliderD.value = 4;
599
+ valD.value = 4;
600
+ }
601
+ window.updateCamera();
602
+ };
603
+
604
+ function updatePromptWithAngle(h, v, d) {
605
+ let parts = [];
606
+ if (h !== 0) {
607
+ const dir = h > 0 ? "向右" : "向左";
608
+ parts.push(`${dir}旋转${Math.abs(h)}度`);
609
+ }
610
+ if (v !== 0) {
611
+ const dir = v > 0 ? "俯视" : "仰视";
612
+ parts.push(`${dir}${Math.abs(v)}度`);
613
+ }
614
+
615
+ // Distance logic
616
+ let lensText = "";
617
+ if (d > 4) {
618
+ lensText = "使用广角镜头";
619
+ } else if (d < 4) {
620
+ lensText = "使用特写镜头";
621
+ }
622
+
623
+ // Removed "保持原位" default text to show placeholder
624
+
625
+ let resultText = "";
626
+ if (parts.length > 0) {
627
+ resultText = `将相机${parts.join(",")}`;
628
+ }
629
+
630
+ if (lensText) {
631
+ resultText += (resultText ? "," : "将相机") + lensText;
632
+ }
633
+
634
+ const promptInput = document.getElementById('promptInput');
635
+ let currentText = promptInput.value;
636
+
637
+ // Regex to find existing angle command (including lens info)
638
+ const regex = /将相机.*?(?=(\n|$))/g;
639
+
640
+ if (regex.test(currentText)) {
641
+ // Replace existing
642
+ promptInput.value = currentText.replace(regex, resultText);
643
+ } else {
644
+ // Append if not exists and resultText is not empty
645
+ if (resultText) {
646
+ if (currentText.trim()) {
647
+ promptInput.value = currentText.trim() + '\n' + resultText;
648
+ } else {
649
+ promptInput.value = resultText;
650
+ }
651
+ }
652
+ }
653
+ }
654
+
655
+ sliderH.addEventListener('input', window.updateCamera);
656
+ sliderV.addEventListener('input', window.updateCamera);
657
+ sliderD.addEventListener('input', window.updateCamera);
658
+ window.updateCamera();
659
+
660
+ // Animation Loop
661
+ function animate() {
662
+ requestAnimationFrame(animate);
663
+ renderer.render(scene, camera);
664
+ }
665
+ animate();
666
+
667
+ // Handle Resize
668
+ const resizeObserver = new ResizeObserver(() => {
669
+ const w = container.clientWidth;
670
+ const h = container.clientHeight;
671
+ camera.aspect = w / h;
672
+ camera.updateProjectionMatrix();
673
+ renderer.setSize(w, h);
674
+ });
675
+ resizeObserver.observe(container);
676
+
677
+ // Expose function to update texture
678
+ window.update3DTexture = (url) => {
679
+ new THREE.TextureLoader().load(url, (texture) => {
680
+ texture.colorSpace = THREE.SRGBColorSpace;
681
+
682
+ // Adjust plane aspect ratio to match image
683
+ const imageAspect = texture.image.width / texture.image.height;
684
+ cube.scale.set(1, 1 / imageAspect, 1);
685
+ if (imageAspect > 1) {
686
+ cube.scale.set(1, 1 / imageAspect, 1);
687
+ // Reset base scale to 3
688
+ cube.geometry.dispose();
689
+ cube.geometry = new THREE.PlaneGeometry(3, 3 / imageAspect);
690
+ cube.scale.set(1, 1, 1);
691
+ } else {
692
+ cube.geometry.dispose();
693
+ cube.geometry = new THREE.PlaneGeometry(3 * imageAspect, 3);
694
+ cube.scale.set(1, 1, 1);
695
+ }
696
+
697
+ cube.material = new THREE.MeshBasicMaterial({
698
+ map: texture,
699
+ side: THREE.DoubleSide
700
+ });
701
+ cube.material.needsUpdate = true;
702
+
703
+ // Show control panel (already visible, but keep logic safe)
704
+ document.getElementById('cameraControl').classList.remove('hidden');
705
+
706
+ // Force resize check after texture load
707
+ setTimeout(() => {
708
+ const w = container.clientWidth;
709
+ const h = container.clientHeight;
710
+ camera.aspect = w / h;
711
+ camera.updateProjectionMatrix();
712
+ renderer.setSize(w, h);
713
+ }, 100);
714
+ });
715
+ };
716
+ </script>
717
+
718
+ <script>
719
+ lucide.createIcons();
720
+
721
+ // --- Engine Switcher Logic (UI Only) ---
722
+ let currentEngine = 'local';
723
+ const ENGINE_MODE_KEY = 'angle_engine_mode';
724
+
725
+ window.switchEngine = function(mode) {
726
+ currentEngine = mode;
727
+ localStorage.setItem(ENGINE_MODE_KEY, mode);
728
+
729
+ const glider = document.getElementById('glider');
730
+ const localBtn = document.getElementById('modeLocal');
731
+ const cloudBtn = document.getElementById('modeCloud');
732
+
733
+ if (mode === 'local') {
734
+ glider.style.transform = 'translateX(0)';
735
+ localBtn.classList.add('active');
736
+ cloudBtn.classList.remove('active');
737
+ } else {
738
+ glider.style.transform = 'translateX(100%)';
739
+ cloudBtn.classList.add('active');
740
+ localBtn.classList.remove('active');
741
+ }
742
+ };
743
+
744
+ function generateUUID() {
745
+ if (typeof crypto !== 'undefined' && crypto.randomUUID) {
746
+ try { return crypto.randomUUID(); } catch (e) { }
747
+ }
748
+ return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function (c) {
749
+ var r = Math.random() * 16 | 0, v = c == 'x' ? r : (r & 0x3 | 0x8);
750
+ return v.toString(16);
751
+ });
752
+ }
753
+ const CLIENT_ID = localStorage.getItem("client_id") || generateUUID();
754
+ localStorage.setItem("client_id", CLIENT_ID);
755
+
756
+ let uploadedPath = "";
757
+ let uploadedFile = null; // Store raw file for cloud upload
758
+ let currentResult = null;
759
+ let allHistory = [];
760
+ let currentIndex = 0;
761
+ const PAGE_SIZE = 30;
762
+
763
+ const dropzone = document.getElementById('dropzone');
764
+ const fileInput = document.getElementById('fileInput');
765
+ const previewImg = document.getElementById('previewImg');
766
+ const promptInput = document.getElementById('promptInput');
767
+
768
+ dropzone.onclick = () => fileInput.click();
769
+ fileInput.onchange = (e) => handleFile(e.target.files[0]);
770
+
771
+ // Drag and Drop
772
+ dropzone.addEventListener('dragover', (e) => {
773
+ e.preventDefault();
774
+ dropzone.classList.add('border-black', 'bg-gray-50');
775
+ });
776
+ dropzone.addEventListener('dragleave', () => {
777
+ dropzone.classList.remove('border-black', 'bg-gray-50');
778
+ });
779
+ dropzone.addEventListener('drop', (e) => {
780
+ e.preventDefault();
781
+ dropzone.classList.remove('border-black', 'bg-gray-50');
782
+ if (e.dataTransfer.files && e.dataTransfer.files[0]) {
783
+ handleFile(e.dataTransfer.files[0]);
784
+ }
785
+ });
786
+
787
+ // Paste support
788
+ let isHovering = false;
789
+ dropzone.addEventListener('mouseenter', () => isHovering = true);
790
+ dropzone.addEventListener('mouseleave', () => isHovering = false);
791
+ window.addEventListener('paste', (e) => {
792
+ if (!isHovering) return;
793
+ const items = (e.clipboardData || e.originalEvent.clipboardData).items;
794
+ for (let item of items) {
795
+ if (item.kind === 'file' && item.type.startsWith('image/')) {
796
+ const file = item.getAsFile();
797
+ handleFile(file);
798
+ break;
799
+ }
800
+ }
801
+ });
802
+
803
+ async function handleFile(file) {
804
+ if (!file) return;
805
+ uploadedFile = file; // Store for cloud usage
806
+ const btn = document.getElementById('genBtn');
807
+ const btnText = document.getElementById('btnText');
808
+
809
+ btn.disabled = true;
810
+ btnText.innerText = "Uploading...";
811
+
812
+ const reader = new FileReader();
813
+ reader.onload = (e) => {
814
+ previewImg.src = e.target.result;
815
+ previewImg.classList.remove('hidden');
816
+ document.getElementById('uploadContent').classList.add('opacity-0');
817
+ document.getElementById('changeOverlay').classList.replace('hidden', 'flex');
818
+
819
+ // Automatically apply to 3D scene
820
+ if (window.update3DTexture) {
821
+ window.update3DTexture(e.target.result);
822
+ }
823
+ };
824
+ reader.readAsDataURL(file);
825
+
826
+ const formData = new FormData();
827
+ formData.append('files', file);
828
+ try {
829
+ const res = await fetch('/api/upload', { method: 'POST', body: formData });
830
+ const data = await res.json();
831
+ uploadedPath = data.files[0].comfy_name;
832
+ btn.disabled = false;
833
+ btnText.innerText = "Generate New Angle";
834
+ } catch (err) {
835
+ console.error("Upload error");
836
+ btnText.innerText = "Upload Failed";
837
+ btn.disabled = false;
838
+ }
839
+ }
840
+
841
+ function applyAngleToPrompt() {
842
+ const h = parseInt(document.getElementById('rotate-h').value);
843
+ const v = parseInt(document.getElementById('rotate-v').value);
844
+
845
+ let parts = [];
846
+ if (h !== 0) {
847
+ const dir = h > 0 ? "向右" : "向左";
848
+ parts.push(`${dir}旋转${Math.abs(h)}度`);
849
+ }
850
+ if (v !== 0) {
851
+ const dir = v > 0 ? "俯视" : "仰视";
852
+ parts.push(`${dir}${Math.abs(v)}度`);
853
+ }
854
+
855
+ if (parts.length === 0) {
856
+ parts.push("保持原位");
857
+ }
858
+
859
+ const resultText = `将相机${parts.join(",")}`;
860
+
861
+ const promptInput = document.getElementById('promptInput');
862
+ // Check if there is existing content, if so append new line
863
+ if (promptInput.value.trim()) {
864
+ promptInput.value += '\n' + resultText;
865
+ } else {
866
+ promptInput.value = resultText;
867
+ }
868
+
869
+ // Visual feedback
870
+ promptInput.style.transition = "0.2s";
871
+ promptInput.style.borderColor = "#000";
872
+ promptInput.style.boxShadow = "0 0 0 2px rgba(0,0,0,0.1)";
873
+ setTimeout(() => {
874
+ promptInput.style.borderColor = "";
875
+ promptInput.style.boxShadow = "";
876
+ }, 500);
877
+ }
878
+
879
+ async function runCloudTask() {
880
+ if (!uploadedFile) throw new Error("Please upload an image first");
881
+
882
+ // Get token from centralized management (Personal -> Global)
883
+ let token = localStorage.getItem('modelscope_api_token');
884
+
885
+ if (!token) {
886
+ try {
887
+ const res = await fetch('/api/config/token');
888
+ if (res.ok) {
889
+ const data = await res.json();
890
+ if (data.token) token = data.token;
891
+ }
892
+ } catch (e) {
893
+ console.warn("Failed to fetch global token", e);
894
+ }
895
+ }
896
+
897
+ if (!token) {
898
+ if (window.parent && typeof window.parent.openTokenModal === 'function') {
899
+ window.parent.openTokenModal();
900
+ // Allow the error to propagate so user sees the message
901
+ // const err = new Error("请先点击右上角设置 ModelScope Token");
902
+ // err.silent = true;
903
+ // throw err;
904
+ }
905
+ throw new Error("请先点击右上角设置 ModelScope Token");
906
+ }
907
+
908
+ // Convert image to Base64
909
+ const toBase64 = file => new Promise((resolve, reject) => {
910
+ const reader = new FileReader();
911
+ reader.readAsDataURL(file);
912
+ reader.onload = () => resolve(reader.result);
913
+ reader.onerror = error => reject(error);
914
+ });
915
+
916
+ const dataUri = await toBase64(uploadedFile);
917
+ console.log("DataURI generated, length:", dataUri.length);
918
+
919
+ // Submit task via dedicated Angle endpoint
920
+ // Get Client ID from parent if available
921
+ let clientId = null;
922
+ try {
923
+ if (window.parent && window.parent.CID) {
924
+ clientId = window.parent.CID;
925
+ }
926
+ } catch (e) { console.warn("Cannot access parent CID", e); }
927
+
928
+ const payload = {
929
+ "prompt": promptInput.value,
930
+ "api_key": token,
931
+ "type": "angle",
932
+ "model": "Qwen/Qwen-Image-Edit-2511",
933
+ "image_urls": [dataUri],
934
+ "client_id": clientId
935
+ };
936
+
937
+ // Reset Progress Bar
938
+ document.getElementById('cloud-progress-container').classList.add('hidden');
939
+ document.getElementById('cloud-progress-bar').style.width = '0%';
940
+ document.getElementById('cloud-percent').innerText = '0%';
941
+
942
+ let response;
943
+ try {
944
+ response = await fetch('/api/angle/generate', {
945
+ method: 'POST',
946
+ headers: { 'Content-Type': 'application/json' },
947
+ body: JSON.stringify(payload)
948
+ });
949
+ } catch (netErr) {
950
+ throw new Error(`Network Error: ${netErr.message}`);
951
+ }
952
+
953
+ // Handle Timeout / Continue Loop
954
+ while (response.ok) {
955
+ const data = await response.json();
956
+
957
+ // If success with URL
958
+ if (data.url) {
959
+ return { images: [data.url] };
960
+ }
961
+
962
+ // If timeout status, ask user
963
+ if (data.status === 'timeout') {
964
+ const taskId = data.task_id;
965
+ const userContinue = confirm("Cloud generation is taking longer than expected (300s). The queue might be full.\n\nDo you want to continue waiting?");
966
+
967
+ if (userContinue) {
968
+ // Call poll endpoint
969
+ const pollPayload = {
970
+ "task_id": taskId,
971
+ "api_key": token,
972
+ "client_id": clientId
973
+ };
974
+
975
+ // Update UI to show we are still waiting
976
+ updateCloudProgress({status: "Resuming...", progress: 0, total: 150});
977
+
978
+ response = await fetch('/api/angle/poll_status', {
979
+ method: 'POST',
980
+ headers: { 'Content-Type': 'application/json' },
981
+ body: JSON.stringify(pollPayload)
982
+ });
983
+ continue; // Loop back to check response
984
+ } else {
985
+ throw new Error("User cancelled waiting.");
986
+ }
987
+ }
988
+
989
+ // Unknown success response
990
+ throw new Error("Unknown response format");
991
+ }
992
+
993
+ if (!response.ok) {
994
+ const errText = await response.text();
995
+ // Try to parse JSON error if possible
996
+ try {
997
+ const errJson = JSON.parse(errText);
998
+ if (errJson.detail) throw new Error(errJson.detail);
999
+ } catch (e) {}
1000
+ throw new Error(`Generation Failed: ${errText}`);
1001
+ }
1002
+
1003
+ const data = await response.json();
1004
+ if (data.url) {
1005
+ return {
1006
+ images: [data.url]
1007
+ };
1008
+ } else {
1009
+ throw new Error("No image URL in response");
1010
+ }
1011
+ }
1012
+
1013
+ async function handleGenerate() {
1014
+ if (!uploadedPath && currentEngine === 'local') {
1015
+ // ... existing local check ...
1016
+ const dropzone = document.getElementById('dropzone');
1017
+ dropzone.style.transition = "0.2s";
1018
+ dropzone.style.borderColor = "#ef4444";
1019
+ dropzone.style.transform = "scale(0.98)";
1020
+ setTimeout(() => {
1021
+ dropzone.style.borderColor = "";
1022
+ dropzone.style.transform = "scale(1)";
1023
+ }, 300);
1024
+ return;
1025
+ }
1026
+
1027
+ if (!uploadedFile && currentEngine === 'cloud') {
1028
+ alert("Please upload an image first");
1029
+ return;
1030
+ }
1031
+
1032
+ // Allow manual prompt even if angle not applied, but require prompt input
1033
+ if (!promptInput.value.trim()) {
1034
+ promptInput.style.transition = "0.2s";
1035
+ promptInput.style.borderColor = "#ef4444";
1036
+ setTimeout(() => {
1037
+ promptInput.style.borderColor = "";
1038
+ }, 300);
1039
+ return;
1040
+ }
1041
+
1042
+ const btn = document.getElementById('genBtn');
1043
+ const btnText = document.getElementById('btnText');
1044
+
1045
+ btn.disabled = true;
1046
+ btn.style.backgroundColor = '#333';
1047
+ btn.innerHTML = `<i data-lucide="zap" class="w-4 h-4 text-yellow-400 fill-yellow-400 animate-pulse"></i><span class="tracking-[0.4em] text-[11px] uppercase">Processing...</span>`;
1048
+ lucide.createIcons();
1049
+
1050
+ document.getElementById('emptyState').classList.add('hidden');
1051
+ document.getElementById('outputImg').classList.add('hidden');
1052
+ document.getElementById('textResult').classList.add('hidden'); // Ensure text result is hidden
1053
+ document.getElementById('loadingState').classList.remove('hidden');
1054
+
1055
+ try {
1056
+ let data;
1057
+
1058
+ if (currentEngine === 'cloud') {
1059
+ // Cloud Logic
1060
+ data = await runCloudTask();
1061
+ } else {
1062
+ // Local Logic
1063
+ const seed = Math.floor(Math.random() * 1000000000000000);
1064
+ const res = await fetch('/api/generate', {
1065
+ method: 'POST',
1066
+ headers: { 'Content-Type': 'application/json' },
1067
+ body: JSON.stringify({
1068
+ workflow_json: "2511.json",
1069
+ params: {
1070
+ "31": { "image": uploadedPath },
1071
+ "11": { "prompt": promptInput.value },
1072
+ "14": { "seed": seed }
1073
+ },
1074
+ type: "angle",
1075
+ client_id: CLIENT_ID
1076
+ })
1077
+ });
1078
+ data = await res.json();
1079
+ if (data.error) throw new Error(data.error);
1080
+ if (!data.images?.length) throw new Error("No images returned");
1081
+ }
1082
+
1083
+ currentResult = data;
1084
+ const outputImg = document.getElementById('outputImg');
1085
+ const downloadBtn = document.getElementById('downloadBtn');
1086
+
1087
+ outputImg.src = data.images[0];
1088
+ outputImg.classList.remove('hidden');
1089
+ document.getElementById('loadingState').classList.add('hidden');
1090
+
1091
+ downloadBtn.href = data.images[0];
1092
+ downloadBtn.classList.remove('hidden');
1093
+ downloadBtn.download = `Angle-${Date.now()}.png`;
1094
+
1095
+ btn.style.backgroundColor = '';
1096
+ btn.innerHTML = `<i data-lucide="zap" class="w-4 h-4 text-yellow-400"></i><span id="btnText">Generate New Angle</span>`;
1097
+ btn.disabled = false;
1098
+ lucide.createIcons();
1099
+
1100
+ // Add to history
1101
+ renderImageCard({
1102
+ images: data.images,
1103
+ prompt: promptInput.value,
1104
+ timestamp: Date.now(),
1105
+ is_cloud: (currentEngine === 'cloud')
1106
+ }, true);
1107
+
1108
+ } catch (err) {
1109
+ console.error(err);
1110
+ btn.style.backgroundColor = '';
1111
+ btn.innerHTML = `<i data-lucide="zap" class="w-4 h-4 text-yellow-400"></i><span id="btnText">Generation Failed</span>`;
1112
+ lucide.createIcons();
1113
+ document.getElementById('loadingState').classList.add('hidden');
1114
+ document.getElementById('emptyState').classList.remove('hidden');
1115
+ btn.disabled = false;
1116
+ if (!err.silent) {
1117
+ alert(err.message);
1118
+ }
1119
+ }
1120
+ }
1121
+
1122
+ window.copyText = () => {
1123
+ const text = document.getElementById('generatedText').innerText;
1124
+ navigator.clipboard.writeText(text).then(() => {
1125
+ const btn = document.querySelector('#textResult button');
1126
+ const originalHTML = btn.innerHTML;
1127
+ btn.innerHTML = `<i data-lucide="check" class="w-3 h-3"></i> Copied`;
1128
+ setTimeout(() => {
1129
+ btn.innerHTML = originalHTML;
1130
+ lucide.createIcons();
1131
+ }, 2000);
1132
+ });
1133
+ };
1134
+
1135
+ // History Management
1136
+ function renderImageCard(data, isNew = false) {
1137
+ const masonry = document.getElementById('masonry');
1138
+ const imgUrl = data.images ? data.images[0] : '';
1139
+ if (!imgUrl) return;
1140
+
1141
+ const card = document.createElement('div');
1142
+ card.className = "masonry-item relative group cursor-pointer";
1143
+
1144
+ card.onclick = () => openLightbox(imgUrl);
1145
+
1146
+ // ModelScope Badge
1147
+ const isCloud = data.is_cloud || (imgUrl && imgUrl.includes('cloud_angle'));
1148
+ const badgeHtml = isCloud ? `
1149
+ <div class="absolute top-3 left-3 z-10">
1150
+ <img src="/static/modelscope.gif" class="h-4 w-auto object-contain bg-white/90 rounded-full p-0.5 shadow-sm">
1151
+ </div>
1152
+ ` : '';
1153
+
1154
+ card.innerHTML = `
1155
+ <img src="${imgUrl}" class="w-full h-full object-cover block transform group-hover:scale-105 transition-transform duration-[1.5s]">
1156
+ ${badgeHtml}
1157
+ <div class="absolute inset-0 bg-gradient-to-t from-black/80 via-transparent to-transparent opacity-0 group-hover:opacity-100 transition-all duration-300 p-6 flex flex-col justify-end pointer-events-none">
1158
+ <p class="text-white text-[10px] font-bold uppercase tracking-widest line-clamp-2">${data.prompt || "Angle Control"}</p>
1159
+ </div>
1160
+ `;
1161
+
1162
+ if (isNew) masonry.prepend(card);
1163
+ else masonry.appendChild(card);
1164
+ }
1165
+
1166
+ function loadNextPage() {
1167
+ const batch = allHistory.slice(currentIndex, currentIndex + PAGE_SIZE);
1168
+ if (batch.length === 0) {
1169
+ const el = document.getElementById('loadMoreTrigger');
1170
+ if (el) el.innerText = "End of Archive";
1171
+ return;
1172
+ }
1173
+ batch.forEach(item => renderImageCard(item, false));
1174
+ currentIndex += PAGE_SIZE;
1175
+ }
1176
+
1177
+ async function loadHistory() {
1178
+ try {
1179
+ const res = await fetch('/api/history?type=angle');
1180
+ const history = await res.json();
1181
+ if (history && Array.isArray(history)) {
1182
+ allHistory = history;
1183
+ document.getElementById('masonry').innerHTML = '';
1184
+ currentIndex = 0;
1185
+ loadNextPage();
1186
+ }
1187
+ } catch (e) { console.error(e); }
1188
+ }
1189
+
1190
+ // Lightbox
1191
+ function openLightbox(url) {
1192
+ const img = document.getElementById('lightboxImg');
1193
+ const resPill = document.getElementById('lightboxRes');
1194
+
1195
+ resPill.style.opacity = '0';
1196
+ img.src = url;
1197
+
1198
+ const lb = document.getElementById('lightbox');
1199
+ lb.classList.replace('hidden', 'flex');
1200
+ img.classList.remove('hidden');
1201
+ document.body.style.overflow = 'hidden';
1202
+
1203
+ const updateRes = () => {
1204
+ if (img.naturalWidth) {
1205
+ resPill.innerText = `${img.naturalWidth} x ${img.naturalHeight}`;
1206
+ resPill.style.opacity = '1';
1207
+ }
1208
+ };
1209
+
1210
+ img.onload = updateRes;
1211
+ if (img.complete) updateRes();
1212
+ }
1213
+
1214
+ function closeLightbox() {
1215
+ const lb = document.getElementById('lightbox');
1216
+ lb.classList.replace('flex', 'hidden');
1217
+ document.body.style.overflow = 'auto';
1218
+ }
1219
+
1220
+ function handleOutsideClick(e) {
1221
+ if (e.target.id === 'lightbox') closeLightbox();
1222
+ }
1223
+
1224
+ function downloadLightboxImage() {
1225
+ const imgUrl = document.getElementById('lightboxImg').src;
1226
+ const link = document.createElement('a');
1227
+ link.href = imgUrl;
1228
+ link.download = `Angle-Master-${Date.now()}.png`;
1229
+ document.body.appendChild(link);
1230
+ link.click();
1231
+ document.body.removeChild(link);
1232
+ }
1233
+
1234
+ function zoomImage() {
1235
+ if (currentResult && currentResult.images && currentResult.images[0]) {
1236
+ openLightbox(currentResult.images[0]);
1237
+ }
1238
+ }
1239
+
1240
+ // Init
1241
+ const observer = new IntersectionObserver((entries) => {
1242
+ if (entries[0].isIntersecting && allHistory.length > 0) {
1243
+ loadNextPage();
1244
+ }
1245
+ }, { threshold: 0.1 });
1246
+
1247
+ window.onload = () => {
1248
+ // Restore engine mode
1249
+ const savedMode = localStorage.getItem(ENGINE_MODE_KEY);
1250
+ if (savedMode && (savedMode === 'local' || savedMode === 'cloud')) {
1251
+ switchEngine(savedMode);
1252
+ }
1253
+
1254
+ loadHistory();
1255
+ observer.observe(document.getElementById('loadMoreTrigger'));
1256
+ };
1257
+
1258
+ // WebSocket for real-time updates (optional but good for multi-tab sync)
1259
+ const wsProtocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:';
1260
+ const wsUrl = `${wsProtocol}//${window.location.host}/ws/stats?client_id=${CLIENT_ID}`;
1261
+ const socket = new WebSocket(wsUrl);
1262
+ socket.onopen = () => {
1263
+ setInterval(() => {
1264
+ if (socket.readyState === WebSocket.OPEN) socket.send("ping");
1265
+ }, 30000);
1266
+ };
1267
+ </script>
1268
+ </body>
1269
+
1270
+ </html>
26-5-10-API-Studio/static/canvas.html ADDED
The diff for this file is too large to render. See raw diff
 
26-5-10-API-Studio/static/enhance.html ADDED
@@ -0,0 +1,881 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <!DOCTYPE html>
2
+ <html lang="zh-CN">
3
+
4
+ <head>
5
+ <meta charset="UTF-8">
6
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
7
+ <link rel="icon" href="/static/logo.png" type="image/png">
8
+ <title>Z-IMAGE | 极简影像重塑</title>
9
+ <script src="https://cdn.tailwindcss.com"></script>
10
+ <script src="https://unpkg.com/lucide@latest"></script>
11
+ <script src="/static/theme.js?v=20260509"></script>
12
+ <style>
13
+ @import url('https://fonts.googleapis.com/css2?family=Inter:wght@300;400;600;800&family=JetBrains+Mono:wght@400;700&display=swap');
14
+
15
+ :root {
16
+ --accent: #111827;
17
+ --bg: #f9fafb;
18
+ --card: #ffffff;
19
+ --easing: cubic-bezier(0.4, 0, 0.2, 1);
20
+ }
21
+
22
+ /* --- 极简悬浮浅灰滚动条 (无底色/左移) --- */
23
+ *::-webkit-scrollbar {
24
+ width: 10px !important;
25
+ height: 10px !important;
26
+ background: transparent !important;
27
+ }
28
+
29
+ *::-webkit-scrollbar-track {
30
+ background: transparent !important;
31
+ border: none !important;
32
+ }
33
+
34
+ *::-webkit-scrollbar-thumb {
35
+ background-color: #d8d8d8 !important;
36
+ border: 3px solid transparent !important;
37
+ border-right-width: 5px !important;
38
+ /* 增加右侧间距,使滚动条向左位移 */
39
+ background-clip: padding-box !important;
40
+ border-radius: 10px !important;
41
+ }
42
+
43
+ *::-webkit-scrollbar-thumb:hover {
44
+ background-color: #c0c0c0 !important;
45
+ }
46
+
47
+ *::-webkit-scrollbar-corner {
48
+ background: transparent !important;
49
+ }
50
+
51
+ * {
52
+ scrollbar-width: thin !important;
53
+ scrollbar-color: #d8d8d8 transparent !important;
54
+ }
55
+
56
+ body {
57
+ background-color: var(--bg);
58
+ font-family: 'Inter', -apple-system, sans-serif;
59
+ color: var(--accent);
60
+ -webkit-font-smoothing: antialiased;
61
+ }
62
+
63
+ .container-box {
64
+ max-width: 1280px;
65
+ margin: 0 auto;
66
+ padding: 0 40px;
67
+ margin-top: 50px;
68
+ }
69
+
70
+ /* 统一组件风格 */
71
+ .glass-btn {
72
+ background: #111827;
73
+ transition: all 0.3s var(--easing);
74
+ }
75
+
76
+ .glass-btn:hover {
77
+ background: #000;
78
+ transform: translateY(-1px);
79
+ box-shadow: 0 12px 24px rgba(0, 0, 0, 0.1);
80
+ }
81
+
82
+ .glass-btn:active {
83
+ transform: scale(0.98);
84
+ }
85
+
86
+ .upload-item {
87
+ background: var(--card);
88
+ border: 1px dashed #e2e8f0;
89
+ transition: all 0.4s var(--easing);
90
+ }
91
+
92
+ .upload-item:hover {
93
+ border-color: #000;
94
+ background: #fff;
95
+ transform: translateY(-2px);
96
+ }
97
+
98
+ .result-frame {
99
+ background: #ffffff;
100
+ border-radius: 32px;
101
+ border: 1px solid #f1f5f9;
102
+ box-shadow: 0 2px 15px rgba(0, 0, 0, 0.02);
103
+ }
104
+
105
+ .masonry-item {
106
+ break-inside: avoid;
107
+ margin-bottom: 1.25rem;
108
+ background: #fff;
109
+ border: 1px solid #f1f5f9;
110
+ border-radius: 24px;
111
+ overflow: hidden;
112
+ transition: all 0.5s var(--easing);
113
+ }
114
+
115
+ .masonry-item:hover {
116
+ transform: translateY(-6px);
117
+ box-shadow: 0 20px 40px rgba(0, 0, 0, 0.08);
118
+ }
119
+
120
+ /* 统一精致 Range 设计 */
121
+ input[type=range] {
122
+ -webkit-appearance: none;
123
+ background: transparent;
124
+ }
125
+
126
+ input[type=range]::-webkit-slider-runnable-track {
127
+ width: 100%;
128
+ height: 2px;
129
+ background: #e5e5e5;
130
+ border-radius: 1px;
131
+ }
132
+
133
+ input[type=range]::-webkit-slider-thumb {
134
+ -webkit-appearance: none;
135
+ height: 14px;
136
+ width: 14px;
137
+ border-radius: 50%;
138
+ background: var(--accent);
139
+ margin-top: -6px;
140
+ box-shadow: 0 1px 4px rgba(0, 0, 0, 0.2);
141
+ transition: transform 0.2s var(--easing);
142
+ cursor: pointer;
143
+ }
144
+
145
+ .masonry-grid {
146
+ columns: 2;
147
+ column-gap: 1.25rem;
148
+ }
149
+
150
+ @media (min-width: 768px) {
151
+ .masonry-grid {
152
+ columns: 4;
153
+ }
154
+ }
155
+
156
+ @keyframes b-loading {
157
+ 0% {
158
+ transform: scale(1);
159
+ background: #000;
160
+ }
161
+
162
+ 50% {
163
+ transform: scale(1.15);
164
+ background: #444;
165
+ }
166
+
167
+ 100% {
168
+ transform: scale(1);
169
+ background: #000;
170
+ }
171
+ }
172
+
173
+ .loading-box {
174
+ width: 10px;
175
+ height: 10px;
176
+ animation: b-loading 1s infinite var(--easing);
177
+ }
178
+
179
+ .ios-switch input:checked+.ios-slider {
180
+ background: #000;
181
+ }
182
+
183
+ .ios-slider:before {
184
+ content: "";
185
+ position: absolute;
186
+ height: 24px;
187
+ width: 24px;
188
+ left: 2px;
189
+ bottom: 2px;
190
+ background: white;
191
+ border-radius: 50%;
192
+ transition: 0.3s var(--easing);
193
+ box-shadow: 0 1px 3px rgba(0, 0, 0, 0.15);
194
+ }
195
+
196
+ input:checked+.ios-slider:before {
197
+ transform: translateX(20px);
198
+ }
199
+
200
+ .engine-switch {
201
+ background: #f8fafc;
202
+ border: 1px solid #edf2f7;
203
+ border-radius: 999px;
204
+ padding: 3px;
205
+ display: grid;
206
+ grid-template-columns: 1fr 1fr;
207
+ gap: 2px;
208
+ }
209
+
210
+ .engine-btn {
211
+ border-radius: 999px;
212
+ height: 34px;
213
+ display: flex;
214
+ align-items: center;
215
+ justify-content: center;
216
+ gap: 6px;
217
+ color: #64748b;
218
+ font-size: 10px;
219
+ font-weight: 600;
220
+ letter-spacing: .02em;
221
+ transition: all .25s var(--easing);
222
+ cursor: pointer;
223
+ }
224
+
225
+ .engine-btn.active {
226
+ background: #fff;
227
+ color: #111827;
228
+ box-shadow: 0 1px 3px rgba(15,23,42,.08);
229
+ }
230
+
231
+ .engine-btn:not(.active):hover {
232
+ color: #111827;
233
+ background: rgba(0,0,0,.04);
234
+ }
235
+
236
+ .engine-panel {
237
+ background: #fff;
238
+ border: 1px solid #edf0f3;
239
+ border-radius: 24px;
240
+ padding: 12px;
241
+ display: flex;
242
+ flex-direction: column;
243
+ gap: 10px;
244
+ }
245
+
246
+ .engine-label {
247
+ display: flex;
248
+ align-items: center;
249
+ gap: 6px;
250
+ color: #94a3b8;
251
+ font-size: 10px;
252
+ font-weight: 600;
253
+ letter-spacing: .04em;
254
+ }
255
+ </style>
256
+ <link rel="stylesheet" href="/static/theme.css?v=20260510-studio-pages-blue-dark10">
257
+ </head>
258
+
259
+ <body class="selection:bg-black selection:text-white">
260
+
261
+ <div class="container-box">
262
+ <header class="flex flex-col md:flex-row justify-between items-end mb-16 gap-6">
263
+ <div class="space-y-1">
264
+ <h1 class="text-4xl font-extrabold tracking-[-0.05em] flex items-center">
265
+ Z IMAGE<span class="text-base mt-3 ml-1">®</span>
266
+ </h1>
267
+ <p class="text-[10px] font-bold uppercase tracking-[0.5em] text-gray-400">Computational Photography
268
+ Archive</p>
269
+ </div>
270
+ <nav class="flex gap-8 text-[11px] font-bold uppercase tracking-widest text-gray-500">
271
+ <span class="text-black border-b-2 border-black pb-1">Enhance</span>
272
+ </nav>
273
+ </header>
274
+
275
+ <main class="grid grid-cols-1 lg:grid-cols-12 gap-12">
276
+ <div class="lg:col-span-4 space-y-10">
277
+ <section class="group">
278
+ <h3 class="text-[9px] font-black uppercase tracking-[0.3em] mb-5 text-gray-400">01. Input Source
279
+ </h3>
280
+ <div id="dropzone"
281
+ class="upload-item relative overflow-hidden rounded-2xl aspect-[4/3] flex flex-col items-center justify-center cursor-pointer">
282
+ <input type="file" id="fileInput" class="hidden" accept="image/*">
283
+
284
+ <div id="uploadContent" class="text-center space-y-4">
285
+ <div
286
+ class="w-14 h-14 rounded-full border border-gray-200 bg-white flex items-center justify-center mx-auto group-hover:bg-black group-hover:text-white group-hover:border-black transition-all duration-500">
287
+ <i data-lucide="arrow-up" class="w-5 h-5"></i>
288
+ </div>
289
+ <p class="text-[11px] font-bold uppercase tracking-tight">Drop image here</p>
290
+ </div>
291
+
292
+ <img id="previewImg" class="hidden absolute inset-0 w-full h-full object-cover">
293
+
294
+ <div id="changeOverlay"
295
+ class="hidden absolute inset-0 bg-black/10 backdrop-blur-sm items-center justify-center opacity-0 hover:opacity-100 transition-opacity">
296
+ <span
297
+ class="bg-white px-5 py-2 rounded-full text-[10px] font-bold uppercase tracking-widest shadow-2xl">Change</span>
298
+ </div>
299
+ </div>
300
+ </section>
301
+
302
+ <section class="space-y-10">
303
+ <h3 class="text-[9px] font-black uppercase tracking-[0.3em] text-gray-400">02. Parameters</h3>
304
+
305
+ <!-- Provider Toggle -->
306
+ <div class="engine-panel">
307
+ <div class="engine-label">
308
+ <i data-lucide="cpu" class="w-3.5 h-3.5"></i>
309
+ <span>Engine</span>
310
+ </div>
311
+ <div class="provider-switch engine-switch">
312
+ <button id="providerLocal" onclick="setEnhanceProvider('local')"
313
+ class="provider-btn engine-btn active">
314
+ <i data-lucide="cpu" class="w-3 h-3"></i>
315
+ Local
316
+ </button>
317
+ <button id="providerMs" onclick="setEnhanceProvider('ms')"
318
+ class="provider-btn engine-btn">
319
+ <i data-lucide="cloud" class="w-3 h-3"></i>
320
+ ModelScope
321
+ </button>
322
+ </div>
323
+ </div>
324
+
325
+ <!-- Local Controls -->
326
+ <div id="localControls" class="space-y-10">
327
+ <div class="space-y-5">
328
+ <div class="flex justify-between items-end">
329
+ <label class="text-xs font-bold uppercase tracking-tight text-gray-800">Refinement
330
+ Strength</label>
331
+ <span id="strengthVal" class="font-mono text-xs font-bold">0.50</span>
332
+ </div>
333
+ <input type="range" id="strengthSlider" min="0.1" max="1.0" step="0.01" value="0.5"
334
+ class="w-full">
335
+ </div>
336
+
337
+ <div class="p-5 bg-white border border-gray-200/50 rounded-2xl shadow-sm">
338
+ <div class="flex items-center justify-between">
339
+ <div class="flex items-center gap-4">
340
+ <div class="p-2.5 bg-gray-50 rounded-xl border border-gray-100"><i data-lucide="layers"
341
+ class="w-4 h-4"></i></div>
342
+ <div>
343
+ <p class="text-[11px] font-bold uppercase">Super Resolution</p>
344
+ <p class="text-[9px] text-gray-400">Double pixels (4K)</p>
345
+ </div>
346
+ </div>
347
+ <label class="ios-switch relative inline-block w-12 h-7">
348
+ <input type="checkbox" id="upscaleToggle" class="opacity-0 w-0 h-0"
349
+ onchange="toggleUpscaleOptions()">
350
+ <span
351
+ class="ios-slider absolute inset-0 cursor-pointer bg-gray-200 rounded-full transition-colors duration-300"></span>
352
+ </label>
353
+ </div>
354
+ <div id="upscaleOptions" class="hidden pt-4 mt-4 border-t border-gray-100 grid-cols-2 gap-3">
355
+ <button id="btn2x" onclick="setUpscaleFactor(2048)"
356
+ class="py-2.5 rounded-xl border border-black bg-black text-white text-[10px] font-bold transition-all">
357
+ 2x (2K)
358
+ </button>
359
+ <button id="btn4x" onclick="setUpscaleFactor(4096)"
360
+ class="py-2.5 rounded-xl border border-gray-200 text-gray-400 text-[10px] font-bold hover:border-black hover:text-black transition-all">
361
+ 4x (4K)
362
+ </button>
363
+ </div>
364
+ </div>
365
+ </div>
366
+
367
+ <!-- MS Cloud Controls -->
368
+ <div id="msControls" class="hidden space-y-5">
369
+ <div class="p-5 bg-white border border-gray-200/50 rounded-2xl shadow-sm space-y-4">
370
+ <div class="flex items-center gap-3">
371
+ <div class="p-2.5 bg-gray-50 rounded-xl border border-gray-100"><i data-lucide="sparkles" class="w-4 h-4"></i></div>
372
+ <div>
373
+ <p class="text-[11px] font-bold uppercase">Klein Detail Enhance</p>
374
+ <p class="text-[9px] text-gray-400">Cloud · LoRA: Daniel8152/Klein-enhance</p>
375
+ </div>
376
+ </div>
377
+ <div>
378
+ <div class="flex justify-between items-end mb-2">
379
+ <label class="text-[10px] font-bold uppercase tracking-tight text-gray-600">Enhancement Strength</label>
380
+ <span id="msStrengthVal" class="font-mono text-xs font-bold">0.80</span>
381
+ </div>
382
+ <input type="range" id="msStrengthSlider" min="0.1" max="1.0" step="0.05" value="0.8" class="w-full">
383
+ </div>
384
+ <div>
385
+ <label class="text-[10px] font-bold uppercase tracking-tight text-gray-600 block mb-2">Prompt (optional)</label>
386
+ <textarea id="msPromptInput" rows="2"
387
+ class="w-full border border-gray-200 rounded-xl p-3 text-[12px] resize-none outline-none focus:border-black transition-colors"
388
+ placeholder="masterpiece, best quality, ultra-detailed, high resolution"></textarea>
389
+ </div>
390
+ </div>
391
+ </div>
392
+
393
+ <button id="genBtn" onclick="handleGenerate()"
394
+ class="glass-btn w-full py-5 text-white rounded-xl font-bold text-[11px] uppercase tracking-[0.4em] flex items-center justify-center gap-3 shadow-xl shadow-black/10 disabled:opacity-50 disabled:cursor-not-allowed">
395
+ <i data-lucide="zap" id="btnIcon" class="w-4 h-4 text-yellow-400"></i>
396
+ <span id="btnText">Begin Remastering</span>
397
+ </button>
398
+ </section>
399
+ </div>
400
+
401
+ <div class="lg:col-span-8">
402
+ <div id="resultBox"
403
+ class="result-frame relative h-[500px] lg:h-[650px] flex items-center justify-center overflow-hidden group">
404
+ <div id="emptyState" class="text-center space-y-4 opacity-20">
405
+ <i data-lucide="layout" class="w-12 h-12 mx-auto stroke-[1px]"></i>
406
+ <p class="text-[10px] font-black tracking-[0.5em] uppercase">Canvas Ready</p>
407
+ </div>
408
+
409
+ <div id="loadingState" class="hidden flex flex-col items-center gap-5">
410
+ <div class="loading-box"></div>
411
+ <p class="text-[10px] font-bold uppercase tracking-[0.4em] animate-pulse">Computing pixels...
412
+ </p>
413
+ </div>
414
+
415
+ <img id="outputImg"
416
+ class="hidden w-full h-full object-contain p-8 cursor-zoom-in transition-all duration-700 hover:scale-[1.02]"
417
+ onclick="zoomImage()">
418
+
419
+ <a id="downloadBtn" href="#" download
420
+ class="hidden absolute bottom-8 right-8 w-14 h-14 bg-white shadow-2xl rounded-2xl flex items-center justify-center hover:bg-black hover:text-white transition-all duration-500 border border-gray-100">
421
+ <i data-lucide="download" class="w-5 h-5"></i>
422
+ </a>
423
+ </div>
424
+ </div>
425
+ </main>
426
+
427
+ <section class="mt-32">
428
+ <div class="flex items-center gap-6 mb-10">
429
+ <h2 class="text-[11px] font-black uppercase tracking-[0.5em]">Archive</h2>
430
+ <div class="h-px flex-1 bg-black/5"></div>
431
+ </div>
432
+ <div id="masonry" class="masonry-grid"></div>
433
+ <div id="loadMoreTrigger"
434
+ class="py-16 text-center opacity-20 text-[10px] font-bold uppercase tracking-widest">
435
+ End of Archive
436
+ </div>
437
+ </section>
438
+ </div>
439
+
440
+ <div id="lightbox" onclick="handleOutsideClick(event)"
441
+ class="hidden fixed inset-0 z-50 bg-white/95 backdrop-blur-3xl flex items-center justify-center p-8">
442
+ <button onclick="closeLightbox()"
443
+ class="absolute top-10 right-10 p-2 hover:rotate-90 transition-transform duration-500">
444
+ <i data-lucide="x" class="w-8 h-8"></i>
445
+ </button>
446
+
447
+ <div class="max-w-6xl w-full h-full flex flex-col items-center justify-center">
448
+ <div class="relative w-full flex justify-center">
449
+ <div id="compareContainer"
450
+ class="hidden relative w-full h-[75vh] rounded-3xl overflow-hidden bg-[#f8f8f9] border border-gray-200/50 shadow-2xl">
451
+ <img id="compareGenerated" class="absolute inset-0 w-full h-full object-contain">
452
+ <div id="compareOriginalWrapper"
453
+ class="absolute inset-0 w-full h-full overflow-hidden border-r-2 border-white/80">
454
+ <img id="compareOriginal" class="absolute inset-0 w-full h-full object-contain">
455
+ </div>
456
+ <div id="compareSlider" class="absolute inset-y-0 left-1/2 w-0.5 bg-white z-20 cursor-ew-resize">
457
+ <div
458
+ class="absolute top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2 w-10 h-10 bg-white shadow-2xl rounded-full flex items-center justify-center border border-gray-100">
459
+ <i data-lucide="move-horizontal" class="w-4 h-4 text-black"></i>
460
+ </div>
461
+ </div>
462
+ </div>
463
+ <img id="lightboxImg" src="" class="hidden max-h-[80vh] rounded-3xl shadow-2xl">
464
+ <div id="lightboxRes"
465
+ class="absolute top-4 left-4 bg-black/30 backdrop-blur-md border border-white/20 text-white px-3 py-1.5 rounded-full text-[10px] font-medium tracking-wider opacity-0 transition-opacity duration-300 pointer-events-none z-30">
466
+ </div>
467
+ </div>
468
+ <div class="mt-8">
469
+ <button onclick="downloadLightboxImage()"
470
+ class="px-10 py-4 bg-black text-white rounded-full text-[10px] font-black uppercase tracking-widest flex items-center gap-3 shadow-xl">
471
+ <i data-lucide="save" class="w-4 h-4"></i> Save Master
472
+ </button>
473
+ </div>
474
+ </div>
475
+ </div>
476
+
477
+ <script>
478
+ lucide.createIcons();
479
+ function generateUUID() {
480
+ if (typeof crypto !== 'undefined' && crypto.randomUUID) {
481
+ try { return crypto.randomUUID(); } catch (e) { }
482
+ }
483
+ return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function (c) {
484
+ var r = Math.random() * 16 | 0, v = c == 'x' ? r : (r & 0x3 | 0x8);
485
+ return v.toString(16);
486
+ });
487
+ }
488
+ const CLIENT_ID = localStorage.getItem("client_id") || generateUUID();
489
+ localStorage.setItem("client_id", CLIENT_ID);
490
+
491
+ let uploadedPath = "";
492
+ let currentResult = null;
493
+ let currentUpscaleFactor = 2048;
494
+ let enhanceProvider = 'local';
495
+
496
+ function setEnhanceProvider(p) {
497
+ enhanceProvider = p;
498
+ document.getElementById('providerLocal').className = p === 'local'
499
+ ? 'provider-btn engine-btn active'
500
+ : 'provider-btn engine-btn';
501
+ document.getElementById('providerMs').className = p === 'ms'
502
+ ? 'provider-btn engine-btn active'
503
+ : 'provider-btn engine-btn';
504
+ document.getElementById('localControls').classList.toggle('hidden', p !== 'local');
505
+ document.getElementById('msControls').classList.toggle('hidden', p !== 'ms');
506
+ lucide.createIcons();
507
+ }
508
+
509
+ const dropzone = document.getElementById('dropzone');
510
+ const fileInput = document.getElementById('fileInput');
511
+ const previewImg = document.getElementById('previewImg');
512
+ const slider = document.getElementById('strengthSlider');
513
+ const valDisplay = document.getElementById('strengthVal');
514
+
515
+ function setUpscaleFactor(factor) {
516
+ currentUpscaleFactor = factor;
517
+ const btn2x = document.getElementById('btn2x');
518
+ const btn4x = document.getElementById('btn4x');
519
+
520
+ // Helper to set active/inactive styles
521
+ const setActive = (btn) => {
522
+ btn.className = "py-2.5 rounded-xl border border-black bg-black text-white text-[10px] font-bold transition-all";
523
+ };
524
+ const setInactive = (btn) => {
525
+ btn.className = "py-2.5 rounded-xl border border-gray-200 text-gray-400 text-[10px] font-bold hover:border-black hover:text-black transition-all";
526
+ };
527
+
528
+ if (factor === 2048) {
529
+ setActive(btn2x);
530
+ setInactive(btn4x);
531
+ } else {
532
+ setInactive(btn2x);
533
+ setActive(btn4x);
534
+ }
535
+ }
536
+
537
+ dropzone.onclick = () => fileInput.click();
538
+ fileInput.onchange = (e) => handleFile(e.target.files[0]);
539
+
540
+ // Drag and Drop
541
+ dropzone.addEventListener('dragover', (e) => {
542
+ e.preventDefault();
543
+ dropzone.classList.add('border-black', 'bg-gray-50');
544
+ });
545
+ dropzone.addEventListener('dragleave', () => {
546
+ dropzone.classList.remove('border-black', 'bg-gray-50');
547
+ });
548
+ dropzone.addEventListener('drop', (e) => {
549
+ e.preventDefault();
550
+ dropzone.classList.remove('border-black', 'bg-gray-50');
551
+ if (e.dataTransfer.files && e.dataTransfer.files[0]) {
552
+ handleFile(e.dataTransfer.files[0]);
553
+ }
554
+ });
555
+
556
+ // Paste support
557
+ let isHovering = false;
558
+ dropzone.addEventListener('mouseenter', () => isHovering = true);
559
+ dropzone.addEventListener('mouseleave', () => isHovering = false);
560
+ window.addEventListener('paste', (e) => {
561
+ if (!isHovering) return;
562
+ const items = (e.clipboardData || e.originalEvent.clipboardData).items;
563
+ for (let item of items) {
564
+ if (item.kind === 'file' && item.type.startsWith('image/')) {
565
+ const file = item.getAsFile();
566
+ handleFile(file);
567
+ break;
568
+ }
569
+ }
570
+ });
571
+
572
+ slider.oninput = function () {
573
+ valDisplay.innerText = parseFloat(this.value).toFixed(2);
574
+ };
575
+
576
+ document.getElementById('msStrengthSlider').oninput = function () {
577
+ document.getElementById('msStrengthVal').innerText = parseFloat(this.value).toFixed(2);
578
+ };
579
+
580
+ async function handleFile(file) {
581
+ if (!file) return;
582
+ const btn = document.getElementById('genBtn');
583
+ const btnText = document.getElementById('btnText');
584
+
585
+ // Disable button during upload
586
+ btn.disabled = true;
587
+ btnText.innerText = "Uploading...";
588
+
589
+ const reader = new FileReader();
590
+ reader.onload = (e) => {
591
+ previewImg.src = e.target.result;
592
+ previewImg.classList.remove('hidden');
593
+ document.getElementById('uploadContent').classList.add('opacity-0');
594
+ document.getElementById('changeOverlay').classList.replace('hidden', 'flex');
595
+ };
596
+ reader.readAsDataURL(file);
597
+
598
+ const formData = new FormData();
599
+ formData.append('files', file);
600
+ try {
601
+ const res = await fetch('/api/upload', { method: 'POST', body: formData });
602
+ const data = await res.json();
603
+ uploadedPath = data.files[0].comfy_name;
604
+ // Enable button after successful upload
605
+ btn.disabled = false;
606
+ btnText.innerText = "Begin Remastering";
607
+ } catch (err) {
608
+ console.error("Upload error");
609
+ btnText.innerText = "Upload Failed";
610
+ btn.disabled = false;
611
+ }
612
+ }
613
+
614
+ function toggleUpscaleOptions() {
615
+ const toggle = document.getElementById('upscaleToggle');
616
+ const options = document.getElementById('upscaleOptions');
617
+ if (toggle.checked) {
618
+ options.classList.remove('hidden');
619
+ options.classList.add('grid');
620
+ } else {
621
+ options.classList.add('hidden');
622
+ options.classList.remove('grid');
623
+ }
624
+ }
625
+
626
+ async function handleGenerate() {
627
+ if (!uploadedPath) {
628
+ const dropzone = document.getElementById('dropzone');
629
+ dropzone.style.transition = "0.2s";
630
+ dropzone.style.borderColor = "#ef4444";
631
+ dropzone.style.transform = "scale(0.98)";
632
+ setTimeout(() => { dropzone.style.borderColor = ""; dropzone.style.transform = "scale(1)"; }, 300);
633
+ return;
634
+ }
635
+ const btn = document.getElementById('genBtn');
636
+ btn.disabled = true;
637
+ btn.style.backgroundColor = '#333';
638
+ btn.innerHTML = `<i data-lucide="zap" class="w-4 h-4 text-yellow-400 fill-yellow-400 animate-pulse"></i><span class="tracking-[0.4em] text-[11px] uppercase">Processing...</span>`;
639
+ lucide.createIcons();
640
+ document.getElementById('emptyState').classList.add('hidden');
641
+ document.getElementById('outputImg').classList.add('hidden');
642
+ document.getElementById('loadingState').classList.remove('hidden');
643
+
644
+ let debugStep = "Start";
645
+ try {
646
+ let finalData;
647
+
648
+ if (enhanceProvider === 'ms') {
649
+ // MS Cloud: Klein model + enhance LoRA
650
+ debugStep = "MS Cloud Request";
651
+ const previewSrc = document.getElementById('previewImg').src;
652
+ if (!previewSrc || previewSrc === window.location.href) throw new Error("请先上传图片");
653
+ const strength = parseFloat(document.getElementById('msStrengthSlider').value) || 0.8;
654
+ const prompt = document.getElementById('msPromptInput').value.trim()
655
+ || 'masterpiece, best quality, ultra-detailed, high resolution';
656
+ const msRes = await fetch('/api/ms/generate', {
657
+ method: 'POST',
658
+ headers: { 'Content-Type': 'application/json' },
659
+ body: JSON.stringify({
660
+ prompt,
661
+ model: 'black-forest-labs/FLUX.2-klein-9B',
662
+ image_urls: [previewSrc],
663
+ loras: { 'Daniel8152/Klein-enhance': strength },
664
+ client_id: CLIENT_ID
665
+ })
666
+ });
667
+ if (!msRes.ok) {
668
+ const err = await msRes.json().catch(() => ({}));
669
+ throw new Error(err.detail || 'MS生成失败');
670
+ }
671
+ const msData = await msRes.json();
672
+ if (!msData.url) throw new Error("MS生成失败: 未返回图片");
673
+ finalData = { images: [msData.url], timestamp: Date.now(), params: { "15": { "image": uploadedPath } } };
674
+
675
+ } else {
676
+ // Local ComfyUI
677
+ const isUpscale = document.getElementById('upscaleToggle').checked;
678
+
679
+ debugStep = "Phase 1 Request";
680
+ btn.innerHTML = `<i data-lucide="zap" class="w-4 h-4 text-yellow-400 fill-yellow-400 animate-pulse"></i><span class="tracking-[0.4em] text-[11px] uppercase">${isUpscale ? "Phase 1/2: Enhancing..." : "Processing..."}</span>`;
681
+ lucide.createIcons();
682
+
683
+ const enhanceRes = await fetch('/api/generate', {
684
+ method: 'POST',
685
+ headers: { 'Content-Type': 'application/json' },
686
+ body: JSON.stringify({
687
+ workflow_json: "Z-Image-Enhance.json",
688
+ params: { "15": { "image": uploadedPath }, "204": { "value": parseFloat(slider.value) } },
689
+ type: "enhance",
690
+ client_id: CLIENT_ID
691
+ })
692
+ });
693
+ const enhanceData = await enhanceRes.json();
694
+ if (enhanceData.error) throw new Error("Enhance API Error: " + enhanceData.error);
695
+ if (!enhanceData.images?.length) throw new Error("Enhance failed: No images returned");
696
+
697
+ finalData = enhanceData;
698
+
699
+ if (isUpscale) {
700
+ debugStep = "Phase 2 Preparation";
701
+ btn.innerHTML = `<i data-lucide="zap" class="w-4 h-4 text-yellow-400 fill-yellow-400 animate-pulse"></i><span class="tracking-[0.4em] text-[11px] uppercase">Phase 2/2: Uploading...</span>`;
702
+ lucide.createIcons();
703
+
704
+ const imgUrl = enhanceData.images[0];
705
+ let imgBlob;
706
+ try {
707
+ const blobRes = await fetch(imgUrl);
708
+ if (!blobRes.ok) throw new Error(`Fetch failed status: ${blobRes.status}`);
709
+ imgBlob = await blobRes.blob();
710
+ } catch (e) { throw new Error(`Failed to fetch Phase 1 image: ${e.message}`); }
711
+
712
+ debugStep = "Phase 2 Upload";
713
+ const formData = new FormData();
714
+ formData.append('files', imgBlob, 'temp_upscale_input.png');
715
+ const uploadRes = await fetch('/api/upload', { method: 'POST', body: formData });
716
+ if (!uploadRes.ok) throw new Error(`Upload failed status: ${uploadRes.status}`);
717
+ const uploadData = await uploadRes.json();
718
+ if (!uploadData.files?.[0]) throw new Error("Intermediate upload failed");
719
+ const uploadedInput = uploadData.files[0].comfy_name;
720
+
721
+ debugStep = "Phase 2 Execution";
722
+ btn.innerHTML = `<i data-lucide="zap" class="w-4 h-4 text-yellow-400 fill-yellow-400 animate-pulse"></i><span class="tracking-[0.4em] text-[11px] uppercase">Phase 2/2: Upscaling...</span>`;
723
+ lucide.createIcons();
724
+ const seed = Math.floor(Math.random() * 4294967295);
725
+ const upscaleRes = await fetch('/api/generate', {
726
+ method: 'POST',
727
+ headers: { 'Content-Type': 'application/json' },
728
+ body: JSON.stringify({
729
+ workflow_json: "upscale.json",
730
+ params: { "15": { "image": uploadedInput }, "172": { "seed": seed, "resolution": currentUpscaleFactor } },
731
+ type: "enhance",
732
+ client_id: CLIENT_ID
733
+ })
734
+ });
735
+ finalData = await upscaleRes.json();
736
+ if (finalData.error) throw new Error("Upscale API Error: " + finalData.error);
737
+ if (!finalData.images?.length) throw new Error("Upscale failed: No images returned");
738
+ }
739
+ }
740
+
741
+ currentResult = finalData;
742
+ const out = document.getElementById('outputImg');
743
+ out.src = finalData.images[0];
744
+ out.classList.remove('hidden');
745
+ document.getElementById('downloadBtn').classList.remove('hidden');
746
+ document.getElementById('downloadBtn').href = finalData.images[0];
747
+ document.getElementById('loadingState').classList.add('hidden');
748
+ renderImageCard(finalData, true);
749
+
750
+ } catch (error) {
751
+ console.error("Generation Error at step " + debugStep, error);
752
+ document.getElementById('emptyState').classList.remove('hidden');
753
+ document.getElementById('loadingState').classList.add('hidden');
754
+ alert(`Error at ${debugStep}: ${error.message || JSON.stringify(error)}`);
755
+ } finally {
756
+ btn.disabled = false;
757
+ btn.style.backgroundColor = '';
758
+ btn.innerHTML = `<i data-lucide="zap" class="w-4 h-4 text-yellow-400"></i><span id="btnText">Begin Remastering</span>`;
759
+ lucide.createIcons();
760
+ }
761
+ }
762
+
763
+ function zoomImage() {
764
+ if (currentResult) {
765
+ // Ensure comparison works by injecting params if missing
766
+ if (!currentResult.params && uploadedPath) {
767
+ currentResult.params = { "15": { "image": uploadedPath } };
768
+ }
769
+ openLightbox(currentResult);
770
+ }
771
+ }
772
+
773
+ function renderImageCard(data, isNew = false) {
774
+ const masonry = document.getElementById('masonry');
775
+ if (document.getElementById(`h-${data.timestamp}`)) return;
776
+
777
+ const card = document.createElement('div');
778
+ card.id = `h-${data.timestamp}`;
779
+ card.className = 'masonry-item group relative rounded-2xl overflow-hidden cursor-zoom-in';
780
+ card.onclick = () => openLightbox(data);
781
+
782
+ card.innerHTML = `
783
+ <img src="${data.images[0]}" class="w-full h-auto block transform group-hover:scale-105 transition-transform duration-1000">
784
+ <div class="absolute inset-0 bg-gradient-to-t from-black/60 to-transparent opacity-0 group-hover:opacity-100 transition-opacity flex flex-col justify-end p-5">
785
+ <p class="text-white text-[9px] font-black uppercase tracking-widest">Remaster Archive</p>
786
+ </div>
787
+ `;
788
+ isNew ? masonry.prepend(card) : masonry.appendChild(card);
789
+ }
790
+
791
+ function initCompareEvents() {
792
+ const container = document.getElementById('compareContainer');
793
+ const wrapper = document.getElementById('compareOriginalWrapper');
794
+ const slider = document.getElementById('compareSlider');
795
+ let isDragging = false;
796
+
797
+ const updateSlider = (clientX) => {
798
+ const rect = container.getBoundingClientRect();
799
+ let x = clientX - rect.left;
800
+ let percent = (x / rect.width) * 100;
801
+ percent = Math.max(0, Math.min(100, percent));
802
+ wrapper.style.clipPath = `inset(0 ${100 - percent}% 0 0)`;
803
+ slider.style.left = `${percent}%`;
804
+ };
805
+
806
+ const start = (e) => { isDragging = true; e.preventDefault(); };
807
+ const end = () => isDragging = false;
808
+ const move = (e) => {
809
+ if (!isDragging) return;
810
+ const clientX = e.touches ? e.touches[0].clientX : e.clientX;
811
+ updateSlider(clientX);
812
+ };
813
+
814
+ container.addEventListener('mousedown', (e) => { if (e.target === slider) return; updateSlider(e.clientX); start(e); });
815
+ slider.addEventListener('mousedown', start);
816
+ window.addEventListener('mouseup', end);
817
+ window.addEventListener('mousemove', move);
818
+ slider.addEventListener('touchstart', start, { passive: false });
819
+ window.addEventListener('touchend', end);
820
+ window.addEventListener('touchmove', move, { passive: false });
821
+ }
822
+
823
+ function openLightbox(data) {
824
+ const compare = document.getElementById('compareContainer');
825
+ const single = document.getElementById('lightboxImg');
826
+ const resPill = document.getElementById('lightboxRes');
827
+
828
+ resPill.style.opacity = '0';
829
+ const updateRes = (target) => {
830
+ if (target.naturalWidth) {
831
+ resPill.innerText = `${target.naturalWidth} x ${target.naturalHeight}`;
832
+ resPill.style.opacity = '1';
833
+ }
834
+ };
835
+
836
+ if (data.params?.["15"]?.image) {
837
+ compare.classList.remove('hidden');
838
+ single.classList.add('hidden');
839
+ const genImg = document.getElementById('compareGenerated');
840
+ document.getElementById('compareOriginal').src = `/api/view?filename=${encodeURIComponent(data.params["15"].image)}&type=input`;
841
+ genImg.src = data.images[0];
842
+ document.getElementById('compareOriginalWrapper').style.clipPath = 'inset(0 50% 0 0)';
843
+ document.getElementById('compareSlider').style.left = '50%';
844
+
845
+ genImg.onload = () => updateRes(genImg);
846
+ if (genImg.complete) updateRes(genImg);
847
+ } else {
848
+ compare.classList.add('hidden');
849
+ single.classList.remove('hidden');
850
+ single.src = data.images[0];
851
+
852
+ single.onload = () => updateRes(single);
853
+ if (single.complete) updateRes(single);
854
+ }
855
+ document.getElementById('lightbox').classList.replace('hidden', 'flex');
856
+ document.body.style.overflow = 'hidden';
857
+ }
858
+
859
+ function closeLightbox() {
860
+ document.getElementById('lightbox').classList.replace('flex', 'hidden');
861
+ document.body.style.overflow = 'auto';
862
+ }
863
+
864
+ function handleOutsideClick(e) { if (e.target.id === 'lightbox') closeLightbox(); }
865
+
866
+ async function loadHistory() {
867
+ try {
868
+ const res = await fetch('/api/history?type=enhance');
869
+ const history = await res.json();
870
+ history.forEach(item => renderImageCard(item));
871
+ } catch (e) { }
872
+ }
873
+
874
+ window.onload = () => {
875
+ loadHistory();
876
+ initCompareEvents();
877
+ };
878
+ </script>
879
+ </body>
880
+
881
+ </html>
26-5-10-API-Studio/static/gpt-chat.html ADDED
@@ -0,0 +1,538 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <!DOCTYPE html>
2
+ <html lang="zh-CN">
3
+ <head>
4
+ <meta charset="UTF-8">
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
+ <link rel="icon" href="/static/logo.png" type="image/png">
7
+ <title>GPT 对话</title>
8
+ <script src="https://cdn.tailwindcss.com"></script>
9
+ <script src="https://unpkg.com/lucide@latest"></script>
10
+ <script src="/static/theme.js?v=20260509-global-theme-output-drag"></script>
11
+ <style>
12
+ @import url('https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;800&display=swap');
13
+ :root { --ease:cubic-bezier(.4,0,.2,1); --content-width:1160px; }
14
+ * { box-sizing:border-box; }
15
+ body { margin:0; height:100vh; overflow:hidden; background:#f8fafc; color:#111827; font-family:'Inter',-apple-system,sans-serif; -webkit-font-smoothing:antialiased; }
16
+ .chat-shell { height:100vh; display:flex; flex-direction:column; min-width:0; }
17
+ .topbar { height:82px; padding:0 36px; border-bottom:1px solid #eef2f7; background:rgba(248,250,252,.88); backdrop-filter:blur(16px); z-index:5; }
18
+ .topbar-inner { width:min(var(--content-width),100%); height:100%; margin:0 auto; display:flex; align-items:center; justify-content:space-between; gap:18px; }
19
+ .title-block { min-width:0; }
20
+ .title-block h1 { font-size:14px; font-weight:900; letter-spacing:.28em; text-transform:uppercase; white-space:nowrap; overflow:hidden; text-overflow:ellipsis; }
21
+ .title-block p { margin-top:5px; font-size:10px; color:#94a3b8; font-weight:800; }
22
+ .top-actions { display:flex; align-items:center; gap:10px; position:relative; }
23
+ .action-btn { height:40px; min-width:40px; padding:0 12px; border-radius:999px; display:flex; align-items:center; justify-content:center; gap:8px; background:#fff; border:1px solid #e8edf3; color:#475569; font-size:10px; font-weight:900; letter-spacing:.14em; transition:all .2s var(--ease); }
24
+ .action-btn:hover { background:#f1f5f9; color:#111827; border-color:#dbe3ec; }
25
+ .history-popover { position:absolute; top:54px; right:0; width:min(360px,calc(100vw - 36px)); max-height:520px; overflow:hidden; background:#fff; border:1px solid #e8edf3; border-radius:24px; box-shadow:0 24px 60px rgba(15,23,42,.14); padding:12px; display:none; z-index:20; }
26
+ .history-popover.open { display:block; animation:popIn .18s var(--ease); }
27
+ .thread-list { max-height:430px; overflow:auto; display:flex; flex-direction:column; gap:8px; padding-right:2px; }
28
+ .thread-row { display:grid; grid-template-columns:minmax(0,1fr) 34px; gap:6px; align-items:stretch; }
29
+ .thread-item { width:100%; border:1px solid transparent; background:#f8fafc; border-radius:16px; padding:13px 14px; text-align:left; transition:all .2s var(--ease); min-height:58px; min-width:0; }
30
+ .thread-item:hover { background:#f1f5f9; }
31
+ .thread-item.active { background:#111827; color:#fff; box-shadow:0 12px 24px rgba(0,0,0,.12); }
32
+ .thread-delete { width:34px; border-radius:14px; display:flex; align-items:center; justify-content:center; color:#cbd5e1; transition:all .2s var(--ease); }
33
+ .thread-delete:hover { background:#fee2e2; color:#dc2626; }
34
+ .messages { flex:1; overflow:auto; padding:34px 42px 268px; display:flex; flex-direction:column; align-items:center; gap:18px; }
35
+ .bubble-row { width:min(var(--content-width),100%); display:flex; gap:12px; align-items:flex-end; }
36
+ .bubble-row.user { justify-content:flex-end; }
37
+ .bubble { max-width:min(780px,78%); border-radius:24px; padding:15px 18px; font-size:14px; line-height:1.7; white-space:pre-wrap; word-break:break-word; box-shadow:0 1px 0 rgba(0,0,0,.03); }
38
+ .bubble.user { background:#111827; color:white; border-bottom-right-radius:8px; }
39
+ .bubble.assistant { background:#fff; border:1px solid #edf2f7; border-bottom-left-radius:8px; }
40
+ .bubble.assistant.streaming::after { content:""; display:inline-block; width:7px; height:1.2em; margin-left:3px; border-radius:999px; background:#111827; vertical-align:-.2em; animation:cursorBlink .9s steps(2,end) infinite; }
41
+ .bubble img.generated { display:block; width:min(460px,100%); border-radius:18px; margin-top:10px; border:1px solid rgba(0,0,0,.06); cursor:zoom-in; }
42
+ .thumbs { display:flex; flex-wrap:wrap; gap:8px; margin-top:10px; }
43
+ .thumb { width:58px; height:58px; border-radius:14px; object-fit:cover; border:1px solid rgba(255,255,255,.28); }
44
+ .assistant .thumb { border-color:#e5e7eb; }
45
+ .composer-wrap { position:absolute; left:0; right:0; bottom:0; padding:24px 42px 30px; background:linear-gradient(to top,#f8fafc 82%,rgba(248,250,252,0)); }
46
+ .composer { width:min(var(--content-width),100%); margin:0 auto; min-height:184px; background:rgba(255,255,255,.96); border:1px solid #e8edf3; border-radius:30px; box-shadow:0 14px 38px rgba(15,23,42,.08); padding:12px; display:flex; flex-direction:column; gap:10px; }
47
+ .mode-switch { background:#f8fafc; border:1px solid #edf2f7; border-radius:999px; padding:2px; display:flex; gap:2px; }
48
+ .mode-switch button { height:30px; min-width:70px; border-radius:999px; display:flex; align-items:center; justify-content:center; gap:5px; color:#64748b; font-size:10px; font-weight:600; letter-spacing:.02em; transition:all .2s var(--ease); }
49
+ .mode-switch button.active { background:#fff; color:#111827; box-shadow:0 1px 3px rgba(15,23,42,.08); }
50
+ /* Provider Switch */
51
+ .provider-switch { background:#f8fafc; border:1px solid #edf2f7; border-radius:999px; padding:2px; display:grid; grid-template-columns:1fr 1fr; gap:2px; }
52
+ .provider-switch button { height:30px; padding:0 10px; white-space:nowrap; border-radius:999px; display:flex; align-items:center; justify-content:center; gap:5px; color:#64748b; font-size:10px; font-weight:600; letter-spacing:.02em; transition:all .2s var(--ease); }
53
+ .provider-switch button.active { background:#fff; color:#111827; box-shadow:0 1px 3px rgba(15,23,42,.08); }
54
+ .image-controls { display:flex; align-items:center; justify-content:flex-end; gap:8px; min-width:0; }
55
+ .image-controls.hidden { display:none; }
56
+ .mini-ratio { display:flex; gap:2px; background:#f8fafc; border:1px solid #edf2f7; border-radius:999px; padding:2px; overflow:auto; }
57
+ .mini-ratio button { height:28px; min-width:42px; border-radius:999px; display:flex; align-items:center; justify-content:center; gap:4px; color:#64748b; font-size:10px; font-weight:600; letter-spacing:0; transition:all .2s var(--ease); }
58
+ .mini-ratio button.active { background:#fff; color:#111827; box-shadow:0 1px 3px rgba(15,23,42,.08); }
59
+ .resolution-toggle { flex:0 0 auto; background:#f8fafc; border:1px solid #edf2f7; border-radius:999px; padding:2px; display:flex; gap:2px; }
60
+ .resolution-option { height:28px; min-width:40px; border-radius:999px; display:flex; align-items:center; justify-content:center; color:#64748b; font-size:10px; font-weight:600; transition:all .2s var(--ease); }
61
+ .resolution-option.active { background:#fff; color:#111827; box-shadow:0 1px 3px rgba(15,23,42,.08); }
62
+ .composer-body { min-height:148px; border:1px solid #f1f5f9; background:#fbfdff; border-radius:22px; padding:14px; display:flex; flex-direction:column; gap:10px; }
63
+ .composer-body.drag-over { border-color:#cbd5e1; background:#f8fafc; box-shadow:inset 0 0 0 1px #e2e8f0; }
64
+ textarea { width:100%; flex:1; min-height:82px; max-height:168px; resize:none; outline:none; border:0; padding:2px; font-size:14px; line-height:1.65; background:transparent; }
65
+ .attachment-stack { display:flex; flex-direction:column; align-items:flex-start; gap:8px; min-width:0; }
66
+ .composer-inside-foot { display:flex; align-items:flex-end; justify-content:space-between; gap:12px; }
67
+ .left-tools { display:flex; align-items:center; gap:8px; min-width:0; flex-wrap:wrap; }
68
+ .inline-controls { display:flex; align-items:center; gap:8px; min-width:0; flex-wrap:wrap; }
69
+ .send-btn { width:44px; height:44px; border-radius:999px; display:flex; align-items:center; justify-content:center; background:#111827; color:#fff; transition:all .2s var(--ease); flex:0 0 auto; }
70
+ .send-btn:disabled { opacity:.45; cursor:not-allowed; }
71
+ .ref-strip { display:flex; align-items:center; gap:8px; overflow:auto; min-width:0; max-width:680px; min-height:0; }
72
+ .ref-strip:empty { display:none; }
73
+ .ref-chip { position:relative; width:54px; height:54px; border-radius:14px; overflow:hidden; border:1px solid #e5e7eb; background:#fff; flex:0 0 auto; }
74
+ .ref-chip img { width:100%; height:100%; object-fit:cover; }
75
+ .ref-chip button { position:absolute; top:3px; right:3px; width:18px; height:18px; border-radius:50%; background:rgba(255,255,255,.92); font-size:12px; line-height:18px; }
76
+ /* MS provider indicator badge */
77
+ .ms-badge { display:inline-flex; align-items:center; gap:4px; font-size:9px; font-weight:800; letter-spacing:.12em; text-transform:uppercase; background:#0d1117; color:#7ee787; border-radius:999px; padding:3px 8px; }
78
+ @keyframes popIn { from { opacity:0; transform:translateY(-6px) scale(.98); } to { opacity:1; transform:translateY(0) scale(1); } }
79
+ @keyframes cursorBlink { 0%,45% { opacity:1; } 46%,100% { opacity:0; } }
80
+ @media (max-width:900px){
81
+ .topbar { padding-left:18px; padding-right:18px; }
82
+ .messages { padding:24px 18px 360px; }
83
+ .composer-wrap { padding:20px 18px 24px; }
84
+ .composer { min-height:296px; }
85
+ .inline-controls { align-items:stretch; flex-direction:column; flex:1; }
86
+ .image-controls { align-items:stretch; flex-direction:column; }
87
+ .resolution-toggle { align-self:flex-start; }
88
+ .composer-inside-foot { align-items:flex-end; }
89
+ .ref-strip { max-width:100%; }
90
+ .bubble { max-width:88%; }
91
+ .action-btn span { display:none; }
92
+ }
93
+ </style>
94
+ <link rel="stylesheet" href="/static/theme.css?v=20260510-studio-pages-blue-dark10">
95
+ </head>
96
+ <body>
97
+ <div class="chat-shell">
98
+ <header class="topbar">
99
+ <div class="topbar-inner">
100
+ <div class="title-block">
101
+ <h1 id="chatTitle">新对话</h1>
102
+ <p id="modelLabel">Ready</p>
103
+ </div>
104
+ <div class="top-actions">
105
+ <button onclick="newConversation()" class="action-btn" title="新建对话"><i data-lucide="plus" class="w-4 h-4"></i><span>NEW</span></button>
106
+ <button onclick="toggleHistory()" class="action-btn" title="历史对话"><i data-lucide="history" class="w-4 h-4"></i><span>HISTORY</span></button>
107
+ <div id="historyPopover" class="history-popover">
108
+ <div class="flex items-center justify-between px-2 pb-3">
109
+ <span class="text-[10px] font-black uppercase tracking-[0.3em] text-gray-400">Conversations</span>
110
+ <button onclick="toggleHistory(false)" class="text-gray-400 hover:text-black"><i data-lucide="x" class="w-4 h-4"></i></button>
111
+ </div>
112
+ <div id="threadList" class="thread-list"></div>
113
+ </div>
114
+ </div>
115
+ </div>
116
+ </header>
117
+
118
+ <section id="messages" class="messages"></section>
119
+
120
+ <div class="composer-wrap">
121
+ <div class="composer">
122
+ <div id="composerBody" class="composer-body">
123
+ <textarea id="messageInput" rows="4" placeholder="输入消息,Enter 发送..." oninput="autoGrow(this)" onkeydown="handleKey(event)"></textarea>
124
+ <div id="refStrip" class="ref-strip"></div>
125
+ <div class="composer-inside-foot">
126
+ <div class="left-tools">
127
+ <div class="inline-controls">
128
+ <div class="mode-switch">
129
+ <button id="chatModeBtn" class="active" onclick="setMode('chat')"><i data-lucide="message-circle" class="w-3.5 h-3.5"></i>Chat</button>
130
+ <button id="imageModeBtn" onclick="setMode('image')"><i data-lucide="image-plus" class="w-3.5 h-3.5"></i>Image</button>
131
+ </div>
132
+ <!-- Provider Switch (chat mode only) -->
133
+ <div id="providerSwitchWrap" class="provider-switch">
134
+ <button id="providerComfly" class="provider-btn active" onclick="setProvider('comfly')">
135
+ <i data-lucide="key-round" class="w-3 h-3"></i>API
136
+ </button>
137
+ <button id="providerMs" class="provider-btn" onclick="setProvider('modelscope')">
138
+ <i data-lucide="cloud" class="w-3 h-3"></i>ModelScope
139
+ </button>
140
+ </div>
141
+ <div id="imageControls" class="image-controls hidden">
142
+ <div class="mini-ratio">
143
+ <button id="chat-ratio-square" class="active" onclick="setChatRatio('square')"><i data-lucide="square" class="w-3 h-3"></i>1:1</button>
144
+ <button id="chat-ratio-portrait" onclick="setChatRatio('portrait')"><i data-lucide="rectangle-vertical" class="w-3 h-3"></i>2:3</button>
145
+ <button id="chat-ratio-landscape" onclick="setChatRatio('landscape')"><i data-lucide="rectangle-horizontal" class="w-3 h-3"></i>3:2</button>
146
+ <button id="chat-ratio-story" onclick="setChatRatio('story')"><i data-lucide="smartphone" class="w-3 h-3"></i>9:16</button>
147
+ <button id="chat-ratio-wide" onclick="setChatRatio('wide')"><i data-lucide="monitor" class="w-3 h-3"></i>16:9</button>
148
+ </div>
149
+ <div class="resolution-toggle" title="分辨率">
150
+ <button id="chat-res-1k" class="resolution-option active" onclick="setChatResolution('1k')">1K</button>
151
+ <button id="chat-res-2k" class="resolution-option" onclick="setChatResolution('2k')">2K</button>
152
+ </div>
153
+ </div>
154
+ </div>
155
+ </div>
156
+ <button id="sendBtn" onclick="sendMessage()" class="send-btn" title="发送"><i data-lucide="send" class="w-5 h-5"></i></button>
157
+ </div>
158
+ </div>
159
+ </div>
160
+ </div>
161
+ </div>
162
+
163
+ <script>
164
+ lucide.createIcons();
165
+ function uuid(){
166
+ if(crypto?.randomUUID) return crypto.randomUUID();
167
+ return 'u-' + Math.random().toString(16).slice(2) + Date.now();
168
+ }
169
+
170
+ const USER_KEY = 'gpt_chat_browser_user';
171
+ const userId = localStorage.getItem(USER_KEY) || uuid();
172
+ localStorage.setItem(USER_KEY, userId);
173
+
174
+ let conversations = [];
175
+ let currentConversation = null;
176
+ let mode = 'chat';
177
+ let provider = 'comfly';
178
+ let msModel = '';
179
+ let chatRatio = 'square';
180
+ let chatResolution = '1k';
181
+ let refs = [];
182
+ let config = { chat_model: 'gpt-5.5', image_model: 'gpt-image-1', ms_chat_models: [] };
183
+
184
+ const SIZE_OPTIONS = {
185
+ square: [['1024x1024','1k'], ['1536x1536','2k']],
186
+ portrait: [['720x1080','1k'], ['1024x1536','2k']],
187
+ landscape: [['1080x720','1k'], ['1536x1024','2k']],
188
+ story: [['720x1280','1k'], ['1080x1920','2k']],
189
+ wide: [['1280x720','1k'], ['1920x1080','2k']]
190
+ };
191
+
192
+ const headers = () => ({'Content-Type':'application/json', 'X-User-ID':userId});
193
+
194
+ async function loadConfig(){
195
+ try {
196
+ const data = await fetch('/api/config').then(r=>r.json());
197
+ config = { ...config, ...data };
198
+ config.chat_model = (config.chat_models || []).find(m => m === 'gpt-5.5') || config.chat_model || 'gpt-5.5';
199
+ // Default MS model to first in list
200
+ if (config.ms_chat_models?.length) msModel = config.ms_chat_models[0];
201
+ updateModelLabel();
202
+ } catch(e) {}
203
+ }
204
+
205
+ function setProvider(p) {
206
+ provider = p;
207
+ document.getElementById('providerComfly').classList.toggle('active', p === 'comfly');
208
+ document.getElementById('providerMs').classList.toggle('active', p === 'modelscope');
209
+ updateModelLabel();
210
+ }
211
+
212
+ async function loadConversations(){
213
+ const data = await fetch('/api/conversations', {headers:{'X-User-ID':userId}}).then(r=>r.json());
214
+ conversations = data.conversations || [];
215
+ renderThreads();
216
+ if(!currentConversation && conversations[0]) await openConversation(conversations[0].id);
217
+ if(!currentConversation) renderMessages([]);
218
+ }
219
+
220
+ function renderThreads(){
221
+ const list = document.getElementById('threadList');
222
+ list.innerHTML = '';
223
+ if(!conversations.length){
224
+ list.innerHTML = '<div class="px-3 py-8 text-center text-[11px] font-bold text-gray-300 uppercase tracking-widest">No History</div>';
225
+ return;
226
+ }
227
+ conversations.forEach(item => {
228
+ const row = document.createElement('div');
229
+ row.className = 'thread-row';
230
+ const btn = document.createElement('button');
231
+ btn.className = `thread-item ${currentConversation?.id === item.id ? 'active' : ''}`;
232
+ btn.onclick = () => openConversation(item.id, true);
233
+ btn.innerHTML = `<div class="text-sm font-bold truncate">${escapeHtml(item.title || '新对话')}</div><div class="text-[11px] opacity-50 truncate mt-1">${escapeHtml(item.last_message || '')}</div>`;
234
+ const del = document.createElement('button');
235
+ del.className = 'thread-delete';
236
+ del.title = '删除对话';
237
+ del.onclick = (event) => deleteConversation(item.id, event);
238
+ del.innerHTML = '<i data-lucide="trash-2" class="w-4 h-4"></i>';
239
+ row.appendChild(btn);
240
+ row.appendChild(del);
241
+ list.appendChild(row);
242
+ });
243
+ lucide.createIcons();
244
+ }
245
+
246
+ async function newConversation(){
247
+ const data = await fetch('/api/conversations', {method:'POST', headers:headers(), body:JSON.stringify({title:'新对话'})}).then(r=>r.json());
248
+ currentConversation = data.conversation;
249
+ document.getElementById('chatTitle').textContent = currentConversation.title || '新对话';
250
+ await loadConversations();
251
+ renderMessages([]);
252
+ toggleHistory(false);
253
+ }
254
+
255
+ async function openConversation(id, closePanel=false){
256
+ const data = await fetch(`/api/conversations/${id}`, {headers:{'X-User-ID':userId}}).then(r=>r.json());
257
+ currentConversation = data.conversation;
258
+ document.getElementById('chatTitle').textContent = currentConversation.title || '新对话';
259
+ renderThreads();
260
+ renderMessages(currentConversation.messages || []);
261
+ if(closePanel) toggleHistory(false);
262
+ }
263
+
264
+ async function deleteConversation(id, event){
265
+ event.stopPropagation();
266
+ if(!confirm('删除这条历史对话?')) return;
267
+ await fetch(`/api/conversations/${id}`, {method:'DELETE', headers:{'X-User-ID':userId}});
268
+ const deletedCurrent = currentConversation?.id === id;
269
+ if(deletedCurrent){ currentConversation = null; document.getElementById('chatTitle').textContent = '新对话'; renderMessages([]); }
270
+ await loadConversations();
271
+ if(deletedCurrent && conversations[0]) await openConversation(conversations[0].id);
272
+ }
273
+
274
+ function toggleHistory(force){
275
+ const pop = document.getElementById('historyPopover');
276
+ const shouldOpen = typeof force === 'boolean' ? force : !pop.classList.contains('open');
277
+ pop.classList.toggle('open', shouldOpen);
278
+ if(shouldOpen) lucide.createIcons();
279
+ }
280
+
281
+ document.addEventListener('click', (event) => {
282
+ const actions = document.querySelector('.top-actions');
283
+ if(actions && !actions.contains(event.target)) toggleHistory(false);
284
+ });
285
+
286
+ function setMode(next){
287
+ mode = next;
288
+ document.getElementById('chatModeBtn').classList.toggle('active', next === 'chat');
289
+ document.getElementById('imageModeBtn').classList.toggle('active', next === 'image');
290
+ document.getElementById('imageControls').classList.toggle('hidden', next !== 'image');
291
+ // Hide provider switch in image mode (COMFLY only for images)
292
+ document.getElementById('providerSwitchWrap').style.display = next === 'image' ? 'none' : '';
293
+ updateModelLabel();
294
+ }
295
+
296
+ function updateModelLabel(){
297
+ let label = '';
298
+ if (mode === 'image') {
299
+ label = config.image_model || 'Image';
300
+ } else if (provider === 'modelscope') {
301
+ label = msModel || (config.ms_chat_models?.[0]) || 'ModelScope';
302
+ // Shorten the display label
303
+ label = label.split('/').pop().split(':')[0];
304
+ } else {
305
+ label = config.chat_model || 'Chat';
306
+ }
307
+ document.getElementById('modelLabel').textContent = label;
308
+ }
309
+
310
+ function setChatRatio(next, preferredSize = ''){
311
+ chatRatio = next;
312
+ ['square','portrait','landscape','story','wide'].forEach(item => {
313
+ document.getElementById(`chat-ratio-${item}`).classList.toggle('active', item === next);
314
+ });
315
+ if(preferredSize){ const match = SIZE_OPTIONS[next].find(([value]) => value === preferredSize); if(match) chatResolution = match[1]; }
316
+ updateChatResolutionUI();
317
+ }
318
+
319
+ function setChatResolution(next){ chatResolution = next; updateChatResolutionUI(); }
320
+ function updateChatResolutionUI(){
321
+ document.getElementById('chat-res-1k').classList.toggle('active', chatResolution === '1k');
322
+ document.getElementById('chat-res-2k').classList.toggle('active', chatResolution === '2k');
323
+ }
324
+
325
+ function currentChatSize(){
326
+ const options = SIZE_OPTIONS[chatRatio] || SIZE_OPTIONS.square;
327
+ const match = options.find(([, label]) => label === chatResolution) || options[0];
328
+ return match[0];
329
+ }
330
+
331
+ function renderMessages(messages){
332
+ const box = document.getElementById('messages');
333
+ box.innerHTML = '';
334
+ if(!messages.length){
335
+ box.innerHTML = `<div class="m-auto text-center opacity-20"><i data-lucide="messages-square" class="w-14 h-14 mx-auto stroke-[1px]"></i><div class="text-[10px] font-black tracking-[0.5em] uppercase mt-4">READY</div></div>`;
336
+ lucide.createIcons();
337
+ return;
338
+ }
339
+ messages.forEach(addMessageBubble);
340
+ scrollBottom();
341
+ }
342
+
343
+ function addMessageBubble(msg){
344
+ const box = document.getElementById('messages');
345
+ const row = document.createElement('div');
346
+ row.className = `bubble-row ${msg.role === 'user' ? 'user' : 'assistant'}`;
347
+ const bubble = document.createElement('div');
348
+ bubble.className = `bubble ${msg.role === 'user' ? 'user' : 'assistant'}`;
349
+ const text = document.createElement('div');
350
+ text.textContent = msg.type === 'image' ? '生成完成' : (msg.content || '');
351
+ text.className = 'bubble-text';
352
+ bubble.appendChild(text);
353
+ if(msg.attachments?.length){
354
+ const thumbs = document.createElement('div');
355
+ thumbs.className = 'thumbs';
356
+ msg.attachments.forEach(ref => thumbs.insertAdjacentHTML('beforeend', `<img class="thumb" src="${ref.url}">`));
357
+ bubble.appendChild(thumbs);
358
+ }
359
+ if(msg.image_url){
360
+ const img = document.createElement('img');
361
+ img.className = 'generated';
362
+ img.src = msg.image_url;
363
+ img.onclick = () => window.open(msg.image_url, '_blank');
364
+ bubble.appendChild(img);
365
+ }
366
+ // Show model badge for assistant messages
367
+ if(msg.role === 'assistant' && msg.model){
368
+ const badge = document.createElement('div');
369
+ const modelShort = msg.model.split('/').pop().split(':')[0];
370
+ const isMs = msg.model.includes('/') && !msg.model.includes('gpt');
371
+ badge.style.cssText = `margin-top:8px;font-size:9px;font-weight:700;opacity:.45;letter-spacing:.06em;`;
372
+ badge.textContent = modelShort;
373
+ bubble.appendChild(badge);
374
+ }
375
+ row.appendChild(bubble);
376
+ box.appendChild(row);
377
+ return {row, bubble, text};
378
+ }
379
+
380
+ async function uploadFiles(files){
381
+ if(!files?.length) return;
382
+ const form = new FormData();
383
+ [...files].slice(0, 4 - refs.length).forEach(file => form.append('files', file));
384
+ const data = await fetch('/api/ai/upload', {method:'POST', body:form}).then(r=>r.json());
385
+ refs.push(...(data.files || []));
386
+ renderRefs();
387
+ }
388
+
389
+ function renderRefs(){
390
+ const strip = document.getElementById('refStrip');
391
+ strip.innerHTML = refs.map((ref,i) => `<div class="ref-chip"><img src="${ref.url}"><button onclick="removeRef(${i})">×</button></div>`).join('');
392
+ }
393
+
394
+ function removeRef(i){ refs.splice(i,1); renderRefs(); }
395
+
396
+ window.addEventListener('paste', e => {
397
+ const files = [...(e.clipboardData?.items || [])].filter(x => x.kind === 'file' && x.type.startsWith('image/')).map(x => x.getAsFile());
398
+ if(files.length) uploadFiles(files);
399
+ });
400
+
401
+ const composerBody = document.getElementById('composerBody');
402
+ composerBody.addEventListener('dragover', e => {
403
+ if(!hasImageFiles(e.dataTransfer?.items)) return;
404
+ e.preventDefault();
405
+ composerBody.classList.add('drag-over');
406
+ });
407
+ composerBody.addEventListener('dragleave', e => {
408
+ if(!composerBody.contains(e.relatedTarget)) composerBody.classList.remove('drag-over');
409
+ });
410
+ composerBody.addEventListener('drop', e => {
411
+ if(!hasImageFiles(e.dataTransfer?.items)) return;
412
+ e.preventDefault();
413
+ composerBody.classList.remove('drag-over');
414
+ const files = [...(e.dataTransfer?.files || [])].filter(file => file.type.startsWith('image/'));
415
+ uploadFiles(files);
416
+ });
417
+
418
+ function hasImageFiles(items){ return [...(items || [])].some(item => item.kind === 'file' && item.type.startsWith('image/')); }
419
+ function autoGrow(el){ el.style.height = 'auto'; el.style.height = Math.min(el.scrollHeight, 150) + 'px'; }
420
+ function handleKey(e){ if(e.key === 'Enter' && !e.shiftKey){ e.preventDefault(); sendMessage(); } }
421
+
422
+ async function sendMessage(){
423
+ const input = document.getElementById('messageInput');
424
+ const message = input.value.trim();
425
+ if(!message) return;
426
+ const btn = document.getElementById('sendBtn');
427
+ btn.disabled = true;
428
+ const pendingRefs = refs.slice();
429
+ refs = [];
430
+ renderRefs();
431
+ input.value = '';
432
+ autoGrow(input);
433
+ if(!currentConversation){
434
+ currentConversation = {id:'', title:'新对话', messages:[]};
435
+ document.getElementById('messages').innerHTML = '';
436
+ }
437
+ addMessageBubble({role:'user', content:message, attachments:pendingRefs});
438
+ const assistantBubble = addMessageBubble({role:'assistant', content: mode === 'image' ? '正在生成图片...' : ''});
439
+ if(mode === 'chat') assistantBubble.bubble.classList.add('streaming');
440
+ scrollBottom();
441
+ try {
442
+ if(mode === 'chat') {
443
+ await streamChatMessage(message, pendingRefs, assistantBubble);
444
+ } else {
445
+ const data = await fetch('/api/chat', {
446
+ method:'POST',
447
+ headers:headers(),
448
+ body:JSON.stringify({
449
+ conversation_id: currentConversation.id || '',
450
+ message,
451
+ mode,
452
+ size: currentChatSize(),
453
+ model: config.chat_model,
454
+ image_model: config.image_model,
455
+ reference_images: pendingRefs,
456
+ provider: 'comfly',
457
+ })
458
+ }).then(async r => { if(!r.ok) throw new Error((await r.json()).detail || '请求失败'); return r.json(); });
459
+ currentConversation = data.conversation;
460
+ document.getElementById('chatTitle').textContent = currentConversation.title || '新对话';
461
+ renderMessages(currentConversation.messages || []);
462
+ await loadConversations();
463
+ }
464
+ } catch(err) {
465
+ renderMessages([...(currentConversation.messages || []), {role:'assistant', content:err.message || '���求失败'}]);
466
+ } finally {
467
+ btn.disabled = false;
468
+ scrollBottom();
469
+ }
470
+ }
471
+
472
+ async function streamChatMessage(message, pendingRefs, assistantBubble){
473
+ const currentProvider = provider;
474
+ const currentMsModel = msModel || (config.ms_chat_models?.[0] || '');
475
+ const currentChatModel = config.chat_model;
476
+
477
+ const res = await fetch('/api/chat/stream', {
478
+ method:'POST',
479
+ headers:headers(),
480
+ body:JSON.stringify({
481
+ conversation_id: currentConversation.id || '',
482
+ message,
483
+ mode:'chat',
484
+ model: currentProvider === 'modelscope' ? currentMsModel : currentChatModel,
485
+ ms_model: currentProvider === 'modelscope' ? currentMsModel : '',
486
+ provider: currentProvider,
487
+ reference_images: pendingRefs
488
+ })
489
+ });
490
+ if(!res.ok) throw new Error((await res.json()).detail || '请求失败');
491
+ const reader = res.body.getReader();
492
+ const decoder = new TextDecoder();
493
+ let buffer = '';
494
+ let fullText = '';
495
+ while(true){
496
+ const {value, done} = await reader.read();
497
+ if(done) break;
498
+ buffer += decoder.decode(value, {stream:true});
499
+ const events = buffer.split('\n\n');
500
+ buffer = events.pop() || '';
501
+ for(const eventText of events){
502
+ const line = eventText.split('\n').find(item => item.startsWith('data:'));
503
+ if(!line) continue;
504
+ const event = JSON.parse(line.slice(5).trim());
505
+ if(event.type === 'meta'){
506
+ currentConversation = event.conversation;
507
+ document.getElementById('chatTitle').textContent = currentConversation.title || '新对话';
508
+ }
509
+ if(event.type === 'delta'){
510
+ fullText += event.delta || '';
511
+ assistantBubble.text.textContent = fullText;
512
+ scrollBottom();
513
+ }
514
+ if(event.type === 'error') throw new Error(event.detail || '请求失败');
515
+ if(event.type === 'done'){
516
+ assistantBubble.bubble.classList.remove('streaming');
517
+ currentConversation = event.conversation;
518
+ document.getElementById('chatTitle').textContent = currentConversation.title || '新对话';
519
+ renderMessages(currentConversation.messages || []);
520
+ await loadConversations();
521
+ }
522
+ }
523
+ }
524
+ assistantBubble.bubble.classList.remove('streaming');
525
+ }
526
+
527
+ function scrollBottom(){ requestAnimationFrame(() => { const box = document.getElementById('messages'); box.scrollTop = box.scrollHeight; }); }
528
+ function escapeHtml(str){ return String(str).replace(/[&<>"']/g, s => ({'&':'&amp;','<':'&lt;','>':'&gt;','"':'&quot;',"'":'&#39;'}[s])); }
529
+
530
+ window.onload = async () => {
531
+ setChatRatio('square');
532
+ setChatResolution('1k');
533
+ await loadConfig();
534
+ await loadConversations();
535
+ };
536
+ </script>
537
+ </body>
538
+ </html>
26-5-10-API-Studio/static/index.html ADDED
@@ -0,0 +1,663 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <!DOCTYPE html>
2
+ <html lang="zh-CN">
3
+
4
+ <head>
5
+ <meta charset="UTF-8">
6
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
7
+ <link rel="icon" href="/static/logo.png" type="image/png">
8
+ <title>AI Studio</title>
9
+ <script src="https://cdn.tailwindcss.com"></script>
10
+ <script src="/static/theme.js?v=20260509"></script>
11
+ <style>
12
+ @import url('https://fonts.googleapis.com/css2?family=Space+Grotesk:wght@300;500;700&display=swap');
13
+
14
+ :root {
15
+ --accent: #000000;
16
+ --fluid-ease: cubic-bezier(0.3, 0, 0, 1);
17
+ --bg: #ffffff;
18
+ --sidebar-bg: #ffffff;
19
+ --stage-bg: #fcfcfc;
20
+ --border: #f2f2f2;
21
+ --stage-border: #f0f0f0;
22
+ --text: #121212;
23
+ --muted: #999;
24
+ --nav-hover-bg: #fafafa;
25
+ --monitor-bg: rgba(255, 255, 255, 0.7);
26
+ --monitor-border: rgba(0, 0, 0, 0.05);
27
+ --monitor-shadow: rgba(0, 0, 0, 0.04);
28
+ --divider: rgba(0, 0, 0, 0.1);
29
+ --scrollbar: #d8d8d8;
30
+ --scrollbar-hover: #c0c0c0;
31
+ --author-name: #000;
32
+ --social-icon: #ccc;
33
+ }
34
+
35
+ body.theme-dark {
36
+ --bg: #0f141d;
37
+ --sidebar-bg: #0f141d;
38
+ --stage-bg: #111722;
39
+ --border: #242d3b;
40
+ --stage-border: #2a3444;
41
+ --text: #e5e9f0;
42
+ --muted: #8f9aab;
43
+ --nav-hover-bg: #171d29;
44
+ --monitor-bg: rgba(23, 29, 41, 0.82);
45
+ --monitor-border: rgba(255, 255, 255, 0.08);
46
+ --monitor-shadow: rgba(0, 0, 0, 0.3);
47
+ --divider: rgba(255, 255, 255, 0.15);
48
+ --scrollbar: #334155;
49
+ --scrollbar-hover: #475569;
50
+ --author-name: #e5e9f0;
51
+ --social-icon: #64748b;
52
+ }
53
+
54
+ /* 极简滚动条 */
55
+ *::-webkit-scrollbar {
56
+ width: 10px !important;
57
+ height: 10px !important;
58
+ background: transparent !important;
59
+ }
60
+
61
+ *::-webkit-scrollbar-track {
62
+ background: transparent !important;
63
+ border: none !important;
64
+ }
65
+
66
+ *::-webkit-scrollbar-thumb {
67
+ background-color: var(--scrollbar) !important;
68
+ border: 3px solid transparent !important;
69
+ border-right-width: 5px !important;
70
+ background-clip: padding-box !important;
71
+ border-radius: 10px !important;
72
+ }
73
+
74
+ *::-webkit-scrollbar-thumb:hover {
75
+ background-color: var(--scrollbar-hover) !important;
76
+ }
77
+
78
+ *::-webkit-scrollbar-corner {
79
+ background: transparent !important;
80
+ }
81
+
82
+ * {
83
+ scrollbar-width: thin !important;
84
+ scrollbar-color: var(--scrollbar) transparent !important;
85
+ }
86
+
87
+ body {
88
+ background: var(--bg);
89
+ font-family: 'Space Grotesk', sans-serif;
90
+ overflow: hidden;
91
+ height: 100vh;
92
+ color: var(--text);
93
+ transition: background 0.3s, color 0.3s;
94
+ }
95
+
96
+ .app-shell {
97
+ display: flex;
98
+ width: 100%;
99
+ height: 100vh;
100
+ background: var(--bg);
101
+ position: relative;
102
+ transition: background 0.3s;
103
+ }
104
+
105
+ /* 侧边栏 */
106
+ .sidebar {
107
+ width: 80px;
108
+ min-width: 80px;
109
+ background: var(--sidebar-bg);
110
+ display: flex;
111
+ flex-direction: column;
112
+ align-items: center;
113
+ border-right: 1px solid var(--border);
114
+ padding: 40px 0;
115
+ transition: width 0.5s var(--fluid-ease) 0.5s, background 0.3s, border-color 0.3s;
116
+ z-index: 50;
117
+ }
118
+
119
+ .sidebar:hover {
120
+ width: 220px;
121
+ transition-delay: 0s;
122
+ }
123
+
124
+ .logo-ring {
125
+ width: 36px;
126
+ height: 36px;
127
+ border: 2px solid var(--text);
128
+ border-radius: 12px;
129
+ display: flex;
130
+ align-items: center;
131
+ justify-content: center;
132
+ transition: all 0.6s var(--fluid-ease) 0.5s, border-color 0.3s;
133
+ }
134
+
135
+ .sidebar:hover .logo-ring {
136
+ transform: rotate(90deg);
137
+ border-radius: 50%;
138
+ transition-delay: 0s;
139
+ }
140
+
141
+ /* 导航项 */
142
+ .nav-item {
143
+ position: relative;
144
+ width: 48px;
145
+ height: 48px;
146
+ margin: 10px 0;
147
+ display: flex;
148
+ align-items: center;
149
+ justify-content: flex-start;
150
+ border-radius: 18px;
151
+ cursor: pointer;
152
+ transition: all 0.3s var(--fluid-ease) 0.5s;
153
+ color: var(--muted);
154
+ overflow: hidden;
155
+ padding-left: 14px;
156
+ }
157
+
158
+ .sidebar:hover .nav-item {
159
+ width: 190px;
160
+ transition-delay: 0s;
161
+ }
162
+
163
+ .nav-item:hover {
164
+ background: var(--nav-hover-bg);
165
+ color: var(--text);
166
+ }
167
+
168
+ .nav-item.active {
169
+ background: var(--accent);
170
+ color: #fff;
171
+ }
172
+
173
+ body.theme-dark .nav-item.active {
174
+ background: #f1f5f9;
175
+ color: #0f172a;
176
+ }
177
+
178
+ .nav-text {
179
+ opacity: 0;
180
+ margin-left: 16px;
181
+ font-weight: 600;
182
+ font-size: 14px;
183
+ white-space: nowrap;
184
+ transition: opacity 0.3s 0.5s;
185
+ }
186
+
187
+ .sidebar:hover .nav-text {
188
+ opacity: 1;
189
+ transition-delay: 0.1s;
190
+ }
191
+
192
+ /* 主舞台区 */
193
+ .stage {
194
+ flex: 1;
195
+ background: var(--stage-bg);
196
+ margin: 16px;
197
+ border-radius: 32px;
198
+ overflow: hidden;
199
+ border: 1px solid var(--stage-border);
200
+ position: relative;
201
+ transition: background 0.3s, border-color 0.3s;
202
+ }
203
+
204
+ iframe {
205
+ position: absolute;
206
+ inset: 0;
207
+ width: 100%;
208
+ height: 100%;
209
+ border: none;
210
+ opacity: 0;
211
+ transform: scale(1.02);
212
+ filter: blur(4px);
213
+ transition: all 0.5s var(--fluid-ease);
214
+ pointer-events: none;
215
+ }
216
+
217
+ iframe.active {
218
+ opacity: 1;
219
+ transform: scale(1);
220
+ filter: blur(0);
221
+ pointer-events: auto;
222
+ }
223
+
224
+ /* 左下角监视器 */
225
+ .nano-monitor {
226
+ position: absolute;
227
+ bottom: 24px;
228
+ left: 24px;
229
+ z-index: 100;
230
+ display: flex;
231
+ align-items: center;
232
+ gap: 8px;
233
+ background: var(--monitor-bg);
234
+ backdrop-filter: blur(12px);
235
+ padding: 6px 14px;
236
+ border-radius: 16px;
237
+ border: 1px solid var(--monitor-border);
238
+ box-shadow: 0 4px 20px var(--monitor-shadow);
239
+ font-family: monospace;
240
+ transition: all 0.4s var(--fluid-ease);
241
+ }
242
+
243
+ .nano-monitor.is-busy {
244
+ background: #000;
245
+ color: #fff;
246
+ border-color: rgba(255, 255, 255, 0.1);
247
+ box-shadow: 0 8px 30px rgba(0, 0, 0, 0.2);
248
+ }
249
+
250
+ .stat-group {
251
+ display: flex;
252
+ align-items: center;
253
+ gap: 6px;
254
+ font-size: 11px;
255
+ font-weight: 700;
256
+ color: var(--text);
257
+ }
258
+
259
+ .nano-monitor.is-busy .stat-group {
260
+ color: #fff;
261
+ }
262
+
263
+ .divider {
264
+ width: 1px;
265
+ height: 12px;
266
+ background: var(--divider);
267
+ }
268
+
269
+ .is-busy .divider {
270
+ background: rgba(255, 255, 255, 0.2);
271
+ }
272
+
273
+ .pulse-dot {
274
+ width: 6px;
275
+ height: 6px;
276
+ border-radius: 50%;
277
+ background: #10b981;
278
+ }
279
+
280
+ .spinner-nano {
281
+ width: 10px;
282
+ height: 10px;
283
+ border: 2px solid rgba(255, 255, 255, 0.2);
284
+ border-top-color: #fff;
285
+ border-radius: 50%;
286
+ animation: spin 0.8s linear infinite;
287
+ display: none;
288
+ }
289
+
290
+ .is-busy .spinner-nano { display: block; }
291
+ .is-busy .pulse-dot { display: none; }
292
+
293
+ @keyframes spin {
294
+ to { transform: rotate(360deg); }
295
+ }
296
+
297
+ .label-nano {
298
+ text-transform: uppercase;
299
+ letter-spacing: 0.5px;
300
+ opacity: 0.5;
301
+ font-size: 9px;
302
+ }
303
+
304
+ /* 作者组件 */
305
+ .author-box {
306
+ width: 100%;
307
+ height: 60px;
308
+ position: relative;
309
+ display: flex;
310
+ align-items: center;
311
+ justify-content: center;
312
+ overflow: hidden;
313
+ }
314
+
315
+ .dx-letter {
316
+ position: absolute;
317
+ font-size: 14px;
318
+ font-weight: 800;
319
+ color: var(--text);
320
+ transition: all 0.5s var(--fluid-ease) 0.4s;
321
+ z-index: 10;
322
+ }
323
+
324
+ .letter-d { transform: translateX(-8px); }
325
+ .letter-x { transform: translateX(8px); }
326
+
327
+ .sidebar:hover .letter-d {
328
+ transform: translateX(-120px);
329
+ opacity: 0;
330
+ transition-delay: 0s;
331
+ }
332
+
333
+ .sidebar:hover .letter-x {
334
+ transform: translateX(120px);
335
+ opacity: 0;
336
+ transition-delay: 0s;
337
+ }
338
+
339
+ .author-content-wrap {
340
+ display: flex;
341
+ flex-direction: column;
342
+ align-items: center;
343
+ opacity: 0;
344
+ transform: scale(0.9);
345
+ transition: all 0.4s var(--fluid-ease) 0s;
346
+ pointer-events: none;
347
+ }
348
+
349
+ .sidebar:hover .author-content-wrap {
350
+ opacity: 1;
351
+ transform: scale(1);
352
+ transition-delay: 0.2s;
353
+ pointer-events: auto;
354
+ }
355
+
356
+ .author-name-lite {
357
+ font-size: 12px;
358
+ font-weight: 700;
359
+ margin-bottom: 8px;
360
+ color: var(--author-name);
361
+ }
362
+
363
+ .social-row-lite { display: flex; gap: 12px; }
364
+
365
+ .social-icon-lite {
366
+ color: var(--social-icon);
367
+ transition: color 0.2s, transform 0.2s;
368
+ }
369
+
370
+ .social-icon-lite:hover {
371
+ color: var(--text);
372
+ transform: translateY(-1px);
373
+ }
374
+
375
+ /* 主题切换按钮 */
376
+ .theme-toggle {
377
+ width: 36px;
378
+ height: 36px;
379
+ border-radius: 50%;
380
+ background: transparent;
381
+ border: 1px solid var(--border);
382
+ color: var(--muted);
383
+ display: flex;
384
+ align-items: center;
385
+ justify-content: center;
386
+ cursor: pointer;
387
+ transition: all 0.3s var(--fluid-ease);
388
+ margin: 8px 0;
389
+ flex-shrink: 0;
390
+ }
391
+
392
+ .theme-toggle:hover {
393
+ background: var(--nav-hover-bg);
394
+ color: var(--text);
395
+ border-color: var(--stage-border);
396
+ }
397
+
398
+ .theme-toggle svg { width: 16px; height: 16px; }
399
+
400
+ .sidebar:hover .theme-toggle {
401
+ /* Expand to match nav item width */
402
+ }
403
+ </style>
404
+ </head>
405
+
406
+ <body>
407
+
408
+ <div class="app-shell">
409
+ <aside class="sidebar">
410
+ <div class="logo-ring mb-12">
411
+ <div class="w-1.5 h-1.5 rounded-full transition-colors" id="logo-dot" style="background:var(--text)"></div>
412
+ </div>
413
+
414
+ <nav>
415
+ <div class="nav-item active" onclick="switchUI(this, 'zimage')">
416
+ <svg class="w-5 h-5 flex-shrink-0" fill="none" stroke="currentColor" viewBox="0 0 24 24">
417
+ <path d="M4 16l4.586-4.586a2 2 0 012.828 0L16 16m-2-2l1.587-1.587a2 2 0 012.828 0L20 14m-6-6h.01M6 20h12a2 2 0 002-2V6a2 2 0 00-2-2H6a2 2 0 00-2 2v12a2 2 0 002 2z" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"></path>
418
+ </svg>
419
+ <span class="nav-text">文生图</span>
420
+ </div>
421
+ <div class="nav-item" onclick="switchUI(this, 'enhance')">
422
+ <svg class="w-5 h-5 flex-shrink-0" fill="none" stroke="currentColor" viewBox="0 0 24 24">
423
+ <path d="M13 10V3L4 14h7v7l9-11h-7z" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"></path>
424
+ </svg>
425
+ <span class="nav-text">细节增强</span>
426
+ </div>
427
+ <div class="nav-item" onclick="switchUI(this, 'klein')">
428
+ <svg class="w-5 h-5 flex-shrink-0" fill="none" stroke="currentColor" viewBox="0 0 24 24">
429
+ <path d="M11 5H6a2 2 0 00-2 2v11a2 2 0 002 2h11a2 2 0 002-2v-5m-1.414-9.414a2 2 0 112.828 2.828L11.828 15H9v-2.828l8.586-8.586z" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"></path>
430
+ </svg>
431
+ <span class="nav-text">图片编辑</span>
432
+ </div>
433
+ <div class="nav-item" onclick="switchUI(this, 'angle')">
434
+ <svg class="w-5 h-5 flex-shrink-0" fill="none" stroke="currentColor" viewBox="0 0 24 24" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
435
+ <path d="M21 16V8a2 2 0 0 0-1-1.73l-7-4a2 2 0 0 0-2 0l-7 4A2 2 0 0 0 3 8v8a2 2 0 0 0 1 1.73l7 4a2 2 0 0 0 2 0l7-4A2 2 0 0 0 21 16z"></path>
436
+ <polyline points="3.27 6.96 12 12.01 20.73 6.96"></polyline>
437
+ <line x1="12" y1="22.08" x2="12" y2="12"></line>
438
+ </svg>
439
+ <span class="nav-text">角度控制</span>
440
+ </div>
441
+
442
+ <!-- 分隔线 -->
443
+ <div style="width:32px;height:1px;background:var(--border);margin:8px auto;transition:background 0.3s;"></div>
444
+
445
+ <div class="nav-item" onclick="switchUI(this, 'online')">
446
+ <svg class="w-5 h-5 flex-shrink-0" fill="none" stroke="currentColor" viewBox="0 0 24 24" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
447
+ <path d="M12 2a10 10 0 1 0 0 20A10 10 0 0 0 12 2z"></path>
448
+ <path d="M2 12h20M12 2a15.3 15.3 0 0 1 4 10 15.3 15.3 0 0 1-4 10 15.3 15.3 0 0 1-4-10 15.3 15.3 0 0 1 4-10z"></path>
449
+ </svg>
450
+ <span class="nav-text">在线生图</span>
451
+ </div>
452
+ <div class="nav-item" onclick="switchUI(this, 'gpt-chat')">
453
+ <svg class="w-5 h-5 flex-shrink-0" fill="none" stroke="currentColor" viewBox="0 0 24 24" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
454
+ <path d="M21 15a2 2 0 0 1-2 2H7l-4 4V5a2 2 0 0 1 2-2h14a2 2 0 0 1 2 2z"></path>
455
+ </svg>
456
+ <span class="nav-text">GPT 对话</span>
457
+ </div>
458
+ <div class="nav-item" onclick="switchUI(this, 'canvas')">
459
+ <svg class="w-5 h-5 flex-shrink-0" fill="none" stroke="currentColor" viewBox="0 0 24 24" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
460
+ <rect x="3" y="3" width="7" height="7"></rect>
461
+ <rect x="14" y="3" width="7" height="7"></rect>
462
+ <rect x="14" y="14" width="7" height="7"></rect>
463
+ <rect x="3" y="14" width="7" height="7"></rect>
464
+ </svg>
465
+ <span class="nav-text">无限画布</span>
466
+ </div>
467
+ </nav>
468
+
469
+ <!-- 主题切换 -->
470
+ <div style="margin-top:auto;margin-bottom:8px;display:flex;align-items:center;justify-content:center;width:100%;">
471
+ <button class="theme-toggle" onclick="toggleTheme()" id="theme-toggle-btn" title="切换夜间模式">
472
+ <svg id="icon-moon" fill="none" stroke="currentColor" viewBox="0 0 24 24" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
473
+ <path d="M21 12.79A9 9 0 1 1 11.21 3 7 7 0 0 0 21 12.79z"></path>
474
+ </svg>
475
+ <svg id="icon-sun" style="display:none" fill="none" stroke="currentColor" viewBox="0 0 24 24" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
476
+ <circle cx="12" cy="12" r="5"></circle>
477
+ <line x1="12" y1="1" x2="12" y2="3"></line>
478
+ <line x1="12" y1="21" x2="12" y2="23"></line>
479
+ <line x1="4.22" y1="4.22" x2="5.64" y2="5.64"></line>
480
+ <line x1="18.36" y1="18.36" x2="19.78" y2="19.78"></line>
481
+ <line x1="1" y1="12" x2="3" y2="12"></line>
482
+ <line x1="21" y1="12" x2="23" y2="12"></line>
483
+ <line x1="4.22" y1="19.78" x2="5.64" y2="18.36"></line>
484
+ <line x1="18.36" y1="5.64" x2="19.78" y2="4.22"></line>
485
+ </svg>
486
+ </button>
487
+ </div>
488
+
489
+ <div class="author-box">
490
+ <span class="dx-letter letter-d">D</span>
491
+ <span class="dx-letter letter-x">X</span>
492
+
493
+ <div class="author-content-wrap">
494
+ <div class="author-name-lite">wuli大雄</div>
495
+ <div class="social-row-lite">
496
+ <a href="https://space.bilibili.com/78652351" target="_blank" class="social-icon-lite">
497
+ <svg class="w-4 h-4" fill="currentColor" viewBox="0 0 24 24">
498
+ <path d="M17.813 4.653h-.854L15.66 3.053a1.147 1.147 0 00-1.63 0l-1.3 1.6h-1.46L9.97 3.053a1.147 1.147 0 00-1.63 0L7.043 4.653h-.854a3.946 3.946 0 00-3.93 3.934v8.117a3.946 3.946 0 003.93 3.934h11.624a3.946 3.946 0 003.93-3.934V8.587a3.946 3.946 0 00-3.93-3.934zM7.152 13.9a1.465 1.465 0 111.47-1.462 1.465 1.465 0 01-1.47 1.462zm7.696 0a1.465 1.465 0 111.47-1.462 1.465 1.465 0 01-1.47 1.462z" />
499
+ </svg>
500
+ </a>
501
+ <a href="https://www.xiaohongshu.com/user/profile/6433c34c000000001a023538" target="_blank" class="social-icon-lite">
502
+ <svg class="w-4 h-4" fill="currentColor" viewBox="0 0 24 24">
503
+ <path d="M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm-1 14.5v-9l6 4.5-6 4.5z" />
504
+ </svg>
505
+ </a>
506
+ <a href="https://www.youtube.com/@%E5%A4%A7%E9%9B%84dx" target="_blank" class="social-icon-lite">
507
+ <svg class="w-4 h-4" fill="currentColor" viewBox="0 0 24 24">
508
+ <path d="M23.498 6.186a3.016 3.016 0 00-2.122-2.136C19.505 3.545 12 3.545 12 3.545s-7.505 0-9.377.505A3.017 3.017 0 00.502 6.186C0 8.07 0 12 0 12s0 3.93.502 5.814a3.016 3.016 0 002.122 2.136c1.871.505 9.376.505 9.376.505s7.505 0 9.377-.505a3.015 3.015 0 002.122-2.136C24 15.93 24 12 24 12s0-3.93-.502-5.814zM9.545 15.568V8.432L15.818 12l-6.273 3.568z" />
509
+ </svg>
510
+ </a>
511
+ <a href="https://x.com/dx8152?s=21" target="_blank" class="social-icon-lite">
512
+ <svg class="w-4 h-4" fill="currentColor" viewBox="0 0 24 24">
513
+ <path d="M18.244 2.25h3.308l-7.227 8.26 8.502 11.24H16.17l-5.214-6.817L4.99 21.75H1.68l7.73-8.835L1.254 2.25H8.08l4.713 6.231zm-1.161 17.52h1.833L7.084 4.126H5.117z" />
514
+ </svg>
515
+ </a>
516
+ </div>
517
+ </div>
518
+ </div>
519
+ </aside>
520
+
521
+ <main class="stage">
522
+ <iframe id="frame-zimage" src="/static/zimage.html?v=31" class="active"></iframe>
523
+ <iframe id="frame-enhance" data-src="/static/enhance.html?v=31"></iframe>
524
+ <iframe id="frame-klein" data-src="/static/klein.html?v=31"></iframe>
525
+ <iframe id="frame-angle" data-src="/static/angle.html?v=31"></iframe>
526
+ <iframe id="frame-online" data-src="/static/online.html?v=1"></iframe>
527
+ <iframe id="frame-gpt-chat" data-src="/static/gpt-chat.html?v=1"></iframe>
528
+ <iframe id="frame-canvas" data-src="/static/canvas.html?v=1"></iframe>
529
+
530
+ <div class="nano-monitor" id="nano-monitor">
531
+ <div class="stat-group">
532
+ <div class="pulse-dot animate-pulse"></div>
533
+ <div class="spinner-nano"></div>
534
+ <span class="label-nano">ONLINE</span>
535
+ <span id="online-val">1</span>
536
+ </div>
537
+ <div class="divider"></div>
538
+ <div class="stat-group">
539
+ <span class="label-nano">QUEUE</span>
540
+ <span id="queue-val">0</span>
541
+ </div>
542
+ </div>
543
+ </main>
544
+ </div>
545
+
546
+ <script>
547
+ function generateUUID() {
548
+ if (typeof crypto !== 'undefined' && crypto.randomUUID) {
549
+ try { return crypto.randomUUID(); } catch (e) { }
550
+ }
551
+ return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function (c) {
552
+ var r = Math.random() * 16 | 0, v = c == 'x' ? r : (r & 0x3 | 0x8);
553
+ return v.toString(16);
554
+ });
555
+ }
556
+ const CID = localStorage.getItem("client_id") || generateUUID();
557
+ localStorage.setItem("client_id", CID);
558
+
559
+ function switchUI(el, id) {
560
+ document.querySelectorAll('.nav-item').forEach(n => n.classList.remove('active'));
561
+ el.classList.add('active');
562
+ document.querySelectorAll('iframe').forEach(f => f.classList.remove('active'));
563
+ const target = document.getElementById('frame-' + id);
564
+ target.classList.add('active');
565
+ if (!target.src) target.src = target.dataset.src;
566
+ // sync theme to newly activated iframe
567
+ syncThemeToFrame(target);
568
+ }
569
+
570
+ async function syncStatus() {
571
+ try {
572
+ const res = await fetch(`/api/queue_status?client_id=${CID}`);
573
+ const data = await res.json();
574
+ const monitor = document.getElementById('nano-monitor');
575
+ const queueVal = document.getElementById('queue-val');
576
+ const logoDot = document.getElementById('logo-dot');
577
+ const total = data.total || 0;
578
+ const pos = data.position || 0;
579
+ if (pos > 0) {
580
+ monitor.classList.add('is-busy');
581
+ queueVal.innerText = `${pos}/${total}`;
582
+ logoDot.style.backgroundColor = '#3b82f6';
583
+ } else {
584
+ monitor.classList.remove('is-busy');
585
+ queueVal.innerText = total > 0 ? total : '0';
586
+ logoDot.style.backgroundColor = 'var(--text)';
587
+ }
588
+ } catch (e) { }
589
+ }
590
+
591
+ const host = window.location.host;
592
+ if (host) {
593
+ const protocol = location.protocol === 'https:' ? 'wss' : 'ws';
594
+ const ws = new WebSocket(`${protocol}://${host}/ws/stats?client_id=${CID}`);
595
+ ws.onmessage = (event) => {
596
+ const data = JSON.parse(event.data);
597
+ if (data.type === 'stats') {
598
+ document.getElementById('online-val').innerText = data.online_count;
599
+ } else if (data.type === 'cloud_status') {
600
+ const iframe = document.querySelector('iframe.active');
601
+ if (iframe && iframe.contentWindow) {
602
+ iframe.contentWindow.postMessage(data, '*');
603
+ }
604
+ }
605
+ };
606
+ setInterval(syncStatus, 2000);
607
+ }
608
+
609
+ // --- 夜间模式 ---
610
+
611
+ function syncThemeToFrame(iframe) {
612
+ const theme = (window.StudioTheme || {get: () => 'light'}).get();
613
+ try {
614
+ if (iframe && iframe.contentWindow) {
615
+ iframe.contentWindow.postMessage({ type: 'studio-theme', theme }, '*');
616
+ }
617
+ } catch (e) {}
618
+ }
619
+
620
+ function broadcastTheme(theme) {
621
+ if (window.StudioTheme) {
622
+ window.StudioTheme.set(theme);
623
+ }
624
+ document.querySelectorAll('iframe').forEach(f => syncThemeToFrame(f));
625
+ updateThemeIcon(theme);
626
+ }
627
+
628
+ function updateThemeIcon(theme) {
629
+ const moon = document.getElementById('icon-moon');
630
+ const sun = document.getElementById('icon-sun');
631
+ if (theme === 'dark') {
632
+ moon.style.display = 'none';
633
+ sun.style.display = 'block';
634
+ } else {
635
+ moon.style.display = 'block';
636
+ sun.style.display = 'none';
637
+ }
638
+ }
639
+
640
+ function toggleTheme() {
641
+ const current = window.StudioTheme ? window.StudioTheme.get() : 'light';
642
+ broadcastTheme(current === 'dark' ? 'light' : 'dark');
643
+ }
644
+
645
+ // listen for theme changes triggered by theme.js
646
+ window.addEventListener('studio-theme-change', (e) => {
647
+ updateThemeIcon(e.detail.theme);
648
+ });
649
+
650
+ // init icon state on load
651
+ window.addEventListener('DOMContentLoaded', () => {
652
+ const theme = window.StudioTheme ? window.StudioTheme.get() : 'light';
653
+ updateThemeIcon(theme);
654
+ });
655
+
656
+ // sync theme when iframe loads
657
+ document.querySelectorAll('iframe').forEach(f => {
658
+ f.addEventListener('load', () => syncThemeToFrame(f));
659
+ });
660
+ </script>
661
+ </body>
662
+
663
+ </html>
26-5-10-API-Studio/static/klein.html ADDED
@@ -0,0 +1,800 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <!DOCTYPE html>
2
+ <html lang="zh-CN">
3
+
4
+ <head>
5
+ <meta charset="UTF-8">
6
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
7
+ <link rel="icon" href="/static/logo.png" type="image/png">
8
+ <title>Flux Klein | 极简一体化终端</title>
9
+ <script src="https://cdn.tailwindcss.com"></script>
10
+ <script src="https://unpkg.com/lucide@latest"></script>
11
+ <script src="/static/theme.js?v=20260509"></script>
12
+ <style>
13
+ @import url('https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;800&display=swap');
14
+
15
+ :root {
16
+ --accent: #111827;
17
+ --bg: #f9fafb;
18
+ --card: #ffffff;
19
+ --easing: cubic-bezier(0.4, 0, 0.2, 1);
20
+ }
21
+
22
+ *::-webkit-scrollbar { width:10px!important; height:10px!important; background:transparent!important; }
23
+ *::-webkit-scrollbar-track { background:transparent!important; border:none!important; }
24
+ *::-webkit-scrollbar-thumb { background-color:#d8d8d8!important; border:3px solid transparent!important; border-right-width:5px!important; background-clip:padding-box!important; border-radius:10px!important; }
25
+ *::-webkit-scrollbar-thumb:hover { background-color:#c0c0c0!important; }
26
+ *::-webkit-scrollbar-corner { background:transparent!important; }
27
+ * { scrollbar-width:thin!important; scrollbar-color:#d8d8d8 transparent!important; }
28
+
29
+ body {
30
+ background-color: var(--bg);
31
+ font-family: 'Inter', -apple-system, sans-serif;
32
+ color: var(--accent);
33
+ -webkit-font-smoothing: antialiased;
34
+ }
35
+
36
+ .container-box {
37
+ max-width: 1280px;
38
+ margin: 0 auto;
39
+ padding: 0 40px;
40
+ margin-top: 50px;
41
+ }
42
+
43
+ .nano-input {
44
+ background: var(--card);
45
+ border: 1px solid #eef0f2;
46
+ transition: all 0.3s var(--easing);
47
+ }
48
+ .nano-input:focus { border-color: #000; box-shadow: 0 0 0 1px #000; }
49
+
50
+ .upload-item {
51
+ background: var(--card);
52
+ border: 1px dashed #e2e8f0;
53
+ transition: all 0.4s var(--easing);
54
+ position: relative;
55
+ overflow: hidden;
56
+ }
57
+ .upload-item:hover { border-color: #000; background: #fff; transform: translateY(-2px); }
58
+ .upload-item.drag-over { border-style: solid; border-color: #000; box-shadow: 0 10px 20px rgba(0,0,0,0.05); }
59
+
60
+ .preview-img {
61
+ position: absolute;
62
+ inset: 0;
63
+ width: 100%;
64
+ height: 100%;
65
+ object-fit: cover;
66
+ animation: fadeIn 0.5s var(--easing);
67
+ }
68
+
69
+ .glass-btn {
70
+ background: #111827;
71
+ transition: all 0.3s var(--easing);
72
+ }
73
+ .glass-btn:hover { background: #000; transform: translateY(-1px); box-shadow: 0 12px 24px rgba(0,0,0,0.1); }
74
+ .glass-btn:active { transform: scale(0.98); }
75
+
76
+ /* Engine panel (above run button) */
77
+ .engine-panel {
78
+ background: #fff;
79
+ border: 1px solid #edf0f3;
80
+ border-radius: 24px;
81
+ padding: 12px;
82
+ display: flex;
83
+ flex-direction: column;
84
+ gap: 10px;
85
+ }
86
+ .engine-label {
87
+ display: flex;
88
+ align-items: center;
89
+ gap: 6px;
90
+ color: #94a3b8;
91
+ font-size: 10px;
92
+ font-weight: 600;
93
+ letter-spacing: .04em;
94
+ }
95
+ .engine-switch {
96
+ background: #f8fafc;
97
+ border: 1px solid #edf2f7;
98
+ border-radius: 999px;
99
+ padding: 3px;
100
+ display: grid;
101
+ grid-template-columns: 1fr 1fr;
102
+ gap: 2px;
103
+ }
104
+ .engine-btn {
105
+ border-radius: 999px;
106
+ height: 34px;
107
+ display: flex;
108
+ align-items: center;
109
+ justify-content: center;
110
+ gap: 6px;
111
+ color: #64748b;
112
+ font-size: 10px;
113
+ font-weight: 600;
114
+ letter-spacing: .02em;
115
+ transition: all .25s var(--easing);
116
+ cursor: pointer;
117
+ }
118
+ .engine-btn.active {
119
+ background: #fff;
120
+ color: #111827;
121
+ box-shadow: 0 1px 3px rgba(15,23,42,.08);
122
+ }
123
+ .engine-btn:not(.active):hover { color: #111827; background: rgba(0,0,0,.04); }
124
+
125
+ /* Cloud progress */
126
+ .cloud-status {
127
+ font-size: 11px;
128
+ font-weight: 700;
129
+ letter-spacing: 0.1em;
130
+ text-transform: uppercase;
131
+ color: #64748b;
132
+ display: none;
133
+ align-items: center;
134
+ gap: 8px;
135
+ }
136
+ .cloud-status.visible { display: flex; }
137
+ .cloud-dot {
138
+ width: 6px;
139
+ height: 6px;
140
+ border-radius: 50%;
141
+ background: #10b981;
142
+ animation: pulse 1.5s ease-in-out infinite;
143
+ }
144
+ @keyframes pulse { 0%,100%{opacity:1;transform:scale(1)} 50%{opacity:.5;transform:scale(1.4)} }
145
+
146
+ .result-frame {
147
+ background: #ffffff;
148
+ border-radius: 32px;
149
+ border: 1px solid #f1f5f9;
150
+ box-shadow: 0 2px 15px rgba(0,0,0,0.02);
151
+ }
152
+
153
+ .masonry-grid {
154
+ display: grid;
155
+ grid-template-columns: repeat(2, 1fr);
156
+ gap: 20px;
157
+ }
158
+ @media (min-width: 768px) { .masonry-grid { grid-template-columns: repeat(4, 1fr); } }
159
+
160
+ .masonry-item {
161
+ aspect-ratio: 1 / 1;
162
+ border-radius: 24px;
163
+ overflow: hidden;
164
+ background: #fff;
165
+ border: 1px solid #f1f5f9;
166
+ transition: all 0.5s var(--easing);
167
+ }
168
+ .masonry-item:hover { transform: translateY(-6px); box-shadow: 0 20px 40px rgba(0,0,0,0.08); }
169
+
170
+ @keyframes fadeIn { from { opacity: 0; } to { opacity: 1; } }
171
+ </style>
172
+ <link rel="stylesheet" href="/static/theme.css?v=20260510-studio-pages-blue-dark10">
173
+ </head>
174
+
175
+ <body class="antialiased transition-colors duration-300">
176
+ <div class="container-box min-h-screen flex flex-col">
177
+ <header class="flex justify-between items-end mb-10">
178
+ <div class="space-y-1">
179
+ <h1 class="text-4xl font-extrabold tracking-tighter italic">FLUX KLEIN</h1>
180
+ <p class="text-[10px] font-bold uppercase tracking-[0.4em] text-gray-400">Next-Gen Generative Interface</p>
181
+ </div>
182
+ </header>
183
+
184
+ <main class="grid grid-cols-1 lg:grid-cols-12 gap-12">
185
+ <div class="lg:col-span-5 space-y-10">
186
+ <section class="space-y-4">
187
+ <div class="flex items-center gap-2 text-gray-400">
188
+ <i data-lucide="terminal" class="w-3.5 h-3.5"></i>
189
+ <span class="text-[10px] font-black uppercase tracking-widest">Input Prompt</span>
190
+ </div>
191
+ <textarea id="promptInput" rows="5"
192
+ class="nano-input w-full p-6 rounded-3xl text-sm outline-none resize-none placeholder-gray-300"
193
+ placeholder="Describe your vision here..."></textarea>
194
+ </section>
195
+
196
+ <section class="space-y-4">
197
+ <div class="flex items-center gap-2 text-gray-400">
198
+ <i data-lucide="image" class="w-3.5 h-3.5"></i>
199
+ <span class="text-[10px] font-black uppercase tracking-widest">Reference Layers</span>
200
+ <span id="cloudImgNote" class="hidden ml-auto text-[9px] font-bold text-blue-500 bg-blue-50 px-2 py-0.5 rounded-full">Cloud uses Main image</span>
201
+ </div>
202
+ <div class="grid grid-cols-3 gap-4">
203
+ <div id="drop-zone-1" onclick="document.getElementById('file1').click()"
204
+ class="upload-item group aspect-square rounded-2xl flex flex-col items-center justify-center cursor-pointer">
205
+ <input type="file" id="file1" class="hidden" onchange="handleFile(this.files[0], 1)">
206
+ <i data-lucide="plus" class="w-5 h-5 text-gray-300 group-hover:text-black transition-colors"></i>
207
+ <span class="text-[9px] mt-2 font-bold text-gray-400 uppercase">Main</span>
208
+ <img id="prev1" class="preview-img hidden">
209
+ <button id="del1" onclick="clearSlot(1, event)"
210
+ class="hidden absolute top-2 right-2 w-6 h-6 bg-white/90 rounded-full shadow-sm z-10 flex items-center justify-center text-xs">×</button>
211
+ </div>
212
+ <div id="drop-zone-2" onclick="document.getElementById('file2').click()"
213
+ class="upload-item group aspect-square rounded-2xl flex flex-col items-center justify-center cursor-pointer">
214
+ <input type="file" id="file2" class="hidden" onchange="handleFile(this.files[0], 2)">
215
+ <i data-lucide="plus" class="w-5 h-5 text-gray-300 group-hover:text-black transition-colors"></i>
216
+ <span class="text-[9px] mt-2 font-bold text-gray-400 uppercase">Aux A</span>
217
+ <img id="prev2" class="preview-img hidden">
218
+ <button id="del2" onclick="clearSlot(2, event)"
219
+ class="hidden absolute top-2 right-2 w-6 h-6 bg-white/90 rounded-full shadow-sm z-10 flex items-center justify-center text-xs">×</button>
220
+ </div>
221
+ <div id="drop-zone-3" onclick="document.getElementById('file3').click()"
222
+ class="upload-item group aspect-square rounded-2xl flex flex-col items-center justify-center cursor-pointer">
223
+ <input type="file" id="file3" class="hidden" onchange="handleFile(this.files[0], 3)">
224
+ <i data-lucide="plus" class="w-5 h-5 text-gray-300 group-hover:text-black transition-colors"></i>
225
+ <span class="text-[9px] mt-2 font-bold text-gray-400 uppercase">Aux B</span>
226
+ <img id="prev3" class="preview-img hidden">
227
+ <button id="del3" onclick="clearSlot(3, event)"
228
+ class="hidden absolute top-2 right-2 w-6 h-6 bg-white/90 rounded-full shadow-sm z-10 flex items-center justify-center text-xs">×</button>
229
+ </div>
230
+ </div>
231
+ </section>
232
+
233
+ <div class="engine-panel">
234
+ <div class="engine-label">
235
+ <i data-lucide="cpu" class="w-3.5 h-3.5"></i>
236
+ <span>Engine</span>
237
+ </div>
238
+ <div class="engine-switch">
239
+ <button id="localBtn" class="engine-btn active" onclick="setEngine('local')">
240
+ <i data-lucide="cpu" class="w-3 h-3"></i>Local
241
+ </button>
242
+ <button id="cloudBtn" class="engine-btn" onclick="setEngine('cloud')">
243
+ <i data-lucide="cloud" class="w-3 h-3"></i>ModelScope
244
+ </button>
245
+ </div>
246
+ <div id="cloudStatusBar" class="cloud-status">
247
+ <div class="cloud-dot"></div>
248
+ <span id="cloudStatusText">Connecting...</span>
249
+ </div>
250
+ <div id="loraSection" class="hidden flex-col gap-3 pt-2 border-t border-gray-100">
251
+ <label class="flex items-center gap-3 cursor-pointer select-none" onclick="toggleLora()">
252
+ <div class="relative w-9 h-5 flex-shrink-0">
253
+ <div id="loraTrack" class="w-full h-full bg-gray-200 rounded-full transition-colors duration-200"></div>
254
+ <div id="loraThumb" class="absolute left-0.5 top-0.5 w-4 h-4 bg-white rounded-full shadow transition-transform duration-200"></div>
255
+ </div>
256
+ <span class="text-[10px] font-bold text-gray-600 uppercase tracking-wider">细节增强 LoRA</span>
257
+ </label>
258
+ <div id="loraStrengthRow" class="hidden flex-col gap-1.5">
259
+ <div class="flex justify-between">
260
+ <span class="text-[9px] font-bold text-gray-400 uppercase tracking-widest">LoRA Strength</span>
261
+ <span id="loraStrengthVal" class="text-[9px] font-bold text-gray-600">0.80</span>
262
+ </div>
263
+ <input type="range" id="loraStrengthSlider" min="0.1" max="1.0" step="0.05" value="0.8"
264
+ class="w-full cursor-pointer accent-black" style="height:2px"
265
+ oninput="document.getElementById('loraStrengthVal').textContent=parseFloat(this.value).toFixed(2)">
266
+ </div>
267
+ </div>
268
+ </div>
269
+
270
+ <button id="genBtn" onclick="submitWorkflow()"
271
+ class="glass-btn w-full py-5 text-white rounded-xl font-bold flex items-center justify-center gap-3 shadow-lg">
272
+ <i data-lucide="zap" id="btnIcon" class="w-4 h-4 text-yellow-400"></i>
273
+ <span id="btnText" class="tracking-[0.3em] text-[11px] uppercase">Execute Synthesis</span>
274
+ </button>
275
+ </div>
276
+
277
+ <div class="lg:col-span-7">
278
+ <div id="resultBox"
279
+ class="result-frame min-h-[500px] lg:h-full flex items-center justify-center relative overflow-hidden group">
280
+ <div id="placeholder" class="text-center space-y-4 opacity-20">
281
+ <i data-lucide="layout" class="w-12 h-12 mx-auto stroke-[1px]"></i>
282
+ <p class="text-[10px] font-black tracking-[0.5em] uppercase">Canvas Ready</p>
283
+ </div>
284
+ <div id="loader" class="hidden text-center space-y-4">
285
+ <div class="w-8 h-8 border-2 border-black border-t-transparent rounded-full animate-spin mx-auto"></div>
286
+ <p class="text-[10px] font-black tracking-[0.5em] uppercase">Synthesizing</p>
287
+ </div>
288
+ <img id="outputImg"
289
+ class="hidden w-full h-full object-contain p-8 cursor-zoom-in transition-all duration-700"
290
+ onclick="zoomImage()">
291
+ <a id="downloadBtn" href="#" download
292
+ class="hidden absolute top-8 right-8 w-12 h-12 bg-white/90 backdrop-blur-md shadow-2xl rounded-2xl flex items-center justify-center hover:bg-black hover:text-white active:scale-95 transition-all">
293
+ <i data-lucide="download" class="w-4 h-4"></i>
294
+ </a>
295
+ </div>
296
+ </div>
297
+ </main>
298
+
299
+ <section class="mt-32">
300
+ <div class="flex items-center gap-6 mb-12">
301
+ <h2 class="text-[11px] font-black uppercase tracking-[0.5em]">Archives</h2>
302
+ <div class="h-px flex-1 bg-gray-100"></div>
303
+ </div>
304
+ <div id="masonry" class="masonry-grid"></div>
305
+ <div id="loadMoreTrigger"
306
+ class="py-20 text-center text-gray-300 text-[10px] font-bold uppercase tracking-widest cursor-pointer hover:text-black transition-colors">
307
+ Load More Archive
308
+ </div>
309
+ </section>
310
+ </div>
311
+
312
+ <div id="lightbox" onclick="handleOutsideClick(event)"
313
+ class="hidden fixed inset-0 z-50 flex items-center justify-center p-6 bg-white/95 backdrop-blur-3xl">
314
+ <div class="max-w-6xl w-full flex flex-col items-center relative">
315
+ <div class="relative w-full flex justify-center mb-8">
316
+ <div id="compareContainer"
317
+ class="hidden relative w-full h-[75vh] rounded-[2.5rem] overflow-hidden shadow-2xl bg-[#fafafa]">
318
+ <img id="compareGenerated" class="absolute inset-0 w-full h-full object-contain">
319
+ <div id="compareOriginalWrapper"
320
+ class="absolute inset-0 w-full h-full overflow-hidden border-r-2 border-white/50">
321
+ <img id="compareOriginal" class="absolute inset-0 w-full h-full object-contain">
322
+ </div>
323
+ <div id="compareSlider" class="absolute inset-y-0 left-1/2 w-0.5 bg-white z-20 cursor-ew-resize">
324
+ <div class="absolute top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2 w-10 h-10 bg-white shadow-2xl rounded-full flex items-center justify-center border border-gray-100">
325
+ <i data-lucide="move-horizontal" class="w-4 h-4 text-black"></i>
326
+ </div>
327
+ </div>
328
+ </div>
329
+ <img id="lightboxImg" src="" class="hidden max-h-[75vh] rounded-[2.5rem] shadow-2xl object-contain">
330
+ <div id="lightboxRes"
331
+ class="absolute top-6 left-6 bg-black/30 backdrop-blur-md border border-white/20 text-white px-3 py-1.5 rounded-full text-[10px] font-medium tracking-wider opacity-0 transition-opacity duration-300 pointer-events-none z-30">
332
+ </div>
333
+ <button onclick="downloadLightboxImage()"
334
+ class="absolute top-6 right-6 bg-black text-white w-12 h-12 rounded-2xl flex items-center justify-center shadow-2xl z-30 hover:scale-105 transition-transform">
335
+ <i data-lucide="download" class="w-5 h-5"></i>
336
+ </button>
337
+ </div>
338
+ <div class="w-full bg-white border border-gray-100 rounded-[2rem] p-8 shadow-sm flex justify-between items-center gap-8">
339
+ <div class="flex-1">
340
+ <span class="text-[9px] font-black text-gray-300 uppercase tracking-widest block mb-2">Prompt Execution</span>
341
+ <p id="lightboxPrompt" class="text-gray-700 text-sm leading-relaxed"></p>
342
+ </div>
343
+ <button id="sameStyleBtn" onclick="applySameStyle()"
344
+ class="hidden whitespace-nowrap bg-black text-white px-8 py-3.5 rounded-2xl text-[10px] font-black uppercase tracking-widest hover:bg-gray-800 transition-all active:scale-95 flex items-center gap-2">
345
+ <i data-lucide="copy" class="w-4 h-4"></i> Replicate
346
+ </button>
347
+ </div>
348
+ <button onclick="closeLightbox()"
349
+ class="absolute -top-12 -right-12 p-4 text-gray-400 hover:text-black transition-colors">
350
+ <i data-lucide="x" class="w-8 h-8"></i>
351
+ </button>
352
+ </div>
353
+ </div>
354
+
355
+ <script>
356
+ lucide.createIcons();
357
+
358
+ function generateUUID() {
359
+ if (typeof crypto !== 'undefined' && crypto.randomUUID) {
360
+ try { return crypto.randomUUID(); } catch (e) { }
361
+ }
362
+ return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function (c) {
363
+ var r = Math.random() * 16 | 0, v = c == 'x' ? r : (r & 0x3 | 0x8);
364
+ return v.toString(16);
365
+ });
366
+ }
367
+ const CLIENT_ID_KEY = "client_id";
368
+ let CLIENT_ID = localStorage.getItem(CLIENT_ID_KEY) || generateUUID();
369
+ localStorage.setItem(CLIENT_ID_KEY, CLIENT_ID);
370
+
371
+ let uploadedNames = { 1: "", 2: "", 3: "" };
372
+ let base64Images = { 1: "", 2: "", 3: "" };
373
+ let allHistory = [];
374
+ let currentResult = null;
375
+ let currentLightboxData = null;
376
+ let currentIndex = 0;
377
+ const PAGE_SIZE = 24;
378
+ let isLoading = false;
379
+ let engine = 'local';
380
+ let loraEnabled = false;
381
+
382
+ // 引擎切换
383
+ function setEngine(mode) {
384
+ engine = mode;
385
+ document.getElementById('localBtn').classList.toggle('active', mode === 'local');
386
+ document.getElementById('cloudBtn').classList.toggle('active', mode === 'cloud');
387
+ document.getElementById('cloudImgNote').classList.toggle('hidden', mode === 'local');
388
+ const loraSection = document.getElementById('loraSection');
389
+ if (mode === 'cloud') {
390
+ loraSection.classList.remove('hidden');
391
+ loraSection.classList.add('flex');
392
+ } else {
393
+ loraSection.classList.add('hidden');
394
+ loraSection.classList.remove('flex');
395
+ }
396
+ }
397
+
398
+ function toggleLora() {
399
+ loraEnabled = !loraEnabled;
400
+ document.getElementById('loraTrack').style.background = loraEnabled ? '#111827' : '';
401
+ document.getElementById('loraThumb').style.transform = loraEnabled ? 'translateX(16px)' : '';
402
+ const row = document.getElementById('loraStrengthRow');
403
+ row.classList.toggle('hidden', !loraEnabled);
404
+ row.classList.toggle('flex', loraEnabled);
405
+ }
406
+
407
+ function setCloudStatus(text, visible = true) {
408
+ const bar = document.getElementById('cloudStatusBar');
409
+ const label = document.getElementById('cloudStatusText');
410
+ label.textContent = text;
411
+ bar.classList.toggle('visible', visible);
412
+ }
413
+
414
+ // 拖拽上传
415
+ let hoveredSlot = null;
416
+ [1, 2, 3].forEach(id => {
417
+ const zone = document.getElementById(`drop-zone-${id}`);
418
+ if (!zone) return;
419
+ zone.ondragover = (e) => { e.preventDefault(); zone.classList.add('drag-over'); };
420
+ zone.ondragleave = () => { zone.classList.remove('drag-over'); };
421
+ zone.ondrop = (e) => { e.preventDefault(); zone.classList.remove('drag-over'); handleFile(e.dataTransfer.files[0], id); };
422
+ zone.addEventListener('mouseenter', () => hoveredSlot = id);
423
+ zone.addEventListener('mouseleave', () => { if (hoveredSlot === id) hoveredSlot = null; });
424
+ });
425
+
426
+ window.addEventListener('paste', (e) => {
427
+ if (!hoveredSlot) return;
428
+ const items = (e.clipboardData || e.originalEvent.clipboardData).items;
429
+ for (let item of items) {
430
+ if (item.kind === 'file' && item.type.startsWith('image/')) {
431
+ handleFile(item.getAsFile(), hoveredSlot);
432
+ break;
433
+ }
434
+ }
435
+ });
436
+
437
+ async function handleFile(file, index) {
438
+ if (!file) return;
439
+ const reader = new FileReader();
440
+ reader.onload = (e) => {
441
+ base64Images[index] = e.target.result;
442
+ const prev = document.getElementById(`prev${index}`);
443
+ prev.src = e.target.result;
444
+ prev.classList.remove('hidden');
445
+ document.getElementById(`del${index}`).classList.remove('hidden');
446
+ };
447
+ reader.readAsDataURL(file);
448
+
449
+ // Also upload to ComfyUI for local mode
450
+ const formData = new FormData();
451
+ formData.append('files', file);
452
+ try {
453
+ const res = await fetch('/api/upload', { method: 'POST', body: formData });
454
+ const data = await res.json();
455
+ uploadedNames[index] = data.files[0].comfy_name;
456
+ } catch (e) { uploadedNames[index] = file.name; }
457
+ }
458
+
459
+ function clearSlot(index, ev) {
460
+ if (ev) ev.stopPropagation();
461
+ const prev = document.getElementById(`prev${index}`);
462
+ prev.src = ""; prev.classList.add("hidden");
463
+ document.getElementById(`del${index}`).classList.add("hidden");
464
+ uploadedNames[index] = "";
465
+ base64Images[index] = "";
466
+ }
467
+
468
+ async function submitWorkflow() {
469
+ if (engine === 'cloud') {
470
+ await submitCloud();
471
+ } else {
472
+ await submitLocal();
473
+ }
474
+ }
475
+
476
+ async function submitLocal() {
477
+ if (!uploadedNames[1]) { alert("Please upload Main Image (Slot 1)"); return; }
478
+ const btn = document.getElementById('genBtn');
479
+ const loader = document.getElementById('loader');
480
+ const placeholder = document.getElementById('placeholder');
481
+ const outputImg = document.getElementById('outputImg');
482
+ const downloadBtn = document.getElementById('downloadBtn');
483
+
484
+ btn.disabled = true;
485
+ btn.style.backgroundColor = '#333';
486
+ btn.innerHTML = `<i data-lucide="zap" class="w-4 h-4 text-yellow-400 fill-yellow-400 animate-pulse"></i><span class="tracking-[0.3em] text-[11px] uppercase">Synthesizing...</span>`;
487
+ lucide.createIcons();
488
+ placeholder.classList.add('hidden');
489
+ loader.classList.remove('hidden');
490
+
491
+ const payload = {
492
+ prompt: document.getElementById('promptInput').value,
493
+ workflow_json: "Flux2-Klein.json",
494
+ type: "klein",
495
+ params: {
496
+ "168": { "text": document.getElementById('promptInput').value },
497
+ "158": { "noise_seed": Math.floor(Math.random() * 1000000) },
498
+ "278": { "image": uploadedNames[1] },
499
+ "270": { "image": uploadedNames[2] || "" },
500
+ "292": { "image": uploadedNames[3] || "" },
501
+ "313": { "value": uploadedNames[2] !== "" },
502
+ "314": { "value": uploadedNames[3] !== "" }
503
+ }
504
+ };
505
+
506
+ try {
507
+ const response = await fetch('/api/generate', {
508
+ method: 'POST',
509
+ headers: { 'Content-Type': 'application/json' },
510
+ body: JSON.stringify({ ...payload, client_id: CLIENT_ID })
511
+ });
512
+ const result = await response.json();
513
+ if (result.images?.[0]) {
514
+ currentResult = result;
515
+ outputImg.src = result.images[0];
516
+ outputImg.classList.remove('hidden');
517
+ downloadBtn.classList.remove('hidden');
518
+ downloadBtn.href = result.images[0];
519
+ renderImageCard(result, true);
520
+ } else if (result.error) {
521
+ alert("Generation failed: " + result.error);
522
+ placeholder.classList.remove('hidden');
523
+ }
524
+ } catch (err) {
525
+ alert("Generation failed: " + err.message);
526
+ placeholder.classList.remove('hidden');
527
+ } finally {
528
+ loader.classList.add('hidden');
529
+ btn.disabled = false;
530
+ btn.style.backgroundColor = '';
531
+ btn.innerHTML = `<i data-lucide="zap" class="w-4 h-4 text-yellow-400"></i><span class="tracking-[0.3em] text-[11px] uppercase">Execute Synthesis</span>`;
532
+ lucide.createIcons();
533
+ }
534
+ }
535
+
536
+ async function submitCloud() {
537
+ const prompt = document.getElementById('promptInput').value.trim();
538
+ if (!prompt) { alert("Please enter a prompt"); return; }
539
+ if (!base64Images[1]) { alert("Please upload Main Image (Slot 1) for cloud generation"); return; }
540
+
541
+ const btn = document.getElementById('genBtn');
542
+ const loader = document.getElementById('loader');
543
+ const placeholder = document.getElementById('placeholder');
544
+ const outputImg = document.getElementById('outputImg');
545
+ const downloadBtn = document.getElementById('downloadBtn');
546
+
547
+ btn.disabled = true;
548
+ btn.style.backgroundColor = '#333';
549
+ btn.innerHTML = `<i data-lucide="cloud" class="w-4 h-4 text-blue-400 animate-pulse"></i><span class="tracking-[0.3em] text-[11px] uppercase">Cloud Processing...</span>`;
550
+ lucide.createIcons();
551
+ placeholder.classList.add('hidden');
552
+ loader.classList.remove('hidden');
553
+ setCloudStatus('Submitting to ModelScope...');
554
+
555
+ // Collect image URLs (base64 data URIs)
556
+ const imageUrls = [base64Images[1], base64Images[2], base64Images[3]].filter(Boolean);
557
+
558
+ try {
559
+ setCloudStatus('Processing with FLUX.2-Klein...');
560
+ const msPayload = {
561
+ prompt,
562
+ model: 'black-forest-labs/FLUX.2-klein-9B',
563
+ image_urls: imageUrls,
564
+ client_id: CLIENT_ID
565
+ };
566
+ if (loraEnabled) {
567
+ const strength = parseFloat(document.getElementById('loraStrengthSlider').value);
568
+ msPayload.loras = { 'Daniel8152/Klein-enhance': strength };
569
+ }
570
+ const response = await fetch('/api/ms/generate', {
571
+ method: 'POST',
572
+ headers: { 'Content-Type': 'application/json' },
573
+ body: JSON.stringify(msPayload)
574
+ });
575
+
576
+ if (!response.ok) {
577
+ const err = await response.json();
578
+ throw new Error(err.detail || 'Cloud generation failed');
579
+ }
580
+
581
+ const result = await response.json();
582
+ if (result.url) {
583
+ const resultData = {
584
+ prompt,
585
+ images: [result.url],
586
+ timestamp: Date.now() / 1000,
587
+ type: 'klein',
588
+ model: 'black-forest-labs/FLUX.2-klein-9B'
589
+ };
590
+ currentResult = resultData;
591
+ outputImg.src = result.url;
592
+ outputImg.classList.remove('hidden');
593
+ downloadBtn.classList.remove('hidden');
594
+ downloadBtn.href = result.url;
595
+ renderImageCard(resultData, true);
596
+ setCloudStatus('Done!', false);
597
+ }
598
+ } catch (err) {
599
+ setCloudStatus(err.message, false);
600
+ alert("Cloud generation failed: " + err.message);
601
+ placeholder.classList.remove('hidden');
602
+ } finally {
603
+ loader.classList.add('hidden');
604
+ btn.disabled = false;
605
+ btn.style.backgroundColor = '';
606
+ btn.innerHTML = `<i data-lucide="zap" class="w-4 h-4 text-yellow-400"></i><span class="tracking-[0.3em] text-[11px] uppercase">Execute Synthesis</span>`;
607
+ lucide.createIcons();
608
+ }
609
+ }
610
+
611
+ // --- Compare Slider ---
612
+ function initCompareSlider() {
613
+ const container = document.getElementById('compareContainer');
614
+ const wrapper = document.getElementById('compareOriginalWrapper');
615
+ const slider = document.getElementById('compareSlider');
616
+ let isDragging = false;
617
+ const updateSlider = (clientX) => {
618
+ const rect = container.getBoundingClientRect();
619
+ let percent = Math.max(0, Math.min(100, (clientX - rect.left) / rect.width * 100));
620
+ wrapper.style.clipPath = `inset(0 ${100 - percent}% 0 0)`;
621
+ slider.style.left = `${percent}%`;
622
+ };
623
+ const start = (e) => { isDragging = true; e.preventDefault(); };
624
+ const end = () => isDragging = false;
625
+ const move = (e) => {
626
+ if (!isDragging) return;
627
+ updateSlider(e.touches ? e.touches[0].clientX : e.clientX);
628
+ };
629
+ container.addEventListener('mousedown', (e) => { if (e.target === slider) return; updateSlider(e.clientX); start(e); });
630
+ slider.addEventListener('mousedown', start);
631
+ window.addEventListener('mouseup', end);
632
+ window.addEventListener('mousemove', move);
633
+ slider.addEventListener('touchstart', start, { passive: false });
634
+ window.addEventListener('touchend', end);
635
+ window.addEventListener('touchmove', move, { passive: false });
636
+ }
637
+ initCompareSlider();
638
+
639
+ function openLightbox(dataOrUrl) {
640
+ const lb = document.getElementById('lightbox');
641
+ const img = document.getElementById('lightboxImg');
642
+ const comp = document.getElementById('compareContainer');
643
+ const promptEl = document.getElementById('lightboxPrompt');
644
+ const sameStyleBtn = document.getElementById('sameStyleBtn');
645
+ const resPill = document.getElementById('lightboxRes');
646
+
647
+ let data = (typeof dataOrUrl === 'string') ? { images: [dataOrUrl] } : dataOrUrl;
648
+ currentLightboxData = data;
649
+ promptEl.textContent = data.prompt || "No prompt metadata found";
650
+ resPill.style.opacity = '0';
651
+
652
+ const updateRes = (target) => {
653
+ if (target.naturalWidth) {
654
+ resPill.innerText = `${target.naturalWidth} x ${target.naturalHeight}`;
655
+ resPill.style.opacity = '1';
656
+ }
657
+ };
658
+
659
+ if (data.params?.["278"]?.image) {
660
+ img.classList.add('hidden');
661
+ comp.classList.remove('hidden');
662
+ const genImg = document.getElementById('compareGenerated');
663
+ genImg.src = data.images[0];
664
+ document.getElementById('compareOriginal').src = `/api/view?filename=${encodeURIComponent(data.params["278"].image)}&type=input`;
665
+ document.getElementById('compareOriginalWrapper').style.clipPath = 'inset(0 50% 0 0)';
666
+ document.getElementById('compareSlider').style.left = '50%';
667
+ genImg.onload = () => updateRes(genImg);
668
+ if (genImg.complete) updateRes(genImg);
669
+ } else {
670
+ comp.classList.add('hidden');
671
+ img.classList.remove('hidden');
672
+ img.src = data.images[0];
673
+ img.onload = () => updateRes(img);
674
+ if (img.complete) updateRes(img);
675
+ }
676
+
677
+ sameStyleBtn.classList.toggle('hidden', !data.params);
678
+ lb.classList.replace('hidden', 'flex');
679
+ document.body.style.overflow = 'hidden';
680
+ }
681
+
682
+ function closeLightbox() {
683
+ document.getElementById('lightbox').classList.replace('flex', 'hidden');
684
+ document.body.style.overflow = 'auto';
685
+ }
686
+
687
+ function handleOutsideClick(e) { if (e.target.id === 'lightbox') closeLightbox(); }
688
+
689
+ function renderImageCard(data, isNew = false) {
690
+ const masonry = document.getElementById('masonry');
691
+ if (document.getElementById(`history-${data.timestamp}`)) return;
692
+ const card = document.createElement('div');
693
+ card.id = `history-${data.timestamp}`;
694
+ card.className = 'masonry-item group relative cursor-zoom-in';
695
+ card.onclick = () => openLightbox(data);
696
+ card.innerHTML = `
697
+ <img src="${data.images[0]}" class="w-full h-full object-cover block transform group-hover:scale-105 transition-transform duration-1000" loading="lazy">
698
+ <button onclick="deleteHistoryItem('${data.timestamp}', event)" class="absolute top-4 right-4 text-white hover:text-red-400 opacity-0 group-hover:opacity-100 transition-opacity z-10">
699
+ <i data-lucide="trash-2" class="w-4 h-4"></i>
700
+ </button>
701
+ <div class="absolute inset-0 bg-gradient-to-t from-black/80 via-transparent to-transparent opacity-0 group-hover:opacity-100 transition-all p-6 flex flex-col justify-end">
702
+ <p class="text-white text-[10px] font-medium line-clamp-2 uppercase tracking-wider">${data.prompt || "Klein Archive"}</p>
703
+ ${data.model ? `<p class="text-white/50 text-[9px] mt-1">${data.model.split('/').pop()}</p>` : ''}
704
+ </div>
705
+ `;
706
+ isNew ? masonry.prepend(card) : masonry.appendChild(card);
707
+ lucide.createIcons();
708
+ }
709
+
710
+ async function applySameStyle() {
711
+ if (!currentLightboxData?.params) return;
712
+ document.getElementById('promptInput').value = currentLightboxData.prompt || "";
713
+ const params = currentLightboxData.params;
714
+ const setSlot = (slotId, nodeId) => {
715
+ if (params[nodeId]?.image) {
716
+ const fname = params[nodeId].image;
717
+ uploadedNames[slotId] = fname;
718
+ const prev = document.getElementById(`prev${slotId}`);
719
+ prev.src = `/api/view?filename=${encodeURIComponent(fname)}&type=input`;
720
+ prev.classList.remove('hidden');
721
+ document.getElementById(`del${slotId}`).classList.remove('hidden');
722
+ } else { clearSlot(slotId); }
723
+ };
724
+ setSlot(1, "278"); setSlot(2, "270"); setSlot(3, "292");
725
+ closeLightbox();
726
+ window.scrollTo({ top: 0, behavior: 'smooth' });
727
+ }
728
+
729
+ async function loadHistory(page = 0) {
730
+ if (isLoading) return;
731
+ const loader = document.getElementById('loadMoreTrigger');
732
+ try {
733
+ isLoading = true;
734
+ if (page === 0) {
735
+ loader.innerText = "Loading Archives...";
736
+ const res = await fetch('/api/history?type=klein');
737
+ allHistory = await res.json();
738
+ document.getElementById('masonry').innerHTML = '';
739
+ currentIndex = 0;
740
+ } else {
741
+ loader.innerText = "Loading...";
742
+ await new Promise(r => setTimeout(r, 400));
743
+ }
744
+ const nextData = allHistory.slice(currentIndex, currentIndex + PAGE_SIZE);
745
+ nextData.forEach(item => renderImageCard(item));
746
+ currentIndex += nextData.length;
747
+ if (currentIndex >= allHistory.length) {
748
+ loader.classList.add('hidden');
749
+ } else {
750
+ loader.classList.remove('hidden');
751
+ loader.innerText = "Load More Archive";
752
+ }
753
+ } catch (e) {
754
+ console.error(e);
755
+ loader.textContent = "Error loading history";
756
+ } finally {
757
+ isLoading = false;
758
+ }
759
+ }
760
+
761
+ function zoomImage() { if (currentResult) openLightbox(currentResult); }
762
+
763
+ function downloadLightboxImage() {
764
+ const url = currentLightboxData?.images[0];
765
+ if (!url) return;
766
+ const link = document.createElement('a');
767
+ link.href = url;
768
+ link.download = `Klein-${Date.now()}.png`;
769
+ link.click();
770
+ }
771
+
772
+ async function deleteHistoryItem(ts, ev) {
773
+ ev.stopPropagation();
774
+ if (!confirm("Delete this archive?")) return;
775
+ try {
776
+ const res = await fetch('/api/history/delete', {
777
+ method: 'POST',
778
+ headers: { 'Content-Type': 'application/json' },
779
+ body: JSON.stringify({ timestamp: ts })
780
+ });
781
+ if ((await res.json()).success) {
782
+ document.getElementById(`history-${ts}`).remove();
783
+ }
784
+ } catch (e) { alert("Delete failed"); }
785
+ }
786
+
787
+ const observer = new IntersectionObserver((entries) => {
788
+ if (entries[0].isIntersecting && !isLoading && currentIndex < allHistory.length) loadHistory(1);
789
+ }, { threshold: 0.1 });
790
+
791
+ window.onload = () => {
792
+ loadHistory(0).then(() => {
793
+ const trigger = document.getElementById('loadMoreTrigger');
794
+ if (trigger) { observer.observe(trigger); trigger.onclick = () => loadHistory(1); }
795
+ });
796
+ };
797
+ </script>
798
+ </body>
799
+
800
+ </html>
26-5-10-API-Studio/static/login.html ADDED
@@ -0,0 +1,292 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <!DOCTYPE html>
2
+ <html lang="zh-CN">
3
+ <head>
4
+ <meta charset="UTF-8">
5
+ <title>Impact & Bound Square - Enhanced</title>
6
+ <style>
7
+ body {
8
+ margin: 0;
9
+ overflow: hidden;
10
+ background-color: #000;
11
+ font-family: 'Inter', -apple-system, sans-serif;
12
+ }
13
+ canvas { display: block; }
14
+
15
+ #loginForm {
16
+ position: absolute;
17
+ top: 50%;
18
+ left: 50%;
19
+ transform: translate(-50%, -40%);
20
+ opacity: 0;
21
+ visibility: hidden;
22
+ transition: all 0.8s cubic-bezier(0.16, 1, 0.3, 1);
23
+ z-index: 10;
24
+ width: 280px;
25
+ padding: 45px;
26
+ background: rgba(255, 255, 255, 0.02);
27
+ backdrop-filter: blur(20px);
28
+ -webkit-backdrop-filter: blur(20px);
29
+ border-radius: 40px;
30
+ border: 1px solid rgba(255, 255, 255, 0.08);
31
+ }
32
+
33
+ #loginForm.visible {
34
+ opacity: 1;
35
+ visibility: visible;
36
+ transform: translate(-50%, -50%);
37
+ }
38
+
39
+ .label {
40
+ color: rgba(255, 255, 255, 0.3);
41
+ font-size: 10px;
42
+ letter-spacing: 4px;
43
+ margin-bottom: 8px;
44
+ text-transform: uppercase;
45
+ }
46
+
47
+ .form-group {
48
+ position: relative;
49
+ margin-bottom: 35px;
50
+ }
51
+
52
+ .form-group input {
53
+ width: 100%;
54
+ padding: 12px 0;
55
+ background: transparent;
56
+ border: none;
57
+ border-bottom: 1px solid rgba(255, 255, 255, 0.1);
58
+ color: #fff;
59
+ font-size: 14px;
60
+ outline: none;
61
+ }
62
+
63
+ .form-group::after {
64
+ content: '';
65
+ position: absolute;
66
+ bottom: 0; left: 0;
67
+ width: 0; height: 1px;
68
+ background: #fff;
69
+ transition: width 0.6s ease;
70
+ }
71
+
72
+ .form-group input:focus ~ ::after { width: 100%; }
73
+
74
+ #loginButton {
75
+ width: 100%;
76
+ padding: 16px;
77
+ background: #fff;
78
+ border: none;
79
+ border-radius: 50px;
80
+ color: #000;
81
+ font-size: 11px;
82
+ font-weight: 800;
83
+ letter-spacing: 6px;
84
+ cursor: pointer;
85
+ transition: all 0.4s;
86
+ margin-top: 10px;
87
+ }
88
+
89
+ #loginButton:hover {
90
+ transform: scale(1.05);
91
+ box-shadow: 0 0 40px rgba(255, 255, 255, 0.4);
92
+ }
93
+
94
+ .timestamp {
95
+ position: absolute;
96
+ top: 40px;
97
+ right: 40px;
98
+ color: rgba(255, 255, 255, 0.15);
99
+ font-size: 10px;
100
+ font-family: monospace;
101
+ }
102
+ </style>
103
+ </head>
104
+ <body>
105
+ <div class="timestamp" id="timer"></div>
106
+ <canvas id="sandCanvas"></canvas>
107
+
108
+ <div id="loginForm">
109
+ <div class="form-group">
110
+ <div class="label">Identity</div>
111
+ <input type="text" id="username" placeholder=" " autocomplete="off">
112
+ </div>
113
+ <div class="form-group">
114
+ <div class="label">Access Code</div>
115
+ <input type="password" id="password" placeholder=" " autocomplete="off">
116
+ </div>
117
+ <button id="loginButton">CONNECT</button>
118
+ </div>
119
+
120
+ <script>
121
+ const canvas = document.getElementById('sandCanvas');
122
+ const ctx = canvas.getContext('2d');
123
+ const loginForm = document.getElementById('loginForm');
124
+ const timerEl = document.getElementById('timer');
125
+
126
+ let width, height, particles = [];
127
+ const particleCount = 4500;
128
+ const mouseThreshold = 220;
129
+ let isHovering = false;
130
+ let noiseTimer = 0;
131
+
132
+ function updateTimer() {
133
+ const now = new Date();
134
+ timerEl.innerText = now.toLocaleString('en-GB').toUpperCase();
135
+ }
136
+ setInterval(updateTimer, 1000);
137
+
138
+ window.addEventListener('resize', init);
139
+ window.addEventListener('mousemove', (e) => {
140
+ const dx = e.clientX - width / 2;
141
+ const dy = e.clientY - height / 2;
142
+ isHovering = Math.sqrt(dx*dx + dy*dy) < mouseThreshold;
143
+ });
144
+
145
+ function isInsideRoundedSquare(px, py, halfSide, radius) {
146
+ const dx = Math.abs(px - width / 2);
147
+ const dy = Math.abs(py - height / 2);
148
+ if (dx > halfSide || dy > halfSide) return false;
149
+ if (dx > halfSide - radius && dy > halfSide - radius) {
150
+ const cx = dx - (halfSide - radius);
151
+ const cy = dy - (halfSide - radius);
152
+ return (cx * cx + cy * cy <= radius * radius);
153
+ }
154
+ return true;
155
+ }
156
+
157
+ class Particle {
158
+ constructor() {
159
+ this.init();
160
+ }
161
+
162
+ init() {
163
+ const angle = Math.random() * Math.PI * 2;
164
+ // 重置时从中心稍外一点出生,避免堆在原点
165
+ const r = 5 + Math.random() * 15;
166
+ this.x = width / 2 + Math.cos(angle) * r;
167
+ this.y = height / 2 + Math.sin(angle) * r;
168
+ this.vx = Math.cos(angle) * 2;
169
+ this.vy = Math.sin(angle) * 2;
170
+ this.size = Math.random() * 1.6;
171
+ this.alpha = Math.random() * 0.5 + 0.2;
172
+ this.isEscaping = false;
173
+ }
174
+
175
+ update(breath) {
176
+ const dx = this.x - width / 2;
177
+ const dy = this.y - height / 2;
178
+ const dist = Math.sqrt(dx * dx + dy * dy) || 1;
179
+
180
+ if (isHovering) {
181
+ this.vx += (Math.random() - 0.5) * 4;
182
+ this.vy += (Math.random() - 0.5) * 4;
183
+ } else {
184
+ // 1. 中心核心斥力 (防止从中心穿过)
185
+ const repulsionRadius = 45;
186
+ if (dist < repulsionRadius) {
187
+ const force = (repulsionRadius - dist) / repulsionRadius;
188
+ this.vx += (dx / dist) * force * 6;
189
+ this.vy += (dy / dist) * force * 6;
190
+ }
191
+
192
+ // 2. 爆发力 (随呼吸曲线)
193
+ const pushBase = Math.pow(breath, 5) * 14;
194
+ const randomScatter = (Math.random() - 0.5) * pushBase * 0.5;
195
+ this.vx += (dx / dist) * pushBase + randomScatter;
196
+ this.vy += (dy / dist) * pushBase + randomScatter;
197
+
198
+ // 3. 抛飞逃逸逻辑:呼吸最强时极小概率触发
199
+ if (breath > 0.88 && Math.random() > 0.98) {
200
+ this.isEscaping = true;
201
+ }
202
+
203
+ // 4. 边界逻辑
204
+ const side = 180;
205
+ const cornerR = 80;
206
+
207
+ if (!this.isEscaping) {
208
+ if (!isInsideRoundedSquare(this.x + this.vx, this.y + this.vy, side, cornerR)) {
209
+ this.vx *= -0.4; // 碰撞衰减
210
+ this.vy *= -0.4;
211
+ this.vx += (Math.random() - 0.5) * 2;
212
+ this.vy += (Math.random() - 0.5) * 2;
213
+ }
214
+
215
+ // 5. 向心引力:回归到圆环
216
+ const targetR = 140;
217
+ const pull = (targetR - dist) * 0.012;
218
+ this.vx += (dx / dist) * pull;
219
+ this.vy += (dy / dist) * pull;
220
+ }
221
+ }
222
+
223
+ this.x += this.vx;
224
+ this.y += this.vy;
225
+
226
+ // 摩擦力
227
+ const friction = this.isEscaping ? 0.98 : 0.86;
228
+ this.vx *= friction;
229
+ this.vy *= friction;
230
+
231
+ // 6. 重置:飞出界外或速度几乎停滞的逃逸粒子
232
+ if (dist > width * 0.6 || (this.isEscaping && Math.abs(this.vx) < 0.05)) {
233
+ this.init();
234
+ }
235
+ }
236
+
237
+ draw(breath) {
238
+ const b = 180 + breath * 75;
239
+ const finalAlpha = this.isEscaping ? this.alpha * 0.4 : this.alpha;
240
+ ctx.fillStyle = `rgba(${b}, ${b}, ${b}, ${finalAlpha})`;
241
+ ctx.fillRect(this.x, this.y, this.size, this.size);
242
+ }
243
+ }
244
+
245
+ function init() {
246
+ width = canvas.width = window.innerWidth;
247
+ height = canvas.height = window.innerHeight;
248
+ particles = [];
249
+ for (let i = 0; i < particleCount; i++) particles.push(new Particle());
250
+ }
251
+
252
+ function drawCore(breath) {
253
+ if (isHovering) return;
254
+ const size = 10 + breath * 4;
255
+ ctx.save();
256
+ ctx.shadowBlur = 40 * breath;
257
+ ctx.shadowColor = '#fff';
258
+ ctx.fillStyle = '#fff';
259
+ ctx.beginPath();
260
+ ctx.arc(width / 2, height / 2, size, 0, Math.PI * 2);
261
+ ctx.fill();
262
+ ctx.restore();
263
+ }
264
+
265
+ function animate() {
266
+ ctx.fillStyle = 'rgba(0, 0, 0, 0.25)';
267
+ ctx.fillRect(0, 0, width, height);
268
+
269
+ noiseTimer += 0.04;
270
+ const breath = Math.pow((Math.sin(noiseTimer) + 1) / 2, 2);
271
+
272
+ if (isHovering) {
273
+ loginForm.classList.add('visible');
274
+ } else {
275
+ loginForm.classList.remove('visible');
276
+ }
277
+
278
+ particles.forEach(p => {
279
+ p.update(breath);
280
+ p.draw(breath);
281
+ });
282
+
283
+ drawCore(breath);
284
+ requestAnimationFrame(animate);
285
+ }
286
+
287
+ init();
288
+ updateTimer();
289
+ animate();
290
+ </script>
291
+ </body>
292
+ </html>
26-5-10-API-Studio/static/logo.png ADDED
26-5-10-API-Studio/static/modelscope.gif ADDED
26-5-10-API-Studio/static/online.html ADDED
@@ -0,0 +1,329 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <!DOCTYPE html>
2
+ <html lang="zh-CN">
3
+ <head>
4
+ <meta charset="UTF-8">
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
+ <link rel="icon" href="/static/logo.png" type="image/png">
7
+ <title>在线生图</title>
8
+ <script src="https://cdn.tailwindcss.com"></script>
9
+ <script src="https://unpkg.com/lucide@latest"></script>
10
+ <script src="/static/theme.js?v=20260509-global-theme-output-drag"></script>
11
+ <style>
12
+ @import url('https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;800&display=swap');
13
+ :root { --accent:#111827; --bg:#f9fafb; --card:#fff; --easing:cubic-bezier(0.4,0,0.2,1); }
14
+ body { background:var(--bg); font-family:'Inter',-apple-system,sans-serif; color:var(--accent); -webkit-font-smoothing:antialiased; }
15
+ .container-box { max-width:1280px; margin:0 auto; padding:0 40px; margin-top:50px; }
16
+ .nano-input { background:var(--card); border:1px solid #eef0f2; transition:all .3s var(--easing); }
17
+ .nano-input:focus { border-color:#000; box-shadow:0 0 0 1px #000; }
18
+ .upload-item { background:var(--card); border:1px dashed #e2e8f0; transition:all .4s var(--easing); position:relative; overflow:hidden; }
19
+ .upload-item:hover,.upload-item.drag-over { border-color:#000; transform:translateY(-2px); }
20
+ .preview-img { position:absolute; inset:0; width:100%; height:100%; object-fit:cover; animation:fadeIn .5s var(--easing); }
21
+ .glass-btn { background:#111827; transition:all .3s var(--easing); }
22
+ .glass-btn:hover { background:#000; transform:translateY(-1px); box-shadow:0 12px 24px rgba(0,0,0,.1); }
23
+ .result-frame { background:#fff; border-radius:32px; border:1px solid #f1f5f9; box-shadow:0 2px 15px rgba(0,0,0,.02); }
24
+ .tool-panel { background:#fff; border:1px solid #edf0f3; border-radius:24px; padding:12px; display:flex; flex-direction:column; gap:10px; }
25
+ .model-pill { background:#f8fafc; border:1px solid #edf2f7; border-radius:999px; padding:3px; display:grid; grid-template-columns:1fr 1fr; gap:2px; }
26
+ .model-option { border-radius:999px; height:34px; display:flex; align-items:center; justify-content:center; gap:6px; color:#64748b; font-size:10px; font-weight:600; letter-spacing:.02em; transition:all .25s var(--easing); }
27
+ .model-option.active { background:#fff; color:#111827; box-shadow:0 1px 3px rgba(15,23,42,.08); }
28
+ .control-panel { background:#fff; border:1px solid #edf0f3; border-radius:24px; padding:12px; display:flex; flex-direction:column; gap:10px; }
29
+ .control-label { display:flex; align-items:center; gap:6px; color:#94a3b8; font-size:10px; font-weight:600; letter-spacing:.04em; }
30
+ .size-row { display:flex; align-items:center; gap:8px; }
31
+ .ratio-grid { flex:1; display:grid; grid-template-columns:repeat(5,1fr); gap:4px; background:#f8fafc; border:1px solid #edf2f7; border-radius:999px; padding:3px; min-width:0; }
32
+ .ratio-option { height:30px; border-radius:999px; display:flex; align-items:center; justify-content:center; gap:4px; color:#64748b; font-size:10px; font-weight:600; letter-spacing:0; transition:all .25s var(--easing); }
33
+ .ratio-option.active { background:#fff; color:#111827; box-shadow:0 1px 3px rgba(15,23,42,.08); }
34
+ .resolution-toggle { flex:0 0 auto; background:#f8fafc; border:1px solid #edf2f7; border-radius:999px; padding:3px; display:flex; gap:2px; }
35
+ .resolution-option { height:30px; min-width:42px; border-radius:999px; display:flex; align-items:center; justify-content:center; color:#64748b; font-size:10px; font-weight:600; transition:all .25s var(--easing); }
36
+ .resolution-option.active { background:#fff; color:#111827; box-shadow:0 1px 3px rgba(15,23,42,.08); }
37
+ .masonry-grid { display:grid; grid-template-columns:repeat(2,1fr); gap:20px; }
38
+ @media (min-width:768px){ .masonry-grid{ grid-template-columns:repeat(4,1fr); } }
39
+ @media (max-width:640px){ .size-row{ align-items:stretch; flex-direction:column; } .ratio-grid{ grid-template-columns:repeat(2,1fr); border-radius:18px; } .resolution-toggle{ align-self:flex-start; } }
40
+ .masonry-item { aspect-ratio:1/1; border-radius:24px; overflow:hidden; background:#fff; border:1px solid #f1f5f9; transition:all .5s var(--easing); }
41
+ .masonry-item:hover { transform:translateY(-6px); box-shadow:0 20px 40px rgba(0,0,0,.08); }
42
+ @keyframes fadeIn { from{opacity:0} to{opacity:1} }
43
+ </style>
44
+ <link rel="stylesheet" href="/static/theme.css?v=20260510-studio-pages-blue-dark10">
45
+ </head>
46
+ <body>
47
+ <div class="container-box min-h-screen flex flex-col">
48
+ <header class="flex justify-between items-end mb-16">
49
+ <div class="space-y-1">
50
+ <h1 class="text-4xl font-extrabold tracking-tighter italic">在线生图</h1>
51
+ <p class="text-[10px] font-bold uppercase tracking-[0.4em] text-gray-400">GPT / NANO BANANA IMAGE STUDIO</p>
52
+ </div>
53
+ <nav class="hidden md:flex gap-8 text-[11px] font-bold uppercase tracking-widest text-gray-400">
54
+ <span class="text-black border-b-2 border-black pb-1">Create</span>
55
+ </nav>
56
+ </header>
57
+
58
+ <main class="grid grid-cols-1 lg:grid-cols-12 gap-12">
59
+ <div class="lg:col-span-5 space-y-8">
60
+ <section class="space-y-4">
61
+ <div class="flex items-center gap-2 text-gray-400">
62
+ <i data-lucide="terminal" class="w-3.5 h-3.5"></i>
63
+ <span class="text-[10px] font-black uppercase tracking-widest">Prompt</span>
64
+ </div>
65
+ <textarea id="promptInput" rows="5" class="nano-input w-full p-6 rounded-3xl text-sm outline-none resize-none placeholder-gray-300" placeholder="输入想生成或编辑的画面..."></textarea>
66
+ </section>
67
+
68
+ <section class="space-y-4">
69
+ <div class="flex items-center gap-2 text-gray-400">
70
+ <i data-lucide="images" class="w-3.5 h-3.5"></i>
71
+ <span class="text-[10px] font-black uppercase tracking-widest">Reference Images</span>
72
+ </div>
73
+ <div class="grid grid-cols-3 gap-4">
74
+ <div id="drop-zone-1" onclick="document.getElementById('file1').click()" class="upload-item group aspect-square rounded-2xl flex flex-col items-center justify-center cursor-pointer">
75
+ <input type="file" id="file1" accept="image/*" class="hidden" onchange="handleFile(this.files[0],1)">
76
+ <i data-lucide="plus" class="w-5 h-5 text-gray-300 group-hover:text-black"></i><span class="text-[9px] mt-2 font-bold text-gray-400 uppercase">Main</span>
77
+ <img id="prev1" class="preview-img hidden"><button id="del1" onclick="clearSlot(1,event)" class="hidden absolute top-2 right-2 w-6 h-6 bg-white/90 rounded-full shadow-sm z-10">×</button>
78
+ </div>
79
+ <div id="drop-zone-2" onclick="document.getElementById('file2').click()" class="upload-item group aspect-square rounded-2xl flex flex-col items-center justify-center cursor-pointer">
80
+ <input type="file" id="file2" accept="image/*" class="hidden" onchange="handleFile(this.files[0],2)">
81
+ <i data-lucide="plus" class="w-5 h-5 text-gray-300 group-hover:text-black"></i><span class="text-[9px] mt-2 font-bold text-gray-400 uppercase">Aux A</span>
82
+ <img id="prev2" class="preview-img hidden"><button id="del2" onclick="clearSlot(2,event)" class="hidden absolute top-2 right-2 w-6 h-6 bg-white/90 rounded-full shadow-sm z-10">×</button>
83
+ </div>
84
+ <div id="drop-zone-3" onclick="document.getElementById('file3').click()" class="upload-item group aspect-square rounded-2xl flex flex-col items-center justify-center cursor-pointer">
85
+ <input type="file" id="file3" accept="image/*" class="hidden" onchange="handleFile(this.files[0],3)">
86
+ <i data-lucide="plus" class="w-5 h-5 text-gray-300 group-hover:text-black"></i><span class="text-[9px] mt-2 font-bold text-gray-400 uppercase">Aux B</span>
87
+ <img id="prev3" class="preview-img hidden"><button id="del3" onclick="clearSlot(3,event)" class="hidden absolute top-2 right-2 w-6 h-6 bg-white/90 rounded-full shadow-sm z-10">×</button>
88
+ </div>
89
+ </div>
90
+ </section>
91
+
92
+ <div class="tool-panel">
93
+ <div class="control-label">
94
+ <i data-lucide="sliders-horizontal" class="w-3.5 h-3.5"></i>
95
+ <span>Model</span>
96
+ </div>
97
+ <div class="model-pill">
98
+ <button id="gptOpt" class="model-option active" onclick="setProvider('gpt')"><i data-lucide="sparkles" class="w-3.5 h-3.5"></i>GPT</button>
99
+ <button id="nanoOpt" class="model-option" onclick="setProvider('nano')"><i data-lucide="bot" class="w-3.5 h-3.5"></i>Nano</button>
100
+ </div>
101
+ <div class="control-label mt-1">
102
+ <i data-lucide="ruler" class="w-3.5 h-3.5"></i>
103
+ <span>Size</span>
104
+ </div>
105
+ <div class="size-row">
106
+ <div class="ratio-grid">
107
+ <button id="ratio-square" class="ratio-option active" onclick="setRatio('square')"><i data-lucide="square" class="w-3 h-3"></i>1:1</button>
108
+ <button id="ratio-portrait" class="ratio-option" onclick="setRatio('portrait')"><i data-lucide="rectangle-vertical" class="w-3 h-3"></i>2:3</button>
109
+ <button id="ratio-landscape" class="ratio-option" onclick="setRatio('landscape')"><i data-lucide="rectangle-horizontal" class="w-3 h-3"></i>3:2</button>
110
+ <button id="ratio-story" class="ratio-option" onclick="setRatio('story')"><i data-lucide="smartphone" class="w-3 h-3"></i>9:16</button>
111
+ <button id="ratio-wide" class="ratio-option" onclick="setRatio('wide')"><i data-lucide="monitor" class="w-3 h-3"></i>16:9</button>
112
+ </div>
113
+ <div class="resolution-toggle" title="分辨率">
114
+ <button id="res-1k" class="resolution-option active" onclick="setResolution('1k')">1K</button>
115
+ <button id="res-2k" class="resolution-option" onclick="setResolution('2k')">2K</button>
116
+ </div>
117
+ </div>
118
+ </div>
119
+
120
+ <button id="genBtn" onclick="submitImage()" class="glass-btn w-full py-5 text-white rounded-3xl font-bold flex items-center justify-center gap-3 shadow-lg">
121
+ <i data-lucide="zap" class="w-4 h-4 text-yellow-400"></i>
122
+ <span class="tracking-[0.3em] text-[11px] uppercase">Generate</span>
123
+ </button>
124
+ </div>
125
+
126
+ <div class="lg:col-span-7">
127
+ <div id="resultBox" class="result-frame min-h-[500px] lg:h-full flex items-center justify-center relative overflow-hidden">
128
+ <div id="placeholder" class="text-center space-y-4 opacity-20"><i data-lucide="layout" class="w-12 h-12 mx-auto stroke-[1px]"></i><p class="text-[10px] font-black tracking-[0.5em] uppercase">Canvas Ready</p></div>
129
+ <div id="loader" class="hidden text-center space-y-4"><div class="w-8 h-8 border-2 border-black border-t-transparent rounded-full animate-spin mx-auto"></div><p class="text-[10px] font-black tracking-[0.5em] uppercase">Generating</p></div>
130
+ <img id="outputImg" class="hidden w-full h-full object-contain p-8 cursor-zoom-in" onclick="zoomImage()">
131
+ <a id="downloadBtn" href="#" download class="hidden absolute top-8 right-8 w-12 h-12 bg-white/90 shadow-2xl rounded-2xl flex items-center justify-center hover:bg-black hover:text-white"><i data-lucide="download" class="w-4 h-4"></i></a>
132
+ </div>
133
+ </div>
134
+ </main>
135
+
136
+ <section class="mt-32">
137
+ <div class="flex items-center gap-6 mb-12"><h2 class="text-[11px] font-black uppercase tracking-[0.5em]">Archives</h2><div class="h-px flex-1 bg-gray-100"></div></div>
138
+ <div id="masonry" class="masonry-grid"></div>
139
+ <div id="loadMoreTrigger" class="py-20 text-center text-gray-300 text-[10px] font-bold uppercase tracking-widest cursor-pointer hover:text-black">Load More Archive</div>
140
+ </section>
141
+ </div>
142
+
143
+ <div id="lightbox" onclick="handleOutsideClick(event)" class="hidden fixed inset-0 z-50 flex items-center justify-center p-6 bg-white/95 backdrop-blur-3xl">
144
+ <div class="max-w-6xl w-full flex flex-col items-center relative">
145
+ <img id="lightboxImg" class="max-h-[75vh] rounded-[2.5rem] shadow-2xl object-contain">
146
+ <div class="w-full bg-white border border-gray-100 rounded-[2rem] p-8 mt-8 shadow-sm flex justify-between items-center gap-8">
147
+ <div class="flex-1"><span class="text-[9px] font-black text-gray-300 uppercase tracking-widest block mb-2">Prompt</span><p id="lightboxPrompt" class="text-gray-700 text-sm leading-relaxed"></p></div>
148
+ <button id="sameStyleBtn" onclick="applySameStyle()" class="bg-black text-white px-8 py-3.5 rounded-2xl text-[10px] font-black uppercase tracking-widest flex items-center gap-2"><i data-lucide="copy" class="w-4 h-4"></i>Reuse</button>
149
+ </div>
150
+ <button onclick="closeLightbox()" class="absolute -top-12 -right-12 p-4 text-gray-400 hover:text-black"><i data-lucide="x" class="w-8 h-8"></i></button>
151
+ </div>
152
+ </div>
153
+
154
+ <script>
155
+ lucide.createIcons();
156
+ let refs = {1:null,2:null,3:null};
157
+ let provider = 'gpt';
158
+ let ratio = 'square';
159
+ let resolution = '1k';
160
+ let models = {gpt:'gpt-image-1', nano:'nano-banana'};
161
+ let allHistory = [], currentIndex = 0, currentResult = null, currentLightboxData = null, isLoading = false;
162
+ const PAGE_SIZE = 24;
163
+ const SIZE_OPTIONS = {
164
+ square: [
165
+ ['1024x1024', '1k'],
166
+ ['1536x1536', '2k']
167
+ ],
168
+ portrait: [
169
+ ['720x1080', '1k'],
170
+ ['1024x1536', '2k']
171
+ ],
172
+ landscape: [
173
+ ['1080x720', '1k'],
174
+ ['1536x1024', '2k']
175
+ ],
176
+ story: [
177
+ ['720x1280', '1k'],
178
+ ['1080x1920', '2k']
179
+ ],
180
+ wide: [
181
+ ['1280x720', '1k'],
182
+ ['1920x1080', '2k']
183
+ ]
184
+ };
185
+
186
+ async function initModels(){
187
+ try {
188
+ const cfg = await fetch('/api/config').then(r=>r.json());
189
+ const imageModels = cfg.image_models || [];
190
+ models.nano = imageModels.find(m => m.toLowerCase().includes('nano')) || 'nano-banana';
191
+ models.gpt = imageModels.find(m => !m.toLowerCase().includes('nano')) || cfg.image_model || 'gpt-image-1';
192
+ } catch(e) {}
193
+ }
194
+
195
+ function setProvider(next){
196
+ provider = next;
197
+ document.getElementById('gptOpt').classList.toggle('active', next === 'gpt');
198
+ document.getElementById('nanoOpt').classList.toggle('active', next === 'nano');
199
+ }
200
+
201
+ function setRatio(next, preferredSize = ''){
202
+ ratio = next;
203
+ ['square','portrait','landscape','story','wide'].forEach(item => {
204
+ document.getElementById(`ratio-${item}`).classList.toggle('active', item === next);
205
+ });
206
+ if (preferredSize) {
207
+ const match = SIZE_OPTIONS[next].find(([value]) => value === preferredSize);
208
+ if (match) resolution = match[1];
209
+ }
210
+ updateResolutionUI();
211
+ }
212
+
213
+ function setResolution(next){
214
+ resolution = next;
215
+ updateResolutionUI();
216
+ }
217
+
218
+ function updateResolutionUI(){
219
+ document.getElementById('res-1k').classList.toggle('active', resolution === '1k');
220
+ document.getElementById('res-2k').classList.toggle('active', resolution === '2k');
221
+ }
222
+
223
+ function currentSize(){
224
+ const options = SIZE_OPTIONS[ratio] || SIZE_OPTIONS.square;
225
+ const match = options.find(([, label]) => label === resolution) || options[0];
226
+ return match[0];
227
+ }
228
+
229
+ [1,2,3].forEach(id => {
230
+ const zone = document.getElementById(`drop-zone-${id}`);
231
+ zone.ondragover = e => { e.preventDefault(); zone.classList.add('drag-over'); };
232
+ zone.ondragleave = () => zone.classList.remove('drag-over');
233
+ zone.ondrop = e => { e.preventDefault(); zone.classList.remove('drag-over'); handleFile(e.dataTransfer.files[0], id); };
234
+ });
235
+ window.addEventListener('paste', e => {
236
+ const item = [...(e.clipboardData?.items || [])].find(x => x.kind === 'file' && x.type.startsWith('image/'));
237
+ if (item) handleFile(item.getAsFile(), firstEmptySlot());
238
+ });
239
+ function firstEmptySlot(){ return [1,2,3].find(id => !refs[id]) || 1; }
240
+
241
+ async function handleFile(file, index){
242
+ if(!file) return;
243
+ const reader = new FileReader();
244
+ reader.onload = e => {
245
+ const prev = document.getElementById(`prev${index}`);
246
+ prev.src = e.target.result; prev.classList.remove('hidden');
247
+ document.getElementById(`del${index}`).classList.remove('hidden');
248
+ };
249
+ reader.readAsDataURL(file);
250
+ const form = new FormData();
251
+ form.append('files', file);
252
+ const data = await fetch('/api/ai/upload', {method:'POST', body:form}).then(r=>r.json());
253
+ refs[index] = data.files[0];
254
+ }
255
+ function clearSlot(index, ev){
256
+ if(ev) ev.stopPropagation();
257
+ refs[index] = null;
258
+ document.getElementById(`prev${index}`).src = '';
259
+ document.getElementById(`prev${index}`).classList.add('hidden');
260
+ document.getElementById(`del${index}`).classList.add('hidden');
261
+ }
262
+
263
+ async function submitImage(){
264
+ const prompt = document.getElementById('promptInput').value.trim();
265
+ if(!prompt){ alert('请输入提示词'); return; }
266
+ const btn = document.getElementById('genBtn'), loader = document.getElementById('loader'), placeholder = document.getElementById('placeholder'), out = document.getElementById('outputImg'), dl = document.getElementById('downloadBtn');
267
+ btn.disabled = true; placeholder.classList.add('hidden'); out.classList.add('hidden'); dl.classList.add('hidden'); loader.classList.remove('hidden');
268
+ try {
269
+ const result = await fetch('/api/online-image', {
270
+ method:'POST', headers:{'Content-Type':'application/json'},
271
+ body:JSON.stringify({ prompt, model:models[provider], size:currentSize(), reference_images:Object.values(refs).filter(Boolean) })
272
+ }).then(async r => { if(!r.ok) throw new Error((await r.json()).detail || '生成失败'); return r.json(); });
273
+ currentResult = result; out.src = result.images[0]; out.classList.remove('hidden'); dl.href = result.images[0]; dl.classList.remove('hidden'); renderImageCard(result, true);
274
+ } catch(err) {
275
+ alert(err.message || 'Generation failed'); placeholder.classList.remove('hidden');
276
+ } finally {
277
+ loader.classList.add('hidden'); btn.disabled = false;
278
+ }
279
+ }
280
+
281
+ function renderImageCard(data, isNew=false){
282
+ if(!data.images?.[0] || document.getElementById(`history-${data.timestamp}`)) return;
283
+ const card = document.createElement('div');
284
+ card.id = `history-${data.timestamp}`;
285
+ card.className = 'masonry-item group relative cursor-zoom-in';
286
+ card.onclick = () => openLightbox(data);
287
+ card.innerHTML = `<img src="${data.images[0]}" class="w-full h-full object-cover block group-hover:scale-105 transition-transform duration-1000" loading="lazy">
288
+ <button onclick="deleteHistoryItem('${data.timestamp}',event)" class="absolute top-4 right-4 text-white hover:text-red-400 opacity-0 group-hover:opacity-100 z-10"><i data-lucide="trash-2" class="w-4 h-4"></i></button>
289
+ <div class="absolute inset-0 bg-gradient-to-t from-black/80 via-transparent to-transparent opacity-0 group-hover:opacity-100 p-6 flex flex-col justify-end"><p class="text-white text-[10px] font-medium line-clamp-2 uppercase tracking-wider">${data.prompt || 'Online Image'}</p></div>`;
290
+ isNew ? document.getElementById('masonry').prepend(card) : document.getElementById('masonry').appendChild(card);
291
+ lucide.createIcons();
292
+ }
293
+ async function loadHistory(page=0){
294
+ if(isLoading) return; isLoading = true;
295
+ const loader = document.getElementById('loadMoreTrigger');
296
+ if(page === 0){ loader.innerText = 'Loading Archives...'; allHistory = await fetch('/api/history?type=online').then(r=>r.json()); document.getElementById('masonry').innerHTML=''; currentIndex=0; }
297
+ const next = allHistory.slice(currentIndex, currentIndex + PAGE_SIZE);
298
+ next.forEach(item => renderImageCard(item)); currentIndex += next.length;
299
+ loader.classList.toggle('hidden', currentIndex >= allHistory.length);
300
+ loader.innerText = 'Load More Archive'; isLoading = false;
301
+ }
302
+ function openLightbox(data){ currentLightboxData = data; document.getElementById('lightboxImg').src = data.images[0]; document.getElementById('lightboxPrompt').textContent = data.prompt || ''; document.getElementById('lightbox').classList.replace('hidden','flex'); document.body.style.overflow='hidden'; }
303
+ function closeLightbox(){ document.getElementById('lightbox').classList.replace('flex','hidden'); document.body.style.overflow='auto'; }
304
+ function handleOutsideClick(e){ if(e.target.id === 'lightbox') closeLightbox(); }
305
+ function zoomImage(){ if(currentResult) openLightbox(currentResult); }
306
+ function applySameStyle(){
307
+ if(!currentLightboxData) return;
308
+ document.getElementById('promptInput').value = currentLightboxData.prompt || '';
309
+ [1,2,3].forEach(id => clearSlot(id));
310
+ (currentLightboxData.params?.reference_images || []).slice(0,3).forEach((ref,i) => {
311
+ refs[i+1] = ref; const prev = document.getElementById(`prev${i+1}`);
312
+ prev.src = ref.url; prev.classList.remove('hidden'); document.getElementById(`del${i+1}`).classList.remove('hidden');
313
+ });
314
+ const size = currentLightboxData.params?.size || '1024x1024';
315
+ const matchedRatio = Object.keys(SIZE_OPTIONS).find(key => SIZE_OPTIONS[key].some(([value]) => value === size)) || 'square';
316
+ setRatio(matchedRatio, size);
317
+ closeLightbox(); window.scrollTo({top:0, behavior:'smooth'});
318
+ }
319
+ async function deleteHistoryItem(ts, ev){
320
+ ev.stopPropagation();
321
+ if(!confirm('删除这条记录?')) return;
322
+ const res = await fetch('/api/history/delete', {method:'POST', headers:{'Content-Type':'application/json'}, body:JSON.stringify({timestamp:ts})}).then(r=>r.json());
323
+ if(res.success) document.getElementById(`history-${ts}`)?.remove();
324
+ }
325
+ const observer = new IntersectionObserver(entries => { if(entries[0].isIntersecting && currentIndex < allHistory.length) loadHistory(1); }, {threshold:.1});
326
+ window.onload = async () => { setRatio('square'); setResolution('1k'); await initModels(); await loadHistory(0); observer.observe(document.getElementById('loadMoreTrigger')); document.getElementById('loadMoreTrigger').onclick = () => loadHistory(1); };
327
+ </script>
328
+ </body>
329
+ </html>
26-5-10-API-Studio/static/theme.css ADDED
@@ -0,0 +1,1591 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ html.studio-theme-dark,
2
+ html.studio-theme-dark body,
3
+ body.studio-theme-dark {
4
+ color-scheme: dark;
5
+ background: #08090c !important;
6
+ color: #e8e8ea !important;
7
+ }
8
+
9
+ body.studio-theme-dark {
10
+ --accent: #e8e8ea;
11
+ --bg: #08090c;
12
+ --bg-base: #08090c;
13
+ --card: #111216;
14
+ --text-main: #e8e8ea;
15
+ }
16
+
17
+ html.studio-theme-dark body .app-shell,
18
+ html.studio-theme-dark body .sidebar,
19
+ html.studio-theme-dark body .stage,
20
+ body.studio-theme-dark .app-shell,
21
+ body.studio-theme-dark .sidebar,
22
+ body.studio-theme-dark .stage {
23
+ background: #08090c !important;
24
+ border-color: #25262b !important;
25
+ }
26
+
27
+ html.studio-theme-dark body .stage,
28
+ body.studio-theme-dark .stage {
29
+ box-shadow: inset 0 0 42px rgba(255,255,255,.015);
30
+ }
31
+
32
+ html.studio-theme-dark body .nav-item,
33
+ body.studio-theme-dark .nav-item {
34
+ color: #8f9aab;
35
+ }
36
+
37
+ html.studio-theme-dark body .nav-item:hover,
38
+ body.studio-theme-dark .nav-item:hover {
39
+ background: #111216 !important;
40
+ color: #e8e8ea !important;
41
+ }
42
+
43
+ html.studio-theme-dark body .nav-item.active,
44
+ body.studio-theme-dark .nav-item.active {
45
+ background: #d8dee9 !important;
46
+ color: #10141d !important;
47
+ box-shadow: 0 12px 24px rgba(0,0,0,.28);
48
+ }
49
+
50
+ html.studio-theme-dark body .logo-ring,
51
+ body.studio-theme-dark .logo-ring {
52
+ border-color: #d8dee9 !important;
53
+ }
54
+
55
+ html.studio-theme-dark body #logo-dot,
56
+ body.studio-theme-dark #logo-dot {
57
+ background: #d8dee9 !important;
58
+ }
59
+
60
+ html.studio-theme-dark body .nano-monitor,
61
+ body.studio-theme-dark .nano-monitor {
62
+ background: rgba(17,18,22,.88) !important;
63
+ color: #d8dee9;
64
+ border-color: rgba(255,255,255,.10) !important;
65
+ box-shadow: 0 12px 34px rgba(0,0,0,.24);
66
+ }
67
+
68
+ html.studio-theme-dark body .divider,
69
+ body.studio-theme-dark .divider {
70
+ background: rgba(216,222,233,.14) !important;
71
+ }
72
+
73
+ html.studio-theme-dark body .theme-dock-btn,
74
+ body.studio-theme-dark .theme-dock-btn {
75
+ background: #111216;
76
+ border-color: #25262b;
77
+ color: #9aa6b8;
78
+ }
79
+
80
+ html.studio-theme-dark body .theme-dock-btn:hover,
81
+ html.studio-theme-dark body .theme-dock-btn.active,
82
+ body.studio-theme-dark .theme-dock-btn:hover,
83
+ body.studio-theme-dark .theme-dock-btn.active {
84
+ background: #d8dee9;
85
+ border-color: #d8dee9;
86
+ color: #10141d;
87
+ }
88
+
89
+ html.studio-theme-dark body .console-card,
90
+ html.studio-theme-dark body .nano-input,
91
+ html.studio-theme-dark body .upload-item,
92
+ html.studio-theme-dark body .result-frame,
93
+ html.studio-theme-dark body .masonry-item,
94
+ html.studio-theme-dark body .work-panel,
95
+ html.studio-theme-dark body .setting-row,
96
+ html.studio-theme-dark body .tool-panel,
97
+ html.studio-theme-dark body .control-panel,
98
+ html.studio-theme-dark body .topbar,
99
+ html.studio-theme-dark body .composer,
100
+ html.studio-theme-dark body .composer-body,
101
+ html.studio-theme-dark body .bubble.assistant,
102
+ html.studio-theme-dark body .history-popover,
103
+ html.studio-theme-dark body .thread-item,
104
+ html.studio-theme-dark body .lightbox-card,
105
+ html.studio-theme-dark body #lightboxCard,
106
+ html.studio-theme-dark body .bg-white,
107
+ html.studio-theme-dark body .bg-gray-50,
108
+ html.studio-theme-dark body .bg-slate-50,
109
+ html.studio-theme-dark body .bg-gray-50\/50,
110
+ html.studio-theme-dark body .bg-white\/90,
111
+ html.studio-theme-dark body .bg-\[\#fbfdff\],
112
+ body.studio-theme-dark .console-card,
113
+ body.studio-theme-dark .nano-input,
114
+ body.studio-theme-dark .upload-item,
115
+ body.studio-theme-dark .result-frame,
116
+ body.studio-theme-dark .masonry-item,
117
+ body.studio-theme-dark .work-panel,
118
+ body.studio-theme-dark .setting-row,
119
+ body.studio-theme-dark .tool-panel,
120
+ body.studio-theme-dark .control-panel,
121
+ body.studio-theme-dark .topbar,
122
+ body.studio-theme-dark .composer,
123
+ body.studio-theme-dark .composer-body,
124
+ body.studio-theme-dark .bubble.assistant,
125
+ body.studio-theme-dark .history-popover,
126
+ body.studio-theme-dark .thread-item,
127
+ body.studio-theme-dark .lightbox-card,
128
+ body.studio-theme-dark #lightboxCard,
129
+ body.studio-theme-dark .bg-white,
130
+ body.studio-theme-dark .bg-gray-50,
131
+ body.studio-theme-dark .bg-slate-50,
132
+ body.studio-theme-dark .bg-gray-50\/50,
133
+ body.studio-theme-dark .bg-white\/90,
134
+ body.studio-theme-dark .bg-\[\#fbfdff\] {
135
+ background-color: #111216 !important;
136
+ border-color: #25262b !important;
137
+ color: #e8e8ea !important;
138
+ }
139
+
140
+ html.studio-theme-dark body .bg-gray-100,
141
+ html.studio-theme-dark body .bg-slate-100,
142
+ html.studio-theme-dark body .select-shell,
143
+ html.studio-theme-dark body .model-pill,
144
+ html.studio-theme-dark body .ratio-grid,
145
+ html.studio-theme-dark body .resolution-toggle,
146
+ html.studio-theme-dark body .mode-switch,
147
+ html.studio-theme-dark body .mini-ratio,
148
+ body.studio-theme-dark .bg-gray-100,
149
+ body.studio-theme-dark .bg-slate-100,
150
+ body.studio-theme-dark .select-shell,
151
+ body.studio-theme-dark .model-pill,
152
+ body.studio-theme-dark .ratio-grid,
153
+ body.studio-theme-dark .resolution-toggle,
154
+ body.studio-theme-dark .mode-switch,
155
+ body.studio-theme-dark .mini-ratio {
156
+ background-color: #0c0d10 !important;
157
+ border-color: #25262b !important;
158
+ }
159
+
160
+ html.studio-theme-dark body input,
161
+ html.studio-theme-dark body textarea,
162
+ html.studio-theme-dark body select,
163
+ body.studio-theme-dark input,
164
+ body.studio-theme-dark textarea,
165
+ body.studio-theme-dark select {
166
+ color: #e8e8ea !important;
167
+ }
168
+
169
+ html.studio-theme-dark body input::placeholder,
170
+ html.studio-theme-dark body textarea::placeholder,
171
+ body.studio-theme-dark input::placeholder,
172
+ body.studio-theme-dark textarea::placeholder {
173
+ color: #657286 !important;
174
+ }
175
+
176
+ html.studio-theme-dark body .text-black,
177
+ html.studio-theme-dark body .text-gray-700,
178
+ html.studio-theme-dark body .text-slate-700,
179
+ html.studio-theme-dark body .setting-title,
180
+ html.studio-theme-dark body h1,
181
+ html.studio-theme-dark body h2,
182
+ html.studio-theme-dark body h3,
183
+ body.studio-theme-dark .text-black,
184
+ body.studio-theme-dark .text-gray-700,
185
+ body.studio-theme-dark .text-slate-700,
186
+ body.studio-theme-dark .setting-title,
187
+ body.studio-theme-dark h1,
188
+ body.studio-theme-dark h2,
189
+ body.studio-theme-dark h3 {
190
+ color: #e8e8ea !important;
191
+ }
192
+
193
+ html.studio-theme-dark body .text-gray-300,
194
+ html.studio-theme-dark body .text-gray-400,
195
+ html.studio-theme-dark body .text-gray-500,
196
+ html.studio-theme-dark body .text-slate-400,
197
+ html.studio-theme-dark body .text-slate-500,
198
+ html.studio-theme-dark body .setting-meta,
199
+ html.studio-theme-dark body .label-nano,
200
+ body.studio-theme-dark .text-gray-300,
201
+ body.studio-theme-dark .text-gray-400,
202
+ body.studio-theme-dark .text-gray-500,
203
+ body.studio-theme-dark .text-slate-400,
204
+ body.studio-theme-dark .text-slate-500,
205
+ body.studio-theme-dark .setting-meta,
206
+ body.studio-theme-dark .label-nano {
207
+ color: #8f9aab !important;
208
+ }
209
+
210
+ html.studio-theme-dark body .border-gray-100,
211
+ html.studio-theme-dark body .border-gray-200,
212
+ html.studio-theme-dark body .border-gray-300,
213
+ html.studio-theme-dark body .border-slate-100,
214
+ html.studio-theme-dark body .border-slate-200,
215
+ html.studio-theme-dark body .border-black,
216
+ html.studio-theme-dark body .border-white,
217
+ html.studio-theme-dark body .border-\[\#f1f5f9\],
218
+ html.studio-theme-dark body .border-\[\#eef2f7\],
219
+ body.studio-theme-dark .border-gray-100,
220
+ body.studio-theme-dark .border-gray-200,
221
+ body.studio-theme-dark .border-gray-300,
222
+ body.studio-theme-dark .border-slate-100,
223
+ body.studio-theme-dark .border-slate-200,
224
+ body.studio-theme-dark .border-black,
225
+ body.studio-theme-dark .border-white,
226
+ body.studio-theme-dark .border-\[\#f1f5f9\],
227
+ body.studio-theme-dark .border-\[\#eef2f7\] {
228
+ border-color: #2a3444 !important;
229
+ }
230
+
231
+ html.studio-theme-dark body .glass-btn,
232
+ html.studio-theme-dark body .btn-action-dark,
233
+ html.studio-theme-dark body .send-btn,
234
+ html.studio-theme-dark body .gen-btn,
235
+ html.studio-theme-dark body .comfy-run,
236
+ html.studio-theme-dark body .llm-run,
237
+ html.studio-theme-dark body .primary-btn,
238
+ html.studio-theme-dark body .bg-black,
239
+ body.studio-theme-dark .glass-btn,
240
+ body.studio-theme-dark .btn-action-dark,
241
+ body.studio-theme-dark .send-btn,
242
+ body.studio-theme-dark .gen-btn,
243
+ body.studio-theme-dark .comfy-run,
244
+ body.studio-theme-dark .llm-run,
245
+ body.studio-theme-dark .primary-btn,
246
+ body.studio-theme-dark .bg-black {
247
+ background-color: #d8dee9 !important;
248
+ color: #10141d !important;
249
+ }
250
+
251
+ html.studio-theme-dark body .glass-btn:hover,
252
+ html.studio-theme-dark body .btn-action-dark:hover,
253
+ html.studio-theme-dark body .bg-black:hover,
254
+ body.studio-theme-dark .glass-btn:hover,
255
+ body.studio-theme-dark .btn-action-dark:hover,
256
+ body.studio-theme-dark .bg-black:hover {
257
+ background-color: #c8d0dc !important;
258
+ }
259
+
260
+ html.studio-theme-dark body .gallery-lightbox,
261
+ html.studio-theme-dark body #lightbox,
262
+ body.studio-theme-dark .gallery-lightbox,
263
+ body.studio-theme-dark #lightbox {
264
+ background: rgba(15,20,29,.78) !important;
265
+ backdrop-filter: blur(22px) saturate(1.05);
266
+ }
267
+
268
+ html.studio-theme-dark body .masonry-item:hover,
269
+ html.studio-theme-dark body .console-card:focus-within,
270
+ html.studio-theme-dark body .select-shell:focus-within,
271
+ body.studio-theme-dark .masonry-item:hover,
272
+ body.studio-theme-dark .console-card:focus-within,
273
+ body.studio-theme-dark .select-shell:focus-within {
274
+ border-color: #566174 !important;
275
+ box-shadow: 0 20px 44px rgba(0,0,0,.28) !important;
276
+ }
277
+
278
+ html.studio-theme-dark body .console-card textarea,
279
+ html.studio-theme-dark body #prompt,
280
+ html.studio-theme-dark body #promptInput,
281
+ body.studio-theme-dark .console-card textarea,
282
+ body.studio-theme-dark #prompt,
283
+ body.studio-theme-dark #promptInput {
284
+ color: #e5e9f0 !important;
285
+ caret-color: #d8dee9;
286
+ }
287
+
288
+ html.studio-theme-dark body .size-btn,
289
+ html.studio-theme-dark body .model-option,
290
+ html.studio-theme-dark body .ratio-option,
291
+ html.studio-theme-dark body .resolution-option,
292
+ html.studio-theme-dark body .mini-ratio button,
293
+ html.studio-theme-dark body .mode-switch button,
294
+ body.studio-theme-dark .size-btn,
295
+ body.studio-theme-dark .model-option,
296
+ body.studio-theme-dark .ratio-option,
297
+ body.studio-theme-dark .resolution-option,
298
+ body.studio-theme-dark .mini-ratio button,
299
+ body.studio-theme-dark .mode-switch button {
300
+ background: #111722 !important;
301
+ border-color: #2a3444 !important;
302
+ color: #9aa6b8 !important;
303
+ }
304
+
305
+ html.studio-theme-dark body .size-btn:hover,
306
+ html.studio-theme-dark body .model-option:hover,
307
+ html.studio-theme-dark body .ratio-option:hover,
308
+ html.studio-theme-dark body .resolution-option:hover,
309
+ body.studio-theme-dark .size-btn:hover,
310
+ body.studio-theme-dark .model-option:hover,
311
+ body.studio-theme-dark .ratio-option:hover,
312
+ body.studio-theme-dark .resolution-option:hover {
313
+ border-color: #566174 !important;
314
+ color: #d8dee9 !important;
315
+ }
316
+
317
+ html.studio-theme-dark body .size-btn.active,
318
+ html.studio-theme-dark body .model-option.active,
319
+ html.studio-theme-dark body .ratio-option.active,
320
+ html.studio-theme-dark body .resolution-option.active,
321
+ html.studio-theme-dark body .mini-ratio button.active,
322
+ html.studio-theme-dark body .mode-switch button.active,
323
+ body.studio-theme-dark .size-btn.active,
324
+ body.studio-theme-dark .model-option.active,
325
+ body.studio-theme-dark .ratio-option.active,
326
+ body.studio-theme-dark .resolution-option.active,
327
+ body.studio-theme-dark .mini-ratio button.active,
328
+ body.studio-theme-dark .mode-switch button.active {
329
+ background: #d8dee9 !important;
330
+ border-color: #d8dee9 !important;
331
+ color: #10141d !important;
332
+ box-shadow: 0 10px 24px rgba(0,0,0,.2) !important;
333
+ }
334
+
335
+ html.studio-theme-dark body .ios-slider,
336
+ body.studio-theme-dark .ios-slider {
337
+ background: #2a3444 !important;
338
+ }
339
+
340
+ html.studio-theme-dark body .ios-switch input:checked + .ios-slider,
341
+ body.studio-theme-dark .ios-switch input:checked + .ios-slider {
342
+ background: #d8dee9 !important;
343
+ }
344
+
345
+ html.studio-theme-dark body .ios-slider:before,
346
+ body.studio-theme-dark .ios-slider:before {
347
+ background: #111722 !important;
348
+ box-shadow: 0 2px 8px rgba(0,0,0,.38) !important;
349
+ }
350
+
351
+ html.studio-theme-dark body .composer-wrap,
352
+ body.studio-theme-dark .composer-wrap {
353
+ background: linear-gradient(to top,#0f141d 82%,rgba(15,20,29,0)) !important;
354
+ }
355
+
356
+ html.studio-theme-dark body .canvas-gate,
357
+ body.studio-theme-dark .canvas-gate {
358
+ background-color: #0f141d !important;
359
+ background-image: radial-gradient(rgba(148,163,184,.18) 1px, transparent 1px) !important;
360
+ }
361
+
362
+ html.studio-theme-dark body .gate-panel,
363
+ html.studio-theme-dark body .canvas-item,
364
+ html.studio-theme-dark body .canvas-open,
365
+ body.studio-theme-dark .gate-panel,
366
+ body.studio-theme-dark .canvas-item,
367
+ body.studio-theme-dark .canvas-open {
368
+ background: #171d29 !important;
369
+ border-color: #2a3444 !important;
370
+ color: #e5e9f0 !important;
371
+ }
372
+
373
+ html.studio-theme-dark body .canvas-item:hover,
374
+ body.studio-theme-dark .canvas-item:hover {
375
+ background: #202838 !important;
376
+ }
377
+
378
+ html.studio-theme-dark body .canvas-item.active,
379
+ body.studio-theme-dark .canvas-item.active {
380
+ background: #263247 !important;
381
+ color: #e5e9f0 !important;
382
+ }
383
+
384
+ html.studio-theme-dark body .canvas-card-title,
385
+ html.studio-theme-dark body .gate-title,
386
+ html.studio-theme-dark body .gate-name-input,
387
+ body.studio-theme-dark .canvas-card-title,
388
+ body.studio-theme-dark .gate-title,
389
+ body.studio-theme-dark .gate-name-input {
390
+ color: #e5e9f0 !important;
391
+ }
392
+
393
+ html.studio-theme-dark body .canvas-card-time,
394
+ html.studio-theme-dark body .gate-subtitle,
395
+ body.studio-theme-dark .canvas-card-time {
396
+ color: #8f9aab !important;
397
+ }
398
+
399
+ body.studio-theme-dark .gate-subtitle {
400
+ color: #8f9aab !important;
401
+ }
402
+
403
+ html.studio-theme-dark body .canvas-preview-mark,
404
+ html.studio-theme-dark body .canvas-delete,
405
+ html.studio-theme-dark body .secondary-btn,
406
+ html.studio-theme-dark body .gate-back,
407
+ html.studio-theme-dark body .gate-trash-entry,
408
+ html.studio-theme-dark body .create-cancel,
409
+ html.studio-theme-dark body .gate-create-row,
410
+ html.studio-theme-dark body .gate-actions,
411
+ html.studio-theme-dark body .trash-note,
412
+ html.studio-theme-dark body .emoji-picker,
413
+ html.studio-theme-dark body .create-menu,
414
+ html.studio-theme-dark body .selection-hub,
415
+ html.studio-theme-dark body .output-preview,
416
+ body.studio-theme-dark .canvas-preview-mark,
417
+ body.studio-theme-dark .canvas-delete,
418
+ body.studio-theme-dark .secondary-btn,
419
+ body.studio-theme-dark .gate-back,
420
+ body.studio-theme-dark .gate-trash-entry,
421
+ body.studio-theme-dark .create-cancel,
422
+ body.studio-theme-dark .gate-create-row,
423
+ body.studio-theme-dark .gate-actions,
424
+ body.studio-theme-dark .trash-note,
425
+ body.studio-theme-dark .emoji-picker,
426
+ body.studio-theme-dark .create-menu,
427
+ body.studio-theme-dark .selection-hub,
428
+ body.studio-theme-dark .output-preview {
429
+ background: #111722 !important;
430
+ border-color: #2a3444 !important;
431
+ color: #d8dee9 !important;
432
+ }
433
+
434
+ html.studio-theme-dark body .gate-trash-badge,
435
+ body.studio-theme-dark .gate-trash-badge {
436
+ background: #d8dee9 !important;
437
+ color: #10141d !important;
438
+ border-color: #111722 !important;
439
+ }
440
+
441
+ html.studio-theme-dark body .secondary-btn.active,
442
+ body.studio-theme-dark .secondary-btn.active {
443
+ background: #d8dee9 !important;
444
+ border-color: #d8dee9 !important;
445
+ color: #10141d !important;
446
+ }
447
+
448
+ html.studio-theme-dark body .canvas-delete:hover,
449
+ html.studio-theme-dark body .create-cancel:hover,
450
+ body.studio-theme-dark .canvas-delete:hover,
451
+ body.studio-theme-dark .create-cancel:hover {
452
+ background: rgba(248,113,113,.14) !important;
453
+ border-color: rgba(248,113,113,.32) !important;
454
+ color: #fca5a5 !important;
455
+ }
456
+
457
+ html.studio-theme-dark body .canvas-restore:hover,
458
+ body.studio-theme-dark .canvas-restore:hover {
459
+ background: rgba(16,185,129,.14) !important;
460
+ border-color: rgba(16,185,129,.32) !important;
461
+ color: #6ee7b7 !important;
462
+ }
463
+
464
+ html.studio-theme-dark body .canvas-delete-box,
465
+ body.studio-theme-dark .canvas-delete-box {
466
+ background: #202838 !important;
467
+ border: 1px solid #2a3444;
468
+ }
469
+
470
+ html.studio-theme-dark body .canvas-confirm-btn,
471
+ html.studio-theme-dark body .create-confirm,
472
+ body.studio-theme-dark .canvas-confirm-btn,
473
+ body.studio-theme-dark .create-confirm {
474
+ background: #d8dee9 !important;
475
+ color: #10141d !important;
476
+ }
477
+
478
+ html.studio-theme-dark body .output-resolution,
479
+ body.studio-theme-dark .output-resolution {
480
+ background: rgba(17,23,34,.86) !important;
481
+ color: #d8dee9 !important;
482
+ }
483
+
484
+ html.studio-theme-dark body .shell .topbar,
485
+ body.studio-theme-dark .shell .topbar {
486
+ background: transparent !important;
487
+ border-color: transparent !important;
488
+ backdrop-filter: none !important;
489
+ }
490
+
491
+ html.studio-theme-dark body .shell,
492
+ html.studio-theme-dark body .shell .board,
493
+ body.studio-theme-dark .shell,
494
+ body.studio-theme-dark .shell .board {
495
+ background-color: #0f141d !important;
496
+ background-image: radial-gradient(rgba(148,163,184,.18) 1px, transparent 1px) !important;
497
+ }
498
+
499
+ html.studio-theme-dark body .shell.theme-dark,
500
+ body.studio-theme-dark .shell.theme-dark {
501
+ --page: #0f141d;
502
+ --grid: rgba(148,163,184,.18);
503
+ --panel: rgba(17,23,34,.88);
504
+ --card: #171d29;
505
+ --card-solid: #171d29;
506
+ --soft: #111722;
507
+ --soft-2: #202838;
508
+ --line: rgba(148,163,184,.16);
509
+ --line-2: rgba(148,163,184,.22);
510
+ --text: #e5e9f0;
511
+ --muted: #9aa6b8;
512
+ --faint: #78869a;
513
+ --shadow: rgba(0,0,0,.2);
514
+ --strong: #d8dee9;
515
+ --strong-text: #10141d;
516
+ }
517
+
518
+ html.studio-theme-dark body .shell .panel,
519
+ body.studio-theme-dark .shell .panel {
520
+ background: rgba(17,23,34,.86) !important;
521
+ border: 1px solid rgba(148,163,184,.16) !important;
522
+ box-shadow: 0 14px 36px rgba(0,0,0,.18) !important;
523
+ }
524
+
525
+ html.studio-theme-dark body .shell .node,
526
+ body.studio-theme-dark .shell .node {
527
+ background: #171d29 !important;
528
+ border-color: rgba(148,163,184,.16) !important;
529
+ box-shadow: 0 16px 42px rgba(0,0,0,.22) !important;
530
+ color: #e5e9f0 !important;
531
+ }
532
+
533
+ html.studio-theme-dark body .shell .node-head,
534
+ body.studio-theme-dark .shell .node-head {
535
+ background: #171d29 !important;
536
+ border-bottom-color: rgba(148,163,184,.14) !important;
537
+ }
538
+
539
+ html.studio-theme-dark body .shell .node-title,
540
+ body.studio-theme-dark .shell .node-title {
541
+ color: #8f9aab !important;
542
+ }
543
+
544
+ html.studio-theme-dark body .shell .node.selected,
545
+ body.studio-theme-dark .shell .node.selected {
546
+ outline-color: rgba(216,222,233,.72) !important;
547
+ }
548
+
549
+ html.studio-theme-dark body .shell .toolbar,
550
+ body.studio-theme-dark .shell .toolbar {
551
+ gap: 8px;
552
+ }
553
+
554
+ html.studio-theme-dark body .shell .tool-btn,
555
+ html.studio-theme-dark body .shell .select-lite,
556
+ html.studio-theme-dark body .shell .gen-count-row,
557
+ html.studio-theme-dark body .shell .seg,
558
+ html.studio-theme-dark body .shell .llm-mode,
559
+ html.studio-theme-dark body .shell .mode-tabs,
560
+ body.studio-theme-dark .shell .tool-btn,
561
+ body.studio-theme-dark .shell .select-lite,
562
+ body.studio-theme-dark .shell .gen-count-row,
563
+ body.studio-theme-dark .shell .seg,
564
+ body.studio-theme-dark .shell .llm-mode,
565
+ body.studio-theme-dark .shell .mode-tabs {
566
+ background: #111722 !important;
567
+ border: 1px solid rgba(148,163,184,.16) !important;
568
+ box-shadow: none !important;
569
+ color: #9aa6b8 !important;
570
+ }
571
+
572
+ html.studio-theme-dark body .shell .gen-settings,
573
+ html.studio-theme-dark body .shell .setting-row,
574
+ html.studio-theme-dark body .shell .comfy-settings,
575
+ body.studio-theme-dark .shell .gen-settings,
576
+ body.studio-theme-dark .shell .setting-row,
577
+ body.studio-theme-dark .shell .comfy-settings {
578
+ background: #111722 !important;
579
+ border: 1px solid rgba(148,163,184,.16) !important;
580
+ box-shadow: none !important;
581
+ color: #d8dee9 !important;
582
+ }
583
+
584
+ html.studio-theme-dark body .shell .setting-title,
585
+ body.studio-theme-dark .shell .setting-title {
586
+ color: #8f9aab !important;
587
+ }
588
+
589
+ html.studio-theme-dark body .shell .setting-input,
590
+ html.studio-theme-dark body .shell .setting-check,
591
+ html.studio-theme-dark body .shell .gen-count-input,
592
+ body.studio-theme-dark .shell .setting-input,
593
+ body.studio-theme-dark .shell .setting-check,
594
+ body.studio-theme-dark .shell .gen-count-input {
595
+ background: #171d29 !important;
596
+ border-color: rgba(148,163,184,.16) !important;
597
+ color: #e5e9f0 !important;
598
+ box-shadow: none !important;
599
+ }
600
+
601
+ html.studio-theme-dark body .shell .gen-step-btn,
602
+ body.studio-theme-dark .shell .gen-step-btn {
603
+ color: #8f9aab !important;
604
+ }
605
+
606
+ html.studio-theme-dark body .shell .gen-step-btn:hover,
607
+ body.studio-theme-dark .shell .gen-step-btn:hover {
608
+ background: #202838 !important;
609
+ color: #e5e9f0 !important;
610
+ }
611
+
612
+ html.studio-theme-dark body .shell .tool-btn:hover,
613
+ html.studio-theme-dark body .shell .select-lite:hover,
614
+ body.studio-theme-dark .shell .tool-btn:hover,
615
+ body.studio-theme-dark .shell .select-lite:hover {
616
+ background: #171d29 !important;
617
+ border-color: #566174 !important;
618
+ color: #e5e9f0 !important;
619
+ }
620
+
621
+ html.studio-theme-dark body .shell .llm-mode button,
622
+ html.studio-theme-dark body .shell .seg button,
623
+ html.studio-theme-dark body .shell .mode-tabs button,
624
+ body.studio-theme-dark .shell .llm-mode button,
625
+ body.studio-theme-dark .shell .seg button,
626
+ body.studio-theme-dark .shell .mode-tabs button {
627
+ background: transparent !important;
628
+ border-color: transparent !important;
629
+ color: #9aa6b8 !important;
630
+ box-shadow: none !important;
631
+ }
632
+
633
+ html.studio-theme-dark body .shell .llm-mode button.active,
634
+ html.studio-theme-dark body .shell .seg button.active,
635
+ html.studio-theme-dark body .shell .mode-tabs button.active,
636
+ body.studio-theme-dark .shell .llm-mode button.active,
637
+ body.studio-theme-dark .shell .seg button.active,
638
+ body.studio-theme-dark .shell .mode-tabs button.active {
639
+ background: #d8dee9 !important;
640
+ border-color: #d8dee9 !important;
641
+ color: #10141d !important;
642
+ }
643
+
644
+ html.studio-theme-dark body .shell .llm-system,
645
+ html.studio-theme-dark body .shell .llm-chat-input,
646
+ html.studio-theme-dark body .shell .llm-chat-log,
647
+ html.studio-theme-dark body .shell .llm-output,
648
+ html.studio-theme-dark body .shell .llm-chat-pane,
649
+ body.studio-theme-dark .shell .llm-system,
650
+ body.studio-theme-dark .shell .llm-chat-input,
651
+ body.studio-theme-dark .shell .llm-chat-log,
652
+ body.studio-theme-dark .shell .llm-output,
653
+ body.studio-theme-dark .shell .llm-chat-pane {
654
+ background: #111722 !important;
655
+ border-color: #2a3444 !important;
656
+ color: #d8dee9 !important;
657
+ }
658
+
659
+ html.studio-theme-dark body .shell .llm-pane-resizer::before,
660
+ body.studio-theme-dark .shell .llm-pane-resizer::before {
661
+ background: #475569 !important;
662
+ }
663
+
664
+ html.studio-theme-dark body .shell .llm-bubble.assistant,
665
+ body.studio-theme-dark .shell .llm-bubble.assistant {
666
+ background: #202838 !important;
667
+ border-color: #2a3444 !important;
668
+ color: #e5e9f0 !important;
669
+ }
670
+
671
+ html.studio-theme-dark body .shell .llm-bubble.user,
672
+ body.studio-theme-dark .shell .llm-bubble.user {
673
+ background: #d8dee9 !important;
674
+ color: #10141d !important;
675
+ }
676
+
677
+ html.studio-theme-dark body .shell .image-node img,
678
+ html.studio-theme-dark body .shell .output-grid img,
679
+ html.studio-theme-dark body .shell .output-preview img,
680
+ html.studio-theme-dark body .shell .blank-image,
681
+ html.studio-theme-dark body .shell .input-item,
682
+ html.studio-theme-dark body .shell .prompt-node textarea,
683
+ html.studio-theme-dark body .shell .llm-system,
684
+ html.studio-theme-dark body .shell .llm-chat-input,
685
+ html.studio-theme-dark body .shell .model-input,
686
+ body.studio-theme-dark .shell .image-node img,
687
+ body.studio-theme-dark .shell .output-grid img,
688
+ body.studio-theme-dark .shell .output-preview img,
689
+ body.studio-theme-dark .shell .blank-image,
690
+ body.studio-theme-dark .shell .input-item,
691
+ body.studio-theme-dark .shell .prompt-node textarea,
692
+ body.studio-theme-dark .shell .llm-system,
693
+ body.studio-theme-dark .shell .llm-chat-input,
694
+ body.studio-theme-dark .shell .model-input {
695
+ background: #0f141d !important;
696
+ border-color: rgba(148,163,184,.16) !important;
697
+ }
698
+
699
+ html.studio-theme-dark body .shell .blank-image,
700
+ body.studio-theme-dark .shell .blank-image {
701
+ color: #8f9aab !important;
702
+ border-style: dashed !important;
703
+ }
704
+
705
+ html.studio-theme-dark body .shell .blank-image:hover,
706
+ html.studio-theme-dark body .shell .blank-image.drag-over,
707
+ body.studio-theme-dark .shell .blank-image:hover,
708
+ body.studio-theme-dark .shell .blank-image.drag-over {
709
+ background: #151b27 !important;
710
+ border-color: rgba(216,222,233,.42) !important;
711
+ color: #d8dee9 !important;
712
+ }
713
+
714
+ html.studio-theme-dark body .shell .image-caption,
715
+ body.studio-theme-dark .shell .image-caption {
716
+ color: #8f9aab !important;
717
+ }
718
+
719
+ html.studio-theme-dark body .shell .input-label,
720
+ body.studio-theme-dark .shell .input-label {
721
+ background: rgba(15,20,29,.78) !important;
722
+ color: #d8dee9 !important;
723
+ }
724
+
725
+ html.studio-theme-dark body .shell .prompt-list .bg-slate-50,
726
+ html.studio-theme-dark body .shell .prompt-list .border-slate-100,
727
+ body.studio-theme-dark .shell .prompt-list .bg-slate-50,
728
+ body.studio-theme-dark .shell .prompt-list .border-slate-100 {
729
+ background: #111722 !important;
730
+ border-color: rgba(148,163,184,.16) !important;
731
+ color: #c8d0dc !important;
732
+ }
733
+
734
+ html.studio-theme-dark body .shell .input-index,
735
+ body.studio-theme-dark .shell .input-index {
736
+ background: #d8dee9 !important;
737
+ color: #10141d !important;
738
+ }
739
+
740
+ html.studio-theme-dark body .shell .port::after,
741
+ body.studio-theme-dark .shell .port::after {
742
+ background: #d8dee9 !important;
743
+ border-color: #111722 !important;
744
+ box-shadow: 0 0 0 1px rgba(148,163,184,.32) !important;
745
+ }
746
+
747
+ html.studio-theme-dark body .shell .drop-overlay,
748
+ body.studio-theme-dark .shell .drop-overlay {
749
+ background: rgba(15,20,29,.44) !important;
750
+ color: #c8d0dc !important;
751
+ backdrop-filter: blur(2px) !important;
752
+ }
753
+
754
+ html.studio-theme-dark body .output-drag-preview,
755
+ body.studio-theme-dark .output-drag-preview,
756
+ .output-drag-preview {
757
+ position: fixed;
758
+ left: -10000px;
759
+ top: -10000px;
760
+ width: 150px;
761
+ height: 150px;
762
+ padding: 8px;
763
+ border-radius: 18px;
764
+ background: #0f141d;
765
+ border: 1px solid rgba(148,163,184,.18);
766
+ box-shadow: 0 16px 40px rgba(0,0,0,.28);
767
+ pointer-events: none;
768
+ z-index: -1;
769
+ }
770
+
771
+ .output-drag-preview img {
772
+ width: 100%;
773
+ height: 100%;
774
+ object-fit: contain;
775
+ border-radius: 12px;
776
+ background: #0f141d;
777
+ }
778
+
779
+ html.studio-theme-dark body .shell .resize-handle,
780
+ body.studio-theme-dark .shell .resize-handle {
781
+ background: rgba(17,23,34,.86) !important;
782
+ border-color: rgba(148,163,184,.18) !important;
783
+ color: #8f9aab !important;
784
+ }
785
+
786
+ html.studio-theme-dark body .shell .resize-handle:hover,
787
+ body.studio-theme-dark .shell .resize-handle:hover {
788
+ background: #171d29 !important;
789
+ border-color: rgba(216,222,233,.36) !important;
790
+ color: #d8dee9 !important;
791
+ }
792
+
793
+ /* Standalone studio pages: zimage / enhance / klein / angle */
794
+ html.studio-theme-dark body .layout-container,
795
+ body.studio-theme-dark .layout-container,
796
+ html.studio-theme-dark body .container-box,
797
+ body.studio-theme-dark .container-box {
798
+ color: #e5e9f0 !important;
799
+ }
800
+
801
+ html.studio-theme-dark body .mode-switcher,
802
+ html.studio-theme-dark body .engine-panel,
803
+ html.studio-theme-dark body .engine-switch,
804
+ html.studio-theme-dark body .ios-slider,
805
+ html.studio-theme-dark body .result-frame,
806
+ html.studio-theme-dark body .gallery-lightbox,
807
+ html.studio-theme-dark body #lightbox,
808
+ html.studio-theme-dark body #compareContainer,
809
+ html.studio-theme-dark body #compareContainerKlein,
810
+ html.studio-theme-dark body #lightboxImg,
811
+ body.studio-theme-dark .mode-switcher,
812
+ body.studio-theme-dark .engine-panel,
813
+ body.studio-theme-dark .engine-switch,
814
+ body.studio-theme-dark .ios-slider,
815
+ body.studio-theme-dark .result-frame,
816
+ body.studio-theme-dark .gallery-lightbox,
817
+ body.studio-theme-dark #lightbox,
818
+ body.studio-theme-dark #compareContainer,
819
+ body.studio-theme-dark #compareContainerKlein,
820
+ body.studio-theme-dark #lightboxImg {
821
+ background-color: #111722 !important;
822
+ border-color: rgba(148,163,184,.16) !important;
823
+ color: #e5e9f0 !important;
824
+ }
825
+
826
+ html.studio-theme-dark body .mode-glider,
827
+ html.studio-theme-dark body .engine-btn.active,
828
+ html.studio-theme-dark body .ios-slider:before,
829
+ body.studio-theme-dark .mode-glider,
830
+ body.studio-theme-dark .engine-btn.active,
831
+ body.studio-theme-dark .ios-slider:before {
832
+ background: #d8dee9 !important;
833
+ color: #10141d !important;
834
+ }
835
+
836
+ html.studio-theme-dark body .mode-btn.active,
837
+ html.studio-theme-dark body .engine-btn.active,
838
+ body.studio-theme-dark .mode-btn.active,
839
+ body.studio-theme-dark .engine-btn.active {
840
+ color: #10141d !important;
841
+ }
842
+
843
+ html.studio-theme-dark body .mode-btn,
844
+ html.studio-theme-dark body .engine-btn,
845
+ html.studio-theme-dark body .cloud-status,
846
+ body.studio-theme-dark .mode-btn,
847
+ body.studio-theme-dark .engine-btn,
848
+ body.studio-theme-dark .cloud-status {
849
+ color: #8f9aab !important;
850
+ }
851
+
852
+ html.studio-theme-dark body .engine-btn:not(.active):hover,
853
+ body.studio-theme-dark .engine-btn:not(.active):hover {
854
+ background: rgba(216,222,233,.08) !important;
855
+ color: #e5e9f0 !important;
856
+ }
857
+
858
+ html.studio-theme-dark body .upload-item:hover,
859
+ html.studio-theme-dark body .upload-item.drag-over,
860
+ body.studio-theme-dark .upload-item:hover,
861
+ body.studio-theme-dark .upload-item.drag-over {
862
+ background: #171d29 !important;
863
+ border-color: rgba(216,222,233,.42) !important;
864
+ }
865
+
866
+ html.studio-theme-dark body .glass-btn,
867
+ html.studio-theme-dark body .btn-render,
868
+ html.studio-theme-dark body #downloadBtn,
869
+ html.studio-theme-dark body #lightboxDownload,
870
+ body.studio-theme-dark .glass-btn,
871
+ body.studio-theme-dark .btn-render,
872
+ body.studio-theme-dark #downloadBtn,
873
+ body.studio-theme-dark #lightboxDownload {
874
+ background: #d8dee9 !important;
875
+ color: #10141d !important;
876
+ border-color: #d8dee9 !important;
877
+ }
878
+
879
+ html.studio-theme-dark body .glass-btn:hover,
880
+ html.studio-theme-dark body .btn-render:hover,
881
+ body.studio-theme-dark .glass-btn:hover,
882
+ body.studio-theme-dark .btn-render:hover {
883
+ background: #f1f5f9 !important;
884
+ color: #0f141d !important;
885
+ }
886
+
887
+ html.studio-theme-dark body input[type=range]::-webkit-slider-runnable-track,
888
+ body.studio-theme-dark input[type=range]::-webkit-slider-runnable-track {
889
+ background: #2a3444 !important;
890
+ }
891
+
892
+ html.studio-theme-dark body input[type=range]::-webkit-slider-thumb,
893
+ body.studio-theme-dark input[type=range]::-webkit-slider-thumb {
894
+ background: #d8dee9 !important;
895
+ }
896
+
897
+ html.studio-theme-dark body .masonry-item img,
898
+ html.studio-theme-dark body .result-frame img,
899
+ body.studio-theme-dark .masonry-item img,
900
+ body.studio-theme-dark .result-frame img {
901
+ background-color: #111722 !important;
902
+ }
903
+
904
+ html.studio-theme-dark body .bg-black,
905
+ body.studio-theme-dark .bg-black {
906
+ background-color: #d8dee9 !important;
907
+ color: #10141d !important;
908
+ }
909
+
910
+ html.studio-theme-dark body .hover\:text-black:hover,
911
+ body.studio-theme-dark .hover\:text-black:hover {
912
+ color: #e5e9f0 !important;
913
+ }
914
+
915
+ /* Final neutral dark pass: remove blue-tinted surfaces from tool pages. */
916
+ html.studio-theme-dark,
917
+ html.studio-theme-dark body,
918
+ body.studio-theme-dark {
919
+ background: #08090c !important;
920
+ color: #e8e8ea !important;
921
+ }
922
+
923
+ body.studio-theme-dark {
924
+ --bg: #08090c;
925
+ --bg-base: #08090c;
926
+ --page: #08090c;
927
+ --card: #111216;
928
+ --card-solid: #111216;
929
+ --soft: #0c0d10;
930
+ --soft-2: #16171b;
931
+ --panel: rgba(17,18,22,.92);
932
+ --text: #e8e8ea;
933
+ --text-main: #e8e8ea;
934
+ --accent: #e8e8ea;
935
+ --line: #25262b;
936
+ --line-2: #32343a;
937
+ }
938
+
939
+ html.studio-theme-dark body .app-shell,
940
+ html.studio-theme-dark body .sidebar,
941
+ html.studio-theme-dark body .stage,
942
+ html.studio-theme-dark body .shell,
943
+ body.studio-theme-dark .app-shell,
944
+ body.studio-theme-dark .sidebar,
945
+ body.studio-theme-dark .stage,
946
+ body.studio-theme-dark .shell {
947
+ background: #08090c !important;
948
+ border-color: #25262b !important;
949
+ }
950
+
951
+ html.studio-theme-dark body .console-card,
952
+ html.studio-theme-dark body .nano-input,
953
+ html.studio-theme-dark body .upload-item,
954
+ html.studio-theme-dark body .result-frame,
955
+ html.studio-theme-dark body .masonry-item,
956
+ html.studio-theme-dark body .engine-panel,
957
+ html.studio-theme-dark body .engine-switch,
958
+ html.studio-theme-dark body .mode-switcher,
959
+ html.studio-theme-dark body .setting-row,
960
+ html.studio-theme-dark body .provider-switch,
961
+ html.studio-theme-dark body .ios-slider,
962
+ html.studio-theme-dark body #compareContainer,
963
+ html.studio-theme-dark body #lightboxImg,
964
+ html.studio-theme-dark body #lightbox .bg-white,
965
+ html.studio-theme-dark body .bg-white,
966
+ html.studio-theme-dark body .bg-white\/90,
967
+ html.studio-theme-dark body .bg-white\/95,
968
+ html.studio-theme-dark body .bg-gray-50,
969
+ html.studio-theme-dark body .bg-gray-50\/50,
970
+ html.studio-theme-dark body .bg-gray-100,
971
+ html.studio-theme-dark body .bg-gray-200,
972
+ html.studio-theme-dark body .bg-\[\#f8f8f9\],
973
+ html.studio-theme-dark body .bg-\[\#fafafa\],
974
+ body.studio-theme-dark .console-card,
975
+ body.studio-theme-dark .nano-input,
976
+ body.studio-theme-dark .upload-item,
977
+ body.studio-theme-dark .result-frame,
978
+ body.studio-theme-dark .masonry-item,
979
+ body.studio-theme-dark .engine-panel,
980
+ body.studio-theme-dark .engine-switch,
981
+ body.studio-theme-dark .mode-switcher,
982
+ body.studio-theme-dark .setting-row,
983
+ body.studio-theme-dark .provider-switch,
984
+ body.studio-theme-dark .ios-slider,
985
+ body.studio-theme-dark #compareContainer,
986
+ body.studio-theme-dark #lightboxImg,
987
+ body.studio-theme-dark #lightbox .bg-white,
988
+ body.studio-theme-dark .bg-white,
989
+ body.studio-theme-dark .bg-white\/90,
990
+ body.studio-theme-dark .bg-white\/95,
991
+ body.studio-theme-dark .bg-gray-50,
992
+ body.studio-theme-dark .bg-gray-50\/50,
993
+ body.studio-theme-dark .bg-gray-100,
994
+ body.studio-theme-dark .bg-gray-200,
995
+ body.studio-theme-dark .bg-\[\#f8f8f9\],
996
+ body.studio-theme-dark .bg-\[\#fafafa\] {
997
+ background-color: #111216 !important;
998
+ border-color: #25262b !important;
999
+ color: #e8e8ea !important;
1000
+ }
1001
+
1002
+ html.studio-theme-dark body #lightbox,
1003
+ body.studio-theme-dark #lightbox {
1004
+ background: rgba(8,9,12,.96) !important;
1005
+ }
1006
+
1007
+ html.studio-theme-dark body .glass-btn,
1008
+ html.studio-theme-dark body .btn-render,
1009
+ html.studio-theme-dark body .mode-glider,
1010
+ html.studio-theme-dark body .engine-btn.active,
1011
+ html.studio-theme-dark body .ios-switch input:checked + .ios-slider,
1012
+ html.studio-theme-dark body #cloud-progress-bar,
1013
+ body.studio-theme-dark .glass-btn,
1014
+ body.studio-theme-dark .btn-render,
1015
+ body.studio-theme-dark .mode-glider,
1016
+ body.studio-theme-dark .engine-btn.active,
1017
+ body.studio-theme-dark .ios-switch input:checked + .ios-slider,
1018
+ body.studio-theme-dark #cloud-progress-bar {
1019
+ background: #e8e8ea !important;
1020
+ color: #08090c !important;
1021
+ border-color: #e8e8ea !important;
1022
+ }
1023
+
1024
+ html.studio-theme-dark body .mode-btn.active,
1025
+ html.studio-theme-dark body .engine-btn.active,
1026
+ html.studio-theme-dark body .text-black,
1027
+ body.studio-theme-dark .mode-btn.active,
1028
+ body.studio-theme-dark .engine-btn.active,
1029
+ body.studio-theme-dark .text-black {
1030
+ color: #e8e8ea !important;
1031
+ }
1032
+
1033
+ html.studio-theme-dark body .mode-glider + .mode-btn.active,
1034
+ body.studio-theme-dark .mode-glider + .mode-btn.active {
1035
+ color: #08090c !important;
1036
+ }
1037
+
1038
+ html.studio-theme-dark body input,
1039
+ html.studio-theme-dark body textarea,
1040
+ html.studio-theme-dark body select,
1041
+ body.studio-theme-dark input,
1042
+ body.studio-theme-dark textarea,
1043
+ body.studio-theme-dark select {
1044
+ background-color: #0c0d10 !important;
1045
+ border-color: #32343a !important;
1046
+ color: #e8e8ea !important;
1047
+ }
1048
+
1049
+ html.studio-theme-dark body .border-gray-100,
1050
+ html.studio-theme-dark body .border-gray-200,
1051
+ html.studio-theme-dark body .border-gray-200\/50,
1052
+ html.studio-theme-dark body .border-black,
1053
+ body.studio-theme-dark .border-gray-100,
1054
+ body.studio-theme-dark .border-gray-200,
1055
+ body.studio-theme-dark .border-gray-200\/50,
1056
+ body.studio-theme-dark .border-black {
1057
+ border-color: #25262b !important;
1058
+ }
1059
+
1060
+ html.studio-theme-dark body .text-gray-300,
1061
+ html.studio-theme-dark body .text-gray-400,
1062
+ html.studio-theme-dark body .text-gray-500,
1063
+ html.studio-theme-dark body .text-gray-600,
1064
+ html.studio-theme-dark body .text-gray-700,
1065
+ html.studio-theme-dark body .text-gray-800,
1066
+ body.studio-theme-dark .text-gray-300,
1067
+ body.studio-theme-dark .text-gray-400,
1068
+ body.studio-theme-dark .text-gray-500,
1069
+ body.studio-theme-dark .text-gray-600,
1070
+ body.studio-theme-dark .text-gray-700,
1071
+ body.studio-theme-dark .text-gray-800 {
1072
+ color: #a7a7ad !important;
1073
+ }
1074
+
1075
+ html.studio-theme-dark body h1,
1076
+ html.studio-theme-dark body h2,
1077
+ html.studio-theme-dark body h3,
1078
+ html.studio-theme-dark body label,
1079
+ body.studio-theme-dark h1,
1080
+ body.studio-theme-dark h2,
1081
+ body.studio-theme-dark h3,
1082
+ body.studio-theme-dark label {
1083
+ color: #e8e8ea !important;
1084
+ }
1085
+
1086
+ html.studio-theme-dark body .mode-switcher .mode-btn.active,
1087
+ html.studio-theme-dark body .engine-switch .engine-btn.active,
1088
+ body.studio-theme-dark .mode-switcher .mode-btn.active,
1089
+ body.studio-theme-dark .engine-switch .engine-btn.active {
1090
+ color: #08090c !important;
1091
+ }
1092
+
1093
+ /* Online-generation dark pass: soft blue-gray instead of heavy black. */
1094
+ html.studio-theme-dark,
1095
+ html.studio-theme-dark body,
1096
+ body.studio-theme-dark {
1097
+ background: #0f141d !important;
1098
+ color: #e5e9f0 !important;
1099
+ }
1100
+
1101
+ body.studio-theme-dark {
1102
+ --bg: #0f141d;
1103
+ --bg-base: #0f141d;
1104
+ --page: #0f141d;
1105
+ --card: #171d29;
1106
+ --card-solid: #171d29;
1107
+ --soft: #111722;
1108
+ --soft-2: #1d2533;
1109
+ --panel: rgba(23,29,41,.92);
1110
+ --text: #e5e9f0;
1111
+ --text-main: #e5e9f0;
1112
+ --accent: #e5e9f0;
1113
+ --line: #2a3444;
1114
+ --line-2: #334155;
1115
+ }
1116
+
1117
+ html.studio-theme-dark body .app-shell,
1118
+ html.studio-theme-dark body .sidebar,
1119
+ html.studio-theme-dark body .stage,
1120
+ html.studio-theme-dark body .shell,
1121
+ body.studio-theme-dark .app-shell,
1122
+ body.studio-theme-dark .sidebar,
1123
+ body.studio-theme-dark .stage,
1124
+ body.studio-theme-dark .shell {
1125
+ background: #0f141d !important;
1126
+ border-color: #2a3444 !important;
1127
+ }
1128
+
1129
+ html.studio-theme-dark body .stage,
1130
+ body.studio-theme-dark .stage {
1131
+ background: #111722 !important;
1132
+ }
1133
+
1134
+ html.studio-theme-dark body .console-card,
1135
+ html.studio-theme-dark body .nano-input,
1136
+ html.studio-theme-dark body .upload-item,
1137
+ html.studio-theme-dark body .result-frame,
1138
+ html.studio-theme-dark body .masonry-item,
1139
+ html.studio-theme-dark body .engine-panel,
1140
+ html.studio-theme-dark body .tool-panel,
1141
+ html.studio-theme-dark body .control-panel,
1142
+ html.studio-theme-dark body .engine-switch,
1143
+ html.studio-theme-dark body .mode-switcher,
1144
+ html.studio-theme-dark body .setting-row,
1145
+ html.studio-theme-dark body .ios-slider,
1146
+ html.studio-theme-dark body #compareContainer,
1147
+ html.studio-theme-dark body #lightboxImg,
1148
+ html.studio-theme-dark body #lightbox .bg-white,
1149
+ html.studio-theme-dark body .bg-white,
1150
+ html.studio-theme-dark body .bg-white\/90,
1151
+ html.studio-theme-dark body .bg-white\/95,
1152
+ html.studio-theme-dark body .bg-gray-50,
1153
+ html.studio-theme-dark body .bg-gray-50\/50,
1154
+ html.studio-theme-dark body .bg-gray-100,
1155
+ html.studio-theme-dark body .bg-gray-200,
1156
+ html.studio-theme-dark body .bg-\[\#f8f8f9\],
1157
+ html.studio-theme-dark body .bg-\[\#fafafa\],
1158
+ body.studio-theme-dark .console-card,
1159
+ body.studio-theme-dark .nano-input,
1160
+ body.studio-theme-dark .upload-item,
1161
+ body.studio-theme-dark .result-frame,
1162
+ body.studio-theme-dark .masonry-item,
1163
+ body.studio-theme-dark .engine-panel,
1164
+ body.studio-theme-dark .tool-panel,
1165
+ body.studio-theme-dark .control-panel,
1166
+ body.studio-theme-dark .engine-switch,
1167
+ body.studio-theme-dark .mode-switcher,
1168
+ body.studio-theme-dark .setting-row,
1169
+ body.studio-theme-dark .ios-slider,
1170
+ body.studio-theme-dark #compareContainer,
1171
+ body.studio-theme-dark #lightboxImg,
1172
+ body.studio-theme-dark #lightbox .bg-white,
1173
+ body.studio-theme-dark .bg-white,
1174
+ body.studio-theme-dark .bg-white\/90,
1175
+ body.studio-theme-dark .bg-white\/95,
1176
+ body.studio-theme-dark .bg-gray-50,
1177
+ body.studio-theme-dark .bg-gray-50\/50,
1178
+ body.studio-theme-dark .bg-gray-100,
1179
+ body.studio-theme-dark .bg-gray-200,
1180
+ body.studio-theme-dark .bg-\[\#f8f8f9\],
1181
+ body.studio-theme-dark .bg-\[\#fafafa\] {
1182
+ background-color: #171d29 !important;
1183
+ border-color: #2a3444 !important;
1184
+ color: #e5e9f0 !important;
1185
+ }
1186
+
1187
+ html.studio-theme-dark body .model-pill,
1188
+ html.studio-theme-dark body .ratio-grid,
1189
+ html.studio-theme-dark body .resolution-toggle,
1190
+ html.studio-theme-dark body .select-shell,
1191
+ html.studio-theme-dark body .mode-switch,
1192
+ html.studio-theme-dark body .mini-ratio,
1193
+ html.studio-theme-dark body input,
1194
+ html.studio-theme-dark body textarea,
1195
+ html.studio-theme-dark body select,
1196
+ body.studio-theme-dark .model-pill,
1197
+ body.studio-theme-dark .ratio-grid,
1198
+ body.studio-theme-dark .resolution-toggle,
1199
+ body.studio-theme-dark .select-shell,
1200
+ body.studio-theme-dark .mode-switch,
1201
+ body.studio-theme-dark .mini-ratio,
1202
+ body.studio-theme-dark input,
1203
+ body.studio-theme-dark textarea,
1204
+ body.studio-theme-dark select {
1205
+ background-color: #111722 !important;
1206
+ border-color: #334155 !important;
1207
+ color: #e5e9f0 !important;
1208
+ }
1209
+
1210
+ html.studio-theme-dark body #lightbox,
1211
+ body.studio-theme-dark #lightbox {
1212
+ background: rgba(15,20,29,.9) !important;
1213
+ }
1214
+
1215
+ html.studio-theme-dark body .glass-btn,
1216
+ html.studio-theme-dark body .btn-render,
1217
+ html.studio-theme-dark body .mode-glider,
1218
+ html.studio-theme-dark body .engine-btn.active,
1219
+ html.studio-theme-dark body .model-option.active,
1220
+ html.studio-theme-dark body .ratio-option.active,
1221
+ html.studio-theme-dark body .resolution-option.active,
1222
+ html.studio-theme-dark body .provider-btn.active,
1223
+ html.studio-theme-dark body .ios-switch input:checked + .ios-slider,
1224
+ html.studio-theme-dark body #cloud-progress-bar,
1225
+ html.studio-theme-dark body #downloadBtn,
1226
+ html.studio-theme-dark body #lightboxDownload,
1227
+ html.studio-theme-dark body .bg-black,
1228
+ body.studio-theme-dark .glass-btn,
1229
+ body.studio-theme-dark .btn-render,
1230
+ body.studio-theme-dark .mode-glider,
1231
+ body.studio-theme-dark .engine-btn.active,
1232
+ body.studio-theme-dark .model-option.active,
1233
+ body.studio-theme-dark .ratio-option.active,
1234
+ body.studio-theme-dark .resolution-option.active,
1235
+ body.studio-theme-dark .provider-btn.active,
1236
+ body.studio-theme-dark .ios-switch input:checked + .ios-slider,
1237
+ body.studio-theme-dark #cloud-progress-bar,
1238
+ body.studio-theme-dark #downloadBtn,
1239
+ body.studio-theme-dark #lightboxDownload,
1240
+ body.studio-theme-dark .bg-black {
1241
+ background: #d8dee9 !important;
1242
+ color: #10141d !important;
1243
+ border-color: #d8dee9 !important;
1244
+ }
1245
+
1246
+ html.studio-theme-dark body .mode-btn,
1247
+ html.studio-theme-dark body .engine-btn,
1248
+ html.studio-theme-dark body .model-option,
1249
+ html.studio-theme-dark body .ratio-option,
1250
+ html.studio-theme-dark body .resolution-option,
1251
+ html.studio-theme-dark body .provider-btn,
1252
+ body.studio-theme-dark .mode-btn,
1253
+ body.studio-theme-dark .engine-btn,
1254
+ body.studio-theme-dark .model-option,
1255
+ body.studio-theme-dark .ratio-option,
1256
+ body.studio-theme-dark .resolution-option {
1257
+ color: #8f9aab !important;
1258
+ }
1259
+
1260
+ body.studio-theme-dark .provider-btn {
1261
+ color: #8f9aab !important;
1262
+ }
1263
+
1264
+ html.studio-theme-dark body .provider-switch button,
1265
+ body.studio-theme-dark .provider-switch button {
1266
+ color: #8f9aab !important;
1267
+ }
1268
+
1269
+ html.studio-theme-dark body .mode-btn:hover,
1270
+ html.studio-theme-dark body .engine-btn:hover,
1271
+ html.studio-theme-dark body .model-option:hover,
1272
+ html.studio-theme-dark body .ratio-option:hover,
1273
+ html.studio-theme-dark body .resolution-option:hover,
1274
+ html.studio-theme-dark body .provider-btn:hover,
1275
+ body.studio-theme-dark .mode-btn:hover,
1276
+ body.studio-theme-dark .engine-btn:hover,
1277
+ body.studio-theme-dark .model-option:hover,
1278
+ body.studio-theme-dark .ratio-option:hover,
1279
+ body.studio-theme-dark .resolution-option:hover,
1280
+ body.studio-theme-dark .provider-btn:hover {
1281
+ background: rgba(216,222,233,.08) !important;
1282
+ color: #e5e9f0 !important;
1283
+ }
1284
+
1285
+ html.studio-theme-dark body .provider-switch button:hover,
1286
+ body.studio-theme-dark .provider-switch button:hover {
1287
+ background: rgba(216,222,233,.08) !important;
1288
+ color: #e5e9f0 !important;
1289
+ }
1290
+
1291
+ html.studio-theme-dark body .mode-btn.active,
1292
+ html.studio-theme-dark body .engine-btn.active,
1293
+ html.studio-theme-dark body .model-option.active,
1294
+ html.studio-theme-dark body .ratio-option.active,
1295
+ html.studio-theme-dark body .resolution-option.active,
1296
+ html.studio-theme-dark body .provider-btn.active,
1297
+ html.studio-theme-dark body .mode-switcher .mode-btn.active,
1298
+ html.studio-theme-dark body .engine-switch .engine-btn.active,
1299
+ body.studio-theme-dark .mode-btn.active,
1300
+ body.studio-theme-dark .engine-btn.active,
1301
+ body.studio-theme-dark .model-option.active,
1302
+ body.studio-theme-dark .ratio-option.active,
1303
+ body.studio-theme-dark .resolution-option.active,
1304
+ body.studio-theme-dark .provider-btn.active,
1305
+ body.studio-theme-dark .mode-switcher .mode-btn.active,
1306
+ body.studio-theme-dark .engine-switch .engine-btn.active {
1307
+ color: #10141d !important;
1308
+ }
1309
+
1310
+ html.studio-theme-dark body .provider-switch button.active,
1311
+ body.studio-theme-dark .provider-switch button.active {
1312
+ background: #d8dee9 !important;
1313
+ color: #10141d !important;
1314
+ border-color: #d8dee9 !important;
1315
+ box-shadow: 0 2px 8px rgba(0,0,0,.18) !important;
1316
+ }
1317
+
1318
+ html.studio-theme-dark body .mode-switcher,
1319
+ body.studio-theme-dark .mode-switcher,
1320
+ html.studio-theme-dark body .provider-switch,
1321
+ body.studio-theme-dark .provider-switch,
1322
+ html.studio-theme-dark body .engine-switch,
1323
+ body.studio-theme-dark .engine-switch {
1324
+ background: #111722 !important;
1325
+ border: 1px solid #334155 !important;
1326
+ box-shadow: none !important;
1327
+ }
1328
+
1329
+ html.studio-theme-dark body .mode-glider,
1330
+ body.studio-theme-dark .mode-glider {
1331
+ background: #d8dee9 !important;
1332
+ box-shadow: 0 2px 8px rgba(0,0,0,.18) !important;
1333
+ }
1334
+
1335
+ html.studio-theme-dark body .mode-btn,
1336
+ body.studio-theme-dark .mode-btn,
1337
+ html.studio-theme-dark body .engine-btn,
1338
+ body.studio-theme-dark .engine-btn,
1339
+ html.studio-theme-dark body .provider-btn,
1340
+ body.studio-theme-dark .provider-btn {
1341
+ border-radius: 999px !important;
1342
+ letter-spacing: .02em !important;
1343
+ }
1344
+
1345
+ html.studio-theme-dark body .provider-switch button,
1346
+ body.studio-theme-dark .provider-switch button {
1347
+ border-radius: 999px !important;
1348
+ min-height: 30px !important;
1349
+ font-size: 10px !important;
1350
+ font-weight: 600 !important;
1351
+ letter-spacing: .02em !important;
1352
+ }
1353
+
1354
+ html.studio-theme-dark body .border-gray-100,
1355
+ html.studio-theme-dark body .border-gray-200,
1356
+ html.studio-theme-dark body .border-gray-200\/50,
1357
+ html.studio-theme-dark body .border-black,
1358
+ body.studio-theme-dark .border-gray-100,
1359
+ body.studio-theme-dark .border-gray-200,
1360
+ body.studio-theme-dark .border-gray-200\/50,
1361
+ body.studio-theme-dark .border-black {
1362
+ border-color: #2a3444 !important;
1363
+ }
1364
+
1365
+ html.studio-theme-dark body .text-gray-300,
1366
+ html.studio-theme-dark body .text-gray-400,
1367
+ html.studio-theme-dark body .text-gray-500,
1368
+ html.studio-theme-dark body .text-gray-600,
1369
+ html.studio-theme-dark body .text-gray-700,
1370
+ html.studio-theme-dark body .text-gray-800,
1371
+ body.studio-theme-dark .text-gray-300,
1372
+ body.studio-theme-dark .text-gray-400,
1373
+ body.studio-theme-dark .text-gray-500,
1374
+ body.studio-theme-dark .text-gray-600,
1375
+ body.studio-theme-dark .text-gray-700,
1376
+ body.studio-theme-dark .text-gray-800 {
1377
+ color: #8f9aab !important;
1378
+ }
1379
+
1380
+ html.studio-theme-dark body h1,
1381
+ html.studio-theme-dark body h2,
1382
+ html.studio-theme-dark body h3,
1383
+ html.studio-theme-dark body label,
1384
+ body.studio-theme-dark h1,
1385
+ body.studio-theme-dark h2,
1386
+ body.studio-theme-dark h3,
1387
+ body.studio-theme-dark label {
1388
+ color: #e5e9f0 !important;
1389
+ }
1390
+
1391
+ html.studio-theme-dark body input[type=range]::-webkit-slider-runnable-track,
1392
+ body.studio-theme-dark input[type=range]::-webkit-slider-runnable-track {
1393
+ background: #2a3444 !important;
1394
+ }
1395
+
1396
+ html.studio-theme-dark body input[type=range]::-webkit-slider-thumb,
1397
+ body.studio-theme-dark input[type=range]::-webkit-slider-thumb {
1398
+ background: #d8dee9 !important;
1399
+ border-color: #0f141d !important;
1400
+ }
1401
+
1402
+ html.studio-theme-dark body ::placeholder,
1403
+ body.studio-theme-dark ::placeholder {
1404
+ color: #64748b !important;
1405
+ }
1406
+
1407
+ /* Z-image console polish: keep the prompt field open and make ModelScope readable. */
1408
+ html.studio-theme-dark body #prompt,
1409
+ body.studio-theme-dark #prompt {
1410
+ background: transparent !important;
1411
+ border-color: transparent !important;
1412
+ color: #e5e9f0 !important;
1413
+ }
1414
+
1415
+ html.studio-theme-dark body .console-card > .bg-gray-50\/50,
1416
+ body.studio-theme-dark .console-card > .bg-gray-50\/50 {
1417
+ background: transparent !important;
1418
+ }
1419
+
1420
+ html.studio-theme-dark body #prompt::placeholder,
1421
+ body.studio-theme-dark #prompt::placeholder {
1422
+ color: #6f7b8f !important;
1423
+ }
1424
+
1425
+ /* GPT chat dark polish: avoid harsh black bars around text areas. */
1426
+ html.studio-theme-dark body .chat-shell,
1427
+ body.studio-theme-dark .chat-shell,
1428
+ html.studio-theme-dark body .messages,
1429
+ body.studio-theme-dark .messages {
1430
+ background: #0f141d !important;
1431
+ }
1432
+
1433
+ html.studio-theme-dark body .topbar,
1434
+ body.studio-theme-dark .topbar {
1435
+ background: rgba(15,20,29,.9) !important;
1436
+ border-bottom-color: #2a3444 !important;
1437
+ }
1438
+
1439
+ html.studio-theme-dark body .composer-wrap,
1440
+ body.studio-theme-dark .composer-wrap {
1441
+ background: linear-gradient(to top,#0f141d 82%,rgba(15,20,29,0)) !important;
1442
+ }
1443
+
1444
+ html.studio-theme-dark body .composer,
1445
+ body.studio-theme-dark .composer,
1446
+ html.studio-theme-dark body .history-popover,
1447
+ body.studio-theme-dark .history-popover {
1448
+ background: #171d29 !important;
1449
+ border-color: #2a3444 !important;
1450
+ color: #e5e9f0 !important;
1451
+ box-shadow: 0 18px 44px rgba(0,0,0,.24) !important;
1452
+ }
1453
+
1454
+ html.studio-theme-dark body .composer-body,
1455
+ body.studio-theme-dark .composer-body {
1456
+ background: #111722 !important;
1457
+ border-color: #334155 !important;
1458
+ }
1459
+
1460
+ html.studio-theme-dark body #messageInput,
1461
+ body.studio-theme-dark #messageInput {
1462
+ background: transparent !important;
1463
+ border-color: transparent !important;
1464
+ color: #e5e9f0 !important;
1465
+ }
1466
+
1467
+ html.studio-theme-dark body #messageInput::placeholder,
1468
+ body.studio-theme-dark #messageInput::placeholder {
1469
+ color: #64748b !important;
1470
+ }
1471
+
1472
+ html.studio-theme-dark body .title-block h1,
1473
+ body.studio-theme-dark .title-block h1 {
1474
+ color: #e5e9f0 !important;
1475
+ }
1476
+
1477
+ html.studio-theme-dark body .title-block p,
1478
+ body.studio-theme-dark .title-block p {
1479
+ color: #8f9aab !important;
1480
+ }
1481
+
1482
+ html.studio-theme-dark body .action-btn,
1483
+ body.studio-theme-dark .action-btn,
1484
+ html.studio-theme-dark body .thread-item,
1485
+ body.studio-theme-dark .thread-item {
1486
+ background: #111722 !important;
1487
+ border-color: #334155 !important;
1488
+ color: #d8dee9 !important;
1489
+ }
1490
+
1491
+ html.studio-theme-dark body .action-btn:hover,
1492
+ body.studio-theme-dark .action-btn:hover,
1493
+ html.studio-theme-dark body .thread-item:hover,
1494
+ body.studio-theme-dark .thread-item:hover {
1495
+ background: #1d2533 !important;
1496
+ color: #e5e9f0 !important;
1497
+ }
1498
+
1499
+ html.studio-theme-dark body .bubble.user,
1500
+ body.studio-theme-dark .bubble.user {
1501
+ background: #2f3b52 !important;
1502
+ color: #f1f5f9 !important;
1503
+ border: 1px solid #435169 !important;
1504
+ box-shadow: 0 10px 24px rgba(0,0,0,.18) !important;
1505
+ }
1506
+
1507
+ html.studio-theme-dark body .bubble.assistant,
1508
+ body.studio-theme-dark .bubble.assistant {
1509
+ background: #171d29 !important;
1510
+ color: #e5e9f0 !important;
1511
+ border-color: #2a3444 !important;
1512
+ box-shadow: 0 8px 20px rgba(0,0,0,.14) !important;
1513
+ }
1514
+
1515
+ html.studio-theme-dark body .bubble.assistant.streaming::after,
1516
+ body.studio-theme-dark .bubble.assistant.streaming::after {
1517
+ background: #d8dee9 !important;
1518
+ }
1519
+
1520
+ html.studio-theme-dark body .bubble img.generated,
1521
+ body.studio-theme-dark .bubble img.generated {
1522
+ border-color: #334155 !important;
1523
+ }
1524
+
1525
+ /* Dark-card clipping: keep inner fills from poking through rounded top corners. */
1526
+ html.studio-theme-dark body .console-card,
1527
+ html.studio-theme-dark body .nano-input,
1528
+ html.studio-theme-dark body .upload-item,
1529
+ html.studio-theme-dark body .result-frame,
1530
+ html.studio-theme-dark body .masonry-item,
1531
+ html.studio-theme-dark body .engine-panel,
1532
+ html.studio-theme-dark body .tool-panel,
1533
+ html.studio-theme-dark body .control-panel,
1534
+ html.studio-theme-dark body .composer,
1535
+ html.studio-theme-dark body .history-popover,
1536
+ html.studio-theme-dark body .model-panel,
1537
+ html.studio-theme-dark body .gate-panel,
1538
+ body.studio-theme-dark .console-card,
1539
+ body.studio-theme-dark .nano-input,
1540
+ body.studio-theme-dark .upload-item,
1541
+ body.studio-theme-dark .result-frame,
1542
+ body.studio-theme-dark .masonry-item,
1543
+ body.studio-theme-dark .engine-panel,
1544
+ body.studio-theme-dark .tool-panel,
1545
+ body.studio-theme-dark .control-panel,
1546
+ body.studio-theme-dark .composer,
1547
+ body.studio-theme-dark .history-popover,
1548
+ body.studio-theme-dark .model-panel,
1549
+ body.studio-theme-dark .gate-panel {
1550
+ overflow: hidden !important;
1551
+ background-clip: padding-box !important;
1552
+ }
1553
+
1554
+ html.studio-theme-dark body .shell .node,
1555
+ body.studio-theme-dark .shell .node {
1556
+ background-clip: padding-box !important;
1557
+ }
1558
+
1559
+ html.studio-theme-dark body .shell .node-head,
1560
+ body.studio-theme-dark .shell .node-head {
1561
+ border-radius: 21px 21px 0 0 !important;
1562
+ overflow: hidden !important;
1563
+ background-clip: padding-box !important;
1564
+ }
1565
+
1566
+ /* Switch contrast: keep iOS toggles visible on blue-gray panels. */
1567
+ html.studio-theme-dark body .ios-switch .ios-slider,
1568
+ body.studio-theme-dark .ios-switch .ios-slider {
1569
+ background: #253044 !important;
1570
+ border: 1px solid #46556c !important;
1571
+ box-shadow: inset 0 1px 2px rgba(0,0,0,.28), 0 0 0 1px rgba(216,222,233,.05) !important;
1572
+ }
1573
+
1574
+ html.studio-theme-dark body .ios-switch .ios-slider:before,
1575
+ body.studio-theme-dark .ios-switch .ios-slider:before {
1576
+ background: #f8fafc !important;
1577
+ box-shadow: 0 3px 9px rgba(0,0,0,.32) !important;
1578
+ }
1579
+
1580
+ html.studio-theme-dark body .ios-switch input:checked + .ios-slider,
1581
+ body.studio-theme-dark .ios-switch input:checked + .ios-slider {
1582
+ background: #d8dee9 !important;
1583
+ border-color: #d8dee9 !important;
1584
+ box-shadow: 0 0 0 3px rgba(216,222,233,.12) !important;
1585
+ }
1586
+
1587
+ html.studio-theme-dark body .ios-switch input:checked + .ios-slider:before,
1588
+ body.studio-theme-dark .ios-switch input:checked + .ios-slider:before {
1589
+ background: #10141d !important;
1590
+ box-shadow: 0 2px 7px rgba(16,20,29,.22) !important;
1591
+ }
26-5-10-API-Studio/static/theme.js ADDED
@@ -0,0 +1,41 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ (function(){
2
+ const KEY = 'studio_theme';
3
+ const LEGACY_KEY = 'canvas_theme';
4
+
5
+ function currentTheme(){
6
+ return localStorage.getItem(KEY) || localStorage.getItem(LEGACY_KEY) || 'light';
7
+ }
8
+
9
+ function applyTheme(theme){
10
+ const next = theme === 'dark' ? 'dark' : 'light';
11
+ const dark = next === 'dark';
12
+ document.documentElement.classList.toggle('studio-theme-dark', dark);
13
+ if(document.body){
14
+ document.body.classList.toggle('studio-theme-dark', dark);
15
+ document.body.classList.toggle('theme-dark', dark);
16
+ }
17
+ window.dispatchEvent(new CustomEvent('studio-theme-change', { detail: { theme: next } }));
18
+ }
19
+
20
+ window.StudioTheme = {
21
+ key: KEY,
22
+ get: currentTheme,
23
+ apply: applyTheme,
24
+ set(theme){
25
+ const next = theme === 'dark' ? 'dark' : 'light';
26
+ localStorage.setItem(KEY, next);
27
+ localStorage.setItem(LEGACY_KEY, next);
28
+ applyTheme(next);
29
+ }
30
+ };
31
+
32
+ applyTheme(currentTheme());
33
+
34
+ document.addEventListener('DOMContentLoaded', () => applyTheme(currentTheme()));
35
+ window.addEventListener('message', event => {
36
+ if(event.data?.type === 'studio-theme') applyTheme(event.data.theme);
37
+ });
38
+ window.addEventListener('storage', event => {
39
+ if(event.key === KEY || event.key === LEGACY_KEY) applyTheme(currentTheme());
40
+ });
41
+ })();
26-5-10-API-Studio/static/zimage.html ADDED
@@ -0,0 +1,586 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <!DOCTYPE html>
2
+ <html lang="zh-CN">
3
+
4
+ <head>
5
+ <meta charset="UTF-8">
6
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
7
+ <link rel="icon" href="/static/logo.png" type="image/png">
8
+ <title>Flux Modern Gallery | Unified Console</title>
9
+ <script src="https://cdn.tailwindcss.com"></script>
10
+ <script src="https://unpkg.com/lucide@latest"></script>
11
+ <script src="/static/theme.js?v=20260509"></script>
12
+ <style>
13
+ :root {
14
+ --bg-base: #f8f8f8;
15
+ --text-main: #1a1a1a;
16
+ --max-w: 1280px;
17
+ --easing: cubic-bezier(0.4, 0, 0.2, 1);
18
+ --accent: #000000;
19
+ }
20
+
21
+ /* --- 极简悬浮浅灰滚动条 (无底色/左移) --- */
22
+ *::-webkit-scrollbar {
23
+ width: 10px !important;
24
+ height: 10px !important;
25
+ background: transparent !important;
26
+ }
27
+
28
+ *::-webkit-scrollbar-track {
29
+ background: transparent !important;
30
+ border: none !important;
31
+ }
32
+
33
+ *::-webkit-scrollbar-thumb {
34
+ background-color: #d8d8d8 !important;
35
+ border: 3px solid transparent !important;
36
+ border-right-width: 5px !important;
37
+ /* 增加右侧间距,使滚动条向左位移 */
38
+ background-clip: padding-box !important;
39
+ border-radius: 10px !important;
40
+ }
41
+
42
+ *::-webkit-scrollbar-thumb:hover {
43
+ background-color: #c0c0c0 !important;
44
+ }
45
+
46
+ *::-webkit-scrollbar-corner {
47
+ background: transparent !important;
48
+ }
49
+
50
+ * {
51
+ scrollbar-width: thin !important;
52
+ scrollbar-color: #d8d8d8 transparent !important;
53
+ }
54
+
55
+ body {
56
+ background-color: var(--bg-base);
57
+ font-family: "Inter", -apple-system, "PingFang SC", sans-serif;
58
+ color: var(--text-main);
59
+ -webkit-font-smoothing: antialiased;
60
+ }
61
+
62
+ .layout-container {
63
+ max-width: var(--max-w);
64
+ margin: 0 auto;
65
+ padding: 0 40px;
66
+ }
67
+
68
+ .console-card {
69
+ background: #ffffff;
70
+ border: 1px solid rgba(0, 0, 0, 0.08);
71
+ box-shadow: 0 2px 10px rgba(0, 0, 0, 0.02);
72
+ }
73
+
74
+ /* 复合切换组件样式 */
75
+ .mode-switcher {
76
+ position: relative;
77
+ background: #f1f1f1;
78
+ padding: 4px;
79
+ border-radius: 14px;
80
+ display: flex;
81
+ width: 260px;
82
+ }
83
+
84
+ .mode-btn {
85
+ position: relative;
86
+ z-index: 10;
87
+ flex: 1;
88
+ padding: 8px 0;
89
+ text-align: center;
90
+ font-size: 11px;
91
+ font-weight: 800;
92
+ text-transform: uppercase;
93
+ color: #999;
94
+ transition: color 0.3s ease;
95
+ cursor: pointer;
96
+ }
97
+
98
+ .mode-btn.active {
99
+ color: #000;
100
+ }
101
+
102
+ .mode-glider {
103
+ position: absolute;
104
+ height: calc(100% - 8px);
105
+ width: calc(50% - 4px);
106
+ background: #fff;
107
+ border-radius: 11px;
108
+ box-shadow: 0 2px 5px rgba(0, 0, 0, 0.05);
109
+ transition: transform 0.3s var(--easing);
110
+ z-index: 1;
111
+ }
112
+
113
+ .masonry-grid {
114
+ display: grid;
115
+ grid-template-columns: repeat(2, 1fr);
116
+ gap: 20px;
117
+ }
118
+
119
+ @media (min-width: 768px) {
120
+ .masonry-grid {
121
+ grid-template-columns: repeat(4, 1fr);
122
+ }
123
+ }
124
+
125
+ .masonry-item {
126
+ aspect-ratio: 1 / 1;
127
+ border-radius: 24px;
128
+ overflow: hidden;
129
+ background: #eee;
130
+ border: 1px solid #f1f5f9;
131
+ transition: all 0.5s var(--easing);
132
+ position: relative;
133
+ }
134
+
135
+ .masonry-item:hover {
136
+ transform: translateY(-6px);
137
+ box-shadow: 0 20px 40px rgba(0, 0, 0, 0.08);
138
+ }
139
+
140
+ .gallery-lightbox {
141
+ background: rgba(255, 255, 255, 0.99);
142
+ }
143
+
144
+ .btn-render {
145
+ background: #000;
146
+ color: #fff;
147
+ transition: all 0.3s ease;
148
+ }
149
+
150
+ .btn-render:hover {
151
+ transform: translateY(-1px);
152
+ background: #222;
153
+ }
154
+
155
+ .btn-render:disabled {
156
+ background: #ccc;
157
+ cursor: not-allowed;
158
+ }
159
+
160
+ input::-webkit-inner-spin-button {
161
+ display: none;
162
+ }
163
+ </style>
164
+ <link rel="stylesheet" href="/static/theme.css?v=20260510-studio-pages-blue-dark10">
165
+ </head>
166
+
167
+ <body class="antialiased">
168
+
169
+ <header class="pt-20 pb-12">
170
+ <div class="layout-container">
171
+ <div class="console-card rounded-3xl p-1.5">
172
+ <div class="bg-gray-50/50 rounded-[22px] p-8">
173
+ <div class="flex justify-between items-center mb-6">
174
+ <span class="text-[10px] font-bold text-gray-400 uppercase tracking-widest">Unified Art
175
+ Console</span>
176
+ <div id="statusDot" class="flex items-center gap-2">
177
+ <span id="statusText" class="text-[9px] font-bold text-gray-500 uppercase">System
178
+ Ready</span>
179
+ <div id="dotColor" class="w-1.5 h-1.5 bg-black rounded-full"></div>
180
+ </div>
181
+ </div>
182
+ <textarea id="prompt" rows="2"
183
+ class="w-full bg-transparent text-2xl font-medium outline-none placeholder:text-gray-200 text-black resize-none leading-relaxed"
184
+ placeholder="Describe your vision..."></textarea>
185
+ </div>
186
+
187
+ <div class="flex flex-col md:flex-row items-center justify-between p-4 px-6 gap-6">
188
+ <div class="flex items-center gap-8">
189
+ <div class="flex flex-col">
190
+ <span class="text-[9px] font-bold text-gray-400 uppercase mb-1">Engine Source</span>
191
+ <div class="mode-switcher">
192
+ <div id="modeLocal" class="mode-btn active flex items-center justify-center gap-1.5"
193
+ onclick="switchEngine('local')">
194
+ <i data-lucide="monitor" class="w-3 h-3"></i>
195
+ <span>Local</span>
196
+ </div>
197
+ <div id="modeCloud" class="mode-btn flex items-center justify-center gap-1.5"
198
+ onclick="switchEngine('cloud')">
199
+ <i data-lucide="cloud" class="w-3 h-3"></i>
200
+ <span>ModelScope</span>
201
+ </div>
202
+ <div id="glider" class="mode-glider"></div>
203
+ </div>
204
+ </div>
205
+
206
+ <div class="h-8 w-px bg-gray-100"></div>
207
+
208
+ <div class="flex flex-col">
209
+ <span class="text-[9px] font-bold text-gray-400 uppercase mb-1">Dimensions</span>
210
+ <div class="flex items-center gap-2 text-xs font-bold">
211
+ <input id="width" type="number" value="1024"
212
+ class="w-10 bg-transparent outline-none border-b border-transparent focus:border-black">
213
+ <span class="text-gray-200">×</span>
214
+ <input id="height" type="number" value="1024"
215
+ class="w-10 bg-transparent outline-none border-b border-transparent focus:border-black">
216
+ </div>
217
+ </div>
218
+
219
+
220
+ </div>
221
+
222
+ <button id="mainGenBtn" onclick="handleRender()"
223
+ class="w-full md:w-56 h-12 btn-render rounded-xl font-bold text-[11px] uppercase flex items-center justify-center gap-3">
224
+ <i data-lucide="zap" id="btnIcon" class="w-4 h-4 text-yellow-400"></i>
225
+ <span id="btnText">Render Art</span>
226
+ </button>
227
+ </div>
228
+ </div>
229
+ </div>
230
+ </header>
231
+
232
+ <main class="pb-24">
233
+ <div class="layout-container">
234
+ <div id="masonry" class="masonry-grid"></div>
235
+ <div id="loadMoreTrigger"
236
+ class="py-12 text-center text-gray-300 text-[10px] font-bold uppercase tracking-widest cursor-pointer hidden">
237
+ Load More Archive
238
+ </div>
239
+ </div>
240
+ </main>
241
+
242
+ <div id="lightbox" onclick="handleOutsideClick(event)"
243
+ class="hidden fixed inset-0 z-50 gallery-lightbox flex flex-col items-center justify-center p-8">
244
+ <button onclick="closeLightbox()" class="absolute top-10 right-10 text-gray-400 hover:text-black"><i
245
+ data-lucide="x" class="w-8 h-8"></i></button>
246
+ <div class="max-w-5xl w-full flex flex-col items-center pointer-events-none">
247
+ <div class="relative pointer-events-auto">
248
+ <img id="lightboxImg" src="" class="max-h-[60vh] rounded-lg shadow-xl">
249
+ <div
250
+ class="absolute top-6 left-6 bg-black/50 backdrop-blur-md text-white px-3 py-1.5 rounded-xl text-[10px] font-black tracking-widest shadow-2xl">
251
+ <span id="lightboxRes">0x0</span>
252
+ </div>
253
+ <button onclick="downloadImage()"
254
+ class="absolute top-6 right-6 bg-black text-white w-12 h-12 rounded-2xl flex items-center justify-center shadow-2xl hover:scale-105 transition-transform">
255
+ <i data-lucide="download" class="w-5 h-5"></i>
256
+ </button>
257
+ </div>
258
+ <div id="lightboxCard"
259
+ class="w-full mt-16 pointer-events-auto bg-white border border-gray-100 rounded-[2rem] p-8 shadow-sm flex justify-between items-center gap-8">
260
+ <div class="flex-1">
261
+ <span class="text-[9px] font-black text-gray-300 uppercase tracking-widest block mb-2">Prompt
262
+ Execution</span>
263
+ <p id="lightboxPrompt" class="text-gray-700 text-sm leading-relaxed max-h-32 overflow-y-auto pr-2">
264
+ </p>
265
+ </div>
266
+ <button onclick="applySameStyle()"
267
+ class="bg-black text-white px-8 py-3.5 rounded-2xl text-[10px] font-black uppercase tracking-widest flex items-center gap-2">
268
+ <i data-lucide="copy" class="w-3 h-3"></i>
269
+ <span>Replicate</span>
270
+ </button>
271
+ </div>
272
+ </div>
273
+ </div>
274
+
275
+ <script>
276
+ lucide.createIcons();
277
+
278
+ function generateUUID() {
279
+ if (typeof crypto !== 'undefined' && crypto.randomUUID) {
280
+ try { return crypto.randomUUID(); } catch (e) { }
281
+ }
282
+ return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function (c) {
283
+ var r = Math.random() * 16 | 0, v = c == 'x' ? r : (r & 0x3 | 0x8);
284
+ return v.toString(16);
285
+ });
286
+ }
287
+ const CLIENT_ID = localStorage.getItem("client_id") || generateUUID();
288
+ localStorage.setItem("client_id", CLIENT_ID);
289
+
290
+ let allHistory = [];
291
+ let currentIndex = 0;
292
+ const PAGE_SIZE = 15;
293
+ let isLoading = false;
294
+
295
+ // WebSocket
296
+ const socket = new WebSocket(`${window.location.protocol === 'https:' ? 'wss:' : 'ws:'}//${window.location.host}/ws/stats`);
297
+ socket.onmessage = (e) => {
298
+ try {
299
+ const msg = JSON.parse(e.data);
300
+ if (msg.type === 'new_image' && msg.data?.type === 'zimage') {
301
+ if (!document.getElementById(`history-${msg.data.timestamp}`)) {
302
+ allHistory.unshift(msg.data);
303
+ renderImageCard(msg.data, true);
304
+ currentIndex++;
305
+ }
306
+ }
307
+ } catch (err) { }
308
+ };
309
+
310
+ let currentEngine = 'local';
311
+ const MS_TOKEN_KEY = 'modelscope_api_token';
312
+ const ENGINE_MODE_KEY = 'zimage_engine_mode';
313
+
314
+ // 1. Token handled via index.html sidebar
315
+
316
+ // 2. 切换引擎逻辑
317
+ function switchEngine(mode) {
318
+ currentEngine = mode;
319
+ localStorage.setItem(ENGINE_MODE_KEY, mode); // Save state
320
+
321
+ const glider = document.getElementById('glider');
322
+ const localBtn = document.getElementById('modeLocal');
323
+ const cloudBtn = document.getElementById('modeCloud');
324
+ const btnText = document.getElementById('btnText');
325
+
326
+ if (mode === 'local') {
327
+ glider.style.transform = 'translateX(0)';
328
+ localBtn.classList.add('active');
329
+ cloudBtn.classList.remove('active');
330
+ btnText.innerText = 'Render Art (Local)';
331
+ } else {
332
+ glider.style.transform = 'translateX(100%)';
333
+ cloudBtn.classList.add('active');
334
+ localBtn.classList.remove('active');
335
+ btnText.innerText = 'Render Art (Cloud)';
336
+ }
337
+ }
338
+
339
+ // Initialize Engine Mode
340
+ const savedEngine = localStorage.getItem(ENGINE_MODE_KEY);
341
+ if (savedEngine && savedEngine !== 'local') {
342
+ switchEngine(savedEngine);
343
+ }
344
+
345
+ // 3. 统一渲染入口
346
+ async function handleRender() {
347
+ const prompt = document.getElementById('prompt').value.trim();
348
+ if (!prompt) return alert("Please enter a prompt");
349
+
350
+ if (currentEngine === 'local') {
351
+ runLocalTask(prompt);
352
+ } else {
353
+ runCloudTask(prompt);
354
+ }
355
+ }
356
+
357
+ // 4. ModelScope 云端逻辑
358
+ async function runCloudTask(prompt) {
359
+ let apiKey = localStorage.getItem(MS_TOKEN_KEY);
360
+ if (!apiKey) {
361
+ try {
362
+ const res = await fetch('/api/config/token');
363
+ const data = await res.json();
364
+ if (data.token) apiKey = data.token;
365
+ } catch (e) { }
366
+ }
367
+ if (!apiKey) return alert("ModelScope Token Required. Please set it in the sidebar (API Token).");
368
+
369
+ const btn = document.getElementById('mainGenBtn');
370
+ setLoading(true);
371
+
372
+ const placeholder = createPlaceholder("ModelScope Rendering");
373
+ document.getElementById('masonry').prepend(placeholder);
374
+
375
+ try {
376
+ const res = await fetch('/generate', {
377
+ method: 'POST',
378
+ headers: { 'Content-Type': 'application/json' },
379
+ body: JSON.stringify({
380
+ prompt: prompt,
381
+ api_key: apiKey,
382
+ resolution: `${document.getElementById('width').value}x${document.getElementById('height').value}`
383
+ })
384
+ });
385
+ const data = await res.json();
386
+ placeholder.remove();
387
+ if (res.ok && data.url) {
388
+ renderImageCard({ timestamp: Date.now(), prompt, images: [data.url], type: 'cloud' }, true);
389
+ } else {
390
+ throw new Error(data.detail?.errors?.message || data.detail || "Cloud Error");
391
+ }
392
+ } catch (e) {
393
+ placeholder.remove();
394
+ alert(e.message);
395
+ } finally {
396
+ setLoading(false);
397
+ }
398
+ }
399
+
400
+ // 5. 本地任务逻辑
401
+ async function runLocalTask(prompt) {
402
+ setLoading(true);
403
+ const placeholder = createPlaceholder("Local Rendering");
404
+ document.getElementById('masonry').prepend(placeholder);
405
+
406
+ try {
407
+ const res = await fetch('/api/generate', {
408
+ method: 'POST',
409
+ headers: { 'Content-Type': 'application/json' },
410
+ body: JSON.stringify({
411
+ prompt,
412
+ width: parseInt(document.getElementById('width').value),
413
+ height: parseInt(document.getElementById('height').value),
414
+ type: "zimage",
415
+ client_id: CLIENT_ID
416
+ })
417
+ });
418
+ const data = await res.json();
419
+ placeholder.remove();
420
+ if (data.images?.length > 0) renderImageCard(data, true);
421
+ } catch (e) {
422
+ placeholder.remove();
423
+ alert("Local render failed");
424
+ } finally {
425
+ setLoading(false);
426
+ }
427
+ }
428
+
429
+ // 工具函数
430
+ function setLoading(isLoading) {
431
+ const btn = document.getElementById('mainGenBtn');
432
+ btn.disabled = isLoading;
433
+
434
+ if (isLoading) {
435
+ // Active state: Dark gray bg, filled yellow pulsing icon
436
+ btn.style.backgroundColor = '#333';
437
+ btn.innerHTML = `<i data-lucide="zap" class="w-4 h-4 text-yellow-400 fill-yellow-400 animate-pulse"></i><span>Processing...</span>`;
438
+ } else {
439
+ // Reset state: Original bg (via CSS), outline yellow icon
440
+ btn.style.backgroundColor = '';
441
+ btn.innerHTML = `<i data-lucide="zap" class="w-4 h-4 text-yellow-400"></i><span id="btnText">Render Art (${currentEngine.toUpperCase()})</span>`;
442
+ }
443
+ lucide.createIcons();
444
+ }
445
+
446
+ function createPlaceholder(text) {
447
+ const div = document.createElement('div');
448
+ div.className = 'masonry-item bg-gray-50 flex flex-col items-center justify-center border-dashed border-2 border-gray-200';
449
+ div.innerHTML = `<div class="w-6 h-6 border-2 border-gray-100 border-t-black rounded-full animate-spin mb-3"></div><span class="text-[8px] font-bold text-gray-400 uppercase tracking-tighter">${text}</span>`;
450
+ return div;
451
+ }
452
+
453
+ function renderImageCard(data, isNew = false) {
454
+ if (document.getElementById(`history-${data.timestamp}`)) return;
455
+ const masonry = document.getElementById('masonry');
456
+ const card = document.createElement('div');
457
+ card.id = `history-${data.timestamp}`;
458
+ card.className = 'masonry-item group cursor-zoom-in animate-in fade-in duration-700';
459
+ card.onclick = () => openLightbox(data.images[0], data.prompt);
460
+ card.innerHTML = `
461
+ <img src="${data.images[0]}" class="w-full h-full object-cover" loading="lazy">
462
+ ${data.type === 'cloud' ? '<div class="absolute top-3 left-3 z-10"><img src="/static/modelscope.gif" class="h-4 w-auto object-contain bg-white/90 rounded-full p-0.5 shadow-sm"></div>' : ''}
463
+ <div class="absolute top-4 right-4 opacity-0 group-hover:opacity-100 transition-opacity z-10">
464
+ <button onclick="deleteHistoryItem('${data.timestamp}', event)" class="w-8 h-8 bg-white/90 backdrop-blur text-black hover:bg-black hover:text-white rounded-lg flex items-center justify-center transition-colors">
465
+ <i data-lucide="trash-2" class="w-4 h-4"></i>
466
+ </button>
467
+ </div>
468
+ `;
469
+ isNew ? masonry.prepend(card) : masonry.appendChild(card);
470
+ lucide.createIcons();
471
+ }
472
+
473
+ async function loadHistory(page = 0) {
474
+ if (isLoading) return;
475
+ const trigger = document.getElementById('loadMoreTrigger');
476
+
477
+ try {
478
+ isLoading = true;
479
+ if (page === 0) {
480
+ allHistory = [];
481
+ document.getElementById('masonry').innerHTML = '';
482
+ currentIndex = 0;
483
+
484
+ trigger.classList.remove('hidden');
485
+ trigger.innerText = "Loading Archives...";
486
+
487
+ const res = await fetch('/api/history?type=zimage');
488
+ allHistory = await res.json();
489
+ } else {
490
+ trigger.innerText = "Loading...";
491
+ await new Promise(r => setTimeout(r, 400));
492
+ }
493
+
494
+ const nextData = allHistory.slice(currentIndex, currentIndex + PAGE_SIZE);
495
+ nextData.forEach(item => renderImageCard(item));
496
+ currentIndex += nextData.length;
497
+
498
+ if (currentIndex >= allHistory.length) {
499
+ trigger.classList.add('hidden');
500
+ } else {
501
+ trigger.classList.remove('hidden');
502
+ trigger.innerText = "Load More Archive";
503
+ }
504
+
505
+ } catch (e) {
506
+ console.error(e);
507
+ trigger.innerText = "Error Loading History";
508
+ } finally {
509
+ isLoading = false;
510
+ }
511
+ }
512
+
513
+ async function deleteHistoryItem(timestamp, event) {
514
+ event.stopPropagation();
515
+ const res = await fetch('/api/history/delete', {
516
+ method: 'POST',
517
+ headers: { 'Content-Type': 'application/json' },
518
+ body: JSON.stringify({ timestamp })
519
+ });
520
+ if ((await res.json()).success) document.getElementById(`history-${timestamp}`).remove();
521
+ }
522
+
523
+ const observer = new IntersectionObserver((entries) => {
524
+ if (entries[0].isIntersecting && !isLoading && currentIndex < allHistory.length) {
525
+ loadHistory(1);
526
+ }
527
+ }, { threshold: 0.1 });
528
+
529
+ window.onload = () => {
530
+ loadHistory(0).then(() => {
531
+ const trigger = document.getElementById('loadMoreTrigger');
532
+ if (trigger) {
533
+ observer.observe(trigger);
534
+ trigger.onclick = () => loadHistory(1);
535
+ }
536
+ });
537
+
538
+ setInterval(async () => {
539
+ try {
540
+ const res = await fetch("/api/queue_status?client_id=" + encodeURIComponent(CLIENT_ID));
541
+ const data = await res.json();
542
+ const statusText = document.getElementById("statusText");
543
+ const dotColor = document.getElementById("dotColor");
544
+ if (statusText && dotColor) {
545
+ if (data.total > 0) {
546
+ statusText.innerText = `Queueing ${data.position}/${data.total}`;
547
+ statusText.className = "text-[9px] font-bold text-orange-500 uppercase";
548
+ dotColor.className = "w-1.5 h-1.5 bg-orange-500 rounded-full animate-pulse";
549
+ } else {
550
+ statusText.innerText = "System Ready";
551
+ statusText.className = "text-[9px] font-bold text-gray-500 uppercase";
552
+ dotColor.className = "w-1.5 h-1.5 bg-black rounded-full";
553
+ }
554
+ }
555
+ } catch (e) { }
556
+ }, 3000);
557
+ };
558
+
559
+ // 基础功能
560
+ function openLightbox(url, prompt) {
561
+ const img = document.getElementById('lightboxImg');
562
+ const resDisplay = document.getElementById('lightboxRes');
563
+ resDisplay.innerText = "...";
564
+ img.src = url;
565
+ img.onload = () => {
566
+ resDisplay.innerText = `${img.naturalWidth} x ${img.naturalHeight}`;
567
+ };
568
+ document.getElementById('lightboxPrompt').innerText = prompt;
569
+ document.getElementById('lightbox').classList.remove('hidden');
570
+ document.body.style.overflow = 'hidden';
571
+ }
572
+ function closeLightbox() { document.getElementById('lightbox').classList.add('hidden'); document.body.style.overflow = 'auto'; }
573
+ function handleOutsideClick(e) { if (e.target.id === 'lightbox') closeLightbox(); }
574
+ function downloadImage() {
575
+ const a = document.createElement('a'); a.href = document.getElementById('lightboxImg').src;
576
+ a.download = `Art-${Date.now()}.png`; a.click();
577
+ }
578
+ function applySameStyle() {
579
+ document.getElementById('prompt').value = document.getElementById('lightboxPrompt').innerText;
580
+ closeLightbox();
581
+ window.scrollTo({ top: 0, behavior: 'smooth' });
582
+ }
583
+ </script>
584
+ </body>
585
+
586
+ </html>
26-5-10-API-Studio/workflows/2511.json ADDED
@@ -0,0 +1,264 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "1": {
3
+ "inputs": {
4
+ "strength": 1,
5
+ "model": [
6
+ "2",
7
+ 0
8
+ ]
9
+ },
10
+ "class_type": "CFGNorm",
11
+ "_meta": {
12
+ "title": "CFG归一化"
13
+ }
14
+ },
15
+ "2": {
16
+ "inputs": {
17
+ "shift": 3,
18
+ "model": [
19
+ "20",
20
+ 0
21
+ ]
22
+ },
23
+ "class_type": "ModelSamplingAuraFlow",
24
+ "_meta": {
25
+ "title": "模型采样算法AuraFlow"
26
+ }
27
+ },
28
+ "3": {
29
+ "inputs": {
30
+ "prompt": "",
31
+ "speak_and_recognation": {
32
+ "__value__": [
33
+ false,
34
+ true
35
+ ]
36
+ },
37
+ "clip": [
38
+ "87",
39
+ 0
40
+ ],
41
+ "vae": [
42
+ "22",
43
+ 0
44
+ ],
45
+ "image1": [
46
+ "39",
47
+ 0
48
+ ]
49
+ },
50
+ "class_type": "TextEncodeQwenImageEditPlus",
51
+ "_meta": {
52
+ "title": "文本编码(QwenImageEditPlus)"
53
+ }
54
+ },
55
+ "10": {
56
+ "inputs": {
57
+ "pixels": [
58
+ "39",
59
+ 0
60
+ ],
61
+ "vae": [
62
+ "22",
63
+ 0
64
+ ]
65
+ },
66
+ "class_type": "VAEEncode",
67
+ "_meta": {
68
+ "title": "VAE编码"
69
+ }
70
+ },
71
+ "11": {
72
+ "inputs": {
73
+ "prompt": "将摄像机向右旋转45度",
74
+ "speak_and_recognation": {
75
+ "__value__": [
76
+ false,
77
+ true
78
+ ]
79
+ },
80
+ "clip": [
81
+ "87",
82
+ 0
83
+ ],
84
+ "vae": [
85
+ "22",
86
+ 0
87
+ ],
88
+ "image1": [
89
+ "39",
90
+ 0
91
+ ]
92
+ },
93
+ "class_type": "TextEncodeQwenImageEditPlus",
94
+ "_meta": {
95
+ "title": "文本编码(QwenImageEditPlus)"
96
+ }
97
+ },
98
+ "12": {
99
+ "inputs": {
100
+ "samples": [
101
+ "14",
102
+ 0
103
+ ],
104
+ "vae": [
105
+ "22",
106
+ 0
107
+ ]
108
+ },
109
+ "class_type": "VAEDecode",
110
+ "_meta": {
111
+ "title": "VAE解码"
112
+ }
113
+ },
114
+ "14": {
115
+ "inputs": {
116
+ "seed": 691951626916858,
117
+ "steps": 8,
118
+ "cfg": 1,
119
+ "sampler_name": "euler",
120
+ "scheduler": "simple",
121
+ "denoise": 1,
122
+ "model": [
123
+ "1",
124
+ 0
125
+ ],
126
+ "positive": [
127
+ "84",
128
+ 0
129
+ ],
130
+ "negative": [
131
+ "85",
132
+ 0
133
+ ],
134
+ "latent_image": [
135
+ "10",
136
+ 0
137
+ ]
138
+ },
139
+ "class_type": "KSampler",
140
+ "_meta": {
141
+ "title": "K采样器"
142
+ }
143
+ },
144
+ "20": {
145
+ "inputs": {
146
+ "lora_name": "qwen\\Qwen-Image-Edit-2511-Lightning-4steps-V1.0-bf16.safetensors",
147
+ "strength_model": 1,
148
+ "model": [
149
+ "76",
150
+ 0
151
+ ]
152
+ },
153
+ "class_type": "LoraLoaderModelOnly",
154
+ "_meta": {
155
+ "title": "LoRA加载器(仅模型)"
156
+ }
157
+ },
158
+ "22": {
159
+ "inputs": {
160
+ "vae_name": "qwen_image_vae.safetensors"
161
+ },
162
+ "class_type": "VAELoader",
163
+ "_meta": {
164
+ "title": "VAE加载器"
165
+ }
166
+ },
167
+ "31": {
168
+ "inputs": {
169
+ "image": "10_start_1.jpg"
170
+ },
171
+ "class_type": "LoadImage",
172
+ "_meta": {
173
+ "title": "加载图像"
174
+ }
175
+ },
176
+ "39": {
177
+ "inputs": {
178
+ "upscale_method": "lanczos",
179
+ "megapixels": 1,
180
+ "resolution_steps": 1,
181
+ "image": [
182
+ "31",
183
+ 0
184
+ ]
185
+ },
186
+ "class_type": "ImageScaleToTotalPixels",
187
+ "_meta": {
188
+ "title": "图像按像素缩放"
189
+ }
190
+ },
191
+ "45": {
192
+ "inputs": {
193
+ "images": [
194
+ "12",
195
+ 0
196
+ ]
197
+ },
198
+ "class_type": "PreviewImage",
199
+ "_meta": {
200
+ "title": "预览图像"
201
+ }
202
+ },
203
+ "76": {
204
+ "inputs": {
205
+ "lora_name": "qwen\\qwen-image-edit-2511-multiple-angles-lora.safetensors",
206
+ "strength_model": 1,
207
+ "model": [
208
+ "86",
209
+ 0
210
+ ]
211
+ },
212
+ "class_type": "LoraLoaderModelOnly",
213
+ "_meta": {
214
+ "title": "LoRA加载器(仅模型)"
215
+ }
216
+ },
217
+ "84": {
218
+ "inputs": {
219
+ "reference_latents_method": "index_timestep_zero",
220
+ "conditioning": [
221
+ "11",
222
+ 0
223
+ ]
224
+ },
225
+ "class_type": "FluxKontextMultiReferenceLatentMethod",
226
+ "_meta": {
227
+ "title": "FluxKontext多参考潜在方法"
228
+ }
229
+ },
230
+ "85": {
231
+ "inputs": {
232
+ "reference_latents_method": "index_timestep_zero",
233
+ "conditioning": [
234
+ "3",
235
+ 0
236
+ ]
237
+ },
238
+ "class_type": "FluxKontextMultiReferenceLatentMethod",
239
+ "_meta": {
240
+ "title": "FluxKontext多参考潜在方法"
241
+ }
242
+ },
243
+ "86": {
244
+ "inputs": {
245
+ "unet_name": "qwen_image_edit_2511_fp8_e4m3fn.safetensors",
246
+ "weight_dtype": "default"
247
+ },
248
+ "class_type": "UNETLoader",
249
+ "_meta": {
250
+ "title": "UNET加载器"
251
+ }
252
+ },
253
+ "87": {
254
+ "inputs": {
255
+ "clip_name": "qwen_2.5_vl_7b_fp8_scaled.safetensors",
256
+ "type": "qwen_image",
257
+ "device": "default"
258
+ },
259
+ "class_type": "CLIPLoader",
260
+ "_meta": {
261
+ "title": "CLIP加载器"
262
+ }
263
+ }
264
+ }
26-5-10-API-Studio/workflows/Flux2-Klein.json ADDED
@@ -0,0 +1,525 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "151": {
3
+ "inputs": {
4
+ "sampler_name": "euler"
5
+ },
6
+ "class_type": "KSamplerSelect",
7
+ "_meta": {
8
+ "title": "K采样器选择"
9
+ }
10
+ },
11
+ "152": {
12
+ "inputs": {
13
+ "steps": 4,
14
+ "width": [
15
+ "157",
16
+ 1
17
+ ],
18
+ "height": [
19
+ "157",
20
+ 1
21
+ ]
22
+ },
23
+ "class_type": "Flux2Scheduler",
24
+ "_meta": {
25
+ "title": "Flux2Scheduler"
26
+ }
27
+ },
28
+ "153": {
29
+ "inputs": {
30
+ "cfg": 1,
31
+ "model": [
32
+ "296",
33
+ 0
34
+ ],
35
+ "positive": [
36
+ "307",
37
+ 0
38
+ ],
39
+ "negative": [
40
+ "308",
41
+ 0
42
+ ]
43
+ },
44
+ "class_type": "CFGGuider",
45
+ "_meta": {
46
+ "title": "CFG引导"
47
+ }
48
+ },
49
+ "154": {
50
+ "inputs": {
51
+ "noise": [
52
+ "158",
53
+ 0
54
+ ],
55
+ "guider": [
56
+ "153",
57
+ 0
58
+ ],
59
+ "sampler": [
60
+ "151",
61
+ 0
62
+ ],
63
+ "sigmas": [
64
+ "152",
65
+ 0
66
+ ],
67
+ "latent_image": [
68
+ "156",
69
+ 0
70
+ ]
71
+ },
72
+ "class_type": "SamplerCustomAdvanced",
73
+ "_meta": {
74
+ "title": "自定义采样器(高级)"
75
+ }
76
+ },
77
+ "155": {
78
+ "inputs": {
79
+ "samples": [
80
+ "154",
81
+ 0
82
+ ],
83
+ "vae": [
84
+ "174",
85
+ 0
86
+ ]
87
+ },
88
+ "class_type": "VAEDecode",
89
+ "_meta": {
90
+ "title": "VAE解码"
91
+ }
92
+ },
93
+ "156": {
94
+ "inputs": {
95
+ "width": [
96
+ "157",
97
+ 0
98
+ ],
99
+ "height": [
100
+ "157",
101
+ 1
102
+ ],
103
+ "batch_size": 1
104
+ },
105
+ "class_type": "EmptyFlux2LatentImage",
106
+ "_meta": {
107
+ "title": "Empty Flux 2 Latent"
108
+ }
109
+ },
110
+ "157": {
111
+ "inputs": {
112
+ "image": [
113
+ "291",
114
+ 0
115
+ ]
116
+ },
117
+ "class_type": "GetImageSize",
118
+ "_meta": {
119
+ "title": "获取图像尺寸"
120
+ }
121
+ },
122
+ "158": {
123
+ "inputs": {
124
+ "noise_seed": 1046852288816614
125
+ },
126
+ "class_type": "RandomNoise",
127
+ "_meta": {
128
+ "title": "随机噪波"
129
+ }
130
+ },
131
+ "159": {
132
+ "inputs": {
133
+ "conditioning": [
134
+ "168",
135
+ 0
136
+ ],
137
+ "latent": [
138
+ "162",
139
+ 0
140
+ ]
141
+ },
142
+ "class_type": "ReferenceLatent",
143
+ "_meta": {
144
+ "title": "参考Latent"
145
+ }
146
+ },
147
+ "160": {
148
+ "inputs": {
149
+ "conditioning": [
150
+ "167",
151
+ 0
152
+ ],
153
+ "latent": [
154
+ "162",
155
+ 0
156
+ ]
157
+ },
158
+ "class_type": "ReferenceLatent",
159
+ "_meta": {
160
+ "title": "参考Latent"
161
+ }
162
+ },
163
+ "162": {
164
+ "inputs": {
165
+ "pixels": [
166
+ "291",
167
+ 0
168
+ ],
169
+ "vae": [
170
+ "174",
171
+ 0
172
+ ]
173
+ },
174
+ "class_type": "VAEEncode",
175
+ "_meta": {
176
+ "title": "VAE编码"
177
+ }
178
+ },
179
+ "164": {
180
+ "inputs": {
181
+ "pixels": [
182
+ "271",
183
+ 0
184
+ ],
185
+ "vae": [
186
+ "174",
187
+ 0
188
+ ]
189
+ },
190
+ "class_type": "VAEEncode",
191
+ "_meta": {
192
+ "title": "VAE编码"
193
+ }
194
+ },
195
+ "165": {
196
+ "inputs": {
197
+ "conditioning": [
198
+ "159",
199
+ 0
200
+ ],
201
+ "latent": [
202
+ "164",
203
+ 0
204
+ ]
205
+ },
206
+ "class_type": "ReferenceLatent",
207
+ "_meta": {
208
+ "title": "参考Latent"
209
+ }
210
+ },
211
+ "166": {
212
+ "inputs": {
213
+ "conditioning": [
214
+ "160",
215
+ 0
216
+ ],
217
+ "latent": [
218
+ "164",
219
+ 0
220
+ ]
221
+ },
222
+ "class_type": "ReferenceLatent",
223
+ "_meta": {
224
+ "title": "参考Latent"
225
+ }
226
+ },
227
+ "167": {
228
+ "inputs": {
229
+ "conditioning": [
230
+ "168",
231
+ 0
232
+ ]
233
+ },
234
+ "class_type": "ConditioningZeroOut",
235
+ "_meta": {
236
+ "title": "条件零化"
237
+ }
238
+ },
239
+ "168": {
240
+ "inputs": {
241
+ "text": "改为夜晚",
242
+ "speak_and_recognation": {
243
+ "__value__": [
244
+ false,
245
+ true
246
+ ]
247
+ },
248
+ "clip": [
249
+ "295",
250
+ 0
251
+ ]
252
+ },
253
+ "class_type": "CLIPTextEncode",
254
+ "_meta": {
255
+ "title": "CLIP文本编码器"
256
+ }
257
+ },
258
+ "174": {
259
+ "inputs": {
260
+ "vae_name": "flux2-vae.safetensors"
261
+ },
262
+ "class_type": "VAELoader",
263
+ "_meta": {
264
+ "title": "VAE加载器"
265
+ }
266
+ },
267
+ "178": {
268
+ "inputs": {
269
+ "pixels": [
270
+ "294",
271
+ 0
272
+ ],
273
+ "vae": [
274
+ "174",
275
+ 0
276
+ ]
277
+ },
278
+ "class_type": "VAEEncode",
279
+ "_meta": {
280
+ "title": "VAE编码"
281
+ }
282
+ },
283
+ "179": {
284
+ "inputs": {
285
+ "conditioning": [
286
+ "165",
287
+ 0
288
+ ],
289
+ "latent": [
290
+ "178",
291
+ 0
292
+ ]
293
+ },
294
+ "class_type": "ReferenceLatent",
295
+ "_meta": {
296
+ "title": "参考Latent"
297
+ }
298
+ },
299
+ "180": {
300
+ "inputs": {
301
+ "conditioning": [
302
+ "166",
303
+ 0
304
+ ],
305
+ "latent": [
306
+ "178",
307
+ 0
308
+ ]
309
+ },
310
+ "class_type": "ReferenceLatent",
311
+ "_meta": {
312
+ "title": "参考Latent"
313
+ }
314
+ },
315
+ "270": {
316
+ "inputs": {
317
+ "image": "05-1.jpg"
318
+ },
319
+ "class_type": "LoadImage",
320
+ "_meta": {
321
+ "title": "加载图像"
322
+ }
323
+ },
324
+ "271": {
325
+ "inputs": {
326
+ "upscale_method": "lanczos",
327
+ "megapixels": 1,
328
+ "resolution_steps": 1,
329
+ "image": [
330
+ "270",
331
+ 0
332
+ ]
333
+ },
334
+ "class_type": "ImageScaleToTotalPixels",
335
+ "_meta": {
336
+ "title": "图像按像素缩放"
337
+ }
338
+ },
339
+ "278": {
340
+ "inputs": {
341
+ "image": "1 (4).jpg"
342
+ },
343
+ "class_type": "LoadImage",
344
+ "_meta": {
345
+ "title": "加载图像"
346
+ }
347
+ },
348
+ "291": {
349
+ "inputs": {
350
+ "upscale_method": "lanczos",
351
+ "megapixels": 1,
352
+ "resolution_steps": 1,
353
+ "image": [
354
+ "278",
355
+ 0
356
+ ]
357
+ },
358
+ "class_type": "ImageScaleToTotalPixels",
359
+ "_meta": {
360
+ "title": "图像按像素缩放"
361
+ }
362
+ },
363
+ "292": {
364
+ "inputs": {
365
+ "image": "05-1.jpg"
366
+ },
367
+ "class_type": "LoadImage",
368
+ "_meta": {
369
+ "title": "加载图像"
370
+ }
371
+ },
372
+ "294": {
373
+ "inputs": {
374
+ "upscale_method": "lanczos",
375
+ "megapixels": 1,
376
+ "resolution_steps": 1,
377
+ "image": [
378
+ "292",
379
+ 0
380
+ ]
381
+ },
382
+ "class_type": "ImageScaleToTotalPixels",
383
+ "_meta": {
384
+ "title": "图像按像素缩放"
385
+ }
386
+ },
387
+ "295": {
388
+ "inputs": {
389
+ "model_name1": "qwen_3_8b_fp8mixed.safetensors",
390
+ "model_name2": "None",
391
+ "model_name3": "None",
392
+ "type": "stable_diffusion",
393
+ "key_opt": "",
394
+ "mode": "Auto",
395
+ "device": "default"
396
+ },
397
+ "class_type": "LoadTextEncoderShared //Inspire",
398
+ "_meta": {
399
+ "title": "Shared Text Encoder Loader (Inspire)"
400
+ }
401
+ },
402
+ "296": {
403
+ "inputs": {
404
+ "model_name": "flux-2-klein-9b-fp8.safetensors",
405
+ "weight_dtype": "default",
406
+ "key_opt": "",
407
+ "mode": "Auto"
408
+ },
409
+ "class_type": "LoadDiffusionModelShared //Inspire",
410
+ "_meta": {
411
+ "title": "Shared Diffusion Model Loader (Inspire)"
412
+ }
413
+ },
414
+ "305": {
415
+ "inputs": {
416
+ "switch": [
417
+ "313",
418
+ 0
419
+ ],
420
+ "on_false": [
421
+ "160",
422
+ 0
423
+ ],
424
+ "on_true": [
425
+ "166",
426
+ 0
427
+ ]
428
+ },
429
+ "class_type": "ComfySwitchNode",
430
+ "_meta": {
431
+ "title": "Switch"
432
+ }
433
+ },
434
+ "306": {
435
+ "inputs": {
436
+ "switch": [
437
+ "313",
438
+ 0
439
+ ],
440
+ "on_false": [
441
+ "159",
442
+ 0
443
+ ],
444
+ "on_true": [
445
+ "165",
446
+ 0
447
+ ]
448
+ },
449
+ "class_type": "ComfySwitchNode",
450
+ "_meta": {
451
+ "title": "Switch"
452
+ }
453
+ },
454
+ "307": {
455
+ "inputs": {
456
+ "switch": [
457
+ "314",
458
+ 0
459
+ ],
460
+ "on_false": [
461
+ "306",
462
+ 0
463
+ ],
464
+ "on_true": [
465
+ "179",
466
+ 0
467
+ ]
468
+ },
469
+ "class_type": "ComfySwitchNode",
470
+ "_meta": {
471
+ "title": "Switch"
472
+ }
473
+ },
474
+ "308": {
475
+ "inputs": {
476
+ "switch": [
477
+ "314",
478
+ 0
479
+ ],
480
+ "on_false": [
481
+ "305",
482
+ 0
483
+ ],
484
+ "on_true": [
485
+ "180",
486
+ 0
487
+ ]
488
+ },
489
+ "class_type": "ComfySwitchNode",
490
+ "_meta": {
491
+ "title": "Switch"
492
+ }
493
+ },
494
+ "313": {
495
+ "inputs": {
496
+ "value": false
497
+ },
498
+ "class_type": "PrimitiveBoolean",
499
+ "_meta": {
500
+ "title": "布尔值2"
501
+ }
502
+ },
503
+ "314": {
504
+ "inputs": {
505
+ "value": false
506
+ },
507
+ "class_type": "PrimitiveBoolean",
508
+ "_meta": {
509
+ "title": "布尔值3"
510
+ }
511
+ },
512
+ "315": {
513
+ "inputs": {
514
+ "filename_prefix": "ComfyUI",
515
+ "images": [
516
+ "155",
517
+ 0
518
+ ]
519
+ },
520
+ "class_type": "SaveImage",
521
+ "_meta": {
522
+ "title": "保存图像"
523
+ }
524
+ }
525
+ }
26-5-10-API-Studio/workflows/Z-Image-Enhance.json ADDED
@@ -0,0 +1,383 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "15": {
3
+ "inputs": {
4
+ "image": "pasted/image (794).png"
5
+ },
6
+ "class_type": "LoadImage",
7
+ "_meta": {
8
+ "title": "加载图像"
9
+ }
10
+ },
11
+ "23": {
12
+ "inputs": {
13
+ "text": "丰富的细节",
14
+ "speak_and_recognation": {
15
+ "__value__": [
16
+ false,
17
+ true
18
+ ]
19
+ },
20
+ "clip": [
21
+ "34",
22
+ 0
23
+ ]
24
+ },
25
+ "class_type": "CLIPTextEncode",
26
+ "_meta": {
27
+ "title": "CLIP文本编码器"
28
+ }
29
+ },
30
+ "24": {
31
+ "inputs": {
32
+ "conditioning": [
33
+ "23",
34
+ 0
35
+ ]
36
+ },
37
+ "class_type": "ConditioningZeroOut",
38
+ "_meta": {
39
+ "title": "条件零化"
40
+ }
41
+ },
42
+ "27": {
43
+ "inputs": {
44
+ "vae_name": "ae.safetensors"
45
+ },
46
+ "class_type": "VAELoader",
47
+ "_meta": {
48
+ "title": "VAE加载器"
49
+ }
50
+ },
51
+ "33": {
52
+ "inputs": {
53
+ "model_name": "z_image_turbo_bf16.safetensors",
54
+ "weight_dtype": "default",
55
+ "key_opt": "",
56
+ "mode": "Auto"
57
+ },
58
+ "class_type": "LoadDiffusionModelShared //Inspire",
59
+ "_meta": {
60
+ "title": "Shared Diffusion Model Loader (Inspire)"
61
+ }
62
+ },
63
+ "34": {
64
+ "inputs": {
65
+ "model_name1": "qwen_3_4b.safetensors",
66
+ "model_name2": "None",
67
+ "model_name3": "None",
68
+ "type": "stable_diffusion",
69
+ "key_opt": "",
70
+ "mode": "Auto",
71
+ "device": "default"
72
+ },
73
+ "class_type": "LoadTextEncoderShared //Inspire",
74
+ "_meta": {
75
+ "title": "Shared Text Encoder Loader (Inspire)"
76
+ }
77
+ },
78
+ "142": {
79
+ "inputs": {
80
+ "pixels": [
81
+ "15",
82
+ 0
83
+ ],
84
+ "vae": [
85
+ "27",
86
+ 0
87
+ ]
88
+ },
89
+ "class_type": "VAEEncode",
90
+ "_meta": {
91
+ "title": "VAE编码"
92
+ }
93
+ },
94
+ "146": {
95
+ "inputs": {
96
+ "seed": 279629946795727,
97
+ "steps": 10,
98
+ "cfg": 0.7,
99
+ "sampler_name": "euler",
100
+ "scheduler": "sgm_uniform",
101
+ "denoise": [
102
+ "202",
103
+ 0
104
+ ],
105
+ "model": [
106
+ "166",
107
+ 0
108
+ ],
109
+ "positive": [
110
+ "23",
111
+ 0
112
+ ],
113
+ "negative": [
114
+ "24",
115
+ 0
116
+ ],
117
+ "latent_image": [
118
+ "142",
119
+ 0
120
+ ]
121
+ },
122
+ "class_type": "KSampler",
123
+ "_meta": {
124
+ "title": "K采样器"
125
+ }
126
+ },
127
+ "147": {
128
+ "inputs": {
129
+ "samples": [
130
+ "146",
131
+ 0
132
+ ],
133
+ "vae": [
134
+ "27",
135
+ 0
136
+ ]
137
+ },
138
+ "class_type": "VAEDecode",
139
+ "_meta": {
140
+ "title": "VAE解码"
141
+ }
142
+ },
143
+ "164": {
144
+ "inputs": {
145
+ "preprocessor": "DepthAnythingV2Preprocessor",
146
+ "resolution": 1024,
147
+ "image": [
148
+ "15",
149
+ 0
150
+ ]
151
+ },
152
+ "class_type": "AIO_Preprocessor",
153
+ "_meta": {
154
+ "title": "Aux集成预处理器"
155
+ }
156
+ },
157
+ "165": {
158
+ "inputs": {
159
+ "name": "Z-Image-Turbo-Fun-Controlnet-Union.safetensors"
160
+ },
161
+ "class_type": "ModelPatchLoader",
162
+ "_meta": {
163
+ "title": "加载模型补丁"
164
+ }
165
+ },
166
+ "166": {
167
+ "inputs": {
168
+ "strength": 0.8,
169
+ "model": [
170
+ "33",
171
+ 0
172
+ ],
173
+ "model_patch": [
174
+ "165",
175
+ 0
176
+ ],
177
+ "vae": [
178
+ "27",
179
+ 0
180
+ ],
181
+ "image": [
182
+ "164",
183
+ 0
184
+ ]
185
+ },
186
+ "class_type": "QwenImageDiffsynthControlnet",
187
+ "_meta": {
188
+ "title": "QwenImageDiffsynthControlnet"
189
+ }
190
+ },
191
+ "174": {
192
+ "inputs": {
193
+ "filename_prefix": "ComfyUI",
194
+ "images": [
195
+ "180",
196
+ 0
197
+ ]
198
+ },
199
+ "class_type": "SaveImage",
200
+ "_meta": {
201
+ "title": "保存图像"
202
+ }
203
+ },
204
+ "180": {
205
+ "inputs": {
206
+ "samples": [
207
+ "181",
208
+ 0
209
+ ],
210
+ "vae": [
211
+ "27",
212
+ 0
213
+ ]
214
+ },
215
+ "class_type": "VAEDecode",
216
+ "_meta": {
217
+ "title": "VAE解码"
218
+ }
219
+ },
220
+ "181": {
221
+ "inputs": {
222
+ "seed": 820960346993579,
223
+ "steps": 10,
224
+ "cfg": 1,
225
+ "sampler_name": "euler_cfg_pp",
226
+ "scheduler": "simple",
227
+ "denoise": [
228
+ "202",
229
+ 0
230
+ ],
231
+ "model": [
232
+ "33",
233
+ 0
234
+ ],
235
+ "positive": [
236
+ "23",
237
+ 0
238
+ ],
239
+ "negative": [
240
+ "24",
241
+ 0
242
+ ],
243
+ "latent_image": [
244
+ "186",
245
+ 0
246
+ ]
247
+ },
248
+ "class_type": "KSampler",
249
+ "_meta": {
250
+ "title": "K采样器"
251
+ }
252
+ },
253
+ "184": {
254
+ "inputs": {
255
+ "seed": 882274830236035,
256
+ "strength": [
257
+ "202",
258
+ 0
259
+ ],
260
+ "image": [
261
+ "15",
262
+ 0
263
+ ]
264
+ },
265
+ "class_type": "ImageAddNoise",
266
+ "_meta": {
267
+ "title": "图像添加噪声"
268
+ }
269
+ },
270
+ "186": {
271
+ "inputs": {
272
+ "pixels": [
273
+ "189",
274
+ 0
275
+ ],
276
+ "vae": [
277
+ "27",
278
+ 0
279
+ ]
280
+ },
281
+ "class_type": "VAEEncode",
282
+ "_meta": {
283
+ "title": "VAE编码"
284
+ }
285
+ },
286
+ "189": {
287
+ "inputs": {
288
+ "blend_factor": [
289
+ "202",
290
+ 0
291
+ ],
292
+ "blend_mode": "multiply",
293
+ "image1": [
294
+ "191",
295
+ 0
296
+ ],
297
+ "image2": [
298
+ "184",
299
+ 0
300
+ ]
301
+ },
302
+ "class_type": "ImageBlend",
303
+ "_meta": {
304
+ "title": "图像混合"
305
+ }
306
+ },
307
+ "191": {
308
+ "inputs": {
309
+ "sharpen_radius": 1,
310
+ "sigma": 0.5,
311
+ "alpha": 0.5,
312
+ "image": [
313
+ "147",
314
+ 0
315
+ ]
316
+ },
317
+ "class_type": "ImageSharpen",
318
+ "_meta": {
319
+ "title": "图像锐化"
320
+ }
321
+ },
322
+ "193": {
323
+ "inputs": {
324
+ "image": [
325
+ "15",
326
+ 0
327
+ ]
328
+ },
329
+ "class_type": "GetImageSize+",
330
+ "_meta": {
331
+ "title": "获取图像尺寸"
332
+ }
333
+ },
334
+ "201": {
335
+ "inputs": {
336
+ "expression": "(a+b)*c/10000",
337
+ "speak_and_recognation": {
338
+ "__value__": [
339
+ false,
340
+ true
341
+ ]
342
+ },
343
+ "a": [
344
+ "193",
345
+ 0
346
+ ],
347
+ "b": [
348
+ "193",
349
+ 1
350
+ ],
351
+ "c": [
352
+ "204",
353
+ 0
354
+ ]
355
+ },
356
+ "class_type": "MathExpression|pysssss",
357
+ "_meta": {
358
+ "title": "数学表达式"
359
+ }
360
+ },
361
+ "202": {
362
+ "inputs": {
363
+ "output_type": "float",
364
+ "*": [
365
+ "201",
366
+ 1
367
+ ]
368
+ },
369
+ "class_type": "easy convertAnything",
370
+ "_meta": {
371
+ "title": "转换任何"
372
+ }
373
+ },
374
+ "204": {
375
+ "inputs": {
376
+ "value": 0.5
377
+ },
378
+ "class_type": "FloatConstant",
379
+ "_meta": {
380
+ "title": "浮点常量"
381
+ }
382
+ }
383
+ }
26-5-10-API-Studio/workflows/Z-Image.json ADDED
@@ -0,0 +1,176 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "20": {
3
+ "inputs": {
4
+ "samples": [
5
+ "22",
6
+ 0
7
+ ],
8
+ "vae": [
9
+ "27",
10
+ 0
11
+ ]
12
+ },
13
+ "class_type": "VAEDecode",
14
+ "_meta": {
15
+ "title": "VAE解码"
16
+ }
17
+ },
18
+ "22": {
19
+ "inputs": {
20
+ "seed": 1104391495230198,
21
+ "steps": 10,
22
+ "cfg": 1,
23
+ "sampler_name": "euler",
24
+ "scheduler": "simple",
25
+ "denoise": 1,
26
+ "model": [
27
+ "33",
28
+ 0
29
+ ],
30
+ "positive": [
31
+ "23",
32
+ 0
33
+ ],
34
+ "negative": [
35
+ "24",
36
+ 0
37
+ ],
38
+ "latent_image": [
39
+ "144",
40
+ 0
41
+ ]
42
+ },
43
+ "class_type": "KSampler",
44
+ "_meta": {
45
+ "title": "K采样器"
46
+ }
47
+ },
48
+ "23": {
49
+ "inputs": {
50
+ "text": "",
51
+ "speak_and_recognation": {
52
+ "__value__": [
53
+ false,
54
+ true
55
+ ]
56
+ },
57
+ "clip": [
58
+ "34",
59
+ 0
60
+ ]
61
+ },
62
+ "class_type": "CLIPTextEncode",
63
+ "_meta": {
64
+ "title": "CLIP文本编码器"
65
+ }
66
+ },
67
+ "24": {
68
+ "inputs": {
69
+ "conditioning": [
70
+ "23",
71
+ 0
72
+ ]
73
+ },
74
+ "class_type": "ConditioningZeroOut",
75
+ "_meta": {
76
+ "title": "条件零化"
77
+ }
78
+ },
79
+ "27": {
80
+ "inputs": {
81
+ "vae_name": "flux ultra vae.safetensors"
82
+ },
83
+ "class_type": "VAELoader",
84
+ "_meta": {
85
+ "title": "VAE加载器"
86
+ }
87
+ },
88
+ "33": {
89
+ "inputs": {
90
+ "model_name": "z_image_turbo_bf16.safetensors",
91
+ "weight_dtype": "default",
92
+ "key_opt": "",
93
+ "mode": "Auto"
94
+ },
95
+ "class_type": "LoadDiffusionModelShared //Inspire",
96
+ "_meta": {
97
+ "title": "Shared Diffusion Model Loader (Inspire)"
98
+ }
99
+ },
100
+ "34": {
101
+ "inputs": {
102
+ "model_name1": "qwen_3_4b.safetensors",
103
+ "model_name2": "None",
104
+ "model_name3": "None",
105
+ "type": "stable_diffusion",
106
+ "key_opt": "",
107
+ "mode": "Auto",
108
+ "device": "default"
109
+ },
110
+ "class_type": "LoadTextEncoderShared //Inspire",
111
+ "_meta": {
112
+ "title": "Shared Text Encoder Loader (Inspire)"
113
+ }
114
+ },
115
+ "89": {
116
+ "inputs": {
117
+ "rgthree_comparer": {
118
+ "images": [
119
+ {
120
+ "name": "A",
121
+ "selected": true,
122
+ "url": "/api/view?filename=rgthree.compare._temp_gmjxy_00001_.png&type=temp&subfolder=&rand=0.7230996201608886"
123
+ },
124
+ {
125
+ "name": "B",
126
+ "selected": true,
127
+ "url": "/api/view?filename=rgthree.compare._temp_gmjxy_00002_.png&type=temp&subfolder=&rand=0.6218111120009978"
128
+ }
129
+ ]
130
+ },
131
+ "image_a": [
132
+ "20",
133
+ 0
134
+ ]
135
+ },
136
+ "class_type": "Image Comparer (rgthree)",
137
+ "_meta": {
138
+ "title": "图像对比"
139
+ }
140
+ },
141
+ "137": {
142
+ "inputs": {
143
+ "images": [
144
+ "20",
145
+ 0
146
+ ]
147
+ },
148
+ "class_type": "PreviewImage",
149
+ "_meta": {
150
+ "title": "预览图像"
151
+ }
152
+ },
153
+ "142": {
154
+ "inputs": {
155
+ "vae": [
156
+ "27",
157
+ 0
158
+ ]
159
+ },
160
+ "class_type": "VAEEncode",
161
+ "_meta": {
162
+ "title": "VAE编码"
163
+ }
164
+ },
165
+ "144": {
166
+ "inputs": {
167
+ "width": 512,
168
+ "height": 512,
169
+ "batch_size": 1
170
+ },
171
+ "class_type": "EmptyLatentImage",
172
+ "_meta": {
173
+ "title": "空Latent"
174
+ }
175
+ }
176
+ }
26-5-10-API-Studio/workflows/upscale.json ADDED
@@ -0,0 +1,90 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "15": {
3
+ "inputs": {
4
+ "image": "pasted/image (794).png"
5
+ },
6
+ "class_type": "LoadImage",
7
+ "_meta": {
8
+ "title": "加载图像"
9
+ }
10
+ },
11
+ "169": {
12
+ "inputs": {
13
+ "model": "seedvr2_ema_3b_fp16.safetensors",
14
+ "device": "cuda:0",
15
+ "blocks_to_swap": 32,
16
+ "swap_io_components": true,
17
+ "offload_device": "cpu",
18
+ "cache_model": false,
19
+ "attention_mode": "sdpa"
20
+ },
21
+ "class_type": "SeedVR2LoadDiTModel",
22
+ "_meta": {
23
+ "title": "SeedVR2 (Down)Load DiT Model"
24
+ }
25
+ },
26
+ "170": {
27
+ "inputs": {
28
+ "model": "ema_vae_fp16.safetensors",
29
+ "device": "cuda:0",
30
+ "encode_tiled": true,
31
+ "encode_tile_size": 1024,
32
+ "encode_tile_overlap": 128,
33
+ "decode_tiled": true,
34
+ "decode_tile_size": 1024,
35
+ "decode_tile_overlap": 128,
36
+ "tile_debug": "false",
37
+ "offload_device": "cpu",
38
+ "cache_model": false
39
+ },
40
+ "class_type": "SeedVR2LoadVAEModel",
41
+ "_meta": {
42
+ "title": "SeedVR2 (Down)Load VAE Model"
43
+ }
44
+ },
45
+ "172": {
46
+ "inputs": {
47
+ "seed": 21053977,
48
+ "resolution": 2048,
49
+ "max_resolution": 4096,
50
+ "batch_size": 5,
51
+ "uniform_batch_size": false,
52
+ "color_correction": "lab",
53
+ "temporal_overlap": 0,
54
+ "prepend_frames": 0,
55
+ "input_noise_scale": 0,
56
+ "latent_noise_scale": 0,
57
+ "offload_device": "cpu",
58
+ "enable_debug": false,
59
+ "image": [
60
+ "15",
61
+ 0
62
+ ],
63
+ "dit": [
64
+ "169",
65
+ 0
66
+ ],
67
+ "vae": [
68
+ "170",
69
+ 0
70
+ ]
71
+ },
72
+ "class_type": "SeedVR2VideoUpscaler",
73
+ "_meta": {
74
+ "title": "SeedVR2 Video Upscaler (v2.5.10)"
75
+ }
76
+ },
77
+ "174": {
78
+ "inputs": {
79
+ "filename_prefix": "ComfyUI",
80
+ "images": [
81
+ "172",
82
+ 0
83
+ ]
84
+ },
85
+ "class_type": "SaveImage",
86
+ "_meta": {
87
+ "title": "保存图像"
88
+ }
89
+ }
90
+ }
26-5-10-API-Studio/启动服务.bat ADDED
@@ -0,0 +1,11 @@
 
 
 
 
 
 
 
 
 
 
 
 
1
+ @echo off
2
+ cd /d "%~dp0"
3
+ echo Starting ComfyUI-API-Modelscope...
4
+ echo Visit: http://127.0.0.1:3000/
5
+ echo Press Ctrl+C to stop.
6
+ echo.
7
+ start /b cmd /c "timeout /t 3 /nobreak >nul && start http://127.0.0.1:3000/"
8
+ python main.py
9
+ echo.
10
+ echo Server stopped.
11
+ pause
26-5-10-API-Studio/安装依赖.bat ADDED
@@ -0,0 +1,37 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ @echo off
2
+ cd /d "%~dp0"
3
+ echo ============================================
4
+ echo Installing dependencies (offline)
5
+ echo ============================================
6
+ echo.
7
+
8
+ python --version >nul 2>&1
9
+ if errorlevel 1 (
10
+ echo [ERROR] Python not found. Please install Python 3.10+
11
+ echo Download: https://www.python.org/downloads/
12
+ pause
13
+ exit /b 1
14
+ )
15
+
16
+ python --version
17
+
18
+ echo.
19
+ echo [1/2] Checking pip...
20
+ python -m pip --version >nul 2>&1
21
+ if errorlevel 1 (
22
+ echo pip not found, bootstrapping...
23
+ python -m ensurepip --upgrade
24
+ )
25
+
26
+ echo [2/2] Installing from local packages folder...
27
+ python -m pip install --no-index --find-links=packages -r requirements.txt
28
+
29
+ if errorlevel 1 (
30
+ echo.
31
+ echo [WARN] Offline install failed, trying online...
32
+ python -m pip install -r requirements.txt
33
+ )
34
+
35
+ echo.
36
+ echo Done. Run "启动服务.bat" to start.
37
+ pause
26-5-10-API-Studio/运行说明.txt ADDED
@@ -0,0 +1,28 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ 1. API网站注册:
2
+ https://ai.comfly.chat/register?aff=HAOj137551
3
+
4
+ 2. 新建API key
5
+ https://ai.comfly.chat/token
6
+
7
+ 3. 将key填写到软件的API目录中的.env
8
+ COMFLY_API_KEY=sk-xxxxx
9
+
10
+ 4. 创建modelscope的API key
11
+ https://www.modelscope.cn/my/access/token
12
+
13
+ 5. 填写到API目录中的.env
14
+ MODELSCOPE_API_KEY=ms-xxxx
15
+
16
+ 6. 如果调用本地comfyui,需要确保workflows目录中的所有工作流都可以在本地正常运行
17
+
18
+ 7. 如果本地comfyui默认的端口是8188,就可以不用修改这个值。现在这个值是读取后台8188和4090两个端口的显卡,如果有多显卡可以修改端口号。
19
+ COMFYUI_INSTANCES=127.0.0.1:8188,127.0.0.1:4090
20
+
21
+ ----运行说明-----
22
+
23
+ 直接运行“启动服务.bat”,如果缺少依赖,则运行“安装依赖.bat”。
24
+
25
+
26
+ ---报错问题---
27
+
28
+ 出现任何报错,可以安装Codex Installer.exe,选中这个文件夹,让codex给你解决运行问题,免费账号每周都有免费额度可以使用