@camstack/addon-pipeline 1.2.621 → 1.2.623

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (49) hide show
  1. package/dist/audio-analyzer/index.js +3 -3
  2. package/dist/audio-analyzer/index.mjs +3 -3
  3. package/dist/{custom-model-catalog-Il8UY4D4.js → custom-model-catalog-BTBdgN2p.js} +29 -16
  4. package/dist/{custom-model-catalog-0_SnSXzN.mjs → custom-model-catalog-D5zTw1LU.mjs} +29 -10
  5. package/dist/detection-pipeline/index.js +67 -416
  6. package/dist/detection-pipeline/index.mjs +91 -440
  7. package/dist/{dist-ClkumZgv.js → dist-BVo3wa7s.js} +711 -302
  8. package/dist/{dist-DHqkQhTQ.mjs → dist-opSx6v5i.mjs} +651 -296
  9. package/dist/{model-catalogs-BagalaYp.js → model-catalogs-CQytR2AN.js} +1 -1
  10. package/dist/{model-catalogs-Z1GZAhSb.mjs → model-catalogs-rIvLUVlw.mjs} +1 -1
  11. package/dist/motion-wasm/index.js +1 -1
  12. package/dist/motion-wasm/index.mjs +1 -1
  13. package/dist/{node-CkJLxlWI.mjs → node-CGIYuJMN.mjs} +1 -1
  14. package/dist/{node-0O_61WVW.js → node-DqLsuN_A.js} +1 -1
  15. package/dist/pipeline-runner/index.js +115 -460
  16. package/dist/pipeline-runner/index.mjs +108 -453
  17. package/dist/{process-memory-Dj3UgL1e.mjs → process-memory-Bd-TuWfM.mjs} +1 -1
  18. package/dist/{process-memory-DdIOhjYB.js → process-memory-DyxG-0mG.js} +1 -1
  19. package/dist/recorder/index.js +2 -2
  20. package/dist/recorder/index.mjs +2 -2
  21. package/dist/{segment-demux-js-BePzacki.js → segment-demux-js-B4bfTQpK.js} +1 -1
  22. package/dist/{segment-demux-js-Bv0t0-tQ.mjs → segment-demux-js-CdVRzwSd.mjs} +1 -1
  23. package/dist/session-decode/decode-worker-main.js +12 -21
  24. package/dist/session-decode/decode-worker-main.mjs +12 -21
  25. package/dist/stream-broker/_stub.js +2 -2
  26. package/dist/stream-broker/{_virtual_mf-localSharedImportMap___mfe_internal__addon_stream_broker_widgets-dNk74ahz.mjs → _virtual_mf-localSharedImportMap___mfe_internal__addon_stream_broker_widgets-DF3FGCR8.mjs} +2 -2
  27. package/dist/stream-broker/_virtual_mf___mfe_internal__addon_stream_broker_widgets__loadShare___mf_0_camstack_mf_1_types__loadShare__.js-oUZNn2Kr.mjs +26 -0
  28. package/dist/stream-broker/_virtual_mf___mfe_internal__addon_stream_broker_widgets__loadShare___mf_0_camstack_mf_1_ui_mf_2_library__loadShare__.js-Bb-ETNGC.mjs +26 -0
  29. package/dist/stream-broker/demux-worker-child.js +1 -1
  30. package/dist/stream-broker/demux-worker-child.mjs +1 -1
  31. package/dist/stream-broker/{hostInit-CH5g78CJ.mjs → hostInit-BiyFzSqq.mjs} +2 -2
  32. package/dist/stream-broker/index.js +3 -3
  33. package/dist/stream-broker/index.mjs +3 -3
  34. package/dist/stream-broker/remoteEntry.js +1 -1
  35. package/dist/{worker-protocol-D_NzPcnh.mjs → worker-protocol-DngGEAT2.js} +102 -2
  36. package/dist/{worker-protocol-C-G8qmye.js → worker-protocol-_avq7y8O.mjs} +67 -25
  37. package/package.json +1 -1
  38. package/python/accelerator_facts.py +275 -0
  39. package/python/coreml_load_probe.py +105 -15
  40. package/python/inference_pool.py +19 -2
  41. package/python/openvino_compile_probe.py +72 -18
  42. package/python/postprocessors/ssd.py +12 -4
  43. package/python/postprocessors/test_ssd.py +64 -0
  44. package/python/test_accelerator_facts.py +105 -0
  45. package/python/test_coreml_load_probe.py +83 -0
  46. package/python/test_openvino_compile_probe.py +114 -0
  47. package/python/webgpu_isolated.py +19 -0
  48. package/dist/stream-broker/_virtual_mf___mfe_internal__addon_stream_broker_widgets__loadShare___mf_0_camstack_mf_1_types__loadShare__.js-2KkGoQ55.mjs +0 -26
  49. package/dist/stream-broker/_virtual_mf___mfe_internal__addon_stream_broker_widgets__loadShare___mf_0_camstack_mf_1_ui_mf_2_library__loadShare__.js-Dx-WrMx0.mjs +0 -26
