akari-video 0.1.19 → 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 (27) 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 +90 -18
  6. package/vendor/packages/edit-store/lib/cut-adjacency.d.ts +25 -0
  7. package/vendor/packages/edit-store/lib/cut-adjacency.js +38 -8
  8. package/vendor/packages/edit-store/lib/edit-store.d.ts +13 -2
  9. package/vendor/packages/edit-store/lib/edit-store.js +69 -3
  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.d.ts +13 -0
  13. package/vendor/packages/edit-store/lib/internal-model.js +98 -18
  14. package/vendor/packages/edit-store/lib/migrate/legacy-parse.js +9 -6
  15. package/vendor/packages/edit-store/lib/timeline-map.d.ts +5 -0
  16. package/vendor/packages/edit-store/lib/timeline-map.js +40 -0
  17. package/vendor/packages/edit-store/lib/transition-vocabulary.d.ts +223 -0
  18. package/vendor/packages/edit-store/lib/transition-vocabulary.js +45 -0
  19. package/vendor/packages/edit-store/lib/webview-kernel.js +98 -0
  20. package/vendor/packages/matte-rvm/package.json +22 -0
  21. package/vendor/packages/schemas/bin/validate-edit.mjs +5 -2
  22. package/vendor/packages/schemas/edit.schema.json +2 -2
  23. package/vendor/packages/schemas/test/transition-vocabulary.test.mjs +64 -0
  24. package/vendor/packages/schemas/test/validate-edit.test.mjs +2 -2
  25. package/vendor/skills/analyze-footage/bin/person-matte/person-cutout.mjs +453 -0
  26. package/vendor/skills/analyze-footage/bin/person-matte/person-matte.mjs +163 -48
  27. package/vendor/skills/analyze-footage/person-matte.md +135 -5
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "akari-video",
3
- "version": "0.1.19",
3
+ "version": "0.1.21",
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": {
@@ -62,6 +62,7 @@
62
62
  "packages/export-nle/package.json",
63
63
  "packages/intake-form/package.json",
64
64
  "packages/intake-form/README.md",
65
+ "packages/matte-rvm/package.json",
65
66
  "packages/media-bin/package.json",
66
67
  "packages/overlay-runtime/package.json",
67
68
  "packages/overlay-runtime/README.md",
@@ -57,7 +57,7 @@
57
57
  "person_matte": { // null(未生成)/ string(旧形)も有効
58
58
  "path": "matte/person-matte.webm", // 必須。VP9 alpha WebM への相対 or 絶対パス
59
59
  "fps": 24, // 必須。マット動画の fps(元素材と異なってよい)
60
- "quality": "balanced", // 任意。fast / balanced / accurate を例示(enum 強制はしない)
60
+ "quality": "balanced", // 任意。fast / balanced / accurate / best を例示(enum 強制はしない)
61
61
  "generated_at": "2026-07-23T01:33:30.069Z", // 任意。ISO8601
62
62
  "tool": "vision-person-segmentation" // 任意。生成手段の記録
63
63
  }
@@ -72,7 +72,7 @@
72
72
  | `tracks.person_matte` | object \| string \| null | **必須(キー)** | — | `null` = 未生成。string = 旧形(`{path}` の糖衣・非推奨) |
73
73
  | `person_matte.path` | string | **必須** | — | マット動画へのパス。相対パスは **analysis.json の所在ディレクトリ**基準、区切りは `/`。実体は VP9 alpha WebM(§3) |
74
74
  | `person_matte.fps` | number | **必須** | — | マット動画の fps。0 より大きい。**元素材の fps と一致しなくてよい**(§4 の時刻対応は fps に依存しない) |
75
- | `person_matte.quality` | string | 否 | — | 生成品質。`fast` / `balanced` / `accurate` を例示。**enum 強制はしない** |
75
+ | `person_matte.quality` | string | 否 | — | 生成品質。`fast` / `balanced` / `accurate` / `best` を例示。**enum 強制はしない** |
76
76
  | `person_matte.generated_at` | string | 否 | — | 生成時刻(ISO8601) |
77
77
  | `person_matte.tool` | string | 否 | — | 生成手段の記録。`vision-person-segmentation` を例示 |
78
78
 
@@ -171,6 +171,45 @@ HEVC alpha MOV は「Apple 系ツールへの受け渡しが要るとき」の
171
171
  quality を上げると Vision の側が伸びる(`accurate` は 135ms/frame・peak 638MB)。
172
172
  `fast` は輪郭が階段状になり本番品質に達しないため、当たり付け専用とする。
173
173
 
