ima2-gen 3.15.0 → 3.15.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.
Files changed (52) hide show
  1. package/README.md +1 -1
  2. package/bin/commands/video.js +6 -4
  3. package/docs/API.md +1 -1
  4. package/docs/API.zh-CN.md +1 -1
  5. package/docs/API.zh-TW.md +1 -1
  6. package/docs/CLI.md +1 -1
  7. package/docs/CLI.zh-CN.md +1 -1
  8. package/docs/CLI.zh-TW.md +1 -1
  9. package/docs/README.ja.md +1 -1
  10. package/docs/README.ko.md +2 -2
  11. package/docs/README.zh-CN.md +1 -1
  12. package/docs/README.zh-TW.md +1 -1
  13. package/docs/grok-video-i2v-research.md +1 -1
  14. package/docs/grok-video-i2v-research.zh-CN.md +1 -1
  15. package/docs/grok-video-i2v-research.zh-TW.md +1 -1
  16. package/docs/migration/runtime-test-inventory.md +5 -1
  17. package/lib/capabilities.js +33 -7
  18. package/lib/grokVideoAdapter.js +9 -4
  19. package/lib/imageModels.js +81 -8
  20. package/lib/providers/registry.js +11 -4
  21. package/package.json +2 -2
  22. package/routes/video.js +30 -15
  23. package/routes/videoExtended.js +8 -3
  24. package/skills/ima2/SKILL.md +25 -14
  25. package/ui/dist/.vite/manifest.json +61 -61
  26. package/ui/dist/assets/{AgentWorkspace-BulKz33A.js → AgentWorkspace-CLJDCRMQ.js} +1 -1
  27. package/ui/dist/assets/App-DkvU6Sex.js +9 -0
  28. package/ui/dist/assets/{AssetGenWorkspace-8SYhS2Y2.js → AssetGenWorkspace-BepUrz23.js} +2 -2
  29. package/ui/dist/assets/{AssetsWorkspace-Dxz-Xksp.js → AssetsWorkspace-IipC1FdD.js} +1 -1
  30. package/ui/dist/assets/{CardNewsWorkspace-DV4hpvKf.js → CardNewsWorkspace-ZXDwYz4l.js} +1 -1
  31. package/ui/dist/assets/{GenerationRequestLogPanel-VFu4cAdd.js → GenerationRequestLogPanel-BL2C0DcW.js} +1 -1
  32. package/ui/dist/assets/{HomeWorkspace-D1UBgwRZ.js → HomeWorkspace-DFfrwS9a.js} +1 -1
  33. package/ui/dist/assets/{KeyingPanel-GdUcaaJR.js → KeyingPanel-BQkEMTe9.js} +1 -1
  34. package/ui/dist/assets/{NodeCanvas-B6zoJhHr.js → NodeCanvas-5u8IO1MA.js} +1 -1
  35. package/ui/dist/assets/{PromptBuilderPanel-CJ56MEk2.js → PromptBuilderPanel-DNt7LHMi.js} +1 -1
  36. package/ui/dist/assets/{PromptImportDialog-DMFnggIT.js → PromptImportDialog-Bl-SB4mK.js} +2 -2
  37. package/ui/dist/assets/{PromptImportDiscoverySection-4KCIWmZ8.js → PromptImportDiscoverySection-DdZAdRMW.js} +1 -1
  38. package/ui/dist/assets/{PromptImportFolderSection-BjqopFX5.js → PromptImportFolderSection-DLHQKl9T.js} +1 -1
  39. package/ui/dist/assets/{PromptLibraryPanel-DwFK4ALC.js → PromptLibraryPanel-PLWyvEz1.js} +2 -2
  40. package/ui/dist/assets/{SettingsWorkspace-BWcNnQ56.js → SettingsWorkspace-Etfeu2IF.js} +1 -1
  41. package/ui/dist/assets/{SpriteRecipeWorkspace-Bsl_NZ3A.js → SpriteRecipeWorkspace-D7H5seOM.js} +1 -1
  42. package/ui/dist/assets/{index-BWzT2RyM.js → index-BAkbTbUC.js} +3 -3
  43. package/ui/dist/assets/index-D0NhOovz.css +1 -0
  44. package/ui/dist/assets/index-DYtUJ4t0.js +22 -0
  45. package/ui/dist/assets/{pptxgen.es-BslswdiL.js → pptxgen.es-Apxh5DQ9.js} +1 -1
  46. package/ui/dist/assets/{promptBuilderStore-Cl2q0Oe6.js → promptBuilderStore-prvdLIUP.js} +1 -1
  47. package/ui/dist/assets/useAgentDialogFocus-B2snKbzK.js +1 -0
  48. package/ui/dist/index.html +2 -2
  49. package/ui/dist/assets/App-juOjrIgh.js +0 -9
  50. package/ui/dist/assets/index-C5zSMtGM.css +0 -1
  51. package/ui/dist/assets/index-Cl5SnWLL.js +0 -22
  52. package/ui/dist/assets/useAgentDialogFocus-B8niAv4W.js +0 -1
package/README.md CHANGED
@@ -182,7 +182,7 @@ If no provider is specified, the app keeps the current GPT OAuth/default behavio
182
182
 
183
183
  Grok image generation exposes a model picker (`grok-imagine-image` / `grok-imagine-image-quality`) and a size picker (aspect ratio + 1k/2k resolution). The Settings page prefers the Grok Build weekly credits percentage and reset time from `GET /v1/billing?format=credits`; if that source is unavailable, it falls back to the legacy monthly billing window and `$used/$limit`. A **Switch Account** button starts a device-code OAuth flow (`POST /api/auth/switch`) for re-authenticating without leaving the app.
184
184
 
185
- Grok video generation defaults to canonical `grok-imagine-video-1.5`; `grok-imagine-video` remains available for base-model-only Ref2V, V2V edit, and extension paths, and the legacy `grok-imagine-video-1.5-preview` string is accepted as an alias. Three modes are auto-detected from reference count: text-to-video (0 refs), image-to-video (1 ref), and reference-to-video (2-7 refs, max 10s duration). 1080p is available for `grok-imagine-video-1.5` prompt-only text-to-video and single image/frame image-to-video; prompt-only 1.5 uses the internal white-canvas I2V shim before the upstream request. Video controls include duration (1-15s), resolution (480p, 720p, 1080p when supported), and aspect ratio (1:1, 16:9, 9:16, 4:3, 3:4, 3:2, 2:3, auto).
185
+ Grok video generation defaults to canonical `grok-imagine-video-1.5`; `grok-imagine-video` remains available for base-model-only Ref2V, V2V edit, and extension paths, and the legacy `grok-imagine-video-1.5-preview` string is accepted as an alias. Three modes are auto-detected from reference count: text-to-video (0 refs), image-to-video (1 ref), and reference-to-video (2-14 refs; up to 15s on grok-imagine-video-1.5, 10s on grok-imagine-video). 1080p is available for `grok-imagine-video-1.5` prompt-only text-to-video and single image/frame image-to-video; prompt-only 1.5 uses the internal white-canvas I2V shim before the upstream request. Video controls include duration (1-15s), resolution (480p, 720p, 1080p when supported), and aspect ratio (1:1, 16:9, 9:16, 4:3, 3:4, 3:2, 2:3, auto).
186
186
 
187
187
  ![Settings workspace showing GPT OAuth active and API key provider available.](assets/screenshots/settings-oauth-generation.png)
188
188
 
@@ -5,7 +5,7 @@ import { out, die, color, exitCodeForError, fail } from "../lib/output.js";
5
5
  import { writeFile, mkdir, readFile, stat } from "node:fs/promises";
