45 KiB
ComfyUI-ScriptFlow(日本語版)
[en | ja]
Version: 1.3.5 License: GPL-3.0
System One の判断モデルを使える、安全な Python 風スクリプトノードです。
TypeSafe AI の Jev API(フォールバックとしてローカルの GGUF LLM)に Yes/No・選択・スコアの質問をして、その答えを if 文の中でそのまま使えます。
デモ:MultiOutputScript (Jev) が TypeSafe API を使ってテキストプロンプトに合うアスペクト比を選び、各比率の確率を一覧にして、幅と高さを画像ノードに渡しています。
デモを簡略化した例です。テキストプロンプトを in_text_1 につなぐと、その答えで幅と高さが決まります。
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 にはアクセスしません。
インストール
ComfyUI/custom_nodes/ディレクトリに移動します。- このリポジトリをクローンします。
git clone https://github.com/kantan-kanto/ComfyUI-ScriptFlow - ComfyUI を再起動します。
MultiOutputScript と centi ノードに追加のパッケージは不要です。MultiOutputScript (Jev) は、使うバックエンドに応じて以下のセットアップが必要です。
MultiOutputScript (Jev) のセットアップ
TypeSafe APIを使う場合
- ComfyUI を実行している Python 環境に SDK をインストールします。
pip install typesafe-sdk - このカスタムノードの実際のインストール先ディレクトリに
api_key.txtを作成し、TypeSafe の API キーを記入します(1 行、引用符なし)。キーは実行のたびにこのファイルから読み込まれます。ComfyUI/custom_nodes/<your-installation-folder>/api_key.txtapi_key.txtは.gitignoreに登録されています。コミットしないでください。
ローカルモデル(フォールバック)を使う場合
- ComfyUI を実行している Python 環境に llama-cpp-python をインストールします。JamePeng 版と、本家の 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 を使う場合は、llama.cpp ネイティブ形式の Clef の GGUF(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 を使う場合は、d1 の GGUF(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 になります。
スクリプト例
# 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 に、テキストについての型付きの質問をし、その答えでスクリプトを分岐できます。
- TypeSafe API(基本):質問をインターネット経由で Jev に送ります。
- ローカル GGUF モデル(フォールバック):Jev API を使えない場合や、テキストを手元のマシンから出したくない場合に使います。汎用の指示追従型モデルで Jev 風の判断を近似するもので、SemIf (OpenJev) の方式に従い、文字を割り当てた選択肢の次トークン確率を 1 回の順伝播で読み取ります。テキストは生成せず、ネットワークにも何も送りません。答えや確率は Jev とは異なります。
- ローカルの Clef モデル:Clef は、Jev と同じ種類の質問に答えるオープンウェイトの判断モデルです。GGUF のメタデータ
general.architectureがclefのモデルを、Clef として扱います。Clef の GGUF を選ぶと、上の近似の代わりにモデルの判断ヘッドを使い、同じstateについての質問すべてに 1 回の順伝播で答えます。stateと質問を Clef の入力書式に並べ、判断ヘッドが出す選択肢ごとのスコアを、質問ごとに softmax で確率にします。文章は生成せず、ネットワークにも何も送信しません。答えと確率は Jev とは異なります。 - ローカルの d1 モデル:d1 は、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 段階)
# 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 で書いた場合のコードとの対応を示します。
| 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 は利用できません。
質問のまとめ送り
質問は可能な範囲でまとめて送られます。
- スクリプトの実行中、
jevの質問はその場では送らず、順に溜めておきます。 - 答えを変数やリストに入れる、自分で定義した関数に渡す、
float()を通す、といった使い方なら実行を続けます。ifの条件、比較、計算、f 文字列、sorted、join、出力などで答えの値を初めて使ったところで、実行を止めます。 - 溜めた質問を
stateごとに 1 回のリクエストで送り、スクリプトを最初から実行し直します。答えが分かった質問はその値を返し、新しい質問は同じように溜めます。
state が違うリクエストは、TypeSafe API では同時に送り、ローカルモデルでは順に処理します。質問を 1 つずつ問い合わせる場合と同じ質問をし、同じ結果を出力します。1 回の実行の中では、実行し直すたびに random の状態と datetime.datetime.now() の時刻が同じところから始まるので、実行し直しても質問は変わりません。
次の例では、a は if まで使われないので、q1 と q2 は 1 回のリクエストで送られます。q3 は a が分かってから 2 回目のリクエストで送られます。
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 の system_one 呼び出し 1 回にあたります。上の例の 1 回目のリクエストは次のとおりです。
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でパーセントの段階を選ばせてください。測定の結果とスクリプトの例は、汎用のローカルモデルは、不確かな問いにどう答えるか にあります。 - ローカルモデルのプロンプトは 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 / 100float_2(FLOAT)=int_2 / 100float_3(FLOAT)=int_3 / 100
注意:
int_nが接続されていない場合、対応するfloat_nの出力はNoneです。- 意図的に最小限の、接続のみのノードにしています。
実行環境
スクリプトは Python の AST で解析され、ScriptFlow の安全なインタープリタで評価されます。代入、計算、分岐、ループ、ユーザー定義のヘルパー関数、辞書、リスト、文字列、一部のメソッドなど、ワークフローでよく使う処理に対応しています。
使える関数
len、min、max、sum、abs、roundint、float、str、boolsorted、reversedenumerate、range、zipany、all、pow、divmodlist、dict、tuplejev.yes、jev.noul、jev.choice、jev.probabilities、jev.score(MultiOutputScript (Jev)のみ。該当の節を参照)
使える名前空間
random:random、randint、uniform、choicedatetime: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、upperlist: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(...)でタイムスタンプを簡潔に整形できます。now = datetime.datetime.now() ot1 = now.strftime("%Y%m%d%H%M%S") - v1.2.0 より前のバージョンでは、同じタイムスタンプを手作業で整形します。
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 テキストを生成できます。
推奨するワークフロー:
LLM Dialogue Cycle
-> transcript_text
-> MultiOutputScript.in_text_1
-> MultiOutputScript.out_text_1
-> dialogue_segments_json input of a TTS node
次のコードを MultiOutputScript.code に貼り付けます。
# 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 文字列です。
{
"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 欄に貼り付けてください。
推奨するワークフロー:
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 に貼り付けます。
# エンハンサー出力がシステムプロンプトにどれだけ従っているかを評価する(モデル非依存)
# 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 は、次のようなレポートです。
準拠スコア: 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 に尋ねます。入力と出力の接続は同じです。
# 長いシステムプロンプト向け:エンハンサー出力がシステムプロンプトにどれだけ従っているかを、部分ごとに評価する
# システムプロンプトを空行の位置で 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 は、次のようなレポートです。
準拠スコア: 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 を参照してください
- Examples:ワークフローのテンプレートは examples/ を参照してください
変更履歴
詳しいバージョン履歴は CHANGELOG.md を参照してください。
現在のバージョン:1.3.5
MultiOutputScript (Jev)が、ローカルの d1 の GGUF モデル(Liquid AI のオープンウェイトの判断モデル)を動かせるように。d1 が学習したプロンプト形式で質問する- Jev、Clef-Flash、汎用のローカルモデルが、答えを知りようのない問いにどう答えるかを調べた技術レポートを追加
