Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions docs/source/en/_toctree.yml
Original file line number Diff line number Diff line change
Expand Up @@ -321,6 +321,8 @@
title: AnyFlowTransformer3DModel
- local: api/models/aura_flow_transformer2d
title: AuraFlowTransformer2DModel
- local: api/models/transformer_bria_fibo2
title: BriaFibo2Transformer2DModel
- local: api/models/transformer_bria_fibo
title: BriaFiboTransformer2DModel
- local: api/models/bria_transformer
Expand Down Expand Up @@ -543,6 +545,8 @@
title: Bria 3.2
- local: api/pipelines/bria_fibo
title: Bria Fibo
- local: api/pipelines/bria_fibo2
title: Bria Fibo 2
- local: api/pipelines/bria_fibo_edit
title: Bria Fibo Edit
- local: api/pipelines/chroma
Expand Down
19 changes: 19 additions & 0 deletions docs/source/en/api/models/transformer_bria_fibo2.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
<!--Copyright 2026 The HuggingFace Team. All rights reserved.

Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with
the License. You may obtain a copy of the License at

http://www.apache.org/licenses/LICENSE-2.0

Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on
an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the
specific language governing permissions and limitations under the License.
-->

# BriaFibo2Transformer2DModel

The single-stream transformer of [Bria](https://huggingface.co/briaai)'s fibo-2, built on the Z-Image block. Its text conditioning comes from Qwen3-VL in two ways: a Perceiver resampler turns the text encoder's hidden states into gist tokens that share the stream with the image tokens, and five blocks add a gated cross-attention to their own bundle of Qwen3-VL layers. To edit, the latents of the images to edit join the stream as clean tokens, each image on its own RoPE plane.

## BriaFibo2Transformer2DModel

[[autodoc]] BriaFibo2Transformer2DModel
93 changes: 93 additions & 0 deletions docs/source/en/api/pipelines/bria_fibo2.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
<!--Copyright 2026 The HuggingFace Team. All rights reserved.

Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with
the License. You may obtain a copy of the License at

http://www.apache.org/licenses/LICENSE-2.0

Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on
an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the
specific language governing permissions and limitations under the License.
-->

# Bria Fibo 2

fibo-2 is an 8.1B parameter flow-matching transformer from [Bria](https://huggingface.co/briaai) that generates images from structured JSON captions. Its text encoder is [Qwen3-VL](https://huggingface.co/Qwen/Qwen3-VL-4B-Instruct): a Perceiver condenses Qwen3-VL's hidden states into gist tokens that join the image tokens in one stream, and five of the transformer's blocks also read Qwen3-VL directly through gated cross-attention.

The checkpoint, [`briaai/fibo-2`](https://huggingface.co/briaai/fibo-2), runs 30 steps with guidance 5, the pipeline's defaults. With guidance on, the default negative prompt is the null caption the model was trained with: the structured caption with every value left empty.

fibo-2 is trained on structured JSON captions and will not work well with freeform text. Pass the caption serialized as JSON:

```py
import json

prompt = json.dumps(caption, separators=(",", ":"), ensure_ascii=False)
```

The pipeline cleans the caption the way training did: it drops empty values and the `aesthetic_score` and `preference_score` fields, which no training caption had, and serializes the caption compactly. Two short forms were trained too and are kept as they are: the caption's `short_description` alone, as plain text, and a caption of exactly a `"short_description"` and a `"text_render"` list, which keeps the list even when it's empty to say the image has no text.

To turn freeform text into a structured caption, see the prompt-to-JSON models on the [Bria Fibo](https://huggingface.co/briaai/FIBO) page.

## Editing

The same pipeline edits images. Pass the image as `image` and the edit instruction as the prompt:

```py
import torch
from diffusers import BriaFibo2Pipeline
from diffusers.utils import load_image

pipe = BriaFibo2Pipeline.from_pretrained("briaai/fibo-2", dtype=torch.bfloat16).to("cuda")
image = load_image(
"https://huggingface.co/datasets/huggingface/documentation-images/resolve/main/diffusers/yarn-art-pikachu.png"
)
edited = pipe("Make the background a snowy forest", image=image).images[0]
```

- The prompt can also be a JSON object with an `"edit_instruction"` key and a caption of the result, in any of the forms fibo-2 was trained to edit with. The pipeline turns it into the prompt fibo-2 was trained to edit with.
- **A full caption:** the caption's keys beside `"edit_instruction"`, as Bria's prompt-to-JSON models write for edits. The pipeline drops empty values and the `aesthetic_score` and `preference_score` fields, which training removed from every caption.
- **A short description:** a string in a `"structured_caption"` key, for example `{"structured_caption": "A woman in a red coat on a snowy street.", "edit_instruction": "Make the coat green."}`.
- **A short description and its text:** exactly a `"short_description"` and a `"text_render"` list. The list is kept even when it's empty, which tells fibo-2 the result has no text.

A `"structured_caption"` key can hold either JSON form too.
- Up to 5 images can be edited together, for example to put an object from one into another. The instruction refers to them in order as `<image_1>`, `<image_2>` and so on.
- Unless `height` and `width` are given, the result takes the size fibo-2 was trained at for the aspect ratio of the first image, about one megapixel. A single image is cropped to the size of the result. Several images are each cropped to the trained size for their own aspect ratio.
- `mask` marks the region of a single image to regenerate, white on black. fibo-2 was trained on edits whose image has that region greyed out, and the pipeline greys it out the same way before the image is encoded: flat (128, 128, 128) grey, through the mask made binary at 128.

## Text in images

When the caption asks for text, in its `text_render` entries, fibo-2 tends to render it better if Qwen3-VL also sees each text drawn on its own. The pipeline draws these images by default, the way training did, and they can be turned off:

```py
image = pipe(prompt, render_text_images=False).images[0]
```

Training showed fibo-2 these images for some captions with text and not for others, so it works either way.

- Each `text_render` entry that has `text` gets an image: the text in one line, black on white, in Pillow's built-in font at size 20. A text longer than 256 characters is split into lines of 256.
- Characters that font lacks are drawn in Aileron, then DejaVu Sans. The pipeline reads these fonts when a text first needs them, from the `fonts` folder of the folder it was loaded from, or else from [`briaai/fibo-2`](https://huggingface.co/briaai/fibo-2).
- The pipeline marks each entry with an `image_tokens` field for its image, and Qwen3-VL sees the images after any images to edit.
- A generation runs its first steps, while the noise level is above `pipe.text_images_max_noise_level` (0.9), on the caption without the images. An edit uses them at every step.
- A caption without text gets no images, nor does a short description, as in training.

To draw the images yourself, pass them as `text_images`, one per `text_render` entry that has `text`, in order, for a single prompt. They take precedence over the pipeline's own. Images drawn differently from training's are out of distribution.

## Attention backends

The transformer runs on any of the diffusers [attention backends](../../optimization/attention_backends), for example FlashAttention-3 on Hopper GPUs such as the H100:

```py
pipe.transformer.set_attention_backend("_flash_3_varlen_hub")
```

`_flash_3_hub` also works for a single prompt, but it doesn't take masks, so it can't run a batch of prompts of different lengths (see below).

## Batching

Several prompts can be generated in one call. Prompts of different lengths are padded, and the padding is masked out of every attention layer, so such a batch needs an attention backend that takes a mask: the default one, or a varlen backend such as `flash_varlen`. In float32, a batch reproduces the images of the separate calls; in bfloat16 the attention kernels differ between padded and unpadded inputs, so batched images can differ from single calls in fine details.

## BriaFibo2Pipeline

[[autodoc]] BriaFibo2Pipeline
- all
- __call__
167 changes: 167 additions & 0 deletions scripts/convert_bria_fibo2_to_diffusers.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,167 @@
import argparse
import json
import os

import safetensors.torch
import torch
from accelerate import init_empty_weights
from huggingface_hub import snapshot_download
from transformers import AutoImageProcessor, Qwen3VLForConditionalGeneration, Qwen3VLProcessor

from diffusers import (
AutoencoderKLFlux2,
BriaFibo2Pipeline,
BriaFibo2Transformer2DModel,
FlowMatchEulerDiscreteScheduler,
)
from diffusers.configuration_utils import FrozenDict


"""
# Transformer only

python scripts/convert_bria_fibo2_to_diffusers.py \
--original_repo_id briaai/fibo-2 \
--output_path fibo-2-diffusers

# Full pipeline

python scripts/convert_bria_fibo2_to_diffusers.py \
--original_repo_id briaai/fibo-2 \
--output_path fibo-2-diffusers \
--full_pipe
"""

parser = argparse.ArgumentParser()
parser.add_argument("--original_repo_id", default=None, type=str)
parser.add_argument("--checkpoint_path", default=None, type=str)
parser.add_argument("--output_path", type=str, required=True)
parser.add_argument("--full_pipe", action="store_true")

args = parser.parse_args()


TRANSFORMER_KEYS_RENAME_DICT = {
# one embedder and one final layer instead of Z-Image's one per patch size
"all_x_embedder.2-1.": "x_embedder.",
"all_final_layer.2-1.": "final_layer.",
# injection blocks and Perceiver layers: cross-attention to the text encoder
"cross_q.": "cross_attention.to_q.",
"cross_k.": "cross_attention.to_k.",
"cross_v.": "cross_attention.to_v.",
"cross_out.": "cross_attention.to_out.0.",
"cross_norm_q.": "cross_attention.norm_q.",
"cross_norm_k.": "cross_attention.norm_k.",
# Perceiver layers: self-attention among the gist tokens
"self_q.": "self_attention.to_q.",
"self_k.": "self_attention.to_k.",
"self_v.": "self_attention.to_v.",
"self_out.": "self_attention.to_out.0.",
"self_norm_q.": "self_attention.norm_q.",
"self_norm_k.": "self_attention.norm_k.",
}

# BriaFibo2Pipeline.text_encoder_out_layers: the Qwen3-VL layers behind the Perceiver, then each injection block
PIPELINE_TEXT_ENCODER_LAYERS = [[9, 20, 31], [5, 16, 27], [7, 18, 29], [9, 20, 31], [11, 22, 33], [13, 24, 35]]


def get_checkpoint_path(args):
if args.original_repo_id is not None:
allow_patterns = None if args.full_pipe else ["transformer/*"]
return snapshot_download(args.original_repo_id, allow_patterns=allow_patterns)
elif args.checkpoint_path is not None:
return args.checkpoint_path
else:
raise ValueError("please provide either `original_repo_id` or a local `checkpoint_path`")


def load_original_checkpoint(checkpoint_path):
transformer_path = os.path.join(checkpoint_path, "transformer")
with open(os.path.join(transformer_path, "config.json")) as f:
original_config = json.load(f)
with open(os.path.join(transformer_path, "diffusion_pytorch_model.safetensors.index.json")) as f:
shard_names = sorted(set(json.load(f)["weight_map"].values()))

original_state_dict = {}
for shard_name in shard_names:
original_state_dict.update(safetensors.torch.load_file(os.path.join(transformer_path, shard_name)))
return original_config, original_state_dict


def get_transformer_config(original_config):
if original_config["perceiver_num_heads"] not in (None, original_config["n_heads"]):
raise ValueError("The Perceiver must use as many attention heads as the main blocks.")
# BriaFibo2Pipeline feeds the transformer fixed Qwen3-VL layers: the Perceiver's, then three per injection block
text_encoder_layers = [original_config["perceiver_llm_layers"], *original_config["injection_llm_layers"]]
if text_encoder_layers != PIPELINE_TEXT_ENCODER_LAYERS:
raise ValueError(
f"The checkpoint reads Qwen3-VL layers {text_encoder_layers}, but BriaFibo2Pipeline feeds it"
f" {PIPELINE_TEXT_ENCODER_LAYERS}."
)

return {
"in_channels": original_config["in_channels"],
"patch_size": original_config["patch_size"],
"dim": original_config["dim"],
"n_layers": original_config["n_layers"],
"n_refiner_layers": original_config["n_refiner_layers"],
"n_heads": original_config["n_heads"],
"cap_feat_dim": original_config["cond_llm_dim"],
"injection_layer_ids": original_config["injection_layer_ids"],
"perceiver_num_layers": original_config["perceiver_num_layers"],
"min_num_gist_tokens": original_config["min_num_gist_tokens"],
"max_num_gist_tokens": original_config["max_num_gist_tokens"],
"gist_step": original_config["gist_step"],
"gist_min_text_len": original_config["gist_min_text_len"],
"gist_max_text_len": original_config["gist_max_text_len"],
}


def convert_bria_fibo2_transformer_to_diffusers(original_config, original_state_dict):
with init_empty_weights():
transformer = BriaFibo2Transformer2DModel.from_config(get_transformer_config(original_config))

converted_state_dict = {}
for key, value in original_state_dict.items():
new_key = key
for old, new in TRANSFORMER_KEYS_RENAME_DICT.items():
new_key = new_key.replace(old, new)
converted_state_dict[new_key] = value

transformer.load_state_dict(converted_state_dict, strict=True, assign=True)
return transformer


def main(args):
checkpoint_path = get_checkpoint_path(args)
original_config, original_state_dict = load_original_checkpoint(checkpoint_path)
transformer = convert_bria_fibo2_transformer_to_diffusers(original_config, original_state_dict)

if not args.full_pipe:
transformer.save_pretrained(os.path.join(args.output_path, "transformer"))
return

vae = AutoencoderKLFlux2.from_pretrained(checkpoint_path, subfolder="vae")
# the original config carries `force_upcast`, which the Flux 2 VAE doesn't take; left in, every load warns about it
vae_config = dict(vae.config)
vae_config.pop("force_upcast", None)
vae._internal_dict = FrozenDict(vae_config)
text_encoder = Qwen3VLForConditionalGeneration.from_pretrained(
checkpoint_path, subfolder="text_encoder", dtype=torch.bfloat16
)
# fibo-2's limits on an image's size in pixels, not Qwen3-VL's defaults; they decide how many tokens an image to
# edit becomes
processor = Qwen3VLProcessor.from_pretrained(checkpoint_path, subfolder="text_encoder")
processor.image_processor = AutoImageProcessor.from_pretrained(
checkpoint_path, subfolder="text_encoder", size={"shortest_edge": 4 * 28 * 28, "longest_edge": 2048 * 28 * 28}
)
scheduler = FlowMatchEulerDiscreteScheduler.from_pretrained(checkpoint_path, subfolder="scheduler")

pipe = BriaFibo2Pipeline(
transformer=transformer, scheduler=scheduler, vae=vae, text_encoder=text_encoder, processor=processor
)
pipe.save_pretrained(args.output_path)


if __name__ == "__main__":
main(args)
4 changes: 4 additions & 0 deletions src/diffusers/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -264,6 +264,7 @@
"AutoencoderTiny",
"AutoencoderVidTok",
"AutoModel",
"BriaFibo2Transformer2DModel",
"BriaFiboTransformer2DModel",
"BriaTransformer2DModel",
"CacheMixin",
Expand Down Expand Up @@ -626,6 +627,7 @@
"AuraFlowPipeline",
"BlipDiffusionControlNetPipeline",
"BlipDiffusionPipeline",
"BriaFibo2Pipeline",
"BriaFiboEditPipeline",
"BriaFiboPipeline",
"BriaPipeline",
Expand Down Expand Up @@ -1162,6 +1164,7 @@
AutoencoderTiny,
AutoencoderVidTok,
AutoModel,
BriaFibo2Transformer2DModel,
BriaFiboTransformer2DModel,
BriaTransformer2DModel,
CacheMixin,
Expand Down Expand Up @@ -1499,6 +1502,7 @@
AudioLDM2UNet2DConditionModel,
AudioLDMPipeline,
AuraFlowPipeline,
BriaFibo2Pipeline,
BriaFiboEditPipeline,
BriaFiboPipeline,
BriaPipeline,
Expand Down
2 changes: 2 additions & 0 deletions src/diffusers/models/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -113,6 +113,7 @@
_import_structure["transformers.transformer_anyflow_far"] = ["AnyFlowFARTransformer3DModel"]
_import_structure["transformers.transformer_bria"] = ["BriaTransformer2DModel"]
_import_structure["transformers.transformer_bria_fibo"] = ["BriaFiboTransformer2DModel"]
_import_structure["transformers.transformer_bria_fibo2"] = ["BriaFibo2Transformer2DModel"]
_import_structure["transformers.transformer_chroma"] = ["ChromaTransformer2DModel"]
_import_structure["transformers.transformer_chronoedit"] = ["ChronoEditTransformer3DModel"]
_import_structure["transformers.transformer_cogview3plus"] = ["CogView3PlusTransformer2DModel"]
Expand Down Expand Up @@ -254,6 +255,7 @@
AnyFlowFARTransformer3DModel,
AnyFlowTransformer3DModel,
AuraFlowTransformer2DModel,
BriaFibo2Transformer2DModel,
BriaFiboTransformer2DModel,
BriaTransformer2DModel,
ChromaTransformer2DModel,
Expand Down
1 change: 1 addition & 0 deletions src/diffusers/models/transformers/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@
from .transformer_anyflow_far import AnyFlowFARTransformer3DModel
from .transformer_bria import BriaTransformer2DModel
from .transformer_bria_fibo import BriaFiboTransformer2DModel
from .transformer_bria_fibo2 import BriaFibo2Transformer2DModel
from .transformer_chroma import ChromaTransformer2DModel
from .transformer_chronoedit import ChronoEditTransformer3DModel
from .transformer_cogview3plus import CogView3PlusTransformer2DModel
Expand Down
Loading
Loading