@camstack/types 1.2.40 → 1.2.42
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/addon.js +1 -1
- package/dist/addon.mjs +1 -1
- package/dist/canonical-hash-7nfBbEqR.mjs +35 -0
- package/dist/canonical-hash-BcZHRHIx.js +40 -0
- package/dist/capabilities/index.d.ts +2 -2
- package/dist/capabilities/notification-rules.cap.d.ts +41 -0
- package/dist/capabilities/pipeline-analytics.cap.d.ts +93 -6
- package/dist/capabilities/pipeline-orchestrator.cap.d.ts +123 -0
- package/dist/capabilities/pipeline-runner.cap.d.ts +143 -7
- package/dist/capabilities/platform-probe.cap.d.ts +3 -3
- package/dist/capabilities/recording.cap.d.ts +3 -0
- package/dist/capabilities/stream-broker.cap.d.ts +300 -0
- package/dist/encode-profile.d.ts +2 -0
- package/dist/ffmpeg/encode-defaults.d.ts +89 -0
- package/dist/ffmpeg/hwaccel.d.ts +98 -0
- package/dist/ffmpeg/invocation.d.ts +250 -0
- package/dist/ffmpeg/process.d.ts +135 -0
- package/dist/ffmpeg/sharing-key.d.ts +39 -0
- package/dist/generated/addon-api.d.ts +56 -0
- package/dist/generated/device-proxy.d.ts +1 -1
- package/dist/generated/method-access-map.d.ts +1 -1
- package/dist/generated/system-proxy.d.ts +2 -2
- package/dist/index.d.ts +6 -0
- package/dist/index.js +1595 -28
- package/dist/index.mjs +1548 -29
- package/dist/interfaces/camera-switches.d.ts +217 -0
- package/dist/interfaces/ops-log.d.ts +4 -0
- package/dist/interfaces/pipeline-runner-capability.d.ts +9 -1
- package/dist/node.d.ts +2 -0
- package/dist/node.js +270 -36
- package/dist/node.mjs +269 -36
- package/dist/pipeline/detail-crop.d.ts +122 -0
- package/dist/{sleep-CXimb854.mjs → sleep-BmNKsY7v.mjs} +5 -0
- package/dist/{sleep-DTce7-ch.js → sleep-Cvi1JxZp.js} +5 -0
- package/package.json +1 -1
package/dist/node.mjs
CHANGED
|
@@ -1,12 +1,13 @@
|
|
|
1
|
+
import { t as canonicalHash } from "./canonical-hash-7nfBbEqR.mjs";
|
|
1
2
|
import { t as errMsg } from "./err-msg-IQTHeDzc.mjs";
|
|
3
|
+
import { createHash } from "node:crypto";
|
|
2
4
|
import * as fs from "node:fs";
|
|
3
5
|
import { chmodSync, createWriteStream, existsSync, mkdirSync, readFileSync, readdirSync, renameSync, rmSync, unlinkSync, writeFileSync } from "node:fs";
|
|
4
6
|
import * as path from "node:path";
|
|
5
7
|
import { basename, join } from "node:path";
|
|
6
8
|
import { pipeline } from "node:stream/promises";
|
|
7
9
|
import { Readable } from "node:stream";
|
|
8
|
-
import { execFileSync } from "node:child_process";
|
|
9
|
-
import { createHash } from "node:crypto";
|
|
10
|
+
import { execFileSync, spawn } from "node:child_process";
|
|
10
11
|
//#region src/deps/binary-downloader.ts
|
|
11
12
|
/**
|
|
12
13
|
* Recursively find the first file named exactly `name` under `dir`. Used as the
|
|
@@ -577,39 +578,6 @@ var FilesystemStorageProvider = class {
|
|
|
577
578
|
}
|
|
578
579
|
};
|
|
579
580
|
//#endregion
|
|
580
|
-
//#region src/utils/canonical-hash.ts
|
|
581
|
-
/**
|
|
582
|
-
* Deterministic SHA-256 hash of an arbitrary serialisable value. The
|
|
583
|
-
* canonical form sorts object keys alphabetically at every depth so two
|
|
584
|
-
* structurally-equal inputs with different key insertion orders produce
|
|
585
|
-
* the same hash. Returns a 64-char lowercase hex digest.
|
|
586
|
-
*
|
|
587
|
-
* Used by export adapters (Alexa, HAP) to short-circuit re-discovery /
|
|
588
|
-
* accessory-rebuild work when the upstream shape is byte-identical to
|
|
589
|
-
* the last applied state — preventing user-visible "re-discovery"
|
|
590
|
-
* notifications on every addon-runner respawn. Each respawn re-fires
|
|
591
|
-
* `DeviceBindingsChanged` for every cap registration, which without
|
|
592
|
-
* this guard would propagate redundant pushes.
|
|
593
|
-
*
|
|
594
|
-
* Note: this is a SYMPTOMATIC fix layered on top of the binding-change
|
|
595
|
-
* subscription. The proper fix is a single "device ready" lifecycle
|
|
596
|
-
* barrier so exports react only when the full cap set has landed —
|
|
597
|
-
* tracked separately for post-HA-integration work.
|
|
598
|
-
*/
|
|
599
|
-
function canonicalHash(value) {
|
|
600
|
-
const canonical = JSON.stringify(value, replaceWithSortedKeys);
|
|
601
|
-
return createHash("sha256").update(canonical ?? "").digest("hex");
|
|
602
|
-
}
|
|
603
|
-
function replaceWithSortedKeys(_key, value) {
|
|
604
|
-
if (value && typeof value === "object" && !Array.isArray(value)) {
|
|
605
|
-
const obj = value;
|
|
606
|
-
const out = {};
|
|
607
|
-
for (const k of Object.keys(obj).toSorted()) out[k] = obj[k];
|
|
608
|
-
return out;
|
|
609
|
-
}
|
|
610
|
-
return value;
|
|
611
|
-
}
|
|
612
|
-
//#endregion
|
|
613
581
|
//#region src/utils/export-reconciler.ts
|
|
614
582
|
/**
|
|
615
583
|
* Compute the stable 64-char lowercase-hex fingerprint of a device's
|
|
@@ -666,4 +634,269 @@ function resolveExportFingerprint(input) {
|
|
|
666
634
|
return input.persisted ?? input.fresh;
|
|
667
635
|
}
|
|
668
636
|
//#endregion
|
|
669
|
-
|
|
637
|
+
//#region src/ffmpeg/process.ts
|
|
638
|
+
/**
|
|
639
|
+
* `FfmpegProcess` — the ONE spawn/lifecycle wrapper for a live-media ffmpeg.
|
|
640
|
+
*
|
|
641
|
+
* Extracted from `TranscodeEgress.spawnAttempt`, which was already the most
|
|
642
|
+
* complete of the repo's hand-rolled lifecycles: first-data deadline, hardware
|
|
643
|
+
* →software retry, SIGTERM-then-SIGKILL. This generalises it and adds the two
|
|
644
|
+
* things every copy was missing — a `tags: { deviceId }` on every line, and a
|
|
645
|
+
* bounded restart — so a consumer gets them by construction instead of by
|
|
646
|
+
* remembering.
|
|
647
|
+
*
|
|
648
|
+
* ## What it owns
|
|
649
|
+
*
|
|
650
|
+
* - spawn, with the argv from the ONE builder (`./invocation.js`);
|
|
651
|
+
* - a FIRST-DATA deadline: an ffmpeg that starts but never emits is dead, and
|
|
652
|
+
* nothing downstream can tell that apart from a slow camera;
|
|
653
|
+
* - HARDWARE→SOFTWARE retry, announced at `warn`. A silent downgrade on this
|
|
654
|
+
* hub is a flow bug, not a capability limit — see `docs/design/decode-path.md`;
|
|
655
|
+
* - exit classification (`ok` / `signalled-by-us` / `crashed` / `no-output`);
|
|
656
|
+
* - bounded restart with backoff, and a terminal give-up (never an infinite
|
|
657
|
+
* loop — the same rule `CrashSupervisor` enforces for runners, D6);
|
|
658
|
+
* - SIGTERM then SIGKILL after a grace, gated on the child not having already
|
|
659
|
+
* exited.
|
|
660
|
+
*
|
|
661
|
+
* ## What it does NOT own
|
|
662
|
+
*
|
|
663
|
+
* The output PLUMBING. A consumer attaches to `stdout` / `stderr` itself,
|
|
664
|
+
* because what the bytes mean is the consumer's business: the broker deframes
|
|
665
|
+
* Annex-B into a restreamer, the WebRTC leg regroups access units, HomeKit
|
|
666
|
+
* writes nothing to stdout at all (its output is two RTP sockets). A wrapper
|
|
667
|
+
* that also owned the bytes would need a mode per consumer, which is the same
|
|
668
|
+
* mistake as one builder per consumer.
|
|
669
|
+
*/
|
|
670
|
+
var DEFAULT_FIRST_DATA_TIMEOUT_MS = 8e3;
|
|
671
|
+
var DEFAULT_RESTART_DELAY_MS = 1e3;
|
|
672
|
+
var DEFAULT_STABLE_RUN_MS = 3e4;
|
|
673
|
+
var DEFAULT_KILL_GRACE_MS = 500;
|
|
674
|
+
var STDERR_TAIL_LINES = 12;
|
|
675
|
+
var FfmpegProcess = class {
|
|
676
|
+
opts;
|
|
677
|
+
child = null;
|
|
678
|
+
stopped = false;
|
|
679
|
+
producedOutput = false;
|
|
680
|
+
consecutiveFailures = 0;
|
|
681
|
+
startedAtMs = 0;
|
|
682
|
+
stderrTail = [];
|
|
683
|
+
activeHwAccel;
|
|
684
|
+
triedSoftwareFallback = false;
|
|
685
|
+
firstDataTimer = null;
|
|
686
|
+
constructor(opts) {
|
|
687
|
+
this.opts = opts;
|
|
688
|
+
this.activeHwAccel = opts.decodeHwAccel;
|
|
689
|
+
}
|
|
690
|
+
/** Queryable tags on every line — `deviceId` is never optional. */
|
|
691
|
+
get logTags() {
|
|
692
|
+
return {
|
|
693
|
+
deviceId: this.opts.deviceId,
|
|
694
|
+
...this.opts.tags
|
|
695
|
+
};
|
|
696
|
+
}
|
|
697
|
+
get now() {
|
|
698
|
+
return (this.opts.now ?? Date.now)();
|
|
699
|
+
}
|
|
700
|
+
/** `true` while a child is running. */
|
|
701
|
+
isRunning() {
|
|
702
|
+
return this.child !== null && !this.stopped;
|
|
703
|
+
}
|
|
704
|
+
/** The backend the CURRENT child decodes with (`null` ⇒ software). */
|
|
705
|
+
activeDecodeHwAccel() {
|
|
706
|
+
return this.activeHwAccel;
|
|
707
|
+
}
|
|
708
|
+
/**
|
|
709
|
+
* Spawn the first child. Resolves as soon as it produces output; rejects if
|
|
710
|
+
* it dies or stays silent past the deadline AFTER the software retry has
|
|
711
|
+
* also been exhausted. A caller that wants fire-and-forget can ignore the
|
|
712
|
+
* promise — the restart loop runs regardless.
|
|
713
|
+
*/
|
|
714
|
+
start() {
|
|
715
|
+
return new Promise((resolve, reject) => {
|
|
716
|
+
this.spawnAttempt(resolve, reject);
|
|
717
|
+
});
|
|
718
|
+
}
|
|
719
|
+
spawnAttempt(onLive, onDead) {
|
|
720
|
+
if (this.stopped) return;
|
|
721
|
+
const args = [...this.opts.buildArgs(this.activeHwAccel)];
|
|
722
|
+
const spawnFn = this.opts.spawnFn ?? spawn;
|
|
723
|
+
const setTimeoutImpl = this.opts.setTimeoutFn ?? setTimeout;
|
|
724
|
+
this.opts.logger.info(`ffmpeg ${this.opts.role}: spawning`, {
|
|
725
|
+
tags: this.logTags,
|
|
726
|
+
meta: {
|
|
727
|
+
decodeHwAccel: this.activeHwAccel ?? "software",
|
|
728
|
+
attempt: this.consecutiveFailures + 1
|
|
729
|
+
}
|
|
730
|
+
});
|
|
731
|
+
let child;
|
|
732
|
+
try {
|
|
733
|
+
child = spawnFn(this.opts.binaryPath, args, { stdio: this.opts.stdio ?? [
|
|
734
|
+
"ignore",
|
|
735
|
+
"pipe",
|
|
736
|
+
"pipe"
|
|
737
|
+
] });
|
|
738
|
+
} catch (err) {
|
|
739
|
+
this.handleFailure("crashed", null, null, err, onLive, onDead);
|
|
740
|
+
return;
|
|
741
|
+
}
|
|
742
|
+
this.child = child;
|
|
743
|
+
this.startedAtMs = this.now;
|
|
744
|
+
this.producedOutput = false;
|
|
745
|
+
this.stderrTail = [];
|
|
746
|
+
let settled = false;
|
|
747
|
+
const timeoutMs = this.opts.firstDataTimeoutMs ?? DEFAULT_FIRST_DATA_TIMEOUT_MS;
|
|
748
|
+
if (timeoutMs > 0) this.firstDataTimer = setTimeoutImpl(() => {
|
|
749
|
+
if (settled || this.producedOutput || this.stopped) return;
|
|
750
|
+
settled = true;
|
|
751
|
+
this.opts.logger.warn(`ffmpeg ${this.opts.role}: no output within ${timeoutMs}ms`, {
|
|
752
|
+
tags: this.logTags,
|
|
753
|
+
meta: { decodeHwAccel: this.activeHwAccel ?? "software" }
|
|
754
|
+
});
|
|
755
|
+
this.killChild();
|
|
756
|
+
this.handleFailure("no-output", null, null, null, onLive, onDead);
|
|
757
|
+
}, timeoutMs);
|
|
758
|
+
child.stderr?.setEncoding("utf8");
|
|
759
|
+
child.stderr?.on("data", (line) => {
|
|
760
|
+
const text = String(line).trim();
|
|
761
|
+
if (text.length === 0) return;
|
|
762
|
+
this.stderrTail.push(text);
|
|
763
|
+
if (this.stderrTail.length > STDERR_TAIL_LINES) this.stderrTail.shift();
|
|
764
|
+
this.opts.logger.debug(`ffmpeg ${this.opts.role}`, {
|
|
765
|
+
tags: this.logTags,
|
|
766
|
+
meta: { line: text }
|
|
767
|
+
});
|
|
768
|
+
});
|
|
769
|
+
child.once("error", (err) => {
|
|
770
|
+
if (settled || this.stopped) return;
|
|
771
|
+
settled = true;
|
|
772
|
+
this.handleFailure("crashed", null, null, err, onLive, onDead);
|
|
773
|
+
});
|
|
774
|
+
child.once("exit", (code, signal) => {
|
|
775
|
+
this.clearFirstDataTimer();
|
|
776
|
+
if (this.child === child) this.child = null;
|
|
777
|
+
if (this.stopped) {
|
|
778
|
+
this.report("stopped", code, signal);
|
|
779
|
+
return;
|
|
780
|
+
}
|
|
781
|
+
if (settled && this.producedOutput) {
|
|
782
|
+
this.report("crashed", code, signal);
|
|
783
|
+
this.scheduleRestart(onLive, onDead);
|
|
784
|
+
return;
|
|
785
|
+
}
|
|
786
|
+
if (settled) return;
|
|
787
|
+
settled = true;
|
|
788
|
+
this.handleFailure(this.producedOutput ? "crashed" : "no-output", code, signal, null, onLive, onDead);
|
|
789
|
+
});
|
|
790
|
+
this.opts.onChild(child);
|
|
791
|
+
child.stdout?.once("data", () => {
|
|
792
|
+
this.producedOutput = true;
|
|
793
|
+
this.clearFirstDataTimer();
|
|
794
|
+
if (settled) return;
|
|
795
|
+
settled = true;
|
|
796
|
+
this.opts.logger.info(`ffmpeg ${this.opts.role}: live`, {
|
|
797
|
+
tags: this.logTags,
|
|
798
|
+
meta: { decodeHwAccel: this.activeHwAccel ?? "software" }
|
|
799
|
+
});
|
|
800
|
+
onLive();
|
|
801
|
+
});
|
|
802
|
+
}
|
|
803
|
+
/**
|
|
804
|
+
* A child failed before going live. Try SOFTWARE once if it was decoding in
|
|
805
|
+
* hardware — loudly — then fall through to the bounded restart.
|
|
806
|
+
*/
|
|
807
|
+
handleFailure(classification, code, signal, err, onLive, onDead) {
|
|
808
|
+
this.report(classification, code, signal, err);
|
|
809
|
+
if (this.activeHwAccel !== null && !this.triedSoftwareFallback && !this.stopped) {
|
|
810
|
+
this.triedSoftwareFallback = true;
|
|
811
|
+
this.opts.logger.warn(`ffmpeg ${this.opts.role}: hardware decode produced no output — retrying in SOFTWARE`, {
|
|
812
|
+
tags: this.logTags,
|
|
813
|
+
meta: {
|
|
814
|
+
decodeHwAccel: this.activeHwAccel,
|
|
815
|
+
classification,
|
|
816
|
+
code,
|
|
817
|
+
signal,
|
|
818
|
+
stderrTail: this.stderrTail
|
|
819
|
+
}
|
|
820
|
+
});
|
|
821
|
+
this.activeHwAccel = null;
|
|
822
|
+
this.spawnAttempt(onLive, onDead);
|
|
823
|
+
return;
|
|
824
|
+
}
|
|
825
|
+
this.consecutiveFailures += 1;
|
|
826
|
+
const maxRestarts = this.opts.maxRestarts ?? 0;
|
|
827
|
+
if (this.consecutiveFailures > maxRestarts || this.stopped) {
|
|
828
|
+
const reason = `ffmpeg ${this.opts.role} gave up after ${this.consecutiveFailures} attempt(s) (${classification}, code=${code} signal=${signal})`;
|
|
829
|
+
this.opts.logger.error(reason, {
|
|
830
|
+
tags: this.logTags,
|
|
831
|
+
meta: { stderrTail: this.stderrTail }
|
|
832
|
+
});
|
|
833
|
+
onDead(new Error(reason));
|
|
834
|
+
return;
|
|
835
|
+
}
|
|
836
|
+
this.scheduleRestart(onLive, onDead);
|
|
837
|
+
}
|
|
838
|
+
scheduleRestart(onLive, onDead) {
|
|
839
|
+
if (this.stopped) return;
|
|
840
|
+
const maxRestarts = this.opts.maxRestarts ?? 0;
|
|
841
|
+
if (maxRestarts === 0) return;
|
|
842
|
+
if (this.producedOutput && this.now - this.startedAtMs >= (this.opts.stableRunMs ?? DEFAULT_STABLE_RUN_MS)) this.consecutiveFailures = 0;
|
|
843
|
+
if (this.consecutiveFailures > maxRestarts) return;
|
|
844
|
+
(this.opts.setTimeoutFn ?? setTimeout)(() => {
|
|
845
|
+
if (this.stopped) return;
|
|
846
|
+
this.spawnAttempt(onLive, onDead);
|
|
847
|
+
}, (this.opts.restartDelayMs ?? DEFAULT_RESTART_DELAY_MS) * Math.min(this.consecutiveFailures + 1, 8));
|
|
848
|
+
}
|
|
849
|
+
report(classification, code, signal, err) {
|
|
850
|
+
const exit = {
|
|
851
|
+
classification,
|
|
852
|
+
code,
|
|
853
|
+
signal,
|
|
854
|
+
stderrTail: [...this.stderrTail],
|
|
855
|
+
decodeHwAccel: this.activeHwAccel
|
|
856
|
+
};
|
|
857
|
+
if (classification === "crashed" || classification === "no-output") this.opts.logger.warn(`ffmpeg ${this.opts.role} exited: ${classification}`, {
|
|
858
|
+
tags: this.logTags,
|
|
859
|
+
meta: {
|
|
860
|
+
code,
|
|
861
|
+
signal,
|
|
862
|
+
decodeHwAccel: this.activeHwAccel ?? "software",
|
|
863
|
+
error: err instanceof Error ? err.message : err === void 0 ? void 0 : String(err),
|
|
864
|
+
stderrTail: exit.stderrTail
|
|
865
|
+
}
|
|
866
|
+
});
|
|
867
|
+
this.opts.onExit?.(exit);
|
|
868
|
+
}
|
|
869
|
+
clearFirstDataTimer() {
|
|
870
|
+
if (this.firstDataTimer !== null) {
|
|
871
|
+
clearTimeout(this.firstDataTimer);
|
|
872
|
+
this.firstDataTimer = null;
|
|
873
|
+
}
|
|
874
|
+
}
|
|
875
|
+
/** Terminate for good. Idempotent; no restart follows. */
|
|
876
|
+
stop() {
|
|
877
|
+
if (this.stopped) return;
|
|
878
|
+
this.stopped = true;
|
|
879
|
+
this.clearFirstDataTimer();
|
|
880
|
+
this.killChild();
|
|
881
|
+
}
|
|
882
|
+
killChild() {
|
|
883
|
+
const child = this.child;
|
|
884
|
+
this.child = null;
|
|
885
|
+
if (!child) return;
|
|
886
|
+
if (child.exitCode !== null || child.signalCode !== null) return;
|
|
887
|
+
try {
|
|
888
|
+
child.kill("SIGTERM");
|
|
889
|
+
} catch {
|
|
890
|
+
return;
|
|
891
|
+
}
|
|
892
|
+
const grace = (this.opts.setTimeoutFn ?? setTimeout)(() => {
|
|
893
|
+
if (child.exitCode !== null || child.signalCode !== null) return;
|
|
894
|
+
try {
|
|
895
|
+
child.kill("SIGKILL");
|
|
896
|
+
} catch {}
|
|
897
|
+
}, this.opts.killGraceMs ?? DEFAULT_KILL_GRACE_MS);
|
|
898
|
+
if (typeof grace === "object" && grace !== null && "unref" in grace) grace.unref();
|
|
899
|
+
}
|
|
900
|
+
};
|
|
901
|
+
//#endregion
|
|
902
|
+
export { FfmpegProcess, FilesystemStorageProvider, PYTHON_VERSION, buildBinaryPath, canonicalDeviceFingerprint, canonicalHash, diffExportTargets, downloadBinary, ensureBinary, ensureFfmpeg, ensurePython, findInPath, getFfmpegDownloadUrl, getPlatformInfo, getPythonDownloadUrl, installPythonPackages, installPythonRequirements, resolveExportFingerprint };
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* THE detail-crop convention — the single derivation of the rectangle a
|
|
3
|
+
* detail/enrichment step (clip-embedding, face-detection, plate-detection…)
|
|
4
|
+
* is fed.
|
|
5
|
+
*
|
|
6
|
+
* ## Why this is one module and not two constants
|
|
7
|
+
*
|
|
8
|
+
* `object-clip` is ONE vector index, and cosine similarity is only meaningful
|
|
9
|
+
* between vectors produced from the same crop convention. Two encode paths
|
|
10
|
+
* write into it — the live detail plane and the embedding rebuild — and they
|
|
11
|
+
* used to derive their crops independently: `DETAIL_CROP_PADDING_RATIO = 0.15`
|
|
12
|
+
* with no squaring on one side, `DEFAULT_CROP_MARGIN = 0.2` with squaring on
|
|
13
|
+
* by default on the other. Every rebuild therefore poured a second, silently
|
|
14
|
+
* incomparable feature space into the index it exists to keep consistent.
|
|
15
|
+
*
|
|
16
|
+
* So the rectangle is derived HERE, once, from ONE convention value. Both
|
|
17
|
+
* paths now reach this function through `pipelineRunner.runDetailSubtree` —
|
|
18
|
+
* the runner is the only process that cuts (see `detail-subtree.ts`), and the
|
|
19
|
+
* convention is a cluster-global `pipeline-orchestrator` setting. There is
|
|
20
|
+
* deliberately no per-node or per-device scope: a per-accelerator crop margin
|
|
21
|
+
* would reintroduce the same split, merely relocated.
|
|
22
|
+
*
|
|
23
|
+
* ## The default IS the live convention
|
|
24
|
+
*
|
|
25
|
+
* {@link DEFAULT_DETAIL_CROP_CONVENTION} reproduces what the live path has
|
|
26
|
+
* been storing (0.15, no squaring). Anything else would invalidate every
|
|
27
|
+
* vector already in the index on the day it shipped. Changing the convention
|
|
28
|
+
* is legitimate — that is what the operator knob is for — but it must be
|
|
29
|
+
* followed by a rebuild, which is now guaranteed to produce crops from this
|
|
30
|
+
* same function.
|
|
31
|
+
*/
|
|
32
|
+
import { z } from 'zod';
|
|
33
|
+
/**
|
|
34
|
+
* Store identity of the convention in `pipeline-orchestrator`'s GLOBAL
|
|
35
|
+
* (cluster-wide) settings.
|
|
36
|
+
*
|
|
37
|
+
* These live here rather than in the orchestrator because the reader is a
|
|
38
|
+
* different addon — the pipeline runner, over the hub-routed `addon-settings`
|
|
39
|
+
* cap. Addons never import each other, so a key owned by the writer would have
|
|
40
|
+
* to be hand-copied by the reader, and a hand-copied key is how a setting
|
|
41
|
+
* silently stops arriving while both sides still look correct.
|
|
42
|
+
*/
|
|
43
|
+
export declare const DETAIL_CROP_SECTION_ID = "detail-crop";
|
|
44
|
+
export declare const DETAIL_CROP_PADDING_KEY = "detailCropPaddingRatio";
|
|
45
|
+
export declare const DETAIL_CROP_SQUARE_KEY = "detailCropSquare";
|
|
46
|
+
/** A `{x,y,w,h}` rectangle in FRAME-space pixels. */
|
|
47
|
+
export interface DetailCropRect {
|
|
48
|
+
readonly x: number;
|
|
49
|
+
readonly y: number;
|
|
50
|
+
readonly w: number;
|
|
51
|
+
readonly h: number;
|
|
52
|
+
}
|
|
53
|
+
/**
|
|
54
|
+
* Operator-tunable crop convention. Single-valued and cluster-wide — see the
|
|
55
|
+
* module docblock for why it cannot be scoped per node or per device.
|
|
56
|
+
*/
|
|
57
|
+
export declare const DetailCropConventionSchema: z.ZodObject<{
|
|
58
|
+
paddingRatio: z.ZodNumber;
|
|
59
|
+
square: z.ZodBoolean;
|
|
60
|
+
}, z.core.$strip>;
|
|
61
|
+
export type DetailCropConvention = z.infer<typeof DetailCropConventionSchema>;
|
|
62
|
+
/**
|
|
63
|
+
* The convention in force when nobody has configured one — byte-for-byte the
|
|
64
|
+
* behaviour of the pre-unification LIVE path (`DETAIL_CROP_PADDING_RATIO`).
|
|
65
|
+
*/
|
|
66
|
+
export declare const DEFAULT_DETAIL_CROP_CONVENTION: DetailCropConvention;
|
|
67
|
+
/**
|
|
68
|
+
* Narrow a FLAT settings record to the convention.
|
|
69
|
+
*
|
|
70
|
+
* Per-FIELD fallback, deliberately: a junk padding must not also discard a
|
|
71
|
+
* valid squaring choice. An absent or invalid value resolves to
|
|
72
|
+
* {@link DEFAULT_DETAIL_CROP_CONVENTION} — the historical live behaviour —
|
|
73
|
+
* rather than to a clamped number nobody chose, so a bad read can never
|
|
74
|
+
* quietly change what the stored vectors mean.
|
|
75
|
+
*/
|
|
76
|
+
export declare function readDetailCropConvention(config: Readonly<Record<string, unknown>>): DetailCropConvention;
|
|
77
|
+
/** One section of an `addon-settings.getGlobalSettings` payload. */
|
|
78
|
+
export interface HydratedSettingsSection {
|
|
79
|
+
readonly fields: readonly unknown[];
|
|
80
|
+
}
|
|
81
|
+
/**
|
|
82
|
+
* Minimal structural view of `ConfigUISchemaWithValues` — only what the
|
|
83
|
+
* convention extraction walks. Structural on purpose: the reader is an addon
|
|
84
|
+
* that must not depend on the writer's schema type.
|
|
85
|
+
*/
|
|
86
|
+
export interface HydratedSettingsView {
|
|
87
|
+
readonly sections: readonly HydratedSettingsSection[];
|
|
88
|
+
}
|
|
89
|
+
/**
|
|
90
|
+
* Extract the convention from an `addon-settings.getGlobalSettings` payload.
|
|
91
|
+
*
|
|
92
|
+
* Walks EVERY section rather than looking inside {@link DETAIL_CROP_SECTION_ID}
|
|
93
|
+
* alone: the keys are unique across the addon's schema, and a section rename
|
|
94
|
+
* must not silently revert the whole cluster to the default. A `null` payload
|
|
95
|
+
* (addon mid-boot) is the default convention.
|
|
96
|
+
*/
|
|
97
|
+
export declare function pickDetailCropConvention(view: HydratedSettingsView | null): DetailCropConvention;
|
|
98
|
+
/** Slider bounds for the operator-facing padding knob (orchestrator settings UI). */
|
|
99
|
+
export declare const DETAIL_CROP_PADDING_FIELD: {
|
|
100
|
+
readonly min: 0;
|
|
101
|
+
readonly max: 1;
|
|
102
|
+
readonly step: 0.05;
|
|
103
|
+
readonly default: number;
|
|
104
|
+
};
|
|
105
|
+
/**
|
|
106
|
+
* Derive the crop rectangle for one parent detection.
|
|
107
|
+
*
|
|
108
|
+
* Order: pad by `paddingRatio` of the box's own size → optionally square in
|
|
109
|
+
* pixel space around the padded centre → keep it inside the frame. Pure:
|
|
110
|
+
* always returns a new rect and never mutates `bbox`.
|
|
111
|
+
*
|
|
112
|
+
* Edge handling differs by mode, on purpose:
|
|
113
|
+
*
|
|
114
|
+
* - **unsquared** — TRUNCATED at the frame border, byte-for-byte what the live
|
|
115
|
+
* path has always done (`padAndClampFrameBbox`). A subject against the edge
|
|
116
|
+
* gets a slightly smaller window. Changing this would silently reinterpret
|
|
117
|
+
* every edge-touching vector already in the index.
|
|
118
|
+
* - **squared** — SLID inward instead, because a truncated square is not
|
|
119
|
+
* square and squaring exists precisely to preserve the aspect the model
|
|
120
|
+
* sees. It only shrinks when the square is larger than the frame itself.
|
|
121
|
+
*/
|
|
122
|
+
export declare function deriveDetailCropRect(bbox: DetailCropRect, frameWidth: number, frameHeight: number, convention: DetailCropConvention): DetailCropRect;
|
|
@@ -3103,6 +3103,8 @@ function createDeviceProxy(api, binding, opts) {
|
|
|
3103
3103
|
updateRule: (input) => dispatch("notification-rules", "notificationRules", "updateRule", "mutation", input),
|
|
3104
3104
|
deleteRule: (input) => dispatch("notification-rules", "notificationRules", "deleteRule", "mutation", input),
|
|
3105
3105
|
setRuleEnabled: (input) => dispatch("notification-rules", "notificationRules", "setRuleEnabled", "mutation", input),
|
|
3106
|
+
listDeviceMutes: (input) => dispatch("notification-rules", "notificationRules", "listDeviceMutes", "query", input),
|
|
3107
|
+
setDeviceMuted: (input) => dispatch("notification-rules", "notificationRules", "setDeviceMuted", "mutation", input),
|
|
3106
3108
|
testRule: (input) => dispatch("notification-rules", "notificationRules", "testRule", "mutation", input),
|
|
3107
3109
|
getConditionCatalog: (input) => dispatch("notification-rules", "notificationRules", "getConditionCatalog", "query", input),
|
|
3108
3110
|
getHistory: (input) => dispatch("notification-rules", "notificationRules", "getHistory", "query", input),
|
|
@@ -3153,6 +3155,7 @@ function createDeviceProxy(api, binding, opts) {
|
|
|
3153
3155
|
pruneTracksBefore: (input) => dispatch("pipeline-analytics", "pipelineAnalytics", "pruneTracksBefore", "mutation", input),
|
|
3154
3156
|
wipeAllAnalytics: (input) => dispatch("pipeline-analytics", "pipelineAnalytics", "wipeAllAnalytics", "mutation", input),
|
|
3155
3157
|
deleteTracks: (input) => dispatch("pipeline-analytics", "pipelineAnalytics", "deleteTracks", "mutation", input),
|
|
3158
|
+
setTrackFlags: (input) => dispatch("pipeline-analytics", "pipelineAnalytics", "setTrackFlags", "mutation", input),
|
|
3156
3159
|
getEventStoreFootprint: (input) => dispatch("pipeline-analytics", "pipelineAnalytics", "getEventStoreFootprint", "query", input),
|
|
3157
3160
|
pruneEvents: (input) => dispatch("pipeline-analytics", "pipelineAnalytics", "pruneEvents", "mutation", input),
|
|
3158
3161
|
deleteDeviceEvents: (input) => dispatch("pipeline-analytics", "pipelineAnalytics", "deleteDeviceEvents", "mutation", input),
|
|
@@ -3388,6 +3391,8 @@ function createDeviceProxy(api, binding, opts) {
|
|
|
3388
3391
|
setCameraStepOverride: (input) => dispatchSystem("pipelineOrchestrator", "setCameraStepOverride", "mutation", input),
|
|
3389
3392
|
setCameraPipelineForAgent: (input) => dispatchSystem("pipelineOrchestrator", "setCameraPipelineForAgent", "mutation", input),
|
|
3390
3393
|
resolvePipeline: (input) => dispatchSystem("pipelineOrchestrator", "resolvePipeline", "query", input),
|
|
3394
|
+
getCameraSwitches: (input) => dispatchSystem("pipelineOrchestrator", "getCameraSwitches", "query", input),
|
|
3395
|
+
setCameraSwitch: (input) => dispatchSystem("pipelineOrchestrator", "setCameraSwitch", "mutation", input),
|
|
3391
3396
|
getCameraStatus: (input) => dispatchSystem("pipelineOrchestrator", "getCameraStatus", "query", input),
|
|
3392
3397
|
getDeviceSettingsContribution: (input) => dispatchSystem("pipelineOrchestrator", "getDeviceSettingsContribution", "query", input),
|
|
3393
3398
|
getDeviceLiveContribution: (input) => dispatchSystem("pipelineOrchestrator", "getDeviceLiveContribution", "query", input),
|
|
@@ -3103,6 +3103,8 @@ function createDeviceProxy(api, binding, opts) {
|
|
|
3103
3103
|
updateRule: (input) => dispatch("notification-rules", "notificationRules", "updateRule", "mutation", input),
|
|
3104
3104
|
deleteRule: (input) => dispatch("notification-rules", "notificationRules", "deleteRule", "mutation", input),
|
|
3105
3105
|
setRuleEnabled: (input) => dispatch("notification-rules", "notificationRules", "setRuleEnabled", "mutation", input),
|
|
3106
|
+
listDeviceMutes: (input) => dispatch("notification-rules", "notificationRules", "listDeviceMutes", "query", input),
|
|
3107
|
+
setDeviceMuted: (input) => dispatch("notification-rules", "notificationRules", "setDeviceMuted", "mutation", input),
|
|
3106
3108
|
testRule: (input) => dispatch("notification-rules", "notificationRules", "testRule", "mutation", input),
|
|
3107
3109
|
getConditionCatalog: (input) => dispatch("notification-rules", "notificationRules", "getConditionCatalog", "query", input),
|
|
3108
3110
|
getHistory: (input) => dispatch("notification-rules", "notificationRules", "getHistory", "query", input),
|
|
@@ -3153,6 +3155,7 @@ function createDeviceProxy(api, binding, opts) {
|
|
|
3153
3155
|
pruneTracksBefore: (input) => dispatch("pipeline-analytics", "pipelineAnalytics", "pruneTracksBefore", "mutation", input),
|
|
3154
3156
|
wipeAllAnalytics: (input) => dispatch("pipeline-analytics", "pipelineAnalytics", "wipeAllAnalytics", "mutation", input),
|
|
3155
3157
|
deleteTracks: (input) => dispatch("pipeline-analytics", "pipelineAnalytics", "deleteTracks", "mutation", input),
|
|
3158
|
+
setTrackFlags: (input) => dispatch("pipeline-analytics", "pipelineAnalytics", "setTrackFlags", "mutation", input),
|
|
3156
3159
|
getEventStoreFootprint: (input) => dispatch("pipeline-analytics", "pipelineAnalytics", "getEventStoreFootprint", "query", input),
|
|
3157
3160
|
pruneEvents: (input) => dispatch("pipeline-analytics", "pipelineAnalytics", "pruneEvents", "mutation", input),
|
|
3158
3161
|
deleteDeviceEvents: (input) => dispatch("pipeline-analytics", "pipelineAnalytics", "deleteDeviceEvents", "mutation", input),
|
|
@@ -3388,6 +3391,8 @@ function createDeviceProxy(api, binding, opts) {
|
|
|
3388
3391
|
setCameraStepOverride: (input) => dispatchSystem("pipelineOrchestrator", "setCameraStepOverride", "mutation", input),
|
|
3389
3392
|
setCameraPipelineForAgent: (input) => dispatchSystem("pipelineOrchestrator", "setCameraPipelineForAgent", "mutation", input),
|
|
3390
3393
|
resolvePipeline: (input) => dispatchSystem("pipelineOrchestrator", "resolvePipeline", "query", input),
|
|
3394
|
+
getCameraSwitches: (input) => dispatchSystem("pipelineOrchestrator", "getCameraSwitches", "query", input),
|
|
3395
|
+
setCameraSwitch: (input) => dispatchSystem("pipelineOrchestrator", "setCameraSwitch", "mutation", input),
|
|
3391
3396
|
getCameraStatus: (input) => dispatchSystem("pipelineOrchestrator", "getCameraStatus", "query", input),
|
|
3392
3397
|
getDeviceSettingsContribution: (input) => dispatchSystem("pipelineOrchestrator", "getDeviceSettingsContribution", "query", input),
|
|
3393
3398
|
getDeviceLiveContribution: (input) => dispatchSystem("pipelineOrchestrator", "getDeviceLiveContribution", "query", input),
|