video-diff-checker 0.1.0__py3-none-any.whl

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 (40) hide show
  1. video_diff_checker/__init__.py +3 -0
  2. video_diff_checker/cli.py +629 -0
  3. video_diff_checker/common_options.py +64 -0
  4. video_diff_checker/detect/__init__.py +35 -0
  5. video_diff_checker/detect/detector.py +333 -0
  6. video_diff_checker/detect/errors.py +17 -0
  7. video_diff_checker/detect/extractor.py +180 -0
  8. video_diff_checker/detect/models.py +79 -0
  9. video_diff_checker/detect/runner.py +167 -0
  10. video_diff_checker/diff/__init__.py +57 -0
  11. video_diff_checker/diff/filtergraph.py +109 -0
  12. video_diff_checker/diff/models.py +66 -0
  13. video_diff_checker/diff/probe.py +204 -0
  14. video_diff_checker/diff/progress.py +77 -0
  15. video_diff_checker/diff/runner.py +180 -0
  16. video_diff_checker/diff/score_parser.py +188 -0
  17. video_diff_checker/ffmpeg_utils.py +136 -0
  18. video_diff_checker/gpu_utils.py +116 -0
  19. video_diff_checker/grid/__init__.py +25 -0
  20. video_diff_checker/grid/filtergraph.py +327 -0
  21. video_diff_checker/grid/models.py +60 -0
  22. video_diff_checker/grid/runner.py +478 -0
  23. video_diff_checker/logger.py +37 -0
  24. video_diff_checker/messages.py +158 -0
  25. video_diff_checker/report/__init__.py +1 -0
  26. video_diff_checker/report/errors.py +13 -0
  27. video_diff_checker/report/evaluator.py +114 -0
  28. video_diff_checker/report/json_formatter.py +122 -0
  29. video_diff_checker/report/messages.py +82 -0
  30. video_diff_checker/report/models.py +121 -0
  31. video_diff_checker/report/runner.py +287 -0
  32. video_diff_checker/report/score_reader.py +87 -0
  33. video_diff_checker/report/segments_reader.py +97 -0
  34. video_diff_checker/report/text_formatter.py +129 -0
  35. video_diff_checker-0.1.0.dist-info/METADATA +216 -0
  36. video_diff_checker-0.1.0.dist-info/RECORD +40 -0
  37. video_diff_checker-0.1.0.dist-info/WHEEL +5 -0
  38. video_diff_checker-0.1.0.dist-info/entry_points.txt +2 -0
  39. video_diff_checker-0.1.0.dist-info/licenses/LICENSE +21 -0
  40. video_diff_checker-0.1.0.dist-info/top_level.txt +1 -0
