# -*- coding: utf-8 -*- """CLI 入口:python -m violation_detector <图片文件夹> [选项]""" import argparse import asyncio import logging import sys from . import __version__ from .config import DEFAULT_CONFIG, load_config, missing_fields from .logutil import log_file, setup_logging from .pipeline import run_detection from .report import build_report, organize_output logger = logging.getLogger("violation_detector.cli") def build_parser() -> argparse.ArgumentParser: p = argparse.ArgumentParser( prog="violation-detector", description="服装商品图合规检测:豆包视觉初筛 + DeepSeek 复检级联,输出 Excel 报表") p.add_argument("folder", help="待检测图片文件夹") p.add_argument("-o", "--output", default=None, help="报表输出目录(默认为图片文件夹)") p.add_argument("-c", "--config", default=None, help="配置文件路径(默认 config.ini)") p.add_argument("--prompt", default=None, help="提示词文件路径(覆盖配置)") p.add_argument("--mode", choices=["cascade", "doubao", "deepseek"], default=None, help="检测模式(默认取 config.ini [run] mode,未配置则 cascade):" "cascade=豆包初筛+DeepSeek复检,doubao=仅豆包,deepseek=仅 DeepSeek 全量") p.add_argument("--workers-ark", type=int, default=None, help="豆包并发数") p.add_argument("--workers-ds", type=int, default=None, help="DeepSeek 并发数") p.add_argument("--max-tokens", type=int, default=None, help="DeepSeek max_tokens(默认 5000,实测零失败最小档)") p.add_argument("--no-verify", action="store_true", help="跳过 DeepSeek 无违规两票复核") p.add_argument("--version", action="version", version=f"%(prog)s {__version__}") return p def main(argv=None) -> int: args = build_parser().parse_args(argv) log_path = setup_logging(console=True) logger.info("商品图合规检测工具 v%s 启动,日志文件:%s", __version__, log_path) cfg = load_config(args.config) if args.prompt: cfg.prompt_file = args.prompt if args.workers_ark: cfg.ark_workers = args.workers_ark if args.workers_ds: cfg.deepseek_workers = args.workers_ds if args.max_tokens: cfg.max_tokens = args.max_tokens mode = args.mode or cfg.mode # 复核开关:CLI --no-verify 强制关;否则跟随 config.ini [run] verify_clean verify = cfg.verify_clean and not args.no_verify missing = missing_fields(cfg, mode) if missing: print(f"配置不完整,请编辑 {DEFAULT_CONFIG} 填写以下必填项:") for m in missing: print(f" - {m}") return 2 try: rows, summary = asyncio.run(run_detection( args.folder, cfg, mode=mode, verify=verify)) except FileNotFoundError as e: logger.error("路径或文件不存在:%s", e) return 1 except KeyboardInterrupt: logger.warning("用户中断") return 130 except Exception: # noqa: BLE001 顶层兜底:完整堆栈进日志与控制台 logger.exception("检测过程发生未预期异常") print(f"\n检测失败,完整日志:{log_file()}") return 1 try: out_dir = args.output or args.folder ts_dir = organize_output(rows, args.folder, out_dir) xlsx = build_report(rows, str(ts_dir), {"mode": mode, **{ k: summary.get(k) for k in ("ds_calls", "ds_peak_cost", "ds_idle_cost")}}) except Exception: # noqa: BLE001 logger.exception("输出组织/报表生成失败") print(f"\n报表生成失败,完整日志:{log_file()}") return 1 print(f"\n分类输出目录:{ts_dir}") print(f"报表已生成:{xlsx}") return 0 if __name__ == "__main__": sys.exit(main())