akari-video 0.1.45 → 0.1.46
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +1 -1
- package/vendor/.akari-capability-sources.json +1 -0
- package/vendor/docs/contract-2026-08-28-gpu-export-v0.md +3 -0
- package/vendor/docs/contract-2026-08-28-v2-audio-roles-v0.md +5 -0
- package/vendor/docs/contract-2026-09-06-vgpu-layer-v0.md +166 -0
- package/vendor/packages/akari-launcher/package.json +1 -1
- package/vendor/packages/edit-store/lib/audio-schedule.d.ts +1 -0
- package/vendor/packages/edit-store/lib/audio-schedule.js +2 -0
- package/vendor/packages/edit-store/lib/edit-v2.d.ts +4 -0
- package/vendor/packages/edit-store/lib/edit-v2.js +5 -1
- package/vendor/packages/edit-store/lib/generated/edit-v2-keys.d.ts +2 -2
- package/vendor/packages/edit-store/lib/generated/edit-v2-keys.js +3 -1
- package/vendor/packages/edit-store/lib/internal-model.js +2 -0
- package/vendor/packages/edit-store/lib/webview-kernel.js +8 -1
- package/vendor/packages/gpu-export/README.ja.md +2 -0
- package/vendor/packages/gpu-export/README.md +2 -0
- package/vendor/packages/media-bin/src/preview-audio-sidecar.mjs +89 -14
- package/vendor/packages/media-bin/test/preview-audio-slot-order.test.mjs +306 -5
- package/vendor/packages/overlay-runtime/README.md +60 -0
- package/vendor/packages/schemas/edit.schema.json +3 -1
- package/vendor/packages/schemas/engine-capabilities.json +2 -0
- package/vendor/packages/schemas/test/edit-v2-schema.test.mjs +12 -0
- package/vendor/packages/schemas/test/engine-capabilities.test.mjs +2 -2
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "akari-video",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.46",
|
|
4
4
|
"description": "AKARI Video launcher CLI — start an AI-edited video project from any directory: scaffold, connection check, then hand over to Claude Code (or opencode). AKARI Video を opencode や Claude Code で、どのディレクトリからでも始めるための `akari` ランチャー CLI。接続確認(doctor)→ 未セットアップならプロジェクト雛形を作成 → AI エージェントを起動する。外部 npm 依存ゼロ(Node.js 組み込みモジュールのみ)。",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -64,6 +64,7 @@
|
|
|
64
64
|
"docs/contract-2026-09-03-clip-adjust-v0.md",
|
|
65
65
|
"docs/contract-2026-09-03-preview-playback-rate-v1.md",
|
|
66
66
|
"docs/contract-2026-09-05-clip-adjust-v1.md",
|
|
67
|
+
"docs/contract-2026-09-06-vgpu-layer-v0.md",
|
|
67
68
|
"packages/akari-launcher/package.json",
|
|
68
69
|
"packages/akari-launcher/README.md",
|
|
69
70
|
"packages/akari-tools/package.json",
|
|
@@ -21,6 +21,7 @@
|
|
|
21
21
|
|---|---|---|
|
|
22
22
|
| `same` | はい | 静的 HTML は起動時、対応済み字幕は unit の初回活性時に 1 回だけスプライト化する |
|
|
23
23
|
| `three` | はい | JSON の宣言型 3D scene と描画先 canvas を持つ overlay。毎コマ Three.js canvas を更新し、登場表現は `three-scene-entrance-curve`、`three-scene-entrance-sampled`、または `three-scene-sampled-composite` で処理する |
|
|
24
|
+
| `vgpu` | はい | pure な `data-akari-vgpu-scene` を WebGPU canvas に描き、毎コマスプライトへ転送する([vgpu v0 契約](./contract-2026-09-06-vgpu-layer-v0.md)) |
|
|
24
25
|
| `degraded` | いいえ | raster 自体は可能でも live DOM と同じ時間変化を保証できない |
|
|
25
26
|
| `unsupported` | いいえ | v0 の表現範囲外であり、正しい完成画を生成できない |
|
|
26
27
|
|
|
@@ -320,6 +321,8 @@ CSS 3D は次の 3 群に分けて判定する。
|
|
|
320
321
|
子孫の描画域が画面上で交差し、手前の要素が DOM 上で先に描かれる矛盾対を検出した場合は警告するが、
|
|
321
322
|
fail-closed にはしない。
|
|
322
323
|
|
|
324
|
+
vgpu の理由語彙は `vgpu-scene-canvas-direct`、`vgpu-stateful-unsupported`、`vgpu-invalid-declaration`、`vgpu-condition:<条件をカンマ連結>`([vgpu v0 契約](./contract-2026-09-06-vgpu-layer-v0.md))。
|
|
325
|
+
|
|
323
326
|
次の条件は fail-closed のまま `degraded` とし、receipt に overlay id、理由、検出条件を全件残す。
|
|
324
327
|
|
|
325
328
|
- `iframe`、`object`、`embed` の埋め込み context。
|
|
@@ -128,6 +128,11 @@ ms へ換算し、絶対値で集計する。全 59 点の最大は 16.667 ms、
|
|
|
128
128
|
書き出しはこの speech 鍵を消費する。プレビューサーバーへ speech 鍵を渡す配線は同契約の範囲外で、
|
|
129
129
|
配線されるまでは narration 鍵だけへ劣化する。
|
|
130
130
|
|
|
131
|
+
`tracks[].items[].source`(`kind: 'media'`)の `gain_db`(-60〜12・省略時 0)と
|
|
132
|
+
`mute`(boolean・省略時 false)で、カットごとに埋め込み音声を減衰 / ミュートできる。
|
|
133
|
+
`mute: true` は speech 宣言そのものを作らず、プレビューのサイドカー要求も PCM 化のコストも消える。
|
|
134
|
+
書き出しは無音区間で尺を保つ。キー無しの既定値は従来と完全同一で、既存案件の出力は変わらない。
|
|
135
|
+
|
|
131
136
|
## 7. 速度変更した台詞のピッチ保持サイドカー
|
|
132
137
|
|
|
133
138
|
`speed` が 1 以外の撮影素材の台詞は、Web Audio の `playbackRate` による近似から外す。
|
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
---
|
|
2
|
+
lifecycle: accepted
|
|
3
|
+
date: 2026-09-06
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# vgpu レイヤー v0 契約
|
|
7
|
+
|
|
8
|
+
## 1. 適用範囲
|
|
9
|
+
|
|
10
|
+
`data-akari-vgpu-scene` に宣言した WebGPU のフルスクリーン fragment パスを、
|
|
11
|
+
`window.akari.vgpuRuntime` が外部から渡された時刻で描く。vgpu 0.4.0 は vendor 同梱し、
|
|
12
|
+
実行時の依存取得は行わない。Three.js と同じ overlay sheet に置き、GPU 書き出しでは
|
|
13
|
+
canvas を SpriteCompositor に登録する。毎コマ `render` の直後に `updateSprite` する。
|
|
14
|
+
WebGPU canvas の提示面は一時的なので、描画せずに前回の canvas を転送してはならない。
|
|
15
|
+
CPU への画素読み戻しはこの経路に追加しない。ブラウザ内部のゼロコピーを保証するものではない。
|
|
16
|
+
|
|
17
|
+
## 2. 宣言スキーマ
|
|
18
|
+
|
|
19
|
+
断片に次の script をちょうど 1 個置く。canvas は任意で、省略時はランタイムが作成する。
|
|
20
|
+
|
|
21
|
+
```html
|
|
22
|
+
<canvas></canvas>
|
|
23
|
+
<script type="application/json" data-akari-vgpu-scene>
|
|
24
|
+
{
|
|
25
|
+
"version": 0,
|
|
26
|
+
"mode": "pure",
|
|
27
|
+
"alphaMode": "premultiplied",
|
|
28
|
+
"seed": 0,
|
|
29
|
+
"uniforms": { "speed": 1 },
|
|
30
|
+
"passes": [{
|
|
31
|
+
"id": "main",
|
|
32
|
+
"wgsl": "struct Params { speed: f32 }; @group(0) @binding(1) var<uniform> params: Params; @fragment fn fs_main(@builtin(position) p: vec4f) -> @location(0) vec4f { let uv = akari_uv(p); return vec4f(uv, 0.5 + 0.5 * sin(akari.time * params.speed), 1.0); }",
|
|
33
|
+
"inputs": [],
|
|
34
|
+
"scale": 1
|
|
35
|
+
}]
|
|
36
|
+
}
|
|
37
|
+
</script>
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
| 項目 | 制約・既定値 |
|
|
41
|
+
|---|---|
|
|
42
|
+
| `version` | 必須、数値 `0` のみ |
|
|
43
|
+
| `mode` | 必須、`pure` のみ。`stateful` は予約語で拒否 |
|
|
44
|
+
| `alphaMode` | 省略時 `premultiplied`。それ以外は拒否。シェーダー出力の RGB も alpha 乗算済みにする |
|
|
45
|
+
| `seed` | 任意の有限数。省略時 `0` |
|
|
46
|
+
| `uniforms` | 任意の plain object。値は有限数、または長さ 2〜4 の有限数配列。省略時 `{}` |
|
|
47
|
+
| `passes` | 必須、1 個以上の配列 |
|
|
48
|
+
| `passes[].id` | 必須、`^[A-Za-z0-9_-]+$`、一意 |
|
|
49
|
+
| `passes[].wgsl` | 必須、空白だけではない WGSL 文字列 |
|
|
50
|
+
| `passes[].inputs` | 先行パス id の文字列配列、最大 8 個。省略時 `[]`。自己参照・前方参照不可 |
|
|
51
|
+
| `passes[].scale` | 正の有限数、省略時 `1`。最終パスでは無視する |
|
|
52
|
+
|
|
53
|
+
未知のトップレベルキー・pass キー、不正 JSON、不正な値は `readDescriptor` が TypeError にする。
|
|
54
|
+
`uniforms` のキーは WGSL `Params` のメンバーに対応し、断片が
|
|
55
|
+
`@group(0) @binding(1) var<uniform> params: Params;` を自分で宣言する。
|
|
56
|
+
型は `f32` / `vec2f` / `vec3f` / `vec4f`。params 宣言のないパスには params を set しない。
|
|
57
|
+
未宣言名・未束縛・WGSL コンパイルエラーは vgpu の検証を通じて失敗する。
|
|
58
|
+
|
|
59
|
+
`pure` は「出力が指定時刻・seed・uniforms の純関数」の authoring 契約である。
|
|
60
|
+
任意時刻へ直接シークでき、前回描画結果への feedback はない。任意の WGSL の数学的な純粋性を
|
|
61
|
+
静的に証明するものではない。時刻を進める時計や乱数源を断片へ提供しない。
|
|
62
|
+
|
|
63
|
+
## 3. 固定 prelude とパス
|
|
64
|
+
|
|
65
|
+
各パスのソース先頭に次を付加する。
|
|
66
|
+
|
|
67
|
+
```wgsl
|
|
68
|
+
struct AkariUniforms { time: f32, aspect: f32, width: f32, height: f32, seed: f32, pad: vec3f };
|
|
69
|
+
@group(0) @binding(0) var<uniform> akari: AkariUniforms;
|
|
70
|
+
fn akari_uv(pos: vec4f) -> vec2f { return pos.xy / vec2f(akari.pad.x, akari.pad.y); }
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
入力があるパスには、入力数だけ texture 宣言と、共通 sampler 宣言を続ける。断片側で重複宣言しない。
|
|
74
|
+
|
|
75
|
+
```wgsl
|
|
76
|
+
@group(1) @binding(0) var input_0: texture_2d<f32>;
|
|
77
|
+
// input_1 ... input_7 は必要な個数だけ binding 1 ... 7 に宣言
|
|
78
|
+
@group(1) @binding(8) var input_sampler: sampler;
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
パスは配列順に、1 回の `frame(gpu, f => ...)` で直列描画する。
|
|
82
|
+
最後のパスだけが surface、それ以前は `scale` 倍のオフスクリーン target へ描く。
|
|
83
|
+
`input_i` には参照先 target の `color` を渡す。resize 後は新しい texture を再束縛する。
|
|
84
|
+
`effect.draw(surface)` は使用しない(0.4.0 の surface は `frame` の `pass` で描く)。
|
|
85
|
+
|
|
86
|
+
`akari.width/height` は常に等倍出力の container 実寸で、`aspect = width / height`。
|
|
87
|
+
**`pad` は予約した形を維持しつつ実値を運ぶ**: x/y は当該パスの描画バッファ実幅・実高、z は previewScale。
|
|
88
|
+
最終パスの x/y は canvas 実寸、中間パスはさらに pass.scale を掛けた target 実寸。
|
|
89
|
+
固定 struct を変えず、`@builtin(position)` のピクセル座標を解像度に依存せず正規化するための裁定補足である。
|
|
90
|
+
断片は `akari_uv(position)`、または vgpu の頂点段が供給する左上原点の `@location(0) uv` を使う。
|
|
91
|
+
`position.xy / vec2f(akari.width, akari.height)` は縮小時に構図が変わるので使わない。
|
|
92
|
+
|
|
93
|
+
## 4. API・解像度・ツマミ
|
|
94
|
+
|
|
95
|
+
- `probe()` はページで 1 回だけ `init()` し、同じ Promise を返す。64×64 一時 surface に 2 コマ描画し、
|
|
96
|
+
GPU queue 完了と検証完了を待つ。device lost を初期化直後から監視し、一時 surface を破棄する。
|
|
97
|
+
成功値は `{ ok: true, adapter: { vendor, architecture }, ms }`。情報取得不可の文字列は空文字。
|
|
98
|
+
- `render(container, localTimeSeconds, { previewScale })` は同期描画。時刻の源は引数のみ。
|
|
99
|
+
未初期化時は probe を開始して loading のまま戻る。書き出しホストは必ず先に probe を await する。
|
|
100
|
+
描画の同期エラーは `VGPU-RENDER:` を投げ、以後その container は no-op とする。
|
|
101
|
+
書き出しホストは inspect の error も確認し、空の層を成功扱いしない。
|
|
102
|
+
- `inspect(container)` は `{ status, adapter, passes, previewScale, drawCount, deviceLost }`。
|
|
103
|
+
status は `idle`(宣言なし)、`loading`、`ready`、`error`。passes は構築したパス数。
|
|
104
|
+
- `dispose(container)` は surface・target と自動作成 canvas を破棄する。共有 device は維持する。
|
|
105
|
+
- `readDescriptor(container)` は宣言なしなら null、宣言不正なら TypeError。
|
|
106
|
+
|
|
107
|
+
previewScale は辺あたり倍率で、未指定 `1`。推奨値は `1` / `0.5` / `0.25` だが、
|
|
108
|
+
実装は有限数 `0 < s <= 1` を受け付ける。範囲外は警告 1 回で `1` にする。
|
|
109
|
+
preview-server の既定は `PREVIEW_VGPU_SCALE = 0.5`。書き出しは options を渡さず常に `1`。
|
|
110
|
+
canvas の CSS は absolute / inset 0 / width・height 100% / display block に固定する。
|
|
111
|
+
内部画素だけが減り、位置・大きさ・時刻・ツマミは変わらない。描画寸法は丸め、最小 1 px。
|
|
112
|
+
container が 0 サイズなら前回寸法を使い、前回もなければ描画しない。
|
|
113
|
+
GPU 合成側は container の解決済み配置変換をスプライトへ渡し、プレビューの位置と拡縮を維持する。
|
|
114
|
+
|
|
115
|
+
`--vgpu-<key>` が container の CSS にある場合は uniforms より優先する。
|
|
116
|
+
数値または空白・カンマ区切りのベクトルを読み、宣言と同じ要素数で全部有限数の場合だけ採用する。
|
|
117
|
+
不一致はキーごとに警告 1 回で宣言値を使う。既存の `vars` からそのまま設定できる。
|
|
118
|
+
|
|
119
|
+
## 5. 適格性と receipt
|
|
120
|
+
|
|
121
|
+
`item-keyframes` の後、three-only 判定の前に vgpu 判定を置く。
|
|
122
|
+
検出条件は `vgpu-runtime`、canvas があればその次に `three-or-canvas-runtime`。
|
|
123
|
+
|
|
124
|
+
| 分類 | 理由 |
|
|
125
|
+
|---|---|
|
|
126
|
+
| `vgpu` | `vgpu-scene-canvas-direct`: pure 宣言が妥当で、他条件が canvas 由来だけ |
|
|
127
|
+
| `degraded` | `vgpu-stateful-unsupported`: stateful |
|
|
128
|
+
| `degraded` | `vgpu-invalid-declaration`: JSON・version・mode・スキーマ不正 |
|
|
129
|
+
| `degraded` | `vgpu-condition:<条件をカンマ連結>`: 他の動的条件や外部参照を含む |
|
|
130
|
+
|
|
131
|
+
3D 宣言が同居する場合は、優先して
|
|
132
|
+
`vgpu-condition:three-or-canvas-runtime(data-akari-3d-scene)` とする。
|
|
133
|
+
その後、他条件、宣言検証の順に判定する。JSON はちょうど 1 宣言だけを受け付ける。
|
|
134
|
+
`forceDegraded` は pure の vgpu 分類を変更しない。degraded になった vgpu は既存の規則どおり
|
|
135
|
+
`forced-dom:<元理由>` で DOM 強制の対象となり、eligible は false のまま残る。
|
|
136
|
+
|
|
137
|
+
`summary.vgpu` は件数が 1 以上のときだけ追加する。`spriteManifest.vgpu` は常に配列で、
|
|
138
|
+
各要素は `{ id, start, duration, index, z }`。overlay sheet は three と vgpu のいずれかがある場合だけ作る。
|
|
139
|
+
receipt の `gpu.vgpu` は run に vgpu があるときだけ、次の形で追加する。
|
|
140
|
+
|
|
141
|
+
```json
|
|
142
|
+
{ "overlays": 2, "adapter": { "vendor": "apple", "architecture": "metal-3" },
|
|
143
|
+
"previewScale": null, "deviceLost": false, "probeMs": 12.5 }
|
|
144
|
+
```
|
|
145
|
+
|
|
146
|
+
overlays は有限非負整数、adapter は文字列、previewScale は数値または null、
|
|
147
|
+
deviceLost は boolean、probeMs は有限非負数または null に正規化する。
|
|
148
|
+
summary と receipt を条件付きにしたのは、vgpu を使わない既存 fixture・receipt を byte/deep-equal で保つため。
|
|
149
|
+
vgpu 不在の overlay sheet には新しい script や seek 文を一切挿入しない。
|
|
150
|
+
|
|
151
|
+
## 6. fail-loud と v0 の限界
|
|
152
|
+
|
|
153
|
+
- WebGPU 未提供・adapter null・初期化/試験描画失敗は `VGPU-UNAVAILABLE:` で probe を reject。
|
|
154
|
+
- device lost 後の render は `VGPU-DEVICE-LOST:` で throw。復帰や別 device への切り替えは行わない。
|
|
155
|
+
- `auto` でも `VGPU-` を含む失敗は OSR へフォールバックせずそのまま伝播する。
|
|
156
|
+
- プレビューは失敗を捕捉し `[data-akari-vgpu-fallback]`(任意、断片の既定 display:none)を表示して、
|
|
157
|
+
console.warn を 1 回出す。他のプレビューは続行する。
|
|
158
|
+
|
|
159
|
+
CLI の `--engine legacy` は既に廃止され、引数処理で拒否される。
|
|
160
|
+
その CLI のエラーを変更せず、`renderProject(..., { engine: 'legacy' })` の API 直呼びには
|
|
161
|
+
vgpu 宣言を検出した時点で `vgpu overlays require --engine gpu` を返す。これが元裁定の
|
|
162
|
+
「engine 解決後の legacy 拒否」と異なる理由は、CLI からその分岐に到達できないためである。
|
|
163
|
+
|
|
164
|
+
stateful・feedback・固定ステップ replay は未対応。OSR は同じ sheet を通すが v0 のパリティ gate 対象外。
|
|
165
|
+
Windows / Linux / 他 GPU は未検証。同一機での 2 走・全コマ SHA 一致は実 GPU の検収で確認する事項であり、
|
|
166
|
+
異機種間の浮動小数点の一致を保証しない。SwiftShader を WebGPU の代替として採用しない。
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "akari-video",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.46",
|
|
4
4
|
"description": "AKARI Video launcher CLI — start an AI-edited video project from any directory: scaffold, connection check, then hand over to Claude Code (or opencode). AKARI Video を opencode や Claude Code で、どのディレクトリからでも始めるための `akari` ランチャー CLI。接続確認(doctor)→ 未セットアップならプロジェクト雛形を作成 → AI エージェントを起動する。外部 npm 依存ゼロ(Node.js 組み込みモジュールのみ)。 [akari-video npm vendor: bin/akari.mjs is reference-only. These CLI entrypoints are not included in the akari-video npm package. Use `akari doctor --json` and run the path reported in `render_cut.path`. Full installations provide it in a monorepo checkout, ~/.akari/app, /Applications/AKARI Video.app/Contents/Resources/packages, or %LOCALAPPDATA%\\Programs\\@akari-videoshell\\resources\\packages.]",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"files": [
|
|
@@ -299,6 +299,8 @@ function projectSpeechDeclarations(cuts, options) {
|
|
|
299
299
|
const cut = normalizedCuts[segment.cutIndex];
|
|
300
300
|
if (!cut || typeof cut.src !== 'string' || !cut.src)
|
|
301
301
|
continue;
|
|
302
|
+
if (cut.mute === true)
|
|
303
|
+
continue;
|
|
302
304
|
const speed = finitePositive(cut.speed) ? cut.speed : 1;
|
|
303
305
|
const segmentIn = typeof segment.in === 'number' ? segment.in : cut.in;
|
|
304
306
|
const cutTimelineStart = segment.outStart - (segmentIn - cut.in) / speed;
|
|
@@ -91,6 +91,10 @@ export interface MediaSourceV2 {
|
|
|
91
91
|
freeze?: Record<string, unknown> | null;
|
|
92
92
|
fx?: unknown[];
|
|
93
93
|
speed?: number;
|
|
94
|
+
/** 埋め込み音声(speech)の音量 dB。-60〜12・省略時 0。 */
|
|
95
|
+
gain_db?: number;
|
|
96
|
+
/** 埋め込み音声(speech)のミュート。省略時 false。 */
|
|
97
|
+
mute?: boolean;
|
|
94
98
|
chroma_key?: Record<string, unknown> | null;
|
|
95
99
|
}
|
|
96
100
|
export interface AudioMediaSourceV2 {
|
|
@@ -302,7 +302,7 @@ function validateItemSource(value, path, sourceIds) {
|
|
|
302
302
|
switch (value.kind) {
|
|
303
303
|
case 'media':
|
|
304
304
|
requireExactKeys(value, new Set([
|
|
305
|
-
'kind', 'src', 'in', 'out', 'framing', 'transition_out', 'freeze', 'fx', 'speed', 'chroma_key'
|
|
305
|
+
'kind', 'src', 'in', 'out', 'framing', 'transition_out', 'freeze', 'fx', 'speed', 'chroma_key', 'gain_db', 'mute'
|
|
306
306
|
]), path);
|
|
307
307
|
requireText(value.src, `${path}.src`);
|
|
308
308
|
if (!sourceIds.has(value.src))
|
|
@@ -319,6 +319,10 @@ function validateItemSource(value, path, sourceIds) {
|
|
|
319
319
|
throw invalid(`${path}.fx`, '配列である必要があります');
|
|
320
320
|
if (hasOwn(value, 'speed'))
|
|
321
321
|
requirePositiveNumber(value.speed, `${path}.speed`);
|
|
322
|
+
if (hasOwn(value, 'gain_db'))
|
|
323
|
+
requireRange(value.gain_db, -60, 12, `${path}.gain_db`);
|
|
324
|
+
if (hasOwn(value, 'mute') && typeof value.mute !== 'boolean')
|
|
325
|
+
throw invalid(`${path}.mute`, 'boolean である必要があります');
|
|
322
326
|
return;
|
|
323
327
|
case 'html':
|
|
324
328
|
requireExactKeys(value, new Set(['kind', 'path', 'part', 'style', 'text', 'exclude', 'derivedFrom', 'vars', 'params']), path);
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
export declare const ITEM_V2_KEYS: readonly ["id", "name", "hidden", "locked", "at", "duration", "anchor", "transform", "opacity", "blend", "crop", "adjust", "perspective", "motion", "animator", "keyframes", "items", "mask", "source", "role", "gain_db", "denoise", "lowcut_hz", "fade_in", "fade_out", "ducking", "duck_db", "duck_attack", "duck_release", "script", "reading", "provenance"];
|
|
2
|
-
export declare const ITEM_SOURCE_V2_KEYS: readonly ["kind", "src", "in", "out", "framing", "transition_out", "freeze", "fx", "speed", "chroma_key", "pitch_semitones", "formant", "path", "part", "style", "text", "exclude", "derivedFrom", "vars", "params", "shape", "preset", "baked", "from", "filter", "id"];
|
|
2
|
+
export declare const ITEM_SOURCE_V2_KEYS: readonly ["kind", "src", "in", "out", "framing", "transition_out", "freeze", "fx", "speed", "gain_db", "mute", "chroma_key", "pitch_semitones", "formant", "path", "part", "style", "text", "exclude", "derivedFrom", "vars", "params", "shape", "preset", "baked", "from", "filter", "id"];
|
|
3
3
|
export declare const KEYFRAME_V2_KEYS: readonly ["t", "transform", "crop", "perspective", "opacity", "gain_db", "animator", "easing"];
|
|
4
4
|
export declare const MOTION_V0_KEYS: readonly ["in", "out", "loop"];
|
|
5
5
|
export declare const ANIMATOR_V0_KEYS: readonly ["id", "basis", "shape", "start", "end", "offset", "randomize", "amount", "ease"];
|
|
@@ -17,7 +17,7 @@ export declare const ITEM_V2_KEYS_BY_DEFINITION: {
|
|
|
17
17
|
readonly itemV2AudioMedia: readonly ["id", "name", "hidden", "locked", "at", "duration", "role", "source", "gain_db", "denoise", "lowcut_hz", "keyframes", "fade_in", "fade_out", "ducking", "duck_db", "duck_attack", "duck_release", "script", "reading", "provenance"];
|
|
18
18
|
};
|
|
19
19
|
export declare const ITEM_SOURCE_V2_KEYS_BY_DEFINITION: {
|
|
20
|
-
readonly itemSourceMediaV2: readonly ["kind", "src", "in", "out", "framing", "transition_out", "freeze", "fx", "speed", "chroma_key"];
|
|
20
|
+
readonly itemSourceMediaV2: readonly ["kind", "src", "in", "out", "framing", "transition_out", "freeze", "fx", "speed", "gain_db", "mute", "chroma_key"];
|
|
21
21
|
readonly itemSourceAudioMediaV2: readonly ["kind", "src", "in", "out", "speed", "pitch_semitones", "formant"];
|
|
22
22
|
readonly itemSourceHtmlV2: readonly ["kind", "path", "part", "style", "text", "exclude", "derivedFrom", "vars", "params"];
|
|
23
23
|
readonly itemSourceShapeV2: readonly ["kind", "shape", "params"];
|
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
5
5
|
exports.ITEM_SOURCE_V2_KEYS_BY_DEFINITION = exports.ITEM_V2_KEYS_BY_DEFINITION = exports.SOURCE_KIND_V2 = exports.MOTION_FILE_V0_KEYS = exports.ANIMATOR_V0_KEYS = exports.MOTION_V0_KEYS = exports.KEYFRAME_V2_KEYS = exports.ITEM_SOURCE_V2_KEYS = exports.ITEM_V2_KEYS = void 0;
|
|
6
6
|
exports.ITEM_V2_KEYS = ["id", "name", "hidden", "locked", "at", "duration", "anchor", "transform", "opacity", "blend", "crop", "adjust", "perspective", "motion", "animator", "keyframes", "items", "mask", "source", "role", "gain_db", "denoise", "lowcut_hz", "fade_in", "fade_out", "ducking", "duck_db", "duck_attack", "duck_release", "script", "reading", "provenance"];
|
|
7
|
-
exports.ITEM_SOURCE_V2_KEYS = ["kind", "src", "in", "out", "framing", "transition_out", "freeze", "fx", "speed", "chroma_key", "pitch_semitones", "formant", "path", "part", "style", "text", "exclude", "derivedFrom", "vars", "params", "shape", "preset", "baked", "from", "filter", "id"];
|
|
7
|
+
exports.ITEM_SOURCE_V2_KEYS = ["kind", "src", "in", "out", "framing", "transition_out", "freeze", "fx", "speed", "gain_db", "mute", "chroma_key", "pitch_semitones", "formant", "path", "part", "style", "text", "exclude", "derivedFrom", "vars", "params", "shape", "preset", "baked", "from", "filter", "id"];
|
|
8
8
|
exports.KEYFRAME_V2_KEYS = ["t", "transform", "crop", "perspective", "opacity", "gain_db", "animator", "easing"];
|
|
9
9
|
exports.MOTION_V0_KEYS = ["in", "out", "loop"];
|
|
10
10
|
exports.ANIMATOR_V0_KEYS = ["id", "basis", "shape", "start", "end", "offset", "randomize", "amount", "ease"];
|
|
@@ -204,6 +204,8 @@ exports.ITEM_SOURCE_V2_KEYS_BY_DEFINITION = {
|
|
|
204
204
|
"freeze",
|
|
205
205
|
"fx",
|
|
206
206
|
"speed",
|
|
207
|
+
"gain_db",
|
|
208
|
+
"mute",
|
|
207
209
|
"chroma_key"
|
|
208
210
|
],
|
|
209
211
|
"itemSourceAudioMediaV2": [
|
|
@@ -962,6 +962,8 @@ function copyMediaSourceFields(source) {
|
|
|
962
962
|
...(source.freeze !== undefined ? { freeze: source.freeze } : {}),
|
|
963
963
|
...(source.fx !== undefined ? { fx: source.fx } : {}),
|
|
964
964
|
...(source.speed !== undefined ? { speed: source.speed } : {}),
|
|
965
|
+
...(source.gain_db !== undefined ? { gain_db: source.gain_db } : {}),
|
|
966
|
+
...(source.mute !== undefined ? { mute: source.mute } : {}),
|
|
965
967
|
...(source.chroma_key !== undefined ? { chroma_key: source.chroma_key } : {})
|
|
966
968
|
};
|
|
967
969
|
}
|
|
@@ -1437,6 +1437,7 @@ var AkariEditKernel = (() => {
|
|
|
1437
1437
|
if (segment.kind !== "src" || segment.cutIndex === null) continue;
|
|
1438
1438
|
const cut = normalizedCuts[segment.cutIndex];
|
|
1439
1439
|
if (!cut || typeof cut.src !== "string" || !cut.src) continue;
|
|
1440
|
+
if (cut.mute === true) continue;
|
|
1440
1441
|
const speed = finitePositive(cut.speed) ? cut.speed : 1;
|
|
1441
1442
|
const segmentIn = typeof segment.in === "number" ? segment.in : cut.in;
|
|
1442
1443
|
const cutTimelineStart = segment.outStart - (segmentIn - cut.in) / speed;
|
|
@@ -2018,7 +2019,9 @@ var AkariEditKernel = (() => {
|
|
|
2018
2019
|
"freeze",
|
|
2019
2020
|
"fx",
|
|
2020
2021
|
"speed",
|
|
2021
|
-
"chroma_key"
|
|
2022
|
+
"chroma_key",
|
|
2023
|
+
"gain_db",
|
|
2024
|
+
"mute"
|
|
2022
2025
|
]), path);
|
|
2023
2026
|
requireText(value.src, `${path}.src`);
|
|
2024
2027
|
if (!sourceIds.has(value.src)) throw invalid(`${path}.src`, `sources[].id \u306B\u5B58\u5728\u3057\u307E\u305B\u3093: ${value.src}`);
|
|
@@ -2030,6 +2033,8 @@ var AkariEditKernel = (() => {
|
|
|
2030
2033
|
}
|
|
2031
2034
|
if (hasOwn(value, "fx") && !Array.isArray(value.fx)) throw invalid(`${path}.fx`, "\u914D\u5217\u3067\u3042\u308B\u5FC5\u8981\u304C\u3042\u308A\u307E\u3059");
|
|
2032
2035
|
if (hasOwn(value, "speed")) requirePositiveNumber(value.speed, `${path}.speed`);
|
|
2036
|
+
if (hasOwn(value, "gain_db")) requireRange(value.gain_db, -60, 12, `${path}.gain_db`);
|
|
2037
|
+
if (hasOwn(value, "mute") && typeof value.mute !== "boolean") throw invalid(`${path}.mute`, "boolean \u3067\u3042\u308B\u5FC5\u8981\u304C\u3042\u308A\u307E\u3059");
|
|
2033
2038
|
return;
|
|
2034
2039
|
case "html":
|
|
2035
2040
|
requireExactKeys(value, /* @__PURE__ */ new Set(["kind", "path", "part", "style", "text", "exclude", "derivedFrom", "vars", "params"]), path);
|
|
@@ -3254,6 +3259,8 @@ var AkariEditKernel = (() => {
|
|
|
3254
3259
|
...source.freeze !== void 0 ? { freeze: source.freeze } : {},
|
|
3255
3260
|
...source.fx !== void 0 ? { fx: source.fx } : {},
|
|
3256
3261
|
...source.speed !== void 0 ? { speed: source.speed } : {},
|
|
3262
|
+
...source.gain_db !== void 0 ? { gain_db: source.gain_db } : {},
|
|
3263
|
+
...source.mute !== void 0 ? { mute: source.mute } : {},
|
|
3257
3264
|
...source.chroma_key !== void 0 ? { chroma_key: source.chroma_key } : {}
|
|
3258
3265
|
};
|
|
3259
3266
|
}
|
|
@@ -236,3 +236,5 @@ frame-engine bundle は生成物です。`generated/frame-engine.js` を直接
|
|
|
236
236
|
生フレーム hash は隔離した検証専用 module からだけ利用でき、実行時 readback trap とは同時に
|
|
237
237
|
有効化できません。DOM frame 検証は隔離した texture sentinel を使い、選択した settle policy
|
|
238
238
|
(`raf2-paint-event` または `sync-layout`)を receipt に記録します。
|
|
239
|
+
|
|
240
|
+
宣言型 `data-akari-vgpu-scene` は同梱 vgpu 0.4.0 の WebGPU ランタイムで描き、描画直後の canvas を GPU 合成へ渡す。v0 は pure な fragment パス、先行パスの texture 入力、CSS `--vgpu-*` ツマミに対応する。ライブプレビューは辺あたり半解像度、GPU 書き出しは等倍で、配置と時刻を共有する。WebGPU の probe・device 障害は `auto` でも失敗として伝播し OSR へ落とさない。receipt の `gpu.vgpu` は使用時だけ追加する。詳しくは [vgpu v0 契約](../../docs/contract-2026-09-06-vgpu-layer-v0.md)。
|
|
@@ -258,3 +258,5 @@ The frame-engine bundle is generated. Do not edit `generated/frame-engine.js` di
|
|
|
258
258
|
Frame hashing is available only through the isolated verification module and cannot be combined
|
|
259
259
|
with the runtime readback trap. DOM frame verification uses an isolated texture sentinel and
|
|
260
260
|
records the selected settle policy (`raf2-paint-event` or `sync-layout`) in the receipt.
|
|
261
|
+
|
|
262
|
+
Declarative `data-akari-vgpu-scene` overlays use the vendored vgpu 0.4.0 WebGPU runtime and transfer each freshly drawn canvas into the GPU compositor. v0 supports pure fragment passes, earlier-pass texture inputs, and CSS `--vgpu-*` uniforms. Live preview defaults to half resolution per edge; GPU export uses full resolution with the same placement and time. WebGPU probe/device failures are fatal, including in `auto` (no OSR fallback); `gpu.vgpu` is present in receipts only when used. See the [vgpu v0 contract](../../docs/contract-2026-09-06-vgpu-layer-v0.md) (Japanese).
|
|
@@ -85,7 +85,8 @@ export function buildAtempoChain(speed) {
|
|
|
85
85
|
|
|
86
86
|
// Counting semaphore whose limit is supplied per acquisition: the active count is shared by
|
|
87
87
|
// the whole process while each caller may state the ceiling it accepts. Waiters are woken in
|
|
88
|
-
// FIFO order
|
|
88
|
+
// FIFO order unless explicitly promoted or passed while probing. The count is bumped
|
|
89
|
+
// synchronously on wake-up, so the ceiling never overshoots.
|
|
89
90
|
class Semaphore {
|
|
90
91
|
#active = 0;
|
|
91
92
|
#waiters = [];
|
|
@@ -121,16 +122,37 @@ class Semaphore {
|
|
|
121
122
|
waiter.reject?.(new Error('semaphore reservation cancelled'));
|
|
122
123
|
this.#drain();
|
|
123
124
|
},
|
|
125
|
+
promote: () => {
|
|
126
|
+
if (waiter.state !== 'waiting') return false;
|
|
127
|
+
const index = this.#waiters.indexOf(waiter);
|
|
128
|
+
if (index <= 0) return false;
|
|
129
|
+
this.#waiters.splice(index, 1);
|
|
130
|
+
this.#waiters.unshift(waiter);
|
|
131
|
+
this.#drain();
|
|
132
|
+
return true;
|
|
133
|
+
},
|
|
134
|
+
hold: () => {
|
|
135
|
+
if (waiter.state !== 'waiting' || waiter.resolve) return;
|
|
136
|
+
waiter.hold = true;
|
|
137
|
+
this.#drain();
|
|
138
|
+
},
|
|
124
139
|
};
|
|
125
140
|
}
|
|
126
141
|
|
|
127
142
|
#drain() {
|
|
128
143
|
if (this.#waiters.length) {
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
//
|
|
132
|
-
|
|
133
|
-
this.#waiters.
|
|
144
|
+
let index = 0;
|
|
145
|
+
let held = 0;
|
|
146
|
+
// Only probing tickets opt in to holding one idle slot. Plain reservations
|
|
147
|
+
// remain FIFO barriers; several probing tickets together hold just one slot.
|
|
148
|
+
while (index < this.#waiters.length && !this.#waiters[index].resolve) {
|
|
149
|
+
if (!this.#waiters[index].hold) return;
|
|
150
|
+
held = 1;
|
|
151
|
+
index += 1;
|
|
152
|
+
}
|
|
153
|
+
const waiter = this.#waiters[index];
|
|
154
|
+
if (!waiter || this.#active + held >= waiter.limit) return;
|
|
155
|
+
this.#waiters.splice(index, 1);
|
|
134
156
|
this.#active += 1;
|
|
135
157
|
waiter.state = 'acquired';
|
|
136
158
|
waiter.resolve();
|
|
@@ -635,6 +657,52 @@ async function generate(prepared, options) {
|
|
|
635
657
|
const generating = new Map();
|
|
636
658
|
const requested = new Map();
|
|
637
659
|
const probing = new Map();
|
|
660
|
+
// Tickets are registered synchronously, then reserved in request order when work
|
|
661
|
+
// starts (or promotion is requested). This also permits immediate promotion after
|
|
662
|
+
// requestPreviewAudioSidecar returns, without starting a probe in that call.
|
|
663
|
+
const tickets = new Set();
|
|
664
|
+
|
|
665
|
+
function registerTicket(options, prepared) {
|
|
666
|
+
const ticket = { sourcePath: prepared.sourcePath, outputDirectory: prepared.outputDirectory,
|
|
667
|
+
key: prepared.key, limit: settingsFrom(options).concurrency.ffmpeg, slot: options.slot };
|
|
668
|
+
tickets.add(ticket);
|
|
669
|
+
return ticket;
|
|
670
|
+
}
|
|
671
|
+
|
|
672
|
+
function reserveTickets() {
|
|
673
|
+
for (const ticket of tickets) ticket.slot ??= ffmpegSlots.reserve(ticket.limit);
|
|
674
|
+
}
|
|
675
|
+
|
|
676
|
+
function discardTicket(ticket, cancel = true) {
|
|
677
|
+
try { if (cancel) ticket.slot?.cancel(); } finally { tickets.delete(ticket); }
|
|
678
|
+
}
|
|
679
|
+
|
|
680
|
+
// keys are bare sidecar keys (an optional .flac/.pcm suffix is accepted).
|
|
681
|
+
// sourcePaths identify probing requests and remain usable after probe handoff.
|
|
682
|
+
// Each actual move is reported in input order; the last move has highest priority.
|
|
683
|
+
export function promotePreviewAudioSidecars({ cacheDir, keys = [], sourcePaths = [] }) {
|
|
684
|
+
const outputDirectory = path.resolve(cacheDir, 'preview-audio');
|
|
685
|
+
const promoted = [];
|
|
686
|
+
const promote = (value, matches) => {
|
|
687
|
+
const matching = [...tickets].filter(ticket => ticket.outputDirectory === outputDirectory && matches(ticket));
|
|
688
|
+
if (!matching.length) return;
|
|
689
|
+
reserveTickets();
|
|
690
|
+
let moved = false;
|
|
691
|
+
for (const ticket of matching) if (ticket.slot.promote?.()) moved = true;
|
|
692
|
+
if (moved) promoted.push(value);
|
|
693
|
+
};
|
|
694
|
+
for (const key of keys) {
|
|
695
|
+
const bareKey = String(key).replace(/\.(?:flac|pcm)$/u, '');
|
|
696
|
+
promote(key, ticket => ticket.key === bareKey);
|
|
697
|
+
}
|
|
698
|
+
for (const sourcePath of sourcePaths) {
|
|
699
|
+
if (typeof sourcePath !== 'string' || !path.isAbsolute(sourcePath)) continue;
|
|
700
|
+
const resolved = path.resolve(sourcePath);
|
|
701
|
+
promote(sourcePath, ticket => ticket.sourcePath === resolved);
|
|
702
|
+
}
|
|
703
|
+
return { promoted };
|
|
704
|
+
}
|
|
705
|
+
|
|
638
706
|
const listeners = new Set();
|
|
639
707
|
const FAILURE_RETRY_MS = 60000;
|
|
640
708
|
|
|
@@ -793,11 +861,15 @@ function requestSidecar(options) {
|
|
|
793
861
|
const resolved = requestOptions(options);
|
|
794
862
|
if (resolved.status) {
|
|
795
863
|
if (!status.probe?.pending || probing.has(resolved.probePath)) return status;
|
|
864
|
+
const ticket = registerTicket(options, { sourcePath: path.resolve(options.sourcePath),
|
|
865
|
+
outputDirectory: path.dirname(resolved.probePath), key: null });
|
|
796
866
|
// Start after returning the declaration. The map is populated before any work runs.
|
|
797
867
|
const pending = new Promise(resolve => setImmediate(resolve)).then(async () => {
|
|
798
|
-
|
|
868
|
+
reserveTickets();
|
|
869
|
+
const { slot } = ticket;
|
|
799
870
|
let handedOff = false;
|
|
800
871
|
try {
|
|
872
|
+
slot.hold?.();
|
|
801
873
|
const probe = await probePreviewAudioSourceAsync(options.sourcePath, options);
|
|
802
874
|
if (probe.ok) {
|
|
803
875
|
writeCacheJson(resolved.probePath, probe);
|
|
@@ -818,18 +890,20 @@ function requestSidecar(options) {
|
|
|
818
890
|
reason: probe.reason, sourcePath: path.resolve(options.sourcePath) });
|
|
819
891
|
}
|
|
820
892
|
} finally {
|
|
821
|
-
|
|
893
|
+
discardTicket(ticket, !handedOff);
|
|
822
894
|
}
|
|
823
895
|
}).catch(error => console.warn('[preview-audio] probe cache failed', error))
|
|
824
|
-
.finally(() => probing.delete(resolved.probePath));
|
|
896
|
+
.finally(() => { tickets.delete(ticket); probing.delete(resolved.probePath); });
|
|
825
897
|
probing.set(resolved.probePath, pending);
|
|
826
898
|
return status;
|
|
827
899
|
}
|
|
828
900
|
if (status.state !== 'missing' && status.state !== 'legacy') return status;
|
|
829
901
|
const prepared = prepare(resolved.options, {}, false);
|
|
902
|
+
const ticket = registerTicket(resolved.options, prepared);
|
|
830
903
|
const pending = new Promise(resolve => setImmediate(resolve)).then(async () => {
|
|
904
|
+
reserveTickets();
|
|
831
905
|
fs.rmSync(path.join(prepared.outputDirectory, `${prepared.key}.failed.json`), { force: true });
|
|
832
|
-
const result = await ensurePreviewAudioSidecar(resolved.options);
|
|
906
|
+
const result = await ensurePreviewAudioSidecar({ ...resolved.options, slot: ticket.slot });
|
|
833
907
|
let state = 'ready';
|
|
834
908
|
if (!result.ok) {
|
|
835
909
|
state = classifyPreviewAudioFailure(result.reason) === 'no-audio' ? 'no-audio' : 'failed';
|
|
@@ -842,9 +916,8 @@ function requestSidecar(options) {
|
|
|
842
916
|
...(result.ok ? { path: result.path, durationSec: result.durationSec } : { reason: result.reason }) };
|
|
843
917
|
}).catch(error => ({ key: prepared.key, state: 'failed', sourcePath: prepared.sourcePath,
|
|
844
918
|
reason: summarize(error?.message, 'preview audio cache failed') }))
|
|
845
|
-
.finally(() =>
|
|
919
|
+
.finally(() => { discardTicket(ticket); requested.delete(prepared.outputPath); })
|
|
846
920
|
.then(event => {
|
|
847
|
-
requested.delete(prepared.outputPath);
|
|
848
921
|
emitSidecarEvent(event);
|
|
849
922
|
});
|
|
850
923
|
requested.set(prepared.outputPath, pending);
|
|
@@ -865,8 +938,10 @@ export function ensurePreviewAudioSidecar(options) {
|
|
|
865
938
|
options.slot?.cancel();
|
|
866
939
|
return joined;
|
|
867
940
|
}
|
|
868
|
-
const
|
|
869
|
-
|
|
941
|
+
const ticket = options.slot ? null : registerTicket(options, prepared);
|
|
942
|
+
if (ticket) reserveTickets();
|
|
943
|
+
const pending = generate(prepared, ticket ? { ...options, slot: ticket.slot } : options)
|
|
944
|
+
.finally(() => { if (ticket) discardTicket(ticket); else options.slot?.cancel(); })
|
|
870
945
|
.catch(error => failure(prepared.outputPath, prepared.key, error));
|
|
871
946
|
generating.set(prepared.outputPath, pending);
|
|
872
947
|
void pending.finally(() => generating.delete(prepared.outputPath));
|
|
@@ -4,6 +4,8 @@ import os from 'node:os';
|
|
|
4
4
|
import path from 'node:path';
|
|
5
5
|
import { spawnSync } from 'node:child_process';
|
|
6
6
|
import childProcess from 'node:child_process';
|
|
7
|
+
import { EventEmitter } from 'node:events';
|
|
8
|
+
import { PassThrough } from 'node:stream';
|
|
7
9
|
import { syncBuiltinESMExports } from 'node:module';
|
|
8
10
|
import { setImmediate as nextTurn, setTimeout as delay } from 'node:timers/promises';
|
|
9
11
|
import test from 'node:test';
|
|
@@ -11,6 +13,7 @@ import { resolveFfmpeg, resolveFfprobe } from '../src/index.mjs';
|
|
|
11
13
|
import {
|
|
12
14
|
__testing, ensurePreviewAudioSidecar, previewAudioSidecarStatus,
|
|
13
15
|
requestPreviewAudioSidecar, subscribePreviewAudioSidecarEvents,
|
|
16
|
+
promotePreviewAudioSidecars,
|
|
14
17
|
} from '../src/preview-audio-sidecar.mjs';
|
|
15
18
|
|
|
16
19
|
const { Semaphore } = __testing;
|
|
@@ -298,6 +301,58 @@ function realBinaries() {
|
|
|
298
301
|
|
|
299
302
|
const binaries = realBinaries();
|
|
300
303
|
|
|
304
|
+
function mockSpawn(t, run) {
|
|
305
|
+
const observed = t.mock.method(childProcess, 'spawn', (command, args) => {
|
|
306
|
+
const child = new EventEmitter();
|
|
307
|
+
child.stdout = new PassThrough();
|
|
308
|
+
child.stderr = new PassThrough();
|
|
309
|
+
child.exitCode = null;
|
|
310
|
+
child.signalCode = null;
|
|
311
|
+
child.kill = () => { child.signalCode = 'SIGKILL'; child.emit('close', null, 'SIGKILL'); };
|
|
312
|
+
setImmediate(async () => {
|
|
313
|
+
try {
|
|
314
|
+
await run(child, command, args);
|
|
315
|
+
child.exitCode = 0;
|
|
316
|
+
child.emit('close', 0, null);
|
|
317
|
+
} catch (error) {
|
|
318
|
+
child.stderr.write(error.message);
|
|
319
|
+
child.exitCode = 1;
|
|
320
|
+
child.emit('close', 1, null);
|
|
321
|
+
}
|
|
322
|
+
});
|
|
323
|
+
return child;
|
|
324
|
+
});
|
|
325
|
+
syncBuiltinESMExports();
|
|
326
|
+
t.after(() => { observed.mock.restore(); syncBuiltinESMExports(); });
|
|
327
|
+
return observed;
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
test('短 SFX の probe キャッシュは request と status の再取得で ffprobe を起動しない',
|
|
331
|
+
{ timeout: 10000 }, async t => {
|
|
332
|
+
const f = fixture(t);
|
|
333
|
+
const observedSpawn = mockSpawn(t, async child => {
|
|
334
|
+
child.stdout.write(JSON.stringify({ streams: [{ sample_rate: '48000', channels: 2 }],
|
|
335
|
+
format: { duration: '0.1' } }));
|
|
336
|
+
});
|
|
337
|
+
const event = deferred();
|
|
338
|
+
t.after(subscribePreviewAudioSidecarEvents(value => event.resolve(value)));
|
|
339
|
+
const options = { ...f.options, decodedBytesThreshold: 64 * 1024 * 1024 };
|
|
340
|
+
const first = requestPreviewAudioSidecar(options);
|
|
341
|
+
assert.equal(first.probe.pending, true);
|
|
342
|
+
assert.equal((await event.promise).state, 'not-needed');
|
|
343
|
+
await nextTurn();
|
|
344
|
+
assert.equal(observedSpawn.mock.callCount(), 1);
|
|
345
|
+
assert.equal(observedSpawn.mock.calls[0].arguments[0], options.ffprobe);
|
|
346
|
+
const cached = JSON.parse(fs.readFileSync(path.join(f.directory, `probe-${first.probe.fingerprint}.json`), 'utf8'));
|
|
347
|
+
assert.ok(cached.durationSec > 0 && cached.durationSec < 1);
|
|
348
|
+
for (const read of [requestPreviewAudioSidecar, previewAudioSidecarStatus]) {
|
|
349
|
+
assert.deepEqual(read(options), { state: 'not-needed', key: null,
|
|
350
|
+
probe: { fingerprint: first.probe.fingerprint } });
|
|
351
|
+
}
|
|
352
|
+
await nextTurn();
|
|
353
|
+
assert.equal(observedSpawn.mock.callCount(), 1, '2 回目はキャッシュを読み spawn を増やさない');
|
|
354
|
+
});
|
|
355
|
+
|
|
301
356
|
test('実 ffmpeg: 120 秒 WAV の A(probe)→ B → C は A が最初に ready になる',
|
|
302
357
|
{ skip: binaries.skip, timeout: 60000 }, async t => {
|
|
303
358
|
const f = fixture(t);
|
|
@@ -328,7 +383,7 @@ test('実 ffmpeg: 120 秒 WAV の A(probe)→ B → C は A が最初に rea
|
|
|
328
383
|
}
|
|
329
384
|
});
|
|
330
385
|
|
|
331
|
-
test('実 ffmpeg: 遅い probe の A
|
|
386
|
+
test('実 ffmpeg: 遅い probe の A は既定 2 枠の 1 枠を予約し B → A → C で起動する',
|
|
332
387
|
{ skip: binaries.skip, timeout: 60000 }, async t => {
|
|
333
388
|
const f = fixture(t);
|
|
334
389
|
const sources = ['a-narration.wav', 'b-speech1.wav', 'c-speech2.wav'].map(name => path.join(f.root, name));
|
|
@@ -351,7 +406,13 @@ test('実 ffmpeg: 遅い probe の A と既知尺 B・C は既定 2 枠で要求
|
|
|
351
406
|
});
|
|
352
407
|
syncBuiltinESMExports();
|
|
353
408
|
t.after(() => { observedSpawn.mock.restore(); syncBuiltinESMExports(); });
|
|
354
|
-
const reservations =
|
|
409
|
+
const reservations = [];
|
|
410
|
+
const originalReserve = Semaphore.prototype.reserve;
|
|
411
|
+
t.mock.method(Semaphore.prototype, 'reserve', function (...args) {
|
|
412
|
+
const slot = originalReserve.apply(this, args);
|
|
413
|
+
reservations.push(slot);
|
|
414
|
+
return slot;
|
|
415
|
+
});
|
|
355
416
|
const events = [];
|
|
356
417
|
t.after(subscribePreviewAudioSidecarEvents(event => events.push(event)));
|
|
357
418
|
const probe = deferred();
|
|
@@ -360,15 +421,255 @@ test('実 ffmpeg: 遅い probe の A と既知尺 B・C は既定 2 枠で要求
|
|
|
360
421
|
probeAudio: () => probe.promise });
|
|
361
422
|
for (const sourcePath of sources.slice(1)) requestPreviewAudioSidecar({ ...common, sourcePath, outSec: 120 });
|
|
362
423
|
let deadline = Date.now() + 10000;
|
|
363
|
-
while (
|
|
424
|
+
while (starts.length < 1 && Date.now() < deadline) await delay(1);
|
|
364
425
|
const startsDuringProbe = [...starts];
|
|
365
426
|
const reservationsDuringProbe = reservations.length;
|
|
366
427
|
probe.resolve(metadata);
|
|
367
428
|
deadline = Date.now() + 30000;
|
|
368
429
|
while (events.length < 3 && Date.now() < deadline) await delay(10);
|
|
369
430
|
assert.equal(reservationsDuringProbe, 3);
|
|
370
|
-
assert.deepEqual(startsDuringProbe, [], 'probe
|
|
371
|
-
assert.deepEqual(starts, sources);
|
|
431
|
+
assert.deepEqual(startsDuringProbe, [sources[1]], 'probe は 1 枠だけ予約し B を通す');
|
|
432
|
+
assert.deepEqual(starts, [sources[1], sources[0], sources[2]]);
|
|
372
433
|
assert.equal(peak, 2);
|
|
373
434
|
assert.deepEqual(events.map(event => event.state), ['ready', 'ready', 'ready']);
|
|
374
435
|
});
|
|
436
|
+
|
|
437
|
+
test('promote は待機札を先頭へ移し先頭・取得済み・破棄済みなら何もしない', async () => {
|
|
438
|
+
const semaphore = new Semaphore();
|
|
439
|
+
await semaphore.acquire(1);
|
|
440
|
+
const slots = [semaphore.reserve(1), semaphore.reserve(1), semaphore.reserve(1)];
|
|
441
|
+
const order = [];
|
|
442
|
+
const jobs = slots.map((slot, index) => slot.acquire().then(() => order.push(index)));
|
|
443
|
+
assert.equal(slots[0].promote(), false);
|
|
444
|
+
assert.equal(slots[2].promote(), true);
|
|
445
|
+
assert.equal(slots[2].promote(), false);
|
|
446
|
+
semaphore.release();
|
|
447
|
+
await jobs[2];
|
|
448
|
+
assert.equal(slots[2].promote(), false);
|
|
449
|
+
semaphore.release();
|
|
450
|
+
await jobs[0];
|
|
451
|
+
semaphore.release();
|
|
452
|
+
await jobs[1];
|
|
453
|
+
semaphore.release();
|
|
454
|
+
assert.deepEqual(order, [2, 0, 1]);
|
|
455
|
+
const cancelled = semaphore.reserve(1);
|
|
456
|
+
cancelled.cancel();
|
|
457
|
+
assert.equal(cancelled.promote(), false);
|
|
458
|
+
});
|
|
459
|
+
|
|
460
|
+
test('hold は 2 枠で B だけを通し probe 完了後は A が C より先に取得する', async () => {
|
|
461
|
+
const semaphore = new Semaphore();
|
|
462
|
+
const a = semaphore.reserve(2);
|
|
463
|
+
a.hold();
|
|
464
|
+
a.hold();
|
|
465
|
+
const order = [];
|
|
466
|
+
const b = semaphore.acquire(2).then(() => order.push('B'));
|
|
467
|
+
const c = semaphore.acquire(2).then(() => order.push('C'));
|
|
468
|
+
await b;
|
|
469
|
+
await nextTurn();
|
|
470
|
+
assert.deepEqual(order, ['B']);
|
|
471
|
+
await a.acquire().then(() => order.push('A'));
|
|
472
|
+
await nextTurn();
|
|
473
|
+
assert.deepEqual(order, ['B', 'A']);
|
|
474
|
+
a.hold();
|
|
475
|
+
semaphore.release();
|
|
476
|
+
await c;
|
|
477
|
+
assert.deepEqual(order, ['B', 'A', 'C']);
|
|
478
|
+
semaphore.release();
|
|
479
|
+
semaphore.release();
|
|
480
|
+
});
|
|
481
|
+
|
|
482
|
+
test('hold は 1 枠なら probe 中の B と C を待たせる', async () => {
|
|
483
|
+
const semaphore = new Semaphore();
|
|
484
|
+
const a = semaphore.reserve(1);
|
|
485
|
+
a.hold();
|
|
486
|
+
const order = [];
|
|
487
|
+
const b = semaphore.acquire(1).then(() => order.push('B'));
|
|
488
|
+
const c = semaphore.acquire(1).then(() => order.push('C'));
|
|
489
|
+
await nextTurn();
|
|
490
|
+
assert.deepEqual(order, []);
|
|
491
|
+
await a.acquire().then(() => order.push('A'));
|
|
492
|
+
assert.deepEqual(order, ['A']);
|
|
493
|
+
semaphore.release();
|
|
494
|
+
await b;
|
|
495
|
+
semaphore.release();
|
|
496
|
+
await c;
|
|
497
|
+
semaphore.release();
|
|
498
|
+
assert.deepEqual(order, ['A', 'B', 'C']);
|
|
499
|
+
});
|
|
500
|
+
|
|
501
|
+
test('複数の hold は合計 1 枠だけを予約し通常札の FIFO 障壁は維持する', async () => {
|
|
502
|
+
const semaphore = new Semaphore();
|
|
503
|
+
const a = semaphore.reserve(2);
|
|
504
|
+
const b = semaphore.reserve(2);
|
|
505
|
+
const barrier = semaphore.reserve(2);
|
|
506
|
+
a.hold();
|
|
507
|
+
b.hold();
|
|
508
|
+
const order = [];
|
|
509
|
+
const c = semaphore.acquire(2).then(() => order.push('C'));
|
|
510
|
+
const d = semaphore.acquire(2).then(() => order.push('D'));
|
|
511
|
+
await nextTurn();
|
|
512
|
+
assert.deepEqual(order, []);
|
|
513
|
+
barrier.cancel();
|
|
514
|
+
await c;
|
|
515
|
+
await nextTurn();
|
|
516
|
+
assert.deepEqual(order, ['C']);
|
|
517
|
+
a.cancel();
|
|
518
|
+
await nextTurn();
|
|
519
|
+
assert.deepEqual(order, ['C'], '残った probe も 1 枠を予約する');
|
|
520
|
+
b.cancel();
|
|
521
|
+
await d;
|
|
522
|
+
semaphore.release();
|
|
523
|
+
semaphore.release();
|
|
524
|
+
});
|
|
525
|
+
|
|
526
|
+
for (const by of ['keys', 'sourcePaths']) {
|
|
527
|
+
test(`昇格 API は ${by} で 3 番目を先頭にして完了順を 3 → 1 → 2 にする`,
|
|
528
|
+
{ timeout: 5000 }, async t => {
|
|
529
|
+
const f = fixture(t);
|
|
530
|
+
const sources = ['first.wav', 'second.wav', 'third.wav'].map(name => path.join(f.root, name));
|
|
531
|
+
for (const sourcePath of sources) fs.writeFileSync(sourcePath, 'fixture');
|
|
532
|
+
const starts = [];
|
|
533
|
+
mockSpawn(t, async (child, command, args) => {
|
|
534
|
+
starts.push(args[args.indexOf('-i') + 1]);
|
|
535
|
+
fs.writeFileSync(args.at(-1), Buffer.alloc(480));
|
|
536
|
+
});
|
|
537
|
+
const events = [];
|
|
538
|
+
const done = deferred();
|
|
539
|
+
t.after(subscribePreviewAudioSidecarEvents(event => {
|
|
540
|
+
events.push(event);
|
|
541
|
+
if (events.length === 3) done.resolve();
|
|
542
|
+
}));
|
|
543
|
+
const probe = deferred();
|
|
544
|
+
const probing = deferred();
|
|
545
|
+
let probeCount = 0;
|
|
546
|
+
t.after(() => probe.resolve(metadata));
|
|
547
|
+
const common = { ...f.options, ffmpeg: 'mock-ffmpeg', probeAudio: () => {
|
|
548
|
+
if (++probeCount === 3) probing.resolve();
|
|
549
|
+
return probe.promise;
|
|
550
|
+
} };
|
|
551
|
+
const statuses = sources.map(sourcePath => requestPreviewAudioSidecar({ ...common, sourcePath,
|
|
552
|
+
...(by === 'keys' ? { outSec: 120 } : { decodedBytesThreshold: 1 }) }));
|
|
553
|
+
const target = by === 'keys' ? statuses[2].key : sources[2];
|
|
554
|
+
if (by === 'sourcePaths') await probing.promise;
|
|
555
|
+
assert.deepEqual(promotePreviewAudioSidecars({ cacheDir: path.join(f.root, 'other'), [by]: [target] }),
|
|
556
|
+
{ promoted: [] });
|
|
557
|
+
assert.deepEqual(promotePreviewAudioSidecars({ cacheDir: f.root, [by]: [target, target] }),
|
|
558
|
+
{ promoted: [target] });
|
|
559
|
+
probe.resolve(metadata);
|
|
560
|
+
await done.promise;
|
|
561
|
+
assert.deepEqual(events.map(event => event.state), ['ready', 'ready', 'ready']);
|
|
562
|
+
assert.deepEqual(events.map(event => event.sourcePath), [sources[2], sources[0], sources[1]]);
|
|
563
|
+
assert.deepEqual(starts, [sources[2], sources[0], sources[1]]);
|
|
564
|
+
assert.deepEqual(promotePreviewAudioSidecars({ cacheDir: f.root,
|
|
565
|
+
keys: [events[0].key, 'missing'], sourcePaths: sources }), { promoted: [] });
|
|
566
|
+
assert.deepEqual(promotePreviewAudioSidecars({ cacheDir: f.root }), { promoted: [] });
|
|
567
|
+
});
|
|
568
|
+
}
|
|
569
|
+
|
|
570
|
+
test('昇格 API は keys → sourcePaths の入力順に移動し取得済み札を無視する',
|
|
571
|
+
{ timeout: 5000 }, async t => {
|
|
572
|
+
const f = fixture(t);
|
|
573
|
+
const sources = ['first.wav', 'second.wav', 'third.wav'].map(name => path.join(f.root, name));
|
|
574
|
+
for (const sourcePath of sources) fs.writeFileSync(sourcePath, 'fixture');
|
|
575
|
+
const entered = deferred();
|
|
576
|
+
const gate = deferred();
|
|
577
|
+
t.after(() => gate.resolve());
|
|
578
|
+
const starts = [];
|
|
579
|
+
mockSpawn(t, async (child, command, args) => {
|
|
580
|
+
starts.push(args[args.indexOf('-i') + 1]);
|
|
581
|
+
entered.resolve();
|
|
582
|
+
await gate.promise;
|
|
583
|
+
fs.writeFileSync(args.at(-1), Buffer.alloc(480));
|
|
584
|
+
});
|
|
585
|
+
const events = [];
|
|
586
|
+
const done = deferred();
|
|
587
|
+
t.after(subscribePreviewAudioSidecarEvents(event => {
|
|
588
|
+
events.push(event);
|
|
589
|
+
if (events.length === 3) done.resolve();
|
|
590
|
+
}));
|
|
591
|
+
const statuses = sources.map(sourcePath => requestPreviewAudioSidecar({ ...f.known, sourcePath,
|
|
592
|
+
ffmpeg: 'mock-ffmpeg' }));
|
|
593
|
+
const keys = [statuses[1].key, statuses[2].key];
|
|
594
|
+
assert.deepEqual(promotePreviewAudioSidecars({ cacheDir: f.root, keys, sourcePaths: [sources[0]] }),
|
|
595
|
+
{ promoted: [...keys, sources[0]] });
|
|
596
|
+
await entered.promise;
|
|
597
|
+
assert.deepEqual(promotePreviewAudioSidecars({ cacheDir: f.root,
|
|
598
|
+
keys: [statuses[0].key], sourcePaths: [sources[0]] }), { promoted: [] });
|
|
599
|
+
gate.resolve();
|
|
600
|
+
await done.promise;
|
|
601
|
+
assert.deepEqual(starts, [sources[0], sources[2], sources[1]]);
|
|
602
|
+
assert.deepEqual(events.map(event => event.state), ['ready', 'ready', 'ready']);
|
|
603
|
+
});
|
|
604
|
+
|
|
605
|
+
test('背景 probe の hold は B だけを通し A → C へ枠を渡す',
|
|
606
|
+
{ timeout: 5000 }, async t => {
|
|
607
|
+
const f = fixture(t);
|
|
608
|
+
const sources = ['first.wav', 'second.wav', 'third.wav'].map(name => path.join(f.root, name));
|
|
609
|
+
for (const sourcePath of sources) fs.writeFileSync(sourcePath, 'fixture');
|
|
610
|
+
const gates = sources.map(() => deferred());
|
|
611
|
+
const entered = sources.map(() => deferred());
|
|
612
|
+
const probe = deferred();
|
|
613
|
+
t.after(() => { probe.resolve(metadata); for (const gate of gates) gate.resolve(); });
|
|
614
|
+
const starts = [];
|
|
615
|
+
let active = 0;
|
|
616
|
+
let peak = 0;
|
|
617
|
+
mockSpawn(t, async (child, command, args) => {
|
|
618
|
+
const sourcePath = args[args.indexOf('-i') + 1];
|
|
619
|
+
const index = sources.indexOf(sourcePath);
|
|
620
|
+
starts.push(sourcePath);
|
|
621
|
+
peak = Math.max(peak, ++active);
|
|
622
|
+
entered[index].resolve();
|
|
623
|
+
await gates[index].promise;
|
|
624
|
+
fs.writeFileSync(args.at(-1), Buffer.alloc(480));
|
|
625
|
+
active -= 1;
|
|
626
|
+
});
|
|
627
|
+
const events = [];
|
|
628
|
+
const done = deferred();
|
|
629
|
+
t.after(subscribePreviewAudioSidecarEvents(event => {
|
|
630
|
+
events.push(event);
|
|
631
|
+
if (events.length === 3) done.resolve();
|
|
632
|
+
}));
|
|
633
|
+
const common = { ...f.options, concurrency: undefined, ffmpeg: 'mock-ffmpeg' };
|
|
634
|
+
requestPreviewAudioSidecar({ ...common, sourcePath: sources[0],
|
|
635
|
+
decodedBytesThreshold: 1, probeAudio: () => probe.promise });
|
|
636
|
+
for (const sourcePath of sources.slice(1)) requestPreviewAudioSidecar({ ...common, sourcePath, outSec: 120 });
|
|
637
|
+
await entered[1].promise;
|
|
638
|
+
await nextTurn();
|
|
639
|
+
assert.deepEqual(starts, [sources[1]]);
|
|
640
|
+
probe.resolve(metadata);
|
|
641
|
+
await entered[0].promise;
|
|
642
|
+
await nextTurn();
|
|
643
|
+
assert.deepEqual(starts, [sources[1], sources[0]]);
|
|
644
|
+
gates[0].resolve();
|
|
645
|
+
await entered[2].promise;
|
|
646
|
+
assert.deepEqual(starts, [sources[1], sources[0], sources[2]]);
|
|
647
|
+
gates[1].resolve();
|
|
648
|
+
gates[2].resolve();
|
|
649
|
+
await done.promise;
|
|
650
|
+
assert.equal(peak, 2);
|
|
651
|
+
assert.deepEqual(events.map(event => event.state), ['ready', 'ready', 'ready']);
|
|
652
|
+
});
|
|
653
|
+
|
|
654
|
+
test('昇格 API は ensure の待機札も動かし失敗・完了後は台帳から外す',
|
|
655
|
+
{ timeout: 5000 }, async t => {
|
|
656
|
+
const f = fixture(t);
|
|
657
|
+
const sources = ['first.wav', 'second.wav', 'third.wav'].map(name => path.join(f.root, name));
|
|
658
|
+
for (const sourcePath of sources) fs.writeFileSync(sourcePath, 'fixture');
|
|
659
|
+
mockSpawn(t, async (child, command, args) => {
|
|
660
|
+
if (args[args.indexOf('-i') + 1] === sources[2]) throw new Error('test generation failure');
|
|
661
|
+
fs.writeFileSync(args.at(-1), Buffer.alloc(480));
|
|
662
|
+
});
|
|
663
|
+
const options = sources.map(sourcePath => ({ ...f.known, sourcePath, ffmpeg: 'mock-ffmpeg' }));
|
|
664
|
+
const keys = options.map(value => previewAudioSidecarStatus(value).key);
|
|
665
|
+
const order = [];
|
|
666
|
+
const jobs = options.map((value, index) => ensurePreviewAudioSidecar(value).then(result => {
|
|
667
|
+
order.push(index);
|
|
668
|
+
return result;
|
|
669
|
+
}));
|
|
670
|
+
assert.deepEqual(promotePreviewAudioSidecars({ cacheDir: f.root, keys: [keys[2]] }), { promoted: [keys[2]] });
|
|
671
|
+
const results = await Promise.all(jobs);
|
|
672
|
+
assert.deepEqual(order, [2, 0, 1]);
|
|
673
|
+
assert.deepEqual(results.map(result => result.ok), [true, true, false]);
|
|
674
|
+
assert.deepEqual(promotePreviewAudioSidecars({ cacheDir: f.root, keys, sourcePaths: sources }), { promoted: [] });
|
|
675
|
+
});
|
|
@@ -65,6 +65,15 @@ Three.js + glTF シーンを決定的な時刻で描画し(`three-runtime.js`
|
|
|
65
65
|
(`summary` = `EditSummary`。下記参照)。以降はタイムライン更新のたびに
|
|
66
66
|
`window.akari.runtime.tick(t, playing)` を呼ぶ
|
|
67
67
|
|
|
68
|
+
vgpu overlay を扱うホストは、three 群がある場合はその後に
|
|
69
|
+
`src/vendor/vgpu-bundle.js` → `src/vgpu-runtime.js` の順で読み込む。
|
|
70
|
+
書き出しは `await window.akari.vgpuRuntime.probe()` 後、container ごとに
|
|
71
|
+
`render(container, localTimeSeconds)` → canvas の登録/更新を同じ同期処理内で行う。
|
|
72
|
+
プレビューは `render(container, localTimeSeconds, { previewScale: 0.5 })`、非表示化と unmount で
|
|
73
|
+
`dispose(container)` を呼ぶ。probe と render の失敗を捕捉して任意の `[data-akari-vgpu-fallback]` を表示し、
|
|
74
|
+
警告は 1 回にまとめる。共有 device は container の破棄時には落とさない。
|
|
75
|
+
宣言と純関数・解像度の約束は [vgpu v0 契約](../../docs/contract-2026-09-06-vgpu-layer-v0.md) を参照。
|
|
76
|
+
|
|
68
77
|
## ホストアダプタ契約(新シェル実装者向け — 本パッケージへの入力)
|
|
69
78
|
|
|
70
79
|
本パッケージの各スクリプトは `window.akari` 名前空間の以下のプロパティを
|
|
@@ -240,6 +249,8 @@ Web UI(preview-server)は `slot-params.js` を `/slot-params.js` で配り
|
|
|
240
249
|
src/
|
|
241
250
|
vendor/three-bundle.js Three.js core + GLTFLoader + RoomEnvironment の単一 IIFE
|
|
242
251
|
vendor/three-LICENSE.txt Three.js の MIT License
|
|
252
|
+
vendor/vgpu-bundle.js vgpu 0.4.0 browser entry の単一 IIFE
|
|
253
|
+
vendor/vgpu-LICENSE.txt vgpu の MIT License
|
|
243
254
|
vendor/vendor-3d-text-bundle.js troika-three-text + opentype.js + matter-js + poly-decomp の単一 IIFE
|
|
244
255
|
vendor/troika-three-text-LICENSE.txt troika-three-text の MIT License
|
|
245
256
|
vendor/opentype.js-LICENSE.txt opentype.js の MIT License
|
|
@@ -502,3 +513,52 @@ npm グローバルインストール禁止の制約内で完結するよう、
|
|
|
502
513
|
231 行で区切ると `#minimap-viewport` 規則の宣言途中で切れ、閉じ括弧を欠く
|
|
503
514
|
不正な CSS になるため、意図(「minimap 用ブロックを抽出」)に沿って実際の
|
|
504
515
|
ブロック終端まで抽出した。詳細は `out/status.json` 隣接の `report.md` を参照
|
|
516
|
+
|
|
517
|
+
## vgpu vendor の固定と再生成
|
|
518
|
+
|
|
519
|
+
`src/vendor/vgpu-bundle.js` は `vgpu@0.4.0` の browser entry を `esbuild@0.24.2` で
|
|
520
|
+
単一 IIFE にしたもの。実行時に npm/CDN/外部 origin を参照しない。
|
|
521
|
+
取得した tarball の integrity は以下に固定する。
|
|
522
|
+
|
|
523
|
+
- `vgpu@0.4.0`: `sha512-U8ofyb2FdiLpbkMJrGSu2PWu/fmiKsXYYpp3agFE3TjM5+6o7aZFD1PBl8XxxnSeN7br/Izmow7fcEmYJTpxSw==`
|
|
524
|
+
- `@vgpu/core@0.4.0`: `sha512-yBkqlNzTRYaRRzOAsootOmppzsDZytL5Xe09rZfV60zJc4FTr5jWZmGCrm/zlWM/1wEKocDtKloVKfKD+2TNPw==`
|
|
525
|
+
- `@vgpu/wgsl@0.4.0`: `sha512-19aESov4jNukukAlbE9VH34T/2VcIzmQPZ45WyphtrUMevFacAqTVU6qJVAP/YGX3rjQaL7Eq4jdvEhtOxNEPg==`
|
|
526
|
+
- `@vgpu/wgsl-std@0.4.0`: `sha512-a0EkK1y5VeA1jCEjPxe7BW7zTebL1UKLIQlEdxqrb8T1Zr+xO7VBe5mTNsuJtKWxgF+lANUqLwScyN+Fs4ooFw==`
|
|
527
|
+
- `@vgpu/adapter-mock@0.4.0`: `sha512-fqOEHCZAt+mMyOpgAA5inFWSoB9siO1LQIq9t+i6iemLbp2VBEEWlhZXCh3Hf+UDqbkut8kQaDrMPLt2J9LpvA==`
|
|
528
|
+
- `esbuild@0.24.2`: `sha512-+9egpBW8I3CD5XPe0n6BfT5fxLzxrlDzqydF3aviG+9ni1lDC/OvMHcxqEFV0+LANZG5R1bFMWfUrjVsdwxJvA==`
|
|
529
|
+
- `@esbuild/darwin-arm64@0.24.2`: `sha512-kj3AnYWc+CekmZnS5IPu9D+HWtUI49hbnyqk0FLEJDbzCIQt7hg7ucF1SQAilhtYpIujfaHr6O0UHlzzSPdOeA==`
|
|
530
|
+
|
|
531
|
+
entry `akari-vgpu-entry.js`:
|
|
532
|
+
|
|
533
|
+
```js
|
|
534
|
+
import { init, effect, surface, target, frame, sampler, uniforms, clock, VGPUError } from "vgpu";
|
|
535
|
+
window.AkariVgpu = Object.freeze({ init, effect, surface, target, frame, sampler, uniforms, clock, VGPUError });
|
|
536
|
+
```
|
|
537
|
+
|
|
538
|
+
再生成(空の一時ディレクトリ、macOS arm64。リポの package.json / lock は変更しない):
|
|
539
|
+
|
|
540
|
+
```sh
|
|
541
|
+
npm pack vgpu@0.4.0 @vgpu/core@0.4.0 @vgpu/wgsl@0.4.0 @vgpu/wgsl-std@0.4.0 @vgpu/adapter-mock@0.4.0 --ignore-scripts
|
|
542
|
+
npm pack esbuild@0.24.2 @esbuild/darwin-arm64@0.24.2 --ignore-scripts
|
|
543
|
+
mkdir -p node_modules/vgpu node_modules/@vgpu/core node_modules/@vgpu/wgsl node_modules/@vgpu/wgsl-std node_modules/@vgpu/adapter-mock node_modules/esbuild node_modules/@esbuild/darwin-arm64
|
|
544
|
+
tar -xzf vgpu-0.4.0.tgz -C node_modules/vgpu --strip-components=1
|
|
545
|
+
tar -xzf vgpu-core-0.4.0.tgz -C node_modules/@vgpu/core --strip-components=1
|
|
546
|
+
tar -xzf vgpu-wgsl-0.4.0.tgz -C node_modules/@vgpu/wgsl --strip-components=1
|
|
547
|
+
tar -xzf vgpu-wgsl-std-0.4.0.tgz -C node_modules/@vgpu/wgsl-std --strip-components=1
|
|
548
|
+
tar -xzf vgpu-adapter-mock-0.4.0.tgz -C node_modules/@vgpu/adapter-mock --strip-components=1
|
|
549
|
+
tar -xzf esbuild-0.24.2.tgz -C node_modules/esbuild --strip-components=1
|
|
550
|
+
tar -xzf esbuild-darwin-arm64-0.24.2.tgz -C node_modules/@esbuild/darwin-arm64 --strip-components=1
|
|
551
|
+
node node_modules/esbuild/bin/esbuild akari-vgpu-entry.js \
|
|
552
|
+
--bundle --format=iife --platform=browser --target=es2020 --minify \
|
|
553
|
+
--legal-comments=inline --metafile=meta.json --outfile=vgpu-bundle.js
|
|
554
|
+
cp node_modules/vgpu/LICENSE vgpu-LICENSE.txt
|
|
555
|
+
grep -cE '@modelcontextprotocol/server|zod|pngjs|pixelmatch|@vgpu/adapter-node|@vgpu/adapter-mock' vgpu-bundle.js || true
|
|
556
|
+
wc -c vgpu-bundle.js
|
|
557
|
+
shasum -a 256 vgpu-bundle.js
|
|
558
|
+
```
|
|
559
|
+
|
|
560
|
+
実測: **152953 bytes** / SHA-256 `9251bb41157e72e36211af2d0766de72db7251023fb2b968c15bdc529c111c25`。
|
|
561
|
+
metafile の npm 入力は `vgpu` / `@vgpu/core` / `@vgpu/wgsl` の 3 パッケージのみ。
|
|
562
|
+
上記 grep は **0 件**。`vgpu/node` / `vgpu/three` / `vgpu/mock` の entry は使用しない。
|
|
563
|
+
唯一の URL 文字列 `https://github.com/vercel-labs/vgpu/issues/294` はエラーメッセージの参照先で、実行時 fetch ではない。
|
|
564
|
+
MIT License(Copyright (c) 2025 Vercel, Inc.)全文は `src/vendor/vgpu-LICENSE.txt` に保持する。
|
|
@@ -1151,9 +1151,11 @@
|
|
|
1151
1151
|
"freeze": { "oneOf": [{ "type": "null" }, { "$ref": "#/$defs/cutFreeze" }] },
|
|
1152
1152
|
"fx": { "type": "array", "items": { "$ref": "#/$defs/cutFx" } },
|
|
1153
1153
|
"speed": { "$ref": "#/$defs/positiveNumber" },
|
|
1154
|
+
"gain_db": { "type": "number", "minimum": -60, "maximum": 12 },
|
|
1155
|
+
"mute": { "type": "boolean" },
|
|
1154
1156
|
"chroma_key": { "oneOf": [{ "type": "null" }, { "$ref": "#/$defs/chromaKey" }] }
|
|
1155
1157
|
},
|
|
1156
|
-
"$comment": "notes-2026-08-18-timeline-latency-and-track-model.md §9 / §10-1。src は editV2.sources[].id を参照し、in/out は素材内の秒。参照整合と out > in は兄弟値比較のため reader / edit-lint が検証する。"
|
|
1158
|
+
"$comment": "notes-2026-08-18-timeline-latency-and-track-model.md §9 / §10-1。src は editV2.sources[].id を参照し、in/out は素材内の秒。参照整合と out > in は兄弟値比較のため reader / edit-lint が検証する。gain_db / mute は埋め込み音声(speech)の減衰 / ミュートで、書き出しとプレビューで同値。省略時は gain 0・鳴る。"
|
|
1157
1159
|
},
|
|
1158
1160
|
"itemSourceAudioMediaV2": {
|
|
1159
1161
|
"type": "object",
|
|
@@ -77,6 +77,8 @@
|
|
|
77
77
|
{ "path": "tracks[].items[].source.fx", "applies_to": ["cuts", "layers"], "gpu": "ignored", "osr": "ignored", "runtime_warning": true, "evidence": "packages/edit-store/src/internal-model.ts copyMediaSourceFields; packages/frame-engine/src/timeline/plan.ts KNOWN_CUT_KEYS/KNOWN_LAYER_KEYS omit fx", "hint": "GPU / OSR frame-engine には登録済み cut FX が無い" },
|
|
78
78
|
{ "path": "tracks[].items[].source.speed", "applies_to": ["cuts"], "gpu": "consumed", "osr": "consumed", "evidence": "packages/edit-store/src/internal-model.ts buildV2VisualItem speed projection; packages/frame-engine/src/timeline/plan.ts buildResolvedTimelinePlan/layerFromPlacement" },
|
|
79
79
|
{ "path": "tracks[].items[].source.speed", "applies_to": ["layers"], "gpu": "ignored", "osr": "ignored", "runtime_warning": true, "evidence": "packages/edit-store/src/internal-model.ts copyMediaSourceFields; packages/frame-engine/src/timeline/plan.ts KNOWN_LAYER_KEYS omits speed" },
|
|
80
|
+
{ "path": "tracks[].items[].source.gain_db", "applies_to": ["cuts"], "gpu": "other-subsystem", "osr": "other-subsystem", "evidence": "packages/render-cut/src/plan.mjs cutSpeechVolumeSuffix (appendSequentialAudioCutFilters / gap-aware); packages/edit-store/src/audio-schedule.ts projectSpeechDeclarations speechGainDb", "note": "埋め込み音声(speech)の減衰。映像エンジンではなく render-cut の cut 音声(全エンジン共通)とプレビューの音声供給が消費する" },
|
|
81
|
+
{ "path": "tracks[].items[].source.mute", "applies_to": ["cuts"], "gpu": "other-subsystem", "osr": "other-subsystem", "evidence": "packages/render-cut/src/plan.mjs cut.mute -> anullsrc branch; packages/edit-store/src/audio-schedule.ts projectSpeechDeclarations skips muted cuts", "note": "埋め込み音声(speech)のミュート。尺は保ち無音区間になる" },
|
|
80
82
|
{ "path": "tracks[].items[].source.chroma_key", "applies_to": ["cuts", "layers"], "gpu": "partial", "osr": "partial", "runtime_warning": true, "evidence": "packages/edit-store/src/internal-model.ts needsLayersEngine/copyMediaSourceFields; packages/frame-engine/src/timeline/plan.ts KNOWN_CUT_KEYS/KNOWN_LAYER_KEYS omit chroma_key", "note": "透過 intent は layers 振り分けに使うが、frame-engine 自体は chroma key 抽出や background 置換をしない" },
|
|
81
83
|
{ "path": "tracks[].items[].source.path", "applies_to": ["overlays"], "gpu": "consumed", "osr": "consumed", "evidence": "packages/edit-store/src/internal-model.ts buildV2VisualItem html source; packages/gpu-export/src/page-builder.mjs loadAndBuildGpuPage; packages/osr-export/src/page-builder.mjs loadAndBuildOsrPage" },
|
|
82
84
|
{ "path": "tracks[].items[].source.path", "applies_to": ["captions"], "gpu": "consumed", "osr": "consumed", "evidence": "packages/edit-store/src/internal-model.ts buildV2VisualItem fixes captions.json; packages/gpu-export/src/page-builder.mjs loadAndBuildGpuPage; packages/osr-export/src/page-builder.mjs loadAndBuildOsrPage", "note": "schema が captions.json の定数に閉じている" },
|
|
@@ -15,6 +15,18 @@ function fixture(name) {
|
|
|
15
15
|
return JSON.parse(readFileSync(join(examplesRoot, name, "edit.json"), "utf8"));
|
|
16
16
|
}
|
|
17
17
|
|
|
18
|
+
test("visual media source validates embedded speech gain and mute", () => {
|
|
19
|
+
const value = fixture("edit-v2-valid");
|
|
20
|
+
Object.assign(value.tracks[3].items[0].source, { gain_db: -12, mute: true });
|
|
21
|
+
assert.equal(validate(value), true, JSON.stringify(validate.errors));
|
|
22
|
+
for (const [key, invalid] of [['gain_db', -61], ['gain_db', 13], ['gain_db', '0'], ['mute', 'true']]) {
|
|
23
|
+
const rejected = structuredClone(value);
|
|
24
|
+
rejected.tracks[3].items[0].source[key] = invalid;
|
|
25
|
+
assert.equal(validate(rejected), false);
|
|
26
|
+
assert.ok(validate.errors.some(error => error.instancePath === `/tracks/3/items/0/source/${key}`));
|
|
27
|
+
}
|
|
28
|
+
});
|
|
29
|
+
|
|
18
30
|
test("editV2 is the third root branch and keeps v2 timing/output definitions separate", () => {
|
|
19
31
|
assert.equal(schema.$id, "urn:akari-video:schema:edit:v1");
|
|
20
32
|
assert.match(schema.title, /v0\/v1\/v2/);
|
|
@@ -27,8 +27,8 @@ test("engine capability table declares the version, engines, and status vocabula
|
|
|
27
27
|
assert.deepEqual(table.statuses, ["consumed", "partial", "ignored", "other-subsystem"]);
|
|
28
28
|
});
|
|
29
29
|
|
|
30
|
-
test("all
|
|
31
|
-
assert.equal(canonicalPaths.size,
|
|
30
|
+
test("all 68 generated item, source, and keyframe keys have a capability row", () => {
|
|
31
|
+
assert.equal(canonicalPaths.size, 68);
|
|
32
32
|
const covered = new Set(table.fields.map((field) => field.path));
|
|
33
33
|
assert.deepEqual([...canonicalPaths].filter((path) => !covered.has(path)), []);
|
|
34
34
|
});
|