Logankunfall commited on
Commit
e5af9f7
·
verified ·
1 Parent(s): 01a4f9c

Upload 21 files

Browse files
backend/server.py CHANGED
@@ -1,38 +1,25 @@
1
  from __future__ import annotations
2
 
3
- import threading
4
- import time
5
- import webbrowser
6
-
7
  import uvicorn
8
 
9
  from .app import app
10
  from .config import load_config
11
 
12
 
13
- def _open_browser_later(url: str, delay: float = 1.5):
14
- def _opener():
15
- time.sleep(delay)
16
- try:
17
- webbrowser.open(url)
18
- except Exception:
19
- pass
20
-
21
- t = threading.Thread(target=_opener, daemon=True)
22
- t.start()
23
-
24
-
25
  if __name__ == "__main__":
26
  cfg = load_config()
27
- # HF 环境使用 0.0.0.0,默认端口 7860
28
- port = int(cfg.port or 7860)
29
  host = "0.0.0.0"
30
 
31
- print(f"[New NAI HF] 服务运行于 http://{host}:{port}")
32
- print(f"[New NAI HF] HF Space 通过公共 URL 访问")
33
-
34
- # HF 环境不自动打开浏览器
35
- # _open_browser_later(url)
 
 
36
 
37
- # 使用 0.0.0.0 监听以便 HF Space 外部访问
38
  uvicorn.run(app, host=host, port=port, reload=False)
 
1
  from __future__ import annotations
2
 
3
+ import os
 
 
 
4
  import uvicorn
5
 
6
  from .app import app
7
  from .config import load_config
8
 
9
 
 
 
 
 
 
 
 
 
 
 
 
 
10
  if __name__ == "__main__":
11
  cfg = load_config()
12
+ # HF 环境固定使用端口 7860
13
+ port = 7860
14
  host = "0.0.0.0"
15
 
16
+ # 检测是否在 HF Space 环境
17
+ space_id = os.environ.get("SPACE_ID", "")
18
+ if space_id:
19
+ print(f"[New NAI HF] 正在 HF Space 中运行: {space_id}")
20
+ print(f"[New NAI HF] 请通过 HF Space 公共 URL 访问")
21
+ else:
22
+ print(f"[New NAI HF] 服务已启动,端口: {port}")
23
 
24
+ # 使用 0.0.0.0 监听以便外部访问
25
  uvicorn.run(app, host=host, port=port, reload=False)
frontend/assets/app.js CHANGED
@@ -194,6 +194,71 @@
194
  },
195
  };
196
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
197
  // Config
