@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 +7 -0
- package/LICENSE.md +30 -0
- package/README.md +58 -0
- package/dist/index.cjs +161 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.ts +74 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +158 -0
- package/dist/index.js.map +1 -0
- package/package.json +60 -0
package/CHANGELOG.md
ADDED
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"]}
|
package/dist/index.d.ts
ADDED
|
@@ -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
|
+
}
|