@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 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. An app with a strict CSP allows `https://alia.onl` in `script-src` and
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
- Two entry points:
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 live avatar
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`. It draws the live 3D avatar at the icon size under its own
134
- `AgentAvatarProvider` (`ALIA_CHARACTER_RUNTIME_URL`), and plays what Alia is doing.
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.6.0",
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.7.0",
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, type AvatarCharacter } from '@oxy.so/bloom/agent-avatar';
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 = BUSY.has(state);
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 { AliaAvatar } from './AliaAvatar';
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's live avatar as a Bloom icon: pass it as `icon` / `leadingIcon` to a Bloom `Button` and
11
- * the button shows her, reacting as she thinks, works and writes (`useAliaActivity`) — also while
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
- * It mounts its own `AgentAvatarProvider` on `ALIA_CHARACTER_RUNTIME_URL`: an avatar outside the
19
- * chat's provider would ask the host for `/bloom-character/runtime.mjs` and get its HTML. The
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. Reduced motion is Bloom's engine's to honour.
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
- <AliaAvatar size={width ?? height ?? DEFAULT_SIZE} state={state} />
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/api-url';
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`. */
@@ -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/api-url';
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 || API_URL,
134
+ apiUrl: optionsRef.current.apiUrl || ALIA_API_URL,
136
135
  token,
137
136
  clip,
138
137
  language: optionsRef.current.lang ?? defaultSpeechLanguage(),
@@ -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 || API_URL;
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 || API_URL,
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; an app with a
7
- * strict CSP allows it in `script-src` and `connect-src`. A host that serves its own copy
8
- * passes `characterRuntimeUrl`.
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 = 'https://alia.onl/bloom-character/runtime.mjs';
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 inline source turned into a Blob URL: the SDK ships raw
15
- * source and a host bundler would not emit a separate worklet file.
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(captureUrl);
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);
@@ -1,2 +0,0 @@
1
- /** Where Alia's API is: `EXPO_PUBLIC_ALIA_API_URL`, then `https://api.alia.onl`. */
2
- export const ALIA_API_URL = process.env.EXPO_PUBLIC_ALIA_API_URL ?? 'https://api.alia.onl';