@openclaw/fish-audio-speech 2026.10.10-beta.5 → 2026.11.5-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 CHANGED
@@ -13,10 +13,10 @@ remain `fish-audio`. Configure `tts.provider: "fish-audio"` and set
13
13
  `FISH_API_KEY`. The provider supports buffered audio, HTTP-streamed playback,
14
14
  native Opus voice notes, 8 kHz PCM telephony, and Fish Audio voice discovery.
15
15
 
16
- Set `tts.providers.fish-audio.model: "drama-3-preview"` to opt in to Drama 3
17
- voice direction. The default remains `s2.1-pro`. Natural-language directions
18
- such as `[calm, reassuring]` and paired tags such as `<whisper>...</whisper>`
19
- are passed through in the spoken text.
16
+ Set `tts.providers.fish-audio.model: "drama-3"` to opt in to Drama 3 voice
17
+ direction (`"drama-3-preview"` is still accepted). The default remains
18
+ `s2.1-pro`. Natural-language directions such as `[calm, reassuring]` and paired
19
+ tags such as `<whisper>...</whisper>` are passed through in the spoken text.
20
20
 
21
21
  See [Fish Audio](https://docs.openclaw.ai/providers/fish-audio) for setup,
22
22
  models, voice selection, expressive tags, and local macOS MLX usage.
@@ -0,0 +1 @@
1
+ {"version":3,"entries":[{"output":"capability-catalog.js","source":"extensions/fish-audio-speech/capability-catalog.ts"},{"output":"index.js","source":"extensions/fish-audio-speech/index.ts"},{"output":"speech-provider.js","source":"extensions/fish-audio-speech/speech-provider.ts"}],"chunks":[{"output":"capability-catalog.js","sha256":"7c9a47df6e8ec5e2db37ce36f3f23369e937a6deb650b55c529f73b37380621a","facadeSource":"extensions/fish-audio-speech/capability-catalog.ts","sources":["extensions/fish-audio-speech/capability-catalog.ts"]},{"output":"index.js","sha256":"7c1ea79ef77e53df527e73de571411ed79266f9cdbffa6a988e7b40b0b0e5edc","facadeSource":"extensions/fish-audio-speech/index.ts","sources":["extensions/fish-audio-speech/index.ts"]},{"output":"speech-provider.js","sha256":"9af3f302bf900c302ce7e95ec77cf92dbf93afbcc6f5ae1cf2bfdaa2538e1fed","facadeSource":"extensions/fish-audio-speech/speech-provider.ts","sources":["extensions/fish-audio-speech/speech-provider.ts"]}],"unrenderedSources":[]}
@@ -1,5 +1,4 @@
1
1
  import { buildFishAudioSpeechProvider } from "./speech-provider.js";
2
- //#region extensions/fish-audio-speech/capability-catalog.ts
2
+ // extensions/fish-audio-speech/capability-catalog.ts
3
3
  var capability_catalog_default = { speechProviders: [buildFishAudioSpeechProvider()] };
4
- //#endregion
5
4
  export { capability_catalog_default as default };
package/dist/index.js CHANGED
@@ -1,6 +1,6 @@
1
1
  import { buildFishAudioSpeechProvider } from "./speech-provider.js";
2
2
  import { definePluginEntry } from "openclaw/plugin-sdk/plugin-entry";
3
- //#region extensions/fish-audio-speech/index.ts
3
+ // extensions/fish-audio-speech/index.ts
4
4
  var fish_audio_speech_default = definePluginEntry({
5
5
  id: "fish-audio-speech",
6
6
  name: "Fish Audio Speech",
@@ -9,5 +9,4 @@ var fish_audio_speech_default = definePluginEntry({
9
9
  api.registerSpeechProvider(buildFishAudioSpeechProvider());
10
10
  }
11
11
  });
12
- //#endregion
13
12
  export { fish_audio_speech_default as default };
@@ -1,8 +1,76 @@
1
- import { FISH_AUDIO_MODELS, fishAudioTts, fishAudioTtsStream, listFishAudioVoices, normalizeFishAudioBaseUrl } from "./tts.js";
2
1
  import { normalizeResolvedSecretInputString } from "openclaw/plugin-sdk/secret-input";
3
- import { MAX_AUDIO_BYTES, parseSpeechDirectiveNumberOverride, resolveSpeechProviderApiKey } from "openclaw/plugin-sdk/speech-provider";
4
- import { asBoolean, asFiniteNumberInRange, asOptionalRecord, filterStringRecord, normalizeOptionalString, parseBooleanValue } from "openclaw/plugin-sdk/string-coerce-runtime";
5
- //#region extensions/fish-audio-speech/speech-provider.ts
2
+ import { MAX_AUDIO_BYTES, createSpeechDirectiveParser, resolveSpeechProviderApiKey } from "openclaw/plugin-sdk/speech-provider";
3
+ import { asBoolean, asFiniteNumberInRange, asOptionalRecord, filterStringRecord, normalizeOptionalString, normalizeTrimmedStringList, parseBooleanValue } from "openclaw/plugin-sdk/string-coerce-runtime";
4
+ // extensions/fish-audio-speech/speech-provider.ts
5
+ const FISH_AUDIO_BASE_URL = "https://api.fish.audio";
6
+ const FISH_AUDIO_VOICES_MAX_BYTES = 2097152;
7
+ const FISH_AUDIO_VOICE_PAGE_SIZE = 100;
8
+ const FISH_AUDIO_MAX_OWN_VOICE_PAGES = 20;
9
+ const FISH_AUDIO_MODELS = [
10
+ "drama-3",
11
+ "drama-3-preview",
12
+ "s2.1-pro-free",
13
+ "s2.1-pro",
14
+ "s2-pro",
15
+ "s1"
16
+ ];
17
+ function normalizeFishAudioBaseUrl(value) {
18
+ const trimmed = value?.trim();
19
+ return trimmed ? trimmed.replace(/\/+$/u, "") : FISH_AUDIO_BASE_URL;
20
+ }
21
+ async function requestFishAudioTts(params) {
22
+ const { fetchWithSsrFGuard, ssrfPolicyFromHttpBaseUrlAllowedHostname } = await import("openclaw/plugin-sdk/ssrf-runtime");
23
+ return await fetchWithSsrFGuard({
24
+ url: `${params.baseUrl}/v1/tts`,
25
+ init: {
26
+ method: "POST",
27
+ headers: {
28
+ Authorization: `Bearer ${params.apiKey}`,
29
+ "Content-Type": "application/json",
30
+ model: params.model
31
+ },
32
+ body: JSON.stringify({
33
+ text: params.text,
34
+ format: params.format,
35
+ ...params.referenceId ? { reference_id: params.referenceId } : {},
36
+ ...params.sampleRate == null ? {} : { sample_rate: params.sampleRate },
37
+ ...params.latency == null ? {} : { latency: params.latency },
38
+ ...params.speed == null ? {} : { prosody: { speed: params.speed } },
39
+ ...params.temperature == null ? {} : { temperature: params.temperature },
40
+ ...params.topP == null ? {} : { top_p: params.topP },
41
+ ...params.normalize == null ? {} : { normalize: params.normalize }
42
+ })
43
+ },
44
+ timeoutMs: params.timeoutMs,
45
+ policy: ssrfPolicyFromHttpBaseUrlAllowedHostname(params.baseUrl),
46
+ auditContext: "fish-audio.tts"
47
+ });
48
+ }
49
+ async function fishAudioTts(params) {
50
+ const { assertOkOrThrowProviderError, readProviderBinaryResponse } = await import("openclaw/plugin-sdk/provider-http");
51
+ const { response, release } = await requestFishAudioTts(params);
52
+ try {
53
+ await assertOkOrThrowProviderError(response, "Fish Audio TTS API error", { requestHeaders: { Authorization: `Bearer ${params.apiKey}` } });
54
+ return await readProviderBinaryResponse(response, "Fish Audio TTS API error", "audio", { maxBytes: params.maxBytes });
55
+ } finally {
56
+ await release();
57
+ }
58
+ }
59
+ function parseVoiceItem(value) {
60
+ const item = asOptionalRecord(value);
61
+ const id = normalizeOptionalString(item?.["_id"]);
62
+ if (!id) return;
63
+ const languages = normalizeTrimmedStringList(item?.languages);
64
+ const tags = normalizeTrimmedStringList(item?.tags);
65
+ return {
66
+ id,
67
+ name: normalizeOptionalString(item?.title),
68
+ description: normalizeOptionalString(item?.description),
69
+ category: normalizeOptionalString(item?.visibility),
70
+ locale: languages[0],
71
+ personalities: tags.length > 0 ? tags : void 0
72
+ };
73
+ }
6
74
  const DEFAULT_MODEL = "s2.1-pro";
7
75
  const DEFAULT_LATENCY = "balanced";
8
76
  const DEFAULT_TIMEOUT_MS = 24e4;
@@ -24,7 +92,7 @@ function resolveReferenceId(raw) {
24
92
  }
25
93
  function normalizeProviderConfig(rawConfig) {
26
94
  const providers = asOptionalRecord(rawConfig.providers);
27
- const raw = asOptionalRecord(providers?.["fish-audio"]) ?? asOptionalRecord(rawConfig["fish-audio"]);
95
+ const raw = asOptionalRecord(providers?.["fish-audio"]);
28
96
  return {
29
97
  apiKey: normalizeResolvedSecretInputString({
30
98
  value: raw?.apiKey,
@@ -50,129 +118,92 @@ function normalizeProviderConfig(rawConfig) {
50
118
  };
51
119
  }
52
120
  function readProviderConfig(config) {
53
- return normalizeProviderConfig({ "fish-audio": {
121
+ return normalizeProviderConfig({ providers: { "fish-audio": {
54
122
  ...config,
55
123
  apiKey: normalizeOptionalString(config.apiKey)
56
- } });
57
- }
58
- function readOverrides(overrides) {
59
- const raw = asOptionalRecord(overrides) ?? {};
60
- return {
61
- model: normalizeOptionalString(raw.model ?? raw.modelId) ? normalizeModel(raw.model ?? raw.modelId) : void 0,
62
- referenceId: resolveReferenceId(raw),
63
- latency: normalizeOptionalString(raw.latency) ? normalizeLatency(raw.latency) : void 0,
64
- speed: asFiniteNumberInRange(raw.speed, {
65
- min: .5,
66
- max: 2
67
- }),
68
- temperature: asFiniteNumberInRange(raw.temperature, {
69
- min: 0,
70
- max: 1
71
- }),
72
- topP: asFiniteNumberInRange(raw.topP ?? raw.top_p, {
73
- min: 0,
74
- max: 1
75
- }),
76
- normalize: asBoolean(raw.normalize)
77
- };
124
+ } } });
78
125
  }
79
126
  function resolveApiKey(configValue) {
80
127
  return resolveSpeechProviderApiKey(configValue, process.env.FISH_API_KEY, process.env.FISH_AUDIO_API_KEY);
81
128
  }
82
- function parseDirectiveToken(ctx) {
83
- switch (ctx.key) {
84
- case "voice":
85
- case "voiceid":
86
- case "voice_id":
87
- case "referenceid":
88
- case "reference_id":
89
- case "fish_voice":
90
- case "fishaudio_voice": return ctx.policy.allowVoice ? {
91
- handled: true,
92
- overrides: {
93
- ...ctx.currentOverrides,
94
- referenceId: ctx.value
95
- }
96
- } : { handled: true };
97
- case "model":
98
- case "modelid":
99
- case "model_id":
100
- case "fish_model":
101
- case "fishaudio_model":
102
- if (!ctx.policy.allowModelId) return { handled: true };
103
- try {
104
- return {
105
- handled: true,
106
- overrides: {
107
- ...ctx.currentOverrides,
108
- model: normalizeModel(ctx.value)
109
- }
110
- };
111
- } catch (error) {
112
- return {
113
- handled: true,
114
- warnings: [String(error)]
115
- };
116
- }
117
- case "speed":
118
- case "fish_speed":
119
- case "temperature":
120
- case "fish_temperature":
121
- case "top_p":
122
- case "topp":
123
- case "fish_top_p": {
124
- const setting = ctx.key.replace(/^fish_/, "");
125
- const overrideKey = setting === "top_p" || setting === "topp" ? "topP" : setting;
126
- const label = overrideKey === "topP" ? "top_p" : overrideKey;
127
- return parseSpeechDirectiveNumberOverride({
128
- ctx,
129
- overrideKey,
130
- range: overrideKey === "speed" ? {
131
- min: .5,
132
- max: 2
133
- } : {
134
- min: 0,
135
- max: 1
136
- },
137
- warning: (value) => `invalid Fish Audio ${label} "${value}"`
138
- });
139
- }
140
- case "latency":
141
- case "fish_latency":
142
- if (!ctx.policy.allowVoiceSettings) return { handled: true };
143
- try {
144
- return {
145
- handled: true,
146
- overrides: {
147
- ...ctx.currentOverrides,
148
- latency: normalizeLatency(ctx.value)
149
- }
150
- };
151
- } catch (error) {
152
- return {
153
- handled: true,
154
- warnings: [String(error)]
155
- };
156
- }
157
- case "normalize":
158
- case "fish_normalize": {
159
- if (!ctx.policy.allowNormalization) return { handled: true };
129
+ const parseDirectiveToken = createSpeechDirectiveParser({
130
+ referenceId: ["allowVoice", [
131
+ "voice",
132
+ "voiceid",
133
+ "voice_id",
134
+ "referenceid",
135
+ "reference_id",
136
+ "fish_voice",
137
+ "fishaudio_voice"
138
+ ]],
139
+ model: [
140
+ "allowModelId",
141
+ [
142
+ "model",
143
+ "modelid",
144
+ "model_id",
145
+ "fish_model",
146
+ "fishaudio_model"
147
+ ],
148
+ { value: (ctx) => normalizeModel(ctx.value) }
149
+ ],
150
+ speed: [
151
+ "allowVoiceSettings",
152
+ ["speed", "fish_speed"],
153
+ { number: {
154
+ range: {
155
+ min: .5,
156
+ max: 2
157
+ },
158
+ warning: (value) => `invalid Fish Audio speed "${value}"`
159
+ } }
160
+ ],
161
+ temperature: [
162
+ "allowVoiceSettings",
163
+ ["temperature", "fish_temperature"],
164
+ { number: {
165
+ range: {
166
+ min: 0,
167
+ max: 1
168
+ },
169
+ warning: (value) => `invalid Fish Audio temperature "${value}"`
170
+ } }
171
+ ],
172
+ topP: [
173
+ "allowVoiceSettings",
174
+ [
175
+ "top_p",
176
+ "topp",
177
+ "fish_top_p"
178
+ ],
179
+ { number: {
180
+ range: {
181
+ min: 0,
182
+ max: 1
183
+ },
184
+ warning: (value) => `invalid Fish Audio top_p "${value}"`
185
+ } }
186
+ ],
187
+ latency: [
188
+ "allowVoiceSettings",
189
+ ["latency", "fish_latency"],
190
+ { value: (ctx) => normalizeLatency(ctx.value) }
191
+ ],
192
+ normalize: [
193
+ "allowNormalization",
194
+ ["normalize", "fish_normalize"],
195
+ { parse: (ctx) => {
160
196
  const normalize = parseBooleanValue(ctx.value);
161
- if (normalize !== void 0) return {
197
+ return normalize !== void 0 ? {
162
198
  handled: true,
163
- overrides: {
164
- ...ctx.currentOverrides,
165
- normalize
166
- }
167
- };
168
- return {
199
+ overrides: { normalize }
200
+ } : {
169
201
  handled: true,
170
202
  warnings: [`invalid Fish Audio normalize "${ctx.value}"`]
171
203
  };
172
- }
173
- default: return { handled: false };
174
- }
175
- }
204
+ } }
205
+ ]
206
+ }, { formatErrorMessage: String });
176
207
  function resolveFormat(target) {
177
208
  if (target === "voice-note") return {
178
209
  format: "opus",
@@ -195,7 +226,25 @@ function resolveFormat(target) {
195
226
  }
196
227
  async function resolveSynthesisRequest(req) {
197
228
  const config = readProviderConfig(req.providerConfig);
198
- const overrides = readOverrides(req.providerOverrides);
229
+ const raw = req.providerOverrides ?? {};
230
+ const overrides = {
231
+ model: normalizeOptionalString(raw.model ?? raw.modelId) ? normalizeModel(raw.model ?? raw.modelId) : void 0,
232
+ referenceId: resolveReferenceId(raw),
233
+ latency: normalizeOptionalString(raw.latency) ? normalizeLatency(raw.latency) : void 0,
234
+ speed: asFiniteNumberInRange(raw.speed, {
235
+ min: .5,
236
+ max: 2
237
+ }),
238
+ temperature: asFiniteNumberInRange(raw.temperature, {
239
+ min: 0,
240
+ max: 1
241
+ }),
242
+ topP: asFiniteNumberInRange(raw.topP ?? raw.top_p, {
243
+ min: 0,
244
+ max: 1
245
+ }),
246
+ normalize: asBoolean(raw.normalize)
247
+ };
199
248
  const apiKey = resolveApiKey(config.apiKey);
200
249
  if (!apiKey) throw new Error("Fish Audio API key missing");
201
250
  const output = resolveFormat(req.target);
@@ -266,10 +315,52 @@ function buildFishAudioSpeechProvider() {
266
315
  const config = readProviderConfig(req.providerConfig ?? {});
267
316
  const apiKey = resolveApiKey(normalizeOptionalString(req.apiKey) ?? config.apiKey);
268
317
  if (!apiKey) throw new Error("Fish Audio API key missing");
269
- return await listFishAudioVoices({
270
- apiKey,
271
- baseUrl: normalizeFishAudioBaseUrl(normalizeOptionalString(req.baseUrl) ?? config.baseUrl),
272
- timeoutMs: req.timeoutMs ?? DEFAULT_TIMEOUT_MS
318
+ const baseUrl = normalizeFishAudioBaseUrl(normalizeOptionalString(req.baseUrl) ?? config.baseUrl);
319
+ const timeoutMs = req.timeoutMs ?? DEFAULT_TIMEOUT_MS;
320
+ const requestVoicePage = async (self, pageNumber) => {
321
+ const url = new URL(`${baseUrl}/model`);
322
+ url.searchParams.set("type", "tts");
323
+ url.searchParams.set("page_size", String(FISH_AUDIO_VOICE_PAGE_SIZE));
324
+ url.searchParams.set("page_number", String(pageNumber));
325
+ if (self) url.searchParams.set("self", "true");
326
+ else url.searchParams.set("sort_by", "score");
327
+ const { assertOkOrThrowProviderError, readProviderJsonResponse } = await import("openclaw/plugin-sdk/provider-http");
328
+ const { fetchWithSsrFGuard, ssrfPolicyFromHttpBaseUrlAllowedHostname } = await import("openclaw/plugin-sdk/ssrf-runtime");
329
+ const requestHeaders = { Authorization: `Bearer ${apiKey}` };
330
+ const { response, release } = await fetchWithSsrFGuard({
331
+ url: url.toString(),
332
+ init: { headers: requestHeaders },
333
+ timeoutMs,
334
+ policy: ssrfPolicyFromHttpBaseUrlAllowedHostname(baseUrl),
335
+ auditContext: "fish-audio.voices"
336
+ });
337
+ try {
338
+ await assertOkOrThrowProviderError(response, "Fish Audio voices API error", { requestHeaders });
339
+ return await readProviderJsonResponse(response, "Fish Audio voices", {
340
+ maxBytes: FISH_AUDIO_VOICES_MAX_BYTES,
341
+ requestHeaders
342
+ });
343
+ } finally {
344
+ await release();
345
+ }
346
+ };
347
+ const own = [];
348
+ for (let pageNumber = 1; pageNumber <= FISH_AUDIO_MAX_OWN_VOICE_PAGES; pageNumber += 1) {
349
+ const payload = await requestVoicePage(true, pageNumber);
350
+ const items = Array.isArray(payload.items) ? payload.items : [];
351
+ own.push(...items.flatMap((item) => parseVoiceItem(item) ?? []));
352
+ if (items.length < FISH_AUDIO_VOICE_PAGE_SIZE || own.length >= (payload.total ?? Number.MAX_SAFE_INTEGER)) break;
353
+ }
354
+ let publicVoices = [];
355
+ try {
356
+ const payload = await requestVoicePage(false, 1);
357
+ publicVoices = (Array.isArray(payload.items) ? payload.items : []).flatMap((item) => parseVoiceItem(item) ?? []);
358
+ } catch {}
359
+ const seen = /* @__PURE__ */ new Set();
360
+ return [...own, ...publicVoices].filter((voice) => {
361
+ if (seen.has(voice.id)) return false;
362
+ seen.add(voice.id);
363
+ return true;
273
364
  });
274
365
  },
275
366
  isConfigured,
@@ -285,30 +376,45 @@ function buildFishAudioSpeechProvider() {
285
376
  },
286
377
  streamSynthesize: async (req) => {
287
378
  const params = await resolveSynthesisRequest(req);
288
- const stream = await fishAudioTtsStream({
379
+ const streamParams = {
289
380
  ...params,
290
381
  maxBytes: Math.min(params.maxBytes, MAX_AUDIO_BYTES)
291
- });
292
- return {
293
- audioStream: stream.audioStream,
294
- outputFormat: params.format,
295
- fileExtension: params.fileExtension,
296
- voiceCompatible: params.voiceCompatible,
297
- release: stream.release
298
382
  };
383
+ const { createBoundedProviderBinaryStream } = await import("openclaw/plugin-sdk/provider-binary-stream");
384
+ const { assertOkOrThrowProviderError, assertProviderBinaryResponseContent } = await import("openclaw/plugin-sdk/provider-http");
385
+ const { response, release } = await requestFishAudioTts(streamParams);
386
+ try {
387
+ await assertOkOrThrowProviderError(response, "Fish Audio TTS API error", { requestHeaders: { Authorization: `Bearer ${streamParams.apiKey}` } });
388
+ assertProviderBinaryResponseContent(response, "Fish Audio TTS API error", "audio");
389
+ if (!response.body) throw new Error("Fish Audio TTS API response missing audio stream");
390
+ const bounded = createBoundedProviderBinaryStream(response.body, {
391
+ maxBytes: streamParams.maxBytes,
392
+ createOverflowError: ({ maxBytes }) => /* @__PURE__ */ new Error(`Fish Audio TTS API error: audio response exceeds ${maxBytes} bytes`),
393
+ createReleaseError: () => /* @__PURE__ */ new Error("Fish Audio TTS stream released"),
394
+ cleanup: release
395
+ });
396
+ return {
397
+ audioStream: bounded.stream,
398
+ outputFormat: params.format,
399
+ fileExtension: params.fileExtension,
400
+ voiceCompatible: params.voiceCompatible,
401
+ release: bounded.release
402
+ };
403
+ } catch (error) {
404
+ await release();
405
+ throw error;
406
+ }
299
407
  },
300
408
  synthesizeTelephony: async (req) => {
301
- const params = await resolveSynthesisRequest({
302
- ...req,
303
- target: "telephony"
304
- });
305
409
  return {
306
- audioBuffer: await fishAudioTts(params),
410
+ audioBuffer: await fishAudioTts(await resolveSynthesisRequest({
411
+ ...req,
412
+ target: "telephony"
413
+ })),
307
414
  outputFormat: "pcm",
308
415
  sampleRate: 8e3
309
416
  };
310
417
  }
311
418
  };
312
419
  }
313
- //#endregion
314
420
  export { buildFishAudioSpeechProvider };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@openclaw/fish-audio-speech",
3
- "version": "2026.10.10-beta.5",
3
+ "version": "2026.11.5-beta.1",
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.10.10-beta.5"
21
+ "pluginApi": ">=2026.11.5-beta.1"
22
22
  },
23
23
  "build": {
24
- "openclawVersion": "2026.10.10-beta.5",
24
+ "openclawVersion": "2026.11.5-beta.1",
25
25
  "bundledDist": false
26
26
  },
27
27
  "release": {
@@ -40,7 +40,7 @@
40
40
  "assets/activity.svg"
41
41
  ],
42
42
  "peerDependencies": {
43
- "openclaw": ">=2026.10.10-beta.5"
43
+ "openclaw": ">=2026.11.5-beta.1"
44
44
  },
45
45
  "peerDependenciesMeta": {
46
46
  "openclaw": {
@@ -1 +0,0 @@
1
- {"version":3,"entries":[{"output":"capability-catalog.js","source":"extensions/fish-audio-speech/capability-catalog.ts"},{"output":"index.js","source":"extensions/fish-audio-speech/index.ts"},{"output":"speech-provider.js","source":"extensions/fish-audio-speech/speech-provider.ts"},{"output":"tts.js","source":"extensions/fish-audio-speech/tts.ts"}],"chunks":[{"output":"capability-catalog.js","sha256":"86ae39a827853c503b7e5f857893700679f66cd8a58c7e34890a5144c7766676","facadeSource":"extensions/fish-audio-speech/capability-catalog.ts","sources":["extensions/fish-audio-speech/capability-catalog.ts"]},{"output":"index.js","sha256":"0e6cca9921213c83fb902489aca7f1dbbdb3913168353b013596a5e8e6d5d231","facadeSource":"extensions/fish-audio-speech/index.ts","sources":["extensions/fish-audio-speech/index.ts"]},{"output":"speech-provider.js","sha256":"a2d388087f256961dbb09d334bffcee23be99a55cef752651dde7fa9e6b520ba","facadeSource":"extensions/fish-audio-speech/speech-provider.ts","sources":["extensions/fish-audio-speech/speech-provider.ts"]},{"output":"tts.js","sha256":"c222808a2bc42e57f644b00d92dc715ed60771a38c8763cc0947984b65388cb9","facadeSource":"extensions/fish-audio-speech/tts.ts","sources":["extensions/fish-audio-speech/tts.ts"]}],"unrenderedSources":[]}
package/dist/tts.js DELETED
@@ -1,151 +0,0 @@
1
- import { asOptionalRecord, normalizeOptionalString, normalizeTrimmedStringList } from "openclaw/plugin-sdk/string-coerce-runtime";
2
- //#region extensions/fish-audio-speech/tts.ts
3
- const FISH_AUDIO_BASE_URL = "https://api.fish.audio";
4
- const FISH_AUDIO_VOICES_MAX_BYTES = 2097152;
5
- const FISH_AUDIO_VOICE_PAGE_SIZE = 100;
6
- const FISH_AUDIO_MAX_OWN_VOICE_PAGES = 20;
7
- const FISH_AUDIO_MODELS = [
8
- "drama-3-preview",
9
- "s2.1-pro-free",
10
- "s2.1-pro",
11
- "s2-pro",
12
- "s1"
13
- ];
14
- function normalizeFishAudioBaseUrl(value) {
15
- const trimmed = value?.trim();
16
- return trimmed ? trimmed.replace(/\/+$/u, "") : FISH_AUDIO_BASE_URL;
17
- }
18
- function buildFishAudioRequestBody(params) {
19
- return JSON.stringify({
20
- text: params.text,
21
- format: params.format,
22
- ...params.referenceId ? { reference_id: params.referenceId } : {},
23
- ...params.sampleRate == null ? {} : { sample_rate: params.sampleRate },
24
- ...params.latency == null ? {} : { latency: params.latency },
25
- ...params.speed == null ? {} : { prosody: { speed: params.speed } },
26
- ...params.temperature == null ? {} : { temperature: params.temperature },
27
- ...params.topP == null ? {} : { top_p: params.topP },
28
- ...params.normalize == null ? {} : { normalize: params.normalize }
29
- });
30
- }
31
- async function requestFishAudioTts(params) {
32
- const baseUrl = normalizeFishAudioBaseUrl(params.baseUrl);
33
- const { fetchWithSsrFGuard, ssrfPolicyFromHttpBaseUrlAllowedHostname } = await import("openclaw/plugin-sdk/ssrf-runtime");
34
- return await fetchWithSsrFGuard({
35
- url: `${baseUrl}/v1/tts`,
36
- init: {
37
- method: "POST",
38
- headers: {
39
- Authorization: `Bearer ${params.apiKey}`,
40
- "Content-Type": "application/json",
41
- model: params.model
42
- },
43
- body: buildFishAudioRequestBody(params)
44
- },
45
- timeoutMs: params.timeoutMs,
46
- policy: ssrfPolicyFromHttpBaseUrlAllowedHostname(baseUrl),
47
- auditContext: "fish-audio.tts"
48
- });
49
- }
50
- async function fishAudioTts(params) {
51
- const { assertOkOrThrowProviderError, readProviderBinaryResponse } = await import("openclaw/plugin-sdk/provider-http");
52
- const { response, release } = await requestFishAudioTts(params);
53
- try {
54
- await assertOkOrThrowProviderError(response, "Fish Audio TTS API error");
55
- return await readProviderBinaryResponse(response, "Fish Audio TTS API error", "audio", { maxBytes: params.maxBytes });
56
- } finally {
57
- await release();
58
- }
59
- }
60
- async function fishAudioTtsStream(params) {
61
- const { createBoundedProviderBinaryStream } = await import("openclaw/plugin-sdk/provider-binary-stream");
62
- const { assertOkOrThrowProviderError, assertProviderBinaryResponseContent } = await import("openclaw/plugin-sdk/provider-http");
63
- const { response, release } = await requestFishAudioTts(params);
64
- let handedOff = false;
65
- try {
66
- await assertOkOrThrowProviderError(response, "Fish Audio TTS API error");
67
- assertProviderBinaryResponseContent(response, "Fish Audio TTS API error", "audio");
68
- if (!response.body) throw new Error("Fish Audio TTS API response missing audio stream");
69
- const bounded = createBoundedProviderBinaryStream(response.body, {
70
- maxBytes: params.maxBytes,
71
- createOverflowError: ({ maxBytes }) => /* @__PURE__ */ new Error(`Fish Audio TTS API error: audio response exceeds ${maxBytes} bytes`),
72
- createReleaseError: () => /* @__PURE__ */ new Error("Fish Audio TTS stream released"),
73
- cleanup: release
74
- });
75
- handedOff = true;
76
- return {
77
- audioStream: bounded.stream,
78
- release: bounded.release
79
- };
80
- } finally {
81
- if (!handedOff) await release();
82
- }
83
- }
84
- function parseVoiceItem(value) {
85
- const item = asOptionalRecord(value);
86
- const id = normalizeOptionalString(item?.["_id"]);
87
- if (!id) return;
88
- const languages = normalizeTrimmedStringList(item?.languages);
89
- const tags = normalizeTrimmedStringList(item?.tags);
90
- return {
91
- id,
92
- name: normalizeOptionalString(item?.title),
93
- description: normalizeOptionalString(item?.description),
94
- category: normalizeOptionalString(item?.visibility),
95
- locale: languages[0],
96
- personalities: tags.length > 0 ? tags : void 0
97
- };
98
- }
99
- async function requestVoicePage(params) {
100
- const url = new URL(`${normalizeFishAudioBaseUrl(params.baseUrl)}/model`);
101
- url.searchParams.set("type", "tts");
102
- url.searchParams.set("page_size", String(FISH_AUDIO_VOICE_PAGE_SIZE));
103
- url.searchParams.set("page_number", String(params.pageNumber));
104
- if (params.self) url.searchParams.set("self", "true");
105
- else url.searchParams.set("sort_by", "score");
106
- const { assertOkOrThrowProviderError, readProviderJsonResponse } = await import("openclaw/plugin-sdk/provider-http");
107
- const { fetchWithSsrFGuard, ssrfPolicyFromHttpBaseUrlAllowedHostname } = await import("openclaw/plugin-sdk/ssrf-runtime");
108
- const { response, release } = await fetchWithSsrFGuard({
109
- url: url.toString(),
110
- init: { headers: { Authorization: `Bearer ${params.apiKey}` } },
111
- timeoutMs: params.timeoutMs,
112
- policy: ssrfPolicyFromHttpBaseUrlAllowedHostname(params.baseUrl),
113
- auditContext: "fish-audio.voices"
114
- });
115
- try {
116
- await assertOkOrThrowProviderError(response, "Fish Audio voices API error");
117
- return await readProviderJsonResponse(response, "Fish Audio voices", { maxBytes: FISH_AUDIO_VOICES_MAX_BYTES });
118
- } finally {
119
- await release();
120
- }
121
- }
122
- async function listFishAudioVoices(params) {
123
- const own = [];
124
- for (let pageNumber = 1; pageNumber <= FISH_AUDIO_MAX_OWN_VOICE_PAGES; pageNumber += 1) {
125
- const payload = await requestVoicePage({
126
- ...params,
127
- self: true,
128
- pageNumber
129
- });
130
- const items = Array.isArray(payload.items) ? payload.items : [];
131
- own.push(...items.flatMap((item) => parseVoiceItem(item) ?? []));
132
- if (items.length < FISH_AUDIO_VOICE_PAGE_SIZE || own.length >= (payload.total ?? Number.MAX_SAFE_INTEGER)) break;
133
- }
134
- let publicVoices = [];
135
- try {
136
- const payload = await requestVoicePage({
137
- ...params,
138
- self: false,
139
- pageNumber: 1
140
- });
141
- publicVoices = (Array.isArray(payload.items) ? payload.items : []).flatMap((item) => parseVoiceItem(item) ?? []);
142
- } catch {}
143
- const seen = /* @__PURE__ */ new Set();
144
- return [...own, ...publicVoices].filter((voice) => {
145
- if (seen.has(voice.id)) return false;
146
- seen.add(voice.id);
147
- return true;
148
- });
149
- }
150
- //#endregion
151
- export { FISH_AUDIO_MODELS, fishAudioTts, fishAudioTtsStream, listFishAudioVoices, normalizeFishAudioBaseUrl };