akari-video 0.1.74 → 0.1.76

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 (34) hide show
  1. package/package.json +1 -1
  2. package/vendor/.akari-capability-sources.json +2 -0
  3. package/vendor/docs/contract-2026-07-13-asset-library.md +34 -0
  4. package/vendor/docs/contract-2026-08-30-edit-json-v2-object-tree-v0.md +6 -4
  5. package/vendor/docs/contract-2026-09-13-generation-v0.md +15 -3
  6. package/vendor/packages/akari-launcher/package.json +1 -1
  7. package/vendor/packages/akari-vibe/README.md +62 -0
  8. package/vendor/packages/akari-vibe/package.json +17 -0
  9. package/vendor/packages/asset-resolver/README.md +117 -4
  10. package/vendor/packages/asset-resolver/bin/akari-assets.mjs +80 -7
  11. package/vendor/packages/asset-resolver/src/add.mjs +298 -0
  12. package/vendor/packages/asset-resolver/src/import-artifacts.mjs +91 -0
  13. package/vendor/packages/asset-resolver/src/library.mjs +89 -6
  14. package/vendor/packages/asset-resolver/src/state.mjs +36 -5
  15. package/vendor/packages/asset-resolver/test/add.test.mjs +488 -0
  16. package/vendor/packages/asset-resolver/test/cli-guards.test.mjs +80 -0
  17. package/vendor/packages/asset-resolver/test/helpers.mjs +2 -0
  18. package/vendor/packages/asset-resolver/test/installed-assets.test.mjs +3 -3
  19. package/vendor/packages/asset-resolver/test/library-migration.test.mjs +2 -1
  20. package/vendor/packages/asset-resolver/test/local-library.test.mjs +171 -0
  21. package/vendor/packages/creator-root/src/index.mjs +52 -8
  22. package/vendor/packages/creator-root/test/library-migration.test.mjs +84 -0
  23. package/vendor/packages/edit-store/lib/caption-display.js +12 -3
  24. package/vendor/packages/edit-store/lib/caption-window.d.ts +2 -0
  25. package/vendor/packages/edit-store/lib/caption-window.js +8 -0
  26. package/vendor/packages/edit-store/lib/edit-v2-item-write.d.ts +4 -0
  27. package/vendor/packages/edit-store/lib/edit-v2-item-write.js +19 -1
  28. package/vendor/packages/edit-store/lib/tree-ops.d.ts +0 -9
  29. package/vendor/packages/edit-store/lib/tree-ops.js +0 -24
  30. package/vendor/packages/edit-store/lib/webview-kernel.js +7 -0
  31. package/vendor/packages/schemas/fixtures/gen-models/openapi/fal_h3-ref.json +2 -2
  32. package/vendor/packages/schemas/gen-models.json +4 -4
  33. package/vendor/packages/schemas/gen-models.schema.json +1 -0
  34. package/vendor/packages/schemas/test/gen-models-reference-tags.test.mjs +44 -0
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "akari-video",
3
- "version": "0.1.74",
3
+ "version": "0.1.76",
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": {
@@ -74,6 +74,8 @@
74
74
  "packages/akari-launcher/README.md",
75
75
  "packages/akari-tools/package.json",
76
76
  "packages/akari-tools/README.md",
77
+ "packages/akari-vibe/package.json",
78
+ "packages/akari-vibe/README.md",
77
79
  "packages/analysis-report/package.json",
78
80
  "packages/analysis-report/README.md",
79
81
  "packages/asset-resolver/package.json",
