#!/usr/bin/env python3 """Batch inference runner for downloaded Feishu issue data.""" from __future__ import annotations import argparse import json import re import subprocess import sys from dataclasses import dataclass from datetime import datetime from pathlib import Path from typing import Iterable ROOT = Path(__file__).resolve().parents[2] DEFAULT_DOWNLOAD_ROOT = Path("/data1/dongying/Mono3d/G1Q3/feishu_project/downloaded_issue_data") DEFAULT_OUTPUT_ROOT = Path("/data1/dongying/Mono3d/G1Q3/feishu_project/inference_issue_data") DEFAULT_INFERENCE_SCRIPT = ROOT / "tools" / "model_inference" / "core" / "run_two_roi_exported_onnx_infer.py" DEFAULT_PYTHON_BIN = Path("/deeplearning_team/ydong/dongying/miniconda/envs/dev/bin/python") ISSUE_DIR_RE = re.compile(r"^issue_(\d+)$") SIGNED_INTEGER_RE = re.compile(r"^[+-]?\d+$") FRAME_ID_CAMERA4_RE = re.compile(r"camera4\s*:\s*([+-]?\d+)", re.IGNORECASE) FRAME_ID_ANY_CAMERA_RE = re.compile(r"(camera\d+)\s*:\s*([+-]?\d+)", re.IGNORECASE) @dataclass(frozen=True) class TargetFrame: camera: str frame_id: int raw_text: str @dataclass(frozen=True) class InferenceCase: issue_id: int issue_dir: Path case_dir: Path camera4_bin: Path relative_case_dir: Path output_dir: Path frame_window_source: str target_frame_text: str | None = None target_frame_id: int | None = None requested_frame_index_start: int | None = None requested_frame_index_end: int | None = None requested_frame_id_start: int | None = None requested_frame_id_end: int | None = None @dataclass class CaseResult: issue_id: int issue_dir: str case_dir: str camera4_bin: str relative_case_dir: str output_dir: str status: str detail: str command: list[str] log_path: str | None = None frame_window_source: str | None = None target_frame_text: str | None = None target_frame_id: int | None = None requested_frame_index_start: int | None = None requested_frame_index_end: int | None = None requested_frame_id_start: int | None = None requested_frame_id_end: int | None = None def to_dict(self) -> dict: return { "issue_id": self.issue_id, "issue_dir": self.issue_dir, "case_dir": self.case_dir, "camera4_bin": self.camera4_bin, "relative_case_dir": self.relative_case_dir, "output_dir": self.output_dir, "status": self.status, "detail": self.detail, "command": self.command, "log_path": self.log_path, "frame_window_source": self.frame_window_source, "target_frame_text": self.target_frame_text, "target_frame_id": self.target_frame_id, "requested_frame_index_start": self.requested_frame_index_start, "requested_frame_index_end": self.requested_frame_index_end, "requested_frame_id_start": self.requested_frame_id_start, "requested_frame_id_end": self.requested_frame_id_end, } def parse_args() -> argparse.Namespace: parser = argparse.ArgumentParser( description="Run exported-model inference on all downloaded issue-data camera4.bin cases." ) parser.add_argument( "--download-root", default=str(DEFAULT_DOWNLOAD_ROOT), help="Root directory produced by download_issue_data.py.", ) parser.add_argument( "--output-root", default=str(DEFAULT_OUTPUT_ROOT), help="Root directory where inference outputs will be written.", ) parser.add_argument( "--manifest-path", default="", help="Optional explicit manifest JSON path. Defaults to /inference_manifest.json", ) parser.add_argument( "--python-bin", default=str(DEFAULT_PYTHON_BIN), help="Python interpreter used to launch run_two_roi_exported_onnx_infer.py.", ) parser.add_argument( "--inference-script", default=str(DEFAULT_INFERENCE_SCRIPT), help="Path to run_two_roi_exported_onnx_infer.py.", ) parser.add_argument( "--issue-json", default="", help="Optional Feishu issue export JSON used to resolve 问题发生frameid windows.", ) parser.add_argument( "--issue-id", action="append", dest="issue_ids", type=int, help="Optional issue id filter. Can be repeated.", ) parser.add_argument("--video-stride", type=int, default=1) parser.add_argument("--max-images", type=int, default=0) parser.add_argument("--frame-index-start", type=int, default=None) parser.add_argument("--frame-index-end", type=int, default=None) parser.add_argument("--frame-id-start", type=int, default=None) parser.add_argument("--frame-id-end", type=int, default=None) parser.add_argument("--target-frame-id", type=int, default=None) parser.add_argument("--frame-before", type=int, default=100) parser.add_argument("--frame-after", type=int, default=100) parser.add_argument("--use-issue-frame-window", action="store_true") parser.add_argument( "--missing-issue-frame-policy", choices=("full", "skip"), default="full", help="How to handle cases without a usable 问题发生frameid when --use-issue-frame-window is enabled.", ) parser.add_argument("--exported-model", type=str, default="") parser.add_argument("--device", type=str, default="") parser.add_argument("--providers", nargs="*", default=None) parser.add_argument("--enable-attr", action="store_true") parser.add_argument("--enable-cross-class-merge-prior", action="store_true") parser.add_argument("--save-aggregate-predictions", action="store_true") parser.add_argument( "--inference-arg", action="append", default=[], help="Extra argument forwarded to run_two_roi_exported_onnx_infer.py. Can be repeated.", ) parser.add_argument("--skip-existing", action="store_true") parser.add_argument("--dry-run", action="store_true") args = parser.parse_args() if args.frame_before < 0: parser.error("--frame-before must be greater than or equal to 0") if args.frame_after < 0: parser.error("--frame-after must be greater than or equal to 0") if ( args.frame_index_start is not None and args.frame_index_end is not None and args.frame_index_start > args.frame_index_end ): parser.error("--frame-index-start must be less than or equal to --frame-index-end") if ( args.frame_id_start is not None and args.frame_id_end is not None and args.frame_id_start > args.frame_id_end ): parser.error("--frame-id-start must be less than or equal to --frame-id-end") if args.use_issue_frame_window and not args.issue_json: parser.error("--issue-json is required when --use-issue-frame-window is enabled") return args def ensure_dir(path: Path, dry_run: bool) -> None: if dry_run: return path.mkdir(parents=True, exist_ok=True) def log_progress(message: str) -> None: timestamp = datetime.now().astimezone().strftime("%Y-%m-%d %H:%M:%S") print(f"[run_issue_data_inference {timestamp}] {message}", flush=True) def compact_text(value: object, max_len: int = 96) -> str: text = "" if value is None else str(value).strip() text = re.sub(r"\s+", " ", text) if len(text) <= max_len: return text return f"{text[: max_len - 3]}..." def parse_issue_id_from_path(path: Path) -> int | None: for part in path.parts: match = ISSUE_DIR_RE.match(part) if match: return int(match.group(1)) return None def load_issue_items(path: Path) -> list[dict]: payload = json.loads(path.read_text(encoding="utf-8")) return payload["items"] def parse_target_frame(frame_text: object) -> tuple[TargetFrame | None, str]: text = "" if frame_text is None else str(frame_text).strip() if not text: return None, "missing 问题发生frameid" camera4_match = FRAME_ID_CAMERA4_RE.search(text) if camera4_match: frame_id = int(camera4_match.group(1)) if frame_id <= 0: return None, f"non-positive 问题发生frameid: {text!r}" return TargetFrame(camera="camera4", frame_id=frame_id, raw_text=text), "" if SIGNED_INTEGER_RE.fullmatch(text): frame_id = int(text) if frame_id <= 0: return None, f"non-positive 问题发生frameid: {text!r}" return TargetFrame(camera="any", frame_id=frame_id, raw_text=text), "" any_camera_match = FRAME_ID_ANY_CAMERA_RE.search(text) if any_camera_match: frame_id = int(any_camera_match.group(2)) if frame_id <= 0: return None, f"non-positive 问题发生frameid: {text!r}" return TargetFrame(camera=any_camera_match.group(1).lower(), frame_id=frame_id, raw_text=text), "" return None, f"unparseable 问题发生frameid: {text!r}" def build_issue_item_lookup(issue_json: Path) -> dict[int, dict]: return {int(item["id"]): item for item in load_issue_items(issue_json)} def has_manual_frame_window(args: argparse.Namespace) -> bool: return any( value is not None for value in ( args.frame_index_start, args.frame_index_end, args.frame_id_start, args.frame_id_end, args.target_frame_id, ) ) def build_case_result( case: InferenceCase, *, status: str, detail: str, command: list[str] | None = None, log_path: str | None = None, ) -> CaseResult: return CaseResult( issue_id=case.issue_id, issue_dir=str(case.issue_dir), case_dir=str(case.case_dir), camera4_bin=str(case.camera4_bin), relative_case_dir=str(case.relative_case_dir), output_dir=str(case.output_dir), status=status, detail=detail, command=command or [], log_path=log_path, frame_window_source=case.frame_window_source, target_frame_text=case.target_frame_text, target_frame_id=case.target_frame_id, requested_frame_index_start=case.requested_frame_index_start, requested_frame_index_end=case.requested_frame_index_end, requested_frame_id_start=case.requested_frame_id_start, requested_frame_id_end=case.requested_frame_id_end, ) def discover_cases(download_root: Path, output_root: Path, issue_filter: set[int] | None) -> list[InferenceCase]: cases: list[InferenceCase] = [] for camera4_bin in sorted(download_root.rglob("camera4.bin")): if camera4_bin.parent.name != "sigmastar.1": continue try: relative_camera = camera4_bin.relative_to(download_root) except ValueError: continue issue_id = parse_issue_id_from_path(relative_camera) if issue_id is None: continue if issue_filter and issue_id not in issue_filter: continue case_dir = camera4_bin.parent.parent relative_case_dir = case_dir.relative_to(download_root) issue_dir = download_root / f"issue_{issue_id}" output_dir = output_root / relative_case_dir cases.append( InferenceCase( issue_id=issue_id, issue_dir=issue_dir, case_dir=case_dir, camera4_bin=camera4_bin, relative_case_dir=relative_case_dir, output_dir=output_dir, frame_window_source="full", ) ) return cases def apply_frame_windows( cases: list[InferenceCase], args: argparse.Namespace, issue_item_lookup: dict[int, dict], ) -> tuple[list[InferenceCase], list[CaseResult]]: prepared_cases: list[InferenceCase] = [] skipped_results: list[CaseResult] = [] for case in cases: prepared_case = InferenceCase( issue_id=case.issue_id, issue_dir=case.issue_dir, case_dir=case.case_dir, camera4_bin=case.camera4_bin, relative_case_dir=case.relative_case_dir, output_dir=case.output_dir, frame_window_source="full", ) if has_manual_frame_window(args): requested_frame_id_start = args.frame_id_start requested_frame_id_end = args.frame_id_end if args.target_frame_id is not None: if requested_frame_id_start is None: requested_frame_id_start = max(0, args.target_frame_id - args.frame_before) if requested_frame_id_end is None: requested_frame_id_end = args.target_frame_id + args.frame_after prepared_case = InferenceCase( issue_id=case.issue_id, issue_dir=case.issue_dir, case_dir=case.case_dir, camera4_bin=case.camera4_bin, relative_case_dir=case.relative_case_dir, output_dir=case.output_dir, frame_window_source="manual", target_frame_id=args.target_frame_id, requested_frame_index_start=args.frame_index_start, requested_frame_index_end=args.frame_index_end, requested_frame_id_start=requested_frame_id_start, requested_frame_id_end=requested_frame_id_end, ) prepared_cases.append(prepared_case) continue if not args.use_issue_frame_window: prepared_cases.append(prepared_case) continue item = issue_item_lookup.get(case.issue_id) if item is None: if args.missing_issue_frame_policy == "skip": skipped_results.append( build_case_result( prepared_case, status="skipped_missing_issue_record", detail="issue id not found in issue_json", ) ) continue prepared_case = InferenceCase( issue_id=case.issue_id, issue_dir=case.issue_dir, case_dir=case.case_dir, camera4_bin=case.camera4_bin, relative_case_dir=case.relative_case_dir, output_dir=case.output_dir, frame_window_source="full_missing_issue_record", ) prepared_cases.append(prepared_case) continue target_frame, target_frame_error = parse_target_frame(item.get("问题发生frameid")) if target_frame is None: if args.missing_issue_frame_policy == "skip": skipped_results.append( build_case_result( prepared_case, status="skipped_missing_issue_frame", detail=target_frame_error, ) ) continue prepared_case = InferenceCase( issue_id=case.issue_id, issue_dir=case.issue_dir, case_dir=case.case_dir, camera4_bin=case.camera4_bin, relative_case_dir=case.relative_case_dir, output_dir=case.output_dir, frame_window_source=( "full_missing_issue_frame" if target_frame_error.startswith("missing ") else "full_invalid_issue_frame" ), ) prepared_cases.append(prepared_case) continue if target_frame.camera not in {"camera4", "any"}: if args.missing_issue_frame_policy == "skip": skipped_results.append( build_case_result( prepared_case, status="skipped_unsupported_issue_camera", detail=f"unsupported target camera for window inference: {target_frame.camera}", ) ) continue prepared_case = InferenceCase( issue_id=case.issue_id, issue_dir=case.issue_dir, case_dir=case.case_dir, camera4_bin=case.camera4_bin, relative_case_dir=case.relative_case_dir, output_dir=case.output_dir, frame_window_source="full_unsupported_issue_camera", target_frame_text=target_frame.raw_text, ) prepared_cases.append(prepared_case) continue prepared_case = InferenceCase( issue_id=case.issue_id, issue_dir=case.issue_dir, case_dir=case.case_dir, camera4_bin=case.camera4_bin, relative_case_dir=case.relative_case_dir, output_dir=case.output_dir, frame_window_source="issue_frame", target_frame_text=target_frame.raw_text, target_frame_id=target_frame.frame_id, requested_frame_id_start=max(0, target_frame.frame_id - args.frame_before), requested_frame_id_end=target_frame.frame_id + args.frame_after, ) prepared_cases.append(prepared_case) return prepared_cases, skipped_results def has_existing_outputs(output_dir: Path) -> bool: if (output_dir / "predictions_merged.json").is_file(): return True merge_json_dir = output_dir / "predictions" / "merge" if merge_json_dir.is_dir(): for child in merge_json_dir.iterdir(): if child.is_file(): return True return False def build_command(case: InferenceCase, args: argparse.Namespace) -> list[str]: command = [ str(Path(args.python_bin).resolve()), str(Path(args.inference_script).resolve()), "--video-case-dir", str(case.camera4_bin), "--output-dir", str(case.output_dir), "--video-stride", str(args.video_stride), ] if args.max_images > 0: command.extend(["--max-images", str(args.max_images)]) if case.target_frame_id is not None: command.extend(["--target-frame-id", str(case.target_frame_id)]) if case.requested_frame_index_start is not None: command.extend(["--frame-index-start", str(case.requested_frame_index_start)]) if case.requested_frame_index_end is not None: command.extend(["--frame-index-end", str(case.requested_frame_index_end)]) if case.requested_frame_id_start is not None: command.extend(["--frame-id-start", str(case.requested_frame_id_start)]) if case.requested_frame_id_end is not None: command.extend(["--frame-id-end", str(case.requested_frame_id_end)]) if args.exported_model: command.extend(["--exported-model", args.exported_model]) if args.device: command.extend(["--device", args.device]) if args.providers: command.extend(["--providers", *args.providers]) if args.enable_attr: command.append("--enable-attr") if args.enable_cross_class_merge_prior: command.append("--enable-cross-class-merge-prior") if args.save_aggregate_predictions: command.append("--save-aggregate-predictions") for extra_arg in args.inference_arg: command.append(extra_arg) return command def write_case_status_files(case: InferenceCase, command: list[str], log_text: str, dry_run: bool) -> str: status_dir = case.output_dir / "_status" if dry_run: return str(status_dir / "inference.log") ensure_dir(status_dir, dry_run=False) (status_dir / "command.txt").write_text(" ".join(command) + "\n", encoding="utf-8") log_path = status_dir / "inference.log" log_path.write_text(log_text, encoding="utf-8") return str(log_path) def summarize_process_output(completed: subprocess.CompletedProcess[str]) -> str: stdout = completed.stdout.strip() stderr = completed.stderr.strip() if completed.returncode == 0: if stdout: return stdout.splitlines()[-1] return "inference completed" if stderr: return stderr.splitlines()[-1] if stdout: return stdout.splitlines()[-1] return f"inference failed with return code {completed.returncode}" def run_case(case: InferenceCase, args: argparse.Namespace) -> CaseResult: command = build_command(case, args) if args.skip_existing and has_existing_outputs(case.output_dir): return build_case_result( case, status="skipped_existing", detail="existing inference outputs found", command=command, log_path=None, ) if args.dry_run: return build_case_result( case, status="planned", detail="would run inference", command=command, log_path=str(case.output_dir / "_status" / "inference.log"), ) ensure_dir(case.output_dir, dry_run=False) completed = subprocess.run( command, cwd=str(ROOT), check=False, capture_output=True, text=True, encoding="utf-8", ) combined_log = "\n".join( part for part in (completed.stdout.strip(), completed.stderr.strip()) if part ) log_path = write_case_status_files(case, command, combined_log + ("\n" if combined_log else ""), dry_run=False) status = "success" if completed.returncode == 0 else "failed" detail = summarize_process_output(completed) return build_case_result( case, status=status, detail=detail, command=command, log_path=log_path, ) def build_manifest( args: argparse.Namespace, download_root: Path, output_root: Path, discovered_cases: list[InferenceCase], results: list[CaseResult], ) -> dict: summary: dict[str, int] = {} for result in results: summary[result.status] = summary.get(result.status, 0) + 1 return { "generated_at": datetime.now().astimezone().isoformat(timespec="seconds"), "download_root": str(download_root), "output_root": str(output_root), "python_bin": str(Path(args.python_bin).resolve()), "inference_script": str(Path(args.inference_script).resolve()), "dry_run": args.dry_run, "skip_existing": args.skip_existing, "issue_filter": args.issue_ids or [], "issue_json": args.issue_json, "use_issue_frame_window": args.use_issue_frame_window, "missing_issue_frame_policy": args.missing_issue_frame_policy, "video_stride": args.video_stride, "max_images": args.max_images, "frame_index_start": args.frame_index_start, "frame_index_end": args.frame_index_end, "frame_id_start": args.frame_id_start, "frame_id_end": args.frame_id_end, "target_frame_id": args.target_frame_id, "frame_before": args.frame_before, "frame_after": args.frame_after, "exported_model": args.exported_model, "device": args.device, "providers": args.providers or [], "enable_attr": args.enable_attr, "enable_cross_class_merge_prior": args.enable_cross_class_merge_prior, "save_aggregate_predictions": args.save_aggregate_predictions, "inference_args": args.inference_arg, "total_cases": len(discovered_cases), "summary": summary, "cases": [result.to_dict() for result in results], } def print_summary(manifest: dict) -> None: print(f"download_root: {manifest['download_root']}") print(f"output_root: {manifest['output_root']}") print(f"dry_run: {manifest['dry_run']}") if manifest["use_issue_frame_window"]: print( "issue_frame_window: " f"enabled issue_json={manifest['issue_json']} " f"before={manifest['frame_before']} after={manifest['frame_after']} " f"missing_policy={manifest['missing_issue_frame_policy']}" ) elif ( manifest["target_frame_id"] is not None or manifest["frame_id_start"] is not None or manifest["frame_id_end"] is not None or manifest["frame_index_start"] is not None or manifest["frame_index_end"] is not None ): print( "manual_frame_window: " f"target_frame_id={manifest['target_frame_id']} " f"frame_id=[{manifest['frame_id_start'] if manifest['frame_id_start'] is not None else '-inf'}, " f"{manifest['frame_id_end'] if manifest['frame_id_end'] is not None else '+inf'}] " f"frame_index=[{manifest['frame_index_start'] if manifest['frame_index_start'] is not None else '-inf'}, " f"{manifest['frame_index_end'] if manifest['frame_index_end'] is not None else '+inf'}]" ) print(f"total_cases: {manifest['total_cases']}") for status, count in sorted(manifest["summary"].items()): print(f"{status}: {count}") def main() -> int: args = parse_args() download_root = Path(args.download_root).resolve() output_root = Path(args.output_root).resolve() manifest_path = ( Path(args.manifest_path).resolve() if args.manifest_path else output_root / "inference_manifest.json" ) issue_filter = set(args.issue_ids) if args.issue_ids else None cases = discover_cases(download_root, output_root, issue_filter) if not cases: raise FileNotFoundError(f"No */sigmastar.1/camera4.bin cases found under {download_root}") issue_item_lookup: dict[int, dict] = {} if args.use_issue_frame_window: issue_item_lookup = build_issue_item_lookup(Path(args.issue_json).resolve()) prepared_cases, skipped_results = apply_frame_windows(cases, args, issue_item_lookup) log_progress( "cases discovered: " f"total={len(cases)} runnable={len(prepared_cases)} skipped_precheck={len(skipped_results)}" ) for skipped_result in skipped_results: log_progress( f"skip issue_{skipped_result.issue_id} {compact_text(skipped_result.relative_case_dir)}: " f"{skipped_result.status} ({compact_text(skipped_result.detail, max_len=72)})" ) ensure_dir(output_root, dry_run=args.dry_run) results = list(skipped_results) total_prepared_cases = len(prepared_cases) for index, case in enumerate(prepared_cases, start=1): log_progress( f"[{index}/{total_prepared_cases}] issue_{case.issue_id} " f"{compact_text(case.relative_case_dir)}: start (window={case.frame_window_source})" ) case_result = run_case(case, args) results.append(case_result) log_progress( f"[{index}/{total_prepared_cases}] issue_{case.issue_id} " f"{compact_text(case.relative_case_dir)}: " f"{case_result.status} ({compact_text(case_result.detail, max_len=72)})" ) manifest = build_manifest(args, download_root, output_root, cases, results) if not args.dry_run: ensure_dir(manifest_path.parent, dry_run=False) manifest_path.write_text( json.dumps(manifest, ensure_ascii=False, indent=2) + "\n", encoding="utf-8", ) print_summary(manifest) if args.dry_run: print(f"manifest (not written in dry-run): {manifest_path}") else: print(f"manifest: {manifest_path}") return 0 if __name__ == "__main__": sys.exit(main())