198
  async function loadConfig() {
199
  try {
@@ -211,9 +276,6 @@
211
  byId("cfg-uc-preset").value = cfg.uc_preset ?? "";
212
  byId("cfg-quality-toggle").checked = !!cfg.quality_toggle;
213
  byId("cfg-legacy-uc").checked = !!cfg.legacy_uc;
214
- byId("cfg-port").value = cfg.port ?? 7860;
215
- byId("cfg-save-output").checked = !!cfg.save_output;
216
- if (byId("cfg-output-dir")) byId("cfg-output-dir").value = cfg.output_dir ?? "";
217
 
218
  // 提示音配置
219
  soundEnabled = !!cfg.sound_enabled;
@@ -221,7 +283,7 @@
221
  if (byId("sound-player")) byId("sound-player").src = soundUrl;
222
  (typeof updateSoundToggle === "function") && updateSoundToggle();
223
 
224
- // 隐藏下方“配置已读取”提示
225
  byId("cfg-message").textContent = "";
226
  } catch (err) {
227
  byId("cfg-message").textContent = "读取失败:" + err.message;
@@ -235,7 +297,8 @@
235
  return v;
236
  }
237
 
238
- async function saveConfig() {
 
239
  const payload = {
240
  key: nullIfEmpty(byId("cfg-key").value),
241
  model: nullIfEmpty(byId("cfg-model").value),
@@ -247,31 +310,48 @@
247
  uc_preset: byId("cfg-uc-preset").value ? Number(byId("cfg-uc-preset").value) : null,
248
  quality_toggle: byId("cfg-quality-toggle").checked,
249
  legacy_uc: byId("cfg-legacy-uc").checked,
250
- port: byId("cfg-port").value ? Number(byId("cfg-port").value) : null,
251
- save_output: byId("cfg-save-output").checked,
252
- output_dir: nullIfEmpty(byId("cfg-output-dir")?.value),
253
  sound_enabled: !!soundEnabled,
254
  sound_url: nullIfEmpty(soundUrl),
255
  };
256
 
257
  try {
258
- loading.show();
259
  const res = await fetch("/api/config", {
260
  method: "PUT",
261
  headers: { "Content-Type": "application/json" },
262
  body: JSON.stringify(payload),
263
  });
264
- if (!res.ok) throw new Error(await res.text());
265
- await res.json();
266
- byId("cfg-message").textContent = "保存成功";
267
- toast("配置已保存", "success");
268
  } catch (err) {
269
- byId("cfg-message").textContent = "保存失败:" + err.message;
270
- toast("保存配置失败", "error");
271
- } finally {
272
- loading.hide();
273
  }
274
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
275
 
276
  // File -> Base64 (strip data URL prefix)
277
  function fileToBase64(file) {
@@ -797,100 +877,27 @@
797
  }
798
 
799
  // Bindings
800
- // 为每个 Tab 注入“数量”输入(默认 1,可调至 8)
801
  ensureCountField("tab-t2i", "t2i-count");
802
  ensureCountField("tab-i2i", "i2i-count");
803
  ensureCountField("tab-inpaint", "inpaint-count");
804
 
805
- byId("btn-save-config").addEventListener("click", saveConfig);
806
- const selOutBtn = byId("btn-select-output-dir");
807
- if (selOutBtn) {
808
- selOutBtn.addEventListener("click", async () => {
809
- // 移动端检测:手机无法弹出服务器上的目录选择器
810
- if (isMobileDevice()) {
811
- toast("手机端请直接在输入框中填写目录路径", "info");
812
- return;
813
- }
814
-
815
- try {
816
- loading.show();
817
- const res = await fetch("/api/select-output-dir");
818
- if (!res.ok) {
819
- const body = await res.json().catch(() => ({}));
820
- // 区分HF环境和本地环境
821
- const isHF = res.status === 501;
822
- if (isHF) {
823
- toast("请直接在输入框中手动填写保存目录路径", "info");
824
- } else {
825
- toast(body?.detail || "选择失败,请手动在输入框填入保存目录", "error");
826
- }
827
- return;
828
- }
829
- const body = await res.json();
830
- const p = String(body?.path || "");
831
- if (p) {
832
- const input = byId("cfg-output-dir");
833
- if (input) input.value = p;
834
- toast("已选择保存目录", "success");
835
- } else {
836
- toast("未选择任何目录", "info");
837
- }
838
- } catch (e) {
839
- toast(String(e?.message || e), "error");
840
- } finally {
841
- loading.hide();
842
- }
843
- });
844
- }
845
- const openOutBtn = byId("btn-open-output-dir");
846
- if (openOutBtn) {
847
- openOutBtn.addEventListener("click", async () => {
848
- // 移动端检测:手机无法打开服务器上的目录
849
- if (isMobileDevice()) {
850
- toast("手机端无法打开服务器目录,请在电脑上操作", "info");
851
- return;
852
- }
853
-
854
- try {
855
- loading.show();
856
- const p = byId("cfg-output-dir")?.value || "";
857
- if (!p) {
858
- toast("请先填写保存目录路径", "error");
859
- return;
860
- }
861
- const res = await fetch("/api/open-dir", {
862
- method: "POST",
863
- headers: { "Content-Type": "application/json" },
864
- body: JSON.stringify({ path: p }),
865
- });
866
- if (!res.ok) {
867
- const body = await res.json().catch(() => ({}));
868
- const isHF = res.status === 501;
869
- if (isHF) {
870
- toast("HF 环境不支持打开系统目录", "info");
871
- } else {
872
- throw new Error(body?.detail || "打开目录失败");
873
- }
874
- return;
875
- }
876
- toast("已打开保存目录", "success");
877
- } catch (e) {
878
- toast(String(e?.message || e), "error");
879
- } finally {
880
- loading.hide();
881
- }
882
- });
883
- }
884
-
885
  byId("btn-t2i").addEventListener("click", handleT2I);
886
  byId("btn-i2i").addEventListener("click", handleI2I);
887
  byId("btn-inpaint").addEventListener("click", handleInpaint);
888
 
889
  // 设置图片信息展开/收起功能
890
  setupImageInfoToggle();
 
 
 
 
 
 
891
 
892
- // Init - 加载配置和恢复上次的生成参数
893
  loadConfig();
 
894
  restoreT2IParams();
895
  restoreI2IParams();
896
  restoreInpaintParams();
 
194
  },
195
  };
196
 
