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.
- package/package.json +1 -1
- package/vendor/.akari-capability-sources.json +1 -0
- package/vendor/docs/contract-2026-07-23-analysis-person-matte.md +102 -2
- package/vendor/packages/akari-launcher/package.json +1 -1
- package/vendor/packages/edit-lint/src/edit-lint.mjs +31 -22
- package/vendor/packages/edit-store/lib/cut-adjacency.d.ts +17 -17
- package/vendor/packages/edit-store/lib/cut-adjacency.js +23 -36
- package/vendor/packages/edit-store/lib/edit-store.d.ts +8 -18
- package/vendor/packages/edit-store/lib/edit-store.js +24 -58
- package/vendor/packages/edit-store/lib/index.d.ts +1 -0
- package/vendor/packages/edit-store/lib/index.js +1 -0
- package/vendor/packages/edit-store/lib/internal-model.js +62 -0
- package/vendor/packages/edit-store/lib/migrate/legacy-parse.js +9 -6
- package/vendor/packages/edit-store/lib/timeline-map.d.ts +5 -0
- package/vendor/packages/edit-store/lib/timeline-map.js +40 -0
- package/vendor/packages/edit-store/lib/transition-vocabulary.d.ts +223 -0
- package/vendor/packages/edit-store/lib/transition-vocabulary.js +45 -0
- package/vendor/packages/edit-store/lib/webview-kernel.js +98 -0
- package/vendor/packages/matte-rvm/package.json +22 -0
- package/vendor/packages/schemas/bin/validate-edit.mjs +5 -2
- package/vendor/packages/schemas/edit.schema.json +2 -2
- package/vendor/packages/schemas/test/transition-vocabulary.test.mjs +64 -0
- package/vendor/packages/schemas/test/validate-edit.test.mjs +2 -2
- package/vendor/skills/analyze-footage/bin/person-matte/person-cutout.mjs +453 -0
- package/vendor/skills/analyze-footage/bin/person-matte/person-matte.mjs +163 -48
- 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.
|
|
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.
|
|
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
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
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 (!
|
|
896
|
-
addFinding(findings, { severity: "error", check: "cuts.transition-out.type", message:
|
|
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) &&
|
|
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
|
|
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
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
24
|
+
/** outgoing の out より後ろに残る素材尺(出力秒)。不明なら Infinity。 */
|
|
25
|
+
outgoingTailRoomSeconds: number;
|
|
26
|
+
/** incoming の in より前に残る素材尺(出力秒)。 */
|
|
27
|
+
incomingHeadRoomSeconds: number;
|
|
28
|
+
outgoingDurationSeconds: number;
|
|
29
|
+
incomingDurationSeconds: number;
|
|
34
30
|
}
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
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.
|
|
10
|
+
exports.planTransitionHandleWindow = planTransitionHandleWindow;
|
|
11
|
+
exports.isStillImageSourcePath = isStillImageSourcePath;
|
|
10
12
|
exports.areCutsAdjacent = areCutsAdjacent;
|
|
11
|
-
|
|
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
|
-
|
|
25
|
-
|
|
26
|
-
*/
|
|
27
|
-
function
|
|
28
|
-
const
|
|
29
|
-
|
|
30
|
-
|
|
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
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
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
|
|
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:
|
|
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:
|
|
144
|
+
type: TransitionType;
|
|
145
145
|
duration: number;
|
|
146
146
|
} | null): string;
|
|
147
|
-
export interface
|
|
147
|
+
export interface RemoveV2TransitionOutWithHandleRetractInput {
|
|
148
148
|
itemId: string;
|
|
149
|
-
|
|
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
|
-
*
|
|
164
|
-
* byte-preserving
|
|
153
|
+
* v0.1.20 の自動のりしろ debris を、transition_out の削除と同じ 1 回の
|
|
154
|
+
* byte-preserving 手術で回収する。新意味論の通常操作では trim を一切変更しない。
|
|
165
155
|
*/
|
|
166
|
-
export declare function
|
|
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.
|
|
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
|
|
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 (
|
|
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
|
-
*
|
|
355
|
-
* byte-preserving
|
|
352
|
+
* v0.1.20 の自動のりしろ debris を、transition_out の削除と同じ 1 回の
|
|
353
|
+
* byte-preserving 手術で回収する。新意味論の通常操作では trim を一切変更しない。
|
|
356
354
|
*/
|
|
357
|
-
function
|
|
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
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
const
|
|
401
|
-
|
|
402
|
-
|
|
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
|
-
|
|
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.
|