@rimelabs/sdk 0.1.0-alpha.2 → 0.1.0-alpha.20

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 (81) hide show
  1. package/CHANGELOG.md +142 -0
  2. package/LICENSE-PRISM +201 -0
  3. package/NOTICE-PRISM +9 -0
  4. package/README.md +278 -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 +206 -0
  23. package/dist/realtime/conversation.d.ts +47 -0
  24. package/dist/realtime/conversation.js +309 -0
  25. package/dist/realtime/protocol.d.ts +84 -0
  26. package/dist/realtime/protocol.js +397 -0
  27. package/dist/realtime/session.d.ts +69 -0
  28. package/dist/realtime/session.js +744 -0
  29. package/dist/realtime/transport.d.ts +16 -0
  30. package/dist/realtime/transport.js +83 -0
  31. package/dist/realtime/types.d.ts +240 -0
  32. package/dist/realtime/types.js +10 -0
  33. package/dist/stt/audio.d.ts +15 -0
  34. package/dist/stt/audio.js +46 -0
  35. package/dist/stt/client.d.ts +17 -0
  36. package/dist/stt/client.js +121 -0
  37. package/dist/stt/policy.d.ts +11 -0
  38. package/dist/stt/policy.js +11 -0
  39. package/dist/stt/protocol.d.ts +14 -0
  40. package/dist/stt/protocol.js +76 -0
  41. package/dist/stt/queue.d.ts +16 -0
  42. package/dist/stt/queue.js +45 -0
  43. package/dist/stt/stream.d.ts +48 -0
  44. package/dist/stt/stream.js +224 -0
  45. package/dist/stt/transport.d.ts +15 -0
  46. package/dist/stt/transport.js +94 -0
  47. package/dist/stt/types.d.ts +51 -0
  48. package/dist/stt/types.js +1 -0
  49. package/dist/{audio.d.ts → tts/audio.d.ts} +9 -0
  50. package/dist/{audio.js → tts/audio.js} +44 -4
  51. package/dist/tts/buffer.d.ts +3 -0
  52. package/dist/tts/buffer.js +53 -0
  53. package/dist/tts/client.d.ts +53 -0
  54. package/dist/tts/client.js +151 -0
  55. package/dist/tts/lexicon.d.ts +7 -0
  56. package/dist/tts/lexicon.js +23 -0
  57. package/dist/{policy.d.ts → tts/policy.d.ts} +2 -3
  58. package/dist/tts/policy.js +41 -0
  59. package/dist/{stream.d.ts → tts/stream.d.ts} +15 -2
  60. package/dist/{stream.js → tts/stream.js} +54 -30
  61. package/dist/tts/timestamps.d.ts +24 -0
  62. package/dist/tts/timestamps.js +46 -0
  63. package/dist/{transport.d.ts → tts/transport.d.ts} +10 -13
  64. package/dist/tts/transport.js +133 -0
  65. package/dist/validation.d.ts +5 -0
  66. package/dist/validation.js +28 -0
  67. package/docs/realtime.md +426 -0
  68. package/docs/stt.md +186 -0
  69. package/docs/tts.md +284 -0
  70. package/package.json +14 -12
  71. package/dist/policy.js +0 -82
  72. package/dist/sentences.d.ts +0 -21
  73. package/dist/sentences.js +0 -142
  74. package/dist/transport.js +0 -223
  75. package/vendor/LICENSE.blingfire +0 -21
  76. package/vendor/README.md +0 -11
  77. package/vendor/blingfire.cjs +0 -4
  78. package/vendor/blingfire.upstream.cjs +0 -1
  79. package/vendor/blingfire.wasm +0 -0
  80. /package/dist/{queue.d.ts → tts/queue.d.ts} +0 -0
  81. /package/dist/{queue.js → tts/queue.js} +0 -0
