#!/usr/bin/env python3
"""
convert_transnetv2.py — verify and optimise the TransNetV2 scene-detector model.

The scene detector (src/nodes/scene_detector_node.hpp → ISceneDetector) consumes
the elya5/transnetv2 ONNX export with a fixed input contract:

    input  "input" : float32 [1, 100, 27, 48, 3]   RGB, channels-last, 0-255
    output "534"   : float32 [1, 100, 1]           per-frame boundary logits (used)
    output "535"   : float32 [1, 100, 1]           many-hot head (ignored)

This script provides the ORT/TRT conversion infra for that model:

  --verify   (default) assert the .onnx matches the contract above and run one
             dummy inference through onnxruntime, reporting output ranges.
  --ort-opt  write a graph-optimised .ort next to the model (ORT loads this
             faster; the runtime also caches its own under ./ort_cache).
  --trt      build a TensorRT engine via trtexec with the pinned 1x100x27x48x3
             profile (delegates to scripts/build_trt_engines.sh SCENE_MODEL).

Usage:
    python scripts/convert_transnetv2.py                    # verify default model
    python scripts/convert_transnetv2.py --ort-opt
    python scripts/convert_transnetv2.py --trt
    python scripts/convert_transnetv2.py --model path/to/transnetv2.onnx --verify
"""
import argparse
import os
import subprocess
import sys

ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
DEFAULT_MODEL = os.path.join(ROOT, "models", "transnetv2.onnx")

# The fixed contract the C++ scene detector depends on.
EXPECTED_INPUT_SHAPE = [1, 100, 27, 48, 3]
EXPECTED_OUTPUT_SHAPE = [1, 100, 1]


def verify(model_path: str) -> int:
    import numpy as np
    import onnx
    import onnxruntime as ort

    print(f"[verify] loading {model_path}")
    m = onnx.load(model_path, load_external_data=False)
    g = m.graph

    def shape(t):
        return [d.dim_value if d.HasField("dim_value") else d.dim_param
                for d in t.type.tensor_type.shape.dim]

    in_shape = shape(g.input[0])
    print(f"[verify] input '{g.input[0].name}': {in_shape}")
    if in_shape != EXPECTED_INPUT_SHAPE:
        print(f"[verify] ERROR: input shape {in_shape} != {EXPECTED_INPUT_SHAPE}")
        return 1

    out_names = [o.name for o in g.output]
    out0_shape = shape(g.output[0])
    print(f"[verify] outputs: {out_names}  primary '{out_names[0]}': {out0_shape}")
    if out0_shape != EXPECTED_OUTPUT_SHAPE:
        print(f"[verify] ERROR: primary output {out0_shape} != {EXPECTED_OUTPUT_SHAPE}")
        return 1

    # One dummy inference: a mid-grey clip should produce low boundary scores.
    sess = ort.InferenceSession(model_path, providers=["CPUExecutionProvider"])
    dummy = np.full(EXPECTED_INPUT_SHAPE, 128.0, dtype=np.float32)
    logits = sess.run([out_names[0]], {g.input[0].name: dummy})[0]
    probs = 1.0 / (1.0 + np.exp(-logits))
    print(f"[verify] dummy inference OK — boundary prob "
          f"min={probs.min():.4f} max={probs.max():.4f} mean={probs.mean():.4f}")
    print("[verify] contract matches the C++ ISceneDetector. ✓")
    return 0


def ort_opt(model_path: str) -> int:
    import onnxruntime as ort

    out_path = os.path.splitext(model_path)[0] + ".ort"
    print(f"[ort-opt] writing graph-optimised model → {out_path}")
    so = ort.SessionOptions()
    so.graph_optimization_level = ort.GraphOptimizationLevel.ORT_ENABLE_ALL
    so.optimized_model_filepath = out_path
    # Constructing the session triggers optimisation + serialisation.
    ort.InferenceSession(model_path, so, providers=["CPUExecutionProvider"])
    print(f"[ort-opt] done: {out_path}")
    return 0


def trt(model_path: str) -> int:
    script = os.path.join(ROOT, "scripts", "build_trt_engines.sh")
    print(f"[trt] delegating to {script} (SCENE_MODEL={model_path})")
    env = dict(os.environ, SCENE_MODEL=model_path)
    # build_trt_engines.sh also builds ArcFace/SCRFD; that's harmless (and a
    # useful sanity check), but if you only want the scene engine, run trtexec
    # directly with the profile printed below.
    print("[trt] equivalent standalone command:")
    print(f"  trtexec --onnx={model_path} --fp16 "
          f"--minShapes=input:1x100x27x48x3 "
          f"--optShapes=input:1x100x27x48x3 "
          f"--maxShapes=input:1x100x27x48x3 "
          f"--saveEngine=trt_cache/transnetv2.100x27x48.fp16.engine --useCudaGraph")
    return subprocess.call(["bash", script], env=env)


def main() -> int:
    ap = argparse.ArgumentParser(description=__doc__,
                                 formatter_class=argparse.RawDescriptionHelpFormatter)
    ap.add_argument("--model", default=DEFAULT_MODEL,
                    help=f"path to transnetv2.onnx (default: {DEFAULT_MODEL})")
    ap.add_argument("--verify", action="store_true", help="verify I/O contract (default)")
    ap.add_argument("--ort-opt", action="store_true", help="write optimised .ort")
    ap.add_argument("--trt", action="store_true", help="build a TensorRT engine")
    args = ap.parse_args()

    if not os.path.isfile(args.model):
        print(f"ERROR: model not found: {args.model}\n"
              f"Fetch it with: bash scripts/download_models.sh", file=sys.stderr)
        return 2

    # Default action is verify when nothing else is requested.
    if not (args.ort_opt or args.trt):
        args.verify = True

    rc = 0
    if args.verify:
        rc |= verify(args.model)
    if rc == 0 and args.ort_opt:
        rc |= ort_opt(args.model)
    if rc == 0 and args.trt:
        rc |= trt(args.model)
    return rc


if __name__ == "__main__":
    sys.exit(main())
