@rimelabs/sdk 0.1.0-alpha.10 → 0.1.0-alpha.13
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 +30 -0
- package/README.md +38 -8
- package/dist/auth.d.ts +1 -0
- package/dist/auth.js +5 -2
- package/dist/cancellation.d.ts +5 -0
- package/dist/cancellation.js +36 -0
- package/dist/client.d.ts +11 -0
- package/dist/client.js +21 -5
- package/dist/grpc.d.ts +27 -0
- package/dist/grpc.js +175 -0
- package/dist/index.d.ts +4 -0
- package/dist/index.js +1 -0
- package/dist/pcm.d.ts +21 -0
- package/dist/pcm.js +52 -0
- package/dist/realtime/async.d.ts +1 -5
- package/dist/realtime/async.js +1 -36
- package/dist/realtime/audio.d.ts +0 -11
- package/dist/realtime/audio.js +2 -45
- package/dist/realtime/session.js +2 -1
- package/dist/realtime/types.d.ts +2 -6
- 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/tts/client.d.ts +8 -0
- package/dist/tts/client.js +24 -7
- package/dist/tts/lexicon.d.ts +7 -0
- package/dist/tts/lexicon.js +23 -0
- package/dist/tts/policy.d.ts +2 -2
- package/dist/tts/policy.js +5 -28
- package/dist/tts/stream.d.ts +11 -2
- package/dist/tts/stream.js +42 -27
- package/dist/tts/timestamps.d.ts +24 -0
- package/dist/tts/timestamps.js +46 -0
- package/dist/tts/transport.d.ts +9 -13
- package/dist/tts/transport.js +43 -140
- package/dist/validation.d.ts +5 -0
- package/dist/validation.js +28 -0
- package/docs/stt.md +103 -0
- package/docs/tts.md +120 -7
- package/package.json +4 -5
- package/dist/tts/sentences.d.ts +0 -21
- package/dist/tts/sentences.js +0 -142
- package/vendor/LICENSE.blingfire +0 -21
- package/vendor/README.md +0 -11
- package/vendor/blingfire.cjs +0 -4
- package/vendor/blingfire.upstream.cjs +0 -1
- package/vendor/blingfire.wasm +0 -0
package/dist/realtime/audio.js
CHANGED
|
@@ -1,14 +1,7 @@
|
|
|
1
1
|
import { RimeAudioFormatError } from "../errors.js";
|
|
2
|
+
import { resolvePCMFormat } from "../pcm.js";
|
|
2
3
|
export function formatOf(chunk) {
|
|
3
|
-
const format =
|
|
4
|
-
sampleRate: chunk.format?.sampleRate ?? 16000,
|
|
5
|
-
channels: chunk.format?.channels ?? 1,
|
|
6
|
-
encoding: chunk.format?.encoding ?? "pcm_s16le",
|
|
7
|
-
};
|
|
8
|
-
if (![8000, 16000, 24000, 48000].includes(format.sampleRate) ||
|
|
9
|
-
![1, 2].includes(format.channels) ||
|
|
10
|
-
format.encoding !== "pcm_s16le")
|
|
11
|
-
throw new RimeAudioFormatError("Use PCM16 at 8, 16, 24 or 48 kHz, with one or two channels");
|
|
4
|
+
const format = resolvePCMFormat(chunk.format);
|
|
12
5
|
if (!(chunk.data instanceof Uint8Array) ||
|
|
13
6
|
chunk.data.length % (2 * format.channels))
|
|
14
7
|
throw new RimeAudioFormatError("Audio must contain complete PCM16 frames as Uint8Array");
|
|
@@ -16,39 +9,3 @@ export function formatOf(chunk) {
|
|
|
16
9
|
throw new RimeAudioFormatError("Send audio in chunks of at most 192000 bytes");
|
|
17
10
|
return format;
|
|
18
11
|
}
|
|
19
|
-
/** Streaming linear interpolation, matching Python audioop.ratecv with no filter.
|
|
20
|
-
* Integer phase remains bounded and preserves samples across chunk boundaries.
|
|
21
|
-
*/
|
|
22
|
-
export class InputConverter {
|
|
23
|
-
format;
|
|
24
|
-
phase = -16000;
|
|
25
|
-
previous = 0;
|
|
26
|
-
constructor(format) {
|
|
27
|
-
this.format = format;
|
|
28
|
-
}
|
|
29
|
-
clone() {
|
|
30
|
-
const copy = new InputConverter(this.format);
|
|
31
|
-
copy.phase = this.phase;
|
|
32
|
-
copy.previous = this.previous;
|
|
33
|
-
return copy;
|
|
34
|
-
}
|
|
35
|
-
process(input) {
|
|
36
|
-
const data = Buffer.from(input.buffer, input.byteOffset, input.byteLength);
|
|
37
|
-
const samples = [];
|
|
38
|
-
for (let offset = 0; offset < data.length; offset += this.format.channels * 2) {
|
|
39
|
-
const current = this.format.channels === 1
|
|
40
|
-
? data.readInt16LE(offset)
|
|
41
|
-
: Math.floor((data.readInt16LE(offset) + data.readInt16LE(offset + 2)) / 2);
|
|
42
|
-
this.phase += 16000;
|
|
43
|
-
while (this.phase >= 0) {
|
|
44
|
-
samples.push(Math.floor((this.previous * this.phase + current * (16000 - this.phase)) /
|
|
45
|
-
16000));
|
|
46
|
-
this.phase -= this.format.sampleRate;
|
|
47
|
-
}
|
|
48
|
-
this.previous = current;
|
|
49
|
-
}
|
|
50
|
-
const output = Buffer.alloc(samples.length * 2);
|
|
51
|
-
samples.forEach((sample, index) => output.writeInt16LE(sample, index * 2));
|
|
52
|
-
return output;
|
|
53
|
-
}
|
|
54
|
-
}
|
package/dist/realtime/session.js
CHANGED
|
@@ -6,7 +6,8 @@ import { setMaxListeners } from "node:events";
|
|
|
6
6
|
import { abortable } from "../cancellation.js";
|
|
7
7
|
import { RimeError, RimeInputError, RimeResourceLimitError, RimeStreamError, RimeTimeoutError, } from "../errors.js";
|
|
8
8
|
import { Deferred, Flag, Mutex, cancellation, scope } from "./async.js";
|
|
9
|
-
import { formatOf
|
|
9
|
+
import { formatOf } from "./audio.js";
|
|
10
|
+
import { InputConverter } from "../pcm.js";
|
|
10
11
|
import * as t from "./types.js";
|
|
11
12
|
import * as p from "./protocol.js";
|
|
12
13
|
const NOT_READY = new Set([
|
package/dist/realtime/types.d.ts
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import type { PCMFormat } from "../pcm.js";
|
|
2
|
+
export type { PCMFormat } from "../pcm.js";
|
|
1
3
|
import { RimeError, RimeTimeoutError } from "../errors.js";
|
|
2
4
|
export type JsonValue = null | boolean | number | string | JsonValue[] | JsonObject;
|
|
3
5
|
export interface JsonObject {
|
|
@@ -30,12 +32,6 @@ export interface ReplyOptions extends RealtimeOperationOptions {
|
|
|
30
32
|
readonly instruction?: string | null;
|
|
31
33
|
readonly toolCall?: ToolCallRef | null;
|
|
32
34
|
}
|
|
33
|
-
/** Omitted fields mean 16 kHz, mono, signed little-endian PCM16. */
|
|
34
|
-
export interface PCMFormat {
|
|
35
|
-
readonly sampleRate?: 8000 | 16000 | 24000 | 48000;
|
|
36
|
-
readonly channels?: 1 | 2;
|
|
37
|
-
readonly encoding?: "pcm_s16le";
|
|
38
|
-
}
|
|
39
35
|
export interface AudioChunk {
|
|
40
36
|
readonly data: Uint8Array;
|
|
41
37
|
readonly format?: PCMFormat;
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { type PCMFormat } from "../pcm.js";
|
|
2
|
+
/** One utterance of headerless signed PCM16 little-endian mono 16 kHz output.
|
|
3
|
+
* Fully consume each feed iterator before feeding the next chunk. Conversion
|
|
4
|
+
* advances one bounded source portion at a time so callers can apply backpressure.
|
|
5
|
+
*/
|
|
6
|
+
export declare class InputAudio {
|
|
7
|
+
private readonly converter;
|
|
8
|
+
private readonly frameBytes;
|
|
9
|
+
private pending;
|
|
10
|
+
private finished;
|
|
11
|
+
constructor(format?: PCMFormat);
|
|
12
|
+
feed(data: Uint8Array): Generator<Buffer>;
|
|
13
|
+
/** Check EOF before committing; never silently pad or truncate a final frame. */
|
|
14
|
+
finish(): void;
|
|
15
|
+
}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import { RimeAudioFormatError, RimeInputError } from "../errors.js";
|
|
2
|
+
import { InputConverter, resolvePCMFormat } from "../pcm.js";
|
|
3
|
+
// Bound conversion work and temporary allocations for large source chunks.
|
|
4
|
+
const sourceBytes = 16384;
|
|
5
|
+
// Maximum decoded STT audio payload, excluding the protobuf envelope.
|
|
6
|
+
const messageBytes = 65536;
|
|
7
|
+
/** One utterance of headerless signed PCM16 little-endian mono 16 kHz output.
|
|
8
|
+
* Fully consume each feed iterator before feeding the next chunk. Conversion
|
|
9
|
+
* advances one bounded source portion at a time so callers can apply backpressure.
|
|
10
|
+
*/
|
|
11
|
+
export class InputAudio {
|
|
12
|
+
converter;
|
|
13
|
+
frameBytes;
|
|
14
|
+
pending = Buffer.alloc(0);
|
|
15
|
+
finished = false;
|
|
16
|
+
constructor(format) {
|
|
17
|
+
const resolved = resolvePCMFormat(format);
|
|
18
|
+
this.converter = new InputConverter(resolved);
|
|
19
|
+
this.frameBytes = 2 * resolved.channels;
|
|
20
|
+
}
|
|
21
|
+
*feed(data) {
|
|
22
|
+
if (this.finished)
|
|
23
|
+
throw new RimeInputError("The audio input is finished");
|
|
24
|
+
if (!(data instanceof Uint8Array))
|
|
25
|
+
throw new RimeAudioFormatError("The audio source must yield PCM16 Uint8Array chunks");
|
|
26
|
+
for (let offset = 0; offset < data.length; offset += sourceBytes) {
|
|
27
|
+
const part = Buffer.concat([
|
|
28
|
+
this.pending,
|
|
29
|
+
data.subarray(offset, offset + sourceBytes),
|
|
30
|
+
]);
|
|
31
|
+
const complete = part.length - (part.length % this.frameBytes);
|
|
32
|
+
this.pending = Buffer.from(part.subarray(complete));
|
|
33
|
+
if (complete) {
|
|
34
|
+
const converted = this.converter.process(part.subarray(0, complete));
|
|
35
|
+
for (let start = 0; start < converted.length; start += messageBytes)
|
|
36
|
+
yield converted.subarray(start, start + messageBytes);
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
/** Check EOF before committing; never silently pad or truncate a final frame. */
|
|
41
|
+
finish() {
|
|
42
|
+
this.finished = true;
|
|
43
|
+
if (this.pending.length)
|
|
44
|
+
throw new RimeAudioFormatError("The audio source ended with an incomplete PCM16 frame");
|
|
45
|
+
}
|
|
46
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { Credentials } from "../auth.js";
|
|
2
|
+
import { TranscriptStream } from "./stream.js";
|
|
3
|
+
import type { AudioSource, TranscriptionOptions } from "./types.js";
|
|
4
|
+
export declare class STT {
|
|
5
|
+
private readonly credentials;
|
|
6
|
+
private readonly endpoint;
|
|
7
|
+
private readonly checkOpen;
|
|
8
|
+
private connection;
|
|
9
|
+
private streams;
|
|
10
|
+
constructor(credentials: Credentials, endpoint: string | null | undefined, checkOpen: () => void);
|
|
11
|
+
stream(source: AudioSource, options: TranscriptionOptions): TranscriptStream;
|
|
12
|
+
close(): Promise<void>;
|
|
13
|
+
}
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import { connect, metadata } from "../grpc.js";
|
|
2
|
+
import { endpointAddress, timeout } from "../validation.js";
|
|
3
|
+
import { RimeInputError } from "../errors.js";
|
|
4
|
+
import { resolvePCMFormat } from "../pcm.js";
|
|
5
|
+
import { transport, requireSchema } from "./transport.js";
|
|
6
|
+
import { TranscriptStream } from "./stream.js";
|
|
7
|
+
import { policy } from "./policy.js";
|
|
8
|
+
export class STT {
|
|
9
|
+
credentials;
|
|
10
|
+
endpoint;
|
|
11
|
+
checkOpen;
|
|
12
|
+
connection = null;
|
|
13
|
+
streams = new Set();
|
|
14
|
+
constructor(credentials, endpoint, checkOpen) {
|
|
15
|
+
this.credentials = credentials;
|
|
16
|
+
this.endpoint = endpoint;
|
|
17
|
+
this.checkOpen = checkOpen;
|
|
18
|
+
}
|
|
19
|
+
stream(source, options) {
|
|
20
|
+
this.checkOpen();
|
|
21
|
+
if (!source || typeof source[Symbol.asyncIterator] !== "function")
|
|
22
|
+
throw new RimeInputError("audio must be an async iterable of Uint8Array chunks");
|
|
23
|
+
if (!options || typeof options.language !== "string")
|
|
24
|
+
throw new RimeInputError("language must be a string");
|
|
25
|
+
const mode = options.mode ?? "written";
|
|
26
|
+
if (mode !== "written" && mode !== "verbatim")
|
|
27
|
+
throw new RimeInputError("mode must be written or verbatim");
|
|
28
|
+
const terms = options.contextTerms ?? [];
|
|
29
|
+
if (!Array.isArray(terms) || terms.some((term) => typeof term !== "string"))
|
|
30
|
+
throw new RimeInputError("contextTerms must be an array of strings");
|
|
31
|
+
const format = resolvePCMFormat(options.inputFormat);
|
|
32
|
+
const target = this.endpoint == null
|
|
33
|
+
? policy.target
|
|
34
|
+
: endpointAddress(this.endpoint).target;
|
|
35
|
+
const stream = new TranscriptStream({
|
|
36
|
+
checkOpen: this.checkOpen,
|
|
37
|
+
forget: (stream) => this.streams.delete(stream),
|
|
38
|
+
prepare: async (signal) => {
|
|
39
|
+
this.checkOpen();
|
|
40
|
+
requireSchema();
|
|
41
|
+
signal.throwIfAborted();
|
|
42
|
+
const headers = metadata(this.credentials.authorization());
|
|
43
|
+
this.connection ??= transport.makeClient(target);
|
|
44
|
+
await connect(this.connection, signal, policy.connectionTimeout);
|
|
45
|
+
signal.throwIfAborted();
|
|
46
|
+
return {
|
|
47
|
+
client: this.connection,
|
|
48
|
+
metadata: headers,
|
|
49
|
+
};
|
|
50
|
+
},
|
|
51
|
+
}, source, {
|
|
52
|
+
language: options.language,
|
|
53
|
+
mode,
|
|
54
|
+
contextTerms: [...terms],
|
|
55
|
+
inputFormat: format,
|
|
56
|
+
timeout: timeout(options.timeout),
|
|
57
|
+
signal: options.signal,
|
|
58
|
+
});
|
|
59
|
+
this.streams.add(stream);
|
|
60
|
+
return stream;
|
|
61
|
+
}
|
|
62
|
+
async close() {
|
|
63
|
+
try {
|
|
64
|
+
const results = await Promise.allSettled([...this.streams].map((stream) => stream.cancel()));
|
|
65
|
+
for (const result of results)
|
|
66
|
+
if (result.status === "rejected")
|
|
67
|
+
throw result.reason;
|
|
68
|
+
}
|
|
69
|
+
finally {
|
|
70
|
+
this.connection?.close();
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
/** Independent resource and transport limits for recognition. Durations are seconds. */
|
|
2
|
+
export declare const policy: {
|
|
3
|
+
target: string;
|
|
4
|
+
connectionTimeout: number;
|
|
5
|
+
acceptanceTimeout: number;
|
|
6
|
+
completionTimeout: number;
|
|
7
|
+
cleanupTimeout: number;
|
|
8
|
+
receiveBytes: number;
|
|
9
|
+
transcriptBytes: number;
|
|
10
|
+
queuedUpdates: number;
|
|
11
|
+
};
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
/** Independent resource and transport limits for recognition. Durations are seconds. */
|
|
2
|
+
export const policy = {
|
|
3
|
+
target: "stt.api.rime.ai:443",
|
|
4
|
+
connectionTimeout: 10,
|
|
5
|
+
acceptanceTimeout: 10,
|
|
6
|
+
completionTimeout: 120,
|
|
7
|
+
cleanupTimeout: 2,
|
|
8
|
+
receiveBytes: 262144,
|
|
9
|
+
transcriptBytes: 65536,
|
|
10
|
+
queuedUpdates: 16,
|
|
11
|
+
};
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import * as schema from "@rimelabs/api";
|
|
2
|
+
import type { TranscriptionPartial, TranscriptionFinal } from "./types.js";
|
|
3
|
+
/** Validate the wire sequence before exposing any final result. */
|
|
4
|
+
export declare class TranscriptState {
|
|
5
|
+
private readonly textLimit;
|
|
6
|
+
language: string | null;
|
|
7
|
+
private revision;
|
|
8
|
+
private text;
|
|
9
|
+
private final;
|
|
10
|
+
constructor(textLimit: number);
|
|
11
|
+
accept(message: schema.StreamingTranscriptionResponse, inputDone: boolean): TranscriptionPartial | null;
|
|
12
|
+
finish(): TranscriptionFinal;
|
|
13
|
+
}
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import * as schema from "@rimelabs/api";
|
|
2
|
+
import { RimeStreamError, RimeResourceLimitError } from "../errors.js";
|
|
3
|
+
/** Validate the wire sequence before exposing any final result. */
|
|
4
|
+
export class TranscriptState {
|
|
5
|
+
textLimit;
|
|
6
|
+
language = null;
|
|
7
|
+
revision = 0n;
|
|
8
|
+
text = "";
|
|
9
|
+
final = null;
|
|
10
|
+
constructor(textLimit) {
|
|
11
|
+
this.textLimit = textLimit;
|
|
12
|
+
}
|
|
13
|
+
accept(message, inputDone) {
|
|
14
|
+
const payload = message.payload;
|
|
15
|
+
if (payload.case === undefined)
|
|
16
|
+
return null;
|
|
17
|
+
if (this.final)
|
|
18
|
+
throw new RimeStreamError("Received a message after transcription completion");
|
|
19
|
+
if (payload.case === "accepted") {
|
|
20
|
+
const accepted = payload.value;
|
|
21
|
+
if (this.language !== null)
|
|
22
|
+
throw new RimeStreamError("Received duplicate transcription acceptance");
|
|
23
|
+
if (accepted.outputContract !==
|
|
24
|
+
schema.StreamingOutputContract.REVISED_HYPOTHESES)
|
|
25
|
+
throw new RimeStreamError("The service accepted a different transcript contract");
|
|
26
|
+
if (!accepted.language?.tag || !accepted.language.source)
|
|
27
|
+
throw new RimeStreamError("The service did not confirm a resolved language");
|
|
28
|
+
this.language = accepted.language.tag;
|
|
29
|
+
return null;
|
|
30
|
+
}
|
|
31
|
+
if (this.language === null)
|
|
32
|
+
throw new RimeStreamError("Received a transcript before acceptance");
|
|
33
|
+
if (payload.case !== "hypothesis" && payload.case !== "done")
|
|
34
|
+
throw new RimeStreamError("Received an unexpected transcription response");
|
|
35
|
+
const value = payload.value;
|
|
36
|
+
if (Buffer.byteLength(value.text, "utf8") > this.textLimit)
|
|
37
|
+
throw new RimeResourceLimitError("The transcript exceeds the SDK text limit");
|
|
38
|
+
if (payload.case === "hypothesis") {
|
|
39
|
+
if (value.revision <= this.revision ||
|
|
40
|
+
(this.revision === 0n && value.revision !== 1n))
|
|
41
|
+
throw new RimeStreamError("Transcript revisions are not increasing from one");
|
|
42
|
+
this.revision = value.revision;
|
|
43
|
+
this.text = value.text;
|
|
44
|
+
return { kind: "partial", text: value.text };
|
|
45
|
+
}
|
|
46
|
+
if (!inputDone)
|
|
47
|
+
throw new RimeStreamError("The service completed before input finished");
|
|
48
|
+
if (value.revision !== this.revision || value.text !== this.text)
|
|
49
|
+
throw new RimeStreamError("Final transcript does not match the last hypothesis");
|
|
50
|
+
const language = payload.value.language;
|
|
51
|
+
if (language?.tag !== this.language || !language.source)
|
|
52
|
+
throw new RimeStreamError("Final language does not match the accepted language");
|
|
53
|
+
this.final = { kind: "final", text: value.text, language: language.tag };
|
|
54
|
+
return null;
|
|
55
|
+
}
|
|
56
|
+
finish() {
|
|
57
|
+
if (!this.final)
|
|
58
|
+
throw new RimeStreamError("The service ended without a final transcript");
|
|
59
|
+
return this.final;
|
|
60
|
+
}
|
|
61
|
+
}
|
|
@@ -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);
|
|
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 ?? []);
|
|
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
|
+
}
|