diff --git a/CHANGELOG.md b/CHANGELOG.md
index 5a16514d5..c5227cad3 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,13 +1,16 @@
# Change Log for SD.Next
-## Update for 2024-10-07
+## Update for 2024-10-08
-### Highlights for 2024-10-07
+### Highlights for 2024-10-08
-- **Reprocess**: New workflow options that allow you to generate at lower quality and then reprocess at higher quality for select images only, or generate without hires/refine and then reprocess with hires/refine
+- **Reprocess**: New workflow options that allow you to generate at lower quality and then
+ reprocess at higher quality for select images only or generate without hires/refine and then reprocess with hires/refine
+ and you can pick any previous latent from auto-captured history!
- **Detailer** Fully built-in detailer workflow without with support for all standard models
- New fine-tuned [CLiP-ViT-L]((https://huggingface.co/zer0int/CLIP-GmP-ViT-L-14)) 1st stage **text-encoders** used by SD15, SDXL, Flux.1, etc. brings additional details to your images
-- Integration with [Ctrl+X](https://github.com/genforce/ctrl-x) which allows for control of **structure and appearance** without the need for extra models and [APG: Adaptive Projected Guidance](https://arxiv.org/pdf/2410.02416) for optimal **guidance** control
+- Integration with [Ctrl+X](https://github.com/genforce/ctrl-x) which allows for control of **structure and appearance** without the need for extra models and
+ [APG: Adaptive Projected Guidance](https://arxiv.org/pdf/2410.02416) for optimal **guidance** control
- Auto-detection of best available **device/dtype** settings for your platform and GPU reduces neeed for manual configuration
- Full rewrite of **sampler options**, not far more streamlined with tons of new options to tweak scheduler behavior
- Improved **LoRA** detection and handling for all supported models
@@ -17,11 +20,21 @@ And other goodies like multiple *XYZ grid* improvements, additional *Flux Contro
### Details for 2024-10-07
- **reprocess**
- - new top-level button: reprocess your last generated image(s)
+ - new top-level button: reprocess latent from your history of generated image(s)
- generate using full-quality:off and then reprocess using *full quality decode*
- generate without hires/refine and then *reprocess with hires/refine*
*note*: you can change hires/refine settings and run-reprocess again!
- - reprocess using *face-restore*
+ - reprocess using *detailer*
+
+- **history**
+ - by default, **reprocess** will pick last latent, but you can select any latent from history!
+ - history is under *networks -> history*
+ each history item includes info on operations that were used, timestamp and metadata
+ - any latent operation during workflow automatically adds one or more items to history
+ e.g. generate base + upscale + hires + detailer
+ - history size: *settings -> execution -> latent history size*
+ memory usage is ~130kb of ram for 1mp image
+ - *note* list of latents in history is not auto-refreshed, use refresh button
- **text encoder**:
- allow loading different custom text encoders: *clip-vit-l, clip-vit-g, t5*
diff --git a/javascript/extraNetworks.js b/javascript/extraNetworks.js
index ab818a8d1..032ee4999 100644
--- a/javascript/extraNetworks.js
+++ b/javascript/extraNetworks.js
@@ -328,6 +328,13 @@ function quickSaveStyle() {
}
}
+function selectHistory(id) {
+ const headers = new Headers();
+ headers.set('Content-Type', 'application/json');
+ const init = { method: 'POST', body: { name: id }, headers };
+ fetch('/sdapi/v1/history', { method: 'POST', body: JSON.stringify({ name: id }), headers });
+}
+
let enDirty = false;
function closeDetailsEN(...args) {
// log('closeDetailsEN');
diff --git a/modules/api/api.py b/modules/api/api.py
index 4b7c750fd..23c0a77f1 100644
--- a/modules/api/api.py
+++ b/modules/api/api.py
@@ -84,6 +84,8 @@ class Api:
self.add_api_route("/sdapi/v1/unload-checkpoint", endpoints.post_unload_checkpoint, methods=["POST"])
self.add_api_route("/sdapi/v1/reload-checkpoint", endpoints.post_reload_checkpoint, methods=["POST"])
self.add_api_route("/sdapi/v1/refresh-vae", endpoints.post_refresh_vae, methods=["POST"])
+ self.add_api_route("/sdapi/v1/history", endpoints.get_history, methods=["GET"], response_model=List[str])
+ self.add_api_route("/sdapi/v1/history", endpoints.post_history, methods=["POST"], response_model=int)
# gallery api
gallery.register_api(app)
diff --git a/modules/api/endpoints.py b/modules/api/endpoints.py
index 08b54c9df..61993db84 100644
--- a/modules/api/endpoints.py
+++ b/modules/api/endpoints.py
@@ -158,3 +158,10 @@ def post_pnginfo(req: models.ReqImageInfo):
params = infotext.parse(geninfo)
script_callbacks.infotext_pasted_callback(geninfo, params)
return models.ResImageInfo(info=geninfo, items=items, parameters=params)
+
+def get_history():
+ return shared.history.list
+
+def post_history(req: models.ReqHistory):
+ shared.history.index = shared.history.find(req.name)
+ return shared.history.index
diff --git a/modules/api/models.py b/modules/api/models.py
index cd67522d1..3cf3aade9 100644
--- a/modules/api/models.py
+++ b/modules/api/models.py
@@ -318,6 +318,9 @@ class ReqVQA(BaseModel):
model: str = Field(default="MS Florence 2 Base", title="Model", description="The interrogate model used.")
question: str = Field(default="describe the image", title="Question", description="Question to ask the model.")
+class ReqHistory(BaseModel):
+ name: str = Field(title="Name", description="Name of the history item to select")
+
class ResVQA(BaseModel):
answer: Optional[str] = Field(default=None, title="Answer", description="The generated answer for the image.")
diff --git a/modules/history.py b/modules/history.py
index f98a0026d..ea82fedb9 100644
--- a/modules/history.py
+++ b/modules/history.py
@@ -1,3 +1,10 @@
+"""
+TODO:
+- apply metadata
+- preview
+- load/save
+"""
+
import sys
import datetime
from collections import deque
@@ -5,17 +12,20 @@ from modules import shared, devices
class Item():
- def __init__(self, latent, preview=None, meta=None):
- self.ts = datetime.datetime.now()
+ def __init__(self, latent, preview=None, info=None, ops=[]):
+ self.ts = datetime.datetime.now().replace(microsecond=0)
+ self.name = self.ts.strftime('%Y-%m-%d %H:%M:%S')
self.latent = latent.detach().clone().to(devices.cpu)
self.preview = preview
- self.meta = meta
+ self.info = info
+ self.ops = ops.copy()
+ self.size = sys.getsizeof(self.latent.storage())
class History():
def __init__(self):
- self.latents = deque(maxlen=1000)
- shared.log.debug(f'History init: max={shared.opts.latent_history}')
+ self.index = -1
+ self.latents = deque(maxlen=1024)
@property
def count(self):
@@ -25,32 +35,48 @@ class History():
def size(self):
s = 0
for item in self.latents:
- s += sys.getsizeof(item.latent.storage())
+ s += item.size
return s
@property
def list(self):
- return [item.ts for item in self.latents]
+ shared.log.info(f'History: items={self.count}/{shared.opts.latent_history} size={self.size}')
+ return [item.name for item in self.latents]
@property
def latest(self):
return self.get(0)
- def add(self, latent, preview=None, meta=None):
- item = Item(latent, preview, meta)
+ @property
+ def selected(self):
+ if self.index >= 0 and self.index < self.count:
+ index = self.index
+ latent = self.get(self.index)
+ self.index = -1
+ return latent, index
+ return self.latest, -1
+
+ def find(self, name):
+ for i, item in enumerate(self.latents):
+ if item.name == name:
+ return i
+ return -1
+
+ def add(self, latent, preview=None, info=None, ops=[]):
+ item = Item(latent, preview, info, ops)
self.latents.appendleft(item)
- shared.log.debug(f'History add: shape={latent.shape} dtype={latent.dtype} count={self.count}')
+ # shared.log.debug(f'History add: shape={latent.shape} dtype={latent.dtype} count={self.count}')
if self.count >= shared.opts.latent_history:
self.latents.pop()
def get(self, index: int = 0):
item = self.latents[index]
- shared.log.debug(f'History get: index={index} time={item.ts} shape={item.latent.shape} dtype={item.latent.dtype} count={self.count}')
+ # shared.log.debug(f'History get: index={index} time={item.ts} shape={item.latent.shape} dtype={item.latent.dtype} count={self.count}')
return item.latent.to(devices.device)
def clear(self):
self.latents.clear()
- shared.log.debug(f'History clear: count={self.count}')
+ # shared.log.debug(f'History clear: count={self.count}')
def load(self):
pass
diff --git a/modules/processing_args.py b/modules/processing_args.py
index 2d6fa58d3..0e2999128 100644
--- a/modules/processing_args.py
+++ b/modules/processing_args.py
@@ -49,7 +49,10 @@ def task_specific_kwargs(p, model):
elif (sd_models.get_diffusers_task(model) == sd_models.DiffusersTaskType.INPAINTING or is_img2img_model) and len(getattr(p, 'init_images', [])) > 0:
if shared.sd_model_type == 'sdxl':
model.register_to_config(requires_aesthetics_score = False)
- p.ops.append('inpaint')
+ if p.detailer:
+ p.ops.append('detailer')
+ else:
+ p.ops.append('inpaint')
width, height = processing_helpers.resize_init_images(p)
task_args = {
'image': p.init_images,
diff --git a/modules/processing_diffusers.py b/modules/processing_diffusers.py
index 8d19dfc4d..0236d6d50 100644
--- a/modules/processing_diffusers.py
+++ b/modules/processing_diffusers.py
@@ -95,6 +95,7 @@ def process_base(p: processing.StableDiffusionProcessing):
output = shared.sd_model(**base_args)
if isinstance(output, dict):
output = SimpleNamespace(**output)
+ shared.history.add(output.images, info=processing.create_infotext(p), ops=p.ops)
timer.process.record('pipeline')
hidiffusion.unapply()
sd_models_compile.openvino_post_compile(op="base") # only executes on compiled vino models
@@ -209,6 +210,7 @@ def process_hires(p: processing.StableDiffusionProcessing, output):
output = shared.sd_model(**hires_args) # pylint: disable=not-callable
if isinstance(output, dict):
output = SimpleNamespace(**output)
+ shared.history.add(output.images, info=processing.create_infotext(p), ops=p.ops)
sd_models_compile.check_deepcache(enable=False)
sd_models_compile.openvino_post_compile(op="base")
except AssertionError as e:
@@ -281,6 +283,7 @@ def process_refine(p: processing.StableDiffusionProcessing, output):
output = shared.sd_refiner(**refiner_args) # pylint: disable=not-callable
if isinstance(output, dict):
output = SimpleNamespace(**output)
+ shared.history.add(output.images, info=processing.create_infotext(p), ops=p.ops)
sd_models_compile.openvino_post_compile(op="refiner")
except AssertionError as e:
shared.log.info(e)
@@ -322,7 +325,6 @@ def process_decode(p: processing.StableDiffusionProcessing, output):
full_quality = p.full_quality,
width = width,
height = height,
- save = p.state == '',
)
elif hasattr(output, 'images'):
results = output.images
@@ -388,7 +390,7 @@ def process_diffusers(p: processing.StableDiffusionProcessing):
if 'base' not in p.skip:
output = process_base(p)
else:
- output = SimpleNamespace(images=shared.history.latest)
+ output, _index = SimpleNamespace(images=shared.history.selected)
if shared.state.interrupted or shared.state.skipped:
shared.sd_model = orig_pipeline
diff --git a/modules/processing_vae.py b/modules/processing_vae.py
index 454eeed32..75347f416 100644
--- a/modules/processing_vae.py
+++ b/modules/processing_vae.py
@@ -145,7 +145,7 @@ def taesd_vae_encode(image):
return encoded
-def vae_decode(latents, model, output_type='np', full_quality=True, width=None, height=None, save=True):
+def vae_decode(latents, model, output_type='np', full_quality=True, width=None, height=None):
t0 = time.time()
if latents is None or not torch.is_tensor(latents): # already decoded
return latents
@@ -166,8 +166,6 @@ def vae_decode(latents, model, output_type='np', full_quality=True, width=None,
latents = latents.unsqueeze(0)
if latents.shape[0] == 4 and latents.shape[1] != 4: # likely animatediff latent
latents = latents.permute(1, 0, 2, 3)
- if save:
- shared.history.add(latents)
if latents.shape[-1] <= 4: # not a latent, likely an image
decoded = latents.float().cpu().numpy()
@@ -213,7 +211,7 @@ def vae_encode(image, model, full_quality=True): # pylint: disable=unused-variab
def reprocess(gallery):
from PIL import Image
from modules import images
- latent = shared.history.latest
+ latent, index = shared.history.selected
if latent is None or gallery is None:
return None
shared.log.info(f'Reprocessing: latent={latent.shape}')
@@ -231,6 +229,7 @@ def reprocess(gallery):
if shared.opts.samples_save:
images.save_image(i1, info=info, forced_filename=fn)
i1.already_saved_as = fn
- outputs.append(i0)
+ if index == -1:
+ outputs.append(i0)
outputs.append(i1)
return outputs
diff --git a/modules/shared.py b/modules/shared.py
index ccfca97cd..81befa466 100644
--- a/modules/shared.py
+++ b/modules/shared.py
@@ -423,9 +423,9 @@ options_templates.update(options_section(('sd', "Execution & Models"), {
"prompt_mean_norm": OptionInfo(False, "Prompt attention normalization", gr.Checkbox),
"comma_padding_backtrack": OptionInfo(20, "Prompt padding", gr.Slider, {"minimum": 0, "maximum": 74, "step": 1, "visible": not native }),
"prompt_attention": OptionInfo("Full parser", "Prompt attention parser", gr.Radio, {"choices": ["Full parser", "Compel parser", "xhinker parser", "A1111 parser", "Fixed attention"] }),
+ "latent_history": OptionInfo(16, "Latent history size", gr.Slider, {"minimum": 1, "maximum": 100, "step": 1}),
"sd_checkpoint_cache": OptionInfo(0, "Cached models", gr.Slider, {"minimum": 0, "maximum": 10, "step": 1, "visible": not native }),
"sd_vae_checkpoint_cache": OptionInfo(0, "Cached VAEs", gr.Slider, {"minimum": 0, "maximum": 10, "step": 1, "visible": False}),
- "latent_history": OptionInfo(1, "Latent history size", gr.Slider, {"minimum": 1, "maximum": 100, "step": 1}),
"sd_disable_ckpt": OptionInfo(False, "Disallow models in ckpt format", gr.Checkbox, {"visible": False}),
"diffusers_version": OptionInfo("", "Diffusers version", gr.Textbox, {"visible": False}),
}))
diff --git a/modules/ui_extra_networks.py b/modules/ui_extra_networks.py
index e8feb2501..58fb463a9 100644
--- a/modules/ui_extra_networks.py
+++ b/modules/ui_extra_networks.py
@@ -82,7 +82,7 @@ def init_api(app):
item = next(iter([x for x in page.items if x['name'] == item]), None)
if item is None:
return JSONResponse({ 'info': 'none' })
- info = page.find_info(item['filename'])
+ info = page.find_info(item.get('filename', None) or item.get('name', None))
if info is None:
info = {}
# shared.log.debug(f"Networks info: page='{page.name}' item={item['name']} len={len(info)}")
@@ -95,7 +95,7 @@ def init_api(app):
item = next(iter([x for x in page.items if x['name'] == item]), None)
if item is None:
return JSONResponse({ 'description': 'none' })
- desc = page.find_description(item['filename'])
+ desc = page.find_description(item.get('filename', None) or item.get('name', None))
if desc is None:
desc = ''
# shared.log.debug(f"Networks desc: page='{page.name}' item={item['name']} len={len(desc)}")
@@ -295,11 +295,11 @@ class ExtraNetworksPage:
args = {
"tabname": tabname,
"page": self.name,
- "name": item["name"],
+ "name": item.get('name', ''),
"title": os.path.basename(item["name"].replace('_', ' ')),
- "filename": item["filename"],
- "tags": '|'.join([item.get("tags")] if isinstance(item.get("tags", {}), str) else list(item.get("tags", {}).keys())),
- "preview": html.escape(item.get("preview", self.link_preview('html/card-no-preview.png'))),
+ "filename": item.get('filename', ''),
+ "tags": '|'.join([item.get('tags')] if isinstance(item.get('tags', {}), str) else list(item.get('tags', {}).keys())),
+ "preview": html.escape(item.get('preview', None) or self.link_preview('html/card-no-preview.png')),
"width": shared.opts.extra_networks_card_size,
"height": shared.opts.extra_networks_card_size if shared.opts.extra_networks_card_square else 'auto',
"fit": shared.opts.extra_networks_card_fit,
@@ -318,6 +318,8 @@ class ExtraNetworksPage:
return self.card.format(**args)
except Exception as e:
shared.log.error(f'Extra networks item error: page={tabname} item={item["name"]} {e}')
+ if os.environ.get('SD_EN_DEBUG', None) is not None:
+ errors.display(e, 'Extra networks')
return ""
def find_preview_file(self, path):
@@ -391,17 +393,18 @@ class ExtraNetworksPage:
if tag == 'p':
self.text += '\n'
- fn = os.path.splitext(path)[0] + '.txt'
- if os.path.exists(fn):
- try:
- with open(fn, "r", encoding="utf-8", errors="replace") as f:
- txt = f.read()
- txt = re.sub('[<>]', '', txt)
- return txt
- except OSError:
- pass
- if info is None:
- info = self.find_info(path)
+ if path is not None:
+ fn = os.path.splitext(path)[0] + '.txt'
+ if os.path.exists(fn):
+ try:
+ with open(fn, "r", encoding="utf-8", errors="replace") as f:
+ txt = f.read()
+ txt = re.sub('[<>]', '', txt)
+ return txt
+ except OSError:
+ pass
+ if info is None:
+ info = self.find_info(path)
desc = info.get('description', '') or ''
f = HTMLFilter()
f.feed(desc)
@@ -413,14 +416,15 @@ class ExtraNetworksPage:
data = {}
if shared.cmd_opts.no_metadata:
return data
- fn = os.path.splitext(path)[0] + '.json'
- if os.path.exists(fn):
- t0 = time.time()
- data = shared.readfile(fn, silent=True)
- if type(data) is list:
- data = data[0]
- t1 = time.time()
- self.info_time += t1-t0
+ if path is not None:
+ fn = os.path.splitext(path)[0] + '.json'
+ if os.path.exists(fn):
+ t0 = time.time()
+ data = shared.readfile(fn, silent=True)
+ if type(data) is list:
+ data = data[0]
+ t1 = time.time()
+ self.info_time += t1-t0
return data
@@ -743,7 +747,8 @@ def create_ui(container, button_parent, tabname, skip_indexing = False):
def show_details(text, img, desc, info, meta, description, prompt, negative, parameters, wildcards, params, _dummy1=None, _dummy2=None):
page, item = get_item(state, params)
- if item is not None and hasattr(item, 'name'):
+ valid = item is not None and hasattr(item, 'name') and hasattr(item, 'filename')
+ if valid:
stat = os.stat(item.filename) if os.path.exists(item.filename) else None
desc = item.description
fullinfo = shared.readfile(os.path.splitext(item.filename)[0] + '.json', silent=True)
@@ -844,7 +849,7 @@ def create_ui(container, button_parent, tabname, skip_indexing = False):
negative, # gr.textbox
parameters, # gr.textbox
wildcards, # gr.textbox
- gr.update(visible=item is not None), # details ui visible
+ gr.update(visible=valid), # details ui visible
gr.update(visible=page is not None and page.title != 'Style'), # details ui tabs visible
gr.update(visible=page is not None and page.title == 'Style'), # details ui text visible
]
@@ -852,6 +857,9 @@ def create_ui(container, button_parent, tabname, skip_indexing = False):
def ui_refresh_click(title):
pages = []
for page in get_pages():
+ if page.title != title:
+ pages.append(page.html)
+ continue
page.page_time = 0
page.refresh_time = 0
page.refresh()
diff --git a/modules/ui_extra_networks_history.py b/modules/ui_extra_networks_history.py
index 7b471e394..72f31731a 100644
--- a/modules/ui_extra_networks_history.py
+++ b/modules/ui_extra_networks_history.py
@@ -1,11 +1,13 @@
import time
+import json
+import html
from modules import shared, ui_extra_networks
class ExtraNetworksPageHistory(ui_extra_networks.ExtraNetworksPage):
def __init__(self):
- super().__init__('History')
# shared.log.trace('History init')
+ super().__init__('History')
self.last_refresh = 0
def refresh(self):
@@ -17,13 +19,21 @@ class ExtraNetworksPageHistory(ui_extra_networks.ExtraNetworksPage):
def list_items(self):
# shared.log.trace('History list')
- return shared.history.list
+ for item in shared.history.latents:
+ title = ', '.join(list(set(item.ops))) + '
' + item.name
+ yield {
+ "type": 'History',
+ "name": title,
+ "preview": item.preview,
+ "mtime": item.ts,
+ "size": item.size,
+ # "info": item.info,
+ # "description": item.info,
+ "onclick": '"' + html.escape(f"""return selectHistory({json.dumps(item.name)})""") + '"',
+ }
- def create_page(self, tabname, skip = False):
- # shared.log.trace(f'History page: tab={tabname} skip={skip}')
- self.page_time = time.time()
- if tabname == 'txt2img':
- self.last_refresh = time.time()
- if self.page_time <= self.last_refresh: # cached page
- self.refresh()
- return self.patch(self.html, tabname)
+ def find_description(self, path, info=None):
+ name = path.split('
')[-1]
+ items = [l for l in shared.history.latents if l.name == name]
+ if len(items) > 0:
+ return items[0].info
diff --git a/wiki b/wiki
index c648e82c3..fa80fa007 160000
--- a/wiki
+++ b/wiki
@@ -1 +1 @@
-Subproject commit c648e82c3b26813bff59dad567438e877fff02a1
+Subproject commit fa80fa0071aea16285359b1ebfacd21b385efad4