Merge branch 'dev' into feat/ltx-tab-unification

This commit is contained in:
Vladimir Mandic
2026-04-21 18:54:50 +02:00
committed by GitHub
37 changed files with 665 additions and 147 deletions
+4
View File
@@ -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.
+4
View File
@@ -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.
+174
View File
@@ -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 <file...>` 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
+30 -5
View File
@@ -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
+2 -2
View File
@@ -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",
+1 -1
View File
@@ -168,7 +168,7 @@ const jsConfig = defineConfig([
'@stylistic/max-len': [
'warn',
{
code: 275,
code: 300,
tabWidth: 2,
},
],
+34 -1
View File
@@ -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]
+3 -2
View File
@@ -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) {
+48 -3
View File
@@ -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 = `<div class="splash-log-row" style="color: ${color}">${logEntry.ts} &nbsp; ${joinArgs(logEntry.msg)}</div>`;
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() {
<div id="splash" class="splash" style="background: ${dark ? 'black' : 'white'}">
<div class="loading"><div class="loader"></div></div>
<div id="motd" class="motd""></div>
<div id="splashLog" class="splash-log" style="position: fixed; bottom: 0; text-align: left; padding: 8vh 8px 8px 8px; font-size: 12px; width: 100%; background: linear-gradient(0deg, darkslategray, transparent); opacity: 50%;"></div>
</div>`;
document.body.insertAdjacentHTML('beforeend', splash);
const ok = await preloadImages();
@@ -51,14 +87,23 @@ async function createSplash() {
return;
}
const imgEl = `<div id="spash-img" class="splash-img" alt="logo" style="background-image: url(file=html/logo-bg-${dark ? 'dark' : 'light'}.jpg), url(file=html/logo-bg-${num}.jpg); background-blend-mode: ${dark ? 'multiply' : 'lighten'}"></div>`;
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;
+13
View File
@@ -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
};
+42 -10
View File
@@ -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 });
});
/**
+15 -6
View File
@@ -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 = () => `
+6 -5
View File
@@ -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);
+136 -35
View File
@@ -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) {
+5 -5
View File
@@ -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}')
Binary file not shown.

After

Width:  |  Height:  |  Size: 64 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 62 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 94 KiB

After

Width:  |  Height:  |  Size: 61 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 76 KiB

After

Width:  |  Height:  |  Size: 64 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 78 KiB

After

Width:  |  Height:  |  Size: 64 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 80 KiB

After

Width:  |  Height:  |  Size: 62 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 76 KiB

After

Width:  |  Height:  |  Size: 61 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 0 B

After

Width:  |  Height:  |  Size: 81 KiB

+1
View File
@@ -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,
+2 -1
View File
@@ -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',
]
+21 -26
View File
@@ -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()
+6
View File
@@ -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}')
+13 -5
View File
@@ -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):
+1
View File
@@ -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'))
+24 -9
View File
@@ -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)
+19
View File
@@ -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("<h2>Appearance</h2>", "", 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("<h2>Mobile</h2>", "", 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("<h2>Log Display</h2>", "", 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}),
+1 -1
View File
@@ -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"<div title='Local'>{ui_symbols.svg_bullet.style('#00C0FD')}</div>"
status = f"<div {style} title='Local'>{ui_symbols.svg_bullet.style('#00C0FD')}</div>"
elif ext['status'] > 0:
if ext['status'] == 1:
status = f"<div {style} title='Verified'>{ui_symbols.svg_bullet.style('#00FD9C')}</div>"
+22 -27
View File
@@ -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'<a href="https://huggingface.co/{url}" target="_blank">https://huggingface.co/models/{url}</a>' if url is not None else 'N/A'
url = f'<a href="https://huggingface.co/{url}" target="_blank">https://huggingface.co/models/{url}</a>'
else:
url = info.get('id', None) if info is not None else None
url = f'<a href="https://civitai.com/models/{url}" target="_blank">civitai.com/models/{url}</a>' 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'<a href="https://civitai.{tld}/models/{info_id}" target="_blank">civitai.{tld}/models/{info_id}</a>' if info_id is not None else 'N/A'
text = f'''
<h2 style="border-bottom: 1px solid var(--button-primary-border-color); margin: 0em 0px 1em 0 !important">{item.name}</h2>
<table style="width: 100%; line-height: 1.5em;"><tbody>
+35
View File
@@ -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[@]}"
+1 -1
Submodule wiki updated: 26c4d7bf56...d286c08c25