akari-video 0.1.15 → 0.1.17

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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "akari-video",
3
- "version": "0.1.15",
3
+ "version": "0.1.17",
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": {
@@ -521,7 +521,10 @@ ffmpeg の `perspective` フィルタの制約(式に時刻変数を持たな
521
521
  プレビューでは、書き出しの `acrossfade=d=<duration>` のような前後 2 音源の同時混合は行わない
522
522
 
523
523
  ### 2.6 トランジション
524
- - 視覚描画は `fade-black` / `fade-white` のみ(dissolve は尺計算のみ)— 現状の両実装の共通仕様として明文化
524
+ - `dissolve` / `fade-black` / `fade-white` / `reveal-down` / `reveal-up` の 5 種を実描画する。
525
+ dissolve は前後 2 面の opacity、fade は前後面 + 色プレート、reveal は incoming 面の
526
+ `clip-path` で表現する。Shell で video FX rail が有効な場合も同じ opacity / clip-path を
527
+ rail canvas へ鏡写しし、LUT / chroma 適用後の面同士を同じ進行率で合成する
525
528
 
526
529
  ### 2.7 書き込み
527
530
  - edit.json への**すべての書き込み経路は edit-lint を通す**(UI・API・RPC を問わず)。
@@ -557,6 +560,9 @@ ffmpeg の `perspective` フィルタの制約(式に時刻変数を持たな
557
560
  | `cuts[].fx`(2026-08-07 実装・近似あり) | 🟡(§2.4.5。5 種対応、3 種は近似バッジ付き) | ❌(未実装) |
558
561
  | `layers[].keyframes`(2026-08-09 実装) | ✅(§2.4.7。transform/crop は連続補間。perspective は blend:"normal" のみ・書き出しの段階保持とサンプル点で一致) | ✅(§2.4.7。同左) |
559
562
  | `cuts[].static-image-source`(2026-08-12 実装。正本: `contract-2026-08-12-still-image-cut-source-v0.md`) | 🟡(`<img>`/`<video>` 出し分け + preview-engine ClipSession/Timeline の image 対応を実装。framing/freeze/transform は流用。実ブラウザでの対話的スクラブ・複数区間切替の実機検証は未実施。2026-08-17: 静止画が stylesheet の display:none に隠れたまま永久に出ない実機バグ(`img.style.display=''`)を是正) | 🟡(2026-08-17 実装 — task/2026-08-17-shell-still-image-cut-preview。#preview-still + gap と同じ壁時計クロックで表示。cut transform/framing/freeze/選択ドラッグは video のスタイル鏡写しで流用。タイムラインの静止画フィルムストリップ/サムネも同時是正(probeForFilmstrip の duration 必須ガードが静止画分岐を dead code 化していた)。Electron 実機での対話検証は未実施) |
563
+ | `output.look` | ✅(WebGL rail。chroma → LUT、intensity 対応) | ✅(WebGL rail。chroma → LUT、intensity 対応) |
564
+ | `chroma_key`(source / layer) | ✅(WebGL rail。source は背景合成、layer はアルファ抜き) | ✅(WebGL rail。source は背景合成、layer はアルファ抜き) |
565
+ | `audio.master` | 🟡(未裁定・badge のみ) | 🟡(未裁定・badge のみ) |
560
566
 
561
567
  - `cuts[].framing`(静的クロップ / ズームキーフレーム)・`cuts[].freeze`(フリーズ)は
562
568
  `contract-2026-07-22-render-basics.md` #6/#7 としてレンダ(render-cut)に加え、
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "akari-video",
3
- "version": "0.1.15",
3
+ "version": "0.1.17",
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. Run them from a full AKARI Video app installation (normally ~/.akari/app) or a monorepo checkout.]",
5
5
  "type": "module",
6
6
  "files": [
@@ -683,6 +683,21 @@ function validateEditV2(edit, findings) {
683
683
  });
684
684
  }
685
685
 
686
+ if (kind === "html" && Object.hasOwn(item.source, "params")) {
687
+ const params = item.source.params;
688
+ const invalidEntry = isRecord(params)
689
+ ? Object.entries(params).find(([, value]) => typeof value !== "string")
690
+ : ["params", params];
691
+ if (invalidEntry) {
692
+ addFinding(findings, {
693
+ severity: "error",
694
+ check: "v2.html-params",
695
+ message: "HTML source params must be an object whose values are strings",
696
+ path: `${itemPath}.source.params${invalidEntry[0] === "params" ? "" : `.${invalidEntry[0]}`}`,
697
+ });
698
+ }
699
+ }
700
+
686
701
  if (track.lane === "audio") {
687
702
  const role = item.role ?? "sfx";
688
703
  if (Object.hasOwn(item, "gain_db")
@@ -21,6 +21,7 @@ export type PreviewItemWriteCommand = {
21
21
  vars?: UnknownRecord;
22
22
  transform?: PreviewItemTransformPatch;
23
23
  html?: string;
24
+ params?: Record<string, string>;
24
25
  };
25
26
  } | {
26
27
  kind: 'layer';
@@ -49,6 +49,15 @@ function resolveV2Write(parsed, command) {
49
49
  if (typeof command.patch.html === 'string') {
50
50
  htmlPath = source.path;
51
51
  }
52
+ if (command.patch.params) {
53
+ for (const [name, value] of Object.entries(command.patch.params)) {
54
+ if (!name || typeof value !== 'string') {
55
+ throw new Error('HTML params は空でないキーと文字列値である必要があります');
56
+ }
57
+ }
58
+ source.params = { ...source.params, ...command.patch.params };
59
+ editChanged = true;
60
+ }
52
61
  if (command.patch.vars) {
53
62
  source.vars = { ...recordOf(source.vars), ...command.patch.vars };
54
63
  editChanged = true;
@@ -109,6 +118,9 @@ function resolveLegacyWrite(edit, command) {
109
118
  throw new Error(`overlays[].html がファイル参照ではありません: ${command.itemId}`);
110
119
  }
111
120
  let editChanged = false;
121
+ if (command.patch.params) {
122
+ throw new Error('HTML params の書き戻しには edit.json version 2 が必要です');
123
+ }
112
124
  if (command.patch.vars) {
113
125
  overlay.vars = { ...recordOf(overlay.vars), ...command.patch.vars };
114
126
  editChanged = true;
@@ -60,6 +60,7 @@ export interface HtmlSourceV2 {
60
60
  kind: 'html';
61
61
  path: string;
62
62
  vars?: Record<string, unknown>;
63
+ params?: Record<string, string>;
63
64
  }
64
65
  export interface TelopSourceV2 {
65
66
  kind: 'telop';
@@ -239,10 +239,17 @@ function validateItemSource(value, path, sourceIds) {
239
239
  requirePositiveNumber(value.speed, `${path}.speed`);
240
240
  return;
241
241
  case 'html':
242
- requireExactKeys(value, new Set(['kind', 'path', 'vars']), path);
242
+ requireExactKeys(value, new Set(['kind', 'path', 'vars', 'params']), path);
243
243
  requireText(value.path, `${path}.path`);
244
244
  if (hasOwn(value, 'vars'))
245
245
  requireRecord(value.vars, `${path}.vars`);
246
+ if (hasOwn(value, 'params')) {
247
+ requireRecord(value.params, `${path}.params`);
248
+ for (const [name, text] of Object.entries(value.params)) {
249
+ if (typeof text !== 'string')
250
+ throw invalid(`${path}.params.${name}`, '文字列である必要があります');
251
+ }
252
+ }
246
253
  return;
247
254
  case 'telop':
248
255
  requireExactKeys(value, new Set(['kind', 'preset', 'params', 'baked']), path);
@@ -326,10 +333,18 @@ function requireRecord(value, path) {
326
333
  function hasOwn(value, key) {
327
334
  return Object.prototype.hasOwnProperty.call(value, key);
328
335
  }
336
+ const UNKNOWN_KEY_GUIDANCE = {
337
+ emphasis_words: '語レベル演出は captions.json のトップレベル emphasis_words[] へ移してください(契約 contract-2026-08-23-captions-emphasis-words-v0.md)',
338
+ };
339
+ const DEFAULT_UNKNOWN_KEY_GUIDANCE = 'このキーは v2 の語彙にありません。手で編集した場合は取り除くか、.akari/backup/ の原本から復元してください';
329
340
  function requireExactKeys(value, allowed, path) {
330
341
  const unknown = Object.keys(value).filter(key => !allowed.has(key));
331
- if (unknown.length > 0)
332
- throw invalid(path, `未定義キーを使用できません: ${unknown.join(', ')}`);
342
+ if (unknown.length > 0) {
343
+ const guidance = unknown
344
+ .map(key => `${key}: ${UNKNOWN_KEY_GUIDANCE[key] ?? DEFAULT_UNKNOWN_KEY_GUIDANCE}`)
345
+ .join(' / ');
346
+ throw invalid(path, `未定義キーを使用できません: ${unknown.join(', ')}。案内: ${guidance}`);
347
+ }
333
348
  }
334
349
  function requireText(value, path) {
335
350
  if (typeof value !== 'string' || value.trim().length === 0)
@@ -19,6 +19,7 @@ export interface InternalHtmlSource {
19
19
  kind: 'html';
20
20
  /** 断片ファイルのパス、またはインライン HTML。 */
21
21
  html: string;
22
+ params?: Record<string, string>;
22
23
  }
23
24
  export interface InternalTelopSource {
24
25
  kind: 'telop';
@@ -466,7 +466,8 @@ function buildV2VisualItem(item, fps, ref, pathOf, chromaKeyOf, legacyIndexCount
466
466
  case 'html': {
467
467
  const declaration = {
468
468
  id: item.id, html: item.source.path, start: at, duration, track: ref,
469
- ...(item.source.vars !== undefined ? { vars: item.source.vars } : {}), ...common
469
+ ...(item.source.vars !== undefined ? { vars: item.source.vars } : {}),
470
+ ...(item.source.params !== undefined ? { params: item.source.params } : {}), ...common
470
471
  };
471
472
  const value = {
472
473
  id: item.id,
@@ -478,7 +479,10 @@ function buildV2VisualItem(item, fps, ref, pathOf, chromaKeyOf, legacyIndexCount
478
479
  return {
479
480
  item: {
480
481
  id: item.id, atFrames, durationFrames, at, duration,
481
- source: { kind: 'html', html: item.source.path },
482
+ source: {
483
+ kind: 'html', html: item.source.path,
484
+ ...(item.source.params !== undefined ? { params: item.source.params } : {})
485
+ },
482
486
  declaration,
483
487
  legacy: { collection: 'overlays', index: nextLegacyIndex(legacyIndexCounters, 'overlays'), value }
484
488
  }
@@ -18,6 +18,12 @@ Three.js + glTF シーンを決定的な時刻で描画し(`three-runtime.js`
18
18
  | `#overlay-stage` 配下のオーバーレイ DOM の mount/tick/unmount | アプリシェルの DOM 骨格(上部バー・字幕リストパネル・トランスポート・スプリッタ等) |
19
19
  | オーバーレイの選択・ドラッグ移動・拡縮ハンドル・ダブルクリックテキスト編集 | edit.json の実ファイル I/O(`overlay_write` の実装) |
20
20
  | ズーム中の全体フレーム + 現在視野ミニマップ(`#minimap`) | 動画プレーンの再生/シーク、書き出しパイプライン |
21
+ | `video-fx.js` による動画面の画素 FX(LUT / chroma。外部時刻駆動) | 動画プレーン自身の再生・音声・クロック |
22
+
23
+ `video-fx.js` は `<video>` / `<img>` 1 面ごとに宣言がある場合だけ WebGL canvas を重ねる。
24
+ 宣言が無い場合は canvas・WebGL context・rAF 負荷を一切作らない。ホストはメディア要素を再生の
25
+ 正として保ち、タイムライン更新時に `rail.render(t)` を呼ぶ。WebGL 初期化・LUT 解決・画像読込が
26
+ 失敗した rail は canvas を畳んで元のメディア表示へ戻るため、再生と音声を止めない。
21
27
 
22
28
  ホストシェル(新モノレポの `apps/shell/` 等)は、本パッケージが期待する DOM id と
23
29
  `window.akari.*` インターフェースを用意した上で、必要なスクリプトを読み込む。
@@ -10,7 +10,7 @@
10
10
  "README.md"
11
11
  ],
12
12
  "scripts": {
13
- "check": "node --check src/vendor/three-bundle.js && node --check src/vendor/vendor-3d-text-bundle.js && node --check src/three-runtime.js && node --check src/overlay-runtime.js && node --check src/interaction.js && node --check src/minimap.js",
13
+ "check": "node --check src/vendor/three-bundle.js && node --check src/vendor/vendor-3d-text-bundle.js && node --check src/three-runtime.js && node --check src/slot-params.js && node --check src/video-fx.js && node --check src/overlay-runtime.js && node --check src/interaction.js && node --check src/minimap.js",
14
14
  "test": "node --test test-harness/*.test.mjs"
15
15
  }
16
16
  }
@@ -666,9 +666,14 @@
666
666
  "properties": {
667
667
  "kind": { "const": "html" },
668
668
  "path": { "type": "string", "minLength": 1, "pattern": "\\S" },
669
- "vars": { "type": "object" }
669
+ "vars": { "type": "object" },
670
+ "params": {
671
+ "type": "object",
672
+ "properties": {},
673
+ "additionalProperties": { "type": "string" }
674
+ }
670
675
  },
671
- "$comment": "notes-2026-08-18-timeline-latency-and-track-model.md §9。HTML はその場で描画する絵の出どころであり、素材トリムの in/out を持たない。"
676
+ "$comment": "notes-2026-08-18-timeline-latency-and-track-model.md §9 / contract-2026-08-22-overlay-html-slots.md §1。HTML はその場で描画する絵の出どころであり、素材トリムの in/out を持たない。params は任意のスロット名をキーにできる一方、値を string のみに閉じる。"
672
677
  },
673
678
  "itemSourceTelopV2": {
674
679
  "type": "object",
@@ -0,0 +1,23 @@
1
+ {
2
+ "version": 2,
3
+ "output": { "width": 1920, "height": 1080, "fps": 30 },
4
+ "sources": [],
5
+ "tracks": [
6
+ {
7
+ "id": "v-html",
8
+ "lane": "visual",
9
+ "items": [
10
+ {
11
+ "id": "chapter-1",
12
+ "at": 0,
13
+ "duration": 90,
14
+ "source": {
15
+ "kind": "html",
16
+ "path": "overlays/chapter.html",
17
+ "params": { "title": 1 }
18
+ }
19
+ }
20
+ ]
21
+ }
22
+ ]
23
+ }
@@ -0,0 +1,23 @@
1
+ {
2
+ "version": 2,
3
+ "output": { "width": 1920, "height": 1080, "fps": 30 },
4
+ "sources": [],
5
+ "tracks": [
6
+ {
7
+ "id": "v-html",
8
+ "lane": "visual",
9
+ "items": [
10
+ {
11
+ "id": "chapter-1",
12
+ "at": 0,
13
+ "duration": 90,
14
+ "source": {
15
+ "kind": "html",
16
+ "path": "overlays/chapter.html",
17
+ "params": { "title": "第1章", "subtitle": "問題の本質" }
18
+ }
19
+ }
20
+ ]
21
+ }
22
+ ]
23
+ }
@@ -134,6 +134,17 @@ test("minimum v2 fixture is valid", () => {
134
134
  assert.equal(validate(fixture("edit-v2-minimal-valid")), true, JSON.stringify(validate.errors, null, 2));
135
135
  });
136
136
 
137
+ test("HTML source params accepts arbitrary slot names but only string values", () => {
138
+ const valid = fixture("edit-v2-html-params-valid");
139
+ assert.equal(validate(valid), true, JSON.stringify(validate.errors, null, 2));
140
+
141
+ const invalid = fixture("edit-v2-html-params-invalid");
142
+ assert.equal(validate(invalid), false);
143
+ assert.ok(validate.errors?.some(error =>
144
+ error.instancePath.endsWith("/source/params/title") && error.keyword === "type"
145
+ ), JSON.stringify(validate.errors, null, 2));
146
+ });
147
+
137
148
  test("editV2 rejects removed top-level vocabulary as additional properties", () => {
138
149
  for (const [key, extension] of [
139
150
  ["beats", []],
@@ -46,12 +46,36 @@ YouTube の safe zone も全端末保証ではない。オーガニック投稿
46
46
  放送の左上・右上に常駐する番組名/コーナー名ラベル。番組タイトル + 現在の章タイトルを出し、章立てに追従させる定型。**素材ライブラリに入れず、この定型から都度生成してよい**(プロジェクト固有のテキスト差し替えが本体で、再利用価値は構造にしかないため)。
47
47
 
48
48
  - **HTML/CSS で作る。画像生成にしない**: 章ごとのテキスト差し替えが必要で、画像だと章数分の生成と文字品質リスク(画像生成の日本語誤字)を抱える。HTML なら 1 断片 + 章ごとのオーバーレイエントリで済み、プレビューと書き出しが同一ソースを通る
49
- - **章追従の実装**: 同じ断片を章数分コピーし、章タイトルのテキストと `data-start` / `data-duration` を章の区間に合わせる(1 章 = 1 オーバーレイエントリ)。テキストは**断片 DOM に直接置く**(CSS 変数でのテキスト保持は禁止。変更は html パッチ / contenteditable 経由)
49
+ - **章追従の実装**: スロット付きの同じ断片を章数分参照し、章タイトルを各 HTML アイテムの `source.params`、区間を `at` / `duration` に持たせる(1 章 = 1 オーバーレイアイテム)。固定文言だけの既存断片は従来どおり DOM 直書きでもよい。CSS 変数へテキストを持たせない
50
50
  - **構造の目安**: ルート 1 つ + プレート(番組名の行 + 章名の行、または 1 行連結)。位置は外側コンテナの transform(ランタイム所有)で決め、断片は見た目だけを持つ
51
51
  - **常駐物の抑制**: 常時表示なので主張を抑える。小さめの `--font-size`、半透明の地、画面の隅で被写体・字幕・映像内 UI と重ねない。字幕(下側)と同時表示が前提なので、下側には置かない
52
52
  - knobs の例: `--plate-bg`、`--accent`、`--font-size`、`--radius`。既定値は `var(--name, fallback)` で公開する
53
53
  - 章替わりの出入りは短い opacity / translate のみ。保持中は動かさない(アニメーション節の原則どおり)
54
54
 
55
+ ## テキストスロット(data-akari-slot)
56
+
57
+ 1 本の HTML を複数アイテムで共有し、文字だけを `edit.json` に分離するときに使う。
58
+
59
+ ```html
60
+ <span data-akari-slot="title">既定タイトル</span>
61
+ ```
62
+
63
+ ```json
64
+ {
65
+ "source": {
66
+ "kind": "html",
67
+ "path": "overlays/chapter-tag.html",
68
+ "params": { "title": "第1章 問題の本質" }
69
+ }
70
+ }
71
+ ```
72
+
73
+ - **宣言**: 差し替えるテキスト要素そのものへ `data-akari-slot="<name>"` を付ける。スロット内はプレーンテキストだけにし、子要素や別スロットを入れ子にしない。ランタイムは値を `textContent` へ入れるため、`<b>` 等を params に書いてもタグにはならない
74
+ - **既定テキスト**: 要素内へ必ず人が読める既定値を書く。対応する params キーが無いアイテムでは、その既定値がそのまま表示される
75
+ - **命名**: `^[a-z][a-z0-9_-]*$` の意味名を使う(例: `title`、`chapter-title`、`speaker_name`)。連番や表示文言そのものを名前にせず、同じ名前を複数要素へ使う場合は同じ文字を表示する意図に限る
76
+ - **共有境界**: 色・余白・書体・アニメーションはテンプレ HTML/CSS、インスタンスごとの文字は `source.params` に置く。スロットをダブルクリック編集した場合も、テンプレファイルではなく params だけが更新される
77
+ - **非スロットとの互換**: `data-akari-slot` の無い断片は従来どおり DOM の文字を編集し、参照先 HTML ファイルへ書き戻す
78
+
55
79
  ## アニメーション
56
80
 
57
81
  - 字幕の出入りは短い opacity / translate を使い、保持中は動かさない。