diff --git a/.agents/preparing-a-release.md b/.agents/preparing-a-release.md new file mode 100644 index 000000000..9f6f9bcfd --- /dev/null +++ b/.agents/preparing-a-release.md @@ -0,0 +1,26 @@ +# Preparing a Release + +A release is not finished when the tag is pushed. The GitHub release, the blog post and the demo clips ship together, because the changelog says what moved and the post and the clips are what make anyone care. + +## What a release must include + +1. **Labels on the merged PRs.** GitHub generates the raw notes from PR labels, so label first, generate second. Wrong labels mean a miscategorised changelog that has to be edited by hand. +2. **`RELEASE_NOTES_vX.Y.Z.md`** at the repository root, in the house style: what changed, why it matters, PR numbers so people can read the diffs. +3. **A blog post under `website/content/blog/`.** One post per release, front matter with `title`, `date`, `author`, `category: "Release"`, `tags`, `summary` and `extracss: ["blog.css"]`. Cover the two or three changes that alter what a user does day to day, not the whole changelog, and link the PR numbers. See `website/content/blog/what-landed-in-localai-4-8.md` for the shape. +4. **Demo clips for the notable features.** Anything visible (a new backend, a UI change, a new endpoint, a measured speedup) gets a short screen recording. Put the file in `website/static/media/`, reference it from the blog post, and reuse it on the marketing pages where it fits. + +A release without a post and without clips is incomplete, in the same way a user-facing code change without a docs update is incomplete. + +## Clip conventions + +- MP4, H.264, no audio track unless the feature is about audio. Keep them short (10 to 30 seconds) and loopable. +- Record the real thing. A clip from the engine's own benchmark suite or a real session, never a mockup. +- Where the change is a speedup, record both sides on the same machine on the same input, so the comparison is honest. +- Name the file after the feature, not the release (`vllm-race.mp4`, not `v4-8-demo.mp4`), so it stays reusable once the release is old. +- The marketing site plays clips with `muted loop playsinline preload="none"` and a `data-lazy` attribute, which the site's IntersectionObserver uses to play and pause them on scroll. Follow that pattern for anything you add. + +## Order of work + +Label the PRs, generate and edit the release notes, cut the draft release, record the clips while the branch is still fresh in your head, then write the post against the notes and the clips. Publishing the release and merging the post should happen on the same day. + +The `creating-localai-releases` skill drives steps 1 to 3 and captures the React UI screenshots that go into the notes. diff --git a/.github/ci/gen-redirects.sh b/.github/ci/gen-redirects.sh new file mode 100755 index 000000000..14dda6f15 --- /dev/null +++ b/.github/ci/gen-redirects.sh @@ -0,0 +1,76 @@ +#!/usr/bin/env bash +# +# Generate client-side redirects for the documentation URLs that used to live at +# the site root. +# +# Until this site existed, the Hugo docs site WAS localai.io, so pages +# were published at /features/..., /getting-started/..., /faq/ and so on. The +# docs now build under /docs/, and GitHub Pages serves static files only: there +# is no server-side rewrite, no .htaccess, no _redirects. The only way to keep +# every published, bookmarked and search-indexed URL alive is to leave a real +# HTML file at the old address that sends the browser to the new one. +# +# Anything the main site already publishes wins: it owns /, /engines/, +# /blog/ and friends, so an existing file is never replaced. +# +# Usage: gen-redirects.sh [base-url] +# public-dir merged output directory (main site with docs/ inside it) +# base-url absolute or root-relative prefix the deployment is served from, +# trailing slash optional (default "/") + +set -euo pipefail + +PUBLIC_DIR=${1:?usage: gen-redirects.sh [base-url]} +BASE_URL=${2:-/} + +# Normalise to exactly one trailing slash so concatenation below is predictable. +BASE_URL="${BASE_URL%/}/" + +DOCS_DIR="${PUBLIC_DIR}/docs" + +if [ ! -d "$DOCS_DIR" ]; then + echo "gen-redirects: no docs output at ${DOCS_DIR}" >&2 + exit 1 +fi + +created=0 +skipped=0 + +# Every .html file is a reachable old URL, not just directory indexes: the +# generated model gallery ships as a bare gallery.html and used to sit at the +# root too. +while IFS= read -r src; do + rel=${src#"$DOCS_DIR"/} + dst="${PUBLIC_DIR}/${rel}" + + if [ -e "$dst" ]; then + skipped=$((skipped + 1)) + continue + fi + + # Link to the directory, not to its index.html, so the redirect target is the + # canonical URL the docs site itself advertises. + target="${BASE_URL}docs/${rel%index.html}" + + mkdir -p "$(dirname "$dst")" + printf '%s' ' + + + +Moved + + + + + +

This page moved to '"$target"'.

+ + +' > "$dst" + + created=$((created + 1)) +done < docs/static/gallery.html - - name: Build site + # Two Hugo sites, one Pages artifact: the main site owns the root, + # the docs site is nested under /docs/. + - name: Build the main site + working-directory: website + run: hugo --minify --baseURL "${{ steps.pages.outputs.base_url }}/" + + - name: Build documentation site working-directory: docs run: | mkdir -p layouts/_default - hugo --minify --baseURL "${{ steps.pages.outputs.base_url }}/" + hugo --minify --baseURL "${{ steps.pages.outputs.base_url }}/docs/" + + - name: Merge documentation into the main site + run: | + mkdir -p website/public/docs + cp -R docs/public/. website/public/docs/ + + # Keeps the pre-split URLs alive; see the script header. + - name: Generate legacy URL redirects + run: .github/ci/gen-redirects.sh website/public "${{ steps.pages.outputs.base_url }}/" - name: Upload artifact uses: actions/upload-pages-artifact@v5 with: - path: docs/public + path: website/public deploy: environment: diff --git a/.gitignore b/.gitignore index 4fb77f49e..3f5287bc8 100644 --- a/.gitignore +++ b/.gitignore @@ -62,6 +62,11 @@ prepare /ggml-metal.metal docs/static/gallery.html +# Hugo build output and lock files (docs/ and website/) +docs/public/ +website/public/ +.hugo_build.lock + # Protobuf generated files *.pb.go *pb2.py diff --git a/AGENTS.md b/AGENTS.md index d997813cd..dd2c79125 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -32,6 +32,7 @@ LocalAI follows the Linux kernel project's [guidelines for AI coding assistants] | [.agents/adding-gallery-models.md](.agents/adding-gallery-models.md) | Adding GGUF models from HuggingFace to the model gallery | | [.agents/localai-assistant-mcp.md](.agents/localai-assistant-mcp.md) | LocalAI Assistant chat modality — adding admin tools to the in-process MCP server, editing skill prompts, keeping REST + MCP + skills in sync | | [.agents/backend-signing.md](.agents/backend-signing.md) | Backend OCI image signing (keyless cosign + sigstore-go) — producer-side CI setup, consumer-side gallery `verification:` block, strict mode (`LOCALAI_REQUIRE_BACKEND_INTEGRITY`), revocation via `not_before` | +| [.agents/preparing-a-release.md](.agents/preparing-a-release.md) | Cutting a release: PR labels, `RELEASE_NOTES_vX.Y.Z.md`, the blog post under `website/content/blog/`, and the demo clips under `website/static/media/` | ## Quick Reference @@ -42,6 +43,7 @@ LocalAI follows the Linux kernel project's [guidelines for AI coding assistants] - **Docs (docs-with-code rule)**: When you change user-facing behavior (API endpoints, CLI flags, config keys, or features), update the corresponding page under `docs/content/` in the SAME change, not as a follow-up. A user-facing change without a matching docs update is incomplete. See also the documentation conventions in [.agents/coding-style.md](.agents/coding-style.md). - **New API endpoints**: LocalAI advertises its capability surface in several independent places — swagger `@Tags`, `/api/instructions` registry, auth `RouteFeatureRegistry`, React UI `capabilities.js`, docs. Read [.agents/api-endpoints-and-auth.md](.agents/api-endpoints-and-auth.md) and follow its checklist — missing any surface means clients, admins, and the UI won't know the endpoint exists. - **Admin endpoints → MCP tool**: every admin endpoint that an admin would manage conversationally (install/list/edit/toggle/upgrade) MUST also be exposed as an MCP tool in `pkg/mcp/localaitools/`. The LocalAI Assistant chat modality and the standalone `local-ai mcp-server` consume that package; drift between REST and MCP is a real risk. Read [.agents/localai-assistant-mcp.md](.agents/localai-assistant-mcp.md) — the `TestToolHTTPRouteMappingComplete` test fails until you wire the new tool and update the route map. +- **Releases ship with a post and clips**: a release is not done at the tag. It needs labelled PRs, `RELEASE_NOTES_vX.Y.Z.md`, a blog post under `website/content/blog/`, and a short demo clip in `website/static/media/` for each notable feature. See [.agents/preparing-a-release.md](.agents/preparing-a-release.md). - **Build**: Inspect `Makefile` and `.github/workflows/` — ask the user before running long builds - **Backend OS coverage**: a new backend must target every OS it can build for, not just Linux. `.github/backend-matrix.yml` has two matrices — `include:` (Linux) and `includeDarwin:` (macOS / Apple Silicon). Most C/C++/GGML and many Python backends build on Darwin too — wire the `includeDarwin` entry + `backend/index.yaml` `metal:` entries, or say in the PR why an OS is unsupported. See the darwin checklist in [.agents/adding-backends.md](.agents/adding-backends.md). - **Gallery variant ranking**: a gallery entry can declare `variants` (alternative builds of the same weights), and LocalAI ranks the ones a host can run by engine preference first, size second. A new backend that should be preferred on some hardware must be listed in `engineNamePreferenceRules` in `pkg/system/capabilities.go`; the sibling `backendBuildTagPreferenceRules` speaks build tags rather than engine names, and using the wrong table matches nothing without erroring. See [.agents/adding-backends.md](.agents/adding-backends.md). diff --git a/Makefile b/Makefile index 699c6fd61..6d64540b8 100644 --- a/Makefile +++ b/Makefile @@ -1548,7 +1548,12 @@ swagger: gen-assets: $(GOCMD) run core/dependencies_manager/manager.go webui_static.yaml core/http/static/assets -## Documentation +## Documentation and website +# The published site is two Hugo sites: website/ owns the root, docs/ is nested +# under /docs/. Serve them separately while editing; use `make site` to get the +# merged tree (including the legacy URL redirects) that GitHub Pages deploys. +SITE_BASE_URL?=http://localhost:8000 + docs/layouts/_default: mkdir -p docs/layouts/_default @@ -1560,12 +1565,30 @@ docs/public: docs/layouts/_default docs/static/gallery.html docs-clean: rm -rf docs/public + rm -rf website/public rm -rf docs/static/gallery.html .PHONY: docs docs: docs/static/gallery.html cd docs && hugo serve +.PHONY: website +website: + cd website && hugo serve + +.PHONY: site +site: docs/static/gallery.html + rm -rf website/public docs/public + cd website && hugo --minify --baseURL "$(SITE_BASE_URL)/" + cd docs && hugo --minify --baseURL "$(SITE_BASE_URL)/docs/" + mkdir -p website/public/docs + cp -R docs/public/. website/public/docs/ + ./.github/ci/gen-redirects.sh website/public "$(SITE_BASE_URL)/" + +.PHONY: site-serve +site-serve: site + cd website/public && python3 -m http.server 8000 + ######################################################## ## Platform-specific builds ######################################################## diff --git a/README.md b/README.md index cbdc44b68..657f43a4b 100644 --- a/README.md +++ b/README.md @@ -238,7 +238,7 @@ Most backends wrap a best-in-class upstream engine. A handful of them are native | [magpie-tts.cpp](https://github.com/mudler/magpie-tts.cpp) | C++/GGML port of NVIDIA's Magpie TTS Multilingual 357M: 22.05 kHz mono text-to-speech in 5 voices and 9+ languages, with the NanoCodec neural codec and tokenizer/G2P embedded in a single GGUF | | [ced.cpp](https://github.com/localai-org/ced.cpp) | C++/GGML port of the CED audio-tagging models: sound-event classification (527-class AudioSet) over REST and the realtime API for live recognition | | [voice-detect.cpp](https://github.com/localai-org/voice-detect.cpp) | Speaker recognition and voice analysis (ECAPA-TDNN, WeSpeaker, ERes2Net, CAM++, wav2vec2 age/gender/emotion), replacing the Python speaker-recognition backend | -| [voxtral-tts.c](https://github.com/mudler/voxtral-tts.c) | Voxtral Realtime 4B speech-to-text in pure C | +| [voxtral-tts.c](https://github.com/mudler/voxtral-tts.c) | Mistral Voxtral-4B-TTS text-to-speech in pure C: 20 preset voices across 9 languages, 24 kHz WAV output, no dependencies beyond libc | | [vibevoice.cpp](https://github.com/mudler/vibevoice.cpp) | Native port of Microsoft VibeVoice for TTS (voice cloning) and long-form ASR with speaker diarization | | [rf-detr.cpp](https://github.com/localai-org/rf-detr.cpp) | Native RF-DETR object detection and instance segmentation | | [locate-anything.cpp](https://github.com/mudler/locate-anything.cpp) | Open-vocabulary object detection and visual grounding (LocateAnything-3B) | diff --git a/docs/content/_index.md b/docs/content/_index.md index e048ba13f..3eebf12b0 100644 --- a/docs/content/_index.md +++ b/docs/content/_index.md @@ -1,86 +1,48 @@ +++ -title = "LocalAI" -description = "The open, modular AI runtime. Run text, vision, voice, image, video, agents, and more on hardware you control." +disableToc = false +title = "LocalAI documentation" +description = "Install LocalAI, run models, and operate it in production." type = "home" +++ -
-
-
-

Open source · MIT licensed

-

One runtime.
Every kind of AI.
Your hardware.

-

LocalAI runs text, vision, speech, sound, images, video, embeddings, reranking, and autonomous agents behind one modular stack-from a CPU laptop to a distributed GPU cluster.

- -
60+ backendsCPU to clusterOpenAI · Anthropic · Ollama · ElevenLabs APIs
-
-
+LocalAI is the open source AI runtime: a small core that speaks the OpenAI and +Anthropic APIs, with each inference backend added only when a model needs it. +It runs text, vision, speech, sound, images, video, embeddings, reranking, and +autonomous agents on hardware you control, from a CPU laptop to a distributed +GPU cluster. -
-

The runtime, not just the endpoint.

Bring the model. Choose the engine. Keep control.

- -
+New here? Read the [Overview]({{% relref "overview" %}}) for what LocalAI is +and how the pieces fit together, then follow the +[Quickstart]({{% relref "getting-started/quickstart" %}}). -
-
-

A small core, not a giant bundle.

-

Backends arrive when the model needs them.

-

LocalAI keeps the core lean. Each backend wraps a best-in-class engine-llama.cpp, vLLM, SGLang, MLX, whisper.cpp, diffusion engines, and many more-as an isolated service pulled on demand.

-
  • Install, update, or remove engines independently.
  • Mix CPU, NVIDIA, AMD, Intel, Apple Silicon, Vulkan, and Jetson.
  • Build your own backend in any language through an open gRPC contract.
- Explore the architecture → -
-
LocalAI's small core connected to independent on-demand model backends
-
+```bash +docker run -ti --name local-ai -p 8080:8080 localai/localai:latest +``` -
-
-

We integrate the best engines. We build new ones, too.

-

Inference work that moves the open ecosystem forward.

-

The LocalAI team develops native C, C++, Rust, and GGML engines when the available stack is too heavy, too closed, or simply does not exist yet.

- See the engines we maintain ↗ -
-
-
Speechparakeet.cppStreaming multilingual ASR
-
Voicevibevoice.cppLong-form TTS and ASR
-
Identityvoice-detect.cppSpeaker recognition and analysis
-
Visionface-detect.cppRecognition and anti-spoofing
-
Perceptionlocate-anything.cppOpen-vocabulary detection
-
Privacyprivacy-filter.cppNative PII detection
-
3Dfree-splatter.cppPose-free reconstruction
-
Quantizationapex-quantMoE-aware GGUF recipes
-
-
+## Sections -
-

Start on one machine. Keep going.

The same runtime from workstation to private AI fabric.

-
-
01Laptop

Run useful models locally, including CPU-only setups.

-
02Team server

Add authentication, API keys, roles, quotas, and usage visibility.

-
03Distributed cluster

Route across workers, fit models across devices, and scale with demand.

-
-
+- **[Getting started]({{% relref "getting-started" %}})** - install LocalAI, + run your first model, call the API, and fix the common startup problems. +- **[Features]({{% relref "features" %}})** - every capability, grouped by + modality: text, agents, audio, vision, image and video, retrieval, + distributed inference, and model management. +- **[Advanced]({{% relref "advanced" %}})** - model configuration, VRAM + management, reverse proxies and TLS, and the rest of the fine-grained + control surface. +- **[Operations]({{% relref "operations" %}})** - running and governing an + instance: middleware, cloud and MITM proxies, backend monitoring. +- **[Reference]({{% relref "reference" %}})** - architecture, CLI flags, the + compatibility table, API and runtime errors, system info, and binaries. +- **[FAQ]({{% relref "faq" %}})** - short answers to the questions that come up + most often. -
-

More than inference

A complete local AI control plane.

-
-
Agents built in

Create agents with MCP tools, skills, memory, RAG, citations, and streamed execution from the UI or API.

-
Realtime by design

Build interruptible voice experiences with WebRTC, streaming STT, LLM output, and TTS.

-
Privacy you can enforce

Keep data on your infrastructure and add PII analysis, redaction, policy middleware, and audit visibility.

-
Models under your control

Discover capabilities, import models, fine-tune, quantize, route, and monitor them in one place.

-
-
+## Also useful -
-

One command to begin

Run your first local AI stack.

