独自カスタマイズ導入中のため更新毎に動作が変わる場合があります。

【備忘録メモ】workflow、埋め込みPNGデータ読込みで無限ロードになる場合の対処方法。ComfyUI用PNG形式に含まれるワークフローを抽出したい

ComfyUIのworkflow(ワークフロー)の読み込み方法には、JSONファイルからのほかにPNG画像に埋め込まれたメタ情報を参照して復元・再現する方法があります。一部データ欠損やComfyUI側に不具合があると無限ロードになってしまうことがありますが、その対処、救済方法です。500KB程度の埋め込み画像でも無限ロードは発生します。

OSはWindows 10 / 11(64bit)かつ、事前にPythonのパスが通っている(フルパスが分かる)こと。(windows環境にPython 3.6以上が導入されている方を対象としています)

civitai.comで入手できるPNG画像(jpgファイル名に偽装?している場合もあり)にはワークフローが含まれていますが、そのままだとうまく読み込まないことがあります。そんなときは!

一度無限ロードになったcomfyUI。特に「StabilityMatrix」上で動かしている場合は、リスタートしても正常なフローワークさえも正しく読み込めなくなることが多いです。一度終了し、起動し直すことをおすすめします。ブラウザも閉じる。その上で、再度PNGファイルが読み込みできないか確認してください。

目次

PNG形式に含まれるワークフローを抽出したい。

前提:Windows/NVIDIA環境。Pythonのパスが通っている(フルパスが分かる)こと。
目指すこと:PNGに含まれるワークフロー(全TEXT、JSON)を抽出したい。

対象:ワークフロー:以下の画像にはワークフローが含まれています。(Workflow: the Image below contains the Workflow.)などで配布されているPNGファイル。(jpgファイル名に偽装しても読み込みます)

以下の手順で進めていきます。

STEP

抽出ファイル(py)を用意する。

新規テキストファイルを作成し、以下のコードをコピペして、ファイル名「extract_png_json.txt」でローカル環境にテキスト保存(文字エンコードをUTF-8)してください。そのテキストの拡張子「txt」を「py」に変更します。(読み込みはPNG形式のみです)

Python 3.6環境の場合は、以下コード内の先頭3行目にある「from future import annotations」の一行を削除してください。

#!/usr/bin/env python3
# -*- coding: utf-8 -*-
from __future__ import annotations  # Python 3.6ならこの行を消す
import sys, json, zlib
from pathlib import Path
from typing import Dict, List, Union

PNG_SIGNATURE = b"\x89PNG\r\n\x1a\n"
def _u32(b: bytes) -> int: return int.from_bytes(b, "big", signed=False)
def _w(path: Path, text: str) -> None: path.write_text(text, encoding="utf-8", errors="replace")
def _add(d: Dict[str, Union[str, List[str]]], k: str, v: str) -> None:
    if k in d: d[k] = d[k] + [v] if isinstance(d[k], list) else [d[k], v]
    else: d[k] = v

def extract_png_text_chunks(p: Path) -> Dict[str, Union[str, List[str]]]:
    data = p.read_bytes()
    if len(data) < 8 or data[:8] != PNG_SIGNATURE: raise ValueError("PNGファイルではない可能性があります。")
    pos, out = 8, {}
    while pos + 8 <= len(data):
        ln = _u32(data[pos:pos+4]); ct = data[pos+4:pos+8]; pos += 8
        if pos + ln + 4 > len(data): break
        ck = data[pos:pos+ln]; pos += ln + 4
        if ct == b"IEND": break
        if ct == b"tEXt":
            try:
                nul = ck.index(b"\x00"); key = ck[:nul].decode("latin-1", "replace"); raw = ck[nul+1:]
                try: val = raw.decode("utf-8")
                except UnicodeDecodeError: val = raw.decode("latin-1", "replace")
                _add(out, key, val)
            except ValueError: _add(out, "tEXt_invalid", ck.decode("latin-1", "replace"))
        elif ct == b"zTXt":
            try:
                nul = ck.index(b"\x00"); key = ck[:nul].decode("latin-1", "replace")
                if nul + 2 > len(ck): continue
                m = ck[nul+1]; comp = ck[nul+2:]
                if m != 0: _add(out, key, f"[zTXt] unsupported compression method: {m}"); continue
                try:
                    raw = zlib.decompress(comp)
                    try: val = raw.decode("utf-8")
                    except UnicodeDecodeError: val = raw.decode("latin-1", "replace")
                    _add(out, key, val)
                except Exception as e: _add(out, key, f"[zTXt] decompress error: {e}")
            except ValueError: _add(out, "zTXt_invalid", ck.decode("latin-1", "replace"))
        elif ct == b"iTXt":
            try:
                p0 = 0; nul = ck.index(b"\x00", p0); key = ck[p0:nul].decode("latin-1", "replace"); p0 = nul + 1
                if p0 + 2 > len(ck): continue
                flag, m = ck[p0], ck[p0+1]; p0 += 2
                nul = ck.index(b"\x00", p0); lang = ck[p0:nul].decode("latin-1", "replace"); p0 = nul + 1
                nul = ck.index(b"\x00", p0); tr = ck[p0:nul].decode("utf-8", "replace"); p0 = nul + 1
                tb = ck[p0:]
                if flag == 1:
                    if m != 0: _add(out, key, f"[iTXt] unsupported compression method: {m}"); continue
                    try: tb = zlib.decompress(tb)
                    except Exception as e: _add(out, key, f"[iTXt] decompress error: {e}"); continue
                _add(out, key, tb.decode("utf-8", "replace"))
                if lang: _add(out, f"{key}__language_tag", lang)
                if tr: _add(out, f"{key}__translated_keyword", tr)
            except ValueError: _add(out, "iTXt_invalid", ck.decode("latin-1", "replace"))
    return out

