voicemix 1.3.4 → 1.3.6
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/package.json +1 -2
- package/providers/cartesia.js +34 -60
- package/providers/elevenlabs.js +34 -39
- package/providers/resemble.js +41 -34
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "voicemix",
|
|
3
3
|
"type": "module",
|
|
4
|
-
"version": "1.3.
|
|
4
|
+
"version": "1.3.6",
|
|
5
5
|
"description": "🗣️ VoiceMix - A simple text-to-speech tool using ElevenLabs, Cartesia and Resemble AI APIs.",
|
|
6
6
|
"main": "index.js",
|
|
7
7
|
"repository": {
|
|
@@ -14,7 +14,6 @@
|
|
|
14
14
|
"author": "Martin Clasen",
|
|
15
15
|
"license": "MIT",
|
|
16
16
|
"dependencies": {
|
|
17
|
-
"axios": "^1.13.4",
|
|
18
17
|
"hash-factory": "^1.1.2"
|
|
19
18
|
},
|
|
20
19
|
"keywords": [
|
package/providers/cartesia.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import axios from 'axios';
|
|
2
1
|
import fs from 'fs';
|
|
3
2
|
import path from 'path';
|
|
3
|
+
import { Readable } from 'stream';
|
|
4
4
|
import { pipeline } from 'stream/promises';
|
|
5
5
|
import { ProviderError } from '../errors.js';
|
|
6
6
|
|
|
@@ -19,33 +19,31 @@ export class CartesiaProvider {
|
|
|
19
19
|
|
|
20
20
|
}
|
|
21
21
|
|
|
22
|
-
|
|
22
|
+
_getFetchParams(voiceId, text, format = 'mp3') {
|
|
23
23
|
// Configure output format based on requested format
|
|
24
|
-
const outputFormat = format === 'mp3'
|
|
24
|
+
const outputFormat = format === 'mp3'
|
|
25
25
|
? { container: 'mp3', encoding: 'mp3', sample_rate: 44100 }
|
|
26
26
|
: { container: 'wav', encoding: 'pcm_f32le', sample_rate: 44100 };
|
|
27
27
|
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
'Cartesia-Version': '2024-06-10',
|
|
34
|
-
'Content-Type': 'application/json'
|
|
35
|
-
},
|
|
36
|
-
data: {
|
|
37
|
-
model_id: this.defaultSettings.model_id,
|
|
38
|
-
transcript: text,
|
|
39
|
-
voice: {
|
|
40
|
-
mode: 'id',
|
|
41
|
-
id: voiceId
|
|
42
|
-
},
|
|
43
|
-
output_format: outputFormat,
|
|
44
|
-
speed: this.defaultSettings.speed,
|
|
45
|
-
generation_config: this.defaultSettings.generation_config
|
|
46
|
-
},
|
|
47
|
-
responseType: 'stream'
|
|
28
|
+
const url = `${this.baseUrl}/tts/bytes`;
|
|
29
|
+
const headers = {
|
|
30
|
+
'Authorization': `Bearer ${this.apiKey}`,
|
|
31
|
+
'Cartesia-Version': '2024-06-10',
|
|
32
|
+
'Content-Type': 'application/json'
|
|
48
33
|
};
|
|
34
|
+
const body = JSON.stringify({
|
|
35
|
+
model_id: this.defaultSettings.model_id,
|
|
36
|
+
transcript: text,
|
|
37
|
+
voice: {
|
|
38
|
+
mode: 'id',
|
|
39
|
+
id: voiceId
|
|
40
|
+
},
|
|
41
|
+
output_format: outputFormat,
|
|
42
|
+
speed: this.defaultSettings.speed,
|
|
43
|
+
generation_config: this.defaultSettings.generation_config
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
return { url, headers, body };
|
|
49
47
|
}
|
|
50
48
|
|
|
51
49
|
async save(voiceId, text, format, filePath, fileName) {
|
|
@@ -63,7 +61,17 @@ export class CartesiaProvider {
|
|
|
63
61
|
throw new ProviderError('File path and name are required', 'cartesia');
|
|
64
62
|
}
|
|
65
63
|
|
|
66
|
-
const
|
|
64
|
+
const { url, headers, body } = this._getFetchParams(voiceId, text, format);
|
|
65
|
+
const response = await fetch(url, { method: 'POST', headers, body });
|
|
66
|
+
|
|
67
|
+
if (!response.ok) {
|
|
68
|
+
const errorMessage = (await response.text().catch(() => '')) || 'Failed to save audio from Cartesia API';
|
|
69
|
+
throw new ProviderError(errorMessage, 'cartesia', {
|
|
70
|
+
status: response.status,
|
|
71
|
+
statusText: response.statusText,
|
|
72
|
+
message: errorMessage
|
|
73
|
+
});
|
|
74
|
+
}
|
|
67
75
|
|
|
68
76
|
if (!fs.existsSync(filePath)) {
|
|
69
77
|
fs.mkdirSync(filePath, { recursive: true });
|
|
@@ -73,7 +81,7 @@ export class CartesiaProvider {
|
|
|
73
81
|
const writer = fs.createWriteStream(fullPath);
|
|
74
82
|
|
|
75
83
|
try {
|
|
76
|
-
await pipeline(response.
|
|
84
|
+
await pipeline(Readable.fromWeb(response.body), writer);
|
|
77
85
|
return fullPath;
|
|
78
86
|
} catch (err) {
|
|
79
87
|
// Clean up partial file on failure
|
|
@@ -90,40 +98,7 @@ export class CartesiaProvider {
|
|
|
90
98
|
if (error instanceof ProviderError) {
|
|
91
99
|
throw error;
|
|
92
100
|
}
|
|
93
|
-
|
|
94
|
-
if (axios.isAxiosError(error)) {
|
|
95
|
-
// Try to read the error message from the response stream
|
|
96
|
-
let errorMessage = 'Failed to save audio from Cartesia API';
|
|
97
|
-
|
|
98
|
-
if (error.response?.data) {
|
|
99
|
-
try {
|
|
100
|
-
// If data is a stream/buffer, try to read it
|
|
101
|
-
if (typeof error.response.data.read === 'function') {
|
|
102
|
-
const errorData = error.response.data.read();
|
|
103
|
-
if (errorData) {
|
|
104
|
-
errorMessage = errorData.toString('utf-8');
|
|
105
|
-
}
|
|
106
|
-
} else if (typeof error.response.data === 'string') {
|
|
107
|
-
errorMessage = error.response.data;
|
|
108
|
-
}
|
|
109
|
-
} catch (readError) {
|
|
110
|
-
// Keep default error message
|
|
111
|
-
}
|
|
112
|
-
}
|
|
113
|
-
|
|
114
|
-
const details = {
|
|
115
|
-
status: error.response?.status,
|
|
116
|
-
statusText: error.response?.statusText,
|
|
117
|
-
message: errorMessage
|
|
118
|
-
};
|
|
119
|
-
|
|
120
|
-
throw new ProviderError(
|
|
121
|
-
errorMessage,
|
|
122
|
-
'cartesia',
|
|
123
|
-
details
|
|
124
|
-
);
|
|
125
|
-
}
|
|
126
|
-
|
|
101
|
+
|
|
127
102
|
throw new ProviderError(
|
|
128
103
|
'An unexpected error occurred while saving audio',
|
|
129
104
|
'cartesia',
|
|
@@ -132,4 +107,3 @@ export class CartesiaProvider {
|
|
|
132
107
|
}
|
|
133
108
|
}
|
|
134
109
|
}
|
|
135
|
-
|
package/providers/elevenlabs.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import axios from 'axios';
|
|
2
1
|
import fs from 'fs';
|
|
3
2
|
import path from 'path';
|
|
3
|
+
import { Readable } from 'stream';
|
|
4
4
|
import { pipeline } from 'stream/promises';
|
|
5
5
|
import { ProviderError } from '../errors.js';
|
|
6
6
|
|
|
@@ -39,24 +39,22 @@ export class ElevenLabsProvider {
|
|
|
39
39
|
};
|
|
40
40
|
|
|
41
41
|
return this;
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
"xi-api-key": this.apiKey,
|
|
51
|
-
"Content-Type": "application/json",
|
|
52
|
-
},
|
|
53
|
-
data: {
|
|
54
|
-
text,
|
|
55
|
-
model_id: this.model_id,
|
|
56
|
-
voice_settings: this.voice_settings,
|
|
57
|
-
},
|
|
58
|
-
responseType: "stream",
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
_getFetchParams(voiceId, text, format) {
|
|
45
|
+
const url = `https://api.elevenlabs.io/v1/text-to-speech/${voiceId}`;
|
|
46
|
+
const headers = {
|
|
47
|
+
accept: 'audio/' + (format === 'mp3' ? 'mpeg' : format),
|
|
48
|
+
'xi-api-key': this.apiKey,
|
|
49
|
+
'Content-Type': 'application/json',
|
|
59
50
|
};
|
|
51
|
+
const body = JSON.stringify({
|
|
52
|
+
text,
|
|
53
|
+
model_id: this.model_id,
|
|
54
|
+
voice_settings: this.voice_settings,
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
return { url, headers, body };
|
|
60
58
|
}
|
|
61
59
|
|
|
62
60
|
async save(voiceId, text, format, filePath, fileName) {
|
|
@@ -74,10 +72,21 @@ export class ElevenLabsProvider {
|
|
|
74
72
|
throw new ProviderError('File path and name are required', 'elevenlabs');
|
|
75
73
|
}
|
|
76
74
|
|
|
77
|
-
const
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
75
|
+
const { url, headers, body } = this._getFetchParams(voiceId, text, format);
|
|
76
|
+
const response = await fetch(url, { method: 'POST', headers, body });
|
|
77
|
+
|
|
78
|
+
if (!response.ok) {
|
|
79
|
+
const errorBody = await response.text().catch(() => '');
|
|
80
|
+
throw new ProviderError(
|
|
81
|
+
'Failed to save audio from ElevenLabs API',
|
|
82
|
+
'elevenlabs',
|
|
83
|
+
{
|
|
84
|
+
status: response.status,
|
|
85
|
+
statusText: response.statusText,
|
|
86
|
+
data: errorBody,
|
|
87
|
+
}
|
|
88
|
+
);
|
|
89
|
+
}
|
|
81
90
|
|
|
82
91
|
if (!fs.existsSync(filePath)) {
|
|
83
92
|
fs.mkdirSync(filePath, { recursive: true });
|
|
@@ -87,7 +96,7 @@ export class ElevenLabsProvider {
|
|
|
87
96
|
const writer = fs.createWriteStream(fullPath);
|
|
88
97
|
|
|
89
98
|
try {
|
|
90
|
-
await pipeline(response.
|
|
99
|
+
await pipeline(Readable.fromWeb(response.body), writer);
|
|
91
100
|
return fullPath;
|
|
92
101
|
} catch (err) {
|
|
93
102
|
// Clean up partial file on failure
|
|
@@ -104,21 +113,7 @@ export class ElevenLabsProvider {
|
|
|
104
113
|
if (error instanceof ProviderError) {
|
|
105
114
|
throw error;
|
|
106
115
|
}
|
|
107
|
-
|
|
108
|
-
if (axios.isAxiosError(error)) {
|
|
109
|
-
const details = {
|
|
110
|
-
status: error.response?.status,
|
|
111
|
-
statusText: error.response?.statusText,
|
|
112
|
-
data: error.response?.data
|
|
113
|
-
};
|
|
114
|
-
|
|
115
|
-
throw new ProviderError(
|
|
116
|
-
'Failed to save audio from ElevenLabs API',
|
|
117
|
-
'elevenlabs',
|
|
118
|
-
details
|
|
119
|
-
);
|
|
120
|
-
}
|
|
121
|
-
|
|
116
|
+
|
|
122
117
|
throw new ProviderError(
|
|
123
118
|
'An unexpected error occurred while saving audio',
|
|
124
119
|
'elevenlabs',
|
|
@@ -126,4 +121,4 @@ export class ElevenLabsProvider {
|
|
|
126
121
|
);
|
|
127
122
|
}
|
|
128
123
|
}
|
|
129
|
-
}
|
|
124
|
+
}
|
package/providers/resemble.js
CHANGED
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
import axios from 'axios';
|
|
2
1
|
import fs from 'fs';
|
|
3
2
|
import path from 'path';
|
|
4
3
|
import { ProviderError, ValidationError } from '../errors.js';
|
|
@@ -15,17 +14,16 @@ export class ResembleProvider {
|
|
|
15
14
|
|
|
16
15
|
}
|
|
17
16
|
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
'Content-Type': 'application/json',
|
|
25
|
-
'Accept-Encoding': 'gzip, deflate, br'
|
|
26
|
-
},
|
|
27
|
-
data
|
|
17
|
+
_getFetchParams(endpoint, data) {
|
|
18
|
+
const url = `${this.baseUrl}${endpoint}`;
|
|
19
|
+
const headers = {
|
|
20
|
+
'Authorization': `Bearer ${this.apiKey}`,
|
|
21
|
+
'Content-Type': 'application/json',
|
|
22
|
+
'Accept-Encoding': 'gzip, deflate, br'
|
|
28
23
|
};
|
|
24
|
+
const body = JSON.stringify(data);
|
|
25
|
+
|
|
26
|
+
return { url, headers, body };
|
|
29
27
|
}
|
|
30
28
|
|
|
31
29
|
async save(voiceId, text, format, filePath, fileName) {
|
|
@@ -43,19 +41,42 @@ export class ResembleProvider {
|
|
|
43
41
|
throw new ProviderError('File path and name are required', 'resemble');
|
|
44
42
|
}
|
|
45
43
|
|
|
46
|
-
const
|
|
44
|
+
const { url, headers, body } = this._getFetchParams('/synthesize', {
|
|
47
45
|
voice_uuid: voiceId,
|
|
48
46
|
data: text,
|
|
49
47
|
sample_rate: this.defaultSettings.sample_rate,
|
|
50
48
|
output_format: format,
|
|
51
49
|
precision: this.defaultSettings.precision
|
|
52
|
-
})
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
const response = await fetch(url, { method: 'POST', headers, body });
|
|
53
|
+
|
|
54
|
+
if (!response.ok) {
|
|
55
|
+
let data;
|
|
56
|
+
const raw = await response.text().catch(() => '');
|
|
57
|
+
try {
|
|
58
|
+
data = raw ? JSON.parse(raw) : raw;
|
|
59
|
+
} catch {
|
|
60
|
+
data = raw;
|
|
61
|
+
}
|
|
62
|
+
throw new ProviderError(
|
|
63
|
+
'Failed to save audio from Resemble API',
|
|
64
|
+
'resemble',
|
|
65
|
+
{
|
|
66
|
+
status: response.status,
|
|
67
|
+
statusText: response.statusText,
|
|
68
|
+
data,
|
|
69
|
+
}
|
|
70
|
+
);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
const responseData = await response.json();
|
|
53
74
|
|
|
54
|
-
if (!
|
|
75
|
+
if (!responseData.success) {
|
|
55
76
|
throw new ProviderError(
|
|
56
|
-
|
|
77
|
+
responseData.issues?.join(', ') || 'Synthesis failed',
|
|
57
78
|
'resemble',
|
|
58
|
-
|
|
79
|
+
responseData
|
|
59
80
|
);
|
|
60
81
|
}
|
|
61
82
|
|
|
@@ -64,8 +85,8 @@ export class ResembleProvider {
|
|
|
64
85
|
}
|
|
65
86
|
|
|
66
87
|
const fullPath = path.join(filePath, fileName);
|
|
67
|
-
const audioBuffer = Buffer.from(
|
|
68
|
-
|
|
88
|
+
const audioBuffer = Buffer.from(responseData.audio_content, 'base64');
|
|
89
|
+
|
|
69
90
|
return new Promise((resolve, reject) => {
|
|
70
91
|
fs.writeFile(fullPath, audioBuffer, (err) => {
|
|
71
92
|
if (err) {
|
|
@@ -83,21 +104,7 @@ export class ResembleProvider {
|
|
|
83
104
|
if (error instanceof ProviderError) {
|
|
84
105
|
throw error;
|
|
85
106
|
}
|
|
86
|
-
|
|
87
|
-
if (axios.isAxiosError(error)) {
|
|
88
|
-
const details = {
|
|
89
|
-
status: error.response?.status,
|
|
90
|
-
statusText: error.response?.statusText,
|
|
91
|
-
data: error.response?.data
|
|
92
|
-
};
|
|
93
|
-
|
|
94
|
-
throw new ProviderError(
|
|
95
|
-
'Failed to save audio from Resemble API',
|
|
96
|
-
'resemble',
|
|
97
|
-
details
|
|
98
|
-
);
|
|
99
|
-
}
|
|
100
|
-
|
|
107
|
+
|
|
101
108
|
throw new ProviderError(
|
|
102
109
|
'An unexpected error occurred while saving audio',
|
|
103
110
|
'resemble',
|
|
@@ -135,4 +142,4 @@ export class ResembleProvider {
|
|
|
135
142
|
this.defaultSettings.output_format = format;
|
|
136
143
|
return this;
|
|
137
144
|
}
|
|
138
|
-
}
|
|
145
|
+
}
|