@cartanova/qgrid-ai-sdk 2.16.0 → 2.17.1
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.ko.md +2 -2
- package/README.md +2 -2
- package/dist/index.js +41 -6
- package/package.json +1 -1
package/README.ko.md
CHANGED
|
@@ -485,9 +485,9 @@ const speech = await generateSpeech({
|
|
|
485
485
|
speech.audio.uint8Array;
|
|
486
486
|
```
|
|
487
487
|
|
|
488
|
-
- `transcribe`는 bytes로 오디오 형식을 감지하고 실패하면 `audio/wav`를 씁니다. 서버는 `audio/*`가 아닌 값을 거부합니다. transcript
|
|
488
|
+
- `transcribe`는 bytes로 오디오 형식을 감지하고 실패하면 `audio/wav`를 씁니다. 서버는 `audio/*`가 아닌 값을 거부합니다. Grok transcript는 들린 단어마다 segment 하나를 담습니다(`segments: [{ text, startSecond, endSecond }]`, 2.17.0+). OpenAI realtime transcript에는 없습니다.
|
|
489
489
|
- 음성 합성 텍스트는 최대 15,000자입니다. `voice`, `language`, `outputFormat`을 지정하지 않으면 서버 기본값 `ara`, `en`, `mp3`가 쓰이며, live 검증된 조합은 이것뿐입니다. 다른 voice, language(`@ai-sdk/xai`의 `auto` 포함), codec은 그대로 전달되지만 미검증입니다. 서버가 돌려준 오디오 MIME 타입은 `providerMetadata.qgrid.mediaType`에 있고, AI SDK의 `speech.audio.mediaType`은 bytes에서 추정한 값입니다.
|
|
490
|
-
- `providerOptions: { xai: { withTimestamps: true } }`(2.16.0+, Grok
|
|
490
|
+
- 단어 타이밍: `providerOptions: { xai: { withTimestamps: true } }`(2.16.0+, Grok) 또는 `providerOptions: { qgrid: { withTimestamps: true } }`(2.17.0+, Grok·OpenAI)를 주면 글자별 타이밍이 `providerMetadata.qgrid.audioTimestamps`에 담깁니다. `graphChars`는 입력 글자(공백·문장부호·음성 태그 포함), `graphTimes`는 같은 인덱스의 `[start, end]` 초이고, 전체 길이는 `providerMetadata.qgrid.duration`입니다. `@ai-sdk/xai`가 `providerMetadata.xai`에 담는 모양과 같습니다. `providerMetadata.qgrid.timestampsSource`는 Grok이면 `"provider"`(Grok이 직접 잰 값), OpenAI면 `"aligned"`입니다. OpenAI는 서버가 생성된 오디오를 Grok STT로 받아쓰고 단어 단위로 원문과 짝지어 만듭니다(못 들은 단어는 앞뒤 단어 사이 구간을 나눠 가짐). 타이밍 단계가 실패해도 오디오는 돌려주고, 메타데이터에 `timestampsError`와 AI SDK warning을 남깁니다. 텍스트 정규화로 풀린 토큰은 첫 글자가 전체 구간을 가집니다.
|
|
491
491
|
|
|
492
492
|
### OpenAI 음성 인식과 합성 (ChatGPT 구독)
|
|
493
493
|
|
package/README.md
CHANGED
|
@@ -497,9 +497,9 @@ const speech = await generateSpeech({
|
|
|
497
497
|
speech.audio.uint8Array;
|
|
498
498
|
```
|
|
499
499
|
|
|
500
|
-
- `transcribe` detects the audio type from its bytes and falls back to `audio/wav`; the server rejects anything that is not `audio/*`.
|
|
500
|
+
- `transcribe` detects the audio type from its bytes and falls back to `audio/wav`; the server rejects anything that is not `audio/*`. Grok transcripts carry one segment per heard word (`segments: [{ text, startSecond, endSecond }]`, 2.17.0+); OpenAI realtime transcripts carry none.
|
|
501
501
|
- Speech text is limited to 15,000 characters. Unset `voice`, `language` and `outputFormat` use the server defaults `ara`, `en` and `mp3`, the only combination verified live. Other voices, languages (including `@ai-sdk/xai`'s `auto`) and codecs are passed through but unverified. The server's audio MIME type is in `providerMetadata.qgrid.mediaType`; AI SDK's `speech.audio.mediaType` is sniffed from the bytes.
|
|
502
|
-
- `providerOptions: { xai: { withTimestamps: true } }` (2.16.0+, Grok
|
|
502
|
+
- Word timing: `providerOptions: { xai: { withTimestamps: true } }` (2.16.0+, Grok) or `providerOptions: { qgrid: { withTimestamps: true } }` (2.17.0+, Grok or OpenAI) returns per-character timing in `providerMetadata.qgrid.audioTimestamps`: `graphChars` (input characters, spaces, punctuation and speech tags included) and the index-aligned `graphTimes` `[start, end]` seconds, plus `providerMetadata.qgrid.duration`. The shape matches what `@ai-sdk/xai` puts under `providerMetadata.xai`. `providerMetadata.qgrid.timestampsSource` is `"provider"` for Grok, which times its own audio, and `"aligned"` for OpenAI, whose audio the server transcribes with Grok STT and aligns word by word to the text (unheard words share the gap between neighbours). If the timing step fails the audio is still returned, with `timestampsError` in the metadata and an AI SDK warning. Text normalization gives an expanded token's whole span to its first character.
|
|
503
503
|
|
|
504
504
|
### OpenAI transcription and speech (ChatGPT subscription)
|
|
505
505
|
|
package/dist/index.js
CHANGED
|
@@ -1531,8 +1531,31 @@ const speakOutputSchema = z.object({
|
|
|
1531
1531
|
graphChars: z.array(z.string()),
|
|
1532
1532
|
graphTimes: z.array(z.tuple([z.number(), z.number()]))
|
|
1533
1533
|
}).optional(),
|
|
1534
|
-
durationSec: z.number().optional()
|
|
1534
|
+
durationSec: z.number().optional(),
|
|
1535
|
+
timestampsSource: z.enum(["provider", "aligned"]).optional(),
|
|
1536
|
+
timestampsError: z.string().optional()
|
|
1535
1537
|
});
|
|
1538
|
+
/**
|
|
1539
|
+
* `providerOptions.qgrid.withTimestamps` works for both providers (OpenAI has no official option
|
|
1540
|
+
* for it). `parse*QgridOptions` drop unknown keys, so it is read here directly.
|
|
1541
|
+
*/
|
|
1542
|
+
const qgridTimestampsOptionSchema = z.looseObject({ withTimestamps: z.boolean().optional() });
|
|
1543
|
+
function qgridWantsTimestamps(raw) {
|
|
1544
|
+
return qgridTimestampsOptionSchema.safeParse(raw).data?.withTimestamps === true;
|
|
1545
|
+
}
|
|
1546
|
+
/** Shared speech metadata: timing fields plus a warning when requested timing could not be made. */
|
|
1547
|
+
function speechTimingMetadata(data, metadata, warnings) {
|
|
1548
|
+
if (data.audioTimestamps) metadata.audioTimestamps = data.audioTimestamps;
|
|
1549
|
+
if (data.durationSec !== void 0) metadata.duration = data.durationSec;
|
|
1550
|
+
if (data.timestampsSource) metadata.timestampsSource = data.timestampsSource;
|
|
1551
|
+
if (data.timestampsError) {
|
|
1552
|
+
metadata.timestampsError = data.timestampsError;
|
|
1553
|
+
warnings.push({
|
|
1554
|
+
type: "other",
|
|
1555
|
+
message: `timestamps unavailable: ${data.timestampsError}`
|
|
1556
|
+
});
|
|
1557
|
+
}
|
|
1558
|
+
}
|
|
1536
1559
|
function base64ToBytes(data) {
|
|
1537
1560
|
if (typeof Buffer !== "undefined") return new Uint8Array(Buffer.from(data, "base64"));
|
|
1538
1561
|
const binary = atob(data);
|
|
@@ -1589,7 +1612,7 @@ function createQgridSpeech(modelId, config) {
|
|
|
1589
1612
|
if (options.voice !== void 0) requestParams.voice = options.voice;
|
|
1590
1613
|
if (options.language !== void 0) requestParams.language = options.language;
|
|
1591
1614
|
if (format !== void 0) requestParams.format = format;
|
|
1592
|
-
if (xai.options?.withTimestamps === true) requestParams.withTimestamps = true;
|
|
1615
|
+
if (xai.options?.withTimestamps === true || qgridWantsTimestamps(options.providerOptions?.qgrid)) requestParams.withTimestamps = true;
|
|
1593
1616
|
if (projectName) requestParams.projectName = projectName;
|
|
1594
1617
|
const params = {
|
|
1595
1618
|
...requestParams,
|
|
@@ -1611,8 +1634,7 @@ function createQgridSpeech(modelId, config) {
|
|
|
1611
1634
|
tokenName: data.tokenName
|
|
1612
1635
|
};
|
|
1613
1636
|
if (data.requestLogId !== void 0) qgridMetadata.requestLogId = data.requestLogId;
|
|
1614
|
-
|
|
1615
|
-
if (data.durationSec !== void 0) qgridMetadata.duration = data.durationSec;
|
|
1637
|
+
speechTimingMetadata(data, qgridMetadata, warnings);
|
|
1616
1638
|
return {
|
|
1617
1639
|
audio: base64ToBytes(data.audio),
|
|
1618
1640
|
warnings,
|
|
@@ -1633,6 +1655,8 @@ function createQgridSpeech(modelId, config) {
|
|
|
1633
1655
|
* (default `marin`, Codex v2 list), `outputFormat` `wav` (default) or `pcm` — other formats
|
|
1634
1656
|
* warn and fall back to wav, as the official provider falls back to mp3 — `openai.instructions`
|
|
1635
1657
|
* as style guidance, `openai.speed` and `language` unsupported with the official warning text.
|
|
1658
|
+
* `providerOptions.qgrid.withTimestamps` returns timing the server infers by aligning a Grok STT
|
|
1659
|
+
* pass over the generated audio (`providerMetadata.qgrid.timestampsSource: "aligned"`).
|
|
1636
1660
|
*/
|
|
1637
1661
|
function createOpenAISpeech(modelId, serverUrl, projectName) {
|
|
1638
1662
|
return {
|
|
@@ -1675,6 +1699,7 @@ function createOpenAISpeech(modelId, serverUrl, projectName) {
|
|
|
1675
1699
|
if (format !== void 0) requestParams.format = format;
|
|
1676
1700
|
const instructions = options.instructions ?? openai.options?.instructions ?? void 0;
|
|
1677
1701
|
if (instructions !== void 0) requestParams.instructions = instructions;
|
|
1702
|
+
if (qgridWantsTimestamps(options.providerOptions?.qgrid)) requestParams.withTimestamps = true;
|
|
1678
1703
|
if (projectName) requestParams.projectName = projectName;
|
|
1679
1704
|
const params = {
|
|
1680
1705
|
...requestParams,
|
|
@@ -1696,6 +1721,7 @@ function createOpenAISpeech(modelId, serverUrl, projectName) {
|
|
|
1696
1721
|
tokenName: data.tokenName
|
|
1697
1722
|
};
|
|
1698
1723
|
if (data.requestLogId !== void 0) qgridMetadata.requestLogId = data.requestLogId;
|
|
1724
|
+
speechTimingMetadata(data, qgridMetadata, warnings);
|
|
1699
1725
|
return {
|
|
1700
1726
|
audio: base64ToBytes(data.audio),
|
|
1701
1727
|
warnings,
|
|
@@ -1784,7 +1810,12 @@ const transcribeOutputSchema = z.object({
|
|
|
1784
1810
|
durationSec: z.number().optional(),
|
|
1785
1811
|
model: z.string(),
|
|
1786
1812
|
tokenName: z.string(),
|
|
1787
|
-
requestLogId: z.number().optional()
|
|
1813
|
+
requestLogId: z.number().optional(),
|
|
1814
|
+
words: z.array(z.object({
|
|
1815
|
+
text: z.string(),
|
|
1816
|
+
start: z.number(),
|
|
1817
|
+
end: z.number()
|
|
1818
|
+
})).optional()
|
|
1788
1819
|
});
|
|
1789
1820
|
function normalizeMediaType(mediaType) {
|
|
1790
1821
|
const bare = mediaType.split(";")[0].trim().toLowerCase();
|
|
@@ -1840,7 +1871,11 @@ function createQgridTranscription(modelId, config) {
|
|
|
1840
1871
|
if (data.requestLogId !== void 0) qgridMetadata.requestLogId = data.requestLogId;
|
|
1841
1872
|
return {
|
|
1842
1873
|
text: data.text,
|
|
1843
|
-
segments: []
|
|
1874
|
+
segments: (data.words ?? []).map((word) => ({
|
|
1875
|
+
text: word.text,
|
|
1876
|
+
startSecond: word.start,
|
|
1877
|
+
endSecond: word.end
|
|
1878
|
+
})),
|
|
1844
1879
|
language: data.language,
|
|
1845
1880
|
durationInSeconds: data.durationSec,
|
|
1846
1881
|
warnings,
|