mirror of
https://github.com/zhw2590582/ArtPlayer.git
synced 2026-10-08 19:06:15 -08:00
refactor(mask): [PKG-MASK-04] migrate strict types and preserve public consumers
This commit is contained in:
1 parent
aba125262a
commit
aca4bc1cd2
30 files changed
+1848
-72
No files matched your search
+23
-22
@@ -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
@@ -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"
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
+3
-1
@@ -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,
|
||||
+27
-14
@@ -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
|
||||
+3
-2
@@ -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() {
|
||||
+8
-5
@@ -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'
|
||||
+10
-6
@@ -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 }
|
||||
@@ -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
@@ -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)
|
||||
|
||||
@@ -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
@@ -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
@@ -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",
|
||||
|
||||
@@ -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
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
|
||||
@@ -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 }
|
||||
Reference in new issue
Block a user