diff --git a/CHANGELOG.md b/CHANGELOG.md
index 3d50229b7..bae797537 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -11,6 +11,7 @@
- **Extra networks**:
- new details interface to view and save data about extra networks
main ui now has a single button on each en to trigger details view
+ details view includes model/lora metadata parser!
- faster search, ability to show/hide/sort networks
- refactored subfolder handling
*note*: this will trigger model hash recaclulation on first model use
@@ -29,6 +30,8 @@
- better pipeline auto-detect when loading from safetensors
- **Logging**
- get browser session info in server log
+ - when running with `--debug` flag, log is force-rotated
+ so each `sdnext.log.*` represents exactly one server run
## Update for 2023-09-13
diff --git a/installer.py b/installer.py
index 9a322b677..28ffa488a 100644
--- a/installer.py
+++ b/installer.py
@@ -20,6 +20,7 @@ class Dot(dict): # dot notation access to dictionary attributes
log = logging.getLogger("sd")
log_file = os.path.join(os.path.dirname(__file__), 'sdnext.log')
+log_rolled = False
quick_allowed = True
errors = 0
opts = {}
@@ -90,7 +91,12 @@ def setup_logging():
rh.setLevel(level)
log.addHandler(rh)
- fh = RotatingFileHandler(log_file, maxBytes=10*1024*1024, backupCount=5, encoding='utf-8', delay=True) # 10MB default for log rotation
+ fh = RotatingFileHandler(log_file, maxBytes=32*1024*1024, backupCount=9, encoding='utf-8', delay=True) # 10MB default for log rotation
+ global log_rolled # pylint: disable=global-statement
+ if not log_rolled and args.debug:
+ fh.doRollover()
+ log.debug(f'Logging: {log_file}')
+ log_rolled = True
fh.formatter = logging.Formatter('%(asctime)s | %(name)s | %(levelname)s | %(module)s | %(message)s')
fh.setLevel(logging.DEBUG)
log.addHandler(fh)
diff --git a/javascript/extraNetworks.js b/javascript/extraNetworks.js
index 0a61074f2..44e397b84 100644
--- a/javascript/extraNetworks.js
+++ b/javascript/extraNetworks.js
@@ -33,37 +33,12 @@ const setENState = (state) => {
updateInput(el);
};
-// popup
-
-let globalPopup = null;
-let globalPopupInner = null;
-
-function popup(contents) {
- if (!globalPopup) {
- globalPopup = document.createElement('div');
- globalPopup.onclick = () => { globalPopup.style.display = 'none'; };
- globalPopup.classList.add('global-popup');
- const close = document.createElement('div');
- close.classList.add('global-popup-close');
- close.onclick = () => { globalPopup.style.display = 'none'; };
- close.title = 'Close';
- globalPopup.appendChild(close);
- globalPopupInner = document.createElement('div');
- globalPopupInner.onclick = (event) => { event.stopPropagation(); return false; };
- globalPopupInner.classList.add('global-popup-inner');
- globalPopup.appendChild(globalPopupInner);
- gradioApp().appendChild(globalPopup);
- }
- globalPopupInner.innerHTML = '';
- globalPopupInner.appendChild(contents);
- globalPopup.style.display = 'flex';
-}
-
// methods
function showCardDetails(event) {
+ console.log('HERE1', event);
const tabname = getENActiveTab();
- setENState({ op: 'showCardDetails' });
+ // setENState({ op: 'showCardDetails' });
const btn = gradioApp().getElementById(`${tabname}_extra_details_btn`);
btn.click();
event.stopPropagation();
@@ -71,47 +46,14 @@ function showCardDetails(event) {
}
function getCardDetails(...args) {
- const el = event?.target?.parentElement?.parentElement?.parentElement;
+ console.log('HERE2', event);
+ const el = event?.target?.parentElement?.parentElement;
if (!el?.classList?.contains('card')) return [...args];
const tabname = getENActiveTab();
setENState({ op: 'getCardDetails', item: el.dataset.name });
return [...args];
}
-function saveCardDescription(event) {
- const el = event?.target?.parentElement?.parentElement?.parentElement;
- if (!el?.classList?.contains('card')) return;
- const tabname = getENActiveTab();
- const description = gradioApp().getElementById(`${tabname}_description`)?.children[0].children[1].value;
- el.dataset.description = description;
- setENState({ op: 'saveCardDescription', item: el.dataset.name, description });
- event.stopPropagation();
- event.preventDefault();
-}
-
-function saveCardPreview(event) {
- const el = event?.target?.parentElement?.parentElement?.parentElement;
- if (!el?.classList?.contains('card')) return;
- const index = selected_gallery_index();
- setENState({ op: 'saveCardPreview', item: el.dataset.name, index });
- event.stopPropagation();
- event.preventDefault();
-}
-
-function readCardMetadata(event, extraPage, cardName) {
- requestGet('./sd_extra_networks/metadata', { page: extraPage, item: cardName }, (data) => {
- if (data?.metadata) {
- if (typeof (data?.metadata) !== 'string') data.metadata = JSON.stringify(data.metadata, null, 2);
- const elem = document.createElement('pre');
- elem.classList.add('popup-metadata');
- elem.textContent = data.metadata;
- popup(elem);
- }
- });
- event.stopPropagation();
- event.preventDefault();
-}
-
function readCardTags(el, tags) {
const clickTag = (e, tag) => {
e.preventDefault();
diff --git a/javascript/style.css b/javascript/style.css
index 58aa2de8d..68b2c9ed1 100644
--- a/javascript/style.css
+++ b/javascript/style.css
@@ -252,14 +252,22 @@ table.settings-value-table td { padding: 0.4em; border: 1px solid #ccc; max-widt
.extra-network-cards .card:hover .overlay { background: rgba(0, 0, 0, 0.40); }
.extra-network-cards .card .overlay .tags { margin: 4px; display: none; overflow-wrap: break-word; }
.extra-network-cards .card .overlay .tag { padding: 2px; margin: 2px; background: var(--neutral-700); cursor: pointer; display: inline-block; }
-.extra-network-cards .card .overlay .actions { font-size: 2.2em; display: none; text-align-last: center; cursor: pointer; font-variant: unicase; height: 0.8em }
-.extra-network-cards .card .overlay .actions > span { padding: 4px; }
-.extra-network-cards .card .overlay .description { line-break: anywhere; display: none; color: white; }
-.extra-network-cards .card:hover .overlay .actions { display: block; }
+.extra-network-cards .card .actions > span { padding: 4px; }
+.extra-network-cards .card:hover .actions { display: block; }
.extra-network-cards .card:hover .overlay .tags { display: block; }
-.extra-network-cards .card:hover .overlay .description { display: block; }
-
-
+.extra-network-cards .card .actions {
+ font-size: 3em;
+ display: none;
+ text-align-last: right;
+ cursor: pointer;
+ font-variant: unicase;
+ position: absolute;
+ z-index: 100;
+ right: 0;
+ height: 0.7em;
+ width: 100%;
+ background: rgba(0, 0, 0, 0.40);
+}
#txt2img_description, #img2img_description { max-height: 63px; overflow-y: auto !important; }
#txt2img_description > label > textarea, #img2img_description > label > textarea { font-size: 0.9em }
#txt2img_extra_details, #img2img_extra_details {
@@ -273,7 +281,7 @@ table.settings-value-table td { padding: 0.4em; border: 1px solid #ccc; max-widt
box-shadow: var(--button-shadow);
}
#txt2img_extra_details > div, #img2img_extra_details > div { overflow-y: auto; min-height: 40vh; max-height: 80vh; align-self: flex-start; }
-#txt2img_extra_details td:first-child, #img2img_extra_details td:first-child { font-weight: bold; }
+#txt2img_extra_details td:first-child, #img2img_extra_details td:first-child { font-weight: bold; vertical-align: top; }
/* controlnet
.controlnet_control_type .controlnet_control_type_filter_group .wrap:last-of-type { display: grid; grid-auto-flow: row; grid-template-columns: repeat(4, minmax(0, 1fr)); }
diff --git a/modules/ui_extra_networks.py b/modules/ui_extra_networks.py
index 2013d17ee..37f4ac1be 100644
--- a/modules/ui_extra_networks.py
+++ b/modules/ui_extra_networks.py
@@ -94,17 +94,15 @@ class ExtraNetworksPage:
self.refresh_time = None
# class additional is to keep old extensions happy
self.card = '''
-
+
{search_term}
-
{title}
-
+
{title}
+
+
@@ -232,7 +230,6 @@ class ExtraNetworksPage:
"prompt": item.get("prompt", None),
"search_term": item.get("search_term", ""),
"description": item.get("description") or "",
- "local_preview": item.get("local_preview"),
"card_click": item.get("onclick", '"' + html.escape(f'return cardClicked({item.get("prompt", None)}, {"true" if self.allow_negative_prompt else "false"})') + '"'),
}
alias = item.get("alias", None)
@@ -333,6 +330,7 @@ class ExtraNetworksUi:
self.search: gr.Textbox = None
self.button_details: gr.Button = None
self.details_components: list = []
+ self.last_item: dict = None
def create_ui(container, button_parent, tabname, skip_indexing = False):
@@ -347,7 +345,7 @@ def create_ui(container, button_parent, tabname, skip_indexing = False):
state = {}
def get_item(state):
- if state is None or state.page is None or state.item is None:
+ if state is None or not hasattr(state, 'page') or not hasattr(state, 'item'):
return None, None
page = next(iter([x for x in get_pages() if x.title == state.page]), None)
if page is None:
@@ -368,43 +366,11 @@ def create_ui(container, button_parent, tabname, skip_indexing = False):
return
page, item = get_item(state)
shared.log.debug(f'Extra network: op={state.op} page={page.title if page is not None else None} item={item.filename if item is not None else None}')
+ ui.last_item = item
if state.op == 'getCardDetails':
pass
- if state.op == 'saveCardDescription':
- if item is None or state.description is None or len(state.description) == 0:
- return
- try:
- filename = os.path.splitext(item.filename)[1] + '.txt'
- with open(filename, 'w', encoding='utf-8') as f:
- f.write(state.description)
- shared.log.info(f'Extra network save: file="{filename}" description={state.description}')
- except Exception as e:
- shared.log.error(f'Extra network save: file="{filename}" {e}')
-
- if state.op == 'saveCardPreview':
- if item is None or item.local_preview is None or len(item.local_preview) == 0:
- return
- try:
- images = list(ui.gallery.temp_files)
- if len(images) < 1:
- shared.log.error(f'Extra network save: preview="{item.local_preview}" no images')
- return
- image = Image.open(images[state.index])
- except Exception as e:
- shared.log.error(f'Extra network save: preview="{item.local_preview}" {e}')
- return
- if image.width > 512 or image.height > 512:
- image = image.convert('RGB')
- image.thumbnail((512, 512), Image.HAMMING)
- image.save(item.local_preview, quality=50)
- thumb = os.path.splitext(item.local_preview)[0] + '.thumb.jpg'
- if os.path.exists(thumb):
- shared.log.debug(f'Extra network delete: thumbnail="{thumb}"')
- os.remove(thumb)
- shared.log.info(f'Extra network save: preview="{item.local_preview}"')
-
def toggle_visibility(is_visible):
is_visible = not is_visible
return is_visible, gr.update(visible=is_visible), gr.update(variant=("secondary-down" if is_visible else "secondary"))
@@ -420,21 +386,21 @@ def create_ui(container, button_parent, tabname, skip_indexing = False):
img = gr.Image(value=None, show_label=False, interactive=False, container=True)
ui.details_components.append(img)
with gr.Row():
- gr.Button('Replace')
- gr.Button('Delete')
+ btn_save_img = gr.Button('Replace', elem_classes=['small-button'])
+ btn_delete_img = gr.Button('Delete', elem_classes=['small-button'])
with gr.Tabs():
with gr.Tab('Description'):
- desc = gr.Textbox('', show_label=False, lines=8)
+ desc = gr.Textbox('', show_label=False, lines=8, placeholder="Extra network description...")
ui.details_components.append(desc)
with gr.Row():
- gr.Button('Save')
- gr.Button('Delete')
+ btn_save_desc = gr.Button('Save', elem_classes=['small-button'])
+ btn_delete_desc = gr.Button('Delete', elem_classes=['small-button'])
with gr.Tab('Info'):
- info = gr.Textbox('', show_label=False, lines=8)
+ info = gr.Textbox('', show_label=False, lines=8, placeholder="Extra network info...")
ui.details_components.append(info)
with gr.Row():
- gr.Button('Save')
- gr.Button('Delete')
+ btn_save_info = gr.Button('Save', elem_classes=['small-button'])
+ btn_delete_info = gr.Button('Delete', elem_classes=['small-button'])
with gr.Tab('Metadata'):
meta = gr.JSON({}, show_label=False, lines=8)
ui.details_components.append(meta)
@@ -443,7 +409,7 @@ def create_ui(container, button_parent, tabname, skip_indexing = False):
button_refresh = ToolButton(symbols.refresh, elem_id=tabname+"_extra_refresh")
button_close = ToolButton(symbols.close, elem_id=tabname+"_extra_close")
ui.search = gr.Textbox('', show_label=False, elem_id=tabname+"_extra_search", placeholder="Search...", elem_classes="textbox", lines=2)
- ui.description = gr.Textbox('', show_label=False, elem_id=tabname+"_description", placeholder="Save/Replace Extra Network Description...", elem_classes="textbox", lines=2)
+ ui.description = gr.Textbox('', show_label=False, elem_id=tabname+"_description", elem_classes="textbox", lines=2, interactive=False)
if ui.tabname == 'txt2img': # refresh only once
global refresh_time # pylint: disable=global-statement
@@ -454,26 +420,125 @@ def create_ui(container, button_parent, tabname, skip_indexing = False):
hmtl = gr.HTML(page.html, elem_id=f'{tabname}{page.name}_extra_page', elem_classes="extra-networks-page")
ui.pages.append(hmtl)
+ def fn_save_img(index):
+ if index is None or index < 0 or ui.last_item is None or ui.last_item.local_preview is None:
+ return 'html/card-no-preview.png'
+ images = list(ui.gallery.temp_files)
+ if len(images) < index + 1:
+ shared.log.warning(f'Extra network no image: item={ui.last_item.name}')
+ return 'html/card-no-preview.png'
+ try:
+ image = Image.open(images[index])
+ except Exception as e:
+ shared.log.error(f'Extra network error opening image: item={ui.last_item.name} {e}')
+ return 'html/card-no-preview.png'
+ fn_delete_img()
+ if image.width > 512 or image.height > 512:
+ image = image.convert('RGB')
+ image.thumbnail((512, 512), Image.HAMMING)
+ image.save(ui.last_item.local_preview, quality=50)
+ shared.log.debug(f'Extra network save image: item={ui.last_item.name} filename={ui.last_item.local_preview}')
+ return image
+
+ def fn_delete_img():
+ preview_extensions = ["jpg", "jpeg", "png", "webp", "tiff", "jp2"]
+ fn = os.path.splitext(ui.last_item.filename)[0]
+ for file in [f'{fn}{mid}{ext}' for ext in preview_extensions for mid in ['.thumb.', '.preview.', '.']]:
+ if os.path.exists(file):
+ os.remove(file)
+ shared.log.debug(f'Extra network delete image: item={ui.last_item.name} filename={file}')
+ return 'html/card-no-preview.png'
+
+ def fn_save_desc(desc):
+ fn = os.path.splitext(ui.last_item.filename)[0] + '.txt'
+ with open(fn, 'w', encoding='utf-8') as f:
+ f.write(desc)
+ shared.log.debug(f'Extra network save desc: item={ui.last_item.name} filename={fn}')
+ return desc
+
+ def fn_delete_desc(desc):
+ if ui.last_item is None:
+ return desc
+ fn = os.path.splitext(ui.last_item.filename)[0] + '.txt'
+ if os.path.exists(fn):
+ shared.log.debug(f'Extra network delete desc: item={ui.last_item.name} filename={fn}')
+ os.remove(fn)
+ return ''
+ return desc
+
+ def fn_save_info(info):
+ fn = os.path.splitext(ui.last_item.filename)[0] + '.info'
+ with open(fn, 'w', encoding='utf-8') as f:
+ f.write(info)
+ shared.log.debug(f'Extra network save desc: item={ui.last_item.name} filename={fn}')
+ return info
+
+ def fn_delete_info(info):
+ if ui.last_item is None:
+ return info
+ fn = os.path.splitext(ui.last_item.filename)[0] + '.info'
+ if os.path.exists(fn):
+ shared.log.debug(f'Extra network delete info: item={ui.last_item.name} filename={fn}')
+ os.remove(fn)
+ return ''
+ return info
+
+ btn_save_img.click(fn=fn_save_img, _js='(img) => { return selected_gallery_index() }', inputs=[img], outputs=[img])
+ btn_delete_img.click(fn=fn_delete_img, inputs=[], outputs=[img])
+ btn_save_desc.click(fn=fn_save_desc, inputs=[desc], outputs=[desc])
+ btn_delete_desc.click(fn=fn_delete_desc, inputs=[desc], outputs=[desc])
+ btn_save_info.click(fn=fn_save_info, inputs=[info], outputs=[info])
+ btn_delete_info.click(fn=fn_delete_info, inputs=[info], outputs=[info])
+
def show_details(text, img, desc, info, meta):
page, item = get_item(state)
if item is not None and os.path.exists(item.filename):
stat = os.stat(item.filename)
+ desc = item.description
+ info = page.info.get(item.name, 'N/A')
+ meta = page.metadata.get(item.name, {}) or {}
+ if type(meta) is str:
+ try:
+ meta = json.loads(meta)
+ except:
+ meta = {}
+ img = page.find_preview_file(item.filename)
+ lora = ''
+ model = ''
+ if page.title == 'Model':
+ merge = len(list(meta.get('sd_merge_models', {})))
+ if merge > 0:
+ model += f'
| Merge models | {merge} recipes |
'
+ if meta.get('modelspec.architecture', None) is not None:
+ model += f'''
+
| Architecture | {meta.get('modelspec.architecture', 'N/A')} |
+
| Title | {meta.get('modelspec.title', 'N/A')} |
+
| Resolution | {meta.get('modelspec.resolution', 'N/A')} |
+ '''
+ if page.title == 'Lora':
+ tags = getattr(item, 'tags', {})
+ tags = [f'{name}:{tags[name]}' for i, name in enumerate(tags)]
+ tags = ' '.join(tags)
+ lora = f'''
+
| Tags | {tags} |
+
| Base model | {meta.get('ss_sd_model_name', 'N/A')} |
+
| Resolution | {meta.get('ss_resolution', 'N/A')} |
+
| Training images | {meta.get('ss_num_train_images', 'N/A')} |
+
| Comment | {meta.get('ss_training_comment', 'N/A')} |
+ '''
text = f'''
{item.name}
| Type | {page.title} |
| Alias | {getattr(item, 'alias', 'N/A')} |
- | Tags | {getattr(item, 'tags', 'N/A')} |
| Filename | {item.filename} |
| Hash | {getattr(item, 'hash', 'N/A')} |
| Size | {round(stat.st_size/1024/1024, 2)} MB |
| Last modified | {datetime.fromtimestamp(stat.st_mtime)} |
+ {lora}
+ {model}
'''
- desc = item.description
- info = page.info.get(item.name, 'N/A')
- meta = page.metadata.get(item.name, 'N/A')
- img = page.find_preview_file(item.filename)
return [text, img, desc, info, meta, gr.update(visible=True)]
def en_refresh(title):
diff --git a/wiki b/wiki
index 521f222ba..9dc03276d 160000
--- a/wiki
+++ b/wiki
@@ -1 +1 @@
-Subproject commit 521f222bae95fcc70ad71809163567fd7431ef48
+Subproject commit 9dc03276dbbd8fb28fd3c24cc29e8fa15ef9a4be