5 Commits
Author SHA1 Message Date
るみなー先生 808dc14bc0 RELEASE_NOTES.mdのバージョン表記を修正 2026-05-21 02:30:53 +09:00
るみなー先生 08c8111c89 READMEとRELEASE_NOTESを更新 2026-05-11 02:27:56 +09:00
るみなー先生 4a55a946bb Enhance README with Checkpoint Name Cycler details
Added a new section about the Checkpoint Name Cycler integration and updated the usage instructions and images.
2026-05-11 02:17:23 +09:00
るみなー先生 dfaeddbbc7 .gitignoreファイルを追加 2026-05-11 00:38:19 +09:00
るみなー先生 90450c4fd5 ComfyUI-CheckpointNameCycler対応を追加 2026-05-10 00:29:02 +09:00
5 changed files with 232 additions and 94 deletions
+22
View File
@@ -0,0 +1,22 @@
# --- Python関連 ---
# これがJavaの.classに近いものじゃ。実行時に自動生成される。
__pycache__/
*.py[cod]
*$py.class
# 仮想環境(もしプロジェクト内に作っておるなら)
venv/
.venv/
env/
# --- エディタ・OS関連 ---
# VS Codeの設定ファイル(個人設定が含まれることがあるので除外)
.vscode/
# macOSが勝手に作るゴミ
.DS_Store
# Windowsのサムネイルキャッシュ
Thumbs.db
# --- ComfyUI/カスタムノード特有 ---
# もし一時ファイルやログを書き出す仕組みを作ったなら、それも追加じゃ
*.log
+8 -4
View File
@@ -16,6 +16,10 @@ ComfyUIを再起動せずに、Checkpoint系の選択リストを現在のファ
- **バックエンドまで貫くパッチ** <br/> - **バックエンドまで貫くパッチ** <br/>
ブラウザ上の表示を変えるだけではない。Python側の型定義(`INPUT_TYPES` など)まで更新するため、バリデーションで弾かれる心配も少ないのじゃ。 ブラウザ上の表示を変えるだけではない。Python側の型定義(`INPUT_TYPES` など)まで更新するため、バリデーションで弾かれる心配も少ないのじゃ。
- **Checkpoint Name Cycler との強力連携** (New!) <br/>
これと対となるノード、**[`Checkpoint Name Cycler`](https://github.com/ruminar/ComfyUI-CheckpointNameCycler)** があれば、巡回状態も初期化するぞ。 <br/>
`start_checkpoint` から改めて巡回を始めたい時は、リフレッシュボタンをクリックじゃ!
- **徹底した軽量設計** <br/> - **徹底した軽量設計** <br/>
常時監視やポーリングは一切しない。おぬしが「今だ!」とボタンを押した時だけ動く、ワークフローの邪魔をしない奥ゆかしい実用品じゃ。 常時監視やポーリングは一切しない。おぬしが「今だ!」とボタンを押した時だけ動く、ワークフローの邪魔をしない奥ゆかしい実用品じゃ。
@@ -25,7 +29,7 @@ ComfyUIの `custom_nodes` ディレクトリで、以下のコマンドを打ち
```bash ```bash
git clone https://github.com/ruminar/ComfyUI-CheckpointWidgetRefresh.git git clone https://github.com/ruminar/ComfyUI-CheckpointWidgetRefresh.git
```` ```
## 使い方 ## 使い方
@@ -41,12 +45,14 @@ git clone https://github.com/ruminar/ComfyUI-CheckpointWidgetRefresh.git
<br/> <br/>
<img width="982" height="418" alt="image" src="https://github.com/user-attachments/assets/112b8c0c-b2da-4d55-a622-5dbd0af9d2be" /> <img width="876" height="480" alt="image" src="https://github.com/user-attachments/assets/ce6f8c34-0828-44b4-aae5-538d7dcb9268" />
## 対応ノード ## 対応ノード
* `CheckpointLoaderSimple` * `CheckpointLoaderSimple`
* `CheckpointNameSelector` 系ノード * `CheckpointNameSelector` 系ノード
* [`CheckpointNameCycler`](https://github.com/ruminar/ComfyUI-CheckpointNameCycler)
## ライセンス ## ライセンス
@@ -56,5 +62,3 @@ GPL-3.0
## 説明画像 ## 説明画像
<img width="1024" height="1536" alt="ComfyUI-CheckpointWidgetRefresh説明" src="https://github.com/user-attachments/assets/d1b7e020-5f60-4ad8-a006-639ae3335fe4" /> <img width="1024" height="1536" alt="ComfyUI-CheckpointWidgetRefresh説明" src="https://github.com/user-attachments/assets/d1b7e020-5f60-4ad8-a006-639ae3335fe4" />
<br/>
+11 -11
View File
@@ -1,8 +1,15 @@
# v0.1.0 # 0.2.0
初回リリースじゃ! CheckpointNameCyclerとの連携機能を追加したのじゃ!
## 特徴 ## 追加
- `Checkpoint Name Cycler` との連携に対応
- `CheckpointNameCycler` の候補リスト更新に対応
- `CheckpointNameCycler` のバックエンド側型定義更新に対応
- `CheckpointNameCycler` の内部状態リセットAPI呼び出しに対応
## 既存機能
- ComfyUIを再起動せずに、Checkpoint系の選択リストを現在のファイル状態へ同期 - ComfyUIを再起動せずに、Checkpoint系の選択リストを現在のファイル状態へ同期
- 標準の `CheckpointLoaderSimple` に対応 - 標準の `CheckpointLoaderSimple` に対応
@@ -10,13 +17,6 @@
- フロントエンドのWidget候補だけでなく、バックエンド側の型定義も更新 - フロントエンドのWidget候補だけでなく、バックエンド側の型定義も更新
- 常時監視やポーリングなしの手動更新方式 - 常時監視やポーリングなしの手動更新方式
## 使い方
1. Checkpointを追加・削除する
2. `Checkpoint Widget Refresh` ノードをキャンバスに置く
3. `Refresh Checkpoint Widgets` ボタンを押す
4. そのまま作画する
## 備考 ## 備考
動作確認は、標準の `CheckpointLoaderSimple` と art-venture の `CheckpointNameSelector` で行っています。 動作確認は、標準の `CheckpointLoaderSimple`、art-venture の `CheckpointNameSelector`、および `Checkpoint Name Cycler` で行っています。
+28 -30
View File
@@ -20,13 +20,6 @@ def _clear_checkpoint_filename_cache():
def _scan_registered_checkpoint_folders(): def _scan_registered_checkpoint_folders():
"""
Scan only ComfyUI-registered checkpoint folders.
This intentionally does NOT accept arbitrary user-supplied paths.
It should pick up files added/removed under normal checkpoints paths,
including paths registered through extra_model_paths.yaml.
"""
results = set() results = set()
extensions = _get_supported_checkpoint_extensions() extensions = _get_supported_checkpoint_extensions()
@@ -54,10 +47,6 @@ def _scan_registered_checkpoint_folders():
def _get_fresh_checkpoints(): def _get_fresh_checkpoints():
"""
Prefer ComfyUI's own list after clearing its filename cache.
Merge direct scanning as a safety net for stale model-library states.
"""
_clear_checkpoint_filename_cache() _clear_checkpoint_filename_cache()
results = set() results = set()
@@ -75,17 +64,7 @@ def _get_fresh_checkpoints():
return sorted(results, key=lambda value: value.lower()) return sorted(results, key=lambda value: value.lower())
def _patch_backend_checkpoint_widget_classes(checkpoints): def _patch_backend_checkpoint_classes(checkpoints):
"""
Patch backend class contracts for checkpoint combo nodes.
Updating only the browser-side widget list is not enough.
ComfyUI backend prompt validation can still use stale INPUT_TYPES / RETURN_TYPES.
Targets:
- CheckpointNameSelector-style nodes
- CheckpointLoaderSimple
"""
patched = [] patched = []
try: try:
@@ -125,6 +104,32 @@ def _patch_backend_checkpoint_widget_classes(checkpoints):
patched.append(name) patched.append(name)
continue continue
if name == "CheckpointNameCycler":
def cycler_input_types(cls, _checkpoint_values=checkpoint_values):
values = list(_checkpoint_values) or [""]
return {
"required": {
"start_checkpoint": (values,),
"mode": (["fixed", "increment", "randomize", "shuffle_once"], {"default": "increment"}),
"change_every": ("INT", {"default": 1, "min": 1, "max": 999999}),
},
"hidden": {
"unique_id": "UNIQUE_ID",
},
}
node_class.INPUT_TYPES = classmethod(cycler_input_types)
node_class.RETURN_TYPES = (
list(checkpoint_values),
"STRING",
"STRING",
"INT",
"INT",
"INT",
)
patched.append(name)
continue
except Exception as exc: except Exception as exc:
print(f"[CheckpointWidgetRefresh] failed to patch {name}: {exc}") print(f"[CheckpointWidgetRefresh] failed to patch {name}: {exc}")
@@ -137,7 +142,7 @@ routes = PromptServer.instance.routes
@routes.get("/checkpoint_widget_refresh/checkpoints") @routes.get("/checkpoint_widget_refresh/checkpoints")
async def checkpoint_widget_refresh_checkpoints(_request): async def checkpoint_widget_refresh_checkpoints(_request):
checkpoints = _get_fresh_checkpoints() checkpoints = _get_fresh_checkpoints()
patched = _patch_backend_checkpoint_widget_classes(checkpoints) patched = _patch_backend_checkpoint_classes(checkpoints)
return web.json_response({ return web.json_response({
"checkpoints": checkpoints, "checkpoints": checkpoints,
@@ -148,13 +153,6 @@ async def checkpoint_widget_refresh_checkpoints(_request):
class CheckpointWidgetRefreshPanel: class CheckpointWidgetRefreshPanel:
"""
UI-only panel node.
The refresh button itself is implemented in web/checkpoint_widget_refresh.js.
This backend node exists so the button can be placed inside a workflow as a small panel.
"""
@classmethod @classmethod
def INPUT_TYPES(cls): def INPUT_TYPES(cls):
return { return {
+163 -49
View File
@@ -23,12 +23,24 @@ function isRefresherNode(node) {
return getNodeTypeName(node) === REFRESHER_NODE_NAME; return getNodeTypeName(node) === REFRESHER_NODE_NAME;
} }
function findWidget(node, name) {
return (node?.widgets ?? []).find((widget) => widget?.name === name);
}
function findComboWidget(node, name) {
const widget = findWidget(node, name);
if (widget && Array.isArray(widget?.options?.values)) {
return widget;
}
return null;
}
function findCheckpointWidget(node) { function findCheckpointWidget(node) {
const widgets = node?.widgets ?? []; return findComboWidget(node, "ckpt_name");
return widgets.find((widget) => }
widget?.name === "ckpt_name" &&
Array.isArray(widget?.options?.values) function findStartCheckpointWidget(node) {
); return findComboWidget(node, "start_checkpoint");
} }
function outputNames(node) { function outputNames(node) {
@@ -47,7 +59,23 @@ function hasLoaderOutputs(node) {
return names.includes("model") && names.includes("clip") && names.includes("vae"); return names.includes("model") && names.includes("clip") && names.includes("vae");
} }
function shouldTargetNode(node) { function isCyclerNode(node) {
const typeName = getNodeTypeName(node);
if (typeName === "CheckpointNameCycler") {
return true;
}
const names = outputNames(node);
return (
names.includes("ckpt_name") &&
names.includes("ckpt_name_str") &&
names.includes("ckpt_name_safe") &&
names.includes("index") &&
names.includes("count")
);
}
function shouldTargetCheckpointWidgetNode(node) {
if (!node || isRefresherNode(node)) { if (!node || isRefresherNode(node)) {
return false; return false;
} }
@@ -79,7 +107,11 @@ function shouldPatchFutureNodeType(nodeData) {
return true; return true;
} }
return name.includes("CheckpointNameSelector") || name === "CheckpointLoaderSimple"; return (
name.includes("CheckpointNameSelector") ||
name === "CheckpointLoaderSimple" ||
name === "CheckpointNameCycler"
);
} }
function chooseReplacement(oldValues, newValues, currentValue) { function chooseReplacement(oldValues, newValues, currentValue) {
@@ -94,7 +126,6 @@ function chooseReplacement(oldValues, newValues, currentValue) {
const oldIndex = Array.isArray(oldValues) ? oldValues.indexOf(currentValue) : -1; const oldIndex = Array.isArray(oldValues) ? oldValues.indexOf(currentValue) : -1;
if (oldIndex >= 0) { if (oldIndex >= 0) {
// Prefer the next surviving item from the old list.
for (let index = oldIndex + 1; index < oldValues.length; index++) { for (let index = oldIndex + 1; index < oldValues.length; index++) {
const candidate = oldValues[index]; const candidate = oldValues[index];
if (newValues.includes(candidate)) { if (newValues.includes(candidate)) {
@@ -102,7 +133,6 @@ function chooseReplacement(oldValues, newValues, currentValue) {
} }
} }
// If the deleted item was at the end, choose the previous survivor.
for (let index = oldIndex - 1; index >= 0; index--) { for (let index = oldIndex - 1; index >= 0; index--) {
const candidate = oldValues[index]; const candidate = oldValues[index];
if (newValues.includes(candidate)) { if (newValues.includes(candidate)) {
@@ -110,7 +140,6 @@ function chooseReplacement(oldValues, newValues, currentValue) {
} }
} }
// Last fallback: same numeric position in the new list if possible.
return newValues[Math.min(oldIndex, newValues.length - 1)] ?? newValues[0]; return newValues[Math.min(oldIndex, newValues.length - 1)] ?? newValues[0];
} }
@@ -164,25 +193,7 @@ function patchCheckpointSlotTypes(node, checkpoints) {
} }
} }
function applyCheckpointListToNode(node, checkpoints) { function updateComboWidget(node, widget, checkpoints) {
if (!shouldTargetNode(node)) {
return {
matched: false,
changed: false,
valueChanged: false,
};
}
const widget = findCheckpointWidget(node);
if (!widget) {
return {
matched: true,
changed: false,
valueChanged: false,
reason: "checkpoint widget not found",
};
}
if (!widget.options) { if (!widget.options) {
widget.options = {}; widget.options = {};
} }
@@ -197,7 +208,6 @@ function applyCheckpointListToNode(node, checkpoints) {
widget.options.values = newValues; widget.options.values = newValues;
widget.value = newValue; widget.value = newValue;
patchCheckpointSlotTypes(node, newValues);
const valuesChanged = !arraysEqual(oldValues, newValues); const valuesChanged = !arraysEqual(oldValues, newValues);
const valueChanged = oldValue !== newValue; const valueChanged = oldValue !== newValue;
@@ -215,7 +225,6 @@ function applyCheckpointListToNode(node, checkpoints) {
} }
return { return {
matched: true,
changed: valuesChanged || valueChanged, changed: valuesChanged || valueChanged,
valueChanged, valueChanged,
oldValue, oldValue,
@@ -223,6 +232,64 @@ function applyCheckpointListToNode(node, checkpoints) {
}; };
} }
function applyCheckpointListToWidgetNode(node, checkpoints) {
if (!shouldTargetCheckpointWidgetNode(node)) {
return {
matched: false,
changed: false,
valueChanged: false,
};
}
const widget = findCheckpointWidget(node);
if (!widget) {
return {
matched: true,
changed: false,
valueChanged: false,
reason: "checkpoint widget not found",
};
}
const result = updateComboWidget(node, widget, checkpoints);
patchCheckpointSlotTypes(node, checkpoints);
return {
matched: true,
...result,
};
}
function applyCheckpointListToCyclerNode(node, checkpoints) {
if (!isCyclerNode(node)) {
return {
matched: false,
changed: false,
valueChanged: false,
};
}
const widget = findStartCheckpointWidget(node);
if (!widget) {
patchCheckpointSlotTypes(node, checkpoints);
node.setDirtyCanvas?.(true, true);
return {
matched: true,
changed: false,
valueChanged: false,
reason: "start_checkpoint widget not found",
};
}
const result = updateComboWidget(node, widget, checkpoints);
patchCheckpointSlotTypes(node, checkpoints);
return {
matched: true,
...result,
};
}
async function fetchCheckpointList() { async function fetchCheckpointList() {
const response = await api.fetchApi("/checkpoint_widget_refresh/checkpoints", { const response = await api.fetchApi("/checkpoint_widget_refresh/checkpoints", {
cache: "no-store", cache: "no-store",
@@ -240,6 +307,36 @@ async function fetchCheckpointList() {
}; };
} }
async function resetCyclersViaApi() {
try {
const response = await api.fetchApi("/checkpoint_name_cycler/reset", {
method: "POST",
});
if (!response.ok) {
return {
ok: false,
status: response.status,
resetCount: 0,
};
}
const payload = await response.json();
return {
ok: Boolean(payload.ok),
status: response.status,
resetCount: payload.reset_count ?? 0,
};
} catch (error) {
// Cycler may not be installed. That is fine.
return {
ok: false,
error,
resetCount: 0,
};
}
}
function updatePanelTitle(panelNode, text) { function updatePanelTitle(panelNode, text) {
if (!panelNode) { if (!panelNode) {
return; return;
@@ -257,35 +354,51 @@ async function refreshCheckpointWidgets(panelNode = null) {
const checkpoints = payload.checkpoints; const checkpoints = payload.checkpoints;
lastCheckpointList = checkpoints; lastCheckpointList = checkpoints;
let matched = 0; let widgetMatched = 0;
let changed = 0; let widgetChanged = 0;
let valueChanged = 0; let valueChanged = 0;
let cyclerMatched = 0;
let cyclerChanged = 0;
let cyclerValueChanged = 0;
for (const node of app.graph?._nodes ?? []) { for (const node of app.graph?._nodes ?? []) {
const result = applyCheckpointListToNode(node, checkpoints); const widgetResult = applyCheckpointListToWidgetNode(node, checkpoints);
if (!result.matched) { if (widgetResult.matched) {
continue; widgetMatched += 1;
if (widgetResult.changed) {
widgetChanged += 1;
}
if (widgetResult.valueChanged) {
valueChanged += 1;
console.log(
`[CheckpointWidgetRefresh] ${getNodeTypeName(node)} #${node.id}: ` +
`${widgetResult.oldValue} -> ${widgetResult.newValue}`
);
}
} }
matched += 1; const cyclerResult = applyCheckpointListToCyclerNode(node, checkpoints);
if (result.changed) { if (cyclerResult.matched) {
changed += 1; cyclerMatched += 1;
} if (cyclerResult.changed) {
if (result.valueChanged) { cyclerChanged += 1;
valueChanged += 1; }
console.log( if (cyclerResult.valueChanged) {
`[CheckpointWidgetRefresh] ${getNodeTypeName(node)} #${node.id}: ` + cyclerValueChanged += 1;
`${result.oldValue} -> ${result.newValue}` }
);
} }
} }
const title = `Updated ${changed}/${matched} ckpt widgets, ${checkpoints.length} ckpts`; const resetResult = await resetCyclersViaApi();
const cyclerPart = cyclerMatched > 0 ? `, ${cyclerMatched} cyclers reset` : "";
const title = `Updated ${widgetChanged}/${widgetMatched} ckpt widgets${cyclerPart}, ${checkpoints.length} ckpts`;
updatePanelTitle(panelNode, title); updatePanelTitle(panelNode, title);
console.log( console.log(
`[CheckpointWidgetRefresh] ${title}. ` + `[CheckpointWidgetRefresh] ${title}. ` +
`valueChanged=${valueChanged}, backend patched=${payload.patchedClassCount}`, `valueChanged=${valueChanged}, cyclerChanged=${cyclerChanged}, ` +
`cyclerValueChanged=${cyclerValueChanged}, backend patched=${payload.patchedClassCount}, ` +
`cyclerResetOk=${resetResult.ok}, cyclerResetCount=${resetResult.resetCount}`,
payload.patchedClasses payload.patchedClasses
); );
@@ -333,7 +446,8 @@ function installFuturePatchHook(nodeType, nodeData) {
if (lastCheckpointList) { if (lastCheckpointList) {
setTimeout(() => { setTimeout(() => {
applyCheckpointListToNode(this, lastCheckpointList); applyCheckpointListToWidgetNode(this, lastCheckpointList);
applyCheckpointListToCyclerNode(this, lastCheckpointList);
app.graph?.setDirtyCanvas?.(true, true); app.graph?.setDirtyCanvas?.(true, true);
}, 0); }, 0);
} }