@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.
Files changed (85) hide show
  1. package/CHANGELOG.md +155 -0
  2. package/LICENSE-PRISM +201 -0
  3. package/NOTICE-PRISM +9 -0
  4. package/README.md +279 -114
  5. package/dist/auth.d.ts +9 -3
  6. package/dist/auth.js +12 -4
  7. package/dist/cancellation.d.ts +6 -0
  8. package/dist/cancellation.js +54 -0
  9. package/dist/client.d.ts +24 -23
  10. package/dist/client.js +41 -92
  11. package/dist/grpc.d.ts +27 -0
  12. package/dist/grpc.js +175 -0
  13. package/dist/index.d.ts +10 -2
  14. package/dist/index.js +6 -2
  15. package/dist/pcm.d.ts +21 -0
  16. package/dist/pcm.js +52 -0
  17. package/dist/realtime/async.d.ts +23 -0
  18. package/dist/realtime/async.js +87 -0
  19. package/dist/realtime/audio.d.ts +2 -0
  20. package/dist/realtime/audio.js +11 -0
  21. package/dist/realtime/client.d.ts +12 -0
  22. package/dist/realtime/client.js +139 -0
  23. package/dist/realtime/conversation.d.ts +47 -0
  24. package/dist/realtime/conversation.js +321 -0
  25. package/dist/realtime/faults.d.ts +2 -0
  26. package/dist/realtime/faults.js +25 -0
  27. package/dist/realtime/protocol.d.ts +99 -0
  28. package/dist/realtime/protocol.js +432 -0
  29. package/dist/realtime/session.d.ts +79 -0
  30. package/dist/realtime/session.js +808 -0
  31. package/dist/realtime/settings.d.ts +11 -0
  32. package/dist/realtime/settings.js +142 -0
  33. package/dist/realtime/transport.d.ts +16 -0
  34. package/dist/realtime/transport.js +86 -0
  35. package/dist/realtime/types.d.ts +292 -0
  36. package/dist/realtime/types.js +12 -0
  37. package/dist/stt/audio.d.ts +15 -0
  38. package/dist/stt/audio.js +46 -0
  39. package/dist/stt/client.d.ts +17 -0
  40. package/dist/stt/client.js +121 -0
  41. package/dist/stt/policy.d.ts +11 -0
  42. package/dist/stt/policy.js +11 -0
  43. package/dist/stt/protocol.d.ts +14 -0
  44. package/dist/stt/protocol.js +76 -0
  45. package/dist/stt/queue.d.ts +16 -0
  46. package/dist/stt/queue.js +45 -0
  47. package/dist/stt/stream.d.ts +48 -0
  48. package/dist/stt/stream.js +224 -0
  49. package/dist/stt/transport.d.ts +15 -0
  50. package/dist/stt/transport.js +94 -0
  51. package/dist/stt/types.d.ts +51 -0
  52. package/dist/stt/types.js +1 -0
  53. package/dist/{audio.d.ts → tts/audio.d.ts} +9 -0
  54. package/dist/{audio.js → tts/audio.js} +44 -4
  55. package/dist/tts/buffer.d.ts +3 -0
  56. package/dist/tts/buffer.js +53 -0
  57. package/dist/tts/client.d.ts +53 -0
  58. package/dist/tts/client.js +151 -0
  59. package/dist/tts/lexicon.d.ts +7 -0
  60. package/dist/tts/lexicon.js +23 -0
  61. package/dist/{policy.d.ts → tts/policy.d.ts} +2 -3
  62. package/dist/tts/policy.js +41 -0
  63. package/dist/{stream.d.ts → tts/stream.d.ts} +15 -2
  64. package/dist/{stream.js → tts/stream.js} +54 -30
  65. package/dist/tts/timestamps.d.ts +24 -0
  66. package/dist/tts/timestamps.js +46 -0
  67. package/dist/{transport.d.ts → tts/transport.d.ts} +10 -13
  68. package/dist/tts/transport.js +133 -0
  69. package/dist/validation.d.ts +5 -0
  70. package/dist/validation.js +28 -0
  71. package/docs/realtime.md +537 -0
  72. package/docs/stt.md +186 -0
  73. package/docs/tts.md +284 -0
  74. package/package.json +14 -12
  75. package/dist/policy.js +0 -82
  76. package/dist/sentences.d.ts +0 -21
  77. package/dist/sentences.js +0 -142
  78. package/dist/transport.js +0 -223
  79. package/vendor/LICENSE.blingfire +0 -21
  80. package/vendor/README.md +0 -11
  81. package/vendor/blingfire.cjs +0 -4
  82. package/vendor/blingfire.upstream.cjs +0 -1
  83. package/vendor/blingfire.wasm +0 -0
  84. /package/dist/{queue.d.ts → tts/queue.d.ts} +0 -0
  85. /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, abortable } from "./policy.js";
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 "./errors.js";
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
- const buffer = new SentenceBuffer(policy.sentenceBytes);
123
+ let highSurrogate = "";
99
124
  let meaningful = false;
100
125
  try {
101
126
  try {
102
- iterator = (typeof source === "string"
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
- const chunk = result.value;
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
- // Never split a surrogate pair when limiting native detector input size.
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
- for (const sentence of buffer.feed(chunk.slice(offset, end))) {
144
- if (!this.submitted)
145
- this.progressAt = performance.now();
146
- this.submitted = true;
147
- await this.call.write(sentence);
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 * as errors from "./errors.js";
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 signal;
18
- private readonly call;
19
- private readonly metadataPromise;
20
- private readonly statusPromise;
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
- private error;
28
- private writeMessage;
29
- start(voice: string, language: string): Promise<void>;
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,5 @@
1
+ export declare function endpointAddress(endpoint: string): {
2
+ target: string;
3
+ audience: string;
4
+ };
5
+ export declare function timeout(value: unknown, inherited?: number | null): number | null;
@@ -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
+ }