6
6
  import { basename, dirname } from "node:path";
7
7
  import { runVideoGenerate } from "../lib/videoMcp.js";
8
- import { GROK_VIDEO_MODEL_15, GROK_VIDEO_MODEL_15_PREVIEW_ALIAS, GROK_VIDEO_MODEL_BASE, validateVideoResolutionForRequest, } from "../../lib/imageModels.js";
8
+ import { GROK_VIDEO_MODEL_15, GROK_VIDEO_MODEL_15_PREVIEW_ALIAS, GROK_VIDEO_MODEL_BASE, MAX_VIDEO_DURATION, MIN_VIDEO_DURATION, validateVideoResolutionForRequest, } from "../../lib/imageModels.js";
9
9
  import { VIDEO_CLIENT_TIMEOUT_SEC } from "../../lib/videoClientTimeouts.js";
10
10
  import { deriveVideoProviderIds } from "../../lib/providers/derive.js";
11
11
  import { listProviders } from "../../lib/mcp/providerRegistry.js";
@@ -291,7 +291,7 @@ async function videoExtendCmd(argv) {
291
291
  const args = parseArgs(argv, spec);
292
292
  rejectUnknownFlags(args);
293
293
  if (args.help) {
294
- out(` ima2 video extend <prompt> --video <url|file_id|generated-file> [--duration 6]\n\n Extend video from its last frame.\n Model: grok-imagine-video only. Extension: 2-10s.\n\n Options:\n --video <value> Source video HTTPS URL, xAI file_id, data URL, or generated filename (required)\n --duration <2-10> Extension duration (default: 6)\n -o, --out <file> Download extended video to file\n --output <file> Alias for --out\n --json Print JSON result\n --timeout <sec> Default: 5400\n --server <url> Override server URL`);
294
+ out(` ima2 video extend <prompt> --video <url|file_id|generated-file> [--duration 6]\n\n Extend video from its last frame.\n Model: grok-imagine-video only (1.5 rejects extensions). Extension: 1-15s.\n duration is the length of the ADDED segment, not the total output.\n\n Options:\n --video <value> Source video HTTPS URL, xAI file_id, data URL, or generated filename (required)\n --duration <1-15> Added-segment duration (default: 6)\n -o, --out <file> Download extended video to file\n --output <file> Alias for --out\n --json Print JSON result\n --timeout <sec> Default: 5400\n --server <url> Override server URL`);
295
295
  return;
296
296
  }
297
297
  const prompt = args.positional.join(" ");
@@ -300,8 +300,10 @@ async function videoExtendCmd(argv) {
300
300
  if (!args.video)
301
301
  die(2, "--video <url> is required");
302
302
  const duration = parseIntegerFlag(args.duration, 6, "--duration");
303
- if (duration < 2 || duration > 10)
304
- die(2, "--duration must be between 2 and 10");
303
+ // Measured 1-15, not the documented 2-10: the endpoint accepts 1 and 11 and refuses
304
+ // only 0 and 16. devlog/_plan/260908_xai_imagine_spec_resync/000_research.md (D11)
305
+ if (duration < MIN_VIDEO_DURATION || duration > MAX_VIDEO_DURATION)
306
+ die(2, `--duration must be between ${MIN_VIDEO_DURATION} and ${MAX_VIDEO_DURATION}`);
305
307
  parseTimeoutSeconds(args.timeout);
306
308
  const server = await resolveServer({ serverFlag: args.server });
307
309
  const res = await fetchServer(server.base, "/api/video/extend", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ prompt, videoUrl: args.video, duration }), signal: timeoutSignal(args.timeout) });
package/docs/API.md CHANGED
@@ -520,7 +520,7 @@ Generate a video via the Grok video provider. Returns Server-Sent Events on the
520
520
  |---|---|---|
521
521
  | No images | text-to-video | 1–15s |
522
522
  | 1 image (`sourceImage` or `sourceFilename`) | image-to-video | 1–15s |
523
- | 2–7 images (`referenceImages` / `referenceFilenames`) | reference-to-video | 1–10s |
523
+ | 2–14 images (`referenceImages` / `referenceFilenames`) | reference-to-video | 1–15s on grok-imagine-video-1.5, 1–10s on grok-imagine-video |
524
524
 
525
525
  1080p is accepted for `grok-imagine-video-1.5` prompt-only text-to-video and image-to-video with one image/frame source, including `continueFromVideo` after the server extracts the parent video's last frame. Prompt-only 1.5 text-to-video uses the internal white-canvas image-to-video shim before the upstream request. 1.5 does not add Ref2V, V2V edit, or extension support.
526
526
 
package/docs/API.zh-CN.md CHANGED
@@ -386,7 +386,7 @@ Grok节点SSE回复不包括回复API `partial`图像事件是因为xAI图片API
386
386
  |---|---|---|
387
387
  |没有图片|文本转视频| 1–15s |
388
388
  |1 张图片(`sourceImage`或者`sourceFilename`) |图像到视频| 1–15s |
389
- |2–7 张图像 (`referenceImages` / `referenceFilenames`) |参考视频| 1–10s |
389
+ |2–14 张图像 (`referenceImages` / `referenceFilenames`) |参考视频| 1.5: 1–15s / base: 1–10s |
390
390
 
391
391
  1080p 可接受`grok-imagine-video-1.5`仅提示文本到视频和图像到视频,具有一个图像/帧源,包括`continueFromVideo`服务器提取父视频的最后一帧后。仅提示 1.5 文本到视频在上游请求之前使用内部白色画布图像到视频 shim。 1.5 不添加 Ref2V、V2V 编辑或扩展支持。
392
392
 
package/docs/API.zh-TW.md CHANGED
@@ -386,7 +386,7 @@ Grok節點SSE回覆不包括回覆API `partial`影像事件是因為xAI圖片API
386
386
  |---|---|---|
387
387
  |沒有圖片|文字轉視頻| 1–15s |
388
388
  |1 張圖片(`sourceImage`或者`sourceFilename`) |影像到視頻| 1–15s |
389
- |2–7 張圖像 (`referenceImages` / `referenceFilenames`) |參考影片| 1–10s |
389
+ |2–14 張圖像 (`referenceImages` / `referenceFilenames`) |參考影片| 1.5: 1–15s / base: 1–10s |
390
390
 
391
391
  1080p 可接受`grok-imagine-video-1.5`僅提示文字到視頻和圖像到視頻,具有一個圖像/幀源,包括`continueFromVideo`伺服器提取父影片的最後一幀後。僅提示 1.5 文字到影片在上游請求之前使用內部白色畫布圖像到影片 shim。 1.5 不新增 Ref2V、V2V 編輯或擴充支援。
392
392
 
package/docs/CLI.md CHANGED
@@ -273,7 +273,7 @@ Video mode is auto-detected from `--ref` count:
273
273
  |---|---|
274
274
  | 0 | text-to-video |
275
275
  | 1 | image-to-video |
276
- | 2–7 | reference-to-video (max 10s duration) |
276
+ | 2–14 | reference-to-video (15s on grok-imagine-video-1.5, 10s on grok-imagine-video) |
277
277
 
278
278
  `grok-imagine-video-1.5` supports 1080p for prompt-only text-to-video and single image/frame image-to-video. Prompt-only 1.5 text-to-video is submitted through the internal white-canvas image-to-video shim because upstream 1.5 rejects raw T2V. The old `grok-imagine-video-1.5-preview` name is accepted as an alias and normalized before the upstream request. 1.5 does not support `reference_images` reference-to-video, V2V edit, or video extension. For 2+ refs, use `grok-imagine-video`; if ima2 auto-retries a 1.5 Ref2V request to the base model, read `video.effectiveModel` and `video.modelFallback` from CLI `--json`, or `effectiveModel` and `modelFallback` from SSE.
