akari-video 0.1.20 → 0.1.21
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- 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
|
@@ -13,7 +13,8 @@
|
|
|
13
13
|
// 使い方:
|
|
14
14
|
// node person-matte.mjs --check
|
|
15
15
|
// node person-matte.mjs --input <video> --out <matte.webm>
|
|
16
|
-
// [--quality fast|balanced|accurate]
|
|
16
|
+
// [--quality fast|balanced|accurate|best]
|
|
17
|
+
// [--model mobilenetv3|resnet50] [--fps 24] [--decode-width 1280]
|
|
17
18
|
//
|
|
18
19
|
// 出力は stdout の 1 行 JSON。成功時 `ok: true` と実測値、失敗時 `ok: false` と `reason`。
|
|
19
20
|
// 外部 npm 依存ゼロ・ネットワーク禁止(edit-lint と同じ規律)。
|
|
@@ -25,18 +26,25 @@ import { spawn, spawnSync } from "node:child_process";
|
|
|
25
26
|
import { fileURLToPath } from "node:url";
|
|
26
27
|
|
|
27
28
|
import { findAlphaModeTag } from "./alpha-tag.mjs";
|
|
29
|
+
import { DEFAULT_RVM_MODEL, resolveRvmModel } from "../../../../packages/matte-rvm/src/index.mjs";
|
|
30
|
+
import { resolveFfmpeg, resolveFfprobe } from "../../../../packages/media-bin/src/index.mjs";
|
|
28
31
|
|
|
29
32
|
const scriptDir = path.dirname(fileURLToPath(import.meta.url));
|
|
30
33
|
const helperSource = path.join(scriptDir, "person-matte-helper.swift");
|
|
31
34
|
const defaultHelperBin = path.join(scriptDir, "person-matte-helper");
|
|
35
|
+
const rvmHelper = fileURLToPath(
|
|
36
|
+
new URL("../../../../packages/matte-rvm/bin/rvm-helper.mjs", import.meta.url),
|
|
37
|
+
);
|
|
32
38
|
|
|
33
39
|
// スパイク実測(2026-07-23)で確定した既定値。入力解像度は Vision の速度にほぼ無関係
|
|
34
40
|
// (quality ごとに内部で固定リサイズされる)ため、デコード幅は 1280 で統一する。
|
|
35
41
|
const DEFAULT_DECODE_WIDTH = 1280;
|
|
36
42
|
const DEFAULT_FPS = 24;
|
|
37
43
|
const DEFAULT_QUALITY = "balanced";
|
|
38
|
-
const QUALITIES = ["fast", "balanced", "accurate"];
|
|
44
|
+
const QUALITIES = ["fast", "balanced", "accurate", "best"];
|
|
45
|
+
const RVM_MODELS = ["mobilenetv3", "resnet50"];
|
|
39
46
|
const TOOL_ID = "vision-person-segmentation";
|
|
47
|
+
const RVM_TOOL_ID = "rvm-person-matting";
|
|
40
48
|
|
|
41
49
|
function printJson(value) {
|
|
42
50
|
process.stdout.write(`${JSON.stringify(value)}\n`);
|
|
@@ -55,31 +63,52 @@ function spawnSyncSafe(command, args) {
|
|
|
55
63
|
}
|
|
56
64
|
}
|
|
57
65
|
|
|
58
|
-
function checkAvailability(
|
|
59
|
-
|
|
60
|
-
|
|
66
|
+
function checkAvailability({
|
|
67
|
+
platform = os.platform(),
|
|
68
|
+
runSync = spawnSyncSafe,
|
|
69
|
+
resolveFfmpegBin = resolveFfmpeg,
|
|
70
|
+
resolveFfprobeBin = resolveFfprobe,
|
|
71
|
+
resolveModel = resolveRvmModel,
|
|
72
|
+
} = {}) {
|
|
73
|
+
if (platform !== "darwin" && platform !== "win32") {
|
|
74
|
+
return { available: false, reason: `未対応のプラットフォームです(${platform})` };
|
|
61
75
|
}
|
|
62
76
|
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
77
|
+
if (platform === "darwin") {
|
|
78
|
+
const swift = runSync("swiftc", ["-version"]);
|
|
79
|
+
if (swift.error?.code === "ENOENT") {
|
|
80
|
+
return { available: false, reason: "swiftc が PATH 上にありません" };
|
|
81
|
+
}
|
|
82
|
+
if (swift.error || swift.status !== 0) {
|
|
83
|
+
return { available: false, reason: "swiftc を起動できません" };
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
let ffmpegBin;
|
|
88
|
+
let ffprobeBin;
|
|
89
|
+
try {
|
|
90
|
+
ffmpegBin = resolveFfmpegBin();
|
|
91
|
+
} catch (error) {
|
|
92
|
+
return { available: false, reason: summarize(error?.message, "ffmpeg が見つかりません") };
|
|
66
93
|
}
|
|
67
|
-
|
|
68
|
-
|
|
94
|
+
try {
|
|
95
|
+
ffprobeBin = resolveFfprobeBin();
|
|
96
|
+
} catch (error) {
|
|
97
|
+
return { available: false, reason: summarize(error?.message, "ffprobe が見つかりません") };
|
|
69
98
|
}
|
|
70
99
|
|
|
71
|
-
for (const command of ["ffmpeg", "ffprobe"]) {
|
|
72
|
-
const probe =
|
|
100
|
+
for (const [label, command] of [["ffmpeg", ffmpegBin], ["ffprobe", ffprobeBin]]) {
|
|
101
|
+
const probe = runSync(command, ["-version"]);
|
|
73
102
|
if (probe.error?.code === "ENOENT") {
|
|
74
|
-
return { available: false, reason: `${
|
|
103
|
+
return { available: false, reason: `${label} が見つかりません` };
|
|
75
104
|
}
|
|
76
105
|
if (probe.error || probe.status !== 0) {
|
|
77
|
-
return { available: false, reason: `${
|
|
106
|
+
return { available: false, reason: `${label} を起動できません` };
|
|
78
107
|
}
|
|
79
108
|
}
|
|
80
109
|
|
|
81
110
|
// VP9 alpha WebM を書けるかは libvpx-vp9 の有無で決まる(ffmpeg 内蔵の vp9 エンコーダは別物)。
|
|
82
|
-
const encoders =
|
|
111
|
+
const encoders = runSync(ffmpegBin, ["-hide_banner", "-encoders"]);
|
|
83
112
|
if (encoders.error || encoders.status !== 0) {
|
|
84
113
|
return { available: false, reason: "ffmpeg -encoders を取得できません" };
|
|
85
114
|
}
|
|
@@ -87,6 +116,16 @@ function checkAvailability() {
|
|
|
87
116
|
return { available: false, reason: "ffmpeg に libvpx-vp9 エンコーダがありません" };
|
|
88
117
|
}
|
|
89
118
|
|
|
119
|
+
if (platform === "win32") {
|
|
120
|
+
const model = resolveModel(DEFAULT_RVM_MODEL);
|
|
121
|
+
if (model.missing) {
|
|
122
|
+
return {
|
|
123
|
+
available: false,
|
|
124
|
+
reason: `RVM model is not installed: ${model.path}. ${model.fetchHint}`,
|
|
125
|
+
};
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
|
|
90
129
|
return { available: true };
|
|
91
130
|
}
|
|
92
131
|
|
|
@@ -99,6 +138,8 @@ function parseArguments(argv) {
|
|
|
99
138
|
fps: DEFAULT_FPS,
|
|
100
139
|
decodeWidth: DEFAULT_DECODE_WIDTH,
|
|
101
140
|
helperBin: defaultHelperBin,
|
|
141
|
+
model: DEFAULT_RVM_MODEL,
|
|
142
|
+
modelExplicit: false,
|
|
102
143
|
};
|
|
103
144
|
for (let index = 0; index < argv.length; index += 1) {
|
|
104
145
|
const argument = argv[index];
|
|
@@ -122,6 +163,10 @@ function parseArguments(argv) {
|
|
|
122
163
|
case "--quality":
|
|
123
164
|
result.quality = value;
|
|
124
165
|
break;
|
|
166
|
+
case "--model":
|
|
167
|
+
result.model = value;
|
|
168
|
+
result.modelExplicit = true;
|
|
169
|
+
break;
|
|
125
170
|
case "--fps":
|
|
126
171
|
result.fps = Number(value);
|
|
127
172
|
break;
|
|
@@ -135,6 +180,12 @@ function parseArguments(argv) {
|
|
|
135
180
|
if (!QUALITIES.includes(result.quality)) {
|
|
136
181
|
throw new Error(`--quality は ${QUALITIES.join(" / ")} のいずれかです`);
|
|
137
182
|
}
|
|
183
|
+
if (!RVM_MODELS.includes(result.model)) {
|
|
184
|
+
throw new Error(`--model は ${RVM_MODELS.join(" / ")} のいずれかです`);
|
|
185
|
+
}
|
|
186
|
+
if (result.modelExplicit && result.quality !== "best") {
|
|
187
|
+
throw new Error("--model は --quality best のときだけ指定できます");
|
|
188
|
+
}
|
|
138
189
|
if (!Number.isFinite(result.fps) || result.fps <= 0 || result.fps > 240) {
|
|
139
190
|
throw new Error("--fps は 0 より大きく 240 以下の数値です");
|
|
140
191
|
}
|
|
@@ -166,8 +217,31 @@ function buildHelper(helperBin) {
|
|
|
166
217
|
return null;
|
|
167
218
|
}
|
|
168
219
|
|
|
169
|
-
function
|
|
170
|
-
|
|
220
|
+
function engineForPlatform(quality, platform = os.platform()) {
|
|
221
|
+
return platform === "win32" || quality === "best" ? "rvm" : "vision";
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
function prepareExecution(options, {
|
|
225
|
+
platform = os.platform(),
|
|
226
|
+
resolveModel = resolveRvmModel,
|
|
227
|
+
buildVisionHelper = buildHelper,
|
|
228
|
+
} = {}) {
|
|
229
|
+
const engine = engineForPlatform(options.quality, platform);
|
|
230
|
+
if (engine === "rvm") {
|
|
231
|
+
const resolved = resolveModel(options.model);
|
|
232
|
+
if (resolved.missing) {
|
|
233
|
+
throw new Error(`RVM model is not installed: ${resolved.path}. ${resolved.fetchHint}`);
|
|
234
|
+
}
|
|
235
|
+
return { ...options, engine, modelPath: resolved.path };
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
const buildError = buildVisionHelper(options.helperBin);
|
|
239
|
+
if (buildError) throw new Error(`swiftc build failed: ${buildError}`);
|
|
240
|
+
return { ...options, engine };
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
function ffprobeJson(args, ffprobeBin = resolveFfprobe()) {
|
|
244
|
+
const result = spawnSyncSafe(ffprobeBin, ["-v", "error", ...args, "-of", "json"]);
|
|
171
245
|
if (result.error || result.status !== 0) {
|
|
172
246
|
throw new Error(summarize(result.stderr, "ffprobe に失敗しました"));
|
|
173
247
|
}
|
|
@@ -179,14 +253,14 @@ function ffprobeJson(args) {
|
|
|
179
253
|
}
|
|
180
254
|
|
|
181
255
|
/// 素材の表示上の寸法(回転を反映した幅・高さ)と尺を得る。
|
|
182
|
-
function probeSource(input) {
|
|
256
|
+
function probeSource(input, ffprobeBin) {
|
|
183
257
|
const probed = ffprobeJson([
|
|
184
258
|
"-select_streams",
|
|
185
259
|
"v:0",
|
|
186
260
|
"-show_entries",
|
|
187
261
|
"stream=width,height,r_frame_rate,duration:stream_side_data=rotation:format=duration",
|
|
188
262
|
input,
|
|
189
|
-
]);
|
|
263
|
+
], ffprobeBin);
|
|
190
264
|
const stream = probed?.streams?.[0];
|
|
191
265
|
if (!stream) throw new Error("映像ストリームが見つかりません");
|
|
192
266
|
const width = Number(stream.width);
|
|
@@ -230,7 +304,9 @@ function stage(name, command, args, stdio) {
|
|
|
230
304
|
}
|
|
231
305
|
|
|
232
306
|
async function generate(options) {
|
|
233
|
-
const
|
|
307
|
+
const ffmpegBin = resolveFfmpeg();
|
|
308
|
+
const ffprobeBin = resolveFfprobe();
|
|
309
|
+
const source = probeSource(options.input, ffprobeBin);
|
|
234
310
|
const size = outputSize(source, options.decodeWidth);
|
|
235
311
|
const metricsPath = path.join(
|
|
236
312
|
os.tmpdir(),
|
|
@@ -240,7 +316,7 @@ async function generate(options) {
|
|
|
240
316
|
|
|
241
317
|
const decoder = stage(
|
|
242
318
|
"decode",
|
|
243
|
-
|
|
319
|
+
ffmpegBin,
|
|
244
320
|
[
|
|
245
321
|
"-hide_banner", "-nostdin", "-loglevel", "error",
|
|
246
322
|
"-i", options.input,
|
|
@@ -251,20 +327,34 @@ async function generate(options) {
|
|
|
251
327
|
],
|
|
252
328
|
["ignore", "pipe", "pipe"],
|
|
253
329
|
);
|
|
254
|
-
const helper =
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
330
|
+
const helper = options.engine === "rvm"
|
|
331
|
+
? stage(
|
|
332
|
+
"segment",
|
|
333
|
+
process.execPath,
|
|
334
|
+
[
|
|
335
|
+
rvmHelper,
|
|
336
|
+
"--width", String(size.width),
|
|
337
|
+
"--height", String(size.height),
|
|
338
|
+
"--model", options.modelPath,
|
|
339
|
+
"--metrics", metricsPath,
|
|
340
|
+
"--total-frames", String(source.duration ? Math.round(source.duration * options.fps) : 0),
|
|
341
|
+
],
|
|
342
|
+
["pipe", "pipe", "pipe"],
|
|
343
|
+
)
|
|
344
|
+
: stage(
|
|
345
|
+
"segment",
|
|
346
|
+
options.helperBin,
|
|
347
|
+
[
|
|
348
|
+
"--width", String(size.width),
|
|
349
|
+
"--height", String(size.height),
|
|
350
|
+
"--quality", options.quality,
|
|
351
|
+
"--metrics", metricsPath,
|
|
352
|
+
],
|
|
353
|
+
["pipe", "pipe", "pipe"],
|
|
354
|
+
);
|
|
265
355
|
const encoder = stage(
|
|
266
356
|
"encode",
|
|
267
|
-
|
|
357
|
+
ffmpegBin,
|
|
268
358
|
[
|
|
269
359
|
"-hide_banner", "-loglevel", "error",
|
|
270
360
|
"-f", "rawvideo", "-pix_fmt", "bgra",
|
|
@@ -289,6 +379,9 @@ async function generate(options) {
|
|
|
289
379
|
}
|
|
290
380
|
decoder.child.stdout?.pipe(helper.child.stdin);
|
|
291
381
|
helper.child.stdout?.pipe(encoder.child.stdin);
|
|
382
|
+
if (options.engine === "rvm") {
|
|
383
|
+
helper.child.stderr?.on("data", (chunk) => process.stderr.write(chunk));
|
|
384
|
+
}
|
|
292
385
|
|
|
293
386
|
const startedAt = Date.now();
|
|
294
387
|
const results = await Promise.all([decoder.exited, helper.exited, encoder.exited]);
|
|
@@ -317,17 +410,20 @@ async function generate(options) {
|
|
|
317
410
|
throw new Error("フレームを 1 枚も処理していません");
|
|
318
411
|
}
|
|
319
412
|
|
|
320
|
-
const verified = verifyOutput(options.out);
|
|
413
|
+
const verified = verifyOutput(options.out, ffprobeBin);
|
|
414
|
+
const engine = options.engine;
|
|
321
415
|
return {
|
|
322
416
|
ok: true,
|
|
323
417
|
path: options.out,
|
|
418
|
+
engine,
|
|
419
|
+
...(engine === "rvm" ? { model: options.model } : {}),
|
|
324
420
|
// analysis.json へ貼る形。path は analysis.json のディレクトリ基準へ書き換えること。
|
|
325
421
|
person_matte: {
|
|
326
422
|
path: options.out,
|
|
327
423
|
fps: options.fps,
|
|
328
424
|
quality: options.quality,
|
|
329
425
|
generated_at: new Date().toISOString(),
|
|
330
|
-
tool: TOOL_ID,
|
|
426
|
+
tool: engine === "rvm" ? RVM_TOOL_ID : TOOL_ID,
|
|
331
427
|
},
|
|
332
428
|
source: { path: options.input, ...source },
|
|
333
429
|
width: size.width,
|
|
@@ -336,7 +432,9 @@ async function generate(options) {
|
|
|
336
432
|
bytes: fs.statSync(options.out).size,
|
|
337
433
|
elapsed_seconds: elapsedSeconds,
|
|
338
434
|
realtime_ratio: source.duration ? elapsedSeconds / source.duration : null,
|
|
339
|
-
|
|
435
|
+
...(engine === "rvm"
|
|
436
|
+
? { rvm_ms_per_frame: metrics.ms_per_frame }
|
|
437
|
+
: { vision_ms_per_frame: metrics.vision_ms_per_frame }),
|
|
340
438
|
mask_size: { width: metrics.mask_width, height: metrics.mask_height },
|
|
341
439
|
alpha_transparent_ratio: metrics.alpha_transparent_ratio,
|
|
342
440
|
alpha_partial_ratio: metrics.alpha_partial_ratio,
|
|
@@ -347,14 +445,14 @@ async function generate(options) {
|
|
|
347
445
|
/// 書き出した WebM が本当に VP9 かつアルファ付きかを ffprobe で確かめる。
|
|
348
446
|
/// ffmpeg 自身は VP9 の alpha レイヤをデコードできない(pix_fmt は yuv420p と報告される)ので、
|
|
349
447
|
/// アルファの有無はコンテナの alpha_mode タグで判定する。
|
|
350
|
-
function verifyOutput(out) {
|
|
448
|
+
function verifyOutput(out, ffprobeBin) {
|
|
351
449
|
const probed = ffprobeJson([
|
|
352
450
|
"-select_streams",
|
|
353
451
|
"v:0",
|
|
354
452
|
"-show_entries",
|
|
355
453
|
"stream=codec_name,width,height,r_frame_rate,nb_frames:stream_tags=alpha_mode:format=duration",
|
|
356
454
|
out,
|
|
357
|
-
]);
|
|
455
|
+
], ffprobeBin);
|
|
358
456
|
const stream = probed?.streams?.[0];
|
|
359
457
|
if (!stream) throw new Error("書き出した WebM に映像ストリームがありません");
|
|
360
458
|
if (stream.codec_name !== "vp9") {
|
|
@@ -385,9 +483,10 @@ async function main() {
|
|
|
385
483
|
return;
|
|
386
484
|
}
|
|
387
485
|
|
|
388
|
-
const
|
|
486
|
+
const platform = os.platform();
|
|
487
|
+
const availability = checkAvailability({ platform });
|
|
389
488
|
if (options.check) {
|
|
390
|
-
printJson(availability);
|
|
489
|
+
printJson({ ...availability, rvm_model: resolveRvmModel(DEFAULT_RVM_MODEL) });
|
|
391
490
|
return;
|
|
392
491
|
}
|
|
393
492
|
if (!options.input || !options.out) {
|
|
@@ -402,17 +501,33 @@ async function main() {
|
|
|
402
501
|
}
|
|
403
502
|
|
|
404
503
|
try {
|
|
405
|
-
const
|
|
406
|
-
|
|
407
|
-
printJson({ ok: false, reason: `swiftc build failed: ${buildError}` });
|
|
408
|
-
process.exitCode = 1;
|
|
409
|
-
return;
|
|
410
|
-
}
|
|
411
|
-
printJson(await generate(options));
|
|
504
|
+
const prepared = prepareExecution(options, { platform });
|
|
505
|
+
printJson(await generate(prepared));
|
|
412
506
|
} catch (error) {
|
|
413
507
|
printJson({ ok: false, reason: summarize(error?.message, "人物マットの生成に失敗しました") });
|
|
414
508
|
process.exitCode = 1;
|
|
415
509
|
}
|
|
416
510
|
}
|
|
417
511
|
|
|
418
|
-
|
|
512
|
+
function isMainModule() {
|
|
513
|
+
if (!process.argv[1]) return false;
|
|
514
|
+
try {
|
|
515
|
+
return fs.realpathSync(fileURLToPath(import.meta.url)) === fs.realpathSync(path.resolve(process.argv[1]));
|
|
516
|
+
} catch {
|
|
517
|
+
return false;
|
|
518
|
+
}
|
|
519
|
+
}
|
|
520
|
+
|
|
521
|
+
if (isMainModule()) await main();
|
|
522
|
+
|
|
523
|
+
export {
|
|
524
|
+
DEFAULT_DECODE_WIDTH,
|
|
525
|
+
DEFAULT_FPS,
|
|
526
|
+
DEFAULT_QUALITY,
|
|
527
|
+
QUALITIES,
|
|
528
|
+
RVM_MODELS,
|
|
529
|
+
checkAvailability,
|
|
530
|
+
engineForPlatform,
|
|
531
|
+
parseArguments,
|
|
532
|
+
prepareExecution,
|
|
533
|
+
};
|
|
@@ -5,7 +5,9 @@
|
|
|
5
5
|
- [実行するかを先に決める](#実行するかを先に決める)
|
|
6
6
|
- [道具を確認する](#道具を確認する)
|
|
7
7
|
- [生成する](#生成する)
|
|
8
|
+
- [カットへ自動配線する](#カットへ自動配線する)
|
|
8
9
|
- [analysis.json へ書く](#analysisjson-へ書く)
|
|
10
|
+
- [Windows 実機検証手順](#windows-実機検証手順)
|
|
9
11
|
- [劣化](#劣化)
|
|
10
12
|
|
|
11
13
|
## 原則
|
|
@@ -37,6 +39,10 @@
|
|
|
37
39
|
| `fast` | 当たり付け・「この素材で切り抜けるか」の事前判定のみ | マット 256x192。輪郭が階段状で**本番不可** |
|
|
38
40
|
| `balanced`(既定) | **本番**(text-behind-person / 切り抜き) | マット 512x384・58 ms/frame・実用輪郭 |
|
|
39
41
|
| `accurate` | 寄りカット・最終仕上げ | マット 2016x1512・135 ms/frame・peak 638 MB |
|
|
42
|
+
| `best` | **髪の毛をくっきり残す必要があるときだけ** | RVM mobilenetv3。10 秒素材で約 1〜1.5 分待つ目安 |
|
|
43
|
+
| `best --model resnet50` | 処理時間をさらに許容できる場合のこだわり指定 | RVM resnet50。既定の mobilenetv3 より大幅に遅い |
|
|
44
|
+
|
|
45
|
+
Windows では全品質段が RVM mobilenetv3 になる。手順は [Windows 実機検証手順](#windows-実機検証手順) を参照する。
|
|
40
46
|
|
|
41
47
|
`fast` の出力を本番の成果物として `analysis.json` に載せない。
|
|
42
48
|
|
|
@@ -46,9 +52,11 @@
|
|
|
46
52
|
node bin/person-matte/person-matte.mjs --check
|
|
47
53
|
```
|
|
48
54
|
|
|
49
|
-
`
|
|
50
|
-
`swiftc
|
|
51
|
-
|
|
55
|
+
`available:true` 以外なら `reason` を報告し、マットを作らずに `null` のまま進む。結果には既定の
|
|
56
|
+
RVM モデルの配備状況を示す `rvm_model` も含まれる。Mac は `swiftc`、ffmpeg / ffprobe、ffmpeg の
|
|
57
|
+
`libvpx-vp9` エンコーダを要求する。Windows は swiftc を要求せず、同じメディア道具に加えて RVM の
|
|
58
|
+
mobilenetv3 モデルを要求する。ffmpeg / ffprobe は `packages/media-bin` の解決規則を使う。
|
|
59
|
+
ネットワークからツールを導入しない。Mac の Swift ヘルパーは初回実行時に `swiftc -O` で自動ビルドされ、
|
|
52
60
|
バイナリはコミットしない([.gitignore](.gitignore) 参照)。
|
|
53
61
|
|
|
54
62
|
## 生成する
|
|
@@ -63,7 +71,9 @@ node bin/person-matte/person-matte.mjs \
|
|
|
63
71
|
|
|
64
72
|
オプション(既定のままで良ければ渡さない):
|
|
65
73
|
|
|
66
|
-
- `--quality fast|balanced|accurate`(既定 `balanced`)
|
|
74
|
+
- `--quality fast|balanced|accurate|best`(既定 `balanced`)
|
|
75
|
+
- `--model mobilenetv3|resnet50`(`--quality best` のときだけ指定可、既定 `mobilenetv3`)。
|
|
76
|
+
`resnet50` は `cd packages/matte-rvm && node scripts/fetch-models.mjs --model resnet50` で明示取得する
|
|
67
77
|
- `--fps <n>`(既定 24)。マット動画の fps。元素材と一致しなくてよい
|
|
68
78
|
- `--decode-width <n>`(既定 1280)。Vision のマット解像度は quality だけで決まり入力解像度に
|
|
69
79
|
依存しないため、通常は変更しない。素材が 1280 幅未満なら拡大せずその幅を使う
|
|
@@ -71,7 +81,8 @@ node bin/person-matte/person-matte.mjs \
|
|
|
71
81
|
`--input` には**原本**を渡す。プロキシ(`proxy.mp4`)を渡してもよいが、その場合マットの RGB は
|
|
72
82
|
720p の絵になる。原本と時刻対応が崩れた素材を入力にしない。
|
|
73
83
|
|
|
74
|
-
成功時は 1 行の JSON が返る。`ok`、`frames`、`bytes`、`elapsed_seconds`、`
|
|
84
|
+
成功時は 1 行の JSON が返る。`ok`、`frames`、`bytes`、`elapsed_seconds`、`engine`、
|
|
85
|
+
Vision 時の `vision_ms_per_frame` または RVM 時の `rvm_ms_per_frame`、
|
|
75
86
|
`alpha_transparent_ratio`(完全透明画素の割合)、`probe`(ffprobe の実測)を含む。
|
|
76
87
|
ヘルパーは書き出した WebM が `codec_name = vp9` かつ `alpha_mode = 1` であることを確認してから
|
|
77
88
|
成功を返すので、`ok: true` はアルファが落ちていないことを含意する。
|
|
@@ -79,6 +90,47 @@ node bin/person-matte/person-matte.mjs \
|
|
|
79
90
|
`ok: false` なら `reason` を報告し、マットを諦めて `null` のまま分析を続ける。**マットが作れない
|
|
80
91
|
ことを理由に分析全体を失敗扱いにしない。**
|
|
81
92
|
|
|
93
|
+
## カットへ自動配線する
|
|
94
|
+
|
|
95
|
+
使い分けは次のとおり。
|
|
96
|
+
|
|
97
|
+
- 素材全体の解析結果として `analysis.json` の `tracks.person_matte` を作る場合は、上の
|
|
98
|
+
`person-matte.mjs` を使う。入力素材の時刻 0 とマットの時刻 0 を一致させる。
|
|
99
|
+
- 編集済みの特定カットを人物レイヤーへ載せる場合は `person-cutout.mjs` を使う。カットの `in` /
|
|
100
|
+
`out` / `speed` を解決し、速度適用済みの区間マットを作って v2 `edit.json` へ自動配線する。
|
|
101
|
+
|
|
102
|
+
```bash
|
|
103
|
+
node skills/analyze-footage/bin/person-matte/person-cutout.mjs \
|
|
104
|
+
--project /path/to/project \
|
|
105
|
+
--cut 0
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
`--cut` は v2 `tracks` を下から上、各 `items` を宣言順に走査した visual media item の 0 始まり
|
|
109
|
+
index で、`0,2,4` のように複数指定できる。自動生成済みの `person-N` item は数えない。
|
|
110
|
+
`--quality fast|balanced|accurate|best`(既定 `balanced`)と、`best` の場合だけ
|
|
111
|
+
`--model mobilenetv3|resnet50` を指定できる。
|
|
112
|
+
|
|
113
|
+
生成物は `<project>/assets/matte/person-<cut index>.webm`。v2 は旧 `layers[]` と
|
|
114
|
+
`timeline.tracks` を持たず、トップレベル `tracks[]` 自体が下→上の z 順の正本なので、コマンドは
|
|
115
|
+
マットを `sources[]` に登録し、対応する media item を `person-cutout` visual track へ置く。この
|
|
116
|
+
track は既存 track の相互順を変えず最前面へ移すため、HTML overlay やテロップより人物が上に来る。
|
|
117
|
+
|
|
118
|
+
同じ cut へ再実行すると `person-cutout-N` source と `person-N` item を更新し、source / item / track
|
|
119
|
+
を重複させない。`edit.json` は候補ファイルを v2 reader と `validate-edit.mjs` の両方で検証してから
|
|
120
|
+
atomic rename し、不合格時は元ファイルを変更しない。
|
|
121
|
+
|
|
122
|
+
変更内容だけを確認する場合は `--dry-run` を付ける。マットの生成、ディレクトリ作成、edit.json の
|
|
123
|
+
書き換えを一切行わず、予定するマット・レイヤー互換表示・track 順を stdout の 1 行 JSON で返す。
|
|
124
|
+
|
|
125
|
+
```bash
|
|
126
|
+
node skills/analyze-footage/bin/person-matte/person-cutout.mjs \
|
|
127
|
+
--project /path/to/project \
|
|
128
|
+
--cut 1,3 \
|
|
129
|
+
--quality best \
|
|
130
|
+
--model resnet50 \
|
|
131
|
+
--dry-run
|
|
132
|
+
```
|
|
133
|
+
|
|
82
134
|
## analysis.json へ書く
|
|
83
135
|
|
|
84
136
|
返ってきた JSON の `person_matte` をそのまま使えるが、**`path` は analysis.json のディレクトリ
|
|
@@ -101,6 +153,84 @@ node bin/person-matte/person-matte.mjs \
|
|
|
101
153
|
- `fps` が実際に書き出したマット動画の fps と一致する。
|
|
102
154
|
- `path` を analysis.json の位置から解決でき、実ファイルが存在する。
|
|
103
155
|
|
|
156
|
+
## Windows 実機検証手順
|
|
157
|
+
|
|
158
|
+
Windows では `fast` / `balanced` / `accurate` / `best` の全段が RVM mobilenetv3 を使う。
|
|
159
|
+
次の PowerShell コマンドは公開リポジトリのルートで実行する。Node.js 20 以上を前提とし、まず版を確認して
|
|
160
|
+
`packages/matte-rvm` 内で依存と既定モデルを配備する。`npm install` の postinstall が
|
|
161
|
+
mobilenetv3 を取得する。
|
|
162
|
+
|
|
163
|
+
```powershell
|
|
164
|
+
node --version
|
|
165
|
+
cd packages\matte-rvm
|
|
166
|
+
npm install
|
|
167
|
+
cd ..\..
|
|
168
|
+
```
|
|
169
|
+
|
|
170
|
+
道具とモデルを検査する。
|
|
171
|
+
|
|
172
|
+
```powershell
|
|
173
|
+
node skills\analyze-footage\bin\person-matte\person-matte.mjs --check
|
|
174
|
+
```
|
|
175
|
+
|
|
176
|
+
期待する 1 行 JSON は次の形で、`available` が `true`、`rvm_model.missing` が `false` になる。
|
|
177
|
+
|
|
178
|
+
```json
|
|
179
|
+
{"available":true,"rvm_model":{"model":"mobilenetv3","path":"C:\\...\\rvm_mobilenetv3_fp32.onnx","missing":false,"fetchHint":"cd packages/matte-rvm && node scripts/fetch-models.mjs"}}
|
|
180
|
+
```
|
|
181
|
+
|
|
182
|
+
次に、人物が映る実素材 1 本を `C:\Users\owner\Videos\person-input.mp4` へ置き、`balanced` で生成する。
|
|
183
|
+
出力先は必要に応じて書き換える。
|
|
184
|
+
|
|
185
|
+
```powershell
|
|
186
|
+
$SOURCE = "C:\Users\owner\Videos\person-input.mp4"
|
|
187
|
+
$OUT = "C:\Users\owner\Videos\person-matte-balanced.webm"
|
|
188
|
+
node skills\analyze-footage\bin\person-matte\person-matte.mjs --input "$SOURCE" --out "$OUT" --quality balanced
|
|
189
|
+
```
|
|
190
|
+
|
|
191
|
+
実際に返る 1 行 JSON から確認するキーの抜粋は次のとおりで、`ok:true`、`engine:"rvm"`、`model:"mobilenetv3"`、
|
|
192
|
+
`probe.alpha_mode:"1"` を含む。`quality` は `person_matte.quality:"balanced"` として指定値のまま残る。
|
|
193
|
+
|
|
194
|
+
```json
|
|
195
|
+
{"ok":true,"engine":"rvm","model":"mobilenetv3","person_matte":{"path":"C:\\Users\\owner\\Videos\\person-matte-balanced.webm","fps":24,"quality":"balanced","tool":"rvm-person-matting"},"probe":{"codec_name":"vp9","alpha_mode":"1"}}
|
|
196
|
+
```
|
|
197
|
+
|
|
198
|
+
速度は `rvm_ms_per_frame`(RVM 推論 1 フレーム当たりのミリ秒)で読む。Mac の既存実測は
|
|
199
|
+
**137〜271 ms/frame** なので、Windows の値を同じ単位で比較する。VP9 エンコードを含む全体時間では
|
|
200
|
+
ないため、総所要時間は `elapsed_seconds` も併記する。
|
|
201
|
+
|
|
202
|
+
実測記録欄:
|
|
203
|
+
|
|
204
|
+
- Windows PC: CPU ______ / RAM ______ GB / GPU ______ / Windows ______
|
|
205
|
+
- 素材: 解像度 ______ / fps ______ / 尺 ______ 秒
|
|
206
|
+
- `rvm_ms_per_frame`: ______ ms/frame(Mac 137〜271 ms/frame と比較)
|
|
207
|
+
- `elapsed_seconds`: ______ 秒
|
|
208
|
+
|
|
209
|
+
つまずいた場合は `reason` を先に読む。
|
|
210
|
+
|
|
211
|
+
- モデル不在: `rvm_model.missing:true` または reason 内の `fetchHint` を確認し、次のコマンドで
|
|
212
|
+
mobilenetv3 を取得してから再検査する。
|
|
213
|
+
|
|
214
|
+
```powershell
|
|
215
|
+
cd packages\matte-rvm
|
|
216
|
+
node scripts\fetch-models.mjs
|
|
217
|
+
cd ..\..
|
|
218
|
+
node skills\analyze-footage\bin\person-matte\person-matte.mjs --check
|
|
219
|
+
```
|
|
220
|
+
|
|
221
|
+
- ffmpeg / ffprobe 不在: `packages/media-bin` の postinstall が同梱バイナリを配備するため、
|
|
222
|
+
次のコマンドで配備をやり直してから `--check` を再実行する。
|
|
223
|
+
|
|
224
|
+
```powershell
|
|
225
|
+
cd packages\media-bin
|
|
226
|
+
npm install
|
|
227
|
+
cd ..\..
|
|
228
|
+
node skills\analyze-footage\bin\person-matte\person-matte.mjs --check
|
|
229
|
+
```
|
|
230
|
+
|
|
231
|
+
- `libvpx-vp9` 不在: 解決された ffmpeg が VP9 alpha を書けない。`reason` に従って media-bin の配備と
|
|
232
|
+
ffmpeg の解決元を確認する。
|
|
233
|
+
|
|
104
234
|
## 劣化
|
|
105
235
|
|
|
106
236
|
道具が無い、生成に失敗した、または品質が実用に達しない場合は `tracks.person_matte` を `null` に
|