diff --git a/README.md b/README.md
index e848e85..c1e0bbe 100644
--- a/README.md
+++ b/README.md
@@ -1,11 +1,12 @@
-# 营响力 AI 短剧数字人 Demo
+# 营响力 AI 视频与短剧生产 Demo
用于验证“数字人形象先进入快快网络素材库审核,再通过特别版 Seedance 2 生成短剧视频”的完整链路。
-页面拆分为三个一级模块:
+页面拆分为四个一级模块:
- **人物素材库**:负责素材入库、审核、历史素材查看与人物组合,是长期资产管理区。
- **通用素材库**:上传并审核公网图片或 MP4/MOV 视频,保留历史素材,并可作为当前片段的其他参考图片或参考视频。
+- **通用视频生成**:不创建角色和短剧片段,直接输入提示词,选择通用素材或补充公网图片/视频生成单条视频;也支持纯文本生成。
- **短剧生产**:以一个短剧项目管理多个片段;每段独立配置出场人物、提示词、图片、视频、连续性和生成参数。
在人物素材库点击“用于短剧生产”,会把该人物绑定到当前场景角色并自动进入生产模块。
@@ -18,7 +19,7 @@
2. 逐张调用 `POST /v1/assets/upload`,以客户输入的统一令牌提交人物素材审核;无需凑齐三张。
3. 对仍在审核的 `mat_*` 素材调用 `GET /v1/assets/{asset_id}`,直至本次图片全部 `ready` 或出现 `failed`。
4. 从全部历史素材中选择多张图片,绑定成可复用的人物/形象;三视图只是常见组合,不再是固定的数据结构。
-5. 为本场角色选择人物素材,并补充场景、道具等非人物图片及参考视频,组织成官方 `content` 数组后调用 `POST /sd/api/v3/contents/generations/tasks`。
+5. 通用生成可直接提交文本、图片与参考视频;短剧生产则为本场角色选择人物素材,并补充场景、道具等非人物图片及参考视频。两种模式都组织成官方 `content` 数组后调用 `POST /sd/api/v3/contents/generations/tasks`。
6. 用返回的 `id` 调用 `GET /sd/api/v3/contents/generations/tasks/{task_id}`,直至 `succeeded` 或 `failed`。
通用素材库同样调用 `POST /v1/assets/upload`:图片传入 `type: "image"`,视频传入 `type: "video"`。审核完成后的 `asset://mat_*` 引用可分别作为当前片段的其他参考图片或 `reference_video` 使用。通用图片用明确的素材用途标记与人物图片分开,避免进入人物组合区。
@@ -27,7 +28,7 @@
已完成的候选视频支持“下载视频”和“复制视频链接”。下载直接使用服务商返回的临时签名地址,不占用本 Demo 服务器的视频下载带宽;如果服务商没有返回附件响应头,浏览器可能先打开播放页,用户仍可保存。复制的是本 Demo 域名下以 `.mp4` 结尾的代理链接,可直接作为后续片段的公网参考视频;服务端会根据任务 ID 重新获取服务商的有效地址。
-短剧、片段、候选版本、任务 ID 和视频地址保存在当前域名的浏览器 `localStorage` 中。普通刷新不会丢失;清理网站数据、使用无痕模式、更换浏览器/设备/域名会丢失本地项目记录。在没有数据库和对象存储的当前版本中,仍建议用户及时下载成品。
+短剧、片段、候选版本、通用视频生成记录、任务 ID 和视频地址保存在当前域名的浏览器 `localStorage` 中。普通刷新不会丢失;清理网站数据、使用无痕模式、更换浏览器/设备/域名会丢失本地记录。在没有数据库和对象存储的当前版本中,仍建议用户及时下载成品。
## 不可见审计日志
diff --git a/public/app.js b/public/app.js
index 931360f..ee65766 100644
--- a/public/app.js
+++ b/public/app.js
@@ -15,6 +15,8 @@ const LEGACY_GROUP_KEY = "yingxiangli.selectedAssetGroup";
const SCENE_ROLES_KEY = "yingxiangli.sceneRoles.v1";
const CHARACTER_LIBRARY_KEY = "yingxiangli.characterLibrary.v1";
const DRAMA_PROJECT_KEY = "yingxiangli.dramaProject.v1";
+const GENERAL_VIDEO_STATE_KEY = "yingxiangli.generalVideo.v1";
+const PENDING_GENERAL_VIDEO_KEY = "yingxiangli.pendingGeneralVideo.v1";
const VIDEO_KEY_STORAGE = "yingxiangli.videoKey.v1";
const AUDIT_CLIENT_KEY = "yingxiangli.auditClientId.v1";
const AUDIT_SESSION_KEY = "yingxiangli.auditSessionId.v1";
@@ -41,6 +43,9 @@ const state = {
activeShotId: null,
activeGenerationShotId: null,
activeGenerationVersionId: null,
+ generalSelectedReferences: new Set(),
+ generalResults: [],
+ generalPollTimer: null,
};
const el = Object.fromEntries([
@@ -59,6 +64,10 @@ const el = Object.fromEntries([
"videoKeyGate", "videoKeyForm", "videoKeyInput", "videoKeyError", "changeVideoKeyButton", "cancelVideoKeyButton",
"genericAssetForm", "genericAssetType", "genericAssetUrl", "genericAssetPurpose", "genericAssetSubmit", "genericAssetStatus",
"genericAssetHint", "genericAssetResult", "genericAssetResultText", "genericAssetList", "genericAssetHistoryCount", "refreshGenericAssets",
+ "generalVideoForm", "generalVideoStatus", "generalPrompt", "generalAssetPicker", "refreshGeneralAssets",
+ "generalImageInputs", "generalVideoInputs", "addGeneralImageButton", "addGeneralVideoButton", "generalInputQuota",
+ "generalModel", "generalRatio", "generalDuration", "generalResolution", "generalGenerateAudio", "generalWatermark",
+ "generalGenerateButton", "generalResultCount", "generalResultList",
].map((id) => [id, document.getElementById(id)]));
const reviewRows = [el.reviewFront, el.reviewSide, el.reviewBack];
@@ -83,6 +92,7 @@ async function init() {
restoreSceneRoles();
restoreCharacterLibrary();
restoreDramaProject();
+ restoreGeneralVideoState();
renderConnection();
if (!state.config.mockMode && !currentVideoKey()) {
showVideoKeyGate(false);
@@ -95,6 +105,7 @@ async function init() {
resumePendingReview();
resumePendingGenericAsset();
if (state.config.mockMode || currentVideoKey()) resumePendingVideo();
+ if (state.config.mockMode || currentVideoKey()) resumePendingGeneralVideo();
} catch (error) {
showToast(error.message, true);
el.connectionState.textContent = "本地服务连接失败";
@@ -204,6 +215,16 @@ el.assetForm.addEventListener("submit", async (event) => {
el.genericAssetForm.addEventListener("submit", submitGenericAsset);
el.genericAssetType.addEventListener("change", updateGenericAssetForm);
+el.generalVideoForm.addEventListener("submit", submitGeneralVideo);
+el.refreshGeneralAssets.addEventListener("click", loadAssets);
+el.addGeneralImageButton.addEventListener("click", () => addGeneralMediaInput("image"));
+el.addGeneralVideoButton.addEventListener("click", () => addGeneralMediaInput("video"));
+[el.generalImageInputs, el.generalVideoInputs].forEach((list) => list.addEventListener("click", (event) => {
+ const button = event.target.closest("[data-remove-general-media]");
+ if (button) removeGeneralMediaInput(button);
+}));
+[el.generalPrompt, el.generalImageInputs, el.generalVideoInputs].forEach((input) => input.addEventListener("input", saveGeneralVideoState));
+[el.generalModel, el.generalRatio, el.generalDuration, el.generalResolution, el.generalGenerateAudio, el.generalWatermark].forEach((input) => input.addEventListener("change", saveGeneralVideoState));
el.videoForm.addEventListener("submit", async (event) => {
event.preventDefault();
@@ -327,6 +348,7 @@ async function loadAssets() {
renderCharacterLibrary();
renderAssetPicker();
renderGenericAssetLibrary();
+ renderGeneralAssetPicker();
el.assetHistoryCount.textContent = String(state.assets.length);
el.assetList.innerHTML = "";
if (!state.assets.length) {
@@ -446,6 +468,379 @@ function renderGenericAssetLibrary() {
}
}
+function restoreGeneralVideoState() {
+ let saved;
+ try { saved = JSON.parse(localStorage.getItem(GENERAL_VIDEO_STATE_KEY) || "null"); } catch { saved = null; }
+ state.generalSelectedReferences = new Set(Array.isArray(saved?.selectedReferences) ? saved.selectedReferences.map(String) : []);
+ state.generalResults = Array.isArray(saved?.results) ? saved.results.slice(0, 20) : [];
+ el.generalPrompt.value = String(saved?.prompt || "");
+ el.generalModel.value = saved?.model || "doubao-seedance-2-0-260128";
+ el.generalRatio.value = saved?.ratio || "9:16";
+ el.generalDuration.value = String(Math.min(14, Math.max(5, Number(saved?.duration) || 5)));
+ el.generalResolution.value = saved?.resolution === "480p" ? "480p" : "720p";
+ el.generalGenerateAudio.checked = saved?.generateAudio !== false;
+ el.generalWatermark.checked = Boolean(saved?.watermark);
+ renderGeneralMediaInputs("image", saved?.imageUrls || "");
+ renderGeneralMediaInputs("video", saved?.videoUrls || "");
+ renderGeneralResults();
+ updateGeneralQuota();
+}
+
+function saveGeneralVideoState() {
+ localStorage.setItem(GENERAL_VIDEO_STATE_KEY, JSON.stringify({
+ prompt: el.generalPrompt.value,
+ selectedReferences: [...state.generalSelectedReferences],
+ imageUrls: collectGeneralMediaUrls("image").join("\n"),
+ videoUrls: collectGeneralMediaUrls("video").join("\n"),
+ model: el.generalModel.value,
+ ratio: el.generalRatio.value,
+ duration: Number(el.generalDuration.value),
+ resolution: el.generalResolution.value,
+ generateAudio: el.generalGenerateAudio.checked,
+ watermark: el.generalWatermark.checked,
+ results: state.generalResults.slice(0, 20),
+ }));
+ updateGeneralQuota();
+}
+
+function renderGeneralAssetPicker() {
+ if (!el.generalAssetPicker) return;
+ const available = new Set(state.genericAssets.map(normalizeAssetReference));
+ for (const reference of [...state.generalSelectedReferences]) {
+ if (!available.has(reference)) state.generalSelectedReferences.delete(reference);
+ }
+ el.generalAssetPicker.innerHTML = "";
+ if (!state.genericAssets.length) {
+ el.generalAssetPicker.innerHTML = '
通用素材库暂无可用图片或视频
';
+ updateGeneralQuota();
+ return;
+ }
+ for (const item of state.genericAssets) {
+ const type = assetItemType(item);
+ const reference = normalizeAssetReference(item);
+ const sourceUrl = item.original_url || item.URL || "";
+ const name = genericAssetName(item, sourceUrl);
+ const button = document.createElement("button");
+ button.type = "button";
+ button.className = `general-asset-choice${state.generalSelectedReferences.has(reference) ? " selected" : ""}`;
+ const preview = document.createElement("span");
+ preview.className = "general-asset-choice-preview";
+ if (sourceUrl && type === "image") {
+ const image = document.createElement("img");
+ image.src = sourceUrl;
+ image.alt = name;
+ image.loading = "lazy";
+ preview.appendChild(image);
+ } else if (sourceUrl && type === "video") {
+ const video = document.createElement("video");
+ video.src = sourceUrl;
+ video.muted = true;
+ video.playsInline = true;
+ video.preload = "metadata";
+ preview.appendChild(video);
+ } else preview.textContent = type === "video" ? "视频" : "图片";
+ const copy = document.createElement("span");
+ copy.className = "general-asset-choice-copy";
+ copy.innerHTML = `${escapeHtml(name)}${type === "video" ? "视频" : "图片"}`;
+ button.append(preview, copy);
+ button.addEventListener("click", () => toggleGeneralAsset(reference, type));
+ el.generalAssetPicker.appendChild(button);
+ }
+ updateGeneralQuota();
+}
+
+function toggleGeneralAsset(reference, type) {
+ if (state.generalSelectedReferences.has(reference)) state.generalSelectedReferences.delete(reference);
+ else {
+ const quota = generalMediaQuota(reference, type);
+ if (!quota.valid) return showToast(quota.message, true);
+ state.generalSelectedReferences.add(reference);
+ }
+ saveGeneralVideoState();
+ renderGeneralAssetPicker();
+}
+
+function generalMediaInputConfig(type) {
+ return type === "video"
+ ? { list: el.generalVideoInputs, button: el.addGeneralVideoButton, limit: 3, placeholder: "公网 MP4/MOV 或 asset:// 素材引用" }
+ : { list: el.generalImageInputs, button: el.addGeneralImageButton, limit: 9, placeholder: "公网图片 URL 或 asset:// 素材引用" };
+}
+
+function createGeneralMediaInput(type, value = "") {
+ const { placeholder } = generalMediaInputConfig(type);
+ const row = document.createElement("div");
+ row.className = "media-url-row";
+ const input = document.createElement("input");
+ input.type = "text";
+ input.value = value;
+ input.placeholder = placeholder;
+ input.setAttribute("aria-label", type === "video" ? "通用生成参考视频" : "通用生成参考图片");
+ const remove = document.createElement("button");
+ remove.type = "button";
+ remove.className = "media-remove-button";
+ remove.dataset.removeGeneralMedia = type;
+ remove.setAttribute("aria-label", "删除该链接");
+ remove.textContent = "×";
+ row.append(input, remove);
+ return row;
+}
+
+function renderGeneralMediaInputs(type, value) {
+ const { list } = generalMediaInputConfig(type);
+ const values = parseMediaLines(value);
+ list.replaceChildren(...(values.length ? values : [""]).map((item) => createGeneralMediaInput(type, item)));
+ refreshGeneralMediaButton(type);
+}
+
+function collectGeneralMediaUrls(type) {
+ const { list } = generalMediaInputConfig(type);
+ return [...new Set([...list.querySelectorAll("input")].map((input) => input.value.trim()).filter(Boolean))];
+}
+
+function addGeneralMediaInput(type) {
+ const { list, limit } = generalMediaInputConfig(type);
+ if (list.children.length >= limit) return;
+ const row = createGeneralMediaInput(type);
+ list.appendChild(row);
+ row.querySelector("input").focus();
+ refreshGeneralMediaButton(type);
+}
+
+function removeGeneralMediaInput(button) {
+ const type = button.dataset.removeGeneralMedia;
+ const { list } = generalMediaInputConfig(type);
+ if (list.children.length === 1) list.querySelector("input").value = "";
+ else button.closest(".media-url-row").remove();
+ refreshGeneralMediaButton(type);
+ saveGeneralVideoState();
+}
+
+function refreshGeneralMediaButton(type) {
+ const { list, button, limit } = generalMediaInputConfig(type);
+ button.disabled = list.children.length >= limit;
+}
+
+function selectedGeneralLibraryMedia() {
+ const selected = state.genericAssets.filter((item) => state.generalSelectedReferences.has(normalizeAssetReference(item)));
+ return {
+ images: selected.filter((item) => assetItemType(item) === "image").map(normalizeAssetReference),
+ videos: selected.filter((item) => assetItemType(item) === "video").map(normalizeAssetReference),
+ };
+}
+
+function generalMediaQuota(pendingReference = "", pendingType = "") {
+ const selected = selectedGeneralLibraryMedia();
+ const imageReferences = new Set([...selected.images, ...collectGeneralMediaUrls("image")]);
+ const videoReferences = new Set([...selected.videos, ...collectGeneralMediaUrls("video")]);
+ if (pendingReference && !state.generalSelectedReferences.has(pendingReference)) {
+ if (pendingType === "video") videoReferences.add(pendingReference);
+ else imageReferences.add(pendingReference);
+ }
+ const imageCount = imageReferences.size;
+ const videoCount = videoReferences.size;
+ if (imageCount > 9) return { valid: false, imageCount, videoCount, message: `参考图片 ${imageCount}/9,已超出上限` };
+ if (videoCount > 3) return { valid: false, imageCount, videoCount, message: `参考视频 ${videoCount}/3,已超出上限` };
+ return { valid: true, imageCount, videoCount, message: "可提交" };
+}
+
+function updateGeneralQuota() {
+ if (!el.generalInputQuota) return;
+ const quota = generalMediaQuota();
+ el.generalInputQuota.textContent = `参考图片 ${quota.imageCount}/9 · 参考视频 ${quota.videoCount}/3`;
+ el.generalInputQuota.className = `input-quota wide ${quota.valid ? "valid" : "invalid"}`;
+}
+
+async function submitGeneralVideo(event) {
+ event.preventDefault();
+ const prompt = el.generalPrompt.value.trim();
+ if (!prompt) return showToast("请填写视频提示词", true);
+ const quota = generalMediaQuota();
+ if (!quota.valid) return showToast(quota.message, true);
+ const selected = selectedGeneralLibraryMedia();
+ const images = [...new Set([...selected.images, ...collectGeneralMediaUrls("image")])];
+ const videos = [...new Set([...selected.videos, ...collectGeneralMediaUrls("video")])];
+ const result = {
+ id: `general-${Date.now()}-${Math.random().toString(36).slice(2, 7)}`,
+ status: "running",
+ taskId: "",
+ videoUrl: "",
+ error: "",
+ ratio: el.generalRatio.value,
+ createdAt: Date.now(),
+ lastCheckedAt: null,
+ progress: 5,
+ };
+ state.generalResults.unshift(result);
+ state.generalResults = state.generalResults.slice(0, 20);
+ el.generalGenerateButton.disabled = true;
+ setGeneralVideoStatus("processing", "生成中");
+ saveGeneralVideoState();
+ renderGeneralResults();
+ try {
+ const response = await api("/api/videos", {
+ method: "POST",
+ body: JSON.stringify({
+ assetReferences: [],
+ extraImageReferences: images,
+ videoReferences: videos,
+ prompt,
+ model: el.generalModel.value,
+ ratio: el.generalRatio.value,
+ duration: Number(el.generalDuration.value),
+ resolution: el.generalResolution.value,
+ generateAudio: el.generalGenerateAudio.checked,
+ watermark: el.generalWatermark.checked,
+ }),
+ });
+ const taskId = response.task_id || response.id || response.data?.task_id || response.data?.id;
+ if (!taskId) throw new Error("视频接口未返回任务 ID");
+ result.taskId = taskId;
+ result.mock = Boolean(response.mock);
+ localStorage.setItem(PENDING_GENERAL_VIDEO_KEY, JSON.stringify({ taskId, resultId: result.id, startedAt: result.createdAt }));
+ saveGeneralVideoState();
+ renderGeneralResults();
+ await pollGeneralVideoTask(taskId, result.id);
+ } catch (error) {
+ const message = contextualizeGenerationError(error.message);
+ if (error.pending) {
+ setGeneralVideoStatus("processing", "仍在生成");
+ showToast(message);
+ } else {
+ result.status = "failed";
+ result.error = message;
+ localStorage.removeItem(PENDING_GENERAL_VIDEO_KEY);
+ setGeneralVideoStatus("error", "生成失败");
+ showToast(message, true);
+ }
+ } finally {
+ saveGeneralVideoState();
+ renderGeneralResults();
+ el.generalGenerateButton.disabled = state.generalResults.some((item) => item.status === "running");
+ }
+}
+
+async function pollGeneralVideoTask(taskId, resultId) {
+ const startedAt = Date.now();
+ while (Date.now() - startedAt < VIDEO_POLL_MAX_MS) {
+ await delay(VIDEO_POLL_INTERVAL_MS);
+ const response = await api(`/api/videos/${encodeURIComponent(taskId)}`);
+ const status = response._normalized?.status || normalizeVideoStatus(response.status || response.data?.status);
+ const result = state.generalResults.find((item) => item.id === resultId);
+ if (!result) return;
+ result.taskId = taskId;
+ result.lastCheckedAt = Date.now();
+ result.progress = Math.max(10, Number(response.progress ?? response.data?.progress ?? (status === "completed" ? 100 : 20)));
+ result.mock = result.mock || Boolean(response.mock);
+ saveGeneralVideoState();
+ renderGeneralResults();
+ if (status === "completed") {
+ result.status = "completed";
+ result.progress = 100;
+ result.videoUrl = response._normalized?.videoUrl || response.video_url || response.output?.video_url || response.data?.video_url || "";
+ localStorage.removeItem(PENDING_GENERAL_VIDEO_KEY);
+ setGeneralVideoStatus("success", "生成完成");
+ saveGeneralVideoState();
+ renderGeneralResults();
+ showToast("通用视频已生成");
+ return;
+ }
+ if (status === "failed") throw new Error(response.error?.message || response.message || "视频生成失败");
+ }
+ const error = new Error("视频仍在服务商侧生成,任务已保存;稍后重新打开页面会继续查询");
+ error.pending = true;
+ throw error;
+}
+
+async function resumePendingGeneralVideo() {
+ let pending;
+ try { pending = JSON.parse(localStorage.getItem(PENDING_GENERAL_VIDEO_KEY) || "null"); } catch { pending = null; }
+ if (!pending?.taskId) return;
+ switchModule("general-video");
+ let result = state.generalResults.find((item) => item.id === pending.resultId);
+ if (!result) {
+ result = { id: pending.resultId || `general-${Date.now()}`, status: "running", taskId: pending.taskId, videoUrl: "", error: "", ratio: "9:16", createdAt: pending.startedAt || Date.now(), lastCheckedAt: null, progress: 20 };
+ state.generalResults.unshift(result);
+ }
+ result.status = "running";
+ el.generalGenerateButton.disabled = true;
+ setGeneralVideoStatus("processing", "继续查询");
+ renderGeneralResults();
+ try {
+ await pollGeneralVideoTask(pending.taskId, result.id);
+ } catch (error) {
+ if (error.pending) {
+ setGeneralVideoStatus("processing", "仍在生成");
+ showToast(error.message);
+ } else {
+ result.status = "failed";
+ result.error = contextualizeGenerationError(error.message);
+ localStorage.removeItem(PENDING_GENERAL_VIDEO_KEY);
+ setGeneralVideoStatus("error", "生成失败");
+ showToast(result.error, true);
+ }
+ } finally {
+ saveGeneralVideoState();
+ renderGeneralResults();
+ el.generalGenerateButton.disabled = state.generalResults.some((item) => item.status === "running");
+ }
+}
+
+function renderGeneralResults() {
+ if (!el.generalResultList) return;
+ el.generalResultCount.textContent = state.generalResults.length ? `${state.generalResults.length} 条记录` : "暂无记录";
+ el.generalResultList.innerHTML = "";
+ if (!state.generalResults.length) {
+ el.generalResultList.innerHTML = '提交任务后,生成状态和视频会出现在这里
';
+ return;
+ }
+ state.generalResults.forEach((result, index) => {
+ const card = document.createElement("div");
+ card.className = "version-card";
+ const preview = document.createElement("div");
+ preview.className = "version-preview";
+ setVersionPreviewRatio(preview, result.ratio || "16:9");
+ if (result.videoUrl) {
+ const video = document.createElement("video");
+ video.src = result.videoUrl;
+ video.controls = true;
+ video.playsInline = true;
+ video.preload = "metadata";
+ video.addEventListener("loadedmetadata", () => {
+ if (video.videoWidth && video.videoHeight) setVersionPreviewRatio(preview, `${video.videoWidth}:${video.videoHeight}`);
+ }, { once: true });
+ preview.appendChild(video);
+ } else preview.textContent = result.status === "running" ? `${result.progress || 20}% · 生成中` : result.status === "completed" ? "流程已完成" : "生成失败";
+ const copy = document.createElement("div");
+ copy.className = "version-copy";
+ const statusText = result.status === "completed" ? "已完成" : result.status === "running" ? `生成中 · 已等待 ${formatElapsed(result.createdAt)}` : "失败";
+ copy.innerHTML = `视频 ${state.generalResults.length - index} · ${statusText}${escapeHtml(result.taskId || result.error || "正在获取任务 ID")}`;
+ const actions = document.createElement("div");
+ actions.className = "version-actions";
+ if (result.status === "completed" && result.taskId && !result.mock) {
+ const download = document.createElement("a");
+ download.className = "video-action-link";
+ download.href = result.videoUrl || videoProxyUrl(result.taskId);
+ download.textContent = "下载视频";
+ download.setAttribute("download", "");
+ download.target = "_blank";
+ download.rel = "noopener";
+ const copyLink = document.createElement("button");
+ copyLink.type = "button";
+ copyLink.className = "video-action-link";
+ copyLink.textContent = "复制视频链接";
+ copyLink.addEventListener("click", () => copyGeneratedVideoLink(result.taskId));
+ actions.append(download, copyLink);
+ }
+ card.append(preview, copy, actions);
+ el.generalResultList.appendChild(card);
+ });
+}
+
+function setGeneralVideoStatus(kind, text) {
+ el.generalVideoStatus.className = `status-badge ${kind}`;
+ el.generalVideoStatus.textContent = text;
+}
+
function useGenericAssetInActiveShot(reference, name, type) {
const shot = activeShot();
if (type === "video") {
@@ -629,7 +1024,7 @@ function bindCharacterToActiveRole(character) {
}
function switchModule(module) {
- if (!["library", "generic-library", "production"].includes(module)) return;
+ if (!["library", "generic-library", "general-video", "production"].includes(module)) return;
state.activeModule = module;
document.body.dataset.activeModule = module;
document.querySelectorAll('.module-tab').forEach((button) => {
diff --git a/public/index.html b/public/index.html
index 900ef47..7bf12a1 100644
--- a/public/index.html
+++ b/public/index.html
@@ -3,14 +3,14 @@
- 营响力|AI 短剧数字人 Demo
-
+ 营响力|AI 视频与短剧生产
+