akari-video 0.1.39 → 0.1.41

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 (40) hide show
  1. package/package.json +1 -1
  2. package/vendor/.akari-capability-sources.json +2 -5
  3. package/vendor/docs/contract-2026-07-13-asset-library.md +2 -3
  4. package/vendor/docs/contract-2026-09-03-clip-adjust-v0.md +166 -0
  5. package/vendor/docs/contract-2026-09-03-preview-playback-rate-v1.md +52 -0
  6. package/vendor/packages/akari-launcher/package.json +1 -1
  7. package/vendor/packages/audio-library-setup/shared/bgm-suggest.mjs +2 -2
  8. package/vendor/packages/edit-lint/src/edit-lint.mjs +128 -0
  9. package/vendor/packages/edit-store/lib/adjust-css-approx.d.ts +2 -0
  10. package/vendor/packages/edit-store/lib/adjust-css-approx.js +31 -0
  11. package/vendor/packages/edit-store/lib/edit-v2.d.ts +25 -0
  12. package/vendor/packages/edit-store/lib/edit-v2.js +38 -1
  13. package/vendor/packages/edit-store/lib/generated/edit-v2-keys.d.ts +8 -8
  14. package/vendor/packages/edit-store/lib/generated/edit-v2-keys.js +8 -1
  15. package/vendor/packages/edit-store/lib/index.d.ts +1 -0
  16. package/vendor/packages/edit-store/lib/index.js +1 -0
  17. package/vendor/packages/edit-store/lib/internal-model.js +1 -0
  18. package/vendor/packages/edit-store/lib/webview-kernel.js +44 -0
  19. package/vendor/packages/media-bin/src/preview-audio-sidecar.mjs +12 -3
  20. package/vendor/packages/overlay-runtime/README.md +10 -2
  21. package/vendor/packages/overlay-runtime/package.json +2 -2
  22. package/vendor/packages/preview-server/package.json +1 -1
  23. package/vendor/packages/schemas/bin/validate-edit.mjs +66 -0
  24. package/vendor/packages/schemas/edit.schema.json +49 -0
  25. package/vendor/packages/schemas/engine-capabilities.json +2 -0
  26. package/vendor/packages/schemas/examples/edit-v2-adjust-lut-empty-invalid/edit.json +16 -0
  27. package/vendor/packages/schemas/examples/edit-v2-adjust-range-invalid/edit.json +16 -0
  28. package/vendor/packages/schemas/examples/edit-v2-adjust-unknown-key-invalid/edit.json +16 -0
  29. package/vendor/packages/schemas/examples/edit-v2-adjust-valid/edit.json +32 -0
  30. package/vendor/packages/schemas/test/edit-v2-schema.test.mjs +20 -0
  31. package/vendor/packages/schemas/test/engine-capabilities.test.mjs +2 -2
  32. package/vendor/packages/schemas/test/validate-edit.test.mjs +18 -0
  33. package/vendor/skills/edit-plan/expression-selection.md +9 -17
  34. package/vendor/skills/overlay-authoring/motion.md +33 -8
  35. package/vendor/skills/overlay-authoring/telop.md +78 -0
  36. package/vendor/packages/bake-layer/README.md +0 -73
  37. package/vendor/packages/bake-layer/package.json +0 -23
  38. package/vendor/packages/template-render/README.ja.md +0 -113
  39. package/vendor/packages/template-render/README.md +0 -113
  40. 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.39",
3
+ "version": "0.1.41",
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": {
@@ -61,6 +61,8 @@
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",
64
66
  "packages/akari-launcher/package.json",
65
67
  "packages/akari-launcher/README.md",
66
68
  "packages/akari-tools/package.json",
@@ -71,8 +73,6 @@
71
73
  "packages/asset-resolver/README.md",
72
74
  "packages/audio-library-setup/package.json",
73
75
  "packages/audio-library-setup/README.md",
74
- "packages/bake-layer/package.json",
75
- "packages/bake-layer/README.md",
76
76
  "packages/chat-bridge/package.json",
77
77
  "packages/creator-root/package.json",
78
78
  "packages/decision-cards/package.json",
@@ -108,9 +108,6 @@
108
108
  "packages/render-cut/README.ja.md",
109
109
  "packages/render-cut/README.md",
110
110
  "packages/schemas/package.json",
111
- "packages/template-render/package.json",
112
- "packages/template-render/README.ja.md",
113
- "packages/template-render/README.md",
114
111
  "packages/word-book/package.json",
