@cartanova/qgrid-ai-sdk 2.15.5 → 2.17.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.ko.md +3 -2
- package/README.md +3 -2
- package/dist/index.d.ts +3 -1
- package/dist/index.js +53 -6
- package/package.json +1 -1
package/README.ko.md
CHANGED
|
@@ -394,7 +394,7 @@ Grok language model은 user 이미지 part(https URL 또는 inline bytes)를 버
|
|
|
394
394
|
| Image | `n`(호출당 최대 3), `aspectRatio`, `xai.resolution`(`1k`/`1.5k`/`2k`), `xai.quality`, `xai.aspect_ratio`(`aspectRatio`가 없을 때) | 참조 `images`는 `referenceImages`(edit endpoint), `xai.output_format`은 `outputFormat`이 되며 `png`, `jpeg`, `webp`만 허용 | `size`, `seed`, `mask`, `xai.sync_mode`, `xai.user` |
|
|
395
395
|
| Video | `duration`(정수 1-15), `aspectRatio`, `xai.resolution`(`480p`/`720p`/`1080p`), `xai.mode: "reference-to-video"`(지원하는 유일한 mode, `xai.referenceImageUrls` 필요), `xai.pollIntervalMs`, `xai.pollTimeoutMs` | `resolution` `WxH`는 `@ai-sdk/xai`와 같이 매핑(`1280x720` → `720p`), prompt `image`는 시작 프레임, `xai.referenceImageUrls`(7개가 아닌 최대 5개)는 `referenceImages`, `xai.lastFrame`(qgrid 확장, `grok-imagine-video-1.5` 전용) | `fps`, `seed`, `xai.keyframes`, `xai.storageOptions`, `xai.referenceVoiceIds`, `xai.user`. `xai.mode` `edit-video`/`extend-video`와 `xai.videoUrl`은 예외 |
|
|
396
396
|
| Transcription | `xai.language` | 오디오 bytes는 base64로, `mediaType`은 파라미터 제거(`audio/webm;codecs=opus` → `audio/webm`) | 나머지 모든 `xai` 키(`audioFormat`, `sampleRate`, `format`, `diarize`, `keyterm`, ...) |
|
|
397
|
-
| Speech | `voice`, `language` | `outputFormat`은 `format`(`mp3`, `wav`, `pcm`, `mulaw`, `alaw`, 그 외 값은 `@ai-sdk/xai`처럼 warning과 함께 `mp3`) | `instructions`, `speed`, 모든 `xai` 키(`sampleRate`, `bitRate`, `
|
|
397
|
+
| Speech | `voice`, `language`, `xai.withTimestamps` | `outputFormat`은 `format`(`mp3`, `wav`, `pcm`, `mulaw`, `alaw`, 그 외 값은 `@ai-sdk/xai`처럼 warning과 함께 `mp3`) | `instructions`, `speed`, 그 밖의 모든 `xai` 키(`sampleRate`, `bitRate`, `replace`, ...) |
|
|
398
398
|
|
|
399
399
|
**요청 크기.** qgrid 서버는 최대 10 MiB JSON body를 받습니다. inline 미디어는 base64로 전송되어 3분의 1만큼
|
|
400
400
|
커지므로, 서버는 inline 오디오, 각 inline 이미지, 한 요청의 inline 이미지 합계(비디오: `image` + `lastFrame` +
|
|
@@ -485,8 +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) 또는 `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을 남깁니다. 텍스트 정규화로 풀린 토큰은 첫 글자가 전체 구간을 가집니다.
|
|
490
491
|
|
|
491
492
|
### OpenAI 음성 인식과 합성 (ChatGPT 구독)
|
|
492
493
|
|
package/README.md
CHANGED
|
@@ -404,7 +404,7 @@ Grok language models keep user image parts (https URLs or inline bytes) instead
|
|
|
404
404
|
| Image | `n` (up to 3 per call), `aspectRatio`, `xai.resolution` (`1k`/`1.5k`/`2k`), `xai.quality`, `xai.aspect_ratio` (used when `aspectRatio` is unset) | reference `images` become `referenceImages` (edit endpoint); `xai.output_format` becomes `outputFormat` and must be `png`, `jpeg`, or `webp` | `size`, `seed`, `mask`, `xai.sync_mode`, `xai.user` |
|
|
405
405
|
| Video | `duration` (integer 1-15), `aspectRatio`, `xai.resolution` (`480p`/`720p`/`1080p`), `xai.mode: "reference-to-video"` (the only supported mode; needs `xai.referenceImageUrls`), `xai.pollIntervalMs`, `xai.pollTimeoutMs` | `resolution` `WxH` is mapped as in `@ai-sdk/xai` (`1280x720` to `720p`); prompt `image` becomes the start frame; `xai.referenceImageUrls` (up to 5, not 7) become `referenceImages`; `xai.lastFrame` (qgrid extension, `grok-imagine-video-1.5` only) | `fps`, `seed`, `xai.keyframes`, `xai.storageOptions`, `xai.referenceVoiceIds`, `xai.user`. `xai.mode` `edit-video`/`extend-video` and `xai.videoUrl` throw |
|
|
406
406
|
| Transcription | `xai.language` | audio bytes become base64 and `mediaType` loses its parameters (`audio/webm;codecs=opus` to `audio/webm`) | every other `xai` key (`audioFormat`, `sampleRate`, `format`, `diarize`, `keyterm`, ...) |
|
|
407
|
-
| Speech | `voice`, `language` | `outputFormat` becomes `format` (`mp3`, `wav`, `pcm`, `mulaw`, `alaw`; anything else falls back to `mp3` with a warning, as in `@ai-sdk/xai`) | `instructions`, `speed`, every `xai` key (`sampleRate`, `bitRate`, `
|
|
407
|
+
| Speech | `voice`, `language`, `xai.withTimestamps` | `outputFormat` becomes `format` (`mp3`, `wav`, `pcm`, `mulaw`, `alaw`; anything else falls back to `mp3` with a warning, as in `@ai-sdk/xai`) | `instructions`, `speed`, every other `xai` key (`sampleRate`, `bitRate`, `replace`, ...) |
|
|
408
408
|
|
|
409
409
|
**Request size.** The qgrid server accepts JSON bodies up to 10 MiB. Inline media travels as
|
|
410
410
|
base64, which grows by a third, so the server caps inline audio, each inline image, and all
|
|
@@ -497,8 +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
|
+
- 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.
|
|
502
503
|
|
|
503
504
|
### OpenAI transcription and speech (ChatGPT subscription)
|
|
504
505
|
|
package/dist/index.d.ts
CHANGED
|
@@ -282,7 +282,9 @@ type QgridOpenAISpeechProviderOptions = {
|
|
|
282
282
|
/**
|
|
283
283
|
* Grok text-to-speech through `POST /api/qgrid/speak`. `voice`, `language` and `outputFormat`
|
|
284
284
|
* map to the server's `voice`, `language` and `format`; when omitted the server applies
|
|
285
|
-
* `ara` / `en` / `mp3
|
|
285
|
+
* `ara` / `en` / `mp3`. The model id only labels the log. `providerOptions.xai.withTimestamps`
|
|
286
|
+
* returns per-character timing in `providerMetadata.qgrid.audioTimestamps` (`graphChars`,
|
|
287
|
+
* `graphTimes`), the shape `@ai-sdk/xai` puts under `providerMetadata.xai`.
|
|
286
288
|
*/
|
|
287
289
|
declare function createQgridSpeech(modelId: QgridSpeechModel, config?: QgridMediaProviderConfig): SpeechModelV3;
|
|
288
290
|
//#endregion
|
package/dist/index.js
CHANGED
|
@@ -1478,7 +1478,7 @@ function isSpeechFormat(value) {
|
|
|
1478
1478
|
}
|
|
1479
1479
|
/**
|
|
1480
1480
|
* `providerOptions.xai` for Grok speech, field-for-field from `@ai-sdk/xai`
|
|
1481
|
-
* `xaiSpeechModelOptionsSchema`.
|
|
1481
|
+
* `xaiSpeechModelOptionsSchema`. Only `withTimestamps` is honored; every other set key warns.
|
|
1482
1482
|
*/
|
|
1483
1483
|
const xaiSpeechOptionsSchema = z.object({
|
|
1484
1484
|
sampleRate: z.union([
|
|
@@ -1520,12 +1520,42 @@ const OPENAI_SPEECH_FORMATS = ["wav", "pcm"];
|
|
|
1520
1520
|
function isOpenAISpeechFormat(value) {
|
|
1521
1521
|
return OPENAI_SPEECH_FORMATS.some((format) => format === value);
|
|
1522
1522
|
}
|
|
1523
|
+
/** `providerOptions.xai` keys honored by `qgrid.speech("grok/...")`. */
|
|
1524
|
+
const XAI_SPEECH_HONORED = /* @__PURE__ */ new Set(["withTimestamps"]);
|
|
1523
1525
|
const speakOutputSchema = z.object({
|
|
1524
1526
|
audio: z.string(),
|
|
1525
1527
|
mimeType: z.string(),
|
|
1526
1528
|
tokenName: z.string(),
|
|
1527
|
-
requestLogId: z.number().optional()
|
|
1529
|
+
requestLogId: z.number().optional(),
|
|
1530
|
+
audioTimestamps: z.object({
|
|
1531
|
+
graphChars: z.array(z.string()),
|
|
1532
|
+
graphTimes: z.array(z.tuple([z.number(), z.number()]))
|
|
1533
|
+
}).optional(),
|
|
1534
|
+
durationSec: z.number().optional(),
|
|
1535
|
+
timestampsSource: z.enum(["provider", "aligned"]).optional(),
|
|
1536
|
+
timestampsError: z.string().optional()
|
|
1528
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
|
+
}
|
|
1529
1559
|
function base64ToBytes(data) {
|
|
1530
1560
|
if (typeof Buffer !== "undefined") return new Uint8Array(Buffer.from(data, "base64"));
|
|
1531
1561
|
const binary = atob(data);
|
|
@@ -1536,7 +1566,9 @@ function base64ToBytes(data) {
|
|
|
1536
1566
|
/**
|
|
1537
1567
|
* Grok text-to-speech through `POST /api/qgrid/speak`. `voice`, `language` and `outputFormat`
|
|
1538
1568
|
* map to the server's `voice`, `language` and `format`; when omitted the server applies
|
|
1539
|
-
* `ara` / `en` / `mp3
|
|
1569
|
+
* `ara` / `en` / `mp3`. The model id only labels the log. `providerOptions.xai.withTimestamps`
|
|
1570
|
+
* returns per-character timing in `providerMetadata.qgrid.audioTimestamps` (`graphChars`,
|
|
1571
|
+
* `graphTimes`), the shape `@ai-sdk/xai` puts under `providerMetadata.xai`.
|
|
1540
1572
|
*/
|
|
1541
1573
|
function createQgridSpeech(modelId, config) {
|
|
1542
1574
|
const provider = voiceProviderOf(modelId, LABEL$2);
|
|
@@ -1550,7 +1582,7 @@ function createQgridSpeech(modelId, config) {
|
|
|
1550
1582
|
if (!options.text.trim()) throw new Error(`${LABEL$2}: text must not be empty`);
|
|
1551
1583
|
if (options.text.length > MAX_TEXT_CHARS) throw new Error(`${LABEL$2}: text is limited to ${MAX_TEXT_CHARS} characters`);
|
|
1552
1584
|
const qgridOptions = parseGrokQgridOptions(options.providerOptions?.qgrid, LABEL$2);
|
|
1553
|
-
const xai = parseXaiMediaOptions(options.providerOptions?.xai, xaiSpeechOptionsSchema,
|
|
1585
|
+
const xai = parseXaiMediaOptions(options.providerOptions?.xai, xaiSpeechOptionsSchema, XAI_SPEECH_HONORED, LABEL$2);
|
|
1554
1586
|
const warnings = [];
|
|
1555
1587
|
let format;
|
|
1556
1588
|
if (options.outputFormat !== void 0) {
|
|
@@ -1580,6 +1612,7 @@ function createQgridSpeech(modelId, config) {
|
|
|
1580
1612
|
if (options.voice !== void 0) requestParams.voice = options.voice;
|
|
1581
1613
|
if (options.language !== void 0) requestParams.language = options.language;
|
|
1582
1614
|
if (format !== void 0) requestParams.format = format;
|
|
1615
|
+
if (xai.options?.withTimestamps === true || qgridWantsTimestamps(options.providerOptions?.qgrid)) requestParams.withTimestamps = true;
|
|
1583
1616
|
if (projectName) requestParams.projectName = projectName;
|
|
1584
1617
|
const params = {
|
|
1585
1618
|
...requestParams,
|
|
@@ -1601,6 +1634,7 @@ function createQgridSpeech(modelId, config) {
|
|
|
1601
1634
|
tokenName: data.tokenName
|
|
1602
1635
|
};
|
|
1603
1636
|
if (data.requestLogId !== void 0) qgridMetadata.requestLogId = data.requestLogId;
|
|
1637
|
+
speechTimingMetadata(data, qgridMetadata, warnings);
|
|
1604
1638
|
return {
|
|
1605
1639
|
audio: base64ToBytes(data.audio),
|
|
1606
1640
|
warnings,
|
|
@@ -1621,6 +1655,8 @@ function createQgridSpeech(modelId, config) {
|
|
|
1621
1655
|
* (default `marin`, Codex v2 list), `outputFormat` `wav` (default) or `pcm` — other formats
|
|
1622
1656
|
* warn and fall back to wav, as the official provider falls back to mp3 — `openai.instructions`
|
|
1623
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"`).
|
|
1624
1660
|
*/
|
|
1625
1661
|
function createOpenAISpeech(modelId, serverUrl, projectName) {
|
|
1626
1662
|
return {
|
|
@@ -1663,6 +1699,7 @@ function createOpenAISpeech(modelId, serverUrl, projectName) {
|
|
|
1663
1699
|
if (format !== void 0) requestParams.format = format;
|
|
1664
1700
|
const instructions = options.instructions ?? openai.options?.instructions ?? void 0;
|
|
1665
1701
|
if (instructions !== void 0) requestParams.instructions = instructions;
|
|
1702
|
+
if (qgridWantsTimestamps(options.providerOptions?.qgrid)) requestParams.withTimestamps = true;
|
|
1666
1703
|
if (projectName) requestParams.projectName = projectName;
|
|
1667
1704
|
const params = {
|
|
1668
1705
|
...requestParams,
|
|
@@ -1684,6 +1721,7 @@ function createOpenAISpeech(modelId, serverUrl, projectName) {
|
|
|
1684
1721
|
tokenName: data.tokenName
|
|
1685
1722
|
};
|
|
1686
1723
|
if (data.requestLogId !== void 0) qgridMetadata.requestLogId = data.requestLogId;
|
|
1724
|
+
speechTimingMetadata(data, qgridMetadata, warnings);
|
|
1687
1725
|
return {
|
|
1688
1726
|
audio: base64ToBytes(data.audio),
|
|
1689
1727
|
warnings,
|
|
@@ -1772,7 +1810,12 @@ const transcribeOutputSchema = z.object({
|
|
|
1772
1810
|
durationSec: z.number().optional(),
|
|
1773
1811
|
model: z.string(),
|
|
1774
1812
|
tokenName: z.string(),
|
|
1775
|
-
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()
|
|
1776
1819
|
});
|
|
1777
1820
|
function normalizeMediaType(mediaType) {
|
|
1778
1821
|
const bare = mediaType.split(";")[0].trim().toLowerCase();
|
|
@@ -1828,7 +1871,11 @@ function createQgridTranscription(modelId, config) {
|
|
|
1828
1871
|
if (data.requestLogId !== void 0) qgridMetadata.requestLogId = data.requestLogId;
|
|
1829
1872
|
return {
|
|
1830
1873
|
text: data.text,
|
|
1831
|
-
segments: []
|
|
1874
|
+
segments: (data.words ?? []).map((word) => ({
|
|
1875
|
+
text: word.text,
|
|
1876
|
+
startSecond: word.start,
|
|
1877
|
+
endSecond: word.end
|
|
1878
|
+
})),
|
|
1832
1879
|
language: data.language,
|
|
1833
1880
|
durationInSeconds: data.durationSec,
|
|
1834
1881
|
warnings,
|