Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
23 changes: 20 additions & 3 deletions .github/workflows/pypi.yml
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,9 @@ jobs:

- uses: ./.github/actions/stamp-version

- name: Install SDL2 (live microphone transcription)
run: brew install sdl2

- name: Build native library
run: |
pip install -e python/
Expand All @@ -48,9 +51,22 @@ jobs:
- name: Build and repair wheel
working-directory: python
run: |
pip install build delocate
pip install build delocate wheel
python -m build --wheel -C="--build-option=--plat-name=macosx_14_0_arm64"
delocate-wheel --require-archs arm64 -w wheelhouse -v dist/*.whl
env -u MACOSX_DEPLOYMENT_TARGET delocate-wheel --require-archs arm64 -w wheelhouse dist/*.whl
whl="$(ls "$PWD"/wheelhouse/*.whl)"
work="$(mktemp -d)"
python -m wheel unpack "$whl" -d "$work"
pkg="$(ls -d "$work"/*/)"
cp "$(brew --prefix sdl3)/lib/libSDL3.0.dylib" "${pkg}cactus/.dylibs/libSDL3.dylib"
chmod +w "${pkg}cactus/.dylibs/libSDL3.dylib"
for lib in "${pkg}cactus/.dylibs/libSDL2-2.0.0.dylib" "${pkg}cactus/.dylibs/libSDL3.dylib"; do
vtool -set-build-version macos 14.0 14.0 -replace -output "$lib" "$lib"
codesign -f -s - "$lib"
done
sed -i '' -E 's/macosx_[0-9]+_[0-9]+_arm64/macosx_14_0_arm64/' "$(ls -d "${pkg}"*.dist-info)/WHEEL"
rm "$whl"
python -m wheel pack "$pkg" -d "$PWD/wheelhouse"

- uses: actions/upload-artifact@v4
with:
Expand All @@ -67,7 +83,8 @@ jobs:

- name: Provision toolchain
run: |
dnf install -y libcurl-devel openssl-devel
dnf install -y epel-release
dnf install -y libcurl-devel openssl-devel SDL2-devel pkgconfig
echo "/opt/python/cp312-cp312/bin" >> "$GITHUB_PATH"

- uses: ./.github/actions/stamp-version
Expand Down
43 changes: 31 additions & 12 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ jobs:
version=$(python3 -c "raw='${raw}'; print(raw + '.0' if raw.count('.') == 1 else raw)")
echo "version=$version" >> "$GITHUB_OUTPUT"

- name: Resolve sdist URL and SHA256 from PyPI
- name: Resolve platform wheel URLs and SHA256 from PyPI
id: pypi
env:
VERSION: ${{ steps.ver.outputs.version }}
Expand All @@ -47,44 +47,63 @@ jobs:
sleep 30
done
[ -n "$json" ] || { echo "PyPI metadata not available for ${PACKAGE} ${VERSION}"; exit 1; }
url=$(echo "$json" | jq -er '.urls[] | select(.packagetype == "sdist") | .url')
sha=$(echo "$json" | jq -er '.urls[] | select(.packagetype == "sdist") | .digests.sha256')
echo "url=$url" >> "$GITHUB_OUTPUT"
echo "sha=$sha" >> "$GITHUB_OUTPUT"
pick() { echo "$json" | jq -er --arg re "$1" '.urls[] | select(.packagetype=="bdist_wheel" and (.filename|test($re))) | .'"$2"; }
{
echo "macos_url=$(pick macosx url)"
echo "macos_sha=$(pick macosx digests.sha256)"
echo "linux_url=$(pick manylinux url)"
echo "linux_sha=$(pick manylinux digests.sha256)"
} >> "$GITHUB_OUTPUT"

- name: Update tap formula
env:
HOMEBREW_TAP_TOKEN: ${{ secrets.HOMEBREW_TAP_TOKEN }}
URL: ${{ steps.pypi.outputs.url }}
SHA: ${{ steps.pypi.outputs.sha }}
VERSION: ${{ steps.ver.outputs.version }}
MACOS_URL: ${{ steps.pypi.outputs.macos_url }}
MACOS_SHA: ${{ steps.pypi.outputs.macos_sha }}
LINUX_URL: ${{ steps.pypi.outputs.linux_url }}
LINUX_SHA: ${{ steps.pypi.outputs.linux_sha }}
run: |
git clone "https://x-access-token:${HOMEBREW_TAP_TOKEN}@github.com/${TAP}.git" tap
mkdir -p tap/Formula
cat > tap/Formula/cactus.rb <<EOF
class Cactus < Formula
desc "On-device AI inference"
homepage "https://cactuscompute.com"
url "${URL}"
sha256 "${SHA}"
version "${VERSION}"
license "Cactus Compute License"
include Language::Python::Virtualenv
depends_on "python@3.12"
depends_on macos: :sonoma
depends_on arch: :arm64

on_macos do
url "${MACOS_URL}"
sha256 "${MACOS_SHA}"
end

on_linux do
url "${LINUX_URL}"
sha256 "${LINUX_SHA}"
end

livecheck do
url "https://pypi.org/pypi/${PACKAGE}/json"
regex(/"version":\s*"(\d+(?:\.\d+)+)"/i)
end

def install
venv = virtualenv_create(libexec, "python3.12")
venv.pip_install "${PACKAGE}==#{version}"
virtualenv_create(libexec, "python3.12")
wheel = Dir[buildpath/"*.whl"].first
odie "no wheel found in build directory" if wheel.nil?
system Formula["python@3.12"].opt_bin/"python3.12", "-m", "pip",
"--python=#{libexec}/bin/python", "install", "--no-deps", wheel
bin.install_symlink libexec/"bin/cactus"
end

