| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| import { ERROR, EngineError } from "./errors.js"; |
|
|
| |
|
|
| export const SOURCE_KIND = { |
| ID: "id", |
| REMOTE: "remote", |
| REGISTER: "register", |
| FILES: "files", |
| }; |
|
|
| |
| const HAS_SCHEME = /^[a-z][a-z0-9+.-]*:/i; |
|
|
| |
| |
| |
| |
| |
| |
| export const looksLikeUrl = (s) => |
| HAS_SCHEME.test(s) || s.startsWith("/") || s.startsWith("./") || s.startsWith("../"); |
|
|
| |
| export const isDataTransfer = (v) => !!v && typeof v === "object" && "items" in v && "files" in v; |
| export const isFileList = (v) => |
| !!v && |
| typeof v === "object" && |
| typeof v.length === "number" && |
| !Array.isArray(v) && |
| (v.length === 0 || typeof v[0]?.name === "string"); |
|
|
| |
| |
| |
| |
| |
| |
| |
| |
| export function idFromUrl(url) { |
| const path = HAS_SCHEME.test(url) ? safeUrlPath(url) : url; |
| const segments = path.split("/").filter((s) => s && s !== "." && s !== ".."); |
| const last = segments.at(-1); |
| return last ? decodeURIComponent(last) : null; |
| } |
|
|
| function safeUrlPath(url) { |
| try { |
| return new URL(url).pathname; |
| } catch { |
| return url; |
| } |
| } |
|
|
| |
| |
| |
| |
| |
| export function classifySource(src, opts = {}) { |
| if (src === null || src === undefined || src === "") { |
| throw new EngineError( |
| ERROR.BAD_REQUEST, |
| "load() needs a model id, a URL, `{ model, modelLib }`, or a folder " + |
| "(`{ files }`, a FileList, or a drop event's DataTransfer).", |
| ); |
| } |
|
|
| if (typeof src === "string") { |
| if (!looksLikeUrl(src)) return { kind: SOURCE_KIND.ID, modelId: src }; |
| return remote(src, opts.modelLib, opts.id ?? idFromUrl(src)); |
| } |
|
|
| |
| |
| if (isDataTransfer(src) || isFileList(src) || Array.isArray(src)) { |
| return { kind: SOURCE_KIND.FILES, files: src, modelId: opts.id }; |
| } |
|
|
| if (typeof src === "object") { |
| if (src.files !== undefined) { |
| if (src.model || src.modelLib) { |
| throw new EngineError( |
| ERROR.BAD_REQUEST, |
| "load() takes either `files` (local, never fetched) or `model`/`modelLib` " + |
| "(a base URL to fetch), not both.", |
| ); |
| } |
| return { kind: SOURCE_KIND.FILES, files: src.files, modelId: src.modelId ?? opts.id }; |
| } |
| if (src.model !== undefined) { |
| |
| |
| const spec = remote( |
| src.model, |
| src.modelLib ?? opts.modelLib, |
| src.modelId ?? opts.id ?? idFromUrl(String(src.model)), |
| ); |
| return { ...spec, kind: SOURCE_KIND.REGISTER }; |
| } |
| } |
|
|
| throw new EngineError( |
| ERROR.BAD_REQUEST, |
| "load() did not recognise that source. Pass a model id, a URL, " + |
| "`{ model, modelLib }`, or a folder (`{ files }`, a FileList, or a DataTransfer).", |
| { received: typeof src }, |
| ); |
| } |
|
|
| |
| function remote(model, modelLib, modelId) { |
| if (!modelLib) { |
| throw new EngineError( |
| ERROR.BAD_REQUEST, |
| `Loading "${model}" from a URL needs \`modelLib\` — the compiled .wasm — and it cannot be ` + |
| "guessed: across all 163 prebuilt models, none has a lib name derivable from its id and " + |
| "none hosts the lib on the same origin as the weights. " + |
| "Pass load(url, { modelLib: \"https://.../foo-webgpu.wasm\" }).", |
| { model }, |
| ); |
| } |
| if (!modelId) { |
| throw new EngineError( |
| ERROR.BAD_REQUEST, |
| `Could not derive a model id from "${model}". Pass one as load(src, { id }).`, |
| { model }, |
| ); |
| } |
| return { kind: SOURCE_KIND.REMOTE, model, modelLib, modelId }; |
| } |
|
|
| |
| |
| |
| |
| |
| |
| |
| export function nearMatches(wanted, available, limit = 3) { |
| const needle = wanted.toLowerCase(); |
| const scored = available |
| .map((id) => { |
| const hay = id.toLowerCase(); |
| if (hay === needle) return { id, score: 0 }; |
| if (hay.includes(needle) || needle.includes(hay)) return { id, score: 1 }; |
| return { id, score: 2 + editDistance(needle, hay) }; |
| }) |
| |
| |
| .filter(({ score, id }) => score <= 2 + Math.ceil(Math.max(id.length, needle.length) / 3)) |
| .sort((a, b) => a.score - b.score || a.id.localeCompare(b.id)); |
|
|
| return scored.slice(0, limit).map(({ id }) => id); |
| } |
|
|
| |
| function editDistance(a, b) { |
| let prev = Array.from({ length: b.length + 1 }, (_, i) => i); |
| for (let i = 1; i <= a.length; i++) { |
| const row = [i]; |
| for (let j = 1; j <= b.length; j++) { |
| row[j] = Math.min( |
| prev[j] + 1, |
| row[j - 1] + 1, |
| prev[j - 1] + (a[i - 1] === b[j - 1] ? 0 : 1), |
| ); |
| } |
| prev = row; |
| } |
| return prev[b.length]; |
| } |
|
|