@burdenoff/microfe-buildmyiq 2026.1029.4 → 2026.1029.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +18 -0
- package/assets/mediapipe/NOTICE.md +17 -0
- package/assets/mediapipe/blaze_face_short_range.tflite +0 -0
- package/dist/buildmyiq/pages/assessments/faceDetection.d.ts.map +1 -1
- package/dist/buildmyiq/pages/assessments/faceDetection.js +4 -3
- package/dist/buildmyiq/pages/assessments/faceDetection.js.map +1 -1
- package/dist/buildmyiq/pages/assessments/proctoringAssetPaths.d.ts +19 -0
- package/dist/buildmyiq/pages/assessments/proctoringAssetPaths.d.ts.map +1 -0
- package/dist/buildmyiq/pages/assessments/proctoringAssetPaths.js +6 -0
- package/dist/buildmyiq/pages/assessments/proctoringAssetPaths.js.map +1 -0
- package/package.json +8 -2
- package/vite/proctoring-assets.d.mts +20 -0
- package/vite/proctoring-assets.mjs +154 -0
package/README.md
CHANGED
|
@@ -10,3 +10,21 @@ See `AGENTS.md` for development commands and architecture details.
|
|
|
10
10
|
## License
|
|
11
11
|
|
|
12
12
|
Proprietary — Copyright Burdenoff Consultancy Services Pvt. Ltd.
|
|
13
|
+
|
|
14
|
+
## AI proctoring assets (app shells)
|
|
15
|
+
|
|
16
|
+
AI-monitored exams run a face detector in the learner's browser. Its
|
|
17
|
+
WebAssembly runtime and model are served by the app shell from its own origin,
|
|
18
|
+
never from a public CDN. Every shell that mounts this microfrontend must add
|
|
19
|
+
the plugin below to its Vite config; it emits the files under
|
|
20
|
+
`/static/vendor/mediapipe/` at build time and serves them in `vite dev`:
|
|
21
|
+
|
|
22
|
+
```ts
|
|
23
|
+
import { buildmyiqProctoringAssets } from '@burdenoff/microfe-buildmyiq/vite';
|
|
24
|
+
|
|
25
|
+
export default defineConfig({ plugins: [buildmyiqProctoringAssets() /* , … */] });
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
`@mediapipe/tasks-vision` is pinned to 0.10.35 because 1.0.0 and later send
|
|
29
|
+
usage metrics to Google with no opt-out. `proctoringAssets.test.ts` fails if an
|
|
30
|
+
upgrade reintroduces a third-party endpoint.
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
# Third-party model: BlazeFace short-range
|
|
2
|
+
|
|
3
|
+
`blaze_face_short_range.tflite` is the MediaPipe BlazeFace short-range face
|
|
4
|
+
detector (float16, model version 1), published by Google under the Apache
|
|
5
|
+
License 2.0.
|
|
6
|
+
|
|
7
|
+
- Source: https://storage.googleapis.com/mediapipe-models/face_detector/blaze_face_short_range/float16/1/blaze_face_short_range.tflite
|
|
8
|
+
- Model card: https://storage.googleapis.com/mediapipe-assets/MediaPipe%20BlazeFace%20Model%20Card%20(Short%20Range).pdf
|
|
9
|
+
- Size: 229,746 bytes
|
|
10
|
+
- SHA-256: `b4578f35940bf5a1a655214a1cce5cab13eba73c1297cd78e1a04c2380b0152f`
|
|
11
|
+
- MD5 (matches the bucket's `x-goog-hash`): `a3dd6ec31725290770b97cec0cbf94c9`
|
|
12
|
+
|
|
13
|
+
It is vendored here so that BuildMyIQ app shells can serve it from their own
|
|
14
|
+
origin (BOFF-10118) through the `buildmyiqProctoringAssets()` Vite plugin in
|
|
15
|
+
`vite/proctoring-assets.mjs`. To replace it, download the new model, update the
|
|
16
|
+
URL path in both `vite/proctoring-assets.mjs` and
|
|
17
|
+
`src/buildmyiq/pages/assessments/proctoringAssetPaths.ts`, and update this file.
|
|
Binary file
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"faceDetection.d.ts","sourceRoot":"","sources":["../../../../src/buildmyiq/pages/assessments/faceDetection.ts"],"names":[],"mappings":"AAAA
|
|
1
|
+
{"version":3,"file":"faceDetection.d.ts","sourceRoot":"","sources":["../../../../src/buildmyiq/pages/assessments/faceDetection.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;GAmBG;AAEH,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,yBAAyB,CAAC;AAK5D,+CAA+C;AAC/C,eAAO,MAAM,cAAc,MAAM,CAAC;AAIlC,oFAAoF;AACpF,wBAAgB,gBAAgB,IAAI,OAAO,CAAC,YAAY,CAAC,CAiBxD;AAED,MAAM,WAAW,eAAe;IAC9B,SAAS,EAAE,MAAM,CAAC;IAClB,2CAA2C;IAC3C,QAAQ,EAAE,MAAM,CAAC;IACjB,6EAA6E;IAC7E,WAAW,EAAE,MAAM,CAAC;IACpB,wEAAwE;IACxE,QAAQ,EAAE,MAAM,GAAG,IAAI,CAAC;CACzB;AAED,0FAA0F;AAC1F,wBAAgB,YAAY,CAC1B,QAAQ,EAAE,YAAY,EACtB,KAAK,EAAE,gBAAgB,EACvB,WAAW,EAAE,MAAM,GAClB,eAAe,GAAG,IAAI,CAcxB"}
|
|
@@ -1,12 +1,13 @@
|
|
|
1
1
|
import { estimateYawRatio as e } from "../../hooks/aiProctoringSignals.js";
|
|
2
|
+
import { FACE_MODEL_URL as t, TASKS_VISION_WASM_URL as n } from "./proctoringAssetPaths.js";
|
|
2
3
|
//#region src/buildmyiq/pages/assessments/faceDetection.ts
|
|
3
|
-
var
|
|
4
|
+
var r = .5, i = null;
|
|
4
5
|
function a() {
|
|
5
6
|
return i || (i = (async () => {
|
|
6
|
-
let { FaceDetector: e, FilesetResolver: i } = await import("@mediapipe/tasks-vision"), a = await i.forVisionTasks(
|
|
7
|
+
let { FaceDetector: e, FilesetResolver: i } = await import("@mediapipe/tasks-vision"), a = await i.forVisionTasks(n);
|
|
7
8
|
return e.createFromOptions(a, {
|
|
8
9
|
baseOptions: {
|
|
9
|
-
modelAssetPath:
|
|
10
|
+
modelAssetPath: t,
|
|
10
11
|
delegate: "CPU"
|
|
11
12
|
},
|
|
12
13
|
runningMode: "VIDEO",
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"faceDetection.js","names":[],"sources":["../../../../src/buildmyiq/pages/assessments/faceDetection.ts"],"sourcesContent":["/**\n * In-browser face detection for AI proctoring (BOFF-9690).\n * @module buildmyiq/pages/assessments/faceDetection\n *\n * Uses MediaPipe's BlazeFace short-range detector (Apache-2.0) running in\n * WebAssembly on the learner's device. Frames never leave the browser: the\n * detector returns face boxes and six keypoints per face, from which we keep\n * only a face count, a score and a head-turn ratio.\n *\n * The JavaScript is code-split (dynamic import) so learners who never sit an\n * AI-monitored exam never download it. The WebAssembly runtime and the model\n * file are
|
|
1
|
+
{"version":3,"file":"faceDetection.js","names":[],"sources":["../../../../src/buildmyiq/pages/assessments/faceDetection.ts"],"sourcesContent":["/**\n * In-browser face detection for AI proctoring (BOFF-9690).\n * @module buildmyiq/pages/assessments/faceDetection\n *\n * Uses MediaPipe's BlazeFace short-range detector (Apache-2.0) running in\n * WebAssembly on the learner's device. Frames never leave the browser: the\n * detector returns face boxes and six keypoints per face, from which we keep\n * only a face count, a score and a head-turn ratio.\n *\n * The JavaScript is code-split (dynamic import) so learners who never sit an\n * AI-monitored exam never download it. The WebAssembly runtime and the model\n * file are served by the app shell itself, from its own origin, under\n * version-pinned paths (BOFF-10118). The learner's browser therefore contacts\n * no third party at all while the detector runs.\n *\n * tasks-vision is pinned to 0.10.35 on purpose: from 1.0.0 the library sends\n * usage metrics to Google (odml.pa.googleapis.com) with no opt-out, which\n * would break the \"nothing leaves the device\" promise made to learners. A\n * unit test fails if an upgrade reintroduces that endpoint.\n */\n\nimport type { FaceDetector } from '@mediapipe/tasks-vision';\n\nimport { estimateYawRatio } from '../../hooks/aiProctoringSignals';\nimport { FACE_MODEL_URL, TASKS_VISION_WASM_URL } from './proctoringAssetPaths';\n\n/** Detections below this score are ignored. */\nexport const MIN_FACE_SCORE = 0.5;\n\nlet detectorPromise: Promise<FaceDetector> | null = null;\n\n/** Load (once per page) the face detector. Rejects if the browser cannot run it. */\nexport function loadFaceDetector(): Promise<FaceDetector> {\n if (!detectorPromise) {\n detectorPromise = (async () => {\n const { FaceDetector: Detector, FilesetResolver } = await import('@mediapipe/tasks-vision');\n const fileset = await FilesetResolver.forVisionTasks(TASKS_VISION_WASM_URL);\n return Detector.createFromOptions(fileset, {\n baseOptions: { modelAssetPath: FACE_MODEL_URL, delegate: 'CPU' },\n runningMode: 'VIDEO',\n minDetectionConfidence: MIN_FACE_SCORE,\n });\n })();\n // A failed load (offline, assets not deployed) may be retried on the next call.\n detectorPromise.catch(() => {\n detectorPromise = null;\n });\n }\n return detectorPromise;\n}\n\nexport interface FaceObservation {\n faceCount: number;\n /** Highest face score (0 when no face). */\n topScore: number;\n /** Second-highest score, i.e. confidence that a second person is present. */\n secondScore: number;\n /** Head-turn ratio of the single face, null unless exactly one face. */\n yawRatio: number | null;\n}\n\n/** Run the detector on the current video frame. Null while the video has no frame yet. */\nexport function observeFaces(\n detector: FaceDetector,\n video: HTMLVideoElement,\n timestampMs: number\n): FaceObservation | null {\n if (video.readyState < 2 || video.videoWidth === 0) return null;\n const result = detector.detectForVideo(video, timestampMs);\n const scored = result.detections\n .map((d) => ({ score: d.categories[0]?.score ?? 0, keypoints: d.keypoints }))\n .filter((d) => d.score >= MIN_FACE_SCORE)\n .sort((a, b) => b.score - a.score);\n const [first, second] = scored;\n return {\n faceCount: scored.length,\n topScore: first?.score ?? 0,\n secondScore: second?.score ?? 0,\n yawRatio: scored.length === 1 && first ? estimateYawRatio(first.keypoints) : null,\n };\n}\n"],"mappings":";;;AA2BA,IAAa,IAAiB,IAE1B,IAAgD;AAGpD,SAAgB,IAA0C;CAgBxD,OAfK,MACH,KAAmB,YAAY;EAC7B,IAAM,EAAE,cAAc,GAAU,uBAAoB,MAAM,OAAO,4BAC3D,IAAU,MAAM,EAAgB,eAAe,CAAqB;EAC1E,OAAO,EAAS,kBAAkB,GAAS;GACzC,aAAa;IAAE,gBAAgB;IAAgB,UAAU;GAAM;GAC/D,aAAa;GACb,wBAAwB;EAC1B,CAAC;CACH,GAAG,GAEH,EAAgB,YAAY;EAC1B,IAAkB;CACpB,CAAC,IAEI;AACT;AAaA,SAAgB,EACd,GACA,GACA,GACwB;CACxB,IAAI,EAAM,aAAa,KAAK,EAAM,eAAe,GAAG,OAAO;CAE3D,IAAM,IADS,EAAS,eAAe,GAAO,CAC/B,EAAO,WACnB,KAAK,OAAO;EAAE,OAAO,EAAE,WAAW,IAAI,SAAS;EAAG,WAAW,EAAE;CAAU,EAAE,EAC3E,QAAQ,MAAM,EAAE,SAAS,CAAc,EACvC,MAAM,GAAG,MAAM,EAAE,QAAQ,EAAE,KAAK,GAC7B,CAAC,GAAO,KAAU;CACxB,OAAO;EACL,WAAW,EAAO;EAClB,UAAU,GAAO,SAAS;EAC1B,aAAa,GAAQ,SAAS;EAC9B,UAAU,EAAO,WAAW,KAAK,IAAQ,EAAiB,EAAM,SAAS,IAAI;CAC/E;AACF"}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Same-origin URLs of the AI proctoring face-detector assets (BOFF-10118).
|
|
3
|
+
* @module buildmyiq/pages/assessments/proctoringAssetPaths
|
|
4
|
+
*
|
|
5
|
+
* The app shell serves these files itself: its Vite config adds the
|
|
6
|
+
* `buildmyiqProctoringAssets()` plugin from `@burdenoff/microfe-buildmyiq/vite`
|
|
7
|
+
* (source: `vite/proctoring-assets.mjs`), which copies them into the build at
|
|
8
|
+
* exactly these paths. The plugin defines the same constants; a unit test
|
|
9
|
+
* fails if the two drift apart or if the installed package version changes.
|
|
10
|
+
*/
|
|
11
|
+
/** Must equal the exact `@mediapipe/tasks-vision` version pinned in package.json. */
|
|
12
|
+
export declare const TASKS_VISION_VERSION = "0.10.35";
|
|
13
|
+
/** URL prefix under which the app shell serves every asset. */
|
|
14
|
+
export declare const PROCTORING_ASSET_BASE = "/static/vendor/mediapipe";
|
|
15
|
+
/** Directory URL handed to `FilesetResolver.forVisionTasks`. */
|
|
16
|
+
export declare const TASKS_VISION_WASM_URL = "/static/vendor/mediapipe/tasks-vision/0.10.35/wasm";
|
|
17
|
+
/** BlazeFace short-range model (float16, model version 1). */
|
|
18
|
+
export declare const FACE_MODEL_URL = "/static/vendor/mediapipe/models/blaze_face_short_range/float16/1/blaze_face_short_range.tflite";
|
|
19
|
+
//# sourceMappingURL=proctoringAssetPaths.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"proctoringAssetPaths.d.ts","sourceRoot":"","sources":["../../../../src/buildmyiq/pages/assessments/proctoringAssetPaths.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AAEH,qFAAqF;AACrF,eAAO,MAAM,oBAAoB,YAAY,CAAC;AAE9C,+DAA+D;AAC/D,eAAO,MAAM,qBAAqB,6BAA6B,CAAC;AAEhE,gEAAgE;AAChE,eAAO,MAAM,qBAAqB,uDAAuE,CAAC;AAE1G,8DAA8D;AAC9D,eAAO,MAAM,cAAc,mGAAmG,CAAC"}
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
//#region src/buildmyiq/pages/assessments/proctoringAssetPaths.ts
|
|
2
|
+
var e = "0.10.35", t = "/static/vendor/mediapipe", n = `${t}/tasks-vision/${e}/wasm`, r = `${t}/models/blaze_face_short_range/float16/1/blaze_face_short_range.tflite`;
|
|
3
|
+
//#endregion
|
|
4
|
+
export { r as FACE_MODEL_URL, n as TASKS_VISION_WASM_URL };
|
|
5
|
+
|
|
6
|
+
//# sourceMappingURL=proctoringAssetPaths.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"proctoringAssetPaths.js","names":[],"sources":["../../../../src/buildmyiq/pages/assessments/proctoringAssetPaths.ts"],"sourcesContent":["/**\n * Same-origin URLs of the AI proctoring face-detector assets (BOFF-10118).\n * @module buildmyiq/pages/assessments/proctoringAssetPaths\n *\n * The app shell serves these files itself: its Vite config adds the\n * `buildmyiqProctoringAssets()` plugin from `@burdenoff/microfe-buildmyiq/vite`\n * (source: `vite/proctoring-assets.mjs`), which copies them into the build at\n * exactly these paths. The plugin defines the same constants; a unit test\n * fails if the two drift apart or if the installed package version changes.\n */\n\n/** Must equal the exact `@mediapipe/tasks-vision` version pinned in package.json. */\nexport const TASKS_VISION_VERSION = '0.10.35';\n\n/** URL prefix under which the app shell serves every asset. */\nexport const PROCTORING_ASSET_BASE = '/static/vendor/mediapipe';\n\n/** Directory URL handed to `FilesetResolver.forVisionTasks`. */\nexport const TASKS_VISION_WASM_URL = `${PROCTORING_ASSET_BASE}/tasks-vision/${TASKS_VISION_VERSION}/wasm`;\n\n/** BlazeFace short-range model (float16, model version 1). */\nexport const FACE_MODEL_URL = `${PROCTORING_ASSET_BASE}/models/blaze_face_short_range/float16/1/blaze_face_short_range.tflite`;\n"],"mappings":";AAYA,IAAa,IAAuB,WAGvB,IAAwB,4BAGxB,IAAwB,GAAG,EAAsB,gBAAgB,EAAqB,QAGtF,IAAiB,GAAG,EAAsB"}
|
package/package.json
CHANGED
|
@@ -1,10 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@burdenoff/microfe-buildmyiq",
|
|
3
|
-
"version": "2026.1029.
|
|
3
|
+
"version": "2026.1029.5",
|
|
4
4
|
"description": "Burdenoff BuildMyIQ Microfrontend — education, learning & development",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"files": [
|
|
7
7
|
"dist",
|
|
8
|
+
"vite",
|
|
9
|
+
"assets",
|
|
8
10
|
"package.json",
|
|
9
11
|
"README.md"
|
|
10
12
|
],
|
|
@@ -20,6 +22,10 @@
|
|
|
20
22
|
"./services/createSandboxAssistantTransport": {
|
|
21
23
|
"types": "./dist/services/createSandboxAssistantTransport.d.ts",
|
|
22
24
|
"import": "./dist/services/createSandboxAssistantTransport.js"
|
|
25
|
+
},
|
|
26
|
+
"./vite": {
|
|
27
|
+
"types": "./vite/proctoring-assets.d.mts",
|
|
28
|
+
"import": "./vite/proctoring-assets.mjs"
|
|
23
29
|
}
|
|
24
30
|
},
|
|
25
31
|
"main": "./dist/index.js",
|
|
@@ -48,7 +54,7 @@
|
|
|
48
54
|
"dependencies": {
|
|
49
55
|
"@apollo/client": "^4.0.11",
|
|
50
56
|
"@burdenoff/fe-libs": "2026.1006.1",
|
|
51
|
-
"@mediapipe/tasks-vision": "
|
|
57
|
+
"@mediapipe/tasks-vision": "0.10.35",
|
|
52
58
|
"@tanstack/react-query": "^5.90.16",
|
|
53
59
|
"clsx": "^2.1.1",
|
|
54
60
|
"graphql": "^16.10.0",
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import type { Plugin } from 'vite';
|
|
2
|
+
|
|
3
|
+
export declare const TASKS_VISION_VERSION: string;
|
|
4
|
+
export declare const PROCTORING_ASSET_BASE: string;
|
|
5
|
+
export declare const TASKS_VISION_WASM_URL: string;
|
|
6
|
+
export declare const FACE_MODEL_URL: string;
|
|
7
|
+
export declare const TASKS_VISION_WASM_FILES: readonly string[];
|
|
8
|
+
|
|
9
|
+
export interface ProctoringAssetsOptions {
|
|
10
|
+
/** Root of the microfe-buildmyiq package. Only tests override it. */
|
|
11
|
+
packageDir?: string;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
/** Map from served URL path to the absolute file that backs it. */
|
|
15
|
+
export declare function resolveProctoringAssets(options?: ProctoringAssetsOptions): Map<string, string>;
|
|
16
|
+
|
|
17
|
+
/** Vite plugin that emits and dev-serves the AI proctoring face-detector assets. */
|
|
18
|
+
export declare function buildmyiqProctoringAssets(options?: ProctoringAssetsOptions): Plugin;
|
|
19
|
+
|
|
20
|
+
export default buildmyiqProctoringAssets;
|
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Vite plugin that self-hosts the AI proctoring face-detector assets (BOFF-10118).
|
|
3
|
+
*
|
|
4
|
+
* The in-browser face detector used by AI-monitored exams needs two kinds of
|
|
5
|
+
* static files at run time: the MediaPipe tasks-vision WebAssembly runtime and
|
|
6
|
+
* the BlazeFace short-range model. Instead of fetching them from public CDNs,
|
|
7
|
+
* every app shell that mounts this microfrontend adds this plugin to its Vite
|
|
8
|
+
* config. It then:
|
|
9
|
+
*
|
|
10
|
+
* - emits the files into the build output under `/static/vendor/mediapipe/…`
|
|
11
|
+
* (the deploy workflow uploads `static/*` with an immutable cache header,
|
|
12
|
+
* and every path is versioned so a later upgrade lands on a new URL), and
|
|
13
|
+
* - serves the same paths from the Vite dev server.
|
|
14
|
+
*
|
|
15
|
+
* The WebAssembly files are copied from the installed, version-pinned
|
|
16
|
+
* `@mediapipe/tasks-vision` package; the model ships inside this package under
|
|
17
|
+
* `assets/mediapipe/`. The browser code in `faceDetection.ts` reads the same
|
|
18
|
+
* paths from `proctoringAssetPaths.ts`; a unit test keeps the two in step.
|
|
19
|
+
*
|
|
20
|
+
* Usage in an app shell's vite.config.ts:
|
|
21
|
+
*
|
|
22
|
+
* import { buildmyiqProctoringAssets } from '@burdenoff/microfe-buildmyiq/vite';
|
|
23
|
+
* export default defineConfig({ plugins: [buildmyiqProctoringAssets(), …] });
|
|
24
|
+
*/
|
|
25
|
+
|
|
26
|
+
import { createReadStream, existsSync, readFileSync, statSync } from 'node:fs';
|
|
27
|
+
import { createRequire } from 'node:module';
|
|
28
|
+
import { dirname, join, resolve } from 'node:path';
|
|
29
|
+
import { fileURLToPath } from 'node:url';
|
|
30
|
+
|
|
31
|
+
/** Must equal the exact `@mediapipe/tasks-vision` version pinned in package.json. */
|
|
32
|
+
export const TASKS_VISION_VERSION = '0.10.35';
|
|
33
|
+
|
|
34
|
+
/** URL prefix (same origin as the app shell) under which every asset is served. */
|
|
35
|
+
export const PROCTORING_ASSET_BASE = '/static/vendor/mediapipe';
|
|
36
|
+
|
|
37
|
+
/** Directory URL handed to `FilesetResolver.forVisionTasks`. */
|
|
38
|
+
export const TASKS_VISION_WASM_URL = `${PROCTORING_ASSET_BASE}/tasks-vision/${TASKS_VISION_VERSION}/wasm`;
|
|
39
|
+
|
|
40
|
+
/** URL of the BlazeFace short-range model (float16, model version 1). */
|
|
41
|
+
export const FACE_MODEL_URL = `${PROCTORING_ASSET_BASE}/models/blaze_face_short_range/float16/1/blaze_face_short_range.tflite`;
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Runtime files `FilesetResolver.forVisionTasks` may pick: the SIMD build, and
|
|
45
|
+
* the non-SIMD fallback for browsers without WebAssembly SIMD.
|
|
46
|
+
*/
|
|
47
|
+
export const TASKS_VISION_WASM_FILES = Object.freeze([
|
|
48
|
+
'vision_wasm_internal.js',
|
|
49
|
+
'vision_wasm_internal.wasm',
|
|
50
|
+
'vision_wasm_nosimd_internal.js',
|
|
51
|
+
'vision_wasm_nosimd_internal.wasm',
|
|
52
|
+
]);
|
|
53
|
+
|
|
54
|
+
const CONTENT_TYPES = {
|
|
55
|
+
'.js': 'text/javascript; charset=utf-8',
|
|
56
|
+
'.wasm': 'application/wasm',
|
|
57
|
+
'.tflite': 'application/octet-stream',
|
|
58
|
+
};
|
|
59
|
+
|
|
60
|
+
const packageRoot = resolve(dirname(fileURLToPath(import.meta.url)), '..');
|
|
61
|
+
|
|
62
|
+
function contentTypeFor(file) {
|
|
63
|
+
const dot = file.lastIndexOf('.');
|
|
64
|
+
return CONTENT_TYPES[dot === -1 ? '' : file.slice(dot)] ?? 'application/octet-stream';
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* Locate every asset on disk. Returns a map from served URL path to absolute
|
|
69
|
+
* file path, and throws a descriptive error when anything is missing or when
|
|
70
|
+
* the installed tasks-vision version differs from the pinned one.
|
|
71
|
+
*
|
|
72
|
+
* @param {{ packageDir?: string }} [options]
|
|
73
|
+
* @returns {Map<string, string>}
|
|
74
|
+
*/
|
|
75
|
+
export function resolveProctoringAssets(options = {}) {
|
|
76
|
+
const baseDir = options.packageDir ?? packageRoot;
|
|
77
|
+
const require = createRequire(join(baseDir, 'package.json'));
|
|
78
|
+
|
|
79
|
+
/** @type {Map<string, string>} */
|
|
80
|
+
const assets = new Map();
|
|
81
|
+
let wasmDir = '';
|
|
82
|
+
for (const file of TASKS_VISION_WASM_FILES) {
|
|
83
|
+
// Each runtime file is an explicit subpath export of @mediapipe/tasks-vision.
|
|
84
|
+
const resolved = require.resolve(`@mediapipe/tasks-vision/${file}`);
|
|
85
|
+
wasmDir = dirname(resolved);
|
|
86
|
+
assets.set(`${TASKS_VISION_WASM_URL}/${file}`, resolved);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
const installedManifest = join(wasmDir, '..', 'package.json');
|
|
90
|
+
const installed = JSON.parse(readFileSync(installedManifest, 'utf8')).version;
|
|
91
|
+
if (installed !== TASKS_VISION_VERSION) {
|
|
92
|
+
throw new Error(
|
|
93
|
+
`[buildmyiq:proctoring-assets] @mediapipe/tasks-vision ${installed} is installed, ` +
|
|
94
|
+
`but the face detector is pinned to ${TASKS_VISION_VERSION}. Install the pinned version.`
|
|
95
|
+
);
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
const model = join(baseDir, 'assets', 'mediapipe', 'blaze_face_short_range.tflite');
|
|
99
|
+
assets.set(FACE_MODEL_URL, model);
|
|
100
|
+
|
|
101
|
+
for (const [url, file] of assets) {
|
|
102
|
+
if (!existsSync(file) || statSync(file).size === 0) {
|
|
103
|
+
throw new Error(`[buildmyiq:proctoring-assets] missing asset for ${url}: ${file}`);
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
return assets;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* The Vite plugin. Pass `packageDir` only in tests.
|
|
111
|
+
*
|
|
112
|
+
* @param {{ packageDir?: string }} [options]
|
|
113
|
+
* @returns {import('vite').Plugin}
|
|
114
|
+
*/
|
|
115
|
+
export function buildmyiqProctoringAssets(options = {}) {
|
|
116
|
+
/** @type {Map<string, string> | null} */
|
|
117
|
+
let assets = null;
|
|
118
|
+
const getAssets = () => {
|
|
119
|
+
assets ??= resolveProctoringAssets(options);
|
|
120
|
+
return assets;
|
|
121
|
+
};
|
|
122
|
+
|
|
123
|
+
return {
|
|
124
|
+
name: 'buildmyiq:proctoring-assets',
|
|
125
|
+
|
|
126
|
+
buildStart() {
|
|
127
|
+
getAssets();
|
|
128
|
+
},
|
|
129
|
+
|
|
130
|
+
configureServer(server) {
|
|
131
|
+
server.middlewares.use((req, res, next) => {
|
|
132
|
+
const path = (req.url ?? '').split('?')[0];
|
|
133
|
+
const file = path.startsWith(PROCTORING_ASSET_BASE) ? getAssets().get(path) : undefined;
|
|
134
|
+
if (!file) {
|
|
135
|
+
next();
|
|
136
|
+
return;
|
|
137
|
+
}
|
|
138
|
+
res.statusCode = 200;
|
|
139
|
+
res.setHeader('Content-Type', contentTypeFor(file));
|
|
140
|
+
res.setHeader('Content-Length', String(statSync(file).size));
|
|
141
|
+
res.setHeader('Cache-Control', 'no-cache');
|
|
142
|
+
createReadStream(file).pipe(res);
|
|
143
|
+
});
|
|
144
|
+
},
|
|
145
|
+
|
|
146
|
+
generateBundle() {
|
|
147
|
+
for (const [url, file] of getAssets()) {
|
|
148
|
+
this.emitFile({ type: 'asset', fileName: url.slice(1), source: readFileSync(file) });
|
|
149
|
+
}
|
|
150
|
+
},
|
|
151
|
+
};
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
export default buildmyiqProctoringAssets;
|