Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
2bb6cb8717 | ||
|
|
ce81103c06 | ||
|
|
1d7635ba7e | ||
|
|
1d68bc85ec | ||
|
|
7a0dd36c1f | ||
|
|
e874207f33 | ||
|
|
f84abc9e8a | ||
|
|
6949a6f236 | ||
|
|
40b2a5fa92 | ||
|
|
c65a9a8f4a | ||
|
|
d1975cc511 | ||
|
|
a977b97d29 | ||
|
|
288275c100 | ||
|
|
af8f8cc25d | ||
|
|
a3a9fc5b63 |
+4
-1
@@ -32,6 +32,9 @@ env/
|
||||
*.swo
|
||||
*~
|
||||
|
||||
# API Keys
|
||||
api_key.txt
|
||||
|
||||
# Model files (too large for git)
|
||||
*.gguf
|
||||
*.safetensors
|
||||
@@ -49,4 +52,4 @@ Thumbs.db
|
||||
output/
|
||||
temp/
|
||||
|
||||
experiments/
|
||||
experiments/
|
||||
@@ -0,0 +1,74 @@
|
||||
# Changelog
|
||||
|
||||
All notable changes to ComfyUI-ScriptFlow will be documented in this file.
|
||||
|
||||
|
||||
## [Unreleased]
|
||||
|
||||
|
||||
## [1.3.1] - 2026-09-29
|
||||
|
||||
- Fixed ComfyUI Registry publishing of 1.3.0
|
||||
- The Registry's automated dynamic-execution scan flagged the local Jev backend because it called the llama-cpp-python evaluation helper, whose method name matches Python's built-in code evaluation
|
||||
- The local backend now decodes the prompt with `llama_batch_init` / `llama_decode` / `llama_batch_free` directly; answers and probabilities are unchanged
|
||||
- Local models are loaded with `n_batch` equal to the 8192-token context so a whole prompt is decoded in one call
|
||||
|
||||
|
||||
## [1.3.0] - 2026-09-29
|
||||
|
||||
- Added `MultiOutputScript (Jev)` node (`utils`)
|
||||
- Same inputs, outputs, and script rules as `MultiOutputScript`, plus a `jev` namespace that answers typed questions about text
|
||||
- `jev.yes(state, question[, threshold])`, `jev.noul(state, question)`, `jev.choice(state, question, options)`, `jev.probabilities(state, question, options)`, and `jev.score(state, question, levels)`
|
||||
- Using a `jev.noul()` probability directly as a condition raises an error instead of always being true
|
||||
- Identical questions within a run are asked only once; `max_jev_calls` limits API requests or model evaluations per run
|
||||
- Default script picks one of eight aspect ratios for the prompt in `in_text_1`, sizes it to the megapixels in `in_value_1`, and lists every ratio's probability in `out_text_1`
|
||||
|
||||
- Added two backends selected by the `model` input; scripts are the same for both
|
||||
- `TypeSafe API`: asks TypeSafe AI's Jev through the optional `typesafe-sdk` package, with the API key read from `api_key.txt` in the node's installation directory on every run
|
||||
- Local GGUF models as a fallback when the Jev API is not available: models under `models/LLM` and `models/text_encoders`, including paths registered in `extra_model_paths.yaml` on other drives
|
||||
- The local backend reads the decision from next-token probabilities of lettered options in one forward pass through the optional `llama-cpp-python` package, adapted from SemIf (OpenJev, MIT License); no text is generated and nothing is sent over the network
|
||||
- The local model is loaded on the first `jev` question of a run and always unloaded when the run ends, freeing VRAM on CUDA, Intel XPU, and other ComfyUI devices
|
||||
|
||||
- Kept `MultiOutputScript` and `centi` unchanged
|
||||
- `jev` is not available in `MultiOutputScript`, which still makes no network requests and loads no models
|
||||
- Existing nodes do not require `typesafe-sdk` or `llama-cpp-python`
|
||||
|
||||
- Added `api_key.txt` to `.gitignore`
|
||||
- Moved release notes from `README.md` to `CHANGELOG.md`
|
||||
- Replaced the README demo with a `MultiOutputScript (Jev)` demo at the top of the README
|
||||
|
||||
|
||||
## [1.2.0] - 2026-05-31
|
||||
|
||||
- Replaced direct Python execution with a safe AST interpreter for ComfyUI Registry compatibility.
|
||||
- Added Python-subset support for helper functions, loops, dictionaries, lists, string methods, f-strings, `random`, `datetime`, and `math`.
|
||||
- Replaced trace-based timeout handling with step and loop limits.
|
||||
- Kept existing `MultiOutputScript` inputs and outputs unchanged.
|
||||
|
||||
|
||||
## [1.1.1] - 2026-04-06
|
||||
|
||||
- Added safe-mode AST validation before script execution.
|
||||
- Blocked unsafe syntax (`Import`, `ImportFrom`, `Global`, `Nonlocal`, `ClassDef`, `Try`, `With`, `AsyncWith`).
|
||||
- Blocked unsafe dynamic execution, file/input access, namespace inspection, and dynamic attribute mutation calls.
|
||||
- Added execution timeout guard (`1.5s` default) to stop runaway scripts.
|
||||
- Updated security notes for trusted-workflow usage.
|
||||
|
||||
|
||||
## [1.1.0] - 2026-04-03
|
||||
|
||||
Added `centi` node (`utils`).
|
||||
- Minimal connector-only utility node.
|
||||
- Three optional integer inputs: `int_1`, `int_2`, `int_3`.
|
||||
- Three float outputs: `float_1`, `float_2`, `float_3`.
|
||||
- Each connected input is converted by `int_n / 100`; unconnected inputs return `None`.
|
||||
|
||||
|
||||
## [1.0.1] - 2026-04-01
|
||||
|
||||
- Improved documentation and project metadata.
|
||||
|
||||
|
||||
## [1.0.0] - 2026-03-31
|
||||
|
||||
- Initial release
|
||||
@@ -0,0 +1,13 @@
|
||||
GNU GENERAL PUBLIC LICENSE
|
||||
Version 3, 29 June 2007
|
||||
|
||||
Copyright (C) 2007 Free Software Foundation, Inc. <https://fsf.org/>
|
||||
Everyone is permitted to copy and distribute verbatim copies
|
||||
of this license document, but changing it is not allowed.
|
||||
|
||||
This project is licensed under the GNU General Public License v3.0 or later
|
||||
(SPDX: GPL-3.0-or-later).
|
||||
|
||||
For the full license text, see:
|
||||
https://www.gnu.org/licenses/gpl-3.0.txt
|
||||
|
||||
+465
@@ -0,0 +1,465 @@
|
||||
# ComfyUI-ScriptFlow(日本語版)
|
||||
[[en](README.md) | ja]
|
||||
|
||||
---
|
||||
|
||||
**Version:** 1.3.1
|
||||
**License:** GPL-3.0
|
||||
|
||||
System One の判断モデルを使える、安全な Python 風スクリプトノードです。
|
||||
TypeSafe AI の Jev API(フォールバックとしてローカルの GGUF LLM)に Yes/No・選択・スコアの質問をして、その答えを `if` 文の中でそのまま使えます。
|
||||
|
||||

