From 9a01c5b92ef10653051cfbbaf5c6450cacc3e726 Mon Sep 17 00:00:00 2001 From: Kailash Date: Sat, 29 Nov 2025 13:28:30 +0530 Subject: [PATCH] FLUX 2 API integeration & documentation update --- README.md | 143 ++- docs/docs/api-reference/flux2.md | 401 +++++++ docs/docs/api-reference/overview.md | 61 + docs/docs/getting-started/configuration.md | 11 + docs/docs/getting-started/installation.md | 6 +- docs/docs/getting-started/quickstart.md | 30 +- docs/docs/intro.md | 2 + image_gen.py | 356 +++++- tryon/api/__init__.py | 3 + tryon/api/flux2.py | 1204 ++++++++++++++++++++ 10 files changed, 2151 insertions(+), 66 deletions(-) create mode 100644 docs/docs/api-reference/flux2.md create mode 100644 tryon/api/flux2.py diff --git a/README.md b/README.md index 8719725..fac88f9 100644 --- a/README.md +++ b/README.md @@ -22,6 +22,9 @@ OpenTryOn is an open-source AI toolkit designed for fashion technology and virtu - **Image Generation**: - Nano Banana (Gemini 2.5 Flash Image) for fast, efficient image generation - Nano Banana Pro (Gemini 3 Pro Image Preview) for advanced 4K image generation with search grounding + - FLUX.2 [PRO] high-quality image generation with text-to-image, image editing, and multi-image composition + - FLUX.2 [FLEX] flexible image generation with advanced controls (guidance, steps, prompt upsampling) + - **Datasets Module**: - Fashion-MNIST dataset loader with automatic download - VITON-HD dataset loader with lazy loading via PyTorch DataLoader @@ -48,6 +51,7 @@ OpenTryOn is an open-source AI toolkit designed for fashion technology and virtu - [Virtual Try-On with Kling AI](#virtual-try-on-with-kling-ai) - [Virtual Try-On with Segmind](#virtual-try-on-with-segmind) - [Image Generation with Nano Banana](#image-generation-with-nano-banana) + - [Image Generation with FLUX.2](#image-generation-with-flux2) - [Preprocessing Functions](#preprocessing-functions) - [Demos](#demos) - [Project Structure](#project-structure) @@ -127,15 +131,21 @@ SEGMIND_API_KEY=your_segmind_api_key # Google Gemini Credentials (required for Nano Banana image generation) GEMINI_API_KEY=your_gemini_api_key + +# BFL API Credentials (required for FLUX.2 image generation) +BFL_API_KEY=your_bfl_api_key ``` **Notes**: - Download the U2Net checkpoint file from the [huggingface-cloth-segmentation repository](https://github.com/wildoctopus/huggingface-cloth-segmentation) - For Amazon Nova Canvas, ensure you have AWS credentials configured (via `.env` file or AWS CLI) and Nova Canvas enabled in your AWS Bedrock console - For Kling AI, obtain your API key and secret key from the [Kling AI Developer Portal](https://app.klingai.com/global/dev/document-api/apiReference/model/functionalityTry) + - For Segmind, obtain your API key from the [Segmind API Portal](https://www.segmind.com/models/try-on-diffusion/api) - For Nano Banana, obtain your API key from the [Google AI Studio](https://aistudio.google.com/app/apikey) +- For FLUX.2 models, obtain your API key from [BFL AI](https://docs.bfl.ai/) + ## šŸŽ® Quick Start ### Basic Preprocessing @@ -740,6 +750,130 @@ images[0].save("result.png") **Reference**: [Gemini Image Generation Documentation](https://ai.google.dev/gemini-api/docs/image-generation) +### Image Generation with FLUX.2 + +Generate high-quality images using FLUX.2 [PRO] and FLUX.2 [FLEX] models from BFL AI. These models support text-to-image generation, image editing, multi-image composition, and advanced controls. + +#### Prerequisites + +1. **BFL AI Account Setup**: + - Sign up for a BFL AI account at [BFL AI](https://docs.bfl.ai/) + - Obtain your API key from the BFL AI dashboard + - Configure credentials in your `.env` file (see Environment Variables section) + +2. **Model Selection**: + - **FLUX.2 [PRO]**: High-quality image generation with standard controls - ideal for most use cases + - **FLUX.2 [FLEX]**: Flexible generation with advanced controls (guidance scale, steps, prompt upsampling) - ideal for fine-tuned control + +#### Command Line Usage + +```bash +# Text-to-image with FLUX.2 PRO +python image_gen.py --provider flux2-pro --prompt "A professional fashion model wearing elegant evening wear" --width 1024 --height 1024 + +# Text-to-image with FLUX.2 FLEX (Advanced controls) +python image_gen.py --provider flux2-flex --prompt "A stylish fashion model wearing elegant evening wear" --width 1024 --height 1024 --guidance 7.5 --steps 50 + +# Image editing +python image_gen.py --provider flux2-pro --mode edit --image person.jpg --prompt "Change the outfit to casual streetwear" + +# Multi-image composition +python image_gen.py --provider flux2-pro --mode compose --images outfit1.jpg outfit2.jpg --prompt "Combine these clothing styles into a cohesive outfit" +``` + +#### Python API Usage + +**FLUX.2 [PRO]:** + +```python +from dotenv import load_dotenv +load_dotenv() + +from tryon.api import Flux2ProAdapter + +# Initialize adapter +adapter = Flux2ProAdapter() + +# Text-to-image generation +images = adapter.generate_text_to_image( + prompt="A professional fashion model wearing elegant evening wear on a runway", + width=1024, + height=1024, + seed=42 +) + +# Image editing +images = adapter.generate_image_edit( + prompt="Change the outfit to casual streetwear style", + input_image="model.jpg", + width=1024, + height=1024 +) + +# Multi-image composition +images = adapter.generate_multi_image( + prompt="Create a fashion catalog layout combining these clothing styles", + images=["outfit1.jpg", "outfit2.jpg", "accessories.jpg"], + width=1024, + height=1024 +) + +# Save results +images[0].save("result.png") +``` + +**FLUX.2 [FLEX]:** + +```python +from tryon.api import Flux2FlexAdapter + +# Initialize adapter +adapter = Flux2FlexAdapter() + +# Text-to-image with advanced controls +images = adapter.generate_text_to_image( + prompt="A stylish fashion model wearing elegant evening wear", + width=1024, + height=1024, + guidance=7.5, # Higher guidance = more adherence to prompt (1.5-10) + steps=50, # More steps = higher quality (default: 28) + prompt_upsampling=True, # Enhance prompt quality + seed=42 +) + +# Image editing with advanced controls +images = adapter.generate_image_edit( + prompt="Transform the outfit to match a vintage 1920s fashion style", + input_image="model.jpg", + width=1024, + height=1024, + guidance=8.0, + steps=50, + prompt_upsampling=True +) + +# Save results +images[0].save("result.png") +``` + +#### Supported Features + +- **Text-to-Image**: Generate images from text descriptions +- **Image Editing**: Edit images using text prompts (add, remove, modify elements) +- **Multi-Image Composition**: Combine up to 8 images with style transfer +- **Custom Dimensions**: Control width and height (minimum: 64 pixels) +- **Advanced Controls** (FLEX only): Guidance scale (1.5-10), steps (default: 28), prompt upsampling +- **Reproducibility**: Seed support for consistent results +- **Safety Controls**: Moderation tolerance (0-5, default: 2) +- **Output Formats**: JPEG or PNG + +#### Key Differences: PRO vs FLEX + +- **FLUX.2 [PRO]**: Simpler API, faster generation, good for most use cases +- **FLUX.2 [FLEX]**: Advanced controls (guidance, steps, prompt upsampling), more fine-tuned control over generation quality + +**Reference**: [FLUX.2 API Documentation](https://docs.bfl.ai/api-reference/models/generate-or-edit-an-image-with-flux2-[pro]) + ### Preprocessing Functions #### Segment Garment @@ -816,7 +950,10 @@ opentryon/ │ ā”œā”€ā”€ api/ # API adapters │ │ ā”œā”€ā”€ nova_canvas.py # Amazon Nova Canvas VTON adapter │ │ ā”œā”€ā”€ kling_ai.py # Kling AI VTON adapter -│ │ └── segmind.py # Segmind Try-On Diffusion adapter +│ │ ā”œā”€ā”€ segmind.py # Segmind Try-On Diffusion adapter +│ │ ā”œā”€ā”€ nano_banana/ # Nano Banana (Gemini) image generation adapters +│ │ │ └── adapter.py # NanoBananaAdapter and NanoBananaProAdapter +│ │ └── flux2.py # FLUX.2 [PRO] and [FLEX] image generation adapters │ ā”œā”€ā”€ datasets/ # Dataset loaders │ │ ā”œā”€ā”€ base.py # Base dataset interface │ │ ā”œā”€ā”€ fashion_mnist.py # Fashion-MNIST dataset @@ -842,7 +979,8 @@ opentryon/ │ └── outfit_generator/ # Outfit generator demo ā”œā”€ā”€ scripts/ # Installation scripts ā”œā”€ā”€ main.py # Main CLI entry point -ā”œā”€ā”€ vton.py # Virtual try-on CLI (Amazon Nova Canvas & Kling AI) +ā”œā”€ā”€ vton.py # Virtual try-on CLI (Amazon Nova Canvas, Kling AI, Segmind) +ā”œā”€ā”€ image_gen.py # Image generation CLI (Nano Banana, FLUX.2) ā”œā”€ā”€ run_demo.py # Demo launcher ā”œā”€ā”€ requirements.txt # Python dependencies └── environment.yml # Conda environment @@ -920,6 +1058,7 @@ See `requirements.txt` or `environment.yml` for the complete list of dependencie - **Kling AI**: [Kling AI API Documentation](https://app.klingai.com/global/dev/document-api/apiReference/model/functionalityTry) - **Segmind**: [Segmind Try-On Diffusion API](https://www.segmind.com/models/try-on-diffusion/api) - **Nano Banana**: [Gemini Image Generation Documentation](https://ai.google.dev/gemini-api/docs/image-generation) +- **FLUX.2**: [BFL AI Documentation](https://docs.bfl.ai/) - **Discord Community**: [Join our Discord](https://discord.gg/T5mPpZHxkY) - **Outfit Generator Model**: [FLUX.1-dev LoRA Outfit Generator](https://huggingface.co/tryonlabs/FLUX.1-dev-LoRA-Outfit-Generator) diff --git a/docs/docs/api-reference/flux2.md b/docs/docs/api-reference/flux2.md new file mode 100644 index 0000000..734afb7 --- /dev/null +++ b/docs/docs/api-reference/flux2.md @@ -0,0 +1,401 @@ +--- +sidebar_position: 5 +title: FLUX.2 Image Generation +description: Generate high-quality images using FLUX.2 [PRO] and FLUX.2 [FLEX] models with text-to-image, image editing, and multi-image composition capabilities. +keywords: + - FLUX.2 + - FLUX.2 PRO + - FLUX.2 FLEX + - image generation + - text-to-image + - image editing + - BFL AI + - image composition +--- + +# FLUX.2 Image Generation + +FLUX.2 models provide high-quality image generation capabilities through the BFL AI API. This module includes adapters for both FLUX.2 [PRO] and FLUX.2 [FLEX] models, supporting text-to-image generation, image editing, and multi-image composition. + +## Overview + +- **FLUX.2 [PRO]**: High-quality image generation with standard controls +- **FLUX.2 [FLEX]**: Flexible generation with advanced controls (guidance scale, steps, prompt upsampling) + +Both models support: +- Text-to-image generation +- Image editing (prompt + input image) +- Multi-image composition (up to 8 reference images) +- Custom width/height control +- Seed for reproducibility +- Safety tolerance control + +## Prerequisites + +1. **BFL AI Account**: Sign up at [BFL AI](https://docs.bfl.ai/) +2. **API Key**: Obtain your API key from the BFL AI dashboard +3. **Environment Variable**: Set `BFL_API_KEY` in your `.env` file + +```bash +BFL_API_KEY=your_bfl_api_key +``` + +## Installation + +The FLUX.2 adapters are included in the `tryon.api` module. No additional installation is required beyond the base dependencies. + +## Quick Start + +### FLUX.2 [PRO] - Text-to-Image + +```python +from tryon.api import Flux2ProAdapter + +# Initialize adapter +adapter = Flux2ProAdapter() + +# Generate image from text prompt +images = adapter.generate_text_to_image( + prompt="A professional fashion model wearing elegant evening wear on a runway", + width=1024, + height=1024, + seed=42 +) + +# Save result +images[0].save("outputs/flux2_pro_result.png") +``` + +### FLUX.2 [FLEX] - Advanced Generation + +```python +from tryon.api import Flux2FlexAdapter + +# Initialize adapter +adapter = Flux2FlexAdapter() + +# Generate with advanced controls +images = adapter.generate_text_to_image( + prompt="A stylish fashion model wearing elegant evening wear", + width=1024, + height=1024, + guidance=7.5, # Higher guidance = more adherence to prompt + steps=50, # More steps = higher quality + prompt_upsampling=True, + seed=42 +) + +images[0].save("outputs/flux2_flex_result.png") +``` + +## API Reference + +### Flux2ProAdapter + +#### `__init__(api_key=None, base_url=None)` + +Initialize the FLUX.2 [PRO] adapter. + +**Parameters:** +- `api_key` (str, optional): BFL API key. Defaults to `BFL_API_KEY` environment variable. +- `base_url` (str, optional): Base URL for BFL API. Defaults to `https://api.bfl.ai`. + +**Raises:** +- `ValueError`: If API key is not provided. + +**Example:** +```python +# Using environment variable +adapter = Flux2ProAdapter() + +# Using parameter +adapter = Flux2ProAdapter(api_key="your_api_key") +``` + +#### `generate_text_to_image(prompt, width=None, height=None, seed=None, safety_tolerance=2, output_format="png", **kwargs)` + +Generate image from text prompt. + +**Parameters:** +- `prompt` (str, required): Text description of the image to generate +- `width` (int, optional): Width of output image (minimum: 64) +- `height` (int, optional): Height of output image (minimum: 64) +- `seed` (int, optional): Seed for reproducibility +- `safety_tolerance` (int): Moderation tolerance (0-5). 0 = most strict, 5 = least strict. Default: 2 +- `output_format` (str): Output format. Options: "jpeg", "png". Default: "png" +- `**kwargs`: Additional parameters (webhook_url, webhook_secret, etc.) + +**Returns:** +- `List[Image.Image]`: List of PIL Image objects + +**Example:** +```python +images = adapter.generate_text_to_image( + prompt="A professional fashion model wearing elegant evening wear", + width=1024, + height=1024, + seed=42 +) +``` + +#### `generate_image_edit(prompt, input_image, width=None, height=None, seed=None, safety_tolerance=2, output_format="png", **kwargs)` + +Generate edited image from prompt and input image. + +**Parameters:** +- `prompt` (str, required): Text description of how to edit the image +- `input_image` (Union[str, io.BytesIO, Image.Image], required): Input image (file path, URL, PIL Image, file-like object, or base64) +- `width` (int, optional): Width of output image (minimum: 64) +- `height` (int, optional): Height of output image (minimum: 64) +- `seed` (int, optional): Seed for reproducibility +- `safety_tolerance` (int): Moderation tolerance (0-5). Default: 2 +- `output_format` (str): Output format. Options: "jpeg", "png". Default: "png" +- `**kwargs`: Additional parameters + +**Returns:** +- `List[Image.Image]`: List of PIL Image objects + +**Example:** +```python +images = adapter.generate_image_edit( + prompt="Change the outfit to casual streetwear style", + input_image="model.jpg", + width=1024, + height=1024 +) +``` + +#### `generate_multi_image(prompt, images, width=None, height=None, seed=None, safety_tolerance=2, output_format="png", **kwargs)` + +Generate image from multiple input images (composition & style transfer). + +**Parameters:** +- `prompt` (str, required): Text description of how to combine/transform the images +- `images` (List[Union[str, io.BytesIO, Image.Image]], required): List of input images (file paths, URLs, PIL Images, file-like objects, or base64). Maximum 8 images supported. +- `width` (int, optional): Width of output image (minimum: 64) +- `height` (int, optional): Height of output image (minimum: 64) +- `seed` (int, optional): Seed for reproducibility +- `safety_tolerance` (int): Moderation tolerance (0-5). Default: 2 +- `output_format` (str): Output format. Options: "jpeg", "png". Default: "png" +- `**kwargs`: Additional parameters + +**Returns:** +- `List[Image.Image]`: List of PIL Image objects + +**Raises:** +- `ValueError`: If more than 8 images are provided. + +**Example:** +```python +images = adapter.generate_multi_image( + prompt="Create a fashion catalog layout combining these clothing styles", + images=["outfit1.jpg", "outfit2.jpg", "accessories.jpg"], + width=1024, + height=1024 +) +``` + +### Flux2FlexAdapter + +#### `__init__(api_key=None, base_url=None)` + +Initialize the FLUX.2 [FLEX] adapter. + +**Parameters:** +- `api_key` (str, optional): BFL API key. Defaults to `BFL_API_KEY` environment variable. +- `base_url` (str, optional): Base URL for BFL API. Defaults to `https://api.bfl.ai`. + +**Raises:** +- `ValueError`: If API key is not provided. + +#### `generate_text_to_image(prompt, width=None, height=None, seed=None, guidance=3.5, steps=28, prompt_upsampling=True, safety_tolerance=2, output_format="png", **kwargs)` + +Generate image from text prompt with advanced controls. + +**Parameters:** +- `prompt` (str, required): Text description of the image to generate +- `width` (int, optional): Width of output image (minimum: 64) +- `height` (int, optional): Height of output image (minimum: 64) +- `seed` (int, optional): Seed for reproducibility +- `guidance` (float): Guidance scale (1.5-10). Higher values = more adherence to prompt. Default: 3.5 +- `steps` (int): Number of generation steps. More steps = higher quality. Default: 28 +- `prompt_upsampling` (bool): Whether to use prompt upsampling. Default: True +- `safety_tolerance` (int): Moderation tolerance (0-5). Default: 2 +- `output_format` (str): Output format. Options: "jpeg", "png". Default: "png" +- `**kwargs`: Additional parameters (webhook_url, webhook_secret, input_image_blob_path, etc.) + +**Returns:** +- `List[Image.Image]`: List of PIL Image objects + +**Raises:** +- `ValueError`: If guidance is not between 1.5 and 10. + +**Example:** +```python +images = adapter.generate_text_to_image( + prompt="A professional fashion model wearing elegant evening wear", + width=1024, + height=1024, + guidance=7.5, + steps=50, + prompt_upsampling=True, + seed=42 +) +``` + +#### `generate_image_edit(prompt, input_image, width=None, height=None, seed=None, guidance=3.5, steps=28, prompt_upsampling=True, safety_tolerance=2, output_format="png", **kwargs)` + +Generate edited image from prompt and input image with advanced controls. + +**Parameters:** +- `prompt` (str, required): Text description of how to edit the image +- `input_image` (Union[str, io.BytesIO, Image.Image], required): Input image +- `width` (int, optional): Width of output image (minimum: 64) +- `height` (int, optional): Height of output image (minimum: 64) +- `seed` (int, optional): Seed for reproducibility +- `guidance` (float): Guidance scale (1.5-10). Default: 3.5 +- `steps` (int): Number of generation steps. Default: 28 +- `prompt_upsampling` (bool): Whether to use prompt upsampling. Default: True +- `safety_tolerance` (int): Moderation tolerance (0-5). Default: 2 +- `output_format` (str): Output format. Options: "jpeg", "png". Default: "png" +- `**kwargs`: Additional parameters + +**Returns:** +- `List[Image.Image]`: List of PIL Image objects + +#### `generate_multi_image(prompt, images, width=None, height=None, seed=None, guidance=3.5, steps=28, prompt_upsampling=True, safety_tolerance=2, output_format="png", **kwargs)` + +Generate image from multiple input images with advanced controls. + +**Parameters:** +- `prompt` (str, required): Text description of how to combine/transform the images +- `images` (List[Union[str, io.BytesIO, Image.Image]], required): List of input images (maximum 8) +- `width` (int, optional): Width of output image (minimum: 64) +- `height` (int, optional): Height of output image (minimum: 64) +- `seed` (int, optional): Seed for reproducibility +- `guidance` (float): Guidance scale (1.5-10). Default: 3.5 +- `steps` (int): Number of generation steps. Default: 28 +- `prompt_upsampling` (bool): Whether to use prompt upsampling. Default: True +- `safety_tolerance` (int): Moderation tolerance (0-5). Default: 2 +- `output_format` (str): Output format. Options: "jpeg", "png". Default: "png" +- `**kwargs`: Additional parameters + +**Returns:** +- `List[Image.Image]`: List of PIL Image objects + +## Supported Input Formats + +All image input parameters accept: +- **File paths** (str): Path to local image file +- **URLs** (str): HTTP/HTTPS URL to image +- **Base64 strings** (str): Base64-encoded image data +- **File-like objects** (io.BytesIO): BytesIO or similar object +- **PIL Images** (Image.Image): PIL Image object + +## Async Processing + +FLUX.2 API processes requests asynchronously. The adapters automatically: +1. Submit the request and receive a `task_id` and `polling_url` +2. Poll the task status endpoint until completion +3. Return PIL Image objects when the task succeeds +4. Raise errors if the task fails or times out (default timeout: 5 minutes) + +## Examples + +### Complete Workflow + +```python +from tryon.api import Flux2ProAdapter, Flux2FlexAdapter + +# FLUX.2 [PRO] - Simple text-to-image +pro_adapter = Flux2ProAdapter() +pro_images = pro_adapter.generate_text_to_image( + prompt="A professional fashion model wearing elegant evening wear", + width=1024, + height=1024 +) +pro_images[0].save("outputs/pro_result.png") + +# FLUX.2 [FLEX] - Advanced generation +flex_adapter = Flux2FlexAdapter() +flex_images = flex_adapter.generate_text_to_image( + prompt="A stylish fashion model wearing elegant evening wear", + width=1024, + height=1024, + guidance=8.0, + steps=50, + prompt_upsampling=True +) +flex_images[0].save("outputs/flex_result.png") + +# Multi-image composition +composition_images = pro_adapter.generate_multi_image( + prompt="Combine these clothing items into a cohesive fashion outfit", + images=["top.jpg", "pants.jpg", "jacket.jpg", "shoes.jpg"], + width=1024, + height=1024 +) +composition_images[0].save("outputs/composition_result.png") +``` + +### Image Editing + +```python +from tryon.api import Flux2FlexAdapter + +adapter = Flux2FlexAdapter() + +# Edit image with advanced controls +images = adapter.generate_image_edit( + prompt="Transform the outfit to match a vintage 1920s fashion style", + input_image="model.jpg", + guidance=8.0, + steps=50, + prompt_upsampling=True, + seed=42 +) + +images[0].save("outputs/edited_result.png") +``` + +## Error Handling + +The adapters raise `ValueError` for: +- Missing API key +- Invalid parameters (e.g., guidance out of range) +- API errors (HTTP errors, task failures) +- Timeout errors (task takes too long) +- Invalid image formats + +**Example:** +```python +try: + images = adapter.generate_text_to_image( + prompt="A fashion model", + guidance=15 # Invalid: must be between 1.5 and 10 + ) +except ValueError as e: + print(f"Error: {e}") +``` + +## Best Practices + +1. **Prompt Quality**: Write detailed, specific prompts for better results +2. **Guidance Scale**: Start with default (3.5) and adjust based on results + - Lower (1.5-3): More creative, less strict to prompt + - Higher (7-10): More strict adherence to prompt +3. **Steps**: More steps = higher quality but slower generation + - Default (28): Good balance + - High quality (50+): Slower but better results +4. **Seed**: Use seeds for reproducible results +5. **Safety Tolerance**: Adjust based on your content moderation needs +6. **Multi-Image**: Use up to 8 reference images for best composition results + +## References + +- [FLUX.2 [PRO] API Documentation](https://docs.bfl.ai/api-reference/models/generate-or-edit-an-image-with-flux2-[pro]) +- [FLUX.2 [FLEX] API Documentation](https://docs.bfl.ai/api-reference/models/generate-or-edit-an-image-with-flux2-[flex]) +- [FLUX.2 Overview](https://docs.bfl.ai/flux_2/flux2_overview) +- [FLUX.2 Text-to-Image Guide](https://docs.bfl.ai/flux_2/flux2_text_to_image) +- [FLUX.2 Image Editing Guide](https://docs.bfl.ai/flux_2/flux2_image_editing) + diff --git a/docs/docs/api-reference/overview.md b/docs/docs/api-reference/overview.md index 7103d15..7d18233 100644 --- a/docs/docs/api-reference/overview.md +++ b/docs/docs/api-reference/overview.md @@ -280,6 +280,67 @@ See [Nano Banana API Documentation](nano-banana) for complete details. --- +### `Flux2ProAdapter` + +Adapter for FLUX.2 [PRO] - high-quality image generation with standard controls. + +```python +from tryon.api import Flux2ProAdapter + +adapter = Flux2ProAdapter(api_key="your_api_key") + +images = adapter.generate_text_to_image( + prompt="A professional fashion model wearing elegant evening wear", + width=1024, + height=1024, + seed=42 +) +``` + +**Parameters:** +- `api_key` (str, optional): BFL API key. Defaults to `BFL_API_KEY` environment variable + +**Methods:** +- `generate_text_to_image(prompt, width, height, seed, safety_tolerance, output_format, ...)` - Generate images from text +- `generate_image_edit(prompt, input_image, width, height, seed, ...)` - Edit images with text prompts +- `generate_multi_image(prompt, images, width, height, seed, ...)` - Compose multiple images (up to 8) + +See [FLUX.2 API Documentation](flux2) for complete details. + +--- + +### `Flux2FlexAdapter` + +Adapter for FLUX.2 [FLEX] - flexible image generation with advanced controls. + +```python +from tryon.api import Flux2FlexAdapter + +adapter = Flux2FlexAdapter(api_key="your_api_key") + +images = adapter.generate_text_to_image( + prompt="A stylish fashion model wearing elegant evening wear", + width=1024, + height=1024, + guidance=7.5, # Higher = more adherence to prompt (1.5-10) + steps=50, # More steps = higher quality + prompt_upsampling=True, + seed=42 +) +``` + +**Parameters:** +- `api_key` (str, optional): BFL API key. Defaults to `BFL_API_KEY` environment variable + +**Methods:** +- `generate_text_to_image(prompt, width, height, seed, guidance, steps, prompt_upsampling, ...)` - Generate images from text with advanced controls +- `generate_image_edit(prompt, input_image, width, height, seed, guidance, steps, ...)` - Edit images with advanced controls +- `generate_multi_image(prompt, images, width, height, seed, guidance, steps, ...)` - Compose multiple images with advanced controls + +See [FLUX.2 API Documentation](flux2) for complete details. + +--- + ## Utility Functions ### `convert_to_jpg` diff --git a/docs/docs/getting-started/configuration.md b/docs/docs/getting-started/configuration.md index 6cd018d..0b12ad8 100644 --- a/docs/docs/getting-started/configuration.md +++ b/docs/docs/getting-started/configuration.md @@ -39,6 +39,9 @@ AMAZON_NOVA_MODEL_ID=amazon.nova-canvas-v1:0 # Optional # Google Gemini (Nano Banana Image Generation) GEMINI_API_KEY=your_gemini_api_key + +# BFL AI (FLUX.2 Image Generation) +BFL_API_KEY=your_bfl_api_key ``` ### Datasets (Optional - Only if using HuggingFace datasets) @@ -103,6 +106,12 @@ from tryon.datasets import FashionMNIST 2. Obtain API key from [API Keys page](https://aistudio.google.com/app/apikey) 3. Add to `.env`: `GEMINI_API_KEY=your_key` +### BFL AI (FLUX.2) + +1. Sign up at [BFL AI](https://docs.bfl.ai/) +2. Obtain your API key from the BFL AI dashboard +3. Add to `.env`: `BFL_API_KEY=your_key` + ## Configuration Options ### GPU Configuration @@ -182,6 +191,7 @@ U2NET_SEGM_CHECKPOINT_PATH=./models/u2net.pth ```env SEGMIND_API_KEY=your_segmind_key GEMINI_API_KEY=your_gemini_key +BFL_API_KEY=your_bfl_key ``` ### Full Setup (Preprocessing + APIs + Datasets) @@ -196,6 +206,7 @@ SEGMIND_API_KEY=your_segmind_key KLING_AI_API_KEY=your_kling_key KLING_AI_SECRET_KEY=your_kling_secret GEMINI_API_KEY=your_gemini_key +BFL_API_KEY=your_bfl_key AWS_ACCESS_KEY_ID=your_aws_key AWS_SECRET_ACCESS_KEY=your_aws_secret AMAZON_NOVA_REGION=us-east-1 diff --git a/docs/docs/getting-started/installation.md b/docs/docs/getting-started/installation.md index 45e2267..5bda22a 100644 --- a/docs/docs/getting-started/installation.md +++ b/docs/docs/getting-started/installation.md @@ -110,6 +110,9 @@ AMAZON_NOVA_REGION=us-east-1 # Google Gemini (Nano Banana Image Generation) GEMINI_API_KEY=your_gemini_api_key + +# BFL AI (FLUX.2 Image Generation) +BFL_API_KEY=your_bfl_api_key ``` **Note**: You only need to configure the APIs you plan to use. For preprocessing-only workflows, only the U2Net checkpoints are required. @@ -212,7 +215,8 @@ Once installed: - Segmind Try-On Diffusion - Kling AI Virtual Try-On - Amazon Nova Canvas - - Nano Banana Image Generation + - Nano Banana Image Generation (Google Gemini) + - FLUX.2 Image Generation (BFL AI) - **šŸ“Š Datasets**: Load and work with fashion datasets - Fashion-MNIST (60K examples) diff --git a/docs/docs/getting-started/quickstart.md b/docs/docs/getting-started/quickstart.md index dfded3b..c051a29 100644 --- a/docs/docs/getting-started/quickstart.md +++ b/docs/docs/getting-started/quickstart.md @@ -56,7 +56,9 @@ images = nova_adapter.generate_and_decode( ### 2. Image Generation APIs -Generate images using Google's Nano Banana models: +Generate images using Google's Nano Banana models or FLUX.2 models: + +**Nano Banana (Google Gemini):** ```python from tryon.api.nano_banana import NanoBananaAdapter, NanoBananaProAdapter @@ -77,6 +79,32 @@ images = pro_adapter.generate_text_to_image( ) ``` +**FLUX.2 (BFL AI):** + +```python +from tryon.api import Flux2ProAdapter, Flux2FlexAdapter + +# FLUX.2 PRO (High-quality) +pro_adapter = Flux2ProAdapter() +images = pro_adapter.generate_text_to_image( + prompt="A professional fashion model wearing elegant evening wear", + width=1024, + height=1024, + seed=42 +) + +# FLUX.2 FLEX (Advanced controls) +flex_adapter = Flux2FlexAdapter() +images = flex_adapter.generate_text_to_image( + prompt="A stylish fashion model wearing elegant evening wear", + width=1024, + height=1024, + guidance=7.5, # Higher = more adherence to prompt + steps=50, # More steps = higher quality + prompt_upsampling=True +) +``` + ### 3. Datasets Load and work with fashion datasets: diff --git a/docs/docs/intro.md b/docs/docs/intro.md index 79448e1..98a6a0c 100644 --- a/docs/docs/intro.md +++ b/docs/docs/intro.md @@ -62,6 +62,8 @@ Cloud-based virtual try-on and image generation APIs: - **Amazon Nova Canvas**: AWS-based virtual try-on service - **Nano Banana**: Google Gemini image generation (fast, efficient, 1024px) - **Nano Banana Pro**: Google Gemini advanced image generation (up to 4K, search grounding) +- **FLUX.2 [PRO]**: High-quality image generation with text-to-image, image editing, and multi-image composition +- **FLUX.2 [FLEX]**: Flexible image generation with advanced controls (guidance scale, steps, prompt upsampling) ### Garment Preprocessing - **Garment Segmentation**: U2Net-based segmentation for upper, lower, and dress categories diff --git a/image_gen.py b/image_gen.py index 954499e..80162fa 100644 --- a/image_gen.py +++ b/image_gen.py @@ -5,29 +5,38 @@ import argparse from pathlib import Path from tryon.api.nano_banana import NanoBananaAdapter, NanoBananaProAdapter +from tryon.api.flux2 import Flux2ProAdapter, Flux2FlexAdapter def main(): parser = argparse.ArgumentParser( - description="Generate images using Google's Nano Banana (Gemini Image Generation) models", + description="Generate images using Nano Banana (Gemini) or FLUX.2 image generation models", formatter_class=argparse.RawDescriptionHelpFormatter, epilog=""" Examples: - # Text-to-image with Nano Banana (Fast) + # Nano Banana (Fast) python image_gen.py --provider nano-banana --prompt "A stylish fashion model wearing a modern casual outfit in a studio setting" - # Text-to-image with Nano Banana Pro (4K) + # Nano Banana Pro (4K) python image_gen.py --provider nano-banana-pro --prompt "Professional fashion photography of elegant evening wear on a runway" --resolution 4K + # FLUX.2 PRO + python image_gen.py --provider flux2-pro --prompt "A professional fashion model wearing elegant evening wear" --width 1024 --height 1024 + + # FLUX.2 FLEX (Advanced controls) + python image_gen.py --provider flux2-flex --prompt "A stylish fashion model wearing elegant evening wear" --width 1024 --height 1024 --guidance 7.5 --steps 50 + # Image editing python image_gen.py --provider nano-banana --mode edit --image person.jpg --prompt "Change the outfit to a formal business suit" + python image_gen.py --provider flux2-pro --mode edit --image person.jpg --prompt "Change the outfit to casual streetwear" # Multi-image composition python image_gen.py --provider nano-banana --mode compose --images outfit1.jpg outfit2.jpg --prompt "Create a fashion catalog layout combining these clothing styles" + python image_gen.py --provider flux2-pro --mode compose --images outfit1.jpg outfit2.jpg --prompt "Combine these clothing styles into a cohesive outfit" # Batch generation from file python image_gen.py --provider nano-banana --batch prompts.txt --output-dir results/ - # Specify aspect ratio + # Specify aspect ratio (Nano Banana only) python image_gen.py --provider nano-banana --prompt "A fashion model showcasing seasonal clothing collection" --aspect-ratio "16:9" # Use search grounding (Nano Banana Pro only) @@ -40,8 +49,8 @@ def main(): '--provider', type=str, required=True, - choices=['nano-banana', 'nano-banana-pro'], - help='Image generation provider. Options: nano-banana (Gemini 2.5 Flash Image), nano-banana-pro (Gemini 3 Pro Image Preview)' + choices=['nano-banana', 'nano-banana-pro', 'flux2-pro', 'flux2-flex'], + help='Image generation provider. Options: nano-banana (Gemini 2.5 Flash Image), nano-banana-pro (Gemini 3 Pro Image Preview), flux2-pro (FLUX.2 PRO), flux2-flex (FLUX.2 FLEX)' ) # Mode selection @@ -94,13 +103,28 @@ def main(): help='Resolution level (Nano Banana Pro only). Options: 1K, 2K, 4K. Default: 1K' ) - # Aspect ratio + # Width/Height (FLUX.2 only) + parser.add_argument( + '--width', + type=int, + default=None, + help='Image width in pixels (FLUX.2 only, minimum: 64)' + ) + + parser.add_argument( + '--height', + type=int, + default=None, + help='Image height in pixels (FLUX.2 only, minimum: 64)' + ) + + # Aspect ratio (Nano Banana only) parser.add_argument( '--aspect-ratio', type=str, default=None, choices=['1:1', '2:3', '3:2', '3:4', '4:3', '4:5', '5:4', '9:16', '16:9', '21:9'], - help='Aspect ratio for generated images. Options: 1:1, 2:3, 3:2, 3:4, 4:3, 4:5, 5:4, 9:16, 16:9, 21:9' + help='Aspect ratio for generated images (Nano Banana only). Options: 1:1, 2:3, 3:2, 3:4, 4:3, 4:5, 5:4, 9:16, 16:9, 21:9' ) # Search grounding (Nano Banana Pro only) @@ -110,6 +134,44 @@ def main(): help='Use Google Search for real-world grounding (Nano Banana Pro only)' ) + # FLUX.2 FLEX specific parameters + parser.add_argument( + '--guidance', + type=float, + default=3.5, + help='Guidance scale for FLUX.2 FLEX (1.5-10). Higher = more adherence to prompt. Default: 3.5' + ) + + parser.add_argument( + '--steps', + type=int, + default=28, + help='Number of generation steps for FLUX.2 FLEX. More steps = higher quality. Default: 28' + ) + + parser.add_argument( + '--seed', + type=int, + default=None, + help='Seed for reproducible results (FLUX.2 only)' + ) + + parser.add_argument( + '--safety-tolerance', + type=int, + default=2, + choices=[0, 1, 2, 3, 4, 5], + help='Safety tolerance level (FLUX.2 only, 0-5). 0 = most strict, 5 = least strict. Default: 2' + ) + + parser.add_argument( + '--output-format', + type=str, + default='png', + choices=['jpeg', 'png'], + help='Output format (FLUX.2 only). Options: jpeg, png. Default: png' + ) + # Output directory parser.add_argument( '-o', '--output-dir', @@ -141,70 +203,143 @@ def main(): if not os.path.exists(img_path): raise FileNotFoundError(f"Image not found: {img_path}") - # Validate resolution for Nano Banana (not supported) - if args.provider == 'nano-banana' and args.resolution != '1K': - print("Warning: Resolution option is only available for Nano Banana Pro. Using default resolution.") - args.resolution = '1K' + # Validate provider-specific arguments + if args.provider in ['nano-banana', 'nano-banana-pro']: + # Validate resolution for Nano Banana (not supported) + if args.provider == 'nano-banana' and args.resolution != '1K': + print("Warning: Resolution option is only available for Nano Banana Pro. Using default resolution.") + args.resolution = '1K' + + # Validate search grounding for Nano Banana (not supported) + if args.provider == 'nano-banana' and args.use_search_grounding: + print("Warning: Search grounding is only available for Nano Banana Pro. Ignoring --use-search-grounding.") + args.use_search_grounding = False + + # Check for Gemini API key + api_key = os.getenv("GEMINI_API_KEY") + if not api_key: + print("\nāœ— Error: GEMINI_API_KEY environment variable is required for Nano Banana providers.") + print(" Please set it in your .env file or environment.") + print(" Get your API key from: https://aistudio.google.com/app/apikey") + return 1 - # Validate search grounding for Nano Banana (not supported) - if args.provider == 'nano-banana' and args.use_search_grounding: - print("Warning: Search grounding is only available for Nano Banana Pro. Ignoring --use-search-grounding.") - args.use_search_grounding = False + elif args.provider in ['flux2-pro', 'flux2-flex']: + # Validate FLUX.2 specific arguments + if args.width is not None and args.width < 64: + raise ValueError(f"Width must be at least 64 pixels, got {args.width}") + if args.height is not None and args.height < 64: + raise ValueError(f"Height must be at least 64 pixels, got {args.height}") + + if args.provider == 'flux2-flex': + if args.guidance < 1.5 or args.guidance > 10: + raise ValueError(f"Guidance must be between 1.5 and 10, got {args.guidance}") + + # Check for BFL API key + api_key = os.getenv("BFL_API_KEY") + if not api_key: + print("\nāœ— Error: BFL_API_KEY environment variable is required for FLUX.2 providers.") + print(" Please set it in your .env file or environment.") + print(" Get your API key from: https://docs.bfl.ai/") + return 1 # Create output directory output_dir = Path(args.output_dir) output_dir.mkdir(parents=True, exist_ok=True) - # Check for API key - api_key = os.getenv("GEMINI_API_KEY") - if not api_key: - print("\nāœ— Error: GEMINI_API_KEY environment variable is required.") - print(" Please set it in your .env file or environment.") - print(" Get your API key from: https://aistudio.google.com/app/apikey") - return 1 - # Initialize adapter if args.provider == 'nano-banana': print("Initializing Nano Banana (Gemini 2.5 Flash Image) adapter...") adapter = NanoBananaAdapter(api_key=api_key) - else: + elif args.provider == 'nano-banana-pro': print(f"Initializing Nano Banana Pro (Gemini 3 Pro Image Preview) adapter...") print(f" Resolution: {args.resolution}") if args.use_search_grounding: print(" Search grounding: Enabled") adapter = NanoBananaProAdapter(api_key=api_key) + elif args.provider == 'flux2-pro': + print("Initializing FLUX.2 [PRO] adapter...") + if args.width: + print(f" Width: {args.width}") + if args.height: + print(f" Height: {args.height}") + adapter = Flux2ProAdapter(api_key=api_key) + elif args.provider == 'flux2-flex': + print("Initializing FLUX.2 [FLEX] adapter...") + if args.width: + print(f" Width: {args.width}") + if args.height: + print(f" Height: {args.height}") + print(f" Guidance: {args.guidance}") + print(f" Steps: {args.steps}") + adapter = Flux2FlexAdapter(api_key=api_key) # try: - # Handle batch generation + # Handle batch generation (Nano Banana only) if args.batch: - print(f"Batch generation mode: Reading prompts from {args.batch}") - with open(args.batch, 'r') as f: - prompts = [line.strip() for line in f if line.strip()] - - print(f"Found {len(prompts)} prompts") - - if args.provider == 'nano-banana': - results = adapter.generate_batch( - prompts=prompts, - aspect_ratio=args.aspect_ratio - ) + if args.provider in ['flux2-pro', 'flux2-flex']: + print("Warning: Batch generation is not supported for FLUX.2 providers. Processing prompts individually...") + with open(args.batch, 'r') as f: + prompts = [line.strip() for line in f if line.strip()] + + for prompt_idx, prompt in enumerate(prompts): + print(f"\nProcessing prompt {prompt_idx + 1}/{len(prompts)}: {prompt}") + if args.provider == 'flux2-pro': + images = adapter.generate_text_to_image( + prompt=prompt, + width=args.width, + height=args.height, + seed=args.seed, + safety_tolerance=args.safety_tolerance, + output_format=args.output_format + ) + else: # flux2-flex + images = adapter.generate_text_to_image( + prompt=prompt, + width=args.width, + height=args.height, + seed=args.seed, + guidance=args.guidance, + steps=args.steps, + safety_tolerance=args.safety_tolerance, + output_format=args.output_format + ) + + for img_idx, image in enumerate(images): + output_path = output_dir / f"batch_{prompt_idx}_{img_idx}.png" + image.save(output_path) + print(f"āœ“ Saved: {output_path}") + + print(f"\nāœ“ Successfully generated images from {len(prompts)} prompt(s)") + return 0 else: - results = adapter.generate_batch( - prompts=prompts, - resolution=args.resolution, - aspect_ratio=args.aspect_ratio, - use_search_grounding=args.use_search_grounding - ) - - # Save batch results - for prompt_idx, images in enumerate(results): - for img_idx, image in enumerate(images): - output_path = output_dir / f"batch_{prompt_idx}_{img_idx}.png" - image.save(output_path) - print(f"āœ“ Saved: {output_path}") - - print(f"\nāœ“ Successfully generated {sum(len(r) for r in results)} image(s) from {len(prompts)} prompt(s)") - return 0 + print(f"Batch generation mode: Reading prompts from {args.batch}") + with open(args.batch, 'r') as f: + prompts = [line.strip() for line in f if line.strip()] + + print(f"Found {len(prompts)} prompts") + + if args.provider == 'nano-banana': + results = adapter.generate_batch( + prompts=prompts, + aspect_ratio=args.aspect_ratio + ) + else: # nano-banana-pro + results = adapter.generate_batch( + prompts=prompts, + resolution=args.resolution, + aspect_ratio=args.aspect_ratio, + use_search_grounding=args.use_search_grounding + ) + + # Save batch results + for prompt_idx, images in enumerate(results): + for img_idx, image in enumerate(images): + output_path = output_dir / f"batch_{prompt_idx}_{img_idx}.png" + image.save(output_path) + print(f"āœ“ Saved: {output_path}") + + print(f"\nāœ“ Successfully generated {sum(len(r) for r in results)} image(s) from {len(prompts)} prompt(s)") + return 0 # Handle single generation images = [] @@ -212,63 +347,160 @@ def main(): if args.mode == 'text': print(f"Generating image from text prompt...") print(f" Prompt: {args.prompt}") - if args.aspect_ratio: - print(f" Aspect ratio: {args.aspect_ratio}") if args.provider == 'nano-banana': + if args.aspect_ratio: + print(f" Aspect ratio: {args.aspect_ratio}") images = adapter.generate_text_to_image( prompt=args.prompt, aspect_ratio=args.aspect_ratio ) - else: + elif args.provider == 'nano-banana-pro': + if args.aspect_ratio: + print(f" Aspect ratio: {args.aspect_ratio}") images = adapter.generate_text_to_image( prompt=args.prompt, resolution=args.resolution, aspect_ratio=args.aspect_ratio, use_search_grounding=args.use_search_grounding ) + elif args.provider == 'flux2-pro': + if args.width: + print(f" Width: {args.width}") + if args.height: + print(f" Height: {args.height}") + images = adapter.generate_text_to_image( + prompt=args.prompt, + width=args.width, + height=args.height, + seed=args.seed, + safety_tolerance=args.safety_tolerance, + output_format=args.output_format + ) + else: # flux2-flex + if args.width: + print(f" Width: {args.width}") + if args.height: + print(f" Height: {args.height}") + print(f" Guidance: {args.guidance}, Steps: {args.steps}") + images = adapter.generate_text_to_image( + prompt=args.prompt, + width=args.width, + height=args.height, + seed=args.seed, + guidance=args.guidance, + steps=args.steps, + safety_tolerance=args.safety_tolerance, + output_format=args.output_format + ) elif args.mode == 'edit': print(f"Editing image...") print(f" Image: {args.image}") print(f" Prompt: {args.prompt}") - if args.aspect_ratio: - print(f" Aspect ratio: {args.aspect_ratio}") if args.provider == 'nano-banana': + if args.aspect_ratio: + print(f" Aspect ratio: {args.aspect_ratio}") images = adapter.generate_image_edit( image=args.image, prompt=args.prompt, aspect_ratio=args.aspect_ratio ) - else: + elif args.provider == 'nano-banana-pro': + if args.aspect_ratio: + print(f" Aspect ratio: {args.aspect_ratio}") images = adapter.generate_image_edit( image=args.image, prompt=args.prompt, resolution=args.resolution, aspect_ratio=args.aspect_ratio ) + elif args.provider == 'flux2-pro': + if args.width: + print(f" Width: {args.width}") + if args.height: + print(f" Height: {args.height}") + images = adapter.generate_image_edit( + prompt=args.prompt, + input_image=args.image, + width=args.width, + height=args.height, + seed=args.seed, + safety_tolerance=args.safety_tolerance, + output_format=args.output_format + ) + else: # flux2-flex + if args.width: + print(f" Width: {args.width}") + if args.height: + print(f" Height: {args.height}") + print(f" Guidance: {args.guidance}, Steps: {args.steps}") + images = adapter.generate_image_edit( + prompt=args.prompt, + input_image=args.image, + width=args.width, + height=args.height, + seed=args.seed, + guidance=args.guidance, + steps=args.steps, + safety_tolerance=args.safety_tolerance, + output_format=args.output_format + ) elif args.mode == 'compose': print(f"Composing images...") print(f" Images: {', '.join(args.images)}") print(f" Prompt: {args.prompt}") - if args.aspect_ratio: - print(f" Aspect ratio: {args.aspect_ratio}") if args.provider == 'nano-banana': + if args.aspect_ratio: + print(f" Aspect ratio: {args.aspect_ratio}") images = adapter.generate_multi_image( images=args.images, prompt=args.prompt, aspect_ratio=args.aspect_ratio ) - else: + elif args.provider == 'nano-banana-pro': + if args.aspect_ratio: + print(f" Aspect ratio: {args.aspect_ratio}") images = adapter.generate_multi_image( images=args.images, prompt=args.prompt, resolution=args.resolution, aspect_ratio=args.aspect_ratio ) + elif args.provider == 'flux2-pro': + if args.width: + print(f" Width: {args.width}") + if args.height: + print(f" Height: {args.height}") + images = adapter.generate_multi_image( + prompt=args.prompt, + images=args.images, + width=args.width, + height=args.height, + seed=args.seed, + safety_tolerance=args.safety_tolerance, + output_format=args.output_format + ) + else: # flux2-flex + if args.width: + print(f" Width: {args.width}") + if args.height: + print(f" Height: {args.height}") + print(f" Guidance: {args.guidance}, Steps: {args.steps}") + images = adapter.generate_multi_image( + prompt=args.prompt, + images=args.images, + width=args.width, + height=args.height, + seed=args.seed, + guidance=args.guidance, + steps=args.steps, + safety_tolerance=args.safety_tolerance, + output_format=args.output_format + ) # Save images for idx, image in enumerate(images): diff --git a/tryon/api/__init__.py b/tryon/api/__init__.py index 86cf48b..10dfaf8 100644 --- a/tryon/api/__init__.py +++ b/tryon/api/__init__.py @@ -2,6 +2,7 @@ from .kling_ai import KlingAIVTONAdapter from .segmind import SegmindVTONAdapter from .nano_banana import NanoBananaAdapter, NanoBananaProAdapter +from .flux2 import Flux2ProAdapter, Flux2FlexAdapter __all__ = [ "AmazonNovaCanvasVTONAdapter", @@ -9,4 +10,6 @@ "SegmindVTONAdapter", "NanoBananaAdapter", "NanoBananaProAdapter", + "Flux2ProAdapter", + "Flux2FlexAdapter", ] \ No newline at end of file diff --git a/tryon/api/flux2.py b/tryon/api/flux2.py new file mode 100644 index 0000000..fb07f7c --- /dev/null +++ b/tryon/api/flux2.py @@ -0,0 +1,1204 @@ +import os +import base64 +import json +import io +import requests +import time +from typing import Optional, Union, List +from PIL import Image + + +class Flux2ProAdapter: + """ + Adapter for FLUX.2 [PRO] image generation API. + + FLUX.2 [PRO] is a high-quality image generation model that supports: + - Text-to-image generation + - Image editing (prompt + input image) + - Multi-image composition (up to 8 reference images) + - Custom width/height control + - Seed for reproducibility + - Safety tolerance control + + Reference: https://docs.bfl.ai/api-reference/models/generate-or-edit-an-image-with-flux2-[pro] + + API endpoint: POST https://api.bfl.ai/v1/flux-2-pro + + Authentication: + Requires API key provided via constructor parameter or environment variable: + - BFL_API_KEY: Your BFL API key + The API key is sent in the 'x-key' header. + + The API accepts base64-encoded images or URLs. This adapter automatically + handles file paths by converting them to base64. + + Example: + >>> import os + >>> os.environ['BFL_API_KEY'] = 'your_api_key' + >>> adapter = Flux2ProAdapter() + >>> images = adapter.generate_text_to_image( + ... prompt="A stylish fashion model wearing elegant evening wear" + ... ) + >>> images[0].save("result.png") + + >>> # Image editing + >>> images = adapter.generate_image_edit( + ... prompt="Change the outfit to casual streetwear", + ... input_image="model.jpg" + ... ) + + >>> # Multi-image composition + >>> images = adapter.generate_multi_image( + ... prompt="Combine these clothing styles into a cohesive outfit", + ... images=["outfit1.jpg", "outfit2.jpg", "accessories.jpg"] + ... ) + """ + + BASE_URL = "https://api.bfl.ai" + ENDPOINT = "/v1/flux-2-pro" + + def __init__(self, api_key: Optional[str] = None, base_url: Optional[str] = None): + """ + Initialize the FLUX.2 [PRO] client. + + Args: + api_key: BFL API key. Defaults to BFL_API_KEY environment variable. + If not provided via parameter or environment variable, raises ValueError. + base_url: Base URL for BFL API. Defaults to 'https://api.bfl.ai' if not set. + + Raises: + ValueError: If API key is not provided. + + Example: + >>> # Using environment variable + >>> import os + >>> os.environ['BFL_API_KEY'] = 'your_api_key' + >>> adapter = Flux2ProAdapter() + + >>> # Using parameter + >>> adapter = Flux2ProAdapter(api_key="your_api_key") + """ + self.api_key = api_key or os.getenv("BFL_API_KEY") + if not self.api_key: + raise ValueError( + "BFL API key is required. Set BFL_API_KEY environment variable " + "or pass api_key parameter." + ) + + self.base_url = base_url or self.BASE_URL + self.endpoint = f"{self.base_url}{self.ENDPOINT}" + + # Default headers + self.headers = { + "x-key": self.api_key, + "Content-Type": "application/json" + } + + def _prepare_image_input(self, image_input: Union[str, io.BytesIO, Image.Image]) -> str: + """ + Prepare image input for API request. + + Handles file paths, file-like objects, PIL Images, URLs, and base64 strings. + For local files and file-like objects, converts to base64. + URLs and base64 strings are passed as-is. + + Args: + image_input: Image input in one of the following formats: + - File path (str): Path to local image file + - URL (str): HTTP/HTTPS URL to image (starts with http:// or https://) + - Base64 string (str): Base64-encoded image data + - File-like object (io.BytesIO): BytesIO or similar object + - PIL Image (Image.Image): PIL Image object + + Returns: + str: URL string (if input is URL) or base64-encoded string (if input is file/base64) + """ + # Handle PIL Image + if isinstance(image_input, Image.Image): + buffer = io.BytesIO() + image_input.save(buffer, format='PNG') + buffer.seek(0) + return base64.b64encode(buffer.read()).decode('utf-8') + + # Handle file-like object + if hasattr(image_input, 'read'): + image_file = image_input + image_file.seek(0) + image_bytes = image_file.read() + image_file.seek(0) + return base64.b64encode(image_bytes).decode('utf-8') + + # Handle string (file path, URL, or base64) + if isinstance(image_input, str): + # Check if it's a URL + if image_input.startswith(("http://", "https://")): + return image_input + + # Check if it's already base64 + if len(image_input) > 100 and not os.path.exists(image_input): + # Likely base64 string + try: + # Try to decode to verify + base64.b64decode(image_input[:100]) + return image_input + except: + pass + + # It's a file path + with open(image_input, "rb") as image_file: + return base64.b64encode(image_file.read()).decode("utf-8") + + raise ValueError("Invalid image input: must be a file path, URL, file-like object, PIL Image, or base64 string") + + def _poll_task(self, task_id: str, polling_url: str, max_wait_time: int = 300) -> dict: + """ + Poll task status until completion. + + Args: + task_id: Task ID returned from API + polling_url: URL to poll for task status + max_wait_time: Maximum time to wait in seconds. Default: 300 (5 minutes) + + Returns: + dict: Task result containing image data + + Raises: + ValueError: If task fails or times out + """ + start_time = time.time() + + while True: + # Check timeout + elapsed_time = time.time() - start_time + if elapsed_time > max_wait_time: + raise ValueError( + f"Task {task_id} timed out after {max_wait_time} seconds." + ) + + # Poll task status + try: + response = requests.get( + polling_url, + headers={"x-key": self.api_key}, + timeout=30 + ) + response.raise_for_status() + task_data = response.json() + except requests.exceptions.RequestException as e: + raise ValueError(f"Failed to poll task status: {str(e)}") + + # Check task status (BFL API uses capitalized status values) + status_raw = task_data.get("status", "") + status = status_raw.lower() + + # BFL API uses "Ready" (capitalized) to indicate task completion + # Also check for common variations + if status_raw == "Ready" or status == "completed" or status == "succeed" or status == "ready": + # Task completed successfully + return task_data + elif status_raw == "Error" or status_raw == "Failed" or status == "failed" or status == "fail" or status == "error": + error_msg = task_data.get("error", {}).get("message", "Unknown error") + if not error_msg or error_msg == "Unknown error": + # Try alternative error message locations + error_msg = task_data.get("message", str(task_data.get("error", "Unknown error"))) + raise ValueError(f"Task {task_id} failed: {error_msg}") + elif status in ["pending", "processing", "submitted", "request moderated", "content moderated"]: + # Task still in progress + elapsed_minutes = int(elapsed_time / 60) + elapsed_seconds = int(elapsed_time % 60) + print( + f"Task {task_id} status: {status_raw} " + f"(elapsed: {elapsed_minutes}m {elapsed_seconds}s)..." + ) + time.sleep(0.5) # Wait 0.5 seconds before next poll (as per BFL API polling example) + elif status == "task not found": + raise ValueError(f"Task {task_id} not found. It may have expired or been deleted.") + else: + raise ValueError(f"Unknown task status: {status_raw}. Response data: {task_data}") + + def _extract_images_from_response(self, response_data: dict) -> List[str]: + """ + Extract image data from API response. + + BFL API returns images in result.sample when status is "Ready": + - {"status": "Ready", "result": {"sample": "base64_string_or_url"}} + - Also supports other formats for compatibility + + Args: + response_data: Response data from API + + Returns: + List[str]: List of base64-encoded image strings or URLs + """ + images = [] + + # BFL API uses result.sample for image data when status is "Ready" + if "result" in response_data and isinstance(response_data["result"], dict): + result = response_data["result"] + # Check for result.sample first (BFL API standard) + if "sample" in result: + sample_data = result["sample"] + if isinstance(sample_data, list): + images.extend(sample_data) + else: + images.append(sample_data) + # Also check for other common formats + if "image" in result: + images.append(result["image"]) + elif "images" in result: + if isinstance(result["images"], list): + images.extend(result["images"]) + else: + images.append(result["images"]) + if "image_url" in result: + images.append(result["image_url"]) + elif "image_urls" in result: + if isinstance(result["image_urls"], list): + images.extend(result["image_urls"]) + else: + images.append(result["image_urls"]) + + if "data" in response_data and isinstance(response_data["data"], dict): + data = response_data["data"] + if "image" in data: + images.append(data["image"]) + elif "images" in data: + if isinstance(data["images"], list): + images.extend(data["images"]) + else: + images.append(data["images"]) + if "image_url" in data: + images.append(data["image_url"]) + elif "image_urls" in data: + if isinstance(data["image_urls"], list): + images.extend(data["image_urls"]) + else: + images.append(data["image_urls"]) + + # Check for direct image data + if "image" in response_data: + images.append(response_data["image"]) + elif "images" in response_data: + if isinstance(response_data["images"], list): + images.extend(response_data["images"]) + else: + images.append(response_data["images"]) + + # Check for image URL + if "image_url" in response_data: + images.append(response_data["image_url"]) + elif "image_urls" in response_data: + if isinstance(response_data["image_urls"], list): + images.extend(response_data["image_urls"]) + else: + images.append(response_data["image_urls"]) + + return images + + def generate_text_to_image( + self, + prompt: str, + width: Optional[int] = None, + height: Optional[int] = None, + seed: Optional[int] = None, + safety_tolerance: int = 2, + output_format: str = "png", + **kwargs + ) -> List[Image.Image]: + """ + Generate image from text prompt. + + Args: + prompt: Text description of the image to generate + width: Width of the output image. Minimum: 64. Default: Model default + height: Height of the output image. Minimum: 64. Default: Model default + seed: Optional seed for reproducibility + safety_tolerance: Tolerance level for moderation (0-5). 0 = most strict, 5 = least strict. Default: 2 + output_format: Output format. Options: "jpeg", "png". Default: "png" + **kwargs: Additional parameters (webhook_url, webhook_secret, etc.) + + Returns: + List[Image.Image]: List of PIL Image objects + + Example: + >>> adapter = Flux2ProAdapter() + >>> images = adapter.generate_text_to_image( + ... prompt="A professional fashion model wearing elegant evening wear", + ... width=1024, + ... height=1024, + ... seed=42 + ... ) + >>> images[0].save("result.png") + """ + # Build payload + payload = { + "prompt": prompt, + "safety_tolerance": safety_tolerance, + "output_format": output_format + } + + if width is not None: + payload["width"] = width + if height is not None: + payload["height"] = height + if seed is not None: + payload["seed"] = seed + + payload.update(kwargs) + + # Make API request + try: + response = requests.post( + self.endpoint, + headers=self.headers, + json=payload, + timeout=300 + ) + response.raise_for_status() + response_data = response.json() + except requests.exceptions.HTTPError as e: + error_msg = f"HTTP {e.response.status_code}" + try: + error_data = e.response.json() + error_msg = error_data.get("detail", str(error_data)) + except: + error_msg = e.response.text or str(e) + raise ValueError(f"BFL API HTTP error ({e.response.status_code}): {error_msg}") + except requests.exceptions.RequestException as e: + raise ValueError(f"Failed to connect to BFL API: {str(e)}") + + # Handle async response + if "polling_url" in response_data: + task_id = response_data.get("id", "unknown") + polling_url = response_data["polling_url"] + response_data = self._poll_task(task_id, polling_url) + + # Extract images + image_data_list = self._extract_images_from_response(response_data) + + if not image_data_list: + raise ValueError("No images returned from API") + + # Decode images + decoded_images = [] + for image_data in image_data_list: + if isinstance(image_data, str) and image_data.startswith(("http://", "https://")): + # Fetch image from URL + img_response = requests.get(image_data) + img_response.raise_for_status() + image_bytes = img_response.content + elif isinstance(image_data, str): + # Decode base64 + image_bytes = base64.b64decode(image_data) + else: + image_bytes = image_data if isinstance(image_data, bytes) else str(image_data).encode() + + image_buffer = io.BytesIO(image_bytes) + image = Image.open(image_buffer) + decoded_images.append(image) + + return decoded_images + + def generate_image_edit( + self, + prompt: str, + input_image: Union[str, io.BytesIO, Image.Image], + width: Optional[int] = None, + height: Optional[int] = None, + seed: Optional[int] = None, + safety_tolerance: int = 2, + output_format: str = "png", + **kwargs + ) -> List[Image.Image]: + """ + Generate edited image from prompt and input image. + + Args: + prompt: Text description of how to edit the image + input_image: Input image (file path, URL, PIL Image, file-like object, or base64) + width: Width of the output image. Minimum: 64. Default: Model default + height: Height of the output image. Minimum: 64. Default: Model default + seed: Optional seed for reproducibility + safety_tolerance: Tolerance level for moderation (0-5). Default: 2 + output_format: Output format. Options: "jpeg", "png". Default: "png" + **kwargs: Additional parameters + + Returns: + List[Image.Image]: List of PIL Image objects + + Example: + >>> adapter = Flux2ProAdapter() + >>> images = adapter.generate_image_edit( + ... prompt="Change the outfit to casual streetwear style", + ... input_image="model.jpg" + ... ) + """ + # Prepare image input + image_input = self._prepare_image_input(input_image) + + # Build payload + payload = { + "prompt": prompt, + "input_image": image_input, + "safety_tolerance": safety_tolerance, + "output_format": output_format + } + + if width is not None: + payload["width"] = width + if height is not None: + payload["height"] = height + if seed is not None: + payload["seed"] = seed + + payload.update(kwargs) + + # Make API request + try: + response = requests.post( + self.endpoint, + headers=self.headers, + json=payload, + timeout=300 + ) + response.raise_for_status() + response_data = response.json() + except requests.exceptions.HTTPError as e: + error_msg = f"HTTP {e.response.status_code}" + try: + error_data = e.response.json() + error_msg = error_data.get("detail", str(error_data)) + except: + error_msg = e.response.text or str(e) + raise ValueError(f"BFL API HTTP error ({e.response.status_code}): {error_msg}") + except requests.exceptions.RequestException as e: + raise ValueError(f"Failed to connect to BFL API: {str(e)}") + + # Handle async response + if "polling_url" in response_data: + task_id = response_data.get("id", "unknown") + polling_url = response_data["polling_url"] + response_data = self._poll_task(task_id, polling_url) + + # Extract images + image_data_list = self._extract_images_from_response(response_data) + + if not image_data_list: + raise ValueError("No images returned from API") + + # Decode images + decoded_images = [] + for image_data in image_data_list: + if isinstance(image_data, str) and image_data.startswith(("http://", "https://")): + img_response = requests.get(image_data) + img_response.raise_for_status() + image_bytes = img_response.content + elif isinstance(image_data, str): + image_bytes = base64.b64decode(image_data) + else: + image_bytes = image_data if isinstance(image_data, bytes) else str(image_data).encode() + + image_buffer = io.BytesIO(image_bytes) + image = Image.open(image_buffer) + decoded_images.append(image) + + return decoded_images + + def generate_multi_image( + self, + prompt: str, + images: List[Union[str, io.BytesIO, Image.Image]], + width: Optional[int] = None, + height: Optional[int] = None, + seed: Optional[int] = None, + safety_tolerance: int = 2, + output_format: str = "png", + **kwargs + ) -> List[Image.Image]: + """ + Generate image from multiple input images (composition & style transfer). + + Supports up to 8 input images. Use multiple input images to compose + a new scene or transfer styles from reference images. + + Args: + prompt: Text description of how to combine/transform the images + images: List of input images (file paths, URLs, PIL Images, file-like objects, or base64) + Maximum 8 images supported + width: Width of the output image. Minimum: 64. Default: Model default + height: Height of the output image. Minimum: 64. Default: Model default + seed: Optional seed for reproducibility + safety_tolerance: Tolerance level for moderation (0-5). Default: 2 + output_format: Output format. Options: "jpeg", "png". Default: "png" + **kwargs: Additional parameters + + Returns: + List[Image.Image]: List of PIL Image objects + + Example: + >>> adapter = Flux2ProAdapter() + >>> images = adapter.generate_multi_image( + ... prompt="Create a fashion catalog layout combining these clothing styles", + ... images=["outfit1.jpg", "outfit2.jpg", "accessories.jpg"] + ... ) + """ + if len(images) > 8: + raise ValueError("Maximum 8 input images supported") + + # Prepare image inputs + image_inputs = [self._prepare_image_input(img) for img in images] + + # Build payload + payload = { + "prompt": prompt, + "safety_tolerance": safety_tolerance, + "output_format": output_format + } + + # Add images to payload (input_image, input_image_2, ..., input_image_8) + for idx, img_input in enumerate(image_inputs): + if idx == 0: + payload["input_image"] = img_input + else: + payload[f"input_image_{idx + 1}"] = img_input + + if width is not None: + payload["width"] = width + if height is not None: + payload["height"] = height + if seed is not None: + payload["seed"] = seed + + payload.update(kwargs) + + # Make API request + try: + response = requests.post( + self.endpoint, + headers=self.headers, + json=payload, + timeout=300 + ) + response.raise_for_status() + response_data = response.json() + except requests.exceptions.HTTPError as e: + error_msg = f"HTTP {e.response.status_code}" + try: + error_data = e.response.json() + error_msg = error_data.get("detail", str(error_data)) + except: + error_msg = e.response.text or str(e) + raise ValueError(f"BFL API HTTP error ({e.response.status_code}): {error_msg}") + except requests.exceptions.RequestException as e: + raise ValueError(f"Failed to connect to BFL API: {str(e)}") + + # Handle async response + if "polling_url" in response_data: + task_id = response_data.get("id", "unknown") + polling_url = response_data["polling_url"] + response_data = self._poll_task(task_id, polling_url) + + # Extract images + image_data_list = self._extract_images_from_response(response_data) + + if not image_data_list: + raise ValueError("No images returned from API") + + # Decode images + decoded_images = [] + for image_data in image_data_list: + if isinstance(image_data, str) and image_data.startswith(("http://", "https://")): + img_response = requests.get(image_data) + img_response.raise_for_status() + image_bytes = img_response.content + elif isinstance(image_data, str): + image_bytes = base64.b64decode(image_data) + else: + image_bytes = image_data if isinstance(image_data, bytes) else str(image_data).encode() + + image_buffer = io.BytesIO(image_bytes) + image = Image.open(image_buffer) + decoded_images.append(image) + + return decoded_images + + +class Flux2FlexAdapter: + """ + Adapter for FLUX.2 [FLEX] image generation API. + + FLUX.2 [FLEX] is a flexible image generation model with additional controls: + - Text-to-image generation with prompt upsampling + - Image editing (prompt + input image) + - Multi-image composition (up to 8 reference images) + - Custom width/height control + - Guidance scale control (1.5-10) + - Steps control for generation + - Seed for reproducibility + - Safety tolerance control + + Reference: https://docs.bfl.ai/api-reference/models/generate-or-edit-an-image-with-flux2-[flex] + + API endpoint: POST https://api.bfl.ai/v1/flux-2-flex + + Authentication: + Requires API key provided via constructor parameter or environment variable: + - BFL_API_KEY: Your BFL API key + The API key is sent in the 'x-key' header. + + Example: + >>> import os + >>> os.environ['BFL_API_KEY'] = 'your_api_key' + >>> adapter = Flux2FlexAdapter() + >>> images = adapter.generate_text_to_image( + ... prompt="A stylish fashion model wearing elegant evening wear", + ... guidance=7.5, + ... steps=50 + ... ) + """ + + BASE_URL = "https://api.bfl.ai" + ENDPOINT = "/v1/flux-2-flex" + + def __init__(self, api_key: Optional[str] = None, base_url: Optional[str] = None): + """ + Initialize the FLUX.2 [FLEX] client. + + Args: + api_key: BFL API key. Defaults to BFL_API_KEY environment variable. + base_url: Base URL for BFL API. Defaults to 'https://api.bfl.ai' if not set. + + Raises: + ValueError: If API key is not provided. + """ + self.api_key = api_key or os.getenv("BFL_API_KEY") + if not self.api_key: + raise ValueError( + "BFL API key is required. Set BFL_API_KEY environment variable " + "or pass api_key parameter." + ) + + self.base_url = base_url or self.BASE_URL + self.endpoint = f"{self.base_url}{self.ENDPOINT}" + + # Default headers + self.headers = { + "x-key": self.api_key, + "Content-Type": "application/json" + } + + def _prepare_image_input(self, image_input: Union[str, io.BytesIO, Image.Image]) -> str: + """Prepare image input for API request (same as Flux2ProAdapter).""" + # Handle PIL Image + if isinstance(image_input, Image.Image): + buffer = io.BytesIO() + image_input.save(buffer, format='PNG') + buffer.seek(0) + return base64.b64encode(buffer.read()).decode('utf-8') + + # Handle file-like object + if hasattr(image_input, 'read'): + image_file = image_input + image_file.seek(0) + image_bytes = image_file.read() + image_file.seek(0) + return base64.b64encode(image_bytes).decode('utf-8') + + # Handle string (file path, URL, or base64) + if isinstance(image_input, str): + # Check if it's a URL + if image_input.startswith(("http://", "https://")): + return image_input + + # Check if it's already base64 + if len(image_input) > 100 and not os.path.exists(image_input): + try: + base64.b64decode(image_input[:100]) + return image_input + except: + pass + + # It's a file path + with open(image_input, "rb") as image_file: + return base64.b64encode(image_file.read()).decode("utf-8") + + raise ValueError("Invalid image input: must be a file path, URL, file-like object, PIL Image, or base64 string") + + def _poll_task(self, task_id: str, polling_url: str, max_wait_time: int = 300) -> dict: + """Poll task status until completion (same as Flux2ProAdapter).""" + start_time = time.time() + + while True: + elapsed_time = time.time() - start_time + if elapsed_time > max_wait_time: + raise ValueError( + f"Task {task_id} timed out after {max_wait_time} seconds." + ) + + try: + response = requests.get( + polling_url, + headers={"x-key": self.api_key}, + timeout=30 + ) + response.raise_for_status() + task_data = response.json() + except requests.exceptions.RequestException as e: + raise ValueError(f"Failed to poll task status: {str(e)}") + + # Check task status (BFL API uses capitalized status values) + status_raw = task_data.get("status", "") + status = status_raw.lower() + + # BFL API uses "Ready" (capitalized) to indicate task completion + # Also check for common variations + if status_raw == "Ready" or status == "completed" or status == "succeed" or status == "ready": + return task_data + elif status_raw == "Error" or status_raw == "Failed" or status == "failed" or status == "fail" or status == "error": + error_msg = task_data.get("error", {}).get("message", "Unknown error") + if not error_msg or error_msg == "Unknown error": + # Try alternative error message locations + error_msg = task_data.get("message", str(task_data.get("error", "Unknown error"))) + raise ValueError(f"Task {task_id} failed: {error_msg}") + elif status in ["pending", "processing", "submitted", "request moderated", "content moderated"]: + elapsed_minutes = int(elapsed_time / 60) + elapsed_seconds = int(elapsed_time % 60) + print( + f"Task {task_id} status: {status_raw} " + f"(elapsed: {elapsed_minutes}m {elapsed_seconds}s)..." + ) + time.sleep(0.5) # Wait 0.5 seconds before next poll (as per BFL API polling example) + elif status == "task not found": + raise ValueError(f"Task {task_id} not found. It may have expired or been deleted.") + else: + raise ValueError(f"Unknown task status: {status_raw}. Response data: {task_data}") + + def _extract_images_from_response(self, response_data: dict) -> List[str]: + """ + Extract image data from API response. + + BFL API returns images in result.sample when status is "Ready": + - {"status": "Ready", "result": {"sample": "base64_string_or_url"}} + - Also supports other formats for compatibility + + Args: + response_data: Response data from API + + Returns: + List[str]: List of base64-encoded image strings or URLs + """ + images = [] + + # BFL API uses result.sample for image data when status is "Ready" + if "result" in response_data and isinstance(response_data["result"], dict): + result = response_data["result"] + # Check for result.sample first (BFL API standard) + if "sample" in result: + sample_data = result["sample"] + if isinstance(sample_data, list): + images.extend(sample_data) + else: + images.append(sample_data) + # Also check for other common formats + if "image" in result: + images.append(result["image"]) + elif "images" in result: + if isinstance(result["images"], list): + images.extend(result["images"]) + else: + images.append(result["images"]) + if "image_url" in result: + images.append(result["image_url"]) + elif "image_urls" in result: + if isinstance(result["image_urls"], list): + images.extend(result["image_urls"]) + else: + images.append(result["image_urls"]) + + if "data" in response_data and isinstance(response_data["data"], dict): + data = response_data["data"] + if "image" in data: + images.append(data["image"]) + elif "images" in data: + if isinstance(data["images"], list): + images.extend(data["images"]) + else: + images.append(data["images"]) + if "image_url" in data: + images.append(data["image_url"]) + elif "image_urls" in data: + if isinstance(data["image_urls"], list): + images.extend(data["image_urls"]) + else: + images.append(data["image_urls"]) + + # Check for direct image data + if "image" in response_data: + images.append(response_data["image"]) + elif "images" in response_data: + if isinstance(response_data["images"], list): + images.extend(response_data["images"]) + else: + images.append(response_data["images"]) + + # Check for image URL + if "image_url" in response_data: + images.append(response_data["image_url"]) + elif "image_urls" in response_data: + if isinstance(response_data["image_urls"], list): + images.extend(response_data["image_urls"]) + else: + images.append(response_data["image_urls"]) + + return images + + def generate_text_to_image( + self, + prompt: str, + width: Optional[int] = None, + height: Optional[int] = None, + seed: Optional[int] = None, + guidance: float = 3.5, + steps: int = 28, + prompt_upsampling: bool = True, + safety_tolerance: int = 2, + output_format: str = "png", + **kwargs + ) -> List[Image.Image]: + """ + Generate image from text prompt with advanced controls. + + Args: + prompt: Text description of the image to generate + width: Width of the output image. Minimum: 64. Default: Model default + height: Height of the output image. Minimum: 64. Default: Model default + seed: Optional seed for reproducibility + guidance: Guidance scale (1.5-10). Higher values = more adherence to prompt. Default: 3.5 + steps: Number of generation steps. Default: 28 + prompt_upsampling: Whether to use prompt upsampling. Default: True + safety_tolerance: Tolerance level for moderation (0-5). Default: 2 + output_format: Output format. Options: "jpeg", "png". Default: "png" + **kwargs: Additional parameters (webhook_url, webhook_secret, input_image_blob_path, etc.) + + Returns: + List[Image.Image]: List of PIL Image objects + + Example: + >>> adapter = Flux2FlexAdapter() + >>> images = adapter.generate_text_to_image( + ... prompt="A professional fashion model wearing elegant evening wear", + ... width=1024, + ... height=1024, + ... guidance=7.5, + ... steps=50, + ... seed=42 + ... ) + """ + # Validate guidance + if guidance < 1.5 or guidance > 10: + raise ValueError("guidance must be between 1.5 and 10") + + # Build payload + payload = { + "prompt": prompt, + "prompt_upsampling": prompt_upsampling, + "guidance": guidance, + "steps": steps, + "safety_tolerance": safety_tolerance, + "output_format": output_format + } + + if width is not None: + payload["width"] = width + if height is not None: + payload["height"] = height + if seed is not None: + payload["seed"] = seed + + payload.update(kwargs) + + # Make API request + try: + response = requests.post( + self.endpoint, + headers=self.headers, + json=payload, + timeout=300 + ) + response.raise_for_status() + response_data = response.json() + except requests.exceptions.HTTPError as e: + error_msg = f"HTTP {e.response.status_code}" + try: + error_data = e.response.json() + error_msg = error_data.get("detail", str(error_data)) + except: + error_msg = e.response.text or str(e) + raise ValueError(f"BFL API HTTP error ({e.response.status_code}): {error_msg}") + except requests.exceptions.RequestException as e: + raise ValueError(f"Failed to connect to BFL API: {str(e)}") + + # Handle async response + if "polling_url" in response_data: + task_id = response_data.get("id", "unknown") + polling_url = response_data["polling_url"] + response_data = self._poll_task(task_id, polling_url) + + # Extract images + image_data_list = self._extract_images_from_response(response_data) + + if not image_data_list: + raise ValueError("No images returned from API") + + # Decode images + decoded_images = [] + for image_data in image_data_list: + if isinstance(image_data, str) and image_data.startswith(("http://", "https://")): + img_response = requests.get(image_data) + img_response.raise_for_status() + image_bytes = img_response.content + elif isinstance(image_data, str): + image_bytes = base64.b64decode(image_data) + else: + image_bytes = image_data if isinstance(image_data, bytes) else str(image_data).encode() + + image_buffer = io.BytesIO(image_bytes) + image = Image.open(image_buffer) + decoded_images.append(image) + + return decoded_images + + def generate_image_edit( + self, + prompt: str, + input_image: Union[str, io.BytesIO, Image.Image], + width: Optional[int] = None, + height: Optional[int] = None, + seed: Optional[int] = None, + guidance: float = 3.5, + steps: int = 28, + prompt_upsampling: bool = True, + safety_tolerance: int = 2, + output_format: str = "png", + **kwargs + ) -> List[Image.Image]: + """ + Generate edited image from prompt and input image with advanced controls. + + Args: + prompt: Text description of how to edit the image + input_image: Input image (file path, URL, PIL Image, file-like object, or base64) + width: Width of the output image. Minimum: 64. Default: Model default + height: Height of the output image. Minimum: 64. Default: Model default + seed: Optional seed for reproducibility + guidance: Guidance scale (1.5-10). Default: 3.5 + steps: Number of generation steps. Default: 28 + prompt_upsampling: Whether to use prompt upsampling. Default: True + safety_tolerance: Tolerance level for moderation (0-5). Default: 2 + output_format: Output format. Options: "jpeg", "png". Default: "png" + **kwargs: Additional parameters + + Returns: + List[Image.Image]: List of PIL Image objects + """ + # Validate guidance + if guidance < 1.5 or guidance > 10: + raise ValueError("guidance must be between 1.5 and 10") + + # Prepare image input + image_input = self._prepare_image_input(input_image) + + # Build payload + payload = { + "prompt": prompt, + "input_image": image_input, + "prompt_upsampling": prompt_upsampling, + "guidance": guidance, + "steps": steps, + "safety_tolerance": safety_tolerance, + "output_format": output_format + } + + if width is not None: + payload["width"] = width + if height is not None: + payload["height"] = height + if seed is not None: + payload["seed"] = seed + + payload.update(kwargs) + + # Make API request + try: + response = requests.post( + self.endpoint, + headers=self.headers, + json=payload, + timeout=300 + ) + response.raise_for_status() + response_data = response.json() + except requests.exceptions.HTTPError as e: + error_msg = f"HTTP {e.response.status_code}" + try: + error_data = e.response.json() + error_msg = error_data.get("detail", str(error_data)) + except: + error_msg = e.response.text or str(e) + raise ValueError(f"BFL API HTTP error ({e.response.status_code}): {error_msg}") + except requests.exceptions.RequestException as e: + raise ValueError(f"Failed to connect to BFL API: {str(e)}") + + # Handle async response + if "polling_url" in response_data: + task_id = response_data.get("id", "unknown") + polling_url = response_data["polling_url"] + response_data = self._poll_task(task_id, polling_url) + + # Extract images + image_data_list = self._extract_images_from_response(response_data) + + if not image_data_list: + raise ValueError("No images returned from API") + + # Decode images + decoded_images = [] + for image_data in image_data_list: + if isinstance(image_data, str) and image_data.startswith(("http://", "https://")): + img_response = requests.get(image_data) + img_response.raise_for_status() + image_bytes = img_response.content + elif isinstance(image_data, str): + image_bytes = base64.b64decode(image_data) + else: + image_bytes = image_data if isinstance(image_data, bytes) else str(image_data).encode() + + image_buffer = io.BytesIO(image_bytes) + image = Image.open(image_buffer) + decoded_images.append(image) + + return decoded_images + + def generate_multi_image( + self, + prompt: str, + images: List[Union[str, io.BytesIO, Image.Image]], + width: Optional[int] = None, + height: Optional[int] = None, + seed: Optional[int] = None, + guidance: float = 3.5, + steps: int = 28, + prompt_upsampling: bool = True, + safety_tolerance: int = 2, + output_format: str = "png", + **kwargs + ) -> List[Image.Image]: + """ + Generate image from multiple input images with advanced controls. + + Supports up to 8 input images for composition and style transfer. + + Args: + prompt: Text description of how to combine/transform the images + images: List of input images (file paths, URLs, PIL Images, file-like objects, or base64) + Maximum 8 images supported + width: Width of the output image. Minimum: 64. Default: Model default + height: Height of the output image. Minimum: 64. Default: Model default + seed: Optional seed for reproducibility + guidance: Guidance scale (1.5-10). Default: 3.5 + steps: Number of generation steps. Default: 28 + prompt_upsampling: Whether to use prompt upsampling. Default: True + safety_tolerance: Tolerance level for moderation (0-5). Default: 2 + output_format: Output format. Options: "jpeg", "png". Default: "png" + **kwargs: Additional parameters + + Returns: + List[Image.Image]: List of PIL Image objects + """ + if len(images) > 8: + raise ValueError("Maximum 8 input images supported") + + # Validate guidance + if guidance < 1.5 or guidance > 10: + raise ValueError("guidance must be between 1.5 and 10") + + # Prepare image inputs + image_inputs = [self._prepare_image_input(img) for img in images] + + # Build payload + payload = { + "prompt": prompt, + "prompt_upsampling": prompt_upsampling, + "guidance": guidance, + "steps": steps, + "safety_tolerance": safety_tolerance, + "output_format": output_format + } + + # Add images to payload + for idx, img_input in enumerate(image_inputs): + if idx == 0: + payload["input_image"] = img_input + else: + payload[f"input_image_{idx + 1}"] = img_input + + if width is not None: + payload["width"] = width + if height is not None: + payload["height"] = height + if seed is not None: + payload["seed"] = seed + + payload.update(kwargs) + + # Make API request + try: + response = requests.post( + self.endpoint, + headers=self.headers, + json=payload, + timeout=300 + ) + response.raise_for_status() + response_data = response.json() + except requests.exceptions.HTTPError as e: + error_msg = f"HTTP {e.response.status_code}" + try: + error_data = e.response.json() + error_msg = error_data.get("detail", str(error_data)) + except: + error_msg = e.response.text or str(e) + raise ValueError(f"BFL API HTTP error ({e.response.status_code}): {error_msg}") + except requests.exceptions.RequestException as e: + raise ValueError(f"Failed to connect to BFL API: {str(e)}") + + # Handle async response + if "polling_url" in response_data: + task_id = response_data.get("id", "unknown") + polling_url = response_data["polling_url"] + response_data = self._poll_task(task_id, polling_url) + + # Extract images + image_data_list = self._extract_images_from_response(response_data) + + if not image_data_list: + raise ValueError("No images returned from API") + + # Decode images + decoded_images = [] + for image_data in image_data_list: + if isinstance(image_data, str) and image_data.startswith(("http://", "https://")): + img_response = requests.get(image_data) + img_response.raise_for_status() + image_bytes = img_response.content + elif isinstance(image_data, str): + image_bytes = base64.b64decode(image_data) + else: + image_bytes = image_data if isinstance(image_data, bytes) else str(image_data).encode() + + image_buffer = io.BytesIO(image_bytes) + image = Image.open(image_buffer) + decoded_images.append(image) + + return decoded_images +