diff --git a/html/reference.json b/html/reference.json index a7c42a338..4fd74b92c 100644 --- a/html/reference.json +++ b/html/reference.json @@ -181,6 +181,15 @@ "extras": "", "size": 56.1, "date": "2025 August" + }, + "Qwen-Image-2512": { + "path": "Qwen/Qwen-Image-2512", + "preview": "Qwen--Qwen-Image-2512.jpg", + "desc": "Qwen-Image-2512 is an Qwen Image successor, that significantly reduces the AI-generated look, got finer natural detailils and improved text rendering.", + "skip": true, + "extras": "", + "size": 53.7, + "date": "2025 December" }, "Qwen-Image-Edit": { "path": "Qwen/Qwen-Image-Edit", diff --git a/models/Reference/Qwen--Qwen-Image-2512.jpg b/models/Reference/Qwen--Qwen-Image-2512.jpg new file mode 100644 index 000000000..dc1839828 Binary files /dev/null and b/models/Reference/Qwen--Qwen-Image-2512.jpg differ diff --git a/modules/extensions.py b/modules/extensions.py index b1b5d4e32..b29a6cbb0 100644 --- a/modules/extensions.py +++ b/modules/extensions.py @@ -25,10 +25,12 @@ def parse_isotime(time_string: str) -> datetime: raise ValueError(f"Unexpected time string format: '{time_string}'") -def format_dt(d: datetime) -> str: +def format_dt(d: datetime, seconds = False) -> str: if d.tzinfo is None: return d.strftime('%Y-%m-%d %H:%M') - return d.astimezone(timezone.utc).strftime('%Y-%m-%d %H:%M:%S') # Ensure UTC time is shown (just in case) + if seconds: + return d.astimezone(timezone.utc).strftime('%Y-%m-%d %H:%M:%S') + return d.astimezone(timezone.utc).strftime('%Y-%m-%d %H:%M') def ts2utc(timestamp: int) -> datetime: diff --git a/modules/ui_extensions.py b/modules/ui_extensions.py index a6c5ca3ec..4a800ec73 100644 --- a/modules/ui_extensions.py +++ b/modules/ui_extensions.py @@ -3,6 +3,7 @@ import json import shutil import errno import html +import re from datetime import datetime, timezone, timedelta import gradio as gr from modules import extensions, shared, paths, errors, ui_symbols, call_queue @@ -29,6 +30,10 @@ sort_ordering = { } +re_snake_case = re.compile(r'_(?=[a-zA-z0-9])') +re_camelCase = re.compile(r'(?<=[a-z])([A-Z])') + + def get_installed(ext): installed = [e for e in extensions.extensions if (e.remote or '').startswith(ext['url'].replace('.git', ''))] return installed[0] if len(installed) > 0 else None @@ -108,10 +113,10 @@ def check_updates(_id_task, disable_list, search_text, sort_column): ext.git_fetch() ext.read_info() commit_date = ext.commit_date or 1577836800 - shared.log.info(f'Extensions updated: {ext.name} {ext.commit_hash[:8]} {extensions.format_dt(extensions.ts2utc(commit_date))}') + shared.log.info(f'Extensions updated: {ext.name} {ext.commit_hash[:8]} {extensions.format_dt(extensions.ts2utc(commit_date), seconds=True)}') else: commit_date = ext.commit_date or 1577836800 - shared.log.debug(f'Extensions no update available: {ext.name} {ext.commit_hash[:8]} {extensions.format_dt(extensions.ts2utc(commit_date))}') + shared.log.debug(f'Extensions no update available: {ext.name} {ext.commit_hash[:8]} {extensions.format_dt(extensions.ts2utc(commit_date), seconds=True)}') except FileNotFoundError as e: if 'FETCH_HEAD' not in str(e): raise @@ -121,26 +126,27 @@ def check_updates(_id_task, disable_list, search_text, sort_column): return create_html(search_text, sort_column), "Extension update complete | Restart required" -def normalize_git_url(url) -> str: - return '' if url is None else url.removesuffix('.git') +def normalize_git_url(url: str | None) -> str: + return '' if url is None else url.strip().removesuffix('.git') def install_extension_from_url(dirname, url, branch_name, search_text, sort_column): if shared.cmd_opts.disable_extension_access: shared.log.error('Extension: apply changes disallowed because public access is enabled and insecure is not specified') return ['', ''] - if url is None or len(url) == 0: + url = normalize_git_url(url) + if not url: shared.log.error('Extension: url is not specified') return ['', ''] - if dirname is None or dirname == "": - dirname = normalize_git_url(url.split('/')[-1]) + if not dirname: + dirname = url.split('/')[-1] target_dir = os.path.join(extensions.extensions_dir, dirname) shared.log.info(f'Installing extension: {url} into {target_dir}') if os.path.exists(target_dir): shared.log.error(f'Extension: path="{target_dir}" directory already exists') return ['', ''] - url = normalize_git_url(url) - assert len([x for x in extensions.extensions if normalize_git_url(x.remote) == url]) == 0, 'Extension with this URL is already installed' + if any(normalize_git_url(x.remote) == url for x in extensions.extensions): + return ['', "Extension with this URL is already installed"] tmpdir = os.path.join(paths.data_path, "tmp", dirname) try: import git @@ -229,10 +235,10 @@ def update_extension(extension_path, search_text, sort_column): ext.git_fetch() ext.read_info() commit_date = ext.commit_date or 1577836800 - shared.log.info(f'Extensions updated: {ext.name} {ext.commit_hash[:8]} {extensions.format_dt(extensions.ts2utc(commit_date))}') + shared.log.info(f'Extensions updated: {ext.name} {ext.commit_hash[:8]} {extensions.format_dt(extensions.ts2utc(commit_date), seconds=True)}') else: commit_date = ext.commit_date or 1577836800 - shared.log.info(f'Extensions no update available: {ext.name} {ext.commit_hash[:8]} {extensions.format_dt(extensions.ts2utc(commit_date))}') + shared.log.info(f'Extensions no update available: {ext.name} {ext.commit_hash[:8]} {extensions.format_dt(extensions.ts2utc(commit_date), seconds=True)}') except FileNotFoundError as e: if 'FETCH_HEAD' not in str(e): raise @@ -268,6 +274,12 @@ def search_extensions(search_text, sort_column): return code, f'Search | {search_text} | {sort_column}' +def make_wrappable_html(text: str) -> str: + text = html.escape(text) + text = re_snake_case.sub("_", text) + return re_camelCase.sub(r"\1", text) + + def create_html(search_text, sort_column): # shared.log.debug(f'Extensions manager: refresh list search="{search_text}" sort="{sort_column}"') code = """ @@ -284,8 +296,8 @@ def create_html(search_text, sort_column): - Status - Enabled + + Extension Description Type @@ -331,7 +343,7 @@ def create_html(search_text, sort_column): local_ver_date = extensions.ts2utc(ext['commit_date']) # TZ-aware update_available = (installed is not None) and (not ext['is_builtin']) and (ext['remote'] is not None) and (updated > local_ver_date) # TZ-aware if update_available: - debug(f'Extension update available: name={ext["name"]} updated={extensions.format_dt(updated)} commit={extensions.format_dt(local_ver_date)}') # TZ-aware + debug(f'Extension update available: name={ext["name"]} updated={extensions.format_dt(updated, seconds=True)} commit={extensions.format_dt(local_ver_date, seconds=True)}') # TZ-aware ext['sort_user'] = f"{'0' if ext['is_builtin'] else '1'}{'1' if ext['installed'] else '0'}{ext.get('name', '')}" ext['sort_enabled'] = f"{'0' if ext['enabled'] else '1'}{'1' if ext['is_builtin'] else '0'}{'1' if ext['installed'] else '0'}{ext.get('updated', '2000-01-01T00:00Z')}" ext['sort_update'] = f"{'1' if update_available else '0'}{'1' if ext['installed'] else '0'}{ext.get('updated', '2000-01-01T00:00Z')}" @@ -368,7 +380,7 @@ def create_html(search_text, sort_column): stats['enabled'] += 1 type_code = f"""
{"SYSTEM" if ext['is_builtin'] else 'USER'}
""" version_code = f"""
{ext['version']}
""" - enabled_code = f"""""" + enabled_code = f"""""" masked_path = html.escape(ext.get("path", "").replace('\\', '/')) if not ext['is_builtin']: install_code = f"""""" @@ -380,36 +392,36 @@ def create_html(search_text, sort_column): if ext.get('status', None) is None or type(ext['status']) == str: # old format ext['status'] = 0 if ext['url'] is None or ext['url'] == '': - status = f"
{ui_symbols.svg_bullet.color('#00C0FD')}
" + status = f"
{ui_symbols.svg_bullet.style('#00C0FD')}
" elif ext['status'] > 0: if ext['status'] == 1: - status = f"
{ui_symbols.svg_bullet.color('#00FD9C')}
" + status = f"
{ui_symbols.svg_bullet.style('#00FD9C')}
" elif ext['status'] == 2: - status = f"
{ui_symbols.svg_bullet.color('#FFC300')}
" + status = f"
{ui_symbols.svg_bullet.style('#FFC300')}
" elif ext['status'] == 3: - status = f"
{ui_symbols.svg_bullet.color('#FFC300')}
" + status = f"
{ui_symbols.svg_bullet.style('#FFC300')}
" elif ext['status'] == 4: - status = f"
{ui_symbols.svg_bullet.color('#4E22FF')}
" + status = f"
{ui_symbols.svg_bullet.style('#4E22FF')}
" elif ext['status'] == 5: - status = f"
{ui_symbols.svg_bullet.color('#CE0000')}
" + status = f"
{ui_symbols.svg_bullet.style('#CE0000')}
" elif ext['status'] == 6: - status = f"
{ui_symbols.svg_bullet.color('#AEAEAE')}
" + status = f"
{ui_symbols.svg_bullet.style('#AEAEAE')}
" else: - status = f"
{ui_symbols.svg_bullet.color('#008EBC')}
" + status = f"
{ui_symbols.svg_bullet.style('#008EBC')}
" else: if updated < datetime.now(timezone.utc) - timedelta(6*30): # TZ-aware - status = f"
{ui_symbols.svg_bullet.color('#C000CF')}
" + status = f"
{ui_symbols.svg_bullet.style('#C000CF')}
" else: - status = f"
{ui_symbols.svg_bullet.color('#7C7C7C')}
" + status = f"
{ui_symbols.svg_bullet.style('#7C7C7C')}
" code += f""" - {status} {enabled_code} - {html.escape(ext.get("name", "unknown"))}
{tags_text} + {status} + {make_wrappable_html(ext.get("name", "unknown"))}
{tags_text} {html.escape(ext.get("description", ""))} -

Created {html.escape(dt('created'))} | Added {html.escape(dt('added'))} | Pushed {html.escape(dt('pushed'))} | Updated {html.escape(dt('updated'))}

-

{author} | Stars {html.escape(str(ext.get('stars', 0)))} | Size {html.escape(str(ext.get('size', 0)))} | Commits {html.escape(str(ext.get('commits', 0)))} | Issues {html.escape(str(ext.get('issues', 0)))} | Trending {html.escape(str(ext['sort_trending']))}

+

Created: {html.escape(dt('created'))} | Added: {html.escape(dt('added'))} | Pushed: {html.escape(dt('pushed'))} | Updated: {html.escape(dt('updated'))}

+

{author} | Stars: {html.escape(str(ext.get('stars', 0)))} | Size: {html.escape(str(ext.get('size', 0)))} | Commits: {html.escape(str(ext.get('commits', 0)))} | Issues: {html.escape(str(ext.get('issues', 0)))} | Trending: {html.escape(str(ext['sort_trending']))}

{type_code} {version_code} diff --git a/modules/ui_symbols.py b/modules/ui_symbols.py index ddc16c273..c880a4a8f 100644 --- a/modules/ui_symbols.py +++ b/modules/ui_symbols.py @@ -1,3 +1,10 @@ +import re +from functools import lru_cache +from typing import final + + +# Basic symbols + refresh = '⟲' close = '✕' load = '⇧' @@ -41,23 +48,45 @@ sort_time_dsc = '\uf0dd' style_apply = '↶' style_save = '↷' +# Configurable symbols + +@final class SVGSymbol: + __created = [] + __re_display = re.compile(r"(?<=display:)\s*([\w\-]+)(?=;)") + + @classmethod + @lru_cache # Class method due to B019, but also mostly so the `style` method shows params in IDE + def __stylize(cls, svg: str, color: str | None = None, display: str | None = None): + if color: + svg = re.sub("currentColor", color, svg) + if display: + svg = cls.__re_display.sub(display, svg, count=1) + return svg + def __init__(self, svg: str): + svg = re.sub(r"\s{2,}", " ", svg.replace("\n", "")).replace("> <", "><").strip() + if svg in self.__created: + raise RuntimeError("SVGSymbol class was created with an existing value. There should only be one instance per symbol.", svg) + else: + self.__created.append(svg) self.svg = svg - self.before = "" - self.after = "" self.supports_color = False + self.supports_display = False if "currentColor" in self.svg: self.supports_color = True - self.before, self.after = self.svg.split("currentColor", maxsplit=1) + if self.__re_display.search(self.svg): + self.supports_display = True - def color(self, color: str): - if self.supports_color: - return self.before + color + self.after - else: - return self.svg + def style(self, color: str | None = None, display: str | None = None) -> str: + style_args = { + "color": color if color and self.supports_color else None, + "display": display if display and self.supports_display else None + } + return self.__stylize(self.svg, **style_args) def __str__(self): return self.svg -svg_bullet = SVGSymbol("") + +svg_bullet = SVGSymbol("")