@@ -423,3 +423,37 @@ catalog に載せる素材は、取得元のライセンスが CC0 相当(帰
423
423
  **人が選んでコピーするか / コードが id で引くか**で置き場を決める。前者は `assets/` か `catalog/`、
424
424
  後者は `presets/`。後者をここへ足すときは、解決するコードのパスと 1:1 で対応させ、その参照箇所を
425
425
  表の INDEX.md に明記する。
426
+
427
+
428
+ ## 手持ち素材の登録(2026-09-22)
429
+
430
+ `akari-assets add --plan / --apply` は利用者の生ファイルを置き場へ複製する。
431
+ 出どころは meta.json の既存 tags に記録し、スキーマは増やさない。
432
+
433
+ | 機械用タグ | 意味 |
434
+ | --- | --- |
435
+ | `origin:own` / `origin:site` | ローカル取り込み / サイトからの取り込み |
436
+ | `site:<id>` | サイト識別子 |
437
+ | `folder:<名前>` | 取り込み元として渡されたフォルダ名 |
438
+ | `license:subscription` | サブスクリプション由来 |
439
+ | `pack:<id>` | 素材セット。置き場直下の packs.json は catalog/packs.json と同型 |
440
+
441
+ 一覧ではこれらを `machineTags` に分け、表示・検索用の `tags` に残さない。
442
+ 出どころはカタログ掲載(lab)、明示 origin タグ(site / own)、AKARI 配布元の source.url(lab)、
443
+ その他の source.url あり(site)、それ以外(own)の順で決める。
444
+ AKARI 配布元は URL を解析し、ホスト github.com かつパスの最初のセグメントが AkariLabs、
445
+ またはホスト akari-oss.app とそのサブドメインで判定する。壊れた URL は site とする。
446
+ 素材ディレクトリの `CREDIT.txt` はクレジット文面 1 行。文面がある場合は
447
+ `license.attribution_required: true`、source がある場合はその attribution_required も true にする。
448
+
449
+ 取り込みの既定は `license.scope: "private-owned"`、`spdx: "LicenseRef-user-owned"`、
450
+ `ai_training_allowed: false`、`price: 0`。利用者の手持ち素材として保管するための値であり、
451
+ 素材そのものの著作権帰属・商用可否・再配布権を認定するものではない。
452
+ サイト由来は source ブロックに配布ページと元の利用条件を残す。
453
+
454
+ ローカル取り込みは全カテゴリに preview.png を置く。音は ffmpeg の波形、映像は先頭フレームを使い、
455
+ ffmpeg 不在・失敗時は Node 組み込みの zlib で生成した決定的なプレースホルダ PNG にする。
456
+ PNG の画像は元画像を複製し、その他の画像・font・scene3d はプレースホルダを使う。
457
+ still は画像を相対参照する fragment.html、scene3d はモデルを参照する data-akari-3d-scene 宣言の
458
+ fragment.html を生成する。これらを含む素材全体が validate-asset の exit 0 を通った場合だけ登録し、
459
+ 非 0 の場合は failures に記録して素材を残さない。title は元ファイル名から拡張子を除いたものにする。
@@ -91,7 +91,7 @@ updated: 2026-08-30
91
91
  - 字幕は**専用の段ではなくグループ**。HTML 袋と同じ形(袋 = captions.json・子 = 行)。畳める・前後へ動かせる・別グループの中に入れられる・行を「出す」は部品と同じ操作。段は全部無名になる(§2)
92
92
  - 子(行)の時間は captions.json の**ソース秒**のまま(既存契約)。写しの `at` / `duration` は読み込み層がタイムライン写像で導出する(edit.json に焼かない)
93
93
  - **行の分離**: `{ "id": "cap-42", "at": 1210, "duration": 48, "transform": { "y": -120 }, "source": { "kind": "caption", "path": "captions.json", "id": "c-0042" } }`。文字・スタイルは captions.json の行が正本のまま、位置と時間だけ木の側で上書きする
94
- - **テロップに変換**: `source: { "kind": "telop", "preset": "…", "params": { "text": "…" }, "from": "captions.json#c-0042" }`。以後は独立したテロップ(`from` は来歴。元の行は `exclude`)
94
+ - **テロップに変換**: `source: { "kind": "telop", "preset": "…", "params": { "text": "…" }, "from": "captions.json#c-0042" }`。以後は独立したテロップ(`from` は来歴。元の行は `exclude`) **追記**: 「テロップに変換」は GUI の操作としては 2026-09-21 に撤去。`kind:"telop"` の器と `baked` の再生は後方互換で残す。
95
95
  - 同時刻に 2 行あるときの表示(副行)は描画側の規則であってデータではない。編集ミスの重なりは lint warning
96
96
  - **旧形 `tracks[].content: { from: "captions.json" }` は読める(tolerant reader)が deprecated**。読み込み層は袋グループと同じ内部表現に落とす。書き手(shell / スキル)は袋グループ形を出し、`akari migrate` が旧形を袋グループ形に正規化する。lint は旧形に warning `v2.captions-content-deprecated`
97
97
 
@@ -129,7 +129,7 @@ updated: 2026-08-30
129
129
  - **1 トラック = 常に 1 行。** 葉のアイテム(子を持たないもの)は帯の中のチップであって、ヘッダ列の行ではない。ヘッダ列の行になるのは (a) 子を持つ純グループ (b) 展開中の純グループの子 だけ
130
130
  - **袋の子へ届く経路は 2 つ**: (a) 帯のチップを直接クリックして選ぶ (b) ダブルクリックでフォーカスモードに入る(§7 の「中に入る」)。袋を段の上で展開する操作は**無い**
131
131
  - 畳んだ帯に子の位置を刻みで示すのは従来どおり(どこに何があるかは帯で分かる)
132
- - 「出す」(§3.1)・「テロップに変換」(§1.5)は**帯のチップに対する操作**として従来どおり使える(行が無くても届く)
132
+ - 「出す」(§3.1)・「テロップに変換」(§1.5)は**帯のチップに対する操作**として従来どおり使える(行が無くても届く) **追記**: 「テロップに変換」は GUI の操作としては 2026-09-21 に撤去。`kind:"telop"` の器と `baked` の再生は後方互換で残す。
133
133
 
134
134
  **根拠**: 袋は「データがそうなっている入れ物」で、人間が作ったものでも、ばらせるものでもない。それを UI で「グループ」と呼ぶと「ではばらせるのか」「展開する必要があるのか」という**答えのない問い**が立つ。実案件(SFX 30・字幕 30 行)で葉まで行にした結果ヘッダ列が 71 行に膨らみ、この混同が破綻として顕在化した。
135
135
 
@@ -158,7 +158,7 @@ updated: 2026-08-30
158
158
 
159
159
  ## 3. 操作の意味論(データ変換として定義。UI のコマンド・キー割り当ては別契約)
160
160
 
161
- 操作は **出す / まとめる / ばらす** の 3 つ + 切り出し・テロップに変換。**「戻す」は無い**(⌘Z のみ。再グループ化は「まとめる」で足りる)。
161
+ 操作は **出す / まとめる / ばらす** の 3 つ + 切り出し・テロップに変換。**「戻す」は無い**(⌘Z のみ。再グループ化は「まとめる」で足りる)。 **追記**: 「テロップに変換」は GUI の操作としては 2026-09-21 に撤去。`kind:"telop"` の器と `baked` の再生は後方互換で残す。
162
162
 
163
163
  ### 3.1 出す(detach)
164
164
 
@@ -181,6 +181,8 @@ updated: 2026-08-30
181
181
 
182
182
  ### 3.4 切り出し(§1.6)/ テロップに変換(§1.5)
183
183
 
184
+ > 追記: 「テロップに変換」は GUI の操作としては 2026-09-21 に撤去。`kind:"telop"` の器と `baked` の再生は後方互換で残す。以下は撤去前の意味論の記録。
185
+
184
186
  - どちらも一方通行。元ファイルは不変・来歴(`derivedFrom` / `from`)を残す
185
187
 
186
188
  ## 4. 描画 — クローンマスク
@@ -309,7 +311,7 @@ await p.save(); // 正規直列化 → lint ゲート
309
311
  | A3 | `object-tree-render` | クローンマスク(§4)を 3 出口(preview / osr / gpu)に・名札の走査・`style` / `text` | A1 |
310
312
  | A4 | `object-tree-write-and-migrate` | shell / preview-server / スキルの書き込みを A2 の API へ・`content` → 袋グループの migrate・1 レコード 1 行での保存 | A2, A3 |
311
313
  | D | タイムライン木行 | 折りたたみ / D&D 再親化 / ⌘G ⌘⇧G / 段の自動生成・消滅(UI 契約) | A2 |
312
- | F | 字幕 = 袋グループ(UI) | 専用段の廃止・畳んだ帯の刻み表示・行を出す / テロップに変換 | D, A3 |
314
+ | F | 字幕 = 袋グループ(UI) | 専用段の廃止・畳んだ帯の刻み表示・行を出す / テロップに変換。**追記**: 「テロップに変換」は GUI の操作としては 2026-09-21 に撤去。`kind:"telop"` の器と `baked` の再生は後方互換で残す | D, A3 |
313
315
  | H | フォーカスモード | 姉妹契約 §7 | D + 姉妹契約 |
314
316
  | I | SKILL.md 読み方規約 | §5.2 を edit-plan / address-review / analyze-project へ | A2 |
315
317
 
@@ -12,7 +12,10 @@
12
12
  5. 絵コンテはタイムラインの印刷(9/6 §5)。生成の入力にしない
13
13
  6. plan.json の仮枠役(`confidence` / `fill`)は退役。`plan-comments.json` の `pass: "scaffold"` の対象は `slot` から **clip id** へ
14
14
  7. **空の枠**(2026-09-21): 仮枠ツール(F)で空いているところに描いた枠は、prompt 未記入の**文字カード png** を素材に持つ media item(絵のないクリップを文字カード png で表す規則の延長)。0.5 秒刻み・端に吸着・隣に食い込まない・0.5 秒未満にしない。専用トラックは作らない(どのトラックにも置ける)
15
+ 実装: 選択 V / 分割 C / 仮枠 F(従来の A / B も有効)。仮枠はスナップ OFF でも近い可視端・再生ヘッドを 0.5 秒格子より優先し、`assets/generated/frame-<時刻>-<一意接尾辞>.png` と隣の `.meta.json` に保存する。
15
16
  8. **すき間から作る枠**(2026-09-21): すき間をクリック →「あいだを生成」で、すき間の位置と長さの枠を置く。前のクリップの最後のコマ・次のクリップの最初のコマを抽出して両端(②③)に入れる。動画からの抽出は 1 コマを `assets/captures/` へ書き出す(§10)
17
+ 実装: 同じ映像トラックで前後にクリップがある 0.5 秒以上の空きだけをすき間とする。前は `source.out − 1/output.fps`(in 以上)、次は `source.in` の合成前のコマを使い、静止画は元パスを使う。
18
+ 抽出 PNG は `assets/captures/frame-<素材名>-<秒>-<素材・更新時刻等のハッシュ>.png` に保存し、同じ素材・同じ時刻は再利用する。
16
19
  9. 枠は**生成前から尺と場所を持ち、生成物は同じ item に入る**(4 の延長)。メディアパネルは経由しない(タイムライン正本)
17
20
 
18
21
  ## 2. 9 スロット(生成入力の正規形)
@@ -96,8 +99,10 @@ mp4 がまだ無い「動画予定」は、**仮枠の素材の meta**(静止
96
99
  1. **読み手の判定**: `next.kind === "video"` かつ `next.status === "planned"` = 「動画予定」。`next` が無い静止画 = 「画像のまま」= 完成品。文字カード(meta 自体が `planned`)で `next` が無いか prompt が空 = 「空の枠」
97
100
  2. `next.inputs` は §2 の 9 スロットの下書き。`first_frame` は**そのクリップの絵とは限らない**(前のクリップの最後のコマ・キャプチャ・空 = プロンプトだけ)
98
101
  3. **`inputs.frames_or_refs`** = `"frames"` / `"references"`(`next` の下書きだけが持つ欄。§2 ⑦ の `mode` = 元動画のモードとは別物)。最初 / 最後と参照が排他のとき(`frames_and_refs_exclusive: true` の行、または同じ family の i2v 行と ref 行の切替)、**下書きは両側を保持し、送るのは `frames_or_refs` の側だけ**。切り替えで中身を消さない。バリデータはこの欄を見て反対側を送信 body から外し、生成物 meta の `inputs` にはこの欄を書かない(送った側だけが残る)
102
+ 右パネルは同じ family のフレーム行 / 参照行を「最初 / 最後|参照」で切り替え、model ID も相方に替える。参照は種類別の札・カウンタ付きグリッドで、素材パネルから複数選択する。
99
103
  4. **送るとき**: CLI は `next`(または `--inputs`)を読み、従来どおり**生成物の隣**に video meta(`generating`)を書く。このとき **`placeholder: { path, sha256, item_id }`** = その item が今指している素材(静止画 / 文字カード)を必ず書く。item への逆引きは `placeholder` が正、`inputs.first_frame.path` は 9/13 時点の meta のための後方互換。`next` は消さない(「同じ入力でもう一度」の元)
100
104
  5. **状態の優先**: `placeholder` で結線された生成物 meta が `generating` / stale / `failed` ならそれを描く。無ければ `next` の `planned` を描く。`done` で差し替わった後は mp4 の meta が直接当たる(§7)
105
+ done で差し替わった後の作り直し(本番の画質・同じ入力でもう一度)の下書きは、mp4 の meta の `next` に持つ。元の静止画の `next` は変えない。
101
106
  6. `next` の更新は undo に入れない(§3 規則 3 と同じ)。右パネルの編集は即保存
102
107
  7. 移行: `.akari/generation/<itemId>.inputs.json` があり `next` が無いときだけ読み、次の保存で `next` へ移す。新規の書き込みはしない
103
108
  8. ビート表(`akari generate still --spec`)はビートごとに動画予定(最初だけ / 最初→最後)を指定でき、指定があれば CLI が `next` を書く。指定が無ければ「画像のまま」
@@ -129,6 +134,7 @@ mp4 がまだ無い「動画予定」は、**仮枠の素材の meta**(静止
129
134
  }
130
135
  ```
131
136
 
137
+ - 参照の順序記法は行ごとの `tag`(接頭辞)+ `tag_joiner`(省略時は空文字)+ 1 始まりの番号。Seedance は `@Image` + 空文字 → `@Image1`、H3 は `Image` + 空白 → `Image 1`。動画・音声も同様。`tag` の末尾に空白は入れない
132
138
  - `inputs.first_frame` / `last_frame` は `"required"` / `"optional"` / `"none"` の 3 値
133
139
  - `duration.format` は `{type: "integer"}` / `{type: "string"}` / `{type: "string", suffix: "s"}` / `{type: "string", auto: true}`
134
140
  - `audio_out` は `true`(切替可)/ `"always"`(欄なしで付く)/ `false`
@@ -148,6 +154,8 @@ mp4 がまだ無い「動画予定」は、**仮枠の素材の meta**(静止
148
154
 
149
155
  Kling v3 standard i2v / Kling v3 pro i2v / Veo 3.1 first-last / Veo 3.1 reference / Seedance 2.0 i2v / Seedance 2.0 reference / Seedance 2.5 i2v / H3 i2v / H3 reference / Wan 2.7 i2v / Grok Imagine i2v / Vidu Q3 i2v。画像: codex-image / nano-banana-pro edit。Sora は OpenAI 直アダプタが出来るまで入れない。
150
156
 
157
+ 動画の登録済みアダプタ(2026-09-22)は `fal:h3-i2v`・`fal:h3-ref`・`fal:kling-v3-standard-i2v`・`fal:kling-v3-pro-i2v`・`fal:seedance-2.0-i2v`・`fal:seedance-2.0-ref`・`fal:veo-3.1-flf` の 7 行。カタログ収載だけでは送信できない。H3 reference は OpenAPI に従い `Image 1` / `Video 1` / `Audio 1` で参照を名指しする。`first_frame` / `last_frame` は拒否する。
158
+
151
159
  ### 4-4. 鮮度とドリフト
152
160
 
153
161
  - `as_of` 必須。UI の費用表示に日付を添える
@@ -160,6 +168,8 @@ Kling v3 standard i2v / Kling v3 pro i2v / Veo 3.1 first-last / Veo 3.1 referenc
160
168
 
161
169
  各アダプタは 9 スロット × 出力ノブの**全セル**に「引数名 + 書式」か「拒否」を持つ。テストは全セルを網羅する。
162
170
 
171
+ 対応一覧は §4-3 の 6 行。Seedance 2.0 reference は画像・動画・音声参照を写し、`first_frame` / `last_frame` / `seed` は拒否する。参照用の OpenAPI 根拠は `packages/generate/test/fixtures/openapi/`(2026-09-22 取得・URL と SHA-256 は同 README)に保存する。H3 reference は記法の不一致が解消するまで登録しない。
172
+
163
173
  ### 5-2. 尺の書式(スパイク実測)
164
174
 
165
175
  | モデル | 送る形 |
@@ -171,9 +181,10 @@ Kling v3 standard i2v / Kling v3 pro i2v / Veo 3.1 first-last / Veo 3.1 referenc
171
181
 
172
182
  ### 5-3. 参照の渡し方
173
183
 
174
- - 画像は data URI で送ってよい(5.2 MB 24 秒)。**20 MB 超は fal storage へ先にアップロード**(後日)
175
- - 順序タグ(@Image1 …)はアダプタが配列順から生成して prompt に付ける。名前 + 役割(PixVerse)は要素の `name` / `role` から
176
- - 参照音声は `range_s` で切り出してから送る(クリップ範囲だけ)
184
+ - Seedance 2.0 reference H3 reference の画像・動画・音声は配列順のまま data URI で送る。**20 MB 超は送らず error**(fal storage へのアップロードは後日)。OpenAPI の上限は画像 9・動画 3・音声 3、全種合計 12 ファイル。Seedance の音声参照には画像か動画が 1 本以上必要。H3 は 2026-09-22 取得の OpenAPI に従い音声単独も可
185
+ - 引数名は Seedance `image_urls` / `video_urls` / `audio_urls`、H3 が `reference_image_urls` / `reference_video_urls` / `reference_audio_urls`。OpenAPI の正本は `packages/schemas/fixtures/gen-models/openapi/`。generate のテストも相対 URL でこの正本を直接読む。`packages/generate/test/fixtures/openapi/` は取得記録の README のみ(取得日・出典・変換方法を記録)
186
+ - 順序タグはカタログ行の `tag` + `tag_joiner`(省略時は空文字)+ 配列順の番号(1 始まり)。prompt の `@画像N` / `@動画N` / `@音声N` を、Seedance では `@ImageN` / `@VideoN` / `@AudioN`、H3 では `Image N` / `Video N` / `Audio N` へ置換する。該当種別の本数を超える番号や 0 以下・非整数は送らず error。provider 記法の直書きは `@` 付きだけ番号を検査する。H3 の素の英語は検査・置換せず、通常文の `Image 1 of 3` や `Image 3` を誤って拒否しない。名指しが無ければ prompt に何も足さない。名前 + 役割(PixVerse)は要素の `name` / `role` から
187
+ - 参照音声は `range_s` があれば media-bin の ffmpeg で切り出してから送る(クリップ範囲だけ)。指定が無ければ元の音声をそのまま送る。切り出しの一時ファイルは成功・失敗ともに削除する
177
188
 
178
189
  ## 6. 状態と見え方
179
190
 
@@ -233,6 +244,7 @@ Kling v3 standard i2v / Kling v3 pro i2v / Veo 3.1 first-last / Veo 3.1 referenc
233
244
  | 費用承認 | 有償生成の実行前ゲート |
234
245
  | 判子 | 書き出しの 1 回(9/6) |
235
246
  | 事実帯 | モデル選択の 1 行(価格・尺・入力・音声・較正)。レーダーの代わり |
247
+ | 下書き → 本番の画質 | `price.by_resolution` に異なる単価が 2 つ以上ある video 行だけ対応。下書きは最安解像度。チェック ON は `next.output.resolution` を最安にして解像度を固定、OFF は直前の選択(無ければカタログ順の既定)へ戻し、見積を再計算する。生成物の `output.resolution` が最安なら下書きと判定し、カタログ・meta に判定用の欄は追加しない。done の `placeholder` を辿った元静止画が存在し `next` を保持している場合だけ「本番の画質にする…」を表示。解像度の初期値は下書き前の選択かカタログの既定で、価格順の 2 番目は使わない。より高い画質を選択 → 見積 → 既存の費用承認 → 同じ `next.inputs`(対応モデルで meta に seed があれば再使用)で生成する。既存の `writeGenerationDraft` で現在の mp4 meta の既存 `next` 欄へ送信下書きを保存し、CLI の `--item` 経路を再利用する。元静止画の `next` は消さず、同じ item の素材だけを §7 に従って差し替え、映像・色の設定を保持する。placeholder が過去の mp4 を指す場合も元静止画まで辿る。表示文言は「同じ入力でもう一度、高い画質で生成します(絵は変わることがあります)」。 |
236
248
 
237
249
  ## 10. コマ保存(キャプチャ)(2026-09-21)
238
250
 
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "akari-video",
3
- "version": "0.1.74",
3
+ "version": "0.1.76",
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": [
@@ -0,0 +1,62 @@
1
+ # AKARI バイブ
2
+
3
+ このディレクトリは内部リポジトリの正本から自動生成する配布物です。直接編集しないでください。
4
+ 変更は内部へ戻し、検査を通して書き出し直します。版はシェルと一緒に管理します。
5
+ 判断は `https://akari.video/api/vibe` のサービスで行い、その実装は同梱しません。
6
+
7
+ ## 起動
8
+
9
+ シェルの backend が `node bin/akari-vibe.mjs --serve` を子プロセスとして起動します。
10
+ Electron では `process.execPath` と `ELECTRON_RUN_AS_NODE=1` を使います。
11
+ シェルが32バイトの乱数を作り、標準入力の最初の行に
12
+ `{"token":"<64桁の16進文字列>"}` と改行を渡します。合言葉を引数や環境変数へ置かないでください。
13
+ 標準入力は接続中開いたままにします。閉じると係は1秒以内に終了します。
14
+
15
+ 待受けは `127.0.0.1:0`。標準出力の最初の行は
16
+ `{"type":"listening","port":<整数>,"protocol":0}` です。
17
+ シェルは Bearer 認証で `/companion/manifest?nonce=<32桁>` を取得し、
18
+ 合言葉を鍵とした nonce の HMAC-SHA256 を照合します。
19
+ manifest の `panelPath` が返す `?k=` を枠の通信に付けます。合言葉と panelPath はログに残しません。
20
+ `companion.json` は書かず、HUD の待受けも開きません。記録も既定では書きません。
21
+ 同時に動く係は1つとし、シェルがウィンドウの切替・終了時に停止を管理します。
22
+
23
+ ## 資源と設定
24
+
25
+ 公開モノレポ、アプリの Resources とも `packages/akari-vibe` と
26
+ `packages/edit-store/lib` を並べ、根に `presets` を配置します。
27
+ 聞き取りのビルド済みヘルパーは `native/bin/akari-vibe-stt` に置きます。
28
+ 対応外 OS やヘルパー未同梱でも係は起動し、聞き取りが使えないことを枠に表示します。
29
+
30
+ | 環境変数 | 用途 |
31
+ | --- | --- |
32
+ | `AKARI_PUBLIC_REPO` | 公開資源の根。通常は相対配置から見つけます |
33
+ | `AKARI_EDIT_STORE_LIB` | edit-store の index.js の明示指定。相対配置より優先します |
34
+ | `AKARI_HOME` | Lab 接続情報の親。既定は `~/.akari` |
35
+ | `AKARI_VOICE_JUDGE_TOKEN` | Lab 接続の鍵を明示指定 |
36
+ | `OPENROUTER_API_KEY` | 利用者の鍵を明示指定 |
37
+ | `AKARI_CREDENTIALS_FILE` | 利用者の鍵を読む env ファイル |
38
+ | `AKARI_VOICE_JUDGE_URL` | 判断 API の接続先。製品の既定は上記サービス |
39
+ | `AKARI_VOICE_JUDGE_TRUST_HOST` | `1` のときだけ別の HTTPS ホストへの鍵の送信を明示許可 |
40
+ | `AKARI_VIBE_STT_BIN` | 聞き取りヘルパーの明示指定 |
41
+ | `AKARI_VIBE_LOG_DIR` | 明示した場合のみ記録する場所 |
42
+ | `AKARI_VIBE_DEV` | `1` の場合は開発用の口とヘルパーのローカルビルドを有効化 |
43
+ | `AKARI_LIBRARY_SOURCE` / `AKARI_CATALOG_JSON` | 素材台帳の方式とファイル |
44
+
45
+ Lab 接続の鍵は `AKARI_VOICE_JUDGE_TOKEN`、次に `AKARI_HOME/store-credentials.json`
46
+ (未指定なら `~/.akari/store-credentials.json`)の token を使います。
47
+ 利用者の OpenRouter の鍵は `OPENROUTER_API_KEY` → `AKARI_CREDENTIALS_FILE` →
48
+ `~/.config/akari-video/credentials.env` → 旧 `~/.config/akari/openrouter.env` の順に探します。
49
+ ファイルは呼び出すたびに読み直します。登録後の再起動は不要です。
50
+
51
+ 通常、保存済みの両方の鍵を送る相手は HTTPS の `akari.video` だけです。
52
+ Lab 接続の鍵を Authorization、利用者の鍵を `x-akari-provider-key` に載せ、
53
+ サービスの `/judge` へ送ります。係からモデル提供先へ直接送ることはありません。
54
+ リダイレクトは拒否します。別ホストへの送信は上表の明示許可がある場合だけです。
55
+ ローカル接続先には保存済みの鍵を送らず、明示した Lab token のみ送ります。
56
+ 戻り値と通信エラーから鍵を除去し、状態確認では登録の有無だけを返します。
57
+
58
+ ## テスト
59
+
60
+ 依存のインストールは不要です。公開のモノレポ配置で
61
+ `node --test test/*.test.mjs` を実行します。
62
+ 判断 API は呼びません。鍵の試験は fetch の差し替え、起動の試験は一時ホームと OS が選ぶポートを使います。
@@ -0,0 +1,17 @@
1
+ {
2
+ "name": "@akari-video/akari-vibe",
3
+ "private": true,
4
+ "version": "0.0.0",
5
+ "type": "module",
6
+ "scripts": {
7
+ "test": "node --test test/*.test.mjs"
8
+ },
9
+ "description": "@akari-video/akari-vibe [akari-video npm vendor: bin/akari-vibe.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.]",
10
+ "akariVideoVendor": {
11
+ "execution": "reference-only",
12
+ "omittedBin": {
13
+ "akari-vibe": "bin/akari-vibe.mjs"
14
+ },
15
+ "guidance": "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."
16
+ }
17
+ }
@@ -19,7 +19,9 @@
19
19
  ## CLI
20
20
 
21
21
  ```sh
22
- akari-assets list [--category <c>] [--json] # 合成カタログ一覧(取得状態バッジ込み)
22
+ akari-assets list [--category <c>] [--source <lab|site|own>] [--json] # 合成カタログ一覧(取得状態バッジ込み)
23
+ akari-assets add <path...> --plan [--json] # ファイル・フォルダの取り込み計画
24
+ akari-assets add --apply <plan.json> [--json] # 確認済み計画を複製して登録
23
25
  akari-assets fetch <id> [--project <dir>] [--force] # 素材を解決してローカルへ登録
24
26
  akari-assets migrate [--dry-run] # 旧置き場を再移行(dry-run は変更しない)
25
27
  akari-assets sync # カタログを取得してローカルにキャッシュ
@@ -120,9 +122,9 @@ checksums 不一致は、いずれも `AssetResolverError`(`code: 'download_fa
120
122
 
121
123
  `AKARI_ASSETS_CATALOG` がリモート URL のとき、`loadCatalog` は取得成功のたびに
122
124
  `~/.akari/catalog-cache.json` へ自動キャッシュする。オフライン時(fetch 失敗)はこのキャッシュへ
123
- フォールバックする。キャッシュも無い場合は「取得できていない」ことを明示するエラーで止まる
124
- (黙って空のカタログを返したりしない)。ただし `installed.json` に導入済み素材がある場合は、
125
- キャッシュが無くてもその素材だけを `list` / `fetch` できる。`akari-assets sync` はオンライン環境で
125
+ フォールバックする。キャッシュも無い場合、`list` は警告を stderr に出し、置き場の素材と `installed.json` の導入済み素材を返す。
126
+ `AKARI_ASSETS_CATALOG` に指定したローカルファイルが読めない場合も同様。
127
+ `fetch` / `sync` の取得エラーの扱いは従来どおり。`akari-assets sync` はオンライン環境で
126
128
  明示的にキャッシュを温めておくためのコマンド。
127
129
 
128
130
  ## テスト
@@ -170,3 +172,114 @@ root(絶対パス)、state(pending / migrating / done / declined)、deci
170
172
  途中失敗は migrating のまま再開可能。done 後に旧 CLI が追加した素材は `akari-assets migrate` で寄せる。
171
173
  作業場なしは何もしない。OneDrive / Dropbox / iCloud Drive / Google Drive 配下は pending に留め、
172
174
  結果に同期先と総容量を返す。ドロップフォルダと reviews はマシン状態として元の場所に残す。
175
+
176
+
177
+ ## カタログ外の素材と出どころ
178
+
179
+ `list --json` は素材配列を返す。リモートカタログにない `<category>/<id>` も含め、
180
+ 新しい置き場を優先し、従来の置き場も読む。カタログ側に同じ key がある場合は 1 件にまとめる。
181
+ ローカルの表示情報は meta.json、`files: [{name, bytes}]` は実ディレクトリから得る。
182
+ 壊れた meta.json も id を title にした item として残し、`warnings[]` に理由を付ける。
183
+
184
+ 追加フィールド:
185
+
186
+ | フィールド | 意味 |
187
+ | --- | --- |
188
+ | `sourceKind` | カタログ収載・ストア導入は `lab`。それ以外は `origin:site` / `origin:own`、AKARI 配布元の `source.url`(lab)、その他の `source.url`(site)、既定 own の順 |
189
+ | `tags` / `machineTags` | 人向けタグ / `origin:*`・`site:*`・`folder:*`・`pack:*`・`license:subscription` |
190
+ | `folder` / `site` / `subscription` | 機械用タグの値(無ければ null / null / false) |
191
+ | `creditText` | CREDIT.txt の先頭 1 行。無ければ null |
192
+ | `libraryDir` / `addedAt` | ローカル素材の絶対パス / ディレクトリ birthtime(無効なら mtime)の ISO 時刻 |
193
+ | `preview` / `mediaFile` | ローカル素材では `preview.png` / 直下で一意な主メディアのファイル名。無ければ null |
194
+
195
+ AKARI 配布元は URL を解析し、ホスト `github.com` かつパスの最初のセグメントが `AkariLabs`、
196
+ またはホスト `akari-oss.app` とそのサブドメインで判定する。壊れた URL は `site` とする。
197
+
198
+ 主メディアはシェルと同じ一意解決の規則で、複数テイクから勝手に選ばない。
199
+ still の `preview.png` は主メディア候補から除く。音・映像・画像に加え、取り込み対象の
200
+ AIFF・SVG・フォント・glTF も判定する(シェルでの配置可否とは別)。
201
+ `composeState()` は一覧に加え取得エラー等の `warnings[]` を返す。
202
+
203
+ ## ローカル取り込みの JSON 契約
204
+
205
+ ```sh
206
+ akari-assets add /path/to/track.wav /path/to/folder --plan --json > plan.json
207
+ # plan.json の items[].kind / selected と任意の credit / pack を編集
208
+ akari-assets add --apply plan.json --json
209
+ ```
210
+
211
+ plan は置き場に書き込まない。隠しファイル・Thumbs.db・シンボリックリンクを除いて再帰し、
212
+ 5,000 ファイルで打ち切った場合は `truncated: true` と `warnings[]` を返す。
213
+
214
+ ```jsonc
215
+ {
216
+ "schema": "akari-assets-add-plan/v0",
217
+ "items": [{
218
+ "path": "/absolute/path/track.wav", "name": "track.wav", "bytes": 2000000,
219
+ "category": "audio", "kind": "sfx", "durationSec": 14,
220
+ "durationSource": "ffprobe", "ambiguous": true, "proposedId": "track",
221
+ "mtimeMs": 1790000000000, "folder": "Tracks"
222
+ }],
223
+ "duplicates": [], "rejected": [], "truncated": false, "limit": 5000, "warnings": []
224
+ }
225
+ ```
226
+
227
+ - 音: 10 秒未満 = `kind: "sfx"`、30 秒以上 = `"bgm"`、間は `ambiguous: true` + 既定 `"sfx"`。
228
+ 利用者の選択は `items[].kind` を `"bgm"` に変えて反映する。`ambiguous` は判定時の情報として残せる。
229
+ - ffprobe 不在だけはサイズで推定する(1,200,000 bytes 未満 = sfx、4,000,000 bytes 超 = bgm、間は ambiguous)。
230
+ `durationSec: null` / `durationSource: "size"`。ffprobe が動いてエラーを返した音は rejected。
231
+ - 画像 = still、映像 = broll、フォント = font、GLB/glTF = scene3d。これらの kind は category と同じ、
232
+ durationSec / durationSource は null。cube は presets 管轄のため rejected。0 バイト・対応外形式も理由つきで rejected。
233
+ - `duplicates[]` は元ファイル情報 + `status: "duplicate"` + 既存の `category` / `id` / `libraryDir`。
234
+ 同じバイト数の既存ペイロードがある場合だけ、取り込み元とその候補の sha256 を計算・比較する。
235
+ 候補がなければ先頭 512 bytes の読み取り確認のみ行い、hash は計算しない。計算した場合だけ `sha256` を plan に含める。`rejected[]` は元ファイル情報 + `reason`。
236
+ - `folder` は渡されたフォルダ名。単独ファイルには付けない。`selected: false` の item は apply で除外する。
237
+ - `credit` は plan 全体または item に指定でき、改行を空白にして CREDIT.txt へ保存する。
238
+ - `pack: {id, title}` は plan 全体で任意指定。全追加素材に pack タグを付け、ライブラリ直下の
239
+ `packs.json` に `akari-catalog-packs/v0` の行を追加する。混在セットの category は最初に登録した素材のもの。
240
+ - サイト取り込みは plan または item に `origin: "site"`, `site`, `sourceUrl`, `licenseAtSource`,
241
+ `subscription` を指定する。source の acquisition は subscription=true なら login、ほかは direct。
242
+ `sourceUrl` と `licenseAtSource` は必須。既定の own には source を作らない。
243
+
244
+ apply の結果は `{added: [{category,id,libraryDir,warnings?}], duplicates, rejected, failures}`。
245
+ failures は `{path?, reason}`。1 件でも失敗した CLI は exit 1 と結果 JSON を返すが、残りの素材は続ける。
246
+ id は proposedId を使い、衝突時だけ `-2` 以降を足す。元ファイルのサイズ・mtime を再確認し(plan に sha256 があればそれも照合)、
247
+ apply 時に計算した hash と複製後の hash を比較して、
248
+ 同じディスクの一時ディレクトリへ CoW 複製 → 検証 → rename する。コピー元は消さない。
249
+ 失敗した素材の一時ファイルと配置済みファイルは削除する。並行 apply は `.add-lock` で排他し、
250
+ 実行中なら failures を返す(強制終了で残った lock は、実行がないことを確認して除去する)。
251
+
252
+ ### プレビュー・表示用ファイルと厳密な検証
253
+
254
+ apply は全カテゴリで `preview.png` を置く。音は register-drop-folder と同じ関数で波形を生成し、
255
+ 映像は ffmpeg で先頭フレームをサムネイルにする。ffmpeg 不在・実行失敗時は、Node 組み込みの
256
+ zlib で作る決定的なプレースホルダ PNG に置き換え、`added[].warnings` にその旨を返す。
257
+ PNG の still は元画像を複製して preview にする。font / scene3d / PNG 以外の still もプレースホルダを使う。
258
+
259
+ still には実体画像を相対参照する最小の `fragment.html`、scene3d には canvas と
260
+ `data-akari-3d-scene` の model 宣言を持つ最小の `fragment.html` を生成する。
261
+ 生成した素材を既存 `validate-asset.mjs` に渡し、exit 0 の場合だけ登録する。
262
+ 非 0 の診断を警告として許容する例外は設けず、`failures[]` に記録して素材を残さない。
263
+
264
+ ### apply が作る meta.json の既定値
265
+
266
+ | 必須項目 | 既定値 |
267
+ | --- | --- |
268
+ | id | ファイル名由来の proposedId(非 ASCII は短いハッシュ、衝突時は接尾辞) |
269
+ | category | 拡張子で決めた audio / still / broll / font / scene3d |
270
+ | title | 元ファイル名(拡張子を除く。例: mid.wav → mid) |
271
+ | description | 利用者がローカルから取り込んだ素材 |
272
+ | when_to_use | 利用者のプロジェクトでこの素材を使うとき |
273
+ | tags | origin:own。必要時に folder:* / sfx / pack:*。site 由来は origin:site / site:* / license:subscription |
274
+ | knobs | [] |
275
+ | ai_usage | 利用者の利用条件の範囲で使用する。再配布・AI 学習には使用しない。 |
276
+ | requires | [] |
277
+ | provenance | {origin: "利用者がローカルから取り込み", generator: null} |
278
+ | author | user |
279
+ | license | {spdx: "LicenseRef-user-owned", scope: "private-owned", attribution_required: false, ai_training_allowed: false}(credit 指定時だけ attribution_required=true) |
280
+ | price | 0 |
281
+
282
+ `planAdd` の `probe(path, {env})` 注入口は秒数か null(不在)を返し、壊れた音なら throw する。
283
+ 追加テストはこの注入口を使うため、ffprobe / ffmpeg は不要。
284
+
285
+ `planAdd` の `hashFile(path)` 注入口は、同サイズ候補がない場合に呼ばれないことを検証するために使える。
@@ -1,12 +1,16 @@
1
1
  #!/usr/bin/env node
2
- // akari-assets — 素材 resolver v0 の CLI(list / fetch / bundle / sync / browse)。
2
+ // akari-assets — 素材 resolver v0 の CLI(list / add / fetch / bundle / migrate / sync / browse)。
3
3
  //
4
- // akari-assets list [--category <c>] [--json]
4
+ // akari-assets list [--category <c>] [--source <lab|site|own>] [--json]
5
+ // akari-assets add <path...> --plan [--json]
6
+ // akari-assets add --apply <plan.json> [--json]
5
7
  // akari-assets fetch <id> [--project <dir>] [--reference] [--force]
6
8
  // akari-assets bundle --project <dir> [--dry-run]
7
9
  // akari-assets sync
8
10
  // akari-assets browse [--port <n>]
9
11
 
12
+ import { readFile } from 'node:fs/promises';
13
+ import { planAdd, applyAdd } from '../src/add.mjs';
10
14
  import { migrateAssetLibrary } from '../../creator-root/src/index.mjs';
11
15
  import { startBrowseServer } from '../src/browse-server.mjs';
12
16
  import { bundleProjectReferences } from '../src/bundle.mjs';
@@ -19,6 +23,41 @@ function flagValue(args, name) {
19
23
  return i >= 0 && i + 1 < args.length ? args[i + 1] : null;
20
24
  }
21
25
 
26
+ // Validate the entire command before any handler can read or mutate user data.
27
+ function validateArgs(sub, args) {
28
+ const specs = {
29
+ list: { values: ['--category', '--source'], flags: ['--json'], max: 0 },
30
+ add: { values: ['--apply'], flags: ['--plan', '--json'], max: Infinity },
31
+ fetch: { values: ['--project'], flags: ['--reference', '--force'], min: 1, max: 1 },
32
+ bundle: { values: ['--project'], flags: ['--dry-run'], max: 0 },
33
+ migrate: { values: [], flags: ['--dry-run'], max: 0 },
34
+ sync: { values: [], flags: [], max: 0 },
35
+ browse: { values: ['--port'], flags: [], max: 0 },
36
+ };
37
+ const spec = specs[sub];
38
+ if (!spec) throw new Error(`不明なコマンド: ${sub}`);
39
+ const seen = new Set();
40
+ const positional = [];
41
+ for (let i = 0; i < args.length; i++) {
42
+ const arg = args[i];
43
+ if (!arg.startsWith('-')) { positional.push(arg); continue; }
44
+ if (!spec.values.includes(arg) && !spec.flags.includes(arg)) throw new Error(`不明なオプション: ${arg}`);
45
+ if (seen.has(arg)) throw new Error(`重複したオプション: ${arg}`);
46
+ seen.add(arg);
47
+ if (spec.values.includes(arg) && (!args[++i] || args[i].startsWith('-'))) throw new Error(`${arg} には値が必要です`);
48
+ }
49
+ if (positional.length < (spec.min ?? 0) || positional.length > spec.max) throw new Error('引数の数が正しくありません');
50
+ if (sub === 'add' && (seen.has('--plan') === seen.has('--apply')
51
+ || (seen.has('--apply') ? positional.length !== 0 : positional.length === 0))) {
52
+ throw new Error('add は <path...> --plan または --apply <plan.json> を指定してください');
53
+ }
54
+ if (sub === 'bundle' && !seen.has('--project')) throw new Error('--project <dir> が必要です');
55
+ if (sub === 'fetch') {
56
+ if (args[0] !== positional[0]) throw new Error('fetch の先頭には素材 ID を指定してください');
57
+ if (seen.has('--reference') && !seen.has('--project')) throw new Error('--reference には --project <dir> が必要です');
58
+ }
59
+ }
60
+
22
61
  const STATE_BADGE = { cached: '✓', locked: '¥', available: '☁' };
23
62
 
24
63
  function badgeOf(item) {
@@ -30,8 +69,11 @@ function badgeOf(item) {
30
69
  async function cmdList(args, env) {
31
70
  const category = flagValue(args, '--category');
32
71
  const asJson = args.includes('--json');
33
- const { libraryRoots, items } = await composeState({ env });
34
- const filtered = category ? items.filter((item) => item.category === category) : items;
72
+ const source = flagValue(args, '--source');
73
+ if (args.includes('--source') && !['lab', 'site', 'own'].includes(source)) throw new Error('--source lab / site / own で指定してください');
74
+ const { libraryRoots, items, warnings } = await composeState({ env });
75
+ for (const warning of warnings) console.error(`警告: ${warning}`);
76
+ const filtered = items.filter(item => (!category || item.category === category) && (!source || item.sourceKind === source));
35
77
 
36
78
  if (asJson) {
37
79
  console.log(JSON.stringify(filtered, null, 2));
@@ -40,8 +82,23 @@ async function cmdList(args, env) {
40
82
 
41
83
  console.log(`使える素材 ${filtered.length} 件(ライブラリ: ${libraryRoots.write})`);
42
84
  for (const item of filtered) {
43
- console.log(` ${badgeOf(item)} ${item.id}\t[${item.category}]\t${item.title}`);
85
+ console.log(` ${badgeOf(item)} ${item.id}\t${item.sourceKind}\t[${item.category}]\t${item.title}`);
86
+ }
87
+ }
88
+
89
+ async function cmdAdd(args, env) {
90
+ const apply = flagValue(args, '--apply');
91
+ if (args.includes('--plan') === args.includes('--apply')) throw new Error('add は --plan または --apply <plan.json> のどちらかを指定してください');
92
+ let result;
93
+ if (args.includes('--apply')) {
94
+ if (!apply || args.some((arg, index) => !['--apply', '--json'].includes(arg) && index !== args.indexOf('--apply') + 1)) throw new Error('使い方: akari-assets add --apply <plan.json> [--json]');
95
+ result = await applyAdd(JSON.parse(await readFile(apply, 'utf8')), { env });
96
+ if (result.failures.length) process.exitCode = 1;
97
+ } else {
98
+ if (args.some(arg => arg.startsWith('--') && !['--plan', '--json'].includes(arg))) throw new Error('使い方: akari-assets add <path...> --plan [--json]');
99
+ result = await planAdd(args.filter(arg => !['--plan', '--json'].includes(arg)), { env });
44
100
  }
101
+ console.log(JSON.stringify(result, null, 2));
45
102
  }
46
103
 
47
104
  async function cmdFetch(args, env) {
@@ -120,9 +177,12 @@ async function cmdBrowse(args, env) {
120
177
  }
121
178
 
122
179
  function printUsage() {
123
- console.log(`使い方: akari-assets <list|fetch|bundle|migrate|sync|browse> [options]
180
+ console.log(`使い方: akari-assets <list|add|fetch|bundle|migrate|sync|browse> [options]
124
181
 
125
- list [--category <c>] [--json] 合成カタログ一覧(取得状態バッジ込み)
182
+ list [--category <c>] [--source <lab|site|own>] [--json]
183
+ 出どころ・取得状態つき素材一覧
184
+ add <path...> --plan [--json] ローカル素材の取り込み計画(書き込みなし)
185
+ add --apply <plan.json> [--json] 計画で選択した素材を複製して登録
126
186
  fetch <id> [--project <dir>] [--reference] [--force]
127
187
  素材を解決して登録(--reference はコピーせず参照を記帳)
128
188
  bundle --project <dir> [--dry-run] 参照素材をプロジェクトへ実体化(素材をまとめる)
@@ -142,6 +202,18 @@ async function main() {
142
202
  const [sub, ...rest] = process.argv.slice(2);
143
203
  const env = process.env;
144
204
 
205
+ if (!sub || process.argv.slice(2).some(arg => arg === '--help' || arg === '-h')) {
206
+ printUsage();
207
+ return;
208
+ }
209
+ try { validateArgs(sub, rest); }
210
+ catch (error) {
211
+ console.error(error.message);
212
+ printUsage();
213
+ process.exitCode = 2;
214
+ return;
215
+ }
216
+
145
217
  if (sub === 'migrate') {
146
218
  const result = await migrateAssetLibrary({ env, dryRun: rest.includes('--dry-run') });
147
219
  console.log(JSON.stringify(result, null, 2));
@@ -149,6 +221,7 @@ async function main() {
149
221
  return;
150
222
  }
151
223
  if (sub === 'list') return cmdList(rest, env);
224
+ if (sub === 'add') return cmdAdd(rest, env);
152
225
  if (sub === 'fetch') return cmdFetch(rest, env);
153
226
  if (sub === 'bundle') return cmdBundle(rest, env);
154
227
  if (sub === 'sync') return cmdSync(rest, env);