refactor(mask): [PKG-MASK-04] migrate strict types and preserve public consumers

This commit is contained in:
Harvey Zhao committed 2026-09-14 00:05:14 +08:00
1 parent aba125262a
commit aca4bc1cd2
30 files changed
+1848 -72

No files matched your search

+23 -22
View File
@@ -1,25 +1,26 @@
interface Option {
solutionPath?: string
modelSelection?: number
smoothSegmentation?: boolean
minDetectionConfidence?: number
minTrackingConfidence?: number
selfieMode?: boolean
drawContour?: boolean
foregroundThreshold?: number
opacity?: number
maskBlurAmount?: number
// Generated from the package public declaration by yarn build:ts. Do not edit.
/* eslint-disable ts/no-redeclare -- Callable and public type namespace intentionally merge. */
declare namespace artplayerPluginDanmukuMaskDefinitions {
export interface Option {
solutionPath?: string
modelSelection?: number
smoothSegmentation?: boolean
minDetectionConfidence?: number
minTrackingConfidence?: number
selfieMode?: boolean
drawContour?: boolean
foregroundThreshold?: number
opacity?: number
maskBlurAmount?: number
}
export interface Result {
name: 'artplayerPluginDanmukuMask'
start: () => Promise<void>
stop: () => void
}
export const artplayerPluginDanmukuMask: (option?: Option) => (art: Artplayer) => Result
}
interface Result {
name: 'artplayerPluginDanmukuMask'
start: () => Promise<void>
stop: () => void
}
declare const artplayerPluginDanmukuMask: (option?: Option) => (art: Artplayer) => Result
export default artplayerPluginDanmukuMask
declare const artplayerPluginDanmukuMask: typeof artplayerPluginDanmukuMaskDefinitions.artplayerPluginDanmukuMask
declare namespace artplayerPluginDanmukuMask { }
export = artplayerPluginDanmukuMask
export as namespace artplayerPluginDanmukuMask;
+2 -1
View File
@@ -102,7 +102,8 @@
"test:danmuku-mask": "node --test test/danmuku-mask-lifecycle.test.js test/danmuku-mask-failures.test.js refactor/scripts/danmuku-mask-contract.test.mjs",
"test:danmuku": "node --test test/danmuku-setting.test.js test/danmuku-heatmap.test.js test/danmuku-renderer.test.js test/danmuku-scheduler.test.js test/danmuku-worker-client.test.js test/danmuku-input.test.js test/danmuku-parser.test.js test/danmuku-failures.test.js refactor/scripts/danmuku-contract.test.mjs",
"test:danmuku-types": "node --test refactor/scripts/danmuku-types.test.mjs",
"test:danmuku-types-package": "node refactor/scripts/danmuku-package-types.mjs"
"test:danmuku-types-package": "node refactor/scripts/danmuku-package-types.mjs",
"test:danmuku-mask-types-package": "node refactor/scripts/danmuku-mask-package-types.mjs"
},
"browserslist": "last 1 Chrome version",
"devDependencies": {
@@ -2,4 +2,5 @@ node_modules
.DS_Store
yarn.lock
package-lock.json
src
src
tsconfig.json
@@ -9,15 +9,47 @@ The compatibility baseline is the actual npm 1.1.0 package and the earlier
| Module | Responsibility |
| --- | --- |
| `src/index.js` | Preserve synchronous registration, capture core template nodes before option getters, expose named start/stop closures |
| `src/config.js` | Snapshot the original option defaults without changing OR/undefined semantics |
| `src/sdk.js` | Existing TF backend selection, MediaPipe adapter configuration, mask calls and model disposal outlet |
| `src/controller.js` | One active run, initialization/inference serialization, cancellation, RAF ownership, ready/destroy subscriptions |
| `src/output.js` | Private canvas/context, unchanged binary mask colors and threshold conversion, guarded maskImage commit, canvas release |
| `src/index.ts` | Preserve synchronous registration, capture core template nodes before option getters, expose named start/stop closures |
| `src/config.ts` | Snapshot the original option defaults without changing OR/undefined semantics |
| `src/sdk.ts` | Existing TF backend selection, MediaPipe adapter configuration, mask calls and model disposal outlet |
| `src/controller.ts` | One active run, initialization/inference serialization, cancellation, RAF ownership, ready/destroy subscriptions |
| `src/output.ts` | Private canvas/context, unchanged binary mask colors and threshold conversion, guarded maskImage commit, canvas release |
| `src/types.ts` | Internal host, normalized options, exact legacy SDK arguments, canvas resources and nullable run state |
| `src/sdk-ambient.d.ts` | Declaration-only import of the SDK's official Long namespace dependency |
These modules remain JavaScript for PKG-MASK-03. Full owned-source TypeScript and
public declaration work belongs to PKG-MASK-04; this task does not claim it is
finished. Public declarations are unchanged.
The owned modules use strict TypeScript. The entry depends on config/controller;
controller depends on SDK/output; output delegates mask operations to SDK. The
shared contracts contain only types and do not add runtime dependencies. The
internal host accepts the actual core declaration in
`refactor/fixtures/implementation/danmuku-mask.ts`; it needs only core template
nodes, destroy state and ready/destroy subscription methods. Public declarations
remain a separate compatibility surface and never import these source types.
Controller fields use `declare` to retain the existing own-property initialization
order. Its limited non-null assertions describe lifecycle invariants: tick runs
only after model/output initialization, disposal waits for busy frame work, and
the public start promise is assigned before the first initialization continuation.
The cancellation resolver is assigned by the synchronous Promise executor.
Output byte indexing relies on native ImageData's complete RGBA pixels. The
backend error's message assertion preserves the old property read, including
its behavior for non-Error rejections; it does not normalize thrown values.
The SDK config uses the official MediaPipe config intersected with the exact
legacy extra keys. `satisfies` checks those fields, and the subsequent assertion
prevents the SDK union's excess-property check from deleting compatibility data.
No SDK option interpretation changes. TF 4.22.0 `dist/hash_util.d.ts:2-3` references
`Long` without an import. The local declaration-only bridge loads its existing
official `@types/long` 4.0.2 UMD namespace. This package imports the TF declarations
directly; it therefore needs the bridge under the root's explicit `types: []`.
Keep `skipLibCheck: false`, do not rewrite Long or add a runtime import for it.
The package tsconfig and all source modules stay outside the packed artifact.
Public factory declarations retain the actual npm 1.0.0/1.1.0 shape. The exact
`typesVersions` legacy mapping fixes old Node-resolution consumers without
changing the root factory. See `types/README.md` for private types, namespace
limits and the semantic online-editor generator. Strict positive/negative
factory replacement and return-type cases run in the baseline suite; installed
consumers separately check packed contents and historical diagnostics.
Mask captures the core's video and `.art-danmuku` layer. The core template
provides that layer before plugins register; Danmuku does not create it.
@@ -104,15 +136,19 @@ make a disposal test appear stronger than its real API.
From the repository root with pinned Node and Yarn:
```sh
node node_modules/typescript/bin/tsc -p packages/artplayer-plugin-danmuku-mask/tsconfig.json
node --test test/danmuku-mask-lifecycle.test.js
yarn test:danmuku-mask
yarn test:danmuku-mask-types-package
yarn build:ts artplayer-plugin-danmuku-mask
node --test test/danmuku-mask-failures.test.js refactor/scripts/danmuku-mask-contract.test.mjs
node node_modules/eslint/bin/eslint.js packages/artplayer-plugin-danmuku-mask/src test/helpers/danmuku-mask-candidate.js test/danmuku-mask-lifecycle.test.js
yarn build artplayer-plugin-danmuku-mask
yarn dev artplayer-plugin-danmuku-mask
```
The candidate helper bundles current owned modules and substitutes the SDK
The candidate helper requires exactly one JS/TS source entry, bundles current
owned modules and substitutes the SDK
imports with controlled implementations. Its fake RAF/video/canvas test
cancellation and output ownership; they do not prove real model quality, canvas
pixels, CSS alignment, CORS, browser scheduling, GPU memory or WASM cleanup.
@@ -129,5 +165,5 @@ by its original player. Native shared TensorFlow backend state remains unverifie
Native acceptance still needs actual local model loading, new/old cores, real
Danmuku composition, pause/seek/source/layout changes, multiple players, failure
recovery and post-destroy resources. The default unversioned solutionPath can
load assets independently of Yarn's SDK resolution. PKG-MASK-04/05/06 remain
separate source/type, real combination and distribution/release stages.
load assets independently of Yarn's SDK resolution. PKG-MASK-05/06 retain the
real combination and distribution/release gates after source/type migration.
@@ -41,5 +41,12 @@
"@tensorflow/tfjs-backend-webgl": "^4.21.0",
"@tensorflow/tfjs-converter": "^4.21.0",
"@tensorflow/tfjs-core": "^4.21.0"
},
"typesVersions": {
"*": {
"legacy": [
"types/artplayer-plugin-danmuku-mask.d.ts"
]
}
}
}
@@ -1,4 +1,6 @@
export default function maskConfig(option) {
import type { MaskConfig, MaskOptions } from './types'
export default function maskConfig(option: MaskOptions): MaskConfig {
return {
solutionPath: option.solutionPath || 'https://cdn.jsdelivr.net/npm/@mediapipe/selfie_segmentation',
modelSelection: option.modelSelection || 1,
@@ -1,8 +1,18 @@
import type { MaskConfig, MaskHost, MaskNodes, MaskRun } from './types'
import { createOutput, releaseOutput, renderMask } from './output'
import { loadSegmenter, releaseSegmenter } from './sdk'
export default class MaskController {
constructor(art, config, { $video, $danmuku }) {
declare art: MaskHost
declare config: MaskConfig
declare video: HTMLVideoElement
declare layer: HTMLElement
declare closed: boolean
declare run: MaskRun | null
declare tail: Promise<void>
declare ready: () => void
constructor(art: MaskHost, config: MaskConfig, { $video, $danmuku }: MaskNodes) {
this.art = art
this.config = config
this.video = $video
@@ -38,11 +48,11 @@ export default class MaskController {
}
}
active(run) {
active(run: MaskRun): boolean {
return run === this.run && run.running && !this.closed && !this.art.isDestroy
}
release(run) {
release(run: MaskRun): Promise<void> {
if (run.releasing)
return run.releasing
const segmenter = run.segmenter
@@ -63,7 +73,7 @@ export default class MaskController {
return run.releasing
}
schedule(run) {
schedule(run: MaskRun): void {
if (!this.active(run) || run.frame !== null || run.busy)
return
run.frame = requestAnimationFrame(() => {
@@ -72,7 +82,7 @@ export default class MaskController {
})
}
tick(run) {
tick(run: MaskRun): void {
if (!this.active(run) || run.busy)
return
const video = this.video
@@ -86,7 +96,8 @@ export default class MaskController {
// Reserve the work slot before SDK code can synchronously reenter start/stop.
await Promise.resolve()
if (this.active(run))
await renderMask(run.output, video, this.layer, run.segmenter, this.config, () => this.active(run))
// Only initialized runs reach tick; release waits for this busy work to settle.
await renderMask(run.output!, video, this.layer, run.segmenter!, this.config, () => this.active(run))
}
catch (error) {
if (this.active(run))
@@ -103,14 +114,16 @@ export default class MaskController {
this.tail = work.catch(error => console.warn('Failed to release danmuku mask resources:', error))
}
async start() {
async start(): Promise<void> {
if (this.closed || this.art.isDestroy)
return
// The start promise is assigned before initialization resumes after its first await.
if (this.run?.running)
return this.run.started
let cancel
const cancelled = new Promise(resolve => cancel = resolve)
const run = { running: true, initializing: true, busy: false, frame: null, segmenter: null, output: null, cancel, started: null }
return this.run.started!
// Promise executors install their resolver synchronously.
let cancel!: () => void
const cancelled = new Promise<void>(resolve => cancel = resolve)
const run: MaskRun = { running: true, initializing: true, busy: false, frame: null, segmenter: null, output: null, cancel, started: null }
const previous = this.tail
this.run = run
const initialize = (async () => {
@@ -149,7 +162,7 @@ export default class MaskController {
return run.started
}
halt() {
halt(): void {
const run = this.run
if (run) {
run.running = false
@@ -163,13 +176,13 @@ export default class MaskController {
}
}
stop() {
stop(): void {
this.halt()
if (this.layer?.style)
this.layer.style.maskImage = 'none'
}
destroy() {
destroy(): void {
if (this.closed)
return
this.closed = true
@@ -1,8 +1,9 @@
import type { MaskHost, MaskOptions } from './types'
import maskConfig from './config'
import MaskController from './controller'
export default function artplayerPluginDanmukuMask(option = {}) {
return (art) => {
export default function artplayerPluginDanmukuMask(option: MaskOptions = {}) {
return (art: MaskHost) => {
const { template: { $video, $danmuku } } = art
const controller = new MaskController(art, maskConfig(option), { $video, $danmuku })
async function startSegmentation() {
@@ -1,15 +1,18 @@
import type { BodySegmenter } from '@tensorflow-models/body-segmentation'
import type { Active, MaskConfig, MaskOutput } from './types'
import { drawMask, toBinaryMask } from './sdk'
export function makeWhiteTransparent(imageData) {
export function makeWhiteTransparent(imageData: ImageData): ImageData {
const data = imageData.data
// Native ImageData stores complete four-byte RGBA pixels.
for (let i = 0; i < data.length; i += 4) {
if (data[i] > 250 && data[i + 1] > 250 && data[i + 2] > 250)
if (data[i]! > 250 && data[i + 1]! > 250 && data[i + 2]! > 250)
data[i + 3] = 0
}
return imageData
}
export function createOutput(layer) {
export function createOutput(layer: HTMLElement): MaskOutput {
const canvas = document.createElement('canvas')
try {
const ctx = canvas.getContext('2d')
@@ -31,7 +34,7 @@ export function createOutput(layer) {
}
}
export async function renderMask(output, video, layer, segmenter, config, active) {
export async function renderMask(output: MaskOutput, video: HTMLVideoElement, layer: HTMLElement, segmenter: BodySegmenter, config: MaskConfig, active: Active): Promise<void> {
const { canvas, ctx } = output
canvas.width = video.videoWidth
canvas.height = video.videoHeight
@@ -57,7 +60,7 @@ export async function renderMask(output, video, layer, segmenter, config, active
layer.style.maskImage = `url(${url})`
}
export function releaseOutput(output) {
export function releaseOutput(output: MaskOutput | null): void {
if (output) {
output.canvas.width = 0
output.canvas.height = 0
@@ -0,0 +1,3 @@
// TF 4.22.0 hash_util.d.ts uses global Long without importing its own dependency.
// Load the official UMD namespace for this internal check; emit no runtime import.
import 'long'
@@ -1,21 +1,25 @@
import type { BodySegmenter } from '@tensorflow-models/body-segmentation'
import type { Active, MaskConfig, SegmenterConfig } from './types'
import * as bodySegmentation from '@tensorflow-models/body-segmentation'
import * as tf from '@tensorflow/tfjs-core'
import '@tensorflow/tfjs-backend-webgl'
import '@tensorflow/tfjs-backend-cpu'
export async function loadSegmenter(config, active) {
export async function loadSegmenter(config: MaskConfig, active: Active): Promise<BodySegmenter | null> {
try {
await tf.setBackend('webgl')
}
catch (error) {
if (!active())
return null
console.warn('WebGL backend not available, falling back to CPU', error.message)
// Keep the historical message property read, including non-Error rejection behavior.
console.warn('WebGL backend not available, falling back to CPU', (error as { message: unknown }).message)
await tf.setBackend('cpu')
}
if (!active())
return null
try {
// Validate the full legacy object without dropping keys absent from the SDK config union.
return await bodySegmentation.createSegmenter(bodySegmentation.SupportedModels.MediaPipeSelfieSegmentation, {
runtime: 'mediapipe',
modelType: 'general',
@@ -25,7 +29,7 @@ export async function loadSegmenter(config, active) {
minDetectionConfidence: config.minDetectionConfidence,
minTrackingConfidence: config.minTrackingConfidence,
selfieMode: config.selfieMode,
})
} satisfies SegmenterConfig as SegmenterConfig)
}
catch (error) {
if (active())
@@ -34,7 +38,7 @@ export async function loadSegmenter(config, active) {
}
}
export async function releaseSegmenter(segmenter) {
export async function releaseSegmenter(segmenter: BodySegmenter): Promise<void> {
try {
await segmenter.dispose()
}
@@ -43,5 +47,5 @@ export async function releaseSegmenter(segmenter) {
}
}
export const toBinaryMask = (...args) => bodySegmentation.toBinaryMask(...args)
export const drawMask = (...args) => bodySegmentation.drawMask(...args)
export const toBinaryMask = (...args: Parameters<typeof bodySegmentation.toBinaryMask>) => bodySegmentation.toBinaryMask(...args)
export const drawMask = (...args: Parameters<typeof bodySegmentation.drawMask>) => bodySegmentation.drawMask(...args)
@@ -0,0 +1,50 @@
import type { BodySegmenter, MediaPipeSelfieSegmentationMediaPipeModelConfig } from '@tensorflow-models/body-segmentation'
// Owned implementation contracts; the historical public entrypoint is maintained separately.
export interface MaskOptions {
solutionPath?: string
modelSelection?: number
smoothSegmentation?: boolean
minDetectionConfidence?: number
minTrackingConfidence?: number
selfieMode?: boolean
drawContour?: boolean
foregroundThreshold?: number
opacity?: number
maskBlurAmount?: number
}
export type MaskConfig = Required<MaskOptions>
export type Active = () => boolean
// These legacy extra keys are passed through even where the current adapter ignores them.
export type SegmenterConfig = MediaPipeSelfieSegmentationMediaPipeModelConfig & Pick<MaskConfig, 'modelSelection' | 'smoothSegmentation' | 'minDetectionConfidence' | 'minTrackingConfidence' | 'selfieMode'>
export interface MaskNodes {
$video: HTMLVideoElement
$danmuku: HTMLElement
}
export interface MaskHost {
isDestroy: boolean
template: MaskNodes
on: (event: 'ready' | 'destroy', callback: () => void) => unknown
off: (event: 'ready' | 'destroy', callback: () => void) => unknown
}
export interface MaskOutput {
canvas: HTMLCanvasElement
ctx: CanvasRenderingContext2D
}
export interface MaskRun {
running: boolean
initializing: boolean
busy: boolean
frame: number | null
segmenter: BodySegmenter | null
output: MaskOutput | null
cancel: () => void
started: Promise<void> | null
releasing?: Promise<void>
}
@@ -0,0 +1,5 @@
{
"extends": "../../tsconfig.base.json",
"compilerOptions": { "allowJs": false },
"include": ["src/**/*.ts", "../../refactor/fixtures/implementation/danmuku-mask.ts"]
}
@@ -0,0 +1,39 @@
# Public type maintenance
`artplayer-plugin-danmuku-mask.d.ts` is the authored public declaration for both
the root and `/legacy` entrypoints. Both actual npm 1.0.0 and 1.1.0 shipped this
same factory shape. Keep its optional argument, synchronous registration,
literal result name, `start(): Promise<void>` and `stop(): void`; ordinary
replacement functions and `Parameters`/`ReturnType` extraction are supported.
The declaration already describes these runtime return values correctly. This
migration does not add a second `/runtime` entrypoint or expose SDK model types.
Internal run, canvas and SDK types live under `src/`; they are not the consumer
contract and are excluded from the package archive.
The manifest's exact `typesVersions` mapping makes `/legacy` resolve with older
Node module resolution, including the supported TypeScript 4.3.5 compiler. The
published packages lacked this mapping; their root declaration remains unchanged.
Modern resolution uses the existing `exports` declaration targets.
The package retains its historical NodeNext ESM declaration namespace behavior.
Do not infer a runtime `factory.default` property from that type-only namespace:
the latest CommonJS runtime exports a function without that property. The npm
1.0.0 CommonJS wrapper had a default export object, while 1.1.0 already exports
the callable directly. These are recorded historical differences.
Numeric option types do not redefine runtime defaults. Zero-valued options still
follow the original OR defaults; the installed SDK uses `modelType: general`
and ignores several additional forwarded options. See `../ARCHITECTURE.md` before
changing their effects or claiming a specific inference backend.
Run `node --test refactor/scripts/danmuku-mask-types.test.mjs` for strict factory,
argument/result and negative consumer cases. After a normal package build,
`yarn test:danmuku-mask-types-package` checks actual tarballs installed outside the
workspace, old/current compilers, NodeNext/Bundler resolution and the historical
namespace diagnostics. Those checks do not run a model or prove GPU cleanup.
`yarn build:ts artplayer-plugin-danmuku-mask` generates the standalone online
editor declaration with the semantic generator. This avoids the former mixture
of default and export-assignment syntax. The private Option/Result interfaces
remain private; extract them through `Parameters` and `ReturnType` when needed.
File diff suppressed because it is too large. Load diff
@@ -0,0 +1,53 @@
# PKG-MASK-04 严格TS与公开类型兼容
## 实现与公开接口
五个自有运行模块迁为TS,types.ts明确最小核心宿主、原始可选参数/完整配置、SDK
额外历史传参、输出画布及每轮可空资源。类字段declare不产生新字段;局部断言说明
对应初始化、Promise resolver或RGBA长度约束。实际Artplayer宿主fixture通过严格检查。
相对MASK03提交aba125262,五模块归一化JS和三种正常构建产物保持一致。
公开声明与npm1.0.0/1.1.0保持字节一致(忽略换行)。原参数可选、同步注册、
Promise<void> start、void stop、固定name、完整工厂双向赋值及类型提取继续保留。
这些返回值本就准确,不为形式统一额外增加/runtime入口或暴露内部SDK类型。
## 两项类型工具修复
实际严格消费者先复现旧Node模块解析下/legacy报2307,现以精确typesVersions路径
映射修复;默认工厂声明与其他入口未变,历史直接声明路径仍验证。npm两版在相同
模式均记录旧失败,候选只允许消除此特定行的2307;NodeNext ESM根命名空间诊断
保持历史结果。根CommonJS最新是函数无default属性,1.0.0对象包装单独验证。
旧在线编辑器生成器把export default和export=混在一起,实际旧文件在两个编译器
均报2309。接入现有语义生成器,生成文件正例及三项非法调用反例通过。私有Option/
Result不改为新增公开类型;消费者用Parameters/ReturnType提取,生成内容校验防漂移。
SDK严格声明另复现TF4.22.0的hash_util.d.ts缺少Long import。声明专用sdk-ambient.d.ts
引用已安装官方@types/long4.0.2,不产生运行时导入、没有重写第三方类型,也不改
types:[]或skipLibCheck:false。包src和tsconfig均不进入tarball。
## 验证
Node24.21.0下联合99项通过:36候选生命周期、54历史缺陷、6历史契约、3公共/编辑器
类型测试。定向lint、全仓404生产TS及当前/兼容编译器流程通过。三种产物正常重建,
docs副本一致,生成编辑器声明通过正反例和文件一致性验证。
最终Yarn实际pack将旧npm1.0.0/1.1.0和候选分别安装到仓库外,复制根锁作依赖种子,
离线安装后再次frozen安装并逐字节核对包内容/锁文件。每包TS5.9.3的Node10、NodeNext
CJS/ESM、Bundler及TS4.3.5的Node10,共15组;每组8项非法输入/返回值独立报错。
实际安装main和legacy执行同步注册/stop/destroy,但不启动模型;不称为播放或SDK
验收。旧Node10/legacy失败用既有直接声明路径核实工厂,其原错误保留在报告。
复跑:yarn test:danmuku-mask、node --test refactor/scripts/danmuku-mask-types.test.mjs、
yarn test:danmuku-mask-types-package、yarn typecheck、yarn build:ts artplayer-plugin-danmuku-mask。
新增安装检查脚本复用现有Yarn/TS版本,无新增依赖或锁文件改动。包内架构及types/README
解释维护路径和历史边界。详见[类型验证](../baselines/danmuku-mask-types-validation.json)。
同步更新风险、SDK来源与影响映射中的当前源码路径,冻结历史JS夹具不改;风险和影响
检查通过,没有因迁移路径失效而忽略SDK或插件组合验证。
## 剩余工作与回退
05仍负责真实模型、原支持范围核心、浏览器/设备/切源/布局/后端与GPU资源;06负责
完整分发、许可、示例和发布验收。WebKit/Chrome设备能力和SDK的close完成仍未证明。
本任务不关闭这些风险、不改变各包独立major发布政策,不推送或发布。
回退此任务源码/类型解析及生成器选择后正常重建即可;03生命周期修复是独立提交。
@@ -0,0 +1,6 @@
import type Artplayer from 'artplayer'
import factory from '../../../packages/artplayer-plugin-danmuku-mask/src'
declare const art: Artplayer
const registration: { name: string, start: () => Promise<void>, stop: () => void } = factory({ opacity: 0, smoothSegmentation: false })(art)
export { registration }
+1 -1
View File
@@ -123,7 +123,7 @@
"consumer": "artplayer-plugin-danmuku-mask",
"reason": "Mask and Danmuku share the core-owned $danmuku layer; retain combination validation for root identity, styles and lifecycle",
"evidence": [
"packages/artplayer-plugin-danmuku-mask/src/index.js",
"packages/artplayer-plugin-danmuku-mask/src/index.ts",
"docs/assets/example/danmuku.mask.js"
]
}
+3 -2
View File
@@ -4,7 +4,7 @@
基线:`40fcda6a37d0049d42e49c1e64e70d4fd9ba5f7f`。总任务 231 项,范围 22 个包及工作区/示例。
状态:todo 67 / doing 14 / blocked 0 / done 150 / deferred 0。风险 L/M/H 表示兼容风险,不表示工期。
状态:todo 66 / doing 14 / blocked 0 / done 151 / deferred 0。风险 L/M/H 表示兼容风险,不表示工期。
前置依赖是启动条件;验收是完成条件。任务可以继续拆分,但不能复用或悄悄删除旧 ID。
@@ -243,7 +243,7 @@
| PKG-MASK-01 | artplayer-plugin-danmuku-mask<br>核对包契约与历史用法 | BASE-05 | 模型参数、start/stop、默认下载路径及 mask 样式 | 源码/声明/README/demo/发布包差异已登记;公开形状和版本范围冻结;接续 SDK-08,分别核实 Yarn 解析版本、未固定版本的模型 solutionPath 和资源来源 | H | done |
| PKG-MASK-02 | artplayer-plugin-danmuku-mask<br>建立特有行为与错误测试 | PKG-MASK-01, ENG-03, ENG-05 | 加载期间停止/销毁、重复启动、推理失败、WebGL/CPU 边界 | 旧版本行为可重跑,成功/失败/切源/销毁有必要断言 | H | done |
| PKG-MASK-03 | artplayer-plugin-danmuku-mask<br>整理内部职责与资源 | PKG-MASK-02, CORE-18, PKG-DANMUKU-07 | 模型加载/推理/画布输出分离,阻止重叠推理与过期写入 | 结构变化和缺陷修复分开记录;原 API/事件/资源生命周期通过 | H | done |
| PKG-MASK-04 | artplayer-plugin-danmuku-mask<br>迁移自有源码和公开类型 | PKG-MASK-03, ENG-04, ENG-06, CORE-07 | 模型 adapter、canvas 和选项的精确类型 | 严格类型检查、旧消费样例通过;声明路径/导出和同步异步兼容 | H | todo |
| PKG-MASK-04 | artplayer-plugin-danmuku-mask<br>迁移自有源码和公开类型 | PKG-MASK-03, ENG-04, ENG-06, CORE-07 | 模型 adapter、canvas 和选项的精确类型 | 严格类型检查、旧消费样例通过;声明路径/导出和同步异步兼容 | H | done |
| PKG-MASK-05 | artplayer-plugin-danmuku-mask<br>验证新旧核心和组合 | PKG-MASK-04, CORE-22 | 真实模型和 danmuku/seek/全屏组合,GPU 资源释放 | 最终核心与原支持范围核心分别通过;设备/SDK 缺证据不能标完成 | H | todo |
| PKG-MASK-06 | artplayer-plugin-danmuku-mask<br>验证分发并同步文档 | PKG-MASK-05, ENG-07 | danmuku.mask.js、资源版本/许可、CPU fallback 和包体积证据 | tarball 入口/资源、类型、8082 demo 和 README 一致,有回退记录 | H | todo |
@@ -535,6 +535,7 @@
- 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-MASK-02: [记录](changes/2026-09-13-PKG-MASK-02-failures.md) [记录](baselines/danmuku-mask-failures-validation.json)
- PKG-MASK-03: [记录](changes/2026-09-13-PKG-MASK-03-lifecycle.md) [记录](baselines/danmuku-mask-lifecycle-validation.json)
- PKG-MASK-04: [记录](changes/2026-09-14-PKG-MASK-04-types.md) [记录](baselines/danmuku-mask-types-validation.json)
- 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)
+9
View File
@@ -1,5 +1,14 @@
# 进度与证据
## PKG-MASK-04 严格TS及公开类型完成
五个自有模块迁TS且运行产物保持MASK03字节;真实核心宿主、SDK声明桥接和私有资源
类型通过严格检查。修复旧Node解析/legacy类型路径与编辑器混合导出,保留根工厂。
联合99项、安装包15编译器模式和全仓404生产TS通过;[详细记录](changes/2026-09-14-PKG-MASK-04-types.md)。
16插件中源码与类型迁移未完成的剩AutoThumbnail/VAST/JASSUB三包;所有后续组合/发布
门槛继续按tasks.json执行,不能把迁移完成当作已可发布。Mask05/06与实际SDK资源风险待验收。
## PKG-MASK-03 生命周期拆分完成
五个职责模块和单运行归属控制器落地;停止/销毁、迟到回调、并发启动、释放等待和
+1 -1
View File
@@ -751,7 +751,7 @@
"PKG-MASK-06"
],
"evidence": [
"packages/artplayer-plugin-danmuku-mask/src/index.js",
"packages/artplayer-plugin-danmuku-mask/src/index.ts",
"refactor/baselines/danmuku-mask-contract.md",
"refactor/baselines/danmuku-mask-contract-validation.json",
"refactor/baselines/danmuku-mask-release.json"
@@ -0,0 +1,22 @@
import fs from 'node:fs'
export const maskInvalidStatements = [
'mask({ modelSelection: "general" })',
'mask({ smoothSegmentation: 1 })',
'mask({ solutionPath: 42 })',
'mask({ opacity: "1" })',
'const invalidName: "other" = result.name',
'const invalidStart: void = result.start()',
'const invalidStop: Promise<void> = result.stop()',
'mask().then(() => {})',
]
export function maskConsumerSource(mode, raw = false) {
let source = fs.readFileSync(new URL('../../test/types/danmuku-mask-public.ts', import.meta.url), 'utf8')
if (mode === 'nodenext-esm' && !raw) {
source = source
.replace('import mask from \'artplayer-plugin-danmuku-mask\'', 'import maskModule from \'artplayer-plugin-danmuku-mask\'\nconst mask = maskModule.default')
.replace('import legacy from \'artplayer-plugin-danmuku-mask/legacy\'', 'import legacyModule from \'artplayer-plugin-danmuku-mask/legacy\'\nconst legacy = legacyModule.default')
}
return source
}
@@ -0,0 +1,108 @@
import assert from 'node:assert/strict'
import fs from 'node:fs'
import path from 'node:path'
import process from 'node:process'
import ts from 'typescript'
import compat from 'typescript-compat'
import { checkFiles, packedFiles } from '../../scripts/package-check.mjs'
import { consumerDirectory, removeConsumer, run, workspace, writeJson } from '../../scripts/package-consumer.mjs'
import { maskConsumerSource, maskInvalidStatements } from './danmuku-mask-consumer.mjs'
import { verifyDanmukuMaskContract } from './danmuku-mask-contract.mjs'
import { hash, readMember } from './releases.mjs'
async function main() {
assert.equal(process.env.npm_config_user_agent?.split(' ')[0], 'yarn/1.22.22', 'Use yarn test:danmuku-mask-types-package')
const yarn = process.env.npm_execpath
assert(yarn && fs.existsSync(yarn))
const output = fs.mkdtempSync(path.join(workspace, 'refactor/.cache/danmuku-mask-package-types-'))
console.log(`Mask installed consumer evidence: ${output}`)
const contract = await verifyDanmukuMaskContract()
const packages = []
for (const name of ['artplayer', 'artplayer-plugin-danmuku-mask']) {
const archive = path.join(output, `${name}.tgz`)
fs.writeFileSync(path.join(output, `${name}-pack.log`), run([yarn, 'pack', '--filename', archive], path.join(workspace, 'packages', name)))
const manifest = JSON.parse(readMember(archive, 'package/package.json'))
const files = packedFiles(archive)
checkFiles(manifest, files, name === contract.baseline.release.name ? Object.keys(contract.baseline.release.files) : [])
packages.push({ name, archive, sha256: hash(fs.readFileSync(archive)), files: Object.fromEntries(files.map(member => [member, hash(readMember(archive, member))])) })
}
const core = packages[0]
const candidates = [
...[...contract.baseline.previous, contract.baseline.release].map(release => ({ ...release, archive: contract.archives.get(release.version), label: `published-${release.version}` })),
{ ...packages[1], label: 'candidate' },
]
const matrix = []
const priorDiagnostics = new Map()
for (const plugin of candidates) {
const consumer = consumerDirectory()
try {
writeJson(path.join(consumer, 'package.json'), { name: 'mask-isolated-consumer', private: true, dependencies: Object.fromEntries([core, plugin].map(pkg => [pkg.name, `file:${pkg.archive.replaceAll('\\', '/')}`])) })
fs.copyFileSync(path.join(workspace, 'yarn.lock'), path.join(consumer, 'yarn.lock'))
fs.writeFileSync(path.join(output, `${plugin.label}-install.log`), run([yarn, 'install', '--offline', '--ignore-scripts', '--non-interactive'], consumer))
const lock = fs.readFileSync(path.join(consumer, 'yarn.lock'))
fs.writeFileSync(path.join(output, `${plugin.label}-frozen-install.log`), run([yarn, 'install', '--offline', '--frozen-lockfile', '--ignore-scripts', '--non-interactive'], consumer))
assert.deepEqual(fs.readFileSync(path.join(consumer, 'yarn.lock')), lock)
fs.writeFileSync(path.join(output, `${plugin.label}-yarn.lock`), lock)
for (const pkg of [core, plugin]) {
const root = path.join(consumer, 'node_modules', pkg.name)
assert.equal(fs.realpathSync(root), root, 'Installed package is a workspace link')
for (const [member, expected] of Object.entries(pkg.files))
assert.equal(hash(fs.readFileSync(path.join(root, member.slice(8)))), expected, `Installed bytes differ: ${member}`)
}
const runtime = path.join(consumer, 'exports.cjs')
fs.writeFileSync(runtime, `const assert = require('node:assert/strict'); global.self = global;
for (const name of ['artplayer-plugin-danmuku-mask', 'artplayer-plugin-danmuku-mask/legacy']) {
const value = require(name); const old = ${plugin.version === '1.0.0'};
assert.equal(typeof value, old ? 'object' : 'function');
assert.equal(typeof value.default, old ? 'function' : 'undefined');
const factory = old ? value.default : value;
const style = {}; const handlers = new Map();
const result = factory()({ template: { $video: {}, $danmuku: {style} }, on(name, fn) { handlers.set(name, fn); }, off(name, fn) { if (handlers.get(name) === fn) handlers.delete(name); } });
assert.deepEqual(Object.keys(result), ['name', 'start', 'stop']);
assert.equal(result.name, 'artplayerPluginDanmukuMask'); assert.equal(typeof result.start, 'function');
assert.equal(result.stop(), undefined); assert.equal(style.maskImage, 'none');
handlers.get('destroy')();
}
console.log('Installed CJS/legacy registration and stop/destroy passed; no model startup.');`)
fs.writeFileSync(path.join(output, `${plugin.label}-runtime.log`), run([runtime], consumer))
for (const [compiler, mode] of [[ts, 'node10-commonjs'], [ts, 'nodenext-cjs'], [ts, 'nodenext-esm'], [ts, 'bundler-esm'], [compat, 'node10-commonjs']]) {
const next = mode.startsWith('nodenext')
const filename = path.join(consumer, `consumer.${next ? mode.endsWith('-cjs') ? 'cts' : 'mts' : 'ts'}`)
const options = { strict: true, noEmit: true, skipLibCheck: false, types: [], esModuleInterop: true, target: compiler.ScriptTarget.ES2020, lib: ['lib.es2020.d.ts', 'lib.dom.d.ts'], module: next ? compiler.ModuleKind.NodeNext : mode === 'bundler-esm' ? compiler.ModuleKind.ESNext : compiler.ModuleKind.CommonJS, moduleResolution: next ? compiler.ModuleResolutionKind.NodeNext : mode === 'bundler-esm' ? compiler.ModuleResolutionKind.Bundler : compiler.ModuleResolutionKind.NodeJs }
const compile = (source) => {
fs.writeFileSync(filename, source)
const program = compiler.createProgram([filename], options)
for (const file of program.getSourceFiles()) {
const actual = fs.realpathSync(file.fileName)
assert(actual.startsWith(fs.realpathSync(consumer) + path.sep) || (program.isSourceFileDefaultLibrary(file) && path.dirname(actual) === fs.realpathSync(path.dirname(compiler.sys.getExecutingFilePath()))), `Type escaped consumer: ${actual}`)
}
return compiler.getPreEmitDiagnostics(program).map(d => ({ code: d.code, file: d.file ? path.relative(consumer, d.file.fileName).replaceAll('\\', '/') : null, line: d.file && d.start !== undefined ? d.file.getLineAndCharacterOfPosition(d.start).line + 1 : null, message: compiler.flattenDiagnosticMessageText(d.messageText, '\n').replaceAll(consumer.replaceAll('\\', '/'), '<consumer>') }))
}
const raw = compile(maskConsumerSource(mode, true))
const key = `${compiler.version}:${mode}`
if (plugin.label === 'published-1.1.0')
priorDiagnostics.set(key, raw)
if (plugin.label === 'candidate')
assert.deepEqual(raw, priorDiagnostics.get(key).filter(item => !(mode === 'node10-commonjs' && item.code === 2307 && item.line === 3)), 'Only the recorded old legacy-path resolution failure may be corrected')
let source = maskConsumerSource(mode)
if (plugin.label !== 'candidate' && mode === 'node10-commonjs') {
assert.deepEqual(raw.map(item => [item.code, item.line]), [[2307, 3]], 'Historical legacy path failure must stay precise')
source = source.replace('from \'artplayer-plugin-danmuku-mask/legacy\'', 'from \'artplayer-plugin-danmuku-mask/types/artplayer-plugin-danmuku-mask\'')
}
const diagnostics = compile(source)
assert.deepEqual(diagnostics, [], `${plugin.label} ${compiler.version} ${mode}`)
const invalid = compile(`${source}\n${maskInvalidStatements.join('\n')}\n`)
assert.equal(invalid.length, maskInvalidStatements.length)
assert.deepEqual(invalid.map(item => item.line), maskInvalidStatements.map((_, index) => source.split('\n').length + 1 + index))
matrix.push({ plugin: plugin.label, compiler: compiler.version, mode, raw, diagnostics, invalid })
}
}
finally { removeConsumer(consumer) }
}
writeJson(path.join(output, 'report.json'), { task: 'PKG-MASK-04', node: process.version, packages, published: candidates.slice(0, 2).map(pkg => ({ label: pkg.label, archive: pkg.archive, sha256: hash(fs.readFileSync(pkg.archive)) })), matrix, scope: 'Actual published and candidate packages installed outside workspace, offline/frozen lock stability, byte checks, strict types and CJS/legacy registration. No model startup, native inference, ESM browser or GPU acceptance.' })
console.log(`Mask installed consumers passed ${matrix.length} compiler modes: ${output}`)
}
main().catch((error) => {
console.error(error)
process.exitCode = 1
})
@@ -0,0 +1,57 @@
import assert from 'node:assert/strict'
import fs from 'node:fs'
// eslint-disable-next-line test/no-import-node-test -- Public compatibility uses the repository Node runner.
import test from 'node:test'
import { ESLint } from 'eslint'
import ts from 'typescript'
import compat from 'typescript-compat'
import { checkPluginEditorDeclaration, generatePluginEditorDeclaration } from '../../scripts/plugin-editor-types.mjs'
import { checkConsumer } from '../../scripts/typecheck.mjs'
import { maskInvalidStatements } from './danmuku-mask-consumer.mjs'
import { verifyDanmukuMaskContract } from './danmuku-mask-contract.mjs'
import { readMember } from './releases.mjs'
test('Mask public declaration keeps both actual published factory and result shapes', async () => {
const contract = await verifyDanmukuMaskContract()
const current = fs.readFileSync('packages/artplayer-plugin-danmuku-mask/types/artplayer-plugin-danmuku-mask.d.ts', 'utf8').replaceAll('\r\n', '\n')
for (const archive of contract.archives.values())
assert.equal(current, readMember(archive, 'package/types/artplayer-plugin-danmuku-mask.d.ts').toString().replaceAll('\r\n', '\n'))
})
test('Mask strict public consumers preserve optional arguments, function replacement and reject invalid results/options', () => {
const source = fs.readFileSync('test/types/danmuku-mask-public.ts', 'utf8')
for (const compiler of [ts, compat]) {
assert.deepEqual(checkConsumer(compiler, 'node10-commonjs', source), [])
assert.deepEqual(checkConsumer(compiler, 'node10-commonjs', source.replace('from \'artplayer-plugin-danmuku-mask\'', 'from \'artplayer-plugin-danmuku-mask/types/artplayer-plugin-danmuku-mask\'')), [], 'Legacy mapping must preserve historical direct declaration paths')
const invalid = checkConsumer(compiler, 'node10-commonjs', `${source}\n${maskInvalidStatements.join('\n')}\n`)
assert.equal(invalid.length, maskInvalidStatements.length)
assert.deepEqual(invalid.map(item => item.line), maskInvalidStatements.map((_, index) => source.split('\n').length + 1 + index))
}
})
test('Mask editor declaration is generated from the unchanged public contract and accepts optional registration', async () => {
const source = fs.readFileSync('packages/artplayer-plugin-danmuku-mask/types/artplayer-plugin-danmuku-mask.d.ts', 'utf8')
const generated = generatePluginEditorDeclaration(source, 'artplayerPluginDanmukuMask')
const file = 'docs/assets/ts/artplayer-plugin-danmuku-mask.d.ts'
const [formatted] = await new ESLint({ fix: true, fixTypes: ['layout'] }).lintText(generated, { filePath: file })
assert.equal(formatted.errorCount, 0)
assert.equal(fs.readFileSync(file, 'utf8').replaceAll('\r\n', '\n'), formatted.output || generated)
const core = fs.readFileSync('docs/assets/ts/artplayer.d.ts', 'utf8')
const consumer = `const option: NonNullable<Parameters<typeof artplayerPluginDanmukuMask>[0]> = {opacity: 0};
const art = new Artplayer({container: '#player', url: '/video.mp4'});
const result = artplayerPluginDanmukuMask(option)(art);
const started: Promise<void> = result.start();
const stopped: void = result.stop();
artplayerPluginDanmukuMask();
const replacement: typeof artplayerPluginDanmukuMask = () => () => ({name: 'artplayerPluginDanmukuMask', async start() {}, stop() {}});
// @ts-expect-error Synchronous registration has no then method.
artplayerPluginDanmukuMask().then(() => {});
// @ts-expect-error The runtime factory has no default property.
artplayerPluginDanmukuMask.default();
// @ts-expect-error Opacity remains numeric.
artplayerPluginDanmukuMask({opacity: '1'});`
for (const compiler of [ts, compat]) {
assert.deepEqual(checkPluginEditorDeclaration(generated, core, consumer, compiler), [])
assert.equal(checkPluginEditorDeclaration(generated, core, consumer.replaceAll(/\/\/ @ts-expect-error[^\n]*\n/g, ''), compiler).length, 3)
}
})
+5 -2
View File
@@ -2585,11 +2585,14 @@
"ENG-06",
"CORE-07"
],
"status": "todo",
"status": "done",
"risk": "H",
"deliverable": "模型 adapter、canvas 和选项的精确类型",
"acceptance": "严格类型检查、旧消费样例通过;声明路径/导出和同步异步兼容",
"evidence": []
"evidence": [
"changes/2026-09-14-PKG-MASK-04-types.md",
"baselines/danmuku-mask-types-validation.json"
]
},
{
"id": "PKG-MASK-05",
+5 -1
View File
@@ -1228,7 +1228,11 @@
"PKG-MASK-06"
],
"sourceFiles": [
"packages/artplayer-plugin-danmuku-mask/src/index.js"
"packages/artplayer-plugin-danmuku-mask/src/index.ts",
"packages/artplayer-plugin-danmuku-mask/src/config.ts",
"packages/artplayer-plugin-danmuku-mask/src/sdk.ts",
"packages/artplayer-plugin-danmuku-mask/src/controller.ts",
"packages/artplayer-plugin-danmuku-mask/src/output.ts"
],
"contract": "Npm dependency versions are separate from unversioned solutionPath CDN assets; keep option override, backend fallback, model result disposal and masks.",
"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.",
+1 -1
View File
@@ -53,7 +53,7 @@ console.log(`✨ Built ${artplayerTSoutput}`);
const type = pluginsTS[index]
const { name, file } = parsePluginInfo(type)
const source = String(fs.readFileSync(type))
const semanticPlugin = ['artplayerPluginDanmuku', 'artplayerPluginChromecast', 'artplayerPluginAsr', 'artplayerPluginMultipleSubtitles', 'artplayerPluginVttThumbnail', 'artplayerPluginHlsControl', 'artplayerPluginAudioTrack', 'artplayerPluginDashControl', 'artplayerPluginAds', 'artplayerPluginAmbilight', 'artplayerProxyCanvas', 'artplayerProxyMediabunny', 'artplayerPluginDocumentPip', 'ArtplayerToolIframe', 'ArtplayerToolThumbnail'].includes(name)
const semanticPlugin = ['artplayerPluginDanmukuMask', 'artplayerPluginDanmuku', 'artplayerPluginChromecast', 'artplayerPluginAsr', 'artplayerPluginMultipleSubtitles', 'artplayerPluginVttThumbnail', 'artplayerPluginHlsControl', 'artplayerPluginAudioTrack', 'artplayerPluginDashControl', 'artplayerPluginAds', 'artplayerPluginAmbilight', 'artplayerProxyCanvas', 'artplayerProxyMediabunny', 'artplayerPluginDocumentPip', 'ArtplayerToolIframe', 'ArtplayerToolThumbnail'].includes(name)
const localTypes = name === 'artplayerProxyMediabunny' ? { './media': fs.readFileSync(path.join(path.dirname(type), 'media.d.ts'), 'utf8') } : {}
const code = semanticPlugin
? generatePluginEditorDeclaration(source, name, localTypes)
+4
View File
@@ -6,6 +6,10 @@ Use the pinned Node/Yarn toolchain from `../refactor/toolchain-setup.md`.
frozen historical defects and public contracts. Candidate tests also run in
`test:unit`; their controlled SDK, RAF and canvas hosts do not prove native model
inference, GPU disposal or browser mask geometry. See the Mask architecture map.
`yarn test:danmuku-mask-types-package` installs both published versions and the
candidate tarball outside the workspace for old/current compiler compatibility,
negative cases and CJS/legacy registration without model startup. The ordinary
baseline suite also checks public declaration identity and editor generation.
`yarn test:mediabunny` runs historical lifecycle observations, candidate load cancellation,
and real SDK input parsing/track contracts; all are in `test:unit`. Candidate failures
+2 -1
View File
@@ -1,10 +1,11 @@
import assert from 'node:assert/strict'
import vm from 'node:vm'
import { build } from 'esbuild'
import { resolveSource } from './load.js'
export const flush = () => new Promise(resolve => setImmediate(resolve))
export async function maskCandidate() {
const result = await build({ entryPoints: ['packages/artplayer-plugin-danmuku-mask/src/index.js'], bundle: true, write: false, format: 'cjs', platform: 'browser', external: ['@tensorflow-models/body-segmentation', '@tensorflow/tfjs-core', '@tensorflow/tfjs-backend-webgl', '@tensorflow/tfjs-backend-cpu'] })
const result = await build({ entryPoints: [resolveSource('packages/artplayer-plugin-danmuku-mask/src/index')], bundle: true, write: false, format: 'cjs', platform: 'browser', external: ['@tensorflow-models/body-segmentation', '@tensorflow/tfjs-core', '@tensorflow/tfjs-backend-webgl', '@tensorflow/tfjs-backend-cpu'] })
return result.outputFiles[0].text
}
+34
View File
@@ -0,0 +1,34 @@
import Artplayer from 'artplayer'
import mask from 'artplayer-plugin-danmuku-mask'
import legacy from 'artplayer-plugin-danmuku-mask/legacy'
interface Option {
solutionPath?: string
modelSelection?: number
smoothSegmentation?: boolean
minDetectionConfidence?: number
minTrackingConfidence?: number
selfieMode?: boolean
drawContour?: boolean
foregroundThreshold?: number
opacity?: number
maskBlurAmount?: number
}
interface Result { name: 'artplayerPluginDanmukuMask', start: () => Promise<void>, stop: () => void }
type Factory = (option?: Option) => (art: Artplayer) => Result
type Equal<A, B> = (<T>() => T extends A ? 1 : 2) extends (<T>() => T extends B ? 1 : 2) ? true : false
type Assert<T extends true> = T
type Arguments = Assert<Equal<Parameters<typeof mask>, [option?: Option]>>
type Registration = Assert<Equal<ReturnType<ReturnType<typeof mask>>, Result>>
const replacement: Factory = () => () => ({ name: 'artplayerPluginDanmukuMask', async start() {}, stop() {} })
const toOld: Factory = mask
const fromOld: typeof mask = replacement
const legacyReplacement: typeof legacy = replacement
const option: Option = { solutionPath: '/models', modelSelection: 0, smoothSegmentation: false, minDetectionConfidence: 0, minTrackingConfidence: 0, selfieMode: false, drawContour: false, foregroundThreshold: 0, opacity: 0, maskBlurAmount: 0 }
const art = new Artplayer({ container: '#player', url: '/video.mp4', plugins: [mask(), mask(option), legacy(undefined)] })
const result = mask(option)(art)
const name: 'artplayerPluginDanmukuMask' = result.name
const starting: Promise<void> = result.start()
const stopped: void = result.stop()
void [toOld, fromOld, legacyReplacement, name, starting, stopped]
export type { Arguments, Registration }