@speechify/ai-sdk-provider 0.0.0-stage → 0.1.0

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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Speechify <developers@speechify.ai> (https://speechify.ai)
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md CHANGED
@@ -1,3 +1,180 @@
1
- # Temporary Holding Version
1
+ # SpeechifyAI provider for the Vercel AI SDK
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ `@speechify/ai-sdk-provider` adds [SpeechifyAI](https://speechify.ai) text to speech to the [Vercel AI SDK](https://ai-sdk.dev).
4
+ It implements the AI SDK's speech model specification (`SpeechModelV4`), so `generateSpeech()` works with Speechify's Simba 3 models and returns word-level speech marks with every clip.
5
+ It is maintained by Speechify and calls [`POST /v1/audio/speech`](https://docs.speechify.ai/build/api-reference/v1/audio/speech) with `fetch`, with no other runtime dependency.
6
+
7
+ ## Install
8
+
9
+ ```bash
10
+ npm install @speechify/ai-sdk-provider ai
11
+ ```
12
+
13
+ Requires AI SDK 7 (`ai@^7` with `@ai-sdk/provider@^4`), which runs on Node.js 22 or later and other runtimes with `fetch`.
14
+
15
+ Set your API key, which you can get by [signing up for free](https://platform.speechify.ai/signup?utm_source=npm&utm_medium=readme&utm_campaign=vercel-ai-sdk):
16
+
17
+ ```bash
18
+ export SPEECHIFY_API_KEY="your-api-key"
19
+ ```
20
+
21
+ ## Example
22
+
23
+ ```ts
24
+ import { writeFile } from 'node:fs/promises';
25
+ import { generateSpeech } from 'ai';
26
+ import { speechify } from '@speechify/ai-sdk-provider';
27
+
28
+ const { audio, providerMetadata } = await generateSpeech({
29
+ model: speechify.speech('simba-3.2'),
30
+ text: 'Hello from Speechify and the AI SDK.',
31
+ voice: 'harper_32',
32
+ });
33
+
34
+ await writeFile('hello.mp3', audio.uint8Array);
35
+ console.log(providerMetadata.speechify?.speechMarks);
36
+ ```
37
+
38
+ ## Provider instance
39
+
40
+ `speechify` is a default instance that reads `SPEECHIFY_API_KEY` when a request is made.
41
+ Use `createSpeechify` to configure your own:
42
+
43
+ ```ts
44
+ import { createSpeechify } from '@speechify/ai-sdk-provider';
45
+
46
+ const speechify = createSpeechify({
47
+ apiKey: process.env.SPEECHIFY_API_KEY,
48
+ });
49
+ ```
50
+
51
+ | Setting | Default | Notes |
52
+ | --- | --- | --- |
53
+ | `apiKey` | `SPEECHIFY_API_KEY` | Sent as `Authorization: Bearer <key>`. |
54
+ | `baseURL` | `https://api.speechify.ai` | The provider appends `/v1/audio/speech`. |
55
+ | `headers` | none | Added to every request, overriding the provider's own headers. |
56
+ | `fetch` | `globalThis.fetch` | A custom implementation, for example for a proxy or tests. |
57
+
58
+ ## Models and voices
59
+
60
+ | Model | Languages |
61
+ | --- | --- |
62
+ | `simba-3.2` | English. Lowest latency and richest expressivity. Every English voice, cloned voices included. |
63
+ | `simba-3.0` | English, German, Spanish (Spain and Mexico), French, Italian and Brazilian Portuguese. |
64
+
65
+ A voice works with a model when the model is in that voice's `models` list in [`GET /v1/voices`](https://docs.speechify.ai/build/api-reference/v1/voices/get).
66
+ `harper_32` is the default and works with both models.
67
+ A voice the model cannot serve fails with a `400`, and an unknown voice with a `404`.
68
+
69
+ ## Options
70
+
71
+ | `generateSpeech()` option | Sent to Speechify as | Behaviour |
72
+ | --- | --- | --- |
73
+ | `text` | `input` | Plain text or SSML, up to 2,000 characters per request, markup included. |
74
+ | `voice` | `voice_id` | Defaults to `harper_32`. |
75
+ | `outputFormat` | `audio_format` or `output_format` | Defaults to `mp3`. `mp3`, `wav`, `ogg`, `aac` and `pcm` go to `audio_format`. Codec strings (`mp3_24000_128`, `pcm_16000`, `ulaw_8000`, `wav_48000` and the rest of the [API's list](https://docs.speechify.ai/build/api-reference/v1/audio/speech)) go to `output_format`. `mulaw` and `ulaw` mean `ulaw_8000`. Anything else falls back to `mp3` with a warning. |
76
+ | `speed` | SSML `<prosody rate>` | A multiplier from 0.5 to 100: `1.25` sends `rate="+25%"`. Plain text is escaped and wrapped in `<speak>`, and SSML input gets the prosody element inside its own `<speak>`. Billing and speech-mark offsets are unchanged. |
77
+ | `language` | `language` | An ISO 639-1 code (`de`) or a locale (`de-DE`). `auto` sends nothing, which is the API's documented auto-detect mode. |
78
+ | `instructions` | not sent | Speechify has no free-text instructions field, so this always produces a warning. |
79
+ | `providerOptions.speechify.loudnessNormalization` | `options.loudness_normalization` | Normalizes loudness to -14 LUFS. Adds latency. |
80
+ | `providerOptions.speechify.textNormalization` | `options.text_normalization` | Reads numbers and dates as words. On by default in the API. |
81
+ | `headers` | request headers | Per-call headers, applied last. |
82
+ | `abortSignal` | | Cancels the request. |
83
+
84
+ ```ts
85
+ const { audio } = await generateSpeech({
86
+ model: speechify.speech('simba-3.0'),
87
+ text: 'Guten Tag, das ist ein kurzer Test.',
88
+ voice: 'anton',
89
+ language: 'de',
90
+ outputFormat: 'mp3_24000_128',
91
+ speed: 1.1,
92
+ providerOptions: { speechify: { loudnessNormalization: true } },
93
+ });
94
+ ```
95
+
96
+ `speed` adds up to 50 characters of markup to `input`, and the 2,000-character limit counts markup.
97
+ On plain text it also escapes `&`, `<` and `>`, so each one counts as its entity (`&amp;` is five characters).
98
+
99
+ ## Warnings
100
+
101
+ An option that Speechify cannot honour is never dropped silently.
102
+ It comes back on `result.warnings`, and the AI SDK logs it unless you turn warning logging off.
103
+
104
+ | Warning `feature` | When | What the provider does |
105
+ | --- | --- | --- |
106
+ | `instructions` | `instructions` is set | Does not send it. Shape delivery with SSML in `text` instead: breaks and prosody rate are applied by the Simba 3 models. |
107
+ | `outputFormat` | The format is not one Speechify serves (for example `flac` or `opus`) | Requests `mp3`. |
108
+ | `speed` | `speed` is below 0.5 or above 100 | Clamps it to the nearest bound. |
109
+ | `speed` | `speed` is zero, negative or not a number | Ignores it. |
110
+ | `providerOptions.speechify.<key>` | An unknown key under `providerOptions.speechify` | Does not send it. |
111
+
112
+ Errors are thrown rather than warned about:
113
+
114
+ - A missing API key throws `LoadAPIKeyError` before any request is made.
115
+ - A `providerOptions.speechify` value of the wrong type throws `InvalidArgumentError`.
116
+ - A non-2xx response throws `APICallError` with the API's error code, message and request id. `408`, `409`, `429`, `5xx` and network failures are marked retryable, so `generateSpeech()` retries them up to `maxRetries` times.
117
+
118
+ ## Speech marks and metadata
119
+
120
+ Every result carries Speechify's speech marks and billing count under `providerMetadata.speechify`:
121
+
122
+ | Field | Value |
123
+ | --- | --- |
124
+ | `speechMarks` | The [speech marks](https://docs.speechify.ai/build/guides/text-to-speech/speech-marks) for the clip: one sentence-level object with a `chunks` array of words. Times are in milliseconds. `start` and `end` index characters of your text. |
125
+ | `billableCharactersCount` | Characters billed for the request. AI SDK telemetry also receives it as `usage.billable_characters_count`. |
126
+ | `audioFormat` | The codec the API returned: `mp3`, `wav`, `ogg`, `aac`, `pcm` or `ulaw`. |
127
+ | `outputFormat` | The full codec string, when you asked for one (for example `pcm_16000`). |
128
+ | `requestId` | The `Speechify-Request-Id`, to quote to support. |
129
+
130
+ ```ts
131
+ import type { SpeechifySpeechProviderMetadata } from '@speechify/ai-sdk-provider';
132
+
133
+ const { speechMarks } = result.providerMetadata.speechify as SpeechifySpeechProviderMetadata;
134
+ for (const word of speechMarks?.chunks ?? []) {
135
+ console.log(word.value, word.start_time, word.end_time);
136
+ }
137
+ ```
138
+
139
+ ## Headless formats and `audio.mediaType`
140
+
141
+ The AI SDK sets `audio.mediaType` by sniffing the audio bytes, which works for `mp3`, `wav`, `ogg` and `aac`.
142
+ Raw PCM and u-law have no header, so the SDK falls back to the `outputFormat` you passed.
143
+ `pcm` gives `audio/pcm` and `mulaw` gives `audio/mulaw`, but a codec string such as `pcm_16000` gives the SDK's default, `audio/mp3`.
144
+ For headerless formats, read the encoding and sample rate from `providerMetadata.speechify.outputFormat` instead of `audio.mediaType`.
145
+ `pcm` without a rate is 16-bit little-endian mono at 24 kHz, and `mulaw` is `ulaw_8000`.
146
+
147
+ ## API version
148
+
149
+ Every request sends `Speechify-Version: 2026-09-30`, so the provider behaves the same whatever your workspace's default API version is.
150
+ At this version the legacy `simba-english` and `simba-multilingual` models return `400 model_retired`, and `pcm_16000` is true 16 kHz audio.
151
+ To pin a different version, set the header yourself:
152
+
153
+ ```ts
154
+ const speechify = createSpeechify({ headers: { 'Speechify-Version': '2026-09-13' } });
155
+ ```
156
+
157
+ A new release of this package moves the pin forward only with a note on what changed.
158
+ The pinned value is exported as `SPEECHIFY_API_VERSION`.
159
+
160
+ ## What the provider does not do
161
+
162
+ - **Streaming.** The AI SDK's speech interface returns a whole clip. For audio you play while it is generated, call [`POST /v1/audio/stream`](https://docs.speechify.ai/build/guides/text-to-speech/streaming) directly.
163
+ - **Texts over 2,000 characters.** Split them, or use the streaming endpoint, which accepts 20,000.
164
+ - **Browser keys.** Call it from server code. An API key shipped to a browser is readable by anyone who opens the page.
165
+
166
+ ## Attribution headers
167
+
168
+ The provider sends `Speechify-Caller: vercel-ai-sdk` and `Speechify-Caller-Version` with this package's version, so the requests show up as this integration in your workspace's request log.
169
+ The headers never affect access, rate limits or price.
170
+
171
+ ## Links
172
+
173
+ - [Using Speechify with the AI SDK](https://docs.speechify.ai/build/guides/integrations/vercel-ai-sdk?utm_source=npm&utm_medium=readme&utm_campaign=vercel-ai-sdk)
174
+ - [AI SDK speech documentation](https://ai-sdk.dev/docs/ai-sdk-core/speech)
175
+ - [Speechify API reference](https://docs.speechify.ai/build/api-reference/v1/audio/speech)
176
+ - [Issues](https://github.com/Speechify-AI/ai-sdk-provider/issues)
177
+
178
+ ## License
179
+
180
+ MIT
package/dist/index.cjs ADDED
@@ -0,0 +1,336 @@
1
+ Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
2
+ let _ai_sdk_provider = require("@ai-sdk/provider");
3
+ //#region src/speechify-speech-options.ts
4
+ /** Values of the API's `audio_format` field. */
5
+ const SPEECHIFY_AUDIO_FORMATS = [
6
+ "mp3",
7
+ "wav",
8
+ "ogg",
9
+ "aac",
10
+ "pcm"
11
+ ];
12
+ /** Values of the API's `output_format` field (`codec_sampleRate_bitrate`). */
13
+ const SPEECHIFY_OUTPUT_FORMATS = [
14
+ "pcm_8000",
15
+ "pcm_16000",
16
+ "pcm_22050",
17
+ "pcm_24000",
18
+ "pcm_44100",
19
+ "pcm_48000",
20
+ "mp3_22050_32",
21
+ "mp3_22050_64",
22
+ "mp3_22050_96",
23
+ "mp3_22050_128",
24
+ "mp3_22050_160",
25
+ "mp3_22050_192",
26
+ "mp3_24000_32",
27
+ "mp3_24000_64",
28
+ "mp3_24000_96",
29
+ "mp3_24000_128",
30
+ "mp3_24000_160",
31
+ "mp3_24000_192",
32
+ "wav_24000",
33
+ "wav_48000",
34
+ "ulaw_8000",
35
+ "ogg_24000",
36
+ "aac_24000"
37
+ ];
38
+ //#endregion
39
+ //#region src/ssml.ts
40
+ /** `<prosody rate>` accepts -50% to +9900%, so speed is bounded to 0.5x..100x. */
41
+ const MIN_SPEED = .5;
42
+ const SPEAK_OPEN_AT_START = /^<speak(\s[^>]*)?>/;
43
+ const SPEAK_CLOSE = "</speak>";
44
+ function isSSML(text) {
45
+ const trimmed = text.trim();
46
+ return SPEAK_OPEN_AT_START.test(trimmed) && trimmed.endsWith(SPEAK_CLOSE);
47
+ }
48
+ /**
49
+ * Applies a speed multiplier as an SSML `<prosody rate>` around the input.
50
+ * Plain text is escaped and wrapped; SSML input gets the prosody element
51
+ * inside its own `<speak>` root. `speed` must already be within bounds.
52
+ */
53
+ function withSpeechRate(text, speed) {
54
+ const percent = Math.round((speed - 1) * 1e4) / 100;
55
+ if (percent === 0) return text;
56
+ const rate = `${percent > 0 ? "+" : ""}${percent}%`;
57
+ if (isSSML(text)) {
58
+ const trimmed = text.trim();
59
+ const open = trimmed.match(SPEAK_OPEN_AT_START)[0];
60
+ return `${open}<prosody rate="${rate}">${trimmed.slice(open.length, trimmed.length - 8)}</prosody>${SPEAK_CLOSE}`;
61
+ }
62
+ return `<speak><prosody rate="${rate}">${escapeXmlText(text)}</prosody>${SPEAK_CLOSE}`;
63
+ }
64
+ function escapeXmlText(text) {
65
+ let out = "";
66
+ for (const char of text) {
67
+ const code = char.codePointAt(0);
68
+ if (char === "&") out += "&amp;";
69
+ else if (char === "<") out += "&lt;";
70
+ else if (char === ">") out += "&gt;";
71
+ else if (isXmlChar(code)) out += char;
72
+ else out += " ";
73
+ }
74
+ return out;
75
+ }
76
+ function isXmlChar(code) {
77
+ return code === 9 || code === 10 || code === 13 || code >= 32 && code <= 55295 || code >= 57344 && code <= 65533 || code >= 65536 && code <= 1114111;
78
+ }
79
+ //#endregion
80
+ //#region src/speechify-speech-model.ts
81
+ const PROVIDER_OPTION_KEYS = {
82
+ loudnessNormalization: "loudness_normalization",
83
+ textNormalization: "text_normalization"
84
+ };
85
+ var SpeechifySpeechModel = class {
86
+ modelId;
87
+ config;
88
+ specificationVersion = "v4";
89
+ constructor(modelId, config) {
90
+ this.modelId = modelId;
91
+ this.config = config;
92
+ }
93
+ get provider() {
94
+ return this.config.provider;
95
+ }
96
+ async doGenerate(options) {
97
+ const timestamp = /* @__PURE__ */ new Date();
98
+ const { body, warnings } = this.getArgs(options);
99
+ const url = `${this.config.baseURL}/v1/audio/speech`;
100
+ const headers = this.buildHeaders(options.headers);
101
+ const fetchFn = this.config.fetch ?? globalThis.fetch;
102
+ let response;
103
+ try {
104
+ response = await fetchFn(url, {
105
+ method: "POST",
106
+ headers,
107
+ body: JSON.stringify(body),
108
+ signal: options.abortSignal
109
+ });
110
+ } catch (error) {
111
+ if (isAbortError(error)) throw error;
112
+ throw new _ai_sdk_provider.APICallError({
113
+ message: `Cannot connect to the Speechify API: ${errorMessage(error)}`,
114
+ url,
115
+ requestBodyValues: body,
116
+ cause: error,
117
+ isRetryable: true
118
+ });
119
+ }
120
+ const responseHeaders = Object.fromEntries(response.headers.entries());
121
+ const responseBody = await response.text();
122
+ if (!response.ok) throw apiCallError({
123
+ url,
124
+ body,
125
+ response,
126
+ responseHeaders,
127
+ responseBody
128
+ });
129
+ let data;
130
+ try {
131
+ data = JSON.parse(responseBody);
132
+ } catch (error) {
133
+ throw new _ai_sdk_provider.InvalidResponseDataError({
134
+ data: responseBody.slice(0, 500),
135
+ message: `The Speechify API returned a body that is not JSON: ${errorMessage(error)}`
136
+ });
137
+ }
138
+ if (typeof data?.audio_data !== "string" || data.audio_data.length === 0) throw new _ai_sdk_provider.InvalidResponseDataError({
139
+ data: {
140
+ ...data,
141
+ audio_data: void 0
142
+ },
143
+ message: "The Speechify API response has no audio_data."
144
+ });
145
+ const metadata = {
146
+ audioFormat: data.audio_format,
147
+ billableCharactersCount: data.billable_characters_count ?? null,
148
+ speechMarks: data.speech_marks ?? null
149
+ };
150
+ if (data.output_format != null) metadata.outputFormat = data.output_format;
151
+ const requestId = response.headers.get("speechify-request-id");
152
+ if (requestId != null) metadata.requestId = requestId;
153
+ return {
154
+ audio: data.audio_data,
155
+ warnings,
156
+ ...data.billable_characters_count != null && { usage: { billable_characters_count: data.billable_characters_count } },
157
+ request: { body },
158
+ response: {
159
+ timestamp,
160
+ modelId: this.modelId,
161
+ headers: responseHeaders
162
+ },
163
+ providerMetadata: { speechify: metadata }
164
+ };
165
+ }
166
+ getArgs({ text, voice, outputFormat, instructions, speed, language, providerOptions }) {
167
+ const warnings = [];
168
+ let input = text;
169
+ if (speed != null) {
170
+ if (!Number.isFinite(speed) || speed <= 0) warnings.push({
171
+ type: "unsupported",
172
+ feature: "speed",
173
+ details: `speed must be a positive number; ${speed} was ignored.`
174
+ });
175
+ else {
176
+ const bounded = Math.min(Math.max(speed, MIN_SPEED), 100);
177
+ if (bounded !== speed) warnings.push({
178
+ type: "unsupported",
179
+ feature: "speed",
180
+ details: `Speechify supports speed from ${MIN_SPEED} to 100; ${speed} was clamped to ${bounded}.`
181
+ });
182
+ input = withSpeechRate(text, bounded);
183
+ }
184
+ }
185
+ const body = {
186
+ input,
187
+ voice_id: voice ?? "harper_32",
188
+ model: this.modelId,
189
+ ...resolveOutputFormat(outputFormat, warnings)
190
+ };
191
+ if (language != null && language !== "" && language.toLowerCase() !== "auto") body.language = language;
192
+ if (instructions != null && instructions !== "") warnings.push({
193
+ type: "unsupported",
194
+ feature: "instructions",
195
+ details: "Speechify has no free-text instructions field. Shape delivery with SSML in the text instead (breaks and prosody rate)."
196
+ });
197
+ const apiOptions = parseProviderOptions(providerOptions?.speechify, warnings);
198
+ if (apiOptions != null) body.options = apiOptions;
199
+ return {
200
+ body,
201
+ warnings
202
+ };
203
+ }
204
+ buildHeaders(callHeaders) {
205
+ const headers = new Headers({
206
+ Authorization: `Bearer ${loadApiKey(this.config.apiKey)}`,
207
+ "Content-Type": "application/json",
208
+ ...this.config.defaultHeaders
209
+ });
210
+ for (const source of [this.config.headers, callHeaders]) for (const [name, value] of Object.entries(source ?? {})) if (value != null) headers.set(name, value);
211
+ return headers;
212
+ }
213
+ };
214
+ function resolveOutputFormat(requested, warnings) {
215
+ if (requested == null || requested.trim() === "") return { audio_format: "mp3" };
216
+ const format = requested.trim().toLowerCase();
217
+ if (format === "mulaw" || format === "ulaw") return { output_format: "ulaw_8000" };
218
+ if (SPEECHIFY_AUDIO_FORMATS.includes(format)) return { audio_format: format };
219
+ if (SPEECHIFY_OUTPUT_FORMATS.includes(format)) return { output_format: format };
220
+ warnings.push({
221
+ type: "unsupported",
222
+ feature: "outputFormat",
223
+ details: `Speechify does not support "${requested}"; returned mp3 instead. Supported: ${[...SPEECHIFY_AUDIO_FORMATS, ...SPEECHIFY_OUTPUT_FORMATS].join(", ")}.`
224
+ });
225
+ return { audio_format: "mp3" };
226
+ }
227
+ function parseProviderOptions(raw, warnings) {
228
+ if (raw == null) return void 0;
229
+ if (typeof raw !== "object" || Array.isArray(raw)) throw new _ai_sdk_provider.InvalidArgumentError({
230
+ argument: "providerOptions.speechify",
231
+ message: "providerOptions.speechify must be an object."
232
+ });
233
+ const options = {};
234
+ for (const [key, value] of Object.entries(raw)) {
235
+ if (value === void 0) continue;
236
+ if (!Object.hasOwn(PROVIDER_OPTION_KEYS, key)) {
237
+ warnings.push({
238
+ type: "unsupported",
239
+ feature: `providerOptions.speechify.${key}`,
240
+ details: `Unknown option, not sent. Supported: ${Object.keys(PROVIDER_OPTION_KEYS).join(", ")}.`
241
+ });
242
+ continue;
243
+ }
244
+ if (typeof value !== "boolean") throw new _ai_sdk_provider.InvalidArgumentError({
245
+ argument: `providerOptions.speechify.${key}`,
246
+ message: `providerOptions.speechify.${key} must be a boolean.`
247
+ });
248
+ options[PROVIDER_OPTION_KEYS[key]] = value;
249
+ }
250
+ return Object.keys(options).length > 0 ? options : void 0;
251
+ }
252
+ function loadApiKey(apiKey) {
253
+ if (apiKey != null && apiKey !== "") return apiKey;
254
+ const fromEnv = typeof process !== "undefined" ? process.env?.SPEECHIFY_API_KEY : void 0;
255
+ if (fromEnv != null && fromEnv !== "") return fromEnv;
256
+ throw new _ai_sdk_provider.LoadAPIKeyError({ message: "Speechify API key is missing. Pass it as the apiKey setting of createSpeechify() or set the SPEECHIFY_API_KEY environment variable." });
257
+ }
258
+ function apiCallError({ url, body, response, responseHeaders, responseBody }) {
259
+ let data;
260
+ try {
261
+ data = JSON.parse(responseBody);
262
+ } catch {
263
+ data = void 0;
264
+ }
265
+ const code = data?.error?.code;
266
+ const detail = data?.error?.message ?? (responseBody.slice(0, 200) || response.statusText);
267
+ const requestId = data?.request_id ?? response.headers.get("speechify-request-id");
268
+ return new _ai_sdk_provider.APICallError({
269
+ message: `Speechify API error ${response.status}${code ? ` ${code}` : ""}: ${detail}` + (requestId ? ` (request id ${requestId})` : ""),
270
+ url,
271
+ requestBodyValues: body,
272
+ statusCode: response.status,
273
+ responseHeaders,
274
+ responseBody,
275
+ data
276
+ });
277
+ }
278
+ function isAbortError(error) {
279
+ return error instanceof Error && (error.name === "AbortError" || error.name === "TimeoutError");
280
+ }
281
+ function errorMessage(error) {
282
+ return error instanceof Error ? error.message : String(error);
283
+ }
284
+ //#endregion
285
+ //#region src/version.ts
286
+ /** This package's version, injected from package.json at build time. */
287
+ const VERSION = "0.1.0";
288
+ //#endregion
289
+ //#region src/speechify-provider.ts
290
+ /**
291
+ * The dated Speechify API version every request pins via `Speechify-Version`,
292
+ * so behaviour never depends on the workspace's default version.
293
+ */
294
+ const SPEECHIFY_API_VERSION = "2026-09-30";
295
+ /** Identifies this integration's traffic to Speechify (`Speechify-Caller`). */
296
+ const SPEECHIFY_CALLER = "vercel-ai-sdk";
297
+ const DEFAULT_BASE_URL = "https://api.speechify.ai";
298
+ /** Creates a Speechify provider instance. */
299
+ function createSpeechify(settings = {}) {
300
+ const baseURL = (settings.baseURL ?? DEFAULT_BASE_URL).replace(/\/+$/, "");
301
+ const createSpeechModel = (modelId) => new SpeechifySpeechModel(modelId, {
302
+ provider: "speechify.speech",
303
+ baseURL,
304
+ apiKey: settings.apiKey,
305
+ defaultHeaders: {
306
+ "Speechify-Caller": SPEECHIFY_CALLER,
307
+ "Speechify-Caller-Version": VERSION,
308
+ "Speechify-Version": SPEECHIFY_API_VERSION
309
+ },
310
+ headers: settings.headers,
311
+ fetch: settings.fetch
312
+ });
313
+ const noSuchModel = (modelType) => (modelId) => {
314
+ throw new _ai_sdk_provider.NoSuchModelError({
315
+ modelId,
316
+ modelType,
317
+ message: `Speechify provides speech models only; use speechify.speech("${modelId}") with generateSpeech().`
318
+ });
319
+ };
320
+ return {
321
+ specificationVersion: "v4",
322
+ speech: createSpeechModel,
323
+ speechModel: createSpeechModel,
324
+ languageModel: noSuchModel("languageModel"),
325
+ embeddingModel: noSuchModel("embeddingModel"),
326
+ imageModel: noSuchModel("imageModel")
327
+ };
328
+ }
329
+ /** Default provider instance. Reads `SPEECHIFY_API_KEY` from the environment. */
330
+ const speechify = createSpeechify();
331
+ //#endregion
332
+ exports.SPEECHIFY_API_VERSION = SPEECHIFY_API_VERSION;
333
+ exports.SPEECHIFY_CALLER = SPEECHIFY_CALLER;
334
+ exports.VERSION = VERSION;
335
+ exports.createSpeechify = createSpeechify;
336
+ exports.speechify = speechify;
@@ -0,0 +1,98 @@
1
+ import { ProviderV4, SpeechModelV4 } from "@ai-sdk/provider";
2
+ //#region src/speechify-speech-options.d.ts
3
+ /**
4
+ * Speech models served by `POST /v1/audio/speech`.
5
+ *
6
+ * - `simba-3.2`: lowest latency and richest expressivity, English only.
7
+ * - `simba-3.0`: English plus de-DE, es-ES, es-MX, fr-FR, it-IT and pt-BR.
8
+ */
9
+ type SpeechifySpeechModelId = 'simba-3.2' | 'simba-3.0' | (string & {});
10
+ /** Values of the API's `audio_format` field. */
11
+ declare const SPEECHIFY_AUDIO_FORMATS: readonly ["mp3", "wav", "ogg", "aac", "pcm"];
12
+ /** Values of the API's `output_format` field (`codec_sampleRate_bitrate`). */
13
+ declare const SPEECHIFY_OUTPUT_FORMATS: readonly ["pcm_8000", "pcm_16000", "pcm_22050", "pcm_24000", "pcm_44100", "pcm_48000", "mp3_22050_32", "mp3_22050_64", "mp3_22050_96", "mp3_22050_128", "mp3_22050_160", "mp3_22050_192", "mp3_24000_32", "mp3_24000_64", "mp3_24000_96", "mp3_24000_128", "mp3_24000_160", "mp3_24000_192", "wav_24000", "wav_48000", "ulaw_8000", "ogg_24000", "aac_24000"];
14
+ type SpeechifyAudioFormat = (typeof SPEECHIFY_AUDIO_FORMATS)[number];
15
+ type SpeechifyCodecOutputFormat = (typeof SPEECHIFY_OUTPUT_FORMATS)[number];
16
+ /** Every value the `outputFormat` option of `generateSpeech()` accepts. */
17
+ type SpeechifyOutputFormat = SpeechifyAudioFormat | SpeechifyCodecOutputFormat | 'mulaw' | 'ulaw';
18
+ /** Options read from `providerOptions.speechify`. */
19
+ interface SpeechifySpeechModelOptions {
20
+ /** Normalize loudness to -14 LUFS. Adds latency. API default: false. */
21
+ loudnessNormalization?: boolean;
22
+ /** Read numbers, dates and the like as words ("55" as "fifty five"). API default: true. */
23
+ textNormalization?: boolean;
24
+ }
25
+ //#endregion
26
+ //#region src/speechify-provider.d.ts
27
+ /**
28
+ * The dated Speechify API version every request pins via `Speechify-Version`,
29
+ * so behaviour never depends on the workspace's default version.
30
+ */
31
+ export declare const SPEECHIFY_API_VERSION = "2026-09-30";
32
+ /** Identifies this integration's traffic to Speechify (`Speechify-Caller`). */
33
+ export declare const SPEECHIFY_CALLER = "vercel-ai-sdk";
34
+ interface SpeechifyProviderSettings {
35
+ /**
36
+ * Speechify API key, sent as a Bearer token. Defaults to the
37
+ * `SPEECHIFY_API_KEY` environment variable, read when a request is made.
38
+ */
39
+ apiKey?: string;
40
+ /** API base URL. Defaults to `https://api.speechify.ai`. */
41
+ baseURL?: string;
42
+ /**
43
+ * Headers added to every request. They override the provider's own
44
+ * headers, so `Speechify-Version` here replaces the pinned version.
45
+ */
46
+ headers?: Record<string, string>;
47
+ /** Custom fetch implementation, for example for proxies or tests. */
48
+ fetch?: typeof globalThis.fetch;
49
+ }
50
+ interface SpeechifyProvider extends ProviderV4 {
51
+ /** Creates a speech model for `generateSpeech()`. */
52
+ speech(modelId: SpeechifySpeechModelId): SpeechModelV4;
53
+ /** Creates a speech model for `generateSpeech()`. */
54
+ speechModel(modelId: SpeechifySpeechModelId): SpeechModelV4;
55
+ }
56
+ /** Creates a Speechify provider instance. */
57
+ export declare function createSpeechify(settings?: SpeechifyProviderSettings): SpeechifyProvider;
58
+ /** Default provider instance. Reads `SPEECHIFY_API_KEY` from the environment. */
59
+ export declare const speechify: SpeechifyProvider;
60
+ //#endregion
61
+ //#region src/speechify-api-types.d.ts
62
+ /** One timed segment of the synthesized speech. */
63
+ type SpeechifySpeechMark = {
64
+ /** Segment kind, for example `word` or `sentence`. */
65
+ type: string;
66
+ /** Start offset in the input text, in characters. */
67
+ start: number;
68
+ /** End offset in the input text, in characters. */
69
+ end: number;
70
+ /** Start time in the audio, in milliseconds. */
71
+ start_time: number;
72
+ /** End time in the audio, in milliseconds. */
73
+ end_time: number;
74
+ /** The text this segment speaks. */
75
+ value: string;
76
+ };
77
+ /** Speech marks for a clip: the whole text, with one chunk per word. */
78
+ type SpeechifySpeechMarks = SpeechifySpeechMark & {
79
+ chunks: SpeechifySpeechMark[];
80
+ };
81
+ /** Shape of `providerMetadata.speechify` on a `generateSpeech()` result. */
82
+ type SpeechifySpeechProviderMetadata = {
83
+ /** Codec of the returned audio: mp3, wav, ogg, aac, pcm or ulaw. */
84
+ audioFormat: string;
85
+ /** Full `codec_sampleRate_bitrate` string, when one was requested. */
86
+ outputFormat?: string;
87
+ /** Characters billed for the request. */
88
+ billableCharactersCount: number | null;
89
+ speechMarks: SpeechifySpeechMarks | null;
90
+ /** `Speechify-Request-Id` of the response. */
91
+ requestId?: string;
92
+ };
93
+ //#endregion
94
+ //#region src/version.d.ts
95
+ /** This package's version, injected from package.json at build time. */
96
+ export declare const VERSION: string;
97
+ //#endregion
98
+ export type { SpeechifyAudioFormat, SpeechifyCodecOutputFormat, SpeechifyOutputFormat, SpeechifyProvider, SpeechifyProviderSettings, SpeechifySpeechMark, SpeechifySpeechMarks, SpeechifySpeechModelId, SpeechifySpeechModelOptions, SpeechifySpeechProviderMetadata };
@@ -0,0 +1,98 @@
1
+ import { ProviderV4, SpeechModelV4 } from "@ai-sdk/provider";
2
+ //#region src/speechify-speech-options.d.ts
3
+ /**
4
+ * Speech models served by `POST /v1/audio/speech`.
5
+ *
6
+ * - `simba-3.2`: lowest latency and richest expressivity, English only.
7
+ * - `simba-3.0`: English plus de-DE, es-ES, es-MX, fr-FR, it-IT and pt-BR.
8
+ */
9
+ type SpeechifySpeechModelId = 'simba-3.2' | 'simba-3.0' | (string & {});
10
+ /** Values of the API's `audio_format` field. */
11
+ declare const SPEECHIFY_AUDIO_FORMATS: readonly ["mp3", "wav", "ogg", "aac", "pcm"];
12
+ /** Values of the API's `output_format` field (`codec_sampleRate_bitrate`). */
13
+ declare const SPEECHIFY_OUTPUT_FORMATS: readonly ["pcm_8000", "pcm_16000", "pcm_22050", "pcm_24000", "pcm_44100", "pcm_48000", "mp3_22050_32", "mp3_22050_64", "mp3_22050_96", "mp3_22050_128", "mp3_22050_160", "mp3_22050_192", "mp3_24000_32", "mp3_24000_64", "mp3_24000_96", "mp3_24000_128", "mp3_24000_160", "mp3_24000_192", "wav_24000", "wav_48000", "ulaw_8000", "ogg_24000", "aac_24000"];
14
+ type SpeechifyAudioFormat = (typeof SPEECHIFY_AUDIO_FORMATS)[number];
15
+ type SpeechifyCodecOutputFormat = (typeof SPEECHIFY_OUTPUT_FORMATS)[number];
16
+ /** Every value the `outputFormat` option of `generateSpeech()` accepts. */
17
+ type SpeechifyOutputFormat = SpeechifyAudioFormat | SpeechifyCodecOutputFormat | 'mulaw' | 'ulaw';
18
+ /** Options read from `providerOptions.speechify`. */
19
+ interface SpeechifySpeechModelOptions {
20
+ /** Normalize loudness to -14 LUFS. Adds latency. API default: false. */
21
+ loudnessNormalization?: boolean;
22
+ /** Read numbers, dates and the like as words ("55" as "fifty five"). API default: true. */
23
+ textNormalization?: boolean;
24
+ }
25
+ //#endregion
26
+ //#region src/speechify-provider.d.ts
27
+ /**
28
+ * The dated Speechify API version every request pins via `Speechify-Version`,
29
+ * so behaviour never depends on the workspace's default version.
30
+ */
31
+ export declare const SPEECHIFY_API_VERSION = "2026-09-30";
32
+ /** Identifies this integration's traffic to Speechify (`Speechify-Caller`). */
33
+ export declare const SPEECHIFY_CALLER = "vercel-ai-sdk";
34
+ interface SpeechifyProviderSettings {
35
+ /**
36
+ * Speechify API key, sent as a Bearer token. Defaults to the
37
+ * `SPEECHIFY_API_KEY` environment variable, read when a request is made.
38
+ */
39
+ apiKey?: string;
40
+ /** API base URL. Defaults to `https://api.speechify.ai`. */
41
+ baseURL?: string;
42
+ /**
43
+ * Headers added to every request. They override the provider's own
44
+ * headers, so `Speechify-Version` here replaces the pinned version.
45
+ */
46
+ headers?: Record<string, string>;
47
+ /** Custom fetch implementation, for example for proxies or tests. */
48
+ fetch?: typeof globalThis.fetch;
49
+ }
50
+ interface SpeechifyProvider extends ProviderV4 {
51
+ /** Creates a speech model for `generateSpeech()`. */
52
+ speech(modelId: SpeechifySpeechModelId): SpeechModelV4;
53
+ /** Creates a speech model for `generateSpeech()`. */
54
+ speechModel(modelId: SpeechifySpeechModelId): SpeechModelV4;
55
+ }
56
+ /** Creates a Speechify provider instance. */
57
+ export declare function createSpeechify(settings?: SpeechifyProviderSettings): SpeechifyProvider;
58
+ /** Default provider instance. Reads `SPEECHIFY_API_KEY` from the environment. */
59
+ export declare const speechify: SpeechifyProvider;
60
+ //#endregion
61
+ //#region src/speechify-api-types.d.ts
62
+ /** One timed segment of the synthesized speech. */
63
+ type SpeechifySpeechMark = {
64
+ /** Segment kind, for example `word` or `sentence`. */
65
+ type: string;
66
+ /** Start offset in the input text, in characters. */
67
+ start: number;
68
+ /** End offset in the input text, in characters. */
69
+ end: number;
70
+ /** Start time in the audio, in milliseconds. */
71
+ start_time: number;
72
+ /** End time in the audio, in milliseconds. */
73
+ end_time: number;
74
+ /** The text this segment speaks. */
75
+ value: string;
76
+ };
77
+ /** Speech marks for a clip: the whole text, with one chunk per word. */
78
+ type SpeechifySpeechMarks = SpeechifySpeechMark & {
79
+ chunks: SpeechifySpeechMark[];
80
+ };
81
+ /** Shape of `providerMetadata.speechify` on a `generateSpeech()` result. */
82
+ type SpeechifySpeechProviderMetadata = {
83
+ /** Codec of the returned audio: mp3, wav, ogg, aac, pcm or ulaw. */
84
+ audioFormat: string;
85
+ /** Full `codec_sampleRate_bitrate` string, when one was requested. */
86
+ outputFormat?: string;
87
+ /** Characters billed for the request. */
88
+ billableCharactersCount: number | null;
89
+ speechMarks: SpeechifySpeechMarks | null;
90
+ /** `Speechify-Request-Id` of the response. */
91
+ requestId?: string;
92
+ };
93
+ //#endregion
94
+ //#region src/version.d.ts
95
+ /** This package's version, injected from package.json at build time. */
96
+ export declare const VERSION: string;
97
+ //#endregion
98
+ export type { SpeechifyAudioFormat, SpeechifyCodecOutputFormat, SpeechifyOutputFormat, SpeechifyProvider, SpeechifyProviderSettings, SpeechifySpeechMark, SpeechifySpeechMarks, SpeechifySpeechModelId, SpeechifySpeechModelOptions, SpeechifySpeechProviderMetadata };
package/dist/index.js ADDED
@@ -0,0 +1,331 @@
1
+ import { APICallError, InvalidArgumentError, InvalidResponseDataError, LoadAPIKeyError, NoSuchModelError } from "@ai-sdk/provider";
2
+ //#region src/speechify-speech-options.ts
3
+ /** Values of the API's `audio_format` field. */
4
+ const SPEECHIFY_AUDIO_FORMATS = [
5
+ "mp3",
6
+ "wav",
7
+ "ogg",
8
+ "aac",
9
+ "pcm"
10
+ ];
11
+ /** Values of the API's `output_format` field (`codec_sampleRate_bitrate`). */
12
+ const SPEECHIFY_OUTPUT_FORMATS = [
13
+ "pcm_8000",
14
+ "pcm_16000",
15
+ "pcm_22050",
16
+ "pcm_24000",
17
+ "pcm_44100",
18
+ "pcm_48000",
19
+ "mp3_22050_32",
20
+ "mp3_22050_64",
21
+ "mp3_22050_96",
22
+ "mp3_22050_128",
23
+ "mp3_22050_160",
24
+ "mp3_22050_192",
25
+ "mp3_24000_32",
26
+ "mp3_24000_64",
27
+ "mp3_24000_96",
28
+ "mp3_24000_128",
29
+ "mp3_24000_160",
30
+ "mp3_24000_192",
31
+ "wav_24000",
32
+ "wav_48000",
33
+ "ulaw_8000",
34
+ "ogg_24000",
35
+ "aac_24000"
36
+ ];
37
+ //#endregion
38
+ //#region src/ssml.ts
39
+ /** `<prosody rate>` accepts -50% to +9900%, so speed is bounded to 0.5x..100x. */
40
+ const MIN_SPEED = .5;
41
+ const SPEAK_OPEN_AT_START = /^<speak(\s[^>]*)?>/;
42
+ const SPEAK_CLOSE = "</speak>";
43
+ function isSSML(text) {
44
+ const trimmed = text.trim();
45
+ return SPEAK_OPEN_AT_START.test(trimmed) && trimmed.endsWith(SPEAK_CLOSE);
46
+ }
47
+ /**
48
+ * Applies a speed multiplier as an SSML `<prosody rate>` around the input.
49
+ * Plain text is escaped and wrapped; SSML input gets the prosody element
50
+ * inside its own `<speak>` root. `speed` must already be within bounds.
51
+ */
52
+ function withSpeechRate(text, speed) {
53
+ const percent = Math.round((speed - 1) * 1e4) / 100;
54
+ if (percent === 0) return text;
55
+ const rate = `${percent > 0 ? "+" : ""}${percent}%`;
56
+ if (isSSML(text)) {
57
+ const trimmed = text.trim();
58
+ const open = trimmed.match(SPEAK_OPEN_AT_START)[0];
59
+ return `${open}<prosody rate="${rate}">${trimmed.slice(open.length, trimmed.length - 8)}</prosody>${SPEAK_CLOSE}`;
60
+ }
61
+ return `<speak><prosody rate="${rate}">${escapeXmlText(text)}</prosody>${SPEAK_CLOSE}`;
62
+ }
63
+ function escapeXmlText(text) {
64
+ let out = "";
65
+ for (const char of text) {
66
+ const code = char.codePointAt(0);
67
+ if (char === "&") out += "&amp;";
68
+ else if (char === "<") out += "&lt;";
69
+ else if (char === ">") out += "&gt;";
70
+ else if (isXmlChar(code)) out += char;
71
+ else out += " ";
72
+ }
73
+ return out;
74
+ }
75
+ function isXmlChar(code) {
76
+ return code === 9 || code === 10 || code === 13 || code >= 32 && code <= 55295 || code >= 57344 && code <= 65533 || code >= 65536 && code <= 1114111;
77
+ }
78
+ //#endregion
79
+ //#region src/speechify-speech-model.ts
80
+ const PROVIDER_OPTION_KEYS = {
81
+ loudnessNormalization: "loudness_normalization",
82
+ textNormalization: "text_normalization"
83
+ };
84
+ var SpeechifySpeechModel = class {
85
+ modelId;
86
+ config;
87
+ specificationVersion = "v4";
88
+ constructor(modelId, config) {
89
+ this.modelId = modelId;
90
+ this.config = config;
91
+ }
92
+ get provider() {
93
+ return this.config.provider;
94
+ }
95
+ async doGenerate(options) {
96
+ const timestamp = /* @__PURE__ */ new Date();
97
+ const { body, warnings } = this.getArgs(options);
98
+ const url = `${this.config.baseURL}/v1/audio/speech`;
99
+ const headers = this.buildHeaders(options.headers);
100
+ const fetchFn = this.config.fetch ?? globalThis.fetch;
101
+ let response;
102
+ try {
103
+ response = await fetchFn(url, {
104
+ method: "POST",
105
+ headers,
106
+ body: JSON.stringify(body),
107
+ signal: options.abortSignal
108
+ });
109
+ } catch (error) {
110
+ if (isAbortError(error)) throw error;
111
+ throw new APICallError({
112
+ message: `Cannot connect to the Speechify API: ${errorMessage(error)}`,
113
+ url,
114
+ requestBodyValues: body,
115
+ cause: error,
116
+ isRetryable: true
117
+ });
118
+ }
119
+ const responseHeaders = Object.fromEntries(response.headers.entries());
120
+ const responseBody = await response.text();
121
+ if (!response.ok) throw apiCallError({
122
+ url,
123
+ body,
124
+ response,
125
+ responseHeaders,
126
+ responseBody
127
+ });
128
+ let data;
129
+ try {
130
+ data = JSON.parse(responseBody);
131
+ } catch (error) {
132
+ throw new InvalidResponseDataError({
133
+ data: responseBody.slice(0, 500),
134
+ message: `The Speechify API returned a body that is not JSON: ${errorMessage(error)}`
135
+ });
136
+ }
137
+ if (typeof data?.audio_data !== "string" || data.audio_data.length === 0) throw new InvalidResponseDataError({
138
+ data: {
139
+ ...data,
140
+ audio_data: void 0
141
+ },
142
+ message: "The Speechify API response has no audio_data."
143
+ });
144
+ const metadata = {
145
+ audioFormat: data.audio_format,
146
+ billableCharactersCount: data.billable_characters_count ?? null,
147
+ speechMarks: data.speech_marks ?? null
148
+ };
149
+ if (data.output_format != null) metadata.outputFormat = data.output_format;
150
+ const requestId = response.headers.get("speechify-request-id");
151
+ if (requestId != null) metadata.requestId = requestId;
152
+ return {
153
+ audio: data.audio_data,
154
+ warnings,
155
+ ...data.billable_characters_count != null && { usage: { billable_characters_count: data.billable_characters_count } },
156
+ request: { body },
157
+ response: {
158
+ timestamp,
159
+ modelId: this.modelId,
160
+ headers: responseHeaders
161
+ },
162
+ providerMetadata: { speechify: metadata }
163
+ };
164
+ }
165
+ getArgs({ text, voice, outputFormat, instructions, speed, language, providerOptions }) {
166
+ const warnings = [];
167
+ let input = text;
168
+ if (speed != null) {
169
+ if (!Number.isFinite(speed) || speed <= 0) warnings.push({
170
+ type: "unsupported",
171
+ feature: "speed",
172
+ details: `speed must be a positive number; ${speed} was ignored.`
173
+ });
174
+ else {
175
+ const bounded = Math.min(Math.max(speed, MIN_SPEED), 100);
176
+ if (bounded !== speed) warnings.push({
177
+ type: "unsupported",
178
+ feature: "speed",
179
+ details: `Speechify supports speed from ${MIN_SPEED} to 100; ${speed} was clamped to ${bounded}.`
180
+ });
181
+ input = withSpeechRate(text, bounded);
182
+ }
183
+ }
184
+ const body = {
185
+ input,
186
+ voice_id: voice ?? "harper_32",
187
+ model: this.modelId,
188
+ ...resolveOutputFormat(outputFormat, warnings)
189
+ };
190
+ if (language != null && language !== "" && language.toLowerCase() !== "auto") body.language = language;
191
+ if (instructions != null && instructions !== "") warnings.push({
192
+ type: "unsupported",
193
+ feature: "instructions",
194
+ details: "Speechify has no free-text instructions field. Shape delivery with SSML in the text instead (breaks and prosody rate)."
195
+ });
196
+ const apiOptions = parseProviderOptions(providerOptions?.speechify, warnings);
197
+ if (apiOptions != null) body.options = apiOptions;
198
+ return {
199
+ body,
200
+ warnings
201
+ };
202
+ }
203
+ buildHeaders(callHeaders) {
204
+ const headers = new Headers({
205
+ Authorization: `Bearer ${loadApiKey(this.config.apiKey)}`,
206
+ "Content-Type": "application/json",
207
+ ...this.config.defaultHeaders
208
+ });
209
+ for (const source of [this.config.headers, callHeaders]) for (const [name, value] of Object.entries(source ?? {})) if (value != null) headers.set(name, value);
210
+ return headers;
211
+ }
212
+ };
213
+ function resolveOutputFormat(requested, warnings) {
214
+ if (requested == null || requested.trim() === "") return { audio_format: "mp3" };
215
+ const format = requested.trim().toLowerCase();
216
+ if (format === "mulaw" || format === "ulaw") return { output_format: "ulaw_8000" };
217
+ if (SPEECHIFY_AUDIO_FORMATS.includes(format)) return { audio_format: format };
218
+ if (SPEECHIFY_OUTPUT_FORMATS.includes(format)) return { output_format: format };
219
+ warnings.push({
220
+ type: "unsupported",
221
+ feature: "outputFormat",
222
+ details: `Speechify does not support "${requested}"; returned mp3 instead. Supported: ${[...SPEECHIFY_AUDIO_FORMATS, ...SPEECHIFY_OUTPUT_FORMATS].join(", ")}.`
223
+ });
224
+ return { audio_format: "mp3" };
225
+ }
226
+ function parseProviderOptions(raw, warnings) {
227
+ if (raw == null) return void 0;
228
+ if (typeof raw !== "object" || Array.isArray(raw)) throw new InvalidArgumentError({
229
+ argument: "providerOptions.speechify",
230
+ message: "providerOptions.speechify must be an object."
231
+ });
232
+ const options = {};
233
+ for (const [key, value] of Object.entries(raw)) {
234
+ if (value === void 0) continue;
235
+ if (!Object.hasOwn(PROVIDER_OPTION_KEYS, key)) {
236
+ warnings.push({
237
+ type: "unsupported",
238
+ feature: `providerOptions.speechify.${key}`,
239
+ details: `Unknown option, not sent. Supported: ${Object.keys(PROVIDER_OPTION_KEYS).join(", ")}.`
240
+ });
241
+ continue;
242
+ }
243
+ if (typeof value !== "boolean") throw new InvalidArgumentError({
244
+ argument: `providerOptions.speechify.${key}`,
245
+ message: `providerOptions.speechify.${key} must be a boolean.`
246
+ });
247
+ options[PROVIDER_OPTION_KEYS[key]] = value;
248
+ }
249
+ return Object.keys(options).length > 0 ? options : void 0;
250
+ }
251
+ function loadApiKey(apiKey) {
252
+ if (apiKey != null && apiKey !== "") return apiKey;
253
+ const fromEnv = typeof process !== "undefined" ? process.env?.SPEECHIFY_API_KEY : void 0;
254
+ if (fromEnv != null && fromEnv !== "") return fromEnv;
255
+ throw new LoadAPIKeyError({ message: "Speechify API key is missing. Pass it as the apiKey setting of createSpeechify() or set the SPEECHIFY_API_KEY environment variable." });
256
+ }
257
+ function apiCallError({ url, body, response, responseHeaders, responseBody }) {
258
+ let data;
259
+ try {
260
+ data = JSON.parse(responseBody);
261
+ } catch {
262
+ data = void 0;
263
+ }
264
+ const code = data?.error?.code;
265
+ const detail = data?.error?.message ?? (responseBody.slice(0, 200) || response.statusText);
266
+ const requestId = data?.request_id ?? response.headers.get("speechify-request-id");
267
+ return new APICallError({
268
+ message: `Speechify API error ${response.status}${code ? ` ${code}` : ""}: ${detail}` + (requestId ? ` (request id ${requestId})` : ""),
269
+ url,
270
+ requestBodyValues: body,
271
+ statusCode: response.status,
272
+ responseHeaders,
273
+ responseBody,
274
+ data
275
+ });
276
+ }
277
+ function isAbortError(error) {
278
+ return error instanceof Error && (error.name === "AbortError" || error.name === "TimeoutError");
279
+ }
280
+ function errorMessage(error) {
281
+ return error instanceof Error ? error.message : String(error);
282
+ }
283
+ //#endregion
284
+ //#region src/version.ts
285
+ /** This package's version, injected from package.json at build time. */
286
+ const VERSION = "0.1.0";
287
+ //#endregion
288
+ //#region src/speechify-provider.ts
289
+ /**
290
+ * The dated Speechify API version every request pins via `Speechify-Version`,
291
+ * so behaviour never depends on the workspace's default version.
292
+ */
293
+ const SPEECHIFY_API_VERSION = "2026-09-30";
294
+ /** Identifies this integration's traffic to Speechify (`Speechify-Caller`). */
295
+ const SPEECHIFY_CALLER = "vercel-ai-sdk";
296
+ const DEFAULT_BASE_URL = "https://api.speechify.ai";
297
+ /** Creates a Speechify provider instance. */
298
+ function createSpeechify(settings = {}) {
299
+ const baseURL = (settings.baseURL ?? DEFAULT_BASE_URL).replace(/\/+$/, "");
300
+ const createSpeechModel = (modelId) => new SpeechifySpeechModel(modelId, {
301
+ provider: "speechify.speech",
302
+ baseURL,
303
+ apiKey: settings.apiKey,
304
+ defaultHeaders: {
305
+ "Speechify-Caller": SPEECHIFY_CALLER,
306
+ "Speechify-Caller-Version": VERSION,
307
+ "Speechify-Version": SPEECHIFY_API_VERSION
308
+ },
309
+ headers: settings.headers,
310
+ fetch: settings.fetch
311
+ });
312
+ const noSuchModel = (modelType) => (modelId) => {
313
+ throw new NoSuchModelError({
314
+ modelId,
315
+ modelType,
316
+ message: `Speechify provides speech models only; use speechify.speech("${modelId}") with generateSpeech().`
317
+ });
318
+ };
319
+ return {
320
+ specificationVersion: "v4",
321
+ speech: createSpeechModel,
322
+ speechModel: createSpeechModel,
323
+ languageModel: noSuchModel("languageModel"),
324
+ embeddingModel: noSuchModel("embeddingModel"),
325
+ imageModel: noSuchModel("imageModel")
326
+ };
327
+ }
328
+ /** Default provider instance. Reads `SPEECHIFY_API_KEY` from the environment. */
329
+ const speechify = createSpeechify();
330
+ //#endregion
331
+ export { SPEECHIFY_API_VERSION, SPEECHIFY_CALLER, VERSION, createSpeechify, speechify };
package/package.json CHANGED
@@ -1,6 +1,80 @@
1
1
  {
2
2
  "name": "@speechify/ai-sdk-provider",
3
- "version": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
6
- }
3
+ "version": "0.1.0",
4
+ "description": "SpeechifyAI text to speech for the Vercel AI SDK: generateSpeech() with Simba 3 models, speech marks included.",
5
+ "license": "MIT",
6
+ "author": "Speechify <developers@speechify.ai> (https://speechify.ai)",
7
+ "homepage": "https://docs.speechify.ai/build/guides/integrations/vercel-ai-sdk",
8
+ "repository": {
9
+ "type": "git",
10
+ "url": "git+https://github.com/Speechify-AI/ai-sdk-provider.git"
11
+ },
12
+ "bugs": {
13
+ "url": "https://github.com/Speechify-AI/ai-sdk-provider/issues"
14
+ },
15
+ "keywords": [
16
+ "ai",
17
+ "ai-sdk",
18
+ "vercel",
19
+ "speechify",
20
+ "text-to-speech",
21
+ "tts",
22
+ "speech",
23
+ "generateSpeech"
24
+ ],
25
+ "type": "module",
26
+ "sideEffects": false,
27
+ "main": "./dist/index.cjs",
28
+ "module": "./dist/index.js",
29
+ "types": "./dist/index.d.cts",
30
+ "exports": {
31
+ ".": {
32
+ "import": {
33
+ "types": "./dist/index.d.ts",
34
+ "default": "./dist/index.js"
35
+ },
36
+ "require": {
37
+ "types": "./dist/index.d.cts",
38
+ "default": "./dist/index.cjs"
39
+ }
40
+ },
41
+ "./package.json": "./package.json"
42
+ },
43
+ "files": [
44
+ "dist"
45
+ ],
46
+ "engines": {
47
+ "node": ">=22"
48
+ },
49
+ "scripts": {
50
+ "build": "tsdown",
51
+ "typecheck": "tsc --noEmit",
52
+ "test": "vitest run --maxWorkers=2 --dir src",
53
+ "test:live": "vitest run --maxWorkers=2 --dir test",
54
+ "check:dist": "node scripts/check-dist.mjs",
55
+ "prepublishOnly": "npm run typecheck && npm test && npm run build && npm run check:dist"
56
+ },
57
+ "peerDependencies": {
58
+ "@ai-sdk/provider": "^4.0.0",
59
+ "ai": "^7.0.0"
60
+ },
61
+ "peerDependenciesMeta": {
62
+ "ai": {
63
+ "optional": true
64
+ }
65
+ },
66
+ "devDependencies": {
67
+ "@ai-sdk/provider": "4.0.21",
68
+ "@arethetypeswrong/core": "0.18.5",
69
+ "@types/node": "22.20.4",
70
+ "ai": "7.0.126",
71
+ "publint": "0.3.24",
72
+ "tsdown": "0.23.0",
73
+ "typescript": "5.9.3",
74
+ "vitest": "5.0.3"
75
+ },
76
+ "publishConfig": {
77
+ "access": "public",
78
+ "provenance": true
79
+ }
80
+ }