@happyvertical/speech 0.96.0 → 0.97.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/AGENT.md CHANGED
@@ -27,7 +27,7 @@ pnpm --filter @happyvertical/speech clean
27
27
 
28
28
  ## Ecosystem Relationships
29
29
  - Provides: Speech provider abstraction for STT and TTS backends
30
- - Implements: Studio Server STT, Studio Server TTS, Qwen3 TTS, OpenAI-compatible TTS
30
+ - Implements: Studio Server STT, OpenAI-compatible STT, Studio Server TTS, Qwen3 TTS, OpenAI-compatible TTS
31
31
  - Requires: none
32
32
  - Stability: experimental (Marked as preview or experimental in package guidance.)
33
33
  <!-- END AGENT:GENERATED -->
@@ -46,6 +46,19 @@ Adapter constructors are internal implementation details. Keep new backends behi
46
46
  ## Adapters
47
47
 
48
48
  - Studio Server STT (`type: 'studio-server'`) posts multipart audio to `/v1/transcribe`.
49
+ - OpenAI-compatible STT (`type: 'openai-compatible'`) posts multipart `file`/`model` to `<base>/audio/transcriptions`.
49
50
  - Studio Server TTS (`type: 'studio-server'`) posts multipart form data to `/v1/tts/synthesize`.
50
51
  - Qwen3 TTS (`type: 'qwen3-tts'`) posts multipart form data to `/v1/audio/speech`.
51
52
  - OpenAI-compatible TTS (`type: 'openai-compatible'`) posts OpenAI-shaped JSON to `/v1/audio/speech`.
