akari-video 0.1.9 → 0.1.10

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 (103) hide show
  1. package/package.json +1 -1
  2. package/src/capability-sources.mjs +5 -2
  3. package/src/status-command.mjs +8 -1
  4. package/vendor/.akari-capability-sources.json +4 -20
  5. package/vendor/docs/contract-2026-08-02-preview-parity.md +1 -0
  6. package/vendor/docs/contract-2026-08-11-analysis-vision-tracks-v0.md +170 -13
  7. package/vendor/docs/contract-2026-08-12-region-filter-layer-v0.md +1 -1
  8. package/vendor/docs/contract-2026-08-12-still-image-cut-source-v0.md +163 -0
  9. package/vendor/docs/contract-2026-08-13-avatar-drive-v0.md +258 -0
  10. package/vendor/docs/contract-2026-08-14-avatar-vrm-v0.md +336 -0
  11. package/vendor/packages/akari-launcher/package.json +9 -5
  12. package/vendor/packages/analysis-report/package.json +8 -4
  13. package/vendor/packages/asset-resolver/src/entitlements.mjs +23 -7
  14. package/vendor/packages/asset-resolver/src/resolve.mjs +1 -1
  15. package/vendor/packages/asset-resolver/src/state.mjs +13 -5
  16. package/vendor/packages/asset-resolver/test/catalog-and-state.test.mjs +16 -1
  17. package/vendor/packages/asset-resolver/test/entitlements.test.mjs +37 -9
  18. package/vendor/packages/asset-resolver/test/resolve-paid-zip.test.mjs +4 -1
  19. package/vendor/packages/audio-library-setup/bin/fetch-akari-sounds.mjs +96 -13
  20. package/vendor/packages/audio-library-setup/test/fetch-akari-sounds.test.mjs +269 -0
  21. package/vendor/packages/bake-layer/package.json +8 -4
  22. package/vendor/packages/chat-bridge/package.json +8 -4
  23. package/vendor/packages/decision-cards/package.json +8 -4
  24. package/vendor/packages/edit-lint/src/derive-tracks.mjs +42 -0
  25. package/vendor/packages/edit-lint/src/edit-lint.mjs +4202 -0
  26. package/vendor/packages/edit-lint/src/report.mjs +100 -0
  27. package/vendor/packages/edit-store/lib/caption-display.d.ts +107 -0
  28. package/vendor/packages/edit-store/lib/caption-display.js +812 -0
  29. package/vendor/packages/edit-store/lib/caption-store.d.ts +127 -0
  30. package/vendor/packages/edit-store/lib/caption-store.js +1000 -0
  31. package/vendor/packages/edit-store/lib/caption-window.d.ts +23 -0
  32. package/vendor/packages/edit-store/lib/caption-window.js +29 -0
  33. package/vendor/packages/edit-store/lib/edit-store.d.ts +219 -0
  34. package/vendor/packages/edit-store/lib/edit-store.js +1753 -0
  35. package/vendor/packages/edit-store/lib/index.d.ts +10 -0
  36. package/vendor/packages/edit-store/lib/index.js +26 -0
  37. package/vendor/packages/edit-store/lib/timeline-map.d.ts +59 -0
  38. package/vendor/packages/edit-store/lib/timeline-map.js +160 -0
  39. package/vendor/packages/edit-store/lib/webview-kernel.d.ts +15 -0
  40. package/vendor/packages/edit-store/lib/webview-kernel.js +194 -0
  41. package/vendor/packages/edit-store/lib/write-gate.d.ts +43 -0
  42. package/vendor/packages/edit-store/lib/write-gate.js +196 -0
  43. package/vendor/packages/export-nle/package.json +8 -4
  44. package/vendor/packages/intake-form/package.json +8 -4
  45. package/vendor/packages/media-bin/src/index.mjs +27 -8
  46. package/vendor/packages/media-bin/test/media-bin.test.mjs +71 -3
  47. package/vendor/packages/preview-server/package.json +8 -3
  48. package/vendor/packages/render-cut/package.json +9 -5
  49. package/vendor/packages/schemas/analysis.schema.json +7 -1
  50. package/vendor/packages/schemas/captions.schema.json +2 -1
  51. package/vendor/packages/schemas/edit.schema.json +4 -2
  52. package/vendor/packages/schemas/examples/edit-cuts-still-image-source-valid/edit.json +14 -0
  53. package/vendor/packages/schemas/test/analysis-body-pose-3d-pointer.test.mjs +14 -0
  54. package/vendor/packages/schemas/test/analysis-face-expression-pointer.test.mjs +14 -0
  55. package/vendor/packages/schemas/test/validate-edit.test.mjs +8 -0
  56. package/vendor/packages/template-render/README.ja.md +6 -0
  57. package/vendor/packages/template-render/README.md +6 -0
  58. package/vendor/packages/template-render/package.json +10 -3
  59. package/vendor/presets/textanim/INDEX.md +32 -0
  60. package/vendor/presets/textanim/index.jsonl +47 -0
  61. package/vendor/skills/analyze-footage/bin/face-expression/artifacts.mjs +83 -0
  62. package/vendor/skills/analyze-footage/bin/face-expression/browser.mjs +63 -0
  63. package/vendor/skills/analyze-footage/bin/face-expression/browser.test.mjs +10 -0
  64. package/vendor/skills/analyze-footage/bin/face-expression/euler.mjs +52 -0
  65. package/vendor/skills/analyze-footage/bin/face-expression/euler.test.mjs +46 -0
  66. package/vendor/skills/analyze-footage/bin/face-expression/face-expression-schema.test.mjs +32 -0
  67. package/vendor/skills/analyze-footage/bin/face-expression/face-expression.mjs +348 -0
  68. package/vendor/skills/analyze-footage/bin/face-expression/model-resolver.mjs +78 -0
  69. package/vendor/skills/analyze-footage/bin/face-expression/model-resolver.test.mjs +80 -0
  70. package/vendor/skills/analyze-footage/bin/face-expression/render-page.mjs +74 -0
  71. package/vendor/skills/analyze-footage/bin/face-expression/render.html +7 -0
  72. package/vendor/skills/analyze-footage/bin/face-expression/runtime-artifacts.test.mjs +17 -0
  73. package/vendor/skills/analyze-footage/bin/face-expression/static-server.mjs +89 -0
  74. package/vendor/skills/analyze-footage/bin/face-expression/static-server.test.mjs +19 -0
  75. package/vendor/skills/analyze-footage/bin/face-expression/vendor/tasks-vision-0.10.17/LICENSE.txt +202 -0
  76. package/vendor/skills/analyze-footage/bin/face-expression/vendor/tasks-vision-0.10.17/README-AKARI.md +12 -0
  77. package/vendor/skills/analyze-footage/bin/face-expression/vendor/tasks-vision-0.10.17/vision_bundle.mjs +2 -0
  78. package/vendor/skills/analyze-footage/bin/face-expression/vendor/tasks-vision-0.10.17/wasm/vision_wasm_internal.js +20 -0
  79. package/vendor/skills/analyze-footage/bin/face-expression/vendor/tasks-vision-0.10.17/wasm/vision_wasm_internal.wasm +0 -0
  80. package/vendor/skills/analyze-footage/bin/face-expression/vendor/tasks-vision-0.10.17/wasm/vision_wasm_nosimd_internal.js +20 -0
  81. package/vendor/skills/analyze-footage/bin/face-expression/vendor/tasks-vision-0.10.17/wasm/vision_wasm_nosimd_internal.wasm +0 -0
  82. package/vendor/skills/analyze-footage/bin/vision-tracks/body-pose-3d-schema.test.mjs +47 -0
  83. package/vendor/skills/analyze-footage/bin/vision-tracks/vision-tracks-helper.swift +74 -6
  84. package/vendor/skills/analyze-footage/bin/vision-tracks/vision-tracks.mjs +30 -6
  85. package/vendor/skills/analyze-footage/references/vision-tracks.schema.json +136 -4
  86. package/vendor/skills/analyze-footage/test/vision-tracks-check.test.mjs +45 -3
  87. package/vendor/skills/analyze-footage/vision-tracks.md +131 -20
  88. package/vendor/skills/edit-plan/SKILL.md +5 -3
  89. package/vendor/skills/edit-plan/execution.md +17 -10
  90. package/vendor/skills/edit-plan/workflow.md +9 -5
  91. package/vendor/skills/overlay-authoring/3d.md +78 -1
  92. package/vendor/skills/overlay-authoring/motion.md +49 -0
  93. package/vendor/packages/akari-launcher/bin/akari.mjs +0 -77
  94. package/vendor/packages/analysis-report/render-analysis-report.mjs +0 -544
  95. package/vendor/packages/bake-layer/bin/bake-layer.mjs +0 -159
  96. package/vendor/packages/chat-bridge/src/telegram.mjs +0 -272
  97. package/vendor/packages/decision-cards/report-helper.mjs +0 -611
  98. package/vendor/packages/export-nle/bin/export-nle.mjs +0 -162
  99. package/vendor/packages/intake-form/intake-form-helper.mjs +0 -37
  100. package/vendor/packages/preview-server/src/server.mjs +0 -682
  101. package/vendor/packages/render-cut/bin/akari-apply-textstyle.mjs +0 -202
  102. package/vendor/packages/render-cut/bin/render-cut.mjs +0 -5
  103. package/vendor/packages/template-render/bin/akari-template-render.mjs +0 -257
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "akari-video",
3
- "version": "0.1.9",
3
+ "version": "0.1.10",
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": {
@@ -2,7 +2,10 @@ import { execFileSync } from "node:child_process";
2
2
  import { existsSync, lstatSync, readFileSync, realpathSync } from "node:fs";
