add storage analyzer

Signed-off-by: Vladimir Mandic <mandic00@live.com>
This commit is contained in:
Vladimir Mandic
2026-08-02 11:22:12 +02:00
parent 78f843f969
commit 514a68b1af
19 changed files with 474 additions and 62 deletions
+8 -2
View File
@@ -1,6 +1,6 @@
# Change Log for SD.Next
## Update for 2026-07-31
## Update for 2026-08-02
- **Models**
- [SeFi-Image](https://huggingface.co/SeFi-Image/SeFi-Image-5B-RL) in *Base* and *Turbo* (distilled) variants
@@ -11,11 +11,17 @@
Mage-Flow is a 4B-scale generative stack for efficient text-to-image generation and instruction-based image editing
*note*: Microsoft released and then unpublished the model, but we still have a mirror available for download
- **Features**
- video: support for scripts/extensions
- storage analyzer: new feature that analyzes your used storage by sdnext per type and location
*system -> storage*
- video: support for scripts/extensions
video processing now supports scripts and extensions (if they support video processing)
*example*: use nudenet to automatically censor video frames :)
- prompt enhance: support for video generation
- startup: optimized server startup
- process: preserve audio when processing video
- remove background: new [lucida](https://huggingface.co/egeorcun/lucida) model
- **API**
- add `/sdapi/v1/storage` endpoint to return storage usage info
- **Fixes**
- seedvr quality
- skip-all do not skip env init
-1
View File
@@ -12,7 +12,6 @@
- Control tab verify overrides handling, @vladmandic
- Cloud providers, @CalamitousFelicitousness
- Video processing add/verify full API support, @CalamitousFelicitousness
- Storage analyzer, @vladmandic
- Lora: new handler, @CalamitousFelicitousness
- Processing -> Video capabilities, @vladmandic
- `RIFE` in processing
+2 -1
View File
@@ -51,7 +51,6 @@ class Api:
self.add_api_route("/sdapi/v1/status", server.get_status, methods=["GET"], response_model=models.ResStatus, tags=["Server"])
self.add_api_route("/sdapi/v1/platform", server.get_platform, methods=["GET"], tags=["Server"])
self.add_api_route("/sdapi/v1/progress", server.get_progress, methods=["GET"], response_model=models.ResProgress, tags=["Server"])
self.add_api_route("/sdapi/v1/history", server.get_history, methods=["GET"], response_model=list[models.ResHistory], tags=["Server"])
self.add_api_route("/sdapi/v1/interrupt", server.post_interrupt, methods=["POST"], status_code=204, tags=["Server"])
self.add_api_route("/sdapi/v1/skip", server.post_skip, methods=["POST"], status_code=204, tags=["Server"])
self.add_api_route("/sdapi/v1/shutdown", server.post_shutdown, methods=["POST"], status_code=204, tags=["Server"])
@@ -60,6 +59,8 @@ class Api:
self.add_api_route("/sdapi/v1/cmd-flags", server.get_cmd_flags, methods=["GET"], response_model=models.FlagsModel, tags=["Server"])
self.add_api_route("/sdapi/v1/gpu", gpu.get_gpu, methods=["GET"], tags=["Server"], response_model=list[dict])
self.add_api_route("/sdapi/v1/gpu-smi", gpu.get_gpu_smi, methods=["GET"], response_model=list[models.ResGPU], tags=["Server"])
self.add_api_route("/sdapi/v1/history", server.get_history, methods=["GET"], response_model=list[models.ResHistory], tags=["Server"])
self.add_api_route("/sdapi/v1/storage", server.get_storage, methods=["GET"], response_model=list[models.ResStorage], tags=["Server"])
# core api using locking
self.add_api_route("/sdapi/v1/txt2img", self.generate.post_text2img, methods=["POST"], response_model=models.ResTxt2Img, tags=["Generation"])
+17 -1
View File
@@ -443,7 +443,6 @@ class ReqGetLog(BaseModel):
lines: int = Field(default=100, title="Lines", description="How many lines to return")
clear: bool = Field(default=False, title="Clear", description="Should the log be cleared after returning the lines?")
class ReqPostLog(BaseModel):
json: dict | None = Field(default=None, title="Data", description="The data to log")
message: str | None = Field(default=None, title="Message", description="The info message to log")
@@ -453,6 +452,10 @@ class ReqPostLog(BaseModel):
class ReqHistory(BaseModel):
id: int | str | None = Field(default=None, title="Task ID", description="Task ID")
class ReqStorage(BaseModel):
folder: str | None = Field(default=None, title="Folder", description="Storage folder(s)")
types: str | None = Field(default=None, title="Types", description="Storage types to filter by")
class ReqProgress(BaseModel):
skip_current_image: bool = Field(default=False, title="Skip current image", description="Skip current image serialization")
@@ -472,6 +475,19 @@ class ResHistory(BaseModel):
duration: float | None = Field(default=None, title="Duration", description="Job duration")
outputs: list[str] = Field(title="Outputs", description="List of filenames")
class ResStorage(BaseModel):
name: str = Field(title="Name", description="Storage location name")
type: str = Field(title="Type", description="Storage location type")
folders: list[str] = Field(title="Folders", description="List of folders in the storage location")
paths: list[str] = Field(title="Paths", description="List of resolved paths in the storage location")
size: int = Field(title="Size", description="Total size of the storage location in bytes")
mtime: float = Field(title="Last modified", description="Last modified timestamp of the storage location")
nfiles: int = Field(title="Files", description="Number files in the storage location")
nfolders: int = Field(title="Folders", description="Number of folders in the storage location")
nsymlinks: int = Field(title="Symlinks", description="Number of symbolic links in the storage location")
nerrors: int = Field(title="Errors", description="Number of errors in the storage location")
time: float = Field(title="Time", description="Time taken to scan the storage location in seconds")
class ResStatus(BaseModel):
status: str = Field(title="Status", description="Current status")
task: str = Field(title="Task", description="Current job")
+9
View File
@@ -111,6 +111,15 @@ def get_history(req: models.ReqHistory = Depends()):
res = [models.ResHistory(**item) for item in res]
return res
def get_storage(req: models.ReqStorage = Depends()):
from modules.storage import check_storage
res = check_storage(folders=req.folder,
types=req.types.split(',') if req.types else None,
silent=True,
)
res = [models.ResStorage(**loc.dict()) for loc in res]
return res
def get_progress(req: models.ReqProgress = Depends()):
if shared.state.job_count == 0 and shared.state.sampling_step == 0: # truly idle
return models.ResProgress(id=shared.state.id, progress=0, eta_relative=0, state=shared.state.dict(), textinfo=shared.state.textinfo)
+2 -2
View File
@@ -407,9 +407,9 @@ def cleanup_models():
def move_files(src_path: str, dest_path: str, ext_filter: str | None = None):
try:
if not os.path.exists(dest_path):
os.makedirs(dest_path)
if os.path.exists(src_path):
if not os.path.exists(dest_path):
os.makedirs(dest_path)
for file in os.listdir(src_path):
fullpath = os.path.join(src_path, file)
if os.path.isfile(fullpath):
+27 -9
View File
@@ -13,43 +13,61 @@ def walk(folder: str):
return files
def stat(folder: str) -> tuple[int, datetime]:
def stat(folder: str, follow: bool = False, extended: bool = False, exclude: list[str] = []):
_files = 0
_folders = 0
_symlinks = 0
_errors = 0
_size = 0
_mtime = 0.0
def recurse(folder: str):
nonlocal _size, _mtime
nonlocal _size, _mtime, _files, _folders, _symlinks, _errors
with os.scandir(folder) as entries:
for entry in entries:
try:
if entry.is_file(follow_symlinks=False):
if any(part == ex for part in entry.path.split(os.sep) for ex in exclude):
continue
if entry.is_file(follow_symlinks=follow):
try:
_stat = entry.stat(follow_symlinks=False)
_stat = entry.stat(follow_symlinks=follow)
except Exception:
_stat = os.stat(entry.path, follow_symlinks=False)
_stat = os.stat(entry.path, follow_symlinks=follow)
_size += _stat.st_size
_files += 1
if _stat.st_mtime > _mtime:
_mtime = _stat.st_mtime
elif entry.is_dir(follow_symlinks=False):
elif entry.is_symlink():
_symlinks += 1
elif entry.is_dir(follow_symlinks=follow):
_folders += 1
recurse(entry.path)
except (FileNotFoundError, PermissionError):
_errors += 1
continue
try:
if os.path.isfile(folder):
_stat = os.stat(folder, follow_symlinks=False)
s_folder = str(folder)
if any(s_folder in ex for ex in exclude):
return _size, datetime.fromtimestamp(_mtime).replace(microsecond=0), _files, _folders, _symlinks, _errors
elif os.path.isfile(folder):
_stat = os.stat(folder, follow_symlinks=follow)
_size = _stat.st_size
_mtime = _stat.st_mtime
_files = 1
elif os.path.isdir(folder):
_folders = 1
recurse(folder)
else:
pass
except (FileNotFoundError, PermissionError):
pass
_errors += 1
try:
_datetime = datetime.fromtimestamp(_mtime).replace(microsecond=0)
except (OSError, ValueError):
_datetime = datetime.fromtimestamp(0)
if extended:
return _size, _datetime, _files, _folders, _symlinks, _errors
return _size, _datetime
+1 -1
View File
@@ -72,7 +72,7 @@ def path_to_repo(checkpoint_info: CheckpointInfo | str):
for opt in [shared.opts.ckpt_dir, shared.opts.diffusers_dir, shared.opts.hfcache_dir]:
remove_prefix.append(opt.replace('\\', '/'))
try:
relative = os.path.relpath(opt, start=shared.opts.models_dir).replace('\\', '/')
relative = os.path.relpath(opt, start=shared.models_path).replace('\\', '/')
if not relative.startswith('.'):
remove_prefix.append(relative)
except Exception:
+176
View File
@@ -0,0 +1,176 @@
import os
import time
from datetime import datetime
from pathlib import Path
from modules import paths
from modules.logger import log
from modules.shared import opts, max_workers
from modules.modelstats import stat
class Location:
name: str
folders: list[str]
paths: list[Path]
nfiles: int = 0
nfolders: int = 0
nsymlinks: int = 0
nerrors: int = 0
type: str = ''
size: int = 0
time: float = 0.0
mtime: datetime = datetime.fromtimestamp(0)
def __init__(self, name: str | None, folders: str | list[str], what: str = ''):
self.type = what
if isinstance(folders, str):
folders = [folders]
self.name = name if name is not None else ', '.join(folders)
self.folders = folders
self.paths = [Path(f).resolve(strict=False) for f in self.folders if f is not None and f != '']
def __repr__(self):
return f'Location(type={self.type} name="{self.name}" folders={self.folders} size={self.size/1024/1024:.3f} mtime="{self.mtime}" files={self.nfiles} folders={self.nfolders} symlinks={self.nsymlinks} errors={self.nerrors} time={self.time:.3f})'
def dict(self):
return {
'name': self.name,
'type': self.type,
'folders': self.folders,
'paths': [str(p) for p in self.paths],
'size': self.size,
'mtime': self.mtime.timestamp(),
'nfiles': self.nfiles,
'nfolders': self.nfolders,
'nsymlinks': self.nsymlinks,
'nerrors': self.nerrors,
'time': self.time,
}
def get_all_locations(types: list[str] | None = []) -> list[Location]:
locations = []
if types is None or 'All' in types or 'Models' in types:
locations.append(Location('SD Models', opts.ckpt_dir, 'Models'))
locations.append(Location('Diffusers Models', opts.diffusers_dir, 'Models'))
locations.append(Location('Huggingface Modules', opts.hfcache_dir, 'Models'))
locations.append(Location('VAE', [opts.vae_dir, os.path.join(paths.models_path, "TAESD")], 'Models'))
locations.append(Location('UNet', opts.unet_dir, 'Models'))
locations.append(Location('TextEncoder', opts.te_dir, 'Models'))
locations.append(Location('LoRA', opts.lora_dir, 'Models'))
locations.append(Location('ControlNets', opts.control_dir, 'Models'))
locations.append(Location('Embeddings', opts.embeddings_dir, 'Models'))
locations.append(Location('Detailers', [opts.yolo_dir, os.path.join(paths.models_path, 'Ultralytics')], 'Models'))
locations.append(Location('Upscalers', [opts.esrgan_models_path, opts.bsrgan_models_path, opts.realesrgan_models_path, opts.scunet_models_path, opts.swinir_models_path, os.path.join(paths.models_path, 'chaiNNer'), os.path.join(paths.models_path, 'GFPGAN'), os.path.join(paths.models_path, 'Spandrel'), os.path.join(paths.models_path, 'SeedVR2')], 'Models')) # chainners extension has late opts init
locations.append(Location('CLiP', opts.clip_models_path, 'Models'))
locations.append(Location('Rembg', os.path.join(paths.models_path, 'Rembg'), 'Models'))
locations.append(Location('RIFE', os.path.join(paths.models_path, 'RIFE'), 'Models'))
if types is None or 'All' in types or 'Data' in types:
locations.append(Location('Configs', ['data', paths.sd_configs_path], 'Data'))
locations.append(Location('AutoComplete', opts.autocomplete_dir, 'Data'))
locations.append(Location('Styles', opts.styles_dir, 'Data'))
locations.append(Location('Wildcards', opts.wildcards_dir, 'Data'))
locations.append(Location('Reference', paths.reference_path, 'Data'))
locations.append(Location('LUTs', os.path.join(paths.models_path, 'LUTs'), 'Data'))
locations.append(Location('Wiki', 'wiki', 'Data'))
if types is None or 'All' in types or 'Cache' in types:
locations.append(Location('Temp', opts.temp_dir, 'Cache'))
locations.append(Location('XET', opts.xetcache_dir, 'Cache'))
locations.append(Location('OpenVINO', opts.openvino_cache_path, 'Cache'))
locations.append(Location('ONNX', opts.onnx_cached_models_path, 'Cache'))
locations.append(Location('VENV', 'venv', 'Cache'))
locations.append(Location('Torch', [opts.tunable_dir, os.getenv("TORCHINDUCTOR_CACHE_DIR", None), os.getenv("TRITON_CACHE_DIR", None)], 'Cache'))
if types is None or 'All' in types or 'Code' in types:
locations.append(Location('Modules', 'modules', 'Code'))
locations.append(Location('Pipelines', 'pipelines', 'Code'))
locations.append(Location('Scripts', 'scripts', 'Code'))
locations.append(Location('UI', 'ui', 'Code'))
locations.append(Location('Builtin', paths.extensions_builtin_dir, 'Code'))
locations.append(Location('Extensions', paths.extensions_dir, 'Code'))
if types is None or 'All' in types or 'Images' in types:
locations.append(Location('Text', [opts.outdir_txt2img_samples], 'Images'))
locations.append(Location('Image', [opts.outdir_img2img_samples], 'Images'))
locations.append(Location('Control', [opts.outdir_control_samples], 'Images'))
locations.append(Location('Extras', [opts.outdir_extras_samples], 'Images'))
locations.append(Location('Save', [opts.outdir_save], 'Images'))
locations.append(Location('Grids', [opts.outdir_txt2img_grids, opts.outdir_img2img_grids, opts.outdir_control_grids], 'Images'))
if types is None or 'All' in types or 'Videos' in types:
locations.append(Location('Video', [opts.outdir_video], 'Videos'))
return locations
def get_other_locations(locations: list[Location], name: str, folder: str, what: str = 'Other') -> list[Location]:
# get list of first level subfolders in `folder` check each if its already in `locations` by comparing resolved paths if not, add each to the list as a new Location with type `what`
existing_paths = set()
for location in locations:
for path in location.paths:
existing_paths.add(path.resolve(strict=False))
try:
with os.scandir(folder) as entries:
for entry in entries:
if entry.is_dir(follow_symlinks=False):
path = Path(entry.path).resolve(strict=False)
if path not in existing_paths:
locations.append(Location(name, entry.path, what))
except (FileNotFoundError, PermissionError):
pass
return locations
def print_summary(locations: list[Location]):
summary = {}
for location in locations:
if location.type not in summary:
summary[location.type] = {
'size': 0,
'mtime': datetime.fromtimestamp(0),
'nfiles': 0,
'nfolders': 0,
'nerrors': 0,
}
summary[location.type]['size'] += location.size
if location.mtime > summary[location.type]['mtime']:
summary[location.type]['mtime'] = location.mtime
summary[location.type]['nfiles'] += location.nfiles
summary[location.type]['nfolders'] += location.nfolders
summary[location.type]['nerrors'] += location.nerrors
for k, v in summary.items():
log.debug(f'Storage: type={k} size={v["size"]/1024/1024:.3f} files={v["nfiles"]} folders={v["nfolders"]}')
def check_storage(folders: str | list[str] | None = None, types: list[str] | None = None, silent: bool = False) -> list[Location]:
if isinstance(folders, str):
folders = [folders]
if folders is not None and len(folders) > 0:
locations = [Location(None, folder) for _i, folder in enumerate(folders)]
else:
locations = get_all_locations(types)
if types is None or 'Other' in types or 'All' in types:
locations = get_other_locations(locations, 'Other', paths.models_path)
log.debug(f'Storage: locations={len(locations)} workers={max_workers} types={types} folders={folders} start')
def update_stats(location: Location) -> Location:
t0 = time.time()
for f in location.paths:
size, mtime, files, folders, symlinks, errors = stat(f, extended=True, exclude=['__pycache__', '.'])
location.size += size
if mtime > location.mtime:
location.mtime = mtime
location.nfiles += files
location.nfolders += folders
location.nsymlinks += symlinks
location.nerrors += errors
location.time = time.time() - t0
return location
t0 = time.time()
from concurrent.futures import ThreadPoolExecutor, as_completed
with ThreadPoolExecutor(max_workers=max_workers) as executor:
future_items = {executor.submit(update_stats, location): location for location in locations}
for future in as_completed(future_items):
location = future.result()
if location.size > 0 and not silent:
log.debug(location)
t1 = time.time()
print_summary(locations)
log.debug(f'Storage: time={t1-t0:.3f} end')
return locations
+5 -1
View File
@@ -1,7 +1,7 @@
import os
import gradio as gr
from modules import timer, shared, paths, theme, sd_models, modelloader, generation_parameters_copypaste, call_queue, script_callbacks
from modules import ui_common, ui_loadsave, ui_history, ui_components, ui_symbols
from modules import ui_common, ui_loadsave, ui_history, ui_storage, ui_components, ui_symbols
from modules.logger import log
@@ -313,6 +313,10 @@ def create_ui(disabled_tabs=None):
with gr.TabItem("History", id="system_history", elem_id="tab_history"):
ui_history.create_ui()
if 'storage' not in disabled_tabs:
with gr.TabItem("Storage", id="system_storage", elem_id="tab_storage"):
ui_storage.create_ui()
if 'monitor' not in disabled_tabs:
with gr.TabItem("GPU Monitor", id="system_gpu", elem_id="tab_gpu"):
with gr.Row(elem_id='gpu-controls'):
+13
View File
@@ -0,0 +1,13 @@
import gradio as gr
def create_ui():
types = ['All', 'Images', 'Videos', 'Models', 'Data', 'Cache', 'Code', 'Other']
with gr.Row():
btn_refresh = gr.Button("Calculate", elem_id='btn_storage_refresh')
storage_type = gr.Dropdown(label="Storage type", elem_id='storage_type', choices=types, value=[types[0]], multiselect=True)
with gr.Row():
_storage_table = gr.HTML('', elem_id='storage_table')
with gr.Row():
_storage_timeline = gr.HTML('', elem_id='storage_timeline')
btn_refresh.click(_js='refreshStorage', fn=None, inputs=[storage_type], outputs=[], show_progress='full')
+99 -35
View File
@@ -2759,15 +2759,15 @@ var require_jquery = __commonJS({
function returnFalse() {
return false;
}
function on(elem, types, selector, data, fn, one) {
function on(elem, types2, selector, data, fn, one) {
var origFn, type;
if (typeof types === "object") {
if (typeof types2 === "object") {
if (typeof selector !== "string") {
data = data || selector;
selector = void 0;
}
for (type in types) {
on(elem, type, selector, data, types[type], one);
for (type in types2) {
on(elem, type, selector, data, types2[type], one);
}
return elem;
}
@@ -2798,11 +2798,11 @@ var require_jquery = __commonJS({
fn.guid = origFn.guid || (origFn.guid = jQuery3.guid++);
}
return elem.each(function() {
jQuery3.event.add(this, types, fn, data, selector);
jQuery3.event.add(this, types2, fn, data, selector);
});
}
jQuery3.event = {
add: function(elem, types, handler, data, selector) {
add: function(elem, types2, handler, data, selector) {
var handleObjIn, eventHandle, tmp, events, t, handleObj, special, handlers, type, namespaces, origType, elemData = dataPriv.get(elem);
if (!acceptData(elem)) {
return;
@@ -2826,10 +2826,10 @@ var require_jquery = __commonJS({
return typeof jQuery3 !== "undefined" && jQuery3.event.triggered !== e.type ? jQuery3.event.dispatch.apply(elem, arguments) : void 0;
};
}
types = (types || "").match(rnothtmlwhite) || [""];
t = types.length;
types2 = (types2 || "").match(rnothtmlwhite) || [""];
t = types2.length;
while (t--) {
tmp = rtypenamespace.exec(types[t]) || [];
tmp = rtypenamespace.exec(types2[t]) || [];
type = origType = tmp[1];
namespaces = (tmp[2] || "").split(".").sort();
if (!type) {
@@ -2871,20 +2871,20 @@ var require_jquery = __commonJS({
}
},
// Detach an event or set of events from an element
remove: function(elem, types, handler, selector, mappedTypes) {
remove: function(elem, types2, handler, selector, mappedTypes) {
var j, origCount, tmp, events, t, handleObj, special, handlers, type, namespaces, origType, elemData = dataPriv.hasData(elem) && dataPriv.get(elem);
if (!elemData || !(events = elemData.events)) {
return;
}
types = (types || "").match(rnothtmlwhite) || [""];
t = types.length;
types2 = (types2 || "").match(rnothtmlwhite) || [""];
t = types2.length;
while (t--) {
tmp = rtypenamespace.exec(types[t]) || [];
tmp = rtypenamespace.exec(types2[t]) || [];
type = origType = tmp[1];
namespaces = (tmp[2] || "").split(".").sort();
if (!type) {
for (type in events) {
jQuery3.event.remove(elem, type + types[t], handler, selector, true);
jQuery3.event.remove(elem, type + types2[t], handler, selector, true);
}
continue;
}
@@ -3234,26 +3234,26 @@ var require_jquery = __commonJS({
};
});
jQuery3.fn.extend({
on: function(types, selector, data, fn) {
return on(this, types, selector, data, fn);
on: function(types2, selector, data, fn) {
return on(this, types2, selector, data, fn);
},
one: function(types, selector, data, fn) {
return on(this, types, selector, data, fn, 1);
one: function(types2, selector, data, fn) {
return on(this, types2, selector, data, fn, 1);
},
off: function(types, selector, fn) {
off: function(types2, selector, fn) {
var handleObj, type;
if (types && types.preventDefault && types.handleObj) {
handleObj = types.handleObj;
jQuery3(types.delegateTarget).off(
if (types2 && types2.preventDefault && types2.handleObj) {
handleObj = types2.handleObj;
jQuery3(types2.delegateTarget).off(
handleObj.namespace ? handleObj.origType + "." + handleObj.namespace : handleObj.origType,
handleObj.selector,
handleObj.handler
);
return this;
}
if (typeof types === "object") {
for (type in types) {
this.off(type, selector, types[type]);
if (typeof types2 === "object") {
for (type in types2) {
this.off(type, selector, types2[type]);
}
return this;
}
@@ -3265,7 +3265,7 @@ var require_jquery = __commonJS({
fn = returnFalse;
}
return this.each(function() {
jQuery3.event.remove(this, types, fn, selector);
jQuery3.event.remove(this, types2, fn, selector);
});
}
});
@@ -5823,17 +5823,17 @@ var require_jquery = __commonJS({
};
});
jQuery3.fn.extend({
bind: function(types, data, fn) {
return this.on(types, null, data, fn);
bind: function(types2, data, fn) {
return this.on(types2, null, data, fn);
},
unbind: function(types, fn) {
return this.off(types, null, fn);
unbind: function(types2, fn) {
return this.off(types2, null, fn);
},
delegate: function(selector, types, data, fn) {
return this.on(types, selector, data, fn);
delegate: function(selector, types2, data, fn) {
return this.on(types2, selector, data, fn);
},
undelegate: function(selector, types, fn) {
return arguments.length === 1 ? this.off(selector, "**") : this.off(types, selector || "**", fn);
undelegate: function(selector, types2, fn) {
return arguments.length === 1 ? this.off(selector, "**") : this.off(types2, selector || "**", fn);
},
hover: function(fnOver, fnOut) {
return this.on("mouseenter", fnOver).on("mouseleave", fnOut || fnOver);
@@ -16448,7 +16448,7 @@ var Timesheet = class {
// ui/history.ts
var inferenceTypes = ["inference", "vae", "te"];
var ioTypes = ["load", "save"];
function refreshHistory() {
async function refreshHistory() {
log("refreshHistory");
authFetch(`${window.api}/history`, { priority: "low" }).then((res) => {
if (!res) return;
@@ -16500,6 +16500,70 @@ function refreshHistory() {
}
window.refreshHistory = refreshHistory;
// ui/storage.ts
var types = ["Images", "Videos", "Models", "Data", "Cache", "Code", "Other"];
function buildTable(type, data) {
const totalSize = data.reduce((acc, entry) => acc + entry.size, 0);
const totalLoc = data.length;
const totalFiles = data.reduce((acc, entry) => acc + entry.nfiles, 0);
const totalFolders = data.reduce((acc, entry) => acc + entry.nfolders, 0);
let title = `Locations: ${totalLoc}
Total Size: ${(totalSize / (1024 * 1024)).toLocaleString(void 0, { minimumFractionDigits: 2, maximumFractionDigits: 2 })} MB
Total Files: ${totalFiles}
Total Folders: ${totalFolders}
`;
let html = `<h2 title="${title}">${type}</h2><table><tbody>`;
for (const entry of data) {
if (entry.size === 0) continue;
const size = (entry.size / (1024 * 1024)).toLocaleString(void 0, { minimumFractionDigits: 2, maximumFractionDigits: 2 }) + " MB";
const mtime = entry.mtime > 0 ? new Date(entry.mtime * 1e3).toLocaleString() : "";
title = `Type: ${entry.type}
Name: ${entry.name}
Size: ${size}
Last modified: ${mtime}
`;
title += `Folders: ${entry.folders.join(", ")}
Resolved paths: ${entry.paths.join(", ")}
`;
title += `Subfolders: ${entry.nfolders}
Files: ${entry.nfiles}
Symlinks: ${entry.nsymlinks}
Errors: ${entry.nerrors}
`;
title += `Time to scan: ${entry.time.toFixed(3)} seconds`;
const perc = Math.round(entry.size / totalSize * 100);
const color = `rgb(${perc}, 50, 80)`;
const css = `background: linear-gradient(to right, ${color} ${perc}%, transparent ${perc}%);`;
html += `<tr title="${title}"><td style="${css}">${entry.name}</td><td>${size}</td><td>${mtime}</td></tr>`;
}
html += "</tbody></table>";
return html;
}
async function refreshStorage(storageTypes) {
log("refreshStorage", storageTypes);
authFetch(`${window.api}/storage?types=${storageTypes.join(",")}`, { priority: "low" }).then((res) => {
if (!res) return;
const timeline = document.getElementById("storage_timeline");
const table = document.getElementById("storage_table");
if (!timeline || !table) return;
timeline.innerHTML = "";
res.json().then((rawData) => {
const data = rawData;
if (!data || !data.length) {
table.innerHTML = "<p>No storage data available.</p>";
return;
}
table.innerHTML = "";
if (storageTypes.includes("All")) storageTypes = types;
for (const type of storageTypes) {
const typeData = data.filter((entry) => entry.type === type);
if (typeData.length > 0) table.innerHTML += buildTable(type, typeData);
}
});
});
}
window.refreshStorage = refreshStorage;
// ui/aspectRatioOverlay.ts
var currentWidth = null;
var currentHeight = null;
+4 -4
View File
File diff suppressed because one or more lines are too long
+1
View File
@@ -119,6 +119,7 @@ declare global {
disableGPU?: () => Promise<void>; // ui/gpu.ts
startGPU?: () => Promise<void>; // ui/gpu.ts
refreshHistory?: () => void; // ui/history.ts
refreshStorage?: (storageTypes: string[]) => void; // ui/storage.ts
inputAccordionChecked?: (id: string, checked: boolean) => void; // ui/inputAccordion.ts
debug?: (...args: unknown[]) => Promise<void>; // ui/logger.ts
error?: (...args: unknown[]) => Promise<void>; // ui/logger.ts
+1 -1
View File
@@ -22,7 +22,7 @@ interface TimelineEntry {
const inferenceTypes = ['inference', 'vae', 'te'];
const ioTypes = ['load', 'save'];
export function refreshHistory() {
export async function refreshHistory() {
log('refreshHistory');
authFetch(`${window.api}/history`, { priority: 'low' }).then((res) => {
if (!res) return;
+1
View File
@@ -27,6 +27,7 @@ import './promptChecker';
import './setHints';
import './monitor';
import './history';
import './storage';
import './aspectRatioOverlay';
import './resolutionLock';
import './authWrap';
+97
View File
@@ -0,0 +1,97 @@
import { Timesheet } from './timesheet';
import { log } from './logger';
import { authFetch } from './authWrap';
const types = ['Images', 'Videos', 'Models', 'Data', 'Cache', 'Code', 'Other'];
interface LocationEntry {
name: string;
type: 'Images' | 'Videos' | 'Models' | 'Data' | 'Cache' | 'Code' | 'Other';
folders: string[];
paths: string[];
size: number;
mtime: number;
nfiles: number;
nfolders: number;
nsymlinks: number;
nerrors: number;
time: number;
}
interface TimelineEntry {
start: number;
end: number;
label: string;
type: 'inference' | 'io' | 'default';
}
function buildTable(type: string, data: LocationEntry[]) {
// let html = `<h2>${type}</h2><table><thead><tr><th>Location</th><th>Size</th><th>MTime</th></tr></thead><tbody>`;
const totalSize = data.reduce((acc, entry) => acc + entry.size, 0);
const totalLoc = data.length;
const totalFiles = data.reduce((acc, entry) => acc + entry.nfiles, 0);
const totalFolders = data.reduce((acc, entry) => acc + entry.nfolders, 0);
let title = `Locations: ${totalLoc}\nTotal Size: ${(totalSize / (1024 * 1024)).toLocaleString(undefined, { minimumFractionDigits: 2, maximumFractionDigits: 2 })} MB\nTotal Files: ${totalFiles}\nTotal Folders: ${totalFolders}\n`;
let html = `<h2 title="${title}">${type}</h2><table><tbody>`;
for (const entry of data) {
if (entry.size === 0) continue;
const size = (entry.size / (1024 * 1024)).toLocaleString(undefined, { minimumFractionDigits: 2, maximumFractionDigits: 2 }) + ' MB';
const mtime = entry.mtime > 0 ? new Date(entry.mtime * 1000).toLocaleString() : '';
title = `Type: ${entry.type}\nName: ${entry.name}\nSize: ${size}\nLast modified: ${mtime}\n`;
title += `Folders: ${entry.folders.join(', ')}\nResolved paths: ${entry.paths.join(', ')}\n`;
title += `Subfolders: ${entry.nfolders}\nFiles: ${entry.nfiles}\nSymlinks: ${entry.nsymlinks}\nErrors: ${entry.nerrors}\n`;
title += `Time to scan: ${entry.time.toFixed(3)} seconds`;
const perc = Math.round((entry.size / totalSize) * 100);
const color = `rgb(${perc}, 50, 80)`;
const css = `background: linear-gradient(to right, ${color} ${perc}%, transparent ${perc}%);`;
html += `<tr title="${title}"><td style="${css}">${entry.name}</td><td>${size}</td><td>${mtime}</td></tr>`;
}
html += '</tbody></table>';
return html;
}
export async function refreshStorage(storageTypes: string[]) {
log('refreshStorage', storageTypes);
authFetch(`${window.api}/storage?types=${storageTypes.join(',')}`, { priority: 'low' }).then((res) => {
if (!res) return;
const timeline = document.getElementById('storage_timeline');
const table = document.getElementById('storage_table');
if (!timeline || !table) return;
timeline.innerHTML = '';
res.json().then((rawData) => {
const data = rawData as LocationEntry[];
if (!data || !data.length) {
table.innerHTML = '<p>No storage data available.</p>';
return;
}
table.innerHTML = '';
if (storageTypes.includes('All')) storageTypes = types;
for (const type of storageTypes) {
const typeData = data.filter((entry) => entry.type === type);
if (typeData.length > 0) table.innerHTML += buildTable(type, typeData);
}
/*
// build timeline
const ts: TimelineEntry[] = [];
for (const entry of data) {
if (entry.op === 'begin') {
const start = entry.timestamp;
const endEntry = data.find((e) => (e.id === entry.id && e.op === 'end'));
const end = endEntry?.timestamp ?? data[data.length - 1].timestamp;
if (end - start < 0.02) continue; // skip very short entries
if (inferenceTypes.some((type) => entry.job.toLowerCase().startsWith(type))) entry.type = 'inference';
else if (ioTypes.some((type) => entry.job.toLowerCase().startsWith(type))) entry.type = 'io';
else entry.type = 'default';
if (start && end) ts.push({ start, end, label: entry.job, type: entry.type });
}
}
if (!ts.length) return;
// eslint-disable-next-line no-new
new Timesheet(timeline, ts);
*/
});
});
}
window.refreshStorage = refreshStorage;
+10 -3
View File
@@ -135,10 +135,17 @@ def initialize():
modules.extra_networks.register_default_extra_networks()
timer.startup.record("networks")
from modules.models_hf import hf_init, hf_check_cache
from modules.models_hf import hf_init
hf_init()
hf_check_cache()
timer.startup.record("huggingface")
if shared.cmd_opts.test:
from modules.models_hf import hf_check_cache
hf_check_cache()
timer.startup.record("huggingface")
if shared.cmd_opts.test:
from modules.storage import check_storage
check_storage()
timer.startup.record("storage")
if shared.cmd_opts.tls_keyfile is not None and shared.cmd_opts.tls_certfile is not None:
try: