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.
- 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 +90 -18
- package/vendor/packages/edit-store/lib/cut-adjacency.d.ts +25 -0
- package/vendor/packages/edit-store/lib/cut-adjacency.js +38 -8
- package/vendor/packages/edit-store/lib/edit-store.d.ts +13 -2
- package/vendor/packages/edit-store/lib/edit-store.js +69 -3
- 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.d.ts +13 -0
- package/vendor/packages/edit-store/lib/internal-model.js +98 -18
- 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
|
@@ -0,0 +1,453 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
// person-cutout.mjs — v2 edit.json の cut から人物マットと最前面 track を一括生成する。
|
|
4
|
+
//
|
|
5
|
+
// stdout は常に 1 行 JSON。重い判断を持たない決定論的 CLI とし、外部 npm 依存は使わない。
|
|
6
|
+
|
|
7
|
+
import fs from "node:fs";
|
|
8
|
+
import os from "node:os";
|
|
9
|
+
import path from "node:path";
|
|
10
|
+
import { spawnSync } from "node:child_process";
|
|
11
|
+
import { createRequire } from "node:module";
|
|
12
|
+
import { fileURLToPath } from "node:url";
|
|
13
|
+
|
|
14
|
+
import { resolveFfmpeg } from "../../../../packages/media-bin/src/index.mjs";
|
|
15
|
+
|
|
16
|
+
const require = createRequire(import.meta.url);
|
|
17
|
+
const { readEditV2 } = require("../../../../packages/edit-store/lib/index.js");
|
|
18
|
+
|
|
19
|
+
const scriptPath = fileURLToPath(import.meta.url);
|
|
20
|
+
const scriptDir = path.dirname(scriptPath);
|
|
21
|
+
const personMatteScript = path.join(scriptDir, "person-matte.mjs");
|
|
22
|
+
const validateEditScript = path.resolve(scriptDir, "../../../../packages/schemas/bin/validate-edit.mjs");
|
|
23
|
+
|
|
24
|
+
const QUALITIES = ["fast", "balanced", "accurate", "best"];
|
|
25
|
+
const MODELS = ["mobilenetv3", "resnet50"];
|
|
26
|
+
const DEFAULT_QUALITY = "balanced";
|
|
27
|
+
const PERSON_TRACK_ID = "person-cutout";
|
|
28
|
+
const MATTE_PATH_PATTERN = /^assets\/matte\/person-\d+\.webm$/u;
|
|
29
|
+
|
|
30
|
+
function printJson(value) {
|
|
31
|
+
process.stdout.write(`${JSON.stringify(value)}\n`);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function summarize(value, fallback) {
|
|
35
|
+
const text = String(value ?? "").replace(/\s+/gu, " ").trim();
|
|
36
|
+
return text ? text.slice(0, 1000) : fallback;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function parseCutIndices(value) {
|
|
40
|
+
if (typeof value !== "string" || value.length === 0) {
|
|
41
|
+
throw new Error("--cut には 0 始まりの index が必要です");
|
|
42
|
+
}
|
|
43
|
+
const parts = value.split(",");
|
|
44
|
+
if (parts.some((part) => !/^(0|[1-9]\d*)$/u.test(part))) {
|
|
45
|
+
throw new Error("--cut は 0 以上の整数(複数はカンマ区切り)です");
|
|
46
|
+
}
|
|
47
|
+
return [...new Set(parts.map(Number))];
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function parseArguments(argv) {
|
|
51
|
+
const options = {
|
|
52
|
+
project: null,
|
|
53
|
+
cuts: null,
|
|
54
|
+
quality: DEFAULT_QUALITY,
|
|
55
|
+
model: "mobilenetv3",
|
|
56
|
+
modelExplicit: false,
|
|
57
|
+
dryRun: false,
|
|
58
|
+
};
|
|
59
|
+
for (let index = 0; index < argv.length; index += 1) {
|
|
60
|
+
const argument = argv[index];
|
|
61
|
+
if (argument === "--dry-run") {
|
|
62
|
+
options.dryRun = true;
|
|
63
|
+
continue;
|
|
64
|
+
}
|
|
65
|
+
const value = argv[index + 1];
|
|
66
|
+
if (!value || value.startsWith("--")) throw new Error(`${argument} の値がありません`);
|
|
67
|
+
index += 1;
|
|
68
|
+
switch (argument) {
|
|
69
|
+
case "--project":
|
|
70
|
+
options.project = path.resolve(value);
|
|
71
|
+
break;
|
|
72
|
+
case "--cut":
|
|
73
|
+
options.cuts = parseCutIndices(value);
|
|
74
|
+
break;
|
|
75
|
+
case "--quality":
|
|
76
|
+
options.quality = value;
|
|
77
|
+
break;
|
|
78
|
+
case "--model":
|
|
79
|
+
options.model = value;
|
|
80
|
+
options.modelExplicit = true;
|
|
81
|
+
break;
|
|
82
|
+
default:
|
|
83
|
+
throw new Error(`不明な引数です: ${argument}`);
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
if (!options.project) throw new Error("--project <dir> が必要です");
|
|
87
|
+
if (!options.cuts) throw new Error("--cut <index> が必要です");
|
|
88
|
+
if (!QUALITIES.includes(options.quality)) {
|
|
89
|
+
throw new Error(`--quality は ${QUALITIES.join(" / ")} のいずれかです`);
|
|
90
|
+
}
|
|
91
|
+
if (!MODELS.includes(options.model)) {
|
|
92
|
+
throw new Error(`--model は ${MODELS.join(" / ")} のいずれかです`);
|
|
93
|
+
}
|
|
94
|
+
if (options.modelExplicit && options.quality !== "best") {
|
|
95
|
+
throw new Error("--model は --quality best のときだけ指定できます");
|
|
96
|
+
}
|
|
97
|
+
return options;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
function readProjectEdit(project) {
|
|
101
|
+
const editPath = path.join(project, "edit.json");
|
|
102
|
+
let source;
|
|
103
|
+
try {
|
|
104
|
+
source = fs.readFileSync(editPath, "utf8");
|
|
105
|
+
} catch (error) {
|
|
106
|
+
throw new Error(`edit.json を読めません: ${summarize(error?.message, editPath)}`);
|
|
107
|
+
}
|
|
108
|
+
let edit;
|
|
109
|
+
try {
|
|
110
|
+
edit = JSON.parse(source);
|
|
111
|
+
} catch (error) {
|
|
112
|
+
throw new Error(`edit.json が JSON ではありません: ${summarize(error?.message, "parse error")}`);
|
|
113
|
+
}
|
|
114
|
+
if (edit?.version !== 2) {
|
|
115
|
+
throw new Error(`edit.json v${String(edit?.version ?? "不明")} は未対応です。v2 へ migrate してから実行してください`);
|
|
116
|
+
}
|
|
117
|
+
try {
|
|
118
|
+
readEditV2(edit);
|
|
119
|
+
} catch (error) {
|
|
120
|
+
throw new Error(summarize(error?.message, "edit.json v2 が不正です"));
|
|
121
|
+
}
|
|
122
|
+
return { edit, editPath };
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
function normalizedRelative(value) {
|
|
126
|
+
return value.replaceAll(path.sep, "/");
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
function isGeneratedPersonItem(item, sourcePaths) {
|
|
130
|
+
if (/^person-\d+$/u.test(String(item?.id ?? ""))) return true;
|
|
131
|
+
if (item?.source?.kind !== "media") return false;
|
|
132
|
+
return MATTE_PATH_PATTERN.test(sourcePaths.get(item.source.src) ?? "");
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
/** v2 の visual media item を、tracks/items の宣言順で cut index へ射影する。 */
|
|
136
|
+
function collectCuts(edit) {
|
|
137
|
+
const sourcePaths = new Map(edit.sources.map((source) => [source.id, normalizedRelative(source.path)]));
|
|
138
|
+
const cuts = [];
|
|
139
|
+
for (const [trackIndex, track] of edit.tracks.entries()) {
|
|
140
|
+
if (track?.lane !== "visual" || !Array.isArray(track.items)) continue;
|
|
141
|
+
for (const [itemIndex, item] of track.items.entries()) {
|
|
142
|
+
if (item?.source?.kind !== "media" || isGeneratedPersonItem(item, sourcePaths)) continue;
|
|
143
|
+
cuts.push({ trackIndex, itemIndex, track, item });
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
return cuts;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
function effectiveSpeed(item, fps) {
|
|
150
|
+
if (item.source.speed !== undefined) return item.source.speed;
|
|
151
|
+
const span = item.source.out - item.source.in;
|
|
152
|
+
const normalFrames = Math.round(span * fps);
|
|
153
|
+
if (Math.abs(normalFrames - item.duration) <= 1) return 1;
|
|
154
|
+
return span / (item.duration / fps);
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
function resolveCutPlans(edit, indices, project) {
|
|
158
|
+
const fps = edit.output.fps;
|
|
159
|
+
const candidates = collectCuts(edit);
|
|
160
|
+
return indices.map((cutIndex) => {
|
|
161
|
+
const resolved = candidates[cutIndex];
|
|
162
|
+
if (!resolved) {
|
|
163
|
+
throw new Error(`cut index ${cutIndex} は範囲外です(対象 cut 数: ${candidates.length})`);
|
|
164
|
+
}
|
|
165
|
+
const sourceEntry = edit.sources.find((source) => source.id === resolved.item.source.src);
|
|
166
|
+
if (!sourceEntry) throw new Error(`cut ${cutIndex} の source が sources[] にありません`);
|
|
167
|
+
if (resolved.item.duration <= 0) {
|
|
168
|
+
throw new Error(`cut ${cutIndex} の duration は 1 フレーム以上である必要があります`);
|
|
169
|
+
}
|
|
170
|
+
const relativeMattePath = `assets/matte/person-${cutIndex}.webm`;
|
|
171
|
+
const durationSeconds = resolved.item.duration / fps;
|
|
172
|
+
return {
|
|
173
|
+
cut: cutIndex,
|
|
174
|
+
input: path.isAbsolute(sourceEntry.path) ? sourceEntry.path : path.resolve(project, sourceEntry.path),
|
|
175
|
+
source: sourceEntry.path,
|
|
176
|
+
in: resolved.item.source.in,
|
|
177
|
+
out: resolved.item.source.out,
|
|
178
|
+
speed: effectiveSpeed(resolved.item, fps),
|
|
179
|
+
atFrames: resolved.item.at,
|
|
180
|
+
durationFrames: resolved.item.duration,
|
|
181
|
+
t: resolved.item.at / fps,
|
|
182
|
+
duration: durationSeconds,
|
|
183
|
+
fps,
|
|
184
|
+
mattePath: relativeMattePath,
|
|
185
|
+
matteAbsolutePath: path.resolve(project, relativeMattePath),
|
|
186
|
+
sourceId: `person-cutout-${cutIndex}`,
|
|
187
|
+
itemId: `person-${cutIndex}`,
|
|
188
|
+
layer: {
|
|
189
|
+
id: `person-${cutIndex}`,
|
|
190
|
+
t: resolved.item.at / fps,
|
|
191
|
+
duration: durationSeconds,
|
|
192
|
+
kind: "video",
|
|
193
|
+
src: relativeMattePath,
|
|
194
|
+
},
|
|
195
|
+
item: {
|
|
196
|
+
id: `person-${cutIndex}`,
|
|
197
|
+
at: resolved.item.at,
|
|
198
|
+
duration: resolved.item.duration,
|
|
199
|
+
source: {
|
|
200
|
+
kind: "media",
|
|
201
|
+
src: `person-cutout-${cutIndex}`,
|
|
202
|
+
in: 0,
|
|
203
|
+
out: durationSeconds,
|
|
204
|
+
},
|
|
205
|
+
},
|
|
206
|
+
};
|
|
207
|
+
});
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
function buildPatchedEdit(edit, plans) {
|
|
211
|
+
const next = structuredClone(edit);
|
|
212
|
+
const beforeTracks = JSON.stringify(next.tracks);
|
|
213
|
+
const actions = [];
|
|
214
|
+
|
|
215
|
+
for (const plan of plans) {
|
|
216
|
+
const sourceIndex = next.sources.findIndex((source) => source.id === plan.sourceId
|
|
217
|
+
|| normalizedRelative(source.path) === plan.mattePath);
|
|
218
|
+
const source = { id: plan.sourceId, path: plan.mattePath };
|
|
219
|
+
if (sourceIndex >= 0) next.sources[sourceIndex] = source;
|
|
220
|
+
else next.sources.push(source);
|
|
221
|
+
|
|
222
|
+
let existed = false;
|
|
223
|
+
for (const track of next.tracks) {
|
|
224
|
+
if (!Array.isArray(track.items)) continue;
|
|
225
|
+
const retained = [];
|
|
226
|
+
for (const item of track.items) {
|
|
227
|
+
if (item.id === plan.itemId) existed = true;
|
|
228
|
+
else retained.push(item);
|
|
229
|
+
}
|
|
230
|
+
track.items = retained;
|
|
231
|
+
}
|
|
232
|
+
actions.push({ cut: plan.cut, action: existed ? "updated" : "added", ...plan.layer });
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
const personIndex = next.tracks.findIndex((track) => track.id === PERSON_TRACK_ID);
|
|
236
|
+
const personTrack = personIndex >= 0
|
|
237
|
+
? next.tracks.splice(personIndex, 1)[0]
|
|
238
|
+
: { id: PERSON_TRACK_ID, lane: "visual", name: "人物切り抜き", items: [] };
|
|
239
|
+
if (personTrack.lane !== "visual" || !Array.isArray(personTrack.items)) {
|
|
240
|
+
throw new Error(`track id ${PERSON_TRACK_ID} は人物 visual track として使用できません`);
|
|
241
|
+
}
|
|
242
|
+
const planIds = new Set(plans.map((plan) => plan.itemId));
|
|
243
|
+
personTrack.items = personTrack.items.filter((item) => !planIds.has(item.id));
|
|
244
|
+
personTrack.items.push(...plans.map((plan) => plan.item));
|
|
245
|
+
personTrack.items.sort((left, right) => left.at - right.at || left.id.localeCompare(right.id));
|
|
246
|
+
// v2 は tracks 配列順が下→上の唯一の z 順。既存相互順を保ち、人物 track だけ最前面へ置く。
|
|
247
|
+
next.tracks.push(personTrack);
|
|
248
|
+
|
|
249
|
+
return {
|
|
250
|
+
edit: next,
|
|
251
|
+
actions,
|
|
252
|
+
tracksChanged: beforeTracks !== JSON.stringify(next.tracks),
|
|
253
|
+
trackOrder: next.tracks.map((track) => track.id),
|
|
254
|
+
tracks: next.tracks.map((track) => ({
|
|
255
|
+
id: track.id,
|
|
256
|
+
lane: track.lane,
|
|
257
|
+
...(track.name !== undefined ? { name: track.name } : {}),
|
|
258
|
+
})),
|
|
259
|
+
};
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
function runChecked(command, args, label, options = {}) {
|
|
263
|
+
const result = spawnSync(command, args, { encoding: "utf8", maxBuffer: 16 * 1024 * 1024, ...options });
|
|
264
|
+
if (result.error || result.status !== 0) {
|
|
265
|
+
throw new Error(`${label} に失敗しました: ${summarize(result.stderr || result.stdout || result.error?.message, `exit ${result.status}`)}`);
|
|
266
|
+
}
|
|
267
|
+
return result;
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
function buildExtractionArgs(plan, temporaryPath) {
|
|
271
|
+
const sourceDuration = plan.out - plan.in;
|
|
272
|
+
const filter = [
|
|
273
|
+
`setpts=(PTS-STARTPTS)/${plan.speed}`,
|
|
274
|
+
`fps=${plan.fps}`,
|
|
275
|
+
`trim=duration=${plan.duration}`,
|
|
276
|
+
"setpts=PTS-STARTPTS",
|
|
277
|
+
].join(",");
|
|
278
|
+
return [
|
|
279
|
+
"-hide_banner", "-nostdin", "-loglevel", "error",
|
|
280
|
+
"-ss", String(plan.in), "-t", String(sourceDuration), "-i", plan.input,
|
|
281
|
+
"-map", "0:v:0", "-vf", filter, "-frames:v", String(plan.durationFrames),
|
|
282
|
+
"-an", "-c:v", "ffv1", "-level", "3", "-pix_fmt", "bgra", "-y", temporaryPath,
|
|
283
|
+
];
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
function extractCut(plan, temporaryPath, ffmpegCommand = resolveFfmpeg()) {
|
|
287
|
+
if (!fs.statSync(plan.input).isFile()) throw new Error(`素材が通常ファイルではありません: ${plan.input}`);
|
|
288
|
+
runChecked(
|
|
289
|
+
ffmpegCommand,
|
|
290
|
+
buildExtractionArgs(plan, temporaryPath),
|
|
291
|
+
`cut ${plan.cut} の速度適用済み切り出し`,
|
|
292
|
+
);
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
function generateMatte(plan, options, temporaryPath) {
|
|
296
|
+
extractCut(plan, temporaryPath);
|
|
297
|
+
fs.mkdirSync(path.dirname(plan.matteAbsolutePath), { recursive: true });
|
|
298
|
+
const args = [
|
|
299
|
+
personMatteScript,
|
|
300
|
+
"--input", temporaryPath,
|
|
301
|
+
"--out", plan.matteAbsolutePath,
|
|
302
|
+
"--quality", options.quality,
|
|
303
|
+
"--fps", String(plan.fps),
|
|
304
|
+
];
|
|
305
|
+
if (options.modelExplicit) args.push("--model", options.model);
|
|
306
|
+
const result = runChecked(process.execPath, args, `cut ${plan.cut} の人物マット生成`);
|
|
307
|
+
let measured;
|
|
308
|
+
try {
|
|
309
|
+
measured = JSON.parse(result.stdout);
|
|
310
|
+
} catch {
|
|
311
|
+
throw new Error(`person-matte の stdout が JSON ではありません: ${summarize(result.stdout, "empty")}`);
|
|
312
|
+
}
|
|
313
|
+
if (measured.ok !== true) throw new Error(measured.reason ?? "person-matte が ok:true を返しませんでした");
|
|
314
|
+
return {
|
|
315
|
+
cut: plan.cut,
|
|
316
|
+
path: plan.mattePath,
|
|
317
|
+
frames: measured.frames,
|
|
318
|
+
bytes: measured.bytes,
|
|
319
|
+
width: measured.width,
|
|
320
|
+
height: measured.height,
|
|
321
|
+
elapsed_seconds: measured.elapsed_seconds,
|
|
322
|
+
realtime_ratio: measured.realtime_ratio,
|
|
323
|
+
...(measured.vision_ms_per_frame !== undefined
|
|
324
|
+
? { vision_ms_per_frame: measured.vision_ms_per_frame }
|
|
325
|
+
: {}),
|
|
326
|
+
...(measured.rvm_ms_per_frame !== undefined ? { rvm_ms_per_frame: measured.rvm_ms_per_frame } : {}),
|
|
327
|
+
engine: measured.engine,
|
|
328
|
+
...(measured.model !== undefined ? { model: measured.model } : {}),
|
|
329
|
+
quality: measured.person_matte?.quality ?? options.quality,
|
|
330
|
+
mask_size: measured.mask_size,
|
|
331
|
+
alpha_transparent_ratio: measured.alpha_transparent_ratio,
|
|
332
|
+
alpha_partial_ratio: measured.alpha_partial_ratio,
|
|
333
|
+
probe: measured.probe,
|
|
334
|
+
};
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
function validateAndWrite(editPath, edit) {
|
|
338
|
+
try {
|
|
339
|
+
readEditV2(edit);
|
|
340
|
+
} catch (error) {
|
|
341
|
+
return { ok: false, reason: summarize(error?.message, "v2 reader validation failed") };
|
|
342
|
+
}
|
|
343
|
+
const temporaryPath = path.join(path.dirname(editPath), `.edit.json.person-cutout-${process.pid}-${Date.now()}.tmp`);
|
|
344
|
+
try {
|
|
345
|
+
fs.writeFileSync(temporaryPath, `${JSON.stringify(edit, null, 2)}\n`, { encoding: "utf8", flag: "wx" });
|
|
346
|
+
const result = spawnSync(process.execPath, [validateEditScript, temporaryPath], {
|
|
347
|
+
encoding: "utf8",
|
|
348
|
+
maxBuffer: 4 * 1024 * 1024,
|
|
349
|
+
});
|
|
350
|
+
if (result.error || result.status !== 0) {
|
|
351
|
+
return {
|
|
352
|
+
ok: false,
|
|
353
|
+
exit_code: result.status,
|
|
354
|
+
reason: summarize(result.stderr || result.stdout || result.error?.message, "validate-edit failed"),
|
|
355
|
+
};
|
|
356
|
+
}
|
|
357
|
+
fs.renameSync(temporaryPath, editPath);
|
|
358
|
+
return { ok: true, exit_code: 0, output: summarize(result.stdout, "OK") };
|
|
359
|
+
} finally {
|
|
360
|
+
fs.rmSync(temporaryPath, { force: true });
|
|
361
|
+
}
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
async function execute(options) {
|
|
365
|
+
const { edit, editPath } = readProjectEdit(options.project);
|
|
366
|
+
const plans = resolveCutPlans(edit, options.cuts, options.project);
|
|
367
|
+
const patched = buildPatchedEdit(edit, plans);
|
|
368
|
+
if (options.dryRun) {
|
|
369
|
+
return {
|
|
370
|
+
ok: true,
|
|
371
|
+
dry_run: true,
|
|
372
|
+
mattes: plans.map((plan) => ({
|
|
373
|
+
cut: plan.cut,
|
|
374
|
+
path: plan.mattePath,
|
|
375
|
+
source: plan.source,
|
|
376
|
+
in: plan.in,
|
|
377
|
+
out: plan.out,
|
|
378
|
+
speed: plan.speed,
|
|
379
|
+
fps: plan.fps,
|
|
380
|
+
})),
|
|
381
|
+
layers: patched.actions,
|
|
382
|
+
tracks_changed: patched.tracksChanged,
|
|
383
|
+
tracks: patched.tracks,
|
|
384
|
+
track_order: patched.trackOrder,
|
|
385
|
+
validate: { ok: true, skipped: true },
|
|
386
|
+
};
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
const temporaryDirectory = fs.mkdtempSync(path.join(os.tmpdir(), "akari-person-cutout-"));
|
|
390
|
+
const mattes = [];
|
|
391
|
+
try {
|
|
392
|
+
for (const plan of plans) {
|
|
393
|
+
const temporaryPath = path.join(temporaryDirectory, `cut-${plan.cut}.mkv`);
|
|
394
|
+
mattes.push(generateMatte(plan, options, temporaryPath));
|
|
395
|
+
}
|
|
396
|
+
} finally {
|
|
397
|
+
fs.rmSync(temporaryDirectory, { recursive: true, force: true });
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
const validate = validateAndWrite(editPath, patched.edit);
|
|
401
|
+
if (!validate.ok) throw new Error(`パッチ後の edit.json が不合格です(元ファイルは未変更): ${validate.reason}`);
|
|
402
|
+
return {
|
|
403
|
+
ok: true,
|
|
404
|
+
dry_run: false,
|
|
405
|
+
mattes,
|
|
406
|
+
layers: patched.actions,
|
|
407
|
+
tracks_changed: patched.tracksChanged,
|
|
408
|
+
tracks: patched.tracks,
|
|
409
|
+
track_order: patched.trackOrder,
|
|
410
|
+
validate,
|
|
411
|
+
};
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
async function main() {
|
|
415
|
+
let options;
|
|
416
|
+
try {
|
|
417
|
+
options = parseArguments(process.argv.slice(2));
|
|
418
|
+
} catch (error) {
|
|
419
|
+
printJson({ ok: false, reason: summarize(error?.message, "引数が不正です") });
|
|
420
|
+
process.exitCode = 2;
|
|
421
|
+
return;
|
|
422
|
+
}
|
|
423
|
+
try {
|
|
424
|
+
printJson(await execute(options));
|
|
425
|
+
} catch (error) {
|
|
426
|
+
printJson({ ok: false, reason: summarize(error?.message, "人物切り抜きの自動配線に失敗しました") });
|
|
427
|
+
process.exitCode = 1;
|
|
428
|
+
}
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
function isMainModule() {
|
|
432
|
+
if (!process.argv[1]) return false;
|
|
433
|
+
try {
|
|
434
|
+
return fs.realpathSync(scriptPath) === fs.realpathSync(path.resolve(process.argv[1]));
|
|
435
|
+
} catch {
|
|
436
|
+
return false;
|
|
437
|
+
}
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
if (isMainModule()) await main();
|
|
441
|
+
|
|
442
|
+
export {
|
|
443
|
+
DEFAULT_QUALITY,
|
|
444
|
+
PERSON_TRACK_ID,
|
|
445
|
+
buildExtractionArgs,
|
|
446
|
+
buildPatchedEdit,
|
|
447
|
+
collectCuts,
|
|
448
|
+
execute,
|
|
449
|
+
parseArguments,
|
|
450
|
+
parseCutIndices,
|
|
451
|
+
resolveCutPlans,
|
|
452
|
+
validateAndWrite,
|
|
453
|
+
};
|