@rimelabs/sdk 0.1.0-alpha.2 → 0.1.0-alpha.21
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +155 -0
- package/LICENSE-PRISM +201 -0
- package/NOTICE-PRISM +9 -0
- package/README.md +279 -114
- package/dist/auth.d.ts +9 -3
- package/dist/auth.js +12 -4
- package/dist/cancellation.d.ts +6 -0
- package/dist/cancellation.js +54 -0
- package/dist/client.d.ts +24 -23
- package/dist/client.js +41 -92
- package/dist/grpc.d.ts +27 -0
- package/dist/grpc.js +175 -0
- package/dist/index.d.ts +10 -2
- package/dist/index.js +6 -2
- package/dist/pcm.d.ts +21 -0
- package/dist/pcm.js +52 -0
- package/dist/realtime/async.d.ts +23 -0
- package/dist/realtime/async.js +87 -0
- package/dist/realtime/audio.d.ts +2 -0
- package/dist/realtime/audio.js +11 -0
- package/dist/realtime/client.d.ts +12 -0
- package/dist/realtime/client.js +139 -0
- package/dist/realtime/conversation.d.ts +47 -0
- package/dist/realtime/conversation.js +321 -0
- package/dist/realtime/faults.d.ts +2 -0
- package/dist/realtime/faults.js +25 -0
- package/dist/realtime/protocol.d.ts +99 -0
- package/dist/realtime/protocol.js +432 -0
- package/dist/realtime/session.d.ts +79 -0
- package/dist/realtime/session.js +808 -0
- package/dist/realtime/settings.d.ts +11 -0
- package/dist/realtime/settings.js +142 -0
- package/dist/realtime/transport.d.ts +16 -0
- package/dist/realtime/transport.js +86 -0
- package/dist/realtime/types.d.ts +292 -0
- package/dist/realtime/types.js +12 -0
- package/dist/stt/audio.d.ts +15 -0
- package/dist/stt/audio.js +46 -0
- package/dist/stt/client.d.ts +17 -0
- package/dist/stt/client.js +121 -0
- package/dist/stt/policy.d.ts +11 -0
- package/dist/stt/policy.js +11 -0
- package/dist/stt/protocol.d.ts +14 -0
- package/dist/stt/protocol.js +76 -0
- package/dist/stt/queue.d.ts +16 -0
- package/dist/stt/queue.js +45 -0
- package/dist/stt/stream.d.ts +48 -0
- package/dist/stt/stream.js +224 -0
- package/dist/stt/transport.d.ts +15 -0
- package/dist/stt/transport.js +94 -0
- package/dist/stt/types.d.ts +51 -0
- package/dist/stt/types.js +1 -0
- package/dist/{audio.d.ts → tts/audio.d.ts} +9 -0
- package/dist/{audio.js → tts/audio.js} +44 -4
- package/dist/tts/buffer.d.ts +3 -0
- package/dist/tts/buffer.js +53 -0
- package/dist/tts/client.d.ts +53 -0
- package/dist/tts/client.js +151 -0
- package/dist/tts/lexicon.d.ts +7 -0
- package/dist/tts/lexicon.js +23 -0
- package/dist/{policy.d.ts → tts/policy.d.ts} +2 -3
- package/dist/tts/policy.js +41 -0
- package/dist/{stream.d.ts → tts/stream.d.ts} +15 -2
- package/dist/{stream.js → tts/stream.js} +54 -30
- package/dist/tts/timestamps.d.ts +24 -0
- package/dist/tts/timestamps.js +46 -0
- package/dist/{transport.d.ts → tts/transport.d.ts} +10 -13
- package/dist/tts/transport.js +133 -0
- package/dist/validation.d.ts +5 -0
- package/dist/validation.js +28 -0
- package/docs/realtime.md +537 -0
- package/docs/stt.md +186 -0
- package/docs/tts.md +284 -0
- package/package.json +14 -12
- package/dist/policy.js +0 -82
- package/dist/sentences.d.ts +0 -21
- package/dist/sentences.js +0 -142
- package/dist/transport.js +0 -223
- package/vendor/LICENSE.blingfire +0 -21
- package/vendor/README.md +0 -11
- package/vendor/blingfire.cjs +0 -4
- package/vendor/blingfire.upstream.cjs +0 -1
- package/vendor/blingfire.wasm +0 -0
- /package/dist/{queue.d.ts → tts/queue.d.ts} +0 -0
- /package/dist/{queue.js → tts/queue.js} +0 -0
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import type { PCMFormat } from "../pcm.js";
|
|
2
|
+
/** Formatting intent, for example "flight 247" or "flight two four seven". */
|
|
3
|
+
export type TranscriptionMode = "written" | "verbatim";
|
|
4
|
+
/** One caller-ended utterance of headerless signed PCM16 little-endian audio. */
|
|
5
|
+
export type AudioSource = AsyncIterable<Uint8Array>;
|
|
6
|
+
/** Select replacement hypotheses (default) or permanent append fragments. */
|
|
7
|
+
export type TranscriptionOutputContract = "revised_hypotheses" | "append_fragments";
|
|
8
|
+
export interface TranscriptionDelta {
|
|
9
|
+
readonly kind: "delta";
|
|
10
|
+
/** Append exactly, preserving the server's newline separators. */
|
|
11
|
+
readonly text: string;
|
|
12
|
+
}
|
|
13
|
+
export interface TranscriptionOptions {
|
|
14
|
+
/** Defaults to revised_hypotheses. Each stream uses one fixed contract. */
|
|
15
|
+
outputContract?: TranscriptionOutputContract;
|
|
16
|
+
/** Required BCP-47 tag, passed unchanged to the service (for example "en"). */
|
|
17
|
+
language: string;
|
|
18
|
+
/** Formatting intent; defaults to written and does not guarantee exact tokens. */
|
|
19
|
+
mode?: TranscriptionMode;
|
|
20
|
+
/** Recognition hints passed unchanged and in order, for example ["Super-G"]. */
|
|
21
|
+
contextTerms?: readonly string[];
|
|
22
|
+
/** Defaults to PCM16 little-endian mono 16 kHz; fixed for the utterance. */
|
|
23
|
+
inputFormat?: PCMFormat;
|
|
24
|
+
/** Optional overall deadline in seconds, including source and consumer pauses. */
|
|
25
|
+
timeout?: number | null;
|
|
26
|
+
/** Cancels only this utterance, including a pending connection. */
|
|
27
|
+
signal?: AbortSignal;
|
|
28
|
+
}
|
|
29
|
+
export interface TranscriptionPartial {
|
|
30
|
+
/** A revisable replacement snapshot. */
|
|
31
|
+
readonly kind: "partial";
|
|
32
|
+
/** Complete current text; replace previous text instead of appending it. */
|
|
33
|
+
readonly text: string;
|
|
34
|
+
}
|
|
35
|
+
export interface TranscriptionFinal {
|
|
36
|
+
/** Successful protocol and transport completion. */
|
|
37
|
+
readonly kind: "final";
|
|
38
|
+
/** Complete final text; silence can produce an empty string. */
|
|
39
|
+
readonly text: string;
|
|
40
|
+
/** Canonical BCP-47 tag selected by the service, for example "en". */
|
|
41
|
+
readonly language: string;
|
|
42
|
+
}
|
|
43
|
+
export type TranscriptionUpdate = TranscriptionPartial | TranscriptionDelta | TranscriptionFinal;
|
|
44
|
+
/** Options for a complete WAV recording. Audio is sent unchanged. */
|
|
45
|
+
export type TranscribeOptions = Omit<TranscriptionOptions, "inputFormat" | "outputContract">;
|
|
46
|
+
/** A successful whole-recording transcription. Silence may produce empty text. */
|
|
47
|
+
export interface Transcription {
|
|
48
|
+
readonly text: string;
|
|
49
|
+
readonly language: string;
|
|
50
|
+
readonly requestId: string | null;
|
|
51
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -1,11 +1,20 @@
|
|
|
1
1
|
export declare class AudioFormat {
|
|
2
2
|
readonly encoding: string;
|
|
3
|
+
/** Fixed preset rate, or zero when selected per request. See AudioStream.sampleRate. */
|
|
3
4
|
readonly sampleRate: number;
|
|
4
5
|
readonly channels: number;
|
|
6
|
+
static readonly PCM: Readonly<AudioFormat>;
|
|
7
|
+
static readonly WAV: Readonly<AudioFormat>;
|
|
8
|
+
static readonly MP3: Readonly<AudioFormat>;
|
|
5
9
|
static readonly PCM_24000: Readonly<AudioFormat>;
|
|
6
10
|
static readonly MULAW_8000: Readonly<AudioFormat>;
|
|
7
11
|
private constructor();
|
|
8
12
|
}
|
|
13
|
+
export declare function wireFormat(format: AudioFormat): string;
|
|
14
|
+
export declare function resolveAudio(format: AudioFormat | null | undefined, sampleRate: number | null | undefined, timeScaleFactor: number | null | undefined): {
|
|
15
|
+
format: AudioFormat;
|
|
16
|
+
sampleRate: number;
|
|
17
|
+
};
|
|
9
18
|
export declare class Converter {
|
|
10
19
|
private profile;
|
|
11
20
|
private tail;
|
|
@@ -1,16 +1,52 @@
|
|
|
1
|
-
import { RimeAudioFormatError } from "
|
|
1
|
+
import { RimeAudioFormatError, RimeInputError } from "../errors.js";
|
|
2
2
|
export class AudioFormat {
|
|
3
3
|
encoding;
|
|
4
4
|
sampleRate;
|
|
5
5
|
channels;
|
|
6
|
+
static PCM = Object.freeze(new AudioFormat("pcm_s16le", 0, 1));
|
|
7
|
+
static WAV = Object.freeze(new AudioFormat("wav", 0, 1));
|
|
8
|
+
static MP3 = Object.freeze(new AudioFormat("mp3", 0, 1));
|
|
6
9
|
static PCM_24000 = Object.freeze(new AudioFormat("pcm_s16le", 24000, 1));
|
|
7
10
|
static MULAW_8000 = Object.freeze(new AudioFormat("mulaw", 8000, 1));
|
|
8
|
-
constructor(encoding,
|
|
11
|
+
constructor(encoding,
|
|
12
|
+
/** Fixed preset rate, or zero when selected per request. See AudioStream.sampleRate. */
|
|
13
|
+
sampleRate, channels) {
|
|
9
14
|
this.encoding = encoding;
|
|
10
15
|
this.sampleRate = sampleRate;
|
|
11
16
|
this.channels = channels;
|
|
12
17
|
}
|
|
13
18
|
}
|
|
19
|
+
export function wireFormat(format) {
|
|
20
|
+
if (format === AudioFormat.WAV)
|
|
21
|
+
return "audio/wav";
|
|
22
|
+
if (format === AudioFormat.MP3)
|
|
23
|
+
return "audio/mpeg";
|
|
24
|
+
return "audio/pcm";
|
|
25
|
+
}
|
|
26
|
+
export function resolveAudio(format, sampleRate, timeScaleFactor) {
|
|
27
|
+
format ??= AudioFormat.PCM;
|
|
28
|
+
if (![
|
|
29
|
+
AudioFormat.PCM,
|
|
30
|
+
AudioFormat.PCM_24000,
|
|
31
|
+
AudioFormat.MULAW_8000,
|
|
32
|
+
AudioFormat.WAV,
|
|
33
|
+
AudioFormat.MP3,
|
|
34
|
+
].includes(format))
|
|
35
|
+
throw new RimeAudioFormatError("Select a named AudioFormat");
|
|
36
|
+
sampleRate ??= format.sampleRate || 24000;
|
|
37
|
+
if (![8000, 16000, 24000].includes(sampleRate))
|
|
38
|
+
throw new RimeAudioFormatError("sampleRate must be 8000, 16000, or 24000");
|
|
39
|
+
if (format.sampleRate && sampleRate !== format.sampleRate)
|
|
40
|
+
throw new RimeAudioFormatError("sampleRate conflicts with the fixed AudioFormat preset");
|
|
41
|
+
if (timeScaleFactor != null &&
|
|
42
|
+
(!Number.isFinite(timeScaleFactor) ||
|
|
43
|
+
timeScaleFactor < 0.4 ||
|
|
44
|
+
timeScaleFactor > 2.5))
|
|
45
|
+
throw new RimeInputError("timeScaleFactor must be a finite number from 0.4 to 2.5");
|
|
46
|
+
if (format === AudioFormat.PCM && sampleRate === 24000)
|
|
47
|
+
format = AudioFormat.PCM_24000;
|
|
48
|
+
return { format, sampleRate };
|
|
49
|
+
}
|
|
14
50
|
const raw = Array.from({ length: 63 }, (_, i) => (i === 31
|
|
15
51
|
? (2 * 3400) / 24000
|
|
16
52
|
: Math.sin(((2 * Math.PI * 3400) / 24000) * (i - 31)) /
|
|
@@ -20,7 +56,8 @@ const total = raw.reduce((a, b) => a + b, 0);
|
|
|
20
56
|
const coefficients = raw.map((x) => x / total);
|
|
21
57
|
function mulaw(sample) {
|
|
22
58
|
sample = Math.max(-32768, Math.min(32767, sample));
|
|
23
|
-
|
|
59
|
+
// G.711 uses one's complement for negative PCM before quantization.
|
|
60
|
+
const sign = sample < 0 ? 128 : 0, magnitude = Math.min(sample < 0 ? ~sample : sample, 32635) + 132;
|
|
24
61
|
const exponent = Math.max(0, Math.floor(Math.log2(magnitude)) - 7);
|
|
25
62
|
return ~(sign | (exponent << 4) | ((magnitude >> (exponent + 3)) & 15)) & 255;
|
|
26
63
|
}
|
|
@@ -46,12 +83,15 @@ export class Converter {
|
|
|
46
83
|
return null;
|
|
47
84
|
}
|
|
48
85
|
process(input, final = false) {
|
|
86
|
+
if (this.profile === AudioFormat.WAV || this.profile === AudioFormat.MP3)
|
|
87
|
+
return Buffer.from(input);
|
|
49
88
|
const data = Buffer.concat([this.tail, input]);
|
|
50
89
|
const size = Math.floor(data.length / 2) * 2;
|
|
51
90
|
this.tail = Buffer.from(data.subarray(size));
|
|
52
91
|
if (final && this.tail.length)
|
|
53
92
|
throw new RimeAudioFormatError("Incomplete final PCM sample frame");
|
|
54
|
-
if (this.profile === AudioFormat.
|
|
93
|
+
if (this.profile === AudioFormat.PCM ||
|
|
94
|
+
this.profile === AudioFormat.PCM_24000)
|
|
55
95
|
return data.subarray(0, size);
|
|
56
96
|
const result = [];
|
|
57
97
|
for (let i = 0; i < size; i += 2) {
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import { RimeAudioFormatError } from "../errors.js";
|
|
2
|
+
export const maxBufferBytes = 64 * 1024 * 1024;
|
|
3
|
+
/** Validate mono PCM16 WAV and replace unknown streaming chunk lengths. */
|
|
4
|
+
export function finalizeWav(data, sampleRate, requestId) {
|
|
5
|
+
const invalid = () => new RimeAudioFormatError("Expected a complete mono PCM16 WAV at the requested sample rate", requestId);
|
|
6
|
+
if (data.length < 12 ||
|
|
7
|
+
data.toString("latin1", 0, 4) !== "RIFF" ||
|
|
8
|
+
data.toString("latin1", 8, 12) !== "WAVE")
|
|
9
|
+
throw invalid();
|
|
10
|
+
if (data.length - 8 > 0xffffffff)
|
|
11
|
+
throw new RimeAudioFormatError("Audio exceeds the WAV size limit", requestId);
|
|
12
|
+
if (![0xffffffff, data.length - 8].includes(data.readUInt32LE(4)))
|
|
13
|
+
throw invalid();
|
|
14
|
+
let offset = 12;
|
|
15
|
+
let foundFormat = false;
|
|
16
|
+
let dataSizeOffset;
|
|
17
|
+
let dataSize = 0;
|
|
18
|
+
while (offset < data.length) {
|
|
19
|
+
if (data.length - offset < 8)
|
|
20
|
+
throw invalid();
|
|
21
|
+
const name = data.toString("latin1", offset, offset + 4);
|
|
22
|
+
let size = data.readUInt32LE(offset + 4);
|
|
23
|
+
const start = offset + 8;
|
|
24
|
+
if (name === "data" && size === 0xffffffff)
|
|
25
|
+
size = data.length - start;
|
|
26
|
+
const end = start + size;
|
|
27
|
+
if (end > data.length)
|
|
28
|
+
throw invalid();
|
|
29
|
+
if (name === "fmt ") {
|
|
30
|
+
if (foundFormat ||
|
|
31
|
+
size < 16 ||
|
|
32
|
+
data.readUInt16LE(start) !== 1 ||
|
|
33
|
+
data.readUInt16LE(start + 2) !== 1 ||
|
|
34
|
+
data.readUInt32LE(start + 4) !== sampleRate ||
|
|
35
|
+
data.readUInt32LE(start + 8) !== sampleRate * 2 ||
|
|
36
|
+
data.readUInt16LE(start + 12) !== 2 ||
|
|
37
|
+
data.readUInt16LE(start + 14) !== 16)
|
|
38
|
+
throw invalid();
|
|
39
|
+
foundFormat = true;
|
|
40
|
+
}
|
|
41
|
+
else if (name === "data") {
|
|
42
|
+
if (!foundFormat || dataSizeOffset !== undefined || size % 2)
|
|
43
|
+
throw invalid();
|
|
44
|
+
dataSizeOffset = offset + 4;
|
|
45
|
+
dataSize = size;
|
|
46
|
+
}
|
|
47
|
+
offset = end + (size % 2);
|
|
48
|
+
}
|
|
49
|
+
if (offset !== data.length || dataSizeOffset === undefined)
|
|
50
|
+
throw invalid();
|
|
51
|
+
data.writeUInt32LE(data.length - 8, 4);
|
|
52
|
+
data.writeUInt32LE(dataSize, dataSizeOffset);
|
|
53
|
+
}
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import { AudioFormat } from "./audio.js";
|
|
2
|
+
import { type PronunciationEntry } from "./lexicon.js";
|
|
3
|
+
import { AudioStream, type TextSource } from "./stream.js";
|
|
4
|
+
import { Credentials } from "../auth.js";
|
|
5
|
+
import { policy } from "./policy.js";
|
|
6
|
+
export interface SynthesisOptions {
|
|
7
|
+
voice?: string | null;
|
|
8
|
+
language?: string;
|
|
9
|
+
audioFormat?: AudioFormat | null;
|
|
10
|
+
/** Requested output rate: 8000, 16000, or 24000 Hz. Defaults to 24000 for PCM/WAV/MP3. */
|
|
11
|
+
sampleRate?: number | null;
|
|
12
|
+
/** Duration multiplier from 0.4 to 2.5. Below 1 speeds speech up; above 1 slows it down. */
|
|
13
|
+
timeScaleFactor?: number | null;
|
|
14
|
+
/** Request final word timestamps. Supported only with model="mistv3". */
|
|
15
|
+
timestamps?: boolean;
|
|
16
|
+
/** Request-wide pronunciation overrides; requires a phoneme-capable Coda deployment. */
|
|
17
|
+
customLexicon?: readonly PronunciationEntry[];
|
|
18
|
+
timeout?: number | null;
|
|
19
|
+
}
|
|
20
|
+
export interface DiscoveryOptions {
|
|
21
|
+
timeout?: number | null;
|
|
22
|
+
}
|
|
23
|
+
export interface VoiceListOptions extends DiscoveryOptions {
|
|
24
|
+
language?: string | null;
|
|
25
|
+
}
|
|
26
|
+
export declare class TTS {
|
|
27
|
+
private readonly credentials;
|
|
28
|
+
private readonly deployment;
|
|
29
|
+
private readonly defaultTimeout;
|
|
30
|
+
private readonly checkOpen;
|
|
31
|
+
private client;
|
|
32
|
+
private streams;
|
|
33
|
+
private discoveryOperations;
|
|
34
|
+
readonly voices: {
|
|
35
|
+
list: (options?: VoiceListOptions) => Promise<string[]>;
|
|
36
|
+
};
|
|
37
|
+
readonly languages: {
|
|
38
|
+
list: (options?: DiscoveryOptions) => Promise<string[]>;
|
|
39
|
+
};
|
|
40
|
+
constructor(credentials: Credentials, deployment: Readonly<typeof policy>, defaultTimeout: number | null, checkOpen: () => void);
|
|
41
|
+
private prepare;
|
|
42
|
+
/** Synthesize complete text and return an audio stream. Do not await this call. */
|
|
43
|
+
synthesize(text: string, options?: SynthesisOptions): AudioStream;
|
|
44
|
+
/** Await complete audio in memory, finalizing WAV lengths. Never returns partial audio.
|
|
45
|
+
* The fixed output limit is 64 MiB (67,108,864 bytes); exceeding it raises
|
|
46
|
+
* RimeResourceLimitError. Use streaming synthesis for larger outputs or timestamps.
|
|
47
|
+
*/
|
|
48
|
+
synthesizeToBuffer(text: string, options?: SynthesisOptions): Promise<Buffer>;
|
|
49
|
+
/** Stream text chunks into synthesis and return an audio stream. Do not await this call. */
|
|
50
|
+
stream(text: TextSource, options?: SynthesisOptions): AudioStream;
|
|
51
|
+
private discover;
|
|
52
|
+
close(): Promise<void>;
|
|
53
|
+
}
|
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
import { AudioFormat, resolveAudio } from "./audio.js";
|
|
2
|
+
import { maxBufferBytes, finalizeWav } from "./buffer.js";
|
|
3
|
+
import { snapshot } from "./lexicon.js";
|
|
4
|
+
import { AudioStream, constructionKey } from "./stream.js";
|
|
5
|
+
import { policy, timeout, nonempty } from "./policy.js";
|
|
6
|
+
import { abortable } from "../cancellation.js";
|
|
7
|
+
import { transport, connect, metadata, discover, } from "./transport.js";
|
|
8
|
+
import { RimeInputError, RimeResourceLimitError, RimeCancelledError, RimeTimeoutError, RimeUnavailableError, } from "../errors.js";
|
|
9
|
+
export class TTS {
|
|
10
|
+
credentials;
|
|
11
|
+
deployment;
|
|
12
|
+
defaultTimeout;
|
|
13
|
+
checkOpen;
|
|
14
|
+
client = null;
|
|
15
|
+
streams = new Set();
|
|
16
|
+
discoveryOperations = new Map();
|
|
17
|
+
voices;
|
|
18
|
+
languages;
|
|
19
|
+
constructor(credentials, deployment, defaultTimeout, checkOpen) {
|
|
20
|
+
this.credentials = credentials;
|
|
21
|
+
this.deployment = deployment;
|
|
22
|
+
this.defaultTimeout = defaultTimeout;
|
|
23
|
+
this.checkOpen = checkOpen;
|
|
24
|
+
this.voices = {
|
|
25
|
+
list: async (options = {}) => {
|
|
26
|
+
if (options.language !== undefined && options.language !== null)
|
|
27
|
+
nonempty(options.language, "language");
|
|
28
|
+
return this.discover("voices", options.language, options.timeout);
|
|
29
|
+
},
|
|
30
|
+
};
|
|
31
|
+
this.languages = {
|
|
32
|
+
list: (options = {}) => this.discover("languages", undefined, options.timeout),
|
|
33
|
+
};
|
|
34
|
+
}
|
|
35
|
+
async prepare(signal) {
|
|
36
|
+
this.checkOpen();
|
|
37
|
+
const auth = await this.credentials.metadata(signal);
|
|
38
|
+
this.checkOpen();
|
|
39
|
+
this.client ??= transport.makeClient(this.deployment.target);
|
|
40
|
+
await connect(this.client, signal);
|
|
41
|
+
return { client: this.client, metadata: metadata(auth) };
|
|
42
|
+
}
|
|
43
|
+
/** Synthesize complete text and return an audio stream. Do not await this call. */
|
|
44
|
+
synthesize(text, options = {}) {
|
|
45
|
+
this.checkOpen();
|
|
46
|
+
if (typeof text !== "string")
|
|
47
|
+
throw new RimeInputError("text must be a string; use stream() for an async text source");
|
|
48
|
+
if (!text.trim())
|
|
49
|
+
throw new RimeInputError("Text must contain non-whitespace characters");
|
|
50
|
+
return this.stream((async function* () {
|
|
51
|
+
yield text;
|
|
52
|
+
})(), options);
|
|
53
|
+
}
|
|
54
|
+
/** Await complete audio in memory, finalizing WAV lengths. Never returns partial audio.
|
|
55
|
+
* The fixed output limit is 64 MiB (67,108,864 bytes); exceeding it raises
|
|
56
|
+
* RimeResourceLimitError. Use streaming synthesis for larger outputs or timestamps.
|
|
57
|
+
*/
|
|
58
|
+
async synthesizeToBuffer(text, options = {}) {
|
|
59
|
+
const audio = this.synthesize(text, options);
|
|
60
|
+
const chunks = [];
|
|
61
|
+
let size = 0;
|
|
62
|
+
try {
|
|
63
|
+
for await (const chunk of audio) {
|
|
64
|
+
if (chunk.byteLength > maxBufferBytes - size)
|
|
65
|
+
throw new RimeResourceLimitError("Audio exceeds the 64 MiB buffer limit", audio.requestId);
|
|
66
|
+
chunks.push(chunk);
|
|
67
|
+
size += chunk.byteLength;
|
|
68
|
+
}
|
|
69
|
+
const data = Buffer.concat(chunks, size);
|
|
70
|
+
if (audio.format === AudioFormat.WAV)
|
|
71
|
+
finalizeWav(data, audio.sampleRate, audio.requestId);
|
|
72
|
+
return data;
|
|
73
|
+
}
|
|
74
|
+
finally {
|
|
75
|
+
await audio.cancel();
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
/** Stream text chunks into synthesis and return an audio stream. Do not await this call. */
|
|
79
|
+
stream(text, options = {}) {
|
|
80
|
+
this.checkOpen();
|
|
81
|
+
if (typeof text === "string" ||
|
|
82
|
+
!text ||
|
|
83
|
+
typeof text[Symbol.asyncIterator] !== "function")
|
|
84
|
+
throw new RimeInputError("text must be an async iterable of strings; use synthesize() for a string");
|
|
85
|
+
const lexicon = snapshot(options.customLexicon);
|
|
86
|
+
const timestamps = options.timestamps === undefined ? false : options.timestamps;
|
|
87
|
+
if (typeof timestamps !== "boolean")
|
|
88
|
+
throw new RimeInputError("timestamps must be a boolean");
|
|
89
|
+
if (timestamps && this.deployment.model !== "mistv3")
|
|
90
|
+
throw new RimeInputError("Word timestamps are supported only with model='mistv3'");
|
|
91
|
+
const voice = options.voice === undefined || options.voice === null
|
|
92
|
+
? this.deployment.defaultVoice
|
|
93
|
+
: nonempty(options.voice, "voice");
|
|
94
|
+
const language = nonempty(options.language ?? "en", "language");
|
|
95
|
+
const { format, sampleRate } = resolveAudio(options.audioFormat, options.sampleRate, options.timeScaleFactor);
|
|
96
|
+
const audio = new AudioStream(constructionKey, {
|
|
97
|
+
prepare: (s) => this.prepare(s),
|
|
98
|
+
checkOpen: () => this.checkOpen(),
|
|
99
|
+
forget: (s) => this.streams.delete(s),
|
|
100
|
+
}, text, voice, language, format, timeout(options.timeout, this.defaultTimeout), timestamps, lexicon, sampleRate, options.timeScaleFactor ?? undefined);
|
|
101
|
+
this.streams.add(audio);
|
|
102
|
+
return audio;
|
|
103
|
+
}
|
|
104
|
+
async discover(kind, language, override) {
|
|
105
|
+
this.checkOpen();
|
|
106
|
+
const budget = Math.min(timeout(override, this.defaultTimeout) ?? Infinity, policy.discoveryTimeout);
|
|
107
|
+
const controller = new AbortController();
|
|
108
|
+
let requestId = null;
|
|
109
|
+
const timer = setTimeout(() => controller.abort(new RimeTimeoutError("Discovery deadline expired")), budget * 1000);
|
|
110
|
+
const operation = (async () => {
|
|
111
|
+
try {
|
|
112
|
+
const prepared = await this.prepare(controller.signal);
|
|
113
|
+
for (let attempt = 0;; attempt++) {
|
|
114
|
+
requestId = null;
|
|
115
|
+
try {
|
|
116
|
+
return await discover(prepared, kind, language, controller.signal);
|
|
117
|
+
}
|
|
118
|
+
catch (error) {
|
|
119
|
+
if (!(error instanceof RimeUnavailableError) || attempt === 2)
|
|
120
|
+
throw error;
|
|
121
|
+
requestId = error.requestId;
|
|
122
|
+
await abortable(new Promise((resolve) => setTimeout(resolve, 50 * 2 ** attempt)), controller.signal);
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
catch (error) {
|
|
127
|
+
if (error instanceof RimeTimeoutError && requestId !== null)
|
|
128
|
+
throw new RimeTimeoutError(error.message, requestId, {
|
|
129
|
+
cause: error.cause,
|
|
130
|
+
});
|
|
131
|
+
throw error;
|
|
132
|
+
}
|
|
133
|
+
finally {
|
|
134
|
+
clearTimeout(timer);
|
|
135
|
+
this.discoveryOperations.delete(controller);
|
|
136
|
+
}
|
|
137
|
+
})();
|
|
138
|
+
this.discoveryOperations.set(controller, operation);
|
|
139
|
+
return operation;
|
|
140
|
+
}
|
|
141
|
+
async close() {
|
|
142
|
+
const discoveries = [...this.discoveryOperations.values()];
|
|
143
|
+
for (const controller of this.discoveryOperations.keys())
|
|
144
|
+
controller.abort(new RimeCancelledError("Client closed during discovery"));
|
|
145
|
+
await Promise.all([
|
|
146
|
+
...[...this.streams].map((stream) => stream.cancel()),
|
|
147
|
+
Promise.allSettled(discoveries),
|
|
148
|
+
]);
|
|
149
|
+
this.client?.close();
|
|
150
|
+
}
|
|
151
|
+
}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
/** A word or phrase and its space-separated X-SAMPA pronunciation. */
|
|
2
|
+
export interface PronunciationEntry {
|
|
3
|
+
readonly spelling: string;
|
|
4
|
+
readonly pronunciation: string;
|
|
5
|
+
}
|
|
6
|
+
/** Copy request options now; model and linguistic validation belong to the service. */
|
|
7
|
+
export declare function snapshot(entries?: readonly PronunciationEntry[]): readonly PronunciationEntry[];
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { RimeInputError } from "../errors.js";
|
|
2
|
+
// Unicode mode treats valid surrogate pairs as one code point outside this range.
|
|
3
|
+
const unpairedSurrogate = /[\uD800-\uDFFF]/u;
|
|
4
|
+
/** Copy request options now; model and linguistic validation belong to the service. */
|
|
5
|
+
export function snapshot(entries = []) {
|
|
6
|
+
if (!Array.isArray(entries))
|
|
7
|
+
throw new RimeInputError("customLexicon must be an array of pronunciation entries");
|
|
8
|
+
// Array.from also visits holes, which are invalid entries.
|
|
9
|
+
return Object.freeze(Array.from(entries, (entry) => {
|
|
10
|
+
if (!entry ||
|
|
11
|
+
typeof entry !== "object" ||
|
|
12
|
+
typeof entry.spelling !== "string" ||
|
|
13
|
+
typeof entry.pronunciation !== "string")
|
|
14
|
+
throw new RimeInputError("Lexicon entries must have string spelling and pronunciation fields");
|
|
15
|
+
if (unpairedSurrogate.test(entry.spelling) ||
|
|
16
|
+
unpairedSurrogate.test(entry.pronunciation))
|
|
17
|
+
throw new RimeInputError("Lexicon spelling and pronunciation must contain valid Unicode");
|
|
18
|
+
return Object.freeze({
|
|
19
|
+
spelling: entry.spelling,
|
|
20
|
+
pronunciation: entry.pronunciation,
|
|
21
|
+
});
|
|
22
|
+
}));
|
|
23
|
+
}
|
|
@@ -1,9 +1,10 @@
|
|
|
1
|
+
export { timeout } from "../validation.js";
|
|
1
2
|
export declare const policy: {
|
|
3
|
+
model: string;
|
|
2
4
|
target: string;
|
|
3
5
|
audience: string;
|
|
4
6
|
defaultVoice: string;
|
|
5
7
|
exchangeUrl: string;
|
|
6
|
-
sentenceBytes: number;
|
|
7
8
|
sourceChars: number;
|
|
8
9
|
outputBytes: number;
|
|
9
10
|
outputChunkBytes: number;
|
|
@@ -16,6 +17,4 @@ export declare const policy: {
|
|
|
16
17
|
cleanupTimeout: number;
|
|
17
18
|
};
|
|
18
19
|
export declare function resolve(model: string, endpoint?: string | null): Readonly<typeof policy>;
|
|
19
|
-
export declare function timeout(value: unknown, inherited?: number | null): number | null;
|
|
20
20
|
export declare function nonempty(value: unknown, name: string): string;
|
|
21
|
-
export declare function abortable<T>(promise: PromiseLike<T>, signal: AbortSignal): Promise<T>;
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import { endpointAddress } from "../validation.js";
|
|
2
|
+
export { timeout } from "../validation.js";
|
|
3
|
+
import { RimeInputError } from "../errors.js";
|
|
4
|
+
const codaHostname = "coda.api.rime.ai";
|
|
5
|
+
export const policy = {
|
|
6
|
+
model: "coda",
|
|
7
|
+
target: `${codaHostname}:443`,
|
|
8
|
+
audience: codaHostname,
|
|
9
|
+
defaultVoice: "clementine",
|
|
10
|
+
exchangeUrl: "https://themis.api.rime.ai/v1/token",
|
|
11
|
+
sourceChars: 1024,
|
|
12
|
+
outputBytes: 96000,
|
|
13
|
+
outputChunkBytes: 9600,
|
|
14
|
+
receiveBytes: 4194304,
|
|
15
|
+
authTimeout: 10,
|
|
16
|
+
connectionTimeout: 10,
|
|
17
|
+
firstAudioTimeout: 30,
|
|
18
|
+
progressTimeout: 60,
|
|
19
|
+
discoveryTimeout: 10,
|
|
20
|
+
cleanupTimeout: 2,
|
|
21
|
+
};
|
|
22
|
+
export function resolve(model, endpoint) {
|
|
23
|
+
const deployment = { ...policy, model };
|
|
24
|
+
if (model === "mistv3") {
|
|
25
|
+
const hostname = "mist.api.rime.ai";
|
|
26
|
+
deployment.target = `${hostname}:443`;
|
|
27
|
+
deployment.audience = hostname;
|
|
28
|
+
deployment.defaultVoice = "astra";
|
|
29
|
+
}
|
|
30
|
+
else if (model !== "coda") {
|
|
31
|
+
throw new RimeInputError("model must be 'coda' or 'mistv3'");
|
|
32
|
+
}
|
|
33
|
+
if (endpoint === undefined || endpoint === null)
|
|
34
|
+
return deployment;
|
|
35
|
+
return { ...deployment, ...endpointAddress(endpoint) };
|
|
36
|
+
}
|
|
37
|
+
export function nonempty(value, name) {
|
|
38
|
+
if (typeof value !== "string" || !value.trim())
|
|
39
|
+
throw new RimeInputError(`${name} must be a non-empty string`);
|
|
40
|
+
return value;
|
|
41
|
+
}
|
|
@@ -1,11 +1,13 @@
|
|
|
1
1
|
import { AudioFormat } from "./audio.js";
|
|
2
|
+
import type { TimestampResult } from "./timestamps.js";
|
|
3
|
+
import type { PronunciationEntry } from "./lexicon.js";
|
|
2
4
|
import { type PreparedConnection } from "./transport.js";
|
|
3
5
|
export interface StreamOwner {
|
|
4
6
|
prepare(signal: AbortSignal): Promise<PreparedConnection>;
|
|
5
7
|
checkOpen(): void;
|
|
6
8
|
forget(stream: AudioStream): void;
|
|
7
9
|
}
|
|
8
|
-
export type TextSource =
|
|
10
|
+
export type TextSource = AsyncIterable<string>;
|
|
9
11
|
export declare const constructionKey: unique symbol;
|
|
10
12
|
export declare class AudioStream implements AsyncIterableIterator<Uint8Array> {
|
|
11
13
|
private owner;
|
|
@@ -14,6 +16,10 @@ export declare class AudioStream implements AsyncIterableIterator<Uint8Array> {
|
|
|
14
16
|
private language;
|
|
15
17
|
private formatValue;
|
|
16
18
|
private timeout;
|
|
19
|
+
private timestampsRequested;
|
|
20
|
+
private customLexicon;
|
|
21
|
+
private sampleRateValue;
|
|
22
|
+
private timeScaleFactor?;
|
|
17
23
|
private readonly controller;
|
|
18
24
|
private readonly queue;
|
|
19
25
|
private worker;
|
|
@@ -31,9 +37,16 @@ export declare class AudioStream implements AsyncIterableIterator<Uint8Array> {
|
|
|
31
37
|
private readonly operationId;
|
|
32
38
|
private bytesReceived;
|
|
33
39
|
private bytesDelivered;
|
|
34
|
-
constructor(key: symbol, owner: StreamOwner, source: TextSource | null, voice: string, language: string, formatValue: AudioFormat, timeout: number | null);
|
|
40
|
+
constructor(key: symbol, owner: StreamOwner, source: TextSource | null, voice: string, language: string, formatValue: AudioFormat, timeout: number | null, timestampsRequested?: boolean, customLexicon?: readonly PronunciationEntry[], sampleRateValue?: number, timeScaleFactor?: number | undefined);
|
|
35
41
|
get format(): AudioFormat;
|
|
42
|
+
/** Requested output rate. MP3 headers describe the encoded rate. */
|
|
43
|
+
get sampleRate(): number;
|
|
36
44
|
get requestId(): string | null;
|
|
45
|
+
/** Read requested timestamps after consuming all audio; never drains audio.
|
|
46
|
+
* Calling before iteration completes rejects with RimeInputError. Synthesis
|
|
47
|
+
* errors reject here too; alignment failures are returned in status.
|
|
48
|
+
*/
|
|
49
|
+
timestamps(): Promise<TimestampResult>;
|
|
37
50
|
private start;
|
|
38
51
|
private fail;
|
|
39
52
|
private produce;
|