@openclaw/fish-audio-speech 2026.7.2-beta.7 → 2026.8.1-beta.3
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 +10 -3
- package/dist/index.js +4 -4
- package/dist/speech-provider.js +13 -27
- package/dist/tts.js +11 -64
- package/openclaw.plugin.json +2 -1
- package/package.json +4 -4
package/README.md
CHANGED
|
@@ -2,9 +2,16 @@
|
|
|
2
2
|
|
|
3
3
|
Official OpenClaw speech provider for Fish Audio's hosted S2.1 API.
|
|
4
4
|
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
5
|
+
Install it with:
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
openclaw plugins install @openclaw/fish-audio-speech
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
The plugin id is `fish-audio-speech`; the speech provider and TTS config id
|
|
12
|
+
remain `fish-audio`. Configure `tts.provider: "fish-audio"` and set
|
|
13
|
+
`FISH_API_KEY`. The provider supports buffered audio, HTTP-streamed playback,
|
|
14
|
+
native Opus voice notes, 8 kHz PCM telephony, and Fish Audio voice discovery.
|
|
8
15
|
|
|
9
16
|
See [Fish Audio](https://docs.openclaw.ai/providers/fish-audio) for setup,
|
|
10
17
|
models, voice selection, expressive tags, and local macOS MLX usage.
|
package/dist/index.js
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import { buildFishAudioSpeechProvider } from "./speech-provider.js";
|
|
2
2
|
import { definePluginEntry } from "openclaw/plugin-sdk/plugin-entry";
|
|
3
|
-
//#region extensions/fish-audio/index.ts
|
|
4
|
-
var
|
|
5
|
-
id: "fish-audio",
|
|
3
|
+
//#region extensions/fish-audio-speech/index.ts
|
|
4
|
+
var fish_audio_speech_default = definePluginEntry({
|
|
5
|
+
id: "fish-audio-speech",
|
|
6
6
|
name: "Fish Audio Speech",
|
|
7
7
|
description: "Hosted Fish Audio S2.1 text-to-speech provider",
|
|
8
8
|
register(api) {
|
|
@@ -10,4 +10,4 @@ var fish_audio_default = definePluginEntry({
|
|
|
10
10
|
}
|
|
11
11
|
});
|
|
12
12
|
//#endregion
|
|
13
|
-
export {
|
|
13
|
+
export { fish_audio_speech_default as default };
|
package/dist/speech-provider.js
CHANGED
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
import { FISH_AUDIO_STREAM_MAX_BYTES, fishAudioTts, fishAudioTtsStream, listFishAudioVoices, normalizeFishAudioBaseUrl } from "./tts.js";
|
|
2
2
|
import { resolveGeneratedMediaMaxBytes } from "openclaw/plugin-sdk/media-generation-runtime";
|
|
3
3
|
import { normalizeResolvedSecretInputString } from "openclaw/plugin-sdk/secret-input";
|
|
4
|
-
import { asBoolean,
|
|
5
|
-
|
|
4
|
+
import { asBoolean, parseSpeechDirectiveNumberOverride, resolveSpeechProviderApiKey, trimToUndefined } from "openclaw/plugin-sdk/speech-core";
|
|
5
|
+
import { asFiniteNumberInRange, asOptionalRecord, parseBooleanValue } from "openclaw/plugin-sdk/string-coerce-runtime";
|
|
6
|
+
//#region extensions/fish-audio-speech/speech-provider.ts
|
|
6
7
|
const FISH_AUDIO_MODELS = [
|
|
7
8
|
"s2.1-pro-free",
|
|
8
9
|
"s2.1-pro",
|
|
@@ -25,14 +26,16 @@ function normalizeLatency(value) {
|
|
|
25
26
|
throw new Error(`invalid Fish Audio latency "${latency}"`);
|
|
26
27
|
}
|
|
27
28
|
function normalizeNumber(value, min, max) {
|
|
28
|
-
|
|
29
|
-
|
|
29
|
+
return asFiniteNumberInRange(value, {
|
|
30
|
+
min,
|
|
31
|
+
max
|
|
32
|
+
});
|
|
30
33
|
}
|
|
31
34
|
function resolveReferenceId(raw) {
|
|
32
35
|
return trimToUndefined(raw?.speakerVoiceId ?? raw?.voiceId ?? raw?.referenceId);
|
|
33
36
|
}
|
|
34
37
|
function normalizeProviderConfig(rawConfig) {
|
|
35
|
-
const raw =
|
|
38
|
+
const raw = asOptionalRecord(asOptionalRecord(rawConfig.providers)?.["fish-audio"]) ?? asOptionalRecord(rawConfig["fish-audio"]);
|
|
36
39
|
return {
|
|
37
40
|
apiKey: normalizeResolvedSecretInputString({
|
|
38
41
|
value: raw?.apiKey,
|
|
@@ -50,7 +53,7 @@ function normalizeProviderConfig(rawConfig) {
|
|
|
50
53
|
}
|
|
51
54
|
function readProviderConfig(config) {
|
|
52
55
|
const defaults = normalizeProviderConfig({});
|
|
53
|
-
const raw =
|
|
56
|
+
const raw = asOptionalRecord(config) ?? {};
|
|
54
57
|
return {
|
|
55
58
|
apiKey: trimToUndefined(raw.apiKey) ?? defaults.apiKey,
|
|
56
59
|
baseUrl: normalizeFishAudioBaseUrl(trimToUndefined(raw.baseUrl) ?? defaults.baseUrl),
|
|
@@ -64,7 +67,7 @@ function readProviderConfig(config) {
|
|
|
64
67
|
};
|
|
65
68
|
}
|
|
66
69
|
function readOverrides(overrides) {
|
|
67
|
-
const raw =
|
|
70
|
+
const raw = asOptionalRecord(overrides) ?? {};
|
|
68
71
|
return {
|
|
69
72
|
model: trimToUndefined(raw.model ?? raw.modelId) ? normalizeModel(raw.model ?? raw.modelId) : void 0,
|
|
70
73
|
referenceId: resolveReferenceId(raw),
|
|
@@ -164,29 +167,12 @@ function parseDirectiveToken(ctx) {
|
|
|
164
167
|
case "normalize":
|
|
165
168
|
case "fish_normalize": {
|
|
166
169
|
if (!ctx.policy.allowNormalization) return { handled: true };
|
|
167
|
-
const
|
|
168
|
-
if (
|
|
169
|
-
"true",
|
|
170
|
-
"1",
|
|
171
|
-
"yes",
|
|
172
|
-
"on"
|
|
173
|
-
].includes(value)) return {
|
|
170
|
+
const normalize = parseBooleanValue(ctx.value);
|
|
171
|
+
if (normalize !== void 0) return {
|
|
174
172
|
handled: true,
|
|
175
173
|
overrides: {
|
|
176
174
|
...ctx.currentOverrides,
|
|
177
|
-
normalize
|
|
178
|
-
}
|
|
179
|
-
};
|
|
180
|
-
if ([
|
|
181
|
-
"false",
|
|
182
|
-
"0",
|
|
183
|
-
"no",
|
|
184
|
-
"off"
|
|
185
|
-
].includes(value)) return {
|
|
186
|
-
handled: true,
|
|
187
|
-
overrides: {
|
|
188
|
-
...ctx.currentOverrides,
|
|
189
|
-
normalize: false
|
|
175
|
+
normalize
|
|
190
176
|
}
|
|
191
177
|
};
|
|
192
178
|
return {
|
package/dist/tts.js
CHANGED
|
@@ -1,8 +1,10 @@
|
|
|
1
|
+
import { asOptionalRecord } from "openclaw/plugin-sdk/string-coerce-runtime";
|
|
1
2
|
import { MAX_AUDIO_BYTES } from "openclaw/plugin-sdk/media-runtime";
|
|
3
|
+
import { createBoundedProviderBinaryStream } from "openclaw/plugin-sdk/provider-binary-stream";
|
|
2
4
|
import { assertOkOrThrowProviderError, assertProviderBinaryResponseContent, readProviderBinaryResponse, readProviderJsonResponse } from "openclaw/plugin-sdk/provider-http";
|
|
3
|
-
import {
|
|
5
|
+
import { trimToUndefined } from "openclaw/plugin-sdk/speech";
|
|
4
6
|
import { fetchWithSsrFGuard, ssrfPolicyFromHttpBaseUrlAllowedHostname } from "openclaw/plugin-sdk/ssrf-runtime";
|
|
5
|
-
//#region extensions/fish-audio/tts.ts
|
|
7
|
+
//#region extensions/fish-audio-speech/tts.ts
|
|
6
8
|
const FISH_AUDIO_BASE_URL = "https://api.fish.audio";
|
|
7
9
|
const FISH_AUDIO_VOICES_MAX_BYTES = 2 * 1024 * 1024;
|
|
8
10
|
const FISH_AUDIO_VOICE_PAGE_SIZE = 100;
|
|
@@ -51,65 +53,6 @@ async function fishAudioTts(params) {
|
|
|
51
53
|
await release();
|
|
52
54
|
}
|
|
53
55
|
}
|
|
54
|
-
function createBoundedFishAudioStream(stream, maxBytes) {
|
|
55
|
-
let reader;
|
|
56
|
-
let totalBytes = 0;
|
|
57
|
-
const releaseReader = (activeReader) => {
|
|
58
|
-
if (reader === activeReader) {
|
|
59
|
-
reader = void 0;
|
|
60
|
-
activeReader.releaseLock();
|
|
61
|
-
}
|
|
62
|
-
};
|
|
63
|
-
const cancelReader = async (reason) => {
|
|
64
|
-
const activeReader = reader;
|
|
65
|
-
if (!activeReader) return;
|
|
66
|
-
try {
|
|
67
|
-
await activeReader.cancel(reason).catch(() => void 0);
|
|
68
|
-
} finally {
|
|
69
|
-
releaseReader(activeReader);
|
|
70
|
-
}
|
|
71
|
-
};
|
|
72
|
-
return {
|
|
73
|
-
audioStream: new ReadableStream({
|
|
74
|
-
start() {
|
|
75
|
-
reader = stream.getReader();
|
|
76
|
-
},
|
|
77
|
-
async pull(controller) {
|
|
78
|
-
const activeReader = reader;
|
|
79
|
-
if (!activeReader) {
|
|
80
|
-
controller.close();
|
|
81
|
-
return;
|
|
82
|
-
}
|
|
83
|
-
try {
|
|
84
|
-
const chunk = await activeReader.read();
|
|
85
|
-
if (chunk.done) {
|
|
86
|
-
releaseReader(activeReader);
|
|
87
|
-
controller.close();
|
|
88
|
-
return;
|
|
89
|
-
}
|
|
90
|
-
const remaining = maxBytes - totalBytes;
|
|
91
|
-
if (chunk.value.byteLength > remaining) {
|
|
92
|
-
if (remaining > 0) controller.enqueue(chunk.value.subarray(0, remaining));
|
|
93
|
-
const error = /* @__PURE__ */ new Error(`Fish Audio TTS API error: audio response exceeds ${maxBytes} bytes`);
|
|
94
|
-
await activeReader.cancel(error).catch(() => void 0);
|
|
95
|
-
releaseReader(activeReader);
|
|
96
|
-
controller.error(error);
|
|
97
|
-
return;
|
|
98
|
-
}
|
|
99
|
-
totalBytes += chunk.value.byteLength;
|
|
100
|
-
controller.enqueue(chunk.value);
|
|
101
|
-
} catch (error) {
|
|
102
|
-
releaseReader(activeReader);
|
|
103
|
-
controller.error(error);
|
|
104
|
-
}
|
|
105
|
-
},
|
|
106
|
-
async cancel(reason) {
|
|
107
|
-
await cancelReader(reason);
|
|
108
|
-
}
|
|
109
|
-
}),
|
|
110
|
-
release: () => cancelReader(/* @__PURE__ */ new Error("Fish Audio TTS stream released"))
|
|
111
|
-
};
|
|
112
|
-
}
|
|
113
56
|
async function fishAudioTtsStream(params) {
|
|
114
57
|
const { response, release } = await requestFishAudioTts(params);
|
|
115
58
|
let handedOff = false;
|
|
@@ -117,7 +60,11 @@ async function fishAudioTtsStream(params) {
|
|
|
117
60
|
await assertOkOrThrowProviderError(response, "Fish Audio TTS API error");
|
|
118
61
|
assertProviderBinaryResponseContent(response, "Fish Audio TTS API error", "audio");
|
|
119
62
|
if (!response.body) throw new Error("Fish Audio TTS API response missing audio stream");
|
|
120
|
-
const bounded =
|
|
63
|
+
const bounded = createBoundedProviderBinaryStream(response.body, {
|
|
64
|
+
maxBytes: params.maxBytes,
|
|
65
|
+
createOverflowError: ({ maxBytes }) => /* @__PURE__ */ new Error(`Fish Audio TTS API error: audio response exceeds ${maxBytes} bytes`),
|
|
66
|
+
createReleaseError: () => /* @__PURE__ */ new Error("Fish Audio TTS stream released")
|
|
67
|
+
});
|
|
121
68
|
let releasePromise;
|
|
122
69
|
const releaseAll = () => {
|
|
123
70
|
releasePromise ??= (async () => {
|
|
@@ -131,7 +78,7 @@ async function fishAudioTtsStream(params) {
|
|
|
131
78
|
};
|
|
132
79
|
handedOff = true;
|
|
133
80
|
return {
|
|
134
|
-
audioStream: bounded.
|
|
81
|
+
audioStream: bounded.stream,
|
|
135
82
|
release: releaseAll
|
|
136
83
|
};
|
|
137
84
|
} finally {
|
|
@@ -139,7 +86,7 @@ async function fishAudioTtsStream(params) {
|
|
|
139
86
|
}
|
|
140
87
|
}
|
|
141
88
|
function parseVoiceItem(value) {
|
|
142
|
-
const item =
|
|
89
|
+
const item = asOptionalRecord(value);
|
|
143
90
|
const id = trimToUndefined(item?.["_id"]);
|
|
144
91
|
if (!id) return;
|
|
145
92
|
const languages = Array.isArray(item?.languages) ? item.languages.flatMap((entry) => typeof entry === "string" && entry.trim() ? [entry.trim()] : []) : [];
|
package/openclaw.plugin.json
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@openclaw/fish-audio-speech",
|
|
3
|
-
"version": "2026.
|
|
3
|
+
"version": "2026.8.1-beta.3",
|
|
4
4
|
"description": "OpenClaw Fish Audio speech plugin.",
|
|
5
5
|
"repository": {
|
|
6
6
|
"type": "git",
|
|
@@ -18,10 +18,10 @@
|
|
|
18
18
|
"minHostVersion": ">=2026.7.2"
|
|
19
19
|
},
|
|
20
20
|
"compat": {
|
|
21
|
-
"pluginApi": ">=2026.
|
|
21
|
+
"pluginApi": ">=2026.8.1-beta.3"
|
|
22
22
|
},
|
|
23
23
|
"build": {
|
|
24
|
-
"openclawVersion": "2026.
|
|
24
|
+
"openclawVersion": "2026.8.1-beta.3",
|
|
25
25
|
"bundledDist": false
|
|
26
26
|
},
|
|
27
27
|
"release": {
|
|
@@ -38,7 +38,7 @@
|
|
|
38
38
|
"README.md"
|
|
39
39
|
],
|
|
40
40
|
"peerDependencies": {
|
|
41
|
-
"openclaw": ">=2026.
|
|
41
|
+
"openclaw": ">=2026.8.1-beta.3"
|
|
42
42
|
},
|
|
43
43
|
"peerDependenciesMeta": {
|
|
44
44
|
"openclaw": {
|