[build-system]
requires = ["setuptools", "pip", "wheel"]
build-backend = "setuptools.build_meta"

[project]
name = "InvokeAI"
description = "A full-featured AI-assisted image generation environment designed for creatives and enthusiasts."
requires-python = ">=3.11, <3.13"
readme = { content-type = "text/markdown", file = "README.md" }
keywords = ["stable-diffusion", "AI"]
dynamic = ["version"]
license = { file = "LICENSE" }
authors = [{ name = "Invoke", email = "support@invoke.ai" }]
classifiers = [
  'Development Status :: 5 - Production/Stable',
  'Environment :: GPU',
  'Environment :: GPU :: NVIDIA CUDA',
  'Environment :: MacOS X',
  'Intended Audience :: End Users/Desktop',
  'Intended Audience :: Developers',
  'License :: OSI Approved :: Apache Software License',
  'Operating System :: POSIX :: Linux',
  'Operating System :: MacOS',
  'Operating System :: Microsoft :: Windows',
  'Programming Language :: Python :: 3 :: Only',
  'Programming Language :: Python :: 3.12',
  'Topic :: Artistic Software',
  'Topic :: Internet :: WWW/HTTP :: WSGI :: Application',
  'Topic :: Internet :: WWW/HTTP :: WSGI :: Server',
  'Topic :: Multimedia :: Graphics',
  'Topic :: Scientific/Engineering :: Artificial Intelligence',
  'Topic :: Scientific/Engineering :: Image Processing',
]
dependencies = [
  # Core generation dependencies, pinned for reproducible builds.
  "accelerate",
  "bitsandbytes; sys_platform!='darwin'",
  "compel>=2.4.0,<3",
  "diffusers[torch]==0.40.0",
  "gguf",
  "mediapipe==0.10.14",                   # needed for "mediapipeface" controlnet model
  "mistral-common>=1.5.4,<2",             # canonical Tekken tokenizer for FLUX.2 [dev] Mistral encoder; the
                                          # loader depends on private surface (Tekkenizer internals) that
                                          # moves across majors, so cap below 2.x (validated against 1.11.6)
  "numpy<2.0.0",
  "onnx==1.16.1",
  "onnxruntime==1.19.2",
  "opencv-contrib-python",
  "imageio[ffmpeg]>=2.37",                 # video encode (for Wan 2.2 T2V/I2V output); encode behavior (macro_block_size) is version-sensitive
  "psutil>=6",                             # video decode worker process-tree termination
  "safetensors",
  "sentencepiece==0.2.0",                 # 0.2.1 coredumps windows when loading t5 tokenizer
  "spandrel",
  # Loosely pinned, will respect requirement of `diffusers[torch]`. Split by platform: linux/win allow
  # >=2.10 so the rocm extra can use torch 2.10.0+rocm7.1, while macOS stays on 2.7.x — newer macOS torch
  # wheels exercise MPS on CI runners (no usable Metal GPU) and fail with MPS OOM.
  # Deliberately NOT capped below 2.12 here. torch 2.12.x+rocm7.1 is reported to break generation (#9410), but
  # this range is what every backend-agnostic install resolves against — manual installs use
  # `--torch-backend=<x>` with no extra (see docs/start-here/manual), so a blanket cap would also
  # reject torch>=2.12 on Windows/Linux CUDA, CPU and ARM64, where 2.12 has no known problem.
  # The ROCm path is constrained where it can actually be targeted: the `rocm` extra below pins an
  # exact version, and the manual docs carry the ROCm-specific caveat.
  "torch>=2.7.0,<3.0; sys_platform != 'darwin'",
  "torch>=2.7.0,<2.8.0; sys_platform == 'darwin'",
  "torchsde",                             # diffusers needs this for SDE solvers, but it is not an explicit dep of diffusers
  "torchvision",
  "transformers>=5.5,<5.6",

  # Core application dependencies, pinned for reproducible builds.
  "fastapi-events",
  # The old pin sat at 0.118.3 because 0.119.0 crashed generating our OpenAPI schema. That was a FastAPI bug, not
  # ours (`KeyError: '$ref'` in fastapi/_compat/v2.py, which assumed every field mapping carries a `$ref`), and it
  # is fixed as of 0.124.0 — no change to AnyInvocation was needed.
  #
  # Two later changes did need adapting to, both handled: 0.130 emits `contentMediaType` instead of
  # `format: binary` for file uploads (see the Blob mapping in frontend/web/scripts/typegen.js), and 0.141 keeps
  # included routers as a single node in `app.routes` rather than copying their routes into it (see
  # `_iter_route_contexts` in tests/app/routers/test_model_manager_authorization.py). Keep the minor-version bound:
  # both of those were silent breakages that only surfaced because something happened to assert on them.
  "fastapi>=0.141.1,<0.142",
  "huggingface-hub",
  "networkx",
  "pydantic-settings",
  "pydantic",
  "python-socketio",
  "uvicorn[standard]",

  # Auxiliary dependencies, pinned only if necessary.
  "blake3",
  "bcrypt<4.0.0",
  "Deprecated",
  "dnspython",
  "dynamicprompts",
  "einops",
  "email-validator>=2.0.0",
  "passlib[bcrypt]>=1.7.4",
  "picklescan",
  "pillow",
  "prompt-toolkit",
  "pypatchmatch",
  "python-jose[cryptography]>=3.3.0",
  "python-multipart",
  "requests",
  "semver~=3.0.1",
  "PyWavelets",
]