53
+
54
+ ## Shared Building Blocks
55
+
56
+ New adapters (including streaming/realtime transcribers) should reuse these modules in `src/shared/` instead of re-implementing them:
57
+
58
+ - `usage.ts`: `SpeechUsage`, `SpeechUsageCallback`, `reportSpeechUsage()` (adapter `onUsage` first, then request `onUsage`; callback errors propagate), and `audioSecondsFromProviderUsage()`.
59
+ - `env.ts`: `resolveTranscriberConfig(options, context)` reads `HAVE_SPEECH_TRANSCRIBER_*` (then legacy `HAVE_SPEECH_STT_*`/`STT_*`) with explicit options winning; headers merge env → context → options via `mergeHeaderInits()`.
60
+ - `audio.ts`: `normalizeAudioInput()` accepts `AudioInput` or a bare Blob/Buffer/Uint8Array/ArrayBuffer/ReadableStream, enforces `maxBytes` while buffering, and derives the multipart filename through `mimeTypeToAudioExtension()`.
61
+ - `retry.ts`: `withSpeechRetry()` retries 429/5xx `SpeechProviderError`s with exponential backoff, honours `Retry-After` (`retryAfterMs`), and stops on abort.
62
+ - `http.ts`: `HttpSpeechAdapter.post(..., retry)` sends auth/extra headers (`headers` option plus per-request `headers`), applies `timeoutMs` per attempt, and redacts the API key from provider error bodies; `resolveOpenAICompatibleUrl()` normalises OpenAI-style base URLs.
63
+
64
+ The OpenAI-compatible transcriber holds an API key and is server-side only.
package/README.md CHANGED
@@ -46,14 +46,71 @@ const spoken = await speech.synthesize({
46
46
  | Provider | Type | Method | Path | Encoding |
47
47
  | --- | --- | --- | --- | --- |
48
48
  | Studio Server STT | `studio-server` | `POST` | `/v1/transcribe` | Multipart (`audio`) |
49
+ | OpenAI-compatible STT | `openai-compatible` | `POST` | `<base>/audio/transcriptions` | Multipart (`file`) |
49
50
  | Studio Server TTS | `studio-server` | `POST` | `/v1/tts/synthesize` | Multipart |
50
51
  | Qwen3 TTS | `qwen3-tts` | `POST` | `/v1/audio/speech` | Multipart |
51
52
  | OpenAI-compatible TTS | `openai-compatible` | `POST` | `/v1/audio/speech` | JSON |
52
53
 
53
54
  Studio Server and Qwen3 accept pre-extracted provider voice prompts through `SpeechVoice.prompt`; the adapters forward these as the multipart `voice_prompt` field.
54
55
 
56
+ ## OpenAI-compatible Transcription
57
+
58
+ `type: 'openai-compatible'` speaks the OpenAI audio API, so one adapter covers OpenAI, Groq, Fireworks, LiteLLM and Bifrost gateways, vLLM, speaches/faster-whisper-server, and whisper.cpp server.
59
+
60
+ > **Server-side only.** The adapter holds the provider API key. Never construct it in a browser. Browsers should record audio (for example with `MediaRecorder`) and post it to your own server route, which calls the transcriber.
61
+
62
+ ```typescript
63
+ import { getTranscriber } from '@happyvertical/speech';
64
+
65
+ const transcriber = await getTranscriber({
66
+ type: 'openai-compatible',
67
+ baseUrl: 'http://bifrost:8080/openai/v1', // or https://api.openai.com
68
+ apiKey: process.env.OPENAI_API_KEY,
69
+ model: 'whisper-1',
70
+ headers: { 'x-bf-vk': virtualKey }, // gateway routing / org / project headers
71
+ onUsage: (usage) => recordUsage(tenantId, usage),
72
+ });
73
+
74
+ // Inside a server route that received the browser upload:
75
+ const result = await transcriber.transcribe({
76
+ audio: request.body, // Blob | Buffer | Uint8Array | ArrayBuffer | ReadableStream
77
+ mimeType: 'audio/webm;codecs=opus', // Safari sends audio/mp4
78
+ language: 'en',
79
+ timestampGranularities: ['word', 'segment'],
80
+ headers: { 'x-bf-vk': tenantVirtualKey }, // per-request headers win
81
+ signal: abortController.signal,
82
+ });
83
+
84
+ result.text;
85
+ result.segments; // present when the server returns verbose_json
86
+ result.words; // present when word timestamps were requested and supported
87
+ result.usage; // { operation, provider, model, audioSeconds, bytes, providerUsage }
88
+ ```
89
+
90
+ - **Base URL.** A server root gets `/v1/audio/transcriptions`; a base ending in a version segment (`http://gateway/stt/v1`) gets `/audio/transcriptions`; a URL already ending in `/audio/transcriptions` is used as-is. Any query string (for example `?api-version=…`) is preserved and a fragment is dropped.
91
+ - **Audio formats.** The multipart filename extension comes from the MIME type (codec parameters are ignored): `webm`, `m4a` (`audio/mp4`), `mp4` (`video/mp4`), `wav`, `ogg`, `mp3`, `flac`. Pass `audio: { data, filename }` to choose the name yourself.
92
+ - **Limits.** `maxBytes` (adapter or request) defaults to 25 MB, the OpenAI upload cap. Oversized or empty input fails with `SpeechConfigurationError` before any request; streams are cancelled as soon as they exceed the limit.
93
+ - **Response formats.** Defaults to `verbose_json` so segments, duration, and (with `timestampGranularities`) word timings map onto `TranscriptResult`. Models that only return `json`/`text` (`gpt-4o-transcribe`, `gpt-4o-mini-transcribe`, including gateway-prefixed names) are sent `json` without timestamps. If any other server rejects `verbose_json` or `timestamp_granularities[]` with a 400/422, the adapter retries once with plain `json`.
94
+ - **Retries.** 429 and 5xx responses are retried with exponential backoff (default 2 retries, 500 ms initial delay, 30 s cap), waiting at least `Retry-After`. A `Retry-After` longer than `maxDelayMs` ends retrying. Configure with `retry: { maxRetries, initialDelayMs, maxDelayMs }` or disable with `retry: false`; non-finite values (`NaN`, `Infinity`) throw `SpeechConfigurationError`. `timeoutMs` applies per attempt; `signal` cancels the request and any backoff wait.
95
+ - **Usage.** `result.usage` and `onUsage` (adapter-level, then request-level) report the model, audio seconds (provider duration, then `usage.seconds`, then `AudioInput.durationSeconds`), uploaded bytes, and the provider `usage` block untouched. Errors thrown by `onUsage` propagate.
96
+ - **Errors.** HTTP failures throw `SpeechProviderError` with `status`, `responseBody`, and `retryAfterMs`. The API key never appears in error messages and is redacted from response bodies.
97
+
55
98
  ## Environment Configuration
56
99
 
100
+ Transcriber settings switch between cloud and self-hosted models without code changes. Explicit options win over the environment:
101
+
102
+ ```bash
103
+ HAVE_SPEECH_TRANSCRIBER_TYPE=openai-compatible
104
+ HAVE_SPEECH_TRANSCRIBER_BASE_URL=http://bifrost:8080/openai/v1
105
+ HAVE_SPEECH_TRANSCRIBER_MODEL=whisper-1
106
+ HAVE_SPEECH_TRANSCRIBER_API_KEY=sk-...
107
+ HAVE_SPEECH_TRANSCRIBER_TIMEOUT=60000 # milliseconds, per attempt
108
+ HAVE_SPEECH_TRANSCRIBER_MAX_BYTES=26214400 # optional
109
+ HAVE_SPEECH_TRANSCRIBER_HEADERS='{"x-bf-vk":"vk-..."}' # optional JSON object
110
+ ```
111
+
112
+ Headers merge in order environment, factory context, then explicit options (later wins per header). The `HAVE_SPEECH_STT_*` names below remain supported as fallbacks.
113
+
57
114
  SDK-style names are preferred:
58
115
 
59
116
  ```bash
@@ -74,11 +131,11 @@ TTS_ADAPTER=qwen3-tts
74
131
  TTS_BASE_URL=http://qwen3-tts.qwen3-tts.svc.cluster.local
75
132
  ```
76
133
 
77
- Optional overrides include `STT_PATH`, `TTS_PATH`, `STT_API_KEY`, `TTS_API_KEY`, `SPEECH_API_KEY`, `STT_TIMEOUT_MS`, and `TTS_TIMEOUT_MS`.
134
+ Optional overrides include `HAVE_SPEECH_STT_MODEL`, `STT_MODEL`, `STT_PATH`, `TTS_PATH`, `STT_API_KEY`, `TTS_API_KEY`, `SPEECH_API_KEY`, `STT_TIMEOUT_MS`, and `TTS_TIMEOUT_MS`.
78
135
 
79
136
  ## Testing
80
137
 
81
- Default tests use tiny in-process HTTP fixture services that mirror the Studio Server, Qwen3, and OpenAI-compatible request shapes. They validate field names, encodings, and response normalization without downloading production-scale models.
138
+ Default tests use tiny in-process HTTP fixture services or an injected `fetch` that mirror the Studio Server, Qwen3, and OpenAI-compatible request shapes. They validate field names, encodings, and response normalization without downloading production-scale models.
82
139
 
83
140
  Docker or Testcontainers integration suites should run the fixture services or Studio Server with mock backends. Model-backed tests must remain opt-in, for example behind `HV_SPEECH_MODEL_TESTS=1`, so the normal SDK suite never downloads model weights.
84
141
 
@@ -0,0 +1,23 @@
1
+ import { HttpSpeechAdapter } from '../shared/http.js';
2
+ import { OpenAICompatibleTranscriberOptions, Transcriber, TranscriptionRequest, TranscriptResult } from '../shared/types.js';
3
+ export declare function isJsonOnlyTranscriptionModel(model: string): boolean;
4
+ /**
5
+ * OpenAI-compatible speech-to-text adapter (`POST <base>/audio/transcriptions`).
6
+ * Works with OpenAI, Groq, Fireworks, LiteLLM/Bifrost gateways, vLLM,
7
+ * speaches/faster-whisper-server, and whisper.cpp server.
8
+ *
9
+ * Server-side only: the adapter holds the API key.
10
+ */
11
+ export declare class OpenAICompatibleTranscriber extends HttpSpeechAdapter implements Transcriber {
12
+ readonly type: "openai-compatible";
13
+ private readonly endpoint;
14
+ private readonly model;
15
+ private readonly responseFormat?;
16
+ private readonly maxBytes;
17
+ private readonly retry;
18
+ private readonly onUsage?;
19
+ constructor(options: OpenAICompatibleTranscriberOptions);
20
+ transcribe(request: TranscriptionRequest): Promise<TranscriptResult>;
21
+ private resolveWireOptions;
22
+ }
23
+ //# sourceMappingURL=openai-compatible-transcriber.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"openai-compatible-transcriber.d.ts","sourceRoot":"","sources":["../../src/adapters/openai-compatible-transcriber.ts"],"names":[],"mappings":"AAKA,OAAO,EAEL,iBAAiB,EAGlB,MAAM,mBAAmB,CAAC;AAK3B,OAAO,KAAK,EACV,kCAAkC,EAElC,WAAW,EACX,oBAAoB,EAEpB,gBAAgB,EACjB,MAAM,oBAAoB,CAAC;AAsB5B,wBAAgB,4BAA4B,CAAC,KAAK,EAAE,MAAM,GAAG,OAAO,CAEnE;AAQD;;;;;;GAMG;AACH,qBAAa,2BACX,SAAQ,iBACR,YAAW,WAAW;IAEtB,QAAQ,CAAC,IAAI,EAAG,mBAAmB,CAAU;IAE7C,OAAO,CAAC,QAAQ,CAAC,QAAQ,CAAS;IAClC,OAAO,CAAC,QAAQ,CAAC,KAAK,CAAS;IAC/B,OAAO,CAAC,QAAQ,CAAC,cAAc,CAAC,CAA8B;IAC9D,OAAO,CAAC,QAAQ,CAAC,QAAQ,CAAS;IAClC,OAAO,CAAC,QAAQ,CAAC,KAAK,CAAuC;IAC7D,OAAO,CAAC,QAAQ,CAAC,OAAO,CAAC,CAAsB;gBAEnC,OAAO,EAAE,kCAAkC;IAajD,UAAU,CAAC,OAAO,EAAE,oBAAoB,GAAG,OAAO,CAAC,gBAAgB,CAAC;IA0E1E,OAAO,CAAC,kBAAkB;CAsB3B"}
@@ -1 +1 @@
1
- {"version":3,"file":"studio-server.d.ts","sourceRoot":"","sources":["../../src/adapters/studio-server.ts"],"names":[],"mappings":"AAAA,OAAO,EAIL,iBAAiB,EAGlB,MAAM,mBAAmB,CAAC;AAC3B,OAAO,KAAK,EACV,iBAAiB,EACjB,oCAAoC,EACpC,8BAA8B,EAC9B,gBAAgB,EAChB,iBAAiB,EACjB,WAAW,EACX,oBAAoB,EACpB,gBAAgB,EACjB,MAAM,oBAAoB,CAAC;AAE5B,qBAAa,uBACX,SAAQ,iBACR,YAAW,WAAW;IAEtB,QAAQ,CAAC,IAAI,EAAG,eAAe,CAAU;IACzC,OAAO,CAAC,QAAQ,CAAC,cAAc,CAAS;gBAE5B,OAAO,EAAE,8BAA8B;IAK7C,UAAU,CAAC,OAAO,EAAE,oBAAoB,GAAG,OAAO,CAAC,gBAAgB,CAAC;CAe3E;AAED,qBAAa,6BACX,SAAQ,iBACR,YAAW,iBAAiB;IAE5B,QAAQ,CAAC,IAAI,EAAG,eAAe,CAAU;IACzC,OAAO,CAAC,QAAQ,CAAC,cAAc,CAAS;IACxC,OAAO,CAAC,QAAQ,CAAC,YAAY,CAAC,CAAS;gBAE3B,OAAO,EAAE,oCAAoC;IAMnD,UAAU,CAAC,OAAO,EAAE,gBAAgB,GAAG,OAAO,CAAC,iBAAiB,CAAC;CAaxE"}
1
+ {"version":3,"file":"studio-server.d.ts","sourceRoot":"","sources":["../../src/adapters/studio-server.ts"],"names":[],"mappings":"AACA,OAAO,EAGL,iBAAiB,EAGlB,MAAM,mBAAmB,CAAC;AAC3B,OAAO,KAAK,EACV,iBAAiB,EACjB,oCAAoC,EACpC,8BAA8B,EAC9B,gBAAgB,EAChB,iBAAiB,EACjB,WAAW,EACX,oBAAoB,EACpB,gBAAgB,EACjB,MAAM,oBAAoB,CAAC;AAE5B,qBAAa,uBACX,SAAQ,iBACR,YAAW,WAAW;IAEtB,QAAQ,CAAC,IAAI,EAAG,eAAe,CAAU;IACzC,OAAO,CAAC,QAAQ,CAAC,cAAc,CAAS;gBAE5B,OAAO,EAAE,8BAA8B;IAK7C,UAAU,CAAC,OAAO,EAAE,oBAAoB,GAAG,OAAO,CAAC,gBAAgB,CAAC;CAuB3E;AAED,qBAAa,6BACX,SAAQ,iBACR,YAAW,iBAAiB;IAE5B,QAAQ,CAAC,IAAI,EAAG,eAAe,CAAU;IACzC,OAAO,CAAC,QAAQ,CAAC,cAAc,CAAS;IACxC,OAAO,CAAC,QAAQ,CAAC,YAAY,CAAC,CAAS;gBAE3B,OAAO,EAAE,oCAAoC;IAMnD,UAAU,CAAC,OAAO,EAAE,gBAAgB,GAAG,OAAO,CAAC,iBAAiB,CAAC;CAaxE"}
package/dist/index.d.ts CHANGED
@@ -21,7 +21,9 @@
21
21
  *
22
22
  * @packageDocumentation
23
23
  */
24
+ export { DEFAULT_MAX_AUDIO_BYTES, mimeTypeToAudioExtension, } from './shared/audio.js';
25
+ export { type SpeechEnv, TRANSCRIBER_ENV_KEYS, } from './shared/env.js';
24
26
  export * from './shared/errors.js';
25
27
  export { getAvailableSpeechAdapters, getSpeech, getSpeechSynthesizer, getTranscriber, type SpeechFactoryContext, } from './shared/factory.js';
26
- export type { AudioBytes, AudioInput, GetSpeechOptions, GetSpeechSynthesizerOptions, GetTranscriberOptions, OpenAICompatibleSpeechSynthesizerOptions, Qwen3SpeechSynthesizerOptions, Speech, SpeechAdapterAvailability, SpeechAdapterType, SpeechFetch, SpeechSynthesizer, SpeechSynthesizerType, SpeechVoice, SpeechVoiceInput, StudioServerSpeechSynthesizerOptions, StudioServerTranscriberOptions, SynthesisRequest, SynthesizedSpeech, Transcriber, TranscriberType, TranscriptionRequest, TranscriptResult, TranscriptSegment, WordTiming, } from './shared/types.js';
28
+ export type { AudioBytes, AudioInput, AudioSource, GetSpeechOptions, GetSpeechSynthesizerOptions, GetTranscriberOptions, OpenAICompatibleSpeechSynthesizerOptions, OpenAICompatibleTranscriberOptions, Qwen3SpeechSynthesizerOptions, Speech, SpeechAdapterAvailability, SpeechAdapterType, SpeechFetch, SpeechOperation, SpeechRetryOptions, SpeechSynthesizer, SpeechSynthesizerType, SpeechUsage, SpeechUsageCallback, SpeechVoice, SpeechVoiceInput, StudioServerSpeechSynthesizerOptions, StudioServerTranscriberOptions, SynthesisRequest, SynthesizedSpeech, TimestampGranularity, Transcriber, TranscriberType, TranscriptionRequest, TranscriptionResponseFormat, TranscriptResult, TranscriptSegment, WordTiming, } from './shared/types.js';
27
29
  //# sourceMappingURL=index.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;GAsBG;AAEH,cAAc,oBAAoB,CAAC;AACnC,OAAO,EACL,0BAA0B,EAC1B,SAAS,EACT,oBAAoB,EACpB,cAAc,EACd,KAAK,oBAAoB,GAC1B,MAAM,qBAAqB,CAAC;AAC7B,YAAY,EACV,UAAU,EACV,UAAU,EACV,gBAAgB,EAChB,2BAA2B,EAC3B,qBAAqB,EACrB,wCAAwC,EACxC,6BAA6B,EAC7B,MAAM,EACN,yBAAyB,EACzB,iBAAiB,EACjB,WAAW,EACX,iBAAiB,EACjB,qBAAqB,EACrB,WAAW,EACX,gBAAgB,EAChB,oCAAoC,EACpC,8BAA8B,EAC9B,gBAAgB,EAChB,iBAAiB,EACjB,WAAW,EACX,eAAe,EACf,oBAAoB,EACpB,gBAAgB,EAChB,iBAAiB,EACjB,UAAU,GACX,MAAM,mBAAmB,CAAC"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;GAsBG;AAEH,OAAO,EACL,uBAAuB,EACvB,wBAAwB,GACzB,MAAM,mBAAmB,CAAC;AAC3B,OAAO,EACL,KAAK,SAAS,EACd,oBAAoB,GACrB,MAAM,iBAAiB,CAAC;AACzB,cAAc,oBAAoB,CAAC;AACnC,OAAO,EACL,0BAA0B,EAC1B,SAAS,EACT,oBAAoB,EACpB,cAAc,EACd,KAAK,oBAAoB,GAC1B,MAAM,qBAAqB,CAAC;AAC7B,YAAY,EACV,UAAU,EACV,UAAU,EACV,WAAW,EACX,gBAAgB,EAChB,2BAA2B,EAC3B,qBAAqB,EACrB,wCAAwC,EACxC,kCAAkC,EAClC,6BAA6B,EAC7B,MAAM,EACN,yBAAyB,EACzB,iBAAiB,EACjB,WAAW,EACX,eAAe,EACf,kBAAkB,EAClB,iBAAiB,EACjB,qBAAqB,EACrB,WAAW,EACX,mBAAmB,EACnB,WAAW,EACX,gBAAgB,EAChB,oCAAoC,EACpC,8BAA8B,EAC9B,gBAAgB,EAChB,iBAAiB,EACjB,oBAAoB,EACpB,WAAW,EACX,eAAe,EACf,oBAAoB,EACpB,2BAA2B,EAC3B,gBAAgB,EAChB,iBAAiB,EACjB,UAAU,GACX,MAAM,mBAAmB,CAAC"}