@camstack/addon-pipeline 1.0.8 → 1.1.1

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 (55) hide show
  1. package/dist/audio-analyzer/index.js +104 -29
  2. package/dist/audio-analyzer/index.mjs +100 -25
  3. package/dist/audio-codec-nodeav/index.js +1 -1
  4. package/dist/audio-codec-nodeav/index.mjs +1 -1
  5. package/dist/decoder-nodeav/index.js +1 -1
  6. package/dist/decoder-nodeav/index.mjs +1 -1
  7. package/dist/detection-pipeline/index.js +685 -577
  8. package/dist/detection-pipeline/index.mjs +673 -565
  9. package/dist/{dist-BA6DR_jV.mjs → dist-BWc-HYQz.mjs} +194 -1
  10. package/dist/{dist-BLcTVvol.js → dist-DnD2tm7T.js} +194 -1
  11. package/dist/{model-download-service-C7AjBsX9-B0ekM6dF.mjs → model-download-service-C-IHWnXx-3Mmeob3l.mjs} +36 -6
  12. package/dist/{model-download-service-C7AjBsX9-rXY-VFDk.js → model-download-service-C-IHWnXx-BnQ_awK4.js} +36 -6
  13. package/dist/motion-wasm/index.js +1 -1
  14. package/dist/motion-wasm/index.mjs +1 -1
  15. package/dist/pipeline-runner/index.js +14 -10
  16. package/dist/pipeline-runner/index.mjs +14 -10
  17. package/dist/recorder/index.js +4 -4
  18. package/dist/recorder/index.mjs +2 -2
  19. package/dist/stream-broker/_stub.js +1 -1
  20. package/dist/stream-broker/{_virtual_mf-localSharedImportMap___mfe_internal__addon_stream_broker_widgets-BFy9iszl.mjs → _virtual_mf-localSharedImportMap___mfe_internal__addon_stream_broker_widgets-Do7lgO8N.mjs} +3 -3
  21. package/dist/stream-broker/_virtual_mf___mfe_internal__addon_stream_broker_widgets__loadShare___mf_0_camstack_mf_1_types__loadShare__.js-FRD2eBuz.mjs +26 -0
  22. package/dist/stream-broker/{hostInit-zRy9SzlX.mjs → hostInit-D5y5VMK8.mjs} +3 -3
  23. package/dist/stream-broker/index.js +8 -8
  24. package/dist/stream-broker/index.mjs +2 -2
  25. package/dist/stream-broker/remoteEntry.js +1 -1
  26. package/embed-dist/assets/{MaskShapeCanvas-DI4BY7W2-DJ7ztnFv.js → MaskShapeCanvas-DI4BY7W2-5UPreLSr.js} +1 -1
  27. package/embed-dist/assets/{MotionZonesSettings-NcxxQN8r-CQzEnQoq.js → MotionZonesSettings-NcxxQN8r-Bxqs-CpZ.js} +1 -1
  28. package/embed-dist/assets/{PrivacyMaskSettings-APgPLF7p-Cl0eOy_U.js → PrivacyMaskSettings-APgPLF7p-BDMPeMJd.js} +1 -1
  29. package/embed-dist/assets/{index-CSuLwWK-.js → index-BgGwqHYl.js} +9 -9
  30. package/embed-dist/index.html +1 -1
  31. package/package.json +1 -1
  32. package/python/inference_pool.py +65 -6
  33. package/python/postprocessors/saliency.py +47 -1
  34. package/python/postprocessors/test_saliency.py +23 -0
  35. package/dist/stream-broker/_virtual_mf___mfe_internal__addon_stream_broker_widgets__loadShare___mf_0_camstack_mf_1_types__loadShare__.js-COa17XL2.mjs +0 -26
  36. package/python/__pycache__/inference_pool.cpython-313.pyc +0 -0
  37. package/python/postprocessors/__pycache__/__init__.cpython-312.pyc +0 -0
  38. package/python/postprocessors/__pycache__/__init__.cpython-313.pyc +0 -0
  39. package/python/postprocessors/__pycache__/_safety.cpython-313.pyc +0 -0
  40. package/python/postprocessors/__pycache__/arcface.cpython-312.pyc +0 -0
  41. package/python/postprocessors/__pycache__/arcface.cpython-313.pyc +0 -0
  42. package/python/postprocessors/__pycache__/ctc.cpython-312.pyc +0 -0
  43. package/python/postprocessors/__pycache__/ctc.cpython-313.pyc +0 -0
  44. package/python/postprocessors/__pycache__/saliency.cpython-312.pyc +0 -0
  45. package/python/postprocessors/__pycache__/saliency.cpython-313.pyc +0 -0
  46. package/python/postprocessors/__pycache__/scrfd.cpython-312.pyc +0 -0
  47. package/python/postprocessors/__pycache__/scrfd.cpython-313.pyc +0 -0
  48. package/python/postprocessors/__pycache__/softmax.cpython-312.pyc +0 -0
  49. package/python/postprocessors/__pycache__/softmax.cpython-313.pyc +0 -0
  50. package/python/postprocessors/__pycache__/yamnet.cpython-312.pyc +0 -0
  51. package/python/postprocessors/__pycache__/yamnet.cpython-313.pyc +0 -0
  52. package/python/postprocessors/__pycache__/yolo.cpython-312.pyc +0 -0
  53. package/python/postprocessors/__pycache__/yolo.cpython-313.pyc +0 -0
  54. package/python/postprocessors/__pycache__/yolo_seg.cpython-312.pyc +0 -0
  55. package/python/postprocessors/__pycache__/yolo_seg.cpython-313.pyc +0 -0
