akari-video 0.1.34 → 0.1.36
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 +1 -1
- package/src/cli.mjs +2 -0
- package/src/repo-assets.mjs +7 -2
- package/src/word-book-command.mjs +16 -0
- package/vendor/.akari-capability-sources.json +2 -0
- package/vendor/docs/contract-2026-07-22-render-basics.md +1 -0
- package/vendor/docs/contract-2026-08-02-preview-parity.md +12 -0
- package/vendor/docs/contract-2026-09-02-asset-reference-model.md +57 -0
- package/vendor/docs/contract-2026-09-02-captions-style-preset-v0.md +17 -1
- package/vendor/docs/contract-2026-09-02-shape-item-v0.md +49 -0
- package/vendor/docs/contract-2026-09-02-word-book-v0.md +3 -1
- package/vendor/packages/akari-launcher/package.json +1 -1
- package/vendor/packages/asset-resolver/bin/akari-assets.mjs +55 -6
- package/vendor/packages/asset-resolver/src/bundle.mjs +43 -0
- package/vendor/packages/asset-resolver/src/project-references.mjs +131 -0
- package/vendor/packages/asset-resolver/src/resolve.mjs +27 -8
- package/vendor/packages/asset-resolver/test/project-references.test.mjs +163 -0
- package/vendor/packages/edit-lint/src/edit-lint.mjs +133 -20
- package/vendor/packages/edit-lint/src/library-reference.mjs +80 -0
- package/vendor/packages/edit-store/lib/caption-display.d.ts +5 -0
- package/vendor/packages/edit-store/lib/caption-display.js +36 -2
- package/vendor/packages/edit-store/lib/caption-store.d.ts +20 -0
- package/vendor/packages/edit-store/lib/caption-store.js +132 -10
- package/vendor/packages/edit-store/lib/edit-v2-item-write.d.ts +2 -0
- package/vendor/packages/edit-store/lib/edit-v2-item-write.js +28 -15
- package/vendor/packages/edit-store/lib/edit-v2.d.ts +17 -1
- package/vendor/packages/edit-store/lib/edit-v2.js +28 -0
- package/vendor/packages/edit-store/lib/generated/edit-v2-keys.d.ts +4 -2
- package/vendor/packages/edit-store/lib/generated/edit-v2-keys.js +26 -2
- package/vendor/packages/edit-store/lib/index.d.ts +1 -0
- package/vendor/packages/edit-store/lib/index.js +1 -0
- package/vendor/packages/edit-store/lib/internal-model.js +29 -0
- package/vendor/packages/edit-store/lib/migrate/geometry.d.ts +71 -0
- package/vendor/packages/edit-store/lib/migrate/geometry.js +186 -0
- package/vendor/packages/edit-store/lib/migrate/index.d.ts +19 -0
- package/vendor/packages/edit-store/lib/migrate/index.js +70 -1
- package/vendor/packages/edit-store/lib/shape-markup.d.ts +3 -0
- package/vendor/packages/edit-store/lib/shape-markup.js +66 -0
- package/vendor/packages/edit-store/lib/webview-kernel.js +145 -0
- package/vendor/packages/media-bin/src/media-dimensions.mjs +96 -0
- package/vendor/packages/media-bin/test/media-dimensions.test.mjs +115 -0
- package/vendor/packages/schemas/edit.schema.json +60 -0
- package/vendor/packages/schemas/engine-capabilities.json +4 -2
- package/vendor/packages/schemas/examples/edit-v2-shape-minimal-valid/edit.json +19 -0
- package/vendor/packages/schemas/examples/edit-v2-shape-negative-stroke-invalid/edit.json +23 -0
- package/vendor/packages/schemas/examples/edit-v2-shape-param-key-invalid/edit.json +23 -0
- package/vendor/packages/schemas/examples/edit-v2-shape-params-valid/edit.json +30 -0
- package/vendor/packages/schemas/examples/edit-v2-shape-unknown-invalid/edit.json +19 -0
- package/vendor/packages/schemas/test/edit-output-geometry.test.mjs +56 -0
- package/vendor/packages/schemas/test/edit-v2-schema.test.mjs +20 -2
- package/vendor/packages/schemas/test/engine-capabilities.test.mjs +2 -2
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "akari-video",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.36",
|
|
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": {
|
package/src/cli.mjs
CHANGED
|
@@ -24,6 +24,7 @@ import {
|
|
|
24
24
|
import { applySelfUpdate, isRunningFromAppDir, rollbackSelfUpdate } from './self-update.mjs';
|
|
25
25
|
import { runCaptureCommand } from './capture-command.mjs';
|
|
26
26
|
import { runMediaCommand } from './media-command.mjs';
|
|
27
|
+
import { runWordBookCommand } from './word-book-command.mjs';
|
|
27
28
|
import { resolveRuntimePaths } from './runtime-diagnostics.mjs';
|
|
28
29
|
|
|
29
30
|
/**
|
|
@@ -44,6 +45,7 @@ export async function run(args, options = {}) {
|
|
|
44
45
|
}
|
|
45
46
|
if (args[0] === 'capture') return runCaptureCommand(args.slice(1), options);
|
|
46
47
|
if (args[0] === 'media') return runMediaCommand(args.slice(1), options);
|
|
48
|
+
if (args[0] === 'word-book') return runWordBookCommand(args.slice(1), options);
|
|
47
49
|
|
|
48
50
|
const log = options.log ?? ((line) => console.log(line));
|
|
49
51
|
const assets = options.assets ?? resolveLauncherAssets();
|
package/src/repo-assets.mjs
CHANGED
|
@@ -55,6 +55,7 @@ export function resolveRepoAssets(repoRoot = DEFAULT_REPO_ROOT_CANDIDATE) {
|
|
|
55
55
|
const renderWhenIdleScript = path.join(repoRoot, RENDER_WHEN_IDLE_SCRIPT_RELATIVE);
|
|
56
56
|
const eyeBarScript = path.join(repoRoot, EYE_BAR_SCRIPT_RELATIVE);
|
|
57
57
|
const mediaScript = path.join(repoRoot, 'packages', 'akari-tools', 'bin', 'media.mjs');
|
|
58
|
+
const wordBookScript = path.join(repoRoot, 'packages', 'akari-tools', 'bin', 'word-book.mjs');
|
|
58
59
|
|
|
59
60
|
return {
|
|
60
61
|
repoRoot,
|
|
@@ -71,7 +72,8 @@ export function resolveRepoAssets(repoRoot = DEFAULT_REPO_ROOT_CANDIDATE) {
|
|
|
71
72
|
captureScript: existsSync(path.join(repoRoot, CAPTURE_SCRIPT_RELATIVE)) ? path.join(repoRoot, CAPTURE_SCRIPT_RELATIVE) : null,
|
|
72
73
|
renderWhenIdleScript: existsSync(renderWhenIdleScript) ? renderWhenIdleScript : null,
|
|
73
74
|
eyeBarScript: existsSync(eyeBarScript) ? eyeBarScript : null,
|
|
74
|
-
mediaScript: existsSync(mediaScript) ? mediaScript : null
|
|
75
|
+
mediaScript: existsSync(mediaScript) ? mediaScript : null,
|
|
76
|
+
...(existsSync(wordBookScript) ? { wordBookScript } : {})
|
|
75
77
|
};
|
|
76
78
|
}
|
|
77
79
|
|
|
@@ -105,6 +107,9 @@ export function resolveLauncherAssets({
|
|
|
105
107
|
...(candidate.captureScript ?? vendor.captureScript ? { captureScript: candidate.captureScript ?? vendor.captureScript } : {}),
|
|
106
108
|
renderWhenIdleScript: candidate.renderWhenIdleScript ?? vendor.renderWhenIdleScript,
|
|
107
109
|
eyeBarScript: candidate.eyeBarScript ?? vendor.eyeBarScript,
|
|
108
|
-
mediaScript: candidate.mediaScript ?? vendor.mediaScript
|
|
110
|
+
mediaScript: candidate.mediaScript ?? vendor.mediaScript,
|
|
111
|
+
...(candidate.wordBookScript ?? vendor.wordBookScript
|
|
112
|
+
? { wordBookScript: candidate.wordBookScript ?? vendor.wordBookScript }
|
|
113
|
+
: {})
|
|
109
114
|
};
|
|
110
115
|
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { spawnSync } from "node:child_process";
|
|
2
|
+
|
|
3
|
+
import { resolveLauncherAssets } from "./repo-assets.mjs";
|
|
4
|
+
|
|
5
|
+
export async function runWordBookCommand(args, options = {}) {
|
|
6
|
+
const logError = options.logError ?? ((line) => console.error(line));
|
|
7
|
+
const assets = options.assets ?? resolveLauncherAssets();
|
|
8
|
+
const spawn = options.spawn ?? spawnSync;
|
|
9
|
+
if (!assets.wordBookScript) {
|
|
10
|
+
logError("akari word-book の実行スクリプトが見つかりません。完全な AKARI Video を再導入してください:");
|
|
11
|
+
logError(" npm install -g akari-video");
|
|
12
|
+
return { exitCode: 1 };
|
|
13
|
+
}
|
|
14
|
+
const result = spawn(process.execPath, [assets.wordBookScript, ...args], { stdio: "inherit" });
|
|
15
|
+
return { exitCode: typeof result?.status === "number" ? result.status : 1 };
|
|
16
|
+
}
|
|
@@ -51,12 +51,14 @@
|
|
|
51
51
|
"docs/contract-2026-08-29-media-inspect-cli-v0.md",
|
|
52
52
|
"docs/contract-2026-08-30-edit-json-v2-object-tree-v0.md",
|
|
53
53
|
"docs/contract-2026-08-30-motion-and-keyframes-v0.md",
|
|
54
|
+
"docs/contract-2026-09-02-asset-reference-model.md",
|
|
54
55
|
"docs/contract-2026-09-02-audio-clip-fx-v1.md",
|
|
55
56
|
"docs/contract-2026-09-02-audio-envelope-v1.md",
|
|
56
57
|
"docs/contract-2026-09-02-audio-insert-level-v1.md",
|
|
57
58
|
"docs/contract-2026-09-02-captions-style-preset-v0.md",
|
|
58
59
|
"docs/contract-2026-09-02-export-verify-declared-vs-measured-v0.md",
|
|
59
60
|
"docs/contract-2026-09-02-item-caption-anchor-v0.md",
|
|
61
|
+
"docs/contract-2026-09-02-shape-item-v0.md",
|
|
60
62
|
"docs/contract-2026-09-02-transcript-unrecognized-spans-v0.md",
|
|
61
63
|
"docs/contract-2026-09-02-word-book-v0.md",
|
|
62
64
|
"packages/akari-launcher/package.json",
|
|
@@ -73,6 +73,7 @@
|
|
|
73
73
|
### 4-1. 画角(`cuts[].framing`)
|
|
74
74
|
|
|
75
75
|
- **crop と keyframes の併存**: 両方宣言された場合は `keyframes` を優先する。`crop` は「1 点ズームの縮退形」であり、両立させる意味論が無いため(複製 drift の温床にもなる)
|
|
76
|
+
- **幾何の基準(2026-09-02 追記・相互参照)**: `output.geometry` は未指定 = fit 互換 / `"source"` = 実寸基準を表すマーカーで、正本は `docs/contract-2026-08-02-preview-parity.md` §2.2(G1 は描画無変更・framing の再定義は G2)。
|
|
76
77
|
- **scale < 1 の扱い**: `keyframes[].scale` は仕組み上「クロップ窓を縮めて拡大する」ため 1 未満(キャンバスの外まで見せる=リビール)は原理的に表現できない。レンダ側で `max(1, scale)` にクランプする(silent drop ではなく仕組み上の上限として契約に明記)
|
|
77
78
|
- **crop.w/h が init 一度しか評価されない**: ffmpeg の `crop` フィルタは `x`/`y` は `t` を使った毎フレーム再評価に対応するが、`w`/`h` は(この ffmpeg ビルドで)フィルタ初期化時の一度きりの評価に固定されており `eval` オプション自体が存在しない(実機検証: `t` を含む `w`/`h` 式は `crop=... w='...t...'` で `Error when evaluating the expression` を返す)。そのため実装は「`scale` を `eval=frame` で `width*scale(t) : height*scale(t)` に広げてから固定サイズ `width:height` で `crop` する」方式を採る(クロップ窓の拡大 = `scale` 側の時間関数、パン位置 = `crop` の `x`/`y` の時間関数、という役割分担)
|
|
78
79
|
- **`crop` の `x`/`y` は上流フレームの実サイズを見ない**: 同フィルタの `iw`/`ih` 定数は(動的サイズの上流から来ていても)negotiate 済みの固定リンクサイズを指し、最初のフレームのサイズに固定されたままになることを実機検証で確認した。そのため `crop` の `x`/`y` 式は `iw`/`ih` を参照せず、`scale` 側と同じ `scale(t)` 式をそのまま再計算する(対称的だが唯一 crop から見て正しい現在値)
|
|
@@ -70,6 +70,13 @@ cut 境界の選択は宣言順ではなく解決済みタイムラインと z-o
|
|
|
70
70
|
`cut <id>: perspective is not applied by the frame-engine base path yet (issue #39)` を warning に出す(無警告で捨てない)。
|
|
71
71
|
- `freeze = {at_sec, duration_sec}` は指定 frame を保持し、cut の出力尺を `duration_sec` だけ伸ばして
|
|
72
72
|
後続の逐次 cut を移動する。freeze の画と独立音声予定表を混同しない。
|
|
73
|
+
- **`output.geometry`(幾何の基準。2026-09-02 追記)**: 未指定 = **fit 互換**(cut は出力へ contain fit した後に
|
|
74
|
+
transform。上記の従来どおり)、`"source"` = **実寸基準**(ソース実寸 × scale の box。layer・layer-style cut と同じ幾何)。
|
|
75
|
+
語彙は `"source"` の 1 つだけで、マーカーが立つのは「今 fit 基準で描かれている全 media item に `scale × fit`
|
|
76
|
+
(`fit = min(outputW / srcW, outputH / srcH)`・srcW / srcH は表示回転後)を一度だけ焼き込んだ」ことを意味する
|
|
77
|
+
(部分適用は禁止。移行は `packages/edit-store/bin/normalize-geometry.mjs`)。x / y / rotate は両基準で同じ意味なので触らない。
|
|
78
|
+
**G1(マーカー・移行・lint の warning `geometry.fit-compat`)ではエンジンはこのマーカーを読まず、描画は 1 バイトも変わらない。
|
|
79
|
+
描画へ反映するのは G2**。cross ref: `docs/contract-2026-07-22-render-basics.md` §4-1(#6 画角操作)。
|
|
73
80
|
|
|
74
81
|
**検収:** framing / transform / opacity / freeze を含む base parity **28 点**、freeze をまたぐ
|
|
75
82
|
frame lifetime **1000 コマ**、故意の **1 px** 差分を必ず FAIL させる否定点で判定する。
|
|
@@ -277,6 +284,11 @@ lint 実行系が見つからない場合は **fail-open**(2026-08-02 オー
|
|
|
277
284
|
保存を続行する。書き込みは tmp ファイルへの出力と rename による atomic 更新とする。実装は
|
|
278
285
|
`packages/edit-store` に一本化し、器や入口ごとの独自書き込み実装を追加してはならない。
|
|
279
286
|
|
|
287
|
+
本編 cut の `crop` 書き戻し(選択枠の辺バー)は **edit.json version 2 の文書だけ**が対象で、legacy の
|
|
288
|
+
`cuts[]` schema には席が無いため読み込み層が拒否する。`output.geometry` を宣言していない文書では、crop の
|
|
289
|
+
無い cut は出力キャンバスへ contain fit されて描かれるので、**初回の crop と同一 patch で `transform.scale`
|
|
290
|
+
へ fit 係数を焼き込む**(ソース実寸基準の layer-style へ移っても画面上の位置・大きさが変わらないため)。
|
|
291
|
+
|
|
280
292
|
### 5.4 ペン
|
|
281
293
|
|
|
282
294
|
ペン描画の単一正本は `packages/pen-visuals` の `PEN_TUNING` と描画プリミティブである。器や overlay
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
# contract — 素材の参照モデル v0(共有ライブラリ参照の台帳と解決規則)
|
|
2
|
+
|
|
3
|
+
- 状態: 実装済み(機械層のみ。シェル UI の採用は後続)
|
|
4
|
+
- 決定日: 2026-09-02
|
|
5
|
+
- 実装: `packages/asset-resolver`(記帳・実体化)/ `packages/render-cut`・`packages/edit-lint`(解決)
|
|
6
|
+
|
|
7
|
+
## 1. 目的
|
|
8
|
+
|
|
9
|
+
カタログ素材をプロジェクトごとに実体コピーすると、同じ素材が何度もダウンロード・複製されて
|
|
10
|
+
プロジェクトが肥大する。実体は**マシン単位の共有ライブラリ**(`~/.akari/assets/<category>/<id>/`)に
|
|
11
|
+
1 部だけ置き、プロジェクトには**参照だけを記録**できるようにする。
|
|
12
|
+
|
|
13
|
+
## 2. 設計の要点
|
|
14
|
+
|
|
15
|
+
- **edit.json は変えない**。参照素材も従来どおり `assets/<category>/<id>/<file>` の
|
|
16
|
+
プロジェクト相対パスで宣言される。実体がプロジェクトに無いことは参照台帳が説明する。
|
|
17
|
+
- 参照台帳 = プロジェクトの `.akari/asset-references.json`:
|
|
18
|
+
|
|
19
|
+
```json
|
|
20
|
+
{ "version": 0, "references": [ { "id": "<素材 id>", "category": "<カテゴリ>" } ] }
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
references は category → id の安定ソート・重複なし。読み手は寛容(無い / 壊れは空扱い)、
|
|
24
|
+
書き込みは tmp + rename の原子的更新。`version` は台帳自身のスキーマ版数であり
|
|
25
|
+
edit.json の version とは無関係。
|
|
26
|
+
- **解決規則**: 宣言されたプロジェクト相対パスが `assets/<category>/<id>/<rest>` の形で、
|
|
27
|
+
(1) プロジェクト実体が存在せず、(2) 台帳に `{category, id}` があるとき、
|
|
28
|
+
`<AKARI_HOME>/assets/<category>/<id>/<rest>`(AKARI_HOME 既定 `~/.akari`・env で上書き可)へ
|
|
29
|
+
フォールバックする。解決先は realpath 後も `<AKARI_HOME>/assets` 配下に収まる正規ファイルで
|
|
30
|
+
あること(`..` 等の脱出は fail-closed で拒否)。
|
|
31
|
+
- render-cut は解決した入力を render inputs 記録に `scope: "library"` として残す
|
|
32
|
+
(既存 `scope: "akari"` と同列の additive 記録)。edit-lint は解決できる参照を欠落と報告せず、
|
|
33
|
+
台帳にあるが実体が無い参照は「共有ライブラリ参照(未取得)」として欠落報告する。
|
|
34
|
+
- render-cut / edit-lint は依存ゼロ CLI のため、解決ロジックは各パッケージ内に**同一実装を重複**して
|
|
35
|
+
持つ(`src/library-reference.mjs`)。挙動同一性は両テストの同一ケース表で担保する。
|
|
36
|
+
|
|
37
|
+
## 3. 使い方(CLI)
|
|
38
|
+
|
|
39
|
+
```sh
|
|
40
|
+
# 参照モードで取得(コピーせず台帳へ記帳。既定は従来どおりコピー)
|
|
41
|
+
akari-assets fetch <id> --project <dir> --reference
|
|
42
|
+
|
|
43
|
+
# 「素材をまとめる」— 参照の実体化(持ち出し・アーカイブ用)
|
|
44
|
+
akari-assets bundle --project <dir> [--dry-run]
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
bundle は台帳の各参照をキャッシュから `assets/<category>/<id>/` へ実体化して台帳から除去する。
|
|
48
|
+
未取得の参照は resolve(取得)を試み、取得できないものは台帳に残して部分成功(exit 非 0)で報告する。冪等。
|
|
49
|
+
|
|
50
|
+
## 4. スコープ外(後続)
|
|
51
|
+
|
|
52
|
+
- シェル UI の採用(取り込みフローの reference 既定化・プロジェクト面の「参照」バッジ・
|
|
53
|
+
プレビュー経路のフォールバック)
|
|
54
|
+
- 共有キャッシュの容量管理 UI
|
|
55
|
+
|
|
56
|
+
出自: 2026-09-02 の素材パネル再設計ラウンドの裁定 3(実体 = 共有キャッシュ・プロジェクトには参照・
|
|
57
|
+
持ち出しは「素材をまとめる」で閉じる)。
|
|
@@ -79,6 +79,22 @@ edit-lint はカタログを読める環境だけ存在検査を行い、未知
|
|
|
79
79
|
|
|
80
80
|
- ルートレベルの `style_preset`
|
|
81
81
|
- price、購入状態、👑 プレミア、Lab 接続
|
|
82
|
-
- テンプレピッカー UI と選択行への一括適用 RPC(T6b)
|
|
83
82
|
- 既存 merge 実装の統合・改修
|
|
84
83
|
- テロップ契約との統合
|
|
84
|
+
|
|
85
|
+
## 8. パネル側の約束(T6b)
|
|
86
|
+
|
|
87
|
+
「台本」パネルの字幕テンプレピッカーは、行選択がある場合は選択行だけ、選択が無い場合は
|
|
88
|
+
全行を適用先にする。全行適用はカード選択後の明示ボタンで確定する。
|
|
89
|
+
|
|
90
|
+
書き戻しは `setCaptionStylePreset` RPC 1 回で対象行を一括更新し、1 回のファイル書き込みと
|
|
91
|
+
1 git commit にまとめる。`text_style` は変更しない。`presetId: null` は `style_preset` キーだけを
|
|
92
|
+
削除する解除操作である。同値の再適用は書き込みも commit も行わない。
|
|
93
|
+
|
|
94
|
+
各行は `🎨 <テンプレ名>` バッジで参照中のテンプレを示す。カタログに無い id は
|
|
95
|
+
`🎨 <id>?` と表示し、edit-lint warning と併用して読み込みや書き出しを壊さない。
|
|
96
|
+
|
|
97
|
+
インスペクターはプリセット解決後の値を表示する。そこで個別の値を上書きすると
|
|
98
|
+
`text_style` に保存され、以後そのフィールドはテンプレ更新に追従しない。
|
|
99
|
+
|
|
100
|
+
👑、price、Lab 接続、パネル内履歴、インスペクターの「テンプレ: xxx」表示は T6c / T9 の範囲とする。
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
# contract — 図形アイテム v0(edit.json v2 `shape` ソースとインライン SVG 降下)
|
|
2
|
+
|
|
3
|
+
- 状態: 実装済み(データ契約 + edit-store 降下。パネル露出・インスペクター UI は後続)
|
|
4
|
+
- 決定日: 2026-09-02
|
|
5
|
+
- 実装: `packages/schemas/edit.schema.json`(`itemSourceShapeV2` / `itemV2Shape`)/
|
|
6
|
+
`packages/edit-store`(`src/shape-markup.ts`・`internal-model.ts`)
|
|
7
|
+
|
|
8
|
+
## 1. 目的
|
|
9
|
+
|
|
10
|
+
四角・線・矢印・吹き出しといった図形を、素材ファイル無しで edit.json v2 の第一級アイテムとして
|
|
11
|
+
宣言できるようにする。レンダラは新設しない — edit-store の内部モデルが図形を**決定論的な
|
|
12
|
+
インライン SVG を持つ html オーバーレイへ降下**させ、既存の html 経路(プレビュー・書き出しとも)が
|
|
13
|
+
そのまま描く。
|
|
14
|
+
|
|
15
|
+
## 2. 語彙 v0
|
|
16
|
+
|
|
17
|
+
```json
|
|
18
|
+
{ "id": "shape-1", "at": 0, "duration": 90,
|
|
19
|
+
"source": { "kind": "shape", "shape": "rect",
|
|
20
|
+
"params": { "width": 600, "height": 340, "fill": "#f97316" } } }
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
- `shape`: `rect | rounded-rect | ellipse | line | arrow | speech-bubble`
|
|
24
|
+
- `params`(全部 optional・additionalProperties false で開始 — 広げる方向は互換):
|
|
25
|
+
`width`(>0・既定 600)/ `height`(>0・既定 340。line / arrow は既定 80)/
|
|
26
|
+
`fill`(既定 `#f97316`)/ `stroke`(既定なし = 描かない)/ `strokeWidth`(≥0・既定 0。line / arrow は 8)/
|
|
27
|
+
`cornerRadius`(≥0・rounded-rect のみ・既定 24)
|
|
28
|
+
- 色文字列は `^[#a-zA-Z0-9(),.%\s-]{1,64}$` に一致しないとき既定色へフォールバック
|
|
29
|
+
(SVG への注入封じ)。数値は有限数のみ受理・範囲外は既定へ。
|
|
30
|
+
- 位置・拡大・不透明度・アニメはアイテム共通機構(`anchor` / `transform` / `opacity` /
|
|
31
|
+
`keyframes` / `motion` / `animator`)に委ね、params に重複ツマミを作らない。
|
|
32
|
+
|
|
33
|
+
## 3. 降下の契約
|
|
34
|
+
|
|
35
|
+
- `shapeMarkup(source)`(`packages/edit-store/src/shape-markup.ts`)は同一入力に対して
|
|
36
|
+
バイト同一の `<svg …>` 文字列を返す(時刻・乱数・環境非依存)。
|
|
37
|
+
- 内部モデルは shape アイテムを html アイテムと同格に扱い、オーバーレイ宣言の `html` に
|
|
38
|
+
インラインマークアップを乗せる(`<` 始まりのため render-cut の `expandedHtmlOverlays` は
|
|
39
|
+
ファイル読込をせずそのまま通す)。
|
|
40
|
+
- 既知の制約: SVG の xmlns URI が GPU 出口の適格性検査に absolute-external-url として
|
|
41
|
+
検知されるため、図形入りの書き出しは現状 **OSR 出口へフォールバック**する(描画は正しい)。
|
|
42
|
+
GPU 適格化(名前空間 URI の許可リスト化)は後続。
|
|
43
|
+
|
|
44
|
+
## 4. スコープ外(後続)
|
|
45
|
+
|
|
46
|
+
- 素材パネルの図形カテゴリ露出(現状は「近日」)・インスペクターの params ツマミ
|
|
47
|
+
- スタンプ(新種別にしない — 画像素材で賄う)
|
|
48
|
+
|
|
49
|
+
出自: 2026-09-02 の素材パネル再設計ラウンド(カテゴリ表「図形は種別追加から」)。
|
|
@@ -240,7 +240,7 @@ cloud)。
|
|
|
240
240
|
| 瞬間 | 何をするか | 実装箇所 |
|
|
241
241
|
|---|---|---|
|
|
242
242
|
| 文字起こし直後 | 解決済み単語帳で全セグメントにプリパス(§3-2)。キャッシュ hit 経路も同じ | `packages/akari-tools/src/media/transcribe.mjs` `transcribeMedia`: `normalizeSegments` / `attachUnrecognizedSpans` の後・`recordTranscribe` の前。`options.wordBook`(`--no-word-book` / `--word-book <path>`) |
|
|
243
|
-
| 台本パネル「覚える」 | 人が行を直した直後に、直した語列を `variants`、直した後を `surface` として登録を提案。**登録先の層を必ず人に確認**(既定 `project`。`channel` / `workspace` は昇格 = 内部契約 §4 の承認ゲート)。登録後、同じプロジェクトの `edited: false` な行と transcript に即時再適用し件数を返す | `apps/shell/extensions/akari-transcript` の node 側 service に `rememberWord` RPC を足し、`packages/word-book` の add + apply
|
|
243
|
+
| 台本パネル「覚える」 | 人が行を直した直後に、直した語列を `variants`、直した後を `surface` として登録を提案。**登録先の層を必ず人に確認**(既定 `project`。`channel` / `workspace` は昇格 = 内部契約 §4 の承認ゲート)。登録後、同じプロジェクトの `edited: false` な行と transcript に即時再適用し件数を返す | `apps/shell/extensions/akari-transcript` の node 側 service に `rememberWord` RPC を足し、`packages/word-book` の add + apply を呼ぶ(訂正 2026-09-02: RPC は akari-annotations の service に足す。UI は akari-transcript) |
|
|
244
244
|
| 手動再適用 | 既存プロジェクトに解決済み単語帳を当て直す。`--dry-run` で件数だけ | `akari word-book apply [--project <dir>] [--dry-run]` |
|
|
245
245
|
| edit-lint | §5 の規則 | `packages/edit-lint/src/edit-lint.mjs`(captions 検査の並び) |
|
|
246
246
|
| 行分割 | §3-6 の軟らかい供給 | `resolveCaptionDisplay` の呼び出し 4 箇所 |
|
|
@@ -400,3 +400,5 @@ lint の一致も §3-2 と同じ語境界規則(`words[]` → 無ければ `I
|
|
|
400
400
|
6. 作業場 fixture(`root.json` + `channels/<c>/videos/<p>`)で `resolve` が 4 層の出所を正しく返し、
|
|
401
401
|
作業場なし fixture で `project` + `builtin` に落ちること
|
|
402
402
|
7. 実際のホーム・作業場へ書き込まない(全テストは一時ディレクトリと `AKARI_WORD_BOOK` で完結)
|
|
403
|
+
8. launcher: `akari word-book --help` が 4 サブコマンド(resolve / validate / add / apply)を列挙し、
|
|
404
|
+
akari-tools 不在時は「インストール方法」を示して exit 1
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "akari-video",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.36",
|
|
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,12 +1,14 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
// akari-assets — 素材 resolver v0 の CLI(list / fetch / sync / browse)。
|
|
2
|
+
// akari-assets — 素材 resolver v0 の CLI(list / fetch / bundle / sync / browse)。
|
|
3
3
|
//
|
|
4
4
|
// akari-assets list [--category <c>] [--json]
|
|
5
|
-
// akari-assets fetch <id> [--project <dir>] [--force]
|
|
5
|
+
// akari-assets fetch <id> [--project <dir>] [--reference] [--force]
|
|
6
|
+
// akari-assets bundle --project <dir> [--dry-run]
|
|
6
7
|
// akari-assets sync
|
|
7
8
|
// akari-assets browse [--port <n>]
|
|
8
9
|
|
|
9
10
|
import { startBrowseServer } from '../src/browse-server.mjs';
|
|
11
|
+
import { bundleProjectReferences } from '../src/bundle.mjs';
|
|
10
12
|
import { cacheCatalog, loadCatalog } from '../src/catalog.mjs';
|
|
11
13
|
import { resolve as resolveAsset } from '../src/resolve.mjs';
|
|
12
14
|
import { composeState } from '../src/state.mjs';
|
|
@@ -43,22 +45,66 @@ async function cmdList(args, env) {
|
|
|
43
45
|
async function cmdFetch(args, env) {
|
|
44
46
|
const id = args[0];
|
|
45
47
|
if (!id || id.startsWith('--')) {
|
|
46
|
-
console.error('使い方: akari-assets fetch <id> [--project <dir>] [--force]');
|
|
48
|
+
console.error('使い方: akari-assets fetch <id> [--project <dir>] [--reference] [--force]');
|
|
47
49
|
process.exitCode = 1;
|
|
48
50
|
return;
|
|
49
51
|
}
|
|
50
52
|
const project = flagValue(args, '--project');
|
|
53
|
+
const reference = args.includes('--reference');
|
|
51
54
|
const force = args.includes('--force');
|
|
55
|
+
if (reference && !project) {
|
|
56
|
+
console.error('--reference には --project <dir> が必要です');
|
|
57
|
+
process.exitCode = 1;
|
|
58
|
+
return;
|
|
59
|
+
}
|
|
52
60
|
try {
|
|
53
|
-
const result = await resolveAsset(id, { env, project, force });
|
|
61
|
+
const result = await resolveAsset(id, { env, project, force, reference });
|
|
54
62
|
console.log(`${result.cached ? '取得済み(キャッシュ)を使用' : '取得しました'}: ${result.dir}`);
|
|
55
63
|
if (result.projectDir) console.log(` プロジェクトへコピー: ${result.projectDir}`);
|
|
64
|
+
if (result.referenced) console.log(` 参照を記帳: ${result.category}/${result.id}`);
|
|
56
65
|
} catch (error) {
|
|
57
66
|
console.error(error instanceof Error ? error.message : String(error));
|
|
58
67
|
process.exitCode = 1;
|
|
59
68
|
}
|
|
60
69
|
}
|
|
61
70
|
|
|
71
|
+
async function cmdBundle(args, env) {
|
|
72
|
+
const project = flagValue(args, '--project');
|
|
73
|
+
const dryRun = args.includes('--dry-run');
|
|
74
|
+
if (!project) {
|
|
75
|
+
console.error('使い方: akari-assets bundle --project <dir> [--dry-run]');
|
|
76
|
+
process.exitCode = 1;
|
|
77
|
+
return;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
const result = await bundleProjectReferences({ project, env, dryRun });
|
|
81
|
+
if (result.planned.length === 0) {
|
|
82
|
+
console.log('実体化する参照はありません');
|
|
83
|
+
return;
|
|
84
|
+
}
|
|
85
|
+
if (dryRun) {
|
|
86
|
+
for (const reference of result.planned) {
|
|
87
|
+
console.log(`実体化予定: ${reference.category}/${reference.id}`);
|
|
88
|
+
}
|
|
89
|
+
return;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
for (const materialized of result.materialized) {
|
|
93
|
+
console.log(
|
|
94
|
+
`実体化しました: ${materialized.category}/${materialized.id} -> ${materialized.projectDir}`,
|
|
95
|
+
);
|
|
96
|
+
}
|
|
97
|
+
if (result.failures.length > 0) {
|
|
98
|
+
console.error(`実体化できなかった参照 ${result.failures.length} 件:`);
|
|
99
|
+
for (const failure of result.failures) {
|
|
100
|
+
console.error(
|
|
101
|
+
` ${failure.reference.category}/${failure.reference.id}: ${failure.message}`,
|
|
102
|
+
);
|
|
103
|
+
}
|
|
104
|
+
process.exitCode = 1;
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
|
|
62
108
|
async function cmdSync(_args, env) {
|
|
63
109
|
const catalog = await loadCatalog({ env });
|
|
64
110
|
await cacheCatalog(env, catalog);
|
|
@@ -72,10 +118,12 @@ async function cmdBrowse(args, env) {
|
|
|
72
118
|
}
|
|
73
119
|
|
|
74
120
|
function printUsage() {
|
|
75
|
-
console.log(`使い方: akari-assets <list|fetch|sync|browse> [options]
|
|
121
|
+
console.log(`使い方: akari-assets <list|fetch|bundle|sync|browse> [options]
|
|
76
122
|
|
|
77
123
|
list [--category <c>] [--json] 合成カタログ一覧(取得状態バッジ込み)
|
|
78
|
-
fetch <id> [--project <dir>] [--force]
|
|
124
|
+
fetch <id> [--project <dir>] [--reference] [--force]
|
|
125
|
+
素材を解決して登録(--reference はコピーせず参照を記帳)
|
|
126
|
+
bundle --project <dir> [--dry-run] 参照素材をプロジェクトへ実体化(素材をまとめる)
|
|
79
127
|
sync カタログを取得してローカルにキャッシュ(オフライン用)
|
|
80
128
|
browse [--port <n>] ローカル HTTP サーバでカタログを閲覧・投入(既定 8910)
|
|
81
129
|
|
|
@@ -92,6 +140,7 @@ async function main() {
|
|
|
92
140
|
|
|
93
141
|
if (sub === 'list') return cmdList(rest, env);
|
|
94
142
|
if (sub === 'fetch') return cmdFetch(rest, env);
|
|
143
|
+
if (sub === 'bundle') return cmdBundle(rest, env);
|
|
95
144
|
if (sub === 'sync') return cmdSync(rest, env);
|
|
96
145
|
if (sub === 'browse') return cmdBrowse(rest, env);
|
|
97
146
|
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import { resolveAkariHome } from './env.mjs';
|
|
2
|
+
import { isAssetCached, localAssetDir } from './library.mjs';
|
|
3
|
+
import { readProjectReferences, removeProjectReference } from './project-references.mjs';
|
|
4
|
+
import { copyIntoProject, resolve as resolveAsset } from './resolve.mjs';
|
|
5
|
+
|
|
6
|
+
export async function bundleProjectReferences({
|
|
7
|
+
project,
|
|
8
|
+
env = process.env,
|
|
9
|
+
dryRun = false,
|
|
10
|
+
} = {}) {
|
|
11
|
+
const planned = await readProjectReferences(project);
|
|
12
|
+
const result = { planned, materialized: [], failures: [] };
|
|
13
|
+
if (dryRun) return result;
|
|
14
|
+
|
|
15
|
+
const home = resolveAkariHome(env);
|
|
16
|
+
for (const reference of planned) {
|
|
17
|
+
try {
|
|
18
|
+
if (!isAssetCached(home, reference.category, reference.id)) {
|
|
19
|
+
const resolved = await resolveAsset(reference.id, { env });
|
|
20
|
+
if (resolved.category !== reference.category) {
|
|
21
|
+
throw new Error(
|
|
22
|
+
`カタログのカテゴリが台帳と一致しません: ${reference.category}/${reference.id}(実際: ${resolved.category})`,
|
|
23
|
+
);
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
const sourceDir = localAssetDir(home, reference.category, reference.id);
|
|
27
|
+
const projectDir = await copyIntoProject(
|
|
28
|
+
sourceDir,
|
|
29
|
+
project,
|
|
30
|
+
reference.category,
|
|
31
|
+
reference.id,
|
|
32
|
+
);
|
|
33
|
+
await removeProjectReference(project, reference);
|
|
34
|
+
result.materialized.push({ ...reference, projectDir });
|
|
35
|
+
} catch (error) {
|
|
36
|
+
result.failures.push({
|
|
37
|
+
reference,
|
|
38
|
+
message: error instanceof Error ? error.message : String(error),
|
|
39
|
+
});
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
return result;
|
|
43
|
+
}
|
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
import { randomUUID } from 'node:crypto';
|
|
2
|
+
import { lstatSync, realpathSync } from 'node:fs';
|
|
3
|
+
import { mkdir, readFile, rename, rm, writeFile } from 'node:fs/promises';
|
|
4
|
+
import path from 'node:path';
|
|
5
|
+
|
|
6
|
+
const REFERENCES_FILE = path.join('.akari', 'asset-references.json');
|
|
7
|
+
|
|
8
|
+
// 参照台帳(.akari/asset-references.json)のスキーマ版数。edit.json の version とは無関係。
|
|
9
|
+
const REFERENCES_SCHEMA_VERSION = 0;
|
|
10
|
+
|
|
11
|
+
function compareReferences(left, right) {
|
|
12
|
+
if (left.category !== right.category) return left.category < right.category ? -1 : 1;
|
|
13
|
+
if (left.id !== right.id) return left.id < right.id ? -1 : 1;
|
|
14
|
+
return 0;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
function isReference(value) {
|
|
18
|
+
return value !== null
|
|
19
|
+
&& typeof value === 'object'
|
|
20
|
+
&& !Array.isArray(value)
|
|
21
|
+
&& typeof value.id === 'string'
|
|
22
|
+
&& value.id.length > 0
|
|
23
|
+
&& typeof value.category === 'string'
|
|
24
|
+
&& value.category.length > 0;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function normalizeReferences(value) {
|
|
28
|
+
const source = Array.isArray(value) ? value : value?.references;
|
|
29
|
+
if (!Array.isArray(source)) return [];
|
|
30
|
+
const unique = new Map();
|
|
31
|
+
for (const entry of source) {
|
|
32
|
+
if (!isReference(entry)) continue;
|
|
33
|
+
const reference = { id: entry.id, category: entry.category };
|
|
34
|
+
unique.set(`${reference.category}\0${reference.id}`, reference);
|
|
35
|
+
}
|
|
36
|
+
return [...unique.values()].sort(compareReferences);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function assertReference(reference) {
|
|
40
|
+
if (!isReference(reference)) {
|
|
41
|
+
throw new TypeError('asset reference requires non-empty id and category strings');
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function referencesPath(projectDir) {
|
|
46
|
+
return path.join(path.resolve(projectDir), REFERENCES_FILE);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
async function writeProjectReferences(projectDir, references) {
|
|
50
|
+
const target = referencesPath(projectDir);
|
|
51
|
+
await mkdir(path.dirname(target), { recursive: true });
|
|
52
|
+
const temp = `${target}.${process.pid}.${randomUUID()}.tmp`;
|
|
53
|
+
const body = `${JSON.stringify({ version: REFERENCES_SCHEMA_VERSION, references: normalizeReferences(references) }, null, 2)}\n`;
|
|
54
|
+
try {
|
|
55
|
+
await writeFile(temp, body, { encoding: 'utf8', flag: 'wx' });
|
|
56
|
+
await rename(temp, target);
|
|
57
|
+
} finally {
|
|
58
|
+
await rm(temp, { force: true }).catch(() => {});
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
export async function readProjectReferences(projectDir) {
|
|
63
|
+
try {
|
|
64
|
+
const parsed = JSON.parse(await readFile(referencesPath(projectDir), 'utf8'));
|
|
65
|
+
if (parsed?.version !== REFERENCES_SCHEMA_VERSION) return [];
|
|
66
|
+
return normalizeReferences(parsed);
|
|
67
|
+
} catch {
|
|
68
|
+
return [];
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
export async function recordProjectReference(projectDir, reference) {
|
|
73
|
+
assertReference(reference);
|
|
74
|
+
const references = await readProjectReferences(projectDir);
|
|
75
|
+
references.push({ id: reference.id, category: reference.category });
|
|
76
|
+
const normalized = normalizeReferences(references);
|
|
77
|
+
await writeProjectReferences(projectDir, normalized);
|
|
78
|
+
return normalized;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
export async function removeProjectReference(projectDir, reference) {
|
|
82
|
+
assertReference(reference);
|
|
83
|
+
const references = (await readProjectReferences(projectDir)).filter(
|
|
84
|
+
(entry) => entry.id !== reference.id || entry.category !== reference.category,
|
|
85
|
+
);
|
|
86
|
+
await writeProjectReferences(projectDir, references);
|
|
87
|
+
return references;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
function parseDeclaredAssetPath(declaredPath) {
|
|
91
|
+
if (typeof declaredPath !== 'string' || declaredPath.length === 0 || path.isAbsolute(declaredPath)) return null;
|
|
92
|
+
const normalized = declaredPath.replaceAll('\\', '/');
|
|
93
|
+
const segments = normalized.split('/');
|
|
94
|
+
if (segments.length < 4
|
|
95
|
+
|| segments[0] !== 'assets'
|
|
96
|
+
|| segments.some((segment) => segment === '' || segment === '.' || segment === '..')) {
|
|
97
|
+
return null;
|
|
98
|
+
}
|
|
99
|
+
return {
|
|
100
|
+
category: segments[1],
|
|
101
|
+
id: segments[2],
|
|
102
|
+
rest: segments.slice(3),
|
|
103
|
+
};
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
function isWithin(root, target) {
|
|
107
|
+
const relative = path.relative(root, target);
|
|
108
|
+
return relative === '' || (!relative.startsWith('..') && !path.isAbsolute(relative));
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
export function resolveLibraryFallback({ declaredPath, references, akariAssetsDir }) {
|
|
112
|
+
const parsed = parseDeclaredAssetPath(declaredPath);
|
|
113
|
+
if (!parsed || typeof akariAssetsDir !== 'string' || akariAssetsDir.length === 0) return null;
|
|
114
|
+
const normalizedReferences = normalizeReferences(references);
|
|
115
|
+
if (!normalizedReferences.some(
|
|
116
|
+
(entry) => entry.category === parsed.category && entry.id === parsed.id,
|
|
117
|
+
)) return null;
|
|
118
|
+
|
|
119
|
+
const lexicalRoot = path.resolve(akariAssetsDir);
|
|
120
|
+
const lexicalTarget = path.resolve(lexicalRoot, parsed.category, parsed.id, ...parsed.rest);
|
|
121
|
+
if (!isWithin(lexicalRoot, lexicalTarget)) return null;
|
|
122
|
+
|
|
123
|
+
try {
|
|
124
|
+
const actualRoot = realpathSync(lexicalRoot);
|
|
125
|
+
const actualTarget = realpathSync(lexicalTarget);
|
|
126
|
+
if (!isWithin(actualRoot, actualTarget) || !lstatSync(actualTarget).isFile()) return null;
|
|
127
|
+
return actualTarget;
|
|
128
|
+
} catch {
|
|
129
|
+
return null;
|
|
130
|
+
}
|
|
131
|
+
}
|