@openclaw/fish-audio-speech 2026.9.1-beta.1 → 2026.9.2
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/dist/capability-catalog.js +5 -0
- package/dist/speech-provider.js +34 -25
- package/dist/tts.js +17 -25
- package/openclaw.plugin.json +11 -3
- package/package.json +4 -4
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
import { buildFishAudioSpeechProvider } from "./speech-provider.js";
|
|
2
|
+
//#region extensions/fish-audio-speech/capability-catalog.ts
|
|
3
|
+
var capability_catalog_default = { speechProviders: [buildFishAudioSpeechProvider()] };
|
|
4
|
+
//#endregion
|
|
5
|
+
export { capability_catalog_default as default };
|
package/dist/speech-provider.js
CHANGED
|
@@ -1,8 +1,7 @@
|
|
|
1
1
|
import { FISH_AUDIO_STREAM_MAX_BYTES, fishAudioTts, fishAudioTtsStream, listFishAudioVoices, normalizeFishAudioBaseUrl } from "./tts.js";
|
|
2
|
-
import { resolveGeneratedMediaMaxBytes } from "openclaw/plugin-sdk/media-generation-runtime";
|
|
3
2
|
import { normalizeResolvedSecretInputString } from "openclaw/plugin-sdk/secret-input";
|
|
4
|
-
import {
|
|
5
|
-
import { asFiniteNumberInRange, asOptionalRecord, parseBooleanValue } from "openclaw/plugin-sdk/string-coerce-runtime";
|
|
3
|
+
import { parseSpeechDirectiveNumberOverride, resolveSpeechProviderApiKey } from "openclaw/plugin-sdk/speech-provider";
|
|
4
|
+
import { asBoolean, asFiniteNumberInRange, asOptionalRecord, normalizeOptionalString, parseBooleanValue } from "openclaw/plugin-sdk/string-coerce-runtime";
|
|
6
5
|
//#region extensions/fish-audio-speech/speech-provider.ts
|
|
7
6
|
const FISH_AUDIO_MODELS = [
|
|
8
7
|
"s2.1-pro-free",
|
|
@@ -14,13 +13,13 @@ const DEFAULT_MODEL = "s2.1-pro";
|
|
|
14
13
|
const DEFAULT_LATENCY = "balanced";
|
|
15
14
|
const DEFAULT_TIMEOUT_MS = 24e4;
|
|
16
15
|
function normalizeModel(value) {
|
|
17
|
-
const model =
|
|
16
|
+
const model = normalizeOptionalString(value);
|
|
18
17
|
if (!model) return DEFAULT_MODEL;
|
|
19
18
|
if (FISH_AUDIO_MODELS.some((candidate) => candidate === model)) return model;
|
|
20
19
|
throw new Error(`invalid Fish Audio model "${model}"`);
|
|
21
20
|
}
|
|
22
21
|
function normalizeLatency(value) {
|
|
23
|
-
const latency =
|
|
22
|
+
const latency = normalizeOptionalString(value)?.toLowerCase();
|
|
24
23
|
if (!latency) return DEFAULT_LATENCY;
|
|
25
24
|
if (latency === "low" || latency === "balanced" || latency === "normal") return latency;
|
|
26
25
|
throw new Error(`invalid Fish Audio latency "${latency}"`);
|
|
@@ -32,16 +31,17 @@ function normalizeNumber(value, min, max) {
|
|
|
32
31
|
});
|
|
33
32
|
}
|
|
34
33
|
function resolveReferenceId(raw) {
|
|
35
|
-
return
|
|
34
|
+
return normalizeOptionalString(raw?.speakerVoiceId ?? raw?.voiceId ?? raw?.referenceId);
|
|
36
35
|
}
|
|
37
36
|
function normalizeProviderConfig(rawConfig) {
|
|
38
|
-
const
|
|
37
|
+
const providers = asOptionalRecord(rawConfig.providers);
|
|
38
|
+
const raw = asOptionalRecord(providers?.["fish-audio"]) ?? asOptionalRecord(rawConfig["fish-audio"]);
|
|
39
39
|
return {
|
|
40
40
|
apiKey: normalizeResolvedSecretInputString({
|
|
41
41
|
value: raw?.apiKey,
|
|
42
42
|
path: "tts.providers.fish-audio.apiKey"
|
|
43
43
|
}),
|
|
44
|
-
baseUrl: normalizeFishAudioBaseUrl(
|
|
44
|
+
baseUrl: normalizeFishAudioBaseUrl(normalizeOptionalString(raw?.baseUrl)),
|
|
45
45
|
model: normalizeModel(raw?.model ?? raw?.modelId),
|
|
46
46
|
referenceId: resolveReferenceId(raw),
|
|
47
47
|
latency: normalizeLatency(raw?.latency),
|
|
@@ -55,8 +55,8 @@ function readProviderConfig(config) {
|
|
|
55
55
|
const defaults = normalizeProviderConfig({});
|
|
56
56
|
const raw = asOptionalRecord(config) ?? {};
|
|
57
57
|
return {
|
|
58
|
-
apiKey:
|
|
59
|
-
baseUrl: normalizeFishAudioBaseUrl(
|
|
58
|
+
apiKey: normalizeOptionalString(raw.apiKey) ?? defaults.apiKey,
|
|
59
|
+
baseUrl: normalizeFishAudioBaseUrl(normalizeOptionalString(raw.baseUrl) ?? defaults.baseUrl),
|
|
60
60
|
model: normalizeModel(raw.model ?? raw.modelId ?? defaults.model),
|
|
61
61
|
referenceId: resolveReferenceId(raw) ?? defaults.referenceId,
|
|
62
62
|
latency: normalizeLatency(raw.latency ?? defaults.latency),
|
|
@@ -69,9 +69,9 @@ function readProviderConfig(config) {
|
|
|
69
69
|
function readOverrides(overrides) {
|
|
70
70
|
const raw = asOptionalRecord(overrides) ?? {};
|
|
71
71
|
return {
|
|
72
|
-
model:
|
|
72
|
+
model: normalizeOptionalString(raw.model ?? raw.modelId) ? normalizeModel(raw.model ?? raw.modelId) : void 0,
|
|
73
73
|
referenceId: resolveReferenceId(raw),
|
|
74
|
-
latency:
|
|
74
|
+
latency: normalizeOptionalString(raw.latency) ? normalizeLatency(raw.latency) : void 0,
|
|
75
75
|
speed: normalizeNumber(raw.speed, .5, 2),
|
|
76
76
|
temperature: normalizeNumber(raw.temperature, 0, 1),
|
|
77
77
|
topP: normalizeNumber(raw.topP ?? raw.top_p, 0, 1),
|
|
@@ -221,7 +221,6 @@ function resolveSynthesisRequest(req) {
|
|
|
221
221
|
topP: overrides.topP ?? config.topP,
|
|
222
222
|
normalize: overrides.normalize ?? config.normalize,
|
|
223
223
|
timeoutMs: req.timeoutMs,
|
|
224
|
-
maxBytes: resolveGeneratedMediaMaxBytes(req.cfg, "audio"),
|
|
225
224
|
...output
|
|
226
225
|
};
|
|
227
226
|
}
|
|
@@ -242,33 +241,37 @@ function buildFishAudioSpeechProvider() {
|
|
|
242
241
|
value: talkProviderConfig.apiKey,
|
|
243
242
|
path: "talk.providers.fish-audio.apiKey"
|
|
244
243
|
}) },
|
|
245
|
-
...
|
|
246
|
-
...
|
|
244
|
+
...normalizeOptionalString(talkProviderConfig.baseUrl) == null ? {} : { baseUrl: normalizeFishAudioBaseUrl(normalizeOptionalString(talkProviderConfig.baseUrl)) },
|
|
245
|
+
...normalizeOptionalString(talkProviderConfig.modelId ?? talkProviderConfig.model) == null ? {} : { model: normalizeModel(talkProviderConfig.modelId ?? talkProviderConfig.model) },
|
|
247
246
|
...resolveReferenceId(talkProviderConfig) == null ? {} : { referenceId: resolveReferenceId(talkProviderConfig) },
|
|
248
|
-
...
|
|
247
|
+
...normalizeOptionalString(talkProviderConfig.latency) == null ? {} : { latency: normalizeLatency(talkProviderConfig.latency) },
|
|
249
248
|
...normalizeNumber(talkProviderConfig.speed, .5, 2) == null ? {} : { speed: normalizeNumber(talkProviderConfig.speed, .5, 2) }
|
|
250
249
|
};
|
|
251
250
|
},
|
|
252
251
|
resolveTalkOverrides: ({ params }) => ({
|
|
253
|
-
...
|
|
252
|
+
...normalizeOptionalString(params.modelId ?? params.model) == null ? {} : { model: normalizeModel(params.modelId ?? params.model) },
|
|
254
253
|
...resolveReferenceId(params) == null ? {} : { referenceId: resolveReferenceId(params) },
|
|
255
254
|
...normalizeNumber(params.speed, .5, 2) == null ? {} : { speed: normalizeNumber(params.speed, .5, 2) }
|
|
256
255
|
}),
|
|
257
256
|
listVoices: async (req) => {
|
|
258
257
|
const config = readProviderConfig(req.providerConfig ?? {});
|
|
259
|
-
const apiKey = resolveApiKey(
|
|
258
|
+
const apiKey = resolveApiKey(normalizeOptionalString(req.apiKey) ?? config.apiKey);
|
|
260
259
|
if (!apiKey) throw new Error("Fish Audio API key missing");
|
|
261
260
|
return await listFishAudioVoices({
|
|
262
261
|
apiKey,
|
|
263
|
-
baseUrl: normalizeFishAudioBaseUrl(
|
|
262
|
+
baseUrl: normalizeFishAudioBaseUrl(normalizeOptionalString(req.baseUrl) ?? config.baseUrl),
|
|
264
263
|
timeoutMs: req.timeoutMs ?? DEFAULT_TIMEOUT_MS
|
|
265
264
|
});
|
|
266
265
|
},
|
|
267
266
|
isConfigured: ({ providerConfig }) => Boolean(resolveApiKey(readProviderConfig(providerConfig).apiKey)),
|
|
268
267
|
synthesize: async (req) => {
|
|
269
268
|
const params = resolveSynthesisRequest(req);
|
|
269
|
+
const { resolveGeneratedMediaMaxBytes } = await import("openclaw/plugin-sdk/media-generation-runtime");
|
|
270
270
|
return {
|
|
271
|
-
audioBuffer: await fishAudioTts(
|
|
271
|
+
audioBuffer: await fishAudioTts({
|
|
272
|
+
...params,
|
|
273
|
+
maxBytes: resolveGeneratedMediaMaxBytes(req.cfg, "audio")
|
|
274
|
+
}),
|
|
272
275
|
outputFormat: params.format,
|
|
273
276
|
fileExtension: params.fileExtension,
|
|
274
277
|
voiceCompatible: params.voiceCompatible
|
|
@@ -276,9 +279,10 @@ function buildFishAudioSpeechProvider() {
|
|
|
276
279
|
},
|
|
277
280
|
streamSynthesize: async (req) => {
|
|
278
281
|
const params = resolveSynthesisRequest(req);
|
|
282
|
+
const { resolveGeneratedMediaMaxBytes } = await import("openclaw/plugin-sdk/media-generation-runtime");
|
|
279
283
|
const stream = await fishAudioTtsStream({
|
|
280
284
|
...params,
|
|
281
|
-
maxBytes: Math.min(
|
|
285
|
+
maxBytes: Math.min(resolveGeneratedMediaMaxBytes(req.cfg, "audio"), FISH_AUDIO_STREAM_MAX_BYTES)
|
|
282
286
|
});
|
|
283
287
|
return {
|
|
284
288
|
audioStream: stream.audioStream,
|
|
@@ -289,11 +293,16 @@ function buildFishAudioSpeechProvider() {
|
|
|
289
293
|
};
|
|
290
294
|
},
|
|
291
295
|
synthesizeTelephony: async (req) => {
|
|
296
|
+
const params = resolveSynthesisRequest({
|
|
297
|
+
...req,
|
|
298
|
+
target: "telephony"
|
|
299
|
+
});
|
|
300
|
+
const { resolveGeneratedMediaMaxBytes } = await import("openclaw/plugin-sdk/media-generation-runtime");
|
|
292
301
|
return {
|
|
293
|
-
audioBuffer: await fishAudioTts(
|
|
294
|
-
...
|
|
295
|
-
|
|
296
|
-
})
|
|
302
|
+
audioBuffer: await fishAudioTts({
|
|
303
|
+
...params,
|
|
304
|
+
maxBytes: resolveGeneratedMediaMaxBytes(req.cfg, "audio")
|
|
305
|
+
}),
|
|
297
306
|
outputFormat: "pcm",
|
|
298
307
|
sampleRate: 8e3
|
|
299
308
|
};
|
package/dist/tts.js
CHANGED
|
@@ -1,12 +1,8 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
3
|
-
import { createBoundedProviderBinaryStream } from "openclaw/plugin-sdk/provider-binary-stream";
|
|
4
|
-
import { assertOkOrThrowProviderError, assertProviderBinaryResponseContent, readProviderBinaryResponse, readProviderJsonResponse } from "openclaw/plugin-sdk/provider-http";
|
|
5
|
-
import { trimToUndefined } from "openclaw/plugin-sdk/speech";
|
|
6
|
-
import { fetchWithSsrFGuard, ssrfPolicyFromHttpBaseUrlAllowedHostname } from "openclaw/plugin-sdk/ssrf-runtime";
|
|
1
|
+
import { MAX_AUDIO_BYTES } from "openclaw/plugin-sdk/speech-provider";
|
|
2
|
+
import { asOptionalRecord, normalizeOptionalString } from "openclaw/plugin-sdk/string-coerce-runtime";
|
|
7
3
|
//#region extensions/fish-audio-speech/tts.ts
|
|
8
4
|
const FISH_AUDIO_BASE_URL = "https://api.fish.audio";
|
|
9
|
-
const FISH_AUDIO_VOICES_MAX_BYTES =
|
|
5
|
+
const FISH_AUDIO_VOICES_MAX_BYTES = 2097152;
|
|
10
6
|
const FISH_AUDIO_VOICE_PAGE_SIZE = 100;
|
|
11
7
|
const FISH_AUDIO_MAX_OWN_VOICE_PAGES = 20;
|
|
12
8
|
function normalizeFishAudioBaseUrl(value) {
|
|
@@ -28,6 +24,7 @@ function buildFishAudioRequestBody(params) {
|
|
|
28
24
|
}
|
|
29
25
|
async function requestFishAudioTts(params) {
|
|
30
26
|
const baseUrl = normalizeFishAudioBaseUrl(params.baseUrl);
|
|
27
|
+
const { fetchWithSsrFGuard, ssrfPolicyFromHttpBaseUrlAllowedHostname } = await import("openclaw/plugin-sdk/ssrf-runtime");
|
|
31
28
|
return await fetchWithSsrFGuard({
|
|
32
29
|
url: `${baseUrl}/v1/tts`,
|
|
33
30
|
init: {
|
|
@@ -45,15 +42,18 @@ async function requestFishAudioTts(params) {
|
|
|
45
42
|
});
|
|
46
43
|
}
|
|
47
44
|
async function fishAudioTts(params) {
|
|
45
|
+
const { assertOkOrThrowProviderError, readProviderBinaryResponse } = await import("openclaw/plugin-sdk/provider-http");
|
|
48
46
|
const { response, release } = await requestFishAudioTts(params);
|
|
49
47
|
try {
|
|
50
48
|
await assertOkOrThrowProviderError(response, "Fish Audio TTS API error");
|
|
51
|
-
return
|
|
49
|
+
return await readProviderBinaryResponse(response, "Fish Audio TTS API error", "audio", { maxBytes: params.maxBytes });
|
|
52
50
|
} finally {
|
|
53
51
|
await release();
|
|
54
52
|
}
|
|
55
53
|
}
|
|
56
54
|
async function fishAudioTtsStream(params) {
|
|
55
|
+
const { createBoundedProviderBinaryStream } = await import("openclaw/plugin-sdk/provider-binary-stream");
|
|
56
|
+
const { assertOkOrThrowProviderError, assertProviderBinaryResponseContent } = await import("openclaw/plugin-sdk/provider-http");
|
|
57
57
|
const { response, release } = await requestFishAudioTts(params);
|
|
58
58
|
let handedOff = false;
|
|
59
59
|
try {
|
|
@@ -63,23 +63,13 @@ async function fishAudioTtsStream(params) {
|
|
|
63
63
|
const bounded = createBoundedProviderBinaryStream(response.body, {
|
|
64
64
|
maxBytes: params.maxBytes,
|
|
65
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")
|
|
66
|
+
createReleaseError: () => /* @__PURE__ */ new Error("Fish Audio TTS stream released"),
|
|
67
|
+
cleanup: release
|
|
67
68
|
});
|
|
68
|
-
let releasePromise;
|
|
69
|
-
const releaseAll = () => {
|
|
70
|
-
releasePromise ??= (async () => {
|
|
71
|
-
try {
|
|
72
|
-
await bounded.release();
|
|
73
|
-
} finally {
|
|
74
|
-
await release();
|
|
75
|
-
}
|
|
76
|
-
})();
|
|
77
|
-
return releasePromise;
|
|
78
|
-
};
|
|
79
69
|
handedOff = true;
|
|
80
70
|
return {
|
|
81
71
|
audioStream: bounded.stream,
|
|
82
|
-
release:
|
|
72
|
+
release: bounded.release
|
|
83
73
|
};
|
|
84
74
|
} finally {
|
|
85
75
|
if (!handedOff) await release();
|
|
@@ -87,15 +77,15 @@ async function fishAudioTtsStream(params) {
|
|
|
87
77
|
}
|
|
88
78
|
function parseVoiceItem(value) {
|
|
89
79
|
const item = asOptionalRecord(value);
|
|
90
|
-
const id =
|
|
80
|
+
const id = normalizeOptionalString(item?.["_id"]);
|
|
91
81
|
if (!id) return;
|
|
92
82
|
const languages = Array.isArray(item?.languages) ? item.languages.flatMap((entry) => typeof entry === "string" && entry.trim() ? [entry.trim()] : []) : [];
|
|
93
83
|
const tags = Array.isArray(item?.tags) ? item.tags.flatMap((entry) => typeof entry === "string" && entry.trim() ? [entry.trim()] : []) : [];
|
|
94
84
|
return {
|
|
95
85
|
id,
|
|
96
|
-
name:
|
|
97
|
-
description:
|
|
98
|
-
category:
|
|
86
|
+
name: normalizeOptionalString(item?.title),
|
|
87
|
+
description: normalizeOptionalString(item?.description),
|
|
88
|
+
category: normalizeOptionalString(item?.visibility),
|
|
99
89
|
locale: languages[0],
|
|
100
90
|
personalities: tags.length > 0 ? tags : void 0
|
|
101
91
|
};
|
|
@@ -107,6 +97,8 @@ async function requestVoicePage(params) {
|
|
|
107
97
|
url.searchParams.set("page_number", String(params.pageNumber));
|
|
108
98
|
if (params.self) url.searchParams.set("self", "true");
|
|
109
99
|
else url.searchParams.set("sort_by", "score");
|
|
100
|
+
const { assertOkOrThrowProviderError, readProviderJsonResponse } = await import("openclaw/plugin-sdk/provider-http");
|
|
101
|
+
const { fetchWithSsrFGuard, ssrfPolicyFromHttpBaseUrlAllowedHostname } = await import("openclaw/plugin-sdk/ssrf-runtime");
|
|
110
102
|
const { response, release } = await fetchWithSsrFGuard({
|
|
111
103
|
url: url.toString(),
|
|
112
104
|
init: { headers: { Authorization: `Bearer ${params.apiKey}` } },
|
package/openclaw.plugin.json
CHANGED
|
@@ -1,6 +1,9 @@
|
|
|
1
1
|
{
|
|
2
2
|
"id": "fish-audio-speech",
|
|
3
|
-
"
|
|
3
|
+
"capabilityCatalogEntry": "./dist/capability-catalog.js",
|
|
4
|
+
"legacyPluginIds": [
|
|
5
|
+
"fish-audio"
|
|
6
|
+
],
|
|
4
7
|
"activation": {
|
|
5
8
|
"onStartup": false
|
|
6
9
|
},
|
|
@@ -10,12 +13,17 @@
|
|
|
10
13
|
"providers": [
|
|
11
14
|
{
|
|
12
15
|
"id": "fish-audio",
|
|
13
|
-
"envVars": [
|
|
16
|
+
"envVars": [
|
|
17
|
+
"FISH_API_KEY",
|
|
18
|
+
"FISH_AUDIO_API_KEY"
|
|
19
|
+
]
|
|
14
20
|
}
|
|
15
21
|
]
|
|
16
22
|
},
|
|
17
23
|
"contracts": {
|
|
18
|
-
"speechProviders": [
|
|
24
|
+
"speechProviders": [
|
|
25
|
+
"fish-audio"
|
|
26
|
+
]
|
|
19
27
|
},
|
|
20
28
|
"configSchema": {
|
|
21
29
|
"type": "object",
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@openclaw/fish-audio-speech",
|
|
3
|
-
"version": "2026.9.
|
|
3
|
+
"version": "2026.9.2",
|
|
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.9.
|
|
21
|
+
"pluginApi": ">=2026.9.2"
|
|
22
22
|
},
|
|
23
23
|
"build": {
|
|
24
|
-
"openclawVersion": "2026.9.
|
|
24
|
+
"openclawVersion": "2026.9.2",
|
|
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.9.
|
|
41
|
+
"openclaw": ">=2026.9.2"
|
|
42
42
|
},
|
|
43
43
|
"peerDependenciesMeta": {
|
|
44
44
|
"openclaw": {
|