diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index ddcd835..e221fe0 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -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 @@ -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 diff --git a/README.md b/README.md index 1bec860..e502b80 100644 --- a/README.md +++ b/README.md @@ -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 @@ -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, diff --git a/web/scripts/fetch-model.ts b/web/scripts/fetch-model.ts index df8a19e..627b837 100644 --- a/web/scripts/fetch-model.ts +++ b/web/scripts/fetch-model.ts @@ -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 }); @@ -24,24 +20,20 @@ async function sha256(path: string): Promise { async function installModel(source: string): Promise { 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); } @@ -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.", ); } diff --git a/web/src/main.ts b/web/src/main.ts index 034fc4c..0df84cd 100644 --- a/web/src/main.ts +++ b/web/src/main.ts @@ -65,8 +65,8 @@ app.innerHTML = `

- 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 bun run fetch-model.

`;