# ComfyUI-ScriptFlow(日本語版) [[en](README.md) | ja] --- **Version:** 1.3.5 **License:** GPL-3.0 System One の判断モデルを使える、安全な Python 風スクリプトノードです。 TypeSafe AI の Jev API(フォールバックとしてローカルの GGUF LLM)に Yes/No・選択・スコアの質問をして、その答えを `if` 文の中でそのまま使えます。 ![MultiOutputScript (Jev) demo](images/MultiOutputScript_Jev.gif) デモ:`MultiOutputScript (Jev)` が TypeSafe API を使ってテキストプロンプトに合うアスペクト比を選び、各比率の確率を一覧にして、幅と高さを画像ノードに渡しています。 デモを簡略化した例です。テキストプロンプトを `in_text_1` につなぐと、その答えで幅と高さが決まります。 ```python if jev.yes(it1, "Does a portrait orientation suit this prompt?"): ov1, ov2 = 832, 1280 else: ov1, ov2 = 1280, 832 ``` System One モデルはテキストを生成せず、1 回の高速な処理で確率として答えを返します。そのため、答えをそのままワークフローの判断に使えます。 複数の入出力により、計算・論理判断・テキスト編集・条件分岐・解像度の選択・ファイル名の生成などを 1 つのノードで扱えます。これまで複数の基本ノードを組み合わせていた処理をまとめられます。スクリプトは制限付きのインタープリタで実行され、ファイルや OS にはアクセスしません。 ## インストール 1. `ComfyUI/custom_nodes/` ディレクトリに移動します。 2. このリポジトリをクローンします。 ```bash git clone https://github.com/kantan-kanto/ComfyUI-ScriptFlow ``` 3. ComfyUI を再起動します。 `MultiOutputScript` と `centi` ノードに追加のパッケージは不要です。`MultiOutputScript (Jev)` は、使うバックエンドに応じて以下のセットアップが必要です。 ### `MultiOutputScript (Jev)` のセットアップ #### TypeSafe APIを使う場合 1. ComfyUI を実行している Python 環境に SDK をインストールします。 ```bash pip install typesafe-sdk ``` 2. このカスタムノードの実際のインストール先ディレクトリに `api_key.txt` を作成し、TypeSafe の API キーを記入します(1 行、引用符なし)。 ``` ComfyUI/custom_nodes//api_key.txt ``` キーは実行のたびにこのファイルから読み込まれます。`api_key.txt` は `.gitignore` に登録されています。コミットしないでください。 #### ローカルモデル(フォールバック)を使う場合 - ComfyUI を実行している Python 環境に [llama-cpp-python](https://github.com/JamePeng/llama-cpp-python) をインストールします。JamePeng 版と、本家の [abetlen/llama-cpp-python](https://github.com/abetlen/llama-cpp-python) 0.3.35 で動作を確認しています。 - 指示追従型(instruction-tuned)の GGUF モデルを `ComfyUI/models/LLM`(または `models/text_encoders`)に置きます。GGUF にはチャットテンプレート(`tokenizer.chat_template` メタデータ)が含まれている必要があります。ほとんどの指示追従型 GGUF には含まれています。Qwen3.5-9B Q8_0 と Gemma-4-E4B Q8 で動作を確認しています。 - Cloudflare のオープンウェイトの判断モデル [Clef](https://huggingface.co/Cloudflare/clef-flash) を使う場合は、llama.cpp ネイティブ形式の Clef の GGUF([ggml-org/Clef-Flash-GGUF](https://huggingface.co/ggml-org/Clef-Flash-GGUF) など)を同じフォルダに置きます。JamePeng 版の llama-cpp-python 0.4.2 以降が必要です。通常のチャットモデルとして変換された Clef の GGUF は汎用モデルとして扱われ、Clef の判断ヘッドは使われません。Windows(Intel Arc、SYCL ビルド)の Clef-Flash Q8_0 で動作を確認しています。Linux と macOS では未確認です。 - Liquid AI のオープンウェイトの判断モデル [d1](https://huggingface.co/LiquidAI/d1-3B) を使う場合は、d1 の GGUF([LiquidAI/d1-3B-GGUF](https://huggingface.co/LiquidAI/d1-3B-GGUF) など)を同じフォルダに置きます。LFM2 モデルを読み込める llama-cpp-python が必要です。`mmproj` ファイルは不要です。Windows(Intel Arc、SYCL ビルド)の d1-3B Q8_0 と JamePeng 版の llama-cpp-python 0.4.2 で動作を確認しています。Linux と macOS では未確認です。 ## 主な機能 - 複数の入力:数値とテキストの入力を自由に組み合わせられます。 - 計算と論理:四則演算、比較、簡単な条件分岐に対応します。 - 複数の出力:結果を別々の出力ポートから下流のノードに渡せます。 - AI による判断:`MultiOutputScript (Jev)` で `jev.yes` / `jev.choice` / `jev.score` を確率付きで使えます。TypeSafe AI の Jev API、またはフォールバックとしてローカルの GGUF モデルを使います。 - 安全な実行:スクリプトは構文を制限した AST インタープリタで実行され、ファイルや OS にはアクセスしません。モデルや TypeSafe API にアクセスするのは、Jev ノードの `jev` 関数だけです。 ## 使用例 - Wan2.2 の I2V ワークフローで、入力画像が縦長か横長かに応じて推奨解像度 (512×384 または 384×512)を自動で選ぶ。 - ファイル名をまとめて生成する(例:ローカル LLM のプロンプト履歴、出力ログ)。 - 条件分岐でワークフローの動作を制御する(例:入力に応じてパラメータや経路を切り替える)。 - LLM の出力を解析・変換し、下流のノード向けの構造化された値にする。 - シード、しきい値、倍率などの数値パラメータを、独自の計算で動的に生成する。 - 生成前に、テキストプロンプトに合うアスペクト比とサイズを、各比率の確率付きで選ぶ(Jev ノードの初期スクリプト)。 - テキストの意味でワークフローを振り分ける(例:プロンプトが夜の情景かを判定する、主題を分類する)。 ## ノード ### `MultiOutputScript` **Category:** `utils` 入力(任意、接続可能): - `in_text_1`(任意の入力を受け付け、実行時に `str` か検証) - `in_text_2`(任意の入力を受け付け、実行時に `str` か検証) - `in_text_3`(任意の入力を受け付け、実行時に `str` か検証) - `in_value_1`(任意の入力を受け付け、実行時に `int` または `float` か検証) - `in_value_2`(任意の入力を受け付け、実行時に `int` または `float` か検証) - `in_value_3`(任意の入力を受け付け、実行時に `int` または `float` か検証) - `code`(複数行の文字列) 出力: - `out_text_1`(STRING) - `out_text_2`(STRING) - `out_text_3`(STRING) - `out_value_1`(INT) - `out_value_2`(INT) - `out_value_3`(INT) 数値出力についての注意: - 出力は `INT` です。 - 整数入力から `FLOAT` の値が必要な場合は、`centi` ノードを使ってください。 - 値が小数の場合、出力時に小数部分は切り捨てられます。 - 四捨五入や切り上げをしたい場合は、スクリプト内で処理してください(例:`round(...)`、`math.ceil(...)`)。 ### スクリプト変数 `code` の中では次の変数を使います。 - 入力:`it1`、`it2`、`it3`、`iv1`、`iv2`、`iv3` - 出力:`ot1`、`ot2`、`ot3`、`ov1`、`ov2`、`ov3` 代入しなかった出力は `None` になります。 ### スクリプト例 ```python # Example: keep landscape at 512x384, portrait at 384x512 # Connect GetImageSize width/height to in_value_1/in_value_2 # iv1: image width, iv2: image height w, h = iv1, iv2 ov1, ov2 = (512, 384) if w >= h else (384, 512) ``` ### `MultiOutputScript (Jev)` **Category:** `utils` `MultiOutputScript` と同じ入力・出力・スクリプトの規則に加えて、`jev` 名前空間を使えます。TypeSafe AI の判断モデルである [Jev](https://typesafe.ai) に、テキストについての型付きの質問をし、その答えでスクリプトを分岐できます。 - **TypeSafe API**(基本):質問をインターネット経由で Jev に送ります。 - **ローカル GGUF モデル**(フォールバック):Jev API を使えない場合や、テキストを手元のマシンから出したくない場合に使います。汎用の指示追従型モデルで Jev 風の判断を近似するもので、[SemIf (OpenJev)](https://github.com/TheoLeeCJ/SemIf-OpenJev) の方式に従い、文字を割り当てた選択肢の次トークン確率を 1 回の順伝播で読み取ります。テキストは生成せず、ネットワークにも何も送りません。答えや確率は Jev とは異なります。 - **ローカルの Clef モデル**:[Clef](https://huggingface.co/Cloudflare/clef-flash) は、Jev と同じ種類の質問に答えるオープンウェイトの判断モデルです。GGUF のメタデータ `general.architecture` が `clef` のモデルを、Clef として扱います。Clef の GGUF を選ぶと、上の近似の代わりにモデルの判断ヘッドを使い、同じ `state` についての質問すべてに 1 回の順伝播で答えます。`state` と質問を Clef の入力書式に並べ、判断ヘッドが出す選択肢ごとのスコアを、質問ごとに softmax で確率にします。文章は生成せず、ネットワークにも何も送信しません。答えと確率は Jev とは異なります。 - **ローカルの d1 モデル**:[d1](https://huggingface.co/LiquidAI/d1-3B) は、Jev と同じ種類の質問に答えるオープンウェイトの判断モデルです。GGUF のメタデータ `lfm2.decision.type` が `lfm2-d1` のモデルを、d1 として扱います。d1 の GGUF を選ぶと、上の文字を割り当てた選択肢の代わりに、d1 が学習したプロンプト形式で質問します。答えは、d1 が学習した返答の次トークン確率から読み取ります。`jev.noul` では yes か no、`jev.choice` では選択肢のコード、`jev.score` では数字です。文章は生成せず、ネットワークにも何も送信しません。答えと確率は Jev とは異なります。 スクリプトはどれでも同じです。各バックエンドのセットアップは[インストール](#インストール)を参照してください。 追加の入力: - `model`:`TypeSafe API`、またはローカルの GGUF モデル(`mmproj-*` ファイルは表示されません)。 - `max_jev_calls`(INT、初期値 8):1 回の実行での Jev リクエストの上限です。同じ `state` についての質問は 1 回のリクエストにまとめて送るため([質問のまとめ送り](#質問のまとめ送り)を参照)、質問数ではなくリクエスト数を制限します。上限を超えるリクエストは送る前にエラーで止まります。同じ実行の中で同じ質問は 1 回しか問い合わせません。 関数(`state` は文字列・辞書・リスト。テキストのみ): - `jev.yes(state, question[, threshold])` → `bool`(Yes の確率が `threshold` 以上か。初期値 `0.5`) - `jev.noul(state, question)` → Yes の確率(`0.0`〜`1.0`)。しきい値と比較して使ってください。そのまま条件に使うとエラーになります。 - `jev.choice(state, question, options)` → 最も確からしい選択肢(`str`)。`options` はラベルのリスト、またはラベルから説明への辞書です(汎用のローカルモデルでは 2〜16 個)。 - `jev.probabilities(state, question, options)` → 各選択肢とその確率の `dict`(合計 `1.0`)。`options` は `jev.choice` と同じです。同じ引数で両方を呼んでも、問い合わせは 1 回です。 - `jev.score(state, question, levels)` → 順序付きの段階の説明に対する期待値(`float`、`0`〜`len(levels) - 1`)(汎用のローカルモデルでは 2〜16 段階) ```python # it1: a prompt text if jev.yes(it1, "Does this prompt describe a night scene?", 0.7): ov1 = 30 else: ov1 = 20 kind = jev.choice(it1, "Which orientation suits this prompt?", ["portrait", "landscape"]) ov2, ov3 = (384, 512) if kind == "portrait" else (512, 384) probs = jev.probabilities(it1, "Which orientation suits this prompt?", ["portrait", "landscape"]) ot1 = f"{kind} (portrait: {probs['portrait']:.3f}, landscape: {probs['landscape']:.3f})" ``` ノードの初期スクリプトは、`it1` のプロンプトに合う 8 種類のアスペクト比から 1 つを選び、`in_value_1` のメガピクセル数(未接続時は `1.0`。幅と高さは 64 の倍数に丸め)に合わせてサイズを決めます。幅と高さを `out_value_1` / `out_value_2` に、各比率の確率を `out_text_1` に出力します。 各関数は 1 つの質問をします。[TypeSafe Python SDK](https://pypi.org/project/typesafe-sdk/) で書いた場合のコードとの対応を示します。 | ScriptFlow | TypeSafe SDK | | --- | --- | | `jev.noul(state, q)` | `client.system_one(state=state, questions={"q": Noul(instructions=q)}).answers["q"].noul` | | `jev.yes(state, q, t)` | `... .answers["q"].noul >= t` | | `jev.choice(state, q, options)` | `client.system_one(state=state, questions={"q": Choice(instructions=q, criteria=options)}).answers["q"].choice` | | `jev.probabilities(state, q, options)` | `... .answers["q"].probabilities`(`jev.choice` と同じリクエスト) | | `jev.score(state, q, levels)` | `client.system_one(state=state, questions={"q": Score(instructions=q, criteria=levels)}).answers["q"].score` | リストで渡した `options` は `criteria={label: None, ...}` として送られます。`confidence`、Score の段階ごとの確率、Noul の `criteria` は利用できません。 #### 質問のまとめ送り 質問は可能な範囲でまとめて送られます。 1. スクリプトの実行中、`jev` の質問はその場では送らず、順に溜めておきます。 2. 答えを変数やリストに入れる、自分で定義した関数に渡す、`float()` を通す、といった使い方なら実行を続けます。`if` の条件、比較、計算、f 文字列、`sorted`、`join`、出力などで答えの値を初めて使ったところで、実行を止めます。 3. 溜めた質問を `state` ごとに 1 回のリクエストで送り、スクリプトを最初から実行し直します。答えが分かった質問はその値を返し、新しい質問は同じように溜めます。 `state` が違うリクエストは、TypeSafe API では同時に送り、ローカルモデルでは順に処理します。質問を 1 つずつ問い合わせる場合と同じ質問をし、同じ結果を出力します。1 回の実行の中では、実行し直すたびに `random` の状態と `datetime.datetime.now()` の時刻が同じところから始まるので、実行し直しても質問は変わりません。 次の例では、`a` は `if` まで使われないので、`q1` と `q2` は 1 回のリクエストで送られます。`q3` は `a` が分かってから 2 回目のリクエストで送られます。 ```python a = jev.noul(it1, "q1") b = jev.noul(it1, "q2") if a > 0.5: c = jev.noul(it1, "q3") ``` 多くの質問を 1 回のリクエストで送るには、`state` を同じにして、質問ごとに違う内容は質問文に入れてください。 TypeSafe API では、1 回のリクエストが [TypeSafe Python SDK](https://pypi.org/project/typesafe-sdk/) の `system_one` 呼び出し 1 回にあたります。上の例の 1 回目のリクエストは次のとおりです。 ```python client.system_one(state=it1, questions={"q0": Noul(instructions="q1"), "q1": Noul(instructions="q2")}) ``` 注意: - TypeSafe API は `state` と質問をインターネット経由で送信します。外部に出せないテキストには使わないでください。 - 汎用のローカルモデルは、質問 1 つにつき 1 回の順伝播で処理します(GPU 上の 4B〜9B モデルで約 1〜1.5 秒)。Clef モデルは、1 回のリクエストに含まれる質問すべてを 1 回の順伝播で処理します(Intel Arc の内蔵 GPU 上の Clef-Flash Q8_0 で、300 トークンのリクエストが約 3 秒)。d1 モデルは、質問 1 つにつき 1 回の順伝播で処理します(同じ GPU 上の d1-3B Q8_0 で約 0.5 秒)。モデルは実行ごとに最初の質問で読み込まれ(数秒)、実行が終わるとアンロードされて、ワークフローの残りのために VRAM を解放します。 - ローカルモデルの確率は較正されておらず、モデルによって変わります。しきい値は実際の入力で試して決めてください。 - Qwen3.5-9B での試験では、汎用のローカルモデルは、テキストから答えが決まらない質問に「No」と答えました。`jev.noul` の低い値は、「起こりにくい」ではなく「真だとは言えない」を表すことがあります。この2つを区別したい場合は、主張とその否定形の両方について聞くか、`jev.score` でパーセントの段階を選ばせてください。測定の結果とスクリプトの例は、[汎用のローカルモデルは、不確かな問いにどう答えるか](docs/local-model-uncertainty/README.ja.md) にあります。 - ローカルモデルのプロンプトは 8,192 トークンまでです。汎用モデルでは `state` と質問 1 つ、Clef では `state` と 1 回のリクエストの質問すべてがこの範囲に収まる必要があります。Clef はテキストのみに対応しています。 - d1 モデルでは、`jev.choice` と `jev.probabilities` の選択肢は 2〜26 個、`jev.score` の段階は 2〜10 個です。画像には対応していません。 - d1-3B での試験では、似た選択肢に確率が分かれ、1 つだけ性質の違う選択肢が最多になることがありました。アスペクト比の例では、縦長の 3 つの比率の合計が 0.49 だったのに対し、0.35 の `1:1` が選ばれました。選択肢がグループに分かれる場合は、`jev.probabilities` をグループごとに合計してから選んでください。 - 実行ごとにコンソールへ `[ComfyUI-ScriptFlow] Jev requests: N, states: S, questions: Q, responses: M` を出力します。送ったリクエスト数、異なる `state` の数、質問数、返信があったリクエスト数です。返信数がリクエスト数より少なければ、途中のリクエストが失敗しています。同じ実行のキャッシュから答えた質問は数えません。 - `typesafe-sdk` がインストールされていると、`httpx2`(SDK が使う HTTP クライアント)と `typesafe_sdk` のロガーを WARNING に設定するため、リクエストごとの INFO ログは表示されません。 - `MultiOutputScript` はモデルを読み込まず、ネットワークにもアクセスしません。`jev` は使えません。 - このプロジェクトは独立したもので、TypeSafe AI とは関係ありません。 ### `centi` **Category:** `utils` 入力(任意、接続可能): - `int_1`(INT) - `int_2`(INT) - `int_3`(INT) 出力: - `float_1`(FLOAT)= `int_1 / 100` - `float_2`(FLOAT)= `int_2 / 100` - `float_3`(FLOAT)= `int_3 / 100` 注意: - `int_n` が接続されていない場合、対応する `float_n` の出力は `None` です。 - 意図的に最小限の、接続のみのノードにしています。 --- ## 実行環境 スクリプトは Python の AST で解析され、ScriptFlow の安全なインタープリタで評価されます。代入、計算、分岐、ループ、ユーザー定義のヘルパー関数、辞書、リスト、文字列、一部のメソッドなど、ワークフローでよく使う処理に対応しています。 ### 使える関数 - `len`、`min`、`max`、`sum`、`abs`、`round` - `int`、`float`、`str`、`bool` - `sorted`、`reversed` - `enumerate`、`range`、`zip` - `any`、`all`、`pow`、`divmod` - `list`、`dict`、`tuple` - `jev.yes`、`jev.noul`、`jev.choice`、`jev.probabilities`、`jev.score`(`MultiOutputScript (Jev)` のみ。[該当の節](#multioutputscript-jev)を参照) ### 使える名前空間 - `random`:`random`、`randint`、`uniform`、`choice` - `datetime`:`datetime.now`、`date.today`、`year`・`month`・`day`・`hour`・`minute`・`second` などの日時の項目、`isoformat` と `strftime`(`strftime` は v1.2.0 以降) - `math`:`ceil`、`floor`、`sqrt`、`sin`、`cos`、`tan`、`asin`、`acos`、`atan`、`atan2`、`log`、`log10`、`exp`、`pow`、`fabs`、`isfinite`、`pi`、`e`、`tau` ### 使えるメソッド - `str`:`replace`、`find`、`strip`、`split`、`splitlines`、`startswith`、`endswith`、`join`、`lower`、`upper` - `list`:`append` ### 使えないもの - `import` 文 - ファイルへのアクセス(`open` など) - OS の操作(`os`、`sys`) - Python の動的な実行 - セーフモードで禁止している構文:`Import`、`ImportFrom`、`Global`、`Nonlocal`、`ClassDef`、`Try`、`With`、`AsyncWith`、`Lambda`、`Delete`、`Yield`、`Await` - セーフモードで禁止している呼び出し:動的な実行、ファイルや入力へのアクセス、実行時の名前空間の参照、属性の動的な変更 ### 実行時の注意 - `random` や `datetime` を使うと、出力は毎回変わる可能性があります。 - v1.3.2 以降では、`datetime.datetime.now()` と `datetime.date.today()` は実行を開始した時刻を返すため、1 つのスクリプトの中で何度呼んでも同じ値になります。 - v1.2.0 以降では、`datetime.strftime(...)` でタイムスタンプを簡潔に整形できます。 ```python now = datetime.datetime.now() ot1 = now.strftime("%Y%m%d%H%M%S") ``` - v1.2.0 より前のバージョンでは、同じタイムスタンプを手作業で整形します。 ```python now = datetime.datetime.now() ot1 = f'{now.year:04d}{now.month:02d}{now.day:02d}{now.hour:02d}{now.minute:02d}{now.second:02d}' ``` - 型が合わない場合はエラーになり、実行が止まります。 - セーフモードでは評価の前に AST を検証し、ステップ数やループ回数の上限を超えたスクリプトを止めます。 - 信頼できるスクリプトやワークフローだけを使ってください。出所の分からないコードは実行しないでください。 ## 例 ### ワークフローの例 ``` examples/ ├─ example_workflow.json ``` ## 応用レシピ ### `LLM Dialogue Cycle` の会話ログを `dialogue_segments_json` に変換する
ComfyUI-LLM-Session の会話ログ変換レシピを表示 `ComfyUI-LLM-Session` は、`LLM Dialogue Cycle` の結果全体を `transcript_text` として出力します。 そのテキストを `MultiOutputScript.in_text_1` につなぎ、下のスクリプトを `code` 欄に貼り付けると、 後段の会話用 TTS ノード向けに、話者タグ付きの JSON テキストを生成できます。 推奨するワークフロー: ```text LLM Dialogue Cycle -> transcript_text -> MultiOutputScript.in_text_1 -> MultiOutputScript.out_text_1 -> dialogue_segments_json input of a TTS node ``` 次のコードを `MultiOutputScript.code` に貼り付けます。 ```python # Convert ComfyUI-LLM-Session "LLM Dialogue Cycle" transcript_text # into dialogue_segments_json for TTS. # Only text inside Japanese corner quotes 「...」 is used as spoken dialogue. # # Input: # it1: transcript_text # # Output: # ot1: JSON text with schema "kantan.dialogue.v1" # # Expected transcript lines: # [2026-05-16T12:00:00] USER -> A: initial prompt # [2026-05-16T12:00:01] A: 彼女は少し考えた。「こんにちは。」 # [2026-05-16T12:00:02] B: 彼は笑った。「やあ。」「調子はどう?」 # # The parser also accepts the unicode arrow form: # USER → A: def json_escape(value): s = str(value) s = s.replace("\\", "\\\\") s = s.replace('"', '\\"') s = s.replace("\n", "\\n") s = s.replace("\r", "\\r") s = s.replace("\t", "\\t") return s def extract_spoken_text(value): source = str(value) parts = [] position = 0 while position < len(source): start = source.find("「", position) if start < 0: position = len(source) else: end = source.find("」", start + 1) if end < 0: position = len(source) else: spoken = source[start + 1:end].strip() if spoken: parts.append(spoken) position = end + 1 return "\n".join(parts) transcript = str(it1 or "") lines = transcript.splitlines() utterances = [] current = None for raw_line in lines: line = str(raw_line) speaker = None text = None timestamp = "" if line.startswith("[") and "] " in line: close_index = line.find("] ") timestamp = line[1:close_index] rest = line[close_index + 2:] if rest.startswith("A:"): speaker = "A" text = rest[2:].strip() elif rest.startswith("B:"): speaker = "B" text = rest[2:].strip() elif rest.startswith("USER -> A:"): speaker = "USER" text = rest[len("USER -> A:"):].strip() elif rest.startswith("USER → A:"): speaker = "USER" text = rest[len("USER → A:"):].strip() if speaker == "A" or speaker == "B": if current is not None: utterances.append(current) current = { "speaker": speaker, "text": text, "timestamp": timestamp, } else: if current is not None: if current["text"]: current["text"] = current["text"] + "\n" + line else: current["text"] = line if current is not None: utterances.append(current) items = [] index = 1 for u in utterances: spoken_text = extract_spoken_text(u["text"]) if not spoken_text: continue item = ( '{"index":' + str(index) + ',"speaker":"' + json_escape(u["speaker"]) + '","text":"' + json_escape(spoken_text) + '","timestamp":"' + json_escape(u["timestamp"]) + '"}' ) items.append(item) index = index + 1 ot1 = ( '{"schema":"kantan.dialogue.v1",' + '"source":"ComfyUI-LLM-Session LLM Dialogue Cycle",' + '"utterances":[' + ",".join(items) + "]}" ) ``` 出力される `out_text_1` は、次のような JSON 文字列です。 ```json { "schema": "kantan.dialogue.v1", "source": "ComfyUI-LLM-Session LLM Dialogue Cycle", "utterances": [ { "index": 1, "speaker": "A", "text": "こんにちは。", "timestamp": "2026-05-16T12:00:01" }, { "index": 2, "speaker": "B", "text": "やあ。\n調子はどう?", "timestamp": "2026-05-16T12:00:02" } ] } ``` 注意: - TTS がキャラクターのセリフだけを読むよう、`USER -> A` / `USER → A` は意図的に除外しています。 - モデルの出力が複数行にわたる場合は、直前の `A` または `B` の発話に含めます。 - `utterances[].text` に出力するのは `「...」` の中のテキストだけです。かぎかっこの外の地の文や心の声は除外します。 - 1 つの発話に複数のかぎかっこがある場合は、改行でつなぎます。 - `「...」` を含まない発話は、TTS 用のスクリプトから除外します。 - このレシピでは、`out_text_2`、`out_text_3`、数値出力は使いません。
### プロンプトエンハンサーの出力をシステムプロンプトと照らし合わせる
プロンプトエンハンサーのルール確認レシピを表示 プロンプトエンハンサーは、短い依頼文を、システムプロンプトのルールに従って画像・動画モデル用のプロンプトに書き直す LLM です。 このレシピは、そのシステムプロンプトをルールに分け、エンハンサーの出力が各ルールを破っていないかを Jev に尋ねて、 スコアと、破っている疑いのあるルールを出力します。ルールは接続したシステムプロンプトから読み取るので、特定のエンハンサーに依存しません。 下のスクリプトを `MultiOutputScript (Jev)` の `code` 欄に貼り付けてください。 推奨するワークフロー: ```text enhancer output -> MultiOutputScript (Jev).in_text_1 system prompt -> MultiOutputScript (Jev).in_text_2 user request (optional) -> MultiOutputScript (Jev).in_text_3 MultiOutputScript (Jev) -> out_text_1 report -> out_text_2 likely violations only -> out_text_3 rules extracted from the system prompt -> out_value_1 adherence score (0-100) -> out_value_2 number of likely violations ``` 次のコードを `MultiOutputScript (Jev).code` に貼り付けます。 ```python # エンハンサー出力がシステムプロンプトにどれだけ従っているかを評価する(モデル非依存) # it1: エンハンサーの出力 # it2: システムプロンプト(テキストそのもの、または "system_prompt" を含む JSON 設定の中身) # it3: ユーザー文(任意。ルールがユーザー文を参照する場合の判定材料) # ot1: レポート(違反の疑いが強い順) / ot2: 違反の疑いがあるルールのみ / ot3: 抽出したルール一覧 # ov1: 準拠スコア (0-100) / ov2: 違反の疑いがあるルール数 # Jev リクエストは 2 回(全ルールで 1 回、総合評価で 1 回。OVERALL = False なら 1 回)。max_jev_calls は 2 以上 MAX_RULES = 40 # これを超えるルールは評価しない THRESHOLD = 0.5 # 違反確率がこれ以上なら違反とみなす SKIP_HEADINGS = ["例", "example", "sample", "思考", "手順", "thinking", "reasoning"] # この語を含む見出しの節は評価しない OVERALL = True # システムプロンプト全体に対する総合評価も行う def has_any(s, words): for w in words: if w in s: return True return False output = str(it1 or "").strip() sp = str(it2 or "") user_text = str(it3 or "").strip() if sp.strip().startswith("{"): k = sp.find('"system_prompt"') if k < 0: raise ValueError("JSON に system_prompt がありません") tail = sp[sp.find('"', sp.find(":", k) + 1) + 1:] tail = tail.replace("\\\\", "\x00").replace('\\"', "\x01") sp = tail[:tail.find('"')].replace("\\n", "\n").replace("\\t", "\t").replace("\x01", '"').replace("\x00", "\\") # 見出しごとに、行頭の項目または段落を1ルールとし、字下げされた行は直前のルールに含める rules = [] heading = "" skip = False for line in sp.splitlines(): s = line.strip() if not s: continue if s[0] in "【#[-": if s[:3] == "---": continue if s[0] != "-" and (s[0] != "[" or s[-1] == "]"): heading = s skip = has_any(s.lower(), SKIP_HEADINGS) continue if skip: continue if rules and line[:1] in " \t ": rules[-1][1] = rules[-1][1] + "\n" + s else: rules.append([heading, s]) truncated = len(rules) > MAX_RULES rules = rules[:MAX_RULES] # 全ルールで state を共通にし、ルールは質問文に入れる(1 リクエストにまとまる) state = {"output": output} if user_text: state["user_input"] = user_text results = [] for r in rules: q = "Does the output clearly break the instruction below? Answer no if the instruction does not concern the output, or if judging it needs a reference image or anything else not given here." if r[0]: q += "\nSection: " + r[0] p = jev.noul(state, q + "\nInstruction: " + r[1]) results.append([float(p), r[0], r[1]]) # 総合評価はシステムプロンプト全体を state にするので別リクエスト。並べ替えの前に聞いてルールと同時に送る if OVERALL: overall_state = {"system_prompt": sp, "output": output} if user_text: overall_state["user_input"] = user_text level = jev.score(overall_state, "How faithfully does the output follow the system prompt as a whole?", ["ignores most instructions", "follows some instructions", "follows about half", "follows most instructions", "follows every instruction"]) ranked = list(reversed(sorted(results))) lines = [] issues = [] total = 0.0 for r in ranked: total += r[0] bad = r[0] >= THRESHOLD line = ("[違反?] " if bad else "[OK] ") + f"p={r[0]:.2f} {r[1]} " + r[2].splitlines()[0][:70] lines.append(line) if bad: issues.append(line) score = round(100 * (1 - total / len(ranked))) if ranked else 0 header = f"準拠スコア: {score}/100 (ルール {len(ranked)} 件中、違反の疑い {len(issues)} 件)" if truncated: header += f"\n※ルールが {MAX_RULES} 件を超えたため、以降は未評価" if OVERALL: header += f"\n総合評価: {level:.2f} / 4" ot1 = header + "\n" + "\n".join(lines) ot2 = header + "\n" + "\n".join(issues) rule_lines = [] for i, r in enumerate(rules): rule_lines.append(f"{i + 1}. {r[0]} {r[1]}") ot3 = "\n\n".join(rule_lines) ov1 = score ov2 = len(issues) ``` 出力される `out_text_1` は、次のようなレポートです。 ```text 準拠スコア: 71/100 (ルール 4 件中、違反の疑い 1 件) 総合評価: 3.10 / 4 [違反?] p=0.74 ## 出力形式 - 文章は書かない。 [OK] p=0.21 あなたは SDXL 用のプロンプトエンハンサーです。 [OK] p=0.12 ## 出力形式 - 先頭は「masterpiece, best quality」にする。 [OK] p=0.09 ## 出力形式 - カンマ区切りのタグを 1 行で出力する。 ``` 注意: - v1.3.2 以降が必要です。v1.3.2 以降では、同じ `state` についての質問を 1 回のリクエストにまとめて送ります。このスクリプトのリクエストは 2 回なので、`max_jev_calls` は 2 以上にしてください。 - `p` は、出力がそのルールを破っていると Jev が判断した確率です。確実な検査ではありません。テストでは、守られているルールを違反と判定することも、破られているルールを見逃すこともあり、`THRESHOLD` 付近の値は実行のたびに変わりました。違反と判定されたルールは、確認すべき候補として扱ってください。 - このレシピは、TypeSafe API の Jev で作成しました。テストでは、ローカルの Clef-Flash モデルは、出力が守っているルールも含めて大半のルールを違反と判定したため、このレシピには向きません。 - 参照画像のように、スクリプトに渡していない情報が必要なルールは判定できません。ユーザーの依頼文に関するルールを判定するには、依頼文を `in_text_3` につないでください。 - `in_text_2` には、システムプロンプトのテキストそのもの、または `"system_prompt"` キーを持つ JSON 設定のテキストを渡せます。JSON の `\uXXXX` エスケープは復元しません。 - `#`、`【`、`[...]` で始まる行は見出しとして扱います。見出しの下では、行頭から始まる行を 1 つのルールとし、字下げされた行は直前のルールに含めます。システムプロンプトがどう分けられたかは `out_text_3` で確認できます。 - 見出しに `SKIP_HEADINGS` の語を含む節は評価しません。例や思考の手順がルールとして判定されるのを防ぐためです。 - 評価するのは先頭から `MAX_RULES` 件までです。システムプロンプトが非常に長いと、スクリプトのステップ上限を超えます。その場合は、下の[長いシステムプロンプトの場合](#長いシステムプロンプトの場合)を参照してください。 - TypeSafe API を使う場合、システムプロンプト、エンハンサーの出力、ユーザーの依頼文はインターネット経由で送信されます。 - `out_text_3` と数値出力は必要に応じて使ってください。`out_value_3` は使いません。 #### 長いシステムプロンプトの場合 上のスクリプトはシステムプロンプトを 1 行ずつ評価するため、数百行あるシステムプロンプトでは `Script exceeded step limit` で止まります。 その場合は、下のスクリプトを使ってください。システムプロンプトを空行の位置で `PART_CHARS` 字程度の部分に区切り、 出力がその部分の指示を破っていないかを、部分ごとに Jev に尋ねます。入力と出力の接続は同じです。 ```python # 長いシステムプロンプト向け:エンハンサー出力がシステムプロンプトにどれだけ従っているかを、部分ごとに評価する # システムプロンプトを空行の位置で PART_CHARS 字程度の部分に区切り、部分ごとに違反の有無を Jev に聞く # (1 行ずつ評価する prompt_rule_check.py がステップ上限を超える長さのシステムプロンプト用) # it1: エンハンサーの出力 # it2: システムプロンプト(テキストそのもの、または "system_prompt" を含む JSON 設定の中身) # it3: ユーザー文(任意。指示がユーザー文を参照する場合の判定材料) # ot1: レポート(違反の疑いが強い順) / ot2: 違反の疑いがある部分のみ / ot3: 区切った部分の一覧 # ov1: 準拠スコア (0-100) / ov2: 違反の疑いがある部分の数 # Jev リクエストは 2 回(全部分で 1 回、総合評価で 1 回。OVERALL = False なら 1 回)。max_jev_calls は 2 以上 PART_CHARS = 1000 # 1 つの部分のおおよその文字数。小さくすると細かく評価する(質問数が増える) MAX_PARTS = 60 # これを超える部分は評価しない THRESHOLD = 0.5 # 違反確率がこれ以上なら違反とみなす OVERALL = True # システムプロンプト全体に対する総合評価も行う output = str(it1 or "").strip() sp = str(it2 or "") user_text = str(it3 or "").strip() if sp.strip().startswith("{"): k = sp.find('"system_prompt"') if k < 0: raise ValueError("JSON に system_prompt がありません") tail = sp[sp.find('"', sp.find(":", k) + 1) + 1:] tail = tail.replace("\\\\", "\x00").replace('\\"', "\x01") sp = tail[:tail.find('"')].replace("\\n", "\n").replace("\\t", "\t").replace("\x01", '"').replace("\x00", "\\") sp = sp.replace("\r\n", "\n").strip() # PART_CHARS 字を超えた後の最初の空行で区切る(段落の途中では切らない) parts = [] pos = 0 while pos < len(sp) and len(parts) < MAX_PARTS: end = sp.find("\n\n", pos + PART_CHARS) if end < 0: end = len(sp) parts.append(sp[pos:end].strip()) pos = end truncated = pos < len(sp) # 全部分で state を共通にし、部分は質問文に入れる(1 リクエストにまとまる) state = {"output": output} if user_text: state["user_input"] = user_text results = [] for i, part in enumerate(parts): p = jev.noul(state, "Does the output clearly break any instruction in the part of the system prompt below? Answer no if the instructions do not concern the output, or if judging them needs a reference image or anything else not given here.\nPart of the system prompt:\n" + part) results.append([float(p), i + 1]) # 総合評価はシステムプロンプト全体を state にするので別リクエスト。並べ替えの前に聞いて部分と同時に送る if OVERALL: overall_state = {"system_prompt": sp, "output": output} if user_text: overall_state["user_input"] = user_text level = jev.score(overall_state, "How faithfully does the output follow the system prompt as a whole?", ["ignores most instructions", "follows some instructions", "follows about half", "follows most instructions", "follows every instruction"]) lines = [] issues = [] total = 0.0 for r in reversed(sorted(results)): total += r[0] bad = r[0] >= THRESHOLD # 部分の先頭行を見出し代わりに表示する(区切り線だけの行は飛ばす) title = parts[r[1] - 1].strip("=-_*# \n\t").splitlines()[0][:60] line = ("[違反?] " if bad else "[OK] ") + f"p={r[0]:.2f} 部分{r[1]}: {title}" lines.append(line) if bad: issues.append(line) score = round(100 * (1 - total / len(parts))) if parts else 0 header = f"準拠スコア: {score}/100 (部分 {len(parts)} 件中、違反の疑い {len(issues)} 件)" if truncated: header += f"\n※部分が {MAX_PARTS} 件を超えたため、以降は未評価" if OVERALL: header += f"\n総合評価: {level:.2f} / 4" ot1 = header + "\n" + "\n".join(lines) ot2 = header + "\n" + "\n".join(issues) part_texts = [] for i, part in enumerate(parts): part_texts.append(f"--- 部分{i + 1} ---\n{part}") ot3 = "\n\n".join(part_texts) ov1 = score ov2 = len(issues) ``` 出力される `out_text_1` は、次のようなレポートです。 ```text 準拠スコア: 74/100 (部分 19 件中、違反の疑い 2 件) 総合評価: 2.90 / 4 [違反?] p=0.78 部分7: 出来事はユーザーが指定した順序のままにする。 [違反?] p=0.61 部分12: カメラの動きは、ユーザーが求めたときだけ書く。 [OK] p=0.34 部分1: あなたは動画モデル用のプロンプトエンハンサーです。 [OK] p=0.22 部分3: 長さはユーザーが指定した秒数にする。 ``` 注意: - `max_jev_calls`、`p`、`in_text_2`、TypeSafe API についての上の注意は、このスクリプトにも当てはまります。 - 部分は、`PART_CHARS` 字を超えた後の最初の空行で区切るので、段落の途中では切れません。見出しや箇条書きは解釈しないので、どんな書き方のシステムプロンプトでも使えますが、例の節は飛ばしません。 - レポートは、各部分をその先頭の行で示します。違反と判定された部分の全文は `out_text_3` で読み、どの指示が問題なのかを確認してください。 - 1 つの質問に複数の指示が含まれるので、1 行ずつ評価するスクリプトより結果は粗くなります。`PART_CHARS` を小さくすると、部分が細かくなり、質問数が増えます。 - テストでは、約 20,000 字のシステムプロンプトが 19 個の部分に区切られ、約 2,200 ステップで実行できました。ステップ上限には十分収まります。
## ライセンス このプロジェクトは **GNU General Public License v3.0** の下でライセンスされています。 **Copyright (C) 2026 kantan-kanto** GitHub: https://github.com/kantan-kanto This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see https://www.gnu.org/licenses/. ## サポート - **Issues**:不具合の報告や機能の要望は GitHub Issues へお願いします - **Documentation**:バージョン履歴は [CHANGELOG.md](CHANGELOG.md) を参照してください - **Examples**:ワークフローのテンプレートは [examples/](examples/) を参照してください --- ## 変更履歴 詳しいバージョン履歴は [CHANGELOG.md](CHANGELOG.md) を参照してください。 ### 現在のバージョン:1.3.5 - `MultiOutputScript (Jev)` が、ローカルの d1 の GGUF モデル(Liquid AI のオープンウェイトの判断モデル)を動かせるように。d1 が学習したプロンプト形式で質問する - Jev、Clef-Flash、汎用のローカルモデルが、答えを知りようのない問いにどう答えるかを調べた技術レポートを追加