use-voice-control 0.1.1 → 0.1.3
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/README.md +1 -1
- package/dist/core/deepgram.d.ts +5 -0
- package/dist/core/kokoro.d.ts +5 -0
- package/dist/index.d.ts +25 -0
- package/dist/index.js +102 -0
- package/dist/index.js.map +1 -0
- package/dist/types/types.d.ts +17 -0
- package/package.json +7 -10
- package/speech/core/KokoroTTS.js +91 -0
- package/speech/core/deepgram.ts +56 -0
- package/speech/core/kokoro.js +93 -0
- package/speech/core/kokoro.ts +81 -0
- package/speech/docs/ARCHITECTURE.md +349 -0
- package/speech/docs/HUGGINGFACE_MIGRATION.md +172 -0
- package/speech/docs/INTEGRATION.md +276 -0
- package/speech/docs/MIGRATION.md +220 -0
- package/speech/docs/QUICKSTART.md +159 -0
- package/speech/docs/README.md +167 -0
- package/speech/index.ts +54 -0
- package/speech/legacy/conversation.js +150 -0
- package/speech/legacy/main.js +56 -0
- package/speech/legacy/stt.js +161 -0
- package/speech/legacy/tts.js +56 -0
- package/speech/legacy/worker.js +71 -0
- package/speech/types/types.ts +34 -0
- package/speech/ui/AudioPlayer.js +84 -0
- package/speech/ui/ui.js +109 -0
- package/speech/ui/voice-selector.js +77 -0
- package/speech/ui/voices.js +259 -0
- package/speech/utils/audio-utils.js +82 -0
- package/speech/utils/phonemize.js +198 -0
- package/speech/utils/semantic-split.js +107 -0
- package/speech/utils/sentence-detector.js +89 -0
package/README.md
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
<p align="center">
|
|
2
|
-
<img src="https://i.imgur.com/ypVzqbg.png"
|
|
2
|
+
<img src="https://i.imgur.com/ypVzqbg.png" />
|
|
3
3
|
<br />
|
|
4
4
|
<a href="https://www.npmjs.com/package/use-voice-control"><img src="https://img.shields.io/npm/dm/use-voice-control.svg" alt="NPM Monthly Downloads"></a>
|
|
5
5
|
<a href="https://www.npmjs.com/package/use-voice-control"><img src="https://img.shields.io/npm/v/use-voice-control.svg" alt="npm version"></a>
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { TTSOptions, TTSResult } from './types/types';
|
|
2
|
+
export * from './types/types';
|
|
3
|
+
/**
|
|
4
|
+
* Generate speech from text using the specified provider
|
|
5
|
+
*
|
|
6
|
+
* @param options - TTS configuration
|
|
7
|
+
* @returns Audio buffer and content type
|
|
8
|
+
*
|
|
9
|
+
* @example
|
|
10
|
+
* ```ts
|
|
11
|
+
* // Use Kokoro (default)
|
|
12
|
+
* const audio = await generateSpeech({
|
|
13
|
+
* text: "Hello world",
|
|
14
|
+
* voice: "af_heart"
|
|
15
|
+
* });
|
|
16
|
+
*
|
|
17
|
+
* // Use Deepgram
|
|
18
|
+
* const audio = await generateSpeech({
|
|
19
|
+
* text: "Hello world",
|
|
20
|
+
* provider: "deepgram",
|
|
21
|
+
* voice: "angus"
|
|
22
|
+
* });
|
|
23
|
+
* ```
|
|
24
|
+
*/
|
|
25
|
+
export declare function generateSpeech(options: TTSOptions): Promise<TTSResult>;
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
const c = [
|
|
2
|
+
"af_heart",
|
|
3
|
+
"af_alloy",
|
|
4
|
+
"af_aoede",
|
|
5
|
+
"af_bella",
|
|
6
|
+
"af_jessica",
|
|
7
|
+
"af_nicole",
|
|
8
|
+
"af_river",
|
|
9
|
+
"af_sarah",
|
|
10
|
+
"af_sky",
|
|
11
|
+
"am_adam",
|
|
12
|
+
"am_echo",
|
|
13
|
+
"am_fable",
|
|
14
|
+
"am_fenrir",
|
|
15
|
+
"am_liam",
|
|
16
|
+
"am_michael",
|
|
17
|
+
"am_onyx"
|
|
18
|
+
], u = [
|
|
19
|
+
"angus",
|
|
20
|
+
"asteria",
|
|
21
|
+
"arcas",
|
|
22
|
+
"orion",
|
|
23
|
+
"orpheus",
|
|
24
|
+
"athena",
|
|
25
|
+
"luna",
|
|
26
|
+
"zeus",
|
|
27
|
+
"perseus",
|
|
28
|
+
"helios",
|
|
29
|
+
"hera",
|
|
30
|
+
"stella"
|
|
31
|
+
];
|
|
32
|
+
let t = null, n = null;
|
|
33
|
+
async function f() {
|
|
34
|
+
return t || (n ? (await n, t) : (n = (async () => {
|
|
35
|
+
try {
|
|
36
|
+
const e = await import("@huggingface/transformers"), { StyleTextToSpeech2Model: o, AutoTokenizer: a } = e, s = "hexgrad/Kokoro-82M", [i, l] = await Promise.all([
|
|
37
|
+
o.from_pretrained(s, {
|
|
38
|
+
device: "cpu",
|
|
39
|
+
dtype: "q8"
|
|
40
|
+
}),
|
|
41
|
+
a.from_pretrained(s)
|
|
42
|
+
]);
|
|
43
|
+
t = { model: i, tokenizer: l }, console.log("[Kokoro] Model loaded successfully");
|
|
44
|
+
} catch (r) {
|
|
45
|
+
throw console.error("[Kokoro] Failed to load model:", r), r;
|
|
46
|
+
} finally {
|
|
47
|
+
n = null;
|
|
48
|
+
}
|
|
49
|
+
})(), await n, t));
|
|
50
|
+
}
|
|
51
|
+
async function d(r, e = "af_heart") {
|
|
52
|
+
c.includes(e), await f();
|
|
53
|
+
try {
|
|
54
|
+
throw new Error("Server-side Kokoro TTS requires additional setup. Use the browser-based implementation via Web Workers.");
|
|
55
|
+
} catch (o) {
|
|
56
|
+
throw console.error("[Kokoro] Error generating speech:", o), o;
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
function h() {
|
|
60
|
+
var e, o;
|
|
61
|
+
const r = globalThis;
|
|
62
|
+
try {
|
|
63
|
+
if (typeof r.getCloudflareContext == "function") {
|
|
64
|
+
const a = r.getCloudflareContext();
|
|
65
|
+
if ((e = a == null ? void 0 : a.env) != null && e.AI) return a.env.AI;
|
|
66
|
+
}
|
|
67
|
+
} catch {
|
|
68
|
+
}
|
|
69
|
+
return ((o = r.__env__) == null ? void 0 : o.AI) ?? r.AI;
|
|
70
|
+
}
|
|
71
|
+
async function m(r, e = "angus") {
|
|
72
|
+
const o = u.includes(e) ? e : "angus", a = h();
|
|
73
|
+
if (!a)
|
|
74
|
+
throw new Error("Cloudflare AI binding not available");
|
|
75
|
+
return {
|
|
76
|
+
audio: await a.run("@cf/deepgram/aura-1", {
|
|
77
|
+
text: r.slice(0, 5e3),
|
|
78
|
+
speaker: o,
|
|
79
|
+
encoding: "mp3"
|
|
80
|
+
}),
|
|
81
|
+
contentType: "audio/mpeg"
|
|
82
|
+
};
|
|
83
|
+
}
|
|
84
|
+
async function _(r) {
|
|
85
|
+
const { text: e, provider: o = "kokoro", voice: a = "af_heart" } = r;
|
|
86
|
+
if (!e || typeof e != "string" || e.trim().length === 0)
|
|
87
|
+
throw new Error("Text is required");
|
|
88
|
+
switch (o) {
|
|
89
|
+
case "kokoro":
|
|
90
|
+
return d(e, a);
|
|
91
|
+
case "deepgram":
|
|
92
|
+
return m(e, a);
|
|
93
|
+
default:
|
|
94
|
+
throw new Error(`Unknown TTS provider: ${o}`);
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
export {
|
|
98
|
+
u as DEEPGRAM_SPEAKERS,
|
|
99
|
+
c as KOKORO_VOICES,
|
|
100
|
+
_ as generateSpeech
|
|
101
|
+
};
|
|
102
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sources":["../speech/types/types.ts","../speech/core/kokoro.ts","../speech/core/deepgram.ts","../speech/index.ts"],"sourcesContent":["/**\n * @fileoverview Type definitions for text-to-speech providers\n */\n\nexport type TTSProvider = \"kokoro\" | \"deepgram\";\n\nexport interface TTSOptions {\n text: string;\n provider?: TTSProvider;\n voice?: string;\n}\n\nexport interface TTSResult {\n audio: ArrayBuffer;\n contentType: string;\n}\n\n// Kokoro voices from the model\nexport const KOKORO_VOICES = [\n \"af_heart\", \"af_alloy\", \"af_aoede\", \"af_bella\",\n \"af_jessica\", \"af_nicole\", \"af_river\", \"af_sarah\", \"af_sky\",\n \"am_adam\", \"am_echo\", \"am_fable\", \"am_fenrir\",\n \"am_liam\", \"am_michael\", \"am_onyx\"\n] as const;\n\nexport type KokoroVoice = (typeof KOKORO_VOICES)[number];\n\n// Deepgram Aura speakers\nexport const DEEPGRAM_SPEAKERS = [\n \"angus\", \"asteria\", \"arcas\", \"orion\", \"orpheus\", \"athena\",\n \"luna\", \"zeus\", \"perseus\", \"helios\", \"hera\", \"stella\",\n] as const;\n\nexport type DeepgramSpeaker = (typeof DEEPGRAM_SPEAKERS)[number];\n","/**\n * @fileoverview Kokoro TTS provider implementation using Hugging Face transformers\n * Runs on Node.js CPU via transformers library\n */\nimport type { TTSResult } from \"../types/types\";\nimport { KOKORO_VOICES, type KokoroVoice } from \"../types/types\";\n\nlet ttsInstance: any = null;\nlet modelLoading: Promise<any> | null = null;\n\n/**\n * Lazy-load Kokoro model using Hugging Face transformers (happens once per server instance)\n */\nasync function getKokoroTTS() {\n if (ttsInstance) return ttsInstance;\n\n if (modelLoading) {\n await modelLoading;\n return ttsInstance;\n }\n\n modelLoading = (async () => {\n try {\n // Dynamic import to load transformers library. The specifier is kept in\n // a variable so the type-checker/bundler treats it as an optional runtime\n // dependency (see optionalDependencies) rather than a build-time one.\n const transformersModule = \"@huggingface/transformers\";\n const transformers: any = await import(/* @vite-ignore */ transformersModule);\n const { StyleTextToSpeech2Model, AutoTokenizer } = transformers;\n\n const model_id = \"hexgrad/Kokoro-82M\";\n\n // Load model and tokenizer in parallel\n const [model, tokenizer] = await Promise.all([\n StyleTextToSpeech2Model.from_pretrained(model_id, {\n device: \"cpu\",\n dtype: \"q8\"\n }),\n AutoTokenizer.from_pretrained(model_id)\n ]);\n\n ttsInstance = { model, tokenizer };\n\n console.log(\"[Kokoro] Model loaded successfully\");\n } catch (error) {\n console.error(\"[Kokoro] Failed to load model:\", error);\n throw error;\n } finally {\n modelLoading = null;\n }\n })();\n\n await modelLoading;\n return ttsInstance;\n}\n\n/**\n * Generate speech from text using Kokoro\n */\nexport async function generateKokoroSpeech(\n text: string,\n voice: string = \"af_heart\"\n): Promise<TTSResult> {\n // Validate voice\n const kokoroVoice = KOKORO_VOICES.includes(voice as KokoroVoice)\n ? (voice as KokoroVoice)\n : \"af_heart\";\n\n const tts = await getKokoroTTS();\n\n try {\n // For Node.js server-side TTS, we'd need the complete transformers implementation\n // This is a placeholder showing the expected interface\n // Consider using the browser-based implementation via Web Workers for production\n\n throw new Error(\"Server-side Kokoro TTS requires additional setup. Use the browser-based implementation via Web Workers.\");\n } catch (error) {\n console.error(\"[Kokoro] Error generating speech:\", error);\n throw error;\n }\n}\n","/**\n * @fileoverview Deepgram TTS provider implementation using Cloudflare Workers AI\n * Requires Cloudflare AI binding\n */\nimport type { TTSResult } from \"../types/types\";\nimport { DEEPGRAM_SPEAKERS, type DeepgramSpeaker } from \"../types/types\";\n\n/**\n * Resolve the Cloudflare Workers AI binding at runtime without a hard\n * dependency on the host application. Consumers running on Cloudflare can\n * expose the context by setting `globalThis.getCloudflareContext` (as the\n * `@opennextjs/cloudflare` / `@cloudflare/next-on-pages` helpers do) or by\n * placing the bound `env` on `globalThis.__env__`.\n */\nfunction resolveCloudflareAI(): any {\n const g = globalThis as any;\n try {\n if (typeof g.getCloudflareContext === \"function\") {\n const ctx = g.getCloudflareContext();\n if (ctx?.env?.AI) return ctx.env.AI;\n }\n } catch {\n // CF context helper threw (e.g. called outside a request scope)\n }\n return g.__env__?.AI ?? g.AI;\n}\n\n/**\n * Generate speech from text using Deepgram Aura via Cloudflare Workers AI\n */\nexport async function generateDeepgramSpeech(\n text: string,\n speaker: string = \"angus\"\n): Promise<TTSResult> {\n // Validate speaker\n const auraVoice = DEEPGRAM_SPEAKERS.includes(speaker as DeepgramSpeaker)\n ? (speaker as DeepgramSpeaker)\n : \"angus\";\n\n const ai = resolveCloudflareAI();\n\n if (!ai) {\n throw new Error(\"Cloudflare AI binding not available\");\n }\n\n const result = await ai.run(\"@cf/deepgram/aura-1\", {\n text: text.slice(0, 5000),\n speaker: auraVoice,\n encoding: \"mp3\",\n });\n\n return {\n audio: result,\n contentType: \"audio/mpeg\",\n };\n}\n","/**\n * @fileoverview Unified text-to-speech API supporting Kokoro (default) and Deepgram\n *\n * Kokoro: Faster, more natural, runs on Node CPU\n * Deepgram: Requires Cloudflare AI binding, MP3 output\n */\nimport type { TTSOptions, TTSResult } from \"./types/types\";\nimport { generateKokoroSpeech } from \"./core/kokoro\";\nimport { generateDeepgramSpeech } from \"./core/deepgram\";\n\nexport * from \"./types/types\";\n\n/**\n * Generate speech from text using the specified provider\n *\n * @param options - TTS configuration\n * @returns Audio buffer and content type\n *\n * @example\n * ```ts\n * // Use Kokoro (default)\n * const audio = await generateSpeech({\n * text: \"Hello world\",\n * voice: \"af_heart\"\n * });\n *\n * // Use Deepgram\n * const audio = await generateSpeech({\n * text: \"Hello world\",\n * provider: \"deepgram\",\n * voice: \"angus\"\n * });\n * ```\n */\nexport async function generateSpeech(\n options: TTSOptions\n): Promise<TTSResult> {\n const { text, provider = \"kokoro\", voice = \"af_heart\" } = options;\n\n if (!text || typeof text !== \"string\" || text.trim().length === 0) {\n throw new Error(\"Text is required\");\n }\n\n switch (provider) {\n case \"kokoro\":\n return generateKokoroSpeech(text, voice);\n\n case \"deepgram\":\n return generateDeepgramSpeech(text, voice);\n\n default:\n throw new Error(`Unknown TTS provider: ${provider}`);\n }\n}\n"],"names":["KOKORO_VOICES","DEEPGRAM_SPEAKERS","ttsInstance","modelLoading","getKokoroTTS","transformers","StyleTextToSpeech2Model","AutoTokenizer","model_id","model","tokenizer","error","generateKokoroSpeech","text","voice","resolveCloudflareAI","_a","_b","g","ctx","generateDeepgramSpeech","speaker","auraVoice","ai","generateSpeech","options","provider"],"mappings":"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"}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview Type definitions for text-to-speech providers
|
|
3
|
+
*/
|
|
4
|
+
export type TTSProvider = "kokoro" | "deepgram";
|
|
5
|
+
export interface TTSOptions {
|
|
6
|
+
text: string;
|
|
7
|
+
provider?: TTSProvider;
|
|
8
|
+
voice?: string;
|
|
9
|
+
}
|
|
10
|
+
export interface TTSResult {
|
|
11
|
+
audio: ArrayBuffer;
|
|
12
|
+
contentType: string;
|
|
13
|
+
}
|
|
14
|
+
export declare const KOKORO_VOICES: readonly ["af_heart", "af_alloy", "af_aoede", "af_bella", "af_jessica", "af_nicole", "af_river", "af_sarah", "af_sky", "am_adam", "am_echo", "am_fable", "am_fenrir", "am_liam", "am_michael", "am_onyx"];
|
|
15
|
+
export type KokoroVoice = (typeof KOKORO_VOICES)[number];
|
|
16
|
+
export declare const DEEPGRAM_SPEAKERS: readonly ["angus", "asteria", "arcas", "orion", "orpheus", "athena", "luna", "zeus", "perseus", "helios", "hera", "stella"];
|
|
17
|
+
export type DeepgramSpeaker = (typeof DEEPGRAM_SPEAKERS)[number];
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "use-voice-control",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.3",
|
|
4
4
|
"description": "React voice control with speech transcription, vocalization, and interruption (STT/TTS/VAD) support.",
|
|
5
5
|
"repository": {
|
|
6
6
|
"type": "git",
|
|
@@ -14,19 +14,12 @@
|
|
|
14
14
|
".": {
|
|
15
15
|
"types": "./dist/index.d.ts",
|
|
16
16
|
"import": "./dist/index.js"
|
|
17
|
-
},
|
|
18
|
-
"./hooks": {
|
|
19
|
-
"types": "./dist/hooks/index.d.ts",
|
|
20
|
-
"import": "./dist/hooks/index.js"
|
|
21
|
-
},
|
|
22
|
-
"./components": {
|
|
23
|
-
"types": "./dist/components/index.d.ts",
|
|
24
|
-
"import": "./dist/components/index.js"
|
|
25
17
|
}
|
|
26
18
|
},
|
|
27
19
|
"files": [
|
|
28
20
|
"dist",
|
|
29
|
-
"
|
|
21
|
+
"speech",
|
|
22
|
+
"README.md"
|
|
30
23
|
],
|
|
31
24
|
"scripts": {
|
|
32
25
|
"build": "tsc && vite build",
|
|
@@ -34,6 +27,7 @@
|
|
|
34
27
|
"type-check": "tsc --noEmit"
|
|
35
28
|
},
|
|
36
29
|
"dependencies": {
|
|
30
|
+
"@huggingface/transformers": "^3.8.1",
|
|
37
31
|
"lucide-react": "^0.344.0",
|
|
38
32
|
"@moonshine-ai/moonshine-js": "^0.1.29",
|
|
39
33
|
"react": "^18.0.0",
|
|
@@ -50,5 +44,8 @@
|
|
|
50
44
|
"react": "^18.0.0",
|
|
51
45
|
"react-dom": "^18.0.0"
|
|
52
46
|
},
|
|
47
|
+
"optionalDependencies": {
|
|
48
|
+
"@huggingface/transformers": "^3.0.0"
|
|
49
|
+
},
|
|
53
50
|
"license": "rights.institute/PROSPER"
|
|
54
51
|
}
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
import { StyleTextToSpeech2Model, AutoTokenizer, Tensor, RawAudio } from "https://cdn.jsdelivr.net/npm/@huggingface/transformers@3.5.1/dist/transformers.min.js";
|
|
2
|
+
|
|
3
|
+
import { phonemize } from "./phonemize.js";
|
|
4
|
+
import { getVoiceData, VOICES } from "./voices.js";
|
|
5
|
+
|
|
6
|
+
const STYLE_DIM = 256;
|
|
7
|
+
const SAMPLE_RATE = 24000;
|
|
8
|
+
|
|
9
|
+
export class KokoroTTS {
|
|
10
|
+
/**
|
|
11
|
+
* Create a new KokoroTTS instance.
|
|
12
|
+
* @param {import('@huggingface/transformers').StyleTextToSpeech2Model} model The model
|
|
13
|
+
* @param {import('@huggingface/transformers').PreTrainedTokenizer} tokenizer The tokenizer
|
|
14
|
+
*/
|
|
15
|
+
constructor(model, tokenizer) {
|
|
16
|
+
this.model = model;
|
|
17
|
+
this.tokenizer = tokenizer;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Load a KokoroTTS model from the Hugging Face Hub.
|
|
22
|
+
* @param {string} model_id The model id
|
|
23
|
+
* @param {Object} options Additional options
|
|
24
|
+
* @param {"fp32"|"fp16"|"q8"|"q4"|"q4f16"} [options.dtype="fp32"] The data type to use.
|
|
25
|
+
* @param {"wasm"|"webgpu"|"cpu"|null} [options.device=null] The device to run the model on.
|
|
26
|
+
* @param {import("@huggingface/transformers").ProgressCallback} [options.progress_callback=null] A callback function that is called with progress information.
|
|
27
|
+
* @returns {Promise<KokoroTTS>} The loaded model
|
|
28
|
+
*/
|
|
29
|
+
static async from_pretrained(model_id, { dtype = "fp32", device = null, progress_callback = null } = {}) {
|
|
30
|
+
const model = StyleTextToSpeech2Model.from_pretrained(model_id, { progress_callback, dtype, device });
|
|
31
|
+
const tokenizer = AutoTokenizer.from_pretrained(model_id, { progress_callback });
|
|
32
|
+
|
|
33
|
+
const info = await Promise.all([model, tokenizer]);
|
|
34
|
+
return new KokoroTTS(...info);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
get voices() {
|
|
38
|
+
return VOICES;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
list_voices() {
|
|
42
|
+
console.table(VOICES);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Generate audio from text.
|
|
47
|
+
*
|
|
48
|
+
* Note: The model will be loaded on the first call, and subsequent calls will use the same model.
|
|
49
|
+
* @param {string} text The input text
|
|
50
|
+
* @param {Object} options Additional options
|
|
51
|
+
* @param {keyof typeof VOICES} [options.voice="af"] The voice style to use
|
|
52
|
+
* @param {number} [options.speed=1] The speaking speed
|
|
53
|
+
* @returns {Promise<RawAudio>} The generated audio
|
|
54
|
+
*/
|
|
55
|
+
async generate(text, { voice = "af", speed = 1 } = {}) {
|
|
56
|
+
if (!VOICES.hasOwnProperty(voice)) {
|
|
57
|
+
console.error(`Voice "${voice}" not found. Available voices:`);
|
|
58
|
+
console.table(VOICES);
|
|
59
|
+
throw new Error(`Voice "${voice}" not found. Should be one of: ${Object.keys(VOICES).join(", ")}.`);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
const language = voice.at(0); // "a" or "b"
|
|
63
|
+
const phonemes = await phonemize(text, language);
|
|
64
|
+
const { input_ids } = this.tokenizer(phonemes, {
|
|
65
|
+
truncation: true,
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
// Select voice style based on number of input tokens
|
|
69
|
+
const num_tokens = Math.max(
|
|
70
|
+
input_ids.dims.at(-1) - 2, // Without padding;
|
|
71
|
+
0,
|
|
72
|
+
);
|
|
73
|
+
|
|
74
|
+
// Load voice style
|
|
75
|
+
const data = await getVoiceData(voice);
|
|
76
|
+
const offset = num_tokens * STYLE_DIM;
|
|
77
|
+
const voiceData = data.slice(offset, offset + STYLE_DIM);
|
|
78
|
+
|
|
79
|
+
// Prepare model inputs
|
|
80
|
+
const inputs = {
|
|
81
|
+
input_ids,
|
|
82
|
+
style: new Tensor("float32", voiceData, [1, STYLE_DIM]),
|
|
83
|
+
speed: new Tensor("float32", [speed], [1]),
|
|
84
|
+
};
|
|
85
|
+
|
|
86
|
+
// Generate audio
|
|
87
|
+
const { waveform } = await this.model(inputs);
|
|
88
|
+
|
|
89
|
+
return new RawAudio(waveform.data, SAMPLE_RATE);
|
|
90
|
+
}
|
|
91
|
+
}
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview Deepgram TTS provider implementation using Cloudflare Workers AI
|
|
3
|
+
* Requires Cloudflare AI binding
|
|
4
|
+
*/
|
|
5
|
+
import type { TTSResult } from "../types/types";
|
|
6
|
+
import { DEEPGRAM_SPEAKERS, type DeepgramSpeaker } from "../types/types";
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Resolve the Cloudflare Workers AI binding at runtime without a hard
|
|
10
|
+
* dependency on the host application. Consumers running on Cloudflare can
|
|
11
|
+
* expose the context by setting `globalThis.getCloudflareContext` (as the
|
|
12
|
+
* `@opennextjs/cloudflare` / `@cloudflare/next-on-pages` helpers do) or by
|
|
13
|
+
* placing the bound `env` on `globalThis.__env__`.
|
|
14
|
+
*/
|
|
15
|
+
function resolveCloudflareAI(): any {
|
|
16
|
+
const g = globalThis as any;
|
|
17
|
+
try {
|
|
18
|
+
if (typeof g.getCloudflareContext === "function") {
|
|
19
|
+
const ctx = g.getCloudflareContext();
|
|
20
|
+
if (ctx?.env?.AI) return ctx.env.AI;
|
|
21
|
+
}
|
|
22
|
+
} catch {
|
|
23
|
+
// CF context helper threw (e.g. called outside a request scope)
|
|
24
|
+
}
|
|
25
|
+
return g.__env__?.AI ?? g.AI;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Generate speech from text using Deepgram Aura via Cloudflare Workers AI
|
|
30
|
+
*/
|
|
31
|
+
export async function generateDeepgramSpeech(
|
|
32
|
+
text: string,
|
|
33
|
+
speaker: string = "angus"
|
|
34
|
+
): Promise<TTSResult> {
|
|
35
|
+
// Validate speaker
|
|
36
|
+
const auraVoice = DEEPGRAM_SPEAKERS.includes(speaker as DeepgramSpeaker)
|
|
37
|
+
? (speaker as DeepgramSpeaker)
|
|
38
|
+
: "angus";
|
|
39
|
+
|
|
40
|
+
const ai = resolveCloudflareAI();
|
|
41
|
+
|
|
42
|
+
if (!ai) {
|
|
43
|
+
throw new Error("Cloudflare AI binding not available");
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
const result = await ai.run("@cf/deepgram/aura-1", {
|
|
47
|
+
text: text.slice(0, 5000),
|
|
48
|
+
speaker: auraVoice,
|
|
49
|
+
encoding: "mp3",
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
return {
|
|
53
|
+
audio: result,
|
|
54
|
+
contentType: "audio/mpeg",
|
|
55
|
+
};
|
|
56
|
+
}
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
import { StyleTextToSpeech2Model, AutoTokenizer, Tensor, RawAudio } from "https://cdn.jsdelivr.net/npm/@huggingface/transformers@3.5.1/dist/transformers.min.js";
|
|
2
|
+
//import { StyleTextToSpeech2Model, AutoTokenizer, Tensor, RawAudio } from "@huggingface/transformers";
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
import { phonemize } from "./phonemize.js";
|
|
6
|
+
import { getVoiceData, VOICES } from "./voices.js";
|
|
7
|
+
|
|
8
|
+
const STYLE_DIM = 256;
|
|
9
|
+
const SAMPLE_RATE = 24000;
|
|
10
|
+
|
|
11
|
+
export class KokoroTTS {
|
|
12
|
+
/**
|
|
13
|
+
* Create a new KokoroTTS instance.
|
|
14
|
+
* @param {import('@huggingface/transformers').StyleTextToSpeech2Model} model The model
|
|
15
|
+
* @param {import('@huggingface/transformers').PreTrainedTokenizer} tokenizer The tokenizer
|
|
16
|
+
*/
|
|
17
|
+
constructor(model, tokenizer) {
|
|
18
|
+
this.model = model;
|
|
19
|
+
this.tokenizer = tokenizer;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Load a KokoroTTS model from the Hugging Face Hub.
|
|
24
|
+
* @param {string} model_id The model id
|
|
25
|
+
* @param {Object} options Additional options
|
|
26
|
+
* @param {"fp32"|"fp16"|"q8"|"q4"|"q4f16"} [options.dtype="fp32"] The data type to use.
|
|
27
|
+
* @param {"wasm"|"webgpu"|"cpu"|null} [options.device=null] The device to run the model on.
|
|
28
|
+
* @param {import("@huggingface/transformers").ProgressCallback} [options.progress_callback=null] A callback function that is called with progress information.
|
|
29
|
+
* @returns {Promise<KokoroTTS>} The loaded model
|
|
30
|
+
*/
|
|
31
|
+
static async from_pretrained(model_id, { dtype = "fp32", device = null, progress_callback = null } = {}) {
|
|
32
|
+
const model = StyleTextToSpeech2Model.from_pretrained(model_id, { progress_callback, dtype, device });
|
|
33
|
+
const tokenizer = AutoTokenizer.from_pretrained(model_id, { progress_callback });
|
|
34
|
+
|
|
35
|
+
const info = await Promise.all([model, tokenizer]);
|
|
36
|
+
return new KokoroTTS(...info);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
get voices() {
|
|
40
|
+
return VOICES;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
list_voices() {
|
|
44
|
+
console.table(VOICES);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Generate audio from text.
|
|
49
|
+
*
|
|
50
|
+
* Note: The model will be loaded on the first call, and subsequent calls will use the same model.
|
|
51
|
+
* @param {string} text The input text
|
|
52
|
+
* @param {Object} options Additional options
|
|
53
|
+
* @param {keyof typeof VOICES} [options.voice="af"] The voice style to use
|
|
54
|
+
* @param {number} [options.speed=1] The speaking speed
|
|
55
|
+
* @returns {Promise<RawAudio>} The generated audio
|
|
56
|
+
*/
|
|
57
|
+
async generate(text, { voice = "af", speed = 1 } = {}) {
|
|
58
|
+
if (!VOICES.hasOwnProperty(voice)) {
|
|
59
|
+
console.error(`Voice "${voice}" not found. Available voices:`);
|
|
60
|
+
console.table(VOICES);
|
|
61
|
+
throw new Error(`Voice "${voice}" not found. Should be one of: ${Object.keys(VOICES).join(", ")}.`);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
const language = voice.at(0); // "a" or "b"
|
|
65
|
+
const phonemes = await phonemize(text, language);
|
|
66
|
+
const { input_ids } = this.tokenizer(phonemes, {
|
|
67
|
+
truncation: true,
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
// Select voice style based on number of input tokens
|
|
71
|
+
const num_tokens = Math.max(
|
|
72
|
+
input_ids.dims.at(-1) - 2, // Without padding;
|
|
73
|
+
0,
|
|
74
|
+
);
|
|
75
|
+
|
|
76
|
+
// Load voice style
|
|
77
|
+
const data = await getVoiceData(voice);
|
|
78
|
+
const offset = num_tokens * STYLE_DIM;
|
|
79
|
+
const voiceData = data.slice(offset, offset + STYLE_DIM);
|
|
80
|
+
|
|
81
|
+
// Prepare model inputs
|
|
82
|
+
const inputs = {
|
|
83
|
+
input_ids,
|
|
84
|
+
style: new Tensor("float32", voiceData, [1, STYLE_DIM]),
|
|
85
|
+
speed: new Tensor("float32", [speed], [1]),
|
|
86
|
+
};
|
|
87
|
+
|
|
88
|
+
// Generate audio
|
|
89
|
+
const { waveform } = await this.model(inputs);
|
|
90
|
+
|
|
91
|
+
return new RawAudio(waveform.data, SAMPLE_RATE);
|
|
92
|
+
}
|
|
93
|
+
}
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview Kokoro TTS provider implementation using Hugging Face transformers
|
|
3
|
+
* Runs on Node.js CPU via transformers library
|
|
4
|
+
*/
|
|
5
|
+
import type { TTSResult } from "../types/types";
|
|
6
|
+
import { KOKORO_VOICES, type KokoroVoice } from "../types/types";
|
|
7
|
+
|
|
8
|
+
let ttsInstance: any = null;
|
|
9
|
+
let modelLoading: Promise<any> | null = null;
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Lazy-load Kokoro model using Hugging Face transformers (happens once per server instance)
|
|
13
|
+
*/
|
|
14
|
+
async function getKokoroTTS() {
|
|
15
|
+
if (ttsInstance) return ttsInstance;
|
|
16
|
+
|
|
17
|
+
if (modelLoading) {
|
|
18
|
+
await modelLoading;
|
|
19
|
+
return ttsInstance;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
modelLoading = (async () => {
|
|
23
|
+
try {
|
|
24
|
+
// Dynamic import to load transformers library. The specifier is kept in
|
|
25
|
+
// a variable so the type-checker/bundler treats it as an optional runtime
|
|
26
|
+
// dependency (see optionalDependencies) rather than a build-time one.
|
|
27
|
+
const transformersModule = "@huggingface/transformers";
|
|
28
|
+
const transformers: any = await import(/* @vite-ignore */ transformersModule);
|
|
29
|
+
const { StyleTextToSpeech2Model, AutoTokenizer } = transformers;
|
|
30
|
+
|
|
31
|
+
const model_id = "hexgrad/Kokoro-82M";
|
|
32
|
+
|
|
33
|
+
// Load model and tokenizer in parallel
|
|
34
|
+
const [model, tokenizer] = await Promise.all([
|
|
35
|
+
StyleTextToSpeech2Model.from_pretrained(model_id, {
|
|
36
|
+
device: "cpu",
|
|
37
|
+
dtype: "q8"
|
|
38
|
+
}),
|
|
39
|
+
AutoTokenizer.from_pretrained(model_id)
|
|
40
|
+
]);
|
|
41
|
+
|
|
42
|
+
ttsInstance = { model, tokenizer };
|
|
43
|
+
|
|
44
|
+
console.log("[Kokoro] Model loaded successfully");
|
|
45
|
+
} catch (error) {
|
|
46
|
+
console.error("[Kokoro] Failed to load model:", error);
|
|
47
|
+
throw error;
|
|
48
|
+
} finally {
|
|
49
|
+
modelLoading = null;
|
|
50
|
+
}
|
|
51
|
+
})();
|
|
52
|
+
|
|
53
|
+
await modelLoading;
|
|
54
|
+
return ttsInstance;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Generate speech from text using Kokoro
|
|
59
|
+
*/
|
|
60
|
+
export async function generateKokoroSpeech(
|
|
61
|
+
text: string,
|
|
62
|
+
voice: string = "af_heart"
|
|
63
|
+
): Promise<TTSResult> {
|
|
64
|
+
// Validate voice
|
|
65
|
+
const kokoroVoice = KOKORO_VOICES.includes(voice as KokoroVoice)
|
|
66
|
+
? (voice as KokoroVoice)
|
|
67
|
+
: "af_heart";
|
|
68
|
+
|
|
69
|
+
const tts = await getKokoroTTS();
|
|
70
|
+
|
|
71
|
+
try {
|
|
72
|
+
// For Node.js server-side TTS, we'd need the complete transformers implementation
|
|
73
|
+
// This is a placeholder showing the expected interface
|
|
74
|
+
// Consider using the browser-based implementation via Web Workers for production
|
|
75
|
+
|
|
76
|
+
throw new Error("Server-side Kokoro TTS requires additional setup. Use the browser-based implementation via Web Workers.");
|
|
77
|
+
} catch (error) {
|
|
78
|
+
console.error("[Kokoro] Error generating speech:", error);
|
|
79
|
+
throw error;
|
|
80
|
+
}
|
|
81
|
+
}
|