279
279
 
package/docs/CLI.zh-CN.md CHANGED
@@ -201,7 +201,7 @@ and rain build, end on a still close-up after the line finishes`。
201
201
  |---|---|
202
202
  | 0 |文本转视频|
203
203
  | 1 |图像到视频|
204
- | 2–7 |参考视频(最长 10 秒持续时间)|
204
+ | 2–14 |参考视频(grok-imagine-video-1.5 最长 15 秒,grok-imagine-video 最长 10 秒)|
205
205
 
206
206
  `grok-imagine-video-1.5`支持 1080p 仅提示文本到视频和单图像/帧图像到视频。仅提示 1.5 文本到视频通过内部白色画布图像到视频 shim 提交,因为上游 1.5 拒绝原始 T2V。旧的`grok-imagine-video-1.5-preview`name 在上游请求之前被接受为别名并进行规范化。 1.5 不支持`reference_images`视频参考、V2V 编辑或视频扩展。对于 2 个以上参考,请使用`grok-imagine-video`;如果ima2自动重试向基本模型发出 1.5 Ref2V 请求,读取`video.effectiveModel`和`video.modelFallback`从CLI `--json`, 或者`effectiveModel`和`modelFallback`从SSE.
207
207
 
package/docs/CLI.zh-TW.md CHANGED
@@ -201,7 +201,7 @@ and rain build, end on a still close-up after the line finishes`。
201
201
  |---|---|
202
202
  | 0 |文字轉視頻|
203
203
  | 1 |影像到視頻|
204
- | 2–7 |參考影片(最長 10 秒持續時間)|
204
+ | 2–14 |參考影片(grok-imagine-video-1.5 最長 15 秒,grok-imagine-video 最長 10 秒)|
205
205
 
206
206
  `grok-imagine-video-1.5`支援 1080p 僅提示文字到影片和單圖像/幀圖像到影片。僅提示 1.5 文字到影片透過內部白色畫布圖像到影片 shim 提交,因為上游 1.5 拒絕原始 T2V。舊的`grok-imagine-video-1.5-preview`name 在上游請求之前被接受為別名並進行規範化。 1.5 不支持`reference_images`影片參考、V2V 編輯或影片擴充。對於 2 個以上參考,請使用`grok-imagine-video`;如果ima2自動重試向基本模型發出 1.5 Ref2V 請求,讀取`video.effectiveModel`和`video.modelFallback`從CLI `--json`, 或者`effectiveModel`和`modelFallback`從SSE.
207
207
 
