akari-video 0.1.40 → 0.1.42

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 (48) hide show
  1. package/package.json +1 -1
  2. package/src/self-update.mjs +16 -2
  3. package/vendor/.akari-capability-sources.json +3 -5
  4. package/vendor/docs/contract-2026-07-13-asset-library.md +2 -3
  5. package/vendor/docs/contract-2026-07-22-render-basics.md +2 -0
  6. package/vendor/docs/contract-2026-09-03-clip-adjust-v0.md +167 -0
  7. package/vendor/docs/contract-2026-09-03-preview-playback-rate-v1.md +52 -0
  8. package/vendor/docs/contract-2026-09-05-clip-adjust-v1.md +102 -0
  9. package/vendor/packages/akari-launcher/package.json +1 -1
  10. package/vendor/packages/audio-library-setup/shared/bgm-suggest.mjs +2 -2
  11. package/vendor/packages/edit-lint/src/edit-lint.mjs +162 -0
  12. package/vendor/packages/edit-store/lib/adjust-css-approx.d.ts +2 -0
  13. package/vendor/packages/edit-store/lib/adjust-css-approx.js +31 -0
  14. package/vendor/packages/edit-store/lib/adjust-css-visual.d.ts +38 -0
  15. package/vendor/packages/edit-store/lib/adjust-css-visual.js +67 -0
  16. package/vendor/packages/edit-store/lib/edit-v2.d.ts +63 -0
  17. package/vendor/packages/edit-store/lib/edit-v2.js +78 -1
  18. package/vendor/packages/edit-store/lib/generated/edit-v2-keys.d.ts +8 -8
  19. package/vendor/packages/edit-store/lib/generated/edit-v2-keys.js +8 -1
  20. package/vendor/packages/edit-store/lib/index.d.ts +2 -0
  21. package/vendor/packages/edit-store/lib/index.js +3 -0
  22. package/vendor/packages/edit-store/lib/internal-model.js +1 -0
  23. package/vendor/packages/edit-store/lib/webview-kernel.d.ts +1 -0
  24. package/vendor/packages/edit-store/lib/webview-kernel.js +131 -0
  25. package/vendor/packages/media-bin/src/preview-audio-sidecar.mjs +12 -3
  26. package/vendor/packages/schemas/bin/validate-edit.mjs +118 -0
  27. package/vendor/packages/schemas/edit.schema.json +321 -0
  28. package/vendor/packages/schemas/engine-capabilities.json +2 -0
  29. package/vendor/packages/schemas/examples/edit-v2-adjust-lut-empty-invalid/edit.json +16 -0
  30. package/vendor/packages/schemas/examples/edit-v2-adjust-range-invalid/edit.json +16 -0
  31. package/vendor/packages/schemas/examples/edit-v2-adjust-unknown-key-invalid/edit.json +16 -0
  32. package/vendor/packages/schemas/examples/edit-v2-adjust-v1-hue-empty-invalid/edit.json +95 -0
  33. package/vendor/packages/schemas/examples/edit-v2-adjust-v1-order-invalid/edit.json +100 -0
  34. package/vendor/packages/schemas/examples/edit-v2-adjust-v1-unknown-invalid/edit.json +101 -0
  35. package/vendor/packages/schemas/examples/edit-v2-adjust-v1-valid/edit.json +100 -0
  36. package/vendor/packages/schemas/examples/edit-v2-adjust-v1-wheels-invalid/edit.json +100 -0
  37. package/vendor/packages/schemas/examples/edit-v2-adjust-valid/edit.json +32 -0
  38. package/vendor/packages/schemas/test/adjust-v1.test.mjs +47 -0
  39. package/vendor/packages/schemas/test/edit-v2-schema.test.mjs +20 -0
  40. package/vendor/packages/schemas/test/engine-capabilities.test.mjs +2 -2
  41. package/vendor/packages/schemas/test/fixtures/adjust-v1-cases.mjs +37 -0
  42. package/vendor/packages/schemas/test/validate-edit.test.mjs +18 -0
  43. package/vendor/skills/edit-plan/expression-selection.md +9 -17
  44. package/vendor/packages/bake-layer/README.md +0 -73
  45. package/vendor/packages/bake-layer/package.json +0 -23
  46. package/vendor/packages/template-render/README.ja.md +0 -113
  47. package/vendor/packages/template-render/README.md +0 -113
  48. package/vendor/packages/template-render/package.json +0 -29
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "akari-video",
3
- "version": "0.1.40",
3
+ "version": "0.1.42",
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": {
@@ -8,7 +8,7 @@ import { resolveAkariHome } from './update-check.mjs';
8
8
 
