Logankunfall commited on
Commit
27362f1
·
verified ·
1 Parent(s): fc033bb

Upload 10 files

Browse files
.env.example ADDED
@@ -0,0 +1,17 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Chutes Image App - .env example
2
+ PORT=3000
3
+ HOST=0.0.0.0
4
+ CHUTES_API_TOKEN=
5
+ GENERATE_API_URL=https://image.chutes.ai/generate
6
+ MODELS_URL=
7
+ MOCK_MODE=false
8
+ TIMEOUT_MS=120000
9
+ LOG_LEVEL=dev
10
+ STATIC_DIR=public
11
+ # Strict routing controls
12
+ STRICT_NO_ROUTING=true
13
+ STRICT_NO_FALLBACK=true
14
+ # Auto-fallback and retry strategy (ignored when STRICT_NO_FALLBACK=true or front-end no_fallback)
15
+ AUTO_FALLBACK=true
16
+ RETRIES=2
17
+ RETRY_BASE_MS=800
Dockerfile ADDED
@@ -0,0 +1,38 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Hugging Face Spaces Dockerfile (Generated)
2
+ FROM node:18-alpine
3
+
4
+ # Install curl for health check
5
+ RUN apk add --no-cache curl
6
+
7
+ # Workdir
8
+ WORKDIR /app
9
+
10
+ # Copy package manifests first for better layer caching
11
+ COPY package.json package-lock.json* ./
12
+
13
+ # Install production deps only
14
+ RUN npm ci --omit=dev || npm i --omit=dev && npm cache clean --force
15
+
16
+ # Copy app files
17
+ COPY server.js ./server.js
18
+ COPY public/ ./public/
19
+ COPY data/ ./data/
20
+ COPY studio/ ./studio/
21
+ COPY utils/ ./utils/
22
+ COPY .env.example ./.env.example
23
+
24
+ # Ensure no persistent logs; we provided a no-op logger implementation in utils/
25
+ # Expose HF default port
26
+ EXPOSE 7860
27
+
28
+ # HF runtime envs
29
+ ENV PORT=7860
30
+ ENV HOST=0.0.0.0
31
+ ENV NODE_ENV=production
32
+
33
+ # Healthcheck
34
+ HEALTHCHECK --interval=30s --timeout=5s --start-period=5s --retries=3 \
35
+ CMD curl -f http://localhost:7860/api/health || exit 1
36
+
37
+ # Start
38
+ CMD ["node", "server.js"]
README.md CHANGED
@@ -1,10 +1,34 @@
1
  ---
2
- title: Chutes
3
- emoji: 📉
4
- colorFrom: red
5
- colorTo: yellow
6
  sdk: docker
7
  pinned: false
 
 
8
  ---
9
 
10
- Check out the configuration reference at https://huggingface.co/docs/hub/spaces-config-reference
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
  ---
2
+ title: Chutes Image Generator
3
+ emoji: 🎨
4
+ colorFrom: blue
5
+ colorTo: purple
6
  sdk: docker
7
  pinned: false
8
+ license: mit
9
+ app_port: 7860
10
  ---
11
 
