@camstack/addon-pipeline 1.1.39 → 1.1.40

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (38) hide show
  1. package/dist/audio-analyzer/index.js +168 -61
  2. package/dist/audio-analyzer/index.mjs +168 -61
  3. package/dist/detection-pipeline/index.js +2 -2
  4. package/dist/detection-pipeline/index.mjs +2 -2
  5. package/dist/{dist-DbGdZ8Nr.js → dist-BJTPkJFw.js} +247 -3
  6. package/dist/{dist-DvvYzO58.mjs → dist-v6cKLmU3.mjs} +247 -3
  7. package/dist/{frame-handle-plane-B7jMIZc2.js → frame-handle-plane-BP8YV4sF.js} +54 -5
  8. package/dist/{frame-handle-plane-BFzoIfkd.mjs → frame-handle-plane-DKAXTtfn.mjs} +54 -5
  9. package/dist/motion-wasm/index.js +1 -1
  10. package/dist/motion-wasm/index.mjs +1 -1
  11. package/dist/pipeline-runner/index.js +893 -12
  12. package/dist/pipeline-runner/index.mjs +892 -13
  13. package/dist/recorder/index.js +2 -2
  14. package/dist/recorder/index.mjs +2 -2
  15. package/dist/session-decode/decode-worker-child.js +511 -0
  16. package/dist/session-decode/decode-worker-child.mjs +510 -0
  17. package/dist/stream-broker/_stub.js +44 -44
  18. package/dist/stream-broker/{_virtual_mf-localSharedImportMap___mfe_internal__addon_stream_broker_widgets-DezFy4Fu.mjs → _virtual_mf-localSharedImportMap___mfe_internal__addon_stream_broker_widgets-6I7-nufj.mjs} +3 -3
  19. 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
  20. 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
  21. package/dist/stream-broker/{hostInit-BOf0hg9q.mjs → hostInit-CUkFGqiI.mjs} +3 -3
  22. package/dist/stream-broker/index.js +268 -38
  23. package/dist/stream-broker/index.mjs +268 -38
  24. package/dist/stream-broker/remoteEntry.js +1 -1
  25. package/dist/worker-protocol-BCfO8gUF.js +67 -0
  26. package/dist/worker-protocol-pk7qdYXt.mjs +56 -0
  27. package/embed-dist/assets/{MaskShapeCanvas-DI4BY7W2-Br2fqwe7.js → MaskShapeCanvas-DI4BY7W2-Cr_fCzY5.js} +1 -1
  28. package/embed-dist/assets/{MotionZonesSettings-NcxxQN8r-DYXRf30g.js → MotionZonesSettings-NcxxQN8r-COcvoHDH.js} +1 -1
  29. package/embed-dist/assets/{PrivacyMaskSettings-APgPLF7p-DLpqLgPo.js → PrivacyMaskSettings-APgPLF7p-Bv-bbyJl.js} +1 -1
  30. package/embed-dist/assets/index-Ctbq9AXh.css +2 -0
  31. package/embed-dist/assets/index-jMj5PyJk.js +81 -0
  32. package/embed-dist/index.html +2 -2
  33. package/package.json +6 -3
  34. package/python/inference_pool.py +3 -1
  35. 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
  36. 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
  37. package/embed-dist/assets/index-C5UpuPr8.css +0 -2
  38. 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-DbGdZ8Nr.js");
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
- pipeline;
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
- constructor(logger, pipeline, backendName, deviceSettingsResolver, deviceContributionResolver, deviceSettingsPatcher, reprobeImpl) {
455
- this.pipeline = pipeline;
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 = 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
- const f32Data = float32FromBytes(chunk.data);
572
- const result = await this.pipeline.classify({
573
- data: f32Data,
574
- sampleRate: chunk.sampleRate,
575
- channels: chunk.channels
576
- }).finally(() => {
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.pipeline !== null;
711
+ return !this.disposed;
623
712
  }
624
713
  async dispose() {
625
- await this.pipeline.dispose();
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 = null;
655
- /** Backend the live `pipeline` actually runs drives the optimistic in-place
656
- * rebuild in `onConfigChanged` (rebuild only when it genuinely flips). */
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.pipeline, backend, deviceSettingsResolver, deviceContributionResolver, deviceSettingsPatcher, () => this.reprobeAudioEngine());
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 — rebuilding pipeline in place", { meta: {
990
+ this.ctx.logger.info("audio backend changed — applying to pipeline", { meta: {
882
991
  from: this.activeBackend,
883
992
  to: backend
884
993
  } });
885
- const next = await this.buildPipeline(backend);
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-DvvYzO58.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-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
- pipeline;
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
- constructor(logger, pipeline, backendName, deviceSettingsResolver, deviceContributionResolver, deviceSettingsPatcher, reprobeImpl) {
450
- this.pipeline = pipeline;
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 = 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
- const f32Data = float32FromBytes(chunk.data);
567
- const result = await this.pipeline.classify({
568
- data: f32Data,
569
- sampleRate: chunk.sampleRate,
570
- channels: chunk.channels
571
- }).finally(() => {
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.pipeline !== null;
706
+ return !this.disposed;
618
707
  }
619
708
  async dispose() {
620
- await this.pipeline.dispose();
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 = null;
650
- /** Backend the live `pipeline` actually runs drives the optimistic in-place
651
- * rebuild in `onConfigChanged` (rebuild only when it genuinely flips). */
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.pipeline, backend, deviceSettingsResolver, deviceContributionResolver, deviceSettingsPatcher, () => this.reprobeAudioEngine());
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 — rebuilding pipeline in place", { meta: {
985
+ this.ctx.logger.info("audio backend changed — applying to pipeline", { meta: {
877
986
  from: this.activeBackend,
878
987
  to: backend
879
988
  } });
880
- const next = await this.buildPipeline(backend);
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-DbGdZ8Nr.js");
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-DvvYzO58.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-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 = [