@@ -1,9 +1,9 @@
1
1
  import { r as __toESM } from "../chunk-DnnnRqeS.mjs";
2
- import { $n as maskUrlCredentials, An as invocationFromEncodeProfile, At as ffmpegRate, D as G711_SCALE_CORRECTION_DB, Fn as BOOT_RECOVERY_BACKOFF_MS, Hn as createEvent, I as RATE_CONTROL_RELAXED, In as BaseAddon, J as addonWidgetsSourceCapability, Kn as expandAudioChunkToF32le, L as RATE_CONTROL_TIGHT, Ln as CAM_PROFILE_ORDER, Mn as DeviceFeature, Mt as gifDeriveSpeed, Nn as DeviceType, Nt as gifFromMp4Args, On as buildFfmpegArgs, Pn as errMsg, Qn as makeSourceBrokerId, Rn as CamProfileSchema, Rt as loadContributionCapability, St as egressTransportFromRequest, Tn as AUDIO_PRESETS, Ut as memoizePinnedFfmpeg, W as RingBuffer, Xn as isSecretMarker, Yn as isEvent, Z as armRemovalReconcile, Zn as makeProfileBrokerId, _r as EventCategory, ar as _enum, bn as streamBrokerCapability, c as CLIP_STILL_REASON_CODE_HEADER, cn as resolveEgressDecodeHwAccel, cr as discriminatedUnion, dr as number, dt as declareLogChannel, er as nodePin, fr as object, gr as union, hr as string, it as cameraStreamsCapability, jn as isSoftwareDecode, lt as createHwAccelCache, mr as record, o as BatteryStatusSchema, or as array, ot as clipExportFailureOf, pt as deriveBatteryPresence, s as CLIP_BROKER_PACED_RATES, sr as boolean, tn as rankStreams, tr as parseProfileBrokerId, tt as batteryCapability, ur as literal, ut as createLogChannelsProvider, vt as deviceRemovalHookCapability, w as EncodeProfileSchema, wn as webrtcSessionCapability, xt as egressTranscodeSharingKey, zt as logChannelsCapability } from "../dist-DHqkQhTQ.mjs";
3
- import { a as defaultFfmpegSpawn, i as NO_COST_CLAIM, n as Fmp4FragmentChild, r as Fmp4FragmentPlane, s as probeFfmpegBinary, t as ChildCostRegistry } from "../node-CkJLxlWI.mjs";
2
+ import { Bn as isSoftwareDecode, Cr as record, D as G711_SCALE_CORRECTION_DB, Er as EventCategory, Et as egressTransportFromRequest, Gn as BaseAddon, Hn as DeviceType, Jt as memoizePinnedFfmpeg, K as RingBuffer, Kn as CAM_PROFILE_ORDER, L as RATE_CONTROL_RELAXED, Ln as buildFfmpegArgs, Lt as gifDeriveSpeed, Nn as webrtcSessionCapability, Pn as AUDIO_PRESETS, Pt as ffmpegRate, R as RATE_CONTROL_TIGHT, Rt as gifFromMp4Args, St as deviceRemovalHookCapability, Tr as union, Tt as egressTranscodeSharingKey, Un as errMsg, Ut as loadContributionCapability, Vn as DeviceFeature, Wn as BOOT_RECOVERY_BACKOFF_MS, Wt as logChannelsCapability, X as addonWidgetsSourceCapability, Zn as createEvent, _r as discriminatedUnion, _t as deriveBatteryPresence, ar as isSecretMarker, br as number, c as CLIP_STILL_REASON_CODE_HEADER, cr as maskUrlCredentials, et as armRemovalReconcile, gn as resolveEgressDecodeHwAccel, gr as boolean, hr as array, ht as declareLogChannel, ir as isEvent, it as batteryCapability, kn as streamBrokerCapability, lr as nodePin, lt as clipExportFailureOf, mr as _enum, mt as createLogChannelsProvider, o as BatteryStatusSchema, or as makeProfileBrokerId, pt as createHwAccelCache, qn as CamProfileSchema, s as CLIP_BROKER_PACED_RATES, sn as rankStreams, sr as makeSourceBrokerId, st as cameraStreamsCapability, tr as expandAudioChunkToF32le, ur as parseProfileBrokerId, w as EncodeProfileSchema, wr as string, xr as object, yr as literal, zn as invocationFromEncodeProfile } from "../dist-opSx6v5i.mjs";
3
+ import { a as defaultFfmpegSpawn, i as NO_COST_CLAIM, n as Fmp4FragmentChild, r as Fmp4FragmentPlane, s as probeFfmpegBinary, t as ChildCostRegistry } from "../node-CGIYuJMN.mjs";
4
4
  import { a as parseRestreamPath, d as PREBUFFER_MAX_MS, f as capPreRollRequestMs, i as isLiveEdgeOnlyDial, m as resolvePrebuffer, n as NO_DECLARATION, p as prebufferRetentionMs, s as sdpVariantFor, u as withSentryIntent } from "../restream-intent-DyGmLAU9.mjs";
