mirror of
https://github.com/zhw2590582/ArtPlayer.git
synced 2026-10-26 08:51:20 -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
@@ -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.
|
||||
Reference in new issue
Block a user