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

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 16 additions & 2 deletions .github/workflows/test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,8 @@ jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: astral-sh/setup-uv@v6
- uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0
- uses: astral-sh/setup-uv@d0cc045d04ccac9d8b7881df0226f9e82c39688e # v6.8.0
with:
enable-cache: true
- run: uv sync --frozen --extra train
Expand All @@ -16,3 +16,17 @@ jobs:
- run: uv run --frozen --extra train pytest -q
- run: uv build --out-dir dist
- run: uv run --frozen python scripts/verify-package-license.py dist

web:
runs-on: ubuntu-latest
defaults:
run:
working-directory: web
steps:
- uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0
- uses: oven-sh/setup-bun@0c5077e51419868618aeaa5fe8019c62421857d6 # v2.2.0
with:
bun-version: 1.4.2
- run: bun install --frozen-lockfile
- run: bun test
- run: bun run build
23 changes: 13 additions & 10 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -120,24 +120,26 @@ the same image; it is not a calibrated confidence value.
### Browser demo

A static crop lab under `web/` runs the same format-v2 ONNX in the browser with
ONNX Runtime Web. Photos stay on-device. The 19.45 MiB weights are gitignored;
`bun run fetch-model` downloads them from a published artifact URL (or a local
path / Modal volume). Bun is the package manager, bundler, test runner, and
local static server. Publish the build with [yeet.page](https://yeet.page/):
ONNX Runtime Web. Photos stay on-device. This repository does not publish a
checkpoint; the 19.45 MiB weights stay gitignored. Export a model, then point
the lab at that file. Bun is the package manager, bundler, test runner, and
local static server:

```sh
cd web
bun install
bun run fetch-model
FOCALNET_ONNX=../artifacts/focalnet-human.onnx bun run fetch-model
bun test
bun run dev
bun run build
bunx @dittmann/yeet dist
```

The demo compares the human-ranked crop with the importance-retention crop from
the same map. A yeet URL is unlisted, not private: anyone with the link can
download `focalnet-human.onnx`.
`FOCALNET_ALLOW_DUMMY=1 bun run fetch-model` writes a tiny placeholder for UI
layout only. Set `FOCALNET_ONNX_SHA256` to verify a known file, or
`FOCALNET_ONNX_URL` to fetch a checkpoint you already host. Source order is
existing `web/public/focalnet-human.onnx`, then `FOCALNET_ONNX`, then
`FOCALNET_ONNX_URL`; dummy mode still runs if a configured URL fails. The demo
compares the human-ranked crop with the importance-retention crop from the same
map.

## Training

Expand Down Expand Up @@ -289,6 +291,7 @@ to other languages against this implementation.
uv run --frozen --extra train ruff check .
uv run --frozen --extra train ruff format --check .
uv run --frozen --extra train pytest -q
(cd web && bun install --frozen-lockfile && bun test)
```

Tests cover coordinate handling, EXIF orientation, alpha composition, padding,
Expand Down
76 changes: 32 additions & 44 deletions web/scripts/fetch-model.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,12 +5,8 @@ import { join } from "node:path";

const root = join(import.meta.dir, "..", "..");
const dest = join(root, "web", "public", "focalnet-human.onnx");
const expected =
Bun.env.FOCALNET_ONNX_SHA256 ??
"59164c601c98cea3f62b25166710831dac63e1a872fc64767c65316ad5385439";
const modelUrl =
Bun.env.FOCALNET_ONNX_URL ?? "https://knowledgeable-sun-ef9f.yeet.page/focalnet-human.onnx";
const volumePath = "/runs/repvit-m0-9-500k-cpc-gaic-lr3e4-candidate/focalnet-human.onnx";
const expected = Bun.env.FOCALNET_ONNX_SHA256;
const modelUrl = Bun.env.FOCALNET_ONNX_URL;
const allowDummy = Bun.env.FOCALNET_ALLOW_DUMMY === "1";

await mkdir(join(dest, ".."), { recursive: true });
Expand All @@ -24,24 +20,20 @@ async function sha256(path: string): Promise<string> {
async function installModel(source: string): Promise<void> {
await Bun.write(dest, Bun.file(source));
const actual = await sha256(dest);
if (actual !== expected) {
if (expected && actual !== expected) {
await unlink(dest);
throw new Error(`Model hash ${actual} does not match ${expected}`);
}
console.log(`Wrote ${dest}`);
console.log(`Wrote ${dest} (${actual})`);
}

if (await Bun.file(dest).exists()) {
const actual = await sha256(dest);
if (actual === expected) {
console.log(`Using existing model at ${dest}`);
if (!expected || actual === expected || allowDummy) {
console.log(`Using existing model at ${dest} (${actual})`);
process.exit(0);
}
if (allowDummy) {
console.log(`Using existing dummy model at ${dest}`);
process.exit(0);
}
console.warn(`Existing model hash ${actual} does not match ${expected}; re-downloading`);
console.warn(`Existing model hash ${actual} does not match ${expected}; replacing`);
await unlink(dest);
}

Expand All @@ -50,40 +42,36 @@ if (Bun.env.FOCALNET_ONNX) {
process.exit(0);
}

try {
const response = await fetch(modelUrl);
if (!response.ok) {
throw new Error(`GET ${modelUrl} → ${response.status}`);
}
if (modelUrl) {
const tmp = join(tmpdir(), "focalnet-human.onnx.download");
await Bun.write(tmp, response);
await installModel(tmp);
await unlink(tmp);
process.exit(0);
} catch (error) {
console.warn(error instanceof Error ? error.message : error);
let installed = false;
try {
const response = await fetch(modelUrl);
if (!response.ok) {
throw new Error(`GET ${modelUrl} → ${response.status}`);
}
await Bun.write(tmp, response);
await installModel(tmp);
installed = true;
} catch (error) {
const detail = error instanceof Error ? error.message : String(error);
if (!allowDummy) {
throw new Error(detail);
}
console.warn(detail);
}
if (installed) {
await unlink(tmp).catch((error) => {
console.warn(error instanceof Error ? error.message : error);
});
process.exit(0);
}
}

if (!allowDummy) {
const modal = Bun.which("uvx");
if (modal) {
const tmpdirPath = join(tmpdir(), "focalnet-modal-onnx");
await mkdir(tmpdirPath, { recursive: true });
const proc = Bun.spawn(
["uvx", "modal", "volume", "get", "focalnet-data", volumePath, tmpdirPath],
{ stdout: "inherit", stderr: "inherit" },
);
if ((await proc.exited) === 0) {
const glob = new Bun.Glob("**/focalnet-human.onnx");
for await (const path of glob.scan({ cwd: tmpdirPath, absolute: true })) {
await installModel(path);
process.exit(0);
}
throw new Error(`Modal volume get did not produce ${volumePath}`);
}
}
throw new Error(
`Could not fetch the production ONNX from ${modelUrl}. Set FOCALNET_ONNX or FOCALNET_ALLOW_DUMMY=1 for UI layout only.`,
"No local checkpoint. Export a model, then set FOCALNET_ONNX to that file " +
"(optional FOCALNET_ONNX_SHA256 to verify). FOCALNET_ALLOW_DUMMY=1 writes a UI placeholder.",
);
}

Expand Down
4 changes: 2 additions & 2 deletions web/src/main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -65,8 +65,8 @@ app.innerHTML = `
</aside>
</div>
<p class="note">
The ranking head is trained on CPC and GAICD judgments. This preview URL is
unlisted, not private: anyone with the link can download the ONNX file.
The ranking head is trained on CPC and GAICD judgments. This lab does not
ship model weights; load a local ONNX with <code>bun run fetch-model</code>.
</p>
</div>
`;
Expand Down
Loading