@absolutejs/voice-rime 0.0.1-beta.1
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/README.md +68 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +170 -0
- package/dist/rime.d.ts +3 -0
- package/dist/types.d.ts +19 -0
- package/package.json +38 -0
package/README.md
ADDED
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
# `@absolutejs/voice-rime`
|
|
2
|
+
|
|
3
|
+
Rime text-to-speech adapter for `@absolutejs/voice`.
|
|
4
|
+
|
|
5
|
+
Wraps Rime's `/v1/rime-tts` HTTP streaming endpoint behind voice's `TTSAdapter` seam. Supports the mist / mistv2 / arcana voice models for fast, conversational TTS.
|
|
6
|
+
|
|
7
|
+
## Install
|
|
8
|
+
|
|
9
|
+
```sh
|
|
10
|
+
bun add @absolutejs/voice-rime
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
`@absolutejs/voice` is a runtime dependency.
|
|
14
|
+
|
|
15
|
+
## Use
|
|
16
|
+
|
|
17
|
+
```ts
|
|
18
|
+
import { voice } from "@absolutejs/voice";
|
|
19
|
+
import { rime } from "@absolutejs/voice-rime";
|
|
20
|
+
|
|
21
|
+
const app = voice({
|
|
22
|
+
// ... stt + other voice options ...
|
|
23
|
+
tts: rime({
|
|
24
|
+
apiKey: process.env.RIME_API_KEY!,
|
|
25
|
+
speaker: "cove",
|
|
26
|
+
// optional:
|
|
27
|
+
modelId: "mistv2", // default; or 'mist' / 'arcana'
|
|
28
|
+
audioFormat: "pcm", // default; or 'mulaw' for telephony
|
|
29
|
+
sampleRate: 22_050, // default
|
|
30
|
+
speedAlpha: 1.0,
|
|
31
|
+
lang: "eng",
|
|
32
|
+
reduceLatency: true,
|
|
33
|
+
pauseBetweenBrackets: true,
|
|
34
|
+
phonemizeBetweenBrackets: true,
|
|
35
|
+
}),
|
|
36
|
+
});
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
For telephony at 8 kHz μ-law:
|
|
40
|
+
|
|
41
|
+
```ts
|
|
42
|
+
rime({
|
|
43
|
+
apiKey,
|
|
44
|
+
speaker: "cove",
|
|
45
|
+
audioFormat: "mulaw",
|
|
46
|
+
});
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
## Options
|
|
50
|
+
|
|
51
|
+
| Option | Required | Default | Notes |
|
|
52
|
+
| --- | --- | --- | --- |
|
|
53
|
+
| `apiKey` | yes | — | Rime API key, sent as `Authorization: Bearer <key>`. |
|
|
54
|
+
| `speaker` | yes | — | Rime speaker id (`cove`, `marsh`, `river`, etc.). |
|
|
55
|
+
| `modelId` | no | `mistv2` | `mist`, `mistv2`, `arcana`, or a future Rime model id. |
|
|
56
|
+
| `audioFormat` | no | `pcm` | Must be `pcm` (PCM s16le) or `mulaw` (telephony @ 8 kHz). |
|
|
57
|
+
| `sampleRate` | no | `22050` | Ignored for `mulaw`. |
|
|
58
|
+
| `lang` | no | — | Language hint (`eng`, etc.). |
|
|
59
|
+
| `speedAlpha`, `inlineSpeedAlpha` | no | — | Forwarded to Rime. |
|
|
60
|
+
| `reduceLatency`, `pauseBetweenBrackets`, `phonemizeBetweenBrackets`, `noTextNormalization` | no | — | Forwarded as boolean controls. |
|
|
61
|
+
| `baseUrl` | no | `https://users.rime.ai` | Override for staging hosts. |
|
|
62
|
+
| `fetch` | no | `globalThis.fetch` | Inject for tests; opportunistic HTTP/2 multiplexing on outbound HTTPS. |
|
|
63
|
+
|
|
64
|
+
## Notes
|
|
65
|
+
|
|
66
|
+
- Whitespace-only `send()` is a no-op.
|
|
67
|
+
- `session.close(reason)` aborts in-flight requests and refuses further sends.
|
|
68
|
+
- This adapter targets Rime's HTTP streaming endpoint. If you need their WebSocket transport, open an issue.
|
package/dist/index.d.ts
ADDED
package/dist/index.js
ADDED
|
@@ -0,0 +1,170 @@
|
|
|
1
|
+
// @bun
|
|
2
|
+
// src/rime.ts
|
|
3
|
+
var DEFAULT_BASE_URL = "https://users.rime.ai";
|
|
4
|
+
var DEFAULT_ENDPOINT_PATH = "/v1/rime-tts";
|
|
5
|
+
var DEFAULT_MODEL = "mistv2";
|
|
6
|
+
var DEFAULT_AUDIO_FORMAT = "pcm";
|
|
7
|
+
var DEFAULT_SAMPLE_RATE = 22050;
|
|
8
|
+
var isHttpsUrl = (url) => typeof url === "string" ? url.startsWith("https://") : url.protocol === "https:";
|
|
9
|
+
var h2IfHttps = (url) => isHttpsUrl(url) ? { protocol: "http2" } : {};
|
|
10
|
+
var createListenerMap = () => ({
|
|
11
|
+
audio: new Set,
|
|
12
|
+
close: new Set,
|
|
13
|
+
error: new Set
|
|
14
|
+
});
|
|
15
|
+
var emit = async (listeners, event, payload) => {
|
|
16
|
+
for (const listener of listeners[event]) {
|
|
17
|
+
await listener(payload);
|
|
18
|
+
}
|
|
19
|
+
};
|
|
20
|
+
var omitUndefined = (value) => Object.fromEntries(Object.entries(value).filter(([, entry]) => entry !== undefined));
|
|
21
|
+
var resolveBaseUrl = (config) => (config.baseUrl ?? DEFAULT_BASE_URL).replace(/\/$/, "");
|
|
22
|
+
var resolveStreamUrl = (config) => new URL(`${resolveBaseUrl(config)}${DEFAULT_ENDPOINT_PATH}`);
|
|
23
|
+
var resolveAcceptHeader = (audioFormat) => {
|
|
24
|
+
switch (audioFormat) {
|
|
25
|
+
case "mulaw":
|
|
26
|
+
return "audio/basic";
|
|
27
|
+
case "pcm":
|
|
28
|
+
default:
|
|
29
|
+
return "audio/pcm";
|
|
30
|
+
}
|
|
31
|
+
};
|
|
32
|
+
var resolveAudioFormat = (config) => {
|
|
33
|
+
const audioFormat = config.audioFormat ?? DEFAULT_AUDIO_FORMAT;
|
|
34
|
+
const sampleRate = config.sampleRate ?? DEFAULT_SAMPLE_RATE;
|
|
35
|
+
if (audioFormat === "pcm") {
|
|
36
|
+
return {
|
|
37
|
+
channels: 1,
|
|
38
|
+
container: "raw",
|
|
39
|
+
encoding: "pcm_s16le",
|
|
40
|
+
sampleRateHz: sampleRate
|
|
41
|
+
};
|
|
42
|
+
}
|
|
43
|
+
if (audioFormat === "mulaw") {
|
|
44
|
+
return {
|
|
45
|
+
channels: 1,
|
|
46
|
+
container: "raw",
|
|
47
|
+
encoding: "mulaw",
|
|
48
|
+
sampleRateHz: 8000
|
|
49
|
+
};
|
|
50
|
+
}
|
|
51
|
+
throw new Error(`Unsupported Rime audio format "${String(audioFormat)}" for @absolutejs/voice TTS streaming. ` + `Use "pcm" for PCM playback or "mulaw" for telephony.`);
|
|
52
|
+
};
|
|
53
|
+
var buildHeaders = (config, audioFormat) => ({
|
|
54
|
+
Accept: resolveAcceptHeader(audioFormat),
|
|
55
|
+
Authorization: `Bearer ${config.apiKey}`,
|
|
56
|
+
"Content-Type": "application/json"
|
|
57
|
+
});
|
|
58
|
+
var resolveErrorMessage = (error) => {
|
|
59
|
+
if (typeof error === "string" && error.trim())
|
|
60
|
+
return error;
|
|
61
|
+
if (error instanceof Error && error.message.trim())
|
|
62
|
+
return error.message;
|
|
63
|
+
return "Rime TTS request failed";
|
|
64
|
+
};
|
|
65
|
+
var buildRequestPayload = (config, text) => omitUndefined({
|
|
66
|
+
audioFormat: config.audioFormat ?? DEFAULT_AUDIO_FORMAT,
|
|
67
|
+
inlineSpeedAlpha: config.inlineSpeedAlpha,
|
|
68
|
+
lang: config.lang,
|
|
69
|
+
modelId: config.modelId ?? DEFAULT_MODEL,
|
|
70
|
+
noTextNormalization: config.noTextNormalization,
|
|
71
|
+
pauseBetweenBrackets: config.pauseBetweenBrackets,
|
|
72
|
+
phonemizeBetweenBrackets: config.phonemizeBetweenBrackets,
|
|
73
|
+
reduceLatency: config.reduceLatency,
|
|
74
|
+
samplingRate: config.sampleRate ?? DEFAULT_SAMPLE_RATE,
|
|
75
|
+
speaker: config.speaker,
|
|
76
|
+
speedAlpha: config.speedAlpha,
|
|
77
|
+
text
|
|
78
|
+
});
|
|
79
|
+
var rime = (config) => {
|
|
80
|
+
if (!config.apiKey) {
|
|
81
|
+
throw new Error("@absolutejs/voice-rime requires an apiKey.");
|
|
82
|
+
}
|
|
83
|
+
if (!config.speaker) {
|
|
84
|
+
throw new Error('@absolutejs/voice-rime requires a speaker (e.g. "cove", "marsh", "river").');
|
|
85
|
+
}
|
|
86
|
+
const fetchImpl = config.fetch ?? globalThis.fetch;
|
|
87
|
+
const audioFormat = config.audioFormat ?? DEFAULT_AUDIO_FORMAT;
|
|
88
|
+
const audioFormatDescriptor = resolveAudioFormat(config);
|
|
89
|
+
return {
|
|
90
|
+
kind: "tts",
|
|
91
|
+
open: () => {
|
|
92
|
+
const listeners = createListenerMap();
|
|
93
|
+
const activeControllers = new Set;
|
|
94
|
+
let closed = false;
|
|
95
|
+
return {
|
|
96
|
+
close: async (reason) => {
|
|
97
|
+
if (closed)
|
|
98
|
+
return;
|
|
99
|
+
closed = true;
|
|
100
|
+
for (const controller of activeControllers) {
|
|
101
|
+
controller.abort(reason);
|
|
102
|
+
}
|
|
103
|
+
await emit(listeners, "close", {
|
|
104
|
+
reason,
|
|
105
|
+
recoverable: false,
|
|
106
|
+
type: "close"
|
|
107
|
+
});
|
|
108
|
+
},
|
|
109
|
+
on: (event, handler) => {
|
|
110
|
+
listeners[event].add(handler);
|
|
111
|
+
return () => {
|
|
112
|
+
listeners[event].delete(handler);
|
|
113
|
+
};
|
|
114
|
+
},
|
|
115
|
+
send: async (text) => {
|
|
116
|
+
if (closed)
|
|
117
|
+
return;
|
|
118
|
+
const trimmed = text.trim();
|
|
119
|
+
if (!trimmed)
|
|
120
|
+
return;
|
|
121
|
+
const controller = new AbortController;
|
|
122
|
+
activeControllers.add(controller);
|
|
123
|
+
try {
|
|
124
|
+
const target = resolveStreamUrl(config);
|
|
125
|
+
const response = await fetchImpl(target, {
|
|
126
|
+
...h2IfHttps(target),
|
|
127
|
+
body: JSON.stringify(buildRequestPayload(config, trimmed)),
|
|
128
|
+
headers: buildHeaders(config, audioFormat),
|
|
129
|
+
method: "POST",
|
|
130
|
+
signal: controller.signal
|
|
131
|
+
});
|
|
132
|
+
if (!response.ok || !response.body) {
|
|
133
|
+
const bodyText = await response.text().catch(() => "");
|
|
134
|
+
throw new Error(`Rime returned ${String(response.status)} ${response.statusText}${bodyText ? `: ${bodyText.slice(0, 200)}` : ""}`);
|
|
135
|
+
}
|
|
136
|
+
const reader = response.body.getReader();
|
|
137
|
+
try {
|
|
138
|
+
while (true) {
|
|
139
|
+
const { done, value } = await reader.read();
|
|
140
|
+
if (done || !value)
|
|
141
|
+
break;
|
|
142
|
+
await emit(listeners, "audio", {
|
|
143
|
+
chunk: value,
|
|
144
|
+
format: audioFormatDescriptor,
|
|
145
|
+
receivedAt: Date.now(),
|
|
146
|
+
type: "audio"
|
|
147
|
+
});
|
|
148
|
+
}
|
|
149
|
+
} finally {
|
|
150
|
+
reader.releaseLock();
|
|
151
|
+
}
|
|
152
|
+
} catch (error) {
|
|
153
|
+
if (error.name === "AbortError")
|
|
154
|
+
return;
|
|
155
|
+
await emit(listeners, "error", {
|
|
156
|
+
error: error instanceof Error ? error : new Error(resolveErrorMessage(error)),
|
|
157
|
+
recoverable: false,
|
|
158
|
+
type: "error"
|
|
159
|
+
});
|
|
160
|
+
} finally {
|
|
161
|
+
activeControllers.delete(controller);
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
};
|
|
165
|
+
}
|
|
166
|
+
};
|
|
167
|
+
};
|
|
168
|
+
export {
|
|
169
|
+
rime
|
|
170
|
+
};
|
package/dist/rime.d.ts
ADDED
package/dist/types.d.ts
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
export type RimeModel = 'arcana' | 'mist' | 'mistv2' | (string & {});
|
|
2
|
+
export type RimeAudioFormat = 'mulaw' | 'pcm' | (string & {});
|
|
3
|
+
export type RimeSampleRate = 8_000 | 16_000 | 22_050 | 24_000 | (number & {});
|
|
4
|
+
export type RimeTTSOptions = {
|
|
5
|
+
apiKey: string;
|
|
6
|
+
baseUrl?: string;
|
|
7
|
+
audioFormat?: RimeAudioFormat;
|
|
8
|
+
fetch?: typeof fetch;
|
|
9
|
+
inlineSpeedAlpha?: number;
|
|
10
|
+
lang?: string;
|
|
11
|
+
modelId?: RimeModel;
|
|
12
|
+
noTextNormalization?: boolean;
|
|
13
|
+
pauseBetweenBrackets?: boolean;
|
|
14
|
+
phonemizeBetweenBrackets?: boolean;
|
|
15
|
+
reduceLatency?: boolean;
|
|
16
|
+
sampleRate?: RimeSampleRate;
|
|
17
|
+
speaker: string;
|
|
18
|
+
speedAlpha?: number;
|
|
19
|
+
};
|
package/package.json
ADDED
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@absolutejs/voice-rime",
|
|
3
|
+
"version": "0.0.1-beta.1",
|
|
4
|
+
"description": "Rime text-to-speech adapter for @absolutejs/voice",
|
|
5
|
+
"repository": {
|
|
6
|
+
"type": "git",
|
|
7
|
+
"url": "https://github.com/absolutejs/voice-adapters.git",
|
|
8
|
+
"directory": "rime"
|
|
9
|
+
},
|
|
10
|
+
"files": [
|
|
11
|
+
"dist",
|
|
12
|
+
"README.md"
|
|
13
|
+
],
|
|
14
|
+
"main": "./dist/index.js",
|
|
15
|
+
"types": "./dist/index.d.ts",
|
|
16
|
+
"exports": {
|
|
17
|
+
".": {
|
|
18
|
+
"import": "./dist/index.js",
|
|
19
|
+
"types": "./dist/index.d.ts"
|
|
20
|
+
}
|
|
21
|
+
},
|
|
22
|
+
"license": "CC BY-NC 4.0",
|
|
23
|
+
"author": "Alex Kahn",
|
|
24
|
+
"scripts": {
|
|
25
|
+
"build": "rm -rf dist && bun build ./src/index.ts --outdir dist --target bun --external @absolutejs/voice && tsc --emitDeclarationOnly --project tsconfig.json",
|
|
26
|
+
"format": "prettier --write \"./**/*.{js,ts,json,md}\"",
|
|
27
|
+
"release": "bun run format && bun run build && bun publish --access public",
|
|
28
|
+
"test": "bun test",
|
|
29
|
+
"typecheck": "bun run tsc --noEmit"
|
|
30
|
+
},
|
|
31
|
+
"dependencies": {
|
|
32
|
+
"@absolutejs/voice": "0.0.22-beta.471"
|
|
33
|
+
},
|
|
34
|
+
"devDependencies": {
|
|
35
|
+
"@types/bun": "1.3.9",
|
|
36
|
+
"typescript": "^5.9.3"
|
|
37
|
+
}
|
|
38
|
+
}
|