From 7d44bce06f256cc618bd6f3035977c9d086c08c9 Mon Sep 17 00:00:00 2001 From: Harvey Zhao Date: Sun, 13 Sep 2026 17:36:11 +0800 Subject: [PATCH] test(danmuku-mask): [PKG-MASK-01] freeze published contracts and model provenance --- .../danmuku-mask-contract-validation.json | 56 ++ refactor/baselines/danmuku-mask-contract.md | 125 +++++ refactor/baselines/danmuku-mask-registry.json | 1 + refactor/baselines/danmuku-mask-release.json | 504 ++++++++++++++++++ .../2026-09-13-PKG-MASK-01-contract.md | 25 + refactor/plan.md | 7 +- refactor/progress.md | 10 + refactor/risk-table.md | 8 +- refactor/risks.json | 114 +++- refactor/scripts/danmuku-mask-contract.mjs | 107 ++++ .../scripts/danmuku-mask-contract.test.mjs | 170 ++++++ refactor/tasks.json | 12 +- refactor/third-party.json | 12 +- 13 files changed, 1138 insertions(+), 13 deletions(-) create mode 100644 refactor/baselines/danmuku-mask-contract-validation.json create mode 100644 refactor/baselines/danmuku-mask-contract.md create mode 100644 refactor/baselines/danmuku-mask-registry.json create mode 100644 refactor/baselines/danmuku-mask-release.json create mode 100644 refactor/changes/2026-09-13-PKG-MASK-01-contract.md create mode 100644 refactor/scripts/danmuku-mask-contract.mjs create mode 100644 refactor/scripts/danmuku-mask-contract.test.mjs diff --git a/refactor/baselines/danmuku-mask-contract-validation.json b/refactor/baselines/danmuku-mask-contract-validation.json new file mode 100644 index 000000000..25f3ad76e --- /dev/null +++ b/refactor/baselines/danmuku-mask-contract-validation.json @@ -0,0 +1,56 @@ +{ + "task": "PKG-MASK-01", + "sourceCommit": "b0cfbfe3a09a84a295a257e45e4577847588d1cb", + "node": "v24.21.0", + "releases": [ + { + "version": "1.1.0", + "sha256": "96682e46cb140a84b164818dd118480d7be42b24afaab390027a25e52e8ae31c", + "integrity": "sha512-VBmmwtNLj0TKYz9lWbyQ7A8osQHdyDiPMazP9g1UZbLyw/rMJ3qRclMgszRpZHN9hod0dQl2iYSJDoaqu3hGOQ==", + "members": 6, + "registryGitHead": "daf133b22630b4a0eecfa3336bbddab0e9119d96", + "sourceManifest": "1.0.1", + "coreAssociation": "5.3.1" + }, + { + "version": "1.0.0", + "sha256": "8a88f52ae843bed1be01a2e60b82cf886d33582f990a5016d49b1409c3493c4b", + "integrity": "sha512-x5Kd6nfPYyXVUoy4xNq7EQ8PeVGOiWVpIT75RVOrGLMjFrBKvAYp0btcjSu9ZZN7aAkB9MtAyFcTB6hbkOX8uw==", + "members": 6, + "registryGitHead": "b36f88db5f7fe0250285fd4f47229842231f402b", + "sourceManifest": "1.0.0", + "coreAssociation": "5.3.1-beta.1" + } + ], + "checks": { + "archiveVerifier": "passed", + "nodeTests": { + "passed": 6, + "failed": 0, + "skipped": 0 + }, + "actualExportProbes": 8, + "lint": "passed", + "diffCheck": "passed" + }, + "evidence": [ + { + "file": "refactor/.cache/danmuku-mask-contract-verify.log", + "sha256": "c36b2313c5d2fbabf6f7e6a4285ac905870f9764663a85df87693693e9648f73" + }, + { + "file": "refactor/.cache/danmuku-mask-contract-tests.log", + "sha256": "a7dc7994625711f0256b16663c49406d734e0c4b12c7cfa689468be5deaf41cd" + } + ], + "riskIds": [ + "MASK-LIFETIME-01", + "MASK-START-01", + "MASK-MODEL-01", + "MASK-BACKEND-01", + "MASK-DOM-01", + "SDK-08", + "MASK-NOTICE-01" + ], + "scope": "Published archive export probes without model startup; owned source uses controlled SDK stubs; installed adapter read-only and existing model assets hashed. No model download or inference, real browser or physical device, installation matrix or release acceptance." +} diff --git a/refactor/baselines/danmuku-mask-contract.md b/refactor/baselines/danmuku-mask-contract.md new file mode 100644 index 000000000..8634e8104 --- /dev/null +++ b/refactor/baselines/danmuku-mask-contract.md @@ -0,0 +1,125 @@ +# Danmuku Mask 历史契约与 SDK-08 证据 + +任务 PKG-MASK-01。依据 [真实归档](danmuku-mask-release.json) 与 +[原始 npm 响应](danmuku-mask-registry.json),捕获时全部稳定版为 1.0.0、1.1.0。 +冻结工作区提交为 `b0cfbfe3a09a84a295a257e45e4577847588d1cb`。 +源码文本以 LF 归一化;tarball、成员、已有模型资源均按原始字节计算哈希。 + +## 发布来源与公开形状 + +两版各有六个实际归档成员:README、manifest、main/legacy/module 三种产物和声明。 +四个 manifest 入口以及 root/legacy exports 目标全部存在。源码由 `.npmignore` +排除;不能声称 npm 包含源码。1.1.0 六个成员与冻结工作区对应文件完全一致。 + +| 项目 | npm 1.0.0 | npm 1.1.0 | +| --- | --- | --- | +| registry gitHead | `b36f88db5f7fe0250285fd4f47229842231f402b` | `daf133b22630b4a0eecfa3336bbddab0e9119d96` | +| gitHead 的插件 manifest | 1.0.0 | **1.0.1**,与发布版本不一致 | +| 同提交核心 manifest | 5.3.1-beta.1 | 5.3.1 | +| 实际 CommonJS main/legacy | 对象的 `.default` 工厂 | 直接工厂,无 `.default` | +| 实际 script main/legacy | 全局 `artplayerPluginDanmukuMask` 工厂 | 同名工厂 | +| module | `.mjs` 中 default export | `.mjs` 中 default export | +| 声明 | optional option、同步注册结果 | 与 1.0.0 字节一致 | + +核心版本仅是 Git 关联,不是已经通过的兼容矩阵。main/legacy 的 CommonJS 与 script +导出已用实际归档字节在隔离 VM 验证,包括不启动模型的注册/stop;module 这里只做 +成员与文本形状核实,尚未完成真实安装 ESM/NodeNext 消费。较早来源源码的额外 +`window.artplayerPluginDanmukuMask = ...` 已在冻结工作区移除;归档全局仍保留。 + +公开工厂 `artplayerPluginDanmukuMask(option?)` 同步返回 registrar;registrar 同步 +返回 `{ name: 'artplayerPluginDanmukuMask', start, stop }`。`start(): Promise`, +`stop(): void`,两版声明均如此。Option/Result 未作为命名导出发布。后续类型工作应 +保持最新完整工厂赋值和 NodeNext 旧模块形状,遵守统一类型政策;旧 CJS `.default` +调用是实际合法 JS,不能因为最新声明没有属性而删掉兼容路径。 + +## 源码参数与样式 + +参数在 registrar 调用时快照为内部 config,之后修改原 option 不会重读。默认值: + +| 字段 | 默认值 / 合并规则 | +| --- | --- | +| solutionPath | `https://cdn.jsdelivr.net/npm/@mediapipe/selfie_segmentation`,`||` | +| modelSelection | 1,`||`,传 0 仍为 1 | +| smoothSegmentation | true,仅 undefined 使用默认,false 保留 | +| minDetectionConfidence / minTrackingConfidence | 各 0.5,`||` | +| selfieMode / drawContour | 各 false,`||` | +| foregroundThreshold | 0.5,`||` | +| opacity | 1,`||` | +| maskBlurAmount | 3,`||` | + +这些是**插件传给适配器的值**,不是所有参数都对模型有效。源码始终指定 +`runtime: 'mediapipe'`、`modelType: 'general'`;先尝试 `tf.setBackend('webgl')`, +仅 rejection 时尝试 `cpu`。这不能证明 MediaPipe 自身推理切换成 CPU,也不能把 +`setBackend` fulfilled false 视为已执行 fallback。 + +registrar 捕获 `art.template.$video/$danmuku`,订阅 ready→start、destroy→stop。 +必须在 Danmuku 插件建立 `$danmuku` 后注册;demo 正是先 Danmuku 再 Mask。 +start 建立 2D canvas 并设置 `$danmuku.style`:maskMode=alpha、maskSize=contain、 +maskRepeat=no-repeat、backgroundSize=contain、backgroundRepeat=no-repeat。 +每轮以视频固有尺寸推理,白色前景/黑色背景生成 binary mask,drawMask 后将 RGB +三个分量均 **>250** 的像素 alpha 置 0,以 canvas PNG data URL 写入 maskImage。 +值等于 250 的边界不透明。stop 只写 maskImage=none 并取消所记住的一次 RAF; +其余样式不恢复。没有额外 public 事件或插件自建可见 DOM。 + +## SDK-08:锁文件、适配器和模型资源是不同来源 + +| 依赖 | manifest 范围 | 冻结 Yarn / 当前安装 | +| --- | --- | --- | +| @mediapipe/selfie_segmentation | ^0.1.1675465747 | 0.1.1675465747 | +| @tensorflow-models/body-segmentation | ^1.0.2 | 1.0.2 | +| @tensorflow/tfjs-backend-cpu | ^4.21.0 | 4.22.0 | +| @tensorflow/tfjs-backend-webgl | ^4.21.0 | 4.22.0 | +| @tensorflow/tfjs-converter | ^4.21.0 | 4.22.0 | +| @tensorflow/tfjs-core | ^4.21.0 | 4.22.0 | + +范围、resolved、integrity、已安装 manifest 哈希、适配器关键文件哈希均在 JSON。 +它们约束本仓库安装,**不约束消费者重新解析的依赖,也不约束默认 CDN URL**。 +没有查询 CDN latest、下载远程模型或从 bundle 中猜出完整历史 TensorFlow 版本。 +归档中出现 MediaPipe `VERSION` 字符串不等于整套 SDK/模型链已经认证。 + +body-segmentation 1.0.2 的 +`dist/selfie_segmentation_mediapipe/segmenter.js` 已按已安装字节冻结。它以 +`config.modelType` 映射 `general → modelSelection=0`、`landscape → 1`;不读取 +插件额外传入的 `config.modelSelection/smoothSegmentation/minDetectionConfidence/ +minTrackingConfidence/selfieMode`。构造时 selfieMode=false;segmentPeople 的 +flipHorizontal 参数另有处理,而插件未传这个参数。后续不能直接把模型参数接通 +当作等价重构;需先记录效果变化及兼容决策。 + +该 adapter 的 locateFile 去掉 solutionPath 尾部斜线再拼资源文件名。已安装 +MediaPipe JS 选择 SIMD/普通 wasm loader、graph binarypb、general/landscape tflite, +loader 再加载 WASM 等资源。插件默认目录未带 `@version`,Yarn 固定 JS 依赖不能 +固定这条额外下载链;自定义 solutionPath 必须继续保留。 + +demo 指向 `/assets/@mediapipe/selfie_segmentation`,注释要求从 node_modules 拷贝。 +仓库已存在 **12 个文件**,package.json 为 0.1.1675465747;冻结 Git 中全部文件与 +当前 node_modules 同名文件字节一致,包括两个 tflite、两个 wasm、loader JS、 +binarypb、data、README/声明/manifest。未新增或执行这些模型;这是本地字节一致性, +不是对其获取链的独立真实性认证,也不是模型输出、离线网络闭包或 GPU 释放验证。 + +## 有依据的后续风险 + +| ID | 已确认事实与最小触发路径 | 归属 / 证据等级 | +| --- | --- | --- | +| MASK-LIFETIME-01 | start 等待初始化或一次推理期间 stop/destroy 不使异步工作失效;后续 continuation 仍能写 mask 并再排 RAF。没有 segmenter.dispose 或事件 off | 02 复现、03 修复;源码路径确认,尚非原生模型复现 | +| MASK-START-01 | start 无 pending/running 守卫;并发启动可重复初始化,已初始化的多次 start 可建立多条 RAF 链,只有一个 animationFrameId | 02/03;源码路径确认 | +| MASK-MODEL-01 | 插件声明/传参和已安装 adapter 实际消费不同,general 始终选择 0;零值受历史 OR 默认影响 | 02/04/05;适配器文本确认,默认/零值有受控源码测试 | +| MASK-BACKEND-01 | WebGL 仅 rejection 时 fallback;MediaPipe runtime 与 TF backend 不等同;createSegmenter rejection 被吞掉后仍能进入等待 RAF | 02/03/05;源码路径确认,真实降级未验证 | +| MASK-DOM-01 | 缺少 Danmuku、2D context 或视频可读帧时无完整前置保护;CORS getImageData 失败被 catch 后下一轮继续 | 02/03/05;需受控失败及真实浏览器复现 | +| SDK-08 | 默认无版本 CDN 与 Yarn 安装分离;本地资源一致不证明真实模型、设备、跨源、断网与资源释放 | 05/06;保持开放 | +| MASK-NOTICE-01 | 当前 SDK manifest 标 Apache-2.0;两版 plugin 归档只有六个文件,无独立第三方通知,抽查 module bundle 无 Apache/Google copyright 字面标记,顶部只有项目 MIT | 06;需核查完整嵌入来源和通知义务,不作已获授权/合规结论 | + +## 重放与边界 + +使用固定 Node/Yarn 环境: + +```sh +node refactor/scripts/danmuku-mask-contract.mjs +node --test refactor/scripts/danmuku-mask-contract.test.mjs +``` + +verifier 校验缓存 tarball SHA512/SHA256、全部成员/入口、原始 registry 快照、Git +来源关系、冻结 Yarn 解析与已安装 SDK/已有资产字节。缓存缺失时只从固定 npm +tarball 地址恢复插件发布包。测试六项:归档关系、两版声明、八个真实 main/legacy +CJS/script 导出探针、受控自有源码默认/零值/像素边界、SDK-08 证据。 +导出探针执行 bundle 顶层注册但不启动模型;源码行为探针用 TF/MediaPipe 替身。 +没有远程模型请求、真实推理、设备、浏览器播放、npm 安装矩阵或发布验证。 diff --git a/refactor/baselines/danmuku-mask-registry.json b/refactor/baselines/danmuku-mask-registry.json new file mode 100644 index 000000000..ecf9d4fe9 --- /dev/null +++ b/refactor/baselines/danmuku-mask-registry.json @@ -0,0 +1 @@ +{"_id":"artplayer-plugin-danmuku-mask","_rev":"2-2b1cc3cc510e405aed54bd6ffbfeabb9","name":"artplayer-plugin-danmuku-mask","dist-tags":{"latest":"1.1.0"},"versions":{"1.0.0":{"name":"artplayer-plugin-danmuku-mask","version":"1.0.0","keywords":["html5","video","player"],"author":{"name":"Harvey Zack","email":"laozhaochaguan@gmail.com"},"license":"MIT","_id":"artplayer-plugin-danmuku-mask@1.0.0","maintainers":[{"name":"zhw2590582","email":"laozhaochaguan@gmail.com"}],"homepage":"https://artplayer.org","bugs":{"url":"https://github.com/zhw2590582/ArtPlayer/issues"},"dist":{"shasum":"b160cf47826f96556d22c7e41f60a7b5e4a878f0","tarball":"https://registry.npmjs.org/artplayer-plugin-danmuku-mask/-/artplayer-plugin-danmuku-mask-1.0.0.tgz","fileCount":6,"integrity":"sha512-x5Kd6nfPYyXVUoy4xNq7EQ8PeVGOiWVpIT75RVOrGLMjFrBKvAYp0btcjSu9ZZN7aAkB9MtAyFcTB6hbkOX8uw==","signatures":[{"sig":"MEQCIEwctUwtJoQITzQ2SGSDh7RVd9s2t/l4YB8DidHz+IFkAiBYw63HOgX0At7/Jq+uM1iQRMibtLKYwlV2k9RmIFYirA==","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"}],"unpackedSize":4285370},"main":"./dist/artplayer-plugin-danmuku-mask.js","types":"./types/artplayer-plugin-danmuku-mask.d.ts","legacy":"./dist/artplayer-plugin-danmuku-mask.legacy.js","module":"./dist/artplayer-plugin-danmuku-mask.mjs","exports":{".":{"types":"./types/artplayer-plugin-danmuku-mask.d.ts","import":"./dist/artplayer-plugin-danmuku-mask.mjs","require":"./dist/artplayer-plugin-danmuku-mask.js"},"./legacy":{"types":"./types/artplayer-plugin-danmuku-mask.d.ts","import":"./dist/artplayer-plugin-danmuku-mask.legacy.js","require":"./dist/artplayer-plugin-danmuku-mask.legacy.js"}},"gitHead":"b36f88db5f7fe0250285fd4f47229842231f402b","_npmUser":{"name":"zhw2590582","email":"laozhaochaguan@gmail.com"},"repository":{"url":"git+https://github.com/zhw2590582/ArtPlayer.git","type":"git"},"_npmVersion":"10.9.3","description":"Danmuku mask plugin for ArtPlayer","directories":{},"_nodeVersion":"22.18.0","browserslist":"last 1 Chrome version","dependencies":{"@tensorflow/tfjs-core":"^4.21.0","@tensorflow/tfjs-converter":"^4.21.0","@tensorflow/tfjs-backend-cpu":"^4.21.0","@mediapipe/selfie_segmentation":"^0.1.1675465747","@tensorflow/tfjs-backend-webgl":"^4.21.0","@tensorflow-models/body-segmentation":"^1.0.2"},"_hasShrinkwrap":false,"_npmOperationalInternal":{"tmp":"tmp/artplayer-plugin-danmuku-mask_1.0.0_1767516539621_0.37797476956956433","host":"s3://npm-registry-packages-npm-production"}},"1.1.0":{"name":"artplayer-plugin-danmuku-mask","version":"1.1.0","description":"Danmuku mask plugin for ArtPlayer","author":{"name":"Harvey Zhao","email":"laozhaochaguan@gmail.com"},"license":"MIT","homepage":"https://artplayer.org","repository":{"type":"git","url":"git+https://github.com/zhw2590582/ArtPlayer.git"},"bugs":{"url":"https://github.com/zhw2590582/ArtPlayer/issues"},"keywords":["html5","video","player"],"exports":{".":{"types":"./types/artplayer-plugin-danmuku-mask.d.ts","import":"./dist/artplayer-plugin-danmuku-mask.mjs","require":"./dist/artplayer-plugin-danmuku-mask.js"},"./legacy":{"types":"./types/artplayer-plugin-danmuku-mask.d.ts","import":"./dist/artplayer-plugin-danmuku-mask.legacy.js","require":"./dist/artplayer-plugin-danmuku-mask.legacy.js"}},"main":"./dist/artplayer-plugin-danmuku-mask.js","module":"./dist/artplayer-plugin-danmuku-mask.mjs","types":"./types/artplayer-plugin-danmuku-mask.d.ts","legacy":"./dist/artplayer-plugin-danmuku-mask.legacy.js","browserslist":"last 1 Chrome version","dependencies":{"@mediapipe/selfie_segmentation":"^0.1.1675465747","@tensorflow-models/body-segmentation":"^1.0.2","@tensorflow/tfjs-backend-cpu":"^4.21.0","@tensorflow/tfjs-backend-webgl":"^4.21.0","@tensorflow/tfjs-converter":"^4.21.0","@tensorflow/tfjs-core":"^4.21.0"},"gitHead":"daf133b22630b4a0eecfa3336bbddab0e9119d96","_id":"artplayer-plugin-danmuku-mask@1.1.0","_nodeVersion":"24.13.0","_npmVersion":"11.6.2","dist":{"integrity":"sha512-VBmmwtNLj0TKYz9lWbyQ7A8osQHdyDiPMazP9g1UZbLyw/rMJ3qRclMgszRpZHN9hod0dQl2iYSJDoaqu3hGOQ==","shasum":"9b1d1854fa3a2c45ba34e92c57bd0b5ec519cfa7","tarball":"https://registry.npmjs.org/artplayer-plugin-danmuku-mask/-/artplayer-plugin-danmuku-mask-1.1.0.tgz","fileCount":6,"unpackedSize":3542013,"signatures":[{"keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U","sig":"MEQCIFvUHXO+0eQ+bKsYVjrDkahzjWuVjiOyFMeFIHaGTT+OAiBkRcdVV1UvzIbNE/8g2kmpFOl8ESRfNUxreScPcnHwMA=="}]},"_npmUser":{"name":"zhw2590582","email":"laozhaochaguan@gmail.com"},"directories":{},"maintainers":[{"name":"zhw2590582","email":"laozhaochaguan@gmail.com"}],"_npmOperationalInternal":{"host":"s3://npm-registry-packages-npm-production","tmp":"tmp/artplayer-plugin-danmuku-mask_1.1.0_1773325276855_0.6942467109165913"},"_hasShrinkwrap":false}},"time":{"created":"2026-01-04T08:48:59.620Z","modified":"2026-03-12T14:21:17.145Z","1.0.0":"2026-01-04T08:48:59.809Z","1.1.0":"2026-03-12T14:21:17.027Z"},"bugs":{"url":"https://github.com/zhw2590582/ArtPlayer/issues"},"author":{"name":"Harvey Zhao","email":"laozhaochaguan@gmail.com"},"license":"MIT","homepage":"https://artplayer.org","keywords":["html5","video","player"],"repository":{"type":"git","url":"git+https://github.com/zhw2590582/ArtPlayer.git"},"description":"Danmuku mask plugin for ArtPlayer","maintainers":[{"name":"zhw2590582","email":"laozhaochaguan@gmail.com"}],"readme":"# artplayer-plugin-danmuku-mask\n\nDanmuku mask plugin for ArtPlayer\n\n## Demo\n\n[https://artplayer.org](https://artplayer.org/?libs=./uncompiled/artplayer-plugin-danmuku/index.js%0A./uncompiled/artplayer-plugin-danmuku-mask/index.js&example=danmuku.mask)\n\n## License\n\nMIT © Harvey Zhao\n","readmeFilename":"README.md"} \ No newline at end of file diff --git a/refactor/baselines/danmuku-mask-release.json b/refactor/baselines/danmuku-mask-release.json new file mode 100644 index 000000000..4d2aa1705 --- /dev/null +++ b/refactor/baselines/danmuku-mask-release.json @@ -0,0 +1,504 @@ +{ + "schemaVersion": 1, + "registry": "https://registry.npmjs.org/artplayer-plugin-danmuku-mask", + "capturedAt": "2026-09-13T09:18:03.395Z", + "sourceCommit": "b0cfbfe3a09a84a295a257e45e4577847588d1cb", + "sourceNormalization": "Git source text normalized to LF; archive and binary asset members hashed as bytes.", + "source": { + "packages/artplayer-plugin-danmuku-mask/.npmignore": "46063058e98be55dba5cf5f33d379e73eca048f4126e7af75f8011bab7f09145", + "packages/artplayer-plugin-danmuku-mask/README.md": "31ecede166379454dfd030c73b1e83cfcffd2d7e3361abe624838f3db7314cae", + "packages/artplayer-plugin-danmuku-mask/dist/artplayer-plugin-danmuku-mask.js": "07cebbfbf77f8ec4b2df6a77d7d7300345b86f2993e220ff14e1aae530e4fb31", + "packages/artplayer-plugin-danmuku-mask/dist/artplayer-plugin-danmuku-mask.legacy.js": "a678a3c0f6d3920bee3b66af9943b3612935028bff61c96677b8eaf8ef7cc8f0", + "packages/artplayer-plugin-danmuku-mask/dist/artplayer-plugin-danmuku-mask.mjs": "48f7a449dc9555ab4060a578902b5431701f81b2f093a7cb0526042a8c417400", + "packages/artplayer-plugin-danmuku-mask/package.json": "b216164ada1b9baf4fcec3388036839c28686f864e3d2a706258af78e6633887", + "packages/artplayer-plugin-danmuku-mask/src/index.js": "2397900688f86bc74bc86a4e870f69478a651bd8df02bba543cdab832773fd6e", + "packages/artplayer-plugin-danmuku-mask/types/artplayer-plugin-danmuku-mask.d.ts": "bbb270d73fc791488ac306f55e8c78990d0d9eef0a9d70cdf6093dadd7dff4b2", + "docs/assets/example/danmuku.mask.js": "c0fe4f2d4cd60738583c4bfeebc24bc3365d8ca382e6f4ff86e1056f5a2729a8" + }, + "registrySnapshot": { + "status": 200, + "bodySha256": "b4caa94b8179a6c6e6ff8f96ed7ec09a615df48afadd2cad66409cf176395820", + "distTags": { + "latest": "1.1.0" + }, + "stableVersions": [ + "1.0.0", + "1.1.0" + ], + "prereleaseVersions": [], + "scope": "All stable versions listed in the live registry snapshot; registry signatures not independently verified." + }, + "release": { + "name": "artplayer-plugin-danmuku-mask", + "version": "1.1.0", + "tarball": "https://registry.npmjs.org/artplayer-plugin-danmuku-mask/-/artplayer-plugin-danmuku-mask-1.1.0.tgz", + "integrity": "sha512-VBmmwtNLj0TKYz9lWbyQ7A8osQHdyDiPMazP9g1UZbLyw/rMJ3qRclMgszRpZHN9hod0dQl2iYSJDoaqu3hGOQ==", + "sha256": "96682e46cb140a84b164818dd118480d7be42b24afaab390027a25e52e8ae31c", + "registryGitHead": "daf133b22630b4a0eecfa3336bbddab0e9119d96", + "publishedAt": "2026-03-12T14:21:17.027Z", + "files": { + "package/README.md": "31ecede166379454dfd030c73b1e83cfcffd2d7e3361abe624838f3db7314cae", + "package/dist/artplayer-plugin-danmuku-mask.js": "07cebbfbf77f8ec4b2df6a77d7d7300345b86f2993e220ff14e1aae530e4fb31", + "package/dist/artplayer-plugin-danmuku-mask.legacy.js": "a678a3c0f6d3920bee3b66af9943b3612935028bff61c96677b8eaf8ef7cc8f0", + "package/dist/artplayer-plugin-danmuku-mask.mjs": "48f7a449dc9555ab4060a578902b5431701f81b2f093a7cb0526042a8c417400", + "package/package.json": "b216164ada1b9baf4fcec3388036839c28686f864e3d2a706258af78e6633887", + "package/types/artplayer-plugin-danmuku-mask.d.ts": "bbb270d73fc791488ac306f55e8c78990d0d9eef0a9d70cdf6093dadd7dff4b2" + }, + "manifest": { + "name": "artplayer-plugin-danmuku-mask", + "version": "1.1.0", + "description": "Danmuku mask plugin for ArtPlayer", + "author": "Harvey Zhao ", + "license": "MIT", + "homepage": "https://artplayer.org", + "repository": { + "type": "git", + "url": "git+https://github.com/zhw2590582/ArtPlayer.git" + }, + "bugs": { + "url": "https://github.com/zhw2590582/ArtPlayer/issues" + }, + "keywords": [ + "html5", + "video", + "player" + ], + "exports": { + ".": { + "types": "./types/artplayer-plugin-danmuku-mask.d.ts", + "import": "./dist/artplayer-plugin-danmuku-mask.mjs", + "require": "./dist/artplayer-plugin-danmuku-mask.js" + }, + "./legacy": { + "types": "./types/artplayer-plugin-danmuku-mask.d.ts", + "import": "./dist/artplayer-plugin-danmuku-mask.legacy.js", + "require": "./dist/artplayer-plugin-danmuku-mask.legacy.js" + } + }, + "main": "./dist/artplayer-plugin-danmuku-mask.js", + "module": "./dist/artplayer-plugin-danmuku-mask.mjs", + "types": "./types/artplayer-plugin-danmuku-mask.d.ts", + "legacy": "./dist/artplayer-plugin-danmuku-mask.legacy.js", + "browserslist": "last 1 Chrome version", + "dependencies": { + "@mediapipe/selfie_segmentation": "^0.1.1675465747", + "@tensorflow-models/body-segmentation": "^1.0.2", + "@tensorflow/tfjs-backend-cpu": "^4.21.0", + "@tensorflow/tfjs-backend-webgl": "^4.21.0", + "@tensorflow/tfjs-converter": "^4.21.0", + "@tensorflow/tfjs-core": "^4.21.0" + } + }, + "entrypoints": { + "main": "./dist/artplayer-plugin-danmuku-mask.js", + "module": "./dist/artplayer-plugin-danmuku-mask.mjs", + "types": "./types/artplayer-plugin-danmuku-mask.d.ts", + "legacy": "./dist/artplayer-plugin-danmuku-mask.legacy.js" + }, + "missingEntrypoints": [], + "registrySourceManifest": { + "commit": "daf133b22630b4a0eecfa3336bbddab0e9119d96", + "file": "packages/artplayer-plugin-danmuku-mask/package.json", + "version": "1.0.1", + "sha256LF": "320f1762d2e062089d11e013fa2199dae36003e3500252900e47c593d16af265", + "versionMatchesRelease": false + }, + "historicalCore": { + "commit": "daf133b22630b4a0eecfa3336bbddab0e9119d96", + "file": "packages/artplayer/package.json", + "version": "5.3.1", + "sha256LF": "862f3697876fe915dd0ada178da766ce823aae7563ed30b3e5b11b811202929c" + }, + "registrySourceComparison": [ + { + "file": "packages/artplayer-plugin-danmuku-mask/.npmignore", + "sha256LF": "46063058e98be55dba5cf5f33d379e73eca048f4126e7af75f8011bab7f09145", + "matchesArchiveBytes": false + }, + { + "file": "packages/artplayer-plugin-danmuku-mask/README.md", + "sha256LF": "31ecede166379454dfd030c73b1e83cfcffd2d7e3361abe624838f3db7314cae", + "matchesArchiveBytes": true + }, + { + "file": "packages/artplayer-plugin-danmuku-mask/dist/artplayer-plugin-danmuku-mask.js", + "sha256LF": "9e4f16458fe4887d5850591ee6a8ed07b13ce819dd2d30d23610548772de5f63", + "matchesArchiveBytes": false + }, + { + "file": "packages/artplayer-plugin-danmuku-mask/dist/artplayer-plugin-danmuku-mask.legacy.js", + "sha256LF": "871af38b439e1e49fad2bdc8e4f25a25bc9335ad023dee9ecfb66ddd326b81cb", + "matchesArchiveBytes": false + }, + { + "file": "packages/artplayer-plugin-danmuku-mask/dist/artplayer-plugin-danmuku-mask.mjs", + "sha256LF": "b0669d5348fc21e3b1d6ec7d0baa95d822e27d79ee555c751ca7ab71e2b3ecb8", + "matchesArchiveBytes": false + }, + { + "file": "packages/artplayer-plugin-danmuku-mask/package.json", + "sha256LF": "320f1762d2e062089d11e013fa2199dae36003e3500252900e47c593d16af265", + "matchesArchiveBytes": false + }, + { + "file": "packages/artplayer-plugin-danmuku-mask/src/index.js", + "sha256LF": "2397900688f86bc74bc86a4e870f69478a651bd8df02bba543cdab832773fd6e", + "matchesArchiveBytes": false + }, + { + "file": "packages/artplayer-plugin-danmuku-mask/types/artplayer-plugin-danmuku-mask.d.ts", + "sha256LF": "bbb270d73fc791488ac306f55e8c78990d0d9eef0a9d70cdf6093dadd7dff4b2", + "matchesArchiveBytes": true + } + ] + }, + "previous": [ + { + "name": "artplayer-plugin-danmuku-mask", + "version": "1.0.0", + "tarball": "https://registry.npmjs.org/artplayer-plugin-danmuku-mask/-/artplayer-plugin-danmuku-mask-1.0.0.tgz", + "integrity": "sha512-x5Kd6nfPYyXVUoy4xNq7EQ8PeVGOiWVpIT75RVOrGLMjFrBKvAYp0btcjSu9ZZN7aAkB9MtAyFcTB6hbkOX8uw==", + "sha256": "8a88f52ae843bed1be01a2e60b82cf886d33582f990a5016d49b1409c3493c4b", + "registryGitHead": "b36f88db5f7fe0250285fd4f47229842231f402b", + "publishedAt": "2026-01-04T08:48:59.809Z", + "files": { + "package/README.md": "8c042edec31eb22fb6022bc5a94f3b896a877653141cc0a7819d853d4ae3add4", + "package/dist/artplayer-plugin-danmuku-mask.js": "940cc240fcdb208427cb25d54180f9b84e0bfa96b5a4d3ba68fa9fb2ad8ffa16", + "package/dist/artplayer-plugin-danmuku-mask.legacy.js": "49da39c368e036f894e623277ce837efa9b7524a040d8ae01e2245b3459c3c9f", + "package/dist/artplayer-plugin-danmuku-mask.mjs": "92ae895c45c3dd39110c3513eb5cb6d99b9249d96b37b7fd1a6437e97febdc8f", + "package/package.json": "c24d66328eff31d93789a24e6e198bc92126f62ce4dd6c8929f64e4458b3b02a", + "package/types/artplayer-plugin-danmuku-mask.d.ts": "bbb270d73fc791488ac306f55e8c78990d0d9eef0a9d70cdf6093dadd7dff4b2" + }, + "manifest": { + "name": "artplayer-plugin-danmuku-mask", + "version": "1.0.0", + "description": "Danmuku mask plugin for ArtPlayer", + "author": "Harvey Zack ", + "license": "MIT", + "homepage": "https://artplayer.org", + "repository": { + "type": "git", + "url": "git+https://github.com/zhw2590582/ArtPlayer.git" + }, + "bugs": { + "url": "https://github.com/zhw2590582/ArtPlayer/issues" + }, + "keywords": [ + "html5", + "video", + "player" + ], + "exports": { + ".": { + "types": "./types/artplayer-plugin-danmuku-mask.d.ts", + "import": "./dist/artplayer-plugin-danmuku-mask.mjs", + "require": "./dist/artplayer-plugin-danmuku-mask.js" + }, + "./legacy": { + "types": "./types/artplayer-plugin-danmuku-mask.d.ts", + "import": "./dist/artplayer-plugin-danmuku-mask.legacy.js", + "require": "./dist/artplayer-plugin-danmuku-mask.legacy.js" + } + }, + "main": "./dist/artplayer-plugin-danmuku-mask.js", + "module": "./dist/artplayer-plugin-danmuku-mask.mjs", + "types": "./types/artplayer-plugin-danmuku-mask.d.ts", + "legacy": "./dist/artplayer-plugin-danmuku-mask.legacy.js", + "browserslist": "last 1 Chrome version", + "dependencies": { + "@mediapipe/selfie_segmentation": "^0.1.1675465747", + "@tensorflow-models/body-segmentation": "^1.0.2", + "@tensorflow/tfjs-backend-cpu": "^4.21.0", + "@tensorflow/tfjs-backend-webgl": "^4.21.0", + "@tensorflow/tfjs-converter": "^4.21.0", + "@tensorflow/tfjs-core": "^4.21.0" + } + }, + "entrypoints": { + "main": "./dist/artplayer-plugin-danmuku-mask.js", + "module": "./dist/artplayer-plugin-danmuku-mask.mjs", + "types": "./types/artplayer-plugin-danmuku-mask.d.ts", + "legacy": "./dist/artplayer-plugin-danmuku-mask.legacy.js" + }, + "missingEntrypoints": [], + "registrySourceManifest": { + "commit": "b36f88db5f7fe0250285fd4f47229842231f402b", + "file": "packages/artplayer-plugin-danmuku-mask/package.json", + "version": "1.0.0", + "sha256LF": "c24d66328eff31d93789a24e6e198bc92126f62ce4dd6c8929f64e4458b3b02a", + "versionMatchesRelease": true + }, + "historicalCore": { + "commit": "b36f88db5f7fe0250285fd4f47229842231f402b", + "file": "packages/artplayer/package.json", + "version": "5.3.1-beta.1", + "sha256LF": "e1a0a390ec10cd8c1c12006ac6515414ae98a964300dd3e9c288377def948497" + }, + "registrySourceComparison": [ + { + "file": "packages/artplayer-plugin-danmuku-mask/.npmignore", + "sha256LF": "19c9675ae41ddf5428d894c9edebf1800ffea8f9be5b7d77c01a3635116cd9e8", + "matchesArchiveBytes": false + }, + { + "file": "packages/artplayer-plugin-danmuku-mask/README.md", + "sha256LF": "8c042edec31eb22fb6022bc5a94f3b896a877653141cc0a7819d853d4ae3add4", + "matchesArchiveBytes": true + }, + { + "file": "packages/artplayer-plugin-danmuku-mask/dist/artplayer-plugin-danmuku-mask.js", + "sha256LF": "940cc240fcdb208427cb25d54180f9b84e0bfa96b5a4d3ba68fa9fb2ad8ffa16", + "matchesArchiveBytes": true + }, + { + "file": "packages/artplayer-plugin-danmuku-mask/dist/artplayer-plugin-danmuku-mask.legacy.js", + "sha256LF": "49da39c368e036f894e623277ce837efa9b7524a040d8ae01e2245b3459c3c9f", + "matchesArchiveBytes": true + }, + { + "file": "packages/artplayer-plugin-danmuku-mask/dist/artplayer-plugin-danmuku-mask.mjs", + "sha256LF": "92ae895c45c3dd39110c3513eb5cb6d99b9249d96b37b7fd1a6437e97febdc8f", + "matchesArchiveBytes": true + }, + { + "file": "packages/artplayer-plugin-danmuku-mask/package.json", + "sha256LF": "c24d66328eff31d93789a24e6e198bc92126f62ce4dd6c8929f64e4458b3b02a", + "matchesArchiveBytes": true + }, + { + "file": "packages/artplayer-plugin-danmuku-mask/src/index.js", + "sha256LF": "105a6ffa4476cc0af680d1ef90e9095b17f73e33ffe5e4b70f245c1c095c196a", + "matchesArchiveBytes": false + }, + { + "file": "packages/artplayer-plugin-danmuku-mask/types/artplayer-plugin-danmuku-mask.d.ts", + "sha256LF": "bbb270d73fc791488ac306f55e8c78990d0d9eef0a9d70cdf6093dadd7dff4b2", + "matchesArchiveBytes": true + } + ] + } + ], + "sdk": { + "lock": { + "file": "yarn.lock", + "commit": "b0cfbfe3a09a84a295a257e45e4577847588d1cb", + "sha256LF": "6dfb6c5b0154bcd3adc279e12c4393db66a6ec52274002adf2d5ab93caa05d4b" + }, + "dependencies": [ + { + "name": "@mediapipe/selfie_segmentation", + "range": "^0.1.1675465747", + "key": "@mediapipe/selfie_segmentation@^0.1.1675465747", + "version": "0.1.1675465747", + "resolved": "https://registry.npmjs.org/@mediapipe/selfie_segmentation/-/selfie_segmentation-0.1.1675465747.tgz", + "integrity": "sha512-IxYxNhwE5VwOm52L1yoFWYLP7q9Pd+NJjzOC5tlepfvEGaY3o9hslhUrx9BgseqdfZtKSDtd/4NfCSMjNzQalA==", + "installedManifest": { + "file": "node_modules/@mediapipe/selfie_segmentation/package.json", + "sha256": "b514e7146be2fbceee0a4a6e8a3895d2df36e552338aad8045a38a12c1c8eff8", + "license": "Apache-2.0", + "peerDependencies": {} + } + }, + { + "name": "@tensorflow-models/body-segmentation", + "range": "^1.0.2", + "key": "@tensorflow-models/body-segmentation@^1.0.2", + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@tensorflow-models/body-segmentation/-/body-segmentation-1.0.2.tgz", + "integrity": "sha512-sbPiL8wpqfKqh01of6qguZzU9yLWOQDTwiPEIHFPA/EAjz5T51LKHKIySll+mmteRo/TxNED8pxd9VJ6q2r7kg==", + "installedManifest": { + "file": "node_modules/@tensorflow-models/body-segmentation/package.json", + "sha256": "53d2b6b38be7d4d4ad44fce80d7d9bc58cf4a666b2791d846324e369d011b3dd", + "license": "Apache-2.0", + "peerDependencies": { + "@mediapipe/selfie_segmentation": "~0.1.0", + "@tensorflow/tfjs-backend-webgl": "^4.9.0", + "@tensorflow/tfjs-converter": "^4.9.0", + "@tensorflow/tfjs-core": "^4.9.0" + } + } + }, + { + "name": "@tensorflow/tfjs-backend-cpu", + "range": "^4.21.0", + "key": "@tensorflow/tfjs-backend-cpu@^4.21.0", + "version": "4.22.0", + "resolved": "https://registry.npmjs.org/@tensorflow/tfjs-backend-cpu/-/tfjs-backend-cpu-4.22.0.tgz", + "integrity": "sha512-1u0FmuLGuRAi8D2c3cocHTASGXOmHc/4OvoVDENJayjYkS119fcTcQf4iHrtLthWyDIPy3JiPhRrZQC9EwnhLw==", + "installedManifest": { + "file": "node_modules/@tensorflow/tfjs-backend-cpu/package.json", + "sha256": "9400f1a4ea5f144646b62c8b393f54ed53b3446a2827321ba6b462ac220f0859", + "license": "Apache-2.0", + "peerDependencies": { + "@tensorflow/tfjs-core": "4.22.0" + } + } + }, + { + "name": "@tensorflow/tfjs-backend-webgl", + "range": "^4.21.0", + "key": "@tensorflow/tfjs-backend-webgl@^4.21.0", + "version": "4.22.0", + "resolved": "https://registry.npmjs.org/@tensorflow/tfjs-backend-webgl/-/tfjs-backend-webgl-4.22.0.tgz", + "integrity": "sha512-H535XtZWnWgNwSzv538czjVlbJebDl5QTMOth4RXr2p/kJ1qSIXE0vZvEtO+5EC9b00SvhplECny2yDewQb/Yg==", + "installedManifest": { + "file": "node_modules/@tensorflow/tfjs-backend-webgl/package.json", + "sha256": "286282c479b83d0b00d71d4ec40071c76244ad6da85441e0efc7a1b51456bf2f", + "license": "Apache-2.0", + "peerDependencies": { + "@tensorflow/tfjs-core": "4.22.0" + } + } + }, + { + "name": "@tensorflow/tfjs-converter", + "range": "^4.21.0", + "key": "@tensorflow/tfjs-converter@^4.21.0", + "version": "4.22.0", + "resolved": "https://registry.npmjs.org/@tensorflow/tfjs-converter/-/tfjs-converter-4.22.0.tgz", + "integrity": "sha512-PT43MGlnzIo+YfbsjM79Lxk9lOq6uUwZuCc8rrp0hfpLjF6Jv8jS84u2jFb+WpUeuF4K33ZDNx8CjiYrGQ2trQ==", + "installedManifest": { + "file": "node_modules/@tensorflow/tfjs-converter/package.json", + "sha256": "b6a4f89058a5f4a5fdfb7fe0fe4cf3181023bdedcc80a374e0349c002ab068b5", + "license": "Apache-2.0", + "peerDependencies": { + "@tensorflow/tfjs-core": "4.22.0" + } + } + }, + { + "name": "@tensorflow/tfjs-core", + "range": "^4.21.0", + "key": "@tensorflow/tfjs-core@^4.21.0", + "version": "4.22.0", + "resolved": "https://registry.npmjs.org/@tensorflow/tfjs-core/-/tfjs-core-4.22.0.tgz", + "integrity": "sha512-LEkOyzbknKFoWUwfkr59vSB68DMJ4cjwwHgicXN0DUi3a0Vh1Er3JQqCI1Hl86GGZQvY8ezVrtDIvqR1ZFW55A==", + "installedManifest": { + "file": "node_modules/@tensorflow/tfjs-core/package.json", + "sha256": "d4b3c3f337f7ea6de582ac1320b7521abe970a9b11f4d486e6929b29ccdbd283", + "license": "Apache-2.0", + "peerDependencies": {} + } + } + ], + "adapterFiles": { + "node_modules/@tensorflow-models/body-segmentation/dist/selfie_segmentation_mediapipe/segmenter.js": "a8d076647e9ba9a787fdf354e48de6ecf62d4d3f74c10505dc58ef975b63bb67", + "node_modules/@tensorflow-models/body-segmentation/dist/selfie_segmentation_mediapipe/types.d.ts": "c259e6d1f55c9d69ba61eaa8eb884461e30ef93557123a01172c9e8380576603", + "node_modules/@tensorflow-models/body-segmentation/dist/selfie_segmentation_mediapipe/segmenter_utils.js": "a6646102407f3ea0bd87e48d6a7c0c6d8fc64a179b7990deafac4b0ba3e4a413", + "node_modules/@tensorflow-models/body-segmentation/dist/selfie_segmentation_mediapipe/constants.js": "27b0ffba42da166433af40ab1d4f2a5eccc97edf416c5d2e2ec4a665a18e9f5d", + "node_modules/@tensorflow-models/body-segmentation/dist/create_segmenter.js": "8f1f9f9bb4d53ffb411e41123298d2053bd344955cf51bdec3eee39898e06ced", + "node_modules/@mediapipe/selfie_segmentation/selfie_segmentation.js": "a4720d2d2374ec9df3510461ff186a4af427fb5bd402375e135875a991467da3" + }, + "assets": [ + { + "file": "docs/assets/@mediapipe/selfie_segmentation/README.md", + "bytes": 379, + "sha256": "9dad28188ae54ea420e3d29c23360b040a0dfb88954b99a3e456b8a8c93cf2a1", + "installedFile": "node_modules/@mediapipe/selfie_segmentation/README.md", + "installedSha256": "9dad28188ae54ea420e3d29c23360b040a0dfb88954b99a3e456b8a8c93cf2a1", + "matchesInstalled": true + }, + { + "file": "docs/assets/@mediapipe/selfie_segmentation/index.d.ts", + "bytes": 2772, + "sha256": "46152cccb63a9249503f7b8f49dbc3cfbd2a2994eda2c9f0db32a2998e1879a9", + "installedFile": "node_modules/@mediapipe/selfie_segmentation/index.d.ts", + "installedSha256": "46152cccb63a9249503f7b8f49dbc3cfbd2a2994eda2c9f0db32a2998e1879a9", + "matchesInstalled": true + }, + { + "file": "docs/assets/@mediapipe/selfie_segmentation/package.json", + "bytes": 734, + "sha256": "b514e7146be2fbceee0a4a6e8a3895d2df36e552338aad8045a38a12c1c8eff8", + "installedFile": "node_modules/@mediapipe/selfie_segmentation/package.json", + "installedSha256": "b514e7146be2fbceee0a4a6e8a3895d2df36e552338aad8045a38a12c1c8eff8", + "matchesInstalled": true + }, + { + "file": "docs/assets/@mediapipe/selfie_segmentation/selfie_segmentation.binarypb", + "bytes": 362, + "sha256": "0007be42c754f111063c0121bba7104880b7c56277d3a9681cdcb17709236aee", + "installedFile": "node_modules/@mediapipe/selfie_segmentation/selfie_segmentation.binarypb", + "installedSha256": "0007be42c754f111063c0121bba7104880b7c56277d3a9681cdcb17709236aee", + "matchesInstalled": true + }, + { + "file": "docs/assets/@mediapipe/selfie_segmentation/selfie_segmentation.js", + "bytes": 44542, + "sha256": "a4720d2d2374ec9df3510461ff186a4af427fb5bd402375e135875a991467da3", + "installedFile": "node_modules/@mediapipe/selfie_segmentation/selfie_segmentation.js", + "installedSha256": "a4720d2d2374ec9df3510461ff186a4af427fb5bd402375e135875a991467da3", + "matchesInstalled": true + }, + { + "file": "docs/assets/@mediapipe/selfie_segmentation/selfie_segmentation.tflite", + "bytes": 249024, + "sha256": "8d13b7fae74af625c641226813616a2117bd6bca19eb3b75574621fc08557f27", + "installedFile": "node_modules/@mediapipe/selfie_segmentation/selfie_segmentation.tflite", + "installedSha256": "8d13b7fae74af625c641226813616a2117bd6bca19eb3b75574621fc08557f27", + "matchesInstalled": true + }, + { + "file": "docs/assets/@mediapipe/selfie_segmentation/selfie_segmentation_landscape.tflite", + "bytes": 249792, + "sha256": "4aafe6223bb8dac6fac8ca8ed56852870a33051ef3f6238822d282a109962894", + "installedFile": "node_modules/@mediapipe/selfie_segmentation/selfie_segmentation_landscape.tflite", + "installedSha256": "4aafe6223bb8dac6fac8ca8ed56852870a33051ef3f6238822d282a109962894", + "matchesInstalled": true + }, + { + "file": "docs/assets/@mediapipe/selfie_segmentation/selfie_segmentation_solution_simd_wasm_bin.data", + "bytes": 0, + "sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", + "installedFile": "node_modules/@mediapipe/selfie_segmentation/selfie_segmentation_solution_simd_wasm_bin.data", + "installedSha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", + "matchesInstalled": true + }, + { + "file": "docs/assets/@mediapipe/selfie_segmentation/selfie_segmentation_solution_simd_wasm_bin.js", + "bytes": 276493, + "sha256": "9406e7e30843bdca189689d4bea413f3e468f71e381560e583ef12792d26d46a", + "installedFile": "node_modules/@mediapipe/selfie_segmentation/selfie_segmentation_solution_simd_wasm_bin.js", + "installedSha256": "9406e7e30843bdca189689d4bea413f3e468f71e381560e583ef12792d26d46a", + "matchesInstalled": true + }, + { + "file": "docs/assets/@mediapipe/selfie_segmentation/selfie_segmentation_solution_simd_wasm_bin.wasm", + "bytes": 5694839, + "sha256": "d93e73677ef1fe791ff5407083c0f9ced6d6050a705bd6c20823fc05919d8dc4", + "installedFile": "node_modules/@mediapipe/selfie_segmentation/selfie_segmentation_solution_simd_wasm_bin.wasm", + "installedSha256": "d93e73677ef1fe791ff5407083c0f9ced6d6050a705bd6c20823fc05919d8dc4", + "matchesInstalled": true + }, + { + "file": "docs/assets/@mediapipe/selfie_segmentation/selfie_segmentation_solution_wasm_bin.js", + "bytes": 276488, + "sha256": "ed59858c14c9db26d34abbf0afa9017e5c6583a6ad63dae63d2fe42e19abfea5", + "installedFile": "node_modules/@mediapipe/selfie_segmentation/selfie_segmentation_solution_wasm_bin.js", + "installedSha256": "ed59858c14c9db26d34abbf0afa9017e5c6583a6ad63dae63d2fe42e19abfea5", + "matchesInstalled": true + }, + { + "file": "docs/assets/@mediapipe/selfie_segmentation/selfie_segmentation_solution_wasm_bin.wasm", + "bytes": 5587523, + "sha256": "37665276fd8e38589a28b07ddec805266efb1b7a1a52fd36667ef11dff43a847", + "installedFile": "node_modules/@mediapipe/selfie_segmentation/selfie_segmentation_solution_wasm_bin.wasm", + "installedSha256": "37665276fd8e38589a28b07ddec805266efb1b7a1a52fd36667ef11dff43a847", + "matchesInstalled": true + } + ], + "defaultSolutionPath": "https://cdn.jsdelivr.net/npm/@mediapipe/selfie_segmentation", + "demoSolutionPath": "/assets/@mediapipe/selfie_segmentation", + "scope": "Read-only installed SDK source and existing local assets. No SDK/model execution, remote model requests, GPU or device validation. Installed assets are compared with frozen Git bytes; not independently authenticated against a newly downloaded SDK archive." + }, + "observations": { + "latestArchiveMatchesFrozenSourceFiles": [ + "packages/artplayer-plugin-danmuku-mask/README.md", + "packages/artplayer-plugin-danmuku-mask/dist/artplayer-plugin-danmuku-mask.js", + "packages/artplayer-plugin-danmuku-mask/dist/artplayer-plugin-danmuku-mask.legacy.js", + "packages/artplayer-plugin-danmuku-mask/dist/artplayer-plugin-danmuku-mask.mjs", + "packages/artplayer-plugin-danmuku-mask/package.json", + "packages/artplayer-plugin-danmuku-mask/types/artplayer-plugin-danmuku-mask.d.ts" + ] + } +} diff --git a/refactor/changes/2026-09-13-PKG-MASK-01-contract.md b/refactor/changes/2026-09-13-PKG-MASK-01-contract.md new file mode 100644 index 000000000..e069515a7 --- /dev/null +++ b/refactor/changes/2026-09-13-PKG-MASK-01-contract.md @@ -0,0 +1,25 @@ +# PKG-MASK-01 Danmuku Mask 契约基线 + +冻结提交 `b0cfbfe3a09a84a295a257e45e4577847588d1cb`,只新增契约与验证文件, +未改生产、依赖、root 脚本、生成产物或 demo。没有推送或发布。 + +实际 registry 全部稳定版本 1.0.0/1.1.0 的归档、SHA512/SHA256、完整六成员及 +四种入口已冻结。1.1.0 六发布文件与冻结工作区相同;registry gitHead 的插件 +manifest 却是 1.0.1,显式保留此差异。两版声明完全相同,真实 CJS main/legacy +分别是旧对象.default、新直接工厂;script 全局仍为同名工厂。 + +SDK-08 记录六个 Yarn 解析版本、已安装 adapter 字节,以及 demo 12 个既有资源与 +已安装 MediaPipe 0.1.1675465747 的字节一致性。默认 solutionPath 仍是无版本 CDN, +没有被锁文件固定。已安装 adapter 以固定 general 选择模型 0,不消费插件额外传入 +的若干模型选项;因此不能将声明可传参数解释为已验证效果。 + +记录 start/stop 异步过期、多重 RAF、backend fallback、DOM/CORS、模型参数和第三方 +通知后续问题。详见 [契约](../baselines/danmuku-mask-contract.md) 和 +[完整来源](../baselines/danmuku-mask-release.json)。这些问题未在本任务修复,也未被 +笼统认定必须保留;由 02 复现、03/04 实施、05/06 做真实模型与分发验收。 + +验证:固定 Node 24.21.0,独立 verifier 通过;6 项 node:test 全部通过、无跳过。 +专属脚本 ESLint 通过。导出探针使用实际 bundle;自有源码默认值/零值/像素探针 +使用受控 SDK 替身。没有新增下载/执行模型、浏览器服务器或真实 GPU/设备验证。 +父代理复核六项测试通过,整合任务/风险/SDK-08索引并创建独立本地提交。 +新增测试由现有test:baseline自动收集;两个直接重放命令见契约文档,无需重复脚本别名。 diff --git a/refactor/plan.md b/refactor/plan.md index 723f030e8..2d5e70935 100644 --- a/refactor/plan.md +++ b/refactor/plan.md @@ -4,7 +4,7 @@ 基线:`40fcda6a37d0049d42e49c1e64e70d4fd9ba5f7f`。总任务 228 项,范围 22 个包及工作区/示例。 -状态:todo 75 / doing 16 / blocked 0 / done 137 / deferred 0。风险 L/M/H 表示兼容风险,不表示工期。 +状态:todo 74 / doing 16 / blocked 0 / done 138 / deferred 0。风险 L/M/H 表示兼容风险,不表示工期。 前置依赖是启动条件;验收是完成条件。任务可以继续拆分,但不能复用或悄悄删除旧 ID。 @@ -240,8 +240,8 @@ | ID | 范围 / 步骤 | 前置依赖 | 交付物 | 验收条件 | 风险 | 状态 | | --- | --- | --- | --- | --- | --- | --- | -| PKG-MASK-01 | artplayer-plugin-danmuku-mask
核对包契约与历史用法 | BASE-05 | 模型参数、start/stop、默认下载路径及 mask 样式 | 源码/声明/README/demo/发布包差异已登记;公开形状和版本范围冻结;接续 SDK-08,分别核实 Yarn 解析版本、未固定版本的模型 solutionPath 和资源来源 | H | doing | -| PKG-MASK-02 | artplayer-plugin-danmuku-mask
建立特有行为与错误测试 | PKG-MASK-01, ENG-03, ENG-05 | 加载期间停止/销毁、重复启动、推理失败、WebGL/CPU 边界 | 旧版本行为可重跑,成功/失败/切源/销毁有必要断言 | H | todo | +| PKG-MASK-01 | artplayer-plugin-danmuku-mask
核对包契约与历史用法 | BASE-05 | 模型参数、start/stop、默认下载路径及 mask 样式 | 源码/声明/README/demo/发布包差异已登记;公开形状和版本范围冻结;接续 SDK-08,分别核实 Yarn 解析版本、未固定版本的模型 solutionPath 和资源来源 | H | done | +| PKG-MASK-02 | artplayer-plugin-danmuku-mask
建立特有行为与错误测试 | PKG-MASK-01, ENG-03, ENG-05 | 加载期间停止/销毁、重复启动、推理失败、WebGL/CPU 边界 | 旧版本行为可重跑,成功/失败/切源/销毁有必要断言 | H | doing | | PKG-MASK-03 | artplayer-plugin-danmuku-mask
整理内部职责与资源 | PKG-MASK-02, CORE-18, PKG-DANMUKU-07 | 模型加载/推理/画布输出分离,阻止重叠推理与过期写入 | 结构变化和缺陷修复分开记录;原 API/事件/资源生命周期通过 | H | todo | | PKG-MASK-04 | artplayer-plugin-danmuku-mask
迁移自有源码和公开类型 | PKG-MASK-03, ENG-04, ENG-06, CORE-07 | 模型 adapter、canvas 和选项的精确类型 | 严格类型检查、旧消费样例通过;声明路径/导出和同步异步兼容 | H | todo | | PKG-MASK-05 | artplayer-plugin-danmuku-mask
验证新旧核心和组合 | PKG-MASK-04, CORE-22 | 真实模型和 danmuku/seek/全屏组合,GPU 资源释放 | 最终核心与原支持范围核心分别通过;设备/SDK 缺证据不能标完成 | H | todo | @@ -529,6 +529,7 @@ - PKG-MULTI-SUB-04: [记录](baselines/multiple-subtitles-runtime-types.json) [记录](changes/2026-09-13-PKG-MULTI-SUB-04-runtime-types.md) [记录](baselines/multiple-subtitles-public-types.json) [记录](changes/2026-09-13-PKG-MULTI-SUB-04-public-types.md) [记录](type-compatibility-policy.md) [记录](baselines/multiple-subtitles-approved-types.json) [记录](changes/2026-09-13-PKG-MULTI-SUB-04-approved-types.md) - PKG-MULTI-SUB-07: [记录](baselines/multiple-subtitles-timestamps.json) [记录](changes/2026-09-13-PKG-MULTI-SUB-07-timestamps.md) - PKG-JASSUB-01: [记录](baselines/jassub-release.json) [记录](baselines/jassub-vendor.json) [记录](baselines/jassub-font-metadata.json) [记录](baselines/jassub-contract.md) [记录](baselines/jassub-contract-validation.json) [记录](changes/2026-09-13-PKG-JASSUB-01-baseline.md) +- PKG-MASK-01: [记录](baselines/danmuku-mask-release.json) [记录](baselines/danmuku-mask-registry.json) [记录](baselines/danmuku-mask-contract.md) [记录](baselines/danmuku-mask-contract-validation.json) [记录](changes/2026-09-13-PKG-MASK-01-contract.md) - PKG-ASR-01: [记录](changes/2026-09-13-PKG-ASR-01-contracts.md) [记录](baselines/asr-contract.md) [记录](baselines/asr-release.json) - PKG-ASR-02: [记录](changes/2026-09-13-PKG-ASR-02-audio-baseline.md) [记录](baselines/asr-audio-validation.json) - PKG-ASR-03: [记录](changes/2026-09-13-PKG-ASR-03-audio-ownership.md) [记录](baselines/asr-ownership-validation.json) diff --git a/refactor/progress.md b/refactor/progress.md index e897bacb4..7ac0f6cd0 100644 --- a/refactor/progress.md +++ b/refactor/progress.md @@ -1,5 +1,15 @@ # 进度与证据 +## PKG-MASK-01 弹幕遮罩契约完成 + +冻结全部两个真实稳定发布版、Git关联和导出差异;核对六个Yarn/SDK版本与12个已有 +本地资源字节。6项测试及父代理复核通过。未固定CDN、无效模型参数、异步生命周期、 +重复启动、backend与DOM/通知风险归入后续任务,SDK-08保持开放。 +见[契约](baselines/danmuku-mask-contract.md)与[变更](changes/2026-09-13-PKG-MASK-01-contract.md)。 +228项:138 done、16 doing、74 todo。02开始受控历史失败测试; +没有启动真实模型、GPU或远程投屏,没有推送或发布。 + + ## PKG-CAST-04 公开类型与隔离安装完成 最新npm 1.1.0根类型保持,/runtime精确描述异步注册、回调与查询。17安装配置、 diff --git a/refactor/risk-table.md b/refactor/risk-table.md index f2560c0a9..154a7444a 100644 --- a/refactor/risk-table.md +++ b/refactor/risk-table.md @@ -40,7 +40,7 @@ | SDK-05 | open / 待取证 | webtorrent 实际集成验证范围 | EX-03 | | SDK-06 | open / 待取证 | google-cast 实际集成验证范围 | PKG-CAST-01, PKG-CAST-05 | | SDK-07 | open / 待取证 | ima-via-glomex 实际集成验证范围 | PKG-VAST-01, PKG-VAST-05 | -| SDK-08 | open / 待取证 | mediapipe-tensorflow 实际集成验证范围 | PKG-MASK-01, PKG-MASK-05 | +| SDK-08 | open / 源码/产物事实 | mediapipe-tensorflow 实际集成验证范围 | PKG-MASK-01, PKG-MASK-05, PKG-MASK-06 | | SDK-09 | open / 待取证 | jassub-worker-wasm-fonts 实际集成验证范围 | PKG-JASSUB-01, PKG-JASSUB-05 | | SDK-10 | open / 待取证 | mediabunny 实际集成验证范围 | PKG-MB-01, PKG-MB-09 | | SDK-11 | open / 待取证 | asr-caller-service 实际集成验证范围 | PKG-ASR-01, PKG-ASR-05, EX-03 | @@ -221,3 +221,9 @@ | CAST-LIFE-01 | resolved / 已复现 | Cast listeners and continuations survive player destruction and update another player icon | PKG-CAST-03, PKG-CAST-05 | | CAST-STATE-01 | resolved / 已复现 | Cast terminal session states leave the normalized callback and icon connected | PKG-CAST-03, PKG-CAST-05 | | CAST-ERROR-01 | resolved / 已复现 | Cast media loading rejection escapes on a detached Promise while the click has already fulfilled | PKG-CAST-03, PKG-CAST-05 | +| MASK-LIFETIME-01 | open / 源码/产物事实 | Mask asynchronous initialization and inference continue after stop or destroy | PKG-MASK-02, PKG-MASK-03 | +| MASK-START-01 | open / 源码/产物事实 | Concurrent Mask start calls can initialize multiple models and create independent RAF chains | PKG-MASK-02, PKG-MASK-03 | +| MASK-MODEL-01 | open / 源码/产物事实 | Published Mask options and installed MediaPipe adapter consume different model settings | PKG-MASK-02, PKG-MASK-04, PKG-MASK-05 | +| MASK-BACKEND-01 | open / 源码/产物事实 | Mask TensorFlow backend fallback does not establish MediaPipe fallback and failed initialization keeps scheduling | PKG-MASK-02, PKG-MASK-03, PKG-MASK-05 | +| MASK-DOM-01 | open / 源码/产物事实 | Mask registration and frame processing assume Danmuku DOM, readable frames and a 2D context | PKG-MASK-02, PKG-MASK-03, PKG-MASK-05 | +| MASK-NOTICE-01 | open / 源码/产物事实 | Published Mask bundles lack a separate third-party notice despite bundled SDK code | PKG-MASK-06 | diff --git a/refactor/risks.json b/refactor/risks.json index de3facad4..e284f0c20 100644 --- a/refactor/risks.json +++ b/refactor/risks.json @@ -743,14 +743,18 @@ { "id": "SDK-08", "title": "mediapipe-tensorflow 实际集成验证范围", - "confirmation": "unverified", + "confirmation": "source-observed", "status": "open", "owners": [ "PKG-MASK-01", - "PKG-MASK-05" + "PKG-MASK-05", + "PKG-MASK-06" ], "evidence": [ - "packages/artplayer-plugin-danmuku-mask/src/index.js" + "packages/artplayer-plugin-danmuku-mask/src/index.js", + "refactor/baselines/danmuku-mask-contract.md", + "refactor/baselines/danmuku-mask-contract-validation.json", + "refactor/baselines/danmuku-mask-release.json" ], "compatibleResolution": "Npm dependency versions are separate from unversioned solutionPath CDN assets; keep option override, backend fallback, model result disposal and masks.", "closureCriteria": "固定 SDK/外部资源版本和环境;成功/失败/切换/销毁有真实证据,设备和网络限制逐项记录。" @@ -4898,6 +4902,110 @@ "test/browser/chromecast-lifecycle.spec.js" ], "resolutionRationale": "35 candidate source/main/legacy tests and 135 native browser DOM/core cases verify corrected behavior against controlled SDK boundaries. 93 frozen historical observations remain. Actual remote SDK and receivers remain SDK06/CAST05, not waived." + }, + { + "id": "MASK-LIFETIME-01", + "title": "Mask asynchronous initialization and inference continue after stop or destroy", + "confirmation": "source-observed", + "status": "open", + "owners": [ + "PKG-MASK-02", + "PKG-MASK-03" + ], + "evidence": [ + "refactor/baselines/danmuku-mask-contract.md", + "refactor/baselines/danmuku-mask-release.json", + "refactor/baselines/danmuku-mask-contract-validation.json" + ], + "compatibleResolution": "Invalidate stale work while keeping restartable stop distinct from final destroy; release model and subscriptions.", + "closureCriteria": "Pending initialization/inference cannot write masks or schedule work after stop/destroy; errors settle and owned resources are released." + }, + { + "id": "MASK-START-01", + "title": "Concurrent Mask start calls can initialize multiple models and create independent RAF chains", + "confirmation": "source-observed", + "status": "open", + "owners": [ + "PKG-MASK-02", + "PKG-MASK-03" + ], + "evidence": [ + "refactor/baselines/danmuku-mask-contract.md", + "refactor/baselines/danmuku-mask-release.json", + "refactor/baselines/danmuku-mask-contract-validation.json" + ], + "compatibleResolution": "Share pending initialization and own at most one inference loop per live registration.", + "closureCriteria": "Repeated and concurrent starts, stop then start, failures and multiple instances have deterministic resource and result assertions." + }, + { + "id": "MASK-MODEL-01", + "title": "Published Mask options and installed MediaPipe adapter consume different model settings", + "confirmation": "source-observed", + "status": "open", + "owners": [ + "PKG-MASK-02", + "PKG-MASK-04", + "PKG-MASK-05" + ], + "evidence": [ + "refactor/baselines/danmuku-mask-contract.md", + "refactor/baselines/danmuku-mask-release.json", + "refactor/baselines/danmuku-mask-contract-validation.json" + ], + "compatibleResolution": "Freeze actual default/zero behavior and distinguish supported SDK configuration from inactive historical option fields before changing effects.", + "closureCriteria": "Effective model selection, zero values and configuration mapping have controlled and real model evidence, with explicit compatibility decisions for effect changes." + }, + { + "id": "MASK-BACKEND-01", + "title": "Mask TensorFlow backend fallback does not establish MediaPipe fallback and failed initialization keeps scheduling", + "confirmation": "source-observed", + "status": "open", + "owners": [ + "PKG-MASK-02", + "PKG-MASK-03", + "PKG-MASK-05" + ], + "evidence": [ + "refactor/baselines/danmuku-mask-contract.md", + "refactor/baselines/danmuku-mask-release.json", + "refactor/baselines/danmuku-mask-contract-validation.json" + ], + "compatibleResolution": "Model backend outcomes explicitly, settle initialization failures and document the actual MediaPipe capability boundary.", + "closureCriteria": "Fulfilled false, rejection, CPU failure, createSegmenter failure and later retry are tested; real inference fallback is separately evidenced." + }, + { + "id": "MASK-DOM-01", + "title": "Mask registration and frame processing assume Danmuku DOM, readable frames and a 2D context", + "confirmation": "source-observed", + "status": "open", + "owners": [ + "PKG-MASK-02", + "PKG-MASK-03", + "PKG-MASK-05" + ], + "evidence": [ + "refactor/baselines/danmuku-mask-contract.md", + "refactor/baselines/danmuku-mask-release.json", + "refactor/baselines/danmuku-mask-contract-validation.json" + ], + "compatibleResolution": "Handle missing dependencies and frame/context failures without stale writes or uncontrolled loops while preserving plugin ordering.", + "closureCriteria": "Missing Danmuku/context, CORS failure, source changes and ready/destroy timing have controlled and real browser evidence." + }, + { + "id": "MASK-NOTICE-01", + "title": "Published Mask bundles lack a separate third-party notice despite bundled SDK code", + "confirmation": "source-observed", + "status": "open", + "owners": [ + "PKG-MASK-06" + ], + "evidence": [ + "refactor/baselines/danmuku-mask-contract.md", + "refactor/baselines/danmuku-mask-release.json", + "refactor/baselines/danmuku-mask-contract-validation.json" + ], + "compatibleResolution": "Trace included SDK assets and license notices against actual bundle sources; preserve required notices in candidate packages.", + "closureCriteria": "Document incorporated SDK provenance and applicable notices; inspect built and packed artifacts, without treating manifest license claims as full clearance." } ] } diff --git a/refactor/scripts/danmuku-mask-contract.mjs b/refactor/scripts/danmuku-mask-contract.mjs new file mode 100644 index 000000000..05055ad1e --- /dev/null +++ b/refactor/scripts/danmuku-mask-contract.mjs @@ -0,0 +1,107 @@ +import assert from 'node:assert/strict' +import { execFileSync } from 'node:child_process' +import fs from 'node:fs' +import path from 'node:path' +import process from 'node:process' +import { fileURLToPath } from 'node:url' +import lockfile from '@yarnpkg/lockfile' +import { archiveFiles, ensureArchive, hash, readMember, refactorDir } from './releases.mjs' + +const workspace = path.dirname(refactorDir.replace(/[\\/]$/u, '')) +const prefix = 'packages/artplayer-plugin-danmuku-mask/' +function gitBytes(commit, file) { + return execFileSync('git', ['show', `${commit}:${file}`], { cwd: workspace, maxBuffer: 32 * 1024 * 1024 }) +} +function gitText(commit, file) { + return gitBytes(commit, file).toString().replaceAll('\r\n', '\n') +} + +export async function verifyDanmukuMaskContract() { + const baseline = JSON.parse(fs.readFileSync(path.join(refactorDir, 'baselines/danmuku-mask-release.json'), 'utf8')) + const registryBytes = fs.readFileSync(path.join(refactorDir, 'baselines/danmuku-mask-registry.json')) + assert.equal(hash(registryBytes), baseline.registrySnapshot.bodySha256) + const registry = JSON.parse(registryBytes) + assert.deepEqual(registry['dist-tags'], baseline.registrySnapshot.distTags) + assert.deepEqual(Object.keys(registry.versions).filter(version => /^\d+\.\d+\.\d+$/u.test(version)), baseline.registrySnapshot.stableVersions) + const archives = new Map() + for (const release of [baseline.release, ...baseline.previous]) { + const metadata = registry.versions[release.version] + assert.equal(metadata.dist.integrity, release.integrity) + assert.equal(metadata.dist.tarball, release.tarball) + assert.equal(metadata.gitHead, release.registryGitHead) + const archive = await ensureArchive(release) + assert.deepEqual(archiveFiles(archive), Object.keys(release.files).sort()) + for (const [file, expected] of Object.entries(release.files)) + assert.equal(hash(readMember(archive, file)), expected, `${release.version}/${file}`) + const manifest = JSON.parse(readMember(archive, 'package/package.json')) + assert.deepEqual(manifest, release.manifest) + assert.equal(manifest.version, release.version) + assert.equal(manifest.name, release.name) + for (const [field, target] of Object.entries(release.entrypoints)) { + assert.equal(manifest[field] ?? null, target) + if (target) + assert(Object.hasOwn(release.files, `package/${target.replace(/^\.\//u, '')}`), `${release.version}/${field}`) + } + function targetExists(target) { + if (typeof target === 'string') + assert(Object.hasOwn(release.files, `package/${target.replace(/^\.\//u, '')}`)) + else if (target && typeof target === 'object') + Object.values(target).forEach(targetExists) + } + targetExists(manifest.exports) + for (const association of [release.registrySourceManifest, release.historicalCore]) { + assert.equal(association.commit, release.registryGitHead) + const text = gitText(association.commit, association.file) + assert.equal(hash(text), association.sha256LF) + assert.equal(JSON.parse(text).version, association.version) + } + assert.equal(release.registrySourceManifest.versionMatchesRelease, release.registrySourceManifest.version === release.version) + for (const comparison of release.registrySourceComparison) { + const text = gitText(release.registryGitHead, comparison.file) + assert.equal(hash(text), comparison.sha256LF) + assert.equal(comparison.matchesArchiveBytes, hash(text) === release.files[`package/${comparison.file.slice(prefix.length)}`]) + } + archives.set(release.version, archive) + } + assert.deepEqual([...archives.keys()].sort(), [...baseline.registrySnapshot.stableVersions].sort()) + const sources = new Map() + for (const [file, expected] of Object.entries(baseline.source)) { + const text = gitText(baseline.sourceCommit, file) + assert.equal(hash(text), expected, file) + sources.set(file, text) + } + for (const file of baseline.observations.latestArchiveMatchesFrozenSourceFiles) + assert.equal(hash(sources.get(file)), baseline.release.files[`package/${file.slice(prefix.length)}`]) + const locked = gitText(baseline.sdk.lock.commit, baseline.sdk.lock.file) + assert.equal(hash(locked), baseline.sdk.lock.sha256LF) + const parsed = lockfile.parse(locked) + assert.equal(parsed.type, 'success') + const manifest = JSON.parse(sources.get(`${prefix}package.json`)) + for (const dependency of baseline.sdk.dependencies) { + assert.equal(manifest.dependencies[dependency.name], dependency.range) + const entry = parsed.object[dependency.key] + for (const field of ['version', 'resolved', 'integrity']) + assert.equal(entry[field], dependency[field]) + const installed = fs.readFileSync(path.join(workspace, dependency.installedManifest.file)) + assert.equal(hash(installed), dependency.installedManifest.sha256) + assert.equal(JSON.parse(installed).version, dependency.version) + } + for (const [file, digest] of Object.entries(baseline.sdk.adapterFiles)) + assert.equal(hash(fs.readFileSync(path.join(workspace, file))), digest, file) + for (const asset of baseline.sdk.assets) { + const bytes = gitBytes(baseline.sourceCommit, asset.file) + assert.equal(bytes.length, asset.bytes) + assert.equal(hash(bytes), asset.sha256) + const installed = fs.readFileSync(path.join(workspace, asset.installedFile)) + assert.equal(hash(installed), asset.installedSha256) + assert.equal(asset.matchesInstalled, asset.sha256 === asset.installedSha256) + } + assert(sources.get(`${prefix}src/index.js`).includes(baseline.sdk.defaultSolutionPath)) + assert(sources.get('docs/assets/example/danmuku.mask.js').includes(baseline.sdk.demoSolutionPath)) + return { baseline, archives, sources } +} + +if (process.argv[1] && path.resolve(process.argv[1]) === fileURLToPath(import.meta.url)) { + const { baseline, archives, sources } = await verifyDanmukuMaskContract() + console.log(JSON.stringify({ sourceCommit: baseline.sourceCommit, sourceFiles: sources.size, releases: [...archives.keys()], dependencies: baseline.sdk.dependencies.map(item => `${item.name}@${item.version}`), existingAssets: baseline.sdk.assets.length, validation: 'Archive SHA512/SHA256, complete entrypoints, Git associations, frozen Yarn versions, installed SDK source and existing asset bytes; no model execution' }, null, 2)) +} diff --git a/refactor/scripts/danmuku-mask-contract.test.mjs b/refactor/scripts/danmuku-mask-contract.test.mjs new file mode 100644 index 000000000..eb2eae639 --- /dev/null +++ b/refactor/scripts/danmuku-mask-contract.test.mjs @@ -0,0 +1,170 @@ +import assert from 'node:assert/strict' +import fs from 'node:fs' +// eslint-disable-next-line test/no-import-node-test -- Frozen package contract verification. +import test from 'node:test' +import vm from 'node:vm' +import { transform } from 'esbuild' +import { verifyDanmukuMaskContract } from './danmuku-mask-contract.mjs' +import { readMember } from './releases.mjs' + +const contract = await verifyDanmukuMaskContract() +const source = contract.sources.get('packages/artplayer-plugin-danmuku-mask/src/index.js') + +async function controlledSource(option) { + const frames = new Map() + const configs = [] + const calls = [] + const listeners = new Map() + const style = {} + const pixels = new Uint8ClampedArray([255, 255, 255, 255, 250, 255, 255, 255]) + let id = 0 + const canvas = { + width: 0, + height: 0, + getContext: () => ({ getImageData: () => ({ data: pixels }), putImageData: value => calls.push(['pixels', [...value.data]]) }), + toDataURL: () => 'data:image/png;base64,controlled', + } + const segmenter = { segmentPeople: async () => [{}] } + const body = { + SupportedModels: { MediaPipeSelfieSegmentation: 'selfie' }, + createSegmenter: async (model, config) => { + configs.push({ model, config: { ...config } }) + return segmenter + }, + toBinaryMask: async (...args) => { + calls.push(['mask', ...args]) + return {} + }, + drawMask: async (...args) => { calls.push(['draw', args[3], args[4]]) }, + } + const tf = { setBackend: async value => calls.push(['backend', value]) } + const module = { exports: {} } + vm.runInNewContext((await transform(source, { format: 'cjs', target: 'es2020' })).code, { + module, + exports: module.exports, + require(name) { + if (name === '@tensorflow-models/body-segmentation') + return body + if (name === '@tensorflow/tfjs-core') + return tf + assert(['@tensorflow/tfjs-backend-webgl', '@tensorflow/tfjs-backend-cpu'].includes(name)) + return {} + }, + document: { createElement: (tag) => { + assert.equal(tag, 'canvas') + return canvas + } }, + requestAnimationFrame: (callback) => { + frames.set(++id, callback) + return id + }, + cancelAnimationFrame: id => frames.delete(id), + console, + }, { timeout: 1000 }) + const art = { template: { $video: { paused: false, ended: false, videoWidth: 2, videoHeight: 1 }, $danmuku: { style } }, on: (name, callback) => listeners.set(name, callback) } + const result = module.exports.default(option)(art) + return { result, listeners, frames, configs, calls, style } +} + +test('Danmuku Mask freezes all published stable archive members and their actual Git associations', () => { + assert.deepEqual([...contract.archives.keys()].sort(), ['1.0.0', '1.1.0']) + for (const release of [contract.baseline.release, ...contract.baseline.previous]) { + assert.equal(Object.keys(release.files).length, 6) + assert.deepEqual(release.missingEntrypoints, []) + } + assert.equal(contract.baseline.release.registrySourceManifest.version, '1.0.1') + assert.equal(contract.baseline.release.registrySourceManifest.versionMatchesRelease, false) + assert.equal(contract.baseline.observations.latestArchiveMatchesFrozenSourceFiles.length, 6) +}) + +test('Both published declarations retain optional options, synchronous registration, async start and void stop', () => { + const declaration = contract.sources.get('packages/artplayer-plugin-danmuku-mask/types/artplayer-plugin-danmuku-mask.d.ts') + for (const archive of contract.archives.values()) + assert.equal(readMember(archive, 'package/types/artplayer-plugin-danmuku-mask.d.ts').toString(), declaration) + assert(declaration.includes('(option?: Option) => (art: Artplayer) => Result')) + assert(declaration.includes('start: () => Promise')) + assert(declaration.includes('stop: () => void')) + assert(declaration.includes('export default artplayerPluginDanmukuMask')) +}) + +test('Actual main and legacy exports preserve historical CommonJS and browser global forms without starting a model', () => { + for (const [version, archive] of contract.archives) { + for (const suffix of ['js', 'legacy.js']) { + const bytes = readMember(archive, `package/dist/artplayer-plugin-danmuku-mask.${suffix}`).toString() + for (const form of ['cjs', 'global']) { + const module = { exports: {} } + const environment = { + console, + fetch() { throw new Error('Network is forbidden in this export probe') }, + } + environment.global = environment + environment.self = environment + if (form === 'cjs') + Object.assign(environment, { module, exports: module.exports }) + else environment.window = environment + vm.runInNewContext(bytes, environment, { timeout: 5000 }) + const value = form === 'cjs' ? module.exports : environment.artplayerPluginDanmukuMask + const oldCommonJs = version === '1.0.0' && form === 'cjs' + assert.equal(typeof value, oldCommonJs ? 'object' : 'function') + assert.equal(typeof value.default, oldCommonJs ? 'function' : 'undefined') + const factory = oldCommonJs ? value.default : value + const events = [] + const style = {} + const result = factory()({ + template: { $video: {}, $danmuku: { style } }, + on: name => events.push(name), + }) + assert.equal(result.name, 'artplayerPluginDanmukuMask') + assert.equal(typeof result.start, 'function') + assert.equal(typeof result.stop, 'function') + assert.equal(result.stop(), undefined) + assert.equal(style.maskImage, 'none') + assert.deepEqual(events, ['ready', 'destroy']) + } + } + } +}) + +test('Frozen owned source keeps its defaults, ready/destroy hooks and alpha mask pixel boundary without a real SDK', async () => { + const env = await controlledSource() + assert.equal(env.result.name, 'artplayerPluginDanmukuMask') + assert.deepEqual([...env.listeners.keys()], ['ready', 'destroy']) + const pending = env.result.start() + assert.equal(typeof pending.then, 'function') + await pending + await new Promise(resolve => setImmediate(resolve)) + assert.deepEqual(env.configs, [{ model: 'selfie', config: { runtime: 'mediapipe', modelType: 'general', solutionPath: contract.baseline.sdk.defaultSolutionPath, modelSelection: 1, smoothSegmentation: true, minDetectionConfidence: 0.5, minTrackingConfidence: 0.5, selfieMode: false } }]) + assert.deepEqual(env.style, { maskMode: 'alpha', maskSize: 'contain', maskRepeat: 'no-repeat', backgroundSize: 'contain', backgroundRepeat: 'no-repeat', maskImage: 'url(data:image/png;base64,controlled)' }) + assert.deepEqual(env.calls.find(call => call[0] === 'draw'), ['draw', 1, 3]) + assert.deepEqual(env.calls.find(call => call[0] === 'pixels'), ['pixels', [255, 255, 255, 0, 250, 255, 255, 255]]) + assert.equal(env.frames.size, 1) + assert.equal(env.result.stop(), undefined) + assert.equal(env.frames.size, 0) + assert.equal(env.style.maskImage, 'none') +}) + +test('Frozen zero numeric options use historical OR defaults while false smoothSegmentation is retained', async () => { + const env = await controlledSource({ solutionPath: '/local-model', modelSelection: 0, smoothSegmentation: false, minDetectionConfidence: 0, minTrackingConfidence: 0, foregroundThreshold: 0, opacity: 0, maskBlurAmount: 0, drawContour: true, selfieMode: true }) + await env.result.start() + await new Promise(resolve => setImmediate(resolve)) + assert.deepEqual(env.configs[0].config, { runtime: 'mediapipe', modelType: 'general', solutionPath: '/local-model', modelSelection: 1, smoothSegmentation: false, minDetectionConfidence: 0.5, minTrackingConfidence: 0.5, selfieMode: true }) + assert.deepEqual(env.calls.find(call => call[0] === 'draw'), ['draw', 1, 3]) + const mask = env.calls.find(call => call[0] === 'mask') + assert.equal(mask[4], true) + assert.equal(mask[5], 0.5) + env.result.stop() +}) + +test('SDK-08 distinguishes the frozen Yarn adapter and local resource bytes from unversioned CDN resolution', () => { + assert.deepEqual(contract.baseline.sdk.dependencies.map(item => item.version), ['0.1.1675465747', '1.0.2', '4.22.0', '4.22.0', '4.22.0', '4.22.0']) + assert.equal(contract.baseline.sdk.assets.length, 12) + assert(contract.baseline.sdk.assets.every(asset => asset.matchesInstalled)) + const adapter = fs.readFileSync('node_modules/@tensorflow-models/body-segmentation/dist/selfie_segmentation_mediapipe/segmenter.js', 'utf8') + assert.match(adapter, /switch \(config\.modelType\)/u) + assert.match(adapter, /case 'general':[\s\S]*?modelSelection = 0;/u) + for (const ignored of ['modelSelection', 'smoothSegmentation', 'minDetectionConfidence', 'minTrackingConfidence', 'selfieMode']) + assert.equal(adapter.includes(`config.${ignored}`), false, `Do not claim ${ignored} is consumed by this adapter`) + assert(adapter.includes('return solutionPath + "/" + path;')) + assert(adapter.includes('this.selfieSegmentationSolution.close();')) + assert.equal(source.includes('.dispose('), false, 'The historical plugin does not dispose its segmenter') +}) diff --git a/refactor/tasks.json b/refactor/tasks.json index 44ad55268..35151b3a8 100644 --- a/refactor/tasks.json +++ b/refactor/tasks.json @@ -2518,11 +2518,17 @@ "dependsOn": [ "BASE-05" ], - "status": "doing", + "status": "done", "risk": "H", "deliverable": "模型参数、start/stop、默认下载路径及 mask 样式", "acceptance": "源码/声明/README/demo/发布包差异已登记;公开形状和版本范围冻结;接续 SDK-08,分别核实 Yarn 解析版本、未固定版本的模型 solutionPath 和资源来源", - "evidence": [] + "evidence": [ + "baselines/danmuku-mask-release.json", + "baselines/danmuku-mask-registry.json", + "baselines/danmuku-mask-contract.md", + "baselines/danmuku-mask-contract-validation.json", + "changes/2026-09-13-PKG-MASK-01-contract.md" + ] }, { "id": "PKG-MASK-02", @@ -2536,7 +2542,7 @@ "ENG-03", "ENG-05" ], - "status": "todo", + "status": "doing", "risk": "H", "deliverable": "加载期间停止/销毁、重复启动、推理失败、WebGL/CPU 边界", "acceptance": "旧版本行为可重跑,成功/失败/切源/销毁有必要断言", diff --git a/refactor/third-party.json b/refactor/third-party.json index dfdedc981..381221eed 100644 --- a/refactor/third-party.json +++ b/refactor/third-party.json @@ -1224,14 +1224,20 @@ "riskId": "SDK-08", "owners": [ "PKG-MASK-01", - "PKG-MASK-05" + "PKG-MASK-05", + "PKG-MASK-06" ], "sourceFiles": [ "packages/artplayer-plugin-danmuku-mask/src/index.js" ], "contract": "Npm dependency versions are separate from unversioned solutionPath CDN assets; keep option override, backend fallback, model result disposal and masks.", - "validationStatus": "Source integration observed; complete real SDK/service/device matrix not yet verified.", - "demoPathsSource": "refactor/baselines/demo-inventory.json" + "validationStatus": "MASK01 verified two actual releases, six frozen Yarn/installed SDK versions, adapter source hashes and 12 existing demo resource byte matches. The default solutionPath remains an unversioned CDN. Controlled export/default/pixel probes do not establish real models, fallback, browser/GPU cleanup or notice completeness; MASK05/06 remain open.", + "demoPathsSource": "refactor/baselines/demo-inventory.json", + "reviewEvidence": [ + "refactor/baselines/danmuku-mask-contract.md", + "refactor/baselines/danmuku-mask-contract-validation.json", + "refactor/baselines/danmuku-mask-release.json" + ] }, { "id": "jassub-worker-wasm-fonts",