@rimelabs/sdk 0.1.0-alpha.10 → 0.1.0-alpha.13
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 +30 -0
- package/README.md +38 -8
- package/dist/auth.d.ts +1 -0
- package/dist/auth.js +5 -2
- package/dist/cancellation.d.ts +5 -0
- package/dist/cancellation.js +36 -0
- package/dist/client.d.ts +11 -0
- package/dist/client.js +21 -5
- package/dist/grpc.d.ts +27 -0
- package/dist/grpc.js +175 -0
- package/dist/index.d.ts +4 -0
- package/dist/index.js +1 -0
- package/dist/pcm.d.ts +21 -0
- package/dist/pcm.js +52 -0
- package/dist/realtime/async.d.ts +1 -5
- package/dist/realtime/async.js +1 -36
- package/dist/realtime/audio.d.ts +0 -11
- package/dist/realtime/audio.js +2 -45
- package/dist/realtime/session.js +2 -1
- package/dist/realtime/types.d.ts +2 -6
- package/dist/stt/audio.d.ts +15 -0
- package/dist/stt/audio.js +46 -0
- package/dist/stt/client.d.ts +13 -0
- package/dist/stt/client.js +73 -0
- package/dist/stt/policy.d.ts +11 -0
- package/dist/stt/policy.js +11 -0
- package/dist/stt/protocol.d.ts +13 -0
- package/dist/stt/protocol.js +61 -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 +13 -0
- package/dist/stt/transport.js +45 -0
- package/dist/stt/types.d.ts +34 -0
- package/dist/stt/types.js +1 -0
- package/dist/tts/client.d.ts +8 -0
- package/dist/tts/client.js +24 -7
- package/dist/tts/lexicon.d.ts +7 -0
- package/dist/tts/lexicon.js +23 -0
- package/dist/tts/policy.d.ts +2 -2
- package/dist/tts/policy.js +5 -28
- package/dist/tts/stream.d.ts +11 -2
- package/dist/tts/stream.js +42 -27
- package/dist/tts/timestamps.d.ts +24 -0
- package/dist/tts/timestamps.js +46 -0
- package/dist/tts/transport.d.ts +9 -13
- package/dist/tts/transport.js +43 -140
- package/dist/validation.d.ts +5 -0
- package/dist/validation.js +28 -0
- package/docs/stt.md +103 -0
- package/docs/tts.md +120 -7
- package/package.json +4 -5
- package/dist/tts/sentences.d.ts +0 -21
- package/dist/tts/sentences.js +0 -142
- 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
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import * as grpc from "@grpc/grpc-js";
|
|
2
|
+
import * as schema from "@rimelabs/api";
|
|
3
|
+
import { BidiCall } from "../grpc.js";
|
|
4
|
+
import type { TranscriptionMode } from "./types.js";
|
|
5
|
+
export declare function requireSchema(): void;
|
|
6
|
+
export declare const transport: {
|
|
7
|
+
makeClient: (target: string) => grpc.Client;
|
|
8
|
+
};
|
|
9
|
+
export declare class TranscriptionCall extends BidiCall<schema.StreamingTranscriptionRequest, schema.StreamingTranscriptionResponse> {
|
|
10
|
+
constructor(client: grpc.Client, auth: grpc.Metadata, signal: AbortSignal);
|
|
11
|
+
start(language: string, mode: TranscriptionMode, contextTerms: readonly string[]): Promise<void>;
|
|
12
|
+
writeAudio(audio: Uint8Array): Promise<void>;
|
|
13
|
+
}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import * as grpc from "@grpc/grpc-js";
|
|
2
|
+
import { create } from "@bufbuild/protobuf";
|
|
3
|
+
import * as schema from "@rimelabs/api";
|
|
4
|
+
import { BidiCall, serializer, deserializer } from "../grpc.js";
|
|
5
|
+
import { RimeInputError } from "../errors.js";
|
|
6
|
+
import { policy } from "./policy.js";
|
|
7
|
+
export function requireSchema() {
|
|
8
|
+
if (!schema.SpeechToText)
|
|
9
|
+
throw new RimeInputError("STT requires @rimelabs/api with SpeechToText definitions");
|
|
10
|
+
}
|
|
11
|
+
export const transport = {
|
|
12
|
+
makeClient: (target) => new grpc.Client(target, grpc.credentials.createSsl(), {
|
|
13
|
+
"grpc.enable_retries": 0,
|
|
14
|
+
"grpc.max_receive_message_length": policy.receiveBytes,
|
|
15
|
+
"grpc.max_send_message_length": 131072,
|
|
16
|
+
}),
|
|
17
|
+
};
|
|
18
|
+
export class TranscriptionCall extends BidiCall {
|
|
19
|
+
constructor(client, auth, signal) {
|
|
20
|
+
requireSchema();
|
|
21
|
+
const service = schema.SpeechToText;
|
|
22
|
+
const method = service.method.transcribeStreaming;
|
|
23
|
+
super(client.makeBidiStreamRequest(`/${service.typeName}/${method.name}`, serializer(method.input), deserializer(method.output), auth), signal);
|
|
24
|
+
}
|
|
25
|
+
start(language, mode, contextTerms) {
|
|
26
|
+
return this.write(create(schema.StreamingTranscriptionRequestSchema, {
|
|
27
|
+
payload: {
|
|
28
|
+
case: "config",
|
|
29
|
+
value: {
|
|
30
|
+
language,
|
|
31
|
+
mode: mode === "verbatim"
|
|
32
|
+
? schema.TranscriptionMode.VERBATIM
|
|
33
|
+
: schema.TranscriptionMode.WRITTEN,
|
|
34
|
+
contextTerms: [...contextTerms],
|
|
35
|
+
outputContract: schema.StreamingOutputContract.REVISED_HYPOTHESES,
|
|
36
|
+
},
|
|
37
|
+
},
|
|
38
|
+
}));
|
|
39
|
+
}
|
|
40
|
+
writeAudio(audio) {
|
|
41
|
+
return this.write(create(schema.StreamingTranscriptionRequestSchema, {
|
|
42
|
+
payload: { case: "audio", value: audio },
|
|
43
|
+
}));
|
|
44
|
+
}
|
|
45
|
+
}
|
|
@@ -0,0 +1,34 @@
|
|
|
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
|
+
export interface TranscriptionOptions {
|
|
7
|
+
/** Required BCP-47 tag, passed unchanged to the service (for example "en"). */
|
|
8
|
+
language: string;
|
|
9
|
+
/** Formatting intent; defaults to written and does not guarantee exact tokens. */
|
|
10
|
+
mode?: TranscriptionMode;
|
|
11
|
+
/** Recognition hints passed unchanged and in order, for example ["Super-G"]. */
|
|
12
|
+
contextTerms?: readonly string[];
|
|
13
|
+
/** Defaults to PCM16 little-endian mono 16 kHz; fixed for the utterance. */
|
|
14
|
+
inputFormat?: PCMFormat;
|
|
15
|
+
/** Optional overall deadline in seconds, including source and consumer pauses. */
|
|
16
|
+
timeout?: number | null;
|
|
17
|
+
/** Cancels only this utterance, including a pending connection. */
|
|
18
|
+
signal?: AbortSignal;
|
|
19
|
+
}
|
|
20
|
+
export interface TranscriptionPartial {
|
|
21
|
+
/** A revisable replacement snapshot. */
|
|
22
|
+
readonly kind: "partial";
|
|
23
|
+
/** Complete current text; replace previous text instead of appending it. */
|
|
24
|
+
readonly text: string;
|
|
25
|
+
}
|
|
26
|
+
export interface TranscriptionFinal {
|
|
27
|
+
/** Successful protocol and transport completion. */
|
|
28
|
+
readonly kind: "final";
|
|
29
|
+
/** Complete final text; silence can produce an empty string. */
|
|
30
|
+
readonly text: string;
|
|
31
|
+
/** Canonical BCP-47 tag selected by the service, for example "en". */
|
|
32
|
+
readonly language: string;
|
|
33
|
+
}
|
|
34
|
+
export type TranscriptionUpdate = TranscriptionPartial | TranscriptionFinal;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
package/dist/tts/client.d.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { AudioFormat } from "./audio.js";
|
|
2
|
+
import { type PronunciationEntry } from "./lexicon.js";
|
|
2
3
|
import { AudioStream, type TextSource } from "./stream.js";
|
|
3
4
|
import { Credentials } from "../auth.js";
|
|
4
5
|
import { policy } from "./policy.js";
|
|
@@ -6,6 +7,10 @@ export interface SynthesisOptions {
|
|
|
6
7
|
voice?: string | null;
|
|
7
8
|
language?: string;
|
|
8
9
|
audioFormat?: AudioFormat | null;
|
|
10
|
+
/** Request final word timestamps. Supported only with model="mistv3". */
|
|
11
|
+
timestamps?: boolean;
|
|
12
|
+
/** Request-wide pronunciation overrides; requires a phoneme-capable Coda deployment. */
|
|
13
|
+
customLexicon?: readonly PronunciationEntry[];
|
|
9
14
|
timeout?: number | null;
|
|
10
15
|
}
|
|
11
16
|
export interface DiscoveryOptions {
|
|
@@ -30,6 +35,9 @@ export declare class TTS {
|
|
|
30
35
|
};
|
|
31
36
|
constructor(credentials: Credentials, deployment: Readonly<typeof policy>, defaultTimeout: number | null, checkOpen: () => void);
|
|
32
37
|
private prepare;
|
|
38
|
+
/** Synthesize complete text and return an audio stream. Do not await this call. */
|
|
39
|
+
synthesize(text: string, options?: SynthesisOptions): AudioStream;
|
|
40
|
+
/** Stream text chunks into synthesis and return an audio stream. Do not await this call. */
|
|
33
41
|
stream(text: TextSource, options?: SynthesisOptions): AudioStream;
|
|
34
42
|
private discover;
|
|
35
43
|
close(): Promise<void>;
|
package/dist/tts/client.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { AudioFormat } from "./audio.js";
|
|
2
|
+
import { snapshot } from "./lexicon.js";
|
|
2
3
|
import { AudioStream, constructionKey } from "./stream.js";
|
|
3
4
|
import { policy, timeout, nonempty } from "./policy.js";
|
|
4
5
|
import { abortable } from "../cancellation.js";
|
|
@@ -38,14 +39,30 @@ export class TTS {
|
|
|
38
39
|
await connect(this.client, signal);
|
|
39
40
|
return { client: this.client, metadata: metadata(auth) };
|
|
40
41
|
}
|
|
42
|
+
/** Synthesize complete text and return an audio stream. Do not await this call. */
|
|
43
|
+
synthesize(text, options = {}) {
|
|
44
|
+
this.checkOpen();
|
|
45
|
+
if (typeof text !== "string")
|
|
46
|
+
throw new RimeInputError("text must be a string; use stream() for an async text source");
|
|
47
|
+
if (!text.trim())
|
|
48
|
+
throw new RimeInputError("Text must contain non-whitespace characters");
|
|
49
|
+
return this.stream((async function* () {
|
|
50
|
+
yield text;
|
|
51
|
+
})(), options);
|
|
52
|
+
}
|
|
53
|
+
/** Stream text chunks into synthesis and return an audio stream. Do not await this call. */
|
|
41
54
|
stream(text, options = {}) {
|
|
42
55
|
this.checkOpen();
|
|
43
|
-
if (typeof text === "string"
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
56
|
+
if (typeof text === "string" ||
|
|
57
|
+
!text ||
|
|
58
|
+
typeof text[Symbol.asyncIterator] !== "function")
|
|
59
|
+
throw new RimeInputError("text must be an async iterable of strings; use synthesize() for a string");
|
|
60
|
+
const lexicon = snapshot(options.customLexicon);
|
|
61
|
+
const timestamps = options.timestamps === undefined ? false : options.timestamps;
|
|
62
|
+
if (typeof timestamps !== "boolean")
|
|
63
|
+
throw new RimeInputError("timestamps must be a boolean");
|
|
64
|
+
if (timestamps && this.deployment.model !== "mistv3")
|
|
65
|
+
throw new RimeInputError("Word timestamps are supported only with model='mistv3'");
|
|
49
66
|
const voice = options.voice === undefined || options.voice === null
|
|
50
67
|
? this.deployment.defaultVoice
|
|
51
68
|
: nonempty(options.voice, "voice");
|
|
@@ -57,7 +74,7 @@ export class TTS {
|
|
|
57
74
|
prepare: (s) => this.prepare(s),
|
|
58
75
|
checkOpen: () => this.checkOpen(),
|
|
59
76
|
forget: (s) => this.streams.delete(s),
|
|
60
|
-
}, text, voice, language, format, timeout(options.timeout, this.defaultTimeout));
|
|
77
|
+
}, text, voice, language, format, timeout(options.timeout, this.defaultTimeout), timestamps, lexicon);
|
|
61
78
|
this.streams.add(audio);
|
|
62
79
|
return audio;
|
|
63
80
|
}
|
|
@@ -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
|
+
}
|
package/dist/tts/policy.d.ts
CHANGED
|
@@ -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,5 +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;
|
package/dist/tts/policy.js
CHANGED
|
@@ -1,11 +1,13 @@
|
|
|
1
|
+
import { endpointAddress } from "../validation.js";
|
|
2
|
+
export { timeout } from "../validation.js";
|
|
1
3
|
import { RimeInputError } from "../errors.js";
|
|
2
4
|
const codaHostname = "coda.api.rime.ai";
|
|
3
5
|
export const policy = {
|
|
6
|
+
model: "coda",
|
|
4
7
|
target: `${codaHostname}:443`,
|
|
5
8
|
audience: codaHostname,
|
|
6
9
|
defaultVoice: "clementine",
|
|
7
10
|
exchangeUrl: "https://themis.api.rime.ai/v1/token",
|
|
8
|
-
sentenceBytes: 65536,
|
|
9
11
|
sourceChars: 1024,
|
|
10
12
|
outputBytes: 96000,
|
|
11
13
|
outputChunkBytes: 9600,
|
|
@@ -18,7 +20,7 @@ export const policy = {
|
|
|
18
20
|
cleanupTimeout: 2,
|
|
19
21
|
};
|
|
20
22
|
export function resolve(model, endpoint) {
|
|
21
|
-
const deployment = { ...policy };
|
|
23
|
+
const deployment = { ...policy, model };
|
|
22
24
|
if (model === "mistv3") {
|
|
23
25
|
const hostname = "mist.api.rime.ai";
|
|
24
26
|
deployment.target = `${hostname}:443`;
|
|
@@ -30,32 +32,7 @@ export function resolve(model, endpoint) {
|
|
|
30
32
|
}
|
|
31
33
|
if (endpoint === undefined || endpoint === null)
|
|
32
34
|
return deployment;
|
|
33
|
-
|
|
34
|
-
if (typeof endpoint !== "string" || /\s/.test(endpoint))
|
|
35
|
-
throw new RimeInputError(error);
|
|
36
|
-
const [host, port, extra] = endpoint.split(":");
|
|
37
|
-
if (!host ||
|
|
38
|
-
host.length > 253 ||
|
|
39
|
-
extra !== undefined ||
|
|
40
|
-
host
|
|
41
|
-
.split(".")
|
|
42
|
-
.some((label) => !/^[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?$/i.test(label)) ||
|
|
43
|
-
(port !== undefined &&
|
|
44
|
-
(!/^[0-9]{1,5}$/.test(port) || Number(port) < 1 || Number(port) > 65535)))
|
|
45
|
-
throw new RimeInputError(error);
|
|
46
|
-
return {
|
|
47
|
-
...deployment,
|
|
48
|
-
target: `${host.toLowerCase()}:${port === undefined ? 443 : Number(port)}`,
|
|
49
|
-
audience: host.toLowerCase(),
|
|
50
|
-
};
|
|
51
|
-
}
|
|
52
|
-
export function timeout(value, inherited = null) {
|
|
53
|
-
if (value === undefined)
|
|
54
|
-
return inherited;
|
|
55
|
-
if (value !== null &&
|
|
56
|
-
(typeof value !== "number" || !Number.isFinite(value) || value <= 0))
|
|
57
|
-
throw new RimeInputError("timeout must be finite positive seconds or null");
|
|
58
|
-
return value;
|
|
35
|
+
return { ...deployment, ...endpointAddress(endpoint) };
|
|
59
36
|
}
|
|
60
37
|
export function nonempty(value, name) {
|
|
61
38
|
if (typeof value !== "string" || !value.trim())
|
package/dist/tts/stream.d.ts
CHANGED
|
@@ -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,8 @@ 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;
|
|
17
21
|
private readonly controller;
|
|
18
22
|
private readonly queue;
|
|
19
23
|
private worker;
|
|
@@ -31,9 +35,14 @@ export declare class AudioStream implements AsyncIterableIterator<Uint8Array> {
|
|
|
31
35
|
private readonly operationId;
|
|
32
36
|
private bytesReceived;
|
|
33
37
|
private bytesDelivered;
|
|
34
|
-
constructor(key: symbol, owner: StreamOwner, source: TextSource | null, voice: string, language: string, formatValue: AudioFormat, timeout: number | null);
|
|
38
|
+
constructor(key: symbol, owner: StreamOwner, source: TextSource | null, voice: string, language: string, formatValue: AudioFormat, timeout: number | null, timestampsRequested?: boolean, customLexicon?: readonly PronunciationEntry[]);
|
|
35
39
|
get format(): AudioFormat;
|
|
36
40
|
get requestId(): string | null;
|
|
41
|
+
/** Read requested timestamps after consuming all audio; never drains audio.
|
|
42
|
+
* Calling before iteration completes rejects with RimeInputError. Synthesis
|
|
43
|
+
* errors reject here too; alignment failures are returned in status.
|
|
44
|
+
*/
|
|
45
|
+
timestamps(): Promise<TimestampResult>;
|
|
37
46
|
private start;
|
|
38
47
|
private fail;
|
|
39
48
|
private produce;
|
package/dist/tts/stream.js
CHANGED
|
@@ -5,7 +5,6 @@ import { Converter } from "./audio.js";
|
|
|
5
5
|
import { policy } from "./policy.js";
|
|
6
6
|
import { abortable } from "../cancellation.js";
|
|
7
7
|
import { ByteQueue } from "./queue.js";
|
|
8
|
-
import { SentenceBuffer, ready } from "./sentences.js";
|
|
9
8
|
import { SynthesisCall } from "./transport.js";
|
|
10
9
|
import { RimeError, RimeInputError, RimeCancelledError, RimeTimeoutError, RimeStreamError, } from "../errors.js";
|
|
11
10
|
export const constructionKey = Symbol("private stream constructor");
|
|
@@ -16,6 +15,8 @@ export class AudioStream {
|
|
|
16
15
|
language;
|
|
17
16
|
formatValue;
|
|
18
17
|
timeout;
|
|
18
|
+
timestampsRequested;
|
|
19
|
+
customLexicon;
|
|
19
20
|
controller = new AbortController();
|
|
20
21
|
queue = new ByteQueue(policy.outputBytes, policy.outputChunkBytes);
|
|
21
22
|
worker = null;
|
|
@@ -33,15 +34,17 @@ export class AudioStream {
|
|
|
33
34
|
operationId = randomUUID();
|
|
34
35
|
bytesReceived = 0;
|
|
35
36
|
bytesDelivered = 0;
|
|
36
|
-
constructor(key, owner, source, voice, language, formatValue, timeout) {
|
|
37
|
+
constructor(key, owner, source, voice, language, formatValue, timeout, timestampsRequested = false, customLexicon = []) {
|
|
37
38
|
this.owner = owner;
|
|
38
39
|
this.source = source;
|
|
39
40
|
this.voice = voice;
|
|
40
41
|
this.language = language;
|
|
41
42
|
this.formatValue = formatValue;
|
|
42
43
|
this.timeout = timeout;
|
|
44
|
+
this.timestampsRequested = timestampsRequested;
|
|
45
|
+
this.customLexicon = customLexicon;
|
|
43
46
|
if (key !== constructionKey)
|
|
44
|
-
throw new TypeError("AudioStream is returned by client.tts.stream()");
|
|
47
|
+
throw new TypeError("AudioStream is returned by client.tts.synthesize() or client.tts.stream()");
|
|
45
48
|
}
|
|
46
49
|
get format() {
|
|
47
50
|
return this.formatValue;
|
|
@@ -49,6 +52,19 @@ export class AudioStream {
|
|
|
49
52
|
get requestId() {
|
|
50
53
|
return this.call?.requestId ?? null;
|
|
51
54
|
}
|
|
55
|
+
/** Read requested timestamps after consuming all audio; never drains audio.
|
|
56
|
+
* Calling before iteration completes rejects with RimeInputError. Synthesis
|
|
57
|
+
* errors reject here too; alignment failures are returned in status.
|
|
58
|
+
*/
|
|
59
|
+
async timestamps() {
|
|
60
|
+
if (!this.timestampsRequested)
|
|
61
|
+
throw new RimeInputError("Enable timestamps: true when creating the stream");
|
|
62
|
+
if (this.failure)
|
|
63
|
+
throw this.failure;
|
|
64
|
+
if (!this.finished)
|
|
65
|
+
throw new RimeInputError("Consume all audio before reading timestamps");
|
|
66
|
+
return this.call.timestampResult();
|
|
67
|
+
}
|
|
52
68
|
start() {
|
|
53
69
|
if (this.failure)
|
|
54
70
|
throw this.failure;
|
|
@@ -96,15 +112,11 @@ export class AudioStream {
|
|
|
96
112
|
async produce() {
|
|
97
113
|
const source = this.source;
|
|
98
114
|
let iterator;
|
|
99
|
-
|
|
115
|
+
let highSurrogate = "";
|
|
100
116
|
let meaningful = false;
|
|
101
117
|
try {
|
|
102
118
|
try {
|
|
103
|
-
iterator = (
|
|
104
|
-
? (async function* () {
|
|
105
|
-
yield source;
|
|
106
|
-
})()
|
|
107
|
-
: source)[Symbol.asyncIterator]();
|
|
119
|
+
iterator = source[Symbol.asyncIterator]();
|
|
108
120
|
}
|
|
109
121
|
catch (error) {
|
|
110
122
|
if (this.controller.signal.aborted)
|
|
@@ -131,35 +143,39 @@ export class AudioStream {
|
|
|
131
143
|
}
|
|
132
144
|
if (result.done)
|
|
133
145
|
break;
|
|
134
|
-
|
|
135
|
-
if (typeof chunk !== "string")
|
|
146
|
+
if (typeof result.value !== "string")
|
|
136
147
|
throw new RimeInputError("The text source must yield strings");
|
|
148
|
+
let chunk = highSurrogate + result.value;
|
|
149
|
+
highSurrogate = "";
|
|
150
|
+
// A JavaScript source may split one Unicode character across yields.
|
|
151
|
+
if (/[\uD800-\uDBFF]$/.test(chunk)) {
|
|
152
|
+
highSurrogate = chunk.slice(-1);
|
|
153
|
+
chunk = chunk.slice(0, -1);
|
|
154
|
+
}
|
|
137
155
|
if (chunk.trim())
|
|
138
156
|
meaningful = true;
|
|
139
|
-
//
|
|
157
|
+
// Bound wire messages without splitting a Unicode character.
|
|
140
158
|
for (let offset = 0; offset < chunk.length;) {
|
|
141
159
|
let end = Math.min(chunk.length, offset + policy.sourceChars);
|
|
142
160
|
if (end < chunk.length && /[\uD800-\uDBFF]/.test(chunk[end - 1]))
|
|
143
161
|
end++;
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
162
|
+
const part = chunk.slice(offset, end);
|
|
163
|
+
if (/[\uD800-\uDFFF]/u.test(part))
|
|
164
|
+
throw new RimeInputError("Text must contain valid Unicode");
|
|
165
|
+
if (!this.submitted)
|
|
166
|
+
this.progressAt = performance.now();
|
|
167
|
+
this.submitted = true;
|
|
168
|
+
await this.call.write(part);
|
|
169
|
+
this.progressAt = performance.now();
|
|
150
170
|
offset = end;
|
|
151
171
|
}
|
|
152
172
|
}
|
|
173
|
+
if (highSurrogate)
|
|
174
|
+
throw new RimeInputError("Text must contain valid Unicode");
|
|
153
175
|
if (!meaningful)
|
|
154
176
|
throw new RimeInputError("The text source contained no meaningful text");
|
|
155
|
-
for (const sentence of buffer.feed("", true)) {
|
|
156
|
-
if (!this.submitted)
|
|
157
|
-
this.progressAt = performance.now();
|
|
158
|
-
this.submitted = true;
|
|
159
|
-
await this.call.write(sentence);
|
|
160
|
-
}
|
|
161
|
-
this.call.finishInput();
|
|
162
177
|
this.inputDone = true;
|
|
178
|
+
this.call.finishInput();
|
|
163
179
|
}
|
|
164
180
|
catch (error) {
|
|
165
181
|
if (error instanceof RimeError || this.controller.signal.aborted)
|
|
@@ -200,10 +216,9 @@ export class AudioStream {
|
|
|
200
216
|
async run() {
|
|
201
217
|
let tasks = [];
|
|
202
218
|
try {
|
|
203
|
-
await abortable(ready, this.controller.signal);
|
|
204
219
|
const prepared = await this.owner.prepare(this.controller.signal);
|
|
205
220
|
this.call = new SynthesisCall(prepared, this.controller.signal);
|
|
206
|
-
await this.call.start(this.voice, this.language);
|
|
221
|
+
await this.call.start(this.voice, this.language, this.timestampsRequested, this.customLexicon);
|
|
207
222
|
tasks = [this.produce(), this.read()];
|
|
208
223
|
await Promise.all(tasks);
|
|
209
224
|
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
|
+
}
|
package/dist/tts/transport.d.ts
CHANGED
|
@@ -1,33 +1,29 @@
|
|
|
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 readonly
|
|
19
|
-
private readonly
|
|
20
|
-
private readonly statusPromise;
|
|
21
|
-
private status;
|
|
22
|
-
private id;
|
|
17
|
+
private timestampsRequested;
|
|
18
|
+
private readonly timestampTrailer;
|
|
19
|
+
private readonly rpc;
|
|
23
20
|
constructor(prepared: PreparedConnection, signal: AbortSignal);
|
|
24
21
|
get requestId(): string | null;
|
|
25
22
|
get done(): boolean;
|
|
26
23
|
cancel(): void;
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
write(sentence: string): Promise<void>;
|
|
24
|
+
start(voice: string, language: string, timestamps?: boolean, customLexicon?: readonly PronunciationEntry[]): Promise<void>;
|
|
25
|
+
timestampResult(): import("./timestamps.js").TimestampResult;
|
|
26
|
+
write(text: string): Promise<void>;
|
|
31
27
|
finishInput(): void;
|
|
32
28
|
private checkFormat;
|
|
33
29
|
audio(): AsyncGenerator<Uint8Array>;
|