ai-chatbot/lib/video-canvas/video-download.ts

102 lines
4.5 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

export const VIDEO_WATERMARK_RULES_STORAGE_KEY = "video-canvas-watermark-rules-v1";
export type VideoCanvasVideoDownloadConfig = {
acknowledgementText: string;
applicationText: string;
responsibilityText: string;
rulesLinkLabel: string;
rulesTitle: string;
rulesUrl: string;
welcomeText: string;
};
export const defaultVideoCanvasVideoDownloadConfig: VideoCanvasVideoDownloadConfig = {
acknowledgementText:
'开启本功能开关并点击 "保存设置" 按钮,即视为您已充分知悉并同意本规则全部内容。后续您可通过 "头像 - AI水印设置" 路径调整本功能配置。',
applicationText:
'您申请我们向您提供没有添加显式标识(以下或称"水印")的AI生成合成内容之前,亦即您使用"去水印"功能之前,请您务必认真阅读并充分理解本规则。您知悉、同意并遵守以下内容的,方可使用本功能。',
responsibilityText:
"经您提出申请,{{siteName}}平台可向您提供未添加前述显式标识的AI生成合成内容。请注意:您在使用网络信息内容传播服务发布该等AI生成合成内容时,需主动作出声明,并通过传播{{siteName}}平台提供的标识功能完成标识操作。您确认已知晓并承诺:若您未依据法律法规要求在AI生成合成内容中添加显式标识,进而引发公众混淆或误认,由此产生的全部后果及法律责任均由您自行承担。更详细的说明见",
rulesLinkLabel: "{{siteName}}水印规范",
rulesTitle: "AI 生成内容水印管理规则",
rulesUrl: "https://resonate.feishu.cn/wiki/",
welcomeText: "欢迎您使用{{siteName}}的产品和服务!",
};
function normalizeDownloadCopy(value: unknown, fallback: string, maxLength: number) {
const normalized = typeof value === "string" ? value.trim().slice(0, maxLength) : "";
return normalized || fallback;
}
function normalizeRulesUrl(value: unknown, fallback: string) {
const candidate = typeof value === "string" ? value.trim().slice(0, 2048) : "";
if (candidate.startsWith("/") && !candidate.startsWith("//")) return candidate;
try {
const url = new URL(candidate);
return url.protocol === "http:" || url.protocol === "https:" ? candidate : fallback;
} catch {
return fallback;
}
}
export function normalizeVideoCanvasVideoDownloadConfig(
value: unknown,
): VideoCanvasVideoDownloadConfig {
const incoming = value && typeof value === "object" && !Array.isArray(value)
? value as Record<string, unknown>
: {};
const fallback = defaultVideoCanvasVideoDownloadConfig;
return {
acknowledgementText: normalizeDownloadCopy(
incoming.acknowledgementText,
fallback.acknowledgementText,
2_000,
),
applicationText: normalizeDownloadCopy(incoming.applicationText, fallback.applicationText, 4_000),
responsibilityText: normalizeDownloadCopy(
incoming.responsibilityText,
fallback.responsibilityText,
6_000,
),
rulesLinkLabel: normalizeDownloadCopy(incoming.rulesLinkLabel, fallback.rulesLinkLabel, 120),
rulesTitle: normalizeDownloadCopy(incoming.rulesTitle, fallback.rulesTitle, 120),
rulesUrl: normalizeRulesUrl(incoming.rulesUrl, fallback.rulesUrl),
welcomeText: normalizeDownloadCopy(incoming.welcomeText, fallback.welcomeText, 500),
};
}
export function resolveVideoDownloadRuleCopy(value: string, siteName: string) {
return value.replaceAll("{{siteName}}", siteName.trim() || "AI平台");
}
const VIDEO_EXTENSION_BY_CONTENT_TYPE: Record<string, string> = {
"video/mp4": "mp4",
"video/quicktime": "mov",
"video/webm": "webm",
};
export function sanitizeVideoDownloadTitle(value: string) {
return (value.trim() || "视频").replace(/[\\/:*?"<>|]+/g, "-").slice(0, 80);
}
export function getVideoDownloadExtension(source: string, contentType = "") {
const normalizedContentType = contentType.split(";", 1)[0]?.trim().toLowerCase();
if (normalizedContentType && VIDEO_EXTENSION_BY_CONTENT_TYPE[normalizedContentType]) {
return VIDEO_EXTENSION_BY_CONTENT_TYPE[normalizedContentType];
}
try {
const pathname = new URL(source, "https://canvas.local").pathname;
const extension = pathname.match(/\.([a-z0-9]{2,5})$/i)?.[1]?.toLowerCase();
if (extension && ["mp4", "mov", "m4v", "webm"].includes(extension)) return extension;
} catch {
// Fall through to the interoperable video default.
}
return "mp4";
}
export function getVideoDownloadFilename(title: string, source: string, contentType = "") {
return `${sanitizeVideoDownloadTitle(title)}.${getVideoDownloadExtension(source, contentType)}`;
}