Merge branch 'dev' into feat/ltx-tab-unification
@@ -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.
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -168,7 +168,7 @@ const jsConfig = defineConfig([
|
||||
'@stylistic/max-len': [
|
||||
'warn',
|
||||
{
|
||||
code: 275,
|
||||
code: 300,
|
||||
tabWidth: 2,
|
||||
},
|
||||
],
|
||||
|
||||
@@ -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]
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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} ${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;
|
||||
|
||||
@@ -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
|
||||
};
|
||||
|
||||
@@ -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,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 = () => `
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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}')
|
||||
|
||||
|
||||
|
After Width: | Height: | Size: 64 KiB |
|
After Width: | Height: | Size: 62 KiB |
|
Before Width: | Height: | Size: 94 KiB After Width: | Height: | Size: 61 KiB |
|
Before Width: | Height: | Size: 76 KiB After Width: | Height: | Size: 64 KiB |
|
Before Width: | Height: | Size: 78 KiB After Width: | Height: | Size: 64 KiB |
|
Before Width: | Height: | Size: 80 KiB After Width: | Height: | Size: 62 KiB |
|
Before Width: | Height: | Size: 76 KiB After Width: | Height: | Size: 61 KiB |
|
Before Width: | Height: | Size: 0 B After Width: | Height: | Size: 81 KiB |
@@ -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,
|
||||
|
||||
@@ -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',
|
||||
]
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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}')
|
||||
|
||||
@@ -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):
|
||||
|
||||
@@ -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'))
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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}),
|
||||
|
||||
@@ -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>"
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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[@]}"
|
||||