From 5bf5457948f9515a335b7f6e9c157c2ecb89e365 Mon Sep 17 00:00:00 2001 From: mac Date: Wed, 15 Jul 2026 19:09:45 +0800 Subject: [PATCH] feat: support grok reference video mode --- CANVAS_SPEC.md | 1 + electron/lib/canvas.js | 13 +++++++++++-- renderer/app.js | 31 ++++++++++++++++++++++++++++++- renderer/styles.css | 2 +- 4 files changed, 43 insertions(+), 4 deletions(-) diff --git a/CANVAS_SPEC.md b/CANVAS_SPEC.md index 516e15d..21dd608 100644 --- a/CANVAS_SPEC.md +++ b/CANVAS_SPEC.md @@ -60,6 +60,7 @@ canvasRun: (kind, params) => ipcRenderer.invoke("canvas-run", { kind, params }), - `model`:用户填写的视频模型(为空返回明确错误)。 - body 组装:`{ model, prompt, ...(mode==='image'?{image}:{}) , size, ratio, duration, ...extra }`(空字段不塞)。 - `endpointPath` 默认 `/v1/videos`,允许覆盖。仍严禁伪造成功。 +- **grokVideo** `{ mode, prompt, model, image?, referenceImages?[], resolution?, seconds?, aspectRatio?, endpointPath? }` —— **Grok 视频**。`mode:"reference"` 走 Reference-to-Video:强制 `model:"grok-imagine-video"`、`resolution:"720p"`,把 `referenceImages[]` 解析为 `reference_images: [{url}]`;`grok-imagine-video-1.5` 仅用于单图首帧场景,不用于多参考图。 - **tts** 增加 `model`(默认 `tts-1` 可保留但允许覆盖)、`format`、可选 `speed`。 ### 新增 canvasModels(后端 + preload) diff --git a/electron/lib/canvas.js b/electron/lib/canvas.js index b79ddaa..7397f97 100644 --- a/electron/lib/canvas.js +++ b/electron/lib/canvas.js @@ -434,7 +434,7 @@ async function runVideo(base, llm, params) { // —— Grok 视频:独立新增接口,按 /v1/videos/generations + /v1/videos/{request_id} 调用 —— async function runGrokVideo(base, llm, params) { - const model = params.model || "grok-imagine-video"; + let model = params.model || "grok-imagine-video"; const prompt = (params.prompt || "").trim(); if (!prompt) return { ok: false, error: "请填写 Grok 视频提示词" }; @@ -458,8 +458,17 @@ async function runGrokVideo(base, llm, params) { const img = await resolveMat(params.imageUrl || params.image); if (!img) return { ok: false, error: "Grok 图生视频需要首帧图:上传 / 手填公网URL / 连接上游文生图" }; body.image = { image_url: img }; + } else if (mode === "reference") { + const refs = Array.isArray(params.referenceImages) ? params.referenceImages : []; + const resolved = (await Promise.all(refs.map((r) => resolveMat(r)))).filter(Boolean); + if (!resolved.length) return { ok: false, error: "Grok 多参考图模式请至少提供 1 张参考图" }; + model = "grok-imagine-video"; + body.model = model; + body.resolution = "720p"; + if (!body.aspect_ratio) body.aspect_ratio = params.aspectRatio || "16:9"; + body.reference_images = resolved.map((url) => ({ url })); } else if (mode !== "text") { - return { ok: false, error: "Grok 视频新增接口只支持「文生视频」和「首帧图生视频」" }; + return { ok: false, error: "Grok 视频新增接口只支持「文生视频」「首帧图生视频」和「多参考图」" }; } const createUrl = joinUrl(base, params.endpointPath || "/v1/videos/generations"); diff --git a/renderer/app.js b/renderer/app.js index 22753b4..942eb11 100644 --- a/renderer/app.js +++ b/renderer/app.js @@ -1775,6 +1775,7 @@ function cvNodeBody(kind) { +
@@ -1804,7 +1813,7 @@ function cvNodeBody(kind) {
-
1.5 的 1080p/15s 需要首帧图;完成前可能在 94% 停留较久
+
多参考图使用 grok-imagine-video + 720p;1.5 的 1080p/15s 需要首帧图
⚙️ 高级
@@ -2087,6 +2096,8 @@ function cvClearVideoMat(nodeEl, slot) { if (slot === "refs") { const box = nodeEl.querySelector(".cv-vmat-refs-thumbs"); if (box) box.innerHTML = ""; + const urls = nodeEl.querySelector('.cv-vmat-urls[data-slot="refs"]'); + if (urls) urls.value = ""; } else { const thumb = nodeEl.querySelector(`.cv-vmat-thumb[data-slot="${slot}"]`); if (thumb) { @@ -2305,6 +2316,12 @@ function cvSyncVideoMode(nodeEl) { const box = nodeEl.querySelector(".cv-vmat-" + slot); if (box) box.classList.toggle("hidden", !show[slot]); }); + if (mode === "reference" && nodeEl.querySelector(".cv-node-grokVideo")) { + const model = nodeEl.querySelector(".cv-field-model"); + const resolution = nodeEl.querySelector(".cv-field-vsize"); + if (model) model.value = "grok-imagine-video"; + if (resolution) resolution.value = "720p"; + } } // 读取单个节点的「输出产物」:文本节点实时读 textarea,其它读运行后缓存的 dataUrl @@ -2606,6 +2623,10 @@ async function cvRunNode(nodeId, btn) { const el = nodeEl.querySelector(`.cv-vmat-url[data-slot="${slot}"]`); return el ? el.value.trim() : ""; }; + const matUrls = (slot) => { + const el = nodeEl.querySelector(`.cv-vmat-urls[data-slot="${slot}"]`); + return el ? el.value.split(/\s+/).map((s) => s.trim()).filter(Boolean) : []; + }; runKind = "grokVideo"; params = { mode, @@ -2627,6 +2648,14 @@ async function cvRunNode(nodeId, btn) { if (!first) return cvSetErr(nodeEl, "首帧图生视频需要首帧:连上游『文生图』/ 上传 / 手填公网URL"); params.image = first; } + if (mode === "reference") { + const refs = [up.image].concat(mats.refs || [], matUrls("refs")).filter(Boolean); + if (!refs.length) return cvSetErr(nodeEl, "多参考图模式请连接上游图片、上传参考图,或填写公网图片URL"); + params.referenceImages = refs; + params.model = "grok-imagine-video"; + params.resolution = "720p"; + params.aspectRatio = "16:9"; + } if (duration) params.seconds = duration; if (endpointPath) params.endpointPath = endpointPath; } else if (kind === "tts") { diff --git a/renderer/styles.css b/renderer/styles.css index 98aaac3..d1e0ee7 100644 --- a/renderer/styles.css +++ b/renderer/styles.css @@ -584,7 +584,7 @@ select.cv-field { resize: none; cursor: pointer; } width: 24px; height: 24px; font-size: 12px; line-height: 1; padding: 0; transition: .15s; } .cv-refimg-clear:hover, .cv-vmat-clear:hover { border-color: var(--danger); color: var(--danger); } /* URL 输入 / 提示 / 多图缩略图 各自独占一行 */ -.cv-vmat-url { flex: 1 1 100%; } +.cv-vmat-url, .cv-vmat-urls { flex: 1 1 100%; } .cv-refimg-row .cv-node-hint, .cv-vmat .cv-node-hint { flex: 1 1 100%; } .cv-vmat-refs-thumbs { display: flex; flex-wrap: wrap; gap: 2px; flex: 1 1 100%; } /* 复选标签(生成音频 / 水印 / 固定镜头)对齐 */