Add Mac OS support for Apple Silicon (#770)

* Made an install script and auto updates env for mac

* GPU sensors and initial training working for MAC. Still WIP.

* Switch dataloader to single threaded until I can work around some mac pickeling issues.

* Get quantization working on mac

* Fix mac exclusive imports so they don't break other builds.

* Add mac instructions to the UI
This commit is contained in:
Jaret Burkett 2026-03-30 09:37:47 -06:00 committed by GitHub
parent bc47fd6755
commit 171535833a
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
25 changed files with 1072 additions and 71 deletions

2
.gitignore vendored
View File

@ -122,6 +122,8 @@ celerybeat.pid
# Environments
.env
.venv
.python
.node
env/
venv/
ENV/

View File

@ -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

View File

@ -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([

View File

@ -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):

166
run_mac.zsh Executable file
View File

@ -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

View File

@ -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()

View File

@ -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)

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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(),
}

View File

@ -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,
},

661
ui/package-lock.json generated
View File

@ -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",

View File

@ -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"
}

View File

@ -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) {

View File

@ -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<MacGpuResult | null> {
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;
}

View File

@ -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

View File

@ -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 (
<>
<form
@ -171,13 +174,15 @@ export default function SimpleJob({
disabled={runId !== null}
required
/>
<SelectInput
label="GPU ID"
value={`${gpuIDs}`}
docKey="gpuids"
onChange={value => setGpuIDs(value)}
options={gpuList.map((gpu: any) => ({ value: `${gpu.index}`, label: `GPU #${gpu.index}` }))}
/>
{showGPUSelect && (
<SelectInput
label="GPU ID"
value={`${gpuIDs}`}
docKey="gpuids"
onChange={value => setGpuIDs(value)}
options={gpuList.map((gpu: any) => ({ value: `${gpu.index}`, label: `GPU #${gpu.index}` }))}
/>
)}
{disableSections.includes('trigger_word') ? null : (
<TextInput
label="Trigger Word"
@ -249,7 +254,7 @@ export default function SimpleJob({
onChange={value => setJobConfig(value, 'config.process[0].model.model_kwargs.match_target_res')}
/>
)}
{modelArch?.additionalSections?.includes('model.layer_offloading') && (
{modelArch?.additionalSections?.includes('model.layer_offloading') && !isMac() && (
<>
<Checkbox
label={

View File

@ -1,3 +1,5 @@
'use client';
import { isMac } from '@/helpers/basic';
import { JobConfig, DatasetConfig, SliderConfig } from '@/types';
export const defaultDatasetConfig: DatasetConfig = {
@ -199,5 +201,9 @@ export const migrateJobConfig = (jobConfig: JobConfig): JobConfig => {
use_ui_logger: true,
};
}
if (isMac()) {
jobConfig.config.process[0].device = 'mps';
}
return jobConfig;
};

View File

@ -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<JobConfig>(objectCopy(defaultJobConfig));
const [jobConfig, setJobConfig] = useNestedState<JobConfig>(objectCopy(migrateJobConfig(defaultJobConfig)));
const [status, setStatus] = useState<'idle' | 'saving' | 'success' | 'error'>('idle');
const fileInputRef = useRef<HTMLInputElement>(null);

View File

@ -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 (
<html lang="en" className="dark">
<head>
<meta name="apple-mobile-web-app-title" content="AI-Toolkit" />
</head>
<body className={inter.className}>
<script dangerouslySetInnerHTML={{ __html: `window.server_platform = "${platform}";` }} />
<ThemeProvider>
<AuthWrapper authRequired={authRequired}>
<div className="flex h-screen bg-gray-950">

View File

@ -98,7 +98,7 @@ const GpuMonitor: React.FC = () => {
);
}
if (!gpuData.hasNvidiaSmi) {
if (!gpuData.hasNvidiaSmi && !gpuData.isMac) {
return (
<div className="bg-yellow-900 border border-yellow-700 text-yellow-300 px-4 py-3 rounded relative" role="alert">
<strong className="font-bold">No NVIDIA GPUs detected!</strong>

View File

@ -14,12 +14,17 @@ interface JobOverviewProps {
}
export default function JobOverview({ job }: JobOverviewProps) {
const gpuIds = useMemo(() => job.gpu_ids.split(',').map(id => parseInt(id)), [job.gpu_ids]);
const gpuIds = useMemo(() => {
if (job.gpu_ids === 'mps') {
return [0]; // For MPS, we can just return a single GPU ID since it's virtualized
}
return job.gpu_ids.split(',').map(id => parseInt(id));
}, [job.gpu_ids]);
const { log, setLog, status: statusLog, refresh: refreshLog } = useJobLog(job.id, 2000);
const logRef = useRef<HTMLDivElement>(null);
// Track whether we should auto-scroll to bottom
const [isScrolledToBottom, setIsScrolledToBottom] = useState(true);
console.log('job.gpu_ids', job.gpu_ids);
const { gpuList, isGPUInfoLoaded } = useGPUInfo(gpuIds, 5000);
const { cpuInfo, isCPUInfoLoaded } = useCPUInfo(5000);
const totalSteps = getTotalSteps(job);

1
ui/src/helpers/basic.ts Normal file
View File

@ -0,0 +1 @@
export const isMac = () => (typeof window !== 'undefined' && (window as any).server_platform === 'darwin') || process.platform === 'darwin';

View File

@ -51,6 +51,7 @@ export interface CpuInfo {
export interface GPUApiResponse {
hasNvidiaSmi: boolean;
isMac: boolean;
gpus: GpuInfo[];
error?: string;
}