package/docs/README.ja.md CHANGED
@@ -120,7 +120,7 @@ API key が env/config に存在する場合、生成エンドポイントで `p
120
120
 
121
121
  Settings に **API key provider available** と表示される場合、API key が検出され、生成・編集・multimode・node request に使用できるという意味です。
122
122
 
123
- Grok 動画の既定値は正式名 `grok-imagine-video-1.5` です。`grok-imagine-video` は Ref2V、V2V edit、extension の互換パスで引き続き使用し、従来の `grok-imagine-video-1.5-preview` も互換 alias として受け付けます。参照数に応じて T2V(0)、I2V(1)、Ref2V(2-7、最大10秒)が自動選択され、1080p は `grok-imagine-video-1.5` の prompt-only T2V と単一画像/フレーム I2V で有効です。duration(1-15s)、resolution(480p/720p/対応時 1080p)、aspect ratio を設定できます。
123
+ Grok 動画の既定値は正式名 `grok-imagine-video-1.5` です。`grok-imagine-video` は Ref2V、V2V edit、extension の互換パスで引き続き使用し、従来の `grok-imagine-video-1.5-preview` も互換 alias として受け付けます。参照数に応じて T2V(0)、I2V(1)、Ref2V(2-14、grok-imagine-video-1.5 は最大15秒 / grok-imagine-video は最大10秒)が自動選択され、1080p は `grok-imagine-video-1.5` の prompt-only T2V と単一画像/フレーム I2V で有効です。duration(1-15s)、resolution(480p/720p/対応時 1080p)、aspect ratio を設定できます。
124
124
 
125
125
  設定画面の QuotaCard に Grok billing `$used/$limit` バーと **Switch Account** ボタン(`POST /api/auth/switch`)が表示されます。
126
126
 
package/docs/README.ko.md CHANGED
@@ -117,7 +117,7 @@ Ctrl+C로 DB, 소켓, 자식 프로세스를 정리할 수 있습니다. Windows
117
117
  - **Video 생성**: 텍스트, 이미지, 또는 여러 레퍼런스에서 짧은 영상을 만듭니다. SSE로 기획→제출→진행률→완료를 실시간 표시합니다. 생성된 영상에서 First/Mid/Last 프레임 복사 버튼으로 키프레임을 추출할 수 있습니다.
118
118
  - **Storyboard mode**: 컴포저에서 스토리보드 모드를 켜면 연속 프레임의 인물·장면 연속성을 유지합니다. 이미지와 영상 생성 모두 지원합니다.
119
119
  - **Local gallery**: 생성물을 내 컴퓨터에 저장하고 세션별 히스토리로 봅니다. 기본적으로 현재 세션만 보이며 All Images 토글로 전체 히스토리를 볼 수 있습니다. 각 이미지의 생성 시간·reasoning effort가 메타데이터에 기록됩니다.
120
- - **Reference images**: 레퍼런스를 드래그, 붙여넣기, 파일 선택으로 추가합니다. 이미지 최대 5장, 영상 최대 7장. 큰 이미지는 업로드 전에 자동 압축됩니다.
120
+ - **Reference images**: 레퍼런스를 드래그, 붙여넣기, 파일 선택으로 추가합니다. 이미지 최대 5장, 영상 최대 14장. 큰 이미지는 업로드 전에 자동 압축됩니다.
121
121
  - **Prompt library imports**: 로컬 prompt pack, GitHub folder, curated GPT-image hint를 내장 prompt library로 가져옵니다.
122
122
  - **Mobile shell**: 작은 화면에서는 app bar, compose sheet, compact settings toggle로 조작합니다.
123
123
  - **Observable jobs**: 진행 중인 작업과 최근 완료된 작업을 request ID로 추적합니다.
@@ -139,7 +139,7 @@ Ctrl+C로 DB, 소켓, 자식 프로세스를 정리할 수 있습니다. Windows
139
139
 
140
140
  Grok은 Classic, Node, Agent 흐름을 지원합니다. Classic 레퍼런스, Node 부모 이미지, Agent 현재 이미지가 있으면 최종 Grok 호출은 xAI image edit 경로로 전환되어 image-to-image 맥락을 유지합니다. 기본 이미지 모델은 `grok-imagine-image-quality`입니다.
141
141
 
142
- Grok video 기본값은 정식 `grok-imagine-video-1.5`입니다. `grok-imagine-video`는 Ref2V, V2V edit, extension 호환 경로에서 계속 쓰며, 기존 `grok-imagine-video-1.5-preview` 문자열도 호환 alias로 받습니다. 레퍼런스 수에 따라 T2V(0), I2V(1), Ref2V(2-7, 최대 10초)가 자동 선택되며, 1080p는 `grok-imagine-video-1.5` 프롬프트 전용 T2V와 단일 이미지/프레임 I2V에서 활성화됩니다. 프롬프트 전용 1.5 T2V는 upstream 요청 전에 내부 흰 캔버스 I2V shim을 사용합니다. duration(1-15s), resolution(480p/720p/지원 시 1080p), aspect ratio 컨트롤을 제공합니다.
142
+ Grok video 기본값은 정식 `grok-imagine-video-1.5`입니다. `grok-imagine-video`는 Ref2V, V2V edit, extension 호환 경로에서 계속 쓰며, 기존 `grok-imagine-video-1.5-preview` 문자열도 호환 alias로 받습니다. 레퍼런스 수에 따라 T2V(0), I2V(1), Ref2V(2-14, grok-imagine-video-1.5는 15초 / grok-imagine-video는 10초)가 자동 선택되며, 1080p는 `grok-imagine-video-1.5` 프롬프트 전용 T2V와 단일 이미지/프레임 I2V에서 활성화됩니다. 프롬프트 전용 1.5 T2V는 upstream 요청 전에 내부 흰 캔버스 I2V shim을 사용합니다. duration(1-15s), resolution(480p/720p/지원 시 1080p), aspect ratio 컨트롤을 제공합니다.
143
143
 
144
144
  설정 화면의 QuotaCard에 Grok billing `$used/$limit` 바와 **Switch Account** 버튼(`POST /api/auth/switch`)이 표시됩니다.
145
145
 
@@ -176,7 +176,7 @@ ima2 skill install --tmp # install to temp dir (fallback)
176
176
 
177
177
  Grok图像生成公开了模型选择器(`grok-imagine-image` / `grok-imagine-image-quality`)和尺寸选择器(长宽比 + 1k/2k 分辨率)。设置页面更喜欢Grok建立每周积分百分比并重置时间`GET /v1/billing?format=credits`;如果该来源不可用,则会退回到传统的每月计费窗口,并且`$used/$limit`. A **切换账户**按钮启动设备代码OAuth流动 (`POST /api/auth/switch`)无需离开应用程序即可重新进行身份验证。
178
178
 
179
- Grok视频生成默认为规范`grok-imagine-video-1.5`; `grok-imagine-video`仍然可用于仅限基本模型的 Ref2V、V2V 编辑和扩展路径,以及旧版本`grok-imagine-video-1.5-preview`字符串被接受作为别名。根据引用计数自动检测三种模式:文本到视频(0 引用)、图像到视频(1 引用)和引用到视频(2-7 引用,最长 10 秒持续时间)。 1080p 可用于`grok-imagine-video-1.5`仅提示文本到视频和单图像/帧图像到视频;仅提示 1.5 在上游请求之前使用内部白色画布 I2V 填充程序。视频控制包括持续时间(1-15秒)、分辨率(480p、720p、1080p(如果支持))和宽高比(1:1、16:9、9:16、4:3、3:4、3:2、2:3、自动)。
179
+ Grok视频生成默认为规范`grok-imagine-video-1.5`; `grok-imagine-video`仍然可用于仅限基本模型的 Ref2V、V2V 编辑和扩展路径,以及旧版本`grok-imagine-video-1.5-preview`字符串被接受作为别名。根据引用计数自动检测三种模式:文本到视频(0 引用)、图像到视频(1 引用)和引用到视频(2-14 引用;grok-imagine-video-1.5 最长 15 秒,grok-imagine-video 最长 10 秒)。 1080p 可用于`grok-imagine-video-1.5`仅提示文本到视频和单图像/帧图像到视频;仅提示 1.5 在上游请求之前使用内部白色画布 I2V 填充程序。视频控制包括持续时间(1-15秒)、分辨率(480p、720p、1080p(如果支持))和宽高比(1:1、16:9、9:16、4:3、3:4、3:2、2:3、自动)。
180
180
 
181
181
  ![设置工作区显示GPT OAuth活跃和API可用的密钥提供者。](../assets/screenshots/settings-oauth-generation.png)
182
182
 
@@ -170,7 +170,7 @@ Web UI 使用單一 `GET /api/events` 伺服器發送事件(SSE)連線,接
170
170
 
171
171
  Grok圖像生成公開了模型選擇器(`grok-imagine-image` / `grok-imagine-image-quality`)和尺寸選擇器(長寬比 + 1k/2k 解析度)。設定頁面更喜歡Grok建立每週積分百分比並重置時間`GET /v1/billing?format=credits`;如果該來源不可用,則會退回到傳統的每月計費窗口,並且`$used/$limit`. A **切換帳戶**按鈕啟動設備代碼OAuth流動 (`POST /api/auth/switch`)無需離開應用程式即可重新進行身份驗證。
172
172
 
173
- Grok影片產生預設為規範`grok-imagine-video-1.5`; `grok-imagine-video`仍可用於僅限基本型號的 Ref2V、V2V 編輯和擴展路徑,以及舊版本`grok-imagine-video-1.5-preview`字串被接受作為別名。根據引用計數自動偵測三種模式:文字到影片(0 引用)、圖像到影片(1 引用)和引用到影片(2-7 引用,最長 10 秒持續時間)。 1080p 可用於`grok-imagine-video-1.5`僅提示文字到影片和單圖像/幀圖像到影片;僅提示 1.5 在上游請求之前使用內部白色畫布 I2V 填充程式。視訊控制包括持續時間(1-15秒)、解析度(480p、720p、1080p(如果支援))和寬高比(1:1、16:9、9:16、4:3、3:4、3:2、2:3、自動)。
173
+ Grok影片產生預設為規範`grok-imagine-video-1.5`; `grok-imagine-video`仍可用於僅限基本型號的 Ref2V、V2V 編輯和擴展路徑,以及舊版本`grok-imagine-video-1.5-preview`字串被接受作為別名。根據引用計數自動偵測三種模式:文字到影片(0 引用)、圖像到影片(1 引用)和引用到影片(2-14 引用;grok-imagine-video-1.5 最長 15 秒,grok-imagine-video 最長 10 秒)。 1080p 可用於`grok-imagine-video-1.5`僅提示文字到影片和單圖像/幀圖像到影片;僅提示 1.5 在上游請求之前使用內部白色畫布 I2V 填充程式。視訊控制包括持續時間(1-15秒)、解析度(480p、720p、1080p(如果支援))和寬高比(1:1、16:9、9:16、4:3、3:4、3:2、2:3、自動)。
174
174
 
175
175
  ![設定工作區顯示GPT OAuth活躍和API可用的密鑰提供者。](../assets/screenshots/settings-oauth-generation.png)
176
176
 
@@ -125,7 +125,7 @@ Status values to handle:
125
125
  | default resolution | `480p` |
126
126
  | video edit input max | 8.7 seconds — **owner-measured, not documented by xAI.** Re-verified as still-believed 2026-08-20; no public source states this number, so do not drop it just because doc sweeps come back empty |
127
127
  | video extension input | 2-15 seconds |
128
- | extension duration | 2-10 seconds |
128
+ | extension duration | 2-10 seconds (superseded: re-measured as 1-15 on 2026-09-08, see devlog/_plan/260908_xai_imagine_spec_resync) |
129
129
  | generated URL | temporary; download promptly |
130
130
 
131
131
  ### Pricing Observed / Documented
@@ -125,7 +125,7 @@
125
125
  |默认分辨率| `480p` |
126
126
  |视频编辑最大输入|8.7秒|
127
127
  |视频扩展输入|2-15秒|
128
- |延长期限|2-10秒|
128
+ |延长期限|2-10秒(已更新:2026-09-08 实测为 1-15 秒)|
129
129
  |生成的URL |暂时的;及时下载|
130
130
 
131
131
  ### 观察/记录的定价
@@ -125,7 +125,7 @@
125
125
  |預設解析度| `480p` |
126
126
  |影片編輯最大輸入|8.7秒|
127
127
  |視訊擴充輸入|2-15秒|
128
- |延長期限|2-10秒|
128
+ |延長期限|2-10秒(已更新:2026-09-08 實測為 1-15 秒)|
129
129
  |產生的URL |暫時的;及時下載|
130
130
 
131
131
  ### 觀察/記錄的定價
@@ -4,7 +4,7 @@ Generated by `npm run test:inventory` (script: `scripts/classify-tests.mjs`).
4
4
 
5
5
  _Tests considered "runtime-importing" if they import from `../lib/`, `../routes/`, `../bin/`, `../server`, or `../config`._
6
6
 
7
- Total: 488 (runtime: 225, contract: 263)
7
+ Total: 492 (runtime: 227, contract: 265)
8
8
 
9
9
  ## Runtime-importing tests
10
10
  - `tests/agent-image-reference-contract.test.ts`
@@ -232,6 +232,8 @@ Total: 488 (runtime: 225, contract: 263)
232
232
  - `tests/videoExtendedRoute.test.ts`
233
233
  - `tests/videoLineage.test.ts`
234
234
  - `tests/videoRoute.test.ts`
235
+ - `tests/voice-picker-contract.test.ts`
236
+ - `tests/xai-video-model-alias-contract.test.ts`
235
237
 
236
238
  ## Contract-only tests
237
239
  - `tests/a11y-modal-contract.test.ts`
@@ -332,6 +334,7 @@ Total: 488 (runtime: 225, contract: 263)
332
334
  - `tests/core-selection-transport.test.mjs`
333
335
  - `tests/current-image-actions-readiness-contract.test.js`
334
336
  - `tests/direct-mode-visual-contract.test.js`
337
+ - `tests/dropped-media-sorting-contract.test.ts`
335
338
  - `tests/duration-slider-contract.test.js`
336
339
  - `tests/e2e-app-environment.test.ts`
337
340
  - `tests/e2e-runtime-build.test.mjs`
@@ -489,6 +492,7 @@ Total: 488 (runtime: 225, contract: 263)
489
492
  - `tests/vectorize-panel-contract.test.ts`
490
493
  - `tests/video-continuity-ui-contract.test.js`
491
494
  - `tests/video-defaults-persistence-contract.test.js`
495
+ - `tests/video-edit-client-contract.test.ts`
492
496
  - `tests/video-extend-ui-contract.test.js`
493
497
  - `tests/video-frame-local-file.test.js`
494
498
  - `tests/video-gallery-refresh-contract.test.ts`
@@ -5,7 +5,7 @@ import { buildCatalog, catalogSummary } from "./contracts/catalog.js";
5
5
  import { loadAllBundledSnapshots } from "./mcp/snapshotStore.js";
6
6
  import { KEY_TO_ENV, WRITABLE_CONFIG_KEYS } from "./configKeys.js";
7
7
  import { DEFAULT_IMAGE_QUALITY, VALID_IMAGE_QUALITIES } from "./oauthNormalize.js";
8
- import { MAX_REF2V_REFERENCES, MAX_REFERENCE_AUDIOS, MAX_VIDEO_DURATION, MIN_VIDEO_DURATION } from "./imageModels.js";
8
+ import { MAX_REF2V_DURATION_15, MAX_REF2V_DURATION_BASE, MAX_REF2V_REFERENCES, MAX_REFERENCE_AUDIOS, MAX_VIDEO_DURATION, MIN_VIDEO_DURATION, } from "./imageModels.js";
9
9
  import { deriveProviderIds, getProviderSurfaceSupport } from "./providers/derive.js";
10
10
  import { PROVIDER_SURFACES } from "./providers/surfaceSupport.js";
11
11
  const VALID_MODES = ["auto", "direct"];
@@ -86,18 +86,21 @@ export function buildIma2Capabilities({ appConfig = runtimeConfigDefault, packag
86
86
  },
87
87
  videoModels: {
88
88
  supported: ["grok-imagine-video", "grok-imagine-video-1.5"],
89
- aliases: { "grok-imagine-video-1.5-preview": "grok-imagine-video-1.5" },
89
+ aliases: {
90
+ "grok-imagine-video-1.5-preview": "grok-imagine-video-1.5",
91
+ "grok-imagine-video-1.5-2026-05-30": "grok-imagine-video-1.5",
92
+ },
90
93
  resolutions: ["480p", "720p", "1080p"],
91
94
  resolutionNotes: { "1080p": "grok-imagine-video-1.5 text-to-video canvas shim or image-to-video; reference-to-video unsupported" },
92
95
  aspectRatios: ["1:1", "16:9", "9:16", "4:3", "3:4", "3:2", "2:3", "auto"],
93
96
  durationRange: [1, 15],
94
- maxReferences: 7,
97
+ maxReferences: MAX_REF2V_REFERENCES,
95
98
  // Flat numbers above describe the widest case, which is not what any single
96
99
  // request is allowed to do. A client that reads only those draws controls the
97
100
  // server will reject. These per-mode entries come from the same constants the
98
101
  // request path enforces, so the advertisement cannot drift from the behavior.
99
- // Verified against api.x.ai on 2026-08-20:
100
- // devlog/_plan/260820_grok15_multi_reference_video/000_research.md
102
+ // Verified against api.x.ai on 2026-09-08:
103
+ // devlog/_plan/260908_xai_imagine_spec_resync/000_research.md
101
104
  modes: {
102
105
  "text-to-video": {
103
106
  maxReferences: 0,
@@ -113,9 +116,15 @@ export function buildIma2Capabilities({ appConfig = runtimeConfigDefault, packag
113
116
  },
114
117
  "reference-to-video": {
115
118
  maxReferences: MAX_REF2V_REFERENCES,
116
- durationRange: [MIN_VIDEO_DURATION, MAX_VIDEO_DURATION],
119
+ // The widest case, which only grok-imagine-video-1.5 can actually reach.
120
+ // durationRangeByModel below is what a single request must obey.
121
+ durationRange: [MIN_VIDEO_DURATION, MAX_REF2V_DURATION_15],
122
+ durationRangeByModel: {
123
+ "grok-imagine-video-1.5": [MIN_VIDEO_DURATION, MAX_REF2V_DURATION_15],
124
+ "grok-imagine-video": [MIN_VIDEO_DURATION, MAX_REF2V_DURATION_BASE],
125
+ },
117
126
  resolutions: ["480p", "720p"],
118
- notes: "References guide the subject without locking the first frame. 1080p is rejected upstream.",
127
+ notes: `References guide the subject without locking the first frame. 1080p is rejected upstream. Up to ${MAX_REF2V_DURATION_15}s on grok-imagine-video-1.5, ${MAX_REF2V_DURATION_BASE}s on grok-imagine-video. A preset voice alone also selects this mode.`,
119
128
  },
120
129
  },
121
130
  referenceAudio: {
@@ -130,6 +139,23 @@ export function buildIma2Capabilities({ appConfig = runtimeConfigDefault, packag
130
139
  "atlas",
131
140
  ],
132
141
  presetsAreAuthoritative: false,
142
+ // Named by the 400 itself: "Custom voice ids created via the /v1/custom-voices
143
+ // API are also accepted." The endpoint answers 200 with a 30-voice cap that is
144
+ // documented nowhere else.
145
+ customVoiceApi: "/v1/custom-voices",
146
+ },
147
+ // Which model takes a video in, and which takes audio in. The two split on input
148
+ // modality, so there is no path that edits a video on 1.5 — it answers 400.
149
+ videoInputModes: {
150
+ edit: {
151
+ models: ["grok-imagine-video"],
152
+ notes: "grok-imagine-video-1.5 answers 400 'Video editing is not supported for this model.'",
153
+ },
154
+ extend: {
155
+ models: ["grok-imagine-video"],
156
+ durationRange: [MIN_VIDEO_DURATION, MAX_VIDEO_DURATION],
157
+ notes: "duration is the added segment, not the total. Measured 1-15s, wider than the documented 2-10.",
158
+ },
133
159
  },
134
160
  },
135
161
  reasoningEfforts: toArray(appConfig.imageModels.validReasoningEfforts),
@@ -5,7 +5,7 @@ import { detectImageMimeFromB64 } from "./refs.js";
5
5
  import { aspectToCanvas, generateWhiteCanvasB64 } from "./grokVideoCanvas.js";
6
6
  import { downloadVideo } from "./grokVideoDownload.js";
7
7
  import { buildGrokVideoPlannerSystemPrompt, composeFallbackVideoPrompt, formatDurationPacingGuidance } from "./grokVideoPlannerPrompt.js";
8
- import { GROK_VIDEO_MODEL_15, GROK_VIDEO_MODEL_15_PREVIEW_ALIAS, GROK_VIDEO_MODEL_BASE, MAX_REF2V_REFERENCES, MAX_REFERENCE_AUDIOS, validateVideoResolutionForRequest, } from "./imageModels.js";
8
+ import { GROK_VIDEO_MODEL_15, GROK_VIDEO_MODEL_BASE, MAX_REF2V_REFERENCES, MAX_REFERENCE_AUDIOS, canonicalGrokVideoModel, validateVideoDurationForRequest, validateVideoResolutionForRequest, } from "./imageModels.js";
9
9
  import { formatVideoContinuityForPlanner } from "./videoContinuity.js";
10
10
  import { DEFAULT_GROK_PLANNER_MODEL } from "../config.js";
11
11
  import { videoConfig, videoEndpoint, withTimeoutSignal, } from "./grokVideoShared.js";
@@ -41,9 +41,7 @@ export function isDegradablePlannerFailure(err) {
41
41
  return { reason: "failed", message };
42
42
  return null;
43
43
  }
44
- function canonicalVideoModel(model) {
45
- return model === GROK_VIDEO_MODEL_15_PREVIEW_ALIAS ? GROK_VIDEO_MODEL_15 : model;
46
- }
44
+ const canonicalVideoModel = canonicalGrokVideoModel;
47
45
  function sourceImageUrl(image, mime) {
48
46
  if (image.startsWith("data:") || image.startsWith("http"))
49
47
  return image;
@@ -314,6 +312,13 @@ export function buildVideoGenerationPayload(plan, opts) {
314
312
  if (!("ok" in resolutionCheck)) {
315
313
  throw grokError(resolutionCheck.error, resolutionCheck.status, resolutionCheck.code);
316
314
  }
315
+ // Answered here as well as at the route because the agent path builds payloads without
316
+ // going through routes/video.ts. Two checks of the same rule are two defenses, not a
317
+ // duplicated rule: both read the same function.
318
+ const durationCheck = validateVideoDurationForRequest(model, plan.duration, plan.mode);
319
+ if (!("ok" in durationCheck)) {
320
+ throw grokError(durationCheck.error, durationCheck.status, durationCheck.code);
321
+ }
317
322
  const payload = { model, prompt: plan.prompt, duration: plan.duration, resolution: plan.resolution };
318
323
  if (plan.aspectRatio && plan.aspectRatio !== "auto")
319
324
  payload.aspect_ratio = plan.aspectRatio;
@@ -171,10 +171,15 @@ export function normalizeComfyWorkflowModel(rawModel) {
171
171
  export const GROK_VIDEO_MODEL_BASE = "grok-imagine-video";
172
172
  export const GROK_VIDEO_MODEL_15 = "grok-imagine-video-1.5";
173
173
  export const GROK_VIDEO_MODEL_15_PREVIEW_ALIAS = "grok-imagine-video-1.5-preview";
174
+ // GET /v1/video-generation-models lists this alongside -preview. Without it a legal
175
+ // model id is rejected locally with INVALID_GROK_VIDEO_MODEL before the request ever
176
+ // reaches xAI. Verified 2026-09-08: devlog/_plan/260908_xai_imagine_spec_resync.
177
+ export const GROK_VIDEO_MODEL_15_DATED_ALIAS = "grok-imagine-video-1.5-2026-05-30";
174
178
  export const GROK_FALLBACK_VIDEO_MODEL = GROK_VIDEO_MODEL_15;
175
179
  export const VALID_GROK_VIDEO_MODELS = new Set([
176
180
  ...deriveModels("grok", "video"),
177
181
  GROK_VIDEO_MODEL_15_PREVIEW_ALIAS,
182
+ GROK_VIDEO_MODEL_15_DATED_ALIAS,
178
183
  ]);
179
184
  export const VALID_VIDEO_RESOLUTIONS = new Set(["480p", "720p", "1080p"]);
180
185
  export const VALID_VIDEO_ASPECT_RATIOS = new Set([
@@ -189,10 +194,25 @@ export const VALID_VIDEO_ASPECT_RATIOS = new Set([
189
194
  ]);
190
195
  export const MIN_VIDEO_DURATION = 1;
191
196
  export const MAX_VIDEO_DURATION = 15;
192
- // reference-to-video (xAI): up to 7 reference images (8 -> 400), 1-15s, 720p max.
193
- // Verified against api.x.ai on 2026-08-20:
194
- // devlog/_plan/260820_grok15_multi_reference_video/000_research.md
195
- export const MAX_REF2V_REFERENCES = 7;
197
+ // reference-to-video (xAI): up to 14 reference images, 720p max.
198
+ //
199
+ // The cap doubled from 7. 15 returns 400 "Too many reference images: 15. Maximum
200
+ // allowed is 14." and 30 names the same number, on BOTH video models, so it is a
201
+ // model-independent limit. @imagine announced it on 2026-09-02; the API docs still
202
+ // say 7, which is why the measurement is the source here and not the page.
203
+ // Re-verified against api.x.ai on 2026-09-08:
204
+ // devlog/_plan/260908_xai_imagine_spec_resync/000_research.md
205
+ export const MAX_REF2V_REFERENCES = 14;
206
+ // reference-to-video carries its own duration ceiling, and it differs by model. The
207
+ // same 16s request answers "maximum allowed for reference-to-video, which is 15s" on
208
+ // grok-imagine-video-1.5 and "... is 10s" on grok-imagine-video.
209
+ //
210
+ // This is NOT the ceiling removed in 260820. That one was invented locally and applied
211
+ // to every model against evidence. This one is the upstream's own per-model rule, and
212
+ // omitting it turns a legal-looking base-model request into an upstream 400 the user
213
+ // pays a planning round for.
214
+ export const MAX_REF2V_DURATION_15 = 15;
215
+ export const MAX_REF2V_DURATION_BASE = 10;
196
216
  // reference_audios: preset voices, grok-imagine-video-1.5 only. 4 -> 400.
197
217
  export const MAX_REFERENCE_AUDIOS = 3;
198
218
  /**
@@ -216,6 +236,55 @@ export function deriveVideoMode(refCount) {
216
236
  export function isGrokVideoModel(value) {
217
237
  return typeof value === "string" && VALID_GROK_VIDEO_MODELS.has(value);
218
238
  }
239
+ /**
240
+ * Folds every alias onto the id xAI bills and validates against.
241
+ *
242
+ * Three call sites used to inline `model === PREVIEW_ALIAS ? GROK_VIDEO_MODEL_15 : model`,
243
+ * which is exactly one alias' worth of knowledge duplicated three times. Adding the dated
244
+ * alias would have had to find all three; one of them would have been missed.
245
+ *
246
+ * Passes non-Grok ids through unchanged: comfy hands a workflow id down this same
247
+ * `model` field (routes/video.ts), and rewriting it would be worse than leaving it alone.
248
+ */
249
+ export function canonicalGrokVideoModel(model) {
250
+ return model === GROK_VIDEO_MODEL_15_PREVIEW_ALIAS || model === GROK_VIDEO_MODEL_15_DATED_ALIAS
251
+ ? GROK_VIDEO_MODEL_15
252
+ : model;
253
+ }
254
+ /**
255
+ * The reference-to-video duration ceiling for a model, or null when the rule does not
256
+ * apply to it.
257
+ *
258
+ * Null is not "unknown" and not "no limit": it means xAI's per-model r2v rule has no
259
+ * jurisdiction here. A comfy workflow id arrives through the same `model` field, and
260
+ * answering the base model's 10s for it would clamp a lane whose ceiling xAI does not
261
+ * own. Callers must handle null explicitly rather than let it fall through to a number.
262
+ */
263
+ export function maxRef2vDuration(model) {
264
+ if (!isGrokVideoModel(model))
265
+ return null;
266
+ return canonicalGrokVideoModel(model) === GROK_VIDEO_MODEL_15
267
+ ? MAX_REF2V_DURATION_15
268
+ : MAX_REF2V_DURATION_BASE;
269
+ }
270
+ /**
271
+ * Validates a duration against the ceiling its mode and model actually impose.
272
+ *
273
+ * Returns ok for every non-reference-to-video request: normalizeVideoDuration already
274
+ * owns the shared 1-15 bound, and re-checking it here would give one rule two owners.
275
+ */
276
+ export function validateVideoDurationForRequest(model, duration, mode) {
277
+ if (mode !== "reference-to-video")
278
+ return { ok: true };
279
+ const ceiling = maxRef2vDuration(model);
280
+ if (ceiling === null || duration <= ceiling)
281
+ return { ok: true };
282
+ return {
283
+ error: `reference-to-video on ${model} allows at most ${ceiling} seconds`,
284
+ code: "INVALID_VIDEO_DURATION",
285
+ status: 400,
286
+ };
287
+ }
219
288
  export function normalizeGrokVideoModel(rawModel) {
220
289
  if (typeof rawModel !== "string" || rawModel.length === 0) {
221
290
  return { model: GROK_FALLBACK_VIDEO_MODEL };
@@ -227,7 +296,7 @@ export function normalizeGrokVideoModel(rawModel) {
227
296
  status: 400,
228
297
  };
229
298
  }
230
- return { model: rawModel === GROK_VIDEO_MODEL_15_PREVIEW_ALIAS ? GROK_VIDEO_MODEL_15 : rawModel };
299
+ return { model: canonicalGrokVideoModel(rawModel) };
231
300
  }
232
301
  export function normalizeVideoResolution(raw) {
233
302
  if (raw === undefined || raw === null || raw === "")
@@ -242,13 +311,17 @@ export function normalizeVideoResolution(raw) {
242
311
  return { resolution: raw };
243
312
  }
244
313
  export function usesGrokVideo15TextCanvasShim(model, mode) {
245
- const canonicalModel = model === GROK_VIDEO_MODEL_15_PREVIEW_ALIAS ? GROK_VIDEO_MODEL_15 : model;
246
- return canonicalModel === GROK_VIDEO_MODEL_15 && mode === "text-to-video";
314
+ return canonicalGrokVideoModel(model) === GROK_VIDEO_MODEL_15 && mode === "text-to-video";
247
315
  }
248
316
  export function validateVideoResolutionForRequest(model, resolution, mode, options = {}) {
249
317
  if (resolution !== "1080p")
250
318
  return { ok: true };
251
- const canonicalModel = model === GROK_VIDEO_MODEL_15_PREVIEW_ALIAS ? GROK_VIDEO_MODEL_15 : model;
319
+ // A non-Grok model reaches this function too: comfy passes its workflow id through the
320
+ // same field. xAI's 1080p rule says nothing about a locally-run workflow, so refusing
321
+ // one here would be this codebase inventing a limit rather than enforcing a real one.
322
+ if (!isGrokVideoModel(model))
323
+ return { ok: true };
324
+ const canonicalModel = canonicalGrokVideoModel(model);
252
325
  if (canonicalModel === GROK_VIDEO_MODEL_15 && mode === "image-to-video") {
253
326
  return { ok: true };
254
327
  }
@@ -68,9 +68,13 @@ export const REGISTRY = [
68
68
  { id: "grok-imagine-image", kind: "image", supports: EDIT },
69
69
  { id: "grok-imagine-image-quality", kind: "image", supports: EDIT },
70
70
  { id: "grok-imagine-video", kind: "video", supports: EDIT },
71
- { id: "grok-imagine-video-1.5", aliases: ["grok-imagine-video-1.5-preview"], kind: "video", supports: EDIT },
71
+ { id: "grok-imagine-video-1.5", aliases: ["grok-imagine-video-1.5-preview", "grok-imagine-video-1.5-2026-05-30"], kind: "video", supports: EDIT },
72
72
  ],
73
- referenceLimits: { image: 3, edit: 3, video: 7 },
73
+ // image/edit 5: measured on grok-imagine-image-2.0 through the oauth proxy on
74
+ // 2026-09-08 (6 -> 400 "This model supports at most 5 input image(s)"), and xAI's
75
+ // release notes say editing "now accepts up to 5 source images per request (was 3)".
76
+ // video 14: see MAX_REF2V_REFERENCES in lib/imageModels.ts.
77
+ referenceLimits: { image: 5, edit: 5, video: 14 },
74
78
  elementTaxonomy: "grok",
75
79
  limits: { timeoutMs: 300_000 },
76
80
  errorPrefix: "GROK_",
@@ -92,9 +96,12 @@ export const REGISTRY = [
92
96
  { id: "grok-imagine-image", kind: "image", supports: EDIT },
93
97
  { id: "grok-imagine-image-quality", kind: "image", supports: EDIT },
94
98
  { id: "grok-imagine-video", kind: "video", supports: EDIT },
95
- { id: "grok-imagine-video-1.5", aliases: ["grok-imagine-video-1.5-preview"], kind: "video", supports: EDIT },
99
+ { id: "grok-imagine-video-1.5", aliases: ["grok-imagine-video-1.5-preview", "grok-imagine-video-1.5-2026-05-30"], kind: "video", supports: EDIT },
96
100
  ],
97
- referenceLimits: { image: 3, edit: 3, video: 7 },
101
+ // Same upstream as the grok lane, reached with a direct key instead of the proxy, so
102
+ // the caps are taken to match. The 5 was measured on the proxy path only; if an edit
103
+ // fails at 4-5 images with a direct key, this assumption is the place to look.
104
+ referenceLimits: { image: 5, edit: 5, video: 14 },
98
105
  elementTaxonomy: "grok",
99
106
  limits: { timeoutMs: 300_000 },
100
107
  errorPrefix: "GROK_",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ima2-gen",
3
- "version": "3.15.0",
3
+ "version": "3.15.1",
4
4
  "packageManager": "npm@11.18.0",
5
5
  "description": "Local-first visual generation runtime and studio for people and coding agents, with reproducible image and video workflows across multiple providers.",
6
6
  "type": "module",
@@ -127,5 +127,5 @@
127
127
  "typescript": "^5.9.3",
128
128
  "yaml": "2.9.0"
129
129
  },
130
- "gitHead": "b96a11ed8a46c5782e673b5eeb61129d9f3aee9d"
130
+ "gitHead": "11900764a59e74146c9008ed296a23d3fc0ac06a"
131
131
  }
package/routes/video.js CHANGED
@@ -16,7 +16,7 @@ import { getVideoSeriesChain } from "../lib/videoSeriesChain.js";
16
16
  import { ACTIVE_VIDEO_PROMPT_GUIDANCE, appendVideoContinuityEntry, lineageFromVideoMetadata, normalizeVideoContinuityLineage, readVideoSidecar, requireActiveVideoPrompt, safeGeneratedVideoFilename, } from "../lib/videoContinuity.js";
17
17
  import { extractGeneratedVideoFrameB64 } from "../lib/videoFrameExtract.js";
18
18
  import { errorEnvelopeFields } from "../lib/errors/envelope.js";
19
- import { normalizeGrokVideoModel, normalizeVideoResolution, normalizeVideoAspectRatio, normalizeVideoDuration, deriveVideoMode, MAX_REF2V_REFERENCES, MAX_REFERENCE_AUDIOS, validateVideoResolutionForRequest, } from "../lib/imageModels.js";
19
+ import { normalizeGrokVideoModel, normalizeVideoResolution, normalizeVideoAspectRatio, normalizeVideoDuration, deriveVideoMode, MAX_REF2V_REFERENCES, MAX_REFERENCE_AUDIOS, validateVideoResolutionForRequest, validateVideoDurationForRequest, } from "../lib/imageModels.js";
20
20
  import { errInfo } from "../lib/errInfo.js";
21
21
  import { requireRuntimeContext } from "../lib/runtimeContext.js";
22
22
  import { generateVideoThumbnail } from "../lib/videoThumb.js";
@@ -338,18 +338,36 @@ export function registerVideoRoutes(app, ctxRaw) {
338
338
  // devlog/_plan/260820_grok15_multi_reference_video/030_single_ref_mode_choice.md
339
339
  const composerRefCount = resolved.filter((r) => r.source === "composer").length;
340
340
  const requestedMode = typeof req.body?.mode === "string" ? req.body.mode : null;
341
+ // Shape only. Which voice ids exist is xAI's to answer, and its 400 names every
342
+ // valid voice — a list we would only get wrong, and which cannot include the
343
+ // caller's custom voices anyway. Parsed here rather than at the call site because
344
+ // the mode derivation below needs to know whether any voice was attached.
345
+ const referenceAudios = toArray(req.body?.referenceAudios)
346
+ .map((voice) => (typeof voice === "string" ? voice.trim() : ""))
347
+ .filter((voice) => voice.length > 0);
348
+ if (referenceAudios.length > MAX_REFERENCE_AUDIOS) {
349
+ return fail(400, "GROK_VIDEO_AUDIO_TOO_MANY", `at most ${MAX_REFERENCE_AUDIOS} reference voices`);
350
+ }
341
351
  const derivedMode = composerRefCount > 0
342
352
  ? "reference-to-video"
343
- : deriveVideoMode(resolved.length);
353
+ // A voice alone selects reference-to-video upstream: "at least one reference of
354
+ // either kind selects the reference-to-video mode" (xAI OpenAPI,
355
+ // GenerateVideoRequest.reference_audios). Calling it text-to-video here would let
356
+ // a 15s base-model request through while xAI applies the 10s r2v ceiling, and the
357
+ // rejection would name a mode this request never asked for.
358
+ : referenceAudios.length > 0
359
+ ? "reference-to-video"
360
+ : deriveVideoMode(resolved.length);
344
361
  const mode = incomingProviderUrl
345
362
  ? "image-to-video"
346
363
  : (requestedMode === "reference-to-video" || requestedMode === "image-to-video" || requestedMode === "text-to-video")
347
364
  ? requestedMode
348
365
  : derivedMode;
349
- // An explicit reference-to-video with nothing to reference would ship an empty
350
- // reference_images array and fail upstream with a less useful message.
351
- if (mode === "reference-to-video" && resolved.length === 0) {
352
- return fail(400, "GROK_VIDEO_INVALID_MODE", "reference-to-video requires at least 1 reference image");
366
+ // An explicit reference-to-video with nothing to reference at all would ship empty
367
+ // arrays and fail upstream with a less useful message. A voice counts as a
368
+ // reference, so audio-only is legitimate and must not be rejected here.
369
+ if (mode === "reference-to-video" && resolved.length === 0 && referenceAudios.length === 0) {
370
+ return fail(400, "GROK_VIDEO_INVALID_MODE", "reference-to-video requires at least 1 reference image or voice");
353
371
  }
354
372
  const duration = durationCheck.duration;
355
373
  const resolutionModeCheck = validateVideoResolutionForRequest(modelCheck.model, resolutionCheck.resolution, mode, {
@@ -357,6 +375,12 @@ export function registerVideoRoutes(app, ctxRaw) {
357
375
  });
358
376
  if (isNormalizeError(resolutionModeCheck))
359
377
  return fail(resolutionModeCheck.status, resolutionModeCheck.code, resolutionModeCheck.error);
378
+ // The r2v ceiling is per model, and the start call is deliberately never retried
379
+ // (a retry could bill a second video), so a 400 we can predict must be answered
380
+ // before the job is admitted rather than asynchronously after planning.
381
+ const durationModeCheck = validateVideoDurationForRequest(modelCheck.model, duration, mode);
382
+ if (isNormalizeError(durationModeCheck))
383
+ return fail(durationModeCheck.status, durationModeCheck.code, durationModeCheck.error);
360
384
  const referenceImages = mode === "reference-to-video" ? resolved.map((r) => r.b64) : undefined;
361
385
  const sourceB64 = incomingProviderUrl || (mode === "image-to-video" ? resolved[0]?.b64 : undefined);
362
386
  const sourceFilename = resolved[0]?.filename ?? null;
@@ -483,15 +507,6 @@ export function registerVideoRoutes(app, ctxRaw) {
483
507
  + (backgroundPreset ? ` ${backgroundPromptSuffix(backgroundPreset, "video")}` : "");
484
508
  const plannerModel = typeof req.body?.plannerModel === "string" ? req.body.plannerModel.trim() : undefined;
485
509
  const directApiKey = provider === "grok-api" ? ctx.xaiApiKey : undefined;
486
- // Only the shape is checked here. Which voice ids exist is xAI's to answer, and its
487
- // 400 names every valid voice — a list we would only get wrong, and which cannot
488
- // include the caller's custom voices anyway.
489
- const referenceAudios = toArray(req.body?.referenceAudios)
490
- .map((voice) => (typeof voice === "string" ? voice.trim() : ""))
491
- .filter((voice) => voice.length > 0);
492
- if (referenceAudios.length > MAX_REFERENCE_AUDIOS) {
493
- return fail(400, "GROK_VIDEO_AUDIO_TOO_MANY", `at most ${MAX_REFERENCE_AUDIOS} reference voices`);
494
- }
495
510
  const result = await generateVideoViaGrok(effectivePrompt, ctx, {
496
511
  model: modelCheck.model,
497
512
  mode,