@mihe-dev/js 0.0.0-stage → 0.1.0

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 (91) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +114 -2
  3. package/dist/audio.d.ts +121 -0
  4. package/dist/audio.d.ts.map +1 -0
  5. package/dist/audio.js +377 -0
  6. package/dist/audio.js.map +1 -0
  7. package/dist/chart.d.ts +15 -0
  8. package/dist/chart.d.ts.map +1 -0
  9. package/dist/chart.js +58 -0
  10. package/dist/chart.js.map +1 -0
  11. package/dist/client.d.ts +226 -0
  12. package/dist/client.d.ts.map +1 -0
  13. package/dist/client.js +358 -0
  14. package/dist/client.js.map +1 -0
  15. package/dist/commands.d.ts +70 -0
  16. package/dist/commands.d.ts.map +1 -0
  17. package/dist/commands.js +295 -0
  18. package/dist/commands.js.map +1 -0
  19. package/dist/debug.d.ts +17 -0
  20. package/dist/debug.d.ts.map +1 -0
  21. package/dist/debug.js +29 -0
  22. package/dist/debug.js.map +1 -0
  23. package/dist/declarations.d.ts +160 -0
  24. package/dist/declarations.d.ts.map +1 -0
  25. package/dist/declarations.js +171 -0
  26. package/dist/declarations.js.map +1 -0
  27. package/dist/highlight.d.ts +77 -0
  28. package/dist/highlight.d.ts.map +1 -0
  29. package/dist/highlight.js +233 -0
  30. package/dist/highlight.js.map +1 -0
  31. package/dist/index.d.ts +39 -0
  32. package/dist/index.d.ts.map +1 -0
  33. package/dist/index.js +31 -0
  34. package/dist/index.js.map +1 -0
  35. package/dist/mihe.d.ts +343 -0
  36. package/dist/mihe.d.ts.map +1 -0
  37. package/dist/mihe.iife.min.js +206 -0
  38. package/dist/mihe.iife.min.js.map +7 -0
  39. package/dist/mihe.js +720 -0
  40. package/dist/mihe.js.map +1 -0
  41. package/dist/mihe.min.js +206 -0
  42. package/dist/mihe.min.js.map +7 -0
  43. package/dist/store.d.ts +175 -0
  44. package/dist/store.d.ts.map +1 -0
  45. package/dist/store.js +208 -0
  46. package/dist/store.js.map +1 -0
  47. package/dist/voice.d.ts +192 -0
  48. package/dist/voice.d.ts.map +1 -0
  49. package/dist/voice.js +443 -0
  50. package/dist/voice.js.map +1 -0
  51. package/dist/widget/card.d.ts +15 -0
  52. package/dist/widget/card.d.ts.map +1 -0
  53. package/dist/widget/card.js +28 -0
  54. package/dist/widget/card.js.map +1 -0
  55. package/dist/widget/dom.d.ts +24 -0
  56. package/dist/widget/dom.d.ts.map +1 -0
  57. package/dist/widget/dom.js +49 -0
  58. package/dist/widget/dom.js.map +1 -0
  59. package/dist/widget/element.d.ts +88 -0
  60. package/dist/widget/element.d.ts.map +1 -0
  61. package/dist/widget/element.js +285 -0
  62. package/dist/widget/element.js.map +1 -0
  63. package/dist/widget/format.d.ts +24 -0
  64. package/dist/widget/format.d.ts.map +1 -0
  65. package/dist/widget/format.js +64 -0
  66. package/dist/widget/format.js.map +1 -0
  67. package/dist/widget/icons.d.ts +5 -0
  68. package/dist/widget/icons.d.ts.map +1 -0
  69. package/dist/widget/icons.js +19 -0
  70. package/dist/widget/icons.js.map +1 -0
  71. package/dist/widget/launcher.d.ts +13 -0
  72. package/dist/widget/launcher.d.ts.map +1 -0
  73. package/dist/widget/launcher.js +27 -0
  74. package/dist/widget/launcher.js.map +1 -0
  75. package/dist/widget/mark.d.ts +10 -0
  76. package/dist/widget/mark.d.ts.map +1 -0
  77. package/dist/widget/mark.js +20 -0
  78. package/dist/widget/mark.js.map +1 -0
  79. package/dist/widget/orb.d.ts +91 -0
  80. package/dist/widget/orb.d.ts.map +1 -0
  81. package/dist/widget/orb.js +497 -0
  82. package/dist/widget/orb.js.map +1 -0
  83. package/dist/widget/panel.d.ts +40 -0
  84. package/dist/widget/panel.d.ts.map +1 -0
  85. package/dist/widget/panel.js +239 -0
  86. package/dist/widget/panel.js.map +1 -0
  87. package/dist/widget/styles.d.ts +22 -0
  88. package/dist/widget/styles.d.ts.map +1 -0
  89. package/dist/widget/styles.js +181 -0
  90. package/dist/widget/styles.js.map +1 -0
  91. package/package.json +43 -4
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Neek Digital Solutions Ltd
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md CHANGED
@@ -1,3 +1,115 @@
1
- # Temporary Holding Version
1
+ # @mihe-dev/js
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ Mihe is an assistant you embed in your web app. It knows your screens,
4
+ components and actions, and it operates the app for your users, under your
5
+ own permission model: it finds things, opens screens, fills in and points
6
+ at forms, and runs actions, asking for approval wherever you say it must.
7
+ Users can type or talk to it.
8
+
9
+ This is the framework-free package: plain JavaScript or TypeScript, for Vue,
10
+ Svelte, Angular, server-rendered pages, or no framework at all. For React,
11
+ use `@mihe-dev/react`, which is built on this one.
12
+
13
+ ## Install
14
+
15
+ ```bash
16
+ npm install @mihe-dev/js
17
+ ```
18
+
19
+ The package is ES modules with TypeScript types, and has no dependencies
20
+ beyond `@mihe-dev/protocol`.
21
+
22
+ No build step? It also ships as one self-contained file, for WordPress, a
23
+ server-rendered page or a plain `static/` folder:
24
+
25
+ ```html
26
+ <script src="https://cdn.jsdelivr.net/npm/@mihe-dev/js@0.1.0/dist/mihe.iife.min.js"></script>
27
+ <script>
28
+ const mihe = Mihe.createMihe({ url: "wss://runtime.mihe.dev/v1/ws", publicKey: "pk_live_…", getUserToken });
29
+ Mihe.mountWidget(mihe);
30
+ </script>
31
+ ```
32
+
33
+ `dist/mihe.min.js` is the same as an ES module. Pin the version in the URL,
34
+ or copy the file into your site.
35
+
36
+ ## A first integration
37
+
38
+ ```ts
39
+ import { createMihe } from "@mihe-dev/js";
40
+
41
+ const mihe = createMihe({
42
+ url: "wss://runtime.mihe.dev/v1/ws",
43
+ publicKey: "pk_live_…",
44
+ // Your server mints this token for the signed-in user, signed with your
45
+ // app secret. It carries who they are and what they may do.
46
+ getUserToken: () => fetch("/api/mihe/token").then((r) => r.text()),
47
+ navigate: (route) => { location.hash = route; },
48
+ // Refetch what the page shows after the assistant changes data.
49
+ onActionComplete: () => refresh(),
50
+ });
51
+
52
+ // On each page: tell the assistant where the user is and what they can see.
53
+ // Keep the returned function and call it when they leave.
54
+ const leave = mihe.setScreen({
55
+ screenId: "orders.list",
56
+ route: "/orders",
57
+ title: "Orders",
58
+ visibleData: { pending: 16 },
59
+ });
60
+ ```
61
+
62
+ Every `set…` and `register…` method returns a function that removes what it
63
+ declared. Calling one again with the same id updates the declaration rather
64
+ than adding a second.
65
+
66
+ ```ts
67
+ mihe.registerComponent(button, "orders.new", { type: "button", label: "New order" });
68
+ mihe.registerAction("print_receipt", ({ orderId }) => printReceipt(orderId));
69
+ mihe.registerTable({
70
+ id: "orders.table",
71
+ label: "Orders",
72
+ columns: [{ key: "number", label: "Order" }, { key: "total", label: "Total" }],
73
+ rows: orders,
74
+ rowId: (order) => order.id,
75
+ rowIdName: "orderId",
76
+ });
77
+ mihe.bindTrigger(document.querySelector("#ask")!);
78
+ ```
79
+
80
+ The widget is `<mihe-assistant>`, a web component in its own shadow root,
81
+ so your page's CSS can't reach it:
82
+
83
+ ```ts
84
+ import { mountWidget } from "@mihe-dev/js";
85
+
86
+ mountWidget(mihe); // the orb
87
+ mountWidget(mihe, { form: "classic" }); // launcher and panel
88
+ mountWidget(mihe, { layout: "inline", container: sidebar }); // the panel, in your layout
89
+ ```
90
+
91
+ Theme it with `--mihe-accent`, `--mihe-radius` and `--mihe-font` on the
92
+ `mihe-assistant` element. If your page already has a floating widget in the
93
+ same corner, such as a support chat, lift Mihe above it:
94
+ `mountWidget(mihe, { offset: { bottom: 92 } })`.
95
+
96
+ Or draw your own UI. State is a snapshot you subscribe to:
97
+
98
+ ```ts
99
+ mihe.subscribe(() => render(mihe.getState()));
100
+ mihe.send("How many orders are pending?");
101
+ ```
102
+
103
+ Create an app and its keys in the [dashboard](https://app.mihe.dev), then
104
+ follow the [quickstart](https://mihe.dev/docs/quickstart).
105
+
106
+ ## Documentation
107
+
108
+ - [Quickstart](https://mihe.dev/docs/quickstart)
109
+ - [SDK reference](https://mihe.dev/docs/sdk-reference)
110
+ - [Security](https://mihe.dev/docs/security): what the assistant can and
111
+ cannot do, and why
112
+
113
+ ## License
114
+
115
+ MIT © Neek Digital Solutions Ltd
@@ -0,0 +1,121 @@
1
+ /**
2
+ * Microphone capture and speaker playback for voice sessions.
3
+ *
4
+ * One format everywhere: **16-bit PCM, 24 kHz, mono, little endian**, matching
5
+ * the voice socket and the provider behind it. Nothing on this path transcodes,
6
+ * which is the point — a resampler in a latency-critical loop buys nothing when
7
+ * every participant already agrees on the rate.
8
+ *
9
+ * Two browser realities shape the code:
10
+ *
11
+ * - **iOS Safari will not start audio without a user gesture.** Push-to-talk
12
+ * makes that free: the button press *is* the gesture. `resume()` is called on
13
+ * press rather than at construction for exactly this reason, and calling it
14
+ * from anywhere else will produce a context stuck in `suspended` with no
15
+ * error.
16
+ * - **`AudioContext` may refuse the sample rate we ask for.** Chrome, Firefox,
17
+ * and current Safari honour it; older Safari does not. So capture resamples
18
+ * when it has to rather than assuming, and playback hands the browser a
19
+ * buffer tagged 24 kHz and lets the graph resample it.
20
+ */
21
+ /** Float samples in [-1, 1] to little-endian 16-bit PCM. */
22
+ export declare function toPcm16(samples: Float32Array): ArrayBuffer;
23
+ /** Little-endian 16-bit PCM back to float samples. */
24
+ export declare function fromPcm16(buffer: ArrayBuffer): Float32Array;
25
+ /**
26
+ * How loud a chunk of speech is, from 0 (silence) to 1 (as loud as speech gets).
27
+ *
28
+ * RMS rather than peak, because a peak jumps on every consonant and a ring
29
+ * drawn from it flickers instead of breathing. Scaled by eye: ordinary speech
30
+ * into a laptop microphone sits around 0.05 to 0.15 RMS, so a square root and
31
+ * a gain put a normal voice in the middle of the range rather than at its foot.
32
+ */
33
+ export declare function pcmLevel(pcm: ArrayBuffer): number;
34
+ /**
35
+ * Linear resample, used only when the browser refused our sample rate.
36
+ *
37
+ * Linear rather than anything better on purpose: the input is a voice at
38
+ * 44.1 or 48 kHz going to 24 kHz, the output is fed to a speech model rather
39
+ * than to a listener, and a windowed-sinc filter would cost more than it could
40
+ * possibly be worth here. When the rates already match this is not called at
41
+ * all.
42
+ */
43
+ export declare function resample(samples: Float32Array, fromRate: number, toRate: number): Float32Array;
44
+ /**
45
+ * The microphone, as a stream of PCM frames.
46
+ *
47
+ * Capture runs continuously once started and is *gated* rather than stopped
48
+ * between utterances: `getUserMedia` takes hundreds of milliseconds and shows a
49
+ * permission prompt the first time, so tearing it down on every button release
50
+ * would clip the beginning of the next sentence — which is exactly where the
51
+ * word the model needs most usually is.
52
+ */
53
+ export declare class MicrophoneCapture {
54
+ private readonly onFrame;
55
+ private context;
56
+ private stream;
57
+ private node;
58
+ private source;
59
+ private open;
60
+ constructor(onFrame: (pcm: ArrayBuffer) => void);
61
+ /** True while frames are being delivered. */
62
+ get capturing(): boolean;
63
+ /**
64
+ * Ask for the microphone and build the graph. Must be called from a user
65
+ * gesture on iOS Safari.
66
+ */
67
+ start(): Promise<void>;
68
+ /** Begin delivering frames. */
69
+ unmute(): void;
70
+ /** Stop delivering frames, without releasing the microphone. */
71
+ mute(): void;
72
+ /** Release the device and tear down the graph. */
73
+ stop(): Promise<void>;
74
+ }
75
+ /**
76
+ * The assistant's voice, as a queue of scheduled buffers.
77
+ *
78
+ * Chunks are scheduled back to back against the audio clock rather than played
79
+ * on arrival, because `AudioBufferSourceNode.start()` with no time argument
80
+ * plays *now* — and forty of those a second produce overlapping speech. The
81
+ * running `nextStart` cursor is what turns a stream of chunks into continuous
82
+ * audio.
83
+ */
84
+ export declare class SpeakerQueue {
85
+ private readonly onLevel?;
86
+ /**
87
+ * Called with each chunk's level at the moment it starts playing, not when
88
+ * it arrives. The provider streams seconds ahead of the speaker, so a level
89
+ * taken on arrival would animate words the user has not heard yet.
90
+ */
91
+ constructor(onLevel?: ((level: number) => void) | undefined);
92
+ private levelTimers;
93
+ private context;
94
+ private gain;
95
+ private sources;
96
+ /** Audio-clock time the next chunk should begin at. */
97
+ private nextStart;
98
+ /** Audio-clock time the current run of speech began. */
99
+ private startedAt;
100
+ /** Must be called from a user gesture on iOS Safari. */
101
+ start(): Promise<void>;
102
+ /** Queue a chunk of assistant speech. */
103
+ enqueue(pcm: ArrayBuffer): void;
104
+ /**
105
+ * How much of the current utterance the user has actually heard, in
106
+ * milliseconds.
107
+ *
108
+ * This is the number the runtime needs to truncate the model's history on a
109
+ * barge-in, and it is not the same as how much has been *received*: the
110
+ * provider streams several seconds ahead of the speaker, so trusting bytes
111
+ * delivered would tell the model the user heard a sentence they were
112
+ * interrupted three words into.
113
+ */
114
+ heardMs(): number;
115
+ /** True while anything is scheduled or playing. */
116
+ get speaking(): boolean;
117
+ /** Drop everything queued — an interruption, or the end of a session. */
118
+ flush(): void;
119
+ stop(): Promise<void>;
120
+ }
121
+ //# sourceMappingURL=audio.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"audio.d.ts","sourceRoot":"","sources":["../../../js/src/audio.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;GAmBG;AAmEH,4DAA4D;AAC5D,wBAAgB,OAAO,CAAC,OAAO,EAAE,YAAY,GAAG,WAAW,CAU1D;AAED,sDAAsD;AACtD,wBAAgB,SAAS,CAAC,MAAM,EAAE,WAAW,GAAG,YAAY,CAO3D;AAED;;;;;;;GAOG;AACH,wBAAgB,QAAQ,CAAC,GAAG,EAAE,WAAW,GAAG,MAAM,CAOjD;AAED;;;;;;;;GAQG;AACH,wBAAgB,QAAQ,CACtB,OAAO,EAAE,YAAY,EACrB,QAAQ,EAAE,MAAM,EAChB,MAAM,EAAE,MAAM,GACb,YAAY,CAed;AAED;;;;;;;;GAQG;AACH,qBAAa,iBAAiB;IAOhB,OAAO,CAAC,QAAQ,CAAC,OAAO;IANpC,OAAO,CAAC,OAAO,CAA6B;IAC5C,OAAO,CAAC,MAAM,CAA4B;IAC1C,OAAO,CAAC,IAAI,CAAuD;IACnE,OAAO,CAAC,MAAM,CAA2C;IACzD,OAAO,CAAC,IAAI,CAAS;gBAEQ,OAAO,EAAE,CAAC,GAAG,EAAE,WAAW,KAAK,IAAI;IAEhE,6CAA6C;IAC7C,IAAI,SAAS,IAAI,OAAO,CAEvB;IAED;;;OAGG;IACG,KAAK,IAAI,OAAO,CAAC,IAAI,CAAC;IAwE5B,+BAA+B;IAC/B,MAAM,IAAI,IAAI;IAId,gEAAgE;IAChE,IAAI,IAAI,IAAI;IAIZ,kDAAkD;IAC5C,IAAI,IAAI,OAAO,CAAC,IAAI,CAAC;CAW5B;AAED;;;;;;;;GAQG;AACH,qBAAa,YAAY;IAMX,OAAO,CAAC,QAAQ,CAAC,OAAO,CAAC;IALrC;;;;OAIG;gBAC0B,OAAO,CAAC,GAAE,CAAC,KAAK,EAAE,MAAM,KAAK,IAAI,aAAA;IAE9D,OAAO,CAAC,WAAW,CAA4C;IAC/D,OAAO,CAAC,OAAO,CAA6B;IAC5C,OAAO,CAAC,IAAI,CAAyB;IACrC,OAAO,CAAC,OAAO,CAAoC;IACnD,uDAAuD;IACvD,OAAO,CAAC,SAAS,CAAK;IACtB,wDAAwD;IACxD,OAAO,CAAC,SAAS,CAAK;IAEtB,wDAAwD;IAClD,KAAK,IAAI,OAAO,CAAC,IAAI,CAAC;IAY5B,yCAAyC;IACzC,OAAO,CAAC,GAAG,EAAE,WAAW,GAAG,IAAI;IAgD/B;;;;;;;;;OASG;IACH,OAAO,IAAI,MAAM;IAMjB,mDAAmD;IACnD,IAAI,QAAQ,IAAI,OAAO,CAEtB;IAED,yEAAyE;IACzE,KAAK,IAAI,IAAI;IAmBP,IAAI,IAAI,OAAO,CAAC,IAAI,CAAC;CAO5B"}
package/dist/audio.js ADDED
@@ -0,0 +1,377 @@
1
+ /**
2
+ * Microphone capture and speaker playback for voice sessions.
3
+ *
4
+ * One format everywhere: **16-bit PCM, 24 kHz, mono, little endian**, matching
5
+ * the voice socket and the provider behind it. Nothing on this path transcodes,
6
+ * which is the point — a resampler in a latency-critical loop buys nothing when
7
+ * every participant already agrees on the rate.
8
+ *
9
+ * Two browser realities shape the code:
10
+ *
11
+ * - **iOS Safari will not start audio without a user gesture.** Push-to-talk
12
+ * makes that free: the button press *is* the gesture. `resume()` is called on
13
+ * press rather than at construction for exactly this reason, and calling it
14
+ * from anywhere else will produce a context stuck in `suspended` with no
15
+ * error.
16
+ * - **`AudioContext` may refuse the sample rate we ask for.** Chrome, Firefox,
17
+ * and current Safari honour it; older Safari does not. So capture resamples
18
+ * when it has to rather than assuming, and playback hands the browser a
19
+ * buffer tagged 24 kHz and lets the graph resample it.
20
+ */
21
+ import { VOICE_SAMPLE_RATE } from "@mihe-dev/protocol";
22
+ /**
23
+ * Frames sent up per second. 50 works out to 20 ms per frame — small enough
24
+ * that the end of an utterance is not padded by a buffer waiting to fill, and
25
+ * large enough that the socket is not doing a send per millisecond.
26
+ */
27
+ const FRAME_MS = 20;
28
+ /**
29
+ * Playback runs this far ahead of the speaker.
30
+ *
31
+ * Without it every chunk that arrives a few milliseconds late produces an
32
+ * audible click as the graph runs dry between buffers. With it, network jitter
33
+ * up to this much is inaudible, at the cost of the same delay before the first
34
+ * word. 200 ms is well under the threshold where a reply feels slow.
35
+ */
36
+ const JITTER_BUFFER_MS = 200;
37
+ /** The AudioWorklet processor, injected as a Blob so the package ships one file. */
38
+ const CAPTURE_WORKLET = `
39
+ class MiheCapture extends AudioWorkletProcessor {
40
+ constructor(options) {
41
+ super();
42
+ // A render quantum is 128 samples — about 5 ms at 24 kHz — so posting one
43
+ // per call means roughly 190 messages a second, each becoming its own
44
+ // WebSocket frame and its own JSON envelope upstream. Under push-to-talk
45
+ // that was merely wasteful; with the microphone open continuously it
46
+ // saturated the relay and audio started being dropped, which reads to the
47
+ // user as the assistant simply not answering.
48
+ // Defaulted rather than assumed. An absent option would make \`target\`
49
+ // undefined, every arithmetic result NaN, and the fill loop exit without
50
+ // ever posting — total silence, no error, and nothing in any log to say
51
+ // the microphone had stopped working.
52
+ this.target = options?.processorOptions?.frameSamples || 480;
53
+ this.buffer = new Float32Array(this.target);
54
+ this.filled = 0;
55
+ }
56
+
57
+ process(inputs) {
58
+ const channel = inputs[0] && inputs[0][0];
59
+ // No input yet, or the track ended. Keep the node alive either way:
60
+ // returning false here permanently removes it from the graph, and the
61
+ // user is only between utterances.
62
+ if (!channel) return true;
63
+
64
+ let offset = 0;
65
+ while (offset < channel.length) {
66
+ const take = Math.min(this.target - this.filled, channel.length - offset);
67
+ this.buffer.set(channel.subarray(offset, offset + take), this.filled);
68
+ this.filled += take;
69
+ offset += take;
70
+
71
+ if (this.filled === this.target) {
72
+ this.port.postMessage(this.buffer.slice());
73
+ this.filled = 0;
74
+ }
75
+ }
76
+
77
+ return true;
78
+ }
79
+ }
80
+ registerProcessor('mihe-capture', MiheCapture);
81
+ `;
82
+ /** Float samples in [-1, 1] to little-endian 16-bit PCM. */
83
+ export function toPcm16(samples) {
84
+ const buffer = new ArrayBuffer(samples.length * 2);
85
+ const view = new DataView(buffer);
86
+ for (let i = 0; i < samples.length; i += 1) {
87
+ // Clamped before scaling: a sample above 1 would wrap to a large negative
88
+ // number, which is heard as a loud click rather than as clipping.
89
+ const clamped = Math.max(-1, Math.min(1, samples[i] ?? 0));
90
+ view.setInt16(i * 2, clamped * 0x7fff, true);
91
+ }
92
+ return buffer;
93
+ }
94
+ /** Little-endian 16-bit PCM back to float samples. */
95
+ export function fromPcm16(buffer) {
96
+ const view = new DataView(buffer);
97
+ const samples = new Float32Array(buffer.byteLength / 2);
98
+ for (let i = 0; i < samples.length; i += 1) {
99
+ samples[i] = view.getInt16(i * 2, true) / 0x7fff;
100
+ }
101
+ return samples;
102
+ }
103
+ /**
104
+ * How loud a chunk of speech is, from 0 (silence) to 1 (as loud as speech gets).
105
+ *
106
+ * RMS rather than peak, because a peak jumps on every consonant and a ring
107
+ * drawn from it flickers instead of breathing. Scaled by eye: ordinary speech
108
+ * into a laptop microphone sits around 0.05 to 0.15 RMS, so a square root and
109
+ * a gain put a normal voice in the middle of the range rather than at its foot.
110
+ */
111
+ export function pcmLevel(pcm) {
112
+ const samples = fromPcm16(pcm);
113
+ if (samples.length === 0)
114
+ return 0;
115
+ let sum = 0;
116
+ for (const sample of samples)
117
+ sum += sample * sample;
118
+ const rms = Math.sqrt(sum / samples.length);
119
+ return Math.min(1, Math.sqrt(rms) * 1.8);
120
+ }
121
+ /**
122
+ * Linear resample, used only when the browser refused our sample rate.
123
+ *
124
+ * Linear rather than anything better on purpose: the input is a voice at
125
+ * 44.1 or 48 kHz going to 24 kHz, the output is fed to a speech model rather
126
+ * than to a listener, and a windowed-sinc filter would cost more than it could
127
+ * possibly be worth here. When the rates already match this is not called at
128
+ * all.
129
+ */
130
+ export function resample(samples, fromRate, toRate) {
131
+ if (fromRate === toRate)
132
+ return samples;
133
+ const ratio = fromRate / toRate;
134
+ const length = Math.floor(samples.length / ratio);
135
+ const out = new Float32Array(length);
136
+ for (let i = 0; i < length; i += 1) {
137
+ const position = i * ratio;
138
+ const index = Math.floor(position);
139
+ const next = Math.min(index + 1, samples.length - 1);
140
+ const fraction = position - index;
141
+ out[i] = (samples[index] ?? 0) * (1 - fraction) + (samples[next] ?? 0) * fraction;
142
+ }
143
+ return out;
144
+ }
145
+ /**
146
+ * The microphone, as a stream of PCM frames.
147
+ *
148
+ * Capture runs continuously once started and is *gated* rather than stopped
149
+ * between utterances: `getUserMedia` takes hundreds of milliseconds and shows a
150
+ * permission prompt the first time, so tearing it down on every button release
151
+ * would clip the beginning of the next sentence — which is exactly where the
152
+ * word the model needs most usually is.
153
+ */
154
+ export class MicrophoneCapture {
155
+ onFrame;
156
+ context = null;
157
+ stream = null;
158
+ node = null;
159
+ source = null;
160
+ open = false;
161
+ constructor(onFrame) {
162
+ this.onFrame = onFrame;
163
+ }
164
+ /** True while frames are being delivered. */
165
+ get capturing() {
166
+ return this.open;
167
+ }
168
+ /**
169
+ * Ask for the microphone and build the graph. Must be called from a user
170
+ * gesture on iOS Safari.
171
+ */
172
+ async start() {
173
+ if (this.context) {
174
+ await this.context.resume();
175
+ return;
176
+ }
177
+ this.stream = await navigator.mediaDevices.getUserMedia({
178
+ audio: {
179
+ channelCount: 1,
180
+ // All three on: the assistant's own voice comes out of the same device
181
+ // in a hands-free session, and without cancellation the model hears
182
+ // itself and answers its own last sentence.
183
+ echoCancellation: true,
184
+ noiseSuppression: true,
185
+ autoGainControl: true,
186
+ },
187
+ });
188
+ const context = new AudioContext({ sampleRate: VOICE_SAMPLE_RATE });
189
+ this.context = context;
190
+ this.source = context.createMediaStreamSource(this.stream);
191
+ const deliver = (samples) => {
192
+ if (!this.open)
193
+ return;
194
+ this.onFrame(toPcm16(resample(samples, context.sampleRate, VOICE_SAMPLE_RATE)));
195
+ };
196
+ try {
197
+ const url = URL.createObjectURL(new Blob([CAPTURE_WORKLET], { type: "application/javascript" }));
198
+ await context.audioWorklet.addModule(url);
199
+ URL.revokeObjectURL(url);
200
+ const worklet = new AudioWorkletNode(context, "mihe-capture", {
201
+ processorOptions: {
202
+ // Measured in the context's own samples: the resample to 24 kHz
203
+ // happens after this, on the main thread.
204
+ frameSamples: Math.round((context.sampleRate * FRAME_MS) / 1000),
205
+ },
206
+ });
207
+ worklet.port.onmessage = (event) => deliver(event.data);
208
+ this.source.connect(worklet);
209
+ // Worklets are not pulled unless something downstream asks for their
210
+ // output. A zero-gain node is the standard way to run the graph without
211
+ // routing the microphone to the speaker.
212
+ const sink = context.createGain();
213
+ sink.gain.value = 0;
214
+ worklet.connect(sink).connect(context.destination);
215
+ this.node = worklet;
216
+ }
217
+ catch {
218
+ // Older Safari, and any browser that blocks Blob worklet modules under a
219
+ // strict CSP. Deprecated but universally present, and it runs on the
220
+ // main thread, which for one mono voice track is fine.
221
+ const frames = Math.max(256, 2 ** Math.round(Math.log2((context.sampleRate * FRAME_MS) / 1000)));
222
+ const processor = context.createScriptProcessor(frames, 1, 1);
223
+ processor.onaudioprocess = (event) => deliver(new Float32Array(event.inputBuffer.getChannelData(0)));
224
+ this.source.connect(processor);
225
+ processor.connect(context.destination);
226
+ this.node = processor;
227
+ }
228
+ await context.resume();
229
+ }
230
+ /** Begin delivering frames. */
231
+ unmute() {
232
+ this.open = true;
233
+ }
234
+ /** Stop delivering frames, without releasing the microphone. */
235
+ mute() {
236
+ this.open = false;
237
+ }
238
+ /** Release the device and tear down the graph. */
239
+ async stop() {
240
+ this.open = false;
241
+ this.node?.disconnect();
242
+ this.source?.disconnect();
243
+ this.stream?.getTracks().forEach((track) => track.stop());
244
+ await this.context?.close().catch(() => undefined);
245
+ this.node = null;
246
+ this.source = null;
247
+ this.stream = null;
248
+ this.context = null;
249
+ }
250
+ }
251
+ /**
252
+ * The assistant's voice, as a queue of scheduled buffers.
253
+ *
254
+ * Chunks are scheduled back to back against the audio clock rather than played
255
+ * on arrival, because `AudioBufferSourceNode.start()` with no time argument
256
+ * plays *now* — and forty of those a second produce overlapping speech. The
257
+ * running `nextStart` cursor is what turns a stream of chunks into continuous
258
+ * audio.
259
+ */
260
+ export class SpeakerQueue {
261
+ onLevel;
262
+ /**
263
+ * Called with each chunk's level at the moment it starts playing, not when
264
+ * it arrives. The provider streams seconds ahead of the speaker, so a level
265
+ * taken on arrival would animate words the user has not heard yet.
266
+ */
267
+ constructor(onLevel) {
268
+ this.onLevel = onLevel;
269
+ }
270
+ levelTimers = new Set();
271
+ context = null;
272
+ gain = null;
273
+ sources = new Set();
274
+ /** Audio-clock time the next chunk should begin at. */
275
+ nextStart = 0;
276
+ /** Audio-clock time the current run of speech began. */
277
+ startedAt = 0;
278
+ /** Must be called from a user gesture on iOS Safari. */
279
+ async start() {
280
+ if (this.context) {
281
+ await this.context.resume();
282
+ return;
283
+ }
284
+ const context = new AudioContext({ sampleRate: VOICE_SAMPLE_RATE });
285
+ this.gain = context.createGain();
286
+ this.gain.connect(context.destination);
287
+ this.context = context;
288
+ await context.resume();
289
+ }
290
+ /** Queue a chunk of assistant speech. */
291
+ enqueue(pcm) {
292
+ const context = this.context;
293
+ if (!context || !this.gain)
294
+ return;
295
+ const samples = fromPcm16(pcm);
296
+ if (samples.length === 0)
297
+ return;
298
+ // Tagged 24 kHz regardless of the context's own rate, so a browser that
299
+ // refused our request resamples on playback instead of playing the
300
+ // assistant at the wrong pitch.
301
+ const buffer = context.createBuffer(1, samples.length, VOICE_SAMPLE_RATE);
302
+ buffer.getChannelData(0).set(samples);
303
+ const source = context.createBufferSource();
304
+ source.buffer = buffer;
305
+ source.connect(this.gain);
306
+ const now = context.currentTime;
307
+ if (this.nextStart < now) {
308
+ // Either the first chunk of an utterance or a gap the network opened up.
309
+ // Restarting the cursor ahead of `now` is what the jitter buffer is:
310
+ // scheduling at `now` exactly means anything arriving late is already
311
+ // too late.
312
+ this.nextStart = now + JITTER_BUFFER_MS / 1000;
313
+ this.startedAt = this.nextStart;
314
+ }
315
+ source.start(this.nextStart);
316
+ if (this.onLevel) {
317
+ const level = pcmLevel(pcm);
318
+ const report = this.onLevel;
319
+ const timer = setTimeout(() => {
320
+ this.levelTimers.delete(timer);
321
+ report(level);
322
+ }, Math.max(0, (this.nextStart - now) * 1000));
323
+ this.levelTimers.add(timer);
324
+ }
325
+ this.nextStart += buffer.duration;
326
+ this.sources.add(source);
327
+ source.onended = () => this.sources.delete(source);
328
+ }
329
+ /**
330
+ * How much of the current utterance the user has actually heard, in
331
+ * milliseconds.
332
+ *
333
+ * This is the number the runtime needs to truncate the model's history on a
334
+ * barge-in, and it is not the same as how much has been *received*: the
335
+ * provider streams several seconds ahead of the speaker, so trusting bytes
336
+ * delivered would tell the model the user heard a sentence they were
337
+ * interrupted three words into.
338
+ */
339
+ heardMs() {
340
+ if (!this.context || this.startedAt === 0)
341
+ return 0;
342
+ const heard = this.context.currentTime - this.startedAt;
343
+ return Math.max(0, Math.round(heard * 1000));
344
+ }
345
+ /** True while anything is scheduled or playing. */
346
+ get speaking() {
347
+ return this.sources.size > 0;
348
+ }
349
+ /** Drop everything queued — an interruption, or the end of a session. */
350
+ flush() {
351
+ for (const source of this.sources) {
352
+ try {
353
+ source.stop();
354
+ }
355
+ catch {
356
+ // Already ended. `stop()` on a finished node throws in some browsers.
357
+ }
358
+ }
359
+ this.sources.clear();
360
+ this.nextStart = 0;
361
+ this.startedAt = 0;
362
+ // Levels for audio that will now never play, and a last zero so a ring
363
+ // following them settles instead of freezing mid-word.
364
+ for (const timer of this.levelTimers)
365
+ clearTimeout(timer);
366
+ this.levelTimers.clear();
367
+ this.onLevel?.(0);
368
+ }
369
+ async stop() {
370
+ this.flush();
371
+ this.gain?.disconnect();
372
+ await this.context?.close().catch(() => undefined);
373
+ this.gain = null;
374
+ this.context = null;
375
+ }
376
+ }
377
+ //# sourceMappingURL=audio.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"audio.js","sourceRoot":"","sources":["../../../js/src/audio.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,15 @@
1
+ /**
2
+ * Chart summaries, worked out where the numbers are (amendment 67, step 4).
3
+ *
4
+ * The model explains a trend from these figures, so it is handed the start,
5
+ * end, high, low, total and largest steps rather than a list to do arithmetic
6
+ * over, which models get wrong.
7
+ */
8
+ import type { ChartPoint, SeriesSummary } from "@mihe-dev/protocol";
9
+ export declare function summarise(points: ChartPoint[], ordered: boolean): SeriesSummary | undefined;
10
+ /**
11
+ * At most `limit` points, evenly spaced, keeping both ends. The summary
12
+ * already carries the high and low, which thinning could skip.
13
+ */
14
+ export declare function thin<T>(points: T[], limit: number): T[];
15
+ //# sourceMappingURL=chart.d.ts.map