akari-video 0.1.41 → 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 (26) hide show
  1. package/package.json +1 -1
  2. package/src/self-update.mjs +16 -2
  3. package/vendor/.akari-capability-sources.json +1 -0
  4. package/vendor/docs/contract-2026-07-22-render-basics.md +2 -0
  5. package/vendor/docs/contract-2026-09-03-clip-adjust-v0.md +1 -0
  6. package/vendor/docs/contract-2026-09-05-clip-adjust-v1.md +102 -0
  7. package/vendor/packages/akari-launcher/package.json +1 -1
  8. package/vendor/packages/edit-lint/src/edit-lint.mjs +56 -3
  9. package/vendor/packages/edit-store/lib/adjust-css-visual.d.ts +38 -0
  10. package/vendor/packages/edit-store/lib/adjust-css-visual.js +67 -0
  11. package/vendor/packages/edit-store/lib/edit-v2.d.ts +40 -2
  12. package/vendor/packages/edit-store/lib/edit-v2.js +42 -2
  13. package/vendor/packages/edit-store/lib/index.d.ts +1 -0
  14. package/vendor/packages/edit-store/lib/index.js +2 -0
  15. package/vendor/packages/edit-store/lib/webview-kernel.d.ts +1 -0
  16. package/vendor/packages/edit-store/lib/webview-kernel.js +89 -2
  17. package/vendor/packages/schemas/bin/validate-edit.mjs +54 -2
  18. package/vendor/packages/schemas/edit.schema.json +285 -13
  19. package/vendor/packages/schemas/examples/edit-v2-adjust-v1-hue-empty-invalid/edit.json +95 -0
  20. package/vendor/packages/schemas/examples/edit-v2-adjust-v1-order-invalid/edit.json +100 -0
  21. package/vendor/packages/schemas/examples/edit-v2-adjust-v1-unknown-invalid/edit.json +101 -0
  22. package/vendor/packages/schemas/examples/edit-v2-adjust-v1-valid/edit.json +100 -0
  23. package/vendor/packages/schemas/examples/edit-v2-adjust-v1-wheels-invalid/edit.json +100 -0
  24. package/vendor/packages/schemas/test/adjust-v1.test.mjs +47 -0
  25. package/vendor/packages/schemas/test/edit-v2-schema.test.mjs +1 -1
  26. package/vendor/packages/schemas/test/fixtures/adjust-v1-cases.mjs +37 -0
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "akari-video",
3
- "version": "0.1.41",
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 };
@@ -63,6 +63,7 @@
63
63
  "docs/contract-2026-09-02-word-book-v0.md",
64
64
  "docs/contract-2026-09-03-clip-adjust-v0.md",
65
65
  "docs/contract-2026-09-03-preview-playback-rate-v1.md",
66
+ "docs/contract-2026-09-05-clip-adjust-v1.md",
66
67
  "packages/akari-launcher/package.json",
67
68
  "packages/akari-launcher/README.md",
68
69
  "packages/akari-tools/package.json",
@@ -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 /
@@ -1,3 +1,4 @@
1
+ > v1(curves / wheels / hue)で拡張。正本は [contract-2026-09-05-clip-adjust-v1.md](contract-2026-09-05-clip-adjust-v1.md)。
1
2
  # edit.json v2 clip adjust v0 契約
2
3
 
3
4
  - 日付: 2026-09-03
