feat: support grok reference video mode
This commit is contained in:
@@ -60,6 +60,7 @@ canvasRun: (kind, params) => ipcRenderer.invoke("canvas-run", { kind, params }),
|
|||||||
- `model`:用户填写的视频模型(为空返回明确错误)。
|
- `model`:用户填写的视频模型(为空返回明确错误)。
|
||||||
- body 组装:`{ model, prompt, ...(mode==='image'?{image}:{}) , size, ratio, duration, ...extra }`(空字段不塞)。
|
- body 组装:`{ model, prompt, ...(mode==='image'?{image}:{}) , size, ratio, duration, ...extra }`(空字段不塞)。
|
||||||
- `endpointPath` 默认 `/v1/videos`,允许覆盖。仍严禁伪造成功。
|
- `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`。
|
- **tts** 增加 `model`(默认 `tts-1` 可保留但允许覆盖)、`format`、可选 `speed`。
|
||||||
|
|
||||||
### 新增 canvasModels(后端 + preload)
|
### 新增 canvasModels(后端 + preload)
|
||||||
|
|||||||
+11
-2
@@ -434,7 +434,7 @@ async function runVideo(base, llm, params) {
|
|||||||
|
|
||||||
// —— Grok 视频:独立新增接口,按 /v1/videos/generations + /v1/videos/{request_id} 调用 ——
|
// —— Grok 视频:独立新增接口,按 /v1/videos/generations + /v1/videos/{request_id} 调用 ——
|
||||||
async function runGrokVideo(base, llm, params) {
|
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();
|
const prompt = (params.prompt || "").trim();
|
||||||
if (!prompt) return { ok: false, error: "请填写 Grok 视频提示词" };
|
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);
|
const img = await resolveMat(params.imageUrl || params.image);
|
||||||
if (!img) return { ok: false, error: "Grok 图生视频需要首帧图:上传 / 手填公网URL / 连接上游文生图" };
|
if (!img) return { ok: false, error: "Grok 图生视频需要首帧图:上传 / 手填公网URL / 连接上游文生图" };
|
||||||
body.image = { image_url: img };
|
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") {
|
} else if (mode !== "text") {
|
||||||
return { ok: false, error: "Grok 视频新增接口只支持「文生视频」和「首帧图生视频」" };
|
return { ok: false, error: "Grok 视频新增接口只支持「文生视频」「首帧图生视频」和「多参考图」" };
|
||||||
}
|
}
|
||||||
|
|
||||||
const createUrl = joinUrl(base, params.endpointPath || "/v1/videos/generations");
|
const createUrl = joinUrl(base, params.endpointPath || "/v1/videos/generations");
|
||||||
|
|||||||
+30
-1
@@ -1775,6 +1775,7 @@ function cvNodeBody(kind) {
|
|||||||
<select class="cv-field cv-field-vmode" title="模式">
|
<select class="cv-field cv-field-vmode" title="模式">
|
||||||
<option value="text">文生视频</option>
|
<option value="text">文生视频</option>
|
||||||
<option value="first">首帧图生视频</option>
|
<option value="first">首帧图生视频</option>
|
||||||
|
<option value="reference">多参考图</option>
|
||||||
</select>
|
</select>
|
||||||
<textarea class="cv-field cv-field-prompt" rows="3" placeholder="Grok 视频提示词(可连接上游文本节点)"></textarea>
|
<textarea class="cv-field cv-field-prompt" rows="3" placeholder="Grok 视频提示词(可连接上游文本节点)"></textarea>
|
||||||
<div class="cv-vmat cv-vmat-first hidden">
|
<div class="cv-vmat cv-vmat-first hidden">
|
||||||
@@ -1785,6 +1786,14 @@ function cvNodeBody(kind) {
|
|||||||
<button type="button" class="cv-vmat-clear hidden" data-slot="first" title="移除">✕</button>
|
<button type="button" class="cv-vmat-clear hidden" data-slot="first" title="移除">✕</button>
|
||||||
<input class="cv-field cv-vmat-url" data-slot="first" placeholder="或手填公网图片URL;留空用上游图" />
|
<input class="cv-field cv-vmat-url" data-slot="first" placeholder="或手填公网图片URL;留空用上游图" />
|
||||||
</div>
|
</div>
|
||||||
|
<div class="cv-vmat cv-vmat-refs hidden">
|
||||||
|
<label class="cv-refimg-pick" title="批量上传参考图">📎 参考图
|
||||||
|
<input type="file" class="cv-vmat-file" data-slot="refs" accept="image/*" multiple hidden />
|
||||||
|
</label>
|
||||||
|
<button type="button" class="cv-vmat-clear hidden" data-slot="refs" title="清空">✕</button>
|
||||||
|
<div class="cv-vmat-refs-thumbs"></div>
|
||||||
|
<textarea class="cv-field cv-vmat-urls" data-slot="refs" rows="2" placeholder="可选:每行一个公网图片URL;会和上传图一起作为参考"></textarea>
|
||||||
|
</div>
|
||||||
<select class="cv-field cv-field-group" title="中转分组"></select>
|
<select class="cv-field cv-field-group" title="中转分组"></select>
|
||||||
<div class="cv-model-row">
|
<div class="cv-model-row">
|
||||||
<input class="cv-field cv-field-model" value="grok-imagine-video" placeholder="grok-imagine-video / grok-imagine-video-1.5" />
|
<input class="cv-field cv-field-model" value="grok-imagine-video" placeholder="grok-imagine-video / grok-imagine-video-1.5" />
|
||||||
@@ -1804,7 +1813,7 @@ function cvNodeBody(kind) {
|
|||||||
<option value="15">15s</option>
|
<option value="15">15s</option>
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
<div class="cv-node-hint cv-grok-hint">1.5 的 1080p/15s 需要首帧图;完成前可能在 94% 停留较久</div>
|
<div class="cv-node-hint cv-grok-hint">多参考图使用 grok-imagine-video + 720p;1.5 的 1080p/15s 需要首帧图</div>
|
||||||
<details class="cv-adv"><summary>⚙️ 高级</summary>
|
<details class="cv-adv"><summary>⚙️ 高级</summary>
|
||||||
<input class="cv-field cv-field-endpoint" placeholder="/v1/videos/generations" />
|
<input class="cv-field cv-field-endpoint" placeholder="/v1/videos/generations" />
|
||||||
</details>
|
</details>
|
||||||
@@ -2087,6 +2096,8 @@ function cvClearVideoMat(nodeEl, slot) {
|
|||||||
if (slot === "refs") {
|
if (slot === "refs") {
|
||||||
const box = nodeEl.querySelector(".cv-vmat-refs-thumbs");
|
const box = nodeEl.querySelector(".cv-vmat-refs-thumbs");
|
||||||
if (box) box.innerHTML = "";
|
if (box) box.innerHTML = "";
|
||||||
|
const urls = nodeEl.querySelector('.cv-vmat-urls[data-slot="refs"]');
|
||||||
|
if (urls) urls.value = "";
|
||||||
} else {
|
} else {
|
||||||
const thumb = nodeEl.querySelector(`.cv-vmat-thumb[data-slot="${slot}"]`);
|
const thumb = nodeEl.querySelector(`.cv-vmat-thumb[data-slot="${slot}"]`);
|
||||||
if (thumb) {
|
if (thumb) {
|
||||||
@@ -2305,6 +2316,12 @@ function cvSyncVideoMode(nodeEl) {
|
|||||||
const box = nodeEl.querySelector(".cv-vmat-" + slot);
|
const box = nodeEl.querySelector(".cv-vmat-" + slot);
|
||||||
if (box) box.classList.toggle("hidden", !show[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
|
// 读取单个节点的「输出产物」:文本节点实时读 textarea,其它读运行后缓存的 dataUrl
|
||||||
@@ -2606,6 +2623,10 @@ async function cvRunNode(nodeId, btn) {
|
|||||||
const el = nodeEl.querySelector(`.cv-vmat-url[data-slot="${slot}"]`);
|
const el = nodeEl.querySelector(`.cv-vmat-url[data-slot="${slot}"]`);
|
||||||
return el ? el.value.trim() : "";
|
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";
|
runKind = "grokVideo";
|
||||||
params = {
|
params = {
|
||||||
mode,
|
mode,
|
||||||
@@ -2627,6 +2648,14 @@ async function cvRunNode(nodeId, btn) {
|
|||||||
if (!first) return cvSetErr(nodeEl, "首帧图生视频需要首帧:连上游『文生图』/ 上传 / 手填公网URL");
|
if (!first) return cvSetErr(nodeEl, "首帧图生视频需要首帧:连上游『文生图』/ 上传 / 手填公网URL");
|
||||||
params.image = first;
|
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 (duration) params.seconds = duration;
|
||||||
if (endpointPath) params.endpointPath = endpointPath;
|
if (endpointPath) params.endpointPath = endpointPath;
|
||||||
} else if (kind === "tts") {
|
} else if (kind === "tts") {
|
||||||
|
|||||||
+1
-1
@@ -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; }
|
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); }
|
.cv-refimg-clear:hover, .cv-vmat-clear:hover { border-color: var(--danger); color: var(--danger); }
|
||||||
/* URL 输入 / 提示 / 多图缩略图 各自独占一行 */
|
/* 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-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%; }
|
.cv-vmat-refs-thumbs { display: flex; flex-wrap: wrap; gap: 2px; flex: 1 1 100%; }
|
||||||
/* 复选标签(生成音频 / 水印 / 固定镜头)对齐 */
|
/* 复选标签(生成音频 / 水印 / 固定镜头)对齐 */
|
||||||
|
|||||||
Reference in New Issue
Block a user