- Narrow the ProgressBar import guard to ImportError only, with a comment explaining why it stays (the module is imported standalone by the test suite outside a live ComfyUI) - Extract the CRAFTKIT_DIALOG_ERROR: sentinel into a shared constant instead of three separate literals, use str.removeprefix - Note that .avif/.heic can also fail on re-encode (with output_format=keep_source), not just decode
171 lines
6.4 KiB
Python
171 lines
6.4 KiB
Python
WEB_DIRECTORY = "./js"
|
|
|
|
from .smart_prompt_controller import NODE_CLASS_MAPPINGS as A, NODE_DISPLAY_NAME_MAPPINGS as A_NAMES
|
|
from .smart_profile_switch import NODE_CLASS_MAPPINGS as B, NODE_DISPLAY_NAME_MAPPINGS as B_NAMES
|
|
from .smart_resize import NODE_CLASS_MAPPINGS as C, NODE_DISPLAY_NAME_MAPPINGS as C_NAMES
|
|
from .smart_batch_resize import NODE_CLASS_MAPPINGS as D, NODE_DISPLAY_NAME_MAPPINGS as D_NAMES
|
|
from .smart_resolution_multiplier import NODE_CLASS_MAPPINGS as E, NODE_DISPLAY_NAME_MAPPINGS as E_NAMES
|
|
|
|
NODE_CLASS_MAPPINGS = {**A, **B, **C, **D, **E}
|
|
NODE_DISPLAY_NAME_MAPPINGS = {**A_NAMES, **B_NAMES, **C_NAMES, **D_NAMES, **E_NAMES}
|
|
|
|
__all__ = ["NODE_CLASS_MAPPINGS", "NODE_DISPLAY_NAME_MAPPINGS"]
|
|
|
|
try:
|
|
import asyncio
|
|
import concurrent.futures
|
|
import os
|
|
import threading
|
|
from aiohttp import web
|
|
from server import PromptServer
|
|
|
|
_dialog_lock = threading.Lock()
|
|
# Must match the literal 'CRAFTKIT_DIALOG_ERROR:' prefix written by the
|
|
# PowerShell catch block below (kept literal there to avoid brace-escaping
|
|
# a dense here-string).
|
|
_DIALOG_ERROR_PREFIX = "CRAFTKIT_DIALOG_ERROR:"
|
|
|
|
async def browse_folder(request):
|
|
import subprocess, sys
|
|
|
|
if request.remote not in ("127.0.0.1", "::1"):
|
|
return web.json_response({"ok": False, "error": "forbidden"}, status=403)
|
|
|
|
origin = request.headers.get("Origin")
|
|
if origin is not None:
|
|
host = request.headers.get("Host", "")
|
|
origin_host = origin.split("://", 1)[-1]
|
|
if origin_host != host:
|
|
return web.json_response({"ok": False, "error": "forbidden"}, status=403)
|
|
|
|
def _open_dialog():
|
|
if sys.platform != "win32":
|
|
return ""
|
|
ps = r"""
|
|
Add-Type -AssemblyName System.Windows.Forms
|
|
Add-Type @"
|
|
using System;
|
|
using System.Runtime.InteropServices;
|
|
|
|
public static class CraftKitFolderPicker
|
|
{
|
|
[ComImport, Guid("DC1C5A9C-E88A-4dde-A5A1-60F82A20AEF7")]
|
|
private class FileOpenDialogRCW { }
|
|
|
|
[ComImport, Guid("42f85136-db7e-439c-85f1-e4075d135fc8"), InterfaceType(ComInterfaceType.InterfaceIsIUnknown)]
|
|
private interface IFileDialog
|
|
{
|
|
[PreserveSig] int Show(IntPtr parent);
|
|
void SetFileTypes(uint cFileTypes, IntPtr rgFilterSpec);
|
|
void SetFileTypeIndex(uint iFileType);
|
|
void GetFileTypeIndex(out uint piFileType);
|
|
void Advise(IntPtr pfde, out uint pdwCookie);
|
|
void Unadvise(uint dwCookie);
|
|
void SetOptions(uint fos);
|
|
void GetOptions(out uint fos);
|
|
void SetDefaultFolder(IntPtr psi);
|
|
void SetFolder(IntPtr psi);
|
|
void GetFolder(out IntPtr ppsi);
|
|
void GetCurrentSelection(out IntPtr ppsi);
|
|
void SetFileName(string pszName);
|
|
void GetFileName(out IntPtr pszName);
|
|
void SetTitle(string pszTitle);
|
|
void SetOkButtonLabel(string pszText);
|
|
void SetFileNameLabel(string pszLabel);
|
|
void GetResult(out IShellItemLocal ppsi);
|
|
void AddPlace(IntPtr psi, uint fdap);
|
|
void SetDefaultExtension(string pszDefaultExtension);
|
|
void Close(int hr);
|
|
void SetClientGuid(ref Guid guid);
|
|
void ClearClientData();
|
|
void SetFilter(IntPtr pFilter);
|
|
}
|
|
|
|
[ComImport, Guid("43826d1e-e718-42ee-bc55-a1e261c37bfe"), InterfaceType(ComInterfaceType.InterfaceIsIUnknown)]
|
|
private interface IShellItemLocal
|
|
{
|
|
void BindToHandler(IntPtr pbc, ref Guid bhid, ref Guid riid, out IntPtr ppv);
|
|
void GetParent(out IShellItemLocal ppsi);
|
|
void GetDisplayName(uint sigdnName, out IntPtr ppszName);
|
|
void GetAttributes(uint sfgaoMask, out uint psfgaoAttribs);
|
|
void Compare(IShellItemLocal psi, uint hint, out int piOrder);
|
|
}
|
|
|
|
public static string ShowDialog(IntPtr owner, string title)
|
|
{
|
|
const uint ERROR_CANCELLED = 0x800704C7;
|
|
var dialog = (IFileDialog)new FileOpenDialogRCW();
|
|
dialog.SetOptions(0x20u | 0x40u); // FOS_PICKFOLDERS | FOS_FORCEFILESYSTEM
|
|
if (!string.IsNullOrEmpty(title)) dialog.SetTitle(title);
|
|
int hr = dialog.Show(owner);
|
|
if (hr != 0)
|
|
{
|
|
if ((uint)hr == ERROR_CANCELLED) return null;
|
|
throw new System.Runtime.InteropServices.COMException("IFileDialog.Show failed", hr);
|
|
}
|
|
IShellItemLocal item;
|
|
dialog.GetResult(out item);
|
|
IntPtr pszPath = IntPtr.Zero;
|
|
try
|
|
{
|
|
item.GetDisplayName(0x80058000u, out pszPath); // SIGDN_FILESYSPATH
|
|
return Marshal.PtrToStringUni(pszPath);
|
|
}
|
|
finally
|
|
{
|
|
if (pszPath != IntPtr.Zero) Marshal.FreeCoTaskMem(pszPath);
|
|
}
|
|
}
|
|
}
|
|
"@
|
|
|
|
$r = ''
|
|
$o = New-Object System.Windows.Forms.Form
|
|
$o.TopMost = $true
|
|
$o.ShowInTaskbar = $false
|
|
$o.FormBorderStyle = 'None'
|
|
$o.Width = 1; $o.Height = 1; $o.Opacity = 0
|
|
$o.StartPosition = 'CenterScreen'
|
|
$o.Add_Shown({
|
|
$o.Activate()
|
|
try {
|
|
$path = [CraftKitFolderPicker]::ShowDialog($o.Handle, 'Select input folder')
|
|
if ($path) { $script:r = $path }
|
|
} catch {
|
|
# Must match _DIALOG_ERROR_PREFIX in __init__.py.
|
|
$script:r = 'CRAFTKIT_DIALOG_ERROR:' + $_.Exception.Message
|
|
}
|
|
$o.Close()
|
|
})
|
|
$o.ShowDialog() | Out-Null
|
|
$r
|
|
"""
|
|
try:
|
|
result = subprocess.run(
|
|
["powershell", "-NoProfile", "-Command", ps],
|
|
capture_output=True, text=True, timeout=300
|
|
)
|
|
except subprocess.TimeoutExpired:
|
|
return ""
|
|
return result.stdout.strip()
|
|
|
|
if not _dialog_lock.acquire(blocking=False):
|
|
return web.json_response({"ok": False, "error": "A folder dialog is already open."}, status=409)
|
|
try:
|
|
loop = asyncio.get_running_loop()
|
|
with concurrent.futures.ThreadPoolExecutor() as pool:
|
|
folder = await loop.run_in_executor(pool, _open_dialog)
|
|
finally:
|
|
_dialog_lock.release()
|
|
|
|
if folder.startswith(_DIALOG_ERROR_PREFIX):
|
|
return web.json_response({"ok": False, "error": folder.removeprefix(_DIALOG_ERROR_PREFIX)}, status=500)
|
|
|
|
if folder and os.path.isdir(folder):
|
|
return web.json_response({"ok": True, "path": folder})
|
|
return web.json_response({"ok": False, "cancelled": True})
|
|
|
|
PromptServer.instance.routes.post("/craftkit/browse_folder")(browse_folder)
|
|
except Exception as e:
|
|
print(f"[CraftKit] Browse folder endpoint unavailable ({e}); paste input folder paths manually.")
|