@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.
@@ -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 };
@@ -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 { asBoolean, parseSpeechDirectiveNumberOverride, resolveSpeechProviderApiKey, trimToUndefined } from "openclaw/plugin-sdk/speech-core";
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 = trimToUndefined(value);
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 = trimToUndefined(value)?.toLowerCase();
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 trimToUndefined(raw?.speakerVoiceId ?? raw?.voiceId ?? raw?.referenceId);
34
+ return normalizeOptionalString(raw?.speakerVoiceId ?? raw?.voiceId ?? raw?.referenceId);
36
35
  }
37
36
  function normalizeProviderConfig(rawConfig) {
38
- const raw = asOptionalRecord(asOptionalRecord(rawConfig.providers)?.["fish-audio"]) ?? asOptionalRecord(rawConfig["fish-audio"]);
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(trimToUndefined(raw?.baseUrl)),
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: trimToUndefined(raw.apiKey) ?? defaults.apiKey,
59
- baseUrl: normalizeFishAudioBaseUrl(trimToUndefined(raw.baseUrl) ?? defaults.baseUrl),
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: trimToUndefined(raw.model ?? raw.modelId) ? normalizeModel(raw.model ?? raw.modelId) : void 0,
72
+ model: normalizeOptionalString(raw.model ?? raw.modelId) ? normalizeModel(raw.model ?? raw.modelId) : void 0,
73
73
  referenceId: resolveReferenceId(raw),
74
- latency: trimToUndefined(raw.latency) ? normalizeLatency(raw.latency) : void 0,
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
- ...trimToUndefined(talkProviderConfig.baseUrl) == null ? {} : { baseUrl: normalizeFishAudioBaseUrl(trimToUndefined(talkProviderConfig.baseUrl)) },
246
- ...trimToUndefined(talkProviderConfig.modelId ?? talkProviderConfig.model) == null ? {} : { model: normalizeModel(talkProviderConfig.modelId ?? talkProviderConfig.model) },
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
- ...trimToUndefined(talkProviderConfig.latency) == null ? {} : { latency: normalizeLatency(talkProviderConfig.latency) },
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
- ...trimToUndefined(params.modelId ?? params.model) == null ? {} : { model: normalizeModel(params.modelId ?? params.model) },
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(trimToUndefined(req.apiKey) ?? config.apiKey);
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(trimToUndefined(req.baseUrl) ?? config.baseUrl),
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(params),
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(params.maxBytes, FISH_AUDIO_STREAM_MAX_BYTES)
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(resolveSynthesisRequest({
294
- ...req,
295
- target: "telephony"
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 { asOptionalRecord } from "openclaw/plugin-sdk/string-coerce-runtime";
2
- import { MAX_AUDIO_BYTES } from "openclaw/plugin-sdk/media-runtime";
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 = 2 * 1024 * 1024;
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 Buffer.from(await readProviderBinaryResponse(response, "Fish Audio TTS API error", "audio", { maxBytes: params.maxBytes }));
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: releaseAll
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 = trimToUndefined(item?.["_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: trimToUndefined(item?.title),
97
- description: trimToUndefined(item?.description),
98
- category: trimToUndefined(item?.visibility),
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}` } },
@@ -1,6 +1,9 @@
1
1
  {
2
2
  "id": "fish-audio-speech",
3
- "legacyPluginIds": ["fish-audio"],
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": ["FISH_API_KEY", "FISH_AUDIO_API_KEY"]
16
+ "envVars": [
17
+ "FISH_API_KEY",
18
+ "FISH_AUDIO_API_KEY"
19
+ ]
14
20
  }
15
21
  ]
16
22
  },
17
23
  "contracts": {
18
- "speechProviders": ["fish-audio"]
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.1-beta.1",
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.1-beta.1"
21
+ "pluginApi": ">=2026.9.2"
22
22
  },
23
23
  "build": {
24
- "openclawVersion": "2026.9.1-beta.1",
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.1-beta.1"
41
+ "openclaw": ">=2026.9.2"
42
42
  },
43
43
  "peerDependenciesMeta": {
44
44
  "openclaw": {