@alia.onl/sdk 15.6.0 → 15.8.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/CHANGELOG.md +26 -0
- package/README.md +24 -6
- package/package.json +8 -4
- package/src/components/AliaAvatar.tsx +8 -13
- package/src/components/AliaAvatarIcon.tsx +23 -8
- package/src/csp.ts +50 -0
- package/src/hooks/useAliaActivity.ts +1 -1
- package/src/hooks/useAliaChat.ts +1 -1
- package/src/hooks/useDictation.ts +2 -3
- package/src/hooks/useTTS.ts +2 -3
- package/src/hooks/useVoiceRoom.ts +2 -3
- package/src/lib/alia-avatar-still.generated.ts +10 -0
- package/src/lib/alia-character.ts +15 -0
- package/src/lib/character-runtime.ts +6 -4
- package/src/lib/origins.ts +15 -0
- package/src/lib/voice-audio-engine.ts +14 -158
- package/src/lib/voice-worklets.ts +19 -0
- package/worklets/voice-processors.js +152 -0
- package/src/lib/api-url.ts +0 -2
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,31 @@
|
|
|
1
1
|
# @alia.onl/sdk
|
|
2
2
|
|
|
3
|
+
## 15.8.0
|
|
4
|
+
|
|
5
|
+
- **The SDK declares its CSP, and the voice call needs no `blob:` script.** In an app with a
|
|
6
|
+
Content-Security-Policy (Mention) the call failed twice over: `connect-src` lacked
|
|
7
|
+
`wss://api.alia.onl`, and the worklets were `blob:` URLs that `script-src` blocks
|
|
8
|
+
(`Failed to load worklet module script`). `@alia.onl/sdk/csp` exports `ALIA_SDK_CSP` and
|
|
9
|
+
`withAliaSdkCsp()` (`OxyCspExtensions` of `@oxy.so/core`), derived from the same origins the
|
|
10
|
+
SDK's URLs start from (`lib/origins.ts`); a test fails if the source names an origin it does not
|
|
11
|
+
declare. The worklets are one plain module, `worklets/voice-processors.js`, taking their numbers
|
|
12
|
+
as `processorOptions` and served by Alia's web app under its content hash
|
|
13
|
+
(`https://alia.onl/sdk/voice-worklets/<revision>.js`, CORS open, every revision kept), under the
|
|
14
|
+
`script-src https://alia.onl` the avatar already needs. The API's URL is defined once (the
|
|
15
|
+
dictation, read-aloud and voice hooks each had their own copy).
|
|
16
|
+
|
|
17
|
+
## 15.7.0
|
|
18
|
+
|
|
19
|
+
- **`AliaAvatarIcon` is her still until she moves.** At icon size it drew Alia through the live 3D
|
|
20
|
+
runtime, so a header button cost every cold load of the app around it the whole runtime from
|
|
21
|
+
alia.onl (measured: 31 requests, 16.1 MiB in headless Chromium; Mention's home header). It is now
|
|
22
|
+
Bloom's `Avatar` with her character and a still made ahead of time (`characterStill`, Bloom
|
|
23
|
+
10.9.0), a 16 KB PNG in the source: nothing of the runtime loads until she thinks or works, when
|
|
24
|
+
she goes live and plays her working turn as before, with the still under her until the first
|
|
25
|
+
frame. Reduced motion keeps her a still. `bun run render:avatar-still` redraws the still with the
|
|
26
|
+
installed Bloom's runtime; a test fails if `ALIA_AVATAR` changes without it. Peer
|
|
27
|
+
`@oxy.so/bloom` is `>=10.9.0`.
|
|
28
|
+
|
|
3
29
|
## 15.6.0
|
|
4
30
|
|
|
5
31
|
- **The composer has its loader again.** While a turn runs, the pill is wrapped in Bloom's
|
package/README.md
CHANGED
|
@@ -28,12 +28,25 @@ The chat is built from Bloom (`@oxy.so/bloom`, a peer dependency): mount Bloom's
|
|
|
28
28
|
Alia's avatar is Bloom's 3D character (`AgentAvatar`). It draws through a runtime of
|
|
29
29
|
static files (about 214 MB), which the chat loads from Alia's own origin
|
|
30
30
|
(`ALIA_CHARACTER_RUNTIME_URL`, served with CORS open), so the app hosts nothing and mounts
|
|
31
|
-
no avatar provider.
|
|
32
|
-
`connect-src` (and `'wasm-unsafe-eval'`). To serve your own copy instead, serve the Bloom
|
|
31
|
+
no avatar provider. To serve your own copy instead, serve the Bloom
|
|
33
32
|
package's `assets/character-runtime/` directory unchanged and pass the URL of `runtime.mjs`
|
|
34
33
|
as `characterRuntimeUrl` to `AliaChatSheet` or `AliaChatScreen`; native avatars run in a
|
|
35
34
|
WebView, so there the URL must be absolute and reachable from the device.
|
|
36
35
|
|
|
36
|
+
A web app with a Content-Security-Policy composes the SDK's into its own instead of listing
|
|
37
|
+
Alia's origins by hand: `@alia.onl/sdk/csp` exports `ALIA_SDK_CSP` and `withAliaSdkCsp(extensions)`
|
|
38
|
+
in the shape of `@oxy.so/core`'s `OxyCspExtensions` (a leaf entry, safe on a server):
|
|
39
|
+
|
|
40
|
+
```ts
|
|
41
|
+
import { withAliaSdkCsp } from '@alia.onl/sdk/csp';
|
|
42
|
+
app.use(createOxySecurityHeaders({ csp: withAliaSdkCsp(APP_CSP_EXTENSIONS) }));
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
It declares the API and its WebSocket (`connect-src`), the avatar runtime and the voice call's
|
|
46
|
+
worklets on `https://alia.onl` (`script-src`, with `'wasm-unsafe-eval'` for the runtime's
|
|
47
|
+
WebAssembly), attachments and read-aloud clips (`img-src`, `media-src`). No `blob:` script:
|
|
48
|
+
the worklets are a content-addressed file on `alia.onl`, not a blob URL.
|
|
49
|
+
|
|
37
50
|
The empty chat greets the signed-in person by name ("Hi Nate, what's on your mind?"); pass
|
|
38
51
|
`welcomeGreeting` to say something else, or `labels.greeting` / `labels.greetingNamed` to translate it.
|
|
39
52
|
|
|
@@ -44,9 +57,10 @@ The chat's own copy is English. Pass `labels` to `AliaChatSheet` or
|
|
|
44
57
|
`AliaChatScreen` to translate it (`AliaChatLabels`; anything omitted stays
|
|
45
58
|
English). Bloom's own strings follow `BloomProvider`'s `locale`.
|
|
46
59
|
|
|
47
|
-
|
|
60
|
+
Entry points:
|
|
48
61
|
|
|
49
62
|
- `@alia.onl/sdk` — text chat, dictation, read-aloud, catalogue, primitives.
|
|
63
|
+
- `@alia.onl/sdk/csp` — the CSP the SDK needs from a host page (above).
|
|
50
64
|
- `@alia.onl/sdk/voice` — `VoiceSession` and `useVoiceRoom`, the voice call.
|
|
51
65
|
Pass `VoiceSession` to the shell as `voiceSession` to offer calls; leave it
|
|
52
66
|
out for text-only chat and the call is never compiled.
|
|
@@ -120,7 +134,7 @@ components render. Those events are the product runtime's, not part of the Oxy
|
|
|
120
134
|
generic inference contract, and this package is written against them on
|
|
121
135
|
purpose: it is a **product** client.
|
|
122
136
|
|
|
123
|
-
## An "Ask Alia" button with her
|
|
137
|
+
## An "Ask Alia" button with her avatar
|
|
124
138
|
|
|
125
139
|
```tsx
|
|
126
140
|
import { AliaAvatarIcon, useAliaActivity } from '@alia.onl/sdk';
|
|
@@ -130,8 +144,12 @@ const state = useAliaActivity(); // 'idle' | 'thinking' | 'working' | 'writing'
|
|
|
130
144
|
```
|
|
131
145
|
|
|
132
146
|
`AliaAvatarIcon` is a Bloom icon component (`BloomIconComponent`): pass it as `icon` /
|
|
133
|
-
`leadingIcon`.
|
|
134
|
-
`
|
|
147
|
+
`leadingIcon`. At rest it is her **still**, a picture made ahead of time and shipped in the source
|
|
148
|
+
(`src/lib/alia-avatar-still.generated.ts`, Bloom `Avatar`'s `characterStill`), so a header that
|
|
149
|
+
shows her fetches nothing of the 3D runtime. While she thinks or works she goes live under its own
|
|
150
|
+
`AgentAvatarProvider` (`ALIA_CHARACTER_RUNTIME_URL`) and plays her working turn; reduced motion
|
|
151
|
+
keeps her a still. Change `ALIA_AVATAR` and run `bun run render:avatar-still` (a test fails until
|
|
152
|
+
you do); run it too when a Bloom upgrade changes how the runtime draws her.
|
|
135
153
|
|
|
136
154
|
`useAliaActivity()` is the busiest of two sources, so a button reacts even with the sheet closed:
|
|
137
155
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@alia.onl/sdk",
|
|
3
|
-
"version": "15.
|
|
3
|
+
"version": "15.8.0",
|
|
4
4
|
"main": "src/index.ts",
|
|
5
5
|
"react-native": "src/index.ts",
|
|
6
6
|
"source": "src/index.ts",
|
|
@@ -9,12 +9,14 @@
|
|
|
9
9
|
".": "./src/index.ts",
|
|
10
10
|
"./avatar": "./src/avatar.ts",
|
|
11
11
|
"./content": "./src/lib/content-utils.ts",
|
|
12
|
+
"./csp": "./src/csp.ts",
|
|
12
13
|
"./tools": "./src/tools.ts",
|
|
13
14
|
"./voice": "./src/voice.ts",
|
|
14
15
|
"./package.json": "./package.json"
|
|
15
16
|
},
|
|
16
17
|
"files": [
|
|
17
18
|
"src",
|
|
19
|
+
"worklets",
|
|
18
20
|
"NOTICE",
|
|
19
21
|
"tsconfig.json",
|
|
20
22
|
"CHANGELOG.md"
|
|
@@ -27,7 +29,8 @@
|
|
|
27
29
|
"scripts": {
|
|
28
30
|
"typecheck": "tsc --noEmit",
|
|
29
31
|
"test": "vitest run",
|
|
30
|
-
"check:entries": "node scripts/check-entry-isolation.mjs"
|
|
32
|
+
"check:entries": "node scripts/check-entry-isolation.mjs",
|
|
33
|
+
"render:avatar-still": "bun scripts/render-avatar-still.ts"
|
|
31
34
|
},
|
|
32
35
|
"dependencies": {
|
|
33
36
|
"clsx": "^2.1.1",
|
|
@@ -39,7 +42,7 @@
|
|
|
39
42
|
"react": "^19.2.3",
|
|
40
43
|
"react-native": "^0.86.0",
|
|
41
44
|
"@expo/vector-icons": "^15.1.1",
|
|
42
|
-
"@oxy.so/bloom": ">=10.
|
|
45
|
+
"@oxy.so/bloom": ">=10.9.0",
|
|
43
46
|
"@oxy.so/core": ">=3.0.0",
|
|
44
47
|
"@oxy.so/services": ">=8.0.0",
|
|
45
48
|
"@react-native-async-storage/async-storage": "^2.2.0",
|
|
@@ -75,6 +78,7 @@
|
|
|
75
78
|
"react-test-renderer": "19.2.3",
|
|
76
79
|
"typescript": "^5.9.3",
|
|
77
80
|
"vitest": "^4.1.10",
|
|
78
|
-
"react-native-audio-api": "0.13.6"
|
|
81
|
+
"react-native-audio-api": "0.13.6",
|
|
82
|
+
"playwright-core": "1.64.0"
|
|
79
83
|
}
|
|
80
84
|
}
|
|
@@ -1,22 +1,17 @@
|
|
|
1
|
-
import { AgentAvatar
|
|
1
|
+
import { AgentAvatar } from '@oxy.so/bloom/agent-avatar';
|
|
2
2
|
import { memo, useEffect, useState } from 'react';
|
|
3
|
+
import { ALIA_AVATAR } from '../lib/alia-character';
|
|
4
|
+
|
|
5
|
+
export { ALIA_AVATAR };
|
|
3
6
|
|
|
4
7
|
/** What Alia is doing; the avatar plays a working turn each time it starts to think or work. */
|
|
5
8
|
export type AliaAvatarState = 'idle' | 'thinking' | 'working' | 'writing';
|
|
6
9
|
|
|
7
|
-
/**
|
|
8
|
-
* Alia's likeness in Bloom's 3D character engine: the six-lobed flower in orchid, plain oval
|
|
9
|
-
* eyes, nothing on its head. It is `blue_beret` with the hat taken off and the paint changed,
|
|
10
|
-
* so Bloom retargets that preset's animation onto it; change it here and nowhere else.
|
|
11
|
-
*/
|
|
12
|
-
export const ALIA_AVATAR: AvatarCharacter = {
|
|
13
|
-
preset: 'blue_beret',
|
|
14
|
-
accessory: 'none',
|
|
15
|
-
color: 'orchid',
|
|
16
|
-
};
|
|
17
|
-
|
|
18
10
|
const BUSY: ReadonlySet<AliaAvatarState> = new Set(['thinking', 'working']);
|
|
19
11
|
|
|
12
|
+
/** Whether a state is work her avatar shows: thinking or working, not writing. */
|
|
13
|
+
export const aliaIsBusy = (state: AliaAvatarState): boolean => BUSY.has(state);
|
|
14
|
+
|
|
20
15
|
export interface AliaAvatarProps {
|
|
21
16
|
/** Width and height in px. */
|
|
22
17
|
size: number;
|
|
@@ -43,7 +38,7 @@ export const AliaAvatar = memo(function AliaAvatar({
|
|
|
43
38
|
portrait = false,
|
|
44
39
|
}: AliaAvatarProps) {
|
|
45
40
|
const [workingKey, setWorkingKey] = useState(0);
|
|
46
|
-
const busy =
|
|
41
|
+
const busy = aliaIsBusy(state);
|
|
47
42
|
useEffect(() => {
|
|
48
43
|
if (busy) setWorkingKey((key) => key + 1);
|
|
49
44
|
}, [busy]);
|
|
@@ -1,30 +1,45 @@
|
|
|
1
1
|
import { AgentAvatarProvider } from '@oxy.so/bloom/agent-avatar';
|
|
2
|
+
import { Avatar } from '@oxy.so/bloom/avatar';
|
|
2
3
|
import type { BloomIconComponent } from '@oxy.so/bloom/icons';
|
|
3
4
|
import { useAliaActivity } from '../hooks/useAliaActivity';
|
|
5
|
+
import { ALIA_AVATAR } from '../lib/alia-character';
|
|
6
|
+
import { ALIA_AVATAR_STILL } from '../lib/alia-avatar-still.generated';
|
|
4
7
|
import { ALIA_CHARACTER_RUNTIME_URL } from '../lib/character-runtime';
|
|
5
|
-
import {
|
|
8
|
+
import { aliaIsBusy } from './AliaAvatar';
|
|
6
9
|
|
|
7
10
|
const DEFAULT_SIZE = 20;
|
|
11
|
+
const STILL = { uri: ALIA_AVATAR_STILL };
|
|
8
12
|
|
|
9
13
|
/**
|
|
10
|
-
* Alia
|
|
11
|
-
*
|
|
12
|
-
* the chat sheet is closed.
|
|
14
|
+
* Alia as a Bloom icon: pass it as `icon` / `leadingIcon` to a Bloom `Button` and the button shows
|
|
15
|
+
* her — also while the chat sheet is closed (`useAliaActivity`).
|
|
13
16
|
*
|
|
14
17
|
* ```tsx
|
|
15
18
|
* <Button icon={AliaAvatarIcon} onPress={openAlia}>Ask Alia</Button>
|
|
16
19
|
* ```
|
|
17
20
|
*
|
|
18
|
-
*
|
|
19
|
-
*
|
|
21
|
+
* At icon size she is her still, a picture made ahead of time (`render:avatar-still`), so a header
|
|
22
|
+
* that shows her loads nothing of the 3D runtime. She goes live — the runtime loads then — while
|
|
23
|
+
* she thinks or works, and plays her working turn; Bloom's `Avatar` decides that
|
|
24
|
+
* (`characterIsLive`), and keeps her a still under reduced motion. The still stays under her
|
|
25
|
+
* until the first live frame.
|
|
26
|
+
*
|
|
27
|
+
* It mounts its own `AgentAvatarProvider` on `ALIA_CHARACTER_RUNTIME_URL`: a live avatar outside
|
|
28
|
+
* the chat's provider would ask the host for `/bloom-character/runtime.mjs` and get its HTML. The
|
|
20
29
|
* colour (`fill`) is ignored — the likeness is painted — and it is never interactive, so the
|
|
21
|
-
* button keeps the press.
|
|
30
|
+
* button keeps the press.
|
|
22
31
|
*/
|
|
23
32
|
export const AliaAvatarIcon: BloomIconComponent = ({ width, height }) => {
|
|
24
33
|
const state = useAliaActivity();
|
|
25
34
|
return (
|
|
26
35
|
<AgentAvatarProvider runtimeUrl={ALIA_CHARACTER_RUNTIME_URL}>
|
|
27
|
-
<
|
|
36
|
+
<Avatar
|
|
37
|
+
character={ALIA_AVATAR}
|
|
38
|
+
characterStill={STILL}
|
|
39
|
+
size={width ?? height ?? DEFAULT_SIZE}
|
|
40
|
+
activity={aliaIsBusy(state) ? 'working' : 'idle'}
|
|
41
|
+
alt="Alia"
|
|
42
|
+
/>
|
|
28
43
|
</AgentAvatarProvider>
|
|
29
44
|
);
|
|
30
45
|
};
|
package/src/csp.ts
ADDED
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `@alia.onl/sdk/csp` — the Content-Security-Policy the SDK needs from a host page, in the
|
|
3
|
+
* shape of `@oxy.so/core`'s `OxyCspExtensions`, so a host composes it into its own policy
|
|
4
|
+
* instead of copying Alia's origins by hand:
|
|
5
|
+
*
|
|
6
|
+
* ```ts
|
|
7
|
+
* import { withAliaSdkCsp } from '@alia.onl/sdk/csp';
|
|
8
|
+
* createOxySecurityHeaders({ csp: withAliaSdkCsp(APP_CSP_EXTENSIONS) });
|
|
9
|
+
* ```
|
|
10
|
+
*
|
|
11
|
+
* Every source is derived from `lib/origins.ts`, the values the SDK's own URLs start from, and a
|
|
12
|
+
* test fails if the SDK's source names an origin this does not declare. A leaf entry: no React,
|
|
13
|
+
* no React Native, safe to import from a server.
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
import type { OxyCspDirective, OxyCspExtensions } from '@oxy.so/core/server';
|
|
17
|
+
import { ALIA_API_URL, ALIA_WEB_ORIGIN } from './lib/origins';
|
|
18
|
+
import { voiceSessionUrl } from './lib/voice-protocol';
|
|
19
|
+
|
|
20
|
+
const api = new URL(ALIA_API_URL).origin;
|
|
21
|
+
const voiceSocket = new URL(voiceSessionUrl(ALIA_API_URL)).origin;
|
|
22
|
+
|
|
23
|
+
/** What the SDK loads, by directive, and why. */
|
|
24
|
+
export const ALIA_SDK_CSP = Object.freeze({
|
|
25
|
+
// The API (chat, dictation, speech, the activity socket's polling) and its WebSocket (the
|
|
26
|
+
// voice call, socket.io); the avatar runtime fetches its WebAssembly and data from the web app.
|
|
27
|
+
connectSrc: Object.freeze([api, voiceSocket, ALIA_WEB_ORIGIN]),
|
|
28
|
+
// The avatar runtime and the voice call's worklets are modules served by the web app; the
|
|
29
|
+
// runtime compiles WebAssembly (`'wasm-unsafe-eval'` allows that and nothing else: no eval).
|
|
30
|
+
scriptSrc: Object.freeze([ALIA_WEB_ORIGIN, "'wasm-unsafe-eval'"]),
|
|
31
|
+
// Stored attachments come from the API; an attachment's preview before upload is a blob.
|
|
32
|
+
imgSrc: Object.freeze([api, 'blob:']),
|
|
33
|
+
// Read-aloud clips are played from the API.
|
|
34
|
+
mediaSrc: Object.freeze([api]),
|
|
35
|
+
} satisfies OxyCspExtensions);
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* The host's own CSP extensions with the SDK's added: each directive is the union, the host's
|
|
39
|
+
* sources first. `@oxy.so/core` dedupes when it builds the policy.
|
|
40
|
+
*/
|
|
41
|
+
export function withAliaSdkCsp(host: OxyCspExtensions = {}): OxyCspExtensions {
|
|
42
|
+
const merged: Partial<Record<OxyCspDirective, readonly string[]>> = { ...host };
|
|
43
|
+
for (const [directive, sources] of Object.entries(ALIA_SDK_CSP) as [
|
|
44
|
+
OxyCspDirective,
|
|
45
|
+
readonly string[],
|
|
46
|
+
][]) {
|
|
47
|
+
merged[directive] = [...(host[directive] ?? []), ...sources];
|
|
48
|
+
}
|
|
49
|
+
return merged;
|
|
50
|
+
}
|
|
@@ -3,7 +3,7 @@ import { useOxy } from '@oxy.so/services/ui/client';
|
|
|
3
3
|
import type { AliaAvatarState } from '../components/AliaAvatar';
|
|
4
4
|
import { selectAliaActivity, useAliaActivityStore } from '../lib/activity-store';
|
|
5
5
|
import { acquireActivitySocket } from '../lib/activity-socket';
|
|
6
|
-
import { ALIA_API_URL } from '../lib/
|
|
6
|
+
import { ALIA_API_URL } from '../lib/origins';
|
|
7
7
|
|
|
8
8
|
export interface UseAliaActivityOptions {
|
|
9
9
|
/** The API the activity socket connects to. Defaults like `useAliaChat`'s `apiUrl`. */
|
package/src/hooks/useAliaChat.ts
CHANGED
|
@@ -7,7 +7,7 @@ import { agentMessageId, delegatedAgent } from '../lib/delegation';
|
|
|
7
7
|
import { streamAliaChat } from '../lib/chat-transport';
|
|
8
8
|
import type { AliaChatStreamEvent } from '../lib/chat-stream';
|
|
9
9
|
|
|
10
|
-
import { ALIA_API_URL } from '../lib/
|
|
10
|
+
import { ALIA_API_URL } from '../lib/origins';
|
|
11
11
|
import { useAliaActivityStore } from '../lib/activity-store';
|
|
12
12
|
import { turnActivityState } from '../lib/turn-state';
|
|
13
13
|
|
|
@@ -28,8 +28,7 @@ import {
|
|
|
28
28
|
import { DictationRecorderError, useDictationRecorder } from '../lib/dictation-recorder';
|
|
29
29
|
import { defaultSpeechLanguage } from '../lib/speech-language';
|
|
30
30
|
import { VOICE_ERROR_MESSAGES, type VoiceErrorCode } from '../lib/speech-messages';
|
|
31
|
-
|
|
32
|
-
const API_URL = process.env.EXPO_PUBLIC_ALIA_API_URL ?? 'https://api.alia.onl';
|
|
31
|
+
import { ALIA_API_URL } from '../lib/origins';
|
|
33
32
|
|
|
34
33
|
/** The remaining time is shown from here on. */
|
|
35
34
|
const COUNTDOWN_FROM_MS = 30_000;
|
|
@@ -132,7 +131,7 @@ export function useDictation(options: UseDictationOptions = {}) {
|
|
|
132
131
|
abortRef.current = controller;
|
|
133
132
|
try {
|
|
134
133
|
const result = await requestTranscription({
|
|
135
|
-
apiUrl: optionsRef.current.apiUrl ||
|
|
134
|
+
apiUrl: optionsRef.current.apiUrl || ALIA_API_URL,
|
|
136
135
|
token,
|
|
137
136
|
clip,
|
|
138
137
|
language: optionsRef.current.lang ?? defaultSpeechLanguage(),
|
package/src/hooks/useTTS.ts
CHANGED
|
@@ -6,8 +6,7 @@ import { errorMessage } from '../lib/utils';
|
|
|
6
6
|
import { create } from 'zustand';
|
|
7
7
|
import { createAudioLevelMeter } from '../lib/audio-level';
|
|
8
8
|
import { requestSpeechClip } from '../lib/speech-synthesis';
|
|
9
|
-
|
|
10
|
-
const API_URL = process.env.EXPO_PUBLIC_ALIA_API_URL ?? 'https://api.alia.onl';
|
|
9
|
+
import { ALIA_API_URL } from '../lib/origins';
|
|
11
10
|
|
|
12
11
|
// ============== OPTIONS ==============
|
|
13
12
|
|
|
@@ -83,7 +82,7 @@ useTTSStore.subscribe((state, previous) => {
|
|
|
83
82
|
// ============== HOOK ==============
|
|
84
83
|
|
|
85
84
|
export function useTTS(options: UseTTSOptions = {}) {
|
|
86
|
-
const apiUrl = options.apiUrl ||
|
|
85
|
+
const apiUrl = options.apiUrl || ALIA_API_URL;
|
|
87
86
|
const voicePref = options.voice ?? 'female';
|
|
88
87
|
const voiceModel = options.model;
|
|
89
88
|
const tonePref = options.tone ?? 'default';
|
|
@@ -24,8 +24,7 @@ import type { VoiceAudioEngine } from '../lib/voice-audio-engine-types';
|
|
|
24
24
|
import { VoiceCall, type VoiceCallSnapshot, type VoiceSocketFactory } from '../lib/voice-call';
|
|
25
25
|
import { VoiceLevelChannel, type VoiceLevelSource } from '../lib/voice-levels';
|
|
26
26
|
import type { ProductVoice, VoiceCloseReason } from '../lib/voice-protocol';
|
|
27
|
-
|
|
28
|
-
const API_URL = process.env.EXPO_PUBLIC_ALIA_API_URL ?? 'https://api.alia.onl';
|
|
27
|
+
import { ALIA_API_URL } from '../lib/origins';
|
|
29
28
|
|
|
30
29
|
export interface UseVoiceRoomOptions {
|
|
31
30
|
apiUrl?: string;
|
|
@@ -120,7 +119,7 @@ export function useVoiceRoom(options: UseVoiceRoomOptions = {}) {
|
|
|
120
119
|
if (live !== undefined && live !== 'ended' && live !== 'error') return;
|
|
121
120
|
const current = optionsRef.current;
|
|
122
121
|
const call = new VoiceCall({
|
|
123
|
-
apiUrl: current.apiUrl ||
|
|
122
|
+
apiUrl: current.apiUrl || ALIA_API_URL,
|
|
124
123
|
getToken,
|
|
125
124
|
conversationId: current.conversationId ?? newConversationId(),
|
|
126
125
|
...(current.agentId === undefined ? {} : { agentId: current.agentId }),
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
// Generated by `bun run render:avatar-still` (scripts/render-avatar-still.ts). Do not edit.
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Alia's still: `ALIA_AVATAR` drawn by @oxy.so/bloom 10.6.0's character runtime as Bloom
|
|
5
|
+
* draws any still (a paused portrait, reduced motion), 64 px at 2x, 16489 bytes of PNG.
|
|
6
|
+
*/
|
|
7
|
+
export const ALIA_AVATAR_STILL = 'data:image/png;base64,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';
|
|
8
|
+
|
|
9
|
+
/** The character it was drawn from; a test holds it equal to `ALIA_AVATAR`. */
|
|
10
|
+
export const ALIA_AVATAR_STILL_OF = {"preset":"blue_beret","accessory":"none","color":"orchid"};
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import type { AvatarCharacter } from '@oxy.so/bloom/agent-avatar';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Alia's likeness in Bloom's 3D character engine: the six-lobed flower in orchid, plain oval
|
|
5
|
+
* eyes, nothing on its head. It is `blue_beret` with the hat taken off and the paint changed,
|
|
6
|
+
* so Bloom retargets that preset's animation onto it; change it here and nowhere else — and run
|
|
7
|
+
* `bun run render:avatar-still` after, so her still (`alia-avatar-still.generated.ts`) matches.
|
|
8
|
+
*
|
|
9
|
+
* A module of its own, without React: the still's renderer script reads it under plain bun.
|
|
10
|
+
*/
|
|
11
|
+
export const ALIA_AVATAR: AvatarCharacter = {
|
|
12
|
+
preset: 'blue_beret',
|
|
13
|
+
accessory: 'none',
|
|
14
|
+
color: 'orchid',
|
|
15
|
+
};
|
|
@@ -1,10 +1,12 @@
|
|
|
1
|
+
import { ALIA_WEB_ORIGIN } from './origins';
|
|
2
|
+
|
|
1
3
|
/**
|
|
2
4
|
* Where the 3D character runtime (Alia's avatar) is served from when the host does not say.
|
|
3
5
|
*
|
|
4
6
|
* The runtime is ~214 MB of engine and data, so the chat loads it from Alia's own web origin
|
|
5
7
|
* instead of asking every app to host a copy. That origin serves it with CORS open
|
|
6
|
-
* (`packages/app/worker/index.js`), which a cross-origin module import needs;
|
|
7
|
-
*
|
|
8
|
-
*
|
|
8
|
+
* (`packages/app/worker/index.js`), which a cross-origin module import needs; a host with a
|
|
9
|
+
* CSP allows it with `@alia.onl/sdk/csp`. A host that serves its own copy passes
|
|
10
|
+
* `characterRuntimeUrl`.
|
|
9
11
|
*/
|
|
10
|
-
export const ALIA_CHARACTER_RUNTIME_URL =
|
|
12
|
+
export const ALIA_CHARACTER_RUNTIME_URL = `${ALIA_WEB_ORIGIN}/bloom-character/runtime.mjs`;
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The two origins the SDK talks to, defined once. Every URL it builds starts from one of them,
|
|
3
|
+
* and `@alia.onl/sdk/csp` (`src/csp.ts`) derives the host's CSP from these same values, so an
|
|
4
|
+
* origin the SDK uses is an origin it declares.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
/** Alia's API: `EXPO_PUBLIC_ALIA_API_URL`, then production. Chat, dictation, speech, voice. */
|
|
8
|
+
export const ALIA_API_URL = process.env.EXPO_PUBLIC_ALIA_API_URL ?? 'https://api.alia.onl';
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Alia's web app, which serves the files the SDK shares with every host: Bloom's character
|
|
12
|
+
* runtime (`/bloom-character/`) and the voice call's worklets (`/sdk/voice-worklets/`), both
|
|
13
|
+
* with CORS open (`packages/app/worker/index.js`).
|
|
14
|
+
*/
|
|
15
|
+
export const ALIA_WEB_ORIGIN = 'https://alia.onl';
|
|
@@ -11,12 +11,14 @@
|
|
|
11
11
|
* playback through a local `RTCPeerConnection` loopback) is not built until the
|
|
12
12
|
* measurement says it is needed.
|
|
13
13
|
*
|
|
14
|
-
* The worklets are
|
|
15
|
-
*
|
|
14
|
+
* The worklets are one module served by Alia's web app (`./voice-worklets.ts`),
|
|
15
|
+
* never a `blob:` URL a host's CSP would have to allow as script. Their numbers
|
|
16
|
+
* (stream rate, frame size, prebuffer) are passed as `processorOptions`.
|
|
16
17
|
*/
|
|
17
18
|
|
|
18
19
|
import { levelOfPcm16, pcm16ToFloat } from './pcm';
|
|
19
20
|
import { VOICE_FRAME_SAMPLES, VOICE_SAMPLE_RATE } from './voice-protocol';
|
|
21
|
+
import { VOICE_WORKLETS_URL } from './voice-worklets';
|
|
20
22
|
import {
|
|
21
23
|
VoiceAudioEngineError,
|
|
22
24
|
type VoiceAudioEngine,
|
|
@@ -30,155 +32,6 @@ const PREBUFFER_MS = 80;
|
|
|
30
32
|
/** A burst shorter than the prebuffer still plays once nothing more arrives for this long. */
|
|
31
33
|
const PREBUFFER_IDLE_MS = 100;
|
|
32
34
|
|
|
33
|
-
const CAPTURE_PROCESSOR = `
|
|
34
|
-
class AliaVoiceCapture extends AudioWorkletProcessor {
|
|
35
|
-
constructor() {
|
|
36
|
-
super();
|
|
37
|
-
this.ratio = sampleRate / ${VOICE_SAMPLE_RATE};
|
|
38
|
-
this.t = 0;
|
|
39
|
-
this.prev = 0;
|
|
40
|
-
this.frame = new Int16Array(${VOICE_FRAME_SAMPLES});
|
|
41
|
-
this.filled = 0;
|
|
42
|
-
}
|
|
43
|
-
process(inputs) {
|
|
44
|
-
const input = inputs[0] && inputs[0][0];
|
|
45
|
-
if (!input || input.length === 0) return true;
|
|
46
|
-
const n = input.length;
|
|
47
|
-
while (this.t < n - 1) {
|
|
48
|
-
const i = Math.floor(this.t);
|
|
49
|
-
const f = this.t - i;
|
|
50
|
-
const a = i < 0 ? this.prev : input[i];
|
|
51
|
-
const b = input[i + 1];
|
|
52
|
-
let s = a + (b - a) * f;
|
|
53
|
-
s = s < -1 ? -1 : s > 1 ? 1 : s;
|
|
54
|
-
this.frame[this.filled++] = s < 0 ? s * 0x8000 : s * 0x7fff;
|
|
55
|
-
if (this.filled === this.frame.length) {
|
|
56
|
-
const out = this.frame;
|
|
57
|
-
this.port.postMessage(out.buffer, [out.buffer]);
|
|
58
|
-
this.frame = new Int16Array(${VOICE_FRAME_SAMPLES});
|
|
59
|
-
this.filled = 0;
|
|
60
|
-
}
|
|
61
|
-
this.t += this.ratio;
|
|
62
|
-
}
|
|
63
|
-
this.t -= n;
|
|
64
|
-
this.prev = input[n - 1];
|
|
65
|
-
return true;
|
|
66
|
-
}
|
|
67
|
-
}
|
|
68
|
-
registerProcessor('alia-voice-capture', AliaVoiceCapture);
|
|
69
|
-
`;
|
|
70
|
-
|
|
71
|
-
const PLAYBACK_PROCESSOR = `
|
|
72
|
-
class AliaVoicePlayback extends AudioWorkletProcessor {
|
|
73
|
-
constructor() {
|
|
74
|
-
super();
|
|
75
|
-
this.step = ${VOICE_SAMPLE_RATE} / sampleRate;
|
|
76
|
-
this.queue = [];
|
|
77
|
-
this.queued = 0;
|
|
78
|
-
this.pos = 0;
|
|
79
|
-
this.buffering = true;
|
|
80
|
-
this.lastChunkAt = 0;
|
|
81
|
-
this.item = null;
|
|
82
|
-
this.played = 0;
|
|
83
|
-
this.sinceReport = 0;
|
|
84
|
-
this.sumSquares = 0;
|
|
85
|
-
this.count = 0;
|
|
86
|
-
this.port.onmessage = (event) => {
|
|
87
|
-
const message = event.data;
|
|
88
|
-
if (message.type === 'chunk') {
|
|
89
|
-
this.queue.push({ itemId: message.itemId, samples: message.samples });
|
|
90
|
-
this.queued += message.samples.length;
|
|
91
|
-
this.lastChunkAt = currentTime;
|
|
92
|
-
} else if (message.type === 'flush') {
|
|
93
|
-
const playedMs = this.item === null ? null : (this.played / ${VOICE_SAMPLE_RATE}) * 1000;
|
|
94
|
-
this.port.postMessage({ type: 'flushed', itemId: this.item, playedMs });
|
|
95
|
-
this.queue = [];
|
|
96
|
-
this.queued = 0;
|
|
97
|
-
this.pos = 0;
|
|
98
|
-
this.buffering = true;
|
|
99
|
-
this.item = null;
|
|
100
|
-
this.played = 0;
|
|
101
|
-
}
|
|
102
|
-
};
|
|
103
|
-
}
|
|
104
|
-
report(force) {
|
|
105
|
-
if (!force && this.sinceReport < sampleRate / 20) return;
|
|
106
|
-
const rms = this.count === 0 ? 0 : Math.sqrt(this.sumSquares / this.count);
|
|
107
|
-
this.port.postMessage({
|
|
108
|
-
type: 'progress',
|
|
109
|
-
itemId: this.item,
|
|
110
|
-
playedMs: (this.played / ${VOICE_SAMPLE_RATE}) * 1000,
|
|
111
|
-
rms,
|
|
112
|
-
});
|
|
113
|
-
this.sinceReport = 0;
|
|
114
|
-
this.sumSquares = 0;
|
|
115
|
-
this.count = 0;
|
|
116
|
-
}
|
|
117
|
-
process(_inputs, outputs) {
|
|
118
|
-
const out = outputs[0][0];
|
|
119
|
-
if (this.buffering) {
|
|
120
|
-
const ready =
|
|
121
|
-
this.queued >= ${(VOICE_SAMPLE_RATE * PREBUFFER_MS) / 1000} ||
|
|
122
|
-
(this.queued > 0 && currentTime - this.lastChunkAt > ${PREBUFFER_IDLE_MS / 1000});
|
|
123
|
-
if (!ready) {
|
|
124
|
-
out.fill(0);
|
|
125
|
-
for (let c = 1; c < outputs[0].length; c++) outputs[0][c].fill(0);
|
|
126
|
-
return true;
|
|
127
|
-
}
|
|
128
|
-
this.buffering = false;
|
|
129
|
-
}
|
|
130
|
-
for (let k = 0; k < out.length; k++) {
|
|
131
|
-
const head = this.queue[0];
|
|
132
|
-
if (!head) {
|
|
133
|
-
out[k] = 0;
|
|
134
|
-
if (!this.buffering) {
|
|
135
|
-
this.buffering = true;
|
|
136
|
-
this.report(true);
|
|
137
|
-
this.port.postMessage({ type: 'idle' });
|
|
138
|
-
}
|
|
139
|
-
continue;
|
|
140
|
-
}
|
|
141
|
-
if (head.itemId !== this.item) {
|
|
142
|
-
this.item = head.itemId;
|
|
143
|
-
this.played = 0;
|
|
144
|
-
this.port.postMessage({ type: 'started', itemId: head.itemId });
|
|
145
|
-
}
|
|
146
|
-
const i = Math.floor(this.pos);
|
|
147
|
-
const f = this.pos - i;
|
|
148
|
-
const a = head.samples[i];
|
|
149
|
-
const b = i + 1 < head.samples.length ? head.samples[i + 1] : a;
|
|
150
|
-
const s = a + (b - a) * f;
|
|
151
|
-
out[k] = s;
|
|
152
|
-
this.sumSquares += s * s;
|
|
153
|
-
this.count++;
|
|
154
|
-
this.pos += this.step;
|
|
155
|
-
const consumed = Math.floor(this.pos);
|
|
156
|
-
if (consumed > 0) {
|
|
157
|
-
const taken = Math.min(consumed, head.samples.length);
|
|
158
|
-
this.played += taken;
|
|
159
|
-
this.queued -= taken;
|
|
160
|
-
if (consumed >= head.samples.length) {
|
|
161
|
-
this.queue.shift();
|
|
162
|
-
this.pos = 0;
|
|
163
|
-
} else {
|
|
164
|
-
head.samples = head.samples.subarray(consumed);
|
|
165
|
-
this.pos -= consumed;
|
|
166
|
-
}
|
|
167
|
-
}
|
|
168
|
-
}
|
|
169
|
-
for (let c = 1; c < outputs[0].length; c++) outputs[0][c].set(out);
|
|
170
|
-
this.sinceReport += out.length;
|
|
171
|
-
this.report(false);
|
|
172
|
-
return true;
|
|
173
|
-
}
|
|
174
|
-
}
|
|
175
|
-
registerProcessor('alia-voice-playback', AliaVoicePlayback);
|
|
176
|
-
`;
|
|
177
|
-
|
|
178
|
-
function moduleUrl(source: string): string {
|
|
179
|
-
return URL.createObjectURL(new Blob([source], { type: 'application/javascript' }));
|
|
180
|
-
}
|
|
181
|
-
|
|
182
35
|
function deviceError(caught: unknown): VoiceAudioEngineError {
|
|
183
36
|
const name = (caught as { name?: unknown } | null)?.name;
|
|
184
37
|
if (name === 'NotAllowedError' || name === 'SecurityError' || name === 'PermissionDeniedError')
|
|
@@ -229,16 +82,10 @@ export function createVoiceAudioEngine(): VoiceAudioEngine {
|
|
|
229
82
|
}
|
|
230
83
|
|
|
231
84
|
context = new AudioContextCtor({ latencyHint: 'interactive' });
|
|
232
|
-
const captureUrl = moduleUrl(CAPTURE_PROCESSOR);
|
|
233
|
-
const playbackUrl = moduleUrl(PLAYBACK_PROCESSOR);
|
|
234
85
|
try {
|
|
235
|
-
await context.audioWorklet.addModule(
|
|
236
|
-
await context.audioWorklet.addModule(playbackUrl);
|
|
86
|
+
await context.audioWorklet.addModule(VOICE_WORKLETS_URL);
|
|
237
87
|
} catch {
|
|
238
88
|
throw new VoiceAudioEngineError('voice-unavailable');
|
|
239
|
-
} finally {
|
|
240
|
-
URL.revokeObjectURL(captureUrl);
|
|
241
|
-
URL.revokeObjectURL(playbackUrl);
|
|
242
89
|
}
|
|
243
90
|
await context.resume().catch(() => undefined);
|
|
244
91
|
|
|
@@ -247,6 +94,10 @@ export function createVoiceAudioEngine(): VoiceAudioEngine {
|
|
|
247
94
|
numberOfInputs: 1,
|
|
248
95
|
numberOfOutputs: 0,
|
|
249
96
|
channelCount: 1,
|
|
97
|
+
processorOptions: {
|
|
98
|
+
streamSampleRate: VOICE_SAMPLE_RATE,
|
|
99
|
+
frameSamples: VOICE_FRAME_SAMPLES,
|
|
100
|
+
},
|
|
250
101
|
});
|
|
251
102
|
capture.port.onmessage = (event: MessageEvent<ArrayBuffer>) => {
|
|
252
103
|
if (stopped) return;
|
|
@@ -260,6 +111,11 @@ export function createVoiceAudioEngine(): VoiceAudioEngine {
|
|
|
260
111
|
numberOfInputs: 0,
|
|
261
112
|
numberOfOutputs: 1,
|
|
262
113
|
outputChannelCount: [context.destination.channelCount || 2],
|
|
114
|
+
processorOptions: {
|
|
115
|
+
streamSampleRate: VOICE_SAMPLE_RATE,
|
|
116
|
+
prebufferMs: PREBUFFER_MS,
|
|
117
|
+
prebufferIdleMs: PREBUFFER_IDLE_MS,
|
|
118
|
+
},
|
|
263
119
|
});
|
|
264
120
|
playback.port.onmessage = (event: MessageEvent) => {
|
|
265
121
|
const message = event.data as
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { ALIA_WEB_ORIGIN } from './origins';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* The voice call's AudioWorklet processors (`worklets/voice-processors.js`), as the web engine
|
|
5
|
+
* loads them.
|
|
6
|
+
*
|
|
7
|
+
* Not a `blob:` URL: a worklet module is a script, so a blob would need `script-src blob:` in
|
|
8
|
+
* every host's CSP, which would let any blob the page makes run as script. Alia's web app serves
|
|
9
|
+
* the file instead, under `script-src https://alia.onl` that the avatar already needs
|
|
10
|
+
* (`@alia.onl/sdk/csp`), with CORS open for the cross-origin `addModule`.
|
|
11
|
+
*
|
|
12
|
+
* The name is the file's content hash, so an SDK embedded in another app (on its own release
|
|
13
|
+
* cycle) always loads the bytes it was written against: `packages/app/public/sdk/voice-worklets/`
|
|
14
|
+
* keeps every revision ever published, and a test fails if this revision is not the hash of the
|
|
15
|
+
* file or is not served.
|
|
16
|
+
*/
|
|
17
|
+
export const VOICE_WORKLETS_REVISION = '543d4a8e42937e99';
|
|
18
|
+
|
|
19
|
+
export const VOICE_WORKLETS_URL = `${ALIA_WEB_ORIGIN}/sdk/voice-worklets/${VOICE_WORKLETS_REVISION}.js`;
|
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
// The web voice call's two AudioWorklet processors (`src/lib/voice-audio-engine.ts`).
|
|
2
|
+
//
|
|
3
|
+
// Served from Alia's web origin, content-addressed (`/sdk/voice-worklets/<revision>.js`,
|
|
4
|
+
// `src/lib/voice-worklets.ts`), so a host page loads it under `script-src https://alia.onl`
|
|
5
|
+
// instead of a `blob:` URL its CSP would have to allow. Plain JavaScript with no imports: a
|
|
6
|
+
// worklet module runs in the AudioWorkletGlobalScope. Every number it needs arrives in
|
|
7
|
+
// `processorOptions`, so the bytes stay the same while the engine's settings change.
|
|
8
|
+
|
|
9
|
+
class AliaVoiceCapture extends AudioWorkletProcessor {
|
|
10
|
+
constructor(options) {
|
|
11
|
+
super();
|
|
12
|
+
const { streamSampleRate, frameSamples } = options.processorOptions;
|
|
13
|
+
this.ratio = sampleRate / streamSampleRate;
|
|
14
|
+
this.frameSamples = frameSamples;
|
|
15
|
+
this.t = 0;
|
|
16
|
+
this.prev = 0;
|
|
17
|
+
this.frame = new Int16Array(frameSamples);
|
|
18
|
+
this.filled = 0;
|
|
19
|
+
}
|
|
20
|
+
process(inputs) {
|
|
21
|
+
const input = inputs[0]?.[0];
|
|
22
|
+
if (!input || input.length === 0) return true;
|
|
23
|
+
const n = input.length;
|
|
24
|
+
while (this.t < n - 1) {
|
|
25
|
+
const i = Math.floor(this.t);
|
|
26
|
+
const f = this.t - i;
|
|
27
|
+
const a = i < 0 ? this.prev : input[i];
|
|
28
|
+
const b = input[i + 1];
|
|
29
|
+
let s = a + (b - a) * f;
|
|
30
|
+
s = s < -1 ? -1 : s > 1 ? 1 : s;
|
|
31
|
+
this.frame[this.filled++] = s < 0 ? s * 0x8000 : s * 0x7fff;
|
|
32
|
+
if (this.filled === this.frame.length) {
|
|
33
|
+
const out = this.frame;
|
|
34
|
+
this.port.postMessage(out.buffer, [out.buffer]);
|
|
35
|
+
this.frame = new Int16Array(this.frameSamples);
|
|
36
|
+
this.filled = 0;
|
|
37
|
+
}
|
|
38
|
+
this.t += this.ratio;
|
|
39
|
+
}
|
|
40
|
+
this.t -= n;
|
|
41
|
+
this.prev = input[n - 1];
|
|
42
|
+
return true;
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
registerProcessor('alia-voice-capture', AliaVoiceCapture);
|
|
46
|
+
|
|
47
|
+
class AliaVoicePlayback extends AudioWorkletProcessor {
|
|
48
|
+
constructor(options) {
|
|
49
|
+
super();
|
|
50
|
+
const { streamSampleRate, prebufferMs, prebufferIdleMs } = options.processorOptions;
|
|
51
|
+
this.streamSampleRate = streamSampleRate;
|
|
52
|
+
this.prebufferSamples = (streamSampleRate * prebufferMs) / 1000;
|
|
53
|
+
this.prebufferIdleSeconds = prebufferIdleMs / 1000;
|
|
54
|
+
this.step = streamSampleRate / sampleRate;
|
|
55
|
+
this.queue = [];
|
|
56
|
+
this.queued = 0;
|
|
57
|
+
this.pos = 0;
|
|
58
|
+
this.buffering = true;
|
|
59
|
+
this.lastChunkAt = 0;
|
|
60
|
+
this.item = null;
|
|
61
|
+
this.played = 0;
|
|
62
|
+
this.sinceReport = 0;
|
|
63
|
+
this.sumSquares = 0;
|
|
64
|
+
this.count = 0;
|
|
65
|
+
this.port.onmessage = (event) => {
|
|
66
|
+
const message = event.data;
|
|
67
|
+
if (message.type === 'chunk') {
|
|
68
|
+
this.queue.push({ itemId: message.itemId, samples: message.samples });
|
|
69
|
+
this.queued += message.samples.length;
|
|
70
|
+
this.lastChunkAt = currentTime;
|
|
71
|
+
} else if (message.type === 'flush') {
|
|
72
|
+
const playedMs = this.item === null ? null : this.playedMs();
|
|
73
|
+
this.port.postMessage({ type: 'flushed', itemId: this.item, playedMs });
|
|
74
|
+
this.queue = [];
|
|
75
|
+
this.queued = 0;
|
|
76
|
+
this.pos = 0;
|
|
77
|
+
this.buffering = true;
|
|
78
|
+
this.item = null;
|
|
79
|
+
this.played = 0;
|
|
80
|
+
}
|
|
81
|
+
};
|
|
82
|
+
}
|
|
83
|
+
playedMs() {
|
|
84
|
+
return (this.played / this.streamSampleRate) * 1000;
|
|
85
|
+
}
|
|
86
|
+
report(force) {
|
|
87
|
+
if (!force && this.sinceReport < sampleRate / 20) return;
|
|
88
|
+
const rms = this.count === 0 ? 0 : Math.sqrt(this.sumSquares / this.count);
|
|
89
|
+
this.port.postMessage({ type: 'progress', itemId: this.item, playedMs: this.playedMs(), rms });
|
|
90
|
+
this.sinceReport = 0;
|
|
91
|
+
this.sumSquares = 0;
|
|
92
|
+
this.count = 0;
|
|
93
|
+
}
|
|
94
|
+
process(_inputs, outputs) {
|
|
95
|
+
const out = outputs[0][0];
|
|
96
|
+
if (this.buffering) {
|
|
97
|
+
const ready =
|
|
98
|
+
this.queued >= this.prebufferSamples ||
|
|
99
|
+
(this.queued > 0 && currentTime - this.lastChunkAt > this.prebufferIdleSeconds);
|
|
100
|
+
if (!ready) {
|
|
101
|
+
out.fill(0);
|
|
102
|
+
for (let c = 1; c < outputs[0].length; c++) outputs[0][c].fill(0);
|
|
103
|
+
return true;
|
|
104
|
+
}
|
|
105
|
+
this.buffering = false;
|
|
106
|
+
}
|
|
107
|
+
for (let k = 0; k < out.length; k++) {
|
|
108
|
+
const head = this.queue[0];
|
|
109
|
+
if (!head) {
|
|
110
|
+
out[k] = 0;
|
|
111
|
+
if (!this.buffering) {
|
|
112
|
+
this.buffering = true;
|
|
113
|
+
this.report(true);
|
|
114
|
+
this.port.postMessage({ type: 'idle' });
|
|
115
|
+
}
|
|
116
|
+
continue;
|
|
117
|
+
}
|
|
118
|
+
if (head.itemId !== this.item) {
|
|
119
|
+
this.item = head.itemId;
|
|
120
|
+
this.played = 0;
|
|
121
|
+
this.port.postMessage({ type: 'started', itemId: head.itemId });
|
|
122
|
+
}
|
|
123
|
+
const i = Math.floor(this.pos);
|
|
124
|
+
const f = this.pos - i;
|
|
125
|
+
const a = head.samples[i];
|
|
126
|
+
const b = i + 1 < head.samples.length ? head.samples[i + 1] : a;
|
|
127
|
+
const s = a + (b - a) * f;
|
|
128
|
+
out[k] = s;
|
|
129
|
+
this.sumSquares += s * s;
|
|
130
|
+
this.count++;
|
|
131
|
+
this.pos += this.step;
|
|
132
|
+
const consumed = Math.floor(this.pos);
|
|
133
|
+
if (consumed > 0) {
|
|
134
|
+
const taken = Math.min(consumed, head.samples.length);
|
|
135
|
+
this.played += taken;
|
|
136
|
+
this.queued -= taken;
|
|
137
|
+
if (consumed >= head.samples.length) {
|
|
138
|
+
this.queue.shift();
|
|
139
|
+
this.pos = 0;
|
|
140
|
+
} else {
|
|
141
|
+
head.samples = head.samples.subarray(consumed);
|
|
142
|
+
this.pos -= consumed;
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
for (let c = 1; c < outputs[0].length; c++) outputs[0][c].set(out);
|
|
147
|
+
this.sinceReport += out.length;
|
|
148
|
+
this.report(false);
|
|
149
|
+
return true;
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
registerProcessor('alia-voice-playback', AliaVoicePlayback);
|
package/src/lib/api-url.ts
DELETED