diff --git a/.gitignore b/.gitignore index 95ccd9e7..e6485fd4 100644 --- a/.gitignore +++ b/.gitignore @@ -122,6 +122,8 @@ celerybeat.pid # Environments .env .venv +.python +.node env/ venv/ ENV/ diff --git a/README.md b/README.md index 96ac86d5..e865139a 100644 --- a/README.md +++ b/README.md @@ -236,7 +236,7 @@ _Last updated: 2026-03-03 15:01 UTC_ ## Installation Requirements: -- python >3.10 +- python >=3.10 (3.12 recommended) - Nvidia GPU with enough ram to do what you need - python venv - git @@ -269,6 +269,20 @@ pip install --no-cache-dir torch==2.9.1 torchvision==0.24.1 torchaudio==2.9.1 -- pip install -r requirements.txt ``` +MacOS: + +Experimental support for Silicon Macs is available. I do not have a Mac with enough RAM to fully test this +so please let me know if there are issues. There is a convience script to install and run on MacOS +locates at `./run_mac.zsh` that will install the dependencies locally and run the UI. To run this, +do the following: + +```bash +git clone https://github.com/ostris/ai-toolkit.git +cd ai-toolkit +chmod +x run_mac.zsh +./run_mac.zsh +``` + # AI Toolkit UI diff --git a/extensions_built_in/sd_trainer/SDTrainer.py b/extensions_built_in/sd_trainer/SDTrainer.py index 9e0d20d9..066a5eea 100644 --- a/extensions_built_in/sd_trainer/SDTrainer.py +++ b/extensions_built_in/sd_trainer/SDTrainer.py @@ -39,11 +39,7 @@ import torch.nn.functional as F from toolkit.unloader import unload_text_encoder from PIL import Image from torchvision.transforms import functional as TF - - -def flush(): - torch.cuda.empty_cache() - gc.collect() +from toolkit.basic import flush adapter_transforms = transforms.Compose([ diff --git a/jobs/process/BaseSDTrainProcess.py b/jobs/process/BaseSDTrainProcess.py index 5d130fc5..dae17596 100644 --- a/jobs/process/BaseSDTrainProcess.py +++ b/jobs/process/BaseSDTrainProcess.py @@ -72,10 +72,7 @@ import hashlib from toolkit.util.blended_blur_noise import get_blended_blur_noise from toolkit.util.get_model import get_model_class - -def flush(): - torch.cuda.empty_cache() - gc.collect() +from toolkit.basic import flush class BaseSDTrainProcess(BaseTrainProcess): diff --git a/run_mac.zsh b/run_mac.zsh new file mode 100755 index 00000000..8f7b49e9 --- /dev/null +++ b/run_mac.zsh @@ -0,0 +1,166 @@ +#!/usr/bin/env zsh +# Update-and-run script for macOS — portable Python 3.12 + PyTorch +set -euo pipefail + +SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" + +# ── Banner ───────────────────────────────────────────────────────── +echo "" +echo "\033[36m" +cat << 'BANNER' + _ ___ _____ _ _ _ _ + / \ |_ _| |_ _| ___ ___ | || | __(_)| |_ + / _ \ | | | | / _ \ / _ \| || |/ /| || __| + / ___ \ | | | | | (_) || (_) | || < | || |_ + /_/ \_\|___| |_| \___/ \___/|_||_|\_\|_| \__| +BANNER +echo "\033[0m" +echo "\033[90m macOS Setup & Launcher\033[0m" +echo "" +VENV_DIR="$SCRIPT_DIR/.venv" +PIP="$VENV_DIR/bin/pip" +PYTHON="$VENV_DIR/bin/python3" +PYTHON_VERSION="3.12.8" +RELEASE_TAG="20241219" + +# --- Package versions (update these as needed) --- +NODE_VERSION="23.11.1" +TORCH_VERSION="2.11.0" +TORCHVISION_VERSION="0.26.0" +TORCHAUDIO_VERSION="2.11.0" + +# Detect architecture +ARCH="$(uname -m)" +if [[ "$ARCH" == "arm64" ]]; then + PLATFORM="aarch64-apple-darwin" +elif [[ "$ARCH" == "x86_64" ]]; then + PLATFORM="x86_64-apple-darwin" +else + echo "Error: Unsupported architecture: $ARCH" + exit 1 +fi + +# ── 1. Download standalone Python if needed ───────────────────────── +PYTHON_DIR="$SCRIPT_DIR/.python" +PYTHON_BIN="$PYTHON_DIR/bin/python3" + +if [[ ! -x "$PYTHON_BIN" ]]; then + TARBALL="cpython-${PYTHON_VERSION}+${RELEASE_TAG}-${PLATFORM}-install_only.tar.gz" + URL="https://github.com/indygreg/python-build-standalone/releases/download/${RELEASE_TAG}/${TARBALL}" + + TMPDIR_DL="$(mktemp -d)" + trap 'rm -rf "$TMPDIR_DL"' EXIT + + echo "Downloading standalone Python ${PYTHON_VERSION} (${PLATFORM})..." + curl -fSL --progress-bar -o "$TMPDIR_DL/$TARBALL" "$URL" + + echo "Extracting..." + tar -xzf "$TMPDIR_DL/$TARBALL" -C "$TMPDIR_DL" + + # Move to permanent location (the archive extracts to a "python" folder) + rm -rf "$PYTHON_DIR" + mv "$TMPDIR_DL/python" "$PYTHON_DIR" + + rm -rf "$TMPDIR_DL" + trap - EXIT + + echo "Standalone Python installed to $PYTHON_DIR" +fi + +# ── 2. Create venv if it doesn't exist ────────────────────────────── +if [[ ! -d "$VENV_DIR" ]]; then + echo "Creating virtual environment at $VENV_DIR..." + "$PYTHON_BIN" -m venv "$VENV_DIR" + echo "Virtual environment created." +fi + +# ── 3. Download / update portable Node.js ────────────────────────── +NODE_DIR="$SCRIPT_DIR/.node" +NODE_BIN="$NODE_DIR/bin/node" + +NEED_NODE=false +if [[ ! -x "$NODE_BIN" ]]; then + NEED_NODE=true +elif [[ "$("$NODE_BIN" --version 2>/dev/null)" != "v${NODE_VERSION}" ]]; then + echo "Node.js version mismatch (want v${NODE_VERSION}, have $("$NODE_BIN" --version))." + NEED_NODE=true +fi + +if $NEED_NODE; then + if [[ "$ARCH" == "arm64" ]]; then + NODE_ARCH="arm64" + else + NODE_ARCH="x64" + fi + + NODE_TARBALL="node-v${NODE_VERSION}-darwin-${NODE_ARCH}.tar.gz" + NODE_URL="https://nodejs.org/dist/v${NODE_VERSION}/${NODE_TARBALL}" + + TMPDIR_DL="$(mktemp -d)" + trap 'rm -rf "$TMPDIR_DL"' EXIT + + echo "Downloading Node.js v${NODE_VERSION} (darwin-${NODE_ARCH})..." + curl -fSL --progress-bar -o "$TMPDIR_DL/$NODE_TARBALL" "$NODE_URL" + + echo "Extracting..." + tar -xzf "$TMPDIR_DL/$NODE_TARBALL" -C "$TMPDIR_DL" + + rm -rf "$NODE_DIR" + mv "$TMPDIR_DL/node-v${NODE_VERSION}-darwin-${NODE_ARCH}" "$NODE_DIR" + + rm -rf "$TMPDIR_DL" + trap - EXIT + + echo "Node.js v${NODE_VERSION} installed to $NODE_DIR" +else + echo "Node.js v${NODE_VERSION} is up to date." +fi + +# ── 4. Install / update PyTorch packages ──────────────────────────── +# Helper: returns 0 if the package is installed at the exact version +pkg_ok() { + local pkg="$1" want="$2" + local got + got="$("$PIP" show "$pkg" 2>/dev/null | awk '/^Version:/{print $2}')" || true + [[ "$got" == "$want" ]] +} + +PKGS_TO_INSTALL=() + +pkg_ok "torch" "$TORCH_VERSION" || PKGS_TO_INSTALL+=("torch==$TORCH_VERSION") +pkg_ok "torchvision" "$TORCHVISION_VERSION" || PKGS_TO_INSTALL+=("torchvision==$TORCHVISION_VERSION") +pkg_ok "torchaudio" "$TORCHAUDIO_VERSION" || PKGS_TO_INSTALL+=("torchaudio==$TORCHAUDIO_VERSION") + +if (( ${#PKGS_TO_INSTALL[@]} )); then + echo "Installing / updating: ${PKGS_TO_INSTALL[*]}" + "$PIP" install "${PKGS_TO_INSTALL[@]}" +else + echo "PyTorch packages are up to date." +fi + +# ── 5. Install / update requirements.txt ──────────────────────────── +REQUIREMENTS="$SCRIPT_DIR/requirements.txt" +REQ_HASH_FILE="$VENV_DIR/.requirements_hash" + +if [[ -f "$REQUIREMENTS" ]]; then + # Hash all requirements files (follows -r includes) + CURRENT_HASH="$(cat "$SCRIPT_DIR"/requirements*.txt 2>/dev/null | shasum -a 256 | awk '{print $1}')" + STORED_HASH="" + [[ -f "$REQ_HASH_FILE" ]] && STORED_HASH="$(cat "$REQ_HASH_FILE")" + + if [[ "$CURRENT_HASH" != "$STORED_HASH" ]]; then + echo "Installing / updating requirements.txt..." + "$PIP" install -r "$REQUIREMENTS" + echo "$CURRENT_HASH" > "$REQ_HASH_FILE" + else + echo "Requirements are up to date." + fi +fi + +# ── 6. Build and start the UI ─────────────────────────────────────── +export PATH="$NODE_DIR/bin:$VENV_DIR/bin:$PATH" + +echo "" +echo "Starting UI..." +cd "$SCRIPT_DIR/ui" +npm run build_and_start diff --git a/toolkit/basic.py b/toolkit/basic.py index d9b9ff7b..37844457 100644 --- a/toolkit/basic.py +++ b/toolkit/basic.py @@ -9,7 +9,11 @@ def value_map(inputs, min_in, max_in, min_out, max_out): def flush(garbage_collect=True): - torch.cuda.empty_cache() + if torch.cuda.is_available(): + torch.cuda.empty_cache() + # if is mps, also clear the mps cache + if torch.backends.mps.is_available(): + torch.mps.empty_cache() if garbage_collect: gc.collect() diff --git a/toolkit/config_modules.py b/toolkit/config_modules.py index 9a7427ed..03d22306 100644 --- a/toolkit/config_modules.py +++ b/toolkit/config_modules.py @@ -7,6 +7,7 @@ import torch import torchaudio from toolkit.prompt_utils import PromptEmbeds +from torchao.quantization.quant_primitives import _DTYPE_TO_BIT_WIDTH ImgExt = Literal['jpg', 'png', 'webp'] @@ -668,6 +669,12 @@ class ModelConfig: self.qtype = "float8" if self.layer_offloading and self.qtype_te == "qfloat8": self.qtype_te = "float8" + + # Mac mps only works with torachao uint + if torch.backends.mps.is_available() and self.qtype == "qfloat8": + self.qtype = "int8" + if torch.backends.mps.is_available() and self.qtype_te == "qfloat8": + self.qtype_te = "int8" # 0 is off and 1.0 is 100% of the layers self.layer_offloading_transformer_percent = kwargs.get("layer_offloading_transformer_percent", 1.0) diff --git a/toolkit/data_loader.py b/toolkit/data_loader.py index 2391adcd..2d96f1ee 100644 --- a/toolkit/data_loader.py +++ b/toolkit/data_loader.py @@ -29,6 +29,9 @@ import platform def is_native_windows(): return platform.system() == "Windows" and platform.release() != "2" +def is_macos(): + return platform.system() == "Darwin" + if TYPE_CHECKING: from toolkit.stable_diffusion_model import StableDiffusion @@ -678,7 +681,7 @@ def get_dataloader_from_datasets( dataloader_kwargs = {} - if is_native_windows(): + if is_native_windows() or is_macos(): dataloader_kwargs['num_workers'] = 0 else: dataloader_kwargs['num_workers'] = dataset_config_list[0].num_workers diff --git a/toolkit/models/base_model.py b/toolkit/models/base_model.py index 938649b3..9e4e8d1a 100644 --- a/toolkit/models/base_model.py +++ b/toolkit/models/base_model.py @@ -41,6 +41,7 @@ from torchvision.transforms import functional as TF from toolkit.accelerator import get_accelerator, unwrap_model from typing import TYPE_CHECKING from toolkit.print import print_acc +from toolkit.basic import flush if TYPE_CHECKING: from toolkit.lora_special import LoRASpecialNetwork @@ -90,11 +91,6 @@ class BlankNetwork: pass -def flush(): - torch.cuda.empty_cache() - gc.collect() - - UNET_IN_CHANNELS = 4 # Stable Diffusion の in_channels は 4 で固定。XLも同じ。 # VAE_SCALE_FACTOR = 8 # 2 ** (len(vae.config.block_out_channels) - 1) = 8 diff --git a/toolkit/stable_diffusion_model.py b/toolkit/stable_diffusion_model.py index d0b54889..2f1030c2 100644 --- a/toolkit/stable_diffusion_model.py +++ b/toolkit/stable_diffusion_model.py @@ -70,6 +70,7 @@ from typing import TYPE_CHECKING from toolkit.print import print_acc from diffusers import FluxFillPipeline from transformers import AutoModel, AutoTokenizer, Gemma2Model, Qwen2Model, LlamaModel +from toolkit.basic import flush if TYPE_CHECKING: from toolkit.lora_special import LoRASpecialNetwork @@ -118,11 +119,6 @@ class BlankNetwork: pass -def flush(): - torch.cuda.empty_cache() - gc.collect() - - UNET_IN_CHANNELS = 4 # Stable Diffusion の in_channels は 4 で固定。XLも同じ。 # VAE_SCALE_FACTOR = 8 # 2 ** (len(vae.config.block_out_channels) - 1) = 8 diff --git a/toolkit/util/quantize.py b/toolkit/util/quantize.py index 27d6733a..03c4d78a 100644 --- a/toolkit/util/quantize.py +++ b/toolkit/util/quantize.py @@ -8,6 +8,7 @@ from torchao.quantization.quant_api import ( quantize_ as torchao_quantize_, Float8WeightOnlyConfig, UIntXWeightOnlyConfig, + Int8WeightOnlyConfig ) from optimum.quanto import freeze from tqdm import tqdm @@ -41,6 +42,7 @@ torchao_qtypes = { "uint6": UIntXWeightOnlyConfig(torch.uint6), "uint7": UIntXWeightOnlyConfig(torch.uint7), "uint8": UIntXWeightOnlyConfig(torch.uint8), + "int8": Int8WeightOnlyConfig(), "float8": Float8WeightOnlyConfig(), } diff --git a/ui/next.config.ts b/ui/next.config.ts index d81192e3..de85a43c 100644 --- a/ui/next.config.ts +++ b/ui/next.config.ts @@ -1,6 +1,13 @@ import type { NextConfig } from 'next'; const nextConfig: NextConfig = { + serverExternalPackages: ['macstats', 'osx-temperature-sensor'], + webpack: (config, { isServer }) => { + if (isServer) { + config.externals.push('osx-temperature-sensor', 'macstats'); + } + return config; + }, devIndicators: { buildActivity: false, }, diff --git a/ui/package-lock.json b/ui/package-lock.json index 1b7ba9cd..b9dce7d1 100644 --- a/ui/package-lock.json +++ b/ui/package-lock.json @@ -18,8 +18,8 @@ "next": "^15.5.9", "node-cache": "^5.1.2", "prisma": "^6.3.1", - "react": "^19.0.0", - "react-dom": "^19.0.0", + "react": "^19.2.0", + "react-dom": "^19.2.0", "react-dropzone": "^14.3.5", "react-global-hooks": "^1.3.5", "react-icons": "^5.5.0", @@ -42,6 +42,39 @@ "tailwindcss": "^3.4.1", "ts-node-dev": "^2.0.0", "typescript": "^5" + }, + "optionalDependencies": { + "macstats": "^4.2.0" + } + }, + "node_modules/@alcalzone/ansi-tokenize": { + "version": "0.2.5", + "resolved": "https://registry.npmjs.org/@alcalzone/ansi-tokenize/-/ansi-tokenize-0.2.5.tgz", + "integrity": "sha512-3NX/MpTdroi0aKz134A6RC2Gb2iXVECN4QaAXnvCIxxIm3C3AVB1mkUe8NaaiyvOpDfsrqWhYtj+Q6a62RrTsw==", + "license": "MIT", + "optional": true, + "dependencies": { + "ansi-styles": "^6.2.1", + "is-fullwidth-code-point": "^5.0.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@alcalzone/ansi-tokenize/node_modules/is-fullwidth-code-point": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-5.1.0.tgz", + "integrity": "sha512-5XHYaSyiqADb4RnZ1Bdad6cPp8Toise4TzEjcOYDHZkTCbKgiUl7WTUCpNWHuxmDt91wnsZBc9xinNzopv3JMQ==", + "license": "MIT", + "optional": true, + "dependencies": { + "get-east-asian-width": "^1.3.1" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, "node_modules/@alloc/quick-lru": { @@ -1616,10 +1649,27 @@ "node": ">=8" } }, + "node_modules/ansi-escapes": { + "version": "7.3.0", + "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-7.3.0.tgz", + "integrity": "sha512-BvU8nYgGQBxcmMuEeUEmNTvrMVjJNSH7RgW24vXexN4Ven6qCvy4TntnvlnwnMLTVlcRQQdbRY8NKnaIoeWDNg==", + "license": "MIT", + "optional": true, + "dependencies": { + "environment": "^1.0.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/ansi-regex": { - "version": "6.1.0", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.1.0.tgz", - "integrity": "sha512-7HSX4QQb4CspciLpVFwyRe79O3xsIZDDLER21kERQ71oaPodF8jL725AgJMFAYbooIqolJoRLuM81SpeUkpkvA==", + "version": "6.2.2", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.2.2.tgz", + "integrity": "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg==", + "license": "MIT", "engines": { "node": ">=12" }, @@ -1628,9 +1678,10 @@ } }, "node_modules/ansi-styles": { - "version": "6.2.1", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.1.tgz", - "integrity": "sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug==", + "version": "6.2.3", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.3.tgz", + "integrity": "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg==", + "license": "MIT", "engines": { "node": ">=12" }, @@ -1823,6 +1874,19 @@ "node": ">=4" } }, + "node_modules/auto-bind": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/auto-bind/-/auto-bind-5.0.1.tgz", + "integrity": "sha512-ooviqdwwgfIfNmDwo94wlshcdzfO64XV0Cg6oDsDYBJfITDz1EngD2z7DkbvCWn+XIMsIqW27sEVF6qcpJrRcg==", + "license": "MIT", + "optional": true, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/axios": { "version": "1.12.2", "resolved": "https://registry.npmjs.org/axios/-/axios-1.12.2.tgz", @@ -2225,6 +2289,69 @@ "node": ">=6" } }, + "node_modules/cli-boxes": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/cli-boxes/-/cli-boxes-3.0.0.tgz", + "integrity": "sha512-/lzGpEWL/8PfI0BmBOPRwp0c/wFNX1RdUML3jK/RcSBA9T8mZDdQpqYBKtCFTOfQbwPqWEOpjqW+Fnayc0969g==", + "license": "MIT", + "optional": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/cli-cursor": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/cli-cursor/-/cli-cursor-4.0.0.tgz", + "integrity": "sha512-VGtlMu3x/4DOtIUwEkRezxUZ2lBacNJCHash0N0WeZDBS+7Ux1dm3XWAgWYxLJFMMdOeXMHXorshEFhbMSGelg==", + "license": "MIT", + "optional": true, + "dependencies": { + "restore-cursor": "^4.0.0" + }, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/cli-truncate": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/cli-truncate/-/cli-truncate-5.2.0.tgz", + "integrity": "sha512-xRwvIOMGrfOAnM1JYtqQImuaNtDEv9v6oIYAs4LIHwTiKee8uwvIi363igssOC0O5U04i4AlENs79LQLu9tEMw==", + "license": "MIT", + "optional": true, + "dependencies": { + "slice-ansi": "^8.0.0", + "string-width": "^8.2.0" + }, + "engines": { + "node": ">=20" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/cli-truncate/node_modules/string-width": { + "version": "8.2.0", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-8.2.0.tgz", + "integrity": "sha512-6hJPQ8N0V0P3SNmP6h2J99RLuzrWz2gvT7VnK5tKvrNqJoyS9W4/Fb8mo31UiPvy00z7DQXkP2hnKBVav76thw==", + "license": "MIT", + "optional": true, + "dependencies": { + "get-east-asian-width": "^1.5.0", + "strip-ansi": "^7.1.2" + }, + "engines": { + "node": ">=20" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/client-only": { "version": "0.0.1", "resolved": "https://registry.npmjs.org/client-only/-/client-only-0.0.1.tgz", @@ -2333,6 +2460,19 @@ "node": ">=6" } }, + "node_modules/code-excerpt": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/code-excerpt/-/code-excerpt-4.0.0.tgz", + "integrity": "sha512-xxodCmBen3iy2i0WtAK8FlFNrRzjUqjRsMfho58xT/wvZU1YTM3fCnRjcy1gJPMepaRlgm/0e6w8SpWHpn3/cA==", + "license": "MIT", + "optional": true, + "dependencies": { + "convert-to-spaces": "^2.0.1" + }, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + } + }, "node_modules/color-convert": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", @@ -2476,6 +2616,16 @@ "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-1.9.0.tgz", "integrity": "sha512-ASFBup0Mz1uyiIjANan1jzLQami9z1PoYSZCiiYW2FczPbenXc45FZdBZLzOT+r6+iciuEModtmCti+hjaAk0A==" }, + "node_modules/convert-to-spaces": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/convert-to-spaces/-/convert-to-spaces-2.0.1.tgz", + "integrity": "sha512-rcQ1bsQO9799wq24uE5AM2tAILy4gXGIK/njFWcVQkGNZ96edlpY+A7bjwvzjYvLDyzmG1MmMLZhpcsb+klNMQ==", + "license": "MIT", + "optional": true, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + } + }, "node_modules/core-util-is": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.3.tgz", @@ -2884,6 +3034,19 @@ "node": ">=6" } }, + "node_modules/environment": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/environment/-/environment-1.1.0.tgz", + "integrity": "sha512-xUtoPkMggbz0MPyPiIWr1Kp4aeWJjDZ6SMvURhimjdZgsRuDplF5/s9hcgGhyXMhs+6vpnuoiZ2kFiu3FMnS8Q==", + "license": "MIT", + "optional": true, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/err-code": { "version": "2.0.3", "resolved": "https://registry.npmjs.org/err-code/-/err-code-2.0.3.tgz", @@ -3271,6 +3434,19 @@ "node": "6.* || 8.* || >= 10.*" } }, + "node_modules/get-east-asian-width": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/get-east-asian-width/-/get-east-asian-width-1.5.0.tgz", + "integrity": "sha512-CQ+bEO+Tva/qlmw24dCejulK5pMzVnUOFOijVogd3KQs07HnRIgp8TGipvCCRT06xeYEbpbgwaCxglFyiuIcmA==", + "license": "MIT", + "optional": true, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/get-intrinsic": { "version": "1.3.0", "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", @@ -3584,6 +3760,165 @@ "integrity": "sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==", "license": "ISC" }, + "node_modules/ink": { + "version": "6.8.0", + "resolved": "https://registry.npmjs.org/ink/-/ink-6.8.0.tgz", + "integrity": "sha512-sbl1RdLOgkO9isK42WCZlJCFN9hb++sX9dsklOvfd1YQ3bQ2AiFu12Q6tFlr0HvEUvzraJntQCCpfEoUe9DSzA==", + "license": "MIT", + "optional": true, + "dependencies": { + "@alcalzone/ansi-tokenize": "^0.2.4", + "ansi-escapes": "^7.3.0", + "ansi-styles": "^6.2.1", + "auto-bind": "^5.0.1", + "chalk": "^5.6.0", + "cli-boxes": "^3.0.0", + "cli-cursor": "^4.0.0", + "cli-truncate": "^5.1.1", + "code-excerpt": "^4.0.0", + "es-toolkit": "^1.39.10", + "indent-string": "^5.0.0", + "is-in-ci": "^2.0.0", + "patch-console": "^2.0.0", + "react-reconciler": "^0.33.0", + "scheduler": "^0.27.0", + "signal-exit": "^3.0.7", + "slice-ansi": "^8.0.0", + "stack-utils": "^2.0.6", + "string-width": "^8.1.1", + "terminal-size": "^4.0.1", + "type-fest": "^5.4.1", + "widest-line": "^6.0.0", + "wrap-ansi": "^9.0.0", + "ws": "^8.18.0", + "yoga-layout": "~3.2.1" + }, + "engines": { + "node": ">=20" + }, + "peerDependencies": { + "@types/react": ">=19.0.0", + "react": ">=19.0.0", + "react-devtools-core": ">=6.1.2" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "react-devtools-core": { + "optional": true + } + } + }, + "node_modules/ink/node_modules/chalk": { + "version": "5.6.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-5.6.2.tgz", + "integrity": "sha512-7NzBL0rN6fMUW+f7A6Io4h40qQlG+xGmtMxfbnH/K7TAtt8JQWVQK+6g0UXKMeVJoyV5EkkNsErQ8pVD3bLHbA==", + "license": "MIT", + "optional": true, + "engines": { + "node": "^12.17.0 || ^14.13 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/ink/node_modules/emoji-regex": { + "version": "10.6.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-10.6.0.tgz", + "integrity": "sha512-toUI84YS5YmxW219erniWD0CIVOo46xGKColeNQRgOzDorgBi1v4D71/OFzgD9GO2UGKIv1C3Sp8DAn0+j5w7A==", + "license": "MIT", + "optional": true + }, + "node_modules/ink/node_modules/indent-string": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/indent-string/-/indent-string-5.0.0.tgz", + "integrity": "sha512-m6FAo/spmsW2Ab2fU35JTYwtOKa2yAwXSwgjSv1TJzh4Mh7mC3lzAOVLBprb72XsTrgkEIsl7YrFNAiDiRhIGg==", + "license": "MIT", + "optional": true, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/ink/node_modules/react-reconciler": { + "version": "0.33.0", + "resolved": "https://registry.npmjs.org/react-reconciler/-/react-reconciler-0.33.0.tgz", + "integrity": "sha512-KetWRytFv1epdpJc3J4G75I4WrplZE5jOL7Yq0p34+OVOKF4Se7WrdIdVC45XsSSmUTlht2FM/fM1FZb1mfQeA==", + "license": "MIT", + "optional": true, + "dependencies": { + "scheduler": "^0.27.0" + }, + "engines": { + "node": ">=0.10.0" + }, + "peerDependencies": { + "react": "^19.2.0" + } + }, + "node_modules/ink/node_modules/signal-exit": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz", + "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==", + "license": "ISC", + "optional": true + }, + "node_modules/ink/node_modules/string-width": { + "version": "8.2.0", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-8.2.0.tgz", + "integrity": "sha512-6hJPQ8N0V0P3SNmP6h2J99RLuzrWz2gvT7VnK5tKvrNqJoyS9W4/Fb8mo31UiPvy00z7DQXkP2hnKBVav76thw==", + "license": "MIT", + "optional": true, + "dependencies": { + "get-east-asian-width": "^1.5.0", + "strip-ansi": "^7.1.2" + }, + "engines": { + "node": ">=20" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/ink/node_modules/wrap-ansi": { + "version": "9.0.2", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-9.0.2.tgz", + "integrity": "sha512-42AtmgqjV+X1VpdOfyTGOYRi0/zsoLqtXQckTmqTeybT+BDIbM/Guxo7x3pE2vtpr1ok6xRqM9OpBe+Jyoqyww==", + "license": "MIT", + "optional": true, + "dependencies": { + "ansi-styles": "^6.2.1", + "string-width": "^7.0.0", + "strip-ansi": "^7.1.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/ink/node_modules/wrap-ansi/node_modules/string-width": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-7.2.0.tgz", + "integrity": "sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ==", + "license": "MIT", + "optional": true, + "dependencies": { + "emoji-regex": "^10.3.0", + "get-east-asian-width": "^1.0.0", + "strip-ansi": "^7.1.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/internmap": { "version": "2.0.3", "resolved": "https://registry.npmjs.org/internmap/-/internmap-2.0.3.tgz", @@ -3662,6 +3997,22 @@ "node": ">=0.10.0" } }, + "node_modules/is-in-ci": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/is-in-ci/-/is-in-ci-2.0.0.tgz", + "integrity": "sha512-cFeerHriAnhrQSbpAxL37W1wcJKUUX07HyLWZCW1URJT/ra3GyUTzBgUnh24TMVfNTV2Hij2HLxkPHFZfOZy5w==", + "license": "MIT", + "optional": true, + "bin": { + "is-in-ci": "cli.js" + }, + "engines": { + "node": ">=20" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/is-lambda": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/is-lambda/-/is-lambda-1.0.1.tgz", @@ -3835,6 +4186,39 @@ "react": "^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, + "node_modules/macstats": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/macstats/-/macstats-4.2.0.tgz", + "integrity": "sha512-+NJmIPjndK62WOwmu4Qbbgj5K84rTON7j3kBMOyFERsEztA7yM1ITyBRQQ/Y7cvRyYVeMi11C6KH7CFhIZlc2A==", + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "dependencies": { + "chalk": "^5.6.2", + "ink": "^6.4.0", + "nan": "^2.23.1", + "react": "^19.2.0" + }, + "bin": { + "macstats": "bin/macstats" + } + }, + "node_modules/macstats/node_modules/chalk": { + "version": "5.6.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-5.6.2.tgz", + "integrity": "sha512-7NzBL0rN6fMUW+f7A6Io4h40qQlG+xGmtMxfbnH/K7TAtt8JQWVQK+6g0UXKMeVJoyV5EkkNsErQ8pVD3bLHbA==", + "license": "MIT", + "optional": true, + "engines": { + "node": "^12.17.0 || ^14.13 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, "node_modules/make-error": { "version": "1.3.6", "resolved": "https://registry.npmjs.org/make-error/-/make-error-1.3.6.tgz", @@ -3949,6 +4333,16 @@ "node": ">= 0.6" } }, + "node_modules/mimic-fn": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-2.1.0.tgz", + "integrity": "sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==", + "license": "MIT", + "optional": true, + "engines": { + "node": ">=6" + } + }, "node_modules/mimic-response": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-3.1.0.tgz", @@ -4193,6 +4587,13 @@ "thenify-all": "^1.0.0" } }, + "node_modules/nan": { + "version": "2.26.2", + "resolved": "https://registry.npmjs.org/nan/-/nan-2.26.2.tgz", + "integrity": "sha512-0tTvBTYkt3tdGw22nrAy50x7gpbGCCFH3AFcyS5WiUu7Eu4vWlri1woE6qHBSfy11vksDqkiwjOnlR7WV8G1Hw==", + "license": "MIT", + "optional": true + }, "node_modules/nanoid": { "version": "3.3.8", "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.8.tgz", @@ -4471,6 +4872,22 @@ "wrappy": "1" } }, + "node_modules/onetime": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/onetime/-/onetime-5.1.2.tgz", + "integrity": "sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg==", + "license": "MIT", + "optional": true, + "dependencies": { + "mimic-fn": "^2.1.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/p-map": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/p-map/-/p-map-4.0.0.tgz", @@ -4520,6 +4937,16 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/patch-console": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/patch-console/-/patch-console-2.0.0.tgz", + "integrity": "sha512-0YNdUceMdaQwoKce1gatDScmMo5pu/tfABfnzEqeG0gtTmd7mh/WcwgUjtAeOU7N8nFFlbQBnFK2gXW5fGvmMA==", + "license": "MIT", + "optional": true, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + } + }, "node_modules/path-is-absolute": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", @@ -4915,22 +5342,24 @@ } }, "node_modules/react": { - "version": "19.0.0", - "resolved": "https://registry.npmjs.org/react/-/react-19.0.0.tgz", - "integrity": "sha512-V8AVnmPIICiWpGfm6GLzCR/W5FXLchHop40W4nXBmdlEceh16rCN8O8LNWm5bh5XUX91fh7KpA+W0TgMKmgTpQ==", + "version": "19.2.4", + "resolved": "https://registry.npmjs.org/react/-/react-19.2.4.tgz", + "integrity": "sha512-9nfp2hYpCwOjAN+8TZFGhtWEwgvWHXqESH8qT89AT/lWklpLON22Lc8pEtnpsZz7VmawabSU0gCjnj8aC0euHQ==", + "license": "MIT", "engines": { "node": ">=0.10.0" } }, "node_modules/react-dom": { - "version": "19.0.0", - "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.0.0.tgz", - "integrity": "sha512-4GV5sHFG0e/0AD4X+ySy6UJd3jVl1iNsNHdpad0qhABJ11twS3TTBnseqsKurKcsNqCEFeGL3uLpVChpIO3QfQ==", + "version": "19.2.4", + "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.2.4.tgz", + "integrity": "sha512-AXJdLo8kgMbimY95O2aKQqsz2iWi9jMgKJhRBAxECE4IFxfcazB2LmzloIoibJI3C12IlY20+KFaLv+71bUJeQ==", + "license": "MIT", "dependencies": { - "scheduler": "^0.25.0" + "scheduler": "^0.27.0" }, "peerDependencies": { - "react": "^19.0.0" + "react": "^19.2.4" } }, "node_modules/react-dropzone": { @@ -5169,6 +5598,30 @@ "node": ">=4" } }, + "node_modules/restore-cursor": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/restore-cursor/-/restore-cursor-4.0.0.tgz", + "integrity": "sha512-I9fPXU9geO9bHOt9pHHOhOkYerIMsmVaWB0rA2AI9ERh/+x/i7MV5HKBNrg+ljO5eoPVgCcnFuRjJ9uH6I/3eg==", + "license": "MIT", + "optional": true, + "dependencies": { + "onetime": "^5.1.0", + "signal-exit": "^3.0.2" + }, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/restore-cursor/node_modules/signal-exit": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz", + "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==", + "license": "ISC", + "optional": true + }, "node_modules/retry": { "version": "0.12.0", "resolved": "https://registry.npmjs.org/retry/-/retry-0.12.0.tgz", @@ -5311,9 +5764,10 @@ "optional": true }, "node_modules/scheduler": { - "version": "0.25.0", - "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.25.0.tgz", - "integrity": "sha512-xFVuu11jh+xcO7JOAGJNOXld8/TcEHK/4CituBUeUb5hqxJLj9YuemAEuvm9gQ/+pgXYfbQuqAkiYu+u7YEsNA==" + "version": "0.27.0", + "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.27.0.tgz", + "integrity": "sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q==", + "license": "MIT" }, "node_modules/semver": { "version": "7.7.3", @@ -5466,6 +5920,39 @@ "simple-concat": "^1.0.0" } }, + "node_modules/slice-ansi": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/slice-ansi/-/slice-ansi-8.0.0.tgz", + "integrity": "sha512-stxByr12oeeOyY2BlviTNQlYV5xOj47GirPr4yA1hE9JCtxfQN0+tVbkxwCtYDQWhEKWFHsEK48ORg5jrouCAg==", + "license": "MIT", + "optional": true, + "dependencies": { + "ansi-styles": "^6.2.3", + "is-fullwidth-code-point": "^5.1.0" + }, + "engines": { + "node": ">=20" + }, + "funding": { + "url": "https://github.com/chalk/slice-ansi?sponsor=1" + } + }, + "node_modules/slice-ansi/node_modules/is-fullwidth-code-point": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-5.1.0.tgz", + "integrity": "sha512-5XHYaSyiqADb4RnZ1Bdad6cPp8Toise4TzEjcOYDHZkTCbKgiUl7WTUCpNWHuxmDt91wnsZBc9xinNzopv3JMQ==", + "license": "MIT", + "optional": true, + "dependencies": { + "get-east-asian-width": "^1.3.1" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/smart-buffer": { "version": "4.2.0", "resolved": "https://registry.npmjs.org/smart-buffer/-/smart-buffer-4.2.0.tgz", @@ -5599,6 +6086,29 @@ "node": ">=8" } }, + "node_modules/stack-utils": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/stack-utils/-/stack-utils-2.0.6.tgz", + "integrity": "sha512-XlkWvfIm6RmsWtNJx+uqtKLS8eqFbxUg0ZzLXqY0caEy9l7hruX8IpiDnjsLavoBgqCCR71TqWO8MaXYheJ3RQ==", + "license": "MIT", + "optional": true, + "dependencies": { + "escape-string-regexp": "^2.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/stack-utils/node_modules/escape-string-regexp": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-2.0.0.tgz", + "integrity": "sha512-UpzcLCXolUWcNu5HtVMHYdXJjArjsF9C0aNnquZYY4uW/Vu0miy5YoWvbV345HauVvcAUnpRuhMMcqTcGOY2+w==", + "license": "MIT", + "optional": true, + "engines": { + "node": ">=8" + } + }, "node_modules/state-local": { "version": "1.0.7", "resolved": "https://registry.npmjs.org/state-local/-/state-local-1.0.7.tgz", @@ -5680,11 +6190,12 @@ } }, "node_modules/strip-ansi": { - "version": "7.1.0", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.0.tgz", - "integrity": "sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ==", + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.2.0.tgz", + "integrity": "sha512-yDPMNjp4WyfYBkHnjIRLfca1i6KMyGCtsVgoKe/z1+6vukgaENdgGBZt+ZmKPc4gavvEZ5OgHfHdrazhgNyG7w==", + "license": "MIT", "dependencies": { - "ansi-regex": "^6.0.1" + "ansi-regex": "^6.2.2" }, "engines": { "node": ">=12" @@ -5837,6 +6348,19 @@ "resolved": "https://registry.npmjs.org/tabbable/-/tabbable-6.2.0.tgz", "integrity": "sha512-Cat63mxsVJlzYvN51JmVXIgNoUokrIaT2zLclCXjRd8boZ0004U4KCs/sToJ75C6sdlByWxpYnb5Boif1VSFew==" }, + "node_modules/tagged-tag": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/tagged-tag/-/tagged-tag-1.0.0.tgz", + "integrity": "sha512-yEFYrVhod+hdNyx7g5Bnkkb0G6si8HJurOoOEgC8B/O0uXLHlaey/65KRv6cuWBNhBgHKAROVpc7QyYqE5gFng==", + "license": "MIT", + "optional": true, + "engines": { + "node": ">=20" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/tailwindcss": { "version": "3.4.17", "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-3.4.17.tgz", @@ -5933,6 +6457,19 @@ "node": ">=8" } }, + "node_modules/terminal-size": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/terminal-size/-/terminal-size-4.0.1.tgz", + "integrity": "sha512-avMLDQpUI9I5XFrklECw1ZEUPJhqzcwSWsyyI8blhRLT+8N1jLJWLWWYQpB2q2xthq8xDvjZPISVh53T/+CLYQ==", + "license": "MIT", + "optional": true, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/text-decoder": { "version": "1.2.3", "resolved": "https://registry.npmjs.org/text-decoder/-/text-decoder-1.2.3.tgz", @@ -6163,6 +6700,22 @@ "node": "*" } }, + "node_modules/type-fest": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-5.5.0.tgz", + "integrity": "sha512-PlBfpQwiUvGViBNX84Yxwjsdhd1TUlXr6zjX7eoirtCPIr08NAmxwa+fcYBTeRQxHo9YC9wwF3m9i700sHma8g==", + "license": "(MIT OR CC0-1.0)", + "optional": true, + "dependencies": { + "tagged-tag": "^1.0.0" + }, + "engines": { + "node": ">=20" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/typescript": { "version": "5.7.3", "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.7.3.tgz", @@ -6338,6 +6891,39 @@ "node": ">=8" } }, + "node_modules/widest-line": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/widest-line/-/widest-line-6.0.0.tgz", + "integrity": "sha512-U89AsyEeAsyoF0zVJBkG9zBgekjgjK7yk9sje3F4IQpXBJ10TF6ByLlIfjMhcmHMJgHZI4KHt4rdNfktzxIAMA==", + "license": "MIT", + "optional": true, + "dependencies": { + "string-width": "^8.1.0" + }, + "engines": { + "node": ">=20" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/widest-line/node_modules/string-width": { + "version": "8.2.0", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-8.2.0.tgz", + "integrity": "sha512-6hJPQ8N0V0P3SNmP6h2J99RLuzrWz2gvT7VnK5tKvrNqJoyS9W4/Fb8mo31UiPvy00z7DQXkP2hnKBVav76thw==", + "license": "MIT", + "optional": true, + "dependencies": { + "get-east-asian-width": "^1.5.0", + "strip-ansi": "^7.1.2" + }, + "engines": { + "node": ">=20" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/wrap-ansi": { "version": "8.1.0", "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-8.1.0.tgz", @@ -6428,6 +7014,28 @@ "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", "license": "ISC" }, + "node_modules/ws": { + "version": "8.20.0", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.20.0.tgz", + "integrity": "sha512-sAt8BhgNbzCtgGbt2OxmpuryO63ZoDk/sqaB/znQm94T4fCEsy/yV+7CdC1kJhOU9lboAEU7R3kquuycDoibVA==", + "license": "MIT", + "optional": true, + "engines": { + "node": ">=10.0.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": ">=5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } + } + }, "node_modules/xtend": { "version": "4.0.2", "resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz", @@ -6540,6 +7148,13 @@ "node": ">=6" } }, + "node_modules/yoga-layout": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/yoga-layout/-/yoga-layout-3.2.1.tgz", + "integrity": "sha512-0LPOt3AxKqMdFBZA3HBAt/t/8vIKq7VaQYbuA8WxCgung+p9TVyKRYdpvCb80HcdTN2NkbIKbhNwKUfm3tQywQ==", + "license": "MIT", + "optional": true + }, "node_modules/zip-stream": { "version": "6.0.1", "resolved": "https://registry.npmjs.org/zip-stream/-/zip-stream-6.0.1.tgz", diff --git a/ui/package.json b/ui/package.json index 19e817ce..62524ce2 100644 --- a/ui/package.json +++ b/ui/package.json @@ -22,8 +22,8 @@ "next": "^15.5.9", "node-cache": "^5.1.2", "prisma": "^6.3.1", - "react": "^19.0.0", - "react-dom": "^19.0.0", + "react": "^19.2.0", + "react-dom": "^19.2.0", "react-dropzone": "^14.3.5", "react-global-hooks": "^1.3.5", "react-icons": "^5.5.0", @@ -47,5 +47,8 @@ "ts-node-dev": "^2.0.0", "typescript": "^5" }, + "optionalDependencies": { + "macstats": "^4.2.0" + }, "prettier": "prettier-basic" } diff --git a/ui/src/app/api/cpu/route.ts b/ui/src/app/api/cpu/route.ts index 0505efb9..61fd2daf 100644 --- a/ui/src/app/api/cpu/route.ts +++ b/ui/src/app/api/cpu/route.ts @@ -1,20 +1,57 @@ import { NextResponse } from 'next/server'; import si from 'systeminformation'; +import { createRequire } from 'module'; +import os from 'os'; import { CpuInfo } from '@/types'; +const isMac = os.platform() === 'darwin'; + export async function GET() { try { const cpuInfoRaw = await si.cpu(); - const memoryData = await si.mem(); - let cpuInfo: CpuInfo = { - name: `${cpuInfoRaw.manufacturer} ${cpuInfoRaw.brand}`, - cores: cpuInfoRaw.cores, - temperature: (await si.cpuTemperature()).main || 0, - totalMemory: memoryData.total / (1024 * 1024), - availableMemory: memoryData.available / (1024 * 1024), - freeMemory: memoryData.free / (1024 * 1024), - currentLoad: (await si.currentLoad()).currentLoad || 0, - }; + let cpuInfo: CpuInfo; + + if (isMac) { + try { + const nativeRequire = createRequire(import.meta.url); + const ms = nativeRequire('macstats') as any; + const ramData = ms.getRAMUsageSync(); + const cpuData = ms.getCpuDataSync(); + + cpuInfo = { + name: `${cpuInfoRaw.manufacturer} ${cpuInfoRaw.brand}`, + cores: cpuInfoRaw.cores, + temperature: cpuData.temperature || 0, + totalMemory: ramData.total / (1024 * 1024), + availableMemory: ramData.free / (1024 * 1024), + freeMemory: ramData.free / (1024 * 1024), + currentLoad: (await si.currentLoad()).currentLoad || 0, + }; + } catch { + // Fallback to systeminformation if macstats fails + const memoryData = await si.mem(); + cpuInfo = { + name: `${cpuInfoRaw.manufacturer} ${cpuInfoRaw.brand}`, + cores: cpuInfoRaw.cores, + temperature: (await si.cpuTemperature()).main || 0, + totalMemory: memoryData.total / (1024 * 1024), + availableMemory: memoryData.available / (1024 * 1024), + freeMemory: memoryData.free / (1024 * 1024), + currentLoad: (await si.currentLoad()).currentLoad || 0, + }; + } + } else { + const memoryData = await si.mem(); + cpuInfo = { + name: `${cpuInfoRaw.manufacturer} ${cpuInfoRaw.brand}`, + cores: cpuInfoRaw.cores, + temperature: (await si.cpuTemperature()).main || 0, + totalMemory: memoryData.total / (1024 * 1024), + availableMemory: memoryData.available / (1024 * 1024), + freeMemory: memoryData.free / (1024 * 1024), + currentLoad: (await si.currentLoad()).currentLoad || 0, + }; + } return NextResponse.json(cpuInfo); } catch (error) { diff --git a/ui/src/app/api/gpu/route.ts b/ui/src/app/api/gpu/route.ts index 54255848..53f06fd9 100644 --- a/ui/src/app/api/gpu/route.ts +++ b/ui/src/app/api/gpu/route.ts @@ -1,15 +1,140 @@ import { NextResponse } from 'next/server'; -import { exec } from 'child_process'; +import { exec, execSync } from 'child_process'; import { promisify } from 'util'; +import { createRequire } from 'module'; import os from 'os'; const execAsync = promisify(exec); +interface MacGpuResult { + name: string; + memUsed: number; + memTotal: number; + gpuLoad: number; + temperature: number; + fanSpeed: number; + powerDraw: number; +} + +async function getMacGpuInfo(): Promise { + try { + const memoryTotal = os.totalmem() / (1024 * 1024); + + // Get GPU name and core count from system_profiler + let gpuName = 'Apple GPU'; + try { + const spOut = execSync( + 'system_profiler SPDisplaysDataType 2>/dev/null | grep -E "Chipset Model|Total Number of Cores"', + { encoding: 'utf-8', timeout: 5000 }, + ); + const nameMatch = spOut.match(/Chipset Model:\s*(.+)/); + const coresMatch = spOut.match(/Total Number of Cores:\s*(\d+)/); + if (nameMatch) { + gpuName = nameMatch[1].trim(); + if (coresMatch) { + gpuName += ` GPU (${coresMatch[1]} cores)`; + } + } + } catch { + // fallback to generic name + } + + let temperature = 0; + let gpuLoad = 0; + let fanSpeed = 0; + let powerDraw = 0; + let memUsed = 0; + let memTotal = memoryTotal; + + try { + // Use createRequire to hide from webpack static analysis so it doesn't fail on non-mac platforms + const nativeRequire = createRequire(import.meta.url); + const ms = nativeRequire('macstats') as any; + + try { + const gpuData = ms.getGpuDataSync(); + temperature = gpuData.temperature || 0; + gpuLoad = gpuData.usage || 0; + } catch { + // ignore + } + + try { + const fanData = ms.getFanDataSync(); + const fanKeys = Object.keys(fanData); + if (fanKeys.length > 0) { + fanSpeed = fanData[fanKeys[0]].rpm || 0; + } + } catch { + // ignore + } + + try { + const powerData = ms.getPowerDataSync(); + powerDraw = powerData.gpu || 0; + } catch { + // ignore + } + + try { + const ramData = ms.getRAMUsageSync(); + memUsed = ramData.used / (1024 * 1024); + memTotal = ramData.total / (1024 * 1024); + } catch { + // ignore + } + } catch (error) { + console.warn('macstats not available:', error); + } + + return { name: gpuName, memUsed, memTotal, gpuLoad, temperature, fanSpeed, powerDraw }; + } catch { + return null; + } +} + export async function GET() { try { // Get platform const platform = os.platform(); const isWindows = platform === 'win32'; + const isMac = platform === 'darwin'; + + if (isMac) { + const macGpu = await getMacGpuInfo(); + if (macGpu) { + return NextResponse.json({ + hasNvidiaSmi: false, + isMac: true, + gpus: [ + { + index: 0, + name: macGpu.name, + driverVersion: 'macOS', + temperature: Math.round(macGpu.temperature), + utilization: { + gpu: macGpu.gpuLoad, + memory: macGpu.memTotal > 0 ? Math.round((macGpu.memUsed / macGpu.memTotal) * 100) : 0, + }, + memory: { + total: Math.round(macGpu.memTotal), + free: Math.round(macGpu.memTotal - macGpu.memUsed), + used: Math.round(macGpu.memUsed), + }, + power: { draw: macGpu.powerDraw, limit: 0 }, + clocks: { graphics: 0, memory: 0 }, + fan: { speed: macGpu.fanSpeed }, + }, + ], + }); + } + return NextResponse.json({ + hasNvidiaSmi: false, + isMac: true, + gpus: [], + error: 'Could not read Mac GPU stats', + }); + } // Check if nvidia-smi is available const hasNvidiaSmi = await checkNvidiaSmi(isWindows); @@ -17,6 +142,7 @@ export async function GET() { if (!hasNvidiaSmi) { return NextResponse.json({ hasNvidiaSmi: false, + isMac: false, gpus: [], error: 'nvidia-smi not found or not accessible', }); @@ -34,6 +160,7 @@ export async function GET() { return NextResponse.json( { hasNvidiaSmi: false, + isMac: false, gpus: [], error: `Failed to fetch GPU stats: ${error instanceof Error ? error.message : String(error)}`, }, @@ -121,3 +248,4 @@ async function getGpuStats(isWindows: boolean) { return gpus; } + diff --git a/ui/src/app/api/jobs/route.ts b/ui/src/app/api/jobs/route.ts index 56ca8501..9a03965c 100644 --- a/ui/src/app/api/jobs/route.ts +++ b/ui/src/app/api/jobs/route.ts @@ -1,5 +1,6 @@ import { NextResponse } from 'next/server'; import { PrismaClient } from '@prisma/client'; +import { isMac } from '@/helpers/basic'; const prisma = new PrismaClient(); @@ -28,7 +29,12 @@ export async function GET(request: Request) { export async function POST(request: Request) { try { const body = await request.json(); - const { id, name, job_config, gpu_ids } = body; + const { id, name, job_config } = body; + let gpu_ids: string = body.gpu_ids; + + if (isMac()) { + gpu_ids = "mps"; + } if (id) { // Update existing training diff --git a/ui/src/app/jobs/new/SimpleJob.tsx b/ui/src/app/jobs/new/SimpleJob.tsx index 64742773..89b8dbb0 100644 --- a/ui/src/app/jobs/new/SimpleJob.tsx +++ b/ui/src/app/jobs/new/SimpleJob.tsx @@ -19,6 +19,7 @@ import SampleControlImage from '@/components/SampleControlImage'; import { FlipHorizontal2, FlipVertical2 } from 'lucide-react'; import { handleModelArchChange } from './utils'; import { IoFlaskSharp } from 'react-icons/io5'; +import { isMac } from '@/helpers/basic'; type Props = { jobConfig: JobConfig; @@ -146,6 +147,8 @@ export default function SimpleJob({ return newQuantizationOptions; }, [modelArch]); + const showGPUSelect = !isMac(); + return ( <>
- setGpuIDs(value)} - options={gpuList.map((gpu: any) => ({ value: `${gpu.index}`, label: `GPU #${gpu.index}` }))} - /> + {showGPUSelect && ( + setGpuIDs(value)} + options={gpuList.map((gpu: any) => ({ value: `${gpu.index}`, label: `GPU #${gpu.index}` }))} + /> + )} {disableSections.includes('trigger_word') ? null : ( setJobConfig(value, 'config.process[0].model.model_kwargs.match_target_res')} /> )} - {modelArch?.additionalSections?.includes('model.layer_offloading') && ( + {modelArch?.additionalSections?.includes('model.layer_offloading') && !isMac() && ( <> { use_ui_logger: true, }; } + if (isMac()) { + jobConfig.config.process[0].device = 'mps'; + } + return jobConfig; }; diff --git a/ui/src/app/jobs/new/page.tsx b/ui/src/app/jobs/new/page.tsx index cd960604..f700a568 100644 --- a/ui/src/app/jobs/new/page.tsx +++ b/ui/src/app/jobs/new/page.tsx @@ -35,7 +35,7 @@ export default function TrainingForm() { const [datasetOptions, setDatasetOptions] = useState<{ value: string; label: string }[]>([]); const [showAdvancedView, setShowAdvancedView] = useState(false); - const [jobConfig, setJobConfig] = useNestedState(objectCopy(defaultJobConfig)); + const [jobConfig, setJobConfig] = useNestedState(objectCopy(migrateJobConfig(defaultJobConfig))); const [status, setStatus] = useState<'idle' | 'saving' | 'success' | 'error'>('idle'); const fileInputRef = useRef(null); diff --git a/ui/src/app/layout.tsx b/ui/src/app/layout.tsx index e770fade..1167b5fe 100644 --- a/ui/src/app/layout.tsx +++ b/ui/src/app/layout.tsx @@ -7,6 +7,7 @@ import ConfirmModal from '@/components/ConfirmModal'; import { Suspense } from 'react'; import AuthWrapper from '@/components/AuthWrapper'; import DocModal from '@/components/DocModal'; +import os from 'os'; export const dynamic = 'force-dynamic'; @@ -21,12 +22,15 @@ export default function RootLayout({ children }: { children: React.ReactNode }) // Check if the AI_TOOLKIT_AUTH environment variable is set const authRequired = process.env.AI_TOOLKIT_AUTH ? true : false; + const platform = os.platform(); + return ( +