akari-video 0.1.28 → 0.1.30

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.
Files changed (100) hide show
  1. package/package.json +1 -1
  2. package/src/capture-command.mjs +10 -1
  3. package/src/migrate-command.mjs +21 -4
  4. package/vendor/.akari-capability-sources.json +6 -0
  5. package/vendor/docs/contract-2026-07-18-edit-json-v1-sources.md +4 -0
  6. package/vendor/docs/contract-2026-07-22-render-basics.md +9 -1
  7. package/vendor/docs/contract-2026-07-25-project-structure-v0.md +12 -0
  8. package/vendor/docs/contract-2026-08-02-preview-parity.md +48 -0
  9. package/vendor/docs/contract-2026-08-09-transform-keyframes-v0.md +35 -0
  10. package/vendor/docs/contract-2026-08-12-still-image-cut-source-v0.md +6 -0
  11. package/vendor/docs/contract-2026-08-28-gpu-export-v0.md +153 -7
  12. package/vendor/docs/contract-2026-08-28-osr-export-v0.md +24 -1
  13. package/vendor/docs/contract-2026-08-28-v2-audio-roles-v0.md +20 -0
  14. package/vendor/docs/contract-2026-08-29-capture-v0.md +55 -0
  15. package/vendor/docs/contract-2026-08-30-edit-json-v2-object-tree-v0.md +298 -0
  16. package/vendor/docs/contract-2026-08-30-motion-and-keyframes-v0.md +163 -0
  17. package/vendor/packages/akari-launcher/package.json +1 -1
  18. package/vendor/packages/edit-lint/src/edit-lint.mjs +193 -7
  19. package/vendor/packages/edit-store/README.md +101 -0
  20. package/vendor/packages/edit-store/lib/audio-schedule.d.ts +19 -1
  21. package/vendor/packages/edit-store/lib/audio-schedule.js +94 -13
  22. package/vendor/packages/edit-store/lib/canonical.d.ts +3 -0
  23. package/vendor/packages/edit-store/lib/canonical.js +205 -0
  24. package/vendor/packages/edit-store/lib/caption-display.d.ts +2 -2
  25. package/vendor/packages/edit-store/lib/caption-display.js +13 -4
  26. package/vendor/packages/edit-store/lib/edit-v2.d.ts +80 -2
  27. package/vendor/packages/edit-store/lib/edit-v2.js +171 -8
  28. package/vendor/packages/edit-store/lib/generated/edit-v2-keys.d.ts +27 -0
  29. package/vendor/packages/edit-store/lib/generated/edit-v2-keys.js +213 -0
  30. package/vendor/packages/edit-store/lib/index.d.ts +2 -0
  31. package/vendor/packages/edit-store/lib/index.js +2 -0
  32. package/vendor/packages/edit-store/lib/internal-model.d.ts +29 -1
  33. package/vendor/packages/edit-store/lib/internal-model.js +145 -35
  34. package/vendor/packages/edit-store/lib/migrate/index.d.ts +31 -2
  35. package/vendor/packages/edit-store/lib/migrate/index.js +171 -6
  36. package/vendor/packages/edit-store/lib/project.d.ts +35 -0
  37. package/vendor/packages/edit-store/lib/project.js +240 -0
  38. package/vendor/packages/edit-store/lib/tree-ops.d.ts +109 -0
  39. package/vendor/packages/edit-store/lib/tree-ops.js +717 -0
  40. package/vendor/packages/edit-store/lib/webview-kernel.js +94 -14
  41. package/vendor/packages/edit-store/lib/write-gate.d.ts +6 -0
  42. package/vendor/packages/edit-store/lib/write-gate.js +59 -0
  43. package/vendor/packages/edit-store/package.json +10 -0
  44. package/vendor/packages/frame-engine/README.ja.md +4 -0
  45. package/vendor/packages/frame-engine/README.md +4 -0
  46. package/vendor/packages/gpu-export/README.ja.md +29 -15
  47. package/vendor/packages/gpu-export/README.md +31 -15
  48. package/vendor/packages/media-bin/src/preview-audio-sidecar.mjs +283 -0
  49. package/vendor/packages/media-bin/src/speech-atempo.mjs +9 -186
  50. package/vendor/packages/media-bin/test/speech-atempo.test.mjs +47 -3
  51. package/vendor/packages/osr-export/README.md +1 -1
  52. package/vendor/packages/overlay-runtime/README.md +26 -0
  53. package/vendor/packages/overlay-runtime/package.json +2 -1
  54. package/vendor/packages/schemas/bin/validate-edit.mjs +46 -0
  55. package/vendor/packages/schemas/bin/validate-motion.mjs +58 -0
  56. package/vendor/packages/schemas/captions.schema.json +2 -2
  57. package/vendor/packages/schemas/edit.schema.json +247 -11
  58. package/vendor/packages/schemas/motion.schema.json +20 -0
  59. package/vendor/packages/schemas/package.json +1 -0
  60. package/vendor/packages/schemas/test/edit-v2-schema.test.mjs +3 -3
  61. package/vendor/packages/schemas/test/fixtures/motion/g-motion.json +1 -0
  62. package/vendor/packages/schemas/test/fixtures/object-tree-a-group.json +1 -0
  63. package/vendor/packages/schemas/test/fixtures/object-tree-b-nested.json +1 -0
  64. package/vendor/packages/schemas/test/fixtures/object-tree-c-html-bag.json +1 -0
  65. package/vendor/packages/schemas/test/fixtures/object-tree-d-captions.json +1 -0
  66. package/vendor/packages/schemas/test/fixtures/object-tree-e-keyframes-ref.json +1 -0
  67. package/vendor/packages/schemas/test/fixtures/object-tree-f-motion-animator.json +1 -0
  68. package/vendor/packages/schemas/test/fixtures/object-tree-g-z-order.json +1 -0
  69. package/vendor/packages/schemas/test/fixtures/object-tree-h-content.json +1 -0
  70. package/vendor/packages/schemas/test/object-tree-schema.test.mjs +101 -0
  71. package/vendor/skills/address-review/SKILL.md +5 -1
  72. package/vendor/skills/analyze-footage/SKILL.md +36 -25
  73. package/vendor/skills/analyze-footage/analysis-json.md +6 -4
  74. package/vendor/skills/analyze-footage/events-and-hooks.md +15 -1
  75. package/vendor/skills/analyze-footage/keyframes-and-review.md +44 -85
  76. package/vendor/skills/analyze-footage/media-and-transcript.md +48 -188
  77. package/vendor/skills/analyze-footage/person-matte.md +10 -8
  78. package/vendor/skills/analyze-footage/vision-tracks.md +11 -6
  79. package/vendor/skills/analyze-footage/workflow.md +60 -61
  80. package/vendor/skills/analyze-project/SKILL.md +8 -1
  81. package/vendor/skills/analyze-project/interpretation.md +6 -2
  82. package/vendor/skills/beat-sync-edit/SKILL.md +3 -0
  83. package/vendor/skills/critique-cut/SKILL.md +92 -0
  84. package/vendor/skills/critique-cut/bin/index.js +6 -0
  85. package/vendor/skills/critique-cut/bin/used-ranges.mjs +402 -0
  86. package/vendor/skills/critique-cut/bin/used-ranges.test.mjs +281 -0
  87. package/vendor/skills/critique-cut/workflow.md +169 -0
  88. package/vendor/skills/edit-lint/SKILL.md +4 -1
  89. package/vendor/skills/edit-plan/SKILL.md +4 -0
  90. package/vendor/skills/edit-plan/beat-sync.md +3 -0
  91. package/vendor/skills/edit-plan/beats.md +3 -0
  92. package/vendor/skills/edit-plan/emphasis-detection.md +3 -0
  93. package/vendor/skills/edit-plan/execution.md +8 -2
  94. package/vendor/skills/edit-plan/workflow.md +3 -0
  95. package/vendor/skills/export-nle/SKILL.md +4 -1
  96. package/vendor/skills/generate-narration/SKILL.md +4 -1
  97. package/vendor/skills/overlay-authoring/SKILL.md +3 -0
  98. package/vendor/skills/overlay-authoring/motion.md +6 -0
  99. package/vendor/skills/overlay-authoring/telop.md +12 -0
  100. package/vendor/skills/render-cut/SKILL.md +3 -0
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "akari-video",
3
- "version": "0.1.28",
3
+ "version": "0.1.30",
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": {
@@ -14,7 +14,7 @@ export async function runCaptureCommand(argv, options = {}) {
14
14
  }
