@cartanova/qgrid-ai-sdk 2.17.1 → 2.19.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 +10 -10
- package/README.md +11 -11
- package/dist/index.d.ts +2 -2
- package/dist/index.js +9 -12
- package/package.json +1 -1
package/README.ko.md
CHANGED
|
@@ -348,7 +348,7 @@ qgrid는 `grok/*` 모델을 qgrid 대시보드에 등록한 xAI 구독 토큰(
|
|
|
348
348
|
| `qgrid.image(id)` | `grok/grok-imagine-image`, `grok/grok-imagine-image-2.0` | `generateImage` |
|
|
349
349
|
| `qgrid.video(id)` | `grok/grok-imagine-video`, `grok/grok-imagine-video-1.5`, `grok/grok-imagine-video-1.5-lite` | `experimental_generateVideo` |
|
|
350
350
|
| `qgrid.transcription(id)` | `grok/grok-voice-transcribe-2.0`; OpenAI 구독: `openai/gpt-4o-mini-transcribe`, `openai/gpt-4o-transcribe`, `openai/gpt-transcribe`, `openai/gpt-live-transcribe`, `openai/gpt-realtime-whisper`, `openai/whisper-1` | `transcribe` |
|
|
351
|
-
| `qgrid.speech(id)` | `grok/grok-tts`(ID는 request log 라벨로만 쓰임); OpenAI 구독: `openai/gpt-realtime-2.1-mini`, `openai/gpt-realtime-2.1
|
|
351
|
+
| `qgrid.speech(id)` | `grok/grok-tts`(ID는 request log 라벨로만 쓰임); OpenAI 구독: `openai/gpt-realtime-2.1-mini`, `openai/gpt-realtime-2.1` | `generateSpeech` |
|
|
352
352
|
|
|
353
353
|
qgrid는 카탈로그에 없는 language model ID를 거부하지 않고 그대로 전달합니다(OpenAI와 같음). 판단은
|
|
354
354
|
subscription backend가 하며 다른 모델로 응답할 수 있으므로, 실제로 실행된 모델은 request log의 응답 모델(또는
|
|
@@ -394,11 +394,11 @@ 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`, `xai.withTimestamps` | `outputFormat`은 `format`(`mp3`, `wav`, `pcm`, `mulaw`, `alaw`, 그 외 값은 `@ai-sdk/xai`처럼 warning과 함께 `mp3`) | `instructions`,
|
|
397
|
+
| Speech | `voice`, `language`, `speed`(0.7-1.5, 2.18.0+), `xai.withTimestamps` | `outputFormat`은 `format`(`mp3`, `wav`, `pcm`, `mulaw`, `alaw`, 그 외 값은 `@ai-sdk/xai`처럼 warning과 함께 `mp3`) | `instructions`, 그 밖의 모든 `xai` 키(`sampleRate`, `bitRate`, `replace`, ...) |
|
|
398
398
|
|
|
399
|
-
**요청 크기.** qgrid 서버는 최대
|
|
399
|
+
**요청 크기.** qgrid 서버는 최대 26 MiB JSON body를 받습니다. inline 미디어는 base64로 전송되어 3분의 1만큼
|
|
400
400
|
커지므로, 서버는 inline 오디오, 각 inline 이미지, 한 요청의 inline 이미지 합계(비디오: `image` + `lastFrame` +
|
|
401
|
-
참조 이미지)를 디코딩 기준
|
|
401
|
+
참조 이미지)를 디코딩 기준 19 MiB로 제한합니다(2.19.0+, 이전 서버와 SDK는 10 MiB와 7 MiB). SDK는 더 큰 입력을 전송 전에 거부하고, 서버 413도 같은 크기
|
|
402
402
|
오류로 보여줍니다. 입력을 압축하거나, 옵션이 허용하면 https 이미지 URL을 넘기세요.
|
|
403
403
|
|
|
404
404
|
### Grok 이미지
|
|
@@ -485,9 +485,9 @@ const speech = await generateSpeech({
|
|
|
485
485
|
speech.audio.uint8Array;
|
|
486
486
|
```
|
|
487
487
|
|
|
488
|
-
- `transcribe`는 bytes로 오디오 형식을 감지하고 실패하면 `audio/wav`를 씁니다. 서버는 `audio/*`가 아닌 값을 거부합니다.
|
|
489
|
-
- 음성 합성 텍스트는 최대 15,000자입니다. `voice`, `language`, `outputFormat`을 지정하지 않으면 서버 기본값 `ara`, `en`, `mp3`가
|
|
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는 서버가 생성된
|
|
488
|
+
- `transcribe`는 bytes로 오디오 형식을 감지하고 실패하면 `audio/wav`를 씁니다. 서버는 `audio/*`가 아닌 값을 거부합니다. transcript는 들린 단어마다 segment 하나를 담습니다(`segments: [{ text, startSecond, endSecond }]`). Grok은 직접 시간을 재고(2.17.0+), OpenAI는 서버가 내장 정렬기로 transcript를 오디오에 맞춥니다(서버 2.19.0+). 정렬에는 클립 길이의 약 5%만큼 시간이 더 듭니다. 서버에 정렬기가 없거나 클립이 정렬 한도(빽빽한 음성 기준 약 4분)를 넘으면 OpenAI segment는 비어 있습니다.
|
|
489
|
+
- 음성 합성 텍스트는 최대 15,000자입니다. `voice`, `language`, `outputFormat`을 지정하지 않으면 서버 기본값 `ara`, `en`, `mp3`가 쓰입니다. 구독 토큰에 보이는 보이스 28종(여성 9, 남성 19. 예: `ara`, `eve`, `rex`)은 2026-11-09에 모두 영어·mp3로 live 생성했습니다. 다른 language(`@ai-sdk/xai`의 `auto` 포함)와 codec은 그대로 전달되지만 미검증입니다. xAI 목록의 음성 태그(`[pause]`, `[laugh]`, `<whisper>...</whisper>` 등)는 2026-11 확인에서 글자로 읽히지 않았고, `[deadpan]` 같은 ElevenLabs식 감정 태그는 그 목록에 없습니다. `speed`는 말하기 속도입니다(1이 보통, 0.7-1.5 밖의 값은 서버가 거부). 서버가 돌려준 오디오 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는 서버가 내장 정렬기로 생성된 오디오에 원문을 맞춰 만듭니다(서버 2.19.0+, 이전 서버는 Grok STT를 거쳤음). 정렬기는 qgrid Docker 이미지에 들어 있고 npm이나 pnpm 설치에서는 `qgrid --install-aligner`로 설치합니다. 정렬기가 없는 서버와 정렬기 한도(빽빽한 음성 기준 약 4분)를 넘는 긴 음성은 `timestampsError`를 돌려줍니다(오류에 그 대본의 한도가 나옴). 타이밍 단계가 실패해도 오디오는 돌려주고, 메타데이터에 `timestampsError`와 AI SDK warning을 남깁니다. 텍스트 정규화로 풀린 토큰은 첫 글자가 전체 구간을 가집니다. Grok mp3에는 앞부분 무음 정보(gapless 태그)가 없어 재생기가 인코더 지연을 그대로 재생하므로, 서버 2.19.0부터 qgrid가 Grok 타이밍에 그 46ms를 더해 실제 재생과 맞춥니다(`@ai-sdk/xai`의 원래 값보다 46ms 늦음).
|
|
491
491
|
|
|
492
492
|
### OpenAI 음성 인식과 합성 (ChatGPT 구독)
|
|
493
493
|
|
|
@@ -511,12 +511,12 @@ const speech = await generateSpeech({
|
|
|
511
511
|
|
|
512
512
|
| 모델 | 그대로 전달(`@ai-sdk/openai` 이름) | 변환 | 미지원(경고) |
|
|
513
513
|
| --- | --- | --- | --- |
|
|
514
|
-
| Transcription | `openai.language`, `openai.prompt`(`gpt-realtime-whisper`는 HTTP 400으로 거부, `whisper-1`은 받지만 업스트림이 무시) | 입력은 PCM16 WAV(`audio/wav`) 또는 raw PCM(`audio/pcm;rate=…;channels=…`)만 가능하며 mp3/webm/ogg는 디코더가 추가되기 전까지 서버가 HTTP 400으로 거부합니다. `durationInSeconds`는 보낸 오디오에서 계산하고 `language`는 요청값을 그대로 돌려줍니다(realtime 세션은 둘 다 보고하지 않음). `segments
|
|
514
|
+
| Transcription | `openai.language`, `openai.prompt`(`gpt-realtime-whisper`는 HTTP 400으로 거부, `whisper-1`은 받지만 업스트림이 무시) | 입력은 PCM16 WAV(`audio/wav`) 또는 raw PCM(`audio/pcm;rate=…;channels=…`)만 가능하며 mp3/webm/ogg는 디코더가 추가되기 전까지 서버가 HTTP 400으로 거부합니다. `durationInSeconds`는 보낸 오디오에서 계산하고 `language`는 요청값을 그대로 돌려줍니다(realtime 세션은 둘 다 보고하지 않음). `segments`에는 정렬한 단어 타이밍이 담깁니다(서버 2.19.0+) | `openai.temperature`, `openai.include`, `openai.timestampGranularities`, `openai.responseFormat`, `openai.chunkingStrategy`, `openai.streaming` |
|
|
515
515
|
| Speech | `voice`(기본 `marin`; Codex v2 목록 `alloy ash ballad coral echo sage shimmer verse marin cedar`, 그 외는 HTTP 400), `instructions` / `openai.instructions`(원문 그대로 읽기 지시문 뒤에 붙는 스타일 안내) | `outputFormat`은 `wav`(기본, PCM16 24 kHz) 또는 `pcm`; 그 외 값은 `@ai-sdk/openai`가 `mp3`로 되돌리듯 경고와 함께 `wav`로 바뀝니다 | `language`, `speed`, `openai.speed` |
|
|
516
516
|
|
|
517
|
-
- 음성 인식은 먼저 Codex realtime call에서 25초 예산으로 실행됩니다(call은 생성 약 30초 뒤 정리됨). 그 안에 전사가 오지 않으면 서버가 직접 realtime 전사 소켓으로 1회 재시도합니다(최대 60초).
|
|
517
|
+
- 음성 인식은 먼저 Codex realtime call에서 25초 예산으로 실행됩니다(call은 생성 약 30초 뒤 정리됨). 그 안에 전사가 오지 않으면 서버가 직접 realtime 전사 소켓으로 1회 재시도합니다(최대 60초). 19 MiB 인라인 제한에는 24 kHz mono WAV가 약 6.9분 들어갑니다. 6.7분 영어 클립은 첫 경로에서 약 20초에 끝났고, transcript가 더 길게 나온 6.4분 클립은 기한을 넘겨 재시도 경로에서 52초에 끝났습니다.
|
|
518
518
|
- 음성 합성은 TTS 모델이 아니라 텍스트를 읽는 대화 모델입니다. qgrid는 `<script>` 태그 안의 텍스트만 읽고 답하지 말라고 지시합니다. 원문 그대로 읽는지는 짧은 영어 문장과 질문형 영어 캐릭터 대사에서 검증했습니다. 모델이 실제로 말한 transcript를 요청 로그에 함께 남겨 비교할 수 있지만, 한영 혼합 텍스트에서는 transcript가 일치해도 영어 음성을 알아듣기 어려운 경우가 있었습니다.
|
|
519
|
-
- 모델별 동작은 서버 저장소의 `docs/diagnosis/2026-10-17-openai-realtime-audio-probe.md`에 기록되어 있습니다. 2026-10-17 기준 알려진 한계: `gpt-live-transcribe`, `gpt-realtime-whisper`는 60초 클립의 절반 정도만 반환하므로 짧은 발화에 쓰세요. `gpt-realtime-whisper`는 `openai.prompt`를 거부합니다(서버가 업스트림 호출 전에 HTTP 400). `whisper-1`은 `openai.prompt`를 받지만 무시합니다. 음성 합성은 `gpt-realtime-2.1-mini`가 모든 테스트 문장을 그대로 읽었고(13/13), `gpt-realtime-2.1`은 1회 의역(12/13)
|
|
519
|
+
- 모델별 동작은 서버 저장소의 `docs/diagnosis/2026-10-17-openai-realtime-audio-probe.md`에 기록되어 있습니다. 2026-10-17 기준 알려진 한계: `gpt-live-transcribe`, `gpt-realtime-whisper`는 60초 클립의 절반 정도만 반환하므로 짧은 발화에 쓰세요. `gpt-realtime-whisper`는 `openai.prompt`를 거부합니다(서버가 업스트림 호출 전에 HTTP 400). `whisper-1`은 `openai.prompt`를 받지만 무시합니다. 음성 합성은 `gpt-realtime-2.1-mini`가 모든 테스트 문장을 그대로 읽었고(13/13), `gpt-realtime-2.1`은 1회 의역(12/13)했습니다. `gpt-realtime-1.5`는 텍스트를 읽지 않고 답변하는 경우가 잦아(6/13) 2.18.0에서 제외했습니다. 2026-11-06에는 현재 프롬프트로 `gpt-realtime-2.1`과 `gpt-realtime-2.1-mini` 모두 질문형 캐릭터 대사 12/12를 그대로 읽었습니다(이전 프롬프트에서는 `gpt-realtime-2.1`이 7/12를 답변). 2026-11-09에 독립 STT 2종으로 영어 108건(6문장, 보이스 `marin` `cedar` `ballad`, 각 3회)을 판정한 결과 `gpt-realtime-2.1`은 54/54, `gpt-realtime-2.1-mini`는 53/54였고, 실패 1건은 "Sure, let me read that out loud for you exactly as written."으로 시작했습니다. 한영 혼합 문장은 두 모델 모두 끝까지 읽었습니다(각 9/9). 모델이 남기는 transcript에는 이런 서두가 나타나지 않으므로, 정확성이 중요하면 transcript 대신 STT나 `withTimestamps`(정렬이 덧붙은 말을 건너뜀)로 오디오를 확인하세요.
|
|
520
520
|
|
|
521
521
|
## Telemetry Logger
|
|
522
522
|
|
package/README.md
CHANGED
|
@@ -358,7 +358,7 @@ Grok model accepts the same factory config as the other models: `serverUrl` (def
|
|
|
358
358
|
| `qgrid.image(id)` | `grok/grok-imagine-image`, `grok/grok-imagine-image-2.0` | `generateImage` |
|
|
359
359
|
| `qgrid.video(id)` | `grok/grok-imagine-video`, `grok/grok-imagine-video-1.5`, `grok/grok-imagine-video-1.5-lite` | `experimental_generateVideo` |
|
|
360
360
|
| `qgrid.transcription(id)` | `grok/grok-voice-transcribe-2.0`; OpenAI subscription: `openai/gpt-4o-mini-transcribe`, `openai/gpt-4o-transcribe`, `openai/gpt-transcribe`, `openai/gpt-live-transcribe`, `openai/gpt-realtime-whisper`, `openai/whisper-1` | `transcribe` |
|
|
361
|
-
| `qgrid.speech(id)` | `grok/grok-tts` (the id only labels the request log); OpenAI subscription: `openai/gpt-realtime-2.1-mini`, `openai/gpt-realtime-2.1
|
|
361
|
+
| `qgrid.speech(id)` | `grok/grok-tts` (the id only labels the request log); OpenAI subscription: `openai/gpt-realtime-2.1-mini`, `openai/gpt-realtime-2.1` | `generateSpeech` |
|
|
362
362
|
|
|
363
363
|
qgrid forwards a language-model id that is not in its catalog unchanged instead of rejecting it,
|
|
364
364
|
as it does for OpenAI; the subscription backend decides. It may serve a different model, so check
|
|
@@ -404,12 +404,12 @@ 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`, `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`,
|
|
407
|
+
| Speech | `voice`, `language`, `speed` (0.7-1.5, 2.18.0+), `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`, every other `xai` key (`sampleRate`, `bitRate`, `replace`, ...) |
|
|
408
408
|
|
|
409
|
-
**Request size.** The qgrid server accepts JSON bodies up to
|
|
409
|
+
**Request size.** The qgrid server accepts JSON bodies up to 26 MiB. Inline media travels as
|
|
410
410
|
base64, which grows by a third, so the server caps inline audio, each inline image, and all
|
|
411
|
-
inline images of one request together (video: `image` + `lastFrame` + references) at
|
|
412
|
-
decoded. The SDK rejects larger input before transport; a server 413 surfaces as the same size
|
|
411
|
+
inline images of one request together (video: `image` + `lastFrame` + references) at 19 MiB
|
|
412
|
+
decoded (2.19.0+; earlier servers and SDKs use 10 MiB and 7 MiB). The SDK rejects larger input before transport; a server 413 surfaces as the same size
|
|
413
413
|
error. Compress the input, or pass an https image URL where the option accepts one.
|
|
414
414
|
|
|
415
415
|
### Grok images
|
|
@@ -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/*`.
|
|
501
|
-
- Speech text is limited to 15,000 characters. Unset `voice`, `language` and `outputFormat` use the server defaults `ara`, `en` and `mp3
|
|
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
|
|
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 one segment per heard word (`segments: [{ text, startSecond, endSecond }]`): Grok times them itself (2.17.0+); for OpenAI the server force-aligns the transcript to the audio with its built-in aligner (server 2.19.0+), adding about 5% of the clip length to the request. OpenAI segments are empty when the server lacks the aligner or the clip is too long to align (about 4 minutes of dense speech).
|
|
501
|
+
- Speech text is limited to 15,000 characters. Unset `voice`, `language` and `outputFormat` use the server defaults `ara`, `en` and `mp3`. All 28 voices the subscription token lists (9 female, 19 male, such as `ara`, `eve`, `rex`) were generated live in English as mp3 on 2026-11-09; other languages (including `@ai-sdk/xai`'s `auto`) and codecs are passed through but unverified. Speech tags from xAI's list (`[pause]`, `[laugh]`, `<whisper>...</whisper>`, ...) were not read aloud in a 2026-11 check; ElevenLabs-style emotion tags such as `[deadpan]` are not in that list. `speed` is the speaking rate (1 is normal; the server rejects values outside 0.7-1.5). 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 force-aligns to the text with its built-in aligner (server 2.19.0+; earlier servers used a Grok STT pass). The aligner ships in the qgrid Docker image and `qgrid --install-aligner` installs it on an npm or pnpm install; a server without it returns `timestampsError`, as does speech longer than the aligner's limit (about 4 minutes of dense speech; the error states the limit for that script). 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. Grok's mp3 carries no gapless tag, so players keep the encoder delay; since server 2.19.0 qgrid adds those 46 ms to Grok timing so it matches what plays (raw `@ai-sdk/xai` values are 46 ms earlier).
|
|
503
503
|
|
|
504
504
|
### OpenAI transcription and speech (ChatGPT subscription)
|
|
505
505
|
|
|
@@ -523,12 +523,12 @@ const speech = await generateSpeech({
|
|
|
523
523
|
|
|
524
524
|
| Model | Faithful (`@ai-sdk/openai` names) | Transformed | Unsupported (warning) |
|
|
525
525
|
| --- | --- | --- | --- |
|
|
526
|
-
| Transcription | `openai.language`, `openai.prompt` (not on `gpt-realtime-whisper`, which rejects it with HTTP 400; accepted but ignored upstream on `whisper-1`) | input must be PCM16 WAV (`audio/wav`) or raw PCM (`audio/pcm;rate=…;channels=…`); mp3/webm/ogg are rejected by the server with HTTP 400 until a decoder lands. `durationInSeconds` is computed from the audio sent; `language` echoes the request (the realtime session reports neither); `segments`
|
|
526
|
+
| Transcription | `openai.language`, `openai.prompt` (not on `gpt-realtime-whisper`, which rejects it with HTTP 400; accepted but ignored upstream on `whisper-1`) | input must be PCM16 WAV (`audio/wav`) or raw PCM (`audio/pcm;rate=…;channels=…`); mp3/webm/ogg are rejected by the server with HTTP 400 until a decoder lands. `durationInSeconds` is computed from the audio sent; `language` echoes the request (the realtime session reports neither); `segments` carries aligned word timing (server 2.19.0+) | `openai.temperature`, `openai.include`, `openai.timestampGranularities`, `openai.responseFormat`, `openai.chunkingStrategy`, `openai.streaming` |
|
|
527
527
|
| Speech | `voice` (default `marin`; Codex v2 list `alloy ash ballad coral echo sage shimmer verse marin cedar`, others fail with HTTP 400), `instructions` / `openai.instructions` (style guidance appended to the verbatim-repeat prompt) | `outputFormat` `wav` (default, PCM16 24 kHz) or `pcm`; anything else falls back to `wav` with a warning, as `@ai-sdk/openai` falls back to `mp3` | `language`, `speed`, `openai.speed` |
|
|
528
528
|
|
|
529
|
-
- Each transcription first runs on the Codex realtime call with a 25 s budget (the call is torn down about 30 s after creation); if that window closes before the transcript arrives, the server retries once on the direct realtime transcription socket (up to 60 s). The
|
|
529
|
+
- Each transcription first runs on the Codex realtime call with a 25 s budget (the call is torn down about 30 s after creation); if that window closes before the transcript arrives, the server retries once on the direct realtime transcription socket (up to 60 s). The 19 MiB inline limit holds about 6.9 min of 24 kHz mono WAV. A 6.7 min English clip finished on the first path in about 20 s; a 6.4 min clip that produced a longer transcript missed the window and finished on the retry after 52 s.
|
|
530
530
|
- Speech is a conversational model reading text, not a TTS model. qgrid's prompt tells it to read only the text inside `<script>` tags and never answer it. Verbatim fidelity was verified on short English sentences and on question-form English character lines. The model's own transcript of what it said is logged with the request for comparison, but for mixed Korean/English text a matching transcript did not guarantee intelligible English audio.
|
|
531
|
-
- Per-model behavior is recorded in the server repository's `docs/diagnosis/2026-10-17-openai-realtime-audio-probe.md`. Known limits on 2026-10-17: `gpt-live-transcribe` and `gpt-realtime-whisper` returned roughly half of a 60 s clip (use them for short utterances); `gpt-realtime-whisper` rejects `openai.prompt` (the server answers HTTP 400 before any upstream call); `whisper-1` accepts but ignores `openai.prompt`. For speech, `gpt-realtime-2.1-mini` repeated every test text verbatim (13/13), `gpt-realtime-2.1` paraphrased once (12/13)
|
|
531
|
+
- Per-model behavior is recorded in the server repository's `docs/diagnosis/2026-10-17-openai-realtime-audio-probe.md`. Known limits on 2026-10-17: `gpt-live-transcribe` and `gpt-realtime-whisper` returned roughly half of a 60 s clip (use them for short utterances); `gpt-realtime-whisper` rejects `openai.prompt` (the server answers HTTP 400 before any upstream call); `whisper-1` accepts but ignores `openai.prompt`. For speech, `gpt-realtime-2.1-mini` repeated every test text verbatim (13/13), `gpt-realtime-2.1` paraphrased once (12/13); `gpt-realtime-1.5` often answered the text instead of repeating it (6/13) and was removed in 2.18.0. On 2026-11-06, with the current prompt, `gpt-realtime-2.1` and `gpt-realtime-2.1-mini` both read 12/12 question-form character lines verbatim (the earlier prompt let `gpt-realtime-2.1` answer 7/12). On 2026-11-09, judged by two independent STT models over 108 English clips (6 lines, voices `marin` `cedar` `ballad`, 3 takes each), `gpt-realtime-2.1` read 54/54 and `gpt-realtime-2.1-mini` 53/54: one take began with "Sure, let me read that out loud for you exactly as written." Both read every Korean/English mixed line in full (9/9 each). The model's own logged transcript does not show such a preamble, so when fidelity matters, check the audio with STT or `withTimestamps` (its alignment skips the extra words) instead of the transcript.
|
|
532
532
|
|
|
533
533
|
## Telemetry Logger
|
|
534
534
|
|
package/dist/index.d.ts
CHANGED
|
@@ -58,9 +58,9 @@ type QgridGrokSpeechModel = "grok/grok-tts";
|
|
|
58
58
|
/**
|
|
59
59
|
* OpenAI subscription text-to-speech ids: realtime conversational models repeating the text
|
|
60
60
|
* (`gpt-4o-mini-tts`/`tts-1` are unavailable on subscription). Verbatim fidelity on 2026-10-17:
|
|
61
|
-
* `gpt-realtime-2.1-mini` 13/13, `gpt-realtime-2.1` 12/13
|
|
61
|
+
* `gpt-realtime-2.1-mini` 13/13, `gpt-realtime-2.1` 12/13 (`gpt-realtime-1.5`, 6/13, removed in 2.18.0).
|
|
62
62
|
*/
|
|
63
|
-
type QgridOpenAISpeechModel = "openai/gpt-realtime-2.1-mini" | "openai/gpt-realtime-2.1"
|
|
63
|
+
type QgridOpenAISpeechModel = "openai/gpt-realtime-2.1-mini" | "openai/gpt-realtime-2.1";
|
|
64
64
|
type QgridSpeechModel = QgridGrokSpeechModel | QgridOpenAISpeechModel;
|
|
65
65
|
type QgridImageProviderConfig = QgridCommonProviderConfig;
|
|
66
66
|
/** `providerOptions.qgrid` for `generateImage({ model: qgrid.image(...) })`. */
|
package/dist/index.js
CHANGED
|
@@ -989,7 +989,7 @@ function imageInputUrl(file) {
|
|
|
989
989
|
return `data:${file.mediaType};base64,${data}`;
|
|
990
990
|
}
|
|
991
991
|
/** Server-side Fastify body limit (`sonamu.config.ts` `bodyLimit`). Larger bodies get a 413. */
|
|
992
|
-
const QGRID_MAX_REQUEST_BODY_BYTES =
|
|
992
|
+
const QGRID_MAX_REQUEST_BODY_BYTES = 27262976;
|
|
993
993
|
function resolveMediaConfig(config) {
|
|
994
994
|
return {
|
|
995
995
|
serverUrl: config?.serverUrl ?? process.env.QGRID_URL ?? "http://localhost:44900",
|
|
@@ -1026,10 +1026,10 @@ function formatMiB(bytes) {
|
|
|
1026
1026
|
}
|
|
1027
1027
|
/**
|
|
1028
1028
|
* Server `MEDIA_MAX_INLINE_BYTES` (qgrid.types.ts): decoded inline audio, each inline image, and all
|
|
1029
|
-
* inline images of one request together. Derived from the
|
|
1030
|
-
* 4/3, so
|
|
1029
|
+
* inline images of one request together. Derived from the 26 MiB body limit: base64 grows bytes by
|
|
1030
|
+
* 4/3, so 19 MiB decoded is about 25.3 MiB encoded, leaving room for the rest of the JSON body.
|
|
1031
1031
|
*/
|
|
1032
|
-
const QGRID_MAX_INLINE_MEDIA_BYTES =
|
|
1032
|
+
const QGRID_MAX_INLINE_MEDIA_BYTES = 19922944;
|
|
1033
1033
|
function base64DecodedBytes(data) {
|
|
1034
1034
|
const padding = data.endsWith("==") ? 2 : data.endsWith("=") ? 1 : 0;
|
|
1035
1035
|
return Math.floor(data.length * 3 / 4) - padding;
|
|
@@ -1045,7 +1045,7 @@ function assertInlineMediaFits(items, label) {
|
|
|
1045
1045
|
const comma = item.startsWith("data:") ? item.indexOf(",") : -1;
|
|
1046
1046
|
total += base64DecodedBytes(comma >= 0 ? item.slice(comma + 1) : item);
|
|
1047
1047
|
}
|
|
1048
|
-
if (total >
|
|
1048
|
+
if (total > 19922944) throw new Error(`${label}: inline media is ${formatMiB(total)}, above the qgrid server limit of ${formatMiB(QGRID_MAX_INLINE_MEDIA_BYTES)} per request; compress it or pass an https URL where supported.`);
|
|
1049
1049
|
}
|
|
1050
1050
|
/**
|
|
1051
1051
|
* Serializes a Sonamu `params` body and rejects it before transport when it would exceed the
|
|
@@ -1054,7 +1054,7 @@ function assertInlineMediaFits(items, label) {
|
|
|
1054
1054
|
function serializeMediaBody(params, label) {
|
|
1055
1055
|
const body = JSON.stringify({ params });
|
|
1056
1056
|
const bytes = new TextEncoder().encode(body).byteLength;
|
|
1057
|
-
if (bytes >
|
|
1057
|
+
if (bytes > 27262976) throw new Error(`${label}: request body is ${formatMiB(bytes)}, above the qgrid server limit of ${formatMiB(QGRID_MAX_REQUEST_BODY_BYTES)}; compress inline media or pass an https URL where supported.`);
|
|
1058
1058
|
return body;
|
|
1059
1059
|
}
|
|
1060
1060
|
const ERROR_BODY_PREVIEW_CHARS = 2e3;
|
|
@@ -1600,16 +1600,13 @@ function createQgridSpeech(modelId, config) {
|
|
|
1600
1600
|
type: "unsupported",
|
|
1601
1601
|
feature: "instructions"
|
|
1602
1602
|
});
|
|
1603
|
-
if (options.speed !== void 0) warnings.push({
|
|
1604
|
-
type: "unsupported",
|
|
1605
|
-
feature: "speed"
|
|
1606
|
-
});
|
|
1607
1603
|
warnings.push(...xai.warnings);
|
|
1608
1604
|
const requestParams = {
|
|
1609
1605
|
model: modelId,
|
|
1610
1606
|
text: options.text
|
|
1611
1607
|
};
|
|
1612
1608
|
if (options.voice !== void 0) requestParams.voice = options.voice;
|
|
1609
|
+
if (options.speed !== void 0) requestParams.speed = options.speed;
|
|
1613
1610
|
if (options.language !== void 0) requestParams.language = options.language;
|
|
1614
1611
|
if (format !== void 0) requestParams.format = format;
|
|
1615
1612
|
if (xai.options?.withTimestamps === true || qgridWantsTimestamps(options.providerOptions?.qgrid)) requestParams.withTimestamps = true;
|
|
@@ -1655,8 +1652,8 @@ function createQgridSpeech(modelId, config) {
|
|
|
1655
1652
|
* (default `marin`, Codex v2 list), `outputFormat` `wav` (default) or `pcm` — other formats
|
|
1656
1653
|
* warn and fall back to wav, as the official provider falls back to mp3 — `openai.instructions`
|
|
1657
1654
|
* as style guidance, `openai.speed` and `language` unsupported with the official warning text.
|
|
1658
|
-
* `providerOptions.qgrid.withTimestamps` returns timing the server
|
|
1659
|
-
*
|
|
1655
|
+
* `providerOptions.qgrid.withTimestamps` returns timing the server's built-in aligner places in the
|
|
1656
|
+
* generated audio (`providerMetadata.qgrid.timestampsSource: "aligned"`).
|
|
1660
1657
|
*/
|
|
1661
1658
|
function createOpenAISpeech(modelId, serverUrl, projectName) {
|
|
1662
1659
|
return {
|