@rimelabs/sdk 0.1.0-alpha.15 → 0.1.0-alpha.17

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 CHANGED
@@ -1,5 +1,19 @@
1
1
  # Changelog
2
2
 
3
+ ## [0.1.0-alpha.17](https://github.com/rimelabs/rime-sdk/compare/typescript-v0.1.0-alpha.16...typescript-v0.1.0-alpha.17) (2026-10-16)
4
+
5
+
6
+ ### Features
7
+
8
+ * add unary STT transcription to all SDKs ([#80](https://github.com/rimelabs/rime-sdk/issues/80)) ([39427eb](https://github.com/rimelabs/rime-sdk/commit/39427eba7738e4f2fe0514a2cddaa219137dcc4d))
9
+
10
+ ## [0.1.0-alpha.16](https://github.com/rimelabs/rime-sdk/compare/typescript-v0.1.0-alpha.15...typescript-v0.1.0-alpha.16) (2026-10-16)
11
+
12
+
13
+ ### Features
14
+
15
+ * **realtime:** expose Prism's speech hint ([#73](https://github.com/rimelabs/rime-sdk/issues/73)) ([cb7187d](https://github.com/rimelabs/rime-sdk/commit/cb7187d687c2210332fee704d43ac9ba45c760ea))
16
+
3
17
  ## [0.1.0-alpha.15](https://github.com/rimelabs/rime-sdk/compare/typescript-v0.1.0-alpha.14...typescript-v0.1.0-alpha.15) (2026-10-13)
4
18
 
5
19
 
package/README.md CHANGED
@@ -224,12 +224,51 @@ and model/language support.
224
224
 
225
225
  ## STT
226
226
 
227
+ ### Transcribe a complete WAV recording
228
+
229
+ For a complete recording, use the native unary `Transcribe` RPC through the SDK.
230
+ Pass **complete WAV bytes, including the header**, up to **16 MiB (16,777,216
231
+ bytes)**. PCM16 mono 16 kHz WAV is the portable format. The SDK sends the file
232
+ unchanged; it does not decode, resample, or add a header. The service validates
233
+ its encoding and the supplied language and recognition hints.
234
+
235
+ The result is a `Transcription` containing final text, resolved language, and a
236
+ request ID when available. Silence may return empty text. There are no partial
237
+ updates or automatic retries. TTS client settings do not apply. An optional
238
+ overall timeout includes connection and upload; the SDK also bounds connection
239
+ time and the unary request (120 seconds). Timeout, cancellation, and service
240
+ errors use the existing SDK error categories. A request ID is included when the
241
+ transport has made it available.
242
+
243
+ ```typescript
244
+ import { readFile } from "node:fs/promises";
245
+ import { Rime } from "@rimelabs/sdk";
246
+
247
+ const client = new Rime();
248
+ try {
249
+ const result = await client.stt.transcribe(await readFile("utterance.wav"), {
250
+ language: "en",
251
+ mode: "written",
252
+ contextTerms: ["Super-G", "USOC"],
253
+ timeout: 120,
254
+ });
255
+ console.log(result.text, result.language, result.requestId);
256
+ } finally {
257
+ await client.close();
258
+ }
259
+ ```
260
+
261
+ `TranscribeOptions` also accepts an `AbortSignal` as `signal`. Aborting it or
262
+ closing the client cancels pending requests with `RimeCancelledError`.
263
+
264
+ ### Stream an utterance
265
+
227
266
  Use `client.stt.stream(audioChunks, options)` to transcribe one utterance.
228
267
  The required source is an async iterable of headerless signed PCM16 little-endian
229
268
  `Uint8Array` chunks (`Buffer` also works). Ending the source ends the utterance;
230
269
  your application decides when to stop.
231
270
 
232
- STT uses streaming gRPC over TLS to `stt.api.rime.ai:443`, with the client's API
271
+ STT uses gRPC over TLS to `stt.api.rime.ai:443`, with the client's API
233
272
  key. There is no STT model, endpoint, or protocol selector. TTS client settings,
234
273
  including its timeout, do not apply to STT.
235
274
 
package/dist/client.d.ts CHANGED
@@ -1,4 +1,4 @@
1
- import type { AudioSource, TranscriptionOptions } from "./stt/types.js";
1
+ import type { AudioSource, TranscriptionOptions, TranscribeOptions, Transcription } from "./stt/types.js";
2
2
  import type { TranscriptStream } from "./stt/stream.js";
3
3
  import { type SynthesisOptions, type DiscoveryOptions, type VoiceListOptions } from "./tts/client.js";
4
4
  import { type AudioStream, type TextSource } from "./tts/stream.js";
@@ -17,6 +17,8 @@ export declare class Rime {
17
17
  private readonly ttsClient;
18
18
  private readonly sttClient;
19
19
  readonly stt: {
20
+ /** Transcribe complete WAV bytes. PCM16 mono 16 kHz is the portable format; maximum 16 MiB. */
21
+ transcribe: (audio: Uint8Array, options: TranscribeOptions) => Promise<Transcription>;
20
22
  stream: (audio: AudioSource, options: TranscriptionOptions) => TranscriptStream;
21
23
  };
22
24
  private readonly realtimeClient;
package/dist/client.js CHANGED
@@ -27,6 +27,7 @@ export class Rime {
27
27
  this.credentials = new Credentials(key, deployment);
28
28
  this.sttClient = new STT(this.credentials, () => this.checkOpen());
29
29
  this.stt = {
30
+ transcribe: (audio, options) => this.sttClient.transcribe(audio, options),
30
31
  stream: (audio, options) => this.sttClient.stream(audio, options),
31
32
  };
32
33
  this.realtimeClient = new Realtime(this.credentials, () => this.checkOpen());
package/dist/index.d.ts CHANGED
@@ -7,4 +7,4 @@ export * from "./errors.js";
7
7
  export { RealtimeSession } from "./realtime/session.js";
8
8
  export * from "./realtime/types.js";
9
9
  export { TranscriptStream } from "./stt/stream.js";
10
- export type { AudioSource, TranscriptionOptions, TranscriptionMode, TranscriptionPartial, TranscriptionFinal, TranscriptionUpdate, } from "./stt/types.js";
10
+ export type { AudioSource, TranscribeOptions, Transcription, TranscriptionOptions, TranscriptionMode, TranscriptionPartial, TranscriptionFinal, TranscriptionUpdate, } from "./stt/types.js";
@@ -192,6 +192,12 @@ function decodeValue(value, sessionId) {
192
192
  case "prism.typed_input.ready":
193
193
  payload = { kind: "input.ready" };
194
194
  break;
195
+ case "prism.input_audio_buffer.speech_hint":
196
+ payload = {
197
+ kind: "speech.hint",
198
+ audioStartMs: integer(event.audio_start_ms, "audio_start_ms"),
199
+ };
200
+ break;
195
201
  case "input_audio_buffer.speech_started":
196
202
  payload = {
197
203
  kind: "speech.started",
@@ -81,6 +81,13 @@ export declare class RimeRealtimeError extends RimeError {
81
81
  export interface InputReady {
82
82
  readonly kind: "input.ready";
83
83
  }
84
+ /** The user may have started speaking over a playing reply. Prism sends it
85
+ * before `speech.started` when it may interrupt the reply; it can be a false
86
+ * alarm, so lower playback on it rather than stopping. */
87
+ export interface SpeechHint {
88
+ readonly kind: "speech.hint";
89
+ readonly audioStartMs: number;
90
+ }
84
91
  export interface SpeechStarted {
85
92
  readonly kind: "speech.started";
86
93
  readonly itemId: string;
@@ -164,7 +171,7 @@ export interface FaultEvent {
164
171
  readonly kind: "error";
165
172
  readonly error: RealtimeFault;
166
173
  }
167
- export type InputEvent = InputReady | SpeechStarted | SpeechStopped | InputCommitted | TranscriptDelta | TranscriptFinal | TranscriptFailed;
174
+ export type InputEvent = InputReady | SpeechHint | SpeechStarted | SpeechStopped | InputCommitted | TranscriptDelta | TranscriptFinal | TranscriptFailed;
168
175
  export type ResponseEvent = ResponseStarted | ResponseAbandoned | MessageStarted | TextDelta | TextDone | AudioDelta | AudioDone | ToolCall | ResponseEnded;
169
176
  export interface SessionEvent {
170
177
  readonly sessionId: string;
@@ -1,12 +1,17 @@
1
1
  import { Credentials } from "../auth.js";
2
2
  import { TranscriptStream } from "./stream.js";
3
- import type { AudioSource, TranscriptionOptions } from "./types.js";
3
+ import type { AudioSource, TranscriptionOptions, TranscribeOptions, Transcription } from "./types.js";
4
4
  export declare class STT {
5
5
  private readonly credentials;
6
6
  private readonly checkOpen;
7
7
  private connection;
8
8
  private streams;
9
+ private readonly cancellation;
10
+ private readonly requests;
9
11
  constructor(credentials: Credentials, checkOpen: () => void);
12
+ /** Transcribe complete WAV bytes (at most 16 MiB), without replay or retries. */
13
+ transcribe(audio: Uint8Array, options: TranscribeOptions): Promise<Transcription>;
14
+ private prepare;
10
15
  stream(source: AudioSource, options: TranscriptionOptions): TranscriptStream;
11
16
  close(): Promise<void>;
12
17
  }
@@ -1,8 +1,9 @@
1
1
  import { connect, metadata } from "../grpc.js";
2
2
  import { timeout } from "../validation.js";
3
- import { RimeInputError } from "../errors.js";
3
+ import { RimeInputError, RimeAudioFormatError, RimeResourceLimitError, RimeCancelledError, RimeTimeoutError, } from "../errors.js";
4
+ import { scope } from "../cancellation.js";
4
5
  import { resolvePCMFormat } from "../pcm.js";
5
- import { transport, requireSchema } from "./transport.js";
6
+ import { transport, requireSchema, transcribe, MAX_WAV_BYTES, } from "./transport.js";
6
7
  import { TranscriptStream } from "./stream.js";
7
8
  import { policy } from "./policy.js";
8
9
  export class STT {
@@ -10,40 +11,72 @@ export class STT {
10
11
  checkOpen;
11
12
  connection = null;
12
13
  streams = new Set();
14
+ cancellation = new AbortController();
15
+ requests = new Set();
13
16
  constructor(credentials, checkOpen) {
14
17
  this.credentials = credentials;
15
18
  this.checkOpen = checkOpen;
16
19
  }
20
+ /** Transcribe complete WAV bytes (at most 16 MiB), without replay or retries. */
21
+ transcribe(audio, options) {
22
+ this.checkOpen();
23
+ if (!(audio instanceof Uint8Array) || audio.byteLength === 0)
24
+ throw new RimeAudioFormatError("audio must contain complete WAV bytes");
25
+ if (audio.byteLength > MAX_WAV_BYTES)
26
+ throw new RimeResourceLimitError("WAV input exceeds 16 MiB");
27
+ validateOptions(options);
28
+ const settings = {
29
+ ...options,
30
+ contextTerms: [...(options.contextTerms ?? [])],
31
+ };
32
+ const data = Buffer.from(audio);
33
+ const deadline = scope([options.signal, this.cancellation.signal], timeout(options.timeout) ?? undefined, new RimeTimeoutError("Overall transcription deadline expired"));
34
+ const operation = (async () => {
35
+ try {
36
+ const prepared = await this.prepare(deadline.signal, policy.target);
37
+ return await transcribe(prepared.client, prepared.metadata, data, settings, deadline.signal);
38
+ }
39
+ catch (error) {
40
+ if (deadline.signal.aborted &&
41
+ !(error instanceof RimeTimeoutError) &&
42
+ !(error instanceof RimeCancelledError)) {
43
+ if (deadline.signal.reason instanceof RimeTimeoutError)
44
+ throw deadline.signal.reason;
45
+ throw new RimeCancelledError("Transcription cancelled");
46
+ }
47
+ throw error;
48
+ }
49
+ finally {
50
+ deadline.dispose();
51
+ }
52
+ })();
53
+ this.requests.add(operation);
54
+ void operation.then(() => this.requests.delete(operation), () => this.requests.delete(operation));
55
+ return operation;
56
+ }
57
+ async prepare(signal, target) {
58
+ this.checkOpen();
59
+ requireSchema();
60
+ signal.throwIfAborted();
61
+ const headers = metadata(this.credentials.authorization());
62
+ this.connection ??= transport.makeClient(target);
63
+ await connect(this.connection, signal, policy.connectionTimeout);
64
+ signal.throwIfAborted();
65
+ return { client: this.connection, metadata: headers };
66
+ }
17
67
  stream(source, options) {
18
68
  this.checkOpen();
19
69
  if (!source || typeof source[Symbol.asyncIterator] !== "function")
20
70
  throw new RimeInputError("audio must be an async iterable of Uint8Array chunks");
21
- if (!options || typeof options.language !== "string")
22
- throw new RimeInputError("language must be a string");
71
+ validateOptions(options);
23
72
  const mode = options.mode ?? "written";
24
- if (mode !== "written" && mode !== "verbatim")
25
- throw new RimeInputError("mode must be written or verbatim");
26
73
  const terms = options.contextTerms ?? [];
27
- if (!Array.isArray(terms) || terms.some((term) => typeof term !== "string"))
28
- throw new RimeInputError("contextTerms must be an array of strings");
29
74
  const format = resolvePCMFormat(options.inputFormat);
30
75
  const target = policy.target;
31
76
  const stream = new TranscriptStream({
32
77
  checkOpen: this.checkOpen,
33
78
  forget: (stream) => this.streams.delete(stream),
34
- prepare: async (signal) => {
35
- this.checkOpen();
36
- requireSchema();
37
- signal.throwIfAborted();
38
- const headers = metadata(this.credentials.authorization());
39
- this.connection ??= transport.makeClient(target);
40
- await connect(this.connection, signal, policy.connectionTimeout);
41
- signal.throwIfAborted();
42
- return {
43
- client: this.connection,
44
- metadata: headers,
45
- };
46
- },
79
+ prepare: (signal) => this.prepare(signal, target),
47
80
  }, source, {
48
81
  language: options.language,
49
82
  mode,
@@ -56,8 +89,12 @@ export class STT {
56
89
  return stream;
57
90
  }
58
91
  async close() {
92
+ this.cancellation.abort(new RimeCancelledError("Client closed"));
59
93
  try {
60
- const results = await Promise.allSettled([...this.streams].map((stream) => stream.cancel()));
94
+ const results = await Promise.allSettled([
95
+ ...[...this.streams].map((stream) => stream.cancel()),
96
+ ...[...this.requests].map((request) => request.catch(() => { })),
97
+ ]);
61
98
  for (const result of results)
62
99
  if (result.status === "rejected")
63
100
  throw result.reason;
@@ -67,3 +104,13 @@ export class STT {
67
104
  }
68
105
  }
69
106
  }
107
+ function validateOptions(options) {
108
+ if (!options || typeof options.language !== "string")
109
+ throw new RimeInputError("language must be a string");
110
+ const mode = options.mode ?? "written";
111
+ if (mode !== "written" && mode !== "verbatim")
112
+ throw new RimeInputError("mode must be written or verbatim");
113
+ const terms = options.contextTerms ?? [];
114
+ if (!Array.isArray(terms) || terms.some((term) => typeof term !== "string"))
115
+ throw new RimeInputError("contextTerms must be an array of strings");
116
+ }
@@ -1,11 +1,13 @@
1
1
  import * as grpc from "@grpc/grpc-js";
2
2
  import * as schema from "@rimelabs/api";
3
3
  import { BidiCall } from "../grpc.js";
4
- import type { TranscriptionMode } from "./types.js";
4
+ import type { TranscriptionMode, TranscribeOptions, Transcription } from "./types.js";
5
+ export declare const MAX_WAV_BYTES: number;
5
6
  export declare function requireSchema(): void;
6
7
  export declare const transport: {
7
8
  makeClient: (target: string) => grpc.Client;
8
9
  };
10
+ export declare function transcribe(client: grpc.Client, auth: grpc.Metadata, audio: Uint8Array, options: TranscribeOptions, signal: AbortSignal): Promise<Transcription>;
9
11
  export declare class TranscriptionCall extends BidiCall<schema.StreamingTranscriptionRequest, schema.StreamingTranscriptionResponse> {
10
12
  constructor(client: grpc.Client, auth: grpc.Metadata, signal: AbortSignal);
11
13
  start(language: string, mode: TranscriptionMode, contextTerms: readonly string[]): Promise<void>;
@@ -1,9 +1,11 @@
1
1
  import * as grpc from "@grpc/grpc-js";
2
2
  import { create } from "@bufbuild/protobuf";
3
3
  import * as schema from "@rimelabs/api";
4
- import { BidiCall, serializer, deserializer } from "../grpc.js";
5
- import { RimeInputError } from "../errors.js";
4
+ import { BidiCall, serializer, deserializer, requestId, nativeError, } from "../grpc.js";
5
+ import { abortable, scope } from "../cancellation.js";
6
+ import { RimeInputError, RimeStreamError, RimeResourceLimitError, RimeTimeoutError, RimeCancelledError, } from "../errors.js";
6
7
  import { policy } from "./policy.js";
8
+ export const MAX_WAV_BYTES = 16 * 1024 * 1024;
7
9
  export function requireSchema() {
8
10
  if (!schema.SpeechToText)
9
11
  throw new RimeInputError("STT requires @rimelabs/api with SpeechToText definitions");
@@ -12,9 +14,54 @@ export const transport = {
12
14
  makeClient: (target) => new grpc.Client(target, grpc.credentials.createSsl(), {
13
15
  "grpc.enable_retries": 0,
14
16
  "grpc.max_receive_message_length": policy.receiveBytes,
15
- "grpc.max_send_message_length": 131072,
17
+ "grpc.max_send_message_length": MAX_WAV_BYTES + 65536,
16
18
  }),
17
19
  };
20
+ export async function transcribe(client, auth, audio, options, signal) {
21
+ const method = schema.SpeechToText.method.transcribe;
22
+ const deadline = scope([signal], policy.completionTimeout, new RimeTimeoutError("Transcription request timed out"));
23
+ let call;
24
+ let id = null;
25
+ try {
26
+ deadline.signal.throwIfAborted();
27
+ const result = await abortable(new Promise((resolve, reject) => {
28
+ call = client.makeUnaryRequest(`/${schema.SpeechToText.typeName}/${method.name}`, serializer(method.input), deserializer(method.output), create(method.input, {
29
+ audio,
30
+ options: {
31
+ language: options.language,
32
+ mode: options.mode === "verbatim"
33
+ ? schema.TranscriptionMode.VERBATIM
34
+ : schema.TranscriptionMode.WRITTEN,
35
+ contextTerms: [...(options.contextTerms ?? [])],
36
+ },
37
+ }), auth, (error, response) => (error ? reject(error) : resolve(response)));
38
+ call.on("metadata", (headers) => {
39
+ id = requestId(headers);
40
+ });
41
+ call.on("status", (status) => {
42
+ id ??= requestId(status.metadata);
43
+ });
44
+ }), deadline.signal);
45
+ if (!result.language?.tag || !result.language.source)
46
+ throw new RimeStreamError("The service did not confirm a resolved language", id);
47
+ if (Buffer.byteLength(result.text) > policy.transcriptBytes)
48
+ throw new RimeResourceLimitError("The transcript exceeds the SDK text limit", id);
49
+ return { text: result.text, language: result.language.tag, requestId: id };
50
+ }
51
+ catch (error) {
52
+ if (deadline.signal.aborted) {
53
+ const reason = deadline.signal.reason;
54
+ if (reason instanceof RimeTimeoutError)
55
+ throw new RimeTimeoutError(reason.message, id);
56
+ throw new RimeCancelledError("Transcription cancelled", id);
57
+ }
58
+ throw nativeError(error, id);
59
+ }
60
+ finally {
61
+ call?.cancel();
62
+ deadline.dispose();
63
+ }
64
+ }
18
65
  export class TranscriptionCall extends BidiCall {
19
66
  constructor(client, auth, signal) {
20
67
  requireSchema();
@@ -32,3 +32,11 @@ export interface TranscriptionFinal {
32
32
  readonly language: string;
33
33
  }
34
34
  export type TranscriptionUpdate = TranscriptionPartial | TranscriptionFinal;
35
+ /** Options for a complete WAV recording. Audio is sent unchanged. */
36
+ export type TranscribeOptions = Omit<TranscriptionOptions, "inputFormat">;
37
+ /** A successful whole-recording transcription. Silence may produce empty text. */
38
+ export interface Transcription {
39
+ readonly text: string;
40
+ readonly language: string;
41
+ readonly requestId: string | null;
42
+ }
package/docs/realtime.md CHANGED
@@ -130,6 +130,7 @@ are exported from `@rimelabs/sdk`.
130
130
  | Kind | Payload fields |
131
131
  | --- | --- |
132
132
  | `input.ready` | No data fields. Turn methods handle the readiness wait. |
133
+ | `speech.hint` | `audioStartMs`; the user may have started speaking over a playing reply. It can be a false alarm: lower playback, do not stop it. |
133
134
  | `speech.started`, `speech.stopped` | `itemId` and `audioStartMs` or `audioEndMs` |
134
135
  | `input.committed` | `itemId` |
135
136
  | `transcript.delta`, `transcript.final` | `itemId` and `delta` or `text` |
package/docs/stt.md CHANGED
@@ -1,4 +1,43 @@
1
- # Streaming speech recognition
1
+ # Speech recognition
2
+
3
+ ## Complete recordings
4
+
5
+ For a complete recording, use the native unary `Transcribe` RPC through the SDK.
6
+ Pass **complete WAV bytes, including the header**, up to **16 MiB (16,777,216
7
+ bytes)**. PCM16 mono 16 kHz WAV is the portable format. The SDK sends the file
8
+ unchanged; it does not decode, resample, or add a header. The service validates
9
+ its encoding and the supplied language and recognition hints.
10
+
11
+ The result is a `Transcription` containing final text, resolved language, and a
12
+ request ID when available. Silence may return empty text. There are no partial
13
+ updates or automatic retries. TTS client settings do not apply. An optional
14
+ overall timeout includes connection and upload; the SDK also bounds connection
15
+ time and the unary request (120 seconds). Timeout, cancellation, and service
16
+ errors use the existing SDK error categories. A request ID is included when the
17
+ transport has made it available.
18
+
19
+ ```typescript
20
+ import { readFile } from "node:fs/promises";
21
+ import { Rime } from "@rimelabs/sdk";
22
+
23
+ const client = new Rime();
24
+ try {
25
+ const result = await client.stt.transcribe(await readFile("utterance.wav"), {
26
+ language: "en",
27
+ mode: "written",
28
+ contextTerms: ["Super-G", "USOC"],
29
+ timeout: 120,
30
+ });
31
+ console.log(result.text, result.language, result.requestId);
32
+ } finally {
33
+ await client.close();
34
+ }
35
+ ```
36
+
37
+ `TranscribeOptions` also accepts an `AbortSignal` as `signal`. Aborting it or
38
+ closing the client cancels pending requests with `RimeCancelledError`.
39
+
40
+ ## Streaming recognition
2
41
 
3
42
  `client.stt.stream` transcribes one utterance from an async source of `Uint8Array` chunks (including Node buffers).
4
43
  The SDK handles the gRPC protocol, authentication, audio conversion, transcript
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rimelabs/sdk",
3
- "version": "0.1.0-alpha.15",
3
+ "version": "0.1.0-alpha.17",
4
4
  "description": "Rime streaming TTS, STT and Prism realtime SDK for Node.js",
5
5
  "license": "MIT",
6
6
  "type": "module",