test do
assert_equal "cactus #{version}\n", shell_output("#{bin}/cactus --version")
assert_match "libcactus_engine",
Dir[libexec/"lib/python*/site-packages/cactus/bindings/lib/*"].join
end
end
EOF
Expand Down
2 changes: 1 addition & 1 deletion CACTUS_VERSION
Original file line number Diff line number Diff line change
@@ -1 +1 @@
v2.0
v2.0.1
4 changes: 2 additions & 2 deletions cactus-engine/tests/transcribe.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -337,8 +337,8 @@ static int run_live_transcription(cactus_model_t model, const std::string& langu
#else

static int run_live_transcription(cactus_model_t, const std::string&) {
std::cerr << "Live microphone transcription requires SDL2. Install SDL2 and rebuild, "
"or pass an audio file (brew install sdl2 / apt-get install libsdl2-dev).\n";
std::cerr << "Live microphone transcription is unavailable in this build.\n"
"Transcribe an audio file instead: cactus transcribe <model> --file audio.wav\n";
return 1;
}

Expand Down
16 changes: 16 additions & 0 deletions python/cactus/cli/common.py
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,22 @@ def mask_key(key):
return key[:4] + "..." + key[-4:] if len(key) >= 8 else "***"


def convert_toolchain_error():
"""Message if the model-conversion toolchain (torch/transformers) is missing, else None."""
import importlib.util
missing = [m for m in ("torch", "transformers")
if importlib.util.find_spec(m) is None]
if not missing:
return None
return (
"the model-conversion toolchain is not installed (missing: "
+ ", ".join(missing) + ").\n"
" Converting a model from source needs it. Either:\n"
" - use a prebuilt model: cactus run <model> or cactus download <model>\n"
" - install the toolchain: pip install \"cactus-compute[convert]\""
)


BIN_DIR = SCRIPT_DIR.parent / "bin"


Expand Down
2 changes: 1 addition & 1 deletion python/cactus/cli/compile.py
Original file line number Diff line number Diff line change
Expand Up @@ -170,7 +170,7 @@ def cmd_build(args):
print_color(YELLOW, "SDL2 not found - voice input disabled")
print_color(YELLOW, "Install SDL2 to enable voice input: brew install sdl2 (macOS)")

rc = build_binary("run", lib_path, sdl2=sdl2)
rc = build_binary("run", lib_path, sdl2=([], []))
if rc != 0:
return rc
rc = build_binary("transcribe", lib_path, sdl2=sdl2)
Expand Down
8 changes: 8 additions & 0 deletions python/cactus/cli/model.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,10 @@

def _convert_from_source(model_id, *, bits, token, weights_dir, skip_model_load=False):
"""Download from HuggingFace and run CQ conversion."""
from .common import convert_toolchain_error
err = convert_toolchain_error()
if err:
raise RuntimeError(err)
print_color(YELLOW, f"Converting {model_id} from HuggingFace source...")
from ..convert.cli import main as cq_main

Expand Down Expand Up @@ -253,6 +257,10 @@ def prepare_bundle(args, *, model_id=None, transpile=None, prebuilt=True,
def ensure_bundle(model_id, *, bits=4, token=None,
reconvert=False, output_dir=None, transpile=None,
skip_model_load=False):
from .common import convert_toolchain_error
err = convert_toolchain_error()
if err:
raise RuntimeError(err)
from .download import get_bundle_dir
from .transpile import run_transpile
from cactus.transpile.component_plan import infer_component_plan_from_output
Expand Down
6 changes: 6 additions & 0 deletions python/cactus/cli/transpile.py
Original file line number Diff line number Diff line change
Expand Up @@ -33,8 +33,14 @@ def _prepend_python_path(env):

def run_transpile(model_id, *, extra_args, execute_after_transpile=False,
allow_unconverted_weights=False):
from .common import convert_toolchain_error
from .runtime import ensure_python_runtime_library

err = convert_toolchain_error()
if err:
print_color(RED, f"Error: {err}")
return 1

extra_args = list(extra_args or [])
command = [sys.executable, "-m", "cactus.transpile.hf_model", "--model-id", model_id]
default_weights_dir = get_weights_dir(model_id)
Expand Down
14 changes: 8 additions & 6 deletions python/pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -28,20 +28,22 @@ classifiers = [
"Topic :: Scientific/Engineering :: Image Recognition",
]
dependencies = [
"torch>=2.8.0,<3",
"transformers==5.5.4",
"numpy>=1.26.0,<3",
"huggingface-hub>=1.5.0,<2.0",
"scipy>=1.13",
"Pillow>=11.0.0,<12",
"torchvision>=0.23.0,<1",
"sentencepiece>=0.2.0",
]

[project.optional-dependencies]
dev = ["pytest>=8.0", "httpx>=0.25.0", "einops>=0.7"]
lora = ["peft>=0.15"]
serve = ["fastapi>=0.100.0", "uvicorn>=0.20.0", "python-multipart>=0.0.5"]
convert = [
"torch>=2.8.0,<3",
"torchvision>=0.23.0,<1",
"transformers==5.5.4",
"scipy>=1.13",
"Pillow>=11.0.0,<12",
"sentencepiece>=0.2.0",
]

[project.urls]
Homepage = "https://cactuscompute.com"
Expand Down
Loading