@camstack/addon-model-studio 1.1.217 → 1.1.218

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 (20) hide show
  1. package/dist/{MotionZonesSettings-B4T-Lq49.mjs → MotionZonesSettings-Capre9Um.mjs} +2 -2
  2. package/dist/{PrivacyMaskSettings-RuT2sLzN.mjs → PrivacyMaskSettings-CTeuaW76.mjs} +4 -4
  3. package/dist/{SceneMonitorEditor-BnWT3tyK.mjs → SceneMonitorEditor-B_wS3Aqg.mjs} +3 -3
  4. package/dist/_stub.js +2954 -2957
  5. package/dist/{_virtual_mf-localSharedImportMap___mfe_internal__addon_model_studio_page-BPVoz67A.mjs → _virtual_mf-localSharedImportMap___mfe_internal__addon_model_studio_page-WTUyarUL.mjs} +3 -3
  6. package/dist/_virtual_mf___mfe_internal__addon_model_studio_page__loadShare___mf_0_camstack_mf_1_types__loadShare__.js-CE6l15x5.mjs +26 -0
  7. package/dist/{hostInit-zqvyamS8.mjs → hostInit-N-aNmJfG.mjs} +2 -2
  8. package/dist/model-studio.addon.js +201 -8
  9. package/dist/model-studio.addon.mjs +201 -8
  10. package/dist/{player-overlays-BU4aAx02.mjs → player-overlays-CYNFSDYV.mjs} +1 -1
  11. package/dist/remoteEntry.js +1 -1
  12. package/dist/{responsive-CU7BiLwa.mjs → responsive-oZzHP3lx.mjs} +1 -1
  13. package/dist/{scene-monitor-copy-BX9UvcxQ.mjs → scene-monitor-copy-C2YKn7wo.mjs} +1 -1
  14. package/dist/{square-DhCedQoj.mjs → square-DUBjO0Wq.mjs} +1 -1
  15. package/dist/{trash-2-BCjCExeI.mjs → trash-2-DpaexsRt.mjs} +1 -1
  16. package/dist/{virtual_mf-REMOTE_ENTRY_ID___mfe_internal__addon_model_studio_page__remoteEntry_js-XvoS0Enz.mjs → virtual_mf-REMOTE_ENTRY_ID___mfe_internal__addon_model_studio_page__remoteEntry_js-BA6FOYZy.mjs} +1 -1
  17. package/package.json +1 -1
  18. package/python/__tests__/test_calibration.py +56 -0
  19. package/python/convert_lib.py +45 -16
  20. package/dist/_virtual_mf___mfe_internal__addon_model_studio_page__loadShare___mf_0_camstack_mf_1_types__loadShare__.js-DgQsf8Wc.mjs +0 -26
