@camstack/addon-training 0.1.5 → 0.1.7

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 (44) hide show
  1. package/dist/MaskShapeCanvas-Kfg9dl6B.mjs +686 -0
  2. package/dist/MotionZonesSettings-DKtYdHUO.mjs +292 -0
  3. package/dist/{PrivacyMaskSettings-BwYjIkQR.mjs → PrivacyMaskSettings-BvLUVz86.mjs} +45 -70
  4. package/dist/ReactKonva-DGuYUN0L.mjs +5416 -0
  5. package/dist/{SceneMonitorEditor-BBfow3Lm.mjs → SceneMonitorEditor-rFYGkGHV.mjs} +344 -344
  6. package/dist/_stub.js +25672 -10252
  7. package/dist/{_virtual_mf-localSharedImportMap___mfe_internal__addon_training_page-CGhkyuU-.mjs → _virtual_mf-localSharedImportMap___mfe_internal__addon_training_page-B3xXUcaU.mjs} +3 -3
  8. package/dist/_virtual_mf___mfe_internal__addon_training_page__loadShare___mf_0_camstack_mf_1_types__loadShare__.js-C7dks3FG.mjs +26 -0
  9. package/dist/_virtual_mf___mfe_internal__addon_training_page__loadShare___mf_0_tanstack_mf_1_react_mf_2_query__loadShare__.js-DpjirGmB.mjs +89 -0
  10. package/dist/_virtual_mf___mfe_internal__addon_training_page__loadShare___mf_0_trpc_mf_1_client__loadShare__.js-CFCVuRAs.mjs +58 -0
  11. package/dist/_virtual_mf___mfe_internal__addon_training_page__loadShare___mf_0_trpc_mf_1_react_mf_2_query__loadShare__.js-BkvDudqY.mjs +62 -0
  12. package/dist/{_virtual_mf___mfe_internal__addon_training_page__loadShare__react__loadShare__.js-BT_wL8s3.mjs → _virtual_mf___mfe_internal__addon_training_page__loadShare__react__loadShare__.js-DeDbF7NI.mjs} +1 -1
  13. package/dist/_virtual_mf___mfe_internal__addon_training_page__loadShare__react_mf_1_jsx_mf_2_runtime__loadShare__.js-DnrbBvrw.mjs +34 -0
  14. package/dist/_virtual_mf___mfe_internal__addon_training_page__loadShare__react_mf_2_dom__loadShare__.js-DAZkHxxU.mjs +45 -0
  15. package/dist/_virtual_mf___mfe_internal__addon_training_page__loadShare__react_mf_2_dom_mf_1_client__loadShare__.js-CHqN9NLt.mjs +34 -0
  16. package/dist/addon-training.css +1 -1
  17. package/dist/{dist-BiSFqfpy.js → dist-DVDBSrjw.js} +282 -147
  18. package/dist/{dist-B_79m03G.mjs → dist-Phot2UwD.mjs} +283 -142
  19. package/dist/grid-2x2-Bl3xw1w7.mjs +21 -0
  20. package/dist/{hostInit-BEPknhsh.mjs → hostInit-BAGO4XYA.mjs} +2 -2
  21. package/dist/index.js +100 -39
  22. package/dist/index.mjs +99 -38
  23. package/dist/lib-CVGFTwe0.mjs +7770 -0
  24. package/dist/orchestrator/training.addon.js +376 -222
  25. package/dist/orchestrator/training.addon.mjs +376 -222
  26. package/dist/{player-overlays-wE5IcDiI.mjs → player-overlays-BT-6fK9N.mjs} +29 -10
  27. package/dist/providers/modal/training-modal.addon.js +361 -155
  28. package/dist/providers/modal/training-modal.addon.mjs +364 -158
  29. package/dist/remoteEntry.js +1 -1
  30. package/dist/{responsive-D17kMsm1.mjs → responsive-Dy5RAD1s.mjs} +42 -38
  31. package/dist/{scene-monitor-copy-LjlvhSRM.mjs → scene-monitor-copy-7HImRbA5.mjs} +4 -4
  32. package/dist/{square-JGDHhLlY.mjs → square-DoPBhbZN.mjs} +29 -4
  33. package/dist/{trash-2-zeFnLJvf.mjs → trash-2-BwunNgLf.mjs} +1 -1
  34. package/dist/{virtual_mf-REMOTE_ENTRY_ID___mfe_internal__addon_training_page__remoteEntry_js-C0RB_rkV.mjs → virtual_mf-REMOTE_ENTRY_ID___mfe_internal__addon_training_page__remoteEntry_js-gU49KvnZ.mjs} +399 -390
  35. package/package.json +1 -1
  36. package/dist/MaskShapeCanvas-BokARvOj.mjs +0 -13806
  37. package/dist/MotionZonesSettings-aj085Nrn.mjs +0 -310
  38. package/dist/_virtual_mf___mfe_internal__addon_training_page__loadShare___mf_0_camstack_mf_1_types__loadShare__.js-BkKuzhOJ.mjs +0 -26
  39. package/dist/_virtual_mf___mfe_internal__addon_training_page__loadShare___mf_0_tanstack_mf_1_react_mf_2_query__loadShare__.js-CaB1jnkt.mjs +0 -25
  40. package/dist/_virtual_mf___mfe_internal__addon_training_page__loadShare___mf_0_trpc_mf_1_client__loadShare__.js-B3rryujf.mjs +0 -25
  41. package/dist/_virtual_mf___mfe_internal__addon_training_page__loadShare___mf_0_trpc_mf_1_react_mf_2_query__loadShare__.js-nL4MGFRP.mjs +0 -26
  42. package/dist/_virtual_mf___mfe_internal__addon_training_page__loadShare__react_mf_1_jsx_mf_2_runtime__loadShare__.js-DDBEvLdo.mjs +0 -26
  43. package/dist/_virtual_mf___mfe_internal__addon_training_page__loadShare__react_mf_2_dom__loadShare__.js-i_pnGDxd.mjs +0 -25
  44. package/dist/_virtual_mf___mfe_internal__addon_training_page__loadShare__react_mf_2_dom_mf_1_client__loadShare__.js-gTIQrhKs.mjs +0 -25
