@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 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에는 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 전용)를 주면 글자별 타이밍이 `providerMetadata.qgrid.audioTimestamps`에 담깁니다. `graphChars`는 입력 글자(공백·문장부호·음성 태그 포함), `graphTimes`는 같은 인덱스의 `[start, end]` 초이고, 전체 길이는 `providerMetadata.qgrid.duration`입니다. `@ai-sdk/xai`가 `providerMetadata.xai`에 담는 모양과 같습니다. 텍스트 정규화로 풀린 토큰은 첫 글자가 전체 구간을 가집니다. `qgrid.speech("openai/...")`는 타이밍 데이터가 없어 서버가 `withTimestamps`를 HTTP 400으로 거부합니다.
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/*`. 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
- - `providerOptions: { xai: { withTimestamps: true } }` (2.16.0+, Grok only) 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`. Text normalization gives an expanded token's whole span to its first character. `qgrid.speech("openai/...")` has no timing data and the server rejects `withTimestamps` with HTTP 400.
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
- if (data.audioTimestamps) qgridMetadata.audioTimestamps = data.audioTimestamps;
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,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cartanova/qgrid-ai-sdk",
3
- "version": "2.16.0",
3
+ "version": "2.17.1",
4
4
  "description": "AI SDK LanguageModelV3 and ImageModelV3 provider for qgrid",
5
5
  "repository": {
6
6
  "type": "git",