197
+ // ===== 自定义背景功能 =====
198
+ const STORAGE_KEY_BG = 'nai_custom_background';
199
+
200
+ function applyCustomBackground(imageDataUrl) {
201
+ if (imageDataUrl) {
202
+ document.body.style.backgroundImage = `url('${imageDataUrl}')`;
203
+ localStorage.setItem(STORAGE_KEY_BG, imageDataUrl);
204
+ }
205
+ }
206
+
207
+ function resetBackground() {
208
+ document.body.style.backgroundImage = '';
209
+ localStorage.removeItem(STORAGE_KEY_BG);
210
+ // 让CSS的默认背景生效
211
+ toast("已重置为默认背景", "success");
212
+ }
213
+
214
+ function loadCustomBackground() {
215
+ try {
216
+ const saved = localStorage.getItem(STORAGE_KEY_BG);
217
+ if (saved) {
218
+ document.body.style.backgroundImage = `url('${saved}')`;
219
+ }
220
+ } catch {}
221
+ }
222
+
223
+ function setupCustomBackground() {
224
+ const bgInput = byId("cfg-custom-bg");
225
+ const resetBtn = byId("btn-reset-bg");
226
+
227
+ if (bgInput) {
228
+ bgInput.addEventListener("change", async (e) => {
229
+ const file = e.target.files?.[0];
230
+ if (!file) return;
231
+
232
+ // 检查文件大小(限制5MB)
233
+ if (file.size > 5 * 1024 * 1024) {
234
+ toast("图片文件过大,请选择小于5MB的图片", "error");
235
+ return;
236
+ }
237
+
238
+ try {
239
+ const reader = new FileReader();
240
+ reader.onload = (ev) => {
241
+ const dataUrl = ev.target?.result;
242
+ if (dataUrl) {
243
+ applyCustomBackground(dataUrl);
244
+ toast("背景已更新", "success");
245
+ }
246
+ };
247
+ reader.onerror = () => {
248
+ toast("读取图片失败", "error");
249
+ };
250
+ reader.readAsDataURL(file);
251
+ } catch (err) {
252
+ toast("设置背景失败", "error");
253
+ }
254
+ });
255
+ }
256
+
257
+ if (resetBtn) {
258
+ resetBtn.addEventListener("click", resetBackground);
259
+ }
260
+ }
261
+
262
  // Config
263
  async function loadConfig() {
264
  try {
 
276
  byId("cfg-uc-preset").value = cfg.uc_preset ?? "";
277
  byId("cfg-quality-toggle").checked = !!cfg.quality_toggle;
278
  byId("cfg-legacy-uc").checked = !!cfg.legacy_uc;
 
 
 
279
 
280
  // 提示音配置
281
  soundEnabled = !!cfg.sound_enabled;
 
283
  if (byId("sound-player")) byId("sound-player").src = soundUrl;
284
  (typeof updateSoundToggle === "function") && updateSoundToggle();
285
 
286
+ // 隐藏下方"配置已读取"提示
287
  byId("cfg-message").textContent = "";
288
  } catch (err) {
289
  byId("cfg-message").textContent = "读取失败:" + err.message;
 
297
  return v;
298
  }
299
 
300
+ // HF版本:配置自动保存到服务器(无需手动点击保存按钮)
301
+ async function autoSaveConfig() {
302
  const payload = {
303
  key: nullIfEmpty(byId("cfg-key").value),
304
  model: nullIfEmpty(byId("cfg-model").value),
 
310
  uc_preset: byId("cfg-uc-preset").value ? Number(byId("cfg-uc-preset").value) : null,
311
  quality_toggle: byId("cfg-quality-toggle").checked,
312
  legacy_uc: byId("cfg-legacy-uc").checked,
 
 
 
313
  sound_enabled: !!soundEnabled,
314
  sound_url: nullIfEmpty(soundUrl),
315
  };
316
 
317
  try {
 
318
  const res = await fetch("/api/config", {
319
  method: "PUT",
320
  headers: { "Content-Type": "application/json" },
321
  body: JSON.stringify(payload),
322
  });
323
+ // 静默保存,不显示提示
 
 
 
324
  } catch (err) {
325
+ // 忽略错误
 
 
 
326
  }
327
  }
328
+
329
+ // 配置字段变化时自动保存(防抖)
330
+ let configSaveTimer = null;
331
+ function setupAutoSaveConfig() {
332
+ const configFields = [
333
+ "cfg-key", "cfg-model", "cfg-sampler", "cfg-steps", "cfg-scale",
334
+ "cfg-cfg-rescale", "cfg-noise-schedule", "cfg-uc-preset",
335
+ "cfg-quality-toggle", "cfg-legacy-uc"
336
+ ];
337
+
338
+ configFields.forEach(id => {
339
+ const el = byId(id);
340
+ if (el) {
341
+ el.addEventListener("change", () => {
342
+ if (configSaveTimer) clearTimeout(configSaveTimer);
343
+ configSaveTimer = setTimeout(autoSaveConfig, 500);
344
+ });
345
+ // 对于���本输入,也监听input事件
346
+ if (el.tagName === "INPUT" && (el.type === "text" || el.type === "password" || el.type === "number")) {
347
+ el.addEventListener("input", () => {
348
+ if (configSaveTimer) clearTimeout(configSaveTimer);
349
+ configSaveTimer = setTimeout(autoSaveConfig, 1000);
350
+ });
351
+ }
352
+ }
353
+ });
354
+ }
355
 
356
  // File -> Base64 (strip data URL prefix)
357
  function fileToBase64(file) {
 
877
  }
878
 
879
  // Bindings
880
+ // 为每个 Tab 注入"数量"输入(默认 1,可调至 8)
881
  ensureCountField("tab-t2i", "t2i-count");
882
  ensureCountField("tab-i2i", "i2i-count");
883
  ensureCountField("tab-inpaint", "inpaint-count");
884
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
885
  byId("btn-t2i").addEventListener("click", handleT2I);
886
  byId("btn-i2i").addEventListener("click", handleI2I);
887
  byId("btn-inpaint").addEventListener("click", handleInpaint);
888
 
889
  // 设置图片信息展开/收起功能
890
  setupImageInfoToggle();
891
+
892
+ // 设置自定义背景功能
893
+ setupCustomBackground();
894
+
895
+ // 设置配置自动保存
896
+ setupAutoSaveConfig();
897
 
898
+ // Init - 加载配置、恢复上次的生成参数、加载自定义背景
899
  loadConfig();
900
+ loadCustomBackground();
901
  restoreT2IParams();
902
  restoreI2IParams();
903
  restoreInpaintParams();
frontend/assets/style.css CHANGED
@@ -470,6 +470,37 @@ input[type="file"] {
470
  border: 1px solid var(--border);
471
  }
472
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
473
  /* ===== 主题变量与覆盖 ===== */
474
  /* 深色为默认 */
475
  :root,
 
470
  border: 1px solid var(--border);
471
  }
