@camstack/addon-pipeline 1.1.39 → 1.1.41
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 +168 -61
- package/dist/audio-analyzer/index.mjs +168 -61
- package/dist/detection-pipeline/index.js +2 -2
- package/dist/detection-pipeline/index.mjs +2 -2
- package/dist/{dist-DbGdZ8Nr.js → dist-BJTPkJFw.js} +247 -3
- package/dist/{dist-DvvYzO58.mjs → dist-v6cKLmU3.mjs} +247 -3
- package/dist/{frame-handle-plane-B7jMIZc2.js → frame-handle-plane-BP8YV4sF.js} +54 -5
- package/dist/{frame-handle-plane-BFzoIfkd.mjs → frame-handle-plane-DKAXTtfn.mjs} +54 -5
- package/dist/motion-wasm/index.js +1 -1
- package/dist/motion-wasm/index.mjs +1 -1
- package/dist/pipeline-runner/index.js +893 -12
- package/dist/pipeline-runner/index.mjs +892 -13
- package/dist/recorder/index.js +2 -2
- package/dist/recorder/index.mjs +2 -2
- package/dist/session-decode/decode-worker-child.js +544 -0
- package/dist/session-decode/decode-worker-child.mjs +543 -0
- package/dist/stream-broker/_stub.js +44 -44
- package/dist/stream-broker/{_virtual_mf-localSharedImportMap___mfe_internal__addon_stream_broker_widgets-DezFy4Fu.mjs → _virtual_mf-localSharedImportMap___mfe_internal__addon_stream_broker_widgets-C2l0kmD4.mjs} +3 -3
- package/dist/stream-broker/_virtual_mf___mfe_internal__addon_stream_broker_widgets__loadShare___mf_0_camstack_mf_1_types__loadShare__.js-DqJvWBKS.mjs +26 -0
- package/dist/stream-broker/_virtual_mf___mfe_internal__addon_stream_broker_widgets__loadShare___mf_0_camstack_mf_1_ui_mf_2_library__loadShare__.js-dGka8yZY.mjs +26 -0
- package/dist/stream-broker/{hostInit-BOf0hg9q.mjs → hostInit-DHaCRMjM.mjs} +3 -3
- package/dist/stream-broker/index.js +268 -38
- package/dist/stream-broker/index.mjs +268 -38
- package/dist/stream-broker/remoteEntry.js +1 -1
- package/dist/worker-protocol-BCfO8gUF.js +67 -0
- package/dist/worker-protocol-pk7qdYXt.mjs +56 -0
- package/embed-dist/assets/{MaskShapeCanvas-DI4BY7W2-Br2fqwe7.js → MaskShapeCanvas-DI4BY7W2-B6fza7ic.js} +1 -1
- package/embed-dist/assets/{MotionZonesSettings-NcxxQN8r-DYXRf30g.js → MotionZonesSettings-NcxxQN8r-Cy-4iTog.js} +1 -1
- package/embed-dist/assets/{PrivacyMaskSettings-APgPLF7p-DLpqLgPo.js → PrivacyMaskSettings-APgPLF7p-CYX33lJb.js} +1 -1
- package/embed-dist/assets/index-BPkayx9u.js +81 -0
- package/embed-dist/assets/index-CSFtK41z.css +2 -0
- package/embed-dist/index.html +2 -2
- package/package.json +6 -3
- package/python/inference_pool.py +3 -1
- package/dist/stream-broker/_virtual_mf___mfe_internal__addon_stream_broker_widgets__loadShare___mf_0_camstack_mf_1_types__loadShare__.js-DhY3MZ2C.mjs +0 -26
- package/dist/stream-broker/_virtual_mf___mfe_internal__addon_stream_broker_widgets__loadShare___mf_0_camstack_mf_1_ui_mf_2_library__loadShare__.js-BsrrdOBU.mjs +0 -26
- package/embed-dist/assets/index-C5UpuPr8.css +0 -2
- package/embed-dist/assets/index-CR367rYR.js +0 -81
|
@@ -3,7 +3,7 @@ Object.defineProperties(exports, {
|
|
|
3
3
|
[Symbol.toStringTag]: { value: "Module" }
|
|
4
4
|
});
|
|
5
5
|
const require_model_download_service_C_IHWnXx = require("../model-download-service-C-IHWnXx-DxM2DSns.js");
|
|
6
|
-
const require_dist = require("../dist-
|
|
6
|
+
const require_dist = require("../dist-BJTPkJFw.js");
|
|
7
7
|
const require_node_topology_platform = require("../node-topology-platform-CFZ7F4xW.js");
|
|
8
8
|
let node_fs = require("node:fs");
|
|
9
9
|
node_fs = require_model_download_service_C_IHWnXx.__toESM(node_fs);
|
|
@@ -415,6 +415,7 @@ var AUDIO_MODEL_OPTIONS = [
|
|
|
415
415
|
*/
|
|
416
416
|
var CLASSIFY_ERROR_SUPPRESS_MS = 3e4;
|
|
417
417
|
var CLASSIFY_MIN_INTERVAL_MS = 500;
|
|
418
|
+
var PIPELINE_IDLE_DISPOSE_MS = 5 * 6e4;
|
|
418
419
|
var GLOBAL_DEVICE_KEY = -1;
|
|
419
420
|
/**
|
|
420
421
|
* Reconstruct a Float32 view over f32le PCM bytes carried in a Uint8Array.
|
|
@@ -431,7 +432,7 @@ function float32FromBytes(raw) {
|
|
|
431
432
|
return new Float32Array(bytes.buffer, bytes.byteOffset, Math.floor(bytes.byteLength / 4));
|
|
432
433
|
}
|
|
433
434
|
var AudioAnalyzerProvider = class {
|
|
434
|
-
|
|
435
|
+
pipelineFactory;
|
|
435
436
|
deviceSettingsResolver;
|
|
436
437
|
deviceContributionResolver;
|
|
437
438
|
deviceSettingsPatcher;
|
|
@@ -451,14 +452,98 @@ var AudioAnalyzerProvider = class {
|
|
|
451
452
|
* run at a time. Without this, concurrent calls from different cameras overwrite the
|
|
452
453
|
* single pendingResolve slot in AppleSoundAnalysisPipeline, causing 30s timeouts. */
|
|
453
454
|
pipelineBusy = false;
|
|
454
|
-
|
|
455
|
-
|
|
455
|
+
/** Live inference pipeline. LAZY: null until the first classify() spawns it
|
|
456
|
+
* (see `ensurePipeline`); idle-disposed back to null after a long quiet
|
|
457
|
+
* window so a node hosting the addon with no consumer never keeps the
|
|
458
|
+
* YAMNet Python / Apple SoundAnalysis subprocess resident. */
|
|
459
|
+
pipeline = null;
|
|
460
|
+
/** Single-flight guard for the lazy spawn — concurrent first-calls await the
|
|
461
|
+
* same build instead of spawning the subprocess twice. */
|
|
462
|
+
pipelineInitPromise = null;
|
|
463
|
+
/** Backend the NEXT lazy init will build (kept in sync with the operator's
|
|
464
|
+
* effective choice via `reconfigureBackend`). */
|
|
465
|
+
desiredBackend;
|
|
466
|
+
/** Idle-dispose timer — (re)armed after every classify. */
|
|
467
|
+
idleTimer = null;
|
|
468
|
+
/** Set on `dispose()` (addon stop) so a late classify/idle-timer never
|
|
469
|
+
* re-spawns the subprocess after teardown. */
|
|
470
|
+
disposed = false;
|
|
471
|
+
constructor(logger, pipelineFactory, initialBackend, deviceSettingsResolver, deviceContributionResolver, deviceSettingsPatcher, reprobeImpl) {
|
|
472
|
+
this.pipelineFactory = pipelineFactory;
|
|
456
473
|
this.deviceSettingsResolver = deviceSettingsResolver;
|
|
457
474
|
this.deviceContributionResolver = deviceContributionResolver;
|
|
458
475
|
this.deviceSettingsPatcher = deviceSettingsPatcher;
|
|
459
476
|
this.reprobeImpl = reprobeImpl;
|
|
460
477
|
this.log = logger;
|
|
461
|
-
this.backendName =
|
|
478
|
+
this.backendName = initialBackend;
|
|
479
|
+
this.desiredBackend = initialBackend;
|
|
480
|
+
}
|
|
481
|
+
/**
|
|
482
|
+
* Lazily spawn + initialize the inference pipeline on first use. The
|
|
483
|
+
* subprocess (YAMNet Python / Apple SoundAnalysis CLI, resident model) is NOT
|
|
484
|
+
* booted at addon start — only when the first classify() actually lands, so a
|
|
485
|
+
* node hosting the addon with no audio-classification consumer never spawns
|
|
486
|
+
* it. Concurrent first-calls single-flight through `pipelineInitPromise` so
|
|
487
|
+
* the subprocess is spawned exactly once.
|
|
488
|
+
*/
|
|
489
|
+
async ensurePipeline() {
|
|
490
|
+
if (this.disposed) throw new Error("audio-analyzer: provider disposed");
|
|
491
|
+
const live = this.pipeline;
|
|
492
|
+
if (live) return live;
|
|
493
|
+
if (this.pipelineInitPromise) return this.pipelineInitPromise;
|
|
494
|
+
const backend = this.desiredBackend;
|
|
495
|
+
const init = (async () => {
|
|
496
|
+
this.log.info("audio pipeline lazy init (first classify)", { meta: { backend } });
|
|
497
|
+
const built = await this.pipelineFactory(backend);
|
|
498
|
+
if (this.disposed) {
|
|
499
|
+
await built.dispose().catch(() => {});
|
|
500
|
+
throw new Error("audio-analyzer: provider disposed during init");
|
|
501
|
+
}
|
|
502
|
+
this.pipeline = built;
|
|
503
|
+
this.backendName = backend;
|
|
504
|
+
return built;
|
|
505
|
+
})();
|
|
506
|
+
this.pipelineInitPromise = init;
|
|
507
|
+
try {
|
|
508
|
+
return await init;
|
|
509
|
+
} finally {
|
|
510
|
+
this.pipelineInitPromise = null;
|
|
511
|
+
}
|
|
512
|
+
}
|
|
513
|
+
/** (Re)arm the idle-dispose timer. Called after every classify. */
|
|
514
|
+
scheduleIdleDispose() {
|
|
515
|
+
if (this.disposed) return;
|
|
516
|
+
if (this.idleTimer) clearTimeout(this.idleTimer);
|
|
517
|
+
const timer = setTimeout(() => {
|
|
518
|
+
this.idleTimer = null;
|
|
519
|
+
this.disposeIdlePipeline();
|
|
520
|
+
}, PIPELINE_IDLE_DISPOSE_MS);
|
|
521
|
+
if (typeof timer.unref === "function") timer.unref();
|
|
522
|
+
this.idleTimer = timer;
|
|
523
|
+
}
|
|
524
|
+
/**
|
|
525
|
+
* Tear the inference subprocess down after a long idle window, freeing the
|
|
526
|
+
* resident model. The next classify lazily re-spawns it. No-op (and re-arms)
|
|
527
|
+
* if a classify is in flight; no-op if nothing is spawned.
|
|
528
|
+
*/
|
|
529
|
+
async disposeIdlePipeline() {
|
|
530
|
+
if (this.disposed) return;
|
|
531
|
+
if (this.pipelineBusy || this.pipelineInitPromise) {
|
|
532
|
+
this.scheduleIdleDispose();
|
|
533
|
+
return;
|
|
534
|
+
}
|
|
535
|
+
const live = this.pipeline;
|
|
536
|
+
if (!live) return;
|
|
537
|
+
this.pipeline = null;
|
|
538
|
+
try {
|
|
539
|
+
await live.dispose();
|
|
540
|
+
this.log.info("audio pipeline idle-disposed — freed inference subprocess", { meta: {
|
|
541
|
+
backend: this.backendName,
|
|
542
|
+
idleMs: PIPELINE_IDLE_DISPOSE_MS
|
|
543
|
+
} });
|
|
544
|
+
} catch (err) {
|
|
545
|
+
this.log.warn("audio pipeline idle-dispose failed", { meta: { error: require_dist.errMsg(err) } });
|
|
546
|
+
}
|
|
462
547
|
}
|
|
463
548
|
async getDeviceSettingsContribution(input) {
|
|
464
549
|
return this.deviceContributionResolver(input.deviceId);
|
|
@@ -568,61 +653,88 @@ var AudioAnalyzerProvider = class {
|
|
|
568
653
|
lastEndMs: state?.lastEndMs ?? 0
|
|
569
654
|
});
|
|
570
655
|
this.pipelineBusy = true;
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
656
|
+
try {
|
|
657
|
+
const pipeline = await this.ensurePipeline();
|
|
658
|
+
const f32Data = float32FromBytes(chunk.data);
|
|
659
|
+
const result = await pipeline.classify({
|
|
660
|
+
data: f32Data,
|
|
661
|
+
sampleRate: chunk.sampleRate,
|
|
662
|
+
channels: chunk.channels
|
|
663
|
+
});
|
|
664
|
+
if (this.debugClassifySamples && (this.classifyCount < 3 || this.classifyCount % 100 === 0)) {
|
|
665
|
+
const rawTop = result.classifications.slice(0, 5).map((c) => `"${c.className}"(${(c.score * 100).toFixed(0)}%)`).join(", ");
|
|
666
|
+
this.log.info("classify debug sample", {
|
|
667
|
+
tags: chunk.deviceId !== void 0 ? { deviceId: chunk.deviceId } : void 0,
|
|
668
|
+
meta: {
|
|
669
|
+
index: this.classifyCount,
|
|
670
|
+
engine: this.backendName,
|
|
671
|
+
rawLabelCount: result.classifications.length,
|
|
672
|
+
top: rawTop,
|
|
673
|
+
inferenceMs: result.inferenceMs
|
|
674
|
+
}
|
|
675
|
+
});
|
|
676
|
+
}
|
|
677
|
+
this.classifyCount++;
|
|
678
|
+
const macroAccum = /* @__PURE__ */ new Map();
|
|
679
|
+
for (const c of result.classifications) {
|
|
680
|
+
const macro = require_dist.mapAudioLabelToMacro(c.className);
|
|
681
|
+
if (!macro) continue;
|
|
682
|
+
const prev = macroAccum.get(macro);
|
|
683
|
+
if (!prev || c.score > prev.score) macroAccum.set(macro, {
|
|
684
|
+
score: c.score,
|
|
685
|
+
rawTop: c.className
|
|
686
|
+
});
|
|
687
|
+
}
|
|
688
|
+
return {
|
|
689
|
+
labels: [...macroAccum.entries()].toSorted((a, b) => b[1].score - a[1].score).map(([className, { score, rawTop }]) => ({
|
|
690
|
+
className,
|
|
691
|
+
originalClass: rawTop,
|
|
692
|
+
score
|
|
693
|
+
})),
|
|
694
|
+
rawLabels: [...result.classifications].toSorted((a, b) => b.score - a.score).map((c) => ({
|
|
695
|
+
className: c.className,
|
|
696
|
+
originalClass: c.className,
|
|
697
|
+
score: c.score
|
|
698
|
+
})),
|
|
699
|
+
inferenceMs: result.inferenceMs
|
|
700
|
+
};
|
|
701
|
+
} finally {
|
|
577
702
|
this.pipelineBusy = false;
|
|
578
703
|
this.cameraState.set(camKey, {
|
|
579
704
|
inProgress: false,
|
|
580
705
|
lastEndMs: Date.now()
|
|
581
706
|
});
|
|
582
|
-
|
|
583
|
-
if (this.debugClassifySamples && (this.classifyCount < 3 || this.classifyCount % 100 === 0)) {
|
|
584
|
-
const rawTop = result.classifications.slice(0, 5).map((c) => `"${c.className}"(${(c.score * 100).toFixed(0)}%)`).join(", ");
|
|
585
|
-
this.log.info("classify debug sample", {
|
|
586
|
-
tags: chunk.deviceId !== void 0 ? { deviceId: chunk.deviceId } : void 0,
|
|
587
|
-
meta: {
|
|
588
|
-
index: this.classifyCount,
|
|
589
|
-
engine: this.backendName,
|
|
590
|
-
rawLabelCount: result.classifications.length,
|
|
591
|
-
top: rawTop,
|
|
592
|
-
inferenceMs: result.inferenceMs
|
|
593
|
-
}
|
|
594
|
-
});
|
|
595
|
-
}
|
|
596
|
-
this.classifyCount++;
|
|
597
|
-
const macroAccum = /* @__PURE__ */ new Map();
|
|
598
|
-
for (const c of result.classifications) {
|
|
599
|
-
const macro = require_dist.mapAudioLabelToMacro(c.className);
|
|
600
|
-
if (!macro) continue;
|
|
601
|
-
const prev = macroAccum.get(macro);
|
|
602
|
-
if (!prev || c.score > prev.score) macroAccum.set(macro, {
|
|
603
|
-
score: c.score,
|
|
604
|
-
rawTop: c.className
|
|
605
|
-
});
|
|
707
|
+
this.scheduleIdleDispose();
|
|
606
708
|
}
|
|
607
|
-
return {
|
|
608
|
-
labels: [...macroAccum.entries()].toSorted((a, b) => b[1].score - a[1].score).map(([className, { score, rawTop }]) => ({
|
|
609
|
-
className,
|
|
610
|
-
originalClass: rawTop,
|
|
611
|
-
score
|
|
612
|
-
})),
|
|
613
|
-
rawLabels: [...result.classifications].toSorted((a, b) => b.score - a.score).map((c) => ({
|
|
614
|
-
className: c.className,
|
|
615
|
-
originalClass: c.className,
|
|
616
|
-
score: c.score
|
|
617
|
-
})),
|
|
618
|
-
inferenceMs: result.inferenceMs
|
|
619
|
-
};
|
|
620
709
|
}
|
|
621
710
|
isReady() {
|
|
622
|
-
return this.
|
|
711
|
+
return !this.disposed;
|
|
623
712
|
}
|
|
624
713
|
async dispose() {
|
|
625
|
-
|
|
714
|
+
this.disposed = true;
|
|
715
|
+
if (this.idleTimer) {
|
|
716
|
+
clearTimeout(this.idleTimer);
|
|
717
|
+
this.idleTimer = null;
|
|
718
|
+
}
|
|
719
|
+
const live = this.pipeline;
|
|
720
|
+
this.pipeline = null;
|
|
721
|
+
if (live) await live.dispose();
|
|
722
|
+
}
|
|
723
|
+
/**
|
|
724
|
+
* Apply a backend/model change. Records the backend the NEXT lazy init will
|
|
725
|
+
* build; if a pipeline is already live it rebuilds IN PLACE (via
|
|
726
|
+
* `swapPipeline`), otherwise the subprocess stays unspawned until the next
|
|
727
|
+
* classify. Lets the audio engine settings apply without an addon restart,
|
|
728
|
+
* mirroring the detection-pipeline's in-place engine swap.
|
|
729
|
+
*/
|
|
730
|
+
async reconfigureBackend(next) {
|
|
731
|
+
this.desiredBackend = next;
|
|
732
|
+
if (!this.pipeline) {
|
|
733
|
+
this.backendName = next;
|
|
734
|
+
return;
|
|
735
|
+
}
|
|
736
|
+
const rebuilt = await this.pipelineFactory(next);
|
|
737
|
+
await this.swapPipeline(rebuilt, next);
|
|
626
738
|
}
|
|
627
739
|
/**
|
|
628
740
|
* Swap the live pipeline in place (optimistic backend/model change) — disposes
|
|
@@ -651,9 +763,9 @@ var AudioAnalyzerProvider = class {
|
|
|
651
763
|
var AudioAnalyzerAddon = class extends require_dist.BaseAddon {
|
|
652
764
|
id = "audio-analyzer";
|
|
653
765
|
provider = null;
|
|
654
|
-
pipeline
|
|
655
|
-
|
|
656
|
-
*
|
|
766
|
+
/** Backend the provider's pipeline is configured for — drives the optimistic
|
|
767
|
+
* in-place rebuild in `onConfigChanged` (rebuild only when it genuinely
|
|
768
|
+
* flips). The provider owns the (lazy) pipeline itself. */
|
|
657
769
|
activeBackend = null;
|
|
658
770
|
constructor() {
|
|
659
771
|
super(require_dist.DEFAULT_AUDIO_ANALYZER_CONFIG);
|
|
@@ -804,8 +916,6 @@ var AudioAnalyzerAddon = class extends require_dist.BaseAddon {
|
|
|
804
916
|
async onInitialize() {
|
|
805
917
|
const logger = this.ctx.logger;
|
|
806
918
|
const backend = this.resolveAudioBackend();
|
|
807
|
-
const p = await this.buildPipeline(backend);
|
|
808
|
-
this.pipeline = p;
|
|
809
919
|
this.activeBackend = backend;
|
|
810
920
|
if (!this.config.probedBestAudioBackend) this.reprobeAudioEngine().catch((err) => {
|
|
811
921
|
logger.warn("audio: auto-reprobe failed", { meta: { error: err instanceof Error ? err.message : String(err) } });
|
|
@@ -829,7 +939,7 @@ var AudioAnalyzerAddon = class extends require_dist.BaseAddon {
|
|
|
829
939
|
return null;
|
|
830
940
|
};
|
|
831
941
|
const deviceSettingsPatcher = async (_deviceId, _patch) => {};
|
|
832
|
-
this.provider = new AudioAnalyzerProvider(logger, this.
|
|
942
|
+
this.provider = new AudioAnalyzerProvider(logger, (b) => this.buildPipeline(b), backend, deviceSettingsResolver, deviceContributionResolver, deviceSettingsPatcher, () => this.reprobeAudioEngine());
|
|
833
943
|
return [{
|
|
834
944
|
capability: require_dist.audioAnalyzerCapability,
|
|
835
945
|
provider: this.provider
|
|
@@ -843,7 +953,6 @@ var AudioAnalyzerAddon = class extends require_dist.BaseAddon {
|
|
|
843
953
|
await this.provider.dispose();
|
|
844
954
|
this.provider = null;
|
|
845
955
|
}
|
|
846
|
-
this.pipeline = null;
|
|
847
956
|
this.activeBackend = null;
|
|
848
957
|
}
|
|
849
958
|
/**
|
|
@@ -878,13 +987,11 @@ var AudioAnalyzerAddon = class extends require_dist.BaseAddon {
|
|
|
878
987
|
if (!this.provider) return;
|
|
879
988
|
const backend = this.resolveAudioBackend();
|
|
880
989
|
if (backend === this.activeBackend) return;
|
|
881
|
-
this.ctx.logger.info("audio backend changed —
|
|
990
|
+
this.ctx.logger.info("audio backend changed — applying to pipeline", { meta: {
|
|
882
991
|
from: this.activeBackend,
|
|
883
992
|
to: backend
|
|
884
993
|
} });
|
|
885
|
-
|
|
886
|
-
await this.provider.swapPipeline(next, backend);
|
|
887
|
-
this.pipeline = next;
|
|
994
|
+
await this.provider.reconfigureBackend(backend);
|
|
888
995
|
this.activeBackend = backend;
|
|
889
996
|
}
|
|
890
997
|
buildDeviceSchema() {
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { t as __require } from "../chunk-BdkLduGY.mjs";
|
|
2
|
-
import { C as mapAudioLabelToMacro, I as errMsg, L as BaseAddon, U as hydrateSchema, h as audioAnalyzerCapability, l as HF_BASE_URL, m as audioAnalysisCapability, n as AUDIO_BACKEND_CHOICES, o as DEFAULT_AUDIO_ANALYZER_CONFIG } from "../dist-
|
|
2
|
+
import { C as mapAudioLabelToMacro, I as errMsg, L as BaseAddon, U as hydrateSchema, h as audioAnalyzerCapability, l as HF_BASE_URL, m as audioAnalysisCapability, n as AUDIO_BACKEND_CHOICES, o as DEFAULT_AUDIO_ANALYZER_CONFIG } from "../dist-v6cKLmU3.mjs";
|
|
3
3
|
import { t as pickNodePlatformArch } from "../node-topology-platform-BkR_k6WT.mjs";
|
|
4
4
|
import { r as downloadFile } from "../model-download-service-C-IHWnXx-BPy6aoAx.mjs";
|
|
5
5
|
import * as fs from "node:fs";
|
|
@@ -410,6 +410,7 @@ var AUDIO_MODEL_OPTIONS = [
|
|
|
410
410
|
*/
|
|
411
411
|
var CLASSIFY_ERROR_SUPPRESS_MS = 3e4;
|
|
412
412
|
var CLASSIFY_MIN_INTERVAL_MS = 500;
|
|
413
|
+
var PIPELINE_IDLE_DISPOSE_MS = 5 * 6e4;
|
|
413
414
|
var GLOBAL_DEVICE_KEY = -1;
|
|
414
415
|
/**
|
|
415
416
|
* Reconstruct a Float32 view over f32le PCM bytes carried in a Uint8Array.
|
|
@@ -426,7 +427,7 @@ function float32FromBytes(raw) {
|
|
|
426
427
|
return new Float32Array(bytes.buffer, bytes.byteOffset, Math.floor(bytes.byteLength / 4));
|
|
427
428
|
}
|
|
428
429
|
var AudioAnalyzerProvider = class {
|
|
429
|
-
|
|
430
|
+
pipelineFactory;
|
|
430
431
|
deviceSettingsResolver;
|
|
431
432
|
deviceContributionResolver;
|
|
432
433
|
deviceSettingsPatcher;
|
|
@@ -446,14 +447,98 @@ var AudioAnalyzerProvider = class {
|
|
|
446
447
|
* run at a time. Without this, concurrent calls from different cameras overwrite the
|
|
447
448
|
* single pendingResolve slot in AppleSoundAnalysisPipeline, causing 30s timeouts. */
|
|
448
449
|
pipelineBusy = false;
|
|
449
|
-
|
|
450
|
-
|
|
450
|
+
/** Live inference pipeline. LAZY: null until the first classify() spawns it
|
|
451
|
+
* (see `ensurePipeline`); idle-disposed back to null after a long quiet
|
|
452
|
+
* window so a node hosting the addon with no consumer never keeps the
|
|
453
|
+
* YAMNet Python / Apple SoundAnalysis subprocess resident. */
|
|
454
|
+
pipeline = null;
|
|
455
|
+
/** Single-flight guard for the lazy spawn — concurrent first-calls await the
|
|
456
|
+
* same build instead of spawning the subprocess twice. */
|
|
457
|
+
pipelineInitPromise = null;
|
|
458
|
+
/** Backend the NEXT lazy init will build (kept in sync with the operator's
|
|
459
|
+
* effective choice via `reconfigureBackend`). */
|
|
460
|
+
desiredBackend;
|
|
461
|
+
/** Idle-dispose timer — (re)armed after every classify. */
|
|
462
|
+
idleTimer = null;
|
|
463
|
+
/** Set on `dispose()` (addon stop) so a late classify/idle-timer never
|
|
464
|
+
* re-spawns the subprocess after teardown. */
|
|
465
|
+
disposed = false;
|
|
466
|
+
constructor(logger, pipelineFactory, initialBackend, deviceSettingsResolver, deviceContributionResolver, deviceSettingsPatcher, reprobeImpl) {
|
|
467
|
+
this.pipelineFactory = pipelineFactory;
|
|
451
468
|
this.deviceSettingsResolver = deviceSettingsResolver;
|
|
452
469
|
this.deviceContributionResolver = deviceContributionResolver;
|
|
453
470
|
this.deviceSettingsPatcher = deviceSettingsPatcher;
|
|
454
471
|
this.reprobeImpl = reprobeImpl;
|
|
455
472
|
this.log = logger;
|
|
456
|
-
this.backendName =
|
|
473
|
+
this.backendName = initialBackend;
|
|
474
|
+
this.desiredBackend = initialBackend;
|
|
475
|
+
}
|
|
476
|
+
/**
|
|
477
|
+
* Lazily spawn + initialize the inference pipeline on first use. The
|
|
478
|
+
* subprocess (YAMNet Python / Apple SoundAnalysis CLI, resident model) is NOT
|
|
479
|
+
* booted at addon start — only when the first classify() actually lands, so a
|
|
480
|
+
* node hosting the addon with no audio-classification consumer never spawns
|
|
481
|
+
* it. Concurrent first-calls single-flight through `pipelineInitPromise` so
|
|
482
|
+
* the subprocess is spawned exactly once.
|
|
483
|
+
*/
|
|
484
|
+
async ensurePipeline() {
|
|
485
|
+
if (this.disposed) throw new Error("audio-analyzer: provider disposed");
|
|
486
|
+
const live = this.pipeline;
|
|
487
|
+
if (live) return live;
|
|
488
|
+
if (this.pipelineInitPromise) return this.pipelineInitPromise;
|
|
489
|
+
const backend = this.desiredBackend;
|
|
490
|
+
const init = (async () => {
|
|
491
|
+
this.log.info("audio pipeline lazy init (first classify)", { meta: { backend } });
|
|
492
|
+
const built = await this.pipelineFactory(backend);
|
|
493
|
+
if (this.disposed) {
|
|
494
|
+
await built.dispose().catch(() => {});
|
|
495
|
+
throw new Error("audio-analyzer: provider disposed during init");
|
|
496
|
+
}
|
|
497
|
+
this.pipeline = built;
|
|
498
|
+
this.backendName = backend;
|
|
499
|
+
return built;
|
|
500
|
+
})();
|
|
501
|
+
this.pipelineInitPromise = init;
|
|
502
|
+
try {
|
|
503
|
+
return await init;
|
|
504
|
+
} finally {
|
|
505
|
+
this.pipelineInitPromise = null;
|
|
506
|
+
}
|
|
507
|
+
}
|
|
508
|
+
/** (Re)arm the idle-dispose timer. Called after every classify. */
|
|
509
|
+
scheduleIdleDispose() {
|
|
510
|
+
if (this.disposed) return;
|
|
511
|
+
if (this.idleTimer) clearTimeout(this.idleTimer);
|
|
512
|
+
const timer = setTimeout(() => {
|
|
513
|
+
this.idleTimer = null;
|
|
514
|
+
this.disposeIdlePipeline();
|
|
515
|
+
}, PIPELINE_IDLE_DISPOSE_MS);
|
|
516
|
+
if (typeof timer.unref === "function") timer.unref();
|
|
517
|
+
this.idleTimer = timer;
|
|
518
|
+
}
|
|
519
|
+
/**
|
|
520
|
+
* Tear the inference subprocess down after a long idle window, freeing the
|
|
521
|
+
* resident model. The next classify lazily re-spawns it. No-op (and re-arms)
|
|
522
|
+
* if a classify is in flight; no-op if nothing is spawned.
|
|
523
|
+
*/
|
|
524
|
+
async disposeIdlePipeline() {
|
|
525
|
+
if (this.disposed) return;
|
|
526
|
+
if (this.pipelineBusy || this.pipelineInitPromise) {
|
|
527
|
+
this.scheduleIdleDispose();
|
|
528
|
+
return;
|
|
529
|
+
}
|
|
530
|
+
const live = this.pipeline;
|
|
531
|
+
if (!live) return;
|
|
532
|
+
this.pipeline = null;
|
|
533
|
+
try {
|
|
534
|
+
await live.dispose();
|
|
535
|
+
this.log.info("audio pipeline idle-disposed — freed inference subprocess", { meta: {
|
|
536
|
+
backend: this.backendName,
|
|
537
|
+
idleMs: PIPELINE_IDLE_DISPOSE_MS
|
|
538
|
+
} });
|
|
539
|
+
} catch (err) {
|
|
540
|
+
this.log.warn("audio pipeline idle-dispose failed", { meta: { error: errMsg(err) } });
|
|
541
|
+
}
|
|
457
542
|
}
|
|
458
543
|
async getDeviceSettingsContribution(input) {
|
|
459
544
|
return this.deviceContributionResolver(input.deviceId);
|
|
@@ -563,61 +648,88 @@ var AudioAnalyzerProvider = class {
|
|
|
563
648
|
lastEndMs: state?.lastEndMs ?? 0
|
|
564
649
|
});
|
|
565
650
|
this.pipelineBusy = true;
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
651
|
+
try {
|
|
652
|
+
const pipeline = await this.ensurePipeline();
|
|
653
|
+
const f32Data = float32FromBytes(chunk.data);
|
|
654
|
+
const result = await pipeline.classify({
|
|
655
|
+
data: f32Data,
|
|
656
|
+
sampleRate: chunk.sampleRate,
|
|
657
|
+
channels: chunk.channels
|
|
658
|
+
});
|
|
659
|
+
if (this.debugClassifySamples && (this.classifyCount < 3 || this.classifyCount % 100 === 0)) {
|
|
660
|
+
const rawTop = result.classifications.slice(0, 5).map((c) => `"${c.className}"(${(c.score * 100).toFixed(0)}%)`).join(", ");
|
|
661
|
+
this.log.info("classify debug sample", {
|
|
662
|
+
tags: chunk.deviceId !== void 0 ? { deviceId: chunk.deviceId } : void 0,
|
|
663
|
+
meta: {
|
|
664
|
+
index: this.classifyCount,
|
|
665
|
+
engine: this.backendName,
|
|
666
|
+
rawLabelCount: result.classifications.length,
|
|
667
|
+
top: rawTop,
|
|
668
|
+
inferenceMs: result.inferenceMs
|
|
669
|
+
}
|
|
670
|
+
});
|
|
671
|
+
}
|
|
672
|
+
this.classifyCount++;
|
|
673
|
+
const macroAccum = /* @__PURE__ */ new Map();
|
|
674
|
+
for (const c of result.classifications) {
|
|
675
|
+
const macro = mapAudioLabelToMacro(c.className);
|
|
676
|
+
if (!macro) continue;
|
|
677
|
+
const prev = macroAccum.get(macro);
|
|
678
|
+
if (!prev || c.score > prev.score) macroAccum.set(macro, {
|
|
679
|
+
score: c.score,
|
|
680
|
+
rawTop: c.className
|
|
681
|
+
});
|
|
682
|
+
}
|
|
683
|
+
return {
|
|
684
|
+
labels: [...macroAccum.entries()].toSorted((a, b) => b[1].score - a[1].score).map(([className, { score, rawTop }]) => ({
|
|
685
|
+
className,
|
|
686
|
+
originalClass: rawTop,
|
|
687
|
+
score
|
|
688
|
+
})),
|
|
689
|
+
rawLabels: [...result.classifications].toSorted((a, b) => b.score - a.score).map((c) => ({
|
|
690
|
+
className: c.className,
|
|
691
|
+
originalClass: c.className,
|
|
692
|
+
score: c.score
|
|
693
|
+
})),
|
|
694
|
+
inferenceMs: result.inferenceMs
|
|
695
|
+
};
|
|
696
|
+
} finally {
|
|
572
697
|
this.pipelineBusy = false;
|
|
573
698
|
this.cameraState.set(camKey, {
|
|
574
699
|
inProgress: false,
|
|
575
700
|
lastEndMs: Date.now()
|
|
576
701
|
});
|
|
577
|
-
|
|
578
|
-
if (this.debugClassifySamples && (this.classifyCount < 3 || this.classifyCount % 100 === 0)) {
|
|
579
|
-
const rawTop = result.classifications.slice(0, 5).map((c) => `"${c.className}"(${(c.score * 100).toFixed(0)}%)`).join(", ");
|
|
580
|
-
this.log.info("classify debug sample", {
|
|
581
|
-
tags: chunk.deviceId !== void 0 ? { deviceId: chunk.deviceId } : void 0,
|
|
582
|
-
meta: {
|
|
583
|
-
index: this.classifyCount,
|
|
584
|
-
engine: this.backendName,
|
|
585
|
-
rawLabelCount: result.classifications.length,
|
|
586
|
-
top: rawTop,
|
|
587
|
-
inferenceMs: result.inferenceMs
|
|
588
|
-
}
|
|
589
|
-
});
|
|
590
|
-
}
|
|
591
|
-
this.classifyCount++;
|
|
592
|
-
const macroAccum = /* @__PURE__ */ new Map();
|
|
593
|
-
for (const c of result.classifications) {
|
|
594
|
-
const macro = mapAudioLabelToMacro(c.className);
|
|
595
|
-
if (!macro) continue;
|
|
596
|
-
const prev = macroAccum.get(macro);
|
|
597
|
-
if (!prev || c.score > prev.score) macroAccum.set(macro, {
|
|
598
|
-
score: c.score,
|
|
599
|
-
rawTop: c.className
|
|
600
|
-
});
|
|
702
|
+
this.scheduleIdleDispose();
|
|
601
703
|
}
|
|
602
|
-
return {
|
|
603
|
-
labels: [...macroAccum.entries()].toSorted((a, b) => b[1].score - a[1].score).map(([className, { score, rawTop }]) => ({
|
|
604
|
-
className,
|
|
605
|
-
originalClass: rawTop,
|
|
606
|
-
score
|
|
607
|
-
})),
|
|
608
|
-
rawLabels: [...result.classifications].toSorted((a, b) => b.score - a.score).map((c) => ({
|
|
609
|
-
className: c.className,
|
|
610
|
-
originalClass: c.className,
|
|
611
|
-
score: c.score
|
|
612
|
-
})),
|
|
613
|
-
inferenceMs: result.inferenceMs
|
|
614
|
-
};
|
|
615
704
|
}
|
|
616
705
|
isReady() {
|
|
617
|
-
return this.
|
|
706
|
+
return !this.disposed;
|
|
618
707
|
}
|
|
619
708
|
async dispose() {
|
|
620
|
-
|
|
709
|
+
this.disposed = true;
|
|
710
|
+
if (this.idleTimer) {
|
|
711
|
+
clearTimeout(this.idleTimer);
|
|
712
|
+
this.idleTimer = null;
|
|
713
|
+
}
|
|
714
|
+
const live = this.pipeline;
|
|
715
|
+
this.pipeline = null;
|
|
716
|
+
if (live) await live.dispose();
|
|
717
|
+
}
|
|
718
|
+
/**
|
|
719
|
+
* Apply a backend/model change. Records the backend the NEXT lazy init will
|
|
720
|
+
* build; if a pipeline is already live it rebuilds IN PLACE (via
|
|
721
|
+
* `swapPipeline`), otherwise the subprocess stays unspawned until the next
|
|
722
|
+
* classify. Lets the audio engine settings apply without an addon restart,
|
|
723
|
+
* mirroring the detection-pipeline's in-place engine swap.
|
|
724
|
+
*/
|
|
725
|
+
async reconfigureBackend(next) {
|
|
726
|
+
this.desiredBackend = next;
|
|
727
|
+
if (!this.pipeline) {
|
|
728
|
+
this.backendName = next;
|
|
729
|
+
return;
|
|
730
|
+
}
|
|
731
|
+
const rebuilt = await this.pipelineFactory(next);
|
|
732
|
+
await this.swapPipeline(rebuilt, next);
|
|
621
733
|
}
|
|
622
734
|
/**
|
|
623
735
|
* Swap the live pipeline in place (optimistic backend/model change) — disposes
|
|
@@ -646,9 +758,9 @@ var AudioAnalyzerProvider = class {
|
|
|
646
758
|
var AudioAnalyzerAddon = class extends BaseAddon {
|
|
647
759
|
id = "audio-analyzer";
|
|
648
760
|
provider = null;
|
|
649
|
-
pipeline
|
|
650
|
-
|
|
651
|
-
*
|
|
761
|
+
/** Backend the provider's pipeline is configured for — drives the optimistic
|
|
762
|
+
* in-place rebuild in `onConfigChanged` (rebuild only when it genuinely
|
|
763
|
+
* flips). The provider owns the (lazy) pipeline itself. */
|
|
652
764
|
activeBackend = null;
|
|
653
765
|
constructor() {
|
|
654
766
|
super(DEFAULT_AUDIO_ANALYZER_CONFIG);
|
|
@@ -799,8 +911,6 @@ var AudioAnalyzerAddon = class extends BaseAddon {
|
|
|
799
911
|
async onInitialize() {
|
|
800
912
|
const logger = this.ctx.logger;
|
|
801
913
|
const backend = this.resolveAudioBackend();
|
|
802
|
-
const p = await this.buildPipeline(backend);
|
|
803
|
-
this.pipeline = p;
|
|
804
914
|
this.activeBackend = backend;
|
|
805
915
|
if (!this.config.probedBestAudioBackend) this.reprobeAudioEngine().catch((err) => {
|
|
806
916
|
logger.warn("audio: auto-reprobe failed", { meta: { error: err instanceof Error ? err.message : String(err) } });
|
|
@@ -824,7 +934,7 @@ var AudioAnalyzerAddon = class extends BaseAddon {
|
|
|
824
934
|
return null;
|
|
825
935
|
};
|
|
826
936
|
const deviceSettingsPatcher = async (_deviceId, _patch) => {};
|
|
827
|
-
this.provider = new AudioAnalyzerProvider(logger, this.
|
|
937
|
+
this.provider = new AudioAnalyzerProvider(logger, (b) => this.buildPipeline(b), backend, deviceSettingsResolver, deviceContributionResolver, deviceSettingsPatcher, () => this.reprobeAudioEngine());
|
|
828
938
|
return [{
|
|
829
939
|
capability: audioAnalyzerCapability,
|
|
830
940
|
provider: this.provider
|
|
@@ -838,7 +948,6 @@ var AudioAnalyzerAddon = class extends BaseAddon {
|
|
|
838
948
|
await this.provider.dispose();
|
|
839
949
|
this.provider = null;
|
|
840
950
|
}
|
|
841
|
-
this.pipeline = null;
|
|
842
951
|
this.activeBackend = null;
|
|
843
952
|
}
|
|
844
953
|
/**
|
|
@@ -873,13 +982,11 @@ var AudioAnalyzerAddon = class extends BaseAddon {
|
|
|
873
982
|
if (!this.provider) return;
|
|
874
983
|
const backend = this.resolveAudioBackend();
|
|
875
984
|
if (backend === this.activeBackend) return;
|
|
876
|
-
this.ctx.logger.info("audio backend changed —
|
|
985
|
+
this.ctx.logger.info("audio backend changed — applying to pipeline", { meta: {
|
|
877
986
|
from: this.activeBackend,
|
|
878
987
|
to: backend
|
|
879
988
|
} });
|
|
880
|
-
|
|
881
|
-
await this.provider.swapPipeline(next, backend);
|
|
882
|
-
this.pipeline = next;
|
|
989
|
+
await this.provider.reconfigureBackend(backend);
|
|
883
990
|
this.activeBackend = backend;
|
|
884
991
|
}
|
|
885
992
|
buildDeviceSchema() {
|
|
@@ -3,16 +3,16 @@ Object.defineProperties(exports, {
|
|
|
3
3
|
[Symbol.toStringTag]: { value: "Module" }
|
|
4
4
|
});
|
|
5
5
|
const require_model_download_service_C_IHWnXx = require("../model-download-service-C-IHWnXx-DxM2DSns.js");
|
|
6
|
-
const require_dist = require("../dist-
|
|
6
|
+
const require_dist = require("../dist-BJTPkJFw.js");
|
|
7
7
|
const require_node_topology_platform = require("../node-topology-platform-CFZ7F4xW.js");
|
|
8
8
|
let _camstack_shm_ring = require("@camstack/shm-ring");
|
|
9
|
+
let node_child_process = require("node:child_process");
|
|
9
10
|
let node_fs = require("node:fs");
|
|
10
11
|
node_fs = require_model_download_service_C_IHWnXx.__toESM(node_fs);
|
|
11
12
|
let node_path = require("node:path");
|
|
12
13
|
node_path = require_model_download_service_C_IHWnXx.__toESM(node_path);
|
|
13
14
|
let node_os = require("node:os");
|
|
14
15
|
node_os = require_model_download_service_C_IHWnXx.__toESM(node_os);
|
|
15
|
-
let node_child_process = require("node:child_process");
|
|
16
16
|
let sharp = require("sharp");
|
|
17
17
|
sharp = require_model_download_service_C_IHWnXx.__toESM(sharp);
|
|
18
18
|
//#region src/detection-pipeline/runtimes.ts
|
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
import { t as __require } from "../chunk-BdkLduGY.mjs";
|
|
2
|
-
import { H as createEvent, I as errMsg, K as parseJsonUnknown, L as BaseAddon, O as pipelineExecutorCapability, P as supportedRuntimes$1, S as hfModelUrl, U as hydrateSchema, V as EventCategory, Y as sleep, a as COCO_TO_MACRO, b as detectionPipelineCapability, f as YAMNET_TO_MACRO, i as COCO_80_LABELS, j as runtimeDevices$1, r as AUDIO_MACRO_LABELS, t as APPLE_SA_TO_MACRO, v as defaultDeviceFor$1, x as evaluateZoneRules } from "../dist-
|
|
2
|
+
import { H as createEvent, I as errMsg, K as parseJsonUnknown, L as BaseAddon, O as pipelineExecutorCapability, P as supportedRuntimes$1, S as hfModelUrl, U as hydrateSchema, V as EventCategory, Y as sleep, a as COCO_TO_MACRO, b as detectionPipelineCapability, f as YAMNET_TO_MACRO, i as COCO_80_LABELS, j as runtimeDevices$1, r as AUDIO_MACRO_LABELS, t as APPLE_SA_TO_MACRO, v as defaultDeviceFor$1, x as evaluateZoneRules } from "../dist-v6cKLmU3.mjs";
|
|
3
3
|
import { t as pickNodePlatformArch } from "../node-topology-platform-BkR_k6WT.mjs";
|
|
4
4
|
import { a as isModelDownloaded, i as ensureModel, n as deleteModelFromDisk } from "../model-download-service-C-IHWnXx-BPy6aoAx.mjs";
|
|
5
5
|
import { FrameRingReaderCache } from "@camstack/shm-ring";
|
|
6
|
+
import { spawn } from "node:child_process";
|
|
6
7
|
import * as fs from "node:fs";
|
|
7
8
|
import * as path$1 from "node:path";
|
|
8
9
|
import * as os from "node:os";
|
|
9
|
-
import { spawn } from "node:child_process";
|
|
10
10
|
import sharp from "sharp";
|
|
11
11
|
//#region src/detection-pipeline/runtimes.ts
|
|
12
12
|
var KNOWN_PLATFORMS = [
|