@@ -5738,7 +5738,7 @@ var ZodIssueCode = {
5738
5738
  var ZodFirstPartyTypeKind;
5739
5739
  ZodFirstPartyTypeKind || (ZodFirstPartyTypeKind = {});
5740
5740
  //#endregion
5741
- //#region ../types/dist/sleep-BNeXenvr.mjs
5741
+ //#region ../types/dist/sleep-B0JG7-_F.mjs
5742
5742
  /**
5743
5743
  * The audio chunk plane's byte format, and the ONE expansion from a coded
5744
5744
  * window to float samples (D455).
@@ -15396,6 +15396,10 @@ var DeviceInfoSchema = object({
15396
15396
  isCamera: boolean(),
15397
15397
  /** Current config values — serializable snapshot */
15398
15398
  config: record(string(), unknown()),
15399
+ /** Paths inside `config` the device's schema declares secret (D783) —
15400
+ * what the non-admin redaction withholds. Paths, never values. Absent
15401
+ * when the owning runner has not reported them. */
15402
+ secretConfigPaths: array(string()).optional(),
15399
15403
  /** Hardware + identity blob (manufacturer / model / firmware / sn /
15400
15404
  * uid / mac / …). Populated by drivers; editable via setMetadata. */
15401
15405
  metadata: record(string(), unknown()).nullable().optional(),
@@ -15601,7 +15605,13 @@ var DeviceRegisterPayloadSchema = object({
15601
15605
  * call (workers re-register on init), so probe-driven updates
15602
15606
  * (e.g. PTZ flag flipping after the abilities probe) propagate. */
15603
15607
  features: array(string()).optional(),
15604
- config: record(string(), unknown())
15608
+ config: record(string(), unknown()),
15609
+ /** Config paths the device's own schema DECLARES secret
15610
+ * (`.meta({ secret: true })` — dotted, `[]` for array items, e.g.
15611
+ * `connection.password`). Re-published on every register; the non-admin
15612
+ * redaction withholds them at any depth (D783). Optional: an older runner
15613
+ * omits it and the keyword backstop applies. */
15614
+ secretConfigPaths: array(string()).optional()
15605
15615
  });
15606
15616
  var DeviceRemovePayloadSchema = object({ deviceId: number() });
15607
15617
  var DevicePersistConfigPayloadSchema = object({
@@ -17260,7 +17270,14 @@ var LoginMethodContributionSchema = discriminatedUnion("kind", [
17260
17270
  icon: string().optional(),
17261
17271
  /** Addon-owned HTTP route the button navigates to (GET). */
17262
17272
  startUrl: string(),
17263
- stage: LoginStageEnum
17273
+ stage: LoginStageEnum,
17274
+ /**
17275
+ * The provider names an external identity a signed-in user can link to
17276
+ * their own account (D782): `auth.listLinkableProviders` /
17277
+ * `beginExternalLink` offer exactly the redirect methods that set this,
17278
+ * whatever their id. Absent / false — a login button only (magic-link).
17279
+ */
17280
+ linkable: boolean().optional()
17264
17281
  }),
17265
17282
  object({
17266
17283
  kind: literal("widget"),
@@ -22663,7 +22680,13 @@ var RetrainFrameSchema = object({
22663
22680
  sizeBytes: number().int(),
22664
22681
  width: number().int(),
22665
22682
  height: number().int(),
22666
- copiedAt: number()
22683
+ copiedAt: number(),
22684
+ /** When the CAMERA captured the frame. Absent on a copy taken before D785. */
22685
+ capturedAt: number().optional(),
22686
+ /** The operator's "every subject in this frame is boxed". Detector
22687
+ * evaluation keeps only complete frames: an unboxed subject would count as
22688
+ * a false positive. */
22689
+ complete: boolean()
22667
22690
  });
22668
22691
  /** Why a copy-on-select could not be honoured — named, never a silent skip. */
22669
22692
  var RetrainCopyRefusalSchema = _enum([
@@ -22684,6 +22707,51 @@ var RetrainFrameListSchema = object({
22684
22707
  /** What the page pre-selects — the native key frame when one survives. */
22685
22708
  autoPickMediaId: number().int().optional()
22686
22709
  });
22710
+ /**
22711
+ * Which frames an annotated export carries. Every field narrows; an empty
22712
+ * filter is every annotated copy. A dataset VERSION is a frozen `frameIds`
22713
+ * list owned by whoever trains (the training orchestrator), so `frameIds` is
22714
+ * the primitive a version export uses; `batchId` re-selects what an earlier
22715
+ * export carried (`exportedInBatch`).
22716
+ */
22717
+ var RetrainExportFilterSchema = object({
22718
+ deviceIds: array(number()).optional(),
22719
+ frameIds: array(string()).max(2e4).optional(),
22720
+ batchId: string().optional(),
22721
+ /** Only frames the operator marked complete (all subjects boxed). */
22722
+ completeOnly: boolean().optional()
22723
+ });
22724
+ /**
22725
+ * `frames`: the full frame copies, boxes in `annotations.json`.
22726
+ * `crops`: additionally one JPEG per subject, cut with the runtime detail-crop
22727
+ * convention — the classifier layout.
22728
+ */
22729
+ var RetrainExportLayoutSchema = _enum(["frames", "crops"]);
22730
+ /**
22731
+ * Where `writeRetrainArchive` streams the tar: one HTTP request whose body is
22732
+ * the archive, sent chunk by chunk. Covers a presigned PUT and a provider
22733
+ * addon's data-plane upload route alike. Headers are sent verbatim and never
22734
+ * logged.
22735
+ */
22736
+ var RetrainArchiveTargetSchema = object({
22737
+ kind: literal("http"),
22738
+ method: _enum(["PUT", "POST"]),
22739
+ url: string().url(),
22740
+ headers: record(string(), string()).optional()
22741
+ });
22742
+ var RetrainArchiveResultSchema = object({
22743
+ batchId: string(),
22744
+ frames: number().int(),
22745
+ crops: number().int(),
22746
+ annotations: number().int(),
22747
+ /** Frames planned but not shipped (unreadable copy, path too long). */
22748
+ missing: number().int(),
22749
+ bytes: number().int(),
22750
+ /** sha256 of the exact bytes sent. */
22751
+ sha256: string(),
22752
+ /** The target's HTTP status. */
22753
+ status: number().int()
22754
+ });
22687
22755
  /** What the operator asked the assist to look for. */
22688
22756
  var RetrainAssistSubjectSchema = discriminatedUnion("kind", [object({
22689
22757
  kind: literal("package"),
@@ -23478,9 +23546,25 @@ DeviceType.Camera, method(object({ deviceId: number() }), array(TrackSchema).rea
23478
23546
  }), RetrainTransitionResultSchema, {
23479
23547
  kind: "mutation",
23480
23548
  auth: "admin"
23481
- }), method(object({ deviceIds: array(number()).optional() }), object({ url: string() }), {
23549
+ }), method(RetrainExportFilterSchema.extend({ layout: RetrainExportLayoutSchema.optional() }), object({ url: string() }), {
23482
23550
  kind: "query",
23483
23551
  auth: "admin"
23552
+ }), method(object({
23553
+ deviceId: number(),
23554
+ trackId: string(),
23555
+ frameId: string(),
23556
+ complete: boolean()
23557
+ }), RetrainFrameSchema, {
23558
+ kind: "mutation",
23559
+ auth: "admin"
23560
+ }), method(object({
23561
+ filter: RetrainExportFilterSchema,
23562
+ layout: RetrainExportLayoutSchema,
23563
+ target: RetrainArchiveTargetSchema
23564
+ }), RetrainArchiveResultSchema, {
23565
+ kind: "mutation",
23566
+ auth: "admin",
23567
+ timeoutMs: 60 * 6e4
23484
23568
  }), method(object({
23485
23569
  /** Which of the three event tables owns the row. */
23486
23570
  ownerType: EventOwnerTypeSchema,
@@ -26770,7 +26854,19 @@ var SsoBridgeClaimsSchema = object({
26770
26854
  * editing a descriptor must not retroactively extend or shorten a link the
26771
26855
  * operator already consented to.
26772
26856
  */
26773
- refreshTtl: union([number().int().positive(), literal("never")]).optional()
26857
+ refreshTtl: union([number().int().positive(), literal("never")]).optional(),
26858
+ /**
26859
+ * Opaque handle of a hub-side LINK intent (D782). Set by an OIDC callback
26860
+ * that was started from a signed-in user's "Link" button: the finish then
26861
+ * links this identity to the intent's user instead of signing anyone in.
26862
+ * The handle carries no user id — the hub resolves it, single-use.
26863
+ */
26864
+ linkIntent: string().optional(),
26865
+ /**
26866
+ * Verified `iss` of the id_token behind an OIDC login bridge (D782). A
26867
+ * subject is unique only per issuer, so a link is bound to it.
26868
+ */
26869
+ issuer: string().optional()
26774
26870
  });
26775
26871
  method(object({
26776
26872
  claims: SsoBridgeClaimsSchema,
@@ -28676,7 +28772,22 @@ DeviceType.Camera, method(object({ deviceId: number().int().nonnegative() }), ar
28676
28772
  * field their hints were silently stripped by the schema and the
28677
28773
  * adaptive ladder started blind on the client's display size.
28678
28774
  */
28679
- hints: webrtcClientHintsSchema.optional()
28775
+ hints: webrtcClientHintsSchema.optional(),
28776
+ /**
28777
+ * Talk-back (D771): relay the PEER's audio to the device's speaker
28778
+ * through the `intercom` cap's push-talk triple. When `true` and the
28779
+ * offer lets the peer send audio, the answer's audio is `sendrecv`
28780
+ * and every inbound frame goes to `intercom.pushTalkAudio` for this
28781
+ * `deviceId`; absent/false answers `sendonly`, as for every viewer.
28782
+ *
28783
+ * TRUSTED-CALLER ONLY. Set by the Alexa export for an Echo on a camera
28784
+ * with `intercom`, when the linked user may control that camera. The
28785
+ * hub STRIPS it from every request-bound call (a browser), exactly as
28786
+ * it overwrites `relayOnly` — a viewer's talk permission is the
28787
+ * intercom cap's own surface (share tokens gate it there), and this
28788
+ * flag must not become a second door to the camera's speaker.
28789
+ */
28790
+ talkback: boolean().optional()
28680
28791
  }), object({
28681
28792
  sessionId: string(),
28682
28793
  sdpAnswer: string()
@@ -36720,6 +36831,32 @@ var TotpStatusSchema = object({
36720
36831
  /** Null when no row exists OR the row is still pending confirmation. */
36721
36832
  confirmedAt: number().nullable()
36722
36833
  });
36834
+ /**
36835
+ * user-management — singleton capability for user accounts, API keys,
36836
+ * and scoped tokens.
36837
+ *
36838
+ * v2 model: every method gated by `auth: 'admin'`. Admin-only delegation
36839
+ * — the subset check on `setUserScopes` / `createScopedToken` is moot
36840
+ * because non-admins never reach these. If/when we open non-admin
36841
+ * delegation, structural caller-scope subset check goes inside the
36842
+ * provider.
36843
+ */
36844
+ var ExternalIdentitySchema = object({
36845
+ /** Login-method id of the provider instance, e.g. `auth-oidc/authentik`. */
36846
+ provider: string(),
36847
+ /** The IdP's stable subject (`sub`) for this account. */
36848
+ subject: string(),
36849
+ /** The verified `iss` of the IdP the identity was linked under — `sub` is unique only per issuer. */
36850
+ issuer: string(),
36851
+ userId: string(),
36852
+ /** What the IdP called the account at link time (username / email) — display only. */
36853
+ label: string().optional(),
36854
+ linkedAt: number()
36855
+ });
36856
+ var ExternalIdentityKeySchema = object({
36857
+ provider: string().min(1),
36858
+ subject: string().min(1)
36859
+ });
36723
36860
  method(_void(), array(UserSummarySchema), { auth: "admin" }), method(CreateUserInputSchema, UserSummarySchema, {
36724
36861
  kind: "mutation",
36725
36862
  auth: "admin",
@@ -36833,7 +36970,22 @@ method(_void(), array(UserSummarySchema), { auth: "admin" }), method(CreateUserI
36833
36970
  userId: string(),
36834
36971
  username: string(),
36835
36972
  scopes: array(TokenScopeSchema)
36836
- }).nullable(), { access: "view" }), method(_void(), array(OauthSessionSummarySchema), { auth: "admin" }), method(object({ id: string() }), object({ success: boolean() }), {
36973
+ }).nullable(), { access: "view" }), method(object({ userId: string() }), array(ExternalIdentitySchema), { auth: "admin" }), method(ExternalIdentityKeySchema, object({
36974
+ userId: string(),
36975
+ issuer: string()
36976
+ }).nullable(), { auth: "admin" }), method(ExternalIdentityKeySchema.extend({
36977
+ userId: string(),
36978
+ issuer: string().min(1),
36979
+ label: string().optional()
36980
+ }), object({ success: literal(true) }), {
36981
+ kind: "mutation",
36982
+ auth: "admin",
36983
+ access: "create"
36984
+ }), method(ExternalIdentityKeySchema.extend({ userId: string() }), object({ success: boolean() }), {
36985
+ kind: "mutation",
36986
+ auth: "admin",
36987
+ access: "delete"
36988
+ }), method(_void(), array(OauthSessionSummarySchema), { auth: "admin" }), method(object({ id: string() }), object({ success: boolean() }), {
36837
36989
  kind: "mutation",
36838
36990
  auth: "admin",
36839
36991
  access: "delete"
@@ -41737,6 +41889,12 @@ Object.freeze({
41737
41889
  addonId: null,
41738
41890
  access: "create"
41739
41891
  },
41892
+ "pipelineAnalytics.setRetrainFrameComplete": {
41893
+ capName: "pipeline-analytics",
41894
+ capScope: "device",
41895
+ addonId: null,
41896
+ access: "create"
41897
+ },
41740
41898
  "pipelineAnalytics.setTrackFlags": {
41741
41899
  capName: "pipeline-analytics",
41742
41900
  capScope: "device",
@@ -41773,6 +41931,12 @@ Object.freeze({
41773
41931
  addonId: null,
41774
41932
  access: "delete"
41775
41933
  },
41934
+ "pipelineAnalytics.writeRetrainArchive": {
41935
+ capName: "pipeline-analytics",
41936
+ capScope: "device",
41937
+ addonId: null,
41938
+ access: "create"
41939
+ },
41776
41940
  "pipelineExecutor.cacheFrameInPool": {
41777
41941
  capName: "pipeline-executor",
41778
41942
  capScope: "system",
@@ -44125,18 +44289,36 @@ Object.freeze({
44125
44289
  addonId: null,
44126
44290
  access: "delete"
44127
44291
  },
44292
+ "userManagement.findUserByExternalIdentity": {
44293
+ capName: "user-management",
44294
+ capScope: "system",
44295
+ addonId: null,
44296
+ access: "view"
44297
+ },
44128
44298
  "userManagement.getTotpStatus": {
44129
44299
  capName: "user-management",
44130
44300
  capScope: "system",
44131
44301
  addonId: null,
44132
44302
  access: "view"
44133
44303
  },
44304
+ "userManagement.linkExternalIdentity": {
44305
+ capName: "user-management",
44306
+ capScope: "system",
44307
+ addonId: null,
44308
+ access: "create"
44309
+ },
44134
44310
  "userManagement.listApiKeys": {
44135
44311
  capName: "user-management",
44136
44312
  capScope: "system",
44137
44313
  addonId: null,
44138
44314
  access: "view"
44139
44315
  },
44316
+ "userManagement.listExternalIdentities": {
44317
+ capName: "user-management",
44318
+ capScope: "system",
44319
+ addonId: null,
44320
+ access: "view"
44321
+ },
44140
44322
  "userManagement.listOauthSessions": {
44141
44323
  capName: "user-management",
44142
44324
  capScope: "system",
@@ -44215,6 +44397,12 @@ Object.freeze({
44215
44397
  addonId: null,
44216
44398
  access: "create"
44217
44399
  },
44400
+ "userManagement.unlinkExternalIdentity": {
44401
+ capName: "user-management",
44402
+ capScope: "system",
44403
+ addonId: null,
44404
+ access: "delete"
44405
+ },
44218
44406
  "userManagement.updateUser": {
44219
44407
  capName: "user-management",
44220
44408
  capScope: "system",
@@ -47554,6 +47742,11 @@ Object.freeze({
47554
47742
  form: "single",
47555
47743
  optional: false
47556
47744
  }],
47745
+ "pipelineAnalytics.setRetrainFrameComplete": [{
47746
+ name: "deviceId",
47747
+ form: "single",
47748
+ optional: false
47749
+ }],
47557
47750
  "pipelineAnalytics.setTrackFlags": [{
47558
47751
  name: "deviceId",
47559
47752
  form: "single",
@@ -1,5 +1,5 @@
1
1
  import { b as e, g as t, l as n, u as r } from "./_virtual_mf___mfe_internal__addon_model_studio_page__loadShare__react__loadShare__.js-CHL8loOl.mjs";
2
- import { b as i, y as a } from "./responsive-CU7BiLwa.mjs";
2
+ import { b as i, y as a } from "./responsive-oZzHP3lx.mjs";
3
3
  import { n as o, r as s, t as c } from "./_virtual_mf___mfe_internal__addon_model_studio_page__loadShare__react_mf_1_jsx_mf_2_runtime__loadShare__.js-ds_Ehzaa.mjs";
4
4
  var l = i("chevron-down", [["path", {
5
5
  d: "m6 9 6 6 6-6",
@@ -1,2 +1,2 @@
1
- import { n as e, t } from "./virtual_mf-REMOTE_ENTRY_ID___mfe_internal__addon_model_studio_page__remoteEntry_js-XvoS0Enz.mjs";
1
+ import { n as e, t } from "./virtual_mf-REMOTE_ENTRY_ID___mfe_internal__addon_model_studio_page__remoteEntry_js-BA6FOYZy.mjs";
2
2
  export { t as get, e as init };
@@ -1,6 +1,6 @@
1
1
  import { _ as e, b as t, c as n, g as r, l as i, m as a, n as o, r as s, t as c, u as l } from "./_virtual_mf___mfe_internal__addon_model_studio_page__loadShare__react__loadShare__.js-CHL8loOl.mjs";
2
2
  import "./_virtual_mf___mfe_internal__addon_model_studio_page__loadShare__react_mf_1_jsx_mf_2_runtime__loadShare__.js-ds_Ehzaa.mjs";
3
- import { f as u } from "./_virtual_mf___mfe_internal__addon_model_studio_page__loadShare___mf_0_camstack_mf_1_types__loadShare__.js-DgQsf8Wc.mjs";
3
+ import { f as u } from "./_virtual_mf___mfe_internal__addon_model_studio_page__loadShare___mf_0_camstack_mf_1_types__loadShare__.js-CE6l15x5.mjs";
4
4
  //#region ../../node_modules/lucide-react/dist/esm/shared/src/utils/mergeClasses.js
5
5
  t();
6
6
  var d = (...e) => e.filter((e, t, n) => !!e && e.trim() !== "" && n.indexOf(e) === t).join(" ").trim(), f = (e) => e.replace(/([a-z0-9])([A-Z])/g, "$1-$2").toLowerCase(), p = (e) => e.replace(/^([A-Z])|[\s-_]+(\w)/g, (e, t, n) => n ? n.toUpperCase() : t.toLowerCase()), m = (e) => {
@@ -1,5 +1,5 @@
1
1
  import { b as e, c as t, g as n, m as r } from "./_virtual_mf___mfe_internal__addon_model_studio_page__loadShare__react__loadShare__.js-CHL8loOl.mjs";
2
- import { b as i } from "./responsive-CU7BiLwa.mjs";
2
+ import { b as i } from "./responsive-oZzHP3lx.mjs";
3
3
  var a = i("camera", [["path", {
4
4
  d: "M13.997 4a2 2 0 0 1 1.76 1.05l.486.9A2 2 0 0 0 18.003 7H20a2 2 0 0 1 2 2v9a2 2 0 0 1-2 2H4a2 2 0 0 1-2-2V9a2 2 0 0 1 2-2h1.997a2 2 0 0 0 1.759-1.048l.489-.904A2 2 0 0 1 10.004 4z",
5
5
  key: "18u6gg"
@@ -1,4 +1,4 @@
1
- import { b as e } from "./responsive-CU7BiLwa.mjs";
1
+ import { b as e } from "./responsive-oZzHP3lx.mjs";
2
2
  var t = e("eye-off", [
3
3
  ["path", {
4
4
  d: "M10.733 5.076a10.744 10.744 0 0 1 11.205 6.575 1 1 0 0 1 0 .696 10.747 10.747 0 0 1-1.444 2.49",
@@ -1,4 +1,4 @@
1
- import { b as e } from "./responsive-CU7BiLwa.mjs";
1
+ import { b as e } from "./responsive-oZzHP3lx.mjs";
2
2
  var t = e("trash-2", [
3
3
  ["path", {
4
4
  d: "M10 11v6",
@@ -2753,7 +2753,7 @@ async function rr(e) {
2753
2753
  }
2754
2754
  }
2755
2755
  async function ir() {
2756
- return tr ||= rr(() => import("./_virtual_mf-localSharedImportMap___mfe_internal__addon_model_studio_page-BPVoz67A.mjs")).catch((e) => {
2756
+ return tr ||= rr(() => import("./_virtual_mf-localSharedImportMap___mfe_internal__addon_model_studio_page-WTUyarUL.mjs")).catch((e) => {
2757
2757
  throw tr = void 0, e;
2758
2758
  }), tr;
2759
2759
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@camstack/addon-model-studio",
3
- "version": "1.1.217",
3
+ "version": "1.1.218",
4
4
  "description": "Custom detection model registry, conversion & distribution for CamStack",
5
5
  "keywords": [
6
6
  "camstack",
@@ -0,0 +1,56 @@
1
+ """INT8 calibration inputs (cloud-training P6): which images, and how they are
2
+ fed. Calibration must see frames the way the runtime feeds the model — the
3
+ inference pool's letterbox is CENTRED (pad = (size - new) // 2, grey 114) —
4
+ and must be able to use the operator's own frames, i.e. the hub's annotated
5
+ export, whose frames sit in nested folders next to `annotations.json`."""
6
+ import numpy as np
7
+ import pytest
8
+
9
+ cv2 = pytest.importorskip("cv2")
10
+
11
+ from convert_lib import calibration_images, letterbox_for_calibration # noqa: E402
12
+
13
+
14
+ def _touch(path, data=b"x"):
15
+ path.parent.mkdir(parents=True, exist_ok=True)
16
+ path.write_bytes(data)
17
+
18
+
19
+ def test_a_flat_folder_is_read_in_file_name_order(tmp_path):
20
+ for name in ("b.jpg", "a.jpg", "c.png", "notes.txt"):
21
+ _touch(tmp_path / name)
22
+ assert [p.name for p in calibration_images(str(tmp_path), 10)] == ["a.jpg", "b.jpg", "c.png"]
23
+
24
+
25
+ def test_the_sample_count_is_a_cap(tmp_path):
26
+ for i in range(5):
27
+ _touch(tmp_path / f"{i}.jpg")
28
+ assert len(calibration_images(str(tmp_path), 3)) == 3
29
+
30
+
31
+ def test_an_export_directory_calibrates_on_its_frames_only(tmp_path):
32
+ _touch(tmp_path / "annotations.json", b"{}")
33
+ _touch(tmp_path / "frames" / "615" / "t1" / "a.jpg")
34
+ _touch(tmp_path / "frames" / "617" / "t2" / "b.jpg")
35
+ _touch(tmp_path / "crops" / "person" / "x.jpg")
36
+ names = [p.relative_to(tmp_path).as_posix() for p in calibration_images(str(tmp_path), 10)]
37
+ assert names == ["frames/615/t1/a.jpg", "frames/617/t2/b.jpg"]
38
+
39
+
40
+ def test_an_empty_folder_is_refused(tmp_path):
41
+ with pytest.raises(ValueError):
42
+ calibration_images(str(tmp_path), 10)
43
+
44
+
45
+ def test_the_letterbox_is_centred_like_the_runtime():
46
+ rgb = np.full((100, 200, 3), 255, dtype=np.uint8) # 2:1 white frame
47
+ out = letterbox_for_calibration(rgb, 64)
48
+ assert out.shape == (1, 3, 64, 64)
49
+ assert out.dtype == np.float32
50
+ # Content 64x32, centred: grey rows 0..15 and 48..63, white in between.
51
+ column = out[0, 0, :, 32]
52
+ assert column[0] == pytest.approx(114 / 255)
53
+ assert column[63] == pytest.approx(114 / 255)
54
+ assert column[32] == pytest.approx(1.0)
55
+ assert column[15] == pytest.approx(114 / 255)
56
+ assert column[16] == pytest.approx(1.0)
@@ -64,32 +64,61 @@ def validate_openvino_ir(xml_path: str) -> None:
64
64
  raise ValueError(f"IR {xml_path} has an empty-named tensor")
65
65
 
66
66
 
67
+ CALIBRATION_SUFFIXES = {".jpg", ".jpeg", ".png"}
68
+
69
+
70
+ def calibration_images(calib_dir: str, max_samples: int) -> "list[Path]":
71
+ """The first ``max_samples`` calibration images of ``calib_dir``, in path order.
72
+
73
+ A flat folder (COCO val2017) is read exactly as before: file-name order. A
74
+ folder is walked recursively, so the operator's own frames work as they
75
+ come. The hub's annotated export (a folder holding ``annotations.json``)
76
+ calibrates on its ``frames/`` only: its ``crops/`` are classifier inputs,
77
+ not what a root detector is fed.
78
+ """
79
+ root = Path(calib_dir)
80
+ if not root.is_dir():
81
+ raise ValueError(f"calibration folder {calib_dir} does not exist")
82
+ base = root / "frames" if (root / "annotations.json").is_file() else root
83
+ images = sorted(
84
+ (p for p in base.rglob("*") if p.is_file() and p.suffix.lower() in CALIBRATION_SUFFIXES),
85
+ key=lambda p: p.relative_to(base).as_posix(),
86
+ )[:max_samples]
87
+ if not images:
88
+ raise ValueError(f"no calibration images in {calib_dir}")
89
+ return images
90
+
91
+
92
+ def letterbox_for_calibration(rgb, imgsz: int):
93
+ """``rgb`` (HxWx3 uint8) → ``[1,3,imgsz,imgsz]`` float32 in [0,1], letterboxed
94
+ as the inference pool does (``inference_pool.letterbox_image``): scale by the
95
+ long side, truncate, CENTRE with grey 114 padding."""
96
+ import cv2
97
+ import numpy as np
98
+
99
+ h, w = rgb.shape[:2]
100
+ scale = imgsz / max(h, w)
101
+ nw, nh = int(w * scale), int(h * scale)
102
+ pad_x, pad_y = (imgsz - nw) // 2, (imgsz - nh) // 2
103
+ resized = rgb if (nw, nh) == (w, h) else cv2.resize(rgb, (nw, nh), interpolation=cv2.INTER_LINEAR)
104
+ canvas = np.full((imgsz, imgsz, 3), 114, dtype=np.uint8)
105
+ canvas[pad_y : pad_y + nh, pad_x : pad_x + nw] = resized
106
+ chw = canvas.transpose(2, 0, 1).astype(np.float32) / 255.0
107
+ return chw[np.newaxis, ...]
108
+
109
+
67
110
  def quantize_openvino_int8(
68
111
  onnx_path: str, out_xml: str, calib_dir: str, imgsz: int, max_samples: int = 300
69
112
  ) -> None:
70
113
  import cv2
71
- import numpy as np
72
114
  import openvino as ov
73
115
  import nncf
74
116
 
75
- calib = Path(calib_dir)
76
- images = sorted(
77
- p for p in calib.iterdir() if p.suffix.lower() in {".jpg", ".jpeg", ".png"}
78
- )[:max_samples]
79
- if not images:
80
- raise ValueError(f"no calibration images in {calib_dir}")
117
+ images = calibration_images(calib_dir, max_samples)
81
118
 
82
119
  def preprocess(path: Path):
83
120
  img = cv2.imread(str(path))
84
- img = cv2.cvtColor(img, cv2.COLOR_BGR2RGB)
85
- h, w = img.shape[:2]
86
- scale = imgsz / max(h, w)
87
- nh, nw = int(round(h * scale)), int(round(w * scale))
88
- resized = cv2.resize(img, (nw, nh))
89
- canvas = np.full((imgsz, imgsz, 3), 114, dtype=np.uint8)
90
- canvas[:nh, :nw] = resized
91
- chw = canvas.transpose(2, 0, 1).astype(np.float32) / 255.0
92
- return chw[np.newaxis, ...]
121
+ return letterbox_for_calibration(cv2.cvtColor(img, cv2.COLOR_BGR2RGB), imgsz)
93
122
 
94
123
  dataset = nncf.Dataset(images, lambda p: preprocess(p))
95
124
  ov_model = ov.convert_model(onnx_path)