Logankunfall commited on
Commit
86a095e
·
verified ·
1 Parent(s): 3d7bd2f

Upload 19 files

Browse files
Dockerfile ADDED
@@ -0,0 +1,24 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # New NAI - Hugging Face Spaces Dockerfile (Node.js/Express)
2
+ # Ensures binding to $PORT and 0.0.0.0, no browser auto-open.
3
+
4
+ FROM node:20-bullseye-slim
5
+
6
+ ENV NODE_ENV=production \
7
+ HOST=0.0.0.0 \
8
+ AUTO_OPEN_BROWSER=0
9
+
10
+ WORKDIR /app
11
+
12
+ # Install production deps first (layer cache friendly)
13
+ COPY package.json package-lock.json* ./
14
+ RUN npm ci --only=production || npm install --only=production
15
+
16
+ # Copy the rest of the app
17
+ COPY . .
18
+
19
+ # Optional (uncomment if curl-based healthcheck is used)
20
+ # RUN apt-get update && apt-get install -y --no-install-recommends curl && rm -rf /var/lib/apt/lists/*
21
+
22
+ # Spaces provides $PORT. We just run and listen on host 0.0.0.0.
23
+ EXPOSE 7860
24
+ CMD ["node", "server.js"]
README.md CHANGED
@@ -1,11 +1,47 @@
1
- ---
2
- title: Newnai2
3
- emoji: 🏃
4
- colorFrom: gray
5
- colorTo: purple
6
- sdk: docker
7
- pinned: false
8
- license: apache-2.0
9
- ---
10
-
11
- Check out the configuration reference at https://huggingface.co/docs/hub/spaces-config-reference
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ ---
2
+ title: New NAI (Node/Express) - HF Spaces
3
+ emoji: 🧩
4
+ colorFrom: green
5
+ colorTo: gray
6
+ sdk: docker
7
+ app_port: 7860
8
+ pinned: false
9
+ ---
10
+
11
+ New NAI(Node/Express 版本)— 适配 Hugging Face Spaces(Docker)
12
+
13
+ 一、目录说明
14
+ - 服务入口: [server.js](server.js:1)
15
+ - 前端页面: [frontend/index.html](frontend/index.html:1)(原生 HTML/CSS/JS)
16
+ - 资源脚本: [frontend/assets/app.js](frontend/assets/app.js:1)、[frontend/assets/style.css](frontend/assets/style.css:1)
17
+ - 上游代理: [novelai.js](novelai.js:1)(调用 NovelAI,解压 ZIP,返回 data URL)
18
+ - 铃声资源: [ring/new-notification-3-398649.mp3](ring/new-notification-3-398649.mp3:1)
19
+ - NPM 清单: [package.json](package.json:1)
20
+ - Docker 配置: [Dockerfile](Dockerfile:1)
21
+
22
+ 二、运行方式(在 Hugging Face Spaces 上)
23
+ - Space 类型: Docker
24
+ - 本仓库根目录包含 [Dockerfile](Dockerfile:1);Spaces 会读取环境变量 PORT,并在容器内以 0.0.0.0:$PORT 启动。
25
+ - Node 版本基于官方 node:20-bullseye-slim 镜像;入口命令为:
26
+ - node [server.js](server.js:1)
27
+ - 服务内已按优先级解析端口: 环境变量 PORT > 配置文件 > 默认 9180,且在 Dockerfile 中已设置 HOST=0.0.0.0
28
+
29
+ 三、环境与配置
30
+ - Web 端访问地址由 Spaces 分配,容器内监听 $PORT/0.0.0.0。
31
+ - 前端“配置”页填写并保存 NovelAI Key(Token),后端通过 Authorization Bearer 与上游通信。
32
+ - 目录选择/打开文件夹等本地功能在 HF 环境将受限;不会影响生成流程。
33
+
34
+ 四、接口(与本地一致)
35
+ - 健康检查: GET /api/health
36
+ - 配置: GET /api/config、PUT /api/config
37
+ - 生成:
38
+ - POST /api/generate/t2i
39
+ - POST /api/generate/i2i
40
+ - POST /api/generate/inpaint
41
+ - 静态资源:
42
+ - / → [frontend/index.html](frontend/index.html:1)
43
+ - /ring → [ring/new-notification-3-398649.mp3](ring/new-notification-3-398649.mp3:1)
44
+
45
+ 五、注意事项(HF 环境差异)
46
+ - 不支持系统级文件夹选择/打开;如需输出路径请在配置中手填或使用默认。
47
+ - 首次请求可能遇到上游限流(429),已实现指数退避重试;可稍后再试。
app.py ADDED
@@ -0,0 +1,9 @@
 
 
 
 
 
 
 
 
 
 
1
+ import os
2
+
3
+ # Hugging Face Spaces entrypoint: run FastAPI app on $PORT
4
+ from backend.app import app
5
+
6
+ if __name__ == "__main__":
7
+ port = int(os.environ.get("PORT", "7860"))
8
+ import uvicorn
9
+ uvicorn.run(app, host="0.0.0.0", port=port)
backend/__init__.py ADDED
@@ -0,0 +1 @@
 
 
1
+ # Package marker for backend
backend/app.py ADDED
@@ -0,0 +1,305 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from __future__ import annotations
2
+
3
+ import base64
4
+ import os
5
+ import platform
6
+ import tempfile
7
+ from pathlib import Path
8
+ from typing import Any, Dict
9
+
10
+ from fastapi import FastAPI, HTTPException
11
+ from fastapi.middleware.cors import CORSMiddleware
12
+ from fastapi.responses import JSONResponse
13
+ from fastapi.staticfiles import StaticFiles
14
+
15
+ from .config import AppConfig, load_config, save_config
16
+ from .models import ConfigUpdate, I2IRequest, InpaintRequest, T2IRequest
17
+ from .services.novelai import generate_i2i, generate_inpaint, generate_t2i
18
+
19
+
20
+ def _ensure_x64(n: int) -> int:
21
+ if n <= 64:
22
+ return 64
23
+ if n % 64 == 0:
24
+ return n
25
+ return ((n // 64) + 1) * 64 if (n / 64) % 1 >= 0.5 else (n // 64) * 64
26
+
27
+
28
+ def _as_data_uri(b64: str) -> str:
29
+ return f"data:image/png;base64,{b64}"
30
+
31
+
32
+ app = FastAPI(title="New NAI", version="1.0.0")
33
+
34
+ # CORS
35
+ app.add_middleware(
36
+ CORSMiddleware,
37
+ allow_origins=["*"],
38
+ allow_credentials=False,
39
+ allow_methods=["*"],
40
+ allow_headers=["*"],
41
+ )
42
+
43
+
44
+ @app.get("/api/health")
45
+ def health() -> Dict[str, Any]:
46
+ return {"status": "ok"}
47
+
48
+
49
+ @app.get("/api/config")
50
+ def get_config() -> Dict[str, Any]:
51
+ cfg: AppConfig = load_config()
52
+ return cfg.model_dump()
53
+
54
+
55
+ @app.put("/api/config")
56
+ def update_config(update: ConfigUpdate) -> Dict[str, Any]:
57
+ cfg = load_config()
58
+ data = update.model_dump(exclude_none=True)
59
+ for k, v in data.items():
60
+ setattr(cfg, k, v)
61
+ save_config(cfg)
62
+ return cfg.model_dump()
63
+
64
+
65
+ @app.get("/api/select-output-dir")
66
+ def api_select_output_dir() -> Dict[str, Any]:
67
+ """
68
+ 在本机弹出目录选择器并返回选择的目录路径。
69
+ 多重回退方案:
70
+ 1) tkinter(如可用)
71
+ 2) Windows: PowerShell + Shell.Application.BrowseForFolder
72
+ 3) macOS: osascript choose folder
73
+ 4) Linux: zenity --file-selection --directory
74
+ 全部失败则返回 500。
75
+ """
76
+ # 1) tkinter
77
+ try:
78
+ import tkinter as tk # type: ignore
79
+ from tkinter import filedialog # type: ignore
80
+ root = tk.Tk()
81
+ root.withdraw()
82
+ path = filedialog.askdirectory(title="选择保存目录")
83
+ try:
84
+ root.destroy()
85
+ except Exception:
86
+ pass
87
+ if path:
88
+ return {"path": path}
89
+ except Exception:
90
+ pass
91
+
92
+ system = platform.system()
93
+ # 2) Windows: PowerShell COM Shell.Application
94
+ if system == "Windows":
95
+ # 2a) PowerShell COM Shell.Application
96
+ try:
97
+ import subprocess
98
+ ps_cmd = r'$f=(New-Object -ComObject Shell.Application).BrowseForFolder(0,"选择保存目录",0); if($f){$f.Self.Path}'
99
+ res = subprocess.run(
100
+ ["powershell", "-NoProfile", "-Command", ps_cmd],
101
+ capture_output=True, text=True, timeout=60
102
+ )
103
+ out = (res.stdout or "").strip()
104
+ if out:
105
+ return {"path": out}
106
+ except Exception:
107
+ pass
108
+ # 2b) VBScript + cscript(兼容禁用 PowerShell 的环境)
109
+ try:
110
+ import subprocess
111
+ vbs = (
112
+ 'Set sh = CreateObject("Shell.Application")\n'
113
+ 'Set f = sh.BrowseForFolder(0, "选择保存目录", 0)\n'
114
+ 'If (Not f Is Nothing) Then\n'
115
+ ' WScript.Echo f.Self.Path\n'
116
+ 'End If\n'
117
+ )
118
+ with tempfile.NamedTemporaryFile(delete=False, suffix=".vbs") as tf:
119
+ tf.write(vbs.encode("utf-8"))
120
+ tf_path = tf.name
121
+ try:
122
+ res = subprocess.run(
123
+ ["cscript", "//nologo", tf_path],
124
+ capture_output=True, text=True, timeout=60
125
+ )
126
+ finally:
127
+ try:
128
+ os.remove(tf_path)
129
+ except Exception:
130
+ pass
131
+ out = (res.stdout or "").strip()
132
+ if out:
133
+ return {"path": out}
134
+ except Exception:
135
+ pass
136
+
137
+ # 3) macOS: AppleScript choose folder
138
+ if system == "Darwin":
139
+ try:
140
+ import subprocess
141
+ script = 'tell application "System Events" to POSIX path of (choose folder with prompt "选择保存目录")'
142
+ res = subprocess.run(
143
+ ["osascript", "-e", script],
144
+ capture_output=True, text=True, timeout=60
145
+ )
146
+ out = (res.stdout or "").strip()
147
+ if out:
148
+ return {"path": out}
149
+ except Exception:
150
+ pass
151
+
152
+ # 4) Linux: zenity
153
+ if system == "Linux":
154
+ try:
155
+ import subprocess
156
+ res = subprocess.run(
157
+ ["zenity", "--file-selection", "--directory", "--title=选择保存目录"],
158
+ capture_output=True, text=True, timeout=60
159
+ )
160
+ out = (res.stdout or "").strip()
161
+ if out:
162
+ return {"path": out}
163
+ except Exception:
164
+ pass
165
+
166
+ raise HTTPException(status_code=500, detail="无法打开文件夹选择器:所有策略均失败")
167
+
168
+
169
+ @app.post("/api/open-dir")
170
+ def api_open_dir(payload: Dict[str, Any]) -> Dict[str, Any]:
171
+ """
172
+ 打开指定目录;若未传 path,则打开配置中的 output_dir。
173
+ Windows 使用 os.startfile,macOS 用 open,Linux 用 xdg-open。
174
+ """
175
+ path = (payload or {}).get("path") or ""
176
+ if not path:
177
+ cfg = load_config()
178
+ path = cfg.output_dir
179
+ if not path:
180
+ raise HTTPException(status_code=400, detail="未提供路径且配置中未设置 output_dir")
181
+
182
+ p = Path(path)
183
+ try:
184
+ p.mkdir(parents=True, exist_ok=True)
185
+ except Exception as e:
186
+ raise HTTPException(status_code=500, detail=f"创建目录失败: {e}") from e
187
+
188
+ try:
189
+ if hasattr(os, "startfile"):
190
+ os.startfile(str(p)) # Windows
191
+ elif platform.system() == "Darwin":
192
+ import subprocess
193
+ subprocess.run(["open", str(p)], check=False)
194
+ else:
195
+ import subprocess
196
+ subprocess.run(["xdg-open", str(p)], check=False)
197
+ return {"ok": True, "path": str(p)}
198
+ except Exception as e:
199
+ raise HTTPException(status_code=500, detail=f"无法打开目录: {e}") from e
200
+
201
+
202
+ @app.post("/api/generate/t2i")
203
+ def api_t2i(req: T2IRequest):
204
+ cfg = load_config()
205
+ if not cfg.key:
206
+ raise HTTPException(status_code=400, detail="尚未配置 key,请先在配置中设置 key。")
207
+
208
+ width = _ensure_x64(req.width or 768)
209
+ height = _ensure_x64(req.height or 768)
210
+
211
+ try:
212
+ b64, saved = generate_t2i(
213
+ cfg,
214
+ prompt=req.prompt,
215
+ negative=req.negative or "",
216
+ width=width,
217
+ height=height,
218
+ scale=req.scale,
219
+ steps=req.steps,
220
+ sampler=req.sampler,
221
+ noise_schedule=req.noise_schedule,
222
+ seed=req.seed,
223
+ variety=req.variety,
224
+ decrisp=req.decrisp,
225
+ cfg_rescale=req.cfg_rescale,
226
+ )
227
+ return {"image_base64": _as_data_uri(b64), "saved_path": saved}
228
+ except Exception as e:
229
+ raise HTTPException(status_code=500, detail=str(e)) from e
230
+
231
+
232
+ @app.post("/api/generate/i2i")
233
+ def api_i2i(req: I2IRequest):
234
+ cfg = load_config()
235
+ if not cfg.key:
236
+ raise HTTPException(status_code=400, detail="尚未配置 key,请先在配置中设置 key。")
237
+
238
+ width = _ensure_x64(req.width or 768)
239
+ height = _ensure_x64(req.height or 768)
240
+
241
+ try:
242
+ b64, saved = generate_i2i(
243
+ cfg,
244
+ positive=req.positive,
245
+ negative=req.negative or "",
246
+ image_base64=req.image_base64,
247
+ width=width,
248
+ height=height,
249
+ scale=req.scale,
250
+ steps=req.steps,
251
+ sampler=req.sampler,
252
+ noise_schedule=req.noise_schedule,
253
+ strength=req.strength or 0.5,
254
+ noise=req.noise or 0.0,
255
+ seed=req.seed,
256
+ variety=req.variety,
257
+ decrisp=req.decrisp,
258
+ cfg_rescale=req.cfg_rescale,
259
+ )
260
+ return {"image_base64": _as_data_uri(b64), "saved_path": saved}
261
+ except Exception as e:
262
+ raise HTTPException(status_code=500, detail=str(e)) from e
263
+
264
+
265
+ @app.post("/api/generate/inpaint")
266
+ def api_inpaint(req: InpaintRequest):
267
+ cfg = load_config()
268
+ if not cfg.key:
269
+ raise HTTPException(status_code=400, detail="尚未配置 key,请先在配置中设置 key。")
270
+
271
+ width = _ensure_x64(req.width or 768)
272
+ height = _ensure_x64(req.height or 768)
273
+
274
+ try:
275
+ b64, saved = generate_inpaint(
276
+ cfg,
277
+ positive=req.positive,
278
+ negative=req.negative or "",
279
+ image_base64=req.image_base64,
280
+ mask_base64=req.mask_base64,
281
+ add_original_image=req.add_original_image,
282
+ width=width,
283
+ height=height,
284
+ scale=req.scale,
285
+ steps=req.steps,
286
+ sampler=req.sampler,
287
+ noise_schedule=req.noise_schedule,
288
+ strength=req.strength or 0.5,
289
+ noise=req.noise or 0.0,
290
+ seed=req.seed,
291
+ variety=req.variety,
292
+ decrisp=req.decrisp,
293
+ cfg_rescale=req.cfg_rescale,
294
+ )
295
+ return {"image_base64": _as_data_uri(b64), "saved_path": saved}
296
+ except Exception as e:
297
+ raise HTTPException(status_code=500, detail=str(e)) from e
298
+
299
+
300
+ # 前端静态资源(仅保留必要 UI,无教程/仓库链接)
301
+ _frontend_dir = Path(__file__).resolve().parent.parent / "frontend"
302
+ # 提示音静态资源映射:/ring -> 项目根/ring 目录(例如 G:\NOVELAI\New NAI\ring)
303
+ _ring_dir = Path(__file__).resolve().parent.parent / "ring"
304
+ app.mount("/ring", StaticFiles(directory=_ring_dir, html=False), name="ring")
305
+ app.mount("/", StaticFiles(directory=_frontend_dir, html=True), name="frontend")
backend/config.json ADDED
@@ -0,0 +1,14 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "key": "",
3
+ "model": "nai-diffusion-4-5-full",
4
+ "sampler": "k_euler",
5
+ "steps": 28,
6
+ "scale": 5.0,
7
+ "cfg_rescale": 0.0,
8
+ "noise_schedule": "karras",
9
+ "uc_preset": 4,
10
+ "quality_toggle": true,
11
+ "legacy_uc": false,
12
+ "port": 9180,
13
+ "save_output": true
14
+ }
backend/config.py ADDED
@@ -0,0 +1,51 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from __future__ import annotations
2
+ from pydantic import BaseModel
3
+ from typing import Optional
4
+ from pathlib import Path
5
+ import json
6
+
7
+ CONFIG_FILE = Path(__file__).parent / "config.json"
8
+
9
+
10
+ class AppConfig(BaseModel):
11
+ # 将 token 重命名为 key
12
+ key: Optional[str] = None
13
+
14
+ # 图片生成默认参数(与原项目保持一致的关键项)
15
+ model: str = "nai-diffusion-3"
16
+ sampler: str = "k_euler"
17
+ steps: int = 28
18
+ scale: float = 5.0
19
+ cfg_rescale: float = 0.0
20
+ noise_schedule: Optional[str] = "karras"
21
+ uc_preset: int = 4
22
+ quality_toggle: bool = True
23
+ legacy_uc: bool = False
24
+
25
+ # 通用配置
26
+ port: int = 9180
27
+ save_output: bool = True
28
+ # 输出根目录(默认:项目根/output)
29
+ output_dir: str = str(Path(__file__).resolve().parent.parent / "output")
30
+ # 颜色方案:auto | bamboo | custom(前端即时应用)
31
+ color_scheme: str = "auto"
32
+ custom_primary: Optional[str] = None
33
+ # 提示音配置
34
+ sound_enabled: bool = False
35
+ sound_url: Optional[str] = "/ring/ring.mp3"
36
+
37
+
38
+ def load_config() -> AppConfig:
39
+ if CONFIG_FILE.exists():
40
+ with open(CONFIG_FILE, "r", encoding="utf-8") as f:
41
+ data = json.load(f)
42
+ return AppConfig(**data)
43
+ cfg = AppConfig()
44
+ save_config(cfg)
45
+ return cfg
46
+
47
+
48
+ def save_config(cfg: AppConfig) -> None:
49
+ CONFIG_FILE.parent.mkdir(parents=True, exist_ok=True)
50
+ with open(CONFIG_FILE, "w", encoding="utf-8") as f:
51
+ json.dump(cfg.model_dump(), f, ensure_ascii=False, indent=2)
backend/models.py ADDED
@@ -0,0 +1,82 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from __future__ import annotations
2
+ from pydantic import BaseModel
3
+ from typing import Optional
4
+
5
+
6
+ # 文生图
7
+ class T2IRequest(BaseModel):
8
+ prompt: str
9
+ negative: Optional[str] = ""
10
+ width: Optional[int] = 768
11
+ height: Optional[int] = 768
12
+ scale: Optional[float] = None
13
+ steps: Optional[int] = None
14
+ sampler: Optional[str] = None
15
+ noise_schedule: Optional[str] = None
16
+ seed: Optional[int] = -1
17
+ variety: Optional[bool] = False
18
+ decrisp: Optional[bool] = False
19
+ cfg_rescale: Optional[float] = None
20
+
21
+
22
+ # 图生图
23
+ class I2IRequest(BaseModel):
24
+ positive: str
25
+ negative: Optional[str] = ""
26
+ image_base64: str
27
+ width: Optional[int] = None
28
+ height: Optional[int] = None
29
+ scale: Optional[float] = None
30
+ steps: Optional[int] = None
31
+ sampler: Optional[str] = None
32
+ noise_schedule: Optional[str] = None
33
+ strength: Optional[float] = 0.5
34
+ noise: Optional[float] = 0.0
35
+ seed: Optional[int] = -1
36
+ variety: Optional[bool] = False
37
+ decrisp: Optional[bool] = False
38
+ cfg_rescale: Optional[float] = None
39
+
40
+
41
+ # 局部重绘
42
+ class InpaintRequest(BaseModel):
43
+ positive: str
44
+ negative: Optional[str] = ""
45
+ image_base64: str
46
+ mask_base64: str
47
+ add_original_image: bool = False
48
+ width: Optional[int] = None
49
+ height: Optional[int] = None
50
+ scale: Optional[float] = None
51
+ steps: Optional[int] = None
52
+ sampler: Optional[str] = None
53
+ noise_schedule: Optional[str] = None
54
+ strength: Optional[float] = 0.5
55
+ noise: Optional[float] = 0.0
56
+ seed: Optional[int] = -1
57
+ variety: Optional[bool] = False
58
+ decrisp: Optional[bool] = False
59
+ cfg_rescale: Optional[float] = None
60
+
61
+
62
+ # 配置更新
63
+ class ConfigUpdate(BaseModel):
64
+ key: Optional[str] = None
65
+ model: Optional[str] = None
66
+ sampler: Optional[str] = None
67
+ steps: Optional[int] = None
68
+ scale: Optional[float] = None
69
+ cfg_rescale: Optional[float] = None
70
+ noise_schedule: Optional[str] = None
71
+ uc_preset: Optional[int] = None
72
+ quality_toggle: Optional[bool] = None
73
+ legacy_uc: Optional[bool] = None
74
+ port: Optional[int] = None
75
+ save_output: Optional[bool] = None
76
+ output_dir: Optional[str] = None
77
+ # 新增:颜色配置(背景方案)
78
+ color_scheme: Optional[str] = None # auto | bamboo | custom
79
+ custom_primary: Optional[str] = None # 自定义背景主色(如 #7ba23f)
80
+ # 新增:提示音配置
81
+ sound_enabled: Optional[bool] = None
82
+ sound_url: Optional[str] = None
backend/server.py ADDED
@@ -0,0 +1,31 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from __future__ import annotations
2
+
3
+ import threading
4
+ import time
5
+ import webbrowser
6
+
7
+ import uvicorn
8
+
9
+ from .app import app
10
+ from .config import load_config
11
+
12
+
13
+ def _open_browser_later(url: str, delay: float = 1.5):
14
+ def _opener():
15
+ time.sleep(delay)
16
+ try:
17
+ webbrowser.open(url)
18
+ except Exception:
19
+ pass
20
+
21
+ t = threading.Thread(target=_opener, daemon=True)
22
+ t.start()
23
+
24
+
25
+ if __name__ == "__main__":
26
+ cfg = load_config()
27
+ port = int(cfg.port or 11451)
28
+ url = f"http://127.0.0.1:{port}"
29
+ _open_browser_later(url)
30
+ # 直接传 app 实例,避免模块路径问题
31
+ uvicorn.run(app, host="127.0.0.1", port=port, reload=False)
backend/services/novelai.py ADDED
@@ -0,0 +1,352 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from __future__ import annotations
2
+
3
+ import base64
4
+ import io
5
+ import json
6
+ import os
7
+ import random
8
+ import time
9
+ import zipfile
10
+ from typing import Optional, Tuple
11
+
12
+ import httpx
13
+
14
+ from ..config import AppConfig, load_config
15
+
16
+ NOVELAI_ENDPOINT = "https://image.novelai.net/ai/generate-image"
17
+
18
+
19
+ def _headers(key: Optional[str]) -> dict:
20
+ if not key:
21
+ raise ValueError("未配置 key,请先在配置中设置 key。")
22
+ return {
23
+ "Accept": "application/zip, */*;q=0.5",
24
+ "Accept-Encoding": "gzip, deflate, br",
25
+ "Accept-Language": "zh-CN,zh;q=0.9,en;q=0.8,en-GB;q=0.7,en-US;q=0.6",
26
+ "Authorization": f"Bearer {key}",
27
+ "Content-Type": "application/json",
28
+ "Origin": "https://novelai.net",
29
+ "Referer": "https://novelai.net/",
30
+ "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/106.0.0.0 Safari/537.36",
31
+ }
32
+
33
+
34
+ def _rand_seed(seed: Optional[int]) -> int:
35
+ if seed is None or seed == -1:
36
+ return random.randint(1_000_000_000, 9_999_999_999)
37
+ return int(seed)
38
+
39
+
40
+ def _post_and_unzip_image(payload: dict, key: str, max_retries: int = 5, backoff: float = 1.5) -> bytes:
41
+ """
42
+ 使用 HTTP/2 与上游通信,提升兼容性与稳定性。
43
+ """
44
+ attempt = 0
45
+ # 复用连接提升性能
46
+ with httpx.Client(http2=True, headers=_headers(key), timeout=120.0) as client:
47
+ while True:
48
+ attempt += 1
49
+ try:
50
+ resp = client.post(NOVELAI_ENDPOINT, json=payload)
51
+ # 频率限制,指数退避重试
52
+ if resp.status_code == 429 and attempt <= max_retries:
53
+ time.sleep(backoff * attempt)
54
+ continue
55
+ resp.raise_for_status()
56
+ with zipfile.ZipFile(io.BytesIO(resp.content), mode="r") as zf:
57
+ with zf.open("image_0.png") as image:
58
+ return image.read()
59
+ except httpx.HTTPStatusError as http_err:
60
+ # 返回上游的错误文本,便于定位(如 key 无效、参数不合法、模型不支持等)
61
+ try:
62
+ detail = resp.json()
63
+ except Exception:
64
+ detail = getattr(resp, "text", str(http_err))
65
+ status = getattr(resp, "status_code", "unknown")
66
+ raise ValueError(f"上游生成失败(HTTP {status}): {detail}") from http_err
67
+ except httpx.RequestError as req_err:
68
+ raise ValueError(f"网络错误: {req_err}") from req_err
69
+
70
+
71
+ def _ensure_dir(p: str) -> None:
72
+ os.makedirs(p, exist_ok=True)
73
+
74
+
75
+ def _save_image_bytes(img: bytes, folder: str) -> str:
76
+ _ensure_dir(folder)
77
+ name = f"{int(time.time()*1000)}_{random.randint(1000,9999)}.png"
78
+ path = os.path.join(folder, name)
79
+ with open(path, "wb") as f:
80
+ f.write(img)
81
+ return path
82
+
83
+
84
+ def _b64(img: bytes) -> str:
85
+ return base64.b64encode(img).decode("utf-8")
86
+
87
+
88
+ def _build_t2i_json(cfg: AppConfig, *, prompt: str, negative: str, width: int, height: int,
89
+ scale: Optional[float], steps: Optional[int], sampler: Optional[str],
90
+ noise_schedule: Optional[str], seed: Optional[int],
91
+ variety: Optional[bool], decrisp: Optional[bool], cfg_rescale: Optional[float]) -> dict:
92
+ is_v4 = "nai-diffusion-4" in (cfg.model or "")
93
+ if is_v4:
94
+ # v4 系列参数严格对齐原项目 json_for_t2i_v4 结构,避免 400/500
95
+ resolved_sampler = (sampler or cfg.sampler) or "k_euler"
96
+ if resolved_sampler == "ddim_v3":
97
+ resolved_sampler = "k_euler"
98
+ params = {
99
+ "params_version": 3,
100
+ "width": int(width),
101
+ "height": int(height),
102
+ "scale": float(scale if scale is not None else cfg.scale),
103
+ "sampler": resolved_sampler,
104
+ "steps": int(steps if steps is not None else cfg.steps),
105
+ "n_samples": 1,
106
+ "ucPreset": cfg.uc_preset,
107
+ "qualityToggle": bool(cfg.quality_toggle),
108
+ "autoSmea": True,
109
+ "dynamic_thresholding": bool(decrisp if decrisp is not None else False),
110
+ "controlnet_strength": 1,
111
+ "legacy": False,
112
+ "add_original_image": False,
113
+ "cfg_rescale": float(cfg_rescale if cfg_rescale is not None else cfg.cfg_rescale),
114
+ "legacy_v3_extend": False,
115
+ "seed": _rand_seed(seed),
116
+ "use_coords": False,
117
+ "legacy_uc": bool(cfg.legacy_uc),
118
+ "characterPrompts": [],
119
+ "negative_prompt": negative or "",
120
+ "reference_image_multiple": [],
121
+ "reference_information_extracted_multiple": [],
122
+ "reference_strength_multiple": [],
123
+ "deliberate_euler_ancestral_bug": False,
124
+ "prefer_brownian": True,
125
+ "stream": "msgpack",
126
+ }
127
+ return {
128
+ "input": prompt,
129
+ "model": cfg.model,
130
+ "action": "generate",
131
+ "parameters": {
132
+ **params,
133
+ "v4_prompt": {
134
+ "caption": {
135
+ "base_caption": prompt,
136
+ "char_captions": [],
137
+ },
138
+ "use_coords": False,
139
+ "use_order": True,
140
+ },
141
+ "v4_negative_prompt": {
142
+ "caption": {
143
+ "base_caption": negative or "",
144
+ "char_captions": [],
145
+ },
146
+ "legacy_uc": bool(cfg.legacy_uc),
147
+ },
148
+ },
149
+ "use_new_shared_trial": True,
150
+ }
151
+ else:
152
+ # 计算与原项目一致的 skip_cfg_above_sigma
153
+ model_name = (cfg.model or "").lower()
154
+ if variety:
155
+ if "nai-diffusion-4-5" in model_name:
156
+ skip_val = 58
157
+ elif "nai-diffusion-4" in model_name:
158
+ skip_val = 19.343056794463642
159
+ else:
160
+ skip_val = 19
161
+ else:
162
+ skip_val = None
163
+
164
+ resolved_sampler_nv4 = (sampler or cfg.sampler) or "k_euler"
165
+
166
+ payload = {
167
+ "input": prompt,
168
+ "model": cfg.model,
169
+ "action": "generate",
170
+ "parameters": {
171
+ "params_version": 3,
172
+ "width": int(width),
173
+ "height": int(height),
174
+ "scale": float(scale if scale is not None else cfg.scale),
175
+ "sampler": resolved_sampler_nv4,
176
+ "steps": int(steps if steps is not None else cfg.steps),
177
+ "n_samples": 1,
178
+ "ucPreset": cfg.uc_preset,
179
+ "qualityToggle": bool(cfg.quality_toggle),
180
+ "sm": False,
181
+ "sm_dyn": False,
182
+ "dynamic_thresholding": bool(decrisp if decrisp is not None else False),
183
+ "controlnet_strength": 1,
184
+ "legacy": False,
185
+ "add_original_image": False,
186
+ "uncond_scale": 1,
187
+ "cfg_rescale": float(cfg_rescale if cfg_rescale is not None else cfg.cfg_rescale),
188
+ "legacy_v3_extend": False,
189
+ "skip_cfg_above_sigma": skip_val,
190
+ "seed": _rand_seed(seed),
191
+ "negative_prompt": negative or "",
192
+ "reference_image_multiple": [],
193
+ "reference_information_extracted_multiple": [],
194
+ "reference_strength_multiple": [],
195
+ },
196
+ }
197
+ # 僅当采样器不是 ddim_v3 时再发送 noise_schedule(对齐原项目)
198
+ if noise_schedule and resolved_sampler_nv4 != "ddim_v3":
199
+ payload["parameters"]["noise_schedule"] = noise_schedule
200
+ return payload
201
+
202
+
203
+ def _build_i2i_json(cfg: AppConfig, *, positive: str, negative: str, image_base64: str,
204
+ width: Optional[int], height: Optional[int],
205
+ scale: Optional[float], steps: Optional[int], sampler: Optional[str],
206
+ noise_schedule: Optional[str], strength: float, noise: float, seed: Optional[int],
207
+ variety: Optional[bool], decrisp: Optional[bool], cfg_rescale: Optional[float]) -> dict:
208
+ payload = {
209
+ "input": positive,
210
+ "model": cfg.model,
211
+ "action": "img2img",
212
+ "parameters": {
213
+ "width": int(width or 768),
214
+ "height": int(height or 768),
215
+ "scale": float(scale if scale is not None else cfg.scale),
216
+ "sampler": sampler or cfg.sampler,
217
+ "steps": int(steps if steps is not None else cfg.steps),
218
+ "n_samples": 1,
219
+ "strength": float(strength),
220
+ "noise": float(noise),
221
+ "ucPreset": cfg.uc_preset,
222
+ "qualityToggle": bool(cfg.quality_toggle),
223
+ "sm": False,
224
+ "sm_dyn": False,
225
+ "dynamic_thresholding": bool(decrisp if decrisp is not None else False),
226
+ "controlnet_strength": 1,
227
+ "legacy": False,
228
+ "add_original_image": False,
229
+ "uncond_scale": 1,
230
+ "cfg_rescale": float(cfg_rescale if cfg_rescale is not None else cfg.cfg_rescale),
231
+ "legacy_v3_extend": False,
232
+ "skip_cfg_above_sigma": 58 if (variety or False) else None,
233
+ "params_version": 3,
234
+ "seed": _rand_seed(seed),
235
+ "image": image_base64,
236
+ "extra_noise_seed": _rand_seed(seed),
237
+ "negative_prompt": negative or "",
238
+ "reference_image_multiple": [],
239
+ "reference_information_extracted_multiple": [],
240
+ "reference_strength_multiple": [],
241
+ },
242
+ }
243
+ if noise_schedule:
244
+ payload["parameters"]["noise_schedule"] = noise_schedule
245
+ return payload
246
+
247
+
248
+ def _build_inpaint_json(cfg: AppConfig, *, positive: str, negative: str, image_base64: str, mask_base64: str,
249
+ add_original_image: bool,
250
+ width: Optional[int], height: Optional[int],
251
+ scale: Optional[float], steps: Optional[int], sampler: Optional[str],
252
+ noise_schedule: Optional[str], strength: float, noise: float, seed: Optional[int],
253
+ variety: Optional[bool], decrisp: Optional[bool], cfg_rescale: Optional[float]) -> dict:
254
+ # 模型名:v3 为 xxx-inpainting,按原项目逻辑做兼容
255
+ model = f"{cfg.model}-inpainting" if cfg.model not in ["nai-diffusion-2", "nai-diffusion-4-curated-preview", "nai-diffusion-4-full"] else "nai-diffusion-3-inpainting"
256
+ payload = {
257
+ "input": positive,
258
+ "model": model,
259
+ "action": "infill",
260
+ "parameters": {
261
+ "width": int(width or 768),
262
+ "height": int(height or 768),
263
+ "scale": float(scale if scale is not None else cfg.scale),
264
+ "sampler": sampler or cfg.sampler,
265
+ "steps": int(steps if steps is not None else cfg.steps),
266
+ "n_samples": 1,
267
+ "strength": float(strength),
268
+ "noise": float(noise),
269
+ "ucPreset": cfg.uc_preset,
270
+ "qualityToggle": bool(cfg.quality_toggle),
271
+ "sm": False,
272
+ "sm_dyn": False,
273
+ "dynamic_thresholding": bool(decrisp if decrisp is not None else False),
274
+ "controlnet_strength": 1,
275
+ "legacy": False,
276
+ "add_original_image": bool(add_original_image),
277
+ "uncond_scale": 1,
278
+ "cfg_rescale": float(cfg_rescale if cfg_rescale is not None else cfg.cfg_rescale),
279
+ "legacy_v3_extend": False,
280
+ "skip_cfg_above_sigma": 58 if (variety or False) else None,
281
+ "params_version": 3,
282
+ "seed": _rand_seed(seed),
283
+ "image": image_base64,
284
+ "mask": mask_base64,
285
+ "extra_noise_seed": _rand_seed(seed),
286
+ "negative_prompt": negative or "",
287
+ "reference_image_multiple": [],
288
+ "reference_information_extracted_multiple": [],
289
+ "reference_strength_multiple": [],
290
+ },
291
+ }
292
+ if noise_schedule:
293
+ payload["parameters"]["noise_schedule"] = noise_schedule
294
+ return payload
295
+
296
+
297
+ def generate_t2i(cfg: AppConfig, *,
298
+ prompt: str, negative: str = "",
299
+ width: int = 768, height: int = 768,
300
+ scale: Optional[float] = None, steps: Optional[int] = None, sampler: Optional[str] = None,
301
+ noise_schedule: Optional[str] = None, seed: Optional[int] = -1,
302
+ variety: Optional[bool] = False, decrisp: Optional[bool] = False,
303
+ cfg_rescale: Optional[float] = None,
304
+ save_folder: str = "../../output/t2i") -> Tuple[str, str]:
305
+ payload = _build_t2i_json(cfg, prompt=prompt, negative=negative, width=width, height=height,
306
+ scale=scale, steps=steps, sampler=sampler, noise_schedule=noise_schedule,
307
+ seed=seed, variety=variety, decrisp=decrisp, cfg_rescale=cfg_rescale)
308
+ img = _post_and_unzip_image(payload, cfg.key or "")
309
+ base_dir = cfg.output_dir or os.path.join(os.path.dirname(__file__), "../../output")
310
+ folder = os.path.join(base_dir, "t2i")
311
+ path = _save_image_bytes(img, folder)
312
+ return _b64(img), path
313
+
314
+
315
+ def generate_i2i(cfg: AppConfig, *,
316
+ positive: str, negative: str, image_base64: str,
317
+ width: Optional[int] = None, height: Optional[int] = None,
318
+ scale: Optional[float] = None, steps: Optional[int] = None, sampler: Optional[str] = None,
319
+ noise_schedule: Optional[str] = None, strength: float = 0.5, noise: float = 0.0, seed: Optional[int] = -1,
320
+ variety: Optional[bool] = False, decrisp: Optional[bool] = False,
321
+ cfg_rescale: Optional[float] = None,
322
+ save_folder: str = "../../output/i2i") -> Tuple[str, str]:
323
+ payload = _build_i2i_json(cfg, positive=positive, negative=negative, image_base64=image_base64,
324
+ width=width, height=height, scale=scale, steps=steps, sampler=sampler,
325
+ noise_schedule=noise_schedule, strength=strength, noise=noise, seed=seed,
326
+ variety=variety, decrisp=decrisp, cfg_rescale=cfg_rescale)
327
+ img = _post_and_unzip_image(payload, cfg.key or "")
328
+ base_dir = cfg.output_dir or os.path.join(os.path.dirname(__file__), "../../output")
329
+ folder = os.path.join(base_dir, "i2i")
330
+ path = _save_image_bytes(img, folder)
331
+ return _b64(img), path
332
+
333
+
334
+ def generate_inpaint(cfg: AppConfig, *,
335
+ positive: str, negative: str,
336
+ image_base64: str, mask_base64: str, add_original_image: bool = False,
337
+ width: Optional[int] = None, height: Optional[int] = None,
338
+ scale: Optional[float] = None, steps: Optional[int] = None, sampler: Optional[str] = None,
339
+ noise_schedule: Optional[str] = None, strength: float = 0.5, noise: float = 0.0, seed: Optional[int] = -1,
340
+ variety: Optional[bool] = False, decrisp: Optional[bool] = False,
341
+ cfg_rescale: Optional[float] = None,
342
+ save_folder: str = "../../output/inpaint") -> Tuple[str, str]:
343
+ payload = _build_inpaint_json(cfg, positive=positive, negative=negative, image_base64=image_base64,
344
+ mask_base64=mask_base64, add_original_image=add_original_image,
345
+ width=width, height=height, scale=scale, steps=steps, sampler=sampler,
346
+ noise_schedule=noise_schedule, strength=strength, noise=noise, seed=seed,
347
+ variety=variety, decrisp=decrisp, cfg_rescale=cfg_rescale)
348
+ img = _post_and_unzip_image(payload, cfg.key or "")
349
+ base_dir = cfg.output_dir or os.path.join(os.path.dirname(__file__), "../../output")
350
+ folder = os.path.join(base_dir, "inpaint")
351
+ path = _save_image_bytes(img, folder)
352
+ return _b64(img), path
frontend/assets/app.js ADDED
@@ -0,0 +1,582 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ /* New NAI Frontend App JS */
2
+
3
+ (() => {
4
+ const $ = (sel, root = document) => root.querySelector(sel);
5
+ const $$ = (sel, root = document) => [...root.querySelectorAll(sel)];
6
+ const byId = (id) => document.getElementById(id);
7
+ // 提示音状态(运行时),默认关闭,URL 指向 /ring/ring.mp3
8
+ let soundEnabled = false;
9
+ let soundUrl = "/ring/ring.mp3";
10
+
11
+ // 主题:深色/浅色 切换
12
+ const rootEl = document.documentElement;
13
+ function applyTheme(theme) {
14
+ rootEl.setAttribute('data-theme', theme);
15
+ localStorage.setItem('theme', theme);
16
+ const btn = byId('theme-toggle');
17
+ if (btn) btn.textContent = '主题:' + (theme === 'dark' ? '深色' : '浅色');
18
+ }
19
+ const initialTheme = localStorage.getItem('theme') || 'dark';
20
+ applyTheme(initialTheme);
21
+ const themeBtn = byId('theme-toggle');
22
+ if (themeBtn) {
23
+ themeBtn.addEventListener('click', () => {
24
+ const next = (rootEl.getAttribute('data-theme') === 'dark') ? 'light' : 'dark';
25
+ applyTheme(next);
26
+ });
27
+ }
28
+ // 提示音开关(在主题按钮旁)
29
+ const soundBtn = byId('sound-toggle');
30
+ const soundPlayer = byId('sound-player');
31
+ function updateSoundToggle() {
32
+ if (!soundBtn) return;
33
+ soundBtn.setAttribute('aria-pressed', soundEnabled ? 'true' : 'false');
34
+ soundBtn.textContent = '提示音:' + (soundEnabled ? '开' : '关');
35
+ }
36
+ if (soundBtn) {
37
+ soundBtn.addEventListener('click', () => {
38
+ soundEnabled = !soundEnabled;
39
+ updateSoundToggle();
40
+ });
41
+ }
42
+
43
+ // ===== 背景方案处理(非字体/主按钮):根据配置切换页面背景 =====
44
+ function setBackground(color) {
45
+ // 统一覆盖背景变量,立即生效(无刷新)
46
+ rootEl.style.setProperty("--bg", color);
47
+ }
48
+ function applyBackgroundScheme(scheme, custom) {
49
+ const theme = rootEl.getAttribute("data-theme") || "dark";
50
+ if (scheme === "custom" && custom) {
51
+ setBackground(custom);
52
+ return;
53
+ }
54
+ if (scheme === "bamboo") {
55
+ setBackground("#7ba23f"); // 竹子色
56
+ return;
57
+ }
58
+ // auto:浅=白 深=深灰
59
+ if (theme === "light") setBackground("#ffffff");
60
+ else setBackground("#121315");
61
+ }
62
+ function applyAccentFromConfig(cfg) {
63
+ const scheme = cfg?.color_scheme || "auto";
64
+ const custom = cfg?.custom_primary || "";
65
+ applyBackgroundScheme(scheme, custom);
66
+ }
67
+
68
+ // Tabs
69
+ $$(".tab-btn").forEach((btn) => {
70
+ btn.addEventListener("click", () => {
71
+ $$(".tab-btn").forEach((b) => b.classList.remove("active"));
72
+ btn.classList.add("active");
73
+ const tabId = btn.getAttribute("data-tab");
74
+ $$(".tab").forEach((tab) => tab.classList.remove("active"));
75
+ byId(tabId).classList.add("active");
76
+ });
77
+ });
78
+
79
+ // UI helpers
80
+ // 固定状态栏:不弹出、不自动隐藏
81
+ const toast = (msg, type = "info") => {
82
+ const el = byId("toast");
83
+ el.textContent = msg || "";
84
+ el.className = `toast ${type}`;
85
+ // 成功提示时若开启提示音则播放
86
+ try {
87
+ if (type === "success" && soundEnabled) {
88
+ const p = byId("sound-player");
89
+ if (p) {
90
+ p.currentTime = 0;
91
+ // 忽略浏览器自动播放限制的异常
92
+ p.play().catch(() => {});
93
+ }
94
+ }
95
+ } catch {}
96
+ };
97
+
98
+ const loading = {
99
+ show() {
100
+ byId("loading").classList.remove("hidden");
101
+ },
102
+ hide() {
103
+ byId("loading").classList.add("hidden");
104
+ },
105
+ };
106
+
107
+ // Config
108
+ async function loadConfig() {
109
+ try {
110
+ const res = await fetch("/api/config");
111
+ if (!res.ok) throw new Error(await res.text());
112
+ const cfg = await res.json();
113
+
114
+ byId("cfg-key").value = cfg.key ?? "";
115
+ byId("cfg-model").value = cfg.model ?? "nai-diffusion-3";
116
+ byId("cfg-sampler").value = cfg.sampler ?? "k_euler";
117
+ byId("cfg-steps").value = cfg.steps ?? "";
118
+ byId("cfg-scale").value = cfg.scale ?? "";
119
+ byId("cfg-cfg-rescale").value = cfg.cfg_rescale ?? "";
120
+ byId("cfg-noise-schedule").value = cfg.noise_schedule ?? "";
121
+ byId("cfg-uc-preset").value = cfg.uc_preset ?? "";
122
+ byId("cfg-quality-toggle").checked = !!cfg.quality_toggle;
123
+ byId("cfg-legacy-uc").checked = !!cfg.legacy_uc;
124
+ byId("cfg-port").value = cfg.port ?? 9180;
125
+ byId("cfg-save-output").checked = !!cfg.save_output;
126
+ if (byId("cfg-output-dir")) byId("cfg-output-dir").value = cfg.output_dir ?? "";
127
+ // 颜色配置(带预览)
128
+ if (byId("cfg-color-scheme")) byId("cfg-color-scheme").value = cfg.color_scheme ?? "auto";
129
+ if (byId("cfg-custom-primary")) byId("cfg-custom-primary").value = cfg.custom_primary ?? "#7ba23f";
130
+ applyAccentFromConfig(cfg);
131
+
132
+ // 提示音配置
133
+ soundEnabled = !!cfg.sound_enabled;
134
+ soundUrl = cfg.sound_url || "/ring/ring.mp3";
135
+ if (byId("sound-player")) byId("sound-player").src = soundUrl;
136
+ (typeof updateSoundToggle === "function") && updateSoundToggle();
137
+
138
+ // 隐藏下方“配置已读取”提示
139
+ byId("cfg-message").textContent = "";
140
+ } catch (err) {
141
+ byId("cfg-message").textContent = "读取失败:" + err.message;
142
+ toast("读取配置失败", "error");
143
+ }
144
+ }
145
+
146
+ function nullIfEmpty(v) {
147
+ if (v === undefined || v === null) return null;
148
+ if (typeof v === "string" && v.trim() === "") return null;
149
+ return v;
150
+ }
151
+
152
+ async function saveConfig() {
153
+ const payload = {
154
+ key: nullIfEmpty(byId("cfg-key").value),
155
+ model: nullIfEmpty(byId("cfg-model").value),
156
+ sampler: nullIfEmpty(byId("cfg-sampler").value),
157
+ steps: byId("cfg-steps").value ? Number(byId("cfg-steps").value) : null,
158
+ scale: byId("cfg-scale").value ? Number(byId("cfg-scale").value) : null,
159
+ cfg_rescale: byId("cfg-cfg-rescale").value ? Number(byId("cfg-cfg-rescale").value) : null,
160
+ noise_schedule: nullIfEmpty(byId("cfg-noise-schedule").value),
161
+ uc_preset: byId("cfg-uc-preset").value ? Number(byId("cfg-uc-preset").value) : null,
162
+ quality_toggle: byId("cfg-quality-toggle").checked,
163
+ legacy_uc: byId("cfg-legacy-uc").checked,
164
+ port: byId("cfg-port").value ? Number(byId("cfg-port").value) : null,
165
+ save_output: byId("cfg-save-output").checked,
166
+ output_dir: nullIfEmpty(byId("cfg-output-dir")?.value),
167
+ color_scheme: nullIfEmpty(byId("cfg-color-scheme")?.value),
168
+ custom_primary: nullIfEmpty(byId("cfg-custom-primary")?.value),
169
+ sound_enabled: !!soundEnabled,
170
+ sound_url: nullIfEmpty(soundUrl),
171
+ };
172
+
173
+ try {
174
+ loading.show();
175
+ const res = await fetch("/api/config", {
176
+ method: "PUT",
177
+ headers: { "Content-Type": "application/json" },
178
+ body: JSON.stringify(payload),
179
+ });
180
+ if (!res.ok) throw new Error(await res.text());
181
+ await res.json();
182
+ byId("cfg-message").textContent = "保存成功";
183
+ toast("配置已保存", "success");
184
+ } catch (err) {
185
+ byId("cfg-message").textContent = "保存失败:" + err.message;
186
+ toast("保存配置失败", "error");
187
+ } finally {
188
+ loading.hide();
189
+ }
190
+ }
191
+
192
+ // File -> Base64 (strip data URL prefix)
193
+ function fileToBase64(file) {
194
+ return new Promise((resolve, reject) => {
195
+ const reader = new FileReader();
196
+ reader.onload = () => {
197
+ const result = reader.result || "";
198
+ const idx = String(result).indexOf(",");
199
+ resolve(idx >= 0 ? String(result).slice(idx + 1) : String(result));
200
+ };
201
+ reader.onerror = reject;
202
+ reader.readAsDataURL(file);
203
+ });
204
+ }
205
+
206
+ // Ensure x64
207
+ function ensureX64(n) {
208
+ const val = Number(n || 0);
209
+ if (!val || val <= 64) return 64;
210
+ if (val % 64 === 0) return val;
211
+ const fract = (val / 64) % 1;
212
+ return fract >= 0.5 ? (Math.floor(val / 64) + 1) * 64 : Math.floor(val / 64) * 64;
213
+ }
214
+
215
+ // ===== 批量/并发与渲染辅助 =====
216
+ function ensureCountField(tabId, inputId) {
217
+ if (byId(inputId)) return;
218
+ const form = byId(tabId)?.querySelector(".form-grid");
219
+ if (!form) return;
220
+ const label = document.createElement("label");
221
+ const span = document.createElement("span");
222
+ span.textContent = "数量";
223
+ const input = document.createElement("input");
224
+ input.type = "number";
225
+ input.min = "1";
226
+ input.max = "8";
227
+ input.step = "1";
228
+ input.value = "1";
229
+ input.id = inputId;
230
+ label.appendChild(span);
231
+ label.appendChild(input);
232
+ form.appendChild(label);
233
+ }
234
+ function ensureGrid(tabId, gridId, singleImgId) {
235
+ const section = byId(tabId);
236
+ if (!section) return null;
237
+ const res = section.querySelector(".result");
238
+ if (!res) return null;
239
+ let grid = byId(gridId);
240
+ if (!grid) {
241
+ grid = document.createElement("div");
242
+ grid.id = gridId;
243
+ res.prepend(grid);
244
+ }
245
+ grid.style.display = "grid";
246
+ grid.style.gridTemplateColumns = "repeat(auto-fill, minmax(160px, 1fr))";
247
+ grid.style.gap = "10px";
248
+ const single = byId(singleImgId);
249
+ if (single) single.style.display = "none";
250
+ return grid;
251
+ }
252
+ function setBusy(btn, busy, busyText = "生成中...") {
253
+ if (!btn) return;
254
+ if (busy) {
255
+ btn.setAttribute("data-text", btn.textContent || "");
256
+ btn.textContent = busyText;
257
+ btn.disabled = true;
258
+ } else {
259
+ const t = btn.getAttribute("data-text");
260
+ if (t !== null) btn.textContent = t;
261
+ btn.disabled = false;
262
+ }
263
+ }
264
+ function getCount(inputId, def = 1) {
265
+ const n = Number(byId(inputId)?.value || def);
266
+ return Math.max(1, Math.min(8, isNaN(n) ? def : n));
267
+ }
268
+
269
+ // T2I
270
+ async function handleT2I() {
271
+ const payloadBase = {
272
+ prompt: byId("t2i-prompt").value || "",
273
+ negative: byId("t2i-negative").value || "",
274
+ width: ensureX64(byId("t2i-width").value || 768),
275
+ height: ensureX64(byId("t2i-height").value || 768),
276
+ steps: byId("t2i-steps").value ? Number(byId("t2i-steps").value) : null,
277
+ scale: byId("t2i-scale").value ? Number(byId("t2i-scale").value) : null,
278
+ sampler: nullIfEmpty(byId("t2i-sampler").value),
279
+ noise_schedule: nullIfEmpty(byId("t2i-noise-schedule").value),
280
+ seed: byId("t2i-seed").value ? Number(byId("t2i-seed").value) : -1,
281
+ variety: byId("t2i-variety").checked,
282
+ decrisp: byId("t2i-decrisp").checked,
283
+ cfg_rescale: byId("t2i-cfg-rescale").value ? Number(byId("t2i-cfg-rescale").value) : null,
284
+ };
285
+
286
+ if (!payloadBase.prompt.trim()) {
287
+ toast("请填写正面提示词", "error");
288
+ return;
289
+ }
290
+
291
+ const btn = byId("btn-t2i");
292
+ setBusy(btn, true);
293
+ const count = getCount("t2i-count", 1);
294
+
295
+ try {
296
+ const tasks = Array.from({ length: count }, async () => {
297
+ const res = await fetch("/api/generate/t2i", {
298
+ method: "POST",
299
+ headers: { "Content-Type": "application/json" },
300
+ body: JSON.stringify(payloadBase),
301
+ });
302
+ const txt = await res.text();
303
+ let body;
304
+ try { body = JSON.parse(txt); } catch { throw new Error(txt); }
305
+ if (!res.ok) throw new Error(body?.detail || "生成失败");
306
+ return body;
307
+ });
308
+
309
+ const settled = await Promise.allSettled(tasks);
310
+ const oks = settled.filter(s => s.status === "fulfilled").map(s => s.value);
311
+ if (!oks.length) throw settled[0]?.reason || new Error("生成失败");
312
+
313
+ const grid = ensureGrid("tab-t2i", "t2i-grid", "t2i-img");
314
+ if (grid) {
315
+ grid.innerHTML = "";
316
+ oks.forEach(o => {
317
+ const im = document.createElement("img");
318
+ im.src = o.image_base64;
319
+ im.alt = "生成结果";
320
+ im.style.width = "100%";
321
+ im.style.borderRadius = "12px";
322
+ im.style.border = "1px solid var(--border)";
323
+ grid.appendChild(im);
324
+ });
325
+ } else {
326
+ const imgEl = byId("t2i-img");
327
+ if (imgEl) imgEl.src = oks[0].image_base64;
328
+ }
329
+ const paths = oks.map(o => o.saved_path || "").filter(Boolean).join(" | ");
330
+ if (byId("t2i-saved")) byId("t2i-saved").value = paths;
331
+ toast(`生成成功 ${oks.length} 张`, "success");
332
+ } catch (err) {
333
+ toast(String(err.message || err), "error");
334
+ } finally {
335
+ setBusy(btn, false);
336
+ }
337
+ }
338
+
339
+ // I2I
340
+ async function handleI2I() {
341
+ const file = byId("i2i-image").files?.[0];
342
+ if (!file) {
343
+ toast("请上传输入图片", "error");
344
+ return;
345
+ }
346
+ const image_base64 = await fileToBase64(file);
347
+
348
+ const payloadBase = {
349
+ positive: byId("i2i-positive").value || "",
350
+ negative: byId("i2i-negative").value || "",
351
+ image_base64,
352
+ width: byId("i2i-width").value ? ensureX64(byId("i2i-width").value) : null,
353
+ height: byId("i2i-height").value ? ensureX64(byId("i2i-height").value) : null,
354
+ steps: byId("i2i-steps").value ? Number(byId("i2i-steps").value) : null,
355
+ scale: byId("i2i-scale").value ? Number(byId("i2i-scale").value) : null,
356
+ sampler: nullIfEmpty(byId("i2i-sampler").value),
357
+ noise_schedule: nullIfEmpty(byId("i2i-noise-schedule").value),
358
+ seed: byId("i2i-seed").value ? Number(byId("i2i-seed").value) : -1,
359
+ strength: byId("i2i-strength").value ? Number(byId("i2i-strength").value) : 0.5,
360
+ noise: byId("i2i-noise").value ? Number(byId("i2i-noise").value) : 0.0,
361
+ variety: byId("i2i-variety").checked,
362
+ decrisp: byId("i2i-decrisp").checked,
363
+ cfg_rescale: byId("i2i-cfg-rescale").value ? Number(byId("i2i-cfg-rescale").value) : null,
364
+ };
365
+
366
+ if (!payloadBase.positive.trim()) {
367
+ toast("请填写正面提示词", "error");
368
+ return;
369
+ }
370
+
371
+ const btn = byId("btn-i2i");
372
+ setBusy(btn, true);
373
+ const count = getCount("i2i-count", 1);
374
+
375
+ try {
376
+ const tasks = Array.from({ length: count }, async () => {
377
+ const res = await fetch("/api/generate/i2i", {
378
+ method: "POST",
379
+ headers: { "Content-Type": "application/json" },
380
+ body: JSON.stringify(payloadBase),
381
+ });
382
+ const txt = await res.text();
383
+ let body;
384
+ try { body = JSON.parse(txt); } catch { throw new Error(txt); }
385
+ if (!res.ok) throw new Error(body?.detail || "生成失败");
386
+ return body;
387
+ });
388
+
389
+ const settled = await Promise.allSettled(tasks);
390
+ const oks = settled.filter(s => s.status === "fulfilled").map(s => s.value);
391
+ if (!oks.length) throw settled[0]?.reason || new Error("生成失败");
392
+
393
+ const grid = ensureGrid("tab-i2i", "i2i-grid", "i2i-img");
394
+ if (grid) {
395
+ grid.innerHTML = "";
396
+ oks.forEach(o => {
397
+ const im = document.createElement("img");
398
+ im.src = o.image_base64;
399
+ im.alt = "生成结果";
400
+ im.style.width = "100%";
401
+ im.style.borderRadius = "12px";
402
+ im.style.border = "1px solid var(--border)";
403
+ grid.appendChild(im);
404
+ });
405
+ } else {
406
+ const imgEl = byId("i2i-img");
407
+ if (imgEl) imgEl.src = oks[0].image_base64;
408
+ }
409
+ const paths = oks.map(o => o.saved_path || "").filter(Boolean).join(" | ");
410
+ if (byId("i2i-saved")) byId("i2i-saved").value = paths;
411
+ toast(`生成成功 ${oks.length} 张`, "success");
412
+ } catch (err) {
413
+ toast(String(err.message || err), "error");
414
+ } finally {
415
+ setBusy(btn, false);
416
+ }
417
+ }
418
+
419
+ // Inpaint
420
+ async function handleInpaint() {
421
+ const imgFile = byId("inpaint-image").files?.[0];
422
+ const maskFile = byId("inpaint-mask").files?.[0];
423
+ if (!imgFile || !maskFile) {
424
+ toast("请上传底图与遮罩", "error");
425
+ return;
426
+ }
427
+ const image_base64 = await fileToBase64(imgFile);
428
+ const mask_base64 = await fileToBase64(maskFile);
429
+
430
+ const payloadBase = {
431
+ positive: byId("inpaint-positive").value || "",
432
+ negative: byId("inpaint-negative").value || "",
433
+ image_base64,
434
+ mask_base64,
435
+ add_original_image: byId("inpaint-add-original").checked,
436
+ width: byId("inpaint-width").value ? ensureX64(byId("inpaint-width").value) : null,
437
+ height: byId("inpaint-height").value ? ensureX64(byId("inpaint-height").value) : null,
438
+ steps: byId("inpaint-steps").value ? Number(byId("inpaint-steps").value) : null,
439
+ scale: byId("inpaint-scale").value ? Number(byId("inpaint-scale").value) : null,
440
+ sampler: nullIfEmpty(byId("inpaint-sampler").value),
441
+ noise_schedule: nullIfEmpty(byId("inpaint-noise-schedule").value),
442
+ seed: byId("inpaint-seed").value ? Number(byId("inpaint-seed").value) : -1,
443
+ strength: byId("inpaint-strength").value ? Number(byId("inpaint-strength").value) : 0.5,
444
+ noise: byId("inpaint-noise-val").value ? Number(byId("inpaint-noise-val").value) : 0.0,
445
+ variety: byId("inpaint-variety").checked,
446
+ decrisp: byId("inpaint-decrisp").checked,
447
+ cfg_rescale: byId("inpaint-cfg-rescale").value ? Number(byId("inpaint-cfg-rescale").value) : null,
448
+ };
449
+
450
+ if (!payloadBase.positive.trim()) {
451
+ toast("请填写 Positive", "error");
452
+ return;
453
+ }
454
+
455
+ const btn = byId("btn-inpaint");
456
+ setBusy(btn, true);
457
+ const count = getCount("inpaint-count", 1);
458
+
459
+ try {
460
+ const tasks = Array.from({ length: count }, async () => {
461
+ const res = await fetch("/api/generate/inpaint", {
462
+ method: "POST",
463
+ headers: { "Content-Type": "application/json" },
464
+ body: JSON.stringify(payloadBase),
465
+ });
466
+ const txt = await res.text();
467
+ let body;
468
+ try { body = JSON.parse(txt); } catch { throw new Error(txt); }
469
+ if (!res.ok) throw new Error(body?.detail || "生成失败");
470
+ return body;
471
+ });
472
+
473
+ const settled = await Promise.allSettled(tasks);
474
+ const oks = settled.filter(s => s.status === "fulfilled").map(s => s.value);
475
+ if (!oks.length) throw settled[0]?.reason || new Error("生成失败");
476
+
477
+ const grid = ensureGrid("tab-inpaint", "inpaint-grid", "inpaint-img");
478
+ if (grid) {
479
+ grid.innerHTML = "";
480
+ oks.forEach(o => {
481
+ const im = document.createElement("img");
482
+ im.src = o.image_base64;
483
+ im.alt = "生成结果";
484
+ im.style.width = "100%";
485
+ im.style.borderRadius = "12px";
486
+ im.style.border = "1px solid var(--border)";
487
+ grid.appendChild(im);
488
+ });
489
+ } else {
490
+ const imgEl = byId("inpaint-img");
491
+ if (imgEl) imgEl.src = oks[0].image_base64;
492
+ }
493
+ const paths = oks.map(o => o.saved_path || "").filter(Boolean).join(" | ");
494
+ if (byId("inpaint-saved")) byId("inpaint-saved").value = paths;
495
+ toast(`生成成功 ${oks.length} 张`, "success");
496
+ } catch (err) {
497
+ toast(String(err.message || err), "error");
498
+ } finally {
499
+ setBusy(btn, false);
500
+ }
501
+ }
502
+
503
+ // Bindings
504
+ // 为每个 Tab 注入“数量”输入(默认 1,可调至 8)
505
+ ensureCountField("tab-t2i", "t2i-count");
506
+ ensureCountField("tab-i2i", "i2i-count");
507
+ ensureCountField("tab-inpaint", "inpaint-count");
508
+
509
+ byId("btn-load-config").addEventListener("click", loadConfig);
510
+ byId("btn-save-config").addEventListener("click", saveConfig);
511
+ const selOutBtn = byId("btn-select-output-dir");
512
+ if (selOutBtn) {
513
+ selOutBtn.addEventListener("click", async () => {
514
+ try {
515
+ loading.show();
516
+ const res = await fetch("/api/select-output-dir");
517
+ let body = {};
518
+ try { body = await res.json(); } catch {}
519
+ if (!res.ok) {
520
+ // 失败兜底:提示手动输入,并尝试打开当前保存目录帮助定位
521
+ toast(body?.detail || "选择失败,请手动在输入框填入保存目录", "error");
522
+ try {
523
+ await fetch("/api/open-dir", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ path: byId("cfg-output-dir")?.value || "" }) });
524
+ } catch {}
525
+ return;
526
+ }
527
+ const p = String(body?.path || "");
528
+ if (p) {
529
+ const input = byId("cfg-output-dir");
530
+ if (input) input.value = p;
531
+ toast("已选择保存目录", "success");
532
+ } else {
533
+ toast("未选择任何目录", "info");
534
+ }
535
+ } catch (e) {
536
+ toast(String(e?.message || e), "error");
537
+ } finally {
538
+ loading.hide();
539
+ }
540
+ });
541
+ }
542
+ const openOutBtn = byId("btn-open-output-dir");
543
+ if (openOutBtn) {
544
+ openOutBtn.addEventListener("click", async () => {
545
+ try {
546
+ loading.show();
547
+ const p = byId("cfg-output-dir")?.value || "";
548
+ const res = await fetch("/api/open-dir", {
549
+ method: "POST",
550
+ headers: { "Content-Type": "application/json" },
551
+ body: JSON.stringify({ path: p }),
552
+ });
553
+ const body = await res.json().catch(() => ({}));
554
+ if (!res.ok) throw new Error(body?.detail || "打开目录失败");
555
+ toast("已打开保存目录", "success");
556
+ } catch (e) {
557
+ toast(String(e?.message || e), "error");
558
+ } finally {
559
+ loading.hide();
560
+ }
561
+ });
562
+ }
563
+ // 颜色方案即时预览(无需保存即可体验)
564
+ const schemeSel = byId("cfg-color-scheme");
565
+ const customColor = byId("cfg-custom-primary");
566
+ if (schemeSel) {
567
+ schemeSel.addEventListener("change", () => {
568
+ applyBackgroundScheme(schemeSel.value || "auto", customColor?.value || "");
569
+ });
570
+ }
571
+ if (customColor) {
572
+ customColor.addEventListener("input", () => {
573
+ applyBackgroundScheme("custom", customColor.value || "#7ba23f");
574
+ });
575
+ }
576
+ byId("btn-t2i").addEventListener("click", handleT2I);
577
+ byId("btn-i2i").addEventListener("click", handleI2I);
578
+ byId("btn-inpaint").addEventListener("click", handleInpaint);
579
+
580
+ // Init
581
+ loadConfig();
582
+ })();
frontend/assets/style.css ADDED
@@ -0,0 +1,508 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ :root {
2
+ /* 将浅色作为默认,切换立即生效;暗色在 [data-theme="dark"] 中覆盖 */
3
+ --bg: #f5f7fb;
4
+ --panel: #ffffff;
5
+ --text: #111;
6
+ --muted: #5b6470;
7
+ /* 浅色用黑色 */
8
+ --primary: #000000;
9
+ --primary-600: #222222;
10
+ --danger: #e95353;
11
+ --success: #22b66b;
12
+ --border: #e6e8ef;
13
+ --card: #ffffff;
14
+ --shadow: 0 10px 30px rgba(0,0,0,0.15);
15
+ --radius: 12px;
16
+ /* 统一焦点环与激活文字颜色 */
17
+ --ring: rgba(0,0,0,0.15);
18
+ --active-text: #ffffff;
19
+ --switch-knob: #ffffff;
20
+ }
21
+
22
+ * { box-sizing: border-box; }
23
+ html, body { height: 100%; }
24
+ html { scrollbar-gutter: stable both-edges; } /* 固定滚动条占位,避免切换/加载导致布局左右抖动 */
25
+ body {
26
+ margin: 0;
27
+ background: var(--bg);
28
+ color: var(--text);
29
+ font: 16px/1.7 system-ui, -apple-system, Segoe UI, Roboto, Helvetica, Arial, "PingFang SC", "Hiragino Sans GB", "Microsoft YaHei", "WenQuanYi Micro Hei", sans-serif;
30
+ -webkit-font-smoothing: antialiased;
31
+ -moz-osx-font-smoothing: grayscale;
32
+ }
33
+
34
+ img { max-width: 100%; display: block; }
35
+
36
+ .app-header {
37
+ position: fixed; /* 彻底固定选项栏(不随滚动、不跳动) */
38
+ top: 0;
39
+ left: 0;
40
+ right: 0;
41
+ width: 100%;
42
+ z-index: 50;
43
+ display: flex;
44
+ gap: 12px;
45
+ align-items: center;
46
+ justify-content: space-between;
47
+ padding: 10px 14px;
48
+ background: rgba(10,12,18,0.75);
49
+ backdrop-filter: blur(8px);
50
+ border-bottom: 1px solid var(--border);
51
+ }
52
+
53
+ .brand {
54
+ font-weight: 700;
55
+ letter-spacing: 0.5px;
56
+ font-size: 16px;
57
+ color: var(--text);
58
+ }
59
+
60
+ .tabs {
61
+ display: inline-flex;
62
+ gap: 8px;
63
+ overflow-x: auto;
64
+ -webkit-overflow-scrolling: touch;
65
+ }
66
+
67
+ /* 头部右侧操作区(主题/提示音) */
68
+ .header-actions {
69
+ display: inline-flex;
70
+ gap: 8px;
71
+ flex-shrink: 0;
72
+ }
73
+
74
+ .tab-btn {
75
+ appearance: none;
76
+ background: transparent;
77
+ color: var(--text);
78
+ border: 1px solid var(--border);
79
+ padding: 10px 14px;
80
+ border-radius: 999px;
81
+ white-space: nowrap;
82
+ /* 彻底固定:移除过渡,避免任何切换动画引起的视觉“移动” */
83
+ transition: none;
84
+ cursor: pointer;
85
+ font-weight: 700;
86
+ }
87
+ .tab-btn:hover { color: var(--text); border-color: var(--border); }
88
+ .tab-btn.active {
89
+ background: var(--primary);
90
+ color: var(--active-text);
91
+ border-color: transparent;
92
+ box-shadow: none;
93
+ }
94
+
95
+ main {
96
+ max-width: 1280px;
97
+ margin: 24px auto;
98
+ /* 预留固定头部高度,避免切换/滚动产生顶端位移 */
99
+ padding: 64px 20px 80px;
100
+ }
101
+
102
+ .tab { display: none; }
103
+ .tab.active { display: block; }
104
+
105
+ h2 {
106
+ margin: 10px 0 14px;
107
+ font-size: 18px;
108
+ font-weight: 700;
109
+ }
110
+
111
+ .card {
112
+ background: var(--panel);
113
+ border: 1px solid var(--border);
114
+ border-radius: var(--radius);
115
+ box-shadow: var(--shadow);
116
+ padding: 14px;
117
+ }
118
+
119
+ /* Form grid - mobile first */
120
+ .form-grid {
121
+ display: grid;
122
+ grid-template-columns: 1fr;
123
+ gap: 12px;
124
+ }
125
+
126
+ /* Larger screens */
127
+ @media (min-width: 640px) {
128
+ .form-grid {
129
+ grid-template-columns: repeat(2, 1fr);
130
+ }
131
+ }
132
+ @media (min-width: 960px) {
133
+ .form-grid {
134
+ grid-template-columns: repeat(3, 1fr);
135
+ }
136
+ }
137
+ @media (min-width: 1200px) {
138
+ .form-grid {
139
+ grid-template-columns: repeat(4, 1fr);
140
+ }
141
+ }
142
+
143
+ label {
144
+ display: grid;
145
+ gap: 6px;
146
+ color: var(--text);
147
+ font-size: 14px;
148
+ font-weight: 600;
149
+ }
150
+
151
+ label.full {
152
+ grid-column: 1 / -1;
153
+ }
154
+
155
+ input[type="text"],
156
+ input[type="password"],
157
+ input[type="number"],
158
+ textarea,
159
+ select {
160
+ width: 100%;
161
+ padding: 12px 14px;
162
+ background: var(--card);
163
+ color: var(--text);
164
+ border: 1px solid var(--border);
165
+ border-radius: 12px;
166
+ outline: none;
167
+ transition: border-color .18s ease, box-shadow .18s ease;
168
+ box-shadow: inset 0 1px 0 rgba(255,255,255,0.03);
169
+ }
170
+ textarea { min-height: 84px; resize: vertical; }
171
+
172
+ input:focus,
173
+ textarea:focus,
174
+ select:focus {
175
+ border-color: var(--primary-600);
176
+ box-shadow: 0 0 0 3px var(--ring);
177
+ }
178
+
179
+ /* Switch */
180
+ .switch {
181
+ display: flex;
182
+ align-items: center;
183
+ gap: 10px;
184
+ }
185
+ .switch input[type="checkbox"] {
186
+ appearance: none;
187
+ width: 44px;
188
+ height: 26px;
189
+ border-radius: 999px;
190
+ background: #2a3040;
191
+ position: relative;
192
+ outline: none;
193
+ border: 1px solid var(--border);
194
+ transition: background .18s ease, border-color .18s ease;
195
+ }
196
+ .switch input[type="checkbox"]::after {
197
+ content: "";
198
+ position: absolute;
199
+ top: 2px; left: 2px;
200
+ width: 20px; height: 20px;
201
+ border-radius: 50%;
202
+ background: var(--switch-knob);
203
+ transition: transform .18s ease, background .18s ease;
204
+ }
205
+ .switch input[type="checkbox"]:checked {
206
+ background: var(--primary);
207
+ border-color: transparent;
208
+ }
209
+ .switch input[type="checkbox"]:checked::after {
210
+ transform: translateX(18px);
211
+ background: var(--switch-knob-checked);
212
+ }
213
+
214
+ /* Actions */
215
+ .actions {
216
+ display: flex;
217
+ flex-wrap: wrap;
218
+ gap: 10px;
219
+ padding-top: 4px;
220
+ }
221
+
222
+ button {
223
+ appearance: none;
224
+ border: 1px solid var(--border);
225
+ background: #1a2030;
226
+ color: #e8ecff;
227
+ border-radius: 12px;
228
+ padding: 12px 16px;
229
+ font-weight: 800;
230
+ font-size: 16px;
231
+ letter-spacing: .2px;
232
+ cursor: pointer;
233
+ /* 去除 transform 过渡,避免点击“抽搐” */
234
+ transition: background .18s ease, border-color .18s ease, box-shadow .2s ease, color .18s ease;
235
+ }
236
+ button:hover { border-color: #3a3a3a; background: #222222; }
237
+ button:active { opacity: 0.94; }
238
+
239
+ /* 可键盘操作的聚焦态 */
240
+ button:focus-visible,
241
+ .tab-btn:focus-visible {
242
+ outline: none;
243
+ box-shadow: 0 0 0 3px var(--ring);
244
+ border-color: var(--primary-600);
245
+ }
246
+
247
+ button.primary {
248
+ /* 使用主题主色:浅色=黑,深色=白 */
249
+ background: var(--primary);
250
+ border-color: transparent;
251
+ color: var(--active-text) !important;
252
+ text-shadow: none;
253
+ box-shadow: none;
254
+ }
255
+ button.primary:hover {
256
+ filter: brightness(0.92);
257
+ }
258
+
259
+ /* Result */
260
+ .result {
261
+ margin-top: 14px;
262
+ display: grid;
263
+ gap: 12px;
264
+ }
265
+ .result img {
266
+ width: 100%;
267
+ border-radius: 12px;
268
+ border: 1px solid var(--border);
269
+ background: #0e1118;
270
+ }
271
+
272
+ .saved-path {
273
+ display: grid;
274
+ grid-template-columns: 120px 1fr;
275
+ gap: 10px;
276
+ align-items: center;
277
+ }
278
+ .saved-path label {
279
+ margin: 0;
280
+ color: var(--muted);
281
+ }
282
+ .saved-path input {
283
+ width: 100%;
284
+ }
285
+
286
+ /* Toast: 固定状态栏,不弹出动画 */
287
+ .toast {
288
+ position: fixed;
289
+ left: 50%;
290
+ bottom: 14px;
291
+ transform: translateX(-50%); /* 不再上下弹出 */
292
+ background: rgba(17, 23, 34, 0.96);
293
+ color: #fff;
294
+ border: 1px solid var(--border);
295
+ padding: 8px 14px;
296
+ border-radius: 10px;
297
+ box-shadow: var(--shadow);
298
+ z-index: 100;
299
+ transition: none;
300
+ max-width: 92%;
301
+ white-space: nowrap;
302
+ overflow: hidden;
303
+ text-overflow: ellipsis;
304
+ }
305
+ .toast:empty { display: none; }
306
+ .toast.success { border-color: rgba(40, 199, 111, .4); }
307
+ .toast.error { border-color: rgba(255, 93, 93, .4); }
308
+
309
+ /* Loading overlay */
310
+ .loading.hidden { display: none; }
311
+ .loading {
312
+ position: fixed;
313
+ inset: 0;
314
+ background: rgba(11,13,18,0.7);
315
+ display: grid;
316
+ place-items: center;
317
+ z-index: 200;
318
+ }
319
+ .spinner {
320
+ width: 46px; height: 46px;
321
+ border-radius: 50%;
322
+ border: 4px solid rgba(255,255,255,0.12);
323
+ border-top-color: var(--primary-600);
324
+ animation: spin 0.8s linear infinite;
325
+ margin: 0 auto 8px;
326
+ }
327
+ .loading-text {
328
+ color: var(--muted);
329
+ font-weight: 600;
330
+ text-align: center;
331
+ }
332
+
333
+ @keyframes spin {
334
+ to { transform: rotate(360deg); }
335
+ }
336
+
337
+ /* Improve mobile tap targets */
338
+ input[type="file"] {
339
+ padding: 10px;
340
+ background: var(--card);
341
+ border-radius: 10px;
342
+ border: 1px solid var(--border);
343
+ }
344
+
345
+ /* ===== 主题变量与覆盖 ===== */
346
+ /* 深色为默认 */
347
+ :root,
348
+ [data-theme="dark"] {
349
+ /* 深色:去蓝化,统一深灰系 */
350
+ --bg: #121315;
351
+ --panel: #1a1a1a;
352
+ --text: #e6e6e6;
353
+ --muted: #9a9a9a;
354
+ /* 深色主按钮前景仍使用白色/黑色对比色 */
355
+ --primary: #ffffff;
356
+ --primary-600: #e6e6e6;
357
+ --danger: #ff5d5d;
358
+ --success: #28c76f;
359
+ --border: #2a2a2a;
360
+ --card: #121212;
361
+ --ring: rgba(255,255,255,0.12);
362
+ --active-text: #111111;
363
+ --switch-knob-checked: #1a1a1a;
364
+ }
365
+
366
+ /* 浅色主题 */
367
+ [data-theme="light"] {
368
+ --bg: #f5f7fb;
369
+ --panel: #ffffff;
370
+ --text: #222;
371
+ --muted: #5b6470;
372
+ /* 浅色用黑色 */
373
+ --primary: #000000;
374
+ --primary-600: #222222;
375
+ --danger: #e95353;
376
+ --success: #22b66b;
377
+ --border: #e6e8ef;
378
+ --card: #ffffff;
379
+ --ring: rgba(0,0,0,0.15);
380
+ --active-text: #ffffff;
381
+ --switch-knob-checked: #ffffff;
382
+ }
383
+
384
+ /* 浅色覆盖 */
385
+ [data-theme="light"] .app-header {
386
+ background: rgba(255,255,255,0.8);
387
+ border-bottom-color: var(--border);
388
+ }
389
+ [data-theme="light"] .tab-btn {
390
+ color: var(--muted);
391
+ border-color: var(--border);
392
+ background: #fff;
393
+ }
394
+ [data-theme="light"] .tab-btn.active {
395
+ background: var(--primary);
396
+ color: var(--active-text);
397
+ border-color: transparent;
398
+ }
399
+ [data-theme="light"] button {
400
+ background: #ffffff;
401
+ color: #1a1a1a;
402
+ border-color: var(--border);
403
+ }
404
+ [data-theme="light"] button:hover {
405
+ background: #f6f7fb;
406
+ border-color: #d6dbe8;
407
+ }
408
+ [data-theme="light"] button.primary {
409
+ color: var(--active-text) !important;
410
+ }
411
+ [data-theme="light"] .result img {
412
+ background: #fff;
413
+ border-color: var(--border);
414
+ }
415
+ [data-theme="light"] .toast {
416
+ background: rgba(255,255,255,0.96);
417
+ color: #222;
418
+ }
419
+
420
+ /* 主要按钮文字颜色由 --active-text 控制(浅色=白、深色=黑) */
421
+
422
+ /* ===== 可读性增强与无障碍优化 ===== */
423
+ input::placeholder,
424
+ textarea::placeholder {
425
+ color: var(--placeholder);
426
+ opacity: 1;
427
+ }
428
+ select,
429
+ option {
430
+ color: var(--text);
431
+ background: var(--card);
432
+ }
433
+ input,
434
+ textarea,
435
+ select {
436
+ caret-color: var(--primary);
437
+ font-size: 15px;
438
+ }
439
+ .message {
440
+ color: var(--muted);
441
+ }
442
+
443
+ /* 主题占位符颜色 */
444
+ :root,
445
+ [data-theme="dark"] {
446
+ --placeholder: #b8c1d1;
447
+ }
448
+ [data-theme="light"] {
449
+ --placeholder: #8a93a8;
450
+ }
451
+
452
+ /* ===== 移动端优化(参考 NAI 生成页交互) ===== */
453
+ @media (max-width: 640px) {
454
+ /* 底部固定操作栏:当前 Tab 的 actions 固定在底部,仅当前 Tab 可见 */
455
+ .actions {
456
+ position: fixed;
457
+ left: 0;
458
+ right: 0;
459
+ bottom: 0;
460
+ padding: 10px 16px;
461
+ background: var(--panel);
462
+ border-top: 1px solid var(--border);
463
+ z-index: 80;
464
+ }
465
+ .actions .primary { width: 100%; }
466
+
467
+ /* 预留底部空间,避免内容被按钮遮挡;并为顶部两行导航预留高度 */
468
+ main {
469
+ padding-bottom: 120px;
470
+ padding-top: 112px; /* 顶部两行(品牌/按钮 + 可横向滚动的 tabs) */
471
+ }
472
+
473
+ /* 顶部改为两行,选项(文生图/图生图/局部重绘)单独一行并可横向滚动,避免“找不到” */
474
+ .app-header {
475
+ padding: 8px 10px;
476
+ flex-direction: column;
477
+ align-items: stretch;
478
+ gap: 6px;
479
+ }
480
+ .header-actions {
481
+ order: 2;
482
+ align-self: flex-end;
483
+ }
484
+ .tabs {
485
+ order: 3;
486
+ display: flex;
487
+ width: 100%;
488
+ overflow-x: auto;
489
+ -webkit-overflow-scrolling: touch;
490
+ gap: 8px;
491
+ padding-bottom: 2px;
492
+ }
493
+ .tabs .tab-btn {
494
+ flex: 0 0 auto;
495
+ padding: 8px 10px;
496
+ font-size: 14px;
497
+ }
498
+
499
+ /* 触控表单更易点按 */
500
+ input[type="text"],
501
+ input[type="password"],
502
+ input[type="number"],
503
+ textarea,
504
+ select {
505
+ padding: 14px 16px;
506
+ font-size: 16px;
507
+ }
508
+ }
frontend/index.html ADDED
@@ -0,0 +1,414 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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,viewport-fit=cover" />
6
+ <title>New NAI - 本地可视化前端</title>
7
+ <link rel="stylesheet" href="assets/style.css" />
8
+ </head>
9
+ <body>
10
+ <header class="app-header">
11
+ <div class="brand">New NAI</div>
12
+ <nav class="tabs">
13
+ <button class="tab-btn active" data-tab="tab-config">配置</button>
14
+ <button class="tab-btn" data-tab="tab-t2i">文生图</button>
15
+ <button class="tab-btn" data-tab="tab-i2i">图生图</button>
16
+ <button class="tab-btn" data-tab="tab-inpaint">局部重绘</button>
17
+ </nav>
18
+ <div class="header-actions">
19
+ <button id="theme-toggle" class="tab-btn">主题:深色</button>
20
+ <button id="sound-toggle" class="tab-btn" aria-pressed="false">提示音:关</button>
21
+ </div>
22
+ </header>
23
+
24
+ <main>
25
+ <!-- 配置 -->
26
+ <section id="tab-config" class="tab active">
27
+ <h2>配置设置</h2>
28
+ <div class="card">
29
+ <div class="form-grid">
30
+ <label>
31
+ <span>Key(原 Token)</span>
32
+ <input id="cfg-key" type="password" placeholder="在此填入 NovelAI Key" autocomplete="off" />
33
+ </label>
34
+ <label>
35
+ <span>模型</span>
36
+ <select id="cfg-model">
37
+ <option value="nai-diffusion-3">nai-diffusion-3</option>
38
+ <option value="nai-diffusion-furry-3">nai-diffusion-furry-3</option>
39
+ <option value="nai-diffusion-4-curated-preview">nai-diffusion-4-curated-preview</option>
40
+ <option value="nai-diffusion-4-full">nai-diffusion-4-full</option>
41
+ <option value="nai-diffusion-4-5-curated">nai-diffusion-4-5-curated</option>
42
+ <option value="nai-diffusion-4-5-full">nai-diffusion-4-5-full</option>
43
+ </select>
44
+ </label>
45
+ <label>
46
+ <span>采样器</span>
47
+ <select id="cfg-sampler">
48
+ <option value="k_euler">k_euler</option>
49
+ <option value="k_euler_ancestral">k_euler_ancestral</option>
50
+ <option value="k_dpmpp_2s_ancestral">k_dpmpp_2s_ancestral</option>
51
+ <option value="k_dpmpp_2m">k_dpmpp_2m</option>
52
+ <option value="k_dpmpp_sde">k_dpmpp_sde</option>
53
+ <option value="k_dpmpp_2m_sde">k_dpmpp_2m_sde</option>
54
+ <option value="ddim_v3">ddim_v3</option>
55
+ </select>
56
+ </label>
57
+ <label>
58
+ <span>步数</span>
59
+ <input id="cfg-steps" type="number" min="1" step="1" placeholder="默认 28" />
60
+ </label>
61
+ <label>
62
+ <span>CFG 强度</span>
63
+ <input id="cfg-scale" type="number" min="0" step="0.1" placeholder="默认 5.0" />
64
+ </label>
65
+ <label>
66
+ <span>CFG 重缩放</span>
67
+ <input id="cfg-cfg-rescale" type="number" min="0" step="0.1" placeholder="默认 0" />
68
+ </label>
69
+ <label>
70
+ <span>噪声调度</span>
71
+ <select id="cfg-noise-schedule">
72
+ <option value="">(随模型/采样器自动)</option>
73
+ <option value="native">native</option>
74
+ <option value="karras">karras</option>
75
+ <option value="exponential">exponential</option>
76
+ <option value="polyexponential">polyexponential</option>
77
+ </select>
78
+ </label>
79
+ <label>
80
+ <span>UC 预设</span>
81
+ <input id="cfg-uc-preset" type="number" min="0" step="1" placeholder="默认 4" />
82
+ </label>
83
+ <label class="switch">
84
+ <input id="cfg-quality-toggle" type="checkbox" />
85
+ <span>质量优化</span>
86
+ </label>
87
+ <label class="switch">
88
+ <input id="cfg-legacy-uc" type="checkbox" />
89
+ <span>兼容旧版 UC</span>
90
+ </label>
91
+ <label>
92
+ <span>服务端口</span>
93
+ <input id="cfg-port" type="number" min="1" step="1" placeholder="默认 9180" />
94
+ </label>
95
+ <label class="switch">
96
+ <input id="cfg-save-output" type="checkbox" />
97
+ <span>保存到输出目录</span>
98
+ </label>
99
+ <label class="full">
100
+ <span>保存目录</span>
101
+ <input id="cfg-output-dir" type="text" placeholder="例如:D:\Pictures\NAI 输出(留空=默认 output)" />
102
+ </label>
103
+ <label>
104
+ <span>背景方案</span>
105
+ <select id="cfg-color-scheme">
106
+ <option value="auto">自动(浅=白/深=深灰)</option>
107
+ <option value="bamboo">竹子色</option>
108
+ <option value="custom">自定义</option>
109
+ </select>
110
+ </label>
111
+ <label>
112
+ <span>自定义背景色</span>
113
+ <input id="cfg-custom-primary" type="color" value="#7ba23f" />
114
+ </label>
115
+ </div>
116
+ <div class="actions">
117
+ <button id="btn-load-config">读取配置</button>
118
+ <button id="btn-select-output-dir">选择保存目录</button>
119
+ <button id="btn-open-output-dir">打开保存目录</button>
120
+ <button id="btn-save-config" class="primary">保存配置</button>
121
+ </div>
122
+ <p id="cfg-message" class="message"></p>
123
+ </div>
124
+ </section>
125
+
126
+ <!-- 文生图 -->
127
+ <section id="tab-t2i" class="tab">
128
+ <h2>文生图</h2>
129
+ <div class="card">
130
+ <div class="form-grid">
131
+ <label class="full">
132
+ <span>正面提示词</span>
133
+ <textarea id="t2i-prompt" rows="3" placeholder="输入提示词"></textarea>
134
+ </label>
135
+ <label class="full">
136
+ <span>负面提示词</span>
137
+ <textarea id="t2i-negative" rows="2" placeholder="可留空"></textarea>
138
+ </label>
139
+ <label>
140
+ <span>宽度</span>
141
+ <input id="t2i-width" type="number" min="64" step="64" value="768" />
142
+ </label>
143
+ <label>
144
+ <span>高度</span>
145
+ <input id="t2i-height" type="number" min="64" step="64" value="768" />
146
+ </label>
147
+ <label>
148
+ <span>步数</span>
149
+ <input id="t2i-steps" type="number" min="1" step="1" placeholder="留空=配置默认" />
150
+ </label>
151
+ <label>
152
+ <span>CFG 强度</span>
153
+ <input id="t2i-scale" type="number" min="0" step="0.1" placeholder="留空=配置默认" />
154
+ </label>
155
+ <label>
156
+ <span>采样器</span>
157
+ <select id="t2i-sampler">
158
+ <option value="">(配置默认)</option>
159
+ <option value="k_euler">k_euler</option>
160
+ <option value="k_euler_ancestral">k_euler_ancestral</option>
161
+ <option value="k_dpmpp_2s_ancestral">k_dpmpp_2s_ancestral</option>
162
+ <option value="k_dpmpp_2m">k_dpmpp_2m</option>
163
+ <option value="k_dpmpp_sde">k_dpmpp_sde</option>
164
+ <option value="k_dpmpp_2m_sde">k_dpmpp_2m_sde</option>
165
+ <option value="ddim_v3">ddim_v3</option>
166
+ </select>
167
+ </label>
168
+ <label>
169
+ <span>噪声调度</span>
170
+ <select id="t2i-noise-schedule">
171
+ <option value="">(自动)</option>
172
+ <option value="native">native</option>
173
+ <option value="karras">karras</option>
174
+ <option value="exponential">exponential</option>
175
+ <option value="polyexponential">polyexponential</option>
176
+ </select>
177
+ </label>
178
+ <label>
179
+ <span>随机种子</span>
180
+ <input id="t2i-seed" type="number" step="1" value="-1" />
181
+ </label>
182
+ <label class="switch">
183
+ <input id="t2i-variety" type="checkbox" />
184
+ <span>多样性</span>
185
+ </label>
186
+ <label class="switch">
187
+ <input id="t2i-decrisp" type="checkbox" />
188
+ <span>动态阈值</span>
189
+ </label>
190
+ <label>
191
+ <span>CFG 重缩放</span>
192
+ <input id="t2i-cfg-rescale" type="number" min="0" step="0.1" placeholder="留空=配置默认" />
193
+ </label>
194
+ </div>
195
+ <div class="actions">
196
+ <button id="btn-t2i" class="primary">生成</button>
197
+ </div>
198
+ <div class="result">
199
+ <img id="t2i-img" alt="生成结果" />
200
+ <div class="saved-path">
201
+ <label>保存路径</label>
202
+ <input id="t2i-saved" type="text" readonly />
203
+ </div>
204
+ </div>
205
+ </div>
206
+ </section>
207
+
208
+ <!-- 图生图 -->
209
+ <section id="tab-i2i" class="tab">
210
+ <h2>图生图</h2>
211
+ <div class="card">
212
+ <div class="form-grid">
213
+ <label class="full">
214
+ <span>正面提示词</span>
215
+ <textarea id="i2i-positive" rows="2" placeholder="输入正面提示词"></textarea>
216
+ </label>
217
+ <label class="full">
218
+ <span>负面提示词</span>
219
+ <textarea id="i2i-negative" rows="2" placeholder="可留空"></textarea>
220
+ </label>
221
+ <label class="full">
222
+ <span>输入图片</span>
223
+ <input id="i2i-image" type="file" accept="image/*" />
224
+ </label>
225
+ <label>
226
+ <span>宽度</span>
227
+ <input id="i2i-width" type="number" min="64" step="64" placeholder="留空=768" />
228
+ </label>
229
+ <label>
230
+ <span>高度</span>
231
+ <input id="i2i-height" type="number" min="64" step="64" placeholder="留空=768" />
232
+ </label>
233
+ <label>
234
+ <span>步数</span>
235
+ <input id="i2i-steps" type="number" min="1" step="1" placeholder="留空=配置默认" />
236
+ </label>
237
+ <label>
238
+ <span>CFG 强度</span>
239
+ <input id="i2i-scale" type="number" min="0" step="0.1" placeholder="留空=配置默认" />
240
+ </label>
241
+ <label>
242
+ <span>采样器</span>
243
+ <select id="i2i-sampler">
244
+ <option value="">(配置默认)</option>
245
+ <option value="k_euler">k_euler</option>
246
+ <option value="k_euler_ancestral">k_euler_ancestral</option>
247
+ <option value="k_dpmpp_2s_ancestral">k_dpmpp_2s_ancestral</option>
248
+ <option value="k_dpmpp_2m">k_dpmpp_2m</option>
249
+ <option value="k_dpmpp_sde">k_dpmpp_sde</option>
250
+ <option value="k_dpmpp_2m_sde">k_dpmpp_2m_sde</option>
251
+ <option value="ddim_v3">ddim_v3</option>
252
+ </select>
253
+ </label>
254
+ <label>
255
+ <span>噪声调度</span>
256
+ <select id="i2i-noise-schedule">
257
+ <option value="">(自动)</option>
258
+ <option value="native">native</option>
259
+ <option value="karras">karras</option>
260
+ <option value="exponential">exponential</option>
261
+ <option value="polyexponential">polyexponential</option>
262
+ </select>
263
+ </label>
264
+ <label>
265
+ <span>随机种子</span>
266
+ <input id="i2i-seed" type="number" step="1" value="-1" />
267
+ </label>
268
+ <label>
269
+ <span>强度</span>
270
+ <input id="i2i-strength" type="number" min="0" max="1" step="0.01" value="0.5" />
271
+ </label>
272
+ <label>
273
+ <span>噪声</span>
274
+ <input id="i2i-noise" type="number" min="0" max="1" step="0.01" value="0.0" />
275
+ </label>
276
+ <label class="switch">
277
+ <input id="i2i-variety" type="checkbox" />
278
+ <span>多样性</span>
279
+ </label>
280
+ <label class="switch">
281
+ <input id="i2i-decrisp" type="checkbox" />
282
+ <span>动态阈值</span>
283
+ </label>
284
+ <label>
285
+ <span>CFG 重缩放</span>
286
+ <input id="i2i-cfg-rescale" type="number" min="0" step="0.1" placeholder="留空=配置默认" />
287
+ </label>
288
+ </div>
289
+ <div class="actions">
290
+ <button id="btn-i2i" class="primary">生成</button>
291
+ </div>
292
+ <div class="result">
293
+ <img id="i2i-img" alt="生成结果" />
294
+ <div class="saved-path">
295
+ <label>保存路径</label>
296
+ <input id="i2i-saved" type="text" readonly />
297
+ </div>
298
+ </div>
299
+ </div>
300
+ </section>
301
+
302
+ <!-- 局部重绘 -->
303
+ <section id="tab-inpaint" class="tab">
304
+ <h2>局部重绘</h2>
305
+ <div class="card">
306
+ <div class="form-grid">
307
+ <label class="full">
308
+ <span>正面提示词</span>
309
+ <textarea id="inpaint-positive" rows="2" placeholder="输入正面提示词"></textarea>
310
+ </label>
311
+ <label class="full">
312
+ <span>负面提示词</span>
313
+ <textarea id="inpaint-negative" rows="2" placeholder="可留空"></textarea>
314
+ </label>
315
+ <label class="full">
316
+ <span>底图</span>
317
+ <input id="inpaint-image" type="file" accept="image/*" />
318
+ </label>
319
+ <label class="full">
320
+ <span>遮罩(白色为重绘区域,黑色保留)</span>
321
+ <input id="inpaint-mask" type="file" accept="image/*" />
322
+ </label>
323
+ <label class="switch">
324
+ <input id="inpaint-add-original" type="checkbox" />
325
+ <span>叠加原图</span>
326
+ </label>
327
+ <label>
328
+ <span>宽度</span>
329
+ <input id="inpaint-width" type="number" min="64" step="64" placeholder="留空=768" />
330
+ </label>
331
+ <label>
332
+ <span>高度</span>
333
+ <input id="inpaint-height" type="number" min="64" step="64" placeholder="留空=768" />
334
+ </label>
335
+ <label>
336
+ <span>步数</span>
337
+ <input id="inpaint-steps" type="number" min="1" step="1" placeholder="留空=配置默认" />
338
+ </label>
339
+ <label>
340
+ <span>CFG 强度</span>
341
+ <input id="inpaint-scale" type="number" min="0" step="0.1" placeholder="留空=配置默认" />
342
+ </label>
343
+ <label>
344
+ <span>采样器</span>
345
+ <select id="inpaint-sampler">
346
+ <option value="">(配置默认)</option>
347
+ <option value="k_euler">k_euler</option>
348
+ <option value="k_euler_ancestral">k_euler_ancestral</option>
349
+ <option value="k_dpmpp_2s_ancestral">k_dpmpp_2s_ancestral</option>
350
+ <option value="k_dpmpp_2m">k_dpmpp_2m</option>
351
+ <option value="k_dpmpp_sde">k_dpmpp_sde</option>
352
+ <option value="k_dpmpp_2m_sde">k_dpmpp_2m_sde</option>
353
+ <option value="ddim_v3">ddim_v3</option>
354
+ </select>
355
+ </label>
356
+ <label>
357
+ <span>噪声调度</span>
358
+ <select id="inpaint-noise-schedule">
359
+ <option value="">(自动)</option>
360
+ <option value="native">native</option>
361
+ <option value="karras">karras</option>
362
+ <option value="exponential">exponential</option>
363
+ <option value="polyexponential">polyexponential</option>
364
+ </select>
365
+ </label>
366
+ <label>
367
+ <span>随机种子</span>
368
+ <input id="inpaint-seed" type="number" step="1" value="-1" />
369
+ </label>
370
+ <label>
371
+ <span>强度</span>
372
+ <input id="inpaint-strength" type="number" min="0" max="1" step="0.01" value="0.5" />
373
+ </label>
374
+ <label>
375
+ <span>噪声</span>
376
+ <input id="inpaint-noise-val" type="number" min="0" max="1" step="0.01" value="0.0" />
377
+ </label>
378
+ <label class="switch">
379
+ <input id="inpaint-variety" type="checkbox" />
380
+ <span>多样性</span>
381
+ </label>
382
+ <label class="switch">
383
+ <input id="inpaint-decrisp" type="checkbox" />
384
+ <span>动态阈值</span>
385
+ </label>
386
+ <label>
387
+ <span>CFG 重缩放</span>
388
+ <input id="inpaint-cfg-rescale" type="number" min="0" step="0.1" placeholder="留空=配置默认" />
389
+ </label>
390
+ </div>
391
+ <div class="actions">
392
+ <button id="btn-inpaint" class="primary">生成</button>
393
+ </div>
394
+ <div class="result">
395
+ <img id="inpaint-img" alt="生成结果" />
396
+ <div class="saved-path">
397
+ <label>保存路径</label>
398
+ <input id="inpaint-saved" type="text" readonly />
399
+ </div>
400
+ </div>
401
+ </div>
402
+ </section>
403
+ </main>
404
+
405
+ <div id="toast" class="toast"></div>
406
+ <div id="loading" class="loading hidden">
407
+ <div class="spinner"></div>
408
+ <div class="loading-text">处理中...</div>
409
+ </div>
410
+
411
+ <audio id="sound-player" src="" preload="auto"></audio>
412
+ <script src="assets/app.js"></script>
413
+ </body>
414
+ </html>
novelai.js ADDED
@@ -0,0 +1,355 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ "use strict";
2
+
3
+ /**
4
+ * NovelAI 上游请求与 Zip 解包(Node 版)
5
+ * - 使用 got(http2) 提升与上游兼容性(对齐 Python httpx(http2=True))
6
+ * - 使用 adm-zip 解压 image_0.png
7
+ * - 导出 generateT2I / generateI2I / generateInpaint
8
+ */
9
+
10
+ const path = require("path");
11
+ const fs = require("fs");
12
+ const fse = require("fs-extra");
13
+ const AdmZip = require("adm-zip");
14
+ const axios = require("axios");
15
+
16
+ const NOVELAI_ENDPOINT = "https://image.novelai.net/ai/generate-image";
17
+ const ROOT = __dirname;
18
+
19
+ // ---------- 公共工具 ----------
20
+ function randSeed(seed) {
21
+ if (seed === undefined || seed === null || Number(seed) === -1) {
22
+ // 10 位随机数(与 Python 版一致范围)
23
+ const min = 1_000_000_000;
24
+ const max = 9_999_999_999;
25
+ return Math.floor(Math.random() * (max - min + 1)) + min;
26
+ }
27
+ return Number(seed);
28
+ }
29
+
30
+ function ensureX64(n) {
31
+ const val = Number(n || 0);
32
+ if (!val || val <= 64) return 64;
33
+ if (val % 64 === 0) return val;
34
+ const fract = (val / 64) % 1;
35
+ return fract >= 0.5 ? (Math.floor(val / 64) + 1) * 64 : Math.floor(val / 64) * 64;
36
+ }
37
+
38
+ function asDataUriPng(b64) {
39
+ return `data:image/png;base64,${b64}`;
40
+ }
41
+
42
+ function headers(key) {
43
+ if (!key) throw new Error("未配置 key,请先在配置中设置 key。");
44
+ return {
45
+ Accept: "application/zip, */*;q=0.5",
46
+ "Accept-Encoding": "gzip, deflate, br",
47
+ "Accept-Language": "zh-CN,zh;q=0.9,en;q=0.8,en-GB;q=0.7,en-US;q=0.6",
48
+ Authorization: `Bearer ${key}`,
49
+ "Content-Type": "application/json",
50
+ Origin: "https://novelai.net",
51
+ Referer: "https://novelai.net/",
52
+ "User-Agent":
53
+ "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/106.0.0.0 Safari/537.36",
54
+ };
55
+ }
56
+
57
+ async function postAndUnzipImage(payload, key, maxRetries = 5, backoff = 1.5) {
58
+ let attempt = 0;
59
+ while (true) {
60
+ attempt += 1;
61
+ let resp;
62
+ try {
63
+ resp = await axios.post(NOVELAI_ENDPOINT, payload, {
64
+ headers: headers(key),
65
+ responseType: "arraybuffer",
66
+ validateStatus: () => true,
67
+ timeout: 120000,
68
+ });
69
+
70
+ const status = resp.status || 0;
71
+ const bodyBuf = Buffer.from(resp.data);
72
+ if (status === 429 && attempt <= maxRetries) {
73
+ await new Promise((r) => setTimeout(r, backoff * attempt * 1000));
74
+ continue;
75
+ }
76
+ if (status < 200 || status >= 300) {
77
+ // 尝试解析上游错误 json
78
+ let detail;
79
+ try {
80
+ detail = JSON.parse(bodyBuf.toString("utf-8"));
81
+ } catch {
82
+ detail = bodyBuf.toString("utf-8");
83
+ }
84
+ throw new Error(`上游生成失败(HTTP ${status}): ${typeof detail === "string" ? detail : JSON.stringify(detail)}`);
85
+ }
86
+
87
+ const zip = new AdmZip(bodyBuf);
88
+ const entry = zip.getEntry("image_0.png");
89
+ if (!entry) {
90
+ throw new Error("Zip 内容缺少 image_0.png");
91
+ }
92
+ const data = entry.getData();
93
+ return data;
94
+ } catch (err) {
95
+ if (attempt > maxRetries) {
96
+ // 最终失败
97
+ throw err;
98
+ }
99
+ // 对网络错误简单重试
100
+ await new Promise((r) => setTimeout(r, backoff * attempt * 1000));
101
+ }
102
+ }
103
+ }
104
+
105
+ function ensureDir(p) {
106
+ fse.ensureDirSync(p);
107
+ }
108
+
109
+ function saveImageBytes(buf, folder) {
110
+ ensureDir(folder);
111
+ const name = `${Date.now()}_${Math.floor(Math.random() * 9000 + 1000)}.png`;
112
+ const full = path.join(folder, name);
113
+ fs.writeFileSync(full, buf);
114
+ return full;
115
+ }
116
+
117
+ function toB64(buf) {
118
+ return Buffer.from(buf).toString("base64");
119
+ }
120
+
121
+ // ---------- 构造 payload ----------
122
+ function buildT2IJson(cfg, { prompt, negative, width, height, scale, steps, sampler, noise_schedule, seed, variety, decrisp, cfg_rescale }) {
123
+ const isV4 = (cfg.model || "").includes("nai-diffusion-4");
124
+
125
+ if (isV4) {
126
+ let resolvedSampler = sampler || cfg.sampler || "k_euler";
127
+ if (resolvedSampler === "ddim_v3") resolvedSampler = "k_euler";
128
+ const params = {
129
+ params_version: 3,
130
+ width: Number(width),
131
+ height: Number(height),
132
+ scale: Number(scale ?? cfg.scale),
133
+ sampler: resolvedSampler,
134
+ steps: Number(steps ?? cfg.steps),
135
+ n_samples: 1,
136
+ ucPreset: cfg.uc_preset,
137
+ qualityToggle: !!cfg.quality_toggle,
138
+ autoSmea: true,
139
+ dynamic_thresholding: !!(decrisp ?? false),
140
+ controlnet_strength: 1,
141
+ legacy: false,
142
+ add_original_image: false,
143
+ cfg_rescale: Number(cfg_rescale ?? cfg.cfg_rescale),
144
+ legacy_v3_extend: false,
145
+ seed: randSeed(seed),
146
+ use_coords: false,
147
+ legacy_uc: !!cfg.legacy_uc,
148
+ characterPrompts: [],
149
+ negative_prompt: negative || "",
150
+ reference_image_multiple: [],
151
+ reference_information_extracted_multiple: [],
152
+ reference_strength_multiple: [],
153
+ deliberate_euler_ancestral_bug: false,
154
+ prefer_brownian: true,
155
+ stream: "msgpack",
156
+ };
157
+ return {
158
+ input: prompt,
159
+ model: cfg.model,
160
+ action: "generate",
161
+ parameters: {
162
+ ...params,
163
+ v4_prompt: {
164
+ caption: { base_caption: prompt, char_captions: [] },
165
+ use_coords: false,
166
+ use_order: true,
167
+ },
168
+ v4_negative_prompt: {
169
+ caption: { base_caption: negative || "", char_captions: [] },
170
+ legacy_uc: !!cfg.legacy_uc,
171
+ },
172
+ },
173
+ use_new_shared_trial: true,
174
+ };
175
+ }
176
+
177
+ // 非 v4:skip_cfg_above_sigma 根据 variety + 模型决定
178
+ const modelName = String(cfg.model || "").toLowerCase();
179
+ let skipVal = null;
180
+ if (variety) {
181
+ if (modelName.includes("nai-diffusion-4-5")) skipVal = 58;
182
+ else if (modelName.includes("nai-diffusion-4")) skipVal = 19.343056794463642;
183
+ else skipVal = 19;
184
+ }
185
+
186
+ const resolvedSampler = sampler || cfg.sampler || "k_euler";
187
+ const payload = {
188
+ input: prompt,
189
+ model: cfg.model,
190
+ action: "generate",
191
+ parameters: {
192
+ params_version: 3,
193
+ width: Number(width),
194
+ height: Number(height),
195
+ scale: Number(scale ?? cfg.scale),
196
+ sampler: resolvedSampler,
197
+ steps: Number(steps ?? cfg.steps),
198
+ n_samples: 1,
199
+ ucPreset: cfg.uc_preset,
200
+ qualityToggle: !!cfg.quality_toggle,
201
+ sm: false,
202
+ sm_dyn: false,
203
+ dynamic_thresholding: !!(decrisp ?? false),
204
+ controlnet_strength: 1,
205
+ legacy: false,
206
+ add_original_image: false,
207
+ uncond_scale: 1,
208
+ cfg_rescale: Number(cfg_rescale ?? cfg.cfg_rescale),
209
+ legacy_v3_extend: false,
210
+ skip_cfg_above_sigma: skipVal,
211
+ seed: randSeed(seed),
212
+ negative_prompt: negative || "",
213
+ reference_image_multiple: [],
214
+ reference_information_extracted_multiple: [],
215
+ reference_strength_multiple: [],
216
+ },
217
+ };
218
+ // 仅当 sampler 不是 ddim_v3 时发送 noise_schedule
219
+ if (noise_schedule && resolvedSampler !== "ddim_v3") {
220
+ payload.parameters.noise_schedule = noise_schedule;
221
+ }
222
+ return payload;
223
+ }
224
+
225
+ function buildI2IJson(cfg, { positive, negative, image_base64, width, height, scale, steps, sampler, noise_schedule, strength, noise, seed, variety, decrisp, cfg_rescale }) {
226
+ const payload = {
227
+ input: positive,
228
+ model: cfg.model,
229
+ action: "img2img",
230
+ parameters: {
231
+ width: Number(width ?? 768),
232
+ height: Number(height ?? 768),
233
+ scale: Number(scale ?? cfg.scale),
234
+ sampler: sampler || cfg.sampler,
235
+ steps: Number(steps ?? cfg.steps),
236
+ n_samples: 1,
237
+ strength: Number(strength ?? 0.5),
238
+ noise: Number(noise ?? 0.0),
239
+ ucPreset: cfg.uc_preset,
240
+ qualityToggle: !!cfg.quality_toggle,
241
+ sm: false,
242
+ sm_dyn: false,
243
+ dynamic_thresholding: !!(decrisp ?? false),
244
+ controlnet_strength: 1,
245
+ legacy: false,
246
+ add_original_image: false,
247
+ uncond_scale: 1,
248
+ cfg_rescale: Number(cfg_rescale ?? cfg.cfg_rescale),
249
+ legacy_v3_extend: false,
250
+ skip_cfg_above_sigma: variety ? 58 : null,
251
+ params_version: 3,
252
+ seed: randSeed(seed),
253
+ image: image_base64,
254
+ extra_noise_seed: randSeed(seed),
255
+ negative_prompt: negative || "",
256
+ reference_image_multiple: [],
257
+ reference_information_extracted_multiple: [],
258
+ reference_strength_multiple: [],
259
+ },
260
+ };
261
+ if (noise_schedule) {
262
+ payload.parameters.noise_schedule = noise_schedule;
263
+ }
264
+ return payload;
265
+ }
266
+
267
+ function buildInpaintJson(cfg, { positive, negative, image_base64, mask_base64, add_original_image, width, height, scale, steps, sampler, noise_schedule, strength, noise, seed, variety, decrisp, cfg_rescale }) {
268
+ // Python 逻辑:某些模型强制使用 v3 inpainting
269
+ const special = ["nai-diffusion-2", "nai-diffusion-4-curated-preview", "nai-diffusion-4-full"];
270
+ const model = special.includes(cfg.model)
271
+ ? "nai-diffusion-3-inpainting"
272
+ : `${cfg.model}-inpainting`;
273
+
274
+ const payload = {
275
+ input: positive,
276
+ model,
277
+ action: "infill",
278
+ parameters: {
279
+ width: Number(width ?? 768),
280
+ height: Number(height ?? 768),
281
+ scale: Number(scale ?? cfg.scale),
282
+ sampler: sampler || cfg.sampler,
283
+ steps: Number(steps ?? cfg.steps),
284
+ n_samples: 1,
285
+ strength: Number(strength ?? 0.5),
286
+ noise: Number(noise ?? 0.0),
287
+ ucPreset: cfg.uc_preset,
288
+ qualityToggle: !!cfg.quality_toggle,
289
+ sm: false,
290
+ sm_dyn: false,
291
+ dynamic_thresholding: !!(decrisp ?? false),
292
+ controlnet_strength: 1,
293
+ legacy: false,
294
+ add_original_image: !!add_original_image,
295
+ uncond_scale: 1,
296
+ cfg_rescale: Number(cfg_rescale ?? cfg.cfg_rescale),
297
+ legacy_v3_extend: false,
298
+ skip_cfg_above_sigma: variety ? 58 : null,
299
+ params_version: 3,
300
+ seed: randSeed(seed),
301
+ image: image_base64,
302
+ mask: mask_base64,
303
+ extra_noise_seed: randSeed(seed),
304
+ negative_prompt: negative || "",
305
+ reference_image_multiple: [],
306
+ reference_information_extracted_multiple: [],
307
+ reference_strength_multiple: [],
308
+ },
309
+ };
310
+ if (noise_schedule) {
311
+ payload.parameters.noise_schedule = noise_schedule;
312
+ }
313
+ return payload;
314
+ }
315
+
316
+ // ---------- 生成导出 ----------
317
+ async function generateT2I(cfg, args) {
318
+ const width = ensureX64(args.width ?? 768);
319
+ const height = ensureX64(args.height ?? 768);
320
+ const payload = buildT2IJson(cfg, { ...args, width, height });
321
+ const img = await postAndUnzipImage(payload, cfg.key || "");
322
+ const baseDir = cfg.output_dir || path.join(ROOT, "output");
323
+ const folder = path.join(baseDir, "t2i");
324
+ const savedPath = saveImageBytes(img, folder);
325
+ return { dataUri: asDataUriPng(toB64(img)), savedPath };
326
+ }
327
+
328
+ async function generateI2I(cfg, args) {
329
+ const width = args.width != null ? ensureX64(args.width) : null;
330
+ const height = args.height != null ? ensureX64(args.height) : null;
331
+ const payload = buildI2IJson(cfg, { ...args, width, height });
332
+ const img = await postAndUnzipImage(payload, cfg.key || "");
333
+ const baseDir = cfg.output_dir || path.join(ROOT, "output");
334
+ const folder = path.join(baseDir, "i2i");
335
+ const savedPath = saveImageBytes(img, folder);
336
+ return { dataUri: asDataUriPng(toB64(img)), savedPath };
337
+ }
338
+
339
+ async function generateInpaint(cfg, args) {
340
+ const width = args.width != null ? ensureX64(args.width) : null;
341
+ const height = args.height != null ? ensureX64(args.height) : null;
342
+ const payload = buildInpaintJson(cfg, { ...args, width, height });
343
+ const img = await postAndUnzipImage(payload, cfg.key || "");
344
+ const baseDir = cfg.output_dir || path.join(ROOT, "output");
345
+ const folder = path.join(baseDir, "inpaint");
346
+ const savedPath = saveImageBytes(img, folder);
347
+ return { dataUri: asDataUriPng(toB64(img)), savedPath };
348
+ }
349
+
350
+ module.exports = {
351
+ ensureX64,
352
+ generateT2I,
353
+ generateI2I,
354
+ generateInpaint,
355
+ };
package-lock.json ADDED
@@ -0,0 +1,1512 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "name": "new-nai-node",
3
+ "version": "1.0.0",
4
+ "lockfileVersion": 3,
5
+ "requires": true,
6
+ "packages": {
7
+ "": {
8
+ "name": "new-nai-node",
9
+ "version": "1.0.0",
10
+ "dependencies": {
11
+ "adm-zip": "^0.5.10",
12
+ "axios": "^1.7.2",
13
+ "compression": "^1.7.4",
14
+ "cors": "^2.8.5",
15
+ "express": "^4.19.2",
16
+ "fs-extra": "^11.2.0",
17
+ "helmet": "^7.0.0",
18
+ "morgan": "^1.10.0"
19
+ },
20
+ "devDependencies": {
21
+ "nodemon": "^3.1.0"
22
+ },
23
+ "engines": {
24
+ "node": ">=18.0.0"
25
+ }
26
+ },
27
+ "node_modules/accepts": {
28
+ "version": "1.3.8",
29
+ "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz",
30
+ "integrity": "sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==",
31
+ "license": "MIT",
32
+ "dependencies": {
33
+ "mime-types": "~2.1.34",
34
+ "negotiator": "0.6.3"
35
+ },
36
+ "engines": {
37
+ "node": ">= 0.6"
38
+ }
39
+ },
40
+ "node_modules/accepts/node_modules/negotiator": {
41
+ "version": "0.6.3",
42
+ "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz",
43
+ "integrity": "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==",
44
+ "license": "MIT",
45
+ "engines": {
46
+ "node": ">= 0.6"
47
+ }
48
+ },
49
+ "node_modules/adm-zip": {
50
+ "version": "0.5.16",
51
+ "resolved": "https://registry.npmjs.org/adm-zip/-/adm-zip-0.5.16.tgz",
52
+ "integrity": "sha512-TGw5yVi4saajsSEgz25grObGHEUaDrniwvA2qwSC060KfqGPdglhvPMA2lPIoxs3PQIItj2iag35fONcQqgUaQ==",
53
+ "license": "MIT",
54
+ "engines": {
55
+ "node": ">=12.0"
56
+ }
57
+ },
58
+ "node_modules/anymatch": {
59
+ "version": "3.1.3",
60
+ "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz",
61
+ "integrity": "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==",
62
+ "dev": true,
63
+ "license": "ISC",
64
+ "dependencies": {
65
+ "normalize-path": "^3.0.0",
66
+ "picomatch": "^2.0.4"
67
+ },
68
+ "engines": {
69
+ "node": ">= 8"
70
+ }
71
+ },
72
+ "node_modules/array-flatten": {
73
+ "version": "1.1.1",
74
+ "resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz",
75
+ "integrity": "sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg==",
76
+ "license": "MIT"
77
+ },
78
+ "node_modules/asynckit": {
79
+ "version": "0.4.0",
80
+ "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz",
81
+ "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==",
82
+ "license": "MIT"
83
+ },
84
+ "node_modules/axios": {
85
+ "version": "1.12.2",
86
+ "resolved": "https://registry.npmjs.org/axios/-/axios-1.12.2.tgz",
87
+ "integrity": "sha512-vMJzPewAlRyOgxV2dU0Cuz2O8zzzx9VYtbJOaBgXFeLc4IV/Eg50n4LowmehOOR61S8ZMpc2K5Sa7g6A4jfkUw==",
88
+ "license": "MIT",
89
+ "dependencies": {
90
+ "follow-redirects": "^1.15.6",
91
+ "form-data": "^4.0.4",
92
+ "proxy-from-env": "^1.1.0"
93
+ }
94
+ },
95
+ "node_modules/balanced-match": {
96
+ "version": "1.0.2",
97
+ "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz",
98
+ "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==",
99
+ "dev": true,
100
+ "license": "MIT"
101
+ },
102
+ "node_modules/basic-auth": {
103
+ "version": "2.0.1",
104
+ "resolved": "https://registry.npmjs.org/basic-auth/-/basic-auth-2.0.1.tgz",
105
+ "integrity": "sha512-NF+epuEdnUYVlGuhaxbbq+dvJttwLnGY+YixlXlME5KpQ5W3CnXA5cVTneY3SPbPDRkcjMbifrwmFYcClgOZeg==",
106
+ "license": "MIT",
107
+ "dependencies": {
108
+ "safe-buffer": "5.1.2"
109
+ },
110
+ "engines": {
111
+ "node": ">= 0.8"
112
+ }
113
+ },
114
+ "node_modules/basic-auth/node_modules/safe-buffer": {
115
+ "version": "5.1.2",
116
+ "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz",
117
+ "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==",
118
+ "license": "MIT"
119
+ },
120
+ "node_modules/binary-extensions": {
121
+ "version": "2.3.0",
122
+ "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.3.0.tgz",
123
+ "integrity": "sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==",
124
+ "dev": true,
125
+ "license": "MIT",
126
+ "engines": {
127
+ "node": ">=8"
128
+ },
129
+ "funding": {
130
+ "url": "https://github.com/sponsors/sindresorhus"
131
+ }
132
+ },
133
+ "node_modules/body-parser": {
134
+ "version": "1.20.3",
135
+ "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.3.tgz",
136
+ "integrity": "sha512-7rAxByjUMqQ3/bHJy7D6OGXvx/MMc4IqBn/X0fcM1QUcAItpZrBEYhWGem+tzXH90c+G01ypMcYJBO9Y30203g==",
137
+ "license": "MIT",
138
+ "dependencies": {
139
+ "bytes": "3.1.2",
140
+ "content-type": "~1.0.5",
141
+ "debug": "2.6.9",
142
+ "depd": "2.0.0",
143
+ "destroy": "1.2.0",
144
+ "http-errors": "2.0.0",
145
+ "iconv-lite": "0.4.24",
146
+ "on-finished": "2.4.1",
147
+ "qs": "6.13.0",
148
+ "raw-body": "2.5.2",
149
+ "type-is": "~1.6.18",
150
+ "unpipe": "1.0.0"
151
+ },
152
+ "engines": {
153
+ "node": ">= 0.8",
154
+ "npm": "1.2.8000 || >= 1.4.16"
155
+ }
156
+ },
157
+ "node_modules/brace-expansion": {
158
+ "version": "1.1.12",
159
+ "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz",
160
+ "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==",
161
+ "dev": true,
162
+ "license": "MIT",
163
+ "dependencies": {
164
+ "balanced-match": "^1.0.0",
165
+ "concat-map": "0.0.1"
166
+ }
167
+ },
168
+ "node_modules/braces": {
169
+ "version": "3.0.3",
170
+ "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz",
171
+ "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==",
172
+ "dev": true,
173
+ "license": "MIT",
174
+ "dependencies": {
175
+ "fill-range": "^7.1.1"
176
+ },
177
+ "engines": {
178
+ "node": ">=8"
179
+ }
180
+ },
181
+ "node_modules/bytes": {
182
+ "version": "3.1.2",
183
+ "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz",
184
+ "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==",
185
+ "license": "MIT",
186
+ "engines": {
187
+ "node": ">= 0.8"
188
+ }
189
+ },
190
+ "node_modules/call-bind-apply-helpers": {
191
+ "version": "1.0.2",
192
+ "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz",
193
+ "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==",
194
+ "license": "MIT",
195
+ "dependencies": {
196
+ "es-errors": "^1.3.0",
197
+ "function-bind": "^1.1.2"
198
+ },
199
+ "engines": {
200
+ "node": ">= 0.4"
201
+ }
202
+ },
203
+ "node_modules/call-bound": {
204
+ "version": "1.0.4",
205
+ "resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.4.tgz",
206
+ "integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==",
207
+ "license": "MIT",
208
+ "dependencies": {
209
+ "call-bind-apply-helpers": "^1.0.2",
210
+ "get-intrinsic": "^1.3.0"
211
+ },
212
+ "engines": {
213
+ "node": ">= 0.4"
214
+ },
215
+ "funding": {
216
+ "url": "https://github.com/sponsors/ljharb"
217
+ }
218
+ },
219
+ "node_modules/chokidar": {
220
+ "version": "3.6.0",
221
+ "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz",
222
+ "integrity": "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==",
223
+ "dev": true,
224
+ "license": "MIT",
225
+ "dependencies": {
226
+ "anymatch": "~3.1.2",
227
+ "braces": "~3.0.2",
228
+ "glob-parent": "~5.1.2",
229
+ "is-binary-path": "~2.1.0",
230
+ "is-glob": "~4.0.1",
231
+ "normalize-path": "~3.0.0",
232
+ "readdirp": "~3.6.0"
233
+ },
234
+ "engines": {
235
+ "node": ">= 8.10.0"
236
+ },
237
+ "funding": {
238
+ "url": "https://paulmillr.com/funding/"
239
+ },
240
+ "optionalDependencies": {
241
+ "fsevents": "~2.3.2"
242
+ }
243
+ },
244
+ "node_modules/combined-stream": {
245
+ "version": "1.0.8",
246
+ "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz",
247
+ "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==",
248
+ "license": "MIT",
249
+ "dependencies": {
250
+ "delayed-stream": "~1.0.0"
251
+ },
252
+ "engines": {
253
+ "node": ">= 0.8"
254
+ }
255
+ },
256
+ "node_modules/compressible": {
257
+ "version": "2.0.18",
258
+ "resolved": "https://registry.npmjs.org/compressible/-/compressible-2.0.18.tgz",
259
+ "integrity": "sha512-AF3r7P5dWxL8MxyITRMlORQNaOA2IkAFaTr4k7BUumjPtRpGDTZpl0Pb1XCO6JeDCBdp126Cgs9sMxqSjgYyRg==",
260
+ "license": "MIT",
261
+ "dependencies": {
262
+ "mime-db": ">= 1.43.0 < 2"
263
+ },
264
+ "engines": {
265
+ "node": ">= 0.6"
266
+ }
267
+ },
268
+ "node_modules/compression": {
269
+ "version": "1.8.1",
270
+ "resolved": "https://registry.npmjs.org/compression/-/compression-1.8.1.tgz",
271
+ "integrity": "sha512-9mAqGPHLakhCLeNyxPkK4xVo746zQ/czLH1Ky+vkitMnWfWZps8r0qXuwhwizagCRttsL4lfG4pIOvaWLpAP0w==",
272
+ "license": "MIT",
273
+ "dependencies": {
274
+ "bytes": "3.1.2",
275
+ "compressible": "~2.0.18",
276
+ "debug": "2.6.9",
277
+ "negotiator": "~0.6.4",
278
+ "on-headers": "~1.1.0",
279
+ "safe-buffer": "5.2.1",
280
+ "vary": "~1.1.2"
281
+ },
282
+ "engines": {
283
+ "node": ">= 0.8.0"
284
+ }
285
+ },
286
+ "node_modules/concat-map": {
287
+ "version": "0.0.1",
288
+ "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz",
289
+ "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==",
290
+ "dev": true,
291
+ "license": "MIT"
292
+ },
293
+ "node_modules/content-disposition": {
294
+ "version": "0.5.4",
295
+ "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.4.tgz",
296
+ "integrity": "sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ==",
297
+ "license": "MIT",
298
+ "dependencies": {
299
+ "safe-buffer": "5.2.1"
300
+ },
301
+ "engines": {
302
+ "node": ">= 0.6"
303
+ }
304
+ },
305
+ "node_modules/content-type": {
306
+ "version": "1.0.5",
307
+ "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.5.tgz",
308
+ "integrity": "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==",
309
+ "license": "MIT",
310
+ "engines": {
311
+ "node": ">= 0.6"
312
+ }
313
+ },
314
+ "node_modules/cookie": {
315
+ "version": "0.7.1",
316
+ "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.1.tgz",
317
+ "integrity": "sha512-6DnInpx7SJ2AK3+CTUE/ZM0vWTUboZCegxhC2xiIydHR9jNuTAASBrfEpHhiGOZw/nX51bHt6YQl8jsGo4y/0w==",
318
+ "license": "MIT",
319
+ "engines": {
320
+ "node": ">= 0.6"
321
+ }
322
+ },
323
+ "node_modules/cookie-signature": {
324
+ "version": "1.0.6",
325
+ "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.6.tgz",
326
+ "integrity": "sha512-QADzlaHc8icV8I7vbaJXJwod9HWYp8uCqf1xa4OfNu1T7JVxQIrUgOWtHdNDtPiywmFbiS12VjotIXLrKM3orQ==",
327
+ "license": "MIT"
328
+ },
329
+ "node_modules/cors": {
330
+ "version": "2.8.5",
331
+ "resolved": "https://registry.npmjs.org/cors/-/cors-2.8.5.tgz",
332
+ "integrity": "sha512-KIHbLJqu73RGr/hnbrO9uBeixNGuvSQjul/jdFvS/KFSIH1hWVd1ng7zOHx+YrEfInLG7q4n6GHQ9cDtxv/P6g==",
333
+ "license": "MIT",
334
+ "dependencies": {
335
+ "object-assign": "^4",
336
+ "vary": "^1"
337
+ },
338
+ "engines": {
339
+ "node": ">= 0.10"
340
+ }
341
+ },
342
+ "node_modules/debug": {
343
+ "version": "2.6.9",
344
+ "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz",
345
+ "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==",
346
+ "license": "MIT",
347
+ "dependencies": {
348
+ "ms": "2.0.0"
349
+ }
350
+ },
351
+ "node_modules/delayed-stream": {
352
+ "version": "1.0.0",
353
+ "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz",
354
+ "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==",
355
+ "license": "MIT",
356
+ "engines": {
357
+ "node": ">=0.4.0"
358
+ }
359
+ },
360
+ "node_modules/depd": {
361
+ "version": "2.0.0",
362
+ "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz",
363
+ "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==",
364
+ "license": "MIT",
365
+ "engines": {
366
+ "node": ">= 0.8"
367
+ }
368
+ },
369
+ "node_modules/destroy": {
370
+ "version": "1.2.0",
371
+ "resolved": "https://registry.npmjs.org/destroy/-/destroy-1.2.0.tgz",
372
+ "integrity": "sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg==",
373
+ "license": "MIT",
374
+ "engines": {
375
+ "node": ">= 0.8",
376
+ "npm": "1.2.8000 || >= 1.4.16"
377
+ }
378
+ },
379
+ "node_modules/dunder-proto": {
380
+ "version": "1.0.1",
381
+ "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz",
382
+ "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==",
383
+ "license": "MIT",
384
+ "dependencies": {
385
+ "call-bind-apply-helpers": "^1.0.1",
386
+ "es-errors": "^1.3.0",
387
+ "gopd": "^1.2.0"
388
+ },
389
+ "engines": {
390
+ "node": ">= 0.4"
391
+ }
392
+ },
393
+ "node_modules/ee-first": {
394
+ "version": "1.1.1",
395
+ "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz",
396
+ "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==",
397
+ "license": "MIT"
398
+ },
399
+ "node_modules/encodeurl": {
400
+ "version": "2.0.0",
401
+ "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz",
402
+ "integrity": "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==",
403
+ "license": "MIT",
404
+ "engines": {
405
+ "node": ">= 0.8"
406
+ }
407
+ },
408
+ "node_modules/es-define-property": {
409
+ "version": "1.0.1",
410
+ "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz",
411
+ "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==",
412
+ "license": "MIT",
413
+ "engines": {
414
+ "node": ">= 0.4"
415
+ }
416
+ },
417
+ "node_modules/es-errors": {
418
+ "version": "1.3.0",
419
+ "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz",
420
+ "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==",
421
+ "license": "MIT",
422
+ "engines": {
423
+ "node": ">= 0.4"
424
+ }
425
+ },
426
+ "node_modules/es-object-atoms": {
427
+ "version": "1.1.1",
428
+ "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz",
429
+ "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==",
430
+ "license": "MIT",
431
+ "dependencies": {
432
+ "es-errors": "^1.3.0"
433
+ },
434
+ "engines": {
435
+ "node": ">= 0.4"
436
+ }
437
+ },
438
+ "node_modules/es-set-tostringtag": {
439
+ "version": "2.1.0",
440
+ "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz",
441
+ "integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==",
442
+ "license": "MIT",
443
+ "dependencies": {
444
+ "es-errors": "^1.3.0",
445
+ "get-intrinsic": "^1.2.6",
446
+ "has-tostringtag": "^1.0.2",
447
+ "hasown": "^2.0.2"
448
+ },
449
+ "engines": {
450
+ "node": ">= 0.4"
451
+ }
452
+ },
453
+ "node_modules/escape-html": {
454
+ "version": "1.0.3",
455
+ "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz",
456
+ "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==",
457
+ "license": "MIT"
458
+ },
459
+ "node_modules/etag": {
460
+ "version": "1.8.1",
461
+ "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz",
462
+ "integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==",
463
+ "license": "MIT",
464
+ "engines": {
465
+ "node": ">= 0.6"
466
+ }
467
+ },
468
+ "node_modules/express": {
469
+ "version": "4.21.2",
470
+ "resolved": "https://registry.npmjs.org/express/-/express-4.21.2.tgz",
471
+ "integrity": "sha512-28HqgMZAmih1Czt9ny7qr6ek2qddF4FclbMzwhCREB6OFfH+rXAnuNCwo1/wFvrtbgsQDb4kSbX9de9lFbrXnA==",
472
+ "license": "MIT",
473
+ "dependencies": {
474
+ "accepts": "~1.3.8",
475
+ "array-flatten": "1.1.1",
476
+ "body-parser": "1.20.3",
477
+ "content-disposition": "0.5.4",
478
+ "content-type": "~1.0.4",
479
+ "cookie": "0.7.1",
480
+ "cookie-signature": "1.0.6",
481
+ "debug": "2.6.9",
482
+ "depd": "2.0.0",
483
+ "encodeurl": "~2.0.0",
484
+ "escape-html": "~1.0.3",
485
+ "etag": "~1.8.1",
486
+ "finalhandler": "1.3.1",
487
+ "fresh": "0.5.2",
488
+ "http-errors": "2.0.0",
489
+ "merge-descriptors": "1.0.3",
490
+ "methods": "~1.1.2",
491
+ "on-finished": "2.4.1",
492
+ "parseurl": "~1.3.3",
493
+ "path-to-regexp": "0.1.12",
494
+ "proxy-addr": "~2.0.7",
495
+ "qs": "6.13.0",
496
+ "range-parser": "~1.2.1",
497
+ "safe-buffer": "5.2.1",
498
+ "send": "0.19.0",
499
+ "serve-static": "1.16.2",
500
+ "setprototypeof": "1.2.0",
501
+ "statuses": "2.0.1",
502
+ "type-is": "~1.6.18",
503
+ "utils-merge": "1.0.1",
504
+ "vary": "~1.1.2"
505
+ },
506
+ "engines": {
507
+ "node": ">= 0.10.0"
508
+ },
509
+ "funding": {
510
+ "type": "opencollective",
511
+ "url": "https://opencollective.com/express"
512
+ }
513
+ },
514
+ "node_modules/fill-range": {
515
+ "version": "7.1.1",
516
+ "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz",
517
+ "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==",
518
+ "dev": true,
519
+ "license": "MIT",
520
+ "dependencies": {
521
+ "to-regex-range": "^5.0.1"
522
+ },
523
+ "engines": {
524
+ "node": ">=8"
525
+ }
526
+ },
527
+ "node_modules/finalhandler": {
528
+ "version": "1.3.1",
529
+ "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.3.1.tgz",
530
+ "integrity": "sha512-6BN9trH7bp3qvnrRyzsBz+g3lZxTNZTbVO2EV1CS0WIcDbawYVdYvGflME/9QP0h0pYlCDBCTjYa9nZzMDpyxQ==",
531
+ "license": "MIT",
532
+ "dependencies": {
533
+ "debug": "2.6.9",
534
+ "encodeurl": "~2.0.0",
535
+ "escape-html": "~1.0.3",
536
+ "on-finished": "2.4.1",
537
+ "parseurl": "~1.3.3",
538
+ "statuses": "2.0.1",
539
+ "unpipe": "~1.0.0"
540
+ },
541
+ "engines": {
542
+ "node": ">= 0.8"
543
+ }
544
+ },
545
+ "node_modules/follow-redirects": {
546
+ "version": "1.15.11",
547
+ "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.11.tgz",
548
+ "integrity": "sha512-deG2P0JfjrTxl50XGCDyfI97ZGVCxIpfKYmfyrQ54n5FO/0gfIES8C/Psl6kWVDolizcaaxZJnTS0QSMxvnsBQ==",
549
+ "funding": [
550
+ {
551
+ "type": "individual",
552
+ "url": "https://github.com/sponsors/RubenVerborgh"
553
+ }
554
+ ],
555
+ "license": "MIT",
556
+ "engines": {
557
+ "node": ">=4.0"
558
+ },
559
+ "peerDependenciesMeta": {
560
+ "debug": {
561
+ "optional": true
562
+ }
563
+ }
564
+ },
565
+ "node_modules/form-data": {
566
+ "version": "4.0.4",
567
+ "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.4.tgz",
568
+ "integrity": "sha512-KrGhL9Q4zjj0kiUt5OO4Mr/A/jlI2jDYs5eHBpYHPcBEVSiipAvn2Ko2HnPe20rmcuuvMHNdZFp+4IlGTMF0Ow==",
569
+ "license": "MIT",
570
+ "dependencies": {
571
+ "asynckit": "^0.4.0",
572
+ "combined-stream": "^1.0.8",
573
+ "es-set-tostringtag": "^2.1.0",
574
+ "hasown": "^2.0.2",
575
+ "mime-types": "^2.1.12"
576
+ },
577
+ "engines": {
578
+ "node": ">= 6"
579
+ }
580
+ },
581
+ "node_modules/forwarded": {
582
+ "version": "0.2.0",
583
+ "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz",
584
+ "integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==",
585
+ "license": "MIT",
586
+ "engines": {
587
+ "node": ">= 0.6"
588
+ }
589
+ },
590
+ "node_modules/fresh": {
591
+ "version": "0.5.2",
592
+ "resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz",
593
+ "integrity": "sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q==",
594
+ "license": "MIT",
595
+ "engines": {
596
+ "node": ">= 0.6"
597
+ }
598
+ },
599
+ "node_modules/fs-extra": {
600
+ "version": "11.3.2",
601
+ "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-11.3.2.tgz",
602
+ "integrity": "sha512-Xr9F6z6up6Ws+NjzMCZc6WXg2YFRlrLP9NQDO3VQrWrfiojdhS56TzueT88ze0uBdCTwEIhQ3ptnmKeWGFAe0A==",
603
+ "license": "MIT",
604
+ "dependencies": {
605
+ "graceful-fs": "^4.2.0",
606
+ "jsonfile": "^6.0.1",
607
+ "universalify": "^2.0.0"
608
+ },
609
+ "engines": {
610
+ "node": ">=14.14"
611
+ }
612
+ },
613
+ "node_modules/fsevents": {
614
+ "version": "2.3.3",
615
+ "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz",
616
+ "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==",
617
+ "dev": true,
618
+ "hasInstallScript": true,
619
+ "license": "MIT",
620
+ "optional": true,
621
+ "os": [
622
+ "darwin"
623
+ ],
624
+ "engines": {
625
+ "node": "^8.16.0 || ^10.6.0 || >=11.0.0"
626
+ }
627
+ },
628
+ "node_modules/function-bind": {
629
+ "version": "1.1.2",
630
+ "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz",
631
+ "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==",
632
+ "license": "MIT",
633
+ "funding": {
634
+ "url": "https://github.com/sponsors/ljharb"
635
+ }
636
+ },
637
+ "node_modules/get-intrinsic": {
638
+ "version": "1.3.0",
639
+ "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz",
640
+ "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==",
641
+ "license": "MIT",
642
+ "dependencies": {
643
+ "call-bind-apply-helpers": "^1.0.2",
644
+ "es-define-property": "^1.0.1",
645
+ "es-errors": "^1.3.0",
646
+ "es-object-atoms": "^1.1.1",
647
+ "function-bind": "^1.1.2",
648
+ "get-proto": "^1.0.1",
649
+ "gopd": "^1.2.0",
650
+ "has-symbols": "^1.1.0",
651
+ "hasown": "^2.0.2",
652
+ "math-intrinsics": "^1.1.0"
653
+ },
654
+ "engines": {
655
+ "node": ">= 0.4"
656
+ },
657
+ "funding": {
658
+ "url": "https://github.com/sponsors/ljharb"
659
+ }
660
+ },
661
+ "node_modules/get-proto": {
662
+ "version": "1.0.1",
663
+ "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz",
664
+ "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==",
665
+ "license": "MIT",
666
+ "dependencies": {
667
+ "dunder-proto": "^1.0.1",
668
+ "es-object-atoms": "^1.0.0"
669
+ },
670
+ "engines": {
671
+ "node": ">= 0.4"
672
+ }
673
+ },
674
+ "node_modules/glob-parent": {
675
+ "version": "5.1.2",
676
+ "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz",
677
+ "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==",
678
+ "dev": true,
679
+ "license": "ISC",
680
+ "dependencies": {
681
+ "is-glob": "^4.0.1"
682
+ },
683
+ "engines": {
684
+ "node": ">= 6"
685
+ }
686
+ },
687
+ "node_modules/gopd": {
688
+ "version": "1.2.0",
689
+ "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz",
690
+ "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==",
691
+ "license": "MIT",
692
+ "engines": {
693
+ "node": ">= 0.4"
694
+ },
695
+ "funding": {
696
+ "url": "https://github.com/sponsors/ljharb"
697
+ }
698
+ },
699
+ "node_modules/graceful-fs": {
700
+ "version": "4.2.11",
701
+ "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz",
702
+ "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==",
703
+ "license": "ISC"
704
+ },
705
+ "node_modules/has-flag": {
706
+ "version": "3.0.0",
707
+ "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz",
708
+ "integrity": "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==",
709
+ "dev": true,
710
+ "license": "MIT",
711
+ "engines": {
712
+ "node": ">=4"
713
+ }
714
+ },
715
+ "node_modules/has-symbols": {
716
+ "version": "1.1.0",
717
+ "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz",
718
+ "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==",
719
+ "license": "MIT",
720
+ "engines": {
721
+ "node": ">= 0.4"
722
+ },
723
+ "funding": {
724
+ "url": "https://github.com/sponsors/ljharb"
725
+ }
726
+ },
727
+ "node_modules/has-tostringtag": {
728
+ "version": "1.0.2",
729
+ "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz",
730
+ "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==",
731
+ "license": "MIT",
732
+ "dependencies": {
733
+ "has-symbols": "^1.0.3"
734
+ },
735
+ "engines": {
736
+ "node": ">= 0.4"
737
+ },
738
+ "funding": {
739
+ "url": "https://github.com/sponsors/ljharb"
740
+ }
741
+ },
742
+ "node_modules/hasown": {
743
+ "version": "2.0.2",
744
+ "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz",
745
+ "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==",
746
+ "license": "MIT",
747
+ "dependencies": {
748
+ "function-bind": "^1.1.2"
749
+ },
750
+ "engines": {
751
+ "node": ">= 0.4"
752
+ }
753
+ },
754
+ "node_modules/helmet": {
755
+ "version": "7.2.0",
756
+ "resolved": "https://registry.npmjs.org/helmet/-/helmet-7.2.0.tgz",
757
+ "integrity": "sha512-ZRiwvN089JfMXokizgqEPXsl2Guk094yExfoDXR0cBYWxtBbaSww/w+vT4WEJsBW2iTUi1GgZ6swmoug3Oy4Xw==",
758
+ "license": "MIT",
759
+ "engines": {
760
+ "node": ">=16.0.0"
761
+ }
762
+ },
763
+ "node_modules/http-errors": {
764
+ "version": "2.0.0",
765
+ "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.0.tgz",
766
+ "integrity": "sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ==",
767
+ "license": "MIT",
768
+ "dependencies": {
769
+ "depd": "2.0.0",
770
+ "inherits": "2.0.4",
771
+ "setprototypeof": "1.2.0",
772
+ "statuses": "2.0.1",
773
+ "toidentifier": "1.0.1"
774
+ },
775
+ "engines": {
776
+ "node": ">= 0.8"
777
+ }
778
+ },
779
+ "node_modules/iconv-lite": {
780
+ "version": "0.4.24",
781
+ "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz",
782
+ "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==",
783
+ "license": "MIT",
784
+ "dependencies": {
785
+ "safer-buffer": ">= 2.1.2 < 3"
786
+ },
787
+ "engines": {
788
+ "node": ">=0.10.0"
789
+ }
790
+ },
791
+ "node_modules/ignore-by-default": {
792
+ "version": "1.0.1",
793
+ "resolved": "https://registry.npmjs.org/ignore-by-default/-/ignore-by-default-1.0.1.tgz",
794
+ "integrity": "sha512-Ius2VYcGNk7T90CppJqcIkS5ooHUZyIQK+ClZfMfMNFEF9VSE73Fq+906u/CWu92x4gzZMWOwfFYckPObzdEbA==",
795
+ "dev": true,
796
+ "license": "ISC"
797
+ },
798
+ "node_modules/inherits": {
799
+ "version": "2.0.4",
800
+ "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz",
801
+ "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==",
802
+ "license": "ISC"
803
+ },
804
+ "node_modules/ipaddr.js": {
805
+ "version": "1.9.1",
806
+ "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz",
807
+ "integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==",
808
+ "license": "MIT",
809
+ "engines": {
810
+ "node": ">= 0.10"
811
+ }
812
+ },
813
+ "node_modules/is-binary-path": {
814
+ "version": "2.1.0",
815
+ "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz",
816
+ "integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==",
817
+ "dev": true,
818
+ "license": "MIT",
819
+ "dependencies": {
820
+ "binary-extensions": "^2.0.0"
821
+ },
822
+ "engines": {
823
+ "node": ">=8"
824
+ }
825
+ },
826
+ "node_modules/is-extglob": {
827
+ "version": "2.1.1",
828
+ "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz",
829
+ "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==",
830
+ "dev": true,
831
+ "license": "MIT",
832
+ "engines": {
833
+ "node": ">=0.10.0"
834
+ }
835
+ },
836
+ "node_modules/is-glob": {
837
+ "version": "4.0.3",
838
+ "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz",
839
+ "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==",
840
+ "dev": true,
841
+ "license": "MIT",
842
+ "dependencies": {
843
+ "is-extglob": "^2.1.1"
844
+ },
845
+ "engines": {
846
+ "node": ">=0.10.0"
847
+ }
848
+ },
849
+ "node_modules/is-number": {
850
+ "version": "7.0.0",
851
+ "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz",
852
+ "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==",
853
+ "dev": true,
854
+ "license": "MIT",
855
+ "engines": {
856
+ "node": ">=0.12.0"
857
+ }
858
+ },
859
+ "node_modules/jsonfile": {
860
+ "version": "6.2.0",
861
+ "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.2.0.tgz",
862
+ "integrity": "sha512-FGuPw30AdOIUTRMC2OMRtQV+jkVj2cfPqSeWXv1NEAJ1qZ5zb1X6z1mFhbfOB/iy3ssJCD+3KuZ8r8C3uVFlAg==",
863
+ "license": "MIT",
864
+ "dependencies": {
865
+ "universalify": "^2.0.0"
866
+ },
867
+ "optionalDependencies": {
868
+ "graceful-fs": "^4.1.6"
869
+ }
870
+ },
871
+ "node_modules/math-intrinsics": {
872
+ "version": "1.1.0",
873
+ "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz",
874
+ "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==",
875
+ "license": "MIT",
876
+ "engines": {
877
+ "node": ">= 0.4"
878
+ }
879
+ },
880
+ "node_modules/media-typer": {
881
+ "version": "0.3.0",
882
+ "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz",
883
+ "integrity": "sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==",
884
+ "license": "MIT",
885
+ "engines": {
886
+ "node": ">= 0.6"
887
+ }
888
+ },
889
+ "node_modules/merge-descriptors": {
890
+ "version": "1.0.3",
891
+ "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.3.tgz",
892
+ "integrity": "sha512-gaNvAS7TZ897/rVaZ0nMtAyxNyi/pdbjbAwUpFQpN70GqnVfOiXpeUUMKRBmzXaSQ8DdTX4/0ms62r2K+hE6mQ==",
893
+ "license": "MIT",
894
+ "funding": {
895
+ "url": "https://github.com/sponsors/sindresorhus"
896
+ }
897
+ },
898
+ "node_modules/methods": {
899
+ "version": "1.1.2",
900
+ "resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz",
901
+ "integrity": "sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w==",
902
+ "license": "MIT",
903
+ "engines": {
904
+ "node": ">= 0.6"
905
+ }
906
+ },
907
+ "node_modules/mime": {
908
+ "version": "1.6.0",
909
+ "resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz",
910
+ "integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==",
911
+ "license": "MIT",
912
+ "bin": {
913
+ "mime": "cli.js"
914
+ },
915
+ "engines": {
916
+ "node": ">=4"
917
+ }
918
+ },
919
+ "node_modules/mime-db": {
920
+ "version": "1.54.0",
921
+ "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.54.0.tgz",
922
+ "integrity": "sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ==",
923
+ "license": "MIT",
924
+ "engines": {
925
+ "node": ">= 0.6"
926
+ }
927
+ },
928
+ "node_modules/mime-types": {
929
+ "version": "2.1.35",
930
+ "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz",
931
+ "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==",
932
+ "license": "MIT",
933
+ "dependencies": {
934
+ "mime-db": "1.52.0"
935
+ },
936
+ "engines": {
937
+ "node": ">= 0.6"
938
+ }
939
+ },
940
+ "node_modules/mime-types/node_modules/mime-db": {
941
+ "version": "1.52.0",
942
+ "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz",
943
+ "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==",
944
+ "license": "MIT",
945
+ "engines": {
946
+ "node": ">= 0.6"
947
+ }
948
+ },
949
+ "node_modules/minimatch": {
950
+ "version": "3.1.2",
951
+ "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz",
952
+ "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==",
953
+ "dev": true,
954
+ "license": "ISC",
955
+ "dependencies": {
956
+ "brace-expansion": "^1.1.7"
957
+ },
958
+ "engines": {
959
+ "node": "*"
960
+ }
961
+ },
962
+ "node_modules/morgan": {
963
+ "version": "1.10.1",
964
+ "resolved": "https://registry.npmjs.org/morgan/-/morgan-1.10.1.tgz",
965
+ "integrity": "sha512-223dMRJtI/l25dJKWpgij2cMtywuG/WiUKXdvwfbhGKBhy1puASqXwFzmWZ7+K73vUPoR7SS2Qz2cI/g9MKw0A==",
966
+ "license": "MIT",
967
+ "dependencies": {
968
+ "basic-auth": "~2.0.1",
969
+ "debug": "2.6.9",
970
+ "depd": "~2.0.0",
971
+ "on-finished": "~2.3.0",
972
+ "on-headers": "~1.1.0"
973
+ },
974
+ "engines": {
975
+ "node": ">= 0.8.0"
976
+ }
977
+ },
978
+ "node_modules/morgan/node_modules/on-finished": {
979
+ "version": "2.3.0",
980
+ "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.3.0.tgz",
981
+ "integrity": "sha512-ikqdkGAAyf/X/gPhXGvfgAytDZtDbr+bkNUJ0N9h5MI/dmdgCs3l6hoHrcUv41sRKew3jIwrp4qQDXiK99Utww==",
982
+ "license": "MIT",
983
+ "dependencies": {
984
+ "ee-first": "1.1.1"
985
+ },
986
+ "engines": {
987
+ "node": ">= 0.8"
988
+ }
989
+ },
990
+ "node_modules/ms": {
991
+ "version": "2.0.0",
992
+ "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz",
993
+ "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==",
994
+ "license": "MIT"
995
+ },
996
+ "node_modules/negotiator": {
997
+ "version": "0.6.4",
998
+ "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.4.tgz",
999
+ "integrity": "sha512-myRT3DiWPHqho5PrJaIRyaMv2kgYf0mUVgBNOYMuCH5Ki1yEiQaf/ZJuQ62nvpc44wL5WDbTX7yGJi1Neevw8w==",
1000
+ "license": "MIT",
1001
+ "engines": {
1002
+ "node": ">= 0.6"
1003
+ }
1004
+ },
1005
+ "node_modules/nodemon": {
1006
+ "version": "3.1.10",
1007
+ "resolved": "https://registry.npmjs.org/nodemon/-/nodemon-3.1.10.tgz",
1008
+ "integrity": "sha512-WDjw3pJ0/0jMFmyNDp3gvY2YizjLmmOUQo6DEBY+JgdvW/yQ9mEeSw6H5ythl5Ny2ytb7f9C2nIbjSxMNzbJXw==",
1009
+ "dev": true,
1010
+ "license": "MIT",
1011
+ "dependencies": {
1012
+ "chokidar": "^3.5.2",
1013
+ "debug": "^4",
1014
+ "ignore-by-default": "^1.0.1",
1015
+ "minimatch": "^3.1.2",
1016
+ "pstree.remy": "^1.1.8",
1017
+ "semver": "^7.5.3",
1018
+ "simple-update-notifier": "^2.0.0",
1019
+ "supports-color": "^5.5.0",
1020
+ "touch": "^3.1.0",
1021
+ "undefsafe": "^2.0.5"
1022
+ },
1023
+ "bin": {
1024
+ "nodemon": "bin/nodemon.js"
1025
+ },
1026
+ "engines": {
1027
+ "node": ">=10"
1028
+ },
1029
+ "funding": {
1030
+ "type": "opencollective",
1031
+ "url": "https://opencollective.com/nodemon"
1032
+ }
1033
+ },
1034
+ "node_modules/nodemon/node_modules/debug": {
1035
+ "version": "4.4.3",
1036
+ "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz",
1037
+ "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==",
1038
+ "dev": true,
1039
+ "license": "MIT",
1040
+ "dependencies": {
1041
+ "ms": "^2.1.3"
1042
+ },
1043
+ "engines": {
1044
+ "node": ">=6.0"
1045
+ },
1046
+ "peerDependenciesMeta": {
1047
+ "supports-color": {
1048
+ "optional": true
1049
+ }
1050
+ }
1051
+ },
1052
+ "node_modules/nodemon/node_modules/ms": {
1053
+ "version": "2.1.3",
1054
+ "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
1055
+ "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",
1056
+ "dev": true,
1057
+ "license": "MIT"
1058
+ },
1059
+ "node_modules/normalize-path": {
1060
+ "version": "3.0.0",
1061
+ "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz",
1062
+ "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==",
1063
+ "dev": true,
1064
+ "license": "MIT",
1065
+ "engines": {
1066
+ "node": ">=0.10.0"
1067
+ }
1068
+ },
1069
+ "node_modules/object-assign": {
1070
+ "version": "4.1.1",
1071
+ "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz",
1072
+ "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==",
1073
+ "license": "MIT",
1074
+ "engines": {
1075
+ "node": ">=0.10.0"
1076
+ }
1077
+ },
1078
+ "node_modules/object-inspect": {
1079
+ "version": "1.13.4",
1080
+ "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz",
1081
+ "integrity": "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==",
1082
+ "license": "MIT",
1083
+ "engines": {
1084
+ "node": ">= 0.4"
1085
+ },
1086
+ "funding": {
1087
+ "url": "https://github.com/sponsors/ljharb"
1088
+ }
1089
+ },
1090
+ "node_modules/on-finished": {
1091
+ "version": "2.4.1",
1092
+ "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz",
1093
+ "integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==",
1094
+ "license": "MIT",
1095
+ "dependencies": {
1096
+ "ee-first": "1.1.1"
1097
+ },
1098
+ "engines": {
1099
+ "node": ">= 0.8"
1100
+ }
1101
+ },
1102
+ "node_modules/on-headers": {
1103
+ "version": "1.1.0",
1104
+ "resolved": "https://registry.npmjs.org/on-headers/-/on-headers-1.1.0.tgz",
1105
+ "integrity": "sha512-737ZY3yNnXy37FHkQxPzt4UZ2UWPWiCZWLvFZ4fu5cueciegX0zGPnrlY6bwRg4FdQOe9YU8MkmJwGhoMybl8A==",
1106
+ "license": "MIT",
1107
+ "engines": {
1108
+ "node": ">= 0.8"
1109
+ }
1110
+ },
1111
+ "node_modules/parseurl": {
1112
+ "version": "1.3.3",
1113
+ "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz",
1114
+ "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==",
1115
+ "license": "MIT",
1116
+ "engines": {
1117
+ "node": ">= 0.8"
1118
+ }
1119
+ },
1120
+ "node_modules/path-to-regexp": {
1121
+ "version": "0.1.12",
1122
+ "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.12.tgz",
1123
+ "integrity": "sha512-RA1GjUVMnvYFxuqovrEqZoxxW5NUZqbwKtYz/Tt7nXerk0LbLblQmrsgdeOxV5SFHf0UDggjS/bSeOZwt1pmEQ==",
1124
+ "license": "MIT"
1125
+ },
1126
+ "node_modules/picomatch": {
1127
+ "version": "2.3.1",
1128
+ "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz",
1129
+ "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==",
1130
+ "dev": true,
1131
+ "license": "MIT",
1132
+ "engines": {
1133
+ "node": ">=8.6"
1134
+ },
1135
+ "funding": {
1136
+ "url": "https://github.com/sponsors/jonschlinkert"
1137
+ }
1138
+ },
1139
+ "node_modules/proxy-addr": {
1140
+ "version": "2.0.7",
1141
+ "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz",
1142
+ "integrity": "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==",
1143
+ "license": "MIT",
1144
+ "dependencies": {
1145
+ "forwarded": "0.2.0",
1146
+ "ipaddr.js": "1.9.1"
1147
+ },
1148
+ "engines": {
1149
+ "node": ">= 0.10"
1150
+ }
1151
+ },
1152
+ "node_modules/proxy-from-env": {
1153
+ "version": "1.1.0",
1154
+ "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz",
1155
+ "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==",
1156
+ "license": "MIT"
1157
+ },
1158
+ "node_modules/pstree.remy": {
1159
+ "version": "1.1.8",
1160
+ "resolved": "https://registry.npmjs.org/pstree.remy/-/pstree.remy-1.1.8.tgz",
1161
+ "integrity": "sha512-77DZwxQmxKnu3aR542U+X8FypNzbfJ+C5XQDk3uWjWxn6151aIMGthWYRXTqT1E5oJvg+ljaa2OJi+VfvCOQ8w==",
1162
+ "dev": true,
1163
+ "license": "MIT"
1164
+ },
1165
+ "node_modules/qs": {
1166
+ "version": "6.13.0",
1167
+ "resolved": "https://registry.npmjs.org/qs/-/qs-6.13.0.tgz",
1168
+ "integrity": "sha512-+38qI9SOr8tfZ4QmJNplMUxqjbe7LKvvZgWdExBOmd+egZTtjLB67Gu0HRX3u/XOq7UU2Nx6nsjvS16Z9uwfpg==",
1169
+ "license": "BSD-3-Clause",
1170
+ "dependencies": {
1171
+ "side-channel": "^1.0.6"
1172
+ },
1173
+ "engines": {
1174
+ "node": ">=0.6"
1175
+ },
1176
+ "funding": {
1177
+ "url": "https://github.com/sponsors/ljharb"
1178
+ }
1179
+ },
1180
+ "node_modules/range-parser": {
1181
+ "version": "1.2.1",
1182
+ "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz",
1183
+ "integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==",
1184
+ "license": "MIT",
1185
+ "engines": {
1186
+ "node": ">= 0.6"
1187
+ }
1188
+ },
1189
+ "node_modules/raw-body": {
1190
+ "version": "2.5.2",
1191
+ "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.5.2.tgz",
1192
+ "integrity": "sha512-8zGqypfENjCIqGhgXToC8aB2r7YrBX+AQAfIPs/Mlk+BtPTztOvTS01NRW/3Eh60J+a48lt8qsCzirQ6loCVfA==",
1193
+ "license": "MIT",
1194
+ "dependencies": {
1195
+ "bytes": "3.1.2",
1196
+ "http-errors": "2.0.0",
1197
+ "iconv-lite": "0.4.24",
1198
+ "unpipe": "1.0.0"
1199
+ },
1200
+ "engines": {
1201
+ "node": ">= 0.8"
1202
+ }
1203
+ },
1204
+ "node_modules/readdirp": {
1205
+ "version": "3.6.0",
1206
+ "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz",
1207
+ "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==",
1208
+ "dev": true,
1209
+ "license": "MIT",
1210
+ "dependencies": {
1211
+ "picomatch": "^2.2.1"
1212
+ },
1213
+ "engines": {
1214
+ "node": ">=8.10.0"
1215
+ }
1216
+ },
1217
+ "node_modules/safe-buffer": {
1218
+ "version": "5.2.1",
1219
+ "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz",
1220
+ "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==",
1221
+ "funding": [
1222
+ {
1223
+ "type": "github",
1224
+ "url": "https://github.com/sponsors/feross"
1225
+ },
1226
+ {
1227
+ "type": "patreon",
1228
+ "url": "https://www.patreon.com/feross"
1229
+ },
1230
+ {
1231
+ "type": "consulting",
1232
+ "url": "https://feross.org/support"
1233
+ }
1234
+ ],
1235
+ "license": "MIT"
1236
+ },
1237
+ "node_modules/safer-buffer": {
1238
+ "version": "2.1.2",
1239
+ "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz",
1240
+ "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==",
1241
+ "license": "MIT"
1242
+ },
1243
+ "node_modules/semver": {
1244
+ "version": "7.7.3",
1245
+ "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.3.tgz",
1246
+ "integrity": "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q==",
1247
+ "dev": true,
1248
+ "license": "ISC",
1249
+ "bin": {
1250
+ "semver": "bin/semver.js"
1251
+ },
1252
+ "engines": {
1253
+ "node": ">=10"
1254
+ }
1255
+ },
1256
+ "node_modules/send": {
1257
+ "version": "0.19.0",
1258
+ "resolved": "https://registry.npmjs.org/send/-/send-0.19.0.tgz",
1259
+ "integrity": "sha512-dW41u5VfLXu8SJh5bwRmyYUbAoSB3c9uQh6L8h/KtsFREPWpbX1lrljJo186Jc4nmci/sGUZ9a0a0J2zgfq2hw==",
1260
+ "license": "MIT",
1261
+ "dependencies": {
1262
+ "debug": "2.6.9",
1263
+ "depd": "2.0.0",
1264
+ "destroy": "1.2.0",
1265
+ "encodeurl": "~1.0.2",
1266
+ "escape-html": "~1.0.3",
1267
+ "etag": "~1.8.1",
1268
+ "fresh": "0.5.2",
1269
+ "http-errors": "2.0.0",
1270
+ "mime": "1.6.0",
1271
+ "ms": "2.1.3",
1272
+ "on-finished": "2.4.1",
1273
+ "range-parser": "~1.2.1",
1274
+ "statuses": "2.0.1"
1275
+ },
1276
+ "engines": {
1277
+ "node": ">= 0.8.0"
1278
+ }
1279
+ },
1280
+ "node_modules/send/node_modules/encodeurl": {
1281
+ "version": "1.0.2",
1282
+ "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.2.tgz",
1283
+ "integrity": "sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w==",
1284
+ "license": "MIT",
1285
+ "engines": {
1286
+ "node": ">= 0.8"
1287
+ }
1288
+ },
1289
+ "node_modules/send/node_modules/ms": {
1290
+ "version": "2.1.3",
1291
+ "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
1292
+ "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",
1293
+ "license": "MIT"
1294
+ },
1295
+ "node_modules/serve-static": {
1296
+ "version": "1.16.2",
1297
+ "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.16.2.tgz",
1298
+ "integrity": "sha512-VqpjJZKadQB/PEbEwvFdO43Ax5dFBZ2UECszz8bQ7pi7wt//PWe1P6MN7eCnjsatYtBT6EuiClbjSWP2WrIoTw==",
1299
+ "license": "MIT",
1300
+ "dependencies": {
1301
+ "encodeurl": "~2.0.0",
1302
+ "escape-html": "~1.0.3",
1303
+ "parseurl": "~1.3.3",
1304
+ "send": "0.19.0"
1305
+ },
1306
+ "engines": {
1307
+ "node": ">= 0.8.0"
1308
+ }
1309
+ },
1310
+ "node_modules/setprototypeof": {
1311
+ "version": "1.2.0",
1312
+ "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz",
1313
+ "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==",
1314
+ "license": "ISC"
1315
+ },
1316
+ "node_modules/side-channel": {
1317
+ "version": "1.1.0",
1318
+ "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.0.tgz",
1319
+ "integrity": "sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==",
1320
+ "license": "MIT",
1321
+ "dependencies": {
1322
+ "es-errors": "^1.3.0",
1323
+ "object-inspect": "^1.13.3",
1324
+ "side-channel-list": "^1.0.0",
1325
+ "side-channel-map": "^1.0.1",
1326
+ "side-channel-weakmap": "^1.0.2"
1327
+ },
1328
+ "engines": {
1329
+ "node": ">= 0.4"
1330
+ },
1331
+ "funding": {
1332
+ "url": "https://github.com/sponsors/ljharb"
1333
+ }
1334
+ },
1335
+ "node_modules/side-channel-list": {
1336
+ "version": "1.0.0",
1337
+ "resolved": "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.0.tgz",
1338
+ "integrity": "sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA==",
1339
+ "license": "MIT",
1340
+ "dependencies": {
1341
+ "es-errors": "^1.3.0",
1342
+ "object-inspect": "^1.13.3"
1343
+ },
1344
+ "engines": {
1345
+ "node": ">= 0.4"
1346
+ },
1347
+ "funding": {
1348
+ "url": "https://github.com/sponsors/ljharb"
1349
+ }
1350
+ },
1351
+ "node_modules/side-channel-map": {
1352
+ "version": "1.0.1",
1353
+ "resolved": "https://registry.npmjs.org/side-channel-map/-/side-channel-map-1.0.1.tgz",
1354
+ "integrity": "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==",
1355
+ "license": "MIT",
1356
+ "dependencies": {
1357
+ "call-bound": "^1.0.2",
1358
+ "es-errors": "^1.3.0",
1359
+ "get-intrinsic": "^1.2.5",
1360
+ "object-inspect": "^1.13.3"
1361
+ },
1362
+ "engines": {
1363
+ "node": ">= 0.4"
1364
+ },
1365
+ "funding": {
1366
+ "url": "https://github.com/sponsors/ljharb"
1367
+ }
1368
+ },
1369
+ "node_modules/side-channel-weakmap": {
1370
+ "version": "1.0.2",
1371
+ "resolved": "https://registry.npmjs.org/side-channel-weakmap/-/side-channel-weakmap-1.0.2.tgz",
1372
+ "integrity": "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==",
1373
+ "license": "MIT",
1374
+ "dependencies": {
1375
+ "call-bound": "^1.0.2",
1376
+ "es-errors": "^1.3.0",
1377
+ "get-intrinsic": "^1.2.5",
1378
+ "object-inspect": "^1.13.3",
1379
+ "side-channel-map": "^1.0.1"
1380
+ },
1381
+ "engines": {
1382
+ "node": ">= 0.4"
1383
+ },
1384
+ "funding": {
1385
+ "url": "https://github.com/sponsors/ljharb"
1386
+ }
1387
+ },
1388
+ "node_modules/simple-update-notifier": {
1389
+ "version": "2.0.0",
1390
+ "resolved": "https://registry.npmjs.org/simple-update-notifier/-/simple-update-notifier-2.0.0.tgz",
1391
+ "integrity": "sha512-a2B9Y0KlNXl9u/vsW6sTIu9vGEpfKu2wRV6l1H3XEas/0gUIzGzBoP/IouTcUQbm9JWZLH3COxyn03TYlFax6w==",
1392
+ "dev": true,
1393
+ "license": "MIT",
1394
+ "dependencies": {
1395
+ "semver": "^7.5.3"
1396
+ },
1397
+ "engines": {
1398
+ "node": ">=10"
1399
+ }
1400
+ },
1401
+ "node_modules/statuses": {
1402
+ "version": "2.0.1",
1403
+ "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz",
1404
+ "integrity": "sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==",
1405
+ "license": "MIT",
1406
+ "engines": {
1407
+ "node": ">= 0.8"
1408
+ }
1409
+ },
1410
+ "node_modules/supports-color": {
1411
+ "version": "5.5.0",
1412
+ "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz",
1413
+ "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==",
1414
+ "dev": true,
1415
+ "license": "MIT",
1416
+ "dependencies": {
1417
+ "has-flag": "^3.0.0"
1418
+ },
1419
+ "engines": {
1420
+ "node": ">=4"
1421
+ }
1422
+ },
1423
+ "node_modules/to-regex-range": {
1424
+ "version": "5.0.1",
1425
+ "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz",
1426
+ "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==",
1427
+ "dev": true,
1428
+ "license": "MIT",
1429
+ "dependencies": {
1430
+ "is-number": "^7.0.0"
1431
+ },
1432
+ "engines": {
1433
+ "node": ">=8.0"
1434
+ }
1435
+ },
1436
+ "node_modules/toidentifier": {
1437
+ "version": "1.0.1",
1438
+ "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz",
1439
+ "integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==",
1440
+ "license": "MIT",
1441
+ "engines": {
1442
+ "node": ">=0.6"
1443
+ }
1444
+ },
1445
+ "node_modules/touch": {
1446
+ "version": "3.1.1",
1447
+ "resolved": "https://registry.npmjs.org/touch/-/touch-3.1.1.tgz",
1448
+ "integrity": "sha512-r0eojU4bI8MnHr8c5bNo7lJDdI2qXlWWJk6a9EAFG7vbhTjElYhBVS3/miuE0uOuoLdb8Mc/rVfsmm6eo5o9GA==",
1449
+ "dev": true,
1450
+ "license": "ISC",
1451
+ "bin": {
1452
+ "nodetouch": "bin/nodetouch.js"
1453
+ }
1454
+ },
1455
+ "node_modules/type-is": {
1456
+ "version": "1.6.18",
1457
+ "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz",
1458
+ "integrity": "sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==",
1459
+ "license": "MIT",
1460
+ "dependencies": {
1461
+ "media-typer": "0.3.0",
1462
+ "mime-types": "~2.1.24"
1463
+ },
1464
+ "engines": {
1465
+ "node": ">= 0.6"
1466
+ }
1467
+ },
1468
+ "node_modules/undefsafe": {
1469
+ "version": "2.0.5",
1470
+ "resolved": "https://registry.npmjs.org/undefsafe/-/undefsafe-2.0.5.tgz",
1471
+ "integrity": "sha512-WxONCrssBM8TSPRqN5EmsjVrsv4A8X12J4ArBiiayv3DyyG3ZlIg6yysuuSYdZsVz3TKcTg2fd//Ujd4CHV1iA==",
1472
+ "dev": true,
1473
+ "license": "MIT"
1474
+ },
1475
+ "node_modules/universalify": {
1476
+ "version": "2.0.1",
1477
+ "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz",
1478
+ "integrity": "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==",
1479
+ "license": "MIT",
1480
+ "engines": {
1481
+ "node": ">= 10.0.0"
1482
+ }
1483
+ },
1484
+ "node_modules/unpipe": {
1485
+ "version": "1.0.0",
1486
+ "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz",
1487
+ "integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==",
1488
+ "license": "MIT",
1489
+ "engines": {
1490
+ "node": ">= 0.8"
1491
+ }
1492
+ },
1493
+ "node_modules/utils-merge": {
1494
+ "version": "1.0.1",
1495
+ "resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz",
1496
+ "integrity": "sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA==",
1497
+ "license": "MIT",
1498
+ "engines": {
1499
+ "node": ">= 0.4.0"
1500
+ }
1501
+ },
1502
+ "node_modules/vary": {
1503
+ "version": "1.1.2",
1504
+ "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz",
1505
+ "integrity": "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==",
1506
+ "license": "MIT",
1507
+ "engines": {
1508
+ "node": ">= 0.8"
1509
+ }
1510
+ }
1511
+ }
1512
+ }
package.json ADDED
@@ -0,0 +1,26 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "name": "new-nai-node",
3
+ "version": "1.0.0",
4
+ "private": true,
5
+ "main": "server.js",
6
+ "scripts": {
7
+ "start": "node server.js",
8
+ "dev": "nodemon --watch ./ --ext js,json --ignore output --ignore \"Semi-Auto-NovelAI-to-Pixiv\" server.js"
9
+ },
10
+ "engines": {
11
+ "node": ">=18.0.0"
12
+ },
13
+ "dependencies": {
14
+ "express": "^4.19.2",
15
+ "cors": "^2.8.5",
16
+ "helmet": "^7.0.0",
17
+ "compression": "^1.7.4",
18
+ "morgan": "^1.10.0",
19
+ "axios": "^1.7.2",
20
+ "adm-zip": "^0.5.10",
21
+ "fs-extra": "^11.2.0"
22
+ },
23
+ "devDependencies": {
24
+ "nodemon": "^3.1.0"
25
+ }
26
+ }
requirements.txt ADDED
@@ -0,0 +1,5 @@
 
 
 
 
 
 
1
+ fastapi>=0.110,<1
2
+ uvicorn[standard]>=0.23,<1
3
+ requests>=2.31,<3
4
+ pydantic>=2.4,<3
5
+ httpx[http2]>=0.25,<1
ring/new-notification-3-398649.mp3 ADDED
Binary file (16.1 kB). View file
 
