Customizing

Swapping models, adding languages, adding tools — how to move through the codebase.

Code layout

src/
├── lib/
│   ├── ort/               ONNX Runtime layer shared by both tools
│   │   ├── runtime.ts     runtime setup (wasm paths, thread count)
│   │   └── modelCache.ts  weight downloads + Cache Storage
│   ├── upscaler/          AI upscaling: model table, session, tensors, tiling
│   ├── cutout/            AI cutout: model table, preprocessing, session, compositing
│   ├── models.ts          upscaler model registry
│   └── types.ts           shared types
├── components/
│   ├── compress/          compression workbench
│   ├── upscale/           upscale workbench
│   └── cutout/            cutout workbench
├── hooks/                 useUpscaler / useCutout state machines
└── i18n/                  Chinese and English copy

One deliberate design choice: the inference layer never contains user-facing copy. Progress is emitted as structured { phase, ratio, detail }, errors are thrown as objects carrying a code, and result notes are structured data. All wording is rendered by i18n — so adding a language never means touching algorithm code.

Swapping in another upscaler

Add an entry in src/lib/models.ts and drop the .onnx file into public/models/:

{
  id: 'my-model',
  label: 'Display name',
  url: '/models/my-model.onnx',
  scale: 4,
  approxBytes: 0,
  inputRange: 'unit',        // 'unit' = 0–1, 'byte' = 0–255
  inputName: 'image',        // input tensor name in the ONNX graph
  outputName: 'upscaled_image',
  fixedInputSize: 128,       // fixed input edge; null for dynamic shapes
  license: 'BSD-3-Clause',
}

Keep any single file under 25 MiB or Cloudflare will reject the deploy. The model description copy goes under upscaleModels in i18n, keyed by id.

Swapping in another cutout model

Add an entry in src/lib/cutout/models.ts:

{
  id: 'my-matting-model',
  label: 'Display name',
  variants: {
    webgpu: { url: 'https://…/model_fp16.onnx', approxBytes: 0, dtype: 'fp16' },
    wasm:   { url: 'https://…/model.onnx',      approxBytes: 0, dtype: 'fp32' },
  },
  inputSize: 512,
  inputName: 'input_image',
  outputName: 'output_image',   // a typo here triggers tensor-missing
  outputIsLogits: true,         // does the output need a sigmoid?
  license: 'MIT',
}
If the model takes a different square input size, just change inputSize and the preprocessing follows. But the ImageNet mean/std in the preprocessing is hard-coded (as IMAGE_MEAN / IMAGE_STD in models.ts) — check it whenever you swap models.

The output tensor name is fault-tolerant. If the registry name doesn't match, it falls back to the sole output; with multiple outputs it picks the one that looks most like a mask (the spatial tensor with the smallest last dimension). Only if nothing can be identified does it throw tensor-missing, listing the output names actually present in the graph.

Adding a language

  1. Create a locale file under src/i18n/locales/, using en.ts as the template. The type is Translation = typeof en, so a missing translation is a compile-time error.
  2. Register the language in src/i18n/index.ts.
  3. Copy the HTML entries: one language-prefixed copy each of index.html, upscale/index.html and cutout/index.html, with canonical and hreflang updated.
  4. Add the new entries to rollupOptions.input in vite.config.ts.

Adding a new tool

  1. Build an inference layer under src/lib/ if the tool needs a model. Reuse src/lib/ort/ for the runtime and cache — do not pull in a second copy of ONNX Runtime.
  2. Write the state machine under src/hooks/, following the same rule: no user-facing copy.
  3. Write the components under src/components/<tool>/.
  4. Add it in src/App.tsx — the Tool type, the PATHS map and the TOOLS list. Tabs and routing follow automatically.
  5. Create the HTML entries (including the English version) and add them to DIR_INDEX and input in vite.config.ts.
  6. Add the i18n copy, the sitemap entries, and the matching documentation page.

Known limits

LimitDetails
Upscale output capped at 64 megapixels At 4× that is roughly a 2000×2000 source. Anything larger must be cropped or downscaled first.
Upscale and cutout are one image at a time Compression supports batches; these two are heavy compute jobs where you check each result.
Cutout mask capped at 512×512 For larger sources, edge accuracy is limited by mask resolution. The result panel adds a note.
A machine without WebGPU has a limited experience It falls back to WASM, and cutout downloads 192 MB instead of 94 MB.
Cutout only finds the most salient subject Multi-object images get only the most prominent one; a pure landscape may be judged "no subject" and error out.

Local development

npm run dev        # dev server (with COOP/COEP headers)
npm run build      # production build
npm run typecheck  # types only
npm run lint       # ESLint
npm run preview    # preview the build output
The dev server also sends the isolation headers (server.headers and preview.headers in vite.config.ts share the same constant), so local and production behaviour match — otherwise multi-threaded WASM would be untestable locally.