5
5
  import { n as profileForStreamId } from "../remote-restream-BeHi78PZ.mjs";
6
- import { a as require_mp4box_all, c as parseNalLengthSize, i as runFfmpegOnce, n as demuxSegmentPreferJs, o as avccSampleToAnnexB, r as FfmpegFallbackRefusedError, s as buildKeyframeAnnexB, t as demuxSegmentFullJs } from "../segment-demux-js-Bv0t0-tQ.mjs";
6
+ import { a as require_mp4box_all, c as parseNalLengthSize, i as runFfmpegOnce, n as demuxSegmentPreferJs, o as avccSampleToAnnexB, r as FfmpegFallbackRefusedError, s as buildKeyframeAnnexB, t as demuxSegmentFullJs } from "../segment-demux-js-CdVRzwSd.mjs";
7
7
  import { createRequire } from "node:module";
8
8
  import * as os$1 from "node:os";
9
9
  import { networkInterfaces, tmpdir } from "node:os";
@@ -30,7 +30,7 @@ async function d(e) {
30
30
  }
31
31
  }
32
32
  async function f() {
33
- return l ||= d(() => import("./_virtual_mf-localSharedImportMap___mfe_internal__addon_stream_broker_widgets-dNk74ahz.mjs")).catch((e) => {
33
+ return l ||= d(() => import("./_virtual_mf-localSharedImportMap___mfe_internal__addon_stream_broker_widgets-DF3FGCR8.mjs")).catch((e) => {
34
34
  throw l = void 0, e;
35
35
  }), l;
36
36
  }
@@ -60,6 +60,71 @@ function formatNativeLeaseKnobs(resolved) {
60
60
  return `native lease knobs — ${NATIVE_LEASE_KNOB_NAMES.map(part).join("; ")}`;
61
61
  }
62
62
  //#endregion
63
+ //#region src/session-decode/session-hwaccel.ts
64
+ var KNOWN_BACKENDS = [
65
+ "videotoolbox",
66
+ "cuda",
67
+ "nvdec",
68
+ "vaapi",
69
+ "qsv",
70
+ "d3d11va",
71
+ "dxva2",
72
+ "amf",
73
+ "vdpau",
74
+ "drm"
75
+ ];
76
+ function isHwAccelBackend(value) {
77
+ return typeof value === "string" && KNOWN_BACKENDS.includes(value);
78
+ }
79
+ /** Wire guard for the `start` message's `hwaccel` field. */
80
+ function isSessionHwaccelOffer(value) {
81
+ return value === null || Array.isArray(value) && value.every(isHwAccelBackend);
82
+ }
83
+ /** The worker's decode order per platform — first successful create() wins. */
84
+ function platformDecodeOrder(platform) {
85
+ switch (platform) {
86
+ case "darwin": return ["videotoolbox"];
87
+ case "linux": return [
88
+ "vaapi",
89
+ "qsv",
90
+ "cuda"
91
+ ];
92
+ case "win32": return [
93
+ "d3d11va",
94
+ "dxva2",
95
+ "cuda"
96
+ ];
97
+ default: return [];
98
+ }
99
+ }
100
+ /**
101
+ * The platform order, restricted to what the resolver offers. An empty offer
102
+ * (VA init failed and no other GPU) means software, with no per-session try.
103
+ */
104
+ function sessionDecodeCandidates(platform, offer) {
105
+ const order = platformDecodeOrder(platform);
106
+ if (offer === null) return order;
107
+ return order.filter((backend) => offer.includes(backend));
108
+ }
109
+ /**
110
+ * The parent's copy of the resolver's auto answer, refreshed on the acquire
111
+ * leg of every session spawn and read synchronously when `start` is sent. A
112
+ * resolve that fails changes nothing (D49): the last answer stands.
113
+ */
114
+ var SessionHwaccelSource = class {
115
+ offer = null;
116
+ current() {
117
+ return this.offer;
118
+ }
119
+ async refresh(resolver) {
120
+ if (!resolver) return;
121
+ try {
122
+ const res = await resolver.resolve(null);
123
+ this.offer = [...res.preferred];
124
+ } catch {}
125
+ }
126
+ };
127
+ //#endregion
63
128
  //#region src/session-decode/worker-protocol.ts
64
129
  /**
65
130
  * Typed IPC protocol between the per-session decode coordinator (Task 3 —
@@ -129,7 +194,7 @@ function isVideoFrameGenOptions(value) {
129
194
  function isWorkerRequest(value) {
130
195
  if (!isRecord(value)) return false;
131
196
  switch (value.kind) {
132
- case "start": return isVideoFrameSource(value.source) && isVideoFrameGenOptions(value.opts) && isWorkerLeaseKnobs(value.lease);
197
+ case "start": return isVideoFrameSource(value.source) && isVideoFrameGenOptions(value.opts) && isWorkerLeaseKnobs(value.lease) && isSessionHwaccelOffer(value.hwaccel);
133
198
  case "pull": return true;
134
199
  case "toBuffer": return isFiniteNumber(value.frameId) && (value.opts === void 0 || isFrameImageOptions(value.opts));
135
200
  case "nativeCrop": return isFiniteNumber(value.requestId) && isFiniteNumber(value.frameId) && isNormalizedBbox(value.bbox) && (value.maxWidth === void 0 || isFiniteNumber(value.maxWidth));
@@ -160,4 +225,39 @@ function isWorkerReply(value) {
160
225
  }
161
226
  }
162
227
  //#endregion
163
- export { formatNativeLeaseKnobs as i, isWorkerRequest as n, logLevelForLine as r, isWorkerReply as t };
228
+ Object.defineProperty(exports, "SessionHwaccelSource", {
229
+ enumerable: true,
230
+ get: function() {
231
+ return SessionHwaccelSource;
232
+ }
233
+ });
234
+ Object.defineProperty(exports, "formatNativeLeaseKnobs", {
235
+ enumerable: true,
236
+ get: function() {
237
+ return formatNativeLeaseKnobs;
238
+ }
239
+ });
240
+ Object.defineProperty(exports, "isWorkerReply", {
241
+ enumerable: true,
242
+ get: function() {
243
+ return isWorkerReply;
244
+ }
245
+ });
246
+ Object.defineProperty(exports, "isWorkerRequest", {
247
+ enumerable: true,
248
+ get: function() {
249
+ return isWorkerRequest;
250
+ }
251
+ });
252
+ Object.defineProperty(exports, "logLevelForLine", {
253
+ enumerable: true,
254
+ get: function() {
255
+ return logLevelForLine;
256
+ }
257
+ });
258
+ Object.defineProperty(exports, "sessionDecodeCandidates", {
259
+ enumerable: true,
260
+ get: function() {
261
+ return sessionDecodeCandidates;
262
+ }
263
+ });
@@ -60,6 +60,71 @@ function formatNativeLeaseKnobs(resolved) {
60
60
  return `native lease knobs — ${NATIVE_LEASE_KNOB_NAMES.map(part).join("; ")}`;
61
61
  }
62
62
  //#endregion
63
+ //#region src/session-decode/session-hwaccel.ts
64
+ var KNOWN_BACKENDS = [
65
+ "videotoolbox",
66
+ "cuda",
67
+ "nvdec",
68
+ "vaapi",
69
+ "qsv",
70
+ "d3d11va",
71
+ "dxva2",
72
+ "amf",
73
+ "vdpau",
74
+ "drm"
75
+ ];
76
+ function isHwAccelBackend(value) {
77
+ return typeof value === "string" && KNOWN_BACKENDS.includes(value);
78
+ }
79
+ /** Wire guard for the `start` message's `hwaccel` field. */
80
+ function isSessionHwaccelOffer(value) {
81
+ return value === null || Array.isArray(value) && value.every(isHwAccelBackend);
82
+ }
83
+ /** The worker's decode order per platform — first successful create() wins. */
84
+ function platformDecodeOrder(platform) {
85
+ switch (platform) {
86
+ case "darwin": return ["videotoolbox"];
87
+ case "linux": return [
88
+ "vaapi",
89
+ "qsv",
90
+ "cuda"
91
+ ];
92
+ case "win32": return [
93
+ "d3d11va",
94
+ "dxva2",
95
+ "cuda"
96
+ ];
97
+ default: return [];
98
+ }
99
+ }
100
+ /**
101
+ * The platform order, restricted to what the resolver offers. An empty offer
102
+ * (VA init failed and no other GPU) means software, with no per-session try.
103
+ */
104
+ function sessionDecodeCandidates(platform, offer) {
105
+ const order = platformDecodeOrder(platform);
106
+ if (offer === null) return order;
107
+ return order.filter((backend) => offer.includes(backend));
108
+ }
109
+ /**
110
+ * The parent's copy of the resolver's auto answer, refreshed on the acquire
111
+ * leg of every session spawn and read synchronously when `start` is sent. A
112
+ * resolve that fails changes nothing (D49): the last answer stands.
113
+ */
114
+ var SessionHwaccelSource = class {
115
+ offer = null;
116
+ current() {
117
+ return this.offer;
118
+ }
119
+ async refresh(resolver) {
120
+ if (!resolver) return;
121
+ try {
122
+ const res = await resolver.resolve(null);
123
+ this.offer = [...res.preferred];
124
+ } catch {}
125
+ }
126
+ };
127
+ //#endregion
63
128
  //#region src/session-decode/worker-protocol.ts
64
129
  /**
65
130
  * Typed IPC protocol between the per-session decode coordinator (Task 3 —
@@ -129,7 +194,7 @@ function isVideoFrameGenOptions(value) {
129
194
  function isWorkerRequest(value) {
130
195
  if (!isRecord(value)) return false;
131
196
  switch (value.kind) {
132
- case "start": return isVideoFrameSource(value.source) && isVideoFrameGenOptions(value.opts) && isWorkerLeaseKnobs(value.lease);
197
+ case "start": return isVideoFrameSource(value.source) && isVideoFrameGenOptions(value.opts) && isWorkerLeaseKnobs(value.lease) && isSessionHwaccelOffer(value.hwaccel);
133
198
  case "pull": return true;
134
199
  case "toBuffer": return isFiniteNumber(value.frameId) && (value.opts === void 0 || isFrameImageOptions(value.opts));
135
200
  case "nativeCrop": return isFiniteNumber(value.requestId) && isFiniteNumber(value.frameId) && isNormalizedBbox(value.bbox) && (value.maxWidth === void 0 || isFiniteNumber(value.maxWidth));
@@ -160,27 +225,4 @@ function isWorkerReply(value) {
160
225
  }
161
226
  }
162
227
  //#endregion
163
- Object.defineProperty(exports, "formatNativeLeaseKnobs", {
164
- enumerable: true,
165
- get: function() {
166
- return formatNativeLeaseKnobs;
167
- }
168
- });
169
- Object.defineProperty(exports, "isWorkerReply", {
170
- enumerable: true,
171
- get: function() {
172
- return isWorkerReply;
173
- }
174
- });
175
- Object.defineProperty(exports, "isWorkerRequest", {
176
- enumerable: true,
177
- get: function() {
178
- return isWorkerRequest;
179
- }
180
- });
181
- Object.defineProperty(exports, "logLevelForLine", {
182
- enumerable: true,
183
- get: function() {
184
- return logLevelForLine;
185
- }
186
- });
228
+ export { sessionDecodeCandidates as a, SessionHwaccelSource as i, isWorkerRequest as n, formatNativeLeaseKnobs as o, logLevelForLine as r, isWorkerReply as t };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@camstack/addon-pipeline",
3
- "version": "1.2.621",
3
+ "version": "1.2.623",
4
4
  "description": "Pipeline bundle — runner, detection, motion, audio + stream broker. Multi-entry npm package shipping pipeline addons under a single bundle.",
5
5
  "keywords": [
6
6
  "camstack",
@@ -0,0 +1,275 @@
1
+ """Per-accelerator compute the Auto tune estimate scales from (D930).
2
+
3
+ OpenVINO devices are enumerated here — the same place the pool already asks
4
+ `available_devices` — and an AMD WebGPU adapter is read when the plugin
5
+ exposes one. A query that fails is ``None``, never ``0`` (D393): zero would
6
+ scale the device as if it had no compute.
7
+
8
+ The pool publishes the list to the temp file the platform probe reads back
9
+ onto ``HardwareInfo.accelerators``. Importing this module does not import
10
+ OpenVINO or ONNX Runtime.
11
+ """
12
+ from __future__ import annotations
13
+
14
+ import json
15
+ import os
16
+ import sys
17
+ import tempfile
18
+ from typing import Any, Optional
19
+
20
+ # One file per host. The inference worker writes it; platform-probe reads it.
21
+ # Two pools (GPU and NPU) merge by deviceKey.
22
+ FACTS_FILENAME = "camstack-accelerator-facts.json"
23
+
24
+ _GOPS_INT8 = ("i8", "int8", "u8", "bin")
25
+ _GOPS_FP16 = ("f16", "fp16", "bf16")
26
+ _CLOCK_PROPERTIES = (
27
+ "GPU_DEVICE_MAX_FREQUENCY",
28
+ "GPU_MAX_FREQUENCY",
29
+ "GPU_FREQUENCY",
30
+ )
31
+ _TOPS_PROPERTIES = ("NPU_MAX_TOPS", "DEVICE_TOPS")
32
+ _CU_ATTRIBUTES = ("compute_units", "cu_count", "max_compute_units")
33
+ _VENDOR_ATTRIBUTES = ("vendor", "vendor_name", "vendorName")
34
+ _ARCH_ATTRIBUTES = ("architecture", "arch")
35
+ _DEVICE_ATTRIBUTES = ("device", "device_name", "name", "description")
36
+
37
+
38
+ def facts_path() -> str:
39
+ return os.path.join(tempfile.gettempdir(), FACTS_FILENAME)
40
+
41
+
42
+ def _positive(value: Any) -> Optional[float]:
43
+ """A reported number, or None when the query failed or answered <= 0."""
44
+ if isinstance(value, bool) or not isinstance(value, (int, float)):
45
+ return None
46
+ number = float(value)
47
+ if number <= 0 or number != number: # NaN
48
+ return None
49
+ return number
50
+
51
+
52
+ def _text(value: Any) -> Optional[str]:
53
+ if not isinstance(value, str):
54
+ return None
55
+ stripped = value.strip()
56
+ return stripped or None
57
+
58
+
59
+ def _blank(device_key: str) -> dict:
60
+ return {
61
+ "deviceKey": device_key,
62
+ "fullDeviceName": None,
63
+ "executionUnits": None,
64
+ "clockMHz": None,
65
+ "gops": None,
66
+ "architecture": None,
67
+ "npuName": None,
68
+ "tops": None,
69
+ "vendor": None,
70
+ "webgpuArchitecture": None,
71
+ "webgpuDevice": None,
72
+ "computeUnits": None,
73
+ }
74
+
75
+
76
+ def _property(core: Any, device: str, name: str) -> Any:
77
+ try:
78
+ return core.get_property(device, name)
79
+ except Exception:
80
+ return None
81
+
82
+
83
+ def _first_property(core: Any, device: str, names: tuple) -> Any:
84
+ for name in names:
85
+ value = _property(core, device, name)
86
+ if value is not None:
87
+ return value
88
+ return None
89
+
90
+
91
+ def _gops(raw: Any) -> Optional[dict]:
92
+ """``DEVICE_GOPS`` as ``{int8, fp16}``. None when the query itself failed."""
93
+ if raw is None:
94
+ return None
95
+ try:
96
+ items = list(raw.items())
97
+ except Exception:
98
+ return None
99
+ int8 = None
100
+ fp16 = None
101
+ for key, value in items:
102
+ name = str(key).lower().rsplit(".", 1)[-1]
103
+ if name in _GOPS_INT8:
104
+ int8 = _positive(value)
105
+ elif name in _GOPS_FP16:
106
+ fp16 = _positive(value)
107
+ return {"int8": int8, "fp16": fp16}
108
+
109
+
110
+ def _device_key(ov_name: str, index: int) -> Optional[str]:
111
+ upper = ov_name.upper()
112
+ if "NPU" in upper:
113
+ kind = "npu"
114
+ elif "GPU" in upper:
115
+ kind = "gpu"
116
+ else:
117
+ return None
118
+ if index == 0:
119
+ return f"openvino:{kind}"
120
+ return f"openvino:{kind}:{index}"
121
+
122
+
123
+ def collect_openvino_device_facts(core: Any) -> Optional[list]:
124
+ """One record per GPU and NPU. None when the device list itself failed."""
125
+ try:
126
+ available = list(core.available_devices)
127
+ except Exception:
128
+ return None
129
+ seen = {"gpu": 0, "npu": 0}
130
+ facts: list = []
131
+ for ov_name in available:
132
+ upper = str(ov_name).upper()
133
+ if "NPU" in upper:
134
+ kind = "npu"
135
+ elif "GPU" in upper:
136
+ kind = "gpu"
137
+ else:
138
+ continue
139
+ key = _device_key(str(ov_name), seen[kind])
140
+ seen[kind] += 1
141
+ if key is None:
142
+ continue
143
+ record = _blank(key)
144
+ name = _text(_property(core, ov_name, "FULL_DEVICE_NAME"))
145
+ record["fullDeviceName"] = name
146
+ record["architecture"] = _text(_property(core, ov_name, "DEVICE_ARCHITECTURE"))
147
+ record["gops"] = _gops(_property(core, ov_name, "DEVICE_GOPS"))
148
+ if kind == "gpu":
149
+ record["executionUnits"] = _positive(
150
+ _property(core, ov_name, "GPU_EXECUTION_UNITS_COUNT")
151
+ )
152
+ record["clockMHz"] = _positive(_first_property(core, ov_name, _CLOCK_PROPERTIES))
153
+ else:
154
+ record["npuName"] = name
155
+ record["tops"] = _positive(_first_property(core, ov_name, _TOPS_PROPERTIES))
156
+ facts.append(record)
157
+ sys.stderr.write(
158
+ f"accelerator facts: {key} name={name} "
159
+ f"eu={record['executionUnits']} clockMHz={record['clockMHz']} "
160
+ f"gops={record['gops']} arch={record['architecture']} tops={record['tops']}\n"
161
+ )
162
+ sys.stderr.flush()
163
+ return facts
164
+
165
+
166
+ def _attr(obj: Any, names: tuple) -> Any:
167
+ if obj is None:
168
+ return None
169
+ if isinstance(obj, dict):
170
+ for name in names:
171
+ if name in obj and obj[name] is not None:
172
+ return obj[name]
173
+ return None
174
+ for name in names:
175
+ if hasattr(obj, name):
176
+ value = getattr(obj, name)
177
+ if value is not None:
178
+ return value
179
+ return None
180
+
181
+
182
+ def webgpu_facts_from_device(device: Any) -> Optional[dict]:
183
+ """Adapter fields off an ORT / Dawn device. None when there is no device."""
184
+ if device is None:
185
+ return None
186
+ record = _blank("onnx:webgpu")
187
+ record["vendor"] = _text(_attr(device, _VENDOR_ATTRIBUTES))
188
+ record["webgpuArchitecture"] = _text(_attr(device, _ARCH_ATTRIBUTES))
189
+ record["webgpuDevice"] = _text(_attr(device, _DEVICE_ATTRIBUTES))
190
+ record["computeUnits"] = _positive(_attr(device, _CU_ATTRIBUTES))
191
+ return record
192
+
193
+
194
+ def publish_accelerator_facts(facts: list, path: Optional[str] = None) -> None:
195
+ """Merge ``facts`` into the published file by deviceKey. Atomic replace."""
196
+ target = path if path is not None else facts_path()
197
+ current: list = []
198
+ try:
199
+ with open(target, "r", encoding="utf-8") as handle:
200
+ payload = json.load(handle)
201
+ if isinstance(payload, dict) and isinstance(payload.get("accelerators"), list):
202
+ current = [row for row in payload["accelerators"] if isinstance(row, dict)]
203
+ except (OSError, ValueError):
204
+ current = []
205
+ by_key = {row.get("deviceKey"): row for row in current if isinstance(row.get("deviceKey"), str)}
206
+ for row in facts:
207
+ key = row.get("deviceKey")
208
+ if isinstance(key, str):
209
+ by_key[key] = row
210
+ merged = [by_key[key] for key in sorted(by_key)]
211
+ tmp = f"{target}.tmp-{os.getpid()}"
212
+ with open(tmp, "w", encoding="utf-8") as handle:
213
+ json.dump({"accelerators": merged}, handle)
214
+ os.replace(tmp, target)
215
+
216
+
217
+ def facts_for_runtime(runtime: str, runtime_lib: Any, device: str) -> Optional[list]:
218
+ """Facts for the runtime this pool just initialised. None when it cannot ask."""
219
+ if runtime == "openvino":
220
+ return collect_openvino_device_facts(runtime_lib)
221
+ if runtime == "onnxruntime" and device.strip().lower() == "webgpu":
222
+ one = _probe_webgpu_adapter()
223
+ return None if one is None else [one]
224
+ return None
225
+
226
+
227
+ def _probe_webgpu_adapter() -> Optional[dict]:
228
+ """Best-effort adapter read. None when the plugin or the device is absent."""
229
+ try:
230
+ import onnxruntime as ort
231
+ import onnxruntime_ep_webgpu as webgpu_ep
232
+ except Exception:
233
+ return None
234
+ try:
235
+ ep_name = webgpu_ep.get_ep_name()
236
+ device = next(
237
+ (item for item in ort.get_ep_devices() if getattr(item, "ep_name", None) == ep_name),
238
+ None,
239
+ )
240
+ except Exception:
241
+ return None
242
+ return webgpu_facts_from_device(device)
243
+
244
+
245
+ def _probe_openvino() -> Optional[list]:
246
+ try:
247
+ from openvino import Core
248
+ except Exception as exc:
249
+ sys.stderr.write(f"accelerator facts: OpenVINO unavailable ({exc})\n")
250
+ sys.stderr.flush()
251
+ return None
252
+ try:
253
+ return collect_openvino_device_facts(Core())
254
+ except Exception as exc:
255
+ sys.stderr.write(f"accelerator facts: OpenVINO probe failed ({exc})\n")
256
+ sys.stderr.flush()
257
+ return None
258
+
259
+
260
+ def main() -> int:
261
+ """Print ``{"accelerators": ...}``. ``null`` when nothing could be asked."""
262
+ ov = _probe_openvino()
263
+ web = _probe_webgpu_adapter()
264
+ if ov is None and web is None:
265
+ sys.stdout.write(json.dumps({"accelerators": None}) + "\n")
266
+ return 0
267
+ facts = [] if ov is None else list(ov)
268
+ if web is not None:
269
+ facts.append(web)
270
+ sys.stdout.write(json.dumps({"accelerators": facts}) + "\n")
271
+ return 0
272
+
273
+
274
+ if __name__ == "__main__":
275
+ raise SystemExit(main())