Build a Production Editing Recipe with MAI-Image-2.5

Private-preview MAI-Image-2.5 notebook covering text-to-image generation, image edits, safety reminders, and troubleshooting.

Author

Anirban Ganguly's avatar
Anirban Ganguly
Contributor
@anihitk07

MAI Image 2.5 (Private Preview) — Edit + Generate Notebook

Model card date: May 2026
Release date: June 2, 2026

MAI-Image-2.5 is a 20B-parameter diffusion model for both:

  • high-quality text-to-image generation, and
  • precise, controllable image-to-image editing.

Model-card highlights reflected in this notebook:

  • Architecture: diffusion + flow-matching objective
  • Inputs: text, plus image input for editing
  • Context length: 32K tokens
  • Output limit: total pixels must be <= 1,048,576 (equivalent to 1024x1024). Either dimension may exceed 1024 if total stays within budget.
  • Strong focus areas: surgical edits, layout preservation, text updates, artifact cleanup, portrait/product quality
  • Quality signal: top-3 ranking on LMArena text-to-image and image-editing leaderboards (per model card)

Private Preview Terms (must follow)

  • Internal-only use; no external end-user access.
  • Private preview models are not for production deployments.
  • Follow Microsoft license terms and Generative AI code of conduct.
  • Abuse monitoring and content filtering remain enabled.
  • Preview is as-is and may change or be discontinued without notice.
  • Do not send regulated personal data in preview feedback.

1. Setup

Environment variables

Variable Required Secret Purpose
MAI_IMAGE_25_ENDPOINT Yes No MAI Image 2.5 endpoint (Cognitive endpoint format recommended).
MAI_IMAGE_25_API_KEY Yes Yes API key for MAI Image 2.5 preview calls.
MAI_IMAGE_25_DEPLOYMENT_NAME Yes No Deployment/model name used in request payloads.
IMAGE_OUTPUT_DIR Optional No Output directory for generated files; defaults to media/mai-image-2-5.
MAI_IMAGE_25_INPUT_IMAGE Optional No Input image for edit flow; defaults to media/mai-image-2-5/01-generated-example.png.
MAI_IMAGE_25_EDIT_SIZE Optional No Edit image size (for example 1024x1024).

Do not commit .env or deployment.env files with secrets.

# %pip install -q requests python-dotenv pillow
import os
import base64
import mimetypes
import socket
from urllib.parse import urlparse
from pathlib import Path
import requests
from dotenv import load_dotenv
from IPython.display import Image as IPImage, display

ENV_PATH = 'deployment.env' if os.path.exists('deployment.env') else os.path.join('..', 'deployment.env')
load_dotenv(ENV_PATH, override=True)

MAI_IMAGE_25_ENDPOINT = os.getenv('MAI_IMAGE_25_ENDPOINT')
MAI_IMAGE_25_DEPLOYMENT_NAME = os.getenv('MAI_IMAGE_25_DEPLOYMENT_NAME')
MAI_IMAGE_25_API_KEY = os.getenv('MAI_IMAGE_25_API_KEY')

image_output_env = os.getenv('IMAGE_OUTPUT_DIR')
IMAGE_OUTPUT_DIR = Path(image_output_env) if image_output_env else Path('media') / 'mai-image-2-5'
IMAGE_OUTPUT_DIR.mkdir(parents=True, exist_ok=True)
input_image_env = os.getenv('MAI_IMAGE_25_INPUT_IMAGE')
INPUT_IMAGE_PATH = Path(input_image_env) if input_image_env else IMAGE_OUTPUT_DIR / '01-generated-example.png'
DEFAULT_EDIT_SIZE = os.getenv('MAI_IMAGE_25_EDIT_SIZE', '1024x1024')

assert MAI_IMAGE_25_ENDPOINT, 'Set MAI_IMAGE_25_ENDPOINT in deployment.env'
assert MAI_IMAGE_25_API_KEY, 'Set MAI_IMAGE_25_API_KEY in deployment.env (MAI Image 2.5 currently uses API key auth in preview)'

def auth_headers(content_type: str | None = None) -&gt; dict:
    h = {'api-key': MAI_IMAGE_25_API_KEY}
    if content_type:
        h['Content-Type'] = content_type
    return h

def preflight_endpoint_dns(endpoint: str) -&gt; None:
    host = urlparse(endpoint).hostname
    try:
        socket.getaddrinfo(host, 443)
    except Exception as ex:
        raise RuntimeError(
            f'Endpoint DNS resolution failed for {host}: {ex}. '
            'Use the Cognitive endpoint format (e.g. https://westcentralus.api.cognitive.microsoft.com/).'
        )

preflight_endpoint_dns(MAI_IMAGE_25_ENDPOINT)

print('Endpoint:', MAI_IMAGE_25_ENDPOINT)
print('Deployment:', MAI_IMAGE_25_DEPLOYMENT_NAME)
print('Auth mode: API key')
print('Output dir:', IMAGE_OUTPUT_DIR)
print('Default edit size:', DEFAULT_EDIT_SIZE)

2. API behavior note (important)

  • Image edit endpoint (/mai/v1/images/edits) uses size (for example, 1024x1024).
  • Text-to-image generation endpoint (/mai/v1/images/generations) uses width and height.
  • For both paths, keep total pixel budget <= 1024x1024.