9
9
  /**
10
10
  * `akari update` の実適用(update-and-versioning 契約(内部リポ)§11):
11
- * フィードの `components.app`(install.sh が入れるフル構成ソース tarball)を
11
+ * フィードの `components.app`(install.sh と共通のソース tarball、既定は CLI + プレビュー)を
12
12
  * DL → sha256 検証 → `~/.akari/staging/<version>/` へ展開 →(検証完了までスワップ開始しない)
13
13
  * → `~/.akari/app` と rename ベースで入れ替え → 旧版は `~/.akari/app-previous/` に 1 世代保持。
14
14
  *
@@ -106,8 +106,22 @@ function extractTarGz({ tarPath, destDir }) {
106
106
  }
107
107
  }
108
108
 
109
- /** install.sh `update_install()` と同じ意味論: 適用後に npm install で node_modules を整合させる。 */
109
+ /** 展開先だけを CLI 用に絞る。明示的なシェル opt-in 時は配布元の設定を保持する。 */
110
+ export function applyCliOnlyWorkspaces({ cwd }) {
111
+ if (process.env.AKARI_INSTALL_SHELL === '1') return;
112
+ const packagePath = join(cwd, 'package.json');
113
+ const pkg = JSON.parse(readFileSync(packagePath, 'utf8'));
114
+ pkg.workspaces = ['packages/*'];
115
+ writeFileSync(packagePath, JSON.stringify(pkg, null, 2) + '\n');
116
+ }
117
+
118
+ /** install.sh と同じ意味論: スワップ後に CLI 用 workspaces を適用して npm install する。 */
110
119
  function defaultRunNpmInstall({ cwd }) {
120
+ try {
121
+ applyCliOnlyWorkspaces({ cwd });
122
+ } catch (error) {
123
+ return { ok: false, message: error.message };
124
+ }
111
125
  const result = spawnSync('npm', ['install', '--no-audit', '--no-fund', '--loglevel=error'], { cwd, stdio: 'pipe' });
112
126
  if (result.error) {
113
127
  return { ok: false, message: result.error.message };
@@ -61,6 +61,9 @@
61
61
  "docs/contract-2026-09-02-shape-item-v0.md",
62
62
  "docs/contract-2026-09-02-transcript-unrecognized-spans-v0.md",
63
63
  "docs/contract-2026-09-02-word-book-v0.md",
64
+ "docs/contract-2026-09-03-clip-adjust-v0.md",
65
+ "docs/contract-2026-09-03-preview-playback-rate-v1.md",
66
+ "docs/contract-2026-09-05-clip-adjust-v1.md",
64
67
  "packages/akari-launcher/package.json",
65
68
  "packages/akari-launcher/README.md",
66
69
  "packages/akari-tools/package.json",
@@ -71,8 +74,6 @@
71
74
  "packages/asset-resolver/README.md",
72
75
  "packages/audio-library-setup/package.json",
73
76
  "packages/audio-library-setup/README.md",
74
- "packages/bake-layer/package.json",
75
- "packages/bake-layer/README.md",
76
77
  "packages/chat-bridge/package.json",
77
78
  "packages/creator-root/package.json",
78
79
  "packages/decision-cards/package.json",
@@ -108,9 +109,6 @@
108
109
  "packages/render-cut/README.ja.md",
109
110
  "packages/render-cut/README.md",
110
111
  "packages/schemas/package.json",
111
- "packages/template-render/package.json",
112
- "packages/template-render/README.ja.md",
113
- "packages/template-render/README.md",
114
112
  "packages/word-book/package.json",
115
113
  "skills/address-review/SKILL.md",
116
114
  "skills/analyze-footage/analysis-json.md",
@@ -78,7 +78,7 @@ assets/ ← 当面はローカルディレクトリ。コミ
78
78
  値へ付く単位だけを書き、**無単位の倍率・比率(短辺比など)では `unit` を省略する**。
79
79
  意味は `label` に書く(例「木枠の太さ(短辺比。0 で枠なし)」)。この規律により、
80
80
  ツールが `--var board-width=940` を `940px` へ、比率のツマミは数値のまま渡せる
81
- (`packages/template-render`)。ここを混ぜると `width: 940` という無効な CSS が生まれ、
81
+ (HTML 素材のパラメータ展開)。ここを混ぜると `width: 940` という無効な CSS が生まれ、
82
82
  **絵は変わるが「効いた」のではなく「壊れた」**という誤検知が検査側にも起きる
83
83
  - `knobs.type` の語彙: `text` / `color` / `slider` / `dropdown` / `checkbox` / `media`
84
84
  (.mogrt と同じ心的モデル。世界中のモーションデザイナーが既に知っている語彙)
@@ -400,8 +400,7 @@ catalog に載せる素材は、取得元のライセンスが CC0 相当(帰
400
400
 
401
401
  現在の収録:
402
402
 
403
- - `presets/telop/` — ATF テロップテンプレート 36 件。`packages/bake-layer` が
404
- `--preset <id>` から `presets/telop/<id>/template.json` を解決する
403
+ - ATF テロップの参照表と描画器は退役。HTML 素材版は Lab で取得する。既存の baked は再生互換を維持する。
405
404
  - `presets/luts/` — 3D LUT 2 件(自前生成)。`packages/render-cut/src/plan.mjs` が
406
405
  `edit.json` の `output.look.lut`(区切り文字を含まない名前)から
407
406
  `presets/luts/<id>/<id>.cube` を解決する
@@ -27,6 +27,8 @@
27
27
 
28
28
  - 除外(次段送り): ブレンドモード・PinP・プリレンダ合成レール(レイヤー機構が前提のため)
29
29
 
30
+ 現況(2026-09-05): ffmpeg `lut3d` 経路は撤去済み。`output.look` は frame-engine の最終パス(WebGL2 LUT)で gpu / osr の 2 出口が適用し、per-clip の色補正は `docs/contract-2026-09-03-clip-adjust-v0.md` §4 の順(item adjust → 合成 → look)で同じエンジンが適用する。
31
+
30
32
  ## 2. 横断要件
31
33
 
32
34
  1. schema は**追記のみ**(既存 edit.json が全て無変更で valid のまま)。validate-edit /
@@ -0,0 +1,167 @@
1
+ > v1(curves / wheels / hue)で拡張。正本は [contract-2026-09-05-clip-adjust-v1.md](contract-2026-09-05-clip-adjust-v1.md)。
2
+ # edit.json v2 clip adjust v0 契約
3
+
4
+ - 日付: 2026-09-03
5
+ - lifecycle: accepted
6
+ - 位置づけ: v2 visual item 共通の per-clip カラー補正語彙と、その段階導入契約
7
+
8
+ ## 0. 位置づけ
9
+
10
+ `adjust` は、1 個の visual item に基本補正と 3D LUT を適用する任意フィールドである。値の保存、
11
+ 検証、プレビュー、GPU / OSR 書き出しが同じ語彙と同じ演算順を使う。v0 は基本補正 10 項目と
12
+ 単一 LUT に限定し、カーブやホイールを混ぜない。
13
+
14
+ 本契約の導入は段階的に行う。M1 では schema、edit-store、validator、lint、能力台帳へ席を作る。
15
+ エンジン消費が入るまでは、正しく保存できる値であっても GPU / OSR の能力台帳では `ignored`、
16
+ `runtime_warning: true` とし、edit-lint は `engine.unsupported-field` error を返す。未実装を黙って
17
+ 描画したように扱わない。
18
+
19
+ ## 1. 席
20
+
21
+ 席は `edit.json` version 2 の visual lane にある `tracks[].items[].adjust` である。現行 schema の
22
+ 本契約対象の visual item(media / html / telop / filter / group / captions / caption)に共通し、audio lane
23
+ の item には置かない。
24
+
25
+ ```jsonc
26
+ {
27
+ "version": 2,
28
+ "tracks": [{
29
+ "id": "visual",
30
+ "lane": "visual",
31
+ "items": [{
32
+ "id": "clip-1",
33
+ "at": 0,
34
+ "duration": 90,
35
+ "adjust": {
36
+ "basic": { "exposure": 0.35, "temperature": -0.1, "saturation": 0.15 },
37
+ "lut": { "lut": "cinematic-warm", "intensity": 0.8 },
38
+ "sections": { "basic": true, "lut": true }
39
+ },
40
+ "source": { "kind": "media", "src": "main", "in": 0, "out": 3 }
41
+ }]
42
+ }]
43
+ }
44
+ ```
45
+
46
+ version 0 / 1 の `cuts[]` と `layers[]` には `adjust` の席を追加しない。
47
+
48
+ ## 2. 語彙
49
+
50
+ `adjust` は追加キーを許さない object で、全フィールドが任意である。
51
+
52
+ | field | 型 | 範囲・既定 |
53
+ |---|---|---|
54
+ | `basic` | object | 追加キー不可。各値の省略は `0`(中立) |
55
+ | `lut` | `null` または object | `null` / 省略は LUT 無し |
56
+ | `sections` | object | `basic?` / `lut?` の boolean だけを持つ疎辞書 |
57
+
58
+ `basic` の 10 項目は次のとおりである。
59
+
60
+ | field | 範囲 | 単位・中立 |
61
+ |---|---:|---|
62
+ | `exposure` | `-3..3` | EV、`0` |
63
+ | `contrast` | `-1..1` | `0` |
64
+ | `highlights` | `-1..1` | `0` |
65
+ | `shadows` | `-1..1` | `0` |
66
+ | `blacks` | `-1..1` | `0` |
67
+ | `whites` | `-1..1` | `0` |
68
+ | `temperature` | `-1..1` | `0` |
69
+ | `tint` | `-1..1` | `0` |
70
+ | `vibrance` | `-1..1` | `0` |
71
+ | `saturation` | `-1..1` | `0` |
72
+
73
+ `lut` object は追加キー不可で、空でない文字列 `lut` を必須とし、任意の `intensity` は `0..1`、
74
+ 省略時 `1` とする。
75
+
76
+ ## 3. 基本補正の数値契約
77
+
78
+ 演算は video-space、すなわち gamma 符号化 sRGB 値のまま行う。scene-linear へ変換しない。
79
+ 入力チャンネルを `c = (r, g, b)`、`clamp01(x) = min(1, max(0, x))`、Rec.709 luma を
80
+ `Y(c) = 0.2126r + 0.7152g + 0.0722b` とする。`smoothstep(a,b,x)` は
81
+ `t = clamp01((x-a)/(b-a))`、`t²(3-2t)` である。
82
+
83
+ 演算順は固定で、次の順に進める。
84
+
85
+ 1. exposure: `c *= 2^exposure`。
86
+ 2. white balance: `r *= 1 + temperature*0.18`、`b *= 1 - temperature*0.18`、
87
+ `g *= 1 - tint*0.12` とし、各チャンネルを `clamp01` する。
88
+ 3. tone zones: highlights → shadows → whites → blacks の順に処理する。各有効ステップの直前に
89
+ 現在の `c` から luma を再計算し、処理後は各チャンネルを `clamp01` する。
90
+ - highlights: `c *= 1 + highlights*smoothstep(0.5,0.9,Y(c))`
91
+ - shadows: `c *= 1 + shadows*(1-smoothstep(0.1,0.5,Y(c)))`
92
+ - whites: `c += whites*smoothstep(0.7,1.0,Y(c))*0.3`
93
+ - blacks: `c += blacks*(1-smoothstep(0.0,0.3,Y(c)))*0.3`
94
+ 4. contrast: 各チャンネルを `(c-0.5)*(1+contrast)+0.5` とし、`clamp01` する。pivot は `0.5`。
95
+ 5. saturation: 現在の Rec.709 luma を使い、各チャンネルを
96
+ `Y(c) + (c-Y(c))*(1+saturation)` として `clamp01` する。
97
+ 6. vibrance: 現在値の `max` と `min` から
98
+ `S = max > 1e-6 ? (max-min)/max : 0`、`amount = vibrance*(1-S)` を求め、各チャンネルを
99
+ `Y(c) + (c-Y(c))*(1+amount)` として `clamp01` する。
100
+ 7. 外部 LUT: `lut` が有効なら trilinear sampler で `c` を置換し、`intensity` で identity 入力と
101
+ LUT 出力を線形混合する。
102
+
103
+ 定数の正本値は `REC709 = (0.2126, 0.7152, 0.0722)`、`TEMP_COEF = 0.18`、
104
+ `TINT_COEF = 0.12`、`CONTRAST_PIVOT = 0.5` である。基本補正を LUT に bake する消費者は
105
+ `LUT_3D_SIZE 33`、R fastest → G → B、各成分小数 6 桁とし、適用時は trilinear 補間する。
106
+
107
+ ## 4. 解決順とバイパス
108
+
109
+ 処理順は次で固定する。
110
+
111
+ 1. item の `adjust.basic`
112
+ 2. 同じ item の `adjust.lut`
113
+ 3. timeline 上の item 合成
114
+ 4. `output.look`(全体へ掛けるグローバル LUT)
115
+
116
+ `adjust.lut.lut` の参照解決は `output.look.lut` と同じである。値にスラッシュが無ければ
117
+ `presets/luts/<id>/<id>.cube`、スラッシュがあればプロジェクトルート相対パスとして解決する。
118
+ パス区切りは Windows と POSIX の双方を扱う。
119
+
120
+ `sections` は疎辞書であり、`sections.basic === false` のときだけ基本補正全体を、
121
+ `sections.lut === false` のときだけ item LUT をバイパスする。キー省略は有効を意味する。
122
+ **OFF はプレビューと書き出しの両方で必ずバイパスする。** 一方だけで OFF を無視したり、
123
+ プレビューと書き出しで結果をずらしたりしてはならない。
124
+
125
+ ## 5. 消費側の約束
126
+
127
+ 最終消費者は GPU export と OSR export の 2 出口である。両者は同じ `adjust` 値、33³ bake、
128
+ LUT 参照解決、適用順、sections バイパスを使い、受領情報にも item adjust の適用状況を残す。
129
+
130
+ プレビューには frame-engine WebGL2 レールと DOM fallback レールがある。WebGL2 は item / quad 単位の
131
+ LUT として適用し、DOM fallback は表現可能な基本補正を CSS 近似し、表現できない項目や LUT を
132
+ 黙って同等と称さない。どちらのレールでも `sections.* === false` を先に評価する。frame-engine active
133
+ 時は DOM fallback と二重適用しない。
134
+
135
+ M1 時点では edit-store が `adjust` を内部 item declaration へ損失なく射影するところまでであり、
136
+ GPU / OSR / frame-engine / DOM preview は未消費である。能力台帳は全 visual item 用途について
137
+ `gpu: ignored`、`osr: ignored`、`runtime_warning: true` とし、frame-engine の未知キー警告へ到達させる。
138
+ 消費実装を導入する便でのみ、実測と同時に `consumed` へ反転する。
139
+
140
+ ## 6. lint 分担
141
+
142
+ - JSON Schema は閉じた object、型、数値範囲、必須の `lut` 文字列、visual/audio の席を検査する。
143
+ - `validate-edit.mjs` は依存を増やさず、同じ構造制約を手書きで検査する。
144
+ - edit-store の v2 reader は未知キーを拒否し、型と範囲を検査して内部 declaration へ射影する。
145
+ - edit-lint は依存ゼロを守るため同じ構造検証を意図的に重複実装する。構造が正しい場合も、M1 の
146
+ 能力台帳に従い `--engine gpu|osr|auto` で `engine.unsupported-field` error を出す。
147
+
148
+ 構造エラーと未消費エラーを混同しない。構造が不正な文書は engine 能力判定へ進めない。
149
+
150
+ ## 7. 書き込み側・パネル側の約束
151
+
152
+ 書き込み側は version 2 の visual item だけを編集対象にする。version 0 / 1 を平坦な近似語彙へ
153
+ 書き換えない。`basic` が全て `0` または省略、`lut` が `null` または省略で、`sections` 以外に
154
+ 効果が無い identity 状態は `adjust` field 自体を削除する。schema は読み込み互換のため identity
155
+ object を拒否しないが、正規の保存形は field 省略である。
156
+
157
+ パネルで section を OFF にしても値は保持してよい。再度 ON にしたとき同じ値へ戻せる一方、消費側は
158
+ OFF 中の値を必ず無視する。UI が値を表示・更新できることと、エンジンが消費済みであることは別であり、
159
+ 未消費の間は lint error と runtime warning を隠さない。
160
+
161
+ ## 8. 非スコープ
162
+
163
+ - version 0 / 1 の `cuts[]` / `layers[]` への席追加
164
+ - RGB curves、color wheels / CDL、hue curves、effects(M2 の語彙)
165
+ - vignette(空間処理であり basic/LUT bake の対象外)
166
+ - 本便での compositor、page-builder、preview、inspector、preset カタログの実装
167
+ - 複数 LUT の `luts[]`、adjust layer の `layers[]` など v0 を越える構造
@@ -0,0 +1,52 @@
1
+ ---
2
+ lifecycle: accepted
3
+ created: 2026-09-03
4
+ updated: 2026-09-03
5
+ ---
6
+
7
+ # プレビュー再生速度・ピッチ保持契約 v1
8
+
9
+ ## 1. UI と状態
10
+
11
+ プレビューのトランスポート右側は pen → rate → zoom → fullscreen の順とする。rate ボタンは
12
+ アイコンではなく現在値を `0.5×` の形式で表示し、ポップアップから
13
+ `0.5 / 0.75 / 1 / 1.25 / 1.5 / 2 / 3` の 7 値を選ぶ。値域は 0.5 以上 3 以下である。
14
+ スライダーとキーボードショートカットは設けない。
15
+
16
+ 速度の正本は webview の `previewRate` とする。変更値は host の当該 preview widget にだけ保持し、
17
+ インスペクター編集による incremental 更新と webview 再構築を跨いで復元する。ディスク、Theia
18
+ preferences、edit.json には保存せず、widget を閉じた後の新しいプレビューは 1× から始める。
19
+ raw 素材プレビューにも同じ UI と速度を適用する。
20
+
21
+ ## 2. `rate` の意味
22
+
23
+ `previewRate` は「出力タイムライン秒 / 実時間秒」である。`playbackTick.rate` とレビューセッションの
24
+ `reviewTransport` に記録する `type: "rate"` の `value` は、どちらもこの値を送る。legacy の cut に
25
+ 宣言された `segment.speed` は素材秒と出力秒の写像であり、レビューの rate イベントには送らない。
26
+ 従来の segment speed 変更イベントは廃止する。
27
+
28
+ frame-engine の時計は経過実時間へ `previewRate` を掛ける。legacy の動画要素は
29
+ `segment.speed × previewRate` で再生し、gap と静止画は壁時計へ `previewRate` を掛ける。
30
+ freeze の実時間ホールドは宣言秒を `previewRate` で割る。速度変更時は現在位置に錨を打ち直し、
31
+ 再生ヘッドを飛ばさない。
32
+
33
+ ## 3. 音声とピッチ保持
34
+
35
+ frame-engine 経路では全音源を `PreviewAudioSupply` の master gain に集め、音源の予定時刻と
36
+ AudioContext 時計を `previewRate` で進める。legacy 経路でも previewAudio の BGM・SFX・ナレーションを
37
+ 同じ倍率で再予定する。legacy の動画要素(台詞を含む)は `preservesPitch = true` を明示する。
38
+
39
+ 1× は master gain から destination への直結である。1× 以外は共通の
40
+ `preview-audio-worklet.js` と `akari-pitch-shift` processor を使い、速度 r で再生した全音声へ
41
+ ratio `1 / r` のピッチ補正を掛ける。worklet の準備前は速度だけを先に反映し、準備完了後に経路へ
42
+ 差し込む。読み込み失敗または AudioWorklet 非対応時も再生を止めず、警告を 1 行出して素の速度へ
43
+ フォールバックする。
44
+
45
+ frame-engine の `debug()` は `rate`、`pitchPreserved`、`stretcher` を返す。`stretcher` は
46
+ `"worklet" | "none"`、`pitchPreserved` は 1× または worklet が実際の経路に入った場合だけ true とする。
47
+ `attachAnalyser()` は master bus 出口へ AnalyserNode を一つだけ接続し、検収用の tap として返す。
48
+
49
+ ## 4. 非目標
50
+
51
+ 本機能はプレビュー専用である。書き出しの速度・音声処理には影響せず、edit.json の内容も変えない。
52
+ 速度の preferences 永続化、キーボードショートカット、速度スライダーは本契約の対象外とする。
@@ -0,0 +1,102 @@
1
+ ---
2
+ lifecycle: accepted
3
+ date: 2026-09-05
4
+ ---
5
+
6
+ # edit.json v2 clip adjust v1 契約
7
+
8
+ ## 0. 位置づけ
9
+
10
+ v0 の基本補正と LUT に curves / wheels / hue を追加する。旧 src/lib/color-grade.ts の数式を正本値として移植する。
11
+
12
+ ## 1. 席
13
+
14
+ version 2 visual item の tracks[].items[].adjust。media / html / telop / filter / group / captions / caption の 7 定義共通。audio と shape、version 0 / 1 の cuts[] / layers[] には追加しない。
15
+
16
+ ## 2. 語彙仕様 v1
17
+
18
+ `$defs.adjustV1`(v0 の `basic` / `lut` / `sections` は不変。以下を追加。すべて任意・additionalProperties false):
19
+
20
+ - `curves`: object `{ master?, r?, g?, b? }`。各値は `[{ in: number 0..1, out: number 0..1 }]`(minItems 2・maxItems 16・
21
+ 各点 additionalProperties false・in / out とも required)。**`in` は狭義単調増加**(schema では表現できないので
22
+ validate-edit / edit-store / edit-lint で検査)。チャンネル identity = ちょうど 2 点 `[{0,0},{1,1}]`(許容 1e-5)。
23
+ - `wheels`: object `{ lift?, gamma?, gain?, offset? }`。各値は object `{ r?, g?, b? }`(number)。範囲:
24
+ lift ±0.25 / gamma ±0.5 / gain ±0.5 / offset ±0.1。省略 = 0(中立)。
25
+ - `hue`: object `{ hue?, sat?, luma? }`。各値は `[{ hue: number 0..1, value: number 0..1 }]`(minItems 1・maxItems 16・
26
+ additionalProperties false・両方 required)。**`hue` は狭義単調増加**。中立 value = 0.5。チャンネル identity =
27
+ 省略または全点 `|value-0.5| ≤ 1e-4`。
28
+ - `sections`: `basic` / `lut` に加えて `curves` / `wheels` / `hue`(boolean・false のときだけバイパス)。
29
+ - **演算順(固定)**: ① basic(v0 §3 の 1〜6)→ ② lut(trilinear・intensity 混合)→ ③ wheels → ④ curves → ⑤ hue。
30
+ - ③ wheels: チャンネルごとに `c = v*(1-lift)+lift` → `c = pow(max(0,c), 1/(1+gamma))` → `c *= 1+gain` → `c = clamp01(c+offset)`
31
+ (lift / gamma / gain / offset は当該チャンネルの値・省略 0)。
32
+ - ④ curves: 区分線形。点列を in 昇順に評価し、`x ≤ 最初の in` は最初の out、`x ≥ 最後の in` は最後の out、
33
+ それ以外は挟む 2 点で線形補間、各評価を clamp01。**master を 3 ch に適用してから r / g / b を各 ch に適用**。
34
+ - ⑤ hue: RGB→HSV(`d = max-min`、`d > 1e-4` のときだけ h を計算(`cmax===r: ((g-b)/d+6)%6`・`cmax===g: (b-r)/d+2`・
35
+ else `(r-g)/d+4`・÷6)、`s = cmax > 1e-4 ? d/cmax : 0`、`v = cmax`)→ `shift = (sample(hue, h)-0.5)*2`・
36
+ `h' = (h+shift+1) % 1`・`s' = clamp01(s * sample(sat, h)*2)`・`v' = clamp01(v * sample(luma, h)*2)` → HSV→RGB
37
+ (sector = floor(h'*6) の 6 分割・`c = s'v'`・`x = c(1-|((h'*6) mod 2)-1|)`・`m = v'-c`・各 clamp01)。
38
+ `sample(ch, h)` = 点列を hue 昇順で評価、0 点なら 0.5、1 点ならその value、範囲外は端点、間は線形。
39
+ **h の参照は 3 ch とも変換前の h**(旧実装どおり)。
40
+ - identity 判定(書き込み側の規範): basic 全 0・lut 無し・wheels 全 0・curves 全 ch identity・hue 全 ch identity なら
41
+ `adjust` field 自体を除去(v0 §7 と同じ)。
42
+ - v1(cuts[] / layers[] 文書)には席を作らない(v0 と同じ)。
43
+ - ルックプリセット(D13)は語彙ではない: `presets/looks/index.jsonl`(`{id, kind:"look", name, description, when_to_use}`)+
44
+ `presets/looks/<id>.json` = `{ "id": "<id>", "adjust": { "basic": {…}, "wheels": {…} } }`。`adjust` の中身は `$defs.adjustV1` に
45
+ 適合すること(schema テストで担保)。
46
+
47
+ 基本補正は exposure ±3 EV、contrast / highlights / shadows / blacks / whites / temperature / tint / vibrance / saturation は ±1。全て任意、既定 0。lut は null または {lut: 空でない文字列, intensity?: 0..1}、intensity 既定 1。各 object は追加キー不可。
48
+
49
+ ## 3. 基本補正の数値契約
50
+
51
+ 演算は video-space、すなわち gamma 符号化 sRGB 値のまま行う。scene-linear へ変換しない。
52
+ 入力チャンネルを `c = (r, g, b)`、`clamp01(x) = min(1, max(0, x))`、Rec.709 luma を
53
+ `Y(c) = 0.2126r + 0.7152g + 0.0722b` とする。`smoothstep(a,b,x)` は
54
+ `t = clamp01((x-a)/(b-a))`、`t²(3-2t)` である。
55
+
56
+ 演算順は固定で、次の順に進める。
57
+
58
+ 1. exposure: `c *= 2^exposure`。
59
+ 2. white balance: `r *= 1 + temperature*0.18`、`b *= 1 - temperature*0.18`、
60
+ `g *= 1 - tint*0.12` とし、各チャンネルを `clamp01` する。
61
+ 3. tone zones: highlights → shadows → whites → blacks の順に処理する。各有効ステップの直前に
62
+ 現在の `c` から luma を再計算し、処理後は各チャンネルを `clamp01` する。
63
+ - highlights: `c *= 1 + highlights*smoothstep(0.5,0.9,Y(c))`
64
+ - shadows: `c *= 1 + shadows*(1-smoothstep(0.1,0.5,Y(c)))`
65
+ - whites: `c += whites*smoothstep(0.7,1.0,Y(c))*0.3`
66
+ - blacks: `c += blacks*(1-smoothstep(0.0,0.3,Y(c)))*0.3`
67
+ 4. contrast: 各チャンネルを `(c-0.5)*(1+contrast)+0.5` とし、`clamp01` する。pivot は `0.5`。
68
+ 5. saturation: 現在の Rec.709 luma を使い、各チャンネルを
69
+ `Y(c) + (c-Y(c))*(1+saturation)` として `clamp01` する。
70
+ 6. vibrance: 現在値の `max` と `min` から
71
+ `S = max > 1e-6 ? (max-min)/max : 0`、`amount = vibrance*(1-S)` を求め、各チャンネルを
72
+ `Y(c) + (c-Y(c))*(1+amount)` として `clamp01` する。
73
+ 7. 外部 LUT: `lut` が有効なら trilinear sampler で `c` を置換し、`intensity` で identity 入力と
74
+ LUT 出力を線形混合する。
75
+
76
+ 定数の正本値は `REC709 = (0.2126, 0.7152, 0.0722)`、`TEMP_COEF = 0.18`、
77
+ `TINT_COEF = 0.12`、`CONTRAST_PIVOT = 0.5` である。基本補正を LUT に bake する消費者は
78
+ `LUT_3D_SIZE 33`、R fastest → G → B、各成分小数 6 桁とし、適用時は trilinear 補間する。
79
+
80
+ ## 4. 解決順とバイパス
81
+
82
+ basic → lut → wheels → curves → hue → item 合成 → output.look。LUT はスラッシュなしなら presets/luts/<id>/<id>.cube、ありならプロジェクト相対(Windows / POSIX の区切り対応)。sections は false のときだけ該当段をバイパスする。OFF はプレビューと書き出しの両方でバイパスし、保存値は保持してよい。
83
+
84
+ 33³、R fastest → G → B、各成分 toFixed(6) の後 Float32 に格納、適用時 trilinear。正規化は範囲 clamp、点列昇順ソート、省略既定の補完。検証器は不正な点列を修復せず拒否する。補間 span < 1e-9 では左点を使用。curves identity は旧実装どおり各差の絶対値 < 1e-5、hue identity は ≤ 1e-4。basic の演算上 identity 許容は 1e-6。
85
+
86
+ 注: hue value=1.0 は式 (value-0.5)*2 により +360°(一周)、+180° は value=0.75。旧実装の式を優先する。
87
+
88
+ ## 5. 消費側の約束
89
+
90
+ M2-1 時点で新 3 セクションはエンジン未消費(bake 関数は用意・plan 未配線)。2/4 便で配線。GPU / OSR と frame-engine preview は同じ全段 bake とバイパスを使う。DOM fallback は新 3 セクションを CSS 近似できないため適用せず、色調整は近似表示の指標を出す。frame-engine active 時は二重適用しない。台帳は adjust の path 単位であり本便では行を増やさない。
91
+
92
+ ## 6. lint 分担
93
+
94
+ Schema は閉じた構造・型・範囲・点数・必須キーを検査。validate-edit / edit-store / edit-lint はさらに in / hue の狭義単調増加を検査する。依存ゼロの検証器は意図的重複。edit-lint は adjust.curves.* / adjust.wheels.* / adjust.hue.* の check id と正確な path を返す。
95
+
96
+ ## 7. 書き込み側・パネル側の約束
97
+
98
+ 全段 identity なら adjust 自体を除去する。読み取り側は identity object を許す。セクション OFF は保存値を失わず戻せる。isItemAdjustIdentity は OFF を考慮する実効判定であり、書き込み側が OFF の値を削除する指示ではない。ルック適用は basic / wheels を丸ごと置換し、lut / curves / hue は保持する。
99
+
100
+ ## 8. 非スコープ
101
+
102
+ effects / vignette / スプリット比較、plan / compositor / page-builder / preview / inspector の配線、LUT ライブラリ。
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "akari-video",
3
- "version": "0.1.40",
3
+ "version": "0.1.42",
4
4
  "description": "AKARI Video launcher CLI — start an AI-edited video project from any directory: scaffold, connection check, then hand over to Claude Code (or opencode). AKARI Video を opencode や Claude Code で、どのディレクトリからでも始めるための `akari` ランチャー CLI。接続確認(doctor)→ 未セットアップならプロジェクト雛形を作成 → AI エージェントを起動する。外部 npm 依存ゼロ(Node.js 組み込みモジュールのみ)。 [akari-video npm vendor: bin/akari.mjs is reference-only. These CLI entrypoints are not included in the akari-video npm package. Use `akari doctor --json` and run the path reported in `render_cut.path`. Full installations provide it in a monorepo checkout, ~/.akari/app, /Applications/AKARI Video.app/Contents/Resources/packages, or %LOCALAPPDATA%\\Programs\\@akari-videoshell\\resources\\packages.]",
5
5
  "type": "module",
6
6
  "files": [
@@ -1,8 +1,8 @@
1
1
  // BGM 自動提案(v0)— intake / 演出の tone 語彙 × AKARI Sounds のトラック系統の突き合わせ。
2
2
  //
3
3
  // 設計方針:
4
- // - tone 語彙は表現選定(skills/edit-plan/expression-selection.md / presets/telop の tone)と
5
- // 同じ 8 語に固定する。BGM だけ別の語彙を発明しない
4
+ // - tone 語彙は表現選定(skills/edit-plan/expression-selection.md)と同じ 8 語に固定する。
5
+ // BGM だけ別の語彙を発明しない
6
6
  // - 対応表は「系統(トラック id の接頭辞)→ tone 重み」の宣言表。トラック個別の
7
7
  // when_to_use(宣言パック = 有償レイヤ)には依存しない — 無料の catalog.json(tags / id)
8
8
  // だけで動く
@@ -1086,6 +1086,9 @@ function validateEditV2(edit, findings) {
1086
1086
  });
1087
1087
  }
1088
1088
  }
