akari-video 0.1.20 → 0.1.22

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
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "akari-video",
3
- "version": "0.1.20",
3
+ "version": "0.1.22",
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.20",
3
+ "version": "0.1.22",
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": [
@@ -23,10 +23,13 @@ const {
23
23
  cutOverlapFrames,
24
24
  findCrossTrackLayerEvacuations,
25
25
  findUnsupportedDeclaredTrackTransitions,
26
+ isStillImageSourcePath,
27
+ planTransitionHandleWindow,
26
28
  projectLegacyEdit,
27
29
  readInternalEdit,
28
30
  resolveCaptionDisplay,
29
31
  visualContentEndSeconds,
32
+ TRANSITION_TYPE_IDS,
30
33
  } = createRequire(import.meta.url)("../../edit-store/lib/index.js");
31
34
 
32
35
  const VERSION = 1;
@@ -257,7 +260,7 @@ export async function lintProject(input, options = {}) {
257
260
  validateCutTransformFields(edit.cuts, findings);
258
261
  validateStillImageCuts(edit, findings);
259
262
  const cutTrackSegments = computeCutTrackSegments(edit.cuts);
260
- validateTransitionAdjacency(edit.cuts, cutTrackSegments, edit.fps, findings);
263
+ validateTransitionAdjacency(edit.cuts, cutTrackSegments, edit.sources, edit.fps, findings);
261
264
  for (const segment of findTrackOverlaps(cutTrackSegments)) {
262
265
  if (isDeclaredTransitionOverlap(edit.cuts, cutTrackSegments, segment, edit.fps)) continue;
263
266
  addFinding(findings, {
@@ -322,7 +325,11 @@ export async function lintProject(input, options = {}) {
322
325
  return writeResult(findings, skipped, inputs, paths, options);
323
326
  }
324
327
 
325
- 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);
326
333
  for (let position = 0; position < segments.length; position += 1) {
327
334
  const earlier = segments[position];
328
335
  const transition = cuts?.[earlier.index]?.transition_out;
@@ -335,13 +342,26 @@ function validateTransitionAdjacency(cuts, segments, fps, findings) {
335
342
  fps,
336
343
  );
337
344
  if (overlapFrames === 0) {
338
- addFinding(findings, {
339
- severity: "warning",
340
- check: "cuts.transition-out.zero-overlap",
341
- message: "トランジションを宣言していますが、次のクリップと重なっていないため効きません。前のクリップの終わりを延ばして重なりを作るか、トランジションを削除してください。",
342
- path: `edit.json#cuts[${earlier.index}].transition_out`,
343
- range: { start: earlier.end, end: later.start },
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),
344
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
+ }
345
365
  continue;
346
366
  }
347
367
  // 正の重なりは、宣言尺未満なら既存の短縮クランプ、宣言尺超なら track-overlap が担当する。
@@ -892,8 +912,8 @@ function validateTransitionOut(value, findings, path) {
892
912
  addFinding(findings, { severity: "error", check: "cuts.transition-out.structure", message: "transition_out must be an object", path });
893
913
  return;
894
914
  }
895
- if (!["dissolve", "fade-black", "fade-white", "reveal-down", "reveal-up"].includes(value.type)) {
896
- 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 });
897
917
  }
898
918
  if (!isPositiveNumber(value.duration)) {
899
919
  addFinding(findings, { severity: "error", check: "cuts.transition-out.duration", message: "duration must be a positive number", path });
@@ -1086,17 +1106,6 @@ function validateCutTransformFields(cuts, findings) {
1086
1106
  }
1087
1107
  }
1088
1108
 
1089
- // docs/contract-2026-08-12-still-image-cut-source-v0.md 裁定1: 判定は拡張子のみ。同じ集合
1090
- // (png/jpe?g/webp/bmp/gif, 大小無視) を packages/render-cut/src/layers.mjs の
1091
- // IMAGE_LAYER_SOURCE_PATTERN / plan.mjs の chroma_key 背景判定と揃える。edit-lint はスキーマ
1092
- // パッケージから独立しているため、ここでは同じパターンを別リテラルとして持つ(3面パリティ
1093
- // テストと同じ流儀: packages/preview-server/test/image-layer-source.test.mjs 参照)。
1094
- const IMAGE_CUT_SOURCE_PATTERN = /\.(png|jpe?g|webp|bmp|gif)$/iu;
1095
-
1096
- function isImageCutSourcePath(pathValue) {
1097
- return typeof pathValue === "string" && IMAGE_CUT_SOURCE_PATTERN.test(pathValue);
1098
- }
1099
-
1100
1109
  // 裁定3〜5(in/out 意味論・freeze/speed 警告・v0 空 cuts 拒否)。source が静止画のときだけ発火する。
1101
1110
  function validateStillImageCuts(edit, findings) {
1102
1111
  if (!isRecord(edit)) return;
@@ -1104,7 +1113,7 @@ function validateStillImageCuts(edit, findings) {
1104
1113
 
1105
1114
  const imageSourceIds = new Set(
1106
1115
  (Array.isArray(edit.sources) ? edit.sources : [])
1107
- .filter((source) => isRecord(source) && isImageCutSourcePath(source.path))
1116
+ .filter((source) => isRecord(source) && isStillImageSourcePath(source.path))
1108
1117
  .map((source) => source.id),
1109
1118
  );
1110
1119
  if (imageSourceIds.size === 0) return;
@@ -12,31 +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;
15
16
  export declare function effectiveCutFps(fps: number): number;
16
17
  /**
17
18
  * earlier の終端と later の開始を出力 fps へ量子化し、実重なりをフレーム数で返す。
18
19
  * 正数は重なり、0 は突き合わせ、負数はすき間を表す。
19
20
  */
20
21
  export declare function cutOverlapFrames(earlier: Pick<CutAdjacencyEarlierLike, 'tlEnd'>, later: CutAdjacencyLaterLike, fps?: number): number;
21
- export type TransitionHandleExtensionOutcome = 'already-overlapping' | 'full' | 'partial' | 'none';
22
- export interface TransitionHandleExtensionPlan {
23
- appliedSeconds: number;
24
- effectiveSeconds: number;
25
- appliedFrames: number;
26
- outcome: TransitionHandleExtensionOutcome;
27
- }
28
- export interface TransitionHandleExtensionInput {
22
+ export interface TransitionHandleWindowInput {
29
23
  declaredSeconds: number;
30
- earlierEndSeconds: number;
31
- laterStartSeconds: number;
32
- maxExtendSeconds: number;
33
- fps?: number;
24
+ /** outgoing の out より後ろに残る素材尺(出力秒)。不明なら Infinity。 */
25
+ outgoingTailRoomSeconds: number;
26
+ /** incoming の in より前に残る素材尺(出力秒)。 */
27
+ incomingHeadRoomSeconds: number;
28
+ outgoingDurationSeconds: number;
29
+ incomingDurationSeconds: number;
34
30
  }
35
- /**
36
- * 突き合わせ境界へ宣言尺ぶんの重なりを作るため、outgoing を何フレーム延ばすか決める。
37
- * メディア長は知らず、呼び出し側が maxExtendSeconds として渡した上限だけを使う。
38
- */
39
- export declare function planTransitionHandleExtension(input: TransitionHandleExtensionInput): TransitionHandleExtensionPlan;
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;
40
40
  /**
41
41
  * ギャップ 0、または earlier の宣言済み transitionOut.duration で説明できる重なりだけを
42
42
  * 隣接とみなす。秒の誤差ではなく、出力 fps で量子化したフレーム数を比較する。
@@ -4,61 +4,48 @@
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;
7
8
  exports.effectiveCutFps = effectiveCutFps;
8
9
  exports.cutOverlapFrames = cutOverlapFrames;
9
- exports.planTransitionHandleExtension = planTransitionHandleExtension;
10
+ exports.planTransitionHandleWindow = planTransitionHandleWindow;
11
+ exports.isStillImageSourcePath = isStillImageSourcePath;
10
12
  exports.areCutsAdjacent = areCutsAdjacent;
11
- const DEFAULT_CUT_ADJACENCY_FPS = 30;
13
+ exports.DEFAULT_CUT_ADJACENCY_FPS = 30;
12
14
  function effectiveCutFps(fps) {
13
- return Number.isFinite(fps) && fps > 0 ? fps : DEFAULT_CUT_ADJACENCY_FPS;
15
+ return Number.isFinite(fps) && fps > 0 ? fps : exports.DEFAULT_CUT_ADJACENCY_FPS;
14
16
  }
15
17
  /**
16
18
  * earlier の終端と later の開始を出力 fps へ量子化し、実重なりをフレーム数で返す。
17
19
  * 正数は重なり、0 は突き合わせ、負数はすき間を表す。
18
20
  */
19
- function cutOverlapFrames(earlier, later, fps = DEFAULT_CUT_ADJACENCY_FPS) {
21
+ function cutOverlapFrames(earlier, later, fps = exports.DEFAULT_CUT_ADJACENCY_FPS) {
20
22
  const resolvedFps = effectiveCutFps(fps);
21
23
  return Math.round(earlier.tlEnd * resolvedFps) - Math.round(later.tlStart * resolvedFps);
22
24
  }
23
- /**
24
- * 突き合わせ境界へ宣言尺ぶんの重なりを作るため、outgoing を何フレーム延ばすか決める。
25
- * メディア長は知らず、呼び出し側が maxExtendSeconds として渡した上限だけを使う。
26
- */
27
- function planTransitionHandleExtension(input) {
28
- const fps = effectiveCutFps(input.fps ?? DEFAULT_CUT_ADJACENCY_FPS);
29
- const declaredFrames = Number.isFinite(input.declaredSeconds) && input.declaredSeconds > 0
30
- ? Math.max(1, Math.round(input.declaredSeconds * fps)) : 0;
31
- const overlapFrames = cutOverlapFrames({ tlEnd: input.earlierEndSeconds }, { tlStart: input.laterStartSeconds }, fps);
32
- if (overlapFrames > 0) {
33
- return {
34
- appliedSeconds: 0,
35
- effectiveSeconds: Math.min(declaredFrames, overlapFrames) / fps,
36
- appliedFrames: 0,
37
- outcome: 'already-overlapping'
38
- };
39
- }
40
- const maximumFrames = input.maxExtendSeconds === Number.POSITIVE_INFINITY
41
- ? Number.POSITIVE_INFINITY
42
- : Number.isFinite(input.maxExtendSeconds) && input.maxExtendSeconds > 0
43
- ? Math.max(0, Math.floor(input.maxExtendSeconds * fps + 1e-9)) : 0;
44
- // すき間(負の overlapFrames)がある場合は、その穴を埋めたうえで宣言尺ぶん重ねる。
45
- // UI は非隣接ガードでこの経路へ入れないが、純関数としては安全な値を返す。
46
- const requiredFrames = Math.max(0, declaredFrames - overlapFrames);
47
- const appliedFrames = Math.min(requiredFrames, maximumFrames);
48
- const effectiveFrames = Math.max(0, Math.min(declaredFrames, overlapFrames + appliedFrames));
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)));
49
33
  return {
50
- appliedSeconds: appliedFrames / fps,
51
- effectiveSeconds: effectiveFrames / fps,
52
- appliedFrames,
53
- outcome: appliedFrames <= 0 ? 'none'
54
- : appliedFrames >= requiredFrames ? 'full' : 'partial'
34
+ effectiveSeconds,
35
+ halfSeconds: effectiveSeconds / 2,
36
+ outcome: effectiveSeconds <= 0 ? 'none'
37
+ : effectiveSeconds < declaredSeconds ? 'clamped' : 'full'
55
38
  };
56
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);
43
+ }
57
44
  /**
58
45
  * ギャップ 0、または earlier の宣言済み transitionOut.duration で説明できる重なりだけを
59
46
  * 隣接とみなす。秒の誤差ではなく、出力 fps で量子化したフレーム数を比較する。
60
47
  */
61
- function areCutsAdjacent(earlier, later, fps = DEFAULT_CUT_ADJACENCY_FPS) {
48
+ function areCutsAdjacent(earlier, later, fps = exports.DEFAULT_CUT_ADJACENCY_FPS) {
62
49
  const resolvedFps = effectiveCutFps(fps);
63
50
  const overlapFrames = cutOverlapFrames(earlier, later, resolvedFps);
64
51
  const declaredDuration = earlier.transitionOut?.duration;
@@ -1,4 +1,4 @@
1
- import { type TransitionHandleExtensionPlan } from './cut-adjacency';
1
+ import { type ReadableTransitionType, type TransitionType } from './transition-vocabulary';
2
2
  export interface EditCut {
3
3
  in: number;
4
4
  out: number;
@@ -12,7 +12,7 @@ export interface EditCut {
12
12
  opacity?: number;
13
13
  speed?: number;
14
14
  transitionOut?: {
15
- type: 'dissolve' | 'fade-black' | 'fade-white' | 'reveal-down' | 'reveal-up';
15
+ type: ReadableTransitionType;
16
16
  duration: number;
17
17
  };
18
18
  at?: number;
@@ -141,29 +141,19 @@ export declare function updateCutTransformInSource(source: string, cutIndex: num
141
141
  }): string;
142
142
  export declare function updateCutOpacityInSource(source: string, cutIndex: number, opacity: number | null): string;
143
143
  export declare function setCutTransitionOutInSource(source: string, cutIndex: number, transitionOut: {
144
- type: 'dissolve' | 'fade-black' | 'fade-white' | 'reveal-down' | 'reveal-up';
144
+ type: TransitionType;
145
145
  duration: number;
146
146
  } | null): string;
147
- export interface SetV2TransitionOutWithHandleInput {
147
+ export interface RemoveV2TransitionOutWithHandleRetractInput {
148
148
  itemId: string;
149
- transitionOut: {
150
- type: 'dissolve' | 'fade-black' | 'fade-white' | 'reveal-down' | 'reveal-up';
151
- duration: number;
152
- };
153
- earlierEndSeconds: number;
154
- laterStartSeconds: number;
155
- maxExtendSeconds: number;
149
+ retractFrames: number;
156
150
  fps: number;
157
151
  }
158
- export interface SetV2TransitionOutWithHandleResult {
159
- source: string;
160
- plan: TransitionHandleExtensionPlan;
161
- }
162
152
  /**
163
- * v2 の transition_out 宣言と outgoing の source.out / duration 延長を 1 回の
164
- * byte-preserving 手術で行う。maxExtendSeconds は出力秒軸の上限で、素材実尺は呼び出し側が決める。
153
+ * v0.1.20 の自動のりしろ debris を、transition_out の削除と同じ 1 回の
154
+ * byte-preserving 手術で回収する。新意味論の通常操作では trim を一切変更しない。
165
155
  */
166
- export declare function setV2TransitionOutWithHandleInSource(source: string, input: SetV2TransitionOutWithHandleInput): SetV2TransitionOutWithHandleResult;
156
+ export declare function removeV2TransitionOutWithHandleRetractInSource(source: string, input: RemoveV2TransitionOutWithHandleRetractInput): string;
167
157
  export declare function reorderCutsInSource(source: string, fromIndex: number, toIndex: number): string;
168
158
  export declare function splitCutInSource(source: string, cutIndex: number, atSeconds: number): string;
169
159
  export declare function deleteCutInSource(source: string, cutIndex: number): {
@@ -9,7 +9,7 @@ exports.setCutSpeedInSource = setCutSpeedInSource;
9
9
  exports.updateCutTransformInSource = updateCutTransformInSource;
10
10
  exports.updateCutOpacityInSource = updateCutOpacityInSource;
11
11
  exports.setCutTransitionOutInSource = setCutTransitionOutInSource;
12
- exports.setV2TransitionOutWithHandleInSource = setV2TransitionOutWithHandleInSource;
12
+ exports.removeV2TransitionOutWithHandleRetractInSource = removeV2TransitionOutWithHandleRetractInSource;
13
13
  exports.reorderCutsInSource = reorderCutsInSource;
14
14
  exports.splitCutInSource = splitCutInSource;
15
15
  exports.deleteCutInSource = deleteCutInSource;
@@ -38,7 +38,7 @@ exports.removeOverlayInSource = removeOverlayInSource;
38
38
  exports.writeTimelineTracksInSource = writeTimelineTracksInSource;
39
39
  exports.updateArrayElementByIndex = updateArrayElementByIndex;
40
40
  exports.updateOverlayVarInSource = updateOverlayVarInSource;
41
- const cut_adjacency_1 = require("./cut-adjacency");
41
+ const transition_vocabulary_1 = require("./transition-vocabulary");
42
42
  const JSON_NUMBER = '-?(?:0|[1-9]\\d*)(?:\\.\\d+)?(?:[eE][+-]?\\d+)?';
43
43
  const LAYER_BLEND_MODES = [
44
44
  'normal', 'screen', 'multiply', 'add', 'difference',
@@ -320,9 +320,7 @@ function updateCutOpacityInSource(source, cutIndex, opacity) {
320
320
  }
321
321
  function setCutTransitionOutInSource(source, cutIndex, transitionOut) {
322
322
  if (transitionOut !== null) {
323
- if (transitionOut.type !== 'dissolve' && transitionOut.type !== 'fade-black'
324
- && transitionOut.type !== 'fade-white' && transitionOut.type !== 'reveal-down'
325
- && transitionOut.type !== 'reveal-up') {
323
+ if (!(0, transition_vocabulary_1.isTransitionType)(transitionOut.type)) {
326
324
  throw new Error('トランジションの種別が不正です。');
327
325
  }
328
326
  if (!Number.isFinite(transitionOut.duration) || transitionOut.duration <= 0) {
@@ -351,18 +349,21 @@ function setCutTransitionOutInSource(source, cutIndex, transitionOut) {
351
349
  });
352
350
  }
353
351
  /**
354
- * v2 の transition_out 宣言と outgoing の source.out / duration 延長を 1 回の
355
- * byte-preserving 手術で行う。maxExtendSeconds は出力秒軸の上限で、素材実尺は呼び出し側が決める。
352
+ * v0.1.20 の自動のりしろ debris を、transition_out の削除と同じ 1 回の
353
+ * byte-preserving 手術で回収する。新意味論の通常操作では trim を一切変更しない。
356
354
  */
357
- function setV2TransitionOutWithHandleInSource(source, input) {
355
+ function removeV2TransitionOutWithHandleRetractInSource(source, input) {
358
356
  const raw = JSON.parse(source);
359
357
  if (raw.version !== 2) {
360
358
  throw new Error('v2 へ変換してから編集してください。');
361
359
  }
362
- assertTransitionOutValue(input.transitionOut);
363
360
  if (!input.itemId) {
364
361
  throw new Error('トランジション対象のアイテム id が空です。');
365
362
  }
363
+ if (!Number.isInteger(input.retractFrames) || input.retractFrames <= 0
364
+ || !Number.isFinite(input.fps) || input.fps <= 0) {
365
+ throw new Error('のりしろの復元量が不正です。');
366
+ }
366
367
  const tracks = locateArray(source, 'tracks');
367
368
  const trackElements = splitTopLevelElements(tracks.inner);
368
369
  const matches = [];
@@ -387,66 +388,31 @@ function setV2TransitionOutWithHandleInSource(source, input) {
387
388
  const match = matches[0];
388
389
  const label = `アイテム ${input.itemId}`;
389
390
  const durationFrames = readNumberProperty(match.item.text, 'duration', label);
391
+ if (!Number.isInteger(durationFrames) || durationFrames < input.retractFrames) {
392
+ throw new Error(`${label} の duration を安全に復元できません。`);
393
+ }
390
394
  const mediaSource = locateTopLevelObjectProperty(match.item.text, 'source');
391
395
  if (readStringProperty(mediaSource.text, 'kind') !== 'media') {
392
396
  throw new Error(`${label} は映像素材ではありません。`);
393
397
  }
394
398
  const sourceIn = readNumberProperty(mediaSource.text, 'in', label);
395
399
  const sourceOut = readNumberProperty(mediaSource.text, 'out', label);
396
- const outputDurationSeconds = durationFrames / input.fps;
397
- const speed = (sourceOut - sourceIn) / outputDurationSeconds;
398
- // 暗黙 speed を維持できない極端値は、素材窓を壊すより宣言だけを残す保守側へ倒す。
399
- // 実用域外(1e-6 未満 / 1e6 超)の比率も浮動小数の増分が不安定なので延長しない。
400
- const safeSpeed = Number.isFinite(speed) && speed >= 1e-6 && speed <= 1e6;
401
- const plan = (0, cut_adjacency_1.planTransitionHandleExtension)({
402
- declaredSeconds: input.transitionOut.duration,
403
- earlierEndSeconds: input.earlierEndSeconds,
404
- laterStartSeconds: input.laterStartSeconds,
405
- maxExtendSeconds: safeSpeed ? input.maxExtendSeconds : 0,
406
- fps: input.fps
407
- });
408
- let nextMediaSource = writeTransitionOutProperty(mediaSource.text, input.transitionOut);
409
- if (plan.appliedFrames > 0) {
410
- const nextOut = sourceOut + plan.appliedSeconds * speed;
411
- if (!Number.isFinite(nextOut)) {
412
- throw new Error(`${label} の out を安全に延長できません。`);
413
- }
414
- nextMediaSource = replaceNumberProperty(nextMediaSource, 'out', nextOut, 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 を安全に復元できません。`);
415
407
  }
408
+ let nextMediaSource = removeObjectProperty(mediaSource.text, 'transition_out');
409
+ nextMediaSource = replaceNumberProperty(nextMediaSource, 'out', nextOut, label);
416
410
  let nextItem = match.item.text.slice(0, mediaSource.start)
417
411
  + nextMediaSource
418
412
  + match.item.text.slice(mediaSource.end);
419
- if (plan.appliedFrames > 0) {
420
- nextItem = replaceNumberProperty(nextItem, 'duration', durationFrames + plan.appliedFrames, label);
421
- }
413
+ nextItem = replaceNumberProperty(nextItem, 'duration', durationFrames - input.retractFrames, label);
422
414
  const nextTrack = replaceElement(match.track.text, match.items.openIndex + 1, match.item, nextItem);
423
- return {
424
- source: replaceElement(source, tracks.openIndex + 1, match.track, nextTrack),
425
- plan
426
- };
427
- }
428
- function assertTransitionOutValue(transitionOut) {
429
- if (transitionOut.type !== 'dissolve' && transitionOut.type !== 'fade-black'
430
- && transitionOut.type !== 'fade-white' && transitionOut.type !== 'reveal-down'
431
- && transitionOut.type !== 'reveal-up') {
432
- throw new Error('トランジションの種別が不正です。');
433
- }
434
- if (!Number.isFinite(transitionOut.duration) || transitionOut.duration <= 0) {
435
- throw new Error('トランジションの尺は正の数で指定してください。');
436
- }
437
- }
438
- function writeTransitionOutProperty(mediaSource, transitionOut) {
439
- const value = { type: transitionOut.type, duration: transitionOut.duration };
440
- if (!hasTopLevelProperty(mediaSource, 'transition_out')) {
441
- return appendJsonProperty(mediaSource, 'transition_out', value);
442
- }
443
- try {
444
- const located = locateTopLevelObjectProperty(mediaSource, 'transition_out');
445
- return mediaSource.slice(0, located.start) + JSON.stringify(value) + mediaSource.slice(located.end);
446
- }
447
- catch {
448
- return appendJsonProperty(removeObjectProperty(mediaSource, 'transition_out'), 'transition_out', value);
449
- }
415
+ return replaceElement(source, tracks.openIndex + 1, match.track, nextTrack);
450
416
  }
451
417
  function reorderCutsInSource(source, fromIndex, toIndex) {
452
418
  const array = locateArray(source, 'cuts');
@@ -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.