test(asr): [PKG-ASR-01] freeze published interfaces and source contracts

This commit is contained in:
Harvey Zhao committed 2026-09-13 14:03:16 +08:00
1 parent 3b016970f2
commit a22586a172
16 files changed
+543 -7

No files matched your search

+3 -2
View File
@@ -51,7 +51,7 @@
"ci:build": "yarn build:types && yarn build all && yarn build:i18n && yarn build:ts && yarn build:docs && yarn test:imports",
"test:imports": "node --test test/esm.test.js test/i18n.test.js test/ssr.test.js",
"typecheck": "node scripts/typecheck.mjs",
"test:unit": "node --test test/jassub.test.js test/multiple-subtitles-merge.test.js test/multiple-subtitles-lifecycle.test.js test/multiple-subtitles-failures.test.js test/multiple-subtitles.test.js test/multiple-subtitles-vendor.test.js test/vtt-thumbnail-parser.test.js test/vtt-thumbnail-lifecycle.test.js test/vtt-thumbnail.test.js test/auto-thumbnail-frames.test.js test/auto-thumbnail-lifecycle.test.js test/auto-thumbnail.test.js test/thumbnail-emitter.test.js test/thumbnail-runtime.test.js test/thumbnail-vendor.test.js test/thumbnail-lifecycle.test.js test/thumbnail-input.test.js test/thumbnail.test.js test/iframe-navigation.test.js test/iframe-boundaries.test.js test/iframe-lifecycle.test.js test/iframe.test.js test/mediabunny.test.js test/mediabunny-shim.test.js test/mediabunny-coordination.test.js test/mediabunny-video.test.js test/mediabunny-audio.test.js test/mediabunny-hls.test.js test/mediabunny-entry.test.js test/mediabunny-capability.test.js test/mediabunny-load.test.js test/mediabunny-input.test.js test/dpip.test.js test/dpip-lifecycle.test.js test/canvas.test.js test/canvas-lifecycle.test.js test/ambilight.test.js test/ambilight-lifecycle.test.js test/ambilight-proxy.test.js test/vast.test.js test/vast-lifecycle.test.js test/ads.test.js test/ads-lifecycle.test.js test/playback.test.js test/dash-control.test.js test/dash-contract.test.js test/dash-lifecycle.test.js test/dash-events.test.js test/hls-control.test.js test/audio-track.test.js test/public-behavior.test.js test/helpers.test.js test/chapter.test.js test/utils.test.js test/resource-scope.test.js test/instance-lifecycle.test.js test/options.test.js test/media-hosts.test.js test/plugins.test.js test/source.test.js test/playback-properties.test.js test/media-events.test.js test/template-resources.test.js test/core-vendor.test.js test/component-resources.test.js test/setting-model.test.js test/setting-layout.test.js test/setting-resources.test.js test/subtitle.test.js test/display-native.test.js test/display-video-fullscreen.test.js test/display-pip.test.js test/display-mini.test.js test/display-sizing.test.js test/display-orientation.test.js test/hotkey.test.js test/listener-registry.test.js test/global-events.test.js test/pointer-events.test.js test/gesture.test.js test/event-scheduling.test.js test/notice.test.js test/fast-forward.test.js test/auto-playback.test.js test/builtin-layers.test.js test/prompt-components.test.js test/screenshot.test.js test/thumbnails.test.js test/progress.test.js test/environment.test.js test/storage.test.js test/facade-properties.test.js test/dom-boundaries.test.js test/initialization.test.js test/entry.test.js test/accessibility-button.test.js test/accessibility-focus.test.js test/accessibility-slider.test.js",
"test:unit": "node --test test/asr.test.js test/jassub.test.js test/multiple-subtitles-merge.test.js test/multiple-subtitles-lifecycle.test.js test/multiple-subtitles-failures.test.js test/multiple-subtitles.test.js test/multiple-subtitles-vendor.test.js test/vtt-thumbnail-parser.test.js test/vtt-thumbnail-lifecycle.test.js test/vtt-thumbnail.test.js test/auto-thumbnail-frames.test.js test/auto-thumbnail-lifecycle.test.js test/auto-thumbnail.test.js test/thumbnail-emitter.test.js test/thumbnail-runtime.test.js test/thumbnail-vendor.test.js test/thumbnail-lifecycle.test.js test/thumbnail-input.test.js test/thumbnail.test.js test/iframe-navigation.test.js test/iframe-boundaries.test.js test/iframe-lifecycle.test.js test/iframe.test.js test/mediabunny.test.js test/mediabunny-shim.test.js test/mediabunny-coordination.test.js test/mediabunny-video.test.js test/mediabunny-audio.test.js test/mediabunny-hls.test.js test/mediabunny-entry.test.js test/mediabunny-capability.test.js test/mediabunny-load.test.js test/mediabunny-input.test.js test/dpip.test.js test/dpip-lifecycle.test.js test/canvas.test.js test/canvas-lifecycle.test.js test/ambilight.test.js test/ambilight-lifecycle.test.js test/ambilight-proxy.test.js test/vast.test.js test/vast-lifecycle.test.js test/ads.test.js test/ads-lifecycle.test.js test/playback.test.js test/dash-control.test.js test/dash-contract.test.js test/dash-lifecycle.test.js test/dash-events.test.js test/hls-control.test.js test/audio-track.test.js test/public-behavior.test.js test/helpers.test.js test/chapter.test.js test/utils.test.js test/resource-scope.test.js test/instance-lifecycle.test.js test/options.test.js test/media-hosts.test.js test/plugins.test.js test/source.test.js test/playback-properties.test.js test/media-events.test.js test/template-resources.test.js test/core-vendor.test.js test/component-resources.test.js test/setting-model.test.js test/setting-layout.test.js test/setting-resources.test.js test/subtitle.test.js test/display-native.test.js test/display-video-fullscreen.test.js test/display-pip.test.js test/display-mini.test.js test/display-sizing.test.js test/display-orientation.test.js test/hotkey.test.js test/listener-registry.test.js test/global-events.test.js test/pointer-events.test.js test/gesture.test.js test/event-scheduling.test.js test/notice.test.js test/fast-forward.test.js test/auto-playback.test.js test/builtin-layers.test.js test/prompt-components.test.js test/screenshot.test.js test/thumbnails.test.js test/progress.test.js test/environment.test.js test/storage.test.js test/facade-properties.test.js test/dom-boundaries.test.js test/initialization.test.js test/entry.test.js test/accessibility-button.test.js test/accessibility-focus.test.js test/accessibility-slider.test.js",
"test:coverage": "node --test test/coverage.test.js && node scripts/coverage.mjs",
"test": "yarn test:node && yarn test:baseline",
"test:browser": "playwright test",
@@ -93,7 +93,8 @@
"test:jassub": "node --test test/jassub.test.js refactor/scripts/jassub-contract.test.mjs",
"check:ci": "node refactor/scripts/ci-workflow.mjs",
"test:ci": "node --test test/ci-summary.test.js refactor/scripts/ci-workflow.test.mjs refactor/scripts/impact.test.mjs test/package-runtime.test.js",
"test:package:runtime": "node scripts/package-runtime.mjs --canonical"
"test:package:runtime": "node scripts/package-runtime.mjs --canonical",
"test:asr": "node --test test/asr.test.js"
},
"browserslist": "last 1 Chrome version",
"devDependencies": {
@@ -0,0 +1,33 @@
# ASR maintenance
The current implementation is still in `src/index.js`. TypeScript migration and
resource ownership changes are tracked in the repository refactor plan as
PKG-ASR-02 through PKG-ASR-06. Do not interpret the baseline tests as completion.
- The default factory snapshots options. Registration creates the `asr` layer
and subscribes to player play, pause, volumechange and destroy events.
- Play creates an AudioContext and tries a media element source, then a captured
MediaStream source. An inline Blob contains the `recorder-processor` Worklet.
- Worklet messages supply first-channel Float32 samples. The interval callback
encodes little-endian mono PCM16 and a 44-byte-header WAV and invokes the
caller's `onAudioChunk({ pcm, wav })`. A returned string replaces subtitles.
- `append` splits punctuation and sets HTML in `div.art-asr-line`; `hide` changes
display only. `src/style.less` owns layout and existing player CSS variables.
- `stop()` is actually asynchronous and releases audio resources, but retains
playback subscriptions. It is not a permanent disable API. There is no public
`start()` method. Public declarations currently describe stop as returning void.
This package does not implement recognition or open an ASR network connection.
The demo service and its WebSocket belong to the caller. Do not run that service
to validate audio capture; use local media and a local callback.
The initial implementation has suspected chunk loss, partial initialization
cleanup and stale callback issues. Preserve the frozen historical tests while
adding candidate regression tests when fixing these. Keep callback/stop typing
compatible with historical assignments, and preserve public entrypoints, HTML
rendering behavior, layer names and stylesheet identity.
From the repository root, `yarn test:asr` verifies historical public behavior;
`yarn test:baseline` includes archive provenance. `yarn dev artplayer-plugin-asr`
and `yarn build artplayer-plugin-asr` use repository build tools. Real browser
audio and lifecycle validation remains a separate required migration step.
+7 -1
View File
@@ -1,6 +1,12 @@
# artplayer-plugin-asr
Ads plugin for ArtPlayer
Audio capture and subtitle display plugin for ArtPlayer. Supplies PCM/WAV chunks
to a caller-provided recognition service.
## Maintenance
See [ARCHITECTURE.md](ARCHITECTURE.md) for the current audio flow, public behavior,
known migration gaps and test commands.
## Demo
+5
View File
@@ -5,6 +5,11 @@
AI 用于源码分析、任务拆分、实现、测试和差异审查。现代化的验收是可验证的类型、行为和构建质量,不以生成代码量为指标。
2026-09-13 用户授权使用子代理加快重构。按独立包或不重叠文件划分有界任务,
主代理负责集成、共享任务台账和每任务提交;子代理不得同时改同一生产模块或
自行批量提交。测试可与契约核对并行,依赖未满足的实现不得提前宣布完成。
按实际影响范围验证,完整回归保留在跨包变更和阶段验收节点;不承诺线性加速。
## 开始任务
1. 阅读根 AGENTS.md、本目录 README、progress、quality-contract.md,以及所选任务关联的契约/设计。
+60
View File
@@ -0,0 +1,60 @@
# ASR 发布契约
PKG-ASR-01 于 2026-09-13 固定 npm 2.0.0、2.1.0 的实际归档,每包 6 个成员。
[来源与逐文件指纹](asr-release.json)另保存工作区提交的 9 个 Git 文本输入。
运行 `node --test test/asr.test.js refactor/scripts/asr-contract.test.mjs`:32 项通过。
首次下载使用固定 npm URL;缓存后离线复核 SHA-512、SHA-256、成员与历史 Git 输入。
## 支持范围与入口
- registry gitHead 对应核心 manifest 为 5.3.1-beta.1 / 5.3.1;这是来源关联,
不是已完成这些核心的播放器组合验证,更不是最低核心版本声明。
- 两个发布包没有 engines/peerDependencies;工作区 2.1.0 不等于发布 tarball。
- main/legacy 可在 CJS 读取并作为普通 script 暴露 artplayerPluginAsr;2.0.0 的 CJS
是只有 default 可枚举成员的对象,2.1.0 是无可枚举成员的函数。测试同时断言这些
原始形状,再从函数或 default 成员取得工厂;迁移必须分别保留历史调用路径。
- 两个实际 ESM 文件均在无 DOM 环境导入,仅导出 default,调用工厂仍不创建音频资源。
- 旧 TS 工厂替换和安装消费者验收归 PKG-ASR-04;不能以本次
VM 运行通过替代实际 tarball 安装验证。
## 公开行为
| 项目 | 既有行为 |
| --- | --- |
| 工厂 | option 可省略;在调用工厂时解构,之后更改 option 不影响注册 |
| 默认值 | length=3、interval=100ms、sampleRate=16000、autoHideTimeout=10000ms |
| 注册 | 同步创建 name=asr 的 layer,返回 name/stop/hide/append;无公开 start |
| 事件 | 注册 video:volumechange、play、pause、destroy;播放事件启动采集 |
| append | 同步返回 undefined;非字符串忽略,字符串替换全部内容 |
| 行拆分 | 按 、。!?!?. 断句,trim/filter,保留最后 length 段;length=0 保留全部 |
| 内容 | div.art-asr-line 包裹,通过 innerHTML 接受 HTML;不能静默改为纯文本 |
| hide | 只隐藏显示,不清内容;下一次字符串 append 恢复并重置自动隐藏定时器 |
| stop | 实际返回 Promise<void>,调用完整音频销毁;保留播放订阅,可随后重启 |
| 样式 | 模块加载时按 artplayer-plugin-asr ID 去重;沿用 art-layer-asr/CSS 变量 |
32 项测试包括两个发布版本的 main/legacy、冻结源码以及冻结工作区 main/legacy,
分别观察 CJS/global 工厂、返回值、选项快照、字幕行为、样式去重;另有归档与 ESM 检查。
冻结源码用 esbuild 转换,仅把 LESS import 换为测试字符串;该项不验证真实 CSS 编译。
真实发布和冻结工作区 bundle 使用其实际内嵌样式。
## 已发现的差异及后续处理
1. 两版及工作区声明把 stop 写成 void,onAudioChunk 写成 void | Promise<void>;
实际 stop 是异步,并等待回调结果交给 append。类型迁移需保留旧函数赋值能力,
补充精确异步消费,不能只改返回类型就宣布兼容。由 PKG-ASR-04 处理。
2. 源码每次 tick 取队列再截断,未满一块的样本和超过块长的尾部均没有放回;
这是源码事实,PKG-ASR-02 用准确 PCM 样本序列复现,PKG-ASR-03 修复。
3. 初始化最后才置 started;停止及异常清理依赖 started,addModule 失败没有 finally
回收 URL;停止不能使在途回调失效。PKG-ASR-02 复现后再作独立生命周期修复。
4. pause 后 sourceNode 仍存在,而 setupAudioSource 仅在不存在时直接返回节点;
再次启动可能进入 captureStream 分支。需验证重复启动与恢复,不能只测一次播放。
5. README 误写 Ads;本次改正包功能描述。demo 使用外部 ASR HTTP/WebSocket,服务
不是本包依赖或已验证能力。本包只产出 PCM/WAV,不请求麦克风,也不自带识别模型。
## 下一步与限制
PKG-ASR-02 建立 PCM/WAV、Worklet 消息、失败、乱序和资源的受控回归;PKG-ASR-03
拆分字幕、编码、队列和音频资源职责;PKG-ASR-04 完成严格 TS 与旧消费者。
PKG-ASR-05 使用本地媒体与本地 onAudioChunk 验证真实 WebAudio/Worklet、音频持续性、
新旧核心与三个浏览器。WebAudio mock 不代替实际音频/采样率/静音/CORS/设备验证。
本任务没有运行真实识别服务,不增加依赖,不改生产源码/分发版本。
+138
View File
@@ -0,0 +1,138 @@
{
"schemaVersion": 1,
"registry": "https://registry.npmjs.org/artplayer-plugin-asr",
"capturedAt": "2026-09-13T05:57:08.117Z",
"sourceCommit": "3b016970f24fea603706a2f4bd50026846f5d939",
"sourceNormalization": "Git text normalized to LF; archive members hashed as bytes.",
"source": {
"packages/artplayer-plugin-asr/package.json": "f0c9fc6c294376c7ed8974556672b077e31fae480bd8e3ecc8b2a060811b29cd",
"packages/artplayer-plugin-asr/README.md": "8bc731990d7ab32837af8382a99759ee14b6872dd24065929054a69fdcea2e4e",
"packages/artplayer-plugin-asr/src/index.js": "af24a6334298d636235d3af417bdea83444d200563ba6df054b66c53cdaef47f",
"packages/artplayer-plugin-asr/src/style.less": "ed40939c1db527bddf98c3e08b8c2e0b1d5a3581804fe2084d1070c9d50af0d6",
"packages/artplayer-plugin-asr/types/artplayer-plugin-asr.d.ts": "2d1ecb04f285b5835744d80dbde3c4a2a124bf1f5ffc995d0d775f5725309d0f",
"packages/artplayer-plugin-asr/dist/artplayer-plugin-asr.js": "7f975b131a9ea207f88a157aafec778bc4906230500db9b30f5bbfbf327b251d",
"packages/artplayer-plugin-asr/dist/artplayer-plugin-asr.legacy.js": "8b9f07ce97a8ce2e4f29e41e79257be530fd7a76558369d34c3d7497ea606c33",
"packages/artplayer-plugin-asr/dist/artplayer-plugin-asr.mjs": "f1c4274541e86544b65936ff470413cfc7001999ebc193969f95c123be48cd61",
"docs/assets/example/asr.js": "bb7fd5eee3df887b7e0c72a6f42f74f0253350f9bc884f73cb9c2996f0e35724"
},
"release": {
"name": "artplayer-plugin-asr",
"version": "2.1.0",
"tarball": "https://registry.npmjs.org/artplayer-plugin-asr/-/artplayer-plugin-asr-2.1.0.tgz",
"integrity": "sha512-15gXff458PAx9LeJxKzZEvmpFhfwKUfSLgb1Mfr7Xb/ZGsnE+6EA7ezTnO5S55W5mFExjv/e95iOIXP4ijuruQ==",
"sha256": "8bc3cd93f78ce99e5c09cc546c770a63e8aea5729d3d584134ce04d826b7708b",
"registryGitHead": "daf133b22630b4a0eecfa3336bbddab0e9119d96",
"files": {
"package/README.md": "8bc731990d7ab32837af8382a99759ee14b6872dd24065929054a69fdcea2e4e",
"package/dist/artplayer-plugin-asr.js": "7f975b131a9ea207f88a157aafec778bc4906230500db9b30f5bbfbf327b251d",
"package/dist/artplayer-plugin-asr.legacy.js": "8b9f07ce97a8ce2e4f29e41e79257be530fd7a76558369d34c3d7497ea606c33",
"package/dist/artplayer-plugin-asr.mjs": "f1c4274541e86544b65936ff470413cfc7001999ebc193969f95c123be48cd61",
"package/package.json": "f0c9fc6c294376c7ed8974556672b077e31fae480bd8e3ecc8b2a060811b29cd",
"package/types/artplayer-plugin-asr.d.ts": "2d1ecb04f285b5835744d80dbde3c4a2a124bf1f5ffc995d0d775f5725309d0f"
},
"manifest": {
"name": "artplayer-plugin-asr",
"version": "2.1.0",
"description": "Asr plugin for ArtPlayer",
"author": "Harvey Zhao <laozhaochaguan@gmail.com>",
"license": "MIT",
"homepage": "https://artplayer.org",
"repository": {
"type": "git",
"url": "git+https://github.com/zhw2590582/ArtPlayer.git"
},
"bugs": {
"url": "https://github.com/zhw2590582/ArtPlayer/issues"
},
"keywords": [
"html5",
"video",
"player"
],
"exports": {
".": {
"types": "./types/artplayer-plugin-asr.d.ts",
"import": "./dist/artplayer-plugin-asr.mjs",
"require": "./dist/artplayer-plugin-asr.js"
},
"./legacy": {
"types": "./types/artplayer-plugin-asr.d.ts",
"import": "./dist/artplayer-plugin-asr.legacy.js",
"require": "./dist/artplayer-plugin-asr.legacy.js"
}
},
"main": "./dist/artplayer-plugin-asr.js",
"module": "./dist/artplayer-plugin-asr.mjs",
"types": "./types/artplayer-plugin-asr.d.ts",
"legacy": "./dist/artplayer-plugin-asr.legacy.js",
"browserslist": "last 1 Chrome version"
},
"historicalCore": {
"commit": "daf133b22630b4a0eecfa3336bbddab0e9119d96",
"file": "packages/artplayer/package.json",
"version": "5.3.1",
"sha256LF": "862f3697876fe915dd0ada178da766ce823aae7563ed30b3e5b11b811202929c"
}
},
"previous": [
{
"name": "artplayer-plugin-asr",
"version": "2.0.0",
"tarball": "https://registry.npmjs.org/artplayer-plugin-asr/-/artplayer-plugin-asr-2.0.0.tgz",
"integrity": "sha512-nXTf2drMSKnHRbyU9CQ+3o7Jsf1Meam2O9T8LqYakIr18ag/+i71p2x9zdFrLz16Xa6jfQgKeddmU5r3sXp4LA==",
"sha256": "faa726b9536306664d935c95367230e0c622bd046faf636fc2dcef4bbce4cd47",
"registryGitHead": "ca34b47180c94048ee765ab7e146face737eac1c",
"files": {
"package/README.md": "24446684a782c4c179516917a42862809164aac1ae9746e21a09da738e19ca42",
"package/dist/artplayer-plugin-asr.js": "30c7b1fc92a262cda8c3aba63afea9c6c038f52c057039d1c6aa957baed155b6",
"package/dist/artplayer-plugin-asr.legacy.js": "2c2bf48751e4d3cfa88f7313f092d395b61bc0564f6cc44f2c76f0fbcfc66412",
"package/dist/artplayer-plugin-asr.mjs": "98ebdec2c66cecd9d19904a2e54e4b645f7c267b569e560d9bcbeee6028b2267",
"package/package.json": "7545d9bb95b48aedec45c0345f39dd8f0728a24ffe148a04e3740e1ef50239df",
"package/types/artplayer-plugin-asr.d.ts": "2d1ecb04f285b5835744d80dbde3c4a2a124bf1f5ffc995d0d775f5725309d0f"
},
"manifest": {
"name": "artplayer-plugin-asr",
"version": "2.0.0",
"description": "Asr plugin for ArtPlayer",
"author": "Harvey Zack <laozhaochaguan@gmail.com>",
"license": "MIT",
"homepage": "https://artplayer.org",
"repository": {
"type": "git",
"url": "git+https://github.com/zhw2590582/ArtPlayer.git"
},
"bugs": {
"url": "https://github.com/zhw2590582/ArtPlayer/issues"
},
"keywords": [
"html5",
"video",
"player"
],
"exports": {
".": {
"types": "./types/artplayer-plugin-asr.d.ts",
"import": "./dist/artplayer-plugin-asr.mjs",
"require": "./dist/artplayer-plugin-asr.js"
},
"./legacy": {
"types": "./types/artplayer-plugin-asr.d.ts",
"import": "./dist/artplayer-plugin-asr.legacy.js",
"require": "./dist/artplayer-plugin-asr.legacy.js"
}
},
"main": "./dist/artplayer-plugin-asr.js",
"module": "./dist/artplayer-plugin-asr.mjs",
"types": "./types/artplayer-plugin-asr.d.ts",
"legacy": "./dist/artplayer-plugin-asr.legacy.js",
"browserslist": "last 1 Chrome version"
},
"historicalCore": {
"commit": "ca34b47180c94048ee765ab7e146face737eac1c",
"file": "packages/artplayer/package.json",
"version": "5.3.1-beta.1",
"sha256LF": "e1a0a390ec10cd8c1c12006ac6515414ae98a964300dd3e9c288377def948497"
}
}
]
}
@@ -0,0 +1,18 @@
# PKG-ASR-01:固定 ASR 公开契约
源起点为 3b016970f24fea603706a2f4bd50026846f5d939。冻结实际 npm 2.0.0/2.1.0、
12 个归档成员、9 个工作区 Git 文本及各自历史核心 manifest;不覆盖初始 BASE 快照。
交付物为 [契约](../baselines/asr-contract.md)、[来源](../baselines/asr-release.json)、
可重跑的 asr-contract 校验器与 test/asr.test.js。生产行为未改动。
确认接口同步注册、异步 stop、工厂参数快照、字幕替换/HTML/隐藏行为及样式去重。
公开声明与异步行为不一致、队列丢样本及初始化清理疑点分别登记到后续任务,
未把源码观察写成已修复问题。README 的 Ads 误写修正并链接包内维护说明。
验证:`node --test test/asr.test.js refactor/scripts/asr-contract.test.mjs` 32 项通过;
定向 ESLint、计划与风险台账检查通过。未运行全量 CI、真实浏览器、安装消费者或外部 ASR。
本批只固定后续迁移依据,真实音频、旧类型消费与发行验收仍由 ASR-02 至 ASR-06 负责。
无新依赖。增加 test:asr 命令并把公开行为测试接入 test:unit;来源检查在既有
test:baseline 通配入口执行。每次重跑都会校验归档,不静默重写基线。
回退本任务提交可移除这些基线、测试、文档与任务状态,不影响当前播放器运行时。
+3 -2
View File
@@ -4,7 +4,7 @@
基线:`40fcda6a37d0049d42e49c1e64e70d4fd9ba5f7f`。总任务 225 项,范围 22 个包及工作区/示例。
状态:todo 86 / doing 16 / blocked 0 / done 123 / deferred 0。风险 L/M/H 表示兼容风险,不表示工期。
状态:todo 85 / doing 16 / blocked 0 / done 124 / deferred 0。风险 L/M/H 表示兼容风险,不表示工期。
前置依赖是启动条件;验收是完成条件。任务可以继续拆分,但不能复用或悄悄删除旧 ID。
@@ -251,7 +251,7 @@
| ID | 范围 / 步骤 | 前置依赖 | 交付物 | 验收条件 | 风险 | 状态 |
| --- | --- | --- | --- | --- | --- | --- |
| PKG-ASR-01 | artplayer-plugin-asr<br>核对包契约与历史用法 | BASE-05 | length/interval/sampleRate/onAudioChunk、append/hide/stop 的真实能力 | 源码/声明/README/demo/发布包差异已登记;公开形状和版本范围冻结 | H | todo |
| PKG-ASR-01 | artplayer-plugin-asr<br>核对包契约与历史用法 | BASE-05 | length/interval/sampleRate/onAudioChunk、append/hide/stop 的真实能力 | 源码/声明/README/demo/发布包差异已登记;公开形状和版本范围冻结 | H | done |
| PKG-ASR-02 | artplayer-plugin-asr<br>建立特有行为与错误测试 | PKG-ASR-01, ENG-03, ENG-05 | PCM/WAV、chunk 时序、回调慢/拒绝、重复初始化与停止 | 旧版本行为可重跑,成功/失败/切源/销毁有必要断言 | H | todo |
| PKG-ASR-03 | artplayer-plugin-asr<br>整理内部职责与资源 | PKG-ASR-02, CORE-10, CORE-18 | AudioContext/Worklet/Stream 生命周期和背压分离 | 结构变化和缺陷修复分开记录;原 API/事件/资源生命周期通过 | H | todo |
| PKG-ASR-04 | artplayer-plugin-asr<br>迁移自有源码和公开类型 | PKG-ASR-03, ENG-04, ENG-06, CORE-07 | 主线程/Worklet/音频 buffer 与异步回调类型 | 严格类型检查、旧消费样例通过;声明路径/导出和同步异步兼容 | H | todo |
@@ -526,6 +526,7 @@
- PKG-MULTI-SUB-04: [记录](baselines/multiple-subtitles-runtime-types.json) [记录](changes/2026-09-13-PKG-MULTI-SUB-04-runtime-types.md) [记录](baselines/multiple-subtitles-public-types.json) [记录](changes/2026-09-13-PKG-MULTI-SUB-04-public-types.md)
- PKG-MULTI-SUB-07: [记录](baselines/multiple-subtitles-timestamps.json) [记录](changes/2026-09-13-PKG-MULTI-SUB-07-timestamps.md)
- PKG-JASSUB-01: [记录](baselines/jassub-release.json) [记录](baselines/jassub-vendor.json) [记录](baselines/jassub-font-metadata.json) [记录](baselines/jassub-contract.md) [记录](baselines/jassub-contract-validation.json) [记录](changes/2026-09-13-PKG-JASSUB-01-baseline.md)
- PKG-ASR-01: [记录](changes/2026-09-13-PKG-ASR-01-contracts.md) [记录](baselines/asr-contract.md) [记录](baselines/asr-release.json)
- PKG-ADS-01: [记录](baselines/ads-release.json) [记录](baselines/ads-contract.md) [记录](baselines/ads-contract-validation.json) [记录](changes/2026-09-12-PKG-ADS-01-contract.md) [记录](scripts/ads-contract.test.mjs)
- PKG-ADS-02: [记录](changes/2026-09-12-PKG-ADS-02-tests.md) [记录](ads-validation.md) [记录](baselines/ads-validation.json)
- PKG-ADS-03: [记录](changes/2026-09-12-PKG-ADS-03-lifecycle.md) [记录](ads-validation.md) [记录](baselines/ads-lifecycle-validation.json)
+12
View File
@@ -1,5 +1,17 @@
# 进度与证据
## PKG-ASR-01 发布契约完成
冻结实际 npm 2.0.0/2.1.0 共 12 成员及工作区 9 个 Git 输入;32 项契约测试通过,
包含 CJS/global、实际 ESM、字幕 HTML/行数/隐藏、异步 stop 和工厂选项快照。
声明异步差异、音频丢样本及生命周期疑点已分别登记,未将源码疑点写成已修复。
修正 README 的 Ads 误写并增加包内维护地图;无生产源码、依赖或版本变化。
见[契约](baselines/asr-contract.md)和[变更](changes/2026-09-13-PKG-ASR-01-contracts.md)。
225 项:124 done、16 doing、85 todo;205 风险。下一步 ASR 受控音频测试与源码拆分。
用户已授权子代理;当前子代理只写独立音频测试文件,主代理负责契约、台账和提交。
实际浏览器、旧类型消费者和全包发布仍待完成;没有运行外部识别服务。
## CORE-25 无 navigator 默认选项修复完成
defaults.ts 使用 typeof 保护不存在的全局绑定,保留浏览器语言小写、lang 自有字段及每次
+3
View File
@@ -206,3 +206,6 @@
| JASSUB-EXPORT-01 | open / 已复现 | JASSUB historical CommonJS object.default changes to a direct factory in 1.1.0 | PKG-JASSUB-04, PKG-JASSUB-06 |
| CORE-DEFAULTS-SSR-01 | resolved / 已复现 | Reading static defaults throws when navigator is absent | CORE-25 |
| ENG-DIST-PUBLIC-01 | resolved / 已复现 | Vite copied authored declarations into library distribution | ENG-12 |
| ASR-TYPE-01 | open / 源码/产物事实 | ASR declarations omit asynchronous stop and callback text results | PKG-ASR-04 |
| ASR-CHUNK-01 | open / 源码/产物事实 | ASR tick discards underfilled chunks and overrun samples in source | PKG-ASR-02, PKG-ASR-03 |
| ASR-LIFE-01 | open / 源码/产物事实 | ASR partial initialization, pause resume and stale callback cleanup need controlled evidence | PKG-ASR-02, PKG-ASR-03, PKG-ASR-05 |
+48
View File
@@ -4541,6 +4541,54 @@
"refactor/changes/2026-09-13-ENG-12-library-public.md"
],
"resolutionRationale": "Three-format disk fixture and actual core archive prove removal; all other package members remain byte-identical. Full 21-library and docs build passed. CORE-25 runtime work was present but is not closed by this task."
},
{
"id": "ASR-TYPE-01",
"title": "ASR declarations omit asynchronous stop and callback text results",
"confirmation": "source-observed",
"status": "open",
"owners": [
"PKG-ASR-04"
],
"evidence": [
"refactor/baselines/asr-contract.md",
"refactor/baselines/asr-release.json"
],
"compatibleResolution": "Keep historical assignment compatibility while supplying accurate asynchronous consumption.",
"closureCriteria": "Actual old/new installed TS consumers cover void callback, Promise<void>, returned text and factory substitution."
},
{
"id": "ASR-CHUNK-01",
"title": "ASR tick discards underfilled chunks and overrun samples in source",
"confirmation": "source-observed",
"status": "open",
"owners": [
"PKG-ASR-02",
"PKG-ASR-03"
],
"evidence": [
"refactor/baselines/asr-contract.md",
"refactor/baselines/asr-release.json"
],
"compatibleResolution": "Reproduce exact sample loss and retain queue remainders with bounded, ordered callback delivery.",
"closureCriteria": "Exact PCM/WAV byte sequence and slow/rejected callback regression tests pass without losing or reordering accepted input."
},
{
"id": "ASR-LIFE-01",
"title": "ASR partial initialization, pause resume and stale callback cleanup need controlled evidence",
"confirmation": "source-observed",
"status": "open",
"owners": [
"PKG-ASR-02",
"PKG-ASR-03",
"PKG-ASR-05"
],
"evidence": [
"refactor/baselines/asr-contract.md",
"refactor/baselines/asr-release.json"
],
"compatibleResolution": "Own pending setup, URLs, source nodes and callbacks across pause, stop, restart and terminal destruction.",
"closureCriteria": "Failure/race regression tests plus native WebAudio playback and cleanup evidence prove resource ownership."
}
]
}
+31
View File
@@ -0,0 +1,31 @@
import assert from 'node:assert/strict'
import { execFileSync } from 'node:child_process'
import fs from 'node:fs'
import path from 'node:path'
import { archiveFiles, ensureArchive, hash, readMember, refactorDir } from './releases.mjs'
export async function verifyAsrContract() {
const baseline = JSON.parse(fs.readFileSync(path.join(refactorDir, 'baselines/asr-release.json'), 'utf8'))
const archives = new Map()
for (const release of [baseline.release, ...baseline.previous]) {
const archive = await ensureArchive(release)
assert.deepEqual(archiveFiles(archive), Object.keys(release.files).sort())
for (const [file, expected] of Object.entries(release.files))
assert.equal(hash(readMember(archive, file)), expected, `${release.version}/${file}`)
assert.deepEqual(JSON.parse(readMember(archive, 'package/package.json')), release.manifest)
for (const field of ['main', 'module', 'types', 'legacy'])
assert(Object.hasOwn(release.files, `package/${release.manifest[field].replace(/^\.\//, '')}`))
const core = release.historicalCore
const source = execFileSync('git', ['show', `${core.commit}:${core.file}`], { encoding: 'utf8' }).replaceAll('\r\n', '\n')
assert.equal(hash(source), core.sha256LF)
assert.equal(JSON.parse(source).version, core.version)
archives.set(release.version, archive)
}
const sources = new Map()
for (const [file, expected] of Object.entries(baseline.source)) {
const source = execFileSync('git', ['show', `${baseline.sourceCommit}:${file}`], { encoding: 'utf8' }).replaceAll('\r\n', '\n')
assert.equal(hash(source), expected, file)
sources.set(file, source)
}
return { baseline, archives, sources }
}
+46
View File
@@ -0,0 +1,46 @@
import assert from 'node:assert/strict'
import { Buffer } from 'node:buffer'
// eslint-disable-next-line test/no-import-node-test -- Frozen published package evidence uses the repository runner.
import test from 'node:test'
import { verifyAsrContract } from './asr-contract.mjs'
import { readMember } from './releases.mjs'
const { baseline, archives, sources } = await verifyAsrContract()
const releases = [baseline.release, ...baseline.previous]
test('ASR verifies both releases, twelve archive members, nine Git inputs and historical core references', () => {
assert.deepEqual(releases.map(r => r.version), ['2.1.0', '2.0.0'])
assert.equal(releases.reduce((n, r) => n + Object.keys(r.files).length, 0), 12)
assert.equal(sources.size, 9)
assert.deepEqual(releases.map(r => r.historicalCore.version), ['5.3.1', '5.3.1-beta.1'])
for (const release of releases) {
assert.equal(release.manifest.peerDependencies, undefined)
assert.equal(release.manifest.engines, undefined)
}
})
test('ASR old declarations describe void callback/stop despite runtime consuming returned text and returning a Promise', () => {
for (const release of releases) {
const types = readMember(archives.get(release.version), 'package/types/artplayer-plugin-asr.d.ts').toString()
assert.match(types, /onAudioChunk\?: \(chunk: AudioChunk\) => void \| Promise<void>/)
assert.match(types, /stop: \(\) => void/)
assert.match(types, /export default artplayerPluginAsr/)
assert.match(types, /option\?: AsrPluginOption/)
}
})
test('ASR actual published ESM exposes a lazy default factory without requiring a browser', async () => {
for (const release of releases) {
const source = readMember(archives.get(release.version), 'package/dist/artplayer-plugin-asr.mjs')
const module = await import(`data:text/javascript;base64,${Buffer.from(source).toString('base64')}`)
assert.deepEqual(Object.keys(module), ['default'])
assert.equal(typeof module.default(), 'function')
}
})
test('ASR source ships an inline Worklet; network transcription is caller-owned example behavior', () => {
const source = sources.get('packages/artplayer-plugin-asr/src/index.js')
assert.match(source, /registerProcessor\('recorder-processor'/)
assert.doesNotMatch(source, /\bfetch\(|new WebSocket\(/)
const demo = sources.get('docs/assets/example/asr.js')
assert.match(demo, /await fetch\(api\)/)
assert.match(demo, /new WebSocket\(url\)/)
for (const release of releases)
assert(!Object.keys(release.files).some(file => /worklet|worker|\.wasm/.test(file)))
})
+6 -2
View File
@@ -2617,11 +2617,15 @@
"dependsOn": [
"BASE-05"
],
"status": "todo",
"status": "done",
"risk": "H",
"deliverable": "length/interval/sampleRate/onAudioChunk、append/hide/stop 的真实能力",
"acceptance": "源码/声明/README/demo/发布包差异已登记;公开形状和版本范围冻结",
"evidence": []
"evidence": [
"changes/2026-09-13-PKG-ASR-01-contracts.md",
"baselines/asr-contract.md",
"baselines/asr-release.json"
]
},
{
"id": "PKG-ASR-02",
+68
View File
@@ -0,0 +1,68 @@
import assert from 'node:assert/strict'
// eslint-disable-next-line test/no-import-node-test -- Historical ASR contracts use the repository runner.
import test from 'node:test'
import { asrEnvironment, asrHistorical } from './helpers/asr.js'
for (const implementation of await asrHistorical()) {
for (const script of [false, true]) {
test(`ASR ${implementation.name}: ${script ? 'global' : 'CJS'} lazy factory, synchronous result and async stop`, async () => {
const env = asrEnvironment(implementation, { script })
if (!script) {
const namespace = implementation.name.startsWith('published-2.0.0') || implementation.name === 'frozen-source'
assert.equal(typeof env.exported, namespace ? 'object' : 'function')
assert.deepEqual(Object.keys(env.exported), namespace ? ['default'] : [])
}
const register = env.factory()
assert.equal(env.layers.length, 0)
const result = register(env.art)
assert.deepEqual(Object.keys(result), ['name', 'stop', 'hide', 'append'])
assert.equal(result.name, 'artplayerPluginAsr')
assert.equal(result.then, undefined)
assert.equal(result.start, undefined)
assert.equal(env.layers[0].name, 'asr')
assert.deepEqual([...env.listeners.keys()], ['video:volumechange', 'play', 'pause', 'destroy'])
const stopped = result.stop()
assert.equal(typeof stopped.then, 'function')
assert.equal(await stopped, undefined)
assert.equal(env.listeners.get('play').length, 1, 'stop retains historical restart subscription')
assert.equal(env.styles.size, 1)
env.reload()
assert.equal(env.styles.size, 1)
})
}
test(`ASR ${implementation.name}: append replaces punctuation lines, permits markup, and hide retains content`, () => {
const env = asrEnvironment(implementation)
const result = env.factory()(env.art)
assert.equal(result.append('One. Two! 三。 Four?'), undefined)
assert.equal(env.layer.innerHTML, '<div class="art-asr-line">Two!</div><div class="art-asr-line">三。</div><div class="art-asr-line">Four?</div>')
assert.equal([...env.timeouts.values()][0].delay, 10000)
result.append('<b>markup</b>')
assert.equal(env.layer.innerHTML, '<div class="art-asr-line"><b>markup</b></div>')
assert.equal(env.timeouts.size, 1)
result.hide()
assert.equal(env.layer.style.display, 'none')
assert.equal(result.append(null), undefined)
assert.equal(env.layer.style.display, 'none')
assert.equal(env.timeouts.size, 1)
result.append('')
assert.equal(env.layer.innerHTML, '')
assert.equal(env.layer.style.display, '')
;[...env.timeouts.values()][0].callback()
assert.equal(env.layer.style.display, 'none')
})
test(`ASR ${implementation.name}: options are captured at factory call and instances own subtitle timers`, () => {
const option = { length: 1, autoHideTimeout: 25 }
const env = asrEnvironment(implementation)
const register = env.factory(option)
option.length = 4
option.autoHideTimeout = 99
register(env.art).append('one. two.')
assert.equal(env.layer.innerHTML, '<div class="art-asr-line">two.</div>')
assert.equal([...env.timeouts.values()][0].delay, 25)
const other = asrEnvironment(implementation)
other.factory({ length: 0 })(other.art).append('one. two.')
assert.equal(other.layer.innerHTML, '<div class="art-asr-line">one.</div><div class="art-asr-line">two.</div>', 'zero retains historical slice(-0) semantics')
assert.equal(env.timeouts.size, 1)
assert.equal(other.timeouts.size, 1)
})
}
+62
View File
@@ -0,0 +1,62 @@
import assert from 'node:assert/strict'
import vm from 'node:vm'
import { transform } from 'esbuild'
import { verifyAsrContract } from '../../refactor/scripts/asr-contract.mjs'
import { readMember } from '../../refactor/scripts/releases.mjs'
export async function asrHistorical() {
const { baseline, archives, sources } = await verifyAsrContract()
const implementations = []
for (const release of [baseline.release, ...baseline.previous]) {
for (const field of ['main', 'legacy'])
implementations.push({ name: `published-${release.version}-${field}`, code: readMember(archives.get(release.version), `package/${release.manifest[field].replace(/^\.\//, '')}`).toString() })
}
const prefix = 'packages/artplayer-plugin-asr'
const source = sources.get(`${prefix}/src/index.js`).replace('import style from \'./style.less?inline\'', 'const style = \'frozen-source-style\'')
implementations.push({ name: 'frozen-source', code: (await transform(source, { format: 'cjs', target: 'es2020' })).code, scriptCode: (await transform(source, { format: 'iife', globalName: 'artplayerPluginAsr', target: 'es2020' })).code })
for (const suffix of ['js', 'legacy.js'])
implementations.push({ name: `frozen-workspace-${suffix}`, code: sources.get(`${prefix}/dist/artplayer-plugin-asr.${suffix}`) })
return implementations
}
export function asrEnvironment(implementation, { script = false } = {}) {
const listeners = new Map()
const timeouts = new Map()
const styles = new Map()
const layer = { style: {}, innerHTML: '' }
let id = 0
const context = vm.createContext({
console,
setTimeout(callback, delay) {
timeouts.set(++id, { callback, delay })
return id
},
clearTimeout(timer) { timeouts.delete(timer) },
document: {
getElementById: id => styles.get(id),
createElement: () => ({}),
head: { appendChild(element) { styles.set(element.id, element) } },
},
})
context.window = context
if (!script) {
context.module = { exports: {} }
context.exports = context.module.exports
}
const code = script ? implementation.scriptCode || implementation.code : `(function () {\n${implementation.code}\n}).call(globalThis)`
vm.runInContext(code, context, { timeout: 1000 })
const exported = script ? context.artplayerPluginAsr : context.module.exports
const factory = typeof exported === 'function' ? exported : exported.default
assert.equal(typeof factory, 'function', implementation.name)
const layers = []
const art = {
video: {},
volume: 0.5,
layers: { add(option) {
layers.push(option)
return layer
} },
on(name, callback) { listeners.set(name, [...listeners.get(name) || [], callback]) },
}
return { context, factory, exported, styles, layer, layers, listeners, timeouts, art, reload: () => vm.runInContext(code, context), emit: name => Promise.all((listeners.get(name) || []).map(callback => callback())) }
}