dsh-comfyui-rp 0.2.2 → 0.2.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 +1 -1
- package/docs/CHANGELOG.md +11 -0
- package/lib/api.js +7 -1
- package/lib/client.js +185 -15
- package/lib/comfy.js +24 -1
- package/lib/jobs.js +6 -0
- package/lib/meta.js +1 -1
- package/lib/store.js +7 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -40,7 +40,7 @@ dsh plugin --profile web add git+https://github.com/bmhty123/dsh-comfyui-rp.git
|
|
|
40
40
|
|
|
41
41
|
## 能做什么
|
|
42
42
|
|
|
43
|
-
- **自动出图**:捕获正文 → 解析提示词 → 出图 →
|
|
43
|
+
- **自动出图**:捕获正文 → 解析提示词 → 出图 → 归档,全自动;投递和出图可分开开关。纯黑图会自动换种子重生成,进行中可以随时点「停止自动补图」。
|
|
44
44
|
- **投递**:正文来源和提示词来源分开,可指到另一个会话甚至另一台 DSH;附加消息放正文最上面或最下面,投递成功后自动留空。
|
|
45
45
|
- **看图**:右侧栏面板、全屏、悬浮球、悬浮图片框。翻页、下载、收藏、刷新、删除、圆角;自己拖过 / 缩过之后,切图不再改回初始大小。
|
|
46
46
|
- **跟图**:滚动正文自动切图,连续图按段落比例拆位置,可切光标跟图、手动偏移、清空定位。
|
package/docs/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,16 @@
|
|
|
1
1
|
# 更新记录
|
|
2
2
|
|
|
3
|
+
## 0.2.3
|
|
4
|
+
|
|
5
|
+
- **自动补图可以随时停下**:补图进行中,图片框左下菜单的按钮会变成「终止」(之前因为补图是内部任务,菜单里根本看不到它),点一下清空队列并中止正在跑的那一单;全屏和工作台的「终止」也一样。右侧栏顶部还有一条「停止自动补图」。下一次新任务开始时自动复位。
|
|
6
|
+
- **黑图自动替换开关**:工作流页 →「图片输出相关」→「额外设置」里新增开关,关掉后不再检测纯黑图,待补队列也会清空。
|
|
7
|
+
- **纯黑判定更严,修掉误判**:以前只要求「暗像素 ≥ 99%」就会判黑,夜景、黑底小主体这类图会被误伤。现在必须同时满足:暗像素 ≥ 99.5%、平均亮度 ≤ 8、亮点(亮度 > 110)占比 < 0.1%,并且不透明像素要占到一半以上(透明底的小图不算黑)。
|
|
8
|
+
- **固定种子可手填**:工作流页「每次换随机种子」下面多了一个「固定种子」输入框。
|
|
9
|
+
- 勾着自动时它是灰的,显示**上一轮实际用的种子**(方便看到满意的一张后直接固定下来);
|
|
10
|
+
- 取消勾选后解锁,填的整数会立刻存进工作流,出图时覆盖每一处字面量 seed;取消的那一刻会把上一轮的种子自动带进来。
|
|
11
|
+
- 这些开关改完直接生效,不用再点「保存」。
|
|
12
|
+
|
|
13
|
+
|
|
3
14
|
## 0.2.2
|
|
4
15
|
|
|
5
16
|
- **LoRA 强度可以填负数**:工作台「连续图」的档位、LoRA 页每条的强度都允许 `-5 ~ 5`(反相 LoRA 用)。
|
package/lib/api.js
CHANGED
|
@@ -358,7 +358,7 @@ export function createApi({ store, jobs, hub, getConfig, ctx, sc, routePath, ver
|
|
|
358
358
|
workflows: compact ? [] : store.listWorkflows(),
|
|
359
359
|
activeWorkflowId: cfg.workflowId,
|
|
360
360
|
activeWorkflow: activeWf
|
|
361
|
-
? { id: activeWf.id, name: activeWf.name, size: activeWf.size || null, sizeMode: activeWf.sizeMode || 'manual', randomSeed: activeWf.randomSeed !== false, nodeCount: Object.keys(activeWf.json || {}).length }
|
|
361
|
+
? { id: activeWf.id, name: activeWf.name, size: activeWf.size || null, sizeMode: activeWf.sizeMode || 'manual', randomSeed: activeWf.randomSeed !== false, blackRetry: activeWf.blackRetry !== false, nodeCount: Object.keys(activeWf.json || {}).length }
|
|
362
362
|
: null,
|
|
363
363
|
jobs: jobsList,
|
|
364
364
|
images: imageItems,
|
|
@@ -604,6 +604,9 @@ export function createApi({ store, jobs, hub, getConfig, ctx, sc, routePath, ver
|
|
|
604
604
|
size: wf.size || null,
|
|
605
605
|
sizeMode: wf.sizeMode || 'manual',
|
|
606
606
|
randomSeed: wf.randomSeed !== false,
|
|
607
|
+
seed: wf.seed === null || wf.seed === undefined ? null : Number(wf.seed),
|
|
608
|
+
lastSeed: wf.lastSeed === null || wf.lastSeed === undefined ? null : Number(wf.lastSeed),
|
|
609
|
+
blackRetry: wf.blackRetry !== false,
|
|
607
610
|
pruneOutputs: wf.pruneOutputs !== false,
|
|
608
611
|
onlyFirst: wf.onlyFirst === true,
|
|
609
612
|
loraState: wf.loraState || null,
|
|
@@ -662,12 +665,15 @@ export function createApi({ store, jobs, hub, getConfig, ctx, sc, routePath, ver
|
|
|
662
665
|
const sizeMode = body.sizeMode || wf?.sizeMode || 'manual';
|
|
663
666
|
const loraState = body.loraState !== undefined ? body.loraState : resolveLoraState(wf || {});
|
|
664
667
|
const loraScan = scanLoraStacks(json);
|
|
668
|
+
// 固定种子:干跑也要按工作流页填的那个值算,否则「干跑」和真出一张的结果对不上。
|
|
669
|
+
const seed = body.seed !== undefined ? body.seed : (wf && wf.seed !== undefined ? wf.seed : null);
|
|
665
670
|
const { workflow, report } = applyOverrides(json, {
|
|
666
671
|
positive: String(body.positive ?? ''),
|
|
667
672
|
negative: String(body.negative ?? ''),
|
|
668
673
|
mapping,
|
|
669
674
|
tokenEmpty: 'blank',
|
|
670
675
|
randomSeed: body.randomSeed === true,
|
|
676
|
+
seed,
|
|
671
677
|
loraState,
|
|
672
678
|
size,
|
|
673
679
|
sizeSource: sizeMode,
|
package/lib/client.js
CHANGED
|
@@ -221,6 +221,30 @@ window.__ModuleLoader__.load({
|
|
|
221
221
|
|
|
222
222
|
function clamp(v, lo, hi) { return Math.max(lo, Math.min(hi, Number(v) || 0)); }
|
|
223
223
|
|
|
224
|
+
/**
|
|
225
|
+
* 自动补图的全局状态:逻辑在 FloatBall 里跑(它一直在,负责检测纯黑图),
|
|
226
|
+
* 但「停止」按钮要能出现在右侧栏面板里,所以两边共享这一份。
|
|
227
|
+
* pending = 队列里的 + 正在跑的;stop = FloatBall 注册过来的停止函数。
|
|
228
|
+
*/
|
|
229
|
+
var BLACK_UI = { pending: 0, stop: null, subs: new Set() };
|
|
230
|
+
function setBlackPendingGlobal(n) {
|
|
231
|
+
var value = Math.max(0, Number(n) || 0);
|
|
232
|
+
if (BLACK_UI.pending === value) return;
|
|
233
|
+
BLACK_UI.pending = value;
|
|
234
|
+
BLACK_UI.subs.forEach(function (fn) { try { fn(value); } catch (e) { /* ignore */ } });
|
|
235
|
+
}
|
|
236
|
+
function useBlackPending() {
|
|
237
|
+
var [pending, setPending] = useState(BLACK_UI.pending);
|
|
238
|
+
useEffect(function () {
|
|
239
|
+
var fn = function (n) { setPending(n); };
|
|
240
|
+
BLACK_UI.subs.add(fn);
|
|
241
|
+
setPending(BLACK_UI.pending);
|
|
242
|
+
return function () { BLACK_UI.subs.delete(fn); };
|
|
243
|
+
}, []);
|
|
244
|
+
return pending;
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
|
|
224
248
|
function loadTheme() {
|
|
225
249
|
try { return localStorage.getItem(LS_THEME) === "light" ? "light" : "dark"; } catch (e) { return "dark"; }
|
|
226
250
|
}
|
|
@@ -504,6 +528,8 @@ window.__ModuleLoader__.load({
|
|
|
504
528
|
:root{--ac-bg:#151517;--ac-bg2:#1b1b1d;--ac-card:#1b1b1d;--ac-raised:#2a2a2c;--ac-line:#303033;--ac-line2:#3a3a3d;--ac-text:#ececee;--ac-text2:#c6c6ca;--ac-muted:#a8a8ad;--ac-dim:#8a8a90;--ac-accent:#b3bfd0;--ac-accent2:#dbe1ea;--ac-ok:#6fbf8f;--ac-warn:#d1ac72;--ac-err:#d18a8a;--ac-fs:12.5px;--ac-fs-s:11.5px;--ac-fs-l:11px;}
|
|
505
529
|
.ac-root{--ac-bg:#151517;--ac-bg2:#1b1b1d;--ac-card:#1b1b1d;--ac-raised:#2a2a2c;--ac-line:#303033;--ac-line2:#3a3a3d;--ac-text:#ececee;--ac-text2:#c6c6ca;--ac-muted:#a8a8ad;--ac-dim:#8a8a90;--ac-accent:#b3bfd0;--ac-accent2:#dbe1ea;--ac-ok:#6fbf8f;--ac-warn:#d1ac72;--ac-err:#d18a8a;--ac-fs:12.5px;--ac-fs-s:11.5px;--ac-fs-l:11px;box-sizing:border-box;height:100%;min-height:0;display:flex;flex-direction:column;background:var(--ac-bg);color:var(--ac-text);font-size:var(--ac-fs);line-height:1.5;font-family:var(--ac-font,inherit);color-scheme:dark;}
|
|
506
530
|
.ac-root *{box-sizing:border-box;}
|
|
531
|
+
.ac-blackbar{display:flex;align-items:center;gap:8px;padding:7px 10px;background:rgba(96,72,30,.5);border-bottom:1px solid var(--ac-line);color:var(--ac-text);font-size:var(--ac-fs-s);flex:0 0 auto;}
|
|
532
|
+
.ac-blackbar .ac-blackbar-txt{flex:1 1 auto;min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;}
|
|
507
533
|
.ac-ballroot,.ac-ballroot *,.ac-overlay,.ac-overlay *{box-sizing:border-box;}
|
|
508
534
|
.ac-root ::selection{background:rgba(130,150,180,.35);}
|
|
509
535
|
.ac-root ::-webkit-scrollbar{width:8px;height:8px;}
|
|
@@ -1758,7 +1784,10 @@ window.__ModuleLoader__.load({
|
|
|
1758
1784
|
var shown = view || img || null;
|
|
1759
1785
|
if (shown && shown.id) lastFrameView = shown;
|
|
1760
1786
|
}, [view && view.id, view && view.url, img && img.id, img && img.url]);
|
|
1761
|
-
|
|
1787
|
+
// 黑图补图是 internal 任务,不在 visibleJobs 里,所以「有没有在跑」要单看这个计数:
|
|
1788
|
+
// 补图进行中也要能把菜单里的「请求生成」换成「终止」。
|
|
1789
|
+
var blackActive = useBlackPending() > 0;
|
|
1790
|
+
var live = blackActive || !!(job && (job.status === "delivering" || job.status === "parsing" || job.status === "running" || job.status === "queued"));
|
|
1762
1791
|
// anchor / turn 也进 key:旧图回填锚点后 id 不变,但跟随逻辑必须重建,
|
|
1763
1792
|
// 否则监听器里还是“没有锚点”的旧 imgs。
|
|
1764
1793
|
var imgKey = imgs.map(function (x) {
|
|
@@ -3693,14 +3722,45 @@ window.__ModuleLoader__.load({
|
|
|
3693
3722
|
});
|
|
3694
3723
|
}, "重新生成");
|
|
3695
3724
|
};
|
|
3725
|
+
/**
|
|
3726
|
+
* 纯黑判定(纯函数,便于自检):从缩放后的 RGBA 采样里算,三条同时满足才算黑:
|
|
3727
|
+
* ① 暗像素(亮度 < 14)占比 ≥ 99.5%
|
|
3728
|
+
* ② 平均亮度 ≤ 8
|
|
3729
|
+
* ③ 亮点(亮度 > 110)占比 < 0.1%
|
|
3730
|
+
* 只满足 ① 会误伤夜景 / 黑底小主体——之前就是这么误判的。
|
|
3731
|
+
* 另外要求不透明像素覆盖 ≥ 50%:透明底的小图不是“黑图”,是透明图。
|
|
3732
|
+
*/
|
|
3733
|
+
function judgeNearlyBlack(d) {
|
|
3734
|
+
var pixels = d.length / 4;
|
|
3735
|
+
var opaque = 0;
|
|
3736
|
+
var dark = 0;
|
|
3737
|
+
var bright = 0;
|
|
3738
|
+
var lumSum = 0;
|
|
3739
|
+
for (var i = 0; i < d.length; i += 4) {
|
|
3740
|
+
if (d[i + 3] < 16) continue; // 透明像素不算
|
|
3741
|
+
opaque += 1;
|
|
3742
|
+
var lum = 0.2126 * d[i] + 0.7152 * d[i + 1] + 0.0722 * d[i + 2];
|
|
3743
|
+
lumSum += lum;
|
|
3744
|
+
if (lum < 14) dark += 1;
|
|
3745
|
+
else if (lum > 110) bright += 1;
|
|
3746
|
+
}
|
|
3747
|
+
var darkRatio = opaque ? dark / opaque : 0;
|
|
3748
|
+
var brightRatio = opaque ? bright / opaque : 0;
|
|
3749
|
+
var meanLum = opaque ? lumSum / opaque : 0;
|
|
3750
|
+
var coverage = pixels ? opaque / pixels : 0;
|
|
3751
|
+
var black = opaque >= 8 && coverage >= 0.5
|
|
3752
|
+
&& darkRatio >= 0.995 && meanLum <= 8 && brightRatio < 0.001;
|
|
3753
|
+
return { black: black, darkRatio: darkRatio, brightRatio: brightRatio, meanLum: meanLum, coverage: coverage, opaque: opaque };
|
|
3754
|
+
}
|
|
3696
3755
|
var isNearlyBlack = function (url) {
|
|
3697
3756
|
return new Promise(function (resolve) {
|
|
3698
3757
|
if (!url || typeof document === "undefined" || typeof Image === "undefined") return resolve(false);
|
|
3699
3758
|
var probe = new Image();
|
|
3700
3759
|
probe.onload = function () {
|
|
3701
3760
|
try {
|
|
3702
|
-
|
|
3703
|
-
var
|
|
3761
|
+
if (!probe.naturalWidth || !probe.naturalHeight) return resolve(false);
|
|
3762
|
+
var w = Math.max(1, Math.min(256, probe.naturalWidth));
|
|
3763
|
+
var h = Math.max(1, Math.min(256, probe.naturalHeight));
|
|
3704
3764
|
var canvas = document.createElement("canvas");
|
|
3705
3765
|
canvas.width = w;
|
|
3706
3766
|
canvas.height = h;
|
|
@@ -3708,14 +3768,7 @@ window.__ModuleLoader__.load({
|
|
|
3708
3768
|
if (!g) return resolve(false);
|
|
3709
3769
|
g.drawImage(probe, 0, 0, w, h);
|
|
3710
3770
|
var d = g.getImageData(0, 0, w, h).data;
|
|
3711
|
-
|
|
3712
|
-
var black = 0;
|
|
3713
|
-
for (var i = 0; i < d.length; i += 4) {
|
|
3714
|
-
if (d[i + 3] < 16) continue; // 透明像素不算黑
|
|
3715
|
-
total += 1;
|
|
3716
|
-
if (d[i] < 16 && d[i + 1] < 16 && d[i + 2] < 16) black += 1;
|
|
3717
|
-
}
|
|
3718
|
-
resolve(total > 0 && black / total >= 0.99);
|
|
3771
|
+
resolve(judgeNearlyBlack(d).black);
|
|
3719
3772
|
} catch (e) { resolve(false); }
|
|
3720
3773
|
};
|
|
3721
3774
|
probe.onerror = function () { resolve(false); };
|
|
@@ -3723,6 +3776,27 @@ window.__ModuleLoader__.load({
|
|
|
3723
3776
|
});
|
|
3724
3777
|
};
|
|
3725
3778
|
var blackQueueRef = useRef([]);
|
|
3779
|
+
// 自动补图可以随时叫停:队列清空 + 正在跑的那单 abort,并且这一轮不再自动补。
|
|
3780
|
+
// blackStopRef 在下一轮新任务完成时复位,避免「点过一次停止后就再也不补」。
|
|
3781
|
+
var blackStopRef = useRef(false);
|
|
3782
|
+
var blackJobRef = useRef("");
|
|
3783
|
+
var syncBlackPending = function () {
|
|
3784
|
+
setBlackPendingGlobal(blackQueueRef.current.length + (blackBusyRef.current ? 1 : 0));
|
|
3785
|
+
};
|
|
3786
|
+
var stopBlack = function () {
|
|
3787
|
+
blackStopRef.current = true;
|
|
3788
|
+
blackQueueRef.current = [];
|
|
3789
|
+
var jid = blackJobRef.current;
|
|
3790
|
+
blackJobRef.current = "";
|
|
3791
|
+
syncBlackPending();
|
|
3792
|
+
showToast("已停止自动补图", "ok");
|
|
3793
|
+
if (jid) api.abort(jid).catch(function () { /* 任务可能已经结束了 */ });
|
|
3794
|
+
};
|
|
3795
|
+
// 面板上的「停止自动补图」通过这个入口回调过来。
|
|
3796
|
+
useEffect(function () {
|
|
3797
|
+
BLACK_UI.stop = stopBlack;
|
|
3798
|
+
return function () { if (BLACK_UI.stop === stopBlack) BLACK_UI.stop = null; };
|
|
3799
|
+
}, []);
|
|
3726
3800
|
/** 同一张图(同一套提示词)跨多次自动换种子仍用同一个 key,用来限制最多补几次。 */
|
|
3727
3801
|
var blackSlotKey = function (sourceJob, im) {
|
|
3728
3802
|
var sets = Array.isArray(sourceJob && sourceJob.sets) ? sourceJob.sets : [];
|
|
@@ -3738,16 +3812,20 @@ window.__ModuleLoader__.load({
|
|
|
3738
3812
|
};
|
|
3739
3813
|
var enqueueBlack = function (im, sourceJob, attempt, key, label) {
|
|
3740
3814
|
if (!im || !im.id || !im.url) return;
|
|
3815
|
+
if (blackStopRef.current) return;
|
|
3741
3816
|
blackQueueRef.current.push({
|
|
3742
3817
|
id: String(im.id), jobId: im.jobId || (sourceJob && sourceJob.id) || "",
|
|
3743
3818
|
setIndex: im.setIndex, url: im.url, attempt: attempt || 1, key: key || "", label: label || "",
|
|
3744
3819
|
});
|
|
3820
|
+
syncBlackPending();
|
|
3745
3821
|
};
|
|
3746
3822
|
var drainBlackQueue = function () {
|
|
3747
3823
|
if (blackBusyRef.current) return;
|
|
3748
3824
|
var next = blackQueueRef.current.shift();
|
|
3749
|
-
if (!next) return;
|
|
3825
|
+
if (!next) { syncBlackPending(); return; }
|
|
3826
|
+
if (blackStopRef.current) { blackQueueRef.current = []; syncBlackPending(); return; }
|
|
3750
3827
|
blackBusyRef.current = true;
|
|
3828
|
+
syncBlackPending();
|
|
3751
3829
|
isNearlyBlack(next.url).then(function (black) {
|
|
3752
3830
|
if (!black) return null;
|
|
3753
3831
|
if (next.attempt === 1) {
|
|
@@ -3767,17 +3845,32 @@ window.__ModuleLoader__.load({
|
|
|
3767
3845
|
mode: "auto",
|
|
3768
3846
|
sessionId: data && data.activeSessionId || "",
|
|
3769
3847
|
sessionTitle: data && data.activeSessionTitle || "",
|
|
3848
|
+
}).then(function (r) {
|
|
3849
|
+
blackJobRef.current = r && r.job && r.job.id ? String(r.job.id) : "";
|
|
3850
|
+
return r;
|
|
3770
3851
|
});
|
|
3771
3852
|
}).catch(function () { /* 黑图重生成失败不阻塞其它图 */ }).then(function () {
|
|
3772
3853
|
blackBusyRef.current = false;
|
|
3854
|
+
blackJobRef.current = "";
|
|
3855
|
+
syncBlackPending();
|
|
3773
3856
|
drainBlackQueue();
|
|
3774
3857
|
});
|
|
3775
3858
|
};
|
|
3776
3859
|
var checkBlackImages = function (sourceJob) {
|
|
3777
3860
|
if (!sourceJob || sourceJob.status !== "done") return;
|
|
3861
|
+
// 工作流页「图片输出相关 → 黑图自动替换」关掉就整个不检测(连已在队列里的也清掉)。
|
|
3862
|
+
if (data && data.activeWorkflow && data.activeWorkflow.blackRetry === false) {
|
|
3863
|
+
if (blackQueueRef.current.length || blackBusyRef.current) {
|
|
3864
|
+
blackQueueRef.current = [];
|
|
3865
|
+
syncBlackPending();
|
|
3866
|
+
}
|
|
3867
|
+
return;
|
|
3868
|
+
}
|
|
3778
3869
|
// 只处理刚完成的这一轮;翻旧记录 / 重启页面不追溯历史图。
|
|
3779
3870
|
var finished = Date.parse(sourceJob.finishedAt || "");
|
|
3780
3871
|
if (!Number.isFinite(finished) || Date.now() - finished > 120000) return;
|
|
3872
|
+
// 新一轮(非自动补图)开始时解除上一次的「停止」,否则停过一次就再也不会补了。
|
|
3873
|
+
if (sourceJob.regenMode !== "auto") blackStopRef.current = false;
|
|
3781
3874
|
|
|
3782
3875
|
if (sourceJob.regenMode === "auto") {
|
|
3783
3876
|
// 自动补图后继续检查:仍然黑就再换种子,最多 BLACK_MAX_RETRIES 次。
|
|
@@ -3821,6 +3914,12 @@ window.__ModuleLoader__.load({
|
|
|
3821
3914
|
drainBlackQueue();
|
|
3822
3915
|
};
|
|
3823
3916
|
var stopJob = function () {
|
|
3917
|
+
// 图片框左下菜单的「终止」就是真正的终止:黑图补图是 internal 任务,
|
|
3918
|
+
// 一起停掉(清队列 + 中止正在跑的那单),否则点了终止它还在后台接着补。
|
|
3919
|
+
if (blackQueueRef.current.length || blackBusyRef.current) {
|
|
3920
|
+
stopBlack();
|
|
3921
|
+
return;
|
|
3922
|
+
}
|
|
3824
3923
|
if (!job || !job.id) return;
|
|
3825
3924
|
act(function () {
|
|
3826
3925
|
return api.abort(job.id).then(function () { showToast("已终止", "ok"); });
|
|
@@ -4547,6 +4646,8 @@ window.__ModuleLoader__.load({
|
|
|
4547
4646
|
});
|
|
4548
4647
|
var [toasts, setToasts] = useState([]);
|
|
4549
4648
|
var [conn, setConn] = useState(true);
|
|
4649
|
+
// 自动补图是 FloatBall 在跑,这里只订阅「还有几个在补」用来显示停止条。
|
|
4650
|
+
var blackPending = useBlackPending();
|
|
4550
4651
|
var [viewer, setViewer] = useState(null);
|
|
4551
4652
|
// 切到右侧栏其它标签时 BridgePanel 会卸载;memory 让回来时还在同一张图。
|
|
4552
4653
|
var bootSession = props && typeof props.sessionId === "string" ? props.sessionId : "";
|
|
@@ -5441,6 +5542,8 @@ window.__ModuleLoader__.load({
|
|
|
5441
5542
|
}).catch(function (e) { showToast("重新生成失败:" + (e.message || e), "err"); });
|
|
5442
5543
|
};
|
|
5443
5544
|
var fullStop = function () {
|
|
5545
|
+
// 全屏里的「终止」也一样:先把自动补图停掉,再中止当前任务。
|
|
5546
|
+
if (BLACK_UI.stop) BLACK_UI.stop();
|
|
5444
5547
|
if (!liveJob || !liveJob.id) return;
|
|
5445
5548
|
Promise.resolve().then(function () { return api.abort(liveJob.id); }).then(function () { showToast("已终止", "ok"); }).catch(function (e) { showToast("终止失败:" + (e.message || e), "err"); });
|
|
5446
5549
|
};
|
|
@@ -5809,6 +5912,11 @@ window.__ModuleLoader__.load({
|
|
|
5809
5912
|
};
|
|
5810
5913
|
|
|
5811
5914
|
return h("div", { className: cls("ac-root", fullImage && "fullimg", theme === "light" && "light") },
|
|
5915
|
+
blackPending > 0 ? h("div", { className: "ac-blackbar" },
|
|
5916
|
+
h("span", { className: "ac-blackbar-txt" },
|
|
5917
|
+
"检测到纯黑图,正在自动重新生成" + (blackPending > 1 ? "(队列 " + blackPending + " 个)" : "")),
|
|
5918
|
+
h(Btn, { small: true, variant: "danger", onClick: function () { if (BLACK_UI.stop) BLACK_UI.stop(); } }, "停止自动补图"),
|
|
5919
|
+
) : null,
|
|
5812
5920
|
fullImage
|
|
5813
5921
|
? h("div", {
|
|
5814
5922
|
className: cls("ac-fullview", square && "square"),
|
|
@@ -6843,7 +6951,7 @@ window.__ModuleLoader__.load({
|
|
|
6843
6951
|
h("div", { className: "ac-wfbar" },
|
|
6844
6952
|
h("span", { className: "ac-wfname", title: wf ? wf.name : "未选择" }, "工作流:" + (wf ? wf.name : "未选择")),
|
|
6845
6953
|
h("span", { style: { flex: 1 } }),
|
|
6846
|
-
job && job.cancellable ? h(Btn, { small: true, variant: "danger", onClick: function () { api.abort(job.id).then(function () { props.toast("已终止", "ok"); }).catch(function (e) { props.toast("终止失败:" + (e.message || e), "err"); }); } }, "终止") : null,
|
|
6954
|
+
job && job.cancellable ? h(Btn, { small: true, variant: "danger", onClick: function () { if (BLACK_UI.stop) BLACK_UI.stop(); api.abort(job.id).then(function () { props.toast("已终止", "ok"); }).catch(function (e) { props.toast("终止失败:" + (e.message || e), "err"); }); } }, "终止") : null,
|
|
6847
6955
|
h(Btn, {
|
|
6848
6956
|
variant: "primary", className: "ac-genbtn",
|
|
6849
6957
|
disabled: busy || !sets.length || !sets.some(function (s) { return s.positive; }) || !!(job && job.cancellable),
|
|
@@ -7363,6 +7471,21 @@ window.__ModuleLoader__.load({
|
|
|
7363
7471
|
var [dry, setDry] = useState(props.initialDry || null);
|
|
7364
7472
|
var [jsonOpen, setJsonOpen] = useState(props.initialJsonOpen === true);
|
|
7365
7473
|
var fileRef = useRef(null);
|
|
7474
|
+
// 「固定种子」文本框:null = 不在编辑(显示自动 / 已保存的值),字符串 = 正在输入的原文。
|
|
7475
|
+
var [seedDraft, setSeedDraft] = useState(null);
|
|
7476
|
+
var seedTimer = useRef(null);
|
|
7477
|
+
useEffect(function () {
|
|
7478
|
+
return function () { if (seedTimer.current) clearTimeout(seedTimer.current); };
|
|
7479
|
+
}, []);
|
|
7480
|
+
// 种子这类开关不走「保存」按钮也会立刻落库,否则改完忘了保存,出图还是老种子。
|
|
7481
|
+
var persistWf = function (p) {
|
|
7482
|
+
patch(p);
|
|
7483
|
+
if (!wf || !wf.id) return;
|
|
7484
|
+
if (seedTimer.current) clearTimeout(seedTimer.current);
|
|
7485
|
+
seedTimer.current = setTimeout(function () {
|
|
7486
|
+
api.updateWorkflow(wf.id, p).catch(function () { /* 落库失败不阻塞界面 */ });
|
|
7487
|
+
}, 400);
|
|
7488
|
+
};
|
|
7366
7489
|
|
|
7367
7490
|
var loadWf = function (wid) {
|
|
7368
7491
|
if (!wid) { setWf(null); setScan(null); setJsonText(""); return Promise.resolve(null); }
|
|
@@ -7425,6 +7548,7 @@ window.__ModuleLoader__.load({
|
|
|
7425
7548
|
size: wf ? wf.size : null,
|
|
7426
7549
|
sizeMode: wf ? wf.sizeMode : "manual",
|
|
7427
7550
|
randomSeed: wf ? wf.randomSeed : true,
|
|
7551
|
+
seed: wf && wf.seed !== undefined && wf.seed !== null && Number.isFinite(Number(wf.seed)) ? Math.round(Number(wf.seed)) : null,
|
|
7428
7552
|
pruneOutputs: wf ? wf.pruneOutputs : true,
|
|
7429
7553
|
onlyFirst: true,
|
|
7430
7554
|
};
|
|
@@ -7455,7 +7579,7 @@ window.__ModuleLoader__.load({
|
|
|
7455
7579
|
doSave(undefined, name);
|
|
7456
7580
|
};
|
|
7457
7581
|
var create = function () {
|
|
7458
|
-
setWf({ id: "", name: "新工作流", json: {}, mapping: {}, mappingSeeded: false, mappingVersion: 0, outputNodes: [], outputSelectionVersion: 2, size: null, sizeMode: "manual", randomSeed: true, pruneOutputs: true, onlyFirst: false, loraState: null });
|
|
7582
|
+
setWf({ id: "", name: "新工作流", json: {}, mapping: {}, mappingSeeded: false, mappingVersion: 0, outputNodes: [], outputSelectionVersion: 2, size: null, sizeMode: "manual", randomSeed: true, seed: null, lastSeed: null, pruneOutputs: true, onlyFirst: false, loraState: null });
|
|
7459
7583
|
setId("");
|
|
7460
7584
|
setJsonText("{}");
|
|
7461
7585
|
setScan(null);
|
|
@@ -7479,6 +7603,32 @@ window.__ModuleLoader__.load({
|
|
|
7479
7603
|
var patch = function (p) {
|
|
7480
7604
|
setWf(function (old) { return old ? Object.assign({}, old, p) : old; });
|
|
7481
7605
|
};
|
|
7606
|
+
// 固定种子:自动模式只读,显示上一轮实际用的种子;取消自动后可以手填,
|
|
7607
|
+
// 填的数字会随保存写进工作流,出图时覆盖每一处字面量 seed。
|
|
7608
|
+
var seedAuto = !wf || wf.randomSeed !== false;
|
|
7609
|
+
var seedDisplay = seedAuto
|
|
7610
|
+
? (wf && wf.lastSeed !== null && wf.lastSeed !== undefined ? String(wf.lastSeed) : "")
|
|
7611
|
+
: (wf && wf.seed !== null && wf.seed !== undefined ? String(wf.seed) : "");
|
|
7612
|
+
var editSeed = function (value) {
|
|
7613
|
+
setSeedDraft(value);
|
|
7614
|
+
var n = Number(value);
|
|
7615
|
+
if (String(value).trim() !== "" && isFinite(n) && n >= 0) patch({ seed: Math.round(n) });
|
|
7616
|
+
};
|
|
7617
|
+
var commitSeed = function (value) {
|
|
7618
|
+
setSeedDraft(null);
|
|
7619
|
+
var raw = String(value === undefined || value === null ? "" : value).trim();
|
|
7620
|
+
if (raw === "") { persistWf({ seed: null }); return; }
|
|
7621
|
+
var n = Number(raw);
|
|
7622
|
+
if (!isFinite(n)) { persistWf({ seed: null }); return; }
|
|
7623
|
+
persistWf({ seed: Math.max(0, Math.min(9007199254740991, Math.round(n))) });
|
|
7624
|
+
};
|
|
7625
|
+
var toggleRandomSeed = function (on) {
|
|
7626
|
+
if (on) { persistWf({ randomSeed: true }); return; }
|
|
7627
|
+
// 取消自动时顺手把「上一轮用的种子」带过来,省得再手敲一遍。
|
|
7628
|
+
var carry = wf.seed !== null && wf.seed !== undefined ? wf.seed
|
|
7629
|
+
: (wf.lastSeed !== null && wf.lastSeed !== undefined ? wf.lastSeed : null);
|
|
7630
|
+
persistWf({ randomSeed: false, seed: carry });
|
|
7631
|
+
};
|
|
7482
7632
|
var mapping = (wf && wf.mapping) || {};
|
|
7483
7633
|
var setMapping = function (nodeId, role) {
|
|
7484
7634
|
var next = Object.assign({}, mapping);
|
|
@@ -7787,11 +7937,31 @@ window.__ModuleLoader__.load({
|
|
|
7787
7937
|
h("div", { className: "ac-subtitle", style: { marginTop: 12 } }, "额外设置"),
|
|
7788
7938
|
h("div", { className: "ac-form" },
|
|
7789
7939
|
h("div", { className: "ac-row" },
|
|
7790
|
-
h(Switch, { checked: wf.randomSeed !== false, onChange: function (e) {
|
|
7940
|
+
h(Switch, { checked: wf.randomSeed !== false, onChange: function (e) { toggleRandomSeed(e.target.checked); } }, "每次换随机种子"),
|
|
7941
|
+
),
|
|
7942
|
+
h("div", { className: "ac-row" },
|
|
7943
|
+
h(Field, { label: "固定种子" }, h(TextInput, {
|
|
7944
|
+
value: seedDraft !== null ? seedDraft : seedDisplay,
|
|
7945
|
+
disabled: seedAuto,
|
|
7946
|
+
placeholder: seedAuto ? "自动(显示上一轮用的种子)" : "填一个整数,每轮都用它",
|
|
7947
|
+
title: seedAuto
|
|
7948
|
+
? "自动换种子时只读:显示上一轮实际用的种子;取消上面勾选即可编辑"
|
|
7949
|
+
: "取消自动后,这里填的数字会写进工作流,每轮都用它",
|
|
7950
|
+
onChange: function (e) { editSeed(e.target.value); },
|
|
7951
|
+
onBlur: function (e) { commitSeed(e.target.value); },
|
|
7952
|
+
onKeyDown: function (e) { if (e.key === "Enter") e.currentTarget.blur(); },
|
|
7953
|
+
})),
|
|
7954
|
+
h("span", { className: "ac-hint" }, seedAuto ? "自动:每轮随机" : "固定:每轮都用这个种子"),
|
|
7791
7955
|
),
|
|
7792
7956
|
h("div", { className: "ac-row" },
|
|
7793
7957
|
h(Switch, { checked: wf.pruneOutputs !== false, onChange: function (e) { patch({ pruneOutputs: e.target.checked }); } }, "提交前裁剪输出节点(只提交上面勾选的)"),
|
|
7794
7958
|
),
|
|
7959
|
+
h("div", { className: "ac-row" },
|
|
7960
|
+
h(Switch, { checked: wf.blackRetry !== false, onChange: function (e) { persistWf({ blackRetry: e.target.checked }); } }, "黑图自动替换"),
|
|
7961
|
+
h("span", { className: "ac-hint" }, wf.blackRetry !== false
|
|
7962
|
+
? "出图后检测到纯黑图就自动换种子重生成;进行中可在图片框菜单点「终止」"
|
|
7963
|
+
: "已关闭:纯黑图不再自动替换"),
|
|
7964
|
+
),
|
|
7795
7965
|
),
|
|
7796
7966
|
),
|
|
7797
7967
|
),
|
package/lib/comfy.js
CHANGED
|
@@ -631,7 +631,7 @@ function countOccurrences(hay, needle) {
|
|
|
631
631
|
*/
|
|
632
632
|
export function applyOverrides(
|
|
633
633
|
workflow,
|
|
634
|
-
{ positive = '', negative = '', mapping = {}, tokenEmpty = 'blank', randomSeed = true, loraState = null, size = null, sizeSource = '' } = {},
|
|
634
|
+
{ positive = '', negative = '', mapping = {}, tokenEmpty = 'blank', randomSeed = true, seed = null, loraState = null, size = null, sizeSource = '' } = {},
|
|
635
635
|
) {
|
|
636
636
|
let wf = structuredClone(workflow);
|
|
637
637
|
const report = {
|
|
@@ -978,6 +978,29 @@ export function applyOverrides(
|
|
|
978
978
|
});
|
|
979
979
|
}
|
|
980
980
|
}
|
|
981
|
+
} else if (seed !== null && seed !== undefined && Number.isFinite(Number(seed))) {
|
|
982
|
+
// 固定种子:把每一处字面量 seed 都写成用户填的这个值(工作台「固定种子」文本框)。
|
|
983
|
+
// 只动和上面同一类节点,连线出来的种子不碰。
|
|
984
|
+
const fixed = Math.round(Number(seed));
|
|
985
|
+
for (const [nodeId, node] of Object.entries(wf)) {
|
|
986
|
+
if (!node || typeof node !== 'object' || !node.inputs) continue;
|
|
987
|
+
if (!SEED_NODE_RE.test(String(node.class_type || ''))) continue;
|
|
988
|
+
for (const key of Object.keys(node.inputs)) {
|
|
989
|
+
if (key !== 'seed' && key !== 'noise_seed' && key !== 'rand_seed') continue;
|
|
990
|
+
const before = node.inputs[key];
|
|
991
|
+
if (typeof before !== 'number' || !Number.isFinite(before)) continue;
|
|
992
|
+
node.inputs[key] = fixed;
|
|
993
|
+
report.seeds.push({
|
|
994
|
+
nodeId,
|
|
995
|
+
classType: node.class_type,
|
|
996
|
+
title: node?._meta?.title || node?.title || '',
|
|
997
|
+
inputKey: key,
|
|
998
|
+
before,
|
|
999
|
+
after: fixed,
|
|
1000
|
+
fixed: true,
|
|
1001
|
+
});
|
|
1002
|
+
}
|
|
1003
|
+
}
|
|
981
1004
|
}
|
|
982
1005
|
|
|
983
1006
|
/* ── 第 4 步:LoRA 开关 ──
|
package/lib/jobs.js
CHANGED
|
@@ -578,6 +578,7 @@ export function createJobsEngine({ ctx, sc, store, hub, getConfig, getActiveSess
|
|
|
578
578
|
mapping: wf.mapping || {},
|
|
579
579
|
tokenEmpty: 'blank',
|
|
580
580
|
randomSeed: job?.forceRandomSeed === true || wf.randomSeed !== false,
|
|
581
|
+
seed: wf.seed === null || wf.seed === undefined ? null : Number(wf.seed),
|
|
581
582
|
loraState: set.continuousShot
|
|
582
583
|
? withLoraStrength(wf, resolveLoraState(wf), set.continuousShot)
|
|
583
584
|
: resolveLoraState(wf),
|
|
@@ -743,6 +744,11 @@ export function createJobsEngine({ ctx, sc, store, hub, getConfig, getActiveSess
|
|
|
743
744
|
const wfN = prepared.workflow;
|
|
744
745
|
if (index === 0) job.overrideReport = prepared.report;
|
|
745
746
|
const usedSeeds = seedsOfWorkflow(wfN);
|
|
747
|
+
// 把这一轮实际用的种子记到工作流上:工作流页「固定种子」那一栏在自动模式下
|
|
748
|
+
// 显示的就是它,用户看到满意的一张可以直接取消自动、把这颗种子固定下来。
|
|
749
|
+
if (index === 0 && wf.id && usedSeeds.length) {
|
|
750
|
+
try { store.patchWorkflow(wf.id, { lastSeed: usedSeeds[0].value }); } catch { /* 记录失败不影响出图 */ }
|
|
751
|
+
}
|
|
746
752
|
if (runSets.length > 1) log('info', `第 ${index + 1}/${runSets.length} 套:排队提交`, { jobId: job.id });
|
|
747
753
|
const { promptId } = await queuePrompt(cfg.comfy.baseUrl, wfN, CLIENT_ID);
|
|
748
754
|
job.comfyPromptId = promptId;
|
package/lib/meta.js
CHANGED
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
*/
|
|
7
7
|
export const PACKAGE_NAME = 'dsh-comfyui-rp';
|
|
8
8
|
export const PLUGIN_NAME = 'dsh-comfyui-rp';
|
|
9
|
-
export const VERSION = '0.2.
|
|
9
|
+
export const VERSION = '0.2.3';
|
|
10
10
|
|
|
11
11
|
/** 对外路由前缀。不能带尾斜杠(dsh-host-webserver 的 prefix 匹配规则)。 */
|
|
12
12
|
export const ROUTE_PATH = '/__airp_comfy';
|
package/lib/store.js
CHANGED
|
@@ -483,6 +483,9 @@ export function createStore(dataDir) {
|
|
|
483
483
|
size: null,
|
|
484
484
|
sizeMode: 'manual',
|
|
485
485
|
randomSeed: true,
|
|
486
|
+
seed: null,
|
|
487
|
+
lastSeed: null,
|
|
488
|
+
blackRetry: true,
|
|
486
489
|
pruneOutputs: true,
|
|
487
490
|
onlyFirst: false,
|
|
488
491
|
loraState: null,
|
|
@@ -553,6 +556,9 @@ export function createStore(dataDir) {
|
|
|
553
556
|
size: null,
|
|
554
557
|
sizeMode: 'manual',
|
|
555
558
|
randomSeed: true,
|
|
559
|
+
seed: null,
|
|
560
|
+
lastSeed: null,
|
|
561
|
+
blackRetry: true,
|
|
556
562
|
pruneOutputs: true,
|
|
557
563
|
onlyFirst: false,
|
|
558
564
|
loraState: null,
|
|
@@ -577,7 +583,7 @@ export function createStore(dataDir) {
|
|
|
577
583
|
const w = api.getWorkflow(id);
|
|
578
584
|
if (!w) return null;
|
|
579
585
|
const allow = ['name', 'json', 'mapping', 'mappingSeeded', 'mappingVersion', 'outputNodes', 'outputSelectionVersion', 'tokenEmpty', 'size', 'sizeMode',
|
|
580
|
-
'randomSeed', 'pruneOutputs', 'onlyFirst', 'loraState'];
|
|
586
|
+
'randomSeed', 'seed', 'lastSeed', 'pruneOutputs', 'onlyFirst', 'loraState', 'blackRetry'];
|
|
581
587
|
for (const k of allow) {
|
|
582
588
|
if (patch && Object.prototype.hasOwnProperty.call(patch, k)) w[k] = k === 'tokenEmpty' ? 'blank' : patch[k];
|
|
583
589
|
}
|
package/package.json
CHANGED