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.
- video_diff_checker/__init__.py +3 -0
- video_diff_checker/cli.py +629 -0
- video_diff_checker/common_options.py +64 -0
- video_diff_checker/detect/__init__.py +35 -0
- video_diff_checker/detect/detector.py +333 -0
- video_diff_checker/detect/errors.py +17 -0
- video_diff_checker/detect/extractor.py +180 -0
- video_diff_checker/detect/models.py +79 -0
- video_diff_checker/detect/runner.py +167 -0
- video_diff_checker/diff/__init__.py +57 -0
- video_diff_checker/diff/filtergraph.py +109 -0
- video_diff_checker/diff/models.py +66 -0
- video_diff_checker/diff/probe.py +204 -0
- video_diff_checker/diff/progress.py +77 -0
- video_diff_checker/diff/runner.py +180 -0
- video_diff_checker/diff/score_parser.py +188 -0
- video_diff_checker/ffmpeg_utils.py +136 -0
- video_diff_checker/gpu_utils.py +116 -0
- video_diff_checker/grid/__init__.py +25 -0
- video_diff_checker/grid/filtergraph.py +327 -0
- video_diff_checker/grid/models.py +60 -0
- video_diff_checker/grid/runner.py +478 -0
- video_diff_checker/logger.py +37 -0
- video_diff_checker/messages.py +158 -0
- video_diff_checker/report/__init__.py +1 -0
- video_diff_checker/report/errors.py +13 -0
- video_diff_checker/report/evaluator.py +114 -0
- video_diff_checker/report/json_formatter.py +122 -0
- video_diff_checker/report/messages.py +82 -0
- video_diff_checker/report/models.py +121 -0
- video_diff_checker/report/runner.py +287 -0
- video_diff_checker/report/score_reader.py +87 -0
- video_diff_checker/report/segments_reader.py +97 -0
- video_diff_checker/report/text_formatter.py +129 -0
- video_diff_checker-0.1.0.dist-info/METADATA +216 -0
- video_diff_checker-0.1.0.dist-info/RECORD +40 -0
- video_diff_checker-0.1.0.dist-info/WHEEL +5 -0
- video_diff_checker-0.1.0.dist-info/entry_points.txt +2 -0
- video_diff_checker-0.1.0.dist-info/licenses/LICENSE +21 -0
- video_diff_checker-0.1.0.dist-info/top_level.txt +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
|