akari-video 0.1.97 → 0.1.99
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/README.md +19 -0
- package/package.json +1 -1
- package/src/doctor-command.mjs +3 -0
- package/src/runtime-diagnostics.mjs +35 -5
- package/src/store-command.mjs +1 -1
- package/src/store-device-connect.mjs +6 -6
- package/vendor/docs/contract-2026-07-22-render-basics.md +1 -1
- package/vendor/docs/contract-2026-07-23-analysis-person-matte.md +1 -1
- package/vendor/docs/contract-2026-08-02-preview-parity.md +1 -1
- package/vendor/docs/contract-2026-08-03-caption-display-encoding-qc-v1.md +8 -4
- package/vendor/docs/contract-2026-08-28-osr-export-v0.md +5 -3
- package/vendor/docs/contract-2026-08-28-v2-approximation-ledger.md +1 -1
- package/vendor/docs/contract-2026-08-28-v2-audio-roles-v0.md +31 -0
- package/vendor/docs/contract-2026-08-29-capture-v0.md +2 -0
- package/vendor/packages/akari-launcher/README.md +19 -0
- package/vendor/packages/akari-launcher/package.json +1 -1
- package/vendor/packages/asset-resolver/browse/app.js +1 -1
- package/vendor/packages/asset-resolver/src/resolve.mjs +2 -2
- package/vendor/packages/edit-lint/README.md +7 -0
- package/vendor/packages/edit-lint/src/edit-lint.mjs +56 -10
- package/vendor/packages/edit-store/lib/internal-model.js +42 -6
- package/vendor/packages/edit-store/lib/webview-kernel.js +28 -4
- package/vendor/packages/gpu-export/README.md +4 -0
- package/vendor/packages/media-bin/src/preview-audio-sidecar.mjs +4 -1
- package/vendor/packages/media-bin/test/preview-audio-pcm-sidecar.test.mjs +19 -0
- package/vendor/packages/osr-export/README.md +2 -0
- package/vendor/packages/overlay-runtime/runtimes.mjs +108 -13
- package/vendor/packages/overlay-runtime/src/overlay-runtime.js +287 -58
- package/vendor/packages/overlay-runtime/src/three-runtime.js +59 -9
- package/vendor/packages/project-scaffold/src/index.mjs +1 -1
- package/vendor/packages/project-scaffold/test/create-project.test.mjs +2 -2
- package/vendor/packages/render-cut/README.ja.md +1 -1
- package/vendor/packages/render-cut/README.md +1 -1
- package/vendor/packages/render-cut/src/html-scan.mjs +175 -7
- package/vendor/packages/schemas/ai-models.json +4 -4
- package/vendor/skills/analyze-footage/SKILL.md +1 -0
- package/vendor/skills/beat-sync-edit/pitfalls.md +1 -1
- package/vendor/skills/edit-lint/SKILL.md +6 -2
- package/vendor/skills/generate-narration/SKILL.md +6 -6
- package/vendor/skills/generate-narration/engines.md +20 -1
- package/vendor/skills/generate-narration/reading-text.md +3 -1
- package/vendor/skills/overlay-authoring/3d.md +3 -3
- package/vendor/skills/render-cut/SKILL.md +6 -2
package/README.md
CHANGED
|
@@ -123,6 +123,25 @@ npm i -g akari-video && akari
|
|
|
123
123
|
npx akari-video
|
|
124
124
|
```
|
|
125
125
|
|
|
126
|
+
### akari doctor の cli.node
|
|
127
|
+
|
|
128
|
+
`akari doctor --json` の `cli.node` は、この CLI を動かしている実行体と、スクリプトを Node として呼ぶ方法を返す。
|
|
129
|
+
|
|
130
|
+
| フィールド | 意味 |
|
|
131
|
+
|---|---|
|
|
132
|
+
| `exec_path` | 診断を実行した実行体のパス。Electron アプリの EXE になることもある |
|
|
133
|
+
| `electron_run_as_node` | 診断を実行したプロセスの環境変数が `ELECTRON_RUN_AS_NODE=1` だったか |
|
|
134
|
+
| `version` | 実行体の Node のバージョン(Electron では内蔵の Node) |
|
|
135
|
+
| `runtime` | 実行体が `electron` か `node` か |
|
|
136
|
+
| `electron_version` | Electron のバージョン。Node 実行体では `null` |
|
|
137
|
+
| `run_as_node_supported` | Electron 実行体を Node として呼べるか |
|
|
138
|
+
| `required_env` | Node として呼ぶために必要な環境変数。Electron では `{ "ELECTRON_RUN_AS_NODE": "1" }`、Node では `{}` |
|
|
139
|
+
| `invocation` | `shell` は既定シェル、`example` はそのシェル用の呼び方。`cmd` と `sh` は両方常に出る。`<script>` を実際のスクリプトパスに置き換える |
|
|
140
|
+
|
|
141
|
+
`electron_run_as_node` は診断時の環境だけを表す。Node として使えるかは `runtime`、`run_as_node_supported`、`required_env` で判断する。`runtime` が `electron` なら `required_env` を付けて呼ぶ。Windows の `cmd` では `set "ELECTRON_RUN_AS_NODE=1"` と引用符で囲む。`set ELECTRON_RUN_AS_NODE=1 &&` と書くと末尾の空白が値に入る。
|
|
142
|
+
|
|
143
|
+
Electron を Node として呼んだプロセスから GUI の Electron を子プロセスとして起動するときは、子へ渡す環境から `ELECTRON_RUN_AS_NODE` を外す。
|
|
144
|
+
|
|
126
145
|
## 既知の制約
|
|
127
146
|
|
|
128
147
|
- npm tarball は追跡済み skills・雛形・schemas と capability source set を `vendor/` に同梱する。
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "akari-video",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.99",
|
|
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": {
|
package/src/doctor-command.mjs
CHANGED
|
@@ -29,6 +29,9 @@ export async function runDoctorCommand(argv, options = {}) {
|
|
|
29
29
|
export function formatDoctorReport(report) {
|
|
30
30
|
const rows = [
|
|
31
31
|
['cli', 'ok', `v${report.cli.version} — ${report.cli.entry_path}`],
|
|
32
|
+
['node', report.cli.node?.runtime ?? 'unknown', report.cli.node
|
|
33
|
+
? `v${report.cli.node.version} — ${report.cli.node.exec_path}${report.cli.node.runtime === 'electron' ? ' (Node として使うには ELECTRON_RUN_AS_NODE=1 を付けて呼ぶ)' : ''}`
|
|
34
|
+
: '診断情報がありません'],
|
|
32
35
|
['app_managed', report.app_managed.status, detail(report.app_managed.path, report.app_managed.version)],
|
|
33
36
|
['app_bundle', report.app_bundle.found ? 'found' : 'missing', detail(report.app_bundle.path, report.app_bundle.version)],
|
|
34
37
|
['render_cut', report.render_cut.origin, report.render_cut.path ?? '見つかりません'],
|
|
@@ -128,6 +128,36 @@ export function resolveRuntimePaths(options = {}) {
|
|
|
128
128
|
};
|
|
129
129
|
}
|
|
130
130
|
|
|
131
|
+
export function describeNodeRuntime(options = {}) {
|
|
132
|
+
const execPath = options.execPath ?? process.execPath;
|
|
133
|
+
const platform = options.platform ?? process.platform;
|
|
134
|
+
const env = options.env ?? process.env;
|
|
135
|
+
const versions = options.versions ?? process.versions;
|
|
136
|
+
const runtime = versions.electron ? 'electron' : 'node';
|
|
137
|
+
const quotedExecPath = `"${execPath}"`;
|
|
138
|
+
const cmd = runtime === 'electron'
|
|
139
|
+
? `set "ELECTRON_RUN_AS_NODE=1" && ${quotedExecPath} <script>`
|
|
140
|
+
: `${quotedExecPath} <script>`;
|
|
141
|
+
const sh = runtime === 'electron'
|
|
142
|
+
? `ELECTRON_RUN_AS_NODE=1 ${quotedExecPath} <script>`
|
|
143
|
+
: `${quotedExecPath} <script>`;
|
|
144
|
+
return {
|
|
145
|
+
exec_path: execPath,
|
|
146
|
+
electron_run_as_node: env.ELECTRON_RUN_AS_NODE === '1',
|
|
147
|
+
version: options.nodeVersion ?? versions.node,
|
|
148
|
+
runtime,
|
|
149
|
+
electron_version: versions.electron ?? null,
|
|
150
|
+
run_as_node_supported: runtime === 'electron',
|
|
151
|
+
required_env: runtime === 'electron' ? { ELECTRON_RUN_AS_NODE: '1' } : {},
|
|
152
|
+
invocation: {
|
|
153
|
+
shell: platform === 'win32' ? 'cmd' : 'sh',
|
|
154
|
+
example: platform === 'win32' ? cmd : sh,
|
|
155
|
+
cmd,
|
|
156
|
+
sh,
|
|
157
|
+
},
|
|
158
|
+
};
|
|
159
|
+
}
|
|
160
|
+
|
|
131
161
|
export async function resolveDoctorReport(options = {}) {
|
|
132
162
|
const env = options.env ?? process.env;
|
|
133
163
|
const platform = options.platform ?? process.platform;
|
|
@@ -144,11 +174,11 @@ export async function resolveDoctorReport(options = {}) {
|
|
|
144
174
|
cli: {
|
|
145
175
|
version: options.cliVersion ?? readOwnVersion(),
|
|
146
176
|
entry_path: options.entryPath ?? process.argv[1] ?? null,
|
|
147
|
-
node: {
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
},
|
|
177
|
+
node: describeNodeRuntime({
|
|
178
|
+
execPath, platform, env,
|
|
179
|
+
versions: options.versions ?? process.versions,
|
|
180
|
+
nodeVersion: options.nodeVersion,
|
|
181
|
+
}),
|
|
152
182
|
},
|
|
153
183
|
...runtime,
|
|
154
184
|
ffmpeg: mediaBin.ffmpeg,
|
package/src/store-command.mjs
CHANGED
|
@@ -265,7 +265,7 @@ export async function runStoreCommand(args, options = {}) {
|
|
|
265
265
|
return { exitCode: 1 };
|
|
266
266
|
}
|
|
267
267
|
|
|
268
|
-
log('ブラウザで AKARI
|
|
268
|
+
log('ブラウザで AKARI Video Lab を開いて、AKARI アカウントの接続を承認してください。');
|
|
269
269
|
log(` 確認コード: ${start.userCode}`);
|
|
270
270
|
log(` URL: ${start.verificationUrl}`);
|
|
271
271
|
if (!args.includes('--no-open')) {
|
|
@@ -62,10 +62,10 @@ export async function fetchStoreEntitlements(fetchImpl, baseUrl, token) {
|
|
|
62
62
|
headers: { authorization: `Bearer ${token}` }
|
|
63
63
|
});
|
|
64
64
|
} catch (error) {
|
|
65
|
-
return { error:
|
|
65
|
+
return { error: `AKARI Video Lab に接続できませんでした: ${error instanceof Error ? error.message : String(error)}` };
|
|
66
66
|
}
|
|
67
67
|
if (response.status === 401) return { error: 'トークンが無効です。マイページで発行し直してください。' };
|
|
68
|
-
if (!response.ok) return { error:
|
|
68
|
+
if (!response.ok) return { error: `AKARI Video Lab がエラーを返しました(${response.status})` };
|
|
69
69
|
return { data: await response.json() };
|
|
70
70
|
}
|
|
71
71
|
|
|
@@ -125,16 +125,16 @@ export async function startDeviceConnection({
|
|
|
125
125
|
} catch (error) {
|
|
126
126
|
return {
|
|
127
127
|
status: 'network-error',
|
|
128
|
-
error:
|
|
128
|
+
error: `AKARI Video Lab に接続できませんでした: ${error instanceof Error ? error.message : String(error)}`
|
|
129
129
|
};
|
|
130
130
|
}
|
|
131
131
|
if (!response.ok) {
|
|
132
|
-
return { status: 'error', error:
|
|
132
|
+
return { status: 'error', error: `AKARI Video Lab に接続できませんでした(${response.status})` };
|
|
133
133
|
}
|
|
134
134
|
const body = await response.json().catch(() => ({}));
|
|
135
135
|
if (typeof body.device_code !== 'string' || typeof body.user_code !== 'string'
|
|
136
136
|
|| typeof body.verification_url !== 'string') {
|
|
137
|
-
return { status: 'error', error: '
|
|
137
|
+
return { status: 'error', error: 'AKARI Video Lab から接続に必要な情報を受け取れませんでした。' };
|
|
138
138
|
}
|
|
139
139
|
const result = {
|
|
140
140
|
status: 'started',
|
|
@@ -168,7 +168,7 @@ export async function pollDeviceConnection({
|
|
|
168
168
|
} catch (error) {
|
|
169
169
|
return {
|
|
170
170
|
status: 'network-error',
|
|
171
|
-
error:
|
|
171
|
+
error: `AKARI Video Lab に接続できませんでした: ${error instanceof Error ? error.message : String(error)}`
|
|
172
172
|
};
|
|
173
173
|
}
|
|
174
174
|
if (response.status === 410) {
|
|
@@ -178,5 +178,5 @@ x264、その他の環境で x264 の順に解決する。ハードウェア対
|
|
|
178
178
|
OSR / GPU の v2 書き出しでは、映像エンジンが `edit.sources` から直接描画するため、cut 段と
|
|
179
179
|
tail-pad 段は音声専用中間物だけを生成する。通常は `cut-audio.mp4`、最終尺までの音声 padding が
|
|
180
180
|
必要な場合は `cut-audio-tail-padded.mp4` を使用し、どちらも `-vn` で映像を処理しない。
|
|
181
|
-
legacy
|
|
181
|
+
旧 legacy 書き出しは `cut.mp4` と必要時の `cut-tail-padded.mp4` を使用していた(現在は廃止)。
|
|
182
182
|
音声入力は cut ごとに入力側シーク(`-ss` / `-t`)し、cut 頭 0.5 s の先読みガード(AAC の overlap-add 用)を設け、cut 段の費用を素材長に依存させない。
|
|
@@ -279,7 +279,7 @@ frame-engine を使う消費側は、VP9 alpha WebM または alpha pixel format
|
|
|
279
279
|
明示 `mask` がある場合も color / mask の取り込みは行い、frame-engine のマスク入力には明示参照を優先する。
|
|
280
280
|
|
|
281
281
|
変換失敗は非致命警告とし、該当 layer だけを frame-engine から外す。旧 `<video>` プレビューは
|
|
282
|
-
元素材の `src`
|
|
282
|
+
元素材の `src` を維持する。旧 legacy 書き出しの alpha filtergraph はこの取り込み変換を使わなかった(現在は廃止)。
|
|
283
283
|
|
|
284
284
|
## 9. 既知の追随事項(本契約が作る宿題)
|
|
285
285
|
|
|
@@ -258,7 +258,7 @@ legacy 合成経路は、移行中の既存利用者と Windows を支えるた
|
|
|
258
258
|
エンジン意味論や完成画の仕様の正本ではない。
|
|
259
259
|
|
|
260
260
|
- `<video>` ベースの二つの互換プレビューは、各器で frame-engine が既定になった後 **2 リリース**保持する。
|
|
261
|
-
- legacy
|
|
261
|
+
- 当時は legacy 書き出しを明示的な互換選択で残す計画だった。現在は廃止済み。
|
|
262
262
|
- legacy 削除(#100b)は、#14 の Windows 実機で OSR が PASS するか、Windows でも OSR を既定にする
|
|
263
263
|
オーナー裁定がある場合にだけ開始する。
|
|
264
264
|
- 退役前も新しい意味論を互換経路へ追加しない。差は清算表へ記録し、v2 の合否は §4.1 と §4.2 で決める。
|
|
@@ -163,15 +163,18 @@ The `filter_report.normalized.output_tp` loudnorm reports for the PCM stage is n
|
|
|
163
163
|
real true peak: the AAC re-encode that follows can measurably overshoot it (a real render measured
|
|
164
164
|
`filter_report` at -1.00 dBTP against a decoded artifact at +0.23 dBFS — about +1.2 dB of
|
|
165
165
|
codec-introduced overshoot; `planning/notes-2026-08-17-mac-fresh-install-bug-reports.md` #05). Two
|
|
166
|
-
additive mitigations apply
|
|
166
|
+
additive mitigations apply to explicit `true_peak_dbtp` in `audio.master` — the -1.5
|
|
167
167
|
dBTP default is unchanged and unmargined:
|
|
168
168
|
|
|
169
|
-
- **Applied margin.**
|
|
169
|
+
- **Applied margin.** Only when the output audio codec is AAC (h264 / hevc),
|
|
170
|
+
`packages/render-cut/src/plan.mjs` hands loudnorm `configured -
|
|
170
171
|
AAC_TRUE_PEAK_OVERSHOOT_MARGIN_DBTP` (1.5 dB, `packages/render-cut/src/audio-qc.mjs`) instead of
|
|
171
172
|
the raw configured value, so the *decoded* artifact — not just the PCM stage — has a better chance
|
|
172
173
|
of landing under what the caller asked for. The receipt records both under an additive
|
|
173
|
-
`audio_qc.true_peak_margin: { overshoot_margin_dbtp, applied_true_peak_dbtp }` field;
|
|
174
|
+
`audio_qc.true_peak_margin: { overshoot_margin_dbtp, applied_true_peak_dbtp, reason: "aac_reencode_overshoot", audio_codec: "aac" }` field;
|
|
174
175
|
`audio_qc.configured.true_peak_dbtp` is unchanged and still reports the caller's original value.
|
|
176
|
+
PCM output (prores422 / png → `pcm_s16le`) passes the configured target directly to loudnorm
|
|
177
|
+
and has no `audio_qc.true_peak_margin` field (#122, 2026-09-28).
|
|
175
178
|
The margin is a fixed mitigation, not a guarantee — real-render testing found synthetic
|
|
176
179
|
high-transient material where even the margined target still decodes above 0 dBFS (this is what
|
|
177
180
|
the next mitigation exists to catch).
|
|
@@ -183,7 +186,8 @@ dBTP default is unchanged and unmargined:
|
|
|
183
186
|
configured target, decoded measurement, and tool version; a mismatch is a structural error.
|
|
184
187
|
|
|
185
188
|
These two fields are additive to the existing `configured` / `filter_report` /
|
|
186
|
-
`decoded_measurement` fields. No margin is recorded when `true_peak_dbtp` is omitted
|
|
189
|
+
`decoded_measurement` fields. No margin is recorded when `true_peak_dbtp` is omitted or the output
|
|
190
|
+
audio codec is PCM, and no
|
|
187
191
|
overshoot warning is recorded when the measured peak does not exceed the threshold.
|
|
188
192
|
|
|
189
193
|
## 4. Recipe boundary and evidence grade
|
|
@@ -55,7 +55,7 @@ A = 255
|
|
|
55
55
|
|
|
56
56
|
BGRA bitmapでは `[0x55, G, R, 255]` となる。左端、中央、右端の3画素を復号し、期待番号との全点一致を要求する。確認後、ffmpegへ渡す前に `buffer.subarray(0, W * H * 4)` で最下行を除く。
|
|
57
57
|
|
|
58
|
-
`--verify stamp|hash|off` を持ち、既定は `stamp` とする。`hash` は直前の映像領域と同じSHA-256ならsettle
|
|
58
|
+
`--verify stamp|hash|off` を持ち、既定は `stamp` とする。`hash` は直前の映像領域と同じSHA-256ならsettle後に再取得し、上限は8回かつ`OSR_STAMP_RETRY_BUDGET_MS`、待ちは伸ばさない。静止画で上限へ達した場合は曖昧件数を記録して受理する。`off` は比較計測用である。通常書き出しではverifyを無効にしない。
|
|
59
59
|
|
|
60
60
|
## 4. 駆動プロトコル
|
|
61
61
|
|
|
@@ -71,6 +71,8 @@ seek → ready → invalidate → paint → verify → write
|
|
|
71
71
|
|
|
72
72
|
`run.json` はseek、paint、toBitmap、verify、writeのp50/p95、1000コマ区切りmedian、先頭と末尾のdriftRatio、paint timeout、verify retry、verify前delta histogram、backpressure、メモリ、ffprobe結果を記録する。
|
|
73
73
|
|
|
74
|
+
**2026-09-28 追記(#113 / #88)**: stampの再試行は時間予算3000 msと回数上限32の先に達した方で失敗する。2回目からsettleの前に16、32、64、…最大500 msを追加で待つ。並走実測では再試行1回が約33〜40 ms、2〜3本並走でも最大2回、1コマ平均が212〜312 msだった。従来の固定8回は約0.3秒で諦めていたため、3秒は並走中の平均1コマの約10倍の猶予とする。失敗文は従来の先頭、読めたstamp値と分類、活性オーバーレイとCSS機能、予算、再実行の案内を順に含む。`run.json` の `verify.budget` / `verify.retryElapsedMs` は完了・失敗の両方に、`verify.failure` はstamp失敗時だけに記録する。
|
|
75
|
+
|
|
74
76
|
## 5. LUT
|
|
75
77
|
|
|
76
78
|
`output.look` のLUTはframe-engine canvas内のsampler3Dで適用する。ページ全体へCSS filterを掛けない。したがって字幕、自由HTML、3DはLUTの外にあり、映像canvasだけが色変換の対象となる。
|
|
@@ -85,7 +87,7 @@ seek → ready → invalidate → paint → verify → write
|
|
|
85
87
|
|
|
86
88
|
第1段・第2段とも、実プロセスのコマンドラインに`--force-device-scale-factor=1`、`--force-color-profile=srgb`、background throttling無効化スイッチを渡す。npm Electronではスクリプトパスを`argv[1]`に保ち、その後へChromiumスイッチを置く。ソフト描画時は加えてGPU無効化とSwiftShaderスイッチを渡す。
|
|
87
89
|
|
|
88
|
-
第1段・第2段とも`--user-data-dir
|
|
90
|
+
第1段・第2段とも`--user-data-dir`を渡し、本体アプリの単一インスタンスロック(userData単位)と分離する。既定は`launchElectronExport`が書き出しごとに`os.tmpdir()`直下へ`mkdtemp("akari-osr-")`で作る短い一意なディレクトリで、子のclose後に`finally`で削除する。呼び出し側が`userDataDir`を明示した場合はそれを使い、作らず消さない。これによりアプリ起動中でも書き出せる。子がexit 0で終了して出力を作らなかった場合は、launcherが失敗として扱う。**2026-09-28 改訂(#114)**: 出力の隣に置くとプロジェクトのパス長とChromiumのキャッシュ階層がWindowsのMAX_PATHを超え、キャッシュ作成エラーがstderrに出ていた。
|
|
89
91
|
|
|
90
92
|
**Windows のアプリ別 GPU 設定の一時上書き(2026-09-01 追記・§11.7)**: `platform === "win32"` かつソフト描画でないとき、第1段・第2段とも launcher(`launchElectronExport`・gpu / osr 共通の spawn 点)は、`auto` では GPU 出口(`options.exit === "gpu"`・gpu-export の electron-main)のときだけ、`force` では OSR 出口でも、spawn の直前に `HKCU\Software\Microsoft\DirectX\UserGpuPreferences` へ「値名 = 実行体のフルパス(`path.win32.resolve` で正規化)・REG_SZ `GpuPreference=2;`」を書き、子の `close` 後(exit code に関わらず・spawn エラーでも)に `finally` で必ず 1 回復元する(無かったなら削除・あったなら元の値へ)。方針は呼び出し側の `gpuPreference` → env `AKARI_EXPORT_GPU_PREFERENCE` → `auto` の順に解決し、`auto` は利用者が明示した値(`GpuPreference=1;` 等)を黙って上書きしない(`force` だけが上書き + 復元する)。書く前に sidecar `<AKARI_HOME ?? ~/.akari>/gpu-preference-override.json` を置き、復元後に削除する。`launchElectronExport` は毎回冒頭で sidecar があれば先に復元する。記録は戻り値の `gpuPreference`(`exit` 込み)と receipt の `provenance.gpu_preference`。OSR 出口を `auto` で外す根拠は §11.7(2026-09-02 改訂)。他 OS はバイト同一の no-op(記録に `reason: platform` だけ残す)。開発用に `AKARI_EXPORT_ALLOW_DESKTOP=0` で第1段(インストール済みアプリ)を候補から外せる(明示引数 `allowDesktop` が env より優先)。
|
|
91
93
|
|
|
@@ -195,7 +197,7 @@ fieldtest / 検収に receipt が 1 件も無く、一度も動いていない
|
|
|
195
197
|
|
|
196
198
|
v0.1.27 からの挙動: `resolveOsrLauncher`(製品入口)はインストール済みアプリを既定で候補から外す
|
|
197
199
|
(`allowInstalledDesktop: false`)。`AKARI_OSR_ELECTRON` の明示指定は従来どおり tier 1。npm Electron(tier 2)が無い
|
|
198
|
-
|
|
200
|
+
当時のパッケージ版では tier 3 = legacy へ警告付きで落ちていた。現在は legacy へフォールバックせず、OSR の Electron が見つからない場合は書き出しを拒否する。
|
|
199
201
|
根本修正 = `--render` を Theia より前に捕捉する書き出し専用の Electron 入口(別票)。入口が入ったら既定を戻す。
|
|
200
202
|
|
|
201
203
|
**2026-08-29 追記(根治)**: 書き出し専用の入口 `apps/shell/electron-entry.js` が合流した(§6 / §11.4)。`resolveOsrLauncher` の既定を戻し、インストール済みアプリを再び tier 1 の候補にする(v0.1.28〜)。`allowInstalledDesktop: false` は明示の opt-out として残す。
|
|
@@ -176,3 +176,34 @@ frame-engine は映像の ready を先に成立させ、その直後から予定
|
|
|
176
176
|
他の圧縮音声は符号化サイズの 16 倍で行う。再生開始は先読み済みの項目を再利用し、未着の項目だけを
|
|
177
177
|
待つため、先読み処理は初期フレームの描画を止めない。decode に失敗した項目は 5 秒空けて再試行し、
|
|
178
178
|
それまで `debug().prefetch.failed` に載る。
|
|
179
|
+
|
|
180
|
+
## 9. BGM の重ね(クロスフェード)
|
|
181
|
+
|
|
182
|
+
複数の `role:"bgm"` item は別の audio トラックに置けば時間で重ねてよい。
|
|
183
|
+
重なり区間は両方が鳴る。render-cut は各 BGM にフェードと ducking 包絡を掛けてから
|
|
184
|
+
`amix normalize=0` で足す。クロスフェードは前の item の `fade_out` と後の item の
|
|
185
|
+
`fade_in` に秒数を指定する。
|
|
186
|
+
|
|
187
|
+
edit-lint は「前に `fade_out` が無く、かつ後に `fade_in` が無い」重なりだけを
|
|
188
|
+
`v2.audio-bgm-multiple` の warning にする(2026-09-28 に error から緩和。#120)。
|
|
189
|
+
同じトラック内の重なりは従来どおり `v2.track-no-overlap` の error となる。
|
|
190
|
+
|
|
191
|
+
実書き出しの測定は `packages/render-cut/test/bgm-crossfade-overlap.test.mjs` に固定した。
|
|
192
|
+
2026-09-28 に Windows・ffmpeg 9.0 で測定。帯域レベルは各時刻の 0.2 秒窓に
|
|
193
|
+
`bandpass`(Q=15)と `volumedetect` を適用し、ピークは `astats` で測った(単位は dB / dBFS)。
|
|
194
|
+
|
|
195
|
+
| 測定項目 | フェードあり | フェードなし |
|
|
196
|
+
|---|---:|---:|
|
|
197
|
+
| 440 Hz 帯域、1.0 / 1.55 / 1.75 / 1.95 / 2.5 秒 | -9.4 / -10.7 / -15.5 / -26.5 / -36.1 | -9.4 / -9.4 / -9.4 / -11.5 / -36.1 |
|
|
198
|
+
| 880 Hz 帯域、同時刻 | -36.1 / -23.7 / -14.6 / -10.0 / -9.3 | -36.1 / -10.3 / -9.2 / -9.3 / -9.3 |
|
|
199
|
+
| 素材 `a.wav` のピーク | -3.06 | -3.06 |
|
|
200
|
+
| 出力全体のピーク | -5.82 | -0.96 |
|
|
201
|
+
| 重なり区間(1.5〜2.0 秒)のピーク | -6.05 | -0.96 |
|
|
202
|
+
| 単独区間・前(0.5〜1.3 秒)のピーク | -5.89 | -5.89 |
|
|
203
|
+
| 単独区間・後のピーク | -5.82 | — |
|
|
204
|
+
|
|
205
|
+
フェードありは重なり区間で両周波数が鳴りつつ、ピークは単独区間を超えない
|
|
206
|
+
(-6.05 対 -5.89 dBFS)。フェードなしは重なり区間のピークが約 4.9 dB 上がる
|
|
207
|
+
(-0.96 dBFS。素材がもっと大きければ 0 dBFS に届き得る)。このためフェードなしの重なりを warning にする。
|
|
208
|
+
|
|
209
|
+
プレビュー(Web Audio)側の実測は未実施。
|
|
@@ -118,6 +118,8 @@ akari capture [-p <project>] (-t <time…> | --auto) [--separate] [--full] [--pe
|
|
|
118
118
|
|
|
119
119
|
## 9. v1 改訂(2026-08-30)— v2 経路への載せ替え
|
|
120
120
|
|
|
121
|
+
> **現況(2026-09-27):** 以下の legacy 分岐と非 macOS フォールバックは当時の設計記録。現在の書き出しは全 OS で `auto` が GPU 適格性により GPU / OSR を選び、`--engine legacy` は拒否する。
|
|
122
|
+
|
|
121
123
|
### 9.1 なぜ改訂するか(確定事実)
|
|
122
124
|
|
|
123
125
|
- v0 の実装は `packages/akari-tools/src/capture/run.mjs` が `renderProject(…, { engine: "legacy" })` を**固定**で呼び、
|
|
@@ -123,6 +123,25 @@ npm i -g akari-video && akari
|
|
|
123
123
|
npx akari-video
|
|
124
124
|
```
|
|
125
125
|
|
|
126
|
+
### akari doctor の cli.node
|
|
127
|
+
|
|
128
|
+
`akari doctor --json` の `cli.node` は、この CLI を動かしている実行体と、スクリプトを Node として呼ぶ方法を返す。
|
|
129
|
+
|
|
130
|
+
| フィールド | 意味 |
|
|
131
|
+
|---|---|
|
|
132
|
+
| `exec_path` | 診断を実行した実行体のパス。Electron アプリの EXE になることもある |
|
|
133
|
+
| `electron_run_as_node` | 診断を実行したプロセスの環境変数が `ELECTRON_RUN_AS_NODE=1` だったか |
|
|
134
|
+
| `version` | 実行体の Node のバージョン(Electron では内蔵の Node) |
|
|
135
|
+
| `runtime` | 実行体が `electron` か `node` か |
|
|
136
|
+
| `electron_version` | Electron のバージョン。Node 実行体では `null` |
|
|
137
|
+
| `run_as_node_supported` | Electron 実行体を Node として呼べるか |
|
|
138
|
+
| `required_env` | Node として呼ぶために必要な環境変数。Electron では `{ "ELECTRON_RUN_AS_NODE": "1" }`、Node では `{}` |
|
|
139
|
+
| `invocation` | `shell` は既定シェル、`example` はそのシェル用の呼び方。`cmd` と `sh` は両方常に出る。`<script>` を実際のスクリプトパスに置き換える |
|
|
140
|
+
|
|
141
|
+
`electron_run_as_node` は診断時の環境だけを表す。Node として使えるかは `runtime`、`run_as_node_supported`、`required_env` で判断する。`runtime` が `electron` なら `required_env` を付けて呼ぶ。Windows の `cmd` では `set "ELECTRON_RUN_AS_NODE=1"` と引用符で囲む。`set ELECTRON_RUN_AS_NODE=1 &&` と書くと末尾の空白が値に入る。
|
|
142
|
+
|
|
143
|
+
Electron を Node として呼んだプロセスから GUI の Electron を子プロセスとして起動するときは、子へ渡す環境から `ELECTRON_RUN_AS_NODE` を外す。
|
|
144
|
+
|
|
126
145
|
## 既知の制約
|
|
127
146
|
|
|
128
147
|
- npm tarball は追跡済み skills・雛形・schemas と capability source set を `vendor/` に同梱する。
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "akari-video",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.99",
|
|
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": [
|
|
@@ -89,7 +89,7 @@
|
|
|
89
89
|
const say = (ok, text) => { msg.className = `msg ${ok ? 'ok' : 'err'}`; msg.textContent = text; };
|
|
90
90
|
|
|
91
91
|
if (item.state === 'locked') {
|
|
92
|
-
actions.append(el(`<button class="btn" disabled>未購入(¥${(item.price ?? 0).toLocaleString()}) —
|
|
92
|
+
actions.append(el(`<button class="btn" disabled>未購入(¥${(item.price ?? 0).toLocaleString()}) — AKARI Video Lab で購入してください</button>`));
|
|
93
93
|
} else if (item.state === 'cached') {
|
|
94
94
|
actions.append(el('<p class="note">✓ ライブラリに取得済みです。</p>'));
|
|
95
95
|
} else {
|
|
@@ -118,7 +118,7 @@ export async function resolve(
|
|
|
118
118
|
const { ids: entitlements } = await fetchEntitlements({ env, fetchImpl });
|
|
119
119
|
if (!entitlements.has(item.id) && !entitlements.has(item.product_id)) {
|
|
120
120
|
throw new AssetResolverError(
|
|
121
|
-
`未購入の素材です(¥${price.toLocaleString()}
|
|
121
|
+
`未購入の素材です(¥${price.toLocaleString()})。AKARI Video Lab で購入してから再度お試しください: ${item.id}`,
|
|
122
122
|
'locked',
|
|
123
123
|
);
|
|
124
124
|
}
|
|
@@ -200,7 +200,7 @@ async function resolvePaidZip(item, { env, fetchImpl, project, reference, home,
|
|
|
200
200
|
if (!credentials) {
|
|
201
201
|
// entitled 判定(fetchEntitlements)が通った直後にここへ来るので通常は発生しないが、
|
|
202
202
|
// その間にトークンが失効した場合も黙って劣化させず拒否する(fail-closed)。
|
|
203
|
-
throw new AssetResolverError(
|
|
203
|
+
throw new AssetResolverError(`AKARI アカウントの接続情報がありません(トークン失効の可能性): ${item.id}`, 'locked');
|
|
204
204
|
}
|
|
205
205
|
|
|
206
206
|
await mkdir(home, { recursive: true });
|
|
@@ -78,6 +78,13 @@ non-decreasing `at` order. It does not change the render meaning or fail lint.
|
|
|
78
78
|
|
|
79
79
|
`timeline.duration-derived` is an info finding when a project without visual media derives its duration from overlays, captions, narration, or SFX; BGM never extends that fallback duration.
|
|
80
80
|
|
|
81
|
+
For v2, `at` and `duration` are frames; `source.in`, `source.out`, and fade values are seconds.
|
|
82
|
+
`v2.item-duration-short` warns when a non-audio item is shorter than half a second (excluding
|
|
83
|
+
caption items). `v2.item-duration-source-mismatch` warns when a media item's source interval,
|
|
84
|
+
after speed adjustment, differs from its timeline duration by at least fivefold. The latter
|
|
85
|
+
also applies to audio items but excludes still images and frozen sources. Both are warnings
|
|
86
|
+
and leave the pass/fail verdict unchanged.
|
|
87
|
+
|
|
81
88
|
## Engine compatibility (`--engine`)
|
|
82
89
|
|
|
83
90
|
Before export, check v2 projects against the selected renderer. Use `auto` when engine selection is
|
|
@@ -204,7 +204,8 @@ export async function lintProject(input, options = {}) {
|
|
|
204
204
|
if (findings.some(finding => finding.severity === "error")) {
|
|
205
205
|
return writeResult(findings, skipped, inputs, paths, options);
|
|
206
206
|
}
|
|
207
|
-
const rawEdit = edit
|
|
207
|
+
const rawEdit = edit.version === 2 && edit.sources === undefined
|
|
208
|
+
? { ...edit, sources: [] } : edit;
|
|
208
209
|
const internalEdit = readEditWithProviderGuidance(rawEdit, { allowCutAudioSplit: true });
|
|
209
210
|
const legacyEdit = projectLegacyEdit(internalEdit);
|
|
210
211
|
if (engineCapabilities !== null && rawEdit.version === 2) {
|
|
@@ -1029,7 +1030,7 @@ function validateEditStructure(edit, findings, paths) {
|
|
|
1029
1030
|
validateLook(edit.output.look, findings, "edit.json#output.look");
|
|
1030
1031
|
}
|
|
1031
1032
|
const sourceIds = new Set();
|
|
1032
|
-
if (!Array.isArray(edit.sources)) {
|
|
1033
|
+
if (!Array.isArray(edit.sources) && !(edit.version === 2 && edit.sources === undefined)) {
|
|
1033
1034
|
structureFinding(findings, editRelative, "sources must be an array");
|
|
1034
1035
|
} else {
|
|
1035
1036
|
for (const [index, source] of edit.sources.entries()) {
|
|
@@ -1154,6 +1155,8 @@ function validateEditV2(edit, findings) {
|
|
|
1154
1155
|
}
|
|
1155
1156
|
}
|
|
1156
1157
|
|
|
1158
|
+
const fps = edit.output?.fps;
|
|
1159
|
+
|
|
1157
1160
|
for (const [trackIndex, track] of edit.tracks.entries()) {
|
|
1158
1161
|
if (!isRecord(track) || !Array.isArray(track.items)) continue;
|
|
1159
1162
|
const visit = (items, parent, parentPath) => {
|
|
@@ -1168,6 +1171,34 @@ function validateEditV2(edit, findings) {
|
|
|
1168
1171
|
if (track.lane === "audio" && Object.hasOwn(item, "link")) {
|
|
1169
1172
|
linkedAudioItems.push({ item, path: `${itemPath}.link` });
|
|
1170
1173
|
}
|
|
1174
|
+
if (isPositiveNumber(fps) && Number.isInteger(item.duration) && item.duration > 0) {
|
|
1175
|
+
const duration = item.duration;
|
|
1176
|
+
const seconds = (duration / fps).toFixed(2);
|
|
1177
|
+
if (track.lane !== "audio" && item.source?.kind !== "caption"
|
|
1178
|
+
&& duration < Math.round(0.5 * fps)) {
|
|
1179
|
+
addFinding(findings, {
|
|
1180
|
+
severity: "warning", check: "v2.item-duration-short",
|
|
1181
|
+
message: `item ${String(item.id)} の duration は ${duration} フレーム(${seconds} 秒)です。at / duration の単位はフレームです。${duration} 秒のつもりなら ${formatNumber(duration * fps)} フレームにしてください(×fps = ${formatNumber(fps)})。`,
|
|
1182
|
+
path: `${itemPath}.duration`,
|
|
1183
|
+
});
|
|
1184
|
+
}
|
|
1185
|
+
const source = item.source;
|
|
1186
|
+
if (source?.kind === "media" && isFiniteNumber(source.out)
|
|
1187
|
+
&& !isStillImageSourcePath(sourcePaths.get(source.src)) && !isRecord(source.freeze)) {
|
|
1188
|
+
const sourceIn = source.in ?? 0;
|
|
1189
|
+
const sourceSeconds = (effectiveSourceOut(item, fps) - sourceIn)
|
|
1190
|
+
/ (isPositiveNumber(source.speed) ? source.speed : 1);
|
|
1191
|
+
const timelineSeconds = duration / fps;
|
|
1192
|
+
const ratio = Math.max(sourceSeconds / timelineSeconds, timelineSeconds / sourceSeconds);
|
|
1193
|
+
if (sourceSeconds > 0 && ratio >= 5) {
|
|
1194
|
+
addFinding(findings, {
|
|
1195
|
+
severity: "warning", check: "v2.item-duration-source-mismatch",
|
|
1196
|
+
message: `item ${String(item.id)} の duration ${duration} フレーム(${seconds} 秒)と source の区間 in ${formatNumber(sourceIn)} 秒 〜 out ${formatNumber(source.out)} 秒(${sourceSeconds.toFixed(2)} 秒${isPositiveNumber(source.speed) && source.speed !== 1 ? `・speed ${formatNumber(source.speed)}` : ""})が ${ratio.toFixed(1)} 倍食い違っています。at / duration はフレーム、source.in / out は秒です。取り違えていないか確かめてください(duration を秒で書いたなら ×fps = ${formatNumber(fps)})。`,
|
|
1197
|
+
path: `${itemPath}.duration`,
|
|
1198
|
+
});
|
|
1199
|
+
}
|
|
1200
|
+
}
|
|
1201
|
+
}
|
|
1171
1202
|
if (track.lane === "visual" && item.source?.kind === "media" && item.audio === false
|
|
1172
1203
|
&& (Object.hasOwn(item.source, "gain_db") || Object.hasOwn(item.source, "mute"))) {
|
|
1173
1204
|
addFinding(findings, {
|
|
@@ -1258,6 +1289,7 @@ function validateEditV2(edit, findings) {
|
|
|
1258
1289
|
? track.items.flatMap((item, itemIndex) =>
|
|
1259
1290
|
isRecord(item) && item.role === "bgm" ? [{
|
|
1260
1291
|
id: item.id, trackIndex, itemIndex, start: item.at, end: item.at + item.duration,
|
|
1292
|
+
fadeIn: item.fade_in, fadeOut: item.fade_out,
|
|
1261
1293
|
}] : [])
|
|
1262
1294
|
: []
|
|
1263
1295
|
);
|
|
@@ -1269,13 +1301,19 @@ function validateEditV2(edit, findings) {
|
|
|
1269
1301
|
const start = Math.max(bgmItems[i].start, bgmItems[j].start);
|
|
1270
1302
|
const end = Math.min(bgmItems[i].end, bgmItems[j].end);
|
|
1271
1303
|
if (Number.isFinite(start) && Number.isFinite(end) && start < end) {
|
|
1272
|
-
|
|
1304
|
+
const [before, after] = bgmItems[i].start <= bgmItems[j].start
|
|
1305
|
+
? [bgmItems[i], bgmItems[j]] : [bgmItems[j], bgmItems[i]];
|
|
1306
|
+
if (!(before.fadeOut > 0) && !(after.fadeIn > 0)) {
|
|
1307
|
+
overlaps.push({ label: `${String(before.id)} / ${String(after.id)} [${start}, ${end})`, frames: end - start });
|
|
1308
|
+
}
|
|
1273
1309
|
}
|
|
1274
1310
|
}
|
|
1275
1311
|
}
|
|
1312
|
+
const exampleSeconds = overlaps.length && isPositiveNumber(fps)
|
|
1313
|
+
? formatNumber(Number((overlaps[0].frames / fps).toFixed(2))) : null;
|
|
1276
1314
|
if (overlaps.length) addFinding(findings, {
|
|
1277
|
-
severity: "
|
|
1278
|
-
message: `BGM ${bgmItems.map(describe).join('、')}。重なり: ${overlaps.
|
|
1315
|
+
severity: "warning", check: "v2.audio-bgm-multiple",
|
|
1316
|
+
message: `BGM ${bgmItems.map(describe).join('、')}。重なり: ${overlaps.map(overlap => overlap.label).join('、')}。重なった区間は両方の BGM が鳴ります。クロスフェードにするなら、前の item に fade_out(秒)、後の item に fade_in(秒)を付けてください${exampleSeconds !== null ? `(例: 重なり ${formatNumber(overlaps[0].frames)} フレーム = ${exampleSeconds} 秒なら "fade_out": ${exampleSeconds} / "fade_in": ${exampleSeconds})` : ""}。意図しない重なりなら時間が重ならないように配置してください。(同じトラックの中では重ねられません。別の audio トラックに置いてください)`,
|
|
1279
1317
|
path: "edit.json#tracks",
|
|
1280
1318
|
});
|
|
1281
1319
|
}
|
|
@@ -2198,19 +2236,27 @@ function validateSfxTracks(sfx, findings) {
|
|
|
2198
2236
|
const hasValidTrack = Object.hasOwn(item, "track") && Number.isInteger(item.track) && item.track >= 0;
|
|
2199
2237
|
const track = hasValidTrack ? item.track : 0;
|
|
2200
2238
|
if (!pointsByTrack.has(track)) pointsByTrack.set(track, []);
|
|
2201
|
-
pointsByTrack.get(track).push({ index, t: item.t });
|
|
2239
|
+
pointsByTrack.get(track).push({ index, t: item.t, path: isNonEmptyString(item.path) ? item.path : undefined, track });
|
|
2202
2240
|
});
|
|
2203
2241
|
for (const list of pointsByTrack.values()) {
|
|
2204
2242
|
list.sort((a, b) => a.t - b.t);
|
|
2243
|
+
let groupStart = 0;
|
|
2205
2244
|
for (let i = 1; i < list.length; i += 1) {
|
|
2206
2245
|
if (Math.abs(list[i].t - list[i - 1].t) <= EPSILON) {
|
|
2246
|
+
const current = list[i];
|
|
2247
|
+
const samePath = current.path !== undefined
|
|
2248
|
+
&& list.slice(groupStart, i).some(previous => previous.path !== undefined && previous.path === current.path);
|
|
2207
2249
|
addFinding(findings, {
|
|
2208
|
-
severity: "warning",
|
|
2250
|
+
severity: samePath ? "warning" : "info",
|
|
2209
2251
|
check: "audio.sfx.track-overlap",
|
|
2210
|
-
message:
|
|
2211
|
-
|
|
2212
|
-
|
|
2252
|
+
message: samePath
|
|
2253
|
+
? `効果音 audio.sfx[${current.index}] が同じ track ${current.track}・同じ時刻 ${formatNumber(current.t)} 秒に同じ素材 ${current.path} で置かれています(二重置きの可能性)。意図した重ねなら track を分けてください(書き出しのミックスは track を見ずに全部鳴らしますが、NLE 書き出しは track を NLE のトラック単位にするため同じトラックに重なります)。`
|
|
2254
|
+
: `効果音 audio.sfx[${current.index}] が同じ track ${current.track}・同じ時刻 ${formatNumber(current.t)} 秒に別素材と重なっています。書き出しのミックスは track を見ずに両方鳴らします。重ねるつもりなら track を分けてください(NLE 書き出しは track を NLE のトラック単位にするため、同じトラックに重なります)。`,
|
|
2255
|
+
path: `edit.json#audio.sfx[${current.index}]`,
|
|
2256
|
+
range: { start: current.t, end: current.t },
|
|
2213
2257
|
});
|
|
2258
|
+
} else {
|
|
2259
|
+
groupStart = i;
|
|
2214
2260
|
}
|
|
2215
2261
|
}
|
|
2216
2262
|
}
|
|
@@ -225,7 +225,7 @@ function readV2Internal(raw) {
|
|
|
225
225
|
// 同時表示も表現できない。先に全 visual media アイテムを横断して区間交差を求め、
|
|
226
226
|
// 参加したアイテムを各トラックの build へ同じ集合として渡す。これにより preview / render
|
|
227
227
|
// の両方が、トラックごとに独立した要素を合成できる layers 経路を選ぶ。
|
|
228
|
-
const overlappingItemIds = computeOverlappingItemIds(edit.tracks.flatMap(track => 'items' in track && track.lane === 'visual' ? [track.items] : []), pathOf);
|
|
228
|
+
const overlappingItemIds = computeOverlappingItemIds(edit.tracks.flatMap(track => 'items' in track && track.lane === 'visual' ? [track.items] : []), pathOf, chromaKeyOf);
|
|
229
229
|
const contentDurationFrames = edit.tracks.reduce((maximum, track) => 'items' in track && track.lane === 'visual' ? track.items.reduce((trackMaximum, item) => Math.max(trackMaximum, item.at + item.duration), maximum) : maximum, 0);
|
|
230
230
|
const tracks = edit.tracks.map(track => {
|
|
231
231
|
// P0 2026-08-21 render-path-unification (実測で発覚): 'cuts' 経路(concat チェーン)は
|
|
@@ -457,7 +457,7 @@ function needsLayersEngine(item, chromaKeyOf, hasOverlappingSibling = false) {
|
|
|
457
457
|
return true;
|
|
458
458
|
return false;
|
|
459
459
|
}
|
|
460
|
-
function analyzeOverlappingItems(itemGroups, pathOf) {
|
|
460
|
+
function analyzeOverlappingItems(itemGroups, pathOf, chromaKeyOf) {
|
|
461
461
|
const overlapping = new Set();
|
|
462
462
|
const crossTrackEvacuations = [];
|
|
463
463
|
const entries = itemGroups.flatMap((group, trackIndex) => group.items.map(item => ({ item, trackIndex, trackId: group.trackId })));
|
|
@@ -497,13 +497,47 @@ function analyzeOverlappingItems(itemGroups, pathOf) {
|
|
|
497
497
|
}
|
|
498
498
|
}
|
|
499
499
|
}
|
|
500
|
+
// 各パスの開始時点の集合だけを原因判定に使う。同じ upper に複数の lower が
|
|
501
|
+
// 重なれば全て記録し、新しく退避した upper は次のパスから原因にする。
|
|
502
|
+
for (;;) {
|
|
503
|
+
const newlyEvacuated = new Set();
|
|
504
|
+
for (let i = 0; i < entries.length; i++) {
|
|
505
|
+
const { item: lower, trackIndex: lowerTrackIndex, trackId: lowerTrackId } = entries[i];
|
|
506
|
+
const lowerIsLayer = lower.source.kind === 'media'
|
|
507
|
+
? needsLayersEngine(lower, chromaKeyOf, overlapping.has(lower.id))
|
|
508
|
+
: lower.source.kind === 'telop' || lower.source.kind === 'filter';
|
|
509
|
+
if (!lowerIsLayer)
|
|
510
|
+
continue;
|
|
511
|
+
for (let j = 0; j < entries.length; j++) {
|
|
512
|
+
const { item: upper, trackIndex: upperTrackIndex, trackId: upperTrackId } = entries[j];
|
|
513
|
+
if (upperTrackIndex <= lowerTrackIndex || upper.source.kind !== 'media'
|
|
514
|
+
|| overlapping.has(upper.id) || needsLayersEngine(upper, chromaKeyOf, false))
|
|
515
|
+
continue;
|
|
516
|
+
if (!(lower.at < upper.at + upper.duration && upper.at < lower.at + lower.duration))
|
|
517
|
+
continue;
|
|
518
|
+
newlyEvacuated.add(upper.id);
|
|
519
|
+
crossTrackEvacuations.push({
|
|
520
|
+
itemId: upper.id,
|
|
521
|
+
trackId: upperTrackId,
|
|
522
|
+
causeItemId: lower.id,
|
|
523
|
+
causeTrackId: lowerTrackId,
|
|
524
|
+
overlapStartFrames: Math.max(lower.at, upper.at),
|
|
525
|
+
overlapEndFrames: Math.min(lower.at + lower.duration, upper.at + upper.duration)
|
|
526
|
+
});
|
|
527
|
+
}
|
|
528
|
+
}
|
|
529
|
+
if (newlyEvacuated.size === 0)
|
|
530
|
+
break;
|
|
531
|
+
for (const id of newlyEvacuated)
|
|
532
|
+
overlapping.add(id);
|
|
533
|
+
}
|
|
500
534
|
return { itemIds: overlapping, crossTrackEvacuations };
|
|
501
535
|
}
|
|
502
|
-
function computeOverlappingItemIds(itemGroups, pathOf) {
|
|
536
|
+
function computeOverlappingItemIds(itemGroups, pathOf, chromaKeyOf) {
|
|
503
537
|
return analyzeOverlappingItems(itemGroups.map((items, index) => ({
|
|
504
538
|
items,
|
|
505
539
|
trackId: String(index)
|
|
506
|
-
})), pathOf).itemIds;
|
|
540
|
+
})), pathOf, chromaKeyOf).itemIds;
|
|
507
541
|
}
|
|
508
542
|
/**
|
|
509
543
|
* 別 visual track との重なりが原因で upper item が layers へ退避される組を返す。
|
|
@@ -513,11 +547,13 @@ function findCrossTrackLayerEvacuations(edit) {
|
|
|
513
547
|
const raw = toRecord(edit);
|
|
514
548
|
const parsed = (0, edit_v2_1.readEditV2)(raw === undefined ? edit : extractV2MediaCaptionSwitches(raw).input);
|
|
515
549
|
const pathOf = (id) => parsed.sources.find(entry => entry.id === id)?.path;
|
|
550
|
+
const chromaKeyOf = (id) => parsed.sources.find(entry => entry.id === id)?.chroma_key ?? undefined;
|
|
516
551
|
return analyzeOverlappingItems(parsed.tracks.flatMap(track => track.lane === 'visual' && 'items' in track
|
|
517
552
|
? [{ items: track.items, trackId: track.id }]
|
|
518
|
-
: []), pathOf).crossTrackEvacuations;
|
|
553
|
+
: []), pathOf, chromaKeyOf).crossTrackEvacuations;
|
|
519
554
|
}
|
|
520
|
-
// cuts の winner-take-all
|
|
555
|
+
// cuts の winner-take-all が下段を隠してよいのは、上段が全画面を不透明に覆い、下段が全部 cuts の場合だけ。
|
|
556
|
+
// 下段に layers があれば上段 cuts はその下に描かれるため、不動点で上段も退避する(#90)。
|
|
521
557
|
// 全画面不透明なソースで transform の単位元を明示しただけなら従来経路を維持する。crop / 半透明 / keyframes は、
|
|
522
558
|
// 現在または途中フレームで下段が見える可能性があるため宣言の存在だけで layers へ退避する。
|
|
523
559
|
// 加えて、アルファを運べるコンテナ(webm / mov — 本製品のマット生成パイプラインの出力形式)は
|