voicekit-client 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +128 -0
- package/index.d.ts +150 -0
- package/package.json +39 -0
- package/src/index.js +645 -0
package/README.md
ADDED
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
# VoiceKit — TypeScript SDK
|
|
2
|
+
|
|
3
|
+
Official TypeScript/JavaScript wrapper for [VoiceKit](https://ttsapi.ru):
|
|
4
|
+
neural speech synthesis, transcription, sentiment analysis, and batch operations.
|
|
5
|
+
|
|
6
|
+
Zero runtime dependencies. Requires Node.js 18+ (global `fetch`).
|
|
7
|
+
|
|
8
|
+
## Install
|
|
9
|
+
|
|
10
|
+
```bash
|
|
11
|
+
npm install voicekit-client
|
|
12
|
+
```
|
|
13
|
+
|
|
14
|
+
## Quick start
|
|
15
|
+
|
|
16
|
+
```js
|
|
17
|
+
import { VoiceKitClient, b64 } from "voicekit-client";
|
|
18
|
+
import { writeFile } from "node:fs/promises";
|
|
19
|
+
|
|
20
|
+
const client = new VoiceKitClient({ apiKey: "YOUR_KEY" });
|
|
21
|
+
|
|
22
|
+
// Synthesis → raw audio bytes
|
|
23
|
+
const audio = await client.synthesize("Привет! Это синтез русской речи.", {
|
|
24
|
+
voice: "preset_anna",
|
|
25
|
+
format: "mp3",
|
|
26
|
+
});
|
|
27
|
+
await writeFile("speech.mp3", audio);
|
|
28
|
+
|
|
29
|
+
// Streaming (Pro/Business)
|
|
30
|
+
for await (const chunk of client.synthesizeStream("Первое предложение. Второе.")) {
|
|
31
|
+
// write chunks to a file or socket
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
// Transcription (async → poll)
|
|
35
|
+
const job = await client.transcribe("audio.wav", { keyterms: ["диагноз"] });
|
|
36
|
+
let result = await client.getTranscriptionJob(job.job_id);
|
|
37
|
+
while (!["completed", "failed"].includes(result.status)) {
|
|
38
|
+
await new Promise(r => setTimeout(r, 1000));
|
|
39
|
+
result = await client.getTranscriptionJob(job.job_id);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
// Short-file sync transcription
|
|
43
|
+
const transcript = await client.transcribeSync("audio.wav");
|
|
44
|
+
|
|
45
|
+
// Analysis (sentiment + keywords + entities)
|
|
46
|
+
const analysis = await client.analyzeSync("audio.wav");
|
|
47
|
+
|
|
48
|
+
// Text intelligence
|
|
49
|
+
const moderation = await client.moderate("Это оскорбительное сообщение.");
|
|
50
|
+
|
|
51
|
+
// Batches
|
|
52
|
+
const batch = await client.batchSynthesize([
|
|
53
|
+
{ text: "Первый текст", voice: "preset_anna" },
|
|
54
|
+
{ text: "Второй текст", voice: "dmitri" },
|
|
55
|
+
]);
|
|
56
|
+
const status = await client.getBatch(batch.batch_id);
|
|
57
|
+
|
|
58
|
+
const analysisBatch = await client.batchAnalyze([
|
|
59
|
+
{ audio: await b64("a.wav"), language: "ru" },
|
|
60
|
+
{ audio: await b64("b.wav"), language: "ru" },
|
|
61
|
+
]);
|
|
62
|
+
|
|
63
|
+
// Voice cloning (Pro/Business)
|
|
64
|
+
const clone = await client.createCloneVoice({
|
|
65
|
+
name: "My voice",
|
|
66
|
+
promptText: "Точный текст образца.",
|
|
67
|
+
samples: "reference.wav",
|
|
68
|
+
});
|
|
69
|
+
console.log(await client.listCloneVoices());
|
|
70
|
+
await client.deleteCloneVoice(clone.id);
|
|
71
|
+
|
|
72
|
+
// VAD (speech segments)
|
|
73
|
+
const segments = await client.vad("audio.wav");
|
|
74
|
+
|
|
75
|
+
// Account
|
|
76
|
+
const usage = await client.usage();
|
|
77
|
+
const balance = await client.billingBalance();
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
### Audio effects (Pro/Business)
|
|
81
|
+
|
|
82
|
+
```js
|
|
83
|
+
// Inline during synthesis — the chain is applied to the synthesized audio
|
|
84
|
+
const fxAudio = await client.synthesize("Привет!", {
|
|
85
|
+
effects: '[{"type":"reverb","room_size":0.5},{"type":"pitch","semitones":2}]',
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
// Async processing of an existing file
|
|
89
|
+
const fxJob = await client.applyAudioEffects("voice.mp3", [{ type: "compressor", ratio: 3 }]);
|
|
90
|
+
let fxResult = await client.getAudioEffectsJob(fxJob.job_id);
|
|
91
|
+
while (!["completed", "failed"].includes(fxResult.status)) {
|
|
92
|
+
await new Promise(r => setTimeout(r, 1000));
|
|
93
|
+
fxResult = await client.getAudioEffectsJob(fxJob.job_id);
|
|
94
|
+
}
|
|
95
|
+
const fxFile = await client.downloadAudioEffects(fxJob.job_id);
|
|
96
|
+
await writeFile("voice_fx.mp3", fxFile);
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
### WebSocket streaming (Pro/Business)
|
|
100
|
+
|
|
101
|
+
```js
|
|
102
|
+
const stream = await client.transcribeStream({ language: "ru", keyterms: ["диагноз"] });
|
|
103
|
+
await stream.sendAudio(pcm16Chunk1); // raw PCM16, 16 kHz mono (Uint8Array)
|
|
104
|
+
await stream.sendAudio(pcm16Chunk2);
|
|
105
|
+
await stream.stop(); // finalize the utterance
|
|
106
|
+
for await (const event of stream.events()) {
|
|
107
|
+
console.log(event.type, event); // session / vad / partial / final / error
|
|
108
|
+
}
|
|
109
|
+
stream.close();
|
|
110
|
+
|
|
111
|
+
const vad = await client.vadStream(); // VAD events only (speech_started/ended)
|
|
112
|
+
await vad.sendAudio(pcm16Chunk);
|
|
113
|
+
await vad.stop();
|
|
114
|
+
for await (const event of vad.events()) {
|
|
115
|
+
console.log(event.type, event);
|
|
116
|
+
}
|
|
117
|
+
vad.close();
|
|
118
|
+
```
|
|
119
|
+
|
|
120
|
+
## Configuration
|
|
121
|
+
|
|
122
|
+
| Option | Default | Description |
|
|
123
|
+
| --- | --- | --- |
|
|
124
|
+
| `apiKey` | — | API key (required) |
|
|
125
|
+
| `baseUrl` | `https://ttsapi.ru` | API base URL (e.g. `http://localhost:5080` for local dev) |
|
|
126
|
+
| `timeoutMs` | `120000` | Per-request timeout in milliseconds |
|
|
127
|
+
|
|
128
|
+
Errors reject with `VoiceKitError` (`.status`, `.code`, `.message`).
|
package/index.d.ts
ADDED
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
export type AudioSource = Uint8Array | string;
|
|
2
|
+
|
|
3
|
+
export interface VoiceKitClientOptions {
|
|
4
|
+
apiKey: string;
|
|
5
|
+
baseUrl?: string;
|
|
6
|
+
timeoutMs?: number;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export class VoiceKitError extends Error {
|
|
10
|
+
readonly status: number;
|
|
11
|
+
readonly code: string;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export class VoiceKitClient {
|
|
15
|
+
constructor(options: VoiceKitClientOptions);
|
|
16
|
+
|
|
17
|
+
synthesize(text: string, opts?: SynthesisOptions): Promise<Uint8Array>;
|
|
18
|
+
synthesizeStream(text: string, opts?: SynthesisOptions): AsyncGenerator<Uint8Array>;
|
|
19
|
+
synthesizeAsync(
|
|
20
|
+
text: string,
|
|
21
|
+
opts?: SynthesisAsyncOptions,
|
|
22
|
+
): Promise<Record<string, unknown>>;
|
|
23
|
+
getSynthesisJob(jobId: string): Promise<Record<string, unknown>>;
|
|
24
|
+
downloadSynthesisAudio(jobId: string): Promise<Uint8Array>;
|
|
25
|
+
voices(): Promise<Array<Record<string, unknown>>>;
|
|
26
|
+
voice(id: string): Promise<Record<string, unknown>>;
|
|
27
|
+
|
|
28
|
+
createCloneVoice(input: CloneVoiceCreateInput): Promise<Record<string, unknown>>;
|
|
29
|
+
listCloneVoices(): Promise<Array<Record<string, unknown>>>;
|
|
30
|
+
getCloneVoice(cloneId: string): Promise<Record<string, unknown>>;
|
|
31
|
+
deleteCloneVoice(cloneId: string): Promise<void>;
|
|
32
|
+
|
|
33
|
+
transcribe(audio: AudioSource, opts?: AudioOptions): Promise<Record<string, unknown>>;
|
|
34
|
+
transcribeSync(audio: AudioSource, opts?: AudioOptions): Promise<Record<string, unknown>>;
|
|
35
|
+
getTranscriptionJob(jobId: string): Promise<Record<string, unknown>>;
|
|
36
|
+
subtitles(jobId: string, format?: "vtt" | "srt"): Promise<string>;
|
|
37
|
+
vad(audio: AudioSource): Promise<Record<string, unknown>>;
|
|
38
|
+
|
|
39
|
+
analyze(audio: AudioSource, opts?: AudioOptions): Promise<Record<string, unknown>>;
|
|
40
|
+
analyzeSync(audio: AudioSource, opts?: AudioOptions): Promise<Record<string, unknown>>;
|
|
41
|
+
getAnalysisJob(jobId: string): Promise<Record<string, unknown>>;
|
|
42
|
+
|
|
43
|
+
detectLanguage(text: string): Promise<Record<string, unknown>>;
|
|
44
|
+
redact(text: string, language?: string): Promise<Record<string, unknown>>;
|
|
45
|
+
topics(text: string, language?: string): Promise<Record<string, unknown>>;
|
|
46
|
+
summarize(
|
|
47
|
+
text: string,
|
|
48
|
+
language?: string,
|
|
49
|
+
maxSentences?: number,
|
|
50
|
+
): Promise<Record<string, unknown>>;
|
|
51
|
+
|
|
52
|
+
moderate(text: string, language?: string): Promise<Record<string, unknown>>;
|
|
53
|
+
|
|
54
|
+
applyAudioEffects(
|
|
55
|
+
audio: AudioSource,
|
|
56
|
+
effects: Array<Record<string, unknown>>,
|
|
57
|
+
opts?: AudioEffectsOptions,
|
|
58
|
+
): Promise<Record<string, unknown>>;
|
|
59
|
+
getAudioEffectsJob(jobId: string): Promise<Record<string, unknown>>;
|
|
60
|
+
downloadAudioEffects(jobId: string): Promise<Uint8Array>;
|
|
61
|
+
|
|
62
|
+
applyVideoEffects(
|
|
63
|
+
video: AudioSource,
|
|
64
|
+
effects: Array<Record<string, unknown>>,
|
|
65
|
+
opts?: VideoEffectsOptions,
|
|
66
|
+
): Promise<Record<string, unknown>>;
|
|
67
|
+
getVideoEffectsJob(jobId: string): Promise<Record<string, unknown>>;
|
|
68
|
+
downloadVideoEffects(jobId: string): Promise<Uint8Array>;
|
|
69
|
+
|
|
70
|
+
batchSynthesize(items: Array<Record<string, unknown>>): Promise<Record<string, unknown>>;
|
|
71
|
+
batchAnalyze(items: Array<Record<string, unknown>>): Promise<Record<string, unknown>>;
|
|
72
|
+
getBatch(batchId: string): Promise<Record<string, unknown>>;
|
|
73
|
+
|
|
74
|
+
usage(): Promise<Record<string, unknown>>;
|
|
75
|
+
billingBalance(): Promise<Record<string, unknown>>;
|
|
76
|
+
|
|
77
|
+
transcribeStream(opts?: TranscribeStreamOptions): Promise<WsStream>;
|
|
78
|
+
vadStream(): Promise<WsStream>;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
export interface SynthesisOptions {
|
|
82
|
+
voice?: string;
|
|
83
|
+
format?: "mp3" | "wav" | "ogg";
|
|
84
|
+
sampleRate?: number;
|
|
85
|
+
speed?: number;
|
|
86
|
+
pitch?: number;
|
|
87
|
+
emotion?: string;
|
|
88
|
+
ssml?: boolean;
|
|
89
|
+
putAccent?: boolean;
|
|
90
|
+
putYo?: boolean;
|
|
91
|
+
normalize?: boolean;
|
|
92
|
+
model?: string;
|
|
93
|
+
language?: string;
|
|
94
|
+
effects?: string;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
export interface AudioEffectsOptions {
|
|
98
|
+
outputFormat?: "wav" | "mp3" | "ogg";
|
|
99
|
+
webhookUrl?: string;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
export interface VideoEffectsOptions {
|
|
103
|
+
mode?: "mux" | "audio";
|
|
104
|
+
audio?: AudioSource;
|
|
105
|
+
outputFormat?: string;
|
|
106
|
+
webhookUrl?: string;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
export interface SynthesisAsyncOptions {
|
|
110
|
+
voice?: string;
|
|
111
|
+
format?: "wav";
|
|
112
|
+
sampleRate?: number;
|
|
113
|
+
speed?: number;
|
|
114
|
+
model?: string;
|
|
115
|
+
language?: string;
|
|
116
|
+
webhookUrl?: string;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
export interface AudioOptions {
|
|
120
|
+
language?: string;
|
|
121
|
+
diarization?: boolean;
|
|
122
|
+
webhookUrl?: string;
|
|
123
|
+
keyterms?: string[];
|
|
124
|
+
emotions?: boolean;
|
|
125
|
+
keywords?: boolean;
|
|
126
|
+
entities?: boolean;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
export interface CloneVoiceCreateInput {
|
|
130
|
+
name: string;
|
|
131
|
+
promptText: string;
|
|
132
|
+
samples: AudioSource | AudioSource[];
|
|
133
|
+
language?: string;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
export interface TranscribeStreamOptions {
|
|
137
|
+
language?: string;
|
|
138
|
+
keyterms?: string[];
|
|
139
|
+
interim?: boolean;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
export class WsStream {
|
|
143
|
+
sendAudio(pcm16: Uint8Array): Promise<void>;
|
|
144
|
+
sendText(payload: unknown): Promise<void>;
|
|
145
|
+
stop(): Promise<void>;
|
|
146
|
+
events(): AsyncGenerator<Record<string, unknown>>;
|
|
147
|
+
close(): void;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
export function b64(audio: AudioSource): Promise<string>;
|
package/package.json
ADDED
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "voicekit-client",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Official TypeScript/JavaScript SDK for VoiceKit (synthesis, transcription, analysis, moderation, batches).",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "./src/index.js",
|
|
7
|
+
"types": "./index.d.ts",
|
|
8
|
+
"exports": {
|
|
9
|
+
".": {
|
|
10
|
+
"types": "./index.d.ts",
|
|
11
|
+
"import": "./src/index.js"
|
|
12
|
+
}
|
|
13
|
+
},
|
|
14
|
+
"files": [
|
|
15
|
+
"src",
|
|
16
|
+
"index.d.ts",
|
|
17
|
+
"README.md"
|
|
18
|
+
],
|
|
19
|
+
"engines": {
|
|
20
|
+
"node": ">=18.0.0"
|
|
21
|
+
},
|
|
22
|
+
"dependencies": {
|
|
23
|
+
"ws": "^8.18.0"
|
|
24
|
+
},
|
|
25
|
+
"keywords": [
|
|
26
|
+
"tts",
|
|
27
|
+
"speech",
|
|
28
|
+
"synthesis",
|
|
29
|
+
"transcription",
|
|
30
|
+
"stt",
|
|
31
|
+
"voice",
|
|
32
|
+
"russian"
|
|
33
|
+
],
|
|
34
|
+
"license": "UNLICENSED",
|
|
35
|
+
"repository": {
|
|
36
|
+
"type": "git",
|
|
37
|
+
"url": "https://ttsapi.ru"
|
|
38
|
+
}
|
|
39
|
+
}
|
package/src/index.js
ADDED
|
@@ -0,0 +1,645 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Official JavaScript/TypeScript SDK for VoiceKit.
|
|
3
|
+
*
|
|
4
|
+
* Requires Node.js 18+ (global `fetch`, web streams). Zero runtime dependencies.
|
|
5
|
+
*
|
|
6
|
+
* @example
|
|
7
|
+
* import { VoiceKitClient } from "voicekit-client";
|
|
8
|
+
*
|
|
9
|
+
* const client = new VoiceKitClient({ apiKey: "YOUR_KEY" });
|
|
10
|
+
* const audio = await client.synthesize("Привет!", { voice: "preset_anna", format: "mp3" });
|
|
11
|
+
* await import("node:fs/promises").then(fs => fs.writeFile("speech.mp3", audio));
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
import { readFile } from "node:fs/promises";
|
|
15
|
+
import { basename } from "node:path";
|
|
16
|
+
|
|
17
|
+
const DEFAULT_BASE_URL = "https://ttsapi.ru";
|
|
18
|
+
|
|
19
|
+
export class VoiceKitError extends Error {
|
|
20
|
+
/**
|
|
21
|
+
* @param {number} status HTTP status code.
|
|
22
|
+
* @param {string} message Human-readable message.
|
|
23
|
+
* @param {string} [code] Machine-readable error code.
|
|
24
|
+
*/
|
|
25
|
+
constructor(status, message, code = "") {
|
|
26
|
+
super(message);
|
|
27
|
+
this.name = "VoiceKitError";
|
|
28
|
+
this.status = status;
|
|
29
|
+
this.code = code;
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Audio input: raw bytes or a path to a local file.
|
|
35
|
+
* @typedef {Uint8Array | string} AudioSource
|
|
36
|
+
*/
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Client options.
|
|
40
|
+
* @typedef {object} VoiceKitClientOptions
|
|
41
|
+
* @property {string} apiKey API key (required).
|
|
42
|
+
* @property {string} [baseUrl] API base URL. Defaults to `https://ttsapi.ru`.
|
|
43
|
+
* @property {number} [timeoutMs] Per-request timeout in milliseconds. Defaults to 120000.
|
|
44
|
+
*/
|
|
45
|
+
|
|
46
|
+
export class VoiceKitClient {
|
|
47
|
+
/**
|
|
48
|
+
* @param {VoiceKitClientOptions} options
|
|
49
|
+
*/
|
|
50
|
+
constructor({ apiKey, baseUrl = DEFAULT_BASE_URL, timeoutMs = 120_000 }) {
|
|
51
|
+
if (!apiKey) throw new Error("apiKey is required");
|
|
52
|
+
/** @private */ this._baseUrl = baseUrl.replace(/\/+$/, "");
|
|
53
|
+
/** @private */ this._headers = { "X-Api-Key": apiKey };
|
|
54
|
+
/** @private */ this._timeoutMs = timeoutMs;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
// ──────────────────────── Synthesis ────────────────────────────────
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Synthesize speech and return raw audio bytes.
|
|
61
|
+
* @param {string} text
|
|
62
|
+
* @param {object} [opts]
|
|
63
|
+
* @returns {Promise<Uint8Array>}
|
|
64
|
+
*/
|
|
65
|
+
async synthesize(text, opts = {}) {
|
|
66
|
+
const body = compact({ text, ...opts });
|
|
67
|
+
const response = await this.#request("/v1/synthesize", { method: "POST", json: body });
|
|
68
|
+
return new Uint8Array(await response.arrayBuffer());
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* Synthesize and yield audio chunks as they are produced (Pro/Business).
|
|
73
|
+
* @param {string} text
|
|
74
|
+
* @param {object} [opts]
|
|
75
|
+
* @returns {AsyncGenerator<Uint8Array>}
|
|
76
|
+
*/
|
|
77
|
+
async *synthesizeStream(text, opts = {}) {
|
|
78
|
+
const body = compact({ text, ...opts });
|
|
79
|
+
const response = await this.#request("/v1/synthesize/stream", { method: "POST", json: body });
|
|
80
|
+
const reader = response.body?.getReader();
|
|
81
|
+
if (!reader) return;
|
|
82
|
+
while (true) {
|
|
83
|
+
const { done, value } = await reader.read();
|
|
84
|
+
if (done) break;
|
|
85
|
+
if (value?.length) yield value;
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* Queue a long-form (audiobook) synthesis job; poll with `getSynthesisJob()`.
|
|
91
|
+
* Long-form synthesis returns WAV only.
|
|
92
|
+
* @param {string} text
|
|
93
|
+
* @param {object} [opts]
|
|
94
|
+
* @returns {Promise<Record<string, unknown>>}
|
|
95
|
+
*/
|
|
96
|
+
async synthesizeAsync(text, opts = {}) {
|
|
97
|
+
const { webhookUrl, ...body } = opts;
|
|
98
|
+
const query = webhookUrl ? `?webhookUrl=${encodeURIComponent(webhookUrl)}` : "";
|
|
99
|
+
const response = await this.#request(`/v1/synthesize/async${query}`, {
|
|
100
|
+
method: "POST",
|
|
101
|
+
json: compact({ text, ...body }),
|
|
102
|
+
});
|
|
103
|
+
return response.json();
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* Poll a long-form synthesis job; returns the audiobook manifest when done.
|
|
108
|
+
* @param {string} jobId
|
|
109
|
+
* @returns {Promise<Record<string, unknown>>}
|
|
110
|
+
*/
|
|
111
|
+
getSynthesisJob(jobId) {
|
|
112
|
+
return this.#getJson(`/v1/synthesize/async/${jobId}`);
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/**
|
|
116
|
+
* Download the produced WAV for a completed long-form synthesis job.
|
|
117
|
+
* @param {string} jobId
|
|
118
|
+
* @returns {Promise<Uint8Array>}
|
|
119
|
+
*/
|
|
120
|
+
async downloadSynthesisAudio(jobId) {
|
|
121
|
+
const response = await this.#request(`/v1/synthesize/async/${jobId}/audio`);
|
|
122
|
+
return new Uint8Array(await response.arrayBuffer());
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/** @returns {Promise<Array<Record<string, unknown>>>} */
|
|
126
|
+
voices() {
|
|
127
|
+
return this.#getJson("/v1/voices");
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/**
|
|
131
|
+
* @param {string} id Voice id, e.g. `preset_anna`.
|
|
132
|
+
* @returns {Promise<Record<string, unknown>>}
|
|
133
|
+
*/
|
|
134
|
+
voice(id) {
|
|
135
|
+
return this.#getJson(`/v1/voices/${id}`);
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
// ──────────────────────── Voice cloning ────────────────────────────
|
|
139
|
+
|
|
140
|
+
/**
|
|
141
|
+
* Create a cloned voice from reference audio (Pro/Business).
|
|
142
|
+
* @param {{ name: string, promptText: string, samples: AudioSource | AudioSource[], language?: string }} input
|
|
143
|
+
* @returns {Promise<Record<string, unknown>>}
|
|
144
|
+
*/
|
|
145
|
+
async createCloneVoice({ name, promptText, samples, language }) {
|
|
146
|
+
const sampleList = Array.isArray(samples) ? samples : [samples];
|
|
147
|
+
const form = new FormData();
|
|
148
|
+
form.append("name", name);
|
|
149
|
+
form.append("prompt_text", promptText);
|
|
150
|
+
if (language) form.append("language", language);
|
|
151
|
+
for (const sample of sampleList) {
|
|
152
|
+
const { data, filename } = await audioToParts(sample);
|
|
153
|
+
form.append("samples", new Blob([data], { type: "audio/wav" }), filename);
|
|
154
|
+
}
|
|
155
|
+
const response = await this.#request("/v1/voices/clone", { method: "POST", form });
|
|
156
|
+
return response.json();
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
/** @returns {Promise<Array<Record<string, unknown>>>} */
|
|
160
|
+
listCloneVoices() {
|
|
161
|
+
return this.#getJson("/v1/voices/clone");
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
/**
|
|
165
|
+
* @param {string} cloneId
|
|
166
|
+
* @returns {Promise<Record<string, unknown>>}
|
|
167
|
+
*/
|
|
168
|
+
getCloneVoice(cloneId) {
|
|
169
|
+
return this.#getJson(`/v1/voices/clone/${cloneId}`);
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
/**
|
|
173
|
+
* @param {string} cloneId
|
|
174
|
+
* @returns {Promise<void>}
|
|
175
|
+
*/
|
|
176
|
+
async deleteCloneVoice(cloneId) {
|
|
177
|
+
await this.#request(`/v1/voices/clone/${cloneId}`, { method: "DELETE" });
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
// ──────────────────────── Transcription ────────────────────────────
|
|
181
|
+
|
|
182
|
+
/**
|
|
183
|
+
* Start an async transcription job; poll with `getTranscriptionJob()`.
|
|
184
|
+
* @param {AudioSource} audio
|
|
185
|
+
* @param {object} [opts]
|
|
186
|
+
* @returns {Promise<Record<string, unknown>>}
|
|
187
|
+
*/
|
|
188
|
+
async transcribe(audio, opts = {}) {
|
|
189
|
+
return this.#postForm("/v1/transcribe", audio, opts);
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
/**
|
|
193
|
+
* Transcribe a short file (≤ 3 min) synchronously.
|
|
194
|
+
* @param {AudioSource} audio
|
|
195
|
+
* @param {object} [opts]
|
|
196
|
+
* @returns {Promise<Record<string, unknown>>}
|
|
197
|
+
*/
|
|
198
|
+
async transcribeSync(audio, opts = {}) {
|
|
199
|
+
return this.#postForm("/v1/transcribe/sync", audio, opts);
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
/**
|
|
203
|
+
* @param {string} jobId
|
|
204
|
+
* @returns {Promise<Record<string, unknown>>}
|
|
205
|
+
*/
|
|
206
|
+
getTranscriptionJob(jobId) {
|
|
207
|
+
return this.#getJson(`/v1/transcribe/${jobId}`);
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
/**
|
|
211
|
+
* Download VTT/SRT subtitles for a completed transcription job.
|
|
212
|
+
* @param {string} jobId
|
|
213
|
+
* @param {"vtt" | "srt"} [format]
|
|
214
|
+
* @returns {Promise<string>}
|
|
215
|
+
*/
|
|
216
|
+
async subtitles(jobId, format = "vtt") {
|
|
217
|
+
const response = await this.#request(`/v1/transcribe/${jobId}/subtitles?format=${format}`);
|
|
218
|
+
return response.text();
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
/**
|
|
222
|
+
* Detect speech segments in an audio file (Silero VAD).
|
|
223
|
+
* @param {AudioSource} audio
|
|
224
|
+
* @returns {Promise<Record<string, unknown>>}
|
|
225
|
+
*/
|
|
226
|
+
vad(audio) {
|
|
227
|
+
return this.#postForm("/v1/vad", audio, {});
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
// ──────────────────────── Analysis ─────────────────────────────────
|
|
231
|
+
|
|
232
|
+
/**
|
|
233
|
+
* Start an async analysis job; poll with `getAnalysisJob()`.
|
|
234
|
+
* @param {AudioSource} audio
|
|
235
|
+
* @param {object} [opts]
|
|
236
|
+
* @returns {Promise<Record<string, unknown>>}
|
|
237
|
+
*/
|
|
238
|
+
async analyze(audio, opts = {}) {
|
|
239
|
+
return this.#postForm("/v1/analyze", audio, opts);
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
/**
|
|
243
|
+
* Analyze a short file (≤ 3 min) synchronously.
|
|
244
|
+
* @param {AudioSource} audio
|
|
245
|
+
* @param {object} [opts]
|
|
246
|
+
* @returns {Promise<Record<string, unknown>>}
|
|
247
|
+
*/
|
|
248
|
+
async analyzeSync(audio, opts = {}) {
|
|
249
|
+
return this.#postForm("/v1/analyze/sync", audio, opts);
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
/**
|
|
253
|
+
* @param {string} jobId
|
|
254
|
+
* @returns {Promise<Record<string, unknown>>}
|
|
255
|
+
*/
|
|
256
|
+
getAnalysisJob(jobId) {
|
|
257
|
+
return this.#getJson(`/v1/analyze/${jobId}`);
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
// ──────────────────────── Text intelligence ────────────────────────
|
|
261
|
+
|
|
262
|
+
/** @param {string} text */
|
|
263
|
+
detectLanguage(text) {
|
|
264
|
+
return this.#postJson("/v1/detect-language", { text });
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
/** @param {string} text */
|
|
268
|
+
redact(text, language) {
|
|
269
|
+
return this.#postJson("/v1/redact", compact({ text, language }));
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
/** @param {string} text */
|
|
273
|
+
topics(text, language) {
|
|
274
|
+
return this.#postJson("/v1/analyze/topics", compact({ text, language }));
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
/** @param {string} text */
|
|
278
|
+
summarize(text, language, maxSentences) {
|
|
279
|
+
return this.#postJson(
|
|
280
|
+
"/v1/analyze/summarize",
|
|
281
|
+
compact({ text, language, max_sentences: maxSentences }),
|
|
282
|
+
);
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
/**
|
|
286
|
+
* Flag profanity, insults and hate speech in raw text.
|
|
287
|
+
* @param {string} text
|
|
288
|
+
* @param {string} [language]
|
|
289
|
+
* @returns {Promise<Record<string, unknown>>}
|
|
290
|
+
*/
|
|
291
|
+
moderate(text, language) {
|
|
292
|
+
return this.#postJson("/v1/moderate", compact({ text, language }));
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
// ──────────────────────── Audio / video effects ───────────────────
|
|
296
|
+
|
|
297
|
+
/**
|
|
298
|
+
* Start an async audio-effects job; poll with `getAudioEffectsJob()`.
|
|
299
|
+
* @param {AudioSource} audio
|
|
300
|
+
* @param {Array<Record<string, unknown>>} effects Effect descriptors.
|
|
301
|
+
* @param {{ outputFormat?: string, webhookUrl?: string }} [opts]
|
|
302
|
+
* @returns {Promise<Record<string, unknown>>}
|
|
303
|
+
*/
|
|
304
|
+
async applyAudioEffects(audio, effects, opts = {}) {
|
|
305
|
+
const { outputFormat = "wav", webhookUrl } = opts;
|
|
306
|
+
const { data, filename } = await audioToParts(audio);
|
|
307
|
+
const form = new FormData();
|
|
308
|
+
form.append("audio", new Blob([data], { type: "audio/wav" }), filename);
|
|
309
|
+
form.append("effects", JSON.stringify(effects));
|
|
310
|
+
form.append("output_format", outputFormat);
|
|
311
|
+
const query = webhookUrl ? `?webhookUrl=${encodeURIComponent(webhookUrl)}` : "";
|
|
312
|
+
const response = await this.#request(`/v1/audio/effects${query}`, { method: "POST", form });
|
|
313
|
+
return response.json();
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
/**
|
|
317
|
+
* Poll an audio-effects job; returns the manifest when completed.
|
|
318
|
+
* @param {string} jobId
|
|
319
|
+
* @returns {Promise<Record<string, unknown>>}
|
|
320
|
+
*/
|
|
321
|
+
getAudioEffectsJob(jobId) {
|
|
322
|
+
return this.#getJson(`/v1/audio/effects/${jobId}`);
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
/**
|
|
326
|
+
* Download the produced audio for a completed audio-effects job.
|
|
327
|
+
* @param {string} jobId
|
|
328
|
+
* @returns {Promise<Uint8Array>}
|
|
329
|
+
*/
|
|
330
|
+
async downloadAudioEffects(jobId) {
|
|
331
|
+
const response = await this.#request(`/v1/audio/effects/${jobId}/audio`);
|
|
332
|
+
return new Uint8Array(await response.arrayBuffer());
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
/**
|
|
336
|
+
* Start an async video-effects job; poll with `getVideoEffectsJob()`.
|
|
337
|
+
* @param {AudioSource} video
|
|
338
|
+
* @param {Array<Record<string, unknown>>} effects Effect descriptors.
|
|
339
|
+
* @param {{ mode?: string, audio?: AudioSource, outputFormat?: string, webhookUrl?: string }} [opts]
|
|
340
|
+
* @returns {Promise<Record<string, unknown>>}
|
|
341
|
+
*/
|
|
342
|
+
async applyVideoEffects(video, effects, opts = {}) {
|
|
343
|
+
const { mode = "mux", audio, outputFormat, webhookUrl } = opts;
|
|
344
|
+
const { data, filename } = await audioToParts(video);
|
|
345
|
+
const form = new FormData();
|
|
346
|
+
form.append("video", new Blob([data], { type: "video/mp4" }), filename);
|
|
347
|
+
if (audio) {
|
|
348
|
+
const parts = await audioToParts(audio);
|
|
349
|
+
form.append("audio", new Blob([parts.data], { type: "audio/wav" }), parts.filename);
|
|
350
|
+
}
|
|
351
|
+
form.append("effects", JSON.stringify(effects));
|
|
352
|
+
form.append("mode", mode);
|
|
353
|
+
if (outputFormat) form.append("output_format", outputFormat);
|
|
354
|
+
const query = webhookUrl ? `?webhookUrl=${encodeURIComponent(webhookUrl)}` : "";
|
|
355
|
+
const response = await this.#request(`/v1/video/effects${query}`, { method: "POST", form });
|
|
356
|
+
return response.json();
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
/**
|
|
360
|
+
* Poll a video-effects job; returns the manifest when completed.
|
|
361
|
+
* @param {string} jobId
|
|
362
|
+
* @returns {Promise<Record<string, unknown>>}
|
|
363
|
+
*/
|
|
364
|
+
getVideoEffectsJob(jobId) {
|
|
365
|
+
return this.#getJson(`/v1/video/effects/${jobId}`);
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
/**
|
|
369
|
+
* Download the produced artifact (video or audio) for a video-effects job.
|
|
370
|
+
* @param {string} jobId
|
|
371
|
+
* @returns {Promise<Uint8Array>}
|
|
372
|
+
*/
|
|
373
|
+
async downloadVideoEffects(jobId) {
|
|
374
|
+
const response = await this.#request(`/v1/video/effects/${jobId}/file`);
|
|
375
|
+
return new Uint8Array(await response.arrayBuffer());
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
// ──────────────────────── Batch ────────────────────────────────────
|
|
379
|
+
|
|
380
|
+
/**
|
|
381
|
+
* Queue a batch of synthesis requests; poll with `getBatch()`.
|
|
382
|
+
* @param {Array<Record<string, unknown>>} items
|
|
383
|
+
* @returns {Promise<Record<string, unknown>>}
|
|
384
|
+
*/
|
|
385
|
+
batchSynthesize(items) {
|
|
386
|
+
return this.#postJson("/v1/batch/synthesize", { items });
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
/**
|
|
390
|
+
* Queue a batch of analysis requests; each item needs inline base64 `audio`.
|
|
391
|
+
* @param {Array<Record<string, unknown>>} items
|
|
392
|
+
* @returns {Promise<Record<string, unknown>>}
|
|
393
|
+
*/
|
|
394
|
+
batchAnalyze(items) {
|
|
395
|
+
return this.#postJson("/v1/batch/analyze", { items });
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
/**
|
|
399
|
+
* @param {string} batchId
|
|
400
|
+
* @returns {Promise<Record<string, unknown>>}
|
|
401
|
+
*/
|
|
402
|
+
getBatch(batchId) {
|
|
403
|
+
return this.#getJson(`/v1/batch/${batchId}`);
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
// ──────────────────────── Account ──────────────────────────────────
|
|
407
|
+
|
|
408
|
+
/** @returns {Promise<Record<string, unknown>>} */
|
|
409
|
+
usage() {
|
|
410
|
+
return this.#getJson("/v1/usage");
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
/** @returns {Promise<Record<string, unknown>>} */
|
|
414
|
+
billingBalance() {
|
|
415
|
+
return this.#getJson("/v1/billing/balance");
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
// ──────────────────────── Streaming (WebSocket) ────────────────────
|
|
419
|
+
|
|
420
|
+
/**
|
|
421
|
+
* Open a streaming transcription session (Pro/Business).
|
|
422
|
+
* @param {{ language?: string, keyterms?: string[], interim?: boolean }} [opts]
|
|
423
|
+
* @returns {Promise<WsStream>}
|
|
424
|
+
*/
|
|
425
|
+
async transcribeStream({ language, keyterms, interim = true } = {}) {
|
|
426
|
+
const url = this.#wsUrl("/v1/transcribe/stream", {
|
|
427
|
+
language,
|
|
428
|
+
keyterms: Array.isArray(keyterms) ? keyterms.join(",") : undefined,
|
|
429
|
+
interim,
|
|
430
|
+
});
|
|
431
|
+
return openWsStream(url, this._headers);
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
/**
|
|
435
|
+
* Open a streaming turn-detection session (VAD events only).
|
|
436
|
+
* @returns {Promise<WsStream>}
|
|
437
|
+
*/
|
|
438
|
+
async vadStream() {
|
|
439
|
+
const url = this.#wsUrl("/v1/vad/stream", {});
|
|
440
|
+
return openWsStream(url, this._headers);
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
// ──────────────────────── Transport ────────────────────────────────
|
|
444
|
+
|
|
445
|
+
/**
|
|
446
|
+
* @param {string} path
|
|
447
|
+
* @returns {Promise<Record<string, unknown>>}
|
|
448
|
+
*/
|
|
449
|
+
async #getJson(path) {
|
|
450
|
+
const response = await this.#request(path);
|
|
451
|
+
return response.json();
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
/**
|
|
455
|
+
* @param {string} path
|
|
456
|
+
* @param {Record<string, unknown>} body
|
|
457
|
+
* @returns {Promise<Record<string, unknown>>}
|
|
458
|
+
*/
|
|
459
|
+
async #postJson(path, body) {
|
|
460
|
+
const response = await this.#request(path, { method: "POST", json: body });
|
|
461
|
+
return response.json();
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
/**
|
|
465
|
+
* @param {string} path
|
|
466
|
+
* @param {AudioSource} audio
|
|
467
|
+
* @param {object} opts
|
|
468
|
+
* @returns {Promise<Record<string, unknown>>}
|
|
469
|
+
*/
|
|
470
|
+
async #postForm(path, audio, opts) {
|
|
471
|
+
const { data, filename } = await audioToParts(audio);
|
|
472
|
+
const form = new FormData();
|
|
473
|
+
form.append("audio", new Blob([data], { type: "audio/wav" }), filename);
|
|
474
|
+
|
|
475
|
+
const { keyterms, ...rest } = opts;
|
|
476
|
+
for (const [key, value] of Object.entries(compact(rest))) {
|
|
477
|
+
form.append(key, String(value));
|
|
478
|
+
}
|
|
479
|
+
if (Array.isArray(keyterms) && keyterms.length) {
|
|
480
|
+
form.append("keyterms", keyterms.join(","));
|
|
481
|
+
}
|
|
482
|
+
|
|
483
|
+
const response = await this.#request(path, { method: "POST", form });
|
|
484
|
+
return response.json();
|
|
485
|
+
}
|
|
486
|
+
|
|
487
|
+
/**
|
|
488
|
+
* @param {string} path
|
|
489
|
+
* @param {{ method?: string, json?: unknown, form?: FormData }} [options]
|
|
490
|
+
* @returns {Promise<Response>}
|
|
491
|
+
*/
|
|
492
|
+
async #request(path, { method = "GET", json, form } = {}) {
|
|
493
|
+
const headers = { ...this._headers };
|
|
494
|
+
const init = { method, headers, signal: AbortSignal.timeout(this._timeoutMs) };
|
|
495
|
+
|
|
496
|
+
if (json !== undefined) {
|
|
497
|
+
init.headers["Content-Type"] = "application/json";
|
|
498
|
+
init.body = JSON.stringify(json);
|
|
499
|
+
} else if (form !== undefined) {
|
|
500
|
+
init.body = form;
|
|
501
|
+
}
|
|
502
|
+
|
|
503
|
+
const response = await fetch(`${this._baseUrl}${path}`, init);
|
|
504
|
+
if (!response.ok) {
|
|
505
|
+
let code = "";
|
|
506
|
+
let detail = await response.text();
|
|
507
|
+
try {
|
|
508
|
+
const payload = JSON.parse(detail);
|
|
509
|
+
code = payload.code ?? "";
|
|
510
|
+
detail = payload.detail ?? payload.title ?? detail;
|
|
511
|
+
} catch {
|
|
512
|
+
/* not JSON */
|
|
513
|
+
}
|
|
514
|
+
throw new VoiceKitError(response.status, detail, code);
|
|
515
|
+
}
|
|
516
|
+
return response;
|
|
517
|
+
}
|
|
518
|
+
|
|
519
|
+
/**
|
|
520
|
+
* Build a `ws(s)://` URL for a streaming endpoint from the base URL.
|
|
521
|
+
* @param {string} path
|
|
522
|
+
* @param {Record<string, unknown>} params
|
|
523
|
+
* @returns {string}
|
|
524
|
+
*/
|
|
525
|
+
#wsUrl(path, params) {
|
|
526
|
+
const url = new URL(this._baseUrl);
|
|
527
|
+
url.protocol = url.protocol === "https:" ? "wss:" : "ws:";
|
|
528
|
+
url.pathname = path;
|
|
529
|
+
url.search = "";
|
|
530
|
+
for (const [key, value] of Object.entries(params)) {
|
|
531
|
+
if (value === undefined || value === null || value === "") continue;
|
|
532
|
+
url.searchParams.set(key, String(value));
|
|
533
|
+
}
|
|
534
|
+
return url.toString();
|
|
535
|
+
}
|
|
536
|
+
}
|
|
537
|
+
|
|
538
|
+
/**
|
|
539
|
+
* @param {AudioSource} audio
|
|
540
|
+
* @returns {Promise<{ data: Uint8Array, filename: string }>}
|
|
541
|
+
*/
|
|
542
|
+
async function audioToParts(audio) {
|
|
543
|
+
if (audio instanceof Uint8Array) {
|
|
544
|
+
return { data: audio, filename: "audio.wav" };
|
|
545
|
+
}
|
|
546
|
+
const data = await readFile(audio);
|
|
547
|
+
return { data: new Uint8Array(data), filename: basename(audio) };
|
|
548
|
+
}
|
|
549
|
+
|
|
550
|
+
/**
|
|
551
|
+
* Encode audio as base64 for `batchAnalyze()`.
|
|
552
|
+
* @param {AudioSource} audio
|
|
553
|
+
* @returns {Promise<string>}
|
|
554
|
+
*/
|
|
555
|
+
export async function b64(audio) {
|
|
556
|
+
const { data } = await audioToParts(audio);
|
|
557
|
+
return Buffer.from(data).toString("base64");
|
|
558
|
+
}
|
|
559
|
+
|
|
560
|
+
/**
|
|
561
|
+
* @param {Record<string, unknown>} object
|
|
562
|
+
* @returns {Record<string, unknown>}
|
|
563
|
+
*/
|
|
564
|
+
function compact(object) {
|
|
565
|
+
return Object.fromEntries(
|
|
566
|
+
Object.entries(object).filter(([, value]) => value !== undefined && value !== null),
|
|
567
|
+
);
|
|
568
|
+
}
|
|
569
|
+
|
|
570
|
+
// ──────────────────────── WebSocket streaming ─────────────────────────
|
|
571
|
+
|
|
572
|
+
/**
|
|
573
|
+
* A connected streaming session (transcription or VAD).
|
|
574
|
+
*/
|
|
575
|
+
export class WsStream {
|
|
576
|
+
/**
|
|
577
|
+
* @param {import("ws")} ws
|
|
578
|
+
*/
|
|
579
|
+
constructor(ws) {
|
|
580
|
+
/** @private */ this._ws = ws;
|
|
581
|
+
// Prevent unhandled 'error' events from crashing the process mid-session.
|
|
582
|
+
ws.on("error", () => {});
|
|
583
|
+
}
|
|
584
|
+
|
|
585
|
+
/**
|
|
586
|
+
* Send raw PCM16 (16 kHz, mono, little-endian) audio.
|
|
587
|
+
* @param {Uint8Array} pcm16
|
|
588
|
+
* @returns {Promise<void>}
|
|
589
|
+
*/
|
|
590
|
+
sendAudio(pcm16) {
|
|
591
|
+
return new Promise((resolve, reject) => {
|
|
592
|
+
this._ws.send(pcm16, (err) => (err ? reject(err) : resolve()));
|
|
593
|
+
});
|
|
594
|
+
}
|
|
595
|
+
|
|
596
|
+
/**
|
|
597
|
+
* Send a text frame (objects are JSON-encoded).
|
|
598
|
+
* @param {unknown} payload
|
|
599
|
+
* @returns {Promise<void>}
|
|
600
|
+
*/
|
|
601
|
+
sendText(payload) {
|
|
602
|
+
const text = typeof payload === "string" ? payload : JSON.stringify(payload);
|
|
603
|
+
return new Promise((resolve, reject) => {
|
|
604
|
+
this._ws.send(text, (err) => (err ? reject(err) : resolve()));
|
|
605
|
+
});
|
|
606
|
+
}
|
|
607
|
+
|
|
608
|
+
/**
|
|
609
|
+
* Signal end of speech so the server finalizes the utterance.
|
|
610
|
+
* @returns {Promise<void>}
|
|
611
|
+
*/
|
|
612
|
+
stop() {
|
|
613
|
+
return this.sendText({ type: "stop" });
|
|
614
|
+
}
|
|
615
|
+
|
|
616
|
+
/**
|
|
617
|
+
* Receive JSON events (`session`, `vad`, `partial`, `final`, `error`).
|
|
618
|
+
* @returns {AsyncGenerator<Record<string, unknown>>}
|
|
619
|
+
*/
|
|
620
|
+
async *events() {
|
|
621
|
+
for await (const message of this._ws) {
|
|
622
|
+
yield typeof message === "string" ? JSON.parse(message) : JSON.parse(message.toString("utf8"));
|
|
623
|
+
}
|
|
624
|
+
}
|
|
625
|
+
|
|
626
|
+
/** Close the session. */
|
|
627
|
+
close() {
|
|
628
|
+
this._ws.close();
|
|
629
|
+
}
|
|
630
|
+
}
|
|
631
|
+
|
|
632
|
+
/**
|
|
633
|
+
* @param {string} url
|
|
634
|
+
* @param {Record<string, string>} headers
|
|
635
|
+
* @returns {Promise<WsStream>}
|
|
636
|
+
*/
|
|
637
|
+
async function openWsStream(url, headers) {
|
|
638
|
+
const { default: WebSocket } = await import("ws");
|
|
639
|
+
const ws = new WebSocket(url, { headers });
|
|
640
|
+
await new Promise((resolve, reject) => {
|
|
641
|
+
ws.once("open", resolve);
|
|
642
|
+
ws.once("error", reject);
|
|
643
|
+
});
|
|
644
|
+
return new WsStream(ws);
|
|
645
|
+
}
|