115
112
  "skills/address-review/SKILL.md",
116
113
  "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` を解決する
@@ -0,0 +1,166 @@
1
+ # edit.json v2 clip adjust v0 契約
2
+
3
+ - 日付: 2026-09-03
4
+ - lifecycle: accepted
5
+ - 位置づけ: v2 visual item 共通の per-clip カラー補正語彙と、その段階導入契約
6
+
7
+ ## 0. 位置づけ
8
+
9
+ `adjust` は、1 個の visual item に基本補正と 3D LUT を適用する任意フィールドである。値の保存、
10
+ 検証、プレビュー、GPU / OSR 書き出しが同じ語彙と同じ演算順を使う。v0 は基本補正 10 項目と
11
+ 単一 LUT に限定し、カーブやホイールを混ぜない。
12
+
13
+ 本契約の導入は段階的に行う。M1 では schema、edit-store、validator、lint、能力台帳へ席を作る。
14
+ エンジン消費が入るまでは、正しく保存できる値であっても GPU / OSR の能力台帳では `ignored`、
15
+ `runtime_warning: true` とし、edit-lint は `engine.unsupported-field` error を返す。未実装を黙って
16
+ 描画したように扱わない。
17
+
18
+ ## 1. 席
19
+
20
+ 席は `edit.json` version 2 の visual lane にある `tracks[].items[].adjust` である。現行 schema の
21
+ 本契約対象の visual item(media / html / telop / filter / group / captions / caption)に共通し、audio lane
22
+ の item には置かない。
23
+
24
+ ```jsonc
25
+ {
26
+ "version": 2,
27
+ "tracks": [{
28
+ "id": "visual",
29
+ "lane": "visual",
30
+ "items": [{
31
+ "id": "clip-1",
32
+ "at": 0,
33
+ "duration": 90,
34
+ "adjust": {
35
+ "basic": { "exposure": 0.35, "temperature": -0.1, "saturation": 0.15 },
36
+ "lut": { "lut": "cinematic-warm", "intensity": 0.8 },
37
+ "sections": { "basic": true, "lut": true }
38
+ },
39
+ "source": { "kind": "media", "src": "main", "in": 0, "out": 3 }
40
+ }]
41
+ }]
42
+ }
43
+ ```
44
+
45
+ version 0 / 1 の `cuts[]` と `layers[]` には `adjust` の席を追加しない。
46
+
47
+ ## 2. 語彙
48
+
49
+ `adjust` は追加キーを許さない object で、全フィールドが任意である。
50
+
51
+ | field | 型 | 範囲・既定 |
52
+ |---|---|---|
53
+ | `basic` | object | 追加キー不可。各値の省略は `0`(中立) |
54
+ | `lut` | `null` または object | `null` / 省略は LUT 無し |
55
+ | `sections` | object | `basic?` / `lut?` の boolean だけを持つ疎辞書 |
56
+
57
+ `basic` の 10 項目は次のとおりである。
58
+
59
+ | field | 範囲 | 単位・中立 |
60
+ |---|---:|---|
61
+ | `exposure` | `-3..3` | EV、`0` |
62
+ | `contrast` | `-1..1` | `0` |
63
+ | `highlights` | `-1..1` | `0` |
64
+ | `shadows` | `-1..1` | `0` |
65
+ | `blacks` | `-1..1` | `0` |
66
+ | `whites` | `-1..1` | `0` |
67
+ | `temperature` | `-1..1` | `0` |
68
+ | `tint` | `-1..1` | `0` |
69
+ | `vibrance` | `-1..1` | `0` |
70
+ | `saturation` | `-1..1` | `0` |
71
+
72
+ `lut` object は追加キー不可で、空でない文字列 `lut` を必須とし、任意の `intensity` は `0..1`、
73
+ 省略時 `1` とする。
74
+
75
+ ## 3. 基本補正の数値契約
76
+
77
+ 演算は video-space、すなわち gamma 符号化 sRGB 値のまま行う。scene-linear へ変換しない。
78
+ 入力チャンネルを `c = (r, g, b)`、`clamp01(x) = min(1, max(0, x))`、Rec.709 luma を
79
+ `Y(c) = 0.2126r + 0.7152g + 0.0722b` とする。`smoothstep(a,b,x)` は
80
+ `t = clamp01((x-a)/(b-a))`、`t²(3-2t)` である。
81
+
82
+ 演算順は固定で、次の順に進める。
83
+
84
+ 1. exposure: `c *= 2^exposure`。
85
+ 2. white balance: `r *= 1 + temperature*0.18`、`b *= 1 - temperature*0.18`、
86
+ `g *= 1 - tint*0.12` とし、各チャンネルを `clamp01` する。
87
+ 3. tone zones: highlights → shadows → whites → blacks の順に処理する。各有効ステップの直前に
88
+ 現在の `c` から luma を再計算し、処理後は各チャンネルを `clamp01` する。
89
+ - highlights: `c *= 1 + highlights*smoothstep(0.5,0.9,Y(c))`
90
+ - shadows: `c *= 1 + shadows*(1-smoothstep(0.1,0.5,Y(c)))`
91
+ - whites: `c += whites*smoothstep(0.7,1.0,Y(c))*0.3`
92
+ - blacks: `c += blacks*(1-smoothstep(0.0,0.3,Y(c)))*0.3`
93
+ 4. contrast: 各チャンネルを `(c-0.5)*(1+contrast)+0.5` とし、`clamp01` する。pivot は `0.5`。
94
+ 5. saturation: 現在の Rec.709 luma を使い、各チャンネルを
95
+ `Y(c) + (c-Y(c))*(1+saturation)` として `clamp01` する。
96
+ 6. vibrance: 現在値の `max` と `min` から
97
+ `S = max > 1e-6 ? (max-min)/max : 0`、`amount = vibrance*(1-S)` を求め、各チャンネルを
98
+ `Y(c) + (c-Y(c))*(1+amount)` として `clamp01` する。
99
+ 7. 外部 LUT: `lut` が有効なら trilinear sampler で `c` を置換し、`intensity` で identity 入力と
100
+ LUT 出力を線形混合する。
101
+
102
+ 定数の正本値は `REC709 = (0.2126, 0.7152, 0.0722)`、`TEMP_COEF = 0.18`、
103
+ `TINT_COEF = 0.12`、`CONTRAST_PIVOT = 0.5` である。基本補正を LUT に bake する消費者は
104
+ `LUT_3D_SIZE 33`、R fastest → G → B、各成分小数 6 桁とし、適用時は trilinear 補間する。
105
+
106
+ ## 4. 解決順とバイパス
107
+
108
+ 処理順は次で固定する。
109
+
110
+ 1. item の `adjust.basic`
111
+ 2. 同じ item の `adjust.lut`
112
+ 3. timeline 上の item 合成
113
+ 4. `output.look`(全体へ掛けるグローバル LUT)
114
+
115
+ `adjust.lut.lut` の参照解決は `output.look.lut` と同じである。値にスラッシュが無ければ
116
+ `presets/luts/<id>/<id>.cube`、スラッシュがあればプロジェクトルート相対パスとして解決する。
117
+ パス区切りは Windows と POSIX の双方を扱う。
118
+
119
+ `sections` は疎辞書であり、`sections.basic === false` のときだけ基本補正全体を、
120
+ `sections.lut === false` のときだけ item LUT をバイパスする。キー省略は有効を意味する。
121
+ **OFF はプレビューと書き出しの両方で必ずバイパスする。** 一方だけで OFF を無視したり、
122
+ プレビューと書き出しで結果をずらしたりしてはならない。
123
+
124
+ ## 5. 消費側の約束
125
+
126
+ 最終消費者は GPU export と OSR export の 2 出口である。両者は同じ `adjust` 値、33³ bake、
127
+ LUT 参照解決、適用順、sections バイパスを使い、受領情報にも item adjust の適用状況を残す。
128
+
129
+ プレビューには frame-engine WebGL2 レールと DOM fallback レールがある。WebGL2 は item / quad 単位の
130
+ LUT として適用し、DOM fallback は表現可能な基本補正を CSS 近似し、表現できない項目や LUT を
131
+ 黙って同等と称さない。どちらのレールでも `sections.* === false` を先に評価する。frame-engine active
132
+ 時は DOM fallback と二重適用しない。
133
+
134
+ M1 時点では edit-store が `adjust` を内部 item declaration へ損失なく射影するところまでであり、
135
+ GPU / OSR / frame-engine / DOM preview は未消費である。能力台帳は全 visual item 用途について
136
+ `gpu: ignored`、`osr: ignored`、`runtime_warning: true` とし、frame-engine の未知キー警告へ到達させる。
137
+ 消費実装を導入する便でのみ、実測と同時に `consumed` へ反転する。
138
+
139
+ ## 6. lint 分担
140
+
141
+ - JSON Schema は閉じた object、型、数値範囲、必須の `lut` 文字列、visual/audio の席を検査する。
142
+ - `validate-edit.mjs` は依存を増やさず、同じ構造制約を手書きで検査する。
143
+ - edit-store の v2 reader は未知キーを拒否し、型と範囲を検査して内部 declaration へ射影する。
144
+ - edit-lint は依存ゼロを守るため同じ構造検証を意図的に重複実装する。構造が正しい場合も、M1 の
145
+ 能力台帳に従い `--engine gpu|osr|auto` で `engine.unsupported-field` error を出す。
146
+
147
+ 構造エラーと未消費エラーを混同しない。構造が不正な文書は engine 能力判定へ進めない。
148
+
149
+ ## 7. 書き込み側・パネル側の約束
150
+
151
+ 書き込み側は version 2 の visual item だけを編集対象にする。version 0 / 1 を平坦な近似語彙へ
152
+ 書き換えない。`basic` が全て `0` または省略、`lut` が `null` または省略で、`sections` 以外に
153
+ 効果が無い identity 状態は `adjust` field 自体を削除する。schema は読み込み互換のため identity
154
+ object を拒否しないが、正規の保存形は field 省略である。
155
+
156
+ パネルで section を OFF にしても値は保持してよい。再度 ON にしたとき同じ値へ戻せる一方、消費側は
157
+ OFF 中の値を必ず無視する。UI が値を表示・更新できることと、エンジンが消費済みであることは別であり、
158
+ 未消費の間は lint error と runtime warning を隠さない。
159
+
160
+ ## 8. 非スコープ
161
+
162
+ - version 0 / 1 の `cuts[]` / `layers[]` への席追加
163
+ - RGB curves、color wheels / CDL、hue curves、effects(M2 の語彙)
164
+ - vignette(空間処理であり basic/LUT bake の対象外)
165
+ - 本便での compositor、page-builder、preview、inspector、preset カタログの実装
166
+ - 複数 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 永続化、キーボードショートカット、速度スライダーは本契約の対象外とする。
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "akari-video",
3
- "version": "0.1.39",
3
+ "version": "0.1.41",
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,104 @@ 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
+ if (!isRecord(value)) {
1569
+ addFinding(findings, {
1570
+ severity: "error", check: "adjust.structure", message: "adjust must be an object", path,
1571
+ });
1572
+ return;
1573
+ }
1574
+ const reportUnknownKeys = (record, allowed, ownerPath) => {
1575
+ for (const key of Object.keys(record)) {
1576
+ if (allowed.has(key)) continue;
1577
+ addFinding(findings, {
1578
+ severity: "error",
1579
+ check: "adjust.unknown-key",
1580
+ message: `${key} is not defined by clip adjust v0`,
1581
+ path: `${ownerPath}.${key}`,
1582
+ });
1583
+ }
1584
+ };
1585
+ reportUnknownKeys(value, new Set(["basic", "lut", "sections"]), path);
1586
+
1587
+ if (Object.hasOwn(value, "basic")) {
1588
+ const basicPath = `${path}.basic`;
1589
+ if (!isRecord(value.basic)) {
1590
+ addFinding(findings, {
1591
+ severity: "error", check: "adjust.basic.structure", message: "basic must be an object", path: basicPath,
1592
+ });
1593
+ } else {
1594
+ const basicKeys = new Set([
1595
+ "exposure", "contrast", "highlights", "shadows", "blacks", "whites",
1596
+ "temperature", "tint", "vibrance", "saturation",
1597
+ ]);
1598
+ reportUnknownKeys(value.basic, basicKeys, basicPath);
1599
+ for (const key of basicKeys) {
1600
+ if (!Object.hasOwn(value.basic, key)) continue;
1601
+ const minimum = key === "exposure" ? -3 : -1;
1602
+ const maximum = key === "exposure" ? 3 : 1;
1603
+ if (!isFiniteNumber(value.basic[key]) || value.basic[key] < minimum || value.basic[key] > maximum) {
1604
+ addFinding(findings, {
1605
+ severity: "error",
1606
+ check: `adjust.basic.${key}`,
1607
+ message: `${key} must be a finite number within [${minimum}, ${maximum}]`,
1608
+ path: `${basicPath}.${key}`,
1609
+ });
1610
+ }
1611
+ }
1612
+ }
1613
+ }
1614
+
1615
+ if (Object.hasOwn(value, "lut") && value.lut !== null) {
1616
+ const lutPath = `${path}.lut`;
1617
+ if (!isRecord(value.lut)) {
1618
+ addFinding(findings, {
1619
+ severity: "error", check: "adjust.lut.structure", message: "lut must be null or an object", path: lutPath,
1620
+ });
1621
+ } else {
1622
+ reportUnknownKeys(value.lut, new Set(["lut", "intensity"]), lutPath);
1623
+ if (!isNonEmptyString(value.lut.lut)) {
1624
+ addFinding(findings, {
1625
+ severity: "error", check: "adjust.lut.lut", message: "lut must be a non-empty string", path: `${lutPath}.lut`,
1626
+ });
1627
+ }
1628
+ if (Object.hasOwn(value.lut, "intensity")
1629
+ && (!isFiniteNumber(value.lut.intensity) || value.lut.intensity < 0 || value.lut.intensity > 1)) {
1630
+ addFinding(findings, {
1631
+ severity: "error",
1632
+ check: "adjust.lut.intensity",
1633
+ message: "intensity must be a finite number within [0, 1]",
1634
+ path: `${lutPath}.intensity`,
1635
+ });
1636
+ }
1637
+ }
1638
+ }
1639
+
1640
+ if (Object.hasOwn(value, "sections")) {
1641
+ const sectionsPath = `${path}.sections`;
1642
+ if (!isRecord(value.sections)) {
1643
+ addFinding(findings, {
1644
+ severity: "error", check: "adjust.sections.structure", message: "sections must be an object", path: sectionsPath,
1645
+ });
1646
+ } else {
1647
+ const sectionKeys = new Set(["basic", "lut"]);
1648
+ reportUnknownKeys(value.sections, sectionKeys, sectionsPath);
1649
+ for (const key of sectionKeys) {
1650
+ if (Object.hasOwn(value.sections, key) && typeof value.sections[key] !== "boolean") {
1651
+ addFinding(findings, {
1652
+ severity: "error",
1653
+ check: `adjust.sections.${key}`,
1654
+ message: `${key} must be a boolean`,
1655
+ path: `${sectionsPath}.${key}`,
1656
+ });
1657
+ }
1658
+ }
1659
+ }
1660
+ }
1661
+ }
1662
+
1554
1663
  function validateChromaKey(value, findings, path) {
1555
1664
  if (value === undefined || value === null) return;
1556
1665
  if (!isRecord(value)) {
@@ -2339,6 +2448,25 @@ async function validateOverlays(overlays, timeline, findings, paths) {
2339
2448
  path: relativePath(paths.projectRoot, htmlPath),
2340
2449
  });
2341
2450
  }
