react-native-tts-kit 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/ATTRIBUTIONS.md +87 -0
- package/LICENSE +21 -0
- package/README.md +231 -0
- package/android/build.gradle +50 -0
- package/android/src/main/AndroidManifest.xml +3 -0
- package/android/src/main/java/expo/modules/ttskit/RNTTSKitModule.kt +158 -0
- package/android/src/main/java/expo/modules/ttskit/supertonic/AudioEngine.kt +158 -0
- package/android/src/main/java/expo/modules/ttskit/supertonic/ModelLocator.kt +372 -0
- package/android/src/main/java/expo/modules/ttskit/supertonic/SupertonicSession.kt +373 -0
- package/android/src/main/java/expo/modules/ttskit/supertonic/TextFrontend.kt +154 -0
- package/android/src/main/java/expo/modules/ttskit/supertonic/VoicePack.kt +47 -0
- package/build/engines/BufferedStreamEmitter.d.ts +26 -0
- package/build/engines/BufferedStreamEmitter.d.ts.map +1 -0
- package/build/engines/BufferedStreamEmitter.js +68 -0
- package/build/engines/BufferedStreamEmitter.js.map +1 -0
- package/build/engines/Engine.d.ts +15 -0
- package/build/engines/Engine.d.ts.map +1 -0
- package/build/engines/Engine.js +2 -0
- package/build/engines/Engine.js.map +1 -0
- package/build/engines/SupertonicEngine.d.ts +14 -0
- package/build/engines/SupertonicEngine.d.ts.map +1 -0
- package/build/engines/SupertonicEngine.js +183 -0
- package/build/engines/SupertonicEngine.js.map +1 -0
- package/build/engines/SystemEngine.d.ts +13 -0
- package/build/engines/SystemEngine.d.ts.map +1 -0
- package/build/engines/SystemEngine.js +78 -0
- package/build/engines/SystemEngine.js.map +1 -0
- package/build/index.d.ts +46 -0
- package/build/index.d.ts.map +1 -0
- package/build/index.js +118 -0
- package/build/index.js.map +1 -0
- package/build/types.d.ts +77 -0
- package/build/types.d.ts.map +1 -0
- package/build/types.js +2 -0
- package/build/types.js.map +1 -0
- package/build/voices/catalog.d.ts +12 -0
- package/build/voices/catalog.d.ts.map +1 -0
- package/build/voices/catalog.js +28 -0
- package/build/voices/catalog.js.map +1 -0
- package/build/voices/prosody.d.ts +8 -0
- package/build/voices/prosody.d.ts.map +1 -0
- package/build/voices/prosody.js +28 -0
- package/build/voices/prosody.js.map +1 -0
- package/expo-module.config.json +9 -0
- package/ios/RNTTSKit.podspec +28 -0
- package/ios/RNTTSKitModule.swift +133 -0
- package/ios/Supertonic/AudioEngine.swift +110 -0
- package/ios/Supertonic/ModelLocator.swift +416 -0
- package/ios/Supertonic/SupertonicSession.swift +405 -0
- package/ios/Supertonic/TextFrontend.swift +216 -0
- package/ios/Supertonic/VoicePack.swift +51 -0
- package/licenses/OpenRAIL-M.txt +209 -0
- package/package.json +77 -0
- package/src/engines/BufferedStreamEmitter.ts +50 -0
- package/src/engines/Engine.ts +28 -0
- package/src/engines/SupertonicEngine.ts +250 -0
- package/src/engines/SystemEngine.ts +96 -0
- package/src/engines/__tests__/BufferedStreamEmitter.test.ts +65 -0
- package/src/index.ts +156 -0
- package/src/types.ts +95 -0
- package/src/voices/__tests__/catalog.test.ts +46 -0
- package/src/voices/__tests__/prosody.test.ts +63 -0
- package/src/voices/catalog.ts +32 -0
- package/src/voices/prosody.ts +39 -0
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import type { ClonedVoice, CloneOptions, EngineCapabilities, EngineId, PrefetchProgress, SpeakOptions, StreamHandle, Voice } from '../types';
|
|
2
|
+
export interface Engine {
|
|
3
|
+
readonly id: EngineId;
|
|
4
|
+
readonly capabilities: EngineCapabilities;
|
|
5
|
+
isAvailable(): Promise<boolean>;
|
|
6
|
+
prefetch(onProgress?: (p: PrefetchProgress) => void): Promise<void>;
|
|
7
|
+
getVoices(): Promise<Voice[]>;
|
|
8
|
+
speak(text: string, options?: SpeakOptions): Promise<void>;
|
|
9
|
+
stream(text: string, options?: SpeakOptions): StreamHandle;
|
|
10
|
+
stop(): Promise<void>;
|
|
11
|
+
/** Delete any locally cached model files so the next prefetch re-downloads. */
|
|
12
|
+
clearCache?(): Promise<void>;
|
|
13
|
+
cloneVoice?(options: CloneOptions): Promise<ClonedVoice>;
|
|
14
|
+
}
|
|
15
|
+
//# sourceMappingURL=Engine.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"Engine.d.ts","sourceRoot":"","sources":["../../src/engines/Engine.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EACV,WAAW,EACX,YAAY,EACZ,kBAAkB,EAClB,QAAQ,EACR,gBAAgB,EAChB,YAAY,EACZ,YAAY,EACZ,KAAK,EACN,MAAM,UAAU,CAAC;AAElB,MAAM,WAAW,MAAM;IACrB,QAAQ,CAAC,EAAE,EAAE,QAAQ,CAAC;IACtB,QAAQ,CAAC,YAAY,EAAE,kBAAkB,CAAC;IAE1C,WAAW,IAAI,OAAO,CAAC,OAAO,CAAC,CAAC;IAChC,QAAQ,CAAC,UAAU,CAAC,EAAE,CAAC,CAAC,EAAE,gBAAgB,KAAK,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IAEpE,SAAS,IAAI,OAAO,CAAC,KAAK,EAAE,CAAC,CAAC;IAC9B,KAAK,CAAC,IAAI,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE,YAAY,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IAC3D,MAAM,CAAC,IAAI,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE,YAAY,GAAG,YAAY,CAAC;IAC3D,IAAI,IAAI,OAAO,CAAC,IAAI,CAAC,CAAC;IAEtB,+EAA+E;IAC/E,UAAU,CAAC,IAAI,OAAO,CAAC,IAAI,CAAC,CAAC;IAE7B,UAAU,CAAC,CAAC,OAAO,EAAE,YAAY,GAAG,OAAO,CAAC,WAAW,CAAC,CAAC;CAC1D"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"Engine.js","sourceRoot":"","sources":["../../src/engines/Engine.ts"],"names":[],"mappings":"","sourcesContent":["import type {\n ClonedVoice,\n CloneOptions,\n EngineCapabilities,\n EngineId,\n PrefetchProgress,\n SpeakOptions,\n StreamHandle,\n Voice,\n} from '../types';\n\nexport interface Engine {\n readonly id: EngineId;\n readonly capabilities: EngineCapabilities;\n\n isAvailable(): Promise<boolean>;\n prefetch(onProgress?: (p: PrefetchProgress) => void): Promise<void>;\n\n getVoices(): Promise<Voice[]>;\n speak(text: string, options?: SpeakOptions): Promise<void>;\n stream(text: string, options?: SpeakOptions): StreamHandle;\n stop(): Promise<void>;\n\n /** Delete any locally cached model files so the next prefetch re-downloads. */\n clearCache?(): Promise<void>;\n\n cloneVoice?(options: CloneOptions): Promise<ClonedVoice>;\n}\n"]}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import type { EngineCapabilities, EngineId, PrefetchProgress, SpeakOptions, StreamHandle, Voice } from '../types';
|
|
2
|
+
import type { Engine } from './Engine';
|
|
3
|
+
export declare class SupertonicEngine implements Engine {
|
|
4
|
+
readonly id: EngineId;
|
|
5
|
+
readonly capabilities: EngineCapabilities;
|
|
6
|
+
isAvailable(): Promise<boolean>;
|
|
7
|
+
prefetch(onProgress?: (p: PrefetchProgress) => void): Promise<void>;
|
|
8
|
+
getVoices(): Promise<Voice[]>;
|
|
9
|
+
speak(text: string, options?: SpeakOptions): Promise<void>;
|
|
10
|
+
stream(text: string, options?: SpeakOptions): StreamHandle;
|
|
11
|
+
stop(): Promise<void>;
|
|
12
|
+
clearCache(): Promise<void>;
|
|
13
|
+
}
|
|
14
|
+
//# sourceMappingURL=SupertonicEngine.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"SupertonicEngine.d.ts","sourceRoot":"","sources":["../../src/engines/SupertonicEngine.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EACV,kBAAkB,EAClB,QAAQ,EACR,gBAAgB,EAChB,YAAY,EACZ,YAAY,EACZ,KAAK,EACN,MAAM,UAAU,CAAC;AAmBlB,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,UAAU,CAAC;AA8EvC,qBAAa,gBAAiB,YAAW,MAAM;IAC7C,QAAQ,CAAC,EAAE,EAAE,QAAQ,CAAgB;IACrC,QAAQ,CAAC,YAAY,EAAE,kBAAkB,CAMvC;IAEI,WAAW,IAAI,OAAO,CAAC,OAAO,CAAC;IAQ/B,QAAQ,CAAC,UAAU,CAAC,EAAE,CAAC,CAAC,EAAE,gBAAgB,KAAK,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC;IAYnE,SAAS,IAAI,OAAO,CAAC,KAAK,EAAE,CAAC;IAI7B,KAAK,CAAC,IAAI,EAAE,MAAM,EAAE,OAAO,GAAE,YAAiB,GAAG,OAAO,CAAC,IAAI,CAAC;IAqCpE,MAAM,CAAC,IAAI,EAAE,MAAM,EAAE,OAAO,GAAE,YAAiB,GAAG,YAAY;IAgExD,IAAI,IAAI,OAAO,CAAC,IAAI,CAAC;IAIrB,UAAU,IAAI,OAAO,CAAC,IAAI,CAAC;CAGlC"}
|
|
@@ -0,0 +1,183 @@
|
|
|
1
|
+
import { requireNativeModule } from 'expo-modules-core';
|
|
2
|
+
import { Platform } from 'react-native';
|
|
3
|
+
// iOS has CoreML/ANE fp16 acceleration; even an 8-step diffusion takes ~1s.
|
|
4
|
+
// Android relies on NNAPI + commodity SoCs (e.g. Snapdragon 720G in mid-tier
|
|
5
|
+
// phones) and tops out around 1-1.2s per diffusion step at fp32, so 8 steps
|
|
6
|
+
// stretches synthesis to 10-14s. Supertonic-3 was trained for 2-8 step
|
|
7
|
+
// inference; 6 steps is the quality/speed sweet spot on Android — audibly
|
|
8
|
+
// closer to 8-step than to 4-step, while still ~25% faster than 8.
|
|
9
|
+
// Callers can override per-call via SpeakOptions.totalStep.
|
|
10
|
+
const DEFAULT_TOTAL_STEP = Platform.OS === 'android' ? 6 : 8;
|
|
11
|
+
import { BufferedStreamEmitter } from './BufferedStreamEmitter';
|
|
12
|
+
import { DEFAULT_LANGUAGE, DEFAULT_VOICE_ID, SUPERTONIC_LANGUAGES, SUPERTONIC_VOICES, findVoice, } from '../voices/catalog';
|
|
13
|
+
import { stripProsody } from '../voices/prosody';
|
|
14
|
+
let nativeModule = null;
|
|
15
|
+
function getNative() {
|
|
16
|
+
if (!nativeModule) {
|
|
17
|
+
nativeModule = requireNativeModule('RNTTSKit');
|
|
18
|
+
}
|
|
19
|
+
return nativeModule;
|
|
20
|
+
}
|
|
21
|
+
let counter = 0;
|
|
22
|
+
const newId = () => `op_${Date.now().toString(36)}_${(++counter).toString(36)}`;
|
|
23
|
+
function decodeBase64(b64) {
|
|
24
|
+
if (typeof atob === 'function') {
|
|
25
|
+
const bin = atob(b64);
|
|
26
|
+
const out = new Uint8Array(bin.length);
|
|
27
|
+
for (let i = 0; i < bin.length; i++)
|
|
28
|
+
out[i] = bin.charCodeAt(i);
|
|
29
|
+
return out;
|
|
30
|
+
}
|
|
31
|
+
const lookup = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/';
|
|
32
|
+
const cleaned = b64.replace(/=+$/, '');
|
|
33
|
+
const out = new Uint8Array((cleaned.length * 3) >> 2);
|
|
34
|
+
let o = 0;
|
|
35
|
+
for (let i = 0; i < cleaned.length; i += 4) {
|
|
36
|
+
const a = lookup.indexOf(cleaned[i]);
|
|
37
|
+
const b = lookup.indexOf(cleaned[i + 1]);
|
|
38
|
+
const c = lookup.indexOf(cleaned[i + 2] ?? 'A');
|
|
39
|
+
const d = lookup.indexOf(cleaned[i + 3] ?? 'A');
|
|
40
|
+
out[o++] = (a << 2) | (b >> 4);
|
|
41
|
+
if (cleaned[i + 2])
|
|
42
|
+
out[o++] = ((b & 15) << 4) | (c >> 2);
|
|
43
|
+
if (cleaned[i + 3])
|
|
44
|
+
out[o++] = ((c & 3) << 6) | d;
|
|
45
|
+
}
|
|
46
|
+
return out.subarray(0, o);
|
|
47
|
+
}
|
|
48
|
+
// BufferedStreamEmitter is exported from its own module so we can unit-test
|
|
49
|
+
// it without needing the native module to load. See ./BufferedStreamEmitter.ts
|
|
50
|
+
function resolveLang(options) {
|
|
51
|
+
const lang = options.language ?? DEFAULT_LANGUAGE;
|
|
52
|
+
if (!SUPERTONIC_LANGUAGES.includes(lang)) {
|
|
53
|
+
throw new Error(`[ttskit] Unsupported language for Supertonic: ${lang}`);
|
|
54
|
+
}
|
|
55
|
+
return lang;
|
|
56
|
+
}
|
|
57
|
+
export class SupertonicEngine {
|
|
58
|
+
id = 'supertonic';
|
|
59
|
+
capabilities = {
|
|
60
|
+
streaming: true,
|
|
61
|
+
cloning: false,
|
|
62
|
+
emotionTags: false,
|
|
63
|
+
offline: true,
|
|
64
|
+
languages: [...SUPERTONIC_LANGUAGES],
|
|
65
|
+
};
|
|
66
|
+
async isAvailable() {
|
|
67
|
+
try {
|
|
68
|
+
return await getNative().isAvailable();
|
|
69
|
+
}
|
|
70
|
+
catch {
|
|
71
|
+
return false;
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
async prefetch(onProgress) {
|
|
75
|
+
const native = getNative();
|
|
76
|
+
const sub = onProgress
|
|
77
|
+
? native.addListener('onPrefetchProgress', (e) => onProgress(e))
|
|
78
|
+
: null;
|
|
79
|
+
try {
|
|
80
|
+
await native.prefetch();
|
|
81
|
+
}
|
|
82
|
+
finally {
|
|
83
|
+
sub?.remove();
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
async getVoices() {
|
|
87
|
+
return SUPERTONIC_VOICES;
|
|
88
|
+
}
|
|
89
|
+
async speak(text, options = {}) {
|
|
90
|
+
const voiceId = options.voice ?? DEFAULT_VOICE_ID;
|
|
91
|
+
if (!findVoice(voiceId))
|
|
92
|
+
throw new Error(`[ttskit] Unknown voice: ${voiceId}`);
|
|
93
|
+
const lang = resolveLang(options);
|
|
94
|
+
const id = newId();
|
|
95
|
+
const native = getNative();
|
|
96
|
+
const cleanText = stripProsody(text);
|
|
97
|
+
const subs = [];
|
|
98
|
+
if (options.onStart) {
|
|
99
|
+
subs.push(native.addListener('onSpeakStart', (e) => {
|
|
100
|
+
if (e.id === id)
|
|
101
|
+
options.onStart?.();
|
|
102
|
+
}));
|
|
103
|
+
}
|
|
104
|
+
if (options.onDone) {
|
|
105
|
+
subs.push(native.addListener('onSpeakDone', (e) => {
|
|
106
|
+
if (e.id === id)
|
|
107
|
+
options.onDone?.();
|
|
108
|
+
}));
|
|
109
|
+
}
|
|
110
|
+
try {
|
|
111
|
+
await native.speak(id, cleanText, voiceId, lang, options.totalStep ?? DEFAULT_TOTAL_STEP, options.rate ?? 1.05, options.volume ?? 1);
|
|
112
|
+
}
|
|
113
|
+
catch (err) {
|
|
114
|
+
options.onError?.(err);
|
|
115
|
+
throw err;
|
|
116
|
+
}
|
|
117
|
+
finally {
|
|
118
|
+
subs.forEach((s) => s.remove());
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
stream(text, options = {}) {
|
|
122
|
+
const voiceId = options.voice ?? DEFAULT_VOICE_ID;
|
|
123
|
+
if (!findVoice(voiceId))
|
|
124
|
+
throw new Error(`[ttskit] Unknown voice: ${voiceId}`);
|
|
125
|
+
const lang = resolveLang(options);
|
|
126
|
+
const id = newId();
|
|
127
|
+
const native = getNative();
|
|
128
|
+
const emitter = new BufferedStreamEmitter();
|
|
129
|
+
const cleanText = stripProsody(text);
|
|
130
|
+
let cleanedUp = false;
|
|
131
|
+
const chunkSub = native.addListener('onStreamChunk', (e) => {
|
|
132
|
+
if (e.id !== id)
|
|
133
|
+
return;
|
|
134
|
+
emitter.emitChunk(decodeBase64(e.pcm));
|
|
135
|
+
});
|
|
136
|
+
const endSub = native.addListener('onStreamEnd', (e) => {
|
|
137
|
+
if (e.id !== id)
|
|
138
|
+
return;
|
|
139
|
+
emitter.emitEnd();
|
|
140
|
+
cleanup();
|
|
141
|
+
});
|
|
142
|
+
const errSub = native.addListener('onStreamError', (e) => {
|
|
143
|
+
if (e.id !== id)
|
|
144
|
+
return;
|
|
145
|
+
emitter.emitError(new Error(e.message));
|
|
146
|
+
cleanup();
|
|
147
|
+
});
|
|
148
|
+
function cleanup() {
|
|
149
|
+
if (cleanedUp)
|
|
150
|
+
return;
|
|
151
|
+
cleanedUp = true;
|
|
152
|
+
chunkSub.remove();
|
|
153
|
+
endSub.remove();
|
|
154
|
+
errSub.remove();
|
|
155
|
+
}
|
|
156
|
+
native
|
|
157
|
+
.stream(id, cleanText, voiceId, lang, options.totalStep ?? DEFAULT_TOTAL_STEP, options.rate ?? 1.05, options.volume ?? 1)
|
|
158
|
+
.catch((err) => {
|
|
159
|
+
emitter.emitError(err);
|
|
160
|
+
cleanup();
|
|
161
|
+
});
|
|
162
|
+
const handle = {
|
|
163
|
+
id,
|
|
164
|
+
on(event, listener) {
|
|
165
|
+
// @ts-expect-error: emitter has overloaded signatures, runtime forwards correctly
|
|
166
|
+
emitter.on(event, listener);
|
|
167
|
+
return handle;
|
|
168
|
+
},
|
|
169
|
+
async cancel() {
|
|
170
|
+
cleanup();
|
|
171
|
+
await native.stop();
|
|
172
|
+
},
|
|
173
|
+
};
|
|
174
|
+
return handle;
|
|
175
|
+
}
|
|
176
|
+
async stop() {
|
|
177
|
+
await getNative().stop();
|
|
178
|
+
}
|
|
179
|
+
async clearCache() {
|
|
180
|
+
await getNative().clearCache();
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
//# sourceMappingURL=SupertonicEngine.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"SupertonicEngine.js","sourceRoot":"","sources":["../../src/engines/SupertonicEngine.ts"],"names":[],"mappings":"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","sourcesContent":["import { requireNativeModule } from 'expo-modules-core';\nimport { Platform } from 'react-native';\n\nimport type {\n EngineCapabilities,\n EngineId,\n PrefetchProgress,\n SpeakOptions,\n StreamHandle,\n Voice,\n} from '../types';\n\n// iOS has CoreML/ANE fp16 acceleration; even an 8-step diffusion takes ~1s.\n// Android relies on NNAPI + commodity SoCs (e.g. Snapdragon 720G in mid-tier\n// phones) and tops out around 1-1.2s per diffusion step at fp32, so 8 steps\n// stretches synthesis to 10-14s. Supertonic-3 was trained for 2-8 step\n// inference; 6 steps is the quality/speed sweet spot on Android — audibly\n// closer to 8-step than to 4-step, while still ~25% faster than 8.\n// Callers can override per-call via SpeakOptions.totalStep.\nconst DEFAULT_TOTAL_STEP = Platform.OS === 'android' ? 6 : 8;\nimport { BufferedStreamEmitter } from './BufferedStreamEmitter';\nimport {\n DEFAULT_LANGUAGE,\n DEFAULT_VOICE_ID,\n SUPERTONIC_LANGUAGES,\n SUPERTONIC_VOICES,\n findVoice,\n} from '../voices/catalog';\nimport { stripProsody } from '../voices/prosody';\nimport type { Engine } from './Engine';\n\ntype Subscription = { remove(): void };\ntype ChunkPayload = { id: string; pcm: string };\ntype IdPayload = { id: string };\ntype ErrorPayload = { id: string; message: string };\n\ninterface TTSKitNative {\n isAvailable(): Promise<boolean>;\n prefetch(): Promise<void>;\n speak(\n id: string,\n text: string,\n voiceId: string,\n lang: string,\n totalStep: number,\n speed: number,\n volume: number\n ): Promise<void>;\n stream(\n id: string,\n text: string,\n voiceId: string,\n lang: string,\n totalStep: number,\n speed: number,\n volume: number\n ): Promise<void>;\n stop(): Promise<void>;\n clearCache(): Promise<void>;\n addListener(name: string, listener: (event: any) => void): Subscription;\n}\n\nlet nativeModule: TTSKitNative | null = null;\nfunction getNative(): TTSKitNative {\n if (!nativeModule) {\n nativeModule = requireNativeModule<TTSKitNative>('RNTTSKit');\n }\n return nativeModule;\n}\n\nlet counter = 0;\nconst newId = () => `op_${Date.now().toString(36)}_${(++counter).toString(36)}`;\n\nfunction decodeBase64(b64: string): Uint8Array {\n if (typeof atob === 'function') {\n const bin = atob(b64);\n const out = new Uint8Array(bin.length);\n for (let i = 0; i < bin.length; i++) out[i] = bin.charCodeAt(i);\n return out;\n }\n const lookup = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/';\n const cleaned = b64.replace(/=+$/, '');\n const out = new Uint8Array((cleaned.length * 3) >> 2);\n let o = 0;\n for (let i = 0; i < cleaned.length; i += 4) {\n const a = lookup.indexOf(cleaned[i]);\n const b = lookup.indexOf(cleaned[i + 1]);\n const c = lookup.indexOf(cleaned[i + 2] ?? 'A');\n const d = lookup.indexOf(cleaned[i + 3] ?? 'A');\n out[o++] = (a << 2) | (b >> 4);\n if (cleaned[i + 2]) out[o++] = ((b & 15) << 4) | (c >> 2);\n if (cleaned[i + 3]) out[o++] = ((c & 3) << 6) | d;\n }\n return out.subarray(0, o);\n}\n\n// BufferedStreamEmitter is exported from its own module so we can unit-test\n// it without needing the native module to load. See ./BufferedStreamEmitter.ts\n\nfunction resolveLang(options: SpeakOptions): string {\n const lang = options.language ?? DEFAULT_LANGUAGE;\n if (!SUPERTONIC_LANGUAGES.includes(lang)) {\n throw new Error(`[ttskit] Unsupported language for Supertonic: ${lang}`);\n }\n return lang;\n}\n\nexport class SupertonicEngine implements Engine {\n readonly id: EngineId = 'supertonic';\n readonly capabilities: EngineCapabilities = {\n streaming: true,\n cloning: false,\n emotionTags: false,\n offline: true,\n languages: [...SUPERTONIC_LANGUAGES],\n };\n\n async isAvailable(): Promise<boolean> {\n try {\n return await getNative().isAvailable();\n } catch {\n return false;\n }\n }\n\n async prefetch(onProgress?: (p: PrefetchProgress) => void): Promise<void> {\n const native = getNative();\n const sub = onProgress\n ? native.addListener('onPrefetchProgress', (e: PrefetchProgress) => onProgress(e))\n : null;\n try {\n await native.prefetch();\n } finally {\n sub?.remove();\n }\n }\n\n async getVoices(): Promise<Voice[]> {\n return SUPERTONIC_VOICES;\n }\n\n async speak(text: string, options: SpeakOptions = {}): Promise<void> {\n const voiceId = options.voice ?? DEFAULT_VOICE_ID;\n if (!findVoice(voiceId)) throw new Error(`[ttskit] Unknown voice: ${voiceId}`);\n const lang = resolveLang(options);\n const id = newId();\n const native = getNative();\n const cleanText = stripProsody(text);\n\n const subs: Subscription[] = [];\n if (options.onStart) {\n subs.push(native.addListener('onSpeakStart', (e: IdPayload) => {\n if (e.id === id) options.onStart?.();\n }));\n }\n if (options.onDone) {\n subs.push(native.addListener('onSpeakDone', (e: IdPayload) => {\n if (e.id === id) options.onDone?.();\n }));\n }\n try {\n await native.speak(\n id,\n cleanText,\n voiceId,\n lang,\n options.totalStep ?? DEFAULT_TOTAL_STEP,\n options.rate ?? 1.05,\n options.volume ?? 1\n );\n } catch (err) {\n options.onError?.(err as Error);\n throw err;\n } finally {\n subs.forEach((s) => s.remove());\n }\n }\n\n stream(text: string, options: SpeakOptions = {}): StreamHandle {\n const voiceId = options.voice ?? DEFAULT_VOICE_ID;\n if (!findVoice(voiceId)) throw new Error(`[ttskit] Unknown voice: ${voiceId}`);\n const lang = resolveLang(options);\n\n const id = newId();\n const native = getNative();\n const emitter = new BufferedStreamEmitter();\n const cleanText = stripProsody(text);\n let cleanedUp = false;\n\n const chunkSub = native.addListener('onStreamChunk', (e: ChunkPayload) => {\n if (e.id !== id) return;\n emitter.emitChunk(decodeBase64(e.pcm));\n });\n const endSub = native.addListener('onStreamEnd', (e: IdPayload) => {\n if (e.id !== id) return;\n emitter.emitEnd();\n cleanup();\n });\n const errSub = native.addListener('onStreamError', (e: ErrorPayload) => {\n if (e.id !== id) return;\n emitter.emitError(new Error(e.message));\n cleanup();\n });\n\n function cleanup() {\n if (cleanedUp) return;\n cleanedUp = true;\n chunkSub.remove();\n endSub.remove();\n errSub.remove();\n }\n\n native\n .stream(\n id,\n cleanText,\n voiceId,\n lang,\n options.totalStep ?? DEFAULT_TOTAL_STEP,\n options.rate ?? 1.05,\n options.volume ?? 1\n )\n .catch((err: Error) => {\n emitter.emitError(err);\n cleanup();\n });\n\n const handle: StreamHandle = {\n id,\n on(event: 'chunk' | 'end' | 'error', listener: (...args: any[]) => void) {\n // @ts-expect-error: emitter has overloaded signatures, runtime forwards correctly\n emitter.on(event, listener);\n return handle;\n },\n async cancel() {\n cleanup();\n await native.stop();\n },\n };\n return handle;\n }\n\n async stop(): Promise<void> {\n await getNative().stop();\n }\n\n async clearCache(): Promise<void> {\n await getNative().clearCache();\n }\n}\n"]}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import type { EngineCapabilities, EngineId, PrefetchProgress, SpeakOptions, StreamHandle, Voice } from '../types';
|
|
2
|
+
import type { Engine } from './Engine';
|
|
3
|
+
export declare class SystemEngine implements Engine {
|
|
4
|
+
readonly id: EngineId;
|
|
5
|
+
readonly capabilities: EngineCapabilities;
|
|
6
|
+
isAvailable(): Promise<boolean>;
|
|
7
|
+
prefetch(_onProgress?: (p: PrefetchProgress) => void): Promise<void>;
|
|
8
|
+
getVoices(): Promise<Voice[]>;
|
|
9
|
+
speak(text: string, options?: SpeakOptions): Promise<void>;
|
|
10
|
+
stream(_text: string, _options?: SpeakOptions): StreamHandle;
|
|
11
|
+
stop(): Promise<void>;
|
|
12
|
+
}
|
|
13
|
+
//# sourceMappingURL=SystemEngine.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"SystemEngine.d.ts","sourceRoot":"","sources":["../../src/engines/SystemEngine.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EACV,kBAAkB,EAClB,QAAQ,EACR,gBAAgB,EAChB,YAAY,EACZ,YAAY,EACZ,KAAK,EACN,MAAM,UAAU,CAAC;AAClB,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,UAAU,CAAC;AAmBvC,qBAAa,YAAa,YAAW,MAAM;IACzC,QAAQ,CAAC,EAAE,EAAE,QAAQ,CAAY;IACjC,QAAQ,CAAC,YAAY,EAAE,kBAAkB,CAMvC;IAEI,WAAW,IAAI,OAAO,CAAC,OAAO,CAAC;IAI/B,QAAQ,CAAC,WAAW,CAAC,EAAE,CAAC,CAAC,EAAE,gBAAgB,KAAK,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC;IAIpE,SAAS,IAAI,OAAO,CAAC,KAAK,EAAE,CAAC;IAYnC,KAAK,CAAC,IAAI,EAAE,MAAM,EAAE,OAAO,GAAE,YAAiB,GAAG,OAAO,CAAC,IAAI,CAAC;IA8B9D,MAAM,CAAC,KAAK,EAAE,MAAM,EAAE,QAAQ,GAAE,YAAiB,GAAG,YAAY;IAI1D,IAAI,IAAI,OAAO,CAAC,IAAI,CAAC;CAI5B"}
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
let cached = null;
|
|
2
|
+
function loadExpoSpeech() {
|
|
3
|
+
if (cached)
|
|
4
|
+
return cached;
|
|
5
|
+
try {
|
|
6
|
+
cached = require('expo-speech');
|
|
7
|
+
return cached;
|
|
8
|
+
}
|
|
9
|
+
catch {
|
|
10
|
+
return null;
|
|
11
|
+
}
|
|
12
|
+
}
|
|
13
|
+
export class SystemEngine {
|
|
14
|
+
id = 'system';
|
|
15
|
+
capabilities = {
|
|
16
|
+
streaming: false,
|
|
17
|
+
cloning: false,
|
|
18
|
+
emotionTags: false,
|
|
19
|
+
offline: true,
|
|
20
|
+
languages: ['*'],
|
|
21
|
+
};
|
|
22
|
+
async isAvailable() {
|
|
23
|
+
return loadExpoSpeech() !== null;
|
|
24
|
+
}
|
|
25
|
+
async prefetch(_onProgress) {
|
|
26
|
+
// No-op: system voices are bundled with the OS.
|
|
27
|
+
}
|
|
28
|
+
async getVoices() {
|
|
29
|
+
const speech = loadExpoSpeech();
|
|
30
|
+
if (!speech)
|
|
31
|
+
return [];
|
|
32
|
+
const voices = await speech.getAvailableVoicesAsync();
|
|
33
|
+
return voices.map((v) => ({
|
|
34
|
+
id: v.identifier,
|
|
35
|
+
name: v.name,
|
|
36
|
+
language: v.language,
|
|
37
|
+
engine: 'system',
|
|
38
|
+
}));
|
|
39
|
+
}
|
|
40
|
+
speak(text, options = {}) {
|
|
41
|
+
const speech = loadExpoSpeech();
|
|
42
|
+
if (!speech) {
|
|
43
|
+
throw new Error('[ttskit] expo-speech is not installed');
|
|
44
|
+
}
|
|
45
|
+
return new Promise((resolve, reject) => {
|
|
46
|
+
try {
|
|
47
|
+
speech.speak(text, {
|
|
48
|
+
voice: options.voice,
|
|
49
|
+
language: options.language,
|
|
50
|
+
rate: options.rate,
|
|
51
|
+
pitch: options.pitch,
|
|
52
|
+
volume: options.volume,
|
|
53
|
+
onStart: options.onStart,
|
|
54
|
+
onDone: () => {
|
|
55
|
+
options.onDone?.();
|
|
56
|
+
resolve();
|
|
57
|
+
},
|
|
58
|
+
onError: (err) => {
|
|
59
|
+
options.onError?.(err);
|
|
60
|
+
reject(err);
|
|
61
|
+
},
|
|
62
|
+
onStopped: () => resolve(),
|
|
63
|
+
});
|
|
64
|
+
}
|
|
65
|
+
catch (err) {
|
|
66
|
+
reject(err);
|
|
67
|
+
}
|
|
68
|
+
});
|
|
69
|
+
}
|
|
70
|
+
stream(_text, _options = {}) {
|
|
71
|
+
throw new Error('[ttskit] System engine does not support streaming. Use engine: "supertonic".');
|
|
72
|
+
}
|
|
73
|
+
async stop() {
|
|
74
|
+
const speech = loadExpoSpeech();
|
|
75
|
+
await speech?.stop();
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
//# sourceMappingURL=SystemEngine.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"SystemEngine.js","sourceRoot":"","sources":["../../src/engines/SystemEngine.ts"],"names":[],"mappings":"AAgBA,IAAI,MAAM,GAA4B,IAAI,CAAC;AAC3C,SAAS,cAAc;IACrB,IAAI,MAAM;QAAE,OAAO,MAAM,CAAC;IAC1B,IAAI,CAAC;QACH,MAAM,GAAG,OAAO,CAAC,aAAa,CAAqB,CAAC;QACpD,OAAO,MAAM,CAAC;IAChB,CAAC;IAAC,MAAM,CAAC;QACP,OAAO,IAAI,CAAC;IACd,CAAC;AACH,CAAC;AAED,MAAM,OAAO,YAAY;IACd,EAAE,GAAa,QAAQ,CAAC;IACxB,YAAY,GAAuB;QAC1C,SAAS,EAAE,KAAK;QAChB,OAAO,EAAE,KAAK;QACd,WAAW,EAAE,KAAK;QAClB,OAAO,EAAE,IAAI;QACb,SAAS,EAAE,CAAC,GAAG,CAAC;KACjB,CAAC;IAEF,KAAK,CAAC,WAAW;QACf,OAAO,cAAc,EAAE,KAAK,IAAI,CAAC;IACnC,CAAC;IAED,KAAK,CAAC,QAAQ,CAAC,WAA2C;QACxD,gDAAgD;IAClD,CAAC;IAED,KAAK,CAAC,SAAS;QACb,MAAM,MAAM,GAAG,cAAc,EAAE,CAAC;QAChC,IAAI,CAAC,MAAM;YAAE,OAAO,EAAE,CAAC;QACvB,MAAM,MAAM,GAAG,MAAM,MAAM,CAAC,uBAAuB,EAAE,CAAC;QACtD,OAAO,MAAM,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC;YACxB,EAAE,EAAE,CAAC,CAAC,UAAU;YAChB,IAAI,EAAE,CAAC,CAAC,IAAI;YACZ,QAAQ,EAAE,CAAC,CAAC,QAAQ;YACpB,MAAM,EAAE,QAAoB;SAC7B,CAAC,CAAC,CAAC;IACN,CAAC;IAED,KAAK,CAAC,IAAY,EAAE,UAAwB,EAAE;QAC5C,MAAM,MAAM,GAAG,cAAc,EAAE,CAAC;QAChC,IAAI,CAAC,MAAM,EAAE,CAAC;YACZ,MAAM,IAAI,KAAK,CAAC,uCAAuC,CAAC,CAAC;QAC3D,CAAC;QACD,OAAO,IAAI,OAAO,CAAC,CAAC,OAAO,EAAE,MAAM,EAAE,EAAE;YACrC,IAAI,CAAC;gBACH,MAAM,CAAC,KAAK,CAAC,IAAI,EAAE;oBACjB,KAAK,EAAE,OAAO,CAAC,KAAK;oBACpB,QAAQ,EAAE,OAAO,CAAC,QAAQ;oBAC1B,IAAI,EAAE,OAAO,CAAC,IAAI;oBAClB,KAAK,EAAE,OAAO,CAAC,KAAK;oBACpB,MAAM,EAAE,OAAO,CAAC,MAAM;oBACtB,OAAO,EAAE,OAAO,CAAC,OAAO;oBACxB,MAAM,EAAE,GAAG,EAAE;wBACX,OAAO,CAAC,MAAM,EAAE,EAAE,CAAC;wBACnB,OAAO,EAAE,CAAC;oBACZ,CAAC;oBACD,OAAO,EAAE,CAAC,GAAU,EAAE,EAAE;wBACtB,OAAO,CAAC,OAAO,EAAE,CAAC,GAAG,CAAC,CAAC;wBACvB,MAAM,CAAC,GAAG,CAAC,CAAC;oBACd,CAAC;oBACD,SAAS,EAAE,GAAG,EAAE,CAAC,OAAO,EAAE;iBAC3B,CAAC,CAAC;YACL,CAAC;YAAC,OAAO,GAAG,EAAE,CAAC;gBACb,MAAM,CAAC,GAAG,CAAC,CAAC;YACd,CAAC;QACH,CAAC,CAAC,CAAC;IACL,CAAC;IAED,MAAM,CAAC,KAAa,EAAE,WAAyB,EAAE;QAC/C,MAAM,IAAI,KAAK,CAAC,8EAA8E,CAAC,CAAC;IAClG,CAAC;IAED,KAAK,CAAC,IAAI;QACR,MAAM,MAAM,GAAG,cAAc,EAAE,CAAC;QAChC,MAAM,MAAM,EAAE,IAAI,EAAE,CAAC;IACvB,CAAC;CACF","sourcesContent":["import type {\n EngineCapabilities,\n EngineId,\n PrefetchProgress,\n SpeakOptions,\n StreamHandle,\n Voice,\n} from '../types';\nimport type { Engine } from './Engine';\n\ntype ExpoSpeechModule = {\n speak(text: string, options?: any): void;\n stop(): Promise<void>;\n getAvailableVoicesAsync(): Promise<Array<{ identifier: string; name: string; language: string }>>;\n};\n\nlet cached: ExpoSpeechModule | null = null;\nfunction loadExpoSpeech(): ExpoSpeechModule | null {\n if (cached) return cached;\n try {\n cached = require('expo-speech') as ExpoSpeechModule;\n return cached;\n } catch {\n return null;\n }\n}\n\nexport class SystemEngine implements Engine {\n readonly id: EngineId = 'system';\n readonly capabilities: EngineCapabilities = {\n streaming: false,\n cloning: false,\n emotionTags: false,\n offline: true,\n languages: ['*'],\n };\n\n async isAvailable(): Promise<boolean> {\n return loadExpoSpeech() !== null;\n }\n\n async prefetch(_onProgress?: (p: PrefetchProgress) => void): Promise<void> {\n // No-op: system voices are bundled with the OS.\n }\n\n async getVoices(): Promise<Voice[]> {\n const speech = loadExpoSpeech();\n if (!speech) return [];\n const voices = await speech.getAvailableVoicesAsync();\n return voices.map((v) => ({\n id: v.identifier,\n name: v.name,\n language: v.language,\n engine: 'system' as EngineId,\n }));\n }\n\n speak(text: string, options: SpeakOptions = {}): Promise<void> {\n const speech = loadExpoSpeech();\n if (!speech) {\n throw new Error('[ttskit] expo-speech is not installed');\n }\n return new Promise((resolve, reject) => {\n try {\n speech.speak(text, {\n voice: options.voice,\n language: options.language,\n rate: options.rate,\n pitch: options.pitch,\n volume: options.volume,\n onStart: options.onStart,\n onDone: () => {\n options.onDone?.();\n resolve();\n },\n onError: (err: Error) => {\n options.onError?.(err);\n reject(err);\n },\n onStopped: () => resolve(),\n });\n } catch (err) {\n reject(err);\n }\n });\n }\n\n stream(_text: string, _options: SpeakOptions = {}): StreamHandle {\n throw new Error('[ttskit] System engine does not support streaming. Use engine: \"supertonic\".');\n }\n\n async stop(): Promise<void> {\n const speech = loadExpoSpeech();\n await speech?.stop();\n }\n}\n"]}
|
package/build/index.d.ts
ADDED
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import type { Engine } from './engines/Engine';
|
|
2
|
+
import type { ClonedVoice, CloneOptions, EngineId, PrefetchProgress, SpeakOptions, StreamHandle, Voice } from './types';
|
|
3
|
+
export type { ClonedVoice, CloneOptions, EngineId, PrefetchProgress, SpeakOptions, StreamHandle, Voice, } from './types';
|
|
4
|
+
export type { Engine } from './engines/Engine';
|
|
5
|
+
export { parseProsody, stripProsody } from './voices/prosody';
|
|
6
|
+
export { SUPERTONIC_VOICES, SUPERTONIC_LANGUAGES } from './voices/catalog';
|
|
7
|
+
export declare const TTSKit: {
|
|
8
|
+
setEngine(id: EngineId): void;
|
|
9
|
+
getEngine(): EngineId;
|
|
10
|
+
registerEngine(engine: Engine): void;
|
|
11
|
+
/**
|
|
12
|
+
* Suggest a sensible engine for the current device.
|
|
13
|
+
*
|
|
14
|
+
* - On iOS, always returns `'supertonic'` — every iPhone with iOS 13+ has the
|
|
15
|
+
* Neural Engine and runs neural TTS well (~1-2s TTFA).
|
|
16
|
+
* - On Android, returns `'supertonic'` for devices that report a recent SoC
|
|
17
|
+
* with NNAPI 1.2+ acceleration, else `'system'`. The heuristic is
|
|
18
|
+
* conservative: it errs toward `system` for any mid-range or older device
|
|
19
|
+
* because Supertonic on a mid-range Snapdragon runs at ~0.5× realtime
|
|
20
|
+
* (10s+ TTFA), which is worse UX than a robotic but instant system voice.
|
|
21
|
+
* - Defaults to `'supertonic'` on web / unknown platforms.
|
|
22
|
+
*
|
|
23
|
+
* This is opt-in. The library default is still Supertonic everywhere — apps
|
|
24
|
+
* that want graceful fallback should call this once at startup:
|
|
25
|
+
*
|
|
26
|
+
* TTSKit.setEngine(TTSKit.recommendEngine());
|
|
27
|
+
*
|
|
28
|
+
* The detection is heuristic. For a hard guarantee, run a one-time benchmark
|
|
29
|
+
* (synthesize a known short input, measure TTFA, persist the result) and
|
|
30
|
+
* decide based on actual numbers — that's more accurate than any static
|
|
31
|
+
* device-tier list.
|
|
32
|
+
*/
|
|
33
|
+
recommendEngine(): EngineId;
|
|
34
|
+
isAvailable(engineId?: EngineId): Promise<boolean>;
|
|
35
|
+
prefetchModel(onProgress?: (p: PrefetchProgress) => void, engineId?: EngineId): Promise<void>;
|
|
36
|
+
getVoices(engineId?: EngineId): Promise<Voice[]>;
|
|
37
|
+
speak(text: string, options?: SpeakOptions): Promise<void>;
|
|
38
|
+
stream(text: string, options?: SpeakOptions): StreamHandle;
|
|
39
|
+
stop(engineId?: EngineId): Promise<void>;
|
|
40
|
+
/** Delete locally cached model files so the next `prefetchModel()` re-downloads.
|
|
41
|
+
* No-op for engines that don't have a cache (e.g. the system engine). */
|
|
42
|
+
clearCache(engineId?: EngineId): Promise<void>;
|
|
43
|
+
cloneVoice(options: CloneOptions, engineId?: EngineId): Promise<ClonedVoice>;
|
|
44
|
+
};
|
|
45
|
+
export default TTSKit;
|
|
46
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAIA,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,kBAAkB,CAAC;AAC/C,OAAO,KAAK,EACV,WAAW,EACX,YAAY,EACZ,QAAQ,EACR,gBAAgB,EAChB,YAAY,EACZ,YAAY,EACZ,KAAK,EACN,MAAM,SAAS,CAAC;AAEjB,YAAY,EACV,WAAW,EACX,YAAY,EACZ,QAAQ,EACR,gBAAgB,EAChB,YAAY,EACZ,YAAY,EACZ,KAAK,GACN,MAAM,SAAS,CAAC;AACjB,YAAY,EAAE,MAAM,EAAE,MAAM,kBAAkB,CAAC;AAC/C,OAAO,EAAE,YAAY,EAAE,YAAY,EAAE,MAAM,kBAAkB,CAAC;AAC9D,OAAO,EAAE,iBAAiB,EAAE,oBAAoB,EAAE,MAAM,kBAAkB,CAAC;AAgB3E,eAAO,MAAM,MAAM;kBACH,QAAQ,GAAG,IAAI;iBAOhB,QAAQ;2BAIE,MAAM,GAAG,IAAI;IAIpC;;;;;;;;;;;;;;;;;;;;;OAqBG;uBACgB,QAAQ;2BA8BE,QAAQ,GAAG,OAAO,CAAC,OAAO,CAAC;+BAKzC,CAAC,CAAC,EAAE,gBAAgB,KAAK,IAAI,aAC/B,QAAQ,GAClB,OAAO,CAAC,IAAI,CAAC;yBAIW,QAAQ,GAAG,OAAO,CAAC,KAAK,EAAE,CAAC;gBAIpC,MAAM,YAAW,YAAY,GAAQ,OAAO,CAAC,IAAI,CAAC;iBAIvD,MAAM,YAAW,YAAY,GAAQ,YAAY;oBAIxC,QAAQ,GAAG,OAAO,CAAC,IAAI,CAAC;IAI9C;8EAC0E;0BAC9C,QAAQ,GAAG,OAAO,CAAC,IAAI,CAAC;wBAO1B,YAAY,aAAa,QAAQ,GAAG,OAAO,CAAC,WAAW,CAAC;CAOnF,CAAC;AAEF,eAAe,MAAM,CAAC"}
|
package/build/index.js
ADDED
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
import { Platform } from 'react-native';
|
|
2
|
+
import { SupertonicEngine } from './engines/SupertonicEngine';
|
|
3
|
+
import { SystemEngine } from './engines/SystemEngine';
|
|
4
|
+
export { parseProsody, stripProsody } from './voices/prosody';
|
|
5
|
+
export { SUPERTONIC_VOICES, SUPERTONIC_LANGUAGES } from './voices/catalog';
|
|
6
|
+
const engines = new Map();
|
|
7
|
+
engines.set('supertonic', new SupertonicEngine());
|
|
8
|
+
engines.set('system', new SystemEngine());
|
|
9
|
+
let activeEngineId = 'supertonic';
|
|
10
|
+
function getEngine(id = activeEngineId) {
|
|
11
|
+
const engine = engines.get(id);
|
|
12
|
+
if (!engine) {
|
|
13
|
+
throw new Error(`[ttskit] Engine "${id}" is not registered.`);
|
|
14
|
+
}
|
|
15
|
+
return engine;
|
|
16
|
+
}
|
|
17
|
+
export const TTSKit = {
|
|
18
|
+
setEngine(id) {
|
|
19
|
+
if (!engines.has(id)) {
|
|
20
|
+
throw new Error(`[ttskit] Engine "${id}" is not registered.`);
|
|
21
|
+
}
|
|
22
|
+
activeEngineId = id;
|
|
23
|
+
},
|
|
24
|
+
getEngine() {
|
|
25
|
+
return activeEngineId;
|
|
26
|
+
},
|
|
27
|
+
registerEngine(engine) {
|
|
28
|
+
engines.set(engine.id, engine);
|
|
29
|
+
},
|
|
30
|
+
/**
|
|
31
|
+
* Suggest a sensible engine for the current device.
|
|
32
|
+
*
|
|
33
|
+
* - On iOS, always returns `'supertonic'` — every iPhone with iOS 13+ has the
|
|
34
|
+
* Neural Engine and runs neural TTS well (~1-2s TTFA).
|
|
35
|
+
* - On Android, returns `'supertonic'` for devices that report a recent SoC
|
|
36
|
+
* with NNAPI 1.2+ acceleration, else `'system'`. The heuristic is
|
|
37
|
+
* conservative: it errs toward `system` for any mid-range or older device
|
|
38
|
+
* because Supertonic on a mid-range Snapdragon runs at ~0.5× realtime
|
|
39
|
+
* (10s+ TTFA), which is worse UX than a robotic but instant system voice.
|
|
40
|
+
* - Defaults to `'supertonic'` on web / unknown platforms.
|
|
41
|
+
*
|
|
42
|
+
* This is opt-in. The library default is still Supertonic everywhere — apps
|
|
43
|
+
* that want graceful fallback should call this once at startup:
|
|
44
|
+
*
|
|
45
|
+
* TTSKit.setEngine(TTSKit.recommendEngine());
|
|
46
|
+
*
|
|
47
|
+
* The detection is heuristic. For a hard guarantee, run a one-time benchmark
|
|
48
|
+
* (synthesize a known short input, measure TTFA, persist the result) and
|
|
49
|
+
* decide based on actual numbers — that's more accurate than any static
|
|
50
|
+
* device-tier list.
|
|
51
|
+
*/
|
|
52
|
+
recommendEngine() {
|
|
53
|
+
if (Platform.OS === 'ios')
|
|
54
|
+
return 'supertonic';
|
|
55
|
+
if (Platform.OS !== 'android')
|
|
56
|
+
return 'supertonic';
|
|
57
|
+
// Android tier detection. We can't read SoC directly from JS, so we rely
|
|
58
|
+
// on what `Platform.constants` exposes: Brand, Manufacturer, Model.
|
|
59
|
+
// The check is "is this likely a flagship / recent device?" — keep it
|
|
60
|
+
// narrow and additive. Anything not on the allow-list falls back to system.
|
|
61
|
+
const c = Platform.constants ?? {};
|
|
62
|
+
const brand = String(c.Brand ?? '').toLowerCase();
|
|
63
|
+
const manufacturer = String(c.Manufacturer ?? '').toLowerCase();
|
|
64
|
+
const model = String(c.Model ?? '').toLowerCase();
|
|
65
|
+
const apiLevel = typeof c.Release === 'string' ? parseInt(c.Release, 10) : 0;
|
|
66
|
+
// Android 10 = API 29 = NNAPI 1.2 floor. Below this, NNAPI partitioning
|
|
67
|
+
// is poor enough that ORT often falls back to XNNPACK silently.
|
|
68
|
+
if (apiLevel && apiLevel < 10)
|
|
69
|
+
return 'system';
|
|
70
|
+
// Pixel 6 and newer have Tensor G1/G2/G3/G4 with a real NPU.
|
|
71
|
+
if (brand === 'google' && /pixel\s*([6-9]|1\d)/.test(model))
|
|
72
|
+
return 'supertonic';
|
|
73
|
+
// Samsung S22+ and Tab S8+ are Snapdragon 8 Gen 1 / Exynos 2200 floor.
|
|
74
|
+
if (manufacturer === 'samsung' && /sm-s9\d\d|sm-x[78]\d\d/i.test(model))
|
|
75
|
+
return 'supertonic';
|
|
76
|
+
// OnePlus 10 Pro+, current generation flagships are usually safe.
|
|
77
|
+
if (brand === 'oneplus' && /ne|le2\d\d\d/i.test(model))
|
|
78
|
+
return 'supertonic';
|
|
79
|
+
// Default for everything else (including the Galaxy A52 you tested on,
|
|
80
|
+
// which has SD720G and gets ~10s TTFA): use the system engine.
|
|
81
|
+
return 'system';
|
|
82
|
+
},
|
|
83
|
+
async isAvailable(engineId) {
|
|
84
|
+
return getEngine(engineId).isAvailable();
|
|
85
|
+
},
|
|
86
|
+
async prefetchModel(onProgress, engineId) {
|
|
87
|
+
return getEngine(engineId).prefetch(onProgress);
|
|
88
|
+
},
|
|
89
|
+
async getVoices(engineId) {
|
|
90
|
+
return getEngine(engineId).getVoices();
|
|
91
|
+
},
|
|
92
|
+
async speak(text, options = {}) {
|
|
93
|
+
return getEngine(options.engine).speak(text, options);
|
|
94
|
+
},
|
|
95
|
+
stream(text, options = {}) {
|
|
96
|
+
return getEngine(options.engine).stream(text, options);
|
|
97
|
+
},
|
|
98
|
+
async stop(engineId) {
|
|
99
|
+
return getEngine(engineId).stop();
|
|
100
|
+
},
|
|
101
|
+
/** Delete locally cached model files so the next `prefetchModel()` re-downloads.
|
|
102
|
+
* No-op for engines that don't have a cache (e.g. the system engine). */
|
|
103
|
+
async clearCache(engineId) {
|
|
104
|
+
const engine = getEngine(engineId);
|
|
105
|
+
if (engine.clearCache) {
|
|
106
|
+
await engine.clearCache();
|
|
107
|
+
}
|
|
108
|
+
},
|
|
109
|
+
async cloneVoice(options, engineId) {
|
|
110
|
+
const engine = getEngine(engineId);
|
|
111
|
+
if (!engine.cloneVoice) {
|
|
112
|
+
throw new Error(`[ttskit] Engine "${engine.id}" does not support voice cloning.`);
|
|
113
|
+
}
|
|
114
|
+
return engine.cloneVoice(options);
|
|
115
|
+
},
|
|
116
|
+
};
|
|
117
|
+
export default TTSKit;
|
|
118
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"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","sourcesContent":["import { Platform } from 'react-native';\n\nimport { SupertonicEngine } from './engines/SupertonicEngine';\nimport { SystemEngine } from './engines/SystemEngine';\nimport type { Engine } from './engines/Engine';\nimport type {\n ClonedVoice,\n CloneOptions,\n EngineId,\n PrefetchProgress,\n SpeakOptions,\n StreamHandle,\n Voice,\n} from './types';\n\nexport type {\n ClonedVoice,\n CloneOptions,\n EngineId,\n PrefetchProgress,\n SpeakOptions,\n StreamHandle,\n Voice,\n} from './types';\nexport type { Engine } from './engines/Engine';\nexport { parseProsody, stripProsody } from './voices/prosody';\nexport { SUPERTONIC_VOICES, SUPERTONIC_LANGUAGES } from './voices/catalog';\n\nconst engines = new Map<EngineId, Engine>();\nengines.set('supertonic', new SupertonicEngine());\nengines.set('system', new SystemEngine());\n\nlet activeEngineId: EngineId = 'supertonic';\n\nfunction getEngine(id: EngineId = activeEngineId): Engine {\n const engine = engines.get(id);\n if (!engine) {\n throw new Error(`[ttskit] Engine \"${id}\" is not registered.`);\n }\n return engine;\n}\n\nexport const TTSKit = {\n setEngine(id: EngineId): void {\n if (!engines.has(id)) {\n throw new Error(`[ttskit] Engine \"${id}\" is not registered.`);\n }\n activeEngineId = id;\n },\n\n getEngine(): EngineId {\n return activeEngineId;\n },\n\n registerEngine(engine: Engine): void {\n engines.set(engine.id, engine);\n },\n\n /**\n * Suggest a sensible engine for the current device.\n *\n * - On iOS, always returns `'supertonic'` — every iPhone with iOS 13+ has the\n * Neural Engine and runs neural TTS well (~1-2s TTFA).\n * - On Android, returns `'supertonic'` for devices that report a recent SoC\n * with NNAPI 1.2+ acceleration, else `'system'`. The heuristic is\n * conservative: it errs toward `system` for any mid-range or older device\n * because Supertonic on a mid-range Snapdragon runs at ~0.5× realtime\n * (10s+ TTFA), which is worse UX than a robotic but instant system voice.\n * - Defaults to `'supertonic'` on web / unknown platforms.\n *\n * This is opt-in. The library default is still Supertonic everywhere — apps\n * that want graceful fallback should call this once at startup:\n *\n * TTSKit.setEngine(TTSKit.recommendEngine());\n *\n * The detection is heuristic. For a hard guarantee, run a one-time benchmark\n * (synthesize a known short input, measure TTFA, persist the result) and\n * decide based on actual numbers — that's more accurate than any static\n * device-tier list.\n */\n recommendEngine(): EngineId {\n if (Platform.OS === 'ios') return 'supertonic';\n if (Platform.OS !== 'android') return 'supertonic';\n\n // Android tier detection. We can't read SoC directly from JS, so we rely\n // on what `Platform.constants` exposes: Brand, Manufacturer, Model.\n // The check is \"is this likely a flagship / recent device?\" — keep it\n // narrow and additive. Anything not on the allow-list falls back to system.\n const c: any = Platform.constants ?? {};\n const brand = String(c.Brand ?? '').toLowerCase();\n const manufacturer = String(c.Manufacturer ?? '').toLowerCase();\n const model = String(c.Model ?? '').toLowerCase();\n const apiLevel = typeof c.Release === 'string' ? parseInt(c.Release, 10) : 0;\n\n // Android 10 = API 29 = NNAPI 1.2 floor. Below this, NNAPI partitioning\n // is poor enough that ORT often falls back to XNNPACK silently.\n if (apiLevel && apiLevel < 10) return 'system';\n\n // Pixel 6 and newer have Tensor G1/G2/G3/G4 with a real NPU.\n if (brand === 'google' && /pixel\\s*([6-9]|1\\d)/.test(model)) return 'supertonic';\n // Samsung S22+ and Tab S8+ are Snapdragon 8 Gen 1 / Exynos 2200 floor.\n if (manufacturer === 'samsung' && /sm-s9\\d\\d|sm-x[78]\\d\\d/i.test(model)) return 'supertonic';\n // OnePlus 10 Pro+, current generation flagships are usually safe.\n if (brand === 'oneplus' && /ne|le2\\d\\d\\d/i.test(model)) return 'supertonic';\n\n // Default for everything else (including the Galaxy A52 you tested on,\n // which has SD720G and gets ~10s TTFA): use the system engine.\n return 'system';\n },\n\n async isAvailable(engineId?: EngineId): Promise<boolean> {\n return getEngine(engineId).isAvailable();\n },\n\n async prefetchModel(\n onProgress?: (p: PrefetchProgress) => void,\n engineId?: EngineId\n ): Promise<void> {\n return getEngine(engineId).prefetch(onProgress);\n },\n\n async getVoices(engineId?: EngineId): Promise<Voice[]> {\n return getEngine(engineId).getVoices();\n },\n\n async speak(text: string, options: SpeakOptions = {}): Promise<void> {\n return getEngine(options.engine).speak(text, options);\n },\n\n stream(text: string, options: SpeakOptions = {}): StreamHandle {\n return getEngine(options.engine).stream(text, options);\n },\n\n async stop(engineId?: EngineId): Promise<void> {\n return getEngine(engineId).stop();\n },\n\n /** Delete locally cached model files so the next `prefetchModel()` re-downloads.\n * No-op for engines that don't have a cache (e.g. the system engine). */\n async clearCache(engineId?: EngineId): Promise<void> {\n const engine = getEngine(engineId);\n if (engine.clearCache) {\n await engine.clearCache();\n }\n },\n\n async cloneVoice(options: CloneOptions, engineId?: EngineId): Promise<ClonedVoice> {\n const engine = getEngine(engineId);\n if (!engine.cloneVoice) {\n throw new Error(`[ttskit] Engine \"${engine.id}\" does not support voice cloning.`);\n }\n return engine.cloneVoice(options);\n },\n};\n\nexport default TTSKit;\n"]}
|
package/build/types.d.ts
ADDED
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
export type EngineId = 'supertonic' | 'system' | 'neutts' | 'cloud:eleven' | 'cloud:openai' | 'cloud:cartesia';
|
|
2
|
+
export type SupertonicLang = 'en' | 'ko' | 'ja' | 'ar' | 'bg' | 'cs' | 'da' | 'de' | 'el' | 'es' | 'et' | 'fi' | 'fr' | 'hi' | 'hr' | 'hu' | 'id' | 'it' | 'lt' | 'lv' | 'nl' | 'pl' | 'pt' | 'ro' | 'ru' | 'sk' | 'sl' | 'sv' | 'tr' | 'uk' | 'vi';
|
|
3
|
+
export interface Voice {
|
|
4
|
+
id: string;
|
|
5
|
+
name: string;
|
|
6
|
+
gender?: 'male' | 'female' | 'neutral';
|
|
7
|
+
engine: EngineId;
|
|
8
|
+
language?: string;
|
|
9
|
+
sampleUrl?: string;
|
|
10
|
+
}
|
|
11
|
+
/**
|
|
12
|
+
* Options for synthesis calls.
|
|
13
|
+
*
|
|
14
|
+
* **Privacy:** the text you pass to `speak()` / `stream()` is processed
|
|
15
|
+
* entirely on-device. It is never sent to a remote server when using the
|
|
16
|
+
* `supertonic` engine. The `system` engine forwards text to the OS-level
|
|
17
|
+
* TTS service (`expo-speech`), which on some platforms (notably some
|
|
18
|
+
* Android OEMs) may route through a cloud service — verify with the
|
|
19
|
+
* device vendor's privacy policy if that matters for your app.
|
|
20
|
+
*/
|
|
21
|
+
export interface SpeakOptions {
|
|
22
|
+
voice?: string;
|
|
23
|
+
engine?: EngineId;
|
|
24
|
+
/**
|
|
25
|
+
* BCP-47 language code passed to the model.
|
|
26
|
+
* Supertonic-3 supports 31 languages (see SupertonicLang); other engines may
|
|
27
|
+
* use this differently (system engine forwards it as-is to expo-speech).
|
|
28
|
+
*/
|
|
29
|
+
language?: string;
|
|
30
|
+
/**
|
|
31
|
+
* Speech speed multiplier (default 1.05 — matches Supertonic upstream).
|
|
32
|
+
* Higher = faster.
|
|
33
|
+
*/
|
|
34
|
+
rate?: number;
|
|
35
|
+
pitch?: number;
|
|
36
|
+
volume?: number;
|
|
37
|
+
/**
|
|
38
|
+
* Number of denoising steps for diffusion-based engines (Supertonic).
|
|
39
|
+
* Default 8. Lower = faster but lower quality.
|
|
40
|
+
*/
|
|
41
|
+
totalStep?: number;
|
|
42
|
+
onStart?: () => void;
|
|
43
|
+
onDone?: () => void;
|
|
44
|
+
onError?: (err: Error) => void;
|
|
45
|
+
}
|
|
46
|
+
export interface StreamHandle {
|
|
47
|
+
id: string;
|
|
48
|
+
on(event: 'chunk', listener: (pcm: Uint8Array) => void): this;
|
|
49
|
+
on(event: 'end', listener: () => void): this;
|
|
50
|
+
on(event: 'error', listener: (err: Error) => void): this;
|
|
51
|
+
cancel(): Promise<void>;
|
|
52
|
+
}
|
|
53
|
+
export interface CloneOptions {
|
|
54
|
+
sampleUri: string;
|
|
55
|
+
name?: string;
|
|
56
|
+
}
|
|
57
|
+
export interface ClonedVoice {
|
|
58
|
+
id: string;
|
|
59
|
+
name: string;
|
|
60
|
+
engine: EngineId;
|
|
61
|
+
}
|
|
62
|
+
export interface PrefetchProgress {
|
|
63
|
+
bytesDownloaded: number;
|
|
64
|
+
totalBytes: number;
|
|
65
|
+
percent: number;
|
|
66
|
+
}
|
|
67
|
+
export interface EngineCapabilities {
|
|
68
|
+
streaming: boolean;
|
|
69
|
+
cloning: boolean;
|
|
70
|
+
emotionTags: boolean;
|
|
71
|
+
offline: boolean;
|
|
72
|
+
languages: string[];
|
|
73
|
+
}
|
|
74
|
+
export interface TTSKitError extends Error {
|
|
75
|
+
code: 'ENGINE_NOT_AVAILABLE' | 'VOICE_NOT_FOUND' | 'MODEL_NOT_LOADED' | 'SYNTHESIS_FAILED' | 'PERMISSION_DENIED' | 'NETWORK_ERROR' | 'CANCELLED';
|
|
76
|
+
}
|
|
77
|
+
//# sourceMappingURL=types.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"AAAA,MAAM,MAAM,QAAQ,GAAG,YAAY,GAAG,QAAQ,GAAG,QAAQ,GAAG,cAAc,GAAG,cAAc,GAAG,gBAAgB,CAAC;AAE/G,MAAM,MAAM,cAAc,GACtB,IAAI,GAAG,IAAI,GAAG,IAAI,GAAG,IAAI,GAAG,IAAI,GAAG,IAAI,GAAG,IAAI,GAAG,IAAI,GAAG,IAAI,GAAG,IAAI,GACnE,IAAI,GAAG,IAAI,GAAG,IAAI,GAAG,IAAI,GAAG,IAAI,GAAG,IAAI,GAAG,IAAI,GAAG,IAAI,GAAG,IAAI,GAAG,IAAI,GACnE,IAAI,GAAG,IAAI,GAAG,IAAI,GAAG,IAAI,GAAG,IAAI,GAAG,IAAI,GAAG,IAAI,GAAG,IAAI,GAAG,IAAI,GAAG,IAAI,GAAG,IAAI,CAAC;AAE/E,MAAM,WAAW,KAAK;IACpB,EAAE,EAAE,MAAM,CAAC;IACX,IAAI,EAAE,MAAM,CAAC;IACb,MAAM,CAAC,EAAE,MAAM,GAAG,QAAQ,GAAG,SAAS,CAAC;IACvC,MAAM,EAAE,QAAQ,CAAC;IACjB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,SAAS,CAAC,EAAE,MAAM,CAAC;CACpB;AAED;;;;;;;;;GASG;AACH,MAAM,WAAW,YAAY;IAC3B,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,MAAM,CAAC,EAAE,QAAQ,CAAC;IAClB;;;;OAIG;IACH,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB;;;OAGG;IACH,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB;;;OAGG;IACH,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,OAAO,CAAC,EAAE,MAAM,IAAI,CAAC;IACrB,MAAM,CAAC,EAAE,MAAM,IAAI,CAAC;IACpB,OAAO,CAAC,EAAE,CAAC,GAAG,EAAE,KAAK,KAAK,IAAI,CAAC;CAChC;AAED,MAAM,WAAW,YAAY;IAC3B,EAAE,EAAE,MAAM,CAAC;IACX,EAAE,CAAC,KAAK,EAAE,OAAO,EAAE,QAAQ,EAAE,CAAC,GAAG,EAAE,UAAU,KAAK,IAAI,GAAG,IAAI,CAAC;IAC9D,EAAE,CAAC,KAAK,EAAE,KAAK,EAAE,QAAQ,EAAE,MAAM,IAAI,GAAG,IAAI,CAAC;IAC7C,EAAE,CAAC,KAAK,EAAE,OAAO,EAAE,QAAQ,EAAE,CAAC,GAAG,EAAE,KAAK,KAAK,IAAI,GAAG,IAAI,CAAC;IACzD,MAAM,IAAI,OAAO,CAAC,IAAI,CAAC,CAAC;CACzB;AAED,MAAM,WAAW,YAAY;IAC3B,SAAS,EAAE,MAAM,CAAC;IAClB,IAAI,CAAC,EAAE,MAAM,CAAC;CACf;AAED,MAAM,WAAW,WAAW;IAC1B,EAAE,EAAE,MAAM,CAAC;IACX,IAAI,EAAE,MAAM,CAAC;IACb,MAAM,EAAE,QAAQ,CAAC;CAClB;AAED,MAAM,WAAW,gBAAgB;IAC/B,eAAe,EAAE,MAAM,CAAC;IACxB,UAAU,EAAE,MAAM,CAAC;IACnB,OAAO,EAAE,MAAM,CAAC;CACjB;AAED,MAAM,WAAW,kBAAkB;IACjC,SAAS,EAAE,OAAO,CAAC;IACnB,OAAO,EAAE,OAAO,CAAC;IACjB,WAAW,EAAE,OAAO,CAAC;IACrB,OAAO,EAAE,OAAO,CAAC;IACjB,SAAS,EAAE,MAAM,EAAE,CAAC;CACrB;AAED,MAAM,WAAW,WAAY,SAAQ,KAAK;IACxC,IAAI,EACA,sBAAsB,GACtB,iBAAiB,GACjB,kBAAkB,GAClB,kBAAkB,GAClB,mBAAmB,GACnB,eAAe,GACf,WAAW,CAAC;CACjB"}
|