@broberg/ai-sdk 0.45.1 → 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 +13 -2
- package/dist/index.js +8 -5
- 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
|
|
|
@@ -2523,8 +2534,8 @@ declare const falStubAdapter: ProviderAdapter;
|
|
|
2523
2534
|
* wires the live adapters. */
|
|
2524
2535
|
declare const stubProviders: Record<string, ProviderAdapter>;
|
|
2525
2536
|
|
|
2526
|
-
declare const VERSION: "0.
|
|
2527
|
-
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";
|
|
2528
2539
|
|
|
2529
2540
|
/** Built-in defaults. Every entry is overridable via AiConfig.defaults or a
|
|
2530
2541
|
* per-call override.
|
package/dist/index.js
CHANGED
|
@@ -1877,6 +1877,7 @@ function azureAdapter(config = {}) {
|
|
|
1877
1877
|
async function tts(req) {
|
|
1878
1878
|
if (req.wordTimings) return ttsBatch(req);
|
|
1879
1879
|
const format = req.format ?? DEFAULT_FORMAT;
|
|
1880
|
+
const ssml = buildSsml(req);
|
|
1880
1881
|
const res = await fetchImpl(
|
|
1881
1882
|
`https://${region()}.tts.speech.microsoft.com/cognitiveservices/v1`,
|
|
1882
1883
|
{
|
|
@@ -1886,7 +1887,7 @@ function azureAdapter(config = {}) {
|
|
|
1886
1887
|
"Content-Type": "application/ssml+xml",
|
|
1887
1888
|
"X-Microsoft-OutputFormat": format
|
|
1888
1889
|
},
|
|
1889
|
-
body:
|
|
1890
|
+
body: ssml
|
|
1890
1891
|
}
|
|
1891
1892
|
);
|
|
1892
1893
|
if (!res.ok) {
|
|
@@ -1894,9 +1895,10 @@ function azureAdapter(config = {}) {
|
|
|
1894
1895
|
throw new Error(`azure tts ${res.status}: ${body.slice(0, 300)}`);
|
|
1895
1896
|
}
|
|
1896
1897
|
const audio = new Uint8Array(await res.arrayBuffer());
|
|
1897
|
-
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) };
|
|
1898
1899
|
}
|
|
1899
1900
|
async function ttsBatch(req) {
|
|
1901
|
+
const ssml = buildSsml(req);
|
|
1900
1902
|
const picked = sttHost();
|
|
1901
1903
|
if (picked.source === "regional-fallback") {
|
|
1902
1904
|
throw new Error(
|
|
@@ -1912,7 +1914,7 @@ function azureAdapter(config = {}) {
|
|
|
1912
1914
|
headers,
|
|
1913
1915
|
body: JSON.stringify({
|
|
1914
1916
|
inputKind: "SSML",
|
|
1915
|
-
inputs: [{ content:
|
|
1917
|
+
inputs: [{ content: ssml }],
|
|
1916
1918
|
properties: {
|
|
1917
1919
|
wordBoundaryEnabled: true,
|
|
1918
1920
|
// ONE audio file and ONE word list for the whole text. Without it a chunked
|
|
@@ -1972,6 +1974,7 @@ function azureAdapter(config = {}) {
|
|
|
1972
1974
|
// Batch defaults to riff PCM, not mp3 — saying audio/mpeg here would be a lie the
|
|
1973
1975
|
// browser would act on.
|
|
1974
1976
|
mimeType: req.format?.includes("mp3") ? "audio/mpeg" : "audio/wav",
|
|
1977
|
+
ssml,
|
|
1975
1978
|
// Aligned against the ORIGINAL text, with the dictionary, so the offsets index the
|
|
1976
1979
|
// manuscript rather than the SSML we sent.
|
|
1977
1980
|
wordTimings: alignWordTimings(req.text, boundaries, { pronunciations: req.pronunciations }),
|
|
@@ -3334,8 +3337,8 @@ var aiConfigSchema = z.object({
|
|
|
3334
3337
|
});
|
|
3335
3338
|
|
|
3336
3339
|
// src/version.ts
|
|
3337
|
-
var VERSION = "0.
|
|
3338
|
-
var SDK_TAG = "@broberg/ai-sdk@0.
|
|
3340
|
+
var VERSION = "0.46.0";
|
|
3341
|
+
var SDK_TAG = "@broberg/ai-sdk@0.46.0";
|
|
3339
3342
|
|
|
3340
3343
|
// src/cost/sinks/upmetrics.ts
|
|
3341
3344
|
function upmetricsSink(config) {
|