1089
+ if (Object.hasOwn(item, "adjust")) {
1090
+ validateAdjust(item.adjust, findings, `${itemPath}.adjust`);
1091
+ }
1089
1092
  if (Array.isArray(item.items)) visit(item.items, item, `${itemPath}.items`);
1090
1093
  }
1091
1094
  };
@@ -1421,6 +1424,14 @@ async function validateV2ObjectTreeFiles(edit, findings, paths) {
1421
1424
 
1422
1425
  const referencedMotion = new Set();
1423
1426
  for (const { item, path: itemPath } of entries) {
1427
+ if (item.source?.kind === "telop" && item.source.baked === undefined) {
1428
+ addFinding(findings, {
1429
+ severity: "error",
1430
+ check: "telop.retired",
1431
+ message: "テロップ(ATF)の描画は退役しました。HTML 素材版のテロップに差し替えてください(Lab で配布)。すでに焼いた baked を持つ項目はそのまま再生できます。",
1432
+ path: `${itemPath}.source`,
1433
+ });
1434
+ }
1424
1435
  if (isRecord(item.keyframes) && isNonEmptyString(item.keyframes.path)) {
1425
1436
  referencedMotion.add(item.keyframes.path);
1426
1437
  const filePath = resolve(paths.projectRoot, item.keyframes.path);
@@ -1551,6 +1562,105 @@ function validateLook(value, findings, path) {
1551
1562
  }
1552
1563
  }
1553
1564
 
1565
+ // docs/contract-2026-09-03-clip-adjust-v0.md。edit-lint は依存ゼロを保つため、schema と
1566
+ // validate-edit.mjs の閉じた adjust 語彙をここでも独立に検証する。
1567
+ function validateAdjust(value, findings, path) {
1568
+ validateAdjustV1Sections(value, findings, path);
1569
+ if (!isRecord(value)) {
1570
+ addFinding(findings, {
1571
+ severity: "error", check: "adjust.structure", message: "adjust must be an object", path,
1572
+ });
1573
+ return;
1574
+ }
1575
+ const reportUnknownKeys = (record, allowed, ownerPath) => {
1576
+ for (const key of Object.keys(record)) {
1577
+ if (allowed.has(key)) continue;
1578
+ addFinding(findings, {
1579
+ severity: "error",
1580
+ check: "adjust.unknown-key",
1581
+ message: `${key} is not defined by clip adjust v1`,
1582
+ path: `${ownerPath}.${key}`,
1583
+ });
1584
+ }
1585
+ };
1586
+ reportUnknownKeys(value, new Set(["basic", "lut", "sections", "curves", "wheels", "hue"]), path);
1587
+
1588
+ if (Object.hasOwn(value, "basic")) {
1589
+ const basicPath = `${path}.basic`;
1590
+ if (!isRecord(value.basic)) {
1591
+ addFinding(findings, {
1592
+ severity: "error", check: "adjust.basic.structure", message: "basic must be an object", path: basicPath,
1593
+ });
1594
+ } else {
1595
+ const basicKeys = new Set([
1596
+ "exposure", "contrast", "highlights", "shadows", "blacks", "whites",
1597
+ "temperature", "tint", "vibrance", "saturation",
1598
+ ]);
1599
+ reportUnknownKeys(value.basic, basicKeys, basicPath);
1600
+ for (const key of basicKeys) {
1601
+ if (!Object.hasOwn(value.basic, key)) continue;
1602
+ const minimum = key === "exposure" ? -3 : -1;
1603
+ const maximum = key === "exposure" ? 3 : 1;
1604
+ if (!isFiniteNumber(value.basic[key]) || value.basic[key] < minimum || value.basic[key] > maximum) {
1605
+ addFinding(findings, {
1606
+ severity: "error",
1607
+ check: `adjust.basic.${key}`,
1608
+ message: `${key} must be a finite number within [${minimum}, ${maximum}]`,
1609
+ path: `${basicPath}.${key}`,
1610
+ });
1611
+ }
1612
+ }
1613
+ }
1614
+ }
1615
+
1616
+ if (Object.hasOwn(value, "lut") && value.lut !== null) {
1617
+ const lutPath = `${path}.lut`;
1618
+ if (!isRecord(value.lut)) {
1619
+ addFinding(findings, {
1620
+ severity: "error", check: "adjust.lut.structure", message: "lut must be null or an object", path: lutPath,
1621
+ });
1622
+ } else {
1623
+ reportUnknownKeys(value.lut, new Set(["lut", "intensity"]), lutPath);
1624
+ if (!isNonEmptyString(value.lut.lut)) {
1625
+ addFinding(findings, {
1626
+ severity: "error", check: "adjust.lut.lut", message: "lut must be a non-empty string", path: `${lutPath}.lut`,
1627
+ });
1628
+ }
1629
+ if (Object.hasOwn(value.lut, "intensity")
1630
+ && (!isFiniteNumber(value.lut.intensity) || value.lut.intensity < 0 || value.lut.intensity > 1)) {
1631
+ addFinding(findings, {
1632
+ severity: "error",
1633
+ check: "adjust.lut.intensity",
1634
+ message: "intensity must be a finite number within [0, 1]",
1635
+ path: `${lutPath}.intensity`,
1636
+ });
1637
+ }
1638
+ }
1639
+ }
1640
+
1641
+ if (Object.hasOwn(value, "sections")) {
1642
+ const sectionsPath = `${path}.sections`;
1643
+ if (!isRecord(value.sections)) {
1644
+ addFinding(findings, {
1645
+ severity: "error", check: "adjust.sections.structure", message: "sections must be an object", path: sectionsPath,
1646
+ });
1647
+ } else {
1648
+ const sectionKeys = new Set(["basic", "lut", "curves", "wheels", "hue"]);
1649
+ reportUnknownKeys(value.sections, sectionKeys, sectionsPath);
1650
+ for (const key of sectionKeys) {
1651
+ if (Object.hasOwn(value.sections, key) && typeof value.sections[key] !== "boolean") {
1652
+ addFinding(findings, {
1653
+ severity: "error",
1654
+ check: `adjust.sections.${key}`,
1655
+ message: `${key} must be a boolean`,
1656
+ path: `${sectionsPath}.${key}`,
1657
+ });
1658
+ }
1659
+ }
1660
+ }
1661
+ }
1662
+ }
1663
+
1554
1664
  function validateChromaKey(value, findings, path) {
1555
1665
  if (value === undefined || value === null) return;
1556
1666
  if (!isRecord(value)) {
@@ -6267,3 +6377,55 @@ function formatDb(value) {
6267
6377
  function messageOf(error) {
6268
6378
  return error instanceof Error ? error.message : String(error);
6269
6379
  }
6380
+
6381
+ // Intentional dependency-free duplicate of the closed adjustV1 structure.
6382
+ function validateAdjustV1Sections(value, findings, path) {
6383
+ if (!isRecord(value)) return;
6384
+ const report = (section, check, at, message) => addFinding(findings, { severity: "error", check: "adjust." + section + "." + check, path: at, message });
6385
+ const object = (v, keys, section, at) => {
6386
+ if (!isRecord(v)) { report(section, 'structure', at, 'は object である必要があります'); return false; }
6387
+ for (const key of Object.keys(v)) if (!keys.includes(key)) report(section, 'unknown-key', at + '.' + key, 'は未知のキーです');
6388
+ return true;
6389
+ };
6390
+ const number = (v, min, max, section, at) => {
6391
+ if (!isFiniteNumber(v) || v < min || v > max) report(section, 'range', at, 'は ' + min + ' から ' + max + ' の範囲の有限数である必要があります');
6392
+ };
6393
+ for (const section of ['curves', 'hue']) {
6394
+ if (!Object.hasOwn(value, section)) continue;
6395
+ const channels = value[section], at = path + '.' + section;
6396
+ const axis = section === 'curves' ? 'in' : 'hue';
6397
+ const output = section === 'curves' ? 'out' : 'value';
6398
+ const minimum = section === 'curves' ? 2 : 1;
6399
+ const keys = section === 'curves' ? ['master', 'r', 'g', 'b'] : ['hue', 'sat', 'luma'];
6400
+ if (!object(channels, keys, section, at)) continue;
6401
+ for (const channel of keys) {
6402
+ if (!Object.hasOwn(channels, channel)) continue;
6403
+ const points = channels[channel], channelPath = at + '.' + channel;
6404
+ if (!Array.isArray(points) || points.length < minimum || points.length > 16) {
6405
+ report(section, 'points', channelPath, 'は ' + minimum + ' から 16 点の配列である必要があります'); continue;
6406
+ }
6407
+ let previous = -Infinity;
6408
+ for (const [index, point] of points.entries()) {
6409
+ const pointPath = channelPath + '[' + index + ']';
6410
+ if (!object(point, [axis, output], section, pointPath)) continue;
6411
+ number(point[axis], 0, 1, section, pointPath + '.' + axis);
6412
+ number(point[output], 0, 1, section, pointPath + '.' + output);
6413
+ if (isFiniteNumber(point[axis])) {
6414
+ if (point[axis] <= previous) report(section, 'order', pointPath + '.' + axis, 'は狭義単調増加である必要があります');
6415
+ previous = point[axis];
6416
+ }
6417
+ }
6418
+ }
6419
+ }
6420
+ if (Object.hasOwn(value, 'wheels')) {
6421
+ const ranges = { lift: 0.25, gamma: 0.5, gain: 0.5, offset: 0.1 };
6422
+ if (!object(value.wheels, Object.keys(ranges), 'wheels', path + '.wheels')) return;
6423
+ for (const [wheel, range] of Object.entries(ranges)) {
6424
+ if (!Object.hasOwn(value.wheels, wheel)) continue;
6425
+ const channels = value.wheels[wheel], at = path + '.wheels.' + wheel;
6426
+ if (!object(channels, ['r', 'g', 'b'], 'wheels', at)) continue;
6427
+ for (const channel of ['r', 'g', 'b']) if (Object.hasOwn(channels, channel)) number(channels[channel], -range, range, 'wheels', at + '.' + channel);
6428
+ }
6429
+ }
6430
+ }
6431
+
@@ -0,0 +1,2 @@
1
+ import type { AdjustBasicV0 } from './edit-v2';
2
+ export declare function adjustBasicToCssApprox(basic: AdjustBasicV0): string;