fix: support video durations from 5 to 14 seconds
This commit is contained in:
@@ -99,7 +99,7 @@ npm start
|
|||||||
- 新版素材库只在列表中返回 `ready` 素材;`pending` / `failed` 通过单素材接口查询。
|
- 新版素材库只在列表中返回 `ready` 素材;`pending` / `failed` 通过单素材接口查询。
|
||||||
- 视频生成应长期保存素材库返回的 `asset://...` 引用,不要依赖原始图片 URL。
|
- 视频生成应长期保存素材库返回的 `asset://...` 引用,不要依赖原始图片 URL。
|
||||||
- 参考图/参考视频/首帧/首尾帧属于不同互斥调用方式;本 Demo 采用“多模态参考图”方式。
|
- 参考图/参考视频/首帧/首尾帧属于不同互斥调用方式;本 Demo 采用“多模态参考图”方式。
|
||||||
- 默认视频 720p,时长 4–15 秒;真人/数字人必须使用审核通过的 `mat_*` 素材 ID。
|
- 默认视频 720p,时长可选 5–14 秒整数;真人/数字人必须使用审核通过的 `mat_*` 素材 ID。
|
||||||
|
|
||||||
接口依据:[快快 AI Hub 最新文档](https://ai.kkidc.com/api-docs/8238027m0)。
|
接口依据:[快快 AI Hub 最新文档](https://ai.kkidc.com/api-docs/8238027m0)。
|
||||||
|
|
||||||
|
|||||||
@@ -456,6 +456,8 @@ function switchModule(module) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function createShot(seed = {}, index = state.shots.length) {
|
function createShot(seed = {}, index = state.shots.length) {
|
||||||
|
const seedDuration = Number(seed.duration);
|
||||||
|
const duration = Number.isFinite(seedDuration) ? Math.min(14, Math.max(5, Math.round(seedDuration))) : 5;
|
||||||
return {
|
return {
|
||||||
id: seed.id || `shot-${Date.now()}-${Math.random().toString(36).slice(2, 7)}`,
|
id: seed.id || `shot-${Date.now()}-${Math.random().toString(36).slice(2, 7)}`,
|
||||||
title: seed.title || (index === 0 ? "电梯口对话" : `片段 ${String(index + 1).padStart(2, "0")}`),
|
title: seed.title || (index === 0 ? "电梯口对话" : `片段 ${String(index + 1).padStart(2, "0")}`),
|
||||||
@@ -466,7 +468,7 @@ function createShot(seed = {}, index = state.shots.length) {
|
|||||||
roleIds: Array.isArray(seed.roleIds) ? [...seed.roleIds] : state.roles.map((role) => role.id),
|
roleIds: Array.isArray(seed.roleIds) ? [...seed.roleIds] : state.roles.map((role) => role.id),
|
||||||
model: seed.model || "doubao-seedance-2-0-260128",
|
model: seed.model || "doubao-seedance-2-0-260128",
|
||||||
ratio: seed.ratio || "9:16",
|
ratio: seed.ratio || "9:16",
|
||||||
duration: Number(seed.duration) || 5,
|
duration,
|
||||||
resolution: seed.resolution || "720p",
|
resolution: seed.resolution || "720p",
|
||||||
generateAudio: seed.generateAudio !== false,
|
generateAudio: seed.generateAudio !== false,
|
||||||
watermark: Boolean(seed.watermark),
|
watermark: Boolean(seed.watermark),
|
||||||
|
|||||||
@@ -204,9 +204,15 @@
|
|||||||
<span>时长</span>
|
<span>时长</span>
|
||||||
<select id="duration">
|
<select id="duration">
|
||||||
<option value="5">5 秒</option>
|
<option value="5">5 秒</option>
|
||||||
|
<option value="6">6 秒</option>
|
||||||
|
<option value="7">7 秒</option>
|
||||||
<option value="8">8 秒</option>
|
<option value="8">8 秒</option>
|
||||||
|
<option value="9">9 秒</option>
|
||||||
<option value="10">10 秒</option>
|
<option value="10">10 秒</option>
|
||||||
<option value="15">15 秒</option>
|
<option value="11">11 秒</option>
|
||||||
|
<option value="12">12 秒</option>
|
||||||
|
<option value="13">13 秒</option>
|
||||||
|
<option value="14">14 秒</option>
|
||||||
</select>
|
</select>
|
||||||
</label>
|
</label>
|
||||||
<label class="field">
|
<label class="field">
|
||||||
@@ -283,6 +289,6 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="toast hidden" id="toast"></div>
|
<div class="toast hidden" id="toast"></div>
|
||||||
<script src="./app.js?v=29"></script>
|
<script src="./app.js?v=30"></script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
10
server.mjs
10
server.mjs
@@ -534,10 +534,6 @@ function requireCustomerToken(request, taskId = "") {
|
|||||||
return token;
|
return token;
|
||||||
}
|
}
|
||||||
|
|
||||||
function clamp(value, min, max) {
|
|
||||||
return Math.min(max, Math.max(min, value));
|
|
||||||
}
|
|
||||||
|
|
||||||
function resolveResolution(model = "", requested = "720p") {
|
function resolveResolution(model = "", requested = "720p") {
|
||||||
return requested === "480p" ? "480p" : "720p";
|
return requested === "480p" ? "480p" : "720p";
|
||||||
}
|
}
|
||||||
@@ -562,6 +558,10 @@ function buildVideoPayload(body) {
|
|||||||
throw new ClientError("参考视频最多 3 条,且必须是公网可访问的 MP4/MOV URL");
|
throw new ClientError("参考视频最多 3 条,且必须是公网可访问的 MP4/MOV URL");
|
||||||
}
|
}
|
||||||
if (!String(body.prompt || "").trim()) throw new ClientError("请填写分镜提示词");
|
if (!String(body.prompt || "").trim()) throw new ClientError("请填写分镜提示词");
|
||||||
|
const duration = Number(body.duration);
|
||||||
|
if (!Number.isInteger(duration) || duration < 5 || duration > 14) {
|
||||||
|
throw new ClientError("视频时长必须是 5~14 秒之间的整数");
|
||||||
|
}
|
||||||
return {
|
return {
|
||||||
model: resolveVideoModel(body.model),
|
model: resolveVideoModel(body.model),
|
||||||
content: [
|
content: [
|
||||||
@@ -577,7 +577,7 @@ function buildVideoPayload(body) {
|
|||||||
role: "reference_video",
|
role: "reference_video",
|
||||||
})),
|
})),
|
||||||
],
|
],
|
||||||
duration: clamp(Number(body.duration || 5), 4, 15),
|
duration,
|
||||||
resolution: resolveResolution(body.model, body.resolution),
|
resolution: resolveResolution(body.model, body.resolution),
|
||||||
ratio: body.ratio || "9:16",
|
ratio: body.ratio || "9:16",
|
||||||
generate_audio: body.generateAudio !== false,
|
generate_audio: body.generateAudio !== false,
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import test from "node:test";
|
import test from "node:test";
|
||||||
import assert from "node:assert/strict";
|
import assert from "node:assert/strict";
|
||||||
|
import { readFileSync } from "node:fs";
|
||||||
import {
|
import {
|
||||||
extractVideoTaskId,
|
extractVideoTaskId,
|
||||||
extractVideoUrl,
|
extractVideoUrl,
|
||||||
@@ -24,3 +25,10 @@ test("兼容视频任务 id 和状态字段", () => {
|
|||||||
test("从常见响应位置提取视频地址", () => {
|
test("从常见响应位置提取视频地址", () => {
|
||||||
assert.equal(extractVideoUrl({ output: { video_url: "https://example.com/demo.mp4" } }), "https://example.com/demo.mp4");
|
assert.equal(extractVideoUrl({ output: { video_url: "https://example.com/demo.mp4" } }), "https://example.com/demo.mp4");
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test("生成时长提供 5 到 14 秒全部整数选项", () => {
|
||||||
|
const html = readFileSync(new URL("../public/index.html", import.meta.url), "utf8");
|
||||||
|
const durationSelect = html.match(/<select id="duration">([\s\S]*?)<\/select>/)?.[1] || "";
|
||||||
|
const values = [...durationSelect.matchAll(/<option value="(\d+)">/g)].map((match) => Number(match[1]));
|
||||||
|
assert.deepEqual(values, [5, 6, 7, 8, 9, 10, 11, 12, 13, 14]);
|
||||||
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user