def _maybe_save_special_files(base: Path, meta: Dict[str, Union[str, List[str]]]) -> List[Path]:
    saved: List[Path] = []
    def first(k: str) -> Union[str, None]:
        if k not in meta: return None
        v = meta[k]; return v[0] if isinstance(v, list) and v else (None if isinstance(v, list) else v)
    for k in ("workflow", "prompt", "parameters"):
        v = first(k)
        if not v: continue
        if k == "workflow":
            try:
                obj = json.loads(v); p = base.with_name(base.name + "_workflow.json")
                _w(p, json.dumps(obj, ensure_ascii=False, indent=2)); saved.append(p); continue
            except Exception: pass
        suf = "_prompt.txt" if k == "prompt" else f"_{k}.txt"
        p = base.with_name(base.name + suf); _w(p, v); saved.append(p)
    return saved

def process_one(p: Path) -> bool:
    if not p.exists(): print(f"失敗: ファイルが見つかりません: {p}"); return False
    try: meta = extract_png_text_chunks(p) # PNG検査
    except Exception as e: print(f"失敗: PNGとして読めません: {p} ({e})"); return False
    base = p.with_suffix("")
    lines: List[str] = []
    for k, v in meta.items():
        lines.append(f"===== {k} =====")
        if isinstance(v, list):
            for i, item in enumerate(v, 1): lines.append(f"[{i}]"); lines.append(item); lines.append("")
        else: lines.append(v); lines.append("")
    allp = base.with_name(base.name + "_all.txt"); _w(allp, "\n".join(lines)); print(f"OK: {allp}")
    for sp in _maybe_save_special_files(base, meta): print(f"OK: {sp}")
    if not meta: print(f"注意: メタ情報が見つかりませんでした: {p}")
    return True

def main(argv: List[str]) -> int:
    if len(argv) <= 1: print("使い方: extract_png_json.py <file1.png> [file2.png ...]"); return 1
    ok = sum(1 for a in argv[1:] if process_one(Path(a)))
    ng = (len(argv) - 1) - ok
    print(f"成功: {ok} / 失敗: {ng}")
    return 0 if ng == 0 else 1

if __name__ == "__main__": raise SystemExit(main(sys.argv))


※変数k には「区切り(見出し)になるキー名」が入ります。
※成功時には生データの「TXT」形式ファイルと、ComfyUI用のworkflow「JSON」形式ファイルが出力されます。

STEP

実行用BATを用意する。

上記STEP1で保存したファイルと同じフォルダに、新規テキストファイルを作成し、
以下のコードをコピペして、ファイル名「extract_png_json.txt」でローカル環境にテキスト保存(文字エンコードをUTF-8)してください。そのテキストの拡張子「txt」を「bat」に変更します。

以下の簡易版もしくは、簡易チェック版を使ってください。

簡易版

@echo off
setlocal
REM Pythonのパスが通っている前提で実行。通ってないなら python.exe のフルパスに置き換えて。
python "%~dp0extract_png_json.py" %*
pause

簡易チェック付き版