174
+ | quality | engine | 用途・実測 |
175
+ |---|---|---|
176
+ | `fast` | Vision | 当たり付け専用。256x192 のマット |
177
+ | `balanced`(既定) | Vision | 通常の本番用途。512x384 のマット |
178
+ | `accurate` | Vision | 寄りカットの仕上げ。2016x1512 のマット |
179
+ | `best` | RVM mobilenetv3 | 髪の毛レベルの細部が必要なときだけ明示指定。CPU 実測 **約 178〜289 ms/frame** |
180
+ | `best --model resnet50` | RVM resnet50 | 処理時間をさらに許容できる場合のこだわり指定 |
181
+
182
+ Mac では `fast` / `balanced` / `accurate` に Vision、`best` に RVM を使う。Windows の対応は
183
+ §6.2 で定める。
184
+
185
+ ### 6.1 RVM の ExecutionProvider 規律
186
+
187
+ RVM は **CPU のみ**で実行する。較正ではアクセラレーション用 EP が CPU と異なるマットを出し、
188
+ 二値マスク IoU が 0.8057 まで崩れた。将来別の EP を追加するときは、採用候補と CPU の出力一致を
189
+ 同一 raw BGRA 入力で測り、**IoU ≈ 1.0 を実測してから**有効化することを受け入れ条件とする。
190
+
191
+ ### 6.2 Windows
192
+
193
+ Windows(`win32`)では Vision を利用できないため、**全品質段を RVM へ接続する**。`quality` は
194
+ ユーザーが指定した値をそのまま結果と `tracks.person_matte.quality` に残すが、実行エンジンと既定モデルは
195
+ 次の表に固定する。
196
+
197
+ | quality | engine | 既定モデル |
198
+ |---|---|---|
199
+ | `fast` | RVM | mobilenetv3 |
200
+ | `balanced`(既定) | RVM | mobilenetv3 |
201
+ | `accurate` | RVM | mobilenetv3 |
202
+ | `best` | RVM | mobilenetv3 |
203
+ | `best --model resnet50` | RVM | resnet50 |
204
+
205
+ Windows では RVM が唯一のエンジンなので、mobilenetv3 モデルの配備を必須とする。`--check` はモデルが
206
+ 無ければ `available:false` と取得用の `fetchHint` を返す。Mac では従来どおり、RVM モデルが無くても
207
+ Vision の品質段を使えるため `available:true` のままとする。
208
+
209
+ ffmpeg / ffprobe は OS の PATH 名を直接起動せず、`packages/media-bin` の `resolveFfmpeg()` /
210
+ `resolveFfprobe()` で解決する。これにより Windows では同梱されたプラットフォーム別バイナリも探索対象に
211
+ なり、デコード、VP9 alpha エンコード、出力検証の全工程で同じ解決規則を使う。
212
+
174
213
  手順は `skills/analyze-footage/person-matte.md`(`bin/person-matte/` のヘルパー)に置く。
175
214
 
176
215
  ## 7. 検証
@@ -215,3 +254,64 @@ JSON として読めるかまでしか見ていない(`analysis.schema` チェ
215
254
  マットが取れることと、それを本番の合成として出せることは別である
216
255
  (`skills/overlay-authoring/text-behind-person.md` の「現在の制約を先に判定する」と同じ判断)
217
256
  - overlay 断片からの**相対 video URL の解決**(preview / 書き出し双方)が未整備
257
+
258
+ ## 10. 配線コマンド person-cutout
259
+
260
+ `skills/analyze-footage/bin/person-matte/person-cutout.mjs` は、編集済みカットの人物マット生成と
261
+ v2 `edit.json` への配線を 1 回で行う決定論的 CLI である。§4 の素材全体マットとは異なり、これは
262
+ プロジェクト固有のカット区間マットを `analysis.json` を経由せず編集トラックへ直接置く。
263
+
264
+ ### 10.1 引数と出力規律
265
+
266
+ ```text
267
+ --project <dir> 必須。edit.json を持つプロジェクト
268
+ --cut <index[,index...]> 必須。0 始まり
269
+ --quality fast|balanced|accurate|best 省略時 balanced
270
+ --model mobilenetv3|resnet50 best のときだけ指定可
271
+ --dry-run 生成・書き換えを行わず予定だけ返す
272
+ ```
273
+
274
+ v2 だけを受理し、v0/v1 は「v2 へ migrate してから」という復旧案内付きで失敗する。stdout は成功・
275
+ 失敗とも 1 行 JSON とし、成功時は `ok`、マットのパスと実測値、追加または更新したレイヤー、track
276
+ 変更の有無、検証結果を含む。失敗時は `ok:false` と `reason` を返し、exit code を非 0 にする。
277
+
278
+ `--dry-run` は edit.json とファイルシステムを変更しない。選択対象は v2 `tracks[]` を下から上、
279
+ 各 `items[]` を宣言順に走査した visual `source.kind:"media"` item である。自動生成した
280
+ `person-N` item と `assets/matte/person-N.webm` source は index の母集団から除外するため、再実行後も
281
+ 同じ index が同じ元カットを指す。
282
+
283
+ ### 10.2 置き場、時間、速度
284
+
285
+ マットは `<project>/assets/matte/person-<cut index>.webm` に固定する。元カットの source path、
286
+ `source.in` / `source.out`、出力 `at` / `duration` を解決し、プロジェクトの整数 fps を使う。
287
+
288
+ `source.speed` がある場合は、ffmpeg の `setpts=(PTS-STARTPTS)/speed` 相当を**マット生成より先に**
289
+ 適用する。省略時は source 区間と出力 duration が 1 フレーム以内なら 1 倍、それを超えて異なる場合は
290
+ `(out-in) / (duration/fps)` を実効速度とする。速度適用済みの一時動画は `os.tmpdir()` 下に置き、成功・
291
+ 失敗を問わず削除する。`person-matte.mjs` には出力 fps を渡し、生成 item の `at` / `duration` は元
292
+ カットと同じ整数フレーム値にする。
293
+
294
+ ### 10.3 v2 track と z 順
295
+
296
+ 現行 v2 は旧版のトップレベル `layers[]` / `timeline.tracks` を持たない。`tracks[]` の配列順そのものが
297
+ 画面の下から上への z 順で、各 visual track の `items[]` が旧 layer item の役割を持つ。このため
298
+ コマンドは次を v2 ネイティブ表現として出力する。
299
+
300
+ - `sources[]`: `{id:"person-cutout-N", path:"assets/matte/person-N.webm"}`
301
+ - 最前面 visual track: `{id:"person-cutout", lane:"visual", items:[...]}`
302
+ - item: 元カットと同じ `at` / `duration`、`source.kind:"media"`、`in:0`、
303
+ `out:duration/fps`
304
+
305
+ 既存 track の相互順は保持し、人物 track だけを末尾(最前面)へ挿入または移動する。これにより人物は
306
+ HTML overlay / telop より上に来る。`deriveTracks` 相当の既定順は変更しない。
307
+
308
+ ### 10.4 冪等性と書き込みゲート
309
+
310
+ 同じ cut の再実行では `person-cutout-N` source、`person-N` item、既存マットを更新し、重複を追加しない。
311
+ 人物 track も常に 1 本である。複数 cut 指定では同じ track に item を集約し、`at`、次いで id の順に
312
+ 決定的に並べる。
313
+
314
+ パッチ候補はまず v2 reader で閉じた語彙、参照、一意性、整数フレームを検証し、同一ディレクトリの
315
+ 一時 edit.json に書いて `packages/schemas/bin/validate-edit.mjs` も通す。両方が成功した場合だけ元
316
+ `edit.json` へ atomic rename する。不合格なら一時ファイルを削除し、元 edit.json は一切変更しない。
317
+ バックアップは作らない(git 管理を復旧手段とする)。
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "akari-video",
3
- "version": "0.1.19",
3
+ "version": "0.1.21",
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. Run them from a full AKARI Video app installation (normally ~/.akari/app) or a monorepo checkout.]",
5
5
  "type": "module",