[project.optional-dependencies]
"xformers" = [
  # Core generation dependencies, pinned for reproducible builds.
  "xformers>=0.0.28.post1; sys_platform!='darwin'",
  # torch 2.4+cu carries its own triton dependency
]

# The +cpu/+cu128/+rocm7.1 pins live on PyTorch's WHL indexes. torchvision has no
# linux_aarch64 wheels there (torch does for +cpu/+cu128), so on linux_aarch64 these
# pins are unsatisfiable and are gated behind a marker that excludes only that
# platform; every other platform (Windows, macOS, Linux x86_64) is unaffected. On
# linux_aarch64 the base `torch` / `torchvision` declarations resolve from PyPI
# instead (CPU-only wheels — an aarch64 install requesting `cuda` gets CPU torch).
# Since torch+cu128 aarch64 wheels DO exist on the index, real CUDA-on-aarch64
# (e.g. Grace/GH200) is feasible later with a torchvision-specific workaround.
"cpu" = [
  "torch==2.7.1+cpu; sys_platform != 'linux' or platform_machine != 'aarch64'",
  "torchvision==0.22.1+cpu; sys_platform != 'linux' or platform_machine != 'aarch64'",
  # linux_aarch64 fallback: same versions, resolved from PyPI. Without these
  # explicit entries the extra's conflict-universe would contain no torch at all
  # on aarch64 (uv partitions the base declarations into the no-extra universe).
  "torch==2.7.1; sys_platform == 'linux' and platform_machine == 'aarch64'",
  "torchvision==0.22.1; sys_platform == 'linux' and platform_machine == 'aarch64'",
]
"cuda" = [
  "torch==2.7.1+cu128; sys_platform != 'linux' or platform_machine != 'aarch64'",
  "torchvision==0.22.1+cu128; sys_platform != 'linux' or platform_machine != 'aarch64'",
  # linux_aarch64 fallback, see note under "cpu"
  "torch==2.7.1; sys_platform == 'linux' and platform_machine == 'aarch64'",
  "torchvision==0.22.1; sys_platform == 'linux' and platform_machine == 'aarch64'",
]
"rocm" = [
  # ROCm wheels are x86_64-linux-only; gate by platform so macOS/win/linux-aarch64 resolution is unaffected.
  "torch==2.10.0+rocm7.1; sys_platform == 'linux' and platform_machine != 'aarch64'",
  "torchvision==0.25.0+rocm7.1; sys_platform == 'linux' and platform_machine != 'aarch64'",
  "triton-rocm==3.6.0; sys_platform == 'linux' and platform_machine != 'aarch64'",
  # linux_aarch64 fallback, see note under "cpu"
  "torch==2.7.1; sys_platform == 'linux' and platform_machine == 'aarch64'",
  "torchvision==0.22.1; sys_platform == 'linux' and platform_machine == 'aarch64'",
]
# Intel XPU (Arc / Battlemage) wheels are published only for linux-x86_64 and
# windows-amd64 on PyTorch's WHL index. Gate the +xpu pins to those platforms; the
# Intel oneAPI runtime libs (intel-sycl-rt, etc.) come in automatically as deps of
# torch+xpu.
# Pinned ahead of the cpu/cuda extras (2.7.1), as the rocm extra already is. Intel's XPU
# backend matured considerably after 2.7.1: torch.xpu.mem_get_info() works on driver/kernel
# combinations where it previously raised, and the bundled oneAPI runtime ships with the wheel
# (intel-sycl-rt), so upgrading torch upgrades the user-space runtime too.
"xpu" = [
  "torch==2.13.0+xpu; (sys_platform == 'linux' and platform_machine == 'x86_64') or sys_platform == 'win32'",
  "torchvision==0.28.0+xpu; (sys_platform == 'linux' and platform_machine == 'x86_64') or sys_platform == 'win32'",
  # XPU triton (for torch.compile); the WHL index ships linux-x86_64 and win_amd64 wheels.
  # Renamed from pytorch-triton-xpu to triton-xpu upstream.
  "triton-xpu==3.7.2; (sys_platform == 'linux' and platform_machine == 'x86_64') or sys_platform == 'win32'",
  # macOS / linux_aarch64 have no +xpu wheels: fall back to base torch so the extra's
  # conflict-universe always contains a torch. See note under "cpu". These stay on 2.7.1 to
  # match the other extras and the project's `torch<2.8.0` constraint on darwin -- only the
  # +xpu wheels above move.
  "torch==2.7.1; sys_platform == 'darwin' or (sys_platform == 'linux' and platform_machine == 'aarch64')",
  "torchvision==0.22.1; sys_platform == 'darwin' or (sys_platform == 'linux' and platform_machine == 'aarch64')",
]