@@ -2,12 +2,12 @@ Object.defineProperties(exports, {
2
2
  __esModule: { value: true },
3
3
  [Symbol.toStringTag]: { value: "Module" }
4
4
  });
5
- const require_model_download_service_C7AjBsX9 = require("../model-download-service-C7AjBsX9-rXY-VFDk.js");
6
- const require_dist = require("../dist-BLcTVvol.js");
5
+ const require_model_download_service_C_IHWnXx = require("../model-download-service-C-IHWnXx-BnQ_awK4.js");
6
+ const require_dist = require("../dist-DnD2tm7T.js");
7
7
  let node_fs = require("node:fs");
8
- node_fs = require_model_download_service_C7AjBsX9.__toESM(node_fs);
8
+ node_fs = require_model_download_service_C_IHWnXx.__toESM(node_fs);
9
9
  let node_path = require("node:path");
10
- node_path = require_model_download_service_C7AjBsX9.__toESM(node_path);
10
+ node_path = require_model_download_service_C_IHWnXx.__toESM(node_path);
11
11
  //#region src/audio-analyzer/audio-pipeline.ts
12
12
  /**
13
13
  * Create the appropriate audio pipeline.
@@ -82,7 +82,7 @@ var YamnetPythonPipeline = class {
82
82
  url: YAMNET_MODEL_URL,
83
83
  dest: modelPath
84
84
  } });
85
- await require_model_download_service_C7AjBsX9.downloadFile(YAMNET_MODEL_URL, modelPath);
85
+ await require_model_download_service_C_IHWnXx.downloadFile(YAMNET_MODEL_URL, modelPath);
86
86
  this.log.info("YAMNet ONNX model downloaded", { meta: { sizeBytes: node_fs.statSync(modelPath).size } });
87
87
  }
88
88
  if (!node_fs.existsSync(labelsPath)) {
@@ -90,7 +90,7 @@ var YamnetPythonPipeline = class {
90
90
  url: YAMNET_LABELS_URL,
91
91
  dest: labelsPath
92
92
  } });
93
- await require_model_download_service_C7AjBsX9.downloadFile(YAMNET_LABELS_URL, labelsPath);
93
+ await require_model_download_service_C_IHWnXx.downloadFile(YAMNET_LABELS_URL, labelsPath);
94
94
  }
95
95
  const pythonDir = resolveAudioPythonDir();
96
96
  if (this.installPythonRequirements) await this.installPythonRequirements(node_path.join(pythonDir, "requirements-audio.txt"));
@@ -623,6 +623,20 @@ var AudioAnalyzerProvider = class {
623
623
  async dispose() {
624
624
  await this.pipeline.dispose();
625
625
  }
626
+ /**
627
+ * Swap the live pipeline in place (optimistic backend/model change) — disposes
628
+ * the old pipeline AFTER the new one is wired in, so an in-flight classify
629
+ * never sees a null pipeline. Lets the audio engine settings apply without an
630
+ * addon restart, mirroring the detection-pipeline's in-place engine swap.
631
+ */
632
+ async swapPipeline(next, nextBackend) {
633
+ const prev = this.pipeline;
634
+ this.pipeline = next;
635
+ this.backendName = nextBackend;
636
+ if (prev && prev !== next) try {
637
+ await prev.dispose();
638
+ } catch {}
639
+ }
626
640
  async reprobeAudioEngine() {
627
641
  return this.reprobeImpl();
628
642
  }