@@ -1,5 +1,5 @@
1
1
  Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
2
- const require_dist = require("../dist-BiSFqfpy.js");
2
+ const require_dist = require("../dist-DVDBSrjw.js");
3
3
  let node_path = require("node:path");
4
4
  node_path = require_dist.__toESM(node_path);
5
5
  let node_crypto = require("node:crypto");
@@ -8,22 +8,123 @@ let node_fs_promises = require("node:fs/promises");
8
8
  //#region src/orchestrator/api-ports.ts
9
9
  var DETECTOR_STEP = "object-detection";
10
10
  var REGISTRY_CAP = "custom-model-registry";
11
- /** The provider cap, routed to ONE provider by the `addonId` every call carries. */
11
+ /** The provider cap, with entryId → owning addon resolution for collection pins. */
12
12
  function providerPort(api) {
13
13
  const p = api.trainingProvider;
14
+ const addonForEntry = async (entryId) => {
15
+ const entry = (await p.listEntries.query({})).find((e) => e.entryId === entryId);
16
+ if (entry === void 0) throw new Error(`unknown training provider entry "${entryId}"`);
17
+ return entry.addonId;
18
+ };
19
+ const addonForType = async (typeId) => {
20
+ const type = (await p.describeTypes.query({})).find((t) => t.typeId === typeId);
21
+ if (type === void 0) throw new Error(`unknown training provider type "${typeId}"`);
22
+ return type.addonId;
23
+ };
14
24
  return {
15
25
  describe: () => p.describe.query({}),
16
- validateCredentials: (i) => p.validateCredentials.mutate(i),
17
- estimateJob: (i) => p.estimateJob.query(i),
18
- uploadDataset: (i) => p.uploadDataset.mutate(i),
19
- submitJob: (i) => p.submitJob.mutate(i),
20
- getJob: (i) => p.getJob.query(i),
21
- listJobEvents: (i) => p.listJobEvents.query(i),
22
- cancelJob: (i) => p.cancelJob.mutate(i),
23
- readResult: (i) => p.readResult.mutate(i),
24
- offerArtifact: (i) => p.offerArtifact.mutate(i),
25
- cleanupJob: (i) => p.cleanupJob.mutate(i),
26
- listJobs: (i) => p.listJobs.query(i)
26
+ describeTypes: () => p.describeTypes.query({}),
27
+ listEntries: () => p.listEntries.query({}),
28
+ createEntry: async (i) => {
29
+ const addonId = await addonForType(i.typeId);
30
+ return p.createEntry.mutate({
31
+ ...i,
32
+ addonId
33
+ });
34
+ },
35
+ deleteEntry: async (i) => {
36
+ const addonId = await addonForEntry(i.entryId);
37
+ return p.deleteEntry.mutate({
38
+ ...i,
39
+ addonId
40
+ });
41
+ },
42
+ validateCredentials: async (i) => {
43
+ const addonId = await addonForEntry(i.entryId);
44
+ return p.validateCredentials.mutate({
45
+ ...i,
46
+ addonId
47
+ });
48
+ },
49
+ estimateJob: async (i) => {
50
+ const addonId = await addonForEntry(i.entryId);
51
+ return p.estimateJob.query({
52
+ ...i,
53
+ addonId
54
+ });
55
+ },
56
+ uploadDataset: async (i) => {
57
+ const addonId = await addonForEntry(i.entryId);
58
+ return p.uploadDataset.mutate({
59
+ ...i,
60
+ addonId
61
+ });
62
+ },
63
+ submitJob: async (i) => {
64
+ const addonId = await addonForEntry(i.entryId);
65
+ return p.submitJob.mutate({
66
+ ...i,
67
+ addonId
68
+ });
69
+ },
70
+ getJob: async (i) => {
71
+ const addonId = await addonForEntry(i.entryId);
72
+ return p.getJob.query({
73
+ ...i,
74
+ addonId
75
+ });
76
+ },
77
+ listJobEvents: async (i) => {
78
+ const addonId = await addonForEntry(i.entryId);
79
+ return p.listJobEvents.query({
80
+ ...i,
81
+ addonId
82
+ });
83
+ },
84
+ cancelJob: async (i) => {
85
+ const addonId = await addonForEntry(i.entryId);
86
+ return p.cancelJob.mutate({
87
+ ...i,
88
+ addonId
89
+ });
90
+ },
91
+ readResult: async (i) => {
92
+ const addonId = await addonForEntry(i.entryId);
93
+ return p.readResult.mutate({
94
+ ...i,
95
+ addonId
96
+ });
97
+ },
98
+ offerArtifact: async (i) => {
99
+ const addonId = await addonForEntry(i.entryId);
100
+ return p.offerArtifact.mutate({
101
+ ...i,
102
+ addonId
103
+ });
104
+ },
105
+ cleanupJob: async (i) => {
106
+ const addonId = await addonForEntry(i.entryId);
107
+ return p.cleanupJob.mutate({
108
+ ...i,
109
+ addonId
110
+ });
111
+ },
112
+ listJobs: async (i) => {
113
+ if (i.entryId !== void 0) {
114
+ const addonId = await addonForEntry(i.entryId);
115
+ return p.listJobs.query({
116
+ ...i,
117
+ addonId
118
+ });
119
+ }
120
+ const types = await p.describeTypes.query({});
121
+ const seen = /* @__PURE__ */ new Set();
122
+ return (await Promise.all(types.map(async (t) => {
123
+ if (seen.has(t.addonId)) return [];
124
+ seen.add(t.addonId);
125
+ return p.listJobs.query({ addonId: t.addonId });
126
+ }))).flat();
127
+ }
27
128
  };
28
129
  }