"onnx" = ["onnxruntime"]
"onnx-cuda" = ["onnxruntime-gpu"]
"onnx-directml" = ["onnxruntime-directml"]
"dist" = ["pip-tools", "pipdeptree", "twine"]
"dev" = ["jurigged", "pudb", "snakeviz", "gprof2dot"]
"test" = [
  "ruff~=0.11.2",
  "ruff-lsp~=0.0.62",
  "mypy",
  "pre-commit",
  "pytest>6.0.0",
  "pytest-cov",
  "pytest-timeout",
  "pytest-datadir",
  "requests_testadapter",
  "httpx",
  "polyfactory==2.19.0",
  "humanize==4.12.1",
]

[tool.uv]
# Prevent opencv-python from ever being chosen during dependency resolution.
# This prevents conflicts with opencv-contrib-python, which Invoke requires.
override-dependencies = ["opencv-python; sys_platform=='never'"]
conflicts = [[{ extra = "cpu" }, { extra = "cuda" }, { extra = "rocm" }, { extra = "xpu" }]]
index-strategy = "unsafe-best-match"
# Restrict resolution to the platforms we support: x86_64/aarch64 Linux, Windows,
# and macOS. (ROCm and the PyTorch WHL-index pins are x86_64-only; on aarch64
# Linux torch/torchvision fall back to PyPI via the markers below.)
environments = ["sys_platform == 'win32' or sys_platform == 'darwin' or (sys_platform == 'linux' and (platform_machine == 'x86_64' or platform_machine == 'aarch64'))"]