@@ -0,0 +1,3 @@
1
+ """Video Diff Checker — pixel-level video comparison tool."""
2
+
3
+ __version__ = "0.1.0"
@@ -0,0 +1,629 @@
1
+ """CLI エントリポイントとサブコマンド構造モジュール。
2
+
3
+ メインパーサーの構築、6 サブコマンドの登録、引数解析、
4
+ および検証・ディスパッチを行う。
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ import argparse
10
+ import sys
11
+ from pathlib import Path
12
+
13
+ from video_diff_checker import __version__
14
+ from video_diff_checker.common_options import create_common_parser
15
+ from video_diff_checker.ffmpeg_utils import (
16
+ FFmpegNotFoundError,
17
+ FFmpegVersionError,
18
+ verify_ffmpeg,
19
+ )
20
+ from video_diff_checker.gpu_utils import verify_gpu
21
+ from video_diff_checker.logger import setup_logger
22
+ from video_diff_checker.messages import (
23
+ COMPARE_DETECT_STAGE_COMPLETE,
24
+ COMPARE_DIFF_STAGE_COMPLETE,
25
+ COMPARE_GRID_STAGE_COMPLETE,
26
+ DETECT_NO_SEGMENTS,
27
+ DIFF_GPU_NOT_SUPPORTED,
28
+ SUBCOMMAND_NOT_IMPLEMENTED,
29
+ )
30
+
31
+ SUBCOMMANDS: list[dict[str, str]] = [
32
+ {"name": "compare", "help": "差分検出・抽出・グリッド生成を一括実行する"},
33
+ {"name": "diff", "help": "フレーム単位のピクセル差分を検出する"},
34
+ {"name": "detect", "help": "差分スコアから変化区間を特定する"},
35
+ {"name": "extract", "help": "変化区間のパート動画を切り出す"},
36
+ {"name": "grid", "help": "2×2 グリッド比較動画を生成する"},
37
+ {"name": "report", "help": "変更サマリーレポートを生成する"},
38
+ ]
39
+
40
+
41
+ def stub_handler(args: argparse.Namespace) -> int:
42
+ """未実装サブコマンドのスタブハンドラ。メッセージ表示後に終了コード 1 を返す。"""
43
+ print(SUBCOMMAND_NOT_IMPLEMENTED.format(command=args.subcommand))
44
+ return 1
45
+
46
+
47
+ def diff_handler(args: argparse.Namespace) -> int:
48
+ """diff サブコマンドのエントリポイント。
49
+
50
+ Returns:
51
+ 終了コード (0: 成功, 1: エラー)
52
+ """
53
+ from video_diff_checker.diff.models import DiffConfig
54
+ from video_diff_checker.diff.runner import DEFAULT_OUTPUT_DIR
55
+ from video_diff_checker.diff.runner import run_diff as _run_diff
56
+
57
+ if args.gpu:
58
+ print(DIFF_GPU_NOT_SUPPORTED)
59
+
60
+ output_dir = Path(args.output) if args.output else DEFAULT_OUTPUT_DIR
61
+ config = DiffConfig(
62
+ old_path=Path(args.old),
63
+ new_path=Path(args.new),
64
+ output_dir=output_dir,
65
+ verbose=args.verbose,
66
+ )
67
+
68
+ try:
69
+ _run_diff(config)
70
+ except Exception as exc:
71
+ print(str(exc), file=sys.stderr)
72
+ return 1
73
+
74
+ return 0
75
+
76
+
77
+ def compare_handler(args: argparse.Namespace) -> int:
78
+ """compare サブコマンドのエントリポイント。
79
+
80
+ パイプラインの各ステージ(diff → detect → extract → grid)を順次実行する。
81
+ 現時点では diff ステージ(F-01)のみ実装済み。
82
+
83
+ Returns:
84
+ 終了コード (0: 成功, 1: エラー)
85
+ """
86
+ from video_diff_checker.diff.models import DiffConfig
87
+ from video_diff_checker.diff.runner import DEFAULT_OUTPUT_DIR
88
+ from video_diff_checker.diff.runner import run_diff as _run_diff
89
+
90
+ if args.gpu:
91
+ print(DIFF_GPU_NOT_SUPPORTED)
92
+
93
+ output_dir = Path(args.output) if args.output else DEFAULT_OUTPUT_DIR
94
+ config = DiffConfig(
95
+ old_path=Path(args.old),
96
+ new_path=Path(args.new),
97
+ output_dir=output_dir,
98
+ verbose=args.verbose,
99
+ )
100
+
101
+ try:
102
+ diff_result = _run_diff(config)
103
+ except Exception as exc:
104
+ print(str(exc), file=sys.stderr)
105
+ return 1
106
+
107
+ print(
108
+ COMPARE_DIFF_STAGE_COMPLETE.format(
109
+ diff_video=diff_result.diff_video,
110
+ scores_csv=diff_result.scores_csv,
111
+ )
112
+ )
113
+
114
+ # F-02: detect/extract ステージ
115
+ from video_diff_checker.detect.runner import run_detect_extract
116
+
117
+ try:
118
+ detect_result = run_detect_extract(
119
+ diff_result,
120
+ threshold=args.threshold,
121
+ margin=args.margin,
122
+ merge_gap=args.merge_gap,
123
+ output_dir=output_dir,
124
+ )
125
+ except Exception as exc:
126
+ print(str(exc), file=sys.stderr)
127
+ return 1
128
+
129
+ if not detect_result.segments:
130
+ print(DETECT_NO_SEGMENTS)
131
+ return 0
132
+
133
+ print(
134
+ COMPARE_DETECT_STAGE_COMPLETE.format(
135
+ segment_count=len(detect_result.segments),
136
+ )
137
+ )
138
+
139
+ # F-03: grid ステージ
140
+ from video_diff_checker.grid.runner import run_grid_pipeline
141
+
142
+ try:
143
+ grid_result = run_grid_pipeline(
144
+ detect_result,
145
+ diff_result,
146
+ output_dir=output_dir,
147
+ )
148
+ except Exception as exc:
149
+ print(str(exc), file=sys.stderr)
150
+ return 1
151
+
152
+ print(
153
+ COMPARE_GRID_STAGE_COMPLETE.format(
154
+ count=len(grid_result.grid_videos),
155
+ )
156
+ )
157
+
158
+ if grid_result.exit_code != 0:
159
+ return grid_result.exit_code
160
+
161
+ # F-04: report ステージ
162
+ from video_diff_checker.report.messages import COMPARE_REPORT_STAGE_COMPLETE
163
+ from video_diff_checker.report.runner import run_report_from_pipeline
164
+
165
+ try:
166
+ report_result = run_report_from_pipeline(
167
+ diff_result,
168
+ detect_result,
169
+ output_dir=output_dir,
170
+ )
171
+ except Exception as exc:
172
+ print(str(exc), file=sys.stderr)
173
+ return 1
174
+
175
+ print(
176
+ COMPARE_REPORT_STAGE_COMPLETE.format(
177
+ category=report_result.overall_category.value,
178
+ ratio=report_result.change_ratio,
179
+ )
180
+ )
181
+
182
+ return 0
183
+
184
+
185
+ def grid_handler(args: argparse.Namespace) -> int:
186
+ """grid サブコマンドのエントリポイント。
187
+
188
+ Returns:
189
+ 終了コード (0: 成功, 1: エラー)
190
+ """
191
+ from video_diff_checker.grid.models import GridConfig
192
+ from video_diff_checker.grid.runner import run_grid
193
+
194
+ output_dir = Path(args.output) if args.output else Path("./vdc-output/")
195
+ config = GridConfig(
196
+ old_video=Path(args.old),
197
+ new_video=Path(args.new),
198
+ diff_video=Path(args.diff),
199
+ segments_json=Path(args.segments),
200
+ output_dir=output_dir,
201
+ workers=args.workers,
202
+ verbose=args.verbose,
203
+ )
204
+
205
+ try:
206
+ result = run_grid(config)
207
+ except Exception as exc:
208
+ print(str(exc), file=sys.stderr)
209
+ return 1
210
+
211
+ return result.exit_code
212
+
213
+
214
+ def extract_handler(args: argparse.Namespace) -> int:
215
+ """extract サブコマンドのエントリポイント。
216
+
217
+ Returns:
218
+ 終了コード (0: 成功, 1: エラー)
219
+ """
220
+ from video_diff_checker.detect.detector import load_segments
221
+ from video_diff_checker.detect.models import ExtractConfig
222
+ from video_diff_checker.detect.runner import run_extract
223
+
224
+ output_dir = Path(args.output) if args.output else Path("./vdc-output/")
225
+ segments = load_segments(Path(args.segments))
226
+
227
+ config = ExtractConfig(
228
+ old_video=Path(args.old),
229
+ new_video=Path(args.new),
230
+ diff_video=Path(args.diff),
231
+ output_dir=output_dir,
232
+ workers=args.workers,
233
+ verbose=args.verbose,
234
+ )
235
+
236
+ try:
237
+ run_extract(segments, config)
238
+ except Exception as exc:
239
+ print(str(exc), file=sys.stderr)
240
+ return 1
241
+
242
+ return 0
243
+
244
+
245
+ def detect_handler(args: argparse.Namespace) -> int:
246
+ """detect サブコマンドのエントリポイント。
247
+
248
+ Returns:
249
+ 終了コード (0: 成功, 1: エラー)
250
+ """
251
+ from video_diff_checker.detect.models import DetectConfig
252
+ from video_diff_checker.detect.runner import run_detect
253
+
254
+ output_dir = Path(args.output) if args.output else Path("./vdc-output/")
255
+ config = DetectConfig(
256
+ scores_csv=Path(args.scores_csv),
257
+ threshold=args.threshold,
258
+ margin=args.margin,
259
+ merge_gap=args.merge_gap,
260
+ fps=args.fps,
261
+ output_dir=output_dir,
262
+ verbose=args.verbose,
263
+ )
264
+
265
+ try:
266
+ run_detect(config)
267
+ except Exception as exc:
268
+ print(str(exc), file=sys.stderr)
269
+ return 1
270
+
271
+ return 0
272
+
273
+
274
+ def register_compare_command(
275
+ subparsers: argparse._SubParsersAction,
276
+ parent: argparse.ArgumentParser,
277
+ ) -> None:
278
+ """compare サブコマンドをサブパーサーに登録する。"""
279
+ compare_cmd = next(cmd for cmd in SUBCOMMANDS if cmd["name"] == "compare")
280
+ parser = subparsers.add_parser(
281
+ "compare",
282
+ parents=[parent],
283
+ help=compare_cmd["help"],
284
+ )
285
+ parser.add_argument(
286
+ "old",
287
+ help="旧動画ファイルのパス",
288
+ )
289
+ parser.add_argument(
290
+ "new",
291
+ help="新動画ファイルのパス",
292
+ )
293
+ parser.add_argument(
294
+ "--output",
295
+ "-o",
296
+ default=None,
297
+ help="出力ディレクトリのパス(デフォルト: ./vdc-output/)",
298
+ )
299
+ parser.add_argument(
300
+ "--margin",
301
+ type=int,
302
+ default=30,
303
+ help="セグメント前後に追加するマージン(フレーム数、デフォルト: 30)",
304
+ )
305
+ parser.add_argument(
306
+ "--merge-gap",
307
+ type=int,
308
+ default=15,
309
+ help="セグメント統合のギャップ閾値(フレーム数、デフォルト: 15)",
310
+ )
311
+ parser.set_defaults(func=compare_handler, subcommand="compare")
312
+
313
+
314
+ def register_diff_command(
315
+ subparsers: argparse._SubParsersAction,
316
+ parent: argparse.ArgumentParser,
317
+ ) -> None:
318
+ """diff サブコマンドをサブパーサーに登録する。"""
319
+ diff_cmd = next(cmd for cmd in SUBCOMMANDS if cmd["name"] == "diff")
320
+ parser = subparsers.add_parser(
321
+ "diff",
322
+ parents=[parent],
323
+ help=diff_cmd["help"],
324
+ )
325
+ parser.add_argument(
326
+ "old",
327
+ help="旧動画ファイルのパス",
328
+ )
329
+ parser.add_argument(
330
+ "new",
331
+ help="新動画ファイルのパス",
332
+ )
333
+ parser.add_argument(
334
+ "--output",
335
+ "-o",
336
+ default=None,
337
+ help="出力ディレクトリのパス(デフォルト: ./vdc-output/)",
338
+ )
339
+ parser.set_defaults(func=diff_handler, subcommand="diff")
340
+
341
+
342
+ def register_detect_command(
343
+ subparsers: argparse._SubParsersAction,
344
+ parent: argparse.ArgumentParser,
345
+ ) -> None:
346
+ """detect サブコマンドをサブパーサーに登録する。"""
347
+ detect_cmd = next(cmd for cmd in SUBCOMMANDS if cmd["name"] == "detect")
348
+ parser = subparsers.add_parser(
349
+ "detect",
350
+ parents=[parent],
351
+ help=detect_cmd["help"],
352
+ )
353
+ parser.add_argument(
354
+ "scores_csv",
355
+ help="スコア CSV ファイルのパス",
356
+ )
357
+ parser.add_argument(
358
+ "--margin",
359
+ type=int,
360
+ default=30,
361
+ help="セグメント前後に追加するマージン(フレーム数、デフォルト: 30)",
362
+ )
363
+ parser.add_argument(
364
+ "--merge-gap",
365
+ type=int,
366
+ default=15,
367
+ help="セグメント統合のギャップ閾値(フレーム数、デフォルト: 15)",
368
+ )
369
+ parser.add_argument(
370
+ "--fps",
371
+ type=float,
372
+ default=30.0,
373
+ help="フレームレート(時刻計算用、デフォルト: 30.0)",
374
+ )
375
+ parser.add_argument(
376
+ "--output",
377
+ "-o",
378
+ default=None,
379
+ help="出力ディレクトリのパス(デフォルト: ./vdc-output/)",
380
+ )
381
+ parser.set_defaults(func=detect_handler, subcommand="detect")
382
+
383
+
384
+ def register_extract_command(
385
+ subparsers: argparse._SubParsersAction,
386
+ parent: argparse.ArgumentParser,
387
+ ) -> None:
388
+ """extract サブコマンドをサブパーサーに登録する。"""
389
+ import os
390
+
391
+ extract_cmd = next(cmd for cmd in SUBCOMMANDS if cmd["name"] == "extract")
392
+ parser = subparsers.add_parser(
393
+ "extract",
394
+ parents=[parent],
395
+ help=extract_cmd["help"],
396
+ )
397
+ parser.add_argument(
398
+ "old",
399
+ help="旧動画ファイルのパス",
400
+ )
401
+ parser.add_argument(
402
+ "new",
403
+ help="新動画ファイルのパス",
404
+ )
405
+ parser.add_argument(
406
+ "diff",
407
+ help="差分動画ファイルのパス",
408
+ )
409
+ parser.add_argument(
410
+ "--segments",
411
+ required=True,
412
+ help="segments.json ファイルのパス",
413
+ )
414
+ parser.add_argument(
415
+ "--output",
416
+ "-o",
417
+ default=None,
418
+ help="出力ディレクトリのパス(デフォルト: ./vdc-output/)",
419
+ )
420
+ parser.add_argument(
421
+ "--workers",
422
+ type=int,
423
+ default=os.cpu_count(),
424
+ help=f"並列ワーカー数の上限(デフォルト: {os.cpu_count()})",
425
+ )
426
+ parser.set_defaults(func=extract_handler, subcommand="extract")
427
+
428
+
429
+ def register_grid_command(
430
+ subparsers: argparse._SubParsersAction,
431
+ parent: argparse.ArgumentParser,
432
+ ) -> None:
433
+ """grid サブコマンドをサブパーサーに登録する。"""
434
+ import os
435
+
436
+ grid_cmd = next(cmd for cmd in SUBCOMMANDS if cmd["name"] == "grid")
437
+ parser = subparsers.add_parser(
438
+ "grid",
439
+ parents=[parent],
440
+ help=grid_cmd["help"],
441
+ )
442
+ parser.add_argument(
443
+ "old",
444
+ help="旧動画ファイルのパス",
445
+ )
446
+ parser.add_argument(
447
+ "new",
448
+ help="新動画ファイルのパス",
449
+ )
450
+ parser.add_argument(
451
+ "diff",
452
+ help="差分動画ファイルのパス",
453
+ )
454
+ parser.add_argument(
455
+ "--segments",
456
+ required=True,
457
+ help="segments.json ファイルのパス",
458
+ )
459
+ parser.add_argument(
460
+ "--output",
461
+ "-o",
462
+ default=None,
463
+ help="出力ディレクトリのパス(デフォルト: ./vdc-output/)",
464
+ )
465
+ parser.add_argument(
466
+ "--workers",
467
+ type=int,
468
+ default=os.cpu_count(),
469
+ help=f"並列ワーカー数の上限(デフォルト: {os.cpu_count()})",
470
+ )
471
+ parser.set_defaults(func=grid_handler, subcommand="grid")
472
+
473
+
474
+ def report_handler(args: argparse.Namespace) -> int:
475
+ """report サブコマンドのエントリポイント。
476
+
477
+ Returns:
478
+ 終了コード (0: 成功, 1: エラー)
479
+ """
480
+ from video_diff_checker.report.runner import run_report
481
+
482
+ from video_diff_checker.report.models import ReportConfig
483
+
484
+ output_dir = Path(args.output) if args.output else Path("./vdc-output/")
485
+ config = ReportConfig(
486
+ scores_csv=Path(args.scores_csv),
487
+ segments_json=Path(args.segments_json),
488
+ output_dir=output_dir,
489
+ format=args.format,
490
+ verbose=args.verbose,
491
+ )
492
+
493
+ try:
494
+ run_report(config)
495
+ except Exception as exc:
496
+ print(str(exc), file=sys.stderr)
497
+ return 1
498
+
499
+ return 0
500
+
501
+
502
+ def register_report_command(
503
+ subparsers: argparse._SubParsersAction,
504
+ ) -> None:
505
+ """report サブコマンドをサブパーサーに登録する。
506
+
507
+ Note: --gpu / --threshold は登録しない(DD-12)。
508
+ 共通パーサーを継承せず、--verbose のみを直接定義する。
509
+ """
510
+ report_cmd = next(cmd for cmd in SUBCOMMANDS if cmd["name"] == "report")
511
+ parser = subparsers.add_parser(
512
+ "report",
513
+ help=report_cmd["help"],
514
+ )
515
+ parser.add_argument(
516
+ "scores_csv",
517
+ help="スコア CSV ファイルのパス",
518
+ )
519
+ parser.add_argument(
520
+ "segments_json",
521
+ help="segments.json ファイルのパス",
522
+ )
523
+ parser.add_argument(
524
+ "--output",
525
+ "-o",
526
+ default=None,
527
+ help="出力ディレクトリのパス(デフォルト: ./vdc-output/)",
528
+ )
529
+ parser.add_argument(
530
+ "--format",
531
+ choices=["json", "text", "all"],
532
+ default="all",
533
+ help="出力形式(json / text / all、デフォルト: all)",
534
+ )
535
+ parser.add_argument(
536
+ "--verbose",
537
+ "-v",
538
+ action="store_true",
539
+ default=False,
540
+ help="詳細ログを出力する",
541
+ )
542
+ parser.set_defaults(func=report_handler, subcommand="report")
543
+
544
+
545
+ def register_subcommands(
546
+ subparsers: argparse._SubParsersAction,
547
+ parent: argparse.ArgumentParser,
548
+ ) -> None:
549
+ """6 サブコマンドをサブパーサーに登録する。"""
550
+ for cmd in SUBCOMMANDS:
551
+ if cmd["name"] == "compare":
552
+ register_compare_command(subparsers, parent)
553
+ continue
554
+ if cmd["name"] == "diff":
555
+ register_diff_command(subparsers, parent)
556
+ continue
557
+ if cmd["name"] == "detect":
558
+ register_detect_command(subparsers, parent)
559
+ continue
560
+ if cmd["name"] == "extract":
561
+ register_extract_command(subparsers, parent)
562
+ continue
563
+ if cmd["name"] == "grid":
564
+ register_grid_command(subparsers, parent)
565
+ continue
566
+ if cmd["name"] == "report":
567
+ register_report_command(subparsers)
568
+ continue
569
+ parser = subparsers.add_parser(
570
+ cmd["name"],
571
+ parents=[parent],
572
+ help=cmd["help"],
573
+ )
574
+ parser.set_defaults(func=stub_handler, subcommand=cmd["name"])
575
+
576
+
577
+ def create_parser(common_parser: argparse.ArgumentParser) -> argparse.ArgumentParser:
578
+ """メインパーサーとサブパーサーを構築する。"""
579
+ parser = argparse.ArgumentParser(
580
+ prog="video-diff-checker",
581
+ description="2 本の動画ファイルを比較し、差分検出・抽出・グリッド比較動画を生成するツール",
582
+ )
583
+ # --version フラグでバージョン情報を表示
584
+ parser.add_argument(
585
+ "--version",
586
+ action="version",
587
+ version=f"%(prog)s {__version__}",
588
+ )
589
+
590
+ subparsers = parser.add_subparsers(dest="subcommand")
591
+ register_subcommands(subparsers, common_parser)
592
+
593
+ return parser
594
+
595
+
596
+ def main() -> None:
597
+ """CLI エントリポイント。引数解析 → ロギング設定 → FFmpeg 検証 → GPU 検証 → ディスパッチ。"""
598
+ common_parser = create_common_parser()
599
+ parser = create_parser(common_parser)
600
+ args = parser.parse_args()
601
+
602
+ # サブコマンド未指定時はヘルプを表示
603
+ if args.subcommand is None:
604
+ parser.print_help()
605
+ sys.exit(0)
606
+
607
+ # ロギング設定
608
+ setup_logger(verbose=args.verbose)
609
+
610
+ # report サブコマンドは FFmpeg / GPU 不要
611
+ if args.subcommand != "report":
612
+ # FFmpeg 検証
613
+ try:
614
+ verify_ffmpeg(verbose=args.verbose)
615
+ except (FFmpegNotFoundError, FFmpegVersionError) as exc:
616
+ print(str(exc), file=sys.stderr)
617
+ sys.exit(1)
618
+
619
+ # GPU 検証(--gpu 指定時のみ)
620
+ if getattr(args, "gpu", False):
621
+ verify_gpu(verbose=args.verbose)
622
+
623
+ # サブコマンドディスパッチ
624
+ exit_code = args.func(args)
625
+ sys.exit(exit_code)
626
+
627
+
628
+ if __name__ == "__main__":
629
+ main()
@@ -0,0 +1,64 @@
1
+ """共通 CLI オプション定義モジュール。
2
+
3
+ 全サブコマンド共通の CLI オプション(--verbose, --threshold, --gpu)を
4
+ 親パーサーとして一箇所で定義し、各サブコマンドから parents= 引数で再利用する。
5
+ """
6
+
7
+ import argparse
8
+
9
+ from video_diff_checker.messages import THRESHOLD_OUT_OF_RANGE
10
+
11
+
12
+ def validate_threshold(value: str) -> float:
13
+ """threshold 値を float に変換し、0.0〜1.0 の範囲を検証する。
14
+
15
+ Args:
16
+ value: コマンドライン引数として渡された文字列。
17
+
18
+ Returns:
19
+ 検証済みの float 値。
20
+
21
+ Raises:
22
+ argparse.ArgumentTypeError: 数値変換に失敗した場合、または範囲外の場合。
23
+ """
24
+ try:
25
+ fvalue = float(value)
26
+ except ValueError:
27
+ raise argparse.ArgumentTypeError(
28
+ THRESHOLD_OUT_OF_RANGE.format(value=value)
29
+ )
30
+ if fvalue < 0.0 or fvalue > 1.0:
31
+ raise argparse.ArgumentTypeError(
32
+ THRESHOLD_OUT_OF_RANGE.format(value=value)
33
+ )
34
+ return fvalue
35
+
36
+
37
+ def create_common_parser() -> argparse.ArgumentParser:
38
+ """共通オプション付き親パーサーを返す。add_help=False で作成する。
39
+
40
+ Returns:
41
+ --verbose, --threshold, --gpu を含む親パーサー。
42
+ """
43
+ parser = argparse.ArgumentParser(add_help=False)
44
+ parser.add_argument(
45
+ "--verbose",
46
+ "-v",
47
+ action="store_true",
48
+ default=False,
49
+ help="詳細ログを出力する",
50
+ )
51
+ parser.add_argument(
52
+ "--threshold",
53
+ "-t",
54
+ type=validate_threshold,
55
+ default=0.95,
56
+ help="SSIM ベースの閾値(0.0〜1.0、デフォルト: 0.95)",
57
+ )
58
+ parser.add_argument(
59
+ "--gpu",
60
+ action="store_true",
61
+ default=False,
62
+ help="GPU アクセラレーションを使用する",
63
+ )
64
+ return parser