release: v0.5.3 — Labタブのワークフロー切替時の表示残留を修正 + Galleryタブ読み込み高速化・rootフォルダ自動展開 + READMEスクリーンショット刷新
- Labタブ: ワークフロー読み込み時、Labサブタブが既にアクティブでも Setting 1のライブ反映セルを再描画するよう修正(前ワークフローの表示残留を解消) - Galleryタブ: フォルダスキャン時の不要なPath.resolve()呼び出しを除去し 9,777枚のフォルダで7.5秒→0.14秒に高速化。os.walk()ベースの再帰スキャンも DirEntryベースに書き換え - Galleryタブ: フォルダツリーのrootをデフォルト展開状態に変更 - README: スクリーンショット5件を削除、Video/Lab/AI TOOLタブの新規4枚を追加 Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
@@ -2,6 +2,7 @@
|
||||
|
||||
Condensed version-by-version index of DEVLOG.md (each line: version + one-line summary of what changed and why). Full rationale, code details, and "How to apply" lessons live in the excluded DEVLOG.md itself; this index exists so graphify can extract a queryable semantic node per release without feeding the full log to the local Ollama model. Regenerate with `python tools/generate_doc_index.py devlog` whenever DEVLOG.md changes.
|
||||
|
||||
- **v0.5.3** — バグ修正: Labタブのワークフロー切替時に前の内容が残留 + Galleryタブの読み込み高速化・root自動展開 + READMEスクリーンショット刷新
|
||||
- **v0.5.2** — 機能追加: GenerateUI/Labタブの動画ワークフロー対応 + VideoタブにFrame抽出/GIF変換機能 + データ管理監査とZIPフルバックアップ + Send to LI node統一
|
||||
- **v0.5.1** — 機能追加: mp4動画の埋め込みメタデータ対応(Gallery Prompt/JSON・Metadataサブタブ・サイドパネルIタブ)+ Gallery Infoタブに寸法/再生時間表示 + MiniMax H3等オールインワン動画ノードのプロンプト抽出バグ修正
|
||||
- **v0.5.0** — 機能追加: mp4動画Gallery対応 + Videoタブ新設(MiniMax H3専用) + バグ修正: Galleryのタグ/メモが再起動後に消える
|
||||
|
||||
@@ -2,6 +2,28 @@
|
||||
|
||||
---
|
||||
|
||||
## v0.5.3
|
||||
|
||||
### バグ修正: Labタブのワークフロー切替時に前の内容が残留 + Galleryタブの読み込み高速化・root自動展開 + READMEスクリーンショット刷新
|
||||
|
||||
ユーザーからの複数のバグ報告・改善要望を1セッションで順に対応した作業。
|
||||
|
||||
**1. バグ修正: Labタブで起動時デフォルトワークフロー読み込み後に別ワークフローを読み込むと前の内容が残る**: 「別のワークフローを読み込んだ。一部以前のワークフローの内容が残ったまま表示される(クリックするか他のタブから戻ると消える)」との報告。Labタブの「Setting 1」列(Checkpoint/LoRA/VAE/Prompt/KSampler)はGenerateUIの現在の読み込み内容をライブ反映する仕組みだが、その再描画(`refreshLabLiveDefaults()`)は「サブタブをLabに切り替えたとき」の一箇所(`generate-tab.js`のサブタブクリックハンドラ)でしかトリガーされていなかった。Labタブを既に開いた状態で別のワークフローを読み込んだ場合、`loadWorkflowIntoEditor()`側では再描画が一切呼ばれず、前のワークフローの表示がそのまま残っていた。「セルをクリックしたり他のタブから戻ると直る」という報告内容は、それらの操作がたまたま再描画をトリガーしていたことの裏返しだった。`loadWorkflowIntoEditor()`の末尾で、現在アクティブなサブタブがLabの場合は`refreshLabLiveDefaults()`を追加で呼ぶよう修正。
|
||||
|
||||
**2. 改善: Galleryタブの生成後の更新が遅い**: 「生成後の画像表示に更新して読み直すのに時間がかかる」との報告。実データ(Output先の`Text2Img`フォルダ、画像9,777枚)で`_scan_folder()`を実測したところ**7.5秒**かかっており、原因を切り分けたところ`entry.stat(follow_symlinks=True)`と、各ファイルごとに呼んでいた`Path(entry.path).resolve()`の2箇所がボトルネックと判明。特に`resolve()`の再解決が支配的で、これだけで1ファイルあたり約0.7ms×9,777件のファイルシステム往復が発生していた。`_scan_folder()`の呼び出し元(`list_images()`/`_bg_index_folder()`)は既にフォルダ自体を`Path(folder_path).resolve()`済みで渡しているため、`entry.path`は既に絶対パスであり、エントリ単位での再`resolve()`は不要だった。`entry.is_file()`/`entry.stat()`を`follow_symlinks=False`に変更し、パス生成も`entry.path.replace("\\", "/")`に簡略化(出力フォルダにシンボリックリンクの画像が置かれる想定はないため安全と判断)。同じ理由で`os.walk()`ベースだった`_scan_folder_recursive()`(ImagePrompt/Style Catalog用の再帰スキャン)も、`os.scandir()`を自前で再帰しDirEntryベースでstatする実装に書き換えた。実測で9,777枚のフォルダに対する`list_images()`全体が0.14秒(コールド)/0.08秒(フォルダキャッシュ温存時)まで短縮(約50倍高速化)。
|
||||
|
||||
**3. 改善: Galleryフォルダツリーのrootをデフォルト展開状態に**: 上記の高速化を受けて「rootフォルダはデフォルトで展開状態で表示したい」との要望。フォルダスキャンが軽くなったことでサブフォルダの内容もすぐ確認できるようになった一方、`renderTreeNode()`はrootも他のノードと同じく初期状態は折りたたみ(▶)だったため気付きにくかった。root(`isRoot`)の場合のみ、ツリー構築時に矢印を展開済み(▼)にし子ノードを即座にレンダリングするよう`gallery-tab.js`を修正。
|
||||
|
||||
**4. READMEスクリーンショット刷新**: 「以下のスクリーンショットは削除したい」との依頼でGenUI Feeder / GenUI LoRA Stack / Models Multi-select Menu / Tagger Tab / Customizeの5件をREADME.mdの表と`docs/`実ファイルの両方から削除(残った項目は2列レイアウトに詰め直し)。続けてVideo Tab・Lab Tab(2枚)・AI TOOL Tabの新規スクリーンショット計4枚をユーザーが`docs/`に配置し、READMEの表に追加した。
|
||||
|
||||
**検証**: JS/Pythonとも`node --check`/`ast.parse()`で構文確認。Gallery高速化は開発元リポジトリの`GalleryService`を実データ(実際の`Text2Img`フォルダ、9,777枚)に対してvenv Pythonから直接呼び出し、修正前後の`_scan_folder()`/`list_images()`の実行時間をベンチマークして改善を数値で確認(7.5秒→0.063秒、`list_images()`全体は0.14秒)。返却されるパス文字列が修正前と同じ絶対パス形式であることも確認済み。開発元gitリポジトリと実行時`custom_nodes`フォルダは別実体のため([[project_dev_deploy_sync]]参照)、変更・削除した全ファイル(`generate-tab.js`, `gallery_service.py`, `gallery-tab.js`, `README.md`, `docs/`配下の画像)を都度両フォルダへ同期し`diff`/存在確認で完全一致を確認、Pythonファイル変更のため`__pycache__`もクリア。全項目ともユーザー自身の実機確認で解消・反映を確認済み。
|
||||
|
||||
**How to apply**: あるタブ/サブタブの表示を「別の場所(GenerateUI本体など)の現在の状態をライブ反映する」設計にする場合、再描画のトリガーを「そのタブへの切替」1箇所だけに置くと、切替を経由しない別の更新経路(今回はワークフロー読み込み)で表示が古いまま取り残される。ライブ反映される値の更新元(今回は`loadWorkflowIntoEditor()`)側でも、現在表示中のタブを見て能動的に再描画を呼ぶ必要がある。パフォーマンス調査では「体感で重い処理」を分解して各行を実測するまで真犯人を決め打ちしない方がよい — 今回も当初`follow_symlinks=True`のstat呼び出しを主因と想定したが、実測すると`Path.resolve()`の方が支配的だった。Windows上で大量ファイルを扱う`os.scandir()`ループでは、`entry.path`が既に呼び出し元でresolve済みのフォルダ配下の絶対パスであることを踏まえ、エントリ単位での`resolve()`/`follow_symlinks=True`はファイル数に比例した無視できないコストになり得るため避ける。バックエンドの表示速度を改善した際は、それに応じてフロントエンドのデフォルトUI状態(今回は折りたたみ初期状態)も見直す価値がある — 「遅いから畳んでおく」という妥協がボトルネック解消後も残ったままになりがち。
|
||||
|
||||
関連: [[project_dev_deploy_sync]]
|
||||
|
||||
---
|
||||
|
||||
## v0.5.2
|
||||
|
||||
### 機能追加: GenerateUI/Labタブの動画ワークフロー対応 + VideoタブにFrame抽出/GIF変換機能 + データ管理監査とZIPフルバックアップ + Send to LI node統一
|
||||
|
||||
@@ -26,7 +26,7 @@ A comprehensive workflow, asset management, and generation UI plugin for [ComfyU
|
||||
- Built-in AI tools (translation and more)
|
||||
|
||||

|
||||

|
||||

|
||||
|
||||
## Features (tab -> feature names)
|
||||
|
||||
|
||||
@@ -22,7 +22,7 @@ A comprehensive workflow, asset management, and generation UI plugin for [ComfyU
|
||||
- Built-in AI tools (translation and more)
|
||||
|
||||

|
||||

|
||||

|
||||
|
||||
## Screenshots
|
||||
|
||||
@@ -30,29 +30,25 @@ A comprehensive workflow, asset management, and generation UI plugin for [ComfyU
|
||||
| :-------------------------------------: | :---------------------------------: |
|
||||
|  |  |
|
||||
|
||||
| Prompt Input Assistance | Gen UI Feeder |
|
||||
| :----------------------------------------------------------: | :---------------------------------------: |
|
||||
|  |  |
|
||||
| Prompt Input Assistance | GenUI Batch |
|
||||
| :----------------------------------------------------------: | :-------------------------------------: |
|
||||
|  |  |
|
||||
|
||||
| GenUI LoRA Stack | GenUI Batch |
|
||||
| :---------------------------------------------: | :-------------------------------------: |
|
||||
|  |  |
|
||||
|
||||
| Models Multi-select Menu | Top Bar |
|
||||
| :-----------------------------------------------------------: | :---------------------------: |
|
||||
|  |  |
|
||||
| Top Bar | Image Edit Tab |
|
||||
| :----------------------------: | :--------------------------------------: |
|
||||
|  |  |
|
||||
|
||||
| WS Library | Library Information |
|
||||
| :----------------------------------: | :---------------------------------------------------: |
|
||||
|  |  |
|
||||
|
||||
| Tagger Tab | Image Edit Tab |
|
||||
| :-------------------------------: | :--------------------------------------: |
|
||||
|  |  |
|
||||
| Video Tab | AI TOOL Tab |
|
||||
| :----------------------------------: | :--------------------------------------: |
|
||||
|  |  |
|
||||
|
||||
| Customize | |
|
||||
| :--------------------------------: | :-: |
|
||||
|  | |
|
||||
| Lab Tab | Lab Index Image |
|
||||
| :--------------------------------: | :------------------------------------------: |
|
||||
|  |  |
|
||||
|
||||
---
|
||||
|
||||
|
||||
|
Before Width: | Height: | Size: 16 KiB |
|
Before Width: | Height: | Size: 181 KiB |
|
After Width: | Height: | Size: 181 KiB |
|
After Width: | Height: | Size: 62 KiB |
|
After Width: | Height: | Size: 181 KiB |
|
After Width: | Height: | Size: 161 KiB |
|
Before Width: | Height: | Size: 278 KiB |
|
Before Width: | Height: | Size: 90 KiB |
|
Before Width: | Height: | Size: 48 KiB |
@@ -312,15 +312,22 @@ class GalleryService:
|
||||
try:
|
||||
with os.scandir(folder) as it:
|
||||
for entry in it:
|
||||
if not entry.is_file(follow_symlinks=True):
|
||||
# follow_symlinks=True forces stat() to make an extra syscall per file
|
||||
# to resolve the link target even for plain files. Output folders don't
|
||||
# contain symlinked images, so False is safe and much cheaper.
|
||||
if not entry.is_file(follow_symlinks=False):
|
||||
continue
|
||||
if Path(entry.name).suffix.lower() not in IMAGE_EXTENSIONS:
|
||||
continue
|
||||
try:
|
||||
st = entry.stat(follow_symlinks=True)
|
||||
st = entry.stat(follow_symlinks=False)
|
||||
# entry.path is already absolute+resolved because callers always
|
||||
# pass in an already-resolved `folder`. Re-resolving it here via
|
||||
# Path(entry.path).resolve() costs an extra filesystem round-trip
|
||||
# per file — on a 9786-file folder that alone was 6.7s vs 0.07s.
|
||||
entries.append((
|
||||
entry.name,
|
||||
str(Path(entry.path).resolve()).replace("\\", "/"),
|
||||
entry.path.replace("\\", "/"),
|
||||
st.st_size,
|
||||
st.st_mtime,
|
||||
))
|
||||
@@ -333,30 +340,45 @@ class GalleryService:
|
||||
return entries
|
||||
|
||||
def _scan_folder_recursive(self, folder: Path) -> list[tuple[str, str, int, float]]:
|
||||
"""os.walk() でフォルダ配下を再帰的に列挙する(サブフォルダの画像も含む)。
|
||||
"""フォルダ配下を再帰的に列挙する(サブフォルダの画像も含む)。
|
||||
ImagePrompt/Style Catalogギャラリーのように深い階層に画像が置かれるケース用。
|
||||
キャッシュはしない(フォルダ数が少なく、都度スキャンしても軽い想定)。
|
||||
os.walk()はDirEntryを介さずPath.stat()(follow_symlinks=True相当)を呼ぶため、
|
||||
_scan_folder()と同じ理由でフォルダ内ファイル数に対して極端に遅くなる。
|
||||
os.scandir()を自前で再帰し、follow_symlinks=Falseでstatする。
|
||||
"""
|
||||
entries = []
|
||||
try:
|
||||
for root, dirs, files in os.walk(folder):
|
||||
dirs[:] = [d for d in dirs if not d.startswith(".")]
|
||||
for name in files:
|
||||
if Path(name).suffix.lower() not in IMAGE_EXTENSIONS:
|
||||
continue
|
||||
full = Path(root) / name
|
||||
try:
|
||||
st = full.stat()
|
||||
entries.append((
|
||||
name,
|
||||
str(full.resolve()).replace("\\", "/"),
|
||||
st.st_size,
|
||||
st.st_mtime,
|
||||
))
|
||||
except OSError:
|
||||
continue
|
||||
except PermissionError:
|
||||
pass
|
||||
|
||||
def _walk(d):
|
||||
try:
|
||||
with os.scandir(d) as it:
|
||||
dir_entries = list(it)
|
||||
except (PermissionError, OSError):
|
||||
return
|
||||
for entry in dir_entries:
|
||||
if entry.is_dir(follow_symlinks=False):
|
||||
if not entry.name.startswith("."):
|
||||
_walk(entry.path)
|
||||
continue
|
||||
if not entry.is_file(follow_symlinks=False):
|
||||
continue
|
||||
if Path(entry.name).suffix.lower() not in IMAGE_EXTENSIONS:
|
||||
continue
|
||||
try:
|
||||
st = entry.stat(follow_symlinks=False)
|
||||
# entry.path is already absolute since the top-level `folder` passed
|
||||
# in by callers is always pre-resolved — see _scan_folder() for why
|
||||
# re-resolving per file here would be costly.
|
||||
entries.append((
|
||||
entry.name,
|
||||
entry.path.replace("\\", "/"),
|
||||
st.st_size,
|
||||
st.st_mtime,
|
||||
))
|
||||
except OSError:
|
||||
continue
|
||||
|
||||
_walk(folder)
|
||||
return entries
|
||||
|
||||
def list_images(
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
[project]
|
||||
name = "comfyui-workflow-studio"
|
||||
description = "A comprehensive workflow, asset management, and generation UI plugin for ComfyUI. Manage workflows, models, images, and prompts; batch generation and Image Feeder in the GenerateUI tab; library side panel with canvas drag & drop and AI tools."
|
||||
version = "0.5.2"
|
||||
version = "0.5.3"
|
||||
license = "MIT"
|
||||
requires-python = ">= 3.10"
|
||||
dependencies = ["jinja2"]
|
||||
|
||||
@@ -467,6 +467,16 @@ function renderTreeNode(node, container, depth, isRoot) {
|
||||
|
||||
container.appendChild(item);
|
||||
container.appendChild(childContainer);
|
||||
|
||||
// root フォルダはデフォルトで展開状態にする(フォルダスキャン高速化により
|
||||
// サブフォルダの内容もすぐ確認できるようになったため、折りたたみ初期状態だと
|
||||
// 気付きにくい)
|
||||
if (isRoot) {
|
||||
arrow.dataset.expanded = "true";
|
||||
arrow.textContent = "▼";
|
||||
childContainer.style.display = "";
|
||||
node.children.forEach(child => renderTreeNode(child, childContainer, depth + 1, false));
|
||||
}
|
||||
} else {
|
||||
container.appendChild(item);
|
||||
}
|
||||
|
||||
@@ -293,6 +293,13 @@ export async function loadWorkflowIntoEditor(workflow, filename) {
|
||||
|
||||
refreshFeederNodeList();
|
||||
|
||||
// Lab subtab may already be the active one (e.g. it was active when this workflow
|
||||
// was loaded) — the subtab-click handler that normally triggers this refresh never
|
||||
// fires in that case, leaving Setting 1's live-reflected cells showing the previous
|
||||
// workflow until the user clicks a cell or revisits the subtab.
|
||||
const activeSubtab = document.querySelector(".wfm-gen-subtab-btn.active")?.dataset.subtab;
|
||||
if (activeSubtab === "lab") refreshLabLiveDefaults();
|
||||
|
||||
if (ckptWarning) {
|
||||
showToast(ckptWarning, "error");
|
||||
} else {
|
||||
|
||||