merge with main

This commit is contained in:
Lincoln Stein 2023-12-19 17:04:30 -05:00
commit 87a5b771c4
13 changed files with 264 additions and 134 deletions

View File

@ -26,7 +26,7 @@ from invokeai.backend.model_manager.config import (
from ..dependencies import ApiDependencies from ..dependencies import ApiDependencies
model_records_router = APIRouter(prefix="/v1/model/record", tags=["model_manager_v2"]) model_records_router = APIRouter(prefix="/v1/model/record", tags=["model_manager_v2_unstable"])
class ModelsList(BaseModel): class ModelsList(BaseModel):

View File

@ -77,7 +77,7 @@ class CalculateImageTilesInvocation(BaseInvocation):
title="Calculate Image Tiles Even Split", title="Calculate Image Tiles Even Split",
tags=["tiles"], tags=["tiles"],
category="tiles", category="tiles",
version="1.0.0", version="1.1.0",
classification=Classification.Beta, classification=Classification.Beta,
) )
class CalculateImageTilesEvenSplitInvocation(BaseInvocation): class CalculateImageTilesEvenSplitInvocation(BaseInvocation):
@ -97,11 +97,11 @@ class CalculateImageTilesEvenSplitInvocation(BaseInvocation):
ge=1, ge=1,
description="Number of tiles to divide image into on the y axis", description="Number of tiles to divide image into on the y axis",
) )
overlap_fraction: float = InputField( overlap: int = InputField(
default=0.25, default=128,
ge=0, ge=0,
lt=1, multiple_of=8,
description="Overlap between adjacent tiles as a fraction of the tile's dimensions (0-1)", description="The overlap, in pixels, between adjacent tiles.",
) )
def invoke(self, context: InvocationContext) -> CalculateImageTilesOutput: def invoke(self, context: InvocationContext) -> CalculateImageTilesOutput:
@ -110,7 +110,7 @@ class CalculateImageTilesEvenSplitInvocation(BaseInvocation):
image_width=self.image_width, image_width=self.image_width,
num_tiles_x=self.num_tiles_x, num_tiles_x=self.num_tiles_x,
num_tiles_y=self.num_tiles_y, num_tiles_y=self.num_tiles_y,
overlap_fraction=self.overlap_fraction, overlap=self.overlap,
) )
return CalculateImageTilesOutput(tiles=tiles) return CalculateImageTilesOutput(tiles=tiles)

View File

@ -5,6 +5,7 @@ from invokeai.app.services.image_files.image_files_base import ImageFileStorageB
from invokeai.app.services.shared.sqlite.sqlite_database import SqliteDatabase from invokeai.app.services.shared.sqlite.sqlite_database import SqliteDatabase
from invokeai.app.services.shared.sqlite_migrator.migrations.migration_1 import build_migration_1 from invokeai.app.services.shared.sqlite_migrator.migrations.migration_1 import build_migration_1
from invokeai.app.services.shared.sqlite_migrator.migrations.migration_2 import build_migration_2 from invokeai.app.services.shared.sqlite_migrator.migrations.migration_2 import build_migration_2
from invokeai.app.services.shared.sqlite_migrator.migrations.migration_3 import build_migration_3
from invokeai.app.services.shared.sqlite_migrator.migrations.migration_4 import build_migration_4 from invokeai.app.services.shared.sqlite_migrator.migrations.migration_4 import build_migration_4
from invokeai.app.services.shared.sqlite_migrator.sqlite_migrator_impl import SqliteMigrator from invokeai.app.services.shared.sqlite_migrator.sqlite_migrator_impl import SqliteMigrator
@ -28,6 +29,7 @@ def init_db(config: InvokeAIAppConfig, logger: Logger, image_files: ImageFileSto
migrator = SqliteMigrator(db=db) migrator = SqliteMigrator(db=db)
migrator.register_migration(build_migration_1()) migrator.register_migration(build_migration_1())
migrator.register_migration(build_migration_2(image_files=image_files, logger=logger)) migrator.register_migration(build_migration_2(image_files=image_files, logger=logger))
migrator.register_migration(build_migration_3())
migrator.register_migration(build_migration_4()) migrator.register_migration(build_migration_4())
migrator.run_migrations() migrator.run_migrations()

View File

@ -11,6 +11,8 @@ from invokeai.app.services.workflow_records.workflow_records_common import (
UnsafeWorkflowWithVersionValidator, UnsafeWorkflowWithVersionValidator,
) )
from .util.migrate_yaml_config_1 import MigrateModelYamlToDb1
class Migration2Callback: class Migration2Callback:
def __init__(self, image_files: ImageFileStorageBase, logger: Logger): def __init__(self, image_files: ImageFileStorageBase, logger: Logger):
@ -24,6 +26,7 @@ class Migration2Callback:
self._add_workflow_library(cursor) self._add_workflow_library(cursor)
self._drop_model_manager_metadata(cursor) self._drop_model_manager_metadata(cursor)
self._recreate_model_config(cursor) self._recreate_model_config(cursor)
self._migrate_model_config_records(cursor)
self._migrate_embedded_workflows(cursor) self._migrate_embedded_workflows(cursor)
def _add_images_has_workflow(self, cursor: sqlite3.Cursor) -> None: def _add_images_has_workflow(self, cursor: sqlite3.Cursor) -> None:
@ -131,6 +134,11 @@ class Migration2Callback:
""" """
) )
def _migrate_model_config_records(self, cursor: sqlite3.Cursor) -> None:
"""After updating the model config table, we repopulate it."""
model_record_migrator = MigrateModelYamlToDb1(cursor)
model_record_migrator.migrate()
def _migrate_embedded_workflows(self, cursor: sqlite3.Cursor) -> None: def _migrate_embedded_workflows(self, cursor: sqlite3.Cursor) -> None:
""" """
In the v3.5.0 release, InvokeAI changed how it handles embedded workflows. The `images` table in In the v3.5.0 release, InvokeAI changed how it handles embedded workflows. The `images` table in

