@mastra/voice-modelslab 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/CHANGELOG.md ADDED
@@ -0,0 +1,7 @@
1
+ # @mastra/voice-modelslab
2
+
3
+ ## 0.1.0
4
+
5
+ ### Minor Changes
6
+
7
+ - Initial release: ModelsLab TTS voice provider for Mastra
package/LICENSE.md ADDED
@@ -0,0 +1,30 @@
1
+ Portions of this software are licensed as follows:
2
+
3
+ - All content that resides under any directory named "ee/" within this
4
+ repository, including but not limited to:
5
+ - `packages/core/src/auth/ee/`
6
+ - `packages/server/src/server/auth/ee/`
7
+ is licensed under the license defined in `ee/LICENSE`.
8
+
9
+ - All third-party components incorporated into the Mastra Software are
10
+ licensed under the original license provided by the owner of the
11
+ applicable component.
12
+
13
+ - Content outside of the above-mentioned directories or restrictions is
14
+ available under the "Apache License 2.0" as defined below.
15
+
16
+ # Apache License 2.0
17
+
18
+ Copyright (c) 2025 Kepler Software, Inc.
19
+
20
+ Licensed under the Apache License, Version 2.0 (the "License");
21
+ you may not use this file except in compliance with the License.
22
+ You may obtain a copy of the License at
23
+
24
+ http://www.apache.org/licenses/LICENSE-2.0
25
+
26
+ Unless required by applicable law or agreed to in writing, software
27
+ distributed under the License is distributed on an "AS IS" BASIS,
28
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
29
+ See the License for the specific language governing permissions and
30
+ limitations under the License.
package/README.md ADDED
@@ -0,0 +1,58 @@
1
+ # @mastra/voice-modelslab
2
+
3
+ [ModelsLab](https://modelslab.com) voice integration for Mastra. Provides text-to-speech using ModelsLab's TTS API.
4
+
5
+ ## Installation
6
+
7
+ ```bash
8
+ npm install @mastra/voice-modelslab
9
+ ```
10
+
11
+ ## Usage
12
+
13
+ ```typescript
14
+ import { ModelsLabVoice } from '@mastra/voice-modelslab';
15
+
16
+ const voice = new ModelsLabVoice({
17
+ speechModel: {
18
+ apiKey: process.env.MODELSLAB_API_KEY,
19
+ },
20
+ speaker: '5', // Female voice
21
+ });
22
+
23
+ // Text to speech
24
+ const audioStream = await voice.speak('Hello, world!', {
25
+ speaker: 'nova', // OpenAI-style voices also work: alloy, echo, fable, onyx, nova, shimmer
26
+ language: 'english',
27
+ speed: 1.0,
28
+ });
29
+
30
+ // List available voices
31
+ const speakers = await voice.getSpeakers();
32
+ ```
33
+
34
+ ## Configuration
35
+
36
+ | Option | Type | Default | Description |
37
+ | -------------------- | -------- | ----------------------- | ------------------------------------------- |
38
+ | `speechModel.apiKey` | `string` | `MODELSLAB_API_KEY` env | ModelsLab API key |
39
+ | `speaker` | `string` | `'1'` | Default voice ID (1–6) or OpenAI voice name |
40
+
41
+ ## Voice IDs
42
+
43
+ | ID | Name | Gender |
44
+ | --- | ------------ | ------- |
45
+ | 1 | Neutral | neutral |
46
+ | 2 | Male | male |
47
+ | 3 | Warm | male |
48
+ | 4 | Deep Male | male |
49
+ | 5 | Female | female |
50
+ | 6 | Clear Female | female |
51
+
52
+ OpenAI voice names are also accepted: `alloy`, `echo`, `fable`, `onyx`, `nova`, `shimmer`.
53
+
54
+ ## API Reference
55
+
56
+ See [ModelsLab TTS docs](https://docs.modelslab.com) for full API details.
57
+
58
+ ModelsLab uses key-in-body authentication (`MODELSLAB_API_KEY`) and asynchronous audio generation with polling.
package/dist/index.cjs ADDED
@@ -0,0 +1,161 @@
1
+ 'use strict';
2
+
3
+ var stream = require('stream');
4
+ var voice = require('@mastra/core/voice');
5
+
6
+ // src/index.ts
7
+ var MODELSLAB_TTS_URL = "https://modelslab.com/api/v6/voice/text_to_speech";
8
+ var MODELSLAB_TTS_FETCH_URL = "https://modelslab.com/api/v6/voice/fetch/";
9
+ var POLL_INTERVAL_MS = 5e3;
10
+ var POLL_TIMEOUT_MS = 3e5;
11
+ var MODELSLAB_VOICES = [
12
+ { voiceId: "1", name: "Neutral", language: "en", gender: "neutral" },
13
+ { voiceId: "2", name: "Male", language: "en", gender: "male" },
14
+ { voiceId: "3", name: "Warm", language: "en", gender: "male" },
15
+ { voiceId: "4", name: "Deep Male", language: "en", gender: "male" },
16
+ { voiceId: "5", name: "Female", language: "en", gender: "female" },
17
+ { voiceId: "6", name: "Clear Female", language: "en", gender: "female" }
18
+ ];
19
+ var OPENAI_VOICE_MAP = {
20
+ alloy: "1",
21
+ echo: "2",
22
+ fable: "3",
23
+ onyx: "4",
24
+ nova: "5",
25
+ shimmer: "6"
26
+ };
27
+ async function sleep(ms) {
28
+ return new Promise((resolve) => setTimeout(resolve, ms));
29
+ }
30
+ var ModelsLabVoice = class extends voice.MastraVoice {
31
+ apiKey;
32
+ constructor({
33
+ speechModel,
34
+ speaker
35
+ } = {}) {
36
+ const apiKey = speechModel?.apiKey ?? process.env.MODELSLAB_API_KEY;
37
+ super({
38
+ speechModel: {
39
+ name: speechModel?.name ?? "default",
40
+ apiKey
41
+ },
42
+ speaker: speaker ?? "1"
43
+ });
44
+ if (!apiKey) {
45
+ throw new Error("MODELSLAB_API_KEY is not set");
46
+ }
47
+ this.apiKey = apiKey;
48
+ }
49
+ /**
50
+ * Returns available ModelsLab voices.
51
+ */
52
+ async getSpeakers() {
53
+ return MODELSLAB_VOICES;
54
+ }
55
+ /**
56
+ * Converts text to speech using the ModelsLab TTS API.
57
+ *
58
+ * ModelsLab returns an audio URL (not a stream). This method:
59
+ * 1. POSTs to the TTS endpoint
60
+ * 2. If processing, polls until the audio URL is ready
61
+ * 3. Downloads the audio and returns a Readable stream
62
+ *
63
+ * @param input - Text to convert to speech
64
+ * @param options - Optional parameters
65
+ * @param options.speaker - ModelsLab voice ID (1–10) or OpenAI voice name (alloy, echo, etc.)
66
+ * @param options.language - Language code (default: 'english')
67
+ * @param options.speed - Speech speed (0.5–2.0, default: 1.0)
68
+ * @returns A Promise resolving to a Readable audio stream
69
+ */
70
+ async speak(input, options) {
71
+ const text = typeof input === "string" ? input : await this.streamToString(input);
72
+ const rawSpeaker = options?.speaker ?? this.speaker ?? "1";
73
+ const voiceId = OPENAI_VOICE_MAP[rawSpeaker] ?? rawSpeaker;
74
+ const body = {
75
+ key: this.apiKey,
76
+ prompt: text,
77
+ language: options?.language ?? "english",
78
+ voice_id: parseInt(voiceId, 10) || 1,
79
+ speed: options?.speed ?? 1
80
+ };
81
+ const initResp = await fetch(MODELSLAB_TTS_URL, {
82
+ method: "POST",
83
+ headers: { "Content-Type": "application/json" },
84
+ body: JSON.stringify(body)
85
+ });
86
+ if (!initResp.ok) {
87
+ throw new Error(`ModelsLab TTS failed: ${initResp.status} ${initResp.statusText}`);
88
+ }
89
+ let data = await initResp.json();
90
+ if (data.status === "error") {
91
+ throw new Error(`ModelsLab TTS error: ${data.message ?? "Unknown error"}`);
92
+ }
93
+ if (data.status === "processing") {
94
+ const requestId = String(data.request_id ?? "");
95
+ if (!requestId) {
96
+ throw new Error("ModelsLab TTS returned processing status without request_id");
97
+ }
98
+ data = await this.pollUntilReady(requestId);
99
+ }
100
+ const audioUrl = data.output;
101
+ if (!audioUrl) {
102
+ throw new Error("ModelsLab TTS returned no audio URL");
103
+ }
104
+ const audioResp = await fetch(audioUrl);
105
+ if (!audioResp.ok) {
106
+ throw new Error(`Failed to download ModelsLab audio: ${audioResp.status}`);
107
+ }
108
+ const audioBuffer = await audioResp.arrayBuffer();
109
+ const readable = new stream.Readable();
110
+ readable.push(Buffer.from(audioBuffer));
111
+ readable.push(null);
112
+ return readable;
113
+ }
114
+ /**
115
+ * ModelsLab does not provide speech-to-text. Throws NotImplemented.
116
+ */
117
+ async listen(_input, _options) {
118
+ throw new Error(
119
+ "ModelsLab does not support speech-to-text. Use a different provider for listening (e.g., @mastra/voice-deepgram)."
120
+ );
121
+ }
122
+ async pollUntilReady(requestId) {
123
+ const fetchUrl = `${MODELSLAB_TTS_FETCH_URL}${requestId}`;
124
+ const deadline = Date.now() + POLL_TIMEOUT_MS;
125
+ while (Date.now() < deadline) {
126
+ await sleep(POLL_INTERVAL_MS);
127
+ const resp = await fetch(fetchUrl, {
128
+ method: "POST",
129
+ headers: { "Content-Type": "application/json" },
130
+ body: JSON.stringify({ key: this.apiKey })
131
+ });
132
+ if (!resp.ok) {
133
+ throw new Error(`ModelsLab TTS poll failed: ${resp.status}`);
134
+ }
135
+ const data = await resp.json();
136
+ if (data.status === "error") {
137
+ throw new Error(`ModelsLab TTS failed: ${data.message ?? "Unknown error"}`);
138
+ }
139
+ if (data.status === "success") {
140
+ return data;
141
+ }
142
+ }
143
+ throw new Error(`ModelsLab TTS timed out after ${POLL_TIMEOUT_MS / 1e3}s (request_id=${requestId})`);
144
+ }
145
+ async streamToString(stream) {
146
+ const chunks = [];
147
+ for await (const chunk of stream) {
148
+ if (typeof chunk === "string") {
149
+ chunks.push(Buffer.from(chunk));
150
+ } else {
151
+ chunks.push(chunk);
152
+ }
153
+ }
154
+ return Buffer.concat(chunks).toString("utf-8");
155
+ }
156
+ };
157
+
158
+ exports.MODELSLAB_VOICES = MODELSLAB_VOICES;
159
+ exports.ModelsLabVoice = ModelsLabVoice;
160
+ //# sourceMappingURL=index.cjs.map
161
+ //# sourceMappingURL=index.cjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/index.ts"],"names":["MastraVoice","Readable"],"mappings":"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","file":"index.cjs","sourcesContent":["import { Readable } from 'node:stream';\n\nimport { MastraVoice } from '@mastra/core/voice';\n\nconst MODELSLAB_TTS_URL = 'https://modelslab.com/api/v6/voice/text_to_speech';\nconst MODELSLAB_TTS_FETCH_URL = 'https://modelslab.com/api/v6/voice/fetch/';\nconst POLL_INTERVAL_MS = 5000;\nconst POLL_TIMEOUT_MS = 300_000;\n\ntype ModelsLabModel = 'default';\n\nexport type ModelsLabVoiceId =\n | '1' // Neutral\n | '2' // Male\n | '3' // Warm\n | '4' // Deep Male\n | '5' // Female\n | '6'; // Clear Female\n\nexport const MODELSLAB_VOICES: { voiceId: ModelsLabVoiceId; name: string; language: string; gender: string }[] = [\n { voiceId: '1', name: 'Neutral', language: 'en', gender: 'neutral' },\n { voiceId: '2', name: 'Male', language: 'en', gender: 'male' },\n { voiceId: '3', name: 'Warm', language: 'en', gender: 'male' },\n { voiceId: '4', name: 'Deep Male', language: 'en', gender: 'male' },\n { voiceId: '5', name: 'Female', language: 'en', gender: 'female' },\n { voiceId: '6', name: 'Clear Female', language: 'en', gender: 'female' },\n];\n\n// OpenAI voice → ModelsLab voice_id mapping\nconst OPENAI_VOICE_MAP: Record<string, ModelsLabVoiceId> = {\n alloy: '1',\n echo: '2',\n fable: '3',\n onyx: '4',\n nova: '5',\n shimmer: '6',\n};\n\ninterface ModelsLabVoiceConfig {\n name?: ModelsLabModel;\n apiKey?: string;\n}\n\ninterface TtsApiResponse {\n status: 'success' | 'processing' | 'error';\n output?: string;\n request_id?: string | number;\n eta?: number;\n message?: string;\n}\n\nasync function sleep(ms: number): Promise<void> {\n return new Promise(resolve => setTimeout(resolve, ms));\n}\n\n/**\n * ModelsLab voice provider for Mastra.\n *\n * Uses ModelsLab's TTS API with key-in-body authentication and async polling.\n * API docs: https://docs.modelslab.com\n *\n * @example\n * ```ts\n * const voice = new ModelsLabVoice({\n * speechModel: { apiKey: process.env.MODELSLAB_API_KEY },\n * speaker: '5', // Female voice\n * });\n *\n * const stream = await voice.speak('Hello, world!');\n * ```\n */\nexport class ModelsLabVoice extends MastraVoice {\n private apiKey: string;\n\n constructor({\n speechModel,\n speaker,\n }: {\n speechModel?: ModelsLabVoiceConfig;\n speaker?: ModelsLabVoiceId | string;\n } = {}) {\n const apiKey = speechModel?.apiKey ?? process.env.MODELSLAB_API_KEY;\n\n super({\n speechModel: {\n name: speechModel?.name ?? 'default',\n apiKey,\n },\n speaker: speaker ?? '1',\n });\n\n if (!apiKey) {\n throw new Error('MODELSLAB_API_KEY is not set');\n }\n\n this.apiKey = apiKey;\n }\n\n /**\n * Returns available ModelsLab voices.\n */\n async getSpeakers(): Promise<{ voiceId: string; name: string; language: string; gender: string }[]> {\n return MODELSLAB_VOICES;\n }\n\n /**\n * Converts text to speech using the ModelsLab TTS API.\n *\n * ModelsLab returns an audio URL (not a stream). This method:\n * 1. POSTs to the TTS endpoint\n * 2. If processing, polls until the audio URL is ready\n * 3. Downloads the audio and returns a Readable stream\n *\n * @param input - Text to convert to speech\n * @param options - Optional parameters\n * @param options.speaker - ModelsLab voice ID (1–10) or OpenAI voice name (alloy, echo, etc.)\n * @param options.language - Language code (default: 'english')\n * @param options.speed - Speech speed (0.5–2.0, default: 1.0)\n * @returns A Promise resolving to a Readable audio stream\n */\n async speak(\n input: string | NodeJS.ReadableStream,\n options?: {\n speaker?: ModelsLabVoiceId | string;\n language?: string;\n speed?: number;\n [key: string]: unknown;\n },\n ): Promise<NodeJS.ReadableStream> {\n const text = typeof input === 'string' ? input : await this.streamToString(input);\n\n // Resolve voice_id: accept numeric ID or OpenAI-style voice name\n const rawSpeaker = options?.speaker ?? this.speaker ?? '1';\n const voiceId = OPENAI_VOICE_MAP[rawSpeaker] ?? rawSpeaker;\n\n const body = {\n key: this.apiKey,\n prompt: text,\n language: options?.language ?? 'english',\n voice_id: parseInt(voiceId, 10) || 1,\n speed: options?.speed ?? 1.0,\n };\n\n const initResp = await fetch(MODELSLAB_TTS_URL, {\n method: 'POST',\n headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify(body),\n });\n\n if (!initResp.ok) {\n throw new Error(`ModelsLab TTS failed: ${initResp.status} ${initResp.statusText}`);\n }\n\n let data = (await initResp.json()) as TtsApiResponse;\n\n if (data.status === 'error') {\n throw new Error(`ModelsLab TTS error: ${data.message ?? 'Unknown error'}`);\n }\n\n if (data.status === 'processing') {\n const requestId = String(data.request_id ?? '');\n if (!requestId) {\n throw new Error('ModelsLab TTS returned processing status without request_id');\n }\n data = await this.pollUntilReady(requestId);\n }\n\n const audioUrl = data.output;\n if (!audioUrl) {\n throw new Error('ModelsLab TTS returned no audio URL');\n }\n\n // Download audio and return as Readable stream\n const audioResp = await fetch(audioUrl);\n if (!audioResp.ok) {\n throw new Error(`Failed to download ModelsLab audio: ${audioResp.status}`);\n }\n\n const audioBuffer = await audioResp.arrayBuffer();\n const readable = new Readable();\n readable.push(Buffer.from(audioBuffer));\n readable.push(null);\n\n return readable;\n }\n\n /**\n * ModelsLab does not provide speech-to-text. Throws NotImplemented.\n */\n async listen(_input: NodeJS.ReadableStream, _options?: Record<string, unknown>): Promise<string> {\n throw new Error(\n 'ModelsLab does not support speech-to-text. Use a different provider for listening (e.g., @mastra/voice-deepgram).',\n );\n }\n\n private async pollUntilReady(requestId: string): Promise<TtsApiResponse> {\n const fetchUrl = `${MODELSLAB_TTS_FETCH_URL}${requestId}`;\n const deadline = Date.now() + POLL_TIMEOUT_MS;\n\n while (Date.now() < deadline) {\n await sleep(POLL_INTERVAL_MS);\n\n const resp = await fetch(fetchUrl, {\n method: 'POST',\n headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify({ key: this.apiKey }),\n });\n\n if (!resp.ok) {\n throw new Error(`ModelsLab TTS poll failed: ${resp.status}`);\n }\n\n const data = (await resp.json()) as TtsApiResponse;\n\n if (data.status === 'error') {\n throw new Error(`ModelsLab TTS failed: ${data.message ?? 'Unknown error'}`);\n }\n\n if (data.status === 'success') {\n return data;\n }\n // status === 'processing' → keep polling\n }\n\n throw new Error(`ModelsLab TTS timed out after ${POLL_TIMEOUT_MS / 1000}s (request_id=${requestId})`);\n }\n\n private async streamToString(stream: NodeJS.ReadableStream): Promise<string> {\n const chunks: Buffer[] = [];\n for await (const chunk of stream) {\n if (typeof chunk === 'string') {\n chunks.push(Buffer.from(chunk));\n } else {\n chunks.push(chunk as Buffer);\n }\n }\n return Buffer.concat(chunks).toString('utf-8');\n }\n}\n"]}
@@ -0,0 +1,74 @@
1
+ import { MastraVoice } from '@mastra/core/voice';
2
+ type ModelsLabModel = 'default';
3
+ export type ModelsLabVoiceId = '1' | '2' | '3' | '4' | '5' | '6';
4
+ export declare const MODELSLAB_VOICES: {
5
+ voiceId: ModelsLabVoiceId;
6
+ name: string;
7
+ language: string;
8
+ gender: string;
9
+ }[];
10
+ interface ModelsLabVoiceConfig {
11
+ name?: ModelsLabModel;
12
+ apiKey?: string;
13
+ }
14
+ /**
15
+ * ModelsLab voice provider for Mastra.
16
+ *
17
+ * Uses ModelsLab's TTS API with key-in-body authentication and async polling.
18
+ * API docs: https://docs.modelslab.com
19
+ *
20
+ * @example
21
+ * ```ts
22
+ * const voice = new ModelsLabVoice({
23
+ * speechModel: { apiKey: process.env.MODELSLAB_API_KEY },
24
+ * speaker: '5', // Female voice
25
+ * });
26
+ *
27
+ * const stream = await voice.speak('Hello, world!');
28
+ * ```
29
+ */
30
+ export declare class ModelsLabVoice extends MastraVoice {
31
+ private apiKey;
32
+ constructor({ speechModel, speaker, }?: {
33
+ speechModel?: ModelsLabVoiceConfig;
34
+ speaker?: ModelsLabVoiceId | string;
35
+ });
36
+ /**
37
+ * Returns available ModelsLab voices.
38
+ */
39
+ getSpeakers(): Promise<{
40
+ voiceId: string;
41
+ name: string;
42
+ language: string;
43
+ gender: string;
44
+ }[]>;
45
+ /**
46
+ * Converts text to speech using the ModelsLab TTS API.
47
+ *
48
+ * ModelsLab returns an audio URL (not a stream). This method:
49
+ * 1. POSTs to the TTS endpoint
50
+ * 2. If processing, polls until the audio URL is ready
51
+ * 3. Downloads the audio and returns a Readable stream
52
+ *
53
+ * @param input - Text to convert to speech
54
+ * @param options - Optional parameters
55
+ * @param options.speaker - ModelsLab voice ID (1–10) or OpenAI voice name (alloy, echo, etc.)
56
+ * @param options.language - Language code (default: 'english')
57
+ * @param options.speed - Speech speed (0.5–2.0, default: 1.0)
58
+ * @returns A Promise resolving to a Readable audio stream
59
+ */
60
+ speak(input: string | NodeJS.ReadableStream, options?: {
61
+ speaker?: ModelsLabVoiceId | string;
62
+ language?: string;
63
+ speed?: number;
64
+ [key: string]: unknown;
65
+ }): Promise<NodeJS.ReadableStream>;
66
+ /**
67
+ * ModelsLab does not provide speech-to-text. Throws NotImplemented.
68
+ */
69
+ listen(_input: NodeJS.ReadableStream, _options?: Record<string, unknown>): Promise<string>;
70
+ private pollUntilReady;
71
+ private streamToString;
72
+ }
73
+ export {};
74
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAEA,OAAO,EAAE,WAAW,EAAE,MAAM,oBAAoB,CAAC;AAOjD,KAAK,cAAc,GAAG,SAAS,CAAC;AAEhC,MAAM,MAAM,gBAAgB,GACxB,GAAG,GACH,GAAG,GACH,GAAG,GACH,GAAG,GACH,GAAG,GACH,GAAG,CAAC;AAER,eAAO,MAAM,gBAAgB,EAAE;IAAE,OAAO,EAAE,gBAAgB,CAAC;IAAC,IAAI,EAAE,MAAM,CAAC;IAAC,QAAQ,EAAE,MAAM,CAAC;IAAC,MAAM,EAAE,MAAM,CAAA;CAAE,EAO3G,CAAC;AAYF,UAAU,oBAAoB;IAC5B,IAAI,CAAC,EAAE,cAAc,CAAC;IACtB,MAAM,CAAC,EAAE,MAAM,CAAC;CACjB;AAcD;;;;;;;;;;;;;;;GAeG;AACH,qBAAa,cAAe,SAAQ,WAAW;IAC7C,OAAO,CAAC,MAAM,CAAS;gBAEX,EACV,WAAW,EACX,OAAO,GACR,GAAE;QACD,WAAW,CAAC,EAAE,oBAAoB,CAAC;QACnC,OAAO,CAAC,EAAE,gBAAgB,GAAG,MAAM,CAAC;KAChC;IAkBN;;OAEG;IACG,WAAW,IAAI,OAAO,CAAC;QAAE,OAAO,EAAE,MAAM,CAAC;QAAC,IAAI,EAAE,MAAM,CAAC;QAAC,QAAQ,EAAE,MAAM,CAAC;QAAC,MAAM,EAAE,MAAM,CAAA;KAAE,EAAE,CAAC;IAInG;;;;;;;;;;;;;;OAcG;IACG,KAAK,CACT,KAAK,EAAE,MAAM,GAAG,MAAM,CAAC,cAAc,EACrC,OAAO,CAAC,EAAE;QACR,OAAO,CAAC,EAAE,gBAAgB,GAAG,MAAM,CAAC;QACpC,QAAQ,CAAC,EAAE,MAAM,CAAC;QAClB,KAAK,CAAC,EAAE,MAAM,CAAC;QACf,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC;KACxB,GACA,OAAO,CAAC,MAAM,CAAC,cAAc,CAAC;IA0DjC;;OAEG;IACG,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,cAAc,EAAE,QAAQ,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,OAAO,CAAC,MAAM,CAAC;YAMlF,cAAc;YAgCd,cAAc;CAW7B"}
package/dist/index.js ADDED
@@ -0,0 +1,158 @@
1
+ import { Readable } from 'stream';
2
+ import { MastraVoice } from '@mastra/core/voice';
3
+
4
+ // src/index.ts
5
+ var MODELSLAB_TTS_URL = "https://modelslab.com/api/v6/voice/text_to_speech";
6
+ var MODELSLAB_TTS_FETCH_URL = "https://modelslab.com/api/v6/voice/fetch/";
7
+ var POLL_INTERVAL_MS = 5e3;
8
+ var POLL_TIMEOUT_MS = 3e5;
9
+ var MODELSLAB_VOICES = [
10
+ { voiceId: "1", name: "Neutral", language: "en", gender: "neutral" },
11
+ { voiceId: "2", name: "Male", language: "en", gender: "male" },
12
+ { voiceId: "3", name: "Warm", language: "en", gender: "male" },
13
+ { voiceId: "4", name: "Deep Male", language: "en", gender: "male" },
14
+ { voiceId: "5", name: "Female", language: "en", gender: "female" },
15
+ { voiceId: "6", name: "Clear Female", language: "en", gender: "female" }
16
+ ];
17
+ var OPENAI_VOICE_MAP = {
18
+ alloy: "1",
19
+ echo: "2",
20
+ fable: "3",
21
+ onyx: "4",
22
+ nova: "5",
23
+ shimmer: "6"
24
+ };
25
+ async function sleep(ms) {
26
+ return new Promise((resolve) => setTimeout(resolve, ms));
27
+ }
28
+ var ModelsLabVoice = class extends MastraVoice {
29
+ apiKey;
30
+ constructor({
31
+ speechModel,
32
+ speaker
33
+ } = {}) {
34
+ const apiKey = speechModel?.apiKey ?? process.env.MODELSLAB_API_KEY;
35
+ super({
36
+ speechModel: {
37
+ name: speechModel?.name ?? "default",
38
+ apiKey
39
+ },
40
+ speaker: speaker ?? "1"
41
+ });
42
+ if (!apiKey) {
43
+ throw new Error("MODELSLAB_API_KEY is not set");
44
+ }
45
+ this.apiKey = apiKey;
46
+ }
47
+ /**
48
+ * Returns available ModelsLab voices.
49
+ */
50
+ async getSpeakers() {
51
+ return MODELSLAB_VOICES;
52
+ }
53
+ /**
54
+ * Converts text to speech using the ModelsLab TTS API.
55
+ *
56
+ * ModelsLab returns an audio URL (not a stream). This method:
57
+ * 1. POSTs to the TTS endpoint
58
+ * 2. If processing, polls until the audio URL is ready
59
+ * 3. Downloads the audio and returns a Readable stream
60
+ *
61
+ * @param input - Text to convert to speech
62
+ * @param options - Optional parameters
63
+ * @param options.speaker - ModelsLab voice ID (1–10) or OpenAI voice name (alloy, echo, etc.)
64
+ * @param options.language - Language code (default: 'english')
65
+ * @param options.speed - Speech speed (0.5–2.0, default: 1.0)
66
+ * @returns A Promise resolving to a Readable audio stream
67
+ */
68
+ async speak(input, options) {
69
+ const text = typeof input === "string" ? input : await this.streamToString(input);
70
+ const rawSpeaker = options?.speaker ?? this.speaker ?? "1";
71
+ const voiceId = OPENAI_VOICE_MAP[rawSpeaker] ?? rawSpeaker;
72
+ const body = {
73
+ key: this.apiKey,
74
+ prompt: text,
75
+ language: options?.language ?? "english",
76
+ voice_id: parseInt(voiceId, 10) || 1,
77
+ speed: options?.speed ?? 1
78
+ };
79
+ const initResp = await fetch(MODELSLAB_TTS_URL, {
80
+ method: "POST",
81
+ headers: { "Content-Type": "application/json" },
82
+ body: JSON.stringify(body)
83
+ });
84
+ if (!initResp.ok) {
85
+ throw new Error(`ModelsLab TTS failed: ${initResp.status} ${initResp.statusText}`);
86
+ }
87
+ let data = await initResp.json();
88
+ if (data.status === "error") {
89
+ throw new Error(`ModelsLab TTS error: ${data.message ?? "Unknown error"}`);
90
+ }
91
+ if (data.status === "processing") {
92
+ const requestId = String(data.request_id ?? "");
93
+ if (!requestId) {
94
+ throw new Error("ModelsLab TTS returned processing status without request_id");
95
+ }
96
+ data = await this.pollUntilReady(requestId);
97
+ }
98
+ const audioUrl = data.output;
99
+ if (!audioUrl) {
100
+ throw new Error("ModelsLab TTS returned no audio URL");
101
+ }
102
+ const audioResp = await fetch(audioUrl);
103
+ if (!audioResp.ok) {
104
+ throw new Error(`Failed to download ModelsLab audio: ${audioResp.status}`);
105
+ }
106
+ const audioBuffer = await audioResp.arrayBuffer();
107
+ const readable = new Readable();
108
+ readable.push(Buffer.from(audioBuffer));
109
+ readable.push(null);
110
+ return readable;
111
+ }
112
+ /**
113
+ * ModelsLab does not provide speech-to-text. Throws NotImplemented.
114
+ */
115
+ async listen(_input, _options) {
116
+ throw new Error(
117
+ "ModelsLab does not support speech-to-text. Use a different provider for listening (e.g., @mastra/voice-deepgram)."
118
+ );
119
+ }
120
+ async pollUntilReady(requestId) {
121
+ const fetchUrl = `${MODELSLAB_TTS_FETCH_URL}${requestId}`;
122
+ const deadline = Date.now() + POLL_TIMEOUT_MS;
123
+ while (Date.now() < deadline) {
124
+ await sleep(POLL_INTERVAL_MS);
125
+ const resp = await fetch(fetchUrl, {
126
+ method: "POST",
127
+ headers: { "Content-Type": "application/json" },
128
+ body: JSON.stringify({ key: this.apiKey })
129
+ });
130
+ if (!resp.ok) {
131
+ throw new Error(`ModelsLab TTS poll failed: ${resp.status}`);
132
+ }
133
+ const data = await resp.json();
134
+ if (data.status === "error") {
135
+ throw new Error(`ModelsLab TTS failed: ${data.message ?? "Unknown error"}`);
136
+ }
137
+ if (data.status === "success") {
138
+ return data;
139
+ }
140
+ }
141
+ throw new Error(`ModelsLab TTS timed out after ${POLL_TIMEOUT_MS / 1e3}s (request_id=${requestId})`);
142
+ }
143
+ async streamToString(stream) {
144
+ const chunks = [];
145
+ for await (const chunk of stream) {
146
+ if (typeof chunk === "string") {
147
+ chunks.push(Buffer.from(chunk));
148
+ } else {
149
+ chunks.push(chunk);
150
+ }
151
+ }
152
+ return Buffer.concat(chunks).toString("utf-8");
153
+ }
154
+ };
155
+
156
+ export { MODELSLAB_VOICES, ModelsLabVoice };
157
+ //# sourceMappingURL=index.js.map
158
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/index.ts"],"names":[],"mappings":"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","file":"index.js","sourcesContent":["import { Readable } from 'node:stream';\n\nimport { MastraVoice } from '@mastra/core/voice';\n\nconst MODELSLAB_TTS_URL = 'https://modelslab.com/api/v6/voice/text_to_speech';\nconst MODELSLAB_TTS_FETCH_URL = 'https://modelslab.com/api/v6/voice/fetch/';\nconst POLL_INTERVAL_MS = 5000;\nconst POLL_TIMEOUT_MS = 300_000;\n\ntype ModelsLabModel = 'default';\n\nexport type ModelsLabVoiceId =\n | '1' // Neutral\n | '2' // Male\n | '3' // Warm\n | '4' // Deep Male\n | '5' // Female\n | '6'; // Clear Female\n\nexport const MODELSLAB_VOICES: { voiceId: ModelsLabVoiceId; name: string; language: string; gender: string }[] = [\n { voiceId: '1', name: 'Neutral', language: 'en', gender: 'neutral' },\n { voiceId: '2', name: 'Male', language: 'en', gender: 'male' },\n { voiceId: '3', name: 'Warm', language: 'en', gender: 'male' },\n { voiceId: '4', name: 'Deep Male', language: 'en', gender: 'male' },\n { voiceId: '5', name: 'Female', language: 'en', gender: 'female' },\n { voiceId: '6', name: 'Clear Female', language: 'en', gender: 'female' },\n];\n\n// OpenAI voice → ModelsLab voice_id mapping\nconst OPENAI_VOICE_MAP: Record<string, ModelsLabVoiceId> = {\n alloy: '1',\n echo: '2',\n fable: '3',\n onyx: '4',\n nova: '5',\n shimmer: '6',\n};\n\ninterface ModelsLabVoiceConfig {\n name?: ModelsLabModel;\n apiKey?: string;\n}\n\ninterface TtsApiResponse {\n status: 'success' | 'processing' | 'error';\n output?: string;\n request_id?: string | number;\n eta?: number;\n message?: string;\n}\n\nasync function sleep(ms: number): Promise<void> {\n return new Promise(resolve => setTimeout(resolve, ms));\n}\n\n/**\n * ModelsLab voice provider for Mastra.\n *\n * Uses ModelsLab's TTS API with key-in-body authentication and async polling.\n * API docs: https://docs.modelslab.com\n *\n * @example\n * ```ts\n * const voice = new ModelsLabVoice({\n * speechModel: { apiKey: process.env.MODELSLAB_API_KEY },\n * speaker: '5', // Female voice\n * });\n *\n * const stream = await voice.speak('Hello, world!');\n * ```\n */\nexport class ModelsLabVoice extends MastraVoice {\n private apiKey: string;\n\n constructor({\n speechModel,\n speaker,\n }: {\n speechModel?: ModelsLabVoiceConfig;\n speaker?: ModelsLabVoiceId | string;\n } = {}) {\n const apiKey = speechModel?.apiKey ?? process.env.MODELSLAB_API_KEY;\n\n super({\n speechModel: {\n name: speechModel?.name ?? 'default',\n apiKey,\n },\n speaker: speaker ?? '1',\n });\n\n if (!apiKey) {\n throw new Error('MODELSLAB_API_KEY is not set');\n }\n\n this.apiKey = apiKey;\n }\n\n /**\n * Returns available ModelsLab voices.\n */\n async getSpeakers(): Promise<{ voiceId: string; name: string; language: string; gender: string }[]> {\n return MODELSLAB_VOICES;\n }\n\n /**\n * Converts text to speech using the ModelsLab TTS API.\n *\n * ModelsLab returns an audio URL (not a stream). This method:\n * 1. POSTs to the TTS endpoint\n * 2. If processing, polls until the audio URL is ready\n * 3. Downloads the audio and returns a Readable stream\n *\n * @param input - Text to convert to speech\n * @param options - Optional parameters\n * @param options.speaker - ModelsLab voice ID (1–10) or OpenAI voice name (alloy, echo, etc.)\n * @param options.language - Language code (default: 'english')\n * @param options.speed - Speech speed (0.5–2.0, default: 1.0)\n * @returns A Promise resolving to a Readable audio stream\n */\n async speak(\n input: string | NodeJS.ReadableStream,\n options?: {\n speaker?: ModelsLabVoiceId | string;\n language?: string;\n speed?: number;\n [key: string]: unknown;\n },\n ): Promise<NodeJS.ReadableStream> {\n const text = typeof input === 'string' ? input : await this.streamToString(input);\n\n // Resolve voice_id: accept numeric ID or OpenAI-style voice name\n const rawSpeaker = options?.speaker ?? this.speaker ?? '1';\n const voiceId = OPENAI_VOICE_MAP[rawSpeaker] ?? rawSpeaker;\n\n const body = {\n key: this.apiKey,\n prompt: text,\n language: options?.language ?? 'english',\n voice_id: parseInt(voiceId, 10) || 1,\n speed: options?.speed ?? 1.0,\n };\n\n const initResp = await fetch(MODELSLAB_TTS_URL, {\n method: 'POST',\n headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify(body),\n });\n\n if (!initResp.ok) {\n throw new Error(`ModelsLab TTS failed: ${initResp.status} ${initResp.statusText}`);\n }\n\n let data = (await initResp.json()) as TtsApiResponse;\n\n if (data.status === 'error') {\n throw new Error(`ModelsLab TTS error: ${data.message ?? 'Unknown error'}`);\n }\n\n if (data.status === 'processing') {\n const requestId = String(data.request_id ?? '');\n if (!requestId) {\n throw new Error('ModelsLab TTS returned processing status without request_id');\n }\n data = await this.pollUntilReady(requestId);\n }\n\n const audioUrl = data.output;\n if (!audioUrl) {\n throw new Error('ModelsLab TTS returned no audio URL');\n }\n\n // Download audio and return as Readable stream\n const audioResp = await fetch(audioUrl);\n if (!audioResp.ok) {\n throw new Error(`Failed to download ModelsLab audio: ${audioResp.status}`);\n }\n\n const audioBuffer = await audioResp.arrayBuffer();\n const readable = new Readable();\n readable.push(Buffer.from(audioBuffer));\n readable.push(null);\n\n return readable;\n }\n\n /**\n * ModelsLab does not provide speech-to-text. Throws NotImplemented.\n */\n async listen(_input: NodeJS.ReadableStream, _options?: Record<string, unknown>): Promise<string> {\n throw new Error(\n 'ModelsLab does not support speech-to-text. Use a different provider for listening (e.g., @mastra/voice-deepgram).',\n );\n }\n\n private async pollUntilReady(requestId: string): Promise<TtsApiResponse> {\n const fetchUrl = `${MODELSLAB_TTS_FETCH_URL}${requestId}`;\n const deadline = Date.now() + POLL_TIMEOUT_MS;\n\n while (Date.now() < deadline) {\n await sleep(POLL_INTERVAL_MS);\n\n const resp = await fetch(fetchUrl, {\n method: 'POST',\n headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify({ key: this.apiKey }),\n });\n\n if (!resp.ok) {\n throw new Error(`ModelsLab TTS poll failed: ${resp.status}`);\n }\n\n const data = (await resp.json()) as TtsApiResponse;\n\n if (data.status === 'error') {\n throw new Error(`ModelsLab TTS failed: ${data.message ?? 'Unknown error'}`);\n }\n\n if (data.status === 'success') {\n return data;\n }\n // status === 'processing' → keep polling\n }\n\n throw new Error(`ModelsLab TTS timed out after ${POLL_TIMEOUT_MS / 1000}s (request_id=${requestId})`);\n }\n\n private async streamToString(stream: NodeJS.ReadableStream): Promise<string> {\n const chunks: Buffer[] = [];\n for await (const chunk of stream) {\n if (typeof chunk === 'string') {\n chunks.push(Buffer.from(chunk));\n } else {\n chunks.push(chunk as Buffer);\n }\n }\n return Buffer.concat(chunks).toString('utf-8');\n }\n}\n"]}
package/package.json ADDED
@@ -0,0 +1,60 @@
1
+ {
2
+ "name": "@mastra/voice-modelslab",
3
+ "version": "0.1.0",
4
+ "description": "Mastra ModelsLab voice integration",
5
+ "type": "module",
6
+ "files": [
7
+ "dist",
8
+ "CHANGELOG.md"
9
+ ],
10
+ "main": "dist/index.js",
11
+ "types": "dist/index.d.ts",
12
+ "exports": {
13
+ ".": {
14
+ "import": {
15
+ "types": "./dist/index.d.ts",
16
+ "default": "./dist/index.js"
17
+ },
18
+ "require": {
19
+ "types": "./dist/index.d.ts",
20
+ "default": "./dist/index.cjs"
21
+ }
22
+ },
23
+ "./package.json": "./package.json"
24
+ },
25
+ "license": "Apache-2.0",
26
+ "dependencies": {},
27
+ "devDependencies": {
28
+ "@types/node": "22.19.7",
29
+ "@vitest/coverage-v8": "4.0.18",
30
+ "@vitest/ui": "4.0.18",
31
+ "eslint": "^9.37.0",
32
+ "tsup": "^8.5.1",
33
+ "typescript": "^5.9.3",
34
+ "vitest": "4.0.18",
35
+ "@internal/types-builder": "0.0.41",
36
+ "@internal/lint": "0.0.66",
37
+ "@mastra/core": "1.10.0"
38
+ },
39
+ "peerDependencies": {
40
+ "@mastra/core": ">=1.0.0-0 <2.0.0-0"
41
+ },
42
+ "homepage": "https://mastra.ai",
43
+ "repository": {
44
+ "type": "git",
45
+ "url": "git+https://github.com/mastra-ai/mastra.git",
46
+ "directory": "voice/modelslab"
47
+ },
48
+ "bugs": {
49
+ "url": "https://github.com/mastra-ai/mastra/issues"
50
+ },
51
+ "engines": {
52
+ "node": ">=22.13.0"
53
+ },
54
+ "scripts": {
55
+ "build": "tsup --silent --config tsup.config.ts",
56
+ "build:watch": "tsup build --watch && tsc -p tsconfig.build.json",
57
+ "test": "vitest run",
58
+ "lint": "eslint ."
59
+ }
60
+ }