* feat: add metadata logging for images inspired by https://github.com/MoonRide303/Fooocus-MRE * feat: add config and checkbox for save_metadata_to_images * feat: add argument disable_metadata * feat: add support for A1111 metadata schemacf2772fab0/modules/processing.py (L672)
* feat: add model hash support for a1111 * feat: use resolved prompts with included expansion and styles for a1111 metadata * fix: code cleanup and resolved prompt fixes * feat: add config metadata_created_by * fix: use stting isntead of quote wrap for A1111 created_by * fix: correctlyy hide/show metadata schema on app start * fix: do not generate hashes when arg --disable-metadata is used * refactor: rename metadata_schema to metadata_scheme * fix: use pnginfo "parameters" insteadf of "Comments" see https://github.com/RupertAvery/DiffusionToolkit/issues/202 andcf2772fab0/modules/processing.py (L939)
* feat: add resolved prompts to metadata * fix: use correct default value in metadata check for created_by * wip: add metadata mapping, reading and writing applying data after reading currently not functional for A1111 * feat: rename metadata tab and import button label * feat: map basic information for scheme A1111 * wip: optimize handling for metadata in Gradio calls * feat: add enums for Performance, Steps and StepsUOV also move MetadataSchema enum to prevent circular dependency * fix: correctly map resolution, use empty styles for A1111 * chore: code cleanup * feat: add A1111 prompt style detection only detects one style as Fooocus doesn't wrap {prompt} with the whole style, but has a separate prompt string for each style * wip: add prompt style extraction for A1111 scheme * feat: sort styles after metadata import * refactor: use central flag for LoRA count * refactor: use central flag for ControlNet image count * fix: use correct LoRA mapping, add fallback for backwards compatibility * feat: add created_by again * feat: add prefix "Fooocus" to version * wip: code cleanup, update todos * fix: use correct order to read LoRA in meta parser * wip: code cleanup, update todos * feat: make sha256 with length 10 default * feat: add lora handling to A1111 scheme * feat: override existing LoRA values when importing, would cause images to differ * fix: correctly extract prompt style when only prompt expansion is selected * feat: allow model / LoRA loading from subfolders * feat: code cleanup, do not queue metadata preview on image upload * refactor: add flag for refiner_swap_method * feat: add metadata handling for all non-img2img parameters * refactor: code cleanup * chore: use str as return type in calculate_sha256 * feat: add hash cache to metadata * chore: code cleanup * feat: add method get_scheme to Metadata * fix: align handling for scheme Fooocus by removing lcm lora from json parsing * refactor: add step before parsing to set data in parser - add constructor for MetadataSchema class - remove showable and copyable from log output - add functional hash cache (model hashing takes about 5 seconds, only required once per model, using hash lazy loading) * feat: sort metadata attributes before writing to image * feat: add translations and hint for image prompt parameters * chore: check and remove ToDo's * refactor: merge metadata.py into meta_parser.py * fix: add missing refiner in A1111 parse_json * wip: add TODO for ultiline prompt style resolution * fix: remove sorting for A1111, change performance key position fixes https://github.com/lllyasviel/Fooocus/pull/1940#issuecomment-1924444633 * fix: add workaround for multiline prompts * feat: add sampler mapping * feat: prevent config reset by renaming metadata_scheme to match config options * chore: remove remaining todos after analysis refiner is added when set restoring multiline prompts has been resolved by using separate parameters "raw_prompt" and "raw_negative_prompt" * chore: specify too broad exception types * feat: add mapping for _gpu samplers to cpu samplers gpu samplers are less deterministic than cpu but in general similar, see https://www.reddit.com/r/comfyui/comments/15hayzo/comment/juqcpep/ * feat: add better handling for image import with empty metadata * fix: parse adaptive_cfg as float instead of string * chore: loosen strict type for parse_json, fix indent * chore: make steps enums more strict * feat: only override steps if metadata value is not in steps enum or in steps enum and performance is not the same * fix: handle empty strings in metadata e.g. raw negative prompt when none is set
123 lines
5.1 KiB
Python
123 lines
5.1 KiB
Python
import os
|
|
import args_manager
|
|
import modules.config
|
|
import json
|
|
import urllib.parse
|
|
|
|
from PIL import Image
|
|
from PIL.PngImagePlugin import PngInfo
|
|
from modules.util import generate_temp_filename
|
|
from modules.meta_parser import MetadataParser
|
|
from tempfile import gettempdir
|
|
|
|
log_cache = {}
|
|
|
|
|
|
def get_current_html_path():
|
|
date_string, local_temp_filename, only_name = generate_temp_filename(folder=modules.config.path_outputs,
|
|
extension='png')
|
|
html_name = os.path.join(os.path.dirname(local_temp_filename), 'log.html')
|
|
return html_name
|
|
|
|
|
|
def log(img, metadata, metadata_parser: MetadataParser | None = None) -> str:
|
|
path_outputs = args_manager.args.temp_path if args_manager.args.disable_image_log else modules.config.path_outputs
|
|
date_string, local_temp_filename, only_name = generate_temp_filename(folder=path_outputs, extension='png')
|
|
os.makedirs(os.path.dirname(local_temp_filename), exist_ok=True)
|
|
|
|
parsed_parameters = metadata_parser.parse_string(metadata) if metadata_parser is not None else ''
|
|
image = Image.fromarray(img)
|
|
|
|
if parsed_parameters != '':
|
|
pnginfo = PngInfo()
|
|
pnginfo.add_text('parameters', parsed_parameters)
|
|
pnginfo.add_text('fooocus_scheme', metadata_parser.get_scheme().value)
|
|
else:
|
|
pnginfo = None
|
|
image.save(local_temp_filename, pnginfo=pnginfo)
|
|
|
|
if args_manager.args.disable_image_log:
|
|
return local_temp_filename
|
|
|
|
html_name = os.path.join(os.path.dirname(local_temp_filename), 'log.html')
|
|
|
|
css_styles = (
|
|
"<style>"
|
|
"body { background-color: #121212; color: #E0E0E0; } "
|
|
"a { color: #BB86FC; } "
|
|
".metadata { border-collapse: collapse; width: 100%; } "
|
|
".metadata .label { width: 15%; } "
|
|
".metadata .value { width: 85%; font-weight: bold; } "
|
|
".metadata th, .metadata td { border: 1px solid #4d4d4d; padding: 4px; } "
|
|
".image-container img { height: auto; max-width: 512px; display: block; padding-right:10px; } "
|
|
".image-container div { text-align: center; padding: 4px; } "
|
|
"hr { border-color: gray; } "
|
|
"button { background-color: black; color: white; border: 1px solid grey; border-radius: 5px; padding: 5px 10px; text-align: center; display: inline-block; font-size: 16px; cursor: pointer; }"
|
|
"button:hover {background-color: grey; color: black;}"
|
|
"</style>"
|
|
)
|
|
|
|
js = (
|
|
"""<script>
|
|
function to_clipboard(txt) {
|
|
txt = decodeURIComponent(txt);
|
|
if (navigator.clipboard && navigator.permissions) {
|
|
navigator.clipboard.writeText(txt)
|
|
} else {
|
|
const textArea = document.createElement('textArea')
|
|
textArea.value = txt
|
|
textArea.style.width = 0
|
|
textArea.style.position = 'fixed'
|
|
textArea.style.left = '-999px'
|
|
textArea.style.top = '10px'
|
|
textArea.setAttribute('readonly', 'readonly')
|
|
document.body.appendChild(textArea)
|
|
|
|
textArea.select()
|
|
document.execCommand('copy')
|
|
document.body.removeChild(textArea)
|
|
}
|
|
alert('Copied to Clipboard!\\nPaste to prompt area to load parameters.\\nCurrent clipboard content is:\\n\\n' + txt);
|
|
}
|
|
</script>"""
|
|
)
|
|
|
|
begin_part = f"<!DOCTYPE html><html><head><title>Fooocus Log {date_string}</title>{css_styles}</head><body>{js}<p>Fooocus Log {date_string} (private)</p>\n<p>All images are clean, without any hidden data/meta, and safe to share with others.</p><!--fooocus-log-split-->\n\n"
|
|
end_part = f'\n<!--fooocus-log-split--></body></html>'
|
|
|
|
middle_part = log_cache.get(html_name, "")
|
|
|
|
if middle_part == "":
|
|
if os.path.exists(html_name):
|
|
existing_split = open(html_name, 'r', encoding='utf-8').read().split('<!--fooocus-log-split-->')
|
|
if len(existing_split) == 3:
|
|
middle_part = existing_split[1]
|
|
else:
|
|
middle_part = existing_split[0]
|
|
|
|
div_name = only_name.replace('.', '_')
|
|
item = f"<div id=\"{div_name}\" class=\"image-container\"><hr><table><tr>\n"
|
|
item += f"<td><a href=\"{only_name}\" target=\"_blank\"><img src='{only_name}' onerror=\"this.closest('.image-container').style.display='none';\" loading='lazy'/></a><div>{only_name}</div></td>"
|
|
item += "<td><table class='metadata'>"
|
|
for label, key, value in metadata:
|
|
value_txt = str(value).replace('\n', ' </br> ')
|
|
item += f"<tr><td class='label'>{label}</td><td class='value'>{value_txt}</td></tr>\n"
|
|
item += "</table>"
|
|
|
|
js_txt = urllib.parse.quote(json.dumps({k: v for _, k, v in metadata}, indent=0), safe='')
|
|
item += f"</br><button onclick=\"to_clipboard('{js_txt}')\">Copy to Clipboard</button>"
|
|
|
|
item += "</td>"
|
|
item += "</tr></table></div>\n\n"
|
|
|
|
middle_part = item + middle_part
|
|
|
|
with open(html_name, 'w', encoding='utf-8') as f:
|
|
f.write(begin_part + middle_part + end_part)
|
|
|
|
print(f'Image generated with private log at: {html_name}')
|
|
|
|
log_cache[html_name] = middle_part
|
|
|
|
return local_temp_filename
|