@@ -0,0 +1,16 @@
1
+ import type { TranscriptionUpdate } from "./types.js";
2
+ /** Bounded, atomic transcript events; failure wakes producers and consumers. */
3
+ export declare class TranscriptQueue {
4
+ private readonly limit;
5
+ private items;
6
+ private closed;
7
+ private error;
8
+ private waiters;
9
+ constructor(limit: number);
10
+ private wait;
11
+ private wake;
12
+ put(value: TranscriptionUpdate): Promise<void>;
13
+ get(): Promise<IteratorResult<TranscriptionUpdate>>;
14
+ finish(): void;
15
+ fail(error: Error): void;
16
+ }
@@ -0,0 +1,45 @@
1
+ /** Bounded, atomic transcript events; failure wakes producers and consumers. */
2
+ export class TranscriptQueue {
3
+ limit;
4
+ items = [];
5
+ closed = false;
6
+ error = null;
7
+ waiters = new Set();
8
+ constructor(limit) {
9
+ this.limit = limit;
10
+ }
11
+ wait() {
12
+ return new Promise((resolve) => this.waiters.add(resolve));
13
+ }
14
+ wake() {
15
+ for (const resolve of this.waiters)
16
+ resolve();
17
+ this.waiters.clear();
18
+ }
19
+ async put(value) {
20
+ while (!this.closed && this.items.length >= this.limit)
21
+ await this.wait();
22
+ if (!this.closed) {
23
+ this.items.push(value);
24
+ this.wake();
25
+ }
26
+ }
27
+ async get() {
28
+ while (!this.closed && !this.items.length)
29
+ await this.wait();
30
+ if (this.error)
31
+ throw this.error;
32
+ const value = this.items.shift();
33
+ this.wake();
34
+ return value ? { done: false, value } : { done: true, value: undefined };
35
+ }
36
+ finish() {
37
+ this.closed = true;
38
+ this.wake();
39
+ }
40
+ fail(error) {
41
+ this.error ??= error;
42
+ this.items = [];
43
+ this.finish();
44
+ }
45
+ }
@@ -0,0 +1,48 @@
1
+ import type * as grpc from "@grpc/grpc-js";
2
+ import type { AudioSource, TranscriptionOptions, TranscriptionUpdate } from "./types.js";
3
+ interface StreamOwner {
4
+ prepare(signal: AbortSignal): Promise<{
5
+ client: grpc.Client;
6
+ metadata: grpc.Metadata;
7
+ }>;
8
+ checkOpen(): void;
9
+ forget(stream: TranscriptStream): void;
10
+ }
11
+ /** Lazy, single-consumer stream. Source exhaustion commits input; return/cancel aborts it. */
12
+ export declare class TranscriptStream implements AsyncIterableIterator<TranscriptionUpdate> {
13
+ private readonly owner;
14
+ private source;
15
+ private readonly options;
16
+ private readonly controller;
17
+ private readonly queue;
18
+ private readonly timers;
19
+ private worker;
20
+ private workerFinished;
21
+ private call;
22
+ private failure;
23
+ private reading;
24
+ private finished;
25
+ private inputDone;
26
+ private accepted;
27
+ private readonly acceptance;
28
+ private admission;
29
+ private completion;
30
+ private readonly externalAbort;
31
+ constructor(owner: StreamOwner, source: AudioSource | null, options: TranscriptionOptions);
32
+ /** Server request identity, available once response headers arrive. */
33
+ get requestId(): string | null;
34
+ private deadline;
35
+ private start;
36
+ private fail;
37
+ private produce;
38
+ private read;
39
+ private run;
40
+ [Symbol.asyncIterator](): this;
41
+ next(): Promise<IteratorResult<TranscriptionUpdate>>;
42
+ private cleanup;
43
+ /** Cancel unfinished work and release owned resources; safe to repeat. */
44
+ cancel(): Promise<void>;
45
+ return(): Promise<IteratorResult<TranscriptionUpdate>>;
46
+ [Symbol.asyncDispose](): Promise<void>;
47
+ }
48
+ export {};
@@ -0,0 +1,224 @@
1
+ import { abortable, scope } from "../cancellation.js";
2
+ import { RimeError, RimeInputError, RimeCancelledError, RimeTimeoutError, RimeStreamError, } from "../errors.js";
3
+ import { InputAudio } from "./audio.js";
4
+ import { TranscriptQueue } from "./queue.js";
5
+ import { TranscriptState } from "./protocol.js";
6
+ import { TranscriptionCall } from "./transport.js";
7
+ import { policy } from "./policy.js";
8
+ /** Lazy, single-consumer stream. Source exhaustion commits input; return/cancel aborts it. */
9
+ export class TranscriptStream {
10
+ owner;
11
+ source;
12
+ options;
13
+ controller = new AbortController();
14
+ queue = new TranscriptQueue(policy.queuedUpdates);
15
+ timers = new Set();
16
+ worker = null;
17
+ workerFinished = false;
18
+ call = null;
19
+ failure = null;
20
+ reading = false;
21
+ finished = false;
22
+ inputDone = false;
23
+ accepted;
24
+ acceptance = new Promise((resolve) => {
25
+ this.accepted = resolve;
26
+ });
27
+ admission = null;
28
+ completion = null;
29
+ externalAbort = () => this.fail(new RimeCancelledError("Transcription cancelled"));
30
+ constructor(owner, source, options) {
31
+ this.owner = owner;
32
+ this.source = source;
33
+ this.options = options;
34
+ }
35
+ /** Server request identity, available once response headers arrive. */
36
+ get requestId() {
37
+ return this.call?.requestId ?? null;
38
+ }
39
+ deadline(seconds, message) {
40
+ const timer = scope([], seconds, new RimeTimeoutError(message));
41
+ timer.signal.addEventListener("abort", () => this.fail(timer.signal.reason), { once: true });
42
+ if (timer.signal.aborted)
43
+ this.fail(timer.signal.reason);
44
+ this.timers.add(timer);
45
+ return timer;
46
+ }
47
+ start() {
48
+ if (this.failure)
49
+ throw this.failure;
50
+ if (this.finished || this.worker)
51
+ return;
52
+ this.owner.checkOpen();
53
+ if (this.options.signal?.aborted) {
54
+ this.externalAbort();
55
+ throw this.failure;
56
+ }
57
+ this.options.signal?.addEventListener("abort", this.externalAbort, {
58
+ once: true,
59
+ });
60
+ if (this.options.timeout != null)
61
+ this.deadline(this.options.timeout, "Overall transcription deadline expired");
62
+ this.worker = this.run();
63
+ }
64
+ fail(error) {
65
+ if (this.failure || this.finished)
66
+ return;
67
+ if (error.requestId === null && this.requestId !== null)
68
+ error = new error.constructor(error.message, this.requestId, { cause: error.cause });
69
+ this.failure = error;
70
+ this.queue.fail(error);
71
+ for (const timer of this.timers)
72
+ timer.dispose();
73
+ this.controller.abort(error);
74
+ this.call?.cancel();
75
+ this.options.signal?.removeEventListener("abort", this.externalAbort);
76
+ if (!this.worker || this.workerFinished) {
77
+ this.source = null;
78
+ this.owner.forget(this);
79
+ }
80
+ }
81
+ async produce() {
82
+ const signal = this.controller.signal;
83
+ await abortable(this.acceptance, signal);
84
+ const audio = new InputAudio(this.options.inputFormat);
85
+ let iterator;
86
+ try {
87
+ try {
88
+ iterator = this.source[Symbol.asyncIterator]();
89
+ }
90
+ catch (error) {
91
+ throw new RimeInputError("The audio source failed", this.requestId, {
92
+ cause: error,
93
+ });
94
+ }
95
+ for (;;) {
96
+ let result;
97
+ try {
98
+ result = await abortable(iterator.next(), signal);
99
+ }
100
+ catch (error) {
101
+ if (signal.aborted)
102
+ throw signal.reason;
103
+ throw new RimeInputError("The audio source failed", this.requestId, {
104
+ cause: error,
105
+ });
106
+ }
107
+ if (result.done)
108
+ break;
109
+ for (const part of audio.feed(result.value))
110
+ await this.call.writeAudio(part);
111
+ }
112
+ audio.finish();
113
+ this.inputDone = true;
114
+ this.completion = this.deadline(policy.completionTimeout, "Transcription completion timed out");
115
+ this.call.finishInput();
116
+ }
117
+ finally {
118
+ if (iterator?.return) {
119
+ const cleanup = scope([], policy.cleanupTimeout, new RimeTimeoutError("Audio source cleanup timed out"));
120
+ try {
121
+ await abortable(Promise.resolve().then(() => iterator.return()), cleanup.signal);
122
+ }
123
+ catch {
124
+ /* Caller-owned source cleanup cannot replace the operation outcome. */
125
+ }
126
+ finally {
127
+ cleanup.dispose();
128
+ }
129
+ }
130
+ }
131
+ }
132
+ async read() {
133
+ const state = new TranscriptState(policy.transcriptBytes, this.options.outputContract);
134
+ for await (const message of this.call.responses()) {
135
+ const update = state.accept(message, this.inputDone);
136
+ if (state.language !== null) {
137
+ this.admission?.dispose();
138
+ this.accepted();
139
+ }
140
+ if (update)
141
+ await this.queue.put(update);
142
+ }
143
+ return state.finish();
144
+ }
145
+ async run() {
146
+ let tasks = [];
147
+ try {
148
+ const prepared = await this.owner.prepare(this.controller.signal);
149
+ this.controller.signal.throwIfAborted();
150
+ this.call = new TranscriptionCall(prepared.client, prepared.metadata, this.controller.signal);
151
+ this.admission = this.deadline(policy.acceptanceTimeout, "Transcription acceptance timed out");
152
+ await this.call.start(this.options.language, this.options.mode ?? "written", this.options.contextTerms ?? [], this.options.outputContract);
153
+ const producer = this.produce(), reader = this.read();
154
+ tasks = [producer, reader];
155
+ const [, final] = await Promise.all([producer, reader]);
156
+ this.completion?.dispose();
157
+ await this.queue.put(final);
158
+ this.queue.finish();
159
+ }
160
+ catch (error) {
161
+ this.fail(error instanceof RimeError
162
+ ? error
163
+ : new RimeStreamError("Transcription failed", this.requestId, {
164
+ cause: error,
165
+ }));
166
+ }
167
+ finally {
168
+ if (this.failure)
169
+ this.controller.abort(this.failure);
170
+ this.call?.cancel();
171
+ await Promise.allSettled(tasks);
172
+ this.source = null;
173
+ this.workerFinished = true;
174
+ if (this.failure)
175
+ this.owner.forget(this);
176
+ }
177
+ }
178
+ [Symbol.asyncIterator]() {
179
+ return this;
180
+ }
181
+ async next() {
182
+ if (this.reading)
183
+ throw new RimeInputError("TranscriptStream permits only one concurrent reader");
184
+ this.reading = true;
185
+ try {
186
+ this.start();
187
+ if (this.finished)
188
+ return { done: true, value: undefined };
189
+ const result = await this.queue.get();
190
+ if (this.failure)
191
+ throw this.failure;
192
+ if (!result.done && result.value.kind === "final") {
193
+ await this.cleanup();
194
+ if (this.failure)
195
+ throw this.failure;
196
+ this.finished = true;
197
+ }
198
+ return result;
199
+ }
200
+ finally {
201
+ this.reading = false;
202
+ }
203
+ }
204
+ async cleanup() {
205
+ await this.worker;
206
+ for (const timer of this.timers)
207
+ timer.dispose();
208
+ this.options.signal?.removeEventListener("abort", this.externalAbort);
209
+ this.owner.forget(this);
210
+ }
211
+ /** Cancel unfinished work and release owned resources; safe to repeat. */
212
+ async cancel() {
213
+ this.fail(new RimeCancelledError("Transcription cancelled"));
214
+ await this.cleanup();
215
+ this.source = null;
216
+ }
217
+ async return() {
218
+ await this.cancel();
219
+ return { done: true, value: undefined };
220
+ }
221
+ async [Symbol.asyncDispose]() {
222
+ await this.cancel();
223
+ }
224
+ }
@@ -0,0 +1,15 @@
1
+ import * as grpc from "@grpc/grpc-js";
2
+ import * as schema from "@rimelabs/api";
3
+ import { BidiCall } from "../grpc.js";
4
+ import type { TranscriptionMode, TranscribeOptions, Transcription } from "./types.js";
5
+ export declare const MAX_WAV_BYTES: number;
6
+ export declare function requireSchema(): void;
7
+ export declare const transport: {
8
+ makeClient: (target: string) => grpc.Client;
9
+ };
10
+ export declare function transcribe(client: grpc.Client, auth: grpc.Metadata, audio: Uint8Array, options: TranscribeOptions, signal: AbortSignal): Promise<Transcription>;
11
+ export declare class TranscriptionCall extends BidiCall<schema.StreamingTranscriptionRequest, schema.StreamingTranscriptionResponse> {
12
+ constructor(client: grpc.Client, auth: grpc.Metadata, signal: AbortSignal);
13
+ start(language: string, mode: TranscriptionMode, contextTerms: readonly string[], outputContract?: import("./types.js").TranscriptionOutputContract): Promise<void>;
14
+ writeAudio(audio: Uint8Array): Promise<void>;
15
+ }
@@ -0,0 +1,94 @@
1
+ import * as grpc from "@grpc/grpc-js";
2
+ import { create } from "@bufbuild/protobuf";
3
+ import * as schema from "@rimelabs/api";
4
+ import { BidiCall, serializer, deserializer, requestId, nativeError, } from "../grpc.js";
5
+ import { abortable, scope } from "../cancellation.js";
6
+ import { RimeInputError, RimeStreamError, RimeResourceLimitError, RimeTimeoutError, RimeCancelledError, } from "../errors.js";
7
+ import { policy } from "./policy.js";
8
+ export const MAX_WAV_BYTES = 16 * 1024 * 1024;
9
+ export function requireSchema() {
10
+ if (!schema.SpeechToText)
11
+ throw new RimeInputError("STT requires @rimelabs/api with SpeechToText definitions");
12
+ }
13
+ export const transport = {
14
+ makeClient: (target) => new grpc.Client(target, grpc.credentials.createSsl(), {
15
+ "grpc.enable_retries": 0,
16
+ "grpc.max_receive_message_length": policy.receiveBytes,
17
+ "grpc.max_send_message_length": MAX_WAV_BYTES + 65536,
18
+ }),
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
+ }
65
+ export class TranscriptionCall extends BidiCall {
66
+ constructor(client, auth, signal) {
67
+ requireSchema();
68
+ const service = schema.SpeechToText;
69
+ const method = service.method.transcribeStreaming;
70
+ super(client.makeBidiStreamRequest(`/${service.typeName}/${method.name}`, serializer(method.input), deserializer(method.output), auth), signal);
71
+ }
72
+ start(language, mode, contextTerms, outputContract = "revised_hypotheses") {
73
+ return this.write(create(schema.StreamingTranscriptionRequestSchema, {
74
+ payload: {
75
+ case: "config",
76
+ value: {
77
+ language,
78
+ mode: mode === "verbatim"
79
+ ? schema.TranscriptionMode.VERBATIM
80
+ : schema.TranscriptionMode.WRITTEN,
81
+ contextTerms: [...contextTerms],
82
+ outputContract: outputContract === "append_fragments"
83
+ ? schema.StreamingOutputContract.APPEND_FRAGMENTS
84
+ : schema.StreamingOutputContract.REVISED_HYPOTHESES,
85
+ },
86
+ },
87
+ }));
88
+ }
89
+ writeAudio(audio) {
90
+ return this.write(create(schema.StreamingTranscriptionRequestSchema, {
91
+ payload: { case: "audio", value: audio },
92
+ }));
93
+ }
94
+ }
@@ -0,0 +1,51 @@
1
+ import type { PCMFormat } from "../pcm.js";
2
+ /** Formatting intent, for example "flight 247" or "flight two four seven". */
3
+ export type TranscriptionMode = "written" | "verbatim";
4
+ /** One caller-ended utterance of headerless signed PCM16 little-endian audio. */
5
+ export type AudioSource = AsyncIterable<Uint8Array>;
6
+ /** Select replacement hypotheses (default) or permanent append fragments. */
7
+ export type TranscriptionOutputContract = "revised_hypotheses" | "append_fragments";
8
+ export interface TranscriptionDelta {
9
+ readonly kind: "delta";
10
+ /** Append exactly, preserving the server's newline separators. */
11
+ readonly text: string;
12
+ }
13
+ export interface TranscriptionOptions {
14
+ /** Defaults to revised_hypotheses. Each stream uses one fixed contract. */
15
+ outputContract?: TranscriptionOutputContract;
16
+ /** Required BCP-47 tag, passed unchanged to the service (for example "en"). */
17
+ language: string;
18
+ /** Formatting intent; defaults to written and does not guarantee exact tokens. */
19
+ mode?: TranscriptionMode;
20
+ /** Recognition hints passed unchanged and in order, for example ["Super-G"]. */
21
+ contextTerms?: readonly string[];
22
+ /** Defaults to PCM16 little-endian mono 16 kHz; fixed for the utterance. */
23
+ inputFormat?: PCMFormat;
24
+ /** Optional overall deadline in seconds, including source and consumer pauses. */
25
+ timeout?: number | null;
26
+ /** Cancels only this utterance, including a pending connection. */
27
+ signal?: AbortSignal;
28
+ }
29
+ export interface TranscriptionPartial {
30
+ /** A revisable replacement snapshot. */
31
+ readonly kind: "partial";
32
+ /** Complete current text; replace previous text instead of appending it. */
33
+ readonly text: string;
34
+ }
35
+ export interface TranscriptionFinal {
36
+ /** Successful protocol and transport completion. */
37
+ readonly kind: "final";
38
+ /** Complete final text; silence can produce an empty string. */
39
+ readonly text: string;
40
+ /** Canonical BCP-47 tag selected by the service, for example "en". */
41
+ readonly language: string;
42
+ }
43
+ export type TranscriptionUpdate = TranscriptionPartial | TranscriptionDelta | TranscriptionFinal;
44
+ /** Options for a complete WAV recording. Audio is sent unchanged. */
45
+ export type TranscribeOptions = Omit<TranscriptionOptions, "inputFormat" | "outputContract">;
46
+ /** A successful whole-recording transcription. Silence may produce empty text. */
47
+ export interface Transcription {
48
+ readonly text: string;
49
+ readonly language: string;
50
+ readonly requestId: string | null;
51
+ }
@@ -0,0 +1 @@
1
+ export {};
@@ -1,11 +1,20 @@
1
1
  export declare class AudioFormat {
2
2
  readonly encoding: string;
3
+ /** Fixed preset rate, or zero when selected per request. See AudioStream.sampleRate. */
3
4
  readonly sampleRate: number;
4
5
  readonly channels: number;
6
+ static readonly PCM: Readonly<AudioFormat>;
7
+ static readonly WAV: Readonly<AudioFormat>;
8
+ static readonly MP3: Readonly<AudioFormat>;
5
9
  static readonly PCM_24000: Readonly<AudioFormat>;
6
10
  static readonly MULAW_8000: Readonly<AudioFormat>;
7
11
  private constructor();
8
12
  }
