@mihe-dev/js 0.0.0-stage → 0.2.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.
- package/LICENSE +21 -0
- package/README.md +114 -2
- package/dist/audio.d.ts +121 -0
- package/dist/audio.d.ts.map +1 -0
- package/dist/audio.js +377 -0
- package/dist/audio.js.map +1 -0
- package/dist/card-view.d.ts +79 -0
- package/dist/card-view.d.ts.map +1 -0
- package/dist/card-view.js +99 -0
- package/dist/card-view.js.map +1 -0
- package/dist/chart.d.ts +15 -0
- package/dist/chart.d.ts.map +1 -0
- package/dist/chart.js +58 -0
- package/dist/chart.js.map +1 -0
- package/dist/client.d.ts +226 -0
- package/dist/client.d.ts.map +1 -0
- package/dist/client.js +358 -0
- package/dist/client.js.map +1 -0
- package/dist/commands.d.ts +70 -0
- package/dist/commands.d.ts.map +1 -0
- package/dist/commands.js +295 -0
- package/dist/commands.js.map +1 -0
- package/dist/debug.d.ts +17 -0
- package/dist/debug.d.ts.map +1 -0
- package/dist/debug.js +29 -0
- package/dist/debug.js.map +1 -0
- package/dist/declarations.d.ts +160 -0
- package/dist/declarations.d.ts.map +1 -0
- package/dist/declarations.js +171 -0
- package/dist/declarations.js.map +1 -0
- package/dist/highlight.d.ts +77 -0
- package/dist/highlight.d.ts.map +1 -0
- package/dist/highlight.js +233 -0
- package/dist/highlight.js.map +1 -0
- package/dist/index.d.ts +41 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +32 -0
- package/dist/index.js.map +1 -0
- package/dist/mihe.d.ts +364 -0
- package/dist/mihe.d.ts.map +1 -0
- package/dist/mihe.iife.min.js +217 -0
- package/dist/mihe.iife.min.js.map +7 -0
- package/dist/mihe.js +758 -0
- package/dist/mihe.js.map +1 -0
- package/dist/mihe.min.js +217 -0
- package/dist/mihe.min.js.map +7 -0
- package/dist/store.d.ts +175 -0
- package/dist/store.d.ts.map +1 -0
- package/dist/store.js +208 -0
- package/dist/store.js.map +1 -0
- package/dist/voice.d.ts +192 -0
- package/dist/voice.d.ts.map +1 -0
- package/dist/voice.js +443 -0
- package/dist/voice.js.map +1 -0
- package/dist/widget/card.d.ts +17 -0
- package/dist/widget/card.d.ts.map +1 -0
- package/dist/widget/card.js +31 -0
- package/dist/widget/card.js.map +1 -0
- package/dist/widget/dom.d.ts +24 -0
- package/dist/widget/dom.d.ts.map +1 -0
- package/dist/widget/dom.js +49 -0
- package/dist/widget/dom.js.map +1 -0
- package/dist/widget/element.d.ts +88 -0
- package/dist/widget/element.d.ts.map +1 -0
- package/dist/widget/element.js +285 -0
- package/dist/widget/element.js.map +1 -0
- package/dist/widget/format.d.ts +24 -0
- package/dist/widget/format.d.ts.map +1 -0
- package/dist/widget/format.js +64 -0
- package/dist/widget/format.js.map +1 -0
- package/dist/widget/icons.d.ts +5 -0
- package/dist/widget/icons.d.ts.map +1 -0
- package/dist/widget/icons.js +19 -0
- package/dist/widget/icons.js.map +1 -0
- package/dist/widget/launcher.d.ts +13 -0
- package/dist/widget/launcher.d.ts.map +1 -0
- package/dist/widget/launcher.js +27 -0
- package/dist/widget/launcher.js.map +1 -0
- package/dist/widget/mark.d.ts +10 -0
- package/dist/widget/mark.d.ts.map +1 -0
- package/dist/widget/mark.js +20 -0
- package/dist/widget/mark.js.map +1 -0
- package/dist/widget/orb.d.ts +92 -0
- package/dist/widget/orb.d.ts.map +1 -0
- package/dist/widget/orb.js +500 -0
- package/dist/widget/orb.js.map +1 -0
- package/dist/widget/panel.d.ts +40 -0
- package/dist/widget/panel.d.ts.map +1 -0
- package/dist/widget/panel.js +240 -0
- package/dist/widget/panel.js.map +1 -0
- package/dist/widget/styles.d.ts +22 -0
- package/dist/widget/styles.d.ts.map +1 -0
- package/dist/widget/styles.js +192 -0
- package/dist/widget/styles.js.map +1 -0
- 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
|
-
#
|
|
1
|
+
# @mihe-dev/js
|
|
2
2
|
|
|
3
|
-
|
|
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
|
package/dist/audio.d.ts
ADDED
|
@@ -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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"}
|