@@ -637,6 +651,9 @@ var AudioAnalyzerAddon = class extends require_dist.BaseAddon {
637
651
  id = "audio-analyzer";
638
652
  provider = null;
639
653
  pipeline = null;
654
+ /** Backend the live `pipeline` actually runs — drives the optimistic in-place
655
+ * rebuild in `onConfigChanged` (rebuild only when it genuinely flips). */
656
+ activeBackend = null;
640
657
  constructor() {
641
658
  super(require_dist.DEFAULT_AUDIO_ANALYZER_CONFIG);
642
659
  }
@@ -670,8 +687,7 @@ var AudioAnalyzerAddon = class extends require_dist.BaseAddon {
670
687
  label: o.label
671
688
  })),
672
689
  default: require_dist.DEFAULT_AUDIO_ANALYZER_CONFIG.audioBackend,
673
- immediate: true,
674
- requiresRestart: true
690
+ immediate: true
675
691
  },
676
692
  {
677
693
  type: "select",
@@ -683,8 +699,7 @@ var AudioAnalyzerAddon = class extends require_dist.BaseAddon {
683
699
  label: o.label
684
700
  })),
685
701
  default: require_dist.DEFAULT_AUDIO_ANALYZER_CONFIG.selectedAudioModel,
686
- immediate: true,
687
- requiresRestart: true
702
+ immediate: true
688
703
  }
689
704
  ]
690
705
  }] };