3
3
  import { dirname, isAbsolute, join, relative, resolve } from "node:path";
4
4
 
5
- export function discoverCheckoutCapabilitySources(repoRoot, { trackedFiles = null } = {}) {
5
+ export function discoverCheckoutCapabilitySources(repoRoot, {
6
+ trackedFiles = null,
7
+ includeBinTargets = true,
8
+ } = {}) {
6
9
  const root = realpathSync(resolve(repoRoot));
7
10
  const tracked = trackedFiles ?? execFileSync("git", ["-C", root, "ls-files", "-z"], {
8
11
  maxBuffer: 64 * 1024 * 1024,
@@ -25,7 +28,7 @@ export function discoverCheckoutCapabilitySources(repoRoot, { trackedFiles = nul
25
28
  throw new Error(`${manifestPath} package.json#bin target is not tracked: ${canonical}`);
26
29
  }
27
30
  assertRegularContainedFile(root, canonical, `${manifestPath} package.json#bin`);
28
- selected.add(canonical);
31
+ if (includeBinTargets) selected.add(canonical);
29
32
  }
30
33
  }
31
34
  const paths = [...selected].sort(compare);
@@ -30,10 +30,17 @@ export async function runStatusCommand(argv, options = {}) {
30
30
  const status = parsed.full
31
31
  ? await resolveFullProjectStatus(parsed.projectRoot)
32
32
  : resolveProjectStatus(parsed.projectRoot, { mode: "fast" });
33
- log(parsed.json ? serializeStatus(status) : `${formatStatusSummary(status)}\n`);
33
+ log(parsed.json ? serializeStatus(status) : `${formatStatusOutput(status)}\n`);
34
34
  return { exitCode: status.state_health === "inconclusive" ? 1 : 0, status };
35
35
  }
36
36
 
37
+ export function formatStatusOutput(status) {
38
+ const summary = formatStatusSummary(status);
39
+ if (status.state_health !== "inconclusive" || status.problems.length === 0) return summary;
40
+ const problems = status.problems.map((problem) => ` - ${problem}`).join("\n");
41
+ return `${summary}\nCould not determine:\n${problems}\nNext: run \`akari status --json\` for the full machine-readable report, or resolve the items above and re-run \`akari status\`.`;
42
+ }
43
+
37
44
  export function parseStatusArguments(argv, cwd = process.cwd()) {
38
45
  let projectRoot = cwd;
39
46
  let pathSeen = false;
@@ -36,36 +36,27 @@
36
36
  "docs/contract-2026-08-12-chat-approval-v0.md",
37
37
  "docs/contract-2026-08-12-color-range-normalization-v0.md",
38
38
  "docs/contract-2026-08-12-region-filter-layer-v0.md",
39
- "packages/akari-launcher/bin/akari.mjs",
39
+ "docs/contract-2026-08-12-still-image-cut-source-v0.md",
40
+ "docs/contract-2026-08-13-avatar-drive-v0.md",
41
+ "docs/contract-2026-08-14-avatar-vrm-v0.md",
40
42
  "packages/akari-launcher/package.json",
41
43
  "packages/akari-launcher/README.md",
42
44
  "packages/akari-tools/package.json",
43
45
  "packages/analysis-report/package.json",
44
46
  "packages/analysis-report/README.md",
45
- "packages/analysis-report/render-analysis-report.mjs",
46
- "packages/asset-resolver/bin/akari-assets.mjs",
47
47
  "packages/asset-resolver/package.json",
48
48
  "packages/asset-resolver/README.md",
49
- "packages/audio-library-setup/bin/gallery-helper.mjs",
50
- "packages/audio-library-setup/bin/generate-candidates-html.mjs",
51
- "packages/audio-library-setup/bin/register-drop-folder.mjs",
52
49
  "packages/audio-library-setup/package.json",
53
50
  "packages/audio-library-setup/README.md",
54
- "packages/bake-layer/bin/bake-layer.mjs",
55
51
  "packages/bake-layer/package.json",
56
52
  "packages/bake-layer/README.md",
57
53
  "packages/chat-bridge/package.json",
58
- "packages/chat-bridge/src/telegram.mjs",
59
54
  "packages/creator-root/package.json",
60
55
  "packages/decision-cards/package.json",
61
56
  "packages/decision-cards/README.md",
62
- "packages/decision-cards/report-helper.mjs",
63
- "packages/edit-lint/bin/edit-lint.mjs",
64
57
  "packages/edit-lint/package.json",
65
58
  "packages/edit-store/package.json",
66
- "packages/export-nle/bin/export-nle.mjs",
67
59
  "packages/export-nle/package.json",
68
- "packages/intake-form/intake-form-helper.mjs",
69
60
  "packages/intake-form/package.json",
70
61
  "packages/intake-form/README.md",
71
62
  "packages/media-bin/package.json",
@@ -75,22 +66,15 @@
75
66
  "packages/preview-engine/package.json",
76
67
  "packages/preview-engine/README.md",
77
68
  "packages/preview-server/package.json",
78
- "packages/preview-server/src/server.mjs",
79
69
  "packages/project-scaffold/package.json",
80
- "packages/render-cut/bin/akari-apply-textstyle.mjs",
81
- "packages/render-cut/bin/render-cut.mjs",
82
70
  "packages/render-cut/package.json",
83
- "packages/schemas/bin/validate-asset.mjs",
84
- "packages/schemas/bin/validate-intake.mjs",
85
- "packages/schemas/bin/validate-plan.mjs",
86
- "packages/schemas/bin/validate-review.mjs",
87
71
  "packages/schemas/package.json",
88
- "packages/template-render/bin/akari-template-render.mjs",
89
72
  "packages/template-render/package.json",
90
73
  "packages/template-render/README.ja.md",
91
74
  "packages/template-render/README.md",
92
75
  "skills/address-review/SKILL.md",
93
76
  "skills/analyze-footage/analysis-json.md",
77
+ "skills/analyze-footage/bin/face-expression/vendor/tasks-vision-0.10.17/README-AKARI.md",
94
78
  "skills/analyze-footage/events-and-hooks.md",
95
79
  "skills/analyze-footage/keyframes-and-review.md",
96
80
  "skills/analyze-footage/media-and-transcript.md",
@@ -539,6 +539,7 @@ ffmpeg の `perspective` フィルタの制約(式に時刻変数を持たな
539
539
  | `layers[].perspective`(2026-08-06 実装) | ✅(§2.4.4。実ブラウザ実測済み) | ✅(§2.4.4。tsc -b + ユニット + Web 同一計算式で担保) |
540
540
  | `cuts[].fx`(2026-08-07 実装・近似あり) | 🟡(§2.4.5。5 種対応、3 種は近似バッジ付き) | ❌(未実装) |
541
541
  | `layers[].keyframes`(2026-08-09 実装) | ✅(§2.4.7。transform/crop は連続補間。perspective は blend:"normal" のみ・書き出しの段階保持とサンプル点で一致) | ✅(§2.4.7。同左) |
542
+ | `cuts[].static-image-source`(2026-08-12 実装。正本: `contract-2026-08-12-still-image-cut-source-v0.md`) | 🟡(`<img>`/`<video>` 出し分け + preview-engine ClipSession/Timeline の image 対応を実装。framing/freeze/transform は流用。実ブラウザでの対話的スクラブ・複数区間切替の実機検証は未実施) | ❌(スコープ外・未対応) |
542
543
 
543
544
  - `cuts[].framing`(静的クロップ / ズームキーフレーム)・`cuts[].freeze`(フリーズ)は
544
545
  `contract-2026-07-22-render-basics.md` #6/#7 としてレンダ(render-cut)に加え、
@@ -1,10 +1,10 @@
1
1
  ---
2
2
  lifecycle: draft
3
3
  created: 2026-08-11
4
- updated: 2026-08-11
4
+ updated: 2026-08-14
5
5
  ---
6
6
 
7
- # 分析トラック契約 v0 — Vision ランドマーク・トラック(face-landmarks / hand-pose)と keyframes 消費
7
+ # 分析トラック契約 v0 — Vision ランドマーク・トラック(face-landmarks / hand-pose / body-pose-3d / face-expression)と keyframes 消費
8
8
 
9
9
  - 日付: 2026-08-11
10
10
  - 状態: **ドラフト**(v0 実装と同時に確定させる。実装で判明した齟齬は追記で解消)
@@ -13,8 +13,9 @@ updated: 2026-08-11
13
13
  - `contract-2026-07-23-analysis-person-matte.md`(Swift サイドカーの流儀・analysis.json の tracks 契約・検証責務の分担)
14
14
  - `contract-2026-07-25-project-structure-v0.md`(分析サイドカーの置き場 = `.akari/sidecars/`)
15
15
  - `contract-2026-08-02-preview-parity.md`(render/Web/shell 3 面パリティの原則)
16
- - スコープ: 動画から抽出する**ランドマーク・トラック**(顔・手)のデータ契約、生成サイドカーの
17
- 入出力、消費(`layers[].keyframes` への変換)の責務分担。**新しいレンダー機構は作らない**
16
+ - スコープ: 動画から抽出する**ランドマーク・トラック**(顔・手・3D ボディポーズ)のデータ契約、生成サイドカーの
17
+ 入出力、消費(`layers[].keyframes` への変換)の責務分担、および MediaPipe 由来の頭部姿勢・
18
+ 表情トラック。**新しいレンダー機構は作らない**
18
19
 
19
20
  ## 0. 設計原則
20
21
 
@@ -34,7 +35,7 @@ updated: 2026-08-11
34
35
 
35
36
  ## 1. analysis.json への追加(additive)
36
37
 
37
- `tracks` に **optional キー** 2 つを追加する。`tracks.required`(speakers / faces /
38
+ `tracks` に **optional キー** 3 つを追加する。`tracks.required`(speakers / faces /
38
39
  person_matte)には**入れない**(person_matte が必須なのは既存消費者の事情であり、新トラックは
39
40
  真に任意として追加する)。
40
41
 
@@ -48,18 +49,40 @@ person_matte)には**入れない**(person_matte が必須なのは既存消
48
49
  "tool": "vision-tracks.mjs v0",
49
50
  "generated_at": "2026-08-11T12:00:00Z"
50
51
  },
51
- "hand_pose": { /* 同形 */ }
52
+ "hand_pose": { /* 同形 */ },
53
+ "body_pose_3d": { // 任意。macOS 14+ の Vision 3D ボディポーズ
54
+ "path": "vision/body-pose-3d.json",
55
+ "sample_fps": 24,
56
+ "provider": "apple-vision",
57
+ "tool": "vision-tracks.mjs v0",
58
+ "generated_at": "2026-08-13T12:00:00Z"
59
+ }
60
+ }
61
+ ```
62
+
63
+ 2026-08-14 additive: 同じ optional pointer 形で `face_expression` を追加する。既存 3 キーと同様に
64
+ `tracks.required` には入れず、未生成はキー無しで表す。
65
+
66
+ ```jsonc
67
+ "face_expression": {
68
+ "path": "vision/face-expression.json",
69
+ "sample_fps": 24,
70
+ "provider": "mediapipe-face-landmarker",
71
+ "tool": "face-expression.mjs v0",
72
+ "generated_at": "2026-08-14T12:00:00Z",
73
+ "features": ["head-pose-ypr-radians", "mediapipe-blendshapes-52"]
52
74
  }
53
75
  ```
54
76
 
55
77
  ## 2. トラックファイル形式(vision-tracks v0)
56
78
 
57
- `path` が指す JSON ファイル。1 ファイル 1 種類(face-landmarks と hand-pose は別ファイル)。
79
+ `path` が指す JSON ファイル。1 ファイル 1 種類(face-landmarks / hand-pose /
80
+ body-pose-3d / face-expression は別ファイル)。
58
81
 
59
82
  ```jsonc
60
83
  {
61
84
  "version": 0,
62
- "kind": "face-landmarks", // "face-landmarks" | "hand-pose"
85
+ "kind": "face-landmarks", // "face-landmarks" | "hand-pose" | "body-pose-3d"
63
86
  "source": { "path": "../..(元動画への相対)", "duration": 12.5 },
64
87
  "sample_fps": 24,
65
88
  "provider": { "name": "apple-vision", "os": "macOS 15.5" },
@@ -70,8 +93,11 @@ person_matte)には**入れない**(person_matte が必須なのは既存消
70
93
  }
71
94
  ```
72
95
 
73
- - **座標系(最重要規約)**: すべて **0〜1 正規化・左上原点**(動画ピクセル系と同じ向き)。
96
+ - **画像座標系(最重要規約)**: face-landmarks / hand-pose と body-pose-3d の
97
+ `projection` はすべて **0〜1 正規化・左上原点**(動画ピクセル系と同じ向き)。
74
98
  Vision framework は左下原点で返すため、**y 反転はサイドカーの責務**。消費側は変換しない
99
+ - body-pose-3d の `position` だけは画像座標ではない。Vision が返すモデル座標
100
+ (root/hip 相対メートル)を変換せず保存する(§2.4)
75
101
  - `samples[].t` は元動画の秒(`in`/`out` と同じ時間軸)。サンプリングは `sample_fps` の等間隔
76
102
  - 生値主義: 平滑化・補間済みの値を入れない。信頼度(`conf`)を必ず併記する
77
103
 
@@ -126,6 +152,86 @@ person_matte)には**入れない**(person_matte が必須なのは既存消
126
152
  (§2.1 の必須 6 キーが揃わない detection を出力に混ぜない)。実測(1 名がほぼ映り続ける
127
153
  26.3 秒素材)ではこの除外は 0 件だった。
128
154
 
155
+ ### 2.4 body-pose-3d の detection(additive・2026-08-13)
156
+
157
+ `VNDetectHumanBodyPose3DRequest` revision 1 が返す 17 関節を保存する。API は macOS 14+
158
+ 限定である。トラックは分析の生値であり、平滑化・間引き・低信頼度除外を行わない。
159
+
160
+ ```jsonc
161
+ {
162
+ "conf": 0.86,
163
+ "joints": {
164
+ "root": {
165
+ "position": [0.0, 0.0, 0.0], // root/hip 相対メートル
166
+ "projection": [0.51, 0.63], // 0〜1 正規化・左上原点
167
+ "conf": 0.86
168
+ },
169
+ "right_hip": { /* 同形 */ },
170
+ "right_knee": { /* 同形 */ },
171
+ "right_ankle": { /* 同形 */ },
172
+ "left_hip": { /* 同形 */ },
173
+ "left_knee": { /* 同形 */ },
174
+ "left_ankle": { /* 同形 */ },
175
+ "spine": { /* 同形 */ },
176
+ "center_shoulder": { /* 同形 */ },
177
+ "center_head": { /* 同形 */ },
178
+ "top_head": { /* 同形 */ },
179
+ "left_shoulder": { /* 同形 */ },
180
+ "left_elbow": { /* 同形 */ },
181
+ "left_wrist": { /* 同形 */ },
182
+ "right_shoulder": { /* 同形 */ },
183
+ "right_elbow": { /* 同形 */ },
184
+ "right_wrist": { /* 同形 */ }
185
+ }
186
+ }
187
+ ```
188
+
189
+ - `position`: `VNHumanBodyRecognizedPoint3D.position` の平行移動成分 `[x,y,z]`。
190
+ Vision のモデル座標で root/hip 相対、単位はメートル。カメラ相対座標へ変換しない
191
+ - `projection`: `VNHumanBodyPose3DObservation.pointInImage` が返す画像投影を y 反転し、
192
+ `[0,1]` へクランプした `[x,y]`
193
+ - `conf`: Vision 3D API は関節別 confidence を公開しないため、apple-vision provider の v0 は
194
+ `VNHumanBodyPose3DObservation.confidence` を各関節へ複製する。これは関節別推定値ではなく、
195
+ 観測全体 confidence の由来明示である。消費者の `min-confidence` はこの値を使う
196
+ - 17 関節のいずれかを Vision から取得できない観測は detection ごと省略する。存在しない
197
+ 関節を補間・捏造せず、`detections: []` のフレームは時刻 `t` とともに残す
198
+ - pose-skeleton の v0 は各フレーム先頭の 1 人(`bodyIndex=0` 固定)のみを消費し、複数人には非対応
199
+
200
+ ### 2.5 face-expression の detection(additive・2026-08-14)
201
+
202
+ MediaPipe Face Landmarker が返す `facialTransformationMatrixes` と 52 blendshape category を、
203
+ 平滑化・補間せず保存する。1 ファイル 1 kind の `kind` は `face-expression`、analysis pointer は
204
+ `tracks.face_expression`、既定ファイル名は `vision/face-expression.json` とする。顔が検出されない
205
+ frame も `{ "t": ..., "detections": [] }` として残す。
206
+
207
+ ```jsonc
208
+ {
209
+ "head": {
210
+ "yaw": 0.12,
211
+ "pitch": -0.04,
212
+ "roll": 0.02
213
+ },
214
+ "blendshapes": {
215
+ "_neutral": 0.07,
216
+ "eyeBlinkLeft": 0.01,
217
+ "mouthSmileLeft": 0.64
218
+ // MediaPipe 固定 category 全 52 キー。各値は 0..1 の生 score
219
+ },
220
+ "conf": 0.64
221
+ }
222
+ ```
223
+
224
+ - `head` は 4x4 row-major 同次変換の上左 3x3 を行正規化し、
225
+ `R = Rz(roll) * Ry(yaw) * Rx(pitch)` で分解した**ラジアン**。右手系の解釈は +X=画像右、
226
+ +Y=画像下、+Z=canonical face 前方で、yaw 正=画面右向き、pitch 正=上向き、
227
+ roll 正=時計回り。gimbal lock は `roll=0` に固定する
228
+ - `blendshapes` は `_neutral` を含む MediaPipe の固定 52 category。キーは表現の生 score で、
229
+ 並びだけ byte 安定のため名前順へ正規化する。値の平滑化・クランプ・感情ラベル化はしない
230
+ - MediaPipe Web API は内部の face-presence score を結果へ公開しない。`conf` は捏造した定数ではなく、
231
+ 同じ detection に返った 52 生 score の最大値を signal confidence として決定論的に記録する。
232
+ face detection confidence そのものではないため、消費者は検出有無と blendshape 個別値を主に使う
233
+ - v0 は `numFaces=1`。複数人追跡・人物同一性の連結はしない
234
+
129
235
  ## 3. サイドカー(生成側)
130
236
 
131
237
  person-matte と同じ分離: **Swift ヘルパーはフレーム変換だけ、コンテナ・時刻・組み立ては
@@ -135,25 +241,73 @@ person-matte と同じ分離: **Swift ヘルパーはフレーム変換だけ、
135
241
  - `vision-tracks-helper.swift` — stdin から raw BGRA フレーム列、stdout へ **JSON Lines
136
242
  (1 フレーム 1 行の検出結果)**。`swiftc -O` オンデマンドビルド・バイナリは `.gitignore`
137
243
  - `vision-tracks.mjs` — `ffmpeg`(デコード・fps/幅統一)→ helper → トラックファイル組み立て →
138
- `analysis.json` の tracks へ追記(原子的置換)。`--kinds face,hand` / `--fps` / `--check`
139
- (macOS / swiftc / ffmpeg の可用性確認)
244
+ `analysis.json` の tracks へ追記(原子的置換)。`--kinds face,hand,body-pose-3d` /
245
+ `--fps` / `--check`(macOS 14+ / swiftc / ffmpeg の可用性確認。macOS 14 未満は
246
+ capability 不足として理由付きで拒否)
140
247
  - 手順書: `skills/analyze-footage/vision-tracks.md`(person-matte.md と同格の任意工程)。
141
248
  `SKILL.md` の実行順・ハードルールに配線する
142
249
  - 起動主体はエージェント(スキル手順に従い bash で直接叩く)。CLI サブコマンドにはしない
143
250
  (判断を伴う工程はスキル、の境界裁定に従う)
144
251
 
252
+ ### 3.1 face-expression の headless Chromium 生成器(additive・2026-08-14)
253
+
254
+ `skills/analyze-footage/bin/face-expression/face-expression.mjs` は face-expression 専用の独立生成器。
255
+ 既存 Swift helper は変更せず、ffmpeg で 24 fps(`--fps` 変更可)・幅 1280 以下の PNG 列へ
256
+ デコードし、Chrome for Testing + `puppeteer-core` のページ内で CPU/WASM 版 Face Landmarker を
257
+ 順に実行する。Chrome の探索順・起動引数・ページ処理後の結果吸い上げは avatar-vrm の既存
258
+ headless 経路を踏襲する。
259
+
260
+ JS 版を選んだ理由は、`@mediapipe/tasks-vision` がブラウザ/WASM 専用である一方、製品には既に
261
+ headless Chromium 経路があり、Python wheel や Swift helper という新しい実行系を増やさずに済むため。
262
+ 固定 Chrome・固定 WASM・CPU delegate・固定時刻入力により、同一環境では同じ行列分解と score 列を
263
+ 得られる。Python 版は arm64 wheel という別インストール面とバージョン解決を増やすため v0 では採らない。
264
+
265
+ モデルはリポジトリへ置かず、初回だけ次を取得する。`AKARI_HOME` があればそれを優先し、既定は
266
+ `~/.akari/models/mediapipe/face-landmarker/float16-1/face_landmarker.task`。既存ファイルも毎回
267
+ SHA-256 検査し、不一致時は再取得で隠さず即エラーにする。新規取得は `.tmp-<pid>` へ書き、検証後に
268
+ rename する。
269
+
270
+ - URL: `https://storage.googleapis.com/mediapipe-models/face_landmarker/face_landmarker/float16/1/face_landmarker.task`
271
+ - SHA-256: `64184e229b263107bc2b804c6625db1341ff2bb731874b0bcc2fe6544e0bc9ff`
272
+
273
+ ブラウザ runtime はネットワーク変動を避けるため `@mediapipe/tasks-vision@0.10.17` の必要ファイルを
274
+ 無変換で vendor した。ライセンスは Apache-2.0。
275
+
276
+ | artifact | SHA-256 |
277
+ |---|---|
278
+ | npm tarball | `d3dd0759295f1adcf5455f22aa652c58b8c1d537c0d14c8db7df78646011d523` |
279
+ | `vision_bundle.mjs` | `1ada13431ea2a8ed7ea449e6c3595122d43fea2a8a4788056ed7da271469b402` |
280
+ | `vision_wasm_internal.js` | `33a4125f825b343d2d9773951a73692f40bee368c9b591af8ff652fd501af90b` |
281
+ | `vision_wasm_internal.wasm` | `c88cf472dd5cab0a3954b071e5f442102ded3701dcccc987a7a02ee8f54aae85` |
282
+ | `vision_wasm_nosimd_internal.js` | `4e8d07dcf8cbb55b343cd76b7fc30d4303220f049d5529d6412f6f93296726a8` |
283
+ | `vision_wasm_nosimd_internal.wasm` | `f840f69d7229f89dedaed39c7ac7a52f0964a7cec02d6cb1ac9eff891db86dc2` |
284
+ | `LICENSE.txt` | `b070d77bfb2c52a1dd6996de0ce5f64c49a0ca55c889b163a963ddf5cb001ee2` |
285
+
286
+ > 注: `LICENSE.txt` のみ npm tarball には含まれず(tarball 展開で実測確認済み)、Apache-2.0 の定型全文を手動で添えたもの(出所は `vendor/.../README-AKARI.md` 参照)。他の行は全て tarball 由来の実ファイル。
287
+
288
+ tarball は `tar -xzf` 後、上記 bundle/WASM だけをコピーし、esbuild 等の再ビルドは行っていない。
289
+ tarball 自体の npm integrity は
290
+ `sha512-CZWV/q6TTe8ta61cZXjfnnHsfWIdFhms03M9T7Cnd5y2mdpylJM0rF1qRq+wsQVRMLz1OYPVEBU9ph2Bx8cxrg==`。
291
+
145
292
  ## 4. 消費(変換器)
146
293
 
147
- トラック → `edit.json` への反映は**決定論の変換器**が行う。v0 の消費者は 2 つ(別契約で
294
+ トラック → `edit.json` への反映は**決定論の変換器**が行う。v0 の消費者は 3 つ(別契約で
148
295
  実装しても本契約の §2 形式だけを入力にする):
149
296
 
150
297
  | 消費者 | 入力 | 出力 |
151
298
  |---|---|---|
152
299
  | eye-bar(目線黒帯) | face_landmarks(両瞳) | 黒帯レイヤー + `layers[].keyframes` の transform(x/y/rotate/scale) |
153
300
  | finger-frame(指フレーム) | hand_pose(両手の thumb_tip / index_tip = 4 点) | 対象レイヤーの `layers[].keyframes` の perspective(4 隅 corner-pin)+ 発動区間 |
301
+ | pose-skeleton | body_pose_3d(17 関節の 2D `projection`) | アルファ付きスティックフィギュアを事前ベイクした `kind: "baked"` layer |
302
+
303
+ `face_expression` のアバター駆動への結線は次の消費側契約に委ねる。本契約では生成 SSOT までとし、
304
+ 既存の「あいうえお」口パクや avatar-vrm / avatar-drive を変更しない。
154
305
 
155
306
  - 平滑化(移動平均・One Euro 等)・キーフレーム間引き・欠測補間は**変換器の責務**。
156
307
  パラメータは変換器の引数で決定論に
308
+ - pose-skeleton は欠測と cut 境界で平滑化状態をリセットし、低 confidence 関節を含む骨を
309
+ 非表示にする。欠測区間を hold せず、別 baked clip / layer へ分割する
310
+ - pose-skeleton の v0 は `bodyIndex=0` 固定で各フレーム先頭の 1 人だけを対象とし、複数人には非対応
157
311
  - perspective keyframes の ffmpeg 側は既存の時間窓分割フォールバック
158
312
  (`expandLayerForPerspectiveKeyframes`)に乗る。新しいレンダー経路は作らない
159
313
  - `cuts[].fx` は**使わない**(全画面ポスト効果の器。空間追跡系はレイヤー機構が正)
@@ -163,10 +317,13 @@ person-matte と同じ分離: **Swift ヘルパーはフレーム変換だけ、
163
317
  - `packages/schemas/bin/` に新しいバリデータ CLI は**作らない**(person-matte 契約 §7 の分担を
164
318
  継続)。トラックファイルの JSON Schema は `skills/analyze-footage/references/
165
319
  vision-tracks.schema.json` に置き、スキル手順の jsonschema 検証が担う
166
- - `analysis.schema.json` への tracks 2 キー追加は additive のみ
320
+ - `analysis.schema.json` への tracks 4 キー追加は additive のみ
167
321
  - **`packages/analysis-report/render-analysis-report.mjs` の軽量チェックを同時に更新する**
168
322
  (person_matte が残した「消費者の追随債務」を新トラックで繰り返さない)
169
323
  - verify は L0(該当 package / skill の `node --test`)。GUI を触らないため L1/L2 は対象外
324
+ - face-expression は schema wiring、52 キー固定、同一行列の Euler 分解一致、モデル初回配置と
325
+ cached/downloaded 両方の SHA-256 不一致拒否を unit test する。実素材の検出率・CPU 時間・
326
+ 実時間比は fieldtest 12 秒窓で別途記録し、値を捏造して本契約へ先書きしない
170
327
 
171
328
  ## 6. やらないこと(v0)
172
329
 
@@ -88,7 +88,7 @@ LUT の `intensity` 省略時は `1` として描画する。
88
88
 
89
89
  ## 7. 実装対応
90
90
 
91
- - `packages/render-cut/src/layers.mjs`: ベース映像の split、ルック適用、白 quad mask、`maskedmerge` による region 内合成を行う。filter layer は追加の `-i` を作らない。
91
+ - `packages/render-cut/src/filter-mask.mjs` / `layers.mjs`: corner keyframes からフレーム単位の縮小 gray8 quad mask を生成し、ベース映像の split、ルック適用、拡大したマスクとの `maskedmerge` による region 内合成を行う。filter layer ごとにマスク動画用の `-i` を 1 本追加する。
92
92
  - `packages/akari-tools/bin/finger-frame.mjs`: `--kind filter --filter invert|lut:<id>|saturation:<value>` から、既存と同じ gesture window と corner keyframes を持つ layer を生成する。
93
93
 
94
94
  ## 8. 検証
@@ -0,0 +1,163 @@
1
+ ---
2
+ lifecycle: draft
3
+ created: 2026-08-12
4
+ updated: 2026-08-12
5
+ ---
6
+
7
+ # 静止画 cut ソース契約 v0
8
+
9
+ - 日付: 2026-08-12
10
+ - 状態: **ドラフト**(v0 実装と同時に確定させる。実装で判明した齟齬は追記で解消)
11
+ - 前提:
12
+ - `contract-2026-07-22-render-basics.md`(cuts[] の speed/freeze/framing/transition_out の残裁定。
13
+ 本契約はそのうち speed/freeze の適用範囲を静止画ソースへ拡張する)
14
+ - `contract-2026-08-10-image-layer-parity.md` 相当の司令塔裁定(`layers[].src` の拡張子判定。
15
+ 本契約はそれを `cuts[]`(メイン時間軸)へ輸入する)
16
+ - `contract-2026-08-02-preview-parity.md`(render/Web UI/shell 3 面パリティの原則。本契約が
17
+ 更新する適合状況表は同ファイル §3)
18
+ - `contract-2026-07-17-data-contract-versioning.md`(version 整数・追加のみ・寛容リーダーの三原則)
19
+ - スコープ: `edit.json` の `cuts[]` がメイン時間軸で静止画ソース(png/jpg/jpeg/webp/bmp/gif)を
20
+ 直接読めるようにする。**新しいスキーマフィールドは作らない**(判定は拡張子のみ)
21
+
22
+ ## 0. 背景
23
+
24
+ これまで `cuts[]` は「時間軸を持つ動画」だけを前提にしていた。画像をタイムラインに置きたい場合、
25
+ 利用者は画像群を先に 1 本の動画へ連結してから `edit.json` に載せる遠回りを強いられていた
26
+ (実機報告 2026-08-12)。旧参照実装 `akari-video-on-os` には 2026-05-09 から静止画クリップの経路
27
+ (`isStatic` フラグ + `-loop 1 -t 尺` + `anullsrc` 無音合成)が実運用されており、新実装への
28
+ 「輸入漏れ」だった。`layers[]`(PinP・B-roll レイヤー)は 2026-08-10 の image-layer-parity で
29
+ 既に拡張子判定の静止画対応が入っている。本契約はその判定方式をメイン時間軸の `cuts[]` へ
30
+ 輸入する。
31
+
32
+ ## 1. 判定規則(司令塔裁定)
33
+
34
+ - **判定は拡張子のみ**: v1 `sources[].path` / v0 `source.path` が
35
+ `/\.(png|jpe?g|webp|bmp|gif)$/i` に一致したら静止画ソースとして扱う。
36
+ - スキーマに `isStatic` 等の新フィールドは**足さない**。`edit.schema.json` の `cutV0`/`cutV1`/
37
+ `sourceV0`/`sourceV1` の形は不変(`$comment` の追記のみ)
38
+ - この正規表現は `packages/render-cut/src/layers.mjs` の `IMAGE_LAYER_SOURCE_PATTERN`
39
+ (画像レイヤーの先行裁定)と同一集合。判定ロジックの実体は 3 箇所で個別に持つ
40
+ (パッケージをまたいだ import はしない方針 — 各パッケージが単体でビルド/型チェック完結する
41
+ 構成を崩さないため。`packages/render-cut/src/plan.mjs`/`render-cut.mjs` だけは同一パッケージ内
42
+ なので `layers.mjs` の `isImageLayerSource` をそのまま import する):
43
+ - `packages/render-cut/src/layers.mjs`(`isImageLayerSource`。既存・本契約は変更しない)
44
+ - `packages/edit-lint/src/edit-lint.mjs`(`IMAGE_CUT_SOURCE_PATTERN`)
45
+ - `packages/preview-engine/src/clipSession.ts`(`STILL_IMAGE_SOURCE_PATTERN`)
46
+ - `packages/preview-server/src/edit-to-timeline.mjs` / `public/app.js`(後者は既存の
47
+ `IMAGE_LAYER_SRC_PATTERN`/`isImageLayerSrc` を cuts 判定にも再利用する)
48
+
49
+ ## 2. レンダー(render-cut)
50
+
51
+ ### 2.1 ffmpeg レシピ
52
+
53
+ 静止画ソースへの入力は `-loop 1` を付けて動画化する(旧参照実装 `akari-video-on-os` が
54
+ 2026-05 から実運用したレシピと同じ発想。`source.chroma_key.background` の画像背景が既に
55
+ 同じ `-loop 1` パターンをこのリポで使っている)。`-loop 1` は image2 デマルチプレクサを
56
+ 無限長ストリームにするだけで、実際の表示区間は既存の `trim=start=<in>:end=<out>` フィルタが
57
+ 決める(cut ごとに毎回この trim を通す設計は動画ソースと共通のため、静止画専用の別経路を
58
+ 新設する必要がなかった)。フレームレートは明示指定せず、既存の `fps=<output.fps>` 正規化フィルタ
59
+ (動画ソースに対しても既に全経路にある)にそのまま乗せる。
60
+
61
+ 対象 3 経路(すべて `packages/render-cut/src/plan.mjs`):
62
+ - `buildCutCommand`(v0 既定の逐次連結パス)
63
+ - `buildGapAwareCutCommand`(v0 の明示 at/track 配置パス)
64
+ - `buildMultiSourceCutCommand`(v1。ソースごとに拡張子判定するため、動画と静止画が
65
+ `sources[]` に混在してよい)
66
+
67
+ ### 2.2 音声
68
+
69
+ 静止画には音声ストリームが無い。`hasAudio`/`source.hasAudio` は ffprobe が音声ストリームを
70
+ 検出しないことで自然に `false` になり、**3 経路とも既存の「無音源」分岐(`anullsrc` の無音
71
+ stereo を合成する分岐)がそのまま発火する**。この分岐は静止画専用に新設したものではなく、
72
+ 音声トラックを持たない動画ソース(無音動画)に対して既に存在していた既定動作である。
73
+ 静止画区間の音声は無音・動画区間は元音声が残る、という裁定はこの既存分岐の副産物として
74
+ 自動的に成立する。
75
+
76
+ ### 2.3 duration probe の例外
77
+
78
+ ffprobe は素の静止画ファイルに対して `format.duration` を報告しない(`-loop 1` を付けて
79
+ probe しても同じ。実測確認済み)。`packages/render-cut/src/render-cut.mjs` の
80
+ `measureCapabilities` は元々この欠落を「ffprobe が正の尺を返さなかった」エラーとして
81
+ 即座に reject していたため、静止画ソースを渡すと duration probe の時点で必ず落ちていた。
82
+ 静止画ソースだけ duration の positivity チェックを skip し、`sourceDuration`/
83
+ `sourceInputs[].duration` は `null` のまま通す(§2.4 の理由により、この `null` が実際に
84
+ 参照される経路は存在しない)。
85
+
86
+ ### 2.4 v0「cuts 空 = source 全体」の不成立
87
+
88
+ v0 は歴史的に `cuts` が空配列のとき「source 全体を 1 カットとして扱う」省略記法を持つ
89
+ (`predictedDuration` が `sourceDuration` をそのまま尺として返す)。静止画には尺という概念が
90
+ 無いため、この省略記法は成立しない。**静止画ソースで `cuts` が空の場合は edit-lint がエラーで
91
+ 止める**(§3.3)。`packages/render-cut/src/plan.mjs` の `buildPlan` にも同じ条件の防御的
92
+ バックストップを置いてある(lint を経由しない直接呼び出し向け。`buildTrackStackPlan` の
93
+ `transition_out` バックストップと同じ姿勢)。
94
+
95
+ ## 3. in/out・freeze・speed の意味論(edit-lint が検証)
96
+
97
+ ### 3.1 in/out
98
+
99
+ 静止画 cut の表示尺は `out - in`。`in` は素材内の「どこから」に対応する概念が静止画には無いため
100
+ **0 を推奨**する。0 以外を指定してもレンダーは `out - in` の尺だけを使い、`in` 自体の値は
101
+ (trim の開始オフセットとしては使われるが、無限長ループの中のどの一点から始めても絵は同じなので)
102
+ 見た目に影響しない。0 以外は edit-lint が **警告**(`cuts.still-image-in`)を出す。
103
+
104
+ ### 3.2 freeze / speed
105
+
106
+ - `cuts[].freeze` は静止画には視覚的な no-op(すでに静止している画に「静止」を足しても変化が
107
+ 無い)。尺だけが `freeze.duration_sec` ぶん伸びる。動作はする(クラッシュしない)が意図が
108
+ 紛れやすいため edit-lint が**警告**(`cuts.still-image-freeze`)を出す。同じ尺の延長を
109
+ 得たいなら `out` を直接伸ばす方が素直、という代替案をメッセージに含める
110
+ - `cuts[].speed` も同様に視覚効果が無い(静止画にコマ送りの概念が無い)。表示尺を
111
+ `(out - in) / speed` へ再スケールするだけなので動作はするが、edit-lint が**警告**
112
+ (`cuts.still-image-speed`)を出す
113
+
114
+ ### 3.3 v0 空 cuts の拒否
115
+
116
+ `source.path` が静止画で `cuts` が空のとき、edit-lint は**エラー**
117
+ (`cuts.still-image-cuts-required`)で止める(§2.4)。
118
+
119
+ ### 3.4 duration probe skip
120
+
121
+ `source.path`(v0)が静止画のとき、edit-lint は `probeDuration` の呼び出し自体を skip する
122
+ (§2.3 と同じ理由 — ffprobe が duration を返さないため。skip しないと edit-lint 自体が
123
+ `ExecutionError` で落ちて PASS/FAIL の verdict を返せなくなる)。`skipped[]` に理由を記録する。
124
+
125
+ ## 4. スキーマ
126
+
127
+ `edit.schema.json` の `sourceV0`/`sourceV1` に `$comment` を追記した(判定規則と cuts 側の
128
+ 意味論への参照)。`cutV0`/`cutV1` 自体の構造は変更なし。`packages/schemas/examples/
129
+ edit-cuts-still-image-source-valid/` に mp4 + png 混在の v1 valid 例を追加した。
130
+
131
+ ## 5. プレビュー(Web UI)
132
+
133
+ ### 5.1 preview-engine(`packages/preview-engine`)
134
+
135
+ `TimelineClip.mediaType`(既存の予約フィールド。`'video' | 'image'`)を実際に使う経路を実装した。
136
+ `ClipSession` はコンストラクタで `src` の拡張子から静止画かどうかを判定し、静止画なら
137
+ `MP4Clip`/WebCodecs デコーダを一切使わず `fetch` + `createImageBitmap` だけで読み込む。
138
+ `tickExact`/`tickApprox`/`tickBackground` は毎回同じ `ImageBitmap` から `new VideoFrame(bitmap,
139
+ {timestamp, duration})` を合成して返す。呼び出し側(`PreviewEngine.renderFrame`・
140
+ `ThumbnailTrack`)は返る `VideoFrame` を `drawImage` して `close()` するだけなので、動画由来か
141
+ ここで合成したものかを区別しない — 既存コードは無改修
142
+
143
+ ### 5.2 preview-server(Web UI 本体)
144
+
145
+ `packages/preview-server/public/index.html` に `<video id="preview-video">` と同じ位置・
146
+ サイズで重なる `<img id="preview-image">` を追加した(既定 `display: none`)。
147
+ `app.js` は現在のセグメントが静止画のとき `<video>` を `pause()` して隠し、`<img>` の `src` を
148
+ セグメントの画像へ合わせて表示する(`showStillImageForSegment`/`showVideoBase`)。`<video>`
149
+ 要素自体は作り直さない — `MediaElementAudioSourceNode` は生成元の要素に紐付くため、要素を
150
+ 差し替えると音声グラフが壊れる。`playedCutLocalSeconds`(`framing`/`freeze` の判定に使う
151
+ カット内経過秒)は静止画区間では `video.currentTime` の代わりにマスタークロック
152
+ `outputTime` から直接算出する(画像はシークしないため `video.currentTime` が更新されない)。
153
+
154
+ ### 5.3 apps/shell(スコープ外)
155
+
156
+ Electron シェル本体(`apps/shell`)のプレビュー対応は本タスクのスコープ外。`layers[]` の
157
+ image-layer-parity ではシェル側 webview も同時対応していたが、本タスクの司令塔裁定でシェルは
158
+ 別タスクへ切り出されている。§3 の適合状況表にシェル列は `❌`(未対応)として記録する。
159
+
160
+ ## 6. 適合状況の更新
161
+
162
+ `contract-2026-08-02-preview-parity.md` §3 の適合状況表に `cuts[].static-image-source` 行を
163
+ 追加した(Web UI / shell 列)。