UNPKG

@tanstack/ai

Version:

Type-safe TypeScript AI SDK for streaming chat, tool calling, agents, structured outputs, and multimodal generation.

231 lines (230 loc) 7.82 kB
import { resolveDebugOption } from "../../logger/resolve.js"; import { streamGenerationResult } from "../stream-generation-result.js"; import { applyGenerationResultTransforms, createGenerationContext, runGenerationAbort, runGenerationError, runGenerationFinish, runGenerationStart, runGenerationUsage } from "../middleware/run.js"; import { abortReasonMessage, createActivityAbortControls, isActivityAbortError, raceWithAbort } from "../../utilities/activity-abort.js"; import "./adapter.js"; import { aiEventClient } from "@tanstack/ai-event-client"; //#region src/activities/generateSpeech/index.ts /** * TTS Activity * * Generates speech audio from text using text-to-speech models. * This is a self-contained module with implementation, types, and JSDoc. */ /** The adapter kind this activity handles */ var kind = "tts"; function createId(prefix) { return `${prefix}-${Date.now()}-${Math.random().toString(36).slice(2, 9)}`; } /** * Validate the text/turns/timestamps trio against what the adapter declares, * and return the `text` every adapter receives. * * For a dialogue request that text is the turn scripts joined by newlines: * dialogue-aware adapters read `turns` and ignore it, but it keeps `text` * non-optional on the adapter contract and gives the devtools event and the * artifact inputs something truthful to show. */ function resolveSpeechText(adapter, input) { const { text, turns, timestamps } = input; if (timestamps && !adapter.capabilities?.timestamps) throw new Error(`${adapter.name} cannot return timestamps. Drop \`timestamps: true\` — the result would have no alignment to read.`); if (turns) { if (text !== void 0) throw new Error("generateSpeech() takes either `text` or `turns`, not both."); if (turns.length === 0) throw new Error("generateSpeech() `turns` must not be empty."); const maxSpeakers = adapter.capabilities?.maxSpeakers; if (maxSpeakers === void 0) throw new Error(`${adapter.name} cannot generate dialogue. Pass \`text\` (and \`voice\`) instead of \`turns\`.`); const speakers = new Set(turns.map((turn) => turn.voice)).size; if (speakers > maxSpeakers) throw new Error(`${adapter.name} accepts at most ${maxSpeakers} distinct voice${maxSpeakers === 1 ? "" : "s"} per request; received ${speakers}.`); return turns.map((turn) => turn.text).join("\n"); } if (text === void 0) throw new Error("generateSpeech() requires either `text` or `turns`."); return text; } /** * TTS activity - generates speech from text. * * Uses AI text-to-speech models to create audio from natural language text. * * @example Generate speech from text * ```ts * import { generateSpeech } from '@tanstack/ai' * import { openaiSpeech } from '@tanstack/ai-openai' * * const result = await generateSpeech({ * adapter: openaiSpeech('tts-1-hd'), * text: 'Hello, welcome to TanStack AI!', * voice: 'nova' * }) * * console.log(result.audio) // base64-encoded audio * ``` * * @example With format and speed options * ```ts * const result = await generateSpeech({ * adapter: openaiSpeech('tts-1'), * text: 'This is slower speech.', * voice: 'alloy', * format: 'wav', * speed: 0.8 * }) * ``` */ function generateSpeech(options) { if (options.stream) return streamGenerationResult((resolved) => runGenerateSpeech({ ...options, runId: resolved.runId }), options); return runGenerateSpeech(options); } /** * Run the core TTS generation logic (non-streaming). */ async function runGenerateSpeech(options) { const { adapter, stream: _stream, debug: _debug, middleware, threadId, runId, timeout, abortSignal: callerAbortSignal, ...rest } = options; const model = adapter.model; const text = resolveSpeechText(adapter, rest); const requestId = createId("speech"); const startTime = Date.now(); const logger = resolveDebugOption(options.debug); const abortControls = createActivityAbortControls({ timeout, abortSignal: callerAbortSignal }); const providerName = adapter.provider ?? adapter.name ?? "unknown"; const mwCtx = createGenerationContext({ requestId, activity: "tts", provider: adapter.name, model, modelOptions: rest.modelOptions, artifactInputs: { text, voice: rest.voice, format: rest.format, speed: rest.speed }, threadId, runId, createId }); await runGenerationStart(middleware, mwCtx); aiEventClient.emit("speech:request:started", { requestId, provider: adapter.name, model, text, voice: rest.voice, format: rest.format, speed: rest.speed, modelOptions: rest.modelOptions, timestamp: startTime }); logger.request(`activity=generateSpeech provider=${providerName}`, { provider: providerName, model }); try { const rawResult = await raceWithAbort(adapter.generateSpeech({ ...rest, text, model, logger, ...abortControls.signal ? { abortSignal: abortControls.signal } : {} }), abortControls.signal); abortControls.clear(); const result = await applyGenerationResultTransforms(mwCtx, rawResult); const duration = Date.now() - startTime; aiEventClient.emit("speech:request:completed", { requestId, provider: adapter.name, model, audio: result.audio, format: result.format, audioDuration: result.duration, contentType: result.contentType, duration, modelOptions: rest.modelOptions, timestamp: Date.now() }); if (result.usage) aiEventClient.emit("speech:usage", { requestId, model, usage: result.usage, modelOptions: rest.modelOptions, timestamp: Date.now() }); logger.output(`activity=generateSpeech bytes=${result.audio.length}`, { bytes: result.audio.length, contentType: result.contentType }); if (result.usage) await runGenerationUsage(middleware, mwCtx, result.usage); await runGenerationFinish(middleware, mwCtx, { duration, usage: result.usage }); return result; } catch (error) { abortControls.clear(); const duration = Date.now() - startTime; const err = error; aiEventClient.emit("speech:request:error", { requestId, provider: adapter.name, model, error: { message: err.message, name: err.name }, duration, modelOptions: rest.modelOptions, timestamp: Date.now() }); if (isActivityAbortError(error, abortControls.signal)) await runGenerationAbort(middleware, mwCtx, { reason: abortReasonMessage(error, abortControls.signal), duration }); else await runGenerationError(middleware, mwCtx, { error, duration }); logger.errors("generateSpeech activity failed", { error, source: "generateSpeech" }); throw error; } } /** * List the voices an account can pass to `generateSpeech()`. * * Only providers with a per-account catalog implement this. A provider whose * voices are a fixed list publishes that list as a const in its package, so * import it from there rather than calling this. * * @example Find the voices you created * ```ts * import { listVoices } from '@tanstack/ai' * import { elevenlabsSpeech } from '@tanstack/ai-elevenlabs' * * const { voices } = await listVoices({ * adapter: elevenlabsSpeech('eleven_v3'), * origins: ['generated', 'cloned'], * }) * ``` */ async function listVoices(options) { const { adapter, ...rest } = options; const list = adapter.listVoices; if (!list) throw new Error(`The ${adapter.name} speech adapter has no per-account voice catalog to list. Its voices are a fixed set — import the voice list or union its package exports instead (for example \`GeminiTTSVoices\` from @tanstack/ai-gemini, or the \`OpenAITTSVoice\` union from @tanstack/ai-openai).`); return await list.call(adapter, rest); } /** * Create typed options for the generateSpeech() function without executing. */ function createSpeechOptions(options) { return options; } //#endregion export { createSpeechOptions, generateSpeech, kind, listVoices }; //# sourceMappingURL=index.js.map