dsh-agnes-create 0.7.0 → 0.11.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 +47 -2
- package/lib/agnes.js +299 -12
- package/lib/client.js +720 -78
- package/lib/index.js +550 -10
- package/package.json +2 -2
package/lib/client.js
CHANGED
|
@@ -13,11 +13,16 @@ window.__ModuleLoader__.load({
|
|
|
13
13
|
// 视频:Flash 固定 720P,seconds 是字符串 "4"–"12"
|
|
14
14
|
const VIDEO_SECONDS = ["4", "5", "6", "7", "8", "9", "10", "11", "12"];
|
|
15
15
|
const VIDEO_RATIOS = ["21:9", "16:9", "4:3", "1:1", "3:4", "9:16"];
|
|
16
|
-
|
|
16
|
+
/* 与宿主 agnes.js 的 HISTORY_MAX 保持一致:首屏先按 24 条渲染,
|
|
17
|
+
loadHistory() 回来后以宿主返回的 max 为准 */
|
|
18
|
+
const HISTORY_MAX = 24;
|
|
17
19
|
const POLL_MS = 6000;
|
|
18
20
|
const VIDEO_DEADLINE_MS = 15 * 60 * 1000;
|
|
19
21
|
// 图生图:一次最多 8 张参考图(与宿主 MAX_REF_IMAGES 保持一致)
|
|
20
22
|
const MAX_REFS = 8;
|
|
23
|
+
|
|
24
|
+
/** 视频 reference 模式的参考图上限(上游:images 最多 5 张) */
|
|
25
|
+
const MAX_VREFS = 5;
|
|
21
26
|
// 发给上游前先下采样:长边 1280 足够模型识别,base64 也压得住
|
|
22
27
|
const REF_MAX_EDGE = 1280;
|
|
23
28
|
const REF_QUALITY = 0.88;
|
|
@@ -45,6 +50,13 @@ window.__ModuleLoader__.load({
|
|
|
45
50
|
.agn-head { display: flex; align-items: baseline; gap: 10px; flex-wrap: wrap; }
|
|
46
51
|
.agn-title { font-size: 16px; font-weight: 600; color: var(--dsw-alias-label-primary, inherit); }
|
|
47
52
|
.agn-meta { font-size: 12px; color: var(--dsw-alias-label-secondary, #888); }
|
|
53
|
+
/* 标题栏里的模型下拉:长得像一行说明文字,但可以点开换模型 */
|
|
54
|
+
.agn-model { font: inherit; font-size: 12px; line-height: 1.4; padding: 1px 4px;
|
|
55
|
+
max-width: 220px; cursor: pointer; border-radius: 6px;
|
|
56
|
+
color: var(--dsw-alias-label-secondary, #888); background: transparent;
|
|
57
|
+
border: 1px solid transparent; }
|
|
58
|
+
.agn-model:hover:not(:disabled) { border-color: var(--dsw-alias-border-l2, #ddd); }
|
|
59
|
+
.agn-model:disabled { opacity: .6; cursor: not-allowed; }
|
|
48
60
|
.agn-card { border: 1px solid var(--dsw-alias-border-l2, #e3e3e3); border-radius: 10px;
|
|
49
61
|
padding: 12px 14px; display: flex; flex-direction: column; gap: 10px; }
|
|
50
62
|
.agn-label { font-size: 12px; font-weight: 600; color: var(--dsw-alias-label-secondary, #666); }
|
|
@@ -206,6 +218,27 @@ window.__ModuleLoader__.load({
|
|
|
206
218
|
.agn-preset:hover:not(:disabled) { border-color: var(--dsw-alias-state-business-primary, #4b7bec);
|
|
207
219
|
color: var(--dsw-alias-state-business-primary, #4b7bec); }
|
|
208
220
|
.agn-preset:disabled { opacity: .45; cursor: not-allowed; }
|
|
221
|
+
/* 我的预设:名字 + 删除叉,包在一个 span 里 */
|
|
222
|
+
.agn-preset-user { display: inline-flex; align-items: stretch; }
|
|
223
|
+
.agn-preset-mine { border-top-right-radius: 0; border-bottom-right-radius: 0;
|
|
224
|
+
border-right: none; }
|
|
225
|
+
.agn-preset-x { font: inherit; font-size: 11px; line-height: 1.4; padding: 2px 7px;
|
|
226
|
+
border-radius: 0 999px 999px 0; cursor: pointer;
|
|
227
|
+
color: var(--dsw-alias-label-secondary, #888); background: transparent;
|
|
228
|
+
border: 1px solid var(--dsw-alias-border-l2, #ddd); }
|
|
229
|
+
.agn-preset-x:hover:not(:disabled) { color: var(--dsw-alias-state-error-primary, #e5484d);
|
|
230
|
+
border-color: var(--dsw-alias-state-error-primary, #e5484d); }
|
|
231
|
+
.agn-preset-x:disabled { opacity: .45; cursor: not-allowed; }
|
|
232
|
+
.agn-preset-add { border-style: dashed; }
|
|
233
|
+
.agn-preset-expand { color: var(--dsw-alias-state-business-primary, #4b7bec);
|
|
234
|
+
border-color: var(--dsw-alias-state-business-primary, #4b7bec); }
|
|
235
|
+
.agn-preset-form { display: flex; gap: 6px; align-items: center; margin-bottom: 6px; }
|
|
236
|
+
.agn-input-sm { flex: 1 1 auto; min-width: 0; font: inherit; font-size: 12px;
|
|
237
|
+
padding: 3px 8px; border-radius: 6px;
|
|
238
|
+
color: var(--dsw-alias-label-primary, #222); background: transparent;
|
|
239
|
+
border: 1px solid var(--dsw-alias-border-l2, #ddd); }
|
|
240
|
+
.agn-act-link { text-decoration: none; color: inherit; display: inline-flex;
|
|
241
|
+
align-items: center; }
|
|
209
242
|
|
|
210
243
|
/* 视频关键帧(图生视频) */
|
|
211
244
|
.agn-frames { display: flex; flex-direction: column; gap: 8px; }
|
|
@@ -243,6 +276,8 @@ window.__ModuleLoader__.load({
|
|
|
243
276
|
color: var(--dsw-alias-state-business-primary, #4b7bec); }
|
|
244
277
|
.agn-act-on { border-color: var(--dsw-alias-state-business-primary, #4b7bec);
|
|
245
278
|
color: var(--dsw-alias-state-business-primary, #4b7bec); }
|
|
279
|
+
/* 「测试连接」的结果行:和按钮之间留一点距离 */
|
|
280
|
+
.agn-check { margin-top: 6px; }
|
|
246
281
|
.agn-notice { font-size: 12px; color: var(--dsw-alias-state-success-primary, #2a2);
|
|
247
282
|
line-height: 1.6; }
|
|
248
283
|
`;
|
|
@@ -261,6 +296,25 @@ window.__ModuleLoader__.load({
|
|
|
261
296
|
return "/agnes/media?name=" + encodeURIComponent(name);
|
|
262
297
|
}
|
|
263
298
|
|
|
299
|
+
/* 「+ 结果」的候选:最近生成的图片 + 当前结果区那张(按文件名去重)。
|
|
300
|
+
从「输出目录」点出来的图只进了结果区、没进历史,只翻历史会漏掉它,
|
|
301
|
+
于是刚在眼前看到的那张图反而选不进参考图 / 关键帧。 */
|
|
302
|
+
function pickableImagesOf(state) {
|
|
303
|
+
const history = (Array.isArray(state.history) ? state.history : []).filter((h) => h.kind === "image");
|
|
304
|
+
const cur = state.result;
|
|
305
|
+
if (cur === null || cur === undefined || cur.kind !== "image") return history;
|
|
306
|
+
if (typeof cur.filename !== "string" || cur.filename === "") return history;
|
|
307
|
+
if (history.some((h) => h.filename === cur.filename)) return history;
|
|
308
|
+
return [cur].concat(history);
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
/* 视频生成方式:宿主回来的 mode 只可能是这三种之一,其余一律当纯文本 */
|
|
312
|
+
function modeOf(mode) {
|
|
313
|
+
if (mode === "keyframe") return "keyframe";
|
|
314
|
+
if (mode === "reference") return "reference";
|
|
315
|
+
return "text";
|
|
316
|
+
}
|
|
317
|
+
|
|
264
318
|
/* ==========================================================================
|
|
265
319
|
参考图预处理(图生图)
|
|
266
320
|
--------------------------------------------------------------------------
|
|
@@ -362,6 +416,7 @@ window.__ModuleLoader__.load({
|
|
|
362
416
|
vProgress: 0,
|
|
363
417
|
vStatus: "",
|
|
364
418
|
videoId: null,
|
|
419
|
+
vTaskId: null,
|
|
365
420
|
health: null,
|
|
366
421
|
healthRequested: false,
|
|
367
422
|
|
|
@@ -369,17 +424,25 @@ window.__ModuleLoader__.load({
|
|
|
369
424
|
cfg: null, // 生效配置 { keyFile, outDir }
|
|
370
425
|
cfgDefaults: null,
|
|
371
426
|
cfgStored: null,
|
|
372
|
-
cfgDraft: { keyFile: "", outDir: "" },
|
|
427
|
+
cfgDraft: { keyFile: "", outDir: "", imageModel: "", videoModel: "" },
|
|
373
428
|
cfgOpen: false,
|
|
374
429
|
cfgSaving: false,
|
|
375
430
|
cfgError: "",
|
|
376
431
|
cfgNotice: "",
|
|
432
|
+
checking: false, // 正在测连通性
|
|
433
|
+
checkResult: null, // { ok, text, detail }
|
|
434
|
+
upModels: null, // 连接测试拿到的上游真实模型清单 { image: [], video: [] }
|
|
377
435
|
|
|
378
436
|
/* ---- 图生图参考图 ---- */
|
|
379
437
|
refs: [], // { id, name, src(dataURI), bytes, from: 'local' | 'result' }
|
|
380
438
|
refsBusy: false,
|
|
381
439
|
refsError: "",
|
|
382
440
|
refPickOpen: false,
|
|
441
|
+
/* ---- 视频参考图(reference 模式,最多 5 张) ---- */
|
|
442
|
+
vRefs: [],
|
|
443
|
+
vRefsBusy: false,
|
|
444
|
+
vRefsError: "",
|
|
445
|
+
vRefPickOpen: false,
|
|
383
446
|
|
|
384
447
|
/* ---- 视频关键帧(可选):本地图直接当首帧 / 尾帧 ---- */
|
|
385
448
|
vFirst: null, // { src(dataURI), name, bytes }
|
|
@@ -401,6 +464,14 @@ window.__ModuleLoader__.load({
|
|
|
401
464
|
libraryBusy: false,
|
|
402
465
|
libraryError: "",
|
|
403
466
|
copiedKey: "", // 刚刚复制了哪一条的提示词(用于显示「已复制」)
|
|
467
|
+
|
|
468
|
+
/* ---- 自定义提示词预设(宿主落盘) + 扩写 ---- */
|
|
469
|
+
presets: [], // [{ label, text }]
|
|
470
|
+
presetsLoaded: false,
|
|
471
|
+
presetFormOpen: false, // 「存为预设」的小表单
|
|
472
|
+
presetLabel: "",
|
|
473
|
+
presetBusy: false,
|
|
474
|
+
expandBusy: false,
|
|
404
475
|
};
|
|
405
476
|
|
|
406
477
|
const snapshot = () => ({ ...state, history: state.history.slice() });
|
|
@@ -437,8 +508,98 @@ window.__ModuleLoader__.load({
|
|
|
437
508
|
set({ result: entry, history: [entry, ...state.history].slice(0, state.historyMax) });
|
|
438
509
|
// 顺手落盘:/agnes/history 是唯一真相,写失败也不影响本次展示
|
|
439
510
|
void appendHistory(entry);
|
|
511
|
+
// 新文件已经写进输出目录了,目录清单开着的话跟着变(没加载过就不去打扰)
|
|
512
|
+
void refreshLibrary();
|
|
440
513
|
};
|
|
441
514
|
|
|
515
|
+
/* ---------- 自定义提示词预设(宿主落盘) ---------- */
|
|
516
|
+
async function loadPresets() {
|
|
517
|
+
try {
|
|
518
|
+
const res = await fetch("/agnes/presets");
|
|
519
|
+
if (!res.ok) { set({ presetsLoaded: true }); return; }
|
|
520
|
+
const data = await res.json();
|
|
521
|
+
if (!data || data.ok !== true) { set({ presetsLoaded: true }); return; }
|
|
522
|
+
set({ presets: Array.isArray(data.presets) ? data.presets : [], presetsLoaded: true });
|
|
523
|
+
} catch {
|
|
524
|
+
set({ presetsLoaded: true });
|
|
525
|
+
}
|
|
526
|
+
}
|
|
527
|
+
|
|
528
|
+
const presetText = (preset) => (
|
|
529
|
+
state.prompt.trim() === ""
|
|
530
|
+
? preset.text
|
|
531
|
+
: state.prompt.replace(/[\s,,;;。.]+$/, "") + "," + preset.text
|
|
532
|
+
);
|
|
533
|
+
|
|
534
|
+
async function saveCurrentAsPreset() {
|
|
535
|
+
if (state.presetBusy) return;
|
|
536
|
+
const text = state.prompt.trim();
|
|
537
|
+
if (text === "") { set({ notice: "先写提示词,再存成预设" }); return; }
|
|
538
|
+
const label = (state.presetLabel.trim() || text.slice(0, 10)).trim();
|
|
539
|
+
set({ presetBusy: true });
|
|
540
|
+
try {
|
|
541
|
+
const res = await fetch("/agnes/presets", {
|
|
542
|
+
method: "POST",
|
|
543
|
+
headers: { "content-type": "application/json" },
|
|
544
|
+
body: JSON.stringify({ label, text }),
|
|
545
|
+
});
|
|
546
|
+
const data = await res.json().catch(() => null);
|
|
547
|
+
if (!res.ok || !data || data.ok !== true) {
|
|
548
|
+
throw new Error((data && data.error) || ("HTTP " + res.status));
|
|
549
|
+
}
|
|
550
|
+
set({
|
|
551
|
+
presets: Array.isArray(data.presets) ? data.presets : state.presets,
|
|
552
|
+
presetBusy: false, presetFormOpen: false, presetLabel: "",
|
|
553
|
+
notice: "已存为预设「" + label + "」",
|
|
554
|
+
});
|
|
555
|
+
} catch (e) {
|
|
556
|
+
set({ presetBusy: false, notice: "存预设失败:" + (e && e.message ? e.message : String(e)) });
|
|
557
|
+
}
|
|
558
|
+
}
|
|
559
|
+
|
|
560
|
+
async function deletePreset(label) {
|
|
561
|
+
if (state.presetBusy) return;
|
|
562
|
+
set({ presetBusy: true });
|
|
563
|
+
try {
|
|
564
|
+
const res = await fetch("/agnes/presets?label=" + encodeURIComponent(label), { method: "DELETE" });
|
|
565
|
+
const data = await res.json().catch(() => null);
|
|
566
|
+
if (!res.ok || !data || data.ok !== true) {
|
|
567
|
+
throw new Error((data && data.error) || ("HTTP " + res.status));
|
|
568
|
+
}
|
|
569
|
+
set({
|
|
570
|
+
presets: Array.isArray(data.presets) ? data.presets : [],
|
|
571
|
+
presetBusy: false,
|
|
572
|
+
notice: "已删除预设「" + label + "」",
|
|
573
|
+
});
|
|
574
|
+
} catch (e) {
|
|
575
|
+
set({ presetBusy: false, notice: "删除预设失败:" + (e && e.message ? e.message : String(e)) });
|
|
576
|
+
}
|
|
577
|
+
}
|
|
578
|
+
|
|
579
|
+
/** 让上游文本模型(agnes-2.5-flash)把这一句扩写成一段描述。 */
|
|
580
|
+
async function expandPrompt() {
|
|
581
|
+
if (state.expandBusy) return;
|
|
582
|
+
const text = state.prompt.trim();
|
|
583
|
+
if (text === "") { set({ notice: "先写一句想画什么,再来扩写" }); return; }
|
|
584
|
+
set({ expandBusy: true, notice: "" });
|
|
585
|
+
try {
|
|
586
|
+
const res = await fetch("/agnes/expand", {
|
|
587
|
+
method: "POST",
|
|
588
|
+
headers: { "content-type": "application/json" },
|
|
589
|
+
body: JSON.stringify({ prompt: text, kind: state.mode === "video" ? "video" : "image" }),
|
|
590
|
+
});
|
|
591
|
+
const data = await res.json().catch(() => null);
|
|
592
|
+
if (!res.ok || !data || data.ok !== true) {
|
|
593
|
+
throw new Error((data && data.error) || ("HTTP " + res.status));
|
|
594
|
+
}
|
|
595
|
+
const next = typeof data.prompt === "string" ? data.prompt.trim() : "";
|
|
596
|
+
if (next === "") throw new Error("上游没有返回内容");
|
|
597
|
+
set({ prompt: next, expandBusy: false, notice: "已扩写(" + (data.model || "agnes-2.5-flash") + ")" });
|
|
598
|
+
} catch (e) {
|
|
599
|
+
set({ expandBusy: false, notice: "扩写失败:" + (e && e.message ? e.message : String(e)) });
|
|
600
|
+
}
|
|
601
|
+
}
|
|
602
|
+
|
|
442
603
|
/* ---------- 历史(宿主持久化) ---------- */
|
|
443
604
|
async function loadHistory() {
|
|
444
605
|
try {
|
|
@@ -512,7 +673,12 @@ window.__ModuleLoader__.load({
|
|
|
512
673
|
cfg: data.config,
|
|
513
674
|
cfgDefaults: data.defaults,
|
|
514
675
|
cfgStored: data.stored,
|
|
515
|
-
cfgDraft: {
|
|
676
|
+
cfgDraft: {
|
|
677
|
+
keyFile: data.config.keyFile,
|
|
678
|
+
outDir: data.config.outDir,
|
|
679
|
+
imageModel: data.config.imageModel || "",
|
|
680
|
+
videoModel: data.config.videoModel || "",
|
|
681
|
+
},
|
|
516
682
|
cfgError: "",
|
|
517
683
|
});
|
|
518
684
|
} catch (e) {
|
|
@@ -530,6 +696,9 @@ window.__ModuleLoader__.load({
|
|
|
530
696
|
body: JSON.stringify({
|
|
531
697
|
keyFile: state.cfgDraft.keyFile,
|
|
532
698
|
outDir: state.cfgDraft.outDir,
|
|
699
|
+
// 空串 = 回退到内置默认模型
|
|
700
|
+
imageModel: state.cfgDraft.imageModel,
|
|
701
|
+
videoModel: state.cfgDraft.videoModel,
|
|
533
702
|
}),
|
|
534
703
|
});
|
|
535
704
|
if (res.status === 404) throw new Error("配置接口尚未就绪:需要完全重启应用(只重载窗口不够)");
|
|
@@ -539,7 +708,12 @@ window.__ModuleLoader__.load({
|
|
|
539
708
|
cfg: data.config,
|
|
540
709
|
cfgDefaults: data.defaults,
|
|
541
710
|
cfgStored: data.stored,
|
|
542
|
-
cfgDraft: {
|
|
711
|
+
cfgDraft: {
|
|
712
|
+
keyFile: data.config.keyFile,
|
|
713
|
+
outDir: data.config.outDir,
|
|
714
|
+
imageModel: data.config.imageModel || "",
|
|
715
|
+
videoModel: data.config.videoModel || "",
|
|
716
|
+
},
|
|
543
717
|
cfgNotice: "已保存",
|
|
544
718
|
});
|
|
545
719
|
// 路径变了,key 是否存在/目录是否可写都要重新算
|
|
@@ -553,6 +727,43 @@ window.__ModuleLoader__.load({
|
|
|
553
727
|
}
|
|
554
728
|
}
|
|
555
729
|
|
|
730
|
+
/** 测一下 Key 能不能连上游:顺便把上游真实的模型名单拉回来 */
|
|
731
|
+
async function checkKey() {
|
|
732
|
+
if (state.checking) return;
|
|
733
|
+
set({ checking: true, checkResult: null, cfgError: "" });
|
|
734
|
+
try {
|
|
735
|
+
const res = await fetch("/agnes/models");
|
|
736
|
+
const data = await res.json().catch(() => ({}));
|
|
737
|
+
if (res.status === 404) {
|
|
738
|
+
throw new Error("模型探测接口尚未就绪:需要完全重启应用(只重载窗口不够)");
|
|
739
|
+
}
|
|
740
|
+
if (!res.ok || !data || data.ok !== true) {
|
|
741
|
+
throw new Error((data && data.error) || ("探测失败:HTTP " + res.status));
|
|
742
|
+
}
|
|
743
|
+
const up = data.upstream || {};
|
|
744
|
+
const imgs = Array.isArray(up.image) ? up.image.length : 0;
|
|
745
|
+
const vids = Array.isArray(up.video) ? up.video.length : 0;
|
|
746
|
+
set({
|
|
747
|
+
checking: false,
|
|
748
|
+
// 把上游真实清单留下:顶部的模型下拉优先用它,比内置清单更可信
|
|
749
|
+
upModels: {
|
|
750
|
+
image: Array.isArray(up.image) ? up.image : [],
|
|
751
|
+
video: Array.isArray(up.video) ? up.video : [],
|
|
752
|
+
},
|
|
753
|
+
checkResult: {
|
|
754
|
+
ok: true,
|
|
755
|
+
text: "连接正常:上游可用 " + imgs + " 个图片模型、" + vids + " 个视频模型",
|
|
756
|
+
detail: ""
|
|
757
|
+
},
|
|
758
|
+
});
|
|
759
|
+
} catch (e) {
|
|
760
|
+
set({
|
|
761
|
+
checking: false,
|
|
762
|
+
checkResult: { ok: false, text: e && e.message ? e.message : String(e), detail: "" },
|
|
763
|
+
});
|
|
764
|
+
}
|
|
765
|
+
}
|
|
766
|
+
|
|
556
767
|
/* ---------- 图片:一次请求就出结果 ---------- */
|
|
557
768
|
async function runImage(token, prompt) {
|
|
558
769
|
const refs = state.refs;
|
|
@@ -690,6 +901,101 @@ window.__ModuleLoader__.load({
|
|
|
690
901
|
set({ refPickOpen: !state.refPickOpen, refsError: "" });
|
|
691
902
|
}
|
|
692
903
|
|
|
904
|
+
/* ---------- 视频参考图(reference 模式,≤5 张) ---------- */
|
|
905
|
+
const vRefsFull = () => state.vRefs.length >= MAX_VREFS;
|
|
906
|
+
|
|
907
|
+
function addVRefsFromFiles(files) {
|
|
908
|
+
const list = Array.from(files || []).filter(
|
|
909
|
+
(f) => f && String(f.type || "").startsWith("image/")
|
|
910
|
+
);
|
|
911
|
+
if (list.length === 0) {
|
|
912
|
+
set({ vRefsError: "没有可用的图片文件(需要 PNG / JPEG / WebP / GIF)" });
|
|
913
|
+
return Promise.resolve();
|
|
914
|
+
}
|
|
915
|
+
|
|
916
|
+
return queueRefOp(async () => {
|
|
917
|
+
set({ vRefsBusy: true, vRefsError: "" });
|
|
918
|
+
const added = [];
|
|
919
|
+
const failed = [];
|
|
920
|
+
try {
|
|
921
|
+
for (const file of list) {
|
|
922
|
+
if (state.vRefs.length + added.length >= MAX_VREFS) {
|
|
923
|
+
failed.push("超出 " + MAX_VREFS + " 张上限");
|
|
924
|
+
break;
|
|
925
|
+
}
|
|
926
|
+
try {
|
|
927
|
+
const r = await shrinkImage(file);
|
|
928
|
+
added.push({
|
|
929
|
+
id: nextRefId(),
|
|
930
|
+
name: file.name || "本地图片",
|
|
931
|
+
src: r.src,
|
|
932
|
+
bytes: r.bytes,
|
|
933
|
+
from: "local",
|
|
934
|
+
});
|
|
935
|
+
} catch (e) {
|
|
936
|
+
failed.push((file.name || "图片") + ":" + (e && e.message ? e.message : String(e)));
|
|
937
|
+
}
|
|
938
|
+
}
|
|
939
|
+
if (added.length > 0) {
|
|
940
|
+
update((prev) => ({ vRefs: prev.vRefs.concat(added) }));
|
|
941
|
+
}
|
|
942
|
+
set({
|
|
943
|
+
vRefsError: failed.length > 0 ? failed.join(";") : "",
|
|
944
|
+
vRefPickOpen: false,
|
|
945
|
+
});
|
|
946
|
+
} finally {
|
|
947
|
+
set({ vRefsBusy: false });
|
|
948
|
+
}
|
|
949
|
+
});
|
|
950
|
+
}
|
|
951
|
+
|
|
952
|
+
function addVRefFromResult(entry) {
|
|
953
|
+
if (!entry || entry.kind !== "image" || !entry.filename) return Promise.resolve();
|
|
954
|
+
if (vRefsFull()) {
|
|
955
|
+
set({ vRefsError: "参考图最多 " + MAX_VREFS + " 张" });
|
|
956
|
+
return Promise.resolve();
|
|
957
|
+
}
|
|
958
|
+
|
|
959
|
+
return queueRefOp(async () => {
|
|
960
|
+
set({ vRefsBusy: true, vRefsError: "" });
|
|
961
|
+
try {
|
|
962
|
+
const res = await fetch(srcOf(entry.filename));
|
|
963
|
+
if (!res.ok) throw new Error("读取生成结果失败:HTTP " + res.status);
|
|
964
|
+
const blob = await res.blob();
|
|
965
|
+
const r = await shrinkImage(blob);
|
|
966
|
+
update((prev) => ({
|
|
967
|
+
vRefs: prev.vRefs.concat([{
|
|
968
|
+
id: nextRefId(),
|
|
969
|
+
name: entry.filename,
|
|
970
|
+
src: r.src,
|
|
971
|
+
bytes: r.bytes,
|
|
972
|
+
from: "result",
|
|
973
|
+
}]),
|
|
974
|
+
vRefPickOpen: false,
|
|
975
|
+
}));
|
|
976
|
+
} catch (e) {
|
|
977
|
+
set({ vRefsError: e && e.message ? e.message : String(e) });
|
|
978
|
+
} finally {
|
|
979
|
+
set({ vRefsBusy: false });
|
|
980
|
+
}
|
|
981
|
+
});
|
|
982
|
+
}
|
|
983
|
+
|
|
984
|
+
function removeVRef(id) {
|
|
985
|
+
if (state.busy) return;
|
|
986
|
+
update((prev) => ({ vRefs: prev.vRefs.filter((r) => r.id !== id), vRefsError: "" }));
|
|
987
|
+
}
|
|
988
|
+
|
|
989
|
+
function clearVRefs() {
|
|
990
|
+
if (state.busy) return;
|
|
991
|
+
set({ vRefs: [], vRefsError: "", vRefPickOpen: false });
|
|
992
|
+
}
|
|
993
|
+
|
|
994
|
+
function toggleVRefPick() {
|
|
995
|
+
if (state.busy) return;
|
|
996
|
+
set({ vRefPickOpen: !state.vRefPickOpen, vRefsError: "" });
|
|
997
|
+
}
|
|
998
|
+
|
|
693
999
|
/* ---------- 视频关键帧(首帧 / 尾帧,本地图直接当帧) ---------- */
|
|
694
1000
|
const frameField = (which) => (which === "last" ? "vLast" : "vFirst");
|
|
695
1001
|
|
|
@@ -797,14 +1103,17 @@ window.__ModuleLoader__.load({
|
|
|
797
1103
|
set({ notice: "已开始下载" });
|
|
798
1104
|
}
|
|
799
1105
|
|
|
800
|
-
/**
|
|
1106
|
+
/** 停掉等待:让在途的轮询/请求结果失效(宿主的任务照常跑,文件会正常落盘) */
|
|
801
1107
|
function cancel() {
|
|
802
1108
|
if (!state.busy) return;
|
|
1109
|
+
const hadTask = state.vTaskId !== null;
|
|
803
1110
|
runToken += 1;
|
|
804
1111
|
stopTimer();
|
|
805
1112
|
set({
|
|
806
1113
|
busy: false, vStatus: "", vProgress: 0,
|
|
807
|
-
notice:
|
|
1114
|
+
notice: hadTask
|
|
1115
|
+
? "已停止跟踪;宿主会继续推进,刷新面板还能接回进度,文件也会照常落盘"
|
|
1116
|
+
: "已停止等待;服务端任务可能仍在跑",
|
|
808
1117
|
});
|
|
809
1118
|
}
|
|
810
1119
|
|
|
@@ -850,6 +1159,23 @@ window.__ModuleLoader__.load({
|
|
|
850
1159
|
}
|
|
851
1160
|
}
|
|
852
1161
|
|
|
1162
|
+
/**
|
|
1163
|
+
* 静默刷新输出目录清单:生成完成后新文件已经落盘,列表要跟着更新。
|
|
1164
|
+
* 只在清单已经加载过时才拉(没展开过就别偷偷把面板撑开),失败保留旧清单。
|
|
1165
|
+
*/
|
|
1166
|
+
async function refreshLibrary() {
|
|
1167
|
+
if (state.library === null || state.libraryBusy) return;
|
|
1168
|
+
try {
|
|
1169
|
+
const res = await fetch("/agnes/library");
|
|
1170
|
+
const data = await res.json().catch(() => ({}));
|
|
1171
|
+
if (!res.ok || !data || data.ok !== true) return;
|
|
1172
|
+
if (!Array.isArray(data.files)) return;
|
|
1173
|
+
update(() => ({ library: data.files, libraryError: "" }));
|
|
1174
|
+
} catch {
|
|
1175
|
+
// 刷新失败保留旧清单,不打扰用户
|
|
1176
|
+
}
|
|
1177
|
+
}
|
|
1178
|
+
|
|
853
1179
|
function toggleLibrary() {
|
|
854
1180
|
if (state.libraryOpen) {
|
|
855
1181
|
set({ libraryOpen: false });
|
|
@@ -882,59 +1208,32 @@ window.__ModuleLoader__.load({
|
|
|
882
1208
|
});
|
|
883
1209
|
}
|
|
884
1210
|
|
|
885
|
-
/* ----------
|
|
886
|
-
|
|
887
|
-
|
|
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,本地图片不用先传图床
|
|
896
|
-
const subRes = await fetch("/agnes/video", {
|
|
897
|
-
method: "POST",
|
|
898
|
-
headers: { "content-type": "application/json" },
|
|
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
|
-
}),
|
|
907
|
-
});
|
|
908
|
-
const sub = await subRes.json();
|
|
909
|
-
if (!sub.ok) throw new Error(sub.error || "提交失败");
|
|
910
|
-
if (token !== runToken) return;
|
|
911
|
-
|
|
912
|
-
const usedMode = sub.mode === "keyframe" ? "keyframe" : "text";
|
|
913
|
-
set({ videoId: sub.videoId, vStatus: "排队中…" });
|
|
1211
|
+
/* ---------- 视频:交给宿主任务队列,面板只负责看进度 ---------- */
|
|
1212
|
+
// 提交后 video_id 由宿主保管:关面板、刷新页面、重启应用都不影响任务继续推进。
|
|
1213
|
+
async function watchTask(token, taskId, fallback) {
|
|
914
1214
|
const deadline = Date.now() + VIDEO_DEADLINE_MS;
|
|
915
|
-
|
|
916
1215
|
let transientFails = 0;
|
|
1216
|
+
|
|
917
1217
|
for (;;) {
|
|
918
1218
|
await sleep(POLL_MS);
|
|
919
1219
|
if (token !== runToken) return;
|
|
920
1220
|
|
|
921
1221
|
let st;
|
|
922
1222
|
try {
|
|
923
|
-
const r = await fetch("/agnes/
|
|
924
|
-
st = await r.json();
|
|
1223
|
+
const r = await fetch("/agnes/task?id=" + encodeURIComponent(taskId));
|
|
1224
|
+
st = r.ok ? await r.json() : { ok: false, error: "查询任务失败:HTTP " + r.status };
|
|
925
1225
|
} catch (e) {
|
|
926
1226
|
st = { ok: false, error: "网络错误:" + (e && e.message ? e.message : String(e)) };
|
|
927
1227
|
}
|
|
928
1228
|
|
|
929
1229
|
if (!st.ok) {
|
|
930
1230
|
const msg = String(st.error || "查询任务失败");
|
|
931
|
-
//
|
|
932
|
-
// 实测提交后立刻查询就会 429,这条路径是必需的。
|
|
1231
|
+
// 宿主自己会重试上游;这里只在本地接口抖动时容忍几次
|
|
933
1232
|
if (/\b429\b|\b503\b|频繁|队列已满|网络错误|timeout/i.test(msg)) {
|
|
934
1233
|
transientFails += 1;
|
|
935
1234
|
if (transientFails > 8) throw new Error(msg);
|
|
936
|
-
set({ vStatus: "
|
|
937
|
-
await sleep(Math.min(30000,
|
|
1235
|
+
set({ vStatus: "重试中…" });
|
|
1236
|
+
await sleep(Math.min(30000, 3000 * transientFails));
|
|
938
1237
|
continue;
|
|
939
1238
|
}
|
|
940
1239
|
throw new Error(msg);
|
|
@@ -943,33 +1242,126 @@ window.__ModuleLoader__.load({
|
|
|
943
1242
|
transientFails = 0;
|
|
944
1243
|
if (token !== runToken) return;
|
|
945
1244
|
|
|
1245
|
+
const task = st.task || {};
|
|
946
1246
|
set({
|
|
947
|
-
vStatus:
|
|
948
|
-
vProgress: typeof
|
|
1247
|
+
vStatus: task.status ? String(task.status) : "",
|
|
1248
|
+
vProgress: typeof task.progress === "number" ? task.progress : 0,
|
|
1249
|
+
videoId: task.videoId || fallback.videoId || null,
|
|
949
1250
|
});
|
|
950
1251
|
|
|
951
|
-
if (
|
|
1252
|
+
if (task.status === "completed") {
|
|
952
1253
|
pushResult({
|
|
953
1254
|
kind: "video",
|
|
954
|
-
filename:
|
|
955
|
-
path:
|
|
956
|
-
bytes:
|
|
1255
|
+
filename: task.filename,
|
|
1256
|
+
path: task.path,
|
|
1257
|
+
bytes: task.bytes,
|
|
957
1258
|
size: "720P",
|
|
958
|
-
ratio:
|
|
959
|
-
seconds:
|
|
960
|
-
transport: "
|
|
961
|
-
mode:
|
|
962
|
-
seed:
|
|
963
|
-
prompt,
|
|
1259
|
+
ratio: task.ratio || fallback.ratio,
|
|
1260
|
+
seconds: task.seconds || fallback.seconds,
|
|
1261
|
+
transport: "宿主任务",
|
|
1262
|
+
mode: modeOf(task.mode || fallback.mode),
|
|
1263
|
+
seed: task.seed === null || task.seed === undefined ? undefined : task.seed,
|
|
1264
|
+
prompt: task.prompt || fallback.prompt,
|
|
964
1265
|
wallSec: ((Date.now() - state.startedAt) / 1000).toFixed(1),
|
|
965
1266
|
});
|
|
966
1267
|
return;
|
|
967
1268
|
}
|
|
968
|
-
if (
|
|
969
|
-
|
|
970
|
-
|
|
1269
|
+
if (task.status === "failed") throw new Error(task.error || "视频生成失败");
|
|
1270
|
+
if (task.status === "cancelled") throw new Error("任务已取消");
|
|
1271
|
+
if (Date.now() > deadline) {
|
|
1272
|
+
throw new Error("等待超时(15 分钟);宿主仍在推进,稍后刷新还能看到结果");
|
|
1273
|
+
}
|
|
1274
|
+
}
|
|
1275
|
+
}
|
|
1276
|
+
|
|
1277
|
+
async function runVideo(token, prompt) {
|
|
1278
|
+
set({ vStatus: "提交中…", vProgress: 0, videoId: null, vTaskId: null });
|
|
1279
|
+
|
|
1280
|
+
const seedText = String(state.vSeed).trim();
|
|
1281
|
+
const seedValue = seedText === "" ? null : Number(seedText);
|
|
1282
|
+
if (seedValue !== null && !(Number.isInteger(seedValue) && seedValue >= 0)) {
|
|
1283
|
+
throw new Error("seed 需要非负整数(也可以留空)");
|
|
1284
|
+
}
|
|
1285
|
+
|
|
1286
|
+
// 关键帧直接发 data URI:上游已实测接受 base64,本地图片不用先传图床
|
|
1287
|
+
const subRes = await fetch("/agnes/tasks", {
|
|
1288
|
+
method: "POST",
|
|
1289
|
+
headers: { "content-type": "application/json" },
|
|
1290
|
+
body: JSON.stringify({
|
|
1291
|
+
prompt,
|
|
1292
|
+
seconds: state.vSeconds,
|
|
1293
|
+
ratio: state.vRatio,
|
|
1294
|
+
seed: seedValue,
|
|
1295
|
+
firstFrame: state.vFirst !== null ? state.vFirst.src : null,
|
|
1296
|
+
lastFrame: state.vLast !== null ? state.vLast.src : null,
|
|
1297
|
+
// 参考图(reference 模式):有首/尾帧时上游会用 keyframe,参考图自动让位
|
|
1298
|
+
referenceImages: state.vRefs.map((r) => r.src),
|
|
1299
|
+
}),
|
|
1300
|
+
});
|
|
1301
|
+
const sub = await subRes.json();
|
|
1302
|
+
if (!sub.ok) throw new Error(sub.error || "提交失败");
|
|
1303
|
+
if (token !== runToken) return;
|
|
1304
|
+
|
|
1305
|
+
const task = sub.task || {};
|
|
1306
|
+
if (!task.id) throw new Error("宿主没有返回任务号");
|
|
1307
|
+
set({ videoId: task.videoId || null, vTaskId: task.id, vStatus: "排队中…" });
|
|
1308
|
+
|
|
1309
|
+
await watchTask(token, task.id, {
|
|
1310
|
+
prompt,
|
|
1311
|
+
ratio: state.vRatio,
|
|
1312
|
+
seconds: state.vSeconds,
|
|
1313
|
+
mode: task.mode,
|
|
1314
|
+
videoId: task.videoId,
|
|
1315
|
+
});
|
|
1316
|
+
}
|
|
1317
|
+
|
|
1318
|
+
/* 面板重新挂载时把宿主上还在跑的任务接回来:刷新页面不会让进度消失 */
|
|
1319
|
+
async function resumeTasks() {
|
|
1320
|
+
if (state.busy) return;
|
|
1321
|
+
try {
|
|
1322
|
+
const r = await fetch("/agnes/tasks");
|
|
1323
|
+
if (!r.ok) return;
|
|
1324
|
+
const data = await r.json();
|
|
1325
|
+
if (!data || data.ok !== true || !Array.isArray(data.tasks)) return;
|
|
1326
|
+
const active = data.tasks.find((t) => t.status === "queued" || t.status === "running");
|
|
1327
|
+
if (!active || state.busy) return;
|
|
1328
|
+
|
|
1329
|
+
const token = ++runToken;
|
|
1330
|
+
const created = Date.parse(active.createdAt);
|
|
1331
|
+
set({
|
|
1332
|
+
mode: "video",
|
|
1333
|
+
busy: true,
|
|
1334
|
+
error: "",
|
|
1335
|
+
notice: "",
|
|
1336
|
+
startedAt: Number.isFinite(created) ? created : Date.now(),
|
|
1337
|
+
elapsed: 0,
|
|
1338
|
+
vFirst: null,
|
|
1339
|
+
vLast: null,
|
|
1340
|
+
videoId: active.videoId || null,
|
|
1341
|
+
vTaskId: active.id,
|
|
1342
|
+
vStatus: active.status,
|
|
1343
|
+
vProgress: typeof active.progress === "number" ? active.progress : 0,
|
|
1344
|
+
prompt: state.prompt.trim() === "" ? String(active.prompt || "") : state.prompt,
|
|
1345
|
+
});
|
|
1346
|
+
startTimer();
|
|
1347
|
+
try {
|
|
1348
|
+
await watchTask(token, active.id, {
|
|
1349
|
+
prompt: active.prompt,
|
|
1350
|
+
ratio: active.ratio || state.vRatio,
|
|
1351
|
+
seconds: active.seconds || state.vSeconds,
|
|
1352
|
+
mode: active.mode,
|
|
1353
|
+
videoId: active.videoId,
|
|
1354
|
+
});
|
|
1355
|
+
} catch (e) {
|
|
1356
|
+
if (token === runToken) set({ error: e && e.message ? e.message : String(e) });
|
|
1357
|
+
} finally {
|
|
1358
|
+
if (token === runToken) {
|
|
1359
|
+
stopTimer();
|
|
1360
|
+
set({ busy: false, elapsed: (Date.now() - state.startedAt) / 1000 });
|
|
1361
|
+
}
|
|
971
1362
|
}
|
|
972
|
-
|
|
1363
|
+
} catch {
|
|
1364
|
+
// 旧宿主没有 /agnes/tasks:静默跳过,不打扰用户
|
|
973
1365
|
}
|
|
974
1366
|
}
|
|
975
1367
|
|
|
@@ -1014,7 +1406,19 @@ window.__ModuleLoader__.load({
|
|
|
1014
1406
|
setCfgDraft(field, value) {
|
|
1015
1407
|
set({ cfgDraft: { ...state.cfgDraft, [field]: value }, cfgNotice: "" });
|
|
1016
1408
|
},
|
|
1409
|
+
|
|
1410
|
+
/* 顶部标题栏直接换模型:先写进草稿,再立刻落盘(不然刷新就回退了) */
|
|
1411
|
+
async setModel(kind, value) {
|
|
1412
|
+
if (state.busy || state.cfgSaving) return;
|
|
1413
|
+
const field = kind === "video" ? "videoModel" : "imageModel";
|
|
1414
|
+
set({ cfgDraft: { ...state.cfgDraft, [field]: value }, cfgNotice: "", cfgError: "" });
|
|
1415
|
+
await saveConfig();
|
|
1416
|
+
if (state.cfgError === "") {
|
|
1417
|
+
set({ notice: "已切换模型:" + (value === "" ? "内置默认" : value) });
|
|
1418
|
+
}
|
|
1419
|
+
},
|
|
1017
1420
|
toggleCfg() { set({ cfgOpen: !state.cfgOpen }); },
|
|
1421
|
+
checkKey,
|
|
1018
1422
|
run,
|
|
1019
1423
|
/* ---- 参考图动作 ---- */
|
|
1020
1424
|
addRefsFromFiles,
|
|
@@ -1022,6 +1426,12 @@ window.__ModuleLoader__.load({
|
|
|
1022
1426
|
removeRef,
|
|
1023
1427
|
clearRefs,
|
|
1024
1428
|
toggleRefPick,
|
|
1429
|
+
/* ---- 视频参考图(reference 模式,≤5 张) ---- */
|
|
1430
|
+
addVRefsFromFiles,
|
|
1431
|
+
addVRefFromResult,
|
|
1432
|
+
removeVRef,
|
|
1433
|
+
clearVRefs,
|
|
1434
|
+
toggleVRefPick,
|
|
1025
1435
|
/* ---- 视频关键帧 ---- */
|
|
1026
1436
|
pickFrame,
|
|
1027
1437
|
setFrameFromFile,
|
|
@@ -1029,18 +1439,30 @@ window.__ModuleLoader__.load({
|
|
|
1029
1439
|
toggleFramePick,
|
|
1030
1440
|
setFrameFromResult,
|
|
1031
1441
|
useResultAsFrame,
|
|
1442
|
+
/* ---- 视频任务(宿主侧推进,刷新可接回) ---- */
|
|
1443
|
+
resumeTasks,
|
|
1032
1444
|
/* ---- 结果收尾 ---- */
|
|
1033
1445
|
copyPath,
|
|
1034
1446
|
reveal,
|
|
1035
1447
|
downloadResult,
|
|
1036
1448
|
cancel,
|
|
1037
1449
|
copyPrompt,
|
|
1450
|
+
/* ---- 自定义预设 / 扩写 ---- */
|
|
1451
|
+
loadPresets,
|
|
1452
|
+
saveCurrentAsPreset,
|
|
1453
|
+
deletePreset,
|
|
1454
|
+
expandPrompt,
|
|
1455
|
+
/** 追加内置或自定义预设到提示词末尾 */
|
|
1456
|
+
appendPreset(preset) { set({ prompt: presetText(preset) }); },
|
|
1457
|
+
setPresetLabel(value) { set({ presetLabel: value }); },
|
|
1458
|
+
togglePresetForm() { set({ presetFormOpen: !state.presetFormOpen, presetLabel: "" }); },
|
|
1038
1459
|
/* ---- 输出目录清单 ---- */
|
|
1039
1460
|
loadLibrary,
|
|
1040
1461
|
toggleLibrary,
|
|
1041
1462
|
setLibraryTab,
|
|
1042
1463
|
showLibraryFile,
|
|
1043
1464
|
maxRefs: MAX_REFS,
|
|
1465
|
+
maxVRefs: MAX_VREFS,
|
|
1044
1466
|
/** 切换主预览到历史里的某一张 */
|
|
1045
1467
|
show(entry) { set({ result: entry }); },
|
|
1046
1468
|
/**
|
|
@@ -1172,7 +1594,14 @@ window.__ModuleLoader__.load({
|
|
|
1172
1594
|
return () => { off(); };
|
|
1173
1595
|
}, []);
|
|
1174
1596
|
|
|
1175
|
-
React.useEffect(() => {
|
|
1597
|
+
React.useEffect(() => {
|
|
1598
|
+
store.loadHealth();
|
|
1599
|
+
store.loadConfig();
|
|
1600
|
+
store.loadHistory();
|
|
1601
|
+
store.loadPresets();
|
|
1602
|
+
// 上次没跑完的视频任务:宿主还在推进,接回进度
|
|
1603
|
+
store.resumeTasks();
|
|
1604
|
+
}, []);
|
|
1176
1605
|
|
|
1177
1606
|
const busy = s.busy;
|
|
1178
1607
|
const isVideo = s.mode === "video";
|
|
@@ -1181,13 +1610,16 @@ window.__ModuleLoader__.load({
|
|
|
1181
1610
|
const shown = s.result;
|
|
1182
1611
|
const hasRefs = s.refs.length > 0;
|
|
1183
1612
|
const refsFull = s.refs.length >= MAX_REFS;
|
|
1613
|
+
const vRefsFull = s.vRefs.length >= MAX_VREFS;
|
|
1184
1614
|
const imageHistory = s.history.filter((h) => h.kind === "image");
|
|
1615
|
+
const pickableImages = pickableImagesOf(s);
|
|
1185
1616
|
const libraryAll = s.library || [];
|
|
1186
1617
|
const libraryImages = libraryAll.filter((f) => f.kind !== "video");
|
|
1187
1618
|
const libraryVideos = libraryAll.filter((f) => f.kind === "video");
|
|
1188
1619
|
const libraryShown = s.libraryTab === "video" ? libraryVideos : libraryImages;
|
|
1189
1620
|
const fileInput = React.useRef(null);
|
|
1190
1621
|
const frameInput = React.useRef(null);
|
|
1622
|
+
const vRefInput = React.useRef(null);
|
|
1191
1623
|
|
|
1192
1624
|
const onKeyDown = (e) => {
|
|
1193
1625
|
if ((e.metaKey || e.ctrlKey) && e.key === "Enter") { e.preventDefault(); store.run(); }
|
|
@@ -1279,15 +1711,15 @@ window.__ModuleLoader__.load({
|
|
|
1279
1711
|
}, "+ 本地"),
|
|
1280
1712
|
React.createElement("button", {
|
|
1281
1713
|
type: "button", className: "agn-frame-from",
|
|
1282
|
-
disabled: busy || s.frameBusy ||
|
|
1283
|
-
title:
|
|
1714
|
+
disabled: busy || s.frameBusy || pickableImages.length === 0,
|
|
1715
|
+
title: pickableImages.length === 0
|
|
1284
1716
|
? "还没有生成过图片,先出一张图再来选"
|
|
1285
1717
|
: "从生成结果里挑一张当" + label,
|
|
1286
1718
|
onClick: () => store.toggleFramePick(which),
|
|
1287
1719
|
}, s.framePickFrom === which ? "收起" : "+ 结果"),
|
|
1288
1720
|
s.framePickFrom === which
|
|
1289
1721
|
? React.createElement("div", { className: "agn-ref-pick agn-frame-pick" },
|
|
1290
|
-
|
|
1722
|
+
pickableImages.map((h) => React.createElement("button", {
|
|
1291
1723
|
key: h.filename, type: "button", title: h.filename,
|
|
1292
1724
|
disabled: busy || s.frameBusy,
|
|
1293
1725
|
onClick: () => store.setFrameFromResult(which, h),
|
|
@@ -1331,12 +1763,49 @@ window.__ModuleLoader__.load({
|
|
|
1331
1763
|
},
|
|
1332
1764
|
}),
|
|
1333
1765
|
|
|
1766
|
+
/* 隐藏的视频参考图选择器(reference 模式,最多 5 张) */
|
|
1767
|
+
React.createElement("input", {
|
|
1768
|
+
ref: vRefInput,
|
|
1769
|
+
type: "file",
|
|
1770
|
+
accept: "image/png,image/jpeg,image/webp,image/gif",
|
|
1771
|
+
multiple: true,
|
|
1772
|
+
style: { display: "none" },
|
|
1773
|
+
onChange: (e) => {
|
|
1774
|
+
const el = e.target;
|
|
1775
|
+
const files = el.files;
|
|
1776
|
+
Promise.resolve(store.addVRefsFromFiles(files))
|
|
1777
|
+
.catch(() => {})
|
|
1778
|
+
.finally(() => { try { el.value = ""; } catch { /* input 可能已卸载 */ } });
|
|
1779
|
+
},
|
|
1780
|
+
}),
|
|
1781
|
+
|
|
1334
1782
|
React.createElement("div", { className: "agn-head" },
|
|
1335
1783
|
React.createElement("span", { className: "agn-title" }, "Agnes 创作"),
|
|
1336
|
-
|
|
1337
|
-
isVideo
|
|
1338
|
-
|
|
1339
|
-
|
|
1784
|
+
(() => {
|
|
1785
|
+
const kind = isVideo ? "video" : "image";
|
|
1786
|
+
const cur = isVideo
|
|
1787
|
+
? (s.cfgDraft.videoModel || (s.health && s.health.videoModel) || "agnes-video-2.5-flash")
|
|
1788
|
+
: (s.cfgDraft.imageModel || (s.health && s.health.model) || "agnes-image-2.5-flash");
|
|
1789
|
+
/* 选项优先用连接测试拿到的上游真实清单,没有就退回内置清单 */
|
|
1790
|
+
const fromUp = s.upModels && Array.isArray(s.upModels[kind]) ? s.upModels[kind] : null;
|
|
1791
|
+
const fromHealth = s.health && s.health.models && Array.isArray(s.health.models[kind])
|
|
1792
|
+
? s.health.models[kind]
|
|
1793
|
+
: [];
|
|
1794
|
+
let list = fromUp !== null && fromUp.length > 0 ? fromUp : fromHealth;
|
|
1795
|
+
if (list.length < 2) {
|
|
1796
|
+
/* 只有一个模型时给下拉没有意义,直接显示名字 */
|
|
1797
|
+
return React.createElement("span", { className: "agn-meta" }, cur);
|
|
1798
|
+
}
|
|
1799
|
+
list = list.slice();
|
|
1800
|
+
if (list.indexOf(cur) === -1) list.unshift(cur);
|
|
1801
|
+
return React.createElement("select", {
|
|
1802
|
+
className: "agn-model",
|
|
1803
|
+
value: cur,
|
|
1804
|
+
disabled: busy || s.cfgSaving,
|
|
1805
|
+
title: "切换" + (isVideo ? "视频" : "图片") + "模型(立刻保存到配置)",
|
|
1806
|
+
onChange: (e) => store.setModel(kind, e.target.value),
|
|
1807
|
+
}, list.map((m) => React.createElement("option", { key: m, value: m }, m)));
|
|
1808
|
+
})()
|
|
1340
1809
|
),
|
|
1341
1810
|
|
|
1342
1811
|
React.createElement("div", { className: "agn-card" },
|
|
@@ -1402,8 +1871,8 @@ window.__ModuleLoader__.load({
|
|
|
1402
1871
|
}, "+ 本地图片"),
|
|
1403
1872
|
React.createElement("button", {
|
|
1404
1873
|
type: "button", className: "agn-ref-add",
|
|
1405
|
-
disabled: busy || refsFull ||
|
|
1406
|
-
title:
|
|
1874
|
+
disabled: busy || refsFull || pickableImages.length === 0,
|
|
1875
|
+
title: pickableImages.length === 0 ? "还没有生成过图片" : "从「生成结果」里挑一张",
|
|
1407
1876
|
onClick: () => store.toggleRefPick(),
|
|
1408
1877
|
}, "+ 生成结果")
|
|
1409
1878
|
),
|
|
@@ -1411,9 +1880,9 @@ window.__ModuleLoader__.load({
|
|
|
1411
1880
|
/* 从生成结果挑图:只列图片(视频不能当参考图) */
|
|
1412
1881
|
s.refPickOpen
|
|
1413
1882
|
? React.createElement("div", { className: "agn-ref-pick" },
|
|
1414
|
-
|
|
1883
|
+
pickableImages.length === 0
|
|
1415
1884
|
? React.createElement("span", { className: "agn-hint" }, "还没有生成过图片")
|
|
1416
|
-
:
|
|
1885
|
+
: pickableImages.map((h) => React.createElement("button", {
|
|
1417
1886
|
key: "pick-" + h.filename,
|
|
1418
1887
|
type: "button",
|
|
1419
1888
|
title: h.filename,
|
|
@@ -1433,21 +1902,140 @@ window.__ModuleLoader__.load({
|
|
|
1433
1902
|
)
|
|
1434
1903
|
: null,
|
|
1435
1904
|
|
|
1905
|
+
/* ---- 视频参考图(reference 模式,≤5 张):给风格 / 主体参考,不是首帧 ---- */
|
|
1906
|
+
isVideo
|
|
1907
|
+
? React.createElement("div", { className: "agn-refs" },
|
|
1908
|
+
React.createElement("div", { className: "agn-ref-head" },
|
|
1909
|
+
React.createElement("span", { className: "agn-label" }, "参考图"),
|
|
1910
|
+
React.createElement("span", { className: "agn-tag" },
|
|
1911
|
+
s.vRefs.length + " / " + MAX_VREFS),
|
|
1912
|
+
s.vRefsBusy ? React.createElement("span", { className: "agn-hint" }, "处理中…") : null,
|
|
1913
|
+
s.vRefs.length > 0
|
|
1914
|
+
? React.createElement("button", {
|
|
1915
|
+
type: "button", className: "agn-btn-ghost", disabled: busy,
|
|
1916
|
+
onClick: () => store.clearVRefs(),
|
|
1917
|
+
}, "清空参考图")
|
|
1918
|
+
: null
|
|
1919
|
+
),
|
|
1920
|
+
React.createElement("div", { className: "agn-ref-strip" },
|
|
1921
|
+
s.vRefs.map((r) => React.createElement("div", {
|
|
1922
|
+
key: r.id,
|
|
1923
|
+
className: "agn-ref",
|
|
1924
|
+
title: r.name + " · " + (r.bytes / 1024).toFixed(0) + " KB · " +
|
|
1925
|
+
(r.from === "result" ? "来自生成结果" : "来自本地"),
|
|
1926
|
+
},
|
|
1927
|
+
React.createElement("img", { src: r.src, alt: r.name }),
|
|
1928
|
+
React.createElement("button", {
|
|
1929
|
+
type: "button", className: "agn-ref-del",
|
|
1930
|
+
title: "移除这张参考图",
|
|
1931
|
+
disabled: busy,
|
|
1932
|
+
onClick: () => store.removeVRef(r.id),
|
|
1933
|
+
}, "×")
|
|
1934
|
+
)),
|
|
1935
|
+
React.createElement("button", {
|
|
1936
|
+
type: "button", className: "agn-ref-add",
|
|
1937
|
+
disabled: busy || vRefsFull,
|
|
1938
|
+
title: "从本地磁盘选择一个或多个图片(PNG / JPEG / WebP / GIF)",
|
|
1939
|
+
onClick: () => {
|
|
1940
|
+
const el = vRefInput.current;
|
|
1941
|
+
if (el !== null) el.click();
|
|
1942
|
+
},
|
|
1943
|
+
}, "+ 本地"),
|
|
1944
|
+
React.createElement("button", {
|
|
1945
|
+
type: "button", className: "agn-ref-add",
|
|
1946
|
+
disabled: busy || vRefsFull || pickableImages.length === 0,
|
|
1947
|
+
title: pickableImages.length === 0 ? "还没有生成过图片" : "从「生成结果」里挑一张",
|
|
1948
|
+
onClick: () => store.toggleVRefPick(),
|
|
1949
|
+
}, "+ 结果")
|
|
1950
|
+
),
|
|
1951
|
+
s.vRefPickOpen
|
|
1952
|
+
? React.createElement("div", { className: "agn-ref-pick" },
|
|
1953
|
+
pickableImages.length === 0
|
|
1954
|
+
? React.createElement("span", { className: "agn-hint" }, "还没有生成过图片")
|
|
1955
|
+
: pickableImages.map((h) => React.createElement("button", {
|
|
1956
|
+
key: "vpick-" + h.filename,
|
|
1957
|
+
type: "button",
|
|
1958
|
+
title: h.filename,
|
|
1959
|
+
onClick: () => store.addVRefFromResult(h),
|
|
1960
|
+
}, React.createElement("img", {
|
|
1961
|
+
src: srcOf(h.filename), alt: h.filename, loading: "lazy",
|
|
1962
|
+
})))
|
|
1963
|
+
)
|
|
1964
|
+
: null,
|
|
1965
|
+
React.createElement("div", { className: "agn-note" },
|
|
1966
|
+
s.vRefs.length > 0
|
|
1967
|
+
? (s.vFirst !== null || s.vLast !== null
|
|
1968
|
+
? "已给关键帧,本次按首尾帧生成,参考图不生效"
|
|
1969
|
+
: "将按参考图生成(" + s.vRefs.length + " 张)——参考风格 / 主体 / 构图")
|
|
1970
|
+
: "可选:给上游 1–5 张参考图;留空 = 纯文本生视频"),
|
|
1971
|
+
s.vRefsError !== "" ? React.createElement("div", { className: "agn-err" }, s.vRefsError) : null
|
|
1972
|
+
)
|
|
1973
|
+
: null,
|
|
1974
|
+
|
|
1436
1975
|
React.createElement("div", { className: "agn-label" }, "提示词"),
|
|
1437
1976
|
React.createElement("div", { className: "agn-presets" },
|
|
1438
1977
|
PROMPT_PRESETS.map((preset) => React.createElement("button", {
|
|
1439
|
-
key: preset.label,
|
|
1978
|
+
key: "builtin-" + preset.label,
|
|
1440
1979
|
type: "button",
|
|
1441
1980
|
className: "agn-preset",
|
|
1442
1981
|
disabled: busy,
|
|
1443
1982
|
title: "追加到提示词末尾:" + preset.text,
|
|
1444
|
-
onClick: () => store.
|
|
1445
|
-
|
|
1446
|
-
|
|
1447
|
-
|
|
1448
|
-
|
|
1449
|
-
},
|
|
1983
|
+
onClick: () => store.appendPreset(preset),
|
|
1984
|
+
}, preset.label)),
|
|
1985
|
+
s.presets.map((preset) => React.createElement("span", {
|
|
1986
|
+
key: "mine-" + preset.label,
|
|
1987
|
+
className: "agn-preset-user",
|
|
1988
|
+
},
|
|
1989
|
+
React.createElement("button", {
|
|
1990
|
+
type: "button",
|
|
1991
|
+
className: "agn-preset agn-preset-mine",
|
|
1992
|
+
disabled: busy,
|
|
1993
|
+
title: "我的预设,点一下追加:" + preset.text,
|
|
1994
|
+
onClick: () => store.appendPreset(preset),
|
|
1995
|
+
}, preset.label),
|
|
1996
|
+
React.createElement("button", {
|
|
1997
|
+
type: "button",
|
|
1998
|
+
className: "agn-preset-x",
|
|
1999
|
+
disabled: busy || s.presetBusy,
|
|
2000
|
+
title: "删除这个预设",
|
|
2001
|
+
onClick: () => store.deletePreset(preset.label),
|
|
2002
|
+
}, "×")
|
|
2003
|
+
)),
|
|
2004
|
+
React.createElement("button", {
|
|
2005
|
+
type: "button",
|
|
2006
|
+
className: "agn-preset agn-preset-add",
|
|
2007
|
+
disabled: busy || s.prompt.trim() === "" || s.presetBusy,
|
|
2008
|
+
title: "把当前提示词存成可复用的预设",
|
|
2009
|
+
onClick: () => store.togglePresetForm(),
|
|
2010
|
+
}, s.presetFormOpen ? "取消保存" : "+ 存为预设"),
|
|
2011
|
+
React.createElement("button", {
|
|
2012
|
+
type: "button",
|
|
2013
|
+
className: "agn-preset agn-preset-expand",
|
|
2014
|
+
disabled: busy || s.expandBusy || s.prompt.trim() === "",
|
|
2015
|
+
title: "用 agnes-2.5-flash 把这一句扩写成一段具体描述",
|
|
2016
|
+
onClick: () => store.expandPrompt(),
|
|
2017
|
+
}, s.expandBusy ? "扩写中…" : "扩写")
|
|
1450
2018
|
),
|
|
2019
|
+
s.presetFormOpen
|
|
2020
|
+
? React.createElement("div", { className: "agn-preset-form" },
|
|
2021
|
+
React.createElement("input", {
|
|
2022
|
+
className: "agn-input agn-input-sm",
|
|
2023
|
+
value: s.presetLabel,
|
|
2024
|
+
placeholder: "给这个预设起个名字(留空则取提示词前 10 个字)",
|
|
2025
|
+
maxLength: 20,
|
|
2026
|
+
disabled: s.presetBusy,
|
|
2027
|
+
onChange: (e) => store.setPresetLabel(e.target.value),
|
|
2028
|
+
onKeyDown: (e) => {
|
|
2029
|
+
if (e.key === "Enter") { e.preventDefault(); store.saveCurrentAsPreset(); }
|
|
2030
|
+
},
|
|
2031
|
+
}),
|
|
2032
|
+
React.createElement("button", {
|
|
2033
|
+
type: "button", className: "agn-btn",
|
|
2034
|
+
disabled: s.presetBusy,
|
|
2035
|
+
onClick: () => store.saveCurrentAsPreset(),
|
|
2036
|
+
}, s.presetBusy ? "保存中…" : "保存")
|
|
2037
|
+
)
|
|
2038
|
+
: null,
|
|
1451
2039
|
React.createElement("textarea", {
|
|
1452
2040
|
className: "agn-textarea",
|
|
1453
2041
|
value: s.prompt,
|
|
@@ -1539,6 +2127,7 @@ window.__ModuleLoader__.load({
|
|
|
1539
2127
|
+ " · " + shown.size + "/" + shown.ratio
|
|
1540
2128
|
+ (shown.seconds ? " · " + shown.seconds + " 秒" : "")
|
|
1541
2129
|
+ (shown.mode === "keyframe" ? " · 图生视频" : "")
|
|
2130
|
+
+ (shown.mode === "reference" ? " · 参考生视频" : "")
|
|
1542
2131
|
+ (shown.mode === "img2img" ? " · 图生图 " + (shown.refCount || 0) + " 张参考" : "")
|
|
1543
2132
|
+ (shown.seed !== undefined && shown.seed !== null ? " · seed " + shown.seed : "")
|
|
1544
2133
|
+ " · 用时 " + (shown.elapsedSec || shown.wallSec) + "s"),
|
|
@@ -1564,6 +2153,13 @@ window.__ModuleLoader__.load({
|
|
|
1564
2153
|
title: "另存一份到「下载」目录",
|
|
1565
2154
|
onClick: () => store.downloadResult(),
|
|
1566
2155
|
}, "下载"),
|
|
2156
|
+
React.createElement("a", {
|
|
2157
|
+
className: "agn-act agn-act-link",
|
|
2158
|
+
href: srcOf(shown.filename),
|
|
2159
|
+
target: "_blank",
|
|
2160
|
+
rel: "noopener noreferrer",
|
|
2161
|
+
title: "用浏览器新标签页打开原文件",
|
|
2162
|
+
}, "新窗口打开"),
|
|
1567
2163
|
shown.kind === "image"
|
|
1568
2164
|
? React.createElement("button", {
|
|
1569
2165
|
type: "button", className: "agn-act",
|
|
@@ -1711,6 +2307,52 @@ window.__ModuleLoader__.load({
|
|
|
1711
2307
|
)
|
|
1712
2308
|
: null,
|
|
1713
2309
|
|
|
2310
|
+
s.cfgOpen
|
|
2311
|
+
? React.createElement("div", { className: "agn-field" },
|
|
2312
|
+
React.createElement("div", { className: "agn-label" }, "图片模型"),
|
|
2313
|
+
React.createElement("select", {
|
|
2314
|
+
className: "agn-input", value: s.cfgDraft.imageModel, disabled: s.cfgSaving,
|
|
2315
|
+
onChange: (e) => store.setCfgDraft("imageModel", e.target.value),
|
|
2316
|
+
},
|
|
2317
|
+
React.createElement("option", { value: "" }, "默认(" + ((s.health && s.health.model) || "agnes-image-2.5-flash") + ")"),
|
|
2318
|
+
((s.health && s.health.models && s.health.models.image) || []).map((m) =>
|
|
2319
|
+
React.createElement("option", { key: m, value: m }, m))
|
|
2320
|
+
),
|
|
2321
|
+
React.createElement("div", { className: "agn-note" }, "只列出当前上游真实存在的图片模型")
|
|
2322
|
+
)
|
|
2323
|
+
: null,
|
|
2324
|
+
|
|
2325
|
+
s.cfgOpen
|
|
2326
|
+
? React.createElement("div", { className: "agn-field" },
|
|
2327
|
+
React.createElement("div", { className: "agn-label" }, "视频模型"),
|
|
2328
|
+
React.createElement("select", {
|
|
2329
|
+
className: "agn-input", value: s.cfgDraft.videoModel, disabled: s.cfgSaving,
|
|
2330
|
+
onChange: (e) => store.setCfgDraft("videoModel", e.target.value),
|
|
2331
|
+
},
|
|
2332
|
+
React.createElement("option", { value: "" }, "默认(" + ((s.health && s.health.videoModel) || "agnes-video-2.5-flash") + ")"),
|
|
2333
|
+
((s.health && s.health.models && s.health.models.video) || []).map((m) =>
|
|
2334
|
+
React.createElement("option", { key: m, value: m }, m))
|
|
2335
|
+
),
|
|
2336
|
+
React.createElement("div", { className: "agn-note" }, "换模型后新提交的任务才会用新模型")
|
|
2337
|
+
)
|
|
2338
|
+
: null,
|
|
2339
|
+
|
|
2340
|
+
s.cfgOpen
|
|
2341
|
+
? React.createElement("div", { className: "agn-field" },
|
|
2342
|
+
React.createElement("button", {
|
|
2343
|
+
type: "button", className: "agn-btn-ghost", disabled: s.checking,
|
|
2344
|
+
title: "用当前 Key 请求一次上游模型清单,确认 Key 能用、网络通",
|
|
2345
|
+
onClick: () => store.checkKey(),
|
|
2346
|
+
}, s.checking ? "连接中…" : "测试连接"),
|
|
2347
|
+
s.checkResult !== null
|
|
2348
|
+
? React.createElement("div", {
|
|
2349
|
+
// 别给 style 传字符串:React 会抛错并卸载整块面板(白屏)
|
|
2350
|
+
className: (s.checkResult.ok ? "agn-okline" : "agn-err") + " agn-check",
|
|
2351
|
+
}, s.checkResult.text)
|
|
2352
|
+
: null
|
|
2353
|
+
)
|
|
2354
|
+
: null,
|
|
2355
|
+
|
|
1714
2356
|
s.cfgOpen
|
|
1715
2357
|
? React.createElement("div", { className: "agn-row" },
|
|
1716
2358
|
React.createElement("button", {
|
|
@@ -1773,7 +2415,7 @@ window.__ModuleLoader__.load({
|
|
|
1773
2415
|
|
|
1774
2416
|
exports.apply = apply;
|
|
1775
2417
|
exports.inject = ["slots"];
|
|
1776
|
-
exports.__internals = { getStore, createStore, srcOf, shrinkImage, MAX_REFS };
|
|
2418
|
+
exports.__internals = { pickableImagesOf, getStore, createStore, srcOf, shrinkImage, MAX_REFS };
|
|
1777
2419
|
return module.exports;
|
|
1778
2420
|
},
|
|
1779
2421
|
});
|