Files

45 KiB
Raw Permalink Blame History

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) demo

デモ: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 にはアクセスしません。

インストール

  1. ComfyUI/custom_nodes/ ディレクトリに移動します。
  2. このリポジトリをクローンします。
    git clone https://github.com/kantan-kanto/ComfyUI-ScriptFlow
    
  3. ComfyUI を再起動します。

MultiOutputScript と centi ノードに追加のパッケージは不要です。MultiOutputScript (Jev) は、使うバックエンドに応じて以下のセットアップが必要です。

MultiOutputScript (Jev) のセットアップ

TypeSafe APIを使う場合

  1. ComfyUI を実行している Python 環境に SDK をインストールします。
    pip install typesafe-sdk
    
  2. このカスタムノードの実際のインストール先ディレクトリに api_key.txt を作成し、TypeSafe の API キーを記入します(1 行、引用符なし)。
    ComfyUI/custom_nodes/<your-installation-folder>/api_key.txt
    
    キーは実行のたびにこのファイルから読み込まれます。api_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 は利用できません。

質問のまとめ送り

質問は可能な範囲でまとめて送られます。

  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 回目のリクエストで送られます。

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 / 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) のみ。該当の節を参照)

使える名前空間

  • 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(...) でタイムスタンプを簡潔に整形できます。
    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、汎用のローカルモデルが、答えを知りようのない問いにどう答えるかを調べた技術レポートを追加