akari-video 0.1.20 → 0.1.21

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 (26) hide show
  1. package/package.json +1 -1
  2. package/vendor/.akari-capability-sources.json +1 -0
  3. package/vendor/docs/contract-2026-07-23-analysis-person-matte.md +102 -2
  4. package/vendor/packages/akari-launcher/package.json +1 -1
  5. package/vendor/packages/edit-lint/src/edit-lint.mjs +31 -22
  6. package/vendor/packages/edit-store/lib/cut-adjacency.d.ts +17 -17
  7. package/vendor/packages/edit-store/lib/cut-adjacency.js +23 -36
  8. package/vendor/packages/edit-store/lib/edit-store.d.ts +8 -18
  9. package/vendor/packages/edit-store/lib/edit-store.js +24 -58
  10. package/vendor/packages/edit-store/lib/index.d.ts +1 -0
  11. package/vendor/packages/edit-store/lib/index.js +1 -0
  12. package/vendor/packages/edit-store/lib/internal-model.js +62 -0
  13. package/vendor/packages/edit-store/lib/migrate/legacy-parse.js +9 -6
  14. package/vendor/packages/edit-store/lib/timeline-map.d.ts +5 -0
  15. package/vendor/packages/edit-store/lib/timeline-map.js +40 -0
  16. package/vendor/packages/edit-store/lib/transition-vocabulary.d.ts +223 -0
  17. package/vendor/packages/edit-store/lib/transition-vocabulary.js +45 -0
  18. package/vendor/packages/edit-store/lib/webview-kernel.js +98 -0
  19. package/vendor/packages/matte-rvm/package.json +22 -0
  20. package/vendor/packages/schemas/bin/validate-edit.mjs +5 -2
  21. package/vendor/packages/schemas/edit.schema.json +2 -2
  22. package/vendor/packages/schemas/test/transition-vocabulary.test.mjs +64 -0
  23. package/vendor/packages/schemas/test/validate-edit.test.mjs +2 -2
  24. package/vendor/skills/analyze-footage/bin/person-matte/person-cutout.mjs +453 -0
  25. package/vendor/skills/analyze-footage/bin/person-matte/person-matte.mjs +163 -48
  26. package/vendor/skills/analyze-footage/person-matte.md +135 -5
@@ -12,6 +12,7 @@ exports.projectLegacyEdit = projectLegacyEdit;
12
12
  exports.toLegacyTrack = toLegacyTrack;
13
13
  exports.derivedLegacyTracks = derivedLegacyTracks;
14
14
  const edit_v2_1 = require("./edit-v2");
15
+ const cut_adjacency_1 = require("./cut-adjacency");
15
16
  const error_1 = require("./migrate/error");
