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 => { 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 = ({ projectId, open, onClose, onSuccess }) => { const [fileList, setFileList] = useState([]); 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 ( setName(e.target.value)} placeholder="素材名称,默认使用文件名" maxLength={256} /> false} onChange={({ fileList }) => setFileList(fileList)} listType="picture" >
当前开放图片和视频,音频暂不接入。视频素材必须在 {MIN_PRIVATE_VIDEO_DURATION}~{MAX_PRIVATE_VIDEO_DURATION} 秒内;入库后会经过火山真人一致性校验,Active 后才可用于 AI 创作。
); }; export default PrivatePortraitAssetUpload;