@symbiote-native/speech 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 A. Prokopenko
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 ADDED
@@ -0,0 +1,131 @@
1
+ # @symbiote-native/speech
2
+
3
+ Read text aloud with the device's text-to-speech engine, with a choice of voice, language, pitch and
4
+ rate. One API for every [SymbioteNative](../../README.md) adapter (React, Vue, Svelte, Solid and
5
+ Angular).
6
+
7
+ It wraps [`expo-speech`](https://github.com/expo/expo/tree/main/packages/expo-speech) the same way
8
+ [`@symbiote-native/print`](../print) wraps its upstream: `expo-modules-core` is a direct
9
+ dependency and the upstream JS is hand-ported into `core/`. See the `symbiote-expo-native-module`
10
+ project skill for the full mechanism.
11
+
12
+ ## Install
13
+
14
+ **New app:**
15
+
16
+ ```bash
17
+ npx @symbiote-native/cli new my-app --speech
18
+ ```
19
+
20
+ **Existing SymbioteNative app:**
21
+
22
+ ```bash
23
+ npx @symbiote-native/cli add --speech
24
+ ```
25
+
26
+ Either way: installs `@symbiote-native/speech` and wires the native autolinking automatically -
27
+ see [`@symbiote-native/cli`](../cli).
28
+
29
+ <details>
30
+ <summary>Manual install (no CLI - installing and wiring native autolinking by hand)</summary>
31
+
32
+ ```bash
33
+ npm install @symbiote-native/speech
34
+ ```
35
+
36
+ `expo-speech` and `expo-modules-core` come along as regular, pinned dependencies - never install
37
+ either yourself, and never add the `expo` meta-package to this project.
38
+
39
+ ### Required one-time step: native autolinking wiring
40
+
41
+ Same one-time app wiring every `expo-modules-core` package shares - see
42
+ [`@symbiote-native/print`'s README](../print/README.md#required-one-time-step-native-autolinking-wiring)
43
+ for the full table; nothing package-specific here.
44
+
45
+ ### No app-level permission, no config plugin
46
+
47
+ - **No runtime permission request from this package.** Android needs a `<queries>` entry
48
+ declaring the `android.intent.action.TTS_SERVICE` intent to see installed TTS engines on
49
+ API 30+ - `expo-speech`'s own bundled `AndroidManifest.xml` already carries it, and Android's
50
+ manifest merger folds it into the app automatically. No `native-link.json` entry exists for
51
+ this (there is no such field, and none is needed).
52
+ - **No config plugin.** Upstream ships none.
53
+
54
+ </details>
55
+
56
+ ## Shape
57
+
58
+ ```
59
+ src/core/ the whole API: speak, getAvailableVoicesAsync, isSpeakingAsync, stop,
60
+ pause, resume, maxSpeechInputLength, VoiceQuality. native-module.ts
61
+ resolves ExpoSpeech through expo-modules-core's requireNativeModule.
62
+ src/angular/ @symbiote-native/speech/angular
63
+ ```
64
+
65
+ `./react`, `./vue`, `./svelte`, and `./solid` are `exports`-map aliases straight onto `src/core/` -
66
+ every export here is a stateless function or constant, nothing to wrap in a hook/composable.
67
+
68
+ ## Use it
69
+
70
+ ```ts
71
+ import { speak, stop, isSpeakingAsync } from '@symbiote-native/speech';
72
+
73
+ speak('Hello world', {
74
+ language: 'en-US',
75
+ onDone: () => console.log('done'),
76
+ });
77
+
78
+ if (await isSpeakingAsync()) {
79
+ await stop();
80
+ }
81
+ ```
82
+
83
+ ## API
84
+
85
+ | Export | Signature | Notes |
86
+ | -------------------------- | -------------------------------------------------------- | ----------------------------------------------- |
87
+ | `speak` | `(text: string, options?: ISpeechOptions) => void` | Queues onto any in-flight utterance. |
88
+ | `getAvailableVoicesAsync` | `() => Promise<IVoice[]>` | Throws `UnavailabilityError` if unsupported. |
89
+ | `isSpeakingAsync` | `() => Promise<boolean>` | `true` even while paused. |
90
+ | `stop` | `() => Promise<void>` | Interrupts and clears the whole queue. |
91
+ | `pause` / `resume` | `() => Promise<void>` | iOS only - throws `UnavailabilityError` on Android. |
92
+ | `maxSpeechInputLength` | `number` | `Number.MAX_VALUE` on iOS. |
93
+ | `VoiceQuality` | `{ Default, Enhanced }` | Enum on `IVoice.quality`. |
94
+
95
+ ## Notes
96
+
97
+ - **No sound in iOS silent mode.** On a physical iPhone `speak` is silent while the device is in
98
+ silent mode, so it can look like the call did nothing.
99
+ - **`speak` queues, it does not interrupt.** Call `stop()` first to replace what is being said.
100
+ - **Android rejects text longer than `maxSpeechInputLength`.** Split long text into sentences and
101
+ queue them.
102
+ - **Web-only fields dropped.** Upstream's `Speech.types.ts` also carries `WebVoice`,
103
+ `SpeechEventCallback` (a raw DOM `SpeechSynthesisEvent` callback shape), `_voiceIndex`, and
104
+ `onMark`/`onPause`/`onResume` - all reachable only through `expo-speech`'s separate web
105
+ implementation. This package only wraps the apple/android native modules (`expo-speech`'s own
106
+ `expo-module.config.json` lists no `"web"` platform either), so none of it applies here.
107
+
108
+ ## Common questions
109
+
110
+ - **No sound on iOS.** Check silent mode first (a physical iPhone is silent then).
111
+ - **`voice` ignored, short voice list.** Pass an `identifier` from `getAvailableVoicesAsync()`
112
+ exactly; the voices installed differ by device and iOS version.
113
+ - **Non-English speech wrong on Android.** Set `language` to a BCP 47 code and install that voice in
114
+ the device's text-to-speech settings.
115
+ - **`onDone` missing after pause/resume.** Pause reports `onStopped`; track state yourself.
116
+ - **Long text rejected.** Android limits input to `maxSpeechInputLength`; queue sentences.
117
+
118
+ Sources: [Expo docs: Speech](https://docs.expo.dev/versions/latest/sdk/speech/),
119
+ [expo/expo#10827](https://github.com/expo/expo/issues/10827),
120
+ [expo/expo#12654](https://github.com/expo/expo/issues/12654),
121
+ [expo/expo#7260](https://github.com/expo/expo/issues/7260).
122
+
123
+ ## Test it
124
+
125
+ ```bash
126
+ pnpm vitest run packages/speech
127
+ ```
128
+
129
+ Upstream ships no test suite of its own (`expo-speech`'s `src/` has no `__tests__`), so nothing to
130
+ port - every test here is net-new coverage of the ported logic (the callback registry, per-id
131
+ event routing, the listen/unlisten toggle, and every `UnavailabilityError` branch).
@@ -0,0 +1 @@
1
+ export * from '../core';
@@ -0,0 +1,2 @@
1
+ // No per-instance state or event stream to wrap in an injectable service - plain re-export.
2
+ export * from '../core/index.js';
@@ -0,0 +1,3 @@
1
+ export { getAvailableVoicesAsync, isSpeakingAsync, maxSpeechInputLength, pause, resume, speak, stop, } from './speech';
2
+ export { VoiceQuality } from './types';
3
+ export type { INativeBoundaryEvent, INativeBoundaryEventCallback, ISpeechOptions, IVoice, } from './types';
@@ -0,0 +1,2 @@
1
+ export { getAvailableVoicesAsync, isSpeakingAsync, maxSpeechInputLength, pause, resume, speak, stop, } from './speech.js';
2
+ export { VoiceQuality } from './types.js';
@@ -0,0 +1,31 @@
1
+ import { NativeModule } from 'expo-modules-core';
2
+ import type { ISpeechOptions, IVoice } from './types';
3
+ type ISpeechEventParams = {
4
+ id: string;
5
+ };
6
+ export type ISpeechEventsMap = {
7
+ 'Exponent.speakingStarted': (params: ISpeechEventParams) => void;
8
+ 'Exponent.speakingWillSayNextString': (params: ISpeechEventParams & {
9
+ charIndex: number;
10
+ charLength: number;
11
+ }) => void;
12
+ 'Exponent.speakingDone': (params: ISpeechEventParams) => void;
13
+ 'Exponent.speakingStopped': (params: ISpeechEventParams) => void;
14
+ /** Android only supplies `error`; iOS never emits this event at all. */
15
+ 'Exponent.speakingError': (params: ISpeechEventParams & {
16
+ error?: string;
17
+ }) => void;
18
+ };
19
+ export declare class NativeSpeechModule extends NativeModule<ISpeechEventsMap> {
20
+ maxSpeechInputLength?: number;
21
+ speak(utteranceId: string, text: string, options: ISpeechOptions): Promise<void>;
22
+ getVoices?(): Promise<IVoice[]>;
23
+ isSpeaking(): Promise<boolean>;
24
+ stop(): Promise<void>;
25
+ /** iOS only; absent on Android's native module. */
26
+ pause?(): Promise<void>;
27
+ /** iOS only; absent on Android's native module. */
28
+ resume?(): Promise<void>;
29
+ }
30
+ export declare const expoSpeech: NativeSpeechModule;
31
+ export {};
@@ -0,0 +1,3 @@
1
+ import { NativeModule, requireNativeModule } from 'expo-modules-core';
2
+ const EXPO_SPEECH_MODULE_NAME = 'ExpoSpeech';
3
+ export const expoSpeech = requireNativeModule(EXPO_SPEECH_MODULE_NAME);
@@ -0,0 +1,14 @@
1
+ import type { ISpeechOptions, IVoice } from './types';
2
+ /** Adds `text` to the speech queue. Calling this while another utterance is speaking queues it. */
3
+ export declare function speak(text: string, options?: ISpeechOptions): void;
4
+ export declare function getAvailableVoicesAsync(): Promise<IVoice[]>;
5
+ /** `true` even while paused. */
6
+ export declare function isSpeakingAsync(): Promise<boolean>;
7
+ /** Interrupts current speech and clears the queue. */
8
+ export declare function stop(): Promise<void>;
9
+ /** iOS only; throws `UnavailabilityError` on Android. */
10
+ export declare function pause(): Promise<void>;
11
+ /** iOS only; throws `UnavailabilityError` on Android. */
12
+ export declare function resume(): Promise<void>;
13
+ /** Platform-dependent; iOS returns `Number.MAX_VALUE`. */
14
+ export declare const maxSpeechInputLength: number;
@@ -0,0 +1,90 @@
1
+ import { UnavailabilityError } from 'expo-modules-core';
2
+ import { expoSpeech } from './native-module.js';
3
+ const callbacksById = {};
4
+ let nextCallbackId = 1;
5
+ let hasRegisteredListeners = false;
6
+ function makeCallbackId() {
7
+ return String(nextCallbackId++);
8
+ }
9
+ function unregisterListenersIfNoneNeeded() {
10
+ if (Object.keys(callbacksById).length > 0)
11
+ return;
12
+ removeSpeakingListener('Exponent.speakingStarted');
13
+ removeSpeakingListener('Exponent.speakingWillSayNextString');
14
+ removeSpeakingListener('Exponent.speakingDone');
15
+ removeSpeakingListener('Exponent.speakingStopped');
16
+ removeSpeakingListener('Exponent.speakingError');
17
+ hasRegisteredListeners = false;
18
+ }
19
+ function registerListenersIfNeeded() {
20
+ if (hasRegisteredListeners)
21
+ return;
22
+ hasRegisteredListeners = true;
23
+ setSpeakingListener('Exponent.speakingStarted', ({ id }) => {
24
+ callbacksById[id]?.onStart?.();
25
+ });
26
+ setSpeakingListener('Exponent.speakingWillSayNextString', ({ id, charIndex, charLength }) => {
27
+ callbacksById[id]?.onBoundary?.({ charIndex, charLength });
28
+ });
29
+ setSpeakingListener('Exponent.speakingDone', ({ id }) => {
30
+ callbacksById[id]?.onDone?.();
31
+ delete callbacksById[id];
32
+ unregisterListenersIfNoneNeeded();
33
+ });
34
+ setSpeakingListener('Exponent.speakingStopped', ({ id }) => {
35
+ callbacksById[id]?.onStopped?.();
36
+ delete callbacksById[id];
37
+ unregisterListenersIfNoneNeeded();
38
+ });
39
+ setSpeakingListener('Exponent.speakingError', ({ id, error }) => {
40
+ callbacksById[id]?.onError?.(new Error(error));
41
+ delete callbacksById[id];
42
+ unregisterListenersIfNoneNeeded();
43
+ });
44
+ }
45
+ function setSpeakingListener(eventName, listener) {
46
+ if (expoSpeech.listenerCount(eventName) > 0) {
47
+ expoSpeech.removeAllListeners(eventName);
48
+ }
49
+ expoSpeech.addListener(eventName, listener);
50
+ }
51
+ function removeSpeakingListener(eventName) {
52
+ expoSpeech.removeAllListeners(eventName);
53
+ }
54
+ /** Adds `text` to the speech queue. Calling this while another utterance is speaking queues it. */
55
+ export function speak(text, options = {}) {
56
+ const id = makeCallbackId();
57
+ callbacksById[id] = options;
58
+ registerListenersIfNeeded();
59
+ expoSpeech.speak(id, text, options);
60
+ }
61
+ export async function getAvailableVoicesAsync() {
62
+ if (!expoSpeech.getVoices) {
63
+ throw new UnavailabilityError('Speech', 'getVoices');
64
+ }
65
+ return expoSpeech.getVoices();
66
+ }
67
+ /** `true` even while paused. */
68
+ export async function isSpeakingAsync() {
69
+ return expoSpeech.isSpeaking();
70
+ }
71
+ /** Interrupts current speech and clears the queue. */
72
+ export async function stop() {
73
+ return expoSpeech.stop();
74
+ }
75
+ /** iOS only; throws `UnavailabilityError` on Android. */
76
+ export async function pause() {
77
+ if (!expoSpeech.pause) {
78
+ throw new UnavailabilityError('Speech', 'pause');
79
+ }
80
+ return expoSpeech.pause();
81
+ }
82
+ /** iOS only; throws `UnavailabilityError` on Android. */
83
+ export async function resume() {
84
+ if (!expoSpeech.resume) {
85
+ throw new UnavailabilityError('Speech', 'resume');
86
+ }
87
+ return expoSpeech.resume();
88
+ }
89
+ /** Platform-dependent; iOS returns `Number.MAX_VALUE`. */
90
+ export const maxSpeechInputLength = expoSpeech.maxSpeechInputLength ?? Number.MAX_VALUE;
@@ -0,0 +1,34 @@
1
+ export declare enum VoiceQuality {
2
+ Default = "Default",
3
+ Enhanced = "Enhanced"
4
+ }
5
+ export type IVoice = {
6
+ identifier: string;
7
+ name: string;
8
+ quality: VoiceQuality;
9
+ language: string;
10
+ };
11
+ export type INativeBoundaryEvent = {
12
+ charIndex: number;
13
+ charLength: number;
14
+ };
15
+ export type INativeBoundaryEventCallback = (event: INativeBoundaryEvent) => void;
16
+ export type ISpeechOptions = {
17
+ /** IETF BCP 47 language code. */
18
+ language?: string;
19
+ /** `1.0` is the normal pitch. */
20
+ pitch?: number;
21
+ /** `1.0` is the normal rate. */
22
+ rate?: number;
23
+ /** iOS only — `false` lets the system manage a separate audio session for the speech. */
24
+ useApplicationAudioSession?: boolean;
25
+ onStart?: () => void;
26
+ /** Invoked when speaking is stopped by calling `stop()`. */
27
+ onStopped?: () => void;
28
+ onDone?: () => void;
29
+ onError?: (error: Error) => void;
30
+ /** `0.0` (muted) to `1.0` (max volume). Default `1.0`. */
31
+ volume?: number;
32
+ voice?: string;
33
+ onBoundary?: INativeBoundaryEventCallback | null;
34
+ };
@@ -0,0 +1,5 @@
1
+ export var VoiceQuality;
2
+ (function (VoiceQuality) {
3
+ VoiceQuality["Default"] = "Default";
4
+ VoiceQuality["Enhanced"] = "Enhanced";
5
+ })(VoiceQuality || (VoiceQuality = {}));
@@ -0,0 +1 @@
1
+ export * from '../core';
@@ -0,0 +1,3 @@
1
+ // No per-instance state or event stream to wrap in an injectable service - plain re-export.
2
+ export * from '../core';
3
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/angular/index.ts"],"names":[],"mappings":"AAAA,4FAA4F;AAC5F,cAAc,SAAS,CAAC"}
@@ -0,0 +1,3 @@
1
+ export { getAvailableVoicesAsync, isSpeakingAsync, maxSpeechInputLength, pause, resume, speak, stop, } from './speech';
2
+ export { VoiceQuality } from './types';
3
+ export type { INativeBoundaryEvent, INativeBoundaryEventCallback, ISpeechOptions, IVoice, } from './types';
@@ -0,0 +1,3 @@
1
+ export { getAvailableVoicesAsync, isSpeakingAsync, maxSpeechInputLength, pause, resume, speak, stop, } from './speech';
2
+ export { VoiceQuality } from './types';
3
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/core/index.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,uBAAuB,EACvB,eAAe,EACf,oBAAoB,EACpB,KAAK,EACL,MAAM,EACN,KAAK,EACL,IAAI,GACL,MAAM,UAAU,CAAC;AAClB,OAAO,EAAE,YAAY,EAAE,MAAM,SAAS,CAAC"}
@@ -0,0 +1,31 @@
1
+ import { NativeModule } from 'expo-modules-core';
2
+ import type { ISpeechOptions, IVoice } from './types';
3
+ type ISpeechEventParams = {
4
+ id: string;
5
+ };
6
+ export type ISpeechEventsMap = {
7
+ 'Exponent.speakingStarted': (params: ISpeechEventParams) => void;
8
+ 'Exponent.speakingWillSayNextString': (params: ISpeechEventParams & {
9
+ charIndex: number;
10
+ charLength: number;
11
+ }) => void;
12
+ 'Exponent.speakingDone': (params: ISpeechEventParams) => void;
13
+ 'Exponent.speakingStopped': (params: ISpeechEventParams) => void;
14
+ /** Android only supplies `error`; iOS never emits this event at all. */
15
+ 'Exponent.speakingError': (params: ISpeechEventParams & {
16
+ error?: string;
17
+ }) => void;
18
+ };
19
+ export declare class NativeSpeechModule extends NativeModule<ISpeechEventsMap> {
20
+ maxSpeechInputLength?: number;
21
+ speak(utteranceId: string, text: string, options: ISpeechOptions): Promise<void>;
22
+ getVoices?(): Promise<IVoice[]>;
23
+ isSpeaking(): Promise<boolean>;
24
+ stop(): Promise<void>;
25
+ /** iOS only; absent on Android's native module. */
26
+ pause?(): Promise<void>;
27
+ /** iOS only; absent on Android's native module. */
28
+ resume?(): Promise<void>;
29
+ }
30
+ export declare const expoSpeech: NativeSpeechModule;
31
+ export {};
@@ -0,0 +1,4 @@
1
+ import { NativeModule, requireNativeModule } from 'expo-modules-core';
2
+ const EXPO_SPEECH_MODULE_NAME = 'ExpoSpeech';
3
+ export const expoSpeech = requireNativeModule(EXPO_SPEECH_MODULE_NAME);
4
+ //# sourceMappingURL=native-module.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"native-module.js","sourceRoot":"","sources":["../../src/core/native-module.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,YAAY,EAAE,mBAAmB,EAAE,MAAM,mBAAmB,CAAC;AAGtE,MAAM,uBAAuB,GAAG,YAAY,CAAC;AAiC7C,MAAM,CAAC,MAAM,UAAU,GAAG,mBAAmB,CAC3C,uBAAuB,CACxB,CAAC"}
@@ -0,0 +1,14 @@
1
+ import type { ISpeechOptions, IVoice } from './types';
2
+ /** Adds `text` to the speech queue. Calling this while another utterance is speaking queues it. */
3
+ export declare function speak(text: string, options?: ISpeechOptions): void;
4
+ export declare function getAvailableVoicesAsync(): Promise<IVoice[]>;
5
+ /** `true` even while paused. */
6
+ export declare function isSpeakingAsync(): Promise<boolean>;
7
+ /** Interrupts current speech and clears the queue. */
8
+ export declare function stop(): Promise<void>;
9
+ /** iOS only; throws `UnavailabilityError` on Android. */
10
+ export declare function pause(): Promise<void>;
11
+ /** iOS only; throws `UnavailabilityError` on Android. */
12
+ export declare function resume(): Promise<void>;
13
+ /** Platform-dependent; iOS returns `Number.MAX_VALUE`. */
14
+ export declare const maxSpeechInputLength: number;
@@ -0,0 +1,91 @@
1
+ import { UnavailabilityError } from 'expo-modules-core';
2
+ import { expoSpeech } from './native-module';
3
+ const callbacksById = {};
4
+ let nextCallbackId = 1;
5
+ let hasRegisteredListeners = false;
6
+ function makeCallbackId() {
7
+ return String(nextCallbackId++);
8
+ }
9
+ function unregisterListenersIfNoneNeeded() {
10
+ if (Object.keys(callbacksById).length > 0)
11
+ return;
12
+ removeSpeakingListener('Exponent.speakingStarted');
13
+ removeSpeakingListener('Exponent.speakingWillSayNextString');
14
+ removeSpeakingListener('Exponent.speakingDone');
15
+ removeSpeakingListener('Exponent.speakingStopped');
16
+ removeSpeakingListener('Exponent.speakingError');
17
+ hasRegisteredListeners = false;
18
+ }
19
+ function registerListenersIfNeeded() {
20
+ if (hasRegisteredListeners)
21
+ return;
22
+ hasRegisteredListeners = true;
23
+ setSpeakingListener('Exponent.speakingStarted', ({ id }) => {
24
+ callbacksById[id]?.onStart?.();
25
+ });
26
+ setSpeakingListener('Exponent.speakingWillSayNextString', ({ id, charIndex, charLength }) => {
27
+ callbacksById[id]?.onBoundary?.({ charIndex, charLength });
28
+ });
29
+ setSpeakingListener('Exponent.speakingDone', ({ id }) => {
30
+ callbacksById[id]?.onDone?.();
31
+ delete callbacksById[id];
32
+ unregisterListenersIfNoneNeeded();
33
+ });
34
+ setSpeakingListener('Exponent.speakingStopped', ({ id }) => {
35
+ callbacksById[id]?.onStopped?.();
36
+ delete callbacksById[id];
37
+ unregisterListenersIfNoneNeeded();
38
+ });
39
+ setSpeakingListener('Exponent.speakingError', ({ id, error }) => {
40
+ callbacksById[id]?.onError?.(new Error(error));
41
+ delete callbacksById[id];
42
+ unregisterListenersIfNoneNeeded();
43
+ });
44
+ }
45
+ function setSpeakingListener(eventName, listener) {
46
+ if (expoSpeech.listenerCount(eventName) > 0) {
47
+ expoSpeech.removeAllListeners(eventName);
48
+ }
49
+ expoSpeech.addListener(eventName, listener);
50
+ }
51
+ function removeSpeakingListener(eventName) {
52
+ expoSpeech.removeAllListeners(eventName);
53
+ }
54
+ /** Adds `text` to the speech queue. Calling this while another utterance is speaking queues it. */
55
+ export function speak(text, options = {}) {
56
+ const id = makeCallbackId();
57
+ callbacksById[id] = options;
58
+ registerListenersIfNeeded();
59
+ expoSpeech.speak(id, text, options);
60
+ }
61
+ export async function getAvailableVoicesAsync() {
62
+ if (!expoSpeech.getVoices) {
63
+ throw new UnavailabilityError('Speech', 'getVoices');
64
+ }
65
+ return expoSpeech.getVoices();
66
+ }
67
+ /** `true` even while paused. */
68
+ export async function isSpeakingAsync() {
69
+ return expoSpeech.isSpeaking();
70
+ }
71
+ /** Interrupts current speech and clears the queue. */
72
+ export async function stop() {
73
+ return expoSpeech.stop();
74
+ }
75
+ /** iOS only; throws `UnavailabilityError` on Android. */
76
+ export async function pause() {
77
+ if (!expoSpeech.pause) {
78
+ throw new UnavailabilityError('Speech', 'pause');
79
+ }
80
+ return expoSpeech.pause();
81
+ }
82
+ /** iOS only; throws `UnavailabilityError` on Android. */
83
+ export async function resume() {
84
+ if (!expoSpeech.resume) {
85
+ throw new UnavailabilityError('Speech', 'resume');
86
+ }
87
+ return expoSpeech.resume();
88
+ }
89
+ /** Platform-dependent; iOS returns `Number.MAX_VALUE`. */
90
+ export const maxSpeechInputLength = expoSpeech.maxSpeechInputLength ?? Number.MAX_VALUE;
91
+ //# sourceMappingURL=speech.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"speech.js","sourceRoot":"","sources":["../../src/core/speech.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,34 @@
1
+ export declare enum VoiceQuality {
2
+ Default = "Default",
3
+ Enhanced = "Enhanced"
4
+ }
5
+ export type IVoice = {
6
+ identifier: string;
7
+ name: string;
8
+ quality: VoiceQuality;
9
+ language: string;
10
+ };
11
+ export type INativeBoundaryEvent = {
12
+ charIndex: number;
13
+ charLength: number;
14
+ };
15
+ export type INativeBoundaryEventCallback = (event: INativeBoundaryEvent) => void;
16
+ export type ISpeechOptions = {
17
+ /** IETF BCP 47 language code. */
18
+ language?: string;
19
+ /** `1.0` is the normal pitch. */
20
+ pitch?: number;
21
+ /** `1.0` is the normal rate. */
22
+ rate?: number;
23
+ /** iOS only — `false` lets the system manage a separate audio session for the speech. */
24
+ useApplicationAudioSession?: boolean;
25
+ onStart?: () => void;
26
+ /** Invoked when speaking is stopped by calling `stop()`. */
27
+ onStopped?: () => void;
28
+ onDone?: () => void;
29
+ onError?: (error: Error) => void;
30
+ /** `0.0` (muted) to `1.0` (max volume). Default `1.0`. */
31
+ volume?: number;
32
+ voice?: string;
33
+ onBoundary?: INativeBoundaryEventCallback | null;
34
+ };
@@ -0,0 +1,6 @@
1
+ export var VoiceQuality;
2
+ (function (VoiceQuality) {
3
+ VoiceQuality["Default"] = "Default";
4
+ VoiceQuality["Enhanced"] = "Enhanced";
5
+ })(VoiceQuality || (VoiceQuality = {}));
6
+ //# sourceMappingURL=types.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"types.js","sourceRoot":"","sources":["../../src/core/types.ts"],"names":[],"mappings":"AAAA,MAAM,CAAN,IAAY,YAGX;AAHD,WAAY,YAAY;IACtB,mCAAmB,CAAA;IACnB,qCAAqB,CAAA;AACvB,CAAC,EAHW,YAAY,KAAZ,YAAY,QAGvB"}
@@ -0,0 +1,12 @@
1
+ {
2
+ "android": {
3
+ "gradleProjectName": "expo-speech",
4
+ "modules": [
5
+ {
6
+ "importPath": "expo.modules.speech.SpeechModule",
7
+ "className": "SpeechModule",
8
+ "nativeName": "ExpoSpeech"
9
+ }
10
+ ]
11
+ }
12
+ }
package/package.json ADDED
@@ -0,0 +1,150 @@
1
+ {
2
+ "name": "@symbiote-native/speech",
3
+ "version": "0.1.0",
4
+ "description": "expo-speech wrapped for SymbioteNative — one framework-agnostic core, built once and reachable from the React, Vue, Svelte, Solid, and Angular adapters. Text-to-speech.",
5
+ "license": "MIT",
6
+ "keywords": [
7
+ "react-native",
8
+ "symbiote-native",
9
+ "expo-speech",
10
+ "react",
11
+ "vue",
12
+ "svelte",
13
+ "solid",
14
+ "angular",
15
+ "speech",
16
+ "tts"
17
+ ],
18
+ "repository": {
19
+ "type": "git",
20
+ "url": "git+https://github.com/OneEyed1366/symbiote-native.git",
21
+ "directory": "packages/speech"
22
+ },
23
+ "homepage": "https://github.com/OneEyed1366/symbiote-native/tree/master/packages/speech#readme",
24
+ "bugs": {
25
+ "url": "https://github.com/OneEyed1366/symbiote-native/issues"
26
+ },
27
+ "author": "Andrey Prokopenko <psevdoproger@gmail.com>",
28
+ "type": "module",
29
+ "main": "./build/core/index.js",
30
+ "module": "./build/core/index.js",
31
+ "types": "./build/core/index.d.ts",
32
+ "exports": {
33
+ ".": {
34
+ "types": "./build/core/index.d.ts",
35
+ "default": "./build/core/index.js"
36
+ },
37
+ "./vue": {
38
+ "types": "./build/core/index.d.ts",
39
+ "default": "./build/core/index.js"
40
+ },
41
+ "./react": {
42
+ "types": "./build/core/index.d.ts",
43
+ "default": "./build/core/index.js"
44
+ },
45
+ "./svelte": {
46
+ "types": "./build/core/index.d.ts",
47
+ "default": "./build/core/index.js"
48
+ },
49
+ "./solid": {
50
+ "types": "./build/core/index.d.ts",
51
+ "default": "./build/core/index.js"
52
+ },
53
+ "./angular": {
54
+ "types": "./build-ngc/angular/index.d.ts",
55
+ "react-native": "./build-ngc/angular/index.js",
56
+ "default": "./src/angular/index.ts"
57
+ }
58
+ },
59
+ "files": [
60
+ "src",
61
+ "build",
62
+ "build-ngc",
63
+ "native-link.json",
64
+ "!src/**/*.test.*",
65
+ "!src/**/*.spec.*",
66
+ "!src/**/*.detox.*"
67
+ ],
68
+ "publishConfig": {
69
+ "access": "public"
70
+ },
71
+ "dependencies": {
72
+ "expo-speech": "57.0.3",
73
+ "expo-modules-core": "57.0.20"
74
+ },
75
+ "peerDependencies": {
76
+ "@angular/core": ">=20",
77
+ "@vue/runtime-core": "^3.5.13",
78
+ "react": ">=19.0.0",
79
+ "react-native": ">=0.86",
80
+ "solid-js": ">=1.9.0",
81
+ "svelte": ">=5.56.0",
82
+ "vue": ">=3.5.0",
83
+ "@symbiote-native/react": "^3.1.0",
84
+ "@symbiote-native/engine": "^1.4.0",
85
+ "@symbiote-native/solid": "^3.0.5",
86
+ "@symbiote-native/angular": "^3.1.3",
87
+ "@symbiote-native/svelte": "^3.0.5",
88
+ "@symbiote-native/vue": "^3.1.0"
89
+ },
90
+ "peerDependenciesMeta": {
91
+ "@symbiote-native/angular": {
92
+ "optional": true
93
+ },
94
+ "@symbiote-native/react": {
95
+ "optional": true
96
+ },
97
+ "@symbiote-native/solid": {
98
+ "optional": true
99
+ },
100
+ "@symbiote-native/svelte": {
101
+ "optional": true
102
+ },
103
+ "@symbiote-native/vue": {
104
+ "optional": true
105
+ },
106
+ "@angular/core": {
107
+ "optional": true
108
+ },
109
+ "@vue/runtime-core": {
110
+ "optional": true
111
+ },
112
+ "react": {
113
+ "optional": true
114
+ },
115
+ "solid-js": {
116
+ "optional": true
117
+ },
118
+ "svelte": {
119
+ "optional": true
120
+ },
121
+ "vue": {
122
+ "optional": true
123
+ }
124
+ },
125
+ "devDependencies": {
126
+ "@angular/compiler": "~22.0.8",
127
+ "@angular/compiler-cli": "~22.0.8",
128
+ "@angular/core": "~22.0.8",
129
+ "@types/node": "^26.0.0",
130
+ "@types/react": "^19.2.0",
131
+ "@vue/runtime-core": "^3.5.13",
132
+ "react": "19.2.3",
133
+ "solid-js": "^1.9.14",
134
+ "svelte": "^5.56.0",
135
+ "typescript": "~6.0.0",
136
+ "@symbiote-native/angular": "3.1.3",
137
+ "@symbiote-native/react": "3.1.0",
138
+ "@symbiote-native/engine": "1.4.0",
139
+ "@symbiote-native/svelte": "3.0.5",
140
+ "@symbiote-native/solid": "3.0.5",
141
+ "@symbiote-native/test-utils": "0.4.5",
142
+ "@symbiote-native/vue": "3.1.0"
143
+ },
144
+ "scripts": {
145
+ "typecheck": "tsc --build",
146
+ "clean": "rm -rf build-ngc",
147
+ "ng:build": "pnpm run clean && ngc -p tsconfig.angular.json",
148
+ "format": "prettier --write \"src/**/*.{ts,tsx}\""
149
+ }
150
+ }
@@ -0,0 +1,2 @@
1
+ // No per-instance state or event stream to wrap in an injectable service - plain re-export.
2
+ export * from '../core';
@@ -0,0 +1,16 @@
1
+ export {
2
+ getAvailableVoicesAsync,
3
+ isSpeakingAsync,
4
+ maxSpeechInputLength,
5
+ pause,
6
+ resume,
7
+ speak,
8
+ stop,
9
+ } from './speech';
10
+ export { VoiceQuality } from './types';
11
+ export type {
12
+ INativeBoundaryEvent,
13
+ INativeBoundaryEventCallback,
14
+ ISpeechOptions,
15
+ IVoice,
16
+ } from './types';
@@ -0,0 +1,39 @@
1
+ import { NativeModule, requireNativeModule } from 'expo-modules-core';
2
+ import type { ISpeechOptions, IVoice } from './types';
3
+
4
+ const EXPO_SPEECH_MODULE_NAME = 'ExpoSpeech';
5
+
6
+ type ISpeechEventParams = { id: string };
7
+
8
+ export type ISpeechEventsMap = {
9
+ 'Exponent.speakingStarted': (params: ISpeechEventParams) => void;
10
+ 'Exponent.speakingWillSayNextString': (
11
+ params: ISpeechEventParams & { charIndex: number; charLength: number },
12
+ ) => void;
13
+ 'Exponent.speakingDone': (params: ISpeechEventParams) => void;
14
+ 'Exponent.speakingStopped': (params: ISpeechEventParams) => void;
15
+ /** Android only supplies `error`; iOS never emits this event at all. */
16
+ 'Exponent.speakingError': (
17
+ params: ISpeechEventParams & { error?: string },
18
+ ) => void;
19
+ };
20
+
21
+ export declare class NativeSpeechModule extends NativeModule<ISpeechEventsMap> {
22
+ maxSpeechInputLength?: number;
23
+ speak(
24
+ utteranceId: string,
25
+ text: string,
26
+ options: ISpeechOptions,
27
+ ): Promise<void>;
28
+ getVoices?(): Promise<IVoice[]>;
29
+ isSpeaking(): Promise<boolean>;
30
+ stop(): Promise<void>;
31
+ /** iOS only; absent on Android's native module. */
32
+ pause?(): Promise<void>;
33
+ /** iOS only; absent on Android's native module. */
34
+ resume?(): Promise<void>;
35
+ }
36
+
37
+ export const expoSpeech = requireNativeModule<NativeSpeechModule>(
38
+ EXPO_SPEECH_MODULE_NAME,
39
+ );
@@ -0,0 +1,111 @@
1
+ import { UnavailabilityError } from 'expo-modules-core';
2
+ import { expoSpeech } from './native-module';
3
+ import type { ISpeechEventsMap } from './native-module';
4
+ import type { ISpeechOptions, IVoice } from './types';
5
+
6
+ const callbacksById: Record<string, ISpeechOptions> = {};
7
+ let nextCallbackId = 1;
8
+ let hasRegisteredListeners = false;
9
+
10
+ function makeCallbackId(): string {
11
+ return String(nextCallbackId++);
12
+ }
13
+
14
+ function unregisterListenersIfNoneNeeded(): void {
15
+ if (Object.keys(callbacksById).length > 0) return;
16
+ removeSpeakingListener('Exponent.speakingStarted');
17
+ removeSpeakingListener('Exponent.speakingWillSayNextString');
18
+ removeSpeakingListener('Exponent.speakingDone');
19
+ removeSpeakingListener('Exponent.speakingStopped');
20
+ removeSpeakingListener('Exponent.speakingError');
21
+ hasRegisteredListeners = false;
22
+ }
23
+
24
+ function registerListenersIfNeeded(): void {
25
+ if (hasRegisteredListeners) return;
26
+ hasRegisteredListeners = true;
27
+
28
+ setSpeakingListener('Exponent.speakingStarted', ({ id }) => {
29
+ callbacksById[id]?.onStart?.();
30
+ });
31
+ setSpeakingListener(
32
+ 'Exponent.speakingWillSayNextString',
33
+ ({ id, charIndex, charLength }) => {
34
+ callbacksById[id]?.onBoundary?.({ charIndex, charLength });
35
+ },
36
+ );
37
+ setSpeakingListener('Exponent.speakingDone', ({ id }) => {
38
+ callbacksById[id]?.onDone?.();
39
+ delete callbacksById[id];
40
+ unregisterListenersIfNoneNeeded();
41
+ });
42
+ setSpeakingListener('Exponent.speakingStopped', ({ id }) => {
43
+ callbacksById[id]?.onStopped?.();
44
+ delete callbacksById[id];
45
+ unregisterListenersIfNoneNeeded();
46
+ });
47
+ setSpeakingListener('Exponent.speakingError', ({ id, error }) => {
48
+ callbacksById[id]?.onError?.(new Error(error));
49
+ delete callbacksById[id];
50
+ unregisterListenersIfNoneNeeded();
51
+ });
52
+ }
53
+
54
+ function setSpeakingListener<TName extends keyof ISpeechEventsMap>(
55
+ eventName: TName,
56
+ listener: ISpeechEventsMap[TName],
57
+ ): void {
58
+ if (expoSpeech.listenerCount(eventName) > 0) {
59
+ expoSpeech.removeAllListeners(eventName);
60
+ }
61
+ expoSpeech.addListener(eventName, listener);
62
+ }
63
+
64
+ function removeSpeakingListener(eventName: keyof ISpeechEventsMap): void {
65
+ expoSpeech.removeAllListeners(eventName);
66
+ }
67
+
68
+ /** Adds `text` to the speech queue. Calling this while another utterance is speaking queues it. */
69
+ export function speak(text: string, options: ISpeechOptions = {}): void {
70
+ const id = makeCallbackId();
71
+ callbacksById[id] = options;
72
+ registerListenersIfNeeded();
73
+ expoSpeech.speak(id, text, options);
74
+ }
75
+
76
+ export async function getAvailableVoicesAsync(): Promise<IVoice[]> {
77
+ if (!expoSpeech.getVoices) {
78
+ throw new UnavailabilityError('Speech', 'getVoices');
79
+ }
80
+ return expoSpeech.getVoices();
81
+ }
82
+
83
+ /** `true` even while paused. */
84
+ export async function isSpeakingAsync(): Promise<boolean> {
85
+ return expoSpeech.isSpeaking();
86
+ }
87
+
88
+ /** Interrupts current speech and clears the queue. */
89
+ export async function stop(): Promise<void> {
90
+ return expoSpeech.stop();
91
+ }
92
+
93
+ /** iOS only; throws `UnavailabilityError` on Android. */
94
+ export async function pause(): Promise<void> {
95
+ if (!expoSpeech.pause) {
96
+ throw new UnavailabilityError('Speech', 'pause');
97
+ }
98
+ return expoSpeech.pause();
99
+ }
100
+
101
+ /** iOS only; throws `UnavailabilityError` on Android. */
102
+ export async function resume(): Promise<void> {
103
+ if (!expoSpeech.resume) {
104
+ throw new UnavailabilityError('Speech', 'resume');
105
+ }
106
+ return expoSpeech.resume();
107
+ }
108
+
109
+ /** Platform-dependent; iOS returns `Number.MAX_VALUE`. */
110
+ export const maxSpeechInputLength: number =
111
+ expoSpeech.maxSpeechInputLength ?? Number.MAX_VALUE;
@@ -0,0 +1,36 @@
1
+ export enum VoiceQuality {
2
+ Default = 'Default',
3
+ Enhanced = 'Enhanced',
4
+ }
5
+
6
+ export type IVoice = {
7
+ identifier: string;
8
+ name: string;
9
+ quality: VoiceQuality;
10
+ language: string;
11
+ };
12
+
13
+ export type INativeBoundaryEvent = { charIndex: number; charLength: number };
14
+ export type INativeBoundaryEventCallback = (
15
+ event: INativeBoundaryEvent,
16
+ ) => void;
17
+
18
+ export type ISpeechOptions = {
19
+ /** IETF BCP 47 language code. */
20
+ language?: string;
21
+ /** `1.0` is the normal pitch. */
22
+ pitch?: number;
23
+ /** `1.0` is the normal rate. */
24
+ rate?: number;
25
+ /** iOS only — `false` lets the system manage a separate audio session for the speech. */
26
+ useApplicationAudioSession?: boolean;
27
+ onStart?: () => void;
28
+ /** Invoked when speaking is stopped by calling `stop()`. */
29
+ onStopped?: () => void;
30
+ onDone?: () => void;
31
+ onError?: (error: Error) => void;
32
+ /** `0.0` (muted) to `1.0` (max volume). Default `1.0`. */
33
+ volume?: number;
34
+ voice?: string;
35
+ onBoundary?: INativeBoundaryEventCallback | null;
36
+ };