@camstack/addon-pipeline 1.1.66 → 1.1.67
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/audio-analyzer/index.js +1 -1
- package/dist/audio-analyzer/index.mjs +1 -1
- package/dist/detection-pipeline/index.js +52 -13
- package/dist/detection-pipeline/index.mjs +52 -13
- package/dist/{dist-D5o9fGxq.js → dist-BxxVKcnw.js} +43 -5
- package/dist/{dist-BpWot5-w.mjs → dist-DkGHtAaK.mjs} +43 -5
- package/dist/motion-wasm/index.js +1 -1
- package/dist/motion-wasm/index.mjs +1 -1
- package/dist/pipeline-runner/index.js +2 -2
- package/dist/pipeline-runner/index.mjs +2 -2
- package/dist/recorder/index.js +1 -1
- package/dist/recorder/index.mjs +1 -1
- package/dist/session-decode/decode-worker-child.js +176 -11
- package/dist/session-decode/decode-worker-child.mjs +176 -11
- package/dist/{step-definitions-81eYyJbW.js → step-definitions-CAs0RD2N.js} +1 -1
- package/dist/{step-definitions-D6R9X0V7.mjs → step-definitions-Delhq28w.mjs} +1 -1
- package/dist/stream-broker/_stub.js +1 -1
- package/dist/stream-broker/{_virtual_mf-localSharedImportMap___mfe_internal__addon_stream_broker_widgets-CIIIVPzB.mjs → _virtual_mf-localSharedImportMap___mfe_internal__addon_stream_broker_widgets-CfFsU700.mjs} +1 -1
- package/dist/stream-broker/{_virtual_mf___mfe_internal__addon_stream_broker_widgets__loadShare___mf_0_camstack_mf_1_types__loadShare__.js-CPbLcza9.mjs → _virtual_mf___mfe_internal__addon_stream_broker_widgets__loadShare___mf_0_camstack_mf_1_types__loadShare__.js-D68Lj69v.mjs} +1 -1
- package/dist/stream-broker/{hostInit-CeBNudbY.mjs → hostInit-D-S5fCDx.mjs} +1 -1
- package/dist/stream-broker/index.js +1 -1
- package/dist/stream-broker/index.mjs +1 -1
- package/dist/stream-broker/remoteEntry.js +1 -1
- package/embed-dist/assets/{MaskShapeCanvas-DI4BY7W2-CbBbcdZb.js → MaskShapeCanvas-DI4BY7W2-CRvzj7Of.js} +1 -1
- package/embed-dist/assets/{MotionZonesSettings-NcxxQN8r-aA6zvms_.js → MotionZonesSettings-NcxxQN8r-CBgvPUB-.js} +1 -1
- package/embed-dist/assets/{PrivacyMaskSettings-APgPLF7p-CcGB7iAE.js → PrivacyMaskSettings-APgPLF7p-DH07fe1k.js} +1 -1
- package/embed-dist/assets/{index-BRG_Fqb5.js → index-BkoPcGtQ.js} +13 -13
- package/embed-dist/index.html +1 -1
- package/package.json +1 -1
- package/python/inference_pool.py +362 -45
- package/python/test_inference_pool_ov_ppp.py +117 -0
- package/python/test_inference_pool_preprocess.py +93 -1
|
@@ -6,6 +6,133 @@ let node_fs = require("node:fs");
|
|
|
6
6
|
node_fs = require_chunk.__toESM(node_fs);
|
|
7
7
|
let node_path = require("node:path");
|
|
8
8
|
node_path = require_chunk.__toESM(node_path);
|
|
9
|
+
//#region src/session-decode/color-conversion.ts
|
|
10
|
+
/**
|
|
11
|
+
* Explicit YUV→RGB colorspace/range resolution for the session-decode worker.
|
|
12
|
+
*
|
|
13
|
+
* WHY THIS EXISTS (RC-5, `docs/superpowers/specs/2026-07-19-track-media-bugs-analysis.md`):
|
|
14
|
+
* an intermittent ~one-GOP DESATURATED stretch was measured on cam 615 — the
|
|
15
|
+
* classic "limited-range YUV interpreted as full-range" signature (lifted
|
|
16
|
+
* blacks, washed color, JPEG size collapse). Root cause: NONE of the decode
|
|
17
|
+
* worker's YUV→RGB conversion sites pinned an explicit colorspace/range, so the
|
|
18
|
+
* conversion depended on whatever `colorspace`/`color_range` metadata each
|
|
19
|
+
* decoded frame happened to carry. A GOP replayed/decoded by a DIFFERENT path
|
|
20
|
+
* (hwaccel-cascade / keyframe-seed fallback, see `docs/design/decode-path.md`)
|
|
21
|
+
* can carry UNSPECIFIED (or a different) range, which the libav auto-inserted
|
|
22
|
+
* converter then guesses differently than its neighbours → one washed-out GOP,
|
|
23
|
+
* then a snap back.
|
|
24
|
+
*
|
|
25
|
+
* The fix is to resolve every frame's source color to an EXPLICIT
|
|
26
|
+
* `(matrix, range)` — using the frame's own tags when present, and a STABLE
|
|
27
|
+
* default (bt709 / limited, the typical H.264/H.265 HD signalling) when they
|
|
28
|
+
* are unspecified — and to pin the conversion to that + full-range RGB output
|
|
29
|
+
* IDENTICALLY at every site. An unspecified/fallback GOP then converts exactly
|
|
30
|
+
* like its neighbours instead of being auto-guessed, so the desaturation cannot
|
|
31
|
+
* recur regardless of which decode path produced the GOP.
|
|
32
|
+
*
|
|
33
|
+
* These functions operate on the raw FFmpeg `AVColorSpace` / `AVColorRange`
|
|
34
|
+
* enum integers (stable ABI values) so they are unit-testable without loading
|
|
35
|
+
* node-av. node-av's branded `AVColorSpace`/`AVColorRange` are `number`
|
|
36
|
+
* subtypes and pass through directly.
|
|
37
|
+
*/
|
|
38
|
+
/**
|
|
39
|
+
* FFmpeg `AVColorSpace` enum values we map to a libswscale/`scale`-filter
|
|
40
|
+
* `in_color_matrix` token. Stable ABI integers (libavutil `pixfmt.h`).
|
|
41
|
+
*/
|
|
42
|
+
var AVCOL_SPC_BT709 = 1;
|
|
43
|
+
var AVCOL_SPC_FCC = 4;
|
|
44
|
+
var AVCOL_SPC_BT470BG = 5;
|
|
45
|
+
var AVCOL_SPC_SMPTE170M = 6;
|
|
46
|
+
var AVCOL_SPC_SMPTE240M = 7;
|
|
47
|
+
var AVCOL_SPC_BT2020_NCL = 9;
|
|
48
|
+
var AVCOL_SPC_BT2020_CL = 10;
|
|
49
|
+
/** FFmpeg `AVColorRange` enum values. Stable ABI integers. */
|
|
50
|
+
var AVCOL_RANGE_MPEG = 1;
|
|
51
|
+
var AVCOL_RANGE_JPEG = 2;
|
|
52
|
+
/**
|
|
53
|
+
* Default source matrix when the frame carries no (or an unusable) colorspace
|
|
54
|
+
* tag — bt709, the near-universal signalling for HD H.264/H.265 IP cameras.
|
|
55
|
+
*/
|
|
56
|
+
var DEFAULT_COLOR_MATRIX = "bt709";
|
|
57
|
+
/**
|
|
58
|
+
* Default source range when the frame carries no (or an unspecified) range tag
|
|
59
|
+
* — limited/mpeg, the near-universal signalling for camera video. Guessing
|
|
60
|
+
* full here is the exact mistake that produced the desaturated GOP.
|
|
61
|
+
*/
|
|
62
|
+
var DEFAULT_COLOR_RANGE = "limited";
|
|
63
|
+
/**
|
|
64
|
+
* Output RGB range — always FULL (0-255). Every RGB conversion site converges
|
|
65
|
+
* on this so downstream media/inference sees a single, path-independent RGB
|
|
66
|
+
* contract.
|
|
67
|
+
*/
|
|
68
|
+
var OUTPUT_RGB_RANGE = "full";
|
|
69
|
+
/** Map an `AVColorSpace` enum integer to a `scale`-filter matrix token. */
|
|
70
|
+
function resolveColorMatrix(colorSpace) {
|
|
71
|
+
switch (colorSpace) {
|
|
72
|
+
case AVCOL_SPC_BT709: return "bt709";
|
|
73
|
+
case AVCOL_SPC_BT470BG:
|
|
74
|
+
case AVCOL_SPC_SMPTE170M: return "bt601";
|
|
75
|
+
case AVCOL_SPC_FCC: return "fcc";
|
|
76
|
+
case AVCOL_SPC_SMPTE240M: return "smpte240m";
|
|
77
|
+
case AVCOL_SPC_BT2020_NCL:
|
|
78
|
+
case AVCOL_SPC_BT2020_CL: return "bt2020nc";
|
|
79
|
+
default: return DEFAULT_COLOR_MATRIX;
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
/** Map an `AVColorRange` enum integer to a `scale`-filter range token. */
|
|
83
|
+
function resolveColorRange(colorRange) {
|
|
84
|
+
switch (colorRange) {
|
|
85
|
+
case AVCOL_RANGE_JPEG: return "full";
|
|
86
|
+
case AVCOL_RANGE_MPEG: return "limited";
|
|
87
|
+
default: return DEFAULT_COLOR_RANGE;
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
/**
|
|
91
|
+
* Resolve a frame's `(colorSpace, colorRange)` tags to the explicit
|
|
92
|
+
* `(matrix, range)` every conversion site pins — applying the stable defaults
|
|
93
|
+
* when a tag is unspecified/unusable.
|
|
94
|
+
*/
|
|
95
|
+
function resolveSourceColor(colorSpace, colorRange) {
|
|
96
|
+
return {
|
|
97
|
+
inColorMatrix: resolveColorMatrix(colorSpace),
|
|
98
|
+
inRange: resolveColorRange(colorRange),
|
|
99
|
+
colorSpaceDefaulted: !(colorSpace === AVCOL_SPC_BT709 || colorSpace === AVCOL_SPC_BT470BG || colorSpace === AVCOL_SPC_SMPTE170M || colorSpace === AVCOL_SPC_FCC || colorSpace === AVCOL_SPC_SMPTE240M || colorSpace === AVCOL_SPC_BT2020_NCL || colorSpace === AVCOL_SPC_BT2020_CL),
|
|
100
|
+
colorRangeDefaulted: !(colorRange === AVCOL_RANGE_JPEG || colorRange === AVCOL_RANGE_MPEG),
|
|
101
|
+
rawColorSpace: colorSpace,
|
|
102
|
+
rawColorRange: colorRange
|
|
103
|
+
};
|
|
104
|
+
}
|
|
105
|
+
/**
|
|
106
|
+
* The `scale`-filter option fragment that pins the explicit YUV→(full)RGB
|
|
107
|
+
* conversion — appended to a `scale=...` filter in the GPU/CPU filtergraph
|
|
108
|
+
* (`scale=W:H:<fragment>` or `scale=<fragment>` for an identity-size convert).
|
|
109
|
+
* Always converges output on full-range RGB; `out_color_matrix` is intentionally
|
|
110
|
+
* omitted (irrelevant for an RGB/gray packed output).
|
|
111
|
+
*/
|
|
112
|
+
function colorConvertFilterOptions(resolved) {
|
|
113
|
+
return `in_color_matrix=${resolved.inColorMatrix}:in_range=${resolved.inRange}:out_range=${OUTPUT_RGB_RANGE}`;
|
|
114
|
+
}
|
|
115
|
+
/**
|
|
116
|
+
* A stable cache key for the resolved source color — folded into the GPU
|
|
117
|
+
* filtergraph key so a mid-session colorspace/range change rebuilds the graph
|
|
118
|
+
* (and re-emits the provenance transition marker) instead of reusing a graph
|
|
119
|
+
* pinned to the old color.
|
|
120
|
+
*/
|
|
121
|
+
function sourceColorKey(resolved) {
|
|
122
|
+
return `${resolved.inColorMatrix}:${resolved.inRange}`;
|
|
123
|
+
}
|
|
124
|
+
/**
|
|
125
|
+
* One-line provenance marker: which decode/scale path produced a frame + the
|
|
126
|
+
* (raw and resolved) source color it was converted with. Emitted on a color
|
|
127
|
+
* TRANSITION (rare, diagnostic-gold: pinpoints an intermittent recurrence) and,
|
|
128
|
+
* behind the debug flag, per frame. Includes a trailing newline for `emitStderr`.
|
|
129
|
+
*/
|
|
130
|
+
function colorProvenanceLine(path, resolved, transition) {
|
|
131
|
+
const spaceTag = resolved.colorSpaceDefaulted ? `${resolved.rawColorSpace}->${resolved.inColorMatrix}(default)` : `${resolved.rawColorSpace}->${resolved.inColorMatrix}`;
|
|
132
|
+
const rangeTag = resolved.colorRangeDefaulted ? `${resolved.rawColorRange}->${resolved.inRange}(default)` : `${resolved.rawColorRange}->${resolved.inRange}`;
|
|
133
|
+
return `session-decode color${transition ? " TRANSITION" : ""} {path:${path}, colorSpace:${spaceTag}, colorRange:${rangeTag}, out:rgb-${OUTPUT_RGB_RANGE}}\n`;
|
|
134
|
+
}
|
|
135
|
+
//#endregion
|
|
9
136
|
//#region src/session-decode/crop-geometry.ts
|
|
10
137
|
/** Clamp to [min,max] then floor to the nearest even number (YUV420P chroma is 2x2 subsampled). */
|
|
11
138
|
function clampEven(value, min, max) {
|
|
@@ -638,9 +765,10 @@ function backendToHwDeviceConst(backend, consts) {
|
|
|
638
765
|
* It is correct and never crashes; it just forgoes the GPU win for that rare
|
|
639
766
|
* call — exactly the safety/behaviour trade-off the task calls for.
|
|
640
767
|
*/
|
|
641
|
-
function buildGpuFilterDescription(scaleFilter, region, target, fullFrame, pixel) {
|
|
642
|
-
|
|
643
|
-
return
|
|
768
|
+
function buildGpuFilterDescription(scaleFilter, region, target, fullFrame, pixel, color) {
|
|
769
|
+
const convert = `scale=${colorConvertFilterOptions(color)}`;
|
|
770
|
+
if (fullFrame) return `${scaleFilter}=w=${target.width}:h=${target.height},hwdownload,format=nv12,${convert},format=${pixel}`;
|
|
771
|
+
return `hwdownload,format=nv12,crop=${region.width}:${region.height}:${region.left}:${region.top},scale=${target.width}:${target.height}:${colorConvertFilterOptions(color)},format=${pixel}`;
|
|
644
772
|
}
|
|
645
773
|
/**
|
|
646
774
|
* Copy a single-plane packed frame (rgb24 / gray8 out of the filtergraph) into
|
|
@@ -771,6 +899,15 @@ var DecodeWorkerChild = class {
|
|
|
771
899
|
/** Throttled native-crop hit/miss counters, emitted with the throughput line. */
|
|
772
900
|
nativeCropHits = 0;
|
|
773
901
|
nativeCropMisses = 0;
|
|
902
|
+
/**
|
|
903
|
+
* Last `(path, rawColorSpace, rawColorRange)` provenance key emitted — a
|
|
904
|
+
* change re-emits a one-line TRANSITION marker (RC-5 diagnostic: pinpoints an
|
|
905
|
+
* intermittent desaturation recurrence by showing the exact GOP whose color
|
|
906
|
+
* tags drifted). See {@link logColorProvenance}.
|
|
907
|
+
*/
|
|
908
|
+
lastColorProvenanceKey = "";
|
|
909
|
+
/** Per-frame color provenance logging (opt-in; the transition marker is always on). */
|
|
910
|
+
colorDebug = process.env["CAMSTACK_DECODE_COLOR_DEBUG"] === "1";
|
|
774
911
|
/** Scrypted-style throughput counters (plain integers, no per-frame allocation). */
|
|
775
912
|
framesDecoded = 0;
|
|
776
913
|
/** Child-side skips only (HW-guard drops + probe/degrade frees); slot drops add {@link FrameSlot.droppedCount}. */
|
|
@@ -971,6 +1108,28 @@ var DecodeWorkerChild = class {
|
|
|
971
1108
|
return this.nativeRing.get(frameId) ?? this.frames.toBuffer(frameId);
|
|
972
1109
|
}
|
|
973
1110
|
/**
|
|
1111
|
+
* Resolve a decoded frame's `(colorSpace, colorRange)` tags to the EXPLICIT
|
|
1112
|
+
* `(matrix, range)` every RGB conversion site pins — the RC-5 fix. `colorSpace`
|
|
1113
|
+
* / `colorRange` are node-av branded `number` subtypes, so they pass to the
|
|
1114
|
+
* plain-number helper directly.
|
|
1115
|
+
*/
|
|
1116
|
+
resolveFrameColor(frame) {
|
|
1117
|
+
return resolveSourceColor(frame.colorSpace, frame.colorRange);
|
|
1118
|
+
}
|
|
1119
|
+
/**
|
|
1120
|
+
* Emit the color-provenance marker: which decode/scale path produced this
|
|
1121
|
+
* frame + its raw and resolved source color. Always logs on a TRANSITION (the
|
|
1122
|
+
* path or the frame's raw color tags changed vs the last emit — rare, and the
|
|
1123
|
+
* exact signal a desaturation recurrence leaves); logs every frame only under
|
|
1124
|
+
* the `CAMSTACK_DECODE_COLOR_DEBUG` flag.
|
|
1125
|
+
*/
|
|
1126
|
+
logColorProvenance(path, color) {
|
|
1127
|
+
const key = `${path}|${color.rawColorSpace}|${color.rawColorRange}`;
|
|
1128
|
+
const transition = key !== this.lastColorProvenanceKey;
|
|
1129
|
+
if (transition) this.lastColorProvenanceKey = key;
|
|
1130
|
+
if (transition || this.colorDebug) this.emitStderr(colorProvenanceLine(path, color, transition));
|
|
1131
|
+
}
|
|
1132
|
+
/**
|
|
974
1133
|
* Retain a just-superseded DELIVERED frame for later native crops. Transfers
|
|
975
1134
|
* ownership from the {@link FrameSlot} (the caller MUST NOT free when this
|
|
976
1135
|
* returns `true`).
|
|
@@ -1053,6 +1212,7 @@ var DecodeWorkerChild = class {
|
|
|
1053
1212
|
const nav = this.nav;
|
|
1054
1213
|
const C = this.consts;
|
|
1055
1214
|
if (!nav || !C) throw new Error("node-av not initialized");
|
|
1215
|
+
this.logColorProvenance("sw-crop", this.resolveFrameColor(frame));
|
|
1056
1216
|
const { region, target } = resolved;
|
|
1057
1217
|
const planes = frame.data;
|
|
1058
1218
|
const strides = frame.linesize;
|
|
@@ -1091,7 +1251,9 @@ var DecodeWorkerChild = class {
|
|
|
1091
1251
|
*/
|
|
1092
1252
|
nativeHwCropToBuffer(frame, resolved) {
|
|
1093
1253
|
const { region, target } = resolved;
|
|
1094
|
-
const
|
|
1254
|
+
const color = this.resolveFrameColor(frame);
|
|
1255
|
+
this.logColorProvenance("hw-crop", color);
|
|
1256
|
+
const outputs = this.ensureNativeCropHwFilter(region, target, color).processAllSync(frame);
|
|
1095
1257
|
const first = outputs[0];
|
|
1096
1258
|
if (!first) {
|
|
1097
1259
|
for (const out of outputs) out.free();
|
|
@@ -1125,17 +1287,17 @@ var DecodeWorkerChild = class {
|
|
|
1125
1287
|
return scaler;
|
|
1126
1288
|
}
|
|
1127
1289
|
/** Build-once / reuse the dedicated native-crop GPU filtergraph (rgb out). */
|
|
1128
|
-
ensureNativeCropHwFilter(region, target) {
|
|
1290
|
+
ensureNativeCropHwFilter(region, target, color) {
|
|
1129
1291
|
const nav = this.nav;
|
|
1130
1292
|
const scaleFilter = this.gpuScaleFilter;
|
|
1131
1293
|
if (!nav || !scaleFilter || !this.hwContext) throw new Error("decode-worker-child: native GPU crop requested without a HW context");
|
|
1132
|
-
const key = `${region.left},${region.top},${region.width},${region.height}->${target.width}x${target.height}`;
|
|
1294
|
+
const key = `${region.left},${region.top},${region.width},${region.height}->${target.width}x${target.height}:${sourceColorKey(color)}`;
|
|
1133
1295
|
const cached = this.nativeCropHwFilter;
|
|
1134
1296
|
if (cached && key === this.nativeCropHwFilterKey) return cached;
|
|
1135
1297
|
this.nativeCropHwFilter?.close();
|
|
1136
1298
|
this.nativeCropHwFilter = null;
|
|
1137
1299
|
this.nativeCropHwFilterKey = "";
|
|
1138
|
-
const description = buildGpuFilterDescription(scaleFilter, region, target, false, "rgb24");
|
|
1300
|
+
const description = buildGpuFilterDescription(scaleFilter, region, target, false, "rgb24", color);
|
|
1139
1301
|
const filter = nav.FilterAPI.create(description, { hardware: this.hwContext });
|
|
1140
1302
|
this.nativeCropHwFilter = filter;
|
|
1141
1303
|
this.nativeCropHwFilterKey = key;
|
|
@@ -1344,6 +1506,7 @@ var DecodeWorkerChild = class {
|
|
|
1344
1506
|
const format = opts?.format ?? this.sessionFormat;
|
|
1345
1507
|
const dstFmt = format === "gray" ? C.AV_PIX_FMT_GRAY8 : C.AV_PIX_FMT_RGB24;
|
|
1346
1508
|
const channels = format === "gray" ? 1 : 3;
|
|
1509
|
+
this.logColorProvenance(isFullFrameRegion(region, frame.width, frame.height) ? "sw-fullframe" : "sw-crop", this.resolveFrameColor(frame));
|
|
1347
1510
|
const planes = frame.data;
|
|
1348
1511
|
const strides = frame.linesize;
|
|
1349
1512
|
const yPlane = planes?.[0];
|
|
@@ -1415,7 +1578,9 @@ var DecodeWorkerChild = class {
|
|
|
1415
1578
|
const target = resolveResizeTarget(region.width, region.height, opts?.resize);
|
|
1416
1579
|
const format = opts?.format ?? this.sessionFormat;
|
|
1417
1580
|
const channels = format === "gray" ? 1 : 3;
|
|
1418
|
-
const
|
|
1581
|
+
const color = this.resolveFrameColor(frame);
|
|
1582
|
+
this.logColorProvenance(isFullFrameRegion(region, frame.width, frame.height) ? "hw-fullframe" : "hw-crop", color);
|
|
1583
|
+
const outputs = this.ensureHwFilter(region, target, frame.width, frame.height, format, color).processAllSync(frame);
|
|
1419
1584
|
const first = outputs[0];
|
|
1420
1585
|
if (!first) throw new Error("GPU filtergraph produced no frame");
|
|
1421
1586
|
try {
|
|
@@ -1437,19 +1602,19 @@ var DecodeWorkerChild = class {
|
|
|
1437
1602
|
* only when the key changes; detection's ROI + resize target are stable, so
|
|
1438
1603
|
* this is a per-worker singleton in the steady state.
|
|
1439
1604
|
*/
|
|
1440
|
-
ensureHwFilter(region, target, srcW, srcH, format) {
|
|
1605
|
+
ensureHwFilter(region, target, srcW, srcH, format, color) {
|
|
1441
1606
|
const nav = this.nav;
|
|
1442
1607
|
const scaleFilter = this.gpuScaleFilter;
|
|
1443
1608
|
if (!nav || !scaleFilter || !this.hwContext) throw new Error("decode-worker-child: GPU filter requested without a HW context");
|
|
1444
1609
|
const fullFrame = isFullFrameRegion(region, srcW, srcH);
|
|
1445
1610
|
const pixel = pixelFilterName(format);
|
|
1446
|
-
const key = `${region.left},${region.top},${region.width},${region.height}->${target.width}x${target.height}:${pixel}`;
|
|
1611
|
+
const key = `${region.left},${region.top},${region.width},${region.height}->${target.width}x${target.height}:${pixel}:${sourceColorKey(color)}`;
|
|
1447
1612
|
const cached = this.hwFilter;
|
|
1448
1613
|
if (cached && key === this.hwFilterKey) return cached;
|
|
1449
1614
|
this.hwFilter?.close();
|
|
1450
1615
|
this.hwFilter = null;
|
|
1451
1616
|
this.hwFilterKey = "";
|
|
1452
|
-
const description = buildGpuFilterDescription(scaleFilter, region, target, fullFrame, pixel);
|
|
1617
|
+
const description = buildGpuFilterDescription(scaleFilter, region, target, fullFrame, pixel, color);
|
|
1453
1618
|
const filter = nav.FilterAPI.create(description, { hardware: this.hwContext });
|
|
1454
1619
|
this.hwFilter = filter;
|
|
1455
1620
|
this.hwFilterKey = key;
|
|
@@ -2,6 +2,133 @@ import { n as isWorkerRequest } from "../worker-protocol-CyVJTZEO.mjs";
|
|
|
2
2
|
import { pathToFileURL } from "node:url";
|
|
3
3
|
import * as fs from "node:fs";
|
|
4
4
|
import * as path$1 from "node:path";
|
|
5
|
+
//#region src/session-decode/color-conversion.ts
|
|
6
|
+
/**
|
|
7
|
+
* Explicit YUV→RGB colorspace/range resolution for the session-decode worker.
|
|
8
|
+
*
|
|
9
|
+
* WHY THIS EXISTS (RC-5, `docs/superpowers/specs/2026-07-19-track-media-bugs-analysis.md`):
|
|
10
|
+
* an intermittent ~one-GOP DESATURATED stretch was measured on cam 615 — the
|
|
11
|
+
* classic "limited-range YUV interpreted as full-range" signature (lifted
|
|
12
|
+
* blacks, washed color, JPEG size collapse). Root cause: NONE of the decode
|
|
13
|
+
* worker's YUV→RGB conversion sites pinned an explicit colorspace/range, so the
|
|
14
|
+
* conversion depended on whatever `colorspace`/`color_range` metadata each
|
|
15
|
+
* decoded frame happened to carry. A GOP replayed/decoded by a DIFFERENT path
|
|
16
|
+
* (hwaccel-cascade / keyframe-seed fallback, see `docs/design/decode-path.md`)
|
|
17
|
+
* can carry UNSPECIFIED (or a different) range, which the libav auto-inserted
|
|
18
|
+
* converter then guesses differently than its neighbours → one washed-out GOP,
|
|
19
|
+
* then a snap back.
|
|
20
|
+
*
|
|
21
|
+
* The fix is to resolve every frame's source color to an EXPLICIT
|
|
22
|
+
* `(matrix, range)` — using the frame's own tags when present, and a STABLE
|
|
23
|
+
* default (bt709 / limited, the typical H.264/H.265 HD signalling) when they
|
|
24
|
+
* are unspecified — and to pin the conversion to that + full-range RGB output
|
|
25
|
+
* IDENTICALLY at every site. An unspecified/fallback GOP then converts exactly
|
|
26
|
+
* like its neighbours instead of being auto-guessed, so the desaturation cannot
|
|
27
|
+
* recur regardless of which decode path produced the GOP.
|
|
28
|
+
*
|
|
29
|
+
* These functions operate on the raw FFmpeg `AVColorSpace` / `AVColorRange`
|
|
30
|
+
* enum integers (stable ABI values) so they are unit-testable without loading
|
|
31
|
+
* node-av. node-av's branded `AVColorSpace`/`AVColorRange` are `number`
|
|
32
|
+
* subtypes and pass through directly.
|
|
33
|
+
*/
|
|
34
|
+
/**
|
|
35
|
+
* FFmpeg `AVColorSpace` enum values we map to a libswscale/`scale`-filter
|
|
36
|
+
* `in_color_matrix` token. Stable ABI integers (libavutil `pixfmt.h`).
|
|
37
|
+
*/
|
|
38
|
+
var AVCOL_SPC_BT709 = 1;
|
|
39
|
+
var AVCOL_SPC_FCC = 4;
|
|
40
|
+
var AVCOL_SPC_BT470BG = 5;
|
|
41
|
+
var AVCOL_SPC_SMPTE170M = 6;
|
|
42
|
+
var AVCOL_SPC_SMPTE240M = 7;
|
|
43
|
+
var AVCOL_SPC_BT2020_NCL = 9;
|
|
44
|
+
var AVCOL_SPC_BT2020_CL = 10;
|
|
45
|
+
/** FFmpeg `AVColorRange` enum values. Stable ABI integers. */
|
|
46
|
+
var AVCOL_RANGE_MPEG = 1;
|
|
47
|
+
var AVCOL_RANGE_JPEG = 2;
|
|
48
|
+
/**
|
|
49
|
+
* Default source matrix when the frame carries no (or an unusable) colorspace
|
|
50
|
+
* tag — bt709, the near-universal signalling for HD H.264/H.265 IP cameras.
|
|
51
|
+
*/
|
|
52
|
+
var DEFAULT_COLOR_MATRIX = "bt709";
|
|
53
|
+
/**
|
|
54
|
+
* Default source range when the frame carries no (or an unspecified) range tag
|
|
55
|
+
* — limited/mpeg, the near-universal signalling for camera video. Guessing
|
|
56
|
+
* full here is the exact mistake that produced the desaturated GOP.
|
|
57
|
+
*/
|
|
58
|
+
var DEFAULT_COLOR_RANGE = "limited";
|
|
59
|
+
/**
|
|
60
|
+
* Output RGB range — always FULL (0-255). Every RGB conversion site converges
|
|
61
|
+
* on this so downstream media/inference sees a single, path-independent RGB
|
|
62
|
+
* contract.
|
|
63
|
+
*/
|
|
64
|
+
var OUTPUT_RGB_RANGE = "full";
|
|
65
|
+
/** Map an `AVColorSpace` enum integer to a `scale`-filter matrix token. */
|
|
66
|
+
function resolveColorMatrix(colorSpace) {
|
|
67
|
+
switch (colorSpace) {
|
|
68
|
+
case AVCOL_SPC_BT709: return "bt709";
|
|
69
|
+
case AVCOL_SPC_BT470BG:
|
|
70
|
+
case AVCOL_SPC_SMPTE170M: return "bt601";
|
|
71
|
+
case AVCOL_SPC_FCC: return "fcc";
|
|
72
|
+
case AVCOL_SPC_SMPTE240M: return "smpte240m";
|
|
73
|
+
case AVCOL_SPC_BT2020_NCL:
|
|
74
|
+
case AVCOL_SPC_BT2020_CL: return "bt2020nc";
|
|
75
|
+
default: return DEFAULT_COLOR_MATRIX;
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
/** Map an `AVColorRange` enum integer to a `scale`-filter range token. */
|
|
79
|
+
function resolveColorRange(colorRange) {
|
|
80
|
+
switch (colorRange) {
|
|
81
|
+
case AVCOL_RANGE_JPEG: return "full";
|
|
82
|
+
case AVCOL_RANGE_MPEG: return "limited";
|
|
83
|
+
default: return DEFAULT_COLOR_RANGE;
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
/**
|
|
87
|
+
* Resolve a frame's `(colorSpace, colorRange)` tags to the explicit
|
|
88
|
+
* `(matrix, range)` every conversion site pins — applying the stable defaults
|
|
89
|
+
* when a tag is unspecified/unusable.
|
|
90
|
+
*/
|
|
91
|
+
function resolveSourceColor(colorSpace, colorRange) {
|
|
92
|
+
return {
|
|
93
|
+
inColorMatrix: resolveColorMatrix(colorSpace),
|
|
94
|
+
inRange: resolveColorRange(colorRange),
|
|
95
|
+
colorSpaceDefaulted: !(colorSpace === AVCOL_SPC_BT709 || colorSpace === AVCOL_SPC_BT470BG || colorSpace === AVCOL_SPC_SMPTE170M || colorSpace === AVCOL_SPC_FCC || colorSpace === AVCOL_SPC_SMPTE240M || colorSpace === AVCOL_SPC_BT2020_NCL || colorSpace === AVCOL_SPC_BT2020_CL),
|
|
96
|
+
colorRangeDefaulted: !(colorRange === AVCOL_RANGE_JPEG || colorRange === AVCOL_RANGE_MPEG),
|
|
97
|
+
rawColorSpace: colorSpace,
|
|
98
|
+
rawColorRange: colorRange
|
|
99
|
+
};
|
|
100
|
+
}
|
|
101
|
+
/**
|
|
102
|
+
* The `scale`-filter option fragment that pins the explicit YUV→(full)RGB
|
|
103
|
+
* conversion — appended to a `scale=...` filter in the GPU/CPU filtergraph
|
|
104
|
+
* (`scale=W:H:<fragment>` or `scale=<fragment>` for an identity-size convert).
|
|
105
|
+
* Always converges output on full-range RGB; `out_color_matrix` is intentionally
|
|
106
|
+
* omitted (irrelevant for an RGB/gray packed output).
|
|
107
|
+
*/
|
|
108
|
+
function colorConvertFilterOptions(resolved) {
|
|
109
|
+
return `in_color_matrix=${resolved.inColorMatrix}:in_range=${resolved.inRange}:out_range=${OUTPUT_RGB_RANGE}`;
|
|
110
|
+
}
|
|
111
|
+
/**
|
|
112
|
+
* A stable cache key for the resolved source color — folded into the GPU
|
|
113
|
+
* filtergraph key so a mid-session colorspace/range change rebuilds the graph
|
|
114
|
+
* (and re-emits the provenance transition marker) instead of reusing a graph
|
|
115
|
+
* pinned to the old color.
|
|
116
|
+
*/
|
|
117
|
+
function sourceColorKey(resolved) {
|
|
118
|
+
return `${resolved.inColorMatrix}:${resolved.inRange}`;
|
|
119
|
+
}
|
|
120
|
+
/**
|
|
121
|
+
* One-line provenance marker: which decode/scale path produced a frame + the
|
|
122
|
+
* (raw and resolved) source color it was converted with. Emitted on a color
|
|
123
|
+
* TRANSITION (rare, diagnostic-gold: pinpoints an intermittent recurrence) and,
|
|
124
|
+
* behind the debug flag, per frame. Includes a trailing newline for `emitStderr`.
|
|
125
|
+
*/
|
|
126
|
+
function colorProvenanceLine(path, resolved, transition) {
|
|
127
|
+
const spaceTag = resolved.colorSpaceDefaulted ? `${resolved.rawColorSpace}->${resolved.inColorMatrix}(default)` : `${resolved.rawColorSpace}->${resolved.inColorMatrix}`;
|
|
128
|
+
const rangeTag = resolved.colorRangeDefaulted ? `${resolved.rawColorRange}->${resolved.inRange}(default)` : `${resolved.rawColorRange}->${resolved.inRange}`;
|
|
129
|
+
return `session-decode color${transition ? " TRANSITION" : ""} {path:${path}, colorSpace:${spaceTag}, colorRange:${rangeTag}, out:rgb-${OUTPUT_RGB_RANGE}}\n`;
|
|
130
|
+
}
|
|
131
|
+
//#endregion
|
|
5
132
|
//#region src/session-decode/crop-geometry.ts
|
|
6
133
|
/** Clamp to [min,max] then floor to the nearest even number (YUV420P chroma is 2x2 subsampled). */
|
|
7
134
|
function clampEven(value, min, max) {
|
|
@@ -634,9 +761,10 @@ function backendToHwDeviceConst(backend, consts) {
|
|
|
634
761
|
* It is correct and never crashes; it just forgoes the GPU win for that rare
|
|
635
762
|
* call — exactly the safety/behaviour trade-off the task calls for.
|
|
636
763
|
*/
|
|
637
|
-
function buildGpuFilterDescription(scaleFilter, region, target, fullFrame, pixel) {
|
|
638
|
-
|
|
639
|
-
return
|
|
764
|
+
function buildGpuFilterDescription(scaleFilter, region, target, fullFrame, pixel, color) {
|
|
765
|
+
const convert = `scale=${colorConvertFilterOptions(color)}`;
|
|
766
|
+
if (fullFrame) return `${scaleFilter}=w=${target.width}:h=${target.height},hwdownload,format=nv12,${convert},format=${pixel}`;
|
|
767
|
+
return `hwdownload,format=nv12,crop=${region.width}:${region.height}:${region.left}:${region.top},scale=${target.width}:${target.height}:${colorConvertFilterOptions(color)},format=${pixel}`;
|
|
640
768
|
}
|
|
641
769
|
/**
|
|
642
770
|
* Copy a single-plane packed frame (rgb24 / gray8 out of the filtergraph) into
|
|
@@ -767,6 +895,15 @@ var DecodeWorkerChild = class {
|
|
|
767
895
|
/** Throttled native-crop hit/miss counters, emitted with the throughput line. */
|
|
768
896
|
nativeCropHits = 0;
|
|
769
897
|
nativeCropMisses = 0;
|
|
898
|
+
/**
|
|
899
|
+
* Last `(path, rawColorSpace, rawColorRange)` provenance key emitted — a
|
|
900
|
+
* change re-emits a one-line TRANSITION marker (RC-5 diagnostic: pinpoints an
|
|
901
|
+
* intermittent desaturation recurrence by showing the exact GOP whose color
|
|
902
|
+
* tags drifted). See {@link logColorProvenance}.
|
|
903
|
+
*/
|
|
904
|
+
lastColorProvenanceKey = "";
|
|
905
|
+
/** Per-frame color provenance logging (opt-in; the transition marker is always on). */
|
|
906
|
+
colorDebug = process.env["CAMSTACK_DECODE_COLOR_DEBUG"] === "1";
|
|
770
907
|
/** Scrypted-style throughput counters (plain integers, no per-frame allocation). */
|
|
771
908
|
framesDecoded = 0;
|
|
772
909
|
/** Child-side skips only (HW-guard drops + probe/degrade frees); slot drops add {@link FrameSlot.droppedCount}. */
|
|
@@ -967,6 +1104,28 @@ var DecodeWorkerChild = class {
|
|
|
967
1104
|
return this.nativeRing.get(frameId) ?? this.frames.toBuffer(frameId);
|
|
968
1105
|
}
|
|
969
1106
|
/**
|
|
1107
|
+
* Resolve a decoded frame's `(colorSpace, colorRange)` tags to the EXPLICIT
|
|
1108
|
+
* `(matrix, range)` every RGB conversion site pins — the RC-5 fix. `colorSpace`
|
|
1109
|
+
* / `colorRange` are node-av branded `number` subtypes, so they pass to the
|
|
1110
|
+
* plain-number helper directly.
|
|
1111
|
+
*/
|
|
1112
|
+
resolveFrameColor(frame) {
|
|
1113
|
+
return resolveSourceColor(frame.colorSpace, frame.colorRange);
|
|
1114
|
+
}
|
|
1115
|
+
/**
|
|
1116
|
+
* Emit the color-provenance marker: which decode/scale path produced this
|
|
1117
|
+
* frame + its raw and resolved source color. Always logs on a TRANSITION (the
|
|
1118
|
+
* path or the frame's raw color tags changed vs the last emit — rare, and the
|
|
1119
|
+
* exact signal a desaturation recurrence leaves); logs every frame only under
|
|
1120
|
+
* the `CAMSTACK_DECODE_COLOR_DEBUG` flag.
|
|
1121
|
+
*/
|
|
1122
|
+
logColorProvenance(path, color) {
|
|
1123
|
+
const key = `${path}|${color.rawColorSpace}|${color.rawColorRange}`;
|
|
1124
|
+
const transition = key !== this.lastColorProvenanceKey;
|
|
1125
|
+
if (transition) this.lastColorProvenanceKey = key;
|
|
1126
|
+
if (transition || this.colorDebug) this.emitStderr(colorProvenanceLine(path, color, transition));
|
|
1127
|
+
}
|
|
1128
|
+
/**
|
|
970
1129
|
* Retain a just-superseded DELIVERED frame for later native crops. Transfers
|
|
971
1130
|
* ownership from the {@link FrameSlot} (the caller MUST NOT free when this
|
|
972
1131
|
* returns `true`).
|
|
@@ -1049,6 +1208,7 @@ var DecodeWorkerChild = class {
|
|
|
1049
1208
|
const nav = this.nav;
|
|
1050
1209
|
const C = this.consts;
|
|
1051
1210
|
if (!nav || !C) throw new Error("node-av not initialized");
|
|
1211
|
+
this.logColorProvenance("sw-crop", this.resolveFrameColor(frame));
|
|
1052
1212
|
const { region, target } = resolved;
|
|
1053
1213
|
const planes = frame.data;
|
|
1054
1214
|
const strides = frame.linesize;
|
|
@@ -1087,7 +1247,9 @@ var DecodeWorkerChild = class {
|
|
|
1087
1247
|
*/
|
|
1088
1248
|
nativeHwCropToBuffer(frame, resolved) {
|
|
1089
1249
|
const { region, target } = resolved;
|
|
1090
|
-
const
|
|
1250
|
+
const color = this.resolveFrameColor(frame);
|
|
1251
|
+
this.logColorProvenance("hw-crop", color);
|
|
1252
|
+
const outputs = this.ensureNativeCropHwFilter(region, target, color).processAllSync(frame);
|
|
1091
1253
|
const first = outputs[0];
|
|
1092
1254
|
if (!first) {
|
|
1093
1255
|
for (const out of outputs) out.free();
|
|
@@ -1121,17 +1283,17 @@ var DecodeWorkerChild = class {
|
|
|
1121
1283
|
return scaler;
|
|
1122
1284
|
}
|
|
1123
1285
|
/** Build-once / reuse the dedicated native-crop GPU filtergraph (rgb out). */
|
|
1124
|
-
ensureNativeCropHwFilter(region, target) {
|
|
1286
|
+
ensureNativeCropHwFilter(region, target, color) {
|
|
1125
1287
|
const nav = this.nav;
|
|
1126
1288
|
const scaleFilter = this.gpuScaleFilter;
|
|
1127
1289
|
if (!nav || !scaleFilter || !this.hwContext) throw new Error("decode-worker-child: native GPU crop requested without a HW context");
|
|
1128
|
-
const key = `${region.left},${region.top},${region.width},${region.height}->${target.width}x${target.height}`;
|
|
1290
|
+
const key = `${region.left},${region.top},${region.width},${region.height}->${target.width}x${target.height}:${sourceColorKey(color)}`;
|
|
1129
1291
|
const cached = this.nativeCropHwFilter;
|
|
1130
1292
|
if (cached && key === this.nativeCropHwFilterKey) return cached;
|
|
1131
1293
|
this.nativeCropHwFilter?.close();
|
|
1132
1294
|
this.nativeCropHwFilter = null;
|
|
1133
1295
|
this.nativeCropHwFilterKey = "";
|
|
1134
|
-
const description = buildGpuFilterDescription(scaleFilter, region, target, false, "rgb24");
|
|
1296
|
+
const description = buildGpuFilterDescription(scaleFilter, region, target, false, "rgb24", color);
|
|
1135
1297
|
const filter = nav.FilterAPI.create(description, { hardware: this.hwContext });
|
|
1136
1298
|
this.nativeCropHwFilter = filter;
|
|
1137
1299
|
this.nativeCropHwFilterKey = key;
|
|
@@ -1340,6 +1502,7 @@ var DecodeWorkerChild = class {
|
|
|
1340
1502
|
const format = opts?.format ?? this.sessionFormat;
|
|
1341
1503
|
const dstFmt = format === "gray" ? C.AV_PIX_FMT_GRAY8 : C.AV_PIX_FMT_RGB24;
|
|
1342
1504
|
const channels = format === "gray" ? 1 : 3;
|
|
1505
|
+
this.logColorProvenance(isFullFrameRegion(region, frame.width, frame.height) ? "sw-fullframe" : "sw-crop", this.resolveFrameColor(frame));
|
|
1343
1506
|
const planes = frame.data;
|
|
1344
1507
|
const strides = frame.linesize;
|
|
1345
1508
|
const yPlane = planes?.[0];
|
|
@@ -1411,7 +1574,9 @@ var DecodeWorkerChild = class {
|
|
|
1411
1574
|
const target = resolveResizeTarget(region.width, region.height, opts?.resize);
|
|
1412
1575
|
const format = opts?.format ?? this.sessionFormat;
|
|
1413
1576
|
const channels = format === "gray" ? 1 : 3;
|
|
1414
|
-
const
|
|
1577
|
+
const color = this.resolveFrameColor(frame);
|
|
1578
|
+
this.logColorProvenance(isFullFrameRegion(region, frame.width, frame.height) ? "hw-fullframe" : "hw-crop", color);
|
|
1579
|
+
const outputs = this.ensureHwFilter(region, target, frame.width, frame.height, format, color).processAllSync(frame);
|
|
1415
1580
|
const first = outputs[0];
|
|
1416
1581
|
if (!first) throw new Error("GPU filtergraph produced no frame");
|
|
1417
1582
|
try {
|
|
@@ -1433,19 +1598,19 @@ var DecodeWorkerChild = class {
|
|
|
1433
1598
|
* only when the key changes; detection's ROI + resize target are stable, so
|
|
1434
1599
|
* this is a per-worker singleton in the steady state.
|
|
1435
1600
|
*/
|
|
1436
|
-
ensureHwFilter(region, target, srcW, srcH, format) {
|
|
1601
|
+
ensureHwFilter(region, target, srcW, srcH, format, color) {
|
|
1437
1602
|
const nav = this.nav;
|
|
1438
1603
|
const scaleFilter = this.gpuScaleFilter;
|
|
1439
1604
|
if (!nav || !scaleFilter || !this.hwContext) throw new Error("decode-worker-child: GPU filter requested without a HW context");
|
|
1440
1605
|
const fullFrame = isFullFrameRegion(region, srcW, srcH);
|
|
1441
1606
|
const pixel = pixelFilterName(format);
|
|
1442
|
-
const key = `${region.left},${region.top},${region.width},${region.height}->${target.width}x${target.height}:${pixel}`;
|
|
1607
|
+
const key = `${region.left},${region.top},${region.width},${region.height}->${target.width}x${target.height}:${pixel}:${sourceColorKey(color)}`;
|
|
1443
1608
|
const cached = this.hwFilter;
|
|
1444
1609
|
if (cached && key === this.hwFilterKey) return cached;
|
|
1445
1610
|
this.hwFilter?.close();
|
|
1446
1611
|
this.hwFilter = null;
|
|
1447
1612
|
this.hwFilterKey = "";
|
|
1448
|
-
const description = buildGpuFilterDescription(scaleFilter, region, target, fullFrame, pixel);
|
|
1613
|
+
const description = buildGpuFilterDescription(scaleFilter, region, target, fullFrame, pixel, color);
|
|
1449
1614
|
const filter = nav.FilterAPI.create(description, { hardware: this.hwContext });
|
|
1450
1615
|
this.hwFilter = filter;
|
|
1451
1616
|
this.hwFilterKey = key;
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { a as COCO_TO_MACRO, i as COCO_80_LABELS, r as AUDIO_MACRO_LABELS, w as hfModelUrl } from "./dist-
|
|
1
|
+
import { a as COCO_TO_MACRO, i as COCO_80_LABELS, r as AUDIO_MACRO_LABELS, w as hfModelUrl } from "./dist-DkGHtAaK.mjs";
|
|
2
2
|
//#region src/detection-pipeline/registry/model-catalogs.ts
|
|
3
3
|
var HF_REPO = "camstack/camstack-models";
|
|
4
4
|
var HF_SCRYPTED = "scrypted/plugin-models";
|
|
@@ -2,7 +2,7 @@ import { a as e, c as t, d as n, i as r, l as i, n as a, o, r as s, s as c, t as
|
|
|
2
2
|
import { a as d, i as f, n as p, o as m, r as h, t as g } from "./_virtual_mf___mfe_internal__addon_stream_broker_widgets__loadShare__react__loadShare__.js-C9j-2lBe.mjs";
|
|
3
3
|
import { n as _, r as v, t as y } from "./_virtual_mf___mfe_internal__addon_stream_broker_widgets__loadShare__react_mf_1_jsx_mf_2_runtime__loadShare__.js-XO0-Pyu6.mjs";
|
|
4
4
|
import { n as b, t as x } from "./_virtual_mf___mfe_internal__addon_stream_broker_widgets__loadShare___mf_0_tanstack_mf_1_react_mf_2_query__loadShare__.js-BO7TIbJV.mjs";
|
|
5
|
-
import { t as S } from "./_virtual_mf___mfe_internal__addon_stream_broker_widgets__loadShare___mf_0_camstack_mf_1_types__loadShare__.js-
|
|
5
|
+
import { t as S } from "./_virtual_mf___mfe_internal__addon_stream_broker_widgets__loadShare___mf_0_camstack_mf_1_types__loadShare__.js-D68Lj69v.mjs";
|
|
6
6
|
//#region ../../node_modules/lucide-react/dist/esm/shared/src/utils.js
|
|
7
7
|
var C = (e) => e.replace(/([a-z0-9])([A-Z])/g, "$1-$2").toLowerCase(), w = (e) => e.replace(/^([A-Z])|[\s-_]+(\w)/g, (e, t, n) => n ? n.toUpperCase() : t.toLowerCase()), T = (e) => {
|
|
8
8
|
let t = w(e);
|