Files
video-gen/video-gen-app/src/components/privatePortrait/library/AssetUpload.tsx
T

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;