View File

@ -0,0 +1,75 @@
import sqlite3
from invokeai.app.services.shared.sqlite_migrator.sqlite_migrator_common import Migration
from .util.migrate_yaml_config_1 import MigrateModelYamlToDb1
class Migration3Callback:
def __init__(self) -> None:
pass
def __call__(self, cursor: sqlite3.Cursor) -> None:
self._drop_model_manager_metadata(cursor)
self._recreate_model_config(cursor)
self._migrate_model_config_records(cursor)
def _drop_model_manager_metadata(self, cursor: sqlite3.Cursor) -> None:
"""Drops the `model_manager_metadata` table."""
cursor.execute("DROP TABLE IF EXISTS model_manager_metadata;")
def _recreate_model_config(self, cursor: sqlite3.Cursor) -> None:
"""
Drops the `model_config` table, recreating it.
In 3.4.0, this table used explicit columns but was changed to use json_extract 3.5.0.
Because this table is not used in production, we are able to simply drop it and recreate it.
"""
cursor.execute("DROP TABLE IF EXISTS model_config;")
cursor.execute(
"""--sql
CREATE TABLE IF NOT EXISTS model_config (
id TEXT NOT NULL PRIMARY KEY,
-- The next 3 fields are enums in python, unrestricted string here
base TEXT GENERATED ALWAYS as (json_extract(config, '$.base')) VIRTUAL NOT NULL,
type TEXT GENERATED ALWAYS as (json_extract(config, '$.type')) VIRTUAL NOT NULL,
name TEXT GENERATED ALWAYS as (json_extract(config, '$.name')) VIRTUAL NOT NULL,
path TEXT GENERATED ALWAYS as (json_extract(config, '$.path')) VIRTUAL NOT NULL,
format TEXT GENERATED ALWAYS as (json_extract(config, '$.format')) VIRTUAL NOT NULL,
original_hash TEXT, -- could be null
-- Serialized JSON representation of the whole config object,
-- which will contain additional fields from subclasses
config TEXT NOT NULL,
created_at DATETIME NOT NULL DEFAULT(STRFTIME('%Y-%m-%d %H:%M:%f', 'NOW')),
-- Updated via trigger
updated_at DATETIME NOT NULL DEFAULT(STRFTIME('%Y-%m-%d %H:%M:%f', 'NOW')),
-- unique constraint on combo of name, base and type
UNIQUE(name, base, type)
);
"""
)
def _migrate_model_config_records(self, cursor: sqlite3.Cursor) -> None:
"""After updating the model config table, we repopulate it."""
model_record_migrator = MigrateModelYamlToDb1(cursor)
model_record_migrator.migrate()
def build_migration_3() -> Migration:
"""
Build the migration from database version 2 to 3.
This migration does the following:
- Drops the `model_config` table, recreating it
- Migrates data from `models.yaml` into the `model_config` table
"""
migration_3 = Migration(
from_version=2,
to_version=3,
callback=Migration3Callback(),
)
return migration_3

View File

@ -86,8 +86,8 @@ def build_migration_4() -> Migration:
Adds the tables needed to store model metadata and tags. Adds the tables needed to store model metadata and tags.
""" """
migration_4 = Migration( migration_4 = Migration(
from_version=2, # until migration_3 is merged, pretend we are doing 2-3 from_version=3,
to_version=3, to_version=4,
callback=Migration4Callback(), callback=Migration4Callback(),
) )

View File