@@ -704,20 +719,24 @@ var AudioAnalyzerAddon = class extends require_dist.BaseAddon {
704
719
  * preview mode (operator typed but didn't save yet) — same
705
720
  * semantics detection-pipeline relies on.
706
721
  */
707
- async getGlobalSettings(overlay) {
722
+ async getGlobalSettings(overlay, nodeId) {
708
723
  const ctx = this.ctxIfReady;
709
724
  const stored = ctx?.settings ? await ctx.settings.readAddonStore() ?? {} : {};
710
725
  const merged = overlay ? {
711
726
  ...stored,
712
727
  ...overlay
713
728
  } : stored;
714
- const operatorChoice = typeof merged.audioBackend === "string" ? merged.audioBackend : require_dist.DEFAULT_AUDIO_ANALYZER_CONFIG.audioBackend;
715
- const effectiveBackend = operatorChoice === "apple-soundanalysis" ? "apple-soundanalysis" : operatorChoice === "yamnet-onnx" ? "yamnet-onnx" : process.platform === "darwin" ? "apple-soundanalysis" : "yamnet-onnx";
716
- const filteredModels = AUDIO_MODEL_OPTIONS.filter((o) => o.value === "" || o.value === effectiveBackend);
729
+ const isDarwin = await this.probeNodePlatform(nodeId) === "darwin";
730
+ const backendOptions = require_dist.AUDIO_BACKEND_CHOICES.filter((o) => o.value !== "apple-soundanalysis" || isDarwin);
731
+ const operatorChoiceRaw = typeof merged.audioBackend === "string" ? merged.audioBackend : require_dist.DEFAULT_AUDIO_ANALYZER_CONFIG.audioBackend;
732
+ const operatorChoice = backendOptions.some((o) => o.value === operatorChoiceRaw) ? operatorChoiceRaw : require_dist.DEFAULT_AUDIO_ANALYZER_CONFIG.audioBackend;
733
+ const effectiveBackend = operatorChoice === "apple-soundanalysis" ? "apple-soundanalysis" : operatorChoice === "yamnet-onnx" ? "yamnet-onnx" : isDarwin ? "apple-soundanalysis" : "yamnet-onnx";
734
+ const filteredModels = AUDIO_MODEL_OPTIONS.filter((o) => (o.value === "" || o.value === effectiveBackend) && (o.value !== "apple-soundanalysis" || isDarwin));
717
735
  const storedModel = typeof merged.selectedAudioModel === "string" ? merged.selectedAudioModel : "";
718
736
  const validModel = filteredModels.find((o) => o.value === storedModel)?.value ?? "";
719
737
  const raw = {
720
738
  ...merged,
739
+ audioBackend: operatorChoice,
721
740
  selectedAudioModel: validModel
722
741
  };
723
742
  const schema = this.globalSettingsSchema();
@@ -726,6 +745,13 @@ var AudioAnalyzerAddon = class extends require_dist.BaseAddon {
726
745
  sections: schema.sections.map((section) => ({
727
746
  ...section,
728
747
  fields: section.fields.map((field) => {
748
+ if (field.type === "select" && field.key === "audioBackend") return {
749
+ ...field,
750
+ options: backendOptions.map((o) => ({
751
+ value: o.value,
752
+ label: o.label
753
+ }))
754
+ };
729
755
  if (field.type === "select" && field.key === "selectedAudioModel") return {
730
756
  ...field,
731
757
  options: filteredModels.map((o) => ({
@@ -739,6 +765,23 @@ var AudioAnalyzerAddon = class extends require_dist.BaseAddon {
739
765
  }, raw);
740
766
  }
741
767
  /**
768
+ * Probe the OS platform of `nodeId` (default: this node) via the
769
+ * `platform-probe` cap — the same source the detection-pipeline uses to gate
770
+ * accelerator backends. Falls back to this host's `process.platform` when the
771
+ * probe is unreachable (cold-start / probe addon absent), so the form stays
772
+ * usable rather than collapsing.
773
+ */
774
+ async probeNodePlatform(nodeId) {
775
+ const api = this.ctxIfReady?.api;
776
+ if (!api) return process.platform;
777
+ const localNode = this.ctxIfReady?.kernel?.localNodeId;
778
+ try {
779
+ return (nodeId && nodeId !== localNode ? await api.platformProbe.getCapabilities.query(void 0, require_dist.nodePin(nodeId)) : await api.platformProbe.getCapabilities.query())?.platform ?? process.platform;
780
+ } catch {
781
+ return process.platform;
782
+ }
783
+ }
784
+ /**
742
785
  * Re-run the platform probe and persist the detected backend into
743
786
  * `probedBestAudioBackend`. Operator `audioBackend` setting is not
744
787
  * touched — only the hint.
@@ -758,23 +801,10 @@ var AudioAnalyzerAddon = class extends require_dist.BaseAddon {
758
801
  }
759
802
  async onInitialize() {
760
803
  const logger = this.ctx.logger;
761
- const modelsDir = await this.ctx.api.storage.resolve.query({
762
- location: "models",
763
- relativePath: ""
764
- }).catch(() => "camstack-data/models");
765
804
  const backend = this.resolveAudioBackend();
766
- logger.info("audio-analyzer: resolving pipeline", { meta: {
767
- operatorChoice: this.config.audioBackend,
768
- effectiveBackend: backend,
769
- selectedModel: this.config.selectedAudioModel || null
770
- } });
771
- const p = await createAudioPipeline(modelsDir, logger, {
772
- backend,
773
- pythonPath: backend === "yamnet-onnx" ? await this.ctx.deps.ensurePython() ?? void 0 : void 0,
774
- installPythonRequirements: (f) => this.ctx.deps.installPythonRequirements(f)
775
- });
776
- await p.initialize();
805
+ const p = await this.buildPipeline(backend);
777
806
  this.pipeline = p;
807
+ this.activeBackend = backend;
778
808
  if (!this.config.probedBestAudioBackend) this.reprobeAudioEngine().catch((err) => {
779
809
  logger.warn("audio: auto-reprobe failed", { meta: { error: err instanceof Error ? err.message : String(err) } });
780
810
  });
@@ -812,6 +842,51 @@ var AudioAnalyzerAddon = class extends require_dist.BaseAddon {
812
842
  this.provider = null;
813
843
  }
814
844
  this.pipeline = null;
845
+ this.activeBackend = null;
846
+ }
847
+ /**
848
+ * Build + initialize an audio pipeline for `backend`. YAMNet runs in the
849
+ * embedded portable Python (resolve interpreter + lazily install reqs); Apple
850
+ * SoundAnalysis needs neither. Shared by `onInitialize` (first boot) and
851
+ * `onConfigChanged` (in-place rebuild).
852
+ */
853
+ async buildPipeline(backend) {
854
+ const logger = this.ctx.logger;
855
+ const modelsDir = await this.ctx.api.storage.resolve.query({
856
+ location: "models",
857
+ relativePath: ""
858
+ }).catch(() => "camstack-data/models");
859
+ logger.info("audio-analyzer: resolving pipeline", { meta: {
860
+ operatorChoice: this.config.audioBackend,
861
+ effectiveBackend: backend,
862
+ selectedModel: this.config.selectedAudioModel || null
863
+ } });
864
+ const p = await createAudioPipeline(modelsDir, logger, {
865
+ backend,
866
+ pythonPath: backend === "yamnet-onnx" ? await this.ctx.deps.ensurePython() ?? void 0 : void 0,
867
+ installPythonRequirements: (f) => this.ctx.deps.installPythonRequirements(f)
868
+ });
869
+ await p.initialize();
870
+ return p;
871
+ }
872
+ /**
873
+ * Optimistic settings apply — rebuild the pipeline IN PLACE when the effective
874
+ * audio backend changes, instead of bouncing the whole addon. The audio engine
875
+ * fields therefore drop `requiresRestart`: BaseAddon calls this after every
876
+ * global-settings write; we rebuild only when the backend genuinely flips.
877
+ */
878
+ async onConfigChanged() {
879
+ if (!this.provider) return;
880
+ const backend = this.resolveAudioBackend();
881
+ if (backend === this.activeBackend) return;
882
+ this.ctx.logger.info("audio backend changed — rebuilding pipeline in place", { meta: {
883
+ from: this.activeBackend,
884
+ to: backend
885
+ } });
886
+ const next = await this.buildPipeline(backend);
887
+ await this.provider.swapPipeline(next, backend);
888
+ this.pipeline = next;
889
+ this.activeBackend = backend;
815
890
  }
816
891
  buildDeviceSchema() {
817
892
  return { sections: [] };
@@ -1,6 +1,6 @@
1
1
  import { t as __require } from "../chunk-BdkLduGY.mjs";
2
- import { B as BaseAddon, E as mapAudioLabelToMacro, J as hydrateSchema, g as audioAnalyzerCapability, h as audioAnalysisCapability, l as HF_BASE_URL, n as AUDIO_BACKEND_CHOICES, o as DEFAULT_AUDIO_ANALYZER_CONFIG, z as errMsg } from "../dist-BA6DR_jV.mjs";
3
- import { r as downloadFile } from "../model-download-service-C7AjBsX9-B0ekM6dF.mjs";
2
+ import { A as nodePin, B as BaseAddon, E as mapAudioLabelToMacro, J as hydrateSchema, g as audioAnalyzerCapability, h as audioAnalysisCapability, l as HF_BASE_URL, n as AUDIO_BACKEND_CHOICES, o as DEFAULT_AUDIO_ANALYZER_CONFIG, z as errMsg } from "../dist-BWc-HYQz.mjs";
3
+ import { r as downloadFile } from "../model-download-service-C-IHWnXx-3Mmeob3l.mjs";
4
4
  import * as fs from "node:fs";
5
5
  import * as path$1 from "node:path";
6
6
  //#region src/audio-analyzer/audio-pipeline.ts
@@ -618,6 +618,20 @@ var AudioAnalyzerProvider = class {
618
618
  async dispose() {
619
619
  await this.pipeline.dispose();
620
620
  }
621
+ /**
622
+ * Swap the live pipeline in place (optimistic backend/model change) — disposes
623
+ * the old pipeline AFTER the new one is wired in, so an in-flight classify
624
+ * never sees a null pipeline. Lets the audio engine settings apply without an
625
+ * addon restart, mirroring the detection-pipeline's in-place engine swap.
626
+ */
627
+ async swapPipeline(next, nextBackend) {
628
+ const prev = this.pipeline;
629
+ this.pipeline = next;
630
+ this.backendName = nextBackend;
631
+ if (prev && prev !== next) try {
632
+ await prev.dispose();
633
+ } catch {}
634
+ }
621
635
  async reprobeAudioEngine() {
622
636
  return this.reprobeImpl();
623
637
  }
@@ -632,6 +646,9 @@ var AudioAnalyzerAddon = class extends BaseAddon {
632
646
  id = "audio-analyzer";
633
647
  provider = null;
634
648
  pipeline = null;
649
+ /** Backend the live `pipeline` actually runs — drives the optimistic in-place
650
+ * rebuild in `onConfigChanged` (rebuild only when it genuinely flips). */
651
+ activeBackend = null;
635
652
  constructor() {
636
653
  super(DEFAULT_AUDIO_ANALYZER_CONFIG);
637
654
  }
@@ -665,8 +682,7 @@ var AudioAnalyzerAddon = class extends BaseAddon {
665
682
  label: o.label
666
683
  })),
667
684
  default: DEFAULT_AUDIO_ANALYZER_CONFIG.audioBackend,
668
- immediate: true,
669
- requiresRestart: true
685
+ immediate: true
670
686
  },
671
687
  {
672
688
  type: "select",
@@ -678,8 +694,7 @@ var AudioAnalyzerAddon = class extends BaseAddon {
678
694
  label: o.label
679
695
  })),
680
696
  default: DEFAULT_AUDIO_ANALYZER_CONFIG.selectedAudioModel,
681
- immediate: true,
682
- requiresRestart: true
697
+ immediate: true
683
698
  }
684
699
  ]
685
700
  }] };
@@ -699,20 +714,24 @@ var AudioAnalyzerAddon = class extends BaseAddon {
699
714
  * preview mode (operator typed but didn't save yet) — same
700
715
  * semantics detection-pipeline relies on.
701
716
  */
702
- async getGlobalSettings(overlay) {
717
+ async getGlobalSettings(overlay, nodeId) {
703
718
  const ctx = this.ctxIfReady;
704
719
  const stored = ctx?.settings ? await ctx.settings.readAddonStore() ?? {} : {};
705
720
  const merged = overlay ? {
706
721
  ...stored,
707
722
  ...overlay
708
723
  } : stored;
709
- const operatorChoice = typeof merged.audioBackend === "string" ? merged.audioBackend : DEFAULT_AUDIO_ANALYZER_CONFIG.audioBackend;
710
- const effectiveBackend = operatorChoice === "apple-soundanalysis" ? "apple-soundanalysis" : operatorChoice === "yamnet-onnx" ? "yamnet-onnx" : process.platform === "darwin" ? "apple-soundanalysis" : "yamnet-onnx";
711
- const filteredModels = AUDIO_MODEL_OPTIONS.filter((o) => o.value === "" || o.value === effectiveBackend);
724
+ const isDarwin = await this.probeNodePlatform(nodeId) === "darwin";
725
+ const backendOptions = AUDIO_BACKEND_CHOICES.filter((o) => o.value !== "apple-soundanalysis" || isDarwin);
726
+ const operatorChoiceRaw = typeof merged.audioBackend === "string" ? merged.audioBackend : DEFAULT_AUDIO_ANALYZER_CONFIG.audioBackend;
727
+ const operatorChoice = backendOptions.some((o) => o.value === operatorChoiceRaw) ? operatorChoiceRaw : DEFAULT_AUDIO_ANALYZER_CONFIG.audioBackend;
728
+ const effectiveBackend = operatorChoice === "apple-soundanalysis" ? "apple-soundanalysis" : operatorChoice === "yamnet-onnx" ? "yamnet-onnx" : isDarwin ? "apple-soundanalysis" : "yamnet-onnx";
729
+ const filteredModels = AUDIO_MODEL_OPTIONS.filter((o) => (o.value === "" || o.value === effectiveBackend) && (o.value !== "apple-soundanalysis" || isDarwin));
712
730
  const storedModel = typeof merged.selectedAudioModel === "string" ? merged.selectedAudioModel : "";
713
731
  const validModel = filteredModels.find((o) => o.value === storedModel)?.value ?? "";
714
732
  const raw = {
715
733
  ...merged,
734
+ audioBackend: operatorChoice,
716
735
  selectedAudioModel: validModel
717
736
  };
718
737
  const schema = this.globalSettingsSchema();
@@ -721,6 +740,13 @@ var AudioAnalyzerAddon = class extends BaseAddon {
721
740
  sections: schema.sections.map((section) => ({
722
741
  ...section,
723
742
  fields: section.fields.map((field) => {
743
+ if (field.type === "select" && field.key === "audioBackend") return {
744
+ ...field,
745
+ options: backendOptions.map((o) => ({
746
+ value: o.value,
747
+ label: o.label
748
+ }))
749
+ };
724
750
  if (field.type === "select" && field.key === "selectedAudioModel") return {
725
751
  ...field,
726
752
  options: filteredModels.map((o) => ({
@@ -734,6 +760,23 @@ var AudioAnalyzerAddon = class extends BaseAddon {
734
760
  }, raw);
735
761
  }
736
762
  /**
763
+ * Probe the OS platform of `nodeId` (default: this node) via the
764
+ * `platform-probe` cap — the same source the detection-pipeline uses to gate
765
+ * accelerator backends. Falls back to this host's `process.platform` when the
766
+ * probe is unreachable (cold-start / probe addon absent), so the form stays
767
+ * usable rather than collapsing.
768
+ */
769
+ async probeNodePlatform(nodeId) {
770
+ const api = this.ctxIfReady?.api;
771
+ if (!api) return process.platform;
772
+ const localNode = this.ctxIfReady?.kernel?.localNodeId;
773
+ try {
774
+ return (nodeId && nodeId !== localNode ? await api.platformProbe.getCapabilities.query(void 0, nodePin(nodeId)) : await api.platformProbe.getCapabilities.query())?.platform ?? process.platform;
775
+ } catch {
776
+ return process.platform;
777
+ }
778
+ }
779
+ /**
737
780
  * Re-run the platform probe and persist the detected backend into
738
781
  * `probedBestAudioBackend`. Operator `audioBackend` setting is not
739
782
  * touched — only the hint.
@@ -753,23 +796,10 @@ var AudioAnalyzerAddon = class extends BaseAddon {
753
796
  }
754
797
  async onInitialize() {
755
798
  const logger = this.ctx.logger;
756
- const modelsDir = await this.ctx.api.storage.resolve.query({
757
- location: "models",
758
- relativePath: ""
759
- }).catch(() => "camstack-data/models");
760
799
  const backend = this.resolveAudioBackend();
761
- logger.info("audio-analyzer: resolving pipeline", { meta: {
762
- operatorChoice: this.config.audioBackend,
763
- effectiveBackend: backend,
764
- selectedModel: this.config.selectedAudioModel || null
765
- } });
766
- const p = await createAudioPipeline(modelsDir, logger, {
767
- backend,
768
- pythonPath: backend === "yamnet-onnx" ? await this.ctx.deps.ensurePython() ?? void 0 : void 0,
769
- installPythonRequirements: (f) => this.ctx.deps.installPythonRequirements(f)
770
- });
771
- await p.initialize();
800
+ const p = await this.buildPipeline(backend);
772
801
  this.pipeline = p;
802
+ this.activeBackend = backend;
773
803
  if (!this.config.probedBestAudioBackend) this.reprobeAudioEngine().catch((err) => {
774
804
  logger.warn("audio: auto-reprobe failed", { meta: { error: err instanceof Error ? err.message : String(err) } });
775
805
  });
@@ -807,6 +837,51 @@ var AudioAnalyzerAddon = class extends BaseAddon {
807
837
  this.provider = null;
808
838
  }
809
839
  this.pipeline = null;
840
+ this.activeBackend = null;
841
+ }
842
+ /**
843
+ * Build + initialize an audio pipeline for `backend`. YAMNet runs in the
844
+ * embedded portable Python (resolve interpreter + lazily install reqs); Apple
845
+ * SoundAnalysis needs neither. Shared by `onInitialize` (first boot) and
846
+ * `onConfigChanged` (in-place rebuild).
847
+ */
848
+ async buildPipeline(backend) {
849
+ const logger = this.ctx.logger;
850
+ const modelsDir = await this.ctx.api.storage.resolve.query({
851
+ location: "models",
852
+ relativePath: ""
853
+ }).catch(() => "camstack-data/models");
854
+ logger.info("audio-analyzer: resolving pipeline", { meta: {
855
+ operatorChoice: this.config.audioBackend,
856
+ effectiveBackend: backend,
857
+ selectedModel: this.config.selectedAudioModel || null
858
+ } });
859
+ const p = await createAudioPipeline(modelsDir, logger, {
860
+ backend,
861
+ pythonPath: backend === "yamnet-onnx" ? await this.ctx.deps.ensurePython() ?? void 0 : void 0,
862
+ installPythonRequirements: (f) => this.ctx.deps.installPythonRequirements(f)
863
+ });
864
+ await p.initialize();
865
+ return p;
866
+ }
867
+ /**
868
+ * Optimistic settings apply — rebuild the pipeline IN PLACE when the effective
869
+ * audio backend changes, instead of bouncing the whole addon. The audio engine
870
+ * fields therefore drop `requiresRestart`: BaseAddon calls this after every
871
+ * global-settings write; we rebuild only when the backend genuinely flips.
872
+ */
873
+ async onConfigChanged() {
874
+ if (!this.provider) return;
875
+ const backend = this.resolveAudioBackend();
876
+ if (backend === this.activeBackend) return;
877
+ this.ctx.logger.info("audio backend changed — rebuilding pipeline in place", { meta: {
878
+ from: this.activeBackend,
879
+ to: backend
880
+ } });
881
+ const next = await this.buildPipeline(backend);
882
+ await this.provider.swapPipeline(next, backend);
883
+ this.pipeline = next;
884
+ this.activeBackend = backend;
810
885
  }
811
886
  buildDeviceSchema() {
812
887
  return { sections: [] };
@@ -2,7 +2,7 @@ Object.defineProperties(exports, {
2
2
  __esModule: { value: true },
3
3
  [Symbol.toStringTag]: { value: "Module" }
4
4
  });
5
- const require_dist = require("../dist-BLcTVvol.js");
5
+ const require_dist = require("../dist-DnD2tm7T.js");
6
6
  const require_codec_runtime = require("../codec-runtime-BOk-13PN.js");
7
7
  let node_crypto = require("node:crypto");
8
8
  //#region src/audio-codec-nodeav/addon/index.ts
@@ -1,4 +1,4 @@
1
- import { B as BaseAddon, _ as audioCodecCapability, z as errMsg } from "../dist-BA6DR_jV.mjs";
1
+ import { B as BaseAddon, _ as audioCodecCapability, z as errMsg } from "../dist-BWc-HYQz.mjs";
2
2
  import { t as DecodeRuntime } from "../codec-runtime-BsqlEjPi.mjs";
3
3
  import { randomUUID } from "node:crypto";
4
4
  //#region src/audio-codec-nodeav/addon/index.ts
@@ -2,7 +2,7 @@ Object.defineProperties(exports, {
2
2
  __esModule: { value: true },
3
3
  [Symbol.toStringTag]: { value: "Module" }
4
4
  });
5
- const require_dist = require("../dist-BLcTVvol.js");
5
+ const require_dist = require("../dist-DnD2tm7T.js");
6
6
  let _camstack_shm_ring = require("@camstack/shm-ring");
7
7
  let node_crypto = require("node:crypto");
8
8
  //#region src/decoder-nodeav/frame-ring-sink.ts
@@ -1,4 +1,4 @@
1
- import { B as BaseAddon, b as decoderCapability, f as RingBuffer, s as DEFAULT_DECODER_HWACCEL_CONFIG, u as HWACCEL_OPTIONS, z as errMsg } from "../dist-BA6DR_jV.mjs";
1
+ import { B as BaseAddon, b as decoderCapability, f as RingBuffer, s as DEFAULT_DECODER_HWACCEL_CONFIG, u as HWACCEL_OPTIONS, z as errMsg } from "../dist-BWc-HYQz.mjs";
2
2
  import { FrameRingReaderCache, FrameRingWriter, MIN_RING_SLOTS, computeSegmentSize, computeSlotByteLength, createSegment, deriveSlotCount } from "@camstack/shm-ring";
3
3
  import { randomUUID } from "node:crypto";
4
4
  //#region src/decoder-nodeav/frame-ring-sink.ts