add more job state updates and update history tab

Signed-off-by: Vladimir Mandic <mandic00@live.com>
pull/4207/head
Vladimir Mandic 2025-09-13 10:54:04 -04:00
parent 4e74172f3a
commit d351fdb98f
26 changed files with 69 additions and 77 deletions

View File

@ -9,6 +9,7 @@ StandardUI is still available and can be selected in settings, but ModernUI is n
*What's else*? **Chroma** is in its final form, there are several new **Qwen-Image** variants and **Nunchaku** hit version 1.0!
Also, there are quite a few offloading improvements and many quality-of-life changes to UI and overal workflows
And check out new **history** tab in the right panel, it now shows visualization of entire processing timeline!
[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)

View File

@ -44,7 +44,6 @@ async function updateGPU() {
}
const gpuTbody = gpuTable.querySelector('tbody');
for (const gpu of data) {
console.log(gpu);
let rows = `<tr><td>GPU</td><td>${gpu.name}</td></tr>`;
for (const item of Object.entries(gpu.data)) rows += `<tr><td>${item[0]}</td><td>${item[1]}</td></tr>`;
gpuTbody.innerHTML = rows;

View File

@ -1,8 +1,12 @@
const inferenceTypes = ['inference', 'vae', 'te'];
const ioTypes = ['load', 'save'];
function refreshHistory() {
log('refreshHistory');
fetch(`${window.api}/history`, { priority: 'low' }).then((res) => {
const timeline = document.getElementById('history_timeline');
const table = document.getElementById('history_table');
timeline.innerHTML = '';
res.json().then((data) => {
if (!data || !data.length) {
table.innerHTML = '<p>No history data available.</p>';
@ -37,10 +41,13 @@ function refreshHistory() {
if (entry.op === 'begin') {
const start = entry.timestamp;
const end = data.find((e) => (e.id === entry.id && e.op === 'end')) || data[data.length - 1].timestamp;
ts.push({ start, end: end.timestamp, label: entry.job, type: '' });
if (end.timestamp - 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';
ts.push({ start, end: end.timestamp, label: entry.job, type: entry.type });
}
}
timeline.innerHTML = '';
if (!ts.length) return;
new Timesheet(timeline, ts); // eslint-disable-line no-undef, no-new
});

View File

@ -7,36 +7,6 @@
background-color: var(--sd-main-background-color);
position: relative;
}
.timesheet.color-scheme-default .bubble-default {
background-color: RGBA(252, 70, 74, 1);
}
.timesheet.color-scheme-default .bubble-lorem {
background-color: RGBA(154, 202, 39, 1);
}
.timesheet.color-scheme-default .bubble-ipsum {
background-color: RGBA(60, 182, 227, 1);
}
.timesheet.color-scheme-default .bubble-dolor {
background-color: RGBA(244, 207, 48, 1);
}
.timesheet.color-scheme-default .bubble-sit {
background-color: RGBA(169, 105, 202, 1);
}
.timesheet.color-scheme-alternative .bubble-default {
background-color: #f3552e;
}
.timesheet.color-scheme-alternative .bubble-lorem {
background-color: #88c33a;
}
.timesheet.color-scheme-alternative .bubble-ipsum {
background-color: #436ae0;
}
.timesheet.color-scheme-alternative .bubble-dolor {
background-color: #f4d234;
}
.timesheet.color-scheme-alternative .bubble-sit {
background-color: #707d86;
}
.timesheet .scale {
height: 100%;
position: absolute;
@ -52,7 +22,8 @@
line-height: 24px;
font-weight: lighter;
border-left: 1px dashed rgba(250, 250, 250, 0.2);
height: 100%}
height: 100%;
}
.timesheet .data {
margin: 10px 0 10px 0;
padding: 0;
@ -93,29 +64,16 @@
float: left;
position: relative;
top: 7px;
border-radius: 4px;
border-radius: 2px;
margin: 0 10px 0 0;
opacity: 0.7;
}
.bubble-default {
background: var(--sd-main-accent-color);
}
#timesheet-alternative {
background-color: RGBA(247, 247, 247, 1);
border-radius: 5px;
.bubble-inference {
background-color: violet;
}
#timesheet-alternative section {
color: RGBA(63, 68, 72, 1);
border-left: 1px dashed RGBA(63, 68, 72, 0.2);
}
#timesheet-alternative section:first-child {
border-left: 1px dashed transparent;
}
#timesheet-alternative .date {
display: none;
}
#timesheet-alternative .bubble {
margin-right: 7px;
}
#timesheet-alternative .label {
padding-left: 0px;
color: RGBA(48, 48, 48, 1);
.bubble-io {
background-color: rgb(75, 75, 150);
}