|
||||
|
||||
デモ:`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/<your-installation-folder>/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 で動作を確認しています。
|
||||
|
||||
## 主な機能
|
||||
- 複数の入力:数値とテキストの入力を自由に組み合わせられます。
|
||||
- 計算と論理:四則演算、比較、簡単な条件分岐に対応します。
|
||||
- 複数の出力:結果を別々の出力ポートから下流のノードに渡せます。
|
||||
- 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 とは異なります。
|
||||
|
||||
スクリプトはどちらでも同じです。各バックエンドのセットアップは[インストール](#インストール)を参照してください。
|
||||
|
||||
追加の入力:
|
||||
- `model`:`TypeSafe API`、またはローカルの GGUF モデル(`mmproj-*` ファイルは表示されません)。
|
||||
- `max_jev_calls`(INT、初期値 8):1 回の実行での API リクエストまたはモデル評価の上限です。同じ実行の中で同じ質問は 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 API を使う場合、[TypeSafe Python SDK](https://pypi.org/project/typesafe-sdk/) の `system_one` リクエスト 1 回に対応します。
|
||||
|
||||
| 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` は利用できません。
|
||||
|
||||
注意:
|
||||
- TypeSafe API は `state` と質問をインターネット経由で送信します。外部に出せないテキストには使わないでください。
|
||||
- ローカルモデルの質問は 1 回の順伝播で処理されます(GPU 上の 4B〜9B モデルで約 1〜1.5 秒)。モデルは実行ごとに最初の質問で読み込まれ(数秒)、実行が終わるとアンロードされて、ワークフローの残りのために VRAM を解放します。
|
||||
- ローカルモデルの確率は較正されておらず、モデルによって変わります。しきい値は実際の入力で試して決めてください。
|
||||
- `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.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` に変換する
|
||||
|
||||
<details>
|
||||
<summary>ComfyUI-LLM-Session の会話ログ変換レシピを表示</summary>
|
||||
|
||||
`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`、数値出力は使いません。
|
||||
|
||||
</details>
|
||||
|
||||
## ライセンス
|
||||
|
||||
このプロジェクトは **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.1
|
||||
- ComfyUI Registry への公開を修正:ローカル Jev バックエンドが llama.cpp の API で直接推論するようにし、Registry の動的実行スキャンでの誤検知を回避
|
||||
- ComfyUI Registry に公開されなかった 1.3.0 の以下の変更を含む
|
||||
- テキストについての型付きの判断をスクリプトで使える `jev` 名前空間付きの `MultiOutputScript (Jev)` ノードを追加
|
||||
- `jev.yes`、`jev.noul`、`jev.choice`、`jev.probabilities`、`jev.score` を追加
|
||||
- `api_key.txt` から API キーを読み込む TypeSafe API バックエンドを追加
|
||||
- SemIf (OpenJev) をもとにした、llama-cpp-python によるローカル GGUF バックエンドを追加
|
||||
- 8 種類のアスペクト比から 1 つを選び、メガピクセル数からサイズを決める初期スクリプトを追加
|
||||
- `MultiOutputScript` と `centi` は変更なし
|
||||
@@ -1,11 +1,28 @@
|
||||
# ComfyUI-ScriptFlow
|
||||
[en | [ja](README.ja.md)]
|
||||
|
||||
**Version:** 1.2.0
|
||||
**Version:** 1.3.1
|
||||
**License:** GPL-3.0
|
||||
|
||||
A general-purpose calculation node that evaluates a safe Python-like script subset and returns multiple text and numeric outputs based on the inputs.
|
||||
Safe Python-like script node for ComfyUI with a System One decision model.
|
||||
Ask yes/no, choice, and score questions via TypeSafe AI's Jev API, with local GGUF LLMs as a fallback, and use the answer directly in an `if` statement.
|
||||
|
||||
This custom node accepts numbers (int/float) and text (string) as inputs, runs calculations and logical operations internally, and returns multiple values through separate output ports. It consolidates workflows that previously required combining several basic nodes into a single node.
|
||||