16
17
  /**
17
18
  * edit.json v2 を内部表現へ読む。v0/v1 は凍結変換ユニットのみが読む。
@@ -172,6 +173,7 @@ function readV2Internal(raw) {
172
173
  // 旧 top-level audio と tracks[] 音声が同居すると、後者に加えてこの fallback も射影される。
173
174
  // どちらを優先するかは未裁定なので、旧 fixture の互換挙動を変えず二重計上の可能性を残す。
174
175
  addV2AudioItems(tracks, edit.audio, fps, legacyIndexCounters);
176
+ synthesizeHiddenTransitionHandlesForRender(tracks, fps);
175
177
  return {
176
178
  output: {
177
179
  width: edit.output.width,
@@ -191,6 +193,66 @@ function readV2Internal(raw) {
191
193
  }
192
194
  };
193
195
  }
196
+ /**
197
+ * render-cut に加え shell プレビューの summary と Web UI の readRenderEdit も
198
+ * item.declaration を読む。そのため出荷プレビューはこの合成済み実重なりを既存窓として描く。
199
+ * UI / lint が読む item の at/duration/source と legacy.value は生タイムラインのまま保ち、
200
+ * 突き合わせ境界の隠れのりしろだけを declaration 上の物理重なりへ合成して既存
201
+ * xfade/acrossfade と 2 つのプレビューへ渡す。生 cuts を直接読む消費者の隠れのりしろ経路とは
202
+ * packages/edit-store/test/transition-window-path-equivalence.test.mjs で等価性を固定する。
203
+ */
204
+ function synthesizeHiddenTransitionHandlesForRender(tracks, fps) {
205
+ const speedOf = (item) => {
206
+ const speed = item.declaration.speed;
207
+ return typeof speed === 'number' && Number.isFinite(speed) && speed > 0 ? speed : 1;
208
+ };
209
+ for (const track of tracks) {
210
+ if (track.lane !== 'visual')
211
+ continue;
212
+ const cuts = track.items.filter((item) => item.legacy.collection === 'cuts' && item.source.kind === 'media');
213
+ for (let index = 0; index + 1 < cuts.length; index++) {
214
+ const outgoing = cuts[index];
215
+ const incoming = cuts[index + 1];
216
+ if ((0, cut_adjacency_1.cutOverlapFrames)({ tlEnd: outgoing.at + outgoing.duration }, { tlStart: incoming.at }, fps) !== 0)
217
+ continue;
218
+ const transition = outgoing.declaration.transition_out;
219
+ if (!isRecord(transition)
220
+ || typeof transition.duration !== 'number'
221
+ || !Number.isFinite(transition.duration)
222
+ || transition.duration <= 0)
223
+ continue;
224
+ const incomingSpeed = speedOf(incoming);
225
+ const incomingStill = (0, cut_adjacency_1.isStillImageSourcePath)(incoming.source.path);
226
+ const plan = (0, cut_adjacency_1.planTransitionHandleWindow)({
227
+ declaredSeconds: transition.duration,
228
+ outgoingTailRoomSeconds: Number.POSITIVE_INFINITY,
229
+ incomingHeadRoomSeconds: incomingStill
230
+ ? Number.POSITIVE_INFINITY : incoming.source.in / incomingSpeed,
231
+ outgoingDurationSeconds: outgoing.duration,
232
+ incomingDurationSeconds: incoming.duration
233
+ });
234
+ if (plan.effectiveSeconds <= 0)
235
+ continue;
236
+ const outgoingSpeed = speedOf(outgoing);
237
+ outgoing.declaration = {
238
+ ...outgoing.declaration,
239
+ out: Number(outgoing.declaration.out) + plan.halfSeconds * outgoingSpeed,
240
+ transition_out: { ...transition, duration: plan.effectiveSeconds }
241
+ };
242
+ incoming.declaration = incomingStill
243
+ ? {
244
+ ...incoming.declaration,
245
+ at: Number(incoming.declaration.at) - plan.halfSeconds,
246
+ out: Number(incoming.declaration.out) + plan.halfSeconds * incomingSpeed
247
+ }
248
+ : {
249
+ ...incoming.declaration,
250
+ at: Number(incoming.declaration.at) - plan.halfSeconds,
251
+ in: Number(incoming.declaration.in) - plan.halfSeconds * incomingSpeed
252
+ };
253
+ }
254
+ }
255
+ }
194
256
  function legacyKindOfV2Track(track, chromaKeyOf, overlappingItemIds) {
195
257
  if (!('items' in track)) {
196
258
  return 'captions';
@@ -1,6 +1,7 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.parseEdit = parseEdit;
4
+ const transition_vocabulary_1 = require("../transition-vocabulary");
4
5
  const LAYER_BLEND_MODES = [
5
6
  'normal', 'screen', 'multiply', 'add', 'difference',
6
7
  'darken', 'lighten', 'overlay', 'hardlight', 'softlight'
@@ -78,16 +79,18 @@ function parseEdit(source) {
78
79
  let transitionOut;
79
80
  if (rawCut.transition_out !== undefined && rawCut.transition_out !== null) {
80
81
  const transition = rawCut.transition_out;
81
- const validType = transition?.type === 'dissolve'
82
- || transition?.type === 'fade-black'
83
- || transition?.type === 'fade-white'
84
- || transition?.type === 'reveal-down'
85
- || transition?.type === 'reveal-up';
82
+ // 正準語彙に加え、手編集で先行した非空文字列も読み取りだけは保持する。
83
+ // 書き込み API は isTransitionType で閉じたまま、preview の汎用フォールバックへ渡す。
84
+ const validType = (0, transition_vocabulary_1.isTransitionType)(transition?.type)
85
+ || (typeof transition?.type === 'string' && transition.type.trim().length > 0);
86
86
  const validDuration = typeof transition?.duration === 'number'
87
87
  && Number.isFinite(transition.duration) && transition.duration > 0;
88
88
  if (transition && typeof transition === 'object' && !Array.isArray(transition)
89
89
  && validType && validDuration) {
90
- transitionOut = { type: transition.type, duration: transition.duration };
90
+ transitionOut = {
91
+ type: transition.type,
92
+ duration: transition.duration
93
+ };
91
94
  }
92
95
  else {
93
96
  warnings.push(`${index + 1} 番目のクリップの transition_out が不正なため無視します。`);
@@ -56,6 +56,11 @@ export interface TimelineMapResult {
56
56
  export declare function transitionProgressAt(window: TimelineTransitionWindow, outputT: number): number;
57
57
  export declare function buildTimelineMap(cuts: readonly EditCut[], options?: {
58
58
  trackZ?: (track: number) => number;
59
+ fps?: number;
60
+ handleRoom?: (cutIndex: number) => {
61
+ tailSeconds?: number;
62
+ headSeconds?: number;
63
+ } | undefined;
59
64
  }): TimelineMapResult;
60
65
  /**
61
66
  * 出力秒 → ソース秒。トランジション重なり区間(隣接 src が重なる出力時刻)は
@@ -16,6 +16,7 @@ exports.transitionProgressAt = transitionProgressAt;
16
16
  exports.buildTimelineMap = buildTimelineMap;
17
17
  exports.outputToSource = outputToSource;
18
18
  const edit_store_1 = require("./edit-store");
19
+ const cut_adjacency_1 = require("./cut-adjacency");
19
20
  function transitionProgressAt(window, outputT) {
20
21
  if (!(window.duration > 0))
21
22
  return 0;
@@ -37,10 +38,49 @@ function buildTimelineMap(cuts, options) {
37
38
  const resolved = trackSegments.map(segment => ({
38
39
  start: segment.at,
39
40
  end: segment.end,
41
+ baseStart: segment.at,
42
+ baseEnd: segment.end,
40
43
  track: segment.track,
41
44
  cut: usableCuts[segment.index],
42
45
  cutIndex: usable[segment.index].index
43
46
  }));
47
+ const fps = options?.fps ?? cut_adjacency_1.DEFAULT_CUT_ADJACENCY_FPS;
48
+ // 隠れのりしろは、フレーム量子化で突き合わせとなる境界にだけ合成する。
49
+ // 実重なり済みの境界は一切変更せず、従来の窓計算へそのまま流す。
50
+ for (let outgoingIndex = 0; outgoingIndex < resolved.length; outgoingIndex++) {
51
+ const outgoing = resolved[outgoingIndex];
52
+ const transition = outgoing.cut.transitionOut;
53
+ if (!transition || !(typeof transition.duration === 'number' && Number.isFinite(transition.duration)
54
+ && transition.duration > 0))
55
+ continue;
56
+ const incoming = resolved.slice(outgoingIndex + 1).find(candidate => candidate.track === outgoing.track);
57
+ if (!incoming || (0, cut_adjacency_1.cutOverlapFrames)({ tlEnd: outgoing.end }, { tlStart: incoming.start }, fps) !== 0)
58
+ continue;
59
+ const outgoingRoom = options?.handleRoom?.(outgoing.cutIndex);
60
+ const incomingRoom = options?.handleRoom?.(incoming.cutIndex);
61
+ const incomingSpeed = typeof incoming.cut.speed === 'number' && incoming.cut.speed > 0
62
+ ? incoming.cut.speed : 1;
63
+ const plan = (0, cut_adjacency_1.planTransitionHandleWindow)({
64
+ declaredSeconds: transition.duration,
65
+ outgoingTailRoomSeconds: outgoingRoom?.tailSeconds ?? Number.POSITIVE_INFINITY,
66
+ incomingHeadRoomSeconds: incomingRoom?.headSeconds ?? incoming.cut.in / incomingSpeed,
67
+ outgoingDurationSeconds: outgoing.baseEnd - outgoing.baseStart,
68
+ incomingDurationSeconds: incoming.baseEnd - incoming.baseStart
69
+ });
70
+ if (plan.effectiveSeconds <= 0)
71
+ continue;
72
+ const cutPoint = outgoing.end;
73
+ outgoing.end = cutPoint + plan.halfSeconds;
74
+ outgoing.cut = {
75
+ ...outgoing.cut,
76
+ transitionOut: { ...transition, duration: plan.effectiveSeconds }
77
+ };
78
+ incoming.start = cutPoint - plan.halfSeconds;
79
+ incoming.cut = {
80
+ ...incoming.cut,
81
+ in: Math.max(0, incoming.cut.in - plan.halfSeconds * incomingSpeed)
82
+ };
83
+ }
44
84
  const segmentSlice = (entry, start, end, transitionOut = null) => {
45
85
  const cut = entry.cut;
46
86
  const speed = typeof cut.speed === 'number' && cut.speed > 0 ? cut.speed : 1;
@@ -0,0 +1,223 @@
1
+ /**
2
+ * edit.json の transition_out.type に使える正準語彙。
3
+ * JSON Schema はリテラル enum を保つため別置きだが、drift test でこの表と一致させる。
4
+ */
5
+ export declare const TRANSITION_VOCABULARY: readonly [{
6
+ readonly id: "dissolve";
7
+ readonly xfadeName: "dissolve";
8
+ readonly labelJa: "ディゾルブ";
9
+ readonly category: "フェード";
10
+ readonly previewKind: "dissolve";
11
+ readonly glyph: "D";
12
+ }, {
13
+ readonly id: "fade";
14
+ readonly xfadeName: "fade";
15
+ readonly labelJa: "クロスフェード";
16
+ readonly category: "フェード";
17
+ readonly previewKind: "fade";
18
+ readonly glyph: "F";
19
+ }, {
20
+ readonly id: "fade-black";
21
+ readonly xfadeName: "fadeblack";
22
+ readonly labelJa: "黒フェード";
23
+ readonly category: "フェード";
24
+ readonly previewKind: "fade-black";
25
+ readonly glyph: "B";
26
+ }, {
27
+ readonly id: "fade-white";
28
+ readonly xfadeName: "fadewhite";
29
+ readonly labelJa: "白フェード";
30
+ readonly category: "フェード";
31
+ readonly previewKind: "fade-white";
32
+ readonly glyph: "W";
33
+ }, {
34
+ readonly id: "fade-grays";
35
+ readonly xfadeName: "fadegrays";
36
+ readonly labelJa: "モノクロフェード";
37
+ readonly category: "フェード";
38
+ readonly previewKind: "fade-grays";
39
+ readonly glyph: "G";
40
+ }, {
41
+ readonly id: "wipe-left";
42
+ readonly xfadeName: "wipeleft";
43
+ readonly labelJa: "ワイプ(左へ)";
44
+ readonly category: "ワイプ";
45
+ readonly previewKind: "wipe-left";
46
+ readonly glyph: "←";
47
+ }, {
48
+ readonly id: "wipe-right";
49
+ readonly xfadeName: "wiperight";
50
+ readonly labelJa: "ワイプ(右へ)";
51
+ readonly category: "ワイプ";
52
+ readonly previewKind: "wipe-right";
53
+ readonly glyph: "→";
54
+ }, {
55
+ readonly id: "wipe-up";
56
+ readonly xfadeName: "wipeup";
57
+ readonly labelJa: "ワイプ(上へ)";
58
+ readonly category: "ワイプ";
59
+ readonly previewKind: "wipe-up";
60
+ readonly glyph: "↑";
61
+ }, {
62
+ readonly id: "wipe-down";
63
+ readonly xfadeName: "wipedown";
64
+ readonly labelJa: "ワイプ(下へ)";
65
+ readonly category: "ワイプ";
66
+ readonly previewKind: "wipe-down";
67
+ readonly glyph: "↓";
68
+ }, {
69
+ readonly id: "radial";
70
+ readonly xfadeName: "radial";
71
+ readonly labelJa: "時計ワイプ";
72
+ readonly category: "ワイプ";
73
+ readonly previewKind: "radial";
74
+ readonly glyph: "◷";
75
+ }, {
76
+ readonly id: "slide-left";
77
+ readonly xfadeName: "slideleft";
78
+ readonly labelJa: "スライド(左へ)";
79
+ readonly category: "スライド";
80
+ readonly previewKind: "slide-left";
81
+ readonly glyph: "←";
82
+ }, {
83
+ readonly id: "slide-right";
84
+ readonly xfadeName: "slideright";
85
+ readonly labelJa: "スライド(右へ)";
86
+ readonly category: "スライド";
87
+ readonly previewKind: "slide-right";
88
+ readonly glyph: "→";
89
+ }, {
90
+ readonly id: "slide-up";
91
+ readonly xfadeName: "slideup";
92
+ readonly labelJa: "スライド(上へ)";
93
+ readonly category: "スライド";
94
+ readonly previewKind: "slide-up";
95
+ readonly glyph: "↑";
96
+ }, {
97
+ readonly id: "slide-down";
98
+ readonly xfadeName: "slidedown";
99
+ readonly labelJa: "スライド(下へ)";
100
+ readonly category: "スライド";
101
+ readonly previewKind: "slide-down";
102
+ readonly glyph: "↓";
103
+ }, {
104
+ readonly id: "cover-left";
105
+ readonly xfadeName: "coverleft";
106
+ readonly labelJa: "カバー(左へ)";
107
+ readonly category: "カバー";
108
+ readonly previewKind: "cover-left";
109
+ readonly glyph: "←";
110
+ }, {
111
+ readonly id: "cover-right";
112
+ readonly xfadeName: "coverright";
113
+ readonly labelJa: "カバー(右へ)";
114
+ readonly category: "カバー";
115
+ readonly previewKind: "cover-right";
116
+ readonly glyph: "→";
117
+ }, {
118
+ readonly id: "cover-up";
119
+ readonly xfadeName: "coverup";
120
+ readonly labelJa: "カバー(上へ)";
121
+ readonly category: "カバー";
122
+ readonly previewKind: "cover-up";
123
+ readonly glyph: "↑";
124
+ }, {
125
+ readonly id: "cover-down";
126
+ readonly xfadeName: "coverdown";
127
+ readonly labelJa: "カバー(下へ)";
128
+ readonly category: "カバー";
129
+ readonly previewKind: "cover-down";
130
+ readonly glyph: "↓";
131
+ }, {
132
+ readonly id: "reveal-left";
133
+ readonly xfadeName: "revealleft";
134
+ readonly labelJa: "リビール(左へ)";
135
+ readonly category: "リビール";
136
+ readonly previewKind: "reveal-left";
137
+ readonly glyph: "←";
138
+ }, {
139
+ readonly id: "reveal-right";
140
+ readonly xfadeName: "revealright";
141
+ readonly labelJa: "リビール(右へ)";
142
+ readonly category: "リビール";
143
+ readonly previewKind: "reveal-right";
144
+ readonly glyph: "→";
145
+ }, {
146
+ readonly id: "reveal-down";
147
+ readonly xfadeName: "revealdown";
148
+ readonly labelJa: "上からリビール";
149
+ readonly category: "リビール";
150
+ readonly previewKind: "reveal-down";
151
+ readonly glyph: "↓";
152
+ }, {
153
+ readonly id: "reveal-up";
154
+ readonly xfadeName: "revealup";
155
+ readonly labelJa: "下からリビール";
156
+ readonly category: "リビール";
157
+ readonly previewKind: "reveal-up";
158
+ readonly glyph: "↑";
159
+ }, {
160
+ readonly id: "circle-open";
161
+ readonly xfadeName: "circleopen";
162
+ readonly labelJa: "サークル(開く)";
163
+ readonly category: "形状";
164
+ readonly previewKind: "circle-open";
165
+ readonly glyph: "○";
166
+ }, {
167
+ readonly id: "circle-close";
168
+ readonly xfadeName: "circleclose";
169
+ readonly labelJa: "サークル(閉じる)";
170
+ readonly category: "形状";
171
+ readonly previewKind: "circle-close";
172
+ readonly glyph: "●";
173
+ }, {
174
+ readonly id: "zoom-in";
175
+ readonly xfadeName: "zoomin";
176
+ readonly labelJa: "ズームイン";
177
+ readonly category: "変形";
178
+ readonly previewKind: "zoom-in";
179
+ readonly glyph: "+";
180
+ }, {
181
+ readonly id: "squeeze-h";
182
+ readonly xfadeName: "squeezeh";
183
+ readonly labelJa: "スクイーズ(縦つぶし)";
184
+ readonly category: "変形";
185
+ readonly previewKind: "squeeze-h";
186
+ readonly glyph: "↕";
187
+ }, {
188
+ readonly id: "squeeze-v";
189
+ readonly xfadeName: "squeezev";
190
+ readonly labelJa: "スクイーズ(横つぶし)";
191
+ readonly category: "変形";
192
+ readonly previewKind: "squeeze-v";
193
+ readonly glyph: "↔";
194
+ }, {
195
+ readonly id: "blur";
196
+ readonly xfadeName: "hblur";
197
+ readonly labelJa: "ブラー";
198
+ readonly category: "質感";
199
+ readonly previewKind: "blur";
200
+ readonly glyph: "B";
201
+ }, {
202
+ readonly id: "pixelize";
203
+ readonly xfadeName: "pixelize";
204
+ readonly labelJa: "ピクセレート";
205
+ readonly category: "質感";
206
+ readonly previewKind: "pixelize";
207
+ readonly glyph: "P";
208
+ }];
209
+ export type TransitionDefinition = typeof TRANSITION_VOCABULARY[number];
210
+ export type TransitionType = TransitionDefinition['id'];
211
+ export type TransitionCategory = TransitionDefinition['category'];
212
+ export type TransitionPreviewKind = TransitionDefinition['previewKind'];
213
+ declare const unknownTransitionTypeBrand: unique symbol;
214
+ /** 読み取り側だけが保持する、schema より先行した未知種別。書き込み API には使わない。 */
215
+ export type UnknownTransitionType = string & {
216
+ readonly [unknownTransitionTypeBrand]: true;
217
+ };
218
+ export type ReadableTransitionType = TransitionType | UnknownTransitionType;
219
+ export declare const TRANSITION_TYPE_IDS: readonly TransitionType[];
220
+ export declare const TRANSITION_CATEGORIES: readonly TransitionCategory[];
221
+ export declare const TRANSITION_BY_ID: Readonly<Record<TransitionType, TransitionDefinition>>;
222
+ export declare function isTransitionType(value: unknown): value is TransitionType;
223
+ export {};
@@ -0,0 +1,45 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.TRANSITION_BY_ID = exports.TRANSITION_CATEGORIES = exports.TRANSITION_TYPE_IDS = exports.TRANSITION_VOCABULARY = void 0;
4
+ exports.isTransitionType = isTransitionType;
5
+ /**
6
+ * edit.json の transition_out.type に使える正準語彙。
7
+ * JSON Schema はリテラル enum を保つため別置きだが、drift test でこの表と一致させる。
8
+ */
9
+ exports.TRANSITION_VOCABULARY = [
10
+ { id: 'dissolve', xfadeName: 'dissolve', labelJa: 'ディゾルブ', category: 'フェード', previewKind: 'dissolve', glyph: 'D' },
11
+ { id: 'fade', xfadeName: 'fade', labelJa: 'クロスフェード', category: 'フェード', previewKind: 'fade', glyph: 'F' },
12
+ { id: 'fade-black', xfadeName: 'fadeblack', labelJa: '黒フェード', category: 'フェード', previewKind: 'fade-black', glyph: 'B' },
13
+ { id: 'fade-white', xfadeName: 'fadewhite', labelJa: '白フェード', category: 'フェード', previewKind: 'fade-white', glyph: 'W' },
14
+ { id: 'fade-grays', xfadeName: 'fadegrays', labelJa: 'モノクロフェード', category: 'フェード', previewKind: 'fade-grays', glyph: 'G' },
15
+ { id: 'wipe-left', xfadeName: 'wipeleft', labelJa: 'ワイプ(左へ)', category: 'ワイプ', previewKind: 'wipe-left', glyph: '←' },
16
+ { id: 'wipe-right', xfadeName: 'wiperight', labelJa: 'ワイプ(右へ)', category: 'ワイプ', previewKind: 'wipe-right', glyph: '→' },
17
+ { id: 'wipe-up', xfadeName: 'wipeup', labelJa: 'ワイプ(上へ)', category: 'ワイプ', previewKind: 'wipe-up', glyph: '↑' },
18
+ { id: 'wipe-down', xfadeName: 'wipedown', labelJa: 'ワイプ(下へ)', category: 'ワイプ', previewKind: 'wipe-down', glyph: '↓' },
19
+ { id: 'radial', xfadeName: 'radial', labelJa: '時計ワイプ', category: 'ワイプ', previewKind: 'radial', glyph: '◷' },
20
+ { id: 'slide-left', xfadeName: 'slideleft', labelJa: 'スライド(左へ)', category: 'スライド', previewKind: 'slide-left', glyph: '←' },
21
+ { id: 'slide-right', xfadeName: 'slideright', labelJa: 'スライド(右へ)', category: 'スライド', previewKind: 'slide-right', glyph: '→' },
22
+ { id: 'slide-up', xfadeName: 'slideup', labelJa: 'スライド(上へ)', category: 'スライド', previewKind: 'slide-up', glyph: '↑' },
23
+ { id: 'slide-down', xfadeName: 'slidedown', labelJa: 'スライド(下へ)', category: 'スライド', previewKind: 'slide-down', glyph: '↓' },
24
+ { id: 'cover-left', xfadeName: 'coverleft', labelJa: 'カバー(左へ)', category: 'カバー', previewKind: 'cover-left', glyph: '←' },
25
+ { id: 'cover-right', xfadeName: 'coverright', labelJa: 'カバー(右へ)', category: 'カバー', previewKind: 'cover-right', glyph: '→' },
26
+ { id: 'cover-up', xfadeName: 'coverup', labelJa: 'カバー(上へ)', category: 'カバー', previewKind: 'cover-up', glyph: '↑' },
27
+ { id: 'cover-down', xfadeName: 'coverdown', labelJa: 'カバー(下へ)', category: 'カバー', previewKind: 'cover-down', glyph: '↓' },
28
+ { id: 'reveal-left', xfadeName: 'revealleft', labelJa: 'リビール(左へ)', category: 'リビール', previewKind: 'reveal-left', glyph: '←' },
29
+ { id: 'reveal-right', xfadeName: 'revealright', labelJa: 'リビール(右へ)', category: 'リビール', previewKind: 'reveal-right', glyph: '→' },
30
+ { id: 'reveal-down', xfadeName: 'revealdown', labelJa: '上からリビール', category: 'リビール', previewKind: 'reveal-down', glyph: '↓' },
31
+ { id: 'reveal-up', xfadeName: 'revealup', labelJa: '下からリビール', category: 'リビール', previewKind: 'reveal-up', glyph: '↑' },
32
+ { id: 'circle-open', xfadeName: 'circleopen', labelJa: 'サークル(開く)', category: '形状', previewKind: 'circle-open', glyph: '○' },
33
+ { id: 'circle-close', xfadeName: 'circleclose', labelJa: 'サークル(閉じる)', category: '形状', previewKind: 'circle-close', glyph: '●' },
34
+ { id: 'zoom-in', xfadeName: 'zoomin', labelJa: 'ズームイン', category: '変形', previewKind: 'zoom-in', glyph: '+' },
35
+ { id: 'squeeze-h', xfadeName: 'squeezeh', labelJa: 'スクイーズ(縦つぶし)', category: '変形', previewKind: 'squeeze-h', glyph: '↕' },
36
+ { id: 'squeeze-v', xfadeName: 'squeezev', labelJa: 'スクイーズ(横つぶし)', category: '変形', previewKind: 'squeeze-v', glyph: '↔' },
37
+ { id: 'blur', xfadeName: 'hblur', labelJa: 'ブラー', category: '質感', previewKind: 'blur', glyph: 'B' },
38
+ { id: 'pixelize', xfadeName: 'pixelize', labelJa: 'ピクセレート', category: '質感', previewKind: 'pixelize', glyph: 'P' }
39
+ ];
40
+ exports.TRANSITION_TYPE_IDS = exports.TRANSITION_VOCABULARY.map(entry => entry.id);
41
+ exports.TRANSITION_CATEGORIES = [...new Set(exports.TRANSITION_VOCABULARY.map(entry => entry.category))];
42
+ exports.TRANSITION_BY_ID = Object.fromEntries(exports.TRANSITION_VOCABULARY.map(entry => [entry.id, entry]));
43
+ function isTransitionType(value) {
44
+ return typeof value === 'string' && Object.prototype.hasOwnProperty.call(exports.TRANSITION_BY_ID, value);
45
+ }
@@ -28,6 +28,42 @@ var AkariEditKernel = (() => {
28
28
  transitionProgressAt: () => transitionProgressAt
29
29
  });
30
30
 
31
+ // src/transition-vocabulary.ts
32
+ var TRANSITION_VOCABULARY = [
33
+ { id: "dissolve", xfadeName: "dissolve", labelJa: "\u30C7\u30A3\u30BE\u30EB\u30D6", category: "\u30D5\u30A7\u30FC\u30C9", previewKind: "dissolve", glyph: "D" },
34
+ { id: "fade", xfadeName: "fade", labelJa: "\u30AF\u30ED\u30B9\u30D5\u30A7\u30FC\u30C9", category: "\u30D5\u30A7\u30FC\u30C9", previewKind: "fade", glyph: "F" },
35
+ { id: "fade-black", xfadeName: "fadeblack", labelJa: "\u9ED2\u30D5\u30A7\u30FC\u30C9", category: "\u30D5\u30A7\u30FC\u30C9", previewKind: "fade-black", glyph: "B" },
36
+ { id: "fade-white", xfadeName: "fadewhite", labelJa: "\u767D\u30D5\u30A7\u30FC\u30C9", category: "\u30D5\u30A7\u30FC\u30C9", previewKind: "fade-white", glyph: "W" },
37
+ { id: "fade-grays", xfadeName: "fadegrays", labelJa: "\u30E2\u30CE\u30AF\u30ED\u30D5\u30A7\u30FC\u30C9", category: "\u30D5\u30A7\u30FC\u30C9", previewKind: "fade-grays", glyph: "G" },
38
+ { id: "wipe-left", xfadeName: "wipeleft", labelJa: "\u30EF\u30A4\u30D7\uFF08\u5DE6\u3078\uFF09", category: "\u30EF\u30A4\u30D7", previewKind: "wipe-left", glyph: "\u2190" },
39
+ { id: "wipe-right", xfadeName: "wiperight", labelJa: "\u30EF\u30A4\u30D7\uFF08\u53F3\u3078\uFF09", category: "\u30EF\u30A4\u30D7", previewKind: "wipe-right", glyph: "\u2192" },
40
+ { id: "wipe-up", xfadeName: "wipeup", labelJa: "\u30EF\u30A4\u30D7\uFF08\u4E0A\u3078\uFF09", category: "\u30EF\u30A4\u30D7", previewKind: "wipe-up", glyph: "\u2191" },
41
+ { id: "wipe-down", xfadeName: "wipedown", labelJa: "\u30EF\u30A4\u30D7\uFF08\u4E0B\u3078\uFF09", category: "\u30EF\u30A4\u30D7", previewKind: "wipe-down", glyph: "\u2193" },
42
+ { id: "radial", xfadeName: "radial", labelJa: "\u6642\u8A08\u30EF\u30A4\u30D7", category: "\u30EF\u30A4\u30D7", previewKind: "radial", glyph: "\u25F7" },
43
+ { id: "slide-left", xfadeName: "slideleft", labelJa: "\u30B9\u30E9\u30A4\u30C9\uFF08\u5DE6\u3078\uFF09", category: "\u30B9\u30E9\u30A4\u30C9", previewKind: "slide-left", glyph: "\u2190" },
44
+ { id: "slide-right", xfadeName: "slideright", labelJa: "\u30B9\u30E9\u30A4\u30C9\uFF08\u53F3\u3078\uFF09", category: "\u30B9\u30E9\u30A4\u30C9", previewKind: "slide-right", glyph: "\u2192" },
45
+ { id: "slide-up", xfadeName: "slideup", labelJa: "\u30B9\u30E9\u30A4\u30C9\uFF08\u4E0A\u3078\uFF09", category: "\u30B9\u30E9\u30A4\u30C9", previewKind: "slide-up", glyph: "\u2191" },
46
+ { id: "slide-down", xfadeName: "slidedown", labelJa: "\u30B9\u30E9\u30A4\u30C9\uFF08\u4E0B\u3078\uFF09", category: "\u30B9\u30E9\u30A4\u30C9", previewKind: "slide-down", glyph: "\u2193" },
47
+ { id: "cover-left", xfadeName: "coverleft", labelJa: "\u30AB\u30D0\u30FC\uFF08\u5DE6\u3078\uFF09", category: "\u30AB\u30D0\u30FC", previewKind: "cover-left", glyph: "\u2190" },
48
+ { id: "cover-right", xfadeName: "coverright", labelJa: "\u30AB\u30D0\u30FC\uFF08\u53F3\u3078\uFF09", category: "\u30AB\u30D0\u30FC", previewKind: "cover-right", glyph: "\u2192" },
49
+ { id: "cover-up", xfadeName: "coverup", labelJa: "\u30AB\u30D0\u30FC\uFF08\u4E0A\u3078\uFF09", category: "\u30AB\u30D0\u30FC", previewKind: "cover-up", glyph: "\u2191" },
50
+ { id: "cover-down", xfadeName: "coverdown", labelJa: "\u30AB\u30D0\u30FC\uFF08\u4E0B\u3078\uFF09", category: "\u30AB\u30D0\u30FC", previewKind: "cover-down", glyph: "\u2193" },
51
+ { id: "reveal-left", xfadeName: "revealleft", labelJa: "\u30EA\u30D3\u30FC\u30EB\uFF08\u5DE6\u3078\uFF09", category: "\u30EA\u30D3\u30FC\u30EB", previewKind: "reveal-left", glyph: "\u2190" },
52
+ { id: "reveal-right", xfadeName: "revealright", labelJa: "\u30EA\u30D3\u30FC\u30EB\uFF08\u53F3\u3078\uFF09", category: "\u30EA\u30D3\u30FC\u30EB", previewKind: "reveal-right", glyph: "\u2192" },
53
+ { id: "reveal-down", xfadeName: "revealdown", labelJa: "\u4E0A\u304B\u3089\u30EA\u30D3\u30FC\u30EB", category: "\u30EA\u30D3\u30FC\u30EB", previewKind: "reveal-down", glyph: "\u2193" },
54
+ { id: "reveal-up", xfadeName: "revealup", labelJa: "\u4E0B\u304B\u3089\u30EA\u30D3\u30FC\u30EB", category: "\u30EA\u30D3\u30FC\u30EB", previewKind: "reveal-up", glyph: "\u2191" },
55
+ { id: "circle-open", xfadeName: "circleopen", labelJa: "\u30B5\u30FC\u30AF\u30EB\uFF08\u958B\u304F\uFF09", category: "\u5F62\u72B6", previewKind: "circle-open", glyph: "\u25CB" },
56
+ { id: "circle-close", xfadeName: "circleclose", labelJa: "\u30B5\u30FC\u30AF\u30EB\uFF08\u9589\u3058\u308B\uFF09", category: "\u5F62\u72B6", previewKind: "circle-close", glyph: "\u25CF" },
57
+ { id: "zoom-in", xfadeName: "zoomin", labelJa: "\u30BA\u30FC\u30E0\u30A4\u30F3", category: "\u5909\u5F62", previewKind: "zoom-in", glyph: "\uFF0B" },
58
+ { id: "squeeze-h", xfadeName: "squeezeh", labelJa: "\u30B9\u30AF\u30A4\u30FC\u30BA\uFF08\u7E26\u3064\u3076\u3057\uFF09", category: "\u5909\u5F62", previewKind: "squeeze-h", glyph: "\u2195" },
59
+ { id: "squeeze-v", xfadeName: "squeezev", labelJa: "\u30B9\u30AF\u30A4\u30FC\u30BA\uFF08\u6A2A\u3064\u3076\u3057\uFF09", category: "\u5909\u5F62", previewKind: "squeeze-v", glyph: "\u2194" },
60
+ { id: "blur", xfadeName: "hblur", labelJa: "\u30D6\u30E9\u30FC", category: "\u8CEA\u611F", previewKind: "blur", glyph: "B" },
61
+ { id: "pixelize", xfadeName: "pixelize", labelJa: "\u30D4\u30AF\u30BB\u30EC\u30FC\u30C8", category: "\u8CEA\u611F", previewKind: "pixelize", glyph: "P" }
62
+ ];
63
+ var TRANSITION_TYPE_IDS = TRANSITION_VOCABULARY.map((entry) => entry.id);
64
+ var TRANSITION_CATEGORIES = [...new Set(TRANSITION_VOCABULARY.map((entry) => entry.category))];
65
+ var TRANSITION_BY_ID = Object.fromEntries(TRANSITION_VOCABULARY.map((entry) => [entry.id, entry]));
66
+
31
67
  // src/edit-store.ts
32
68
  function computeCutTrackSegments(cuts) {
33
69
  const cursorByTrack = /* @__PURE__ */ new Map();
@@ -50,6 +86,32 @@ var AkariEditKernel = (() => {
50
86
  return segments;
51
87
  }
52
88
 
89
+ // src/cut-adjacency.ts
90
+ var DEFAULT_CUT_ADJACENCY_FPS = 30;
91
+ function effectiveCutFps(fps) {
92
+ return Number.isFinite(fps) && fps > 0 ? fps : DEFAULT_CUT_ADJACENCY_FPS;
93
+ }
94
+ function cutOverlapFrames(earlier, later, fps = DEFAULT_CUT_ADJACENCY_FPS) {
95
+ const resolvedFps = effectiveCutFps(fps);
96
+ return Math.round(earlier.tlEnd * resolvedFps) - Math.round(later.tlStart * resolvedFps);
97
+ }
98
+ var nonNegativeRoom = (value) => value === Number.POSITIVE_INFINITY ? value : Number.isFinite(value) && value > 0 ? value : 0;
99
+ function planTransitionHandleWindow(input) {
100
+ const declaredSeconds = Number.isFinite(input.declaredSeconds) && input.declaredSeconds > 0 ? input.declaredSeconds : 0;
101
+ const effectiveSeconds = Math.max(0, Math.min(
102
+ declaredSeconds,
103
+ 2 * nonNegativeRoom(input.outgoingTailRoomSeconds),
104
+ 2 * nonNegativeRoom(input.incomingHeadRoomSeconds),
105
+ 2 * nonNegativeRoom(input.outgoingDurationSeconds),
106
+ 2 * nonNegativeRoom(input.incomingDurationSeconds)
107
+ ));
108
+ return {
109
+ effectiveSeconds,
110
+ halfSeconds: effectiveSeconds / 2,
111
+ outcome: effectiveSeconds <= 0 ? "none" : effectiveSeconds < declaredSeconds ? "clamped" : "full"
112
+ };
113
+ }
114
+
53
115
  // src/timeline-map.ts
54
116
  function transitionProgressAt(window, outputT) {
55
117
  if (!(window.duration > 0)) return 0;
@@ -68,10 +130,46 @@ var AkariEditKernel = (() => {
68
130
  const resolved = trackSegments.map((segment) => ({
69
131
  start: segment.at,
70
132
  end: segment.end,
133
+ baseStart: segment.at,
134
+ baseEnd: segment.end,
71
135
  track: segment.track,
72
136
  cut: usableCuts[segment.index],
73
137
  cutIndex: usable[segment.index].index
74
138
  }));
139
+ const fps = options?.fps ?? DEFAULT_CUT_ADJACENCY_FPS;
140
+ for (let outgoingIndex = 0; outgoingIndex < resolved.length; outgoingIndex++) {
141
+ const outgoing = resolved[outgoingIndex];
142
+ const transition = outgoing.cut.transitionOut;
143
+ if (!transition || !(typeof transition.duration === "number" && Number.isFinite(transition.duration) && transition.duration > 0)) continue;
144
+ const incoming = resolved.slice(outgoingIndex + 1).find((candidate) => candidate.track === outgoing.track);
145
+ if (!incoming || cutOverlapFrames(
146
+ { tlEnd: outgoing.end },
147
+ { tlStart: incoming.start },
148
+ fps
149
+ ) !== 0) continue;
150
+ const outgoingRoom = options?.handleRoom?.(outgoing.cutIndex);
151
+ const incomingRoom = options?.handleRoom?.(incoming.cutIndex);
152
+ const incomingSpeed = typeof incoming.cut.speed === "number" && incoming.cut.speed > 0 ? incoming.cut.speed : 1;
153
+ const plan = planTransitionHandleWindow({
154
+ declaredSeconds: transition.duration,
155
+ outgoingTailRoomSeconds: outgoingRoom?.tailSeconds ?? Number.POSITIVE_INFINITY,
156
+ incomingHeadRoomSeconds: incomingRoom?.headSeconds ?? incoming.cut.in / incomingSpeed,
157
+ outgoingDurationSeconds: outgoing.baseEnd - outgoing.baseStart,
158
+ incomingDurationSeconds: incoming.baseEnd - incoming.baseStart
159
+ });
160
+ if (plan.effectiveSeconds <= 0) continue;
161
+ const cutPoint = outgoing.end;
162
+ outgoing.end = cutPoint + plan.halfSeconds;
163
+ outgoing.cut = {
164
+ ...outgoing.cut,
165
+ transitionOut: { ...transition, duration: plan.effectiveSeconds }
166
+ };
167
+ incoming.start = cutPoint - plan.halfSeconds;
168
+ incoming.cut = {
169
+ ...incoming.cut,
170
+ in: Math.max(0, incoming.cut.in - plan.halfSeconds * incomingSpeed)
171
+ };
172
+ }
75
173
  const segmentSlice = (entry, start, end, transitionOut = null) => {
76
174
  const cut = entry.cut;
77
175
  const speed = typeof cut.speed === "number" && cut.speed > 0 ? cut.speed : 1;
@@ -0,0 +1,22 @@
1
+ {
2
+ "name": "@akari-video/matte-rvm",
3
+ "version": "0.1.0",
4
+ "private": true,
5
+ "type": "module",
6
+ "description": "Robust Video Matting model deployment and raw BGRA inference helper.",
7
+ "main": "src/index.mjs",
8
+ "exports": {
9
+ ".": "./src/index.mjs"
10
+ },
11
+ "scripts": {
12
+ "postinstall": "node scripts/fetch-models.mjs",
13
+ "fetch:resnet50": "node scripts/fetch-models.mjs --model resnet50",
14
+ "test": "node --test \"test/*.test.mjs\""
15
+ },
16
+ "dependencies": {
17
+ "onnxruntime-node": "1.29.0"
18
+ },
19
+ "engines": {
20
+ "node": ">=20"
21
+ }
22
+ }
@@ -6,6 +6,9 @@
6
6
  import fs from "node:fs";
7
7
  import path from "node:path";
8
8
  import { fileURLToPath } from "node:url";
9
+ import { createRequire } from "node:module";
10
+
11
+ const { TRANSITION_TYPE_IDS } = createRequire(import.meta.url)("../../edit-store/lib/index.js");
9
12
 
10
13
  const LAYER_KINDS = new Set(["baked", "video", "filter"]);
11
14
  const LAYER_BLEND_MODES = new Set([
@@ -1063,8 +1066,8 @@ function validateTransitionOut(value, label) {
1063
1066
  fail(`${label} は object である必要があります`);
1064
1067
  return;
1065
1068
  }
1066
- if (!["dissolve", "fade-black", "fade-white", "reveal-down", "reveal-up"].includes(value.type)) {
1067
- fail(`${label}.type は dissolve/fade-black/fade-white/reveal-down/reveal-up のいずれかである必要があります`);
1069
+ if (!TRANSITION_TYPE_IDS.includes(value.type)) {
1070
+ fail(`${label}.type は ${TRANSITION_TYPE_IDS.join("/")} のいずれかである必要があります`);
1068
1071
  }
1069
1072
  if (!isFiniteNumber(value.duration) || value.duration <= 0) {
1070
1073
  fail(`${label}.duration は 0 より大きい有限数である必要があります`);
@@ -279,10 +279,10 @@
279
279
  "additionalProperties": true,
280
280
  "required": ["type", "duration"],
281
281
  "properties": {
282
- "type": { "enum": ["dissolve", "fade-black", "fade-white", "reveal-down", "reveal-up"] },
282
+ "type": { "enum": ["dissolve", "fade", "fade-black", "fade-white", "fade-grays", "wipe-left", "wipe-right", "wipe-up", "wipe-down", "radial", "slide-left", "slide-right", "slide-up", "slide-down", "cover-left", "cover-right", "cover-up", "cover-down", "reveal-left", "reveal-right", "reveal-down", "reveal-up", "circle-open", "circle-close", "zoom-in", "squeeze-h", "squeeze-v", "blur", "pixelize"] },
283
283
  "duration": { "$ref": "#/$defs/positiveNumber" }
284
284
  },
285
- "$comment": "docs/contract-2026-07-22-render-basics.md #3。v0 は指定されたカット境界のみ xfade 経路(残裁定3)。ffmpeg xfade の transition 名へ直接対応: dissolve/fadeblack/fadewhite/revealdown/revealup。reveal-down / reveal-up は前カットが丸ごとその方向へ動いて画面外へ抜け、空いた側から次カットが現れる(2026-08-14 追加)。"
285
+ "$comment": "docs/contract-2026-07-22-render-basics.md #3。v0 は指定されたカット境界のみ xfade 経路(残裁定3)。enum は packages/edit-store の正準トランジション語彙と drift test で一致させる。"
286
286
  },
287
287
  "cutTransform": {
288
288
  "type": "object",