@echo off
chcp 65001 >nul
setlocal EnableExtensions DisableDelayedExpansion
if "%~1"=="" exit /b 1
for %%F in (%*) do echo 対象: "%%~fF"
for %%F in (%*) do call :c "%%~fF"||goto end
python "%~dp0extract_png_json.py" %*||goto end
goto end
:c
set "P=%~1"
if not exist "%P%" echo エラー: ファイルが見つからない。ファイル名に%%があると失敗します。%%を消してリネームしてね: "%P%"&exit /b 1
setlocal EnableDelayedExpansion&set "Q=%P%"&if not "!Q:%%=!"=="!Q!" endlocal&goto bad&endlocal
echo(%P%|findstr /R "[\&\|\<\>\^!]" >nul&&goto bad
exit /b 0
:bad
echo エラー: ファイル名に記号が含まれています。記号を除いてリネームしてから再度チャレンジしてね: "%P%"
exit /b 1
:end
pause
STEP

PNGファイルをBATファイルにドラッグアンドドロップして読み込ませる。

BATファイルは管理者権限では実行しないでください。
ファイル名に記号(-や_や.以外)が含まれていれば取り除いてから実行してください。

上記STEP1~2で作成したファイルと同じフォルダに、読み込ませたいPNGファイルをコピー。
上記STEP2で作成したBATファイルに、PNGファイルをドラッグアンドドロップして読み込ませる。
対象PNGに含まれるテキスト全データと、JSONファイルを、同じフォルダ先に出力します。

保存されたJSONファイルを、ComfyUIのworkflowとして読み込んでください。

読み込まないよ対策!ワークフローとプロンプトを分離して保存したい場合

抽出ファイルがワークフローとAPI(prompt)部分が一緒になっているまたは、workflow キーが無いJSONファイルの場合、ComfyUIに直接workflowを投げ込んでも「ワークフローが見つからない」と表示され場合があります。

以下のコードを新規テキストファイルに保存した後、
コード内のsrc = の先を読み込ませたいファイル名に書き直します。
拡張子を「py」に変更して実行します。

コードの更新日:2026/01/28

import json
from datetime import datetime, timezone, timedelta
from pathlib import Path

JST = timezone(timedelta(hours=9))
ts = datetime.now(JST).strftime("%Y%m%d_%H%M%S")

src = "workflow.json.txt"
data = json.loads(Path(src).read_text(encoding="utf-8"))

# 1) UI用(workflow)
is_snapshot = "workflow" not in data
ui_obj = data["workflow"] if not is_snapshot else data

suffix = "_snapshot" if is_snapshot else ""
ui_path = f"{ts}_workflow_ui{suffix}.json"
Path(ui_path).write_text(json.dumps(ui_obj, ensure_ascii=False, indent=2), encoding="utf-8")

# 2) API用(prompt「文字列JSON」で parse)
api_obj = None
if "prompt" in data and isinstance(data["prompt"], str):
    s = data["prompt"].strip()
    if s.startswith("{") or s.startswith("["):
        api_obj = json.loads(s)

if api_obj is not None:
    api_path = f"{ts}_prompt_api{suffix}.json"
    Path(api_path).write_text(json.dumps(api_obj, ensure_ascii=False, indent=2), encoding="utf-8")
    print("OK:", ui_path, api_path)
else:
    print("OK:", ui_path, "(API用が作成できません:対応形式と異なっています)")

保存された 日時+workflow_ui.json をComfyUIに直接投げ込んで読み込ませてみてください。
もし、workflow キーが無い場合には内容を参照できるように、日時+出力ファイル名+snapshot.jsonも作成します。
補足:mp4のコメント欄に書かれたJSONコードもファイル化すれば対応できます。(以前のコードは未対応の場合は0サイズ出力になっていた)

その他(PNG画像がcivitai.comにないよ!PNGにワークフローが埋め込まれてない)

画像のリンク先のURLを確認して、URL内に/width=数字/が含まれているかを確認してください。ここの数字は、参照しているページによって変化します。

以下の例では、幅600サイズに縮小された画像のURLの一部には、

/width=600/

が含まれている。
この場合、画像が圧縮されており埋め込みコードが無効化されています。末尾の拡張子がjpegなら特に。

/width=true/

該当部分だけを「true」置き換えて、オリジナル画像を表示します。オリジナル画像を参照することで、拡張子もjpegからpngに自動で置き換わっているはずです。自分のローカル環境にオリジナル画像を保存してください。

これでワークフローの読み込みができるはずです。それでも読み込みできない時は、上記の「PNG形式に含まれるワークフローを抽出したい。」を試してみてね。

参考にされる場合は、あくまでも自己責任でご判断ください。

よかったらシェアしてね!
  • URLをコピーしました!
目次