@broberg/ai-sdk 0.45.0 → 0.46.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/README.md +37 -0
- package/dist/index.d.ts +24 -4
- package/dist/index.js +22 -10
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -81,6 +81,43 @@ const { data } = await ai.contracts.extract({
|
|
|
81
81
|
// also: ai.contracts.{ mockup, design, classify, rerank }
|
|
82
82
|
```
|
|
83
83
|
|
|
84
|
+
## Read-aloud: what was actually SPOKEN is not your `text`
|
|
85
|
+
|
|
86
|
+
`ai.tts({ pronunciations })` rewrites your text before the provider ever sees it —
|
|
87
|
+
`broberg.ai` becomes four spoken words. So **`text` is a retelling of the audio**, and a
|
|
88
|
+
word-highlighter built on `text` alone will drift at every rewritten word.
|
|
89
|
+
|
|
90
|
+
Two fields close that gap:
|
|
91
|
+
|
|
92
|
+
```ts
|
|
93
|
+
const { audio, wordTimings, ssml } = await ai.tts({
|
|
94
|
+
text, voice, wordTimings: true, pronunciations,
|
|
95
|
+
override: { provider: "azure" },
|
|
96
|
+
});
|
|
97
|
+
wordTimings.words // [{ text, startMs, endMs, sourceStart, sourceEnd }]
|
|
98
|
+
wordTimings.unaligned // spoken words we could NOT place — never a guessed offset
|
|
99
|
+
ssml // the EXACT markup we sent, verbatim — your ground truth
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
`sourceStart`/`sourceEnd` index **your original text**, because Azure reports audio time
|
|
103
|
+
only and carries no text offset at all; the link back to the manuscript is derived here.
|
|
104
|
+
`ssml` exists so you can check that derivation instead of trusting it — it is the string
|
|
105
|
+
we sent, not a rebuild (filed by a consumer who could not verify what was spoken, F055.4).
|
|
106
|
+
It is `undefined` on routes that build no markup; an empty string would be a different claim.
|
|
107
|
+
|
|
108
|
+
**Two matcher rules that surprise people**, both measured in `pronunciations`:
|
|
109
|
+
|
|
110
|
+
- **Case-insensitive.** `ai`, `Ai` and `AI` all match a rule for `AI`.
|
|
111
|
+
- **A word touching a hyphen keeps its spelling.** `broberg.ai-drevet` is NOT rewritten
|
|
112
|
+
unless that entry sets `matchInCompounds: true` — the rule that keeps `mail` out of
|
|
113
|
+
`e-mail`. A consumer deriving their own expected-word list got exactly these two
|
|
114
|
+
occurrences wrong before the field existed.
|
|
115
|
+
|
|
116
|
+
**Word timings need Azure's BATCH route**, a different call shape (submit → poll → ZIP),
|
|
117
|
+
and that route requires the resource's custom subdomain — the regional host answers 401
|
|
118
|
+
with a valid key, blaming the key. Set `AZURE_SPEECH_RESOURCE`; without it `wordTimings`
|
|
119
|
+
fails before the call, naming the fix.
|
|
120
|
+
|
|
84
121
|
## Providers & tiers
|
|
85
122
|
|
|
86
123
|
Adapters: **Anthropic** (HTTP + `claude -p` subprocess), **OpenAI**, **Google
|
package/dist/index.d.ts
CHANGED
|
@@ -555,6 +555,17 @@ interface PodcastResult {
|
|
|
555
555
|
* — Azure reports only audio time, so the link back to the manuscript is derived
|
|
556
556
|
* here. `unaligned` names any spoken word that could not be placed. */
|
|
557
557
|
wordTimings?: AlignedWordTimings;
|
|
558
|
+
/** F055.4 — the EXACT markup we sent the provider, verbatim, not rebuilt.
|
|
559
|
+
*
|
|
560
|
+
* Why it exists: the text you passed is not what was spoken. A pronunciation
|
|
561
|
+
* dictionary rewrites it, so `text` is a RETELLING of the audio — and without this
|
|
562
|
+
* field a consumer cannot check our retelling any more than we could check theirs.
|
|
563
|
+
* cms filed exactly that (11 September 2026) while we were asking them for markup
|
|
564
|
+
* only we could produce.
|
|
565
|
+
*
|
|
566
|
+
* UNDEFINED when the route builds no markup (ElevenLabs routes by voice, not SSML).
|
|
567
|
+
* An empty string would say "we sent empty markup", which is a different claim. */
|
|
568
|
+
ssml?: string;
|
|
558
569
|
usage: Usage;
|
|
559
570
|
}
|
|
560
571
|
|
|
@@ -2385,8 +2396,17 @@ declare function azureAdapter(config?: {
|
|
|
2385
2396
|
sttPricePerMin?: number;
|
|
2386
2397
|
/** STT base URL override (e.g. a resource custom domain). */
|
|
2387
2398
|
sttBaseUrl?: string;
|
|
2388
|
-
/** Resource name → custom-domain
|
|
2389
|
-
* (or env AZURE_SPEECH_RESOURCE).
|
|
2399
|
+
/** Resource name → custom-domain host `{resource}.cognitiveservices.azure.com`
|
|
2400
|
+
* (or env AZURE_SPEECH_RESOURCE).
|
|
2401
|
+
*
|
|
2402
|
+
* **STT works without it** — the regional host is a legitimate route for
|
|
2403
|
+
* `speechtotext/transcriptions` and is the tested default (F029).
|
|
2404
|
+
*
|
|
2405
|
+
* **BATCH SYNTHESIS REQUIRES IT.** Every example in Microsoft's batch-synthesis
|
|
2406
|
+
* docs uses the resource host; the regional host is shown nowhere, and cms measured
|
|
2407
|
+
* it answering 401 with a valid key (F055.3). Without this set, `wordTimings` fails
|
|
2408
|
+
* before the call rather than spending your time on Azure's "invalid subscription
|
|
2409
|
+
* key or wrong API endpoint" — which names the key first and sends you the wrong way. */
|
|
2390
2410
|
resource?: string;
|
|
2391
2411
|
/** Fast-transcription api-version (overrides the GA default). */
|
|
2392
2412
|
sttApiVersion?: string;
|
|
@@ -2514,8 +2534,8 @@ declare const falStubAdapter: ProviderAdapter;
|
|
|
2514
2534
|
* wires the live adapters. */
|
|
2515
2535
|
declare const stubProviders: Record<string, ProviderAdapter>;
|
|
2516
2536
|
|
|
2517
|
-
declare const VERSION: "0.
|
|
2518
|
-
declare const SDK_TAG: "@broberg/ai-sdk@0.
|
|
2537
|
+
declare const VERSION: "0.46.0";
|
|
2538
|
+
declare const SDK_TAG: "@broberg/ai-sdk@0.46.0";
|
|
2519
2539
|
|
|
2520
2540
|
/** Built-in defaults. Every entry is overridable via AiConfig.defaults or a
|
|
2521
2541
|
* per-call override.
|
package/dist/index.js
CHANGED
|
@@ -1836,11 +1836,14 @@ function azureAdapter(config = {}) {
|
|
|
1836
1836
|
}
|
|
1837
1837
|
return classifyRegionName(region());
|
|
1838
1838
|
}
|
|
1839
|
-
function
|
|
1840
|
-
if (config.sttBaseUrl) return config.sttBaseUrl.replace(/\/$/, "");
|
|
1839
|
+
function sttHost() {
|
|
1840
|
+
if (config.sttBaseUrl) return { url: config.sttBaseUrl.replace(/\/$/, ""), source: "explicit" };
|
|
1841
1841
|
const resource = config.resource ?? process.env.AZURE_SPEECH_RESOURCE;
|
|
1842
|
-
if (resource) return `https://${resource}.cognitiveservices.azure.com
|
|
1843
|
-
return `https://${region()}.api.cognitive.microsoft.com
|
|
1842
|
+
if (resource) return { url: `https://${resource}.cognitiveservices.azure.com`, source: "resource" };
|
|
1843
|
+
return { url: `https://${region()}.api.cognitive.microsoft.com`, source: "regional-fallback" };
|
|
1844
|
+
}
|
|
1845
|
+
function sttBaseUrl() {
|
|
1846
|
+
return sttHost().url;
|
|
1844
1847
|
}
|
|
1845
1848
|
function priceFor(chars, model) {
|
|
1846
1849
|
const usage = freshUsage({
|
|
@@ -1874,6 +1877,7 @@ function azureAdapter(config = {}) {
|
|
|
1874
1877
|
async function tts(req) {
|
|
1875
1878
|
if (req.wordTimings) return ttsBatch(req);
|
|
1876
1879
|
const format = req.format ?? DEFAULT_FORMAT;
|
|
1880
|
+
const ssml = buildSsml(req);
|
|
1877
1881
|
const res = await fetchImpl(
|
|
1878
1882
|
`https://${region()}.tts.speech.microsoft.com/cognitiveservices/v1`,
|
|
1879
1883
|
{
|
|
@@ -1883,7 +1887,7 @@ function azureAdapter(config = {}) {
|
|
|
1883
1887
|
"Content-Type": "application/ssml+xml",
|
|
1884
1888
|
"X-Microsoft-OutputFormat": format
|
|
1885
1889
|
},
|
|
1886
|
-
body:
|
|
1890
|
+
body: ssml
|
|
1887
1891
|
}
|
|
1888
1892
|
);
|
|
1889
1893
|
if (!res.ok) {
|
|
@@ -1891,10 +1895,17 @@ function azureAdapter(config = {}) {
|
|
|
1891
1895
|
throw new Error(`azure tts ${res.status}: ${body.slice(0, 300)}`);
|
|
1892
1896
|
}
|
|
1893
1897
|
const audio = new Uint8Array(await res.arrayBuffer());
|
|
1894
|
-
return { audio, mimeType: "audio/mpeg", usage: priceFor(req.text.length, req.spec.model) };
|
|
1898
|
+
return { audio, mimeType: "audio/mpeg", ssml, usage: priceFor(req.text.length, req.spec.model) };
|
|
1895
1899
|
}
|
|
1896
1900
|
async function ttsBatch(req) {
|
|
1897
|
-
const
|
|
1901
|
+
const ssml = buildSsml(req);
|
|
1902
|
+
const picked = sttHost();
|
|
1903
|
+
if (picked.source === "regional-fallback") {
|
|
1904
|
+
throw new Error(
|
|
1905
|
+
`azure batch synthesis (wordTimings) needs the resource's custom subdomain, not the regional host ${picked.url}. Set AZURE_SPEECH_RESOURCE (or config.resource) to your Speech resource name so the call goes to {resource}.cognitiveservices.azure.com, and make sure custom subdomain is enabled on that resource. Azure answers 401 here with a VALID key, and its message blames the key \u2014 measured by cms 11 September 2026.`
|
|
1906
|
+
);
|
|
1907
|
+
}
|
|
1908
|
+
const host = picked.url;
|
|
1898
1909
|
const api = "api-version=2024-04-01";
|
|
1899
1910
|
const id = `wt-${Date.now()}-${Math.random().toString(36).slice(2, 10)}`;
|
|
1900
1911
|
const headers = { "Ocp-Apim-Subscription-Key": key(), "Content-Type": "application/json" };
|
|
@@ -1903,7 +1914,7 @@ function azureAdapter(config = {}) {
|
|
|
1903
1914
|
headers,
|
|
1904
1915
|
body: JSON.stringify({
|
|
1905
1916
|
inputKind: "SSML",
|
|
1906
|
-
inputs: [{ content:
|
|
1917
|
+
inputs: [{ content: ssml }],
|
|
1907
1918
|
properties: {
|
|
1908
1919
|
wordBoundaryEnabled: true,
|
|
1909
1920
|
// ONE audio file and ONE word list for the whole text. Without it a chunked
|
|
@@ -1963,6 +1974,7 @@ function azureAdapter(config = {}) {
|
|
|
1963
1974
|
// Batch defaults to riff PCM, not mp3 — saying audio/mpeg here would be a lie the
|
|
1964
1975
|
// browser would act on.
|
|
1965
1976
|
mimeType: req.format?.includes("mp3") ? "audio/mpeg" : "audio/wav",
|
|
1977
|
+
ssml,
|
|
1966
1978
|
// Aligned against the ORIGINAL text, with the dictionary, so the offsets index the
|
|
1967
1979
|
// manuscript rather than the SSML we sent.
|
|
1968
1980
|
wordTimings: alignWordTimings(req.text, boundaries, { pronunciations: req.pronunciations }),
|
|
@@ -3325,8 +3337,8 @@ var aiConfigSchema = z.object({
|
|
|
3325
3337
|
});
|
|
3326
3338
|
|
|
3327
3339
|
// src/version.ts
|
|
3328
|
-
var VERSION = "0.
|
|
3329
|
-
var SDK_TAG = "@broberg/ai-sdk@0.
|
|
3340
|
+
var VERSION = "0.46.0";
|
|
3341
|
+
var SDK_TAG = "@broberg/ai-sdk@0.46.0";
|
|
3330
3342
|
|
|
3331
3343
|
// src/cost/sinks/upmetrics.ts
|
|
3332
3344
|
function upmetricsSink(config) {
|