From e3d027527282300cd2312bae14e82a4d0ee4be18 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Fri, 18 Sep 2026 07:05:54 +0000 Subject: [PATCH 1/7] docs: document Appwrite's FocalNet backend MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Present FocalNet as Autogravity's own compact crop-ranking model, keep YuNet + U²-Net as the default, and bring the docs site in line with the merged human-ranking path, public weights, and crop response. Co-authored-by: Torsten Dittmann --- CONTRIBUTING.md | 4 + README.md | 56 ++++++++----- docs/src/lib/site.ts | 3 +- docs/src/routes/index.tsx | 166 +++++++++++++++++++++++++++++++++----- docs/src/styles.css | 12 +++ models/README.md | 21 ++--- 6 files changed, 211 insertions(+), 51 deletions(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index bfc40a4..e2cd8dd 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -39,6 +39,10 @@ weights. Candidate generation, letterbox `content`, and the retention-gated ranking path are checked against FocalNet Python goldens. +`make model-focalnet` downloads Appwrite's public FocalNet human-ranking +weights for local `MODEL_BACKEND=focalnet` runs. Those 19 MiB weights are not +required for the default test or integration suites. + To also run the real YuNet and U²-Net models: ```sh diff --git a/README.md b/README.md index 69ab671..bd01f86 100644 --- a/README.md +++ b/README.md @@ -3,10 +3,12 @@ # autogravity `autogravity` is a small Go HTTP service that finds the best crop focus in an -image. It prioritizes a confidently detected face using YuNet, then falls back -to the weighted centroid of the strongest U²-Net salient region. Results are -normalized X/Y coordinates. It never crops, stores, identifies, or modifies the -submitted image. +image. By default it prioritizes a confidently detected face using YuNet, then +falls back to the weighted centroid of the strongest U²-Net salient region. +`MODEL_BACKEND=focalnet` switches to [FocalNet](https://github.com/appwrite/focalnet), +Appwrite's compact ONNX that distills those teacher signals into one importance +map and ranks crop composition. Results are normalized X/Y coordinates. It never +crops, stores, identifies, or modifies the submitted image. ## Requirements @@ -14,6 +16,9 @@ submitted image. - The included INT8 U²-Net model (approximately 42 MiB) and YuNet face detector (approximately 230 KiB). `make model` verifies them and downloads/verifies the FP32 U²-Net fallback (approximately 168 MiB). +- Optional: Appwrite's FocalNet human-ranking model (approximately 19 MiB). + `make model-focalnet` downloads the public `2026-09-14-rc1` weights and + verifies their SHA-256. - An ONNX Runtime shared library. Version 1.23.2 is used by the Docker image and matches the pinned Go binding. @@ -32,6 +37,10 @@ export ONNXRUNTIME_LIB=/absolute/path/to/libonnxruntime.dylib # macOS make model make build ./autogravity + +# Optional: Appwrite's FocalNet backend +make model-focalnet +MODEL_BACKEND=focalnet ./autogravity ``` The server listens on `:8080`. These environment variables are available: @@ -39,7 +48,7 @@ The server listens on `:8080`. These environment variables are available: | Variable | Default | Purpose | | --- | --- | --- | | `ADDR` | `:8080` | HTTP listen address | -| `MODEL_BACKEND` | `u2net` | `u2net` (YuNet + U²-Net) or `focalnet` (published human-ranking model) | +| `MODEL_BACKEND` | `u2net` | `u2net` (YuNet + U²-Net) or `focalnet` (Appwrite's human-ranking model) | | `MODEL_PRECISION` | `int8` | `int8` or `fp32` for the U²-Net backend | | `MODEL_PATH` | unset | Explicit model path; overrides `MODEL_PRECISION` / FocalNet default | | `FACE_MODEL_PATH` | `models/face_detection_yunet_2023mar.onnx` | YuNet face-detection model path (U²-Net backend only) | @@ -98,11 +107,11 @@ the tighter constraint. The image bundles the verified INT8 and face models and downloads the verified FP32 model and CPU-only ONNX Runtime during the build. FocalNet weights are -optional in PR images: `make model-focalnet` verifies the published graph and -stages it at `models/optional/focalnet-human.onnx`. Docker copies that file -only when its SHA-256 matches `2026-09-14-rc1`. The contract dummy is never -accepted. The default `MODEL_BACKEND=u2net` still works if the file is absent; -`MODEL_BACKEND=focalnet` fails at startup. Release images set +optional in PR images: `make model-focalnet` downloads Appwrite's public +`2026-09-14-rc1` graph and stages it at `models/optional/focalnet-human.onnx`. +Docker copies that file only when its SHA-256 matches. The contract dummy is +never accepted. The default `MODEL_BACKEND=u2net` still works if the file is +absent; `MODEL_BACKEND=focalnet` fails at startup. Release images set `REQUIRE_FOCALNET_MODEL=1` so they cannot publish without the real weights. U²-Net precision selection is by environment, not architecture. @@ -111,7 +120,7 @@ docker build -t autogravity . docker run --rm -p 8080:8080 autogravity # Select FP32 without rebuilding: docker run --rm -p 8080:8080 -e MODEL_PRECISION=fp32 autogravity -# Use the published FocalNet human-ranking model: +# Use Appwrite's FocalNet human-ranking model: docker run --rm -p 8080:8080 -e MODEL_BACKEND=focalnet autogravity ``` @@ -204,15 +213,17 @@ are reused safely across requests. ## FocalNet backend -[FocalNet](https://github.com/appwrite/focalnet) publishes a format-v2 human -crop-ranking ONNX graph. Set `MODEL_BACKEND=focalnet` to load -`models/focalnet-human.onnx` instead of YuNet + U²-Net. YuNet is not run; -faces are already fused into the 64×64 importance map. `/analyze` generates -the same candidate crops as FocalNet's Python runtime, scores them with the -trained ranking head, keeps candidates within 0.05 of the best importance -retention, and returns the selected crop's center as `gravity`. - -The evaluated artifact is the FocalNet GitHub release `2026-09-14-rc1` +[FocalNet](https://github.com/appwrite/focalnet) is Appwrite's compact +crop-ranking model: a 19 MiB FP32 RepViT-M0.9 graph that predicts a 64×64 +importance map (distilled from Autogravity's YuNet + U²-Net teacher) and ranks +candidate crops with a human-preference head. Set `MODEL_BACKEND=focalnet` to +load `models/focalnet-human.onnx` instead of YuNet + U²-Net. YuNet is not run; +faces are already fused into the importance map. `/analyze` generates the same +candidate crops as FocalNet's Python runtime, scores them with the trained +ranking head, keeps candidates within 0.05 of the best importance retention, +and returns the selected crop's center as `gravity`. + +The evaluated artifact is the public FocalNet GitHub release `2026-09-14-rc1` (`focalnet-human.onnx`, SHA-256 `59164c601c98cea3f62b25166710831dac63e1a872fc64767c65316ad5385439`). It is not vendored in git. Download it with `make model-focalnet`, then: @@ -277,6 +288,11 @@ previously missed by U²-NetP, but still misses two difficult scenes. See the [fixture evaluation](internal/testimages/testdata/README.md) for measured outputs and unchanged expected regions. +FocalNet's published importance model reports map MAE 0.09975 and 91.09% 1:1 +importance retained against that teacher on a 10k validation split. Those +figures are teacher-agreement, not a guarantee that Autogravity traffic will +match the face-priority backend. See [FocalNet's results](https://github.com/appwrite/focalnet/blob/main/docs/results-500k.md). + ## Contributing See [CONTRIBUTING.md](CONTRIBUTING.md) for development setup, tests, quality diff --git a/docs/src/lib/site.ts b/docs/src/lib/site.ts index 0348bb7..45895ba 100644 --- a/docs/src/lib/site.ts +++ b/docs/src/lib/site.ts @@ -3,7 +3,7 @@ export const SITE = { title: 'autogravity docs', url: 'https://6a9e5e660013e2481b3d.appwrite.network', description: - "Image focal-point detection as a microservice. Post an image, get back one coordinate pair: the weighted centre of the strongest salient region.", + "Image focal-point detection as a microservice. Post an image, get back a crop focus from YuNet + U²-Net, or from Appwrite's own FocalNet model.", tagline: 'Find the subject. Crop nothing.', github: 'https://github.com/appwrite/autogravity', appwrite: 'https://appwrite.io', @@ -15,6 +15,7 @@ export const NAV = { start: [ { href: '#overview', label: 'Overview' }, { href: '#face-priority', label: 'Face priority' }, + { href: '#focalnet', label: 'FocalNet' }, { href: '#preview', label: 'Storage preview' }, { href: '#install', label: 'Install' }, { href: '#config', label: 'Configuration' }, diff --git a/docs/src/routes/index.tsx b/docs/src/routes/index.tsx index c51da9b..e00a50a 100644 --- a/docs/src/routes/index.tsx +++ b/docs/src/routes/index.tsx @@ -20,13 +20,16 @@ function DocsPage() {

Focal points, as a service.

A small Go HTTP service that finds the best crop focus in an image. - It prioritizes confidently detected faces with YuNet, then falls - back to U²-Net saliency. It never crops, stores, identifies, or - modifies the submitted image. + The default backend prioritizes confidently detected faces with + YuNet, then falls back to U²-Net saliency.{' '} + MODEL_BACKEND=focalnet switches to + Appwrite's own compact FocalNet model. It never crops, stores, + identifies, or modifies the submitted image.

Face-first · saliency fallback - JPEG · PNG · WebP + FocalNet backend + JPEG · PNG · WebP · GIF CPU-only
@@ -35,7 +38,7 @@ function DocsPage() {

@@ -45,6 +48,68 @@ function DocsPage() {

+
+ +

+ The default production backend stays YuNet + U²-Net. Set{' '} + MODEL_BACKEND=focalnet after{' '} + make model-focalnet to load the public{' '} + 2026-09-14-rc1 weights from{' '} + + appwrite/focalnet + + . Pass ?aspect_ratio=16:9 to rank a + non-square crop; the default is 1:1. +

+ + + $ make model-focalnet + {'\n'} + $ MODEL_BACKEND=focalnet + ./autogravity + {'\n'} + $ docker run --rm -p 8080:8080 -e + {' '} + MODEL_BACKEND=focalnet ghcr.io/appwrite/autogravity + + +
+
+ Backend + Models + Result +
+
+ u2net + YuNet + U²-Net + + Face center, or saliency centroid. Default. + +
+
+ focalnet + focalnet-human.onnx + + Ranked crop center plus crop rectangle. Optional. + +
+
+

+ FocalNet reports map MAE 0.09975 and 91.09% 1:1 importance retained + against the teacher on a 10k validation split. Those figures are + teacher-agreement, not a guarantee that Autogravity traffic will + match the face-priority backend. +

+
+
@@ -83,8 +148,10 @@ function DocsPage() {

Or build from source with Go 1.25 or newer.{' '} - make model downloads and verifies the ONNX - models. + make model downloads and verifies the + default ONNX models.{' '} + make model-focalnet downloads Appwrite's + FocalNet weights.

@@ -93,6 +160,11 @@ function DocsPage() { $ make build {'\n'} $ ./autogravity + {'\n'} + $ make model-focalnet + {'\n'} + $ MODEL_BACKEND=focalnet + ./autogravity
@@ -114,7 +186,14 @@ function DocsPage() { MODEL_BACKEND u2net - u2net (YuNet + U²-Net) or focalnet + u2net (YuNet + U²-Net) or focalnet (Appwrite's model) + + +
+ MODEL_PRECISION + int8 + + int8 or fp32 for the U²-Net backend
@@ -127,12 +206,16 @@ function DocsPage() {
FACE_MODEL_PATH models/face_detection_yunet_2023mar.onnx - YuNet model path + + YuNet model path (U²-Net backend only) +
FACE_SCORE_THRESHOLD 0.85 - Minimum reliable face score + + Minimum reliable face score (U²-Net backend only) +
ONNXRUNTIME_LIB @@ -155,13 +238,13 @@ function DocsPage() {
@@ -228,13 +311,53 @@ function DocsPage() { applied before analysis. On the default backend a reliable face supplies its bounding-box center; otherwise U²-Net supplies the saliency centroid. Set MODEL_BACKEND=focalnet{' '} - to use the distilled importance model instead —{' '} - source is then focalnet{' '} - and YuNet is not consulted. Confidence is that strategy's - model score, not an identity match or a calibrated probability. + to use Appwrite's human-ranking model instead.{' '} + source is then{' '} + focalnet, YuNet is not consulted, and the + response includes a crop rectangle. + Confidence is that strategy's model score, not an identity + match or a calibrated probability.

+
+ + + curl -sS -X POST \{'\n'} + {' '}http://localhost:8080/analyze?aspect_ratio=16:9 \{'\n'} + {' '}-F 'image=@photo.jpg' + + + + + {'{'}{'\n'} + {' '}"gravity": {'{'}{'\n'} + {' '}"x":{' '} + 0.52,{'\n'} + {' '}"y":{' '} + 0.41{'\n'} + {' '}{'}'},{'\n'} + {' '}"confidence":{' '} + 0.91,{'\n'} + {' '}"source":{' '} + "focalnet",{'\n'} + {' '}"crop": {'{'}{'\n'} + {' '}"left":{' '} + 120,{'\n'} + {' '}"top":{' '} + 40,{'\n'} + {' '}"width":{' '} + 480,{'\n'} + {' '}"height":{' '} + 480,{'\n'} + {' '}"retained_importance":{' '} + 0.88{'\n'} + {' '}{'}'}{'\n'} + {'}'} + + +
+ @@ -279,8 +403,10 @@ function DocsPage() {

Historical U²-Net fallback timings on Apple M3 Pro, CPU-only ONNX Runtime 1.23.2, Go 1.25.14. Median of five sequential benchmark - samples. Face-selected requests skip U²-Net. Performance varies with - hardware and input images. + samples. Face-selected requests skip U²-Net. FocalNet is a 19 MiB + FP32 graph versus ~42 MiB INT8 U²-Net plus YuNet; measure it on + your deploy target. Performance varies with hardware and input + images.

diff --git a/docs/src/styles.css b/docs/src/styles.css index a245513..e268d3f 100644 --- a/docs/src/styles.css +++ b/docs/src/styles.css @@ -356,6 +356,18 @@ a:hover { color: var(--muted); } +.doc-copy a, +.doc-lead a { + color: var(--pink-soft); + text-decoration: underline; + text-underline-offset: 0.18em; +} + +.doc-copy a:hover, +.doc-lead a:hover { + color: var(--aw-pink); +} + .doc-copy--flush { max-width: none; } diff --git a/models/README.md b/models/README.md index c0b8bf4..abc7c64 100644 --- a/models/README.md +++ b/models/README.md @@ -50,8 +50,9 @@ obscured faces may not reach the threshold. ## FocalNet -FocalNet is an optional `MODEL_BACKEND=focalnet` path. The published -human-ranking graph is `focalnet-human.onnx` from the +FocalNet is Appwrite's own compact crop-ranking model, used when +`MODEL_BACKEND=focalnet`. The human-ranking graph is `focalnet-human.onnx` from +the public [FocalNet `2026-09-14-rc1` release](https://github.com/appwrite/focalnet/releases/tag/2026-09-14-rc1) (20,398,992 bytes). `make model-focalnet` downloads it, verifies this SHA-256, and stages a copy at `models/optional/focalnet-human.onnx` for Docker: @@ -68,12 +69,12 @@ contract-compatible placeholder lives at `internal/focalnet/testdata/dummy.onnx` for Go tests; it is image-independent and must not be copied into `models/focalnet-human.onnx` or a production image. Docker accepts only the published checksum above. The artifact is redistributed under the MIT license -in `FOCALNET_LICENSE`. +in `FOCALNET_LICENSE`. Training code, ONNX contract, and teacher-agreement +figures live in [appwrite/focalnet](https://github.com/appwrite/focalnet). -The FocalNet GitHub release is currently private, so Autogravity's default -`GITHUB_TOKEN` cannot download it. PR image builds omit the weights rather than -substituting the dummy; the default U²-Net backend still starts. -`MODEL_BACKEND=focalnet` fails at startup if the file is missing. Published -release images require the real checksum (`REQUIRE_FOCALNET_MODEL=1`) and a -token that can read `appwrite/focalnet` (set repo secret -`FOCALNET_GITHUB_TOKEN`, or run `make model-focalnet` before `docker build`). +PR image builds still tolerate a missing file so the default U²-Net backend +starts; they never substitute the dummy. `MODEL_BACKEND=focalnet` fails at +startup if the file is missing. Published release images require the real +checksum (`REQUIRE_FOCALNET_MODEL=1`). Run `make model-focalnet` before +`docker build`, or set repo secret `FOCALNET_GITHUB_TOKEN` if a workflow cannot +read the public release. From cbe67542230108d3cc224834e313c8da5b4faa44 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Fri, 18 Sep 2026 07:18:05 +0000 Subject: [PATCH 2/7] docs: wrap FocalNet commands so they fit the page Break the Docker run example and give the ranked-crop request a full-width panel so the commands are readable without horizontal scrolling. Co-authored-by: Torsten Dittmann --- docs/src/routes/index.tsx | 80 +++++++++++++++++++-------------------- 1 file changed, 40 insertions(+), 40 deletions(-) diff --git a/docs/src/routes/index.tsx b/docs/src/routes/index.tsx index e00a50a..585a3e4 100644 --- a/docs/src/routes/index.tsx +++ b/docs/src/routes/index.tsx @@ -76,9 +76,11 @@ function DocsPage() { $ MODEL_BACKEND=focalnet ./autogravity {'\n'} - $ docker run --rm -p 8080:8080 -e - {' '} - MODEL_BACKEND=focalnet ghcr.io/appwrite/autogravity + $ docker run --rm -p 8080:8080 \ + {'\n'} + {' '}-e MODEL_BACKEND=focalnet \ + {'\n'} + {' '}ghcr.io/appwrite/autogravity
@@ -320,43 +322,41 @@ function DocsPage() {

-
- - - curl -sS -X POST \{'\n'} - {' '}http://localhost:8080/analyze?aspect_ratio=16:9 \{'\n'} - {' '}-F 'image=@photo.jpg' - - - - - {'{'}{'\n'} - {' '}"gravity": {'{'}{'\n'} - {' '}"x":{' '} - 0.52,{'\n'} - {' '}"y":{' '} - 0.41{'\n'} - {' '}{'}'},{'\n'} - {' '}"confidence":{' '} - 0.91,{'\n'} - {' '}"source":{' '} - "focalnet",{'\n'} - {' '}"crop": {'{'}{'\n'} - {' '}"left":{' '} - 120,{'\n'} - {' '}"top":{' '} - 40,{'\n'} - {' '}"width":{' '} - 480,{'\n'} - {' '}"height":{' '} - 480,{'\n'} - {' '}"retained_importance":{' '} - 0.88{'\n'} - {' '}{'}'}{'\n'} - {'}'} - - -
+ + + curl -sS -X POST \{'\n'} + {' '}http://localhost:8080/analyze?aspect_ratio=16:9 \{'\n'} + {' '}-F 'image=@photo.jpg' + + + + + {'{'}{'\n'} + {' '}"gravity": {'{'}{'\n'} + {' '}"x":{' '} + 0.52,{'\n'} + {' '}"y":{' '} + 0.41{'\n'} + {' '}{'}'},{'\n'} + {' '}"confidence":{' '} + 0.91,{'\n'} + {' '}"source":{' '} + "focalnet",{'\n'} + {' '}"crop": {'{'}{'\n'} + {' '}"left":{' '} + 120,{'\n'} + {' '}"top":{' '} + 40,{'\n'} + {' '}"width":{' '} + 480,{'\n'} + {' '}"height":{' '} + 480,{'\n'} + {' '}"retained_importance":{' '} + 0.88{'\n'} + {' '}{'}'}{'\n'} + {'}'} + + Date: Fri, 18 Sep 2026 07:26:49 +0000 Subject: [PATCH 3/7] docs: wrap long config values in the docs table FACE_MODEL_PATH overflowed into the purpose column. Allow monospace cells to break so the configuration table stays readable. Co-authored-by: Torsten Dittmann --- docs/src/styles.css | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/src/styles.css b/docs/src/styles.css index e268d3f..61ba71a 100644 --- a/docs/src/styles.css +++ b/docs/src/styles.css @@ -896,6 +896,7 @@ a:hover { } .data-table-row .accent { + overflow-wrap: anywhere; color: var(--fg); font-family: var(--font-mono); font-size: 0.8125rem; From c096ff403c018f10b378ed84af5f106e506ba2d9 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Fri, 18 Sep 2026 07:41:26 +0000 Subject: [PATCH 4/7] docs: simplify FocalNet copy Restore the original site description, drop model-internals and metrics from the README and docs site, and keep the new section focused on how to switch backends. Co-authored-by: Torsten Dittmann --- CONTRIBUTING.md | 5 +- README.md | 57 ++++++++--------------- docs/src/lib/site.ts | 2 +- docs/src/routes/index.tsx | 97 +++++++++++---------------------------- models/README.md | 20 ++++---- 5 files changed, 60 insertions(+), 121 deletions(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index e2cd8dd..a090800 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -39,9 +39,8 @@ weights. Candidate generation, letterbox `content`, and the retention-gated ranking path are checked against FocalNet Python goldens. -`make model-focalnet` downloads Appwrite's public FocalNet human-ranking -weights for local `MODEL_BACKEND=focalnet` runs. Those 19 MiB weights are not -required for the default test or integration suites. +`make model-focalnet` downloads FocalNet weights for local runs. They are not +required for the default tests. To also run the real YuNet and U²-Net models: diff --git a/README.md b/README.md index bd01f86..af09128 100644 --- a/README.md +++ b/README.md @@ -3,11 +3,10 @@ # autogravity `autogravity` is a small Go HTTP service that finds the best crop focus in an -image. By default it prioritizes a confidently detected face using YuNet, then -falls back to the weighted centroid of the strongest U²-Net salient region. -`MODEL_BACKEND=focalnet` switches to [FocalNet](https://github.com/appwrite/focalnet), -Appwrite's compact ONNX that distills those teacher signals into one importance -map and ranks crop composition. Results are normalized X/Y coordinates. It never +image. It prioritizes a confidently detected face using YuNet, then falls back +to the weighted centroid of the strongest U²-Net salient region. Set +`MODEL_BACKEND=focalnet` to use [FocalNet](https://github.com/appwrite/focalnet), +Appwrite's own model, instead. Results are normalized X/Y coordinates. It never crops, stores, identifies, or modifies the submitted image. ## Requirements @@ -16,9 +15,8 @@ crops, stores, identifies, or modifies the submitted image. - The included INT8 U²-Net model (approximately 42 MiB) and YuNet face detector (approximately 230 KiB). `make model` verifies them and downloads/verifies the FP32 U²-Net fallback (approximately 168 MiB). -- Optional: Appwrite's FocalNet human-ranking model (approximately 19 MiB). - `make model-focalnet` downloads the public `2026-09-14-rc1` weights and - verifies their SHA-256. +- Optional: Appwrite's FocalNet model (approximately 19 MiB). Download it with + `make model-focalnet`. - An ONNX Runtime shared library. Version 1.23.2 is used by the Docker image and matches the pinned Go binding. @@ -48,7 +46,7 @@ The server listens on `:8080`. These environment variables are available: | Variable | Default | Purpose | | --- | --- | --- | | `ADDR` | `:8080` | HTTP listen address | -| `MODEL_BACKEND` | `u2net` | `u2net` (YuNet + U²-Net) or `focalnet` (Appwrite's human-ranking model) | +| `MODEL_BACKEND` | `u2net` | `u2net` (YuNet + U²-Net) or `focalnet` (Appwrite's model) | | `MODEL_PRECISION` | `int8` | `int8` or `fp32` for the U²-Net backend | | `MODEL_PATH` | unset | Explicit model path; overrides `MODEL_PRECISION` / FocalNet default | | `FACE_MODEL_PATH` | `models/face_detection_yunet_2023mar.onnx` | YuNet face-detection model path (U²-Net backend only) | @@ -107,10 +105,9 @@ the tighter constraint. The image bundles the verified INT8 and face models and downloads the verified FP32 model and CPU-only ONNX Runtime during the build. FocalNet weights are -optional in PR images: `make model-focalnet` downloads Appwrite's public -`2026-09-14-rc1` graph and stages it at `models/optional/focalnet-human.onnx`. -Docker copies that file only when its SHA-256 matches. The contract dummy is -never accepted. The default `MODEL_BACKEND=u2net` still works if the file is +optional in PR images: `make model-focalnet` downloads them and stages a copy +at `models/optional/focalnet-human.onnx`. Docker copies that file only when its +SHA-256 matches. The default `MODEL_BACKEND=u2net` still works if the file is absent; `MODEL_BACKEND=focalnet` fails at startup. Release images set `REQUIRE_FOCALNET_MODEL=1` so they cannot publish without the real weights. U²-Net precision selection is by environment, not architecture. @@ -120,7 +117,7 @@ docker build -t autogravity . docker run --rm -p 8080:8080 autogravity # Select FP32 without rebuilding: docker run --rm -p 8080:8080 -e MODEL_PRECISION=fp32 autogravity -# Use Appwrite's FocalNet human-ranking model: +# Use Appwrite's FocalNet model: docker run --rm -p 8080:8080 -e MODEL_BACKEND=focalnet autogravity ``` @@ -213,20 +210,15 @@ are reused safely across requests. ## FocalNet backend -[FocalNet](https://github.com/appwrite/focalnet) is Appwrite's compact -crop-ranking model: a 19 MiB FP32 RepViT-M0.9 graph that predicts a 64×64 -importance map (distilled from Autogravity's YuNet + U²-Net teacher) and ranks -candidate crops with a human-preference head. Set `MODEL_BACKEND=focalnet` to -load `models/focalnet-human.onnx` instead of YuNet + U²-Net. YuNet is not run; -faces are already fused into the importance map. `/analyze` generates the same -candidate crops as FocalNet's Python runtime, scores them with the trained -ranking head, keeps candidates within 0.05 of the best importance retention, -and returns the selected crop's center as `gravity`. - -The evaluated artifact is the public FocalNet GitHub release `2026-09-14-rc1` +[FocalNet](https://github.com/appwrite/focalnet) is Appwrite's own model. Set +`MODEL_BACKEND=focalnet` to load `models/focalnet-human.onnx` instead of YuNet + +U²-Net. It picks a crop and returns that crop's center as `gravity`. Faces are +already part of the model, so YuNet is not run. + +The weights come from the public FocalNet GitHub release `2026-09-14-rc1` (`focalnet-human.onnx`, SHA-256 `59164c601c98cea3f62b25166710831dac63e1a872fc64767c65316ad5385439`). -It is not vendored in git. Download it with `make model-focalnet`, then: +They are not stored in git. Download them with `make model-focalnet`, then: ```sh export MODEL_BACKEND=focalnet @@ -251,12 +243,8 @@ non-square crop. The default is `1:1`. Example response: } ``` -The ONNX contract is RGB NCHW `image` `[1,3,256,256]`, padded `boxes` -`[1,128,4]`, letterbox `content` `[1,4]` in, and `importance` `[1,1,64,64]` -plus `crop_scores` `[1,128]` out. Autogravity unpads the letterboxed map, -applies FocalNet's candidate and retention-gate ranking, and uses the -selected crop center as the gravity point. Preprocessing matches FocalNet's -Python runtime (`imaging.py`) via the existing Go letterbox path at 256×256. +See [models/README.md](models/README.md) for the ONNX contract and checksum +details. ## Telemetry and shutdown @@ -288,11 +276,6 @@ previously missed by U²-NetP, but still misses two difficult scenes. See the [fixture evaluation](internal/testimages/testdata/README.md) for measured outputs and unchanged expected regions. -FocalNet's published importance model reports map MAE 0.09975 and 91.09% 1:1 -importance retained against that teacher on a 10k validation split. Those -figures are teacher-agreement, not a guarantee that Autogravity traffic will -match the face-priority backend. See [FocalNet's results](https://github.com/appwrite/focalnet/blob/main/docs/results-500k.md). - ## Contributing See [CONTRIBUTING.md](CONTRIBUTING.md) for development setup, tests, quality diff --git a/docs/src/lib/site.ts b/docs/src/lib/site.ts index 45895ba..7c422ac 100644 --- a/docs/src/lib/site.ts +++ b/docs/src/lib/site.ts @@ -3,7 +3,7 @@ export const SITE = { title: 'autogravity docs', url: 'https://6a9e5e660013e2481b3d.appwrite.network', description: - "Image focal-point detection as a microservice. Post an image, get back a crop focus from YuNet + U²-Net, or from Appwrite's own FocalNet model.", + "Image focal-point detection as a microservice. Post an image, get back one coordinate pair: the weighted centre of the strongest salient region.", tagline: 'Find the subject. Crop nothing.', github: 'https://github.com/appwrite/autogravity', appwrite: 'https://appwrite.io', diff --git a/docs/src/routes/index.tsx b/docs/src/routes/index.tsx index 585a3e4..624418e 100644 --- a/docs/src/routes/index.tsx +++ b/docs/src/routes/index.tsx @@ -20,11 +20,10 @@ function DocsPage() {

Focal points, as a service.

A small Go HTTP service that finds the best crop focus in an image. - The default backend prioritizes confidently detected faces with - YuNet, then falls back to U²-Net saliency.{' '} - MODEL_BACKEND=focalnet switches to - Appwrite's own compact FocalNet model. It never crops, stores, - identifies, or modifies the submitted image. + It prioritizes confidently detected faces with YuNet, then falls + back to U²-Net saliency. You can also switch to Appwrite's + FocalNet model. It never crops, stores, identifies, or modifies the + submitted image.

Face-first · saliency fallback @@ -38,7 +37,7 @@ function DocsPage() {

@@ -52,22 +51,22 @@ function DocsPage() {

- The default production backend stays YuNet + U²-Net. Set{' '} + The default path is still YuNet + U²-Net. Set{' '} MODEL_BACKEND=focalnet after{' '} - make model-focalnet to load the public{' '} - 2026-09-14-rc1 weights from{' '} + make model-focalnet to switch. Pass{' '} + ?aspect_ratio=16:9 for a widescreen crop; + the default is 1:1. See{' '} appwrite/focalnet - - . Pass ?aspect_ratio=16:9 to rank a - non-square crop; the default is 1:1. + {' '} + for the model itself.

@@ -83,33 +82,6 @@ function DocsPage() { {' '}ghcr.io/appwrite/autogravity -
-
- Backend - Models - Result -
-
- u2net - YuNet + U²-Net - - Face center, or saliency centroid. Default. - -
-
- focalnet - focalnet-human.onnx - - Ranked crop center plus crop rectangle. Optional. - -
-
-

- FocalNet reports map MAE 0.09975 and 91.09% 1:1 importance retained - against the teacher on a 10k validation split. Those figures are - teacher-agreement, not a guarantee that Autogravity traffic will - match the face-priority backend. -

@@ -135,7 +107,7 @@ function DocsPage() { @@ -150,10 +122,9 @@ function DocsPage() {

Or build from source with Go 1.25 or newer.{' '} - make model downloads and verifies the - default ONNX models.{' '} - make model-focalnet downloads Appwrite's - FocalNet weights. + make model downloads and verifies the ONNX + models. Use make model-focalnet if you want + Appwrite's FocalNet weights.

@@ -162,11 +133,6 @@ function DocsPage() { $ make build {'\n'} $ ./autogravity - {'\n'} - $ make model-focalnet - {'\n'} - $ MODEL_BACKEND=focalnet - ./autogravity
@@ -188,7 +154,7 @@ function DocsPage() { MODEL_BACKEND u2net - u2net (YuNet + U²-Net) or focalnet (Appwrite's model) + u2net (YuNet + U²-Net) or focalnet
@@ -208,16 +174,12 @@ function DocsPage() {
FACE_MODEL_PATH models/face_detection_yunet_2023mar.onnx - - YuNet model path (U²-Net backend only) - + YuNet model path
FACE_SCORE_THRESHOLD 0.85 - - Minimum reliable face score (U²-Net backend only) - + Minimum reliable face score
ONNXRUNTIME_LIB @@ -246,7 +208,7 @@ function DocsPage() {
@@ -313,12 +275,12 @@ function DocsPage() { applied before analysis. On the default backend a reliable face supplies its bounding-box center; otherwise U²-Net supplies the saliency centroid. Set MODEL_BACKEND=focalnet{' '} - to use Appwrite's human-ranking model instead.{' '} + to use Appwrite's model instead.{' '} source is then{' '} - focalnet, YuNet is not consulted, and the - response includes a crop rectangle. - Confidence is that strategy's model score, not an identity - match or a calibrated probability. + focalnet and the response includes a{' '} + crop rectangle. Confidence is that + strategy's model score, not an identity match or a calibrated + probability.

@@ -376,8 +338,7 @@ function DocsPage() { Separate upload and analysis admission limits bound buffered-body and decoded-image memory without allowing slow uploads to reserve inference capacity. Both models are loaded once at startup and their - inference sessions are reused across requests. FocalNet loads a - single session instead of YuNet + U²-Net. + inference sessions are reused across requests.

@@ -403,10 +364,8 @@ function DocsPage() {

Historical U²-Net fallback timings on Apple M3 Pro, CPU-only ONNX Runtime 1.23.2, Go 1.25.14. Median of five sequential benchmark - samples. Face-selected requests skip U²-Net. FocalNet is a 19 MiB - FP32 graph versus ~42 MiB INT8 U²-Net plus YuNet; measure it on - your deploy target. Performance varies with hardware and input - images. + samples. Face-selected requests skip U²-Net. Performance varies with + hardware and input images.

diff --git a/models/README.md b/models/README.md index abc7c64..4011125 100644 --- a/models/README.md +++ b/models/README.md @@ -50,9 +50,8 @@ obscured faces may not reach the threshold. ## FocalNet -FocalNet is Appwrite's own compact crop-ranking model, used when -`MODEL_BACKEND=focalnet`. The human-ranking graph is `focalnet-human.onnx` from -the public +FocalNet is Appwrite's own model, used when `MODEL_BACKEND=focalnet`. The +weights are `focalnet-human.onnx` from the public [FocalNet `2026-09-14-rc1` release](https://github.com/appwrite/focalnet/releases/tag/2026-09-14-rc1) (20,398,992 bytes). `make model-focalnet` downloads it, verifies this SHA-256, and stages a copy at `models/optional/focalnet-human.onnx` for Docker: @@ -69,12 +68,11 @@ contract-compatible placeholder lives at `internal/focalnet/testdata/dummy.onnx` for Go tests; it is image-independent and must not be copied into `models/focalnet-human.onnx` or a production image. Docker accepts only the published checksum above. The artifact is redistributed under the MIT license -in `FOCALNET_LICENSE`. Training code, ONNX contract, and teacher-agreement -figures live in [appwrite/focalnet](https://github.com/appwrite/focalnet). +in `FOCALNET_LICENSE`. See [appwrite/focalnet](https://github.com/appwrite/focalnet) +for training code and results. -PR image builds still tolerate a missing file so the default U²-Net backend -starts; they never substitute the dummy. `MODEL_BACKEND=focalnet` fails at -startup if the file is missing. Published release images require the real -checksum (`REQUIRE_FOCALNET_MODEL=1`). Run `make model-focalnet` before -`docker build`, or set repo secret `FOCALNET_GITHUB_TOKEN` if a workflow cannot -read the public release. +PR image builds still start without the file. `MODEL_BACKEND=focalnet` fails at +startup if it is missing. Release images require the real checksum +(`REQUIRE_FOCALNET_MODEL=1`). Run `make model-focalnet` before `docker build`, +or set repo secret `FOCALNET_GITHUB_TOKEN` if a workflow cannot download the +release. From ab4801d6ce9331a0ff432a362aecaec82f99845f Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Fri, 18 Sep 2026 07:49:36 +0000 Subject: [PATCH 5/7] docs: match the FocalNet crop example to 16:9 The sample request asks for a widescreen crop, so the response should not be square. Also clarify that gravity is the selected crop's center. Co-authored-by: Torsten Dittmann --- README.md | 6 +++--- docs/src/routes/index.tsx | 4 ++-- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index af09128..4a230ea 100644 --- a/README.md +++ b/README.md @@ -225,8 +225,8 @@ export MODEL_BACKEND=focalnet ./autogravity ``` -Pass `?aspect_ratio=16:9` (or a positive float such as `1.5`) to rank a -non-square crop. The default is `1:1`. Example response: +Pass `?aspect_ratio=16:9` (or a number such as `1.5`) for a non-square crop. +The default is `1:1`. Example response: ```json { @@ -237,7 +237,7 @@ non-square crop. The default is `1:1`. Example response: "left": 120, "top": 40, "width": 480, - "height": 480, + "height": 270, "retained_importance": 0.88 } } diff --git a/docs/src/routes/index.tsx b/docs/src/routes/index.tsx index 624418e..17ebc8b 100644 --- a/docs/src/routes/index.tsx +++ b/docs/src/routes/index.tsx @@ -51,7 +51,7 @@ function DocsPage() {

The default path is still YuNet + U²-Net. Set{' '} @@ -312,7 +312,7 @@ function DocsPage() { {' '}"width":{' '} 480,{'\n'} {' '}"height":{' '} - 480,{'\n'} + 270,{'\n'} {' '}"retained_importance":{' '} 0.88{'\n'} {' '}{'}'}{'\n'} From 532397e2e716ac6f717bbd2652ef03db84b360d3 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Fri, 18 Sep 2026 07:52:57 +0000 Subject: [PATCH 6/7] docs: drop the FocalNet pill and extra how-to paragraph Keep the section to the lead and run commands. Co-authored-by: Torsten Dittmann --- docs/src/routes/index.tsx | 16 ---------------- 1 file changed, 16 deletions(-) diff --git a/docs/src/routes/index.tsx b/docs/src/routes/index.tsx index 17ebc8b..85f4339 100644 --- a/docs/src/routes/index.tsx +++ b/docs/src/routes/index.tsx @@ -27,7 +27,6 @@ function DocsPage() {

Face-first · saliency fallback - FocalNet backend JPEG · PNG · WebP · GIF CPU-only
@@ -53,21 +52,6 @@ function DocsPage() { title="Appwrite's FocalNet model" lead="FocalNet is Appwrite's own model. It picks a crop and uses the crop's center as the focal point, without a separate face detector." /> -

- The default path is still YuNet + U²-Net. Set{' '} - MODEL_BACKEND=focalnet after{' '} - make model-focalnet to switch. Pass{' '} - ?aspect_ratio=16:9 for a widescreen crop; - the default is 1:1. See{' '} - - appwrite/focalnet - {' '} - for the model itself. -

$ make model-focalnet From 74dfd5dd3e133bff69d6e7a3f7d3f2c3fbf61060 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Fri, 18 Sep 2026 07:58:03 +0000 Subject: [PATCH 7/7] docs: note that aspect_ratio must be a positive number Zero and negative values are rejected by the API. Co-authored-by: Torsten Dittmann --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 4a230ea..35b3cc2 100644 --- a/README.md +++ b/README.md @@ -225,7 +225,7 @@ export MODEL_BACKEND=focalnet ./autogravity ``` -Pass `?aspect_ratio=16:9` (or a number such as `1.5`) for a non-square crop. +Pass `?aspect_ratio=16:9` (or a positive number such as `1.5`) for a non-square crop. The default is `1:1`. Example response: ```json