namespace Poe2Trade.Screen; using System.Diagnostics; using System.Text.Json; using System.Text.Json.Serialization; using OpenCvSharp; using Serilog; /// /// Manages a persistent Python subprocess for YOLO object detection. /// Lazy-starts on first request; reuses the process for subsequent calls. /// Same stdin/stdout JSON-per-line protocol as PythonOcrBridge. /// class PythonDetectBridge : IDisposable { private static readonly JsonSerializerOptions JsonOptions = new() { PropertyNamingPolicy = JsonNamingPolicy.CamelCase, DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull, }; private Process? _proc; private readonly string _daemonScript; private readonly string _pythonExe; private readonly object _lock = new(); public PythonDetectBridge() { _daemonScript = Path.GetFullPath(Path.Combine("tools", "python-detect", "daemon.py")); var venvPython = Path.GetFullPath(Path.Combine("tools", "python-detect", ".venv", "Scripts", "python.exe")); _pythonExe = File.Exists(venvPython) ? venvPython : "python"; } /// /// Run YOLO detection on a BGR Mat. Returns parsed detection results. /// public DetectResponse Detect(Mat bgrMat, float conf = 0.3f, float iou = 0.45f, int imgsz = 640, string? model = null) { EnsureRunning(); var imageBytes = bgrMat.ToBytes(".jpg", [(int)ImwriteFlags.JpegQuality, 95]); var imageBase64 = Convert.ToBase64String(imageBytes); var req = new Dictionary { ["cmd"] = "detect", ["imageBase64"] = imageBase64, ["conf"] = conf, ["iou"] = iou, ["imgsz"] = imgsz, ["model"] = model, }; return SendRequest(req); } private DetectResponse SendRequest(object req) { var json = JsonSerializer.Serialize(req, JsonOptions); string responseLine; lock (_lock) { _proc!.StandardInput.WriteLine(json); _proc.StandardInput.Flush(); responseLine = _proc.StandardOutput.ReadLine() ?? throw new Exception("Python detect daemon returned null"); } var resp = JsonSerializer.Deserialize(responseLine, JsonOptions); if (resp == null) throw new Exception("Failed to parse Python detect response"); if (!resp.Ok) throw new Exception(resp.Error ?? "Python detect failed"); return new DetectResponse { Count = resp.Count, InferenceMs = resp.InferenceMs, Detections = resp.Detections ?? [], }; } private void EnsureRunning() { if (_proc != null && !_proc.HasExited) return; _proc?.Dispose(); _proc = null; if (!File.Exists(_daemonScript)) throw new Exception($"Python detect daemon not found at {_daemonScript}"); Log.Information("Spawning Python detect daemon: {Python} {Script}", _pythonExe, _daemonScript); var proc = new Process { StartInfo = new ProcessStartInfo { FileName = _pythonExe, Arguments = $"\"{_daemonScript}\"", UseShellExecute = false, RedirectStandardInput = true, RedirectStandardOutput = true, RedirectStandardError = true, CreateNoWindow = true, } }; proc.ErrorDataReceived += (_, e) => { if (!string.IsNullOrEmpty(e.Data)) Log.Debug("[python-detect] {Line}", e.Data); }; try { proc.Start(); proc.BeginErrorReadLine(); // Wait for ready signal (up to 60s for CUDA warmup) var readyTask = Task.Run(() => proc.StandardOutput.ReadLine()); if (!readyTask.Wait(TimeSpan.FromSeconds(60))) throw new Exception("Python detect daemon timed out waiting for ready signal"); var readyLine = readyTask.Result; if (readyLine == null) throw new Exception("Python detect daemon exited before ready signal"); var ready = JsonSerializer.Deserialize(readyLine, JsonOptions); if (ready?.Ready != true) throw new Exception($"Python detect daemon did not send ready signal: {readyLine}"); } catch { try { if (!proc.HasExited) proc.Kill(); } catch { /* best effort */ } proc.Dispose(); throw; } _proc = proc; Log.Information("Python detect daemon ready"); } public void Dispose() { if (_proc != null && !_proc.HasExited) { try { _proc.StandardInput.Close(); _proc.WaitForExit(3000); if (!_proc.HasExited) _proc.Kill(); } catch { /* ignore */ } } _proc?.Dispose(); _proc = null; } // -- Response types -- public class DetectResponse { public int Count { get; set; } public float InferenceMs { get; set; } public List Detections { get; set; } = []; } public class Detection { [JsonPropertyName("class")] public string ClassName { get; set; } = ""; public int ClassId { get; set; } public float Confidence { get; set; } public int X { get; set; } public int Y { get; set; } public int Width { get; set; } public int Height { get; set; } public int Cx { get; set; } public int Cy { get; set; } } private class PythonDetectResponse { public bool Ok { get; set; } public bool? Ready { get; set; } public string? Error { get; set; } public int Count { get; set; } public float InferenceMs { get; set; } public List? Detections { get; set; } } }