akari-video 1.2.0-beta.4 → 1.2.0

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 (25) hide show
  1. package/package.json +1 -1
  2. package/src/kits.mjs +9 -11
  3. package/src/vendor-sources.mjs +2 -0
  4. package/vendor/docs/contract-2026-08-30-edit-json-v2-object-tree-v0.md +20 -0
  5. package/vendor/packages/akari-launcher/package.json +1 -1
  6. package/vendor/packages/edit-lint/src/engine-capabilities.json +3 -1
  7. package/vendor/packages/edit-lint/src/lint/edit-v2.mjs +26 -0
  8. package/vendor/packages/edit-store/lib/caption-timeline.d.ts +7 -0
  9. package/vendor/packages/edit-store/lib/caption-timeline.js +46 -0
  10. package/vendor/packages/edit-store/lib/cut-ranges.js +918 -14
  11. package/vendor/packages/edit-store/lib/edit-v2.d.ts +23 -0
  12. package/vendor/packages/edit-store/lib/edit-v2.js +89 -3
  13. package/vendor/packages/edit-store/lib/generated/edit-v2-keys.d.ts +3 -3
  14. package/vendor/packages/edit-store/lib/generated/edit-v2-keys.js +5 -2
  15. package/vendor/packages/edit-store/lib/internal-model.d.ts +2 -1
  16. package/vendor/packages/edit-store/lib/internal-model.js +1 -0
  17. package/vendor/packages/edit-store/lib/webview-kernel.js +88 -2
  18. package/vendor/packages/frame-scene/src/schema.mjs +102 -0
  19. package/vendor/packages/overlay-runtime/src/fragment-source-write.mjs +21 -6
  20. package/vendor/packages/overlay-runtime/src/interaction.js +175 -64
  21. package/vendor/packages/schemas/bin/validate-edit.mjs +25 -0
  22. package/vendor/packages/schemas/edit.schema.json +26 -1
  23. package/vendor/packages/schemas/engine-capabilities.json +3 -1
  24. package/vendor/packages/schemas/test/edit-v2-schema.test.mjs +16 -0
  25. package/vendor/packages/schemas/test/engine-capabilities.test.mjs +1 -1
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "akari-video",
3
- "version": "1.2.0-beta.4",
3
+ "version": "1.2.0",
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/kits.mjs CHANGED
@@ -8,6 +8,7 @@ import {
8
8
  import path from 'node:path';
9
9
 
10
10
  import { resolveLauncherAssets } from './repo-assets.mjs';
11
+ import { compareVersions } from './version.mjs';
11
12
  const creatorRootModulePath = resolveLauncherAssets().creatorRootModulePath;
12
13
  const creatorRoot = creatorRootModulePath ? await import(pathToFileURL(creatorRootModulePath).href) : null;
13
14
  function resolveAssetLibraryRoots(env) {
@@ -20,15 +21,11 @@ const INSTALLED_ASSETS_SCHEMA = 'akari-installed-assets/v0';
20
21
  const PLUGIN_DESCRIPTION = 'AKARI Video 拡張キットのスキルをまとめて提供するローカルプラグイン。';
21
22
 
22
23
  function parseVersion(value) {
23
- const match = String(value).match(/^(\d+)\.(\d+)\.(\d+)$/u);
24
- return match ? match.slice(1).map(Number) : null;
25
- }
26
-
27
- function compareVersions(left, right) {
28
- for (let index = 0; index < 3; index += 1) {
29
- if (left[index] !== right[index]) return left[index] - right[index];
30
- }
31
- return 0;
24
+ const match = typeof value === 'string'
25
+ ? value.match(/^(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)(?:-([0-9A-Za-z-]+(?:\.[0-9A-Za-z-]+)*))?(?:\+[0-9A-Za-z.-]+)?$/u)
26
+ : null;
27
+ if (!match || match[4]?.split('.').some(id => /^0\d+$/u.test(id))) return null;
28
+ return match.slice(1, 4).map(Number);
32
29
  }
33
30
 
34
31
  function satisfies(version, range) {
@@ -36,13 +33,14 @@ function satisfies(version, range) {
36
33
  const match = String(range).match(/^(\^|~|>=)?(\d+\.\d+\.\d+)$/u);
37
34
  if (!actual || !match) return false;
38
35
  const required = parseVersion(match[2]);
39
- if (compareVersions(actual, required) < 0) return false;
36
+ if (!required) return false;
37
+ if (compareVersions(version, match[2]) < 0) return false;
40
38
  if (match[1] === '^') {
41
39
  return actual[0] === required[0] && (required[0] !== 0 || actual[1] === required[1]);
42
40
  }
43
41
  if (match[1] === '~') return actual[0] === required[0] && actual[1] === required[1];
44
42
  if (match[1] === '>=') return true;
45
- return compareVersions(actual, required) === 0;
43
+ return compareVersions(version, match[2]) === 0;
46
44
  }
47
45
 
48
46
  export function readKitManifest(kitDir) {
@@ -14,6 +14,8 @@ export const VENDOR_SOURCES = [
14
14
  'presets/textanim',
15
15
  'assets/font/noto-sans-jp/NotoSansJP-Variable.ttf',
16
16
  'packages/schemas',
17
+ // schemas の frame-scene 検証器が参照する schema 定義だけを同梱する。
18
+ 'packages/frame-scene/src/schema.mjs',
17
19
  // schemas の asset / kit 検証は runtimes.mjs を実行し、runtime 宣言は src の
18
20
  // レジストリ・ブラウザスクリプトと既定フォントをファイルとして読み込む。
19
21
  'packages/overlay-runtime/runtimes.mjs',
@@ -347,4 +347,24 @@ await p.save(); // 正規直列化 → lint ゲート
347
347
 
348
348
  ## 音声片のカット端
349
349
 
350
+ visual lane の media item も、組にした素材のカットを戻すため任意の `cut_edge: { "in": 秒, "out": 秒, "at": 整数フレーム }` を持てる。元の素材端と配置を編集用に記録する欄であり、再生は現在の `source.in/out` と `at/duration` に従う。
351
+
352
+ audio item の任意の `reason`(`silence` / `word`)と `label` はカット復元用の編集情報である。映像と音声を組にした行カットでは、行に元から値がある場合だけ引き継ぐ。
353
+
350
354
  edit.json v2 の audio lane にある分離済み speech item は、カットで素材端が失われたとき、任意の `cut_edge: { "in": 秒, "out": 秒, "at": 整数フレーム }` を持てる。`in` と `out` はこの音声片自身の元の素材端であり、頭がまたぎで削られた場合だけ `in` が現在の `source.in` と異なり、尻がまたぎで削られた場合だけ `out` が現在の `source.out` と異なる。両端が現在の素材端と等しくなれば欄を消す。`at` は「頭を `cut_edge.in` まで戻したときの開始フレーム」で、現在の `at - round((source.in - cut_edge.in) / 1 フレームぶんの素材秒)` から求める派生値である。カットで音声片を割ったときはそれぞれの片の端だけを保持し、結合時は左片の `in` と右片の `out` を採る。`in` と `out` は 0 以上の有限数で `in < out`、`at` は整数。先行カットのリップルを反映した途中状態では `cut_edge.at` が負になることもある。再生範囲の `source.in/out` と現在位置の `at` はそのまま正本とする。`provenance` はナレーションの生成元専用で、この用途には使わない。欄のない既存 item の意味は変わらない。
355
+
356
+ ## 別ファイル素材の同期の組
357
+
358
+ v2 の任意欄 `sync_groups` は、同じ収録に属する `sources[].id` を結ぶ。`link` は同じ素材から分離した映像 item と音声 item の関係なので、別ファイルの同期には使わない。
359
+
360
+ ```json
361
+ "sync_groups": [{
362
+ "id": "take-1",
363
+ "members": [
364
+ { "source": "camera", "offset_sec": 0 },
365
+ { "source": "microphone", "offset_sec": 0.12 }
366
+ ]
367
+ }]
368
+ ```
369
+
370
+ 最初の member が基準で `offset_sec` は 0。ほかの offset は基準素材の時刻に加える素材秒であり、負の値も許す。各組は 2 素材以上、id は組間で一意、素材 id は存在する `sources[]` を参照し、1 素材は 1 組だけに入れる。欄を省略した既存データの挙動は変わらない。組の初期配置には offset を使えるが、台本から組の一員を切る位置と字幕の時刻はタイムライン上の実際の配置から決める。声の切り口は映像が実際に切れた出力フレームに合わせる。組外の素材は対象にならない。
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "akari-video",
3
- "version": "1.2.0-beta.4",
3
+ "version": "1.2.0",
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": [
@@ -5,7 +5,9 @@
5
5
  "fields": [
6
6
  { "path": "tracks[].items[].audio", "applies_to": ["cuts"], "gpu": "consumed", "osr": "consumed", "evidence": "packages/render-cut/src/internal-render.mjs projectRendererCompatibilityEdit; packages/render-cut/src/plan.mjs cut_audio uses isCutAudioAudible before GPU/OSR audioSourcePath mux" },
7
7
  { "path": "tracks[].items[].link", "applies_to": ["audio"], "gpu": "ignored", "osr": "ignored", "evidence": "Editing association only. Engines use the audio item source and timing; link is not consumed." },
8
- { "path": "tracks[].items[].cut_edge", "applies_to": ["audio"], "gpu": "ignored", "osr": "ignored", "evidence": "Editing-only restore record of the pre-cut audio edge; engines use the item source and timing." },
8
+ { "path": "tracks[].items[].cut_edge", "applies_to": ["audio", "cuts", "layers", "overlays", "baked"], "gpu": "ignored", "osr": "ignored", "evidence": "Editing-only restore record of the pre-cut media edge on visual and audio items; engines use the item source and timing." },
9
+ { "path": "tracks[].items[].reason", "applies_to": ["audio"], "gpu": "ignored", "osr": "ignored", "evidence": "Editing-only provenance on surviving audio pieces for cut restoration; engines use the item source and timing." },
10
+ { "path": "tracks[].items[].label", "applies_to": ["audio"], "gpu": "ignored", "osr": "ignored", "evidence": "Editing-only provenance label on surviving audio pieces for cut restoration; engines use the item source and timing." },
9
11
  { "path": "tracks[].items[].mute", "applies_to": ["audio"], "gpu": "other-subsystem", "osr": "other-subsystem", "consumer": "render-cut audio mix", "evidence": "packages/edit-store/src/audio-ownership.ts isAudioItemAudible; packages/render-cut/src/internal-render.mjs and plan.mjs audio mix" },
10
12
  { "path": "tracks[].items[].id", "applies_to": ["cuts", "layers", "overlays", "baked", "captions", "group"], "gpu": "consumed", "osr": "consumed", "evidence": "packages/edit-store/src/internal-model.ts buildV2VisualItem; packages/frame-engine/src/timeline/plan.ts layerFromPlacement/resolvedCompositeLayers; packages/render-cut/src/internal-render.mjs captionItemOverlays/renderItemDeclaration" },
11
13
  { "path": "tracks[].items[].id", "applies_to": ["audio"], "gpu": "other-subsystem", "osr": "other-subsystem", "consumer": "render-cut audio mix", "evidence": "packages/edit-store/src/internal-model.ts buildV2AudioItem" },
@@ -151,6 +151,32 @@ export function validateEditV2(edit, findings) {
151
151
  }
152
152
  }
153
153
 
154
+ if (edit.sync_groups !== undefined) {
155
+ const groupIds = new Set();
156
+ const grouped = new Set();
157
+ const fail = (path, message) => addFinding(findings, {
158
+ severity: "error", check: "v2.sync-groups", message, path,
159
+ });
160
+ if (!Array.isArray(edit.sync_groups)) fail("edit.json#sync_groups", "sync_groups must be an array");
161
+ else for (const [index, group] of edit.sync_groups.entries()) {
162
+ const path = `edit.json#sync_groups[${index}]`;
163
+ if (!isRecord(group) || !isNonEmptyString(group.id) || !Array.isArray(group.members)
164
+ || group.members.length < 2) { fail(path, "sync group needs an id and at least two members"); continue; }
165
+ if (groupIds.has(group.id)) fail(`${path}.id`, "duplicate sync group id");
166
+ groupIds.add(group.id);
167
+ for (const [memberIndex, member] of group.members.entries()) {
168
+ const memberPath = `${path}.members[${memberIndex}]`;
169
+ if (!isRecord(member) || !sourceIds.has(member.source)
170
+ || typeof member.offset_sec !== "number" || !Number.isFinite(member.offset_sec)) {
171
+ fail(memberPath, "sync member needs an existing source and finite offset_sec"); continue;
172
+ }
173
+ if (grouped.has(member.source)) fail(`${memberPath}.source`, "source belongs to more than one sync group");
174
+ grouped.add(member.source);
175
+ if (memberIndex === 0 && member.offset_sec !== 0) fail(`${memberPath}.offset_sec`, "first member offset must be 0");
176
+ }
177
+ }
178
+ }
179
+
154
180
  const fps = edit.output?.fps;
155
181
 
156
182
  for (const [trackIndex, track] of edit.tracks.entries()) {
@@ -5,6 +5,13 @@ type RawCaptionEdit = {
5
5
  output?: {
6
6
  fps?: number;
7
7
  };
8
+ sync_groups?: Array<{
9
+ id: string;
10
+ members: Array<{
11
+ source: string;
12
+ offset_sec: number;
13
+ }>;
14
+ }>;
8
15
  tracks: Array<{
9
16
  lane?: string;
10
17
  muted?: boolean;
@@ -9,6 +9,8 @@ function buildCaptionTimelineSegments(cuts, edit, options = {}) {
9
9
  return visual;
10
10
  const audio = [];
11
11
  const fps = options.fps ?? edit.output?.fps ?? 30;
12
+ const syncGroups = edit.syncGroups
13
+ ?? edit.sync_groups;
12
14
  const subtract = (parts, start, end) => parts.flatMap(part => end <= part.start || start >= part.end ? [part] : [
13
15
  ...(start > part.start ? [{ start: part.start, end: Math.min(start, part.end) }] : []),
14
16
  ...(end < part.end ? [{ start: Math.max(end, part.start), end: part.end }] : [])
@@ -54,6 +56,50 @@ function buildCaptionTimelineSegments(cuts, edit, options = {}) {
54
56
  if (!(duration > 0) || !(speed > 0))
55
57
  continue;
56
58
  let uncovered = [{ start: at, end: at + duration }];
59
+ const group = syncGroups?.find(candidate => candidate.members.some(member => member.source === src));
60
+ if (group) {
61
+ const projected = [];
62
+ for (const member of group.members) {
63
+ if (member.source === src)
64
+ continue;
65
+ for (const visualTrack of edit.tracks) {
66
+ if (visualTrack.lane !== 'visual')
67
+ continue;
68
+ for (const visualItem of visualTrack.items ?? []) {
69
+ const timedVisual = visualItem;
70
+ const visualSource = visualItem.source;
71
+ if (visualSource.kind !== 'media' || (visualSource.sourceId ?? visualSource.src) !== member.source
72
+ || visualSource.in === undefined || visualSource.out === undefined)
73
+ continue;
74
+ const visualAt = typeof timedVisual.atFrames === 'number' ? timedVisual.at ?? 0 : (timedVisual.at ?? 0) / fps;
75
+ const visualDuration = typeof timedVisual.durationFrames === 'number'
76
+ ? timedVisual.duration ?? 0 : (timedVisual.duration ?? 0) / fps;
77
+ const start = Math.max(at, visualAt);
78
+ const end = Math.min(at + duration, visualAt + visualDuration);
79
+ if (end <= start)
80
+ continue;
81
+ projected.push({ start, end,
82
+ in: sourceIn + (start - at) * speed,
83
+ out: sourceIn + (end - at) * speed });
84
+ }
85
+ }
86
+ }
87
+ if (projected.length) {
88
+ for (const part of projected)
89
+ audio.push({ kind: 'src', outStart: part.start,
90
+ outEnd: part.end, cutIndex: null, src, in: part.in, out: part.out,
91
+ speed: (part.out - part.in) / (part.end - part.start) });
92
+ let sourceRemainder = [{ start: sourceIn, end: sourceOut }];
93
+ for (const part of projected)
94
+ sourceRemainder = subtract(sourceRemainder, part.in, part.out);
95
+ for (const part of sourceRemainder)
96
+ audio.push({ kind: 'src',
97
+ outStart: at + (part.start - sourceIn) / speed,
98
+ outEnd: at + (part.end - sourceIn) / speed,
99
+ cutIndex: null, src, in: part.start, out: part.end, speed });
100
+ continue;
101
+ }
102
+ }
57
103
  const root = item.id?.match(/^(.*)-audio(?:-split(?:-\d+)*)*$/)?.[1];
58
104
  const family = root && visualFamilies.get(`${src}\0${root}`);
59
105
  if (family && (family.length > 1 || family[0].id !== root