Illustrative examples from one run:

The images below are illustrative examples from one run. Your output may differ because image generation and editing are non-deterministic.

Generated image example

Edited image example

3. Text-to-Image Helper (width + height)

def _validate_pixel_budget(width: int, height: int, source: str) -&gt; None:
    assert width &gt; 0 and height &gt; 0, f'{source}: width and height must be positive'
    assert width * height &lt;= 1_048_576, f'{source}: width*height must be <= 1,048,576'


def generate_image_25(prompt: str, out_file: str, width: int = 1024, height: int = 1024) -&gt; Path:
    _validate_pixel_budget(width, height, 'generation')

    url = f"{MAI_IMAGE_25_ENDPOINT.rstrip('/')}/mai/v1/images/generations"
    payload = {
        'model': MAI_IMAGE_25_DEPLOYMENT_NAME,
        'prompt': prompt,
        'width': width,
        'height': height,
    }
    resp = requests.post(url, headers=auth_headers('application/json'), json=payload, timeout=180)
    if not resp.ok:
        raise requests.HTTPError(f'Generation failed with {resp.status_code}: {resp.text}', response=resp)
    data = resp.json().get('data', [])
    assert data and data[0].get('b64_json'), f'No image output in response: {resp.text[:500]}'
    out_path = IMAGE_OUTPUT_DIR / out_file
    out_path.write_bytes(base64.b64decode(data[0]['b64_json']))
    return out_path

4. Image Edit Helper (size)

def _parse_size(size: str) -&gt; tuple[int, int]:
    parts = size.lower().split('x')
    assert len(parts) == 2, "size must be in '<width>x<height>' format, e.g. 1024x1024"
    w, h = int(parts[0]), int(parts[1])
    return w, h


def edit_image_25(prompt: str, input_image: Path, out_file: str, size: str = DEFAULT_EDIT_SIZE) -&gt; Path:
    w, h = _parse_size(size)
    _validate_pixel_budget(w, h, 'edit')

    url = f"{MAI_IMAGE_25_ENDPOINT.rstrip('/')}/mai/v1/images/edits"
    mime = mimetypes.guess_type(str(input_image))[0] or 'image/png'
    assert input_image.exists(), f'Input image not found: {input_image}'

    headers = auth_headers()
    with input_image.open('rb') as f:
        files = {'image': (input_image.name, f, mime)}
        form = {
            'prompt': prompt,
            'model': MAI_IMAGE_25_DEPLOYMENT_NAME,
            'size': size,
        }
        resp = requests.post(url, headers=headers, files=files, data=form, timeout=300)

    if not resp.ok:
        raise requests.HTTPError(f'Edit failed with {resp.status_code}: {resp.text}', response=resp)
    data = resp.json().get('data', [])
    assert data and data[0].get('b64_json'), 'b64_json missing or null (common with bad decoding flow)'
    out_path = IMAGE_OUTPUT_DIR / out_file
    out_path.write_bytes(base64.b64decode(data[0]['b64_json']))
    return out_path

5. Run a generation sample

generated = generate_image_25(
    prompt='A photorealistic concept art poster of an Azure AI lab at sunset, cinematic lighting',
    out_file='mai_image25_generation.png',
    width=1024,
    height=1024,
)
print('Generated image:', generated)
display(IPImage(filename=str(generated), width=512))
INPUT_IMAGE_PATH=generated

Example response shape:

{
  "created": 0,
  "data": [
    {
      "b64_json": "<redacted-base64-image>"
    }
  ]
}

6. Run an edit sample (requires input image)

if INPUT_IMAGE_PATH.exists():
    edited = edit_image_25(
        prompt='Turn this into a clean futuristic product shot with studio lighting',
        input_image=INPUT_IMAGE_PATH,
        out_file='mai_image25_edited.png',
        size=DEFAULT_EDIT_SIZE,
    )
    print('Edited image:', edited)
    display(IPImage(filename=str(edited), width=512))
else:
    print(f'Skipping edit run: input image not found at {INPUT_IMAGE_PATH}')

Example response shape:

{
  "created": 0,
  "data": [
    {
      "b64_json": "<redacted-base64-image>"
    }
  ]
}

7. Troubleshooting

Error Resolution
DeploymentModelNotSupported Subscription not whitelisted or model name/version mismatch. Verify deployment and model availability.
Output image is 0-3 bytes b64_json is null or decode flow is wrong. Save response JSON first, then decode.
base64: unrecognized option --ignore-garbage On macOS use base64 -d instead of GNU flags.
401/403 Invalid/expired API key or wrong endpoint. Re-check MAI_IMAGE_25_API_KEY and MAI_IMAGE_25_ENDPOINT.

8. Usage, safety, and out-of-scope reminders

Primary use cases aligned to model card:

  • Creative text-to-image generation
  • Surgical image editing (object removal/replacement, inpainting, text updates, attribute changes)
  • Product/branding visuals, portraits, and production design workflows

Out-of-scope / not allowed:

  • Deceptive impersonation or misleading identity content
  • Illegal or policy-violating content generation
  • Harmful or abusive content generation

Responsible AI notes:

  • This model includes layered safety guardrails (prompt and output filtering) in deployed systems.
  • Continue to review outputs and prompts for policy compliance before downstream use.

Tags

mai models inference multimodal