# compel, could you please not pull in all of Jupyter. (compel declares `notebook>=6.5.7` but does not
# reference IPython/Jupyter anywhere in its code.) Dropping it takes ~65 packages out of the environment.
#
# Deliberately the *global* form rather than `{ package = ..., dependencies = ... }`: the per-package form
# needs uv >= 0.11.25 and is a hard parse error on 0.10.0 - 0.11.24, while this form has been understood
# since uv 0.9.8 and resolves to exactly the same set. Older uv only warns and ignores it, so no install
# path breaks -- and the launcher syncs `--frozen` from the lockfile below, so users get the trimmed
# environment whichever uv they have. Keep it that way: a `required-version` here would hard-fail the uv
# 0.6.12 bundled in launcher <= 1.8.1 for no gain, since CI's `uv lock --locked` already catches a
# lockfile regenerated by a uv too old to honor this.
exclude-dependencies = ["notebook"]

[tool.uv.sources]
# Exclude linux_aarch64 from the PyTorch WHL indexes so that uv resolves torch /
# torchvision from PyPI on that platform. The indexes DO carry aarch64 torch wheels
# (+cpu/+cu128), but no aarch64 torchvision wheels, so the pinned pairs cannot
# resolve there. All other platforms (Windows, macOS, Linux x86_64) are unaffected.
torch = [
  { index = "torch-cpu", extra = "cpu", marker = "sys_platform != 'linux' or platform_machine != 'aarch64'" },
  { index = "torch-cuda", extra = "cuda", marker = "sys_platform != 'linux' or platform_machine != 'aarch64'" },
  { index = "torch-rocm", extra = "rocm", marker = "sys_platform != 'linux' or platform_machine != 'aarch64'" },
  { index = "torch-xpu", extra = "xpu", marker = "(sys_platform == 'linux' and platform_machine == 'x86_64') or sys_platform == 'win32'" },
  { index = "pypi", marker = "sys_platform == 'linux' and platform_machine == 'aarch64'" },
]
torchvision = [
  { index = "torch-cpu", extra = "cpu", marker = "sys_platform != 'linux' or platform_machine != 'aarch64'" },
  { index = "torch-cuda", extra = "cuda", marker = "sys_platform != 'linux' or platform_machine != 'aarch64'" },
  { index = "torch-rocm", extra = "rocm", marker = "sys_platform != 'linux' or platform_machine != 'aarch64'" },
  { index = "torch-xpu", extra = "xpu", marker = "(sys_platform == 'linux' and platform_machine == 'x86_64') or sys_platform == 'win32'" },
  { index = "pypi", marker = "sys_platform == 'linux' and platform_machine == 'aarch64'" },
]
triton-rocm = [
  { index = "torch-rocm", marker = "sys_platform == 'linux' and platform_machine != 'aarch64'" },
]
triton-xpu = [
  { index = "torch-xpu", marker = "(sys_platform == 'linux' and platform_machine == 'x86_64') or sys_platform == 'win32'" },
]

[[tool.uv.index]]
name = "pypi"
url = "https://pypi.org/simple"

[[tool.uv.index]]
name = "torch-cpu"
url = "https://download.pytorch.org/whl/cpu"
explicit = true

[[tool.uv.index]]
name = "torch-cuda"
url = "https://download.pytorch.org/whl/cu128"
explicit = true

[[tool.uv.index]]
name = "torch-rocm"
url = "https://download.pytorch.org/whl/rocm7.1"
explicit = true

[[tool.uv.index]]
name = "torch-xpu"
url = "https://download.pytorch.org/whl/xpu"
explicit = true

[project.scripts]
"invokeai-web" = "invokeai.app.run_app:run_app"
"invoke-useradd" = "invokeai.app.util.user_management:useradd"
"invoke-userdel" = "invokeai.app.util.user_management:userdel"
"invoke-userlist" = "invokeai.app.util.user_management:userlist"
"invoke-usermod" = "invokeai.app.util.user_management:usermod"

