akari-video 0.1.46 → 0.1.47
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/vendor/docs/contract-2026-08-28-v2-audio-roles-v0.md +6 -0
- package/vendor/docs/contract-2026-08-30-motion-and-keyframes-v0.md +15 -0
- package/vendor/packages/akari-launcher/package.json +1 -1
- package/vendor/packages/edit-lint/src/edit-lint.mjs +14 -0
- package/vendor/packages/edit-store/lib/canonical.js +1 -1
- package/vendor/packages/edit-store/lib/edit-v2.d.ts +6 -0
- package/vendor/packages/edit-store/lib/edit-v2.js +4 -1
- package/vendor/packages/edit-store/lib/internal-model.js +8 -1
- package/vendor/packages/edit-store/lib/legacy-audio-view.js +1 -0
- package/vendor/packages/edit-store/lib/webview-kernel.js +10 -2
- package/vendor/packages/schemas/edit.schema.json +3 -1
- package/vendor/packages/schemas/engine-capabilities.json +1 -1
- package/vendor/packages/schemas/test/edit-v2-schema.test.mjs +14 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "akari-video",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.47",
|
|
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": {
|
|
@@ -133,6 +133,12 @@ ms へ換算し、絶対値で集計する。全 59 点の最大は 16.667 ms、
|
|
|
133
133
|
`mute: true` は speech 宣言そのものを作らず、プレビューのサイドカー要求も PCM 化のコストも消える。
|
|
134
134
|
書き出しは無音区間で尺を保つ。キー無しの既定値は従来と完全同一で、既存案件の出力は変わらない。
|
|
135
135
|
|
|
136
|
+
### 6.1 トラック単位の `tracks[].muted`
|
|
137
|
+
|
|
138
|
+
`muted?: boolean` は省略時 false。visual トラックの `muted: true` は cut の埋め込み音声を anullsrc に置き換え、尺は保つ(video layer は対象外)。
|
|
139
|
+
audio トラックの `muted: true` は item を書き出し・プレビューの mix から除外し、ducking の対象からも外れる。
|
|
140
|
+
トラックの `hidden` / `locked` は従来どおり storage 側で保持し、edit.json には出ない。
|
|
141
|
+
|
|
136
142
|
## 7. 速度変更した台詞のピッチ保持サイドカー
|
|
137
143
|
|
|
138
144
|
`speed` が 1 以外の撮影素材の台詞は、Web Audio の `playbackRate` による近似から外す。
|
|
@@ -44,6 +44,21 @@ updated: 2026-08-30
|
|
|
44
44
|
- HTML 部品の L0 は既存どおり `vars`(CSS 変数 `--anim-duration / stagger / distance / easing / delay`)。`motion` は native アイテム(telop / media / caption / group)用
|
|
45
45
|
- グループの `motion` は子全体にかかる(グループ = 小さなコンポジション)
|
|
46
46
|
|
|
47
|
+
- **実装状態(2026-09-06)**: frame-engine の layer / cut 評価で `motion` を描画する。シェルプレビュー / gpu / osr の 3 出口で同じ評価を使う
|
|
48
|
+
- `amount` の既定値は次表。`scale` / `pulse` は倍率に対する差分(0.2 = 20%)、`spin` は回転方向(±1)
|
|
49
|
+
|
|
50
|
+
| プリセット | `amount` の既定値 |
|
|
51
|
+
|---|---|
|
|
52
|
+
| `slide-up` / `slide-down` / `slide-left` / `slide-right` | 40 px |
|
|
53
|
+
| `scale` | 0.2 |
|
|
54
|
+
| `pulse` | 0.05 |
|
|
55
|
+
| `float` | 6 px |
|
|
56
|
+
| `spin` | 1(回転方向)|
|
|
57
|
+
|
|
58
|
+
- 合成は **keyframes の結果(未指定なら静的値)に motion を合成する 1 段**。`in` / `out` / `loop` の `dx` / `dy` / `rotate` は加算、`scale` と `opacity` は乗算、`reveal`(wipe)は交差
|
|
59
|
+
- `wipe` は crop 窓の中の表示率 `reveal` で表し、隠れた部分は完全透過。閉じた端点でも crop の幅・高さは `Number.EPSILON` 以上を保ち、opacity 0 で完全透過にする
|
|
60
|
+
- `duration` / `period` が 0 以下・非有限、または未知 preset の席は描画で無視する。未知 preset は席ごとに lint warning `motion.unknown-preset`(path は `.motion.in` / `.motion.out` / `.motion.loop`)
|
|
61
|
+
|
|
47
62
|
## 2. L1 キーフレーム — `keyframes` の一般化
|
|
48
63
|
|
|
49
64
|
### 2.1 形(1 つだけ: 点形)
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "akari-video",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.47",
|
|
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": [
|
|
@@ -49,6 +49,8 @@ const { captionsHaveRenderableCues, collectFitBasisCandidates } = createRequire(
|
|
|
49
49
|
|
|
50
50
|
const VERSION = 1;
|
|
51
51
|
const EPSILON = 1e-6;
|
|
52
|
+
const MOTION_IN_OUT_PRESETS = new Set(["fade", "slide-up", "slide-down", "slide-left", "slide-right", "scale", "wipe"]);
|
|
53
|
+
const MOTION_LOOP_PRESETS = new Set(["pulse", "float", "spin"]);
|
|
52
54
|
const REPOSITORY_ROOT = resolve(dirname(fileURLToPath(import.meta.url)), "../../..");
|
|
53
55
|
const CAPTIONS_SCHEMA = JSON.parse(readFileSync(
|
|
54
56
|
new URL("../../schemas/captions.schema.json", import.meta.url),
|
|
@@ -1073,6 +1075,18 @@ function validateEditV2(edit, findings) {
|
|
|
1073
1075
|
});
|
|
1074
1076
|
}
|
|
1075
1077
|
if (isRecord(item.motion)) {
|
|
1078
|
+
for (const seat of ["in", "out", "loop"]) {
|
|
1079
|
+
const entry = item.motion[seat];
|
|
1080
|
+
const presets = seat === "loop" ? MOTION_LOOP_PRESETS : MOTION_IN_OUT_PRESETS;
|
|
1081
|
+
if (isRecord(entry) && typeof entry.preset === "string" && !presets.has(entry.preset)) {
|
|
1082
|
+
addFinding(findings, {
|
|
1083
|
+
severity: "warning",
|
|
1084
|
+
check: "motion.unknown-preset",
|
|
1085
|
+
message: `unknown motion ${seat} preset: ${entry.preset}; ignored when rendering`,
|
|
1086
|
+
path: `${itemPath}.motion.${seat}`,
|
|
1087
|
+
});
|
|
1088
|
+
}
|
|
1089
|
+
}
|
|
1076
1090
|
const inDuration = isRecord(item.motion.in) && Number.isInteger(item.motion.in.duration)
|
|
1077
1091
|
? item.motion.in.duration : 0;
|
|
1078
1092
|
const outDuration = isRecord(item.motion.out) && Number.isInteger(item.motion.out.duration)
|
|
@@ -9,7 +9,7 @@ const ITEM_KEY_ORDER = [
|
|
|
9
9
|
'blend', 'crop', 'perspective', 'motion', 'animator', 'keyframes', 'source', 'items'
|
|
10
10
|
];
|
|
11
11
|
const EDIT_KEY_ORDER = ['version', 'output', 'sources', 'audio', 'tracks'];
|
|
12
|
-
const TRACK_KEY_ORDER = ['id', 'lane', 'name', 'items', 'content'];
|
|
12
|
+
const TRACK_KEY_ORDER = ['id', 'lane', 'name', 'muted', 'items', 'content'];
|
|
13
13
|
const CAPTION_KEY_ORDER = [
|
|
14
14
|
'id', 'start', 'end', 'text', 'speaker', 'sourceRef', 'edited', 'time_domain', 'text_style'
|
|
15
15
|
];
|
|
@@ -325,12 +325,16 @@ export interface VisualItemsTrackV2 {
|
|
|
325
325
|
id: string;
|
|
326
326
|
lane: 'visual';
|
|
327
327
|
name?: string;
|
|
328
|
+
/** トラックの音声をミュート。visual は cut の埋め込み音声、audio は item を書き出し・プレビューから除外。 */
|
|
329
|
+
muted?: boolean;
|
|
328
330
|
items: ItemV2[];
|
|
329
331
|
}
|
|
330
332
|
export interface AudioItemsTrackV2 {
|
|
331
333
|
id: string;
|
|
332
334
|
lane: 'audio';
|
|
333
335
|
name?: string;
|
|
336
|
+
/** トラックの音声をミュート。visual は cut の埋め込み音声、audio は item を書き出し・プレビューから除外。 */
|
|
337
|
+
muted?: boolean;
|
|
334
338
|
items: AudioMediaItemV2[];
|
|
335
339
|
}
|
|
336
340
|
export type ItemsTrackV2 = VisualItemsTrackV2 | AudioItemsTrackV2;
|
|
@@ -338,6 +342,8 @@ export interface ContentTrackV2 {
|
|
|
338
342
|
id: string;
|
|
339
343
|
lane: LaneV2;
|
|
340
344
|
name?: string;
|
|
345
|
+
/** トラックの音声をミュート。visual は cut の埋め込み音声、audio は item を書き出し・プレビューから除外。 */
|
|
346
|
+
muted?: boolean;
|
|
341
347
|
content: CaptionTrackContentV2;
|
|
342
348
|
}
|
|
343
349
|
export type TrackV2 = ItemsTrackV2 | ContentTrackV2;
|
|
@@ -120,7 +120,7 @@ function validateEditSource(value, index, ids) {
|
|
|
120
120
|
function validateTrack(value, index, trackIds, itemIds, sourceIds) {
|
|
121
121
|
const path = `edit.json.tracks[${index}]`;
|
|
122
122
|
requireRecord(value, path);
|
|
123
|
-
requireExactKeys(value, new Set(['id', 'lane', 'name', 'items', 'content']), path);
|
|
123
|
+
requireExactKeys(value, new Set(['id', 'lane', 'name', 'muted', 'items', 'content']), path);
|
|
124
124
|
requireText(value.id, `${path}.id`);
|
|
125
125
|
if (trackIds.has(value.id))
|
|
126
126
|
throw invalid(`${path}.id`, `track id が重複しています: ${value.id}`);
|
|
@@ -131,6 +131,9 @@ function validateTrack(value, index, trackIds, itemIds, sourceIds) {
|
|
|
131
131
|
if (hasOwn(value, 'name') && typeof value.name !== 'string') {
|
|
132
132
|
throw invalid(`${path}.name`, '文字列である必要があります');
|
|
133
133
|
}
|
|
134
|
+
if (hasOwn(value, 'muted') && typeof value.muted !== 'boolean') {
|
|
135
|
+
throw invalid(`${path}.muted`, 'boolean である必要があります');
|
|
136
|
+
}
|
|
134
137
|
const hasItems = hasOwn(value, 'items');
|
|
135
138
|
const hasContent = hasOwn(value, 'content');
|
|
136
139
|
if (hasItems === hasContent) {
|
|
@@ -227,6 +227,7 @@ function readV2Internal(raw) {
|
|
|
227
227
|
lane: track.lane,
|
|
228
228
|
z: track.z,
|
|
229
229
|
...(track.name !== undefined ? { name: track.name } : {}),
|
|
230
|
+
...(track.muted === undefined ? {} : { muted: track.muted }),
|
|
230
231
|
origin: 'declared',
|
|
231
232
|
...('content' in track ? { content: { from: 'captions.json' } } : {}),
|
|
232
233
|
items,
|
|
@@ -1087,6 +1088,8 @@ function projectLegacyEdit(internal) {
|
|
|
1087
1088
|
const audioNarration = [];
|
|
1088
1089
|
let audioBgm;
|
|
1089
1090
|
for (const track of internal.tracks) {
|
|
1091
|
+
if (track.lane === 'audio' && track.muted === true)
|
|
1092
|
+
continue;
|
|
1090
1093
|
for (const item of track.items) {
|
|
1091
1094
|
const value = item.legacy.value;
|
|
1092
1095
|
if (value === undefined) {
|
|
@@ -1116,7 +1119,11 @@ function projectLegacyEdit(internal) {
|
|
|
1116
1119
|
layers.push({ index: item.legacy.index, value: value });
|
|
1117
1120
|
break;
|
|
1118
1121
|
default:
|
|
1119
|
-
cuts.push({
|
|
1122
|
+
cuts.push({
|
|
1123
|
+
index: item.legacy.index,
|
|
1124
|
+
value: (track.lane === 'visual' && track.muted === true
|
|
1125
|
+
? { ...value, mute: true } : value)
|
|
1126
|
+
});
|
|
1120
1127
|
break;
|
|
1121
1128
|
}
|
|
1122
1129
|
break;
|
|
@@ -7,6 +7,7 @@ exports.projectLegacyAudioView = projectLegacyAudioView;
|
|
|
7
7
|
*/
|
|
8
8
|
function projectLegacyAudioView(internal) {
|
|
9
9
|
const ordered = internal.tracks
|
|
10
|
+
.filter(track => !(track.lane === 'audio' && track.muted === true))
|
|
10
11
|
.flatMap(track => track.items)
|
|
11
12
|
.filter(item => item.legacy.collection === 'sfx'
|
|
12
13
|
|| item.legacy.collection === 'narration'
|
|
@@ -1864,7 +1864,7 @@ var AkariEditKernel = (() => {
|
|
|
1864
1864
|
function validateTrack(value, index, trackIds, itemIds, sourceIds) {
|
|
1865
1865
|
const path = `edit.json.tracks[${index}]`;
|
|
1866
1866
|
requireRecord(value, path);
|
|
1867
|
-
requireExactKeys(value, /* @__PURE__ */ new Set(["id", "lane", "name", "items", "content"]), path);
|
|
1867
|
+
requireExactKeys(value, /* @__PURE__ */ new Set(["id", "lane", "name", "muted", "items", "content"]), path);
|
|
1868
1868
|
requireText(value.id, `${path}.id`);
|
|
1869
1869
|
if (trackIds.has(value.id)) throw invalid(`${path}.id`, `track id \u304C\u91CD\u8907\u3057\u3066\u3044\u307E\u3059: ${value.id}`);
|
|
1870
1870
|
trackIds.add(value.id);
|
|
@@ -1874,6 +1874,9 @@ var AkariEditKernel = (() => {
|
|
|
1874
1874
|
if (hasOwn(value, "name") && typeof value.name !== "string") {
|
|
1875
1875
|
throw invalid(`${path}.name`, "\u6587\u5B57\u5217\u3067\u3042\u308B\u5FC5\u8981\u304C\u3042\u308A\u307E\u3059");
|
|
1876
1876
|
}
|
|
1877
|
+
if (hasOwn(value, "muted") && typeof value.muted !== "boolean") {
|
|
1878
|
+
throw invalid(`${path}.muted`, "boolean \u3067\u3042\u308B\u5FC5\u8981\u304C\u3042\u308A\u307E\u3059");
|
|
1879
|
+
}
|
|
1877
1880
|
const hasItems = hasOwn(value, "items");
|
|
1878
1881
|
const hasContent = hasOwn(value, "content");
|
|
1879
1882
|
if (hasItems === hasContent) {
|
|
@@ -2554,6 +2557,7 @@ var AkariEditKernel = (() => {
|
|
|
2554
2557
|
lane: track.lane,
|
|
2555
2558
|
z: track.z,
|
|
2556
2559
|
...track.name !== void 0 ? { name: track.name } : {},
|
|
2560
|
+
...track.muted === void 0 ? {} : { muted: track.muted },
|
|
2557
2561
|
origin: "declared",
|
|
2558
2562
|
..."content" in track ? { content: { from: "captions.json" } } : {},
|
|
2559
2563
|
items,
|
|
@@ -3388,6 +3392,7 @@ var AkariEditKernel = (() => {
|
|
|
3388
3392
|
const audioNarration = [];
|
|
3389
3393
|
let audioBgm;
|
|
3390
3394
|
for (const track of internal.tracks) {
|
|
3395
|
+
if (track.lane === "audio" && track.muted === true) continue;
|
|
3391
3396
|
for (const item of track.items) {
|
|
3392
3397
|
const value = item.legacy.value;
|
|
3393
3398
|
if (value === void 0) {
|
|
@@ -3412,7 +3417,10 @@ var AkariEditKernel = (() => {
|
|
|
3412
3417
|
layers.push({ index: item.legacy.index, value });
|
|
3413
3418
|
break;
|
|
3414
3419
|
default:
|
|
3415
|
-
cuts.push({
|
|
3420
|
+
cuts.push({
|
|
3421
|
+
index: item.legacy.index,
|
|
3422
|
+
value: track.lane === "visual" && track.muted === true ? { ...value, mute: true } : value
|
|
3423
|
+
});
|
|
3416
3424
|
break;
|
|
3417
3425
|
}
|
|
3418
3426
|
break;
|
|
@@ -1558,6 +1558,7 @@
|
|
|
1558
1558
|
"id": { "type": "string", "minLength": 1, "pattern": "\\S" },
|
|
1559
1559
|
"lane": { "enum": ["visual", "audio"] },
|
|
1560
1560
|
"name": { "type": "string" },
|
|
1561
|
+
"muted": { "type": "boolean" },
|
|
1561
1562
|
"items": { "type": "array" }
|
|
1562
1563
|
}
|
|
1563
1564
|
},
|
|
@@ -1569,11 +1570,12 @@
|
|
|
1569
1570
|
"id": { "type": "string", "minLength": 1, "pattern": "\\S" },
|
|
1570
1571
|
"lane": { "enum": ["visual", "audio"] },
|
|
1571
1572
|
"name": { "type": "string" },
|
|
1573
|
+
"muted": { "type": "boolean" },
|
|
1572
1574
|
"content": { "$ref": "#/$defs/captionTrackContentV2" }
|
|
1573
1575
|
}
|
|
1574
1576
|
}
|
|
1575
1577
|
],
|
|
1576
|
-
"$comment": "deprecated: tracks[].content は tolerant reader のためだけに残す。lane は visual/audio の 2 値。audio lane の items は itemV2AudioMedia、visual lane の items は itemV2 を使う。items と字幕 content は閉じた oneOf により必ず排他。"
|
|
1578
|
+
"$comment": "deprecated: tracks[].content は tolerant reader のためだけに残す。lane は visual/audio の 2 値。audio lane の items は itemV2AudioMedia、visual lane の items は itemV2 を使う。items と字幕 content は閉じた oneOf により必ず排他。muted はトラックの音声をミュート。visual トラックなら cut の埋め込み音声、audio トラックなら item を書き出し・プレビューから除外。"
|
|
1577
1579
|
},
|
|
1578
1580
|
"editV0": {
|
|
1579
1581
|
"type": "object",
|
|
@@ -29,7 +29,7 @@
|
|
|
29
29
|
{ "path": "tracks[].items[].perspective", "applies_to": ["cuts"], "gpu": "ignored", "osr": "ignored", "runtime_warning": true, "evidence": "packages/frame-engine/src/timeline/plan.ts cutDeclaresPerspective warning; layerStyleVisualAt does not read perspective", "issue": 39, "hint": "layers 経路(mask / 非 normal blend)へ寄せるか perspective を外す" },
|
|
30
30
|
{ "path": "tracks[].items[].perspective", "applies_to": ["layers", "baked"], "gpu": "consumed", "osr": "consumed", "evidence": "packages/frame-engine/src/timeline/plan.ts resolvedCompositeLayers" },
|
|
31
31
|
{ "path": "tracks[].items[].perspective", "applies_to": ["overlays", "captions", "group"], "gpu": "ignored", "osr": "ignored", "evidence": "packages/render-cut/src/rasterize.mjs renderOverlayNode applies no perspective geometry", "hint": "overlay の HTML/CSS 内で perspective を表現する" },
|
|
32
|
-
{ "path": "tracks[].items[].motion", "applies_to": ["cuts", "layers", "baked"], "gpu": "
|
|
32
|
+
{ "path": "tracks[].items[].motion", "applies_to": ["cuts", "layers", "baked"], "gpu": "consumed", "osr": "consumed", "evidence": "packages/frame-engine/src/timeline/item-motion.ts motionVisualAt is composed by packages/frame-engine/src/timeline/plan.ts resolvedCompositeLayers and visualAt for layer and cut evaluation" },
|
|
33
33
|
{ "path": "tracks[].items[].motion", "applies_to": ["overlays", "captions", "group"], "gpu": "ignored", "osr": "ignored", "evidence": "packages/render-cut/src/rasterize.mjs renderOverlayNode has no item motion evaluator; packages/gpu-export/src/page-runtime.js rasterizeSprite only probes legacy duration_sec", "hint": "overlay の HTML/CSS animation または keyframes を使う" },
|
|
34
34
|
{ "path": "tracks[].items[].animator", "applies_to": ["cuts", "layers", "baked"], "gpu": "ignored", "osr": "ignored", "runtime_warning": true, "evidence": "packages/edit-store/src/internal-model.ts buildV2VisualItem projects animator; packages/frame-engine/src/timeline/plan.ts KNOWN_CUT_KEYS/KNOWN_LAYER_KEYS omit animator" },
|
|
35
35
|
{ "path": "tracks[].items[].animator", "applies_to": ["overlays", "captions", "group"], "gpu": "ignored", "osr": "ignored", "evidence": "packages/render-cut/src/rasterize.mjs renderOverlayNode has no animator evaluator", "hint": "animator 対応が実装されるまで宣言しない" },
|
|
@@ -15,6 +15,20 @@ function fixture(name) {
|
|
|
15
15
|
return JSON.parse(readFileSync(join(examplesRoot, name, "edit.json"), "utf8"));
|
|
16
16
|
}
|
|
17
17
|
|
|
18
|
+
test("track muted accepts booleans and rejects other types in both items and content branches", () => {
|
|
19
|
+
for (const trackIndex of [0, 3, 4]) {
|
|
20
|
+
for (const muted of [true, false, 1, "yes", null]) {
|
|
21
|
+
const value = fixture("edit-v2-valid");
|
|
22
|
+
value.tracks[trackIndex].muted = muted;
|
|
23
|
+
assert.equal(validate(value), typeof muted === "boolean", JSON.stringify(validate.errors));
|
|
24
|
+
if (typeof muted !== "boolean") {
|
|
25
|
+
assert.ok(validate.errors.some(error => error.instancePath === `/tracks/${trackIndex}/muted`
|
|
26
|
+
&& error.keyword === "type"));
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
});
|
|
31
|
+
|
|
18
32
|
test("visual media source validates embedded speech gain and mute", () => {
|
|
19
33
|
const value = fixture("edit-v2-valid");
|
|
20
34
|
Object.assign(value.tracks[3].items[0].source, { gain_db: -12, mute: true });
|