awaz 0.0.1
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/LICENSE +21 -0
- package/README.md +97 -0
- package/dist/cli.d.ts +6 -0
- package/dist/cli.js +695 -0
- package/dist/cli.js.map +1 -0
- package/dist/index.d.ts +66 -0
- package/dist/index.js +143 -0
- package/dist/index.js.map +1 -0
- package/package.json +68 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2025 Peter Steinberger
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
# awaz
|
|
2
|
+
|
|
3
|
+
Text to speech. Done right.
|
|
4
|
+
|
|
5
|
+
```bash
|
|
6
|
+
npm i -g awaz
|
|
7
|
+
awaz "Ship it"
|
|
8
|
+
```
|
|
9
|
+
|
|
10
|
+
## Setup
|
|
11
|
+
|
|
12
|
+
```bash
|
|
13
|
+
export ELEVENLABS_API_KEY="your-key"
|
|
14
|
+
```
|
|
15
|
+
|
|
16
|
+
Get one at [elevenlabs.io](https://elevenlabs.io)
|
|
17
|
+
|
|
18
|
+
## Usage
|
|
19
|
+
|
|
20
|
+
```bash
|
|
21
|
+
# Speak
|
|
22
|
+
awaz "Hello world"
|
|
23
|
+
|
|
24
|
+
# Pick a voice
|
|
25
|
+
awaz -v Roger "Hello world"
|
|
26
|
+
|
|
27
|
+
# Save to file
|
|
28
|
+
awaz -o hello.mp3 "Hello world"
|
|
29
|
+
|
|
30
|
+
# Pipe it
|
|
31
|
+
echo "Hello world" | awaz
|
|
32
|
+
|
|
33
|
+
# Go faster
|
|
34
|
+
awaz --speed 1.2 "Ship faster"
|
|
35
|
+
|
|
36
|
+
# List voices
|
|
37
|
+
awaz voices
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
## Why awaz?
|
|
41
|
+
|
|
42
|
+
Mac's `say` command but with voices that don't sound like robots from 2003.
|
|
43
|
+
|
|
44
|
+
Zero config. One command. Works.
|
|
45
|
+
|
|
46
|
+
## Options
|
|
47
|
+
|
|
48
|
+
```
|
|
49
|
+
-v, --voice <name> Voice name or ID
|
|
50
|
+
-o, --output <file> Save to file
|
|
51
|
+
-r, --rate <wpm> Words per minute
|
|
52
|
+
--speed <n> Speed multiplier (0.5-2.0)
|
|
53
|
+
--model-id <id> ElevenLabs model
|
|
54
|
+
--stability <n> Voice consistency (0-1)
|
|
55
|
+
--similarity <n> Match original voice (0-1)
|
|
56
|
+
--style <n> Expressiveness (0-1)
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
## Commands
|
|
60
|
+
|
|
61
|
+
```bash
|
|
62
|
+
awaz [text] # Speak (default)
|
|
63
|
+
awaz voices # List voices
|
|
64
|
+
awaz prompting # Tips for better output
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
## Environment
|
|
68
|
+
|
|
69
|
+
```bash
|
|
70
|
+
ELEVENLABS_API_KEY # Required
|
|
71
|
+
ELEVENLABS_VOICE_ID # Default voice (optional)
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
## For AI Agents
|
|
75
|
+
|
|
76
|
+
```bash
|
|
77
|
+
npx skills ahmadawais/awaz
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
Agents can use awaz to speak text, generate audio files, or preview how text sounds. See [skills/SKILL.md](./skills/SKILL.md) for full agent instructions.
|
|
81
|
+
|
|
82
|
+
## Dev
|
|
83
|
+
|
|
84
|
+
```bash
|
|
85
|
+
pnpm install
|
|
86
|
+
pnpm build
|
|
87
|
+
pnpm test
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
## Inspiration
|
|
91
|
+
|
|
92
|
+
- macOS `say` command — the OG.
|
|
93
|
+
- steipete's `sag` in Go.
|
|
94
|
+
|
|
95
|
+
## License
|
|
96
|
+
|
|
97
|
+
MIT © [Ahmad Awais](https://twitter.com/MrAhmadAwais)
|
package/dist/cli.d.ts
ADDED
package/dist/cli.js
ADDED
|
@@ -0,0 +1,695 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
var __defProp = Object.defineProperty;
|
|
3
|
+
var __export = (target, all) => {
|
|
4
|
+
for (var name in all)
|
|
5
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
6
|
+
};
|
|
7
|
+
|
|
8
|
+
// src/cli.ts
|
|
9
|
+
import { Command as Command4, Option } from "commander";
|
|
10
|
+
import { config } from "dotenv";
|
|
11
|
+
import { resolve } from "path";
|
|
12
|
+
|
|
13
|
+
// src/commands/speak.ts
|
|
14
|
+
import { Command } from "commander";
|
|
15
|
+
import { spawn } from "child_process";
|
|
16
|
+
import fs2 from "fs";
|
|
17
|
+
import os from "os";
|
|
18
|
+
import path2 from "path";
|
|
19
|
+
import ora from "ora";
|
|
20
|
+
|
|
21
|
+
// src/lib/elevenlabs.ts
|
|
22
|
+
var ElevenLabsClient = class {
|
|
23
|
+
baseUrl;
|
|
24
|
+
apiKey;
|
|
25
|
+
constructor(config2) {
|
|
26
|
+
this.apiKey = config2.apiKey;
|
|
27
|
+
this.baseUrl = config2.baseUrl || "https://api.elevenlabs.io";
|
|
28
|
+
}
|
|
29
|
+
async request(path3, options = {}) {
|
|
30
|
+
const url = `${this.baseUrl}${path3}`;
|
|
31
|
+
const headers = {
|
|
32
|
+
"xi-api-key": this.apiKey,
|
|
33
|
+
Accept: "application/json",
|
|
34
|
+
...options.headers
|
|
35
|
+
};
|
|
36
|
+
const response = await fetch(url, {
|
|
37
|
+
...options,
|
|
38
|
+
headers
|
|
39
|
+
});
|
|
40
|
+
if (!response.ok) {
|
|
41
|
+
const text = await response.text();
|
|
42
|
+
throw new Error(`ElevenLabs API error: ${response.status} ${response.statusText}: ${text}`);
|
|
43
|
+
}
|
|
44
|
+
return response.json();
|
|
45
|
+
}
|
|
46
|
+
/**
|
|
47
|
+
* List available voices
|
|
48
|
+
*/
|
|
49
|
+
async listVoices(search) {
|
|
50
|
+
let path3 = "/v1/voices";
|
|
51
|
+
if (search) {
|
|
52
|
+
path3 += `?search=${encodeURIComponent(search)}`;
|
|
53
|
+
}
|
|
54
|
+
const response = await this.request(path3);
|
|
55
|
+
return response.voices;
|
|
56
|
+
}
|
|
57
|
+
/**
|
|
58
|
+
* Stream TTS audio
|
|
59
|
+
*/
|
|
60
|
+
async streamTTS(voiceId, payload, latencyTier = 0) {
|
|
61
|
+
let path3 = `/v1/text-to-speech/${voiceId}/stream`;
|
|
62
|
+
if (latencyTier > 0) {
|
|
63
|
+
path3 += `?optimize_streaming_latency=${latencyTier}`;
|
|
64
|
+
}
|
|
65
|
+
const response = await fetch(`${this.baseUrl}${path3}`, {
|
|
66
|
+
method: "POST",
|
|
67
|
+
headers: {
|
|
68
|
+
"Content-Type": "application/json",
|
|
69
|
+
Accept: "audio/mpeg",
|
|
70
|
+
"xi-api-key": this.apiKey
|
|
71
|
+
},
|
|
72
|
+
body: JSON.stringify(payload)
|
|
73
|
+
});
|
|
74
|
+
if (!response.ok) {
|
|
75
|
+
const text = await response.text();
|
|
76
|
+
throw new Error(`Stream TTS failed: ${response.status}: ${text}`);
|
|
77
|
+
}
|
|
78
|
+
return response.body;
|
|
79
|
+
}
|
|
80
|
+
/**
|
|
81
|
+
* Convert text to speech (non-streaming)
|
|
82
|
+
*/
|
|
83
|
+
async convertTTS(voiceId, payload) {
|
|
84
|
+
const path3 = `/v1/text-to-speech/${voiceId}`;
|
|
85
|
+
const response = await fetch(`${this.baseUrl}${path3}`, {
|
|
86
|
+
method: "POST",
|
|
87
|
+
headers: {
|
|
88
|
+
"Content-Type": "application/json",
|
|
89
|
+
Accept: "audio/mpeg",
|
|
90
|
+
"xi-api-key": this.apiKey
|
|
91
|
+
},
|
|
92
|
+
body: JSON.stringify(payload)
|
|
93
|
+
});
|
|
94
|
+
if (!response.ok) {
|
|
95
|
+
const text = await response.text();
|
|
96
|
+
throw new Error(`Convert TTS failed: ${response.status}: ${text}`);
|
|
97
|
+
}
|
|
98
|
+
const arrayBuffer = await response.arrayBuffer();
|
|
99
|
+
return Buffer.from(arrayBuffer);
|
|
100
|
+
}
|
|
101
|
+
};
|
|
102
|
+
function getApiKey(providedKey) {
|
|
103
|
+
const key = providedKey || process.env.ELEVENLABS_API_KEY || process.env.AWAZ_API_KEY;
|
|
104
|
+
if (!key) {
|
|
105
|
+
throw new Error(
|
|
106
|
+
"Missing ElevenLabs API key. Set --api-key or ELEVENLABS_API_KEY environment variable."
|
|
107
|
+
);
|
|
108
|
+
}
|
|
109
|
+
return key;
|
|
110
|
+
}
|
|
111
|
+
function getDefaultVoiceId() {
|
|
112
|
+
return process.env.ELEVENLABS_VOICE_ID || process.env.AWAZ_VOICE_ID;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
// src/utils/banner.ts
|
|
116
|
+
import pc from "picocolors";
|
|
117
|
+
var banner = `
|
|
118
|
+
${pc.white("\u2584\u2580\u2588 \u2588\u2591\u2588\u2591\u2588 \u2584\u2580\u2588 \u2580\u2588")}
|
|
119
|
+
${pc.gray("\u2588\u2580\u2588 \u2580\u2584\u2580\u2584\u2580 \u2588\u2580\u2588 \u2588\u2584")}
|
|
120
|
+
${pc.dim("text to speech cli.")}
|
|
121
|
+
`;
|
|
122
|
+
|
|
123
|
+
// src/utils/log.ts
|
|
124
|
+
var log_exports = {};
|
|
125
|
+
__export(log_exports, {
|
|
126
|
+
bold: () => bold,
|
|
127
|
+
cyan: () => cyan,
|
|
128
|
+
dim: () => dim,
|
|
129
|
+
error: () => error,
|
|
130
|
+
green: () => green,
|
|
131
|
+
info: () => info,
|
|
132
|
+
red: () => red,
|
|
133
|
+
success: () => success,
|
|
134
|
+
warn: () => warn,
|
|
135
|
+
yellow: () => yellow
|
|
136
|
+
});
|
|
137
|
+
import pc2 from "picocolors";
|
|
138
|
+
function error(msg) {
|
|
139
|
+
console.error(`${pc2.red("\u2716")} ${msg}`);
|
|
140
|
+
}
|
|
141
|
+
function warn(msg) {
|
|
142
|
+
console.error(`${pc2.yellow("\u26A0")} ${msg}`);
|
|
143
|
+
}
|
|
144
|
+
function info(msg) {
|
|
145
|
+
console.error(`${pc2.blue("\u2139")} ${msg}`);
|
|
146
|
+
}
|
|
147
|
+
function success(msg) {
|
|
148
|
+
console.error(`${pc2.green("\u2714")} ${msg}`);
|
|
149
|
+
}
|
|
150
|
+
function dim(msg) {
|
|
151
|
+
return pc2.dim(msg);
|
|
152
|
+
}
|
|
153
|
+
function bold(msg) {
|
|
154
|
+
return pc2.bold(msg);
|
|
155
|
+
}
|
|
156
|
+
function cyan(msg) {
|
|
157
|
+
return pc2.cyan(msg);
|
|
158
|
+
}
|
|
159
|
+
function yellow(msg) {
|
|
160
|
+
return pc2.yellow(msg);
|
|
161
|
+
}
|
|
162
|
+
function green(msg) {
|
|
163
|
+
return pc2.green(msg);
|
|
164
|
+
}
|
|
165
|
+
function red(msg) {
|
|
166
|
+
return pc2.red(msg);
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
// src/utils/package.ts
|
|
170
|
+
import fs from "fs";
|
|
171
|
+
import path from "path";
|
|
172
|
+
import { fileURLToPath } from "url";
|
|
173
|
+
var __dirname2 = path.dirname(fileURLToPath(import.meta.url));
|
|
174
|
+
var _packageJson = null;
|
|
175
|
+
function getPackageJson() {
|
|
176
|
+
if (_packageJson) {
|
|
177
|
+
return _packageJson;
|
|
178
|
+
}
|
|
179
|
+
let dir = __dirname2;
|
|
180
|
+
for (let i = 0; i < 5; i++) {
|
|
181
|
+
const pkgPath = path.join(dir, "package.json");
|
|
182
|
+
if (fs.existsSync(pkgPath)) {
|
|
183
|
+
_packageJson = JSON.parse(fs.readFileSync(pkgPath, "utf-8"));
|
|
184
|
+
return _packageJson;
|
|
185
|
+
}
|
|
186
|
+
dir = path.dirname(dir);
|
|
187
|
+
}
|
|
188
|
+
return {
|
|
189
|
+
name: "awaz",
|
|
190
|
+
version: "0.0.1",
|
|
191
|
+
description: "Text to speech. Done right."
|
|
192
|
+
};
|
|
193
|
+
}
|
|
194
|
+
function getVersion() {
|
|
195
|
+
return getPackageJson().version;
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
// src/utils/helpers.ts
|
|
199
|
+
function padRight(str, len) {
|
|
200
|
+
if (str.length >= len) return str.slice(0, len);
|
|
201
|
+
return str + " ".repeat(len - str.length);
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
// src/commands/speak.ts
|
|
205
|
+
var DEFAULT_WPM = 175;
|
|
206
|
+
function createSpeakCommand() {
|
|
207
|
+
return new Command("speak").description("Speak text (streams by default)").argument("[text...]", "Text to speak").option("--voice-id <id>", "Voice ID").option("-v, --voice <name>", 'Voice name or ID (use "?" to list)').option("--model-id <id>", "ElevenLabs model (eleven_v3, eleven_multilingual_v2, etc.)", "eleven_v3").option("-o, --output <path>", "Save audio to file").option("--format <format>", "Audio format", "mp3_44100_128").option("--stream", "Stream audio while generating", true).option("--no-stream", "Disable streaming").option("--play", "Play audio through speakers", true).option("--no-play", "Disable playback").option("--latency-tier <n>", "Lower = faster (0-4)", "0").option("--speed <n>", "Speed multiplier (0.5-2.0)", "1.0").option("-r, --rate <wpm>", "Words per minute (default 175)").option("-f, --input-file <path>", 'Read from file (use "-" for stdin)').option("--stability <n>", "Voice consistency (0-1)").option("--similarity <n>", "Match to original voice (0-1)").option("--similarity-boost <n>", "Same as --similarity").option("--style <n>", "Expressiveness (0-1)").option("--speaker-boost", "Add clarity").option("--no-speaker-boost", "No clarity boost").option("--seed <n>", "For reproducible output").option("--normalize <mode>", "Handle numbers/URLs: auto|on|off").option("--lang <code>", "Language (en, de, fr, etc.)").option("--metrics", "Show timing stats", false).option("--progress", "macOS say compatibility (no-op)").option("--network-send <host>", "macOS say compatibility (no-op)").option("--audio-device <device>", "macOS say compatibility (no-op)").option("--interactive <mode>", "macOS say compatibility (no-op)").option("--file-format <fmt>", "macOS say compatibility (no-op)").option("--data-format <fmt>", "macOS say compatibility (no-op)").option("--channels <n>", "macOS say compatibility (no-op)").option("--bit-rate <n>", "macOS say compatibility (no-op)").option("--quality <n>", "macOS say compatibility (no-op)").action(async function(textArgs) {
|
|
208
|
+
const opts = this.optsWithGlobals();
|
|
209
|
+
try {
|
|
210
|
+
const speed = parseFloat(String(opts.speed));
|
|
211
|
+
const latencyTier = parseInt(String(opts.latencyTier), 10);
|
|
212
|
+
let finalSpeed = speed;
|
|
213
|
+
if (opts.rate) {
|
|
214
|
+
const rate = parseInt(String(opts.rate), 10);
|
|
215
|
+
finalSpeed = rate / DEFAULT_WPM;
|
|
216
|
+
if (finalSpeed <= 0.5 || finalSpeed >= 2) {
|
|
217
|
+
throw new Error(
|
|
218
|
+
`Rate ${rate} wpm maps to speed ${finalSpeed.toFixed(2)}, which is outside the allowed 0.5\u20132.0 range`
|
|
219
|
+
);
|
|
220
|
+
}
|
|
221
|
+
} else if (finalSpeed <= 0.5 || finalSpeed >= 2) {
|
|
222
|
+
throw new Error("Speed must be between 0.5 and 2.0 (e.g. 1.1 for 10% faster)");
|
|
223
|
+
}
|
|
224
|
+
const apiKey = getApiKey(opts.apiKey);
|
|
225
|
+
const client = new ElevenLabsClient({
|
|
226
|
+
apiKey,
|
|
227
|
+
baseUrl: opts.baseUrl
|
|
228
|
+
});
|
|
229
|
+
let voiceId = opts.voice || opts.voiceId || getDefaultVoiceId();
|
|
230
|
+
if (voiceId === "?") {
|
|
231
|
+
const voices = await client.listVoices();
|
|
232
|
+
console.log(`${padRight("VOICE ID", 24)} ${padRight("NAME", 24)} CATEGORY`);
|
|
233
|
+
for (const v of voices) {
|
|
234
|
+
console.log(
|
|
235
|
+
`${padRight(v.voice_id, 24)} ${padRight(v.name, 24)} ${v.category}`
|
|
236
|
+
);
|
|
237
|
+
}
|
|
238
|
+
return;
|
|
239
|
+
}
|
|
240
|
+
voiceId = await resolveVoice(client, voiceId);
|
|
241
|
+
const text = await resolveText(textArgs, opts.inputFile);
|
|
242
|
+
let shouldPlay = opts.play;
|
|
243
|
+
if (opts.output && !this.getOptionValueSource("play")) {
|
|
244
|
+
shouldPlay = false;
|
|
245
|
+
}
|
|
246
|
+
let outputFormat = opts.format;
|
|
247
|
+
if (opts.output) {
|
|
248
|
+
const inferred = inferFormatFromExt(opts.output);
|
|
249
|
+
if (inferred) {
|
|
250
|
+
outputFormat = inferred;
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
const payload = buildTTSRequest(this, opts, text, finalSpeed, outputFormat);
|
|
254
|
+
const spinner = ora("Generating speech...").start();
|
|
255
|
+
const start = Date.now();
|
|
256
|
+
let bytes = 0;
|
|
257
|
+
if (opts.stream) {
|
|
258
|
+
bytes = await streamAndSave(
|
|
259
|
+
client,
|
|
260
|
+
voiceId,
|
|
261
|
+
payload,
|
|
262
|
+
latencyTier,
|
|
263
|
+
opts.output,
|
|
264
|
+
shouldPlay,
|
|
265
|
+
spinner
|
|
266
|
+
);
|
|
267
|
+
} else {
|
|
268
|
+
bytes = await convertAndSave(
|
|
269
|
+
client,
|
|
270
|
+
voiceId,
|
|
271
|
+
payload,
|
|
272
|
+
opts.output,
|
|
273
|
+
shouldPlay,
|
|
274
|
+
spinner
|
|
275
|
+
);
|
|
276
|
+
}
|
|
277
|
+
spinner.succeed("Done");
|
|
278
|
+
if (opts.metrics) {
|
|
279
|
+
const duration = Date.now() - start;
|
|
280
|
+
console.error(
|
|
281
|
+
`metrics: chars=${text.length} bytes=${bytes} model=${opts.modelId} voice=${voiceId} stream=${opts.stream} latencyTier=${latencyTier} dur=${duration}ms`
|
|
282
|
+
);
|
|
283
|
+
}
|
|
284
|
+
} catch (err) {
|
|
285
|
+
log_exports.error(err.message);
|
|
286
|
+
process.exit(1);
|
|
287
|
+
}
|
|
288
|
+
});
|
|
289
|
+
}
|
|
290
|
+
async function resolveVoice(client, voiceInput) {
|
|
291
|
+
if (!voiceInput) {
|
|
292
|
+
const voices2 = await client.listVoices();
|
|
293
|
+
if (voices2.length === 0) {
|
|
294
|
+
throw new Error("No voices available; specify --voice or set ELEVENLABS_VOICE_ID");
|
|
295
|
+
}
|
|
296
|
+
log_exports.info(`Defaulting to voice ${voices2[0].name} (${voices2[0].voice_id})`);
|
|
297
|
+
return voices2[0].voice_id;
|
|
298
|
+
}
|
|
299
|
+
if (voiceInput.length >= 15 && /[0-9]/.test(voiceInput)) {
|
|
300
|
+
return voiceInput;
|
|
301
|
+
}
|
|
302
|
+
const voices = await client.listVoices(voiceInput);
|
|
303
|
+
const voiceInputLower = voiceInput.toLowerCase();
|
|
304
|
+
const exact = voices.find((v) => v.name.toLowerCase() === voiceInputLower);
|
|
305
|
+
if (exact) {
|
|
306
|
+
log_exports.info(`Using voice ${exact.name} (${exact.voice_id})`);
|
|
307
|
+
return exact.voice_id;
|
|
308
|
+
}
|
|
309
|
+
if (voices.length > 0) {
|
|
310
|
+
const v = voices[0];
|
|
311
|
+
log_exports.info(`Using closest voice match ${v.name} (${v.voice_id})`);
|
|
312
|
+
return v.voice_id;
|
|
313
|
+
}
|
|
314
|
+
throw new Error(`Voice "${voiceInput}" not found; try 'awaz voices' or -v '?'`);
|
|
315
|
+
}
|
|
316
|
+
async function resolveText(args, inputFile) {
|
|
317
|
+
if (inputFile) {
|
|
318
|
+
if (inputFile === "-") {
|
|
319
|
+
return readStdin();
|
|
320
|
+
}
|
|
321
|
+
const data = fs2.readFileSync(inputFile, "utf-8");
|
|
322
|
+
const text = data.trim();
|
|
323
|
+
if (!text) {
|
|
324
|
+
throw new Error("Input file was empty");
|
|
325
|
+
}
|
|
326
|
+
return text;
|
|
327
|
+
}
|
|
328
|
+
if (args.length > 0) {
|
|
329
|
+
return args.join(" ");
|
|
330
|
+
}
|
|
331
|
+
return readStdin();
|
|
332
|
+
}
|
|
333
|
+
function readStdin() {
|
|
334
|
+
return new Promise((resolve2, reject) => {
|
|
335
|
+
if (process.stdin.isTTY) {
|
|
336
|
+
reject(new Error("No text provided; pass text args, --input-file, or pipe input"));
|
|
337
|
+
return;
|
|
338
|
+
}
|
|
339
|
+
let data = "";
|
|
340
|
+
process.stdin.setEncoding("utf-8");
|
|
341
|
+
process.stdin.on("data", (chunk) => {
|
|
342
|
+
data += chunk;
|
|
343
|
+
});
|
|
344
|
+
process.stdin.on("end", () => {
|
|
345
|
+
const text = data.trim();
|
|
346
|
+
if (!text) {
|
|
347
|
+
reject(new Error("stdin was empty"));
|
|
348
|
+
return;
|
|
349
|
+
}
|
|
350
|
+
resolve2(text);
|
|
351
|
+
});
|
|
352
|
+
process.stdin.on("error", reject);
|
|
353
|
+
});
|
|
354
|
+
}
|
|
355
|
+
function buildTTSRequest(_cmd, opts, text, speed, outputFormat) {
|
|
356
|
+
const voiceSettings = {
|
|
357
|
+
speed
|
|
358
|
+
};
|
|
359
|
+
if (opts.stability !== void 0) {
|
|
360
|
+
const stability = parseFloat(String(opts.stability));
|
|
361
|
+
if (stability < 0 || stability > 1) {
|
|
362
|
+
throw new Error("Stability must be between 0 and 1");
|
|
363
|
+
}
|
|
364
|
+
if (opts.modelId === "eleven_v3") {
|
|
365
|
+
if (![0, 0.5, 1].some((v) => Math.abs(stability - v) < 1e-9)) {
|
|
366
|
+
throw new Error(
|
|
367
|
+
"For eleven_v3, stability must be one of 0.0, 0.5, 1.0 (Creative/Natural/Robust)"
|
|
368
|
+
);
|
|
369
|
+
}
|
|
370
|
+
}
|
|
371
|
+
voiceSettings.stability = stability;
|
|
372
|
+
}
|
|
373
|
+
const similarity = opts.similarity ?? opts.similarityBoost;
|
|
374
|
+
if (similarity !== void 0) {
|
|
375
|
+
const simVal = parseFloat(String(similarity));
|
|
376
|
+
if (simVal < 0 || simVal > 1) {
|
|
377
|
+
throw new Error("Similarity must be between 0 and 1");
|
|
378
|
+
}
|
|
379
|
+
voiceSettings.similarity_boost = simVal;
|
|
380
|
+
}
|
|
381
|
+
if (opts.style !== void 0) {
|
|
382
|
+
const styleVal = parseFloat(String(opts.style));
|
|
383
|
+
if (styleVal < 0 || styleVal > 1) {
|
|
384
|
+
throw new Error("Style must be between 0 and 1");
|
|
385
|
+
}
|
|
386
|
+
voiceSettings.style = styleVal;
|
|
387
|
+
}
|
|
388
|
+
if (opts.speakerBoost && opts.noSpeakerBoost) {
|
|
389
|
+
throw new Error("Choose only one of --speaker-boost or --no-speaker-boost");
|
|
390
|
+
}
|
|
391
|
+
if (opts.speakerBoost) {
|
|
392
|
+
voiceSettings.use_speaker_boost = true;
|
|
393
|
+
} else if (opts.noSpeakerBoost) {
|
|
394
|
+
voiceSettings.use_speaker_boost = false;
|
|
395
|
+
}
|
|
396
|
+
const request = {
|
|
397
|
+
text,
|
|
398
|
+
model_id: opts.modelId,
|
|
399
|
+
output_format: outputFormat,
|
|
400
|
+
voice_settings: voiceSettings
|
|
401
|
+
};
|
|
402
|
+
if (opts.seed !== void 0) {
|
|
403
|
+
const seedVal = parseInt(String(opts.seed), 10);
|
|
404
|
+
if (seedVal < 0 || seedVal > 4294967295) {
|
|
405
|
+
throw new Error("Seed must be between 0 and 4294967295");
|
|
406
|
+
}
|
|
407
|
+
request.seed = seedVal;
|
|
408
|
+
}
|
|
409
|
+
if (opts.normalize) {
|
|
410
|
+
const norm = opts.normalize.toLowerCase().trim();
|
|
411
|
+
if (!["auto", "on", "off"].includes(norm)) {
|
|
412
|
+
throw new Error("Normalize must be one of: auto, on, off");
|
|
413
|
+
}
|
|
414
|
+
request.apply_text_normalization = norm;
|
|
415
|
+
}
|
|
416
|
+
if (opts.lang) {
|
|
417
|
+
const lang = opts.lang.toLowerCase().trim();
|
|
418
|
+
if (lang.length !== 2 || !/^[a-z]+$/.test(lang)) {
|
|
419
|
+
throw new Error("Lang must be a 2-letter ISO 639-1 code (e.g. en, de, fr)");
|
|
420
|
+
}
|
|
421
|
+
request.language_code = lang;
|
|
422
|
+
}
|
|
423
|
+
return request;
|
|
424
|
+
}
|
|
425
|
+
async function streamAndSave(client, voiceId, payload, latencyTier, outputPath, shouldPlay, spinner) {
|
|
426
|
+
const stream = await client.streamTTS(voiceId, payload, latencyTier);
|
|
427
|
+
if (!stream) {
|
|
428
|
+
throw new Error("No stream returned from API");
|
|
429
|
+
}
|
|
430
|
+
const chunks = [];
|
|
431
|
+
const reader = stream.getReader();
|
|
432
|
+
let totalBytes = 0;
|
|
433
|
+
while (true) {
|
|
434
|
+
const { done, value } = await reader.read();
|
|
435
|
+
if (done) break;
|
|
436
|
+
if (value) {
|
|
437
|
+
chunks.push(value);
|
|
438
|
+
totalBytes += value.length;
|
|
439
|
+
spinner.text = `Generating speech... ${Math.round(totalBytes / 1024)}KB`;
|
|
440
|
+
}
|
|
441
|
+
}
|
|
442
|
+
const audioData = Buffer.concat(chunks);
|
|
443
|
+
if (outputPath) {
|
|
444
|
+
const dir = path2.dirname(outputPath);
|
|
445
|
+
if (dir && dir !== ".") {
|
|
446
|
+
fs2.mkdirSync(dir, { recursive: true });
|
|
447
|
+
}
|
|
448
|
+
fs2.writeFileSync(outputPath, audioData);
|
|
449
|
+
log_exports.info(`Audio saved to ${outputPath}`);
|
|
450
|
+
}
|
|
451
|
+
if (shouldPlay) {
|
|
452
|
+
await playAudio(audioData, spinner);
|
|
453
|
+
}
|
|
454
|
+
return totalBytes;
|
|
455
|
+
}
|
|
456
|
+
async function playAudio(audioData, spinner) {
|
|
457
|
+
const platform = os.platform();
|
|
458
|
+
const tmpFile = path2.join(os.tmpdir(), `awaz-${Date.now()}.mp3`);
|
|
459
|
+
fs2.writeFileSync(tmpFile, audioData);
|
|
460
|
+
return new Promise((resolve2, reject) => {
|
|
461
|
+
let player;
|
|
462
|
+
if (platform === "darwin") {
|
|
463
|
+
player = spawn("afplay", [tmpFile]);
|
|
464
|
+
} else if (platform === "linux") {
|
|
465
|
+
player = spawn("aplay", [tmpFile]);
|
|
466
|
+
} else if (platform === "win32") {
|
|
467
|
+
player = spawn("powershell", ["-c", `(New-Object Media.SoundPlayer '${tmpFile}').PlaySync()`]);
|
|
468
|
+
} else {
|
|
469
|
+
fs2.unlinkSync(tmpFile);
|
|
470
|
+
reject(new Error(`Unsupported platform: ${platform}`));
|
|
471
|
+
return;
|
|
472
|
+
}
|
|
473
|
+
spinner.text = "Playing audio...";
|
|
474
|
+
player.on("close", (code) => {
|
|
475
|
+
fs2.unlinkSync(tmpFile);
|
|
476
|
+
if (code === 0) {
|
|
477
|
+
resolve2();
|
|
478
|
+
} else {
|
|
479
|
+
reject(new Error(`Audio player exited with code ${code}`));
|
|
480
|
+
}
|
|
481
|
+
});
|
|
482
|
+
player.on("error", (err) => {
|
|
483
|
+
fs2.unlinkSync(tmpFile);
|
|
484
|
+
reject(err);
|
|
485
|
+
});
|
|
486
|
+
});
|
|
487
|
+
}
|
|
488
|
+
async function convertAndSave(client, voiceId, payload, outputPath, shouldPlay, spinner) {
|
|
489
|
+
spinner.text = "Converting text to speech...";
|
|
490
|
+
const audioData = await client.convertTTS(voiceId, payload);
|
|
491
|
+
if (outputPath) {
|
|
492
|
+
const dir = path2.dirname(outputPath);
|
|
493
|
+
if (dir && dir !== ".") {
|
|
494
|
+
fs2.mkdirSync(dir, { recursive: true });
|
|
495
|
+
}
|
|
496
|
+
fs2.writeFileSync(outputPath, audioData);
|
|
497
|
+
log_exports.info(`Audio saved to ${outputPath}`);
|
|
498
|
+
}
|
|
499
|
+
if (shouldPlay) {
|
|
500
|
+
await playAudio(audioData, spinner);
|
|
501
|
+
}
|
|
502
|
+
return audioData.length;
|
|
503
|
+
}
|
|
504
|
+
function inferFormatFromExt(filePath) {
|
|
505
|
+
const ext = path2.extname(filePath).toLowerCase();
|
|
506
|
+
switch (ext) {
|
|
507
|
+
case ".mp3":
|
|
508
|
+
return "mp3_44100_128";
|
|
509
|
+
case ".wav":
|
|
510
|
+
case ".wave":
|
|
511
|
+
return "pcm_44100";
|
|
512
|
+
default:
|
|
513
|
+
return null;
|
|
514
|
+
}
|
|
515
|
+
}
|
|
516
|
+
|
|
517
|
+
// src/commands/voices.ts
|
|
518
|
+
import { Command as Command2 } from "commander";
|
|
519
|
+
import ora2 from "ora";
|
|
520
|
+
function createVoicesCommand() {
|
|
521
|
+
return new Command2("voices").description("List voices").option("--search <query>", "Filter by name").option("--limit <n>", "Max results (0 = all)", "100").action(async function() {
|
|
522
|
+
const opts = this.optsWithGlobals();
|
|
523
|
+
const limit = parseInt(String(opts.limit), 10);
|
|
524
|
+
const spinner = ora2("Fetching voices...").start();
|
|
525
|
+
try {
|
|
526
|
+
const apiKey = getApiKey(opts.apiKey);
|
|
527
|
+
const client = new ElevenLabsClient({
|
|
528
|
+
apiKey,
|
|
529
|
+
baseUrl: opts.baseUrl
|
|
530
|
+
});
|
|
531
|
+
let voices = await client.listVoices(opts.search);
|
|
532
|
+
spinner.stop();
|
|
533
|
+
if (limit > 0 && voices.length > limit) {
|
|
534
|
+
voices = voices.slice(0, limit);
|
|
535
|
+
}
|
|
536
|
+
console.log(
|
|
537
|
+
`${padRight("VOICE ID", 24)} ${padRight("NAME", 24)} CATEGORY`
|
|
538
|
+
);
|
|
539
|
+
for (const v of voices) {
|
|
540
|
+
console.log(
|
|
541
|
+
`${padRight(v.voice_id, 24)} ${padRight(v.name, 24)} ${v.category}`
|
|
542
|
+
);
|
|
543
|
+
}
|
|
544
|
+
if (voices.length === 0) {
|
|
545
|
+
log_exports.info("No voices found");
|
|
546
|
+
}
|
|
547
|
+
} catch (err) {
|
|
548
|
+
spinner.fail("Failed to fetch voices");
|
|
549
|
+
log_exports.error(err.message);
|
|
550
|
+
process.exit(1);
|
|
551
|
+
}
|
|
552
|
+
});
|
|
553
|
+
}
|
|
554
|
+
|
|
555
|
+
// src/commands/prompting.ts
|
|
556
|
+
import { Command as Command3 } from "commander";
|
|
557
|
+
|
|
558
|
+
// src/commands/prompting-guide.ts
|
|
559
|
+
var promptingGuide = `# Better results from awaz
|
|
560
|
+
|
|
561
|
+
What you write matters. Here's what works.
|
|
562
|
+
|
|
563
|
+
## Models
|
|
564
|
+
|
|
565
|
+
**v3 (default)** \u2014 Most expressive. Tags like [whispers], [laughs]. No SSML.
|
|
566
|
+
**v2** \u2014 Reliable. SSML pauses work. Predictable.
|
|
567
|
+
**Flash** \u2014 Fast. ~75ms. Half the cost.
|
|
568
|
+
**Turbo** \u2014 Balanced. ~250ms. Good quality.
|
|
569
|
+
|
|
570
|
+
## Writing tips
|
|
571
|
+
|
|
572
|
+
Write how you'd say it. Short sentences. Natural breaks.
|
|
573
|
+
|
|
574
|
+
**Punctuation = pacing:**
|
|
575
|
+
- Comma, dash \u2192 slight pause
|
|
576
|
+
- Ellipsis... \u2192 dramatic pause
|
|
577
|
+
- Period \u2192 full stop
|
|
578
|
+
- ! \u2192 energy
|
|
579
|
+
|
|
580
|
+
**Spell it how it sounds.** "API" wrong? Try "A P I" or "ay-pee-eye".
|
|
581
|
+
|
|
582
|
+
## v3 tags
|
|
583
|
+
|
|
584
|
+
**Emotions:** [whispers], [shouts], [laughs], [sighs], [sarcastic], [excited]
|
|
585
|
+
**Actions:** [clears throat], [exhales], [swallows]
|
|
586
|
+
**Effects:** [applause], [short pause], [long pause]
|
|
587
|
+
**Experimental:** [strong French accent], [sings]
|
|
588
|
+
|
|
589
|
+
Not every voice responds to every tag. Experiment.
|
|
590
|
+
|
|
591
|
+
## The knobs
|
|
592
|
+
|
|
593
|
+
**--stability** (0-1)
|
|
594
|
+
Higher = consistent. Lower = varied. v3 uses presets: 0=Creative, 0.5=Natural, 1=Robust.
|
|
595
|
+
|
|
596
|
+
**--similarity** (0-1)
|
|
597
|
+
Higher = closer to original voice sample.
|
|
598
|
+
|
|
599
|
+
**--style** (0-1)
|
|
600
|
+
Higher = more expressive. Can get weird if too high.
|
|
601
|
+
|
|
602
|
+
**--speaker-boost**
|
|
603
|
+
Adds clarity. Sometimes helps.
|
|
604
|
+
|
|
605
|
+
**--seed**
|
|
606
|
+
Same seed = same output. Good for A/B testing.
|
|
607
|
+
|
|
608
|
+
## Examples
|
|
609
|
+
|
|
610
|
+
Natural:
|
|
611
|
+
awaz -v Roger --stability 0.5 "We shipped today. It worked."
|
|
612
|
+
|
|
613
|
+
Fast:
|
|
614
|
+
awaz --model-id eleven_flash_v2_5 "Quick and cheap."
|
|
615
|
+
|
|
616
|
+
Dramatic (v3):
|
|
617
|
+
awaz "[whispers] Don't move. [short pause] Something's there..."
|
|
618
|
+
|
|
619
|
+
## TL;DR
|
|
620
|
+
|
|
621
|
+
1. v3 for expression, v2 for reliability, Flash for speed
|
|
622
|
+
2. Write naturally
|
|
623
|
+
3. Experiment with tags on v3
|
|
624
|
+
4. Tweak stability/similarity if it sounds off
|
|
625
|
+
`;
|
|
626
|
+
|
|
627
|
+
// src/commands/prompting.ts
|
|
628
|
+
function createPromptingCommand() {
|
|
629
|
+
return new Command3("prompting").aliases(["prompt", "guide", "tips"]).description("Tips for better output").action(() => {
|
|
630
|
+
console.log(promptingGuide.trim());
|
|
631
|
+
});
|
|
632
|
+
}
|
|
633
|
+
|
|
634
|
+
// src/cli.ts
|
|
635
|
+
var program = new Command4();
|
|
636
|
+
var version = getVersion();
|
|
637
|
+
program.name("awaz").description("Text to speech. Done right.").version(version, "-v, --version", "Print version and exit").helpOption("-h, --help", "Display help for command").addOption(
|
|
638
|
+
new Option("--api-key <key>", "ElevenLabs API key (or ELEVENLABS_API_KEY)")
|
|
639
|
+
).addOption(
|
|
640
|
+
new Option("--base-url <url>", "Override ElevenLabs API base URL").default("https://api.elevenlabs.io")
|
|
641
|
+
).addOption(new Option("--local").hideHelp()).addOption(new Option("--env <path>", "Load env file from path"));
|
|
642
|
+
program.addCommand(createSpeakCommand());
|
|
643
|
+
program.addCommand(createVoicesCommand());
|
|
644
|
+
program.addCommand(createPromptingCommand());
|
|
645
|
+
function maybeDefaultToSpeak() {
|
|
646
|
+
if (process.argv.length <= 2) {
|
|
647
|
+
return;
|
|
648
|
+
}
|
|
649
|
+
if (process.argv[2] === "--") {
|
|
650
|
+
process.argv = [process.argv[0], process.argv[1], ...process.argv.slice(3)];
|
|
651
|
+
if (process.argv.length <= 2) {
|
|
652
|
+
return;
|
|
653
|
+
}
|
|
654
|
+
}
|
|
655
|
+
const first = process.argv[2];
|
|
656
|
+
const knownCommands = ["speak", "voices", "prompting", "prompt", "guide", "tips"];
|
|
657
|
+
const isKnown = knownCommands.includes(first.toLowerCase()) || first === "-h" || first === "--help" || first === "-v" || first === "--version";
|
|
658
|
+
if (!isKnown) {
|
|
659
|
+
process.argv = [process.argv[0], process.argv[1], "speak", ...process.argv.slice(2)];
|
|
660
|
+
}
|
|
661
|
+
}
|
|
662
|
+
program.configureOutput({
|
|
663
|
+
writeOut: (str) => process.stdout.write(str),
|
|
664
|
+
writeErr: (str) => process.stderr.write(str)
|
|
665
|
+
});
|
|
666
|
+
var originalHelp = program.helpInformation.bind(program);
|
|
667
|
+
program.helpInformation = function() {
|
|
668
|
+
return banner + "\n" + originalHelp();
|
|
669
|
+
};
|
|
670
|
+
async function run() {
|
|
671
|
+
if (process.argv.includes("-v") || process.argv.includes("--version")) {
|
|
672
|
+
console.log(version);
|
|
673
|
+
process.exit(0);
|
|
674
|
+
}
|
|
675
|
+
const envIndex = process.argv.findIndex((arg) => arg === "--env");
|
|
676
|
+
const hasEnvFlag = envIndex !== -1 && process.argv[envIndex + 1];
|
|
677
|
+
const hasEnvKey = process.env.ELEVENLABS_API_KEY || process.env.AWAZ_API_KEY;
|
|
678
|
+
if (hasEnvFlag) {
|
|
679
|
+
config({ path: resolve(process.argv[envIndex + 1]), quiet: true });
|
|
680
|
+
}
|
|
681
|
+
if (!hasEnvFlag && !hasEnvKey) {
|
|
682
|
+
config({ quiet: true });
|
|
683
|
+
}
|
|
684
|
+
maybeDefaultToSpeak();
|
|
685
|
+
await program.parseAsync(process.argv);
|
|
686
|
+
}
|
|
687
|
+
run().catch((err) => {
|
|
688
|
+
console.error(err);
|
|
689
|
+
process.exit(1);
|
|
690
|
+
});
|
|
691
|
+
export {
|
|
692
|
+
program,
|
|
693
|
+
run
|
|
694
|
+
};
|
|
695
|
+
//# sourceMappingURL=cli.js.map
|
package/dist/cli.js.map
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/cli.ts","../src/commands/speak.ts","../src/lib/elevenlabs.ts","../src/utils/banner.ts","../src/utils/log.ts","../src/utils/package.ts","../src/utils/helpers.ts","../src/commands/voices.ts","../src/commands/prompting.ts","../src/commands/prompting-guide.ts"],"sourcesContent":["import { Command, Option } from 'commander';\nimport { config } from 'dotenv';\nimport { resolve } from 'node:path';\nimport { createPromptingCommand, createSpeakCommand, createVoicesCommand } from './commands/index.js';\nimport { banner, getVersion } from './utils/index.js';\n\nconst program = new Command();\n\n// Get version from package.json\nconst version = getVersion();\n\nprogram\n\t.name('awaz')\n\t.description('Text to speech. Done right.')\n\t.version(version, '-v, --version', 'Print version and exit')\n\t.helpOption('-h, --help', 'Display help for command')\n\t.addOption(\n\t\tnew Option('--api-key <key>', 'ElevenLabs API key (or ELEVENLABS_API_KEY)')\n\t)\n\t.addOption(\n\t\tnew Option('--base-url <url>', 'Override ElevenLabs API base URL')\n\t\t\t.default('https://api.elevenlabs.io')\n\t)\n\t.addOption(new Option('--local').hideHelp())\n\t.addOption(new Option('--env <path>', 'Load env file from path'));\n\n// Add commands\nprogram.addCommand(createSpeakCommand());\nprogram.addCommand(createVoicesCommand());\nprogram.addCommand(createPromptingCommand());\n\n// Check if called like `awaz \"Hello\"` (default to speak subcommand)\nfunction maybeDefaultToSpeak(): void {\n\tif (process.argv.length <= 2) {\n\t\treturn;\n\t}\n\n\t// npm/pnpm pass-through typically prefixes args with \"--\"; drop it\n\tif (process.argv[2] === '--') {\n\t\tprocess.argv = [process.argv[0], process.argv[1], ...process.argv.slice(3)];\n\t\tif (process.argv.length <= 2) {\n\t\t\treturn;\n\t\t}\n\t}\n\n\tconst first = process.argv[2];\n\n\t// Check for known subcommands\n\tconst knownCommands = ['speak', 'voices', 'prompting', 'prompt', 'guide', 'tips'];\n\tconst isKnown =\n\t\tknownCommands.includes(first.toLowerCase()) ||\n\t\tfirst === '-h' ||\n\t\tfirst === '--help' ||\n\t\tfirst === '-v' ||\n\t\tfirst === '--version';\n\n\tif (!isKnown) {\n\t\t// Insert 'speak' subcommand\n\t\tprocess.argv = [process.argv[0], process.argv[1], 'speak', ...process.argv.slice(2)];\n\t}\n}\n\n// Override version output to be clean (no banner)\nprogram.configureOutput({\n\twriteOut: (str) => process.stdout.write(str),\n\twriteErr: (str) => process.stderr.write(str)\n});\n\n// Custom help to show banner\nconst originalHelp = program.helpInformation.bind(program);\nprogram.helpInformation = function (): string {\n\treturn banner + '\\n' + originalHelp();\n};\n\nexport async function run(): Promise<void> {\n\t// Check for version flag early\n\tif (process.argv.includes('-v') || process.argv.includes('--version')) {\n\t\tconsole.log(version);\n\t\tprocess.exit(0);\n\t}\n\n\t// Load .env file: explicit path > local .env > env var\n\tconst envIndex = process.argv.findIndex(arg => arg === '--env');\n\tconst hasEnvFlag = envIndex !== -1 && process.argv[envIndex + 1];\n\tconst hasEnvKey = process.env.ELEVENLABS_API_KEY || process.env.AWAZ_API_KEY;\n\n\tif (hasEnvFlag) {\n\t\tconfig({ path: resolve(process.argv[envIndex + 1]), quiet: true });\n\t}\n\n\tif (!hasEnvFlag && !hasEnvKey) {\n\t\tconfig({ quiet: true });\n\t}\n\n\tmaybeDefaultToSpeak();\n\tawait program.parseAsync(process.argv);\n}\n\nexport { program };\n\n// Run the CLI\nrun().catch((err) => {\n\tconsole.error(err);\n\tprocess.exit(1);\n});\n","import { Command } from 'commander';\nimport { spawn } from 'node:child_process';\nimport fs from 'node:fs';\nimport os from 'node:os';\nimport path from 'node:path';\nimport ora from 'ora';\nimport {\n\tElevenLabsClient,\n\tgetApiKey,\n\tgetDefaultVoiceId,\n\ttype TTSRequest,\n\ttype Voice,\n\ttype VoiceSettings\n} from '../lib/elevenlabs.js';\nimport { log, padRight } from '../utils/index.js';\n\ninterface SpeakOptions {\n\tvoice?: string;\n\tvoiceId?: string;\n\tmodelId: string;\n\toutput?: string;\n\tformat: string;\n\tstream: boolean;\n\tplay: boolean;\n\tlatencyTier: number;\n\tspeed: number;\n\trate?: number;\n\tinputFile?: string;\n\tstability?: number;\n\tsimilarity?: number;\n\tsimilarityBoost?: number;\n\tstyle?: number;\n\tspeakerBoost?: boolean;\n\tnoSpeakerBoost?: boolean;\n\tseed?: number;\n\tnormalize?: string;\n\tlang?: string;\n\tmetrics: boolean;\n\tapiKey?: string;\n\tbaseUrl: string;\n}\n\nconst DEFAULT_WPM = 175;\n\nexport function createSpeakCommand(): Command {\n\treturn new Command('speak')\n\t\t.description('Speak text (streams by default)')\n\t\t.argument('[text...]', 'Text to speak')\n\t\t.option('--voice-id <id>', 'Voice ID')\n\t\t.option('-v, --voice <name>', 'Voice name or ID (use \"?\" to list)')\n\t\t.option('--model-id <id>', 'ElevenLabs model (eleven_v3, eleven_multilingual_v2, etc.)', 'eleven_v3')\n\t\t.option('-o, --output <path>', 'Save audio to file')\n\t\t.option('--format <format>', 'Audio format', 'mp3_44100_128')\n\t\t.option('--stream', 'Stream audio while generating', true)\n\t\t.option('--no-stream', 'Disable streaming')\n\t\t.option('--play', 'Play audio through speakers', true)\n\t\t.option('--no-play', 'Disable playback')\n\t\t.option('--latency-tier <n>', 'Lower = faster (0-4)', '0')\n\t\t.option('--speed <n>', 'Speed multiplier (0.5-2.0)', '1.0')\n\t\t.option('-r, --rate <wpm>', 'Words per minute (default 175)')\n\t\t.option('-f, --input-file <path>', 'Read from file (use \"-\" for stdin)')\n\t\t.option('--stability <n>', 'Voice consistency (0-1)')\n\t\t.option('--similarity <n>', 'Match to original voice (0-1)')\n\t\t.option('--similarity-boost <n>', 'Same as --similarity')\n\t\t.option('--style <n>', 'Expressiveness (0-1)')\n\t\t.option('--speaker-boost', 'Add clarity')\n\t\t.option('--no-speaker-boost', 'No clarity boost')\n\t\t.option('--seed <n>', 'For reproducible output')\n\t\t.option('--normalize <mode>', 'Handle numbers/URLs: auto|on|off')\n\t\t.option('--lang <code>', 'Language (en, de, fr, etc.)')\n\t\t.option('--metrics', 'Show timing stats', false)\n\t\t// macOS say compatibility flags (no-op)\n\t\t.option('--progress', 'macOS say compatibility (no-op)')\n\t\t.option('--network-send <host>', 'macOS say compatibility (no-op)')\n\t\t.option('--audio-device <device>', 'macOS say compatibility (no-op)')\n\t\t.option('--interactive <mode>', 'macOS say compatibility (no-op)')\n\t\t.option('--file-format <fmt>', 'macOS say compatibility (no-op)')\n\t\t.option('--data-format <fmt>', 'macOS say compatibility (no-op)')\n\t\t.option('--channels <n>', 'macOS say compatibility (no-op)')\n\t\t.option('--bit-rate <n>', 'macOS say compatibility (no-op)')\n\t\t.option('--quality <n>', 'macOS say compatibility (no-op)')\n\t\t.action(async function (this: Command, textArgs: string[]) {\n\t\t\tconst opts = this.optsWithGlobals<SpeakOptions>();\n\n\t\t\ttry {\n\t\t\t\t// Parse numeric options\n\t\t\t\tconst speed = parseFloat(String(opts.speed));\n\t\t\t\tconst latencyTier = parseInt(String(opts.latencyTier), 10);\n\n\t\t\t\t// Validate speed\n\t\t\t\tlet finalSpeed = speed;\n\t\t\t\tif (opts.rate) {\n\t\t\t\t\tconst rate = parseInt(String(opts.rate), 10);\n\t\t\t\t\tfinalSpeed = rate / DEFAULT_WPM;\n\t\t\t\t\tif (finalSpeed <= 0.5 || finalSpeed >= 2.0) {\n\t\t\t\t\t\tthrow new Error(\n\t\t\t\t\t\t\t`Rate ${rate} wpm maps to speed ${finalSpeed.toFixed(2)}, which is outside the allowed 0.5–2.0 range`\n\t\t\t\t\t\t);\n\t\t\t\t\t}\n\t\t\t\t} else if (finalSpeed <= 0.5 || finalSpeed >= 2.0) {\n\t\t\t\t\tthrow new Error('Speed must be between 0.5 and 2.0 (e.g. 1.1 for 10% faster)');\n\t\t\t\t}\n\n\t\t\t\t// Get API key and create client\n\t\t\t\tconst apiKey = getApiKey(opts.apiKey);\n\t\t\t\tconst client = new ElevenLabsClient({\n\t\t\t\t\tapiKey,\n\t\t\t\t\tbaseUrl: opts.baseUrl\n\t\t\t\t});\n\n\t\t\t\t// Resolve voice\n\t\t\t\tlet voiceId = opts.voice || opts.voiceId || getDefaultVoiceId();\n\n\t\t\t\tif (voiceId === '?') {\n\t\t\t\t\t// List voices and exit\n\t\t\t\t\tconst voices = await client.listVoices();\n\t\t\t\t\tconsole.log(`${padRight('VOICE ID', 24)} ${padRight('NAME', 24)} CATEGORY`);\n\t\t\t\t\tfor (const v of voices) {\n\t\t\t\t\t\tconsole.log(\n\t\t\t\t\t\t\t`${padRight(v.voice_id, 24)} ${padRight(v.name, 24)} ${v.category}`\n\t\t\t\t\t\t);\n\t\t\t\t\t}\n\t\t\t\t\treturn;\n\t\t\t\t}\n\n\t\t\t\tvoiceId = await resolveVoice(client, voiceId);\n\n\t\t\t\t// Resolve text\n\t\t\t\tconst text = await resolveText(textArgs, opts.inputFile);\n\n\t\t\t\t// Determine if we should play (disable if output is set and play wasn't explicitly set)\n\t\t\t\tlet shouldPlay = opts.play;\n\t\t\t\tif (opts.output && !this.getOptionValueSource('play')) {\n\t\t\t\t\tshouldPlay = false;\n\t\t\t\t}\n\n\t\t\t\t// Infer format from output extension\n\t\t\t\tlet outputFormat = opts.format;\n\t\t\t\tif (opts.output) {\n\t\t\t\t\tconst inferred = inferFormatFromExt(opts.output);\n\t\t\t\t\tif (inferred) {\n\t\t\t\t\t\toutputFormat = inferred;\n\t\t\t\t\t}\n\t\t\t\t}\n\n\t\t\t\t// Build TTS request\n\t\t\t\tconst payload = buildTTSRequest(this, opts, text, finalSpeed, outputFormat);\n\n\t\t\t\tconst spinner = ora('Generating speech...').start();\n\t\t\t\tconst start = Date.now();\n\t\t\t\tlet bytes = 0;\n\n\t\t\t\tif (opts.stream) {\n\t\t\t\t\tbytes = await streamAndSave(\n\t\t\t\t\t\tclient,\n\t\t\t\t\t\tvoiceId,\n\t\t\t\t\t\tpayload,\n\t\t\t\t\t\tlatencyTier,\n\t\t\t\t\t\topts.output,\n\t\t\t\t\t\tshouldPlay,\n\t\t\t\t\t\tspinner\n\t\t\t\t\t);\n\t\t\t\t} else {\n\t\t\t\t\tbytes = await convertAndSave(\n\t\t\t\t\t\tclient,\n\t\t\t\t\t\tvoiceId,\n\t\t\t\t\t\tpayload,\n\t\t\t\t\t\topts.output,\n\t\t\t\t\t\tshouldPlay,\n\t\t\t\t\t\tspinner\n\t\t\t\t\t);\n\t\t\t\t}\n\n\t\t\t\tspinner.succeed('Done');\n\n\t\t\t\tif (opts.metrics) {\n\t\t\t\t\tconst duration = Date.now() - start;\n\t\t\t\t\tconsole.error(\n\t\t\t\t\t\t`metrics: chars=${text.length} bytes=${bytes} model=${opts.modelId} voice=${voiceId} stream=${opts.stream} latencyTier=${latencyTier} dur=${duration}ms`\n\t\t\t\t\t);\n\t\t\t\t}\n\t\t\t} catch (err) {\n\t\t\t\tlog.error((err as Error).message);\n\t\t\t\tprocess.exit(1);\n\t\t\t}\n\t\t});\n}\n\nasync function resolveVoice(\n\tclient: ElevenLabsClient,\n\tvoiceInput?: string\n): Promise<string> {\n\tif (!voiceInput) {\n\t\t// Get first available voice\n\t\tconst voices = await client.listVoices();\n\t\tif (voices.length === 0) {\n\t\t\tthrow new Error('No voices available; specify --voice or set ELEVENLABS_VOICE_ID');\n\t\t}\n\t\tlog.info(`Defaulting to voice ${voices[0].name} (${voices[0].voice_id})`);\n\t\treturn voices[0].voice_id;\n\t}\n\n\t// If it looks like an ID (UUID-like), use directly\n\tif (voiceInput.length >= 15 && /[0-9]/.test(voiceInput)) {\n\t\treturn voiceInput;\n\t}\n\n\t// Search for voice by name\n\tconst voices = await client.listVoices(voiceInput);\n\tconst voiceInputLower = voiceInput.toLowerCase();\n\n\t// Exact match\n\tconst exact = voices.find((v: Voice) => v.name.toLowerCase() === voiceInputLower);\n\tif (exact) {\n\t\tlog.info(`Using voice ${exact.name} (${exact.voice_id})`);\n\t\treturn exact.voice_id;\n\t}\n\n\t// Closest match\n\tif (voices.length > 0) {\n\t\tconst v = voices[0];\n\t\tlog.info(`Using closest voice match ${v.name} (${v.voice_id})`);\n\t\treturn v.voice_id;\n\t}\n\n\tthrow new Error(`Voice \"${voiceInput}\" not found; try 'awaz voices' or -v '?'`);\n}\n\nasync function resolveText(args: string[], inputFile?: string): Promise<string> {\n\tif (inputFile) {\n\t\tif (inputFile === '-') {\n\t\t\treturn readStdin();\n\t\t}\n\t\tconst data = fs.readFileSync(inputFile, 'utf-8');\n\t\tconst text = data.trim();\n\t\tif (!text) {\n\t\t\tthrow new Error('Input file was empty');\n\t\t}\n\t\treturn text;\n\t}\n\n\tif (args.length > 0) {\n\t\treturn args.join(' ');\n\t}\n\n\treturn readStdin();\n}\n\nfunction readStdin(): Promise<string> {\n\treturn new Promise((resolve, reject) => {\n\t\tif (process.stdin.isTTY) {\n\t\t\treject(new Error('No text provided; pass text args, --input-file, or pipe input'));\n\t\t\treturn;\n\t\t}\n\n\t\tlet data = '';\n\t\tprocess.stdin.setEncoding('utf-8');\n\t\tprocess.stdin.on('data', (chunk) => {\n\t\t\tdata += chunk;\n\t\t});\n\t\tprocess.stdin.on('end', () => {\n\t\t\tconst text = data.trim();\n\t\t\tif (!text) {\n\t\t\t\treject(new Error('stdin was empty'));\n\t\t\t\treturn;\n\t\t\t}\n\t\t\tresolve(text);\n\t\t});\n\t\tprocess.stdin.on('error', reject);\n\t});\n}\n\nfunction buildTTSRequest(\n\t_cmd: Command,\n\topts: SpeakOptions,\n\ttext: string,\n\tspeed: number,\n\toutputFormat: string\n): TTSRequest {\n\tconst voiceSettings: VoiceSettings = {\n\t\tspeed\n\t};\n\n\t// Stability\n\tif (opts.stability !== undefined) {\n\t\tconst stability = parseFloat(String(opts.stability));\n\t\tif (stability < 0 || stability > 1) {\n\t\t\tthrow new Error('Stability must be between 0 and 1');\n\t\t}\n\t\tif (opts.modelId === 'eleven_v3') {\n\t\t\tif (![0, 0.5, 1].some((v) => Math.abs(stability - v) < 1e-9)) {\n\t\t\t\tthrow new Error(\n\t\t\t\t\t'For eleven_v3, stability must be one of 0.0, 0.5, 1.0 (Creative/Natural/Robust)'\n\t\t\t\t);\n\t\t\t}\n\t\t}\n\t\tvoiceSettings.stability = stability;\n\t}\n\n\t// Similarity\n\tconst similarity = opts.similarity ?? opts.similarityBoost;\n\tif (similarity !== undefined) {\n\t\tconst simVal = parseFloat(String(similarity));\n\t\tif (simVal < 0 || simVal > 1) {\n\t\t\tthrow new Error('Similarity must be between 0 and 1');\n\t\t}\n\t\tvoiceSettings.similarity_boost = simVal;\n\t}\n\n\t// Style\n\tif (opts.style !== undefined) {\n\t\tconst styleVal = parseFloat(String(opts.style));\n\t\tif (styleVal < 0 || styleVal > 1) {\n\t\t\tthrow new Error('Style must be between 0 and 1');\n\t\t}\n\t\tvoiceSettings.style = styleVal;\n\t}\n\n\t// Speaker boost\n\tif (opts.speakerBoost && opts.noSpeakerBoost) {\n\t\tthrow new Error('Choose only one of --speaker-boost or --no-speaker-boost');\n\t}\n\tif (opts.speakerBoost) {\n\t\tvoiceSettings.use_speaker_boost = true;\n\t} else if (opts.noSpeakerBoost) {\n\t\tvoiceSettings.use_speaker_boost = false;\n\t}\n\n\tconst request: TTSRequest = {\n\t\ttext,\n\t\tmodel_id: opts.modelId,\n\t\toutput_format: outputFormat,\n\t\tvoice_settings: voiceSettings\n\t};\n\n\t// Seed\n\tif (opts.seed !== undefined) {\n\t\tconst seedVal = parseInt(String(opts.seed), 10);\n\t\tif (seedVal < 0 || seedVal > 4294967295) {\n\t\t\tthrow new Error('Seed must be between 0 and 4294967295');\n\t\t}\n\t\trequest.seed = seedVal;\n\t}\n\n\t// Normalize\n\tif (opts.normalize) {\n\t\tconst norm = opts.normalize.toLowerCase().trim();\n\t\tif (!['auto', 'on', 'off'].includes(norm)) {\n\t\t\tthrow new Error('Normalize must be one of: auto, on, off');\n\t\t}\n\t\trequest.apply_text_normalization = norm;\n\t}\n\n\t// Language\n\tif (opts.lang) {\n\t\tconst lang = opts.lang.toLowerCase().trim();\n\t\tif (lang.length !== 2 || !/^[a-z]+$/.test(lang)) {\n\t\t\tthrow new Error('Lang must be a 2-letter ISO 639-1 code (e.g. en, de, fr)');\n\t\t}\n\t\trequest.language_code = lang;\n\t}\n\n\treturn request;\n}\n\nasync function streamAndSave(\n\tclient: ElevenLabsClient,\n\tvoiceId: string,\n\tpayload: TTSRequest,\n\tlatencyTier: number,\n\toutputPath: string | undefined,\n\tshouldPlay: boolean,\n\tspinner: ReturnType<typeof ora>\n): Promise<number> {\n\tconst stream = await client.streamTTS(voiceId, payload, latencyTier);\n\n\tif (!stream) {\n\t\tthrow new Error('No stream returned from API');\n\t}\n\n\tconst chunks: Uint8Array[] = [];\n\tconst reader = stream.getReader();\n\n\tlet totalBytes = 0;\n\n\twhile (true) {\n\t\tconst { done, value } = await reader.read();\n\t\tif (done) break;\n\t\tif (value) {\n\t\t\tchunks.push(value);\n\t\t\ttotalBytes += value.length;\n\t\t\tspinner.text = `Generating speech... ${Math.round(totalBytes / 1024)}KB`;\n\t\t}\n\t}\n\n\tconst audioData = Buffer.concat(chunks);\n\n\t// Save to file if requested\n\tif (outputPath) {\n\t\tconst dir = path.dirname(outputPath);\n\t\tif (dir && dir !== '.') {\n\t\t\tfs.mkdirSync(dir, { recursive: true });\n\t\t}\n\t\tfs.writeFileSync(outputPath, audioData);\n\t\tlog.info(`Audio saved to ${outputPath}`);\n\t}\n\n\t// Play audio\n\tif (shouldPlay) {\n\t\tawait playAudio(audioData, spinner);\n\t}\n\n\treturn totalBytes;\n}\n\nasync function playAudio(audioData: Buffer, spinner: ReturnType<typeof ora>): Promise<void> {\n\tconst platform = os.platform();\n\t\n\t// Write to temp file\n\tconst tmpFile = path.join(os.tmpdir(), `awaz-${Date.now()}.mp3`);\n\tfs.writeFileSync(tmpFile, audioData);\n\n\treturn new Promise((resolve, reject) => {\n\t\tlet player: ReturnType<typeof spawn>;\n\n\t\tif (platform === 'darwin') {\n\t\t\tplayer = spawn('afplay', [tmpFile]);\n\t\t} else if (platform === 'linux') {\n\t\t\tplayer = spawn('aplay', [tmpFile]);\n\t\t} else if (platform === 'win32') {\n\t\t\tplayer = spawn('powershell', ['-c', `(New-Object Media.SoundPlayer '${tmpFile}').PlaySync()`]);\n\t\t} else {\n\t\t\tfs.unlinkSync(tmpFile);\n\t\t\treject(new Error(`Unsupported platform: ${platform}`));\n\t\t\treturn;\n\t\t}\n\n\t\tspinner.text = 'Playing audio...';\n\n\t\tplayer.on('close', (code) => {\n\t\t\tfs.unlinkSync(tmpFile);\n\t\t\tif (code === 0) {\n\t\t\t\tresolve();\n\t\t\t} else {\n\t\t\t\treject(new Error(`Audio player exited with code ${code}`));\n\t\t\t}\n\t\t});\n\n\t\tplayer.on('error', (err) => {\n\t\t\tfs.unlinkSync(tmpFile);\n\t\t\treject(err);\n\t\t});\n\t});\n}\n\nasync function convertAndSave(\n\tclient: ElevenLabsClient,\n\tvoiceId: string,\n\tpayload: TTSRequest,\n\toutputPath: string | undefined,\n\tshouldPlay: boolean,\n\tspinner: ReturnType<typeof ora>\n): Promise<number> {\n\tspinner.text = 'Converting text to speech...';\n\tconst audioData = await client.convertTTS(voiceId, payload);\n\n\t// Save to file if requested\n\tif (outputPath) {\n\t\tconst dir = path.dirname(outputPath);\n\t\tif (dir && dir !== '.') {\n\t\t\tfs.mkdirSync(dir, { recursive: true });\n\t\t}\n\t\tfs.writeFileSync(outputPath, audioData);\n\t\tlog.info(`Audio saved to ${outputPath}`);\n\t}\n\n\t// Play audio\n\tif (shouldPlay) {\n\t\tawait playAudio(audioData, spinner);\n\t}\n\n\treturn audioData.length;\n}\n\nfunction inferFormatFromExt(filePath: string): string | null {\n\tconst ext = path.extname(filePath).toLowerCase();\n\tswitch (ext) {\n\t\tcase '.mp3':\n\t\t\treturn 'mp3_44100_128';\n\t\tcase '.wav':\n\t\tcase '.wave':\n\t\t\treturn 'pcm_44100';\n\t\tdefault:\n\t\t\treturn null;\n\t}\n}\n\n\n","/**\n * ElevenLabs API client for text-to-speech\n */\n\nexport interface Voice {\n\tvoice_id: string;\n\tname: string;\n\tcategory: string;\n\tlabels?: Record<string, string>;\n\tpreview_url?: string;\n}\n\nexport interface VoiceSettings {\n\tstability?: number;\n\tsimilarity_boost?: number;\n\tstyle?: number;\n\tuse_speaker_boost?: boolean;\n\tspeed?: number;\n}\n\nexport interface TTSRequest {\n\ttext: string;\n\tmodel_id?: string;\n\tvoice_settings?: VoiceSettings;\n\toutput_format?: string;\n\tseed?: number;\n\tapply_text_normalization?: string;\n\tlanguage_code?: string;\n}\n\ninterface ListVoicesResponse {\n\tvoices: Voice[];\n\tnext_page_token?: string;\n}\n\nexport interface ElevenLabsClientConfig {\n\tapiKey: string;\n\tbaseUrl?: string;\n}\n\nexport class ElevenLabsClient {\n\tprivate readonly baseUrl: string;\n\tprivate readonly apiKey: string;\n\n\tconstructor(config: ElevenLabsClientConfig) {\n\t\tthis.apiKey = config.apiKey;\n\t\tthis.baseUrl = config.baseUrl || 'https://api.elevenlabs.io';\n\t}\n\n\tprivate async request<T>(\n\t\tpath: string,\n\t\toptions: RequestInit = {}\n\t): Promise<T> {\n\t\tconst url = `${this.baseUrl}${path}`;\n\t\tconst headers: Record<string, string> = {\n\t\t\t'xi-api-key': this.apiKey,\n\t\t\tAccept: 'application/json',\n\t\t\t...(options.headers as Record<string, string>)\n\t\t};\n\n\t\tconst response = await fetch(url, {\n\t\t\t...options,\n\t\t\theaders\n\t\t});\n\n\t\tif (!response.ok) {\n\t\t\tconst text = await response.text();\n\t\t\tthrow new Error(`ElevenLabs API error: ${response.status} ${response.statusText}: ${text}`);\n\t\t}\n\n\t\treturn response.json() as Promise<T>;\n\t}\n\n\t/**\n\t * List available voices\n\t */\n\tasync listVoices(search?: string): Promise<Voice[]> {\n\t\tlet path = '/v1/voices';\n\t\tif (search) {\n\t\t\tpath += `?search=${encodeURIComponent(search)}`;\n\t\t}\n\n\t\tconst response = await this.request<ListVoicesResponse>(path);\n\t\treturn response.voices;\n\t}\n\n\t/**\n\t * Stream TTS audio\n\t */\n\tasync streamTTS(\n\t\tvoiceId: string,\n\t\tpayload: TTSRequest,\n\t\tlatencyTier = 0\n\t): Promise<ReadableStream<Uint8Array> | null> {\n\t\tlet path = `/v1/text-to-speech/${voiceId}/stream`;\n\t\tif (latencyTier > 0) {\n\t\t\tpath += `?optimize_streaming_latency=${latencyTier}`;\n\t\t}\n\n\t\tconst response = await fetch(`${this.baseUrl}${path}`, {\n\t\t\tmethod: 'POST',\n\t\t\theaders: {\n\t\t\t\t'Content-Type': 'application/json',\n\t\t\t\tAccept: 'audio/mpeg',\n\t\t\t\t'xi-api-key': this.apiKey\n\t\t\t},\n\t\t\tbody: JSON.stringify(payload)\n\t\t});\n\n\t\tif (!response.ok) {\n\t\t\tconst text = await response.text();\n\t\t\tthrow new Error(`Stream TTS failed: ${response.status}: ${text}`);\n\t\t}\n\n\t\treturn response.body;\n\t}\n\n\t/**\n\t * Convert text to speech (non-streaming)\n\t */\n\tasync convertTTS(voiceId: string, payload: TTSRequest): Promise<Buffer> {\n\t\tconst path = `/v1/text-to-speech/${voiceId}`;\n\n\t\tconst response = await fetch(`${this.baseUrl}${path}`, {\n\t\t\tmethod: 'POST',\n\t\t\theaders: {\n\t\t\t\t'Content-Type': 'application/json',\n\t\t\t\tAccept: 'audio/mpeg',\n\t\t\t\t'xi-api-key': this.apiKey\n\t\t\t},\n\t\t\tbody: JSON.stringify(payload)\n\t\t});\n\n\t\tif (!response.ok) {\n\t\t\tconst text = await response.text();\n\t\t\tthrow new Error(`Convert TTS failed: ${response.status}: ${text}`);\n\t\t}\n\n\t\tconst arrayBuffer = await response.arrayBuffer();\n\t\treturn Buffer.from(arrayBuffer);\n\t}\n}\n\n/**\n * Get API key from environment or throw\n */\nexport function getApiKey(providedKey?: string): string {\n\tconst key =\n\t\tprovidedKey ||\n\t\tprocess.env.ELEVENLABS_API_KEY ||\n\t\tprocess.env.AWAZ_API_KEY;\n\n\tif (!key) {\n\t\tthrow new Error(\n\t\t\t'Missing ElevenLabs API key. Set --api-key or ELEVENLABS_API_KEY environment variable.'\n\t\t);\n\t}\n\n\treturn key;\n}\n\n/**\n * Get default voice ID from environment\n */\nexport function getDefaultVoiceId(): string | undefined {\n\treturn process.env.ELEVENLABS_VOICE_ID || process.env.AWAZ_VOICE_ID;\n}\n","import pc from 'picocolors';\n\nexport const banner = `\n${pc.white('▄▀█ █░█░█ ▄▀█ ▀█')}\n${pc.gray('█▀█ ▀▄▀▄▀ █▀█ █▄')}\n${pc.dim('text to speech cli.')}\n`;\n\nexport function showBanner(): void {\n\tconsole.log(banner);\n}\n","import pc from 'picocolors';\n\nexport function error(msg: string): void {\n\tconsole.error(`${pc.red('✖')} ${msg}`);\n}\n\nexport function warn(msg: string): void {\n\tconsole.error(`${pc.yellow('⚠')} ${msg}`);\n}\n\nexport function info(msg: string): void {\n\tconsole.error(`${pc.blue('ℹ')} ${msg}`);\n}\n\nexport function success(msg: string): void {\n\tconsole.error(`${pc.green('✔')} ${msg}`);\n}\n\nexport function dim(msg: string): string {\n\treturn pc.dim(msg);\n}\n\nexport function bold(msg: string): string {\n\treturn pc.bold(msg);\n}\n\nexport function cyan(msg: string): string {\n\treturn pc.cyan(msg);\n}\n\nexport function yellow(msg: string): string {\n\treturn pc.yellow(msg);\n}\n\nexport function green(msg: string): string {\n\treturn pc.green(msg);\n}\n\nexport function red(msg: string): string {\n\treturn pc.red(msg);\n}\n","import fs from 'node:fs';\nimport path from 'node:path';\nimport { fileURLToPath } from 'node:url';\n\nconst __dirname = path.dirname(fileURLToPath(import.meta.url));\n\ninterface PackageJson {\n\tname: string;\n\tversion: string;\n\tdescription: string;\n}\n\nlet _packageJson: PackageJson | null = null;\n\nexport function getPackageJson(): PackageJson {\n\tif (_packageJson) {\n\t\treturn _packageJson;\n\t}\n\n\t// Try to find package.json, walking up from current file\n\tlet dir = __dirname;\n\tfor (let i = 0; i < 5; i++) {\n\t\tconst pkgPath = path.join(dir, 'package.json');\n\t\tif (fs.existsSync(pkgPath)) {\n\t\t\t_packageJson = JSON.parse(fs.readFileSync(pkgPath, 'utf-8')) as PackageJson;\n\t\t\treturn _packageJson;\n\t\t}\n\t\tdir = path.dirname(dir);\n\t}\n\n\t// Fallback\n\treturn {\n\t\tname: 'awaz',\n\t\tversion: '0.0.1',\n\t\tdescription: 'Text to speech. Done right.'\n\t};\n}\n\nexport function getVersion(): string {\n\treturn getPackageJson().version;\n}\n","export function padRight(str: string, len: number): string {\n\tif (str.length >= len) return str.slice(0, len);\n\treturn str + ' '.repeat(len - str.length);\n}\n","import { Command } from 'commander';\nimport ora from 'ora';\nimport { ElevenLabsClient, getApiKey } from '../lib/elevenlabs.js';\nimport { log, padRight } from '../utils/index.js';\n\ninterface VoicesOptions {\n\tsearch?: string;\n\tlimit: number;\n\tapiKey?: string;\n\tbaseUrl: string;\n}\n\nexport function createVoicesCommand(): Command {\n\treturn new Command('voices')\n\t\t.description('List voices')\n\t\t.option('--search <query>', 'Filter by name')\n\t\t.option('--limit <n>', 'Max results (0 = all)', '100')\n\t\t.action(async function (this: Command) {\n\t\t\tconst opts = this.optsWithGlobals<VoicesOptions>();\n\t\t\tconst limit = parseInt(String(opts.limit), 10);\n\n\t\t\tconst spinner = ora('Fetching voices...').start();\n\n\t\t\ttry {\n\t\t\t\tconst apiKey = getApiKey(opts.apiKey);\n\t\t\t\tconst client = new ElevenLabsClient({\n\t\t\t\t\tapiKey,\n\t\t\t\t\tbaseUrl: opts.baseUrl\n\t\t\t\t});\n\n\t\t\t\tlet voices = await client.listVoices(opts.search);\n\t\t\t\tspinner.stop();\n\n\t\t\t\tif (limit > 0 && voices.length > limit) {\n\t\t\t\t\tvoices = voices.slice(0, limit);\n\t\t\t\t}\n\n\t\t\t\t// Print header\n\t\t\t\tconsole.log(\n\t\t\t\t\t`${padRight('VOICE ID', 24)} ${padRight('NAME', 24)} CATEGORY`\n\t\t\t\t);\n\n\t\t\t\t// Print rows\n\t\t\t\tfor (const v of voices) {\n\t\t\t\t\tconsole.log(\n\t\t\t\t\t\t`${padRight(v.voice_id, 24)} ${padRight(v.name, 24)} ${v.category}`\n\t\t\t\t\t);\n\t\t\t\t}\n\n\t\t\t\tif (voices.length === 0) {\n\t\t\t\t\tlog.info('No voices found');\n\t\t\t\t}\n\t\t\t} catch (err) {\n\t\t\t\tspinner.fail('Failed to fetch voices');\n\t\t\t\tlog.error((err as Error).message);\n\t\t\t\tprocess.exit(1);\n\t\t\t}\n\t\t});\n}\n\n\n","import { Command } from 'commander';\nimport { promptingGuide } from './prompting-guide.js';\n\nexport function createPromptingCommand(): Command {\n\treturn new Command('prompting')\n\t\t.aliases(['prompt', 'guide', 'tips'])\n\t\t.description('Tips for better output')\n\t\t.action(() => {\n\t\t\tconsole.log(promptingGuide.trim());\n\t\t});\n}\n","export const promptingGuide = `# Better results from awaz\n\nWhat you write matters. Here's what works.\n\n## Models\n\n**v3 (default)** — Most expressive. Tags like [whispers], [laughs]. No SSML.\n**v2** — Reliable. SSML pauses work. Predictable.\n**Flash** — Fast. ~75ms. Half the cost.\n**Turbo** — Balanced. ~250ms. Good quality.\n\n## Writing tips\n\nWrite how you'd say it. Short sentences. Natural breaks.\n\n**Punctuation = pacing:**\n- Comma, dash → slight pause\n- Ellipsis... → dramatic pause\n- Period → full stop\n- ! → energy\n\n**Spell it how it sounds.** \"API\" wrong? Try \"A P I\" or \"ay-pee-eye\".\n\n## v3 tags\n\n**Emotions:** [whispers], [shouts], [laughs], [sighs], [sarcastic], [excited]\n**Actions:** [clears throat], [exhales], [swallows]\n**Effects:** [applause], [short pause], [long pause]\n**Experimental:** [strong French accent], [sings]\n\nNot every voice responds to every tag. Experiment.\n\n## The knobs\n\n**--stability** (0-1)\nHigher = consistent. Lower = varied. v3 uses presets: 0=Creative, 0.5=Natural, 1=Robust.\n\n**--similarity** (0-1)\nHigher = closer to original voice sample.\n\n**--style** (0-1)\nHigher = more expressive. Can get weird if too high.\n\n**--speaker-boost**\nAdds clarity. Sometimes helps.\n\n**--seed**\nSame seed = same output. Good for A/B testing.\n\n## Examples\n\nNatural:\n awaz -v Roger --stability 0.5 \"We shipped today. It worked.\"\n\nFast:\n awaz --model-id eleven_flash_v2_5 \"Quick and cheap.\"\n\nDramatic (v3):\n awaz \"[whispers] Don't move. [short pause] Something's there...\"\n\n## TL;DR\n\n1. v3 for expression, v2 for reliability, Flash for speed\n2. Write naturally\n3. Experiment with tags on v3\n4. Tweak stability/similarity if it sounds off\n`;\n"],"mappings":";;;;;;;;AAAA,SAAS,WAAAA,UAAS,cAAc;AAChC,SAAS,cAAc;AACvB,SAAS,eAAe;;;ACFxB,SAAS,eAAe;AACxB,SAAS,aAAa;AACtB,OAAOC,SAAQ;AACf,OAAO,QAAQ;AACf,OAAOC,WAAU;AACjB,OAAO,SAAS;;;ACmCT,IAAM,mBAAN,MAAuB;AAAA,EACZ;AAAA,EACA;AAAA,EAEjB,YAAYC,SAAgC;AAC3C,SAAK,SAASA,QAAO;AACrB,SAAK,UAAUA,QAAO,WAAW;AAAA,EAClC;AAAA,EAEA,MAAc,QACbC,OACA,UAAuB,CAAC,GACX;AACb,UAAM,MAAM,GAAG,KAAK,OAAO,GAAGA,KAAI;AAClC,UAAM,UAAkC;AAAA,MACvC,cAAc,KAAK;AAAA,MACnB,QAAQ;AAAA,MACR,GAAI,QAAQ;AAAA,IACb;AAEA,UAAM,WAAW,MAAM,MAAM,KAAK;AAAA,MACjC,GAAG;AAAA,MACH;AAAA,IACD,CAAC;AAED,QAAI,CAAC,SAAS,IAAI;AACjB,YAAM,OAAO,MAAM,SAAS,KAAK;AACjC,YAAM,IAAI,MAAM,yBAAyB,SAAS,MAAM,IAAI,SAAS,UAAU,KAAK,IAAI,EAAE;AAAA,IAC3F;AAEA,WAAO,SAAS,KAAK;AAAA,EACtB;AAAA;AAAA;AAAA;AAAA,EAKA,MAAM,WAAW,QAAmC;AACnD,QAAIA,QAAO;AACX,QAAI,QAAQ;AACX,MAAAA,SAAQ,WAAW,mBAAmB,MAAM,CAAC;AAAA,IAC9C;AAEA,UAAM,WAAW,MAAM,KAAK,QAA4BA,KAAI;AAC5D,WAAO,SAAS;AAAA,EACjB;AAAA;AAAA;AAAA;AAAA,EAKA,MAAM,UACL,SACA,SACA,cAAc,GAC+B;AAC7C,QAAIA,QAAO,sBAAsB,OAAO;AACxC,QAAI,cAAc,GAAG;AACpB,MAAAA,SAAQ,+BAA+B,WAAW;AAAA,IACnD;AAEA,UAAM,WAAW,MAAM,MAAM,GAAG,KAAK,OAAO,GAAGA,KAAI,IAAI;AAAA,MACtD,QAAQ;AAAA,MACR,SAAS;AAAA,QACR,gBAAgB;AAAA,QAChB,QAAQ;AAAA,QACR,cAAc,KAAK;AAAA,MACpB;AAAA,MACA,MAAM,KAAK,UAAU,OAAO;AAAA,IAC7B,CAAC;AAED,QAAI,CAAC,SAAS,IAAI;AACjB,YAAM,OAAO,MAAM,SAAS,KAAK;AACjC,YAAM,IAAI,MAAM,sBAAsB,SAAS,MAAM,KAAK,IAAI,EAAE;AAAA,IACjE;AAEA,WAAO,SAAS;AAAA,EACjB;AAAA;AAAA;AAAA;AAAA,EAKA,MAAM,WAAW,SAAiB,SAAsC;AACvE,UAAMA,QAAO,sBAAsB,OAAO;AAE1C,UAAM,WAAW,MAAM,MAAM,GAAG,KAAK,OAAO,GAAGA,KAAI,IAAI;AAAA,MACtD,QAAQ;AAAA,MACR,SAAS;AAAA,QACR,gBAAgB;AAAA,QAChB,QAAQ;AAAA,QACR,cAAc,KAAK;AAAA,MACpB;AAAA,MACA,MAAM,KAAK,UAAU,OAAO;AAAA,IAC7B,CAAC;AAED,QAAI,CAAC,SAAS,IAAI;AACjB,YAAM,OAAO,MAAM,SAAS,KAAK;AACjC,YAAM,IAAI,MAAM,uBAAuB,SAAS,MAAM,KAAK,IAAI,EAAE;AAAA,IAClE;AAEA,UAAM,cAAc,MAAM,SAAS,YAAY;AAC/C,WAAO,OAAO,KAAK,WAAW;AAAA,EAC/B;AACD;AAKO,SAAS,UAAU,aAA8B;AACvD,QAAM,MACL,eACA,QAAQ,IAAI,sBACZ,QAAQ,IAAI;AAEb,MAAI,CAAC,KAAK;AACT,UAAM,IAAI;AAAA,MACT;AAAA,IACD;AAAA,EACD;AAEA,SAAO;AACR;AAKO,SAAS,oBAAwC;AACvD,SAAO,QAAQ,IAAI,uBAAuB,QAAQ,IAAI;AACvD;;;ACtKA,OAAO,QAAQ;AAER,IAAM,SAAS;AAAA,EACpB,GAAG,MAAM,mFAAkB,CAAC;AAAA,EAC5B,GAAG,KAAK,mFAAkB,CAAC;AAAA,EAC3B,GAAG,IAAI,qBAAqB,CAAC;AAAA;;;ACL/B;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,OAAOC,SAAQ;AAER,SAAS,MAAM,KAAmB;AACxC,UAAQ,MAAM,GAAGA,IAAG,IAAI,QAAG,CAAC,IAAI,GAAG,EAAE;AACtC;AAEO,SAAS,KAAK,KAAmB;AACvC,UAAQ,MAAM,GAAGA,IAAG,OAAO,QAAG,CAAC,IAAI,GAAG,EAAE;AACzC;AAEO,SAAS,KAAK,KAAmB;AACvC,UAAQ,MAAM,GAAGA,IAAG,KAAK,QAAG,CAAC,IAAI,GAAG,EAAE;AACvC;AAEO,SAAS,QAAQ,KAAmB;AAC1C,UAAQ,MAAM,GAAGA,IAAG,MAAM,QAAG,CAAC,IAAI,GAAG,EAAE;AACxC;AAEO,SAAS,IAAI,KAAqB;AACxC,SAAOA,IAAG,IAAI,GAAG;AAClB;AAEO,SAAS,KAAK,KAAqB;AACzC,SAAOA,IAAG,KAAK,GAAG;AACnB;AAEO,SAAS,KAAK,KAAqB;AACzC,SAAOA,IAAG,KAAK,GAAG;AACnB;AAEO,SAAS,OAAO,KAAqB;AAC3C,SAAOA,IAAG,OAAO,GAAG;AACrB;AAEO,SAAS,MAAM,KAAqB;AAC1C,SAAOA,IAAG,MAAM,GAAG;AACpB;AAEO,SAAS,IAAI,KAAqB;AACxC,SAAOA,IAAG,IAAI,GAAG;AAClB;;;ACxCA,OAAO,QAAQ;AACf,OAAO,UAAU;AACjB,SAAS,qBAAqB;AAE9B,IAAMC,aAAY,KAAK,QAAQ,cAAc,YAAY,GAAG,CAAC;AAQ7D,IAAI,eAAmC;AAEhC,SAAS,iBAA8B;AAC7C,MAAI,cAAc;AACjB,WAAO;AAAA,EACR;AAGA,MAAI,MAAMA;AACV,WAAS,IAAI,GAAG,IAAI,GAAG,KAAK;AAC3B,UAAM,UAAU,KAAK,KAAK,KAAK,cAAc;AAC7C,QAAI,GAAG,WAAW,OAAO,GAAG;AAC3B,qBAAe,KAAK,MAAM,GAAG,aAAa,SAAS,OAAO,CAAC;AAC3D,aAAO;AAAA,IACR;AACA,UAAM,KAAK,QAAQ,GAAG;AAAA,EACvB;AAGA,SAAO;AAAA,IACN,MAAM;AAAA,IACN,SAAS;AAAA,IACT,aAAa;AAAA,EACd;AACD;AAEO,SAAS,aAAqB;AACpC,SAAO,eAAe,EAAE;AACzB;;;ACxCO,SAAS,SAAS,KAAa,KAAqB;AAC1D,MAAI,IAAI,UAAU,IAAK,QAAO,IAAI,MAAM,GAAG,GAAG;AAC9C,SAAO,MAAM,IAAI,OAAO,MAAM,IAAI,MAAM;AACzC;;;ALuCA,IAAM,cAAc;AAEb,SAAS,qBAA8B;AAC7C,SAAO,IAAI,QAAQ,OAAO,EACxB,YAAY,iCAAiC,EAC7C,SAAS,aAAa,eAAe,EACrC,OAAO,mBAAmB,UAAU,EACpC,OAAO,sBAAsB,oCAAoC,EACjE,OAAO,mBAAmB,8DAA8D,WAAW,EACnG,OAAO,uBAAuB,oBAAoB,EAClD,OAAO,qBAAqB,gBAAgB,eAAe,EAC3D,OAAO,YAAY,iCAAiC,IAAI,EACxD,OAAO,eAAe,mBAAmB,EACzC,OAAO,UAAU,+BAA+B,IAAI,EACpD,OAAO,aAAa,kBAAkB,EACtC,OAAO,sBAAsB,wBAAwB,GAAG,EACxD,OAAO,eAAe,8BAA8B,KAAK,EACzD,OAAO,oBAAoB,gCAAgC,EAC3D,OAAO,2BAA2B,oCAAoC,EACtE,OAAO,mBAAmB,yBAAyB,EACnD,OAAO,oBAAoB,+BAA+B,EAC1D,OAAO,0BAA0B,sBAAsB,EACvD,OAAO,eAAe,sBAAsB,EAC5C,OAAO,mBAAmB,aAAa,EACvC,OAAO,sBAAsB,kBAAkB,EAC/C,OAAO,cAAc,yBAAyB,EAC9C,OAAO,sBAAsB,kCAAkC,EAC/D,OAAO,iBAAiB,6BAA6B,EACrD,OAAO,aAAa,qBAAqB,KAAK,EAE9C,OAAO,cAAc,iCAAiC,EACtD,OAAO,yBAAyB,iCAAiC,EACjE,OAAO,2BAA2B,iCAAiC,EACnE,OAAO,wBAAwB,iCAAiC,EAChE,OAAO,uBAAuB,iCAAiC,EAC/D,OAAO,uBAAuB,iCAAiC,EAC/D,OAAO,kBAAkB,iCAAiC,EAC1D,OAAO,kBAAkB,iCAAiC,EAC1D,OAAO,iBAAiB,iCAAiC,EACzD,OAAO,eAA+B,UAAoB;AAC1D,UAAM,OAAO,KAAK,gBAA8B;AAEhD,QAAI;AAEH,YAAM,QAAQ,WAAW,OAAO,KAAK,KAAK,CAAC;AAC3C,YAAM,cAAc,SAAS,OAAO,KAAK,WAAW,GAAG,EAAE;AAGzD,UAAI,aAAa;AACjB,UAAI,KAAK,MAAM;AACd,cAAM,OAAO,SAAS,OAAO,KAAK,IAAI,GAAG,EAAE;AAC3C,qBAAa,OAAO;AACpB,YAAI,cAAc,OAAO,cAAc,GAAK;AAC3C,gBAAM,IAAI;AAAA,YACT,QAAQ,IAAI,sBAAsB,WAAW,QAAQ,CAAC,CAAC;AAAA,UACxD;AAAA,QACD;AAAA,MACD,WAAW,cAAc,OAAO,cAAc,GAAK;AAClD,cAAM,IAAI,MAAM,6DAA6D;AAAA,MAC9E;AAGA,YAAM,SAAS,UAAU,KAAK,MAAM;AACpC,YAAM,SAAS,IAAI,iBAAiB;AAAA,QACnC;AAAA,QACA,SAAS,KAAK;AAAA,MACf,CAAC;AAGD,UAAI,UAAU,KAAK,SAAS,KAAK,WAAW,kBAAkB;AAE9D,UAAI,YAAY,KAAK;AAEpB,cAAM,SAAS,MAAM,OAAO,WAAW;AACvC,gBAAQ,IAAI,GAAG,SAAS,YAAY,EAAE,CAAC,KAAK,SAAS,QAAQ,EAAE,CAAC,YAAY;AAC5E,mBAAW,KAAK,QAAQ;AACvB,kBAAQ;AAAA,YACP,GAAG,SAAS,EAAE,UAAU,EAAE,CAAC,KAAK,SAAS,EAAE,MAAM,EAAE,CAAC,KAAK,EAAE,QAAQ;AAAA,UACpE;AAAA,QACD;AACA;AAAA,MACD;AAEA,gBAAU,MAAM,aAAa,QAAQ,OAAO;AAG5C,YAAM,OAAO,MAAM,YAAY,UAAU,KAAK,SAAS;AAGvD,UAAI,aAAa,KAAK;AACtB,UAAI,KAAK,UAAU,CAAC,KAAK,qBAAqB,MAAM,GAAG;AACtD,qBAAa;AAAA,MACd;AAGA,UAAI,eAAe,KAAK;AACxB,UAAI,KAAK,QAAQ;AAChB,cAAM,WAAW,mBAAmB,KAAK,MAAM;AAC/C,YAAI,UAAU;AACb,yBAAe;AAAA,QAChB;AAAA,MACD;AAGA,YAAM,UAAU,gBAAgB,MAAM,MAAM,MAAM,YAAY,YAAY;AAE1E,YAAM,UAAU,IAAI,sBAAsB,EAAE,MAAM;AAClD,YAAM,QAAQ,KAAK,IAAI;AACvB,UAAI,QAAQ;AAEZ,UAAI,KAAK,QAAQ;AAChB,gBAAQ,MAAM;AAAA,UACb;AAAA,UACA;AAAA,UACA;AAAA,UACA;AAAA,UACA,KAAK;AAAA,UACL;AAAA,UACA;AAAA,QACD;AAAA,MACD,OAAO;AACN,gBAAQ,MAAM;AAAA,UACb;AAAA,UACA;AAAA,UACA;AAAA,UACA,KAAK;AAAA,UACL;AAAA,UACA;AAAA,QACD;AAAA,MACD;AAEA,cAAQ,QAAQ,MAAM;AAEtB,UAAI,KAAK,SAAS;AACjB,cAAM,WAAW,KAAK,IAAI,IAAI;AAC9B,gBAAQ;AAAA,UACP,kBAAkB,KAAK,MAAM,UAAU,KAAK,UAAU,KAAK,OAAO,UAAU,OAAO,WAAW,KAAK,MAAM,gBAAgB,WAAW,QAAQ,QAAQ;AAAA,QACrJ;AAAA,MACD;AAAA,IACD,SAAS,KAAK;AACb,kBAAI,MAAO,IAAc,OAAO;AAChC,cAAQ,KAAK,CAAC;AAAA,IACf;AAAA,EACD,CAAC;AACH;AAEA,eAAe,aACd,QACA,YACkB;AAClB,MAAI,CAAC,YAAY;AAEhB,UAAMC,UAAS,MAAM,OAAO,WAAW;AACvC,QAAIA,QAAO,WAAW,GAAG;AACxB,YAAM,IAAI,MAAM,iEAAiE;AAAA,IAClF;AACA,gBAAI,KAAK,uBAAuBA,QAAO,CAAC,EAAE,IAAI,KAAKA,QAAO,CAAC,EAAE,QAAQ,GAAG;AACxE,WAAOA,QAAO,CAAC,EAAE;AAAA,EAClB;AAGA,MAAI,WAAW,UAAU,MAAM,QAAQ,KAAK,UAAU,GAAG;AACxD,WAAO;AAAA,EACR;AAGA,QAAM,SAAS,MAAM,OAAO,WAAW,UAAU;AACjD,QAAM,kBAAkB,WAAW,YAAY;AAG/C,QAAM,QAAQ,OAAO,KAAK,CAAC,MAAa,EAAE,KAAK,YAAY,MAAM,eAAe;AAChF,MAAI,OAAO;AACV,gBAAI,KAAK,eAAe,MAAM,IAAI,KAAK,MAAM,QAAQ,GAAG;AACxD,WAAO,MAAM;AAAA,EACd;AAGA,MAAI,OAAO,SAAS,GAAG;AACtB,UAAM,IAAI,OAAO,CAAC;AAClB,gBAAI,KAAK,6BAA6B,EAAE,IAAI,KAAK,EAAE,QAAQ,GAAG;AAC9D,WAAO,EAAE;AAAA,EACV;AAEA,QAAM,IAAI,MAAM,UAAU,UAAU,0CAA0C;AAC/E;AAEA,eAAe,YAAY,MAAgB,WAAqC;AAC/E,MAAI,WAAW;AACd,QAAI,cAAc,KAAK;AACtB,aAAO,UAAU;AAAA,IAClB;AACA,UAAM,OAAOC,IAAG,aAAa,WAAW,OAAO;AAC/C,UAAM,OAAO,KAAK,KAAK;AACvB,QAAI,CAAC,MAAM;AACV,YAAM,IAAI,MAAM,sBAAsB;AAAA,IACvC;AACA,WAAO;AAAA,EACR;AAEA,MAAI,KAAK,SAAS,GAAG;AACpB,WAAO,KAAK,KAAK,GAAG;AAAA,EACrB;AAEA,SAAO,UAAU;AAClB;AAEA,SAAS,YAA6B;AACrC,SAAO,IAAI,QAAQ,CAACC,UAAS,WAAW;AACvC,QAAI,QAAQ,MAAM,OAAO;AACxB,aAAO,IAAI,MAAM,+DAA+D,CAAC;AACjF;AAAA,IACD;AAEA,QAAI,OAAO;AACX,YAAQ,MAAM,YAAY,OAAO;AACjC,YAAQ,MAAM,GAAG,QAAQ,CAAC,UAAU;AACnC,cAAQ;AAAA,IACT,CAAC;AACD,YAAQ,MAAM,GAAG,OAAO,MAAM;AAC7B,YAAM,OAAO,KAAK,KAAK;AACvB,UAAI,CAAC,MAAM;AACV,eAAO,IAAI,MAAM,iBAAiB,CAAC;AACnC;AAAA,MACD;AACA,MAAAA,SAAQ,IAAI;AAAA,IACb,CAAC;AACD,YAAQ,MAAM,GAAG,SAAS,MAAM;AAAA,EACjC,CAAC;AACF;AAEA,SAAS,gBACR,MACA,MACA,MACA,OACA,cACa;AACb,QAAM,gBAA+B;AAAA,IACpC;AAAA,EACD;AAGA,MAAI,KAAK,cAAc,QAAW;AACjC,UAAM,YAAY,WAAW,OAAO,KAAK,SAAS,CAAC;AACnD,QAAI,YAAY,KAAK,YAAY,GAAG;AACnC,YAAM,IAAI,MAAM,mCAAmC;AAAA,IACpD;AACA,QAAI,KAAK,YAAY,aAAa;AACjC,UAAI,CAAC,CAAC,GAAG,KAAK,CAAC,EAAE,KAAK,CAAC,MAAM,KAAK,IAAI,YAAY,CAAC,IAAI,IAAI,GAAG;AAC7D,cAAM,IAAI;AAAA,UACT;AAAA,QACD;AAAA,MACD;AAAA,IACD;AACA,kBAAc,YAAY;AAAA,EAC3B;AAGA,QAAM,aAAa,KAAK,cAAc,KAAK;AAC3C,MAAI,eAAe,QAAW;AAC7B,UAAM,SAAS,WAAW,OAAO,UAAU,CAAC;AAC5C,QAAI,SAAS,KAAK,SAAS,GAAG;AAC7B,YAAM,IAAI,MAAM,oCAAoC;AAAA,IACrD;AACA,kBAAc,mBAAmB;AAAA,EAClC;AAGA,MAAI,KAAK,UAAU,QAAW;AAC7B,UAAM,WAAW,WAAW,OAAO,KAAK,KAAK,CAAC;AAC9C,QAAI,WAAW,KAAK,WAAW,GAAG;AACjC,YAAM,IAAI,MAAM,+BAA+B;AAAA,IAChD;AACA,kBAAc,QAAQ;AAAA,EACvB;AAGA,MAAI,KAAK,gBAAgB,KAAK,gBAAgB;AAC7C,UAAM,IAAI,MAAM,0DAA0D;AAAA,EAC3E;AACA,MAAI,KAAK,cAAc;AACtB,kBAAc,oBAAoB;AAAA,EACnC,WAAW,KAAK,gBAAgB;AAC/B,kBAAc,oBAAoB;AAAA,EACnC;AAEA,QAAM,UAAsB;AAAA,IAC3B;AAAA,IACA,UAAU,KAAK;AAAA,IACf,eAAe;AAAA,IACf,gBAAgB;AAAA,EACjB;AAGA,MAAI,KAAK,SAAS,QAAW;AAC5B,UAAM,UAAU,SAAS,OAAO,KAAK,IAAI,GAAG,EAAE;AAC9C,QAAI,UAAU,KAAK,UAAU,YAAY;AACxC,YAAM,IAAI,MAAM,uCAAuC;AAAA,IACxD;AACA,YAAQ,OAAO;AAAA,EAChB;AAGA,MAAI,KAAK,WAAW;AACnB,UAAM,OAAO,KAAK,UAAU,YAAY,EAAE,KAAK;AAC/C,QAAI,CAAC,CAAC,QAAQ,MAAM,KAAK,EAAE,SAAS,IAAI,GAAG;AAC1C,YAAM,IAAI,MAAM,yCAAyC;AAAA,IAC1D;AACA,YAAQ,2BAA2B;AAAA,EACpC;AAGA,MAAI,KAAK,MAAM;AACd,UAAM,OAAO,KAAK,KAAK,YAAY,EAAE,KAAK;AAC1C,QAAI,KAAK,WAAW,KAAK,CAAC,WAAW,KAAK,IAAI,GAAG;AAChD,YAAM,IAAI,MAAM,0DAA0D;AAAA,IAC3E;AACA,YAAQ,gBAAgB;AAAA,EACzB;AAEA,SAAO;AACR;AAEA,eAAe,cACd,QACA,SACA,SACA,aACA,YACA,YACA,SACkB;AAClB,QAAM,SAAS,MAAM,OAAO,UAAU,SAAS,SAAS,WAAW;AAEnE,MAAI,CAAC,QAAQ;AACZ,UAAM,IAAI,MAAM,6BAA6B;AAAA,EAC9C;AAEA,QAAM,SAAuB,CAAC;AAC9B,QAAM,SAAS,OAAO,UAAU;AAEhC,MAAI,aAAa;AAEjB,SAAO,MAAM;AACZ,UAAM,EAAE,MAAM,MAAM,IAAI,MAAM,OAAO,KAAK;AAC1C,QAAI,KAAM;AACV,QAAI,OAAO;AACV,aAAO,KAAK,KAAK;AACjB,oBAAc,MAAM;AACpB,cAAQ,OAAO,wBAAwB,KAAK,MAAM,aAAa,IAAI,CAAC;AAAA,IACrE;AAAA,EACD;AAEA,QAAM,YAAY,OAAO,OAAO,MAAM;AAGtC,MAAI,YAAY;AACf,UAAM,MAAMC,MAAK,QAAQ,UAAU;AACnC,QAAI,OAAO,QAAQ,KAAK;AACvB,MAAAF,IAAG,UAAU,KAAK,EAAE,WAAW,KAAK,CAAC;AAAA,IACtC;AACA,IAAAA,IAAG,cAAc,YAAY,SAAS;AACtC,gBAAI,KAAK,kBAAkB,UAAU,EAAE;AAAA,EACxC;AAGA,MAAI,YAAY;AACf,UAAM,UAAU,WAAW,OAAO;AAAA,EACnC;AAEA,SAAO;AACR;AAEA,eAAe,UAAU,WAAmB,SAAgD;AAC3F,QAAM,WAAW,GAAG,SAAS;AAG7B,QAAM,UAAUE,MAAK,KAAK,GAAG,OAAO,GAAG,QAAQ,KAAK,IAAI,CAAC,MAAM;AAC/D,EAAAF,IAAG,cAAc,SAAS,SAAS;AAEnC,SAAO,IAAI,QAAQ,CAACC,UAAS,WAAW;AACvC,QAAI;AAEJ,QAAI,aAAa,UAAU;AAC1B,eAAS,MAAM,UAAU,CAAC,OAAO,CAAC;AAAA,IACnC,WAAW,aAAa,SAAS;AAChC,eAAS,MAAM,SAAS,CAAC,OAAO,CAAC;AAAA,IAClC,WAAW,aAAa,SAAS;AAChC,eAAS,MAAM,cAAc,CAAC,MAAM,kCAAkC,OAAO,eAAe,CAAC;AAAA,IAC9F,OAAO;AACN,MAAAD,IAAG,WAAW,OAAO;AACrB,aAAO,IAAI,MAAM,yBAAyB,QAAQ,EAAE,CAAC;AACrD;AAAA,IACD;AAEA,YAAQ,OAAO;AAEf,WAAO,GAAG,SAAS,CAAC,SAAS;AAC5B,MAAAA,IAAG,WAAW,OAAO;AACrB,UAAI,SAAS,GAAG;AACf,QAAAC,SAAQ;AAAA,MACT,OAAO;AACN,eAAO,IAAI,MAAM,iCAAiC,IAAI,EAAE,CAAC;AAAA,MAC1D;AAAA,IACD,CAAC;AAED,WAAO,GAAG,SAAS,CAAC,QAAQ;AAC3B,MAAAD,IAAG,WAAW,OAAO;AACrB,aAAO,GAAG;AAAA,IACX,CAAC;AAAA,EACF,CAAC;AACF;AAEA,eAAe,eACd,QACA,SACA,SACA,YACA,YACA,SACkB;AAClB,UAAQ,OAAO;AACf,QAAM,YAAY,MAAM,OAAO,WAAW,SAAS,OAAO;AAG1D,MAAI,YAAY;AACf,UAAM,MAAME,MAAK,QAAQ,UAAU;AACnC,QAAI,OAAO,QAAQ,KAAK;AACvB,MAAAF,IAAG,UAAU,KAAK,EAAE,WAAW,KAAK,CAAC;AAAA,IACtC;AACA,IAAAA,IAAG,cAAc,YAAY,SAAS;AACtC,gBAAI,KAAK,kBAAkB,UAAU,EAAE;AAAA,EACxC;AAGA,MAAI,YAAY;AACf,UAAM,UAAU,WAAW,OAAO;AAAA,EACnC;AAEA,SAAO,UAAU;AAClB;AAEA,SAAS,mBAAmB,UAAiC;AAC5D,QAAM,MAAME,MAAK,QAAQ,QAAQ,EAAE,YAAY;AAC/C,UAAQ,KAAK;AAAA,IACZ,KAAK;AACJ,aAAO;AAAA,IACR,KAAK;AAAA,IACL,KAAK;AACJ,aAAO;AAAA,IACR;AACC,aAAO;AAAA,EACT;AACD;;;AM/eA,SAAS,WAAAC,gBAAe;AACxB,OAAOC,UAAS;AAWT,SAAS,sBAA+B;AAC9C,SAAO,IAAIC,SAAQ,QAAQ,EACzB,YAAY,aAAa,EACzB,OAAO,oBAAoB,gBAAgB,EAC3C,OAAO,eAAe,yBAAyB,KAAK,EACpD,OAAO,iBAA+B;AACtC,UAAM,OAAO,KAAK,gBAA+B;AACjD,UAAM,QAAQ,SAAS,OAAO,KAAK,KAAK,GAAG,EAAE;AAE7C,UAAM,UAAUC,KAAI,oBAAoB,EAAE,MAAM;AAEhD,QAAI;AACH,YAAM,SAAS,UAAU,KAAK,MAAM;AACpC,YAAM,SAAS,IAAI,iBAAiB;AAAA,QACnC;AAAA,QACA,SAAS,KAAK;AAAA,MACf,CAAC;AAED,UAAI,SAAS,MAAM,OAAO,WAAW,KAAK,MAAM;AAChD,cAAQ,KAAK;AAEb,UAAI,QAAQ,KAAK,OAAO,SAAS,OAAO;AACvC,iBAAS,OAAO,MAAM,GAAG,KAAK;AAAA,MAC/B;AAGA,cAAQ;AAAA,QACP,GAAG,SAAS,YAAY,EAAE,CAAC,KAAK,SAAS,QAAQ,EAAE,CAAC;AAAA,MACrD;AAGA,iBAAW,KAAK,QAAQ;AACvB,gBAAQ;AAAA,UACP,GAAG,SAAS,EAAE,UAAU,EAAE,CAAC,KAAK,SAAS,EAAE,MAAM,EAAE,CAAC,KAAK,EAAE,QAAQ;AAAA,QACpE;AAAA,MACD;AAEA,UAAI,OAAO,WAAW,GAAG;AACxB,oBAAI,KAAK,iBAAiB;AAAA,MAC3B;AAAA,IACD,SAAS,KAAK;AACb,cAAQ,KAAK,wBAAwB;AACrC,kBAAI,MAAO,IAAc,OAAO;AAChC,cAAQ,KAAK,CAAC;AAAA,IACf;AAAA,EACD,CAAC;AACH;;;AC1DA,SAAS,WAAAC,gBAAe;;;ACAjB,IAAM,iBAAiB;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;;ADGvB,SAAS,yBAAkC;AACjD,SAAO,IAAIC,SAAQ,WAAW,EAC5B,QAAQ,CAAC,UAAU,SAAS,MAAM,CAAC,EACnC,YAAY,wBAAwB,EACpC,OAAO,MAAM;AACb,YAAQ,IAAI,eAAe,KAAK,CAAC;AAAA,EAClC,CAAC;AACH;;;ARJA,IAAM,UAAU,IAAIC,SAAQ;AAG5B,IAAM,UAAU,WAAW;AAE3B,QACE,KAAK,MAAM,EACX,YAAY,6BAA6B,EACzC,QAAQ,SAAS,iBAAiB,wBAAwB,EAC1D,WAAW,cAAc,0BAA0B,EACnD;AAAA,EACA,IAAI,OAAO,mBAAmB,4CAA4C;AAC3E,EACC;AAAA,EACA,IAAI,OAAO,oBAAoB,kCAAkC,EAC/D,QAAQ,2BAA2B;AACtC,EACC,UAAU,IAAI,OAAO,SAAS,EAAE,SAAS,CAAC,EAC1C,UAAU,IAAI,OAAO,gBAAgB,yBAAyB,CAAC;AAGjE,QAAQ,WAAW,mBAAmB,CAAC;AACvC,QAAQ,WAAW,oBAAoB,CAAC;AACxC,QAAQ,WAAW,uBAAuB,CAAC;AAG3C,SAAS,sBAA4B;AACpC,MAAI,QAAQ,KAAK,UAAU,GAAG;AAC7B;AAAA,EACD;AAGA,MAAI,QAAQ,KAAK,CAAC,MAAM,MAAM;AAC7B,YAAQ,OAAO,CAAC,QAAQ,KAAK,CAAC,GAAG,QAAQ,KAAK,CAAC,GAAG,GAAG,QAAQ,KAAK,MAAM,CAAC,CAAC;AAC1E,QAAI,QAAQ,KAAK,UAAU,GAAG;AAC7B;AAAA,IACD;AAAA,EACD;AAEA,QAAM,QAAQ,QAAQ,KAAK,CAAC;AAG5B,QAAM,gBAAgB,CAAC,SAAS,UAAU,aAAa,UAAU,SAAS,MAAM;AAChF,QAAM,UACL,cAAc,SAAS,MAAM,YAAY,CAAC,KAC1C,UAAU,QACV,UAAU,YACV,UAAU,QACV,UAAU;AAEX,MAAI,CAAC,SAAS;AAEb,YAAQ,OAAO,CAAC,QAAQ,KAAK,CAAC,GAAG,QAAQ,KAAK,CAAC,GAAG,SAAS,GAAG,QAAQ,KAAK,MAAM,CAAC,CAAC;AAAA,EACpF;AACD;AAGA,QAAQ,gBAAgB;AAAA,EACvB,UAAU,CAAC,QAAQ,QAAQ,OAAO,MAAM,GAAG;AAAA,EAC3C,UAAU,CAAC,QAAQ,QAAQ,OAAO,MAAM,GAAG;AAC5C,CAAC;AAGD,IAAM,eAAe,QAAQ,gBAAgB,KAAK,OAAO;AACzD,QAAQ,kBAAkB,WAAoB;AAC7C,SAAO,SAAS,OAAO,aAAa;AACrC;AAEA,eAAsB,MAAqB;AAE1C,MAAI,QAAQ,KAAK,SAAS,IAAI,KAAK,QAAQ,KAAK,SAAS,WAAW,GAAG;AACtE,YAAQ,IAAI,OAAO;AACnB,YAAQ,KAAK,CAAC;AAAA,EACf;AAGA,QAAM,WAAW,QAAQ,KAAK,UAAU,SAAO,QAAQ,OAAO;AAC9D,QAAM,aAAa,aAAa,MAAM,QAAQ,KAAK,WAAW,CAAC;AAC/D,QAAM,YAAY,QAAQ,IAAI,sBAAsB,QAAQ,IAAI;AAEhE,MAAI,YAAY;AACf,WAAO,EAAE,MAAM,QAAQ,QAAQ,KAAK,WAAW,CAAC,CAAC,GAAG,OAAO,KAAK,CAAC;AAAA,EAClE;AAEA,MAAI,CAAC,cAAc,CAAC,WAAW;AAC9B,WAAO,EAAE,OAAO,KAAK,CAAC;AAAA,EACvB;AAEA,sBAAoB;AACpB,QAAM,QAAQ,WAAW,QAAQ,IAAI;AACtC;AAKA,IAAI,EAAE,MAAM,CAAC,QAAQ;AACpB,UAAQ,MAAM,GAAG;AACjB,UAAQ,KAAK,CAAC;AACf,CAAC;","names":["Command","fs","path","config","path","pc","__dirname","voices","fs","resolve","path","Command","ora","Command","ora","Command","Command","Command"]}
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ElevenLabs API client for text-to-speech
|
|
3
|
+
*/
|
|
4
|
+
interface Voice {
|
|
5
|
+
voice_id: string;
|
|
6
|
+
name: string;
|
|
7
|
+
category: string;
|
|
8
|
+
labels?: Record<string, string>;
|
|
9
|
+
preview_url?: string;
|
|
10
|
+
}
|
|
11
|
+
interface VoiceSettings {
|
|
12
|
+
stability?: number;
|
|
13
|
+
similarity_boost?: number;
|
|
14
|
+
style?: number;
|
|
15
|
+
use_speaker_boost?: boolean;
|
|
16
|
+
speed?: number;
|
|
17
|
+
}
|
|
18
|
+
interface TTSRequest {
|
|
19
|
+
text: string;
|
|
20
|
+
model_id?: string;
|
|
21
|
+
voice_settings?: VoiceSettings;
|
|
22
|
+
output_format?: string;
|
|
23
|
+
seed?: number;
|
|
24
|
+
apply_text_normalization?: string;
|
|
25
|
+
language_code?: string;
|
|
26
|
+
}
|
|
27
|
+
interface ElevenLabsClientConfig {
|
|
28
|
+
apiKey: string;
|
|
29
|
+
baseUrl?: string;
|
|
30
|
+
}
|
|
31
|
+
declare class ElevenLabsClient {
|
|
32
|
+
private readonly baseUrl;
|
|
33
|
+
private readonly apiKey;
|
|
34
|
+
constructor(config: ElevenLabsClientConfig);
|
|
35
|
+
private request;
|
|
36
|
+
/**
|
|
37
|
+
* List available voices
|
|
38
|
+
*/
|
|
39
|
+
listVoices(search?: string): Promise<Voice[]>;
|
|
40
|
+
/**
|
|
41
|
+
* Stream TTS audio
|
|
42
|
+
*/
|
|
43
|
+
streamTTS(voiceId: string, payload: TTSRequest, latencyTier?: number): Promise<ReadableStream<Uint8Array> | null>;
|
|
44
|
+
/**
|
|
45
|
+
* Convert text to speech (non-streaming)
|
|
46
|
+
*/
|
|
47
|
+
convertTTS(voiceId: string, payload: TTSRequest): Promise<Buffer>;
|
|
48
|
+
}
|
|
49
|
+
/**
|
|
50
|
+
* Get API key from environment or throw
|
|
51
|
+
*/
|
|
52
|
+
declare function getApiKey(providedKey?: string): string;
|
|
53
|
+
/**
|
|
54
|
+
* Get default voice ID from environment
|
|
55
|
+
*/
|
|
56
|
+
declare function getDefaultVoiceId(): string | undefined;
|
|
57
|
+
|
|
58
|
+
interface PackageJson {
|
|
59
|
+
name: string;
|
|
60
|
+
version: string;
|
|
61
|
+
description: string;
|
|
62
|
+
}
|
|
63
|
+
declare function getPackageJson(): PackageJson;
|
|
64
|
+
declare function getVersion(): string;
|
|
65
|
+
|
|
66
|
+
export { ElevenLabsClient, type ElevenLabsClientConfig, type TTSRequest, type Voice, type VoiceSettings, getApiKey, getDefaultVoiceId, getPackageJson, getVersion };
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
// src/lib/elevenlabs.ts
|
|
4
|
+
var ElevenLabsClient = class {
|
|
5
|
+
baseUrl;
|
|
6
|
+
apiKey;
|
|
7
|
+
constructor(config) {
|
|
8
|
+
this.apiKey = config.apiKey;
|
|
9
|
+
this.baseUrl = config.baseUrl || "https://api.elevenlabs.io";
|
|
10
|
+
}
|
|
11
|
+
async request(path2, options = {}) {
|
|
12
|
+
const url = `${this.baseUrl}${path2}`;
|
|
13
|
+
const headers = {
|
|
14
|
+
"xi-api-key": this.apiKey,
|
|
15
|
+
Accept: "application/json",
|
|
16
|
+
...options.headers
|
|
17
|
+
};
|
|
18
|
+
const response = await fetch(url, {
|
|
19
|
+
...options,
|
|
20
|
+
headers
|
|
21
|
+
});
|
|
22
|
+
if (!response.ok) {
|
|
23
|
+
const text = await response.text();
|
|
24
|
+
throw new Error(`ElevenLabs API error: ${response.status} ${response.statusText}: ${text}`);
|
|
25
|
+
}
|
|
26
|
+
return response.json();
|
|
27
|
+
}
|
|
28
|
+
/**
|
|
29
|
+
* List available voices
|
|
30
|
+
*/
|
|
31
|
+
async listVoices(search) {
|
|
32
|
+
let path2 = "/v1/voices";
|
|
33
|
+
if (search) {
|
|
34
|
+
path2 += `?search=${encodeURIComponent(search)}`;
|
|
35
|
+
}
|
|
36
|
+
const response = await this.request(path2);
|
|
37
|
+
return response.voices;
|
|
38
|
+
}
|
|
39
|
+
/**
|
|
40
|
+
* Stream TTS audio
|
|
41
|
+
*/
|
|
42
|
+
async streamTTS(voiceId, payload, latencyTier = 0) {
|
|
43
|
+
let path2 = `/v1/text-to-speech/${voiceId}/stream`;
|
|
44
|
+
if (latencyTier > 0) {
|
|
45
|
+
path2 += `?optimize_streaming_latency=${latencyTier}`;
|
|
46
|
+
}
|
|
47
|
+
const response = await fetch(`${this.baseUrl}${path2}`, {
|
|
48
|
+
method: "POST",
|
|
49
|
+
headers: {
|
|
50
|
+
"Content-Type": "application/json",
|
|
51
|
+
Accept: "audio/mpeg",
|
|
52
|
+
"xi-api-key": this.apiKey
|
|
53
|
+
},
|
|
54
|
+
body: JSON.stringify(payload)
|
|
55
|
+
});
|
|
56
|
+
if (!response.ok) {
|
|
57
|
+
const text = await response.text();
|
|
58
|
+
throw new Error(`Stream TTS failed: ${response.status}: ${text}`);
|
|
59
|
+
}
|
|
60
|
+
return response.body;
|
|
61
|
+
}
|
|
62
|
+
/**
|
|
63
|
+
* Convert text to speech (non-streaming)
|
|
64
|
+
*/
|
|
65
|
+
async convertTTS(voiceId, payload) {
|
|
66
|
+
const path2 = `/v1/text-to-speech/${voiceId}`;
|
|
67
|
+
const response = await fetch(`${this.baseUrl}${path2}`, {
|
|
68
|
+
method: "POST",
|
|
69
|
+
headers: {
|
|
70
|
+
"Content-Type": "application/json",
|
|
71
|
+
Accept: "audio/mpeg",
|
|
72
|
+
"xi-api-key": this.apiKey
|
|
73
|
+
},
|
|
74
|
+
body: JSON.stringify(payload)
|
|
75
|
+
});
|
|
76
|
+
if (!response.ok) {
|
|
77
|
+
const text = await response.text();
|
|
78
|
+
throw new Error(`Convert TTS failed: ${response.status}: ${text}`);
|
|
79
|
+
}
|
|
80
|
+
const arrayBuffer = await response.arrayBuffer();
|
|
81
|
+
return Buffer.from(arrayBuffer);
|
|
82
|
+
}
|
|
83
|
+
};
|
|
84
|
+
function getApiKey(providedKey) {
|
|
85
|
+
const key = providedKey || process.env.ELEVENLABS_API_KEY || process.env.AWAZ_API_KEY;
|
|
86
|
+
if (!key) {
|
|
87
|
+
throw new Error(
|
|
88
|
+
"Missing ElevenLabs API key. Set --api-key or ELEVENLABS_API_KEY environment variable."
|
|
89
|
+
);
|
|
90
|
+
}
|
|
91
|
+
return key;
|
|
92
|
+
}
|
|
93
|
+
function getDefaultVoiceId() {
|
|
94
|
+
return process.env.ELEVENLABS_VOICE_ID || process.env.AWAZ_VOICE_ID;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
// src/utils/banner.ts
|
|
98
|
+
import pc from "picocolors";
|
|
99
|
+
var banner = `
|
|
100
|
+
${pc.white("\u2584\u2580\u2588 \u2588\u2591\u2588\u2591\u2588 \u2584\u2580\u2588 \u2580\u2588")}
|
|
101
|
+
${pc.gray("\u2588\u2580\u2588 \u2580\u2584\u2580\u2584\u2580 \u2588\u2580\u2588 \u2588\u2584")}
|
|
102
|
+
${pc.dim("text to speech cli.")}
|
|
103
|
+
`;
|
|
104
|
+
|
|
105
|
+
// src/utils/log.ts
|
|
106
|
+
import pc2 from "picocolors";
|
|
107
|
+
|
|
108
|
+
// src/utils/package.ts
|
|
109
|
+
import fs from "fs";
|
|
110
|
+
import path from "path";
|
|
111
|
+
import { fileURLToPath } from "url";
|
|
112
|
+
var __dirname2 = path.dirname(fileURLToPath(import.meta.url));
|
|
113
|
+
var _packageJson = null;
|
|
114
|
+
function getPackageJson() {
|
|
115
|
+
if (_packageJson) {
|
|
116
|
+
return _packageJson;
|
|
117
|
+
}
|
|
118
|
+
let dir = __dirname2;
|
|
119
|
+
for (let i = 0; i < 5; i++) {
|
|
120
|
+
const pkgPath = path.join(dir, "package.json");
|
|
121
|
+
if (fs.existsSync(pkgPath)) {
|
|
122
|
+
_packageJson = JSON.parse(fs.readFileSync(pkgPath, "utf-8"));
|
|
123
|
+
return _packageJson;
|
|
124
|
+
}
|
|
125
|
+
dir = path.dirname(dir);
|
|
126
|
+
}
|
|
127
|
+
return {
|
|
128
|
+
name: "awaz",
|
|
129
|
+
version: "0.0.1",
|
|
130
|
+
description: "Text to speech. Done right."
|
|
131
|
+
};
|
|
132
|
+
}
|
|
133
|
+
function getVersion() {
|
|
134
|
+
return getPackageJson().version;
|
|
135
|
+
}
|
|
136
|
+
export {
|
|
137
|
+
ElevenLabsClient,
|
|
138
|
+
getApiKey,
|
|
139
|
+
getDefaultVoiceId,
|
|
140
|
+
getPackageJson,
|
|
141
|
+
getVersion
|
|
142
|
+
};
|
|
143
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/lib/elevenlabs.ts","../src/utils/banner.ts","../src/utils/log.ts","../src/utils/package.ts"],"sourcesContent":["/**\n * ElevenLabs API client for text-to-speech\n */\n\nexport interface Voice {\n\tvoice_id: string;\n\tname: string;\n\tcategory: string;\n\tlabels?: Record<string, string>;\n\tpreview_url?: string;\n}\n\nexport interface VoiceSettings {\n\tstability?: number;\n\tsimilarity_boost?: number;\n\tstyle?: number;\n\tuse_speaker_boost?: boolean;\n\tspeed?: number;\n}\n\nexport interface TTSRequest {\n\ttext: string;\n\tmodel_id?: string;\n\tvoice_settings?: VoiceSettings;\n\toutput_format?: string;\n\tseed?: number;\n\tapply_text_normalization?: string;\n\tlanguage_code?: string;\n}\n\ninterface ListVoicesResponse {\n\tvoices: Voice[];\n\tnext_page_token?: string;\n}\n\nexport interface ElevenLabsClientConfig {\n\tapiKey: string;\n\tbaseUrl?: string;\n}\n\nexport class ElevenLabsClient {\n\tprivate readonly baseUrl: string;\n\tprivate readonly apiKey: string;\n\n\tconstructor(config: ElevenLabsClientConfig) {\n\t\tthis.apiKey = config.apiKey;\n\t\tthis.baseUrl = config.baseUrl || 'https://api.elevenlabs.io';\n\t}\n\n\tprivate async request<T>(\n\t\tpath: string,\n\t\toptions: RequestInit = {}\n\t): Promise<T> {\n\t\tconst url = `${this.baseUrl}${path}`;\n\t\tconst headers: Record<string, string> = {\n\t\t\t'xi-api-key': this.apiKey,\n\t\t\tAccept: 'application/json',\n\t\t\t...(options.headers as Record<string, string>)\n\t\t};\n\n\t\tconst response = await fetch(url, {\n\t\t\t...options,\n\t\t\theaders\n\t\t});\n\n\t\tif (!response.ok) {\n\t\t\tconst text = await response.text();\n\t\t\tthrow new Error(`ElevenLabs API error: ${response.status} ${response.statusText}: ${text}`);\n\t\t}\n\n\t\treturn response.json() as Promise<T>;\n\t}\n\n\t/**\n\t * List available voices\n\t */\n\tasync listVoices(search?: string): Promise<Voice[]> {\n\t\tlet path = '/v1/voices';\n\t\tif (search) {\n\t\t\tpath += `?search=${encodeURIComponent(search)}`;\n\t\t}\n\n\t\tconst response = await this.request<ListVoicesResponse>(path);\n\t\treturn response.voices;\n\t}\n\n\t/**\n\t * Stream TTS audio\n\t */\n\tasync streamTTS(\n\t\tvoiceId: string,\n\t\tpayload: TTSRequest,\n\t\tlatencyTier = 0\n\t): Promise<ReadableStream<Uint8Array> | null> {\n\t\tlet path = `/v1/text-to-speech/${voiceId}/stream`;\n\t\tif (latencyTier > 0) {\n\t\t\tpath += `?optimize_streaming_latency=${latencyTier}`;\n\t\t}\n\n\t\tconst response = await fetch(`${this.baseUrl}${path}`, {\n\t\t\tmethod: 'POST',\n\t\t\theaders: {\n\t\t\t\t'Content-Type': 'application/json',\n\t\t\t\tAccept: 'audio/mpeg',\n\t\t\t\t'xi-api-key': this.apiKey\n\t\t\t},\n\t\t\tbody: JSON.stringify(payload)\n\t\t});\n\n\t\tif (!response.ok) {\n\t\t\tconst text = await response.text();\n\t\t\tthrow new Error(`Stream TTS failed: ${response.status}: ${text}`);\n\t\t}\n\n\t\treturn response.body;\n\t}\n\n\t/**\n\t * Convert text to speech (non-streaming)\n\t */\n\tasync convertTTS(voiceId: string, payload: TTSRequest): Promise<Buffer> {\n\t\tconst path = `/v1/text-to-speech/${voiceId}`;\n\n\t\tconst response = await fetch(`${this.baseUrl}${path}`, {\n\t\t\tmethod: 'POST',\n\t\t\theaders: {\n\t\t\t\t'Content-Type': 'application/json',\n\t\t\t\tAccept: 'audio/mpeg',\n\t\t\t\t'xi-api-key': this.apiKey\n\t\t\t},\n\t\t\tbody: JSON.stringify(payload)\n\t\t});\n\n\t\tif (!response.ok) {\n\t\t\tconst text = await response.text();\n\t\t\tthrow new Error(`Convert TTS failed: ${response.status}: ${text}`);\n\t\t}\n\n\t\tconst arrayBuffer = await response.arrayBuffer();\n\t\treturn Buffer.from(arrayBuffer);\n\t}\n}\n\n/**\n * Get API key from environment or throw\n */\nexport function getApiKey(providedKey?: string): string {\n\tconst key =\n\t\tprovidedKey ||\n\t\tprocess.env.ELEVENLABS_API_KEY ||\n\t\tprocess.env.AWAZ_API_KEY;\n\n\tif (!key) {\n\t\tthrow new Error(\n\t\t\t'Missing ElevenLabs API key. Set --api-key or ELEVENLABS_API_KEY environment variable.'\n\t\t);\n\t}\n\n\treturn key;\n}\n\n/**\n * Get default voice ID from environment\n */\nexport function getDefaultVoiceId(): string | undefined {\n\treturn process.env.ELEVENLABS_VOICE_ID || process.env.AWAZ_VOICE_ID;\n}\n","import pc from 'picocolors';\n\nexport const banner = `\n${pc.white('▄▀█ █░█░█ ▄▀█ ▀█')}\n${pc.gray('█▀█ ▀▄▀▄▀ █▀█ █▄')}\n${pc.dim('text to speech cli.')}\n`;\n\nexport function showBanner(): void {\n\tconsole.log(banner);\n}\n","import pc from 'picocolors';\n\nexport function error(msg: string): void {\n\tconsole.error(`${pc.red('✖')} ${msg}`);\n}\n\nexport function warn(msg: string): void {\n\tconsole.error(`${pc.yellow('⚠')} ${msg}`);\n}\n\nexport function info(msg: string): void {\n\tconsole.error(`${pc.blue('ℹ')} ${msg}`);\n}\n\nexport function success(msg: string): void {\n\tconsole.error(`${pc.green('✔')} ${msg}`);\n}\n\nexport function dim(msg: string): string {\n\treturn pc.dim(msg);\n}\n\nexport function bold(msg: string): string {\n\treturn pc.bold(msg);\n}\n\nexport function cyan(msg: string): string {\n\treturn pc.cyan(msg);\n}\n\nexport function yellow(msg: string): string {\n\treturn pc.yellow(msg);\n}\n\nexport function green(msg: string): string {\n\treturn pc.green(msg);\n}\n\nexport function red(msg: string): string {\n\treturn pc.red(msg);\n}\n","import fs from 'node:fs';\nimport path from 'node:path';\nimport { fileURLToPath } from 'node:url';\n\nconst __dirname = path.dirname(fileURLToPath(import.meta.url));\n\ninterface PackageJson {\n\tname: string;\n\tversion: string;\n\tdescription: string;\n}\n\nlet _packageJson: PackageJson | null = null;\n\nexport function getPackageJson(): PackageJson {\n\tif (_packageJson) {\n\t\treturn _packageJson;\n\t}\n\n\t// Try to find package.json, walking up from current file\n\tlet dir = __dirname;\n\tfor (let i = 0; i < 5; i++) {\n\t\tconst pkgPath = path.join(dir, 'package.json');\n\t\tif (fs.existsSync(pkgPath)) {\n\t\t\t_packageJson = JSON.parse(fs.readFileSync(pkgPath, 'utf-8')) as PackageJson;\n\t\t\treturn _packageJson;\n\t\t}\n\t\tdir = path.dirname(dir);\n\t}\n\n\t// Fallback\n\treturn {\n\t\tname: 'awaz',\n\t\tversion: '0.0.1',\n\t\tdescription: 'Text to speech. Done right.'\n\t};\n}\n\nexport function getVersion(): string {\n\treturn getPackageJson().version;\n}\n"],"mappings":"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","names":["path","pc","__dirname"]}
|
package/package.json
ADDED
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "awaz",
|
|
3
|
+
"version": "0.0.1",
|
|
4
|
+
"description": "Text to speech. Done right.",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"bin": {
|
|
7
|
+
"awaz": "./dist/cli.js"
|
|
8
|
+
},
|
|
9
|
+
"main": "./dist/index.js",
|
|
10
|
+
"module": "./dist/index.js",
|
|
11
|
+
"types": "./dist/index.d.ts",
|
|
12
|
+
"exports": {
|
|
13
|
+
".": {
|
|
14
|
+
"types": "./dist/index.d.ts",
|
|
15
|
+
"import": "./dist/index.js"
|
|
16
|
+
}
|
|
17
|
+
},
|
|
18
|
+
"files": [
|
|
19
|
+
"dist"
|
|
20
|
+
],
|
|
21
|
+
"scripts": {
|
|
22
|
+
"build": "tsup",
|
|
23
|
+
"dev": "tsup --watch",
|
|
24
|
+
"start": "node dist/cli.js",
|
|
25
|
+
"lint": "tsc --noEmit",
|
|
26
|
+
"format": "prettier --write \"src/**/*.ts\"",
|
|
27
|
+
"test": "vitest run",
|
|
28
|
+
"test:watch": "vitest",
|
|
29
|
+
"test:coverage": "vitest run --coverage",
|
|
30
|
+
"prepublishOnly": "pnpm build",
|
|
31
|
+
"clean": "rm -rf dist"
|
|
32
|
+
},
|
|
33
|
+
"keywords": [
|
|
34
|
+
"elevenlabs",
|
|
35
|
+
"tts",
|
|
36
|
+
"text-to-speech",
|
|
37
|
+
"speech",
|
|
38
|
+
"say",
|
|
39
|
+
"cli",
|
|
40
|
+
"voice",
|
|
41
|
+
"audio"
|
|
42
|
+
],
|
|
43
|
+
"author": "Ahmad Awais <me@AhmadAwais.com> (https://AhmadAwais.com)",
|
|
44
|
+
"license": "MIT",
|
|
45
|
+
"repository": {
|
|
46
|
+
"type": "git",
|
|
47
|
+
"url": "https://github.com/ahmadawais/awaz"
|
|
48
|
+
},
|
|
49
|
+
"bugs": {
|
|
50
|
+
"url": "https://github.com/ahmadawais/awaz/issues"
|
|
51
|
+
},
|
|
52
|
+
"homepage": "https://github.com/ahmadawais/awaz#readme",
|
|
53
|
+
"engines": {
|
|
54
|
+
"node": ">=18.0.0"
|
|
55
|
+
},
|
|
56
|
+
"dependencies": {
|
|
57
|
+
"commander": "^12.1.0",
|
|
58
|
+
"dotenv": "^17.2.3",
|
|
59
|
+
"ora": "^8.1.1",
|
|
60
|
+
"picocolors": "^1.1.1"
|
|
61
|
+
},
|
|
62
|
+
"devDependencies": {
|
|
63
|
+
"@types/node": "^22.10.7",
|
|
64
|
+
"tsup": "^8.3.5",
|
|
65
|
+
"typescript": "^5.7.3",
|
|
66
|
+
"vitest": "^2.1.8"
|
|
67
|
+
}
|
|
68
|
+
}
|