12
+ # Chutes Image Generator (Hugging Face)
13
+
14
+ 这是一个基于 Chutes.ai API 的图片生成应用的 Hugging Face 部署包(生成自本地项目)。
15
+
16
+ 重要说明(安全与隐私):
17
+ - 本部署包不包含任何服务器端 API Key,也不会写入任何服务器端日志文件
18
+ - 用户需要在前端页面右上角输入自己的 CHUTES_API_TOKEN(UI 会将其以请求头 x-api-key 使用)
19
+ - 不会在服务器端持久化用户的 Token
20
+
21
+ ## 部署
22
+
23
+ 1) 创建 Space(SDK 选择 Docker)
24
+ 2) 上传本目录全部文件
25
+ 3) 等待构建完成即可访问
26
+
27
+ ## 运行时环境变量(可选)
28
+ - MODELS_URL:远程模型列表(不设置则使用本地 data/models.json)
29
+ - MOCK_MODE:true/false(可选)
30
+ - 其他参见 server.js 顶部环境变量注释(无需设置 PORT/HOST,已固定为 HF 默认)
31
+
32
+ ## 使用
33
+ - 在页面右上角粘贴你的 CHUTES_API_TOKEN
34
+ - 选择模型、输入提示词,点击生成
data/models.json ADDED
@@ -0,0 +1,22 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ [
2
+ { "id": "JuggernautXL", "name": "JuggernautXL", "free": true },
3
+ { "id": "qwen-image", "name": "qwen-image", "free": false },
4
+ { "id": "hidream", "name": "hidream", "free": false },
5
+ { "id": "FLUX.1-dev", "name": "FLUX.1-dev", "free": false },
6
+ { "id": "FLUX.1-schnell", "name": "FLUX.1-schnell", "free": false },
7
+ { "id": "chroma", "name": "chroma", "free": true },
8
+ { "id": "neta-lumina", "name": "neta-lumina", "free": true },
9
+ { "id": "nova-anime3d-xl", "name": "nova-anime3d-xl", "free": false },
10
+ { "id": "Illustrij", "name": "Illustrij", "free": false },
11
+ { "id": "stabilityai/stable-diffusion-xl-base-1.0", "name": "stable-diffusion-xl-base-1.0", "free": false },
12
+ { "id": "iLustMix", "name": "iLustMix", "free": false },
13
+ { "id": "Animij", "name": "Animij", "free": false },
14
+ { "id": "NovaFurryXL", "name": "NovaFurryXL", "free": false },
15
+ { "id": "Lykon/dreamshaper-xl-1-0", "name": "dreamshaper-xl-1-0", "free": false },
16
+ { "id": "Shitao/OmniGen-v1", "name": "OmniGen-v1", "free": false },
17
+ { "id": "diagonalge/Booba", "name": "Booba", "free": false },
18
+ { "id": "HassakuXL", "name": "HassakuXL", "free": false },
19
+ { "id": "nova-cartoon-xl", "name": "nova-cartoon-xl", "free": false },
20
+ { "id": "orphic-lora", "name": "orphic-lora", "free": false },
21
+ { "id": "diagonalge/ConstShaper", "name": "ConstShaper", "free": false }
22
+ ]
package-lock.json ADDED
@@ -0,0 +1,1475 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "name": "chutes-image-app",
3
+ "version": "1.0.0",
4
+ "lockfileVersion": 3,
5
+ "requires": true,
6
+ "packages": {
7
+ "": {
8
+ "name": "chutes-image-app",
9
+ "version": "1.0.0",
10
+ "hasInstallScript": true,
11
+ "license": "MIT",
12
+ "dependencies": {
13
+ "axios": "^1.7.7",
14
+ "compression": "^1.7.4",
15
+ "cors": "^2.8.5",
16
+ "dotenv": "^16.4.5",
17
+ "express": "^4.19.2",
18
+ "helmet": "^7.1.0",
19
+ "morgan": "^1.10.0"
20
+ },
21
+ "devDependencies": {
22
+ "nodemon": "^3.1.0"
23
+ },
24
+ "engines": {
25
+ "node": ">=18"
26
+ }
27
+ },
28
+ "node_modules/accepts": {
29
+ "version": "1.3.8",
30
+ "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz",
31
+ "integrity": "sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==",
32
+ "license": "MIT",
33
+ "dependencies": {
34
+ "mime-types": "~2.1.34",
35
+ "negotiator": "0.6.3"
36
+ },
37
+ "engines": {
38
+ "node": ">= 0.6"
39
+ }
40
+ },
41
+ "node_modules/accepts/node_modules/negotiator": {
42
+ "version": "0.6.3",
43
+ "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz",
44
+ "integrity": "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==",
45
+ "license": "MIT",
46
+ "engines": {
47
+ "node": ">= 0.6"
48
+ }
49
+ },
50
+ "node_modules/anymatch": {
51
+ "version": "3.1.3",
52
+ "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz",
53
+ "integrity": "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==",
54
+ "dev": true,
55
+ "license": "ISC",
56
+ "dependencies": {
57
+ "normalize-path": "^3.0.0",
58
+ "picomatch": "^2.0.4"
59
+ },
60
+ "engines": {
61
+ "node": ">= 8"
62
+ }
63
+ },
64
+ "node_modules/array-flatten": {
65
+ "version": "1.1.1",
66
+ "resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz",
67
+ "integrity": "sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg==",
68
+ "license": "MIT"
69
+ },
70
+ "node_modules/asynckit": {
71
+ "version": "0.4.0",
72
+ "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz",
73
+ "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==",
74
+ "license": "MIT"
75
+ },
76
+ "node_modules/axios": {
77
+ "version": "1.12.2",
78
+ "resolved": "https://registry.npmjs.org/axios/-/axios-1.12.2.tgz",
79
+ "integrity": "sha512-vMJzPewAlRyOgxV2dU0Cuz2O8zzzx9VYtbJOaBgXFeLc4IV/Eg50n4LowmehOOR61S8ZMpc2K5Sa7g6A4jfkUw==",
80
+ "license": "MIT",
81
+ "dependencies": {
82
+ "follow-redirects": "^1.15.6",
83
+ "form-data": "^4.0.4",
84
+ "proxy-from-env": "^1.1.0"
85
+ }
86
+ },
87
+ "node_modules/balanced-match": {
88
+ "version": "1.0.2",
89
+ "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz",
90
+ "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==",
91
+ "dev": true,
92
+ "license": "MIT"
93
+ },
94
+ "node_modules/basic-auth": {
95
+ "version": "2.0.1",
96
+ "resolved": "https://registry.npmjs.org/basic-auth/-/basic-auth-2.0.1.tgz",
97
+ "integrity": "sha512-NF+epuEdnUYVlGuhaxbbq+dvJttwLnGY+YixlXlME5KpQ5W3CnXA5cVTneY3SPbPDRkcjMbifrwmFYcClgOZeg==",
98
+ "license": "MIT",
99
+ "dependencies": {
100
+ "safe-buffer": "5.1.2"
101
+ },
102
+ "engines": {
103
+ "node": ">= 0.8"
104
+ }
105
+ },
106
+ "node_modules/basic-auth/node_modules/safe-buffer": {
107
+ "version": "5.1.2",
108
+ "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz",
109
+ "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==",
110
+ "license": "MIT"
111
+ },
112
+ "node_modules/binary-extensions": {
113
+ "version": "2.3.0",
114
+ "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.3.0.tgz",
115
+ "integrity": "sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==",
116
+ "dev": true,
117
+ "license": "MIT",
118
+ "engines": {
119
+ "node": ">=8"
120
+ },
121
+ "funding": {
122
+ "url": "https://github.com/sponsors/sindresorhus"
123
+ }
124
+ },
125
+ "node_modules/body-parser": {
126
+ "version": "1.20.3",
127
+ "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.3.tgz",
128
+ "integrity": "sha512-7rAxByjUMqQ3/bHJy7D6OGXvx/MMc4IqBn/X0fcM1QUcAItpZrBEYhWGem+tzXH90c+G01ypMcYJBO9Y30203g==",
129
+ "license": "MIT",
130
+ "dependencies": {
131
+ "bytes": "3.1.2",
132
+ "content-type": "~1.0.5",
133
+ "debug": "2.6.9",
134
+ "depd": "2.0.0",
135
+ "destroy": "1.2.0",
136
+ "http-errors": "2.0.0",
137
+ "iconv-lite": "0.4.24",
138
+ "on-finished": "2.4.1",
139
+ "qs": "6.13.0",
140
+ "raw-body": "2.5.2",
141
+ "type-is": "~1.6.18",
142
+ "unpipe": "1.0.0"
143
+ },
144
+ "engines": {
145
+ "node": ">= 0.8",
146
+ "npm": "1.2.8000 || >= 1.4.16"
147
+ }
148
+ },
149
+ "node_modules/brace-expansion": {
150
+ "version": "1.1.12",
151
+ "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz",
152
+ "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==",
153
+ "dev": true,
154
+ "license": "MIT",
155
+ "dependencies": {
156
+ "balanced-match": "^1.0.0",
157
+ "concat-map": "0.0.1"
158
+ }
159
+ },
160
+ "node_modules/braces": {
161
+ "version": "3.0.3",
162
+ "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz",
163
+ "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==",
164
+ "dev": true,
165
+ "license": "MIT",
166
+ "dependencies": {
167
+ "fill-range": "^7.1.1"
168
+ },
169
+ "engines": {
170
+ "node": ">=8"
171
+ }
172
+ },
173
+ "node_modules/bytes": {
174
+ "version": "3.1.2",
175
+ "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz",
176
+ "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==",
177
+ "license": "MIT",
178
+ "engines": {
179
+ "node": ">= 0.8"
180
+ }
181
+ },
182
+ "node_modules/call-bind-apply-helpers": {
183
+ "version": "1.0.2",
184
+ "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz",
185
+ "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==",
186
+ "license": "MIT",
187
+ "dependencies": {
188
+ "es-errors": "^1.3.0",
189
+ "function-bind": "^1.1.2"
190
+ },
191
+ "engines": {
192
+ "node": ">= 0.4"
193
+ }
194
+ },
195
+ "node_modules/call-bound": {
196
+ "version": "1.0.4",
197
+ "resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.4.tgz",
198
+ "integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==",
199
+ "license": "MIT",
200
+ "dependencies": {
201
+ "call-bind-apply-helpers": "^1.0.2",
202
+ "get-intrinsic": "^1.3.0"
203
+ },
204
+ "engines": {
205
+ "node": ">= 0.4"
206
+ },
207
+ "funding": {
208
+ "url": "https://github.com/sponsors/ljharb"
209
+ }
210
+ },
211
+ "node_modules/chokidar": {
212
+ "version": "3.6.0",
213
+ "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz",
214
+ "integrity": "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==",
215
+ "dev": true,
216
+ "license": "MIT",
217
+ "dependencies": {
218
+ "anymatch": "~3.1.2",
219
+ "braces": "~3.0.2",
220
+ "glob-parent": "~5.1.2",
221
+ "is-binary-path": "~2.1.0",
222
+ "is-glob": "~4.0.1",
223
+ "normalize-path": "~3.0.0",
224
+ "readdirp": "~3.6.0"
225
+ },
226
+ "engines": {
227
+ "node": ">= 8.10.0"
228
+ },
229
+ "funding": {
230
+ "url": "https://paulmillr.com/funding/"
231
+ },
232
+ "optionalDependencies": {
233
+ "fsevents": "~2.3.2"
234
+ }
235
+ },
236
+ "node_modules/combined-stream": {
237
+ "version": "1.0.8",
238
+ "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz",
239
+ "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==",
240
+ "license": "MIT",
241
+ "dependencies": {
242
+ "delayed-stream": "~1.0.0"
243
+ },
244
+ "engines": {
245
+ "node": ">= 0.8"
246
+ }
247
+ },
248
+ "node_modules/compressible": {
249
+ "version": "2.0.18",
250
+ "resolved": "https://registry.npmjs.org/compressible/-/compressible-2.0.18.tgz",
251
+ "integrity": "sha512-AF3r7P5dWxL8MxyITRMlORQNaOA2IkAFaTr4k7BUumjPtRpGDTZpl0Pb1XCO6JeDCBdp126Cgs9sMxqSjgYyRg==",
252
+ "license": "MIT",
253
+ "dependencies": {
254
+ "mime-db": ">= 1.43.0 < 2"
255
+ },
256
+ "engines": {
257
+ "node": ">= 0.6"
258
+ }
259
+ },
260
+ "node_modules/compression": {
261
+ "version": "1.8.1",
262
+ "resolved": "https://registry.npmjs.org/compression/-/compression-1.8.1.tgz",
263
+ "integrity": "sha512-9mAqGPHLakhCLeNyxPkK4xVo746zQ/czLH1Ky+vkitMnWfWZps8r0qXuwhwizagCRttsL4lfG4pIOvaWLpAP0w==",
264
+ "license": "MIT",
265
+ "dependencies": {
266
+ "bytes": "3.1.2",
267
+ "compressible": "~2.0.18",
268
+ "debug": "2.6.9",
269
+ "negotiator": "~0.6.4",
270
+ "on-headers": "~1.1.0",
271
+ "safe-buffer": "5.2.1",
272
+ "vary": "~1.1.2"
273
+ },
274
+ "engines": {
275
+ "node": ">= 0.8.0"
276
+ }
277
+ },
278
+ "node_modules/concat-map": {
279
+ "version": "0.0.1",
280
+ "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz",
281
+ "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==",
282
+ "dev": true,
283
+ "license": "MIT"
284
+ },
285
+ "node_modules/content-disposition": {
286
+ "version": "0.5.4",
287
+ "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.4.tgz",
288
+ "integrity": "sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ==",
289
+ "license": "MIT",
290
+ "dependencies": {
291
+ "safe-buffer": "5.2.1"
292
+ },
293
+ "engines": {
294
+ "node": ">= 0.6"
295
+ }
296
+ },
297
+ "node_modules/content-type": {
298
+ "version": "1.0.5",
299
+ "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.5.tgz",
300
+ "integrity": "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==",
301
+ "license": "MIT",
302
+ "engines": {
303
+ "node": ">= 0.6"
304
+ }
305
+ },
306
+ "node_modules/cookie": {
307
+ "version": "0.7.1",
308
+ "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.1.tgz",
309
+ "integrity": "sha512-6DnInpx7SJ2AK3+CTUE/ZM0vWTUboZCegxhC2xiIydHR9jNuTAASBrfEpHhiGOZw/nX51bHt6YQl8jsGo4y/0w==",
310
+ "license": "MIT",
311
+ "engines": {
312
+ "node": ">= 0.6"
313
+ }
314
+ },
315
+ "node_modules/cookie-signature": {
316
+ "version": "1.0.6",
317
+ "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.6.tgz",
318
+ "integrity": "sha512-QADzlaHc8icV8I7vbaJXJwod9HWYp8uCqf1xa4OfNu1T7JVxQIrUgOWtHdNDtPiywmFbiS12VjotIXLrKM3orQ==",
319
+ "license": "MIT"
320
+ },
321
+ "node_modules/cors": {
322
+ "version": "2.8.5",
323
+ "resolved": "https://registry.npmjs.org/cors/-/cors-2.8.5.tgz",
324
+ "integrity": "sha512-KIHbLJqu73RGr/hnbrO9uBeixNGuvSQjul/jdFvS/KFSIH1hWVd1ng7zOHx+YrEfInLG7q4n6GHQ9cDtxv/P6g==",
325
+ "license": "MIT",
326
+ "dependencies": {
327
+ "object-assign": "^4",
328
+ "vary": "^1"
329
+ },
330
+ "engines": {
331
+ "node": ">= 0.10"
332
+ }
333
+ },
334
+ "node_modules/debug": {
335
+ "version": "2.6.9",
336
+ "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz",
337
+ "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==",
338
+ "license": "MIT",
339
+ "dependencies": {
340
+ "ms": "2.0.0"
341
+ }
342
+ },
343
+ "node_modules/delayed-stream": {
344
+ "version": "1.0.0",
345
+ "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz",
346
+ "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==",
347
+ "license": "MIT",
348
+ "engines": {
349
+ "node": ">=0.4.0"
350
+ }
351
+ },
352
+ "node_modules/depd": {
353
+ "version": "2.0.0",
354
+ "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz",
355
+ "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==",
356
+ "license": "MIT",
357
+ "engines": {
358
+ "node": ">= 0.8"
359
+ }
360
+ },
361
+ "node_modules/destroy": {
362
+ "version": "1.2.0",
363
+ "resolved": "https://registry.npmjs.org/destroy/-/destroy-1.2.0.tgz",
364
+ "integrity": "sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg==",
365
+ "license": "MIT",
366
+ "engines": {
367
+ "node": ">= 0.8",
368
+ "npm": "1.2.8000 || >= 1.4.16"
369
+ }
370
+ },
371
+ "node_modules/dotenv": {
372
+ "version": "16.6.1",
373
+ "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.6.1.tgz",
374
+ "integrity": "sha512-uBq4egWHTcTt33a72vpSG0z3HnPuIl6NqYcTrKEg2azoEyl2hpW0zqlxysq2pK9HlDIHyHyakeYaYnSAwd8bow==",
375
+ "license": "BSD-2-Clause",
376
+ "engines": {
377
+ "node": ">=12"
378
+ },
379
+ "funding": {
380
+ "url": "https://dotenvx.com"
381
+ }
382
+ },
383
+ "node_modules/dunder-proto": {
384
+ "version": "1.0.1",
385
+ "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz",
386
+ "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==",
387
+ "license": "MIT",
388
+ "dependencies": {
389
+ "call-bind-apply-helpers": "^1.0.1",
390
+ "es-errors": "^1.3.0",
391
+ "gopd": "^1.2.0"
392
+ },
393
+ "engines": {
394
+ "node": ">= 0.4"
395
+ }
396
+ },
397
+ "node_modules/ee-first": {
398
+ "version": "1.1.1",
399
+ "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz",
400
+ "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==",
401
+ "license": "MIT"
402
+ },
403
+ "node_modules/encodeurl": {
404
+ "version": "2.0.0",
405
+ "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz",
406
+ "integrity": "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==",
407
+ "license": "MIT",
408
+ "engines": {
409
+ "node": ">= 0.8"
410
+ }
411
+ },
412
+ "node_modules/es-define-property": {
413
+ "version": "1.0.1",
414
+ "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz",
415
+ "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==",
416
+ "license": "MIT",
417
+ "engines": {
418
+ "node": ">= 0.4"
419
+ }
420
+ },
421
+ "node_modules/es-errors": {
422
+ "version": "1.3.0",
423
+ "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz",
424
+ "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==",
425
+ "license": "MIT",
426
+ "engines": {
427
+ "node": ">= 0.4"
428
+ }
429
+ },
430
+ "node_modules/es-object-atoms": {
431
+ "version": "1.1.1",
432
+ "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz",
433
+ "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==",
434
+ "license": "MIT",
435
+ "dependencies": {
436
+ "es-errors": "^1.3.0"
437
+ },
438
+ "engines": {
439
+ "node": ">= 0.4"
440
+ }
441
+ },
442
+ "node_modules/es-set-tostringtag": {
443
+ "version": "2.1.0",
444
+ "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz",
445
+ "integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==",
446
+ "license": "MIT",
447
+ "dependencies": {
448
+ "es-errors": "^1.3.0",
449
+ "get-intrinsic": "^1.2.6",
450
+ "has-tostringtag": "^1.0.2",
451
+ "hasown": "^2.0.2"
452
+ },
453
+ "engines": {
454
+ "node": ">= 0.4"
455
+ }
456
+ },
457
+ "node_modules/escape-html": {
458
+ "version": "1.0.3",
459
+ "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz",
460
+ "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==",
461
+ "license": "MIT"
462
+ },
463
+ "node_modules/etag": {
464
+ "version": "1.8.1",
465
+ "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz",
466
+ "integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==",
467
+ "license": "MIT",
468
+ "engines": {
469
+ "node": ">= 0.6"
470
+ }
471
+ },
472
+ "node_modules/express": {
473
+ "version": "4.21.2",
474
+ "resolved": "https://registry.npmjs.org/express/-/express-4.21.2.tgz",
475
+ "integrity": "sha512-28HqgMZAmih1Czt9ny7qr6ek2qddF4FclbMzwhCREB6OFfH+rXAnuNCwo1/wFvrtbgsQDb4kSbX9de9lFbrXnA==",
476
+ "license": "MIT",
477
+ "dependencies": {
478
+ "accepts": "~1.3.8",
479
+ "array-flatten": "1.1.1",
480
+ "body-parser": "1.20.3",
481
+ "content-disposition": "0.5.4",
482
+ "content-type": "~1.0.4",
483
+ "cookie": "0.7.1",
484
+ "cookie-signature": "1.0.6",
485
+ "debug": "2.6.9",
486
+ "depd": "2.0.0",
487
+ "encodeurl": "~2.0.0",
488
+ "escape-html": "~1.0.3",
489
+ "etag": "~1.8.1",
490
+ "finalhandler": "1.3.1",
491
+ "fresh": "0.5.2",
492
+ "http-errors": "2.0.0",
493
+ "merge-descriptors": "1.0.3",
494
+ "methods": "~1.1.2",
495
+ "on-finished": "2.4.1",
496
+ "parseurl": "~1.3.3",
497
+ "path-to-regexp": "0.1.12",
498
+ "proxy-addr": "~2.0.7",
499
+ "qs": "6.13.0",
500
+ "range-parser": "~1.2.1",
501
+ "safe-buffer": "5.2.1",
502
+ "send": "0.19.0",
503
+ "serve-static": "1.16.2",
504
+ "setprototypeof": "1.2.0",
505
+ "statuses": "2.0.1",
506
+ "type-is": "~1.6.18",
507
+ "utils-merge": "1.0.1",
508
+ "vary": "~1.1.2"
509
+ },
510
+ "engines": {
511
+ "node": ">= 0.10.0"
512
+ },
513
+ "funding": {
514
+ "type": "opencollective",
515
+ "url": "https://opencollective.com/express"
516
+ }
517
+ },
518
+ "node_modules/fill-range": {
519
+ "version": "7.1.1",
520
+ "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz",
521
+ "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==",
522
+ "dev": true,
523
+ "license": "MIT",
524
+ "dependencies": {
525
+ "to-regex-range": "^5.0.1"
526
+ },
527
+ "engines": {
528
+ "node": ">=8"
529
+ }
530
+ },
531
+ "node_modules/finalhandler": {
532
+ "version": "1.3.1",
533
+ "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.3.1.tgz",
534
+ "integrity": "sha512-6BN9trH7bp3qvnrRyzsBz+g3lZxTNZTbVO2EV1CS0WIcDbawYVdYvGflME/9QP0h0pYlCDBCTjYa9nZzMDpyxQ==",
535
+ "license": "MIT",
536
+ "dependencies": {
537
+ "debug": "2.6.9",
538
+ "encodeurl": "~2.0.0",
539
+ "escape-html": "~1.0.3",
540
+ "on-finished": "2.4.1",
541
+ "parseurl": "~1.3.3",
542
+ "statuses": "2.0.1",
543
+ "unpipe": "~1.0.0"
544
+ },
545
+ "engines": {
546
+ "node": ">= 0.8"
547
+ }
548
+ },
549
+ "node_modules/follow-redirects": {
550
+ "version": "1.15.11",
551
+ "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.11.tgz",
552
+ "integrity": "sha512-deG2P0JfjrTxl50XGCDyfI97ZGVCxIpfKYmfyrQ54n5FO/0gfIES8C/Psl6kWVDolizcaaxZJnTS0QSMxvnsBQ==",
553
+ "funding": [
554
+ {
555
+ "type": "individual",
556
+ "url": "https://github.com/sponsors/RubenVerborgh"
557
+ }
558
+ ],
559
+ "license": "MIT",
560
+ "engines": {
561
+ "node": ">=4.0"
562
+ },
563
+ "peerDependenciesMeta": {
564
+ "debug": {
565
+ "optional": true
566
+ }
567
+ }
568
+ },
569
+ "node_modules/form-data": {
570
+ "version": "4.0.4",
571
+ "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.4.tgz",
572
+ "integrity": "sha512-KrGhL9Q4zjj0kiUt5OO4Mr/A/jlI2jDYs5eHBpYHPcBEVSiipAvn2Ko2HnPe20rmcuuvMHNdZFp+4IlGTMF0Ow==",
573
+ "license": "MIT",
574
+ "dependencies": {
575
+ "asynckit": "^0.4.0",
576
+ "combined-stream": "^1.0.8",
577
+ "es-set-tostringtag": "^2.1.0",
578
+ "hasown": "^2.0.2",
579
+ "mime-types": "^2.1.12"
580
+ },
581
+ "engines": {
582
+ "node": ">= 6"
583
+ }
584
+ },
585
+ "node_modules/forwarded": {
586
+ "version": "0.2.0",
587
+ "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz",
588
+ "integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==",
589
+ "license": "MIT",
590
+ "engines": {
591
+ "node": ">= 0.6"
592
+ }
593
+ },
594
+ "node_modules/fresh": {
595
+ "version": "0.5.2",
596
+ "resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz",
597
+ "integrity": "sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q==",
598
+ "license": "MIT",
599
+ "engines": {
600
+ "node": ">= 0.6"
601
+ }
602
+ },
603
+ "node_modules/fsevents": {
604
+ "version": "2.3.3",
605
+ "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz",
606
+ "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==",
607
+ "dev": true,
608
+ "hasInstallScript": true,
609
+ "license": "MIT",
610
+ "optional": true,
611
+ "os": [
612
+ "darwin"
613
+ ],
614
+ "engines": {
615
+ "node": "^8.16.0 || ^10.6.0 || >=11.0.0"
616
+ }
617
+ },
618
+ "node_modules/function-bind": {
619
+ "version": "1.1.2",
620
+ "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz",
621
+ "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==",
622
+ "license": "MIT",
623
+ "funding": {
624
+ "url": "https://github.com/sponsors/ljharb"
625
+ }
626
+ },
627
+ "node_modules/get-intrinsic": {
628
+ "version": "1.3.0",
629
+ "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz",
630
+ "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==",
631
+ "license": "MIT",
632
+ "dependencies": {
633
+ "call-bind-apply-helpers": "^1.0.2",
634
+ "es-define-property": "^1.0.1",
635
+ "es-errors": "^1.3.0",
636
+ "es-object-atoms": "^1.1.1",
637
+ "function-bind": "^1.1.2",
638
+ "get-proto": "^1.0.1",
639
+ "gopd": "^1.2.0",
640
+ "has-symbols": "^1.1.0",
641
+ "hasown": "^2.0.2",
642
+ "math-intrinsics": "^1.1.0"
643
+ },
644
+ "engines": {
645
+ "node": ">= 0.4"
646
+ },
647
+ "funding": {
648
+ "url": "https://github.com/sponsors/ljharb"
649
+ }
650
+ },
651
+ "node_modules/get-proto": {
652
+ "version": "1.0.1",
653
+ "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz",
654
+ "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==",
655
+ "license": "MIT",
656
+ "dependencies": {
657
+ "dunder-proto": "^1.0.1",
658
+ "es-object-atoms": "^1.0.0"
659
+ },
660
+ "engines": {
661
+ "node": ">= 0.4"
662
+ }
663
+ },
664
+ "node_modules/glob-parent": {
665
+ "version": "5.1.2",
666
+ "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz",
667
+ "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==",
668
+ "dev": true,
669
+ "license": "ISC",
670
+ "dependencies": {
671
+ "is-glob": "^4.0.1"
672
+ },
673
+ "engines": {
674
+ "node": ">= 6"
675
+ }
676
+ },
677
+ "node_modules/gopd": {
678
+ "version": "1.2.0",
679
+ "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz",
680
+ "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==",
681
+ "license": "MIT",
682
+ "engines": {
683
+ "node": ">= 0.4"
684
+ },
685
+ "funding": {
686
+ "url": "https://github.com/sponsors/ljharb"
687
+ }
688
+ },
689
+ "node_modules/has-flag": {
690
+ "version": "3.0.0",
691
+ "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz",
692
+ "integrity": "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==",
693
+ "dev": true,
694
+ "license": "MIT",
695
+ "engines": {
696
+ "node": ">=4"
697
+ }
698
+ },
699
+ "node_modules/has-symbols": {
700
+ "version": "1.1.0",
701
+ "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz",
702
+ "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==",
703
+ "license": "MIT",
704
+ "engines": {
705
+ "node": ">= 0.4"
706
+ },
707
+ "funding": {
708
+ "url": "https://github.com/sponsors/ljharb"
709
+ }
710
+ },
711
+ "node_modules/has-tostringtag": {
712
+ "version": "1.0.2",
713
+ "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz",
714
+ "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==",
715
+ "license": "MIT",
716
+ "dependencies": {
717
+ "has-symbols": "^1.0.3"
718
+ },
719
+ "engines": {
720
+ "node": ">= 0.4"
721
+ },
722
+ "funding": {
723
+ "url": "https://github.com/sponsors/ljharb"
724
+ }
725
+ },
726
+ "node_modules/hasown": {
727
+ "version": "2.0.2",
728
+ "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz",
729
+ "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==",
730
+ "license": "MIT",
731
+ "dependencies": {
732
+ "function-bind": "^1.1.2"
733
+ },
734
+ "engines": {
735
+ "node": ">= 0.4"
736
+ }
737
+ },
738
+ "node_modules/helmet": {
739
+ "version": "7.2.0",
740
+ "resolved": "https://registry.npmjs.org/helmet/-/helmet-7.2.0.tgz",
741
+ "integrity": "sha512-ZRiwvN089JfMXokizgqEPXsl2Guk094yExfoDXR0cBYWxtBbaSww/w+vT4WEJsBW2iTUi1GgZ6swmoug3Oy4Xw==",
742
+ "license": "MIT",
743
+ "engines": {
744
+ "node": ">=16.0.0"
745
+ }
746
+ },
747
+ "node_modules/http-errors": {
748
+ "version": "2.0.0",
749
+ "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.0.tgz",
750
+ "integrity": "sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ==",
751
+ "license": "MIT",
752
+ "dependencies": {
753
+ "depd": "2.0.0",
754
+ "inherits": "2.0.4",
755
+ "setprototypeof": "1.2.0",
756
+ "statuses": "2.0.1",
757
+ "toidentifier": "1.0.1"
758
+ },
759
+ "engines": {
760
+ "node": ">= 0.8"
761
+ }
762
+ },
763
+ "node_modules/iconv-lite": {
764
+ "version": "0.4.24",
765
+ "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz",
766
+ "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==",
767
+ "license": "MIT",
768
+ "dependencies": {
769
+ "safer-buffer": ">= 2.1.2 < 3"
770
+ },
771
+ "engines": {
772
+ "node": ">=0.10.0"
773
+ }
774
+ },
775
+ "node_modules/ignore-by-default": {
776
+ "version": "1.0.1",
777
+ "resolved": "https://registry.npmjs.org/ignore-by-default/-/ignore-by-default-1.0.1.tgz",
778
+ "integrity": "sha512-Ius2VYcGNk7T90CppJqcIkS5ooHUZyIQK+ClZfMfMNFEF9VSE73Fq+906u/CWu92x4gzZMWOwfFYckPObzdEbA==",
779
+ "dev": true,
780
+ "license": "ISC"
781
+ },
782
+ "node_modules/inherits": {
783
+ "version": "2.0.4",
784
+ "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz",
785
+ "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==",
786
+ "license": "ISC"
787
+ },
788
+ "node_modules/ipaddr.js": {
789
+ "version": "1.9.1",
790
+ "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz",
791
+ "integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==",
792
+ "license": "MIT",
793
+ "engines": {
794
+ "node": ">= 0.10"
795
+ }
796
+ },
797
+ "node_modules/is-binary-path": {
798
+ "version": "2.1.0",
799
+ "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz",
800
+ "integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==",
801
+ "dev": true,
802
+ "license": "MIT",
803
+ "dependencies": {
804
+ "binary-extensions": "^2.0.0"
805
+ },
806
+ "engines": {
807
+ "node": ">=8"
808
+ }
809
+ },
810
+ "node_modules/is-extglob": {
811
+ "version": "2.1.1",
812
+ "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz",
813
+ "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==",
814
+ "dev": true,
815
+ "license": "MIT",
816
+ "engines": {
817
+ "node": ">=0.10.0"
818
+ }
819
+ },
820
+ "node_modules/is-glob": {
821
+ "version": "4.0.3",
822
+ "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz",
823
+ "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==",
824
+ "dev": true,
825
+ "license": "MIT",
826
+ "dependencies": {
827
+ "is-extglob": "^2.1.1"
828
+ },
829
+ "engines": {
830
+ "node": ">=0.10.0"
831
+ }
832
+ },
833
+ "node_modules/is-number": {
834
+ "version": "7.0.0",
835
+ "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz",
836
+ "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==",
837
+ "dev": true,
838
+ "license": "MIT",
839
+ "engines": {
840
+ "node": ">=0.12.0"
841
+ }
842
+ },
843
+ "node_modules/math-intrinsics": {
844
+ "version": "1.1.0",
845
+ "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz",
846
+ "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==",
847
+ "license": "MIT",
848
+ "engines": {
849
+ "node": ">= 0.4"
850
+ }
851
+ },
852
+ "node_modules/media-typer": {
853
+ "version": "0.3.0",
854
+ "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz",
855
+ "integrity": "sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==",
856
+ "license": "MIT",
857
+ "engines": {
858
+ "node": ">= 0.6"
859
+ }
860
+ },
861
+ "node_modules/merge-descriptors": {
862
+ "version": "1.0.3",
863
+ "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.3.tgz",
864
+ "integrity": "sha512-gaNvAS7TZ897/rVaZ0nMtAyxNyi/pdbjbAwUpFQpN70GqnVfOiXpeUUMKRBmzXaSQ8DdTX4/0ms62r2K+hE6mQ==",
865
+ "license": "MIT",
866
+ "funding": {
867
+ "url": "https://github.com/sponsors/sindresorhus"
868
+ }
869
+ },
870
+ "node_modules/methods": {
871
+ "version": "1.1.2",
872
+ "resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz",
873
+ "integrity": "sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w==",
874
+ "license": "MIT",
875
+ "engines": {
876
+ "node": ">= 0.6"
877
+ }
878
+ },
879
+ "node_modules/mime": {
880
+ "version": "1.6.0",
881
+ "resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz",
882
+ "integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==",
883
+ "license": "MIT",
884
+ "bin": {
885
+ "mime": "cli.js"
886
+ },
887
+ "engines": {
888
+ "node": ">=4"
889
+ }
890
+ },
891
+ "node_modules/mime-db": {
892
+ "version": "1.54.0",
893
+ "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.54.0.tgz",
894
+ "integrity": "sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ==",
895
+ "license": "MIT",
896
+ "engines": {
897
+ "node": ">= 0.6"
898
+ }
899
+ },
900
+ "node_modules/mime-types": {
901
+ "version": "2.1.35",
902
+ "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz",
903
+ "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==",
904
+ "license": "MIT",
905
+ "dependencies": {
906
+ "mime-db": "1.52.0"
907
+ },
908
+ "engines": {
909
+ "node": ">= 0.6"
910
+ }
911
+ },
912
+ "node_modules/mime-types/node_modules/mime-db": {
913
+ "version": "1.52.0",
914
+ "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz",
915
+ "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==",
916
+ "license": "MIT",
917
+ "engines": {
918
+ "node": ">= 0.6"
919
+ }
920
+ },
921
+ "node_modules/minimatch": {
922
+ "version": "3.1.2",
923
+ "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz",
924
+ "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==",
925
+ "dev": true,
926
+ "license": "ISC",
927
+ "dependencies": {
928
+ "brace-expansion": "^1.1.7"
929
+ },
930
+ "engines": {
931
+ "node": "*"
932
+ }
933
+ },
934
+ "node_modules/morgan": {
935
+ "version": "1.10.1",
936
+ "resolved": "https://registry.npmjs.org/morgan/-/morgan-1.10.1.tgz",
937
+ "integrity": "sha512-223dMRJtI/l25dJKWpgij2cMtywuG/WiUKXdvwfbhGKBhy1puASqXwFzmWZ7+K73vUPoR7SS2Qz2cI/g9MKw0A==",
938
+ "license": "MIT",
939
+ "dependencies": {
940
+ "basic-auth": "~2.0.1",
941
+ "debug": "2.6.9",
942
+ "depd": "~2.0.0",
943
+ "on-finished": "~2.3.0",
944
+ "on-headers": "~1.1.0"
945
+ },
946
+ "engines": {
947
+ "node": ">= 0.8.0"
948
+ }
949
+ },
950
+ "node_modules/morgan/node_modules/on-finished": {
951
+ "version": "2.3.0",
952
+ "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.3.0.tgz",
953
+ "integrity": "sha512-ikqdkGAAyf/X/gPhXGvfgAytDZtDbr+bkNUJ0N9h5MI/dmdgCs3l6hoHrcUv41sRKew3jIwrp4qQDXiK99Utww==",
954
+ "license": "MIT",
955
+ "dependencies": {
956
+ "ee-first": "1.1.1"
957
+ },
958
+ "engines": {
959
+ "node": ">= 0.8"
960
+ }
961
+ },
962
+ "node_modules/ms": {
963
+ "version": "2.0.0",
964
+ "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz",
965
+ "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==",
966
+ "license": "MIT"
967
+ },
968
+ "node_modules/negotiator": {
969
+ "version": "0.6.4",
970
+ "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.4.tgz",
971
+ "integrity": "sha512-myRT3DiWPHqho5PrJaIRyaMv2kgYf0mUVgBNOYMuCH5Ki1yEiQaf/ZJuQ62nvpc44wL5WDbTX7yGJi1Neevw8w==",
972
+ "license": "MIT",
973
+ "engines": {
974
+ "node": ">= 0.6"
975
+ }
976
+ },
977
+ "node_modules/nodemon": {
978
+ "version": "3.1.10",
979
+ "resolved": "https://registry.npmjs.org/nodemon/-/nodemon-3.1.10.tgz",
980
+ "integrity": "sha512-WDjw3pJ0/0jMFmyNDp3gvY2YizjLmmOUQo6DEBY+JgdvW/yQ9mEeSw6H5ythl5Ny2ytb7f9C2nIbjSxMNzbJXw==",
981
+ "dev": true,
982
+ "license": "MIT",
983
+ "dependencies": {
984
+ "chokidar": "^3.5.2",
985
+ "debug": "^4",
986
+ "ignore-by-default": "^1.0.1",
987
+ "minimatch": "^3.1.2",
988
+ "pstree.remy": "^1.1.8",
989
+ "semver": "^7.5.3",
990
+ "simple-update-notifier": "^2.0.0",
991
+ "supports-color": "^5.5.0",
992
+ "touch": "^3.1.0",
993
+ "undefsafe": "^2.0.5"
994
+ },
995
+ "bin": {
996
+ "nodemon": "bin/nodemon.js"
997
+ },
998
+ "engines": {
999
+ "node": ">=10"
1000
+ },
1001
+ "funding": {
1002
+ "type": "opencollective",
1003
+ "url": "https://opencollective.com/nodemon"
1004
+ }
1005
+ },
1006
+ "node_modules/nodemon/node_modules/debug": {
1007
+ "version": "4.4.3",
1008
+ "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz",
1009
+ "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==",
1010
+ "dev": true,
1011
+ "license": "MIT",
1012
+ "dependencies": {
1013
+ "ms": "^2.1.3"
1014
+ },
1015
+ "engines": {
1016
+ "node": ">=6.0"
1017
+ },
1018
+ "peerDependenciesMeta": {
1019
+ "supports-color": {
1020
+ "optional": true
1021
+ }
1022
+ }
1023
+ },
1024
+ "node_modules/nodemon/node_modules/ms": {
1025
+ "version": "2.1.3",
1026
+ "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
1027
+ "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",
1028
+ "dev": true,
1029
+ "license": "MIT"
1030
+ },
1031
+ "node_modules/normalize-path": {
1032
+ "version": "3.0.0",
1033
+ "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz",
1034
+ "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==",
1035
+ "dev": true,
1036
+ "license": "MIT",
1037
+ "engines": {
1038
+ "node": ">=0.10.0"
1039
+ }
1040
+ },
1041
+ "node_modules/object-assign": {
1042
+ "version": "4.1.1",
1043
+ "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz",
1044
+ "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==",
1045
+ "license": "MIT",
1046
+ "engines": {
1047
+ "node": ">=0.10.0"
1048
+ }
1049
+ },
1050
+ "node_modules/object-inspect": {
1051
+ "version": "1.13.4",
1052
+ "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz",
1053
+ "integrity": "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==",
1054
+ "license": "MIT",
1055
+ "engines": {
1056
+ "node": ">= 0.4"
1057
+ },
1058
+ "funding": {
1059
+ "url": "https://github.com/sponsors/ljharb"
1060
+ }
1061
+ },
1062
+ "node_modules/on-finished": {
1063
+ "version": "2.4.1",
1064
+ "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz",
1065
+ "integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==",
1066
+ "license": "MIT",
1067
+ "dependencies": {
1068
+ "ee-first": "1.1.1"
1069
+ },
1070
+ "engines": {
1071
+ "node": ">= 0.8"
1072
+ }
1073
+ },
1074
+ "node_modules/on-headers": {
1075
+ "version": "1.1.0",
1076
+ "resolved": "https://registry.npmjs.org/on-headers/-/on-headers-1.1.0.tgz",
1077
+ "integrity": "sha512-737ZY3yNnXy37FHkQxPzt4UZ2UWPWiCZWLvFZ4fu5cueciegX0zGPnrlY6bwRg4FdQOe9YU8MkmJwGhoMybl8A==",
1078
+ "license": "MIT",
1079
+ "engines": {
1080
+ "node": ">= 0.8"
1081
+ }
1082
+ },
1083
+ "node_modules/parseurl": {
1084
+ "version": "1.3.3",
1085
+ "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz",
1086
+ "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==",
1087
+ "license": "MIT",
1088
+ "engines": {
1089
+ "node": ">= 0.8"
1090
+ }
1091
+ },
1092
+ "node_modules/path-to-regexp": {
1093
+ "version": "0.1.12",
1094
+ "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.12.tgz",
1095
+ "integrity": "sha512-RA1GjUVMnvYFxuqovrEqZoxxW5NUZqbwKtYz/Tt7nXerk0LbLblQmrsgdeOxV5SFHf0UDggjS/bSeOZwt1pmEQ==",
1096
+ "license": "MIT"
1097
+ },
1098
+ "node_modules/picomatch": {
1099
+ "version": "2.3.1",
1100
+ "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz",
1101
+ "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==",
1102
+ "dev": true,
1103
+ "license": "MIT",
1104
+ "engines": {
1105
+ "node": ">=8.6"
1106
+ },
1107
+ "funding": {
1108
+ "url": "https://github.com/sponsors/jonschlinkert"
1109
+ }
1110
+ },
1111
+ "node_modules/proxy-addr": {
1112
+ "version": "2.0.7",
1113
+ "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz",
1114
+ "integrity": "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==",
1115
+ "license": "MIT",
1116
+ "dependencies": {
1117
+ "forwarded": "0.2.0",
1118
+ "ipaddr.js": "1.9.1"
1119
+ },
1120
+ "engines": {
1121
+ "node": ">= 0.10"
1122
+ }
1123
+ },
1124
+ "node_modules/proxy-from-env": {
1125
+ "version": "1.1.0",
1126
+ "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz",
1127
+ "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==",
1128
+ "license": "MIT"
1129
+ },
1130
+ "node_modules/pstree.remy": {
1131
+ "version": "1.1.8",
1132
+ "resolved": "https://registry.npmjs.org/pstree.remy/-/pstree.remy-1.1.8.tgz",
1133
+ "integrity": "sha512-77DZwxQmxKnu3aR542U+X8FypNzbfJ+C5XQDk3uWjWxn6151aIMGthWYRXTqT1E5oJvg+ljaa2OJi+VfvCOQ8w==",
1134
+ "dev": true,
1135
+ "license": "MIT"
1136
+ },
1137
+ "node_modules/qs": {
1138
+ "version": "6.13.0",
1139
+ "resolved": "https://registry.npmjs.org/qs/-/qs-6.13.0.tgz",
1140
+ "integrity": "sha512-+38qI9SOr8tfZ4QmJNplMUxqjbe7LKvvZgWdExBOmd+egZTtjLB67Gu0HRX3u/XOq7UU2Nx6nsjvS16Z9uwfpg==",
1141
+ "license": "BSD-3-Clause",
1142
+ "dependencies": {
1143
+ "side-channel": "^1.0.6"
1144
+ },
1145
+ "engines": {
1146
+ "node": ">=0.6"
1147
+ },
1148
+ "funding": {
1149
+ "url": "https://github.com/sponsors/ljharb"
1150
+ }
1151
+ },
1152
+ "node_modules/range-parser": {
1153
+ "version": "1.2.1",
1154
+ "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz",
1155
+ "integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==",
1156
+ "license": "MIT",
1157
+ "engines": {
1158
+ "node": ">= 0.6"
1159
+ }
1160
+ },
1161
+ "node_modules/raw-body": {
1162
+ "version": "2.5.2",
1163
+ "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.5.2.tgz",
1164
+ "integrity": "sha512-8zGqypfENjCIqGhgXToC8aB2r7YrBX+AQAfIPs/Mlk+BtPTztOvTS01NRW/3Eh60J+a48lt8qsCzirQ6loCVfA==",
1165
+ "license": "MIT",
1166
+ "dependencies": {
1167
+ "bytes": "3.1.2",
1168
+ "http-errors": "2.0.0",
1169
+ "iconv-lite": "0.4.24",
1170
+ "unpipe": "1.0.0"
1171
+ },
1172
+ "engines": {
1173
+ "node": ">= 0.8"
1174
+ }
1175
+ },
1176
+ "node_modules/readdirp": {
1177
+ "version": "3.6.0",
1178
+ "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz",
1179
+ "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==",
1180
+ "dev": true,
1181
+ "license": "MIT",
1182
+ "dependencies": {
1183
+ "picomatch": "^2.2.1"
1184
+ },
1185
+ "engines": {
1186
+ "node": ">=8.10.0"
1187
+ }
1188
+ },
1189
+ "node_modules/safe-buffer": {
1190
+ "version": "5.2.1",
1191
+ "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz",
1192
+ "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==",
1193
+ "funding": [
1194
+ {
1195
+ "type": "github",
1196
+ "url": "https://github.com/sponsors/feross"
1197
+ },
1198
+ {
1199
+ "type": "patreon",
1200
+ "url": "https://www.patreon.com/feross"
1201
+ },
1202
+ {
1203
+ "type": "consulting",
1204
+ "url": "https://feross.org/support"
1205
+ }
1206
+ ],
1207
+ "license": "MIT"
1208
+ },
1209
+ "node_modules/safer-buffer": {
1210
+ "version": "2.1.2",
1211
+ "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz",
1212
+ "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==",
1213
+ "license": "MIT"
1214
+ },
1215
+ "node_modules/semver": {
1216
+ "version": "7.7.2",
1217
+ "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.2.tgz",
1218
+ "integrity": "sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA==",
1219
+ "dev": true,
1220
+ "license": "ISC",
1221
+ "bin": {
1222
+ "semver": "bin/semver.js"
1223
+ },
1224
+ "engines": {
1225
+ "node": ">=10"
1226
+ }
1227
+ },
1228
+ "node_modules/send": {
1229
+ "version": "0.19.0",
1230
+ "resolved": "https://registry.npmjs.org/send/-/send-0.19.0.tgz",
1231
+ "integrity": "sha512-dW41u5VfLXu8SJh5bwRmyYUbAoSB3c9uQh6L8h/KtsFREPWpbX1lrljJo186Jc4nmci/sGUZ9a0a0J2zgfq2hw==",
1232
+ "license": "MIT",
1233
+ "dependencies": {
1234
+ "debug": "2.6.9",
1235
+ "depd": "2.0.0",
1236
+ "destroy": "1.2.0",
1237
+ "encodeurl": "~1.0.2",
1238
+ "escape-html": "~1.0.3",
1239
+ "etag": "~1.8.1",
1240
+ "fresh": "0.5.2",
1241
+ "http-errors": "2.0.0",
1242
+ "mime": "1.6.0",
1243
+ "ms": "2.1.3",
1244
+ "on-finished": "2.4.1",
1245
+ "range-parser": "~1.2.1",
1246
+ "statuses": "2.0.1"
1247
+ },
1248
+ "engines": {
1249
+ "node": ">= 0.8.0"
1250
+ }
1251
+ },
1252
+ "node_modules/send/node_modules/encodeurl": {
1253
+ "version": "1.0.2",
1254
+ "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.2.tgz",
1255
+ "integrity": "sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w==",
1256
+ "license": "MIT",
1257
+ "engines": {
1258
+ "node": ">= 0.8"
1259
+ }
1260
+ },
1261
+ "node_modules/send/node_modules/ms": {
1262
+ "version": "2.1.3",
1263
+ "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
1264
+ "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",
1265
+ "license": "MIT"
1266
+ },
1267
+ "node_modules/serve-static": {
1268
+ "version": "1.16.2",
1269
+ "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.16.2.tgz",
1270
+ "integrity": "sha512-VqpjJZKadQB/PEbEwvFdO43Ax5dFBZ2UECszz8bQ7pi7wt//PWe1P6MN7eCnjsatYtBT6EuiClbjSWP2WrIoTw==",
1271
+ "license": "MIT",
1272
+ "dependencies": {
1273
+ "encodeurl": "~2.0.0",
1274
+ "escape-html": "~1.0.3",
1275
+ "parseurl": "~1.3.3",
1276
+ "send": "0.19.0"
1277
+ },
1278
+ "engines": {
1279
+ "node": ">= 0.8.0"
1280
+ }
1281
+ },
1282
+ "node_modules/setprototypeof": {
1283
+ "version": "1.2.0",
1284
+ "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz",
1285
+ "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==",
1286
+ "license": "ISC"
1287
+ },
1288
+ "node_modules/side-channel": {
1289
+ "version": "1.1.0",
1290
+ "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.0.tgz",
1291
+ "integrity": "sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==",
1292
+ "license": "MIT",
1293
+ "dependencies": {
1294
+ "es-errors": "^1.3.0",
1295
+ "object-inspect": "^1.13.3",
1296
+ "side-channel-list": "^1.0.0",
1297
+ "side-channel-map": "^1.0.1",
1298
+ "side-channel-weakmap": "^1.0.2"
1299
+ },
1300
+ "engines": {
1301
+ "node": ">= 0.4"
1302
+ },
1303
+ "funding": {
1304
+ "url": "https://github.com/sponsors/ljharb"
1305
+ }
1306
+ },
1307
+ "node_modules/side-channel-list": {
1308
+ "version": "1.0.0",
1309
+ "resolved": "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.0.tgz",
1310
+ "integrity": "sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA==",
1311
+ "license": "MIT",
1312
+ "dependencies": {
1313
+ "es-errors": "^1.3.0",
1314
+ "object-inspect": "^1.13.3"
1315
+ },
1316
+ "engines": {
1317
+ "node": ">= 0.4"
1318
+ },
1319
+ "funding": {
1320
+ "url": "https://github.com/sponsors/ljharb"
1321
+ }
1322
+ },
1323
+ "node_modules/side-channel-map": {
1324
+ "version": "1.0.1",
1325
+ "resolved": "https://registry.npmjs.org/side-channel-map/-/side-channel-map-1.0.1.tgz",
1326
+ "integrity": "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==",
1327
+ "license": "MIT",
1328
+ "dependencies": {
1329
+ "call-bound": "^1.0.2",
1330
+ "es-errors": "^1.3.0",
1331
+ "get-intrinsic": "^1.2.5",
1332
+ "object-inspect": "^1.13.3"
1333
+ },
1334
+ "engines": {
1335
+ "node": ">= 0.4"
1336
+ },
1337
+ "funding": {
1338
+ "url": "https://github.com/sponsors/ljharb"
1339
+ }
1340
+ },
1341
+ "node_modules/side-channel-weakmap": {
1342
+ "version": "1.0.2",
1343
+ "resolved": "https://registry.npmjs.org/side-channel-weakmap/-/side-channel-weakmap-1.0.2.tgz",
1344
+ "integrity": "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==",
1345
+ "license": "MIT",
1346
+ "dependencies": {
1347
+ "call-bound": "^1.0.2",
1348
+ "es-errors": "^1.3.0",
1349
+ "get-intrinsic": "^1.2.5",
1350
+ "object-inspect": "^1.13.3",
1351
+ "side-channel-map": "^1.0.1"
1352
+ },
1353
+ "engines": {
1354
+ "node": ">= 0.4"
1355
+ },
1356
+ "funding": {
1357
+ "url": "https://github.com/sponsors/ljharb"
1358
+ }
1359
+ },
1360
+ "node_modules/simple-update-notifier": {
1361
+ "version": "2.0.0",
1362
+ "resolved": "https://registry.npmjs.org/simple-update-notifier/-/simple-update-notifier-2.0.0.tgz",
1363
+ "integrity": "sha512-a2B9Y0KlNXl9u/vsW6sTIu9vGEpfKu2wRV6l1H3XEas/0gUIzGzBoP/IouTcUQbm9JWZLH3COxyn03TYlFax6w==",
1364
+ "dev": true,
1365
+ "license": "MIT",
1366
+ "dependencies": {
1367
+ "semver": "^7.5.3"
1368
+ },
1369
+ "engines": {
1370
+ "node": ">=10"
1371
+ }
1372
+ },
1373
+ "node_modules/statuses": {
1374
+ "version": "2.0.1",
1375
+ "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz",
1376
+ "integrity": "sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==",
1377
+ "license": "MIT",
1378
+ "engines": {
1379
+ "node": ">= 0.8"
1380
+ }
1381
+ },
1382
+ "node_modules/supports-color": {
1383
+ "version": "5.5.0",
1384
+ "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz",
1385
+ "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==",
1386
+ "dev": true,
1387
+ "license": "MIT",
1388
+ "dependencies": {
1389
+ "has-flag": "^3.0.0"
1390
+ },
1391
+ "engines": {
1392
+ "node": ">=4"
1393
+ }
1394
+ },
1395
+ "node_modules/to-regex-range": {
1396
+ "version": "5.0.1",
1397
+ "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz",
1398
+ "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==",
1399
+ "dev": true,
1400
+ "license": "MIT",
1401
+ "dependencies": {
1402
+ "is-number": "^7.0.0"
1403
+ },
1404
+ "engines": {
1405
+ "node": ">=8.0"
1406
+ }
1407
+ },
1408
+ "node_modules/toidentifier": {
1409
+ "version": "1.0.1",
1410
+ "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz",
1411
+ "integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==",
1412
+ "license": "MIT",
1413
+ "engines": {
1414
+ "node": ">=0.6"
1415
+ }
1416
+ },
1417
+ "node_modules/touch": {
1418
+ "version": "3.1.1",
1419
+ "resolved": "https://registry.npmjs.org/touch/-/touch-3.1.1.tgz",
1420
+ "integrity": "sha512-r0eojU4bI8MnHr8c5bNo7lJDdI2qXlWWJk6a9EAFG7vbhTjElYhBVS3/miuE0uOuoLdb8Mc/rVfsmm6eo5o9GA==",
1421
+ "dev": true,
1422
+ "license": "ISC",
1423
+ "bin": {
1424
+ "nodetouch": "bin/nodetouch.js"
1425
+ }
1426
+ },
1427
+ "node_modules/type-is": {
1428
+ "version": "1.6.18",
1429
+ "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz",
1430
+ "integrity": "sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==",
1431
+ "license": "MIT",
1432
+ "dependencies": {
1433
+ "media-typer": "0.3.0",
1434
+ "mime-types": "~2.1.24"
1435
+ },
1436
+ "engines": {
1437
+ "node": ">= 0.6"
1438
+ }
1439
+ },
1440
+ "node_modules/undefsafe": {
1441
+ "version": "2.0.5",
1442
+ "resolved": "https://registry.npmjs.org/undefsafe/-/undefsafe-2.0.5.tgz",
1443
+ "integrity": "sha512-WxONCrssBM8TSPRqN5EmsjVrsv4A8X12J4ArBiiayv3DyyG3ZlIg6yysuuSYdZsVz3TKcTg2fd//Ujd4CHV1iA==",
1444
+ "dev": true,
1445
+ "license": "MIT"
1446
+ },
1447
+ "node_modules/unpipe": {
1448
+ "version": "1.0.0",
1449
+ "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz",
1450
+ "integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==",
1451
+ "license": "MIT",
1452
+ "engines": {
1453
+ "node": ">= 0.8"
1454
+ }
1455
+ },
1456
+ "node_modules/utils-merge": {
1457
+ "version": "1.0.1",
1458
+ "resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz",
1459
+ "integrity": "sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA==",
1460
+ "license": "MIT",
1461
+ "engines": {
1462
+ "node": ">= 0.4.0"
1463
+ }
1464
+ },
1465
+ "node_modules/vary": {
1466
+ "version": "1.1.2",
1467
+ "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz",
1468
+ "integrity": "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==",
1469
+ "license": "MIT",
1470
+ "engines": {
1471
+ "node": ">= 0.8"
1472
+ }
1473
+ }
1474
+ }
1475
+ }
package.json ADDED
@@ -0,0 +1,39 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "name": "chutes-image-app",
3
+ "version": "1.0.0",
4
+ "private": true,
5
+ "description": "Full-stack image generation client for chutes.ai with model list and adjustable parameters, ready for local and cloud deployment.",
6
+ "main": "server.js",
7
+ "scripts": {
8
+ "dev": "nodemon server.js",
9
+ "start": "node server.js",
10
+ "postinstall": "echo No build step",
11
+ "ps-start": "powershell -NoLogo -NoProfile -ExecutionPolicy Bypass -File ./scripts/start-local.ps1",
12
+ "ps-start-token": "powershell -NoLogo -NoProfile -ExecutionPolicy Bypass -File ./scripts/start-local.ps1 $env:CHUTES_API_TOKEN",
13
+ "ps-stop": "powershell -NoLogo -NoProfile -ExecutionPolicy Bypass -File ./scripts/stop-local.ps1",
14
+ "ps-stop-3000": "powershell -NoLogo -NoProfile -ExecutionPolicy Bypass -File ./scripts/stop-local.ps1 3000",
15
+ "win-start": "scripts\\start-local.cmd"
16
+ },
17
+ "dependencies": {
18
+ "axios": "^1.7.7",
19
+ "compression": "^1.7.4",
20
+ "cors": "^2.8.5",
21
+ "dotenv": "^16.4.5",
22
+ "express": "^4.19.2",
23
+ "helmet": "^7.1.0",
24
+ "morgan": "^1.10.0"
25
+ },
26
+ "devDependencies": {
27
+ "nodemon": "^3.1.0"
28
+ },
29
+ "engines": {
30
+ "node": "\u003e=18"
31
+ },
32
+ "keywords": [
33
+ "chutes",
34
+ "image-generation",
35
+ "express",
36
+ "fullstack"
37
+ ],
38
+ "license": "MIT"
39
+ }
public/index.html ADDED
@@ -0,0 +1,587 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <!DOCTYPE html>
2
+ <html lang="zh-CN">
3
+ <head>
4
+ <meta charset="UTF-8">
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
+ <title>Chutes 图像生成</title>
7
+ <style>
8
+ :root {
9
+ --bg: #f8fafc;
10
+ --fg: #1e293b;
11
+ --card: #ffffff;
12
+ --muted: #64748b;
13
+ --accent: #3b82f6;
14
+ --border: #e2e8f0;
15
+ }
16
+ [data-theme="dark"] {
17
+ --bg: #0f172a;
18
+ --fg: #f1f5f9;
19
+ --card: #1e293b;
20
+ --muted: #94a3b8;
21
+ --accent: #3b82f6;
22
+ --border: #334155;
23
+ }
24
+ * { box-sizing: border-box; }
25
+ html, body { height: 100%; margin: 0; padding: 0; }
26
+ body {
27
+ background: var(--bg);
28
+ color: var(--fg);
29
+ font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
30
+ transition: background-color 0.2s, color 0.2s;
31
+ }
32
+
33
+ header {
34
+ display: flex;
35
+ align-items: center;
36
+ justify-content: space-between;
37
+ padding: 12px 16px;
38
+ border-bottom: 1px solid var(--border);
39
+ background: var(--card);
40
+ position: sticky;
41
+ top: 0;
42
+ z-index: 10;
43
+ }
44
+ header h1 { font-size: 18px; margin: 0; }
45
+
46
+ .controls {
47
+ display: flex;
48
+ gap: 8px;
49
+ align-items: center;
50
+ }
51
+ .toggle-btn {
52
+ padding: 4px 8px;
53
+ border: 1px solid var(--border);
54
+ border-radius: 12px;
55
+ background: var(--bg);
56
+ color: var(--fg);
57
+ font-size: 12px;
58
+ cursor: pointer;
59
+ transition: all 0.2s;
60
+ }
61
+ .toggle-btn:hover { background: var(--accent); color: white; }
62
+ .toggle-btn.active { background: var(--accent); color: white; }
63
+
64
+ .api-input {
65
+ display: flex;
66
+ align-items: center;
67
+ gap: 6px;
68
+ padding: 4px 8px;
69
+ border: 1px solid var(--border);
70
+ border-radius: 12px;
71
+ background: var(--bg);
72
+ font-size: 12px;
73
+ }
74
+ .api-input input {
75
+ border: none;
76
+ background: transparent;
77
+ color: var(--fg);
78
+ width: 180px;
79
+ padding: 2px;
80
+ font-size: 12px;
81
+ }
82
+ .api-input input:focus { outline: none; }
83
+
84
+ main {
85
+ display: grid;
86
+ grid-template-columns: 280px 1fr;
87
+ gap: 8px;
88
+ padding: 8px;
89
+ height: calc(100vh - 60px);
90
+ }
91
+
92
+ .panel {
93
+ background: var(--card);
94
+ border: 1px solid var(--border);
95
+ border-radius: 12px;
96
+ padding: 8px;
97
+ box-shadow: 0 1px 3px rgba(0,0,0,0.1);
98
+ }
99
+ .panel.results { overflow: auto; }
100
+
101
+ .group { margin-bottom: 8px; }
102
+ .group label {
103
+ display: block;
104
+ font-size: 13px;
105
+ color: var(--muted);
106
+ margin-bottom: 4px;
107
+ font-weight: 500;
108
+ }
109
+ .group input, .group select, .group textarea {
110
+ width: 100%;
111
+ padding: 6px;
112
+ border: 1px solid var(--border);
113
+ border-radius: 8px;
114
+ background: var(--bg);
115
+ color: var(--fg);
116
+ font-size: 14px;
117
+ }
118
+ .group input:focus, .group select:focus, .group textarea:focus {
119
+ outline: none;
120
+ border-color: var(--accent);
121
+ }
122
+
123
+ .form-grid {
124
+ display: grid;
125
+ grid-template-columns: 1fr 1fr;
126
+ gap: 6px;
127
+ }
128
+
129
+ .row {
130
+ display: flex;
131
+ gap: 6px;
132
+ align-items: center;
133
+ }
134
+
135
+ button {
136
+ background: var(--accent);
137
+ color: white;
138
+ border: none;
139
+ border-radius: 8px;
140
+ padding: 8px 12px;
141
+ cursor: pointer;
142
+ font-size: 14px;
143
+ transition: all 0.2s;
144
+ }
145
+ button:hover { opacity: 0.9; }
146
+ button.secondary {
147
+ background: transparent;
148
+ color: var(--fg);
149
+ border: 1px solid var(--border);
150
+ }
151
+ button.secondary:hover {
152
+ background: var(--border);
153
+ }
154
+
155
+ .seed-toggle {
156
+ background: var(--bg);
157
+ color: var(--fg);
158
+ border: 1px solid var(--border);
159
+ border-radius: 8px;
160
+ padding: 6px 10px;
161
+ cursor: pointer;
162
+ font-size: 14px;
163
+ transition: all 0.2s;
164
+ }
165
+ .seed-toggle.active {
166
+ background: var(--accent);
167
+ color: white;
168
+ border-color: var(--accent);
169
+ }
170
+
171
+ .status { font-size: 14px; color: var(--muted); }
172
+
173
+ .gallery {
174
+ display: grid;
175
+ grid-template-columns: repeat(auto-fill, minmax(200px, 1fr));
176
+ gap: 8px;
177
+ }
178
+ .card {
179
+ background: var(--card);
180
+ border: 1px solid var(--border);
181
+ border-radius: 8px;
182
+ overflow: hidden;
183
+ }
184
+ .card img { width: 100%; display: block; }
185
+ .card .meta {
186
+ padding: 8px;
187
+ border-top: 1px solid var(--border);
188
+ font-size: 13px;
189
+ color: var(--muted);
190
+ line-height: 1.4;
191
+ }
192
+ .placeholder {
193
+ display: flex;
194
+ align-items: center;
195
+ justify-content: center;
196
+ height: 80px;
197
+ color: var(--muted);
198
+ font-size: 14px;
199
+ }
200
+ </style>
201
+ </head>
202
+ <body>
203
+ <header>
204
+ <h1>Chutes 图像生成</h1>
205
+ <div class="controls">
206
+ <button id="soundToggle" class="toggle-btn">提示音</button>
207
+ <button id="themeToggle" class="toggle-btn">深色主题</button>
208
+ <div class="api-input">
209
+ <span>API Key</span>
210
+ <input type="password" id="apiKeyInput" placeholder="粘贴你的 CHUTES_API_TOKEN">
211
+ </div>
212
+ </div>
213
+ </header>
214
+ <main>
215
+ <section class="panel">
216
+ <div class="group">
217
+ <label>模型</label>
218
+ <select id="modelSelect"></select>
219
+ </div>
220
+ <div class="group">
221
+ <label>提示词</label>
222
+ <textarea id="prompt" rows="2" placeholder="例如:原神角色 插画风,清晰细节,高质量"></textarea>
223
+ </div>
224
+ <div class="group">
225
+ <label>反向提示词</label>
226
+ <textarea id="negative_prompt" rows="1" placeholder="blurry, lowres, bad anatomy, artifacts"></textarea>
227
+ </div>
228
+ <div class="group form-grid">
229
+ <div class="group">
230
+ <label>宽度</label>
231
+ <input type="number" id="width" value="1024">
232
+ </div>
233
+ <div class="group">
234
+ <label>高度</label>
235
+ <input type="number" id="height" value="1024">
236
+ </div>
237
+ <div class="group">
238
+ <label>指导系数</label>
239
+ <input type="number" id="guidance_scale" step="0.1" value="6">
240
+ </div>
241
+ <div class="group">
242
+ <label>推理步数</label>
243
+ <input type="number" id="num_inference_steps" value="20">
244
+ </div>
245
+ </div>
246
+ <div class="group">
247
+ <button id="seedToggle" class="seed-toggle">使用随机</button>
248
+ </div>
249
+ <div class="group form-grid">
250
+ <div class="group">
251
+ <label>数量(最多10)</label>
252
+ <input type="number" id="batchCount" value="1" placeholder="1-10">
253
+ </div>
254
+ <div class="group">
255
+ <label>保存位置</label>
256
+ <button id="chooseFolder" class="secondary" type="button">选择保存文件夹</button>
257
+ <div class="status" id="folderStatus" style="margin-top: 2px;">未选择</div>
258
+ </div>
259
+ </div>
260
+ <div class="group">
261
+ <button id="generateBtn">生成</button>
262
+ </div>
263
+ </section>
264
+ <section class="panel results">
265
+ <div class="row" style="justify-content: space-between; margin-bottom: 8px;">
266
+ <div class="status">生成结果</div>
267
+ <button id="downloadAll" class="secondary">逐个下载</button>
268
+ </div>
269
+ <div id="gallery" class="gallery"></div>
270
+ </section>
271
+ </main>
272
+ <audio id="notify" src="/studio/new-notification-3-398649.mp3" preload="auto"></audio>
273
+ <script>
274
+ const qs = s => document.querySelector(s);
275
+ const qsa = s => Array.from(document.querySelectorAll(s));
276
+ const ls = {
277
+ get(k, d) { try { const v = localStorage.getItem(k); return v !== null ? JSON.parse(v) : d; } catch(e) { return d; } },
278
+ set(k, v) { try { localStorage.setItem(k, JSON.stringify(v)); } catch(e) {} }
279
+ };
280
+
281
+ const state = {
282
+ models: [],
283
+ sound: true,
284
+ theme: 'light',
285
+ apiKey: '',
286
+ folderHandle: null,
287
+ seedRandom: true
288
+ };
289
+
290
+ function setTheme(t) {
291
+ const theme = t === 'dark' ? 'dark' : 'light';
292
+ document.documentElement.setAttribute('data-theme', theme);
293
+ state.theme = theme;
294
+ ls.set('theme', theme);
295
+ const btn = qs('#themeToggle');
296
+ if (btn) {
297
+ btn.classList.toggle('active', theme === 'dark');
298
+ btn.textContent = theme === 'dark' ? '浅色主题' : '深色主题';
299
+ }
300
+ }
301
+
302
+ function setSound(on) {
303
+ state.sound = !!on;
304
+ ls.set('sound', state.sound);
305
+ const btn = qs('#soundToggle');
306
+ if (btn) {
307
+ btn.classList.toggle('active', state.sound);
308
+ btn.textContent = state.sound ? '关闭提示音' : '开启提示音';
309
+ }
310
+ }
311
+
312
+ function setApiKey(k) {
313
+ state.apiKey = (k || '').trim();
314
+ ls.set('apiKey', state.apiKey);
315
+ const el = qs('#apiKeyInput');
316
+ if (el && el.value !== state.apiKey) el.value = state.apiKey;
317
+ }
318
+
319
+ function updateSeedToggle() {
320
+ const btn = qs('#seedToggle');
321
+ if (btn) {
322
+ btn.classList.toggle('active', state.seedRandom);
323
+ btn.textContent = state.seedRandom ? '使用随机' : '固定种子';
324
+ }
325
+ }
326
+
327
+ function genRandomSeed() {
328
+ return Math.floor(Math.random() * 4294967295);
329
+ }
330
+
331
+ async function fetchModels() {
332
+ try {
333
+ const r = await fetch('/api/models', {
334
+ headers: state.apiKey ? { 'x-api-key': state.apiKey } : {}
335
+ });
336
+ const j = await r.json();
337
+ state.models = (j.models || []).map(m => ({
338
+ id: String(m.id || m.name || ''),
339
+ name: String(m.name || m.id || '')
340
+ }));
341
+ renderModels();
342
+ } catch(e) {
343
+ console.error('获取模型列表失败:', e);
344
+ }
345
+ }
346
+
347
+ function renderModels() {
348
+ const sel = qs('#modelSelect');
349
+ sel.innerHTML = '';
350
+ state.models.forEach(m => {
351
+ const opt = document.createElement('option');
352
+ opt.value = m.id;
353
+ opt.textContent = m.name;
354
+ sel.appendChild(opt);
355
+ });
356
+ const last = ls.get('lastParams', null);
357
+ if (last && last.model) sel.value = last.model;
358
+ }
359
+
360
+ function currentParams() {
361
+ return {
362
+ model: qs('#modelSelect').value || '',
363
+ prompt: (qs('#prompt').value || '').trim(),
364
+ negative_prompt: (qs('#negative_prompt').value || '').trim(),
365
+ width: Number(qs('#width').value) || 1024,
366
+ height: Number(qs('#height').value) || 1024,
367
+ guidance_scale: Number(qs('#guidance_scale').value) || 6,
368
+ num_inference_steps: Number(qs('#num_inference_steps').value) || 20,
369
+ seed: state.seedRandom ? null : 0
370
+ };
371
+ }
372
+
373
+ function createPlaceholderCard(i, params) {
374
+ const wrap = document.createElement('div');
375
+ wrap.className = 'card';
376
+ const ph = document.createElement('div');
377
+ ph.className = 'placeholder';
378
+ ph.textContent = `生成中 #${i}`;
379
+ wrap.appendChild(ph);
380
+ const meta = document.createElement('div');
381
+ meta.className = 'meta';
382
+ meta.innerHTML = `${params.model} | ${params.width}x${params.height}`;
383
+ wrap.appendChild(meta);
384
+ qs('#gallery').prepend(wrap);
385
+ return wrap;
386
+ }
387
+
388
+ function updateCardWithImage(wrap, dataUrl, params, filename) {
389
+ wrap.innerHTML = '';
390
+ const img = document.createElement('img');
391
+ img.src = dataUrl;
392
+ img.alt = params.prompt || 'image';
393
+ const meta = document.createElement('div');
394
+ meta.className = 'meta';
395
+ meta.innerHTML = `
396
+ <div style="font-weight: 600; margin-bottom: 4px;">${params.model}</div>
397
+ <div style="margin-bottom: 4px;">尺寸: ${params.width}x${params.height}</div>
398
+ <div style="margin-bottom: 4px;">步数: ${params.num_inference_steps} | 引导: ${params.guidance_scale}</div>
399
+ <div style="margin-bottom: 6px; font-size: 12px; line-height: 1.3;">${params.prompt}</div>
400
+ <div class="row">
401
+ <button class="secondary" onclick="downloadImage('${filename}', '${dataUrl}')">下载</button>
402
+ </div>
403
+ `;
404
+ wrap.appendChild(img);
405
+ wrap.appendChild(meta);
406
+ }
407
+
408
+ function removeCard(wrap) {
409
+ try { wrap.remove(); } catch(e) {}
410
+ }
411
+
412
+ function dataURLtoBlob(dataUrl) {
413
+ const arr = dataUrl.split(',');
414
+ const mime = arr[0].match(/:(.*?);/)[1];
415
+ const bstr = atob(arr[1]);
416
+ let n = bstr.length;
417
+ const u8 = new Uint8Array(n);
418
+ while(n--) { u8[n] = bstr.charCodeAt(n); }
419
+ return new Blob([u8], { type: mime });
420
+ }
421
+
422
+ async function saveToChosenFolder(filename, dataUrl) {
423
+ if (!state.folderHandle) return false;
424
+ try {
425
+ const fileHandle = await state.folderHandle.getFileHandle(filename, { create: true });
426
+ const writable = await fileHandle.createWritable();
427
+ await writable.write(dataURLtoBlob(dataUrl));
428
+ await writable.close();
429
+ return true;
430
+ } catch(e) {
431
+ console.warn('保存失败:', e);
432
+ return false;
433
+ }
434
+ }
435
+
436
+ function downloadImage(filename, dataUrl) {
437
+ const a = document.createElement('a');
438
+ a.href = dataUrl;
439
+ a.download = filename;
440
+ document.body.appendChild(a);
441
+ a.click();
442
+ a.remove();
443
+ }
444
+
445
+ async function chooseFolder() {
446
+ try {
447
+ if (!('showDirectoryPicker' in window)) {
448
+ qs('#folderStatus').textContent = '浏览器不支持';
449
+ return;
450
+ }
451
+ const handle = await window.showDirectoryPicker();
452
+ state.folderHandle = handle;
453
+ qs('#folderStatus').textContent = '已选择';
454
+ } catch(e) {
455
+ qs('#folderStatus').textContent = '未选择';
456
+ }
457
+ }
458
+
459
+ async function generate() {
460
+ const p = currentParams();
461
+ if (!p.model || !p.prompt) {
462
+ alert('模型与提示词必填');
463
+ return;
464
+ }
465
+
466
+ const countRaw = Number(qs('#batchCount').value || 1);
467
+ const count = Math.max(1, Math.min(10, Number.isFinite(countRaw) ? Math.trunc(countRaw) : 1));
468
+
469
+ const headers = {
470
+ 'Content-Type': 'application/json',
471
+ ...(state.apiKey ? { 'x-api-key': state.apiKey } : {})
472
+ };
473
+
474
+ const tasks = [];
475
+ for (let i = 1; i <= count; i++) {
476
+ const placeholder = createPlaceholderCard(i, p);
477
+ const args = {
478
+ prompt: p.prompt,
479
+ negative_prompt: p.negative_prompt || '',
480
+ width: p.width,
481
+ height: p.height,
482
+ guidance_scale: p.guidance_scale,
483
+ num_inference_steps: p.num_inference_steps,
484
+ seed: state.seedRandom ? genRandomSeed() : 0
485
+ };
486
+ const body = { model: p.model, input_args: args };
487
+
488
+ const task = (async () => {
489
+ try {
490
+ const r = await fetch('/api/generate', {
491
+ method: 'POST',
492
+ headers,
493
+ body: JSON.stringify(body)
494
+ });
495
+ const jr = await r.json().catch(() => ({ ok: false, error: '响应解析失败' }));
496
+
497
+ if (!jr.ok) {
498
+ const msg = (jr && jr.error) ? String(jr.error) : '生成失败';
499
+ if (!/timeout of \d+ms exceeded/i.test(msg)) {
500
+ console.error('生成失败:', msg);
501
+ }
502
+ removeCard(placeholder);
503
+ return;
504
+ }
505
+
506
+ const dataUrl = jr.image;
507
+ const fname = `image_${Date.now()}_${Math.random().toString(16).slice(2)}.jpg`;
508
+ updateCardWithImage(placeholder, dataUrl, p, fname);
509
+
510
+ if (state.folderHandle) {
511
+ await saveToChosenFolder(fname, dataUrl);
512
+ }
513
+
514
+ if (state.sound) {
515
+ const audio = qs('#notify');
516
+ if (audio) {
517
+ audio.currentTime = 0;
518
+ audio.play().catch(() => {});
519
+ }
520
+ }
521
+ } catch(e) {
522
+ const msg = String(e && e.message || e || '请求失败');
523
+ if (!/timeout of \d+ms exceeded/i.test(msg)) {
524
+ console.error('请求失败:', msg);
525
+ }
526
+ removeCard(placeholder);
527
+ }
528
+ })();
529
+
530
+ tasks.push(task);
531
+ }
532
+
533
+ try {
534
+ await Promise.allSettled(tasks);
535
+ ls.set('lastParams', p);
536
+ } catch(e) {
537
+ console.error('批量生成失败:', e);
538
+ }
539
+ }
540
+
541
+ function init() {
542
+ // 加载设置
543
+ setTheme(ls.get('theme', 'light'));
544
+ setSound(ls.get('sound', true));
545
+ setApiKey(ls.get('apiKey', ''));
546
+ updateSeedToggle();
547
+
548
+ // 绑定事件
549
+ qs('#themeToggle').addEventListener('click', () => {
550
+ setTheme(state.theme === 'dark' ? 'light' : 'dark');
551
+ });
552
+
553
+ qs('#soundToggle').addEventListener('click', () => {
554
+ setSound(!state.sound);
555
+ });
556
+
557
+ qs('#apiKeyInput').addEventListener('input', (e) => {
558
+ setApiKey(e.target.value);
559
+ });
560
+
561
+ qs('#seedToggle').addEventListener('click', () => {
562
+ state.seedRandom = !state.seedRandom;
563
+ updateSeedToggle();
564
+ });
565
+
566
+ qs('#chooseFolder').addEventListener('click', chooseFolder);
567
+
568
+ qs('#generateBtn').addEventListener('click', generate);
569
+
570
+ qs('#downloadAll').addEventListener('click', () => {
571
+ qsa('#gallery img').forEach((img, i) => {
572
+ downloadImage(`image_${i+1}.jpg`, img.src);
573
+ });
574
+ });
575
+
576
+ document.addEventListener('keydown', (e) => {
577
+ if (e.key === 'Enter') generate();
578
+ });
579
+
580
+ // 加载模型
581
+ fetchModels();
582
+ }
583
+
584
+ init();
585
+ </script>
586
+ </body>
587
+ </html>
server.js ADDED
@@ -0,0 +1,491 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ /**
2
+ * Chutes Image App - Express Server
3
+ * Features:
4
+ * - Static hosting
5
+ * - /api/models: fetch remote models (if MODELS_URL) or fallback to local data/models.json
6
+ * - /api/generate: proxy to chutes generate API with robust payload strategy and error handling
7
+ * - Security (helmet), CORS, compression, logging, timeouts
8
+ */
9
+
10
+ require('dotenv').config();
11
+
12
+ const express = require('express');
13
+ const axios = require('axios');
14
+ const helmet = require('helmet');
15
+ const cors = require('cors');
16
+ const compression = require('compression');
17
+ const morgan = require('morgan');
18
+ const fs = require('fs');
19
+ const path = require('path');
20
+ const { createAxiosLogger } = require('./utils/api-logger');
21
+
22
+ const app = express();
23
+
24
+ // Env
25
+ const PORT = parseInt(process.env.PORT || '3000', 10);
26
+ const HOST = process.env.HOST || '0.0.0.0';
27
+ const STATIC_DIR = process.env.STATIC_DIR || 'public';
28
+ const GENERATE_API_URL = process.env.GENERATE_API_URL || 'https://image.chutes.ai/generate';
29
+ const MODELS_URL = process.env.MODELS_URL || '';
30
+ const CHUTES_API_TOKEN = process.env.CHUTES_API_TOKEN || '';
31
+ const MOCK_MODE = /^true$/i.test(process.env.MOCK_MODE || 'false');
32
+ const TIMEOUT_MS = parseInt(process.env.TIMEOUT_MS || '120000', 10);
33
+ const LOG_LEVEL = process.env.LOG_LEVEL || 'dev';
34
+ // Allow end-user to optionally provide API key from frontend (header x-api-key)
35
+ const ALLOW_CLIENT_API_KEY = /^true$/i.test(process.env.ALLOW_CLIENT_API_KEY || 'true');
36
+ // Strict switches to close any possibility of routing/fallback
37
+ // - STRICT_NO_ROUTING=true 不做本地映射,除非前端或调用方显式传 upstream_id
38
+ // - STRICT_NO_FALLBACK=true 强制禁用回落(即使前端未设置 no_fallback)
39
+ const STRICT_NO_ROUTING = /^true$/i.test(process.env.STRICT_NO_ROUTING || 'true');
40
+ const STRICT_NO_FALLBACK = /^true$/i.test(process.env.STRICT_NO_FALLBACK || 'true');
41
+ // Auto fallback to another model when upstream capacity/infrastructure errors
42
+ const AUTO_FALLBACK = /^true$/i.test(process.env.AUTO_FALLBACK || 'true');
43
+ // Retry strategy for transient upstream errors
44
+ const RETRIES = parseInt(process.env.RETRIES || '2', 10);
45
+ const RETRY_BASE_MS = parseInt(process.env.RETRY_BASE_MS || '800', 10);
46
+ // Upstream auth mode: '' or 'x-api-key' (default: Authorization Bearer)
47
+ const UPSTREAM_AUTH_MODE = process.env.UPSTREAM_AUTH_MODE || '';
48
+ // Force sending minimal payload (only prompt) to upstream (default false)
49
+ const FORCE_MINIMAL = /^true$/i.test(process.env.FORCE_MINIMAL || 'false');
50
+
51
+ // Middlewares
52
+ app.use(helmet({
53
+ crossOriginResourcePolicy: { policy: 'cross-origin' }
54
+ }));
55
+
56
+ // Enable CSP and allow inline script/style for this SPA.
57
+ // Also allow connections to the upstream image API.
58
+ app.use(helmet.contentSecurityPolicy({
59
+ useDefaults: true,
60
+ directives: {
61
+ "default-src": ["'self'"],
62
+ "script-src": ["'self'", "'unsafe-inline'"],
63
+ "style-src": ["'self'", "'unsafe-inline'"],
64
+ "img-src": ["'self'", "data:", "blob:"],
65
+ "font-src": ["'self'", "data:"],
66
+ "connect-src": ["'self'", "https://image.chutes.ai"],
67
+ "media-src": ["'self'", "data:", "blob:"],
68
+ "frame-ancestors": ["'self'"]
69
+ }
70
+ }));
71
+
72
+ app.use(cors());
73
+ app.use(compression());
74
+ app.use(express.json({ limit: '2mb' }));
75
+ app.use(morgan(LOG_LEVEL));
76
+
77
+ // Static files
78
+ const staticPath = path.resolve(__dirname, STATIC_DIR);
79
+ app.use(express.static(staticPath, {
80
+ etag: true,
81
+ lastModified: true,
82
+ maxAge: '1h',
83
+ setHeaders: (res, filePath) => {
84
+ if (/\.(html)$/.test(filePath)) {
85
+ res.setHeader('Cache-Control', 'no-cache');
86
+ }
87
+ }
88
+ }));
89
+
90
+ // Expose studio assets (notification sound)
91
+ const studioPath = path.resolve(__dirname, 'studio');
92
+ app.use('/studio', express.static(studioPath, {
93
+ etag: true,
94
+ lastModified: true,
95
+ maxAge: '1h'
96
+ }));
97
+
98
+ // Utilities
99
+ const localModelsPath = path.resolve(__dirname, 'data', 'models.json');
100
+
101
+ /**
102
+ * Read local models fallback file.
103
+ * @returns {Promise<Array>}
104
+ */
105
+ async function readLocalModels() {
106
+ try {
107
+ const raw = await fs.promises.readFile(localModelsPath, 'utf-8');
108
+ const data = JSON.parse(raw);
109
+ if (Array.isArray(data)) return data;
110
+ if (Array.isArray(data.models)) return data.models;
111
+ return [];
112
+ } catch (err) {
113
+ console.error('Failed to read local models:', err.message);
114
+ return [];
115
+ }
116
+ }
117
+
118
+ /**
119
+ * Fetch remote models from MODELS_URL if provided.
120
+ * Expected to return either Array or { models: [] }
121
+ * @param {string} tokenOverride optional API token per-request
122
+ * @returns {Promise<Array|null>}
123
+ */
124
+ // 创建用于获取模型列表的axios实例并启用日志记录
125
+ const axiosModels = axios.create({
126
+ timeout: Math.min(TIMEOUT_MS, 30000),
127
+ validateStatus: () => true
128
+ });
129
+ createAxiosLogger(axiosModels);
130
+
131
+ async function fetchRemoteModels(tokenOverride = '') {
132
+ if (!MODELS_URL) return null;
133
+ try {
134
+ const resp = await axiosModels.get(MODELS_URL, {
135
+ headers: {
136
+ 'Content-Type': 'application/json',
137
+ ...(tokenOverride ? { 'Authorization': `Bearer ${tokenOverride}` } : (CHUTES_API_TOKEN ? { 'Authorization': `Bearer ${CHUTES_API_TOKEN}` } : {}))
138
+ }
139
+ });
140
+ const payload = resp.data;
141
+ if (Array.isArray(payload)) return payload;
142
+ if (payload && Array.isArray(payload.models)) return payload.models;
143
+ return null;
144
+ } catch (err) {
145
+ console.error('Failed to fetch remote models:', err.message);
146
+ return null;
147
+ }
148
+ }
149
+
150
+ /**
151
+ * Merge "free" flags from local list into remote list by model id or name
152
+ */
153
+ function mergeFreeFlags(remoteList, localList) {
154
+ const freeMap = new Map();
155
+ for (const m of localList) {
156
+ const key = (m.id || m.name || '').toLowerCase();
157
+ if (key) freeMap.set(key, !!m.free);
158
+ }
159
+ return remoteList.map(m => {
160
+ const key = (m.id || m.name || '').toLowerCase();
161
+ const free = freeMap.has(key) ? freeMap.get(key) : (typeof m.free === 'boolean' ? m.free : false);
162
+ return { ...m, free };
163
+ });
164
+ }
165
+
166
+ /**
167
+ * Clamp helper
168
+ */
169
+ function clamp(n, min, max) {
170
+ if (typeof n !== 'number' || Number.isNaN(n)) return min;
171
+ return Math.max(min, Math.min(max, n));
172
+ }
173
+
174
+ /**
175
+ * Axios instance for generate
176
+ */
177
+ const axiosGen = axios.create({
178
+ timeout: TIMEOUT_MS,
179
+ responseType: 'arraybuffer', // for image/jpeg
180
+ validateStatus: () => true
181
+ });
182
+
183
+ // 启用API日志记录
184
+ createAxiosLogger(axiosGen);
185
+
186
+ function delay(ms) { return new Promise(resolve => setTimeout(resolve, ms)); }
187
+
188
+ // Routes
189
+ app.get('/api/health', (req, res) => {
190
+ res.json({ ok: true, mock: MOCK_MODE, version: '1.0.0', allowClientKey: ALLOW_CLIENT_API_KEY });
191
+ });
192
+
193
+ /**
194
+ * GET /api/models
195
+ * 1) Try remote MODELS_URL
196
+ * 2) Fallback local data/models.json
197
+ * 3) Ensure each model has: { id, name, free }
198
+ */
199
+ app.get('/api/models', async (req, res) => {
200
+ try {
201
+ const localList = await readLocalModels();
202
+ const apiToken = req.headers['x-api-key'] || CHUTES_API_TOKEN;
203
+ let models = await fetchRemoteModels(apiToken);
204
+ if (models && models.length) {
205
+ // Normalize remote items
206
+ models = models.map((m, idx) => {
207
+ const id = (m.id || m.slug || m.model || m.name || `model-${idx}`).toString();
208
+ const name = (m.name || id).toString();
209
+ const free = typeof m.free === 'boolean' ? m.free : false;
210
+ return { id, name, free };
211
+ });
212
+ // Merge free flags from local mapping
213
+ if (localList.length) {
214
+ models = mergeFreeFlags(models, localList);
215
+ }
216
+ return res.json({ source: 'remote', models });
217
+ }
218
+
219
+ // Fallback to local
220
+ const normalized = localList.map((m, idx) => {
221
+ const id = (m.id || m.slug || m.model || m.name || `model-${idx}`).toString();
222
+ const name = (m.name || id).toString();
223
+ const free = !!m.free;
224
+ return { id, name, free };
225
+ });
226
+ return res.json({ source: 'local', models: normalized });
227
+ } catch (err) {
228
+ console.error('GET /api/models failed:', err);
229
+ res.status(500).json({ ok: false, error: 'Failed to load models' });
230
+ }
231
+ });
232
+
233
+ /**
234
+ * POST /api/generate
235
+ * Body supports two shapes:
236
+ * A) { model, input_args: { prompt, negative_prompt, width, height, guidance_scale, num_inference_steps, seed } }
237
+ * B) { model, prompt, negative_prompt, width, height, guidance_scale, num_inference_steps, seed }
238
+ *
239
+ * Server will attempt upstream with A first, then fallback to B if A fails.
240
+ * Returns JSON: { ok, image (base64 data URL), contentType, meta, tried }
241
+ */
242
+ app.post('/api/generate', async (req, res) => {
243
+ try {
244
+ if (MOCK_MODE) {
245
+ // Return a tiny transparent PNG as mock
246
+ const pngBase64 =
247
+ 'iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR4nGMAAQAABQABDQottQAAAABJRU5ErkJggg==';
248
+ return res.json({
249
+ ok: true,
250
+ image: `data:image/png;base64,${pngBase64}`,
251
+ contentType: 'image/png',
252
+ meta: { mock: true }
253
+ });
254
+ }
255
+
256
+ const body = req.body || {};
257
+ const flat = {
258
+ model: (body.model || (body.input_args && body.input_args.model) || '').toString(),
259
+ prompt: (body.prompt ?? (body.input_args ? body.input_args.prompt : undefined) ?? '').toString(),
260
+ negative_prompt: (body.negative_prompt ?? (body.input_args ? body.input_args.negative_prompt : undefined) ?? '').toString(),
261
+ width: clamp(parseInt(body.width ?? (body.input_args ? body.input_args.width : 1024), 10) || 1024, 128, 2048),
262
+ height: clamp(parseInt(body.height ?? (body.input_args ? body.input_args.height : 1024), 10) || 1024, 128, 2048),
263
+ guidance_scale: clamp(parseFloat(body.guidance_scale ?? (body.input_args ? body.input_args.guidance_scale : 7.5)) || 7.5, 1, 20),
264
+ num_inference_steps: clamp(parseInt(body.num_inference_steps ?? (body.input_args ? body.input_args.num_inference_steps : 25), 10) || 25, 1, 50),
265
+ // Seed: support top-level or input_args.seed; if missing/null -> null (omit from payload)
266
+ seed: (() => {
267
+ const raw = (body.seed ?? (body.input_args ? body.input_args.seed : undefined));
268
+ if (raw === null || raw === undefined || raw === '') return null;
269
+ const n = Number(raw);
270
+ if (!Number.isFinite(n)) return null;
271
+ const clamped = Math.max(0, Math.min(4294967295, Math.trunc(n)));
272
+ return clamped;
273
+ })()
274
+ };
275
+
276
+ if (!flat.prompt || !flat.model) {
277
+ return res.status(400).json({ ok: false, error: 'model and prompt are required' });
278
+ }
279
+
280
+ // Resolve an upstream model id via local mapping, with optional client override.
281
+ const localList = await readLocalModels();
282
+ function resolveUpstream(id) {
283
+ const key = (id || '').toLowerCase();
284
+ for (const m of localList) {
285
+ const mid = (m.id || '').toLowerCase();
286
+ const name = (m.name || '').toLowerCase();
287
+ if (mid === key || name === key) {
288
+ if (m.upstream_id) return m.upstream_id;
289
+ }
290
+ }
291
+ return id;
292
+ }
293
+ const overrideUpstream = (body.upstream_id ?? (body.input_args ? body.input_args.upstream_id : undefined));
294
+ let targetModel = (overrideUpstream && String(overrideUpstream).trim())
295
+ ? String(overrideUpstream).trim()
296
+ : (STRICT_NO_ROUTING ? flat.model : resolveUpstream(flat.model));
297
+ // honor global strict no-fallback if enabled
298
+ const NO_FALLBACK = STRICT_NO_FALLBACK || Boolean(body.no_fallback ?? (body.input_args ? body.input_args.no_fallback : false));
299
+
300
+
301
+ const apiToken = req.headers['x-api-key'] || CHUTES_API_TOKEN;
302
+ const headers = {
303
+ 'Content-Type': 'application/json',
304
+ 'Accept': 'image/jpeg,application/octet-stream,application/json',
305
+ ...(apiToken ? { 'Authorization': `Bearer ${apiToken}` } : {})
306
+ };
307
+
308
+ const variantA = {
309
+ model: targetModel,
310
+ input_args: {
311
+ prompt: flat.prompt,
312
+ negative_prompt: flat.negative_prompt || '',
313
+ width: flat.width,
314
+ height: flat.height,
315
+ guidance_scale: flat.guidance_scale,
316
+ num_inference_steps: flat.num_inference_steps,
317
+ ...(flat.seed !== null ? { seed: flat.seed } : {})
318
+ }
319
+ };
320
+
321
+ const variantB = {
322
+ model: targetModel,
323
+ prompt: flat.prompt,
324
+ negative_prompt: flat.negative_prompt || '',
325
+ width: flat.width,
326
+ height: flat.height,
327
+ guidance_scale: flat.guidance_scale,
328
+ num_inference_steps: flat.num_inference_steps,
329
+ ...(flat.seed !== null ? { seed: flat.seed } : {})
330
+ };
331
+
332
+ // Minimal payload (some models reject extended fields). Only prompt required.
333
+ const variantCMinimal = {
334
+ model: targetModel,
335
+ input_args: {
336
+ prompt: flat.prompt
337
+ }
338
+ };
339
+
340
+ // duplicate removed
341
+
342
+ async function tryCall(payload, label) {
343
+ const resp = await axiosGen.post(GENERATE_API_URL, payload, { headers });
344
+ const ctype = (resp.headers && (resp.headers['content-type'] || resp.headers['Content-Type'])) || '';
345
+ const status = resp.status;
346
+ if (status >= 200 && status < 300 && /image\//i.test(ctype)) {
347
+ const base64 = Buffer.from(resp.data).toString('base64');
348
+ return { ok: true, imageBase64: base64, contentType: ctype, tried: label };
349
+ } else {
350
+ let raw = '';
351
+ try {
352
+ raw = Buffer.from(resp.data).toString();
353
+ } catch (e) {}
354
+
355
+ // Friendly diagnostics mapping
356
+ let code = 'UPSTREAM_ERROR';
357
+ let hint = '';
358
+ let mappedStatus = status;
359
+ let detailText = '';
360
+ try {
361
+ const j = JSON.parse(raw);
362
+ detailText = j && (j.detail || j.message || j.error || '');
363
+ } catch (e) {
364
+ detailText = raw;
365
+ }
366
+
367
+ const lower = (detailText || '').toLowerCase();
368
+ if (lower.includes('exhausted all available targets')) {
369
+ code = 'UPSTREAM_CAPACITY_EXHAUSTED';
370
+ hint = '上游容量不足(GPU/目标不可用或排队中),请稍后重试、换模型,或降低分辨率/步数。';
371
+ mappedStatus = 503; // service unavailable
372
+ } else if (status === 404 && lower.includes('model not found')) {
373
+ code = 'UPSTREAM_MODEL_NOT_FOUND';
374
+ hint = '上游模型不存在或标识不匹配。请更换模型,或在 data/models.json 为该模型添加正确的 \"upstream_id\" 映射后重试。';
375
+ } else if (status === 400 && (lower.includes('invalid request') || lower.includes('invalid input'))) {
376
+ code = 'UPSTREAM_INVALID_PARAMS';
377
+ hint = '上游参数不接受:尝试使用最小输入(仅 prompt)重试。';
378
+ }
379
+
380
+ const err = new Error(hint || `Upstream ${label} failed: ${status} ${ctype} ${raw || ''}`);
381
+ err.status = mappedStatus;
382
+ err.code = code;
383
+ err.hint = hint;
384
+ throw err;
385
+ }
386
+ }
387
+
388
+ let result;
389
+ try {
390
+ result = await tryCall(variantA, 'nested');
391
+ } catch (e1) {
392
+ try {
393
+ result = await tryCall(variantB, 'flat');
394
+ } catch (e2) {
395
+ // Auto fallback to another model when capacity/infrastructure errors (disabled when NO_FALLBACK=true)
396
+ const capacityCodes = ['UPSTREAM_CAPACITY_EXHAUSTED','UPSTREAM_NO_INSTANCES','UPSTREAM_INFRASTRUCTURE','UPSTREAM_BAD_GATEWAY'];
397
+ if (AUTO_FALLBACK && !NO_FALLBACK && capacityCodes.includes(e2.code || '') ) {
398
+ // choose fallback model (prefer free and different from current)
399
+ function chooseFallback(currentId, list) {
400
+ const key = (currentId || '').toLowerCase();
401
+ const free = list.filter(m => m.free && (m.id || m.name || '').toLowerCase() !== key);
402
+ if (free.length) return (free[0].upstream_id || free[0].id || free[0].name);
403
+ const any = list.find(m => (m.id || m.name || '').toLowerCase() !== key);
404
+ if (any) return (any.upstream_id || any.id || any.name);
405
+ return null;
406
+ }
407
+ const fallbackModel = chooseFallback(targetModel, localList);
408
+ if (fallbackModel && fallbackModel !== targetModel) {
409
+ const fbA = { ...variantA, model: fallbackModel };
410
+ const fbB = { ...variantB, model: fallbackModel };
411
+ try {
412
+ result = await tryCall(fbA, 'nested-fallback');
413
+ } catch (e3) {
414
+ try {
415
+ result = await tryCall(fbB, 'flat-fallback');
416
+ } catch (e4) {
417
+ const status = e4.status || 502;
418
+ return res.status(status).json({
419
+ ok: false,
420
+ error: e4.hint || e4.message || 'Upstream error',
421
+ code: e4.code || 'UPSTREAM_ERROR',
422
+ upstream_model: targetModel,
423
+ fallback_model: fallbackModel
424
+ });
425
+ }
426
+ }
427
+ // success with fallback
428
+ return res.json({
429
+ ok: true,
430
+ image: `data:${result.contentType};base64,${result.imageBase64}`,
431
+ contentType: result.contentType,
432
+ meta: {
433
+ model: flat.model,
434
+ upstream_model: targetModel,
435
+ fallback_used: true,
436
+ fallback_model: fallbackModel,
437
+ width: flat.width,
438
+ height: flat.height,
439
+ guidance_scale: flat.guidance_scale,
440
+ num_inference_steps: flat.num_inference_steps,
441
+ seed: flat.seed
442
+ },
443
+ tried: result.tried
444
+ });
445
+ }
446
+ }
447
+ const status = e2.status || 502;
448
+ return res.status(status).json({
449
+ ok: false,
450
+ error: e2.hint || e2.message || 'Upstream error',
451
+ code: e2.code || 'UPSTREAM_ERROR',
452
+ upstream_model: targetModel
453
+ });
454
+ }
455
+ }
456
+
457
+ return res.json({
458
+ ok: true,
459
+ image: `data:${result.contentType};base64,${result.imageBase64}`,
460
+ contentType: result.contentType,
461
+ meta: {
462
+ model: flat.model,
463
+ upstream_model: targetModel,
464
+ width: flat.width,
465
+ height: flat.height,
466
+ guidance_scale: flat.guidance_scale,
467
+ num_inference_steps: flat.num_inference_steps,
468
+ seed: flat.seed
469
+ },
470
+ tried: result.tried
471
+ });
472
+ } catch (err) {
473
+ console.error('POST /api/generate failed:', err);
474
+ res.status(500).json({ ok: false, error: 'Server error' });
475
+ }
476
+ });
477
+
478
+ // Favicon placeholder to avoid noisy 404 in dev
479
+ app.get('/favicon.ico', (req, res) => {
480
+ res.status(204).end();
481
+ });
482
+
483
+ // Fallback to index.html for direct root access
484
+ app.get('/', (req, res) => {
485
+ res.sendFile(path.join(staticPath, 'index.html'));
486
+ });
487
+
488
+ // Start server
489
+ app.listen(PORT, HOST, () => {
490
+ console.log(`Server running at http://${HOST}:${PORT}`);
491
+ });
studio/new-notification-3-398649.mp3 ADDED
Binary file (16.1 kB). View file
 
utils/api-logger.js ADDED
@@ -0,0 +1,21 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ /**
2
+ * HF-space logger stub: 禁用文件日志与控制台日志。
3
+ * 仅保持 axios 拦截器结构以避免依赖错误。
4
+ */
5
+ function createAxiosLogger(axiosInstance) {
6
+ // request pass-through
7
+ axiosInstance.interceptors.request.use(
8
+ (config) => config,
9
+ (error) => Promise.reject(error)
10
+ );
11
+ // response pass-through
12
+ axiosInstance.interceptors.response.use(
13
+ (response) => response,
14
+ (error) => Promise.reject(error)
15
+ );
16
+ }
17
+
18
+ function logApiRequest() { /* noop */ }
19
+ function readApiLogs() { return []; }
20
+
21
+ module.exports = { createAxiosLogger, logApiRequest, readApiLogs };