feat(website): split the site, move docs to /docs, add a landing page (#11243)
* feat(website): split the site, move docs to /docs, add a landing page The Hugo docs site has always been localai.io itself, which left nowhere to explain what LocalAI is or show what the team builds. This adds a separate marketing site at the root and moves the documentation under /docs/. Docs: The existing site keeps its content tree and its Relearn theme, and now builds with baseURL <root>/docs/. Its _index.md, which held a hand written landing page, becomes a real documentation home. Every previously published URL keeps working. GitHub Pages has no server side rewrites, so .github/ci/gen-redirects.sh walks the built docs output and leaves a meta refresh plus a canonical link at each old root path. It covers bare .html files too, which is what keeps /gallery.html alive, and it never overwrites a path the marketing site already owns. Website: A second Hugo site under website/ with its own layouts and no external theme, so the marketing side does not have to fight Relearn's home rooted menu and asset pipeline. CI builds both and merges them into one Pages artifact. The design is derived from the project logo rather than invented: the navy of the triangle, the cyan of the llama, the purple of the speed bars. Those offset bars became the motion signature. The background renders a real depth-anything.cpp depth map as contour lines and switches to a locate-anything.cpp style detection overlay over the engines section. Also included: an /engines/ index driven entirely by data/engines.yaml, a /blog/ section with five posts written from the release notes and the engine benchmark suites, install.sh and a Kubernetes manifest since the site advertises both, and a rule in .agents/ that release preparation now includes a blog post and demo clips. Every figure on the site is derived from the repository or the GitHub API, not from memory. Correcting them against their sources found one error in README.md: voxtral-tts.c is text to speech, not speech to text. Signed-off-by: Ettore Di Giacinto <mudler@localai.io> Assisted-by: Claude Code:claude-opus-5 [Bash] [Edit] [Write] [Agent] * feat(website): add a star history chart, rewrite the history post in first person The history post read like a changelog written by a committee. It is now in Ettore's voice, first person, with the admissions left in. The numbers paragraph in particular read like a directory listing. It now says what the figures mean rather than which file they came from. Adds an interactive star history chart, built from the GitHub stargazers API rather than embedded from a third party, so the page makes no external request and cannot break when someone else's service is down. The four releases the post is organised around are marked on the curve, and the labels stack into rows because three of them land within two months of each other. The API stops paginating at 40,000 items, so the curve is measured up to December 2025 and the segment from there to today's total is drawn dashed, labelled as an estimate in the caption and in the tooltip. It is a straight line between two known points, and the chart says so rather than implying it is data. Also drops "marketing site" from the README heading and everywhere else it appeared, and calls it the main site instead. Signed-off-by: Ettore Di Giacinto <mudler@localai.io> Assisted-by: Claude Code:claude-opus-5 [Bash] [Edit] [Write] --------- Signed-off-by: Ettore Di Giacinto <mudler@localai.io> Co-authored-by: Ettore Di Giacinto <mudler@localai.io>
26
.agents/preparing-a-release.md
Normal file
@@ -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.
|
||||
76
.github/ci/gen-redirects.sh
vendored
Executable file
@@ -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 <public-dir> [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 <public-dir> [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' '<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<title>Moved</title>
|
||||
<link rel="canonical" href="'"$target"'">
|
||||
<meta name="robots" content="noindex">
|
||||
<meta http-equiv="refresh" content="0; url='"$target"'">
|
||||
</head>
|
||||
<body>
|
||||
<p>This page moved to <a href="'"$target"'">'"$target"'</a>.</p>
|
||||
</body>
|
||||
</html>
|
||||
' > "$dst"
|
||||
|
||||
created=$((created + 1))
|
||||
done <<EOF
|
||||
$(find "$DOCS_DIR" -type f -name '*.html' | sort)
|
||||
EOF
|
||||
|
||||
echo "gen-redirects: ${created} redirect(s) written, ${skipped} path(s) left to the main site"
|
||||
27
.github/workflows/gh-pages.yml
vendored
@@ -1,4 +1,4 @@
|
||||
name: Deploy docs to GitHub Pages
|
||||
name: Deploy site to GitHub Pages
|
||||
|
||||
on:
|
||||
push:
|
||||
@@ -6,9 +6,11 @@ on:
|
||||
- master
|
||||
paths:
|
||||
- 'docs/**'
|
||||
- 'website/**'
|
||||
- 'gallery/**'
|
||||
- 'images/**'
|
||||
- '.github/ci/modelslist.go'
|
||||
- '.github/ci/gen-redirects.sh'
|
||||
- '.github/workflows/gh-pages.yml'
|
||||
workflow_dispatch:
|
||||
|
||||
@@ -49,19 +51,36 @@ jobs:
|
||||
id: pages
|
||||
uses: actions/configure-pages@v6
|
||||
|
||||
# The gallery page is generated from the model index and shipped as a
|
||||
# static asset of the docs site, so it has to exist before Hugo runs.
|
||||
- name: Generate gallery
|
||||
run: go run ./.github/ci/modelslist.go ./gallery/index.yaml > 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:
|
||||
|
||||
5
.gitignore
vendored
@@ -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
|
||||
|
||||
@@ -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).
|
||||
|
||||
25
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
|
||||
########################################################
|
||||
|
||||
@@ -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) |
|
||||
|
||||
@@ -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"
|
||||
+++
|
||||
|
||||
<div class="lai-home">
|
||||
<section class="lai-hero">
|
||||
<div class="lai-hero__copy">
|
||||
<p class="lai-signal"><span></span> Open source · MIT licensed</p>
|
||||
<h1>One runtime.<br><strong>Every kind of AI.</strong><br>Your hardware.</h1>
|
||||
<p class="lai-hero__lede">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.</p>
|
||||
<div class="lai-actions">
|
||||
<a class="lai-button" href="/installation/">Install LocalAI <b>→</b></a>
|
||||
<a class="lai-link" href="https://github.com/mudler/LocalAI">View on GitHub ↗</a>
|
||||
</div>
|
||||
<div class="lai-proof"><span>60+ backends</span><span>CPU to cluster</span><span>OpenAI · Anthropic · Ollama · ElevenLabs APIs</span></div>
|
||||
</div>
|
||||
</section>
|
||||
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.
|
||||
|
||||
<section class="lai-breadth">
|
||||
<header><p>The runtime, not just the endpoint.</p><h2>Bring the model. Choose the engine. Keep control.</h2></header>
|
||||
<div class="lai-lanes">
|
||||
<a href="/features/text-generation/"><span>Reason</span><b>Language models · tools · structured output</b><em>Text</em></a>
|
||||
<a href="/features/openai-realtime/"><span>Listen & speak</span><b>Realtime WebRTC · transcription · TTS · diarization</b><em>Voice</em></a>
|
||||
<a href="/features/image-generation/"><span>Create</span><b>Images · video · music · sound</b><em>Media</em></a>
|
||||
<a href="/features/object-detection/"><span>See</span><b>Vision · detection · recognition · depth</b><em>Perception</em></a>
|
||||
<a href="/features/agents/"><span>Act</span><b>Agents · MCP · skills · RAG · interactive tools</b><em>Agentic</em></a>
|
||||
</div>
|
||||
</section>
|
||||
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" %}}).
|
||||
|
||||
<section class="lai-architecture">
|
||||
<div class="lai-architecture__copy">
|
||||
<p>A small core, not a giant bundle.</p>
|
||||
<h2>Backends arrive when the model needs them.</h2>
|
||||
<p>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.</p>
|
||||
<ul><li>Install, update, or remove engines independently.</li><li>Mix CPU, NVIDIA, AMD, Intel, Apple Silicon, Vulkan, and Jetson.</li><li>Build your own backend in any language through an open gRPC contract.</li></ul>
|
||||
<a href="/reference/architecture/">Explore the architecture →</a>
|
||||
</div>
|
||||
<figure><img src="/images/diagrams/composable-core.png" alt="LocalAI's small core connected to independent on-demand model backends" /></figure>
|
||||
</section>
|
||||
```bash
|
||||
docker run -ti --name local-ai -p 8080:8080 localai/localai:latest
|
||||
```
|
||||
|
||||
<section class="lai-engines">
|
||||
<div class="lai-engines__intro">
|
||||
<p>We integrate the best engines. We build new ones, too.</p>
|
||||
<h2>Inference work that moves the open ecosystem forward.</h2>
|
||||
<p>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.</p>
|
||||
<a href="https://github.com/mudler/LocalAI#backends-built-by-us">See the engines we maintain ↗</a>
|
||||
</div>
|
||||
<div class="lai-engine-reel">
|
||||
<div><span>Speech</span><b>parakeet.cpp</b><small>Streaming multilingual ASR</small></div>
|
||||
<div><span>Voice</span><b>vibevoice.cpp</b><small>Long-form TTS and ASR</small></div>
|
||||
<div><span>Identity</span><b>voice-detect.cpp</b><small>Speaker recognition and analysis</small></div>
|
||||
<div><span>Vision</span><b>face-detect.cpp</b><small>Recognition and anti-spoofing</small></div>
|
||||
<div><span>Perception</span><b>locate-anything.cpp</b><small>Open-vocabulary detection</small></div>
|
||||
<div><span>Privacy</span><b>privacy-filter.cpp</b><small>Native PII detection</small></div>
|
||||
<div><span>3D</span><b>free-splatter.cpp</b><small>Pose-free reconstruction</small></div>
|
||||
<div><span>Quantization</span><b>apex-quant</b><small>MoE-aware GGUF recipes</small></div>
|
||||
</div>
|
||||
</section>
|
||||
## Sections
|
||||
|
||||
<section class="lai-scale">
|
||||
<header><p>Start on one machine. Keep going.</p><h2>The same runtime from workstation to private AI fabric.</h2></header>
|
||||
<div class="lai-scale__path">
|
||||
<div><span>01</span><b>Laptop</b><p>Run useful models locally, including CPU-only setups.</p></div>
|
||||
<div><span>02</span><b>Team server</b><p>Add authentication, API keys, roles, quotas, and usage visibility.</p></div>
|
||||
<div><span>03</span><b>Distributed cluster</b><p>Route across workers, fit models across devices, and scale with demand.</p></div>
|
||||
</div>
|
||||
</section>
|
||||
- **[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.
|
||||
|
||||
<section class="lai-platform">
|
||||
<div><p>More than inference</p><h2>A complete local AI control plane.</h2></div>
|
||||
<div class="lai-platform__list">
|
||||
<article><b>Agents built in</b><p>Create agents with MCP tools, skills, memory, RAG, citations, and streamed execution from the UI or API.</p></article>
|
||||
<article><b>Realtime by design</b><p>Build interruptible voice experiences with WebRTC, streaming STT, LLM output, and TTS.</p></article>
|
||||
<article><b>Privacy you can enforce</b><p>Keep data on your infrastructure and add PII analysis, redaction, policy middleware, and audit visibility.</p></article>
|
||||
<article><b>Models under your control</b><p>Discover capabilities, import models, fine-tune, quantize, route, and monitor them in one place.</p></article>
|
||||
</div>
|
||||
</section>
|
||||
## Also useful
|
||||
|
||||
<section class="lai-start">
|
||||
<div><p>One command to begin</p><h2>Run your first local AI stack.</h2></div>
|
||||
<pre><code>docker run -ti --name local-ai -p 8080:8080 localai/localai:latest</code></pre>
|
||||
<div class="lai-start__links"><a href="/installation/">Installation options</a><a href="https://models.localai.io">Browse models</a><a href="/model-compatibility/">Compare backends</a><a href="https://discord.gg/uJAeKSAGDy">Join Discord</a></div>
|
||||
</section>
|
||||
</div>
|
||||
- **[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.
|
||||
|
||||
@@ -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"
|
||||
|
||||
+++
|
||||
|
||||
@@ -5,7 +5,7 @@ toc = true
|
||||
description = "What is LocalAI?"
|
||||
tags = ["Beginners"]
|
||||
categories = [""]
|
||||
url = "/docs/overview"
|
||||
url = "/overview"
|
||||
author = "Ettore Di Giacinto"
|
||||
icon = "info"
|
||||
+++
|
||||
|
||||
@@ -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'
|
||||
|
||||
|
||||
100
docs/static/css/custom.css
vendored
@@ -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);
|
||||
}
|
||||
}
|
||||
4
docs/static/css/localai-home.css
vendored
@@ -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}}
|
||||
29
website/README.md
Normal file
@@ -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`).
|
||||
4
website/content/_index.md
Normal file
@@ -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."
|
||||
---
|
||||
5
website/content/blog/_index.md
Normal file
@@ -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"]
|
||||
---
|
||||
112
website/content/blog/apex-moe-quantization.md
Normal file
@@ -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.
|
||||
|
||||
<div class="tw">
|
||||
<table>
|
||||
<thead><tr><th>Build</th><th>Size</th><th>Perplexity</th><th>KL mean</th><th>HellaSwag</th><th>MMLU</th><th>tg128 t/s</th></tr></thead>
|
||||
<tbody>
|
||||
<tr><td>F16</td><td>64.6 GB</td><td>6.537</td><td>-</td><td>82.5%</td><td>41.5%</td><td>30.4</td></tr>
|
||||
<tr><td>Q8_0</td><td>34.4 GB</td><td>6.533</td><td>0.0046</td><td>83.0%</td><td>41.2%</td><td>52.5</td></tr>
|
||||
<tr><td>Unsloth UD-Q8_K_XL</td><td>45.3 GB</td><td>6.536</td><td>0.0025</td><td>82.5%</td><td>41.3%</td><td>36.4</td></tr>
|
||||
<tr><td>Unsloth UD-Q4_K_XL</td><td>20.7 GB</td><td>6.554</td><td>0.0097</td><td>83.0%</td><td>40.6%</td><td>58.1</td></tr>
|
||||
<tr><td>bartowski IQ2_M</td><td>11.3 GB</td><td>7.303</td><td>0.1113</td><td>80.3%</td><td>39.6%</td><td>76.2</td></tr>
|
||||
<tr><td><b>APEX Quality</b></td><td><b>21.3 GB</b></td><td><b>6.527</b></td><td>0.0114</td><td>83.0%</td><td>41.2%</td><td><b>62.3</b></td></tr>
|
||||
<tr><td><b>APEX I-Quality</b></td><td><b>21.3 GB</b></td><td>6.552</td><td>0.0102</td><td><b>83.5%</b></td><td>41.4%</td><td>63.1</td></tr>
|
||||
<tr><td><b>APEX Balanced</b></td><td>23.6 GB</td><td>6.533</td><td>0.0088</td><td>83.0%</td><td>41.3%</td><td>60.8</td></tr>
|
||||
<tr><td><b>APEX Compact</b></td><td>16.1 GB</td><td>6.783</td><td>0.0469</td><td>82.5%</td><td>40.9%</td><td>69.8</td></tr>
|
||||
<tr><td><b>APEX I-Compact</b></td><td>16.1 GB</td><td>6.669</td><td>0.0332</td><td>81.8%</td><td>41.7%</td><td>69.8</td></tr>
|
||||
<tr><td><b>APEX Mini</b></td><td><b>12.2 GB</b></td><td>7.088</td><td>0.0870</td><td>81.0%</td><td>41.3%</td><td><b>74.4</b></td></tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
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).
|
||||
79
website/content/blog/localai-since-march-2023.md
Normal file
@@ -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.
|
||||
104
website/content/blog/parakeet-cpp-asr-on-cpu.md
Normal file
@@ -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.
|
||||
|
||||
<div class="tw">
|
||||
<table>
|
||||
<thead><tr><th>Model</th><th>RTFx NeMo</th><th>RTFx f32</th><th>Speedup f32</th><th>Speedup q8_0</th><th>Agreement WER</th><th>RSS NeMo</th><th>RSS ours</th></tr></thead>
|
||||
<tbody>
|
||||
<tr><td>ctc-0.6b</td><td>30.7</td><td>41.7</td><td>1.36x</td><td>1.61x</td><td>0.0000%</td><td>5447 MB</td><td>2457 MB</td></tr>
|
||||
<tr><td>ctc-1.1b</td><td>17.8</td><td>26.1</td><td>1.46x</td><td>1.63x</td><td>0.0222%</td><td>8914 MB</td><td>4201 MB</td></tr>
|
||||
<tr><td>rnnt-0.6b</td><td>24.3</td><td>34.1</td><td>1.40x</td><td>1.52x</td><td>0.0000%</td><td>5516 MB</td><td>2487 MB</td></tr>
|
||||
<tr><td>rnnt-1.1b</td><td>15.3</td><td>21.4</td><td>1.40x</td><td>1.57x</td><td>0.0000%</td><td>8982 MB</td><td>4231 MB</td></tr>
|
||||
<tr><td>tdt-0.6b-v2</td><td>22.4</td><td>32.4</td><td>1.45x</td><td>1.55x</td><td>0.0000%</td><td>5499 MB</td><td>2545 MB</td></tr>
|
||||
<tr><td>tdt-1.1b</td><td>14.6</td><td>22.6</td><td>1.54x</td><td>1.74x</td><td>0.0000%</td><td>8956 MB</td><td>4231 MB</td></tr>
|
||||
<tr><td>tdt_ctc-1.1b</td><td>13.3</td><td>22.5</td><td><b>1.69x</b></td><td><b>1.89x</b></td><td>0.0985%</td><td>8909 MB</td><td>4236 MB</td></tr>
|
||||
<tr><td>tdt_ctc-110m</td><td>72.9</td><td>81.1</td><td>1.11x</td><td>1.26x</td><td>0.0196%</td><td>1650 MB</td><td>563 MB</td></tr>
|
||||
<tr><td>rt-eou-120m-v1</td><td>61.8</td><td>70.6</td><td>1.14x</td><td>1.23x</td><td>0.0000%</td><td>1714 MB</td><td>621 MB</td></tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<figure>
|
||||
<img src="/img/parakeet-speedup.jpg" alt="Bar chart of parakeet.cpp CPU speedup over NeMo, per model and per GGUF dtype" loading="lazy">
|
||||
<figcaption>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.</figcaption>
|
||||
</figure>
|
||||
|
||||
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 `<EOU>` when the speaker has finished a turn and `<EOB>` for a backchannel, as events alongside the text. A voice loop can start generating a reply the moment `<EOU>` 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 `<EOU>` 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.
|
||||
|
||||
<div class="tw">
|
||||
<table>
|
||||
<thead><tr><th>Attention</th><th>Window</th><th>Wall clock</th><th>RTFx</th><th>Peak RSS</th></tr></thead>
|
||||
<tbody>
|
||||
<tr><td>full (global)</td><td>-</td><td>148.3 s</td><td>6.7x</td><td>54.0 GB</td></tr>
|
||||
<tr><td>banded</td><td>W=32</td><td>39.5 s</td><td>25.2x</td><td>8.9 GB</td></tr>
|
||||
<tr><td><b>banded</b></td><td><b>W=128</b></td><td><b>36.9 s</b></td><td><b>27.0x</b></td><td><b>9.4 GB</b></td></tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
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`.
|
||||
125
website/content/blog/what-landed-in-localai-4-8.md
Normal file
@@ -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.
|
||||
|
||||
<div class="tw">
|
||||
<table>
|
||||
<thead><tr><th>Measurement</th><th>Before</th><th>After</th><th>Change</th></tr></thead>
|
||||
<tbody>
|
||||
<tr><td>React JS + CSS over the wire</td><td>2,815,513 B</td><td>807,918 B</td><td><b>3.48x smaller</b></td></tr>
|
||||
<tr><td>All embedded assets, fonts included</td><td>3,953,917 B</td><td>1,559,787 B</td><td>2.53x smaller</td></tr>
|
||||
<tr><td>Repeat navigation asset transfer</td><td>full re-download</td><td>0 bytes</td><td>eliminated</td></tr>
|
||||
<tr><td><code>/api/backend-traces</code> poll payload</td><td>21,131,097 B</td><td>7,201 B</td><td><b>~2900x smaller</b></td></tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
## 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:<n>, num_blocks:<n>
|
||||
```
|
||||
|
||||
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.
|
||||
88
website/content/blog/why-we-write-our-own-engines.md
Normal file
@@ -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`:
|
||||
|
||||
<div class="tw">
|
||||
<table>
|
||||
<thead><tr><th>Concurrency</th><th>1</th><th>2</th><th>4</th><th>8</th><th>16</th><th>32</th></tr></thead>
|
||||
<tbody>
|
||||
<tr><td><b>vllm.cpp</b> tok/s</td><td><b>86.05</b></td><td><b>159.68</b></td><td><b>292.34</b></td><td><b>508.77</b></td><td><b>801.76</b></td><td><b>1095.01</b></td></tr>
|
||||
<tr><td>vLLM tok/s</td><td>82.32</td><td>158.03</td><td>290.31</td><td>505.46</td><td>789.16</td><td>1076.25</td></tr>
|
||||
<tr><td>Ratio</td><td>1.045x</td><td>1.011x</td><td>1.007x</td><td>1.007x</td><td>1.016x</td><td>1.017x</td></tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
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.
|
||||
|
||||
<div class="tw">
|
||||
<table>
|
||||
<thead><tr><th>Engine</th><th>Quant</th><th>Model MB</th><th>Load ms</th><th>Infer ms</th><th>Peak RAM MB</th><th>vs PyTorch</th></tr></thead>
|
||||
<tbody>
|
||||
<tr><td>PyTorch</td><td>f32</td><td>516</td><td>749</td><td>416.9</td><td>1328</td><td>1.00x</td></tr>
|
||||
<tr><td><b>C++/ggml</b></td><td>q8_0</td><td>142</td><td><b>40</b></td><td><b>319.4</b></td><td><b>363</b></td><td><b>1.31x</b></td></tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
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).
|
||||
5
website/content/engines/_index.md
Normal file
@@ -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"]
|
||||
---
|
||||
281
website/data/engines.yaml
Normal file
@@ -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
|
||||
6
website/data/milestones.json
Normal file
@@ -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"}
|
||||
]
|
||||
1
website/data/stars.json
Normal file
@@ -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]]
|
||||
1
website/data/starsmeta.json
Normal file
@@ -0,0 +1 @@
|
||||
{"measuredUntil": "2025-12-24", "measuredStars": 40000, "note": "GitHub caps stargazers pagination at 400 pages"}
|
||||
31
website/hugo.toml
Normal file
@@ -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 <root>/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'
|
||||
12
website/layouts/_default/baseof.html
Normal file
@@ -0,0 +1,12 @@
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
{{ partial "head.html" . }}
|
||||
<body>
|
||||
<div class="prog"><i id="pbar"></i></div>
|
||||
<canvas id="field" aria-hidden="true"></canvas>
|
||||
{{ partial "nav.html" . }}
|
||||
{{ block "main" . }}{{ end }}
|
||||
{{ partial "footer.html" . }}
|
||||
<script src="{{ "js/site.js" | relURL }}" defer></script>
|
||||
</body>
|
||||
</html>
|
||||
17
website/layouts/_default/taxonomy.html
Normal file
@@ -0,0 +1,17 @@
|
||||
{{ define "main" }}
|
||||
<main id="top">
|
||||
<section class="navy">
|
||||
<div class="shell">
|
||||
<div class="bars" aria-hidden="true"><i></i><i></i><i></i><i></i></div>
|
||||
<p class="kicker">Tags</p>
|
||||
<h2 class="mt1">Everything we have written about</h2>
|
||||
<div class="chips mt2">
|
||||
{{ range .Data.Terms.Alphabetical }}
|
||||
<a href="{{ .Page.RelPermalink }}"><span>{{ .Page.Title }} ({{ .Count }})</span></a>
|
||||
{{ end }}
|
||||
</div>
|
||||
<p class="mt3"><a class="btn btn--o" href="{{ "blog/" | relURL }}">All posts →</a></p>
|
||||
</div>
|
||||
</section>
|
||||
</main>
|
||||
{{ end }}
|
||||
23
website/layouts/_default/term.html
Normal file
@@ -0,0 +1,23 @@
|
||||
{{ define "main" }}
|
||||
<main id="top">
|
||||
<section class="navy">
|
||||
<div class="shell">
|
||||
<div class="bars" aria-hidden="true"><i></i><i></i><i></i><i></i></div>
|
||||
<p class="kicker">Tagged</p>
|
||||
<h2 class="mt1">{{ .Title }}</h2>
|
||||
<p class="lede mt2">{{ len .Pages }} post{{ if ne (len .Pages) 1 }}s{{ end }} tagged {{ .Title }}.</p>
|
||||
<div class="cards">
|
||||
{{ range .Pages.ByDate.Reverse }}
|
||||
<a class="cd" href="{{ .RelPermalink }}">
|
||||
<p class="cd__k">{{ with .Params.category }}{{ . }}{{ else }}Post{{ end }}</p>
|
||||
<h3>{{ .Title }}</h3>
|
||||
<p>{{ .Params.summary | default .Summary }}</p>
|
||||
<span class="cd__go">Read the post →</span>
|
||||
</a>
|
||||
{{ end }}
|
||||
</div>
|
||||
<p class="mt3"><a class="btn btn--o" href="{{ "blog/" | relURL }}">All posts →</a></p>
|
||||
</div>
|
||||
</section>
|
||||
</main>
|
||||
{{ end }}
|
||||
45
website/layouts/blog/list.html
Normal file
@@ -0,0 +1,45 @@
|
||||
{{ define "main" }}
|
||||
<main id="top">
|
||||
|
||||
<section class="navy bp-top">
|
||||
<div class="shell">
|
||||
<div class="bars" aria-hidden="true"><i></i><i></i><i></i><i></i></div>
|
||||
<p class="kicker">From the team</p>
|
||||
<h1 class="bp-h1 mt1">{{ .Title }}</h1>
|
||||
{{ with .Description }}<p class="lede mt2">{{ . }}</p>{{ end }}
|
||||
{{ with .Content }}<div class="bp-intro">{{ . }}</div>{{ end }}
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="navy bp-index">
|
||||
<div class="shell">
|
||||
<div class="bp-list">
|
||||
{{ range .Pages.ByDate.Reverse }}
|
||||
<a class="bp-item rv" href="{{ .RelPermalink }}">
|
||||
<span class="bp-item__m">
|
||||
<time datetime="{{ .Date.Format "2006-01-02" }}">{{ .Date.Format "2 Jan 2006" }}</time>
|
||||
{{ with .Params.category }}<em>{{ . }}</em>{{ end }}
|
||||
</span>
|
||||
<span class="bp-item__b">
|
||||
<span class="bp-item__t">{{ .Title }}</span>
|
||||
<span class="bp-item__s">{{ with .Params.summary }}{{ . }}{{ else }}{{ .Summary }}{{ end }}</span>
|
||||
</span>
|
||||
<span class="bp-item__go">Read →</span>
|
||||
</a>
|
||||
{{ end }}
|
||||
</div>
|
||||
|
||||
<div class="bp-end rv">
|
||||
<p class="kicker">Next</p>
|
||||
<h2 class="bp-end__h">Install it and check the numbers yourself.</h2>
|
||||
<p>Every figure in these posts comes out of a benchmark suite or a release that you can run on your own hardware.</p>
|
||||
<div class="acts">
|
||||
<a class="btn" href="{{ .Site.Params.docsURL }}">Read the documentation →</a>
|
||||
<a class="btn btn--o" href="{{ .Site.Params.github }}">LocalAI on GitHub ↗</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
</main>
|
||||
{{ end }}
|
||||
66
website/layouts/blog/single.html
Normal file
@@ -0,0 +1,66 @@
|
||||
{{ define "main" }}
|
||||
<main id="top">
|
||||
|
||||
<article class="navy bp-article">
|
||||
<div class="shell">
|
||||
|
||||
<p class="bp-back"><a href="{{ "/blog/" | relURL }}">← All posts</a></p>
|
||||
|
||||
<header class="bp-hd">
|
||||
<div class="bars" aria-hidden="true"><i></i><i></i><i></i><i></i></div>
|
||||
{{ with .Params.category }}<p class="kicker">{{ . }}</p>{{ end }}
|
||||
<h1 class="bp-h1 mt1">{{ .Title }}</h1>
|
||||
{{ with .Params.summary }}<p class="lede mt2 bp-sum">{{ . }}</p>{{ end }}
|
||||
<p class="bp-meta">
|
||||
{{ with .Params.author }}<span>{{ . }}</span>{{ end }}
|
||||
<span><time datetime="{{ .Date.Format "2006-01-02" }}">{{ .Date.Format "2 January 2006" }}</time></span>
|
||||
<span>{{ .ReadingTime }} min read</span>
|
||||
</p>
|
||||
</header>
|
||||
|
||||
<div class="bp-body">
|
||||
{{ .Content }}
|
||||
</div>
|
||||
|
||||
{{ with .Params.tags }}
|
||||
<div class="bp-tags">
|
||||
{{ range . }}<span>{{ . }}</span>{{ end }}
|
||||
</div>
|
||||
{{ end }}
|
||||
|
||||
<div class="bp-cta">
|
||||
<div>
|
||||
<p class="kicker">Try it</p>
|
||||
<h2 class="bp-cta__h">Run this on the machine you are reading it on.</h2>
|
||||
<p>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.</p>
|
||||
</div>
|
||||
<div class="acts">
|
||||
<a class="btn" href="{{ .Site.Params.docsURL }}">Read the documentation →</a>
|
||||
<a class="btn btn--o" href="{{ .Site.Params.github }}">LocalAI on GitHub ↗</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{{ if or .PrevInSection .NextInSection }}
|
||||
<nav class="bp-pn" aria-label="More posts">
|
||||
{{ with .NextInSection }}
|
||||
<a class="bp-pn__i" href="{{ .RelPermalink }}">
|
||||
<span class="bp-pn__k">← Newer</span>
|
||||
<span class="bp-pn__t">{{ .Title }}</span>
|
||||
</a>
|
||||
{{ else }}<span></span>{{ end }}
|
||||
{{ with .PrevInSection }}
|
||||
<a class="bp-pn__i bp-pn__i--r" href="{{ .RelPermalink }}">
|
||||
<span class="bp-pn__k">Older →</span>
|
||||
<span class="bp-pn__t">{{ .Title }}</span>
|
||||
</a>
|
||||
{{ end }}
|
||||
</nav>
|
||||
{{ end }}
|
||||
|
||||
<p class="bp-back bp-back--b"><a href="{{ "/blog/" | relURL }}">← All posts</a></p>
|
||||
|
||||
</div>
|
||||
</article>
|
||||
|
||||
</main>
|
||||
{{ end }}
|
||||
130
website/layouts/engines/list.html
Normal file
@@ -0,0 +1,130 @@
|
||||
{{ define "main" }}
|
||||
{{ $data := .Site.Data.engines }}
|
||||
{{ $cats := $data.categories }}
|
||||
{{ $engines := $data.engines }}
|
||||
<main id="top">
|
||||
|
||||
<!-- HEAD -->
|
||||
<section class="hero navy">
|
||||
<div class="shell">
|
||||
<p class="kicker fd" style="margin-top:0">Engines we build</p>
|
||||
<h1 class="eng-h1"><u><b>Eighteen engines,</b></u><u><b><s>written from scratch.</s></b></u></h1>
|
||||
<div class="bars" aria-hidden="true"><i></i><i></i><i></i><i></i></div>
|
||||
<p class="lede fd mt2">Most backends wrap somebody else's engine. These do not. Each one exists because the thing we needed was a multi-gigabyte Python install, or closed, or nobody had built it yet. What you get instead is a binary and a GGUF file, checked against the reference implementation in CI, running on the machine you already own.</p>
|
||||
<div class="acts fd">
|
||||
<a class="btn" href="/#start">Install LocalAI <span>→</span></a>
|
||||
<a class="btn btn--o" href="/docs/features/backends/">How backends work</a>
|
||||
</div>
|
||||
<div class="figures fd">
|
||||
<div><b class="tnum" data-count="{{ len $engines }}">0</b><span>Engines</span></div>
|
||||
<div><b class="tnum" data-count="{{ len $cats }}">0</b><span>Modalities</span></div>
|
||||
<div><b class="tnum" data-count="0">0</b><span>Python at inference</span></div>
|
||||
<div><b class="tnum" data-count="0" data-text="MIT">0</b><span>Licence</span></div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- CATALOGUE -->
|
||||
<section class="navy" id="catalogue">
|
||||
<div class="shell">
|
||||
|
||||
<div class="engf rv" role="group" aria-label="Filter engines by modality">
|
||||
<button type="button" class="engf__c" data-cat="all" aria-pressed="true">All <b>{{ len $engines }}</b></button>
|
||||
{{ range $cats }}
|
||||
{{ $n := len (where $engines "category" .id) }}
|
||||
{{ if gt $n 0 }}<button type="button" class="engf__c" data-cat="{{ .id }}" aria-pressed="false">{{ .label }} <b>{{ $n }}</b></button>{{ end }}
|
||||
{{ end }}
|
||||
</div>
|
||||
|
||||
{{ range $cats }}
|
||||
{{ $inCat := where $engines "category" .id }}
|
||||
{{ if gt (len $inCat) 0 }}
|
||||
<div class="engg" data-group="{{ .id }}" id="{{ .id }}">
|
||||
<div class="engg__h rv">
|
||||
<p class="kicker">{{ .label }}</p>
|
||||
<p class="engg__b">{{ .blurb }}</p>
|
||||
</div>
|
||||
<div class="engs">
|
||||
{{ range $inCat }}
|
||||
{{ $e := . }}
|
||||
<a class="eng rv{{ if $e.featured }} eng--f{{ end }}" data-cat="{{ $e.category }}" href="{{ $e.repo }}">
|
||||
{{ with $e.media }}
|
||||
<div class="eng__m">
|
||||
<video src="{{ . }}"{{ with $e.poster }} poster="{{ . }}"{{ end }} muted loop playsinline preload="none" data-lazy aria-label="{{ $e.name }}: {{ $e.tagline }}"></video>
|
||||
</div>
|
||||
{{ end }}
|
||||
<div class="eng__b">
|
||||
<p class="eng__k"><span>{{ $e.language }}</span>{{ with $e.status }}<b class="eng__s">{{ . }}</b>{{ end }}</p>
|
||||
<h3 class="eng__n">{{ $e.name }}</h3>
|
||||
<p class="eng__t">{{ $e.tagline }}</p>
|
||||
{{ with $e.highlights }}
|
||||
<ul class="eng__l">{{ range . }}<li>{{ . }}</li>{{ end }}</ul>
|
||||
{{ end }}
|
||||
{{ with $e.clips }}
|
||||
<div class="eng__x">
|
||||
{{ range . }}
|
||||
<figure>
|
||||
<video src="{{ .src }}" muted loop playsinline preload="none" data-lazy aria-label="{{ .caption }}"></video>
|
||||
<figcaption>{{ .caption }}</figcaption>
|
||||
</figure>
|
||||
{{ end }}
|
||||
</div>
|
||||
{{ end }}
|
||||
<span class="eng__go">{{ $e.name }} on GitHub ↗</span>
|
||||
</div>
|
||||
</a>
|
||||
{{ end }}
|
||||
</div>
|
||||
</div>
|
||||
{{ end }}
|
||||
{{ end }}
|
||||
|
||||
<p class="engn rv" role="status" hidden>Nothing in that modality yet.</p>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- CLOSE -->
|
||||
<section class="deepbg" id="why">
|
||||
<div class="shell">
|
||||
<div class="bars rv" aria-hidden="true"><i></i><i></i><i></i><i></i></div>
|
||||
<p class="kicker rv">The rule we hold them to</p>
|
||||
<h2 class="rv mt1" style="max-width:20ch">A port only ships once it matches the original.</h2>
|
||||
<p class="lede rv mt2">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.</p>
|
||||
<div class="acts rv">
|
||||
<a class="btn" href="/#start">Install LocalAI →</a>
|
||||
<a class="btn btn--o" href="https://github.com/mudler/LocalAI">LocalAI on GitHub ↗</a>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
</main>
|
||||
|
||||
<script>
|
||||
(function(){
|
||||
var chips = [].slice.call(document.querySelectorAll('.engf__c'));
|
||||
var cards = [].slice.call(document.querySelectorAll('.eng'));
|
||||
var groups = [].slice.call(document.querySelectorAll('.engg'));
|
||||
var none = document.querySelector('.engn');
|
||||
if (!chips.length) return;
|
||||
function apply(cat){
|
||||
var shown = 0;
|
||||
cards.forEach(function(c){
|
||||
var on = (cat === 'all' || c.dataset.cat === cat);
|
||||
c.hidden = !on;
|
||||
if (on) shown++;
|
||||
});
|
||||
groups.forEach(function(g){
|
||||
g.hidden = !(cat === 'all' || g.dataset.group === cat);
|
||||
});
|
||||
chips.forEach(function(c){ c.setAttribute('aria-pressed', String(c.dataset.cat === cat)); });
|
||||
if (none) none.hidden = shown > 0;
|
||||
try { history.replaceState(null, '', cat === 'all' ? location.pathname : location.pathname + '#' + cat); } catch (e) {}
|
||||
}
|
||||
chips.forEach(function(c){
|
||||
c.addEventListener('click', function(){ apply(c.dataset.cat); });
|
||||
});
|
||||
var start = (location.hash || '').replace('#','');
|
||||
if (start && chips.some(function(c){ return c.dataset.cat === start; })) apply(start);
|
||||
})();
|
||||
</script>
|
||||
{{ end }}
|
||||
541
website/layouts/index.html
Normal file
@@ -0,0 +1,541 @@
|
||||
{{ define "main" }}
|
||||
<main id="top">
|
||||
|
||||
<!-- HERO -->
|
||||
<section class="hero navy">
|
||||
<div class="shell">
|
||||
<div class="hero__grid">
|
||||
<div>
|
||||
<p class="kicker fd" style="margin-top:0">Open source · MIT · v4.8.0</p>
|
||||
<h1><u><b>Make AI run on</b></u><u><b><s>every machine.</s></b></u></h1>
|
||||
<div class="bars" aria-hidden="true"><i></i><i></i><i></i><i></i></div>
|
||||
<p class="lede fd mt2">Text, voice, vision, images, video, 3D and agents, from one open runtime. It works on the laptop you already own, and scales to a room full of GPUs when you have one. When the engine we need is too heavy, too closed, or does not exist, we write it.</p>
|
||||
<div class="acts fd">
|
||||
<a class="btn" href="#start">Install LocalAI <span>→</span></a>
|
||||
<a class="btn btn--o" href="/docs/">Read the docs</a>
|
||||
</div>
|
||||
<div class="figures fd">
|
||||
<div><b class="tnum" data-count="48042">0</b><span>GitHub stars</span></div>
|
||||
<div><b class="tnum" data-count="73">0</b><span>Backends</span></div>
|
||||
<div><b class="tnum" data-count="18">0</b><span>Engines we wrote</span></div>
|
||||
<div><b class="tnum" data-count="1585">0</b><span>Models, one click</span></div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="fd">
|
||||
<figure class="screen" style="margin:0">
|
||||
<figcaption class="screen__bar"><i></i> localai · chat <b>CPU only, no GPU</b></figcaption>
|
||||
<video src="/media/hero-ui.mp4" poster="/img/hero-poster.jpg" autoplay muted loop playsinline preload="metadata" aria-label="The LocalAI interface running a chat completion on CPU"></video>
|
||||
</figure>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- LOCALAI -->
|
||||
<section class="navy" id="localai">
|
||||
<div class="shell">
|
||||
<div class="bars rv" aria-hidden="true"><i></i><i></i><i></i><i></i></div>
|
||||
<p class="kicker rv">The runtime</p>
|
||||
<h2 class="rv mt1" style="max-width:21ch">LocalAI is the engine everything else plugs into.</h2>
|
||||
<p class="lede rv mt2">One binary with an OpenAI-compatible API in front of it. Point an existing client at it and the calls keep working, except now the model is on your machine. It also speaks the Anthropic, Ollama and ElevenLabs APIs, so most tools need a URL change and nothing else.</p>
|
||||
<p class="lede rv mt2">Underneath, a small core pulls each engine in as a separate backend, only when a model asks for it. That is why one install covers this much ground without becoming a 9 GB download.</p>
|
||||
<div class="apis rv">
|
||||
<span>OpenAI API</span><span>Anthropic API</span><span>Ollama API</span><span>ElevenLabs API</span><span>Realtime over WebRTC</span>
|
||||
</div>
|
||||
<div class="duo mt3">
|
||||
<div>
|
||||
<div class="lanes rv">
|
||||
<a class="lane" href="/docs/features/text-generation/"><span class="lane__k">Reason</span><span class="lane__d">Language models, tool calling, structured output</span><span class="lane__t">llama.cpp · vLLM · MLX</span></a>
|
||||
<a class="lane" href="/docs/features/openai-realtime/"><span class="lane__k">Listen</span><span class="lane__d">Realtime voice, transcription, diarization</span><span class="lane__t">parakeet · whisper</span></a>
|
||||
<a class="lane" href="/docs/features/text-to-audio/"><span class="lane__k">Speak</span><span class="lane__d">Speech synthesis and voice cloning</span><span class="lane__t">moss-tts · piper</span></a>
|
||||
<a class="lane" href="/docs/features/object-detection/"><span class="lane__k">See</span><span class="lane__d">Vision, detection, recognition, depth, 3D</span><span class="lane__t">rf-detr · depth-anything</span></a>
|
||||
<a class="lane" href="/docs/features/image-generation/"><span class="lane__k">Create</span><span class="lane__d">Images, video, music and sound</span><span class="lane__t">diffusers · ace-step</span></a>
|
||||
<a class="lane" href="/docs/features/agents/"><span class="lane__k">Act</span><span class="lane__d">Agents, MCP, skills, RAG, interactive tools</span><span class="lane__t">agents · MCP apps</span></a>
|
||||
</div>
|
||||
<p class="rv mt2"><a class="btn btn--o" href="/docs/">Read the documentation →</a></p>
|
||||
</div>
|
||||
<div class="duo__m rv">
|
||||
<figure class="screen" style="margin:0">
|
||||
<figcaption class="screen__bar"><i></i> localai · model gallery <b>1,585 models</b></figcaption>
|
||||
<video src="/media/gallery.mp4" muted loop playsinline preload="none" data-lazy aria-label="Installing a model from the LocalAI gallery"></video>
|
||||
</figure>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- MISSION -->
|
||||
<section class="navy" id="mission">
|
||||
<div class="shell">
|
||||
<div class="bars rv" aria-hidden="true"><i></i><i></i><i></i><i></i></div>
|
||||
<p class="kicker rv">Why the project exists</p>
|
||||
<h2 class="rv mt1" style="max-width:19ch">Local AI should not need a datacenter.</h2>
|
||||
<div class="mission">
|
||||
<div class="mi rv">
|
||||
<p class="mi__n">01 / HARDWARE</p>
|
||||
<h3>Every feature ships a CPU path first.</h3>
|
||||
<p>Not a degraded mode that technically runs. The real one, tested in CI, on the hardware most people already have. GPUs make it faster, they are not the price of entry.</p>
|
||||
<p class="mi__meta">x86_64 · ARM64 · CUDA · ROCm · SYCL · Metal · Vulkan</p>
|
||||
</div>
|
||||
<div class="mi rv">
|
||||
<p class="mi__n">02 / REALTIME</p>
|
||||
<h3>You can talk to it, and it answers.</h3>
|
||||
<p>Speech in, tool calls in the middle, speech out over WebRTC, fast enough to feel like a conversation. Transcription, diarization and speech synthesis all run without a GPU.</p>
|
||||
<p class="mi__meta">Realtime API · WebRTC · streaming ASR · TTS · VAD</p>
|
||||
</div>
|
||||
<div class="mi rv">
|
||||
<p class="mi__n">03 / DISTRIBUTED</p>
|
||||
<h3>Plug in a second machine and stop there.</h3>
|
||||
<p>Routing, VRAM-aware placement, prefix-cache affinity and failover are the runtime's problem. You add hardware, the cluster works out what to do with it.</p>
|
||||
<p class="mi__meta">Smart routing · autoscaling · P2P · NATS · federation</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- SENSES -->
|
||||
<section class="navy" id="senses">
|
||||
<div class="shell">
|
||||
<div class="bars rv" aria-hidden="true"><i></i><i></i><i></i><i></i></div>
|
||||
<p class="kicker rv">Senses</p>
|
||||
<h2 class="rv mt1" style="max-width:20ch">Give the model eyes and ears.</h2>
|
||||
<p class="lede rv mt2">A chat model can only work with what somebody types at it. Our engines change what it has access to: what is happening in the room, who walked into it, where things are in space, and how to answer out loud. All of it runs locally, most of it on a CPU.</p>
|
||||
<div class="senses">
|
||||
<div class="sn rv"><p class="sn__v">Hear <u>words</u></p>
|
||||
<p>Live transcription with speaker labels and timestamps, fast enough to keep up with a meeting while it is still happening.</p>
|
||||
<div class="sn__e"><span>parakeet.cpp</span><span>moss-transcribe.cpp</span></div></div>
|
||||
<div class="sn rv"><p class="sn__v">Hear <u>the room</u></p>
|
||||
<p>527 kinds of sound event: a door, a dog, breaking glass, a smoke alarm. The model notices things nobody thought to type.</p>
|
||||
<div class="sn__e"><span>ced.cpp</span></div></div>
|
||||
<div class="sn rv"><p class="sn__v">Know <u>who</u></p>
|
||||
<p>Recognise a voice, recognise a face, and tell a live person from a photo held up to the camera.</p>
|
||||
<div class="sn__e"><span>voice-detect.cpp</span><span>face-detect.cpp</span></div></div>
|
||||
<div class="sn rv"><p class="sn__v">See <u>things</u></p>
|
||||
<p>Ask for "the red mug on the left" in plain language and get back coordinates, not a caption.</p>
|
||||
<div class="sn__e"><span>locate-anything.cpp</span><span>rf-detr.cpp</span></div></div>
|
||||
<div class="sn rv"><p class="sn__v">See <u>space</u></p>
|
||||
<p>Distance in metres from one ordinary photo, and a full 3D reconstruction from a handful of them. No rig, no camera poses, no GPU.</p>
|
||||
<div class="sn__e"><span>depth-anything.cpp</span><span>free-splatter.cpp</span><span>trellis2.cpp</span></div></div>
|
||||
<div class="sn rv"><p class="sn__v">Speak</p>
|
||||
<p>Long-form speech in a cloned voice, across dozens of languages, up to 48 kHz.</p>
|
||||
<div class="sn__e"><span>moss-tts.cpp</span><span>magpie-tts.cpp</span><span>vibevoice.cpp</span><span>voxtral-tts.c</span></div></div>
|
||||
<div class="sn rv"><p class="sn__v">Hear <u>clearly</u></p>
|
||||
<p>Echo cancellation, noise suppression and dereverberation, so a voice loop survives a real room with a real speaker in it.</p>
|
||||
<div class="sn__e"><span>LocalVQE</span></div></div>
|
||||
<div class="sn rv"><p class="sn__v">Forget <u>on purpose</u></p>
|
||||
<p>Names, addresses and card numbers get caught and redacted on the machine, before anything is sent anywhere.</p>
|
||||
<div class="sn__e"><span>privacy-filter.cpp</span></div></div>
|
||||
</div>
|
||||
<p class="lede rv mt3" style="max-width:74ch;color:var(--ink)">One session can do all of it at once: hear the room, work out who is talking, read what is on the desk, call a tool, and answer out loud. One API, one machine, nothing leaves the building.</p>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- MADE WITH -->
|
||||
<section class="navy" id="made">
|
||||
<div class="shell">
|
||||
<div class="bars rv" aria-hidden="true"><i></i><i></i><i></i><i></i></div>
|
||||
<p class="kicker rv">Made with LocalAI</p>
|
||||
<h2 class="rv mt1" style="max-width:23ch">Nobody was at the keyboard.</h2>
|
||||
<div class="made">
|
||||
<div class="rv">
|
||||
<div class="player" id="player">
|
||||
<span class="player__tag">Sound on</span>
|
||||
<video id="pv" src="/media/presenter.mp4" poster="/img/presenter-poster.jpg" playsinline preload="none" aria-label="A generated presenter explaining voice cloning, produced entirely with LocalAI"></video>
|
||||
<div class="player__ui"><span class="player__btn"><i></i> Play with sound</span></div>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<p class="lede rv">Every part of this clip came out of LocalAI, and nib drove the machine that made it. The agent opened the app, ran the demo and captured the screen, while a local model wrote the script, a cloned voice read it, and the video endpoint generated and lip-synced the presenter. No human touched the keyboard, and nothing left the building.</p>
|
||||
<div class="recipe rv">
|
||||
<div><b>Direction</b><span>nib, our agent harness, drove the machine end to end</span></div>
|
||||
<div><b>Script</b><span>Written by a local language model</span></div>
|
||||
<div><b>Voice</b><span>Cloned from a few seconds of reference audio</span></div>
|
||||
<div><b>Presenter</b><span>Generated and lip-synced through the video endpoint</span></div>
|
||||
</div>
|
||||
<p class="rv mt2"><a class="btn btn--o" href="/docs/features/text-to-audio/">How the voice pipeline works →</a></p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- ENGINES -->
|
||||
<section class="navy" id="engines">
|
||||
<div class="shell">
|
||||
<div class="bars rv" aria-hidden="true"><i></i><i></i><i></i><i></i></div>
|
||||
<p class="kicker rv">Engines we build</p>
|
||||
<h2 class="rv mt1" style="max-width:22ch">Eighteen engines, written from scratch.</h2>
|
||||
<p class="lede rv mt2">Most backends wrap somebody else's engine. These do not. They exist because the thing we needed was a 9 GB Python install, or closed, or nobody had built it yet. Each one is a binary and a GGUF file, checked against the reference implementation in CI.</p>
|
||||
|
||||
<div class="spot rv">
|
||||
<div>
|
||||
<h3>parakeet.cpp</h3>
|
||||
<p class="spot__h">Twenty-seven times faster than whisper.cpp, on a CPU.</p>
|
||||
<p>NVIDIA NeMo Parakeet, ported to C++ and ggml. Ten checkpoints, all of them verified at WER 0 against NeMo, which means the transcript comes out byte for byte identical while finishing first. Cache-aware streaming with end-of-utterance detection handles live audio, and the multilingual streaming model covers 40 or more locales.</p>
|
||||
<div class="facts">
|
||||
<div><b>27x</b><span>vs whisper.cpp, CPU</span></div>
|
||||
<div><b>1.40x</b><span>vs NeMo, CPU median</span></div>
|
||||
<div><b>WER 0</b><span>Parity with NeMo</span></div>
|
||||
<div><b>37%</b><span>Size at q8_0</span></div>
|
||||
</div>
|
||||
<p class="mt2"><a class="btn btn--o" href="https://github.com/mudler/parakeet.cpp">parakeet.cpp on GitHub ↗</a></p>
|
||||
</div>
|
||||
<div class="spot__m">
|
||||
<figure><video src="/media/parakeet-duel.mp4" muted loop playsinline preload="none" data-lazy aria-label="parakeet.cpp finishing ahead of NeMo on the same audio"></video>
|
||||
<figcaption>Same audio, same words, ours finishes first</figcaption></figure>
|
||||
<figure><img src="/img/parakeet-speedup.jpg" alt="Bar chart of parakeet.cpp CPU speedup over NeMo per dtype" loading="lazy">
|
||||
<figcaption>CPU speedup over NeMo, by dtype</figcaption></figure>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="spot spot--r rv">
|
||||
<div>
|
||||
<h3>depth-anything.cpp</h3>
|
||||
<p class="spot__h">Beats PyTorch on CPU, in half the memory.</p>
|
||||
<p>Depth Anything 3 as a 99 MB file. It gives you metric depth, per-pixel confidence, camera intrinsics and extrinsics, and a back-projected point cloud you can export to glb or COLMAP. Output correlates 1.0 with the reference implementation, component by component, and there is no Python or CUDA toolkit anywhere at inference.</p>
|
||||
<div class="facts">
|
||||
<div><b>1.31x</b><span>vs PyTorch, CPU</span></div>
|
||||
<div><b>363 MB</b><span>Peak RAM, q8_0</span></div>
|
||||
<div><b>6.7x</b><span>Faster to load</span></div>
|
||||
<div><b>99 MB</b><span>Smallest build</span></div>
|
||||
</div>
|
||||
<p class="mt2"><a class="btn btn--o" href="https://github.com/mudler/depth-anything.cpp">depth-anything.cpp on GitHub ↗</a></p>
|
||||
</div>
|
||||
<div class="spot__m">
|
||||
<figure><video src="/media/depth-race.mp4" muted loop playsinline preload="none" data-lazy aria-label="depth-anything.cpp finishing ahead of PyTorch on CPU"></video>
|
||||
<figcaption>One photo in, distance in metres out, ahead of PyTorch on the same CPU</figcaption></figure>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="wallhead rv">
|
||||
<h3>And the rest of them, running.</h3>
|
||||
<p>Every clip comes out of the benchmark suite that runs in CI on that engine. Where you see two panes, ours is racing the reference implementation on the same machine, on the same input.</p>
|
||||
</div>
|
||||
<div class="wall">
|
||||
<a class="wi rv" href="https://github.com/mudler/parakeet.cpp"><video src="/media/parakeet-long.mp4" muted loop playsinline preload="none" data-lazy aria-label="parakeet.cpp racing NeMo on long audio"></video>
|
||||
<div class="wi__c"><em>parakeet.cpp</em> long-form transcription <b>vs NeMo</b></div></a>
|
||||
<a class="wi rv" href="https://github.com/localai-org/ced.cpp"><video src="/media/ced.mp4" muted loop playsinline preload="none" data-lazy aria-label="ced.cpp tagging sound events live"></video>
|
||||
<div class="wi__c"><em>ced.cpp</em> sound events, live <b>527 classes</b></div></a>
|
||||
<a class="wi rv" href="https://github.com/mudler/face-detect.cpp"><video src="/media/face.mp4" muted loop playsinline preload="none" data-lazy aria-label="face-detect.cpp detecting, landmarking and recognising faces"></video>
|
||||
<div class="wi__c"><em>face-detect.cpp</em> detect, landmark, recognise <b>no Python</b></div></a>
|
||||
<a class="wi rv" href="https://github.com/mudler/face-detect.cpp"><video src="/media/face-id.mp4" muted loop playsinline preload="none" data-lazy aria-label="face-detect.cpp finding the same person in a lineup"></video>
|
||||
<div class="wi__c"><em>face-detect.cpp</em> same person, different photo <b>1 to N</b></div></a>
|
||||
<a class="wi rv" href="https://github.com/localai-org/voice-detect.cpp"><video src="/media/voice.mp4" muted loop playsinline preload="none" data-lazy aria-label="voice-detect.cpp speaker recognition benchmark"></video>
|
||||
<div class="wi__c"><em>voice-detect.cpp</em> who is speaking <b>vs reference</b></div></a>
|
||||
<a class="wi rv" href="https://github.com/mudler/depth-anything.cpp"><video src="/media/depth.mp4" muted loop playsinline preload="none" data-lazy aria-label="depth-anything.cpp racing PyTorch on CPU"></video>
|
||||
<div class="wi__c"><em>depth-anything.cpp</em> metric depth <b>vs PyTorch, CPU</b></div></a>
|
||||
<a class="wi rv" href="https://github.com/mudler/locate-anything.cpp"><video src="/media/locate.mp4" muted loop playsinline preload="none" data-lazy aria-label="locate-anything.cpp open vocabulary detection race"></video>
|
||||
<div class="wi__c"><em>locate-anything.cpp</em> say it, find it <b>open vocabulary</b></div></a>
|
||||
<a class="wi rv" href="https://github.com/mudler/moss-tts.cpp"><video src="/media/moss.mp4" muted loop playsinline preload="none" data-lazy aria-label="moss-tts.cpp synthesis benchmark"></video>
|
||||
<div class="wi__c"><em>moss-tts.cpp</em> 48 kHz voice cloning <b>vs reference</b></div></a>
|
||||
<a class="wi rv" href="https://github.com/mudler/magpie-tts.cpp"><video src="/media/magpie.mp4" muted loop playsinline preload="none" data-lazy aria-label="magpie-tts.cpp synthesis benchmark"></video>
|
||||
<div class="wi__c"><em>magpie-tts.cpp</em> 9 languages, 5 voices <b>vs reference</b></div></a>
|
||||
</div>
|
||||
|
||||
<div class="reel rv" aria-hidden="true">
|
||||
<div class="reel__t">
|
||||
<span>vllm.cpp</span><span>parakeet.cpp</span><span>moss-transcribe.cpp</span><span>moss-tts.cpp</span><span>magpie-tts.cpp</span><span>ced.cpp</span><span>voice-detect.cpp</span><span>voxtral-tts.c</span><span>vibevoice.cpp</span><span>rf-detr.cpp</span><span>locate-anything.cpp</span><span>depth-anything.cpp</span><span>face-detect.cpp</span><span>free-splatter.cpp</span><span>trellis2.cpp</span><span>privacy-filter.cpp</span><span>LocalVQE</span><span>local-store</span><span>apex-quant</span>
|
||||
<span>vllm.cpp</span><span>parakeet.cpp</span><span>moss-transcribe.cpp</span><span>moss-tts.cpp</span><span>magpie-tts.cpp</span><span>ced.cpp</span><span>voice-detect.cpp</span><span>voxtral-tts.c</span><span>vibevoice.cpp</span><span>rf-detr.cpp</span><span>locate-anything.cpp</span><span>depth-anything.cpp</span><span>face-detect.cpp</span><span>free-splatter.cpp</span><span>trellis2.cpp</span><span>privacy-filter.cpp</span><span>LocalVQE</span><span>local-store</span><span>apex-quant</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="quiet rv">
|
||||
<div>
|
||||
<p class="quiet__tag">In development, not announced yet</p>
|
||||
<h4>vllm.cpp</h4>
|
||||
<p>vLLM ported to C++20, with paged attention, continuous batching and prefix caching, on CPU, CUDA, Metal and Vulkan. It installs as 66 MB instead of a 9.1 GB virtualenv, and it stays ahead of vLLM at every concurrency level we have measured so far. Still being finished, so treat the numbers as provisional.</p>
|
||||
</div>
|
||||
<video src="/media/vllm-race.mp4" muted loop playsinline preload="none" data-lazy aria-label="vllm.cpp ahead of vLLM at every concurrency level"></video>
|
||||
</div>
|
||||
|
||||
<p class="rv mt2"><a class="btn btn--o" href="/engines/">All eighteen engines →</a></p>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- APEX -->
|
||||
<section class="paper" id="apex">
|
||||
<div class="shell">
|
||||
<div class="bars rv" aria-hidden="true"><i></i><i></i><i></i><i></i></div>
|
||||
<p class="kicker rv">APEX quantization</p>
|
||||
<h2 class="rv mt1" style="max-width:19ch">The model you could not fit, on the card you already own.</h2>
|
||||
<p class="lede rv mt2">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.</p>
|
||||
<div class="sizes rv">
|
||||
<div class="sz"><span>F16 · 64.6 GB</span><i style="width:100%"></i><u>30.4 t/s</u></div>
|
||||
<div class="sz"><span>Q8_0 · 34.4 GB</span><i style="width:53%"></i><u>52.5 t/s</u></div>
|
||||
<div class="sz"><span>APEX Quality · 21.3 GB</span><i style="width:33%"></i><u>62.3 t/s</u></div>
|
||||
<div class="sz"><span>APEX Mini · 12.2 GB</span><i style="width:19%"></i><u>74.4 t/s</u></div>
|
||||
</div>
|
||||
<div class="tw rv">
|
||||
<table>
|
||||
<thead><tr><th>Build</th><th>Size</th><th>Perplexity</th><th>HellaSwag</th><th>MMLU</th><th>tg128 t/s</th></tr></thead>
|
||||
<tbody>
|
||||
<tr><td>F16</td><td>64.6 GB</td><td>6.537</td><td>82.5%</td><td>41.5%</td><td>30.4</td></tr>
|
||||
<tr><td>Q8_0</td><td>34.4 GB</td><td>6.533</td><td>83.0%</td><td>41.2%</td><td>52.5</td></tr>
|
||||
<tr><td>Unsloth UD-Q8_K_XL</td><td>45.3 GB</td><td>6.536</td><td>82.5%</td><td>41.3%</td><td>36.4</td></tr>
|
||||
<tr data-a><td>APEX Quality</td><td>21.3 GB</td><td>6.527</td><td>83.0%</td><td>41.2%</td><td>62.3</td></tr>
|
||||
<tr data-a><td>APEX I-Quality</td><td>21.3 GB</td><td>6.552</td><td>83.5%</td><td>41.4%</td><td>63.1</td></tr>
|
||||
<tr data-a><td>APEX Compact</td><td>16.1 GB</td><td>6.783</td><td>82.5%</td><td>40.9%</td><td>69.8</td></tr>
|
||||
<tr data-a><td>APEX Mini</td><td>12.2 GB</td><td>7.088</td><td>81.0%</td><td>41.3%</td><td>74.4</td></tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<p class="mono rv" style="margin-top:.9rem;font-size:.66rem;color:var(--p-dim)">Qwen3.5-35B-A3B on an NVIDIA DGX Spark (GB10). Perplexity on wikitext-2-raw at context 2048. Full methodology in the technical report.</p>
|
||||
<div class="acts rv">
|
||||
<a class="btn" href="https://huggingface.co/collections/mudler/apex-quants">APEX models on Hugging Face ↗</a>
|
||||
<a class="btn btn--o" href="https://github.com/localai-org/apex-quant">Technical report ↗</a>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- CLUSTER -->
|
||||
<section class="navy" id="cluster">
|
||||
<div class="shell">
|
||||
<div class="bars rv" aria-hidden="true"><i></i><i></i><i></i><i></i></div>
|
||||
<p class="kicker rv">Distributed</p>
|
||||
<h2 class="rv mt1" style="max-width:17ch">Two machines behave like one.</h2>
|
||||
<div class="scene">
|
||||
<div>
|
||||
<div class="st rv"><p class="st__k">01</p><h3>Start a worker</h3>
|
||||
<p>One command on any box. It reports what hardware it has and which backends it can run, then joins the pool.</p></div>
|
||||
<div class="st rv"><p class="st__k">02</p><h3>Work goes where it is cheapest</h3>
|
||||
<p>Requests land on the replica that already holds the model and the matching prefix cache, sized against real free VRAM rather than a guess.</p></div>
|
||||
<div class="st rv"><p class="st__k">03</p><h3>Losing a node is boring</h3>
|
||||
<p>In-flight work reschedules, the model loads somewhere else, and the client never finds out.</p></div>
|
||||
</div>
|
||||
<div class="pin rv">
|
||||
<div class="rig">
|
||||
<div class="nodes" id="nodes">
|
||||
<div class="nd"><b>node-01</b>24 GB · CUDA</div>
|
||||
<div class="nd"><b>node-02</b>16 GB · ROCm</div>
|
||||
<div class="nd"><b>node-03</b>CPU · 64 GB</div>
|
||||
</div>
|
||||
<div class="link"></div>
|
||||
<div class="nodes"><div class="nd" style="grid-column:1/4"><b>router</b>prefix affinity · VRAM aware · autoscaling</div></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- MODELS -->
|
||||
<section class="navy" id="models">
|
||||
<div class="shell">
|
||||
<div class="bars rv" aria-hidden="true"><i></i><i></i><i></i><i></i></div>
|
||||
<p class="kicker rv">The gallery</p>
|
||||
<h2 class="rv mt1" style="max-width:20ch">1,585 models. No notebook, no conversion script.</h2>
|
||||
<div class="cards">
|
||||
<a class="cd rv" href="/docs/getting-started/models/"><p class="cd__k">Quantizations</p><h3>201 APEX builds</h3>
|
||||
<p>Every tier of every model we quantize, ranked against the hardware you actually have and installed with one click.</p><span class="cd__go">Browse the gallery →</span></a>
|
||||
<a class="cd rv" href="#"><p class="cd__k">Speech</p><h3>italian-asr</h3>
|
||||
<p>Italian speech recognition trained and published by the team, streaming on a CPU through parakeet.cpp.</p><span class="cd__go">Model card →</span></a>
|
||||
<a class="cd rv" href="/docs/features/text-to-audio/"><p class="cd__k">Voices</p><h3>60 Piper voices</h3>
|
||||
<p>Forty-two languages of text to speech, small enough to run on a Raspberry Pi, installed from the web interface.</p><span class="cd__go">Voice catalogue →</span></a>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- NIB -->
|
||||
<section class="deepbg" id="nib">
|
||||
<div class="shell">
|
||||
<div class="bars rv" aria-hidden="true"><i></i><i></i><i></i><i></i></div>
|
||||
<p class="kicker rv">nib</p>
|
||||
<div class="duo">
|
||||
<div>
|
||||
<h2 class="rv mt1" style="max-width:16ch">An agent you can drop on any box you SSH into.</h2>
|
||||
<p class="lede rv mt2">One Go binary, about 20 MB, no runtime and no daemon. Press <span class="mono" style="color:var(--cyan-hi)">Ctrl+Space</span> 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.</p>
|
||||
<div class="chips rv"><span>Go</span><span>zero dependencies</span><span>MCP</span><span>plugins</span><span>skills</span><span>sub-agents</span></div>
|
||||
<p class="rv mt2"><a class="btn btn--o" href="https://github.com/mudler/nib">nib on GitHub ↗</a></p>
|
||||
</div>
|
||||
<div class="duo__m rv">
|
||||
<figure class="screen" style="margin:0">
|
||||
<figcaption class="screen__bar"><i></i> nib <b>~20 MB, one binary</b></figcaption>
|
||||
<video src="/media/nib.mp4" muted loop playsinline preload="none" data-lazy aria-label="The nib terminal agent running a task"></video>
|
||||
</figure>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- PROOF -->
|
||||
<section class="navy" id="proof">
|
||||
<div class="shell">
|
||||
<div class="bars rv" aria-hidden="true"><i></i><i></i><i></i><i></i></div>
|
||||
<p class="kicker rv">Since March 2023</p>
|
||||
<h2 class="rv mt1" style="max-width:22ch">Forty-eight thousand stars, and still shipping every week.</h2>
|
||||
<div class="trend rv">
|
||||
<img src="/img/trendshift.svg" alt="LocalAI on Trendshift">
|
||||
<p>LocalAI has been <b>trending on GitHub</b> repeatedly since it launched, and it is one of the most starred self-hosted AI projects there is. <b>224 people</b> have contributed code, <b>3,187</b> are in the Discord, and the README is kept translated into <b>eight languages</b> because the users are everywhere.</p>
|
||||
</div>
|
||||
<div class="big rv">
|
||||
<div><b class="tnum" data-count="48042">0</b><span>Stars</span></div>
|
||||
<div><b class="tnum" data-count="4314">0</b><span>Forks</span></div>
|
||||
<div><b class="tnum" data-count="224">0</b><span>Contributors</span></div>
|
||||
<div><b class="tnum" data-count="133">0</b><span>Releases</span></div>
|
||||
<div><b class="tnum" data-count="3187">0</b><span>In Discord</span></div>
|
||||
<div><b class="tnum" data-count="0" data-text="3 yrs">0</b><span>Shipping since</span></div>
|
||||
</div>
|
||||
<div class="tl rv">
|
||||
<div class="tl__t">
|
||||
<div class="tl__i"><p class="tl__d">MAR 2023</p><h4>First commit</h4>
|
||||
<p>An OpenAI-compatible API in front of llama.cpp, so a laptop could answer the same calls as the cloud.</p></div>
|
||||
<div class="tl__i"><p class="tl__d">JUL 2025</p><h4>The core gets small</h4>
|
||||
<p>Every backend moves out of the binary. You install only the engines your models need.</p></div>
|
||||
<div class="tl__i"><p class="tl__d">MAR 2026</p><h4>Agents and a new interface</h4>
|
||||
<p>Native agentic orchestration, a full React rewrite with canvas mode, WebRTC realtime audio.</p></div>
|
||||
<div class="tl__i"><p class="tl__d">APR 2026</p><h4>It becomes a cluster</h4>
|
||||
<p>Distributed mode with VRAM-aware routing, autoscaling, multi-user auth and per-user quotas.</p></div>
|
||||
<div class="tl__i"><p class="tl__d">MAY 2026</p><h4>It sees and hears</h4>
|
||||
<p>Voice recognition, face recognition with liveness, diarization, video generation, drop-in Ollama API.</p></div>
|
||||
<div class="tl__i"><p class="tl__d">JUL 2026</p><h4>Nineteen engines of our own</h4>
|
||||
<p>The native C and C++ ports take over the heavy Python backends, one modality at a time.</p></div>
|
||||
</div>
|
||||
</div>
|
||||
<p class="kicker rv mt3">Where it runs</p>
|
||||
<div class="marks rv">
|
||||
<span>NVIDIA CUDA</span><span>AMD ROCm</span><span>Intel SYCL</span><span>Apple Metal</span><span>Vulkan</span>
|
||||
<span>Jetson</span><span>Raspberry Pi</span><span>x86_64</span><span>ARM64</span><span>Kubernetes</span><span>Docker</span>
|
||||
</div>
|
||||
<div class="langs rv">
|
||||
<span>Deutsch</span><span>Español</span><span>français</span><span>日本語</span>
|
||||
<span>한국어</span><span>Português</span><span>Русский</span><span>中文</span>
|
||||
</div>
|
||||
<p class="kicker rv mt3">What other people say</p>
|
||||
<div class="headline">
|
||||
<a class="hq rv" href="https://x.com/ggerganov/status/2065447087311917459">
|
||||
<blockquote>“Some cool ggml-based work by @mudler_it recently, make sure to check it out.”</blockquote>
|
||||
<p class="hq__w">@ggerganov<b>Georgi Gerganov, author of llama.cpp and ggml</b></p>
|
||||
<span class="hq__go">On X, 2026 ↗</span></a>
|
||||
<a class="hq rv" href="https://x.com/badlogicgames/status/2061201400059531729">
|
||||
<blockquote>“What a wonderful project: parakeet.cpp. A ggml based parakeet inference pipeline that is 2x faster than my ONNX parakeet pipeline on Apple Silicon.”</blockquote>
|
||||
<p class="hq__w">@badlogicgames<b>Mario Zechner, author of pi.agent</b></p>
|
||||
<span class="hq__go">On X, 2026 ↗</span></a>
|
||||
<a class="hq rv" href="https://www.linkedin.com/posts/adimargolink_spinoza-saw-all-things-straining-to-become-activity-7468135820076634113-u3Hc">
|
||||
<blockquote>“Build something good enough that the community chooses to carry it beyond your reach. This week, Ettore Di Giacinto brought NVIDIA Parakeet to the CPU.”</blockquote>
|
||||
<p class="hq__w">Adi Margolin<b>On LinkedIn</b></p>
|
||||
<span class="hq__go">Read the post ↗</span></a>
|
||||
</div>
|
||||
|
||||
<p class="kicker rv mt3">Built on, integrated with, written about</p>
|
||||
<div class="posts">
|
||||
<a class="post rv" href="https://x.com/sozercan/status/1769769695081546236">
|
||||
<p class="post__h">@sozercan <em>builds on</em></p>
|
||||
<p>AIKit now offers an extensible solution for finetuning LLMs! Thanks to @UnslothAI, you can finetune fast and efficiently. Then, deploy seamlessly with AIKit using @LocalAI_API for an end-to-end solution!</p>
|
||||
<span class="post__go">On X, 2024 ↗</span></a>
|
||||
<a class="post rv" href="https://x.com/ivanfioravanti/status/2062526685484851440">
|
||||
<p class="post__h">@ivanfioravanti <em>benchmarks</em></p>
|
||||
<p>An M5 Max with 40 GPU cores just beat an M3 Ultra with 80 on parakeet.cpp. Every model. ~1.7x faster on average, up to ~2x. Half the cores.</p>
|
||||
<span class="post__go">On X, 2026 ↗</span></a>
|
||||
<a class="post rv" href="https://x.com/PulumiCorp/status/1794038185061663083">
|
||||
<p class="post__h">@PulumiCorp <em>integration</em></p>
|
||||
<p>Explore how to build and deploy a LLM app using @FlowiseAI and @LocalAI_API with AWS EKS, Pulumi, and TypeScript! Run your models locally or on-prem.</p>
|
||||
<span class="post__go">On X, 2024 ↗</span></a>
|
||||
<a class="post rv" href="https://x.com/enricoros/status/1912401037794898354">
|
||||
<p class="post__h">@enricoros <em>ecosystem</em></p>
|
||||
<p>Congrats to @LocalAI_API for launching LocalAGI (Agents), and LocalRecall (Memory). The Local stack is well designed and expanding.</p>
|
||||
<span class="post__go">On X, 2025 ↗</span></a>
|
||||
<a class="post rv" href="https://x.com/UniverseAdam/status/1779854715519459432">
|
||||
<p class="post__h">@UniverseAdam <em>in print</em></p>
|
||||
<p>My hardcopies of the official @Raspberry_Pi magazine @TheMagPi have arrived! And they have my Automatic Speech Recognition project based around @NordVPN's Meshnet and a self-hosted @LocalAI_API language model inside.</p>
|
||||
<span class="post__go">On X, 2024 ↗</span></a>
|
||||
<a class="post rv" href="https://x.com/ivanfioravanti/status/2038141571678212580">
|
||||
<p class="post__h">@ivanfioravanti <em>community</em></p>
|
||||
<p>LocalAI is becoming stronger and better release, after release! Keep pushing @mudler_it and @LocalAI_API</p>
|
||||
<span class="post__go">On X, 2026 ↗</span></a>
|
||||
<a class="post rv" href="https://x.com/alepiad/status/1654502947697442816">
|
||||
<p class="post__h">@alepiad <em>early days</em></p>
|
||||
<p>I'm really excited about the prospect of open-source LLMs. In that respect, take a look at @LocalAI_API, a drop-in replacement for OpenAI's API but serving GGML models right on your own infrastructure.</p>
|
||||
<span class="post__go">On X, 2023 ↗</span></a>
|
||||
<a class="post rv" href="https://x.com/mattapperson/status/1727390465543041423">
|
||||
<p class="post__h">@mattapperson <em>builds on</em></p>
|
||||
<p>Oh, high there @LocalAI_API, nice to see a terminal based UI for ya! (it's a WIP, but just wanted something cleaner then CURL calls)</p>
|
||||
<span class="post__go">On X, 2023 ↗</span></a>
|
||||
</div>
|
||||
<div class="cards">
|
||||
<a class="cd rv" href="https://modelslab.com/blog/audio-generation/parakeet-cpp-vs-whisper-self-hosted-asr-comparison-2026"><p class="cd__k">Coverage</p>
|
||||
<h3>parakeet.cpp vs Whisper</h3><p>An independent comparison of self-hosted ASR options put parakeet.cpp against Whisper on accuracy and speed.</p>
|
||||
<span class="cite">modelslab.com ↗</span></a>
|
||||
<a class="cd rv" href="https://snailtext.app/blog/whisper-vs-parakeet-tdt/"><p class="cd__k">Coverage</p>
|
||||
<h3>Roughly 4x faster on CPU</h3><p>A third-party benchmark measured Parakeet TDT against Whisper Small through whisper.cpp on the same machine.</p>
|
||||
<span class="cite">snailtext.app ↗</span></a>
|
||||
<a class="cd rv" href="https://github.com/mudler/LocalAI/graphs/contributors"><p class="cd__k">Contributors</p>
|
||||
<h3>224 people have shipped code</h3><p>Backends, gallery entries, docs, translations and bug fixes, from people who are not on the team.</p>
|
||||
<span class="cite">github.com ↗</span></a>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- BLOG -->
|
||||
<section class="deepbg" id="blog">
|
||||
<div class="shell">
|
||||
<div class="bars rv" aria-hidden="true"><i></i><i></i><i></i><i></i></div>
|
||||
<p class="kicker rv">From the team</p>
|
||||
<h2 class="rv mt1" style="max-width:20ch">Every release gets written up and demonstrated.</h2>
|
||||
<p class="lede rv mt2">A changelog tells you what moved. These posts show you what it does, with a recording you can watch and a command you can run.</p>
|
||||
<div class="cards">
|
||||
<a class="cd rv" href="/blog/"><p class="cd__k">Release · v4.8.0</p><h3>What landed in LocalAI 4.8</h3>
|
||||
<p>The audio.cpp backend, Valkey vector search, configurable VAE tiling, and a faster realtime path.</p><span class="cd__go">Read the post →</span></a>
|
||||
<a class="cd rv" href="/blog/"><p class="cd__k">Engineering</p><h3>Porting vLLM to C++</h3>
|
||||
<p>What paged attention looks like without a Python runtime, and what it cost us to get there.</p><span class="cd__go">Read the post →</span></a>
|
||||
<a class="cd rv" href="/blog/"><p class="cd__k">Benchmarks</p><h3>How APEX beats Q8_0 at a third of the size</h3>
|
||||
<p>Per-tensor, per-layer precision for mixture-of-experts models, measured against the alternatives.</p><span class="cd__go">Read the post →</span></a>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- START -->
|
||||
<section class="navy" id="start">
|
||||
<div class="shell">
|
||||
<div class="bars rv" aria-hidden="true"><i></i><i></i><i></i><i></i></div>
|
||||
<p class="kicker rv">Get started</p>
|
||||
<div class="duo">
|
||||
<div>
|
||||
<h2 class="rv mt1" style="max-width:14ch">Running in about a minute.</h2>
|
||||
<p class="lede rv mt2">A container on any platform, a DMG on macOS, a binary on Linux, or a chart on Kubernetes. Backends download themselves the first time a model asks for one, so the base install stays small.</p>
|
||||
<div class="acts rv">
|
||||
<a class="btn" href="/docs/getting-started/">Installation guide →</a>
|
||||
<a class="btn btn--o" href="https://discord.gg/uJAeKSAGDy">Join the Discord</a>
|
||||
</div>
|
||||
</div>
|
||||
<div class="rv">
|
||||
<div class="tabs" role="tablist" aria-label="Installation method">
|
||||
<button class="tab" type="button" role="tab" aria-selected="true" data-pane="p-script">Script</button>
|
||||
<button class="tab" type="button" role="tab" aria-selected="false" data-pane="p-docker">Docker</button>
|
||||
<button class="tab" type="button" role="tab" aria-selected="false" data-pane="p-k8s">Kubernetes</button>
|
||||
</div>
|
||||
<div class="term">
|
||||
<div class="term__b"><span id="term-label">bash</span> <button class="cpy" type="button" id="cpy" data-copy="curl -sSL https://localai.io/install.sh | sh">copy</button></div>
|
||||
<div class="pane" id="p-script">
|
||||
<pre><span class="p">$</span> <span class="c">curl -sSL https://localai.io/install.sh | sh</span>
|
||||
<span class="o">detecting platform</span> <span class="h">linux/amd64</span>
|
||||
<span class="o">fetching latest release binary</span>
|
||||
<span class="o">installed to</span> <span class="h">/usr/local/bin/local-ai</span>
|
||||
|
||||
<span class="p">$</span> <span class="c">local-ai run qwen3.5-35b-a3b-apex</span>
|
||||
<span class="o">pulling backend llama-cpp</span>
|
||||
<span class="o">API ready on</span> <span class="h">http://localhost:8080</span>
|
||||
<span class="o">model ready ·</span> <span class="h">74.4 tok/s</span></pre>
|
||||
</div>
|
||||
<div class="pane" id="p-docker" hidden>
|
||||
<pre><span class="p">$</span> <span class="c">docker run -p 8080:8080 --name local-ai -ti localai/localai:latest</span>
|
||||
<span class="o">starting local-ai</span>
|
||||
<span class="o">detected: CPU (AVX-512), 32 GB RAM</span>
|
||||
<span class="o">API ready on</span> <span class="h">http://localhost:8080</span>
|
||||
|
||||
<span class="o"># Podman works the same way</span>
|
||||
<span class="p">$</span> <span class="c">podman run -p 8080:8080 --name local-ai -ti localai/localai:latest</span></pre>
|
||||
</div>
|
||||
<div class="pane" id="p-k8s" hidden>
|
||||
<pre><span class="p">$</span> <span class="c">kubectl apply -f https://localai.io/install/kubernetes.yaml</span>
|
||||
<span class="o">deployment.apps/local-ai created</span>
|
||||
<span class="o">service/local-ai created</span>
|
||||
|
||||
<span class="o"># or with Helm</span>
|
||||
<span class="p">$</span> <span class="c">helm install local-ai go-skynet/local-ai</span></pre>
|
||||
</div>
|
||||
</div>
|
||||
<div class="alts"><span>macOS: DMG</span><span>Linux: binary</span><span>Podman</span><span>Helm chart</span><span>Build from source</span></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
</main>
|
||||
{{ end }}
|
||||
14
website/layouts/partials/footer.html
Normal file
@@ -0,0 +1,14 @@
|
||||
<footer>
|
||||
<div class="shell">
|
||||
<div class="foot">
|
||||
<div>
|
||||
<img src="/img/logo-full.png" alt="LocalAI">
|
||||
<p style="margin-top:1rem;font-size:.88rem;max-width:34ch;color:var(--dim)">AI that runs on the hardware you already own. MIT licensed, built in the open.</p>
|
||||
</div>
|
||||
<div><h4>Product</h4><ul><li><a href="/docs/">Documentation</a></li><li><a href="/engines/">Engines</a></li><li><a href="/blog/">Blog</a></li><li><a href="/docs/getting-started/">Install</a></li></ul></div>
|
||||
<div><h4>Community</h4><ul><li><a href="https://github.com/mudler/LocalAI">GitHub</a></li><li><a href="https://discord.gg/uJAeKSAGDy">Discord</a></li><li><a href="https://twitter.com/LocalAI_API">X</a></li><li><a href="https://huggingface.co/mudler">Hugging Face</a></li></ul></div>
|
||||
<div><h4>Support us</h4><ul><li><a href="https://github.com/sponsors/mudler">Sponsor</a></li><li><a href="https://github.com/mudler/LocalAI/graphs/contributors">Contributors</a></li><li><a href="/docs/">Contributing</a></li></ul></div>
|
||||
</div>
|
||||
<div class="foot__n"><span>© 2026 LocalAI</span><span>MIT licence</span><span>Design mock, not a live page</span></div>
|
||||
</div>
|
||||
</footer>
|
||||
26
website/layouts/partials/head.html
Normal file
@@ -0,0 +1,26 @@
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<title>{{ if .IsHome }}{{ .Site.Title }} · Make AI run on every machine{{ else }}{{ .Title }} · {{ .Site.Title }}{{ end }}</title>
|
||||
{{ $desc := or .Description .Site.Params.description }}
|
||||
<meta name="description" content="{{ $desc }}">
|
||||
<meta name="author" content="{{ .Site.Params.author }}">
|
||||
{{ if not hugo.IsProduction }}<meta name="robots" content="noindex">{{ end }}
|
||||
<link rel="canonical" href="{{ .Permalink }}">
|
||||
|
||||
<meta property="og:type" content="website">
|
||||
<meta property="og:site_name" content="{{ .Site.Title }}">
|
||||
<meta property="og:title" content="{{ if .IsHome }}Make AI run on every machine{{ else }}{{ .Title }}{{ end }}">
|
||||
<meta property="og:description" content="{{ $desc }}">
|
||||
<meta property="og:url" content="{{ .Permalink }}">
|
||||
<meta property="og:image" content="{{ "img/logo-full.png" | absURL }}">
|
||||
<meta name="twitter:card" content="summary_large_image">
|
||||
<meta name="twitter:site" content="@LocalAI_API">
|
||||
|
||||
<link rel="icon" href="{{ "img/logo-mark.png" | relURL }}">
|
||||
<link rel="preload" as="font" type="font/woff2" href="{{ "fonts/sora-700.woff2" | relURL }}" crossorigin>
|
||||
<link rel="preload" as="font" type="font/woff2" href="{{ "fonts/geist-mono.woff2" | relURL }}" crossorigin>
|
||||
<link rel="stylesheet" href="{{ "css/site.css" | relURL }}">
|
||||
{{ range .Params.extracss }}<link rel="stylesheet" href="{{ printf "css/%s" . | relURL }}">
|
||||
{{ end }}
|
||||
</head>
|
||||
19
website/layouts/partials/nav.html
Normal file
@@ -0,0 +1,19 @@
|
||||
{{- $home := .Site.Home.RelPermalink -}}
|
||||
<header class="top">
|
||||
<a class="brand" href="{{ $home }}"><img src="{{ "img/logo-mark.png" | relURL }}" alt=""> <span>LocalAI</span></a>
|
||||
<nav>
|
||||
{{/* Anchors resolve against the home page so the nav still works from
|
||||
/engines/ and /blog/, not only from the landing page. */}}
|
||||
<a href="{{ $home }}#localai">LocalAI</a>
|
||||
<a href="{{ $home }}#senses">Senses</a>
|
||||
<a href="{{ $home }}#made">Made with it</a>
|
||||
<a href="{{ "engines/" | relURL }}">Engines</a>
|
||||
<a href="{{ $home }}#apex">APEX</a>
|
||||
<a href="{{ "blog/" | relURL }}">Blog</a>
|
||||
<a href="{{ .Site.Params.docsURL }}">Docs</a>
|
||||
</nav>
|
||||
<div class="topright">
|
||||
<a class="pill" href="{{ .Site.Params.github }}/stargazers">★ {{ .Site.Params.stars }}</a>
|
||||
<a class="go-btn" href="{{ $home }}#start">Install</a>
|
||||
</div>
|
||||
</header>
|
||||
170
website/layouts/shortcodes/starchart.html
Normal file
@@ -0,0 +1,170 @@
|
||||
{{- $pts := .Site.Data.stars -}}
|
||||
{{- $marks := .Site.Data.milestones -}}
|
||||
{{- $meta := .Site.Data.starsmeta -}}
|
||||
{{- $last := index $pts (sub (len $pts) 1) -}}
|
||||
<figure class="sc" role="group" aria-labelledby="sc-title" aria-describedby="sc-desc">
|
||||
<figcaption class="sc__head">
|
||||
<p class="sc__k">GitHub stars, {{ index (index $pts 0) 0 }} to {{ index $last 0 }}</p>
|
||||
<h3 id="sc-title" class="sc__title">{{ lang.FormatNumber 0 (index $last 1) }} stars, and the four changes along the way</h3>
|
||||
<p id="sc-desc" class="sc__desc">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.</p>
|
||||
</figcaption>
|
||||
|
||||
<div class="sc__plot">
|
||||
<svg class="sc__svg" viewBox="0 0 900 340" preserveAspectRatio="none" aria-hidden="true" focusable="false">
|
||||
<defs>
|
||||
<linearGradient id="scFill" x1="0" y1="0" x2="0" y2="1">
|
||||
<stop offset="0%" stop-color="#5FCDE4" stop-opacity=".22"/>
|
||||
<stop offset="100%" stop-color="#5FCDE4" stop-opacity="0"/>
|
||||
</linearGradient>
|
||||
</defs>
|
||||
<g class="sc__grid"></g>
|
||||
<g class="sc__marks"></g>
|
||||
<path class="sc__area" fill="url(#scFill)"></path>
|
||||
<path class="sc__line" fill="none" stroke="#5FCDE4" stroke-width="2"
|
||||
stroke-linejoin="round" stroke-linecap="round" vector-effect="non-scaling-stroke"></path>
|
||||
<path class="sc__est" fill="none" stroke="#5FCDE4" stroke-width="2" stroke-dasharray="3 5"
|
||||
stroke-linecap="round" vector-effect="non-scaling-stroke"></path>
|
||||
<g class="sc__cursor" hidden>
|
||||
<line class="sc__cross" y1="0" y2="340"></line>
|
||||
</g>
|
||||
</svg>
|
||||
<div class="sc__dot" hidden></div>
|
||||
<div class="sc__tip" hidden role="status"></div>
|
||||
<div class="sc__ylab"></div>
|
||||
<div class="sc__xlab"></div>
|
||||
</div>
|
||||
|
||||
<details class="sc__data">
|
||||
<summary>Read it as a table</summary>
|
||||
<div class="sc__tablewrap">
|
||||
<table>
|
||||
<caption>Cumulative GitHub stars, sampled every 800 stars</caption>
|
||||
<thead><tr><th scope="col">Date</th><th scope="col">Stars</th></tr></thead>
|
||||
<tbody>
|
||||
{{- range $i, $p := $pts }}{{ if or (eq (mod $i 5) 0) (eq $i (sub (len $pts) 1)) }}
|
||||
<tr><td>{{ index $p 0 }}</td><td>{{ index $p 1 }}</td></tr>
|
||||
{{- end }}{{ end }}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</details>
|
||||
</figure>
|
||||
|
||||
<script>
|
||||
(function(){
|
||||
var PTS = {{ $pts | jsonify | safeJS }};
|
||||
var MARKS = {{ $marks | jsonify | safeJS }};
|
||||
var MEASURED = {{ $meta.measuredStars }};
|
||||
var fig = document.currentScript.previousElementSibling;
|
||||
var svg = fig.querySelector('.sc__svg');
|
||||
var W = 900, H = 340, PADL = 8, PADR = 8, PADT = 16, PADB = 8;
|
||||
|
||||
var xs = PTS.map(function(p){ return Date.parse(p[0]); });
|
||||
var ys = PTS.map(function(p){ return p[1]; });
|
||||
var x0 = xs[0], x1 = xs[xs.length-1], y1 = Math.ceil(ys[ys.length-1]/10000)*10000;
|
||||
function X(t){ return PADL + (t-x0)/(x1-x0) * (W-PADL-PADR); }
|
||||
function Y(v){ return H-PADB - v/y1 * (H-PADT-PADB); }
|
||||
|
||||
/* recessive horizontal grid, one line per 10k */
|
||||
var grid = '';
|
||||
for (var g=10000; g<=y1; g+=10000){
|
||||
grid += '<line x1="0" x2="'+W+'" y1="'+Y(g)+'" y2="'+Y(g)+'"/>';
|
||||
}
|
||||
fig.querySelector('.sc__grid').innerHTML = grid;
|
||||
|
||||
/* The y labels live in the plot box, which is taller than the SVG because the
|
||||
x labels sit under it. Size the label track to the SVG so they line up with
|
||||
the grid rather than drifting past the baseline. */
|
||||
var ylabEl = fig.querySelector('.sc__ylab');
|
||||
var ylab = '';
|
||||
for (var g2=10000; g2<=y1; g2+=10000){
|
||||
ylab += '<span style="top:'+(Y(g2)/H*100)+'%">'+(g2/1000)+'k</span>';
|
||||
}
|
||||
ylabEl.innerHTML = ylab;
|
||||
function fitYLabels(){ ylabEl.style.height = svg.getBoundingClientRect().height + 'px'; }
|
||||
fitYLabels();
|
||||
addEventListener('resize', fitYLabels);
|
||||
|
||||
/* solid where the API gave us points, dashed across the gap it will not serve */
|
||||
var cut = PTS.findIndex(function(p){ return p[1] >= MEASURED; });
|
||||
if (cut < 0) cut = PTS.length - 1;
|
||||
function seg(a, b){
|
||||
return PTS.slice(a, b+1).map(function(p,i){
|
||||
return (i?'L':'M') + X(Date.parse(p[0])).toFixed(1) + ' ' + Y(p[1]).toFixed(1);
|
||||
}).join(' ');
|
||||
}
|
||||
var d = seg(0, cut);
|
||||
fig.querySelector('.sc__line').setAttribute('d', d);
|
||||
fig.querySelector('.sc__est').setAttribute('d', seg(cut, PTS.length-1));
|
||||
fig.querySelector('.sc__area').setAttribute('d',
|
||||
seg(0, PTS.length-1) + ' L' + X(x1).toFixed(1) + ' ' + (H-PADB) + ' L' + X(x0).toFixed(1) + ' ' + (H-PADB) + ' Z');
|
||||
|
||||
function starsAt(t){
|
||||
if (t <= xs[0]) return ys[0];
|
||||
for (var i=1;i<xs.length;i++){
|
||||
if (t <= xs[i]){
|
||||
var f = (t-xs[i-1])/(xs[i]-xs[i-1]);
|
||||
return Math.round(ys[i-1] + f*(ys[i]-ys[i-1]));
|
||||
}
|
||||
}
|
||||
return ys[ys.length-1];
|
||||
}
|
||||
|
||||
/* the four turning points, as annotations rather than a second series */
|
||||
var mk = '';
|
||||
MARKS.forEach(function(m){
|
||||
var t = Date.parse(m.date), mx = X(t), my = Y(starsAt(t));
|
||||
mk += '<line class="sc__mline" x1="'+mx+'" x2="'+mx+'" y1="'+my+'" y2="'+(H-PADB)+'"/>'
|
||||
+ '<circle class="sc__mdot" cx="'+mx+'" cy="'+my+'" r="4"/>';
|
||||
});
|
||||
fig.querySelector('.sc__marks').innerHTML = mk;
|
||||
|
||||
/* Three of the four releases land within two months of each other, so a single
|
||||
row of labels overlaps into mush. Push each one down a row until it clears. */
|
||||
var LBL_W = 12.4; /* label width as a percentage of the plot */
|
||||
var rows = [];
|
||||
var xlab = MARKS.map(function(m){
|
||||
var pct = X(Date.parse(m.date)) / W * 100;
|
||||
var row = 0;
|
||||
while (rows[row] !== undefined && pct - rows[row] < LBL_W) row++;
|
||||
rows[row] = pct;
|
||||
return '<span class="sc__x' + row + '" style="left:' + pct + '%;top:' + (row * 2.55) + 'rem">'
|
||||
+ '<b>' + m.tag + '</b>' + m.label + '</span>';
|
||||
}).join('');
|
||||
fig.querySelector('.sc__xlab').innerHTML = xlab;
|
||||
fig.querySelector('.sc__xlab').style.height = (rows.length * 2.55 + 1.1) + 'rem';
|
||||
fig.querySelector('.sc__plot').style.paddingBottom = (rows.length * 2.55 + 1.1) + 'rem';
|
||||
|
||||
/* hover layer */
|
||||
var plot = fig.querySelector('.sc__plot');
|
||||
var cursor = fig.querySelector('.sc__cursor');
|
||||
var cross = fig.querySelector('.sc__cross');
|
||||
var dot = fig.querySelector('.sc__dot');
|
||||
var tip = fig.querySelector('.sc__tip');
|
||||
var fmt = new Intl.NumberFormat('en-GB');
|
||||
var months = ['January','February','March','April','May','June','July','August','September','October','November','December'];
|
||||
|
||||
function move(ev){
|
||||
var r = plot.getBoundingClientRect();
|
||||
var px = Math.min(Math.max(0, ev.clientX - r.left), r.width);
|
||||
var t = x0 + (px/r.width) * (x1-x0);
|
||||
var v = starsAt(t);
|
||||
var dObj = new Date(t);
|
||||
cursor.hidden = false; dot.hidden = false; tip.hidden = false;
|
||||
cross.setAttribute('x1', px/r.width*W); cross.setAttribute('x2', px/r.width*W);
|
||||
dot.style.left = px + 'px';
|
||||
dot.style.top = (Y(v)/H) * r.height + 'px';
|
||||
var est = v > MEASURED;
|
||||
tip.innerHTML = '<b>' + fmt.format(v) + '</b> stars<span>' + months[dObj.getUTCMonth()] + ' ' + dObj.getUTCFullYear()
|
||||
+ (est ? ' · estimated' : '') + '</span>';
|
||||
var tw = tip.offsetWidth || 150;
|
||||
tip.style.left = Math.min(Math.max(0, px - tw/2), r.width - tw) + 'px';
|
||||
}
|
||||
function leave(){ cursor.hidden = true; dot.hidden = true; tip.hidden = true; }
|
||||
plot.addEventListener('pointermove', move);
|
||||
plot.addEventListener('pointerleave', leave);
|
||||
})();
|
||||
</script>
|
||||
206
website/static/css/blog.css
Normal file
@@ -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}
|
||||
85
website/static/css/engines.css
Normal file
@@ -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}
|
||||
434
website/static/css/site.css
Normal file
@@ -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)}
|
||||
}
|
||||
BIN
website/static/fonts/geist-mono.woff2
Normal file
BIN
website/static/fonts/sora-700.woff2
Normal file
BIN
website/static/img/depth-field.jpg
Normal file
|
After Width: | Height: | Size: 1.7 KiB |
BIN
website/static/img/hero-poster.jpg
Normal file
|
After Width: | Height: | Size: 64 KiB |
BIN
website/static/img/logo-full.png
Normal file
|
After Width: | Height: | Size: 144 KiB |
BIN
website/static/img/logo-mark.png
Normal file
|
After Width: | Height: | Size: 48 KiB |
BIN
website/static/img/parakeet-speedup.jpg
Normal file
|
After Width: | Height: | Size: 51 KiB |
BIN
website/static/img/presenter-poster.jpg
Normal file
|
After Width: | Height: | Size: 39 KiB |
16
website/static/img/trendshift.svg
Normal file
@@ -0,0 +1,16 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" viewBox="0 0 250 53" width="250" height="55" data-date-format="longDate">
|
||||
<rect xmlns="http://www.w3.org/2000/svg" stroke="#4a0e99" stroke-width="1" fill="#FFFFFF" x="0.5" y="0.5" width="249" height="53" rx="10"/>
|
||||
<foreignObject width="198" height="17" style="font-size: 9px;color: rgb(67, 39, 135);font-family: Arial;font-weight: 400;text-align: center;letter-spacing: 0em;line-height: 1.5;" x="6" y="10" selection="true">
|
||||
<div xmlns="http://www.w3.org/1999/xhtml">GITHUB TRENDING</div>
|
||||
</foreignObject>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" version="1.1" id="Слой_1" viewBox="0 0 80 80" width="48" height="45" x="10" y="8">
|
||||
<path fill="#49278e" d="M70.71,40.31C75.74,44.3,80,37.86,80,37.86s-5.64-2.17-8.55,0.61c0.59-1.62,1.02-3.31,1.28-5.01 c4.08,2.16,6.44-2.95,6.44-2.95s-4.41-0.97-6.26,1.4c0.08-0.91,0.12-1.82,0.1-2.73c-0.01-0.36-0.02-0.73-0.05-1.09 c2.96-3.68-1.73-6.99-1.73-6.99s-2.14,5.09,0.98,7.09c0.02,0.33,0.03,0.66,0.03,1c0.01,0.76-0.03,1.52-0.1,2.27 c-0.85-2.69-4.91-3.69-4.91-3.69s-0.13,5.78,4.68,5.48c-0.28,1.69-0.73,3.35-1.34,4.95c-0.19-4.03-5.79-6.33-5.79-6.33 s-1.33,7.55,5.01,8.16c-0.38,0.8-0.8,1.57-1.25,2.32c-0.56,0.95-1.21,1.84-1.89,2.71c0.97-3.99-3.96-7.72-3.96-7.72 s-3.18,6.94,2.73,9.15c-0.38,0.43-0.8,0.81-1.2,1.21c-0.21,0.2-0.43,0.38-0.64,0.58l-0.32,0.29c-0.11,0.09-0.22,0.18-0.33,0.27 l-0.67,0.54l-0.7,0.51c-0.08,0.05-0.16,0.11-0.23,0.16c1.62-3.42-2.07-7.77-2.07-7.77s-4.21,5.55,0.49,8.78 c-1.34,0.79-2.74,1.45-4.2,1.98c1.91-2.59-0.23-6.89-0.23-6.89s-4.66,3.77-1.52,7.46c-1.15,0.33-2.33,0.57-3.51,0.74 c1.46-1.68,0.55-4.83,0.55-4.83s-3.7,2.03-2.18,5c-0.52,0.03-1.05,0.07-1.57,0.06c-0.29,0-0.57,0.01-0.86,0l-0.86-0.04 c-0.85-0.06-1.7-0.15-2.54-0.28l0.68-0.27l0.42-0.17l0.41-0.19l0.82-0.38c0,0,0.01,0,0.01,0c0.39-0.18,0.55-0.65,0.37-1.03 c-0.18-0.39-0.65-0.55-1.03-0.37l-0.04,0.02l-0.77,0.37l-0.39,0.18l-0.39,0.16l-0.79,0.33l-0.8,0.29l-0.4,0.14l-0.41,0.12L40,53.6 l-0.51-0.15l-0.41-0.12l-0.4-0.14l-0.8-0.29l-0.79-0.33l-0.39-0.16l-0.39-0.18l-0.77-0.37l-0.04-0.02c0,0,0,0-0.01,0 c-0.39-0.18-0.85-0.01-1.03,0.38c-0.18,0.39-0.01,0.85,0.38,1.03l0.82,0.38l0.41,0.19l0.42,0.17l0.68,0.27 c-0.84,0.14-1.69,0.22-2.54,0.28l-0.86,0.04c-0.29,0.01-0.57,0-0.86,0c-0.53,0.01-1.05-0.03-1.57-0.06c1.51-2.98-2.18-5-2.18-5 s-0.92,3.15,0.55,4.83c-1.19-0.16-2.36-0.41-3.51-0.74c3.15-3.7-1.52-7.46-1.52-7.46s-2.14,4.31-0.23,6.89 c-1.46-0.53-2.86-1.19-4.2-1.98c4.7-3.22,0.49-8.78,0.49-8.78s-3.69,4.34-2.07,7.77c-0.08-0.05-0.16-0.1-0.23-0.16l-0.7-0.51 l-0.67-0.54c-0.11-0.09-0.23-0.18-0.33-0.27l-0.32-0.29c-0.21-0.19-0.43-0.38-0.64-0.58c-0.4-0.4-0.82-0.79-1.2-1.21 c5.91-2.21,2.73-9.15,2.73-9.15s-4.93,3.73-3.96,7.72c-0.68-0.86-1.33-1.76-1.89-2.71c-0.46-0.75-0.87-1.53-1.25-2.32 c6.33-0.61,5.01-8.16,5.01-8.16s-5.6,2.31-5.79,6.33c-0.61-1.6-1.06-3.26-1.34-4.95c4.81,0.3,4.68-5.48,4.68-5.48 s-4.05,0.99-4.91,3.69c-0.07-0.76-0.1-1.51-0.1-2.27c0-0.33,0.01-0.66,0.03-1c3.11-2.01,0.98-7.09,0.98-7.09s-4.69,3.31-1.73,6.99 C7,28.46,6.99,28.82,6.98,29.18c-0.02,0.91,0.01,1.82,0.1,2.73c-1.84-2.38-6.26-1.4-6.26-1.4s2.37,5.11,6.44,2.95 c0.26,1.71,0.69,3.39,1.28,5.01C5.64,35.69,0,37.86,0,37.86s4.26,6.43,9.29,2.45c0.39,0.87,0.83,1.72,1.31,2.54 c0.47,0.83,1.01,1.63,1.58,2.4C8.71,43.7,4.11,47,4.11,47s5.7,5.1,9.56,0.08c0.04,0.04,0.07,0.08,0.11,0.12 c0.39,0.45,0.82,0.87,1.24,1.3c0.21,0.21,0.44,0.41,0.66,0.61l0.33,0.3c0.11,0.1,0.23,0.19,0.34,0.29l0.69,0.57l0.23,0.17 c-3.34-0.34-6.58,3.29-6.58,3.29s6.19,3.47,8.69-1.83c1.2,0.75,2.47,1.41,3.78,1.96c-2.76,0.6-4.62,4.13-4.62,4.13 s5.89,1.62,6.98-3.26c1.03,0.32,2.07,0.58,3.13,0.78c-1.63,0.99-2.39,3.38-2.39,3.38s4.31,0.39,4.61-3.07 c0.07,0.01,0.14,0.02,0.21,0.02c0.6,0.04,1.2,0.1,1.8,0.09c0.3,0,0.6,0.02,0.9,0.01l0.9-0.03c1.2-0.07,2.41-0.18,3.59-0.42 l0.45-0.08c0.15-0.03,0.29-0.07,0.44-0.1L40,55.13l0.81,0.19c0.15,0.03,0.29,0.07,0.44,0.1l0.45,0.08c1.18,0.23,2.39,0.35,3.59,0.42 l0.9,0.03c0.3,0.01,0.6-0.01,0.9-0.01c0.6,0,1.2-0.06,1.8-0.09c0.07-0.01,0.14-0.02,0.21-0.02c0.31,3.45,4.61,3.07,4.61,3.07 s-0.76-2.39-2.39-3.38c1.06-0.2,2.11-0.46,3.13-0.78c1.09,4.88,6.98,3.26,6.98,3.26s-1.86-3.52-4.62-4.13 c1.31-0.55,2.57-1.21,3.78-1.96c2.5,5.3,8.69,1.83,8.69,1.83s-3.24-3.63-6.58-3.29l0.23-0.17l0.69-0.57 c0.11-0.1,0.23-0.19,0.34-0.29l0.33-0.3c0.22-0.2,0.45-0.4,0.66-0.61c0.42-0.43,0.85-0.84,1.24-1.3c0.03-0.04,0.07-0.08,0.11-0.12 C70.19,52.1,75.89,47,75.89,47s-4.6-3.31-8.08-1.75c0.57-0.77,1.11-1.56,1.58-2.4C69.88,42.03,70.31,41.18,70.71,40.31z"/>
|
||||
</svg>
|
||||
<foreignObject width="230" height="35" style="font-size: 14px;color: rgb(67, 39, 135);font-family: Arial;font-weight: 700;text-align: left;letter-spacing: 0em;line-height: 1.5;" x="64" y="24">
|
||||
<div xmlns="http://www.w3.org/1999/xhtml">#1 Repository Of The Day</div>
|
||||
</foreignObject>
|
||||
<foreignObject width="141" height="36" style="font-size: 18px;color: rgb(74, 14, 153);font-family: Arial;font-weight: 400;text-align: center;letter-spacing: 0em;line-height: 1.5;" x="-36" y="9">
|
||||
<div xmlns="http://www.w3.org/1999/xhtml">1</div>
|
||||
</foreignObject>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 5.1 KiB |
273
website/static/install.sh
Executable file
@@ -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/"
|
||||
161
website/static/install/kubernetes.yaml
Normal file
@@ -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
|
||||
412
website/static/js/site.js
Normal file
@@ -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;i<n;i++) boxes.push(newBox(stagger ? rnd(0,2.2) : rnd(0,6)));
|
||||
}
|
||||
function size(){
|
||||
W=innerWidth; H=innerHeight;
|
||||
small = W < 760;
|
||||
dpr = Math.min(devicePixelRatio||1, small ? 1.5 : 2);
|
||||
cv.width=W*dpr; cv.height=H*dpr; cv.style.width=W+'px'; cv.style.height=H+'px';
|
||||
ctx.setTransform(dpr,0,0,dpr,0,0);
|
||||
seedBoxes(false); seeded=true;
|
||||
}
|
||||
addEventListener('resize', size);
|
||||
addEventListener('orientationchange', size);
|
||||
|
||||
/* clicking anywhere that is not a control pings the field:
|
||||
a ring travels out, and in detection mode something gets found there */
|
||||
document.addEventListener('pointerdown', function(e){
|
||||
if (e.target.closest('a,button,input,textarea,select,video,summary')) return;
|
||||
rings.push({x:e.clientX, y:e.clientY, t:0});
|
||||
if (rings.length>5) 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;i<GW*GH;i++) depth[i]=d[i*4]/255;
|
||||
disp=new Float32Array(GW*GH);
|
||||
size(); requestAnimationFrame(loop);
|
||||
};
|
||||
img.src='/img/depth-field.jpg';
|
||||
|
||||
document.addEventListener('visibilitychange', function(){
|
||||
run=!document.hidden;
|
||||
if (run){ t0=performance.now(); last=0; requestAnimationFrame(loop); }
|
||||
});
|
||||
|
||||
function glow(t){
|
||||
var x1=W*(.28+Math.sin(t*.05)*.14), y1=H*(.32+Math.cos(t*.04)*.13);
|
||||
var g1=ctx.createRadialGradient(x1,y1,0,x1,y1,Math.max(W,H)*.55);
|
||||
g1.addColorStop(0,'rgba(95,205,228,.13)'); g1.addColorStop(1,'rgba(95,205,228,0)');
|
||||
ctx.fillStyle=g1; ctx.fillRect(0,0,W,H);
|
||||
var x2=W*(.74+Math.cos(t*.043)*.15), y2=H*(.66+Math.sin(t*.037)*.14);
|
||||
var g2=ctx.createRadialGradient(x2,y2,0,x2,y2,Math.max(W,H)*.5);
|
||||
g2.addColorStop(0,'rgba(129,129,196,.11)'); g2.addColorStop(1,'rgba(129,129,196,0)');
|
||||
ctx.fillStyle=g2; ctx.fillRect(0,0,W,H);
|
||||
}
|
||||
|
||||
function ripple(dt){
|
||||
if (!disp) return;
|
||||
disp.fill(0);
|
||||
if (!rings.length) return;
|
||||
for (var ri=0; ri<rings.length; ri++){
|
||||
var r=rings[ri];
|
||||
var gx=(r.x/W)*(GW-1), gy=(r.y/H)*(GH-1);
|
||||
var front=r.t*46, decay=Math.max(0, 1-r.t/1.5);
|
||||
if (decay<=0) continue;
|
||||
var amp=0.16*decay;
|
||||
for (var y=0;y<GH;y++){
|
||||
var dy=y-gy;
|
||||
for (var x=0;x<GW;x++){
|
||||
var dx=x-gx, d2=dx*dx+dy*dy;
|
||||
var dist=Math.sqrt(d2);
|
||||
var band=dist-front;
|
||||
if (band<-26 || band>26) 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<LN; li++){
|
||||
var th=0.10+li*(small?0.14:0.093)+drift;
|
||||
if (th<=0.02||th>=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;y<GH-1;y++) for (var x=0;x<GW-1;x++){
|
||||
var i0=y*GW+x, i1=i0+1, i2=i0+GW+1, i3=i0+GW;
|
||||
var A=depth[i0]+disp[i0], B=depth[i1]+disp[i1], C=depth[i2]+disp[i2], D=depth[i3]+disp[i3];
|
||||
var id=(A>th?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;i<boxes.length;i++){
|
||||
var b=boxes[i]; b.t+=dt;
|
||||
if (b.t>b.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 '<span class="ln">' + (l || ' ') + '</span>'; }).join('');
|
||||
pre.insertAdjacentHTML('beforeend', '<span class="caret" aria-hidden="true"></span>');
|
||||
}
|
||||
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);
|
||||
});
|
||||
});
|
||||
});
|
||||
})();
|
||||