真人素材库本地完成
This commit is contained in:
@@ -30,6 +30,8 @@ import {
|
||||
} from '../api';
|
||||
|
||||
import { useAppStore } from '../store/useAppStore';
|
||||
import { PrivatePortraitAssetPicker } from '../components/privatePortrait';
|
||||
import type { PrivatePortraitSelectableAsset } from '../types';
|
||||
|
||||
import {
|
||||
PlusOutlined,
|
||||
@@ -71,6 +73,9 @@ interface MediaReference {
|
||||
duration?: number;
|
||||
role?: string;
|
||||
label?: string;
|
||||
source?: string;
|
||||
private_asset_id?: string;
|
||||
remote_asset_id?: string;
|
||||
}
|
||||
|
||||
interface Message {
|
||||
@@ -167,6 +172,7 @@ const AIChatPage: React.FC = () => {
|
||||
const [lastFrame, setLastFrame] = useState<MediaReference | null>(null);
|
||||
const [uploadTarget, setUploadTarget] = useState<'first' | 'last' | null>(null);
|
||||
const [referenceModeDropdownVisible, setReferenceModeDropdownVisible] = useState(false);
|
||||
const [privateAssetPickerOpen, setPrivateAssetPickerOpen] = useState(false);
|
||||
const [mediaStackHovered, setMediaStackHovered] = useState(false);
|
||||
const mediaStackCloseTimerRef = useRef<number | null>(null);
|
||||
const openMediaStackTray = useCallback(() => {
|
||||
@@ -1298,6 +1304,41 @@ const AIChatPage: React.FC = () => {
|
||||
};
|
||||
|
||||
|
||||
const handlePrivatePortraitAssetsSelected = (assets: PrivatePortraitSelectableAsset[]) => {
|
||||
if (mediaType !== 'video') {
|
||||
message.warning('真人素材库第一版仅支持视频创作参考');
|
||||
return;
|
||||
}
|
||||
const imageCount = currentMedia.filter((m) => m.type === 'image').length;
|
||||
const available = Math.max(0, maxImage - imageCount);
|
||||
if (assets.length > available) {
|
||||
message.warning(`当前引擎最多还能添加 ${available} 张图片参考`);
|
||||
return;
|
||||
}
|
||||
const added: MediaReference[] = assets.map((asset) => ({
|
||||
name: asset.name || '真人素材',
|
||||
type: 'image',
|
||||
url: asset.previewUrl || '',
|
||||
source: 'private_portrait_asset',
|
||||
private_asset_id: asset.id,
|
||||
label: '',
|
||||
}));
|
||||
const newList = [...currentMedia, ...added];
|
||||
const labels = generateMediaLabels(newList);
|
||||
setCurrentMedia(newList.map((m, i) => ({ ...m, label: labels[i] })));
|
||||
message.success(`已添加 ${assets.length} 个真人素材参考`);
|
||||
};
|
||||
|
||||
const buildPreviewUrl = (url: string) => {
|
||||
if (!url) return '';
|
||||
if (url.startsWith('http://') || url.startsWith('https://') || url.startsWith('data:') || url.startsWith('blob:')) {
|
||||
return url;
|
||||
}
|
||||
const base = (import.meta.env.VITE_API_BASE || '').replace(/\/$/, '');
|
||||
return `${base}${url.startsWith('/') ? '' : '/'}${url}`;
|
||||
};
|
||||
|
||||
|
||||
const handleRemoveMedia = (index: number) => {
|
||||
const newList = currentMedia.filter((_, i) => i !== index);
|
||||
const labels = generateMediaLabels(newList);
|
||||
@@ -2407,6 +2448,15 @@ const AIChatPage: React.FC = () => {
|
||||
</Tooltip>
|
||||
</Upload>
|
||||
)}
|
||||
{mediaType === 'video' && currentMedia.length === 0 && (
|
||||
<Button
|
||||
size="small"
|
||||
onClick={() => setPrivateAssetPickerOpen(true)}
|
||||
style={{ marginTop: 8, borderRadius: 8, color: '#8b5cf6', borderColor: '#ddd6fe', background: '#fff' }}
|
||||
>
|
||||
真人素材库
|
||||
</Button>
|
||||
)}
|
||||
|
||||
{/* 层叠附件展示 - 鼠标移入向右排列展开 */}
|
||||
{currentMedia.length > 0 && (
|
||||
@@ -2432,7 +2482,7 @@ const AIChatPage: React.FC = () => {
|
||||
<div style={{ position: 'relative', width: 52, height: 60 }}>
|
||||
{media.type === 'image' ? (
|
||||
<img
|
||||
src={`${import.meta.env.VITE_API_BASE || "http://localhost:8000"}${media.url}`}
|
||||
src={buildPreviewUrl(media.url)}
|
||||
alt={media.name}
|
||||
onClick={() => {
|
||||
setAttachmentPreviewUrl(media.url);
|
||||
@@ -2444,7 +2494,7 @@ const AIChatPage: React.FC = () => {
|
||||
/>
|
||||
) : media.type === 'video' ? (
|
||||
<video
|
||||
src={`${import.meta.env.VITE_API_BASE || "http://localhost:8000"}${media.url}`}
|
||||
src={buildPreviewUrl(media.url)}
|
||||
muted
|
||||
onClick={() => {
|
||||
setAttachmentPreviewUrl(media.url);
|
||||
@@ -2462,7 +2512,7 @@ const AIChatPage: React.FC = () => {
|
||||
}}
|
||||
style={{ width: 52, height: 60, objectFit: 'cover', borderRadius: 10, cursor: 'pointer', border: '1px solid rgba(255,255,255,0.98)', boxShadow: '0 4px 12px rgba(31,41,55,0.15)', background: 'linear-gradient(135deg, #8b5cf6 0%, #a78bfa 100%)', display: 'flex', alignItems: 'center', justifyContent: 'center' }}
|
||||
>
|
||||
{playingAudioUrl === (media.url.startsWith('http') ? media.url : `${import.meta.env.VITE_API_BASE || "http://localhost:8000"}${media.url}`) ? (
|
||||
{playingAudioUrl === (buildPreviewUrl(media.url)) ? (
|
||||
<PauseOutlined style={{ fontSize: 20, color: '#fff' }} />
|
||||
) : (
|
||||
<AudioOutlined style={{ fontSize: 20, color: '#fff' }} />
|
||||
@@ -2553,6 +2603,34 @@ const AIChatPage: React.FC = () => {
|
||||
</Tooltip>
|
||||
</Upload>
|
||||
)}
|
||||
{mediaType === 'video' && currentMedia.length > 0 && (
|
||||
<Tooltip title="选择真人素材库">
|
||||
<div
|
||||
onClick={(e) => { e.stopPropagation(); setPrivateAssetPickerOpen(true); }}
|
||||
style={{
|
||||
position: 'absolute',
|
||||
right: -32,
|
||||
bottom: 0,
|
||||
width: 28,
|
||||
height: 28,
|
||||
borderRadius: 50,
|
||||
background: '#fff',
|
||||
border: '1px solid #ddd6fe',
|
||||
color: '#8b5cf6',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
cursor: 'pointer',
|
||||
fontSize: 12,
|
||||
fontWeight: 800,
|
||||
boxShadow: '0 2px 8px rgba(47, 52, 64, 0.08)',
|
||||
zIndex: 100,
|
||||
}}
|
||||
>
|
||||
真
|
||||
</div>
|
||||
</Tooltip>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
@@ -3906,20 +3984,28 @@ const AIChatPage: React.FC = () => {
|
||||
<div style={{ display: 'flex', alignItems: 'center', justifyContent: 'center', width: '100%', height: '100%' }}>
|
||||
{attachmentPreviewType === 'image' ? (
|
||||
<img
|
||||
src={`${import.meta.env.VITE_API_BASE || "http://localhost:8000"}${attachmentPreviewUrl}`}
|
||||
src={buildPreviewUrl(attachmentPreviewUrl)}
|
||||
alt="预览"
|
||||
style={{ width: '100%', maxHeight: '400px', objectFit: 'contain' }}
|
||||
/>
|
||||
) : (
|
||||
<video
|
||||
ref={attachmentPreviewVideoRef}
|
||||
src={`${import.meta.env.VITE_API_BASE || "http://localhost:8000"}${attachmentPreviewUrl}`}
|
||||
src={buildPreviewUrl(attachmentPreviewUrl)}
|
||||
controls
|
||||
style={{ maxWidth: '100%', maxHeight: '400px' }}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</Modal>
|
||||
<PrivatePortraitAssetPicker
|
||||
open={privateAssetPickerOpen}
|
||||
onClose={() => setPrivateAssetPickerOpen(false)}
|
||||
onSelect={handlePrivatePortraitAssetsSelected}
|
||||
selectedIds={currentMedia.map((m) => m.private_asset_id).filter(Boolean) as string[]}
|
||||
maxCount={maxImage}
|
||||
/>
|
||||
|
||||
</Layout>
|
||||
);
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user