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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "akari-video",
3
- "version": "0.1.46",
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.46",
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({ index: item.legacy.index, value: value });
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({ index: item.legacy.index, value });
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": "ignored", "osr": "ignored", "runtime_warning": true, "evidence": "packages/edit-store/src/internal-model.ts buildV2VisualItem projects motion; packages/frame-engine/src/timeline/plan.ts KNOWN_CUT_KEYS/KNOWN_LAYER_KEYS omit motion" },
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 });