You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
DroneDetector/scripts/capture_nn_results.py

224 lines
6.1 KiB
Python

This file contains ambiguous Unicode characters!

This file contains ambiguous Unicode characters that may be confused with others in your current locale. If your use case is intentional and legitimate, you can safely ignore this warning. Use the Escape button to highlight these characters.

#!/usr/bin/env python3
import argparse
import csv
import json
import os
import re
import subprocess
import sys
import time
from datetime import datetime, timezone
RESULT_RE = re.compile(
r"^(?P<docker_ts>\S+)\s+RESULT Модель (?P<model_id>\d+) с типом (?P<model_type>.+?): "
r"(?P<prediction>\S+) \(probability=(?P<probability>[-+]?\d+(?:\.\d+)?)\)\s*$"
)
FREQ_RE = re.compile(r"(\d{3,5})")
def parse_args():
parser = argparse.ArgumentParser(
description="Capture NN inference RESULT logs into CSV or JSONL until time or size limit."
)
parser.add_argument(
"--output",
required=True,
help="Output file path (.csv or .jsonl recommended).",
)
parser.add_argument(
"--format",
choices=("csv", "jsonl"),
default="csv",
help="Output format.",
)
parser.add_argument(
"--minutes",
type=float,
default=0.0,
help="Stop after this many minutes. 0 means no time limit.",
)
parser.add_argument(
"--max-bytes",
type=int,
default=3 * 1024 * 1024 * 1024,
help="Stop when output file reaches this size in bytes. Default: 3 GiB.",
)
parser.add_argument(
"--since",
default=None,
help="Optional docker logs --since value, e.g. 20m, 2h, 2026-05-04T12:00:00.",
)
parser.add_argument(
"--tail",
type=int,
default=0,
help="How many previous log lines to include before following. Default: 0.",
)
parser.add_argument(
"--compose-file",
default="deploy/docker/docker-compose.yml",
help="Path to docker compose file.",
)
parser.add_argument(
"--service",
default="dronedetector-nn-server",
help="Docker compose service name.",
)
parser.add_argument(
"--follow",
action="store_true",
help="Follow logs live. By default the script captures a finite history snapshot.",
)
return parser.parse_args()
def extract_freq(model_type):
matches = FREQ_RE.findall(model_type)
if not matches:
return ""
known_freqs = {"433", "750", "868", "915", "1200", "1500", "2400", "3300", "4500", "5200", "5800"}
for value in matches:
if value in known_freqs:
return value
return matches[0]
def parse_docker_timestamp(value):
try:
return datetime.fromisoformat(value.replace("Z", "+00:00"))
except ValueError:
return datetime.now(timezone.utc)
def open_output(path, fmt):
os.makedirs(os.path.dirname(os.path.abspath(path)), exist_ok=True)
fh = open(path, "a", encoding="utf-8", newline="")
writer = None
if fmt == "csv":
writer = csv.writer(fh)
if fh.tell() == 0:
writer.writerow(
[
"docker_timestamp",
"event_time_iso",
"event_time_epoch",
"freq",
"model_id",
"model_type",
"prediction",
"probability",
]
)
fh.flush()
return fh, writer
def write_record(fh, writer, fmt, record):
if fmt == "csv":
writer.writerow(
[
record["docker_timestamp"],
record["event_time_iso"],
record["event_time_epoch"],
record["freq"],
record["model_id"],
record["model_type"],
record["prediction"],
record["probability"],
]
)
else:
fh.write(json.dumps(record, ensure_ascii=False) + "\n")
fh.flush()
def build_command(args):
cmd = [
"docker",
"compose",
"-f",
args.compose_file,
"logs",
"--timestamps",
"--no-log-prefix",
args.service,
]
if args.since:
cmd[5:5] = ["--since", args.since]
if int(args.tail) > 0:
cmd[-1:-1] = ["--tail", str(args.tail)]
if args.follow:
cmd.insert(-1, "-f")
return cmd
def main():
args = parse_args()
deadline = time.time() + (args.minutes * 60.0) if args.minutes > 0 else None
fh, writer = open_output(args.output, args.format)
cmd = build_command(args)
print("Running:", " ".join(cmd), file=sys.stderr)
proc = subprocess.Popen(
cmd,
stdout=subprocess.PIPE,
stderr=subprocess.STDOUT,
text=True,
encoding="utf-8",
errors="replace",
bufsize=1,
)
captured = 0
try:
assert proc.stdout is not None
for line in proc.stdout:
if deadline is not None and time.time() >= deadline:
print("Stopping: time limit reached", file=sys.stderr)
break
match = RESULT_RE.match(line.rstrip("\n"))
if not match:
continue
docker_dt = parse_docker_timestamp(match.group("docker_ts"))
event_dt = docker_dt.astimezone()
model_type = match.group("model_type")
record = {
"docker_timestamp": match.group("docker_ts"),
"event_time_iso": event_dt.isoformat(timespec="seconds"),
"event_time_epoch": round(docker_dt.timestamp(), 3),
"freq": extract_freq(model_type),
"model_id": int(match.group("model_id")),
"model_type": model_type,
"prediction": match.group("prediction"),
"probability": float(match.group("probability")),
}
write_record(fh, writer, args.format, record)
captured += 1
if fh.tell() >= args.max_bytes:
print("Stopping: file size limit reached", file=sys.stderr)
break
finally:
try:
proc.terminate()
except Exception:
pass
try:
proc.wait(timeout=5)
except Exception:
try:
proc.kill()
except Exception:
pass
fh.close()
print(f"Captured {captured} inference results into {args.output}", file=sys.stderr)
if __name__ == "__main__":
main()