@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.
- package/CHANGELOG.md +96 -11
- package/LICENSE-PRISM +201 -0
- package/NOTICE-PRISM +9 -0
- package/README.md +175 -63
- package/dist/auth.d.ts +9 -3
- package/dist/auth.js +12 -4
- package/dist/cancellation.d.ts +6 -0
- package/dist/cancellation.js +54 -0
- package/dist/client.d.ts +23 -23
- package/dist/client.js +39 -92
- package/dist/grpc.d.ts +27 -0
- package/dist/{transport.js → grpc.js} +36 -106
- package/dist/index.d.ts +6 -2
- package/dist/index.js +5 -2
- package/dist/pcm.d.ts +21 -0
- package/dist/pcm.js +52 -0
- package/dist/realtime/async.d.ts +23 -0
- package/dist/realtime/async.js +87 -0
- package/dist/realtime/audio.d.ts +2 -0
- package/dist/realtime/audio.js +11 -0
- package/dist/realtime/client.d.ts +12 -0
- package/dist/realtime/client.js +156 -0
- package/dist/realtime/protocol.d.ts +80 -0
- package/dist/realtime/protocol.js +354 -0
- package/dist/realtime/session.d.ts +67 -0
- package/dist/realtime/session.js +731 -0
- package/dist/realtime/transport.d.ts +16 -0
- package/dist/realtime/transport.js +83 -0
- package/dist/realtime/types.d.ts +186 -0
- package/dist/realtime/types.js +10 -0
- 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/{audio.js → tts/audio.js} +3 -2
- package/dist/tts/client.d.ts +39 -0
- package/dist/tts/client.js +121 -0
- package/dist/{policy.d.ts → tts/policy.d.ts} +2 -2
- package/dist/tts/policy.js +41 -0
- package/dist/{sentences.js → tts/sentences.js} +2 -2
- package/dist/{stream.d.ts → tts/stream.d.ts} +1 -1
- package/dist/{stream.js → tts/stream.js} +5 -8
- package/dist/{transport.d.ts → tts/transport.d.ts} +3 -11
- package/dist/tts/transport.js +116 -0
- package/dist/validation.d.ts +5 -0
- package/dist/validation.js +28 -0
- package/docs/realtime.md +220 -0
- package/docs/stt.md +103 -0
- package/docs/tts.md +139 -0
- package/package.json +14 -6
- package/dist/policy.js +0 -73
- /package/dist/{audio.d.ts → tts/audio.d.ts} +0 -0
- /package/dist/{queue.d.ts → tts/queue.d.ts} +0 -0
- /package/dist/{queue.js → tts/queue.js} +0 -0
- /package/dist/{sentences.d.ts → tts/sentences.d.ts} +0 -0
|
@@ -0,0 +1,116 @@
|
|
|
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
|
+
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": policy.sentenceBytes + 65536,
|
|
16
|
+
}),
|
|
17
|
+
};
|
|
18
|
+
export function connect(client, signal) {
|
|
19
|
+
return connectGrpc(client, signal, policy.connectionTimeout);
|
|
20
|
+
}
|
|
21
|
+
function openStream(client, auth) {
|
|
22
|
+
return client.makeBidiStreamRequest("/rime.TextToSpeech/SynthesizeStreaming", serializer(schema.StreamingSynthesisRequestSchema), deserializer(schema.SynthesisResponseStreamSchema), auth);
|
|
23
|
+
}
|
|
24
|
+
function header(voice, language) {
|
|
25
|
+
return create(schema.StreamingSynthesisRequestSchema, {
|
|
26
|
+
payload: {
|
|
27
|
+
case: "header",
|
|
28
|
+
value: create(schema.SynthesisRequestSchema, {
|
|
29
|
+
speaker: voice,
|
|
30
|
+
language,
|
|
31
|
+
audioParameters: { audioFormat: "audio/pcm", samplingRate: 24000 },
|
|
32
|
+
}),
|
|
33
|
+
},
|
|
34
|
+
});
|
|
35
|
+
}
|
|
36
|
+
function textMessage(text) {
|
|
37
|
+
return create(schema.StreamingSynthesisRequestSchema, {
|
|
38
|
+
payload: { case: "textChunk", value: text },
|
|
39
|
+
});
|
|
40
|
+
}
|
|
41
|
+
async function unary(prepared, name, input, output, request, signal) {
|
|
42
|
+
let call;
|
|
43
|
+
let id = null;
|
|
44
|
+
try {
|
|
45
|
+
signal.throwIfAborted();
|
|
46
|
+
const operation = new Promise((resolve, reject) => {
|
|
47
|
+
call = prepared.client.makeUnaryRequest("/rime.TextToSpeech/" + name, serializer(input), deserializer(output), request, prepared.metadata, (error, result) => (error ? reject(error) : resolve(result)));
|
|
48
|
+
call.once("metadata", (metadata) => {
|
|
49
|
+
id = requestId(metadata);
|
|
50
|
+
});
|
|
51
|
+
});
|
|
52
|
+
return await abortable(operation, signal);
|
|
53
|
+
}
|
|
54
|
+
catch (error) {
|
|
55
|
+
call?.cancel();
|
|
56
|
+
if (signal.aborted) {
|
|
57
|
+
if (signal.reason instanceof errors.RimeTimeoutError)
|
|
58
|
+
throw new errors.RimeTimeoutError(signal.reason.message, id, {
|
|
59
|
+
cause: signal.reason.cause,
|
|
60
|
+
});
|
|
61
|
+
throw signal.reason;
|
|
62
|
+
}
|
|
63
|
+
throw nativeError(error, id);
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
export async function discover(prepared, kind, language, signal) {
|
|
67
|
+
// Exactly one attempt. The client owns retries and the shared deadline.
|
|
68
|
+
if (kind === "voices") {
|
|
69
|
+
const method = schema.TextToSpeech.method.getSupportedSpeakers;
|
|
70
|
+
const response = await unary(prepared, method.name, method.input, method.output, create(method.input, { language: language ?? undefined }), signal);
|
|
71
|
+
return response.speakers;
|
|
72
|
+
}
|
|
73
|
+
const method = schema.TextToSpeech.method.getSupportedLanguages;
|
|
74
|
+
const response = await unary(prepared, method.name, method.input, method.output, create(method.input), signal);
|
|
75
|
+
return response.languages;
|
|
76
|
+
}
|
|
77
|
+
export class SynthesisCall {
|
|
78
|
+
rpc;
|
|
79
|
+
constructor(prepared, signal) {
|
|
80
|
+
this.rpc = new BidiCall(openStream(prepared.client, prepared.metadata), signal);
|
|
81
|
+
}
|
|
82
|
+
get requestId() {
|
|
83
|
+
return this.rpc.requestId;
|
|
84
|
+
}
|
|
85
|
+
get done() {
|
|
86
|
+
return this.rpc.done;
|
|
87
|
+
}
|
|
88
|
+
cancel() {
|
|
89
|
+
this.rpc.cancel();
|
|
90
|
+
}
|
|
91
|
+
start(voice, language) {
|
|
92
|
+
return this.rpc.write(header(voice, language));
|
|
93
|
+
}
|
|
94
|
+
write(sentence) {
|
|
95
|
+
return this.rpc.write(textMessage(sentence));
|
|
96
|
+
}
|
|
97
|
+
finishInput() {
|
|
98
|
+
this.rpc.finishInput();
|
|
99
|
+
}
|
|
100
|
+
checkFormat(contentType) {
|
|
101
|
+
if (contentType !== "audio/pcm")
|
|
102
|
+
throw new errors.RimeAudioFormatError("Expected raw audio/pcm from the service", this.requestId);
|
|
103
|
+
}
|
|
104
|
+
async *audio() {
|
|
105
|
+
const headers = await this.rpc.headers();
|
|
106
|
+
const contentType = headers.get("x-rime-audio-content-type")[0];
|
|
107
|
+
for await (const response of this.rpc.responses()) {
|
|
108
|
+
if (response.payload.case !== "audio")
|
|
109
|
+
continue;
|
|
110
|
+
if (response.payload.value.length)
|
|
111
|
+
this.checkFormat(contentType);
|
|
112
|
+
yield response.payload.value;
|
|
113
|
+
}
|
|
114
|
+
this.checkFormat(contentType);
|
|
115
|
+
}
|
|
116
|
+
}
|
|
@@ -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
|
+
}
|
package/docs/realtime.md
ADDED
|
@@ -0,0 +1,220 @@
|
|
|
1
|
+
# Realtime / Prism for Node.js
|
|
2
|
+
|
|
3
|
+
Keep one event consumer running while your application sends input, runs tools,
|
|
4
|
+
and plays audio. The SDK manages the protocol state. Your application owns
|
|
5
|
+
microphone capture, tool execution, and playback.
|
|
6
|
+
|
|
7
|
+
Start with the [recorded-speech quick start](../README.md#prism-send-recorded-speech)
|
|
8
|
+
to save a spoken reply without audio hardware.
|
|
9
|
+
|
|
10
|
+
For live conversations, keep the session open after normal cancellation and
|
|
11
|
+
report playback from the output clock. Saving a file is not playback.
|
|
12
|
+
Typed turns also support mixed text and voice applications.
|
|
13
|
+
|
|
14
|
+
## Connect
|
|
15
|
+
|
|
16
|
+
Use `await client.realtime.connect(options)`. It opens a WebSocket, initializes
|
|
17
|
+
one conversation, and returns a `RealtimeSession`. Settings apply once. Open a
|
|
18
|
+
new session to change them.
|
|
19
|
+
|
|
20
|
+
| Option | Required? | Default and behavior |
|
|
21
|
+
| --- | --- | --- |
|
|
22
|
+
| `endpoint` | Yes | Full `wss://host/v1/realtime` URL. Plain `ws://` is allowed only for localhost. |
|
|
23
|
+
| `voice` | If the server has no default voice | Omitted or `null` uses the server default. Supplied strings must be nonblank. |
|
|
24
|
+
| `instructions` | No | Omitted or `null` uses server defaults. Supplied strings must be nonblank. |
|
|
25
|
+
| `tools` | No | `[]`. Each tool requires `name` and a JSON Schema `parameters` object. `description` defaults to `""`. |
|
|
26
|
+
| `interruptOnSpeech` | No | `true`. Requests interruption when the server confirms user speech. |
|
|
27
|
+
| `timeouts` | No | `{ connectS: 10, readyS: 30, requestS: 10 }`. Each field is optional. Units are seconds. |
|
|
28
|
+
| `model` | No | `"prism"`, the only accepted value. |
|
|
29
|
+
| `signal` | No | An `AbortSignal` that cancels connection setup. It does not cancel an established session. |
|
|
30
|
+
|
|
31
|
+
Credentials come from `new Rime({ apiKey: "..." })` or `RIME_API_KEY`. Prism uses
|
|
32
|
+
`Authorization: Bearer <key>`, without a TTS token exchange. TTS endpoint, model,
|
|
33
|
+
voice, and timeout settings do not apply to Realtime.
|
|
34
|
+
|
|
35
|
+
After connection, `session.info` contains `sessionId`, `model`, `voice`,
|
|
36
|
+
`interruptOnSpeech`, `toolResultTimeoutS`, and `toolContinuationTimeoutS`.
|
|
37
|
+
Check the effective interruption setting there. Tool wait durations are total
|
|
38
|
+
server deadlines, not remaining time. A `null` duration can mean an omitted
|
|
39
|
+
field; it does not prove that no deadline exists.
|
|
40
|
+
|
|
41
|
+
Close the session in `finally` with `await session.close()`. Closing the parent
|
|
42
|
+
client closes all its TTS and Realtime operations. Both objects also support
|
|
43
|
+
`Symbol.asyncDispose`. Repeated close calls are safe.
|
|
44
|
+
|
|
45
|
+
## Choose an operation
|
|
46
|
+
|
|
47
|
+
Every method returns a promise. Arguments without `?` are required.
|
|
48
|
+
|
|
49
|
+
| Method | Result and behavior |
|
|
50
|
+
| --- | --- |
|
|
51
|
+
| `sendText(text, options?)` | Starts a typed user turn after readiness; returns `ResponseRef` at response creation. |
|
|
52
|
+
| `sendAudio(chunk, options?)` | Sends microphone audio with transport backpressure. |
|
|
53
|
+
| `requestReply(options?)` | Requests a reply without new user input; returns `ResponseRef`. Options can include `instruction` and `toolCall`. |
|
|
54
|
+
| `addMessage(role, text, options?)` | Adds user or assistant history without a reply; returns `ItemRef` after acknowledgment. |
|
|
55
|
+
| `submitToolResult(call, output, options?)` | Records a string result and waits for acknowledgment. |
|
|
56
|
+
| `continueReply(parent, options?)` | Waits for parent completion and all tool result acknowledgments, then returns a new `ResponseRef`. |
|
|
57
|
+
| `cancel(response, options?)` | Requests generation cancellation and waits for a terminal response or refusal. |
|
|
58
|
+
| `clearAudio(options?)` | Clears buffered input and resets audio conversion after acknowledgment. |
|
|
59
|
+
| `reportPlayback(report, options?)` | Reports interrupted or finished playback. |
|
|
60
|
+
| `close()` | Closes the session and releases its resources. |
|
|
61
|
+
|
|
62
|
+
Operation options accept `signal?: AbortSignal`. `requestReply` accepts its
|
|
63
|
+
instruction, tool call, and signal in one object. Methods other than `sendText`,
|
|
64
|
+
`requestReply`, `addMessage`, and `continueReply` return `Promise<void>`.
|
|
65
|
+
|
|
66
|
+
Text must be nonblank and contain at most 4,000 Unicode characters. History
|
|
67
|
+
roles are `"user"` and `"assistant"`. A reply instruction applies to one response;
|
|
68
|
+
it does not replace session instructions.
|
|
69
|
+
|
|
70
|
+
A returned `ResponseRef` means generation started. Read `response.ended` for the
|
|
71
|
+
terminal status. Generation can finish before cancellation takes effect, so a
|
|
72
|
+
successful `cancel()` does not guarantee a `"cancelled"` status.
|
|
73
|
+
|
|
74
|
+
## Send audio
|
|
75
|
+
|
|
76
|
+
Pass a plain object with `data: Uint8Array`. Node.js `Buffer` is also accepted.
|
|
77
|
+
The optional `format` object defaults to 16 kHz mono signed little-endian PCM16.
|
|
78
|
+
|
|
79
|
+
```typescript
|
|
80
|
+
await session.sendAudio({
|
|
81
|
+
data: pcmBytes,
|
|
82
|
+
format: { sampleRate: 48000, channels: 2 },
|
|
83
|
+
});
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
Supported `sampleRate` values are `8000`, `16000`, `24000`, and `48000`.
|
|
87
|
+
Channels can be `1` or `2`; `encoding` can only be `"pcm_s16le"`. Each field in
|
|
88
|
+
`format` is optional. Chunks must contain complete sample frames and at most
|
|
89
|
+
192,000 bytes. The SDK downmixes and resamples to 16 kHz mono, retaining state
|
|
90
|
+
across calls. A format change or acknowledged `clearAudio()` resets that state.
|
|
91
|
+
|
|
92
|
+
Await each send and bound your capture queue. Keep audio flowing, including
|
|
93
|
+
silence and while the assistant speaks or tools run. Prism detects and commits
|
|
94
|
+
speech; there is no manual commit method. Echo cancellation belongs to your app.
|
|
95
|
+
|
|
96
|
+
Received `audio.delta` events carry 24 kHz mono PCM16. The outgoing chunk limit
|
|
97
|
+
does not apply to received audio.
|
|
98
|
+
|
|
99
|
+
## Handle tools
|
|
100
|
+
|
|
101
|
+
Declare tools with plain `ToolDefinition` objects. Names must be nonblank and
|
|
102
|
+
unique; schemas must contain finite JSON values. The SDK copies schemas before
|
|
103
|
+
connection setup, so later caller changes cannot alter initialization.
|
|
104
|
+
|
|
105
|
+
Execute a tool once for each `tool.call` event. Validate its parsed `arguments`,
|
|
106
|
+
then submit a string result with `submitToolResult(payload.call, output)`.
|
|
107
|
+
Use `JSON.stringify(...)` for a structured result. Call
|
|
108
|
+
`continueReply(payload.call)` when the assistant should answer; a tool call
|
|
109
|
+
reference also carries its parent response identity.
|
|
110
|
+
|
|
111
|
+
Keep the event consumer running while submitting tool results and requesting
|
|
112
|
+
continuation. Run slow tools asynchronously or in a worker so they do not block
|
|
113
|
+
the event loop.
|
|
114
|
+
|
|
115
|
+
A new user turn or proactive reply supersedes the old tool round. Late results
|
|
116
|
+
can still be recorded. Use `requestReply({ toolCall: call })` to ask the assistant
|
|
117
|
+
to report a recorded result. Repeating the same result is safe within the live
|
|
118
|
+
session; changing it is rejected. Do not rerun a tool because delivery failed.
|
|
119
|
+
|
|
120
|
+
## Read events and references
|
|
121
|
+
|
|
122
|
+
Use `for await (const event of session.events)`. Each `SessionEvent` has
|
|
123
|
+
`sessionId`, `eventId`, nullable `requestId`, and a `payload`. Switch on
|
|
124
|
+
`payload.kind` to narrow the TypeScript union. All public types and error classes
|
|
125
|
+
are exported from `@rimelabs/sdk`.
|
|
126
|
+
|
|
127
|
+
| Kind | Payload fields |
|
|
128
|
+
| --- | --- |
|
|
129
|
+
| `input.ready` | No data fields. Turn methods handle the readiness wait. |
|
|
130
|
+
| `speech.started`, `speech.stopped` | `itemId` and `audioStartMs` or `audioEndMs` |
|
|
131
|
+
| `input.committed` | `itemId` |
|
|
132
|
+
| `transcript.delta`, `transcript.final` | `itemId` and `delta` or `text` |
|
|
133
|
+
| `transcript.failed` | `itemId`, `error` |
|
|
134
|
+
| `response.started` | `response`, `cause`, nullable `parent` and `inputItemId` |
|
|
135
|
+
| `message.started` | `output` |
|
|
136
|
+
| `text.delta`, `text.done` | `output` and `delta` or `text` |
|
|
137
|
+
| `audio.delta`, `audio.done` | `output`; a delta also has `audio.data` and a complete `audio.format` |
|
|
138
|
+
| `tool.call` | `call`, `itemId`, `name`, parsed `arguments` |
|
|
139
|
+
| `response.ended` | `response`, `status`, nullable `reason` |
|
|
140
|
+
| `response.abandoned` | `response`; the SDK requests cancellation after the requesting caller aborted. |
|
|
141
|
+
| `error` | `error`, a `RealtimeFault` |
|
|
142
|
+
|
|
143
|
+
References come from the SDK. Pass them back to identify the object you mean.
|
|
144
|
+
|
|
145
|
+
| Reference | Fields |
|
|
146
|
+
| --- | --- |
|
|
147
|
+
| `ResponseRef` | `sessionId`, `responseId` |
|
|
148
|
+
| `ItemRef` | `sessionId`, `itemId` |
|
|
149
|
+
| `ToolCallRef` | `sessionId`, `responseId`, `callId` |
|
|
150
|
+
| `OutputRef` | `response`, `itemId`, `outputIndex`, `contentIndex` |
|
|
151
|
+
|
|
152
|
+
Every reference belongs to one session. Only the last 128 responses are retained;
|
|
153
|
+
older references expire. Current output uses `contentIndex: 0`.
|
|
154
|
+
|
|
155
|
+
Unknown event kinds and extra fields are ignored. Unknown response causes,
|
|
156
|
+
terminal statuses, and error scopes become `"unknown"`. An absent response cause
|
|
157
|
+
means speech. The local `response.abandoned` event has an empty `eventId` and
|
|
158
|
+
`requestId: null`; it can follow `response.ended` when completion races cancellation.
|
|
159
|
+
|
|
160
|
+
## Report playback
|
|
161
|
+
|
|
162
|
+
Generation completion and playback completion are separate. Report the player's
|
|
163
|
+
actual state, including output latency and queued audio.
|
|
164
|
+
|
|
165
|
+
| Report object | Required fields | Optional fields |
|
|
166
|
+
| --- | --- | --- |
|
|
167
|
+
| Interrupted | `kind: "interrupted"`, `output`, `audioEndMs` | None |
|
|
168
|
+
| Finished | `kind: "finished"`, `response` | `playedMs`, `audibleTailMs`; both default to `null` |
|
|
169
|
+
|
|
170
|
+
Pass the object to `await session.reportPlayback(report)`. Interruption records
|
|
171
|
+
the point where caller speech stopped an assistant message. `audioEndMs` must
|
|
172
|
+
be an integer from 0 through 4,294,967,295. Finished playback means all queued
|
|
173
|
+
audio has played or has permanently stopped. Optional durations must be finite,
|
|
174
|
+
nonnegative milliseconds. The SDK waits for generation completion and supplies
|
|
175
|
+
the server's drain token.
|
|
176
|
+
|
|
177
|
+
Saving audio does not establish playback. Reports are optional. If you cannot
|
|
178
|
+
measure playback, omit them and let the server use its estimate. The recorded and typed examples have no player and omit these reports.
|
|
179
|
+
The voice examples report actual playback.
|
|
180
|
+
|
|
181
|
+
## Timeouts and failures
|
|
182
|
+
|
|
183
|
+
Configure positive, finite seconds through `timeouts`. `connectS` bounds the
|
|
184
|
+
WebSocket handshake and initial session event. `readyS` bounds turn admission,
|
|
185
|
+
tool readiness, and waiting for generation to end before a playback report.
|
|
186
|
+
Once a request is submitted, `requestS` bounds its write and acknowledgment.
|
|
187
|
+
|
|
188
|
+
These limits do not bound the entire conversation or generation after creation.
|
|
189
|
+
Set an application deadline and close the session when it expires. The recorded and typed examples use a 60-second deadline. Voice conversations
|
|
190
|
+
run until stopped or the connection fails.
|
|
191
|
+
|
|
192
|
+
`RealtimeAdmissionTimeout` means readiness expired with no request outstanding;
|
|
193
|
+
the session remains usable. An unknown request outcome closes the connection and
|
|
194
|
+
is never replayed. The SDK retries typed-turn admission only after named refusals
|
|
195
|
+
that confirm the request made no change.
|
|
196
|
+
|
|
197
|
+
Protocol faults appear as `error` events and can also reject an affected method
|
|
198
|
+
with `RimeRealtimeError`. Its `.fault` has `code`, `message`, `scope`, nullable
|
|
199
|
+
`requestId`, `responseId`, `itemId`, `correlationId`, `parameter`, and a `callIds`
|
|
200
|
+
array. A response fault does not itself end generation. Session faults and lost
|
|
201
|
+
connections end the session.
|
|
202
|
+
|
|
203
|
+
Malformed fields in known events and mismatched request acknowledgments close
|
|
204
|
+
the session with `RimeStreamError`. Unknown event kinds and extra fields remain
|
|
205
|
+
accepted and ignored. The SDK does not replay a request after a protocol failure.
|
|
206
|
+
|
|
207
|
+
Aborting an operation rejects it with `RimeCancelledError`. For reply requests,
|
|
208
|
+
the SDK prevents an unsent request or cancels a late accepted response. Control
|
|
209
|
+
operations can wait for acknowledgment before rejecting, to keep acknowledgment
|
|
210
|
+
matching correct. A submitted tool result keeps matching its acknowledgment even
|
|
211
|
+
if its caller aborts. Aborting audio after submission starts closes the session;
|
|
212
|
+
before submission, it restores conversion state. Do not resend a chunk with an
|
|
213
|
+
unknown outcome.
|
|
214
|
+
|
|
215
|
+
Keep up with the event stream. The queue holds 256 events; incoming messages
|
|
216
|
+
are limited to 1 MiB. A slow consumer ends the session. There is no automatic
|
|
217
|
+
reconnect, history replay, or live configuration update.
|
|
218
|
+
|
|
219
|
+
Local peer tests cover the SDK protocol handling. Validate the gateway handshake
|
|
220
|
+
and endpoint behavior for the deployment you use.
|
package/docs/stt.md
ADDED
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
# Streaming speech recognition
|
|
2
|
+
|
|
3
|
+
`client.stt.stream` transcribes one utterance from an async source of `Uint8Array` chunks (including Node buffers).
|
|
4
|
+
The SDK handles the gRPC protocol, authentication, audio conversion, transcript
|
|
5
|
+
revisions, backpressure, and cancellation. Your application decides when speech
|
|
6
|
+
ends by exhausting the source.
|
|
7
|
+
|
|
8
|
+
```typescript
|
|
9
|
+
import { createReadStream } from "node:fs";
|
|
10
|
+
import { Rime } from "@rimelabs/sdk";
|
|
11
|
+
|
|
12
|
+
async function* audioChunks() {
|
|
13
|
+
yield* createReadStream("utterance.pcm");
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
const client = new Rime();
|
|
17
|
+
try {
|
|
18
|
+
const transcript = client.stt.stream(audioChunks(), {
|
|
19
|
+
language: "en",
|
|
20
|
+
inputFormat: { sampleRate: 16000, channels: 1 },
|
|
21
|
+
mode: "written",
|
|
22
|
+
contextTerms: ["Super-G", "USOC"],
|
|
23
|
+
timeout: 120,
|
|
24
|
+
});
|
|
25
|
+
for await (const update of transcript) {
|
|
26
|
+
if (update.kind === "partial") console.log("Current transcript:", update.text);
|
|
27
|
+
else console.log("Final:", update.text, "Language:", update.language);
|
|
28
|
+
}
|
|
29
|
+
console.log("Request:", transcript.requestId);
|
|
30
|
+
} finally {
|
|
31
|
+
await client.close();
|
|
32
|
+
}
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
The runnable [example](../examples/stt/stream.mjs) reads a PCM file
|
|
36
|
+
without loading it all into memory. Use Node.js 22 or later; browsers are not supported.
|
|
37
|
+
Open files inside the async generator so file errors are reported through the
|
|
38
|
+
SDK and resources are opened only after the service accepts the utterance.
|
|
39
|
+
|
|
40
|
+
## Options
|
|
41
|
+
|
|
42
|
+
| Option | Meaning |
|
|
43
|
+
| --- | --- |
|
|
44
|
+
| `language` | Required spoken BCP-47 tag. Passed unchanged to the service; no detection or SDK language allowlist. The deployed catalog currently supports English and Spanish. |
|
|
45
|
+
| `mode` | `"written"` (default) or `"verbatim"`. Formatting intent, not a guarantee of exact tokens. |
|
|
46
|
+
| `contextTerms` | Sequence of recognition hints, copied unchanged and in order. The service validates terms and prompt budgets. |
|
|
47
|
+
| `inputFormat` | `PCMFormat`: signed little-endian PCM16 at 8, 16, 24 or 48 kHz, mono or interleaved stereo. Defaults to 16 kHz mono. |
|
|
48
|
+
| `timeout` | Optional positive overall timeout in seconds. Includes connection time, source pauses and consumer pauses. `null` disables this SDK deadline, but not internal or server deadlines. |
|
|
49
|
+
|
|
50
|
+
Input contains raw samples, with no WAV header or compressed codec. Chunks may
|
|
51
|
+
split sample frames; the SDK carries incomplete bytes into the next chunk. A
|
|
52
|
+
truncated final frame raises `RimeAudioFormatError`, without committing the
|
|
53
|
+
utterance. Each gRPC audio payload is at most 64 KiB of 16 kHz mono PCM16.
|
|
54
|
+
|
|
55
|
+
The default recognition endpoint is `stt.api.rime.ai:443`, using TLS and the same
|
|
56
|
+
API key as the other features. `new Rime({ sttEndpoint: "host:443" })` overrides only STT.
|
|
57
|
+
The existing `model`, `endpoint` and client `timeout` settings remain TTS settings.
|
|
58
|
+
There is no STT voice/model selector; the service selects its recognizer.
|
|
59
|
+
|
|
60
|
+
## Results and completion
|
|
61
|
+
|
|
62
|
+
`TranscriptionPartial` and `TranscriptionFinal` are discriminated TypeScript types, exported from `@rimelabs/sdk`. Every update contains the complete current `text`.
|
|
63
|
+
**Replace the previous transcript. Do not concatenate partials.** Later partials
|
|
64
|
+
can change earlier words. `TranscriptionFinal.language` is the canonical language
|
|
65
|
+
selected by the service.
|
|
66
|
+
|
|
67
|
+
The source is not consumed until the server accepts the configuration. Audio
|
|
68
|
+
upload and transcript reading run concurrently. Decoding currently uses cumulative
|
|
69
|
+
two-second audio windows, so a partial is not promised for each audio packet.
|
|
70
|
+
Silence can finish successfully with empty final text and no partials.
|
|
71
|
+
|
|
72
|
+
Exhausting the source half-closes gRPC input. The SDK continues reading and emits
|
|
73
|
+
one final result only after a matching `done` and successful gRPC completion.
|
|
74
|
+
EOF, an error status, or cancellation cannot turn a partial into a final result.
|
|
75
|
+
No audio is automatically replayed after a connection failure.
|
|
76
|
+
|
|
77
|
+
## Lifetime and limits
|
|
78
|
+
|
|
79
|
+
Construction is lazy: starting iteration begins work. Consume each stream with
|
|
80
|
+
one reader. Breaking a `for await` loop calls the iterator's `return` and cancels
|
|
81
|
+
unfinished work. `await transcript.cancel()` and async disposal also cancel it.
|
|
82
|
+
Pass an `AbortSignal` through the stream's `signal` option to cancel just that
|
|
83
|
+
utterance. Client close cancels all STT, TTS and realtime operations; cancelling
|
|
84
|
+
one stream leaves its siblings running.
|
|
85
|
+
|
|
86
|
+
The SDK bounds the transcript queue to 16 updates, each with at most 64 KiB of
|
|
87
|
+
UTF-8 text, and bounds gRPC responses to 256 KiB. Slow consumers apply backpressure.
|
|
88
|
+
Internal connection and acceptance waits are 10 seconds; completion after source
|
|
89
|
+
exhaustion is bounded to 120 seconds. The completion timer stops after successful
|
|
90
|
+
gRPC completion. The optional overall timeout remains active until the final
|
|
91
|
+
result is consumed. An outstanding source `next()` cannot be forcibly stopped by
|
|
92
|
+
JavaScript; the SDK stops waiting for it and attempts `return()` for at most two
|
|
93
|
+
seconds. Sources that own devices should support cancellation themselves.
|
|
94
|
+
|
|
95
|
+
Server input-idle, utterance-size, request-duration and model-context limits still
|
|
96
|
+
apply. The current wire limit is 16 MiB per utterance; model context can impose a
|
|
97
|
+
smaller limit depending on audio and recognition terms. There is no SDK VAD,
|
|
98
|
+
server endpointing, word timing, diarization, or file-format detection.
|
|
99
|
+
|
|
100
|
+
Failures use the shared SDK error hierarchy: caller/input errors, cancellation,
|
|
101
|
+
authentication, permission, resource limits, timeouts, transient availability and
|
|
102
|
+
protocol failures stay distinct. `requestId` is retained on streams and failures
|
|
103
|
+
when the server supplies it. Keep it when reporting a problem.
|
package/docs/tts.md
ADDED
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
# TTS for Node.js
|
|
2
|
+
|
|
3
|
+
You supply text; the SDK handles sentence boundaries and streams audio back.
|
|
4
|
+
Your application owns playback. Start with the complete WAV example in the
|
|
5
|
+
[package quick start](../README.md#tts-save-speech).
|
|
6
|
+
|
|
7
|
+
## Complete text
|
|
8
|
+
|
|
9
|
+
Use `client.tts.synthesize(text)` for one nonblank string. It returns an audio
|
|
10
|
+
stream; it does not wait for all audio to be generated. Do not await this call.
|
|
11
|
+
Use the audio loop and cleanup shown in the package quick start.
|
|
12
|
+
|
|
13
|
+
## Stream incoming text
|
|
14
|
+
|
|
15
|
+
Use `client.tts.stream(chunks)` for an async iterable of strings. A plain string
|
|
16
|
+
belongs in `synthesize`, not `stream`. The source can supply partial sentences:
|
|
17
|
+
|
|
18
|
+
```javascript
|
|
19
|
+
async function* text() {
|
|
20
|
+
yield "Hello. ";
|
|
21
|
+
yield "This text arrives in separate chunks.";
|
|
22
|
+
}
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
Use `client.tts.stream(text())` with the same file write and cleanup.
|
|
26
|
+
The SDK handles sentence boundaries. Do not await `tts.stream()`; the first
|
|
27
|
+
iterator read starts work. The quick start collects chunks before writing a WAV file.
|
|
28
|
+
|
|
29
|
+
## Configuration
|
|
30
|
+
|
|
31
|
+
Select Mist v3 with `new Rime({ model: "mistv3" })`. Mist v1 and v2 are not supported.
|
|
32
|
+
|
|
33
|
+
| Model | Standard endpoint | Default voice |
|
|
34
|
+
| --- | --- | --- |
|
|
35
|
+
| `coda` | `coda.api.rime.ai:443` | `clementine` |
|
|
36
|
+
| `mistv3` | `mist.api.rime.ai:443` | `astra` |
|
|
37
|
+
|
|
38
|
+
### Client options
|
|
39
|
+
|
|
40
|
+
| `new Rime({ ... })` option | Default | Meaning |
|
|
41
|
+
| --- | --- | --- |
|
|
42
|
+
| `apiKey` | `RIME_API_KEY` | API key; an explicit value overrides the environment |
|
|
43
|
+
| `model` | `"coda"` | Model for speech and discovery |
|
|
44
|
+
| `endpoint` | Model's standard endpoint | Custom hostname and optional port for speech and discovery |
|
|
45
|
+
| `timeout` | No overall deadline | Positive seconds; `null` disables the overall deadline |
|
|
46
|
+
|
|
47
|
+
Authentication uses bearer tokens over TLS. The SDK does not load `.env` files.
|
|
48
|
+
|
|
49
|
+
Custom deployments use `new Rime({ model: "coda", endpoint: "host:8443" })`.
|
|
50
|
+
Omit the scheme and path. TLS is required; the default port is `443`.
|
|
51
|
+
Model defaults still apply.
|
|
52
|
+
|
|
53
|
+
### Synthesis options
|
|
54
|
+
|
|
55
|
+
Both methods require text input: a nonblank string for `synthesize`, or an
|
|
56
|
+
async iterable of strings for `stream`. Both accept the following optional settings:
|
|
57
|
+
|
|
58
|
+
| Option | Default | Meaning |
|
|
59
|
+
| --- | --- | --- |
|
|
60
|
+
| `voice` | Model's default voice | Voice name from `client.voices.list()` |
|
|
61
|
+
| `language` | `"en"` | Language code |
|
|
62
|
+
| `audioFormat` | `AudioFormat.PCM_24000` | Output profile, imported from `@rimelabs/sdk` |
|
|
63
|
+
| `timeout` | Client setting | Seconds; pass `null` to disable the overall deadline |
|
|
64
|
+
|
|
65
|
+
### Audio formats
|
|
66
|
+
|
|
67
|
+
Both profiles return raw audio without a file header.
|
|
68
|
+
|
|
69
|
+
| `AudioFormat` profile | Encoding | Sample rate | Channels |
|
|
70
|
+
| --- | --- | --- | --- |
|
|
71
|
+
| `PCM_24000` | Signed 16-bit little-endian PCM | 24 kHz | Mono |
|
|
72
|
+
| `MULAW_8000` | G.711 mu-law | 8 kHz | Mono |
|
|
73
|
+
|
|
74
|
+
Chunks are `Uint8Array` values with complete sample frames; sizes vary. Inspect the
|
|
75
|
+
read-only `audio.format.encoding`, `audio.format.sampleRate`, and `audio.format.channels`.
|
|
76
|
+
|
|
77
|
+
### Timeouts
|
|
78
|
+
|
|
79
|
+
An overall deadline includes pauses between reads. Disabling it leaves internal
|
|
80
|
+
connection and progress limits active. Discovery allows at most 10 seconds;
|
|
81
|
+
a shorter supplied timeout takes precedence. Omit an operation's `timeout` or
|
|
82
|
+
pass `undefined` to inherit the client setting.
|
|
83
|
+
|
|
84
|
+
## Discover voices and languages
|
|
85
|
+
|
|
86
|
+
List voices and languages while the client is open:
|
|
87
|
+
|
|
88
|
+
```javascript
|
|
89
|
+
const voices = await client.voices.list({ language: "en" });
|
|
90
|
+
const languages = await client.languages.list();
|
|
91
|
+
console.log(voices, languages);
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
Both return string arrays and accept `timeout`. Omit `language` to disable filtering.
|
|
95
|
+
|
|
96
|
+
## Errors and cancellation
|
|
97
|
+
|
|
98
|
+
Report SDK errors and request IDs while closing the client on failure:
|
|
99
|
+
|
|
100
|
+
```javascript
|
|
101
|
+
import { writeFile } from "node:fs/promises";
|
|
102
|
+
import { Rime, RimeError } from "@rimelabs/sdk";
|
|
103
|
+
|
|
104
|
+
let client;
|
|
105
|
+
try {
|
|
106
|
+
client = new Rime();
|
|
107
|
+
await writeFile("speech.pcm", client.tts.synthesize("Hello. This is Rime."));
|
|
108
|
+
} catch (error) {
|
|
109
|
+
if (error instanceof RimeError) {
|
|
110
|
+
console.error(`Speech failed: ${error.message}; requestId=${error.requestId}`);
|
|
111
|
+
}
|
|
112
|
+
throw error;
|
|
113
|
+
} finally {
|
|
114
|
+
await client?.close();
|
|
115
|
+
}
|
|
116
|
+
```
|
|
117
|
+
|
|
118
|
+
Each SDK error inherits from `RimeError`:
|
|
119
|
+
|
|
120
|
+
| Error | Meaning |
|
|
121
|
+
| --- | --- |
|
|
122
|
+
| `RimeAuthenticationError` | Missing or rejected credentials |
|
|
123
|
+
| `RimePermissionError` | Access denied |
|
|
124
|
+
| `RimeInputError` | Invalid input or client use |
|
|
125
|
+
| `RimeResourceLimitError` | Service resource limit reached |
|
|
126
|
+
| `RimeUnavailableError` | Service unavailable |
|
|
127
|
+
| `RimeTimeoutError` | Overall or internal deadline reached |
|
|
128
|
+
| `RimeAudioFormatError` | Unsupported or unexpected audio format |
|
|
129
|
+
| `RimeCancelledError` | Operation cancelled |
|
|
130
|
+
| `RimeStreamError` | Other stream or transport failure |
|
|
131
|
+
|
|
132
|
+
Partial audio can arrive before an error. Output is complete only after successful
|
|
133
|
+
iteration. Synthesis is not retried automatically.
|
|
134
|
+
|
|
135
|
+
Keep cleanup when stopping early. A `for await` loop exit cancels the stream.
|
|
136
|
+
For manual reads, call `await audio.cancel()` in `finally`. `await client.close()`
|
|
137
|
+
stops all client work. Both calls are safe to repeat; both objects support
|
|
138
|
+
`Symbol.asyncDispose`. Cancellation stops the RPC and limits cleanup time, but
|
|
139
|
+
cannot force your text source to finish an unrelated promise.
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@rimelabs/sdk",
|
|
3
|
-
"version": "0.1.0-alpha.
|
|
4
|
-
"description": "Rime streaming
|
|
3
|
+
"version": "0.1.0-alpha.12",
|
|
4
|
+
"description": "Rime streaming TTS, STT and Prism realtime SDK for Node.js",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "module",
|
|
7
7
|
"engines": {
|
|
@@ -16,8 +16,11 @@
|
|
|
16
16
|
},
|
|
17
17
|
"files": [
|
|
18
18
|
"dist",
|
|
19
|
+
"docs",
|
|
19
20
|
"vendor",
|
|
20
21
|
"LICENSE",
|
|
22
|
+
"LICENSE-PRISM",
|
|
23
|
+
"NOTICE-PRISM",
|
|
21
24
|
"README.md",
|
|
22
25
|
"CHANGELOG.md"
|
|
23
26
|
],
|
|
@@ -26,16 +29,21 @@
|
|
|
26
29
|
"check": "tsc -p tsconfig.json --noEmit",
|
|
27
30
|
"test": "npm run build && node --test tests/*.test.mjs",
|
|
28
31
|
"prepack": "npm run build",
|
|
29
|
-
"lint": "prettier --check src tests"
|
|
32
|
+
"lint": "prettier --check src tests examples/tts examples/realtime examples/common examples/browser examples/tests examples/stt"
|
|
30
33
|
},
|
|
31
34
|
"dependencies": {
|
|
32
35
|
"@bufbuild/protobuf": "^2.11.0",
|
|
33
36
|
"@grpc/grpc-js": "^1.14.0",
|
|
34
|
-
"@rimelabs/api": "0.0
|
|
37
|
+
"@rimelabs/api": "0.4.0",
|
|
38
|
+
"ws": "^8.22.0"
|
|
35
39
|
},
|
|
36
40
|
"devDependencies": {
|
|
37
41
|
"@types/node": "^22.0.0",
|
|
42
|
+
"@types/ws": "^8.18.2",
|
|
43
|
+
"ajv": "^8.20.0",
|
|
38
44
|
"prettier": "3.9.8",
|
|
39
|
-
"typescript": "^5.9.0"
|
|
40
|
-
|
|
45
|
+
"typescript": "^5.9.0",
|
|
46
|
+
"yaml": "^2.9.1"
|
|
47
|
+
},
|
|
48
|
+
"homepage": "https://docs.rime.ai/"
|
|
41
49
|
}
|