@absolutejs/voice 0.0.19 → 0.0.21

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 (49) hide show
  1. package/README.md +387 -4
  2. package/dist/angular/index.d.ts +1 -0
  3. package/dist/angular/index.js +669 -3
  4. package/dist/angular/voice-controller.service.d.ts +21 -0
  5. package/dist/audioConditioning.d.ts +3 -0
  6. package/dist/client/actions.d.ts +7 -0
  7. package/dist/client/connection.d.ts +5 -0
  8. package/dist/client/controller.d.ts +2 -0
  9. package/dist/client/htmxBootstrap.js +576 -167
  10. package/dist/client/index.d.ts +1 -0
  11. package/dist/client/index.js +486 -3
  12. package/dist/client/microphone.d.ts +4 -2
  13. package/dist/correction.d.ts +16 -0
  14. package/dist/index.d.ts +4 -0
  15. package/dist/index.js +1314 -283
  16. package/dist/presets.d.ts +13 -0
  17. package/dist/react/index.d.ts +1 -0
  18. package/dist/react/index.js +642 -3
  19. package/dist/react/useVoiceController.d.ts +20 -0
  20. package/dist/react/useVoiceStream.d.ts +1 -0
  21. package/dist/store.d.ts +2 -2
  22. package/dist/svelte/index.d.ts +1 -0
  23. package/dist/svelte/index.js +607 -3
  24. package/dist/testing/benchmark.d.ts +36 -0
  25. package/dist/testing/fixtures.d.ts +1 -0
  26. package/dist/testing/index.d.ts +2 -0
  27. package/dist/testing/index.js +1975 -4
  28. package/dist/testing/resilience.d.ts +20 -0
  29. package/dist/testing/sessionBenchmark.d.ts +126 -0
  30. package/dist/testing/stt.d.ts +1 -0
  31. package/dist/turnDetection.d.ts +5 -1
  32. package/dist/turnProfiles.d.ts +6 -0
  33. package/dist/types.d.ts +198 -8
  34. package/dist/vue/index.d.ts +1 -0
  35. package/dist/vue/index.js +660 -3
  36. package/dist/vue/useVoiceController.d.ts +19 -0
  37. package/fixtures/README.md +24 -0
  38. package/fixtures/manifest.json +127 -0
  39. package/fixtures/pcm/dialogue-three-clean.pcm +0 -0
  40. package/fixtures/pcm/dialogue-three-mixed.pcm +0 -0
  41. package/fixtures/pcm/dialogue-two-clean.pcm +0 -0
  42. package/fixtures/pcm/dialogue-two-noisy.pcm +0 -0
  43. package/fixtures/pcm/multiturn-three-mixed.pcm +0 -0
  44. package/fixtures/pcm/multiturn-two-clean.pcm +0 -0
  45. package/fixtures/pcm/stella-bulgaria-bulgarian20.pcm +0 -0
  46. package/fixtures/pcm/stella-jamaica-jamaican-creole-english1.pcm +0 -0
  47. package/fixtures/pcm/stella-liberia-liberian-pidgin-english2.pcm +0 -0
  48. package/fixtures/pcm/stella-sierra-leone-krio5.pcm +0 -0
  49. package/package.json +25 -1
