@rimelabs/sdk 0.1.0-alpha.1 → 0.1.0-alpha.12

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 (67) hide show
  1. package/CHANGELOG.md +96 -11
  2. package/LICENSE-PRISM +201 -0
  3. package/NOTICE-PRISM +9 -0
  4. package/README.md +175 -63
  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 +23 -23
  10. package/dist/client.js +39 -92
  11. package/dist/grpc.d.ts +27 -0
  12. package/dist/{transport.js → grpc.js} +36 -106
  13. package/dist/index.d.ts +6 -2
  14. package/dist/index.js +5 -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 +156 -0
  23. package/dist/realtime/protocol.d.ts +80 -0
  24. package/dist/realtime/protocol.js +354 -0
  25. package/dist/realtime/session.d.ts +67 -0
  26. package/dist/realtime/session.js +731 -0
  27. package/dist/realtime/transport.d.ts +16 -0
  28. package/dist/realtime/transport.js +83 -0
  29. package/dist/realtime/types.d.ts +186 -0
  30. package/dist/realtime/types.js +10 -0
  31. package/dist/stt/audio.d.ts +15 -0
  32. package/dist/stt/audio.js +46 -0
  33. package/dist/stt/client.d.ts +13 -0
  34. package/dist/stt/client.js +73 -0
  35. package/dist/stt/policy.d.ts +11 -0
  36. package/dist/stt/policy.js +11 -0
  37. package/dist/stt/protocol.d.ts +13 -0
  38. package/dist/stt/protocol.js +61 -0
  39. package/dist/stt/queue.d.ts +16 -0
  40. package/dist/stt/queue.js +45 -0
  41. package/dist/stt/stream.d.ts +48 -0
  42. package/dist/stt/stream.js +224 -0
  43. package/dist/stt/transport.d.ts +13 -0
  44. package/dist/stt/transport.js +45 -0
  45. package/dist/stt/types.d.ts +34 -0
  46. package/dist/stt/types.js +1 -0
  47. package/dist/{audio.js → tts/audio.js} +3 -2
  48. package/dist/tts/client.d.ts +39 -0
  49. package/dist/tts/client.js +121 -0
  50. package/dist/{policy.d.ts → tts/policy.d.ts} +2 -2
  51. package/dist/tts/policy.js +41 -0
  52. package/dist/{sentences.js → tts/sentences.js} +2 -2
  53. package/dist/{stream.d.ts → tts/stream.d.ts} +1 -1
  54. package/dist/{stream.js → tts/stream.js} +5 -8
  55. package/dist/{transport.d.ts → tts/transport.d.ts} +3 -11
  56. package/dist/tts/transport.js +116 -0
  57. package/dist/validation.d.ts +5 -0
  58. package/dist/validation.js +28 -0
  59. package/docs/realtime.md +220 -0
  60. package/docs/stt.md +103 -0
  61. package/docs/tts.md +139 -0
  62. package/package.json +14 -6
  63. package/dist/policy.js +0 -73
  64. /package/dist/{audio.d.ts → tts/audio.d.ts} +0 -0
  65. /package/dist/{queue.d.ts → tts/queue.d.ts} +0 -0
  66. /package/dist/{queue.js → tts/queue.js} +0 -0
  67. /package/dist/{sentences.d.ts → tts/sentences.d.ts} +0 -0
package/dist/auth.js CHANGED
@@ -1,6 +1,6 @@
1
- import { policy, abortable } from "./policy.js";
1
+ import { abortable } from "./cancellation.js";
2
2
  import { RimeError, RimeAuthenticationError, RimePermissionError, RimeTimeoutError, RimeCancelledError, RimeResourceLimitError, RimeUnavailableError, } from "./errors.js";