2451
+ // テキスト分割断片の CSS animation は [data-akari-active] ゲートの中で宣言する
2452
+ // (skills/overlay-authoring/telop.md「テキスト分割と stagger 規約」)。
2453
+ // getAnimations() のコストはドキュメント全体の animation 総数に比例するため、
2454
+ // ゲート無しの断片が 1 つでも混ざると全体の tick が落ちる。分割はその危険を
2455
+ // 分割数ぶんに増幅する(実測: 1,200 断片 × 8 分割 = 9,600 本で 221ms/tick。
2456
+ // akari-video-internal contract-2026-08-15-telop-motion-grammar-v0 §6)。
2457
+ if (/\bdata-akari-split\s*=/.test(html) && /(^|[^-\w])animation\s*:/.test(html)) {
2458
+ const gated = /\[data-akari-active\][^{}]*\.[^{}]*\{[^{}]*animation\s*:/.test(html);
2459
+ if (!gated) {
2460
+ addFinding(findings, {
2461
+ severity: "error",
2462
+ check: "overlays.split-animation-gate",
2463
+ message:
2464
+ "text-split fragment must declare animations under a [data-akari-active] selector",
2465
+ path: relativePath(paths.projectRoot, htmlPath),
2466
+ });
2467
+ }
2468
+ }
2469
+
2342
2470
  for (const [attribute, expected] of [
2343
2471
  ["data-start", overlay.start],
2344
2472
  ["data-duration", overlay.duration],
@@ -0,0 +1,2 @@
1
+ import type { AdjustBasicV0 } from './edit-v2';
2
+ export declare function adjustBasicToCssApprox(basic: AdjustBasicV0): string;
@@ -0,0 +1,31 @@
1
+ "use strict";
2
+ // Serialized into the preview webview via Function.prototype.toString() -- see
3
+ // apps/shell/extensions/akari-preview/src/common/cut-framing-visual.ts for the established
4
+ // pattern. Keep this function self-contained: no closures over module state and no calls to
5
+ // sibling functions in this file.
6
+ Object.defineProperty(exports, "__esModule", { value: true });
7
+ exports.adjustBasicToCssApprox = adjustBasicToCssApprox;
8
+ function adjustBasicToCssApprox(basic) {
9
+ const source = basic ?? {};
10
+ const exposure = Number.isFinite(source.exposure) ? source.exposure : 0;
11
+ const contrast = Number.isFinite(source.contrast) ? source.contrast : 0;
12
+ const saturation = Number.isFinite(source.saturation) ? source.saturation : 0;
13
+ const temperature = Number.isFinite(source.temperature) ? source.temperature : 0;
14
+ const parts = [];
15
+ if (Math.abs(exposure) > 0.005) {
16
+ parts.push(`brightness(${Math.pow(2, exposure).toFixed(2)})`);
17
+ }
18
+ if (Math.abs(contrast) > 0.005) {
19
+ parts.push(`contrast(${(1 + contrast).toFixed(2)})`);
20
+ }
21
+ if (Math.abs(saturation) > 0.005) {
22
+ parts.push(`saturate(${(1 + saturation).toFixed(2)})`);
23
+ }
24
+ if (temperature > 0.005) {
25
+ parts.push(`sepia(${(temperature * 0.3).toFixed(2)})`);
26
+ }
27
+ else if (temperature < -0.005) {
28
+ parts.push(`hue-rotate(${(-temperature * 20).toFixed(0)}deg)`);
29
+ }
30
+ return parts.join(' ');
31
+ }
@@ -164,6 +164,30 @@ export interface CaptionSourceV2 {
164
164
  id: string;
165
165
  }
166
166
  export type SourceV2 = MediaSourceV2 | HtmlSourceV2 | ShapeSourceV2 | TelopSourceV2 | FilterSourceV2 | GroupSourceV2 | CaptionsSourceV2 | CaptionSourceV2;
167
+ export interface AdjustBasicV0 {
168
+ exposure?: number;
169
+ contrast?: number;
170
+ highlights?: number;
171
+ shadows?: number;
172
+ blacks?: number;
173
+ whites?: number;
174
+ temperature?: number;
175
+ tint?: number;
176
+ vibrance?: number;
177
+ saturation?: number;
178
+ }
179
+ export interface AdjustLutV0 {
180
+ lut: string;
181
+ intensity?: number;
182
+ }
183
+ export interface AdjustV0 {
184
+ basic?: AdjustBasicV0;
185
+ lut?: AdjustLutV0 | null;
186
+ sections?: {
187
+ basic?: boolean;
188
+ lut?: boolean;
189
+ };
190
+ }
167
191
  export interface ItemV2Base {
168
192
  id: string;
169
193
  name?: string;
@@ -186,6 +210,7 @@ export interface ItemV2Base {
186
210
  opacity?: number;
187
211
  blend?: BlendModeV2;
188
212
  crop?: CropV2;
213
+ adjust?: AdjustV0;
189
214
  perspective?: Record<string, unknown>;
190
215
  motion?: MotionV0;
191
216
  animator?: AnimatorV0[];
@@ -9,7 +9,7 @@ const SHAPE_KINDS = new Set([
9
9
  'rect', 'rounded-rect', 'ellipse', 'line', 'arrow', 'speech-bubble'
10
10
  ]);
11
11
  const ITEM_KEYS = new Set([
12
- 'id', 'name', 'hidden', 'locked', 'at', 'duration', 'transform', 'opacity', 'blend', 'crop', 'perspective',
12
+ 'id', 'name', 'hidden', 'locked', 'at', 'duration', 'transform', 'opacity', 'blend', 'crop', 'adjust', 'perspective',
13
13
  'motion', 'animator', 'keyframes', 'items', 'mask', 'source'
14
14
  ]);
15
15
  const AUDIO_ITEM_KEYS = new Set([
@@ -265,6 +265,8 @@ function validateItem(value, path, ids, sourceIds) {
265
265
  }
266
266
  if (hasOwn(value, 'crop'))
267
267
  validateCrop(value.crop, `${path}.crop`);
268
+ if (hasOwn(value, 'adjust'))
269
+ validateAdjust(value.adjust, `${path}.adjust`);
268
270
  if (hasOwn(value, 'perspective'))
269
271
  requireRecord(value.perspective, `${path}.perspective`);
270
272
  if (hasOwn(value, 'motion'))
@@ -457,6 +459,41 @@ function validateCrop(value, path) {
457
459
  throw invalid(`${path}.${key}`, '0 より大きい必要があります');
458
460
  }
459
461
  }
462
+ function validateAdjust(value, path) {
463
+ requireRecord(value, path);
464
+ requireExactKeys(value, new Set(['basic', 'lut', 'sections']), path);
465
+ if (hasOwn(value, 'basic')) {
466
+ requireRecord(value.basic, `${path}.basic`);
467
+ const basicKeys = new Set([
468
+ 'exposure', 'contrast', 'highlights', 'shadows', 'blacks', 'whites',
469
+ 'temperature', 'tint', 'vibrance', 'saturation'
470
+ ]);
471
+ requireExactKeys(value.basic, basicKeys, `${path}.basic`);
472
+ for (const key of basicKeys) {
473
+ if (!hasOwn(value.basic, key))
474
+ continue;
475
+ const [minimum, maximum] = key === 'exposure' ? [-3, 3] : [-1, 1];
476
+ requireRange(value.basic[key], minimum, maximum, `${path}.basic.${key}`);
477
+ }
478
+ }
479
+ if (hasOwn(value, 'lut') && value.lut !== null) {
480
+ requireRecord(value.lut, `${path}.lut`);
481
+ requireExactKeys(value.lut, new Set(['lut', 'intensity']), `${path}.lut`);
482
+ requireText(value.lut.lut, `${path}.lut.lut`);
483
+ if (hasOwn(value.lut, 'intensity'))
484
+ requireRange(value.lut.intensity, 0, 1, `${path}.lut.intensity`);
485
+ }
486
+ if (hasOwn(value, 'sections')) {
487
+ requireRecord(value.sections, `${path}.sections`);
488
+ const sectionKeys = new Set(['basic', 'lut']);
489
+ requireExactKeys(value.sections, sectionKeys, `${path}.sections`);
490
+ for (const key of sectionKeys) {
491
+ if (hasOwn(value.sections, key) && typeof value.sections[key] !== 'boolean') {
492
+ throw invalid(`${path}.sections.${key}`, 'boolean である必要があります');
493
+ }
494
+ }
495
+ }
496
+ }
460
497
  const EASINGS = new Set([
461
498
  'linear', 'ease-in-out', 'in-quad', 'out-quad', 'in-out-quad', 'in-cubic', 'out-cubic',
462
499
  'in-out-cubic', 'in-quart', 'out-quart', 'in-out-quart', 'in-expo', 'out-expo', 'in-out-expo',
@@ -1,4 +1,4 @@
1
- export declare const ITEM_V2_KEYS: readonly ["id", "name", "hidden", "locked", "at", "duration", "anchor", "transform", "opacity", "blend", "crop", "perspective", "motion", "animator", "keyframes", "items", "mask", "source", "role", "gain_db", "denoise", "lowcut_hz", "fade_in", "fade_out", "ducking", "duck_db", "duck_attack", "duck_release", "script", "reading", "provenance"];
1
+ export declare const ITEM_V2_KEYS: readonly ["id", "name", "hidden", "locked", "at", "duration", "anchor", "transform", "opacity", "blend", "crop", "adjust", "perspective", "motion", "animator", "keyframes", "items", "mask", "source", "role", "gain_db", "denoise", "lowcut_hz", "fade_in", "fade_out", "ducking", "duck_db", "duck_attack", "duck_release", "script", "reading", "provenance"];
2
2
  export declare const ITEM_SOURCE_V2_KEYS: readonly ["kind", "src", "in", "out", "framing", "transition_out", "freeze", "fx", "speed", "chroma_key", "pitch_semitones", "formant", "path", "part", "style", "text", "exclude", "derivedFrom", "vars", "params", "shape", "preset", "baked", "from", "filter", "id"];
3
3
  export declare const KEYFRAME_V2_KEYS: readonly ["t", "transform", "crop", "perspective", "opacity", "gain_db", "animator", "easing"];
4
4
  export declare const MOTION_V0_KEYS: readonly ["in", "out", "loop"];
@@ -6,14 +6,14 @@ export declare const ANIMATOR_V0_KEYS: readonly ["id", "basis", "shape", "start"
6
6
  export declare const MOTION_FILE_V0_KEYS: readonly ["version", "group", "items"];
7
7
  export declare const SOURCE_KIND_V2: readonly ["media", "html", "shape", "telop", "filter", "group", "captions", "caption"];
8
8
  export declare const ITEM_V2_KEYS_BY_DEFINITION: {
9
- readonly itemV2Media: readonly ["id", "name", "hidden", "locked", "at", "duration", "anchor", "transform", "opacity", "blend", "crop", "perspective", "motion", "animator", "keyframes", "items", "mask", "source"];
10
- readonly itemV2Html: readonly ["id", "name", "hidden", "locked", "at", "duration", "anchor", "transform", "opacity", "blend", "crop", "perspective", "motion", "animator", "keyframes", "items", "source"];
9
+ readonly itemV2Media: readonly ["id", "name", "hidden", "locked", "at", "duration", "anchor", "transform", "opacity", "blend", "crop", "adjust", "perspective", "motion", "animator", "keyframes", "items", "mask", "source"];
10
+ readonly itemV2Html: readonly ["id", "name", "hidden", "locked", "at", "duration", "anchor", "transform", "opacity", "blend", "crop", "adjust", "perspective", "motion", "animator", "keyframes", "items", "source"];
11
11
  readonly itemV2Shape: readonly ["id", "name", "hidden", "locked", "at", "duration", "anchor", "transform", "opacity", "blend", "crop", "perspective", "motion", "animator", "keyframes", "items", "source"];
12
- readonly itemV2Telop: readonly ["id", "name", "hidden", "locked", "at", "duration", "anchor", "transform", "opacity", "blend", "crop", "perspective", "motion", "animator", "keyframes", "items", "source"];
13
- readonly itemV2Filter: readonly ["id", "name", "hidden", "locked", "at", "duration", "anchor", "transform", "opacity", "blend", "crop", "perspective", "motion", "animator", "keyframes", "items", "source"];
14
- readonly itemV2Group: readonly ["id", "name", "hidden", "locked", "at", "duration", "anchor", "transform", "opacity", "blend", "crop", "perspective", "motion", "animator", "keyframes", "items", "source"];
15
- readonly itemV2Captions: readonly ["id", "name", "hidden", "locked", "at", "duration", "transform", "opacity", "blend", "crop", "perspective", "motion", "animator", "keyframes", "items", "source"];
16
- readonly itemV2Caption: readonly ["id", "name", "hidden", "locked", "at", "duration", "transform", "opacity", "blend", "crop", "perspective", "motion", "animator", "keyframes", "items", "source"];
12
+ readonly itemV2Telop: readonly ["id", "name", "hidden", "locked", "at", "duration", "anchor", "transform", "opacity", "blend", "crop", "adjust", "perspective", "motion", "animator", "keyframes", "items", "source"];
13
+ readonly itemV2Filter: readonly ["id", "name", "hidden", "locked", "at", "duration", "anchor", "transform", "opacity", "blend", "crop", "adjust", "perspective", "motion", "animator", "keyframes", "items", "source"];
14
+ readonly itemV2Group: readonly ["id", "name", "hidden", "locked", "at", "duration", "anchor", "transform", "opacity", "blend", "crop", "adjust", "perspective", "motion", "animator", "keyframes", "items", "source"];
15
+ readonly itemV2Captions: readonly ["id", "name", "hidden", "locked", "at", "duration", "transform", "opacity", "blend", "crop", "adjust", "perspective", "motion", "animator", "keyframes", "items", "source"];
16
+ readonly itemV2Caption: readonly ["id", "name", "hidden", "locked", "at", "duration", "transform", "opacity", "blend", "crop", "adjust", "perspective", "motion", "animator", "keyframes", "items", "source"];
17
17
  readonly itemV2AudioMedia: readonly ["id", "name", "hidden", "locked", "at", "duration", "role", "source", "gain_db", "denoise", "lowcut_hz", "keyframes", "fade_in", "fade_out", "ducking", "duck_db", "duck_attack", "duck_release", "script", "reading", "provenance"];
18
18
  };
19
19
  export declare const ITEM_SOURCE_V2_KEYS_BY_DEFINITION: {