15
15
 
16
16
  const isHelp = argv.includes("--help") || argv.includes("-h");
17
- if (!isHelp) {
17
+ if (!isHelp && captureNeedsChrome(argv, options.platform ?? process.platform)) {
18
18
  const browser = await resolveBrowserDiagnostics(assets, options);
19
19
  if (!await browser.findChromePath()) {
20
20
  logError(await browser.describeChromeNotFound());
@@ -30,6 +30,15 @@ export async function runCaptureCommand(argv, options = {}) {
30
30
  return { exitCode: typeof result.status === "number" ? result.status : 1 };
31
31
  }
32
32
 
33
+ export function captureNeedsChrome(argv, platform = process.platform) {
34
+ let engine = "auto";
35
+ for (let index = 0; index < argv.length; index += 1) {
36
+ if (argv[index] === "--engine" && argv[index + 1] !== undefined) engine = argv[index + 1];
37
+ else if (argv[index].startsWith("--engine=")) engine = argv[index].slice("--engine=".length);
38
+ }
39
+ return engine === "legacy" || (engine === "auto" && !["darwin", "win32"].includes(platform));
40
+ }
41
+
33
42
  async function resolveBrowserDiagnostics(assets, options) {
34
43
  if (options.findChromePath && options.describeChromeNotFound) {
35
44
  return {
@@ -39,7 +39,15 @@ export async function runMigrateCommand(args, options = {}) {
39
39
  // captions.json の不在・読み取り失敗・壊れた JSON は cue なしとして移行を続ける。
40
40
  }
41
41
  const hasCaptions = migrate.captionsHaveRenderableCues(captionsRoot);
42
- const proposal = migrate.planMigration(projectRoot, editPath, text, { hasCaptions, now: options.now });
42
+ let editVersion;
43
+ try {
44
+ editVersion = migrate.detectEditVersion(JSON.parse(text));
45
+ } catch {
46
+ // JSON error の文言は従来どおり planMigration に一元化する。
47
+ }
48
+ const proposal = editVersion === 2
49
+ ? migrate.planV2Normalization(projectRoot, editPath, text, { now: options.now })
50
+ : migrate.planMigration(projectRoot, editPath, text, { hasCaptions, now: options.now });
43
51
  if (proposal.ok === false) {
44
52
  if (parsed.json) {
45
53
  log(JSON.stringify({ ok: false, error: 'このプロジェクトは変換できません', blockers: proposal.blockers }));
@@ -49,13 +57,18 @@ export async function runMigrateCommand(args, options = {}) {
49
57
  }
50
58
  return { exitCode: 2 };
51
59
  }
60
+ if (proposal.noop === true) {
61
+ if (parsed.json) log(JSON.stringify({ ok: true, noop: true, version: proposal.version }));
62
+ else log('変換の必要はありません。');
63
+ return { exitCode: 0, proposal };
64
+ }
52
65
  if (parsed.json) {
53
66
  log(JSON.stringify({
54
67
  ok: true, dryRun: parsed.dryRun, version: proposal.version,
55
68
  filePath: proposal.filePath, backupPath: proposal.backupPath, changes: proposal.changes
56
69
  }));
57
70
  } else {
58
- log(`変換対象: ${proposal.filePath} (version ${proposal.version} -> 2)`);
71
+ log(`変換対象: ${proposal.filePath} (version ${proposal.version}${proposal.version === 2 ? ' 正規化' : ' -> 2'})`);
59
72
  for (const change of proposal.changes) log(`- ${change.path}: ${change.note}`);
60
73
  log(`変換前の退避先: ${proposal.backupPath}`);
61
74
  }
@@ -78,7 +91,11 @@ export async function runMigrateCommand(args, options = {}) {
78
91
  }
79
92
  }
80
93
  await migrate.applyMigration(proposal);
81
- if (!parsed.json) log(`version 2 へ変換しました。元ファイル: ${proposal.backupPath}`);
94
+ if (!parsed.json) {
95
+ log(proposal.version === 2
96
+ ? `version 2 を正規化しました。元ファイル: ${proposal.backupPath}`
97
+ : `version 2 へ変換しました。元ファイル: ${proposal.backupPath}`);
98
+ }
82
99
  return { exitCode: 0, proposal };
83
100
  }
84
101
 
@@ -121,7 +138,7 @@ export function migrateHelp() {
121
138
  return [
122
139
  '使い方: akari migrate [dir] [--yes] [--dry-run] [--json]',
123
140
  '',
124
- 'v0/v1 の edit.json を v2 へ片道変換します。',
141
+ 'v0/v1 の edit.json を v2 へ片道変換し、v2 は正規形へ移行します。',
125
142
  '既定は変更内容を表示して y/n で確認し、変換前の全文を .akari/backup/ へ退避します。',
126
143
  '',
127
144
  ' --yes, -y 表示後の確認を省略',
@@ -31,6 +31,7 @@
31
31
  "docs/contract-2026-08-03-cut-candidate-bridge-v1.md",
32
32
  "docs/contract-2026-08-03-status-integrity-v1.md",
33
33
  "docs/contract-2026-08-05-fx-v0.md",
34
+ "docs/contract-2026-08-09-transform-keyframes-v0.md",
34
35
  "docs/contract-2026-08-11-analysis-vision-tracks-v0.md",
35
36
  "docs/contract-2026-08-11-review-session-ui-events.md",
36
37
  "docs/contract-2026-08-12-chat-approval-v0.md",
@@ -48,6 +49,8 @@
48
49
  "docs/contract-2026-08-28-v2-audio-roles-v0.md",
49
50
  "docs/contract-2026-08-29-capture-v0.md",
50
51
  "docs/contract-2026-08-29-media-inspect-cli-v0.md",
52
+ "docs/contract-2026-08-30-edit-json-v2-object-tree-v0.md",
53
+ "docs/contract-2026-08-30-motion-and-keyframes-v0.md",
51
54
  "packages/akari-launcher/package.json",
52
55
  "packages/akari-launcher/README.md",
53
56
  "packages/akari-tools/package.json",
@@ -69,6 +72,7 @@
69
72
  "packages/edit-lint/package.json",
70
73
  "packages/edit-lint/README.md",
71
74
  "packages/edit-store/package.json",
75
+ "packages/edit-store/README.md",
72
76
  "packages/export-nle/package.json",
73
77
  "packages/frame-engine/package.json",
74
78
  "packages/frame-engine/README.ja.md",
@@ -120,6 +124,8 @@
120
124
  "skills/compile-review-session/dev-fixtures/README.md",
121
125
  "skills/compile-review-session/SKILL.md",
122
126
  "skills/create-project/SKILL.md",
127
+ "skills/critique-cut/SKILL.md",
128
+ "skills/critique-cut/workflow.md",
123
129
  "skills/declare-audio/after-save.md",
124
130
  "skills/declare-audio/launch.md",
125
131
  "skills/declare-audio/SKILL.md",
@@ -44,6 +44,10 @@
44
44
 
45
45
  `sources[].proxy` の生成規格は [プレビュー用プロキシの規格](./contract-2026-08-02-preview-parity.md#55-プレビュー用プロキシの規格) に従う。
46
46
 
47
+ v2 プレビューでは `sources[].proxy` は任意の最適化である。宣言されていれば既定でそれを使い、
48
+ 起動を速くする。宣言が無ければ `VideoDecoder.isConfigSupported` で器の実力を調べ、原本を直接読む。
49
+ 器が原本を扱えない場合は preview-server が同じプロキシ規格で自動生成する。
50
+
47
51
  参照は path ではなく安定した `id` で行う。これにより素材の差し替えや path 変更で cut や
48
52
  サイドカーの参照が壊れない。JSON Schema は将来の任意フィールドを許容する tolerant reader とし、
49
53
  既知フィールドの型、version ごとの必須形、`source` / `sources[]` の排他を検証する。
@@ -40,7 +40,7 @@
40
40
  素の色のまま合成されて、窓の継ぎ目で肌色が食い違った。「プロジェクト全体の色」だと
41
41
  誤解しやすいため、ここに明記する。
42
42
  4. **書き出しエンジンの既定(2026-08-28 改訂)**: `--engine` 省略時は `auto`。
43
- darwin では v2(OSR)、win32 / linux では legacy に解決する。従来経路へ戻す場合は
43
+ darwin / win32 では適格なら GPU、不適格なら OSR、linux では legacy に解決する。従来経路へ戻す場合は
44
44
  `render-cut --engine legacy` を明示する。
45
45
 
46
46
  ### 2-4. reveal 系トランジション(`reveal-down` / `reveal-up`。2026-08-14 追加)
@@ -168,3 +168,11 @@ x264、その他の環境で x264 の順に解決する。ハードウェア対
168
168
  誤判定を避けるため 256x144 で試し焼きし、`AKARI_EXPORT_FORCE_X264=1` のときは試し焼きせず
169
169
  すべて不採用とする。明示指定した Windows 向け方式が利用不能なら x264 へ暗黙移行せず停止する。
170
170
  `AKARI_EXPORT_FORCE_X264=1` のときに Windows 向け方式を明示指定した場合も、同じく停止する。
171
+
172
+ ## 7. v2 書き出しの cut 中間物(2026-08-29 追記)
173
+
174
+ OSR / GPU の v2 書き出しでは、映像エンジンが `edit.sources` から直接描画するため、cut 段と
175
+ tail-pad 段は音声専用中間物だけを生成する。通常は `cut-audio.mp4`、最終尺までの音声 padding が
176
+ 必要な場合は `cut-audio-tail-padded.mp4` を使用し、どちらも `-vn` で映像を処理しない。
177
+ legacy 書き出しは従来どおり `cut.mp4` と必要時の `cut-tail-padded.mp4` を使用する。
178
+ 音声入力は cut ごとに入力側シーク(`-ss` / `-t`)し、cut 頭 0.5 s の先読みガード(AAC の overlap-add 用)を設け、cut 段の費用を素材長に依存させない。
@@ -149,3 +149,15 @@ updated: 2026-08-26
149
149
  - スキーマ変更(`edit.json` 等の既存契約ファイル形は不変)
150
150
  - 「paths 宣言」(プロジェクトごとの置き場所カスタマイズ UI)の導入
151
151
  - ルート直下 `cache/`(レガシー)の最終的な扱いの裁定(§3・§6)
152
+
153
+ ## 8. 追記(2026-08-30)— `motion/` を正本ディレクトリとして登録
154
+
155
+ `contract-2026-08-30-edit-json-v2-object-tree-v0.md` / `contract-2026-08-30-motion-and-keyframes-v0.md` により、
156
+ プロジェクト直下に **`motion/`** を追加する。
157
+
158
+ | 用途 | 置き場 | 作成者 | 性質 |
159
+ |---|---|---|---|
160
+ | キーフレーム曲線の袋(`motion/<group-id>.json`。edit.json の `keyframes: { path, count }` から参照)| `motion/` | edit-store(保存時に inline から振り分け)| **正本**(再生成不可。`.akari/cache/` ではない)|
161
+
162
+ - edit.json / captions.json と同じトランザクションで保存され、同じ lint ゲートを通る
163
+ - §2 の「ルート直下への新規ファイル作成は正本ファイルに限る」の例外ではなく、正本ファイルの追加である
@@ -10,6 +10,7 @@
10
10
  |---|---|---|
11
11
  | 2026-08-02 | v0 | Web UI と shell の挙動仕様を統合 |
12
12
  | 2026-08-28 | v2 | `packages/frame-engine` の意味論へ統合し、検収をゴールデンフレームへ一本化。出口を OSR と GPU 直結の 2 本に固定し、互換経路を退役節へ移動 |
13
+ | 2026-08-31 | v2.1 | §5.2 に断片 CSS の `vw` / `vh` 系単位の出力サイズ基準化(`viewport-units.js`。プレビューがウィンドウ幅基準で解いていた実機報告の修正)を追記 |
13
14
 
14
15
  ## 1. 役割分担
15
16
 
@@ -242,6 +243,14 @@ legacy 合成経路は、移行中の既存利用者と Windows を支えるた
242
243
  `edit.json` のあるディレクトリからの相対ファイルパスとして解決する。`vars` は `--` で始まるキーだけを
243
244
  CSS カスタムプロパティとして overlay root へ適用し、それ以外のキーは DOM や JavaScript へ注入しない。
244
245
 
246
+ 断片 CSS の `vw` / `vh` / `vmin` / `vmax`(`dvw` 等の接頭辞付き・`vi` / `vb` 含む)は**出力サイズ基準**で
247
+ 解決する(`1vw` = `output.width / 100` px)。書き出しは出力サイズちょうどの viewport で overlay sheet を
248
+ 描くので素のままで正しいが、器のプレビューはステージを `scale()` でペインへ収めるため素の `vw` は
249
+ ウィンドウ幅基準になってしまう。器は mount 時に `packages/overlay-runtime/src/viewport-units.js` で
250
+ `<style>` / `style=""` の `<数値><単位>` を `calc(<数値> * var(--akari-vw, 1vw))` へ書き換え、ステージ要素に
251
+ `--akari-vw` 等(出力サイズ / 100 px)を定義して一致させる(2026-08-31。shell / Web 共通。`@media` 等の
252
+ プレリュード・文字列・`url()` は書き換えない)。
253
+
245
254
  ### 5.3 書き込み経路
246
255
 
247
256
  UI、API、RPC の別を問わず、`edit.json` / `captions.json` へのすべての書き込みは edit-lint ゲートを通す。
@@ -267,3 +276,42 @@ frame-engine がランダムアクセスするプレビュー用プロキシは
267
276
  いずれも `packages/media-bin/src/proxy-recipe.mjs` を唯一の定義として使う。レシピ版
268
277
  `gop1s-v1` は shell のキャッシュキーと preview-server の出力名へ含め、旧規格のキャッシュを
269
278
  次回参照時に再利用しない。
279
+
280
+ ### 5.6 読み込み予算と原本 / proxy の選択規則
281
+
282
+ frame-engine のソース読み込み予算は、`Content-Length` が得られる場合
283
+ `max(10 秒, bytes / 8 MiB毎秒)` とする。予算を超えても受信進捗が続く間は打ち切らず、進捗が
284
+ 5 秒間止まった場合に失敗とする。同一 URL の fetch はセッションにつき 1 回に限り、再試行では
285
+ 取得済みバイトと解析済み moov / キーフレーム索引を再利用する。
286
+
287
+ v2 プレビューの既定選択は次の順序とする。
288
+
289
+ 1. 宣言済み proxy があれば proxy を使う(`declared`)
290
+ 2. proxy が無ければ codec をプローブし、`hw || any` で扱える場合は原本を使う
291
+ (`hardware-ok` / `decoder-ok`)
292
+ 3. 扱えない場合は preview-server に自動プロキシを要求し、生成中は非致命の通知を表示する
293
+ (`auto-proxy`)
294
+
295
+ Web UI の `?frameEngineSource=original`、または shell の
296
+ `AKARI_FRAME_ENGINE_SOURCE=original` では 1 を飛ばして器の実力判定へ進む。`=proxy` では従来どおり
297
+ proxy を無条件に優先する。
298
+ `AKARI_FRAME_ENGINE_FORCE_SW=1` はハードウェアデコード不可を模擬するテスト用スイッチである。
299
+ HEVC は `prefer-software` が通らないため、codec プローブが `sw=false` を返した系列について
300
+ ClipSessionPool はソフトウェア退避を学習しない。ソフトウェア退避の学習対象は H.264 のみとする。
301
+
302
+ tkhd に 90 / 180 / 270 度の回転を持つ素材は、既定でデコーダ出力を毎フレームの
303
+ OffscreenCanvas へ焼き直さない。frame-engine は回転メタをフレームへ付帯し、compositor の
304
+ UV 逆写像で表示回転を 1 回だけ適用する。crop、framing、keyframe、既存 transform / perspective は
305
+ 回転後の論理空間を基準とし、90 / 270 度では coded width / height を入れ替えた論理寸法を使う。
306
+ この規則は VideoFrame の直接 upload と copyTo の両経路に共通である。
307
+
308
+ デコーダエラーは window 全域イベントで飛ぶため、他クリップの失敗と区別できない。frame-engine は
309
+ 検出後 `decoderErrorGraceMs`(既定 1 秒)だけ自分の操作の成功を待ち、期限内に成功した場合はその
310
+ エラーを無視する。prime がフレームを返さず、かつエラーを観測した場合だけ、その試行を失敗とする。
311
+
312
+ ### 5.7 映像ソースの読み方とパリティ
313
+
314
+ frame-engine が MP4 を全体ストリームとして読むか、`ftyp` / `moov` の索引と必要な圧縮サンプルの
315
+ Range として読むかは、完成画の意味論に影響しない。どちらの読み込み経路も同じ presentation 時刻の
316
+ VideoFrame を §4.1 の評価点へ供給し、`elst.media_time`、B フレームの並べ替え、メディア終端を含めて
317
+ golden の `diff 0` を満たさなければならない。ソース取得方法の変更をパリティ差の許容理由にしてはならない。
@@ -0,0 +1,35 @@
1
+ ---
2
+ lifecycle: implemented
3
+ created: 2026-08-09
4
+ updated: 2026-08-30
5
+ ---
6
+
7
+ # 変形キーフレーム契約 v0(`layers[].keyframes` / v2 `items[].keyframes`)— 2026-08-30 復元
8
+
9
+ > **復元の注記(2026-08-30)**: 本契約は `packages/schemas/edit.schema.json`(`layerKeyframe` / `layerItem` / `keyframeV2` の `$comment`)・
10
+ > `packages/schemas/bin/validate-edit.mjs`・`packages/render-cut/src/layer-keyframes.mjs` ほか 10 箇所から参照されていたが、
11
+ > 実ファイルがどのブランチの履歴にも存在しなかった。スキーマの `$comment` に残っていた意味論からそのまま再構成した。
12
+ > **後継 = `contract-2026-08-30-motion-and-keyframes-v0.md` §2**(opacity の追加・easing 語彙の拡張・`motion/` 袋への参照形)。
13
+ > 本ファイルは v0 の意味論の記録であり、これ以上追記しない。
14
+
15
+ - 日付: 2026-08-09(実装済み・`contract-2026-07-22-render-basics.md` §4-4 に要約あり)
16
+ - 状態: implemented(v1 `layers[].keyframes`・v2 `items[].keyframes` の両方で有効)
17
+
18
+ ## 1. 意味論
19
+
20
+ - `keyframes[]` はレイヤー / アイテムの `transform` / `crop` / `perspective` を時間で動かす共通機構
21
+ - `t` は**ローカル時間**: v1 `layers[].keyframes[].t` はレイヤー内秒(`layerItem.t` を 0 とする。`cuts[].framing.keyframes[].t` と同じ規約)、
22
+ v2 `items[].keyframes[].t` は**アイテム内の整数フレーム**(`item.at` を 0 とする)
23
+ - `transform` / `crop` / `perspective` はそれぞれ**独立の任意プロパティ**(プロパティごとの別トラックにはしない — 1 点で複数プロパティを同時に動かせる)
24
+ - ある区間の両端点が同じプロパティを持てばその間は**線形補間**。片方の端点にしか無ければ直近の宣言値を**保持(hold)**
25
+ - どの点にも一度も宣言されないプロパティは、レイヤー / アイテム直下の**静的値**(省略時は各 `$def` の既定値)を全区間で保持する
26
+ - `easing` は点ごとに設定し、**その点へ入る区間**(1 つ前の点からこの点まで)の補間カーブを決める。先頭点の `easing` は無視。省略時 `linear`。語彙は `linear` / `ease-in-out`
27
+ - 2 点以上・`t` 昇順・重複禁止(`validate-edit.mjs` で検証)。`keyframes` が無い、または使える点が 2 点未満のときは既存の静的値のみが効く(回帰なし・バイト等価)
28
+ - render-cut は cuts 合成後のベース映像へ `t` 順に合成する。プレビューは同じ補間を CSS / WebGL で再現する(`contract-2026-08-02-preview-parity.md`)
29
+
30
+ ## 2. 参照元(復元時点)
31
+
32
+ `packages/schemas/edit.schema.json` / `packages/schemas/bin/validate-edit.mjs` / `packages/render-cut/src/layer-keyframes.mjs` / `packages/render-cut/src/layers.mjs` /
33
+ `packages/render-cut/test/layer-keyframes.test.mjs` / `packages/preview-server/test/layer-keyframes-visual.test.mjs` /
34
+ `apps/shell/extensions/akari-preview/src/common/edit-summary-fields.ts` / `apps/shell/extensions/akari-preview/src/common/layer-keyframes-visual.ts` /
35
+ `apps/shell/extensions/akari-preview/src/browser/akari-preview-open-handler.ts` とそのテスト
@@ -23,6 +23,12 @@ updated: 2026-08-12
23
23
  - `contract-2026-07-17-data-contract-versioning.md`(version 整数・追加のみ・寛容リーダーの三原則)
24
24
  - スコープ: `edit.json` の `cuts[]` がメイン時間軸で静止画ソース(png/jpg/jpeg/webp/bmp/gif)を
25
25
  直接読めるようにする。**新しいスキーマフィールドは作らない**(判定は拡張子のみ)
26
+ - 2026-08-31 追記(issue #30): frame-engine 経路(`--engine gpu` / `osr`、および v2 プレビュー)でも同じ
27
+ 意味論で描く。runtime が拡張子で `CachedStillImageSource` として登録した素材を、`plan.ts` が
28
+ `kind: 'image'` の base 層(`sourceTimeUs` は常に 0・尺は `out - in`・transform / crop / keyframes は
29
+ 動画 cut と同じ・トランジションの outgoing / incoming にもなれる)として評価し、compositor は
30
+ layers と同じ texture cache を base の RGBA 経路へ結ぶ。それまでは `layerFromPlacement` が
31
+ `decode` 持ちしか受けず `no video frame source registered` で落ちていた(legacy との受理差)
26
32
 
27
33
  ## 0. 背景
28
34
 
@@ -27,6 +27,22 @@ CSS animation/transition/keyframes、filter/mask/clip-path 等を検出する。
27
27
  `<script type="application/json" data-akari-3d-scene>` 宣言を属性順にかかわらずちょうど 1 個持ち、
28
28
  それ以外の script と video を持たない宣言型 3D は `three` とする。3D の描画先である canvas は許可する。
29
29
 
30
+ `data-akari-slot` への文言注入(`source.params`。正本 `contract-2026-08-22-overlay-html-slots.md`)は、
31
+ 静的 HTML のスプライト化の直前と DOM 層の mount 時に、legacy の rasterize / プレビューの overlay-runtime と
32
+ 同じ `packages/overlay-runtime/src/slot-params.js` の `renderTextSlots` で適用する。params を持つ overlay が
33
+ 1 件でもあればページに同 runtime を inline し、receipt の manifest に `textSlotOverlayCount` を残す。
34
+ params があるのに runtime が無い状態は既定文言を黙って焼かず fail-closed にする(2026-08-31・issue #32)。
35
+
36
+ frame-engine のメイン時間軸(cuts)は静止画ソース(edit-store の `isStillImageSourcePath`)を
37
+ `kind: 'image'` の base 層として描く(尺は `out - in`・ソース時刻なし・transform / crop / keyframes は動画 cut と
38
+ 同じ。正本 `contract-2026-08-12-still-image-cut-source-v0.md`。2026-08-31・issue #30)。2 本目以降の visual
39
+ トラックの映像クリップは `at` / `track` を保持して絶対配置し、番号が大きいトラックが前面(v2 の `tracks[]` 配列順)。
40
+ track 0 は従来どおり連結チェーン(freeze で伸び、トランジション重なりは宣言から再計算)のまま
41
+ (2026-08-31・issue #31。それまでは GPU / OSR の runtime が導出 `at` / `track` を全 cut から外していたため、
42
+ 上段のクリップが直列に連結されて出力尺の外へ押し出され、PASS のまま絵が消えていた)。同じ規則を
43
+ frame-engine へ cuts を渡す残り 2 面(preview-server の Web UI プレビュー・シェルのプレビュー)にも適用し、
44
+ プレビューと書き出しが同じ絵を出す(preview parity。シェルは `renderTrack` の最小値を最下段とみなす)。
45
+
30
46
  字幕は cue ごとに rasterize し、出現・loop・消失を解析的な opacity と中心基準 affine 変換で
31
47
  再現する。v2 では `words[]` を持つ karaoke、pop、reveal、reveal-word と `emphasis_words` も
32
48
  語矩形タイルとして GPU-native に合成する。次は引き続き `unsupported` とする。
@@ -89,8 +105,8 @@ SVG の入力は data URL に固定する。Blob URL と同一オリジン HTTP
89
105
  2 走一致、製品経路の読み戻し 0 を要求する。性能 gate は cue ラスタ p50 500 ms 以下、karaoke 44 cue の
90
106
  akari-video-pv 18 ms/コマ以下、小 fixture(360 コマ・3 cue)の RSS peak 900 MB 以下とする。
91
107
 
92
- `--engine auto` は macOS で全件適格なら `gpu`、不適格なら `osr` を選ぶ。他 OS の `auto` は従来の
93
- 選択を維持する。明示 `--engine gpu` と不適格の組み合わせは理由を全件表示して fail-closed とし、
108
+ `--engine auto` は macOS / Windows で全件適格なら `gpu`、不適格なら `osr` を選ぶ。Linux の `auto`
109
+ `legacy` を維持する。明示 `--engine gpu` と不適格の組み合わせは理由を全件表示して fail-closed とし、
94
110
  黙って OSR へ変更しない。GPU launcher が利用できない明示指定も fail-closed とする。
95
111
 
96
112
  ## 3. 合成順と LUT
@@ -195,8 +211,8 @@ OSR receipt と同じ warning/hard-stop 語彙を使う。`--engine osr` と `--
195
211
 
196
212
  - 語矩形で表せない演出、色補間と幾何変形が同居する cue、縦書きの語単位字幕は glyph atlas 等の次段が必要。
197
213
  - 動的自由 HTML は OSR または事前ベイクが必要。
198
- - Windows / Linux は launcher tier 1 / 2 があれば `--engine gpu` 明示で利用できる。`auto` GPU 候補は
199
- macOS のみとし、Windows の既定切替は実機実測後の別契約で扱う。
214
+ - Windows は launcher tier 1 / 2 があれば `auto` で適格時に GPU、不適格時に OSR を使う。Linux は
215
+ `--engine gpu` 明示時だけ GPU を利用でき、`auto` は legacy を維持する。
200
216
  - 長尺の区間並列、複数 process 並列は非対応。
201
217
  - ~~インストール済みデスクトップアプリ経由(launcher tier 1)の GPU 書き出しは未配線~~ **2026-08-29 解消**: shell の `electron-entry.js` が `--akari-main packages/gpu-export/src/electron-main.mjs` を受け、`buildElectronArguments` が tier 1 にそれを渡す(osr 契約 §6)。`resolveGpuLauncher` の fail-closed(allowDesktop 既定 false)は v0.1.28 で解除。以下は v0.1.26〜v0.1.27 の記録: (v0.1.25 で判明)。shell の `--render` は OSR ランタイムしか読まず、`buildElectronArguments` は tier 2 にしか mainScript を渡さない。v0.1.26 から `resolveGpuLauncher` は tier 1 を候補から外す(fail-closed): `auto` は OSR へ(provenance に `engine_fallback` と理由)、`--engine gpu` 明示は拒否。tier 1 の配線(shell contribution に GPU ランタイム選択を足す)は別票。
202
218
 
@@ -205,7 +221,7 @@ OSR receipt と同じ warning/hard-stop 語彙を使う。`--engine osr` と `--
205
221
  | platform | `--engine gpu` | `--engine auto` | launcher |
206
222
  |---|---|---|---|
207
223
  | macOS | 明示利用可 | 適格なら GPU、不適格なら OSR | tier 1 / 2(ただし tier 1 は現状未配線で fail-closed) |
208
- | Windows | 明示利用可 | legacy のまま | tier 1 / 2(同上) |
224
+ | Windows | 明示利用可 | 適格なら GPU、不適格なら OSR | tier 1 / 2(同上) |
209
225
  | Linux | 明示利用可 | legacy のまま | tier 1 / 2(同上) |
210
226
 
211
227
  npm Electron の tier 2 は `node_modules/electron/path.txt` を必須とする。値は win32 が
@@ -233,9 +249,17 @@ GPU 出口だけに `--enable-features=CanvasDrawElement`、`--disable-gpu-vsync
233
249
  - `perspective`、`preserve-3d`、`rotateX/Y/3d`、`matrix3d`、`translateZ/3d` のいずれかを含む
234
250
  CSS 3D transform。先行実験では `translateZ` 単独・`perspective` 単独は正しく転写できたため、
235
251
  将来はこの粒度まで緩和できる余地があるが、v1 では緩和しない。
252
+ **例外(2026-08-31・issue #34)**: Z 成分がリテラル 0 の `translateZ(0)` / `translate3d(x, y, 0)` は
253
+ 2D の `translate` と描画結果が同一(実測: 静的スプライトで全コマ YMAX=0)なので検出しない。
254
+ Z が 0 以外、引数の個数が違う、Z が `var()` / `calc()` 等でリテラルとして読めない場合は従来どおり `degraded`。
255
+ 引数の切り出しは括弧の入れ子を数えるので、`translate3d(var(--x), calc(1px + 2px), 0)` のように X / Y が
256
+ CSS 変数・calc 駆動でも Z のリテラル 0 を読める(オーバーレイ規約は調整値を CSS 変数に出すため自然に現れる形)。
236
257
  - `requestAnimationFrame`、`setTimeout`、`setInterval`、`Date.now`、`performance.now` で自走する時計。
237
258
  - `video`、`audio`、canvas/宣言型 3D 以外の runtime、JSON 以外の script。
238
- - 絶対 URL と外部 font/image/background resource
259
+ - 絶対 URL と外部 font/image/background resource。`background(-image)` の `url(` 走査は宣言の区切り
260
+ (`;` `}`)に加えて引用符とタグ境界(`"` `'` `<` `>`)で止め、`url(#id)` の同一文書内フラグメント参照は
261
+ 外部扱いしない(2026-08-31・issue #33。それまでは末尾に `;` の無いインライン style から後続 SVG の
262
+ `fill="url(#id)"` まで走査が届いて誤検出していた)。
239
263
  - `drawElementImage` が利用できない実行環境、または device pixel ratio が 1 でない環境。
240
264
 
241
265
  settle は mount 時に一度だけ決める。`canvas.requestPaint` がある Chromium では rAF 2 回の後に
@@ -272,11 +296,36 @@ OSR より遅く、同じ題材から字幕を外した実測は GPU 19.2 ms/コ
272
296
  +23.4 ms/コマから +1.65 ms/コマへ縮小した。毎コマの字幕描画費用という限界は解消し、実素材
273
297
  `akari-video-pv`(5,999 コマ)では GPU / OSR 7.2〜8.2 倍へ到達した。
274
298
 
275
- 残る字幕差は毎コマの合成費用ではなく、cue 採寸と SVG ラスタの起動費用である。実素材 PV の字幕ありは
299
+ (2026-08-29 #120f 時点の記述)残る字幕差は毎コマの合成費用ではなく、cue 採寸と SVG ラスタの起動費用である。実素材 PV の字幕ありは
276
300
  150.7 秒(25.1 ms/コマ)、字幕なし対照は 88.0 秒(14.7 ms/コマ)で 1.71 倍だった。30 cue の
277
301
  `akari-project/dynamic` では字幕ラスタが約 47 秒から 9.95 秒へ短縮したが、30 秒級の短い題材では
278
302
  起動費用の比率が大きく、GPU / OSR は 1.07〜1.14 倍にとどまる。
279
303
 
304
+ 2026-08-30 の #120h では、実素材 `akari-video-pv`(5,999 コマ・44 cue・88 band / 6 batch)の
305
+ receipt `gpu.captionStartup` をラッパーが 5 走実測した。#120f 時点で約 63 秒だった字幕の起動費用
306
+ (SVG ラスタ 20.5〜21.3 秒 + cue 採寸 88 variant)は、`captionStartup.totalMs` 2.75〜5.01 秒と
307
+ `captionRasterTotalMs` 5.90〜7.34 秒、合計 **8.7〜12.3 秒**になった。代表する 1 走の内訳は、
308
+ フォントの base64 符号化 0.66 秒(符号化後 13.6 MB・走に 1 回だけ)、cue 採寸 1.54 秒
309
+ (88 stable call / 176 pass / 264 variant・うちフォント待ち 0.75 秒・レイアウト 0.073 秒)、
310
+ SVG ラスタ 5.90 秒(SVG 組み立て 0.046 秒、data URL 割り当て 0.52 秒、decode 2.80 秒、
311
+ 中間 sheet への描画 1.72 秒、band の blit 0.11 秒、テクスチャ登録 0.004 秒)である。
312
+
313
+ frame loop 中の `stages.captionRasterBatch` は **0 回**で、6 バッチすべてが書き出し開始前に焼き終わる。
314
+ frame loop の `stages.captions` は p50 0 ms / p95 0.1 ms である。採寸の使い回しは cue の内容
315
+ (出力寸法・CSS 変数・cue の HTML・unit index・CSS 変種列)が完全に一致したときだけ効く。PV の
316
+ 44 cue は本文がすべて異なるため `reusedStableCalls` は 0 で、88 stable call は 88 distinct key の
317
+ ままである。同じ本文を共有する 3 cue の fixture では、6 stable call のうち 4 が使い回され、
318
+ distinct key は 2 へ落ちる。採寸が 32 回で収束しない unit は、その unit だけ sprite へ降格し、
319
+ receipt の `gpu.captions[].mode = "sprite"` と warning に出して書き出しは完走する(fail-closed にしない)。
320
+
321
+ 速度の絶対値は 2026-08-30 の測定では取れていない。1 分 load < 20 の静かな窓を 40 分 × 3 回待っても
322
+ 来ず、観測した最小 load は 52 だった。PV は load 77〜421 の下で 250.6〜687.4 秒、同じ高負荷下の
323
+ 字幕なし対照は 220.4〜785.4 秒で、字幕ありとの差は走ごとの 3 倍以上の load 変動に埋もれ、対照より
324
+ 速い走もあった。したがって、静かな窓での「PV ≤ 110 秒」「dynamic ≥ 2×(OSR 比)」は未検証である。
325
+ 参考値として、高負荷下の `akari-project/dynamic` は GPU 71.1〜80.7 秒 / OSR 93.6〜97.8 秒
326
+ (1.2〜1.3 倍)で、#120f 時点の 1.07〜1.14 倍からは改善している。RSS の上限は 531〜914 MB
327
+ (1 GB 以内)、`--trap-readback` の読み戻しは 0 だった。
328
+
280
329
  ## 10. v3 — 宣言型 3D の登場曲線
281
330
 
282
331
  v3 は、宣言型 Three.js scene のルート要素にある 1 回きりの登場 CSS animation を時刻の関数へ解析し、
@@ -332,3 +381,100 @@ value = from + (to - from) * eased
332
381
  `getComputedStyle` と delay 前・登場中 3 点・終了後の 5 時刻で突き合わせた実測差は、translate 最大
333
382
  0.00043 px、scale 最大 0.000001、opacity 0 だった。検収閾値は translate 0.5 px 以下、opacity 0.005
334
383
  以下、3D 登場区間の GPU / OSR 外接矩形内 MAD 1.0 以下とする。
384
+
385
+ ## 11. v2 の cut 音声中間物(2026-08-29 追記)
386
+
387
+ GPU 経路の映像は `edit.sources` をページ側で直接読み、`cut.mp4` の映像を使用しない。そのため
388
+ cut 段は `cut-audio.mp4`、尺延長が必要な場合は続けて `cut-audio-tail-padded.mp4` を生成し、
389
+ 音声ストリームだけを最終 mux へ渡す。両コマンドは `-vn` とし、映像のデコード・フィルタ・
390
+ エンコードを行わない。音声の trim、速度、freeze 無音、transition、gap、AAC 48 kHz の意味論は
391
+ 従来の映像込み cut / tail-pad と同じである。legacy 経路は従来どおり映像込み中間物を使用する。
392
+ 音声入力は cut ごとに入力側シーク(`-ss` / `-t`)し、cut 頭 0.5 s の先読みガード(AAC の overlap-add 用)を設け、cut 段の費用を素材長に依存させない。
393
+
394
+ ## 12. 採寸不安定の根治・実行時フォールバック・生フレーム dump(2026-08-30 追記)
395
+
396
+ ### 12.1 事実(2026-08-30・capture-v2-engine レーンの実測と司令塔のコード確認)
397
+
398
+ - 内部 fieldtest 案件(11 秒・1080p・字幕 **3 cue・`words[]` 無し・style 無指定**・HTML オーバーレイ 2・LUT)で、
399
+ §2.1 の採寸が **32 回で収束せず `caption-measure-unstable` で fail-closed** する。`6da9a353` 以前の main でも同じ地点で落ちる既存不良。
400
+ いちばん単純な字幕でも起こるため、原因は karaoke / pop の語矩形ではなく採寸の土台にある可能性が高い(未特定)
401
+ - `render-cut.mjs` は `exportWithGpu` の**実行時失敗を捕捉しない**。`engine_fallback` が発火するのは launcher tier 3(Electron 不在)だけで、
402
+ page runtime が fail-closed すると書き出し全体が失敗する。`--engine auto` は事前の適格判定で gpu を選ぶが、採寸の収束は実行時にしか
403
+ 分からないため、適格判定では弾けない
404
+ - `akari capture --engine auto` は書き出しと同じ関数でエンジンを解決する(capture 契約 §9.2)ので、同じ案件で同じ地点で落ちる。
405
+ `--engine osr` 明示なら通る
406
+ - gpu 書き出しには、エンコーダへ渡す直前のフレームを取り出す機構が無い(読み戻しゼロの設計 §4)。osr には `--dump-frames`
407
+ (`raw/frame-N.bgra`)があり、capture 契約 §9.3 の可逆比較は osr でだけ成立している
408
+
409
+ ### 12.2 要求 A — 採寸不安定の原因特定と決定論化
410
+
411
+ - 収束しなかった試行について、**どの cue のどの値がどれだけ揺れたか**(variant / token / rect の差分)を run.json と receipt に残す
412
+ (現状は attempts の count / p50 / max だけ)。原因は推測で決めず、この差分と再現案件で特定する
413
+ - 特定した原因に対して決定論化の手を入れる。**許容差・平均・丸めで揺らぎを隠さない**(§2.1 の原則は不変)。
414
+ 候補は実測で選ぶ: レイアウト確定の待ち方(`getBoundingClientRect` 1 回で足りているか・rAF / フォント metrics の遅延)、
415
+ root の挿入位置・サイズの固定、DPR / zoom の固定、など
416
+ - 根治できた範囲と、できなかった条件(あれば)を契約に追記できる形で報告する
417
+
418
+ ### 12.3 要求 B — 実行時フォールバック(必須)
419
+
420
+ - `--engine auto` で gpu の page runtime が **閉じた集合の理由コード**(v0 は `caption-measure-unstable` のみ)で fail-closed したとき、
421
+ render-cut は **osr で再実行**し、`provenance.engine_fallback = { from: "gpu", reason: "<reasonCode>" }` と gpu 側の失敗 run.json への
422
+ 参照を receipt に残す。中間物は捨てて osr を最初から回す(部分再利用しない)
423
+ - 明示 `--engine gpu` は従来どおり fail-closed(理由を表示して exit ≠ 0)。フォールバックは `auto` だけ
424
+ - `akari capture --engine auto` も同じ関数・同じ理由コード集合でフォールバックし、`capture.json.engine.fallback` に記録する
425
+ - フォールバック対象外の失敗(デコード不可・Electron クラッシュ等)は従来どおり失敗のまま。集合を広げるときは本契約に追記する
426
+
427
+ ### 12.4 要求 C — gpu 書き出しの生フレーム dump(検証専用)
428
+
429
+ - `--dump-frames <n,...>` を gpu 書き出しに追加。エンコーダへ渡す直前の `finalCanvas` を読み戻して `raw/frame-N.rgba`(8bit・行順は
430
+ osr の dump と同じ規約で明記)に書く。osr の `--dump-frames` と引数・出力先の形を揃える
431
+ - **フラグ無しの製品経路は読み戻しゼロを維持**(`assert-zero-readback` の静的監査と `--trap-readback` の両方が引き続き通ること)。
432
+ dump は `--verify-frames` と同じ検証専用の枠に置く
433
+ - capture 契約 §9.3 の可逆比較を gpu でも成立させる: `capture --engine gpu -t N` の PNG ≡ `--dump-frames N` の raw(MAD 0)
434
+
435
+ ### 12.5 受け入れ
436
+
437
+ - fieldtest 案件(複製)で `render-cut --engine auto` が**完走**する。根治できていれば gpu で、できていなければ osr へのフォールバックで。
438
+ receipt にどちらだったかと理由が残る。`capture --engine auto -t 0 3 6` も同様に完走
439
+ - 収束しなかった試行の差分ログが run.json / receipt に出る(フィクスチャで再現できなければ、揺らぎを注入したユニットテストで形を固定)
440
+ - `--dump-frames` で取った raw と `capture --engine gpu` の PNG が bit 一致(MAD 0・3 フレーム)
441
+ - フォールバックが発火しない既存フィクスチャの mp4 SHA は不変。`assert-zero-readback` PASS・`--trap-readback` 完走
442
+ - 明示 `--engine gpu` の fail-closed 挙動と exit code は不変(テストで固定)
443
+
444
+ ### 12.6 実装レーンの実測による追記(2026-08-30・検収後)
445
+
446
+ - **原因(確定)**: `captions.mjs` の `.akari-caption__plate` に付く entrance fade(`akari-caption-fade 180ms ease-out`・
447
+ `translateY(0.18em → 0)` = 既定 38 px で 6.84 px)を、ラスタ側は `settleCss` で停止していたが**採寸 root には当てていなかった**
448
+ (`CAPTION_WORD_FREEZE_CSS` が止めるのは語単位の 6 セレクタのみ)。採寸は生きている transform を任意時点でサンプルしていたため、
449
+ `ease-out` 終端の 0〜0.5 px の残差が毎回違い、許容差ゼロの連続 2 回一致が 32 回でも成立しなかった。#120c r0 の「語矩形 y が
450
+ 最大 1.71 px 揺れる」も同じ現象。差分ログの実測: 31 対すべて不一致・揺れたフィールドは `plate.y/bottom` `line[0].y/bottom` の 4 つだけ・
451
+ 試行 1→2 で −6.33〜−6.43 px(= 0.18em)
452
+ - **原則(§2.1 に追加)**: **採寸 root に適用する CSS は、ラスタ band に適用する CSS と同一集合でなければならない**
453
+ (採寸 = ラスタされる幾何、を構成上保証する)。実装は `measureCss = CAPTION_WORD_FREEZE_CSS + settleCss` で採寸・probe・両 variant を揃え、
454
+ 静的テスト「caption measurement roots are frozen in the same settled state the raster uses」で固定。許容差・丸め・平均は入れていない
455
+ - **根治の実測**: fieldtest 案件で `captionMeasureAttempts = {count 3, p50 2, max 2}`(理論下限)・diffs 0・`captionLayoutMaxDeltaPx` 0・
456
+ gpu 2 走 mp4 SHA 一致。§12.1 の「32 回で収束せず」は正確には**高確率で**(base では確率的に収束することもある)
457
+ - **32 回の上限**は据え置く。根治後は 2 回で確定するため上限は実質保険。下げるなら揺らぎが残る条件を別途観測してから
458
+ - **`--dump-frames` の形**: 行順は上から下で osr と同規約。チャネル順はエンジンが本来読み戻す形式のまま(gpu = RGBA `raw/frame-N.rgba` /
459
+ osr = BGRA `raw/frame-N.bgra`)で拡張子が表す。`--trap-readback` とは相互排他
460
+ - **receipt / provenance のキー**: フォールバック時は `provenance.engine = "osr"`・`engine_fallback = { from: "gpu", reason }`・
461
+ **`provenance.gpu_failure_run`**(gpu 失敗 run.json のプロジェクト相対パス)。capture は `capture.json.engine.fallback` に同じ内容
462
+ - **capture の parity ガード**: render-cut がフォールバックした receipt(`provenance.engine = "osr"`)に対し、capture 側の解決が `"gpu"` でも
463
+ `engine_fallback.from` が一致すれば parity として受ける(これが無いと capture がフォールバックに到達する前に落ちる。launcher tier 3 由来の
464
+ 既存フォールバックにも同じ穴があり同時に塞いだ)
465
+ - 判定は構造化された `error.reasonCode` だけを見る(メッセージ文字列一致では発火しない)
466
+
467
+ ### 12.7 §9(#120h の「降格して完走」)との関係 — 司令塔裁定(2026-08-30・r3 合流時)
468
+
469
+ - **事実**: #120h(§9 追記)は採寸が収束しない unit を **sprite へ降格して書き出しを完走**させる(語アニメが落ちる・receipt の
470
+ `captionStartup.measure.degradedUnits` に計上)。§12.3 は「`auto` は osr へフォールバック / 明示 `gpu` は fail-closed」を要求する。
471
+ r3 の合流はこれを **「実測由来の不安定 = 降格(§9)/ 故障注入 `AKARI_GPU_CAPTION_MEASURE_FAULT` = `caption-measure-unstable` を伝播
472
+ → `auto` は osr フォールバック・明示 `gpu` は fail-closed(§12.3)」** に分けて両方を残した。注入は「復旧不能な採寸失敗の代役」であり、
473
+ 降格経路を撃つスイッチではなくなった
474
+ - **裁定(v0)**: この分離を**採る**。根治(§12.6)により実測由来の不安定は理論下限 2 回で収束しており、降格経路は保険。
475
+ 降格が起きたときは warning と `degradedUnits` で**黙らずに**記録される(§2.1「揺らぎを隠さない」に反しない)
476
+ - **次版の候補(別票 D・小)**: `auto` で `degradedUnits > 0` になった走は「近似で完走」より「osr で正確に完走」を選ぶべきかを裁定し、
477
+ 採るなら降格を `FALLBACK_REASONS` 相当(例 `caption-measure-degraded`)として `auto` だけ osr へ回す。明示 `gpu` は降格 + warning のまま。
478
+ 降格経路を実機で撃つための注入モード(例: 値の接尾辞で降格を選ぶ)も同票で
479
+ - 採寸 settle の実装は #120h の **`.akari-measure-root` にスコープした `measureSettleCss`** に一本化(§12.6 の原則を満たし、ページ全体の
480
+ アニメは止めない)。`contentKey` で再利用される安定結果は `cssVariants` を鍵に含むため必ず settled 状態で測ったもの(実機確認済み)
@@ -10,7 +10,7 @@
10
10
  | platform | `auto` の解決 | 備考 |
11
11
  |---|---|---|
12
12
  | darwin | `osr` | v2 を既定とする |
13
- | win32 | `legacy` | Windows 実機実測 #14 が完了するまで OSR opt-in |
13
+ | win32 | 適格なら `gpu`、不適格なら `osr` | GPU / OSR launcher が利用不能なら順に `legacy` へフォールバック |
14
14
  | linux | `legacy` | OSR は opt-in |
15
15
 
16
16
  `.akari/render.json` の provenance は、指定値を `engine_requested`、解決後の実走値を `engine` に
@@ -166,6 +166,20 @@ v0.1.27 からの挙動: `resolveOsrLauncher`(製品入口)はインスト
166
166
 
167
167
  **2026-08-29 追記(根治)**: 書き出し専用の入口 `apps/shell/electron-entry.js` が合流した(§6 / §11.4)。`resolveOsrLauncher` の既定を戻し、インストール済みアプリを再び tier 1 の候補にする(v0.1.28〜)。`allowInstalledDesktop: false` は明示の opt-out として残す。
168
168
 
169
+ ### 11.6 親の `ELECTRON_RUN_AS_NODE` が Electron 子プロセスへ継承される(2026-08-29 追記・#27)
170
+
171
+ v0.1.28 実機(macOS Apple Silicon / Windows RTX 5060)で実証: shell 配布の `akari` shim
172
+ (`ELECTRON_RUN_AS_NODE=1 exec <同梱 Electron> akari.mjs`)・アプリ内書き出し・パートナー CLI サーバーは、同梱 Electron を
173
+ node として使うためにこの変数を立てる。`launchElectronExport` が親の環境をそのまま子へ渡していたため、tier 1 の AKARI Video は
174
+ Chromium スイッチを `bad option` で拒否して exit 9、tier 2 の npm Electron は `electron-main.mjs` を素の Node で実行して
175
+ `app` が undefined になり、いずれも PROGRESS 0 行で終わる。GPU 出口(§12)も同じ launcher を共有するため同時に落ちる。
176
+ §11.4 / §11.5 の解消後に露出した、起動環境の問題。
177
+
178
+ 修正後: `spawnAndWait` は `electronChildEnvironment(env)` を通した環境で起動する
179
+ (`ELECTRON_CHILD_ENV_BLOCKLIST = ["ELECTRON_RUN_AS_NODE"]`、名前は Windows に合わせ大文字小文字非区別で比較)。
180
+ 他の変数(`AKARI_OSR_*` / `AKARI_FFMPEG_BIN` / `PATH` 等)は従来どおり継承する。shim 側で変数を外す案は採らない
181
+ (shim の外で立てられた変数には効かず、書き出し側で一律に守るのが唯一の境界)。
182
+
169
183
  ## 12. GPU 直結出口との共有境界(2026-08-28 追記)
170
184
 
171
185
  [GPU 直結書き出し v0](./contract-2026-08-28-gpu-export-v0.md) は、本契約の launcher 3 段、static
@@ -174,3 +188,12 @@ server、page builder の入力解決、memory guard、ffprobe、音声 mux、re
174
188
  mux、fail-closed 条件は GPU 契約を正本とし、OSR の seek/paint/stamp 経路へ逆流させない。
175
189
  §1 の darwin `auto → osr` は GPU 出口追加前の記述であり、GPU 契約の適格性を満たさない場合、
176
190
  または GPU launcher が利用できない場合の選択として読む。適格時の `auto → gpu` は GPU 契約を優先する。
191
+
192
+ ## 13. v2 の cut 音声中間物(2026-08-29 追記)
193
+
194
+ OSR 経路の映像は `edit.sources` をページ側で直接読み、`cut.mp4` の映像を使用しない。そのため
195
+ cut 段は `cut-audio.mp4`、尺延長が必要な場合は続けて `cut-audio-tail-padded.mp4` を生成し、
196
+ 音声ストリームだけを最終 mux へ渡す。両コマンドは `-vn` とし、映像のデコード・フィルタ・
197
+ エンコードを行わない。音声の trim、速度、freeze 無音、transition、gap、AAC 48 kHz の意味論は
198
+ 従来の映像込み cut / tail-pad と同じである。legacy 経路は従来どおり映像込み中間物を使用する。
199
+ 音声入力は cut ごとに入力側シーク(`-ss` / `-t`)し、cut 頭 0.5 s の先読みガード(AAC の overlap-add 用)を設け、cut 段の費用を素材長に依存させない。
@@ -140,3 +140,23 @@ ms へ換算し、絶対値で集計する。全 59 点の最大は 16.667 ms、
140
140
  `atempo` チェーンから決まり、同じ入力は再生成しない。生成またはデコードに失敗した項目だけは
141
141
  警告を 1 行出して従来の `playbackRate` 経路へ退避し、映像および他の音声のプレビューを止めない。
142
142
  したがって §5 の「残す近似」に速度変更した台詞のピッチ差は含めない。
143
+
144
+ ## 8. プレビュー音声サイドカーと先読み(§7 の更新)
145
+
146
+ §7 の WAV は廃止し、撮影素材の台詞は速度にかかわらず、使用区間だけを 48 kHz・元チャンネル数の
147
+ FLAC(compression level 5)へ一度だけ切り出す。`transition_out` 境界では前後のハンドルも同じ
148
+ サイドカーへ含め、プレビュー予定表は ffmpeg `acrossfade` の既定
149
+ `c1=tri:c2=tri` と同じ線形ゲインで重ねる。したがって「台詞の供給元」と
150
+ 「トランジション区間の音」は §2 / §5 の近似対象に含めない。
151
+
152
+ 生成先は案件内の `.akari/cache/preview-audio/` とする。ファイル名は
153
+ `sha1(sourcePath|size|mtime|in|out|speed|padBefore|padAfter|recipe)` で、recipe は
154
+ `preview-audio-flac-v1`。同じキーは再生成せず、予定表を作るたびに現在使うキー集合を正として、
155
+ 集合に無い FLAC と旧 `.akari/cache/speech-atempo/*.wav` を掃除する。生成失敗時だけ元ファイルへ
156
+ 退避し、台詞の元ファイルが 64 MB 以上ならレンダラへ全体を載せず、その項目を警告 1 行で省く。
157
+
158
+ BGM / SFX / narration は、元ファイルが WAV かつ 8 MB 超のとき同じ FLAC サイドカーを使う。
159
+ frame-engine は映像の ready を先に成立させ、その直後から予定表上の初回使用時刻順・同時 2 本で
160
+ 全音声を非同期にデコードする。デコード済み PCM は合計バイトで管理し、既定上限は 256 MB。
161
+ 上限超過時は次に使う時刻が最も遠い項目から退避する。再生開始は先読み済みの項目を再利用し、
162
+ 未着の項目だけを待つため、先読み処理は初期フレームの描画を止めない。