diff --git a/.github/copilot-instructions.md b/.github/copilot-instructions.md index 0f0bbe83d..b9e2b4e0c 100644 --- a/.github/copilot-instructions.md +++ b/.github/copilot-instructions.md @@ -125,4 +125,8 @@ Use these repo-local skills for recurring SD.Next model integration work: File: `.github/skills/todo/SKILL.md` Use when searching the full codebase for `TODO` markers and producing a markdown document with proposed next steps for each item. +- `update-docs` + File: `.github/skills/update-docs/SKILL.md` + Use when reading markdown files from `wiki/` to correct markdown syntax, improve readability, and optionally normalize structure, links, and terminology while preserving technical meaning. + When creating and updating skills, update this file and the index in `.github/skills/README.md` accordingly. diff --git a/.github/skills/README.md b/.github/skills/README.md index d41712ee2..a1f4a3c70 100644 --- a/.github/skills/README.md +++ b/.github/skills/README.md @@ -60,6 +60,10 @@ This folder contains repo-local Copilot skills for recurring SD.Next tasks. File: `todo/SKILL.md` Use when scanning the full codebase for `TODO` markers and producing a markdown document with proposed next steps for each item. +- `update-docs` + File: `update-docs/SKILL.md` + Use when reading markdown files from `wiki/` to correct markdown syntax, improve readability, and optionally normalize structure/links while preserving technical meaning. + ## Notes - Keep skills narrowly task-oriented and reusable. diff --git a/.github/skills/update-docs/SKILL.md b/.github/skills/update-docs/SKILL.md new file mode 100644 index 000000000..ca0c728be --- /dev/null +++ b/.github/skills/update-docs/SKILL.md @@ -0,0 +1,174 @@ +--- +name: update-docs +description: "Update wiki markdown docs for syntax correctness, readability, link integrity, heading hierarchy normalization, and code block language tagging. Use when a user asks to clean up markdown formatting and improve clarity while preserving technical meaning." +argument-hint: "Provide one or more wiki/*.md paths and optional scope (syntax-only, readability, or full pass)" +--- + +# Update Wiki Docs + +Read markdown files in the `wiki/` folder, fix markdown syntax issues, and improve readability without changing technical intent. + +## When To Use + +- The user asks to clean, polish, or normalize markdown docs in `wiki/` +- Headings/lists/code fences/tables render incorrectly +- Documentation is hard to scan due to long paragraphs or inconsistent structure +- A doc needs editorial cleanup before sharing or release + +## Scope + +Primary scope: + +- Files under `wiki/**/*.md`, but can be used on any markdown file in the repo if specified by the user +- Skip any files starting with `_` (e.g. `_footer.md`) or non-markdown files (e.g. `LICENSE.txt`) to avoid unintended edits to non-doc files or templates. + +Baseline actions: + +- Correct markdown syntax +- Correct general readability issues +- Run link integrity pass for obvious broken links and anchors +- Normalize heading hierarchy and section flow +- Add or correct code block language tags when known + +Expanded actions (when user allows full pass): + +- Improve list/table/code-block consistency +- Standardize terminology and naming across the document +- Flag stale or unverifiable claims for follow-up + +## Style Rules + +- Write in concise technical style that remains approachable to non-expert users +- Prefer short to medium sentences; avoid unnecessary long sentences +- Explain terms briefly when first used if they may be unfamiliar +- Avoid unexplained jargon and acronym-heavy phrasing +- Keep wording direct, specific, and neutral + +## Safety Rules + +- Preserve technical meaning and factual content +- Do not invent commands, API behavior, or version claims +- Keep edits narrow and reversible +- Preserve existing project-specific terminology unless clearly inconsistent +- If uncertainty is high, prefer adding a short clarification request over guessing + +## Procedure + +## Validation Tool + +Use the repo-local validation script before and after doc edits when possible: + +- `test/check-docs` to validate all wiki markdown files +- `test/check-docs wiki/File.md` to validate one or more specific files +- `test/check-docs --fix wiki/File.md` only when a safe markdownlint auto-fix is appropriate + +### 1. Confirm Target And Depth + +Extract from user prompt: + +- target markdown file(s) in `wiki/` +- desired depth: syntax-only, readability, or full pass +- constraints (tone, audience, preserve wording, max rewrite level) + +If targets are missing, ask for paths before editing. + +### 2. Read And Diagnose + +For each target file: + +- If the scope is broad or unclear, run `test/check-docs` first to establish the current markdown baseline. +- Scan for syntax/rendering issues +- Identify readability pain points (dense blocks, weak headings, mixed terminology) +- Note risky sections where edits may alter meaning + +### 3. Normalize Heading Hierarchy + +Apply heading structure rules before deep rewrites: + +- keep one logical top-level heading per file where appropriate +- avoid skipped levels (`##` directly to `####`) unless source constraints require it +- ensure sibling sections use consistent levels +- rename headings only when it improves clarity without changing meaning + +### 4. Apply Syntax Fixes First + +Fix rendering/correctness issues first, such as: + +- broken heading levels +- malformed lists +- unclosed/misfenced code blocks +- malformed links/images +- inconsistent table delimiter rows +- accidental HTML/markdown mixing that breaks rendering + +### 5. Apply Readability Improvements + +Make editorial improvements while preserving meaning: + +- split long paragraphs +- convert prose enumerations into lists when clearer +- improve section titles for scanability +- remove repetition and tighten wording +- align terminology within the same document + +Apply tone constraints during edits: + +- concise technical phrasing +- approachable wording for normal users +- no unexplained technical babble + +### 6. Run Link Integrity Pass + +Check and fix obvious link issues: + +- malformed inline/reference links +- anchors that no longer match heading text after edits +- obvious relative-path mistakes in wiki cross-links + +If link targets cannot be verified from repo context, keep the original target and flag it in the report. + +### 7. Add Code Block Language Tags + +For fenced code blocks: + +- add language tags when confidently inferable (`bash`, `python`, `json`, `yaml`, etc.) +- correct clearly wrong tags +- leave tag blank only when language cannot be inferred safely + +### 8. Run Completion Checks + +Validate each edited file against this checklist: + +- `test/check-docs` or `test/check-docs ` passes for the affected scope +- markdown renders correctly +- heading hierarchy is logical +- code fences include language where known +- links/anchors are internally consistent and obviously valid +- no factual changes introduced +- tone is concise, technical, and approachable + +### 9. Report Results + +Return: + +- files edited +- categories of changes made (syntax/readability/structure) +- any unresolved ambiguity or potential factual follow-ups + +## Branching Guidance + +- If the user asks minimal edits, still run heading normalization, link integrity checks, and code-block language tagging with minimal wording changes. +- If the user asks broad cleanup, run full pass including structure and terminology normalization. +- If a section appears technically outdated but cannot be verified from repo context, do not rewrite claims; flag it in the report. + +## Pass Criteria + +A successful pass means: + +- requested wiki markdown files were edited +- syntax/rendering issues were corrected +- heading hierarchy was normalized +- link integrity pass was completed +- code block language tagging was applied where known +- readability clearly improved without changing intent +- output report summarizes edits and open follow-ups diff --git a/CHANGELOG.md b/CHANGELOG.md index c702ba997..61345ca84 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,20 +1,23 @@ # Change Log for SD.Next -## Update for 2026-04-16 +## Update for 2026-04-21 -### Highlights for 2026-04-16 +### Highlights for 2026-04-21 *What's New?* -- Built-in **Tag-Autocomplete** with support for 10+ tag databases - New models! **Zeta-Chroma**, **ERNIE**, **Nucleus**, **Bria-FIBO**, **Anima-v3**, **SDXS-1B**, **LTX 2.3 v1.1** +- Major **Kanvas** update for enhanced inpaint/outpaint and overal more responsive **UI** +- Built-in **Tag-Autocomplete** with support for *10+* tag databases - Additional Schedulers, updates to NudeNet, OpenVINO and ROCm and other features And tons of *quality-of-life* improvements and *bug-fixes*! In addition, to jump on a bandwagon, we're now fully **AI agent** ready with detailed instructions and skills! +For full details, see [ChangeLog](https://github.com/vladmandic/automatic/blob/master/CHANGELOG.md) + [ReadMe](https://github.com/vladmandic/automatic/blob/master/README.md) | [ChangeLog](https://github.com/vladmandic/automatic/blob/master/CHANGELOG.md) | [Docs](https://vladmandic.github.io/sdnext-docs/) | [WiKi](https://github.com/vladmandic/automatic/wiki) | [Discord](https://discord.com/invite/sd-next-federal-batch-inspectors-1101998836328697867) | [Sponsor](https://github.com/sponsors/vladmandic) -### Details for 2026-04-16 +### Details for 2026-04-21 - **Models** - [Zeta-Chroma](https://huggingface.co/lodestones/Zeta-Chroma) pixel-space diffusion transformer image model @@ -51,6 +54,7 @@ In addition, to jump on a bandwagon, we're now fully **AI agent** ready with det allows for any *settings* property name (as defined in `modules/ui_definitions.py` and saved to `config.json`) - **preview** add explicit `method=None` if you want to skip preview, but show finished images, works with batch progression + - add **xet cache** to *settings -> paths* and initialize on startup - **Compute** - **ROCm** futher work on advanced configuration and tuning, thanks @resonantsky now covers both ROCm on Windows and Linux @@ -63,9 +67,18 @@ In addition, to jump on a bandwagon, we're now fully **AI agent** ready with det - **SDNQ** improvements add quant support to `nn.Embedding` type support fp execution according to gpu capabilities + enhanced `triton` kernels for RDNA2/RDNA3 +- **Kanvas** + multi-image workflow: add additional stagaes as needed (when starting generate, sdnext will use image/mask from active stage) + full undo/redo + list, select, transform any shapes + magic-wand paint with auto-fill and perceptual tolerance + see [Kanvas Docs](https://vladmandic.github.io/sdnext-docs/Kanvas) for details - **UI** - `gallery` send-to button advanced options with right-click - `tag autocomplete` quick toggle in main prompt area + - a lot of small performance optimizations that add up to faster load times and more responsive ui + - add ui `log` during startup - **Caption & Prompt Enhance** - [Google Gemma 4] in *E2B* and *E4B* variants as well as *heretic* fine-tune - **Agents** @@ -81,17 +94,29 @@ In addition, to jump on a bandwagon, we're now fully **AI agent** ready with det *model*: `port-model`, `debug-model`, `analyze-model`, `reference-catalog` *github*: `github-issues`, `github-features` *diffusers*: `diffusers-code` + *docs*: `update-docs` *other*: `todo` - **CLI** - add `cli/hf-info` and update `cli/hf-search.py` +- **Docs** + - validation of all links + - syntax/structure/language corrections accross all documents - **Obsoleted** - removed *system-info* from *extensions-builtin* - **Internal** + - sync `kanvas` branch with core branch - `history` accepts both latent and pixel entries - wrap `hf-download` methods - additional *typing* and *typechecks*, thanks @awsr - refactor `hash-cache` management, thanks @awsr - validate all `reference` jsons and backfill all fields + - sticter `js` linting, thanks @awsr + - ui: remove non-passive event listeners + - ui: add debounce to ui updates + - ui: utilize requestanimationframe for paint optimizations + - ui: profile callbacks + - ui: validate callbacks before use, thanks @awsr + - ui: log formatting - **Fixes** - Prohibit `python==3.14` unless `--experimental` - UI CSS fixes, thanks @awsr @@ -113,7 +138,7 @@ In addition, to jump on a bandwagon, we're now fully **AI agent** ready with det - patch `z-image` for fp16 compatibility, thanks @resonantsky - patch `unipc` for timesteps device placement, thanks @resonantsky - `civitai` search and base-model discovery improvements - - ui log formatting + - auto-masking with `rembg` ## Update for 2026-04-01 diff --git a/data/reference-quant.json b/data/reference-quant.json index 0869aa52c..edeb0dbf2 100644 --- a/data/reference-quant.json +++ b/data/reference-quant.json @@ -238,7 +238,7 @@ }, "Baidu ERNIE-Image sdnq-dynamic-int4": { "path": "OzzyGT/ERNIE_Image_sdnq_dynamic_int4", - "preview": "baidu--ERNIE-Image.jpg", + "preview": "OzzyGT--ERNIE_Image_sdnq_dynamic_int4.jpg", "desc": "ERNIE-Image is a text-to-image diffusion transformer model that combines a Mistral3 text encoder with a FlowMatch transformer and Flux2-style VAE for 1024px image generation.", "skip": true, "extras": "sampler: Default, cfg_scale: 4.0, steps: 50", @@ -247,7 +247,7 @@ }, "Baidu ERNIE-Image-Turbo sdnq-dynamic-int4": { "path": "OzzyGT/ERNIE_Image_Turbo_sdnq_dynamic_int4", - "preview": "baidu--ERNIE-Image-Turbo.jpg", + "preview": "OzzyGT--ERNIE_Image_Turbo_sdnq_dynamic_int4.jpg", "desc": "ERNIE-Image-Turbo is a distilled ERNIE-Image variant optimized for fast generation with fewer denoising steps.", "skip": true, "extras": "sampler: Default, cfg_scale: 1.0, steps: 8", diff --git a/eslint.config.mjs b/eslint.config.mjs index eb04c771d..2f247dafa 100644 --- a/eslint.config.mjs +++ b/eslint.config.mjs @@ -168,7 +168,7 @@ const jsConfig = defineConfig([ '@stylistic/max-len': [ 'warn', { - code: 275, + code: 300, tabWidth: 2, }, ], diff --git a/extensions-builtin/sdnext-kanvas b/extensions-builtin/sdnext-kanvas index 1e840033b..e58871fc7 160000 --- a/extensions-builtin/sdnext-kanvas +++ b/extensions-builtin/sdnext-kanvas @@ -1 +1 @@ -Subproject commit 1e840033b040d8915ddfb5dbf62c80f411bcec0a +Subproject commit e58871fc70d5a6bd9a1e29eac31fb96d1c51201d diff --git a/extensions-builtin/sdnext-modernui b/extensions-builtin/sdnext-modernui index 8cbc80bc4..447324070 160000 --- a/extensions-builtin/sdnext-modernui +++ b/extensions-builtin/sdnext-modernui @@ -1 +1 @@ -Subproject commit 8cbc80bc49410c4cc436aacd0d25a3290f53630f +Subproject commit 447324070e71df8430ca3ca6191798c299218a5e diff --git a/installer.py b/installer.py index 98856072e..1d559822f 100644 --- a/installer.py +++ b/installer.py @@ -179,7 +179,7 @@ def installed(package, friendly: str | None = None, quiet = False): # pylint: di if args.experimental: log.warning(f'Install: package="{p[0]}" installed={pkg_version} required={p[1]} allowing experimental') else: - log.warning(f'Install: package="{p[0]}" installed={pkg_version} required={p[1]} version mismatch') + log.warning(f'Install: package="{p[0]}" installed={pkg_version} required={p[1]} updating...') global restart_required # pylint: disable=global-statement restart_required = True ok = ok and (exact or args.experimental) @@ -1363,12 +1363,14 @@ def get_version(force=False): subprocess.run('git config log.showsignature false', capture_output=True, shell=True, check=True) except Exception: pass + try: ver = run('git', 'log --pretty=format:"%h %ad" -1 --date=short', check=True)[0].stdout or ' ' commit, updated = ver.split(' ') version['commit'], version['updated'] = commit, updated except Exception as e: log.warning(f'Version: where=commit {e}') + try: origin = run('git', 'remote get-url origin', check=True)[0].stdout branch_name = run('git', 'rev-parse --abbrev-ref HEAD', check=True)[0].stdout @@ -1378,6 +1380,7 @@ def get_version(force=False): log.warning('Version: detached state detected') except Exception as e: log.warning(f'Version: where=branch {e}') + try: if os.path.exists('extensions-builtin/sdnext-modernui'): branch_ui = run('git', 'rev-parse --abbrev-ref HEAD', check=True, cwd='extensions-builtin/sdnext-modernui')[0].stdout @@ -1387,6 +1390,7 @@ def get_version(force=False): except Exception as e: log.warning(f'Version: where=modernui {e}') version['ui'] = 'unknown' + try: if os.environ.get('SD_KANVAS_DISABLE', None) is not None: version['kanvas'] = 'disabled' @@ -1398,6 +1402,7 @@ def get_version(force=False): except Exception as e: log.warning(f'Version: where=kanvas {e}') version['kanvas'] = 'unknown' + ts('version', t_start) return version @@ -1429,6 +1434,33 @@ def check_ui(ver): ts('ui', t_start) +def check_kanvas(ver): + def same(ver): + core = ver['branch'] if ver is not None and 'branch' in ver else 'unknown' + kanvas = ver['kanvas'] if ver is not None and 'kanvas' in ver else 'unknown' + return (core == kanvas) or (core == 'master' and kanvas == 'main') or (core == 'dev' and kanvas == 'dev') or (core == 'HEAD') + + if 'vladmandic/sdnext' not in ver.get('url', ''): + return + t_start = time.time() + if not same(ver): + log.debug(f'Branch mismatch: {ver}') + try: + if 'dev' in ver['branch']: + target = 'dev' + elif 'main' in ver['branch'] or 'master' in ver['branch']: + target = 'main' + else: + target =None + if target: + git('checkout ' + target, folder='extensions-builtin/sdnext-kanvas', ignore=True, optional=True) + ver = get_version(force=True) + log.debug(f'Branch sync: {ver}') + except Exception as e: + log.debug(f'Branch switch: {e}') + ts('kanvas', t_start) + + def check_venv(): def try_relpath(p): try: @@ -1480,6 +1512,7 @@ def check_version(reset=True): # pylint: disable=unused-argument if args.version or args.skip_git: return check_ui(ver) + check_kanvas(ver) commit = git('rev-parse HEAD') global git_commit # pylint: disable=global-statement git_commit = commit[:7] diff --git a/javascript/gallery.js b/javascript/gallery.js index f1b6cb450..297a7ef36 100644 --- a/javascript/gallery.js +++ b/javascript/gallery.js @@ -1270,11 +1270,12 @@ async function initGalleryAutoRefresh() { let galleryTab = isModern ? document.getElementById('gallery_tabitem') : document.getElementById('tab_gallery'); let timeout = 0; while (!galleryTab && timeout++ < 60) { - await new Promise((resolve) => { setTimeout(resolve, 1000); }); + await new Promise((resolve) => { setTimeout(resolve, 2500); }); galleryTab = isModern ? document.getElementById('gallery_tabitem') : document.getElementById('tab_gallery'); } if (!galleryTab) { - throw new Error('Timed out waiting for gallery tab element'); + error('Gallery: timeout'); + return; } const displayNoneRegEx = /display:\s*none/; async function galleryAutoRefresh(mutations) { diff --git a/javascript/loader.js b/javascript/loader.js index 597854fd1..733408bb9 100644 --- a/javascript/loader.js +++ b/javascript/loader.js @@ -1,4 +1,5 @@ const appStartTime = performance.now(); +let monitorLogActive = false; async function preloadImages() { const dark = window.matchMedia && window.matchMedia('(prefers-color-scheme: dark)').matches; @@ -26,6 +27,39 @@ async function preloadImages() { } } +function joinArgs(messages) { + let output = ''; + for (let i = 0; i < messages.length; i++) { + let arg = messages[i]; + if (arg === undefined) arg = 'undefined'; + if (arg === null) arg = 'null'; + output += ' '; + if (typeof arg === 'object') output += JSON.stringify(arg).replace(/["]+/g, ''); + else output += arg; + } + return output; +} + +async function monitorLog() { + if (window.logBufferDirty) { + window.logBufferDirty = false; + const maxLines = 100; // print last n logs from ring buffer to splash-log + const lines = []; + // print last n logs from ring buffer in time order + for (let i = Math.max(0, window.logRingBuffer.length - maxLines); i < window.logRingBuffer.length; i++) { + const logEntry = window.logRingBuffer[i]; + let color = 'white'; + if (logEntry.type === 'error') color = 'palevioletred'; + else if (logEntry.type === 'debug') color = 'gray'; + const html = `
${logEntry.ts}   ${joinArgs(logEntry.msg)}
`; + lines.push(html); + } + const splashLogEl = document.getElementById('splashLog'); + if (splashLogEl) splashLogEl.innerHTML = lines.join(''); + } + if (monitorLogActive) setTimeout(monitorLog, 250); +} + async function removeSplash() { const splash = document.getElementById('splash'); if (splash) splash.remove(); @@ -33,6 +67,7 @@ async function removeSplash() { const t = Math.round(performance.now() - appStartTime); log('startupTime', t); xhrPost(`${window.api}/log`, { message: `ready time=${t}` }); + monitorLogActive = false; } async function createSplash() { @@ -43,6 +78,7 @@ async function createSplash() {
+
`; document.body.insertAdjacentHTML('beforeend', splash); const ok = await preloadImages(); @@ -51,14 +87,23 @@ async function createSplash() { return; } const imgEl = `
`; - document.getElementById('splash').insertAdjacentHTML('afterbegin', imgEl); - authFetch(`${window.api}/motd`) + const splashEl = document.getElementById('splash'); + if (splashEl) splashEl.insertAdjacentHTML('afterbegin', imgEl); + + monitorLogActive = true; + monitorLog(); + + await authFetch(`${window.api}/motd`) .then((res) => res.text()) .then((text) => { + const clean = text.replace(/["]+/g, ''); + log('getMOTD', clean); const motdEl = document.getElementById('motd'); - if (motdEl) motdEl.innerHTML = text.replace(/["]+/g, ''); + if (motdEl) motdEl.innerHTML = clean; }) .catch((err) => error(`getMOTD: ${err}`)); + + log('loadGradioUi'); } window.onload = createSplash; diff --git a/javascript/logger.js b/javascript/logger.js index 739afbb73..52dcf84b1 100644 --- a/javascript/logger.js +++ b/javascript/logger.js @@ -1,3 +1,13 @@ +window.logRingBuffer = []; +window.logBufferDirty = false; + +const logBuffer = (ts, type, msg) => { + const maxLogLength = 8; + window.logRingBuffer.push({ ts, type, msg }); + if (window.logRingBuffer.length > maxLogLength) window.logRingBuffer.shift(); + window.logBufferDirty = true; +}; + const scrollBottom = async (el) => { const lastChild = el.lastElementChild; if (lastChild) lastChild.scrollIntoView({ behavior: 'smooth' }); @@ -11,6 +21,7 @@ const log = async (...msg) => { scrollBottom(window.logger); } console.log(ts, ...msg); + logBuffer(ts, 'log', msg); }; const debug = async (...msg) => { @@ -21,6 +32,7 @@ const debug = async (...msg) => { scrollBottom(window.logger); } console.debug(ts, ...msg); + logBuffer(ts, 'debug', msg); }; const error = async (...msg) => { @@ -31,6 +43,7 @@ const error = async (...msg) => { scrollBottom(window.logger); } console.error(ts, ...msg); + logBuffer(ts, 'error', msg); // const txt = msg.join(' '); // if (!txt.includes('asctime') && !txt.includes('xhr.')) xhrPost('/sdapi/v1/log', { error: txt }); // eslint-disable-line no-use-before-define }; diff --git a/javascript/script.js b/javascript/script.js index b8f474d04..cec278867 100644 --- a/javascript/script.js +++ b/javascript/script.js @@ -1,5 +1,5 @@ async function sleep(ms) { - return new Promise((resolve) => setTimeout(resolve, ms)); // eslint-disable-line no-promise-executor-return + return new Promise((resolve) => { setTimeout(resolve, ms); }); } function gradioApp() { @@ -9,7 +9,7 @@ function gradioApp() { return elem.shadowRoot ? elem.shadowRoot : elem; } -function logFn(func) { +function logFn(func) { // not recommended: use log, debug or error explicitly return async function () { // eslint-disable-line func-names const t0 = performance.now(); const returnValue = func(...arguments); @@ -39,26 +39,50 @@ let uiCurrentTab = null; let uiAfterUpdateTimeout = null; function onAfterUiUpdate(callback) { + if (typeof callback !== 'function') { + error(`onAfterUiUpdate was called without a valid value. Expected a function but got: ${callback}`); + return; + } uiAfterUpdateCallbacks.push(callback); } function onUiUpdate(callback) { + if (typeof callback !== 'function') { + error(`onUiUpdate was called without a valid value. Expected a function but got: ${callback}`); + return; + } uiUpdateCallbacks.push(callback); } function onUiLoaded(callback) { + if (typeof callback !== 'function') { + error(`onUiLoaded was called without a valid value. Expected a function but got: ${callback}`); + return; + } uiLoadedCallbacks.push(callback); } function onUiReady(callback) { + if (typeof callback !== 'function') { + error(`onUiReady was called without a valid value. Expected a function but got: ${callback}`); + return; + } uiReadyCallbacks.push(callback); } function onUiTabChange(callback) { + if (typeof callback !== 'function') { + error(`onUiTabChange was called without a valid value. Expected a function but got: ${callback}`); + return; + } uiTabChangeCallbacks.push(callback); } function onOptionsChanged(callback) { + if (typeof callback !== 'function') { + error(`onOptionsChanged was called without a valid value. Expected a function but got: ${callback}`); + return; + } optionsChangedCallbacks.push(callback); } @@ -67,7 +91,10 @@ function executeCallbacks(queue, arg) { for (const callback of queue) { if (!callback) continue; try { + const t0 = performance.now(); callback(arg); + const t1 = performance.now(); + if (t1 - t0 > 250) log('callbackSlow', callback.name || callback, `time=${Math.round(t1 - t0)}`); } catch (e) { error(`executeCallbacks: ${callback} ${e}`); } @@ -83,17 +110,21 @@ function scheduleAfterUiUpdateCallbacks() { let executedOnLoaded = false; const ignoreElements = ['logMonitorData', 'logWarnings', 'logErrors', 'tooltip-container', 'logger']; +const ignoreElementsSet = new Set(ignoreElements); const ignoreClasses = ['wrap']; let mutationTimer = null; let validMutations = []; + async function mutationCallback(mutations) { - let newMutations = mutations; - if (newMutations.length > 0) newMutations = newMutations.filter((m) => m.target.nodeName !== 'LABEL'); - if (newMutations.length > 0) newMutations = newMutations.filter((m) => ignoreElements.indexOf(m.target.id) === -1); - if (newMutations.length > 0) newMutations = newMutations.filter((m) => m.target.id !== 'logWarnings' && m.target.id !== 'logErrors'); - if (newMutations.length > 0) newMutations = newMutations.filter((m) => !m.target.classList?.contains('wrap')); - if (newMutations.length > 0) validMutations = validMutations.concat(newMutations); + if (mutations.length <= 0) return; + for (const mutation of mutations) { + const target = mutation.target; + if (target.nodeName === 'LABEL') continue; + if (ignoreElementsSet.has(target.id)) continue; + if (target.classList?.contains(ignoreClasses[0])) continue; + validMutations.push(mutation); + } if (validMutations.length < 1) return; if (mutationTimer) clearTimeout(mutationTimer); @@ -113,12 +144,13 @@ async function mutationCallback(mutations) { } validMutations = []; mutationTimer = null; - }, 50); + }, 100); } document.addEventListener('DOMContentLoaded', () => { + log('DOMContentLoaded'); const mutationObserver = new MutationObserver(mutationCallback); - mutationObserver.observe(gradioApp(), { childList: true, subtree: true }); + mutationObserver.observe(gradioApp(), { childList: true, subtree: true, attributes: false }); }); /** diff --git a/javascript/settings.js b/javascript/settings.js index 9aa22c626..798e4ac82 100644 --- a/javascript/settings.js +++ b/javascript/settings.js @@ -15,7 +15,7 @@ function monitorOption(option, callback) { monitoredOpts.push({ [option]: callback }); } -const AppyOpts = [ +const AppyOpts = [ // monitored opts { compact_view: (val, old) => toggleCompact(val, old) }, { gradio_theme: (val, old) => setTheme(val, old) }, { font_size: (val, old) => setFontSize(val, old) }, @@ -38,7 +38,12 @@ async function updateOpts(json_string) { for (const op of AppyOpts) { const [key, callback] = Object.entries(op)[0]; - if (callback) callback(new_opts[key], opts[key]); + if (callback) { + const t3 = performance.now(); + callback(new_opts[key], opts[key]); + const t4 = performance.now(); + if (t4 - t3 > 100) debug('AppyOptSlow', key, `time=${Math.round(t4 - t3)}`); + } } const t2 = performance.now(); @@ -109,7 +114,7 @@ function updateAllOpts() { return true; } -onAfterUiUpdate(async () => { +async function onAfterUiUpdateCallback() { if (!updateAllOpts()) return; const json_elem = gradioApp().getElementById('settings_json'); const textarea = json_elem.querySelector('textarea'); @@ -146,15 +151,19 @@ onAfterUiUpdate(async () => { }); }, 250); }; -}); +} -onOptionsChanged(() => { +onAfterUiUpdate(onAfterUiUpdateCallback); + +async function onOptionsChangedCallback() { const setting_elems = gradioApp().querySelectorAll('#settings [id^="setting_"]'); setting_elems.forEach((elem) => { const setting_name = elem.id.replace('setting_', ''); markIfModified(setting_name, opts[setting_name]); }); -}); +} + +onOptionsChanged(onOptionsChangedCallback); async function initModels() { const warn = () => ` diff --git a/javascript/startup.js b/javascript/startup.js index a51a0272d..7168b0a9d 100644 --- a/javascript/startup.js +++ b/javascript/startup.js @@ -18,15 +18,15 @@ async function waitForOpts() { break; } } - await sleep(50); + await sleep(100); t1 = performance.now(); } } async function initStartup() { const t0 = performance.now(); - log('gradio', `time=${Math.round(t0 - appStartTime)}`); - log('initStartup'); + log('initGradio', `time=${Math.round(t0 - appStartTime)}`); + log('initUi'); if (window.setupLogger) await setupLogger(); // all items here are non-blocking async calls @@ -54,22 +54,23 @@ async function initStartup() { } setRefreshInterval(); executeCallbacks(uiReadyCallbacks); - initLogMonitor(); setupExtraNetworks(); // optinally wait for modern ui if (window.waitForUiReady) await waitForUiReady(); initAutocomplete(); monitorConnection(); - removeSplash(); // post startup tasks that may take longer but are not critical showNetworks(); setHints(); applyStyles(); initIndexDB(); + initLogMonitor(); t1 = performance.now(); log('initStartup', Math.round(1000 * (t1 - t0) / 1000000)); + + removeSplash(); } onUiLoaded(initStartup); diff --git a/javascript/ui.js b/javascript/ui.js index 913cb0fc6..9a939d282 100644 --- a/javascript/ui.js +++ b/javascript/ui.js @@ -4,6 +4,10 @@ window.titles = {}; let tabSelected = ''; let txt2img_textarea; let img2img_textarea; +let fontSizeApplyRaf = 0; +let pendingFontSize = null; +let appliedFontSize = null; +let cachedGradioRoot = null; const wait_time = 800; const token_timeouts = {}; let uiLoaded = false; @@ -132,18 +136,34 @@ async function setTheme(val, old) { } function setFontSize(val, old) { - const size = val || opts.font_size; - if (size === old) return; - document.documentElement.style.setProperty('--font-size', `${size}px`); - gradioApp().style.setProperty('--font-size', `${size}px`); - gradioApp().style.setProperty('--text-xxs', `${size - 3}px`); - gradioApp().style.setProperty('--text-xs', `${size - 2}px`); - gradioApp().style.setProperty('--text-sm', `${size - 1}px`); - gradioApp().style.setProperty('--text-md', `${size}px`); - gradioApp().style.setProperty('--text-lg', `${size + 1}px`); - gradioApp().style.setProperty('--text-xl', `${size + 2}px`); - gradioApp().style.setProperty('--text-xxl', `${size + 3}px`); - log('setFontSize', size); + const size = Number(val || opts.font_size); + if (!Number.isFinite(size)) return; + if (size === old || size === appliedFontSize || size === pendingFontSize) return; + pendingFontSize = size; + if (fontSizeApplyRaf) return; + + fontSizeApplyRaf = requestAnimationFrame(() => { + const t0 = performance.now(); + fontSizeApplyRaf = 0; + const nextSize = pendingFontSize; + pendingFontSize = null; + if (!Number.isFinite(nextSize) || nextSize === appliedFontSize) return; + + cachedGradioRoot = cachedGradioRoot || gradioApp(); + const rootStyle = cachedGradioRoot.style; + document.documentElement.style.setProperty('--font-size', `${nextSize}px`); + rootStyle.setProperty('--font-size', `${nextSize}px`); + rootStyle.setProperty('--text-xxs', `${nextSize - 3}px`); + rootStyle.setProperty('--text-xs', `${nextSize - 2}px`); + rootStyle.setProperty('--text-sm', `${nextSize - 1}px`); + rootStyle.setProperty('--text-md', `${nextSize}px`); + rootStyle.setProperty('--text-lg', `${nextSize + 1}px`); + rootStyle.setProperty('--text-xl', `${nextSize + 2}px`); + rootStyle.setProperty('--text-xxl', `${nextSize + 3}px`); + appliedFontSize = nextSize; + const t1 = performance.now(); + log('setFontSize', nextSize, `time=${Math.round(t1 - t0)}`); + }); } function switchToTab(tab) { @@ -350,6 +370,28 @@ function clearPrompts(prompt, negative_prompt) { } const promptTokenCountUpdateFuncs = {}; +const registeredPromptTextareas = new WeakSet(); +const registeredPromptIds = new Set(); +const pendingCounterPlacement = new Set(); +const promptRegistrationConfig = [ + ['txt2img_prompt', 'txt2img_token_counter', 'txt2img_token_button'], + ['txt2img_neg_prompt', 'txt2img_negative_token_counter', 'txt2img_negative_token_button'], + ['img2img_prompt', 'img2img_token_counter', 'img2img_token_button'], + ['img2img_neg_prompt', 'img2img_negative_token_counter', 'img2img_negative_token_button'], + ['control_prompt', 'control_token_counter', 'control_token_button'], + ['control_neg_prompt', 'control_negative_token_counter', 'control_negative_token_button'], +]; +let promptRegistrationRaf = 0; +let promptRegistrationCursor = 0; +let promptRegistrationInProgress = false; + +function scheduleIdleUI(task) { + if (typeof window.requestIdleCallback === 'function') { + window.requestIdleCallback(task, { timeout: 500 }); + } else { + setTimeout(task, 0); + } +} function recalculatePromptTokens(name) { if (promptTokenCountUpdateFuncs[name]) { @@ -427,30 +469,89 @@ function sortUIElements() { log('sortUIElements'); } -onAfterUiUpdate(async () => { - async function registerTextarea(id, id_counter, id_button) { - const prompt = gradioApp().getElementById(id); - if (!prompt) return; - const counter = gradioApp().getElementById(id_counter); - const localTextarea = gradioApp().querySelector(`#${id} > label > textarea`); - if (counter.parentElement === prompt.parentElement) return; - prompt.parentElement.insertBefore(counter, prompt); - prompt.parentElement.style.position = 'relative'; - promptTokenCountUpdateFuncs[id] = () => { update_token_counter(id_button); }; - localTextarea.addEventListener('input', promptTokenCountUpdateFuncs[id]); - } - +function registerTextareaCallback() { // sortUIElements(); if (promptsInitialized) return; - log('initPrompts'); - registerTextarea('txt2img_prompt', 'txt2img_token_counter', 'txt2img_token_button'); - registerTextarea('txt2img_neg_prompt', 'txt2img_negative_token_counter', 'txt2img_negative_token_button'); - registerTextarea('img2img_prompt', 'img2img_token_counter', 'img2img_token_button'); - registerTextarea('img2img_neg_prompt', 'img2img_negative_token_counter', 'img2img_negative_token_button'); - registerTextarea('control_prompt', 'control_token_counter', 'control_token_button'); - registerTextarea('control_neg_prompt', 'control_negative_token_counter', 'control_negative_token_button'); - promptsInitialized = true; -}); + if (promptRegistrationInProgress) return; + + const app = gradioApp(); + if (!app) return; + + const registerTextarea = (id, id_counter, id_button) => { + const prompt = app.getElementById(id); + const counter = app.getElementById(id_counter); + const localTextarea = prompt?.querySelector('label > textarea'); + if (!prompt || !counter || !localTextarea || !prompt.parentElement) return false; + + const promptParent = prompt.parentElement; + const needsCounterPlacement = counter.parentElement !== promptParent || counter.nextElementSibling !== prompt; + if (needsCounterPlacement && !pendingCounterPlacement.has(id)) { + pendingCounterPlacement.add(id); + scheduleIdleUI(() => { + pendingCounterPlacement.delete(id); + const currentPrompt = app.getElementById(id); + const currentCounter = app.getElementById(id_counter); + if (!currentPrompt || !currentCounter || !currentPrompt.parentElement) return; + const currentParent = currentPrompt.parentElement; + if (currentCounter.parentElement !== currentParent || currentCounter.nextElementSibling !== currentPrompt) { + currentParent.insertBefore(currentCounter, currentPrompt); + } + if (currentParent.style.position !== 'relative') { + currentParent.style.position = 'relative'; + } + }); + } + + if (!promptTokenCountUpdateFuncs[id]) promptTokenCountUpdateFuncs[id] = () => { update_token_counter(id_button); }; + if (!registeredPromptTextareas.has(localTextarea)) { + localTextarea.addEventListener('input', promptTokenCountUpdateFuncs[id]); + registeredPromptTextareas.add(localTextarea); + } + return true; + }; + + const runPromptRegistrationStep = () => { + promptRegistrationRaf = 0; + const total = promptRegistrationConfig.length; + let cfg = null; + + // Process one prompt registration per frame to avoid long blocking work. + for (let attempts = 0; attempts < total; attempts += 1) { + const nextCfg = promptRegistrationConfig[promptRegistrationCursor]; + promptRegistrationCursor = (promptRegistrationCursor + 1) % total; + const [id] = nextCfg; + if (!registeredPromptIds.has(id)) { + cfg = nextCfg; + break; + } + } + + if (cfg) { + const [id] = cfg; + if (registerTextarea(...cfg)) { + registeredPromptIds.add(id); + } else { + // Prompt not available yet, retry on next onAfterUiUpdate callback. + promptRegistrationInProgress = false; + return; + } + } + + promptsInitialized = registeredPromptIds.size === total; + if (promptsInitialized) { + promptRegistrationInProgress = false; + log('initPrompts', registeredPromptIds.size); + return; + } + + promptRegistrationRaf = requestAnimationFrame(runPromptRegistrationStep); + }; + + promptRegistrationInProgress = true; + promptRegistrationRaf = requestAnimationFrame(runPromptRegistrationStep); +} + +onAfterUiUpdate(registerTextareaCallback); function update_txt2img_tokens(...args) { update_token_counter('txt2img_token_button'); @@ -564,7 +665,7 @@ function createThemeElement() { return el; } -function toggleCompact(val, old) { +async function toggleCompact(val, old) { if (val === old) return; log('toggleCompact', val, old); if (val) { diff --git a/launch.py b/launch.py index e1c191f31..20b982331 100755 --- a/launch.py +++ b/launch.py @@ -182,11 +182,11 @@ def clean_server(): pass collected = gc.collect() # python gc modules_cleaned = sorted(sys.modules.keys()) - modules_keys = [m.split('.')[0] for m in modules_cleaned if not m.startswith('_')] - modules_sorted = {} - for module_key in modules_keys: - modules_sorted[module_key] = len([m for m in modules_cleaned if m.startswith(module_key)]) - log.trace(f'Server modules: {modules_sorted}') + # modules_keys = [m.split('.')[0] for m in modules_cleaned if not m.startswith('_')] + # modules_sorted = {} + # for module_key in modules_keys: + # modules_sorted[module_key] = len([m for m in modules_cleaned if m.startswith(module_key)]) + # log.trace(f'Server modules: {modules_sorted}') t1 = time.time() log.trace(f'Server modules: total={len(modules_loaded)} unloaded={len(removed_removed)} remaining={len(modules_cleaned)} gc={collected} time={t1-t0:.2f}') diff --git a/models/Reference/OzzyGT--ERNIE_Image_Turbo_sdnq_dynamic_int4.jpg b/models/Reference/OzzyGT--ERNIE_Image_Turbo_sdnq_dynamic_int4.jpg new file mode 100644 index 000000000..42b1dcff4 Binary files /dev/null and b/models/Reference/OzzyGT--ERNIE_Image_Turbo_sdnq_dynamic_int4.jpg differ diff --git a/models/Reference/OzzyGT--ERNIE_Image_sdnq_dynamic_int4.jpg b/models/Reference/OzzyGT--ERNIE_Image_sdnq_dynamic_int4.jpg new file mode 100644 index 000000000..a4d8a8ce5 Binary files /dev/null and b/models/Reference/OzzyGT--ERNIE_Image_sdnq_dynamic_int4.jpg differ diff --git a/models/Reference/baidu--ERNIE-Image-Turbo.jpg b/models/Reference/baidu--ERNIE-Image-Turbo.jpg index 73c5f7c09..e3ada61c9 100644 Binary files a/models/Reference/baidu--ERNIE-Image-Turbo.jpg and b/models/Reference/baidu--ERNIE-Image-Turbo.jpg differ diff --git a/models/Reference/baidu--ERNIE-Image.jpg b/models/Reference/baidu--ERNIE-Image.jpg index 49cb777b3..992e23c18 100644 Binary files a/models/Reference/baidu--ERNIE-Image.jpg and b/models/Reference/baidu--ERNIE-Image.jpg differ diff --git a/models/Reference/briaai--FIBO.jpg b/models/Reference/briaai--FIBO.jpg index b05e7e260..b93f2a163 100644 Binary files a/models/Reference/briaai--FIBO.jpg and b/models/Reference/briaai--FIBO.jpg differ diff --git a/models/Reference/briaai--Fibo-Edit.jpg b/models/Reference/briaai--Fibo-Edit.jpg index 9b5ef97e3..e21db5050 100644 Binary files a/models/Reference/briaai--Fibo-Edit.jpg and b/models/Reference/briaai--Fibo-Edit.jpg differ diff --git a/models/Reference/briaai--Fibo-lite.jpg b/models/Reference/briaai--Fibo-lite.jpg index 41cf0c350..3bf59057c 100644 Binary files a/models/Reference/briaai--Fibo-lite.jpg and b/models/Reference/briaai--Fibo-lite.jpg differ diff --git a/models/Reference/lodestones--Zeta-Chroma.jpg b/models/Reference/lodestones--Zeta-Chroma.jpg index e69de29bb..6111f7a68 100644 Binary files a/models/Reference/lodestones--Zeta-Chroma.jpg and b/models/Reference/lodestones--Zeta-Chroma.jpg differ diff --git a/modules/api/validate.py b/modules/api/validate.py index 3bcc1f393..58952332c 100644 --- a/modules/api/validate.py +++ b/modules/api/validate.py @@ -21,6 +21,7 @@ log_cost = { "/sdapi/v1/browser/thumb": -1, "/sdapi/v1/network/thumb": -1, "/run/predict": -1, + "/queue/join": -1, "/internal/progress": -1, "/sdapi/v1/version": -1, "/sdapi/v1/log": -1, diff --git a/modules/images.py b/modules/images.py index e83eb2871..d294e02b7 100644 --- a/modules/images.py +++ b/modules/images.py @@ -1,7 +1,7 @@ from modules.image.metadata import image_data, read_info_from_image from modules.image.save import save_image, sanitize_filename_part from modules.image.resize import resize_image -from modules.image.namegen import FilenameGenerator +from modules.image.namegen import FilenameGenerator, get_next_sequence_number from modules.image.grid import Grid, image_grid, check_grid_size, get_grid_size, draw_grid_annotations, draw_prompt_matrix, combine_grid, get_font __all__ = [ @@ -19,4 +19,5 @@ __all__ = [ 'sanitize_filename_part', 'save_image', 'get_font', + 'get_next_sequence_number', ] diff --git a/modules/json_helpers.py b/modules/json_helpers.py index 253a6c3d2..e28f3e8bd 100644 --- a/modules/json_helpers.py +++ b/modules/json_helpers.py @@ -7,8 +7,7 @@ import fasteners import orjson from modules.logger import log - -locking_available = True # used by file read/write locking +locking_available = True # used by file read/write locking @overload @@ -18,39 +17,42 @@ def readfile(filename: str, silent: bool = False, lock: bool = False, *, as_type @overload def readfile(filename: str, silent: bool = False, lock: bool = False) -> dict | list: ... def readfile(filename: str, silent: bool = False, lock: bool = False, *, as_type="") -> dict | list: - global locking_available # pylint: disable=global-statement + global locking_available # pylint: disable=global-statement data = {} if as_type == "dict" else [] lock_file = None locked = False + if lock and locking_available: try: lock_file = fasteners.InterProcessReaderWriterLock(f"{filename}.lock") - lock_file.logger.disabled = True # type: ignore - False positive. Bad typing in Fasteners. + lock_file.logger.disabled = True # type: ignore - False positive. Bad typing in Fasteners. locked = lock_file.acquire_read_lock(blocking=True, timeout=3) except Exception as err: lock_file = None locking_available = False log.error(f'File read lock: file="{filename}" {err}') locked = False + try: # if not os.path.exists(filename): # return {} t0 = time.time() with open(filename, "rb") as file: b = file.read() - data = orjson.loads(b) # pylint: disable=no-member + data = orjson.loads(b) # pylint: disable=no-member # if type(data) is str: # data = json.loads(data) t1 = time.time() if not silent: - fn = f'{sys._getframe(2).f_code.co_name}:{sys._getframe(1).f_code.co_name}' # pylint: disable=protected-access - log.debug(f'Read: file="{filename}" json={len(data)} bytes={os.path.getsize(filename)} time={t1-t0:.3f} fn={fn}') + fn = f"{sys._getframe(2).f_code.co_name}:{sys._getframe(1).f_code.co_name}" # pylint: disable=protected-access + log.debug(f'Read: file="{filename}" json={len(data)} bytes={os.path.getsize(filename)} time={t1 - t0:.3f} fn={fn}') except FileNotFoundError as err: if not silent: log.debug(f'Read failed: file="{filename}" {err}') except Exception as err: if not silent: log.error(f'Read failed: file="{filename}" {err}') + try: if locking_available and lock_file is not None: lock_file.release_read_lock() @@ -58,6 +60,7 @@ def readfile(filename: str, silent: bool = False, lock: bool = False, *, as_type os.remove(f"{filename}.lock") except Exception: locking_available = False + if isinstance(data, list) and as_type == "dict": if not data: return {} @@ -74,9 +77,10 @@ def readfile(filename: str, silent: bool = False, lock: bool = False, *, as_type return data -def writefile(obj, filename, mode='w', silent=False, atomic=False): +def writefile(obj: dict | list, filename, mode="w", silent=False, atomic=False): import tempfile - global locking_available # pylint: disable=global-statement + + global locking_available # pylint: disable=global-statement lock_file = None locked = False @@ -86,33 +90,23 @@ def writefile(obj, filename, mode='w', silent=False, atomic=False): try: t0 = time.time() - data = obj.copy() - # skipkeys=True, ensure_ascii=True, check_circular=True, allow_nan=True - if type(data) == dict: - output = json.dumps(data, indent=2, default=default) - elif type(data) == list: - output = json.dumps(data, indent=2, default=default) - elif isinstance(data, object): - simple = {} - for k in data.__dict__: - if data.__dict__[k] is not None: - simple[k] = data.__dict__[k] - output = json.dumps(simple, indent=2, default=default) - else: - raise ValueError('not a valid object') + data = obj.copy() # Ensure keys/items aren't added/deleted during json.dumps + output = json.dumps(data, indent=2, default=default) except Exception as err: log.error(f'Save failed: file="{filename}" {err}') return + try: if locking_available: lock_file = fasteners.InterProcessReaderWriterLock(f"{filename}.lock") if locking_available else None - lock_file.logger.disabled = True # type: ignore - False positive. Bad typing in Fasteners. + lock_file.logger.disabled = True # type: ignore - False positive. Bad typing in Fasteners. locked = lock_file.acquire_write_lock(blocking=True, timeout=3) if lock_file is not None else False except Exception as err: locking_available = False lock_file = None log.error(f'File write lock: file="{filename}" {err}') locked = False + try: if atomic: with tempfile.NamedTemporaryFile(mode=mode, encoding="utf8", delete=False, dir=os.path.dirname(filename)) as f: @@ -125,10 +119,11 @@ def writefile(obj, filename, mode='w', silent=False, atomic=False): file.write(output) t1 = time.time() if not silent: - datalength = len(data) if isinstance(data, (dict, list)) else (len(data.__dict__)) - log.debug(f'Save: file="{filename}" json={datalength} bytes={len(output)} time={t1-t0:.3f}') + datalength = len(data) + log.debug(f'Save: file="{filename}" json={datalength} bytes={len(output)} time={t1 - t0:.3f}') except Exception as err: log.error(f'Save failed: file="{filename}" {err}') + try: if locking_available and lock_file is not None: lock_file.release_write_lock() diff --git a/modules/masking.py b/modules/masking.py index 9cd70c5a5..9b5b193fa 100644 --- a/modules/masking.py +++ b/modules/masking.py @@ -4,6 +4,7 @@ import sys import time import gradio as gr import numpy as np +import torch import cv2 from PIL import Image, ImageFilter, ImageOps from transformers import SamModel, SamImageProcessor, MaskGenerationPipeline @@ -236,6 +237,8 @@ def run_segment(input_image: gr.Image, input_mask: np.ndarray): input_mask_size = np.count_nonzero(input_mask) debug(f'Segment SAM: {vars(opts)}') for mask, score in zip(outputs['masks'], outputs['scores'], strict=False): + if isinstance(mask, torch.Tensor): + mask = mask.cpu().numpy() mask = mask.astype('uint8') mask_size = np.count_nonzero(mask) if mask_size == 0: @@ -259,6 +262,9 @@ def run_segment(input_image: gr.Image, input_mask: np.ndarray): def run_rembg(input_image: Image.Image, input_mask: np.ndarray): try: + from installer import install + for pkg in ["dctorch==0.1.2", "pymatting", "pooch", "rembg"]: + install(pkg, no_deps=True, ignore=False) import rembg except Exception as e: log.error(f'Mask Rembg load failed: {e}') diff --git a/modules/models_hf.py b/modules/models_hf.py index 7f3d3f039..c3e643b06 100644 --- a/modules/models_hf.py +++ b/modules/models_hf.py @@ -18,6 +18,7 @@ def hf_init(): os.environ.setdefault('HF_HUB_ETAG_TIMEOUT', '10') os.environ.setdefault('HF_ENABLE_PARALLEL_LOADING', 'true' if opts.sd_parallel_load else 'false') os.environ.setdefault('HF_HUB_CACHE', opts.hfcache_dir) + os.environ.setdefault('HF_XET_CACHE', opts.xetcache_dir) if opts.hf_transfer_mode == 'requests': os.environ.setdefault('HF_XET_HIGH_PERFORMANCE', 'false') os.environ.setdefault('HF_HUB_ENABLE_HF_TRANSFER', 'false') @@ -42,14 +43,21 @@ def hf_init(): def hf_check_cache(): - prev_default = os.environ.get("SD_HFCACHEDIR", None) or os.path.join(os.path.expanduser('~'), '.cache', 'huggingface', 'hub') from modules.modelstats import stat + prev_default = os.environ.get("SD_HFCACHEDIR", None) or os.path.join(os.path.expanduser('~'), '.cache', 'huggingface', 'hub') if opts.hfcache_dir != prev_default: size, _mtime = stat(prev_default) - if size//1024//1024 > 16: - log.warning(f'Cache location changed: previous="{prev_default}" size={size//1024//1024} MB') - size, _mtime = stat(opts.hfcache_dir) - log.debug(f'Huggingface: cache="{opts.hfcache_dir}" size={size//1024//1024} MB') + if size//1024//1024 > 32: + log.warning(f'Huggingface cache changed: type=huggingface unused="{prev_default}" size={size//1024//1024} MB') + prev_default = os.path.join(os.path.expanduser('~'), '.cache', 'huggingface', 'xet') + if opts.xetcache_dir != prev_default: + size, _mtime = stat(prev_default) + if size//1024//1024 > 32: + log.warning(f'Huggingface cache changed: type=xet unused="{prev_default}" size={size//1024//1024} MB') + + hf_size, _mtime = stat(opts.hfcache_dir) + xet_size, _mtime = stat(opts.xetcache_dir) + log.debug(f'Huggingface: cache="{opts.hfcache_dir}" size={hf_size//1024//1024} MB xet="{opts.xetcache_dir}" size={xet_size//1024//1024} MB') def hf_search(keyword): diff --git a/modules/paths.py b/modules/paths.py index 06851a6f5..0c1408f86 100644 --- a/modules/paths.py +++ b/modules/paths.py @@ -107,6 +107,7 @@ def create_paths(opts): create_path(fix_path('ckpt_dir')) create_path(fix_path('diffusers_dir')) create_path(fix_path('hfcache_dir')) + create_path(fix_path('xetcache_dir')) create_path(fix_path('vae_dir')) create_path(fix_path('unet_dir')) create_path(fix_path('te_dir')) diff --git a/modules/sdnq/triton_mm.py b/modules/sdnq/triton_mm.py index beda09f03..955761e88 100644 --- a/modules/sdnq/triton_mm.py +++ b/modules/sdnq/triton_mm.py @@ -12,16 +12,31 @@ import torch import triton import triton.language as tl +try: + from .common import is_rdna2_and_older +except Exception: + is_rdna2_and_older = False -matmul_configs = [ - triton.Config({'BLOCK_SIZE_M': BM, 'BLOCK_SIZE_N': BN, "BLOCK_SIZE_K": BK, "GROUP_SIZE_M": GM}, num_warps=w, num_stages=s) - for BM in [32, 64, 128, 256] - for BN in [32, 64, 128, 256] - for BK in [32, 64, 128] - for GM in [4, 8] - for w in [4, 8] - for s in [2] -] +if is_rdna2_and_older: + matmul_configs = [ + triton.Config({'BLOCK_SIZE_M': BM, 'BLOCK_SIZE_N': BN, "BLOCK_SIZE_K": BK, "GROUP_SIZE_M": GM}, num_warps=w, num_stages=s) + for BM in [64, 128] + for BN in [64, 128] + for BK in [64] + for GM in [2, 4] + for w in [2, 4] + for s in [2] + ] +else: + matmul_configs = [ + triton.Config({'BLOCK_SIZE_M': BM, 'BLOCK_SIZE_N': BN, "BLOCK_SIZE_K": BK, "GROUP_SIZE_M": GM}, num_warps=w, num_stages=s) + for BM in [32, 64, 128, 256] + for BN in [32, 64, 128, 256] + for BK in [32, 64, 128] + for GM in [4, 8] + for w in [4, 8] + for s in [2] + ] @triton.autotune(configs=matmul_configs, key=["M", "N", "K", "stride_bk", "ACCUMULATOR_DTYPE"], cache_results=True) diff --git a/modules/ui_definitions.py b/modules/ui_definitions.py index e5bbb1a5b..e01eb72be 100644 --- a/modules/ui_definitions.py +++ b/modules/ui_definitions.py @@ -64,6 +64,7 @@ def create_settings(cmd_opts): default_hfcache_dir = os.environ.get("SD_HFCACHEDIR", None) or os.path.join(paths.models_path, 'huggingface') default_checkpoint = list_checkpoint_titles()[0] if len(list_checkpoint_titles()) > 0 else "model.safetensors" + default_xetcache_dir = os.environ.get("HF_XET_CACHE ", None) or os.path.join(paths.models_path, 'xet') hide_dirs = {"visible": not cmd_opts.hide_ui_dir_config} @@ -384,6 +385,7 @@ def create_settings(cmd_opts): "ckpt_dir": OptionInfo(os.path.join(paths.models_path, 'Stable-diffusion'), "Folder with stable diffusion models", folder=True), "diffusers_dir": OptionInfo(os.path.join(paths.models_path, 'Diffusers'), "Folder with Huggingface models", folder=True), "hfcache_dir": OptionInfo(default_hfcache_dir, "Folder for Huggingface cache", folder=True), + "xetcache_dir": OptionInfo(default_xetcache_dir, "Folder for XET cache", folder=True), "tunable_dir": OptionInfo(os.path.join(paths.models_path, 'tunable'), "Folder for Tunable ops cache", folder=True), "vae_dir": OptionInfo(os.path.join(paths.models_path, 'VAE'), "Folder with VAE files", folder=True), "unet_dir": OptionInfo(os.path.join(paths.models_path, 'UNET'), "Folder with UNET files", folder=True), @@ -524,6 +526,23 @@ def create_settings(cmd_opts): "compact_view": OptionInfo(False, "Compact view"), "ui_columns": OptionInfo(4, "Gallery view columns", gr.Slider, {"minimum": 1, "maximum": 8, "step": 1}), + 'uiux_separator_appearance': OptionInfo("

Appearance

", "", gr.HTML), + "uiux_grid_image_size": OptionInfo(150, "Grid image size", gr.Slider, {"minimum": 64, "maximum": 1024, "step": 1}), + "uiux_panel_min_width": OptionInfo(35, "Panel minimum width", gr.Number), + "uiux_hide_legacy": OptionInfo(True, "Hide legacy tabs"), + "uiux_persist_layout": OptionInfo(True, "Persist UI layout"), + "uiux_no_slider_layout": OptionInfo(False, "Hide input range sliders"), + "uiux_show_labels_aside": OptionInfo(False, "Show labels for aside tabs"), + "uiux_show_labels_main": OptionInfo(False, "Show labels for main tabs"), + "uiux_show_labels_tabs": OptionInfo(True, "Show labels for page tabs"), + "uiux_show_input_range_ticks": OptionInfo(True, "Show ticks for input range slider", gr.Checkbox, {"visible": False}), + "uiux_no_headers_params": OptionInfo(False, "Hide params headers", gr.Checkbox, {"visible": False}), + "uiux_show_outline_params": OptionInfo(True, "Show parameter outline", gr.Checkbox, {"visible": False}), + + 'uiux_separator_mobile': OptionInfo("

Mobile

", "", gr.HTML), + "uiux_default_layout": OptionInfo("Auto", "Layout", gr.Radio, {"choices": ["Auto","Desktop", "Mobile"]}), + "uiux_mobile_scale": OptionInfo(0.7, "Mobile scale", gr.Slider, {"minimum": 0.5, "maximum": 1, "step": 0.05}), + "images_sep_log": OptionInfo("

Log Display

", "", gr.HTML), "logmonitor_show": OptionInfo(True, "Show log view"), "logmonitor_refresh_period": OptionInfo(5000, "Log view update period", gr.Slider, {"minimum": 0, "maximum": 30000, "step": 25}), diff --git a/modules/ui_extensions.py b/modules/ui_extensions.py index a77b1a296..2fe1386ba 100644 --- a/modules/ui_extensions.py +++ b/modules/ui_extensions.py @@ -395,7 +395,7 @@ def create_html(search_text, sort_column): ext['status'] = 0 style = "style='cursor: help;width: 1rem;margin: 0.2em;'" if ext['url'] is None or ext['url'] == '': - status = f"
{ui_symbols.svg_bullet.style('#00C0FD')}
" + status = f"
{ui_symbols.svg_bullet.style('#00C0FD')}
" elif ext['status'] > 0: if ext['status'] == 1: status = f"
{ui_symbols.svg_bullet.style('#00FD9C')}
" diff --git a/modules/ui_extra_networks.py b/modules/ui_extra_networks.py index 17a1c5b7e..e599a99cd 100644 --- a/modules/ui_extra_networks.py +++ b/modules/ui_extra_networks.py @@ -8,7 +8,6 @@ import html import base64 import urllib.parse import threading -from typing import TYPE_CHECKING from types import SimpleNamespace from pathlib import Path from html.parser import HTMLParser @@ -134,7 +133,7 @@ class DateTimeEncoder(json.JSONEncoder): class ExtraNetworksPage: - def __init__(self, title): + def __init__(self, title: str): self.title = title self.name = title.lower() self.allow_negative_prompt = False @@ -198,7 +197,7 @@ class ExtraNetworksPage: errors.display(e, 'Network version') return all_versions[0] - def link_preview(self, filename): + def link_preview(self, filename: str): quoted_filename = urllib.parse.quote(filename.replace('\\', '/')) mtime = os.path.getmtime(filename) if os.path.exists(filename) else 0 preview = f"{shared.opts.subpath}/sdapi/v1/network/thumb?filename={quoted_filename}&mtime={mtime}" @@ -256,7 +255,7 @@ class ExtraNetworksPage: log.info(f'Network thumbnails: type={self.name} created={created}') self.missing_thumbs.clear() - def create_items(self, tabname): + def create_items(self, tabname: str): if self.refresh_time is not None and self.refresh_time > refresh_time: # cached results return t0 = time.time() @@ -276,7 +275,7 @@ class ExtraNetworksPage: debug(f'EN create-items: page={self.name} items={len(self.items)} time={t1-t0:.2f}') self.list_time += t1-t0 - def create_page(self, tabname, skip = False): + def create_page(self, tabname: str, skip = False): debug(f'EN create-page: {self.name}') if self.page_time > refresh_time and len(self.html) > 0: # cached page return self.patch(self.html, tabname) @@ -388,7 +387,7 @@ class ExtraNetworksPage: def allowed_directories_for_previews(self): return [] - def create_html(self, item, tabname): + def create_html(self, item, tabname: str): def random_bright_color(): r = random.randint(100, 255) g = random.randint(100, 255) @@ -429,7 +428,7 @@ class ExtraNetworksPage: errors.display(e, 'Networks') return "" - def find_preview_file(self, path): + def find_preview_file(self, path: str | None): if path is None: return 'html/missing.png' if os.path.join('models', 'Reference') in path: @@ -450,7 +449,7 @@ class ExtraNetworksPage: return file return 'html/missing.png' - def find_preview(self, filename): + def find_preview(self, filename: str): t0 = time.time() preview_file = self.find_preview_file(filename) self.preview_time += time.time() - t0 @@ -503,7 +502,7 @@ class ExtraNetworksPage: debug(f'EN missing-preview: {item["name"]}') self.preview_time += time.time() - t0 - def find_description(self, path, info=None): + def find_description(self, path: str | None, info=None): t0 = time.time() class HTMLFilter(HTMLParser): text = "" @@ -535,7 +534,7 @@ class ExtraNetworksPage: self.desc_time += t1-t0 return f.text - def find_info(self, path): + def find_info(self, path: str | None): data = {} if shared.cmd_opts.no_metadata: return data @@ -594,7 +593,7 @@ def register_pages(): register_page(ExtraNetworksPageTextualInversion()) -def get_pages(title=None): +def get_pages(title: str | None = None): visible = shared.opts.extra_networks pages: list[ExtraNetworksPage] = [] if 'All' in visible or visible == []: # default en sort order @@ -646,7 +645,7 @@ class ExtraNetworksUi: self.state: gr.State = None -def create_ui(container, button_parent, tabname, skip_indexing = False): +def create_ui(container, button_parent: gr.Button, tabname: str, skip_indexing = False): if 'networks' in shared.opts.ui_disabled: return None debug(f'EN create-ui: {tabname}') @@ -881,25 +880,19 @@ def create_ui(container, button_parent, tabname, skip_indexing = False): from modules import images page, item = get_item(state, params) is_style = (page is not None) and (page.title == 'Style') - is_valid = (item is not None) and hasattr(item, 'name') and hasattr(item, 'filename') + is_valid = False - if is_valid: - if TYPE_CHECKING: - assert item is not None # Part of the definition of "is_valid" + if (item is not None) and hasattr(item, 'name') and hasattr(item, 'filename'): + is_valid = True stat_size, stat_mtime = modelstats.stat(item.filename) if hasattr(item, 'size') and item.size > 0: stat_size = item.size if hasattr(item, 'mtime') and item.mtime is not None: stat_mtime = item.mtime desc = item.description - fullinfo = shared.readfile(os.path.splitext(item.filename)[0] + '.json', silent=True, as_type="dict") - if 'modelVersions' in fullinfo: # sanitize massive objects - fullinfo['modelVersions'] = [] - info = fullinfo - if isinstance(info, list): - item.filename = None - log.warning('Network: show details not supported for compound item') - info = None + info = shared.readfile(os.path.splitext(item.filename)[0] + '.json', silent=True, as_type="dict") + if 'modelVersions' in info: # sanitize massive objects + info['modelVersions'] = [] if prompt is not None and len(prompt) > 0: item.prompt = prompt if negative is not None and len(negative) > 0: @@ -974,10 +967,12 @@ def create_ui(container, button_parent, tabname, skip_indexing = False): ''' if item.name.startswith('Diffusers'): url = item.name.replace('Diffusers/', '') - url = f'https://huggingface.co/models/{url}' if url is not None else 'N/A' + url = f'https://huggingface.co/models/{url}' else: - url = info.get('id', None) if info is not None else None - url = f'civitai.com/models/{url}' if url is not None else 'N/A' + info_id = info.get('id', None) + nsfw = info.get('nsfw', False) if info_id is not None else False + tld = "red" if nsfw else "com" + url = f'civitai.{tld}/models/{info_id}' if info_id is not None else 'N/A' text = f'''

{item.name}

diff --git a/test/check-docs b/test/check-docs new file mode 100755 index 000000000..0f95a844b --- /dev/null +++ b/test/check-docs @@ -0,0 +1,35 @@ +#!/usr/bin/env bash + +set -euo pipefail + +ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" +cd "$ROOT_DIR" + +if [[ "${1:-}" == "-h" || "${1:-}" == "--help" ]]; then + cat <<'EOF' +Usage: + test/check-docs + test/check-docs wiki/File.md wiki/Other.md + test/check-docs --fix wiki/File.md + +Runs markdownlint-cli2 against wiki markdown files. +If no files are provided, all wiki markdown files are checked except files whose +basename starts with an underscore. + +Any arguments are passed through to markdownlint-cli2. +EOF + exit 0 +fi + +if [[ $# -eq 0 ]]; then + mapfile -t targets < <(rg --files wiki | rg '\.md$' | rg -v '(^|/)_') +else + targets=("$@") +fi + +if [[ ${#targets[@]} -eq 0 ]]; then + echo "No markdown files matched." >&2 + exit 1 +fi + +exec npx --yes markdownlint-cli2 "${targets[@]}" \ No newline at end of file diff --git a/wiki b/wiki index 26c4d7bf5..d286c08c2 160000 --- a/wiki +++ b/wiki @@ -1 +1 @@ -Subproject commit 26c4d7bf56a4f515f6e8baa7398573eecd19a95d +Subproject commit d286c08c2513ff6e69b7f332a03edc9db164d851