@stacksjs/ai 0.75.38 → 0.75.41

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/dist/index.d.ts CHANGED
@@ -17,6 +17,8 @@ export * from './context';
17
17
  export * from './text';
18
18
  // Image generation & vision
19
19
  export * from './image';
20
+ // Text-to-speech & speech-to-text (stacksjs/stacks#306)
21
+ export * from './speech';
20
22
  // Semantic search, embeddings & RAG
21
23
  export * from './search';
22
24
  // Personalization, sentiment & classification
package/dist/index.js CHANGED
@@ -1 +1 @@
1
- export*from"./types";export*from"./drivers";export*from"./client";export*from"./agents";export*from"./buddy";export*from"./context";export*from"./text";export*from"./image";export*from"./search";export*from"./personalization";export*from"./mcp";export*from"./utils/client-bedrock";export*from"./utils/client-bedrock-runtime";export{buildMessageWithImages,normalizeMessagesForProvider}from"./utils/vision";export{fetchWithRetry}from"./utils/retry";export{clearUsageReporters,listUsageReporters,onUsage,recordUsage}from"./utils/usage";export{estimateMessageTokens,estimateTokens,sanitizePrompt}from"./utils/tokens";export*from"./utils/model-access";
1
+ export*from"./types";export*from"./drivers";export*from"./client";export*from"./agents";export*from"./buddy";export*from"./context";export*from"./text";export*from"./image";export*from"./speech";export*from"./search";export*from"./personalization";export*from"./mcp";export*from"./utils/client-bedrock";export*from"./utils/client-bedrock-runtime";export{buildMessageWithImages,normalizeMessagesForProvider}from"./utils/vision";export{fetchWithRetry}from"./utils/retry";export{clearUsageReporters,listUsageReporters,onUsage,recordUsage}from"./utils/usage";export{estimateMessageTokens,estimateTokens,sanitizePrompt}from"./utils/tokens";export*from"./utils/model-access";
@@ -0,0 +1,87 @@
1
+ import type { AIProvider } from './types';
2
+ /** The MIME type for a speech format. */
3
+ export declare function speechMimeType(format: SpeechFormat): string;
4
+ /**
5
+ * Synthesize speech from text.
6
+ *
7
+ * @example
8
+ * ```ts
9
+ * const speech = await textToSpeech('Your order has shipped.', { voice: 'nova' })
10
+ * return new Response(speech.audio, { headers: { 'Content-Type': speech.mimeType } })
11
+ * ```
12
+ */
13
+ export declare function textToSpeech(text: string, options?: TextToSpeechOptions): Promise<SpeechResult>;
14
+ /**
15
+ * Write synthesized speech to a Stacks storage disk.
16
+ *
17
+ * @example
18
+ * ```ts
19
+ * const speech = await textToSpeech('Welcome back!')
20
+ * await storeSpeech(speech, 'audio/welcome.mp3', { disk: 'public' })
21
+ * ```
22
+ */
23
+ export declare function storeSpeech(speech: Pick<SpeechResult, 'audio' | 'mimeType'>, path: string, options?: StoreSpeechOptions): Promise<StoredSpeech>;
24
+ /**
25
+ * Transcribe audio to text.
26
+ *
27
+ * @example
28
+ * ```ts
29
+ * const { text } = await speechToText(Bun.file('memo.m4a'), { language: 'en' })
30
+ * ```
31
+ */
32
+ export declare function speechToText(audio: SpeechInput, options?: SpeechToTextOptions): Promise<TranscriptionResult>;
33
+ /** Drivers that implement text-to-speech / speech-to-text. */
34
+ export declare const SPEECH_DRIVERS: readonly AIProvider[];
35
+ declare interface SpeechRequestOptions {
36
+ driver?: SpeechDriver
37
+ config?: ConfiguredAIOptions
38
+ retry?: RetryConfig
39
+ signal?: AbortSignal
40
+ }
41
+ export declare interface TextToSpeechOptions extends SpeechRequestOptions {
42
+ model?: string
43
+ voice?: OpenAISpeechVoice | (string & {})
44
+ format?: SpeechFormat
45
+ speed?: number
46
+ instructions?: string
47
+ }
48
+ export declare interface SpeechResult {
49
+ audio: Uint8Array
50
+ mimeType: string
51
+ format: SpeechFormat
52
+ driver: AIProvider
53
+ model: string
54
+ voice: string
55
+ }
56
+ export declare interface SpeechToTextOptions extends SpeechRequestOptions {
57
+ model?: string
58
+ language?: string
59
+ prompt?: string
60
+ temperature?: number
61
+ filename?: string
62
+ }
63
+ export declare interface TranscriptionResult {
64
+ text: string
65
+ driver: AIProvider
66
+ model: string
67
+ }
68
+ /** Anything with a Stacks `StorageAdapter`-compatible `write()`. */
69
+ export declare interface SpeechStorageDisk {
70
+ write: (path: string, contents: Uint8Array) => Promise<{ path: string, size: number, contentType?: string }>
71
+ }
72
+ export declare interface StoreSpeechOptions {
73
+ disk?: string | SpeechStorageDisk
74
+ }
75
+ export declare interface StoredSpeech {
76
+ path: string
77
+ size: number
78
+ mimeType: string
79
+ }
80
+ /** A driver name: one of the built-in providers, or anything `config/ai.ts` holds. */
81
+ export type SpeechDriver = AIProvider | (string & {});
82
+ /** Audio container/codec the provider should return. */
83
+ export type SpeechFormat = 'mp3' | 'opus' | 'aac' | 'flac' | 'wav' | 'pcm';
84
+ /** OpenAI's built-in voices. Any other string is passed through (custom voice ids). */
85
+ export type OpenAISpeechVoice = | 'alloy' | 'ash' | 'ballad' | 'cedar' | 'coral' | 'echo' | 'fable'
86
+ | 'marin' | 'nova' | 'onyx' | 'sage' | 'shimmer' | 'verse';
87
+ export type SpeechInput = Blob | Uint8Array | ArrayBuffer;
package/dist/speech.js ADDED
@@ -0,0 +1 @@
1
+ import{resolveAIProvider}from"./client";import{fetchWithRetry}from"./utils/retry";export const SPEECH_DRIVERS=["openai"];const OPENAI_BASE_URL="https://api.openai.com/v1",MAX_OPENAI_INPUT=4096;async function loadConfig(config){if(config)return config;try{const{ai}=await import("@stacksjs/config");return ai??{}}catch{return{}}}function unsupported(feature,driver){return Error(`${feature} is not supported by the "${driver}" AI driver. Drivers that support it: ${SPEECH_DRIVERS.join(", ")}. Pass { driver: '${SPEECH_DRIVERS[0]}' } or set \`default\` in config/ai.ts.`)}function resolveSpeechDriver(feature,config,override){const requested=String(override||config.default||"").trim();if(!requested)throw Error(`${feature} needs an AI driver. Pass { driver: '${SPEECH_DRIVERS[0]}' } or set \`default\` in config/ai.ts.`);let provider;try{provider=resolveAIProvider({default:requested})}catch{throw unsupported(feature,requested)}if(!SPEECH_DRIVERS.includes(provider))throw unsupported(feature,provider);return provider}function openAICredentials(config){const driver=config.drivers?.openai??{},apiKey=driver.apiKey?.trim()||process.env.OPENAI_API_KEY?.trim()||"",customBase=driver.baseUrl?.trim();if(!apiKey&&!customBase)throw Error("OpenAI API key not set. Set OPENAI_API_KEY or `drivers.openai.apiKey` in config/ai.ts.");return{apiKey,baseUrl:(customBase||OPENAI_BASE_URL).replace(/\/+$/,"")}}async function failure(provider,operation,response){let body="";try{body=(await response.text()).trim()}catch{}const snippet=body.length>500?`${body.slice(0,500)}...`:body;return Error(`${provider} ${operation} failed with HTTP ${response.status}${snippet?`: ${snippet}`:""}`)}const MIME_TYPES={mp3:"audio/mpeg",opus:"audio/ogg",aac:"audio/aac",flac:"audio/flac",wav:"audio/wav",pcm:"audio/pcm"};export function speechMimeType(format){return MIME_TYPES[format]}export async function textToSpeech(text,options={}){if(typeof text!=="string"||!text.trim())throw Error("textToSpeech() needs non-empty text.");const format=options.format??"mp3";if(!(format in MIME_TYPES))throw Error(`Unsupported speech format "${format}". Use one of: ${Object.keys(MIME_TYPES).join(", ")}.`);if(options.speed!==void 0&&!(options.speed>=0.25&&options.speed<=4))throw Error(`Speech speed must be between 0.25 and 4.0, got ${options.speed}.`);const config=await loadConfig(options.config),driver=resolveSpeechDriver("Text-to-speech",config,options.driver);return openAITextToSpeech(text,format,options,config,driver)}async function openAITextToSpeech(text,format,options,config,driver){if(text.length>MAX_OPENAI_INPUT)throw Error(`OpenAI text-to-speech accepts at most ${MAX_OPENAI_INPUT} characters per request, got ${text.length}. Split the text into chunks.`);const model=options.model??"gpt-4o-mini-tts",voice=options.voice??"alloy";if(options.instructions&&/^tts-1(?:-hd)?$/.test(model))throw Error(`OpenAI model "${model}" does not support \`instructions\`. Use gpt-4o-mini-tts.`);const{apiKey,baseUrl}=openAICredentials(config),body={model,input:text,voice,response_format:format};if(options.speed!==void 0)body.speed=options.speed;if(options.instructions)body.instructions=options.instructions;const headers={"Content-Type":"application/json"};if(apiKey)headers.Authorization=`Bearer ${apiKey}`;const response=await fetchWithRetry(`${baseUrl}/audio/speech`,{method:"POST",headers,body:JSON.stringify(body),signal:options.signal},options.retry);if(!response.ok)throw await failure("OpenAI","text-to-speech",response);const audio=new Uint8Array(await response.arrayBuffer()),contentType=response.headers.get("content-type")?.split(";")[0]?.trim(),mimeType=contentType?.startsWith("audio/")?contentType:MIME_TYPES[format];return{audio,mimeType,format,driver,model,voice}}export async function storeSpeech(speech,path,options={}){if(!path?.trim())throw Error("storeSpeech() needs a destination path.");let disk;if(options.disk&&typeof options.disk==="object")disk=options.disk;else{let storage;try{storage=await import("@stacksjs/storage")}catch(error){throw Error(`storeSpeech() needs @stacksjs/storage to resolve a disk by name (${error instanceof Error?error.message:String(error)}). Install it, or pass a storage adapter as { disk }.`)}disk=storage.Storage.disk(options.disk||void 0)}const result=await disk.write(path,speech.audio);return{path:result.path,size:result.size,mimeType:speech.mimeType}}const EXTENSIONS={"audio/mpeg":"mp3","audio/mp3":"mp3","audio/mp4":"m4a","audio/m4a":"m4a","audio/x-m4a":"m4a","audio/wav":"wav","audio/x-wav":"wav","audio/webm":"webm","audio/ogg":"ogg","audio/flac":"flac"};export async function speechToText(audio,options={}){if(options.temperature!==void 0&&!(options.temperature>=0&&options.temperature<=1))throw Error(`Transcription temperature must be between 0 and 1, got ${options.temperature}.`);const config=await loadConfig(options.config),driver=resolveSpeechDriver("Speech-to-text",config,options.driver),model=options.model??"whisper-1",{apiKey,baseUrl}=openAICredentials(config),blob=audio instanceof Blob?audio:new Blob([audio]);if(blob.size===0)throw Error("speechToText() needs non-empty audio.");const ownName=audio.name,fileName=options.filename||(typeof ownName==="string"&&ownName?ownName.split("/").pop():void 0)||`audio.${EXTENSIONS[blob.type.split(";")[0]??""]??"mp3"}`,form=new FormData;form.append("file",blob,fileName);form.append("model",model);form.append("response_format","json");if(options.language)form.append("language",options.language);if(options.prompt)form.append("prompt",options.prompt);if(options.temperature!==void 0)form.append("temperature",String(options.temperature));const headers={};if(apiKey)headers.Authorization=`Bearer ${apiKey}`;const response=await fetchWithRetry(`${baseUrl}/audio/transcriptions`,{method:"POST",headers,body:form,signal:options.signal},options.retry);if(!response.ok)throw await failure("OpenAI","speech-to-text",response);const data=await response.json();if(typeof data?.text!=="string")throw Error("OpenAI speech-to-text returned no text.");return{text:data.text,driver,model}}
package/package.json CHANGED
@@ -2,7 +2,7 @@
2
2
  "name": "@stacksjs/ai",
3
3
  "type": "module",
4
4
  "sideEffects": false,
5
- "version": "0.75.38",
5
+ "version": "0.75.41",
6
6
  "description": "Stacks Artificial Intelligence.",
7
7
  "author": "Chris Breuer",
8
8
  "contributors": [
@@ -65,8 +65,9 @@
65
65
  "prepublishOnly": "bun run build"
66
66
  },
67
67
  "dependencies": {
68
- "@stacksjs/cli": "0.75.38",
69
- "@stacksjs/config": "0.75.38",
68
+ "@stacksjs/cli": "0.75.41",
69
+ "@stacksjs/config": "0.75.41",
70
+ "@stacksjs/storage": "0.75.41",
70
71
  "@stacksjs/ts-cloud": "^0.16.15"
71
72
  },
72
73
  "peerDependencies": {
@@ -78,7 +79,7 @@
78
79
  }
79
80
  },
80
81
  "devDependencies": {
81
- "@anthropic-ai/claude-agent-sdk": "^0.2.79",
82
+ "@anthropic-ai/claude-agent-sdk": "^0.3.288",
82
83
  "better-dx": "^0.2.24"
83
84
  }
84
85
  }