@ -1,8 +1,12 @@
# Copyright (c) 2023 Lincoln D. Stein # Copyright (c) 2023 Lincoln D. Stein
"""Migrate from the InvokeAI v2 models.yaml format to the v3 sqlite format.""" """Migrate from the InvokeAI v2 models.yaml format to the v3 sqlite format."""
import json
import sqlite3
from hashlib import sha1 from hashlib import sha1
from logging import Logger from logging import Logger
from pathlib import Path
from typing import Optional
from omegaconf import DictConfig, OmegaConf from omegaconf import DictConfig, OmegaConf
from pydantic import TypeAdapter from pydantic import TypeAdapter
@ -10,13 +14,12 @@ from pydantic import TypeAdapter
from invokeai.app.services.config import InvokeAIAppConfig from invokeai.app.services.config import InvokeAIAppConfig
from invokeai.app.services.model_records import ( from invokeai.app.services.model_records import (
DuplicateModelException, DuplicateModelException,
ModelRecordServiceSQL,
UnknownModelException, UnknownModelException,
) )
from invokeai.app.services.shared.sqlite.sqlite_database import SqliteDatabase
from invokeai.backend.model_manager.config import ( from invokeai.backend.model_manager.config import (
AnyModelConfig, AnyModelConfig,
BaseModelType, BaseModelType,
ModelConfigFactory,
ModelType, ModelType,
) )
from invokeai.backend.model_manager.hash import FastModelHash from invokeai.backend.model_manager.hash import FastModelHash
@ -25,9 +28,9 @@ from invokeai.backend.util.logging import InvokeAILogger
ModelsValidator = TypeAdapter(AnyModelConfig) ModelsValidator = TypeAdapter(AnyModelConfig)
class MigrateModelYamlToDb: class MigrateModelYamlToDb1:
""" """
Migrate the InvokeAI models.yaml format (VERSION 3.0.0) to SQL3 database format (VERSION 3.2.0) Migrate the InvokeAI models.yaml format (VERSION 3.0.0) to SQL3 database format (VERSION 3.5.0).
The class has one externally useful method, migrate(), which scans the The class has one externally useful method, migrate(), which scans the
currently models.yaml file and imports all its entries into invokeai.db. currently models.yaml file and imports all its entries into invokeai.db.
@ -41,17 +44,13 @@ class MigrateModelYamlToDb:
config: InvokeAIAppConfig config: InvokeAIAppConfig
logger: Logger logger: Logger
cursor: sqlite3.Cursor
def __init__(self) -> None: def __init__(self, cursor: sqlite3.Cursor = None) -> None:
self.config = InvokeAIAppConfig.get_config() self.config = InvokeAIAppConfig.get_config()
self.config.parse_args() self.config.parse_args()
self.logger = InvokeAILogger.get_logger() self.logger = InvokeAILogger.get_logger()
self.cursor = cursor
def get_db(self) -> ModelRecordServiceSQL:
"""Fetch the sqlite3 database for this installation."""
db_path = None if self.config.use_memory_db else self.config.db_path
db = SqliteDatabase(db_path=db_path, logger=self.logger, verbose=self.config.log_sql)
return ModelRecordServiceSQL(db)
def get_yaml(self) -> DictConfig: def get_yaml(self) -> DictConfig:
"""Fetch the models.yaml DictConfig for this installation.""" """Fetch the models.yaml DictConfig for this installation."""
@ -62,8 +61,10 @@ class MigrateModelYamlToDb:
def migrate(self) -> None: def migrate(self) -> None:
"""Do the migration from models.yaml to invokeai.db.""" """Do the migration from models.yaml to invokeai.db."""
db = self.get_db() try:
yaml = self.get_yaml() yaml = self.get_yaml()
except OSError:
return
for model_key, stanza in yaml.items(): for model_key, stanza in yaml.items():
if model_key == "__metadata__": if model_key == "__metadata__":
@ -86,22 +87,62 @@ class MigrateModelYamlToDb:
new_config: AnyModelConfig = ModelsValidator.validate_python(stanza) # type: ignore # see https://github.com/pydantic/pydantic/discussions/7094 new_config: AnyModelConfig = ModelsValidator.validate_python(stanza) # type: ignore # see https://github.com/pydantic/pydantic/discussions/7094
try: try:
if original_record := db.search_by_path(stanza.path): if original_record := self._search_by_path(stanza.path):
key = original_record[0].key key = original_record.key
self.logger.info(f"Updating model {model_name} with information from models.yaml using key {key}") self.logger.info(f"Updating model {model_name} with information from models.yaml using key {key}")
db.update_model(key, new_config) self._update_model(key, new_config)
else: else:
self.logger.info(f"Adding model {model_name} with key {model_key}") self.logger.info(f"Adding model {model_name} with key {model_key}")
db.add_model(new_key, new_config) self._add_model(new_key, new_config)
except DuplicateModelException: except DuplicateModelException:
self.logger.warning(f"Model {model_name} is already in the database") self.logger.warning(f"Model {model_name} is already in the database")
except UnknownModelException: except UnknownModelException:
self.logger.warning(f"Model at {stanza.path} could not be found in database") self.logger.warning(f"Model at {stanza.path} could not be found in database")
def _search_by_path(self, path: Path) -> Optional[AnyModelConfig]:
self.cursor.execute(
"""--sql
SELECT config FROM model_config
WHERE path=?;
""",
(str(path),),
)
results = [ModelConfigFactory.make_config(json.loads(x[0])) for x in self.cursor.fetchall()]
return results[0] if results else None
def main(): def _update_model(self, key: str, config: AnyModelConfig) -> None:
MigrateModelYamlToDb().migrate() record = ModelConfigFactory.make_config(config, key=key) # ensure it is a valid config obect
json_serialized = record.model_dump_json() # and turn it into a json string.
self.cursor.execute(
"""--sql
UPDATE model_config
SET
config=?
WHERE id=?;
""",
(json_serialized, key),
)
if self.cursor.rowcount == 0:
raise UnknownModelException("model not found")
def _add_model(self, key: str, config: AnyModelConfig) -> None:
if __name__ == "__main__": record = ModelConfigFactory.make_config(config, key=key) # ensure it is a valid config obect.
main() json_serialized = record.model_dump_json() # and turn it into a json string.
try:
self.cursor.execute(
"""--sql
INSERT INTO model_config (
id,
original_hash,
config
)
VALUES (?,?,?);
""",
(
key,
record.original_hash,
json_serialized,
),
)
except sqlite3.IntegrityError as exc:
raise DuplicateModelException(f"{record.name}: model is already in database") from exc