@@ -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.41",
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": [
@@ -1565,6 +1565,7 @@ function validateLook(value, findings, path) {
1565
1565
  // docs/contract-2026-09-03-clip-adjust-v0.md。edit-lint は依存ゼロを保つため、schema と
1566
1566
  // validate-edit.mjs の閉じた adjust 語彙をここでも独立に検証する。
1567
1567
  function validateAdjust(value, findings, path) {
1568
+ validateAdjustV1Sections(value, findings, path);
1568
1569
  if (!isRecord(value)) {
1569
1570
  addFinding(findings, {
1570
1571
  severity: "error", check: "adjust.structure", message: "adjust must be an object", path,
@@ -1577,12 +1578,12 @@ function validateAdjust(value, findings, path) {
1577
1578
  addFinding(findings, {
1578
1579
  severity: "error",
1579
1580
  check: "adjust.unknown-key",
1580
- message: `${key} is not defined by clip adjust v0`,
1581
+ message: `${key} is not defined by clip adjust v1`,
1581
1582
  path: `${ownerPath}.${key}`,
1582
1583
  });
1583
1584
  }
1584
1585
  };
1585
- reportUnknownKeys(value, new Set(["basic", "lut", "sections"]), path);
1586
+ reportUnknownKeys(value, new Set(["basic", "lut", "sections", "curves", "wheels", "hue"]), path);
1586
1587
 
1587
1588
  if (Object.hasOwn(value, "basic")) {
1588
1589
  const basicPath = `${path}.basic`;
@@ -1644,7 +1645,7 @@ function validateAdjust(value, findings, path) {
1644
1645
  severity: "error", check: "adjust.sections.structure", message: "sections must be an object", path: sectionsPath,
1645
1646
  });
1646
1647
  } else {
1647
- const sectionKeys = new Set(["basic", "lut"]);
1648
+ const sectionKeys = new Set(["basic", "lut", "curves", "wheels", "hue"]);
1648
1649
  reportUnknownKeys(value.sections, sectionKeys, sectionsPath);
1649
1650
  for (const key of sectionKeys) {
1650
1651
  if (Object.hasOwn(value.sections, key) && typeof value.sections[key] !== "boolean") {
@@ -6376,3 +6377,55 @@ function formatDb(value) {
6376
6377
  function messageOf(error) {
6377
6378
  return error instanceof Error ? error.message : String(error);
6378
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,38 @@
1
+ import type { AdjustCurvesV1, AdjustWheelsV1, AdjustHueCurvesV1 } from './edit-v2';
2
+ export interface AdjustCssBasic {
3
+ exposure?: number;
4
+ contrast?: number;
5
+ highlights?: number;
6
+ shadows?: number;
7
+ blacks?: number;
8
+ whites?: number;
9
+ temperature?: number;
10
+ tint?: number;
11
+ vibrance?: number;
12
+ saturation?: number;
13
+ }
14
+ export interface AdjustCssInput {
15
+ basic?: AdjustCssBasic | null;
16
+ curves?: AdjustCurvesV1;
17
+ wheels?: AdjustWheelsV1;
18
+ hue?: AdjustHueCurvesV1;
19
+ sections?: {
20
+ basic?: boolean;
21
+ lut?: boolean;
22
+ curves?: boolean;
23
+ wheels?: boolean;
24
+ hue?: boolean;
25
+ } | null;
26
+ }
27
+ export interface AdjustCssVisual {
28
+ filter: string;
29
+ /** True when active controls contain values that CSS filters cannot reproduce. */
30
+ hasApproximation: boolean;
31
+ }
32
+ /**
33
+ * Resolves an item's basic colour adjustment and an optional transition filter into the single
34
+ * CSS `filter` seat used by DOM preview media. Returns null only when neither input owns that
35
+ * seat. A basic section with all-zero values deliberately returns `{ filter: '' }`, allowing an
36
+ * edit refresh to clear a previously active adjustment without inventing a CSS no-op.
37
+ */
38
+ export declare function computeAdjustCssVisual(adjust: AdjustCssInput | null | undefined, transitionFilter?: string | null): AdjustCssVisual | null;
@@ -0,0 +1,67 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.computeAdjustCssVisual = computeAdjustCssVisual;
4
+ /**
5
+ * Resolves an item's basic colour adjustment and an optional transition filter into the single
6
+ * CSS `filter` seat used by DOM preview media. Returns null only when neither input owns that
7
+ * seat. A basic section with all-zero values deliberately returns `{ filter: '' }`, allowing an
8
+ * edit refresh to clear a previously active adjustment without inventing a CSS no-op.
9
+ */
10
+ function computeAdjustCssVisual(adjust, transitionFilter) {
11
+ const source = adjust && typeof adjust === 'object' && !Array.isArray(adjust) ? adjust : null;
12
+ const rawBasic = source && source.sections?.basic !== false ? source.basic : null;
13
+ const basic = rawBasic && typeof rawBasic === 'object' && !Array.isArray(rawBasic) ? rawBasic : null;
14
+ const rawTransition = typeof transitionFilter === 'string' ? transitionFilter.trim() : '';
15
+ const transition = rawTransition === 'none' ? '' : rawTransition;
16
+ // Match kernel normalization and identity tolerances without a runtime dependency.
17
+ const clamp01 = (value) => Number.isFinite(value) ? Math.max(0, Math.min(1, value)) : 0;
18
+ const hasWheels = source?.sections?.wheels !== false
19
+ && ['lift', 'gamma', 'gain', 'offset'].some(wheel => ['r', 'g', 'b'].some(channel => {
20
+ const value = source?.wheels?.[wheel]?.[channel];
21
+ return Number.isFinite(value) && value !== 0;
22
+ }));
23
+ const hasCurves = source?.sections?.curves !== false
24
+ && ['master', 'r', 'g', 'b'].some(channel => {
25
+ const raw = source?.curves?.[channel];
26
+ if (raw == null)
27
+ return false;
28
+ const points = raw.map(point => ({ in: clamp01(point.in), out: clamp01(point.out) }))
29
+ .sort((a, b) => a.in - b.in);
30
+ return !(points.length === 2
31
+ && Math.abs(points[0].in) < 1e-5 && Math.abs(points[0].out) < 1e-5
32
+ && Math.abs(points[1].in - 1) < 1e-5 && Math.abs(points[1].out - 1) < 1e-5);
33
+ });
34
+ const hasHue = source?.sections?.hue !== false
35
+ && ['hue', 'sat', 'luma'].some(channel => (source?.hue?.[channel] ?? []).some(point => Math.abs((Number.isFinite(point.value) ? clamp01(point.value) : 0.5) - 0.5) > 1e-4));
36
+ const hasUnsupportedSection = hasWheels || hasCurves || hasHue;
37
+ if (!basic && !transition && !hasUnsupportedSection)
38
+ return null;
39
+ const exposure = basic && Number.isFinite(basic.exposure) ? basic.exposure : 0;
40
+ const contrast = basic && Number.isFinite(basic.contrast) ? basic.contrast : 0;
41
+ const saturation = basic && Number.isFinite(basic.saturation) ? basic.saturation : 0;
42
+ const temperature = basic && Number.isFinite(basic.temperature) ? basic.temperature : 0;
43
+ const parts = [];
44
+ if (Math.abs(exposure) > 0.005) {
45
+ parts.push('brightness(' + Math.pow(2, exposure).toFixed(2) + ')');
46
+ }
47
+ if (Math.abs(contrast) > 0.005) {
48
+ parts.push('contrast(' + (1 + contrast).toFixed(2) + ')');
49
+ }
50
+ if (Math.abs(saturation) > 0.005) {
51
+ parts.push('saturate(' + (1 + saturation).toFixed(2) + ')');
52
+ }
53
+ if (temperature > 0.005) {
54
+ parts.push('sepia(' + (temperature * 0.3).toFixed(2) + ')');
55
+ }
56
+ else if (temperature < -0.005) {
57
+ parts.push('hue-rotate(' + (-temperature * 20).toFixed(0) + 'deg)');
58
+ }
59
+ if (transition)
60
+ parts.push(transition);
61
+ const unsupportedKeys = ['tint', 'highlights', 'shadows', 'blacks', 'whites', 'vibrance'];
62
+ const hasApproximation = hasUnsupportedSection || Boolean(basic) && unsupportedKeys.some(key => {
63
+ const value = basic?.[key];
64
+ return Number.isFinite(value) && value !== 0;
65
+ });
66
+ return { filter: parts.join(' '), hasApproximation };
67
+ }
@@ -180,14 +180,52 @@ export interface AdjustLutV0 {
180
180
  lut: string;
181
181
  intensity?: number;
182
182
  }
183
- export interface AdjustV0 {
183
+ export interface AdjustCurvePointV1 {
184
+ in: number;
185
+ out: number;
186
+ }
187
+ export interface AdjustCurvesV1 {
188
+ master?: AdjustCurvePointV1[];
189
+ r?: AdjustCurvePointV1[];
190
+ g?: AdjustCurvePointV1[];
191
+ b?: AdjustCurvePointV1[];
192
+ }
193
+ export interface AdjustWheelV1 {
194
+ r?: number;
195
+ g?: number;
196
+ b?: number;
197
+ }
198
+ export interface AdjustWheelsV1 {
199
+ lift?: AdjustWheelV1;
200
+ gamma?: AdjustWheelV1;
201
+ gain?: AdjustWheelV1;
202
+ offset?: AdjustWheelV1;
203
+ }
204
+ export interface AdjustHuePointV1 {
205
+ hue: number;
206
+ value: number;
207
+ }
208
+ export interface AdjustHueCurvesV1 {
209
+ hue?: AdjustHuePointV1[];
210
+ sat?: AdjustHuePointV1[];
211
+ luma?: AdjustHuePointV1[];
212
+ }
213
+ export interface AdjustV1 {
184
214
  basic?: AdjustBasicV0;
185
215
  lut?: AdjustLutV0 | null;
216
+ curves?: AdjustCurvesV1;
217
+ wheels?: AdjustWheelsV1;
218
+ hue?: AdjustHueCurvesV1;
186
219
  sections?: {
187
220
  basic?: boolean;
188
221
  lut?: boolean;
222
+ curves?: boolean;
223
+ wheels?: boolean;
224
+ hue?: boolean;
189
225
  };
190
226
  }
227
+ /** @deprecated Use AdjustV1. */
228
+ export type AdjustV0 = AdjustV1;
191
229
  export interface ItemV2Base {
192
230
  id: string;
193
231
  name?: string;
@@ -210,7 +248,7 @@ export interface ItemV2Base {
210
248
  opacity?: number;
211
249
  blend?: BlendModeV2;
212
250
  crop?: CropV2;
213
- adjust?: AdjustV0;
251
+ adjust?: AdjustV1;
214
252
  perspective?: Record<string, unknown>;
215
253
  motion?: MotionV0;
216
254
  animator?: AnimatorV0[];
@@ -461,7 +461,47 @@ function validateCrop(value, path) {
461
461
  }
462
462
  function validateAdjust(value, path) {
463
463
  requireRecord(value, path);
464
- requireExactKeys(value, new Set(['basic', 'lut', 'sections']), path);
464
+ requireExactKeys(value, new Set(['basic', 'lut', 'sections', 'curves', 'wheels', 'hue']), path);
465
+ for (const section of ['curves', 'hue']) {
466
+ if (!hasOwn(value, section))
467
+ continue;
468
+ const channels = value[section];
469
+ const sectionPath = `${path}.${section}`;
470
+ requireRecord(channels, sectionPath);
471
+ const axis = section === 'curves' ? 'in' : 'hue';
472
+ const output = section === 'curves' ? 'out' : 'value';
473
+ const minimum = section === 'curves' ? 2 : 1;
474
+ requireExactKeys(channels, new Set(section === 'curves' ? ['master', 'r', 'g', 'b'] : ['hue', 'sat', 'luma']), sectionPath);
475
+ for (const [channel, points] of Object.entries(channels)) {
476
+ const channelPath = `${sectionPath}.${channel}`;
477
+ if (!Array.isArray(points) || points.length < minimum || points.length > 16) {
478
+ throw invalid(channelPath, `${minimum} から 16 点の配列である必要があります`);
479
+ }
480
+ let previous = -Infinity;
481
+ for (const [index, point] of points.entries()) {
482
+ const pointPath = `${channelPath}[${index}]`;
483
+ requireRecord(point, pointPath);
484
+ requireExactKeys(point, new Set([axis, output]), pointPath);
485
+ requireRange(point[axis], 0, 1, `${pointPath}.${axis}`);
486
+ requireRange(point[output], 0, 1, `${pointPath}.${output}`);
487
+ if (point[axis] <= previous)
488
+ throw invalid(`${pointPath}.${axis}`, '狭義単調増加である必要があります');
489
+ previous = point[axis];
490
+ }
491
+ }
492
+ }
493
+ if (hasOwn(value, 'wheels')) {
494
+ requireRecord(value.wheels, `${path}.wheels`);
495
+ const ranges = { lift: 0.25, gamma: 0.5, gain: 0.5, offset: 0.1 };
496
+ requireExactKeys(value.wheels, new Set(Object.keys(ranges)), `${path}.wheels`);
497
+ for (const [wheel, channels] of Object.entries(value.wheels)) {
498
+ const wheelPath = `${path}.wheels.${wheel}`;
499
+ requireRecord(channels, wheelPath);
500
+ requireExactKeys(channels, new Set(['r', 'g', 'b']), wheelPath);
501
+ for (const [channel, amount] of Object.entries(channels))
502
+ requireRange(amount, -ranges[wheel], ranges[wheel], `${wheelPath}.${channel}`);
503
+ }
504
+ }
465
505
  if (hasOwn(value, 'basic')) {
466
506
  requireRecord(value.basic, `${path}.basic`);
467
507
  const basicKeys = new Set([
@@ -485,7 +525,7 @@ function validateAdjust(value, path) {
485
525
  }
486
526
  if (hasOwn(value, 'sections')) {
487
527
  requireRecord(value.sections, `${path}.sections`);
488
- const sectionKeys = new Set(['basic', 'lut']);
528
+ const sectionKeys = new Set(['basic', 'lut', 'curves', 'wheels', 'hue']);
489
529
  requireExactKeys(value.sections, sectionKeys, `${path}.sections`);
490
530
  for (const key of sectionKeys) {
491
531
  if (hasOwn(value.sections, key) && typeof value.sections[key] !== 'boolean') {
@@ -34,3 +34,4 @@ export * from './cut-ranges';
34
34
  export * from './adjust-css-approx';
35
35
  export { parseEdit } from './migrate/legacy-parse';
36
36
  export { LegacyEditVersionError } from './migrate/error';
37
+ export * from './adjust-css-visual';
@@ -29,6 +29,7 @@ __exportStar(require("./caption-window"), exports);
29
29
  __exportStar(require("./caption-clock"), exports);
30
30
  __exportStar(require("./timeline-map"), exports);
31
31
  __exportStar(require("./caption-display"), exports);
32
+ // Includes AdjustV1 and its curve/wheel/hue types; AdjustV0 remains an alias.
32
33
  __exportStar(require("./edit-v2"), exports);
33
34
  __exportStar(require("./edit-v2-item-write"), exports);
34
35
  __exportStar(require("./internal-model"), exports);
@@ -56,3 +57,4 @@ var legacy_parse_1 = require("./migrate/legacy-parse");
56
57
  Object.defineProperty(exports, "parseEdit", { enumerable: true, get: function () { return legacy_parse_1.parseEdit; } });
57
58
  var error_1 = require("./migrate/error");
58
59
  Object.defineProperty(exports, "LegacyEditVersionError", { enumerable: true, get: function () { return error_1.LegacyEditVersionError; } });
60
+ __exportStar(require("./adjust-css-visual"), exports);
@@ -23,3 +23,4 @@ export declare function findActiveResolvedCaption<T extends {
23
23
  start: number;
24
24
  end: number;
25
25
  }>(cues: readonly T[], outputTime: number): T | undefined;
26
+ export * from './adjust-css-visual';
@@ -37,6 +37,7 @@ var AkariEditKernel = (() => {
37
37
  captionClockDomainOf: () => captionClockDomainOf,
38
38
  captionWindowSeconds: () => captionWindowSeconds,
39
39
  composeEnvelopesDb: () => composeEnvelopesDb,
40
+ computeAdjustCssVisual: () => computeAdjustCssVisual,
40
41
  computeDuckEnvelope: () => computeDuckEnvelope,
41
42
  computeDuckIntervals: () => computeDuckIntervals,
42
43
  computeTransitionVisual: () => computeTransitionVisual,
@@ -2153,7 +2154,44 @@ var AkariEditKernel = (() => {
2153
2154
  }
2154
2155
  function validateAdjust(value, path) {
2155
2156
  requireRecord(value, path);
2156
- requireExactKeys(value, /* @__PURE__ */ new Set(["basic", "lut", "sections"]), path);
2157
+ requireExactKeys(value, /* @__PURE__ */ new Set(["basic", "lut", "sections", "curves", "wheels", "hue"]), path);
2158
+ for (const section of ["curves", "hue"]) {
2159
+ if (!hasOwn(value, section)) continue;
2160
+ const channels = value[section];
2161
+ const sectionPath = `${path}.${section}`;
2162
+ requireRecord(channels, sectionPath);
2163
+ const axis = section === "curves" ? "in" : "hue";
2164
+ const output = section === "curves" ? "out" : "value";
2165
+ const minimum = section === "curves" ? 2 : 1;
2166
+ requireExactKeys(channels, new Set(section === "curves" ? ["master", "r", "g", "b"] : ["hue", "sat", "luma"]), sectionPath);
2167
+ for (const [channel, points] of Object.entries(channels)) {
2168
+ const channelPath = `${sectionPath}.${channel}`;
2169
+ if (!Array.isArray(points) || points.length < minimum || points.length > 16) {
2170
+ throw invalid(channelPath, `${minimum} \u304B\u3089 16 \u70B9\u306E\u914D\u5217\u3067\u3042\u308B\u5FC5\u8981\u304C\u3042\u308A\u307E\u3059`);
2171
+ }
2172
+ let previous = -Infinity;
2173
+ for (const [index, point] of points.entries()) {
2174
+ const pointPath = `${channelPath}[${index}]`;
2175
+ requireRecord(point, pointPath);
2176
+ requireExactKeys(point, /* @__PURE__ */ new Set([axis, output]), pointPath);
2177
+ requireRange(point[axis], 0, 1, `${pointPath}.${axis}`);
2178
+ requireRange(point[output], 0, 1, `${pointPath}.${output}`);
2179
+ if (point[axis] <= previous) throw invalid(`${pointPath}.${axis}`, "\u72ED\u7FA9\u5358\u8ABF\u5897\u52A0\u3067\u3042\u308B\u5FC5\u8981\u304C\u3042\u308A\u307E\u3059");
2180
+ previous = point[axis];
2181
+ }
2182
+ }
2183
+ }
2184
+ if (hasOwn(value, "wheels")) {
2185
+ requireRecord(value.wheels, `${path}.wheels`);
2186
+ const ranges = { lift: 0.25, gamma: 0.5, gain: 0.5, offset: 0.1 };
2187
+ requireExactKeys(value.wheels, new Set(Object.keys(ranges)), `${path}.wheels`);
2188
+ for (const [wheel, channels] of Object.entries(value.wheels)) {
2189
+ const wheelPath = `${path}.wheels.${wheel}`;
2190
+ requireRecord(channels, wheelPath);
2191
+ requireExactKeys(channels, /* @__PURE__ */ new Set(["r", "g", "b"]), wheelPath);
2192
+ for (const [channel, amount] of Object.entries(channels)) requireRange(amount, -ranges[wheel], ranges[wheel], `${wheelPath}.${channel}`);
2193
+ }
2194
+ }
2157
2195
  if (hasOwn(value, "basic")) {
2158
2196
  requireRecord(value.basic, `${path}.basic`);
2159
2197
  const basicKeys = /* @__PURE__ */ new Set([
@@ -2183,7 +2221,7 @@ var AkariEditKernel = (() => {
2183
2221
  }
2184
2222
  if (hasOwn(value, "sections")) {
2185
2223
  requireRecord(value.sections, `${path}.sections`);
2186
- const sectionKeys = /* @__PURE__ */ new Set(["basic", "lut"]);
2224
+ const sectionKeys = /* @__PURE__ */ new Set(["basic", "lut", "curves", "wheels", "hue"]);
2187
2225
  requireExactKeys(value.sections, sectionKeys, `${path}.sections`);
2188
2226
  for (const key of sectionKeys) {
2189
2227
  if (hasOwn(value.sections, key) && typeof value.sections[key] !== "boolean") {
@@ -3630,6 +3668,55 @@ var AkariEditKernel = (() => {
3630
3668
  return value !== null && typeof value === "object" && !Array.isArray(value);
3631
3669
  }
3632
3670
 
3671
+ // src/adjust-css-visual.ts
3672
+ function computeAdjustCssVisual(adjust, transitionFilter) {
3673
+ const source = adjust && typeof adjust === "object" && !Array.isArray(adjust) ? adjust : null;
3674
+ const rawBasic = source && source.sections?.basic !== false ? source.basic : null;
3675
+ const basic = rawBasic && typeof rawBasic === "object" && !Array.isArray(rawBasic) ? rawBasic : null;
3676
+ const rawTransition = typeof transitionFilter === "string" ? transitionFilter.trim() : "";
3677
+ const transition = rawTransition === "none" ? "" : rawTransition;
3678
+ const clamp01 = (value) => Number.isFinite(value) ? Math.max(0, Math.min(1, value)) : 0;
3679
+ const hasWheels = source?.sections?.wheels !== false && ["lift", "gamma", "gain", "offset"].some((wheel) => ["r", "g", "b"].some((channel) => {
3680
+ const value = source?.wheels?.[wheel]?.[channel];
3681
+ return Number.isFinite(value) && value !== 0;
3682
+ }));
3683
+ const hasCurves = source?.sections?.curves !== false && ["master", "r", "g", "b"].some((channel) => {
3684
+ const raw = source?.curves?.[channel];
3685
+ if (raw == null) return false;
3686
+ const points = raw.map((point) => ({ in: clamp01(point.in), out: clamp01(point.out) })).sort((a, b) => a.in - b.in);
3687
+ return !(points.length === 2 && Math.abs(points[0].in) < 1e-5 && Math.abs(points[0].out) < 1e-5 && Math.abs(points[1].in - 1) < 1e-5 && Math.abs(points[1].out - 1) < 1e-5);
3688
+ });
3689
+ const hasHue = source?.sections?.hue !== false && ["hue", "sat", "luma"].some((channel) => (source?.hue?.[channel] ?? []).some((point) => Math.abs((Number.isFinite(point.value) ? clamp01(point.value) : 0.5) - 0.5) > 1e-4));
3690
+ const hasUnsupportedSection = hasWheels || hasCurves || hasHue;
3691
+ if (!basic && !transition && !hasUnsupportedSection) return null;
3692
+ const exposure = basic && Number.isFinite(basic.exposure) ? basic.exposure : 0;
3693
+ const contrast = basic && Number.isFinite(basic.contrast) ? basic.contrast : 0;
3694
+ const saturation = basic && Number.isFinite(basic.saturation) ? basic.saturation : 0;
3695
+ const temperature = basic && Number.isFinite(basic.temperature) ? basic.temperature : 0;
3696
+ const parts = [];
3697
+ if (Math.abs(exposure) > 5e-3) {
3698
+ parts.push("brightness(" + Math.pow(2, exposure).toFixed(2) + ")");
3699
+ }
3700
+ if (Math.abs(contrast) > 5e-3) {
3701
+ parts.push("contrast(" + (1 + contrast).toFixed(2) + ")");
3702
+ }
3703
+ if (Math.abs(saturation) > 5e-3) {
3704
+ parts.push("saturate(" + (1 + saturation).toFixed(2) + ")");
3705
+ }
3706
+ if (temperature > 5e-3) {
3707
+ parts.push("sepia(" + (temperature * 0.3).toFixed(2) + ")");
3708
+ } else if (temperature < -5e-3) {
3709
+ parts.push("hue-rotate(" + (-temperature * 20).toFixed(0) + "deg)");
3710
+ }
3711
+ if (transition) parts.push(transition);
3712
+ const unsupportedKeys = ["tint", "highlights", "shadows", "blacks", "whites", "vibrance"];
3713
+ const hasApproximation = hasUnsupportedSection || Boolean(basic) && unsupportedKeys.some((key) => {
3714
+ const value = basic?.[key];
3715
+ return Number.isFinite(value) && value !== 0;
3716
+ });
3717
+ return { filter: parts.join(" "), hasApproximation };
3718
+ }
3719
+
3633
3720
  // src/webview-kernel.ts
3634
3721
  function findActiveResolvedCaption(cues, outputTime) {
3635
3722
  return cues.find((cue) => cue.start <= outputTime && outputTime < cue.end);