ac180ecad9
新增画板底部生成音乐入口,支持游戏音效和游戏背景音乐生成面板。 接入编辑器音频生成前后端契约、VectorEngine 请求与 BFF 路由。 补齐音频结果图层、元数据、画布恢复和客户端提交流程。 统一生成类面板 Lovart 式参数浮层、参考素材入口与相关测试。 更新编辑器音乐生成设计文档和项目长期记忆。
135 lines
3.6 KiB
TypeScript
135 lines
3.6 KiB
TypeScript
export function isImageFile(file: File) {
|
|
return file.type.startsWith('image/');
|
|
}
|
|
|
|
export const SEEDANCE_REFERENCE_ACCEPT = {
|
|
image:
|
|
'.jpg,.jpeg,.png,.webp,.bmp,.tiff,.tif,.gif,.heic,.heif,image/jpeg,image/png,image/webp,image/bmp,image/tiff,image/gif,image/heic,image/heif',
|
|
video: '.mp4,.mov,video/mp4,video/quicktime',
|
|
audio: '.wav,.mp3,audio/wav,audio/x-wav,audio/mpeg,audio/mp3',
|
|
} as const;
|
|
|
|
export function getEditorUploadAccept(target: string) {
|
|
if (target === 'video-reference-video') {
|
|
return SEEDANCE_REFERENCE_ACCEPT.video;
|
|
}
|
|
if (target === 'video-reference-audio') {
|
|
return SEEDANCE_REFERENCE_ACCEPT.audio;
|
|
}
|
|
if (target === 'video-reference-image') {
|
|
return SEEDANCE_REFERENCE_ACCEPT.image;
|
|
}
|
|
return 'image/*';
|
|
}
|
|
|
|
export const SEEDANCE_REFERENCE_FILE_LIMITS = {
|
|
imageBytes: 30 * 1024 * 1024,
|
|
videoBytes: 50 * 1024 * 1024,
|
|
audioBytes: 15 * 1024 * 1024,
|
|
mediaMinDurationSeconds: 2,
|
|
mediaMaxDurationSeconds: 15,
|
|
mediaTotalDurationSeconds: 15,
|
|
} as const;
|
|
|
|
const SEEDANCE_IMAGE_EXTENSIONS = new Set([
|
|
'jpg',
|
|
'jpeg',
|
|
'png',
|
|
'webp',
|
|
'bmp',
|
|
'tiff',
|
|
'tif',
|
|
'gif',
|
|
'heic',
|
|
'heif',
|
|
]);
|
|
const SEEDANCE_VIDEO_EXTENSIONS = new Set(['mp4', 'mov']);
|
|
const SEEDANCE_AUDIO_EXTENSIONS = new Set(['wav', 'mp3']);
|
|
|
|
function getFileExtension(file: File) {
|
|
return file.name.split('.').pop()?.toLowerCase() ?? '';
|
|
}
|
|
|
|
function hasAllowedExtension(file: File, extensions: Set<string>) {
|
|
return extensions.has(getFileExtension(file));
|
|
}
|
|
|
|
export function isSeedanceReferenceImageFile(file: File) {
|
|
return (
|
|
(file.type.startsWith('image/') || file.type === '') &&
|
|
hasAllowedExtension(file, SEEDANCE_IMAGE_EXTENSIONS)
|
|
);
|
|
}
|
|
|
|
export function isSeedanceReferenceVideoFile(file: File) {
|
|
return (
|
|
(file.type === 'video/mp4' ||
|
|
file.type === 'video/quicktime' ||
|
|
file.type === '') &&
|
|
hasAllowedExtension(file, SEEDANCE_VIDEO_EXTENSIONS)
|
|
);
|
|
}
|
|
|
|
export function isSeedanceReferenceAudioFile(file: File) {
|
|
return (
|
|
(file.type === 'audio/wav' ||
|
|
file.type === 'audio/x-wav' ||
|
|
file.type === 'audio/mpeg' ||
|
|
file.type === 'audio/mp3' ||
|
|
file.type === '') &&
|
|
hasAllowedExtension(file, SEEDANCE_AUDIO_EXTENSIONS)
|
|
);
|
|
}
|
|
|
|
export function readFileAsDataUrl(file: File) {
|
|
return new Promise<string>((resolve, reject) => {
|
|
const reader = new FileReader();
|
|
reader.onload = () => {
|
|
if (typeof reader.result === 'string') {
|
|
resolve(reader.result);
|
|
return;
|
|
}
|
|
reject(new Error('文件读取失败'));
|
|
};
|
|
reader.onerror = () => reject(reader.error ?? new Error('文件读取失败'));
|
|
reader.readAsDataURL(file);
|
|
});
|
|
}
|
|
|
|
export function readImageFileAsDataUrl(file: File) {
|
|
return readFileAsDataUrl(file);
|
|
}
|
|
|
|
export function probeMediaDurationSeconds(
|
|
file: File,
|
|
mediaType: 'video' | 'audio',
|
|
) {
|
|
return new Promise<number>((resolve, reject) => {
|
|
if (typeof document === 'undefined' || typeof URL === 'undefined') {
|
|
resolve(Number.NaN);
|
|
return;
|
|
}
|
|
const element =
|
|
mediaType === 'video'
|
|
? document.createElement('video')
|
|
: document.createElement('audio');
|
|
const objectUrl = URL.createObjectURL(file);
|
|
const cleanup = () => {
|
|
URL.revokeObjectURL(objectUrl);
|
|
element.removeAttribute('src');
|
|
element.load();
|
|
};
|
|
element.preload = 'metadata';
|
|
element.onloadedmetadata = () => {
|
|
const duration = element.duration;
|
|
cleanup();
|
|
resolve(duration);
|
|
};
|
|
element.onerror = () => {
|
|
cleanup();
|
|
reject(new Error('读取媒体时长失败'));
|
|
};
|
|
element.src = objectUrl;
|
|
});
|
|
}
|