dsh-agnes-create 0.4.1 → 0.7.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +44 -5
- package/lib/agnes.js +386 -9
- package/lib/client.js +662 -19
- package/lib/index.js +131 -5
- package/package.json +7 -3
package/lib/client.js
CHANGED
|
@@ -23,6 +23,19 @@ window.__ModuleLoader__.load({
|
|
|
23
23
|
const REF_QUALITY = 0.88;
|
|
24
24
|
const REF_PNG_KEEP_BYTES = 800 * 1024;
|
|
25
25
|
|
|
26
|
+
/**
|
|
27
|
+
* 提示词预设:点一下把风格描述追加到当前提示词末尾。
|
|
28
|
+
* 刻意写成「可叠加的风格后缀」,而不是整段替换——用户已经写好的主体不会被冲掉。
|
|
29
|
+
*/
|
|
30
|
+
const PROMPT_PRESETS = [
|
|
31
|
+
{ label: "写实摄影", text: "写实摄影风格,自然光,浅景深,35mm 镜头,细节丰富" },
|
|
32
|
+
{ label: "水彩", text: "水彩画风格,纸纹质感,柔和晕染,留白构图" },
|
|
33
|
+
{ label: "3D 渲染", text: "3D 渲染风格,柔光棚拍,材质清晰,背景简洁" },
|
|
34
|
+
{ label: "海报", text: "平面海报构图,大面积留白,主体突出,配色克制" },
|
|
35
|
+
{ label: "夜景霓虹", text: "夜景霓虹灯光,湿润地面反光,氛围浓烈" },
|
|
36
|
+
{ label: "极简", text: "极简构图,单一主体,纯净背景,柔和渐变光" },
|
|
37
|
+
];
|
|
38
|
+
|
|
26
39
|
const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
|
|
27
40
|
|
|
28
41
|
/* ---------- 样式 ---------- */
|
|
@@ -73,7 +86,8 @@ window.__ModuleLoader__.load({
|
|
|
73
86
|
transition: width .3s ease; }
|
|
74
87
|
|
|
75
88
|
/* 媒体展示区 */
|
|
76
|
-
.agn-shot {
|
|
89
|
+
.agn-shot { position: relative; border-radius: 10px;
|
|
90
|
+
border: 1px solid var(--dsw-alias-border-l2, #e3e3e3);
|
|
77
91
|
background: var(--dsw-alias-bg-layer-2, #fafafa); overflow: hidden;
|
|
78
92
|
display: flex; align-items: center; justify-content: center; min-height: 120px; }
|
|
79
93
|
.agn-shot img, .agn-shot video { display: block; max-width: 100%; height: auto; }
|
|
@@ -82,6 +96,39 @@ window.__ModuleLoader__.load({
|
|
|
82
96
|
.agn-shot-msg { padding: 18px; font-size: 12px; text-align: center; line-height: 1.7;
|
|
83
97
|
color: var(--dsw-alias-label-secondary, #888); }
|
|
84
98
|
|
|
99
|
+
/* 悬停浮现的提示词:贴在媒体底部,默认透明,鼠标进来才出现 */
|
|
100
|
+
.agn-prompt { position: absolute; left: 0; right: 0; bottom: 0;
|
|
101
|
+
display: flex; align-items: flex-end; gap: 8px; padding: 8px 10px;
|
|
102
|
+
background: linear-gradient(to top, rgba(0,0,0,.62), rgba(0,0,0,0));
|
|
103
|
+
opacity: 0; transition: opacity .16s ease; pointer-events: none; }
|
|
104
|
+
.agn-shot:hover .agn-prompt { opacity: 1; pointer-events: auto; }
|
|
105
|
+
.agn-prompt-text { flex: 1 1 auto; min-width: 0; font-size: 11px; line-height: 1.5;
|
|
106
|
+
color: #fff; display: -webkit-box; -webkit-line-clamp: 3;
|
|
107
|
+
-webkit-box-orient: vertical; overflow: hidden; word-break: break-word; }
|
|
108
|
+
.agn-prompt-copy { flex: none; font: inherit; font-size: 11px; line-height: 1.3;
|
|
109
|
+
padding: 2px 8px; border-radius: 5px; cursor: pointer;
|
|
110
|
+
color: #fff; background: rgba(255,255,255,.18);
|
|
111
|
+
border: 1px solid rgba(255,255,255,.45); }
|
|
112
|
+
.agn-prompt-copy:hover { background: rgba(255,255,255,.3); }
|
|
113
|
+
|
|
114
|
+
/* 输出目录清单 */
|
|
115
|
+
.agn-lib { display: flex; flex-direction: column; gap: 8px; margin-top: 8px; }
|
|
116
|
+
.agn-lib-tabs { display: flex; gap: 6px; align-items: center; flex-wrap: wrap; }
|
|
117
|
+
.agn-lib-grid { display: flex; gap: 8px; flex-wrap: wrap;
|
|
118
|
+
max-height: 300px; overflow-y: auto; padding: 2px; }
|
|
119
|
+
.agn-lib-item { position: relative; width: 84px; height: 84px; border-radius: 8px;
|
|
120
|
+
padding: 0; overflow: hidden; cursor: pointer; flex: none;
|
|
121
|
+
border: 2px solid var(--dsw-alias-border-l2, #e3e3e3);
|
|
122
|
+
background: var(--dsw-alias-bg-layer-2, #fafafa); }
|
|
123
|
+
.agn-lib-item:hover { border-color: var(--dsw-alias-state-business-primary, #4b7bec); }
|
|
124
|
+
.agn-lib-item img, .agn-lib-item video { width: 100%; height: 100%;
|
|
125
|
+
object-fit: cover; display: block; pointer-events: none; }
|
|
126
|
+
.agn-lib-badge { position: absolute; left: 4px; bottom: 4px; font-size: 10px;
|
|
127
|
+
line-height: 1.4; padding: 0 5px; border-radius: 4px;
|
|
128
|
+
color: #fff; background: rgba(0,0,0,.55); }
|
|
129
|
+
.agn-lib-empty { font-size: 12px; line-height: 1.6;
|
|
130
|
+
color: var(--dsw-alias-label-secondary, #888); }
|
|
131
|
+
|
|
85
132
|
/* 历史缩略图 */
|
|
86
133
|
.agn-hist { display: flex; flex-direction: column; gap: 6px; }
|
|
87
134
|
.agn-hist-strip { display: flex; gap: 8px; flex-wrap: wrap; }
|
|
@@ -90,10 +137,15 @@ window.__ModuleLoader__.load({
|
|
|
90
137
|
border: 2px solid var(--dsw-alias-border-l2, #e3e3e3);
|
|
91
138
|
background: var(--dsw-alias-bg-layer-2, #fafafa); }
|
|
92
139
|
.agn-thumb-on { border-color: var(--dsw-alias-state-business-primary, #4b7bec); }
|
|
93
|
-
|
|
140
|
+
/* 缩略图里放 <video> 取首帧;pointer-events: none 让点击继续落到外层按钮上
|
|
141
|
+
(video 属于可交互内容,套在 button 里会吃掉点击) */
|
|
142
|
+
.agn-thumb img, .agn-thumb video { width: 100%; height: 100%;
|
|
143
|
+
object-fit: cover; display: block; pointer-events: none; }
|
|
144
|
+
/* ▶ 只是角标,压在首帧上;别挡点击,也别把首帧盖住 */
|
|
94
145
|
.agn-thumb-play { position: absolute; inset: 0; display: flex; align-items: center;
|
|
95
|
-
justify-content: center; font-size:
|
|
96
|
-
color: #fff;
|
|
146
|
+
justify-content: center; font-size: 20px; line-height: 1;
|
|
147
|
+
color: #fff; text-shadow: 0 1px 4px rgba(0, 0, 0, .75);
|
|
148
|
+
pointer-events: none; background: rgba(0, 0, 0, .12); }
|
|
97
149
|
.agn-thumb-cap { position: absolute; left: 0; right: 0; bottom: 0; font-size: 10px;
|
|
98
150
|
text-align: center; padding: 1px 0;
|
|
99
151
|
color: var(--dsw-alias-label-primary-foreground, #fff);
|
|
@@ -144,6 +196,55 @@ window.__ModuleLoader__.load({
|
|
|
144
196
|
.agn-tag { font-size: 11px; padding: 2px 8px; border-radius: 999px;
|
|
145
197
|
background: var(--dsw-alias-bg-layer-2, #f1f1f1);
|
|
146
198
|
color: var(--dsw-alias-label-secondary, #555); }
|
|
199
|
+
|
|
200
|
+
/* 提示词预设 */
|
|
201
|
+
.agn-presets { display: flex; gap: 6px; flex-wrap: wrap; margin-bottom: 6px; }
|
|
202
|
+
.agn-preset { font: inherit; font-size: 11px; line-height: 1.4; padding: 2px 9px;
|
|
203
|
+
border-radius: 999px; cursor: pointer;
|
|
204
|
+
color: var(--dsw-alias-label-secondary, #555); background: transparent;
|
|
205
|
+
border: 1px solid var(--dsw-alias-border-l2, #ddd); }
|
|
206
|
+
.agn-preset:hover:not(:disabled) { border-color: var(--dsw-alias-state-business-primary, #4b7bec);
|
|
207
|
+
color: var(--dsw-alias-state-business-primary, #4b7bec); }
|
|
208
|
+
.agn-preset:disabled { opacity: .45; cursor: not-allowed; }
|
|
209
|
+
|
|
210
|
+
/* 视频关键帧(图生视频) */
|
|
211
|
+
.agn-frames { display: flex; flex-direction: column; gap: 8px; }
|
|
212
|
+
.agn-frames-row { display: flex; gap: 8px; flex-wrap: wrap; align-items: flex-start; }
|
|
213
|
+
.agn-frame { display: flex; flex-direction: column; gap: 4px; flex: none; }
|
|
214
|
+
.agn-frame-cap { font-size: 11px; color: var(--dsw-alias-label-secondary, #888); }
|
|
215
|
+
.agn-frame-box { position: relative; width: 72px; height: 72px; border-radius: 8px;
|
|
216
|
+
overflow: hidden; border: 1px solid var(--dsw-alias-border-l2, #e3e3e3);
|
|
217
|
+
background: var(--dsw-alias-bg-layer-2, #fafafa); }
|
|
218
|
+
.agn-frame-box img { width: 100%; height: 100%; object-fit: cover; display: block; }
|
|
219
|
+
.agn-frame-add { width: 72px; height: 72px; border-radius: 8px; cursor: pointer;
|
|
220
|
+
font: inherit; font-size: 11px; line-height: 1.35; padding: 4px;
|
|
221
|
+
color: var(--dsw-alias-label-secondary, #666); background: transparent;
|
|
222
|
+
border: 1px dashed var(--dsw-alias-border-l2, #ccc); }
|
|
223
|
+
.agn-frame-add:disabled { opacity: .45; cursor: not-allowed; }
|
|
224
|
+
/* 「+ 结果」:从已生成的图片里挑一张当关键帧 */
|
|
225
|
+
.agn-frame-from { width: 72px; font: inherit; font-size: 11px; line-height: 1.4;
|
|
226
|
+
padding: 2px 0; border-radius: 6px; cursor: pointer;
|
|
227
|
+
color: var(--dsw-alias-label-secondary, #555); background: transparent;
|
|
228
|
+
border: 1px solid var(--dsw-alias-border-l2, #ddd); }
|
|
229
|
+
.agn-frame-from:hover:not(:disabled) {
|
|
230
|
+
border-color: var(--dsw-alias-state-business-primary, #4b7bec);
|
|
231
|
+
color: var(--dsw-alias-state-business-primary, #4b7bec); }
|
|
232
|
+
.agn-frame-from:disabled { opacity: .45; cursor: not-allowed; }
|
|
233
|
+
.agn-frame .agn-ref-pick { margin-top: 2px; }
|
|
234
|
+
.agn-seed { width: 72px; }
|
|
235
|
+
|
|
236
|
+
/* 结果收尾动作 */
|
|
237
|
+
.agn-acts { display: flex; gap: 6px; flex-wrap: wrap; margin-top: 6px; }
|
|
238
|
+
.agn-act { font: inherit; font-size: 11px; line-height: 1.4; padding: 3px 10px;
|
|
239
|
+
border-radius: 6px; cursor: pointer;
|
|
240
|
+
color: var(--dsw-alias-label-secondary, #555); background: transparent;
|
|
241
|
+
border: 1px solid var(--dsw-alias-border-l2, #ddd); }
|
|
242
|
+
.agn-act:hover { border-color: var(--dsw-alias-state-business-primary, #4b7bec);
|
|
243
|
+
color: var(--dsw-alias-state-business-primary, #4b7bec); }
|
|
244
|
+
.agn-act-on { border-color: var(--dsw-alias-state-business-primary, #4b7bec);
|
|
245
|
+
color: var(--dsw-alias-state-business-primary, #4b7bec); }
|
|
246
|
+
.agn-notice { font-size: 12px; color: var(--dsw-alias-state-success-primary, #2a2);
|
|
247
|
+
line-height: 1.6; }
|
|
147
248
|
`;
|
|
148
249
|
|
|
149
250
|
function ensureCss() {
|
|
@@ -279,6 +380,27 @@ window.__ModuleLoader__.load({
|
|
|
279
380
|
refsBusy: false,
|
|
280
381
|
refsError: "",
|
|
281
382
|
refPickOpen: false,
|
|
383
|
+
|
|
384
|
+
/* ---- 视频关键帧(可选):本地图直接当首帧 / 尾帧 ---- */
|
|
385
|
+
vFirst: null, // { src(dataURI), name, bytes }
|
|
386
|
+
vLast: null,
|
|
387
|
+
vSeed: "", // 空串 = 不传 seed
|
|
388
|
+
frameBusy: false, // 正在压缩关键帧图片
|
|
389
|
+
framePick: "", // '' | 'first' | 'last':本地文件选择器的目标
|
|
390
|
+
framePickFrom: "", // '' | 'first' | 'last':生成结果选择面板的目标
|
|
391
|
+
|
|
392
|
+
/* ---- 历史:宿主落盘,刷新/重启后还在 ---- */
|
|
393
|
+
historyMax: HISTORY_MAX,
|
|
394
|
+
historyLoaded: false,
|
|
395
|
+
notice: "", // 一次性操作反馈(复制路径、打开访达…)
|
|
396
|
+
|
|
397
|
+
/* ---- 输出目录清单:把磁盘上已有的图/视频捞回来看 ---- */
|
|
398
|
+
library: null, // null = 还没加载过;[] = 加载过但是空
|
|
399
|
+
libraryOpen: false,
|
|
400
|
+
libraryTab: "image", // 'image' | 'video'
|
|
401
|
+
libraryBusy: false,
|
|
402
|
+
libraryError: "",
|
|
403
|
+
copiedKey: "", // 刚刚复制了哪一条的提示词(用于显示「已复制」)
|
|
282
404
|
};
|
|
283
405
|
|
|
284
406
|
const snapshot = () => ({ ...state, history: state.history.slice() });
|
|
@@ -312,9 +434,58 @@ window.__ModuleLoader__.load({
|
|
|
312
434
|
};
|
|
313
435
|
|
|
314
436
|
const pushResult = (entry) => {
|
|
315
|
-
set({ result: entry, history: [entry, ...state.history].slice(0,
|
|
437
|
+
set({ result: entry, history: [entry, ...state.history].slice(0, state.historyMax) });
|
|
438
|
+
// 顺手落盘:/agnes/history 是唯一真相,写失败也不影响本次展示
|
|
439
|
+
void appendHistory(entry);
|
|
316
440
|
};
|
|
317
441
|
|
|
442
|
+
/* ---------- 历史(宿主持久化) ---------- */
|
|
443
|
+
async function loadHistory() {
|
|
444
|
+
try {
|
|
445
|
+
const res = await fetch("/agnes/history");
|
|
446
|
+
if (!res.ok) {
|
|
447
|
+
// 旧版宿主没有这条路由:保持空历史即可,不要报错吓人
|
|
448
|
+
set({ historyLoaded: true });
|
|
449
|
+
return;
|
|
450
|
+
}
|
|
451
|
+
const data = await res.json();
|
|
452
|
+
if (!data || data.ok !== true) {
|
|
453
|
+
set({ historyLoaded: true });
|
|
454
|
+
return;
|
|
455
|
+
}
|
|
456
|
+
const entries = Array.isArray(data.entries) ? data.entries : [];
|
|
457
|
+
const max = typeof data.max === "number" && data.max > 0 ? data.max : HISTORY_MAX;
|
|
458
|
+
update((prev) => ({
|
|
459
|
+
history: entries,
|
|
460
|
+
historyMax: max,
|
|
461
|
+
historyLoaded: true,
|
|
462
|
+
// 主预览默认显示最新一条;已经有结果时不动它
|
|
463
|
+
result: prev.result === null ? (entries[0] || null) : prev.result,
|
|
464
|
+
}));
|
|
465
|
+
} catch {
|
|
466
|
+
set({ historyLoaded: true });
|
|
467
|
+
}
|
|
468
|
+
}
|
|
469
|
+
|
|
470
|
+
async function appendHistory(entry) {
|
|
471
|
+
try {
|
|
472
|
+
const res = await fetch("/agnes/history", {
|
|
473
|
+
method: "POST",
|
|
474
|
+
headers: { "content-type": "application/json" },
|
|
475
|
+
body: JSON.stringify(entry),
|
|
476
|
+
});
|
|
477
|
+
if (!res.ok) return;
|
|
478
|
+
const data = await res.json();
|
|
479
|
+
if (!data || data.ok !== true) return;
|
|
480
|
+
// 只有宿主确实回了列表才覆盖本地:宁可用旧列表,也不要把界面清空
|
|
481
|
+
if (!Array.isArray(data.entries)) return;
|
|
482
|
+
update((prev) => ({
|
|
483
|
+
history: data.entries,
|
|
484
|
+
historyMax: typeof data.max === "number" && data.max > 0 ? data.max : prev.historyMax,
|
|
485
|
+
}));
|
|
486
|
+
} catch { /* 落盘失败不影响当前结果展示 */ }
|
|
487
|
+
}
|
|
488
|
+
|
|
318
489
|
async function loadHealth() {
|
|
319
490
|
if (state.healthRequested) return;
|
|
320
491
|
state.healthRequested = true;
|
|
@@ -400,7 +571,7 @@ window.__ModuleLoader__.load({
|
|
|
400
571
|
const data = await res.json();
|
|
401
572
|
if (!data.ok) throw new Error(data.error || "生成失败");
|
|
402
573
|
if (token !== runToken) return;
|
|
403
|
-
pushResult({ ...data, kind: "image", wallSec: ((Date.now() - state.startedAt) / 1000).toFixed(1) });
|
|
574
|
+
pushResult({ ...data, kind: "image", prompt, wallSec: ((Date.now() - state.startedAt) / 1000).toFixed(1) });
|
|
404
575
|
}
|
|
405
576
|
|
|
406
577
|
/* ---------- 参考图(图生图) ---------- */
|
|
@@ -519,19 +690,226 @@ window.__ModuleLoader__.load({
|
|
|
519
690
|
set({ refPickOpen: !state.refPickOpen, refsError: "" });
|
|
520
691
|
}
|
|
521
692
|
|
|
693
|
+
/* ---------- 视频关键帧(首帧 / 尾帧,本地图直接当帧) ---------- */
|
|
694
|
+
const frameField = (which) => (which === "last" ? "vLast" : "vFirst");
|
|
695
|
+
|
|
696
|
+
function pickFrame(which) {
|
|
697
|
+
if (state.busy || state.frameBusy) return;
|
|
698
|
+
set({ framePick: which, error: "" });
|
|
699
|
+
}
|
|
700
|
+
|
|
701
|
+
async function setFrameFromFile(which, file) {
|
|
702
|
+
set({ frameBusy: true, error: "" });
|
|
703
|
+
try {
|
|
704
|
+
// 和参考图走同一套下采样:长边 1280、PNG 优先,既省上行又不会糊
|
|
705
|
+
const r = await shrinkImage(file);
|
|
706
|
+
set({
|
|
707
|
+
[frameField(which)]: { src: r.src, name: file.name || "关键帧", bytes: r.bytes },
|
|
708
|
+
framePick: "",
|
|
709
|
+
});
|
|
710
|
+
} catch (e) {
|
|
711
|
+
set({ framePick: "", error: e && e.message ? e.message : String(e) });
|
|
712
|
+
} finally {
|
|
713
|
+
set({ frameBusy: false });
|
|
714
|
+
}
|
|
715
|
+
}
|
|
716
|
+
|
|
717
|
+
function clearFrame(which) {
|
|
718
|
+
if (state.busy) return;
|
|
719
|
+
set({ [frameField(which)]: null, framePickFrom: "" });
|
|
720
|
+
}
|
|
721
|
+
|
|
722
|
+
/* 把「生成结果」里的图直接当关键帧:和本地图完全同一条链路(blob → 下采样 → data URI) */
|
|
723
|
+
function toggleFramePick(which) {
|
|
724
|
+
if (state.busy || state.frameBusy) return;
|
|
725
|
+
set({ framePickFrom: state.framePickFrom === which ? "" : which, error: "" });
|
|
726
|
+
}
|
|
727
|
+
|
|
728
|
+
async function setFrameFromResult(which, entry) {
|
|
729
|
+
if (!entry || entry.kind !== "image" || !entry.filename) return;
|
|
730
|
+
if (state.busy || state.frameBusy) return;
|
|
731
|
+
set({ frameBusy: true, error: "" });
|
|
732
|
+
try {
|
|
733
|
+
const res = await fetch(srcOf(entry.filename));
|
|
734
|
+
if (!res.ok) throw new Error("读取生成结果失败:HTTP " + res.status);
|
|
735
|
+
const blob = await res.blob();
|
|
736
|
+
const r = await shrinkImage(blob);
|
|
737
|
+
set({
|
|
738
|
+
[frameField(which)]: { src: r.src, name: entry.filename, bytes: r.bytes },
|
|
739
|
+
framePickFrom: "",
|
|
740
|
+
});
|
|
741
|
+
} catch (e) {
|
|
742
|
+
set({ framePickFrom: "", error: e && e.message ? e.message : String(e) });
|
|
743
|
+
} finally {
|
|
744
|
+
set({ frameBusy: false });
|
|
745
|
+
}
|
|
746
|
+
}
|
|
747
|
+
|
|
748
|
+
/* 结果区的一键设置:只在「结果本身是图片」时有意义 */
|
|
749
|
+
function useResultAsFrame(which) {
|
|
750
|
+
const cur = state.result;
|
|
751
|
+
if (cur === null || cur.kind !== "image") return;
|
|
752
|
+
return setFrameFromResult(which, cur);
|
|
753
|
+
}
|
|
754
|
+
|
|
755
|
+
/* ---------- 结果收尾动作:复制路径 / 访达显示 / 下载 ---------- */
|
|
756
|
+
async function copyPath() {
|
|
757
|
+
const cur = state.result;
|
|
758
|
+
if (cur === null) return;
|
|
759
|
+
const text = cur.path || "";
|
|
760
|
+
try {
|
|
761
|
+
await navigator.clipboard.writeText(text);
|
|
762
|
+
set({ notice: "路径已复制" });
|
|
763
|
+
} catch {
|
|
764
|
+
set({ notice: "复制失败,可手动复制:" + text });
|
|
765
|
+
}
|
|
766
|
+
}
|
|
767
|
+
|
|
768
|
+
async function reveal() {
|
|
769
|
+
const cur = state.result;
|
|
770
|
+
if (cur === null) return;
|
|
771
|
+
try {
|
|
772
|
+
const res = await fetch("/agnes/reveal", {
|
|
773
|
+
method: "POST",
|
|
774
|
+
headers: { "content-type": "application/json" },
|
|
775
|
+
body: JSON.stringify({ name: cur.filename }),
|
|
776
|
+
});
|
|
777
|
+
const data = await res.json().catch(() => ({}));
|
|
778
|
+
set({
|
|
779
|
+
notice: data && data.ok === true
|
|
780
|
+
? "已在访达中显示"
|
|
781
|
+
: "打开失败:" + ((data && data.error) || "HTTP " + res.status),
|
|
782
|
+
});
|
|
783
|
+
} catch (e) {
|
|
784
|
+
set({ notice: "打开失败:" + (e && e.message ? e.message : String(e)) });
|
|
785
|
+
}
|
|
786
|
+
}
|
|
787
|
+
|
|
788
|
+
function downloadResult() {
|
|
789
|
+
const cur = state.result;
|
|
790
|
+
if (cur === null) return;
|
|
791
|
+
const a = document.createElement("a");
|
|
792
|
+
a.href = srcOf(cur.filename);
|
|
793
|
+
a.download = cur.filename;
|
|
794
|
+
document.body.appendChild(a);
|
|
795
|
+
a.click();
|
|
796
|
+
a.remove();
|
|
797
|
+
set({ notice: "已开始下载" });
|
|
798
|
+
}
|
|
799
|
+
|
|
800
|
+
/** 停掉等待:让在途的轮询/请求结果失效(服务端任务本身无法撤回) */
|
|
801
|
+
function cancel() {
|
|
802
|
+
if (!state.busy) return;
|
|
803
|
+
runToken += 1;
|
|
804
|
+
stopTimer();
|
|
805
|
+
set({
|
|
806
|
+
busy: false, vStatus: "", vProgress: 0,
|
|
807
|
+
notice: "已停止等待;服务端任务可能仍在跑",
|
|
808
|
+
});
|
|
809
|
+
}
|
|
810
|
+
|
|
811
|
+
/* ---------- 输出目录:把磁盘上已经有的图/视频捞回来 ---------- */
|
|
812
|
+
async function copyPrompt(text) {
|
|
813
|
+
const t = typeof text === "string" ? text.trim() : "";
|
|
814
|
+
if (t === "") return;
|
|
815
|
+
try {
|
|
816
|
+
await navigator.clipboard.writeText(t);
|
|
817
|
+
set({ notice: "提示词已复制", copiedKey: t });
|
|
818
|
+
} catch {
|
|
819
|
+
set({ notice: "复制失败,可手动选中复制" });
|
|
820
|
+
}
|
|
821
|
+
}
|
|
822
|
+
|
|
823
|
+
async function loadLibrary() {
|
|
824
|
+
if (state.libraryBusy) return;
|
|
825
|
+
set({ libraryBusy: true, libraryError: "" });
|
|
826
|
+
try {
|
|
827
|
+
const res = await fetch("/agnes/library");
|
|
828
|
+
const data = await res.json().catch(() => ({}));
|
|
829
|
+
if (!res.ok || !data || data.ok !== true) {
|
|
830
|
+
throw new Error((data && data.error) || "HTTP " + res.status);
|
|
831
|
+
}
|
|
832
|
+
const files = Array.isArray(data.files) ? data.files : [];
|
|
833
|
+
set({
|
|
834
|
+
library: files,
|
|
835
|
+
libraryOpen: true,
|
|
836
|
+
libraryBusy: false,
|
|
837
|
+
libraryError: "",
|
|
838
|
+
});
|
|
839
|
+
const images = files.filter((f) => f.kind === "image").length;
|
|
840
|
+
if (images === 0) {
|
|
841
|
+
const videos = files.filter((f) => f.kind === "video").length;
|
|
842
|
+
if (videos > 0) set({ libraryTab: "video" });
|
|
843
|
+
}
|
|
844
|
+
} catch (e) {
|
|
845
|
+
set({
|
|
846
|
+
libraryBusy: false,
|
|
847
|
+
libraryOpen: true,
|
|
848
|
+
libraryError: e && e.message ? e.message : String(e),
|
|
849
|
+
});
|
|
850
|
+
}
|
|
851
|
+
}
|
|
852
|
+
|
|
853
|
+
function toggleLibrary() {
|
|
854
|
+
if (state.libraryOpen) {
|
|
855
|
+
set({ libraryOpen: false });
|
|
856
|
+
return;
|
|
857
|
+
}
|
|
858
|
+
// 没加载过就先拉一次;已经有清单则直接展开
|
|
859
|
+
if (state.library === null) return loadLibrary();
|
|
860
|
+
set({ libraryOpen: true });
|
|
861
|
+
}
|
|
862
|
+
|
|
863
|
+
function setLibraryTab(tab) {
|
|
864
|
+
set({ libraryTab: tab === "video" ? "video" : "image" });
|
|
865
|
+
}
|
|
866
|
+
|
|
867
|
+
/** 点磁盘上的文件:直接放进结果区看,不经过生成流程 */
|
|
868
|
+
function showLibraryFile(file) {
|
|
869
|
+
if (!file || !file.name) return;
|
|
870
|
+
if (state.busy) return;
|
|
871
|
+
set({
|
|
872
|
+
result: {
|
|
873
|
+
kind: file.kind === "video" ? "video" : "image",
|
|
874
|
+
filename: file.name,
|
|
875
|
+
path: file.path || "",
|
|
876
|
+
bytes: file.bytes || 0,
|
|
877
|
+
prompt: typeof file.prompt === "string" ? file.prompt : "",
|
|
878
|
+
fromDisk: true,
|
|
879
|
+
},
|
|
880
|
+
error: "",
|
|
881
|
+
notice: "",
|
|
882
|
+
});
|
|
883
|
+
}
|
|
884
|
+
|
|
522
885
|
/* ---------- 视频:提交 → 轮询 → 下载完成 ---------- */
|
|
523
886
|
async function runVideo(token, prompt) {
|
|
524
887
|
set({ vStatus: "提交中…", vProgress: 0, videoId: null });
|
|
525
888
|
|
|
889
|
+
const seedText = String(state.vSeed).trim();
|
|
890
|
+
const seedValue = seedText === "" ? null : Number(seedText);
|
|
891
|
+
if (seedValue !== null && !(Number.isInteger(seedValue) && seedValue >= 0)) {
|
|
892
|
+
throw new Error("seed 需要非负整数(也可以留空)");
|
|
893
|
+
}
|
|
894
|
+
|
|
895
|
+
// 关键帧直接发 data URI:上游已实测接受 base64,本地图片不用先传图床
|
|
526
896
|
const subRes = await fetch("/agnes/video", {
|
|
527
897
|
method: "POST",
|
|
528
898
|
headers: { "content-type": "application/json" },
|
|
529
|
-
body: JSON.stringify({
|
|
899
|
+
body: JSON.stringify({
|
|
900
|
+
prompt,
|
|
901
|
+
seconds: state.vSeconds,
|
|
902
|
+
ratio: state.vRatio,
|
|
903
|
+
seed: seedValue,
|
|
904
|
+
firstFrame: state.vFirst !== null ? state.vFirst.src : null,
|
|
905
|
+
lastFrame: state.vLast !== null ? state.vLast.src : null,
|
|
906
|
+
}),
|
|
530
907
|
});
|
|
531
908
|
const sub = await subRes.json();
|
|
532
909
|
if (!sub.ok) throw new Error(sub.error || "提交失败");
|
|
533
910
|
if (token !== runToken) return;
|
|
534
911
|
|
|
912
|
+
const usedMode = sub.mode === "keyframe" ? "keyframe" : "text";
|
|
535
913
|
set({ videoId: sub.videoId, vStatus: "排队中…" });
|
|
536
914
|
const deadline = Date.now() + VIDEO_DEADLINE_MS;
|
|
537
915
|
|
|
@@ -580,6 +958,9 @@ window.__ModuleLoader__.load({
|
|
|
580
958
|
ratio: state.vRatio,
|
|
581
959
|
seconds: st.seconds || state.vSeconds,
|
|
582
960
|
transport: "异步任务",
|
|
961
|
+
mode: usedMode,
|
|
962
|
+
seed: seedValue === null ? undefined : seedValue,
|
|
963
|
+
prompt,
|
|
583
964
|
wallSec: ((Date.now() - state.startedAt) / 1000).toFixed(1),
|
|
584
965
|
});
|
|
585
966
|
return;
|
|
@@ -628,6 +1009,7 @@ window.__ModuleLoader__.load({
|
|
|
628
1009
|
set,
|
|
629
1010
|
loadHealth,
|
|
630
1011
|
loadConfig,
|
|
1012
|
+
loadHistory,
|
|
631
1013
|
saveConfig,
|
|
632
1014
|
setCfgDraft(field, value) {
|
|
633
1015
|
set({ cfgDraft: { ...state.cfgDraft, [field]: value }, cfgNotice: "" });
|
|
@@ -640,16 +1022,36 @@ window.__ModuleLoader__.load({
|
|
|
640
1022
|
removeRef,
|
|
641
1023
|
clearRefs,
|
|
642
1024
|
toggleRefPick,
|
|
1025
|
+
/* ---- 视频关键帧 ---- */
|
|
1026
|
+
pickFrame,
|
|
1027
|
+
setFrameFromFile,
|
|
1028
|
+
clearFrame,
|
|
1029
|
+
toggleFramePick,
|
|
1030
|
+
setFrameFromResult,
|
|
1031
|
+
useResultAsFrame,
|
|
1032
|
+
/* ---- 结果收尾 ---- */
|
|
1033
|
+
copyPath,
|
|
1034
|
+
reveal,
|
|
1035
|
+
downloadResult,
|
|
1036
|
+
cancel,
|
|
1037
|
+
copyPrompt,
|
|
1038
|
+
/* ---- 输出目录清单 ---- */
|
|
1039
|
+
loadLibrary,
|
|
1040
|
+
toggleLibrary,
|
|
1041
|
+
setLibraryTab,
|
|
1042
|
+
showLibraryFile,
|
|
643
1043
|
maxRefs: MAX_REFS,
|
|
644
1044
|
/** 切换主预览到历史里的某一张 */
|
|
645
1045
|
show(entry) { set({ result: entry }); },
|
|
646
1046
|
/**
|
|
647
|
-
*
|
|
648
|
-
*
|
|
1047
|
+
* 清空界面上的生成结果。删的是界面状态和宿主持久化的历史记录,
|
|
1048
|
+
* 不删磁盘文件、不动提示词和参数。进行中的请求不清。
|
|
649
1049
|
*/
|
|
650
1050
|
clear() {
|
|
651
1051
|
if (state.busy) return;
|
|
652
|
-
set({ result: null, history: [], error: "" });
|
|
1052
|
+
set({ result: null, history: [], error: "", notice: "" });
|
|
1053
|
+
// 宿主是历史的真源;这里的 DELETE 失败也不该打断界面
|
|
1054
|
+
void fetch("/agnes/history", { method: "DELETE" }).catch(() => {});
|
|
653
1055
|
},
|
|
654
1056
|
dispose() {
|
|
655
1057
|
stopTimer();
|
|
@@ -666,7 +1068,7 @@ window.__ModuleLoader__.load({
|
|
|
666
1068
|
}
|
|
667
1069
|
|
|
668
1070
|
/* ---------- 媒体显示:视频直接用 <video>,图片带加载兜底 ---------- */
|
|
669
|
-
function MediaBox({ entry, big }) {
|
|
1071
|
+
function MediaBox({ entry, big, onCopyPrompt }) {
|
|
670
1072
|
const isVideo = entry !== null && entry.kind === "video";
|
|
671
1073
|
const name = entry === null ? null : entry.filename;
|
|
672
1074
|
|
|
@@ -715,6 +1117,8 @@ window.__ModuleLoader__.load({
|
|
|
715
1117
|
React.createElement("span", { className: "agn-code" }, entry.path || name)));
|
|
716
1118
|
}
|
|
717
1119
|
|
|
1120
|
+
const promptText = typeof entry.prompt === "string" ? entry.prompt.trim() : "";
|
|
1121
|
+
|
|
718
1122
|
return React.createElement("div", { className: "agn-shot" + (big ? " agn-shot-big" : "") },
|
|
719
1123
|
isVideo
|
|
720
1124
|
? React.createElement("video", {
|
|
@@ -724,7 +1128,20 @@ window.__ModuleLoader__.load({
|
|
|
724
1128
|
preload: "metadata",
|
|
725
1129
|
onError: () => setFailed(true),
|
|
726
1130
|
})
|
|
727
|
-
: React.createElement("img", { src, alt: "生成结果", onError })
|
|
1131
|
+
: React.createElement("img", { src, alt: "生成结果", onError }),
|
|
1132
|
+
/* 鼠标移上去才浮出来的一层:显示当初的提示词,带一键复制 */
|
|
1133
|
+
promptText === ""
|
|
1134
|
+
? null
|
|
1135
|
+
: React.createElement("div", { className: "agn-prompt" },
|
|
1136
|
+
React.createElement("div", { className: "agn-prompt-text", title: promptText }, promptText),
|
|
1137
|
+
React.createElement("button", {
|
|
1138
|
+
type: "button", className: "agn-prompt-copy",
|
|
1139
|
+
title: "复制这段提示词",
|
|
1140
|
+
onClick: (e) => {
|
|
1141
|
+
e.stopPropagation();
|
|
1142
|
+
if (typeof onCopyPrompt === "function") onCopyPrompt(promptText);
|
|
1143
|
+
},
|
|
1144
|
+
}, "复制"))
|
|
728
1145
|
);
|
|
729
1146
|
}
|
|
730
1147
|
|
|
@@ -755,7 +1172,7 @@ window.__ModuleLoader__.load({
|
|
|
755
1172
|
return () => { off(); };
|
|
756
1173
|
}, []);
|
|
757
1174
|
|
|
758
|
-
React.useEffect(() => { store.loadHealth(); store.loadConfig(); }, []);
|
|
1175
|
+
React.useEffect(() => { store.loadHealth(); store.loadConfig(); store.loadHistory(); }, []);
|
|
759
1176
|
|
|
760
1177
|
const busy = s.busy;
|
|
761
1178
|
const isVideo = s.mode === "video";
|
|
@@ -765,7 +1182,12 @@ window.__ModuleLoader__.load({
|
|
|
765
1182
|
const hasRefs = s.refs.length > 0;
|
|
766
1183
|
const refsFull = s.refs.length >= MAX_REFS;
|
|
767
1184
|
const imageHistory = s.history.filter((h) => h.kind === "image");
|
|
1185
|
+
const libraryAll = s.library || [];
|
|
1186
|
+
const libraryImages = libraryAll.filter((f) => f.kind !== "video");
|
|
1187
|
+
const libraryVideos = libraryAll.filter((f) => f.kind === "video");
|
|
1188
|
+
const libraryShown = s.libraryTab === "video" ? libraryVideos : libraryImages;
|
|
768
1189
|
const fileInput = React.useRef(null);
|
|
1190
|
+
const frameInput = React.useRef(null);
|
|
769
1191
|
|
|
770
1192
|
const onKeyDown = (e) => {
|
|
771
1193
|
if ((e.metaKey || e.ctrlKey) && e.key === "Enter") { e.preventDefault(); store.run(); }
|
|
@@ -797,6 +1219,13 @@ window.__ModuleLoader__.load({
|
|
|
797
1219
|
onChange: (e) => store.set({ vRatio: e.target.value }),
|
|
798
1220
|
}, VIDEO_RATIOS.map((x) => React.createElement("option", { key: x, value: x }, x))),
|
|
799
1221
|
React.createElement("span", { key: "m", className: "agn-meta" }, "720P"),
|
|
1222
|
+
React.createElement("span", { key: "l3", className: "agn-label" }, "种子"),
|
|
1223
|
+
React.createElement("input", {
|
|
1224
|
+
key: "s3", type: "text", inputMode: "numeric", className: "agn-seed",
|
|
1225
|
+
value: s.vSeed, disabled: busy, placeholder: "随机",
|
|
1226
|
+
title: "固定 seed 可让同一提示词出片更稳定;留空由服务端随机",
|
|
1227
|
+
onChange: (e) => store.set({ vSeed: e.target.value }),
|
|
1228
|
+
}),
|
|
800
1229
|
]
|
|
801
1230
|
: [
|
|
802
1231
|
React.createElement("span", { key: "l1", className: "agn-label" }, "尺寸"),
|
|
@@ -815,6 +1244,57 @@ window.__ModuleLoader__.load({
|
|
|
815
1244
|
? "生成结果(还没有)"
|
|
816
1245
|
: (shown.kind === "video" ? "生成结果 · 视频" : "生成结果 · 图片");
|
|
817
1246
|
|
|
1247
|
+
/* 视频缩略图:放一个真的 <video>,浏览器会解码出首帧当封面。
|
|
1248
|
+
#t=0.1 是媒体片段定位,避免第一帧偶尔是黑场;muted + playsInline
|
|
1249
|
+
保证不会被自动播放策略拦掉,也不会突然出声。 */
|
|
1250
|
+
const videoThumb = (name) => React.createElement("video", {
|
|
1251
|
+
src: srcOf(name) + "#t=0.1",
|
|
1252
|
+
preload: "metadata",
|
|
1253
|
+
muted: true,
|
|
1254
|
+
playsInline: true,
|
|
1255
|
+
tabIndex: -1,
|
|
1256
|
+
});
|
|
1257
|
+
|
|
1258
|
+
/* 关键帧小格:有图显示缩略图 + ×,无图显示「+ 本地」;
|
|
1259
|
+
下方固定一行「+ 结果」,从已经生成过的图片里挑一张 */
|
|
1260
|
+
const frameSlot = (which, label, cur) => React.createElement("div", { className: "agn-frame" },
|
|
1261
|
+
React.createElement("div", { className: "agn-frame-cap" }, label),
|
|
1262
|
+
cur !== null
|
|
1263
|
+
? React.createElement("div", { className: "agn-frame-box" },
|
|
1264
|
+
React.createElement("img", { src: cur.src, alt: label }),
|
|
1265
|
+
React.createElement("button", {
|
|
1266
|
+
type: "button", className: "agn-ref-del", title: "移除" + label,
|
|
1267
|
+
disabled: busy,
|
|
1268
|
+
onClick: () => store.clearFrame(which),
|
|
1269
|
+
}, "×"))
|
|
1270
|
+
: React.createElement("button", {
|
|
1271
|
+
type: "button", className: "agn-frame-add",
|
|
1272
|
+
disabled: busy || s.frameBusy,
|
|
1273
|
+
title: "从本地磁盘选择" + label + "图片(PNG / JPEG / WebP / GIF)",
|
|
1274
|
+
onClick: () => {
|
|
1275
|
+
store.pickFrame(which);
|
|
1276
|
+
const el = frameInput.current;
|
|
1277
|
+
if (el !== null) el.click();
|
|
1278
|
+
},
|
|
1279
|
+
}, "+ 本地"),
|
|
1280
|
+
React.createElement("button", {
|
|
1281
|
+
type: "button", className: "agn-frame-from",
|
|
1282
|
+
disabled: busy || s.frameBusy || imageHistory.length === 0,
|
|
1283
|
+
title: imageHistory.length === 0
|
|
1284
|
+
? "还没有生成过图片,先出一张图再来选"
|
|
1285
|
+
: "从生成结果里挑一张当" + label,
|
|
1286
|
+
onClick: () => store.toggleFramePick(which),
|
|
1287
|
+
}, s.framePickFrom === which ? "收起" : "+ 结果"),
|
|
1288
|
+
s.framePickFrom === which
|
|
1289
|
+
? React.createElement("div", { className: "agn-ref-pick agn-frame-pick" },
|
|
1290
|
+
imageHistory.map((h) => React.createElement("button", {
|
|
1291
|
+
key: h.filename, type: "button", title: h.filename,
|
|
1292
|
+
disabled: busy || s.frameBusy,
|
|
1293
|
+
onClick: () => store.setFrameFromResult(which, h),
|
|
1294
|
+
}, React.createElement("img", { src: srcOf(h.filename), alt: h.filename }))))
|
|
1295
|
+
: null
|
|
1296
|
+
);
|
|
1297
|
+
|
|
818
1298
|
return React.createElement("div", { className: "agn-root" },
|
|
819
1299
|
/* 隐藏的文件选择器:由「+ 本地图片」按钮触发,支持多选 */
|
|
820
1300
|
React.createElement("input", {
|
|
@@ -835,6 +1315,22 @@ window.__ModuleLoader__.load({
|
|
|
835
1315
|
},
|
|
836
1316
|
}),
|
|
837
1317
|
|
|
1318
|
+
/* 隐藏的关键帧选择器:首帧 / 尾帧共用,靠 store.framePick 区分目标 */
|
|
1319
|
+
React.createElement("input", {
|
|
1320
|
+
ref: frameInput,
|
|
1321
|
+
type: "file",
|
|
1322
|
+
accept: "image/png,image/jpeg,image/webp,image/gif",
|
|
1323
|
+
style: { display: "none" },
|
|
1324
|
+
onChange: (e) => {
|
|
1325
|
+
const el = e.target;
|
|
1326
|
+
const file = el.files && el.files[0];
|
|
1327
|
+
const which = s.framePick === "last" ? "last" : "first";
|
|
1328
|
+
Promise.resolve(store.setFrameFromFile(which, file))
|
|
1329
|
+
.catch(() => {})
|
|
1330
|
+
.finally(() => { try { el.value = ""; } catch { /* input 可能已卸载 */ } });
|
|
1331
|
+
},
|
|
1332
|
+
}),
|
|
1333
|
+
|
|
838
1334
|
React.createElement("div", { className: "agn-head" },
|
|
839
1335
|
React.createElement("span", { className: "agn-title" }, "Agnes 创作"),
|
|
840
1336
|
React.createElement("span", { className: "agn-meta" },
|
|
@@ -849,6 +1345,22 @@ window.__ModuleLoader__.load({
|
|
|
849
1345
|
modeBtn("video", "视频")
|
|
850
1346
|
),
|
|
851
1347
|
|
|
1348
|
+
/* ---- 视频关键帧(图生视频):本地图片直接当首帧/尾帧,留空即纯文生视频 ---- */
|
|
1349
|
+
isVideo
|
|
1350
|
+
? React.createElement("div", { className: "agn-frames" },
|
|
1351
|
+
React.createElement("div", { className: "agn-ref-head" },
|
|
1352
|
+
React.createElement("span", { className: "agn-label" }, "关键帧"),
|
|
1353
|
+
React.createElement("span", { className: "agn-hint" },
|
|
1354
|
+
s.vFirst !== null || s.vLast !== null ? "图生视频" : "留空 = 纯文生视频"),
|
|
1355
|
+
s.frameBusy ? React.createElement("span", { className: "agn-hint" }, "处理中…") : null
|
|
1356
|
+
),
|
|
1357
|
+
React.createElement("div", { className: "agn-frames-row" },
|
|
1358
|
+
frameSlot("first", "首帧", s.vFirst),
|
|
1359
|
+
frameSlot("last", "尾帧", s.vLast)
|
|
1360
|
+
)
|
|
1361
|
+
)
|
|
1362
|
+
: null,
|
|
1363
|
+
|
|
852
1364
|
/* ---- 参考图(图生图 / 多图合成):本地文件 + 生成结果两种来源 ---- */
|
|
853
1365
|
!isVideo
|
|
854
1366
|
? React.createElement("div", { className: "agn-refs" },
|
|
@@ -922,6 +1434,20 @@ window.__ModuleLoader__.load({
|
|
|
922
1434
|
: null,
|
|
923
1435
|
|
|
924
1436
|
React.createElement("div", { className: "agn-label" }, "提示词"),
|
|
1437
|
+
React.createElement("div", { className: "agn-presets" },
|
|
1438
|
+
PROMPT_PRESETS.map((preset) => React.createElement("button", {
|
|
1439
|
+
key: preset.label,
|
|
1440
|
+
type: "button",
|
|
1441
|
+
className: "agn-preset",
|
|
1442
|
+
disabled: busy,
|
|
1443
|
+
title: "追加到提示词末尾:" + preset.text,
|
|
1444
|
+
onClick: () => store.set({
|
|
1445
|
+
prompt: s.prompt.trim() === ""
|
|
1446
|
+
? preset.text
|
|
1447
|
+
: s.prompt.replace(/[\s,,;;。.]+$/, "") + "," + preset.text,
|
|
1448
|
+
}),
|
|
1449
|
+
}, preset.label))
|
|
1450
|
+
),
|
|
925
1451
|
React.createElement("textarea", {
|
|
926
1452
|
className: "agn-textarea",
|
|
927
1453
|
value: s.prompt,
|
|
@@ -938,7 +1464,17 @@ window.__ModuleLoader__.load({
|
|
|
938
1464
|
className: "agn-btn", type: "button",
|
|
939
1465
|
disabled: busy || s.refsBusy || s.prompt.trim() === "" || keyBad,
|
|
940
1466
|
onClick: () => store.run(),
|
|
941
|
-
}, buttonLabel)
|
|
1467
|
+
}, buttonLabel),
|
|
1468
|
+
/* 视频是异步任务,等待可能很长:给出真正的「停止等待」而不是让用户干等 */
|
|
1469
|
+
busy
|
|
1470
|
+
? React.createElement("button", {
|
|
1471
|
+
className: "agn-btn-ghost", type: "button",
|
|
1472
|
+
title: isVideo
|
|
1473
|
+
? "停止等待(服务端任务可能仍在跑,已生成的文件不会丢)"
|
|
1474
|
+
: "放弃这次等待(请求已发出,结果可能仍会落盘)",
|
|
1475
|
+
onClick: () => store.cancel(),
|
|
1476
|
+
}, "取消")
|
|
1477
|
+
: null
|
|
942
1478
|
),
|
|
943
1479
|
|
|
944
1480
|
/* 视频是异步任务:给出阶段与百分比,避免只看到一个跳动的秒数 */
|
|
@@ -960,6 +1496,7 @@ window.__ModuleLoader__.load({
|
|
|
960
1496
|
"未找到 Key:" + (key.file || "") + " —— 把 Key 写进该文件后可立即使用")
|
|
961
1497
|
: null,
|
|
962
1498
|
s.error !== "" ? React.createElement("div", { className: "agn-err" }, s.error) : null,
|
|
1499
|
+
s.notice !== "" ? React.createElement("div", { className: "agn-notice" }, s.notice) : null,
|
|
963
1500
|
busy && !isVideo
|
|
964
1501
|
? React.createElement("div", { className: "agn-hint" },
|
|
965
1502
|
"正在生成:可切到别的面板,回来仍会接着显示结果(请求不会中断)")
|
|
@@ -970,6 +1507,12 @@ window.__ModuleLoader__.load({
|
|
|
970
1507
|
React.createElement("div", { className: "agn-card" },
|
|
971
1508
|
React.createElement("div", { className: "agn-card-head" },
|
|
972
1509
|
React.createElement("div", { className: "agn-label" }, resultLabel),
|
|
1510
|
+
React.createElement("button", {
|
|
1511
|
+
type: "button",
|
|
1512
|
+
className: "agn-btn-ghost",
|
|
1513
|
+
title: "把输出目录里已有的图片 / 视频列出来,点一下就能显示在上方",
|
|
1514
|
+
onClick: () => store.toggleLibrary(),
|
|
1515
|
+
}, s.libraryOpen ? "收起目录" : "输出目录"),
|
|
973
1516
|
React.createElement("button", {
|
|
974
1517
|
type: "button",
|
|
975
1518
|
className: "agn-btn-ghost",
|
|
@@ -980,7 +1523,7 @@ window.__ModuleLoader__.load({
|
|
|
980
1523
|
),
|
|
981
1524
|
|
|
982
1525
|
shown !== null
|
|
983
|
-
? React.createElement(MediaBox, { entry: shown, big: true })
|
|
1526
|
+
? React.createElement(MediaBox, { entry: shown, big: true, onCopyPrompt: (t) => store.copyPrompt(t) })
|
|
984
1527
|
: React.createElement("div", { className: "agn-shot" },
|
|
985
1528
|
React.createElement("div", { className: "agn-shot-msg" },
|
|
986
1529
|
busy ? (isVideo ? "正在生成视频…" : "正在生成图片…")
|
|
@@ -989,19 +1532,116 @@ window.__ModuleLoader__.load({
|
|
|
989
1532
|
shown !== null
|
|
990
1533
|
? React.createElement("div", { className: "agn-hint" },
|
|
991
1534
|
React.createElement("span", null,
|
|
992
|
-
shown.
|
|
1535
|
+
(shown.fromDisk ? "来自输出目录 · " : "")
|
|
1536
|
+
+ shown.filename + " · " + (shown.bytes / 1024 / 1024 >= 1
|
|
993
1537
|
? (shown.bytes / 1024 / 1024).toFixed(2) + " MB"
|
|
994
1538
|
: (shown.bytes / 1024).toFixed(0) + " KB")
|
|
995
1539
|
+ " · " + shown.size + "/" + shown.ratio
|
|
996
1540
|
+ (shown.seconds ? " · " + shown.seconds + " 秒" : "")
|
|
1541
|
+
+ (shown.mode === "keyframe" ? " · 图生视频" : "")
|
|
997
1542
|
+ (shown.mode === "img2img" ? " · 图生图 " + (shown.refCount || 0) + " 张参考" : "")
|
|
998
|
-
+ " ·
|
|
1543
|
+
+ (shown.seed !== undefined && shown.seed !== null ? " · seed " + shown.seed : "")
|
|
1544
|
+
+ " · 用时 " + (shown.elapsedSec || shown.wallSec) + "s"),
|
|
999
1545
|
React.createElement("br"),
|
|
1000
1546
|
React.createElement("span", null, "已保存到 "),
|
|
1001
1547
|
React.createElement("span", { className: "agn-code" }, shown.path || ""))
|
|
1548
|
+
: null,
|
|
1549
|
+
|
|
1550
|
+
shown !== null
|
|
1551
|
+
? React.createElement("div", { className: "agn-acts" },
|
|
1552
|
+
React.createElement("button", {
|
|
1553
|
+
type: "button", className: "agn-act",
|
|
1554
|
+
title: "把文件路径复制到剪贴板",
|
|
1555
|
+
onClick: () => store.copyPath(),
|
|
1556
|
+
}, "复制路径"),
|
|
1557
|
+
React.createElement("button", {
|
|
1558
|
+
type: "button", className: "agn-act",
|
|
1559
|
+
title: "在访达中显示这个文件",
|
|
1560
|
+
onClick: () => store.reveal(),
|
|
1561
|
+
}, "在访达中显示"),
|
|
1562
|
+
React.createElement("button", {
|
|
1563
|
+
type: "button", className: "agn-act",
|
|
1564
|
+
title: "另存一份到「下载」目录",
|
|
1565
|
+
onClick: () => store.downloadResult(),
|
|
1566
|
+
}, "下载"),
|
|
1567
|
+
shown.kind === "image"
|
|
1568
|
+
? React.createElement("button", {
|
|
1569
|
+
type: "button", className: "agn-act",
|
|
1570
|
+
disabled: busy || s.frameBusy,
|
|
1571
|
+
title: "把这张图设为视频首帧(切到视频模式即可看到)",
|
|
1572
|
+
onClick: () => store.useResultAsFrame("first"),
|
|
1573
|
+
}, "设为首帧")
|
|
1574
|
+
: null,
|
|
1575
|
+
shown.kind === "image"
|
|
1576
|
+
? React.createElement("button", {
|
|
1577
|
+
type: "button", className: "agn-act",
|
|
1578
|
+
disabled: busy || s.frameBusy,
|
|
1579
|
+
title: "把这张图设为视频尾帧(切到视频模式即可看到)",
|
|
1580
|
+
onClick: () => store.useResultAsFrame("last"),
|
|
1581
|
+
}, "设为尾帧")
|
|
1582
|
+
: null)
|
|
1002
1583
|
: null
|
|
1003
1584
|
),
|
|
1004
1585
|
|
|
1586
|
+
/* ---- 输出目录:把磁盘上已有的图 / 视频捞回来看 ---- */
|
|
1587
|
+
s.libraryOpen
|
|
1588
|
+
? React.createElement("div", { className: "agn-card" },
|
|
1589
|
+
React.createElement("div", { className: "agn-card-head" },
|
|
1590
|
+
React.createElement("div", { className: "agn-label" }, "输出目录"),
|
|
1591
|
+
React.createElement("button", {
|
|
1592
|
+
type: "button", className: "agn-btn-ghost",
|
|
1593
|
+
disabled: s.libraryBusy,
|
|
1594
|
+
title: "重新读一遍输出目录(新生成的文件会自动出现在这里)",
|
|
1595
|
+
onClick: () => store.loadLibrary(),
|
|
1596
|
+
}, s.libraryBusy ? "读取中…" : "刷新")
|
|
1597
|
+
),
|
|
1598
|
+
React.createElement("div", { className: "agn-hint" },
|
|
1599
|
+
"点缩略图会把它显示到上方结果区,并带上当初写进图片里的提示词"),
|
|
1600
|
+
React.createElement("div", { className: "agn-lib" },
|
|
1601
|
+
React.createElement("div", { className: "agn-lib-tabs" },
|
|
1602
|
+
React.createElement("button", {
|
|
1603
|
+
type: "button",
|
|
1604
|
+
className: "agn-act" + (s.libraryTab === "image" ? " agn-act-on" : ""),
|
|
1605
|
+
onClick: () => store.setLibraryTab("image"),
|
|
1606
|
+
}, "图片 " + libraryImages.length),
|
|
1607
|
+
React.createElement("button", {
|
|
1608
|
+
type: "button",
|
|
1609
|
+
className: "agn-act" + (s.libraryTab === "video" ? " agn-act-on" : ""),
|
|
1610
|
+
onClick: () => store.setLibraryTab("video"),
|
|
1611
|
+
}, "视频 " + libraryVideos.length)
|
|
1612
|
+
),
|
|
1613
|
+
s.libraryError !== ""
|
|
1614
|
+
? React.createElement("div", { className: "agn-err" }, s.libraryError)
|
|
1615
|
+
: null,
|
|
1616
|
+
libraryShown.length === 0
|
|
1617
|
+
? React.createElement("div", { className: "agn-lib-empty" },
|
|
1618
|
+
s.libraryBusy
|
|
1619
|
+
? "正在读取输出目录…"
|
|
1620
|
+
: "这个目录里还没有" + (s.libraryTab === "video" ? "视频" : "图片"))
|
|
1621
|
+
: React.createElement("div", { className: "agn-lib-grid" },
|
|
1622
|
+
libraryShown.map((f) => React.createElement("button", {
|
|
1623
|
+
key: f.name,
|
|
1624
|
+
type: "button",
|
|
1625
|
+
className: "agn-lib-item" + (shown && shown.filename === f.name ? " agn-thumb-on" : ""),
|
|
1626
|
+
title: (f.prompt ? f.prompt + "\n" : "") + f.name,
|
|
1627
|
+
disabled: busy,
|
|
1628
|
+
onClick: () => store.showLibraryFile(f),
|
|
1629
|
+
},
|
|
1630
|
+
f.kind === "video"
|
|
1631
|
+
? videoThumb(f.name)
|
|
1632
|
+
: React.createElement("img", { src: srcOf(f.name), alt: f.name, loading: "lazy" }),
|
|
1633
|
+
f.kind === "video"
|
|
1634
|
+
? React.createElement("div", { className: "agn-thumb-play" }, "▶")
|
|
1635
|
+
: null,
|
|
1636
|
+
f.kind === "video"
|
|
1637
|
+
? React.createElement("span", { className: "agn-lib-badge" }, "视频")
|
|
1638
|
+
: null
|
|
1639
|
+
))
|
|
1640
|
+
)
|
|
1641
|
+
)
|
|
1642
|
+
)
|
|
1643
|
+
: null,
|
|
1644
|
+
|
|
1005
1645
|
/* ---- 历史 ---- */
|
|
1006
1646
|
s.history.length > 1
|
|
1007
1647
|
? React.createElement("div", { className: "agn-card agn-hist" },
|
|
@@ -1015,8 +1655,11 @@ window.__ModuleLoader__.load({
|
|
|
1015
1655
|
onClick: () => store.show(h),
|
|
1016
1656
|
},
|
|
1017
1657
|
h.kind === "video"
|
|
1018
|
-
?
|
|
1658
|
+
? videoThumb(h.filename)
|
|
1019
1659
|
: React.createElement("img", { src: srcOf(h.filename), alt: h.filename, loading: "lazy" }),
|
|
1660
|
+
h.kind === "video"
|
|
1661
|
+
? React.createElement("div", { className: "agn-thumb-play" }, "▶")
|
|
1662
|
+
: null,
|
|
1020
1663
|
React.createElement("span", { className: "agn-thumb-cap" },
|
|
1021
1664
|
h.kind === "video"
|
|
1022
1665
|
? (h.seconds ? h.seconds + "s" : "视频")
|
|
@@ -1091,7 +1734,7 @@ window.__ModuleLoader__.load({
|
|
|
1091
1734
|
s.cfgOpen
|
|
1092
1735
|
? React.createElement("div", { className: "agn-note" },
|
|
1093
1736
|
"保存到 ",
|
|
1094
|
-
(s.health && s.health.configFile) || "~/.dsh/agnes-
|
|
1737
|
+
(s.health && s.health.configFile) || "~/.dsh/agnes-create/config.json")
|
|
1095
1738
|
: null
|
|
1096
1739
|
),
|
|
1097
1740
|
|