diff --git a/CHANGES.md b/CHANGES.md
index 074506f1..bde70f1e 100644
--- a/CHANGES.md
+++ b/CHANGES.md
@@ -21,6 +21,9 @@
Our focus continues to remain on an easy installation experience, and an easy user-interface. While still remaining pretty powerful, in terms of features and speed.
### Detailed changelog
+* 2.5.31 - 6 Apr 2023 - Allow seeds upto `4,294,967,295`. Thanks @ogmaresca.
+* 2.5.31 - 6 Apr 2023 - Buttons to show the previous/next image in the image popup. Thanks @ogmaresca.
+* 2.5.30 - 5 Apr 2023 - Fix a bug where the JPEG image quality wasn't being respected when embedding the metadata into it. Thanks @JeLuf.
* 2.5.30 - 1 Apr 2023 - (beta-only) Slider to control the strength of the LoRA model.
* 2.5.30 - 28 Mar 2023 - Refactor task entry config to use a generating method. Added ability for plugins to easily add to this. Removed confusing sentence from `contributing.md`
* 2.5.30 - 28 Mar 2023 - Allow the user to undo the deletion of tasks or images, instead of showing a pop-up each time. The new `Undo` button will be present at the top of the UI. Thanks @JeLuf.
diff --git a/ui/easydiffusion/server.py b/ui/easydiffusion/server.py
index e27f9c5b..92453917 100644
--- a/ui/easydiffusion/server.py
+++ b/ui/easydiffusion/server.py
@@ -10,7 +10,7 @@ from typing import List, Union
from fastapi import FastAPI, HTTPException
from fastapi.staticfiles import StaticFiles
from starlette.responses import FileResponse, JSONResponse, StreamingResponse
-from pydantic import BaseModel
+from pydantic import BaseModel, Extra
from easydiffusion import app, model_manager, task_manager
from easydiffusion.types import TaskData, GenerateImageRequest, MergeRequest
@@ -44,7 +44,7 @@ class NoCacheStaticFiles(StaticFiles):
return super().is_not_modified(response_headers, request_headers)
-class SetAppConfigRequest(BaseModel):
+class SetAppConfigRequest(BaseModel, extra=Extra.allow):
update_branch: str = None
render_devices: Union[List[str], List[int], str, int] = None
model_vae: str = None
@@ -136,6 +136,10 @@ def set_app_config_internal(req: SetAppConfigRequest):
config["test_diffusers"] = req.test_diffusers
+ for property, property_value in req.dict().items():
+ if property_value is not None and property not in req.__fields__:
+ config[property] = property_value
+
try:
app.setConfig(config)
diff --git a/ui/easydiffusion/utils/save_utils.py b/ui/easydiffusion/utils/save_utils.py
index 950f04b0..71524cbd 100644
--- a/ui/easydiffusion/utils/save_utils.py
+++ b/ui/easydiffusion/utils/save_utils.py
@@ -1,13 +1,18 @@
import os
import time
+import base64
import re
+from easydiffusion import app
from easydiffusion.types import TaskData, GenerateImageRequest
+from functools import reduce
+from datetime import datetime
from sdkit.utils import save_images, save_dicts
from numpy import base_repr
filename_regex = re.compile("[^a-zA-Z0-9._-]")
+img_number_regex = re.compile("([0-9]{5,})")
# keep in sync with `ui/media/js/dnd.js`
TASK_TEXT_MAPPING = {
@@ -28,15 +33,89 @@ TASK_TEXT_MAPPING = {
"use_hypernetwork_model": "Hypernetwork model",
"hypernetwork_strength": "Hypernetwork Strength",
"use_lora_model": "LoRA model",
- # "lora_alpha": "LoRA Strength",
+ "lora_alpha": "LoRA Strength",
}
+time_placeholders = {
+ "$yyyy": "%Y",
+ "$MM": "%m",
+ "$dd": "%d",
+ "$HH": "%H",
+ "$mm": "%M",
+ "$ss": "%S",
+}
+
+other_placeholders = {
+ "$id": lambda req, task_data: filename_regex.sub("_", task_data.session_id),
+ "$p": lambda req, task_data: filename_regex.sub("_", req.prompt)[:50],
+ "$s": lambda req, task_data: str(req.seed),
+}
+
+class ImageNumber:
+ _factory = None
+ _evaluated = False
+
+ def __init__(self, factory):
+ self._factory = factory
+ self._evaluated = None
+ def __call__(self) -> int:
+ if self._evaluated is None:
+ self._evaluated = self._factory()
+ return self._evaluated
+
+def format_placeholders(format: str, req: GenerateImageRequest, task_data: TaskData, now = None):
+ if now is None:
+ now = time.time()
+
+ for placeholder, time_format in time_placeholders.items():
+ if placeholder in format:
+ format = format.replace(placeholder, datetime.fromtimestamp(now).strftime(time_format))
+ for placeholder, replace_func in other_placeholders.items():
+ if placeholder in format:
+ format = format.replace(placeholder, replace_func(req, task_data))
+
+ return format
+
+def format_folder_name(format: str, req: GenerateImageRequest, task_data: TaskData):
+ format = format_placeholders(format, req, task_data)
+ return filename_regex.sub("_", format)
+
+def format_file_name(
+ format: str,
+ req: GenerateImageRequest,
+ task_data: TaskData,
+ now: float,
+ batch_file_number: int,
+ folder_img_number: ImageNumber,
+):
+ format = format_placeholders(format, req, task_data, now)
+
+ if "$n" in format:
+ format = format.replace("$n", f"{folder_img_number():05}")
+
+ if "$tsb64" in format:
+ img_id = base_repr(int(now * 10000), 36)[-7:] + base_repr(int(batch_file_number), 36) # Base 36 conversion, 0-9, A-Z
+ format = format.replace("$tsb64", img_id)
+
+ if "$ts" in format:
+ format = format.replace("$ts", str(int(now * 1000) + batch_file_number))
+
+ return filename_regex.sub("_", format)
def save_images_to_disk(images: list, filtered_images: list, req: GenerateImageRequest, task_data: TaskData):
now = time.time()
- save_dir_path = os.path.join(task_data.save_to_disk_path, filename_regex.sub("_", task_data.session_id))
+ app_config = app.getConfig()
+ folder_format = app_config.get("folder_format", "$id")
+ save_dir_path = os.path.join(task_data.save_to_disk_path, format_folder_name(folder_format, req, task_data))
metadata_entries = get_metadata_entries_for_request(req, task_data)
- make_filename = make_filename_callback(req, now=now)
+ file_number = calculate_img_number(save_dir_path, task_data)
+ make_filename = make_filename_callback(
+ app_config.get("filename_format", "$p_$tsb64"),
+ req,
+ task_data,
+ file_number,
+ now=now,
+ )
if task_data.show_only_filtered_image or filtered_images is images:
save_images(
@@ -58,7 +137,7 @@ def save_images_to_disk(images: list, filtered_images: list, req: GenerateImageR
file_format=task_data.output_format,
)
else:
- make_filter_filename = make_filename_callback(req, now=now, suffix="filtered")
+ make_filter_filename = make_filename_callback(req, task_data, file_number, now=now, suffix="filtered")
save_images(
images,
@@ -105,9 +184,6 @@ def get_metadata_entries_for_request(req: GenerateImageRequest, task_data: TaskD
if task_data.use_lora_model is None:
if "lora_alpha" in metadata:
del metadata["lora_alpha"]
-
- from easydiffusion import app
-
app_config = app.getConfig()
if not app_config.get("test_diffusers", False) and "use_lora_model" in metadata:
del metadata["use_lora_model"]
@@ -133,16 +209,66 @@ def get_printable_request(req: GenerateImageRequest):
return metadata
-def make_filename_callback(req: GenerateImageRequest, suffix=None, now=None):
+def make_filename_callback(
+ filename_format: str,
+ req: GenerateImageRequest,
+ task_data: TaskData,
+ folder_img_number: int,
+ suffix=None,
+ now=None,
+):
if now is None:
now = time.time()
def make_filename(i):
- img_id = base_repr(int(now * 10000), 36)[-7:] + base_repr(int(i),36) # Base 36 conversion, 0-9, A-Z
-
- prompt_flattened = filename_regex.sub("_", req.prompt)[:50]
- name = f"{prompt_flattened}_{img_id}"
+ name = format_file_name(filename_format, req, task_data, now, i, folder_img_number)
name = name if suffix is None else f"{name}_{suffix}"
+
return name
return make_filename
+
+def _calculate_img_number(save_dir_path: str, task_data: TaskData):
+ def get_highest_img_number(accumulator: int, file: os.DirEntry) -> int:
+ if not file.is_file:
+ return accumulator
+
+ if len(list(filter(lambda e: file.name.endswith(e), app.IMAGE_EXTENSIONS))) == 0:
+ return accumulator
+
+ get_highest_img_number.number_of_images = get_highest_img_number.number_of_images + 1
+
+ number_match = img_number_regex.match(file.name)
+ if not number_match:
+ return accumulator
+
+ file_number = number_match.group().lstrip('0')
+
+ # Handle 00000
+ return int(file_number) if file_number else 0
+
+ get_highest_img_number.number_of_images = 0
+
+ highest_file_number = -1
+
+ if os.path.isdir(save_dir_path):
+ existing_files = list(os.scandir(save_dir_path))
+ highest_file_number = reduce(get_highest_img_number, existing_files, -1)
+
+ calculated_img_number = max(highest_file_number, get_highest_img_number.number_of_images - 1)
+
+ if task_data.session_id in _calculate_img_number.session_img_numbers:
+ calculated_img_number = max(
+ _calculate_img_number.session_img_numbers[task_data.session_id],
+ calculated_img_number,
+ )
+
+ calculated_img_number = calculated_img_number + 1
+
+ _calculate_img_number.session_img_numbers[task_data.session_id] = calculated_img_number
+ return calculated_img_number
+
+_calculate_img_number.session_img_numbers = {}
+
+def calculate_img_number(save_dir_path: str, task_data: TaskData):
+ return ImageNumber(lambda: _calculate_img_number(save_dir_path, task_data))
diff --git a/ui/index.html b/ui/index.html
index cb3eae81..7e8680d0 100644
--- a/ui/index.html
+++ b/ui/index.html
@@ -30,7 +30,7 @@
Easy Diffusion
- v2.5.30
+ v2.5.31
@@ -221,7 +221,7 @@
-
+
@@ -293,6 +293,12 @@
+
+ Type a prompt and press the "Make Image" button.
You can set an "Initial Image" if you want to guide the AI.
+ You can also add modifiers like "Realistic", "Pencil Sketch", "ArtStation" etc by browsing through the "Image Modifiers" section
+ and selecting the desired modifiers.
+ Click "Image Settings" for additional settings like seed, image size, number of images to generate etc.
Enjoy! :)
+
@@ -326,12 +332,6 @@
-
- Type a prompt and press the "Make Image" button.
You can set an "Initial Image" if you want to guide the AI.
- You can also add modifiers like "Realistic", "Pencil Sketch", "ArtStation" etc by browsing through the "Image Modifiers" section
- and selecting the desired modifiers.
- Click "Image Settings" for additional settings like seed, image size, number of images to generate etc.