|
||||
|
||||
`MultiOutputScript (Jev)` picks an aspect ratio for a text prompt through the TypeSafe API, lists every ratio's probability, and passes the width and height to an image node.
|
||||
|
||||
A simplified version of the demo: connect a text prompt to `in_text_1`, and the answer picks the width and height.
|
||||
|
||||
```python
|
||||
if jev.yes(it1, "Does a portrait orientation suit this prompt?"):
|
||||
ov1, ov2 = 832, 1280
|
||||
else:
|
||||
ov1, ov2 = 1280, 832
|
||||
```
|
||||
|
||||
A System One model answers with probabilities in a single fast pass instead of generating text, so the answer can drive workflow logic directly.
|
||||
|
||||
Multiple inputs and outputs let one node handle math, logic, text editing, conditional routing, resolution selection, filename generation, and more — work that previously took several basic nodes. Scripts run in a restricted interpreter with no file or OS access.
|
||||
|
||||
## Installation
|
||||
|
||||
@@ -16,11 +33,33 @@ This custom node accepts numbers (int/float) and text (string) as inputs, runs c
|
||||
```
|
||||
3. Restart ComfyUI.
|
||||
|
||||
The `MultiOutputScript` and `centi` nodes need no extra packages. `MultiOutputScript (Jev)` needs the setup below for the backend you use.
|
||||
|
||||
### For `MultiOutputScript (Jev)`
|
||||
|
||||
#### TypeSafe API
|
||||
|
||||
1. Install the SDK in the Python environment that runs ComfyUI:
|
||||
```bash
|
||||
pip install typesafe-sdk
|
||||
```
|
||||
2. Create `api_key.txt` in the actual installation directory of this custom node and add your TypeSafe API key (single line, no quotes):
|
||||
```
|
||||
ComfyUI/custom_nodes/<your-installation-folder>/api_key.txt
|
||||
```
|
||||
The key is read from this file on every run. `api_key.txt` is listed in `.gitignore`; never commit it.
|
||||
|
||||
#### Local models (fallback)
|
||||
|
||||
- Install [llama-cpp-python](https://github.com/JamePeng/llama-cpp-python) in the Python environment that runs ComfyUI. Tested with the JamePeng fork and the upstream [abetlen/llama-cpp-python](https://github.com/abetlen/llama-cpp-python) 0.3.35.
|
||||
- Put an instruction-tuned GGUF model in `ComfyUI/models/LLM` (or `models/text_encoders`). The GGUF must include a chat template (`tokenizer.chat_template` metadata), as most instruction-tuned GGUFs do. Tested with Qwen3.5-9B Q8_0 and Gemma-4-E4B Q8.
|
||||
|
||||
## Key Features
|
||||
- Multiple inputs: freely combine numeric and text inputs.
|
||||
- Calculations and logic: supports arithmetic, comparison, and simple conditional branching.
|
||||
- Multiple outputs: pass results to downstream nodes via separate output ports.
|
||||
- Safer execution: AST interpreter with restricted syntax and no file/OS access.
|
||||
- AI decisions: `jev.yes` / `jev.choice` / `jev.score` in `MultiOutputScript (Jev)`, with probabilities, through TypeSafe AI's Jev API or a local GGUF model as a fallback.
|
||||
- Safer execution: scripts run in an AST interpreter with restricted syntax and no file/OS access. Only the Jev node's `jev` functions reach a model or the TypeSafe API.
|
||||
|
||||
## Use Cases
|
||||
- In Wan2.2 I2V workflows, automatically select a recommended resolution
|
||||
@@ -28,7 +67,9 @@ This custom node accepts numbers (int/float) and text (string) as inputs, runs c
|
||||
- Batch-generate file names (e.g., local LLM prompt history, output logs).
|
||||
- Apply conditional logic to control workflow behavior (e.g., switch parameters or routes based on inputs).
|
||||
- Parse and transform LLM outputs into structured values for downstream nodes.
|
||||
Dynamically generate numeric parameters (e.g., seeds, thresholds, scaling values) using custom calculations.
|
||||
- Dynamically generate numeric parameters (e.g., seeds, thresholds, scaling values) using custom calculations.
|
||||
- Pick an aspect ratio and size for a text prompt before generation, with the probability of every ratio (Jev node default script).
|
||||
- Route workflows by the meaning of text, e.g. check whether a prompt describes a night scene or classify its main subject.
|
||||
|
||||
## Nodes
|
||||
|
||||
@@ -74,6 +115,62 @@ w, h = iv1, iv2
|
||||
ov1, ov2 = (512, 384) if w >= h else (384, 512)
|
||||
```
|
||||
|
||||
### `MultiOutputScript (Jev)`
|
||||
**Category:** `utils`
|
||||
|
||||
Same inputs, outputs, and script rules as `MultiOutputScript`, plus a `jev` namespace that asks [Jev](https://typesafe.ai), TypeSafe AI's decision model, typed questions about text and lets scripts branch on the answer.
|
||||
|
||||
- **TypeSafe API** (primary): sends the questions to Jev over the internet.
|
||||
- **Local GGUF model** (fallback): for when the Jev API is not available, or when text must stay on your machine. It approximates Jev-style decisions with a general instruction-tuned model, reading the answer from next-token probabilities of lettered options in one forward pass, following [SemIf (OpenJev)](https://github.com/TheoLeeCJ/SemIf-OpenJev). No text is generated and nothing is sent over the network. Answers and probabilities differ from Jev's.
|
||||
|
||||
Scripts are the same for both. See [Installation](#installation) for the setup of each backend.
|
||||
|
||||
Extra inputs:
|
||||
- `model`: `TypeSafe API`, or a local GGUF model (`mmproj-*` files are hidden).
|
||||
- `max_jev_calls` (INT, default 8): maximum API requests or model evaluations per run. Identical questions within a run are asked only once.
|
||||
|
||||
Functions (`state` is a string, dict, or list; text only):
|
||||
- `jev.yes(state, question[, threshold])` → `bool` (yes-probability ≥ `threshold`, default `0.5`)
|
||||
- `jev.noul(state, question)` → yes-probability (`0.0`–`1.0`). Compare it with a threshold; using it directly as a condition raises an error.
|
||||
- `jev.choice(state, question, options)` → the most likely option (`str`). `options` is a list of labels, or a dict of labels to descriptions (2–16 options for local models).
|
||||
- `jev.probabilities(state, question, options)` → `dict` of each option to its probability (sums to `1.0`). Same `options` as `jev.choice`; asking both with the same arguments asks the model once.
|
||||
- `jev.score(state, question, levels)` → expected level (`float`, `0` to `len(levels) - 1`) for ordered level descriptions (2–16 levels for local models)
|
||||
|
||||
```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})"
|
||||
```
|
||||
|
||||
The node's default script picks one of eight aspect ratios for the prompt in `it1`, sizes it to the megapixels in `in_value_1` (default `1.0` when unconnected; width and height rounded to multiples of 64), outputs the width and height to `out_value_1` / `out_value_2`, and lists every ratio's probability in `out_text_1`.
|
||||
|
||||
Each function asks one question. With the TypeSafe API it maps to one `system_one` request of the [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` (same request as `jev.choice`) |
|
||||
| `jev.score(state, q, levels)` | `client.system_one(state=state, questions={"q": Score(instructions=q, criteria=levels)}).answers["q"].score` |
|
||||
|
||||
A list of `options` is sent as `criteria={label: None, ...}`. `confidence`, the Score level probabilities, and Noul `criteria` are not exposed.
|
||||
|
||||
Notes:
|
||||
- The TypeSafe API sends `state` and questions over the internet. Do not use it with text you cannot share externally.
|
||||
- A local question takes one forward pass (about 1–1.5 s with a 4B–9B model on GPU). The model is loaded on the first question of each run (several seconds) and unloaded when the run ends to free VRAM for the rest of the workflow.
|
||||
- Local probabilities are uncalibrated and depend on the model; test thresholds on your own inputs.
|
||||
- `MultiOutputScript` never loads a model or makes network requests; `jev` is not available there.
|
||||
- This project is independent and not affiliated with TypeSafe AI.
|
||||
|
||||
### `centi`
|
||||
**Category:** `utils`
|
||||
|
||||
@@ -103,6 +200,7 @@ The script is parsed with Python AST and evaluated by ScriptFlow's safe interpre
|
||||
- `enumerate`, `range`, `zip`
|
||||
- `any`, `all`, `pow`, `divmod`
|
||||
- `list`, `dict`, `tuple`
|
||||
- `jev.yes`, `jev.noul`, `jev.choice`, `jev.probabilities`, `jev.score` (`MultiOutputScript (Jev)` only; see [its section](#multioutputscript-jev))
|
||||
|
||||
### Allowed namespaces
|
||||
- `random`: `random`, `randint`, `uniform`, `choice`
|
||||
@@ -139,15 +237,20 @@ The script is parsed with Python AST and evaluated by ScriptFlow's safe interpre
|
||||
|
||||
## Examples
|
||||
|
||||
### Demo
|
||||

|
||||
### Example Workflow
|
||||
|
||||
Demonstrates running the sample workflow with a landscape image and a portrait image.
|
||||
```
|
||||
examples/
|
||||
├─ example_workflow.json
|
||||
```
|
||||
|
||||
## Application Recipes
|
||||
|
||||
### Convert `LLM Dialogue Cycle` transcript text to `dialogue_segments_json`
|
||||
|
||||
<details>
|
||||
<summary>Show ComfyUI-LLM-Session transcript conversion recipe</summary>
|
||||
|
||||
`ComfyUI-LLM-Session` returns the full result of `LLM Dialogue Cycle` as `transcript_text`.
|
||||
You can connect that text to `MultiOutputScript.in_text_1` and paste the script below into
|
||||
the `code` field to generate a speaker-tagged JSON text for downstream dialogue TTS nodes.
|
||||
@@ -322,12 +425,7 @@ Notes:
|
||||
- Utterances without `「...」` are omitted from the TTS script.
|
||||
- `out_text_2`, `out_text_3`, and numeric outputs are unused by this recipe.
|
||||
|
||||
### Example Workflow
|
||||
|
||||
```
|
||||
examples/
|
||||
├─ example_workflow.json
|
||||
```
|
||||
</details>
|
||||
|
||||
## License
|
||||
|
||||
@@ -345,33 +443,21 @@ You should have received a copy of the GNU General Public License along with thi
|
||||
## Support
|
||||
|
||||
- **Issues**: Report bugs or request features via GitHub Issues
|
||||
- **Documentation**: See [CHANGELOG.md](CHANGELOG.md) for version history
|
||||
- **Examples**: Check [examples/](examples/) for workflow templates
|
||||
|
||||
---
|
||||
|
||||
## Release Notes
|
||||
### 1.2.0
|
||||
- Replaced direct Python execution with a safe AST interpreter for ComfyUI Registry compatibility.
|
||||
- Added Python-subset support for helper functions, loops, dictionaries, lists, string methods, f-strings, `random`, `datetime`, and `math`.
|
||||
- Replaced trace-based timeout handling with step and loop limits.
|
||||
- Kept existing `MultiOutputScript` inputs and outputs unchanged.
|
||||
## Changelog
|
||||
|
||||
### 1.1.1
|
||||
- Added safe-mode AST validation before script execution.
|
||||
- Blocked unsafe syntax (`Import`, `ImportFrom`, `Global`, `Nonlocal`, `ClassDef`, `Try`, `With`, `AsyncWith`).
|
||||
- Blocked unsafe dynamic execution, file/input access, namespace inspection, and dynamic attribute mutation calls.
|
||||
- Added execution timeout guard (`1.5s` default) to stop runaway scripts.
|
||||
- Updated security notes for trusted-workflow usage.
|
||||
See [CHANGELOG.md](CHANGELOG.md) for detailed version history.
|
||||
|
||||
### 1.1.0
|
||||
Added `centi` node (`utils`).
|
||||
- Minimal connector-only utility node.
|
||||
- Three optional integer inputs: `int_1`, `int_2`, `int_3`.
|
||||
- Three float outputs: `float_1`, `float_2`, `float_3`.
|
||||
- Each connected input is converted by `int_n / 100`; unconnected inputs return `None`.
|
||||
|
||||
### 1.0.1
|
||||
- Improved documentation and project metadata.
|
||||
|
||||
### 1.0.0
|
||||
- Initial release
|
||||
### Current Version: 1.3.1
|
||||
- Fixed ComfyUI Registry publishing: the local Jev backend now decodes through the llama.cpp API directly, avoiding a false positive in the Registry's dynamic-execution scan
|
||||
- Includes the 1.3.0 changes below, which were not published to the ComfyUI Registry
|
||||
- Added `MultiOutputScript (Jev)` node with a `jev` namespace for typed decisions in scripts
|
||||
- Added `jev.yes`, `jev.noul`, `jev.choice`, `jev.probabilities`, and `jev.score`
|
||||
- Added TypeSafe API backend with the API key read from `api_key.txt`
|
||||
- Added local GGUF backend through llama-cpp-python, adapted from SemIf (OpenJev)
|
||||
- Added a default script that picks one of eight aspect ratios and sizes it from megapixels
|
||||
- Kept `MultiOutputScript` and `centi` unchanged
|
||||
|
||||
+177
-35
@@ -8,18 +8,28 @@
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
__version__ = "1.2.0"
|
||||
|
||||
import ast
|
||||
import datetime
|
||||
import math
|
||||
import random
|
||||
from pathlib import Path
|
||||
from typing import Any, Dict
|
||||
|
||||
from .jev_backend import (
|
||||
JevBackend,
|
||||
LocalJev,
|
||||
TypeSafeClient,
|
||||
TypeSafeJev,
|
||||
list_local_models,
|
||||
unload_local_model,
|
||||
)
|
||||
|
||||
|
||||
_DEFAULT_MAX_STEPS = 10000
|
||||
_DEFAULT_MAX_CALL_DEPTH = 32
|
||||
_MAX_RANGE_SIZE = 10000
|
||||
_API_KEY_PATH = Path(__file__).resolve().parent / "api_key.txt"
|
||||
_TYPESAFE_API = "TypeSafe API"
|
||||
|
||||
_BLOCKED_AST_NODES = (
|
||||
ast.Import,
|
||||
@@ -148,6 +158,10 @@ class _SafeNamespace:
|
||||
self.name = name
|
||||
|
||||
|
||||
class _JevProbability(float):
|
||||
pass
|
||||
|
||||
|
||||
class _SafeFunction:
|
||||
def __init__(self, name: str, args: ast.arguments, body: list[ast.stmt]):
|
||||
self.name = name
|
||||
@@ -182,8 +196,10 @@ class _SafeScriptInterpreter:
|
||||
variables: Dict[str, Any],
|
||||
max_steps: int = _DEFAULT_MAX_STEPS,
|
||||
max_call_depth: int = _DEFAULT_MAX_CALL_DEPTH,
|
||||
jev: JevBackend | None = None,
|
||||
):
|
||||
self.frames = [variables]
|
||||
self.jev = jev
|
||||
self.max_steps = max_steps
|
||||
self.max_call_depth = max_call_depth
|
||||
self.steps = 0
|
||||
@@ -491,8 +507,34 @@ class _SafeScriptInterpreter:
|
||||
return datetime.date.today()
|
||||
if namespace == "math" and name in _SAFE_MATH_FUNCTIONS:
|
||||
return self._call_math(name, args)
|
||||
if namespace == "jev":
|
||||
return self._call_jev(name, args)
|
||||
raise ValueError(f"Unsupported namespace call: {namespace}.{name}")
|
||||
|
||||
def _call_jev(self, name: str, args: list[Any]) -> Any:
|
||||
if name == "yes":
|
||||
if len(args) not in (2, 3):
|
||||
raise ValueError("jev.yes() expects 2 or 3 arguments")
|
||||
threshold = args[2] if len(args) == 3 else 0.5
|
||||
return self.jev.noul(args[0], args[1]) >= threshold
|
||||
if name == "noul":
|
||||
if len(args) != 2:
|
||||
raise ValueError("jev.noul() expects 2 arguments")
|
||||
return _JevProbability(self.jev.noul(*args))
|
||||
if name == "choice":
|
||||
if len(args) != 3:
|
||||
raise ValueError("jev.choice() expects 3 arguments")
|
||||
return self.jev.choice(*args)
|
||||
if name == "probabilities":
|
||||
if len(args) != 3:
|
||||
raise ValueError("jev.probabilities() expects 3 arguments")
|
||||
return self.jev.probabilities(*args)
|
||||
if name == "score":
|
||||
if len(args) != 3:
|
||||
raise ValueError("jev.score() expects 3 arguments")
|
||||
return self.jev.score(*args)
|
||||
raise ValueError(f"Unsupported namespace call: jev.{name}")
|
||||
|
||||
def _call_math(self, name: str, args: list[Any]) -> Any:
|
||||
if name == "ceil":
|
||||
return math.ceil(*args)
|
||||
@@ -680,6 +722,8 @@ class _SafeScriptInterpreter:
|
||||
return frame[name]
|
||||
if name in ("random", "datetime", "math"):
|
||||
return _SafeNamespace(name)
|
||||
if name == "jev" and self.jev is not None:
|
||||
return _SafeNamespace(name)
|
||||
return None
|
||||
|
||||
def _get_name(self, name: str) -> Any:
|
||||
@@ -695,6 +739,8 @@ class _SafeScriptInterpreter:
|
||||
|
||||
@staticmethod
|
||||
def _truthy(value: Any) -> bool:
|
||||
if isinstance(value, _JevProbability):
|
||||
raise ValueError("Compare jev.noul() with a threshold, or use jev.yes()")
|
||||
return bool(value)
|
||||
|
||||
|
||||
@@ -821,42 +867,136 @@ class MultiOutputScript:
|
||||
in_value_2: Any = None,
|
||||
in_value_3: Any = None,
|
||||
):
|
||||
it1 = _select_text_value("", in_text_1, "in_text_1")
|
||||
it2 = _select_text_value("", in_text_2, "in_text_2")
|
||||
it3 = _select_text_value("", in_text_3, "in_text_3")
|
||||
iv1 = _select_numeric_value(0.0, in_value_1, "in_value_1")
|
||||
iv2 = _select_numeric_value(0.0, in_value_2, "in_value_2")
|
||||
iv3 = _select_numeric_value(0.0, in_value_3, "in_value_3")
|
||||
|
||||
locals_dict: Dict[str, Any] = {
|
||||
"it1": it1,
|
||||
"it2": it2,
|
||||
"it3": it3,
|
||||
"iv1": iv1,
|
||||
"iv2": iv2,
|
||||
"iv3": iv3,
|
||||
"ot1": None,
|
||||
"ot2": None,
|
||||
"ot3": None,
|
||||
"ov1": None,
|
||||
"ov2": None,
|
||||
"ov3": None,
|
||||
}
|
||||
|
||||
locals_dict = _SafeScriptInterpreter(locals_dict).run(code)
|
||||
|
||||
_validate_outputs(locals_dict)
|
||||
|
||||
return (
|
||||
locals_dict.get("ot1"),
|
||||
locals_dict.get("ot2"),
|
||||
locals_dict.get("ot3"),
|
||||
None if locals_dict.get("ov1") is None else int(locals_dict.get("ov1")),
|
||||
None if locals_dict.get("ov2") is None else int(locals_dict.get("ov2")),
|
||||
None if locals_dict.get("ov3") is None else int(locals_dict.get("ov3")),
|
||||
return _run_script(
|
||||
code, None, in_text_1, in_text_2, in_text_3, in_value_1, in_value_2, in_value_3
|
||||
)
|
||||
|
||||
|
||||
class MultiOutputScriptJev(MultiOutputScript):
|
||||
@classmethod
|
||||
def INPUT_TYPES(cls):
|
||||
types = super().INPUT_TYPES()
|
||||
types["required"]["code"][1]["default"] = (
|
||||
"# Jev: jev.yes(state, question[, threshold]) -> bool\n"
|
||||
"# jev.noul(state, question) -> probability\n"
|
||||
"# jev.choice(state, question, options) -> str\n"
|
||||
"# jev.probabilities(state, question, options) -> {option: probability}\n"
|
||||
"# jev.score(state, question, levels) -> float\n"
|
||||
"# Example: pick an aspect ratio for the prompt in it1 and size it to iv1 megapixels\n"
|
||||
"megapixels = iv1 if iv1 > 0 else 1.0\n"
|
||||
"multiple = 64\n"
|
||||
"ratios = [\n"
|
||||
" \"1:1 (Square)\",\n"
|
||||
" \"2:3 (Portrait Photo)\",\n"
|
||||
" \"3:2 (Photo)\",\n"
|
||||
" \"3:4 (Portrait Standard)\",\n"
|
||||
" \"4:3 (Standard)\",\n"
|
||||
" \"9:16 (Portrait Widescreen)\",\n"
|
||||
" \"16:9 (Widescreen)\",\n"
|
||||
" \"21:9 (Ultrawide)\",\n"
|
||||
"]\n"
|
||||
"question = \"Which image aspect ratio best suits this prompt?\"\n"
|
||||
"best = jev.choice(it1, question, ratios)\n"
|
||||
"probs = jev.probabilities(it1, question, ratios)\n"
|
||||
"rw, rh = best.split(\" \")[0].split(\":\")\n"
|
||||
"pixels = megapixels * 1024 * 1024\n"
|
||||
"ov1 = round(math.sqrt(pixels * int(rw) / int(rh)) / multiple) * multiple\n"
|
||||
"ov2 = round(math.sqrt(pixels * int(rh) / int(rw)) / multiple) * multiple\n"
|
||||
"lines = [f\"{best} -> {ov1}x{ov2}\"]\n"
|
||||
"for ratio in ratios:\n"
|
||||
" lines.append(f\"{ratio}: {probs[ratio]:.3f}\")\n"
|
||||
"ot1 = \"\\n\".join(lines)\n"
|
||||
)
|
||||
types["required"]["model"] = ([_TYPESAFE_API] + list(list_local_models()),)
|
||||
types["required"]["max_jev_calls"] = ("INT", {"default": 8, "min": 1, "max": 64})
|
||||
return types
|
||||
|
||||
def run(
|
||||
self,
|
||||
code: str,
|
||||
model: str,
|
||||
max_jev_calls: int,
|
||||
in_text_1: Any = None,
|
||||
in_text_2: Any = None,
|
||||
in_text_3: Any = None,
|
||||
in_value_1: Any = None,
|
||||
in_value_2: Any = None,
|
||||
in_value_3: Any = None,
|
||||
):
|
||||
inputs = (in_text_1, in_text_2, in_text_3, in_value_1, in_value_2, in_value_3)
|
||||
if model == _TYPESAFE_API:
|
||||
with _typesafe_client() as client:
|
||||
return _run_script(code, JevBackend(TypeSafeJev(client), max_jev_calls), *inputs)
|
||||
model_path = list_local_models().get(model)
|
||||
if model_path is None:
|
||||
raise ValueError(f"GGUF model not found under models/LLM or models/text_encoders: {model}")
|
||||
try:
|
||||
return _run_script(code, JevBackend(LocalJev(model_path), max_jev_calls), *inputs)
|
||||
finally:
|
||||
unload_local_model()
|
||||
|
||||
|
||||
def _typesafe_client() -> Any:
|
||||
if TypeSafeClient is None:
|
||||
raise RuntimeError("typesafe-sdk is not installed. Run: pip install typesafe-sdk")
|
||||
if not _API_KEY_PATH.exists():
|
||||
raise EnvironmentError(
|
||||
f"API key file not found: {_API_KEY_PATH}\nPlease create this file with your TypeSafe API key."
|
||||
)
|
||||
api_key = _API_KEY_PATH.read_text(encoding="utf-8").strip()
|
||||
if not api_key:
|
||||
raise EnvironmentError(
|
||||
f'API key is not set in "{_API_KEY_PATH}"\nPlease add your TypeSafe API key to this file.'
|
||||
)
|
||||
return TypeSafeClient(api_key=api_key)
|
||||
|
||||
|
||||
def _run_script(
|
||||
code: str,
|
||||
jev: JevBackend | None,
|
||||
in_text_1: Any,
|
||||
in_text_2: Any,
|
||||
in_text_3: Any,
|
||||
in_value_1: Any,
|
||||
in_value_2: Any,
|
||||
in_value_3: Any,
|
||||
):
|
||||
it1 = _select_text_value("", in_text_1, "in_text_1")
|
||||
it2 = _select_text_value("", in_text_2, "in_text_2")
|
||||
it3 = _select_text_value("", in_text_3, "in_text_3")
|
||||
iv1 = _select_numeric_value(0.0, in_value_1, "in_value_1")
|
||||
iv2 = _select_numeric_value(0.0, in_value_2, "in_value_2")
|
||||
iv3 = _select_numeric_value(0.0, in_value_3, "in_value_3")
|
||||
|
||||
locals_dict: Dict[str, Any] = {
|
||||
"it1": it1,
|
||||
"it2": it2,
|
||||
"it3": it3,
|
||||
"iv1": iv1,
|
||||
"iv2": iv2,
|
||||
"iv3": iv3,
|
||||
"ot1": None,
|
||||
"ot2": None,
|
||||
"ot3": None,
|
||||
"ov1": None,
|
||||
"ov2": None,
|
||||
"ov3": None,
|
||||
}
|
||||
|
||||
locals_dict = _SafeScriptInterpreter(locals_dict, jev=jev).run(code)
|
||||
|
||||
_validate_outputs(locals_dict)
|
||||
|
||||
return (
|
||||
locals_dict.get("ot1"),
|
||||
locals_dict.get("ot2"),
|
||||
locals_dict.get("ot3"),
|
||||
None if locals_dict.get("ov1") is None else int(locals_dict.get("ov1")),
|
||||
None if locals_dict.get("ov2") is None else int(locals_dict.get("ov2")),
|
||||
None if locals_dict.get("ov3") is None else int(locals_dict.get("ov3")),
|
||||
)
|
||||
|
||||
|
||||
class Centi:
|
||||
@classmethod
|
||||
def INPUT_TYPES(cls):
|
||||
@@ -889,10 +1029,12 @@ class Centi:
|
||||
|
||||
NODE_CLASS_MAPPINGS = {
|
||||
"MultiOutputScript": MultiOutputScript,
|
||||
"MultiOutputScriptJev": MultiOutputScriptJev,
|
||||
"centi": Centi,
|
||||
}
|
||||
|
||||
NODE_DISPLAY_NAME_MAPPINGS = {
|
||||
"MultiOutputScript": "MultiOutputScript",
|
||||
"MultiOutputScriptJev": "MultiOutputScript (Jev)",
|
||||
"centi": "centi",
|
||||
}
|
||||
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 9.6 MiB |
Binary file not shown.
|
After Width: | Height: | Size: 2.9 MiB |
+242
@@ -0,0 +1,242 @@
|
||||
# ComfyUI-ScriptFlow
|
||||
# Copyright (C) 2026 kantan-kanto (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.
|
||||
#
|
||||
# The decision readout is adapted from SemIf (formerly OpenJev),
|
||||
# Copyright (c) 2026 TheoLeeCJ, MIT License: https://github.com/TheoLeeCJ/SemIf-OpenJev
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import gc
|
||||
import json
|
||||
import os
|
||||
from typing import Any
|
||||
|
||||
import comfy.model_management
|
||||
import folder_paths
|
||||
import numpy as np
|
||||
|
||||
try:
|
||||
from llama_cpp import (
|
||||
Llama,
|
||||
llama_batch_free,
|
||||
llama_batch_init,
|
||||
llama_decode,
|
||||
llama_get_logits_ith,
|
||||
llama_get_memory,
|
||||
llama_memory_clear,
|
||||
)
|
||||
from llama_cpp.llama_chat_format import Jinja2ChatFormatter
|
||||
except ImportError:
|
||||
Llama = None
|
||||
|
||||
try:
|
||||
from typesafe_sdk import Choice, Noul, Score, TypeSafeClient
|
||||
except ImportError:
|
||||
TypeSafeClient = None
|
||||
|
||||
|
||||
_LETTERS = "ABCDEFGHIJKLMNOP"
|
||||
_DIRECT_SYSTEM = (
|
||||
"Apply the supplied criterion to the supplied evidence. Choose exactly one listed option. "
|
||||
"Respond with only its uppercase letter, with no explanation or reasoning."
|
||||
)
|
||||
_N_CTX = 8192
|
||||
|
||||
_loaded_model: tuple[str, Any, Any] | None = None
|
||||
|
||||
|
||||
class JevBackend:
|
||||
"""Caches answers and limits model calls for a single script run."""
|
||||
|
||||
def __init__(self, engine: Any, max_calls: int):
|
||||
self.engine = engine
|
||||
self.max_calls = max_calls
|
||||
self.calls = 0
|
||||
self.cache: dict[str, Any] = {}
|
||||
|
||||
def noul(self, state: Any, question: Any) -> float:
|
||||
return self._ask(state, "noul", question, None)[0]
|
||||
|
||||
def choice(self, state: Any, question: Any, options: list | dict) -> str:
|
||||
probabilities = self.probabilities(state, question, options)
|
||||
return max(probabilities, key=probabilities.get)
|
||||
|
||||
def probabilities(self, state: Any, question: Any, options: list | dict) -> dict[str, float]:
|
||||
if isinstance(options, list):
|
||||
options = {str(option): None for option in options}
|
||||
return dict(zip(options, self._ask(state, "choice", question, options)))
|
||||
|
||||
def score(self, state: Any, question: Any, levels: list) -> float:
|
||||
probabilities = self._ask(state, "score", question, list(levels))
|
||||
return sum(level * p for level, p in enumerate(probabilities))
|
||||
|
||||
def _ask(self, state: Any, kind: str, question: Any, criteria: Any) -> list[float]:
|
||||
key = json.dumps([state, kind, question, criteria], sort_keys=True, ensure_ascii=False)
|
||||
if key in self.cache:
|
||||
return self.cache[key]
|
||||
if self.calls >= self.max_calls:
|
||||
raise RuntimeError(f"Script exceeded max_jev_calls ({self.max_calls})")
|
||||
self.calls += 1
|
||||
probabilities = self.engine.ask(state, kind, question, criteria)
|
||||
self.cache[key] = probabilities
|
||||
return probabilities
|
||||
|
||||
|
||||
class LocalJev:
|
||||
"""Answers with a local GGUF model. Noul is asked as Yes/No options."""
|
||||
|
||||
def __init__(self, model_path: str):
|
||||
self.model_path = model_path
|
||||
|
||||
def ask(self, state: Any, kind: str, question: Any, criteria: Any) -> list[float]:
|
||||
if kind == "noul":
|
||||
options = ["Yes", "No"]
|
||||
elif kind == "choice":
|
||||
options = [label if description is None else f"{label}: {description}" for label, description in criteria.items()]
|
||||
else:
|
||||
options = criteria
|
||||
return _option_probabilities(self.model_path, state, question, options)
|
||||
|
||||
|
||||
class TypeSafeJev:
|
||||
"""Answers with TypeSafe's Jev API. Returns probabilities in the order of the options."""
|
||||
|
||||
def __init__(self, client: Any):
|
||||
self.client = client
|
||||
|
||||
def ask(self, state: Any, kind: str, question: Any, criteria: Any) -> list[float]:
|
||||
if kind == "noul":
|
||||
p = self._answer(state, Noul(instructions=question)).noul
|
||||
return [p, 1.0 - p]
|
||||
if kind == "choice":
|
||||
probabilities = self._answer(state, Choice(instructions=question, criteria=criteria)).probabilities
|
||||
return [probabilities[label] for label in criteria]
|
||||
probabilities = self._answer(state, Score(instructions=question, criteria=criteria)).probabilities
|
||||
return [probabilities[level] for level in range(len(criteria))]
|
||||
|
||||
def _answer(self, state: Any, question: Any) -> Any:
|
||||
return self.client.system_one(state=state, questions={"q": question}).answers["q"]
|
||||
|
||||
|
||||
def _option_probabilities(model_path: str, state: Any, question: Any, options: list) -> list[float]:
|
||||
"""Reads one decision from the next-token logits of the option letters, like SemIf's direct path."""
|
||||
if not 2 <= len(options) <= len(_LETTERS):
|
||||
raise ValueError(f"Jev questions need 2-{len(_LETTERS)} options, got {len(options)}")
|
||||
llm, formatter = load_local_model(model_path)
|
||||
payload = {
|
||||
"evidence": state,
|
||||
"criterion": question,
|
||||
"options": [{"letter": letter, "description": option} for letter, option in zip(_LETTERS, options)],
|
||||
}
|
||||
messages = [
|
||||
{"role": "system", "content": _DIRECT_SYSTEM},
|
||||
{"role": "user", "content": json.dumps(payload, ensure_ascii=False)},
|
||||
]
|
||||
prompt = formatter(messages=messages, enable_thinking=False).prompt
|
||||
tokens = llm.tokenize(prompt.encode("utf-8"), add_bos=False, special=True)
|
||||
if len(tokens) >= llm.n_ctx():
|
||||
raise ValueError(f"Jev prompt is {len(tokens)} tokens, over the {llm.n_ctx()} token context")
|
||||
slots = []
|
||||
for letter in _LETTERS[: len(options)]:
|
||||
encoded = llm.tokenize(letter.encode("utf-8"), add_bos=False, special=False)
|
||||
if len(encoded) != 1:
|
||||
raise ValueError(f"Model tokenizer does not encode {letter!r} as one token")
|
||||
slots.append(encoded[0])
|
||||
# Each question starts from empty memory, including the recurrent state of hybrid models such as Qwen3.5.
|
||||
# Decode with the llama.cpp API directly instead of the Llama evaluation helper,
|
||||
# whose method name trips the ComfyUI Registry dynamic-execution scanner.
|
||||
llama_memory_clear(llama_get_memory(llm.ctx), True)
|
||||
batch = llama_batch_init(len(tokens), 0, 1)
|
||||
try:
|
||||
for index, token in enumerate(tokens):
|
||||
batch.token[index] = token
|
||||
batch.pos[index] = index
|
||||
batch.n_seq_id[index] = 1
|
||||
batch.seq_id[index][0] = 0
|
||||
batch.logits[index] = int(index == len(tokens) - 1)
|
||||
batch.n_tokens = len(tokens)
|
||||
if llama_decode(llm.ctx, batch) != 0:
|
||||
raise RuntimeError("llama_decode failed for the Jev prompt")
|
||||
finally:
|
||||
llama_batch_free(batch)
|
||||
vocabulary = np.ctypeslib.as_array(llama_get_logits_ith(llm.ctx, -1), shape=(llm.n_vocab(),))
|
||||
logits = vocabulary[slots].astype(np.float64)
|
||||
weights = np.exp(logits - logits.max())
|
||||
return (weights / weights.sum()).tolist()
|
||||
|
||||
|
||||
def _model_dirs() -> list[str]:
|
||||
dirs = []
|
||||
for key in ("LLM", "llm", "text_encoders"):
|
||||
try:
|
||||
dirs.extend(folder_paths.get_folder_paths(key))
|
||||
except KeyError:
|
||||
pass
|
||||
dirs.append(os.path.join(folder_paths.models_dir, "LLM"))
|
||||
return list(dict.fromkeys(os.path.normpath(d) for d in dirs))
|
||||
|
||||
|
||||
def list_local_models() -> dict[str, str]:
|
||||
"""GGUF language models under models/LLM and models/text_encoders, keyed by display name."""
|
||||
models = {}
|
||||
for root in _model_dirs():
|
||||
if not os.path.isdir(root):
|
||||
continue
|
||||
for current_dir, _, files in os.walk(root):
|
||||
for file_name in files:
|
||||
if file_name.endswith(".gguf") and not file_name.startswith("mmproj"):
|
||||
path = os.path.join(current_dir, file_name)
|
||||
models[_display_name(path)] = path
|
||||
return dict(sorted(models.items(), key=lambda item: item[0].lower()))
|
||||
|
||||
|
||||
def _display_name(path: str) -> str:
|
||||
# models registered through extra_model_paths.yaml may be outside models_dir or on another drive
|
||||
try:
|
||||
rel = os.path.relpath(path, folder_paths.models_dir)
|
||||
except ValueError:
|
||||
return path
|
||||
return path if rel.startswith("..") else rel.replace("\\", "/")
|
||||
|
||||
|
||||
def load_local_model(model_path: str) -> tuple[Any, Any]:
|
||||
global _loaded_model
|
||||
if _loaded_model is not None and _loaded_model[0] == model_path:
|
||||
return _loaded_model[1], _loaded_model[2]
|
||||
if Llama is None:
|
||||
raise RuntimeError("llama-cpp-python is not installed. See the README for installation.")
|
||||
unload_local_model()
|
||||
device = comfy.model_management.get_torch_device()
|
||||
comfy.model_management.free_memory(os.path.getsize(model_path) * 1.2, device)
|
||||
# n_batch = n_ctx so one llama_decode call can take the whole prompt
|
||||
llm = Llama(model_path=model_path, n_ctx=_N_CTX, n_batch=_N_CTX, n_gpu_layers=-1, verbose=False)
|
||||
template = llm.metadata.get("tokenizer.chat_template")
|
||||
if not template:
|
||||
llm.close()
|
||||
raise ValueError(f"GGUF has no chat template: {model_path}")
|
||||
bos, eos = llm.token_bos(), llm.token_eos()
|
||||
formatter = Jinja2ChatFormatter(
|
||||
template=template,
|
||||
bos_token=llm.detokenize([bos], special=True).decode("utf-8") if bos != -1 else "",
|
||||
eos_token=llm.detokenize([eos], special=True).decode("utf-8") if eos != -1 else "",
|
||||
)
|
||||
_loaded_model = (model_path, llm, formatter)
|
||||
return llm, formatter
|
||||
|
||||
|
||||
def unload_local_model() -> None:
|
||||
# Same cleanup as ComfyUI-LLM-Session's Unload LLM Model: drop the model, collect, then empty the device cache
|
||||
global _loaded_model
|
||||
if _loaded_model is None:
|
||||
return
|
||||
llm = _loaded_model[1]
|
||||
_loaded_model = None
|
||||
llm.close()
|
||||
del llm
|
||||
gc.collect()
|
||||
comfy.model_management.soft_empty_cache()
|
||||
+2
-2
@@ -1,7 +1,7 @@
|
||||
[project]
|
||||
name = "ComfyUI-ScriptFlow"
|
||||
description = "Python script node for ComfyUI supporting math, calc, logic, and text editing with multi output. Handle conditional workflows, auto-select resolution, parse LLM outputs, and generate filenames — all in one flexible node."
|
||||
version = "1.2.0"
|
||||
description = "Safe Python-like script node with a System One decision model: ask yes/no, choice, and score questions via TypeSafe AI's Jev API, with local GGUF LLMs as a fallback. Multiple inputs and outputs let one node handle math, logic, text editing, conditional routing, resolution selection, filename generation, and more."
|
||||
version = "1.3.1"
|
||||
license = {file = "LICENSE"}
|
||||
# classifiers = [
|
||||
# # For OS-independent nodes (works on all operating systems)
|
||||
|
||||
Reference in New Issue
Block a user