3
- export async function exchangeKey(key, signal, configuration = policy) {
3
+ export async function exchangeKey(key, signal, configuration) {
4
4
  try {
5
5
  const response = await fetch(configuration.exchangeUrl, {
6
6
  method: "POST",
@@ -77,17 +77,25 @@ export class Credentials {
77
77
  refreshAt = 0;
78
78
  controller = new AbortController();
79
79
  closed = false;
80
- constructor(key, configuration = policy) {
80
+ constructor(key, configuration) {
81
81
  this.key = key;
82
82
  this.configuration = configuration;
83
83
  }
84
+ authorization() {
85
+ if (this.closed)
86
+ throw new RimeAuthenticationError("Credentials are closed");
87
+ return `Bearer ${this.key}`;
88
+ }
89
+ realtimeAuthorization() {
90
+ return this.authorization();
91
+ }
84
92
  async metadata(signal) {
85
93
  // TEMPORARY until Themis is ready: replace this body with the call below.
86
94
  // return this.themisMetadata(signal);
87
95
  if (this.closed)
88
96
  throw new RimeAuthenticationError("Credentials are closed");
89
97
  signal.throwIfAborted();
90
- return `Bearer ${this.key}`;
98
+ return this.authorization();
91
99
  }
92
100
  async themisMetadata(signal) {
93
101
  if (this.closed)
@@ -0,0 +1,6 @@
1
+ export declare function abortable<T>(promise: PromiseLike<T>, signal: AbortSignal): Promise<T>;
2
+ /** A disposable deadline avoids retaining timers and caller abort listeners. */
3
+ export declare function scope(signals: (AbortSignal | undefined)[], seconds?: number, error?: Error): {
4
+ signal: AbortSignal;
5
+ dispose(): void;
6
+ };
@@ -0,0 +1,54 @@
1
+ export function abortable(promise, signal) {
2
+ if (signal.aborted)
3
+ return Promise.reject(signal.reason);
4
+ return new Promise((resolve, reject) => {
5
+ const abort = () => {
6
+ signal.removeEventListener("abort", abort);
7
+ reject(signal.reason);
8
+ };
9
+ signal.addEventListener("abort", abort, { once: true });
10
+ Promise.resolve(promise).then((value) => {
11
+ signal.removeEventListener("abort", abort);
12
+ resolve(value);
13
+ }, (error) => {
14
+ signal.removeEventListener("abort", abort);
15
+ reject(error);
16
+ });
17
+ });
18
+ }
19
+ /** A disposable deadline avoids retaining timers and caller abort listeners. */
20
+ export function scope(signals, seconds, error) {
21
+ const controller = new AbortController();
22
+ const handlers = new Map();
23
+ for (const signal of new Set(signals)) {
24
+ if (!signal)
25
+ continue;
26
+ const abort = () => controller.abort(signal.reason);
27
+ if (signal.aborted)
28
+ abort();
29
+ else {
30
+ signal.addEventListener("abort", abort, { once: true });
31
+ handlers.set(signal, abort);
32
+ }
33
+ }
34
+ let timer;
35
+ if (seconds !== undefined) {
36
+ const end = performance.now() + seconds * 1000;
37
+ const expire = () => {
38
+ const remaining = end - performance.now();
39
+ if (remaining <= 0)
40
+ controller.abort(error);
41
+ else
42
+ timer = setTimeout(expire, Math.min(remaining, 2147483647));
43
+ };
44
+ expire();
45
+ }
46
+ return {
47
+ signal: controller.signal,
48
+ dispose() {
49
+ clearTimeout(timer);
50
+ for (const [signal, handler] of handlers)
51
+ signal.removeEventListener("abort", handler);
52
+ },
53
+ };
54
+ }
package/dist/client.d.ts CHANGED
@@ -1,33 +1,36 @@
1
- import { AudioFormat } from "./audio.js";
2
- import { AudioStream, type TextSource } from "./stream.js";
1
+ import type { AudioSource, TranscriptionOptions } from "./stt/types.js";
2
+ import type { TranscriptStream } from "./stt/stream.js";
3
+ import { type SynthesisOptions, type DiscoveryOptions, type VoiceListOptions } from "./tts/client.js";
4
+ import { type AudioStream, type TextSource } from "./tts/stream.js";
5
+ import type { RealtimeConnectOptions } from "./realtime/types.js";
6
+ import type { RealtimeSession } from "./realtime/session.js";
7
+ export type { SynthesisOptions, DiscoveryOptions, VoiceListOptions, } from "./tts/client.js";
3
8
  export interface RimeOptions {
4
9
  apiKey?: string | null;
10
+ /** Select "coda" (default) or "mistv3" for speech and discovery. */
5
11
  model?: string;
6
12
  endpoint?: string | null;
7
13
  timeout?: number | null;
8
- }
9
- export interface SynthesisOptions {
10
- voice?: string | null;
11
- language?: string;
12
- audioFormat?: AudioFormat | null;
13
- timeout?: number | null;
14
- }
15
- export interface DiscoveryOptions {
16
- timeout?: number | null;
17
- }
18
- export interface VoiceListOptions extends DiscoveryOptions {
19
- language?: string | null;
14
+ /** Recognition hostname and optional port; defaults to stt.api.rime.ai:443. */
15
+ sttEndpoint?: string | null;
20
16
  }
21
17
  export declare class Rime {
22
- private readonly deployment;
23
- private credentials;
24
- private client;
18
+ private readonly credentials;
19
+ private readonly ttsClient;
20
+ private readonly sttClient;
21
+ readonly stt: {
22
+ stream: (audio: AudioSource, options: TranscriptionOptions) => TranscriptStream;
23
+ };
24
+ private readonly realtimeClient;
25
+ readonly realtime: {
26
+ connect: (options: RealtimeConnectOptions) => Promise<RealtimeSession>;
27
+ };
25
28
  private closed;
26
- private streams;
27
- private discoveryControllers;
28
29
  private closing;
29
- private defaultTimeout;
30
30
  readonly tts: {
31
+ /** Synthesize complete text and return streaming audio. Do not await this call. */
32
+ synthesize: (text: string, options?: SynthesisOptions) => AudioStream;
33
+ /** Supply incremental text and return streaming audio. Do not await this call. */
31
34
  stream: (text: TextSource, options?: SynthesisOptions) => AudioStream;
32
35
  };
33
36
  readonly voices: {
@@ -38,9 +41,6 @@ export declare class Rime {
38
41
  };
39
42
  constructor(options?: RimeOptions);
40
43
  private checkOpen;
41
- private prepare;
42
- private stream;
43
- private discover;
44
44
  close(): Promise<void>;
45
45
  [Symbol.asyncDispose](): Promise<void>;
46
46
  }
package/dist/client.js CHANGED
@@ -1,18 +1,18 @@
1
- import { AudioFormat } from "./audio.js";
2
- import { AudioStream, constructionKey } from "./stream.js";
1
+ import { STT } from "./stt/client.js";
3
2
  import { Credentials } from "./auth.js";
4
- import { policy, resolve, timeout, nonempty, abortable } from "./policy.js";
5
- import { transport, connect, metadata, discover, } from "./transport.js";
6
- import { RimeAuthenticationError, RimeInputError, RimeAudioFormatError, RimeCancelledError, RimeTimeoutError, RimeUnavailableError, } from "./errors.js";
3
+ import { RimeAuthenticationError, RimeInputError } from "./errors.js";
4
+ import { TTS, } from "./tts/client.js";
5
+ import { resolve, timeout } from "./tts/policy.js";
6
+ import { Realtime } from "./realtime/client.js";
7
7
  export class Rime {
8
- deployment;
9
8
  credentials;
10
- client = null;
9
+ ttsClient;
10
+ sttClient;
11
+ stt;
12
+ realtimeClient;
13
+ realtime;
11
14
  closed = false;
12
- streams = new Set();
13
- discoveryControllers = new Set();
14
15
  closing = null;
15
- defaultTimeout;
16
16
  tts;
17
17
  voices;
18
18
  languages;
@@ -22,99 +22,46 @@ export class Rime {
22
22
  : options.apiKey;
23
23
  if (typeof key !== "string" || !key.trim())
24
24
  throw new RimeAuthenticationError("Provide apiKey or set RIME_API_KEY");
25
- this.defaultTimeout = timeout(options.timeout);
26
- this.deployment = resolve(options.model ?? "coda", options.endpoint);
27
- this.credentials = new Credentials(key, this.deployment);
28
- this.tts = { stream: (text, options = {}) => this.stream(text, options) };
29
- this.voices = {
30
- list: async (options = {}) => {
31
- if (options.language !== undefined && options.language !== null)
32
- nonempty(options.language, "language");
33
- return this.discover("voices", options.language, options.timeout);
34
- },
25
+ const defaultTimeout = timeout(options.timeout);
26
+ const deployment = resolve(options.model ?? "coda", options.endpoint);
27
+ this.credentials = new Credentials(key, deployment);
28
+ this.sttClient = new STT(this.credentials, options.sttEndpoint, () => this.checkOpen());
29
+ this.stt = {
30
+ stream: (audio, options) => this.sttClient.stream(audio, options),
35
31
  };
36
- this.languages = {
37
- list: (options = {}) => this.discover("languages", undefined, options.timeout),
32
+ this.realtimeClient = new Realtime(this.credentials, () => this.checkOpen());
33
+ this.realtime = {
34
+ connect: (options) => this.realtimeClient.connect(options),
38
35
  };
36
+ this.ttsClient = new TTS(this.credentials, deployment, defaultTimeout, () => this.checkOpen());
37
+ this.tts = {
38
+ synthesize: (text, options) => this.ttsClient.synthesize(text, options),
39
+ stream: (text, options) => this.ttsClient.stream(text, options),
40
+ };
41
+ this.voices = this.ttsClient.voices;
42
+ this.languages = this.ttsClient.languages;
39
43
  }
40
44
  checkOpen() {
41
45
  if (this.closed)
42
46
  throw new RimeInputError("The Rime client is closed");
43
47
  }
44
- async prepare(signal) {
45
- this.checkOpen();
46
- const auth = await this.credentials.metadata(signal);
47
- this.checkOpen();
48
- this.client ??= transport.makeClient(this.deployment.target);
49
- await connect(this.client, signal);
50
- return { client: this.client, metadata: metadata(auth) };
51
- }
52
- stream(text, options) {
53
- this.checkOpen();
54
- if (typeof text === "string") {
55
- if (!text.trim())
56
- throw new RimeInputError("Text must contain non-whitespace characters");
57
- }
58
- else if (!text || typeof text[Symbol.asyncIterator] !== "function")
59
- throw new RimeInputError("text must be a string or an async iterable of strings");
60
- const voice = options.voice === undefined || options.voice === null
61
- ? "clementine"
62
- : nonempty(options.voice, "voice");
63
- const language = nonempty(options.language ?? "en", "language");
64
- const format = options.audioFormat ?? AudioFormat.PCM_24000;
65
- if (format !== AudioFormat.PCM_24000 && format !== AudioFormat.MULAW_8000)
66
- throw new RimeAudioFormatError("Select a named AudioFormat profile");
67
- const audio = new AudioStream(constructionKey, {
68
- prepare: (s) => this.prepare(s),
69
- checkOpen: () => this.checkOpen(),
70
- forget: (s) => this.streams.delete(s),
71
- }, text, voice, language, format, timeout(options.timeout, this.defaultTimeout));
72
- this.streams.add(audio);
73
- return audio;
74
- }
75
- async discover(kind, language, override) {
76
- this.checkOpen();
77
- const budget = Math.min(timeout(override, this.defaultTimeout) ?? Infinity, policy.discoveryTimeout);
78
- const controller = new AbortController();
79
- let requestId = null;
80
- this.discoveryControllers.add(controller);
81
- const timer = setTimeout(() => controller.abort(new RimeTimeoutError("Discovery deadline expired")), budget * 1000);
82
- try {
83
- const prepared = await this.prepare(controller.signal);
84
- for (let attempt = 0;; attempt++) {
85
- requestId = null;
86
- try {
87
- return await discover(prepared, kind, language, controller.signal);
88
- }
89
- catch (error) {
90
- if (!(error instanceof RimeUnavailableError) || attempt === 2)
91
- throw error;
92
- requestId = error.requestId;
93
- await abortable(new Promise((resolve) => setTimeout(resolve, 50 * 2 ** attempt)), controller.signal);
94
- }
95
- }
96
- }
97
- catch (error) {
98
- if (error instanceof RimeTimeoutError && requestId !== null)
99
- throw new RimeTimeoutError(error.message, requestId, {
100
- cause: error.cause,
101
- });
102
- throw error;
103
- }
104
- finally {
105
- clearTimeout(timer);
106
- this.discoveryControllers.delete(controller);
107
- }
108
- }
109
48
  async close() {
110
49
  if (!this.closing) {
111
50
  this.closed = true;
112
51
  this.closing = (async () => {
113
- for (const controller of this.discoveryControllers)
114
- controller.abort(new RimeCancelledError("Client closed during discovery"));
115
- await Promise.all([...this.streams].map((stream) => stream.cancel()));
116
- await this.credentials.close();
117
- this.client?.close();
52
+ try {
53
+ const results = await Promise.allSettled([
54
+ this.ttsClient.close(),
55
+ this.sttClient.close(),
56
+ this.realtimeClient.close(),
57
+ ]);
58
+ for (const result of results)
59
+ if (result.status === "rejected")
60
+ throw result.reason;
61
+ }
62
+ finally {
63
+ await this.credentials.close();
64
+ }
118
65
  })();
119
66
  }
120
67
  await this.closing;
package/dist/grpc.d.ts ADDED
@@ -0,0 +1,27 @@
1
+ import * as grpc from "@grpc/grpc-js";
2
+ import { type DescMessage, type MessageShape } from "@bufbuild/protobuf";
3
+ import * as errors from "./errors.js";
4
+ export declare function rpcError(code: number, requestId?: string | null): errors.RimeError;
5
+ export declare function serializer<D extends DescMessage>(desc: D): (message: MessageShape<D>) => Buffer<ArrayBuffer>;
6
+ export declare function deserializer<D extends DescMessage>(desc: D): (data: Buffer) => MessageShape<D>;
7
+ export declare function requestId(metadata?: grpc.Metadata): string | null;
8
+ export declare function nativeError(error: unknown, id: string | null): errors.RimeError;
9
+ export declare class BidiCall<Input, Output> {
10
+ private readonly call;
11
+ private signal;
12
+ private readonly metadataPromise;
13
+ private readonly statusPromise;
14
+ private status;
15
+ private id;
16
+ constructor(call: grpc.ClientDuplexStream<Input, Output>, signal: AbortSignal);
17
+ get requestId(): string | null;
18
+ get done(): boolean;
19
+ cancel(): void;
20
+ private error;
21
+ write(message: Input): Promise<void>;
22
+ finishInput(): void;
23
+ headers(): Promise<grpc.Metadata>;
24
+ responses(): AsyncGenerator<Output>;
25
+ }
26
+ export declare function connect(client: grpc.Client, signal: AbortSignal, seconds: number): Promise<void>;
27
+ export declare function metadata(value: string): grpc.Metadata;
@@ -1,7 +1,6 @@
1
+ import { abortable } from "./cancellation.js";
1
2
  import * as grpc from "@grpc/grpc-js";
2
- import { create, fromBinary, toBinary, } from "@bufbuild/protobuf";
3
- import * as schema from "@rimelabs/api";
4
- import { policy, abortable } from "./policy.js";
3
+ import { fromBinary, toBinary, } from "@bufbuild/protobuf";
5
4
  import * as errors from "./errors.js";
6
5
  export function rpcError(code, requestId = null) {
7
6
  const classes = {
@@ -16,53 +15,16 @@ export function rpcError(code, requestId = null) {
16
15
  const ErrorType = classes[code] ?? errors.RimeStreamError;
17
16
  return new ErrorType(`Rime operation failed: ${grpc.status[code] ?? "UNKNOWN"}`, requestId);
18
17
  }
19
- export const transport = {
20
- makeClient: (target) => new grpc.Client(target, grpc.credentials.createSsl(), {
21
- "grpc.enable_retries": 0,
22
- "grpc.max_receive_message_length": policy.receiveBytes,
23
- "grpc.max_send_message_length": policy.sentenceBytes + 65536,
24
- }),
25
- };
26
- function serializer(desc) {
18
+ export function serializer(desc) {
27
19
  return (message) => Buffer.from(toBinary(desc, message));
28
20
  }
29
- function deserializer(desc) {
21
+ export function deserializer(desc) {
30
22
  return (data) => fromBinary(desc, data);
31
23
  }
32
- export async function connect(client, signal) {
33
- await abortable(new Promise((resolve, reject) => client.waitForReady(Date.now() + policy.connectionTimeout * 1000, (error) => error
34
- ? reject(new errors.RimeTimeoutError("Connection establishment timed out"))
35
- : resolve())), signal);
36
- }
37
- export function metadata(value) {
38
- const result = new grpc.Metadata();
39
- result.set("authorization", value);
40
- return result;
41
- }
42
- function openStream(client, auth) {
43
- return client.makeBidiStreamRequest("/rime.TextToSpeech/SynthesizeStreaming", serializer(schema.StreamingSynthesisRequestSchema), deserializer(schema.SynthesisResponseStreamSchema), auth);
44
- }
45
- function header(voice, language) {
46
- return create(schema.StreamingSynthesisRequestSchema, {
47
- payload: {
48
- case: "header",
49
- value: create(schema.SynthesisRequestSchema, {
50
- speaker: voice,
51
- language,
52
- audioParameters: { audioFormat: "audio/pcm", samplingRate: 24000 },
53
- }),
54
- },
55
- });
56
- }
57
- function textMessage(text) {
58
- return create(schema.StreamingSynthesisRequestSchema, {
59
- payload: { case: "textChunk", value: text },
60
- });
61
- }
62
- function requestId(metadata) {
24
+ export function requestId(metadata) {
63
25
  return metadata?.get("x-request-id")[0]?.toString() ?? null;
64
26
  }
65
- function nativeError(error, id) {
27
+ export function nativeError(error, id) {
66
28
  if (error instanceof errors.RimeError)
67
29
  return error;
68
30
  if (error &&
@@ -72,56 +34,20 @@ function nativeError(error, id) {
72
34
  const metadata = "metadata" in error ? error.metadata : null;
73
35
  return rpcError(error.code, id ?? (metadata instanceof grpc.Metadata ? requestId(metadata) : null));
74
36
  }
75
- return new errors.RimeStreamError("Coda transport failed", id, {
37
+ return new errors.RimeStreamError("Rime transport failed", id, {
76
38
  cause: error,
77
39
  });
78
40
  }
79
- async function unary(prepared, name, input, output, request, signal) {
80
- let call;
81
- let id = null;
82
- try {
83
- signal.throwIfAborted();
84
- const operation = new Promise((resolve, reject) => {
85
- call = prepared.client.makeUnaryRequest("/rime.TextToSpeech/" + name, serializer(input), deserializer(output), request, prepared.metadata, (error, result) => (error ? reject(error) : resolve(result)));
86
- call.once("metadata", (metadata) => {
87
- id = requestId(metadata);
88
- });
89
- });
90
- return await abortable(operation, signal);
91
- }
92
- catch (error) {
93
- call?.cancel();
94
- if (signal.aborted) {
95
- if (signal.reason instanceof errors.RimeTimeoutError)
96
- throw new errors.RimeTimeoutError(signal.reason.message, id, {
97
- cause: signal.reason.cause,
98
- });
99
- throw signal.reason;
100
- }
101
- throw nativeError(error, id);
102
- }
103
- }
104
- export async function discover(prepared, kind, language, signal) {
105
- // Exactly one attempt. The client owns retries and the shared deadline.
106
- if (kind === "voices") {
107
- const method = schema.TextToSpeech.method.getSupportedSpeakers;
108
- const response = await unary(prepared, method.name, method.input, method.output, create(method.input, { language: language ?? undefined }), signal);
109
- return response.speakers;
110
- }
111
- const method = schema.TextToSpeech.method.getSupportedLanguages;
112
- const response = await unary(prepared, method.name, method.input, method.output, create(method.input), signal);
113
- return response.languages;
114
- }
115
- export class SynthesisCall {
116
- signal;
41
+ export class BidiCall {
117
42
  call;
43
+ signal;
118
44
  metadataPromise;
119
45
  statusPromise;
120
46
  status = null;
121
47
  id = null;
122
- constructor(prepared, signal) {
48
+ constructor(call, signal) {
49
+ this.call = call;
123
50
  this.signal = signal;
124
- this.call = openStream(prepared.client, prepared.metadata);
125
51
  this.metadataPromise = new Promise((resolve, reject) => {
126
52
  this.call.once("metadata", (metadata) => {
127
53
  this.id = requestId(metadata);
@@ -155,7 +81,7 @@ export class SynthesisCall {
155
81
  this.id ??= failure.requestId;
156
82
  return failure;
157
83
  }
158
- async writeMessage(message) {
84
+ async write(message) {
159
85
  try {
160
86
  this.signal.throwIfAborted();
161
87
  if (this.status) {
@@ -180,13 +106,6 @@ export class SynthesisCall {
180
106
  throw this.error(error);
181
107
  }
182
108
  }
183
- async start(voice, language) {
184
- // Writes do not depend on response metadata.
185
- await this.writeMessage(header(voice, language));
186
- }
187
- async write(sentence) {
188
- await this.writeMessage(textMessage(sentence));
189
- }
190
109
  finishInput() {
191
110
  try {
192
111
  this.call.end();
@@ -195,24 +114,25 @@ export class SynthesisCall {
195
114
  throw this.error(error);
196
115
  }
197
116
  }
198
- checkFormat(contentType) {
199
- if (contentType !== "audio/pcm")
200
- throw new errors.RimeAudioFormatError("Expected raw audio/pcm from the service", this.id);
117
+ async headers() {
118
+ try {
119
+ return await abortable(this.metadataPromise, this.signal);
120
+ }
121
+ catch (error) {
122
+ if (this.signal.aborted)
123
+ throw this.signal.reason;
124
+ throw this.error(error);
125
+ }
201
126
  }
202
- async *audio() {
127
+ async *responses() {
203
128
  try {
204
- const metadata = await abortable(this.metadataPromise, this.signal);
205
- const contentType = metadata.get("x-rime-audio-content-type")[0];
206
- for await (const response of this.call) {
207
- if (response.audio.length)
208
- this.checkFormat(contentType);
209
- yield response.audio;
210
- }
129
+ await this.headers();
130
+ const responses = this.call;
131
+ for await (const response of responses)
132
+ yield response;
211
133
  const status = await abortable(this.statusPromise, this.signal);
212
134
  if (status.code !== grpc.status.OK)
213
135
  throw rpcError(status.code, this.id);
214
- // Validate empty success only after giving a rejection its proper error.
215
- this.checkFormat(contentType);
216
136
  }
217
137
  catch (error) {
218
138
  if (this.signal.aborted)
@@ -221,3 +141,13 @@ export class SynthesisCall {
221
141
  }
222
142
  }
223
143
  }
144
+ export async function connect(client, signal, seconds) {
145
+ await abortable(new Promise((resolve, reject) => client.waitForReady(Date.now() + seconds * 1000, (error) => error
146
+ ? reject(new errors.RimeTimeoutError("Connection establishment timed out"))
147
+ : resolve())), signal);
148
+ }
149
+ export function metadata(value) {
150
+ const result = new grpc.Metadata();
151
+ result.set("authorization", value);
152
+ return result;
153
+ }
package/dist/index.d.ts CHANGED
@@ -1,4 +1,8 @@
1
1
  export { Rime, type RimeOptions, type SynthesisOptions, type DiscoveryOptions, type VoiceListOptions, } from "./client.js";
2
- export { AudioStream } from "./stream.js";
3
- export { AudioFormat } from "./audio.js";
2
+ export { AudioStream } from "./tts/stream.js";
3
+ export { AudioFormat } from "./tts/audio.js";
4
4
  export * from "./errors.js";
5
+ export { RealtimeSession } from "./realtime/session.js";
6
+ export * from "./realtime/types.js";
7
+ export { TranscriptStream } from "./stt/stream.js";
8
+ export type { AudioSource, TranscriptionOptions, TranscriptionMode, TranscriptionPartial, TranscriptionFinal, TranscriptionUpdate, } from "./stt/types.js";
package/dist/index.js CHANGED
@@ -1,4 +1,7 @@
1
1
  export { Rime, } from "./client.js";
2
- export { AudioStream } from "./stream.js";
3
- export { AudioFormat } from "./audio.js";
2
+ export { AudioStream } from "./tts/stream.js";
3
+ export { AudioFormat } from "./tts/audio.js";
4
4
  export * from "./errors.js";
5
+ export { RealtimeSession } from "./realtime/session.js";
6
+ export * from "./realtime/types.js";
7
+ export { TranscriptStream } from "./stt/stream.js";
package/dist/pcm.d.ts ADDED
@@ -0,0 +1,21 @@
1
+ /** Headerless signed little-endian PCM16; stereo samples are interleaved. */
2
+ export interface PCMFormat {
3
+ /** Input frames per second; omitted means 16000. */
4
+ readonly sampleRate?: 8000 | 16000 | 24000 | 48000;
5
+ /** Samples per frame; omitted means one (mono). */
6
+ readonly channels?: 1 | 2;
7
+ /** Two-byte signed samples with no file header. */
8
+ readonly encoding?: "pcm_s16le";
9
+ }
10
+ export declare function resolvePCMFormat(input?: PCMFormat): Required<PCMFormat>;
11
+ /** Streaming linear interpolation, matching Python audioop.ratecv with no filter.
12
+ * Integer phase remains bounded and preserves samples across chunk boundaries.
13
+ */
14
+ export declare class InputConverter {
15
+ readonly format: Required<PCMFormat>;
16
+ private phase;
17
+ private previous;
18
+ constructor(format: Required<PCMFormat>);
19
+ clone(): InputConverter;
20
+ process(input: Uint8Array): Buffer;
21
+ }
package/dist/pcm.js ADDED
@@ -0,0 +1,52 @@
1
+ import { RimeAudioFormatError } from "./errors.js";
2
+ export function resolvePCMFormat(input) {
3
+ if (input !== undefined &&
4
+ (input === null || typeof input !== "object" || Array.isArray(input)))
5
+ throw new RimeAudioFormatError("Input format must be a PCM format object");
6
+ const format = {
7
+ sampleRate: input?.sampleRate ?? 16000,
8
+ channels: input?.channels ?? 1,
9
+ encoding: input?.encoding ?? "pcm_s16le",
10
+ };
11
+ if (![8000, 16000, 24000, 48000].includes(format.sampleRate) ||
12
+ ![1, 2].includes(format.channels) ||
13
+ format.encoding !== "pcm_s16le")
14
+ throw new RimeAudioFormatError("Use PCM16 at 8, 16, 24 or 48 kHz, with one or two channels");
15
+ return format;
16
+ }
17
+ /** Streaming linear interpolation, matching Python audioop.ratecv with no filter.
18
+ * Integer phase remains bounded and preserves samples across chunk boundaries.
19
+ */
20
+ export class InputConverter {
21
+ format;
22
+ phase = -16000;
23
+ previous = 0;
24
+ constructor(format) {
25
+ this.format = format;
26
+ }
27
+ clone() {
28
+ const copy = new InputConverter(this.format);
29
+ copy.phase = this.phase;
30
+ copy.previous = this.previous;
31
+ return copy;
32
+ }
33
+ process(input) {
34
+ const data = Buffer.from(input.buffer, input.byteOffset, input.byteLength);
35
+ const samples = [];
36
+ for (let offset = 0; offset < data.length; offset += this.format.channels * 2) {
37
+ const current = this.format.channels === 1
38
+ ? data.readInt16LE(offset)
39
+ : Math.floor((data.readInt16LE(offset) + data.readInt16LE(offset + 2)) / 2);
40
+ this.phase += 16000;
41
+ while (this.phase >= 0) {
42
+ samples.push(Math.floor((this.previous * this.phase + current * (16000 - this.phase)) /
43
+ 16000));
44
+ this.phase -= this.format.sampleRate;
45
+ }
46
+ this.previous = current;
47
+ }
48
+ const output = Buffer.alloc(samples.length * 2);
49
+ samples.forEach((sample, index) => output.writeInt16LE(sample, index * 2));
50
+ return output;
51
+ }
52
+ }