[project.urls]
"Homepage" = "https://invoke.ai/"
"Documentation" = "https://invoke.ai/"
"Source" = "https://github.com/invoke-ai/InvokeAI/"
"Bug Reports" = "https://github.com/invoke-ai/InvokeAI/issues"
"Discord" = "https://discord.gg/ZmtBAhwWhy"

[tool.setuptools.dynamic]
version = { attr = "invokeai.version.__version__" }

[tool.setuptools.packages.find]
"where" = ["."]
"include" = [
  "invokeai.assets.fonts*",
  "invokeai.version*",
  "invokeai.generator*",
  "invokeai.backend*",
  "invokeai.frontend*",
  "invokeai.frontend.web.dist*",
  "invokeai.frontend.web.static*",
  "invokeai.configs*",
  "invokeai.app*",
  "invokeai.invocation_api*",
]

[tool.setuptools.package-data]
"invokeai.app.assets" = ["**/*.png"]
"invokeai.backend.anima" = ["tokenizer/*.json"]
"invokeai.backend.qwen3" = ["tokenizer/*.json", "tokenizer/*.json.gz"]
"invokeai.backend.t5" = ["tokenizer/*.json"]
"invokeai.app.services.workflow_records.default_workflows" = ["*.json"]
"invokeai.backend.hidiffusion" = ["sd_module_key/*.txt"]
"invokeai.app.services.style_preset_records" = ["*.json"]
"invokeai.app.services.style_preset_images.default_style_preset_images" = [
  "*.png",
]
"invokeai.assets.fonts" = ["**/*.ttf"]
"invokeai.backend" = ["**.png", "**/*.icc"]
"invokeai.configs" = ["*.example", "**/*.yaml", "*.txt"]
"invokeai.frontend.web.dist" = ["**"]
"invokeai.frontend.web.static" = ["**"]
"invokeai.app.invocations" = ["**"]

#=== Begin: PyTest and Coverage
[tool.pytest.ini_options]
addopts = "--cov-report term --cov-report html --cov-report xml --strict-markers -m \"not slow\""
markers = [
  "slow: Marks tests as slow. Disabled by default. To run all tests, use -m \"\". To run only slow tests, use -m \"slow\".",
  "timeout: Marks the timeout override.",
]
[tool.coverage.run]
branch = true
source = ["invokeai"]
omit = ["*tests*", "*migrations*", ".venv/*", "*.env"]
[tool.coverage.report]
show_missing = true
fail_under = 85     # let's set something sensible on Day 1 ...
[tool.coverage.json]
output = "coverage/coverage.json"
pretty_print = true
[tool.coverage.html]
directory = "coverage/html"
[tool.coverage.xml]
output = "coverage/index.xml"
#=== End: PyTest and Coverage

#=== Begin: Ruff
[tool.ruff]
line-length = 120
exclude = [
  ".git",
  "__pycache__",
  "build",
  "dist",
  "invokeai/frontend/web/node_modules/",
  ".venv*",
  "*.ipynb",
  "invokeai/backend/image_util/mediapipe_face/", # External code
  "invokeai/backend/image_util/mlsd/",           # External code
  "invokeai/backend/image_util/normal_bae/",     # External code
  "invokeai/backend/image_util/pidi/",           # External code
  "invokeai/backend/image_util/imwatermark/",    # External code
]

[tool.ruff.lint]
ignore = [
  "E501", # https://docs.astral.sh/ruff/rules/line-too-long/
  "C901", # https://docs.astral.sh/ruff/rules/complex-structure/
  "B008", # https://docs.astral.sh/ruff/rules/function-call-in-default-argument/
  "B904", # https://docs.astral.sh/ruff/rules/raise-without-from-inside-except/
]
select = ["B", "C", "E", "F", "W", "I", "TID"]

[tool.ruff.lint.flake8-tidy-imports]
# Disallow all relative imports.
ban-relative-imports = "all"

#=== End: Ruff

#=== Begin: MyPy

