mirror of
https://github.com/vladmandic/automatic
synced 2026-08-25 22:20:46 +02:00
d38f2c97b2
Move the per-component placement walk from the api server into memstats next to ram and gpu stats, and add it to the --monitor tick so offload placement is visible in a log rather than only over the api. - guard the walk internally: the supervisor loop logs its monitor line unguarded, and the walk can race a reload or an offload rewrap - monitor reports gb and merges into a fresh dict, since memory_stats returns a module global that the per-generation log also prints - endpoint keeps reporting raw bytes
202 lines
8.3 KiB
Python
202 lines
8.3 KiB
Python
import os
|
|
import time
|
|
from pathlib import Path
|
|
from fastapi import Request, Depends, BackgroundTasks, Response
|
|
from fastapi.exceptions import HTTPException
|
|
from fastapi.responses import FileResponse
|
|
import installer
|
|
from modules import shared
|
|
from modules.logger import log
|
|
from modules.api import models, helpers
|
|
|
|
|
|
def get_js(request: Request):
|
|
file = request.query_params.get("file", None)
|
|
if (file is None) or (len(file) == 0):
|
|
raise HTTPException(status_code=400, detail="file parameter is required")
|
|
# Security: validate path is within allowed directories
|
|
if shared.demo is None:
|
|
raise HTTPException(status_code=503, detail="server not ready")
|
|
allowed_dirs = shared.demo.allowed_paths
|
|
if not any(Path(folder).absolute() in Path(file).absolute().parents for folder in allowed_dirs):
|
|
raise HTTPException(status_code=403, detail=f"file {file}: must be in one of allowed directories")
|
|
ext = file.split('.')[-1]
|
|
if ext not in ['js', 'css', 'map', 'html', 'wasm', 'ttf', 'mjs', 'json']:
|
|
raise HTTPException(status_code=400, detail=f"invalid file extension: {ext}")
|
|
if not os.path.exists(file):
|
|
log.error(f"API: file not found: {file}")
|
|
raise HTTPException(status_code=404, detail=f"file not found: {file}")
|
|
if ext in ['js', 'mjs']:
|
|
media_type = 'application/javascript'
|
|
elif ext in ['map', 'json']:
|
|
media_type = 'application/json'
|
|
elif ext in ['css']:
|
|
media_type = 'text/css'
|
|
elif ext in ['html']:
|
|
media_type = 'text/html'
|
|
elif ext in ['wasm']:
|
|
media_type = 'application/wasm'
|
|
elif ext in ['ttf']:
|
|
media_type = 'font/ttf'
|
|
else:
|
|
media_type = 'application/octet-stream'
|
|
return FileResponse(file, media_type=media_type)
|
|
|
|
def get_version():
|
|
return installer.get_version()
|
|
|
|
def get_icon():
|
|
icon_path = os.path.join(shared.script_path, "ui", "assets", "favicon.png")
|
|
return FileResponse(icon_path, media_type="image/png")
|
|
|
|
def get_manifest():
|
|
from modules import paths
|
|
manifest_path = os.path.join(paths.script_path, "ui", "manifest", "manifest.json")
|
|
log.debug(f"API manifest={manifest_path}")
|
|
return FileResponse(manifest_path, media_type="application/json")
|
|
|
|
def get_motd():
|
|
import requests
|
|
motd = ""
|
|
ver = get_version()
|
|
if ver.get("updated", None) is not None:
|
|
motd = f"version <b>{ver['commit']} {ver['updated']}</b> <span style='color: var(--primary-500)'>{ver['url'].split('/')[-1]}</span><br>" # pylint: disable=use-maxsplit-arg
|
|
if shared.opts.motd:
|
|
try:
|
|
res = requests.get("https://vladmandic.github.io/sdnext/motd", timeout=3)
|
|
if res.status_code == 200:
|
|
msg = (res.text or "").strip()
|
|
log.info(f"MOTD: {msg if len(msg) > 0 else 'N/A'}")
|
|
motd += res.text
|
|
else:
|
|
log.error(f"MOTD: {res.status_code}")
|
|
except Exception as err:
|
|
log.error(f"MOTD: {err}")
|
|
return motd
|
|
|
|
def get_platform():
|
|
from modules.loader import get_packages as loader_get_packages
|
|
return { **installer.get_platform(), **loader_get_packages() }
|
|
|
|
def get_torch():
|
|
return dict(installer.torch_info)
|
|
|
|
def get_log(req: models.ReqGetLog = Depends()):
|
|
lines = log.buffer[:req.lines] if req.lines > 0 else log.buffer.copy()
|
|
if req.clear:
|
|
log.buffer.clear()
|
|
return lines
|
|
|
|
def post_log(req: models.ReqPostLog):
|
|
if req.json is not None:
|
|
log.info(f'UI {req.message or ""}: {req.json}')
|
|
elif req.message is not None:
|
|
log.info(f'UI: {req.message}')
|
|
elif req.debug is not None:
|
|
log.debug(f'UI: {req.debug}')
|
|
elif req.error is not None:
|
|
log.error(f'UI: {req.error}')
|
|
return Response(status_code=204)
|
|
|
|
def post_shutdown(background_tasks: BackgroundTasks):
|
|
log.info("Server shutdown request received")
|
|
background_tasks.add_task(os._exit, 0)
|
|
return Response(status_code=204)
|
|
|
|
def post_restart(background_tasks: BackgroundTasks):
|
|
log.info("Server restart request received")
|
|
from installer import restart
|
|
background_tasks.add_task(restart)
|
|
return Response(status_code=204)
|
|
|
|
def get_cmd_flags():
|
|
return vars(shared.cmd_opts)
|
|
|
|
def get_history(req: models.ReqHistory = Depends()):
|
|
if req.id is not None and ((isinstance(req.id, str) and len(req.id) > 0) or isinstance(req.id, int)):
|
|
_id = str(req.id) if isinstance(req.id, int) else req.id
|
|
res = [item for item in shared.state.state_history if item['id'] == _id]
|
|
else:
|
|
res = shared.state.state_history
|
|
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)
|
|
shared.state.do_set_current_image()
|
|
current_image = None
|
|
if shared.state.current_image and not req.skip_current_image:
|
|
current_image = helpers.encode_pil_to_base64(shared.state.current_image)
|
|
batch_x = max(shared.state.job_no, 0)
|
|
batch_y = max(shared.state.job_count, 1)
|
|
step_x = max(shared.state.sampling_step, 0)
|
|
prev_steps = max(shared.state.sampling_steps, 1)
|
|
while step_x > shared.state.sampling_steps:
|
|
shared.state.sampling_steps += prev_steps
|
|
step_y = max(shared.state.sampling_steps, 1)
|
|
current = step_y * batch_x + step_x
|
|
total = step_y * batch_y
|
|
progress = min((current / total) if current > 0 and total > 0 else 0, 1)
|
|
time_since_start = time.time() - shared.state.time_start
|
|
eta_relative = (time_since_start / progress) - time_since_start if progress > 0 else 0
|
|
# log.trace(f'get_progress: batch {batch_x}/{batch_y} step {step_x}/{step_y} current {current}/{total} time={time_since_start} eta={eta_relative}')
|
|
# log.trace(shared.state)
|
|
res = models.ResProgress(id=shared.state.id, progress=round(progress, 2), eta_relative=round(eta_relative, 2), current_image=current_image, textinfo=shared.state.textinfo, state=shared.state.dict(), )
|
|
return res
|
|
|
|
def get_status():
|
|
return shared.state.status()
|
|
|
|
def post_interrupt():
|
|
shared.state.interrupt()
|
|
return Response(status_code=204)
|
|
|
|
def post_skip():
|
|
shared.state.skip()
|
|
return Response(status_code=204)
|
|
|
|
def get_memory():
|
|
try:
|
|
import psutil
|
|
process = psutil.Process(os.getpid())
|
|
res = process.memory_info() # only rss is cross-platform guaranteed so we dont rely on other values
|
|
ram_total = 100 * res.rss / process.memory_percent() # and total memory is calculated as actual value is not cross-platform safe
|
|
ram = { 'free': ram_total - res.rss, 'used': res.rss, 'total': ram_total }
|
|
except Exception as err:
|
|
ram = { 'error': f'{err}' }
|
|
try:
|
|
import torch
|
|
if torch.cuda.is_available():
|
|
s = torch.cuda.mem_get_info()
|
|
system = { 'free': s[0], 'used': s[1] - s[0], 'total': s[1] }
|
|
s = dict(torch.cuda.memory_stats(shared.device))
|
|
allocated = { 'current': s['allocated_bytes.all.current'], 'peak': s['allocated_bytes.all.peak'] }
|
|
reserved = { 'current': s['reserved_bytes.all.current'], 'peak': s['reserved_bytes.all.peak'] }
|
|
active = { 'current': s['active_bytes.all.current'], 'peak': s['active_bytes.all.peak'] }
|
|
inactive = { 'current': s['inactive_split_bytes.all.current'], 'peak': s['inactive_split_bytes.all.peak'] }
|
|
warnings = { 'retries': s['num_alloc_retries'], 'oom': s['num_ooms'] }
|
|
cuda = {
|
|
'system': system,
|
|
'active': active,
|
|
'allocated': allocated,
|
|
'reserved': reserved,
|
|
'inactive': inactive,
|
|
'events': warnings,
|
|
}
|
|
else:
|
|
cuda = { 'error': 'unavailable' }
|
|
except Exception as err:
|
|
cuda = { 'error': f'{err}' }
|
|
from modules import memstats
|
|
return models.ResMemory(ram = ram, cuda = cuda, model = memstats.model_stats())
|