6
6
  "files": [
@@ -20,11 +20,16 @@ import { resolveFfmpeg, resolveFfprobe } from "../../media-bin/src/index.mjs";
20
20
 
21
21
  const {
22
22
  areCutsAdjacent,
23
+ cutOverlapFrames,
24
+ findCrossTrackLayerEvacuations,
23
25
  findUnsupportedDeclaredTrackTransitions,
26
+ isStillImageSourcePath,
27
+ planTransitionHandleWindow,
24
28
  projectLegacyEdit,
25
29
  readInternalEdit,
26
30
  resolveCaptionDisplay,
27
31
  visualContentEndSeconds,
32
+ TRANSITION_TYPE_IDS,
28
33
  } = createRequire(import.meta.url)("../../edit-store/lib/index.js");
29
34
 
30
35
  const VERSION = 1;
@@ -135,6 +140,7 @@ export async function lintProject(input, options = {}) {
135
140
  const rawEdit = edit;
136
141
  const internalEdit = readInternalEdit(rawEdit);
137
142
  const legacyEdit = projectLegacyEdit(internalEdit);
143
+ validateTransitionLayerEvacuations(rawEdit, internalEdit, findings);
138
144
  const rawAudio = isRecord(rawEdit.audio) ? rawEdit.audio : {};
139
145
  const projectedAudio = projectAudioForLint(internalEdit);
140
146
  edit = {
@@ -254,7 +260,7 @@ export async function lintProject(input, options = {}) {
254
260
  validateCutTransformFields(edit.cuts, findings);
255
261
  validateStillImageCuts(edit, findings);
256
262
  const cutTrackSegments = computeCutTrackSegments(edit.cuts);
257
- validateTransitionAdjacency(edit.cuts, cutTrackSegments, edit.fps, findings);
263
+ validateTransitionAdjacency(edit.cuts, cutTrackSegments, edit.sources, edit.fps, findings);
258
264
  for (const segment of findTrackOverlaps(cutTrackSegments)) {
259
265
  if (isDeclaredTransitionOverlap(edit.cuts, cutTrackSegments, segment, edit.fps)) continue;
260
266
  addFinding(findings, {
@@ -319,14 +325,47 @@ export async function lintProject(input, options = {}) {
319
325
  return writeResult(findings, skipped, inputs, paths, options);
320
326
  }
321
327
 
322
- function validateTransitionAdjacency(cuts, segments, fps, findings) {
328
+ function validateTransitionAdjacency(cuts, segments, sources, fps, findings) {
329
+ const sourcePaths = new Map((Array.isArray(sources) ? sources : [])
330
+ .filter(source => isRecord(source) && typeof source.id === "string")
331
+ .map(source => [source.id, source.path]));
332
+ const isStillCut = cut => isStillImageSourcePath(sourcePaths.get(cut?.src) ?? cut?.src);
323
333
  for (let position = 0; position < segments.length; position += 1) {
324
334
  const earlier = segments[position];
325
335
  const transition = cuts?.[earlier.index]?.transition_out;
326
336
  if (!isRecord(transition) || !isPositiveNumber(transition.duration)) continue;
327
337
  const later = segments.slice(position + 1).find(candidate => candidate.track === earlier.track);
328
- if (!later || later.start <= earlier.end) continue;
329
- if (areCutsAdjacent(
338
+ if (!later) continue;
339
+ const overlapFrames = cutOverlapFrames(
340
+ { tlEnd: earlier.end },
341
+ { tlStart: later.start },
342
+ fps,
343
+ );
344
+ if (overlapFrames === 0) {
345
+ const outgoingCut = cuts?.[earlier.index];
346
+ const incomingCut = cuts?.[later.index];
347
+ const incomingSpeed = isPositiveNumber(incomingCut?.speed) ? incomingCut.speed : 1;
348
+ const plan = planTransitionHandleWindow({
349
+ declaredSeconds: transition.duration,
350
+ outgoingTailRoomSeconds: Number.POSITIVE_INFINITY,
351
+ incomingHeadRoomSeconds: isStillCut(incomingCut)
352
+ ? Number.POSITIVE_INFINITY : Math.max(0, Number(incomingCut?.in) || 0) / incomingSpeed,
353
+ outgoingDurationSeconds: Math.max(0, earlier.end - earlier.start),
354
+ incomingDurationSeconds: Math.max(0, later.end - later.start),
355
+ });
356
+ if (plan.effectiveSeconds <= 0) {
357
+ addFinding(findings, {
358
+ severity: "warning",
359
+ check: "cuts.transition-out.zero-overlap",
360
+ message: "トランジションを宣言していますが、のりしろにできる素材の余りがないため効きません。素材のトリムを調整するか、トランジションを削除してください。",
361
+ path: `edit.json#cuts[${earlier.index}].transition_out`,
362
+ range: { start: earlier.end, end: later.start },
363
+ });
364
+ }
365
+ continue;
366
+ }
367
+ // 正の重なりは、宣言尺未満なら既存の短縮クランプ、宣言尺超なら track-overlap が担当する。
368
+ if (overlapFrames > 0 || areCutsAdjacent(
330
369
  { tlEnd: earlier.end, transitionOut: { duration: transition.duration } },
331
370
  { tlStart: later.start },
332
371
  fps,
@@ -341,6 +380,50 @@ function validateTransitionAdjacency(cuts, segments, fps, findings) {
341
380
  }
342
381
  }
343
382
 
383
+ function validateTransitionLayerEvacuations(rawEdit, internalEdit, findings) {
384
+ if (!isRecord(rawEdit) || rawEdit.version !== 2) return;
385
+ const crossTrackCauses = new Map();
386
+ for (const cause of findCrossTrackLayerEvacuations(rawEdit)) {
387
+ if (!crossTrackCauses.has(cause.itemId)) crossTrackCauses.set(cause.itemId, cause);
388
+ }
389
+ const rawLocations = new Map();
390
+ if (Array.isArray(rawEdit.tracks)) {
391
+ rawEdit.tracks.forEach((track, trackIndex) => {
392
+ if (!isRecord(track) || !Array.isArray(track.items)) return;
393
+ track.items.forEach((item, itemIndex) => {
394
+ if (isRecord(item) && typeof item.id === "string") {
395
+ rawLocations.set(item.id, { trackIndex, itemIndex });
396
+ }
397
+ });
398
+ });
399
+ }
400
+ for (const track of internalEdit.tracks) {
401
+ for (const item of track.items) {
402
+ const transition = item.declaration?.transition_out;
403
+ if (item.legacy.collection !== "layers" || !isRecord(transition)) continue;
404
+ const location = rawLocations.get(item.id);
405
+ const cause = crossTrackCauses.get(item.id);
406
+ const reason = cause
407
+ ? `このクリップは他トラックのアイテム(${cause.causeItemId})と重なっているため PiP 経路へ退避され、宣言したトランジションは書き出されません。`
408
+ : "このクリップは合成機能または同一トラック内の重なりにより PiP 経路へ退避され、宣言したトランジションは書き出されません。";
409
+ addFinding(findings, {
410
+ severity: "warning",
411
+ check: "cuts.transition-out.layer-evacuated",
412
+ message: `${reason}重なりを解消するか、トランジションを削除してください。`,
413
+ path: location
414
+ ? `edit.json#tracks[${location.trackIndex}].items[${location.itemIndex}].source.transition_out`
415
+ : `edit.json#tracks[${track.z}].items`,
416
+ ...(cause ? {
417
+ range: {
418
+ start: cause.overlapStartFrames / internalEdit.output.fps,
419
+ end: cause.overlapEndFrames / internalEdit.output.fps,
420
+ },
421
+ } : {}),
422
+ });
423
+ }
424
+ }
425
+ }
426
+
344
427
  function projectAudioForLint(internalEdit) {
345
428
  const sfx = [];
346
429
  const narration = [];
@@ -829,8 +912,8 @@ function validateTransitionOut(value, findings, path) {
829
912
  addFinding(findings, { severity: "error", check: "cuts.transition-out.structure", message: "transition_out must be an object", path });
830
913
  return;
831
914
  }
832
- if (!["dissolve", "fade-black", "fade-white", "reveal-down", "reveal-up"].includes(value.type)) {
833
- addFinding(findings, { severity: "error", check: "cuts.transition-out.type", message: "type must be dissolve/fade-black/fade-white/reveal-down/reveal-up", path });
915
+ if (!TRANSITION_TYPE_IDS.includes(value.type)) {
916
+ addFinding(findings, { severity: "error", check: "cuts.transition-out.type", message: `type must be ${TRANSITION_TYPE_IDS.join("/")}`, path });
834
917
  }
835
918
  if (!isPositiveNumber(value.duration)) {
836
919
  addFinding(findings, { severity: "error", check: "cuts.transition-out.duration", message: "duration must be a positive number", path });
@@ -1023,17 +1106,6 @@ function validateCutTransformFields(cuts, findings) {
1023
1106
  }
1024
1107
  }
1025
1108
 
1026
- // docs/contract-2026-08-12-still-image-cut-source-v0.md 裁定1: 判定は拡張子のみ。同じ集合
1027
- // (png/jpe?g/webp/bmp/gif, 大小無視) を packages/render-cut/src/layers.mjs の
1028
- // IMAGE_LAYER_SOURCE_PATTERN / plan.mjs の chroma_key 背景判定と揃える。edit-lint はスキーマ
1029
- // パッケージから独立しているため、ここでは同じパターンを別リテラルとして持つ(3面パリティ
1030
- // テストと同じ流儀: packages/preview-server/test/image-layer-source.test.mjs 参照)。
1031
- const IMAGE_CUT_SOURCE_PATTERN = /\.(png|jpe?g|webp|bmp|gif)$/iu;
1032
-
1033
- function isImageCutSourcePath(pathValue) {
1034
- return typeof pathValue === "string" && IMAGE_CUT_SOURCE_PATTERN.test(pathValue);
1035
- }
1036
-
1037
1109
  // 裁定3〜5(in/out 意味論・freeze/speed 警告・v0 空 cuts 拒否)。source が静止画のときだけ発火する。
1038
1110
  function validateStillImageCuts(edit, findings) {
1039
1111
  if (!isRecord(edit)) return;
@@ -1041,7 +1113,7 @@ function validateStillImageCuts(edit, findings) {
1041
1113
 
1042
1114
  const imageSourceIds = new Set(
1043
1115
  (Array.isArray(edit.sources) ? edit.sources : [])
1044
- .filter((source) => isRecord(source) && isImageCutSourcePath(source.path))
1116
+ .filter((source) => isRecord(source) && isStillImageSourcePath(source.path))
1045
1117
  .map((source) => source.id),
1046
1118
  );
1047
1119
  if (imageSourceIds.size === 0) return;
@@ -12,6 +12,31 @@ export interface CutAdjacencyEarlierLike {
12
12
  export interface CutAdjacencyLaterLike {
13
13
  tlStart: number;
14
14
  }
15
+ export declare const DEFAULT_CUT_ADJACENCY_FPS = 30;
16
+ export declare function effectiveCutFps(fps: number): number;
17
+ /**
18
+ * earlier の終端と later の開始を出力 fps へ量子化し、実重なりをフレーム数で返す。
19
+ * 正数は重なり、0 は突き合わせ、負数はすき間を表す。
20
+ */
21
+ export declare function cutOverlapFrames(earlier: Pick<CutAdjacencyEarlierLike, 'tlEnd'>, later: CutAdjacencyLaterLike, fps?: number): number;
22
+ export interface TransitionHandleWindowInput {
23
+ declaredSeconds: number;
24
+ /** outgoing の out より後ろに残る素材尺(出力秒)。不明なら Infinity。 */
25
+ outgoingTailRoomSeconds: number;
26
+ /** incoming の in より前に残る素材尺(出力秒)。 */
27
+ incomingHeadRoomSeconds: number;
28
+ outgoingDurationSeconds: number;
29
+ incomingDurationSeconds: number;
30
+ }
31
+ export interface TransitionHandleWindowPlan {
32
+ effectiveSeconds: number;
33
+ halfSeconds: number;
34
+ outcome: 'full' | 'clamped' | 'none';
35
+ }
36
+ /** 隠れのりしろ窓の実効尺を秒の連続量で決める単一定義。 */
37
+ export declare function planTransitionHandleWindow(input: TransitionHandleWindowInput): TransitionHandleWindowPlan;
38
+ export declare const STILL_IMAGE_SOURCE_PATTERN: RegExp;
39
+ export declare function isStillImageSourcePath(path: unknown): boolean;
15
40
  /**
16
41
  * ギャップ 0、または earlier の宣言済み transitionOut.duration で説明できる重なりだけを
17
42
  * 隣接とみなす。秒の誤差ではなく、出力 fps で量子化したフレーム数を比較する。
@@ -4,20 +4,50 @@
4
4
  * タイムライン UI と edit-lint は必ずこの関数を使い、フレーム量子化の条件式を複製しない。
5
5
  */
6
6
  Object.defineProperty(exports, "__esModule", { value: true });
7
+ exports.STILL_IMAGE_SOURCE_PATTERN = exports.DEFAULT_CUT_ADJACENCY_FPS = void 0;
8
+ exports.effectiveCutFps = effectiveCutFps;
9
+ exports.cutOverlapFrames = cutOverlapFrames;
10
+ exports.planTransitionHandleWindow = planTransitionHandleWindow;
11
+ exports.isStillImageSourcePath = isStillImageSourcePath;
7
12
  exports.areCutsAdjacent = areCutsAdjacent;
8
- const DEFAULT_CUT_ADJACENCY_FPS = 30;
9
- function effectiveFps(fps) {
10
- return Number.isFinite(fps) && fps > 0 ? fps : DEFAULT_CUT_ADJACENCY_FPS;
13
+ exports.DEFAULT_CUT_ADJACENCY_FPS = 30;
14
+ function effectiveCutFps(fps) {
15
+ return Number.isFinite(fps) && fps > 0 ? fps : exports.DEFAULT_CUT_ADJACENCY_FPS;
16
+ }
17
+ /**
18
+ * earlier の終端と later の開始を出力 fps へ量子化し、実重なりをフレーム数で返す。
19
+ * 正数は重なり、0 は突き合わせ、負数はすき間を表す。
20
+ */
21
+ function cutOverlapFrames(earlier, later, fps = exports.DEFAULT_CUT_ADJACENCY_FPS) {
22
+ const resolvedFps = effectiveCutFps(fps);
23
+ return Math.round(earlier.tlEnd * resolvedFps) - Math.round(later.tlStart * resolvedFps);
24
+ }
25
+ const nonNegativeRoom = (value) => value === Number.POSITIVE_INFINITY
26
+ ? value
27
+ : Number.isFinite(value) && value > 0 ? value : 0;
28
+ /** 隠れのりしろ窓の実効尺を秒の連続量で決める単一定義。 */
29
+ function planTransitionHandleWindow(input) {
30
+ const declaredSeconds = Number.isFinite(input.declaredSeconds) && input.declaredSeconds > 0
31
+ ? input.declaredSeconds : 0;
32
+ const effectiveSeconds = Math.max(0, Math.min(declaredSeconds, 2 * nonNegativeRoom(input.outgoingTailRoomSeconds), 2 * nonNegativeRoom(input.incomingHeadRoomSeconds), 2 * nonNegativeRoom(input.outgoingDurationSeconds), 2 * nonNegativeRoom(input.incomingDurationSeconds)));
33
+ return {
34
+ effectiveSeconds,
35
+ halfSeconds: effectiveSeconds / 2,
36
+ outcome: effectiveSeconds <= 0 ? 'none'
37
+ : effectiveSeconds < declaredSeconds ? 'clamped' : 'full'
38
+ };
39
+ }
40
+ exports.STILL_IMAGE_SOURCE_PATTERN = /\.(png|jpe?g|webp|bmp|gif)$/iu;
41
+ function isStillImageSourcePath(path) {
42
+ return typeof path === 'string' && exports.STILL_IMAGE_SOURCE_PATTERN.test(path);
11
43
  }
12
44
  /**
13
45
  * ギャップ 0、または earlier の宣言済み transitionOut.duration で説明できる重なりだけを
14
46
  * 隣接とみなす。秒の誤差ではなく、出力 fps で量子化したフレーム数を比較する。
15
47
  */
16
- function areCutsAdjacent(earlier, later, fps = DEFAULT_CUT_ADJACENCY_FPS) {
17
- const resolvedFps = effectiveFps(fps);
18
- const earlierEndFrame = Math.round(earlier.tlEnd * resolvedFps);
19
- const laterStartFrame = Math.round(later.tlStart * resolvedFps);
20
- const overlapFrames = earlierEndFrame - laterStartFrame;
48
+ function areCutsAdjacent(earlier, later, fps = exports.DEFAULT_CUT_ADJACENCY_FPS) {
49
+ const resolvedFps = effectiveCutFps(fps);
50
+ const overlapFrames = cutOverlapFrames(earlier, later, resolvedFps);
21
51
  const declaredDuration = earlier.transitionOut?.duration;
22
52
  const declaredOverlapFrames = typeof declaredDuration === 'number'
23
53
  && Number.isFinite(declaredDuration) && declaredDuration > 0
@@ -1,3 +1,4 @@
1
+ import { type ReadableTransitionType, type TransitionType } from './transition-vocabulary';
1
2
  export interface EditCut {
2
3
  in: number;
3
4
  out: number;
@@ -11,7 +12,7 @@ export interface EditCut {
11
12
  opacity?: number;
12
13
  speed?: number;
13
14
  transitionOut?: {
14
- type: 'dissolve' | 'fade-black' | 'fade-white' | 'reveal-down' | 'reveal-up';
15
+ type: ReadableTransitionType;
15
16
  duration: number;
16
17
  };
17
18
  at?: number;
@@ -140,9 +141,19 @@ export declare function updateCutTransformInSource(source: string, cutIndex: num
140
141
  }): string;
141
142
  export declare function updateCutOpacityInSource(source: string, cutIndex: number, opacity: number | null): string;
142
143
  export declare function setCutTransitionOutInSource(source: string, cutIndex: number, transitionOut: {
143
- type: 'dissolve' | 'fade-black' | 'fade-white' | 'reveal-down' | 'reveal-up';
144
+ type: TransitionType;
144
145
  duration: number;
145
146
  } | null): string;
147
+ export interface RemoveV2TransitionOutWithHandleRetractInput {
148
+ itemId: string;
149
+ retractFrames: number;
150
+ fps: number;
151
+ }
152
+ /**
153
+ * v0.1.20 の自動のりしろ debris を、transition_out の削除と同じ 1 回の
154
+ * byte-preserving 手術で回収する。新意味論の通常操作では trim を一切変更しない。
155
+ */
156
+ export declare function removeV2TransitionOutWithHandleRetractInSource(source: string, input: RemoveV2TransitionOutWithHandleRetractInput): string;
146
157
  export declare function reorderCutsInSource(source: string, fromIndex: number, toIndex: number): string;
147
158
  export declare function splitCutInSource(source: string, cutIndex: number, atSeconds: number): string;
148
159
  export declare function deleteCutInSource(source: string, cutIndex: number): {
@@ -9,6 +9,7 @@ exports.setCutSpeedInSource = setCutSpeedInSource;
9
9
  exports.updateCutTransformInSource = updateCutTransformInSource;
10
10
  exports.updateCutOpacityInSource = updateCutOpacityInSource;
11
11
  exports.setCutTransitionOutInSource = setCutTransitionOutInSource;
12
+ exports.removeV2TransitionOutWithHandleRetractInSource = removeV2TransitionOutWithHandleRetractInSource;
12
13
  exports.reorderCutsInSource = reorderCutsInSource;
13
14
  exports.splitCutInSource = splitCutInSource;
14
15
  exports.deleteCutInSource = deleteCutInSource;
@@ -37,6 +38,7 @@ exports.removeOverlayInSource = removeOverlayInSource;
37
38
  exports.writeTimelineTracksInSource = writeTimelineTracksInSource;
38
39
  exports.updateArrayElementByIndex = updateArrayElementByIndex;
39
40
  exports.updateOverlayVarInSource = updateOverlayVarInSource;
41
+ const transition_vocabulary_1 = require("./transition-vocabulary");
40
42
  const JSON_NUMBER = '-?(?:0|[1-9]\\d*)(?:\\.\\d+)?(?:[eE][+-]?\\d+)?';
41
43
  const LAYER_BLEND_MODES = [
42
44
  'normal', 'screen', 'multiply', 'add', 'difference',
@@ -318,9 +320,7 @@ function updateCutOpacityInSource(source, cutIndex, opacity) {
318
320
  }
319
321
  function setCutTransitionOutInSource(source, cutIndex, transitionOut) {
320
322
  if (transitionOut !== null) {
321
- if (transitionOut.type !== 'dissolve' && transitionOut.type !== 'fade-black'
322
- && transitionOut.type !== 'fade-white' && transitionOut.type !== 'reveal-down'
323
- && transitionOut.type !== 'reveal-up') {
323
+ if (!(0, transition_vocabulary_1.isTransitionType)(transitionOut.type)) {
324
324
  throw new Error('トランジションの種別が不正です。');
325
325
  }
326
326
  if (!Number.isFinite(transitionOut.duration) || transitionOut.duration <= 0) {
@@ -348,6 +348,72 @@ function setCutTransitionOutInSource(source, cutIndex, transitionOut) {
348
348
  }
349
349
  });
350
350
  }
351
+ /**
352
+ * v0.1.20 の自動のりしろ debris を、transition_out の削除と同じ 1 回の
353
+ * byte-preserving 手術で回収する。新意味論の通常操作では trim を一切変更しない。
354
+ */
355
+ function removeV2TransitionOutWithHandleRetractInSource(source, input) {
356
+ const raw = JSON.parse(source);
357
+ if (raw.version !== 2) {
358
+ throw new Error('v2 へ変換してから編集してください。');
359
+ }
360
+ if (!input.itemId) {
361
+ throw new Error('トランジション対象のアイテム id が空です。');
362
+ }
363
+ if (!Number.isInteger(input.retractFrames) || input.retractFrames <= 0
364
+ || !Number.isFinite(input.fps) || input.fps <= 0) {
365
+ throw new Error('のりしろの復元量が不正です。');
366
+ }
367
+ const tracks = locateArray(source, 'tracks');
368
+ const trackElements = splitTopLevelElements(tracks.inner);
369
+ const matches = [];
370
+ for (const track of trackElements) {
371
+ let items;
372
+ try {
373
+ items = locateArray(track.text, 'items');
374
+ }
375
+ catch {
376
+ continue;
377
+ }
378
+ const item = splitTopLevelElements(items.inner)
379
+ .find(candidate => readStringProperty(candidate.text, 'id') === input.itemId);
380
+ if (item)
381
+ matches.push({ track, items, item });
382
+ }
383
+ if (matches.length !== 1) {
384
+ throw new Error(matches.length === 0
385
+ ? `アイテム ${input.itemId} が見つかりません`
386
+ : `アイテム ${input.itemId} が複数あります`);
387
+ }
388
+ const match = matches[0];
389
+ const label = `アイテム ${input.itemId}`;
390
+ const durationFrames = readNumberProperty(match.item.text, 'duration', label);
391
+ if (!Number.isInteger(durationFrames) || durationFrames < input.retractFrames) {
392
+ throw new Error(`${label} の duration を安全に復元できません。`);
393
+ }
394
+ const mediaSource = locateTopLevelObjectProperty(match.item.text, 'source');
395
+ if (readStringProperty(mediaSource.text, 'kind') !== 'media') {
396
+ throw new Error(`${label} は映像素材ではありません。`);
397
+ }
398
+ const sourceIn = readNumberProperty(mediaSource.text, 'in', label);
399
+ const sourceOut = readNumberProperty(mediaSource.text, 'out', label);
400
+ const speed = (sourceOut - sourceIn) / (durationFrames / input.fps);
401
+ if (!Number.isFinite(speed) || speed <= 0) {
402
+ throw new Error(`${label} の speed を安全に復元できません。`);
403
+ }
404
+ const nextOut = sourceOut - (input.retractFrames / input.fps) * speed;
405
+ if (!Number.isFinite(nextOut) || nextOut < sourceIn) {
406
+ throw new Error(`${label} の out を安全に復元できません。`);
407
+ }
408
+ let nextMediaSource = removeObjectProperty(mediaSource.text, 'transition_out');
409
+ nextMediaSource = replaceNumberProperty(nextMediaSource, 'out', nextOut, label);
410
+ let nextItem = match.item.text.slice(0, mediaSource.start)
411
+ + nextMediaSource
412
+ + match.item.text.slice(mediaSource.end);
413
+ nextItem = replaceNumberProperty(nextItem, 'duration', durationFrames - input.retractFrames, label);
414
+ const nextTrack = replaceElement(match.track.text, match.items.openIndex + 1, match.item, nextItem);
415
+ return replaceElement(source, tracks.openIndex + 1, match.track, nextTrack);
416
+ }
351
417
  function reorderCutsInSource(source, fromIndex, toIndex) {
352
418
  const array = locateArray(source, 'cuts');
353
419
  const elements = splitTopLevelElements(array.inner);
@@ -16,5 +16,6 @@ export * from './retime';
16
16
  export * from './track-order';
17
17
  export * from './track-transition-compatibility';
18
18
  export * from './cut-adjacency';
19
+ export * from './transition-vocabulary';
19
20
  export { parseEdit } from './migrate/legacy-parse';
20
21
  export { LegacyEditVersionError } from './migrate/error';
@@ -33,6 +33,7 @@ __exportStar(require("./retime"), exports);
33
33
  __exportStar(require("./track-order"), exports);
34
34
  __exportStar(require("./track-transition-compatibility"), exports);
35
35
  __exportStar(require("./cut-adjacency"), exports);
36
+ __exportStar(require("./transition-vocabulary"), exports);
36
37
  // Legacy parser implementation lives in the frozen migration unit. This re-export keeps
37
38
  // text-surgery consumers source-compatible while preventing legacy knowledge from returning
38
39
  // to edit-store.ts.
@@ -167,6 +167,19 @@ export declare function readInternalSources(source: string | unknown): InternalS
167
167
  * 検証される側であり、検証対象自身を尺の分母に混ぜると常に「収まっている」判定になってしまう。
168
168
  */
169
169
  export declare function visualContentEndSeconds(internal: InternalEdit): number;
170
+ export interface CrossTrackLayerEvacuation {
171
+ itemId: string;
172
+ trackId: string;
173
+ causeItemId: string;
174
+ causeTrackId: string;
175
+ overlapStartFrames: number;
176
+ overlapEndFrames: number;
177
+ }
178
+ /**
179
+ * 別 visual track との重なりが原因で upper item が layers へ退避される組を返す。
180
+ * edit-lint と UI は理由文言に必要な相手 id を、この単一定義から得る。
181
+ */
182
+ export declare function findCrossTrackLayerEvacuations(edit: unknown): CrossTrackLayerEvacuation[];
170
183
  export interface LegacyEditView {
171
184
  cuts: EditCut[];
172
185
  sources?: EditSource[];