# global mypy config
[tool.mypy]
ignore_missing_imports = true # ignores missing types in third-party libraries
strict = true
plugins = "pydantic.mypy"
exclude = ["tests/*"]

# overrides for specific modules
[[tool.mypy.overrides]]
follow_imports = "skip" # skips type checking of the modules listed below
module = [
  "invokeai.app.api.routers.models",
  "invokeai.app.invocations.compel",
  "invokeai.app.invocations.denoise_latents",
  "invokeai.app.services.invocation_stats.invocation_stats_default",
  "invokeai.app.services.model_manager.model_manager_base",
  "invokeai.app.services.model_manager.model_manager_default",
  "invokeai.app.services.model_manager.store.model_records_sql",
  "invokeai.app.util.controlnet_utils",
  "invokeai.backend.image_util.txt2mask",
  "invokeai.backend.image_util.safety_checker",
  "invokeai.backend.image_util.patchmatch",
  "invokeai.backend.image_util.invisible_watermark",
  "invokeai.backend.install.model_install_backend",
  "invokeai.backend.ip_adapter.ip_adapter",
  "invokeai.backend.ip_adapter.resampler",
  "invokeai.backend.ip_adapter.unet_patcher",
  "invokeai.backend.model_management.convert_ckpt_to_diffusers",
  "invokeai.backend.model_management.lora",
  "invokeai.backend.model_management.model_cache",
  "invokeai.backend.model_management.model_manager",
  "invokeai.backend.model_management.model_merge",
  "invokeai.backend.model_management.model_probe",
  "invokeai.backend.model_management.model_search",
  "invokeai.backend.model_management.models.*",                            # this is needed to ignore the module's `__init__.py`
  "invokeai.backend.model_management.models.base",
  "invokeai.backend.model_management.models.controlnet",
  "invokeai.backend.model_management.models.ip_adapter",
  "invokeai.backend.model_management.models.lora",
  "invokeai.backend.model_management.models.sdxl",
  "invokeai.backend.model_management.models.stable_diffusion",
  "invokeai.backend.model_management.models.vae",
  "invokeai.backend.model_management.seamless",
  "invokeai.backend.model_management.util",
  "invokeai.backend.stable_diffusion.diffusers_pipeline",
  "invokeai.backend.stable_diffusion.diffusion.shared_invokeai_diffusion",
  "invokeai.backend.util.hotfixes",
  "invokeai.backend.util.mps_fixes",
  "invokeai.backend.util.util",
  "invokeai.frontend.install.model_install",
]
#=== End: MyPy

[tool.pyright]
# Start from strict mode
typeCheckingMode = "strict"
# This errors whenever an import is missing a type stub file - way too noisy
reportMissingTypeStubs = "none"
# These are the rest of the rules enabled by strict mode - enable them @ warning
reportConstantRedefinition = "warning"
reportDeprecated = "warning"
reportDuplicateImport = "warning"
reportIncompleteStub = "warning"
reportInconsistentConstructor = "warning"
reportInvalidStubStatement = "warning"
reportMatchNotExhaustive = "warning"
reportMissingParameterType = "warning"
reportMissingTypeArgument = "warning"
reportPrivateUsage = "warning"
reportTypeCommentUsage = "warning"
reportUnknownArgumentType = "warning"
reportUnknownLambdaType = "warning"
reportUnknownMemberType = "warning"
reportUnknownParameterType = "warning"
reportUnknownVariableType = "warning"
reportUnnecessaryCast = "warning"
reportUnnecessaryComparison = "warning"
reportUnnecessaryContains = "warning"
reportUnnecessaryIsInstance = "warning"
reportUnusedClass = "warning"
reportUnusedImport = "warning"
reportUnusedFunction = "warning"
reportUnusedVariable = "warning"
reportUntypedBaseClass = "warning"
reportUntypedClassDecorator = "warning"
reportUntypedFunctionDecorator = "warning"
reportUntypedNamedTuple = "warning"