-
docker run -ti --name local-ai -p 8080:8080 localai/localai:latest
- -
-
+- **[Integrations]({{% relref "integrations" %}})** - projects and tools built + on top of LocalAI. +- **[News]({{% relref "whats-new" %}})** - where release notes live. +- **[Model gallery](https://models.localai.io)** - browse the models you can + install with one click. +- **[GitHub](https://github.com/mudler/LocalAI)** and + **[Discord](https://discord.gg/uJAeKSAGDy)** - report an issue or ask a + question. diff --git a/docs/content/getting-started/customize-model.md b/docs/content/getting-started/customize-model.md index 7412f4d02..751a2e6fa 100644 --- a/docs/content/getting-started/customize-model.md +++ b/docs/content/getting-started/customize-model.md @@ -2,7 +2,7 @@ disableToc = false title = "Customizing the Model" weight = 5 -url = "/docs/getting-started/customize-model" +url = "/getting-started/customize-model" icon = "rocket_launch" +++ diff --git a/docs/content/overview.md b/docs/content/overview.md index 0e8a03a4c..7c569c09e 100644 --- a/docs/content/overview.md +++ b/docs/content/overview.md @@ -5,7 +5,7 @@ toc = true description = "What is LocalAI?" tags = ["Beginners"] categories = [""] -url = "/docs/overview" +url = "/overview" author = "Ettore Di Giacinto" icon = "info" +++ diff --git a/docs/hugo.toml b/docs/hugo.toml index 4ac94bd26..b6523e7da 100644 --- a/docs/hugo.toml +++ b/docs/hugo.toml @@ -1,4 +1,7 @@ -baseURL = 'https://localai.io/' +# The documentation is a second Hugo site, served under /docs/ of the marketing +# site (see ../website). CI passes the same path via --baseURL, so keeping it +# here means a local build produces the same links as production. +baseURL = 'https://localai.io/docs/' languageCode = 'en-GB' defaultContentLanguage = 'en' diff --git a/docs/static/css/custom.css b/docs/static/css/custom.css deleted file mode 100644 index e2b3a4cfb..000000000 --- a/docs/static/css/custom.css +++ /dev/null @@ -1,100 +0,0 @@ -@import url("./localai-home.css"); - -/* The homepage deliberately fits Relearn's documentation content column. */ -#R-body-inner.home article.home > h1#localai { - display: none; -} - -#R-body-inner.home > .flex-block-wrapper { - max-width: 960px; -} - -/* Relearn scrolls the content pane; keep that behavior without a rail beside the hero. */ -#R-body-inner.home { - overflow-x: clip; - scrollbar-width: none; -} - -#R-body-inner.home::-webkit-scrollbar { - display: none; -} - -.lai-home, -.lai-home > section, -.lai-home header, -.lai-home figure, -.lai-home [class*="__copy"] { - box-sizing: border-box; - min-width: 0; - max-width: 100%; -} - -.lai-home h1, -.lai-home h2, -.lai-home h3, -.lai-home p { - overflow-wrap: anywhere; -} - -.lai-home img { - max-width: 100%; -} - -.lai-home * { - scrollbar-width: none; -} - -.lai-home *::-webkit-scrollbar { - display: none; -} - -.lai-start pre { - overflow: visible !important; - white-space: pre-wrap; - overflow-wrap: anywhere; - word-break: break-word; -} - -.lai-hero { - grid-template-columns: minmax(0, 1fr) !important; - min-height: auto; - padding-block: clamp(3rem, 6vw, 5rem); -} - -.lai-hero__copy { - max-width: 48rem; -} - -.lai-hero h1 { - max-width: 12ch; - font-size: clamp(3rem, 5.5vw, 4.65rem); - text-align: left; -} - -.lai-hero__lede { - max-width: 43rem; -} - -.lai-product-shot { - width: 100%; - max-width: 54rem; - transform: none; -} - -@media (max-width: 47.99rem) { - .lai-home, - .lai-home > section, - .lai-hero, - .lai-hero__copy, - .lai-product-shot { - box-sizing: border-box; - width: 100%; - min-width: 0; - max-width: 100%; - } - - .lai-hero h1 { - max-width: 10ch; - font-size: clamp(2.4rem, 11vw, 3.5rem); - } -} diff --git a/docs/static/css/localai-home.css b/docs/static/css/localai-home.css deleted file mode 100644 index 83b41c787..000000000 --- a/docs/static/css/localai-home.css +++ /dev/null @@ -1,4 +0,0 @@ -.lai-home{--lai-bg:#0d1117;--lai-surface:#131a23;--lai-surface-2:#192330;--lai-line:#29384a;--lai-ink:#edf4fc;--lai-muted:#9aabc0;--lai-blue:#4f8cff;--lai-green:#56d6a4;--lai-amber:#f1b95d;max-width:1180px;margin:0 auto;color:var(--lai-ink)}.lai-home *{box-sizing:border-box}.lai-home h1,.lai-home h2{color:var(--lai-ink);letter-spacing:-.04em;line-height:.96;text-wrap:balance}.lai-home h2{font-size:clamp(2.2rem,5vw,4.4rem)}.lai-home p{color:var(--lai-muted);text-wrap:pretty}.lai-home a{text-decoration:none}.lai-hero{display:grid;min-height:78vh;align-items:center;gap:clamp(2rem,6vw,5.5rem);padding:clamp(4rem,10vw,8rem) 0}.lai-signal{display:flex;align-items:center;gap:.5rem;margin:0 0 1.1rem!important;font-size:.73rem;font-weight:800;letter-spacing:.07em;text-transform:uppercase}.lai-signal span{width:.5rem;height:.5rem;border-radius:50%;background:var(--lai-green);box-shadow:0 0 0 .25rem color-mix(in srgb,var(--lai-green) 15%,transparent)}.lai-hero h1{margin:0 0 1.4rem;font-size:clamp(3.2rem,7vw,5.8rem)}.lai-hero h1 strong{color:var(--lai-blue);font-weight:760}.lai-hero__lede{max-width:42rem;margin:0!important;font-size:clamp(1.03rem,1.5vw,1.25rem);line-height:1.65}.lai-actions{display:flex;flex-wrap:wrap;align-items:center;gap:1rem;margin-top:1.8rem}.lai-button{display:inline-flex;min-height:3.1rem;align-items:center;gap:.75rem;padding:.7rem 1rem;border:1px solid var(--lai-blue);border-radius:7px;background:var(--lai-blue);color:white!important;font-weight:800;transition:transform .18s cubic-bezier(.16,1,.3,1),background .18s ease}.lai-button:hover{background:#70a2ff;transform:translateY(-2px)}.lai-link{color:var(--lai-ink)!important;font-weight:750;text-decoration:underline!important;text-underline-offset:.25rem}.lai-proof{display:flex;flex-wrap:wrap;gap:.55rem 1rem;margin-top:1.6rem}.lai-proof span{display:flex;align-items:center;gap:.35rem;color:var(--lai-muted);font-size:.68rem;font-weight:750}.lai-proof span::before{width:.32rem;height:.32rem;border-radius:50%;background:var(--lai-green);content:""}.lai-product-shot{overflow:hidden;margin:0;border:1px solid var(--lai-line);border-radius:12px;background:var(--lai-surface);transform:rotate(1deg)}.lai-product-shot img{display:block;width:100%;height:auto}.lai-product-shot figcaption{display:flex;align-items:center;justify-content:space-between;gap:.7rem;padding:.75rem .9rem;border-top:1px solid var(--lai-line);color:var(--lai-muted);font-size:.64rem}.lai-product-shot figcaption span{display:flex;align-items:center;gap:.35rem}.lai-product-shot figcaption i{width:.42rem;height:.42rem;border-radius:50%;background:var(--lai-green)} -.lai-breadth,.lai-scale,.lai-platform,.lai-start{padding:clamp(5rem,10vw,8rem) 0;border-top:1px solid var(--lai-line)}.lai-breadth header,.lai-scale header{display:grid;gap:.7rem;margin-bottom:2.5rem}.lai-breadth header p,.lai-scale header p,.lai-architecture__copy>p:first-child,.lai-engines__intro>p:first-child,.lai-platform>div:first-child>p,.lai-start>div>p{margin:0!important;color:var(--lai-green);font-size:.72rem;font-weight:800;letter-spacing:.06em;text-transform:uppercase}.lai-breadth h2,.lai-scale h2,.lai-platform h2,.lai-start h2{max-width:16ch;margin:0}.lai-lanes{display:grid;border-top:1px solid var(--lai-line)}.lai-lanes a{display:grid;grid-template-columns:minmax(6rem,.45fr) minmax(0,1.55fr) auto;align-items:center;gap:1rem;padding:1.1rem .2rem;border-bottom:1px solid var(--lai-line);color:var(--lai-ink)!important;transition:padding .18s ease,background .18s ease}.lai-lanes a:hover{padding-inline:.8rem;background:var(--lai-surface)}.lai-lanes span{color:var(--lai-blue);font-size:.73rem;font-weight:800;text-transform:uppercase}.lai-lanes b{font-size:clamp(.9rem,1.6vw,1.15rem)}.lai-lanes em{color:var(--lai-muted);font-size:.66rem;font-style:normal}.lai-architecture{display:grid;gap:clamp(2.5rem,7vw,6rem);align-items:center;padding:clamp(5rem,11vw,9rem) 0;border-top:1px solid var(--lai-line)}.lai-architecture__copy h2,.lai-engines__intro h2{max-width:14ch;margin:.7rem 0 1.2rem;font-size:clamp(2.2rem,5vw,4.4rem)}.lai-architecture__copy>p:not(:first-child),.lai-engines__intro>p:not(:first-child){max-width:43rem;line-height:1.65}.lai-architecture ul{display:grid;gap:.55rem;margin:1.4rem 0;padding:0;list-style:none}.lai-architecture li{display:flex;gap:.5rem;color:var(--lai-muted)}.lai-architecture li::before{color:var(--lai-green);content:"✓"}.lai-architecture__copy>a,.lai-engines__intro>a{color:var(--lai-blue)!important;font-weight:800}.lai-architecture figure{margin:0;padding:1rem;border:1px solid var(--lai-line);border-radius:10px;background:var(--lai-surface)}.lai-architecture figure img{display:block;width:100%} -.lai-engines{display:grid;gap:clamp(2.5rem,7vw,6rem);padding:clamp(5rem,11vw,9rem) 0;border-top:1px solid var(--lai-line);background:linear-gradient(90deg,transparent,var(--lai-surface),transparent)}.lai-engine-reel{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:1px;border:1px solid var(--lai-line);background:var(--lai-line)}.lai-engine-reel>div{display:grid;min-height:7.3rem;align-content:center;gap:.15rem;padding:1rem;background:var(--lai-bg)}.lai-engine-reel span{color:var(--lai-green);font-size:.62rem;font-weight:800;text-transform:uppercase}.lai-engine-reel b{font-family:monospace;font-size:.88rem}.lai-engine-reel small{color:var(--lai-muted);font-size:.65rem}.lai-scale__path{display:grid;gap:1px;border:1px solid var(--lai-line);background:var(--lai-line)}.lai-scale__path>div{min-height:12rem;padding:1.25rem;background:var(--lai-surface)}.lai-scale__path span{display:block;margin-bottom:2.4rem;color:var(--lai-blue);font-family:monospace;font-size:.7rem}.lai-scale__path b{font-size:1.2rem}.lai-scale__path p{margin:.45rem 0 0!important;font-size:.84rem}.lai-platform{display:grid;gap:3rem}.lai-platform__list{border-top:1px solid var(--lai-line)}.lai-platform article{display:grid;grid-template-columns:minmax(9rem,.6fr) minmax(0,1.4fr);gap:1rem;padding:1.15rem 0;border-bottom:1px solid var(--lai-line)}.lai-platform article b{color:var(--lai-ink);font-size:1rem}.lai-platform article p{margin:0!important}.lai-start{display:grid;gap:2rem}.lai-start pre{overflow:auto;margin:0!important;padding:1.2rem!important;border:1px solid var(--lai-line);border-radius:8px;background:#080b0f!important}.lai-start code{color:var(--lai-green)!important}.lai-start__links{display:flex;flex-wrap:wrap;gap:.6rem}.lai-start__links a{padding:.5rem .65rem;border:1px solid var(--lai-line);border-radius:5px;color:var(--lai-ink)!important;font-size:.72rem;font-weight:750}.lai-start__links a:hover{border-color:var(--lai-blue)} -@media(min-width:48rem){.lai-hero{grid-template-columns:minmax(22rem,.9fr) minmax(24rem,1.1fr)}.lai-breadth header,.lai-scale header{grid-template-columns:1fr 1.3fr;align-items:end}.lai-architecture{grid-template-columns:minmax(22rem,.9fr) minmax(24rem,1.1fr)}.lai-engines{grid-template-columns:minmax(22rem,.9fr) minmax(25rem,1.1fr)}.lai-engine-reel{grid-template-columns:repeat(2,minmax(0,1fr))}.lai-scale__path{grid-template-columns:repeat(3,1fr)}.lai-platform{grid-template-columns:.75fr 1.25fr}.lai-start{grid-template-columns:.75fr 1.25fr}.lai-start__links{grid-column:2}}@media(max-width:47.99rem){.lai-home{padding-inline:.2rem}.lai-lanes a{grid-template-columns:1fr auto}.lai-lanes b{grid-column:1/3}.lai-product-shot{transform:none}.lai-platform article{grid-template-columns:1fr}.lai-start__links{gap:.4rem}}@media(prefers-reduced-motion:reduce){.lai-home *{scroll-behavior:auto!important;transition-duration:.01ms!important}.lai-product-shot{transform:none}} diff --git a/website/README.md b/website/README.md new file mode 100644 index 000000000..7878c4f51 --- /dev/null +++ b/website/README.md @@ -0,0 +1,29 @@ +# localai.io + +The Hugo site served at the root of localai.io. The documentation is a separate +Hugo site in [`../docs`](../docs) and is served under `/docs/`. CI builds both +and uploads them as a single GitHub Pages artifact +(see [`.github/workflows/gh-pages.yml`](../.github/workflows/gh-pages.yml)). + +## Running locally + +From the repository root: + +```bash +make website # this site only, http://localhost:1313/ +make docs # documentation only, http://localhost:1313/docs/ +make site # build both, merged into website/public, exactly as CI does +make site-serve # the same merged build, served on http://localhost:8000 +``` + +`make website` and `make docs` run `hugo serve`, so they pick up edits live but +only cover one site at a time. Use `make site-serve` when you need the cross +links between the two sites, or the redirects from the pre-split URLs, to work. + +`make site` also runs `.github/ci/gen-redirects.sh`, which leaves a meta-refresh +page at every URL the documentation used to occupy before it moved under +`/docs/`. GitHub Pages has no server-side redirects, so those files are the only +thing keeping the old links alive. + +Set `SITE_BASE_URL` to change the base URL the merged build is generated for +(default `http://localhost:8000`). diff --git a/website/content/_index.md b/website/content/_index.md new file mode 100644 index 000000000..4ea4cc04d --- /dev/null +++ b/website/content/_index.md @@ -0,0 +1,4 @@ +--- +title: "LocalAI" +description: "LocalAI is the open source AI engine. Run any model, LLMs, vision, voice, image and video, on any hardware. No GPU required." +--- diff --git a/website/content/blog/_index.md b/website/content/blog/_index.md new file mode 100644 index 000000000..8c95e2e38 --- /dev/null +++ b/website/content/blog/_index.md @@ -0,0 +1,5 @@ +--- +title: "Blog" +description: "Release write-ups, benchmark reports and engineering notes from the LocalAI team. Every number here comes out of a benchmark suite, a release or a commit, and the source is named so you can check it." +extracss: ["blog.css"] +--- diff --git a/website/content/blog/apex-moe-quantization.md b/website/content/blog/apex-moe-quantization.md new file mode 100644 index 000000000..b7de05d5f --- /dev/null +++ b/website/content/blog/apex-moe-quantization.md @@ -0,0 +1,112 @@ +--- +title: "APEX: a 35B MoE model at 12.2 GB, and faster than F16" +date: 2026-04-10 +author: "Ettore Di Giacinto" +category: "Research" +tags: ["quantization", "APEX", "mixture-of-experts", "llama.cpp", "benchmarks"] +summary: "Qwen3.5-35B-A3B goes from 64.6 GB to 12.2 GB and speeds up from 30.4 to 74.4 tokens per second. Perplexity moves from 6.537 to 7.088. Here is the precision assignment that does it, and where it costs you." +extracss: ["blog.css"] +--- + +A 35B mixture-of-experts model at full precision is a 64.6 GB file, which puts it out of reach of every consumer GPU. APEX gets Qwen3.5-35B-A3B down to 12.2 GB, where it fits a 16 GB card with room for context, and it generates at 74.4 tokens per second instead of 30.4. The output is an ordinary GGUF that stock llama.cpp opens with no patches and no custom build. + +The compression is not free at that tier, and the numbers below say exactly what it costs. At the 21.3 GB tier it is closer to free than we expected: APEX Quality has a lower perplexity than the F16 model it was quantized from. + +## The measurements + +All of this is Qwen3.5-35B-A3B on an NVIDIA DGX Spark (GB10, 122 GB unified VRAM, CUDA 13). Perplexity is wikitext-2-raw at context 2048 over the full test set. HellaSwag and the other accuracy benchmarks run through llama.cpp at 400 tasks. `tg128` is token generation throughput. + +
+ + + + + + + + + + + + + + + +
BuildSizePerplexityKL meanHellaSwagMMLUtg128 t/s
F1664.6 GB6.537-82.5%41.5%30.4
Q8_034.4 GB6.5330.004683.0%41.2%52.5
Unsloth UD-Q8_K_XL45.3 GB6.5360.002582.5%41.3%36.4
Unsloth UD-Q4_K_XL20.7 GB6.5540.009783.0%40.6%58.1
bartowski IQ2_M11.3 GB7.3030.111380.3%39.6%76.2
APEX Quality21.3 GB6.5270.011483.0%41.2%62.3
APEX I-Quality21.3 GB6.5520.010283.5%41.4%63.1
APEX Balanced23.6 GB6.5330.008883.0%41.3%60.8
APEX Compact16.1 GB6.7830.046982.5%40.9%69.8
APEX I-Compact16.1 GB6.6690.033281.8%41.7%69.8
APEX Mini12.2 GB7.0880.087081.0%41.3%74.4
+
+ +Three things in that table are worth stopping on. + +APEX Quality is 21.3 GB, a third of F16, and its perplexity of 6.527 is lower than F16's 6.537 and lower than Q8_0's 6.533. Quantization noise acting as mild regularization on a wikitext evaluation is a known effect and we are not claiming the quantized model is smarter. The honest reading is that at this tier the loss is below the measurement floor. + +Against Unsloth's UD-Q8_K_XL, APEX I-Quality is half the size (21.3 GB against 45.3 GB), one point ahead on HellaSwag (83.5% against 82.5%), within 0.016 on perplexity, and 73% faster (63.1 t/s against 36.4). That is the comparison that matters for anyone choosing a published quant today. + +At the bottom end, APEX Mini beats bartowski IQ2_M on every metric while being 0.9 GB larger: perplexity 7.088 against 7.303, HellaSwag 81.0% against 80.3%, MMLU 41.3% against 39.6%. + +## Why it gets faster, not just smaller + +Token generation on a single stream is bound by memory bandwidth, not by arithmetic. Every generated token requires reading the active weights out of memory, so halving the bytes roughly halves the time spent waiting for them. Going from 64.6 GB to 12.2 GB takes throughput from 30.4 to 74.4 tokens per second, a 2.45x gain on the same hardware with the same kernels. Every APEX tier clears 60 t/s. + +That is also why a large well-behaved quant such as UD-Q8_K_XL is slower than a smaller one with equal quality. Size is a speed knob as much as a memory knob. + +## Per-tensor and per-layer precision + +Uniform quantization gives every tensor the same bit width, which spends the same precision on a weight that fires for every token and one that fires for 3% of them. In a mixture-of-experts model those two populations are enormous and they are easy to tell apart. + +APEX classifies every tensor into one of three roles and treats them differently. + +**Routed expert weights** (the gate, up and down projections inside the experts) are the bulk of the parameters, and only 8 of 256 experts are active per token. That 97% structural sparsity is what makes aggressive quantization safe here. The routing decision itself reads full-precision gate weights, so quantization noise inside an expert that was not selected never reaches the output at all. When an expert is selected, its contribution is one of eight summed paths, which further dilutes per-tensor error. + +**Shared expert weights** run for every single token and their weight distribution is heavy-tailed, with a kurtosis of 13.10 against 3.41 for routed experts. Those outliers carry real signal and low-bit formats clip them. Q8_0 is the minimum viable precision here, and dropping it is the fastest way to wreck a build. + +**Attention and SSM weights** are dense, contribute few parameters relative to the experts, and matter for generation quality. They sit at Q6_K throughout. + +On top of the role split there is a layer-wise gradient. The first and last five transformer layers do input embedding alignment and output logit generation, and they are measurably more sensitive than the middle layers, which perform more redundant intermediate processing. So routed experts get Q6_K at the edges (L0-4 and L35-39), Q5_K near the edges (L5-9 and L30-34), and Q4_K or IQ4_XS in the middle (L10-29). The tiers differ mostly in how far down that middle band is pushed: Compact runs Q4_K edges with Q3_K middle, and Mini goes to IQ2_S in the middle. + +None of this needs a patched llama.cpp. The assignments are expressed with the stock `--tensor-type-file` flag for per-layer rules and `--tensor-type` for component-level ones. + +## What the experiments ruled out + +Twenty-five or so systematic runs produced a few results that saved a lot of time later. + +Going from Q6_K to Q8_0 on routed experts costs 7.5 GB and buys zero perplexity improvement. Going below Q5_K on them causes measurable degradation. Q6_K is the ceiling worth paying for. + +Layer position matters more than uniform bit width. A two-tier gradient of Q6_K edges and Q5_K middle matches Q8_0 quality; a uniform Q5_K assignment at a similar size does not. + +IQ formats underperform K-quants on MoE experts. IQ3_S gives worse perplexity than Q3_K on routed expert tensors at a similar bit rate, because the near-Gaussian expert weight distribution (kurtosis 3.41) suits the K-quant block structure better. + +Five C-level modifications to the quantization algorithms themselves, including error feedback, enhanced scale search, super-block refinement and Gaussian-density weighting, all showed zero improvement. Stock llama.cpp quantization is already good. The gains here come entirely from deciding where to spend bits. + +## The I-variants and their calibration set + +Standard imatrix calibration uses Wikipedia text, which is also what wikitext perplexity measures, so the calibration and the benchmark agree with each other by construction. The I-variants calibrate on a diverse set spanning chat, code, reasoning and tool-calling, with no Wikipedia in it. + +That trade shows up clearly. I-Compact drops perplexity from 6.783 to 6.669, cuts KL max from 7.56 to 5.50, and lifts MMLU from 40.9% to 41.7%. At the Quality tier, I-Quality gives up 0.025 perplexity against Quality and takes the highest HellaSwag score of anything tested (83.5%), the best TruthfulQA (38.4%), and a lower KL divergence. If your workload is chat, code or agents rather than encyclopedic prose, take the I variant. + +## Where it costs you + +The Compact and Mini tiers are real compression, and they are not free. + +Compact at 16.1 GB moves perplexity from 6.537 to 6.783, a 3.8% increase, and its KL mean rises tenfold against Q8_0, from 0.0046 to 0.0469. Mini at 12.2 GB goes to 7.088, an 8.4% increase, with a KL mean of 0.0870 and HellaSwag down 1.5 points to 81.0%. Those are the numbers to weigh against the fact that the model now runs at all on a 16 GB card. + +The accuracy benchmarks in that table run at 400 tasks, so a single point of difference sits inside the noise. MMLU in particular stays between 39.6% and 41.7% across every build including F16, which says more about the resolution of a 400-task evaluation than about the quantizations. Perplexity and KL divergence are the metrics that separate these builds cleanly, and KL max is the one that shows worst-case outlier behaviour rather than the average. + +Running the accuracy evaluations at all required fixing llama.cpp's hybrid memory path for recurrent architectures, since Qwen3.5-35B-A3B uses both attention and SSM blocks and the evaluations crashed before the fix. That went upstream as [llama.cpp #21224](https://github.com/ggml-org/llama.cpp/pull/21224). + +## Running one + +The GGUFs are published in the [APEX collection on Hugging Face](https://huggingface.co/collections/mudler/apex-quants) and open in any current llama.cpp build. With LocalAI: + +```bash +local-ai run mudler/Qwen3.5-35B-A3B-APEX-GGUF@Qwen3.5-35B-A3B-APEX-Balanced.gguf +``` + +To quantize your own MoE model, the recipes are shell scripts over stock llama.cpp: + +```bash +git clone https://github.com/mudler/apex-quant.git +cd apex-quant +./scripts/quantize.sh --i-quality model-f16.gguf model-apex-i-quality.gguf +``` + +The default configuration assumes 40 transformer layers, as in Qwen3.5-35B-A3B. Set `NUM_LAYERS` for a different depth and the edge and middle boundaries move with it. The full methodology, every experiment and all the plots are in the [technical report](https://github.com/localai-org/apex-quant). diff --git a/website/content/blog/localai-since-march-2023.md b/website/content/blog/localai-since-march-2023.md new file mode 100644 index 000000000..9f2782918 --- /dev/null +++ b/website/content/blog/localai-since-march-2023.md @@ -0,0 +1,79 @@ +--- +title: "LocalAI, from March 2023 to now" +date: 2026-07-29 +author: "Ettore Di Giacinto" +category: "History" +tags: ["history", "architecture", "releases", "community"] +summary: "Three years, 133 releases and 224 contributors later. The four changes that mattered most were making the core small, adding agents, making it a cluster, and giving it eyes and ears." +extracss: ["blog.css"] +--- + +I created the LocalAI repository on 18 March 2023. It was an OpenAI-compatible HTTP API in front of llama.cpp, and that was the whole idea: change one URL in code you already wrote, and the answers start coming from your own laptop instead of somebody's datacenter. + +Three years on, 224 people have put code into it, across 133 releases. It has picked up 48,042 stars along the way, which still surprises me. It now runs 73 different backends, you can install any of 1,585 models from the gallery without writing a line of Python, and eighteen of the engines doing the actual work are C or C++ ports we sat down and wrote ourselves. + +None of those numbers are rounded up. You can read every one of them off the repository or the GitHub API right now, and the ones further down this post come from the release notes. + +{{< starchart >}} + +The curve is not the point, but it is a useful map. The four marks on it are the four decisions below, and you can see each of them in the slope afterwards. + +What follows is how it got here. Not the feature list, which you can read in the releases, but the four decisions that changed the shape of the thing. + +## 2023 to 2024: an API in front of llama.cpp + +One rule has not moved since the first commit: if a feature only works on a GPU, it does not ship as the main path. Every modality gets a CPU path, and that path is tested. Most people do not have a spare A100 sitting around, and the ones who do still want to develop on the train. + +The first two years were spent widening what sat behind the API while keeping that constraint: whisper.cpp for transcription, stable-diffusion for images, embeddings, then reranking, then constrained grammars and the function-calling surface. The API compatibility list grew alongside it, and later picked up the Anthropic and ElevenLabs shapes as well as OpenAI's, so that the same server answers whichever client somebody already has. + +The bill for all that breadth came due in the binary. Everything was compiled in, so you downloaded CUDA kernels whether or not you owned a GPU, a Python runtime whether or not you wanted one, and image models when all you asked for was chat. Building from source meant building the lot. It got embarrassing. + +## July 2025: the core gets small + +Every backend moved out of the main binary in [v3.2.0](https://github.com/mudler/LocalAI/releases/tag/v3.2.0). Backends became separate OCI images, pulled on demand the first time a model asks for one, with a small core that speaks the API and manages processes. + +You install one thing and it stays small. Ask for a GGUF model and llama-cpp arrives. Ask for transcription and whisper or parakeet arrives. Nothing else is fetched, and a machine that only ever serves one model never downloads the other sixty-nine backends. + +That one change is what made everything after it possible. Adding a backend stopped meaning adding weight to everybody's install, so "should we support this engine" stopped being an argument about download size and went back to being an argument about whether the engine is any good. It is also the reason we can afford to maintain eighteen engines of our own, which comes later. + +## March 2026: agents, and a new interface + +[LocalAI 4.0.0](https://github.com/mudler/LocalAI/releases/tag/v4.0.0) added native agentic orchestration with the [Agenthub](https://agenthub.localai.io) community hub, so agents with tool use, RAG, skills and streaming run inside the same server rather than as a separate stack you wire up yourself. + +The web interface was rewritten in React at the same time, with a Canvas mode, MCP Apps and client-side tools with tool streaming ([#8947](https://github.com/mudler/LocalAI/pull/8947)), and WebRTC realtime audio ([#8790](https://github.com/mudler/LocalAI/pull/8790)). MLX gained a distributed mode ([#8801](https://github.com/mudler/LocalAI/pull/8801)). + +The realtime audio path is the piece that changed what people built. Speech in, tool calls in the middle, speech out, over WebRTC, fast enough that it feels like a conversation rather than a walkie-talkie. It had landed as the Realtime API in February 2026 ([#6245](https://github.com/mudler/LocalAI/pull/6245)), and the interface rewrite finally gave it a face. + +## April 2026: it becomes a cluster + +[LocalAI 4.1.0](https://github.com/mudler/LocalAI/releases/tag/v4.1.0) added distributed cluster mode. You start a worker on another box, it reports its hardware and the backends it can run, and it joins the pool. Requests are placed by a scheduler that knows real free VRAM rather than a static guess, and the pool autoscales. + +The same release turned LocalAI into something you can put more than one person on: OIDC and API keys, per-user quotas with predictive analytics, in-UI fine-tuning with TRL that exports straight to GGUF, an on-the-fly quantization backend, and a visual pipeline editor. + +[4.3.0](https://github.com/mudler/LocalAI/releases/tag/v4.3.0) in May followed with per-request replica routing, per-API-key and per-user usage attribution ([#9920](https://github.com/mudler/LocalAI/pull/9920)), keyless cosign signing of the backend OCI images ([#9823](https://github.com/mudler/LocalAI/pull/9823)), and llama.cpp prompt caching on by default ([#9925](https://github.com/mudler/LocalAI/pull/9925)), which collapses repeated system prompts from minutes to seconds. In June, prefix-cache-aware routing ([#10071](https://github.com/mudler/LocalAI/pull/10071)) started sending a request to the replica that already holds the matching prefix cache rather than to whichever node was least busy. + +Distributed mode is also where most of the painful bugs have lived since. In the 4.8.0 cycle alone we fixed a reaper that cheerfully deleted rows for backends that were alive and busy, frontend stubs that outlived the replicas behind them, and `in_flight` counters that leaked and then pinned VRAM against eviction. Running across machines finds failure modes a single process will never show you, and it finds them in production. + +## May and June 2026: it sees and hears + +[LocalAI 4.2.0](https://github.com/mudler/LocalAI/releases/tag/v4.2.0) added voice recognition ([#9500](https://github.com/mudler/LocalAI/pull/9500)), face recognition with anti-spoofing liveness ([#9480](https://github.com/mudler/LocalAI/pull/9480)) and speaker diarization, alongside video generation ([#9420](https://github.com/mudler/LocalAI/pull/9420)), a drop-in Ollama API ([#9284](https://github.com/mudler/LocalAI/pull/9284)) and eleven new backends. + +A chat model only knows what somebody typed at it. Recognition widens that: who is in the room, who is talking, whether the face at the camera is a live person or a photo somebody is holding up. All of it runs on the same machine as the model, which for biometrics is the only arrangement most people can honestly deploy at all. + +In June those two capabilities stopped being Python. [voice-detect.cpp](https://github.com/localai-org/voice-detect.cpp) and [face-detect.cpp](https://github.com/mudler/face-detect.cpp) replaced the Python `speaker-recognition` and `insightface` backends with from-scratch C++ and ggml engines, self-contained GGUF weights, no onnxruntime at inference, and bit-exact parity with the references they replaced ([#10441](https://github.com/mudler/LocalAI/pull/10441)). + +## Mid 2026: eighteen engines of our own + +The README now has a table called "Backends built by us". It lists eighteen native C and C++ engines, plus [apex-quant](https://github.com/localai-org/apex-quant), our quantization recipe for mixture-of-experts models. + +Each one exists for one of three reasons. Some replace a Python dependency that was simply too heavy to ship, like insightface, speaker-recognition, or vLLM itself. Some port a model nobody had written in C++ yet: Depth Anything 3, LocateAnything, CED audio tagging, TRELLIS.2. And some fill a hole in what a local assistant can do at all, like acoustic echo cancellation, which is the unglamorous thing that stops a voice loop from sitting there transcribing its own speaker. + +The recipe is the same every time. A model shows up as PyTorch or ONNX plus a Python package. We port the graph to ggml, convert the weights into one GGUF file, and gate every component against reference tensors dumped from the original. Only after it is correct do we let ourselves look at the clock. LocalAI then loads the shared library through purego over a flat C ABI, so there is no Python process anywhere on the serving path. + +The most recent one is [vllm.cpp](https://github.com/mudler/vllm.cpp), a C++20 port of vLLM's V1 serving architecture with paged KV cache, continuous batching and prefix caching, which shipped as the `vllm-cpp` backend in 4.8.0. + +## Where it stands + +Still MIT, still a community project. 224 people have put code in, and the README is kept translated into eight languages because the people using this are not all in one place. The [contributors graph](https://github.com/mudler/LocalAI/graphs/contributors) is the honest picture of who actually built this, and it is not me. + +If you want to add something, backends and gallery entries are the two places a first contribution lands cleanly. There is a step-by-step checklist for a new backend in `.agents/adding-backends.md`, and a gallery entry is just a YAML block. Come say hello in [Discord](https://discord.gg/uJAeKSAGDy) if you get stuck. diff --git a/website/content/blog/parakeet-cpp-asr-on-cpu.md b/website/content/blog/parakeet-cpp-asr-on-cpu.md new file mode 100644 index 000000000..2942c888c --- /dev/null +++ b/website/content/blog/parakeet-cpp-asr-on-cpu.md @@ -0,0 +1,104 @@ +--- +title: "parakeet.cpp: NeMo transcripts, byte for byte, without the Python" +date: 2026-06-05 +author: "Ettore Di Giacinto" +category: "Benchmarks" +tags: ["parakeet.cpp", "ASR", "ggml", "streaming", "benchmarks"] +summary: "Same transcript as NVIDIA NeMo, character for character, at a median 1.40x on CPU and about 27x the speed of whisper.cpp. One binary, one GGUF file, no Python at inference." +extracss: ["blog.css"] +--- + +You can drop a single binary and a GGUF file onto a machine with no GPU and get NVIDIA NeMo Parakeet transcription out of it, at a median 1.40x NeMo's own PyTorch CPU speed, with a transcript that matches NeMo character for character. That is [parakeet.cpp](https://github.com/mudler/parakeet.cpp), a C++17 port of the Parakeet speech-recognition family built on ggml. + +Accuracy came first and speed came second, in that order, because a faster transcriber that disagrees with the reference is a different model, not a port. + +## WER 0 against NeMo + +Every published checkpoint is validated at WER 0 against NeMo. Across the LibriSpeech test-clean set the mean f32 agreement WER, meaning the word error rate between our transcript and NeMo's on the same audio, is 0.0155%. On seven of the ten models it is exactly 0.0000%, which is a byte-identical transcript. + +That number is what makes the speed comparison meaningful. Both engines did the same work and produced the same output, so the only difference left is how long they took. + +## CPU, against NeMo's own runtime + +Measured on a 20-core x86 host with 8 threads for both engines, LibriSpeech test-clean, batch size 1 on both sides. NeMo 2.7.3 on PyTorch CPU is the reference. RTFx is audio seconds divided by processing seconds, so higher is faster. + +
+ + + + + + + + + + + + + +
ModelRTFx NeMoRTFx f32Speedup f32Speedup q8_0Agreement WERRSS NeMoRSS ours
ctc-0.6b30.741.71.36x1.61x0.0000%5447 MB2457 MB
ctc-1.1b17.826.11.46x1.63x0.0222%8914 MB4201 MB
rnnt-0.6b24.334.11.40x1.52x0.0000%5516 MB2487 MB
rnnt-1.1b15.321.41.40x1.57x0.0000%8982 MB4231 MB
tdt-0.6b-v222.432.41.45x1.55x0.0000%5499 MB2545 MB
tdt-1.1b14.622.61.54x1.74x0.0000%8956 MB4231 MB
tdt_ctc-1.1b13.322.51.69x1.89x0.0985%8909 MB4236 MB
tdt_ctc-110m72.981.11.11x1.26x0.0196%1650 MB563 MB
rt-eou-120m-v161.870.61.14x1.23x0.0000%1714 MB621 MB
+
+ +
+ Bar chart of parakeet.cpp CPU speedup over NeMo, per model and per GGUF dtype +
CPU speedup over NeMo per model and per dtype, from the benchmark suite that runs in CI. Values above 1.0 mean parakeet.cpp finished first on the same audio with the same transcript.
+
+ +The range across all ten models is 1.11x to 1.69x at f32, with a median of 1.40x. Quantizing to q8_0 shrinks the file to 37% of f32 and pushes the best case to 1.86x, staying near-lossless. f16 is 57% of the size and reaches 1.70x. K-quants below that keep shrinking the model at a small and monotonic accuracy cost, which the per-model tables in the repository lay out. + +Peak resident memory is roughly half NeMo's on every model, and lower again once quantized. The 110M model runs in 563 MB against NeMo's 1650 MB, which is the difference between fitting on a small edge box and not. + +Against whisper.cpp turbo on the same clip and at the same accuracy (1.6% WER on that audio), parakeet.cpp is about 27x faster on CPU and about 12x faster on GPU. That gap is mostly architectural: Whisper is an encoder-decoder that processes fixed 30 second windows, and Parakeet's FastConformer plus transducer decodes only the frames it has. + +## Where the CPU speed came from + +The decisive win was on the decode side. A transducer decodes autoregressively, and profiling showed the prediction-network LSTM taking about 97% of RNN-T decode time while producing the same output over and over: on a non-emitting frame the prediction network's input has not changed, so its forward pass is redundant. Caching that forward across non-emitting frames removed most of the decode cost. + +The encoder side is a set of smaller wins with no single hero: a persistent ggml backend with `gallocr`, zero-copy weights straight out of the GGUF mapping, one fused graph rather than per-layer graph building, and tinyBLAS through `GGML_LLAMAFILE`. + +## On the GPU + +On an NVIDIA GB10 (Grace-Blackwell), parakeet.cpp wins on all ten models, with a median of 1.25x and up to 4.3x on the large TDT and hybrid models. The reference here is NeMo-GPU inside the `nvcr.io/nvidia/nemo` container, because NeMo cannot run on that host's torch and CUDA stack directly. + +The 4.3x cases have a specific cause. NeMo's TDT greedy decode is not CUDA-graph accelerated and falls back to a per-step Python loop, while ours is a lean C++ loop. Where NeMo's decode is CUDA-graph accelerated, as it is for RNN-T, the gap narrows to about 1.16x at f32 and 1.30x at q8_0. On the pure-encoder CTC models the margin is around 1.2x, because ggml's generic CUDA conv and attention kernels still trail NVIDIA's tuned cuDNN. That is the main piece of GPU headroom left in the project and we say so in the README rather than averaging it away. + +Batching several clips through the decoder together reaches about 10x to 12x at batch size 16 on the GB10, and about 3x to 5x on CPU. It applies to transducer models only, since CTC has no autoregressive decode to batch, and the batched path is bit-identical to running the clips one at a time. + +On Apple M4 through ggml's Metal backend, the larger models run about 3x to 5x faster than the same models on that machine's CPU. + +## Cache-aware streaming, and what end-of-utterance detection buys you + +Offline transcription hands you a file and waits. A voice assistant cannot do that, so `parakeet_realtime_eou_120m-v1` runs a cache-aware streaming path instead: you feed it 16 kHz mono PCM as it arrives and it returns newly finalized text as it becomes stable. + +Cache-aware means the cost per chunk stays flat. Each chunk's forward pass carries per-layer convolution and attention caches plus the transducer decoder state forward, so nothing before the current chunk is recomputed. Without that, every chunk would re-run the encoder over the whole session so far, and the per-chunk cost would grow with the length of the conversation until the loop fell behind. The implementation covers layer norm with causal convolution, causal subsampling, and chunked-limited attention, and its transcript matches NeMo's own cache-aware streaming byte for byte. + +End-of-utterance detection is the part that changes how an assistant feels. The model emits `` when the speaker has finished a turn and `` for a backchannel, as events alongside the text. A voice loop can start generating a reply the moment `` arrives rather than waiting out a fixed silence timer, which is where most of the perceived lag in a spoken assistant comes from. The alternative, a VAD with a 700 ms hangover, either cuts people off mid-sentence or makes the assistant feel slow, and it cannot tell "mm-hm" from the end of a thought. `finalize` flushes the tail at end of stream without fabricating an `` that NeMo would not have emitted. + +The streaming path measures at RTFx 3.80 on a 7.43 second clip. That sits well below the offline number by design, because streaming runs many small chunked passes rather than one large one, and it is still several times faster than real time on a CPU. + +There is also a multilingual streaming model, `nemotron-3.5-asr-streaming-0.6b`, covering 40 or more locales with a per-language prompt. On CPU it runs at 2.40x NeMo at f32 and 2.52x at q8_0, with agreement WER 0.0000% in both cases, offline and streaming. + +## Long audio without the memory cliff + +The FastConformer encoder uses global relative-position self-attention, which is O(T squared) in time and in memory. A 16.6 minute file subsamples to roughly 12,000 encoder frames, and the score and mask tensors alone reach tens of gigabytes, which is enough to take down a node. + +parakeet.cpp ports NeMo's `rel_pos_local_attn`, a banded attention where each query attends only to keys within a window, making attention O(T times W). It turns on automatically past 8192 encoder frames, and `PARAKEET_ATT_CONTEXT` forces a specific window. + +
+ + + + + + + +
AttentionWindowWall clockRTFxPeak RSS
full (global)-148.3 s6.7x54.0 GB
bandedW=3239.5 s25.2x8.9 GB
bandedW=12836.9 s27.0x9.4 GB
+
+ +At NeMo's full W=128 window that is about 4x faster and about 5.7x less peak memory than the global path. The band is built with a chunk-matmul construction, overlapping key and value chunks feeding one batched GEMM plus a diagonal skew view, so the graph node count does not depend on the window. The wide window costs the same as the narrow one. Short clips stay on the global path and remain byte-identical to before. + +## Using it + +parakeet.cpp ships prebuilt `parakeet-cli` bundles for Linux x64 (CPU, Vulkan, CUDA), Linux arm64, macOS arm64 with Metal, macOS x64 and Windows x64. In LocalAI it is the `parakeet-cpp` backend, which dlopens `libparakeet.so` through purego and calls the C ABI directly, so there is no Python process in the serving path and transcription comes back on the standard OpenAI-compatible endpoint. + +The full benchmark suite, methodology and per-model plots are in [benchmarks/BENCHMARK.md](https://github.com/mudler/parakeet.cpp/blob/main/benchmarks/BENCHMARK.md), and the parity matrix per checkpoint is in `docs/parity.md`. diff --git a/website/content/blog/what-landed-in-localai-4-8.md b/website/content/blog/what-landed-in-localai-4-8.md new file mode 100644 index 000000000..19c565cf8 --- /dev/null +++ b/website/content/blog/what-landed-in-localai-4-8.md @@ -0,0 +1,125 @@ +--- +title: "What landed in LocalAI 4.8" +date: 2026-07-28 +author: "Ettore Di Giacinto" +category: "Release" +tags: ["release", "vllm.cpp", "gallery", "distributed", "performance"] +summary: "The web interface got 3.48x lighter, gallery entries now install the build your hardware can actually run, and there is a new inference engine in the box. 214 pull requests in thirteen days." +extracss: ["blog.css"] +--- + +LocalAI 4.8.0 is out. It took thirteen days and 214 merged pull requests, and the changes you will notice first are the boring ones: the web interface loads faster, model installs stop asking you to pick a quantization, and a cluster no longer reports models as loaded when they are gone. + +The full notes list everything. This post covers the parts that change what you do day to day, with the pull request numbers so you can read the diffs. + +## The web interface got 3.48x lighter + +Open the UI over a slow link and you now wait about a third as long. Three separate HTTP problems were fixed together in [#11056](https://github.com/mudler/LocalAI/pull/11056), all measured on a live deployment. + +The server was sending no `Content-Encoding` at all, whatever the client asked for. There is now gzip middleware, on by default, with `--disable-http-compression` and `--http-compression-min-length` (default 1024) if you want to change it. Streaming responses are skipped explicitly, because buffering an SSE stream behind a gzip writer defeats incremental flushing and looks to the client like a hung request. Completion, realtime, speech, transcription, agent-job and log-tail paths are all on that skip list, as are already-compressed formats, which gzip made marginally larger. + +Vite content-hashes the bundle filenames, so an `/assets/` URL can never change content, yet the assets shipped with no `Cache-Control`, `ETag` or `Last-Modified`. They now carry `public, max-age=31536000, immutable`, and `index.html` is explicitly `no-cache` so a deploy is still picked up. + +The third one was `/api/traces` returning a 21 MB unpaginated blob that the UI polled every five seconds. Both trace endpoints now take `limit` (default 50, max 1000, `0` for all), `offset` and `full`, and summarize by default, dropping bodies and headers but keeping the byte counters so the UI can still say what it dropped. Every trace carries a process-lifetime `id`, and `GET /api/traces/{id}` serves the full record when you expand a row. + +
+ + + + + + + + +
MeasurementBeforeAfterChange
React JS + CSS over the wire2,815,513 B807,918 B3.48x smaller
All embedded assets, fonts included3,953,917 B1,559,787 B2.53x smaller
Repeat navigation asset transferfull re-download0 byteseliminated
/api/backend-traces poll payload21,131,097 B7,201 B~2900x smaller
+
+ +## One gallery entry, several builds + +Installing a model no longer means reading a list of quantizations and guessing which one your card will hold. A gallery entry can now declare `variants:`, a list of references to other entries that are alternative builds of the same weights: + +```yaml +- name: nanbeige4.1-3b-q4 + url: github:mudler/LocalAI/gallery/nanbeige4.1.yaml@master + overrides: {parameters: {model: nanbeige4.1-3b-q4_k_m.gguf}} + files: [...] + variants: + - model: nanbeige4.1-3b-q8 +``` + +At install time LocalAI drops the variants this host cannot run, which it derives from the backend name rather than from hardware conditions an author would have to write by hand, so MLX disappears on Linux and CUDA disappears on a Mac. It then drops the ones that will not fit, using VRAM on GPU hosts and cgroup-aware system RAM on CPU hosts so a container sees its own limit rather than the machine's. Of what is left it picks the largest, on the assumption that a bigger footprint is a better build of the same weights. The entry's own build always competes and is never filtered out, so selection ends with something installable. + +Sizes come from the existing `pkg/vram` estimator, which reads the remote GGUF header, falls back to an HTTP `HEAD`, then the declared `size:`, then the Hugging Face repo listing. Nothing is downloaded to make the decision, and a failed probe never fails an install. + +Every surface can override the choice: `variant` on `POST /models/apply`, `local-ai models install --variant`, the `install_model` MCP tool, and a split-button in the models table. An explicit choice is honored even when it does not fit, with a warning, because that is a deliberate operator decision. Older clients read the same live `gallery/index.yaml`, ignore the key they do not understand, and install exactly as before. + +One gap worth knowing about: in distributed mode `InstallModel` resolves against the frontend rather than the worker that will serve the model, so a cluster with a small frontend and large workers selects conservatively. PRs [#10943](https://github.com/mudler/LocalAI/pull/10943), [#10983](https://github.com/mudler/LocalAI/pull/10983), [#10992](https://github.com/mudler/LocalAI/pull/10992), [#11027](https://github.com/mudler/LocalAI/pull/11027) and [#11139](https://github.com/mudler/LocalAI/pull/11139). + +## A new engine: vllm.cpp + +[vllm.cpp](https://github.com/mudler/vllm.cpp) is a from-scratch C++20 port of vLLM, written and maintained by the LocalAI team under Apache-2.0, and it ships here as the `vllm-cpp` backend ([#11100](https://github.com/mudler/LocalAI/pull/11100)). It mirrors vLLM's V1 architecture, so paged KV cache, continuous batching, prefix caching, scheduler and sampler, on a portable tensor runtime with no Python, no PyTorch and no ggml at inference. It loads Hugging Face safetensors and GGUF, enforces structured output inside the engine (JSON schema, regex, choice, GBNF), and builds for CPU amd64 and arm64, CUDA 12 and 13 including Blackwell, L4T for GB10, Vulkan and Darwin Metal. + +Tool calling is at llama.cpp parity by construction, because chat deliberately reuses the same autoparser path: full minja chat templates, `tool_choice: auto` lowered to a lazy structural-tag decode constraint, 30 tool dialects, 7 reasoning parsers, and streamed `ChatDelta` and `ToolCallDelta`. + +Configuration is a normal backend install: + +```yaml +name: qwen3-vllm +backend: vllm-cpp +context_size: 8192 +parameters: + model: Qwen3-4B # a safetensors directory or a .gguf file +options: +- max_num_seqs:16 # also: block_size:, num_blocks: +``` + +The CPU path is verified end to end against `Qwen3.5-2B-UD-Q8_K_XL.gguf` with the full Ginkgo suite, covering blocking and streaming byte-parity, greedy determinism, stop words, GBNF-constrained generation, concurrent streams, reasoning split and both `required` and `auto` tool calls. The maturity statement from the release notes is worth repeating in full: + +> The GPU images build and ship, but their runtime behavior has not been through the same e2e gate yet. This is a first release of a young engine: no throughput comparison against upstream vLLM is claimed here, and `llama-cpp` remains the default recommendation for general use. Try it, and please report what breaks. + +## VRAM budgets, per node + +You can now cap how much of a card LocalAI is allowed to use, as a percentage or an absolute amount ([#10833](https://github.com/mudler/LocalAI/pull/10833)): + +``` +LOCALAI_VRAM_BUDGET=80% +LOCALAI_VRAM_BUDGET=12GB +``` + +Everywhere LocalAI reads VRAM to make an allocation decision it now uses `min(detected, budget)`. Percentages above 100 are rejected and absolute values above physical are clamped, so the ceiling can only ever lower usable VRAM. Standalone it is a hard per-process cap that hardware defaults, context auto-fit, GGUF warnings and the watchdog all inherit. Distributed it is a placement ceiling: the worker reports raw VRAM plus its budget string, the node registry resolves it to a byte ceiling on registration and heartbeat, and the SQL scheduler needed no query change. Admin overrides through `PUT` and `DELETE /api/nodes/:id/vram-budget` survive worker restarts, and the same thing is available as the `set_node_vram_budget` MCP tool. Unset means all detected VRAM, so existing deployments do not change. + +## Three new backends for speech and small quants + +`magpie-tts-cpp` wraps [magpie-tts.cpp](https://github.com/mudler/magpie-tts.cpp), a C++17 and ggml port of NVIDIA Magpie TTS Multilingual 357M with the NanoCodec vocoder embedded. Five voices, nine or more languages, 22.05 kHz mono, out of one self-contained GGUF. The upstream engine is parity-gated against NeMo per component, with a teacher-forced replay maximum absolute difference of 3.6e-5. + +`moss-tts-cpp` wraps [moss-tts.cpp](https://github.com/mudler/moss-tts.cpp) and serves MOSS-TTS-Local v1.5 at 48 kHz stereo, with optional reference-audio voice cloning. Images cover CPU, CUDA 12 and 13, Intel SYCL, Vulkan, ROCm, L4T and Darwin Metal. Both landed in [#11115](https://github.com/mudler/LocalAI/pull/11115), [#10860](https://github.com/mudler/LocalAI/pull/10860) and [#10877](https://github.com/mudler/LocalAI/pull/10877). + +The `bonsai` backend serves the 1-bit (Q1_0) and ternary (Q2_0) Bonsai quantizations of Qwen3 8B and Qwen3.6-27B, from about 1.15 GB. Stock llama.cpp has no kernels for those formats, so the backend builds against the PrismML fork through a wrapper Makefile that swaps only `LLAMA_REPO` and `LLAMA_VERSION`, reusing the same `grpc-server.cpp` with zero skew patches. Eight gallery entries ship with it. If Q1_0 and Q2_0 reach mainline llama.cpp, this backend retires into a routine version bump ([#10834](https://github.com/mudler/LocalAI/pull/10834), [#10866](https://github.com/mudler/LocalAI/pull/10866)). + +## Distributed mode stops reaping live backends + +A model that showed as loaded on the home page but appeared on no node in the cluster turned out to be four separate bugs, all fixed in this cycle. + +The reaper was deleting `node_models` rows for backends that were alive and working. `probeLoadedModels` reaped a row after a single failed one second gRPC health check, and a backend that is merely busy cannot answer one, because a single-threaded Python backend blocks for minutes inside a request. The worker spawned the backend and holds the process handle, so its answer is not blocked by whatever the backend is doing. A new `models.running` request-reply subject asks the worker directly, and the reconciler diffs the worker's process keys against the registry rows before any port probe runs. A worker that does not answer is skipped rather than assumed empty, so a NATS blip cannot delete a node's rows. Where the port probe still runs, it no longer conflates failure modes: `DeadlineExceeded` means busy, `Unavailable` means gone, and only the second counts toward a threshold that now needs three consecutive misses. + +Every routed model also left an in-process stub in the frontend's `ModelLoader`, and removal paths deleted only the database row, so the stub outlived the replica and the model was reported as loaded forever. The replica-removed hook became a list, and a new invalidator drops the local stub once no healthy replica remains anywhere in the cluster. + +Alongside those, `in_flight` counters no longer leak high and pin a replica's VRAM against eviction, model-load deadlines scale with checkpoint size and with progress rather than wall-clock, staging verification counts as progress rather than as a stall, backend discovery stopped hiding worker-installed and GPU-only backends behind the controller's own filesystem, and the scheduler will not place a model on a node that cannot store it. The full list is in [#11142](https://github.com/mudler/LocalAI/pull/11142) and the eighteen PRs around it. + +## A security fix you should read + +`POST /api/fine-tuning/jobs` accepted `reward_functions[].code`, an inline Python body that ran against a hand-rolled builtin allowlist. That allowlist was not a security boundary. Standard CPython introspection reaches the real `os` module from inside it, which is arbitrary code execution on the host, and the execution happened during a smoke test at job start on an endpoint that is unauthenticated by default. + +Inline reward code is now refused unless the operator sets `LOCALAI_TRL_ALLOW_INLINE_REWARD=true` on the backend. Builtin reward functions are unaffected and need no configuration. The documentation no longer describes the allowlist as a sandbox ([#11068](https://github.com/mudler/LocalAI/pull/11068)). This release also picks up hono 4.12.25 for CVE-2026-54290 ([#11023](https://github.com/mudler/LocalAI/pull/11023)). + +## The rest, briefly + +Hugging Face model artifacts are now a managed snapshot flow: immutable snapshot resolution, authenticated downloads with real progress, materialization on gallery install and preload, runtime binding to staged artifacts, and per-file resume of an interrupted download rather than starting over. Python backends reuse the Go download path instead of fetching on their own. + +The traces panel gained a sortable User column, plus client IP and user agent in the expanded row, taken from echo's `RealIP()` so a trusted proxy is honored. + +Documentation got an onboarding overhaul driven by a per-page audit: one model, `qwen3-4b`, now carries through install, the web UI and a curl call; there is a new walkthrough for building your first agent; and a new runtime-errors reference is keyed on the literal error strings users actually see. + +Fifteen people contributed to this release, five of them for the first time. The gallery went from 1,221 entries to 1,476. + +To upgrade, pull `localai/localai:latest` or re-run the install script. The [full changelog](https://github.com/mudler/LocalAI/compare/v4.7.1...v4.8.0) has the other 180 pull requests. diff --git a/website/content/blog/why-we-write-our-own-engines.md b/website/content/blog/why-we-write-our-own-engines.md new file mode 100644 index 000000000..411242b08 --- /dev/null +++ b/website/content/blog/why-we-write-our-own-engines.md @@ -0,0 +1,88 @@ +--- +title: "Why we write our own C and C++ engines" +date: 2026-07-24 +author: "Ettore Di Giacinto" +category: "Engineering" +tags: ["engineering", "ggml", "vllm.cpp", "depth-anything.cpp", "parity"] +summary: "A 66 MiB binary instead of a 9.1 GiB virtualenv, depth estimation that beats PyTorch on CPU in half the memory, and biometrics that match insightface bit for bit. The method, the measurements, and what it costs us." +extracss: ["blog.css"] +--- + +Most LocalAI backends wrap somebody else's engine, and that is the right default. llama.cpp, vLLM, whisper.cpp, stable-diffusion, MLX and the rest are maintained by people who are better at those models than we are, and wrapping them costs a Dockerfile and a gRPC shim. + +Eighteen of our backends do not wrap anything. They are C or C++ ports we wrote from scratch, and each one exists because wrapping the upstream engine would have meant shipping something we could not ship: a multi-gigabyte Python install, a non-portable CUDA-only stack, or a model that had no C++ implementation at all. This post is about what those ports buy, measured, and what they cost. + +## What you get: one file, and memory you can predict + +Deploying a Python inference stack means resolving a dependency tree at install time, on the target machine, against whatever CUDA and glibc it has. Deploying a ggml port means copying a shared library and a GGUF file. + +The clearest measurement of that difference is [vllm.cpp](https://github.com/mudler/vllm.cpp), our C++20 port of vLLM's V1 serving architecture. Installing vLLM produces a 9.1 GiB virtualenv. Installing vllm.cpp produces a 66 MiB binary. The engine implements the same things the Python original does, including paged KV cache, continuous batching, prefix caching, the scheduler and the sampler, with no Python, no PyTorch and no ggml at inference. + +The obvious question is what that costs in throughput. On an NVIDIA GB10 running Qwen3.6-27B in NVFP4, greedy, closed loop, against vLLM in its production graphed configuration rather than `--enforce-eager`: + +
+ + + + + + + +
Concurrency12481632
vllm.cpp tok/s86.05159.68292.34508.77801.761095.01
vLLM tok/s82.32158.03290.31505.46789.161076.25
Ratio1.045x1.011x1.007x1.007x1.016x1.017x
+
+ +We are ahead at all six points, and five of those six are ties. Our run-to-run noise band is 0.5%, and concurrency 2 through 32 land between 0.7% and 1.7%, so the honest reading is that only the single-stream case (4.5%) is clearly outside noise. Output is token-for-token identical to vLLM at every point on that curve. Peak host memory is 24.88 GiB against 28.18 GiB. + +A tie against a mature CUDA stack is a good result for a 66 MiB binary, and it means the footprint saving is not paid for in throughput. Against llama.cpp on CPU from the same GGUF file, prefill runs 1.18x faster (223.8 against 177.3 tok/s), decode is a tie inside llama.cpp's own spread, and the tokens are byte-identical to its greedy decode. Against MLX-LM on an Apple M4, prefill time to first token is 1.5% ahead and warm total throughput is 97.6% of MLX-LM, a real 2.4% gap that sits entirely in decode. + +## Sometimes the port is simply faster + +[depth-anything.cpp](https://github.com/mudler/depth-anything.cpp) is a port of ByteDance's Depth Anything 3, which gives you metric depth in metres from one ordinary photo, plus per-pixel confidence, camera intrinsics and extrinsics, and a back-projected point cloud. On CPU it is faster than PyTorch running the same model. + +
+ + + + + + +
EngineQuantModel MBLoad msInfer msPeak RAM MBvs PyTorch
PyTorchf32516749416.913281.00x
C++/ggmlq8_014240319.43631.31x
+
+ +Same model, 1.31x the speed, 27% of the memory, and a load that finishes in 40 ms instead of 749 ms, on a Ryzen 9 9950X3D at 504x336 with 16 threads. The quantized q4_k build is a 99 MB file and stays near-lossless. Output correlates 1.0 with the reference forward pass, component by component, across 37 parity tests. + +The reason it is faster has nothing to do with writing better matmul kernels than PyTorch. Two positional embeddings, the DPT head's UV embedding and the backbone's bicubic position embedding, were being recomputed on every forward pass with single-threaded scalar sin, cos and bicubic loops, even though they depend only on the input geometry and are identical every call. Caching them removed about 95 ms of host-side overhead per forward, which is most of the gap. PyTorch builds the same embeddings with vectorized operations and never paid that cost. + +That is the general shape of these wins. The heavy GEMMs are close to a wash, because everyone is calling into the same class of BLAS kernel. The difference sits in host-side work that a Python reference implementation never bothered to optimize, and in not loading an interpreter and a framework to do inference. On GPU the picture flips back to parity: with the ggml CUDA backend and flash attention on a GB10, depth-anything.cpp ties PyTorch's tuned cuDNN at 47.3 ms per forward, and wins only the cold start, loading 1.75x to 2.9x faster. + +## Parity is the gate, speed is the follow-up + +[face-detect.cpp](https://github.com/mudler/face-detect.cpp) and [voice-detect.cpp](https://github.com/localai-org/voice-detect.cpp) replaced LocalAI's Python `insightface` and `speaker-recognition` backends. Both are the case where we do not claim a CPU speed win, and both shipped anyway. + +face-detect.cpp runs the whole insightface buffalo chain, so SCRFD detection, five-landmark similarity-transform alignment to 112x112, and the ArcFace embedding, out of one self-contained GGUF with no Python and no onnxruntime. Detector boxes and landmarks match insightface to within 1 pixel, and the recognition embedding matches to cosine 1.000000, held at any thread count. On CPU it is slower than onnxruntime: SCRFD detect runs at about 0.83x at one thread and 0.69x at eight, ArcFace embed at about 0.61x and 0.84x. onnxruntime's MLAS convolution kernels sit at the FMA-port peak, and a custom AVX2 Winograd path narrowed the gap without closing it. On GPU, routing the same convolutions through cuDNN takes SCRFD from 14.8 ms to 6.4 ms and lands at torch-cuDNN parity. + +voice-detect.cpp is the same story with a memory result attached. A WeSpeaker verification peaks at about 62 MB in our binary against about 334 MB for the CPU-only Python, torch and onnxruntime path, roughly 5.4x lower, with an identical verdict and embedding cosine 1.000000. End to end on CPU the two land within 10 to 15% of each other, trading the lead by model and thread count, and on GPU the conv encoders match the reference. + +For a biometric pipeline, matching the reference exactly matters more than being faster than it. An embedding that differs in the fourth decimal place changes verification decisions at a threshold, and every enrolled template in a deployment would have to be recomputed. Parity is what makes the replacement a drop-in rather than a migration. + +## The method + +Every port follows the same sequence, and the order is the important part. + +Convert the weights first, into one GGUF with the tokenizer, the vocabulary and any auxiliary model embedded, so that deploying the model is copying a file. + +Port the graph second, and gate it component by component against reference tensors dumped from the original implementation. depth-anything.cpp has 37 ctest cases covering preprocessing, backbone, attention, the DPT head, depth, pose, the ray head, the ray to pose solver and the exporters. parakeet.cpp gates on transcript agreement with NeMo at WER 0. face-detect.cpp gates on box and landmark distance in pixels and embedding cosine. A port that is fast and slightly wrong is worthless, and without a per-component gate you find out it is wrong months later. + +Optimize third, with a profiler, and only after parity holds. In parakeet.cpp the decisive win was caching a prediction-network LSTM forward pass that was 97% of transducer decode time and mostly redundant. In depth-anything.cpp it was two cached positional embeddings. Neither was a kernel rewrite, and neither would have been findable without a working baseline to profile. + +Expose a flat C ABI last. LocalAI dlopens the shared library through purego and calls that ABI directly, so there is no subprocess, no gRPC hop to a Python server, and no interpreter in the serving path. + +## What it costs + +Maintenance, mostly. Each engine is a repository with its own CI, its own benchmark suite, its own GGUF conversion script and its own parity baselines, and upstream keeps releasing new checkpoints that need converter work. + +GPU kernels are the weak spot. ggml's generic CUDA convolution and attention kernels trail NVIDIA's tuned cuDNN on the conv-heavy models, which is why face-detect.cpp needs an explicit cuDNN path to reach parity, and why parakeet.cpp's GPU margin over NeMo is a median 1.25x while its CPU margin is wider. + +Porting also does not scale to everything. llama.cpp, vLLM, whisper.cpp, MLX and diffusers stay wrapped, because those projects are large, fast-moving and already excellent at what they do. We write an engine when a model has no C++ implementation, when the Python dependency is heavier than the model, or when the thing we need does not exist yet. Everything else we install from somebody else. + +Every engine listed above keeps its own benchmark suite, its parity gates and its methodology in its own repository, including the runs that did not work. The full list of them is the "Backends built by us" table in the [LocalAI README](https://github.com/mudler/LocalAI#backends-built-by-us). diff --git a/website/content/engines/_index.md b/website/content/engines/_index.md new file mode 100644 index 000000000..f2d91f487 --- /dev/null +++ b/website/content/engines/_index.md @@ -0,0 +1,5 @@ +--- +title: "Engines" +description: "Nineteen native C, C++ and Go engines written by the LocalAI team. No Python at inference, checked against the reference implementation in CI, and small enough to ship as one file." +extracss: ["engines.css"] +--- diff --git a/website/data/engines.yaml b/website/data/engines.yaml new file mode 100644 index 000000000..ca178e442 --- /dev/null +++ b/website/data/engines.yaml @@ -0,0 +1,281 @@ +# The nineteen native engines the LocalAI team wrote, and the one quantization +# recipe that feeds them. This file is the single source of truth for the +# /engines/ page: the layout renders whatever is here, in this order, and adds +# nothing of its own. Numbers in `highlights` come from each engine's own +# benchmark suite, so if a README moves, move the number here too. +# +# Fields per engine: +# name display name, matching how the repo calls itself +# tagline one sentence, benefit first, what the user gets +# category must match a category id below +# language implementation language +# repo canonical GitHub URL +# featured optional, renders the entry wide with its clip +# status optional badge for anything not generally announced +# media optional clip under /media/ +# poster optional still under /img/ +# clips optional extra clips, only shown on featured entries +# highlights optional list of concrete facts + +categories: + - id: hearing + label: Hearing + blurb: Turning sound into something a model can act on, words first and then everything else in the room. + - id: voice + label: Voice + blurb: Speech coming back out, in a voice you chose or one you cloned from a few seconds of audio. + - id: identity + label: Identity + blurb: Working out who is in front of the microphone or the camera, and whether they are really there. + - id: vision + label: Vision + blurb: Finding things in an image and naming them, including things nobody trained a class for. + - id: space + label: Space + blurb: Reading distance, camera pose and shape out of ordinary photos, with no rig and no capture setup. + - id: text + label: Text + blurb: Serving language models, and cleaning what goes into them before it leaves the machine. + - id: data + label: Data + blurb: The storage and quantization work that decides what actually fits on your hardware. + +engines: + # ---------------------------------------------------------------- hearing + - name: parakeet.cpp + tagline: Transcribe a meeting on a laptop CPU and be finished before whisper.cpp has cleared the first minute. + category: hearing + language: C++17 + repo: https://github.com/mudler/parakeet.cpp + featured: true + media: /media/parakeet-duel.mp4 + clips: + - src: /media/parakeet-long.mp4 + caption: Long-form audio, ours against NeMo on the same machine + highlights: + - About 27x faster than whisper.cpp turbo on CPU, and about 12x on GPU + - WER 0 against NVIDIA NeMo on every published checkpoint, so the transcript is identical + - Cache-aware streaming with end-of-utterance detection, for live audio + - Ten checkpoints, from 110M to 1.1B, and 40 or more locales on the streaming multilingual model + + - name: moss-transcribe.cpp + tagline: Get the transcript, the speaker labels and the timestamps out of a single pass, then export straight to srt or json. + category: hearing + language: C++17 + repo: https://github.com/localai-org/moss-transcribe.cpp + highlights: + - 1.58x to 1.78x faster than PyTorch on CPU, on about 1.5x less memory + - Byte-identical transcript against the reference, cosine 1.0 component by component + - 3.4 GB at f32 down to 511 MB at q4_k, still byte-identical through q5_k + + - name: ced.cpp + tagline: Let the model hear a smoke alarm, a dog or breaking glass, not only the words somebody typed. + category: hearing + language: C++17 + repo: https://github.com/localai-org/ced.cpp + media: /media/ced.mp4 + highlights: + - 527 AudioSet sound classes, multi-label, tagged in about 55 ms + - 6 MB on disk at ced-tiny q8_0, 111 MB at ced-base + - About 1.25x faster than PyTorch at f32 on half the memory + - Works over REST and live over the realtime websocket + + - name: LocalVQE + tagline: Keep a voice loop usable in a real room, with the echo, the noise and the reverb removed before the model ever hears it. + category: hearing + language: C++ + repo: https://github.com/localai-org/LocalVQE + highlights: + - Echo cancellation, noise suppression and dereverberation in one pass + - Streaming and causal, 16 ms latency, 5x realtime on a desktop CPU + - From a 17 KB linear filter to a 19 MB joint model, pick what your CPU can afford + - About 21x realtime on a single Raspberry Pi 5 core + + # ------------------------------------------------------------------ voice + - name: moss-tts.cpp + tagline: Clone a voice from a short reference clip and have it read anything back at 48 kHz stereo. + category: voice + language: C++17 + repo: https://github.com/mudler/moss-tts.cpp + media: /media/moss.mp4 + highlights: + - About 1.9x faster per frame than PyTorch on CPU, both at fp32 + - Codec decode matches the reference at 114.9 dB SNR + - 48 kHz stereo out of MOSS-TTS-Local v1.5, 12 codebooks + - Flat C API as well as a CLI, so it embeds anywhere + + - name: magpie-tts.cpp + tagline: Ship multilingual speech from one GGUF that already carries the codec, the tokenizer and the pronunciation dictionaries. + category: voice + language: C++17 + repo: https://github.com/mudler/magpie-tts.cpp + media: /media/magpie.mp4 + highlights: + - 63x faster than the NeMo reference at f32, 73x at q8_0 + - 5 named voices, 9 languages plus 3 Arabic variants + - 541 MB at q4_k, everything bundled in the single file + - Deterministic from a seed, and parity holds to 3.6e-5 on the full decode + + - name: vibevoice.cpp + tagline: Read a multi-speaker script in cloned voices, and transcribe long recordings back with speaker labels, from the same binary. + category: voice + language: C++ + repo: https://github.com/localai-org/vibevoice.cpp + highlights: + - Voice cloning from roughly 5 seconds of reference audio + - Long-form ASR with diarization on the same engine as the synthesis + - 11 GB down to 6.8 GB at Q8_0 with no measurable recall loss + - Realtime 0.5B, 1.5B and a 7B ASR model + + - name: voxtral-tts.c + tagline: Run a 4B speech model with nothing but a C compiler and libm, reading the weights straight off the safetensors file. + category: voice + language: C + repo: https://github.com/mudler/voxtral-tts.c + status: Experimental + highlights: + - Pure C, no dependency beyond the C standard library and math + - BF16 weights read from mmap, no conversion step + - 20 preset voices across 9 languages, 24 kHz output + - Optional BLAS, Apple Accelerate, NEON and CUDA paths + + # --------------------------------------------------------------- identity + - name: voice-detect.cpp + tagline: Tell who is speaking, and read their age, gender and mood, without an onnxruntime install anywhere near it. + category: identity + language: C++17 + repo: https://github.com/localai-org/voice-detect.cpp + media: /media/voice.mp4 + highlights: + - Embedding cosine 0.9999 or better against the reference, often exactly 1.0 + - 5.4x lower peak memory than the Python path, 62 MB against 334 MB + - Six model families, ECAPA-TDNN and WeSpeaker through ERes2Net and CAM++ + - Verification, identification against a registry, plus age, gender and emotion + + - name: face-detect.cpp + tagline: Detect, recognise and verify a face, and catch a photo held up to the camera, all from one shared library. + category: identity + language: C++17 + repo: https://github.com/mudler/face-detect.cpp + featured: true + media: /media/face.mp4 + clips: + - src: /media/face-id.mp4 + caption: The same person found again in a different photo, one against many + highlights: + - Boxes and landmarks land within 1 pixel of insightface + - Recognition embedding cosine 1.000000 against the reference + - Detect, align, recognise, demographics and anti-spoofing in one pipeline + - The yunet-sface pack is Apache-2.0, so it is usable commercially + + # ----------------------------------------------------------------- vision + - name: locate-anything.cpp + tagline: Ask for the red mug on the left in plain words and get coordinates back, not a caption. + category: vision + language: C++17 + repo: https://github.com/mudler/locate-anything.cpp + featured: true + media: /media/locate.mp4 + highlights: + - 1.66x to 3.09x faster than the official PyTorch on CPU + - Identical detections, IoU 1.000 against the reference + - At q8_0 it is about 4.8x faster than PyTorch f32 and still box-identical + - 9.2 GB at f16, 4.7 GB at q4_k, and an annotated PNG out of the box + + - name: rf-detr.cpp + tagline: Get boxes and instance masks at COCO quality out of any image, with no PyTorch anywhere in the process. + category: vision + language: C++17 + repo: https://github.com/localai-org/rf-detr.cpp + highlights: + - 11 variants, 5 detection and 6 segmentation, from Nano to 2XLarge + - About 9% faster than PyTorch on CPU at F16, and 1.86x smaller + - Mean mask IoU 0.99 against PyTorch on the small segmentation variants + - 44 published GGUFs, F32 through Q4_K + + # ------------------------------------------------------------------ space + - name: depth-anything.cpp + tagline: Turn one ordinary photo into distance in metres, a camera pose and a point cloud you can open in a 3D viewer. + category: space + language: C++17 + repo: https://github.com/localai-org/depth-anything.cpp + featured: true + media: /media/depth-race.mp4 + clips: + - src: /media/depth.mp4 + caption: Metric depth on CPU, against PyTorch on the same box + highlights: + - 1.31x faster than PyTorch on CPU at q8_0, in half the memory + - Loads about 6.7x faster, 112 ms against 749 ms + - 99 MB at q4_k, and correlation 1.0 with the reference component by component + - Exports to glb, COLMAP and PLY, plus confidence and a sky mask + + - name: free-splatter.cpp + tagline: Turn a handful of snapshots into a 3D Gaussian scene with no camera poses, no rig and no GPU. + category: space + language: C++ + repo: https://github.com/localai-org/free-splatter.cpp + highlights: + - 0.22 s per forward pass on Vulkan, against 1.37 s for the PyTorch reference on CUDA + - 14 s on 12 CPU threads, roughly 4x the reference, with no GPU at all + - Pose-free, so ordinary photos are enough + - One 3D Gaussian per pixel, ready for any splat viewer + + - name: trellis2.cpp + tagline: Drop in one image and get back a watertight textured mesh you can hand straight to a 3D tool. + category: space + language: C++ + repo: https://github.com/localai-org/trellis2cpp + highlights: + - Single image to GLB with PBR materials, all inference in C++ + - Prebuilt f16 GGUFs, so no safetensors download and no conversion + - Flat C ABI plus a Go demo server with a browser mesh viewer + - Metal on by default on Apple, CUDA and CPU elsewhere + + # ------------------------------------------------------------------- text + - name: vllm.cpp + tagline: Serve a language model with vLLM's throughput from a 66 MiB binary instead of a 9 GB virtualenv. + category: text + language: C++20 + repo: https://github.com/mudler/vllm.cpp + status: In development + media: /media/vllm-race.mp4 + highlights: + - 66 MiB to install, against 9.1 GiB for a vLLM environment + - Continuous batching, paged KV cache, prefix caching and speculative decoding + - 25 or more architectures, gated token for token against a pinned vLLM oracle + - CPU, CUDA, Metal and Vulkan from the same source + + - name: privacy-filter.cpp + tagline: Catch names, addresses and card numbers on the machine, before any of it reaches a model or a log. + category: text + language: C++ + repo: https://github.com/localai-org/privacy-filter.cpp + highlights: + - 7.7x faster than HF Transformers on an 8k token document, on CPU + - Runs flat to 131k tokens on GPU where HF runs out of memory at about 16k + - Exact UTF-8 byte offsets for every span it finds + - 360 tokens a second on a Raspberry Pi 5, on-device + + # ------------------------------------------------------------------- data + - name: local-store + tagline: Get vector search inside LocalAI with nothing to deploy, nothing to configure and no second service to run. + category: data + language: Go + repo: https://github.com/mudler/LocalAI + highlights: + - Ships in-tree and is the default, so embeddings work on a fresh install + - Exact cosine similarity, zero configuration + - Backs RAG, the face and voice registries and the semantic router cache + - Swap in valkey-store per request when you need durability + + - name: apex-quant + tagline: Fit a 35B mixture-of-experts model on a card you already own, and watch it run faster than the full-size build. + category: data + language: Shell + repo: https://github.com/localai-org/apex-quant + highlights: + - 64.6 GB down to 12.2 GB, at 74.4 tokens a second against 30.4 + - APEX Quality beats F16 perplexity at a third of the size + - Ordinary GGUF files, so stock llama.cpp opens them unpatched + - 201 builds already sitting in the LocalAI gallery diff --git a/website/data/milestones.json b/website/data/milestones.json new file mode 100644 index 000000000..b5ec84fcc --- /dev/null +++ b/website/data/milestones.json @@ -0,0 +1,6 @@ +[ + {"date": "2025-07-15", "label": "Backends leave the binary", "tag": "v3.2.0"}, + {"date": "2026-03-15", "label": "Agents, and a new interface", "tag": "v4.0.0"}, + {"date": "2026-04-15", "label": "It becomes a cluster", "tag": "v4.1.0"}, + {"date": "2026-05-15", "label": "It sees and hears", "tag": "v4.2.0"} +] diff --git a/website/data/stars.json b/website/data/stars.json new file mode 100644 index 000000000..4cbaf4e56 --- /dev/null +++ b/website/data/stars.json @@ -0,0 +1 @@ +[["2023-03-31", 100], ["2023-04-25", 900], ["2023-05-03", 1700], ["2023-05-14", 2500], ["2023-05-17", 3300], ["2023-05-22", 4100], ["2023-05-30", 4900], ["2023-06-07", 5700], ["2023-06-16", 6500], ["2023-07-02", 7300], ["2023-07-22", 8100], ["2023-08-08", 8900], ["2023-08-29", 9700], ["2023-09-21", 10500], ["2023-10-15", 11300], ["2023-11-08", 12100], ["2023-11-29", 12900], ["2023-12-22", 13700], ["2024-01-14", 14500], ["2024-02-05", 15300], ["2024-02-28", 16100], ["2024-03-20", 16900], ["2024-04-07", 17700], ["2024-04-22", 18500], ["2024-05-13", 19300], ["2024-06-10", 20100], ["2024-07-14", 20900], ["2024-08-17", 21700], ["2024-09-19", 22500], ["2024-10-26", 23300], ["2024-11-12", 24100], ["2024-11-18", 24900], ["2024-12-04", 25700], ["2025-01-03", 26500], ["2025-01-13", 27300], ["2025-01-21", 28100], ["2025-02-01", 28900], ["2025-02-28", 29700], ["2025-04-04", 30500], ["2025-04-29", 31300], ["2025-06-03", 32100], ["2025-07-10", 32900], ["2025-08-12", 33700], ["2025-09-15", 34500], ["2025-10-24", 35300], ["2025-11-04", 36100], ["2025-11-07", 36900], ["2025-11-14", 37700], ["2025-11-26", 38500], ["2025-12-09", 39300], ["2025-12-24", 40000], ["2026-07-31", 48042]] \ No newline at end of file diff --git a/website/data/starsmeta.json b/website/data/starsmeta.json new file mode 100644 index 000000000..a4d138eee --- /dev/null +++ b/website/data/starsmeta.json @@ -0,0 +1 @@ +{"measuredUntil": "2025-12-24", "measuredStars": 40000, "note": "GitHub caps stargazers pagination at 400 pages"} \ No newline at end of file diff --git a/website/hugo.toml b/website/hugo.toml new file mode 100644 index 000000000..7b753468d --- /dev/null +++ b/website/hugo.toml @@ -0,0 +1,31 @@ +baseURL = 'https://localai.io/' +languageCode = 'en-GB' +defaultContentLanguage = 'en' +title = 'LocalAI' +enableEmoji = true + +# The main site. Documentation is a second Hugo site under ../docs, +# built with baseURL /docs/ and merged into this site's public/ by CI. +# See .github/workflows/gh-pages.yml. + +[params] + description = 'LocalAI is the open source AI engine. Run any model, LLMs, vision, voice, image and video, on any hardware. No GPU required.' + author = 'Ettore Di Giacinto' + docsURL = '/docs/' + github = 'https://github.com/mudler/LocalAI' + discord = 'https://discord.gg/uJAeKSAGDy' + x = 'https://twitter.com/LocalAI_API' + huggingface = 'https://huggingface.co/mudler' + # Refreshed by hand or by CI; shown in the nav and the traction band. + stars = '48,042' + +[markup.goldmark.renderer] + unsafe = true + +[outputs] + home = ['html'] + section = ['html', 'rss'] + page = ['html'] + +[taxonomies] + tag = 'tags' diff --git a/website/layouts/_default/baseof.html b/website/layouts/_default/baseof.html new file mode 100644 index 000000000..b4b93e4f6 --- /dev/null +++ b/website/layouts/_default/baseof.html @@ -0,0 +1,12 @@ + + +{{ partial "head.html" . }} + +
+ + {{ partial "nav.html" . }} + {{ block "main" . }}{{ end }} + {{ partial "footer.html" . }} + + + diff --git a/website/layouts/_default/taxonomy.html b/website/layouts/_default/taxonomy.html new file mode 100644 index 000000000..05df529af --- /dev/null +++ b/website/layouts/_default/taxonomy.html @@ -0,0 +1,17 @@ +{{ define "main" }} +
+ +
+{{ end }} diff --git a/website/layouts/_default/term.html b/website/layouts/_default/term.html new file mode 100644 index 000000000..951e2062b --- /dev/null +++ b/website/layouts/_default/term.html @@ -0,0 +1,23 @@ +{{ define "main" }} +
+ +
+{{ end }} diff --git a/website/layouts/blog/list.html b/website/layouts/blog/list.html new file mode 100644 index 000000000..0244b9d95 --- /dev/null +++ b/website/layouts/blog/list.html @@ -0,0 +1,45 @@ +{{ define "main" }} +
+ + + + + +
+{{ end }} diff --git a/website/layouts/blog/single.html b/website/layouts/blog/single.html new file mode 100644 index 000000000..62dbe3519 --- /dev/null +++ b/website/layouts/blog/single.html @@ -0,0 +1,66 @@ +{{ define "main" }} +
+ +
+
+ +

← All posts

+ +
+ + {{ with .Params.category }}

{{ . }}

{{ end }} +

{{ .Title }}

+ {{ with .Params.summary }}

{{ . }}

{{ end }} +

+ {{ with .Params.author }}{{ . }}{{ end }} + + {{ .ReadingTime }} min read +

+
+ +
+ {{ .Content }} +
+ + {{ with .Params.tags }} +
+ {{ range . }}{{ . }}{{ end }} +
+ {{ end }} + +
+
+

Try it

+

Run this on the machine you are reading it on.

+

LocalAI installs as a container, a binary, a macOS DMG or a Helm chart, and pulls a backend the first time a model asks for one.

+
+ +
+ + {{ if or .PrevInSection .NextInSection }} + + {{ end }} + +

← All posts

+ +
+
+ +
+{{ end }} diff --git a/website/layouts/engines/list.html b/website/layouts/engines/list.html new file mode 100644 index 000000000..462e51498 --- /dev/null +++ b/website/layouts/engines/list.html @@ -0,0 +1,130 @@ +{{ define "main" }} +{{ $data := .Site.Data.engines }} +{{ $cats := $data.categories }} +{{ $engines := $data.engines }} +
+ + + + + + + + +
+
+ +

The rule we hold them to

+

A port only ships once it matches the original.

+

Every engine here is gated against the framework it replaces, on the same input, on the same machine. That means a transcript that comes out word for word identical, boxes that land on the same pixels, or a waveform inside a stated tolerance. Speed is the part we then go and win, and the numbers on this page come out of each engine's own benchmark suite, not a marketing run.

+ +
+
+ +
+ + +{{ end }} diff --git a/website/layouts/index.html b/website/layouts/index.html new file mode 100644 index 000000000..2de376e4f --- /dev/null +++ b/website/layouts/index.html @@ -0,0 +1,541 @@ +{{ define "main" }} +
+ + + + + + + + + + + + + + + + + + + + +
+
+ +

APEX quantization

+

The model you could not fit, on the card you already own.

+

A 35B mixture-of-experts model is 64.6 GB at full precision, which puts it out of reach of every consumer GPU. APEX gets it to 12.2 GB, and it runs at 74 tokens a second, more than twice the speed of the original. Quality barely moves. The file is an ordinary GGUF, so stock llama.cpp opens it with no patches, and 201 of them are already sitting in the LocalAI gallery.

+
+
F16 · 64.6 GB30.4 t/s
+
Q8_0 · 34.4 GB52.5 t/s
+
APEX Quality · 21.3 GB62.3 t/s
+
APEX Mini · 12.2 GB74.4 t/s
+
+
+ + + + + + + + + + + +
BuildSizePerplexityHellaSwagMMLUtg128 t/s
F1664.6 GB6.53782.5%41.5%30.4
Q8_034.4 GB6.53383.0%41.2%52.5
Unsloth UD-Q8_K_XL45.3 GB6.53682.5%41.3%36.4
APEX Quality21.3 GB6.52783.0%41.2%62.3
APEX I-Quality21.3 GB6.55283.5%41.4%63.1
APEX Compact16.1 GB6.78382.5%40.9%69.8
APEX Mini12.2 GB7.08881.0%41.3%74.4
+
+

Qwen3.5-35B-A3B on an NVIDIA DGX Spark (GB10). Perplexity on wikitext-2-raw at context 2048. Full methodology in the technical report.

+ +
+
+ + + + + + + + +
+
+ +

nib

+
+
+

An agent you can drop on any box you SSH into.

+

One Go binary, about 20 MB, no runtime and no daemon. Press Ctrl+Space anywhere and it opens. Point it at any OpenAI-compatible endpoint, including a model running on your own laptop, and it works. Tool calls go through an approval gate you control, and Claude Code plugins load as they are.

+
Gozero dependenciesMCPpluginsskillssub-agents
+

nib on GitHub ↗

+
+
+
+
nib ~20 MB, one binary
+ +
+
+
+
+
+ + + + + +
+ +
+ + + + +
+{{ end }} diff --git a/website/layouts/partials/footer.html b/website/layouts/partials/footer.html new file mode 100644 index 000000000..ef78c57ca --- /dev/null +++ b/website/layouts/partials/footer.html @@ -0,0 +1,14 @@ + diff --git a/website/layouts/partials/head.html b/website/layouts/partials/head.html new file mode 100644 index 000000000..0a16775b2 --- /dev/null +++ b/website/layouts/partials/head.html @@ -0,0 +1,26 @@ + + + + {{ if .IsHome }}{{ .Site.Title }} · Make AI run on every machine{{ else }}{{ .Title }} · {{ .Site.Title }}{{ end }} + {{ $desc := or .Description .Site.Params.description }} + + + {{ if not hugo.IsProduction }}{{ end }} + + + + + + + + + + + + + + + + {{ range .Params.extracss }} + {{ end }} + diff --git a/website/layouts/partials/nav.html b/website/layouts/partials/nav.html new file mode 100644 index 000000000..df6451299 --- /dev/null +++ b/website/layouts/partials/nav.html @@ -0,0 +1,19 @@ +{{- $home := .Site.Home.RelPermalink -}} +
+ LocalAI + + +
diff --git a/website/layouts/shortcodes/starchart.html b/website/layouts/shortcodes/starchart.html new file mode 100644 index 000000000..26b4b3a48 --- /dev/null +++ b/website/layouts/shortcodes/starchart.html @@ -0,0 +1,170 @@ +{{- $pts := .Site.Data.stars -}} +{{- $marks := .Site.Data.milestones -}} +{{- $meta := .Site.Data.starsmeta -}} +{{- $last := index $pts (sub (len $pts) 1) -}} +
+
+

GitHub stars, {{ index (index $pts 0) 0 }} to {{ index $last 0 }}

+

{{ lang.FormatNumber 0 (index $last 1) }} stars, and the four changes along the way

+

Read off the GitHub stargazers API, which records when each star was given, so this is + the measured curve rather than a redrawing of it. Hover or drag across the line to read any date. + The API stops paginating at 40,000 items, so everything up to {{ $meta.measuredUntil }} is measured and + the dashed tail to today's total is a straight line between two known points, not data.

+
+ +
+ + + +
+
+
+ +
+ Read it as a table +
+ + + + + {{- range $i, $p := $pts }}{{ if or (eq (mod $i 5) 0) (eq $i (sub (len $pts) 1)) }} + + {{- end }}{{ end }} + +
Cumulative GitHub stars, sampled every 800 stars
DateStars
{{ index $p 0 }}{{ index $p 1 }}
+
+
+
+ + diff --git a/docs/static/CNAME b/website/static/CNAME similarity index 100% rename from docs/static/CNAME rename to website/static/CNAME diff --git a/website/static/css/blog.css b/website/static/css/blog.css new file mode 100644 index 000000000..64ee270c0 --- /dev/null +++ b/website/static/css/blog.css @@ -0,0 +1,206 @@ +/* Blog: index and article. Uses only the tokens declared in site.css. + Everything is prefixed .bp- so it cannot collide with the home page. */ + +.bp-top{padding-bottom:0} +.bp-h1{font-size:clamp(2.1rem,5vw,3.9rem);max-width:22ch} +.bp-intro{max-width:58ch;color:var(--dim);font-size:1.06rem;line-height:1.68;margin-top:1.6rem} +.bp-intro p+p{margin-top:1rem} + +/* ---------- index ---------- */ + +.bp-index{padding-top:clamp(2.5rem,5vw,3.6rem)} +.bp-list{border-top:1px solid var(--line2)} +.bp-item{display:grid;gap:.5rem 1.8rem;align-items:baseline;position:relative; + padding:1.6rem .9rem 1.7rem 1rem;border-bottom:1px solid var(--line2); + transition:background .3s,padding-left .3s} +@media(min-width:900px){.bp-item{grid-template-columns:11rem 1fr 6rem}} +.bp-item::before{content:"";position:absolute;left:0;top:0;bottom:0;width:3px;background:var(--glitch); + transform:scaleY(0);transform-origin:top;transition:transform .35s cubic-bezier(.16,1,.3,1)} +.bp-item:hover::before{transform:scaleY(1)} +.bp-item:hover{background:rgba(95,205,228,.055);padding-left:1.6rem} +.bp-item__m{display:flex;flex-wrap:wrap;gap:.55rem;align-items:baseline; + font-family:'Geist Mono',monospace;font-size:.66rem;letter-spacing:.09em; + text-transform:uppercase;color:var(--faint)} +.bp-item__m em{font-style:normal;color:var(--cyan)} +.bp-item__b{display:grid;gap:.45rem} +.bp-item__t{font-family:var(--display),sans-serif;font-weight:700; + font-variation-settings:'wdth' 104,'wght' 780,'opsz' 144;letter-spacing:var(--dtrack); + font-size:clamp(1.25rem,2.4vw,1.7rem);line-height:1.14;text-wrap:balance} +.bp-item:hover .bp-item__t{color:var(--cyan-hi)} +.bp-item__s{color:var(--dim);font-size:.95rem;line-height:1.6;max-width:62ch} +.bp-item__go{font-family:'Geist Mono',monospace;font-size:.64rem;letter-spacing:.1em; + text-transform:uppercase;color:var(--faint);text-align:left} +@media(min-width:900px){.bp-item__go{text-align:right}} +.bp-item:hover .bp-item__go{color:var(--cyan)} + +.bp-end{margin-top:3rem;padding:1.8rem 1.7rem 2rem;border:1px solid var(--line);border-radius:10px; + background:rgba(255,255,255,.028)} +.bp-end__h{font-size:clamp(1.4rem,2.6vw,1.95rem);max-width:22ch;margin-top:.55rem} +.bp-end p{color:var(--dim);font-size:.95rem;max-width:56ch;margin-top:.7rem} + +/* ---------- article ---------- */ + +.bp-back{font-family:'Geist Mono',monospace;font-size:.68rem;letter-spacing:.1em;text-transform:uppercase} +.bp-back a{color:var(--faint)} .bp-back a:hover{color:var(--cyan)} +.bp-back--b{margin-top:2.6rem} +.bp-hd{margin-top:1.6rem;padding-bottom:2rem;border-bottom:1px solid var(--line2)} +.bp-sum{max-width:62ch;color:var(--ink)} +.bp-meta{display:flex;flex-wrap:wrap;gap:.5rem 1.3rem;margin-top:1.6rem; + font-family:'Geist Mono',monospace;font-size:.66rem;letter-spacing:.09em; + text-transform:uppercase;color:var(--faint)} +.bp-meta span:first-child{color:var(--cyan)} + +/* body copy, held at a comfortable measure */ +.bp-body{max-width:68ch;margin-top:2.6rem;color:var(--dim);font-size:1.02rem;line-height:1.75} +.bp-body>*+*{margin-top:1.25rem} +.bp-body p{text-wrap:pretty} +.bp-body a{color:var(--cyan-hi);border-bottom:1px solid rgba(95,205,228,.4)} +.bp-body a:hover{border-bottom-color:var(--cyan-hi)} +.bp-body strong{color:var(--ink);font-weight:600} +.bp-body em{color:var(--ink)} + +.bp-body h2{font-size:clamp(1.5rem,3vw,2.1rem);max-width:24ch;margin-top:3rem;color:var(--ink)} +.bp-body h3{font-size:clamp(1.15rem,2.1vw,1.35rem);max-width:30ch;margin-top:2.2rem;color:var(--ink)} +.bp-body h2+*,.bp-body h3+*{margin-top:.9rem} +.bp-body h2::after{content:"";display:block;width:52px;height:3px;border-radius:2px; + background:var(--cyan);opacity:.85;margin-top:.85rem} + +.bp-body ul,.bp-body ol{margin-top:1.1rem;padding-left:1.2rem;display:grid;gap:.55rem} +.bp-body li{padding-left:.2rem} +.bp-body li::marker{color:var(--cyan)} +.bp-body li>ul,.bp-body li>ol{margin-top:.55rem} + +.bp-body blockquote{margin:2rem 0;padding:.2rem 0 .2rem 1.4rem;border-left:3px solid var(--cyan); + color:var(--ink);font-family:var(--display),sans-serif;font-weight:700; + font-variation-settings:'wdth' 102,'wght' 700;letter-spacing:-.018em; + font-size:1.12rem;line-height:1.5} +.bp-body blockquote p+p{margin-top:.7rem} + +.bp-body code{font-family:'Geist Mono',ui-monospace,monospace;font-size:.84em; + background:rgba(95,205,228,.10);border:1px solid var(--line2);border-radius:4px; + padding:.08em .36em;color:var(--cyan-hi)} +.bp-body pre{margin:1.8rem 0;padding:1.15rem 1.2rem;overflow-x:auto;background:var(--deep); + border:1px solid var(--line);border-radius:10px; + box-shadow:0 18px 40px -28px rgba(0,0,0,.9)} +.bp-body pre code{background:none;border:0;padding:0;color:var(--dim); + font-size:.82rem;line-height:1.85;white-space:pre} + +/* Hugo's highlighter writes its own palette as inline styles. Pull it back + onto the site's colours so a code block looks like the rest of the page. */ +.bp-body .highlight{margin:1.8rem 0} +.bp-body .highlight pre{margin:0;padding:1.15rem 1.2rem;overflow-x:auto; + background:var(--deep)!important;color:var(--dim)!important; + border:1px solid var(--line);border-radius:10px; + box-shadow:0 18px 40px -28px rgba(0,0,0,.9)} +.bp-body .highlight code{background:none;border:0;padding:0;color:inherit; + font-size:.82rem;line-height:1.85} +.bp-body .highlight span{color:inherit!important;background:none!important} + +.bp-body hr{margin:2.8rem 0;border:0;border-top:1px solid var(--line2)} + +.bp-body figure{margin:2.2rem 0} +.bp-body figure img{width:100%;border:1px solid var(--line);border-radius:11px;background:var(--deep)} +.bp-body figcaption{margin-top:.7rem;font-family:'Geist Mono',monospace;font-size:.64rem; + line-height:1.6;color:var(--faint)} + +/* tables ride the same white card the home page uses, so wide numeric + tables stay readable and can scroll on their own */ +.bp-body .tw{margin:2rem 0;max-width:none} +@media(min-width:1080px){.bp-body .tw{width:min(78ch,calc(100vw - 2*var(--pad)))}} +.bp-body .tw caption{caption-side:bottom;padding:.7rem .9rem;text-align:left; + font-family:'Geist Mono',monospace;font-size:.62rem;line-height:1.6;color:var(--p-dim)} +.bp-body .tw td b{color:#0F6F86} +.bp-body .tw code{color:#0F6F86;background:rgba(15,111,134,.09);border-color:var(--p-line)} + +.bp-note{margin:2rem 0;padding:1.1rem 1.2rem;border:1px dashed var(--line);border-radius:9px; + background:rgba(11,35,48,.5);font-size:.93rem;color:var(--dim)} +.bp-note b{color:var(--ink)} + +/* ---------- article footer ---------- */ + +.bp-tags{display:flex;flex-wrap:wrap;gap:.35rem;margin-top:2.6rem;padding-top:1.6rem; + border-top:1px solid var(--line2)} +.bp-tags span{font-family:'Geist Mono',monospace;font-size:.63rem;color:var(--dim); + border:1px solid var(--line);padding:.24rem .5rem;border-radius:999px} + +.bp-cta{display:grid;gap:1.6rem;margin-top:2.6rem;padding:1.8rem 1.7rem 2rem; + border:1px solid var(--line);border-radius:10px;background:rgba(95,205,228,.055)} +@media(min-width:900px){.bp-cta{grid-template-columns:1.2fr .8fr;align-items:center}} +.bp-cta__h{font-size:clamp(1.35rem,2.4vw,1.8rem);max-width:20ch;margin-top:.55rem} +.bp-cta p{color:var(--dim);font-size:.94rem;max-width:52ch;margin-top:.7rem} +.bp-cta .acts{margin-top:0} + +.bp-pn{display:grid;gap:.85rem;margin-top:1.6rem} +@media(min-width:760px){.bp-pn{grid-template-columns:1fr 1fr}} +.bp-pn__i{display:flex;flex-direction:column;gap:.5rem;padding:1.1rem 1.2rem 1.2rem; + border:1px solid var(--line);border-radius:10px;background:rgba(255,255,255,.028); + transition:transform .28s cubic-bezier(.16,1,.3,1),border-color .28s} +.bp-pn__i:hover{transform:translateY(-3px);border-color:var(--cyan)} +.bp-pn__i--r{text-align:right} +.bp-pn__k{font-family:'Geist Mono',monospace;font-size:.62rem;letter-spacing:.12em; + text-transform:uppercase;color:var(--cyan)} +.bp-pn__t{font-family:var(--display),sans-serif;font-weight:700;font-size:1.02rem; + font-variation-settings:'wdth' 104,'wght' 740;letter-spacing:var(--dtrack);line-height:1.25} + +/* ---- Star history chart ------------------------------------------------ + One series, so no legend: the title names it. Grid and axes stay + recessive, the four release markers are annotations rather than a second + series, and the numbers are also available as a table. */ +.sc{margin:2.6rem 0;padding:1.5rem 1.5rem 1.3rem;border:1px solid var(--line);border-radius:12px; + background:rgba(4,19,28,.86);backdrop-filter:blur(6px); + box-shadow:0 20px 44px -30px rgba(0,0,0,.9)} +.sc__head{margin:0 0 1.4rem} +.sc__k{font-family:'Geist Mono',monospace;font-size:.63rem;letter-spacing:.14em; + text-transform:uppercase;color:var(--cyan);margin:0} +.sc__title{font-family:var(--display),sans-serif;font-weight:700;letter-spacing:-.026em; + font-size:1.24rem;margin:.55rem 0 .5rem;color:var(--ink)} +.sc__desc{font-family:var(--display),ui-sans-serif,system-ui,sans-serif; + color:var(--dim);font-size:.88rem;line-height:1.6;margin:0;max-width:62ch} + +.sc__plot{position:relative;padding-left:2.4rem;padding-bottom:3.4rem;touch-action:pan-y} +.sc__svg{display:block;width:100%;height:clamp(190px,26vw,300px);overflow:visible} +.sc__grid line{stroke:rgba(255,255,255,.07);stroke-width:1} +.sc__mline{stroke:rgba(255,255,255,.16);stroke-width:1;stroke-dasharray:2 4} +.sc__mdot{fill:var(--navy0);stroke:var(--cyan-hi);stroke-width:2} +.sc__cross{stroke:rgba(155,230,245,.55);stroke-width:1} +.sc__dot{position:absolute;width:11px;height:11px;margin:-5.5px 0 0 -5.5px;border-radius:50%; + background:var(--cyan-hi);box-shadow:0 0 0 3px rgba(95,205,228,.25);pointer-events:none} + +.sc__ylab{position:absolute;left:0;top:0;width:2.4rem} +.sc__ylab span{position:absolute;left:0;transform:translateY(-50%); + font-family:'Geist Mono',monospace;font-size:.6rem;color:var(--faint);font-variant-numeric:tabular-nums} +.sc__xlab{position:absolute;left:2.4rem;right:0;bottom:0;height:3.1rem} +.sc__xlab span{position:absolute;transform:translateX(-50%);text-align:center;width:8.4rem; + font-family:var(--display),ui-sans-serif,system-ui,sans-serif; + font-size:.66rem;line-height:1.32;color:var(--dim)} +/* rows after the first sit further from the axis, so a leader keeps them attached */ +.sc__xlab span:not(.sc__x0)::before{content:"";position:absolute;left:50%;top:-.55rem; + width:1px;height:.55rem;background:rgba(255,255,255,.18)} +.sc__xlab b{display:block;font-family:'Geist Mono',monospace;font-size:.6rem; + color:var(--cyan);font-weight:400;margin-bottom:.15rem} + +.sc__tip{position:absolute;top:-.2rem;padding:.45rem .6rem;border-radius:7px;pointer-events:none; + background:rgba(1,10,17,.94);border:1px solid var(--line);backdrop-filter:blur(6px); + font-family:'Geist Mono',monospace;font-size:.68rem;color:var(--ink);white-space:nowrap; + box-shadow:0 10px 26px -14px rgba(0,0,0,.95)} +.sc__tip b{color:var(--cyan-hi);font-variant-numeric:tabular-nums} +.sc__tip span{display:block;color:var(--faint);font-size:.6rem;margin-top:.12rem} + +.sc__data{margin-top:1.2rem;border-top:1px solid var(--line);padding-top:.9rem} +.sc__data summary{cursor:pointer;font-family:'Geist Mono',monospace;font-size:.68rem;color:var(--dim)} +.sc__data summary:hover{color:var(--cyan)} +.sc__tablewrap{overflow-x:auto;margin-top:.9rem;max-height:19rem} +.sc__tablewrap table{width:100%;border-collapse:collapse;font-variant-numeric:tabular-nums} +.sc__tablewrap caption{text-align:left;font-size:.68rem;color:var(--faint);padding-bottom:.5rem} +.sc__tablewrap th,.sc__tablewrap td{text-align:left;padding:.4rem .7rem .4rem 0; + font-family:'Geist Mono',monospace;font-size:.72rem;border-bottom:1px solid var(--line)} +.sc__tablewrap th{color:var(--faint);font-weight:400;font-size:.6rem;letter-spacing:.1em;text-transform:uppercase} +.sc__tablewrap td{color:var(--dim)} + +@media(max-width:640px){ + .sc{padding:1.1rem 1rem} + .sc__xlab span{width:5.4rem;font-size:.58rem} + .sc__plot{padding-bottom:4rem} +} +@media(prefers-reduced-motion:reduce){.sc *{transition:none!important}} +.sc__est{opacity:.5} diff --git a/website/static/css/engines.css b/website/static/css/engines.css new file mode 100644 index 000000000..ccd5b26a9 --- /dev/null +++ b/website/static/css/engines.css @@ -0,0 +1,85 @@ +/* /engines/ only. Everything here leans on the tokens and components already + defined in site.css: the same navy, the same cyan, Sora for display and + Geist Mono for anything that reads as data. Nothing new is invented, the + page just needs a card and a filter row that the home page does not have. */ + +.eng-h1{font-size:clamp(2.2rem,5.4vw,4.2rem);font-variation-settings:'wdth' 118,'wght' 880,'opsz' 144; + margin:1.1rem 0 0;line-height:1.03} +.eng-h1 u{display:block;overflow:hidden;text-decoration:none} +.eng-h1 u b{display:block;font-weight:inherit;transform:translateY(108%); + transition:transform .95s cubic-bezier(.16,1,.3,1)} +.loaded .eng-h1 u:nth-child(2) b{transition-delay:.09s} +.loaded .eng-h1 u b{transform:none} +.eng-h1 s{text-decoration:none;color:var(--cyan)} +@media(prefers-reduced-motion:reduce){.eng-h1 u b{transform:none}} + +/* filter row. Sticky, because the catalogue is long and the chips are the + only way back out of a modality once you are deep in it. */ +.engf{position:sticky;top:62px;z-index:40;display:flex;flex-wrap:wrap;gap:.4rem; + margin:0 0 .4rem;padding:.85rem 0;background:rgba(4,19,28,.9);backdrop-filter:blur(12px); + border-bottom:1px solid var(--line2)} +.engf__c{font-family:'Geist Mono',monospace;font-size:.7rem;letter-spacing:.02em;cursor:pointer; + display:inline-flex;align-items:center;gap:.45rem;color:var(--dim);background:transparent; + border:1px solid var(--line);padding:.4rem .75rem;border-radius:999px; + transition:color .22s,border-color .22s,background .22s} +.engf__c b{font-weight:400;font-size:.62rem;color:var(--faint)} +.engf__c:hover{border-color:var(--cyan);color:var(--ink)} +.engf__c[aria-pressed="true"]{background:var(--cyan);border-color:var(--cyan);color:var(--deep)} +.engf__c[aria-pressed="true"] b{color:rgba(1,10,17,.62)} + +.engg{padding:clamp(2.2rem,4.5vw,3.4rem) 0 .4rem} +.engg[hidden]{display:none} +.engg__h{max-width:62ch} +.engg__b{color:var(--dim);font-size:1rem;margin-top:.55rem;text-wrap:pretty} +.engs{display:grid;gap:.9rem;margin-top:1.8rem} +@media(min-width:720px){.engs{grid-template-columns:repeat(2,1fr)}} +@media(min-width:1080px){.engs{grid-template-columns:repeat(3,1fr)}} + +.eng{display:flex;flex-direction:column;border:1px solid var(--line);border-radius:11px; + background:rgba(255,255,255,.028);overflow:hidden; + box-shadow:0 16px 36px -26px rgba(0,0,0,.9); + transition:transform .28s cubic-bezier(.16,1,.3,1),border-color .28s,box-shadow .28s} +.eng[hidden]{display:none} +.eng:hover{transform:translateY(-4px);border-color:rgba(95,205,228,.55); + box-shadow:0 24px 48px -22px rgba(0,0,0,.95),0 0 40px -18px rgba(95,205,228,.45)} +.eng__m{background:var(--deep);border-bottom:1px solid var(--line2);overflow:hidden} +.eng__m video{width:100%;aspect-ratio:16/10;object-fit:cover; + transition:transform .6s cubic-bezier(.16,1,.3,1)} +.eng:hover .eng__m video{transform:scale(1.03)} +.eng__b{display:flex;flex-direction:column;gap:.55rem;padding:1.15rem 1.2rem 1.3rem} +.eng__k{display:flex;align-items:center;gap:.5rem;font-family:'Geist Mono',monospace; + font-size:.6rem;letter-spacing:.12em;text-transform:uppercase;color:var(--faint)} +.eng__s{margin-left:auto;font-weight:400;color:var(--purple-hi); + border:1px solid rgba(129,129,196,.45);border-radius:999px;padding:.14rem .5rem; + letter-spacing:.1em;background:rgba(129,129,196,.12)} +.eng__n{font-family:'Geist Mono',monospace;font-size:1.12rem;font-variation-settings:normal; + letter-spacing:-.02em;color:var(--cyan-hi);font-weight:600} +.eng:hover .eng__n{color:var(--ink)} +.eng__t{color:var(--ink);font-size:.95rem;line-height:1.5;text-wrap:pretty} +.eng__l{list-style:none;margin:.15rem 0 0;padding:0;display:grid;gap:.34rem} +.eng__l li{position:relative;padding-left:.95rem;color:var(--dim);font-size:.83rem;line-height:1.5} +.eng__l li::before{content:"";position:absolute;left:0;top:.62em;width:5px;height:1px;background:var(--cyan)} +.eng__go{margin-top:auto;padding-top:.35rem;font-family:'Geist Mono',monospace; + font-size:.64rem;color:var(--faint)} +.eng:hover .eng__go{color:var(--cyan)} + +/* featured: same card, given the width to carry its clips and read as the + headline of its modality. */ +@media(min-width:720px){ + .eng--f{grid-column:1/-1;flex-direction:row-reverse;align-items:stretch} + .eng--f .eng__m{flex:1 1 52%;border-bottom:0;border-left:1px solid var(--line2);display:flex} + .eng--f .eng__m video{aspect-ratio:auto;height:100%;min-height:100%} + .eng--f .eng__b{flex:1 1 48%;padding:clamp(1.4rem,2.6vw,2.1rem);justify-content:center} + .eng--f .eng__n{font-size:1.5rem} + .eng--f .eng__t{font-size:1.06rem;max-width:34ch} +} +.eng--f .eng__x{display:grid;gap:.6rem;margin-top:.7rem} +@media(min-width:720px){.eng--f .eng__x{grid-template-columns:1fr}} +.eng__x figure{margin:0;border:1px solid var(--line2);border-radius:8px;overflow:hidden;background:var(--deep)} +.eng__x video{width:100%;display:block} +.eng__x figcaption{padding:.45rem .7rem;border-top:1px solid var(--line2); + font-family:'Geist Mono',monospace;font-size:.6rem;color:var(--faint)} + +.engn{font-family:'Geist Mono',monospace;font-size:.8rem;color:var(--faint); + padding:3rem 0;text-align:center} +.engn[hidden]{display:none} diff --git a/website/static/css/site.css b/website/static/css/site.css new file mode 100644 index 000000000..134ab4186 --- /dev/null +++ b/website/static/css/site.css @@ -0,0 +1,434 @@ +@font-face{font-family:'Geist Mono';src:url(/fonts/geist-mono.woff2) format('woff2');font-weight:100 900;font-style:normal;font-display:block} +@font-face{font-family:'Sora';src:url(/fonts/sora-700.woff2) format('woff2');font-weight:700;font-display:block} + +/* Palette sampled straight from the LocalAI logo: the navy of the triangle, + the cyan of the llama, the purple of the speed bars, the off-white wordmark. + One deliberate visual world, so both theme toggles resolve the same. */ +:root{ + --navy0:#04131C; --navy1:#0B2330; --navy2:#0E2C3A; --deep:#010A11; + --line:rgba(255,255,255,.09); --line2:rgba(255,255,255,.06); + --ink:#FBFDFE; --dim:rgba(246,251,253,.76); --faint:rgba(246,251,253,.46); + --veil:.43; --glowtext:0 1px 16px rgba(4,19,28,.92), 0 0 4px rgba(4,19,28,.7); + --cyan:#5FCDE4; --cyan-hi:#9BE6F5; --purple:#8181C4; --purple-hi:#ADA7EA; + --paper:#F4F6F5; --p-ink:#04131C; --p-dim:#51666F; --p-line:#DBE2E1; + --glitch:linear-gradient(90deg,var(--cyan),rgba(95,205,228,.15)); + --display:'Sora'; --dtrack:-.032em; --dcase:none; + --pad:clamp(1.25rem,4vw,3.25rem); --band:clamp(4.5rem,8.5vw,7.5rem); +} +:root[data-theme="light"],:root[data-theme="dark"]{color-scheme:dark} +*{box-sizing:border-box} +body{margin:0;background:var(--navy0);color:var(--ink); + font-family:var(--display),ui-sans-serif,system-ui,sans-serif;font-variation-settings:'wdth' 100,'opsz' 18; + font-size:16.5px;line-height:1.62;-webkit-font-smoothing:antialiased;overflow-x:hidden} +a{color:inherit;text-decoration:none} +img,video{display:block;max-width:100%} +:focus-visible{outline:2px solid var(--cyan);outline-offset:3px;border-radius:2px} +p{margin:0} +h1,h2,h3{margin:0;font-family:var(--display),system-ui,sans-serif;font-weight:700; + font-variation-settings:'wdth' 104,'wght' 780,'opsz' 144; + letter-spacing:var(--dtrack);line-height:1.04;text-wrap:balance} +.mono{font-family:'Geist Mono',ui-monospace,monospace} +.tnum{font-variant-numeric:tabular-nums} + +/* the logo's speed bars, reused as the page's motion signature */ +.bars{display:grid;gap:3px;width:74px} +.bars i{display:block;height:3px;border-radius:2px;background:var(--cyan);opacity:.85; + transform-origin:left;transform:scaleX(0);transition:transform .55s cubic-bezier(.16,1,.3,1)} +.bars i:nth-child(1){width:100%} +.bars i:nth-child(2){width:62%;background:var(--cyan);opacity:.42;transition-delay:.07s} +.bars i:nth-child(3){width:84%;transition-delay:.14s} +.bars i:nth-child(4){width:40%;background:var(--cyan);opacity:.28;transition-delay:.21s} +.in .bars i,.loaded .bars i{transform:scaleX(1)} + +.prog{position:fixed;top:0;left:0;height:3px;width:100%;z-index:70} +.prog i{display:block;height:100%;width:0;background:var(--glitch)} +.top{position:sticky;top:0;z-index:60;display:flex;align-items:center;gap:1.6rem;height:62px; + padding:0 var(--pad);background:rgba(7,22,32,.86);backdrop-filter:blur(12px);border-bottom:1px solid var(--line2)} +.brand{display:flex;align-items:center;gap:.55rem} +.brand img{height:32px;width:auto;transition:transform .3s cubic-bezier(.16,1,.3,1)} +.brand:hover img{transform:translateX(3px)} +.brand span{font-family:var(--display),sans-serif;font-size:1.06rem;font-weight:700; + letter-spacing:var(--dtrack);font-variation-settings:'wdth' 104,'wght' 760} +.top nav{display:none;gap:1.35rem;font-size:.87rem;color:var(--dim)} +@media(min-width:940px){.top nav{display:flex}} +.top nav a{position:relative;padding-bottom:3px} +.top nav a::after{content:"";position:absolute;left:0;right:100%;bottom:0;height:2px;background:var(--glitch); + transition:right .3s cubic-bezier(.16,1,.3,1)} +.top nav a:hover{color:var(--ink)} .top nav a:hover::after{right:0} +.topright{margin-left:auto;display:flex;align-items:center;gap:.55rem} +.pill{display:inline-flex;align-items:center;gap:.4rem;height:34px;padding:0 .8rem;border-radius:999px; + border:1px solid var(--line);font-family:'Geist Mono',monospace;font-size:.7rem;color:var(--dim)} +.pill:hover{border-color:var(--cyan);color:var(--ink)} +.go-btn{display:inline-flex;align-items:center;height:34px;padding:0 1rem;border-radius:999px; + background:var(--cyan);color:var(--deep);font-size:.83rem;font-variation-settings:'wght' 720} +.go-btn:hover{background:var(--cyan-hi)} + +.shell{max-width:1280px;margin:0 auto;padding:0 var(--pad)} +section{position:relative;padding:var(--band) 0} +.navy{background:rgba(4,19,28,var(--veil))} .surf{background:var(--navy1)} .deepbg{background:var(--deep)} +.navy h1,.navy h2,.navy h3,.navy .lede,.navy p,.navy .kicker{text-shadow:var(--glowtext)} +.navy .mono,.navy .chips span,.navy .apis span,.navy .sn__e span{text-shadow:0 1px 10px rgba(4,19,28,.85)} +#field{position:fixed;inset:0;z-index:0;pointer-events:none;opacity:1;transition:opacity .7s ease} +.top,main,footer{position:relative;z-index:1} +.paper{background:var(--paper);color:var(--p-ink)} +.paper p,.paper .lede{color:var(--p-dim)} .paper h2,.paper h3{color:var(--p-ink)} +.paper .kicker{color:#0F6F86} .paper .bars i{background:#0F6F86} +.paper .bars i:nth-child(2),.paper .bars i:nth-child(4){background:#0F6F86;opacity:.4} +.kicker{font-family:'Geist Mono',monospace;font-size:.69rem;letter-spacing:.16em; + text-transform:uppercase;color:var(--cyan);margin-top:.9rem;display:block} +h2{font-size:clamp(2.1rem,5vw,3.9rem)} +.lede{max-width:58ch;color:var(--dim);font-size:1.06rem;line-height:1.68;text-wrap:pretty} +.mt1{margin-top:.9rem}.mt2{margin-top:1.6rem}.mt3{margin-top:2.6rem} + +/* hero */ +.hero{padding-top:clamp(2.5rem,5vw,4.5rem);overflow:hidden} +.hero::before{content:"";position:absolute;inset:-25% 50% 35% -20%; + background:radial-gradient(closest-side,rgba(71,177,199,.22),transparent 70%);filter:blur(24px)} +.hero::after{content:"";position:absolute;inset:30% -18% -25% 52%; + background:radial-gradient(closest-side,rgba(129,129,196,.18),transparent 70%);filter:blur(24px)} +.hero .shell{position:relative} +.hero__grid{display:grid;gap:clamp(2.5rem,5vw,4rem);align-items:center} +@media(min-width:1020px){.hero__grid{grid-template-columns:1.06fr .94fr}} +.hero h1{font-size:clamp(2.5rem,6.1vw,4.9rem);font-variation-settings:'wdth' 118,'wght' 880,'opsz' 144; + margin:1.2rem 0 0;line-height:1.02} +.hero h1 u{display:block;overflow:hidden;text-decoration:none} +.hero h1 u b{display:block;font-weight:inherit} +.hero h1 s{text-decoration:none;color:var(--cyan)} +.hero .bars{width:190px;margin-top:1.5rem} +.acts{display:flex;flex-wrap:wrap;gap:.7rem;margin-top:2rem;align-items:center} +.btn{display:inline-flex;align-items:center;gap:.55rem;height:48px;padding:0 1.25rem;border-radius:6px; + background:var(--cyan);color:var(--deep);font-size:.95rem;font-variation-settings:'wght' 730; + transition:transform .22s cubic-bezier(.16,1,.3,1),background .22s} +.btn:hover{background:var(--cyan-hi);transform:translateY(-2px)} +.btn--o{background:transparent;border:1px solid var(--line);color:var(--ink)} +.btn--o:hover{border-color:var(--cyan);background:rgba(71,177,199,.08)} +.paper .btn{background:var(--p-ink);color:var(--paper)} +.paper .btn--o{background:transparent;border-color:var(--p-line);color:var(--p-ink)} +.figures{display:grid;grid-template-columns:repeat(2,1fr);margin-top:2.6rem;border-top:1px solid var(--line2)} +@media(min-width:600px){.figures{grid-template-columns:repeat(4,1fr)}} +.figures div{padding:1rem .2rem;border-bottom:1px solid var(--line2)} +.figures b{display:block;font-family:var(--display),sans-serif;font-weight:700;font-size:1.72rem; + font-variation-settings:'wdth' 118,'wght' 820;letter-spacing:var(--dtrack)} +.figures span{font-family:'Geist Mono',monospace;font-size:.6rem;letter-spacing:.11em;text-transform:uppercase;color:var(--faint)} +.figures b{color:var(--cyan)} +.screen{border-radius:12px;overflow:hidden;background:var(--deep);border:1px solid var(--line); + box-shadow:0 2px 0 rgba(255,255,255,.05) inset, 0 28px 60px -30px rgba(0,0,0,.9), 0 60px 120px -60px rgba(95,205,228,.28)} +.screen__bar{display:flex;align-items:center;gap:.5rem;padding:.55rem .85rem;border-bottom:1px solid var(--line2); + font-family:'Geist Mono',monospace;font-size:.63rem;color:var(--faint)} +.screen__bar i{width:7px;height:7px;border-radius:50%;background:var(--cyan)} +.screen__bar b{margin-left:auto;font-weight:400;color:var(--dim)} + +/* mission */ +.mission{display:grid;margin-top:3rem;border-top:1px solid var(--line2)} +@media(min-width:900px){.mission{grid-template-columns:repeat(3,1fr)}} +.mi{padding:2rem 2.2rem 2.2rem 0;border-bottom:1px solid var(--line2)} +@media(min-width:900px){.mi+.mi{padding-left:2.4rem;border-left:1px solid var(--line2)}} +.mi__n{font-family:'Geist Mono',monospace;font-size:.67rem;letter-spacing:.14em;color:var(--faint);margin-top:1rem} +.mi h3{font-size:1.5rem;margin:.55rem 0 .7rem} +.mi p{color:var(--dim);font-size:.95rem} +.mi__meta{margin-top:1.2rem;font-family:'Geist Mono',monospace;font-size:.63rem;color:var(--faint)} + +/* lanes */ +.lanes{margin-top:2.4rem;border-top:1px solid var(--line2)} +.lane{display:grid;grid-template-columns:1fr auto;gap:.25rem 1.2rem;align-items:center; + padding:1.1rem .9rem 1.1rem 1rem;border-bottom:1px solid var(--line2);position:relative; + transition:background .28s,padding-left .28s} +@media(min-width:780px){.lane{grid-template-columns:9.5rem 1fr auto}} +.lane::before{content:"";position:absolute;left:0;top:0;bottom:0;width:3px;background:var(--cyan); + transform:scaleY(0);transform-origin:top;transition:transform .32s cubic-bezier(.16,1,.3,1)} +.lane:hover::before{transform:scaleY(1)} +.lane:hover{background:rgba(95,205,228,.055);padding-left:1.6rem} +.lane__k{font-family:'Geist Mono',monospace;font-size:.72rem;letter-spacing:.12em; + text-transform:uppercase;color:var(--cyan)} +.lane__d{font-size:1rem} +@media(max-width:779px){.lane__d{grid-column:1/3}} +.lane__t{font-family:'Geist Mono',monospace;font-size:.64rem;color:var(--faint)} +.apis{display:flex;flex-wrap:wrap;gap:.4rem;margin-top:1.6rem} +.apis span{font-family:'Geist Mono',monospace;font-size:.68rem;color:var(--dim); + border:1px solid var(--line);padding:.32rem .6rem;border-radius:999px} + +/* community */ +.headline{display:grid;gap:.85rem;margin-top:2.2rem} +@media(min-width:900px){.headline{grid-template-columns:repeat(3,1fr)}} +.hq{border:1px solid rgba(95,205,228,.34);border-radius:12px;padding:1.5rem 1.5rem 1.4rem; + background:linear-gradient(180deg,rgba(95,205,228,.09),rgba(255,255,255,.025)); + display:flex;flex-direction:column;gap:.85rem; + box-shadow:0 20px 44px -28px rgba(0,0,0,.9), 0 0 40px -22px rgba(95,205,228,.5); + transition:transform .28s cubic-bezier(.16,1,.3,1),border-color .28s} +.hq:hover{transform:translateY(-3px);border-color:rgba(95,205,228,.7)} +.hq blockquote{margin:0;font-size:1.06rem;line-height:1.5;color:var(--ink); + font-family:'Sora',sans-serif;font-weight:700;letter-spacing:-.022em} +.hq__w{font-family:'Geist Mono',monospace;font-size:.68rem;color:var(--cyan)} +.hq__w b{display:block;color:var(--dim);font-weight:400;font-size:.63rem;margin-top:.2rem} +.hq__go{margin-top:auto;font-family:'Geist Mono',monospace;font-size:.63rem;color:var(--faint)} +.hq:hover .hq__go{color:var(--cyan)} +.posts{display:grid;gap:.85rem;margin-top:2.2rem} +@media(min-width:700px){.posts{grid-template-columns:repeat(2,1fr)}} +@media(min-width:1050px){.posts{grid-template-columns:repeat(4,1fr)}} +.post{border:1px solid var(--line);border-radius:11px;padding:1.1rem 1.1rem 1.2rem; + background:rgba(255,255,255,.028);display:flex;flex-direction:column;gap:.6rem;min-height:158px; + box-shadow:0 16px 34px -26px rgba(0,0,0,.9); + transition:transform .28s cubic-bezier(.16,1,.3,1),border-color .28s,box-shadow .28s} +.post:hover{transform:translateY(-3px);border-color:rgba(95,205,228,.5); + box-shadow:0 22px 44px -22px rgba(0,0,0,.95),0 0 36px -16px rgba(95,205,228,.45)} +.post__h{display:flex;align-items:center;gap:.5rem;font-family:'Geist Mono',monospace;font-size:.72rem;color:var(--cyan)} +.post__h em{margin-left:auto;font-style:normal;font-size:.6rem;color:var(--faint);letter-spacing:.1em;text-transform:uppercase} +.post p{color:var(--dim);font-size:.87rem} +.post__slot{color:var(--faint);font-size:.82rem;font-style:italic} +.post__go{margin-top:auto;font-family:'Geist Mono',monospace;font-size:.64rem;color:var(--faint)} +.post:hover .post__go{color:var(--cyan)} + +/* senses */ +.senses{margin-top:2.6rem;border-top:1px solid var(--line2)} +.sn{display:grid;gap:.4rem 1.8rem;padding:1.45rem .9rem 1.45rem 1rem;border-bottom:1px solid var(--line2); + align-items:baseline;position:relative;transition:background .3s,padding-left .3s} +@media(min-width:860px){.sn{grid-template-columns:12rem 1fr 16rem}} +.sn::before{content:"";position:absolute;left:0;top:0;bottom:0;width:3px;background:var(--glitch); + transform:scaleY(0);transform-origin:top;transition:transform .35s cubic-bezier(.16,1,.3,1)} +.sn:hover::before{transform:scaleY(1)} +.sn:hover{background:rgba(71,177,199,.055);padding-left:1.6rem} +.sn__v{font-family:'Geist Mono',monospace;font-size:.77rem;letter-spacing:.13em;text-transform:uppercase;color:var(--cyan)} + +.sn__v u{text-decoration:none;color:var(--faint)} +.sn p{color:var(--dim);font-size:.96rem;max-width:54ch} +.sn__e{display:flex;flex-wrap:wrap;gap:.35rem} +.sn__e span{font-family:'Geist Mono',monospace;font-size:.63rem;color:var(--dim); + border:1px solid var(--line);padding:.24rem .5rem;border-radius:999px} +.sn:hover .sn__e span{border-color:var(--cyan);color:var(--ink)} + +/* made with */ +.made{display:grid;gap:clamp(2rem,4vw,3.2rem);align-items:center;margin-top:2.6rem} +@media(min-width:980px){.made{grid-template-columns:1.15fr .85fr}} +.player{position:relative;border-radius:12px;overflow:hidden;border:1px solid var(--line); + background:var(--deep);cursor:pointer; + box-shadow:0 2px 0 rgba(255,255,255,.05) inset, 0 30px 62px -32px rgba(0,0,0,.92), + 0 60px 110px -60px rgba(95,205,228,.32)} +.player video{width:100%;display:block} +.player__ui{position:absolute;inset:0;display:grid;place-items:center; + background:linear-gradient(0deg,rgba(4,19,28,.55),rgba(4,19,28,.12));transition:opacity .35s} +.player.playing .player__ui{opacity:0;pointer-events:none} +.player__btn{display:flex;align-items:center;gap:.7rem;padding:.75rem 1.15rem .75rem .95rem;border-radius:999px; + background:rgba(95,205,228,.95);color:var(--deep);font-family:'Geist Mono',monospace;font-size:.76rem; + font-weight:600;box-shadow:0 12px 34px -12px rgba(95,205,228,.8);transition:transform .25s cubic-bezier(.16,1,.3,1)} +.player:hover .player__btn{transform:scale(1.05)} +.player__btn i{width:0;height:0;border-left:11px solid var(--deep); + border-top:7px solid transparent;border-bottom:7px solid transparent;margin-left:2px} +.player__tag{position:absolute;left:.8rem;top:.8rem;font-family:'Geist Mono',monospace;font-size:.6rem; + letter-spacing:.1em;text-transform:uppercase;color:var(--ink);background:rgba(4,19,28,.72); + border:1px solid var(--line);padding:.3rem .55rem;border-radius:999px;backdrop-filter:blur(6px)} +.recipe{display:grid;gap:1px;margin-top:1.6rem;background:var(--line2);border:1px solid var(--line2);border-radius:9px;overflow:hidden} +.recipe div{display:flex;gap:.9rem;align-items:baseline;padding:.75rem .9rem;background:rgba(4,19,28,.55)} +.recipe b{font-family:'Geist Mono',monospace;font-size:.66rem;color:var(--cyan);min-width:6.5rem} +.recipe span{color:var(--dim);font-size:.86rem} + +/* media wall */ +.wallhead{margin-top:3rem;padding-top:2.2rem;border-top:1px solid var(--line2)} +.wallhead h3{font-size:clamp(1.4rem,2.6vw,1.95rem);max-width:20ch} +.wallhead p{color:var(--dim);font-size:.97rem;max-width:58ch;margin-top:.7rem} +.wall{display:grid;gap:.85rem;margin-top:2.6rem} +@media(min-width:680px){.wall{grid-template-columns:repeat(2,1fr)}} +@media(min-width:1000px){.wall{grid-template-columns:repeat(3,1fr)}} +.wi{background:var(--deep);position:relative;overflow:hidden;display:block;border-radius:10px; + box-shadow:0 18px 40px -24px rgba(0,0,0,.9); + transition:transform .35s cubic-bezier(.16,1,.3,1),box-shadow .35s,outline-color .35s; + outline:1px solid transparent;outline-offset:-1px} +.wi:hover{transform:translateY(-4px);outline-color:rgba(95,205,228,.55); + box-shadow:0 26px 54px -22px rgba(0,0,0,.95), 0 0 46px -14px rgba(95,205,228,.5);z-index:2} +.wi video{width:100%;aspect-ratio:16/10;object-fit:cover;transition:transform .6s cubic-bezier(.16,1,.3,1)} +.wi:hover video{transform:scale(1.03)} +.wi__c{position:absolute;left:0;right:0;bottom:0;padding:.75rem .85rem;display:flex;align-items:flex-end;gap:.6rem; + background:linear-gradient(0deg,rgba(5,15,23,.94),rgba(5,15,23,.5) 55%,transparent); + font-family:'Geist Mono',monospace;font-size:.66rem;color:var(--ink)} +.wi__c em{font-style:normal;color:var(--cyan)} +.wi__c b{margin-left:auto;font-weight:400;color:var(--faint);text-align:right} +.wi:hover .wi__c em{color:var(--cyan-hi)} + +/* duo */ +.duo{display:grid;gap:clamp(2rem,4.5vw,3.4rem);align-items:center;margin-top:2.6rem} +@media(min-width:980px){.duo{grid-template-columns:1fr 1fr}.duo--r .duo__m{order:-1}} +.claim{display:flex;flex-wrap:wrap;margin-top:1.8rem;border-top:1px solid var(--line2)} +.claim div{flex:1 1 8rem;padding:.9rem 1rem .9rem 0;border-bottom:1px solid var(--line2)} +.claim b{display:block;font-family:'Geist Mono',monospace;font-size:1.25rem;letter-spacing:-.02em;color:var(--cyan)} +.claim span{font-family:'Geist Mono',monospace;font-size:.58rem;letter-spacing:.1em;text-transform:uppercase;color:var(--faint)} + +/* engines */ +.spot{display:grid;gap:clamp(1.8rem,4vw,3.2rem);align-items:center;padding:clamp(2.4rem,5vw,3.6rem) 0; + border-top:1px solid var(--line2)} +@media(min-width:980px){.spot{grid-template-columns:1fr 1.1fr}.spot--r .spot__m{order:-1}} +.spot__n{font-family:'Geist Mono',monospace;font-size:.63rem;letter-spacing:.14em;color:var(--faint)} +.spot h3{font-family:'Geist Mono',monospace;font-size:1.55rem;font-variation-settings:normal; + letter-spacing:-.02em;color:var(--cyan-hi);margin:.7rem 0 .4rem} +.spot__h{font-size:clamp(1.4rem,2.6vw,2rem);max-width:17ch;margin:.2rem 0 .9rem} +.spot p{color:var(--dim);font-size:.97rem;max-width:50ch} +.spot__m{display:grid;gap:.7rem} +.spot__m figure{margin:0;border:1px solid var(--line);border-radius:11px;overflow:hidden;background:var(--deep); + box-shadow:0 2px 0 rgba(255,255,255,.05) inset, 0 24px 52px -28px rgba(0,0,0,.92), 0 48px 90px -54px rgba(95,205,228,.26)} +.spot__m figcaption{padding:.55rem .8rem;border-top:1px solid var(--line2); + font-family:'Geist Mono',monospace;font-size:.62rem;color:var(--faint)} +.spot__m img,.spot__m video{width:100%} +.facts{display:flex;flex-wrap:wrap;margin-top:1.4rem;border-top:1px solid var(--line2)} +.facts div{flex:1 1 7.5rem;padding:.85rem 1rem .85rem 0;border-bottom:1px solid var(--line2)} +.facts b{display:block;font-family:'Geist Mono',monospace;font-size:1.15rem;color:var(--cyan);letter-spacing:-.02em} +.facts span{font-family:'Geist Mono',monospace;font-size:.57rem;letter-spacing:.1em;text-transform:uppercase;color:var(--faint)} +.quiet{display:grid;gap:1.4rem;margin-top:1.6rem;padding:1.4rem 1.5rem;border:1px dashed var(--line); + border-radius:9px;background:rgba(11,35,48,.5)} +@media(min-width:820px){.quiet{grid-template-columns:1.25fr .75fr;align-items:center}} +.quiet h4{margin:.5rem 0 .5rem;font-family:'Geist Mono',monospace;font-size:1.1rem;font-weight:600;color:var(--ink)} +.quiet p{color:var(--dim);font-size:.9rem} +.quiet__tag{font-family:'Geist Mono',monospace;font-size:.58rem;letter-spacing:.13em;text-transform:uppercase;color:var(--faint)} +.quiet video{border:1px solid var(--line2);border-radius:9px;box-shadow:0 18px 40px -26px rgba(0,0,0,.9)} +.chips{display:flex;flex-wrap:wrap;gap:.35rem;margin-top:1.3rem} +.chips span{font-family:'Geist Mono',monospace;font-size:.63rem;color:var(--dim); + border:1px solid var(--line);padding:.24rem .5rem;border-radius:999px} +.reel{margin-top:2.2rem;border-top:1px solid var(--line2);border-bottom:1px solid var(--line2);padding:.85rem 0; + overflow:hidden;-webkit-mask-image:linear-gradient(90deg,transparent,#000 7%,#000 93%,transparent); + mask-image:linear-gradient(90deg,transparent,#000 7%,#000 93%,transparent)} +.reel__t{display:flex;gap:1.9rem;width:max-content;animation:roll 46s linear infinite} +.reel span{font-family:'Geist Mono',monospace;font-size:.78rem;color:var(--dim);white-space:nowrap} +.reel span::before{content:"/ ";color:var(--cyan)} +@keyframes roll{to{transform:translateX(-50%)}} + +/* apex */ +.tw{overflow-x:auto;margin-top:2.2rem;border:1px solid var(--p-line);border-radius:8px;background:#fff} +table{border-collapse:collapse;width:100%;min-width:640px;font-variant-numeric:tabular-nums} +th,td{text-align:right;padding:.72rem .9rem;font-family:'Geist Mono',monospace;font-size:.79rem; + border-bottom:1px solid var(--p-line);white-space:nowrap;color:var(--p-ink)} +th{font-size:.6rem;letter-spacing:.1em;text-transform:uppercase;color:var(--p-dim);background:var(--paper)} +td:first-child,th:first-child{text-align:left} +tr[data-a] td{background:rgba(71,177,199,.13)} +tr[data-a] td:first-child{font-weight:600;color:#0F6F86} +tbody tr:hover td{background:rgba(95,205,228,.10)} +.sizes{display:grid;gap:.6rem;margin-top:2rem} +.sz{display:grid;grid-template-columns:10.5rem 1fr 4.4rem;gap:.8rem;align-items:center; + font-family:'Geist Mono',monospace;font-size:.73rem;color:var(--p-dim)} +.sz i{display:block;height:14px;border-radius:3px;background:linear-gradient(90deg,#0B5C72,#7FC9DC); + transform-origin:left;transform:scaleX(0);transition:transform 1s cubic-bezier(.16,1,.3,1)} +.in .sz i{transform:scaleX(1)} +.sz u{text-decoration:none;text-align:right;color:var(--p-ink)} + +/* cluster */ +.scene{display:grid;gap:2.4rem;margin-top:2.6rem} +@media(min-width:980px){.scene{grid-template-columns:1fr 1fr;align-items:start}} +.pin{position:sticky;top:100px} +.st{padding:1.55rem 1.3rem 1.65rem 1.4rem;border-top:1px solid var(--line2);position:relative;transition:background .3s} +.st::before{content:"";position:absolute;left:0;top:-1px;bottom:0;width:3px;background:var(--glitch); + transform:scaleY(0);transform-origin:top;transition:transform .5s cubic-bezier(.16,1,.3,1)} +.st[data-on="1"]::before{transform:scaleY(1)} +.st[data-on="1"]{background:rgba(71,177,199,.05)} +.st h3{font-size:1.18rem;margin-bottom:.45rem} +.st p{color:var(--dim);font-size:.92rem} +.st__k{font-family:'Geist Mono',monospace;font-size:.63rem;color:var(--faint);letter-spacing:.12em} +.rig{border:1px solid var(--line);border-radius:10px;background:rgba(255,255,255,.028);padding:1.5rem} +.nodes{display:grid;grid-template-columns:repeat(3,1fr);gap:.7rem} +.nd{border:1px solid var(--line);border-radius:7px;background:rgba(0,0,0,.28);padding:.85rem .7rem; + font-family:'Geist Mono',monospace;font-size:.6rem;color:var(--faint);transition:all .4s} +.nd b{display:block;color:var(--dim);font-size:.74rem;font-weight:500;margin-bottom:.3rem} +.nd[data-live="1"]{border-color:var(--cyan);color:var(--cyan);box-shadow:0 0 24px -8px var(--cyan)} +.nd[data-live="1"] b{color:var(--ink)} +.link{height:1px;background:var(--line2);margin:1.2rem 0;position:relative;overflow:hidden} +.link::after{content:"";position:absolute;inset:0;width:32%;background:var(--glitch);animation:sweep 3.2s linear infinite} +@keyframes sweep{from{transform:translateX(-110%)}to{transform:translateX(340%)}} + +/* traction */ +.trend{display:grid;gap:1.6rem;align-items:center;margin-top:2.4rem;padding:1.4rem 1.5rem; + border:1px solid var(--line);border-radius:10px;background:rgba(95,205,228,.055)} +@media(min-width:820px){.trend{grid-template-columns:auto 1fr}} +.trend img{width:230px;height:auto;border-radius:5px} +.trend p{color:var(--dim);font-size:.94rem;max-width:56ch} +.trend b{color:var(--ink);font-weight:600} +.big{display:grid;grid-template-columns:repeat(2,1fr);margin-top:2.4rem;border-top:1px solid var(--line2)} +@media(min-width:760px){.big{grid-template-columns:repeat(3,1fr)}} +@media(min-width:1060px){.big{grid-template-columns:repeat(6,1fr)}} +.big div{padding:1.1rem .3rem;border-bottom:1px solid var(--line2)} +.big b{display:block;font-family:var(--display),sans-serif;font-weight:700;font-size:1.62rem;font-variation-settings:'wdth' 118,'wght' 820; + letter-spacing:-.02em;color:var(--cyan);font-variant-numeric:tabular-nums} +.big span{font-family:'Geist Mono',monospace;font-size:.58rem;letter-spacing:.11em; + text-transform:uppercase;color:var(--faint)} +.tl{margin-top:2.6rem;border-top:1px solid var(--line2);overflow-x:auto} +.tl__t{display:flex;gap:0;min-width:min-content;padding-top:1.4rem} +.tl__i{flex:0 0 15rem;padding:0 1.3rem 0 0;position:relative} +.tl__i::before{content:"";position:absolute;left:0;top:-1.4rem;width:7px;height:7px;border-radius:50%; + background:var(--cyan);transform:translateY(-50%) scale(0);transition:transform .45s cubic-bezier(.16,1,.3,1)} +.in .tl__i::before{transform:translateY(-50%) scale(1)} +.tl__i:nth-child(1)::before{transition-delay:.05s}.tl__i:nth-child(2)::before{transition-delay:.13s} +.tl__i:nth-child(3)::before{transition-delay:.21s}.tl__i:nth-child(4)::before{transition-delay:.29s} +.tl__i:nth-child(5)::before{transition-delay:.37s}.tl__i:nth-child(6)::before{transition-delay:.45s} +.tl__d{font-family:'Geist Mono',monospace;font-size:.63rem;letter-spacing:.1em;color:var(--cyan)} +.tl__i h4{margin:.5rem 0 .35rem;font-family:var(--display),sans-serif;font-weight:700; + font-size:1rem;font-variation-settings:'wdth' 104,'wght' 760;letter-spacing:var(--dtrack)} +.tl__i p{color:var(--dim);font-size:.83rem} +.langs{display:flex;flex-wrap:wrap;gap:.4rem;margin-top:1.6rem} +.langs span{font-family:'Geist Mono',monospace;font-size:.66rem;color:var(--faint); + border:1px solid var(--line2);padding:.28rem .5rem;border-radius:999px} + +/* cards */ +.cards{display:grid;gap:1.15rem;margin-top:2.4rem} +@media(min-width:820px){.cards{grid-template-columns:repeat(3,1fr)}} +.cd{border:1px solid var(--line);border-radius:10px;padding:1.35rem 1.3rem 1.45rem;background:rgba(255,255,255,.028); + display:flex;flex-direction:column;gap:.55rem;min-height:196px; + transition:transform .28s cubic-bezier(.16,1,.3,1),border-color .28s} +.cd:hover{transform:translateY(-4px);border-color:var(--cyan)} +.cd__k{font-family:'Geist Mono',monospace;font-size:.6rem;letter-spacing:.12em;text-transform:uppercase;color:var(--cyan)} +.cd h3{font-size:1.12rem} .cd p{color:var(--dim);font-size:.89rem} +.cd__go{margin-top:auto;font-family:'Geist Mono',monospace;font-size:.68rem;color:var(--cyan)} +.paper .cd{background:#fff;border-color:var(--p-line)} +.paper .cd p{color:var(--p-dim)} .paper .cd__k,.paper .cd__go{color:#0F6F86} +.paper .cd:hover{border-color:#0F6F86} +.cite{font-family:'Geist Mono',monospace;font-size:.63rem;color:var(--faint);margin-top:auto} + +/* terminal */ +.term{border:1px solid var(--line);border-radius:10px;background:var(--deep);overflow:hidden;margin-top:2.2rem} +.term__b{display:flex;gap:.5rem;align-items:center;padding:.6rem .85rem;border-bottom:1px solid var(--line2); + font-family:'Geist Mono',monospace;font-size:.63rem;color:var(--faint)} +.term pre{margin:0;padding:1.2rem;overflow-x:auto;font-family:'Geist Mono',monospace;font-size:.83rem;line-height:1.95} +.term .p{color:var(--cyan)} .term .c{color:var(--cyan-hi)} .term .o{color:var(--dim)} .term .h{color:var(--ink)} +.cpy{margin-left:auto;font-family:'Geist Mono',monospace;font-size:.63rem;color:var(--cyan); + background:none;border:0;cursor:pointer;padding:0} +.alts{display:flex;flex-wrap:wrap;gap:.45rem;margin-top:.9rem} +.alts span{font-family:'Geist Mono',monospace;font-size:.65rem;color:var(--faint); + border:1px solid var(--line2);padding:.3rem .55rem;border-radius:4px} +.tabs{display:flex;gap:.3rem;margin-bottom:-1px;position:relative;z-index:2} +.tab{font-family:'Geist Mono',monospace;font-size:.68rem;letter-spacing:.04em;cursor:pointer; + color:var(--faint);background:transparent;border:1px solid var(--line2);border-bottom-color:transparent; + padding:.5rem .8rem;border-radius:7px 7px 0 0;transition:color .2s,background .2s,border-color .2s} +.tab:hover{color:var(--dim)} +.tab[aria-selected="true"]{color:var(--deep);background:var(--cyan);border-color:var(--cyan);font-weight:600} +.pane[hidden]{display:none} +.ln{display:block;opacity:0;transform:translateY(3px); + transition:opacity .32s ease,transform .32s cubic-bezier(.16,1,.3,1)} +.ln.on{opacity:1;transform:none} +.caret{display:inline-block;width:.55em;height:1em;vertical-align:-.14em;margin-left:.15em; + background:var(--cyan);animation:blink 1.05s steps(2,start) infinite} +@keyframes blink{50%{opacity:0}} +@media(prefers-reduced-motion:reduce){.ln{opacity:1;transform:none}.caret{animation:none}} + +.marks{display:flex;flex-wrap:wrap;gap:.5rem;margin-top:1.8rem} +.marks span{font-family:'Geist Mono',monospace;font-size:.71rem;color:var(--dim); + border:1px solid var(--line);padding:.42rem .72rem;border-radius:999px} +.marks span:hover{border-color:var(--cyan);color:var(--ink)} + + + +footer{background:var(--deep);border-top:1px solid var(--line2);padding:3rem 0 4rem;color:var(--faint)} +.foot{display:grid;gap:2rem} +@media(min-width:760px){.foot{grid-template-columns:1.5fr 1fr 1fr 1fr}} +.foot h4{margin:0 0 .8rem;font-family:'Geist Mono',monospace;font-size:.62rem;letter-spacing:.13em; + text-transform:uppercase;color:var(--dim);font-variation-settings:normal} +.foot ul{list-style:none;margin:0;padding:0;display:grid;gap:.42rem;font-size:.88rem} +.foot a{color:var(--dim)} .foot a:hover{color:var(--ink)} +.foot img{height:64px;width:auto} +.foot__n{margin-top:2.4rem;padding-top:1.4rem;border-top:1px solid var(--line2); + font-family:'Geist Mono',monospace;font-size:.64rem;display:flex;flex-wrap:wrap;gap:.9rem} + +.rv{opacity:0;transform:translateY(20px);transition:opacity .75s cubic-bezier(.16,1,.3,1),transform .75s cubic-bezier(.16,1,.3,1)} +.rv.in{opacity:1;transform:none} +.hero h1 u b{transform:translateY(108%);transition:transform .95s cubic-bezier(.16,1,.3,1)} +.loaded .hero h1 u:nth-child(2) b{transition-delay:.09s} +.loaded .hero h1 u b{transform:none} +.fd{opacity:0;transform:translateY(12px);transition:opacity .85s ease .35s,transform .85s cubic-bezier(.16,1,.3,1) .35s} +.loaded .fd{opacity:1;transform:none} +@media(prefers-reduced-motion:reduce){ + *,*::before,*::after{animation:none!important;transition:none!important} + .rv,.fd{opacity:1;transform:none}.hero h1 u b{transform:none} + .bars i,.sz i{transform:scaleX(1)} +} diff --git a/website/static/fonts/geist-mono.woff2 b/website/static/fonts/geist-mono.woff2 new file mode 100644 index 000000000..504be8626 Binary files /dev/null and b/website/static/fonts/geist-mono.woff2 differ diff --git a/website/static/fonts/sora-700.woff2 b/website/static/fonts/sora-700.woff2 new file mode 100644 index 000000000..930c60fde Binary files /dev/null and b/website/static/fonts/sora-700.woff2 differ diff --git a/website/static/img/depth-field.jpg b/website/static/img/depth-field.jpg new file mode 100644 index 000000000..87fca63e0 Binary files /dev/null and b/website/static/img/depth-field.jpg differ diff --git a/website/static/img/hero-poster.jpg b/website/static/img/hero-poster.jpg new file mode 100644 index 000000000..ee0de7e2b Binary files /dev/null and b/website/static/img/hero-poster.jpg differ diff --git a/website/static/img/logo-full.png b/website/static/img/logo-full.png new file mode 100644 index 000000000..3c8396012 Binary files /dev/null and b/website/static/img/logo-full.png differ diff --git a/website/static/img/logo-mark.png b/website/static/img/logo-mark.png new file mode 100644 index 000000000..41692df6e Binary files /dev/null and b/website/static/img/logo-mark.png differ diff --git a/website/static/img/parakeet-speedup.jpg b/website/static/img/parakeet-speedup.jpg new file mode 100644 index 000000000..144acde14 Binary files /dev/null and b/website/static/img/parakeet-speedup.jpg differ diff --git a/website/static/img/presenter-poster.jpg b/website/static/img/presenter-poster.jpg new file mode 100644 index 000000000..f5dd218b2 Binary files /dev/null and b/website/static/img/presenter-poster.jpg differ diff --git a/website/static/img/trendshift.svg b/website/static/img/trendshift.svg new file mode 100644 index 000000000..218d2b645 --- /dev/null +++ b/website/static/img/trendshift.svg @@ -0,0 +1,16 @@ + + + + +
GITHUB TRENDING
+
+ + + + +
#1 Repository Of The Day
+
+ +
1
+
+
\ No newline at end of file diff --git a/website/static/install.sh b/website/static/install.sh new file mode 100755 index 000000000..24a44c387 --- /dev/null +++ b/website/static/install.sh @@ -0,0 +1,273 @@ +#!/bin/sh +# LocalAI installer. +# +# curl -sSL https://localai.io/install.sh | sh +# +# Downloads the release binary that matches this machine, checks it against the +# checksums published with the release, and puts it on your PATH. Nothing is +# built, nothing is left behind, and running it twice is a no-op. +# +# Environment: +# LOCALAI_VERSION pin a release, for example v4.7.1 (default: latest) +# INSTALL_DIR where to put the binary (default: /usr/local/bin, +# falling back to ~/.local/bin when that is not writable) +# LOCALAI_FORCE set to 1 to reinstall even if the same build is present +# LOCALAI_NO_SUDO set to 1 to never escalate, use ~/.local/bin instead +# GITHUB_TOKEN optional, only used to avoid GitHub API rate limits + +set -eu + +REPO="mudler/LocalAI" +API="https://api.github.com/repos/${REPO}/releases" +DL="https://github.com/${REPO}/releases/download" +BIN="local-ai" +TMPDIR_LOCALAI="" + +say() { printf '%s\n' "$*"; } +step() { printf ' %s\n' "$*"; } + +die() { + printf '\nlocalai install: %s\n' "$1" >&2 + if [ $# -gt 1 ]; then + printf '%s\n' "$2" >&2 + fi + exit 1 +} + +cleanup() { + if [ -n "$TMPDIR_LOCALAI" ] && [ -d "$TMPDIR_LOCALAI" ]; then + rm -rf "$TMPDIR_LOCALAI" + fi +} +trap cleanup EXIT INT TERM HUP + +have() { command -v "$1" >/dev/null 2>&1; } + +# --- fetching --------------------------------------------------------------- + +fetch_to_stdout() { + if have curl; then + if [ -n "${GITHUB_TOKEN:-}" ]; then + curl -fsSL -H "Authorization: Bearer ${GITHUB_TOKEN}" "$1" + else + curl -fsSL "$1" + fi + elif have wget; then + if [ -n "${GITHUB_TOKEN:-}" ]; then + wget -qO- --header="Authorization: Bearer ${GITHUB_TOKEN}" "$1" + else + wget -qO- "$1" + fi + else + die "neither curl nor wget is installed" \ + "Install one of them and run this again, or download the binary by hand from https://github.com/${REPO}/releases" + fi +} + +# fetch_to_file URL DEST. Returns non-zero instead of exiting, so optional +# files (the checksum list) can be missing without failing the install. +fetch_to_file() { + if have curl; then + curl -fsSL --retry 3 --retry-delay 1 -o "$2" "$1" + elif have wget; then + wget -q --tries=3 -O "$2" "$1" + else + die "neither curl nor wget is installed" \ + "Install one of them and run this again, or download the binary by hand from https://github.com/${REPO}/releases" + fi +} + +# --- platform --------------------------------------------------------------- + +detect_platform() { + uname_s="$(uname -s 2>/dev/null || echo unknown)" + uname_m="$(uname -m 2>/dev/null || echo unknown)" + + case "$uname_s" in + Linux) OS="linux" ;; + Darwin) OS="darwin" ;; + *) + die "unsupported operating system: ${uname_s}" \ + "LocalAI publishes binaries for Linux and macOS. On Windows, use WSL2 or the container image: docker run -p 8080:8080 localai/localai:latest" + ;; + esac + + case "$uname_m" in + x86_64 | amd64) ARCH="amd64" ;; + aarch64 | arm64) ARCH="arm64" ;; + *) + die "unsupported CPU architecture: ${uname_m}" \ + "LocalAI publishes amd64 and arm64 binaries. Build from source instead: https://localai.io/docs/getting-started/build/" + ;; + esac + + if [ "$OS" = "darwin" ] && [ "$ARCH" = "amd64" ]; then + die "there is no Intel macOS binary in the LocalAI releases" \ + "On an Intel Mac, use the container image (docker run -p 8080:8080 localai/localai:latest) or build from source: https://localai.io/docs/getting-started/build/" + fi +} + +# --- version ---------------------------------------------------------------- + +resolve_version() { + if [ -n "${LOCALAI_VERSION:-}" ]; then + VERSION="$LOCALAI_VERSION" + case "$VERSION" in + v*) ;; + *) VERSION="v${VERSION}" ;; + esac + return 0 + fi + + VERSION="$(fetch_to_stdout "${API}/latest" 2>/dev/null | + sed -n 's/.*"tag_name"[[:space:]]*:[[:space:]]*"\([^"]*\)".*/\1/p' | + head -n 1)" + + if [ -z "$VERSION" ]; then + die "could not work out the latest LocalAI release" \ + "The GitHub API may be rate limiting you. Pin a version instead, for example: LOCALAI_VERSION=v4.7.1 sh install.sh" + fi +} + +# --- checksums -------------------------------------------------------------- + +sha256_of() { + if have sha256sum; then + sha256sum "$1" | awk '{print $1}' + elif have shasum; then + shasum -a 256 "$1" | awk '{print $1}' + elif have openssl; then + openssl dgst -sha256 "$1" | awk '{print $NF}' + else + return 1 + fi +} + +# --- install target --------------------------------------------------------- + +# Sets TARGET_DIR and SUDO ("" or "sudo"). +choose_target() { + SUDO="" + + if [ -n "${INSTALL_DIR:-}" ]; then + TARGET_DIR="$INSTALL_DIR" + mkdir -p "$TARGET_DIR" 2>/dev/null || die "cannot create ${TARGET_DIR}" \ + "Pick a directory you can write to, for example: INSTALL_DIR=\$HOME/.local/bin sh install.sh" + [ -w "$TARGET_DIR" ] || die "cannot write to ${TARGET_DIR}" \ + "Pick a directory you can write to, for example: INSTALL_DIR=\$HOME/.local/bin sh install.sh" + return 0 + fi + + TARGET_DIR="/usr/local/bin" + + if mkdir -p "$TARGET_DIR" 2>/dev/null && [ -w "$TARGET_DIR" ]; then + return 0 + fi + + if [ "${LOCALAI_NO_SUDO:-0}" != "1" ] && have sudo; then + SUDO="sudo" + return 0 + fi + + TARGET_DIR="${HOME}/.local/bin" + mkdir -p "$TARGET_DIR" 2>/dev/null || die "cannot create ${TARGET_DIR}" \ + "Set INSTALL_DIR to somewhere writable and run this again." + [ -w "$TARGET_DIR" ] || die "cannot write to ${TARGET_DIR}" \ + "Set INSTALL_DIR to somewhere writable and run this again." +} + +on_path() { + case ":${PATH}:" in + *":$1:"*) return 0 ;; + *) return 1 ;; + esac +} + +# --- main ------------------------------------------------------------------- + +say "LocalAI installer" + +detect_platform +step "platform ${OS}/${ARCH}" + +resolve_version +step "release ${VERSION}" + +ASSET="${BIN}-${VERSION}-${OS}-${ARCH}" +SUMS="LocalAI-${VERSION}-checksums.txt" + +choose_target +step "install to ${TARGET_DIR}${SUDO:+ (via sudo)}" + +TMPDIR_LOCALAI="$(mktemp -d 2>/dev/null || mktemp -d -t localai)" || + die "could not create a temporary directory" + +# Expected checksum first: it tells us whether an existing install is already +# the exact build we are about to fetch, so a second run downloads nothing. +EXPECTED="" +if fetch_to_file "${DL}/${VERSION}/${SUMS}" "${TMPDIR_LOCALAI}/${SUMS}" 2>/dev/null; then + EXPECTED="$(awk -v f="$ASSET" '$2 == f || $2 == "*" f {print $1}' "${TMPDIR_LOCALAI}/${SUMS}" | head -n 1)" +fi + +if [ -n "$EXPECTED" ] && [ "${LOCALAI_FORCE:-0}" != "1" ] && [ -x "${TARGET_DIR}/${BIN}" ]; then + CURRENT="$(sha256_of "${TARGET_DIR}/${BIN}" 2>/dev/null || true)" + if [ -n "$CURRENT" ] && [ "$CURRENT" = "$EXPECTED" ]; then + step "already at ${VERSION}, nothing to do" + say "" + say "Run it with: ${BIN} run qwen3.5-35b-a3b-apex" + exit 0 + fi +fi + +step "downloading ${ASSET}" +if ! fetch_to_file "${DL}/${VERSION}/${ASSET}" "${TMPDIR_LOCALAI}/${BIN}"; then + die "could not download ${DL}/${VERSION}/${ASSET}" \ + "Check the release exists and lists that file: https://github.com/${REPO}/releases/tag/${VERSION}" +fi + +if [ ! -s "${TMPDIR_LOCALAI}/${BIN}" ]; then + die "the downloaded file is empty" \ + "Try again, or download it by hand from https://github.com/${REPO}/releases/tag/${VERSION}" +fi + +if [ -n "$EXPECTED" ]; then + if ACTUAL="$(sha256_of "${TMPDIR_LOCALAI}/${BIN}")"; then + if [ "$ACTUAL" != "$EXPECTED" ]; then + die "checksum mismatch for ${ASSET}" \ + "expected ${EXPECTED}, got ${ACTUAL}. The download was corrupted or tampered with, so nothing was installed." + fi + step "checksum ok" + else + step "checksum skipped, no sha256sum, shasum or openssl on this machine" + fi +else + step "checksum skipped, this release publishes no checksum file" +fi + +chmod 0755 "${TMPDIR_LOCALAI}/${BIN}" + +# Write to a sibling name and rename, so a running local-ai is never replaced +# underneath itself and a failed copy cannot leave a half-written binary. +if ! $SUDO cp "${TMPDIR_LOCALAI}/${BIN}" "${TARGET_DIR}/${BIN}.new"; then + die "could not write to ${TARGET_DIR}" \ + "Re-run with INSTALL_DIR set to somewhere you can write, for example: INSTALL_DIR=\$HOME/.local/bin sh install.sh" +fi +$SUDO chmod 0755 "${TARGET_DIR}/${BIN}.new" +$SUDO mv "${TARGET_DIR}/${BIN}.new" "${TARGET_DIR}/${BIN}" + +step "installed ${TARGET_DIR}/${BIN}" + +say "" +say "Next steps" +if ! on_path "$TARGET_DIR"; then + say " ${TARGET_DIR} is not on your PATH yet. Add it:" + say " export PATH=\"${TARGET_DIR}:\$PATH\"" +fi +say " Start the API and pull a model in one go:" +say " ${BIN} run qwen3.5-35b-a3b-apex" +say " Or just start the server and use the web interface:" +say " ${BIN}" +say " open http://localhost:8080" +say "" +say " Backends download themselves the first time a model asks for one." +say " Documentation: https://localai.io/docs/" diff --git a/website/static/install/kubernetes.yaml b/website/static/install/kubernetes.yaml new file mode 100644 index 000000000..98bfa3b90 --- /dev/null +++ b/website/static/install/kubernetes.yaml @@ -0,0 +1,161 @@ +# LocalAI on Kubernetes, CPU only. +# +# kubectl apply -f https://localai.io/install/kubernetes.yaml +# kubectl -n local-ai rollout status deploy/local-ai +# kubectl -n local-ai port-forward svc/local-ai 8080:8080 +# open http://localhost:8080 +# +# Everything lands in its own `local-ai` namespace, so removing it again is +# `kubectl delete namespace local-ai` (which also deletes the volumes). +# +# Two volumes, because LocalAI downloads both parts on demand and you do not +# want either of them fetched again on every restart: +# /models the model weights you install from the gallery +# /backends the engine images pulled the first time a model asks for one +# +# For GPUs, add the vendor device plugin's resource to the container's +# `resources.limits` (for example nvidia.com/gpu: 1) and switch the image to a +# GPU tag. See https://localai.io/docs/getting-started/kubernetes/ for the +# Helm chart and the GPU variants. +--- +apiVersion: v1 +kind: Namespace +metadata: + name: local-ai +--- +apiVersion: v1 +kind: PersistentVolumeClaim +metadata: + name: local-ai-models + namespace: local-ai + labels: + app.kubernetes.io/name: local-ai +spec: + accessModes: + - ReadWriteOnce + resources: + requests: + storage: 20Gi +--- +apiVersion: v1 +kind: PersistentVolumeClaim +metadata: + name: local-ai-backends + namespace: local-ai + labels: + app.kubernetes.io/name: local-ai +spec: + accessModes: + - ReadWriteOnce + resources: + requests: + storage: 20Gi +--- +apiVersion: apps/v1 +kind: Deployment +metadata: + name: local-ai + namespace: local-ai + labels: + app.kubernetes.io/name: local-ai +spec: + replicas: 1 + # The volumes are ReadWriteOnce, so the old pod has to let go before the new + # one can start. + strategy: + type: Recreate + selector: + matchLabels: + app.kubernetes.io/name: local-ai + template: + metadata: + labels: + app.kubernetes.io/name: local-ai + spec: + securityContext: + # So the mounted volumes are writable whatever the storage class does + # with ownership. + fsGroup: 1000 + containers: + - name: local-ai + image: localai/localai:latest + imagePullPolicy: IfNotPresent + ports: + - name: http + containerPort: 8080 + protocol: TCP + env: + - name: LOCALAI_MODELS_PATH + value: /models + - name: LOCALAI_BACKENDS_PATH + value: /backends + - name: LOCALAI_ADDRESS + value: ":8080" + # Uncomment to require an API key on every request. + # - name: LOCALAI_API_KEY + # valueFrom: + # secretKeyRef: + # name: local-ai + # key: api-key + volumeMounts: + - name: models + mountPath: /models + - name: backends + mountPath: /backends + resources: + requests: + cpu: "1" + memory: 4Gi + limits: + # No CPU limit on purpose: inference is CPU bound and a limit + # only buys you throttling. + memory: 12Gi + # First boot writes out its configuration before the API answers, so + # the startup probe carries the slow case and the others stay tight. + startupProbe: + httpGet: + path: /readyz + port: http + periodSeconds: 10 + failureThreshold: 60 + readinessProbe: + httpGet: + path: /readyz + port: http + periodSeconds: 10 + timeoutSeconds: 5 + failureThreshold: 3 + livenessProbe: + httpGet: + path: /healthz + port: http + periodSeconds: 30 + timeoutSeconds: 5 + failureThreshold: 5 + volumes: + - name: models + persistentVolumeClaim: + claimName: local-ai-models + - name: backends + persistentVolumeClaim: + claimName: local-ai-backends +--- +apiVersion: v1 +kind: Service +metadata: + name: local-ai + namespace: local-ai + labels: + app.kubernetes.io/name: local-ai +spec: + # ClusterIP by default, so this applies cleanly on any cluster. Reach it with + # `kubectl -n local-ai port-forward svc/local-ai 8080:8080`, or change the + # type to LoadBalancer where your cluster can provision one. + type: ClusterIP + selector: + app.kubernetes.io/name: local-ai + ports: + - name: http + protocol: TCP + port: 8080 + targetPort: http diff --git a/website/static/js/site.js b/website/static/js/site.js new file mode 100644 index 000000000..ea94ec58a --- /dev/null +++ b/website/static/js/site.js @@ -0,0 +1,412 @@ +(function(){ + var reduce = matchMedia('(prefers-reduced-motion: reduce)').matches; + requestAnimationFrame(function(){ document.body.classList.add('loaded'); }); + + var rv = [].slice.call(document.querySelectorAll('.rv')); + if (reduce) rv.forEach(function(e){ e.classList.add('in'); }); + else { + var io = new IntersectionObserver(function(es){ + es.forEach(function(e){ + if (!e.isIntersecting) return; + var sib = [].slice.call(e.target.parentNode.children).filter(function(n){return n.classList.contains('rv');}); + e.target.style.transitionDelay = Math.min(Math.max(0,sib.indexOf(e.target)),8)*60 + 'ms'; + e.target.classList.add('in'); io.unobserve(e.target); + }); + }, {rootMargin:'0px 0px -12% 0px', threshold:0.06}); + rv.forEach(function(e){ io.observe(e); }); + } + + var vio = new IntersectionObserver(function(es){ + es.forEach(function(e){ + if (e.isIntersecting) { if (!reduce) e.target.play().catch(function(){}); } + else e.target.pause(); + }); + }, {threshold:0.2}); + document.querySelectorAll('video[data-lazy]').forEach(function(v){ vio.observe(v); }); + + var fmt = new Intl.NumberFormat('en-US'); + var cio = new IntersectionObserver(function(es){ + es.forEach(function(e){ + if (!e.isIntersecting) return; + var el = e.target, txt = el.getAttribute('data-text'); cio.unobserve(el); + if (txt) { el.textContent = txt; return; } + var to = parseInt(el.getAttribute('data-count'),10)||0; + if (reduce) { el.textContent = fmt.format(to); return; } + var t0 = performance.now(); + (function tick(now){ + var p = Math.min(1,(now-t0)/1150), k = 1-Math.pow(1-p,3); + el.textContent = fmt.format(Math.round(to*k)); + if (p<1) requestAnimationFrame(tick); + })(t0); + }); + }, {threshold:0.5}); + document.querySelectorAll('[data-count]').forEach(function(e){ cio.observe(e); }); + + var bar = document.getElementById('pbar'), tick = false; + function frame(){ + tick = false; + var vh = innerHeight, max = document.body.scrollHeight - vh; + bar.style.width = (max>0 ? Math.min(1, scrollY/max)*100 : 0) + '%'; + var steps = [].slice.call(document.querySelectorAll('.st')), on = -1; + steps.forEach(function(s,i){ + var r = s.getBoundingClientRect(), hit = r.top < vh*0.6 && r.bottom > vh*0.2; + if (hit) on = i; + s.setAttribute('data-on', hit ? '1':'0'); + }); + document.querySelectorAll('#nodes .nd').forEach(function(n,i){ + n.setAttribute('data-live', on>=0 && i<=on ? '1':'0'); + }); + } + addEventListener('scroll', function(){ if(!tick){ tick=true; requestAnimationFrame(frame); } }, {passive:true}); + addEventListener('resize', frame); frame(); + + /* Background. Contour lines drawn from a real depth-anything.cpp depth map + everywhere, switching to a locate-anything.cpp style detection sweep while + you are inside the engines section. The two are separated by a scan line + that wipes across, so the change reads as a sensor switching mode. */ + (function(){ + var cv = document.getElementById('field'), ctx = cv.getContext('2d'); + var W=0, H=0, dpr = Math.min(devicePixelRatio||1, 2); + var depth=null, disp=null, GW=0, GH=0, boxes=[], seeded=false, reticles=[]; + var blend=0, target=0, last=0, t0=performance.now(), run=true; + + var LABELS=['person 0.98','laptop 0.94','cup 0.91','chair 0.88','monitor 0.96', + 'bottle 0.79','keyboard 0.92','plant 0.83','phone 0.87','book 0.81']; + var small=false, rings=[]; + function rnd(a,b){ return a+Math.random()*(b-a); } + function newBox(delay){ + var w=rnd(100,320), h=rnd(80,230); + var x, tries=0; + /* keep boxes out of the middle third on wide screens, which is where the copy sits */ + do { x=rnd(16, Math.max(40, W-w-16)); tries++; } + while (W>900 && tries<6 && x+w > W*0.22 && x < W*0.62); + return {x:x, y:rnd(70,Math.max(120,H-h-40)), w:w, h:h, + l:LABELS[Math.floor(Math.random()*LABELS.length)], t:-delay, life:rnd(3.6,7)}; + } + function seedBoxes(stagger){ + var n = small ? 5 : 9; + boxes=[]; for (var i=0;i5) rings.shift(); + if (target===1){ + var w=rnd(120,220), h=rnd(96,168); + reticles.push({cx:e.clientX, cy:e.clientY, w:w, h:h, t:0, + l:LABELS[Math.floor(Math.random()*LABELS.length)]}); + if (reticles.length>4) reticles.shift(); + } + }, {passive:true}); + + var img=new Image(); + img.onload=function(){ + var g=document.createElement('canvas'); GW=img.width; GH=img.height; + g.width=GW; g.height=GH; + var gx=g.getContext('2d',{willReadFrequently:true}); gx.drawImage(img,0,0); + var d=gx.getImageData(0,0,GW,GH).data; + depth=new Float32Array(GW*GH); + for (var i=0;i26) continue; + disp[y*GW+x] += amp*Math.cos(band*0.24)*Math.exp(-Math.abs(band)/13)*Math.exp(-dist/95); + } + } + } + } + + function contours(t, alpha){ + if (!depth) return; + var sx=W/(GW-1), sy=H/(GH-1); + var scrollN=scrollY/Math.max(1, document.body.scrollHeight-H); + var drift=Math.sin(t*.09)*.05 + scrollN*.12; + ctx.lineWidth=1; + var LN = small ? 6 : 9; + for (var li=0; li=0.99) continue; + var a = (li<3 ? .22 : li<6 ? .27 : .32) * alpha; + ctx.strokeStyle = li<3 ? 'rgba(129,129,196,'+a+')' + : li<6 ? 'rgba(95,205,228,'+a+')' + : 'rgba(189,240,250,'+a+')'; + ctx.beginPath(); + for (var y=0;yth?8:0)|(B>th?4:0)|(C>th?2:0)|(D>th?1:0); + if (id===0||id===15) continue; + var X=x*sx, Y=y*sy; + var T=[X+sx*.5,Y], R=[X+sx,Y+sy*.5], Bo=[X+sx*.5,Y+sy], L=[X,Y+sy*.5], seg=null; + if (id===1||id===14) seg=[L,Bo]; else if (id===2||id===13) seg=[Bo,R]; + else if (id===3||id===12) seg=[L,R]; else if (id===4||id===11) seg=[T,R]; + else if (id===6||id===9) seg=[T,Bo]; else seg=[L,T]; + ctx.moveTo(seg[0][0],seg[0][1]); ctx.lineTo(seg[1][0],seg[1][1]); + } + ctx.stroke(); + } + } + + function detections(dt, alpha){ + ctx.font='10px ui-monospace, SFMono-Regular, monospace'; + for (var i=0;ib.life){ boxes[i]=newBox(0); continue; } + if (b.t<0) continue; + var inK=Math.min(1,b.t/.5), outK=Math.min(1,(b.life-b.t)/.7), al=Math.min(inK,outK)*alpha; + var grow=1-Math.pow(1-inK,3), w=b.w*grow, h=b.h*grow; + ctx.globalAlpha=al*.45; ctx.strokeStyle='#5FCDE4'; ctx.lineWidth=1.4; + ctx.strokeRect(b.x,b.y,w,h); + ctx.globalAlpha=al*.09; ctx.fillStyle='#5FCDE4'; ctx.fillRect(b.x,b.y,w,h); + /* corner ticks, the way a detector overlay usually draws them */ + ctx.globalAlpha=al*.75; ctx.strokeStyle='#BDF0FA'; ctx.lineWidth=2; var c=9; + ctx.beginPath(); + ctx.moveTo(b.x,b.y+c); ctx.lineTo(b.x,b.y); ctx.lineTo(b.x+c,b.y); + ctx.moveTo(b.x+w-c,b.y+h); ctx.lineTo(b.x+w,b.y+h); ctx.lineTo(b.x+w,b.y+h-c); + ctx.stroke(); + if (inK>.55){ + var tw=ctx.measureText(b.l).width+12; + ctx.globalAlpha=al*.62; ctx.fillStyle='#5FCDE4'; ctx.fillRect(b.x,b.y-14,tw,14); + ctx.globalAlpha=al*.95; ctx.fillStyle='#04131C'; ctx.fillText(b.l,b.x+6,b.y-4); + } + } + ctx.globalAlpha=1; + } + + function acquire(dt){ + for (var i=reticles.length-1;i>=0;i--){ + var q=reticles[i]; q.t+=dt; + var LIFE=3.6; + if (q.t>LIFE){ reticles.splice(i,1); continue; } + var lock=Math.min(1, q.t/0.42); /* reticle closes in */ + var open=Math.max(0, Math.min(1,(q.t-0.42)/0.34)); /* box snaps out */ + var fade=Math.min(1,(LIFE-q.t)/0.8); + var ease=1-Math.pow(1-open,3); + + if (lock<1){ + var far=110*(1-lock), a=lock*0.9*fade; + ctx.globalAlpha=a; ctx.strokeStyle='#BDF0FA'; ctx.lineWidth=1.6; + ctx.beginPath(); + ctx.arc(q.cx,q.cy, 16+far, 0, 6.2832); ctx.stroke(); + ctx.globalAlpha=a*.55; + ctx.beginPath(); + ctx.moveTo(q.cx-24-far,q.cy); ctx.lineTo(q.cx-6,q.cy); + ctx.moveTo(q.cx+6,q.cy); ctx.lineTo(q.cx+24+far,q.cy); + ctx.moveTo(q.cx,q.cy-24-far); ctx.lineTo(q.cx,q.cy-6); + ctx.moveTo(q.cx,q.cy+6); ctx.lineTo(q.cx,q.cy+24+far); + ctx.stroke(); + } + if (open>0){ + var w=q.w*ease, h=q.h*ease, x=q.cx-w/2, y=q.cy-h/2; + ctx.globalAlpha=.55*fade; ctx.strokeStyle='#5FCDE4'; ctx.lineWidth=1.5; + ctx.strokeRect(x,y,w,h); + ctx.globalAlpha=.11*fade; ctx.fillStyle='#5FCDE4'; ctx.fillRect(x,y,w,h); + ctx.globalAlpha=.9*fade; ctx.strokeStyle='#BDF0FA'; ctx.lineWidth=2.2; + var c=12; + ctx.beginPath(); + ctx.moveTo(x,y+c); ctx.lineTo(x,y); ctx.lineTo(x+c,y); + ctx.moveTo(x+w-c,y); ctx.lineTo(x+w,y); ctx.lineTo(x+w,y+c); + ctx.moveTo(x,y+h-c); ctx.lineTo(x,y+h); ctx.lineTo(x+c,y+h); + ctx.moveTo(x+w-c,y+h); ctx.lineTo(x+w,y+h); ctx.lineTo(x+w,y+h-c); + ctx.stroke(); + if (open>0.6){ + var conf=(0.62+ease*0.34).toFixed(2); + var txt=q.l.replace(/\s[0-9.]+$/,'') + ' ' + conf; + ctx.font='10px ui-monospace, SFMono-Regular, monospace'; + var tw=ctx.measureText(txt).width+12; + ctx.globalAlpha=.85*fade; ctx.fillStyle='#5FCDE4'; ctx.fillRect(x,y-15,tw,15); + ctx.globalAlpha=fade; ctx.fillStyle='#04131C'; ctx.fillText(txt,x+6,y-4); + } + } + } + ctx.globalAlpha=1; + } + + function pings(dt){ + for (var i=rings.length-1;i>=0;i--){ + var r=rings[i]; r.t+=dt; + if (r.t>1.5){ rings.splice(i,1); continue; } + var k=r.t/1.5, ease=1-Math.pow(1-k,3); + var rad=ease*Math.max(W,H)*0.46, a=1-k; + /* a soft shockwave band with a bright leading edge */ + if (rad>6){ + var g=ctx.createRadialGradient(r.x,r.y,Math.max(0,rad-30),r.x,r.y,rad+8); + g.addColorStop(0,'rgba(95,205,228,0)'); + g.addColorStop(.72,'rgba(95,205,228,'+(a*.13).toFixed(3)+')'); + g.addColorStop(1,'rgba(189,240,250,0)'); + ctx.fillStyle=g; ctx.beginPath(); ctx.arc(r.x,r.y,rad+8,0,6.2832); ctx.fill(); + ctx.globalAlpha=a*.55; ctx.strokeStyle='#BDF0FA'; ctx.lineWidth=1.3; + ctx.beginPath(); ctx.arc(r.x,r.y,rad,0,6.2832); ctx.stroke(); + } + /* the impact point flashes and settles */ + var f=Math.max(0,1-r.t/0.34); + ctx.globalAlpha=a*.65+f*.35; ctx.fillStyle='#BDF0FA'; + var d0=2+f*7; ctx.fillRect(r.x-d0/2, r.y-d0/2, d0, d0); + } + ctx.globalAlpha=1; + } + + function scanline(y){ + var g=ctx.createLinearGradient(0,y-90,0,y+90); + g.addColorStop(0,'rgba(95,205,228,0)'); + g.addColorStop(.5,'rgba(95,205,228,.16)'); + g.addColorStop(1,'rgba(95,205,228,0)'); + ctx.fillStyle=g; ctx.fillRect(0,y-90,W,180); + ctx.globalAlpha=.9; ctx.fillStyle='#BDF0FA'; ctx.fillRect(0,y-1,W,2); + ctx.globalAlpha=.35; ctx.fillStyle='#5FCDE4'; ctx.fillRect(0,y-4,W,8); + ctx.globalAlpha=1; + } + + function render(ms){ + var t=ms/1000, dt=Math.min(.05,(ms-last)/1000); last=ms; + ripple(dt); + + var head=document.getElementById('localai'), eng=document.getElementById('engines'); + var hr=head?head.getBoundingClientRect():null, er=eng?eng.getBoundingClientRect():null; + var inTop = hr ? hr.bottom > H*0.45 : false; + var inEng = er ? (er.top < H*0.55 && er.bottom > H*0.35) : false; + var scheme=document.body.dataset.scheme||'engines', want=0; + if (scheme==='top') want = inTop?1:0; + else if (scheme==='engines') want = inEng?1:0; + else if (scheme==='both') want = (inTop||inEng)?1:0; + else if (scheme==='detect') want = 1; + if (want !== target){ target=want; if (want) seedBoxes(true); } + /* the layer steps back once the reading starts, so copy always wins */ + cv.style.opacity = (hr && hr.bottom > H*0.25) ? '1' : (small ? '0.72' : '0.85'); + var d=target-blend; + if (Math.abs(d)>0.0015) blend += d*Math.min(1, dt*2.6); else blend=target; + + ctx.clearRect(0,0,W,H); + glow(t); + + if (blend<=0.002){ contours(t,1); } + else if (blend>=0.998){ detections(dt,1); } + else { + var y=blend*H; + ctx.save(); ctx.beginPath(); ctx.rect(0,0,W,y); ctx.clip(); + detections(dt, Math.min(1, blend*1.6)); ctx.restore(); + ctx.save(); ctx.beginPath(); ctx.rect(0,y,W,H-y); ctx.clip(); + contours(t, Math.min(1,(1-blend)*1.6)); ctx.restore(); + scanline(y); + } + acquire(dt); + pings(dt); + } + + function loop(now){ + if (!run) return; + render(now-t0); + if (!reduce) requestAnimationFrame(loop); + } + })(); + + /* the install transcript prints itself the first time you reach it */ + (function(){ + function prep(pre){ + if (pre.dataset.split) return; + pre.dataset.split = '1'; + pre.innerHTML = pre.innerHTML.split('\n') + .map(function(l){ return '' + (l || ' ') + ''; }).join(''); + pre.insertAdjacentHTML('beforeend', ''); + } + function play(pre){ + prep(pre); + var lines = pre.querySelectorAll('.ln'); + lines.forEach(function(l,i){ + l.classList.remove('on'); + setTimeout(function(){ l.classList.add('on'); }, reduce ? 0 : 90 + i*135); + }); + } + window.__playTerm = play; + var term = document.querySelector('#p-script pre'); + if (!term) return; + var io = new IntersectionObserver(function(es){ + es.forEach(function(e){ if (e.isIntersecting){ play(e.target); io.unobserve(e.target); } }); + }, {threshold:0.35}); + io.observe(term); + })(); + + var CMD = {'p-script':'curl -sSL https://localai.io/install.sh | sh', + 'p-docker':'docker run -p 8080:8080 --name local-ai -ti localai/localai:latest', + 'p-k8s':'kubectl apply -f https://localai.io/install/kubernetes.yaml'}; + var LBL = {'p-script':'bash','p-docker':'docker','p-k8s':'kubectl'}; + document.querySelectorAll('.tab').forEach(function(t){ + t.addEventListener('click', function(){ + document.querySelectorAll('.tab').forEach(function(o){ o.setAttribute('aria-selected', String(o===t)); }); + document.querySelectorAll('.pane').forEach(function(p){ p.hidden = (p.id !== t.dataset.pane); }); + var c = document.getElementById('cpy'); + c.dataset.copy = CMD[t.dataset.pane]; c.textContent = 'copy'; + document.getElementById('term-label').textContent = LBL[t.dataset.pane]; + var pre = document.querySelector('#' + t.dataset.pane + ' pre'); + if (pre && window.__playTerm) window.__playTerm(pre); + }); + }); + + document.body.dataset.scheme = 'engines'; + + var pl=document.getElementById('player'), pv=document.getElementById('pv'); + if (pl && pv){ + pl.addEventListener('click', function(){ + if (pv.paused){ pv.play(); pl.classList.add('playing'); } + else { pv.pause(); pl.classList.remove('playing'); } + }); + pv.addEventListener('ended', function(){ pl.classList.remove('playing'); }); + pv.addEventListener('pause', function(){ pl.classList.remove('playing'); }); + } + + document.querySelectorAll('.cpy').forEach(function(b){ + b.addEventListener('click', function(){ + navigator.clipboard.writeText(b.dataset.copy).then(function(){ + b.textContent = 'copied'; setTimeout(function(){ b.textContent='copy'; }, 1600); + }); + }); + }); +})(); diff --git a/website/static/media/ced.mp4 b/website/static/media/ced.mp4 new file mode 100644 index 000000000..3d4297e73 Binary files /dev/null and b/website/static/media/ced.mp4 differ diff --git a/website/static/media/depth-race.mp4 b/website/static/media/depth-race.mp4 new file mode 100644 index 000000000..85254fc25 Binary files /dev/null and b/website/static/media/depth-race.mp4 differ diff --git a/website/static/media/depth.mp4 b/website/static/media/depth.mp4 new file mode 100644 index 000000000..c65ca379c Binary files /dev/null and b/website/static/media/depth.mp4 differ diff --git a/website/static/media/face-id.mp4 b/website/static/media/face-id.mp4 new file mode 100644 index 000000000..5f36afbd2 Binary files /dev/null and b/website/static/media/face-id.mp4 differ diff --git a/website/static/media/face.mp4 b/website/static/media/face.mp4 new file mode 100644 index 000000000..f7e12a3df Binary files /dev/null and b/website/static/media/face.mp4 differ diff --git a/website/static/media/gallery.mp4 b/website/static/media/gallery.mp4 new file mode 100644 index 000000000..bbe9c6ec7 Binary files /dev/null and b/website/static/media/gallery.mp4 differ diff --git a/website/static/media/hero-ui.mp4 b/website/static/media/hero-ui.mp4 new file mode 100644 index 000000000..d4989b3b1 Binary files /dev/null and b/website/static/media/hero-ui.mp4 differ diff --git a/website/static/media/locate.mp4 b/website/static/media/locate.mp4 new file mode 100644 index 000000000..e5f049860 Binary files /dev/null and b/website/static/media/locate.mp4 differ diff --git a/website/static/media/magpie.mp4 b/website/static/media/magpie.mp4 new file mode 100644 index 000000000..96c076d1c Binary files /dev/null and b/website/static/media/magpie.mp4 differ diff --git a/website/static/media/moss.mp4 b/website/static/media/moss.mp4 new file mode 100644 index 000000000..d99cad425 Binary files /dev/null and b/website/static/media/moss.mp4 differ diff --git a/website/static/media/nib.mp4 b/website/static/media/nib.mp4 new file mode 100644 index 000000000..51a77138c Binary files /dev/null and b/website/static/media/nib.mp4 differ diff --git a/website/static/media/parakeet-duel.mp4 b/website/static/media/parakeet-duel.mp4 new file mode 100644 index 000000000..b023a8d53 Binary files /dev/null and b/website/static/media/parakeet-duel.mp4 differ diff --git a/website/static/media/parakeet-long.mp4 b/website/static/media/parakeet-long.mp4 new file mode 100644 index 000000000..b4a9d0766 Binary files /dev/null and b/website/static/media/parakeet-long.mp4 differ diff --git a/website/static/media/presenter.mp4 b/website/static/media/presenter.mp4 new file mode 100644 index 000000000..a1027b494 Binary files /dev/null and b/website/static/media/presenter.mp4 differ diff --git a/website/static/media/vllm-race.mp4 b/website/static/media/vllm-race.mp4 new file mode 100644 index 000000000..364af52d7 Binary files /dev/null and b/website/static/media/vllm-race.mp4 differ diff --git a/website/static/media/voice.mp4 b/website/static/media/voice.mp4 new file mode 100644 index 000000000..5208154c8 Binary files /dev/null and b/website/static/media/voice.mp4 differ