View File

@ -389,7 +389,7 @@ class TextualInversionCheckpointProbe(CheckpointProbeBase):
elif "clip_g" in checkpoint: elif "clip_g" in checkpoint:
token_dim = checkpoint["clip_g"].shape[-1] token_dim = checkpoint["clip_g"].shape[-1]
else: else:
token_dim = list(checkpoint.values())[0].shape[0] token_dim = list(checkpoint.values())[0].shape[-1]
if token_dim == 768: if token_dim == 768:
return BaseModelType.StableDiffusion1 return BaseModelType.StableDiffusion1
elif token_dim == 1024: elif token_dim == 1024:

View File

@ -102,7 +102,7 @@ def calc_tiles_with_overlap(
def calc_tiles_even_split( def calc_tiles_even_split(
image_height: int, image_width: int, num_tiles_x: int, num_tiles_y: int, overlap_fraction: float = 0 image_height: int, image_width: int, num_tiles_x: int, num_tiles_y: int, overlap: int = 0
) -> list[Tile]: ) -> list[Tile]:
"""Calculate the tile coordinates for a given image shape with the number of tiles requested. """Calculate the tile coordinates for a given image shape with the number of tiles requested.
@ -111,31 +111,35 @@ def calc_tiles_even_split(
image_width (int): The image width in px. image_width (int): The image width in px.
num_x_tiles (int): The number of tile to split the image into on the X-axis. num_x_tiles (int): The number of tile to split the image into on the X-axis.
num_y_tiles (int): The number of tile to split the image into on the Y-axis. num_y_tiles (int): The number of tile to split the image into on the Y-axis.
overlap_fraction (float, optional): The target overlap as fraction of the tiles size. Defaults to 0. overlap (int, optional): The overlap between adjacent tiles in pixels. Defaults to 0.
Returns: Returns:
list[Tile]: A list of tiles that cover the image shape. Ordered from left-to-right, top-to-bottom. list[Tile]: A list of tiles that cover the image shape. Ordered from left-to-right, top-to-bottom.
""" """
# Ensure the image is divisible by LATENT_SCALE_FACTOR
# Ensure tile size is divisible by 8
if image_width % LATENT_SCALE_FACTOR != 0 or image_height % LATENT_SCALE_FACTOR != 0: if image_width % LATENT_SCALE_FACTOR != 0 or image_height % LATENT_SCALE_FACTOR != 0:
raise ValueError(f"image size (({image_width}, {image_height})) must be divisible by {LATENT_SCALE_FACTOR}") raise ValueError(f"image size (({image_width}, {image_height})) must be divisible by {LATENT_SCALE_FACTOR}")
# Calculate the overlap size based on the percentage and adjust it to be divisible by 8 (rounding up)
overlap_x = LATENT_SCALE_FACTOR * math.ceil(
int((image_width / num_tiles_x) * overlap_fraction) / LATENT_SCALE_FACTOR
)
overlap_y = LATENT_SCALE_FACTOR * math.ceil(
int((image_height / num_tiles_y) * overlap_fraction) / LATENT_SCALE_FACTOR
)
# Calculate the tile size based on the number of tiles and overlap, and ensure it's divisible by 8 (rounding down) # Calculate the tile size based on the number of tiles and overlap, and ensure it's divisible by 8 (rounding down)
if num_tiles_x > 1:
# ensure the overlap is not more than the maximum overlap if we only have 1 tile then we dont care about overlap
assert overlap <= image_width - (LATENT_SCALE_FACTOR * (num_tiles_x - 1))
tile_size_x = LATENT_SCALE_FACTOR * math.floor( tile_size_x = LATENT_SCALE_FACTOR * math.floor(
((image_width + overlap_x * (num_tiles_x - 1)) // num_tiles_x) / LATENT_SCALE_FACTOR ((image_width + overlap * (num_tiles_x - 1)) // num_tiles_x) / LATENT_SCALE_FACTOR
) )
assert overlap < tile_size_x
else:
tile_size_x = image_width
if num_tiles_y > 1:
# ensure the overlap is not more than the maximum overlap if we only have 1 tile then we dont care about overlap
assert overlap <= image_height - (LATENT_SCALE_FACTOR * (num_tiles_y - 1))
tile_size_y = LATENT_SCALE_FACTOR * math.floor( tile_size_y = LATENT_SCALE_FACTOR * math.floor(
((image_height + overlap_y * (num_tiles_y - 1)) // num_tiles_y) / LATENT_SCALE_FACTOR ((image_height + overlap * (num_tiles_y - 1)) // num_tiles_y) / LATENT_SCALE_FACTOR
) )
assert overlap < tile_size_y
else:
tile_size_y = image_height
# tiles[y * num_tiles_x + x] is the tile for the y'th row, x'th column. # tiles[y * num_tiles_x + x] is the tile for the y'th row, x'th column.
tiles: list[Tile] = [] tiles: list[Tile] = []
@ -143,7 +147,7 @@ def calc_tiles_even_split(
# Calculate tile coordinates. (Ignore overlap values for now.) # Calculate tile coordinates. (Ignore overlap values for now.)
for tile_idx_y in range(num_tiles_y): for tile_idx_y in range(num_tiles_y):
# Calculate the top and bottom of the row # Calculate the top and bottom of the row
top = tile_idx_y * (tile_size_y - overlap_y) top = tile_idx_y * (tile_size_y - overlap)
bottom = min(top + tile_size_y, image_height) bottom = min(top + tile_size_y, image_height)
# For the last row adjust bottom to be the height of the image # For the last row adjust bottom to be the height of the image
if tile_idx_y == num_tiles_y - 1: if tile_idx_y == num_tiles_y - 1:
@ -151,7 +155,7 @@ def calc_tiles_even_split(
for tile_idx_x in range(num_tiles_x): for tile_idx_x in range(num_tiles_x):
# Calculate the left & right coordinate of each tile # Calculate the left & right coordinate of each tile
left = tile_idx_x * (tile_size_x - overlap_x) left = tile_idx_x * (tile_size_x - overlap)
right = min(left + tile_size_x, image_width) right = min(left + tile_size_x, image_width)
# For the last tile in the row adjust right to be the width of the image # For the last tile in the row adjust right to be the width of the image
if tile_idx_x == num_tiles_x - 1: if tile_idx_x == num_tiles_x - 1:

View File

@ -4,6 +4,7 @@ pip install <path_to_git_source>.
""" """
import os import os
import platform import platform
from distutils.version import LooseVersion
import pkg_resources import pkg_resources
import psutil import psutil
@ -31,10 +32,6 @@ else:
console = Console(style=Style(color="grey74", bgcolor="grey19")) console = Console(style=Style(color="grey74", bgcolor="grey19"))
def get_versions() -> dict:
return requests.get(url=INVOKE_AI_REL).json()
def invokeai_is_running() -> bool: def invokeai_is_running() -> bool:
for p in psutil.process_iter(): for p in psutil.process_iter():
try: try:
@ -50,6 +47,20 @@ def invokeai_is_running() -> bool:
return False return False
def get_pypi_versions():
url = "https://pypi.org/pypi/invokeai/json"
try:
data = requests.get(url).json()
except Exception:
raise Exception("Unable to fetch version information from PyPi")
versions = list(data["releases"].keys())
versions.sort(key=LooseVersion, reverse=True)
latest_version = [v for v in versions if "rc" not in v][0]
latest_release_candidate = [v for v in versions if "rc" in v][0]
return latest_version, latest_release_candidate, versions
def welcome(latest_release: str, latest_prerelease: str): def welcome(latest_release: str, latest_prerelease: str):
@group() @group()
def text(): def text():
@ -63,8 +74,7 @@ def welcome(latest_release: str, latest_prerelease: str):
yield "[bold yellow]Options:" yield "[bold yellow]Options:"
yield f"""[1] Update to the latest [bold]official release[/bold] ([italic]{latest_release}[/italic]) yield f"""[1] Update to the latest [bold]official release[/bold] ([italic]{latest_release}[/italic])
[2] Update to the latest [bold]pre-release[/bold] (may be buggy; caveat emptor!) ([italic]{latest_prerelease}[/italic]) [2] Update to the latest [bold]pre-release[/bold] (may be buggy; caveat emptor!) ([italic]{latest_prerelease}[/italic])
[3] Manually enter the [bold]tag name[/bold] for the version you wish to update to [3] Manually enter the [bold]version[/bold] you wish to update to"""
[4] Manually enter the [bold]branch name[/bold] for the version you wish to update to"""
console.rule() console.rule()
print( print(
@ -92,44 +102,35 @@ def get_extras():
def main(): def main():
versions = get_versions()
released_versions = [x for x in versions if not (x["draft"] or x["prerelease"])]
prerelease_versions = [x for x in versions if not x["draft"] and x["prerelease"]]
latest_release = released_versions[0]["tag_name"] if len(released_versions) else None
latest_prerelease = prerelease_versions[0]["tag_name"] if len(prerelease_versions) else None
if invokeai_is_running(): if invokeai_is_running():
print(":exclamation: [bold red]Please terminate all running instances of InvokeAI before updating.[/red bold]") print(":exclamation: [bold red]Please terminate all running instances of InvokeAI before updating.[/red bold]")
input("Press any key to continue...") input("Press any key to continue...")
return return
latest_release, latest_prerelease, versions = get_pypi_versions()
welcome(latest_release, latest_prerelease) welcome(latest_release, latest_prerelease)
tag = None release = latest_release
branch = None choice = Prompt.ask("Choice:", choices=["1", "2", "3"], default="1")
release = None
choice = Prompt.ask("Choice:", choices=["1", "2", "3", "4"], default="1")
if choice == "1": if choice == "1":
release = latest_release release = latest_release
elif choice == "2": elif choice == "2":
release = latest_prerelease release = latest_prerelease
elif choice == "3": elif choice == "3":
while not tag: while True:
tag = Prompt.ask("Enter an InvokeAI tag name") release = Prompt.ask("Enter an InvokeAI version")
elif choice == "4": release.strip()
while not branch: if release in versions:
branch = Prompt.ask("Enter an InvokeAI branch name") break
print(f":exclamation: [bold red]'{release}' is not a recognized InvokeAI release.[/red bold]")
extras = get_extras() extras = get_extras()
print(f":crossed_fingers: Upgrading to [yellow]{tag or release or branch}[/yellow]") print(f":crossed_fingers: Upgrading to [yellow]{release}[/yellow]")
if release: cmd = f'pip install "invokeai{extras}=={release}" --use-pep517 --upgrade'
cmd = f'pip install "invokeai{extras} @ {INVOKE_AI_SRC}/{release}.zip" --use-pep517 --upgrade'
elif tag:
cmd = f'pip install "invokeai{extras} @ {INVOKE_AI_TAG}/{tag}.zip" --use-pep517 --upgrade'
else:
cmd = f'pip install "invokeai{extras} @ {INVOKE_AI_BRANCH}/{branch}.zip" --use-pep517 --upgrade'
print("") print("")
print("") print("")
if os.system(cmd) == 0: if os.system(cmd) == 0:

View File

@ -1109,7 +1109,10 @@
"deletedInvalidEdge": "Eliminata connessione non valida {{source}} -> {{target}}", "deletedInvalidEdge": "Eliminata connessione non valida {{source}} -> {{target}}",
"unknownInput": "Input sconosciuto: {{name}}", "unknownInput": "Input sconosciuto: {{name}}",
"prototypeDesc": "Questa invocazione è un prototipo. Potrebbe subire modifiche sostanziali durante gli aggiornamenti dell'app e potrebbe essere rimossa in qualsiasi momento.", "prototypeDesc": "Questa invocazione è un prototipo. Potrebbe subire modifiche sostanziali durante gli aggiornamenti dell'app e potrebbe essere rimossa in qualsiasi momento.",
"betaDesc": "Questa invocazione è in versione beta. Fino a quando non sarà stabile, potrebbe subire modifiche importanti durante gli aggiornamenti dell'app. Abbiamo intenzione di supportare questa invocazione a lungo termine." "betaDesc": "Questa invocazione è in versione beta. Fino a quando non sarà stabile, potrebbe subire modifiche importanti durante gli aggiornamenti dell'app. Abbiamo intenzione di supportare questa invocazione a lungo termine.",
"newWorkflow": "Nuovo flusso di lavoro",
"newWorkflowDesc": "Creare un nuovo flusso di lavoro?",
"newWorkflowDesc2": "Il flusso di lavoro attuale presenta modifiche non salvate."
}, },
"boards": { "boards": {
"autoAddBoard": "Aggiungi automaticamente bacheca", "autoAddBoard": "Aggiungi automaticamente bacheca",
@ -1629,7 +1632,10 @@
"deleteWorkflow": "Elimina flusso di lavoro", "deleteWorkflow": "Elimina flusso di lavoro",
"workflows": "Flussi di lavoro", "workflows": "Flussi di lavoro",
"noDescription": "Nessuna descrizione", "noDescription": "Nessuna descrizione",
"userWorkflows": "I miei flussi di lavoro" "userWorkflows": "I miei flussi di lavoro",
"newWorkflowCreated": "Nuovo flusso di lavoro creato",
"downloadWorkflow": "Salva su file",
"uploadWorkflow": "Carica da file"
}, },
"app": { "app": {
"storeNotInitialized": "Il negozio non è inizializzato" "storeNotInitialized": "Il negozio non è inizializzato"

View File

@ -138,7 +138,6 @@ dependencies = [
"invokeai-node-web" = "invokeai.app.api_app:invoke_api" "invokeai-node-web" = "invokeai.app.api_app:invoke_api"
"invokeai-import-images" = "invokeai.frontend.install.import_images:main" "invokeai-import-images" = "invokeai.frontend.install.import_images:main"
"invokeai-db-maintenance" = "invokeai.backend.util.db_maintenance:main" "invokeai-db-maintenance" = "invokeai.backend.util.db_maintenance:main"
"invokeai-migrate-models-to-db" = "invokeai.backend.model_manager.migrate_to_db:main"
[project.urls] [project.urls]
"Homepage" = "https://invoke-ai.github.io/InvokeAI/" "Homepage" = "https://invoke-ai.github.io/InvokeAI/"

View File

@ -305,9 +305,7 @@ def test_calc_tiles_min_overlap_input_validation(
def test_calc_tiles_even_split_single_tile(): def test_calc_tiles_even_split_single_tile():
"""Test calc_tiles_even_split() behavior when a single tile covers the image.""" """Test calc_tiles_even_split() behavior when a single tile covers the image."""
tiles = calc_tiles_even_split( tiles = calc_tiles_even_split(image_height=512, image_width=1024, num_tiles_x=1, num_tiles_y=1, overlap=64)
image_height=512, image_width=1024, num_tiles_x=1, num_tiles_y=1, overlap_fraction=0.25
)
expected_tiles = [ expected_tiles = [
Tile( Tile(
@ -322,36 +320,34 @@ def test_calc_tiles_even_split_single_tile():
def test_calc_tiles_even_split_evenly_divisible(): def test_calc_tiles_even_split_evenly_divisible():
"""Test calc_tiles_even_split() behavior when the image is evenly covered by multiple tiles.""" """Test calc_tiles_even_split() behavior when the image is evenly covered by multiple tiles."""
# Parameters mimic roughly the same output as the original tile generations of the same test name # Parameters mimic roughly the same output as the original tile generations of the same test name
tiles = calc_tiles_even_split( tiles = calc_tiles_even_split(image_height=576, image_width=1600, num_tiles_x=3, num_tiles_y=2, overlap=64)
image_height=576, image_width=1600, num_tiles_x=3, num_tiles_y=2, overlap_fraction=0.25
)
expected_tiles = [ expected_tiles = [
# Row 0 # Row 0
Tile( Tile(
coords=TBLR(top=0, bottom=320, left=0, right=624), coords=TBLR(top=0, bottom=320, left=0, right=576),
overlap=TBLR(top=0, bottom=72, left=0, right=136), overlap=TBLR(top=0, bottom=64, left=0, right=64),
), ),
Tile( Tile(
coords=TBLR(top=0, bottom=320, left=488, right=1112), coords=TBLR(top=0, bottom=320, left=512, right=1088),
overlap=TBLR(top=0, bottom=72, left=136, right=136), overlap=TBLR(top=0, bottom=64, left=64, right=64),
), ),
Tile( Tile(
coords=TBLR(top=0, bottom=320, left=976, right=1600), coords=TBLR(top=0, bottom=320, left=1024, right=1600),
overlap=TBLR(top=0, bottom=72, left=136, right=0), overlap=TBLR(top=0, bottom=64, left=64, right=0),
), ),
# Row 1 # Row 1
Tile( Tile(
coords=TBLR(top=248, bottom=576, left=0, right=624), coords=TBLR(top=256, bottom=576, left=0, right=576),
overlap=TBLR(top=72, bottom=0, left=0, right=136), overlap=TBLR(top=64, bottom=0, left=0, right=64),
), ),
Tile( Tile(
coords=TBLR(top=248, bottom=576, left=488, right=1112), coords=TBLR(top=256, bottom=576, left=512, right=1088),
overlap=TBLR(top=72, bottom=0, left=136, right=136), overlap=TBLR(top=64, bottom=0, left=64, right=64),
), ),
Tile( Tile(
coords=TBLR(top=248, bottom=576, left=976, right=1600), coords=TBLR(top=256, bottom=576, left=1024, right=1600),
overlap=TBLR(top=72, bottom=0, left=136, right=0), overlap=TBLR(top=64, bottom=0, left=64, right=0),
), ),
] ]
assert tiles == expected_tiles assert tiles == expected_tiles
@ -360,36 +356,34 @@ def test_calc_tiles_even_split_evenly_divisible():
def test_calc_tiles_even_split_not_evenly_divisible(): def test_calc_tiles_even_split_not_evenly_divisible():
"""Test calc_tiles_even_split() behavior when the image requires 'uneven' overlaps to achieve proper coverage.""" """Test calc_tiles_even_split() behavior when the image requires 'uneven' overlaps to achieve proper coverage."""
# Parameters mimic roughly the same output as the original tile generations of the same test name # Parameters mimic roughly the same output as the original tile generations of the same test name
tiles = calc_tiles_even_split( tiles = calc_tiles_even_split(image_height=400, image_width=1200, num_tiles_x=3, num_tiles_y=2, overlap=64)
image_height=400, image_width=1200, num_tiles_x=3, num_tiles_y=2, overlap_fraction=0.25
)
expected_tiles = [ expected_tiles = [
# Row 0 # Row 0
Tile( Tile(
coords=TBLR(top=0, bottom=224, left=0, right=464), coords=TBLR(top=0, bottom=232, left=0, right=440),
overlap=TBLR(top=0, bottom=56, left=0, right=104), overlap=TBLR(top=0, bottom=64, left=0, right=64),
), ),
Tile( Tile(
coords=TBLR(top=0, bottom=224, left=360, right=824), coords=TBLR(top=0, bottom=232, left=376, right=816),
overlap=TBLR(top=0, bottom=56, left=104, right=104), overlap=TBLR(top=0, bottom=64, left=64, right=64),
), ),
Tile( Tile(
coords=TBLR(top=0, bottom=224, left=720, right=1200), coords=TBLR(top=0, bottom=232, left=752, right=1200),
overlap=TBLR(top=0, bottom=56, left=104, right=0), overlap=TBLR(top=0, bottom=64, left=64, right=0),
), ),
# Row 1 # Row 1
Tile( Tile(
coords=TBLR(top=168, bottom=400, left=0, right=464), coords=TBLR(top=168, bottom=400, left=0, right=440),
overlap=TBLR(top=56, bottom=0, left=0, right=104), overlap=TBLR(top=64, bottom=0, left=0, right=64),
), ),
Tile( Tile(
coords=TBLR(top=168, bottom=400, left=360, right=824), coords=TBLR(top=168, bottom=400, left=376, right=816),
overlap=TBLR(top=56, bottom=0, left=104, right=104), overlap=TBLR(top=64, bottom=0, left=64, right=64),
), ),
Tile( Tile(
coords=TBLR(top=168, bottom=400, left=720, right=1200), coords=TBLR(top=168, bottom=400, left=752, right=1200),
overlap=TBLR(top=56, bottom=0, left=104, right=0), overlap=TBLR(top=64, bottom=0, left=64, right=0),
), ),
] ]
@ -399,28 +393,26 @@ def test_calc_tiles_even_split_not_evenly_divisible():
def test_calc_tiles_even_split_difficult_size(): def test_calc_tiles_even_split_difficult_size():
"""Test calc_tiles_even_split() behavior when the image is a difficult size to spilt evenly and keep div8.""" """Test calc_tiles_even_split() behavior when the image is a difficult size to spilt evenly and keep div8."""
# Parameters are a difficult size for other tile gen routines to calculate # Parameters are a difficult size for other tile gen routines to calculate
tiles = calc_tiles_even_split( tiles = calc_tiles_even_split(image_height=1000, image_width=1000, num_tiles_x=2, num_tiles_y=2, overlap=64)
image_height=1000, image_width=1000, num_tiles_x=2, num_tiles_y=2, overlap_fraction=0.25
)
expected_tiles = [ expected_tiles = [
# Row 0 # Row 0
Tile( Tile(
coords=TBLR(top=0, bottom=560, left=0, right=560), coords=TBLR(top=0, bottom=528, left=0, right=528),
overlap=TBLR(top=0, bottom=128, left=0, right=128), overlap=TBLR(top=0, bottom=64, left=0, right=64),
), ),
Tile( Tile(
coords=TBLR(top=0, bottom=560, left=432, right=1000), coords=TBLR(top=0, bottom=528, left=464, right=1000),
overlap=TBLR(top=0, bottom=128, left=128, right=0), overlap=TBLR(top=0, bottom=64, left=64, right=0),
), ),
# Row 1 # Row 1
Tile( Tile(
coords=TBLR(top=432, bottom=1000, left=0, right=560), coords=TBLR(top=464, bottom=1000, left=0, right=528),
overlap=TBLR(top=128, bottom=0, left=0, right=128), overlap=TBLR(top=64, bottom=0, left=0, right=64),
), ),
Tile( Tile(
coords=TBLR(top=432, bottom=1000, left=432, right=1000), coords=TBLR(top=464, bottom=1000, left=464, right=1000),
overlap=TBLR(top=128, bottom=0, left=128, right=0), overlap=TBLR(top=64, bottom=0, left=64, right=0),
), ),
] ]
@ -428,11 +420,13 @@ def test_calc_tiles_even_split_difficult_size():
@pytest.mark.parametrize( @pytest.mark.parametrize(
["image_height", "image_width", "num_tiles_x", "num_tiles_y", "overlap_fraction", "raises"], ["image_height", "image_width", "num_tiles_x", "num_tiles_y", "overlap", "raises"],
[ [
(128, 128, 1, 1, 0.25, False), # OK (128, 128, 1, 1, 127, False), # OK
(128, 128, 1, 1, 0, False), # OK (128, 128, 1, 1, 0, False), # OK
(128, 128, 2, 1, 0, False), # OK (128, 128, 2, 2, 0, False), # OK
(128, 128, 2, 1, 120, True), # overlap equals tile_height.
(128, 128, 1, 2, 120, True), # overlap equals tile_width.
(127, 127, 1, 1, 0, True), # image size must be dividable by 8 (127, 127, 1, 1, 0, True), # image size must be dividable by 8
], ],
) )
@ -441,15 +435,15 @@ def test_calc_tiles_even_split_input_validation(
image_width: int, image_width: int,
num_tiles_x: int, num_tiles_x: int,
num_tiles_y: int, num_tiles_y: int,
overlap_fraction: float, overlap: int,
raises: bool, raises: bool,
): ):
"""Test that calc_tiles_even_split() raises an exception if the inputs are invalid.""" """Test that calc_tiles_even_split() raises an exception if the inputs are invalid."""
if raises: if raises:
with pytest.raises(ValueError): with pytest.raises((AssertionError, ValueError)):
calc_tiles_even_split(image_height, image_width, num_tiles_x, num_tiles_y, overlap_fraction) calc_tiles_even_split(image_height, image_width, num_tiles_x, num_tiles_y, overlap)
else: else:
calc_tiles_even_split(image_height, image_width, num_tiles_x, num_tiles_y, overlap_fraction) calc_tiles_even_split(image_height, image_width, num_tiles_x, num_tiles_y, overlap)
############################################# #############################################