@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
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
import { randomUUID } from "node:crypto";
|
|
2
2
|
import { debuglog } from "node:util";
|
|
3
3
|
const log = debuglog("rime-sdk");
|
|
4
|
-
import { Converter } from "./audio.js";
|
|
5
|
-
import { policy
|
|
4
|
+
import { AudioFormat, Converter, wireFormat } from "./audio.js";
|
|
5
|
+
import { policy } from "./policy.js";
|
|
6
|
+
import { abortable } from "../cancellation.js";
|
|
6
7
|
import { ByteQueue } from "./queue.js";
|
|
7
|
-
import { SentenceBuffer, ready } from "./sentences.js";
|
|
8
8
|
import { SynthesisCall } from "./transport.js";
|
|
9
|
-
import { RimeError, RimeInputError, RimeCancelledError, RimeTimeoutError, RimeStreamError, } from "
|
|
9
|
+
import { RimeError, RimeInputError, RimeCancelledError, RimeTimeoutError, RimeStreamError, } from "../errors.js";
|
|
10
10
|
export const constructionKey = Symbol("private stream constructor");
|
|
11
11
|
export class AudioStream {
|
|
12
12
|
owner;
|
|
@@ -15,6 +15,10 @@ export class AudioStream {
|
|
|
15
15
|
language;
|
|
16
16
|
formatValue;
|
|
17
17
|
timeout;
|
|
18
|
+
timestampsRequested;
|
|
19
|
+
customLexicon;
|
|
20
|
+
sampleRateValue;
|
|
21
|
+
timeScaleFactor;
|
|
18
22
|
controller = new AbortController();
|
|
19
23
|
queue = new ByteQueue(policy.outputBytes, policy.outputChunkBytes);
|
|
20
24
|
worker = null;
|
|
@@ -32,22 +36,43 @@ export class AudioStream {
|
|
|
32
36
|
operationId = randomUUID();
|
|
33
37
|
bytesReceived = 0;
|
|
34
38
|
bytesDelivered = 0;
|
|
35
|
-
constructor(key, owner, source, voice, language, formatValue, timeout) {
|
|
39
|
+
constructor(key, owner, source, voice, language, formatValue, timeout, timestampsRequested = false, customLexicon = [], sampleRateValue = 24000, timeScaleFactor) {
|
|
36
40
|
this.owner = owner;
|
|
37
41
|
this.source = source;
|
|
38
42
|
this.voice = voice;
|
|
39
43
|
this.language = language;
|
|
40
44
|
this.formatValue = formatValue;
|
|
41
45
|
this.timeout = timeout;
|
|
46
|
+
this.timestampsRequested = timestampsRequested;
|
|
47
|
+
this.customLexicon = customLexicon;
|
|
48
|
+
this.sampleRateValue = sampleRateValue;
|
|
49
|
+
this.timeScaleFactor = timeScaleFactor;
|
|
42
50
|
if (key !== constructionKey)
|
|
43
|
-
throw new TypeError("AudioStream is returned by client.tts.stream()");
|
|
51
|
+
throw new TypeError("AudioStream is returned by client.tts.synthesize() or client.tts.stream()");
|
|
44
52
|
}
|
|
45
53
|
get format() {
|
|
46
54
|
return this.formatValue;
|
|
47
55
|
}
|
|
56
|
+
/** Requested output rate. MP3 headers describe the encoded rate. */
|
|
57
|
+
get sampleRate() {
|
|
58
|
+
return this.sampleRateValue;
|
|
59
|
+
}
|
|
48
60
|
get requestId() {
|
|
49
61
|
return this.call?.requestId ?? null;
|
|
50
62
|
}
|
|
63
|
+
/** Read requested timestamps after consuming all audio; never drains audio.
|
|
64
|
+
* Calling before iteration completes rejects with RimeInputError. Synthesis
|
|
65
|
+
* errors reject here too; alignment failures are returned in status.
|
|
66
|
+
*/
|
|
67
|
+
async timestamps() {
|
|
68
|
+
if (!this.timestampsRequested)
|
|
69
|
+
throw new RimeInputError("Enable timestamps: true when creating the stream");
|
|
70
|
+
if (this.failure)
|
|
71
|
+
throw this.failure;
|
|
72
|
+
if (!this.finished)
|
|
73
|
+
throw new RimeInputError("Consume all audio before reading timestamps");
|
|
74
|
+
return this.call.timestampResult();
|
|
75
|
+
}
|
|
51
76
|
start() {
|
|
52
77
|
if (this.failure)
|
|
53
78
|
throw this.failure;
|
|
@@ -95,15 +120,11 @@ export class AudioStream {
|
|
|
95
120
|
async produce() {
|
|
96
121
|
const source = this.source;
|
|
97
122
|
let iterator;
|
|
98
|
-
|
|
123
|
+
let highSurrogate = "";
|
|
99
124
|
let meaningful = false;
|
|
100
125
|
try {
|
|
101
126
|
try {
|
|
102
|
-
iterator = (
|
|
103
|
-
? (async function* () {
|
|
104
|
-
yield source;
|
|
105
|
-
})()
|
|
106
|
-
: source)[Symbol.asyncIterator]();
|
|
127
|
+
iterator = source[Symbol.asyncIterator]();
|
|
107
128
|
}
|
|
108
129
|
catch (error) {
|
|
109
130
|
if (this.controller.signal.aborted)
|
|
@@ -130,35 +151,39 @@ export class AudioStream {
|
|
|
130
151
|
}
|
|
131
152
|
if (result.done)
|
|
132
153
|
break;
|
|
133
|
-
|
|
134
|
-
if (typeof chunk !== "string")
|
|
154
|
+
if (typeof result.value !== "string")
|
|
135
155
|
throw new RimeInputError("The text source must yield strings");
|
|
156
|
+
let chunk = highSurrogate + result.value;
|
|
157
|
+
highSurrogate = "";
|
|
158
|
+
// A JavaScript source may split one Unicode character across yields.
|
|
159
|
+
if (/[\uD800-\uDBFF]$/.test(chunk)) {
|
|
160
|
+
highSurrogate = chunk.slice(-1);
|
|
161
|
+
chunk = chunk.slice(0, -1);
|
|
162
|
+
}
|
|
136
163
|
if (chunk.trim())
|
|
137
164
|
meaningful = true;
|
|
138
|
-
//
|
|
165
|
+
// Bound wire messages without splitting a Unicode character.
|
|
139
166
|
for (let offset = 0; offset < chunk.length;) {
|
|
140
167
|
let end = Math.min(chunk.length, offset + policy.sourceChars);
|
|
141
168
|
if (end < chunk.length && /[\uD800-\uDBFF]/.test(chunk[end - 1]))
|
|
142
169
|
end++;
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
170
|
+
const part = chunk.slice(offset, end);
|
|
171
|
+
if (/[\uD800-\uDFFF]/u.test(part))
|
|
172
|
+
throw new RimeInputError("Text must contain valid Unicode");
|
|
173
|
+
if (!this.submitted)
|
|
174
|
+
this.progressAt = performance.now();
|
|
175
|
+
this.submitted = true;
|
|
176
|
+
await this.call.write(part);
|
|
177
|
+
this.progressAt = performance.now();
|
|
149
178
|
offset = end;
|
|
150
179
|
}
|
|
151
180
|
}
|
|
181
|
+
if (highSurrogate)
|
|
182
|
+
throw new RimeInputError("Text must contain valid Unicode");
|
|
152
183
|
if (!meaningful)
|
|
153
184
|
throw new RimeInputError("The text source contained no meaningful text");
|
|
154
|
-
for (const sentence of buffer.feed("", true)) {
|
|
155
|
-
if (!this.submitted)
|
|
156
|
-
this.progressAt = performance.now();
|
|
157
|
-
this.submitted = true;
|
|
158
|
-
await this.call.write(sentence);
|
|
159
|
-
}
|
|
160
|
-
this.call.finishInput();
|
|
161
185
|
this.inputDone = true;
|
|
186
|
+
this.call.finishInput();
|
|
162
187
|
}
|
|
163
188
|
catch (error) {
|
|
164
189
|
if (error instanceof RimeError || this.controller.signal.aborted)
|
|
@@ -199,10 +224,9 @@ export class AudioStream {
|
|
|
199
224
|
async run() {
|
|
200
225
|
let tasks = [];
|
|
201
226
|
try {
|
|
202
|
-
await abortable(ready, this.controller.signal);
|
|
203
227
|
const prepared = await this.owner.prepare(this.controller.signal);
|
|
204
228
|
this.call = new SynthesisCall(prepared, this.controller.signal);
|
|
205
|
-
await this.call.start(this.voice, this.language);
|
|
229
|
+
await this.call.start(this.voice, this.language, this.timestampsRequested, this.customLexicon, wireFormat(this.format), this.format === AudioFormat.MULAW_8000 ? 24000 : this.sampleRate, this.timeScaleFactor);
|
|
206
230
|
tasks = [this.produce(), this.read()];
|
|
207
231
|
await Promise.all(tasks);
|
|
208
232
|
this.queue.finish();
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import type { SynthesisResponseTrailer } from "@rimelabs/api";
|
|
2
|
+
/** A spoken, normalized word with start/end seconds from synthesis start. */
|
|
3
|
+
export interface WordTimestamp {
|
|
4
|
+
readonly text: string;
|
|
5
|
+
readonly start: number;
|
|
6
|
+
readonly end: number;
|
|
7
|
+
}
|
|
8
|
+
/** Alignment's google.rpc.Code (0 means OK) and service explanation. */
|
|
9
|
+
export interface TimestampStatus {
|
|
10
|
+
readonly code: number;
|
|
11
|
+
readonly message: string;
|
|
12
|
+
}
|
|
13
|
+
/** Final alignment status and words. Non-OK statuses carry no spans. */
|
|
14
|
+
export interface TimestampResult {
|
|
15
|
+
readonly status: TimestampStatus;
|
|
16
|
+
readonly spans: readonly WordTimestamp[];
|
|
17
|
+
}
|
|
18
|
+
export declare class TimestampTrailer {
|
|
19
|
+
seen: boolean;
|
|
20
|
+
invalid: boolean;
|
|
21
|
+
private value;
|
|
22
|
+
accept(trailer: SynthesisResponseTrailer): void;
|
|
23
|
+
result(requestId: string | null): TimestampResult;
|
|
24
|
+
}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import { RimeStreamError } from "../errors.js";
|
|
2
|
+
export class TimestampTrailer {
|
|
3
|
+
seen = false;
|
|
4
|
+
invalid = false;
|
|
5
|
+
value;
|
|
6
|
+
accept(trailer) {
|
|
7
|
+
if (this.seen)
|
|
8
|
+
this.invalid = true;
|
|
9
|
+
else {
|
|
10
|
+
this.seen = true;
|
|
11
|
+
this.value = trailer.timestamps;
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
result(requestId) {
|
|
15
|
+
const malformed = () => new RimeStreamError("Invalid timestamp trailer", requestId);
|
|
16
|
+
if (this.invalid)
|
|
17
|
+
throw malformed();
|
|
18
|
+
const value = this.value;
|
|
19
|
+
if (!value)
|
|
20
|
+
throw new RimeStreamError("The service did not return requested timestamps", requestId);
|
|
21
|
+
if (!value.status || (value.status.code !== 0 && value.spans.length))
|
|
22
|
+
throw malformed();
|
|
23
|
+
const seconds = (duration) => {
|
|
24
|
+
if (!duration ||
|
|
25
|
+
duration.seconds < 0n ||
|
|
26
|
+
duration.seconds > 315576000000n ||
|
|
27
|
+
duration.nanos < 0 ||
|
|
28
|
+
duration.nanos >= 1000000000)
|
|
29
|
+
throw malformed();
|
|
30
|
+
return Number(duration.seconds) + duration.nanos / 1e9;
|
|
31
|
+
};
|
|
32
|
+
const spans = value.spans.map((span) => {
|
|
33
|
+
const start = seconds(span.start), end = seconds(span.end);
|
|
34
|
+
if (!span.text.trim() || end < start)
|
|
35
|
+
throw malformed();
|
|
36
|
+
return Object.freeze({ text: span.text, start, end });
|
|
37
|
+
});
|
|
38
|
+
return Object.freeze({
|
|
39
|
+
status: Object.freeze({
|
|
40
|
+
code: value.status.code,
|
|
41
|
+
message: value.status.message,
|
|
42
|
+
}),
|
|
43
|
+
spans: Object.freeze(spans),
|
|
44
|
+
});
|
|
45
|
+
}
|
|
46
|
+
}
|
|
@@ -1,33 +1,30 @@
|
|
|
1
|
+
export { metadata } from "../grpc.js";
|
|
2
|
+
export { rpcError } from "../grpc.js";
|
|
1
3
|
import * as grpc from "@grpc/grpc-js";
|
|
2
|
-
import
|
|
4
|
+
import type { PronunciationEntry } from "./lexicon.js";
|
|
3
5
|
export type Connection = grpc.Client;
|
|
4
6
|
export interface PreparedConnection {
|
|
5
7
|
client: Connection;
|
|
6
8
|
metadata: grpc.Metadata;
|
|
7
9
|
}
|
|
8
10
|
export type DiscoveryKind = "voices" | "languages";
|
|
9
|
-
export declare function rpcError(code: number, requestId?: string | null): errors.RimeError;
|
|
10
11
|
export declare const transport: {
|
|
11
12
|
makeClient: (target: string) => grpc.Client;
|
|
12
13
|
};
|
|
13
14
|
export declare function connect(client: grpc.Client, signal: AbortSignal): Promise<void>;
|
|
14
|
-
export declare function metadata(value: string): grpc.Metadata;
|
|
15
15
|
export declare function discover(prepared: PreparedConnection, kind: DiscoveryKind, language: string | null | undefined, signal: AbortSignal): Promise<string[]>;
|
|
16
16
|
export declare class SynthesisCall {
|
|
17
|
-
private
|
|
18
|
-
private
|
|
19
|
-
private readonly
|
|
20
|
-
private readonly
|
|
21
|
-
private status;
|
|
22
|
-
private id;
|
|
17
|
+
private audioFormat;
|
|
18
|
+
private timestampsRequested;
|
|
19
|
+
private readonly timestampTrailer;
|
|
20
|
+
private readonly rpc;
|
|
23
21
|
constructor(prepared: PreparedConnection, signal: AbortSignal);
|
|
24
22
|
get requestId(): string | null;
|
|
25
23
|
get done(): boolean;
|
|
26
24
|
cancel(): void;
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
write(sentence: string): Promise<void>;
|
|
25
|
+
start(voice: string, language: string, timestamps?: boolean, customLexicon?: readonly PronunciationEntry[], audioFormat?: string, sampleRate?: number, timeScaleFactor?: number): Promise<void>;
|
|
26
|
+
timestampResult(): import("./timestamps.js").TimestampResult;
|
|
27
|
+
write(text: string): Promise<void>;
|
|
31
28
|
finishInput(): void;
|
|
32
29
|
private checkFormat;
|
|
33
30
|
audio(): AsyncGenerator<Uint8Array>;
|
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
import { connect as connectGrpc } from "../grpc.js";
|
|
2
|
+
export { metadata } from "../grpc.js";
|
|
3
|
+
import { BidiCall, serializer, deserializer, requestId, nativeError, } from "../grpc.js";
|
|
4
|
+
export { rpcError } from "../grpc.js";
|
|
5
|
+
import * as grpc from "@grpc/grpc-js";
|
|
6
|
+
import { create, } from "@bufbuild/protobuf";
|
|
7
|
+
import * as schema from "@rimelabs/api";
|
|
8
|
+
import { policy } from "./policy.js";
|
|
9
|
+
import { abortable } from "../cancellation.js";
|
|
10
|
+
import * as errors from "../errors.js";
|
|
11
|
+
import { TimestampTrailer } from "./timestamps.js";
|
|
12
|
+
export const transport = {
|
|
13
|
+
makeClient: (target) => new grpc.Client(target, grpc.credentials.createSsl(), {
|
|
14
|
+
"grpc.enable_retries": 0,
|
|
15
|
+
"grpc.max_receive_message_length": policy.receiveBytes,
|
|
16
|
+
"grpc.max_send_message_length": 131072,
|
|
17
|
+
}),
|
|
18
|
+
};
|
|
19
|
+
export function connect(client, signal) {
|
|
20
|
+
return connectGrpc(client, signal, policy.connectionTimeout);
|
|
21
|
+
}
|
|
22
|
+
function openStream(client, auth) {
|
|
23
|
+
return client.makeBidiStreamRequest("/rime.TextToSpeech/SynthesizeStreaming", serializer(schema.StreamingSynthesisRequestSchema), deserializer(schema.SynthesisResponseStreamSchema), auth);
|
|
24
|
+
}
|
|
25
|
+
function header(voice, language, timestamps, customLexicon, audioFormat, samplingRate, timeScaleFactor) {
|
|
26
|
+
return create(schema.StreamingSynthesisRequestSchema, {
|
|
27
|
+
payload: {
|
|
28
|
+
case: "header",
|
|
29
|
+
value: create(schema.SynthesisRequestSchema, {
|
|
30
|
+
speaker: voice,
|
|
31
|
+
language,
|
|
32
|
+
splitStrategy: schema.SplitStrategy.ACCUMULATE_SENTENCE,
|
|
33
|
+
audioParameters: { audioFormat, samplingRate, timeScaleFactor },
|
|
34
|
+
timestamps: timestamps ? { enable: true } : undefined,
|
|
35
|
+
customLexicon: [...customLexicon],
|
|
36
|
+
}),
|
|
37
|
+
},
|
|
38
|
+
});
|
|
39
|
+
}
|
|
40
|
+
function textMessage(text) {
|
|
41
|
+
return create(schema.StreamingSynthesisRequestSchema, {
|
|
42
|
+
payload: { case: "textChunk", value: text },
|
|
43
|
+
});
|
|
44
|
+
}
|
|
45
|
+
async function unary(prepared, name, input, output, request, signal) {
|
|
46
|
+
let call;
|
|
47
|
+
let id = null;
|
|
48
|
+
try {
|
|
49
|
+
signal.throwIfAborted();
|
|
50
|
+
const operation = new Promise((resolve, reject) => {
|
|
51
|
+
call = prepared.client.makeUnaryRequest("/rime.TextToSpeech/" + name, serializer(input), deserializer(output), request, prepared.metadata, (error, result) => (error ? reject(error) : resolve(result)));
|
|
52
|
+
call.once("metadata", (metadata) => {
|
|
53
|
+
id = requestId(metadata);
|
|
54
|
+
});
|
|
55
|
+
});
|
|
56
|
+
return await abortable(operation, signal);
|
|
57
|
+
}
|
|
58
|
+
catch (error) {
|
|
59
|
+
call?.cancel();
|
|
60
|
+
if (signal.aborted) {
|
|
61
|
+
if (signal.reason instanceof errors.RimeTimeoutError)
|
|
62
|
+
throw new errors.RimeTimeoutError(signal.reason.message, id, {
|
|
63
|
+
cause: signal.reason.cause,
|
|
64
|
+
});
|
|
65
|
+
throw signal.reason;
|
|
66
|
+
}
|
|
67
|
+
throw nativeError(error, id);
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
export async function discover(prepared, kind, language, signal) {
|
|
71
|
+
// Exactly one attempt. The client owns retries and the shared deadline.
|
|
72
|
+
if (kind === "voices") {
|
|
73
|
+
const method = schema.TextToSpeech.method.getSupportedSpeakers;
|
|
74
|
+
const response = await unary(prepared, method.name, method.input, method.output, create(method.input, { language: language ?? undefined }), signal);
|
|
75
|
+
return response.speakers;
|
|
76
|
+
}
|
|
77
|
+
const method = schema.TextToSpeech.method.getSupportedLanguages;
|
|
78
|
+
const response = await unary(prepared, method.name, method.input, method.output, create(method.input), signal);
|
|
79
|
+
return response.languages;
|
|
80
|
+
}
|
|
81
|
+
export class SynthesisCall {
|
|
82
|
+
audioFormat = "audio/pcm";
|
|
83
|
+
timestampsRequested = false;
|
|
84
|
+
timestampTrailer = new TimestampTrailer();
|
|
85
|
+
rpc;
|
|
86
|
+
constructor(prepared, signal) {
|
|
87
|
+
this.rpc = new BidiCall(openStream(prepared.client, prepared.metadata), signal);
|
|
88
|
+
}
|
|
89
|
+
get requestId() {
|
|
90
|
+
return this.rpc.requestId;
|
|
91
|
+
}
|
|
92
|
+
get done() {
|
|
93
|
+
return this.rpc.done;
|
|
94
|
+
}
|
|
95
|
+
cancel() {
|
|
96
|
+
this.rpc.cancel();
|
|
97
|
+
}
|
|
98
|
+
start(voice, language, timestamps = false, customLexicon = [], audioFormat = "audio/pcm", sampleRate = 24000, timeScaleFactor) {
|
|
99
|
+
this.timestampsRequested = timestamps;
|
|
100
|
+
this.audioFormat = audioFormat;
|
|
101
|
+
const message = header(voice, language, timestamps, customLexicon, audioFormat, sampleRate, timeScaleFactor);
|
|
102
|
+
return this.rpc.write(message);
|
|
103
|
+
}
|
|
104
|
+
timestampResult() {
|
|
105
|
+
return this.timestampTrailer.result(this.requestId);
|
|
106
|
+
}
|
|
107
|
+
write(text) {
|
|
108
|
+
return this.rpc.write(textMessage(text));
|
|
109
|
+
}
|
|
110
|
+
finishInput() {
|
|
111
|
+
this.rpc.finishInput();
|
|
112
|
+
}
|
|
113
|
+
checkFormat(contentType) {
|
|
114
|
+
if (contentType !== this.audioFormat)
|
|
115
|
+
throw new errors.RimeAudioFormatError(`Expected ${this.audioFormat} from the service`, this.requestId);
|
|
116
|
+
}
|
|
117
|
+
async *audio() {
|
|
118
|
+
const headers = await this.rpc.headers();
|
|
119
|
+
const contentType = headers.get("x-rime-audio-content-type")[0];
|
|
120
|
+
for await (const response of this.rpc.responses()) {
|
|
121
|
+
if (response.payload.case === "trailer" && this.timestampsRequested)
|
|
122
|
+
this.timestampTrailer.accept(response.payload.value);
|
|
123
|
+
if (response.payload.case !== "audio")
|
|
124
|
+
continue;
|
|
125
|
+
if (this.timestampTrailer.seen)
|
|
126
|
+
this.timestampTrailer.invalid = true;
|
|
127
|
+
if (response.payload.value.length)
|
|
128
|
+
this.checkFormat(contentType);
|
|
129
|
+
yield response.payload.value;
|
|
130
|
+
}
|
|
131
|
+
this.checkFormat(contentType);
|
|
132
|
+
}
|
|
133
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { RimeInputError } from "./errors.js";
|
|
2
|
+
export function endpointAddress(endpoint) {
|
|
3
|
+
const error = "endpoint must be a hostname with an optional port (1-65535), without a scheme or path";
|
|
4
|
+
if (typeof endpoint !== "string" || /\s/.test(endpoint))
|
|
5
|
+
throw new RimeInputError(error);
|
|
6
|
+
const [host, port, extra] = endpoint.split(":");
|
|
7
|
+
if (!host ||
|
|
8
|
+
host.length > 253 ||
|
|
9
|
+
extra !== undefined ||
|
|
10
|
+
host
|
|
11
|
+
.split(".")
|
|
12
|
+
.some((label) => !/^[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?$/i.test(label)) ||
|
|
13
|
+
(port !== undefined &&
|
|
14
|
+
(!/^[0-9]{1,5}$/.test(port) || Number(port) < 1 || Number(port) > 65535)))
|
|
15
|
+
throw new RimeInputError(error);
|
|
16
|
+
return {
|
|
17
|
+
target: `${host.toLowerCase()}:${port === undefined ? 443 : Number(port)}`,
|
|
18
|
+
audience: host.toLowerCase(),
|
|
19
|
+
};
|
|
20
|
+
}
|
|
21
|
+
export function timeout(value, inherited = null) {
|
|
22
|
+
if (value === undefined)
|
|
23
|
+
return inherited;
|
|
24
|
+
if (value !== null &&
|
|
25
|
+
(typeof value !== "number" || !Number.isFinite(value) || value <= 0))
|
|
26
|
+
throw new RimeInputError("timeout must be finite positive seconds or null");
|
|
27
|
+
return value;
|
|
28
|
+
}
|