Delete api.py
This commit is contained in:
@@ -1,988 +0,0 @@
|
||||
import os
|
||||
import sys
|
||||
import json
|
||||
import urllib.parse
|
||||
import subprocess
|
||||
import threading
|
||||
from aiohttp import web
|
||||
import folder_paths
|
||||
|
||||
def get_metadata(file_path):
|
||||
base_path = os.path.splitext(file_path)[0]
|
||||
metadata = {
|
||||
"name": os.path.basename(base_path),
|
||||
"description": "",
|
||||
"notes": "",
|
||||
"trainedWords": [],
|
||||
"baseModel": "",
|
||||
"civitai_url": "",
|
||||
"hash": ""
|
||||
}
|
||||
|
||||
info_files = [f"{base_path}.civitai.info", f"{base_path}.info"]
|
||||
for info_file in info_files:
|
||||
if os.path.exists(info_file):
|
||||
try:
|
||||
with open(info_file, 'r', encoding='utf-8') as f:
|
||||
data = json.load(f)
|
||||
|
||||
name = data.get("name", "")
|
||||
if "model" in data and isinstance(data["model"], dict):
|
||||
model_name = data["model"].get("name", "")
|
||||
name = f"{model_name} - {name}".strip(' -')
|
||||
|
||||
description = data.get("description", "") or ""
|
||||
if not description and "model" in data and isinstance(data["model"], dict):
|
||||
description = data["model"].get("description", "")
|
||||
notes = data.get("notes", "")
|
||||
trained_words = data.get("trainedWords", [])
|
||||
base_model = data.get("baseModel", "")
|
||||
|
||||
model_id = data.get("modelId", "")
|
||||
version_id = data.get("id", "")
|
||||
civitai_url = ""
|
||||
if model_id:
|
||||
# Handle Civitai's new mature content policy
|
||||
nsfw_level = data.get("nsfwLevel", 1)
|
||||
is_nsfw = False
|
||||
if "model" in data and isinstance(data["model"], dict):
|
||||
is_nsfw = data["model"].get("nsfw", False)
|
||||
|
||||
domain = "civitai.red" if (nsfw_level > 1 or is_nsfw) else "civitai.com"
|
||||
civitai_url = f"https://{domain}/models/{model_id}"
|
||||
if version_id:
|
||||
civitai_url += f"?modelVersionId={version_id}"
|
||||
|
||||
|
||||
hash_val = ""
|
||||
for file_info in data.get("files", []):
|
||||
if isinstance(file_info, dict):
|
||||
hashes = file_info.get("hashes", {})
|
||||
if isinstance(hashes, dict) and "SHA256" in hashes:
|
||||
hash_val = hashes["SHA256"]
|
||||
break
|
||||
|
||||
metadata["name"] = name if name else metadata["name"]
|
||||
metadata["description"] = description
|
||||
metadata["notes"] = notes
|
||||
metadata["trainedWords"] = trained_words
|
||||
metadata["baseModel"] = base_model
|
||||
metadata["civitai_url"] = civitai_url
|
||||
metadata["hash"] = hash_val
|
||||
break
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
return metadata
|
||||
|
||||
|
||||
async def api_get_folders(request):
|
||||
types = ['checkpoints', 'loras', 'unet', 'diffusion_models']
|
||||
result = []
|
||||
seen_dirs = set()
|
||||
|
||||
for t in types:
|
||||
try:
|
||||
paths = folder_paths.get_folder_paths(t)
|
||||
except Exception:
|
||||
continue
|
||||
if not paths:
|
||||
continue
|
||||
|
||||
for path_idx, base_dir in enumerate(paths):
|
||||
if not os.path.exists(base_dir):
|
||||
continue
|
||||
real_path = os.path.realpath(base_dir)
|
||||
if real_path in seen_dirs:
|
||||
continue
|
||||
seen_dirs.add(real_path)
|
||||
|
||||
tree = {}
|
||||
for root, dirs, files in os.walk(base_dir):
|
||||
has_models = any(f.endswith(('.safetensors', '.ckpt', '.pt')) for f in files)
|
||||
rel = os.path.relpath(root, base_dir)
|
||||
if rel == '.':
|
||||
rel = '/'
|
||||
else:
|
||||
rel = '/' + rel.replace('\\', '/')
|
||||
tree[rel] = {
|
||||
"path": rel,
|
||||
"name": os.path.basename(root) if rel != '/' else '[Root]',
|
||||
"has_models": has_models,
|
||||
"model_count": sum(1 for f in files if f.endswith(('.safetensors', '.ckpt', '.pt')))
|
||||
}
|
||||
|
||||
label = t.capitalize()
|
||||
if path_idx > 0:
|
||||
label += f" ({os.path.basename(base_dir)})"
|
||||
|
||||
result.append({
|
||||
"type": t,
|
||||
"path_idx": path_idx,
|
||||
"label": label,
|
||||
"folders": tree
|
||||
})
|
||||
|
||||
return web.json_response({"folders": result})
|
||||
|
||||
|
||||
async def api_get_models(request):
|
||||
folder_type = request.query.get('type', 'checkpoints')
|
||||
subfolder = request.query.get('subfolder', '/')
|
||||
try:
|
||||
path_idx = int(request.query.get('path_idx', 0))
|
||||
except:
|
||||
path_idx = 0
|
||||
|
||||
try:
|
||||
paths = folder_paths.get_folder_paths(folder_type)
|
||||
except Exception:
|
||||
return web.json_response({"models": []})
|
||||
if not paths or path_idx >= len(paths):
|
||||
return web.json_response({"models": []})
|
||||
|
||||
if '..' in subfolder:
|
||||
return web.Response(status=400, text='Invalid subfolder')
|
||||
|
||||
base_dir = paths[path_idx]
|
||||
if subfolder == '/':
|
||||
target_dir = base_dir
|
||||
rel_subfolder = ""
|
||||
else:
|
||||
rel_subfolder = subfolder.strip('/')
|
||||
target_dir = os.path.join(base_dir, rel_subfolder)
|
||||
|
||||
if not os.path.exists(target_dir):
|
||||
return web.json_response({"models": []})
|
||||
|
||||
models = []
|
||||
try:
|
||||
entries = os.listdir(target_dir)
|
||||
except Exception:
|
||||
return web.json_response({"models": []})
|
||||
|
||||
for f in sorted(entries):
|
||||
if f.endswith('.safetensors') or f.endswith('.ckpt') or f.endswith('.pt'):
|
||||
file_path = os.path.join(target_dir, f)
|
||||
if not os.path.isfile(file_path):
|
||||
continue
|
||||
|
||||
meta = get_metadata(file_path)
|
||||
|
||||
base_name = os.path.splitext(f)[0]
|
||||
preview_file = None
|
||||
for ext in ['.preview.png', '.png', '.jpg', '.jpeg', '.webp', '.mp4', '.webm']:
|
||||
if os.path.exists(os.path.join(target_dir, base_name + ext)):
|
||||
preview_file = base_name + ext
|
||||
break
|
||||
|
||||
preview_url = ""
|
||||
if preview_file:
|
||||
q_type = urllib.parse.quote(folder_type)
|
||||
q_idx = str(path_idx)
|
||||
q_sub = urllib.parse.quote(rel_subfolder)
|
||||
q_file = urllib.parse.quote(preview_file)
|
||||
preview_url = f"/anomalous/image?type={q_type}&path_idx={q_idx}&subfolder={q_sub}&filename={q_file}"
|
||||
|
||||
try:
|
||||
size_bytes = os.path.getsize(file_path)
|
||||
size_mb = round(size_bytes / (1024 * 1024), 1)
|
||||
except Exception:
|
||||
size_mb = 0
|
||||
size_bytes = 0
|
||||
|
||||
models.append({
|
||||
"filename": f,
|
||||
"size_mb": size_mb,
|
||||
"size_bytes": size_bytes,
|
||||
"metadata": meta,
|
||||
"preview_url": preview_url
|
||||
})
|
||||
|
||||
return web.json_response({"models": models})
|
||||
|
||||
|
||||
async def api_serve_image(request):
|
||||
"""Dedicated image serving endpoint for model preview images."""
|
||||
folder_type = request.query.get('type', 'checkpoints')
|
||||
try:
|
||||
path_idx = int(request.query.get('path_idx', 0))
|
||||
except:
|
||||
path_idx = 0
|
||||
subfolder = request.query.get('subfolder', '')
|
||||
filename = request.query.get('filename', '')
|
||||
|
||||
if not filename or '..' in filename or '..' in subfolder:
|
||||
return web.Response(status=400, text='Invalid request')
|
||||
|
||||
try:
|
||||
paths = folder_paths.get_folder_paths(folder_type)
|
||||
except Exception:
|
||||
return web.Response(status=404, text='Folder type not found')
|
||||
if not paths or path_idx >= len(paths):
|
||||
return web.Response(status=404, text='Folder type not found')
|
||||
|
||||
base_dir = paths[path_idx]
|
||||
if subfolder:
|
||||
file_path = os.path.join(base_dir, subfolder, filename)
|
||||
else:
|
||||
file_path = os.path.join(base_dir, filename)
|
||||
|
||||
if not os.path.exists(file_path) or not os.path.isfile(file_path):
|
||||
return web.Response(status=404, text='Image not found')
|
||||
|
||||
ext = os.path.splitext(filename)[1].lower()
|
||||
content_types = {
|
||||
'.png': 'image/png',
|
||||
'.jpg': 'image/jpeg',
|
||||
'.jpeg': 'image/jpeg',
|
||||
'.webp': 'image/webp',
|
||||
'.gif': 'image/gif'
|
||||
}
|
||||
content_type = content_types.get(ext, 'application/octet-stream')
|
||||
|
||||
return web.FileResponse(file_path, headers={'Content-Type': content_type})
|
||||
|
||||
|
||||
|
||||
async def api_scan_status(request):
|
||||
folder_type = request.query.get('type', 'checkpoints')
|
||||
subfolder = request.query.get('subfolder', '/')
|
||||
try:
|
||||
path_idx = int(request.query.get('path_idx', 0))
|
||||
except:
|
||||
path_idx = 0
|
||||
try:
|
||||
paths = folder_paths.get_folder_paths(folder_type)
|
||||
except Exception:
|
||||
return web.json_response({"scanning": False})
|
||||
if not paths or path_idx >= len(paths) or '..' in subfolder:
|
||||
return web.json_response({"scanning": False})
|
||||
|
||||
base_dir = paths[path_idx]
|
||||
if subfolder == '/':
|
||||
target_dir = base_dir
|
||||
else:
|
||||
target_dir = os.path.join(base_dir, subfolder.strip('/'))
|
||||
|
||||
marker_file = os.path.join(target_dir, '.scan_in_progress')
|
||||
result_file = os.path.join(target_dir, '.scan_result.json')
|
||||
scanning = os.path.exists(marker_file)
|
||||
|
||||
data = {"scanning": scanning}
|
||||
if not scanning and os.path.exists(result_file):
|
||||
try:
|
||||
with open(result_file, 'r', encoding='utf-8') as f:
|
||||
data["result"] = __import__('json').load(f)
|
||||
os.remove(result_file)
|
||||
except:
|
||||
pass
|
||||
|
||||
return web.json_response(data)
|
||||
|
||||
async def api_scan_folder(request):
|
||||
"""Launches the scraper in the background for a specific directory."""
|
||||
folder_type = request.query.get('type', 'checkpoints')
|
||||
subfolder = request.query.get('subfolder', '/')
|
||||
try:
|
||||
path_idx = int(request.query.get('path_idx', 0))
|
||||
except:
|
||||
path_idx = 0
|
||||
|
||||
if '..' in subfolder:
|
||||
return web.json_response({"status": "error", "message": "Invalid subfolder"})
|
||||
|
||||
try:
|
||||
paths = folder_paths.get_folder_paths(folder_type)
|
||||
except Exception:
|
||||
return web.json_response({"status": "error", "message": "Invalid folder type"})
|
||||
|
||||
if not paths or path_idx >= len(paths):
|
||||
return web.json_response({"status": "error", "message": "Invalid path index"})
|
||||
|
||||
base_dir = paths[path_idx]
|
||||
if subfolder == '/':
|
||||
target_dir = base_dir
|
||||
else:
|
||||
target_dir = os.path.join(base_dir, subfolder.strip('/'))
|
||||
|
||||
if not os.path.exists(target_dir):
|
||||
return web.json_response({"status": "error", "message": "Directory does not exist"})
|
||||
|
||||
# Get the scraper path relative to this script
|
||||
script_dir = os.path.dirname(os.path.abspath(__file__))
|
||||
scraper_path = os.path.join(script_dir, "scraper.py")
|
||||
|
||||
if not os.path.exists(scraper_path):
|
||||
return web.json_response({"status": "error", "message": "scraper.py not found in extension directory"})
|
||||
|
||||
print(f"[Anomalous Browser] Starting background scan for: {target_dir}")
|
||||
|
||||
try:
|
||||
marker_file = os.path.join(target_dir, '.scan_in_progress')
|
||||
with open(marker_file, 'w') as f: f.write('1')
|
||||
def run_bg():
|
||||
try:
|
||||
subprocess.run(
|
||||
[sys.executable, scraper_path, target_dir],
|
||||
cwd=script_dir,
|
||||
stdout=subprocess.DEVNULL,
|
||||
stderr=subprocess.DEVNULL
|
||||
)
|
||||
finally:
|
||||
if hasattr(folder_paths, "filename_list_cache"):
|
||||
folder_paths.filename_list_cache.clear()
|
||||
if hasattr(folder_paths, "cache_helper") and hasattr(folder_paths.cache_helper, "clear"):
|
||||
folder_paths.cache_helper.clear()
|
||||
|
||||
if os.path.exists(marker_file):
|
||||
try: os.remove(marker_file)
|
||||
except: pass
|
||||
|
||||
threading.Thread(target=run_bg, daemon=True).start()
|
||||
|
||||
return web.json_response({"status": "ok", "message": "Scan started in background. Check console for details."})
|
||||
except Exception as e:
|
||||
return web.json_response({"status": "error", "message": str(e)})
|
||||
|
||||
async def api_scan_all(request):
|
||||
script_dir = os.path.dirname(os.path.abspath(__file__))
|
||||
scraper_path = os.path.join(script_dir, "scraper.py")
|
||||
marker_file = os.path.join(script_dir, '.global_scan_in_progress')
|
||||
|
||||
if os.path.exists(marker_file):
|
||||
return web.json_response({"status": "error", "message": "Global scan already in progress"})
|
||||
|
||||
try:
|
||||
with open(marker_file, 'w') as f: f.write('1')
|
||||
|
||||
def run_global_bg():
|
||||
try:
|
||||
types = ['checkpoints', 'loras', 'unet', 'diffusion_models']
|
||||
for t in types:
|
||||
try:
|
||||
paths = folder_paths.get_folder_paths(t)
|
||||
if not paths: continue
|
||||
for base_dir in paths:
|
||||
if not os.path.exists(base_dir): continue
|
||||
print(f"[Anomalous Browser] Global scan processing: {base_dir}")
|
||||
subprocess.run(
|
||||
[sys.executable, scraper_path, base_dir, "--skip-rename", "--skip-media"],
|
||||
cwd=script_dir,
|
||||
stdout=subprocess.DEVNULL,
|
||||
stderr=subprocess.DEVNULL
|
||||
)
|
||||
except Exception as e:
|
||||
print(f"[Anomalous Browser] Global scan error on {t}: {e}")
|
||||
finally:
|
||||
if hasattr(folder_paths, "filename_list_cache"):
|
||||
folder_paths.filename_list_cache.clear()
|
||||
if hasattr(folder_paths, "cache_helper") and hasattr(folder_paths.cache_helper, "clear"):
|
||||
folder_paths.cache_helper.clear()
|
||||
|
||||
if os.path.exists(marker_file):
|
||||
try: os.remove(marker_file)
|
||||
except: pass
|
||||
|
||||
threading.Thread(target=run_global_bg, daemon=True).start()
|
||||
return web.json_response({"status": "ok", "message": "Global scan started"})
|
||||
except Exception as e:
|
||||
return web.json_response({"status": "error", "message": str(e)})
|
||||
|
||||
async def api_global_scan_status(request):
|
||||
script_dir = os.path.dirname(os.path.abspath(__file__))
|
||||
marker_file = os.path.join(script_dir, '.global_scan_in_progress')
|
||||
return web.json_response({"scanning": os.path.exists(marker_file)})
|
||||
async def api_clear_cache(request):
|
||||
try:
|
||||
if hasattr(folder_paths, "filename_list_cache"):
|
||||
folder_paths.filename_list_cache.clear()
|
||||
if hasattr(folder_paths, "cache_helper") and hasattr(folder_paths.cache_helper, "clear"):
|
||||
folder_paths.cache_helper.clear()
|
||||
return web.json_response({"status": "success"})
|
||||
except Exception as e:
|
||||
return web.json_response({"status": "error", "message": str(e)})
|
||||
|
||||
async def api_get_config(request):
|
||||
script_dir = os.path.dirname(os.path.abspath(__file__))
|
||||
config_path = os.path.join(script_dir, "config.json")
|
||||
has_key = False
|
||||
if os.path.exists(config_path):
|
||||
try:
|
||||
with open(config_path, 'r', encoding='utf-8') as f:
|
||||
cfg = json.load(f)
|
||||
has_key = bool(cfg.get("CIVITAI_API_KEY", "").strip())
|
||||
except:
|
||||
pass
|
||||
return web.json_response({"has_api_key": has_key})
|
||||
|
||||
async def api_save_config(request):
|
||||
try:
|
||||
data = await request.json()
|
||||
api_key = data.get("api_key", "").strip()
|
||||
script_dir = os.path.dirname(os.path.abspath(__file__))
|
||||
config_path = os.path.join(script_dir, "config.json")
|
||||
|
||||
cfg = {}
|
||||
if os.path.exists(config_path):
|
||||
try:
|
||||
with open(config_path, 'r', encoding='utf-8') as f:
|
||||
cfg = json.load(f)
|
||||
except:
|
||||
pass
|
||||
|
||||
cfg["CIVITAI_API_KEY"] = api_key
|
||||
with open(config_path, 'w', encoding='utf-8') as f:
|
||||
json.dump(cfg, f, indent=4)
|
||||
|
||||
return web.json_response({"status": "ok"})
|
||||
except Exception as e:
|
||||
return web.json_response({"status": "error", "message": str(e)})
|
||||
|
||||
|
||||
async def api_delete_model(request):
|
||||
try:
|
||||
data = await request.json()
|
||||
folder_type = data.get('type', 'checkpoints')
|
||||
subfolder = data.get('subfolder', '/')
|
||||
filename = data.get('filename', '')
|
||||
try:
|
||||
path_idx = int(data.get('path_idx', 0))
|
||||
except:
|
||||
path_idx = 0
|
||||
|
||||
if not filename or '..' in filename or '..' in subfolder:
|
||||
return web.json_response({"status": "error", "message": "Invalid request parameters"})
|
||||
|
||||
try:
|
||||
paths = folder_paths.get_folder_paths(folder_type)
|
||||
except Exception:
|
||||
return web.json_response({"status": "error", "message": "Invalid folder type"})
|
||||
|
||||
if not paths or path_idx >= len(paths):
|
||||
return web.json_response({"status": "error", "message": "Invalid path index"})
|
||||
|
||||
base_dir = paths[path_idx]
|
||||
if subfolder == '/':
|
||||
target_dir = base_dir
|
||||
else:
|
||||
target_dir = os.path.join(base_dir, subfolder.strip('/'))
|
||||
|
||||
model_path = os.path.join(target_dir, filename)
|
||||
if not os.path.exists(model_path):
|
||||
return web.json_response({"status": "error", "message": "Model file not found"})
|
||||
|
||||
# 1. 优先尝试删除你点击的主模型文件
|
||||
try:
|
||||
os.remove(model_path)
|
||||
except Exception as e:
|
||||
error_msg = str(e)
|
||||
if "being used" in error_msg or "WinError 32" in error_msg or "Permission" in error_msg:
|
||||
error_msg = "文件被占用 (正在被 ComfyUI 使用)。请先重启 ComfyUI 或在工作流中卸载该模型后再删除!"
|
||||
return web.json_response({"status": "error", "message": f"主模型删除失败: {error_msg}"})
|
||||
|
||||
base_name = os.path.splitext(filename)[0]
|
||||
|
||||
# 2. 主模型成功删除后,再清理配套的垃圾文件
|
||||
associated_exts = [
|
||||
'.safetensors', '.ckpt', '.pt', '.bin',
|
||||
'.info', '.civitai.info',
|
||||
'.png', '.preview.png', '.jpg', '.jpeg', '.webp', '.gif',
|
||||
'.json', '.txt', '.yaml'
|
||||
]
|
||||
|
||||
deleted_files = [filename]
|
||||
for ext in associated_exts:
|
||||
file_to_del = os.path.join(target_dir, base_name + ext)
|
||||
if file_to_del == model_path:
|
||||
continue
|
||||
if os.path.exists(file_to_del) and os.path.isfile(file_to_del):
|
||||
try:
|
||||
os.remove(file_to_del)
|
||||
deleted_files.append(base_name + ext)
|
||||
except Exception as e:
|
||||
print(f"[Anomalous Browser] Warning: Failed to delete {file_to_del}: {e}")
|
||||
|
||||
# 3. 修正:前端期待的成功状态是 "success" 而不是 "ok"
|
||||
return web.json_response({"status": "success", "deleted": deleted_files})
|
||||
|
||||
except Exception as e:
|
||||
import traceback
|
||||
traceback.print_exc()
|
||||
return web.json_response({"status": "error", "message": str(e)}, status=500)
|
||||
|
||||
async def api_clean_civitai_info(request):
|
||||
try:
|
||||
types = ['checkpoints', 'loras', 'unet', 'diffusion_models']
|
||||
deleted_count = 0
|
||||
for t in types:
|
||||
try:
|
||||
paths = folder_paths.get_folder_paths(t)
|
||||
except Exception:
|
||||
continue
|
||||
if not paths: continue
|
||||
|
||||
for base_dir in paths:
|
||||
if not os.path.exists(base_dir): continue
|
||||
for root, dirs, files in os.walk(base_dir):
|
||||
for file in files:
|
||||
if file.endswith('.civitai.info'):
|
||||
file_path = os.path.join(root, file)
|
||||
try:
|
||||
os.remove(file_path)
|
||||
deleted_count += 1
|
||||
except Exception as e:
|
||||
pass
|
||||
|
||||
return web.json_response({"status": "success", "count": deleted_count})
|
||||
except Exception as e:
|
||||
return web.json_response({"status": "error", "message": str(e)}, status=500)
|
||||
|
||||
async def api_compatible_models(request):
|
||||
base_model = request.query.get('base_model', '')
|
||||
target_type = request.query.get('target_type', 'loras')
|
||||
|
||||
if not base_model:
|
||||
return web.json_response({"models": []})
|
||||
|
||||
target_types = [t.strip() for t in target_type.split(',')]
|
||||
compatible_models = []
|
||||
seen_files = set()
|
||||
|
||||
for t in target_types:
|
||||
try:
|
||||
paths = folder_paths.get_folder_paths(t)
|
||||
except Exception:
|
||||
continue
|
||||
|
||||
if not paths:
|
||||
continue
|
||||
|
||||
for path_idx, base_dir in enumerate(paths):
|
||||
if not os.path.exists(base_dir):
|
||||
continue
|
||||
|
||||
for root, _, files in os.walk(base_dir):
|
||||
for f in files:
|
||||
if f.endswith('.safetensors') or f.endswith('.ckpt') or f.endswith('.pt'):
|
||||
file_path = os.path.join(root, f)
|
||||
real_path = os.path.realpath(file_path)
|
||||
if real_path in seen_files:
|
||||
continue
|
||||
seen_files.add(real_path)
|
||||
|
||||
meta = get_metadata(file_path)
|
||||
m_bm = str(meta.get("baseModel", "")).strip().lower().replace(" ", "")
|
||||
req_bm = str(base_model).strip().lower().replace(" ", "")
|
||||
|
||||
if req_bm and m_bm and (req_bm in m_bm or m_bm in req_bm):
|
||||
rel_subfolder = os.path.relpath(root, base_dir)
|
||||
if rel_subfolder == '.':
|
||||
rel_subfolder = '/'
|
||||
else:
|
||||
rel_subfolder = '/' + rel_subfolder.replace('\\', '/')
|
||||
|
||||
base_name = os.path.splitext(f)[0]
|
||||
preview_file = None
|
||||
for ext in ['.preview.png', '.png', '.jpg', '.jpeg', '.webp', '.mp4', '.webm']:
|
||||
if os.path.exists(os.path.join(root, base_name + ext)):
|
||||
preview_file = base_name + ext
|
||||
break
|
||||
|
||||
preview_url = ""
|
||||
if preview_file:
|
||||
q_type = urllib.parse.quote(t)
|
||||
q_idx = str(path_idx)
|
||||
q_sub = urllib.parse.quote(rel_subfolder.strip('/')) if rel_subfolder != '/' else ""
|
||||
q_file = urllib.parse.quote(preview_file)
|
||||
preview_url = f"/anomalous/image?type={q_type}&path_idx={q_idx}&subfolder={q_sub}&filename={q_file}"
|
||||
|
||||
try:
|
||||
size_bytes = os.path.getsize(file_path)
|
||||
size_mb = round(size_bytes / (1024 * 1024), 1)
|
||||
except Exception:
|
||||
size_mb = 0
|
||||
size_bytes = 0
|
||||
|
||||
compatible_models.append({
|
||||
"type": t,
|
||||
"path_idx": path_idx,
|
||||
"subfolder": rel_subfolder,
|
||||
"filename": f,
|
||||
"size_mb": size_mb,
|
||||
"size_bytes": size_bytes,
|
||||
"preview_url": preview_url,
|
||||
"metadata": meta
|
||||
})
|
||||
|
||||
return web.json_response({"models": compatible_models})
|
||||
|
||||
async def api_get_notebooks(request):
|
||||
script_dir = os.path.dirname(os.path.abspath(__file__))
|
||||
nb_dir = os.path.join(script_dir, "notebooks")
|
||||
if not os.path.exists(nb_dir):
|
||||
os.makedirs(nb_dir)
|
||||
|
||||
notebooks = []
|
||||
for f in os.listdir(nb_dir):
|
||||
if f.endswith('.json'):
|
||||
try:
|
||||
with open(os.path.join(nb_dir, f), 'r', encoding='utf-8') as file:
|
||||
data = json.load(file)
|
||||
notebooks.append({
|
||||
"filename": f,
|
||||
"name": data.get("name", f.replace('.json', '')),
|
||||
"data": data
|
||||
})
|
||||
except Exception:
|
||||
pass
|
||||
return web.json_response({"notebooks": notebooks})
|
||||
|
||||
async def api_save_notebook(request):
|
||||
try:
|
||||
data = await request.json()
|
||||
filename = data.get("filename", "")
|
||||
if not filename or '..' in filename:
|
||||
return web.json_response({"status": "error", "message": "Invalid filename"})
|
||||
if not filename.endswith('.json'):
|
||||
filename += '.json'
|
||||
|
||||
script_dir = os.path.dirname(os.path.abspath(__file__))
|
||||
nb_dir = os.path.join(script_dir, "notebooks")
|
||||
if not os.path.exists(nb_dir):
|
||||
os.makedirs(nb_dir)
|
||||
|
||||
file_path = os.path.join(nb_dir, filename)
|
||||
with open(file_path, 'w', encoding='utf-8') as f:
|
||||
json.dump(data.get("data", {}), f, indent=4, ensure_ascii=False)
|
||||
|
||||
return web.json_response({"status": "success"})
|
||||
except Exception as e:
|
||||
return web.json_response({"status": "error", "message": str(e)})
|
||||
|
||||
async def api_delete_notebook(request):
|
||||
try:
|
||||
data = await request.json()
|
||||
filename = data.get("filename", "")
|
||||
if not filename or '..' in filename:
|
||||
return web.json_response({"status": "error", "message": "Invalid filename"})
|
||||
|
||||
script_dir = os.path.dirname(os.path.abspath(__file__))
|
||||
nb_dir = os.path.join(script_dir, "notebooks")
|
||||
file_path = os.path.join(nb_dir, filename)
|
||||
|
||||
if os.path.exists(file_path):
|
||||
os.remove(file_path)
|
||||
return web.json_response({"status": "success"})
|
||||
return web.json_response({"status": "error", "message": "File not found"})
|
||||
except Exception as e:
|
||||
return web.json_response({"status": "error", "message": str(e)})
|
||||
|
||||
async def api_translate(request):
|
||||
try:
|
||||
data = await request.json()
|
||||
text = data.get("text", "")
|
||||
tl = data.get("target_lang", "zh-CN")
|
||||
if not text:
|
||||
return web.json_response({"translated": ""})
|
||||
|
||||
# Try DeepL if API key exists
|
||||
script_dir = os.path.dirname(os.path.abspath(__file__))
|
||||
config_path = os.path.join(script_dir, "config.json")
|
||||
deepl_key = ""
|
||||
if os.path.exists(config_path):
|
||||
try:
|
||||
with open(config_path, 'r', encoding='utf-8') as f:
|
||||
cfg = json.load(f)
|
||||
deepl_key = cfg.get("DEEPL_API_KEY", "").strip()
|
||||
except:
|
||||
pass
|
||||
|
||||
if deepl_key:
|
||||
deepl_map = { "zh-CN": "ZH", "en": "EN", "ja": "JA", "ko": "KO", "fr": "FR", "de": "DE", "es": "ES", "ru": "RU" }
|
||||
d_tl = deepl_map.get(tl, "EN")
|
||||
url = "https://api-free.deepl.com/v2/translate" if ":fx" in deepl_key else "https://api.deepl.com/v2/translate"
|
||||
payload = urllib.parse.urlencode({
|
||||
"auth_key": deepl_key,
|
||||
"text": text,
|
||||
"target_lang": d_tl
|
||||
}).encode('utf-8')
|
||||
req = urllib.request.Request(url, data=payload)
|
||||
with urllib.request.urlopen(req, timeout=5) as resp:
|
||||
result = json.loads(resp.read().decode('utf-8'))
|
||||
return web.json_response({"translated": result["translations"][0]["text"]})
|
||||
else:
|
||||
# Fallback to Google Translate (free, no key)
|
||||
url = f"https://translate.googleapis.com/translate_a/single?client=gtx&sl=auto&tl={tl}&dt=t&q={urllib.parse.quote(text)}"
|
||||
req = urllib.request.Request(url, headers={'User-Agent': 'Mozilla/5.0'})
|
||||
with urllib.request.urlopen(req, timeout=5) as resp:
|
||||
result = json.loads(resp.read().decode('utf-8'))
|
||||
# Google Translate returns a list of fragments
|
||||
translated_text = "".join([part[0] for part in result[0]])
|
||||
return web.json_response({"translated": translated_text})
|
||||
|
||||
except Exception as e:
|
||||
print(f"Translate Error: {e}")
|
||||
return web.json_response({"translated": text, "error": str(e)})
|
||||
|
||||
|
||||
async def api_base_models(request):
|
||||
target_types = ['checkpoints', 'unet', 'diffusion_models', 'loras']
|
||||
base_models = set()
|
||||
seen_files = set()
|
||||
|
||||
for t in target_types:
|
||||
try:
|
||||
paths = folder_paths.get_folder_paths(t)
|
||||
except Exception:
|
||||
continue
|
||||
if not paths: continue
|
||||
|
||||
for base_dir in paths:
|
||||
if not os.path.exists(base_dir): continue
|
||||
for root, _, files in os.walk(base_dir):
|
||||
for f in files:
|
||||
if f.endswith('.safetensors') or f.endswith('.ckpt') or f.endswith('.pt'):
|
||||
file_path = os.path.join(root, f)
|
||||
real_path = os.path.realpath(file_path)
|
||||
if real_path in seen_files: continue
|
||||
seen_files.add(real_path)
|
||||
|
||||
meta = get_metadata(file_path)
|
||||
m_bm = meta.get("baseModel", "")
|
||||
if m_bm and str(m_bm).strip():
|
||||
# Remove typical generic strings that might pollute
|
||||
clean_bm = str(m_bm).strip()
|
||||
base_models.add(clean_bm)
|
||||
|
||||
return web.json_response({"base_models": sorted(list(base_models))})
|
||||
|
||||
async def api_get_gallery_images(request):
|
||||
try:
|
||||
output_dir = folder_paths.get_output_directory()
|
||||
if not os.path.exists(output_dir):
|
||||
return web.json_response({"images": [], "total": 0, "page": 1, "pages": 0})
|
||||
|
||||
page = int(request.query.get('page', 1))
|
||||
limit = int(request.query.get('limit', 50))
|
||||
|
||||
valid_exts = {'.png', '.jpg', '.jpeg', '.webp', '.gif'}
|
||||
images = []
|
||||
|
||||
for root, dirs, files in os.walk(output_dir):
|
||||
for f in files:
|
||||
ext = os.path.splitext(f)[1].lower()
|
||||
if ext in valid_exts:
|
||||
rel_path = os.path.relpath(root, output_dir)
|
||||
subfolder = "" if rel_path == "." else rel_path.replace('\\', '/')
|
||||
full_path = os.path.join(root, f)
|
||||
try:
|
||||
mtime = os.path.getmtime(full_path)
|
||||
except:
|
||||
mtime = 0
|
||||
images.append({
|
||||
"filename": f,
|
||||
"subfolder": subfolder,
|
||||
"type": "output",
|
||||
"mtime": mtime
|
||||
})
|
||||
|
||||
# Sort by mtime descending (newest first)
|
||||
images.sort(key=lambda x: x['mtime'], reverse=True)
|
||||
|
||||
total = len(images)
|
||||
start_idx = (page - 1) * limit
|
||||
end_idx = start_idx + limit
|
||||
paginated_images = images[start_idx:end_idx]
|
||||
|
||||
return web.json_response({
|
||||
"images": paginated_images,
|
||||
"total": total,
|
||||
"page": page,
|
||||
"pages": (total + limit - 1) // limit if limit > 0 else 0
|
||||
})
|
||||
except Exception as e:
|
||||
return web.json_response({"error": str(e)}, status=500)
|
||||
|
||||
async def api_delete_gallery_image(request):
|
||||
try:
|
||||
data = await request.json()
|
||||
filename = data.get("filename")
|
||||
subfolder = data.get("subfolder", "")
|
||||
|
||||
if not filename or '..' in filename or '..' in subfolder:
|
||||
return web.json_response({"status": "error", "message": "Invalid parameters"})
|
||||
|
||||
output_dir = folder_paths.get_output_directory()
|
||||
if subfolder:
|
||||
file_path = os.path.join(output_dir, subfolder, filename)
|
||||
else:
|
||||
file_path = os.path.join(output_dir, filename)
|
||||
|
||||
if os.path.exists(file_path):
|
||||
os.remove(file_path)
|
||||
return web.json_response({"status": "success"})
|
||||
else:
|
||||
return web.json_response({"status": "error", "message": "File not found"})
|
||||
except Exception as e:
|
||||
return web.json_response({"status": "error", "message": str(e)}, status=500)
|
||||
|
||||
async def api_resolve_hash(request):
|
||||
target_hash = request.query.get("hash", "").strip().upper()
|
||||
size_str = request.query.get("size", "").strip()
|
||||
filename_query = request.query.get("filename", "").strip()
|
||||
target_size = int(size_str) if size_str.isdigit() else None
|
||||
|
||||
if not target_hash and not target_size and not filename_query:
|
||||
return web.json_response({"found": False})
|
||||
|
||||
types = ['checkpoints', 'loras', 'unet', 'diffusion_models']
|
||||
|
||||
# First pass: try exact hash match in .info files
|
||||
if target_hash:
|
||||
for t in types:
|
||||
try:
|
||||
paths = folder_paths.get_folder_paths(t)
|
||||
except Exception:
|
||||
continue
|
||||
if not paths:
|
||||
continue
|
||||
|
||||
for base_dir in paths:
|
||||
if not os.path.exists(base_dir):
|
||||
continue
|
||||
|
||||
for root, dirs, files in os.walk(base_dir):
|
||||
for file in files:
|
||||
if file.endswith('.safetensors') or file.endswith('.ckpt') or file.endswith('.pt'):
|
||||
file_path = os.path.join(root, file)
|
||||
meta = get_metadata(file_path)
|
||||
if meta.get("hash", "").upper() == target_hash:
|
||||
# ComfyUI widget expects relative path from folder_paths base_dir
|
||||
rel_path = os.path.relpath(file_path, base_dir)
|
||||
if rel_path.startswith('.\\') or rel_path.startswith('./'):
|
||||
rel_path = rel_path[2:]
|
||||
rel_path = rel_path.replace('\\', '/')
|
||||
return web.json_response({
|
||||
"found": True,
|
||||
"type": t,
|
||||
"filename": rel_path
|
||||
})
|
||||
|
||||
# Second pass: Fallback size match
|
||||
if target_size is not None:
|
||||
size_matches = []
|
||||
seen_realpaths = set()
|
||||
for t in types:
|
||||
try:
|
||||
paths = folder_paths.get_folder_paths(t)
|
||||
except Exception:
|
||||
continue
|
||||
if not paths:
|
||||
continue
|
||||
|
||||
for base_dir in paths:
|
||||
if not os.path.exists(base_dir):
|
||||
continue
|
||||
|
||||
for root, dirs, files in os.walk(base_dir):
|
||||
for file in files:
|
||||
if file.endswith('.safetensors') or file.endswith('.ckpt') or file.endswith('.pt'):
|
||||
file_path = os.path.join(root, file)
|
||||
try:
|
||||
if os.path.getsize(file_path) == target_size:
|
||||
real_p = os.path.realpath(file_path)
|
||||
if real_p in seen_realpaths:
|
||||
continue
|
||||
seen_realpaths.add(real_p)
|
||||
|
||||
rel_path = os.path.relpath(file_path, base_dir)
|
||||
if rel_path.startswith('.\\') or rel_path.startswith('./'):
|
||||
rel_path = rel_path[2:]
|
||||
rel_path = rel_path.replace('\\', '/')
|
||||
size_matches.append({
|
||||
"type": t,
|
||||
"filename": rel_path
|
||||
})
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
# If exactly ONE file matches the size, it's a definitive match
|
||||
if len(size_matches) == 1:
|
||||
return web.json_response({
|
||||
"found": True,
|
||||
"type": size_matches[0]["type"],
|
||||
"filename": size_matches[0]["filename"],
|
||||
"matched_by_size": True
|
||||
})
|
||||
|
||||
return web.json_response({"found": False})
|
||||
|
||||
async def api_get_all_hashes(request):
|
||||
"""
|
||||
Returns a dictionary of all scanned models with their hash and size.
|
||||
Keyed by both relative path and basename for maximum frontend resilience.
|
||||
"""
|
||||
import asyncio
|
||||
|
||||
def fetch_all():
|
||||
hashes = {}
|
||||
types = ['checkpoints', 'loras', 'unet', 'diffusion_models']
|
||||
for t in types:
|
||||
try:
|
||||
paths = folder_paths.get_folder_paths(t)
|
||||
if not paths: continue
|
||||
for base_dir in paths:
|
||||
if not os.path.exists(base_dir): continue
|
||||
for root, dirs, files in os.walk(base_dir):
|
||||
for file in files:
|
||||
if file.endswith('.safetensors') or file.endswith('.ckpt') or file.endswith('.pt'):
|
||||
file_path = os.path.join(root, file)
|
||||
try:
|
||||
size_bytes = os.path.getsize(file_path)
|
||||
except Exception:
|
||||
size_bytes = 0
|
||||
|
||||
meta = get_metadata(file_path)
|
||||
hash_val = ""
|
||||
if meta and meta.get("hash"):
|
||||
hash_val = meta["hash"]
|
||||
|
||||
rel_path = os.path.relpath(file_path, base_dir)
|
||||
if rel_path.startswith('.\\') or rel_path.startswith('./'):
|
||||
rel_path = rel_path[2:]
|
||||
rel_path = rel_path.replace('\\', '/')
|
||||
basename = os.path.basename(file_path)
|
||||
|
||||
val = {"hash": hash_val, "size": size_bytes}
|
||||
hashes[rel_path] = val
|
||||
hashes[basename] = val
|
||||
except Exception:
|
||||
pass
|
||||
return hashes
|
||||
|
||||
hashes = await asyncio.to_thread(fetch_all)
|
||||
return web.json_response(hashes)
|
||||
|
||||
def setup_routes(app):
|
||||
app.router.add_get('/anomalous/folders', api_get_folders)
|
||||
app.router.add_get('/anomalous/models', api_get_models)
|
||||
app.router.add_get('/anomalous/image', api_serve_image)
|
||||
app.router.add_post('/anomalous/scan', api_scan_folder)
|
||||
app.router.add_get('/anomalous/scan_status', api_scan_status)
|
||||
app.router.add_get('/anomalous/config', api_get_config)
|
||||
app.router.add_post('/anomalous/save_config', api_save_config)
|
||||
app.router.add_post('/anomalous/delete_model', api_delete_model)
|
||||
app.router.add_post('/anomalous/clean_civitai_info', api_clean_civitai_info)
|
||||
app.router.add_get('/anomalous/compatible_models', api_compatible_models)
|
||||
app.router.add_get('/anomalous/notebooks', api_get_notebooks)
|
||||
app.router.add_post('/anomalous/save_notebook', api_save_notebook)
|
||||
app.router.add_post('/anomalous/delete_notebook', api_delete_notebook)
|
||||
app.router.add_post('/anomalous/translate', api_translate)
|
||||
app.router.add_get('/anomalous/base_models', api_base_models)
|
||||
app.router.add_get('/anomalous/gallery_images', api_get_gallery_images)
|
||||
app.router.add_post('/anomalous/delete_gallery_image', api_delete_gallery_image)
|
||||
app.router.add_get('/anomalous/resolve_hash', api_resolve_hash)
|
||||
app.router.add_get('/anomalous/all_hashes', api_get_all_hashes)
|
||||
app.router.add_post('/anomalous/scan_all', api_scan_all)
|
||||
app.router.add_get('/anomalous/global_scan_status', api_global_scan_status)
|
||||
Reference in New Issue
Block a user