View File

@ -1,14 +1,15 @@
/* eslint max-classes-per-file: ["error", 2] */
class Bubble {
constructor(min, start, end, label, scale) {
constructor(min, start, end, label, scale, type) {
this.type = type;
this.label = label;
this.min = min;
this.start = start;
this.end = end;
this.scale = scale;
this.offset = Math.round(this.scale * (this.start - this.min));
this.width = Math.round(this.scale * (this.end - this.start));
this.label = label;
this.duration = Math.round(1000 * (this.end - this.start)) / 1000;
this.title = `Job: ${this.label}\nDuration: ${this.duration}s\nStart: ${new Date(1000 * this.start).toLocaleString()}\nEnd: ${new Date(1000 * this.end).toLocaleString()}`;
}
@ -38,11 +39,11 @@ class Timesheet {
html = [];
for (let n = 0, m = this.data.length; n < m; n++) {
const cur = this.data[n];
const bubble = new Bubble(this.min, cur.start, cur.end, cur.label, this.scale);
const bubble = new Bubble(this.min, cur.start, cur.end, cur.label, this.scale, cur.type);
const line = [
`<span title="${bubble.title}" style="margin-left: ${bubble.offset}px; width: ${bubble.width}px;" class="bubble" data-duration="${bubble.duration}"></span>`,
`<span class="date">${bubble.duration}</span> `,
`<span class="label">${bubble.label}</span>`,
`<span title="${bubble.title}" style="margin-left: ${bubble.offset}px; width: ${bubble.width}px;" class="bubble bubble-${bubble.type}" data-duration="${bubble.duration}"></span>`,
`<span class="date" title="${bubble.title}">${bubble.duration}</span> `,
`<span class="label" title="${bubble.title}">${bubble.label}</span>`,
].join('');
html.push(`<li>${line}</li>`);
}

View File

@ -189,7 +189,6 @@ function get_tab_index(tabId) {
.forEach((button, i) => {
if (button.className.indexOf('selected') !== -1) res = i;
});
console.log('get_tab_index', tabId, res);
return res;
}
@ -297,7 +296,6 @@ function submit_video_wrapper(...args) {
log('submitVideoWrapper', id);
const btn = gradioApp().getElementById(`${id}_generate_btn`);
if (btn) btn.click();
else console.log('submit_video_wrapper: no button found', id);
}
function submit_postprocessing(...args) {

View File

@ -62,7 +62,7 @@ def download_civit_preview(model_path: str, preview_url: str):
block_size = 16384 # 16KB blocks
written = 0
img = None
jobid = shared.state.begin('Download')
jobid = shared.state.begin('Download CivitAI')
if pbar is None:
pbar = p.Progress(p.TextColumn('[cyan]Download'), p.DownloadColumn(), p.BarColumn(), p.TaskProgressColumn(), p.TimeRemainingColumn(), p.TimeElapsedColumn(), p.TransferSpeedColumn(), p.TextColumn('[yellow]{task.description}'), console=shared.console)
try:
@ -139,7 +139,7 @@ def download_civit_model_thread(model_name: str, model_url: str, model_path: str
res += f' size={round((starting_pos + total_size)/1024/1024, 2)}Mb'
shared.log.info(res)
jobid = shared.state.begin('Download')
jobid = shared.state.begin('Download CivitAI')
block_size = 16384 # 16KB blocks
written = starting_pos
global pbar # pylint: disable=global-statement

View File

@ -51,6 +51,7 @@ def preprocess_image(
has_models:bool = False,
):
t0 = time.time()
jobid = shared.state.begin('Preprocess')
# run resize before
if p.resize_mode_before != 0 and p.resize_name_before != 'None':
@ -246,4 +247,5 @@ def preprocess_image(
t1 = time.time()
process_timer.add('proc', t1-t0)
shared.state.end(jobid)
return processed_image, blended_image

View File

@ -160,6 +160,7 @@ class Processor():
self.load_config[k] = v
def load(self, processor_id: str = None, force: bool = True) -> str:
from modules.shared import state
try:
t0 = time.time()
processor_id = processor_id or self.processor_id
@ -179,6 +180,7 @@ class Processor():
cls = config[processor_id]['class']
# log.debug(f'Control Processor loading: id="{processor_id}" class={cls.__name__}')
debug(f'Control Processor config={self.load_config}')
jobid = state.begin('Load processor')
if 'DWPose' in processor_id:
det_ckpt = 'https://download.openmmlab.com/mmdetection/v2.0/yolox/yolox_l_8x8_300e_coco/yolox_l_8x8_300e_coco_20211126_140236-d3bd2b23.pth'
if 'Tiny' == config['DWPose']['model']:
@ -209,6 +211,7 @@ class Processor():
else:
self.model = cls() # class instance only
t1 = time.time()
state.end(jobid)
self.processor_id = processor_id
log.debug(f'Control Processor loaded: id="{processor_id}" class={self.model.__class__.__name__} time={t1-t0:.2f}')
return f'Processor loaded: {processor_id}'

View File

@ -597,12 +597,9 @@ def control_run(state: str = '', # pylint: disable=keyword-arg-before-vararg
if processed_image is not None and isinstance(processed_image, Image.Image):
output_images.append(processed_image)
if is_generator:
if is_generator and frame is not None and video is not None:
image_txt = f'{output_image.width}x{output_image.height}' if output_image is not None else 'None'
if video is not None:
msg = f'Control output | {index} of {frames} skip {video_skip_frames} | Frame {image_txt}'
else:
msg = f'Control output | {index} of {len(inputs)} | Image {image_txt}'
msg = f'Control output | {index} of {frames} skip {video_skip_frames} | Frame {image_txt}'
yield (output_image, blended_image, msg) # result is control_output, proces_output
if video is not None and frame is not None:
@ -643,5 +640,7 @@ def control_run(state: str = '', # pylint: disable=keyword-arg-before-vararg
if len(info_txt) > 0:
html_txt = html_txt + infotext_to_html(info_txt[0])
if is_generator:
jobid = shared.state.begin('UI')
yield (output_images, blended_image, html_txt, output_filename)
shared.state.end(jobid)
return (output_images, blended_image, html_txt, output_filename)

View File

@ -4,7 +4,7 @@ import threading
from typing import Union
from diffusers import StableDiffusionPipeline, StableDiffusionXLPipeline, FluxPipeline, StableDiffusion3Pipeline, ControlNetModel
from modules.control.units import detect
from modules.shared import log, opts, cmd_opts, listdir
from modules.shared import log, opts, cmd_opts, state, listdir
from modules import errors, sd_models, devices, model_quant
from modules.processing import StableDiffusionProcessingControl
@ -308,6 +308,7 @@ class ControlNet():
log.error(f'Control {what} model load: id="{model_id}" unknown base model')
return
self.reset()
jobid = state.begin(f'Load {what}')
if model_path.endswith('.safetensors'):
self.load_safetensors(model_id, model_path, cls, config)
else:
@ -365,6 +366,7 @@ class ControlNet():
t1 = time.time()
self.model_id = model_id
log.info(f'Control {what} model loaded: id="{model_id}" path="{model_path}" cls={cls.__name__} time={t1-t0:.2f}')
state.end(jobid)
return f'{what} loaded model: {model_id}'
except Exception as e:
log.error(f'Control {what} model load: id="{model_id}" {e}')

View File

@ -61,7 +61,7 @@ def reset_model():
def load_model(variant:str=None, pipeline:str=None, text_encoder:str=None, text_encoder_2:str=None, feature_extractor:str=None, image_encoder:str=None, transformer:str=None):
shared.state.begin('Load')
shared.state.begin('Load FramePack')
if variant is not None:
if variant not in models.keys():
raise ValueError(f'FramePack: variant="{variant}" invalid')

View File

@ -84,6 +84,7 @@ def worker(
prompts = list(reversed(prompts))
def text_encode(prompt, i:int=None):
jobid = shared.state.begin('TE Encode')
pbar.update(task, description=f'text encode section={i}')
t0 = time.time()
torch.manual_seed(seed)
@ -102,9 +103,11 @@ def worker(
llama_vec_n, llama_attention_mask_n = utils.crop_or_pad_yield_mask(llama_vec_n, length=512)
sd_models.apply_balanced_offload(shared.sd_model)
timer.process.add('prompt', time.time()-t0)
shared.state.end(jobid)
return llama_vec, llama_vec_n, llama_attention_mask, llama_attention_mask_n, clip_l_pooler, clip_l_pooler_n
def latents_encode(input_image, end_image):
jobid = shared.state.begin('VAE Encode')
pbar.update(task, description='image encode')
# shared.log.debug(f'FramePack: image encode init={input_image.shape} end={end_image.shape if end_image is not None else None}')
t0 = time.time()
@ -126,6 +129,7 @@ def worker(
end_latent = None
sd_models.apply_balanced_offload(shared.sd_model)
timer.process.add('encode', time.time()-t0)
shared.state.end(jobid)
return start_latent, end_latent
def vision_encode(input_image, end_image):

View File

@ -46,7 +46,7 @@ def atomically_save_image():
Image.MAX_IMAGE_PIXELS = None # disable check in Pillow and rely on check below to allow large custom image sizes
while True:
image, filename, extension, params, exifinfo, filename_txt, is_grid = save_queue.get()
jobid = shared.state.begin('Save')
jobid = shared.state.begin('Save image')
shared.state.image_history += 1
if len(exifinfo) > 2:
with open(paths.params_path, "w", encoding="utf8") as file:

View File

@ -135,7 +135,7 @@ def interrogate(image, mode, caption=None):
def interrogate_image(image, clip_model, blip_model, mode):
jobid = shared.state.begin('Interrogate')
jobid = shared.state.begin('Interrogate CLiP')
try:
if shared.sd_loaded:
from modules.sd_models import apply_balanced_offload # prevent circular import

View File

@ -605,7 +605,7 @@ def sa2(question: str, image: Image.Image, repo: str = None):
def interrogate(question:str='', system_prompt:str=None, prompt:str=None, image:Image.Image=None, model_name:str=None, quiet:bool=False):
global quant_args # pylint: disable=global-statement
jobid = shared.state.begin('Interrogate')
jobid = shared.state.begin('Interrogate LLM')
t0 = time.time()
quant_args = model_quant.create_config(module='LLM')
model_name = model_name or shared.opts.interrogate_vlm_model

View File

@ -188,6 +188,7 @@ def load_image_encoder(pipe: diffusers.DiffusionPipeline, adapter_names: list[st
# load image encoder used by ip adapter
if pipe.image_encoder is None or clip_loaded != f'{clip_repo}/{clip_subfolder}':
jobid = shared.state.begin('Load encoder')
try:
if shared.sd_model_type == 'sd3':
image_encoder = transformers.SiglipVisionModel.from_pretrained(clip_repo, torch_dtype=devices.dtype, cache_dir=shared.opts.hfcache_dir)
@ -209,6 +210,7 @@ def load_image_encoder(pipe: diffusers.DiffusionPipeline, adapter_names: list[st
shared.log.error(f'IP adapter load: encoder="{clip_repo}/{clip_subfolder}" {e}')
errors.display(e, 'IP adapter: type=encoder')
return False
shared.state.end(jobid)
sd_models.move_model(pipe.image_encoder, devices.device)
return True
@ -217,6 +219,7 @@ def load_feature_extractor(pipe):
# load feature extractor used by ip adapter
if pipe.feature_extractor is None:
try:
jobid = shared.state.begin('Load extractor')
if shared.sd_model_type == 'sd3':
feature_extractor = transformers.SiglipImageProcessor.from_pretrained(SIGLIP_ID, torch_dtype=devices.dtype, cache_dir=shared.opts.hfcache_dir)
else:
@ -231,6 +234,7 @@ def load_feature_extractor(pipe):
shared.log.error(f'IP adapter load: extractor {e}')
errors.display(e, 'IP adapter: type=extractor')
return False
shared.state.end(jobid)
return True

View File

@ -137,6 +137,7 @@ def task_specific_kwargs(p, model):
def set_pipeline_args(p, model, prompts:list, negative_prompts:list, prompts_2:typing.Optional[list]=None, negative_prompts_2:typing.Optional[list]=None, prompt_attention:typing.Optional[str]=None, desc:typing.Optional[str]='', **kwargs):
t0 = time.time()
shared.sd_model = sd_models.apply_balanced_offload(shared.sd_model)
argsid = shared.state.begin('Params')
apply_circular(p.tiling, model)
args = {}
has_vae = hasattr(model, 'vae') or (hasattr(model, 'pipe') and hasattr(model.pipe, 'vae'))
@ -445,4 +446,5 @@ def set_pipeline_args(p, model, prompts:list, negative_prompts:list, prompts_2:t
else:
_args[k] = v
shared.state.end(argsid)
return _args

View File

@ -316,6 +316,7 @@ def vae_decode(latents, model, output_type='np', vae_type='Full', width=None, he
def vae_encode(image, model, vae_type='Full'): # pylint: disable=unused-variable
jobid = shared.state.begin('VAE Encode')
import torchvision.transforms.functional as f
if shared.state.interrupted or shared.state.skipped:
return []
@ -331,6 +332,7 @@ def vae_encode(image, model, vae_type='Full'): # pylint: disable=unused-variable
else:
latents = taesd_vae_encode(image=tensor)
devices.torch_gc()
shared.state.end(jobid)
return latents

View File

@ -1122,7 +1122,7 @@ def reload_model_weights(sd_model=None, info=None, op='model', force=False, revi
if checkpoint_info is None:
unload_model_weights(op=op)
return None
jobid = shared.state.begin('Load')
jobid = shared.state.begin('Load model')
if sd_model is None:
sd_model = model_data.sd_model if op == 'model' or op == 'dict' else model_data.sd_refiner
if sd_model is None: # previous model load failed

View File

@ -319,6 +319,7 @@ class StyleDatabase:
if seeds is None or not isinstance(prompts, list):
shared.log.error(f'Styles invalid seeds: {seeds}')
return prompts, negatives
jobid = shared.state.begin('Styles')
parsed_positive = []
parsed_negative = []
for i in range(len(prompts)):
@ -331,6 +332,7 @@ class StyleDatabase:
prompt = apply_styles_to_prompt(prompt, [self.find_style(x).negative_prompt for x in styles])
prompt = apply_wildcards_to_prompt(prompt, [self.find_style(x).wildcards for x in styles], seeds[i])
parsed_negative.append(prompt)
shared.state.end(jobid)
return parsed_positive, parsed_negative
def apply_styles_to_prompt(self, prompt, styles, wildcards:bool=True):

View File

@ -29,6 +29,7 @@ def save_video_atomic(images, filename, video_type: str = 'none', duration: floa
except Exception as e:
shared.log.error(f'Save video: cv2: {e}')
return
jobid = shared.state.begin('Save video')
os.makedirs(os.path.dirname(filename), exist_ok=True)
if video_type.lower() in ['gif', 'png']:
append = images.copy()
@ -56,6 +57,7 @@ def save_video_atomic(images, filename, video_type: str = 'none', duration: floa
video_writer.write(img)
size = os.path.getsize(filename)
shared.log.info(f'Save video: file="{filename}" frames={len(frames)} duration={duration} fourcc={fourcc} size={size}')
shared.state.end(jobid)
def save_video(p, images, filename = None, video_type: str = 'none', duration: float = 2.0, loop: bool = False, interpolate: int = 0, scale: float = 1.0, pad: int = 1, change: float = 0.3, sync: bool = False):

View File

@ -15,7 +15,7 @@ def load_model(selected: models_def.Model):
return ''
sd_models.unload_model_weights()
t0 = time.time()
jobid = shared.state.begin('Load')
jobid = shared.state.begin('Load model')
video_cache.apply_teacache_patch(selected.dit_cls)

View File

@ -78,6 +78,7 @@ def save_video(
size = pixels.element_size() * pixels.numel()
shared.log.debug(f'Video: video={mp4_video} export={mp4_frames} safetensors={mp4_sf} interpolate={mp4_interpolate}')
shared.log.debug(f'Video: encode={t} raw={size} latent={pixels.shape} fps={mp4_fps} codec={mp4_codec} ext={mp4_ext} options="{mp4_opt}"')
jobid = shared.state.begin('Save video')
try:
if stream is not None:
stream.output_queue.push(('progress', (None, 'Saving video...')))
@ -124,4 +125,5 @@ def save_video(
shared.log.error(f'Video save: raw={size} {e}')
errors.display(e, 'video')
timer.process.add('save', time.time()-t_save)
shared.state.end(jobid)
return t, output_video

View File

@ -10,6 +10,7 @@ debug = os.environ.get('SD_LOAD_DEBUG', None) is not None
def load_transformer(repo_id, cls_name, load_config={}, subfolder="transformer", allow_quant=True, variant=None, dtype=None, modules_to_not_convert=[], modules_dtype_dict={}):
transformer = None
jobid = shared.state.begin('Load DiT')
try:
load_args, quant_args = model_quant.get_dit_args(load_config, module='Model', device_map=True, allow_quant=allow_quant, modules_to_not_convert=modules_to_not_convert, modules_dtype_dict=modules_dtype_dict)
quant_type = model_quant.get_quant_type(quant_args)
@ -68,11 +69,13 @@ def load_transformer(repo_id, cls_name, load_config={}, subfolder="transformer",
errors.display(e, 'Load:')
raise
devices.torch_gc()
shared.state.end(jobid)
return transformer
def load_text_encoder(repo_id, cls_name, load_config={}, subfolder="text_encoder", allow_quant=True, allow_shared=True, variant=None, dtype=None, modules_to_not_convert=[], modules_dtype_dict={}):
text_encoder = None
jobid = shared.state.begin('Load TE')
try:
load_args, quant_args = model_quant.get_dit_args(load_config, module='TE', device_map=True, allow_quant=allow_quant, modules_to_not_convert=modules_to_not_convert, modules_dtype_dict=modules_dtype_dict)
quant_type = model_quant.get_quant_type(quant_args)
@ -159,4 +162,5 @@ def load_text_encoder(repo_id, cls_name, load_config={}, subfolder="text_encoder
errors.display(e, 'Load:')
raise
devices.torch_gc()
shared.state.end(jobid)
return text_encoder

View File

@ -155,7 +155,7 @@ def load_model():
if not shared.opts.sd_checkpoint_autoload and shared.cmd_opts.ckpt is None:
log.info('Model: autoload=False')
else:
jobid = shared.state.begin('Load')
jobid = shared.state.begin('Load model')
thread_model = Thread(target=lambda: shared.sd_model)
thread_model.start()
thread_refiner = Thread(target=lambda: shared.sd_refiner)