29
130
  function datasetPort(api) {
@@ -38,10 +139,13 @@ function datasetPort(api) {
38
139
  };
39
140
  } };
40
141
  }
142
+ function corpusPort(api) {
143
+ return { resolve: (selection) => api.pipelineAnalytics.resolveRetrainTrainingCorpus.query(selection) };
144
+ }
41
145
  /** Model Studio's registry: the hub's provider of `custom-model-registry`. */
42
146
  function registryPort(api) {
43
147
  return { importBundle: async (input) => {
44
- const hub = (await api.addons.listCapabilityProviders.query({ capName: REGISTRY_CAP })).find((p) => !p.addonId.includes("@"));
148
+ const hub = (await api.addons.listCapabilityProviders.query({ capName: REGISTRY_CAP })).find((prov) => !prov.addonId.includes("@"));
45
149
  if (hub === void 0) throw new Error("no custom-model registry on the hub — is Model Studio installed?");
46
150
  return api.customModelRegistry.importModelBundle.mutate({
47
151
  ...input,
@@ -540,27 +644,36 @@ function dataNoticeFor(provider, region, frames) {
540
644
  return `${String(frames)} frames and their boxes are uploaded to YOUR ${provider.label} account, ${where}. ${provider.dataResidencyNote} They are deleted there as soon as the result is fetched (${String(grace)} h after a failure, so it can be inspected), and they are never put in a log, an environment variable or the job file. The result stays on this hub for ${String(14)} days; nothing is registered until you click Register.`;
541
645
  }
542
646
  async function planJob(request, deps) {
543
- const version = await deps.versions.get(request.datasetVersionId);
544
- if (version === null) return refused("unknown-dataset", `dataset version "${request.datasetVersionId}" does not exist`);
545
- const provider = (await deps.provider.describe({})).find((p) => p.addonId === request.addonId);
546
- if (provider === void 0) return refused("unknown-provider", `no training provider "${request.addonId}"`);
547
- const notice = dataNoticeFor(provider, request.region, version.frameIds.length);
548
- if (!provider.configured) return refused("provider-not-configured", `${provider.label} has no credentials yet — set them in its settings`, notice);
647
+ const resolved = await deps.corpus.resolve(request.selection);
648
+ if (resolved.kind === "refused") {
649
+ if (resolved.reason === "unknown-collection") return refused("unknown-dataset", resolved.detail);
650
+ return refused("no-training-subjects", resolved.detail);
651
+ }
652
+ const corpus = {
653
+ frameIds: resolved.frameIds,
654
+ stats: resolved.stats,
655
+ annotationHash: resolved.annotationHash,
656
+ layout: resolved.layout
657
+ };
658
+ const provider = (await deps.provider.describe({})).find((p) => p.entryId === request.entryId);
659
+ if (provider === void 0) return refused("unknown-provider", `no training provider entry "${request.entryId}"`);
660
+ const notice = dataNoticeFor(provider, request.region, corpus.frameIds.length);
661
+ if (!provider.configured) return refused("provider-not-configured", `${provider.name} (${provider.label}) has no credentials yet — set them on the entry`, notice);
549
662
  const base = findBaseModel(request.baseModelId);
550
663
  if (base === null) return refused("unknown-base-model", `"${request.baseModelId}" is not a trainable base`, notice);
551
664
  if (!knownGpu(provider.gpuTypes, request.gpuType)) return refused("unknown-gpu", `${provider.label} offers no "${request.gpuType}"`, notice);
552
- const datasetBytes = datasetBytesEstimate(version.frameIds.length);
665
+ const datasetBytes = datasetBytesEstimate(corpus.frameIds.length);
553
666
  if (datasetBytes > provider.maxDatasetBytes) return refused("dataset-too-large", `about ${String(Math.round(datasetBytes / 1e6))} MB is over ${provider.label}'s limit`, notice);
554
- const { split, warnings } = planSplit(version.stats, request.holdoutCameras);
667
+ const { split, warnings } = planSplit(corpus.stats, request.holdoutCameras);
555
668
  if (split.trainSubjects <= 0) return refused("no-training-subjects", "no boxed subject is left to train on once the holdout is set aside", notice);
556
669
  const gpu = estimateGpuSeconds({
557
- frames: version.frameIds.length,
670
+ frames: corpus.frameIds.length,
558
671
  base,
559
672
  preset: request.preset,
560
673
  gpuType: request.gpuType
561
674
  });
562
675
  const usd = await deps.provider.estimateJob({
563
- addonId: request.addonId,
676
+ entryId: request.entryId,
564
677
  gpuType: request.gpuType,
565
678
  region: request.region,
566
679
  datasetBytes,
@@ -570,7 +683,7 @@ async function planJob(request, deps) {
570
683
  const maxCostUsd = defaultCostCap(usd.usdHigh, provider.defaults.costCapUsd);
571
684
  const allWarnings = [...warnings];
572
685
  if (usd.usdHigh > maxCostUsd) allWarnings.push(`The high estimate ($${usd.usdHigh.toFixed(2)}) is above the cost cap ($${maxCostUsd.toFixed(2)}): the job may be stopped before it finishes.`);
573
- if (version.stats.completeness < .5) allWarnings.push("Fewer than half of the frames are marked complete: evaluation uses complete frames only, so the comparison rests on few frames.");
686
+ if (corpus.stats.completeness < .5) allWarnings.push("Fewer than half of the frames are marked complete: evaluation uses complete frames only, so the comparison rests on few frames.");
574
687
  return {
575
688
  plan: {
576
689
  refusal: null,
@@ -592,7 +705,7 @@ async function planJob(request, deps) {
592
705
  dataNotice: notice
593
706
  },
594
707
  accepted: {
595
- version,
708
+ corpus,
596
709
  provider,
597
710
  base,
598
711
  split
@@ -670,14 +783,14 @@ var TrainingEngine = class {
670
783
  kind: "started",
671
784
  job: existing
672
785
  };
673
- const active = (await this.ports.jobs.list()).find((j) => ACTIVE.has(j.state));
674
- if (active !== void 0) return {
786
+ const busy = this.busyJob(await this.ports.jobs.list());
787
+ if (busy !== void 0) return {
675
788
  kind: "refused",
676
789
  reason: "job-already-running",
677
- detail: `"${active.name}" is still ${active.state}`
790
+ detail: `"${busy.name}" is still ${busy.state}`
678
791
  };
679
792
  const planned = await planJob(input, {
680
- versions: this.ports.versions,
793
+ corpus: this.ports.corpus,
681
794
  provider: this.ports.provider
682
795
  });
683
796
  if (planned.plan.refusal !== null || planned.accepted === null) {
@@ -691,55 +804,24 @@ var TrainingEngine = class {
691
804
  detail: refusal.detail
692
805
  };
693
806
  }
694
- const { version, split } = planned.accepted;
695
- const now = this.ports.now();
696
807
  const { name, maxCostUsd, maxRuntimeSec, idempotencyKey, ...request } = input;
697
- const job = {
698
- id: this.ports.newId(),
699
- idempotencyKey,
700
- name,
701
- createdAt: now,
702
- updatedAt: now,
703
- state: "uploading",
704
- stage: null,
705
- failure: null,
808
+ const job = this.newJobRow({
706
809
  request,
810
+ name,
707
811
  maxCostUsd,
708
812
  maxRuntimeSec,
709
- trainer: this.trainer,
710
- datasetLayout: version.layout,
711
- dataset: null,
712
- split,
713
- provider: {
714
- state: null,
715
- providerJobId: null,
716
- startedAt: null,
717
- endedAt: null,
718
- billedSeconds: 0,
719
- costUsd: 0,
720
- costIsEstimate: true,
721
- lastPolledAt: null
722
- },
723
- progress: {
724
- epoch: null,
725
- epochs: null,
726
- etaSec: null,
727
- series: []
728
- },
729
- logTail: [],
730
- logCursor: null,
731
- artifacts: null,
732
- cloudDatasetDeletedAt: null,
733
- cloudCleanupDueAt: null,
734
- registeredModelIds: [],
735
- canary: null
736
- };
813
+ idempotencyKey,
814
+ batchId: null,
815
+ state: "uploading",
816
+ accepted: planned.accepted
817
+ });
737
818
  await this.ports.jobs.put(job);
738
819
  this.ports.logger.info("training job started", { meta: {
739
820
  jobId: job.id,
740
- provider: request.addonId,
821
+ provider: request.entryId,
741
822
  gpu: request.gpuType,
742
- frames: version.frameIds.length
823
+ frames: job.frameCount,
824
+ collections: request.selection.collectionIds.length
743
825
  } });
744
826
  this.runTask(job.id, () => this.uploadAndSubmit(job.id));
745
827
  return {
@@ -747,9 +829,109 @@ var TrainingEngine = class {
747
829
  job
748
830
  };
749
831
  }
832
+ /**
833
+ * One job per base model, same live selection snapshot. The first uploads;
834
+ * siblings stay `queued` until the prior job in the batch settles.
835
+ */
836
+ async startJobBatch(input) {
837
+ const existing = await this.ports.jobs.findByIdempotencyKey(input.idempotencyKey);
838
+ if (existing !== null) return {
839
+ kind: "started",
840
+ batchId: existing.batchId ?? existing.id,
841
+ jobs: existing.batchId === null ? [existing] : (await this.ports.jobs.list()).filter((j) => j.batchId === existing.batchId).toSorted((a, b) => a.createdAt - b.createdAt)
842
+ };
843
+ const busy = this.busyJob(await this.ports.jobs.list());
844
+ if (busy !== void 0) return {
845
+ kind: "refused",
846
+ reason: "job-already-running",
847
+ detail: `"${busy.name}" is still ${busy.state}`
848
+ };
849
+ const models = uniqueBaseModelIds(input.baseModelIds);
850
+ for (const modelId of models) if (findBaseModel(modelId) === null) return {
851
+ kind: "refused",
852
+ reason: "unknown-base-model",
853
+ detail: `"${modelId}" is not a trainable base`
854
+ };
855
+ const headModel = models[0];
856
+ if (headModel === void 0) return {
857
+ kind: "refused",
858
+ reason: "unknown-base-model",
859
+ detail: "no base models given"
860
+ };
861
+ const shared = {
862
+ selection: input.selection,
863
+ entryId: input.entryId,
864
+ gpuType: input.gpuType,
865
+ region: input.region,
866
+ preset: input.preset,
867
+ freeze: input.freeze,
868
+ holdoutCameras: input.holdoutCameras
869
+ };
870
+ const planned = await planJob({
871
+ ...shared,
872
+ baseModelId: headModel
873
+ }, {
874
+ corpus: this.ports.corpus,
875
+ provider: this.ports.provider
876
+ });
877
+ if (planned.plan.refusal !== null || planned.accepted === null) {
878
+ const refusal = planned.plan.refusal ?? {
879
+ reason: "unknown-dataset",
880
+ detail: "no plan"
881
+ };
882
+ return {
883
+ kind: "refused",
884
+ reason: refusal.reason,
885
+ detail: refusal.detail
886
+ };
887
+ }
888
+ const batchId = this.ports.newId();
889
+ const jobs = [];
890
+ for (let i = 0; i < models.length; i += 1) {
891
+ const modelId = models[i];
892
+ if (modelId === void 0) continue;
893
+ const job = this.newJobRow({
894
+ request: {
895
+ ...shared,
896
+ baseModelId: modelId
897
+ },
898
+ name: input.name,
899
+ maxCostUsd: input.maxCostUsd,
900
+ maxRuntimeSec: input.maxRuntimeSec,
901
+ idempotencyKey: i === 0 ? input.idempotencyKey : `${input.idempotencyKey}:${modelId}`,
902
+ batchId,
903
+ state: i === 0 ? "uploading" : "queued",
904
+ accepted: planned.accepted
905
+ });
906
+ await this.ports.jobs.put(job);
907
+ jobs.push(job);
908
+ }
909
+ const head = jobs[0];
910
+ if (head === void 0) return {
911
+ kind: "refused",
912
+ reason: "unknown-base-model",
913
+ detail: "no base models given"
914
+ };
915
+ this.ports.logger.info("training job batch started", { meta: {
916
+ batchId,
917
+ jobIds: jobs.map((j) => j.id),
918
+ models: [...models],
919
+ frames: head.frameCount
920
+ } });
921
+ this.runTask(head.id, () => this.uploadAndSubmit(head.id));
922
+ return {
923
+ kind: "started",
924
+ batchId,
925
+ jobs
926
+ };
927
+ }
750
928
  async cancelJob(id) {
751
929
  const job = await this.ports.jobs.get(id);
752
930
  if (job === null) throw new Error(`training job "${id}" does not exist`);
931
+ if (job.state === "queued") return await this.mutator.update(id, (j) => ({
932
+ ...j,
933
+ state: "cancelled"
934
+ })) ?? job;
753
935
  if (!ACTIVE.has(job.state) || job.state === "cancelling") return job;
754
936
  const moved = await this.mutator.update(id, (j) => ({
755
937
  ...j,
@@ -757,7 +939,7 @@ var TrainingEngine = class {
757
939
  }));
758
940
  if (job.state !== "uploading") try {
759
941
  await this.ports.provider.cancelJob({
760
- addonId: job.request.addonId,
942
+ entryId: job.request.entryId,
761
943
  jobKey: id
762
944
  });
763
945
  } catch (err) {
@@ -788,7 +970,7 @@ var TrainingEngine = class {
788
970
  const swept = [];
789
971
  for (const provider of await this.ports.provider.describe({})) {
790
972
  if (!provider.configured) continue;
791
- for (const status of await this.ports.provider.listJobs({ addonId: provider.addonId })) {
973
+ for (const status of await this.ports.provider.listJobs({ entryId: provider.entryId })) {
792
974
  const record = known.get(status.jobKey);
793
975
  const live = record !== void 0 && !SETTLED.has(record.state);
794
976
  const stillRunning = status.state === "pending" || status.state === "starting" || status.state === "running";
@@ -799,11 +981,11 @@ var TrainingEngine = class {
799
981
  state: status.state
800
982
  } });
801
983
  await this.ports.provider.cancelJob({
802
- addonId: provider.addonId,
984
+ entryId: provider.entryId,
803
985
  jobKey: status.jobKey
804
986
  });
805
987
  await this.ports.provider.cleanupJob({
806
- addonId: provider.addonId,
988
+ entryId: provider.entryId,
807
989
  jobKey: status.jobKey
808
990
  });
809
991
  swept.push(status.jobKey);
@@ -824,23 +1006,19 @@ var TrainingEngine = class {
824
1006
  async uploadAndSubmit(id) {
825
1007
  const job = await this.ports.jobs.get(id);
826
1008
  if (job === null) return;
827
- const version = await this.ports.versions.get(job.request.datasetVersionId);
828
1009
  const base = findBaseModel(job.request.baseModelId);
829
- if (version === null || base === null) {
830
- await this.fail(id, "upload-failed", null, "the dataset version or base model disappeared");
1010
+ if (base === null || job.frameIds.length === 0) {
1011
+ await this.fail(id, "upload-failed", null, "the corpus snapshot or base model disappeared");
831
1012
  return;
832
1013
  }
833
1014
  let uploaded;
834
1015
  try {
835
1016
  const offer = await this.ports.datasets.offerArchive({
836
- filter: {
837
- frameIds: [...version.frameIds],
838
- ...version.completeOnly ? { completeOnly: true } : {}
839
- },
840
- layout: version.layout
1017
+ filter: { frameIds: [...job.frameIds] },
1018
+ layout: job.datasetLayout
841
1019
  });
842
1020
  uploaded = await this.ports.provider.uploadDataset({
843
- addonId: job.request.addonId,
1021
+ entryId: job.request.entryId,
844
1022
  jobKey: id,
845
1023
  ticket: offer.ticket
846
1024
  });
@@ -865,12 +1043,12 @@ var TrainingEngine = class {
865
1043
  const spec = buildJobSpec({
866
1044
  job: afterUpload ?? job,
867
1045
  base,
868
- versionId: version.id,
869
- annotationHash: version.annotationHash,
1046
+ versionId: `live-${job.annotationHash.slice(0, 12)}`,
1047
+ annotationHash: job.annotationHash,
870
1048
  datasetSha256: uploaded.sha256
871
1049
  });
872
1050
  const status = await this.ports.provider.submitJob({
873
- addonId: job.request.addonId,
1051
+ entryId: job.request.entryId,
874
1052
  jobKey: id,
875
1053
  trainer: job.trainer,
876
1054
  jobSpecJson: JSON.stringify(spec),
@@ -919,7 +1097,7 @@ var TrainingEngine = class {
919
1097
  }
920
1098
  async poll(job, now) {
921
1099
  const ref = {
922
- addonId: job.request.addonId,
1100
+ entryId: job.request.entryId,
923
1101
  jobKey: job.id
924
1102
  };
925
1103
  const status = await this.ports.provider.getJob(ref);
@@ -949,7 +1127,7 @@ var TrainingEngine = class {
949
1127
  }
950
1128
  async afterPoll(job, status, now) {
951
1129
  const ref = {
952
- addonId: job.request.addonId,
1130
+ entryId: job.request.entryId,
953
1131
  jobKey: job.id
954
1132
  };
955
1133
  const running = status.state === "pending" || status.state === "starting" || status.state === "running";
@@ -985,15 +1163,18 @@ var TrainingEngine = class {
985
1163
  await this.fail(job.id, "provider-error", job.stage, "the provider stopped the job");
986
1164
  return;
987
1165
  }
988
- if (status.state === "lost") await this.mutator.update(job.id, (j) => ({
989
- ...j,
990
- state: "lost"
991
- }));
1166
+ if (status.state === "lost") {
1167
+ await this.mutator.update(job.id, (j) => ({
1168
+ ...j,
1169
+ state: "lost"
1170
+ }));
1171
+ await this.advanceBatchQueue(job.id);
1172
+ }
992
1173
  }
993
1174
  async failureDetail(job) {
994
1175
  try {
995
1176
  const raw = await this.ports.provider.readResult({
996
- addonId: job.request.addonId,
1177
+ entryId: job.request.entryId,
997
1178
  jobKey: job.id
998
1179
  });
999
1180
  if (!raw.found || raw.text === void 0) return null;
@@ -1008,7 +1189,7 @@ var TrainingEngine = class {
1008
1189
  const job = await this.ports.jobs.get(id);
1009
1190
  if (job === null || job.state !== "collecting") return;
1010
1191
  const ref = {
1011
- addonId: job.request.addonId,
1192
+ entryId: job.request.entryId,
1012
1193
  jobKey: id
1013
1194
  };
1014
1195
  try {
@@ -1055,6 +1236,7 @@ var TrainingEngine = class {
1055
1236
  bytes: got.bytes
1056
1237
  } });
1057
1238
  await this.cleanupCloud(id);
1239
+ await this.advanceBatchQueue(id);
1058
1240
  } catch (err) {
1059
1241
  await this.fail(id, "fetch-failed", "package", errText(err));
1060
1242
  }
@@ -1065,6 +1247,7 @@ var TrainingEngine = class {
1065
1247
  state: "cancelled"
1066
1248
  }));
1067
1249
  await this.cleanupCloud(id);
1250
+ await this.advanceBatchQueue(id);
1068
1251
  }
1069
1252
  /** Delete the cloud copy (dataset first). A failure is retried by the next tick. */
1070
1253
  async cleanupCloud(id) {
@@ -1072,7 +1255,7 @@ var TrainingEngine = class {
1072
1255
  if (job === null || job.cloudDatasetDeletedAt !== null) return;
1073
1256
  try {
1074
1257
  const { deleted } = await this.ports.provider.cleanupJob({
1075
- addonId: job.request.addonId,
1258
+ entryId: job.request.entryId,
1076
1259
  jobKey: id
1077
1260
  });
1078
1261
  await this.mutator.update(id, (j) => ({
@@ -1094,7 +1277,7 @@ var TrainingEngine = class {
1094
1277
  async fail(id, reason, stage, detail) {
1095
1278
  let grace = 0;
1096
1279
  const job = await this.ports.jobs.get(id);
1097
- if (job !== null && reason === "trainer-failed") grace = ((await this.ports.provider.describe({})).find((p) => p.addonId === job.request.addonId)?.defaults.failedDatasetGraceHours ?? 0) * 60 * 6e4;
1280
+ if (job !== null && reason === "trainer-failed") grace = ((await this.ports.provider.describe({})).find((p) => p.entryId === job.request.entryId)?.defaults.failedDatasetGraceHours ?? 0) * 60 * 6e4;
1098
1281
  await this.mutator.update(id, (j) => ({
1099
1282
  ...j,
1100
1283
  state: "failed",
@@ -1111,6 +1294,7 @@ var TrainingEngine = class {
1111
1294
  stage,
1112
1295
  detail
1113
1296
  } });
1297
+ await this.advanceBatchQueue(id);
1114
1298
  }
1115
1299
  withStatus(job, status) {
1116
1300
  const mapped = job.state === "cancelling" ? "cancelling" : status.state === "pending" ? "submitted" : status.state === "starting" ? "starting" : status.state === "running" ? "running" : job.state;
@@ -1129,7 +1313,96 @@ var TrainingEngine = class {
1129
1313
  }
1130
1314
  };
1131
1315
  }
1316
+ /**
1317
+ * Blocks a new start from outside while any non-settled job is in flight —
1318
+ * GPU-holding (`ACTIVE`) or a batch sibling still `queued`. Poll/cancel keep
1319
+ * using `ACTIVE` alone (queued holds no GPU).
1320
+ */
1321
+ busyJob(jobs) {
1322
+ return jobs.find((j) => ACTIVE.has(j.state) || j.state === "queued");
1323
+ }
1324
+ newJobRow(args) {
1325
+ const now = this.ports.now();
1326
+ const { corpus, split } = args.accepted;
1327
+ return {
1328
+ id: this.ports.newId(),
1329
+ idempotencyKey: args.idempotencyKey,
1330
+ name: args.name,
1331
+ createdAt: now,
1332
+ updatedAt: now,
1333
+ state: args.state,
1334
+ batchId: args.batchId,
1335
+ stage: null,
1336
+ failure: null,
1337
+ request: args.request,
1338
+ maxCostUsd: args.maxCostUsd,
1339
+ maxRuntimeSec: args.maxRuntimeSec,
1340
+ trainer: this.trainer,
1341
+ datasetLayout: corpus.layout,
1342
+ frameCount: corpus.frameIds.length,
1343
+ frameIds: [...corpus.frameIds],
1344
+ annotationHash: corpus.annotationHash,
1345
+ dataset: null,
1346
+ split,
1347
+ provider: {
1348
+ state: null,
1349
+ providerJobId: null,
1350
+ startedAt: null,
1351
+ endedAt: null,
1352
+ billedSeconds: 0,
1353
+ costUsd: 0,
1354
+ costIsEstimate: true,
1355
+ lastPolledAt: null
1356
+ },
1357
+ progress: {
1358
+ epoch: null,
1359
+ epochs: null,
1360
+ etaSec: null,
1361
+ series: []
1362
+ },
1363
+ logTail: [],
1364
+ logCursor: null,
1365
+ artifacts: null,
1366
+ cloudDatasetDeletedAt: null,
1367
+ cloudCleanupDueAt: null,
1368
+ registeredModelIds: [],
1369
+ canary: null
1370
+ };
1371
+ }
1372
+ /**
1373
+ * When a batch member settles and nothing else is ACTIVE, promote the next
1374
+ * `queued` sibling (same `batchId`, oldest first) into `uploading`.
1375
+ */
1376
+ async advanceBatchQueue(settledId) {
1377
+ const settled = await this.ports.jobs.get(settledId);
1378
+ if (settled === null || settled.batchId === null || !SETTLED.has(settled.state)) return;
1379
+ const jobs = await this.ports.jobs.list();
1380
+ if (jobs.some((j) => ACTIVE.has(j.state))) return;
1381
+ const next = jobs.filter((j) => j.batchId === settled.batchId && j.state === "queued").toSorted((a, b) => a.createdAt - b.createdAt)[0];
1382
+ if (next === void 0) return;
1383
+ const moved = await this.mutator.update(next.id, (j) => j.state === "queued" ? {
1384
+ ...j,
1385
+ state: "uploading"
1386
+ } : j);
1387
+ if (moved === null || moved.state !== "uploading") return;
1388
+ this.ports.logger.info("training batch advanced to next model", { meta: {
1389
+ batchId: settled.batchId,
1390
+ jobId: moved.id,
1391
+ baseModelId: moved.request.baseModelId
1392
+ } });
1393
+ this.runTask(moved.id, () => this.uploadAndSubmit(moved.id));
1394
+ }
1132
1395
  };
1396
+ function uniqueBaseModelIds(ids) {
1397
+ const seen = /* @__PURE__ */ new Set();
1398
+ const out = [];
1399
+ for (const id of ids) {
1400
+ if (seen.has(id)) continue;
1401
+ seen.add(id);
1402
+ out.push(id);
1403
+ }
1404
+ return out;
1405
+ }
1133
1406
  //#endregion
1134
1407
  //#region src/orchestrator/return-flow.ts
1135
1408
  /**
@@ -1172,7 +1445,7 @@ function catalogEntryFromDraft(input) {
1172
1445
  return {
1173
1446
  id: draft.id,
1174
1447
  name: draft.name,
1175
- description: `${draft.description} Fine-tuned from ${base.modelId} by CamStack Training (job ${job.id}, dataset ${job.request.datasetVersionId}).`,
1448
+ description: `${draft.description} Fine-tuned from ${base.modelId} by CamStack Training (job ${job.id}, ${String(job.frameCount)} frames from ${String(job.request.selection.collectionIds.length)} collection(s)).`,
1176
1449
  inputSize: { ...draft.inputSize },
1177
1450
  labels: [],
1178
1451
  preprocessMode: "letterbox",
@@ -1186,7 +1459,7 @@ function catalogEntryFromDraft(input) {
1186
1459
  url: "https://github.com/ultralytics/ultralytics"
1187
1460
  },
1188
1461
  attribution: `© Ultralytics, AGPL-3.0 (YOLOv9 weights); ${result.licence}. Corresponding source: ${TRAINER_SOURCE_URL} at revision ${result.trainer.revision}.`,
1189
- modifications: `fine-tuned on the operator's own frames (dataset ${job.request.datasetVersionId}) and exported by CamStack Training`
1462
+ modifications: `fine-tuned on the operator's own frames (${String(job.frameCount)} frames; collections ${job.request.selection.collectionIds.join(", ")}) and exported by CamStack Training`
1190
1463
  },
1191
1464
  formats
1192
1465
  };
@@ -1343,7 +1616,7 @@ var ReturnFlow = class {
1343
1616
  //#endregion
1344
1617
  //#region src/orchestrator/stores.ts
1345
1618
  /**
1346
- * The orchestrator's two durable tables, both in the settings store.
1619
+ * The orchestrator's durable job table in the settings store.
1347
1620
  *
1348
1621
  * Rows are validated on the way OUT (a row a newer build wrote, or a hand
1349
1622
  * edit, never reaches the state machine as a half-shaped job) and dropped
@@ -1359,13 +1632,6 @@ var ReturnFlow = class {
1359
1632
  * separately (14 days after ready/registration) and the row says so."
1360
1633
  */
1361
1634
  var TRAINING_JOBS_COLLECTION = "training:jobs";
1362
- /**
1363
- * @durable class=ledger owner=training
1364
- * write="one row per dataset version, inserted once (cut by pipeline-analytics, or moved
1365
- * from its local store on first sight of this capability) and never updated."
1366
- * retention="none — rows leave only when the operator deletes the version."
1367
- */
1368
- var TRAINING_DATASET_VERSIONS_COLLECTION = "training:dataset-versions";
1369
1635
  var JOB_COLUMNS = [
1370
1636
  {
1371
1637
  name: "id",
@@ -1402,37 +1668,6 @@ var JOB_INDEXES = [{
1402
1668
  name: "idx_training_jobs_created",
1403
1669
  columns: ["createdAt"]
1404
1670
  }];
1405
- var VERSION_COLUMNS = [
1406
- {
1407
- name: "id",
1408
- type: "TEXT",
1409
- primaryKey: true,
1410
- notNull: true
1411
- },
1412
- {
1413
- name: "nameKey",
1414
- type: "TEXT",
1415
- notNull: true
1416
- },
1417
- {
1418
- name: "createdAt",
1419
- type: "INTEGER",
1420
- notNull: true
1421
- },
1422
- {
1423
- name: "row",
1424
- type: "JSON",
1425
- notNull: true
1426
- }
1427
- ];
1428
- var VERSION_INDEXES = [{
1429
- name: "idx_training_versions_name_key",
1430
- columns: ["nameKey"],
1431
- unique: true
1432
- }, {
1433
- name: "idx_training_versions_created",
1434
- columns: ["createdAt"]
1435
- }];
1436
1671
  function parseJson(value) {
1437
1672
  if (typeof value !== "string") return value;
1438
1673
  try {
@@ -1450,11 +1685,6 @@ async function declareTrainingCollections(store) {
1450
1685
  columns: [...JOB_COLUMNS],
1451
1686
  indexes: [...JOB_INDEXES]
1452
1687
  });
1453
- await store.declareCollection.mutate({
1454
- collection: TRAINING_DATASET_VERSIONS_COLLECTION,
1455
- columns: [...VERSION_COLUMNS],
1456
- indexes: [...VERSION_INDEXES]
1457
- });
1458
1688
  }
1459
1689
  var SettingsJobStore = class {
1460
1690
  store;
@@ -1514,74 +1744,6 @@ var SettingsJobStore = class {
1514
1744
  return null;
1515
1745
  }
1516
1746
  };
1517
- var nameKeyOf = (name) => name.trim().toLowerCase();
1518
- var SettingsVersionStore = class {
1519
- store;
1520
- logger;
1521
- constructor(store, logger) {
1522
- this.store = store;
1523
- this.logger = logger;
1524
- }
1525
- /** Insert once; an existing id is left as it is (a version is immutable). */
1526
- put = async (row) => {
1527
- if (await this.get(row.id) !== null) return false;
1528
- await this.store.set.mutate({
1529
- collection: TRAINING_DATASET_VERSIONS_COLLECTION,
1530
- key: row.id,
1531
- value: {
1532
- nameKey: nameKeyOf(row.name),
1533
- createdAt: row.createdAt,
1534
- row
1535
- }
1536
- });
1537
- return true;
1538
- };
1539
- get = async (id) => {
1540
- const row = await this.store.get.query({
1541
- collection: TRAINING_DATASET_VERSIONS_COLLECTION,
1542
- key: id
1543
- });
1544
- return row === null || row === void 0 ? null : this.parse(id, recordOf(row));
1545
- };
1546
- list = async () => {
1547
- return (await this.store.query.query({
1548
- collection: TRAINING_DATASET_VERSIONS_COLLECTION,
1549
- filter: { orderBy: {
1550
- field: "createdAt",
1551
- direction: "desc"
1552
- } }
1553
- })).flatMap((r) => {
1554
- const parsed = this.parse(r.id, recordOf(r.data));
1555
- return parsed === null ? [] : [parsed];
1556
- });
1557
- };
1558
- findIdByName = async (name) => {
1559
- return (await this.store.query.query({
1560
- collection: "training:dataset-versions",
1561
- filter: {
1562
- where: { nameKey: nameKeyOf(name) },
1563
- limit: 1
1564
- }
1565
- }))[0]?.id ?? null;
1566
- };
1567
- remove = async (id) => {
1568
- if (await this.get(id) === null) return false;
1569
- await this.store.delete.mutate({
1570
- collection: TRAINING_DATASET_VERSIONS_COLLECTION,
1571
- key: id
1572
- });
1573
- return true;
1574
- };
1575
- parse(id, data) {
1576
- const parsed = require_dist.TrainingDatasetVersionRowSchema.safeParse(parseJson(data["row"]));
1577
- if (parsed.success) return parsed.data;
1578
- this.logger.warn("training dataset version row does not parse — skipped", { meta: {
1579
- versionId: id,
1580
- issue: parsed.error.issues[0]?.message ?? "unknown"
1581
- } });
1582
- return null;
1583
- }
1584
- };
1585
1747
  //#endregion
1586
1748
  //#region src/orchestrator/trainer-source.ts
1587
1749
  var TRAINER_SOURCE = {
@@ -1622,11 +1784,10 @@ var TrainingAddon = class extends require_dist.BaseAddon {
1622
1784
  if (peer === void 0) throw new Error("training needs the peer-bytes facility");
1623
1785
  await declareTrainingCollections(api.settingsStore);
1624
1786
  const logger = this.ctx.logger;
1625
- const versions = new SettingsVersionStore(api.settingsStore, logger);
1626
1787
  const ports = {
1627
1788
  provider: providerPort(api),
1628
1789
  datasets: datasetPort(api),
1629
- versions,
1790
+ corpus: corpusPort(api),
1630
1791
  jobs: new SettingsJobStore(api.settingsStore, logger),
1631
1792
  artifacts: createDiskArtifacts(node_path.default.join(this.ctx.dataDir, "training", "jobs"), peer),
1632
1793
  registry: registryPort(api),
@@ -1639,20 +1800,13 @@ var TrainingAddon = class extends require_dist.BaseAddon {
1639
1800
  const flow = new ReturnFlow(ports, engine.mutator);
1640
1801
  this.engine = engine;
1641
1802
  const provider = {
1642
- putDatasetVersion: async (row) => ({ stored: await versions.put(row) }),
1643
- getDatasetVersion: async ({ id }) => versions.get(id),
1644
- listDatasetVersions: async () => (await versions.list()).map(({ frameIds, ...rest }) => ({
1645
- ...rest,
1646
- frameCount: frameIds.length
1647
- })),
1648
- findDatasetVersionIdByName: async ({ name }) => ({ id: await versions.findIdByName(name) }),
1649
- removeDatasetVersion: async ({ id }) => ({ removed: await versions.remove(id) }),
1650
1803
  listBaseModels: async () => BASE_MODELS.map(publicBaseModel),
1651
1804
  planJob: async (request) => (await planJob(request, {
1652
- versions,
1805
+ corpus: ports.corpus,
1653
1806
  provider: ports.provider
1654
1807
  })).plan,
1655
1808
  startJob: async (input) => engine.startJob(input),
1809
+ startJobBatch: async (input) => engine.startJobBatch(input),
1656
1810
  listJobs: async () => ports.jobs.list(),
1657
1811
  getJob: async ({ id }) => ports.jobs.get(id),
1658
1812
  cancelJob: async ({ id }) => engine.cancelJob(id),