13
+ export declare function wireFormat(format: AudioFormat): string;
14
+ export declare function resolveAudio(format: AudioFormat | null | undefined, sampleRate: number | null | undefined, timeScaleFactor: number | null | undefined): {
15
+ format: AudioFormat;
16
+ sampleRate: number;
17
+ };
9
18
  export declare class Converter {
10
19
  private profile;
11
20
  private tail;
@@ -1,16 +1,52 @@
1
- import { RimeAudioFormatError } from "./errors.js";
1
+ import { RimeAudioFormatError, RimeInputError } from "../errors.js";
2
2
  export class AudioFormat {
3
3
  encoding;
4
4
  sampleRate;
5
5
  channels;
6
+ static PCM = Object.freeze(new AudioFormat("pcm_s16le", 0, 1));
7
+ static WAV = Object.freeze(new AudioFormat("wav", 0, 1));
8
+ static MP3 = Object.freeze(new AudioFormat("mp3", 0, 1));
6
9
  static PCM_24000 = Object.freeze(new AudioFormat("pcm_s16le", 24000, 1));
7
10
  static MULAW_8000 = Object.freeze(new AudioFormat("mulaw", 8000, 1));
8
- constructor(encoding, sampleRate, channels) {
11
+ constructor(encoding,
12
+ /** Fixed preset rate, or zero when selected per request. See AudioStream.sampleRate. */
13
+ sampleRate, channels) {
9
14
  this.encoding = encoding;
10
15
  this.sampleRate = sampleRate;
11
16
  this.channels = channels;
12
17
  }
13
18
  }
19
+ export function wireFormat(format) {
20
+ if (format === AudioFormat.WAV)
21
+ return "audio/wav";
22
+ if (format === AudioFormat.MP3)
23
+ return "audio/mpeg";
24
+ return "audio/pcm";
25
+ }
26
+ export function resolveAudio(format, sampleRate, timeScaleFactor) {
27
+ format ??= AudioFormat.PCM;
28
+ if (![
29
+ AudioFormat.PCM,
30
+ AudioFormat.PCM_24000,
31
+ AudioFormat.MULAW_8000,
32
+ AudioFormat.WAV,
33
+ AudioFormat.MP3,
34
+ ].includes(format))
35
+ throw new RimeAudioFormatError("Select a named AudioFormat");
36
+ sampleRate ??= format.sampleRate || 24000;
37
+ if (![8000, 16000, 24000].includes(sampleRate))
38
+ throw new RimeAudioFormatError("sampleRate must be 8000, 16000, or 24000");
39
+ if (format.sampleRate && sampleRate !== format.sampleRate)
40
+ throw new RimeAudioFormatError("sampleRate conflicts with the fixed AudioFormat preset");
41
+ if (timeScaleFactor != null &&
42
+ (!Number.isFinite(timeScaleFactor) ||
43
+ timeScaleFactor < 0.4 ||
44
+ timeScaleFactor > 2.5))
45
+ throw new RimeInputError("timeScaleFactor must be a finite number from 0.4 to 2.5");
46
+ if (format === AudioFormat.PCM && sampleRate === 24000)
47
+ format = AudioFormat.PCM_24000;
48
+ return { format, sampleRate };
49
+ }
14
50
  const raw = Array.from({ length: 63 }, (_, i) => (i === 31
15
51
  ? (2 * 3400) / 24000
16
52
  : Math.sin(((2 * Math.PI * 3400) / 24000) * (i - 31)) /
@@ -20,7 +56,8 @@ const total = raw.reduce((a, b) => a + b, 0);
20
56
  const coefficients = raw.map((x) => x / total);
21
57
  function mulaw(sample) {
22
58
  sample = Math.max(-32768, Math.min(32767, sample));
23
- const sign = sample < 0 ? 128 : 0, magnitude = Math.min(Math.abs(sample), 32635) + 132;
59
+ // G.711 uses one's complement for negative PCM before quantization.
60
+ const sign = sample < 0 ? 128 : 0, magnitude = Math.min(sample < 0 ? ~sample : sample, 32635) + 132;
24
61
  const exponent = Math.max(0, Math.floor(Math.log2(magnitude)) - 7);
25
62
  return ~(sign | (exponent << 4) | ((magnitude >> (exponent + 3)) & 15)) & 255;
26
63
  }
@@ -46,12 +83,15 @@ export class Converter {
46
83
  return null;
47
84
  }
48
85
  process(input, final = false) {
86
+ if (this.profile === AudioFormat.WAV || this.profile === AudioFormat.MP3)
87
+ return Buffer.from(input);
49
88
  const data = Buffer.concat([this.tail, input]);
50
89
  const size = Math.floor(data.length / 2) * 2;
51
90
  this.tail = Buffer.from(data.subarray(size));
52
91
  if (final && this.tail.length)
53
92
  throw new RimeAudioFormatError("Incomplete final PCM sample frame");
54
- if (this.profile === AudioFormat.PCM_24000)
93
+ if (this.profile === AudioFormat.PCM ||
94
+ this.profile === AudioFormat.PCM_24000)
55
95
  return data.subarray(0, size);
56
96
  const result = [];
57
97
  for (let i = 0; i < size; i += 2) {
@@ -0,0 +1,3 @@
1
+ export declare const maxBufferBytes: number;
2
+ /** Validate mono PCM16 WAV and replace unknown streaming chunk lengths. */
3
+ export declare function finalizeWav(data: Buffer, sampleRate: number, requestId: string | null): void;
@@ -0,0 +1,53 @@
1
+ import { RimeAudioFormatError } from "../errors.js";
2
+ export const maxBufferBytes = 64 * 1024 * 1024;
3
+ /** Validate mono PCM16 WAV and replace unknown streaming chunk lengths. */
4
+ export function finalizeWav(data, sampleRate, requestId) {
5
+ const invalid = () => new RimeAudioFormatError("Expected a complete mono PCM16 WAV at the requested sample rate", requestId);
6
+ if (data.length < 12 ||
7
+ data.toString("latin1", 0, 4) !== "RIFF" ||
8
+ data.toString("latin1", 8, 12) !== "WAVE")
9
+ throw invalid();
10
+ if (data.length - 8 > 0xffffffff)
11
+ throw new RimeAudioFormatError("Audio exceeds the WAV size limit", requestId);
12
+ if (![0xffffffff, data.length - 8].includes(data.readUInt32LE(4)))
13
+ throw invalid();
14
+ let offset = 12;
15
+ let foundFormat = false;
16
+ let dataSizeOffset;
17
+ let dataSize = 0;
18
+ while (offset < data.length) {
19
+ if (data.length - offset < 8)
20
+ throw invalid();
21
+ const name = data.toString("latin1", offset, offset + 4);
22
+ let size = data.readUInt32LE(offset + 4);
23
+ const start = offset + 8;
24
+ if (name === "data" && size === 0xffffffff)
25
+ size = data.length - start;
26
+ const end = start + size;
27
+ if (end > data.length)
28
+ throw invalid();
29
+ if (name === "fmt ") {
30
+ if (foundFormat ||
31
+ size < 16 ||
32
+ data.readUInt16LE(start) !== 1 ||
33
+ data.readUInt16LE(start + 2) !== 1 ||
34
+ data.readUInt32LE(start + 4) !== sampleRate ||
35
+ data.readUInt32LE(start + 8) !== sampleRate * 2 ||
36
+ data.readUInt16LE(start + 12) !== 2 ||
37
+ data.readUInt16LE(start + 14) !== 16)
38
+ throw invalid();
39
+ foundFormat = true;
40
+ }
41
+ else if (name === "data") {
42
+ if (!foundFormat || dataSizeOffset !== undefined || size % 2)
43
+ throw invalid();
44
+ dataSizeOffset = offset + 4;
45
+ dataSize = size;
46
+ }
47
+ offset = end + (size % 2);
48
+ }
49
+ if (offset !== data.length || dataSizeOffset === undefined)
50
+ throw invalid();
51
+ data.writeUInt32LE(data.length - 8, 4);
52
+ data.writeUInt32LE(dataSize, dataSizeOffset);
53
+ }