@@ -130,6 +130,7 @@ var serverMessageToAction = (message) => {
130
130
  case "session":
131
131
  return {
132
132
  sessionId: message.sessionId,
133
+ scenarioId: message.scenarioId,
133
134
  status: message.status,
134
135
  type: "session"
135
136
  };
@@ -150,24 +151,30 @@ var WS_NORMAL_CLOSURE = 1000;
150
151
  var DEFAULT_MAX_RECONNECT_ATTEMPTS = 10;
151
152
  var DEFAULT_PING_INTERVAL = 30000;
152
153
  var RECONNECT_DELAY_MS = 500;
154
+ var DEFAULT_SCENARIO_QUERY_PARAM = "scenarioId";
153
155
  var noop = () => {};
154
156
  var noopUnsubscribe = () => noop;
155
157
  var NOOP_CONNECTION = {
158
+ start: () => {},
156
159
  close: noop,
157
160
  endTurn: noop,
158
161
  getReadyState: () => WS_CLOSED,
162
+ getScenarioId: () => "",
159
163
  getSessionId: () => "",
160
164
  send: noop,
161
165
  sendAudio: noop,
162
166
  subscribe: noopUnsubscribe
163
167
  };
164
168
  var createSessionId = () => crypto.randomUUID();
165
- var buildWsUrl = (path, sessionId) => {
169
+ var buildWsUrl = (path, sessionId, scenarioId) => {
166
170
  const { hostname, port, protocol } = window.location;
167
171
  const wsProtocol = protocol === "https:" ? "wss:" : "ws:";
168
172
  const portSuffix = port ? `:${port}` : "";
169
173
  const url = new URL(`${wsProtocol}//${hostname}${portSuffix}${path}`);
170
174
  url.searchParams.set("sessionId", sessionId);
175
+ if (scenarioId) {
176
+ url.searchParams.set(DEFAULT_SCENARIO_QUERY_PARAM, scenarioId);
177
+ }
171
178
  return url.toString();
172
179
  };
173
180
  var isVoiceServerMessage = (value) => {
@@ -210,6 +217,7 @@ var createVoiceConnection = (path, options = {}) => {
210
217
  const state = {
211
218
  isConnected: false,
212
219
  pendingMessages: [],
220
+ scenarioId: options.scenarioId ?? null,
213
221
  pingInterval: null,
214
222
  reconnectAttempts: 0,
215
223
  reconnectTimeout: null,
@@ -247,13 +255,14 @@ var createVoiceConnection = (path, options = {}) => {
247
255
  }, RECONNECT_DELAY_MS);
248
256
  };
249
257
  const connect = () => {
250
- const ws = new WebSocket(buildWsUrl(path, state.sessionId));
258
+ const ws = new WebSocket(buildWsUrl(path, state.sessionId, state.scenarioId));
251
259
  ws.binaryType = "arraybuffer";
252
260
  ws.onopen = () => {
253
261
  state.isConnected = true;
254
262
  state.reconnectAttempts = 0;
255
263
  flushPendingMessages();
256
264
  listeners.forEach((listener) => listener({
265
+ scenarioId: state.scenarioId ?? undefined,
257
266
  sessionId: state.sessionId,
258
267
  status: "active",
259
268
  type: "session"
@@ -271,6 +280,7 @@ var createVoiceConnection = (path, options = {}) => {
271
280
  }
272
281
  if (parsed.type === "session") {
273
282
  state.sessionId = parsed.sessionId;
283
+ state.scenarioId = parsed.scenarioId ?? state.scenarioId;
274
284
  }
275
285
  listeners.forEach((listener) => listener(parsed));
276
286
  };
@@ -294,6 +304,19 @@ var createVoiceConnection = (path, options = {}) => {
294
304
  const send = (message) => {
295
305
  sendSerialized(JSON.stringify(message));
296
306
  };
307
+ const start = (input = {}) => {
308
+ if (input.sessionId) {
309
+ state.sessionId = input.sessionId;
310
+ }
311
+ if (input.scenarioId) {
312
+ state.scenarioId = input.scenarioId;
313
+ }
314
+ send({
315
+ type: "start",
316
+ sessionId: state.sessionId,
317
+ scenarioId: state.scenarioId ?? undefined
318
+ });
319
+ };
297
320
  const sendAudio = (audio) => {
298
321
  sendSerialized(audio);
299
322
  };
@@ -317,9 +340,11 @@ var createVoiceConnection = (path, options = {}) => {
317
340
  };
318
341
  connect();
319
342
  return {
343
+ start,
320
344
  close,
321
345
  endTurn,
322
346
  getReadyState: () => state.ws?.readyState ?? WS_CLOSED,
347
+ getScenarioId: () => state.scenarioId ?? "",
323
348
  getSessionId: () => state.sessionId,
324
349
  send,
325
350
  sendAudio,
@@ -332,6 +357,7 @@ var createInitialState = () => ({
332
357
  assistantTexts: [],
333
358
  error: null,
334
359
  isConnected: false,
360
+ scenarioId: null,
335
361
  partial: "",
336
362
  sessionId: null,
337
363
  status: "idle",
@@ -393,6 +419,7 @@ var createVoiceStreamStore = () => {
393
419
  state = {
394
420
  ...state,
395
421
  error: null,
422
+ scenarioId: action.scenarioId ?? state.scenarioId,
396
423
  isConnected: action.status === "active",
397
424
  sessionId: action.sessionId,
398
425
  status: action.status
@@ -426,6 +453,12 @@ var createVoiceStream = (path, options = {}) => {
426
453
  const connection = createVoiceConnection(path, options);
427
454
  const store = createVoiceStreamStore();
428
455
  const subscribers = new Set;
456
+ const start = (input) => Promise.resolve().then(() => {
457
+ if (!input?.sessionId && !input?.scenarioId) {
458
+ return;
459
+ }
460
+ connection.start(input);
461
+ });
429
462
  const notify = () => {
430
463
  subscribers.forEach((subscriber) => subscriber());
431
464
  };
@@ -458,6 +491,10 @@ var createVoiceStream = (path, options = {}) => {
458
491
  get isConnected() {
459
492
  return store.getSnapshot().isConnected;
460
493
  },
494
+ get scenarioId() {
495
+ return store.getSnapshot().scenarioId;
496
+ },
497
+ start,
461
498
  get partial() {
462
499
  return store.getSnapshot().partial;
463
500
  },
@@ -533,6 +570,635 @@ VoiceStreamService = __decorateElement(_init, 0, "VoiceStreamService", _dec, Voi
533
570
  __runInitializers(_init, 1, VoiceStreamService);
534
571
  __decoratorMetadata(_init, VoiceStreamService);
535
572
  let _VoiceStreamService = VoiceStreamService;
573
+ // src/angular/voice-controller.service.ts
574
+ import { computed as computed2, Injectable as Injectable2, signal as signal2 } from "@angular/core";
575
+
576
+ // src/client/htmx.ts
577
+ var DEFAULT_EVENT_NAME = "voice-refresh";
578
+ var DEFAULT_QUERY_PARAM = "sessionId";
579
+ var resolveElement = (input) => {
580
+ if (typeof input !== "string") {
581
+ return input;
582
+ }
583
+ return document.querySelector(input);
584
+ };
585
+ var buildRoute = (element, route, queryParam, sessionId) => {
586
+ const baseRoute = route ?? element.getAttribute("hx-get") ?? "";
587
+ if (!baseRoute) {
588
+ return "";
589
+ }
590
+ const url = new URL(baseRoute, window.location.origin);
591
+ if (sessionId) {
592
+ url.searchParams.set(queryParam, sessionId);
593
+ } else {
594
+ url.searchParams.delete(queryParam);
595
+ }
596
+ return `${url.pathname}${url.search}${url.hash}`;
597
+ };
598
+ var bindVoiceHTMX = (stream, options) => {
599
+ if (typeof window === "undefined" || typeof document === "undefined") {
600
+ return () => {};
601
+ }
602
+ const element = resolveElement(options.element);
603
+ if (!element) {
604
+ return () => {};
605
+ }
606
+ const eventName = options.eventName ?? DEFAULT_EVENT_NAME;
607
+ const queryParam = options.sessionQueryParam ?? DEFAULT_QUERY_PARAM;
608
+ const sync = () => {
609
+ const htmxWindow = window;
610
+ const nextRoute = buildRoute(element, options.route, queryParam, stream.sessionId);
611
+ if (nextRoute) {
612
+ element.setAttribute("hx-get", nextRoute);
613
+ }
614
+ htmxWindow.htmx?.process?.(element);
615
+ htmxWindow.htmx?.trigger?.(element, eventName);
616
+ };
617
+ const unsubscribe = stream.subscribe(sync);
618
+ sync();
619
+ return () => {
620
+ unsubscribe();
621
+ };
622
+ };
623
+
624
+ // src/client/microphone.ts
625
+ var clampSample = (value) => Math.max(-1, Math.min(1, value));
626
+ var floatTo16BitPCM = (input) => {
627
+ const output = new Int16Array(input.length);
628
+ for (let index = 0;index < input.length; index += 1) {
629
+ const sample = clampSample(input[index] ?? 0);
630
+ output[index] = sample < 0 ? sample * 32768 : sample * 32767;
631
+ }
632
+ return new Uint8Array(output.buffer);
633
+ };
634
+ var getPcmLevel = (audio) => {
635
+ const bytes = audio instanceof Uint8Array ? audio : new Uint8Array(audio);
636
+ if (bytes.byteLength < 2) {
637
+ return 0;
638
+ }
639
+ const samples = new Int16Array(bytes.buffer, bytes.byteOffset, Math.floor(bytes.byteLength / 2));
640
+ if (samples.length === 0) {
641
+ return 0;
642
+ }
643
+ let sumSquares = 0;
644
+ for (const sample of samples) {
645
+ const normalized = sample / 32768;
646
+ sumSquares += normalized * normalized;
647
+ }
648
+ return Math.min(1, Math.max(0, Math.sqrt(sumSquares / samples.length) * 5.5));
649
+ };
650
+ var downsampleBuffer = (input, sourceRate, targetRate) => {
651
+ if (sourceRate === targetRate) {
652
+ return input;
653
+ }
654
+ const ratio = sourceRate / targetRate;
655
+ const length = Math.round(input.length / ratio);
656
+ const output = new Float32Array(length);
657
+ let offsetResult = 0;
658
+ let offsetBuffer = 0;
659
+ while (offsetResult < output.length) {
660
+ const nextOffsetBuffer = Math.round((offsetResult + 1) * ratio);
661
+ let accum = 0;
662
+ let count = 0;
663
+ for (let index = offsetBuffer;index < nextOffsetBuffer && index < input.length; index += 1) {
664
+ accum += input[index] ?? 0;
665
+ count += 1;
666
+ }
667
+ output[offsetResult] = count > 0 ? accum / count : 0;
668
+ offsetResult += 1;
669
+ offsetBuffer = nextOffsetBuffer;
670
+ }
671
+ return output;
672
+ };
673
+ var createMicrophoneCapture = (options) => {
674
+ let audioContext = null;
675
+ let sourceNode = null;
676
+ let processorNode = null;
677
+ let mediaStream = null;
678
+ const start = async () => {
679
+ if (typeof navigator === "undefined" || !navigator.mediaDevices?.getUserMedia) {
680
+ throw new Error("Browser microphone capture requires navigator.mediaDevices.getUserMedia.");
681
+ }
682
+ const AudioContextCtor = (typeof window !== "undefined" ? window.AudioContext ?? window.webkitAudioContext : undefined) ?? AudioContext;
683
+ if (!AudioContextCtor) {
684
+ throw new Error("Browser microphone capture requires AudioContext support.");
685
+ }
686
+ mediaStream = await navigator.mediaDevices.getUserMedia({
687
+ audio: {
688
+ channelCount: options.channelCount ?? 1
689
+ }
690
+ });
691
+ audioContext = new AudioContextCtor;
692
+ sourceNode = audioContext.createMediaStreamSource(mediaStream);
693
+ processorNode = audioContext.createScriptProcessor(4096, 1, 1);
694
+ processorNode.onaudioprocess = (event) => {
695
+ const channel = event.inputBuffer.getChannelData(0);
696
+ const downsampled = downsampleBuffer(channel, audioContext?.sampleRate ?? 48000, options.sampleRateHz ?? 16000);
697
+ const pcm = floatTo16BitPCM(downsampled);
698
+ options.onLevel?.(getPcmLevel(pcm));
699
+ options.onAudio(pcm);
700
+ };
701
+ sourceNode.connect(processorNode);
702
+ processorNode.connect(audioContext.destination);
703
+ };
704
+ const stop = () => {
705
+ processorNode?.disconnect();
706
+ sourceNode?.disconnect();
707
+ mediaStream?.getTracks().forEach((track) => track.stop());
708
+ audioContext?.close();
709
+ options.onLevel?.(0);
710
+ audioContext = null;
711
+ mediaStream = null;
712
+ processorNode = null;
713
+ sourceNode = null;
714
+ };
715
+ return { start, stop };
716
+ };
717
+
718
+ // src/audioConditioning.ts
719
+ var DEFAULT_TARGET_LEVEL = 0.08;
720
+ var DEFAULT_MAX_GAIN = 3;
721
+ var DEFAULT_NOISE_GATE_THRESHOLD = 0.006;
722
+ var DEFAULT_NOISE_GATE_ATTENUATION = 0.15;
723
+ var toInt16Array = (audio) => {
724
+ if (audio instanceof ArrayBuffer) {
725
+ return new Int16Array(audio, 0, Math.floor(audio.byteLength / 2));
726
+ }
727
+ return new Int16Array(audio.buffer, audio.byteOffset, Math.floor(audio.byteLength / 2));
728
+ };
729
+ var computeRms = (samples) => {
730
+ if (samples.length === 0) {
731
+ return 0;
732
+ }
733
+ let sumSquares = 0;
734
+ for (const sample of samples) {
735
+ const normalized = sample / 32768;
736
+ sumSquares += normalized * normalized;
737
+ }
738
+ return Math.sqrt(sumSquares / samples.length);
739
+ };
740
+ var resolveAudioConditioningConfig = (config) => {
741
+ if (!config || config.enabled === false) {
742
+ return;
743
+ }
744
+ return {
745
+ enabled: true,
746
+ maxGain: config.maxGain ?? DEFAULT_MAX_GAIN,
747
+ noiseGateAttenuation: config.noiseGateAttenuation ?? DEFAULT_NOISE_GATE_ATTENUATION,
748
+ noiseGateThreshold: config.noiseGateThreshold ?? DEFAULT_NOISE_GATE_THRESHOLD,
749
+ targetLevel: config.targetLevel ?? DEFAULT_TARGET_LEVEL
750
+ };
751
+ };
752
+ var conditionAudioChunk = (audio, config) => {
753
+ if (!config) {
754
+ return audio;
755
+ }
756
+ const source = toInt16Array(audio);
757
+ if (source.length === 0) {
758
+ return audio;
759
+ }
760
+ const rms = computeRms(source);
761
+ const output = new Int16Array(source.length);
762
+ const gateFactor = rms < config.noiseGateThreshold ? config.noiseGateAttenuation : 1;
763
+ const baseLevel = Math.max(rms * gateFactor, 0.000001);
764
+ const gain = Math.min(config.maxGain, config.targetLevel / baseLevel);
765
+ const appliedGain = Math.max(0.25, gain) * gateFactor;
766
+ for (let index = 0;index < source.length; index += 1) {
767
+ const next = Math.round(source[index] * appliedGain);
768
+ output[index] = Math.max(-32768, Math.min(32767, next));
769
+ }
770
+ return new Uint8Array(output.buffer);
771
+ };
772
+
773
+ // src/turnProfiles.ts
774
+ var TURN_PROFILE_DEFAULTS = {
775
+ balanced: {
776
+ qualityProfile: "general",
777
+ silenceMs: 1400,
778
+ speechThreshold: 0.012,
779
+ transcriptStabilityMs: 1000
780
+ },
781
+ fast: {
782
+ qualityProfile: "general",
783
+ silenceMs: 700,
784
+ speechThreshold: 0.015,
785
+ transcriptStabilityMs: 450
786
+ },
787
+ "long-form": {
788
+ qualityProfile: "general",
789
+ silenceMs: 2200,
790
+ speechThreshold: 0.01,
791
+ transcriptStabilityMs: 1500
792
+ }
793
+ };
794
+ var QUALITY_PROFILE_DEFAULTS = {
795
+ general: {},
796
+ "accent-heavy": {
797
+ silenceMs: 1200,
798
+ speechThreshold: 0.01,
799
+ transcriptStabilityMs: 1200
800
+ },
801
+ "noisy-room": {
802
+ silenceMs: 2000,
803
+ speechThreshold: 0.02,
804
+ transcriptStabilityMs: 1600
805
+ },
806
+ "short-command": {
807
+ silenceMs: 500,
808
+ speechThreshold: 0.016,
809
+ transcriptStabilityMs: 420
810
+ }
811
+ };
812
+ var DEFAULT_TURN_PROFILE = "fast";
813
+ var DEFAULT_QUALITY_PROFILE = "general";
814
+ var resolveTurnDetectionConfig = (config) => {
815
+ const profile = config?.profile ?? DEFAULT_TURN_PROFILE;
816
+ const qualityProfile = config?.qualityProfile ?? DEFAULT_QUALITY_PROFILE;
817
+ const preset = TURN_PROFILE_DEFAULTS[profile];
818
+ const quality = QUALITY_PROFILE_DEFAULTS[qualityProfile];
819
+ return {
820
+ profile,
821
+ qualityProfile,
822
+ silenceMs: config?.silenceMs ?? quality.silenceMs ?? preset.silenceMs,
823
+ speechThreshold: config?.speechThreshold ?? quality.speechThreshold ?? preset.speechThreshold,
824
+ transcriptStabilityMs: config?.transcriptStabilityMs ?? quality.transcriptStabilityMs ?? preset.transcriptStabilityMs
825
+ };
826
+ };
827
+
828
+ // src/presets.ts
829
+ var PRESET_INPUTS = {
830
+ chat: {
831
+ audioConditioning: {
832
+ enabled: true,
833
+ maxGain: 2.5,
834
+ noiseGateAttenuation: 0,
835
+ noiseGateThreshold: 0.004,
836
+ targetLevel: 0.08
837
+ },
838
+ capture: {
839
+ channelCount: 1,
840
+ sampleRateHz: 16000
841
+ },
842
+ connection: {
843
+ maxReconnectAttempts: 10,
844
+ pingInterval: 30000,
845
+ reconnect: true
846
+ },
847
+ sttLifecycle: "continuous",
848
+ turnDetection: {
849
+ qualityProfile: "short-command",
850
+ profile: "balanced"
851
+ }
852
+ },
853
+ default: {
854
+ capture: {
855
+ channelCount: 1,
856
+ sampleRateHz: 16000
857
+ },
858
+ connection: {
859
+ maxReconnectAttempts: 10,
860
+ pingInterval: 30000,
861
+ reconnect: true
862
+ },
863
+ sttLifecycle: "continuous",
864
+ turnDetection: {
865
+ qualityProfile: "general",
866
+ profile: "fast"
867
+ }
868
+ },
869
+ dictation: {
870
+ audioConditioning: {
871
+ enabled: true,
872
+ maxGain: 2.25,
873
+ noiseGateAttenuation: 0.05,
874
+ noiseGateThreshold: 0.003,
875
+ targetLevel: 0.08
876
+ },
877
+ capture: {
878
+ channelCount: 1,
879
+ sampleRateHz: 16000
880
+ },
881
+ connection: {
882
+ maxReconnectAttempts: 12,
883
+ pingInterval: 30000,
884
+ reconnect: true
885
+ },
886
+ sttLifecycle: "continuous",
887
+ turnDetection: {
888
+ qualityProfile: "accent-heavy",
889
+ profile: "long-form"
890
+ }
891
+ },
892
+ "guided-intake": {
893
+ audioConditioning: {
894
+ enabled: true,
895
+ maxGain: 2.5,
896
+ noiseGateAttenuation: 0,
897
+ noiseGateThreshold: 0.004,
898
+ targetLevel: 0.08
899
+ },
900
+ capture: {
901
+ channelCount: 1,
902
+ sampleRateHz: 16000
903
+ },
904
+ connection: {
905
+ maxReconnectAttempts: 12,
906
+ pingInterval: 30000,
907
+ reconnect: true
908
+ },
909
+ sttLifecycle: "turn-scoped",
910
+ turnDetection: {
911
+ qualityProfile: "accent-heavy",
912
+ profile: "long-form"
913
+ }
914
+ },
915
+ "noisy-room": {
916
+ audioConditioning: {
917
+ enabled: true,
918
+ maxGain: 3,
919
+ noiseGateAttenuation: 0.12,
920
+ noiseGateThreshold: 0.006,
921
+ targetLevel: 0.085
922
+ },
923
+ capture: {
924
+ channelCount: 1,
925
+ sampleRateHz: 16000
926
+ },
927
+ connection: {
928
+ maxReconnectAttempts: 14,
929
+ pingInterval: 45000,
930
+ reconnect: true
931
+ },
932
+ sttLifecycle: "continuous",
933
+ turnDetection: {
934
+ qualityProfile: "noisy-room",
935
+ profile: "long-form",
936
+ silenceMs: 2100,
937
+ speechThreshold: 0.02,
938
+ transcriptStabilityMs: 1650
939
+ }
940
+ },
941
+ reliability: {
942
+ audioConditioning: {
943
+ enabled: true,
944
+ maxGain: 2.9,
945
+ noiseGateAttenuation: 0.08,
946
+ noiseGateThreshold: 0.005,
947
+ targetLevel: 0.08
948
+ },
949
+ capture: {
950
+ channelCount: 1,
951
+ sampleRateHz: 16000
952
+ },
953
+ connection: {
954
+ maxReconnectAttempts: 14,
955
+ pingInterval: 45000,
956
+ reconnect: true
957
+ },
958
+ sttLifecycle: "continuous",
959
+ turnDetection: {
960
+ qualityProfile: "noisy-room",
961
+ profile: "long-form"
962
+ }
963
+ }
964
+ };
965
+ var resolveVoiceRuntimePreset = (name = "default") => {
966
+ const preset = PRESET_INPUTS[name];
967
+ return {
968
+ audioConditioning: resolveAudioConditioningConfig(preset.audioConditioning),
969
+ capture: {
970
+ channelCount: preset.capture?.channelCount ?? 1,
971
+ sampleRateHz: preset.capture?.sampleRateHz ?? 16000
972
+ },
973
+ connection: {
974
+ ...preset.connection
975
+ },
976
+ name,
977
+ sttLifecycle: preset.sttLifecycle ?? "continuous",
978
+ turnDetection: resolveTurnDetectionConfig(preset.turnDetection)
979
+ };
980
+ };
981
+
982
+ // src/client/controller.ts
983
+ var createInitialState2 = (stream) => ({
984
+ assistantTexts: [...stream.assistantTexts],
985
+ error: stream.error,
986
+ isConnected: stream.isConnected,
987
+ isRecording: false,
988
+ partial: stream.partial,
989
+ recordingError: null,
990
+ sessionId: stream.sessionId,
991
+ scenarioId: stream.scenarioId,
992
+ status: stream.status,
993
+ turns: [...stream.turns]
994
+ });
995
+ var createVoiceController = (path, options = {}) => {
996
+ const preset = resolveVoiceRuntimePreset(options.preset);
997
+ const stream = createVoiceStream(path, {
998
+ ...preset.connection,
999
+ ...options.connection
1000
+ });
1001
+ let capture = null;
1002
+ let state = createInitialState2(stream);
1003
+ const subscribers = new Set;
1004
+ const notify = () => {
1005
+ for (const subscriber of subscribers) {
1006
+ subscriber();
1007
+ }
1008
+ };
1009
+ const sync = () => {
1010
+ state = {
1011
+ ...state,
1012
+ assistantTexts: [...stream.assistantTexts],
1013
+ error: stream.error,
1014
+ isConnected: stream.isConnected,
1015
+ partial: stream.partial,
1016
+ sessionId: stream.sessionId,
1017
+ scenarioId: stream.scenarioId,
1018
+ status: stream.status,
1019
+ turns: [...stream.turns]
1020
+ };
1021
+ if (options.autoStopOnComplete !== false && state.status === "completed" && state.isRecording) {
1022
+ capture?.stop();
1023
+ capture = null;
1024
+ state = {
1025
+ ...state,
1026
+ isRecording: false
1027
+ };
1028
+ }
1029
+ notify();
1030
+ };
1031
+ const unsubscribeStream = stream.subscribe(sync);
1032
+ sync();
1033
+ const ensureCapture = () => {
1034
+ if (capture) {
1035
+ return capture;
1036
+ }
1037
+ capture = createMicrophoneCapture({
1038
+ channelCount: options.capture?.channelCount ?? preset.capture.channelCount,
1039
+ onLevel: options.capture?.onLevel,
1040
+ onAudio: (audio) => stream.sendAudio(audio),
1041
+ sampleRateHz: options.capture?.sampleRateHz ?? preset.capture.sampleRateHz
1042
+ });
1043
+ return capture;
1044
+ };
1045
+ const stopRecording = () => {
1046
+ capture?.stop();
1047
+ capture = null;
1048
+ state = {
1049
+ ...state,
1050
+ isRecording: false
1051
+ };
1052
+ notify();
1053
+ };
1054
+ const startRecording = async () => {
1055
+ if (state.isRecording) {
1056
+ return;
1057
+ }
1058
+ try {
1059
+ state = {
1060
+ ...state,
1061
+ recordingError: null
1062
+ };
1063
+ notify();
1064
+ await ensureCapture().start();
1065
+ state = {
1066
+ ...state,
1067
+ isRecording: true
1068
+ };
1069
+ notify();
1070
+ } catch (error) {
1071
+ capture = null;
1072
+ state = {
1073
+ ...state,
1074
+ isRecording: false,
1075
+ recordingError: error instanceof Error ? error.message : String(error)
1076
+ };
1077
+ notify();
1078
+ throw error;
1079
+ }
1080
+ };
1081
+ const close = () => {
1082
+ unsubscribeStream();
1083
+ stopRecording();
1084
+ stream.close();
1085
+ };
1086
+ return {
1087
+ bindHTMX(bindingOptions) {
1088
+ return bindVoiceHTMX(stream, bindingOptions);
1089
+ },
1090
+ close,
1091
+ endTurn: () => stream.endTurn(),
1092
+ get error() {
1093
+ return state.error;
1094
+ },
1095
+ getServerSnapshot: () => state,
1096
+ getSnapshot: () => state,
1097
+ get isConnected() {
1098
+ return state.isConnected;
1099
+ },
1100
+ get isRecording() {
1101
+ return state.isRecording;
1102
+ },
1103
+ get partial() {
1104
+ return state.partial;
1105
+ },
1106
+ get recordingError() {
1107
+ return state.recordingError;
1108
+ },
1109
+ sendAudio: (audio) => stream.sendAudio(audio),
1110
+ get sessionId() {
1111
+ return state.sessionId;
1112
+ },
1113
+ get scenarioId() {
1114
+ return state.scenarioId;
1115
+ },
1116
+ startRecording,
1117
+ get status() {
1118
+ return state.status;
1119
+ },
1120
+ stopRecording,
1121
+ subscribe: (subscriber) => {
1122
+ subscribers.add(subscriber);
1123
+ return () => {
1124
+ subscribers.delete(subscriber);
1125
+ };
1126
+ },
1127
+ toggleRecording: async () => {
1128
+ if (state.isRecording) {
1129
+ stopRecording();
1130
+ return;
1131
+ }
1132
+ await startRecording();
1133
+ },
1134
+ get turns() {
1135
+ return state.turns;
1136
+ },
1137
+ get assistantTexts() {
1138
+ return state.assistantTexts;
1139
+ }
1140
+ };
1141
+ };
1142
+
1143
+ // src/angular/voice-controller.service.ts
1144
+ var _dec = [
1145
+ Injectable2({ providedIn: "root" })
1146
+ ];
1147
+ var _init = __decoratorStart(undefined);
1148
+
1149
+ class VoiceControllerService {
1150
+ connect(path, options = {}) {
1151
+ const controller = createVoiceController(path, options);
1152
+ const assistantTextsSignal = signal2([]);
1153
+ const errorSignal = signal2(null);
1154
+ const isConnectedSignal = signal2(false);
1155
+ const isRecordingSignal = signal2(false);
1156
+ const partialSignal = signal2("");
1157
+ const recordingErrorSignal = signal2(null);
1158
+ const sessionIdSignal = signal2(controller.sessionId);
1159
+ const statusSignal = signal2(controller.status);
1160
+ const turnsSignal = signal2([]);
1161
+ const sync = () => {
1162
+ assistantTextsSignal.set([...controller.assistantTexts]);
1163
+ errorSignal.set(controller.error);
1164
+ isConnectedSignal.set(controller.isConnected);
1165
+ isRecordingSignal.set(controller.isRecording);
1166
+ partialSignal.set(controller.partial);
1167
+ recordingErrorSignal.set(controller.recordingError);
1168
+ sessionIdSignal.set(controller.sessionId);
1169
+ statusSignal.set(controller.status);
1170
+ turnsSignal.set([...controller.turns]);
1171
+ };
1172
+ const unsubscribe = controller.subscribe(sync);
1173
+ sync();
1174
+ return {
1175
+ assistantTexts: computed2(() => assistantTextsSignal()),
1176
+ bindHTMX: controller.bindHTMX,
1177
+ close: () => {
1178
+ unsubscribe();
1179
+ controller.close();
1180
+ },
1181
+ endTurn: () => controller.endTurn(),
1182
+ error: computed2(() => errorSignal()),
1183
+ isConnected: computed2(() => isConnectedSignal()),
1184
+ isRecording: computed2(() => isRecordingSignal()),
1185
+ partial: computed2(() => partialSignal()),
1186
+ recordingError: computed2(() => recordingErrorSignal()),
1187
+ sendAudio: (audio) => controller.sendAudio(audio),
1188
+ sessionId: computed2(() => sessionIdSignal()),
1189
+ startRecording: () => controller.startRecording(),
1190
+ status: computed2(() => statusSignal()),
1191
+ stopRecording: () => controller.stopRecording(),
1192
+ toggleRecording: () => controller.toggleRecording(),
1193
+ turns: computed2(() => turnsSignal())
1194
+ };
1195
+ }
1196
+ }
1197
+ VoiceControllerService = __decorateElement(_init, 0, "VoiceControllerService", _dec, VoiceControllerService);
1198
+ __runInitializers(_init, 1, VoiceControllerService);
1199
+ __decoratorMetadata(_init, VoiceControllerService);
1200
+ let _VoiceControllerService = VoiceControllerService;
536
1201
  export {
537
- VoiceStreamService
1202
+ VoiceStreamService,
1203
+ VoiceControllerService
538
1204
  };