@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 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`, `withTimestamps`, ...) |
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에는 segment가 없습니다(`segments: []`).
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`, `withTimestamps`, ...) |
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/*`. Transcripts carry no segments (`segments: []`).
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` (the only combination verified live). The model id only labels the log.
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`. The qgrid server takes none of them; every set key warns.
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` (the only combination verified live). The model id only labels the log.
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, /* @__PURE__ */ new Set(), LABEL$2);
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,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cartanova/qgrid-ai-sdk",
3
- "version": "2.15.5",
3
+ "version": "2.17.0",
4
4
  "description": "AI SDK LanguageModelV3 and ImageModelV3 provider for qgrid",
5
5
  "repository": {
6
6
  "type": "git",