dsh-agnes-create 0.7.0 → 0.10.3
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 +34 -0
- package/lib/agnes.js +166 -12
- package/lib/client.js +530 -71
- package/lib/index.js +496 -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); }
|
|
@@ -243,6 +255,8 @@ window.__ModuleLoader__.load({
|
|
|
243
255
|
color: var(--dsw-alias-state-business-primary, #4b7bec); }
|
|
244
256
|
.agn-act-on { border-color: var(--dsw-alias-state-business-primary, #4b7bec);
|
|
245
257
|
color: var(--dsw-alias-state-business-primary, #4b7bec); }
|
|
258
|
+
/* 「测试连接」的结果行:和按钮之间留一点距离 */
|
|
259
|
+
.agn-check { margin-top: 6px; }
|
|
246
260
|
.agn-notice { font-size: 12px; color: var(--dsw-alias-state-success-primary, #2a2);
|
|
247
261
|
line-height: 1.6; }
|
|
248
262
|
`;
|
|
@@ -261,6 +275,25 @@ window.__ModuleLoader__.load({
|
|
|
261
275
|
return "/agnes/media?name=" + encodeURIComponent(name);
|
|
262
276
|
}
|
|
263
277
|
|
|
278
|
+
/* 「+ 结果」的候选:最近生成的图片 + 当前结果区那张(按文件名去重)。
|
|
279
|
+
从「输出目录」点出来的图只进了结果区、没进历史,只翻历史会漏掉它,
|
|
280
|
+
于是刚在眼前看到的那张图反而选不进参考图 / 关键帧。 */
|
|
281
|
+
function pickableImagesOf(state) {
|
|
282
|
+
const history = (Array.isArray(state.history) ? state.history : []).filter((h) => h.kind === "image");
|
|
283
|
+
const cur = state.result;
|
|
284
|
+
if (cur === null || cur === undefined || cur.kind !== "image") return history;
|
|
285
|
+
if (typeof cur.filename !== "string" || cur.filename === "") return history;
|
|
286
|
+
if (history.some((h) => h.filename === cur.filename)) return history;
|
|
287
|
+
return [cur].concat(history);
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
/* 视频生成方式:宿主回来的 mode 只可能是这三种之一,其余一律当纯文本 */
|
|
291
|
+
function modeOf(mode) {
|
|
292
|
+
if (mode === "keyframe") return "keyframe";
|
|
293
|
+
if (mode === "reference") return "reference";
|
|
294
|
+
return "text";
|
|
295
|
+
}
|
|
296
|
+
|
|
264
297
|
/* ==========================================================================
|
|
265
298
|
参考图预处理(图生图)
|
|
266
299
|
--------------------------------------------------------------------------
|
|
@@ -362,6 +395,7 @@ window.__ModuleLoader__.load({
|
|
|
362
395
|
vProgress: 0,
|
|
363
396
|
vStatus: "",
|
|
364
397
|
videoId: null,
|
|
398
|
+
vTaskId: null,
|
|
365
399
|
health: null,
|
|
366
400
|
healthRequested: false,
|
|
367
401
|
|
|
@@ -369,17 +403,25 @@ window.__ModuleLoader__.load({
|
|
|
369
403
|
cfg: null, // 生效配置 { keyFile, outDir }
|
|
370
404
|
cfgDefaults: null,
|
|
371
405
|
cfgStored: null,
|
|
372
|
-
cfgDraft: { keyFile: "", outDir: "" },
|
|
406
|
+
cfgDraft: { keyFile: "", outDir: "", imageModel: "", videoModel: "" },
|
|
373
407
|
cfgOpen: false,
|
|
374
408
|
cfgSaving: false,
|
|
375
409
|
cfgError: "",
|
|
376
410
|
cfgNotice: "",
|
|
411
|
+
checking: false, // 正在测连通性
|
|
412
|
+
checkResult: null, // { ok, text, detail }
|
|
413
|
+
upModels: null, // 连接测试拿到的上游真实模型清单 { image: [], video: [] }
|
|
377
414
|
|
|
378
415
|
/* ---- 图生图参考图 ---- */
|
|
379
416
|
refs: [], // { id, name, src(dataURI), bytes, from: 'local' | 'result' }
|
|
380
417
|
refsBusy: false,
|
|
381
418
|
refsError: "",
|
|
382
419
|
refPickOpen: false,
|
|
420
|
+
/* ---- 视频参考图(reference 模式,最多 5 张) ---- */
|
|
421
|
+
vRefs: [],
|
|
422
|
+
vRefsBusy: false,
|
|
423
|
+
vRefsError: "",
|
|
424
|
+
vRefPickOpen: false,
|
|
383
425
|
|
|
384
426
|
/* ---- 视频关键帧(可选):本地图直接当首帧 / 尾帧 ---- */
|
|
385
427
|
vFirst: null, // { src(dataURI), name, bytes }
|
|
@@ -437,6 +479,8 @@ window.__ModuleLoader__.load({
|
|
|
437
479
|
set({ result: entry, history: [entry, ...state.history].slice(0, state.historyMax) });
|
|
438
480
|
// 顺手落盘:/agnes/history 是唯一真相,写失败也不影响本次展示
|
|
439
481
|
void appendHistory(entry);
|
|
482
|
+
// 新文件已经写进输出目录了,目录清单开着的话跟着变(没加载过就不去打扰)
|
|
483
|
+
void refreshLibrary();
|
|
440
484
|
};
|
|
441
485
|
|
|
442
486
|
/* ---------- 历史(宿主持久化) ---------- */
|
|
@@ -512,7 +556,12 @@ window.__ModuleLoader__.load({
|
|
|
512
556
|
cfg: data.config,
|
|
513
557
|
cfgDefaults: data.defaults,
|
|
514
558
|
cfgStored: data.stored,
|
|
515
|
-
cfgDraft: {
|
|
559
|
+
cfgDraft: {
|
|
560
|
+
keyFile: data.config.keyFile,
|
|
561
|
+
outDir: data.config.outDir,
|
|
562
|
+
imageModel: data.config.imageModel || "",
|
|
563
|
+
videoModel: data.config.videoModel || "",
|
|
564
|
+
},
|
|
516
565
|
cfgError: "",
|
|
517
566
|
});
|
|
518
567
|
} catch (e) {
|
|
@@ -530,6 +579,9 @@ window.__ModuleLoader__.load({
|
|
|
530
579
|
body: JSON.stringify({
|
|
531
580
|
keyFile: state.cfgDraft.keyFile,
|
|
532
581
|
outDir: state.cfgDraft.outDir,
|
|
582
|
+
// 空串 = 回退到内置默认模型
|
|
583
|
+
imageModel: state.cfgDraft.imageModel,
|
|
584
|
+
videoModel: state.cfgDraft.videoModel,
|
|
533
585
|
}),
|
|
534
586
|
});
|
|
535
587
|
if (res.status === 404) throw new Error("配置接口尚未就绪:需要完全重启应用(只重载窗口不够)");
|
|
@@ -539,7 +591,12 @@ window.__ModuleLoader__.load({
|
|
|
539
591
|
cfg: data.config,
|
|
540
592
|
cfgDefaults: data.defaults,
|
|
541
593
|
cfgStored: data.stored,
|
|
542
|
-
cfgDraft: {
|
|
594
|
+
cfgDraft: {
|
|
595
|
+
keyFile: data.config.keyFile,
|
|
596
|
+
outDir: data.config.outDir,
|
|
597
|
+
imageModel: data.config.imageModel || "",
|
|
598
|
+
videoModel: data.config.videoModel || "",
|
|
599
|
+
},
|
|
543
600
|
cfgNotice: "已保存",
|
|
544
601
|
});
|
|
545
602
|
// 路径变了,key 是否存在/目录是否可写都要重新算
|
|
@@ -553,6 +610,43 @@ window.__ModuleLoader__.load({
|
|
|
553
610
|
}
|
|
554
611
|
}
|
|
555
612
|
|
|
613
|
+
/** 测一下 Key 能不能连上游:顺便把上游真实的模型名单拉回来 */
|
|
614
|
+
async function checkKey() {
|
|
615
|
+
if (state.checking) return;
|
|
616
|
+
set({ checking: true, checkResult: null, cfgError: "" });
|
|
617
|
+
try {
|
|
618
|
+
const res = await fetch("/agnes/models");
|
|
619
|
+
const data = await res.json().catch(() => ({}));
|
|
620
|
+
if (res.status === 404) {
|
|
621
|
+
throw new Error("模型探测接口尚未就绪:需要完全重启应用(只重载窗口不够)");
|
|
622
|
+
}
|
|
623
|
+
if (!res.ok || !data || data.ok !== true) {
|
|
624
|
+
throw new Error((data && data.error) || ("探测失败:HTTP " + res.status));
|
|
625
|
+
}
|
|
626
|
+
const up = data.upstream || {};
|
|
627
|
+
const imgs = Array.isArray(up.image) ? up.image.length : 0;
|
|
628
|
+
const vids = Array.isArray(up.video) ? up.video.length : 0;
|
|
629
|
+
set({
|
|
630
|
+
checking: false,
|
|
631
|
+
// 把上游真实清单留下:顶部的模型下拉优先用它,比内置清单更可信
|
|
632
|
+
upModels: {
|
|
633
|
+
image: Array.isArray(up.image) ? up.image : [],
|
|
634
|
+
video: Array.isArray(up.video) ? up.video : [],
|
|
635
|
+
},
|
|
636
|
+
checkResult: {
|
|
637
|
+
ok: true,
|
|
638
|
+
text: "连接正常:上游可用 " + imgs + " 个图片模型、" + vids + " 个视频模型",
|
|
639
|
+
detail: ""
|
|
640
|
+
},
|
|
641
|
+
});
|
|
642
|
+
} catch (e) {
|
|
643
|
+
set({
|
|
644
|
+
checking: false,
|
|
645
|
+
checkResult: { ok: false, text: e && e.message ? e.message : String(e), detail: "" },
|
|
646
|
+
});
|
|
647
|
+
}
|
|
648
|
+
}
|
|
649
|
+
|
|
556
650
|
/* ---------- 图片:一次请求就出结果 ---------- */
|
|
557
651
|
async function runImage(token, prompt) {
|
|
558
652
|
const refs = state.refs;
|
|
@@ -690,6 +784,101 @@ window.__ModuleLoader__.load({
|
|
|
690
784
|
set({ refPickOpen: !state.refPickOpen, refsError: "" });
|
|
691
785
|
}
|
|
692
786
|
|
|
787
|
+
/* ---------- 视频参考图(reference 模式,≤5 张) ---------- */
|
|
788
|
+
const vRefsFull = () => state.vRefs.length >= MAX_VREFS;
|
|
789
|
+
|
|
790
|
+
function addVRefsFromFiles(files) {
|
|
791
|
+
const list = Array.from(files || []).filter(
|
|
792
|
+
(f) => f && String(f.type || "").startsWith("image/")
|
|
793
|
+
);
|
|
794
|
+
if (list.length === 0) {
|
|
795
|
+
set({ vRefsError: "没有可用的图片文件(需要 PNG / JPEG / WebP / GIF)" });
|
|
796
|
+
return Promise.resolve();
|
|
797
|
+
}
|
|
798
|
+
|
|
799
|
+
return queueRefOp(async () => {
|
|
800
|
+
set({ vRefsBusy: true, vRefsError: "" });
|
|
801
|
+
const added = [];
|
|
802
|
+
const failed = [];
|
|
803
|
+
try {
|
|
804
|
+
for (const file of list) {
|
|
805
|
+
if (state.vRefs.length + added.length >= MAX_VREFS) {
|
|
806
|
+
failed.push("超出 " + MAX_VREFS + " 张上限");
|
|
807
|
+
break;
|
|
808
|
+
}
|
|
809
|
+
try {
|
|
810
|
+
const r = await shrinkImage(file);
|
|
811
|
+
added.push({
|
|
812
|
+
id: nextRefId(),
|
|
813
|
+
name: file.name || "本地图片",
|
|
814
|
+
src: r.src,
|
|
815
|
+
bytes: r.bytes,
|
|
816
|
+
from: "local",
|
|
817
|
+
});
|
|
818
|
+
} catch (e) {
|
|
819
|
+
failed.push((file.name || "图片") + ":" + (e && e.message ? e.message : String(e)));
|
|
820
|
+
}
|
|
821
|
+
}
|
|
822
|
+
if (added.length > 0) {
|
|
823
|
+
update((prev) => ({ vRefs: prev.vRefs.concat(added) }));
|
|
824
|
+
}
|
|
825
|
+
set({
|
|
826
|
+
vRefsError: failed.length > 0 ? failed.join(";") : "",
|
|
827
|
+
vRefPickOpen: false,
|
|
828
|
+
});
|
|
829
|
+
} finally {
|
|
830
|
+
set({ vRefsBusy: false });
|
|
831
|
+
}
|
|
832
|
+
});
|
|
833
|
+
}
|
|
834
|
+
|
|
835
|
+
function addVRefFromResult(entry) {
|
|
836
|
+
if (!entry || entry.kind !== "image" || !entry.filename) return Promise.resolve();
|
|
837
|
+
if (vRefsFull()) {
|
|
838
|
+
set({ vRefsError: "参考图最多 " + MAX_VREFS + " 张" });
|
|
839
|
+
return Promise.resolve();
|
|
840
|
+
}
|
|
841
|
+
|
|
842
|
+
return queueRefOp(async () => {
|
|
843
|
+
set({ vRefsBusy: true, vRefsError: "" });
|
|
844
|
+
try {
|
|
845
|
+
const res = await fetch(srcOf(entry.filename));
|
|
846
|
+
if (!res.ok) throw new Error("读取生成结果失败:HTTP " + res.status);
|
|
847
|
+
const blob = await res.blob();
|
|
848
|
+
const r = await shrinkImage(blob);
|
|
849
|
+
update((prev) => ({
|
|
850
|
+
vRefs: prev.vRefs.concat([{
|
|
851
|
+
id: nextRefId(),
|
|
852
|
+
name: entry.filename,
|
|
853
|
+
src: r.src,
|
|
854
|
+
bytes: r.bytes,
|
|
855
|
+
from: "result",
|
|
856
|
+
}]),
|
|
857
|
+
vRefPickOpen: false,
|
|
858
|
+
}));
|
|
859
|
+
} catch (e) {
|
|
860
|
+
set({ vRefsError: e && e.message ? e.message : String(e) });
|
|
861
|
+
} finally {
|
|
862
|
+
set({ vRefsBusy: false });
|
|
863
|
+
}
|
|
864
|
+
});
|
|
865
|
+
}
|
|
866
|
+
|
|
867
|
+
function removeVRef(id) {
|
|
868
|
+
if (state.busy) return;
|
|
869
|
+
update((prev) => ({ vRefs: prev.vRefs.filter((r) => r.id !== id), vRefsError: "" }));
|
|
870
|
+
}
|
|
871
|
+
|
|
872
|
+
function clearVRefs() {
|
|
873
|
+
if (state.busy) return;
|
|
874
|
+
set({ vRefs: [], vRefsError: "", vRefPickOpen: false });
|
|
875
|
+
}
|
|
876
|
+
|
|
877
|
+
function toggleVRefPick() {
|
|
878
|
+
if (state.busy) return;
|
|
879
|
+
set({ vRefPickOpen: !state.vRefPickOpen, vRefsError: "" });
|
|
880
|
+
}
|
|
881
|
+
|
|
693
882
|
/* ---------- 视频关键帧(首帧 / 尾帧,本地图直接当帧) ---------- */
|
|
694
883
|
const frameField = (which) => (which === "last" ? "vLast" : "vFirst");
|
|
695
884
|
|
|
@@ -797,14 +986,17 @@ window.__ModuleLoader__.load({
|
|
|
797
986
|
set({ notice: "已开始下载" });
|
|
798
987
|
}
|
|
799
988
|
|
|
800
|
-
/**
|
|
989
|
+
/** 停掉等待:让在途的轮询/请求结果失效(宿主的任务照常跑,文件会正常落盘) */
|
|
801
990
|
function cancel() {
|
|
802
991
|
if (!state.busy) return;
|
|
992
|
+
const hadTask = state.vTaskId !== null;
|
|
803
993
|
runToken += 1;
|
|
804
994
|
stopTimer();
|
|
805
995
|
set({
|
|
806
996
|
busy: false, vStatus: "", vProgress: 0,
|
|
807
|
-
notice:
|
|
997
|
+
notice: hadTask
|
|
998
|
+
? "已停止跟踪;宿主会继续推进,刷新面板还能接回进度,文件也会照常落盘"
|
|
999
|
+
: "已停止等待;服务端任务可能仍在跑",
|
|
808
1000
|
});
|
|
809
1001
|
}
|
|
810
1002
|
|
|
@@ -850,6 +1042,23 @@ window.__ModuleLoader__.load({
|
|
|
850
1042
|
}
|
|
851
1043
|
}
|
|
852
1044
|
|
|
1045
|
+
/**
|
|
1046
|
+
* 静默刷新输出目录清单:生成完成后新文件已经落盘,列表要跟着更新。
|
|
1047
|
+
* 只在清单已经加载过时才拉(没展开过就别偷偷把面板撑开),失败保留旧清单。
|
|
1048
|
+
*/
|
|
1049
|
+
async function refreshLibrary() {
|
|
1050
|
+
if (state.library === null || state.libraryBusy) return;
|
|
1051
|
+
try {
|
|
1052
|
+
const res = await fetch("/agnes/library");
|
|
1053
|
+
const data = await res.json().catch(() => ({}));
|
|
1054
|
+
if (!res.ok || !data || data.ok !== true) return;
|
|
1055
|
+
if (!Array.isArray(data.files)) return;
|
|
1056
|
+
update(() => ({ library: data.files, libraryError: "" }));
|
|
1057
|
+
} catch {
|
|
1058
|
+
// 刷新失败保留旧清单,不打扰用户
|
|
1059
|
+
}
|
|
1060
|
+
}
|
|
1061
|
+
|
|
853
1062
|
function toggleLibrary() {
|
|
854
1063
|
if (state.libraryOpen) {
|
|
855
1064
|
set({ libraryOpen: false });
|
|
@@ -882,59 +1091,32 @@ window.__ModuleLoader__.load({
|
|
|
882
1091
|
});
|
|
883
1092
|
}
|
|
884
1093
|
|
|
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: "排队中…" });
|
|
1094
|
+
/* ---------- 视频:交给宿主任务队列,面板只负责看进度 ---------- */
|
|
1095
|
+
// 提交后 video_id 由宿主保管:关面板、刷新页面、重启应用都不影响任务继续推进。
|
|
1096
|
+
async function watchTask(token, taskId, fallback) {
|
|
914
1097
|
const deadline = Date.now() + VIDEO_DEADLINE_MS;
|
|
915
|
-
|
|
916
1098
|
let transientFails = 0;
|
|
1099
|
+
|
|
917
1100
|
for (;;) {
|
|
918
1101
|
await sleep(POLL_MS);
|
|
919
1102
|
if (token !== runToken) return;
|
|
920
1103
|
|
|
921
1104
|
let st;
|
|
922
1105
|
try {
|
|
923
|
-
const r = await fetch("/agnes/
|
|
924
|
-
st = await r.json();
|
|
1106
|
+
const r = await fetch("/agnes/task?id=" + encodeURIComponent(taskId));
|
|
1107
|
+
st = r.ok ? await r.json() : { ok: false, error: "查询任务失败:HTTP " + r.status };
|
|
925
1108
|
} catch (e) {
|
|
926
1109
|
st = { ok: false, error: "网络错误:" + (e && e.message ? e.message : String(e)) };
|
|
927
1110
|
}
|
|
928
1111
|
|
|
929
1112
|
if (!st.ok) {
|
|
930
1113
|
const msg = String(st.error || "查询任务失败");
|
|
931
|
-
//
|
|
932
|
-
// 实测提交后立刻查询就会 429,这条路径是必需的。
|
|
1114
|
+
// 宿主自己会重试上游;这里只在本地接口抖动时容忍几次
|
|
933
1115
|
if (/\b429\b|\b503\b|频繁|队列已满|网络错误|timeout/i.test(msg)) {
|
|
934
1116
|
transientFails += 1;
|
|
935
1117
|
if (transientFails > 8) throw new Error(msg);
|
|
936
|
-
set({ vStatus: "
|
|
937
|
-
await sleep(Math.min(30000,
|
|
1118
|
+
set({ vStatus: "重试中…" });
|
|
1119
|
+
await sleep(Math.min(30000, 3000 * transientFails));
|
|
938
1120
|
continue;
|
|
939
1121
|
}
|
|
940
1122
|
throw new Error(msg);
|
|
@@ -943,33 +1125,126 @@ window.__ModuleLoader__.load({
|
|
|
943
1125
|
transientFails = 0;
|
|
944
1126
|
if (token !== runToken) return;
|
|
945
1127
|
|
|
1128
|
+
const task = st.task || {};
|
|
946
1129
|
set({
|
|
947
|
-
vStatus:
|
|
948
|
-
vProgress: typeof
|
|
1130
|
+
vStatus: task.status ? String(task.status) : "",
|
|
1131
|
+
vProgress: typeof task.progress === "number" ? task.progress : 0,
|
|
1132
|
+
videoId: task.videoId || fallback.videoId || null,
|
|
949
1133
|
});
|
|
950
1134
|
|
|
951
|
-
if (
|
|
1135
|
+
if (task.status === "completed") {
|
|
952
1136
|
pushResult({
|
|
953
1137
|
kind: "video",
|
|
954
|
-
filename:
|
|
955
|
-
path:
|
|
956
|
-
bytes:
|
|
1138
|
+
filename: task.filename,
|
|
1139
|
+
path: task.path,
|
|
1140
|
+
bytes: task.bytes,
|
|
957
1141
|
size: "720P",
|
|
958
|
-
ratio:
|
|
959
|
-
seconds:
|
|
960
|
-
transport: "
|
|
961
|
-
mode:
|
|
962
|
-
seed:
|
|
963
|
-
prompt,
|
|
1142
|
+
ratio: task.ratio || fallback.ratio,
|
|
1143
|
+
seconds: task.seconds || fallback.seconds,
|
|
1144
|
+
transport: "宿主任务",
|
|
1145
|
+
mode: modeOf(task.mode || fallback.mode),
|
|
1146
|
+
seed: task.seed === null || task.seed === undefined ? undefined : task.seed,
|
|
1147
|
+
prompt: task.prompt || fallback.prompt,
|
|
964
1148
|
wallSec: ((Date.now() - state.startedAt) / 1000).toFixed(1),
|
|
965
1149
|
});
|
|
966
1150
|
return;
|
|
967
1151
|
}
|
|
968
|
-
if (
|
|
969
|
-
|
|
970
|
-
|
|
1152
|
+
if (task.status === "failed") throw new Error(task.error || "视频生成失败");
|
|
1153
|
+
if (task.status === "cancelled") throw new Error("任务已取消");
|
|
1154
|
+
if (Date.now() > deadline) {
|
|
1155
|
+
throw new Error("等待超时(15 分钟);宿主仍在推进,稍后刷新还能看到结果");
|
|
971
1156
|
}
|
|
972
|
-
|
|
1157
|
+
}
|
|
1158
|
+
}
|
|
1159
|
+
|
|
1160
|
+
async function runVideo(token, prompt) {
|
|
1161
|
+
set({ vStatus: "提交中…", vProgress: 0, videoId: null, vTaskId: null });
|
|
1162
|
+
|
|
1163
|
+
const seedText = String(state.vSeed).trim();
|
|
1164
|
+
const seedValue = seedText === "" ? null : Number(seedText);
|
|
1165
|
+
if (seedValue !== null && !(Number.isInteger(seedValue) && seedValue >= 0)) {
|
|
1166
|
+
throw new Error("seed 需要非负整数(也可以留空)");
|
|
1167
|
+
}
|
|
1168
|
+
|
|
1169
|
+
// 关键帧直接发 data URI:上游已实测接受 base64,本地图片不用先传图床
|
|
1170
|
+
const subRes = await fetch("/agnes/tasks", {
|
|
1171
|
+
method: "POST",
|
|
1172
|
+
headers: { "content-type": "application/json" },
|
|
1173
|
+
body: JSON.stringify({
|
|
1174
|
+
prompt,
|
|
1175
|
+
seconds: state.vSeconds,
|
|
1176
|
+
ratio: state.vRatio,
|
|
1177
|
+
seed: seedValue,
|
|
1178
|
+
firstFrame: state.vFirst !== null ? state.vFirst.src : null,
|
|
1179
|
+
lastFrame: state.vLast !== null ? state.vLast.src : null,
|
|
1180
|
+
// 参考图(reference 模式):有首/尾帧时上游会用 keyframe,参考图自动让位
|
|
1181
|
+
referenceImages: state.vRefs.map((r) => r.src),
|
|
1182
|
+
}),
|
|
1183
|
+
});
|
|
1184
|
+
const sub = await subRes.json();
|
|
1185
|
+
if (!sub.ok) throw new Error(sub.error || "提交失败");
|
|
1186
|
+
if (token !== runToken) return;
|
|
1187
|
+
|
|
1188
|
+
const task = sub.task || {};
|
|
1189
|
+
if (!task.id) throw new Error("宿主没有返回任务号");
|
|
1190
|
+
set({ videoId: task.videoId || null, vTaskId: task.id, vStatus: "排队中…" });
|
|
1191
|
+
|
|
1192
|
+
await watchTask(token, task.id, {
|
|
1193
|
+
prompt,
|
|
1194
|
+
ratio: state.vRatio,
|
|
1195
|
+
seconds: state.vSeconds,
|
|
1196
|
+
mode: task.mode,
|
|
1197
|
+
videoId: task.videoId,
|
|
1198
|
+
});
|
|
1199
|
+
}
|
|
1200
|
+
|
|
1201
|
+
/* 面板重新挂载时把宿主上还在跑的任务接回来:刷新页面不会让进度消失 */
|
|
1202
|
+
async function resumeTasks() {
|
|
1203
|
+
if (state.busy) return;
|
|
1204
|
+
try {
|
|
1205
|
+
const r = await fetch("/agnes/tasks");
|
|
1206
|
+
if (!r.ok) return;
|
|
1207
|
+
const data = await r.json();
|
|
1208
|
+
if (!data || data.ok !== true || !Array.isArray(data.tasks)) return;
|
|
1209
|
+
const active = data.tasks.find((t) => t.status === "queued" || t.status === "running");
|
|
1210
|
+
if (!active || state.busy) return;
|
|
1211
|
+
|
|
1212
|
+
const token = ++runToken;
|
|
1213
|
+
const created = Date.parse(active.createdAt);
|
|
1214
|
+
set({
|
|
1215
|
+
mode: "video",
|
|
1216
|
+
busy: true,
|
|
1217
|
+
error: "",
|
|
1218
|
+
notice: "",
|
|
1219
|
+
startedAt: Number.isFinite(created) ? created : Date.now(),
|
|
1220
|
+
elapsed: 0,
|
|
1221
|
+
vFirst: null,
|
|
1222
|
+
vLast: null,
|
|
1223
|
+
videoId: active.videoId || null,
|
|
1224
|
+
vTaskId: active.id,
|
|
1225
|
+
vStatus: active.status,
|
|
1226
|
+
vProgress: typeof active.progress === "number" ? active.progress : 0,
|
|
1227
|
+
prompt: state.prompt.trim() === "" ? String(active.prompt || "") : state.prompt,
|
|
1228
|
+
});
|
|
1229
|
+
startTimer();
|
|
1230
|
+
try {
|
|
1231
|
+
await watchTask(token, active.id, {
|
|
1232
|
+
prompt: active.prompt,
|
|
1233
|
+
ratio: active.ratio || state.vRatio,
|
|
1234
|
+
seconds: active.seconds || state.vSeconds,
|
|
1235
|
+
mode: active.mode,
|
|
1236
|
+
videoId: active.videoId,
|
|
1237
|
+
});
|
|
1238
|
+
} catch (e) {
|
|
1239
|
+
if (token === runToken) set({ error: e && e.message ? e.message : String(e) });
|
|
1240
|
+
} finally {
|
|
1241
|
+
if (token === runToken) {
|
|
1242
|
+
stopTimer();
|
|
1243
|
+
set({ busy: false, elapsed: (Date.now() - state.startedAt) / 1000 });
|
|
1244
|
+
}
|
|
1245
|
+
}
|
|
1246
|
+
} catch {
|
|
1247
|
+
// 旧宿主没有 /agnes/tasks:静默跳过,不打扰用户
|
|
973
1248
|
}
|
|
974
1249
|
}
|
|
975
1250
|
|
|
@@ -1014,7 +1289,19 @@ window.__ModuleLoader__.load({
|
|
|
1014
1289
|
setCfgDraft(field, value) {
|
|
1015
1290
|
set({ cfgDraft: { ...state.cfgDraft, [field]: value }, cfgNotice: "" });
|
|
1016
1291
|
},
|
|
1292
|
+
|
|
1293
|
+
/* 顶部标题栏直接换模型:先写进草稿,再立刻落盘(不然刷新就回退了) */
|
|
1294
|
+
async setModel(kind, value) {
|
|
1295
|
+
if (state.busy || state.cfgSaving) return;
|
|
1296
|
+
const field = kind === "video" ? "videoModel" : "imageModel";
|
|
1297
|
+
set({ cfgDraft: { ...state.cfgDraft, [field]: value }, cfgNotice: "", cfgError: "" });
|
|
1298
|
+
await saveConfig();
|
|
1299
|
+
if (state.cfgError === "") {
|
|
1300
|
+
set({ notice: "已切换模型:" + (value === "" ? "内置默认" : value) });
|
|
1301
|
+
}
|
|
1302
|
+
},
|
|
1017
1303
|
toggleCfg() { set({ cfgOpen: !state.cfgOpen }); },
|
|
1304
|
+
checkKey,
|
|
1018
1305
|
run,
|
|
1019
1306
|
/* ---- 参考图动作 ---- */
|
|
1020
1307
|
addRefsFromFiles,
|
|
@@ -1022,6 +1309,12 @@ window.__ModuleLoader__.load({
|
|
|
1022
1309
|
removeRef,
|
|
1023
1310
|
clearRefs,
|
|
1024
1311
|
toggleRefPick,
|
|
1312
|
+
/* ---- 视频参考图(reference 模式,≤5 张) ---- */
|
|
1313
|
+
addVRefsFromFiles,
|
|
1314
|
+
addVRefFromResult,
|
|
1315
|
+
removeVRef,
|
|
1316
|
+
clearVRefs,
|
|
1317
|
+
toggleVRefPick,
|
|
1025
1318
|
/* ---- 视频关键帧 ---- */
|
|
1026
1319
|
pickFrame,
|
|
1027
1320
|
setFrameFromFile,
|
|
@@ -1029,6 +1322,8 @@ window.__ModuleLoader__.load({
|
|
|
1029
1322
|
toggleFramePick,
|
|
1030
1323
|
setFrameFromResult,
|
|
1031
1324
|
useResultAsFrame,
|
|
1325
|
+
/* ---- 视频任务(宿主侧推进,刷新可接回) ---- */
|
|
1326
|
+
resumeTasks,
|
|
1032
1327
|
/* ---- 结果收尾 ---- */
|
|
1033
1328
|
copyPath,
|
|
1034
1329
|
reveal,
|
|
@@ -1041,6 +1336,7 @@ window.__ModuleLoader__.load({
|
|
|
1041
1336
|
setLibraryTab,
|
|
1042
1337
|
showLibraryFile,
|
|
1043
1338
|
maxRefs: MAX_REFS,
|
|
1339
|
+
maxVRefs: MAX_VREFS,
|
|
1044
1340
|
/** 切换主预览到历史里的某一张 */
|
|
1045
1341
|
show(entry) { set({ result: entry }); },
|
|
1046
1342
|
/**
|
|
@@ -1172,7 +1468,13 @@ window.__ModuleLoader__.load({
|
|
|
1172
1468
|
return () => { off(); };
|
|
1173
1469
|
}, []);
|
|
1174
1470
|
|
|
1175
|
-
React.useEffect(() => {
|
|
1471
|
+
React.useEffect(() => {
|
|
1472
|
+
store.loadHealth();
|
|
1473
|
+
store.loadConfig();
|
|
1474
|
+
store.loadHistory();
|
|
1475
|
+
// 上次没跑完的视频任务:宿主还在推进,接回进度
|
|
1476
|
+
store.resumeTasks();
|
|
1477
|
+
}, []);
|
|
1176
1478
|
|
|
1177
1479
|
const busy = s.busy;
|
|
1178
1480
|
const isVideo = s.mode === "video";
|
|
@@ -1181,13 +1483,16 @@ window.__ModuleLoader__.load({
|
|
|
1181
1483
|
const shown = s.result;
|
|
1182
1484
|
const hasRefs = s.refs.length > 0;
|
|
1183
1485
|
const refsFull = s.refs.length >= MAX_REFS;
|
|
1486
|
+
const vRefsFull = s.vRefs.length >= MAX_VREFS;
|
|
1184
1487
|
const imageHistory = s.history.filter((h) => h.kind === "image");
|
|
1488
|
+
const pickableImages = pickableImagesOf(s);
|
|
1185
1489
|
const libraryAll = s.library || [];
|
|
1186
1490
|
const libraryImages = libraryAll.filter((f) => f.kind !== "video");
|
|
1187
1491
|
const libraryVideos = libraryAll.filter((f) => f.kind === "video");
|
|
1188
1492
|
const libraryShown = s.libraryTab === "video" ? libraryVideos : libraryImages;
|
|
1189
1493
|
const fileInput = React.useRef(null);
|
|
1190
1494
|
const frameInput = React.useRef(null);
|
|
1495
|
+
const vRefInput = React.useRef(null);
|
|
1191
1496
|
|
|
1192
1497
|
const onKeyDown = (e) => {
|
|
1193
1498
|
if ((e.metaKey || e.ctrlKey) && e.key === "Enter") { e.preventDefault(); store.run(); }
|
|
@@ -1279,15 +1584,15 @@ window.__ModuleLoader__.load({
|
|
|
1279
1584
|
}, "+ 本地"),
|
|
1280
1585
|
React.createElement("button", {
|
|
1281
1586
|
type: "button", className: "agn-frame-from",
|
|
1282
|
-
disabled: busy || s.frameBusy ||
|
|
1283
|
-
title:
|
|
1587
|
+
disabled: busy || s.frameBusy || pickableImages.length === 0,
|
|
1588
|
+
title: pickableImages.length === 0
|
|
1284
1589
|
? "还没有生成过图片,先出一张图再来选"
|
|
1285
1590
|
: "从生成结果里挑一张当" + label,
|
|
1286
1591
|
onClick: () => store.toggleFramePick(which),
|
|
1287
1592
|
}, s.framePickFrom === which ? "收起" : "+ 结果"),
|
|
1288
1593
|
s.framePickFrom === which
|
|
1289
1594
|
? React.createElement("div", { className: "agn-ref-pick agn-frame-pick" },
|
|
1290
|
-
|
|
1595
|
+
pickableImages.map((h) => React.createElement("button", {
|
|
1291
1596
|
key: h.filename, type: "button", title: h.filename,
|
|
1292
1597
|
disabled: busy || s.frameBusy,
|
|
1293
1598
|
onClick: () => store.setFrameFromResult(which, h),
|
|
@@ -1331,12 +1636,49 @@ window.__ModuleLoader__.load({
|
|
|
1331
1636
|
},
|
|
1332
1637
|
}),
|
|
1333
1638
|
|
|
1639
|
+
/* 隐藏的视频参考图选择器(reference 模式,最多 5 张) */
|
|
1640
|
+
React.createElement("input", {
|
|
1641
|
+
ref: vRefInput,
|
|
1642
|
+
type: "file",
|
|
1643
|
+
accept: "image/png,image/jpeg,image/webp,image/gif",
|
|
1644
|
+
multiple: true,
|
|
1645
|
+
style: { display: "none" },
|
|
1646
|
+
onChange: (e) => {
|
|
1647
|
+
const el = e.target;
|
|
1648
|
+
const files = el.files;
|
|
1649
|
+
Promise.resolve(store.addVRefsFromFiles(files))
|
|
1650
|
+
.catch(() => {})
|
|
1651
|
+
.finally(() => { try { el.value = ""; } catch { /* input 可能已卸载 */ } });
|
|
1652
|
+
},
|
|
1653
|
+
}),
|
|
1654
|
+
|
|
1334
1655
|
React.createElement("div", { className: "agn-head" },
|
|
1335
1656
|
React.createElement("span", { className: "agn-title" }, "Agnes 创作"),
|
|
1336
|
-
|
|
1337
|
-
isVideo
|
|
1338
|
-
|
|
1339
|
-
|
|
1657
|
+
(() => {
|
|
1658
|
+
const kind = isVideo ? "video" : "image";
|
|
1659
|
+
const cur = isVideo
|
|
1660
|
+
? (s.cfgDraft.videoModel || (s.health && s.health.videoModel) || "agnes-video-2.5-flash")
|
|
1661
|
+
: (s.cfgDraft.imageModel || (s.health && s.health.model) || "agnes-image-2.5-flash");
|
|
1662
|
+
/* 选项优先用连接测试拿到的上游真实清单,没有就退回内置清单 */
|
|
1663
|
+
const fromUp = s.upModels && Array.isArray(s.upModels[kind]) ? s.upModels[kind] : null;
|
|
1664
|
+
const fromHealth = s.health && s.health.models && Array.isArray(s.health.models[kind])
|
|
1665
|
+
? s.health.models[kind]
|
|
1666
|
+
: [];
|
|
1667
|
+
let list = fromUp !== null && fromUp.length > 0 ? fromUp : fromHealth;
|
|
1668
|
+
if (list.length < 2) {
|
|
1669
|
+
/* 只有一个模型时给下拉没有意义,直接显示名字 */
|
|
1670
|
+
return React.createElement("span", { className: "agn-meta" }, cur);
|
|
1671
|
+
}
|
|
1672
|
+
list = list.slice();
|
|
1673
|
+
if (list.indexOf(cur) === -1) list.unshift(cur);
|
|
1674
|
+
return React.createElement("select", {
|
|
1675
|
+
className: "agn-model",
|
|
1676
|
+
value: cur,
|
|
1677
|
+
disabled: busy || s.cfgSaving,
|
|
1678
|
+
title: "切换" + (isVideo ? "视频" : "图片") + "模型(立刻保存到配置)",
|
|
1679
|
+
onChange: (e) => store.setModel(kind, e.target.value),
|
|
1680
|
+
}, list.map((m) => React.createElement("option", { key: m, value: m }, m)));
|
|
1681
|
+
})()
|
|
1340
1682
|
),
|
|
1341
1683
|
|
|
1342
1684
|
React.createElement("div", { className: "agn-card" },
|
|
@@ -1402,8 +1744,8 @@ window.__ModuleLoader__.load({
|
|
|
1402
1744
|
}, "+ 本地图片"),
|
|
1403
1745
|
React.createElement("button", {
|
|
1404
1746
|
type: "button", className: "agn-ref-add",
|
|
1405
|
-
disabled: busy || refsFull ||
|
|
1406
|
-
title:
|
|
1747
|
+
disabled: busy || refsFull || pickableImages.length === 0,
|
|
1748
|
+
title: pickableImages.length === 0 ? "还没有生成过图片" : "从「生成结果」里挑一张",
|
|
1407
1749
|
onClick: () => store.toggleRefPick(),
|
|
1408
1750
|
}, "+ 生成结果")
|
|
1409
1751
|
),
|
|
@@ -1411,9 +1753,9 @@ window.__ModuleLoader__.load({
|
|
|
1411
1753
|
/* 从生成结果挑图:只列图片(视频不能当参考图) */
|
|
1412
1754
|
s.refPickOpen
|
|
1413
1755
|
? React.createElement("div", { className: "agn-ref-pick" },
|
|
1414
|
-
|
|
1756
|
+
pickableImages.length === 0
|
|
1415
1757
|
? React.createElement("span", { className: "agn-hint" }, "还没有生成过图片")
|
|
1416
|
-
:
|
|
1758
|
+
: pickableImages.map((h) => React.createElement("button", {
|
|
1417
1759
|
key: "pick-" + h.filename,
|
|
1418
1760
|
type: "button",
|
|
1419
1761
|
title: h.filename,
|
|
@@ -1433,6 +1775,76 @@ window.__ModuleLoader__.load({
|
|
|
1433
1775
|
)
|
|
1434
1776
|
: null,
|
|
1435
1777
|
|
|
1778
|
+
/* ---- 视频参考图(reference 模式,≤5 张):给风格 / 主体参考,不是首帧 ---- */
|
|
1779
|
+
isVideo
|
|
1780
|
+
? React.createElement("div", { className: "agn-refs" },
|
|
1781
|
+
React.createElement("div", { className: "agn-ref-head" },
|
|
1782
|
+
React.createElement("span", { className: "agn-label" }, "参考图"),
|
|
1783
|
+
React.createElement("span", { className: "agn-tag" },
|
|
1784
|
+
s.vRefs.length + " / " + MAX_VREFS),
|
|
1785
|
+
s.vRefsBusy ? React.createElement("span", { className: "agn-hint" }, "处理中…") : null,
|
|
1786
|
+
s.vRefs.length > 0
|
|
1787
|
+
? React.createElement("button", {
|
|
1788
|
+
type: "button", className: "agn-btn-ghost", disabled: busy,
|
|
1789
|
+
onClick: () => store.clearVRefs(),
|
|
1790
|
+
}, "清空参考图")
|
|
1791
|
+
: null
|
|
1792
|
+
),
|
|
1793
|
+
React.createElement("div", { className: "agn-ref-strip" },
|
|
1794
|
+
s.vRefs.map((r) => React.createElement("div", {
|
|
1795
|
+
key: r.id,
|
|
1796
|
+
className: "agn-ref",
|
|
1797
|
+
title: r.name + " · " + (r.bytes / 1024).toFixed(0) + " KB · " +
|
|
1798
|
+
(r.from === "result" ? "来自生成结果" : "来自本地"),
|
|
1799
|
+
},
|
|
1800
|
+
React.createElement("img", { src: r.src, alt: r.name }),
|
|
1801
|
+
React.createElement("button", {
|
|
1802
|
+
type: "button", className: "agn-ref-del",
|
|
1803
|
+
title: "移除这张参考图",
|
|
1804
|
+
disabled: busy,
|
|
1805
|
+
onClick: () => store.removeVRef(r.id),
|
|
1806
|
+
}, "×")
|
|
1807
|
+
)),
|
|
1808
|
+
React.createElement("button", {
|
|
1809
|
+
type: "button", className: "agn-ref-add",
|
|
1810
|
+
disabled: busy || vRefsFull,
|
|
1811
|
+
title: "从本地磁盘选择一个或多个图片(PNG / JPEG / WebP / GIF)",
|
|
1812
|
+
onClick: () => {
|
|
1813
|
+
const el = vRefInput.current;
|
|
1814
|
+
if (el !== null) el.click();
|
|
1815
|
+
},
|
|
1816
|
+
}, "+ 本地"),
|
|
1817
|
+
React.createElement("button", {
|
|
1818
|
+
type: "button", className: "agn-ref-add",
|
|
1819
|
+
disabled: busy || vRefsFull || pickableImages.length === 0,
|
|
1820
|
+
title: pickableImages.length === 0 ? "还没有生成过图片" : "从「生成结果」里挑一张",
|
|
1821
|
+
onClick: () => store.toggleVRefPick(),
|
|
1822
|
+
}, "+ 结果")
|
|
1823
|
+
),
|
|
1824
|
+
s.vRefPickOpen
|
|
1825
|
+
? React.createElement("div", { className: "agn-ref-pick" },
|
|
1826
|
+
pickableImages.length === 0
|
|
1827
|
+
? React.createElement("span", { className: "agn-hint" }, "还没有生成过图片")
|
|
1828
|
+
: pickableImages.map((h) => React.createElement("button", {
|
|
1829
|
+
key: "vpick-" + h.filename,
|
|
1830
|
+
type: "button",
|
|
1831
|
+
title: h.filename,
|
|
1832
|
+
onClick: () => store.addVRefFromResult(h),
|
|
1833
|
+
}, React.createElement("img", {
|
|
1834
|
+
src: srcOf(h.filename), alt: h.filename, loading: "lazy",
|
|
1835
|
+
})))
|
|
1836
|
+
)
|
|
1837
|
+
: null,
|
|
1838
|
+
React.createElement("div", { className: "agn-note" },
|
|
1839
|
+
s.vRefs.length > 0
|
|
1840
|
+
? (s.vFirst !== null || s.vLast !== null
|
|
1841
|
+
? "已给关键帧,本次按首尾帧生成,参考图不生效"
|
|
1842
|
+
: "将按参考图生成(" + s.vRefs.length + " 张)——参考风格 / 主体 / 构图")
|
|
1843
|
+
: "可选:给上游 1–5 张参考图;留空 = 纯文本生视频"),
|
|
1844
|
+
s.vRefsError !== "" ? React.createElement("div", { className: "agn-err" }, s.vRefsError) : null
|
|
1845
|
+
)
|
|
1846
|
+
: null,
|
|
1847
|
+
|
|
1436
1848
|
React.createElement("div", { className: "agn-label" }, "提示词"),
|
|
1437
1849
|
React.createElement("div", { className: "agn-presets" },
|
|
1438
1850
|
PROMPT_PRESETS.map((preset) => React.createElement("button", {
|
|
@@ -1539,6 +1951,7 @@ window.__ModuleLoader__.load({
|
|
|
1539
1951
|
+ " · " + shown.size + "/" + shown.ratio
|
|
1540
1952
|
+ (shown.seconds ? " · " + shown.seconds + " 秒" : "")
|
|
1541
1953
|
+ (shown.mode === "keyframe" ? " · 图生视频" : "")
|
|
1954
|
+
+ (shown.mode === "reference" ? " · 参考生视频" : "")
|
|
1542
1955
|
+ (shown.mode === "img2img" ? " · 图生图 " + (shown.refCount || 0) + " 张参考" : "")
|
|
1543
1956
|
+ (shown.seed !== undefined && shown.seed !== null ? " · seed " + shown.seed : "")
|
|
1544
1957
|
+ " · 用时 " + (shown.elapsedSec || shown.wallSec) + "s"),
|
|
@@ -1711,6 +2124,52 @@ window.__ModuleLoader__.load({
|
|
|
1711
2124
|
)
|
|
1712
2125
|
: null,
|
|
1713
2126
|
|
|
2127
|
+
s.cfgOpen
|
|
2128
|
+
? React.createElement("div", { className: "agn-field" },
|
|
2129
|
+
React.createElement("div", { className: "agn-label" }, "图片模型"),
|
|
2130
|
+
React.createElement("select", {
|
|
2131
|
+
className: "agn-input", value: s.cfgDraft.imageModel, disabled: s.cfgSaving,
|
|
2132
|
+
onChange: (e) => store.setCfgDraft("imageModel", e.target.value),
|
|
2133
|
+
},
|
|
2134
|
+
React.createElement("option", { value: "" }, "默认(" + ((s.health && s.health.model) || "agnes-image-2.5-flash") + ")"),
|
|
2135
|
+
((s.health && s.health.models && s.health.models.image) || []).map((m) =>
|
|
2136
|
+
React.createElement("option", { key: m, value: m }, m))
|
|
2137
|
+
),
|
|
2138
|
+
React.createElement("div", { className: "agn-note" }, "只列出当前上游真实存在的图片模型")
|
|
2139
|
+
)
|
|
2140
|
+
: null,
|
|
2141
|
+
|
|
2142
|
+
s.cfgOpen
|
|
2143
|
+
? React.createElement("div", { className: "agn-field" },
|
|
2144
|
+
React.createElement("div", { className: "agn-label" }, "视频模型"),
|
|
2145
|
+
React.createElement("select", {
|
|
2146
|
+
className: "agn-input", value: s.cfgDraft.videoModel, disabled: s.cfgSaving,
|
|
2147
|
+
onChange: (e) => store.setCfgDraft("videoModel", e.target.value),
|
|
2148
|
+
},
|
|
2149
|
+
React.createElement("option", { value: "" }, "默认(" + ((s.health && s.health.videoModel) || "agnes-video-2.5-flash") + ")"),
|
|
2150
|
+
((s.health && s.health.models && s.health.models.video) || []).map((m) =>
|
|
2151
|
+
React.createElement("option", { key: m, value: m }, m))
|
|
2152
|
+
),
|
|
2153
|
+
React.createElement("div", { className: "agn-note" }, "换模型后新提交的任务才会用新模型")
|
|
2154
|
+
)
|
|
2155
|
+
: null,
|
|
2156
|
+
|
|
2157
|
+
s.cfgOpen
|
|
2158
|
+
? React.createElement("div", { className: "agn-field" },
|
|
2159
|
+
React.createElement("button", {
|
|
2160
|
+
type: "button", className: "agn-btn-ghost", disabled: s.checking,
|
|
2161
|
+
title: "用当前 Key 请求一次上游模型清单,确认 Key 能用、网络通",
|
|
2162
|
+
onClick: () => store.checkKey(),
|
|
2163
|
+
}, s.checking ? "连接中…" : "测试连接"),
|
|
2164
|
+
s.checkResult !== null
|
|
2165
|
+
? React.createElement("div", {
|
|
2166
|
+
// 别给 style 传字符串:React 会抛错并卸载整块面板(白屏)
|
|
2167
|
+
className: (s.checkResult.ok ? "agn-okline" : "agn-err") + " agn-check",
|
|
2168
|
+
}, s.checkResult.text)
|
|
2169
|
+
: null
|
|
2170
|
+
)
|
|
2171
|
+
: null,
|
|
2172
|
+
|
|
1714
2173
|
s.cfgOpen
|
|
1715
2174
|
? React.createElement("div", { className: "agn-row" },
|
|
1716
2175
|
React.createElement("button", {
|
|
@@ -1773,7 +2232,7 @@ window.__ModuleLoader__.load({
|
|
|
1773
2232
|
|
|
1774
2233
|
exports.apply = apply;
|
|
1775
2234
|
exports.inject = ["slots"];
|
|
1776
|
-
exports.__internals = { getStore, createStore, srcOf, shrinkImage, MAX_REFS };
|
|
2235
|
+
exports.__internals = { pickableImagesOf, getStore, createStore, srcOf, shrinkImage, MAX_REFS };
|
|
1777
2236
|
return module.exports;
|
|
1778
2237
|
},
|
|
1779
2238
|
});
|