Skip to content
Open
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
1 change: 1 addition & 0 deletions .github/workflows/deploy-pages.yml
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ on:
branches: [main]
paths: # redeploy only when web files change
- "index.html"
- "install-macos.sh"
- "assets/**"
- "update/**" # desktop updater manifests (stable.json / beta.json)
- "CNAME"
Expand Down
94 changes: 80 additions & 14 deletions index.html
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,8 @@
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>FreeToken — Bring Frontier to Edge | FlashML</title>
<meta name="description" content="FreeToken by FlashML — 本地大模型运行时与桌面客户端, 前沿智能, 触手可及。Bring Frontier to Edge: download and run local LLMs on your own Windows or Linux machine. Private, offline, free download." />
<meta name="keywords" content="FreeToken, FlashML, local LLM, LLM runtime, run LLM locally, 本地大模型, 本地部署, 大语言模型, desktop app, Windows, Linux" />
<meta name="description" content="FreeToken by FlashML — 本地大模型运行时与桌面客户端, 前沿智能, 触手可及。Bring Frontier to Edge: download and run local LLMs on your own Windows, Linux, or macOS Apple Silicon machine. Private, offline, free download." />
<meta name="keywords" content="FreeToken, FlashML, local LLM, LLM runtime, run LLM locally, 本地大模型, 本地部署, 大语言模型, desktop app, Windows, Linux, macOS, Apple Silicon" />
<link rel="canonical" href="https://www.flashml.ai/" />
<meta property="og:title" content="FreeToken — Bring Frontier to Edge" />
<meta property="og:description" content="Download and run large language models on your own machine — private, offline, free." />
Expand All @@ -19,7 +19,7 @@
<meta property="og:locale:alternate" content="en_US" />
<meta name="twitter:card" content="summary" />
<meta name="twitter:title" content="FreeToken — Bring Frontier to Edge" />
<meta name="twitter:description" content="Download and run large language models on your own machine. Free for Windows &amp; Linux." />
<meta name="twitter:description" content="Download and run large language models on your own machine. Free for Windows, Linux, and macOS Apple Silicon." />
<meta name="twitter:image" content="https://www.flashml.ai/assets/icon.png" />
<script type="application/ld+json">
{
Expand All @@ -30,7 +30,7 @@
"image": "https://www.flashml.ai/assets/icon.png",
"description": "FreeToken is a local-LLM runtime and desktop control panel: download large language models and run them on your own machine — private and offline. Bring Frontier to Edge.",
"applicationCategory": "DeveloperApplication",
"operatingSystem": "Windows, Linux",
"operatingSystem": "Windows, Linux, macOS",
"offers": { "@type": "Offer", "price": "0", "priceCurrency": "USD" },
"downloadUrl": "https://www.flashml.ai/",
"codeRepository": "https://github.com/FlashML-org/FreeToken",
Expand Down Expand Up @@ -524,6 +524,14 @@
}
.card .os-name { font-size: 14px; font-weight: 600; color: var(--text); }
.card .file { font-family: var(--mono); font-size: 11px; color: var(--text-3); word-break: break-all; }
button.card {
appearance: none;
margin: 0;
font: inherit;
color: inherit;
cursor: pointer;
width: 100%;
}

