akari-video 0.1.19 → 0.1.21

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (27) hide show
  1. package/package.json +1 -1
  2. package/vendor/.akari-capability-sources.json +1 -0
  3. package/vendor/docs/contract-2026-07-23-analysis-person-matte.md +102 -2
  4. package/vendor/packages/akari-launcher/package.json +1 -1
  5. package/vendor/packages/edit-lint/src/edit-lint.mjs +90 -18
  6. package/vendor/packages/edit-store/lib/cut-adjacency.d.ts +25 -0
  7. package/vendor/packages/edit-store/lib/cut-adjacency.js +38 -8
  8. package/vendor/packages/edit-store/lib/edit-store.d.ts +13 -2
  9. package/vendor/packages/edit-store/lib/edit-store.js +69 -3
  10. package/vendor/packages/edit-store/lib/index.d.ts +1 -0
  11. package/vendor/packages/edit-store/lib/index.js +1 -0
  12. package/vendor/packages/edit-store/lib/internal-model.d.ts +13 -0
  13. package/vendor/packages/edit-store/lib/internal-model.js +98 -18
  14. package/vendor/packages/edit-store/lib/migrate/legacy-parse.js +9 -6
  15. package/vendor/packages/edit-store/lib/timeline-map.d.ts +5 -0
  16. package/vendor/packages/edit-store/lib/timeline-map.js +40 -0
  17. package/vendor/packages/edit-store/lib/transition-vocabulary.d.ts +223 -0
  18. package/vendor/packages/edit-store/lib/transition-vocabulary.js +45 -0
  19. package/vendor/packages/edit-store/lib/webview-kernel.js +98 -0
  20. package/vendor/packages/matte-rvm/package.json +22 -0
  21. package/vendor/packages/schemas/bin/validate-edit.mjs +5 -2
  22. package/vendor/packages/schemas/edit.schema.json +2 -2
  23. package/vendor/packages/schemas/test/transition-vocabulary.test.mjs +64 -0
  24. package/vendor/packages/schemas/test/validate-edit.test.mjs +2 -2
  25. package/vendor/skills/analyze-footage/bin/person-matte/person-cutout.mjs +453 -0
  26. package/vendor/skills/analyze-footage/bin/person-matte/person-matte.mjs +163 -48
  27. package/vendor/skills/analyze-footage/person-matte.md +135 -5
@@ -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] [--fps 24] [--decode-width 1280]
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
- if (os.platform() !== "darwin") {
60
- return { available: false, reason: `macOS ではありません(${os.platform()})` };
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
- const swift = spawnSyncSafe("swiftc", ["-version"]);
64
- if (swift.error?.code === "ENOENT") {
65
- return { available: false, reason: "swiftc が PATH 上にありません" };
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
- if (swift.error || swift.status !== 0) {
68
- return { available: false, reason: "swiftc を起動できません" };
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 = spawnSyncSafe(command, ["-version"]);
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: `${command} が PATH 上にありません` };
103
+ return { available: false, reason: `${label} が見つかりません` };
75
104
  }
76
105
  if (probe.error || probe.status !== 0) {
77
- return { available: false, reason: `${command} を起動できません` };
106
+ return { available: false, reason: `${label} を起動できません` };
78
107
  }
79
108
  }
80
109
 
81
110
  // VP9 alpha WebM を書けるかは libvpx-vp9 の有無で決まる(ffmpeg 内蔵の vp9 エンコーダは別物)。
82
- const encoders = spawnSyncSafe("ffmpeg", ["-hide_banner", "-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 ffprobeJson(args) {
170
- const result = spawnSyncSafe("ffprobe", ["-v", "error", ...args, "-of", "json"]);
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 source = probeSource(options.input);
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
- "ffmpeg",
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 = stage(
255
- "segment",
256
- options.helperBin,
257
- [
258
- "--width", String(size.width),
259
- "--height", String(size.height),
260
- "--quality", options.quality,
261
- "--metrics", metricsPath,
262
- ],
263
- ["pipe", "pipe", "pipe"],
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
- "ffmpeg",
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
- vision_ms_per_frame: metrics.vision_ms_per_frame,
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 availability = checkAvailability();
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 buildError = buildHelper(options.helperBin);
406
- if (buildError) {
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
- await main();
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
- `{"available":true}` 以外なら `reason` を報告し、マットを作らずに `null` のまま進む。macOS、
50
- `swiftc`、`ffmpeg`/`ffprobe`、ffmpeg の `libvpx-vp9` エンコーダのいずれかが欠けていれば作れない。
51
- ネットワークからツールを導入しない。Swift ヘルパーは初回実行時に `swiftc -O` で自動ビルドされ、
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`、`vision_ms_per_frame`、
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` に