fix prompt parser, image save

This commit is contained in:
Vladimir Mandic
2024-03-21 12:11:47 -04:00
parent 1a27871c70
commit 8a869d7e27
4 changed files with 49 additions and 32 deletions
+25 -22
View File
@@ -5,37 +5,40 @@
- Include reference styles
- Quick apply style
- Add refine workflow in img2img
- Gallery send to buttons row
- Gallery client-side caching? thumbnails, metadata
- Gallery search by metadata
## Update for 2024-03-20
Gallery:
- All operations are async, non-blocking and auto-cancelled as needed
This gives up a little bit of raw performance, but there is no "wait for current folder which has 10k images"
Right now enumerate is ~100-300 images/sec which is sufficient to show first screen of images near-instant
- Images are loaded using lazy-loading, meaning on-demand when they are scrolled into view
But...If you want to have 10k images in a folder and then scroll-to-bottom, well...
- Majority of work is done client-side in browser without needing for typical Gradio round-trip
- Nothing cached by choice, images are always up-to-date (I may add thumbnail caching later)
- Search is matching any part of folder/filename and image properties, but not image metadata (yet)
- Search allows for syntax like `size > 1000000` or `width > 1000`
Operators are `<>=` and keys are `size`, `width`, `height`, `mtime`
- Sort and search can easily handle 20k images-per-sec
- Folders are scanned recursively
- Optional user-defined folders: settings -> image options -> image browser
- Thumbnails can be fixed or varible width, set in settings -> image options -> image browser
Gallery ToDo:
- Send to buttons row
- Client-side caching? thumbnails, metadata
- Search by metadata
Changes:
**Features**:
- Gallery:
- All operations are async, non-blocking and auto-cancelled as needed
This gives up a little bit of raw performance, but there is no "wait for current folder which has 10k images"
Right now enumerate is ~100-300 images/sec which is sufficient to show first screen of images near-instant
- Images are loaded using lazy-loading, meaning on-demand when they are scrolled into view
But...If you want to have 10k images in a folder and then scroll-to-bottom, well...
- Majority of work is done client-side in browser without needing for typical Gradio round-trip
- Nothing cached by choice, images are always up-to-date (I may add thumbnail caching later)
- Search is matching any part of folder/filename and image properties, but not image metadata (yet)
- Search allows for syntax like `size > 1000000` or `width > 1000`
Operators are `<>=` and keys are `size`, `width`, `height`, `mtime`
- Sort and search can easily handle 20k images-per-sec
- Folders are scanned recursively
- Optional user-defined folders: settings -> image options -> image browser
- Thumbnails can be fixed or varible width, set in settings -> image options -> image browser
**Changes**:
- Removed built-in extensions: *ControlNet* and *Image-Browser*
as both *image-browser* and *controlnet* have native equivalents
both can still be installed by user if desired
Improvements:
**Improvements**:
- Styles apply wildcards to params
- Make metadata in full screen viewer optional
Fixes:
- Propmpt params parser
- Fix image save without metadata
## Update for 2024-03-19
### Highlights 2024-03-19
+9 -3
View File
@@ -18,12 +18,13 @@ def unquote(text):
return text
def parse_generation_parameters(infotext): # copied from modules.generation_parameters_copypaste
def parse_generation_parameters(infotext):
if not isinstance(infotext, str):
return {}
re_param = re.compile(r'\s*([\w ]+):\s*("(?:\\"[^,]|\\"|\\|[^\"])+"|[^,]*)(?:,|$)') # multi-word: value
re_size = re.compile(r"^(\d+)x(\d+)$") # int x int
basic_params = ['steps', 'seed', 'width', 'height', 'sampler', 'size', 'cfg scale', 'hires'] # first param is one of those
sanitized = infotext.replace('prompt:', 'Prompt:').replace('negative prompt:', 'Negative prompt:').replace('Negative Prompt', 'Negative prompt') # cleanup everything in brackets so re_params can work
sanitized = re.sub(r'<[^>]*>', lambda match: ' ' * len(match.group()), sanitized)
sanitized = re.sub(r'\([^)]*\)', lambda match: ' ' * len(match.group()), sanitized)
@@ -31,7 +32,10 @@ def parse_generation_parameters(infotext): # copied from modules.generation_para
params = dict(re_param.findall(sanitized))
params = { k.strip():params[k].strip() for k in params if k.lower() not in ['hashes', 'lora', 'embeddings', 'prompt', 'negative prompt']} # remove some keys
first_param = next(iter(params)) if params else None
first_param, first_param_idx = next((s, i) for i, s in enumerate(params) if any(x in s.lower() for x in basic_params))
if first_param_idx > 0:
for _i in range(first_param_idx):
params.pop(next(iter(params)))
params_idx = sanitized.find(f'{first_param}:') if first_param else -1
negative_idx = infotext.find("Negative prompt:")
@@ -81,6 +85,8 @@ class Exif: # pylint: disable=single-string-used-for-slots
try:
exif_dict = dict(img._getexif().items()) # pylint: disable=protected-access
except Exception:
pass
if not exif_dict:
exif_dict = dict(img.info.items())
for key, val in exif_dict.items():
if isinstance(val, bytes): # decode bytestring
+6 -1
View File
@@ -192,6 +192,8 @@ def parse_generation_parameters(infotext):
debug(f'Parse infotext: {infotext}')
re_param = re.compile(r'\s*([\w ]+):\s*("(?:\\"[^,]|\\"|\\|[^\"])+"|[^,]*)(?:,|$)') # multi-word: value
re_size = re.compile(r"^(\d+)x(\d+)$") # int x int
basic_params = ['steps', 'seed', 'width', 'height', 'sampler', 'size', 'cfg scale'] # first param is one of those
sanitized = infotext.replace('prompt:', 'Prompt:').replace('negative prompt:', 'Negative prompt:').replace('Negative Prompt', 'Negative prompt') # cleanup everything in brackets so re_params can work
sanitized = re.sub(r'<[^>]*>', lambda match: ' ' * len(match.group()), sanitized)
sanitized = re.sub(r'\([^)]*\)', lambda match: ' ' * len(match.group()), sanitized)
@@ -200,7 +202,10 @@ def parse_generation_parameters(infotext):
params = dict(re_param.findall(sanitized))
debug(f"Parse params: {params}")
params = { k.strip():params[k].strip() for k in params if k.lower() not in ['hashes', 'lora', 'embeddings', 'prompt', 'negative prompt']} # remove some keys
first_param = next(iter(params)) if params else None
first_param, first_param_idx = next((s, i) for i, s in enumerate(params) if any(x in s.lower() for x in basic_params))
if first_param_idx > 0:
for _i in range(first_param_idx):
params.pop(next(iter(params)))
params_idx = sanitized.find(f'{first_param}:') if first_param else -1
negative_idx = infotext.find("Negative prompt:")
+9 -6
View File
@@ -548,6 +548,7 @@ def atomically_save_image():
image = set_watermark(image, shared.opts.image_watermark)
size = os.path.getsize(fn) if os.path.exists(fn) else 0
shared.log.info(f'Saving: image="{fn}" type={image_format} resolution={image.width}x{image.height} size={size}')
exifinfo = (exifinfo or "") if shared.opts.image_metadata else ""
# additional metadata saved in files
if shared.opts.save_txt and len(exifinfo) > 0:
try:
@@ -557,7 +558,6 @@ def atomically_save_image():
except Exception as e:
shared.log.warning(f'Saving failed: description={filename_txt} {e}')
# actual save
exifinfo = (exifinfo or "") if shared.opts.image_metadata else ""
if image_format == 'PNG':
pnginfo_data = PngImagePlugin.PngInfo()
for k, v in params.pnginfo.items():
@@ -569,19 +569,22 @@ def atomically_save_image():
image = image.convert("RGB")
elif image.mode == 'I;16':
image = image.point(lambda p: p * 0.0038910505836576).convert("L")
exif_bytes = piexif.dump({ "Exif": { piexif.ExifIFD.UserComment: piexif.helper.UserComment.dump(exifinfo, encoding="unicode") } })
save_args = { 'optimize': True, 'quality': shared.opts.jpeg_quality, 'exif': exif_bytes if shared.opts.image_metadata else None }
save_args = { 'optimize': True, 'quality': shared.opts.jpeg_quality }
if shared.opts.image_metadata:
save_args['exif'] = piexif.dump({ "Exif": { piexif.ExifIFD.UserComment: piexif.helper.UserComment.dump(exifinfo, encoding="unicode") } })
elif image_format == 'WEBP':
if image.mode == 'I;16':
image = image.point(lambda p: p * 0.0038910505836576).convert("RGB")
exif_bytes = piexif.dump({ "Exif": { piexif.ExifIFD.UserComment: piexif.helper.UserComment.dump(exifinfo, encoding="unicode") } })
save_args = { 'optimize': True, 'quality': shared.opts.jpeg_quality, 'exif': exif_bytes if shared.opts.image_metadata else None, 'lossless': shared.opts.webp_lossless }
save_args = { 'optimize': True, 'quality': shared.opts.jpeg_quality, 'lossless': shared.opts.webp_lossless }
if shared.opts.image_metadata:
save_args['exif'] = piexif.dump({ "Exif": { piexif.ExifIFD.UserComment: piexif.helper.UserComment.dump(exifinfo, encoding="unicode") } })
else:
save_args = { 'quality': shared.opts.jpeg_quality }
try:
image.save(fn, format=image_format, **save_args)
except Exception as e:
shared.log.error(f'Saving failed: file="{fn}" format={image_format} {e}')
shared.log.error(f'Saving failed: file="{fn}" format={image_format} args={save_args} {e}')
errors.display(e, 'Image save')
if shared.opts.save_log_fn != '' and len(exifinfo) > 0:
fn = os.path.join(paths.data_path, shared.opts.save_log_fn)
if not fn.endswith('.json'):