@ai-sdk/fish-audio 0.0.0 → 2.0.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.
@@ -0,0 +1,346 @@
1
+ ---
2
+ title: Fish Audio
3
+ description: Learn how to use the Fish Audio provider for the AI SDK.
4
+ ---
5
+
6
+ # Fish Audio Provider
7
+
8
+ The [Fish Audio](https://fish.audio/) provider contains speech generation (S1 and
9
+ S2 models) and speech-to-text transcription support.
10
+
11
+ ## Setup
12
+
13
+ The Fish Audio provider is available in the `@ai-sdk/fish-audio` module. You can install it with
14
+
15
+ <Tabs items={['pnpm', 'npm', 'yarn', 'bun']}>
16
+ <Tab>
17
+ <Snippet text="pnpm add @ai-sdk/fish-audio" dark />
18
+ </Tab>
19
+ <Tab>
20
+ <Snippet text="npm install @ai-sdk/fish-audio" dark />
21
+ </Tab>
22
+ <Tab>
23
+ <Snippet text="yarn add @ai-sdk/fish-audio" dark />
24
+ </Tab>
25
+
26
+ <Tab>
27
+ <Snippet text="bun add @ai-sdk/fish-audio" dark />
28
+ </Tab>
29
+ </Tabs>
30
+
31
+ ## Provider Instance
32
+
33
+ You can import the default provider instance `fishAudio` from `@ai-sdk/fish-audio`:
34
+
35
+ ```ts
36
+ import { fishAudio } from '@ai-sdk/fish-audio';
37
+ ```
38
+
39
+ If you need a customized setup, you can import `createFishAudio` from `@ai-sdk/fish-audio` and create a provider instance with your settings:
40
+
41
+ ```ts
42
+ import { createFishAudio } from '@ai-sdk/fish-audio';
43
+
44
+ const fishAudio = createFishAudio({
45
+ // custom settings, e.g.
46
+ fetch: customFetch,
47
+ });
48
+ ```
49
+
50
+ You can use the following optional settings to customize the Fish Audio provider instance:
51
+
52
+ - **apiKey** _string_
53
+
54
+ API key that is being sent using the `Authorization` header.
55
+ It defaults to the `FISH_AUDIO_API_KEY` environment variable.
56
+
57
+ - **baseURL** _string_
58
+
59
+ Base URL for the API calls.
60
+ Defaults to `https://api.fish.audio`.
61
+
62
+ - **headers** _Record&lt;string,string&gt;_
63
+
64
+ Custom headers to include in the requests.
65
+
66
+ - **fetch** _(input: RequestInfo, init?: RequestInit) => Promise&lt;Response&gt;_
67
+
68
+ Custom [fetch](https://developer.mozilla.org/en-US/docs/Web/API/fetch) implementation.
69
+ Defaults to the global `fetch` function.
70
+ You can use it as a middleware to intercept requests,
71
+ or to provide a custom fetch implementation for e.g. testing.
72
+
73
+ ## Speech Models
74
+
75
+ You can create models that call the [Fish Audio text-to-speech API](https://docs.fish.audio/api-reference/endpoint/openapi-v1/text-to-speech)
76
+ using the `.speech()` factory method.
77
+
78
+ ```ts
79
+ import { fishAudio } from '@ai-sdk/fish-audio';
80
+ import { generateSpeech } from 'ai';
81
+
82
+ const { audio } = await generateSpeech({
83
+ model: fishAudio.speech('s1'),
84
+ text: 'Hello from Fish Audio!',
85
+ });
86
+ ```
87
+
88
+ The `voice` option selects a Fish Audio voice model ID (`reference_id`), either
89
+ from the [Fish Audio voice library](https://fish.audio/) or one of your own
90
+ uploaded models. Omit it to use the default voice.
91
+
92
+ ```ts
93
+ const { audio } = await generateSpeech({
94
+ model: fishAudio.speech('s1'),
95
+ text: 'Hello from Fish Audio!',
96
+ voice: '933563129e564b19a115bedd57b7406a',
97
+ outputFormat: 'opus',
98
+ speed: 1.1,
99
+ });
100
+ ```
101
+
102
+ Voice listing is not part of the AI SDK speech model specification, so browse
103
+ voices with the [Fish Audio list-models
104
+ endpoint](https://docs.fish.audio/api-reference/endpoint/openapi-v1/list-models)
105
+ directly:
106
+
107
+ ```ts
108
+ const response = await fetch(
109
+ 'https://api.fish.audio/model?page_size=20&sort_by=task_count',
110
+ { headers: { Authorization: `Bearer ${process.env.FISH_AUDIO_API_KEY}` } },
111
+ );
112
+ const { items } = await response.json();
113
+ // Each item's `_id` is a value you can pass as `voice`.
114
+ ```
115
+
116
+ Add `self=true` to list only your own uploaded models, or `language=en` / `tag=narration` to filter.
117
+
118
+ ### Provider Options
119
+
120
+ The following provider options are available:
121
+
122
+ - **referenceId** _string | array of strings_
123
+
124
+ Voice model ID(s). A single ID selects one speaker; an array enables
125
+ multi-speaker dialogue (S2-Pro models). Takes precedence over the top-level
126
+ `voice` option.
127
+ Optional.
128
+
129
+ - **sampleRate** _number_
130
+
131
+ Output sample rate in Hz. Falls back to the format default when unset
132
+ (44100 Hz for `wav`/`pcm`/`mp3`, 48000 Hz for `opus`).
133
+ Optional.
134
+
135
+ - **mp3Bitrate** _64 | 128 | 192_
136
+
137
+ Bitrate in kbps for `mp3` output. Ignored for other formats.
138
+ Optional.
139
+
140
+ - **opusBitrate** _-1000 | 24000 | 32000 | 48000 | 64000_
141
+
142
+ Bitrate in bps for `opus` output, where `-1000` selects automatic. Ignored for
143
+ other formats.
144
+ Optional.
145
+
146
+ - **latency** _'low' | 'normal' | 'balanced'_
147
+
148
+ Latency/quality tradeoff. `normal` gives the best quality, `balanced` reduces
149
+ latency, and `low` is the fastest.
150
+ Optional.
151
+
152
+ - **volume** _number_
153
+
154
+ Volume offset in dB. Negative values are quieter.
155
+ Optional.
156
+
157
+ - **normalizeLoudness** _boolean_
158
+
159
+ Loudness normalization. Supported by the S2 family (`s2-pro` and
160
+ `s2.1-pro`). Fish Audio accepts it on `s1` but ignores it, so the provider
161
+ drops it and emits a warning in that case.
162
+ Optional.
163
+
164
+ - **temperature** _number_
165
+
166
+ Governs expressiveness (0 to 1). Higher values are more varied.
167
+ Optional.
168
+
169
+ - **topP** _number_
170
+
171
+ Controls diversity via nucleus sampling (0 to 1).
172
+ Optional.
173
+
174
+ - **chunkLength** _number_
175
+
176
+ Text segment size for processing (100 to 300).
177
+ Optional.
178
+
179
+ - **minChunkLength** _number_
180
+
181
+ Minimum characters before splitting into a new chunk (0 to 100).
182
+ Optional.
183
+
184
+ - **normalize** _boolean_
185
+
186
+ Text normalization for English and Chinese. Helps stability with numbers.
187
+ Optional.
188
+
189
+ - **maxNewTokens** _number_
190
+
191
+ Maximum audio tokens to generate per text chunk.
192
+ Optional.
193
+
194
+ - **repetitionPenalty** _number_
195
+
196
+ Values above 1.0 discourage repeated audio patterns.
197
+ Optional.
198
+
199
+ - **conditionOnPreviousChunks** _boolean_
200
+
201
+ Reuse prior audio as context for voice consistency across chunks.
202
+ Optional.
203
+
204
+ - **earlyStopThreshold** _number_
205
+
206
+ Early-stop threshold used in batch processing (0 to 1).
207
+ Optional.
208
+
209
+ - **features** _array of strings_
210
+
211
+ Request-scoped flags passed through to the inference backend, e.g.
212
+ `['quality-guard']`.
213
+ Optional.
214
+
215
+ ### Multi-Speaker Dialogue
216
+
217
+ S2-Pro models support multi-speaker dialogue. Pass an array of voice model IDs
218
+ via `referenceId` and mark turns in the text with `<|speaker:N|>`, where `N`
219
+ indexes into that array.
220
+
221
+ ```ts
222
+ const { audio } = await generateSpeech({
223
+ model: fishAudio.speech('s2-pro'),
224
+ text: '<|speaker:0|>Hello!<|speaker:1|>Hi there!',
225
+ providerOptions: {
226
+ fishAudio: {
227
+ referenceId: [
228
+ '933563129e564b19a115bedd57b7406a',
229
+ 'bf322df2096a46f18c579d0baa36f41d',
230
+ ],
231
+ },
232
+ },
233
+ });
234
+ ```
235
+
236
+ ### Output Formats
237
+
238
+ Fish Audio supports the `wav`, `pcm`, `mp3`, and `opus` output formats. Any other
239
+ value falls back to `mp3` and produces a warning.
240
+
241
+ <Note>
242
+ Fish Audio infers the language from the input text and the selected voice, and
243
+ has no language parameter. The `language` and `instructions` options are not
244
+ supported and produce warnings.
245
+ </Note>
246
+
247
+ ### Model Capabilities
248
+
249
+ | Model | Multi-Speaker | Notes |
250
+ | --------------- | ------------------- | ------------------------------------------------------------------------- |
251
+ | `s1` | | Ignores `normalizeLoudness` |
252
+ | `s2-pro` | <Check size={18} /> | Supports `normalizeLoudness` |
253
+ | `s2.1-pro` | <Check size={18} /> | Recommended default; supports `normalizeLoudness` |
254
+ | `s2.1-pro-free` | | Free developer tier; no time-to-first-audio or data-processing guarantees |
255
+
256
+ <Note>
257
+ Streaming text-to-speech (Fish Audio's TTS-live WebSocket and timestamped
258
+ streaming endpoints) is not currently supported, and neither is inline
259
+ zero-shot voice cloning via `references`, which requires a MessagePack request
260
+ body. Upload reference audio to Fish Audio and pass its `reference_id` via
261
+ `voice` or `referenceId` instead.
262
+ </Note>
263
+
264
+ ## Transcription Models
265
+
266
+ You can create models that call the [Fish Audio speech-to-text API](https://docs.fish.audio/api-reference/endpoint/openapi-v1/speech-to-text)
267
+ using the `.transcription()` factory method.
268
+
269
+ ```ts
270
+ import { fishAudio } from '@ai-sdk/fish-audio';
271
+ import { transcribe } from 'ai';
272
+ import { readFile } from 'node:fs/promises';
273
+
274
+ const result = await transcribe({
275
+ model: fishAudio.transcription(),
276
+ audio: await readFile('audio.mp3'),
277
+ });
278
+ ```
279
+
280
+ The Fish Audio speech-to-text endpoint currently exposes no model selector and
281
+ serves a single model, so the model ID is optional and defaults to
282
+ `'transcribe-1'`. It is a routing label and is not sent to the API. Fish Audio
283
+ expects to add more ASR models and to select them with the `model` HTTP header,
284
+ matching the text-to-speech endpoint.
285
+
286
+ ### Provider Options
287
+
288
+ The following provider options are available:
289
+
290
+ - **language** _string_
291
+
292
+ Language of the audio. A hint only: Fish Audio passes it to the model, but
293
+ auto-detection is authoritative and overrides it, so it changes neither the
294
+ transcript nor the reported language.
295
+ Optional.
296
+
297
+ - **ignoreTimestamps** _boolean_
298
+
299
+ Whether to skip precise timestamps. Mirrors the Fish Audio
300
+ `ignore_timestamps` parameter, whose API default is `true`. This provider
301
+ defaults it to `false` so that `segments` is populated. Fish Audio documents
302
+ an added latency cost for audio shorter than 30 seconds; set this to `true` to
303
+ trade segments for that latency.
304
+ Optional.
305
+
306
+ ```ts
307
+ const result = await transcribe({
308
+ model: fishAudio.transcription(),
309
+ audio: await readFile('audio.mp3'),
310
+ providerOptions: {
311
+ fishAudio: {
312
+ language: 'en',
313
+ ignoreTimestamps: false,
314
+ },
315
+ },
316
+ });
317
+ ```
318
+
319
+ `result.language` reports the detected language as an ISO-639-1 code (e.g. `en`).
320
+ It is always a two-letter code, never a locale such as `en-US`, and is
321
+ `undefined` when Fish Audio detects no language.
322
+
323
+ The human-readable language name (e.g. `English`) is available as provider
324
+ metadata:
325
+
326
+ ```ts
327
+ console.log(result.language); // 'en'
328
+ console.log(result.providerMetadata?.fishAudio?.language); // 'English'
329
+ ```
330
+
331
+ <Note>
332
+ The provider metadata `language` is a display name intended for presentation.
333
+ Its exact form is not guaranteed, so avoid matching or branching on it — use
334
+ `result.language` for anything programmatic.
335
+ </Note>
336
+
337
+ <Note>
338
+ Setting `ignoreTimestamps` to `true` makes Fish Audio return an empty
339
+ `segments` array. The provider therefore requests timestamps by default.
340
+ </Note>
341
+
342
+ ### Model Capabilities
343
+
344
+ | Model | Transcription | Duration | Segments | Language |
345
+ | -------------- | ------------------- | ------------------- | ------------------- | ------------------- |
346
+ | `transcribe-1` | <Check size={18} /> | <Check size={18} /> | <Check size={18} /> | <Check size={18} /> |
package/package.json CHANGED
@@ -1,20 +1,76 @@
1
1
  {
2
2
  "name": "@ai-sdk/fish-audio",
3
- "version": "0.0.0",
4
- "description": "AI SDK provider for Fish Audio",
5
- "main": "index.js",
6
- "scripts": {
7
- "test": "echo \"Error: no test specified\" && exit 1"
8
- },
9
- "keywords": [],
10
- "author": "",
11
- "license": "Apache-2.0",
3
+ "version": "2.0.1",
12
4
  "type": "module",
13
- "publishConfig": {
14
- "access": "public"
15
- },
5
+ "license": "Apache-2.0",
6
+ "sideEffects": false,
7
+ "main": "./dist/index.js",
8
+ "types": "./dist/index.d.ts",
16
9
  "files": [
17
- "index.js",
10
+ "dist/**/*",
11
+ "docs/**/*",
12
+ "src",
13
+ "!src/**/*.test.ts",
14
+ "!src/**/*.test-d.ts",
15
+ "!src/**/__snapshots__",
16
+ "!src/**/__fixtures__",
17
+ "CHANGELOG.md",
18
18
  "README.md"
19
- ]
20
- }
19
+ ],
20
+ "directories": {
21
+ "doc": "./docs"
22
+ },
23
+ "exports": {
24
+ "./package.json": "./package.json",
25
+ ".": {
26
+ "types": "./dist/index.d.ts",
27
+ "import": "./dist/index.js",
28
+ "default": "./dist/index.js"
29
+ }
30
+ },
31
+ "dependencies": {
32
+ "@ai-sdk/provider": "3.0.14",
33
+ "@ai-sdk/provider-utils": "4.0.42"
34
+ },
35
+ "devDependencies": {
36
+ "@types/node": "20.17.24",
37
+ "tsup": "^8",
38
+ "typescript": "5.6.3",
39
+ "zod": "3.25.76",
40
+ "@ai-sdk/test-server": "1.0.6",
41
+ "@vercel/ai-tsconfig": "0.0.0"
42
+ },
43
+ "peerDependencies": {
44
+ "zod": "^3.25.76 || ^4.1.8"
45
+ },
46
+ "engines": {
47
+ "node": ">=18"
48
+ },
49
+ "publishConfig": {
50
+ "access": "public",
51
+ "provenance": true
52
+ },
53
+ "homepage": "https://ai-sdk.dev/docs",
54
+ "repository": {
55
+ "type": "git",
56
+ "url": "https://github.com/vercel/ai",
57
+ "directory": "packages/fish-audio"
58
+ },
59
+ "bugs": {
60
+ "url": "https://github.com/vercel/ai/issues"
61
+ },
62
+ "keywords": [
63
+ "ai"
64
+ ],
65
+ "scripts": {
66
+ "build": "tsup --tsconfig tsconfig.build.json",
67
+ "build:watch": "tsup --tsconfig tsconfig.build.json --watch",
68
+ "clean": "del-cli dist docs",
69
+ "type-check": "tsc --noEmit",
70
+ "test": "pnpm test:node && pnpm test:edge",
71
+ "test:update": "pnpm test:node -u",
72
+ "test:watch": "vitest --config vitest.node.config.js",
73
+ "test:edge": "vitest --config vitest.edge.config.js --run",
74
+ "test:node": "vitest --config vitest.node.config.js --run"
75
+ }
76
+ }
@@ -0,0 +1,8 @@
1
+ import type { FetchFunction } from '@ai-sdk/provider-utils';
2
+
3
+ export type FishAudioConfig = {
4
+ provider: string;
5
+ url: (options: { modelId: string; path: string }) => string;
6
+ headers?: () => Record<string, string | undefined>;
7
+ fetch?: FetchFunction;
8
+ };
@@ -0,0 +1,17 @@
1
+ import { createJsonErrorResponseHandler } from '@ai-sdk/provider-utils';
2
+ import { z } from 'zod/v4';
3
+
4
+ // Fish Audio returns `{ status, message }` for documented error responses
5
+ // (401 no permission, 402 no payment).
6
+ // https://docs.fish.audio/api-reference/endpoint/openapi-v1/text-to-speech
7
+ export const fishAudioErrorDataSchema = z.object({
8
+ status: z.number().nullish(),
9
+ message: z.string().nullish(),
10
+ });
11
+
12
+ export type FishAudioErrorData = z.infer<typeof fishAudioErrorDataSchema>;
13
+
14
+ export const fishAudioFailedResponseHandler = createJsonErrorResponseHandler({
15
+ errorSchema: fishAudioErrorDataSchema,
16
+ errorToMessage: data => data.message ?? 'Unknown Fish Audio error',
17
+ });
@@ -0,0 +1,163 @@
1
+ import {
2
+ NoSuchModelError,
3
+ type ProviderV3,
4
+ type SpeechModelV3,
5
+ type TranscriptionModelV3,
6
+ } from '@ai-sdk/provider';
7
+ import {
8
+ loadApiKey,
9
+ withUserAgentSuffix,
10
+ type FetchFunction,
11
+ } from '@ai-sdk/provider-utils';
12
+ import { FishAudioSpeechModel } from './fish-audio-speech-model';
13
+ import type { FishAudioSpeechModelId } from './fish-audio-speech-options';
14
+ import { FishAudioTranscriptionModel } from './fish-audio-transcription-model';
15
+ import type { FishAudioTranscriptionModelId } from './fish-audio-transcription-options';
16
+ import { VERSION } from './version';
17
+
18
+ export interface FishAudioProvider extends ProviderV3 {
19
+ (
20
+ modelId: FishAudioSpeechModelId,
21
+ settings?: {},
22
+ ): {
23
+ speech: FishAudioSpeechModel;
24
+ };
25
+
26
+ /**
27
+ * Creates a model for speech generation.
28
+ */
29
+ speech(modelId: FishAudioSpeechModelId): SpeechModelV3;
30
+
31
+ /**
32
+ * Creates a model for speech generation.
33
+ *
34
+ * Narrowed to required: Fish Audio always provides speech models.
35
+ */
36
+ speechModel(modelId: FishAudioSpeechModelId): SpeechModelV3;
37
+
38
+ /**
39
+ * Creates a model for transcription.
40
+ */
41
+ transcription(modelId?: FishAudioTranscriptionModelId): TranscriptionModelV3;
42
+
43
+ /**
44
+ * Creates a model for transcription.
45
+ *
46
+ * Narrowed to required: Fish Audio always provides a transcription model.
47
+ */
48
+ transcriptionModel(
49
+ modelId?: FishAudioTranscriptionModelId,
50
+ ): TranscriptionModelV3;
51
+ }
52
+
53
+ export interface FishAudioProviderSettings {
54
+ /**
55
+ * API key for authenticating requests.
56
+ */
57
+ apiKey?: string;
58
+
59
+ /**
60
+ * Base URL for the API calls.
61
+ */
62
+ baseURL?: string;
63
+
64
+ /**
65
+ * Custom headers to include in the requests.
66
+ */
67
+ headers?: Record<string, string>;
68
+
69
+ /**
70
+ * Custom fetch implementation. You can use it as a middleware to intercept requests,
71
+ * or to provide a custom fetch implementation for e.g. testing.
72
+ */
73
+ fetch?: FetchFunction;
74
+ }
75
+
76
+ const DEFAULT_BASE_URL = 'https://api.fish.audio';
77
+
78
+ /**
79
+ * Create a Fish Audio provider instance.
80
+ */
81
+ export function createFishAudio(
82
+ options: FishAudioProviderSettings = {},
83
+ ): FishAudioProvider {
84
+ const baseURL = options.baseURL?.replace(/\/$/, '') ?? DEFAULT_BASE_URL;
85
+
86
+ const getHeaders = () =>
87
+ withUserAgentSuffix(
88
+ {
89
+ Authorization: `Bearer ${loadApiKey({
90
+ apiKey: options.apiKey,
91
+ environmentVariableName: 'FISH_AUDIO_API_KEY',
92
+ description: 'Fish Audio',
93
+ })}`,
94
+ ...options.headers,
95
+ },
96
+ `ai-sdk/fish-audio/${VERSION}`,
97
+ );
98
+
99
+ const createSpeechModel = (modelId: FishAudioSpeechModelId) =>
100
+ new FishAudioSpeechModel(modelId, {
101
+ provider: 'fish-audio.speech',
102
+ url: ({ path }) => `${baseURL}${path}`,
103
+ headers: getHeaders,
104
+ fetch: options.fetch,
105
+ });
106
+
107
+ // `/v1/asr` has no model selector, so the model ID is a routing label and
108
+ // defaults to `transcribe-1`.
109
+ const createTranscriptionModel = (
110
+ modelId: FishAudioTranscriptionModelId = 'transcribe-1',
111
+ ) =>
112
+ new FishAudioTranscriptionModel(modelId, {
113
+ provider: 'fish-audio.transcription',
114
+ url: ({ path }) => `${baseURL}${path}`,
115
+ headers: getHeaders,
116
+ fetch: options.fetch,
117
+ });
118
+
119
+ const provider = function (modelId: FishAudioSpeechModelId) {
120
+ return {
121
+ speech: createSpeechModel(modelId),
122
+ };
123
+ };
124
+
125
+ provider.specificationVersion = 'v3' as const;
126
+ provider.speech = createSpeechModel;
127
+ provider.speechModel = createSpeechModel;
128
+ provider.transcription = createTranscriptionModel;
129
+ provider.transcriptionModel = createTranscriptionModel;
130
+
131
+ // Required ProviderV3 methods that are not supported
132
+ provider.languageModel = (modelId: string) => {
133
+ throw new NoSuchModelError({
134
+ modelId,
135
+ modelType: 'languageModel',
136
+ message: 'Fish Audio does not provide language models',
137
+ });
138
+ };
139
+
140
+ provider.embeddingModel = (modelId: string) => {
141
+ throw new NoSuchModelError({
142
+ modelId,
143
+ modelType: 'embeddingModel',
144
+ message: 'Fish Audio does not provide embedding models',
145
+ });
146
+ };
147
+ provider.textEmbeddingModel = provider.embeddingModel;
148
+
149
+ provider.imageModel = (modelId: string) => {
150
+ throw new NoSuchModelError({
151
+ modelId,
152
+ modelType: 'imageModel',
153
+ message: 'Fish Audio does not provide image models',
154
+ });
155
+ };
156
+
157
+ return provider as FishAudioProvider;
158
+ }
159
+
160
+ /**
161
+ * Default Fish Audio provider instance.
162
+ */
163
+ export const fishAudio = createFishAudio();