@camstack/addon-model-studio 1.1.31 → 1.1.32
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.
- package/dist/{MotionZonesSettings-BydNTXed.mjs → MotionZonesSettings-sZkppmGH.mjs} +2 -2
- package/dist/{PrivacyMaskSettings-Dq03Q0ea.mjs → PrivacyMaskSettings-C4IQ_tob.mjs} +4 -4
- package/dist/{SceneMonitorEditor-8PcMSAlF.mjs → SceneMonitorEditor-Dv55kWSO.mjs} +3 -3
- package/dist/_stub.js +3211 -2648
- package/dist/{_virtual_mf-localSharedImportMap___mfe_internal__addon_model_studio_page-BxmPMYYE.mjs → _virtual_mf-localSharedImportMap___mfe_internal__addon_model_studio_page-Cg1VXUiH.mjs} +4 -4
- package/dist/_virtual_mf___mfe_internal__addon_model_studio_page__loadShare___mf_0_camstack_mf_1_types__loadShare__.js-Do4Kqwhj.mjs +26 -0
- package/dist/{hostInit-CnNmgbGN.mjs → hostInit-DXmPMLUp.mjs} +3 -3
- package/dist/model-studio.addon.js +269 -56
- package/dist/model-studio.addon.mjs +270 -57
- package/dist/{player-overlays-DkYP-ilS.mjs → player-overlays-CS5IM4vB.mjs} +1 -1
- package/dist/remoteEntry.js +1 -1
- package/dist/{responsive-BPPcVvwi.mjs → responsive-CWtuZhDz.mjs} +1 -1
- package/dist/{square-CpMmCZRC.mjs → square-OXaWZ-Np.mjs} +1 -1
- package/dist/{trash-2-BY1LnuAV.mjs → trash-2-BtfCrPnQ.mjs} +1 -1
- package/dist/{use-device-snapshot-DTkJH7xh.mjs → use-device-snapshot-Bx87WhSL.mjs} +1 -1
- package/dist/{virtual_mf-REMOTE_ENTRY_ID___mfe_internal__addon_model_studio_page__remoteEntry_js-B03fcjt1.mjs → virtual_mf-REMOTE_ENTRY_ID___mfe_internal__addon_model_studio_page__remoteEntry_js-CnPUM5JZ.mjs} +1 -1
- package/package.json +1 -1
- package/python/convert_lib.py +28 -5
- package/dist/_virtual_mf___mfe_internal__addon_model_studio_page__loadShare___mf_0_camstack_mf_1_types__loadShare__.js-BMel40Gn.mjs +0 -26
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { createRequire } from "node:module";
|
|
2
2
|
import { spawn } from "node:child_process";
|
|
3
|
-
import crypto$1 from "node:crypto";
|
|
3
|
+
import crypto$1, { createHash, randomUUID } from "node:crypto";
|
|
4
4
|
import * as fs$1 from "node:fs";
|
|
5
5
|
import fs from "node:fs";
|
|
6
6
|
import os from "node:os";
|
|
@@ -7677,7 +7677,7 @@ var RecordingBandSchema = object({
|
|
|
7677
7677
|
postBufferSec: number().min(0).optional()
|
|
7678
7678
|
});
|
|
7679
7679
|
({
|
|
7680
|
-
preBufferSec:
|
|
7680
|
+
preBufferSec: 15,
|
|
7681
7681
|
postBufferSec: 30
|
|
7682
7682
|
}).postBufferSec * 1e3;
|
|
7683
7683
|
/**
|
|
@@ -13657,128 +13657,103 @@ var COCO_TO_MACRO = {
|
|
|
13657
13657
|
var AUDIO_MACRO_LABELS = [
|
|
13658
13658
|
{
|
|
13659
13659
|
id: "speech",
|
|
13660
|
-
name: "Speech"
|
|
13661
|
-
icon: "🗣️"
|
|
13660
|
+
name: "Speech"
|
|
13662
13661
|
},
|
|
13663
13662
|
{
|
|
13664
13663
|
id: "scream",
|
|
13665
|
-
name: "Scream / Shout"
|
|
13666
|
-
icon: "😱"
|
|
13664
|
+
name: "Scream / Shout"
|
|
13667
13665
|
},
|
|
13668
13666
|
{
|
|
13669
13667
|
id: "crying",
|
|
13670
|
-
name: "Crying / Baby"
|
|
13671
|
-
icon: "😢"
|
|
13668
|
+
name: "Crying / Baby"
|
|
13672
13669
|
},
|
|
13673
13670
|
{
|
|
13674
13671
|
id: "laughter",
|
|
13675
|
-
name: "Laughter"
|
|
13676
|
-
icon: "😂"
|
|
13672
|
+
name: "Laughter"
|
|
13677
13673
|
},
|
|
13678
13674
|
{
|
|
13679
13675
|
id: "music",
|
|
13680
|
-
name: "Music"
|
|
13681
|
-
icon: "🎵"
|
|
13676
|
+
name: "Music"
|
|
13682
13677
|
},
|
|
13683
13678
|
{
|
|
13684
13679
|
id: "dog",
|
|
13685
|
-
name: "Dog"
|
|
13686
|
-
icon: "🐕"
|
|
13680
|
+
name: "Dog"
|
|
13687
13681
|
},
|
|
13688
13682
|
{
|
|
13689
13683
|
id: "cat",
|
|
13690
|
-
name: "Cat"
|
|
13691
|
-
icon: "🐈"
|
|
13684
|
+
name: "Cat"
|
|
13692
13685
|
},
|
|
13693
13686
|
{
|
|
13694
13687
|
id: "bird",
|
|
13695
|
-
name: "Bird"
|
|
13696
|
-
icon: "🐦"
|
|
13688
|
+
name: "Bird"
|
|
13697
13689
|
},
|
|
13698
13690
|
{
|
|
13699
13691
|
id: "animal",
|
|
13700
|
-
name: "Animal (other)"
|
|
13701
|
-
icon: "🐾"
|
|
13692
|
+
name: "Animal (other)"
|
|
13702
13693
|
},
|
|
13703
13694
|
{
|
|
13704
13695
|
id: "alarm",
|
|
13705
|
-
name: "Alarm / Siren"
|
|
13706
|
-
icon: "🚨"
|
|
13696
|
+
name: "Alarm / Siren"
|
|
13707
13697
|
},
|
|
13708
13698
|
{
|
|
13709
13699
|
id: "doorbell",
|
|
13710
|
-
name: "Doorbell / Knock"
|
|
13711
|
-
icon: "🔔"
|
|
13700
|
+
name: "Doorbell / Knock"
|
|
13712
13701
|
},
|
|
13713
13702
|
{
|
|
13714
13703
|
id: "glass_breaking",
|
|
13715
|
-
name: "Glass Breaking"
|
|
13716
|
-
icon: "💥"
|
|
13704
|
+
name: "Glass Breaking"
|
|
13717
13705
|
},
|
|
13718
13706
|
{
|
|
13719
13707
|
id: "gunshot",
|
|
13720
|
-
name: "Gunshot / Explosion"
|
|
13721
|
-
icon: "💣"
|
|
13708
|
+
name: "Gunshot / Explosion"
|
|
13722
13709
|
},
|
|
13723
13710
|
{
|
|
13724
13711
|
id: "vehicle",
|
|
13725
|
-
name: "Vehicle"
|
|
13726
|
-
icon: "🚗"
|
|
13712
|
+
name: "Vehicle"
|
|
13727
13713
|
},
|
|
13728
13714
|
{
|
|
13729
13715
|
id: "siren",
|
|
13730
|
-
name: "Emergency Siren"
|
|
13731
|
-
icon: "🚑"
|
|
13716
|
+
name: "Emergency Siren"
|
|
13732
13717
|
},
|
|
13733
13718
|
{
|
|
13734
13719
|
id: "fire",
|
|
13735
|
-
name: "Fire / Smoke"
|
|
13736
|
-
icon: "🔥"
|
|
13720
|
+
name: "Fire / Smoke"
|
|
13737
13721
|
},
|
|
13738
13722
|
{
|
|
13739
13723
|
id: "water",
|
|
13740
|
-
name: "Water"
|
|
13741
|
-
icon: "💧"
|
|
13724
|
+
name: "Water"
|
|
13742
13725
|
},
|
|
13743
13726
|
{
|
|
13744
13727
|
id: "wind",
|
|
13745
|
-
name: "Wind / Weather"
|
|
13746
|
-
icon: "🌬️"
|
|
13728
|
+
name: "Wind / Weather"
|
|
13747
13729
|
},
|
|
13748
13730
|
{
|
|
13749
13731
|
id: "door",
|
|
13750
|
-
name: "Door"
|
|
13751
|
-
icon: "🚪"
|
|
13732
|
+
name: "Door"
|
|
13752
13733
|
},
|
|
13753
13734
|
{
|
|
13754
13735
|
id: "footsteps",
|
|
13755
|
-
name: "Footsteps"
|
|
13756
|
-
icon: "👣"
|
|
13736
|
+
name: "Footsteps"
|
|
13757
13737
|
},
|
|
13758
13738
|
{
|
|
13759
13739
|
id: "crowd",
|
|
13760
|
-
name: "Crowd / Chatter"
|
|
13761
|
-
icon: "👥"
|
|
13740
|
+
name: "Crowd / Chatter"
|
|
13762
13741
|
},
|
|
13763
13742
|
{
|
|
13764
13743
|
id: "telephone",
|
|
13765
|
-
name: "Telephone"
|
|
13766
|
-
icon: "📞"
|
|
13744
|
+
name: "Telephone"
|
|
13767
13745
|
},
|
|
13768
13746
|
{
|
|
13769
13747
|
id: "engine",
|
|
13770
|
-
name: "Engine / Motor"
|
|
13771
|
-
icon: "⚙️"
|
|
13748
|
+
name: "Engine / Motor"
|
|
13772
13749
|
},
|
|
13773
13750
|
{
|
|
13774
13751
|
id: "tools",
|
|
13775
|
-
name: "Tools / Construction"
|
|
13776
|
-
icon: "🔨"
|
|
13752
|
+
name: "Tools / Construction"
|
|
13777
13753
|
},
|
|
13778
13754
|
{
|
|
13779
13755
|
id: "silence",
|
|
13780
|
-
name: "Silence"
|
|
13781
|
-
icon: "🤫"
|
|
13756
|
+
name: "Silence"
|
|
13782
13757
|
}
|
|
13783
13758
|
];
|
|
13784
13759
|
var YAMNET_TO_MACRO = {
|
|
@@ -16242,7 +16217,7 @@ var TrackEnvelopeSchema = object({
|
|
|
16242
16217
|
* `snapshots[]` references — megabytes across a page of tracks. `slim`
|
|
16243
16218
|
* keeps every scalar the list surfaces actually render (ids, class(es),
|
|
16244
16219
|
* label / audioLabels / importance enrichment, firstSeen/lastSeen, state,
|
|
16245
|
-
* zonesVisited, bestEventId, envelope, hasFace, hasRider) and returns `positions` /
|
|
16220
|
+
* zonesVisited, bestEventId, envelope, hasFace, hasEmbeddedFace, hasRider) and returns `positions` /
|
|
16246
16221
|
* `snapshots` as EMPTY arrays — detail views re-fetch the full row via
|
|
16247
16222
|
* `getTrack`. Mirrors the event-store `projection` convention
|
|
16248
16223
|
* (`getObjectEvents` et al.).
|
|
@@ -16537,6 +16512,35 @@ var TrackSchema = object({
|
|
|
16537
16512
|
*/
|
|
16538
16513
|
hasFace: boolean().optional(),
|
|
16539
16514
|
/**
|
|
16515
|
+
* This track has a face row IN THE GALLERY: a crop **and** an embedding — a
|
|
16516
|
+
* face an operator could ASSIGN to an identity.
|
|
16517
|
+
*
|
|
16518
|
+
* The STRICT twin of {@link hasFace}, and the pair only earns its keep
|
|
16519
|
+
* because the two disagree. `hasFace` is stamped at the TOP of the face
|
|
16520
|
+
* branch, before every gate, and means no more than "a face detector produced
|
|
16521
|
+
* a face detail". This one is stamped at the single moment the gallery row
|
|
16522
|
+
* LANDS — after `FaceRecognizer.onTrackEnd` successfully persists it, i.e.
|
|
16523
|
+
* past the embedding-magnitude verdict, the `minFacePx` detection gate, the
|
|
16524
|
+
* candidate gate, the imageless-track drop (no crop was ever captured) and
|
|
16525
|
+
* the crop-store drop. Everything between the detector and that insert can
|
|
16526
|
+
* legitimately refuse the face, so a flag written any earlier promises the
|
|
16527
|
+
* operator something to assign and delivers nothing.
|
|
16528
|
+
*
|
|
16529
|
+
* **Independent of recognition.** A face collected but never auto-matched is
|
|
16530
|
+
* still assignable — it is in fact the face an operator most wants to reach —
|
|
16531
|
+
* so this is NOT gated on `recognizedIdentityId`. Recognition lands in
|
|
16532
|
+
* `subLabel`; this says only that the raw material exists.
|
|
16533
|
+
*
|
|
16534
|
+
* **Set once, never cleared.** A track that produced a gallery row produced
|
|
16535
|
+
* one; deleting the row later is the gallery's business, not this flag's.
|
|
16536
|
+
*
|
|
16537
|
+
* **Absent ≠ false**, the same rule as {@link hasFace}: every row written
|
|
16538
|
+
* before the column omits it, and so does every server that predates the
|
|
16539
|
+
* field. A consumer must test `=== true` and render nothing otherwise —
|
|
16540
|
+
* never infer "no assignable face".
|
|
16541
|
+
*/
|
|
16542
|
+
hasEmbeddedFace: boolean().optional(),
|
|
16543
|
+
/**
|
|
16540
16544
|
* This subject CONTAINS a folded rider — a person the rider-pairing step
|
|
16541
16545
|
* ([D34](../decisions/adr-0034.md)) removed from the frame BEFORE the tracker,
|
|
16542
16546
|
* so the passage is tracked once and as a VEHICLE.
|
|
@@ -17886,6 +17890,32 @@ var DetailResultSchema = object({
|
|
|
17886
17890
|
/** FRAME-space bbox (already mapped back from crop space). */
|
|
17887
17891
|
bbox: NativeCropBboxSchema.optional(),
|
|
17888
17892
|
embedding: string().optional(),
|
|
17893
|
+
/**
|
|
17894
|
+
* L2 magnitude of the RAW pre-normalization embedding. Additive and optional
|
|
17895
|
+
* so an older runner that does not send it degrades to "unmeasurable", which
|
|
17896
|
+
* the consuming gate treats as accept — never as reject.
|
|
17897
|
+
*/
|
|
17898
|
+
embeddingMagnitude: number().optional(),
|
|
17899
|
+
/**
|
|
17900
|
+
* Model that produced `embedding` — the SAME contract `labelModelId` has for
|
|
17901
|
+
* `label`, and for the same reason: the producing node is the only side that
|
|
17902
|
+
* knows which model actually ran, and re-deriving it downstream is exactly
|
|
17903
|
+
* the inference this model exists to forbid.
|
|
17904
|
+
*
|
|
17905
|
+
* Additive and optional, so a runner that predates it degrades to
|
|
17906
|
+
* "unmeasurable" rather than to a wrong answer — the consumer falls back to
|
|
17907
|
+
* its previous assumption instead of refusing.
|
|
17908
|
+
*
|
|
17909
|
+
* Its absence was not cosmetic. `FaceRecognizer` needs the probe's model id
|
|
17910
|
+
* to use `face-matcher`'s same-model gate (`sample.modelId !== probe.modelId`);
|
|
17911
|
+
* with nothing on the wire it took the id from the GALLERY, so the gate
|
|
17912
|
+
* compared the gallery against itself and could never fire. That turned the
|
|
17913
|
+
* one guard protecting the face feature space into a tautology, and it is
|
|
17914
|
+
* why swapping `face-embedding`'s model would NOT have stranded the enrolled
|
|
17915
|
+
* gallery as the catalog comment predicts — it would have scored the new
|
|
17916
|
+
* model's vectors against the old model's samples in one cosine space.
|
|
17917
|
+
*/
|
|
17918
|
+
embeddingModelId: string().optional(),
|
|
17889
17919
|
label: string().optional(),
|
|
17890
17920
|
/**
|
|
17891
17921
|
* The tier `label` occupies, copied VERBATIM from the producing step's
|
|
@@ -19296,6 +19326,7 @@ DeviceType.Camera, method(object({
|
|
|
19296
19326
|
isBattery: boolean(),
|
|
19297
19327
|
reason: _enum([
|
|
19298
19328
|
"disabled",
|
|
19329
|
+
"offline",
|
|
19299
19330
|
"sleeping",
|
|
19300
19331
|
"unreachable",
|
|
19301
19332
|
"waking"
|
|
@@ -19341,9 +19372,17 @@ targets: array(object({
|
|
|
19341
19372
|
sleeping: boolean(),
|
|
19342
19373
|
/** Current device state rendered over the cached frame. State images
|
|
19343
19374
|
* remain authoritative even when their photographic background is
|
|
19344
|
-
* old; null means the link must carry a current camera frame.
|
|
19375
|
+
* old; null means the link must carry a current camera frame.
|
|
19376
|
+
*
|
|
19377
|
+
* **This is the ONLY source a surface may derive an inactive tile
|
|
19378
|
+
* from.** A client that reads a device's `online`/`disabled` flags and
|
|
19379
|
+
* draws its own verdict will disagree with the pixels it is painting
|
|
19380
|
+
* — the frame comes from here, and so must the sentence over it
|
|
19381
|
+
* (D190). Pair with `ageMs` for the age of the photograph behind the
|
|
19382
|
+
* state. */
|
|
19345
19383
|
stateReason: _enum([
|
|
19346
19384
|
"disabled",
|
|
19385
|
+
"offline",
|
|
19347
19386
|
"sleeping",
|
|
19348
19387
|
"unreachable",
|
|
19349
19388
|
"waking"
|
|
@@ -33529,7 +33568,6 @@ var NC_AUDIO_DEFAULTS = {
|
|
|
33529
33568
|
hitPercent: 60,
|
|
33530
33569
|
samplingSeconds: 10
|
|
33531
33570
|
};
|
|
33532
|
-
new Map(AUDIO_MACRO_LABELS.flatMap((macro) => macro.icon === void 0 ? [] : [[macro.id, macro.icon]]));
|
|
33533
33571
|
new Set(["devices", "classes"]);
|
|
33534
33572
|
NC_AUDIO_DEFAULTS.hitPercent, NC_AUDIO_DEFAULTS.samplingSeconds;
|
|
33535
33573
|
/**
|
|
@@ -33763,6 +33801,39 @@ TimelapseRuleInputSchema.extend({
|
|
|
33763
33801
|
createdAt: number(),
|
|
33764
33802
|
updatedAt: number()
|
|
33765
33803
|
});
|
|
33804
|
+
Object.freeze(Object.fromEntries([{
|
|
33805
|
+
stepId: "face-embedding",
|
|
33806
|
+
label: "Face recognition model",
|
|
33807
|
+
defaultModelId: "arcface-r100",
|
|
33808
|
+
indexName: "the enrolled face gallery",
|
|
33809
|
+
options: [
|
|
33810
|
+
{
|
|
33811
|
+
id: "arcface-r100",
|
|
33812
|
+
label: "ArcFace (ResNet34)"
|
|
33813
|
+
},
|
|
33814
|
+
{
|
|
33815
|
+
id: "auraface-r100",
|
|
33816
|
+
label: "AuraFace R100"
|
|
33817
|
+
},
|
|
33818
|
+
{
|
|
33819
|
+
id: "inception-resnet-v1",
|
|
33820
|
+
label: "Inception ResNet V1"
|
|
33821
|
+
}
|
|
33822
|
+
]
|
|
33823
|
+
}, {
|
|
33824
|
+
stepId: "clip-embedding",
|
|
33825
|
+
label: "Semantic search model",
|
|
33826
|
+
defaultModelId: "mobileclip-s1",
|
|
33827
|
+
indexName: "the object semantic-search index",
|
|
33828
|
+
options: [{
|
|
33829
|
+
id: "mobileclip-s1",
|
|
33830
|
+
label: "MobileCLIP S1"
|
|
33831
|
+
}, {
|
|
33832
|
+
id: "mobileclip-s2",
|
|
33833
|
+
label: "MobileCLIP S2"
|
|
33834
|
+
}]
|
|
33835
|
+
}].map((s) => [s.stepId, s.defaultModelId])));
|
|
33836
|
+
string().min(1);
|
|
33766
33837
|
object({
|
|
33767
33838
|
/**
|
|
33768
33839
|
* Fraction of the box's own size added on EACH side before cutting.
|
|
@@ -33783,10 +33854,21 @@ object({
|
|
|
33783
33854
|
*/
|
|
33784
33855
|
square: boolean()
|
|
33785
33856
|
});
|
|
33786
|
-
|
|
33857
|
+
/**
|
|
33858
|
+
* The convention in force when nobody has configured one — byte-for-byte the
|
|
33859
|
+
* behaviour of the pre-unification LIVE path (`DETAIL_CROP_PADDING_RATIO`).
|
|
33860
|
+
*/
|
|
33861
|
+
var DEFAULT_DETAIL_CROP_CONVENTION = {
|
|
33787
33862
|
paddingRatio: .15,
|
|
33788
33863
|
square: false
|
|
33789
|
-
}
|
|
33864
|
+
};
|
|
33865
|
+
Object.freeze({
|
|
33866
|
+
x: 0,
|
|
33867
|
+
y: 0,
|
|
33868
|
+
w: 1,
|
|
33869
|
+
h: 1
|
|
33870
|
+
});
|
|
33871
|
+
DEFAULT_DETAIL_CROP_CONVENTION.paddingRatio;
|
|
33790
33872
|
/**
|
|
33791
33873
|
* WHICH delivered frames the decode worker retains a native copy of.
|
|
33792
33874
|
*
|
|
@@ -34062,6 +34144,85 @@ function removeCustomModel(list, modelId) {
|
|
|
34062
34144
|
removed: next.length !== list.length
|
|
34063
34145
|
};
|
|
34064
34146
|
}
|
|
34147
|
+
var MODEL_UPLOAD_ALLOWED_EXTENSIONS = [".onnx", ".tflite"];
|
|
34148
|
+
var FILENAME_RE = /^[a-zA-Z0-9._-]+$/;
|
|
34149
|
+
/**
|
|
34150
|
+
* Validate an upload filename: plain basename (no separators, no traversal),
|
|
34151
|
+
* conservative charset, allowlisted model extension. Returns it unchanged so
|
|
34152
|
+
* call sites read as an assertion. Throws with an actionable message.
|
|
34153
|
+
*/
|
|
34154
|
+
function sanitizeModelFilename(filename) {
|
|
34155
|
+
if (filename.includes("/") || filename.includes("\\") || filename.includes("..") || !FILENAME_RE.test(filename)) throw new Error(`Invalid model filename "${filename}" — plain basename with letters, digits, dot, underscore, hyphen only`);
|
|
34156
|
+
if (!MODEL_UPLOAD_ALLOWED_EXTENSIONS.some((ext) => filename.endsWith(ext))) throw new Error(`Unsupported model file extension on "${filename}" — expected one of ${MODEL_UPLOAD_ALLOWED_EXTENSIONS.join(", ")}`);
|
|
34157
|
+
return filename;
|
|
34158
|
+
}
|
|
34159
|
+
var ModelUploadStore = class {
|
|
34160
|
+
uploads = /* @__PURE__ */ new Map();
|
|
34161
|
+
opts;
|
|
34162
|
+
maxBytes;
|
|
34163
|
+
constructor(opts) {
|
|
34164
|
+
this.opts = opts;
|
|
34165
|
+
this.maxBytes = opts.maxBytes ?? 536870912;
|
|
34166
|
+
}
|
|
34167
|
+
begin(input) {
|
|
34168
|
+
const filename = sanitizeModelFilename(input.filename);
|
|
34169
|
+
if (!Number.isFinite(input.totalBytes) || input.totalBytes <= 0) throw new Error("totalBytes must be a positive number");
|
|
34170
|
+
if (input.totalBytes > this.maxBytes) throw new Error(`Upload too large: ${input.totalBytes} bytes exceeds the ${this.maxBytes}-byte cap`);
|
|
34171
|
+
fs$1.mkdirSync(this.opts.stagingDir, { recursive: true });
|
|
34172
|
+
const uploadId = randomUUID();
|
|
34173
|
+
const stagingPath = path$1.join(this.opts.stagingDir, `${uploadId}-${filename}`);
|
|
34174
|
+
fs$1.writeFileSync(stagingPath, Buffer.alloc(0));
|
|
34175
|
+
this.uploads.set(uploadId, {
|
|
34176
|
+
filename,
|
|
34177
|
+
totalBytes: input.totalBytes,
|
|
34178
|
+
stagingPath,
|
|
34179
|
+
hash: createHash("sha256"),
|
|
34180
|
+
received: 0
|
|
34181
|
+
});
|
|
34182
|
+
return { uploadId };
|
|
34183
|
+
}
|
|
34184
|
+
append(input) {
|
|
34185
|
+
const upload = this.mustGet(input.uploadId);
|
|
34186
|
+
const chunk = Buffer.from(input.chunkBase64, "base64");
|
|
34187
|
+
if (chunk.length === 0) throw new Error("Empty chunk");
|
|
34188
|
+
if (upload.received + chunk.length > upload.totalBytes) {
|
|
34189
|
+
this.abort({ uploadId: input.uploadId });
|
|
34190
|
+
throw new Error(`Chunk exceeds the declared totalBytes (${upload.totalBytes}) — upload aborted`);
|
|
34191
|
+
}
|
|
34192
|
+
fs$1.appendFileSync(upload.stagingPath, chunk);
|
|
34193
|
+
upload.hash.update(chunk);
|
|
34194
|
+
upload.received += chunk.length;
|
|
34195
|
+
return { received: upload.received };
|
|
34196
|
+
}
|
|
34197
|
+
commit(input) {
|
|
34198
|
+
const upload = this.mustGet(input.uploadId);
|
|
34199
|
+
if (upload.received !== upload.totalBytes) throw new Error(`Upload incomplete: received ${upload.received} of ${upload.totalBytes} bytes`);
|
|
34200
|
+
fs$1.mkdirSync(this.opts.modelsDir, { recursive: true });
|
|
34201
|
+
const destPath = path$1.join(this.opts.modelsDir, upload.filename);
|
|
34202
|
+
fs$1.renameSync(upload.stagingPath, destPath);
|
|
34203
|
+
this.uploads.delete(input.uploadId);
|
|
34204
|
+
return {
|
|
34205
|
+
filename: upload.filename,
|
|
34206
|
+
bytes: upload.received,
|
|
34207
|
+
sha256: upload.hash.digest("hex")
|
|
34208
|
+
};
|
|
34209
|
+
}
|
|
34210
|
+
abort(input) {
|
|
34211
|
+
const upload = this.uploads.get(input.uploadId);
|
|
34212
|
+
if (upload) {
|
|
34213
|
+
try {
|
|
34214
|
+
fs$1.rmSync(upload.stagingPath, { force: true });
|
|
34215
|
+
} catch {}
|
|
34216
|
+
this.uploads.delete(input.uploadId);
|
|
34217
|
+
}
|
|
34218
|
+
return { ok: true };
|
|
34219
|
+
}
|
|
34220
|
+
mustGet(uploadId) {
|
|
34221
|
+
const upload = this.uploads.get(uploadId);
|
|
34222
|
+
if (!upload) throw new Error(`Unknown uploadId "${uploadId}" — begin a new upload`);
|
|
34223
|
+
return upload;
|
|
34224
|
+
}
|
|
34225
|
+
};
|
|
34065
34226
|
//#endregion
|
|
34066
34227
|
//#region src/model-studio-actions.ts
|
|
34067
34228
|
/**
|
|
@@ -34102,6 +34263,28 @@ var modelStudioActions = defineCustomActions({
|
|
|
34102
34263
|
}), { kind: "mutation" }),
|
|
34103
34264
|
listAvailability: customAction(_void(), ModelAvailabilityMapSchema),
|
|
34104
34265
|
/**
|
|
34266
|
+
* Chunked model-file upload (Frigate import). One tRPC mutation cannot
|
|
34267
|
+
* carry a model file (hub HTTP body limit ~1 MB), so the page streams
|
|
34268
|
+
* base64 chunks: begin → append× → commit. Commit lands the artifact in the
|
|
34269
|
+
* hub's shared modelsDir, where the `camstack-local://` catalog-URL
|
|
34270
|
+
* convention resolves it by basename and `model-distributor` can ship it to
|
|
34271
|
+
* agents. Abort (or an oversize chunk) discards the staging file.
|
|
34272
|
+
*/
|
|
34273
|
+
beginModelUpload: customAction(object({
|
|
34274
|
+
filename: string(),
|
|
34275
|
+
totalBytes: number().int().positive()
|
|
34276
|
+
}), object({ uploadId: string() }), { kind: "mutation" }),
|
|
34277
|
+
appendModelUpload: customAction(object({
|
|
34278
|
+
uploadId: string(),
|
|
34279
|
+
chunkBase64: string()
|
|
34280
|
+
}), object({ received: number() }), { kind: "mutation" }),
|
|
34281
|
+
commitModelUpload: customAction(object({ uploadId: string() }), object({
|
|
34282
|
+
filename: string(),
|
|
34283
|
+
bytes: number(),
|
|
34284
|
+
sha256: string()
|
|
34285
|
+
}), { kind: "mutation" }),
|
|
34286
|
+
abortModelUpload: customAction(object({ uploadId: string() }), object({ ok: literal(true) }), { kind: "mutation" }),
|
|
34287
|
+
/**
|
|
34105
34288
|
* Rank nodes for model conversion based on hardware capabilities.
|
|
34106
34289
|
* CoreML targets require darwin; OpenVINO targets require an OpenVINO-capable node.
|
|
34107
34290
|
*/
|
|
@@ -34199,6 +34382,8 @@ var ModelStudioAddon = class extends BaseAddon {
|
|
|
34199
34382
|
id = "model-studio";
|
|
34200
34383
|
modelsState = null;
|
|
34201
34384
|
availabilityState = null;
|
|
34385
|
+
/** Lazily-built chunked-upload store (hub only) — see `upload/model-upload-store.ts`. */
|
|
34386
|
+
uploadStore = null;
|
|
34202
34387
|
/**
|
|
34203
34388
|
* Module-Federation page declaration surfaced via the `addon-pages-source`
|
|
34204
34389
|
* collection cap. Mirrors the manifest `pages[]` entry — admin-ui's
|
|
@@ -34285,6 +34470,18 @@ var ModelStudioAddon = class extends BaseAddon {
|
|
|
34285
34470
|
providers,
|
|
34286
34471
|
customActions: isHub ? modelStudioActions : void 0,
|
|
34287
34472
|
actionHandlers: isHub ? {
|
|
34473
|
+
beginModelUpload: async (input) => (await this.getUploadStore()).begin(input),
|
|
34474
|
+
appendModelUpload: async (input) => (await this.getUploadStore()).append(input),
|
|
34475
|
+
commitModelUpload: async (input) => {
|
|
34476
|
+
const result = (await this.getUploadStore()).commit(input);
|
|
34477
|
+
this.ctx.logger.info("Model upload landed in modelsDir", { meta: {
|
|
34478
|
+
filename: result.filename,
|
|
34479
|
+
bytes: result.bytes,
|
|
34480
|
+
sha256: result.sha256
|
|
34481
|
+
} });
|
|
34482
|
+
return result;
|
|
34483
|
+
},
|
|
34484
|
+
abortModelUpload: async (input) => (await this.getUploadStore()).abort(input),
|
|
34288
34485
|
registerCustomModel: async (input) => this.registerCustomModel(input),
|
|
34289
34486
|
removeCustomModel: async (input) => this.removeCustomModel(input),
|
|
34290
34487
|
listCustomModels: async () => this.listCustomModels(),
|
|
@@ -34366,6 +34563,22 @@ var ModelStudioAddon = class extends BaseAddon {
|
|
|
34366
34563
|
} : void 0
|
|
34367
34564
|
};
|
|
34368
34565
|
}
|
|
34566
|
+
/**
|
|
34567
|
+
* Build (once) the upload store, anchored at this node's writable data root:
|
|
34568
|
+
* staging under `<dataDir>/model-uploads`, commits into the SHARED
|
|
34569
|
+
* `<nodeDataDir>/models` — the same directory every inference addon loads
|
|
34570
|
+
* from, which is what makes the `camstack-local://` URL convention resolve.
|
|
34571
|
+
*/
|
|
34572
|
+
async getUploadStore() {
|
|
34573
|
+
if (this.uploadStore) return this.uploadStore;
|
|
34574
|
+
const modelsDir = await this.resolveModelsDir();
|
|
34575
|
+
const stagingDir = path.join(this.ctx.dataDir ?? os.tmpdir(), "model-uploads");
|
|
34576
|
+
this.uploadStore = new ModelUploadStore({
|
|
34577
|
+
stagingDir,
|
|
34578
|
+
modelsDir
|
|
34579
|
+
});
|
|
34580
|
+
return this.uploadStore;
|
|
34581
|
+
}
|
|
34369
34582
|
get state_() {
|
|
34370
34583
|
if (!this.modelsState) throw new Error("ModelStudioAddon: durable state not initialized");
|
|
34371
34584
|
return this.modelsState;
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { h as e, l as t, u as n, y as r } from "./_virtual_mf___mfe_internal__addon_model_studio_page__loadShare__react__loadShare__.js-DJDHChgO.mjs";
|
|
2
|
-
import { o as i, s as a } from "./responsive-
|
|
2
|
+
import { o as i, s as a } from "./responsive-CWtuZhDz.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 = a("chevron-down", [["path", {
|
|
5
5
|
d: "m6 9 6 6 6-6",
|
package/dist/remoteEntry.js
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
import { n as e, t } from "./virtual_mf-REMOTE_ENTRY_ID___mfe_internal__addon_model_studio_page__remoteEntry_js-
|
|
1
|
+
import { n as e, t } from "./virtual_mf-REMOTE_ENTRY_ID___mfe_internal__addon_model_studio_page__remoteEntry_js-CnPUM5JZ.mjs";
|
|
2
2
|
export { t as get, e as init };
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { c as e, g as t, h as n, l as r, n as i, p as a, r as o, t as s, u as c, y as l } from "./_virtual_mf___mfe_internal__addon_model_studio_page__loadShare__react__loadShare__.js-DJDHChgO.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 {
|
|
3
|
+
import { t as u } from "./_virtual_mf___mfe_internal__addon_model_studio_page__loadShare___mf_0_camstack_mf_1_types__loadShare__.js-Do4Kqwhj.mjs";
|
|
4
4
|
//#region ../ui-library/node_modules/lucide-react/dist/esm/shared/src/utils/mergeClasses.js
|
|
5
5
|
l();
|
|
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 { c as e, h as t, p as n, y as r } from "./_virtual_mf___mfe_internal__addon_model_studio_page__loadShare__react__loadShare__.js-DJDHChgO.mjs";
|
|
2
|
-
import { s as i } from "./responsive-
|
|
2
|
+
import { s as i } from "./responsive-CWtuZhDz.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"
|
|
@@ -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-
|
|
2756
|
+
return tr ||= rr(() => import("./_virtual_mf-localSharedImportMap___mfe_internal__addon_model_studio_page-Cg1VXUiH.mjs")).catch((e) => {
|
|
2757
2757
|
throw tr = void 0, e;
|
|
2758
2758
|
}), tr;
|
|
2759
2759
|
}
|
package/package.json
CHANGED
package/python/convert_lib.py
CHANGED
|
@@ -65,7 +65,14 @@ def quantize_openvino_int8(
|
|
|
65
65
|
ov.save_model(quantized, out_xml)
|
|
66
66
|
|
|
67
67
|
|
|
68
|
-
def export_coreml_from_onnx(
|
|
68
|
+
def export_coreml_from_onnx(
|
|
69
|
+
onnx_path: str,
|
|
70
|
+
out_pkg: str,
|
|
71
|
+
imgsz: int,
|
|
72
|
+
*,
|
|
73
|
+
input_shape: "tuple[int, ...] | None" = None,
|
|
74
|
+
fp32: bool = False,
|
|
75
|
+
) -> None:
|
|
69
76
|
"""ONNX -> CoreML mlpackage (darwin only).
|
|
70
77
|
|
|
71
78
|
Modern coremltools (>=6) dropped the ONNX frontend entirely — ``ct.convert``
|
|
@@ -75,6 +82,22 @@ def export_coreml_from_onnx(onnx_path: str, out_pkg: str, imgsz: int) -> None:
|
|
|
75
82
|
it with a representative image input, then coremltools converts the
|
|
76
83
|
TorchScript. Image models are NCHW ``(1, 3, imgsz, imgsz)``.
|
|
77
84
|
|
|
85
|
+
``input_shape`` overrides that square-NCHW assumption for models whose input
|
|
86
|
+
is neither square nor channels-first — the plate recognizer is NHWC
|
|
87
|
+
``(1, 64, 128, 3)``. Without it the trace runs on a wrongly-shaped tensor and
|
|
88
|
+
either throws deep inside the graph or, worse, produces a package whose
|
|
89
|
+
declared input silently disagrees with the real one. ``imgsz`` stays the
|
|
90
|
+
positional contract for every existing caller.
|
|
91
|
+
|
|
92
|
+
``fp32`` keeps full precision instead of coremltools' default fp16 weights.
|
|
93
|
+
Detectors and embedders absorb fp16 — a box moves a fraction of a pixel, a
|
|
94
|
+
cosine moves 5e-5 — but a RECOGNIZER cannot: its output is an argmax plus a
|
|
95
|
+
confidence threshold, so a small shift in one softmax flips a stored
|
|
96
|
+
character or silences a real read. Measured on the plate recognizer, fp16
|
|
97
|
+
moved the weakest-character probability by up to 0.16 and changed the label
|
|
98
|
+
on a ground-truthed plate. The default stays fp16 so nothing already
|
|
99
|
+
published moves.
|
|
100
|
+
|
|
78
101
|
Dynamic-shape models (e.g. YOLO) emit ``Shape`` ops that coremltools' Torch
|
|
79
102
|
converter cannot lower ("Converter is not implemented … 'Shape'"). We first
|
|
80
103
|
run onnx-simplifier with the input shape pinned, which constant-folds those
|
|
@@ -86,16 +109,15 @@ def export_coreml_from_onnx(onnx_path: str, out_pkg: str, imgsz: int) -> None:
|
|
|
86
109
|
from onnx2torch import convert as onnx_to_torch
|
|
87
110
|
from onnxsim import simplify
|
|
88
111
|
|
|
112
|
+
shape = list(input_shape) if input_shape is not None else [1, 3, imgsz, imgsz]
|
|
89
113
|
onnx_model = onnx.load(onnx_path)
|
|
90
114
|
input_name = onnx_model.graph.input[0].name
|
|
91
|
-
simplified, ok = simplify(
|
|
92
|
-
onnx_model, overwrite_input_shapes={input_name: [1, 3, imgsz, imgsz]}
|
|
93
|
-
)
|
|
115
|
+
simplified, ok = simplify(onnx_model, overwrite_input_shapes={input_name: shape})
|
|
94
116
|
if not ok:
|
|
95
117
|
simplified = onnx_model # fall back to the original if simplify bailed
|
|
96
118
|
|
|
97
119
|
model = onnx_to_torch(simplified).eval()
|
|
98
|
-
example = torch.rand(
|
|
120
|
+
example = torch.rand(*shape)
|
|
99
121
|
with torch.no_grad():
|
|
100
122
|
traced = torch.jit.trace(model, example, strict=False)
|
|
101
123
|
|
|
@@ -104,6 +126,7 @@ def export_coreml_from_onnx(onnx_path: str, out_pkg: str, imgsz: int) -> None:
|
|
|
104
126
|
source="pytorch",
|
|
105
127
|
inputs=[ct.TensorType(name="input", shape=example.shape)],
|
|
106
128
|
minimum_deployment_target=ct.target.iOS16,
|
|
129
|
+
**({"compute_precision": ct.precision.FLOAT32} if fp32 else {}),
|
|
107
130
|
)
|
|
108
131
|
Path(out_pkg).parent.mkdir(parents=True, exist_ok=True)
|
|
109
132
|
mlmodel.save(out_pkg)
|