@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 CHANGED
@@ -2,9 +2,16 @@
2
2
 
3
3
  Official OpenClaw speech provider for Fish Audio's hosted S2.1 API.
4
4
 
5
- Configure `tts.provider: "fish-audio"` and set `FISH_API_KEY`. The provider
6
- supports buffered audio, HTTP-streamed playback, native Opus voice notes,
7
- 8 kHz PCM telephony, and Fish Audio voice discovery.
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 fish_audio_default = definePluginEntry({
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 { fish_audio_default as default };
13
+ export { fish_audio_speech_default as default };
@@ -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, asFiniteNumber, asObject, parseSpeechDirectiveNumberOverride, resolveSpeechProviderApiKey, trimToUndefined } from "openclaw/plugin-sdk/speech-core";
5
- //#region extensions/fish-audio/speech-provider.ts
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
- const number = asFiniteNumber(value);
29
- return number != null && number >= min && number <= max ? number : void 0;
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 = asObject(asObject(rawConfig.providers)?.["fish-audio"]) ?? asObject(rawConfig["fish-audio"]);
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 = asObject(config) ?? {};
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 = asObject(overrides) ?? {};
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 value = ctx.value.trim().toLowerCase();
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: true
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 { asObject, trimToUndefined } from "openclaw/plugin-sdk/speech";
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 = createBoundedFishAudioStream(response.body, params.maxBytes);
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.audioStream,
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 = asObject(value);
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()] : []) : [];
@@ -1,5 +1,6 @@
1
1
  {
2
- "id": "fish-audio",
2
+ "id": "fish-audio-speech",
3
+ "legacyPluginIds": ["fish-audio"],
3
4
  "activation": {
4
5
  "onStartup": false
5
6
  },
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@openclaw/fish-audio-speech",
3
- "version": "2026.7.2-beta.7",
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.7.2-beta.7"
21
+ "pluginApi": ">=2026.8.1-beta.3"
22
22
  },
23
23
  "build": {
24
- "openclawVersion": "2026.7.2-beta.7",
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.7.2-beta.7"
41
+ "openclaw": ">=2026.8.1-beta.3"
42
42
  },
43
43
  "peerDependenciesMeta": {
44
44
  "openclaw": {