144 lines
4.8 KiB
TypeScript
144 lines
4.8 KiB
TypeScript
import React, { useState } from 'react';
|
|
import { Button, Input, Modal, Space, Upload, message } from 'antd';
|
|
import { UploadOutlined } from '@ant-design/icons';
|
|
import type { UploadFile } from 'antd/es/upload/interface';
|
|
import { createPrivatePortraitAsset, uploadPrivatePortraitImage, uploadPrivatePortraitVideo } from '../../../api';
|
|
|
|
const MIN_PRIVATE_VIDEO_DURATION = 2;
|
|
const MAX_PRIVATE_VIDEO_DURATION = 15;
|
|
|
|
interface Props {
|
|
projectId: string;
|
|
open: boolean;
|
|
onClose: () => void;
|
|
onSuccess: () => void;
|
|
}
|
|
|
|
const guessAssetType = (file?: File | null): 'Image' | 'Video' => {
|
|
if (!file) return 'Image';
|
|
if (file.type.startsWith('video/')) return 'Video';
|
|
const name = file.name.toLowerCase();
|
|
if (/\.(mp4|mov|webm|m4v|avi|mkv)$/.test(name)) return 'Video';
|
|
return 'Image';
|
|
};
|
|
|
|
const getVideoDuration = (file: File): Promise<number | null> => {
|
|
return new Promise((resolve) => {
|
|
if (!file.type.startsWith('video/')) {
|
|
resolve(null);
|
|
return;
|
|
}
|
|
const url = URL.createObjectURL(file);
|
|
const video = document.createElement('video');
|
|
video.preload = 'metadata';
|
|
video.onloadedmetadata = () => {
|
|
const duration = Number.isFinite(video.duration) ? video.duration : null;
|
|
URL.revokeObjectURL(url);
|
|
resolve(duration);
|
|
};
|
|
video.onerror = () => {
|
|
URL.revokeObjectURL(url);
|
|
resolve(null);
|
|
};
|
|
video.src = url;
|
|
});
|
|
};
|
|
|
|
const validatePrivateVideoDuration = (duration: number | null): duration is number => {
|
|
if (duration == null || !Number.isFinite(duration) || duration <= 0) {
|
|
message.error('无法读取视频秒数,请检查视频文件是否损坏');
|
|
return false;
|
|
}
|
|
if (duration < MIN_PRIVATE_VIDEO_DURATION) {
|
|
message.error(`视频素材最短不能少于 ${MIN_PRIVATE_VIDEO_DURATION} 秒`);
|
|
return false;
|
|
}
|
|
if (duration > MAX_PRIVATE_VIDEO_DURATION) {
|
|
message.error(`视频素材最长不能超过 ${MAX_PRIVATE_VIDEO_DURATION} 秒`);
|
|
return false;
|
|
}
|
|
return true;
|
|
};
|
|
|
|
const PrivatePortraitAssetUpload: React.FC<Props> = ({ projectId, open, onClose, onSuccess }) => {
|
|
const [fileList, setFileList] = useState<UploadFile[]>([]);
|
|
const [name, setName] = useState('');
|
|
const [loading, setLoading] = useState(false);
|
|
|
|
const reset = () => {
|
|
setFileList([]);
|
|
setName('');
|
|
};
|
|
|
|
const handleSubmit = async () => {
|
|
const file = fileList[0]?.originFileObj as File | undefined;
|
|
if (!file) {
|
|
message.warning('请先选择图片或视频素材');
|
|
return;
|
|
}
|
|
const assetType = guessAssetType(file);
|
|
if (assetType === 'Image' && !file.type.startsWith('image/')) {
|
|
message.error('仅支持图片或视频素材');
|
|
return;
|
|
}
|
|
|
|
setLoading(true);
|
|
try {
|
|
const videoDuration = assetType === 'Video' ? await getVideoDuration(file) : null;
|
|
if (assetType === 'Video' && !validatePrivateVideoDuration(videoDuration)) {
|
|
return;
|
|
}
|
|
|
|
const uploaded = assetType === 'Video' ? await uploadPrivatePortraitVideo(file, videoDuration || undefined) : await uploadPrivatePortraitImage(file);
|
|
await createPrivatePortraitAsset(projectId, {
|
|
url: uploaded.url,
|
|
assetType,
|
|
name: name.trim() || file.name,
|
|
videoDuration: uploaded.duration_seconds ?? videoDuration,
|
|
fileSize: uploaded.file_size_bytes ?? file.size,
|
|
mimeType: file.type || null,
|
|
uploadResourceId: uploaded.resource_id || null,
|
|
});
|
|
message.success(assetType === 'Video' ? '视频素材已提交入库,处理中' : '图片素材已提交入库,处理中');
|
|
reset();
|
|
onSuccess();
|
|
onClose();
|
|
} catch (e: any) {
|
|
message.error(e?.message || '上传素材失败');
|
|
} finally {
|
|
setLoading(false);
|
|
}
|
|
};
|
|
|
|
return (
|
|
<Modal
|
|
title="上传真人素材"
|
|
open={open}
|
|
onCancel={onClose}
|
|
onOk={handleSubmit}
|
|
confirmLoading={loading}
|
|
okText="提交入库"
|
|
destroyOnHidden
|
|
>
|
|
<Space direction="vertical" style={{ width: '100%' }} size={12}>
|
|
<Input value={name} onChange={(e) => setName(e.target.value)} placeholder="素材名称,默认使用文件名" maxLength={256} />
|
|
<Upload
|
|
accept="image/*,video/*"
|
|
maxCount={1}
|
|
fileList={fileList}
|
|
beforeUpload={() => false}
|
|
onChange={({ fileList }) => setFileList(fileList)}
|
|
listType="picture"
|
|
>
|
|
<Button icon={<UploadOutlined />}>选择图片或视频</Button>
|
|
</Upload>
|
|
<div style={{ color: '#64748b', fontSize: 12 }}>
|
|
当前开放图片和视频,音频暂不接入。视频素材必须在 {MIN_PRIVATE_VIDEO_DURATION}~{MAX_PRIVATE_VIDEO_DURATION} 秒内;入库后会经过火山真人一致性校验,Active 后才可用于 AI 创作。
|
|
</div>
|
|
</Space>
|
|
</Modal>
|
|
);
|
|
};
|
|
|
|
export default PrivatePortraitAssetUpload;
|