akari-video 0.1.33 → 0.1.35

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 (112) hide show
  1. package/package.json +1 -1
  2. package/src/cli.mjs +2 -0
  3. package/src/repo-assets.mjs +7 -2
  4. package/src/word-book-command.mjs +16 -0
  5. package/vendor/.akari-capability-sources.json +9 -0
  6. package/vendor/docs/contract-2026-07-14-edit-json-v1-audio.md +4 -0
  7. package/vendor/docs/contract-2026-07-20-edit-json-v1-narration.md +3 -0
  8. package/vendor/docs/contract-2026-07-22-render-basics.md +1 -0
  9. package/vendor/docs/contract-2026-08-02-creator-root-v1.md +2 -1
  10. package/vendor/docs/contract-2026-08-02-preview-parity.md +20 -0
  11. package/vendor/docs/contract-2026-08-28-v2-audio-roles-v0.md +16 -11
  12. package/vendor/docs/contract-2026-08-29-media-inspect-cli-v0.md +2 -1
  13. package/vendor/docs/contract-2026-09-02-asset-reference-model.md +57 -0
  14. package/vendor/docs/contract-2026-09-02-audio-clip-fx-v1.md +56 -0
  15. package/vendor/docs/contract-2026-09-02-audio-envelope-v1.md +91 -0
  16. package/vendor/docs/contract-2026-09-02-audio-insert-level-v1.md +110 -0
  17. package/vendor/docs/contract-2026-09-02-captions-style-preset-v0.md +17 -1
  18. package/vendor/docs/contract-2026-09-02-item-caption-anchor-v0.md +9 -5
  19. package/vendor/docs/contract-2026-09-02-shape-item-v0.md +49 -0
  20. package/vendor/docs/contract-2026-09-02-transcript-unrecognized-spans-v0.md +21 -0
  21. package/vendor/docs/contract-2026-09-02-word-book-v0.md +404 -0
  22. package/vendor/packages/akari-launcher/package.json +1 -1
  23. package/vendor/packages/akari-tools/README.md +21 -0
  24. package/vendor/packages/akari-tools/package.json +3 -2
  25. package/vendor/packages/asset-resolver/bin/akari-assets.mjs +55 -6
  26. package/vendor/packages/asset-resolver/src/bundle.mjs +43 -0
  27. package/vendor/packages/asset-resolver/src/project-references.mjs +131 -0
  28. package/vendor/packages/asset-resolver/src/resolve.mjs +27 -8
  29. package/vendor/packages/asset-resolver/test/project-references.test.mjs +163 -0
  30. package/vendor/packages/audio-library-setup/README.md +11 -0
  31. package/vendor/packages/audio-library-setup/shared/insert-level.mjs +105 -0
  32. package/vendor/packages/audio-library-setup/test/insert-level.test.mjs +110 -0
  33. package/vendor/packages/edit-lint/src/edit-lint.mjs +341 -34
  34. package/vendor/packages/edit-lint/src/library-reference.mjs +80 -0
  35. package/vendor/packages/edit-store/lib/audio-schedule.d.ts +17 -3
  36. package/vendor/packages/edit-store/lib/audio-schedule.js +98 -33
  37. package/vendor/packages/edit-store/lib/caption-clock.d.ts +56 -0
  38. package/vendor/packages/edit-store/lib/caption-clock.js +80 -0
  39. package/vendor/packages/edit-store/lib/caption-display.d.ts +5 -0
  40. package/vendor/packages/edit-store/lib/caption-display.js +36 -2
  41. package/vendor/packages/edit-store/lib/caption-store.d.ts +28 -0
  42. package/vendor/packages/edit-store/lib/caption-store.js +167 -8
  43. package/vendor/packages/edit-store/lib/ducking.d.ts +1 -6
  44. package/vendor/packages/edit-store/lib/ducking.js +3 -12
  45. package/vendor/packages/edit-store/lib/edit-store.d.ts +38 -0
  46. package/vendor/packages/edit-store/lib/edit-v2-item-write.d.ts +2 -0
  47. package/vendor/packages/edit-store/lib/edit-v2-item-write.js +28 -15
  48. package/vendor/packages/edit-store/lib/edit-v2.d.ts +32 -2
  49. package/vendor/packages/edit-store/lib/edit-v2.js +80 -5
  50. package/vendor/packages/edit-store/lib/envelope.d.ts +34 -0
  51. package/vendor/packages/edit-store/lib/envelope.js +289 -0
  52. package/vendor/packages/edit-store/lib/generated/edit-v2-keys.d.ts +8 -6
  53. package/vendor/packages/edit-store/lib/generated/edit-v2-keys.js +38 -5
  54. package/vendor/packages/edit-store/lib/index.d.ts +3 -0
  55. package/vendor/packages/edit-store/lib/index.js +3 -0
  56. package/vendor/packages/edit-store/lib/internal-model.d.ts +3 -0
  57. package/vendor/packages/edit-store/lib/internal-model.js +100 -7
  58. package/vendor/packages/edit-store/lib/item-anchor.d.ts +2 -0
  59. package/vendor/packages/edit-store/lib/item-anchor.js +25 -0
  60. package/vendor/packages/edit-store/lib/legacy-audio-view.js +16 -4
  61. package/vendor/packages/edit-store/lib/migrate/geometry.d.ts +71 -0
  62. package/vendor/packages/edit-store/lib/migrate/geometry.js +186 -0
  63. package/vendor/packages/edit-store/lib/migrate/index.d.ts +19 -0
  64. package/vendor/packages/edit-store/lib/migrate/index.js +166 -13
  65. package/vendor/packages/edit-store/lib/shape-markup.d.ts +3 -0
  66. package/vendor/packages/edit-store/lib/shape-markup.js +66 -0
  67. package/vendor/packages/edit-store/lib/timeline-map.d.ts +16 -0
  68. package/vendor/packages/edit-store/lib/timeline-map.js +43 -0
  69. package/vendor/packages/edit-store/lib/webview-kernel.d.ts +2 -0
  70. package/vendor/packages/edit-store/lib/webview-kernel.js +722 -57
  71. package/vendor/packages/media-bin/README.ja.md +19 -0
  72. package/vendor/packages/media-bin/README.md +19 -0
  73. package/vendor/packages/media-bin/bin/audio-measure.mjs +64 -0
  74. package/vendor/packages/media-bin/package.json +3 -0
  75. package/vendor/packages/media-bin/src/audio-measure.mjs +119 -0
  76. package/vendor/packages/media-bin/src/media-dimensions.mjs +96 -0
  77. package/vendor/packages/media-bin/src/preview-audio-sidecar.mjs +436 -123
  78. package/vendor/packages/media-bin/test/audio-clip-fx.test.mjs +103 -0
  79. package/vendor/packages/media-bin/test/audio-measure.test.mjs +123 -0
  80. package/vendor/packages/media-bin/test/fixtures/audio-measure/click-300ms.stderr +15 -0
  81. package/vendor/packages/media-bin/test/fixtures/audio-measure/silence-3s.stderr +12 -0
  82. package/vendor/packages/media-bin/test/fixtures/audio-measure/tone-1k-minus20db.stderr +15 -0
  83. package/vendor/packages/media-bin/test/media-dimensions.test.mjs +115 -0
  84. package/vendor/packages/media-bin/test/preview-audio-sidecar-async.test.mjs +257 -0
  85. package/vendor/packages/overlay-runtime/README.md +29 -2
  86. package/vendor/packages/schemas/bin/validate-edit.mjs +85 -0
  87. package/vendor/packages/schemas/bin/validate-word-book.mjs +165 -0
  88. package/vendor/packages/schemas/edit.schema.json +141 -3
  89. package/vendor/packages/schemas/engine-capabilities.json +12 -2
  90. package/vendor/packages/schemas/examples/edit-audio-clip-fx-invalid/edit.json +17 -0
  91. package/vendor/packages/schemas/examples/edit-audio-clip-fx-valid/edit.json +47 -0
  92. package/vendor/packages/schemas/examples/edit-audio-keyframes-t-order-invalid/edit.json +16 -0
  93. package/vendor/packages/schemas/examples/edit-audio-keyframes-valid/edit.json +19 -0
  94. package/vendor/packages/schemas/examples/edit-duck-db-out-of-range/edit.json +14 -0
  95. package/vendor/packages/schemas/examples/edit-sfx-ducking-valid/edit.json +19 -0
  96. package/vendor/packages/schemas/examples/edit-v2-shape-minimal-valid/edit.json +19 -0
  97. package/vendor/packages/schemas/examples/edit-v2-shape-negative-stroke-invalid/edit.json +23 -0
  98. package/vendor/packages/schemas/examples/edit-v2-shape-param-key-invalid/edit.json +23 -0
  99. package/vendor/packages/schemas/examples/edit-v2-shape-params-valid/edit.json +30 -0
  100. package/vendor/packages/schemas/examples/edit-v2-shape-unknown-invalid/edit.json +19 -0
  101. package/vendor/packages/schemas/examples/word-book-v0-invalid-reading-only-variants/word-book.json +6 -0
  102. package/vendor/packages/schemas/examples/word-book-v0-invalid-variant-conflict/word-book.json +7 -0
  103. package/vendor/packages/schemas/examples/word-book-v0-invalid-version-1/word-book.json +4 -0
  104. package/vendor/packages/schemas/examples/word-book-v0-valid/word-book.json +27 -0
  105. package/vendor/packages/schemas/test/audio-clip-fx.test.mjs +39 -0
  106. package/vendor/packages/schemas/test/edit-output-geometry.test.mjs +56 -0
  107. package/vendor/packages/schemas/test/edit-v2-schema.test.mjs +20 -2
  108. package/vendor/packages/schemas/test/engine-capabilities.test.mjs +2 -2
  109. package/vendor/packages/schemas/test/validate-edit.test.mjs +22 -0
  110. package/vendor/packages/schemas/test/validate-word-book.test.mjs +83 -0
  111. package/vendor/packages/schemas/word-book.schema.json +33 -0
  112. package/vendor/packages/word-book/package.json +11 -0
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "akari-video",
3
- "version": "0.1.33",
3
+ "version": "0.1.35",
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/cli.mjs CHANGED
@@ -24,6 +24,7 @@ import {
24
24
  import { applySelfUpdate, isRunningFromAppDir, rollbackSelfUpdate } from './self-update.mjs';
25
25
  import { runCaptureCommand } from './capture-command.mjs';
26
26
  import { runMediaCommand } from './media-command.mjs';
27
+ import { runWordBookCommand } from './word-book-command.mjs';
27
28
  import { resolveRuntimePaths } from './runtime-diagnostics.mjs';
28
29
 
29
30
  /**
@@ -44,6 +45,7 @@ export async function run(args, options = {}) {
44
45
  }
45
46
  if (args[0] === 'capture') return runCaptureCommand(args.slice(1), options);
46
47
  if (args[0] === 'media') return runMediaCommand(args.slice(1), options);
48
+ if (args[0] === 'word-book') return runWordBookCommand(args.slice(1), options);
47
49
 
48
50
  const log = options.log ?? ((line) => console.log(line));
49
51
  const assets = options.assets ?? resolveLauncherAssets();
@@ -55,6 +55,7 @@ export function resolveRepoAssets(repoRoot = DEFAULT_REPO_ROOT_CANDIDATE) {
55
55
  const renderWhenIdleScript = path.join(repoRoot, RENDER_WHEN_IDLE_SCRIPT_RELATIVE);
56
56
  const eyeBarScript = path.join(repoRoot, EYE_BAR_SCRIPT_RELATIVE);
57
57
  const mediaScript = path.join(repoRoot, 'packages', 'akari-tools', 'bin', 'media.mjs');
58
+ const wordBookScript = path.join(repoRoot, 'packages', 'akari-tools', 'bin', 'word-book.mjs');
58
59
 
59
60
  return {
60
61
  repoRoot,
@@ -71,7 +72,8 @@ export function resolveRepoAssets(repoRoot = DEFAULT_REPO_ROOT_CANDIDATE) {
71
72
  captureScript: existsSync(path.join(repoRoot, CAPTURE_SCRIPT_RELATIVE)) ? path.join(repoRoot, CAPTURE_SCRIPT_RELATIVE) : null,
72
73
  renderWhenIdleScript: existsSync(renderWhenIdleScript) ? renderWhenIdleScript : null,
73
74
  eyeBarScript: existsSync(eyeBarScript) ? eyeBarScript : null,
74
- mediaScript: existsSync(mediaScript) ? mediaScript : null
75
+ mediaScript: existsSync(mediaScript) ? mediaScript : null,
76
+ ...(existsSync(wordBookScript) ? { wordBookScript } : {})
75
77
  };
76
78
  }
77
79
 
@@ -105,6 +107,9 @@ export function resolveLauncherAssets({
105
107
  ...(candidate.captureScript ?? vendor.captureScript ? { captureScript: candidate.captureScript ?? vendor.captureScript } : {}),
106
108
  renderWhenIdleScript: candidate.renderWhenIdleScript ?? vendor.renderWhenIdleScript,
107
109
  eyeBarScript: candidate.eyeBarScript ?? vendor.eyeBarScript,
108
- mediaScript: candidate.mediaScript ?? vendor.mediaScript
110
+ mediaScript: candidate.mediaScript ?? vendor.mediaScript,
111
+ ...(candidate.wordBookScript ?? vendor.wordBookScript
112
+ ? { wordBookScript: candidate.wordBookScript ?? vendor.wordBookScript }
113
+ : {})
109
114
  };
110
115
  }
@@ -0,0 +1,16 @@
1
+ import { spawnSync } from "node:child_process";
2
+
3
+ import { resolveLauncherAssets } from "./repo-assets.mjs";
4
+
5
+ export async function runWordBookCommand(args, options = {}) {
6
+ const logError = options.logError ?? ((line) => console.error(line));
7
+ const assets = options.assets ?? resolveLauncherAssets();
8
+ const spawn = options.spawn ?? spawnSync;
9
+ if (!assets.wordBookScript) {
10
+ logError("akari word-book の実行スクリプトが見つかりません。完全な AKARI Video を再導入してください:");
11
+ logError(" npm install -g akari-video");
12
+ return { exitCode: 1 };
13
+ }
14
+ const result = spawn(process.execPath, [assets.wordBookScript, ...args], { stdio: "inherit" });
15
+ return { exitCode: typeof result?.status === "number" ? result.status : 1 };
16
+ }
@@ -51,10 +51,16 @@
51
51
  "docs/contract-2026-08-29-media-inspect-cli-v0.md",
52
52
  "docs/contract-2026-08-30-edit-json-v2-object-tree-v0.md",
53
53
  "docs/contract-2026-08-30-motion-and-keyframes-v0.md",
54
+ "docs/contract-2026-09-02-asset-reference-model.md",
55
+ "docs/contract-2026-09-02-audio-clip-fx-v1.md",
56
+ "docs/contract-2026-09-02-audio-envelope-v1.md",
57
+ "docs/contract-2026-09-02-audio-insert-level-v1.md",
54
58
  "docs/contract-2026-09-02-captions-style-preset-v0.md",
55
59
  "docs/contract-2026-09-02-export-verify-declared-vs-measured-v0.md",
56
60
  "docs/contract-2026-09-02-item-caption-anchor-v0.md",
61
+ "docs/contract-2026-09-02-shape-item-v0.md",
57
62
  "docs/contract-2026-09-02-transcript-unrecognized-spans-v0.md",
63
+ "docs/contract-2026-09-02-word-book-v0.md",
58
64
  "packages/akari-launcher/package.json",
59
65
  "packages/akari-launcher/README.md",
60
66
  "packages/akari-tools/package.json",
@@ -88,6 +94,8 @@
88
94
  "packages/intake-form/README.md",
89
95
  "packages/matte-rvm/package.json",
90
96
  "packages/media-bin/package.json",
97
+ "packages/media-bin/README.ja.md",
98
+ "packages/media-bin/README.md",
91
99
  "packages/osr-export/package.json",
92
100
  "packages/osr-export/README.md",
93
101
  "packages/overlay-runtime/package.json",
@@ -100,6 +108,7 @@
100
108
  "packages/template-render/package.json",
101
109
  "packages/template-render/README.ja.md",
102
110
  "packages/template-render/README.md",
111
+ "packages/word-book/package.json",
103
112
  "skills/address-review/SKILL.md",
104
113
  "skills/analyze-footage/analysis-json.md",
105
114
  "skills/analyze-footage/bin/face-expression/vendor/tasks-vision-0.10.17/README-AKARI.md",
@@ -87,6 +87,10 @@ edit.json の親ディレクトリを基準にした相対パス(絶対パス
87
87
 
88
88
  ## 4. ducking の v1 定義
89
89
 
90
+ > 2026-09-02 更新: `sidechaincompress` 方式は廃止した。現在は narration と speech の宣言区間を鍵に、
91
+ > attack / release を持つ共通決定論エンベロープをプレビューと書き出しの両方で使う。
92
+ > 以下は当初 v1 の設計記録として残す。
93
+
90
94
  **採用: `sidechaincompress`(書き出し側のみ、ffmpeg 標準フィルタ)。**
91
95
  「字幕/発話区間ベースの単純 gain」は不採用。
92
96
 
@@ -99,6 +99,9 @@ edit.json の親ディレクトリを基準にした相対パス(絶対パス
99
99
 
100
100
  ## 3. ducking の主従
101
101
 
102
+ 2026-09-02 契約により sidechain 方式を廃止し、鍵を narration と speech の宣言区間の和集合とする
103
+ 決定論エンベロープ方式へ移行した。既定の鍵は両方であり、対象は `ducking:true` の bgm / sfx である。
104
+
102
105
  `contract-2026-07-14-edit-json-v1-audio.md` §4 は `audio.bgm.ducking: true` のサイドチェイン
103
106
  入力(トリガー)を「ダイアログ音声(source 由来の音声トラックそのもの)」と定義していた。
104
107
  本契約でナレーションが第一級データになったことに伴い、この入力の**主従**を以下のとおり正文化する:
@@ -73,6 +73,7 @@
73
73
  ### 4-1. 画角(`cuts[].framing`)
74
74
 
75
75
  - **crop と keyframes の併存**: 両方宣言された場合は `keyframes` を優先する。`crop` は「1 点ズームの縮退形」であり、両立させる意味論が無いため(複製 drift の温床にもなる)
76
+ - **幾何の基準(2026-09-02 追記・相互参照)**: `output.geometry` は未指定 = fit 互換 / `"source"` = 実寸基準を表すマーカーで、正本は `docs/contract-2026-08-02-preview-parity.md` §2.2(G1 は描画無変更・framing の再定義は G2)。
76
77
  - **scale < 1 の扱い**: `keyframes[].scale` は仕組み上「クロップ窓を縮めて拡大する」ため 1 未満(キャンバスの外まで見せる=リビール)は原理的に表現できない。レンダ側で `max(1, scale)` にクランプする(silent drop ではなく仕組み上の上限として契約に明記)
77
78
  - **crop.w/h が init 一度しか評価されない**: ffmpeg の `crop` フィルタは `x`/`y` は `t` を使った毎フレーム再評価に対応するが、`w`/`h` は(この ffmpeg ビルドで)フィルタ初期化時の一度きりの評価に固定されており `eval` オプション自体が存在しない(実機検証: `t` を含む `w`/`h` 式は `crop=... w='...t...'` で `Error when evaluating the expression` を返す)。そのため実装は「`scale` を `eval=frame` で `width*scale(t) : height*scale(t)` に広げてから固定サイズ `width:height` で `crop` する」方式を採る(クロップ窓の拡大 = `scale` 側の時間関数、パン位置 = `crop` の `x`/`y` の時間関数、という役割分担)
78
79
  - **`crop` の `x`/`y` は上流フレームの実サイズを見ない**: 同フィルタの `iw`/`ih` 定数は(動的サイズの上流から来ていても)negotiate 済みの固定リンクサイズを指し、最初のフレームのサイズに固定されたままになることを実機検証で確認した。そのため `crop` の `x`/`y` 式は `iw`/`ih` を参照せず、`scale` 側と同じ `scale(t)` 式をそのまま再計算する(対称的だが唯一 crop から見て正しい現在値)
@@ -61,6 +61,7 @@ AKARI Video がマシン上に持ってよい場所は次の 3 つ**だけ**で
61
61
  ├── AGENTS.md # 橋渡し(akari.md / design.md へ誘導)。初回はスタブ生成
62
62
  ├── channels/
63
63
  │ └── <channel>/
64
+ │ ├── .akari/memory/word-book.json # 任意。チャンネル単語帳(word book v0 契約)
64
65
  │ ├── design.md # チャンネル設計書(任意。生成工程は非スコープ)
65
66
  │ └── videos/
66
67
  │ └── <project>/ # 動画プロジェクト(内部は project-structure-v0 のまま)
@@ -69,7 +70,7 @@ AKARI Video がマシン上に持ってよい場所は次の 3 つ**だけ**で
69
70
  └── .akari/
70
71
  ├── root.json # 作業場マニフェスト(版数・チャンネル一覧・生成日)
71
72
  ├── connections.json # provider・モデル・コスト承認ポリシーの作業場既定
72
- ├── memory/ # スタイル学習・記憶(memory-connection の既定接続先 §7)
73
+ ├── memory/ # スタイル学習・記憶(memory-connection の既定接続先 §7、word-book.json
73
74
  └── cache/ # 再生成可能物
74
75
  ```
75
76
 
@@ -11,6 +11,7 @@
11
11
  | 2026-08-02 | v0 | Web UI と shell の挙動仕様を統合 |
12
12
  | 2026-08-28 | v2 | `packages/frame-engine` の意味論へ統合し、検収をゴールデンフレームへ一本化。出口を OSR と GPU 直結の 2 本に固定し、互換経路を退役節へ移動 |
13
13
  | 2026-08-31 | v2.1 | §5.2 に断片 CSS の `vw` / `vh` 系単位の出力サイズ基準化(`viewport-units.js`。プレビューがウィンドウ幅基準で解いていた実機報告の修正)を追記 |
14
+ | 2026-09-02 | v2.2 | §2.8 に字幕時計の規約(active cue の判定は両プレビューとも出力秒。共有カーネル `caption-clock`)を追記。Web UI を shell に揃えた 4 点(字幕時計・字幕フォント名・`slot-params.js` の差し込み・最下段 cut の track 規則)の記録 |
14
15
 
15
16
  ## 1. 役割分担
16
17
 
@@ -69,6 +70,13 @@ cut 境界の選択は宣言順ではなく解決済みタイムラインと z-o
69
70
  `cut <id>: perspective is not applied by the frame-engine base path yet (issue #39)` を warning に出す(無警告で捨てない)。
70
71
  - `freeze = {at_sec, duration_sec}` は指定 frame を保持し、cut の出力尺を `duration_sec` だけ伸ばして
71
72
  後続の逐次 cut を移動する。freeze の画と独立音声予定表を混同しない。
73
+ - **`output.geometry`(幾何の基準。2026-09-02 追記)**: 未指定 = **fit 互換**(cut は出力へ contain fit した後に
74
+ transform。上記の従来どおり)、`"source"` = **実寸基準**(ソース実寸 × scale の box。layer・layer-style cut と同じ幾何)。
75
+ 語彙は `"source"` の 1 つだけで、マーカーが立つのは「今 fit 基準で描かれている全 media item に `scale × fit`
76
+ (`fit = min(outputW / srcW, outputH / srcH)`・srcW / srcH は表示回転後)を一度だけ焼き込んだ」ことを意味する
77
+ (部分適用は禁止。移行は `packages/edit-store/bin/normalize-geometry.mjs`)。x / y / rotate は両基準で同じ意味なので触らない。
78
+ **G1(マーカー・移行・lint の warning `geometry.fit-compat`)ではエンジンはこのマーカーを読まず、描画は 1 バイトも変わらない。
79
+ 描画へ反映するのは G2**。cross ref: `docs/contract-2026-07-22-render-basics.md` §4-1(#6 画角操作)。
72
80
 
73
81
  **検収:** framing / transform / opacity / freeze を含む base parity **28 点**、freeze をまたぐ
74
82
  frame lifetime **1000 コマ**、故意の **1 px** 差分を必ず FAIL させる否定点で判定する。
@@ -128,6 +136,12 @@ finalFrameNumber **239**、lookahead hits **8**を要求する。
128
136
  shell のプレビューでは DOM 層として提示し、書き出しでは同じ DOM 規約から overlay sheet を構成する。
129
137
  器専用の字幕 HTML や出口専用の再レイアウトを持たない。
130
138
 
139
+ active cue の判定は両プレビューとも**出力秒**で行う。source 秒の cue(`time_domain: "source"` と未宣言の
140
+ legacy)は共有カーネル `packages/edit-store/src/caption-clock.ts` の `normalizeCaptionClock` が cut map
141
+ で出力秒へ射影し、削除区間をまたぐ cue は 1 本ずつに分割する(`<id>-output-<n>`、元 id は
142
+ `sourceCueId`)。shell は `normalizePreviewCaptionClock`、Web UI は `updateCaption` がこの正規化済み表を
143
+ 読み、描画層は domain 判定を行わない(2026-09-02。それまで Web UI は source 秒で判定していた)。
144
+
131
145
  **検収:** OSR ソフト描画の同一 fixture 2 走について、字幕を含む全コマ raw BGRA の SHA-256 一致を要求する。
132
146
 
133
147
  ### 2.9 overlays
@@ -161,6 +175,7 @@ SHA-256 一致を要求する。GPU は同一マシン一致率を診断値と
161
175
  | cuts の perspective | 🟡 未適用・warning のみ(issue #39) | 🟡 同左 | ✅ DOM 層 | 🟡 warning を run.json へ(seek の warning を回収) | 🟡 warning を run.json へ |
162
176
  | 5 transitions | ✅ 評価 | ✅ 完成 frame を提示 | ✅ 完成 frame を提示 | ✅ 完成 frame を捕捉 | ✅ canvas を直結 |
163
177
  | matte / chroma key | ✅ 評価 | ✅ stamp 同期 | ✅ stamp 同期 | ✅ stamp 同期・捕捉 | ✅ 同一 frame 評価 |
178
+ | アルファ層の取り込み(`.webm` / `.mov` → color + mask mp4) | — 入力境界の外(media-bin `alpha-intake` が正本) | ✅ サーバ側 `prepareAlphaLayers`(`frameEngine.intake`) | ✅ node RPC `prepareAlphaIntake`(同一 media-bin・同一派生物、2026-09-02) | ✅ page-builder | ✅ page-builder |
164
179
  | LUT / `bt709-limited` | ✅ 評価 | ✅ 提示 | ✅ 提示 | ✅ 捕捉・encode | ✅ LUT 後 canvas を直結 |
165
180
  | 字幕 | — DOM 規約へ active state を供給 | ✅ DOM 層 | ✅ DOM 層 | ✅ 同規約の overlay sheet | 🟡 適格 cue を sprite 化 |
166
181
  | overlays / 3D | — DOM 規約へ時刻を供給 | ✅ DOM 層 | ✅ DOM 層 | ✅ 同規約の overlay sheet | 🟡 static / 宣言型 3D のみ |
@@ -269,6 +284,11 @@ lint 実行系が見つからない場合は **fail-open**(2026-08-02 オー
269
284
  保存を続行する。書き込みは tmp ファイルへの出力と rename による atomic 更新とする。実装は
270
285
  `packages/edit-store` に一本化し、器や入口ごとの独自書き込み実装を追加してはならない。
271
286
 
287
+ 本編 cut の `crop` 書き戻し(選択枠の辺バー)は **edit.json version 2 の文書だけ**が対象で、legacy の
288
+ `cuts[]` schema には席が無いため読み込み層が拒否する。`output.geometry` を宣言していない文書では、crop の
289
+ 無い cut は出力キャンバスへ contain fit されて描かれるので、**初回の crop と同一 patch で `transform.scale`
290
+ へ fit 係数を焼き込む**(ソース実寸基準の layer-style へ移っても画面上の位置・大きさが変わらないため)。
291
+
272
292
  ### 5.4 ペン
273
293
 
274
294
  ペン描画の単一正本は `packages/pen-visuals` の `PEN_TUNING` と描画プリミティブである。器や overlay
@@ -25,7 +25,7 @@ render-cut は最終成果物の音声品質を決める唯一の正である。
25
25
 
26
26
  | 項目 | プレビュー(Web Audio) | 書き出し(ffmpeg) | 差の性質 |
27
27
  |---|---|---|---|
28
- | ducking | narration の実効区間を矩形エンベロープ化し、区間内をカーネル既定の固定減衰にする | narration を主、素材ダイアログを fallback の sidechain とする `sidechaincompress` | プレビューは発話レベルに反応せず、attack / release も持たない。master 無しの実測差は 1.67 dB(§3.2) |
28
+ | ducking | 共通決定論エンベロープを `exponentialRampToValueAtTime` で適用する | 同じエンベロープを f32le 化して `amultiply` する | デコーダとサンプル境界の差だけを許容する |
29
29
  | BGM fade | タイムライン上の `fadeIn` / `fadeOut` を線形 `AudioParam` automation にする | `afade` をマスター音声チェーン内で適用する | master 無しの実測差は ±0.034 dB 以内(§3.2)。後段処理後の振幅は一致しない場合がある |
30
30
  | SFX fade | クリップ実効尺に対する `fade_in` / `fade_out` を線形 automation にする | trim 後の実効尺へ `afade` を適用する | 基本形状は同じ。後段マスター処理の影響は書き出しだけに生じる |
31
31
  | ラウドネス | 個別 gain と fade / ducking の合成のみ。番組全体の規格化はしない | EBU R128 計測とマスター処理を適用する | I / LRA / TP は一致を保証しない |
@@ -105,7 +105,7 @@ ms へ換算し、絶対値で集計する。全 59 点の最大は 16.667 ms、
105
105
 
106
106
  **G3 裁定(2026-08-28):**
107
107
 
108
- 1. ducking は矩形近似を残す。カーネル共通エンベロープ化は別票とする。
108
+ 1. 2026-09-02 契約で共通決定論エンベロープ化した。`sidechaincompress` は廃止した。
109
109
  2. fade は時刻が一致しているため近似として残す。
110
110
  3. ループ境界 / trim 境界の追加測定は別票とする。
111
111
  4. SFX の true peak は書き出し確認必須のままとし、プレビューに保護を入れない。
@@ -115,8 +115,7 @@ ms へ換算し、絶対値で集計する。全 59 点の最大は 16.667 ms、
115
115
 
116
116
  - Web Audio と ffmpeg のデコーダ、サンプル境界、出力デバイスの差
117
117
  - プレビューで afftdn / loudnorm / true peak guard を実行しないこと
118
- - プレビュー ducking を固定区間で表すこと。ただし §3 の差が G3 の許容範囲を超える場合は、
119
- §4.1 の共通エンベロープ案を採用してから G3 へ進む
118
+ - ducking の共通エンベロープは両経路で同じで、デコーダとサンプル境界の差だけを残すこと
120
119
  - 聴感確認の即時性を優先し、プレビューでは最終マスター処理の計算コストを負わないこと
121
120
 
122
121
  これらの近似を残しても、タイムライン上の開始、trim、loop、gain、fade、ducking 対象区間は
@@ -124,10 +123,10 @@ ms へ換算し、絶対値で集計する。全 59 点の最大は 16.667 ms、
124
123
 
125
124
  ## 6. 撮影素材の台詞と ducking 側鎖
126
125
 
127
- `speech` は cuts[] の撮影素材に含まれる音声を表し、各カットの trim / speed / freeze を
128
- 出力タイムラインへ投影して Web Audio プレビューへ供給する。ただし `speech` ducking
129
- 側鎖へ入れない。書き出しの `sidechaincompress` で鍵入力になるのは narration のみであり、
130
- 土台カット音声 `[0:a]` は鍵ではないため、プレビューも同じ対象区間を維持する。
126
+ `speech` は cuts[] の撮影素材に含まれる音声を表し、各カットの trim / speed を出力タイムラインへ
127
+ 投影する。2026-09-02 契約では `analysis.json` transcript 区間を同じ写像で ducking 鍵へ加える。
128
+ 書き出しはこの speech 鍵を消費する。プレビューサーバーへ speech 鍵を渡す配線は同契約の範囲外で、
129
+ 配線されるまでは narration 鍵だけへ劣化する。
131
130
 
132
131
  ## 7. 速度変更した台詞のピッチ保持サイドカー
133
132
 
@@ -157,6 +156,12 @@ FLAC(compression level 5)へ一度だけ切り出す。`transition_out` 境
157
156
 
158
157
  BGM / SFX / narration は、元ファイルが WAV かつ 8 MB 超のとき同じ FLAC サイドカーを使う。
159
158
  frame-engine は映像の ready を先に成立させ、その直後から予定表上の初回使用時刻順・同時 2 本で
160
- 全音声を非同期にデコードする。デコード済み PCM は合計バイトで管理し、既定上限は 256 MB
161
- 上限超過時は次に使う時刻が最も遠い項目から退避する。再生開始は先読み済みの項目を再利用し、
162
- 未着の項目だけを待つため、先読み処理は初期フレームの描画を止めない。
159
+ 全音声を非同期にデコードする。デコード済み PCM は合計バイトで管理し、既定予算は 256 MiB
160
+ 予算を超えても buffer は捨てない(2026-09-02 改訂。以前は次に使う時刻が最も遠い項目から黙って
161
+ 退避し、その音源が予定表から消えて無音になっていた)。超過は警告 1 行と `debug().prefetch.overBudget`
162
+ で示す。展開後のサイズが 64 MiB(48 kHz ステレオ約 2.9 分)を超えると見積もられる音源は、
163
+ `OfflineAudioContext` で 24 kHz に落として decode しモノラルへ畳んで保持する(compact。
164
+ プレビュー専用の近似で、納品マスターには影響しない)。見積もりは WAV / FLAC のヘッダから、
165
+ 他の圧縮音声は符号化サイズの 16 倍で行う。再生開始は先読み済みの項目を再利用し、未着の項目だけを
166
+ 待つため、先読み処理は初期フレームの描画を止めない。decode に失敗した項目は 5 秒空けて再試行し、
167
+ それまで `debug().prefetch.failed` に載る。
@@ -126,7 +126,7 @@ ffmpeg / ffprobe / whisper.cpp を直接叩いている。本契約はそれを
126
126
  transcribe の要否を決めるゲートに使い、字幕の根拠にはしない
127
127
  - 目安コスト: 秒。帳面には `tracks.waveform` として追記(§3)
128
128
 
129
- ### 2.5 `akari media transcribe <target> [--in <time> --out <time>] [--backend <name>] [--lang <code>] [--no-unrecognized] [--unrecognized-min-gap <sec>] [--unrecognized-min-voiced <sec>]`
129
+ ### 2.5 `akari media transcribe <target> [--in <time> --out <time>] [--backend <name>] [--lang <code>] [--no-unrecognized] [--unrecognized-min-gap <sec>] [--unrecognized-min-voiced <sec>] [--no-word-book] [--word-book <path>]`
130
130
 
131
131
  何を喋っているかを、語ごとの時刻つきで返す。
132
132
 
@@ -144,6 +144,7 @@ ffmpeg / ffprobe / whisper.cpp を直接叩いている。本契約はそれを
144
144
 
145
145
  - `segments[]` の形は analysis.json v0 の `transcript[]` と同一(そのまま写せる)。時刻は **source 秒**
146
146
  - 各 segment は任意の `unrecognized: [{ start, end }]` に「音はあるが文字にできなかった区間」を持てる。既定は語の隙間 0.45 秒以上から無音を引いた残り 0.3 秒以上を採用し、`--unrecognized-min-gap` / `--unrecognized-min-voiced` で閾値を変え、`--no-unrecognized` で検出を止める。キャッシュ key は従来どおりで、古い cache hit に `unrecognized` が無い場合は仕様どおりそのまま返し、再 transcribe で付与する
147
+ - 既定では STT 直後に作業場基底の単語帳を語境界で適用する。`--no-word-book` で無効化し、`--word-book <path>` で project より近い検証用の層を追加できる。文字起こしキャッシュは置換前の生出力を保存し、単語帳プリパスは cache hit / miss ともキャッシュ読み出し後・analysis.json 記録前に毎回適用する
147
148
  - バックエンドは analyze-footage の 3 層と同じ優先順: macOS SpeechAnalyzer(26+・swiftc 可)→ whisper.cpp → クラウド。
148
149
  **クラウドは `--backend cloud:<connection-id>` を明示したときだけ**で、`.akari/connections.json` に doctor `ok` で
149
150
  登録済みの接続に限る。既定で外部に音声を送らない。キーの値を stdout / stderr / 出力ファイルに出さない
@@ -0,0 +1,57 @@
1
+ # contract — 素材の参照モデル v0(共有ライブラリ参照の台帳と解決規則)
2
+
3
+ - 状態: 実装済み(機械層のみ。シェル UI の採用は後続)
4
+ - 決定日: 2026-09-02
5
+ - 実装: `packages/asset-resolver`(記帳・実体化)/ `packages/render-cut`・`packages/edit-lint`(解決)
6
+
7
+ ## 1. 目的
8
+
9
+ カタログ素材をプロジェクトごとに実体コピーすると、同じ素材が何度もダウンロード・複製されて
10
+ プロジェクトが肥大する。実体は**マシン単位の共有ライブラリ**(`~/.akari/assets/<category>/<id>/`)に
11
+ 1 部だけ置き、プロジェクトには**参照だけを記録**できるようにする。
12
+
13
+ ## 2. 設計の要点
14
+
15
+ - **edit.json は変えない**。参照素材も従来どおり `assets/<category>/<id>/<file>` の
16
+ プロジェクト相対パスで宣言される。実体がプロジェクトに無いことは参照台帳が説明する。
17
+ - 参照台帳 = プロジェクトの `.akari/asset-references.json`:
18
+
19
+ ```json
20
+ { "version": 0, "references": [ { "id": "<素材 id>", "category": "<カテゴリ>" } ] }
21
+ ```
22
+
23
+ references は category → id の安定ソート・重複なし。読み手は寛容(無い / 壊れは空扱い)、
24
+ 書き込みは tmp + rename の原子的更新。`version` は台帳自身のスキーマ版数であり
25
+ edit.json の version とは無関係。
26
+ - **解決規則**: 宣言されたプロジェクト相対パスが `assets/<category>/<id>/<rest>` の形で、
27
+ (1) プロジェクト実体が存在せず、(2) 台帳に `{category, id}` があるとき、
28
+ `<AKARI_HOME>/assets/<category>/<id>/<rest>`(AKARI_HOME 既定 `~/.akari`・env で上書き可)へ
29
+ フォールバックする。解決先は realpath 後も `<AKARI_HOME>/assets` 配下に収まる正規ファイルで
30
+ あること(`..` 等の脱出は fail-closed で拒否)。
31
+ - render-cut は解決した入力を render inputs 記録に `scope: "library"` として残す
32
+ (既存 `scope: "akari"` と同列の additive 記録)。edit-lint は解決できる参照を欠落と報告せず、
33
+ 台帳にあるが実体が無い参照は「共有ライブラリ参照(未取得)」として欠落報告する。
34
+ - render-cut / edit-lint は依存ゼロ CLI のため、解決ロジックは各パッケージ内に**同一実装を重複**して
35
+ 持つ(`src/library-reference.mjs`)。挙動同一性は両テストの同一ケース表で担保する。
36
+
37
+ ## 3. 使い方(CLI)
38
+
39
+ ```sh
40
+ # 参照モードで取得(コピーせず台帳へ記帳。既定は従来どおりコピー)
41
+ akari-assets fetch <id> --project <dir> --reference
42
+
43
+ # 「素材をまとめる」— 参照の実体化(持ち出し・アーカイブ用)
44
+ akari-assets bundle --project <dir> [--dry-run]
45
+ ```
46
+
47
+ bundle は台帳の各参照をキャッシュから `assets/<category>/<id>/` へ実体化して台帳から除去する。
48
+ 未取得の参照は resolve(取得)を試み、取得できないものは台帳に残して部分成功(exit 非 0)で報告する。冪等。
49
+
50
+ ## 4. スコープ外(後続)
51
+
52
+ - シェル UI の採用(取り込みフローの reference 既定化・プロジェクト面の「参照」バッジ・
53
+ プレビュー経路のフォールバック)
54
+ - 共有キャッシュの容量管理 UI
55
+
56
+ 出自: 2026-09-02 の素材パネル再設計ラウンドの裁定 3(実体 = 共有キャッシュ・プロジェクトには参照・
57
+ 持ち出しは「素材をまとめる」で閉じる)。
@@ -0,0 +1,56 @@
1
+ # 音声クリップ FX v1
2
+
3
+ - 日付: 2026-09-02
4
+ - 状態: 実装契約
5
+ - 対象: legacy `audio.*`、v2 audio item、render-cut、preview-audio sidecar
6
+
7
+ ## 1. 宣言
8
+
9
+ | キー | 対象 | 型・範囲 | 既定 | v2 の位置 |
10
+ |---|---|---|---|---|
11
+ | `speed` | sfx / bgm | number `(0.25, 4]` | `1` | `item.source` |
12
+ | `pitch_semitones` | sfx / bgm | number `[-24, 24]` | `0` | `item.source` |
13
+ | `formant` | sfx / bgm | `preserve \| shift` | `preserve` | `item.source` |
14
+ | `denoise` | sfx / bgm / narration | `{ method: fft \| nlm, strength: 0..1 }` | なし | item |
15
+ | `lowcut_hz` | sfx / bgm / narration | number `[0, 400]` | `0` | item |
16
+
17
+ `speed` はタイムライン上の `t` を変えず、実効尺を素材窓の尺 / `speed` とする。
18
+ `pitch_semitones` は速度を変えない。narration の `speed` / `pitch_semitones` は TTS 側の責務なので
19
+ lint warning として無視する。legacy と v2 の投影は上表の値を往復で保持する。
20
+
21
+ ## 2. フィルタチェーン
22
+
23
+ クリップごとの順序は次で固定する。
24
+
25
+ `atrim(in/out)` → `highpass=f=<lowcut_hz>:p=2` → `highpass=f=<lowcut_hz>:p=2` → denoise →
26
+ `rubberband=tempo=<speed>:pitch=<2^(pitch_semitones/12)>:formant=<preserved|shifted>:pitchq=quality` →
27
+ `volume` → `afade` → envelope `amultiply` → `adelay`
28
+
29
+ lowcut は同一の 2 次 highpass を 2 段カスケードし、24 dB/oct の減衰特性で L1 ゲート
30
+ (1 oct 下で 15 dB 以上の減衰)を満たすための裁定逸脱とする。
31
+
32
+ denoise は `fft` なら `afftdn=nr=<12+strength*76>:nf=-30`、`nlm` なら
33
+ `anlmdn=s=<0.00001+strength*0.0002>` とする。rubberband は `speed != 1` または
34
+ `pitch_semitones != 0` のときだけ作る。全キーが既定なら入力もフィルタも追加せず、従来の
35
+ filtergraph をバイト単位で維持する。
36
+
37
+ fade、envelope、duck のクリップ窓は speed 適用後の実効尺を使う。BGM は素材先頭へ戻る既存の
38
+ ループ意味論を保ち、ループした入力に同じ clip FX を適用してタイムライン尺へ切る。
39
+
40
+ ## 3. プレビューサイドカー
41
+
42
+ recipe は `preview-audio-flac-v2`。対象クリップの `[in,out)` を `atrim` し、上記と同じ
43
+ clip-FX フィルタ生成関数で処理して 48 kHz FLAC へ焼く。cache key は source の絶対パス・size・
44
+ mtime・in/out・pad・recipe に加え、`atrim` を含む完全なフィルタ列を含む。同一入力・同一列は
45
+ 同じ FLAC を再利用し、列が変われば別 key にする。掃除は従来どおり keep key 以外の FLAC と
46
+ 旧 `speech-atempo/*.wav` を除く。
47
+
48
+ サイドカー化された sfx / bgm / narration は Web Audio で `playbackRate = 1` とし、FLAC の実尺を
49
+ 予定表の実効尺にする。生成失敗時はプレビューを停止せず元ファイルへ退避し、書き出しとの近似が
50
+ 崩れる旨を warning 1 行で報告する。
51
+
52
+ ## 4. provenance と残す近似
53
+
54
+ receipt の `provenance.audio.clip_fx` は `processed_items` と `filters` の件数を持つ。
55
+ プレビューと書き出しで意図的に残す近似は、サイドカー FLAC の再圧縮とデコーダ/サンプル境界の
56
+ 差だけとする。
@@ -0,0 +1,91 @@
1
+ # 音声エンベロープ・カーネル v1
2
+
3
+ - 日付: 2026-09-02
4
+ - 状態: 実装契約
5
+ - 対象: legacy `audio.*`、v2 audio item、Web Audio プレビュー、render-cut
6
+
7
+ ## 1. 共通 primitive
8
+
9
+ `EnvelopePoint` は `{ t, gainDb, easing? }` とし、`t` はクリップ先頭を 0 とする秒、`gainDb` は
10
+ 基準ゲインへ加える dB である。点の前後は端点値を保持し、点間は overlay keyframe と同じ easing
11
+ 係数で dB 補間する。`hold` は前値保持、既定は `linear` とする。
12
+
13
+ エンベロープ同士は dB 加算する。非線形区間は最大 20 ms 間隔の折線へ展開する。Web Audio では
14
+ dB 線形区間を `exponentialRampToValueAtTime` へ変換し、線形ゲインの下限を `1e-4`(-80 dB)とする。
15
+ 書き出しでは同じ評価関数を 48 kHz の mono f32le に標本化する。
16
+
17
+ ## 2. 音量キーフレーム
18
+
19
+ | 形式 | 宣言 | 時刻 | 範囲 |
20
+ |---|---|---|---|
21
+ | v2 audio item | `keyframes[].gain_db` | item 相対の整数フレーム | `[-60, 12]` dB |
22
+ | legacy bgm / sfx / narration | `keyframes[].gain_db` | クリップ相対秒 | `[-60, 12]` dB |
23
+
24
+ v2 の visual 用キーは audio item では無視し、lint warning を出す。legacy view は v2 のフレームを
25
+ `output.fps` で秒へ変換する。キーフレーム値はクリップの `gain_db` に加算し、fade と ducking とは
26
+ 独立に線形領域で乗算する。
27
+
28
+ ## 3. ダッキング
29
+
30
+ | キー | 型・範囲 | 既定 | 意味 |
31
+ |---|---|---|---|
32
+ | `ducking` | boolean | `false` | bgm / sfx を対象にする |
33
+ | `duck_db` | `[-40, 0]` dB | `-12` | 減衰量 |
34
+ | `duck_attack` | `[0, 2]` 秒 | `0.3` | 鍵開始前の下降時間 |
35
+ | `duck_release` | `[0, 5]` 秒 | `0.8` | 鍵終了後の復帰時間 |
36
+ | `audio.duck_keys` | `narration` / `speech` の配列 | 両方 | 鍵の選択 |
37
+
38
+ 2026-09-02 のオーナー実機フィードバック「切り替わりが急」を受け、既定の attack / release をよりなだらかに変更した。
39
+
40
+ `narration` 鍵は配置時刻とデコード/probe 実尺から作る。`speech` 鍵はプロジェクト直下の
41
+ `analysis.json` にある source 秒の transcript を、cut の in / out / speed と timeline map で写像する。
42
+ source は analysis.json の所在ディレクトリ基準で正規化し、`sources[].path` と一致する cut だけを使う。
43
+ 350 ms 未満の発話間隔は結合し、150 ms 未満の孤立区間は捨てる。analysis 不在・空・source 不一致は
44
+ 空の鍵と warning 1 行へ劣化する。
45
+
46
+ 鍵区間の隙間が `attack + release` 未満なら結合する。各区間 `[s,e)` は `s-attack` の 0 dB から
47
+ `s` の `duck_db` へ下降し、`e` まで保持して `e+release` で 0 dB に戻る。負時刻は 0 に固定し、
48
+ 対象クリップへ相対化して範囲外を切り詰める。実効ゲインは
49
+ `gain_db + keyframes(t) + duck(t)` を線形化した値に fade を掛けたものとする。
50
+
51
+ ## 4. プレビュー
52
+
53
+ `audio-schedule` は全音声 kind に `envelopeEvents` を出す。frame-engine はイベントがある場合だけ
54
+ 第 2 GainNode を作り、`setValueAtTime` または `exponentialRampToValueAtTime` で適用する。
55
+ `duckIntervals` は UI 表示互換のため残す。
56
+
57
+ 現版では preview-server へ speech interval を供給する配線はスコープ外である。入力が無い場合は
58
+ narration 鍵だけで動作し、共通カーネル自体と render-cut の speech 写像を正とする。
59
+
60
+ ## 5. 書き出し
61
+
62
+ `sidechaincompress` と narration 分岐用 `asplit` は使用しない。keyframe または実際の duck 区間を持つ
63
+ 対象だけに `env-<label>.f32` を作り、48 kHz mono f32le 入力として読む。対象音声は `amultiply` の直前に
64
+ `aformat=sample_fmts=fltp:sample_rates=48000:channel_layouts=stereo` で stereo/fltp に揃え、mono 素材には
65
+ 既定 rematrix の mono→stereo 係数を適用する。env は
66
+ `aformat=sample_fmts=fltp:sample_rates=48000,pan=stereo|c0=c0|c1=c0` で左右へ単位ゲインの等倍複製を行う。
67
+ これにより `amultiply` は素材のチャンネル数によらず常に stereo×stereo になる。挿入位置は `volume` と
68
+ `afade` の後、`adelay` の前とする。全区間 0 dB なら envelope 入力を作らず、従来 filtergraph を維持する。
69
+
70
+ 音声クリップ FX v1 を持つ入力では、チェーンを `atrim` → `highpass` → `afftdn` / `anlmdn` →
71
+ `rubberband` → `volume` → `afade` → envelope `amultiply` → `adelay` の順に固定する。したがって
72
+ clip FX は envelope の前段に入り、speed 適用後の実効尺を envelope と duck のクリップ窓に使う。
73
+
74
+ run / receipt の provenance は `audio.envelope` に `duck_keys`、`speech_intervals`、`ducked_items`、
75
+ `keyframed_items` を記録する。plan は配列そのものを保持せず、path と点数だけを JSON 化する。
76
+
77
+ ## 6. 検証と互換
78
+
79
+ schema と reader は型・範囲・時刻順を error にする。lint は実効尺超過、narration の
80
+ `ducking:true`、v2 audio keyframe の visual キーを warning にする。legacy の
81
+ `STATIC_DUCK_GAIN_DB`、`computeDuckIntervals`、`isWithinDuckInterval` は既存 shell 消費者の移行まで
82
+ 互換面として残す。
83
+
84
+ | 処理 | プレビュー | 書き出し |
85
+ |---|---|---|
86
+ | keyframe / duck 補間 | 共通 dB envelope → exponential automation | 共通 dB envelope → f32le → `amultiply` |
87
+ | fade | base GainNode | `afade` |
88
+ | 鍵実尺 | decode 実尺 | ffprobe 実尺 |
89
+ | 許容する差 | デコーダとサンプル境界 | デコーダとサンプル境界 |
90
+
91
+ afftdn、loudnorm、true peak guard は最終マスターだけの責務であり、プレビューには実装しない。
@@ -0,0 +1,110 @@
1
+ # 音声素材の挿入時レベル契約 v1
2
+
3
+ - 日付: 2026-09-02
4
+ - 状態: 実装済み(S1)
5
+ - 前提: `contract-2026-07-14-edit-json-v1-audio.md`、
6
+ `contract-2026-07-20-edit-json-v1-narration.md`、
7
+ `contract-2026-08-28-v2-audio-roles-v0.md`
8
+ - スコープ: 音声素材単体の決定論的なレベル計測、役割別挿入値の計算、
9
+ `akari media audio-level` による dry-run と edit.json への明示値保存
10
+
11
+ ## 1. 原則
12
+
13
+ 自動レベル合わせは再生中の追従処理ではない。素材を挿入するときに測定し、同じ入力から同じ
14
+ `gain_db` と既定 fade を導出して edit.json に数値として残す。以後のプレビューと書き出しは
15
+ その宣言値を消費する。素材カタログの既存ラウドネス値は使わず、外部素材と同じ実体計測を行う。
16
+
17
+ ## 2. 計測契約
18
+
19
+ 計測器の metric は `akari-audio-measure-v1` とし、次を返す。
20
+
21
+ | フィールド | 単位 | 定義 |
22
+ |---|---:|---|
23
+ | `integrated_lufs` | LUFS | EBU R128 Summary の I。-70.0 LUFS 以下または解析不能は `null` |
24
+ | `loudness_range_lu` | LU | EBU R128 Summary の LRA。I が無効なら `null` 可 |
25
+ | `true_peak_dbtp` | dBTP | EBU R128 Summary の Peak。無音の `-inf` は `null` |
26
+ | `sample_peak_dbfs` | dBFS | astats Overall の Peak level。無音の `-inf` は `null` |
27
+ | `rms_dbfs` | dBFS | astats Overall の RMS level。無音の `-inf` は `null` |
28
+ | `duration_sec` | 秒 | ffprobe format duration |
29
+ | `sample_rate` | Hz | ffprobe の先頭 audio stream |
30
+ | `channels` | — | ffprobe の先頭 audio stream |
31
+
32
+ ffmpeg は音声ごとに 1 パスだけ実行し、次の引数を使う。
33
+
34
+ ```text
35
+ -vn -sn -dn -af ebur128=peak=true:framelog=verbose,astats=measure_perchannel=none:measure_overall=Peak_level+RMS_level -f null -
36
+ ```
37
+
38
+ duration / sample rate / channels は ffprobe で決定論的に取得する。パーサは最後の ebur128
39
+ `Summary:` と astats `Overall` を読み、映像・字幕・data stream は計測対象にしない。
40
+
41
+ ### 2.1 キャッシュ
42
+
43
+ 素材の realpath、byte size、`mtimeMs`、metric を `|` で連結し、
44
+ `sha1(realpath|size|mtimeMs|metric)` を key とする。保存先は
45
+ `<cacheDir>/<key>.json`。`akari media audio-level` の cacheDir は
46
+ `<projectRoot>/.akari/cache/audio-measure/` である。単体の `akari-audio-measure` CLI も素材の
47
+ 親から上へ辿って最初に `.akari` ディレクトリを持つ projectRoot を使い、見つからない場合だけ
48
+ 素材の親を projectRoot とみなす。`--no-cache` または `useCache: false` は
49
+ 必ず再計測し、同じ key の JSON を上書きする。
50
+
51
+ ## 3. 役割別の目標
52
+
53
+ | role | integrated target (LUFS) | fade in (s) | fade out (s) |
54
+ |---|---:|---:|---:|
55
+ | narration | -16 | 0 | 0 |
56
+ | sfx | -18 | 0 | 0 |
57
+ | jingle | -18 | 0 | 0.3 |
58
+ | music | -20 | 0.2 | 1.0 |
59
+ | ambience | -26 | 0.5 | 0.5 |
60
+ | bgm | -26 | 0 | 0 |
61
+
62
+ true peak ceiling は -1.0 dBTP、短尺境界は 1.0 秒、短尺 sample peak target は -3.0 dBFS。
63
+ 未知 role は目標と fade の両方で sfx として扱う。
64
+
65
+ ## 4. 挿入値の式
66
+
67
+ `computeInsertLevel` は次の順序で 1 回だけ値を決める。
68
+
69
+ 1. 計測値が無ければ `basis: none`、`gain_db: 0` と役割既定 fade を返す。
70
+ 2. `duration_sec < 1.0` または `integrated_lufs == null` なら `basis: peak` とし、
71
+ `gain = -3.0 - sample_peak_dbfs`。sample peak も無ければ 1 と同じ。
72
+ 3. それ以外は `basis: lufs` とし、`gain = role target - integrated_lufs`。
73
+ 4. true peak があれば `gain = min(gain, ceilingDbtp - true_peak_dbtp)` とする。
74
+ 実際に gain を下げた場合だけ `peak_guard_applied: true`。
75
+ 5. gain を `[-60, 12]` へクランプした後、`Math.round(x * 10) / 10` で 0.1 dB に丸める。
76
+ `-0` は `0` に正規化する。
77
+
78
+ `detail` は採用 target、計測値、peak guard 適用有無、クランプ有無を保持する。
79
+
80
+ ## 5. 役割判定
81
+
82
+ v2 の明示 `role` が narration / bgm / jingle / music / ambience なら最優先し、そのまま使う。
83
+ それ以外(sfx、未知、未指定)は legacy collection の bgm / narration を優先した後、SFX
84
+ ヒューリスティクスへ進む。パスを小文字化し、`jingle` / `sting` を含めば jingle、
85
+ `ambien` / `room` / `env` を含めば ambience、計測尺が 20 秒以上なら music、それ以外は sfx とする。
86
+
87
+ ## 6. CLI
88
+
89
+ ```text
90
+ akari media audio-level <projectDir> [--write] [--targets '<json>'] [--ceiling <dBTP>] [--json] [--no-cache]
91
+ ```
92
+
93
+ v2 の audio lane items と legacy `audio.bgm` / `audio.sfx[]` / `audio.narration[]` を読み、
94
+ `gain_db` 未指定の項目だけを対象とする。素材パスは edit.json の親ディレクトリ基準。
95
+ dry-run の標準出力は header と 1 クリップ 1 行の表で、path / role / basis / I / TP /
96
+ `gain_db` / `fade_in` / `fade_out` を含む。`--json` は同じ結果の JSON 配列 1 つだけを返す。
97
+ 素材不在または計測不能は stderr の warning 1 行でその項目だけを省く。対象 0 件は exit 0。
98
+
99
+ `--write` は `gain_db` と、未指定の `fade_in` / `fade_out` だけを書く。v2 は Project API の
100
+ item patch、legacy は edit-store の既存書き込み API を使う。保存後に edit-lint を実行し、
101
+ severity `error` が 1 件でもあれば退避した edit.json 全文を戻す。成功後の再実行は対象 0 件で
102
+ あり、冪等である。
103
+
104
+ legacy 形(version 0/1)は現行 edit-lint が「古い形式」として error にするため、`--write` は
105
+ 常に巻き戻される(dry-run / `--json` は利用できる)。書き込む場合は `akari migrate` で v2 にしてから実行する。
106
+
107
+ ## 7. 次段
108
+
109
+ 設定(`audio.level_targets` / アプリ設定)と shell 挿入フックは S2 / 別票とする。
110
+ 本契約では schema、設定画面、実行時追従、shell UI を追加しない。