Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
655dda30ac | ||
|
|
773a83703f | ||
|
|
ca50e11275 | ||
|
|
057248fa3a | ||
|
|
56481721a7 |
@@ -1,12 +1,12 @@
|
||||
---
|
||||
description: Bump the package version in pyproject.toml (patch | minor | major) and suggest the matching Conventional Commits release commit message.
|
||||
description: Bump the package version in pyproject.toml (patch | minor | major), date its CHANGELOG section, and suggest the release commit message and tag.
|
||||
allowed-tools: Read, Edit, Bash(git diff:*)
|
||||
argument-hint: patch | minor | major
|
||||
---
|
||||
|
||||
# `/version-bump` — bump pyproject.toml version
|
||||
|
||||
Edits a single field — `[project] version` in `pyproject.toml` — using semver.
|
||||
Edits `[project] version` in `pyproject.toml` using semver, and dates the matching `CHANGELOG.md` section (the publish workflow sends that section to the registry as the version's changelog).
|
||||
|
||||
## Workflow
|
||||
|
||||
@@ -18,18 +18,21 @@ Edits a single field — `[project] version` in `pyproject.toml` — using semve
|
||||
- If missing or invalid: ask once.
|
||||
3. Edit `pyproject.toml`:
|
||||
- `version = "<old>"` → `version = "<new>"`.
|
||||
4. `git diff pyproject.toml` — confirm exactly one line changed.
|
||||
5. Propose a commit message (do not run `git commit`):
|
||||
4. Edit `CHANGELOG.md`:
|
||||
- `## [Unreleased]` → `## [<new>] — <today, YYYY-MM-DD>`.
|
||||
- If there is no `## [Unreleased]` section, warn the user: the registry will show no changelog for `<new>`.
|
||||
5. `git diff pyproject.toml CHANGELOG.md` — confirm exactly one line changed in each.
|
||||
6. Propose the commit message and the tag (do not run `git commit` or `git tag`):
|
||||
|
||||
```
|
||||
chore(release): bump version to <new>
|
||||
```
|
||||
|
||||
Suggest the user run `/commit` (or `git commit -m '...' pyproject.toml`) when they're ready.
|
||||
Suggest the user run `/commit` (or `git commit -m '...' pyproject.toml CHANGELOG.md`), then tag that commit with `git tag -a v<new> -m "v<new>"` and push both with `git push --follow-tags`.
|
||||
|
||||
## Safety rails
|
||||
|
||||
- **Only** modify the `version` field. No description / license / dependency changes.
|
||||
- Do not touch any other file (no CHANGELOG, no git tag, no push).
|
||||
- **Only** modify the `version` field and the CHANGELOG heading. No description / license / dependency changes.
|
||||
- Do not touch any other file (no git tag, no push).
|
||||
- Do not commit.
|
||||
- If `version` is missing or unparseable, stop and ask the user.
|
||||
|
||||
@@ -0,0 +1,54 @@
|
||||
"""Print a version's CHANGELOG.md entry as plain text for the Comfy Registry.
|
||||
|
||||
The registry's "Updates" section shows the changelog as one plain-text
|
||||
paragraph (no Markdown, newlines collapse), so each table is reduced to its
|
||||
first column: "Added: A; B. Fixed: C. Full changelog: <url>".
|
||||
|
||||
Usage: registry_changelog.py [version] (defaults to pyproject.toml's version)
|
||||
"""
|
||||
|
||||
import re
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[2]
|
||||
|
||||
|
||||
def plain(cell):
|
||||
cell = re.sub(r"\[([^\]]*)\]\([^)]*\)", r"\1", cell) # [text](url) -> text
|
||||
return cell.replace("\\|", "|").replace("`", "").replace("**", "").strip()
|
||||
|
||||
|
||||
def main():
|
||||
pyproject = (ROOT / "pyproject.toml").read_text(encoding="utf-8")
|
||||
version = sys.argv[1] if len(sys.argv) > 1 else re.search(r'^version\s*=\s*"([^"]+)"', pyproject, re.M).group(1)
|
||||
changelog = (ROOT / "CHANGELOG.md").read_text(encoding="utf-8")
|
||||
section = re.search(rf"^## \[{re.escape(version)}\][^\n]*\n(.*?)(?=^## \[|\Z)", changelog, re.M | re.S)
|
||||
if not section:
|
||||
print(
|
||||
f"::warning::CHANGELOG.md has no '## [{version}]' section; publishing without a changelog", file=sys.stderr
|
||||
)
|
||||
return
|
||||
|
||||
groups, in_table = [], False # [(heading, [first-column cells])]
|
||||
for line in section.group(1).splitlines():
|
||||
if line.startswith("### "):
|
||||
groups.append((line[4:].strip(), []))
|
||||
in_table = False
|
||||
elif line.startswith("|"):
|
||||
if not in_table:
|
||||
in_table = True # the first row of every table is its header
|
||||
elif groups and not re.match(r"^\|[\s:|-]+$", line): # skip the |---| separator
|
||||
groups[-1][1].append(plain(re.split(r"(?<!\\)\|", line)[1]))
|
||||
else:
|
||||
in_table = False
|
||||
parts = [f"{heading}: {'; '.join(items)}." for heading, items in groups if items]
|
||||
|
||||
repo = re.search(r'^Repository\s*=\s*"([^"]+)"', pyproject, re.M)
|
||||
if repo:
|
||||
parts.append(f"Full changelog: {repo.group(1)}/blob/master/CHANGELOG.md")
|
||||
print(" ".join(parts))
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -19,6 +19,14 @@ jobs:
|
||||
steps:
|
||||
- name: Check out code
|
||||
uses: actions/checkout@v4
|
||||
# comfy-cli sends $COMFY_NODE_CHANGELOG as the version's changelog (the registry's "Updates" section)
|
||||
- name: Build registry changelog from CHANGELOG.md
|
||||
run: |
|
||||
{
|
||||
echo "COMFY_NODE_CHANGELOG<<CHANGELOG_EOF"
|
||||
python3 .github/scripts/registry_changelog.py
|
||||
echo "CHANGELOG_EOF"
|
||||
} >> "$GITHUB_ENV"
|
||||
- name: Publish Custom Node
|
||||
uses: Comfy-Org/publish-node-action@v1
|
||||
with:
|
||||
|
||||
@@ -2,6 +2,31 @@
|
||||
|
||||
All notable changes to this project from v1.1.0 onward are documented in this file. Earlier history lives in `git log`.
|
||||
|
||||
## [1.7.0] — 2026-10-17
|
||||
|
||||
### Added
|
||||
|
||||
| Node / Feature | Endpoint | Notes |
|
||||
|---|---|---|
|
||||
| Flux 3 Image Klein (BFL) | `POST /v1/flux-3-image-klein` | BFL release 2026-10-15: the open-weights 5.6B FLUX 3 Image. Same prompt (bounding boxes as a JSON list at its end), `image_1`–`image_10` references, `resolution` and `aspect_ratio` as Flux 3 Image, and no `grounding`. Adds `seed` (-1 = random), `steps` (1–16), `output_format` (jpeg / png), an exact `width` × `height` (64–6256, sent only when both are set, overriding resolution and aspect ratio) and `safety_tolerance` 0–7. Optional fields are sent only when changed. Shares Flux 3 Image's 120-attempt polling ceiling. |
|
||||
| FLUX 3 Image Klein example workflow groups | — | `workflows/BFL-API-flux-3-image.json` gets two groups: **Klein — Text to Image** (bounding boxes, fixed seed) and **Klein — Edit an Image** (reference by URL). Appended only, so the existing nodes, links and groups are unchanged. The new groups are bypassed like the rest. Loaded in ComfyUI 0.37.0 (frontend 1.52.7). |
|
||||
|
||||
## [1.6.0] — 2026-10-02
|
||||
|
||||
### Added
|
||||
|
||||
| Node / Feature | Endpoint | Notes |
|
||||
|---|---|---|
|
||||
| Flux 3 Image (BFL) | `POST /v1/flux-3-image` | BFL release 2026-10-01. One node for text-to-image and editing: `prompt` (bounding boxes go at its end as a JSON list), up to 10 reference sockets `image_1`–`image_10` sent as `images` and numbered Image 1…N in socket order (a skipped socket logs a warning, since the numbering shifts), `resolution` (`768sq` / `1k` / `1.5k` / `2k` / `4k`), `aspect_ratio` (15 ratios or `auto`), `grounding` (web and image search, on by default), `safety_tolerance` (0–4). The endpoint rejects `seed`, `output_format` and webhooks, so none are exposed. Polls up to 120 × 5 s because `4k` can take several minutes, and keeps the result lossless (png) on its way into the IMAGE tensor. |
|
||||
| FLUX 3 Image example workflow | — | New `workflows/BFL-API-flux-3-image.json`, built and saved with ComfyUI 0.37.0 (frontend 1.52.7): text-to-image with bounding boxes in the prompt, an edit with a reference given by URL (core **Text** node), and a style transfer that combines a local image (Load Image → Image to Base64, as Image 1) with a URL reference (Image 2). Groups are bypassed like the other examples. |
|
||||
|
||||
### Fixed
|
||||
|
||||
| Issue | Detail |
|
||||
|---|---|
|
||||
| Polling ignored the returned polling_url | BFL requires polling the `polling_url` from the submit response (it points at the region holding the task; FLUX 3 Image returns a regional one). `BaseFlux.post_request` now passes it to `get_result` along with the task id, for every node; the self-built `get_result?id=` is only a fallback when a response has none. |
|
||||
| Failed tasks reported as HTTP 503 were polled until timeout | FLUX 3 can report a failed task as HTTP 503 with a normal JSON body. A non-200 poll whose body carries `Error`, `Content Moderated` or `Request Moderated` now stops at once and logs the details instead of retrying to `max_attempts`. |
|
||||
|
||||
## [1.5.0] — 2026-09-26
|
||||
|
||||
### Added
|
||||
|
||||
@@ -89,10 +89,10 @@ This is a **ComfyUI custom-nodes package** that wraps the **Black Forest Labs (B
|
||||
- **Seed handling**: `seed = -1` means *random*. The node parameter defaults to `-1`. Only insert `arguments["seed"] = seed` into the request body when `seed != -1` — never send `-1` to BFL.
|
||||
- **Optional argument inclusion**: build the request `arguments` dict with required keys, then conditionally add optional ones only when non-default / non-empty (`if webhook_url:`, `if image_prompt:`, …). Matches every node in `nodes/api_node.py`.
|
||||
- **Graceful failure**: on any unhandled exception, on terminal API status (`Error`, `Content Moderated`, `Request Moderated`), or on polling exhaustion, return `self.create_blank_image()` — a black 512×512 IMAGE tensor. ComfyUI workflows must never crash because of a BFL failure.
|
||||
- **Polling**: 5-second interval, max 40 attempts (~200 s ceiling). Already implemented in `BaseFlux.get_result` — do not write a second polling loop in a subclass.
|
||||
- **Polling**: 5-second interval, max 40 attempts (~200 s ceiling). Already implemented in `BaseFlux.get_result` — do not write a second polling loop in a subclass. Slow endpoints pass a larger `max_attempts` instead (Flux 3 Video 240, Flux 3 Image 120).
|
||||
- **Config resolution**: every generation node accepts an optional `config` input of type `BFL_CONFIG` (the output of `FluxConfig_BFL`). Pass it through to `super().generate_image(url_path, arguments, config)`; the base class hands it to `get_config_loader(config)` from `nodes/config_node.py:57-67`, which decides whether to use the `config.ini`-backed default loader or build one from the override. **Both paths must work** — `config=None` (use file) and `config=<dict>` (override).
|
||||
- **Conventional Commits 1.0.0**: `<type>[scope][!]: <description>`. Types: `feat`, `fix`, `refactor`, `chore`, `docs`, `style`, `perf`, `test`, `build`, `ci`. Scopes: `api`, `finetune`, `config`, `utils`, `web`, `docs`, `release`, `tooling`. Description: lowercase, imperative ("add", not "added"/"adds"), no trailing period, ≤ 72 chars. Body explains *why*, wrapped at 72. Use `!` after type/scope for breaking changes. No Jira prefix. **No `Co-Authored-By: Claude` / "Generated with…" footer.** Do NOT inspect `git log` to derive style — historical commits used `update:` and similar non-standard types; the new standard starts now.
|
||||
- **Version bumps**: edit `pyproject.toml` `version` field only. Commit message: `chore(release): bump version to X.Y.Z`.
|
||||
- **Version bumps**: edit `pyproject.toml` `version` field and rename `CHANGELOG.md`'s `## [Unreleased]` heading to `## [X.Y.Z] — YYYY-MM-DD` — the publish workflow turns that section into the registry's changelog (`.github/scripts/registry_changelog.py` keeps the first column of each table row, so keep those entries short and plain). Commit message: `chore(release): bump version to X.Y.Z`. Then tag that commit `git tag -a vX.Y.Z -m "vX.Y.Z"` and push both with `git push --follow-tags`.
|
||||
|
||||
### Never do
|
||||
|
||||
@@ -109,7 +109,7 @@ This is a **ComfyUI custom-nodes package** that wraps the **Black Forest Labs (B
|
||||
- `ConfigLoader` in `nodes/config.py:85-86` constructs a module-level singleton `config_loader` at import time. Some nodes still reference that singleton; new code should use `get_config_loader(config)` from `nodes/config_node.py:57-67` (which respects the per-call override) instead.
|
||||
- `BASE_URL` in `config.ini` may or may not end with `/`. `ConfigLoader.create_url` uses `urljoin` — pass the `path` argument **without** a leading slash, otherwise `urljoin` will strip the existing prefix path.
|
||||
- **Finetune endpoints are regional**: `api.us.bfl.ai`, `api.eu.bfl.ai`. The default `api.bfl.ai/v1/` does **not** serve finetune routes. Region selection is via `FluxConfig_BFL`'s `region` field or per-call. See `nodes/finetune.py`.
|
||||
- BFL's async pattern: POST to `flux-pro-1.1` (etc.) returns `{"id": "<task_id>"}`. Polling GET goes to `get_result?id=<task_id>` — note: **query parameter `id`**, not a path segment. `BaseFlux.get_result` already builds this correctly; if you write a one-off polling call, match it.
|
||||
- BFL's async pattern: POST to `flux-pro-1.1` (etc.) returns `{"id": "<task_id>", "polling_url": "..."}`. BFL requires polling that **returned `polling_url`** — it points at the region holding the task (FLUX 3 Image returns a regional one), so don't build the URL yourself. `BaseFlux.post_request` returns a `TaskId` (a `str` subclass) that carries it, and `BaseFlux.get_result` polls it; only a plain id with no polling URL falls back to `get_result?id=<task_id>` (query parameter `id`, not a path segment). If you write a one-off POST, pass its `polling_url` through the same way.
|
||||
- The newer Flux 2 / Klein nodes (`nodes/api_node.py` lower half) sometimes inline `self.post_request` + `self.get_result` instead of calling `super().generate_image(...)`. Both styles exist in the codebase; if you're scaffolding a new node, **prefer `super().generate_image(...)`** — it's shorter and runs the dimension check for you.
|
||||
- `__init__.py` does dynamic import of every entry in `node_list`. A new node file in `nodes/<name>.py` is invisible to ComfyUI until you append `"<name>"` to that list.
|
||||
- `WEB_DIRECTORY = "./web"` in `__init__.py:21` exposes `web/js/*` to ComfyUI's frontend. Renaming or moving `web/` breaks the frontend extensions silently — they just won't load.
|
||||
|
||||
@@ -36,11 +36,22 @@ Custom nodes for integrating Flux models with the BFL API.
|
||||
|
||||
You can either use `config.ini` for a global API key, or connect a **Flux Config (BFL)** node directly to any generation node to override the key, base URL, and region per-node. If no config node is connected, `config.ini` is used automatically.
|
||||
|
||||
### How results are polled
|
||||
|
||||
Every BFL request is asynchronous: the node submits the job, then polls until the result is ready.
|
||||
|
||||
- The submit response contains a `polling_url`, and the node polls **that URL**. BFL requires this because it points at the region that holds your task (Flux 3 Image, for example, returns a regional one).
|
||||
- Only when a response has no `polling_url` does the node fall back to building `<base_url>get_result?id=<task_id>` itself, with the base URL from `config.ini` or the **Flux Config (BFL)** node.
|
||||
- Polls run every 5 s: up to 40 attempts (~200 s) for image nodes, 120 for **Flux 3 Image** and **Flux 3 Image Klein** (`4k` can take several minutes) and 240 for the video nodes. A terminal status (`Error`, `Request Moderated`, `Content Moderated`) stops polling at once, even when BFL reports it as HTTP 503, and the node returns a black placeholder so the workflow keeps running.
|
||||
- Every poll is logged in the ComfyUI console as `[BFL] Poll attempt … | GET <url>`, so you can see which URL was used.
|
||||
|
||||
## Nodes
|
||||
|
||||
### Generation
|
||||
| Node | Description |
|
||||
|---|---|
|
||||
| Flux 3 Image (BFL) | Text-to-image and editing with FLUX 3 in one node — up to 10 references (`image_1`…`image_10` are "Image 1"…"Image 10" in the prompt), bounding boxes as a JSON list at the end of the prompt, `768sq` to `4k` output (`flux-3-image`) |
|
||||
| Flux 3 Image Klein (BFL) | Open-weights 5.6B FLUX 3 — same prompt, 10 references and bounding boxes as Flux 3 Image, plus `seed`, `steps` (1–16), `output_format` and exact `width` × `height`; no grounding (`flux-3-image-klein`) |
|
||||
| Flux 3 Video T2V (BFL) | Text-to-video with FLUX 3 Video (up to 20 s, with audio) |
|
||||
| Flux 3 Video I2V (BFL) | Image-to-video — feed keyframes from the Flux 3 Keyframes node or a single image |
|
||||
| Flux 3 Video V2V (BFL) | Video continuation from an existing MP4 (URL or base64), up to 15 s |
|
||||
|
||||
@@ -850,6 +850,122 @@ class Flux2Klein4b(BaseFlux):
|
||||
return self.create_blank_image()
|
||||
|
||||
|
||||
class Flux3Image(BaseFlux):
|
||||
CATEGORY = "BFL/Flux3"
|
||||
# 4k renders can take several minutes (BFL docs); 5s interval -> ~10 min
|
||||
MAX_ATTEMPTS = 120
|
||||
IMAGE_SLOTS = [f"image_{i}" for i in range(1, 11)]
|
||||
|
||||
@classmethod
|
||||
def INPUT_TYPES(cls):
|
||||
image_socket = ("STRING", {
|
||||
"forceInput": True,
|
||||
"tooltip": "Reference image (base64 from Image to Base64, or a URL). The prompt calls the "
|
||||
"connected images Image 1, Image 2, ... in socket order"
|
||||
})
|
||||
return {
|
||||
"required": {
|
||||
"prompt": ("STRING", {"default": "", "multiline": True}),
|
||||
"resolution": (["768sq", "1k", "1.5k", "2k", "4k"], {"default": "1k"}),
|
||||
"aspect_ratio": (["auto", "21:9", "2:1", "16:9", "3:2", "7:5", "4:3", "5:4", "1:1", "4:5", "3:4",
|
||||
"5:7", "2:3", "9:16", "1:2", "9:21"], {"default": "auto"}),
|
||||
"grounding": ("BOOLEAN", {
|
||||
"default": True,
|
||||
"tooltip": "Let FLUX 3 research the prompt with web and image search before generating"
|
||||
}),
|
||||
"safety_tolerance": ("INT", {"default": 2, "min": 0, "max": 4})
|
||||
},
|
||||
"optional": {
|
||||
**{name: image_socket for name in cls.IMAGE_SLOTS},
|
||||
"config": ("BFL_CONFIG",)
|
||||
}
|
||||
}
|
||||
|
||||
def generate_image(self, prompt, resolution, aspect_ratio, grounding, safety_tolerance, config=None, **images):
|
||||
arguments = {"prompt": prompt}
|
||||
connected = [name for name in self.IMAGE_SLOTS if images.get(name)]
|
||||
if connected:
|
||||
arguments["images"] = [images[name] for name in connected]
|
||||
if connected != self.IMAGE_SLOTS[:len(connected)]:
|
||||
print(f"[BFL] Warning: references {connected} skip a socket — BFL numbers them Image 1.."
|
||||
f"{len(connected)} in this order, so 'Image N' in the prompt shifts")
|
||||
if resolution != "1k":
|
||||
arguments["resolution"] = resolution
|
||||
if aspect_ratio != "auto":
|
||||
arguments["aspect_ratio"] = aspect_ratio
|
||||
if safety_tolerance != 2:
|
||||
arguments["safety_tolerance"] = safety_tolerance
|
||||
if not grounding:
|
||||
arguments["grounding"] = False
|
||||
try:
|
||||
task_id = self.post_request("flux-3-image", arguments, config)
|
||||
if task_id:
|
||||
# png: process_result re-encodes the result into this format, and jpeg would recompress it
|
||||
return self.get_result(task_id, output_format="png", max_attempts=self.MAX_ATTEMPTS,
|
||||
config_override=config)
|
||||
return self.create_blank_image()
|
||||
except Exception as e:
|
||||
print(f"[BFL] Error generating image: {str(e)}")
|
||||
return self.create_blank_image()
|
||||
|
||||
|
||||
class Flux3ImageKlein(Flux3Image):
|
||||
@classmethod
|
||||
def INPUT_TYPES(cls):
|
||||
types = super().INPUT_TYPES()
|
||||
del types["required"]["grounding"]
|
||||
types["required"]["safety_tolerance"] = ("INT", {"default": 2, "min": 0, "max": 7})
|
||||
types["required"]["output_format"] = (["jpeg", "png"], {"default": "jpeg"})
|
||||
types["required"]["steps"] = ("INT", {"default": 16, "min": 1, "max": 16})
|
||||
size = ("INT", {
|
||||
"default": 0, "min": 0, "max": 6256,
|
||||
"tooltip": "0 = not sent. Set width and height together (64-6256) to override resolution and aspect_ratio"
|
||||
})
|
||||
types["optional"] = {
|
||||
"width": size,
|
||||
"height": size,
|
||||
"seed": ("INT", {"default": -1}),
|
||||
**types["optional"]
|
||||
}
|
||||
return types
|
||||
|
||||
def generate_image(self, prompt, resolution, aspect_ratio, safety_tolerance, output_format, steps,
|
||||
width=0, height=0, seed=-1, config=None, **images):
|
||||
arguments = {"prompt": prompt}
|
||||
connected = [name for name in self.IMAGE_SLOTS if images.get(name)]
|
||||
if connected:
|
||||
arguments["images"] = [images[name] for name in connected]
|
||||
if connected != self.IMAGE_SLOTS[:len(connected)]:
|
||||
print(f"[BFL] Warning: references {connected} skip a socket — BFL numbers them Image 1.."
|
||||
f"{len(connected)} in this order, so 'Image N' in the prompt shifts")
|
||||
if width > 0 and height > 0:
|
||||
arguments["width"] = width
|
||||
arguments["height"] = height
|
||||
elif width > 0 or height > 0:
|
||||
print("[BFL] Warning: width and height must be set together — sending neither")
|
||||
if resolution != "1k":
|
||||
arguments["resolution"] = resolution
|
||||
if aspect_ratio != "auto":
|
||||
arguments["aspect_ratio"] = aspect_ratio
|
||||
if safety_tolerance != 2:
|
||||
arguments["safety_tolerance"] = safety_tolerance
|
||||
if output_format != "jpeg":
|
||||
arguments["output_format"] = output_format
|
||||
if steps != 16:
|
||||
arguments["steps"] = steps
|
||||
if seed != -1:
|
||||
arguments["seed"] = seed
|
||||
try:
|
||||
task_id = self.post_request("flux-3-image-klein", arguments, config)
|
||||
if task_id:
|
||||
return self.get_result(task_id, output_format=output_format, max_attempts=self.MAX_ATTEMPTS,
|
||||
config_override=config)
|
||||
return self.create_blank_image()
|
||||
except Exception as e:
|
||||
print(f"[BFL] Error generating image: {str(e)}")
|
||||
return self.create_blank_image()
|
||||
|
||||
|
||||
class FluxCredits:
|
||||
RETURN_TYPES = ("STRING",)
|
||||
FUNCTION = "get_credits"
|
||||
@@ -887,6 +1003,8 @@ NODE_CLASS_MAPPINGS = {
|
||||
"Flux2Klein9b_BFL": Flux2Klein9b,
|
||||
"Flux2Klein9bPreview_BFL": Flux2Klein9bPreview,
|
||||
"Flux2Klein4b_BFL": Flux2Klein4b,
|
||||
"Flux3Image_BFL": Flux3Image,
|
||||
"Flux3ImageKlein_BFL": Flux3ImageKlein,
|
||||
"FluxCredits_BFL": FluxCredits
|
||||
}
|
||||
|
||||
@@ -905,5 +1023,7 @@ NODE_DISPLAY_NAME_MAPPINGS = {
|
||||
"Flux2Klein9b_BFL": "Flux 2 Klein 9B (BFL)",
|
||||
"Flux2Klein9bPreview_BFL": "Flux 2 Klein 9B Preview (BFL)",
|
||||
"Flux2Klein4b_BFL": "Flux 2 Klein 4B (BFL)",
|
||||
"Flux3Image_BFL": "Flux 3 Image (BFL)",
|
||||
"Flux3ImageKlein_BFL": "Flux 3 Image Klein (BFL)",
|
||||
"FluxCredits_BFL": "Flux Credits (BFL)"
|
||||
}
|
||||
|
||||
+29
-4
@@ -12,6 +12,16 @@ from .status import Status
|
||||
REQUEST_TIMEOUT = 300 # seconds for connect + read
|
||||
|
||||
|
||||
class TaskId(str):
|
||||
"""Task id from post_request that also carries the submit response's polling_url.
|
||||
|
||||
BFL requires polling the returned polling_url (it points at the region holding the
|
||||
task); as a str subclass, callers keep passing the id straight to get_result.
|
||||
"""
|
||||
|
||||
polling_url = None
|
||||
|
||||
|
||||
class BaseFlux:
|
||||
RETURN_TYPES = ("IMAGE",)
|
||||
FUNCTION = "generate_image"
|
||||
@@ -66,19 +76,32 @@ class BaseFlux:
|
||||
print(f"[BFL] POST response: {response.status_code}")
|
||||
|
||||
if response.status_code == 200:
|
||||
task_id = response.json().get("id")
|
||||
print(f"[BFL] Task ID: {task_id}")
|
||||
body = response.json()
|
||||
print(f"[BFL] Task ID: {body.get('id')}")
|
||||
if not body.get("id"):
|
||||
return None
|
||||
task_id = TaskId(body["id"])
|
||||
task_id.polling_url = body.get("polling_url")
|
||||
return task_id
|
||||
else:
|
||||
print(f"[BFL] Error initiating request: {response.status_code}, {response.text}")
|
||||
return None
|
||||
|
||||
@staticmethod
|
||||
def _reports_terminal_status(response):
|
||||
try:
|
||||
status = response.json().get("status")
|
||||
except (ValueError, AttributeError):
|
||||
return False
|
||||
return status in (Status.ERROR.value, Status.CONTENT_MODERATED.value, Status.REQUEST_MODERATED.value)
|
||||
|
||||
def get_result(self, task_id, output_format="jpeg", max_attempts=40, config_override=None):
|
||||
# Use ConfigLoader with optional config override
|
||||
config_loader_instance = get_config_loader(config_override)
|
||||
|
||||
headers = {"x-key": config_loader_instance.get_x_key()}
|
||||
get_url = config_loader_instance.create_url(f"get_result?id={task_id}")
|
||||
# Build get_result?id= only for a plain id without a returned polling_url (e.g. a caller's own POST)
|
||||
get_url = getattr(task_id, "polling_url", None) or config_loader_instance.create_url(f"get_result?id={task_id}")
|
||||
attempt = 1
|
||||
start_time = time.time()
|
||||
print(f"[BFL] Polling task {task_id} (max {max_attempts} attempts, 5s interval)")
|
||||
@@ -90,7 +113,9 @@ class BaseFlux:
|
||||
result_response = requests.get(get_url, headers=headers, timeout=REQUEST_TIMEOUT)
|
||||
print(f"[BFL] Poll response: {result_response.status_code}")
|
||||
|
||||
if result_response.status_code != 200:
|
||||
# FLUX 3 reports some failed tasks as HTTP 503 with a normal JSON body: let those fall
|
||||
# through to the terminal-status branch below instead of retrying until max_attempts
|
||||
if result_response.status_code != 200 and not self._reports_terminal_status(result_response):
|
||||
print(
|
||||
f"[BFL] HTTP error on attempt {attempt}/{max_attempts}: "
|
||||
f"{result_response.status_code}, {result_response.text}"
|
||||
|
||||
+1
-1
@@ -1,7 +1,7 @@
|
||||
[project]
|
||||
name = "comfyui-flux-bfl-api"
|
||||
description = "Custom nodes for integrating Flux models with the BFL API."
|
||||
version = "1.5.0"
|
||||
version = "1.7.0"
|
||||
license = {file = "LICENSE"}
|
||||
dependencies = ["torch"]
|
||||
|
||||
|
||||
File diff suppressed because one or more lines are too long
Reference in New Issue
Block a user