.footer {
height: 44px;
Expand Down Expand Up @@ -652,42 +660,51 @@ <h1 class="wordmark">
// Desktop full package (bundles the whl) — uploaded under fixed filenames so the URLs stay stable.
// Linux ships three formats; the browser can't tell the distro (UA is a generic "X11; Linux"),
// so we default Linux to .deb and let the user pick Arch / AppImage from "All platforms".
// macOS has no .dmg yet: Apple Silicon installs the Metal CLI via install-macos.sh.
const MAC_INSTALL = `curl -fsSL https://www.flashml.ai/install-macos.sh | bash`;
const PLATFORMS = [
{ key: "mac", label: "macOS", kind: "cli", command: MAC_INSTALL, file: "install-macos.sh" },
{ key: "win", label: "Windows", file: "FreeToken-Setup-win-x64.exe" },
{ key: "deb", label: "Ubuntu", file: "freetoken-desktop-amd64.deb" },
{ key: "arch", label: "Arch Linux", file: "freetoken-desktop-x86_64.pkg.tar.zst" },
{ key: "appimage", label: "AppImage", file: "freetoken-desktop-x86_64.AppImage" },
];
const LINUX_KEYS = ["deb", "arch", "appimage"];

// Detect the current OS -> return its default download; anything but Win/Linux -> null.
// Detect the current OS -> return its default download; iOS/Android -> null.
// Linux is marked "linux" and defaults to .deb (Arch / AppImage live in the dropdown).
// Append ?os=win|linux|arch|appimage|none to force a state (handy for local preview).
// Append ?os=mac|win|linux|arch|appimage|none to force a state (handy for local preview).
function detectPlatform() {
const forced = new URLSearchParams(location.search).get("os");
if (forced === "none") return null;
if (forced === "linux") return PLATFORMS.find(p => p.key === "deb"); // Linux default = .deb
if (forced) return PLATFORMS.find(p => p.key === forced) || null;
const ua = navigator.userAgent || "";
if (/Android/i.test(ua)) return null; // mobile
if (/Android|iPhone|iPad|iPod/i.test(ua)) return null; // mobile
if (/Windows|Win32|Win64/i.test(ua)) return PLATFORMS.find(p => p.key === "win");
if (/Macintosh|Mac OS X/i.test(ua)) return PLATFORMS.find(p => p.key === "mac");
if (/Linux|X11/i.test(ua)) return PLATFORMS.find(p => p.key === "deb"); // Android already excluded above
Comment on lines 682 to 686
return null; // macOS / iOS / others
return null;
}
const current = detectPlatform();

// Downloads grouped by OS so the Linux formats sit together (platform names are proper
// nouns, left untranslated). Windows is a single card; Linux shows its three formats.
const GROUPS = [
{ name: "macOS", keys: ["mac"] },
{ name: "Windows", keys: ["win"] },
{ name: "Linux", keys: LINUX_KEYS },
];
const byKey = k => PLATFORMS.find(p => p.key === k);
const cardHtml = p => `
<a class="card" href="${DL}/${p.file}">
<span class="os-name">${p.label}</span>
<span class="file">${p.file}</span>
</a>`;
const cardHtml = p => p.kind === "cli"
? `<button type="button" class="card" data-copy="${p.command.replace(/"/g, "&quot;")}">
<span class="os-name">${p.label}</span>
<span class="file">${p.file}</span>
</button>`
: `<a class="card" href="${DL}/${p.file}">
<span class="os-name">${p.label}</span>
<span class="file">${p.file}</span>
</a>`;
document.getElementById("cards-inner").innerHTML = GROUPS.map(g => {
const items = g.keys.map(byKey).filter(Boolean);
// Fixed 3-column grid across every group so all cards are the same size
Expand Down Expand Up @@ -733,6 +750,8 @@ <h1 class="wordmark">
title: "FreeToken — 前沿智能, 触手可及 | FlashML",
lead: "前沿智能, 触手可及",
download: "下载 FreeToken",
install: "安装 FreeToken",
copied: "已复制安装命令",
unsupported: "暂不支持当前系统",
moreVersions: "其他版本",
platforms: "所有平台",
Expand All @@ -745,6 +764,8 @@ <h1 class="wordmark">
title: "FreeToken — Bring Frontier to Edge | FlashML",
lead: "Bring Frontier to Edge",
download: "Download FreeToken",
install: "Install FreeToken",
copied: "Copied install command",
unsupported: "Not supported on your OS yet",
moreVersions: "Other versions",
platforms: "All platforms",
Expand All @@ -771,13 +792,20 @@ <h1 class="wordmark">
document.getElementById("lead").textContent = t.lead;
document.getElementById("platforms-label").textContent = t.platforms;
const primary = document.getElementById("primary");
if (current) {
if (current && current.kind === "cli") {
primary.classList.remove("is-disabled");
primary.setAttribute("href", "#");
primary.setAttribute("role", "button");
primary.innerHTML = `${t.install} <span class="os">${current.label}</span>`;
} else if (current) {
primary.classList.remove("is-disabled");
primary.removeAttribute("role");
primary.setAttribute("href", `${DL}/${current.file}`);
primary.innerHTML = `${t.download} <span class="os">${current.label}</span>`;
} else {
primary.classList.add("is-disabled");
primary.removeAttribute("href");
primary.removeAttribute("role");
primary.textContent = t.unsupported;
}
if (moreBtn) moreBtn.setAttribute("aria-label", t.moreVersions);
Expand Down Expand Up @@ -829,6 +857,44 @@ <h1 class="wordmark">
setPlatformsOpen(platformsToggle.getAttribute("aria-expanded") !== "true");
});

function copyText(text) {
if (navigator.clipboard && window.isSecureContext) {
return navigator.clipboard.writeText(text);
}
return new Promise((resolve, reject) => {
const ta = document.createElement("textarea");
ta.value = text;
ta.setAttribute("readonly", "");
ta.style.position = "fixed";
ta.style.left = "-9999px";
document.body.appendChild(ta);
ta.select();
const ok = document.execCommand("copy");
ta.remove();
ok ? resolve() : reject(new Error("copy failed"));
});
}
function flashCopied(el) {
const t = I18N[lang];
const prev = el.innerHTML;
el.textContent = t.copied;
setTimeout(() => { el.innerHTML = prev; }, 1600);
}
if (current && current.kind === "cli") {
document.getElementById("primary").addEventListener("click", e => {
e.preventDefault();
copyText(current.command).then(() => flashCopied(e.currentTarget));
});
}
document.getElementById("cards-inner").addEventListener("click", e => {
const btn = e.target.closest("[data-copy]");
if (!btn) return;
copyText(btn.getAttribute("data-copy")).then(() => {
const file = btn.querySelector(".file");
if (file) flashCopied(file);
});
});

applyLang();
</script>
</body>
Expand Down
114 changes: 114 additions & 0 deletions install-macos.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,114 @@
#!/usr/bin/env bash
#
# FreeToken installer for macOS Apple Silicon (Metal backend).
#
# Typical use:
# curl -fsSL https://www.flashml.ai/install-macos.sh | bash
#
# Configurable:
# FREETOKEN_SRC existing engine checkout (skips git clone)
# FREETOKEN_HOME install root (default: ~/.freetoken)
# FREETOKEN_PY_VERSION python for the venv (default: 3.12)
# FREETOKEN_BIN_DIR where to symlink `ft` (default: ~/.local/bin)
# FREETOKEN_REPO git URL (default: https://github.com/FlashML-org/FreeToken.git)
# FREETOKEN_REF git ref to clone (default: HEAD of default branch)
#
set -euo pipefail

FT_HOME="${FREETOKEN_HOME:-$HOME/.freetoken}"
VENV="$FT_HOME/venv"
SRC="${FREETOKEN_SRC:-$FT_HOME/src}"
PY_VERSION="${FREETOKEN_PY_VERSION:-3.12}"
BIN_DIR="${FREETOKEN_BIN_DIR:-$HOME/.local/bin}"
REPO="${FREETOKEN_REPO:-https://github.com/FlashML-org/FreeToken.git}"
FALLBACK_REPO="${FREETOKEN_FALLBACK_REPO:-https://github.com/jasonkneen/FreeToken.git}"
# Default to the Metal branch until it is the repo default. Override with
# FREETOKEN_REF=main after that lands.
REF="${FREETOKEN_REF:-feat/apple-metal-backend}"

die() { printf '[error] %s\n' "$*" >&2; exit 1; }
say() { printf '==> %s\n' "$*"; }

[ "$(uname -s)" = Darwin ] || die "this installer is macOS-only (this is $(uname -s))"
[ "$(uname -m)" = arm64 ] || die "Apple Silicon (arm64) required — this machine is $(uname -m). An Intel/Rosetta shell cannot install mlx."

if command -v uv >/dev/null 2>&1; then
UV="$(command -v uv)"
else
command -v curl >/dev/null 2>&1 || die "need curl to bootstrap uv"
say "bootstrapping uv into $BIN_DIR ..."
mkdir -p "$BIN_DIR"
UV_UNMANAGED_INSTALL="$BIN_DIR" curl -LsSf https://astral.sh/uv/install.sh | sh
UV="$BIN_DIR/uv"
[ -x "$UV" ] || die "uv bootstrap failed — install from https://docs.astral.sh/uv/"
fi
say "uv $($UV --version | awk '{print $2}')"

metal_ready() {
local pp="$1/pyproject.toml"
[ -f "$pp" ] || return 1
grep -Eq "platform_system == ['\"]Linux['\"]" "$pp" && grep -Eq "torch[^;]*;[[:space:]]*platform_system" "$pp"
}

clone_engine() {
local branch="$1"
command -v git >/dev/null 2>&1 || die "need git to clone FreeToken"
[ "$SRC" = "$FT_HOME/src" ] || die "won't clone over FREETOKEN_SRC=$SRC"
mkdir -p "$(dirname "$SRC")"
rm -rf "$SRC"
say "cloning $REPO ($branch) into $SRC"
if git clone --depth 1 --branch "$branch" "$REPO" "$SRC"; then
return 0
fi
say "$REPO has no $branch; trying $FALLBACK_REPO"
rm -rf "$SRC"
git clone --depth 1 --branch "$branch" "$FALLBACK_REPO" "$SRC" \
|| die "could not clone $branch from $REPO or $FALLBACK_REPO"
}

if [ ! -f "$SRC/pyproject.toml" ]; then
clone_engine "$REF"
fi

# The Metal path requires CUDA-only deps to be Linux-marked. A CUDA-only tree
# (today's default branch) would pull NVIDIA torch/flashlib and fail on macOS.
if ! metal_ready "$SRC"; then
if [ -n "${FREETOKEN_SRC:-}" ] && [ "$SRC" = "$FREETOKEN_SRC" ]; then
die "FREETOKEN_SRC=$SRC is CUDA-only and cannot install on macOS. Point it at a Metal-capable checkout, or unset FREETOKEN_SRC to clone $REF."
fi
say "tree at $SRC is CUDA-only; fetching $REF"
clone_engine "$REF"
metal_ready "$SRC" || die "cloned $REF but it is still CUDA-only"
fi

say "creating venv at $VENV (python $PY_VERSION)"
mkdir -p "$FT_HOME"
"$UV" venv "$VENV" --python "$PY_VERSION" --clear
say "installing FreeToken (editable) from $SRC"
"$UV" pip install --python "$VENV" -e "$SRC"
say "installing mlx-lm (Apple Metal engine)"
"$UV" pip install --python "$VENV" mlx-lm

FT_BIN="$VENV/bin/ft"
[ -x "$FT_BIN" ] || die "install finished but $FT_BIN is missing"
mkdir -p "$BIN_DIR"
ln -sf "$FT_BIN" "$BIN_DIR/ft"
say "symlinked $BIN_DIR/ft -> $FT_BIN"

if "$FT_BIN" --help >/dev/null 2>&1; then
say "self-check: ft --help OK"
else
die "self-check failed — inspect with: $FT_BIN --help"
fi

cat <<EOF

FreeToken (Metal) installed.

ft binary $FT_BIN
on PATH as $BIN_DIR/ft (ensure $BIN_DIR is on PATH)

Run:
ft serve --model mlx-community/Qwen3-0.6B-4bit --port 1919

EOF