server.js ADDED
@@ -0,0 +1,432 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ /**
2
+ * New NAI - Node.js/Express 服务
3
+ * 目标:保持与现有 FastAPI 接口一致,使前端无需改动即可运行。
4
+ *
5
+ * 实现接口:
6
+ * - GET /api/health
7
+ * - GET /api/config
8
+ * - PUT /api/config
9
+ * - GET /api/select-output-dir (本地桌面环境:弹出目录选择器;无 GUI 环境会失败)
10
+ * - POST /api/open-dir (打开目录,若未传 path 则读取配置中的 output_dir)
11
+ *
12
+ * 静态资源:
13
+ * - / -> ./frontend (index.html 单页)
14
+ * - /ring -> ./ring (提示音目录)
15
+ * - 兼容别名:/ring/ring.mp3 若实际不存在则回退到 new-notification-3-398649.mp3
16
+ *
17
+ * 注意:本文件尚未实现 /api/generate/* 生成接口;将在后续步骤补充。
18
+ */
19
+
20
+ const path = require('path');
21
+ const fs = require('fs');
22
+ const fse = require('fs-extra');
23
+ const os = require('os');
24
+ const http = require('http');
25
+ const express = require('express');
26
+ const cors = require('cors');
27
+ const helmet = require('helmet');
28
+ const compression = require('compression');
29
+ const morgan = require('morgan');
30
+ const { spawn } = require('child_process');
31
+ const { generateT2I, generateI2I, generateInpaint } = require('./novelai');
32
+
33
+ const app = express();
34
+
35
+ // ---------- 常量与路径 ----------
36
+ const ROOT = __dirname;
37
+ const FRONTEND_DIR = path.join(ROOT, 'frontend');
38
+ const RING_DIR = path.join(ROOT, 'ring');
39
+ const CONFIG_PATH = path.join(ROOT, 'backend', 'config.json');
40
+
41
+ // ---------- 中间件 ----------
42
+ app.disable('x-powered-by');
43
+ app.use(cors({
44
+ origin: '*',
45
+ credentials: false,
46
+ methods: ['GET', 'POST', 'PUT', 'DELETE', 'OPTIONS'],
47
+ allowedHeaders: ['Content-Type', 'Authorization', 'x-api-key']
48
+ }));
49
+
50
+ // Helmet 安全头(简化版 CSP,允许被同源 iframe 嵌入;可按需扩展)
51
+ app.use(helmet({
52
+ contentSecurityPolicy: {
53
+ useDefaults: true,
54
+ directives: {
55
+ "default-src": ["'self'"],
56
+ "img-src": ["'self'", "data:", "blob:"],
57
+ "style-src": ["'self'", "'unsafe-inline'"],
58
+ "script-src": ["'self'"],
59
+ "font-src": ["'self'", "data:"],
60
+ "connect-src": ["'self'"],
61
+ "frame-ancestors": ["'self'"], // 需要允许外部嵌入可在此添加域
62
+ }
63
+ },
64
+ crossOriginResourcePolicy: { policy: "same-origin" }
65
+ }));
66
+
67
+ app.use(compression());
68
+ app.use(express.json({ limit: '50mb' })); // 前端传参与图片 Base64,放宽到 50MB
69
+ app.use(morgan('dev'));
70
+
71
+ // ---------- 工具函数:配置读写与默认值 ----------
72
+ const DEFAULT_CONFIG = {
73
+ key: null,
74
+ model: "nai-diffusion-3",
75
+ sampler: "k_euler",
76
+ steps: 28,
77
+ scale: 5.0,
78
+ cfg_rescale: 0.0,
79
+ noise_schedule: "karras",
80
+ uc_preset: 4,
81
+ quality_toggle: true,
82
+ legacy_uc: false,
83
+ port: 9180,
84
+ save_output: true,
85
+ output_dir: path.join(ROOT, 'output'),
86
+ // UI 配色与提示音(与前端一致)
87
+ color_scheme: "auto",
88
+ custom_primary: null,
89
+ sound_enabled: false,
90
+ sound_url: "/ring/ring.mp3"
91
+ };
92
+
93
+ function readConfig() {
94
+ try {
95
+ if (fs.existsSync(CONFIG_PATH)) {
96
+ const raw = fs.readFileSync(CONFIG_PATH, 'utf-8');
97
+ const fileCfg = JSON.parse(raw || '{}');
98
+ // 合并默认值,确保前端读取字段齐全
99
+ return { ...DEFAULT_CONFIG, ...fileCfg };
100
+ }
101
+ } catch (e) {
102
+ // ignore and fallback
103
+ }
104
+ // 若不存在则写入默认文件
105
+ writeConfig(DEFAULT_CONFIG);
106
+ return { ...DEFAULT_CONFIG };
107
+ }
108
+
109
+ function writeConfig(cfg) {
110
+ const merged = { ...DEFAULT_CONFIG, ...(cfg || {}) };
111
+ fse.ensureDirSync(path.dirname(CONFIG_PATH));
112
+ fs.writeFileSync(CONFIG_PATH, JSON.stringify(merged, null, 2), 'utf-8');
113
+ return merged;
114
+ }
115
+
116
+ // ---------- 工具函数:系统命令 ----------
117
+ function runSpawn(cmd, args, options = {}) {
118
+ return new Promise((resolve, reject) => {
119
+ const child = spawn(cmd, args, {
120
+ stdio: ['ignore', 'pipe', 'pipe'],
121
+ shell: false,
122
+ ...options
123
+ });
124
+ let stdout = '';
125
+ let stderr = '';
126
+ child.stdout.on('data', (d) => { stdout += d.toString(); });
127
+ child.stderr.on('data', (d) => { stderr += d.toString(); });
128
+ child.on('error', reject);
129
+ child.on('close', (code) => {
130
+ if (code === 0) resolve({ stdout, stderr, code });
131
+ else reject(new Error(stderr || `Exit code ${code}`));
132
+ });
133
+ });
134
+ }
135
+
136
+ function sleep(ms) {
137
+ return new Promise(r => setTimeout(r, ms));
138
+ }
139
+
140
+ // Windows: 使用 COM Shell.Application 弹出目录选择器
141
+ async function winBrowseForFolder() {
142
+ try {
143
+ const psCmd = `$f=(New-Object -ComObject Shell.Application).BrowseForFolder(0,"选择保存目录",0); if($f){$f.Self.Path}`;
144
+ const { stdout } = await runSpawn('powershell.exe', ['-NoProfile', '-Command', psCmd], { timeout: 60000 });
145
+ const out = (stdout || '').trim();
146
+ if (out) return out;
147
+ } catch (e) {
148
+ // ignore, fallback to VBS
149
+ }
150
+
151
+ // VBScript 兜底:cscript //nologo temp.vbs
152
+ const vbs = [
153
+ 'Set sh = CreateObject("Shell.Application")',
154
+ 'Set f = sh.BrowseForFolder(0, "选择保存目录", 0)',
155
+ 'If (Not f Is Nothing) Then',
156
+ ' WScript.Echo f.Self.Path',
157
+ 'End If'
158
+ ].join('\n');
159
+
160
+ const osTmp = os.tmpdir();
161
+ const vbsPath = path.join(osTmp, `browse_${Date.now()}.vbs`);
162
+ fs.writeFileSync(vbsPath, vbs, 'utf-8');
163
+ try {
164
+ const { stdout } = await runSpawn('cscript.exe', ['//nologo', vbsPath], { timeout: 60000 });
165
+ const out = (stdout || '').trim();
166
+ if (out) return out;
167
+ } finally {
168
+ try { fs.unlinkSync(vbsPath); } catch {}
169
+ }
170
+ throw new Error('Windows 目录选择失败');
171
+ }
172
+
173
+ async function darwinChooseFolder() {
174
+ const script = 'tell application "System Events" to POSIX path of (choose folder with prompt "选择保存目录")';
175
+ const { stdout } = await runSpawn('osascript', ['-e', script], { timeout: 60000 });
176
+ const out = (stdout || '').trim();
177
+ if (out) return out;
178
+ throw new Error('macOS 目录选择失败');
179
+ }
180
+
181
+ async function linuxZenity() {
182
+ const { stdout } = await runSpawn('zenity', ['--file-selection', '--directory', '--title=选择保存目录'], { timeout: 60000 });
183
+ const out = (stdout || '').trim();
184
+ if (out) return out;
185
+ throw new Error('Linux 目录选择失败 / zenity 不可用');
186
+ }
187
+
188
+ async function pickDirectory() {
189
+ const sys = os.platform(); // win32, darwin, linux
190
+ if (sys === 'win32') {
191
+ return await winBrowseForFolder();
192
+ } else if (sys === 'darwin') {
193
+ return await darwinChooseFolder();
194
+ } else if (sys === 'linux') {
195
+ return await linuxZenity();
196
+ }
197
+ throw new Error(`不支持的系统平台:${sys}`);
198
+ }
199
+
200
+ async function openDirectory(p) {
201
+ const sys = os.platform();
202
+ if (sys === 'win32') {
203
+ // 使用 explorer 打开目录;或 start 命令
204
+ await runSpawn('explorer.exe', [p]);
205
+ } else if (sys === 'darwin') {
206
+ await runSpawn('open', [p]);
207
+ } else {
208
+ // linux
209
+ await runSpawn('xdg-open', [p]);
210
+ }
211
+ }
212
+
213
+ // ---------- API 路由 ----------
214
+
215
+ // 健康检查
216
+ app.get('/api/health', (req, res) => {
217
+ res.json({ status: 'ok' });
218
+ });
219
+
220
+ // 获取配置
221
+ app.get('/api/config', (req, res) => {
222
+ try {
223
+ const cfg = readConfig();
224
+ res.json(cfg);
225
+ } catch (e) {
226
+ res.status(500).json({ detail: String(e && e.message || e) });
227
+ }
228
+ });
229
+
230
+ // 更新配置(部分字段)
231
+ app.put('/api/config', (req, res) => {
232
+ try {
233
+ const current = readConfig();
234
+ const body = req.body || {};
235
+ // 仅合并非 undefined 的字段;允许 null 写入(表示清空)
236
+ const next = { ...current };
237
+ for (const k of Object.keys(body)) {
238
+ next[k] = body[k];
239
+ }
240
+ const saved = writeConfig(next);
241
+ res.json(saved);
242
+ } catch (e) {
243
+ res.status(500).json({ detail: String(e && e.message || e) });
244
+ }
245
+ });
246
+
247
+ // 选择输出目录(桌面环境)
248
+ app.get('/api/select-output-dir', async (req, res) => {
249
+ try {
250
+ const p = await pickDirectory();
251
+ if (!p) return res.status(500).json({ detail: '未选择任何目录' });
252
+ return res.json({ path: p });
253
+ } catch (e) {
254
+ // 与前端对齐:返回 { detail }
255
+ return res.status(500).json({ detail: String(e && e.message || e) });
256
+ }
257
+ });
258
+
259
+ // 打开目录
260
+ app.post('/api/open-dir', async (req, res) => {
261
+ try {
262
+ let target = (req.body && req.body.path) || '';
263
+ if (!target) {
264
+ const cfg = readConfig();
265
+ target = cfg.output_dir || '';
266
+ }
267
+ if (!target) {
268
+ return res.status(400).json({ detail: '未提供路径且配置中未设置 output_dir' });
269
+ }
270
+ await fse.ensureDir(target);
271
+ try {
272
+ await openDirectory(target);
273
+ } catch (e) {
274
+ // 某些无 GUI 环境会失败,但仍视作已准备好目录
275
+ }
276
+ res.json({ ok: true, path: target });
277
+ } catch (e) {
278
+ res.status(500).json({ detail: String(e && e.message || e) });
279
+ }
280
+ });
281
+
282
+ // 生成接口:T2I
283
+ app.post('/api/generate/t2i', async (req, res) => {
284
+ try {
285
+ const cfg = readConfig();
286
+ if (!cfg.key) {
287
+ return res.status(400).json({ detail: '尚未配置 key,请先在配置中设置 key。' });
288
+ }
289
+ const b = req.body || {};
290
+ const { dataUri, savedPath } = await generateT2I(cfg, {
291
+ prompt: b.prompt,
292
+ negative: b.negative || '',
293
+ width: b.width ?? 768,
294
+ height: b.height ?? 768,
295
+ scale: b.scale ?? null,
296
+ steps: b.steps ?? null,
297
+ sampler: b.sampler ?? null,
298
+ noise_schedule: b.noise_schedule ?? null,
299
+ seed: b.seed ?? -1,
300
+ variety: !!b.variety,
301
+ decrisp: !!b.decrisp,
302
+ cfg_rescale: b.cfg_rescale ?? null,
303
+ });
304
+ return res.json({ image_base64: dataUri, saved_path: savedPath });
305
+ } catch (e) {
306
+ return res.status(500).json({ detail: String((e && e.message) || e) });
307
+ }
308
+ });
309
+
310
+ // 生成接口:I2I
311
+ app.post('/api/generate/i2i', async (req, res) => {
312
+ try {
313
+ const cfg = readConfig();
314
+ if (!cfg.key) {
315
+ return res.status(400).json({ detail: '尚未配置 key,请先在配置中设置 key。' });
316
+ }
317
+ const b = req.body || {};
318
+ const { dataUri, savedPath } = await generateI2I(cfg, {
319
+ positive: b.positive || '',
320
+ negative: b.negative || '',
321
+ image_base64: b.image_base64,
322
+ width: b.width ?? null,
323
+ height: b.height ?? null,
324
+ scale: b.scale ?? null,
325
+ steps: b.steps ?? null,
326
+ sampler: b.sampler ?? null,
327
+ noise_schedule: b.noise_schedule ?? null,
328
+ strength: b.strength ?? 0.5,
329
+ noise: b.noise ?? 0.0,
330
+ seed: b.seed ?? -1,
331
+ variety: !!b.variety,
332
+ decrisp: !!b.decrisp,
333
+ cfg_rescale: b.cfg_rescale ?? null,
334
+ });
335
+ return res.json({ image_base64: dataUri, saved_path: savedPath });
336
+ } catch (e) {
337
+ return res.status(500).json({ detail: String((e && e.message) || e) });
338
+ }
339
+ });
340
+
341
+ // 生成接口:Inpaint
342
+ app.post('/api/generate/inpaint', async (req, res) => {
343
+ try {
344
+ const cfg = readConfig();
345
+ if (!cfg.key) {
346
+ return res.status(400).json({ detail: '尚未配置 key,请先在配置中设置 key。' });
347
+ }
348
+ const b = req.body || {};
349
+ const { dataUri, savedPath } = await generateInpaint(cfg, {
350
+ positive: b.positive || '',
351
+ negative: b.negative || '',
352
+ image_base64: b.image_base64,
353
+ mask_base64: b.mask_base64,
354
+ add_original_image: !!b.add_original_image,
355
+ width: b.width ?? null,
356
+ height: b.height ?? null,
357
+ scale: b.scale ?? null,
358
+ steps: b.steps ?? null,
359
+ sampler: b.sampler ?? null,
360
+ noise_schedule: b.noise_schedule ?? null,
361
+ strength: b.strength ?? 0.5,
362
+ noise: b.noise ?? 0.0,
363
+ seed: b.seed ?? -1,
364
+ variety: !!b.variety,
365
+ decrisp: !!b.decrisp,
366
+ cfg_rescale: b.cfg_rescale ?? null,
367
+ });
368
+ return res.json({ image_base64: dataUri, saved_path: savedPath });
369
+ } catch (e) {
370
+ return res.status(500).json({ detail: String((e && e.message) || e) });
371
+ }
372
+ });
373
+
374
+ // ---------- 静态资源 ----------
375
+
376
+ // 铃声目录
377
+ app.use('/ring', express.static(RING_DIR, { fallthrough: true }));
378
+
379
+ // 兼容别名:/ring/ring.mp3 -> 若 ring.mp3 不存在则回退到 new-notification-3-398649.mp3
380
+ app.get('/ring/ring.mp3', (req, res, next) => {
381
+ const main = path.join(RING_DIR, 'ring.mp3');
382
+ const alt = path.join(RING_DIR, 'new-notification-3-398649.mp3');
383
+ if (fs.existsSync(main)) return res.sendFile(main);
384
+ if (fs.existsSync(alt)) return res.sendFile(alt);
385
+ return res.status(404).end();
386
+ });
387
+
388
+ // 前端静态资源(单页)
389
+ app.use('/', express.static(FRONTEND_DIR, { index: 'index.html' }));
390
+
391
+ // ---------- 启动服务 ----------
392
+ function resolvePort() {
393
+ // 优先环境变量 PORT,其次配置文件 port,最后默认 9180
394
+ const envPort = parseInt(process.env.PORT || '', 10);
395
+ if (!Number.isNaN(envPort) && envPort > 0) return envPort;
396
+ const cfg = readConfig();
397
+ const cfgPort = parseInt(String(cfg.port || ''), 10);
398
+ if (!Number.isNaN(cfgPort) && cfgPort > 0) return cfgPort;
399
+ return 9180;
400
+ }
401
+
402
+ const PORT = resolvePort();
403
+ const HOST = process.env.HOST || '0.0.0.0';
404
+
405
+ const server = http.createServer(app);
406
+ server.listen(PORT, HOST, () => {
407
+ const url = `http://${HOST}:${PORT}`;
408
+ // 模拟 Python 版本的延迟打开浏览器(仅本地)
409
+ if (HOST === '127.0.0.1' || HOST === '0.0.0.0') {
410
+ const shouldOpen = process.env.AUTO_OPEN_BROWSER !== '0';
411
+ if (shouldOpen) {
412
+ setTimeout(() => {
413
+ try {
414
+ const platform = os.platform();
415
+ if (platform === 'win32') {
416
+ // start "" "url"
417
+ spawn('cmd', ['/c', 'start', '', url], { detached: true, stdio: 'ignore' }).unref();
418
+ } else if (platform === 'darwin') {
419
+ spawn('open', [url], { detached: true, stdio: 'ignore' }).unref();
420
+ } else {
421
+ spawn('xdg-open', [url], { detached: true, stdio: 'ignore' }).unref();
422
+ }
423
+ } catch { /* ignore */ }
424
+ }, 1500);
425
+ }
426
+ }
427
+ // eslint-disable-next-line no-console
428
+ console.log(`[New NAI] Express server is running at ${url}`);
429
+ });
430
+
431
+ // 导出 app 以便测试或其他入口使用
432
+ module.exports = { app };