472
 
473
+ /* 自定义背景上传组件 */
474
+ .background-upload-wrapper {
475
+ display: flex;
476
+ gap: 10px;
477
+ align-items: center;
478
+ flex-wrap: wrap;
479
+ }
480
+
481
+ .background-upload-wrapper input[type="file"] {
482
+ flex: 1;
483
+ min-width: 200px;
484
+ }
485
+
486
+ .background-upload-wrapper button {
487
+ flex-shrink: 0;
488
+ padding: 10px 16px;
489
+ font-size: 14px;
490
+ }
491
+
492
+ /* HF 环境提示信息 */
493
+ .hf-notice {
494
+ color: var(--muted);
495
+ font-size: 13px;
496
+ margin-top: 12px;
497
+ padding: 10px 14px;
498
+ background: var(--card);
499
+ border: 1px dashed var(--border);
500
+ border-radius: var(--radius);
501
+ text-align: center;
502
+ }
503
+
504
  /* ===== 主题变量与覆盖 ===== */
505
  /* 深色为默认 */
506
  :root,
frontend/index.html CHANGED
@@ -108,25 +108,17 @@
108
  <input id="cfg-legacy-uc" type="checkbox" />
109
  <span>兼容旧版 UC</span>
110
  </label>
111
- <label>
112
- <span>服务端口</span>
113
- <input id="cfg-port" type="number" min="1" step="1" placeholder="默认 9180" />
114
- </label>
115
- <label class="switch">
116
- <input id="cfg-save-output" type="checkbox" />
117
- <span>保存到输出目录</span>
118
- </label>
119
  <label class="full">
120
- <span>保存目录</span>
121
- <input id="cfg-output-dir" type="text" placeholder="例如:D:\Pictures\NAI 输出(留空=默认 output)" />
 
 
 
122
  </label>
123
  </div>
124
- <div class="actions">
125
- <button id="btn-select-output-dir">选择保存目录</button>
126
- <button id="btn-open-output-dir">打开保存目录</button>
127
- <button id="btn-save-config" class="primary">保存配置</button>
128
- </div>
129
  <p id="cfg-message" class="message"></p>
 
130
  </div>
131
  </section>
132
 
 
108
  <input id="cfg-legacy-uc" type="checkbox" />
109
  <span>兼容旧版 UC</span>
110
  </label>
111
+ <!-- 自定义背景 -->
 
 
 
 
 
 
 
112
  <label class="full">
113
+ <span>自定义背景图片</span>
114
+ <div class="background-upload-wrapper">
115
+ <input id="cfg-custom-bg" type="file" accept="image/*" />
116
+ <button id="btn-reset-bg" type="button">重置背景</button>
117
+ </div>
118
  </label>
119
  </div>
 
 
 
 
 
120
  <p id="cfg-message" class="message"></p>
121
+ <p class="hf-notice">注:HF 云端环境无需保存配置,设置会自动应用</p>
122
  </div>
123
  </section>
124