上传素材管控-历史素材success
This commit is contained in:
@@ -33,7 +33,7 @@ import {
|
||||
|
||||
import { useAppStore } from '../store/useAppStore';
|
||||
import { PrivatePortraitAssetPicker } from '../components/privatePortrait';
|
||||
import type { PrivatePortraitLibraryType, PrivatePortraitSelectableAsset } from '../types';
|
||||
import type { PrivatePortraitLibraryType, PrivatePortraitSelectableAsset, UploadResourceHistoryItem } from '../types';
|
||||
|
||||
import {
|
||||
PlusOutlined,
|
||||
@@ -78,8 +78,10 @@ interface MediaReference {
|
||||
source?: string;
|
||||
private_asset_id?: string;
|
||||
remote_asset_id?: string;
|
||||
upload_resource_id?: string;
|
||||
}
|
||||
|
||||
|
||||
interface Message {
|
||||
id: string;
|
||||
original_prompt: string;
|
||||
@@ -1503,6 +1505,48 @@ const AIChatPage: React.FC = () => {
|
||||
return true;
|
||||
};
|
||||
|
||||
|
||||
const normalizeUploadResourceHistoryItem = (item: UploadResourceHistoryItem): MediaReference | null => {
|
||||
const media = item.mediaReference;
|
||||
const refType = (media?.type || item.resourceType) as 'image' | 'video' | 'audio';
|
||||
const url = media?.url || item.resourceUrl || item.displayUrl || item.previewUrl || '';
|
||||
if (!url) {
|
||||
message.error(`${item.fileName || item.id} 缺少素材地址,不能用于 AI 创作`);
|
||||
return null;
|
||||
}
|
||||
if (refType === 'audio' && mediaType !== 'video') {
|
||||
message.error('仅视频模式支持添加音频素材');
|
||||
return null;
|
||||
}
|
||||
const duration = Number(media?.duration ?? item.durationSeconds);
|
||||
return {
|
||||
name: media?.name || item.fileName || item.id,
|
||||
type: refType,
|
||||
url,
|
||||
source: 'upload_resource',
|
||||
upload_resource_id: item.id,
|
||||
label: '',
|
||||
...((refType === 'video' || refType === 'audio') && Number.isFinite(duration) && duration > 0 ? { duration } : {}),
|
||||
};
|
||||
};
|
||||
|
||||
const handleUploadResourceHistorySelected = (items: UploadResourceHistoryItem[]) => {
|
||||
const normalized = items
|
||||
.map(normalizeUploadResourceHistoryItem)
|
||||
.filter(Boolean) as MediaReference[];
|
||||
if (!normalized.length) return;
|
||||
|
||||
const latestMedia = useAppStore.getState().currentMedia as MediaReference[];
|
||||
if (!validateMediaReferencesBeforeAdd(latestMedia, normalized)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const newList = [...latestMedia, ...normalized];
|
||||
const labels = generateMediaLabels(newList);
|
||||
setCurrentMedia(newList.map((m, i) => ({ ...m, label: labels[i] })));
|
||||
message.success(`已添加 ${normalized.length} 个历史上传素材参考`);
|
||||
};
|
||||
|
||||
const normalizePrivatePortraitAsset = (asset: PrivatePortraitSelectableAsset): MediaReference | null => {
|
||||
if (asset.assetType === 'Audio') {
|
||||
message.error('音频私域素材暂不支持用于 AI 创作');
|
||||
@@ -2612,16 +2656,7 @@ const AIChatPage: React.FC = () => {
|
||||
<UploadSelector
|
||||
accept={mediaType === 'image' ? 'image/*' : 'image/*,video/*,audio/*'}
|
||||
onLocalSelect={handleBatchUpload}
|
||||
onHistorySelect={(items) => {
|
||||
const newMedia = items.map((item: any) => ({
|
||||
name: item.name,
|
||||
type: item.type as 'image' | 'video' | 'audio',
|
||||
url: '',
|
||||
label: '',
|
||||
}));
|
||||
setCurrentMedia([...currentMedia, ...newMedia]);
|
||||
message.success(`成功添加${items.length}个历史记录`);
|
||||
}}
|
||||
onHistorySelect={handleUploadResourceHistorySelected}
|
||||
onPortraitLibrarySelect={(libraryType) => {
|
||||
openPrivatePortraitPicker(libraryType);
|
||||
}}
|
||||
@@ -2792,16 +2827,7 @@ const AIChatPage: React.FC = () => {
|
||||
<UploadSelector
|
||||
accept={mediaType === 'image' ? 'image/*' : 'image/*,video/*,audio/*'}
|
||||
onLocalSelect={handleBatchUpload}
|
||||
onHistorySelect={(items) => {
|
||||
const newMedia = items.map((item: any) => ({
|
||||
name: item.name,
|
||||
type: item.type as 'image' | 'video' | 'audio',
|
||||
url: '',
|
||||
label: '',
|
||||
}));
|
||||
setCurrentMedia([...currentMedia, ...newMedia]);
|
||||
message.success(`成功添加${items.length}个历史记录`);
|
||||
}}
|
||||
onHistorySelect={handleUploadResourceHistorySelected}
|
||||
onPortraitLibrarySelect={(libraryType) => {
|
||||
openPrivatePortraitPicker(libraryType);
|
||||
}}
|
||||
|
||||
@@ -20,6 +20,7 @@ import {
|
||||
} from '@ant-design/icons';
|
||||
import { useSearchParams } from 'react-router-dom';
|
||||
import { PrivatePortraitLibraryPanel } from '../components/privatePortrait';
|
||||
import { UploadResourceHistoryPanel } from '../components/uploadResource';
|
||||
import { gethistory, gethistoryItems, getOAuthList, asyncBatchUploadMaterial, updateFilename, getUploadHistory, getAllOAuthAccountList, getOpenTypeAll, getPreTestList, getDefaultPreTest, deleteHistory, deleteResourcesMaterial } from '../api';
|
||||
|
||||
const { Search } = Input;
|
||||
@@ -52,8 +53,8 @@ const GeneratedRecord: React.FC = () => {
|
||||
});
|
||||
|
||||
const [searchParams] = useSearchParams();
|
||||
const initialFilterType = searchParams.get('filterType') === 'private_portrait' ? 'private_portrait' : 'project';
|
||||
const [filterType, setFilterType] = useState<'project' | 'creation' | 'hot_opening_replicate' | 'shot_replicate' | 'private_portrait'>(initialFilterType);
|
||||
const initialFilterType = searchParams.get('filterType') === 'private_portrait' ? 'private_portrait' : searchParams.get('filterType') === 'upload_resource' ? 'upload_resource' : 'project';
|
||||
const [filterType, setFilterType] = useState<'project' | 'creation' | 'hot_opening_replicate' | 'shot_replicate' | 'private_portrait' | 'upload_resource'>(initialFilterType);
|
||||
const [filterMedia, setFilterMedia] = useState<'video' | 'image'>('video');
|
||||
const [recordlist, setRecordList] = useState<any[]>([]);
|
||||
const [Pagebreak, setPagebreak] = useState<any>({
|
||||
@@ -759,7 +760,7 @@ const GeneratedRecord: React.FC = () => {
|
||||
});
|
||||
};
|
||||
const loadRecordList = () => {
|
||||
if (filterType === 'private_portrait') {
|
||||
if (filterType === 'private_portrait' || filterType === 'upload_resource') {
|
||||
setLoading(false);
|
||||
setRecordList([]);
|
||||
return;
|
||||
@@ -1033,9 +1034,29 @@ const GeneratedRecord: React.FC = () => {
|
||||
>
|
||||
私域素材库
|
||||
</Button>
|
||||
<Button
|
||||
type={filterType === 'upload_resource' ? 'primary' : 'default'}
|
||||
onClick={() => {
|
||||
setFilterType('upload_resource');
|
||||
setIsSelectionMode(false);
|
||||
setSelectedItems(new Set());
|
||||
}}
|
||||
style={{
|
||||
borderRadius: 8,
|
||||
background: filterType === 'upload_resource'
|
||||
? 'linear-gradient(135deg, #6366f1, #8b5cf6)'
|
||||
: '#f8f9fc',
|
||||
border: filterType === 'upload_resource' ? 'none' : '1px solid #e2e8f0',
|
||||
color: filterType === 'upload_resource' ? '#fff' : '#64748b',
|
||||
fontWeight: 600,
|
||||
}}
|
||||
icon={<UploadOutlined />}
|
||||
>
|
||||
历史素材
|
||||
</Button>
|
||||
</Space>
|
||||
</div>
|
||||
{filterType !== 'private_portrait' && (
|
||||
{filterType !== 'private_portrait' && filterType !== 'upload_resource' && (
|
||||
<div style={{ display: 'flex', alignItems: 'center', gap: 4, flexWrap: 'wrap' }}>
|
||||
{/* 多选模式按钮 */}
|
||||
{isSelectionMode ? (
|
||||
@@ -1120,6 +1141,8 @@ const GeneratedRecord: React.FC = () => {
|
||||
</div>
|
||||
{filterType === 'private_portrait' ? (
|
||||
<PrivatePortraitLibraryPanel />
|
||||
) : filterType === 'upload_resource' ? (
|
||||
<UploadResourceHistoryPanel />
|
||||
) : (
|
||||
<>
|
||||
{/* Second row filter: 视频 / 图片 */}
|
||||
|
||||
@@ -18,11 +18,20 @@ import {
|
||||
LoadingOutlined,
|
||||
} from '@ant-design/icons';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import { uploadVideo, uploadImage, generateReplication, getReplicationList, getone, getReplicationDetail } from '../api';
|
||||
import { uploadHotOpeningVideo, uploadHotOpeningImage, generateReplication, getReplicationList, getone, getReplicationDetail } from '../api';
|
||||
|
||||
const { Header, Content } = Layout;
|
||||
const { TextArea } = Input;
|
||||
|
||||
const API_BASE = import.meta.env.VITE_API_BASE || 'http://localhost:8000';
|
||||
|
||||
const buildAssetUrl = (url?: string): string => {
|
||||
if (!url) return '';
|
||||
if (/^https?:\/\//i.test(url) || url.startsWith('blob:')) return url;
|
||||
return `${API_BASE}${url}`;
|
||||
};
|
||||
|
||||
|
||||
const GenerateConver: React.FC = () => {
|
||||
const navigate = useNavigate();
|
||||
|
||||
@@ -50,8 +59,11 @@ const GenerateConver: React.FC = () => {
|
||||
// 文件状态
|
||||
const [videoFile, setVideoFile] = useState<File | null>(null);
|
||||
const [videoUrl, setVideoUrl] = useState<string>('');
|
||||
const [videoResourceId, setVideoResourceId] = useState<string>('');
|
||||
const [videoDurationSeconds, setVideoDurationSeconds] = useState<number | null>(null);
|
||||
const [imageFile, setImageFile] = useState<File | null>(null);
|
||||
const [imageUrl, setImageUrl] = useState<string>('');
|
||||
const [imageResourceId, setImageResourceId] = useState<string>('');
|
||||
const [videoUploading, setVideoUploading] = useState(false);
|
||||
const [imageUploading, setImageUploading] = useState(false);
|
||||
|
||||
@@ -222,9 +234,11 @@ const GenerateConver: React.FC = () => {
|
||||
// 调用上传接口
|
||||
setVideoUploading(true);
|
||||
try {
|
||||
const res = await uploadVideo(file);
|
||||
const res = await uploadHotOpeningVideo(file, video.duration);
|
||||
setVideoFile(file);
|
||||
setVideoUrl(`${import.meta.env.VITE_API_BASE || 'http://localhost:8000'}${res.url}`);
|
||||
setVideoUrl(res.url);
|
||||
setVideoResourceId(res.resource_id || '');
|
||||
setVideoDurationSeconds(video.duration);
|
||||
message.success('视频上传成功');
|
||||
resolve(false);
|
||||
} catch (error) {
|
||||
@@ -280,9 +294,10 @@ const GenerateConver: React.FC = () => {
|
||||
// 调用上传接口
|
||||
setImageUploading(true);
|
||||
try {
|
||||
const res = await uploadImage(file);
|
||||
const res = await uploadHotOpeningImage(file);
|
||||
setImageFile(file);
|
||||
setImageUrl(`${import.meta.env.VITE_API_BASE || 'http://localhost:8000'}${res.url}`);
|
||||
setImageUrl(res.url);
|
||||
setImageResourceId(res.resource_id || '');
|
||||
message.success('图片上传成功');
|
||||
resolve(false);
|
||||
} catch (error) {
|
||||
@@ -363,6 +378,9 @@ const GenerateConver: React.FC = () => {
|
||||
let params = {
|
||||
material_video_url: videoUrl,
|
||||
material_image_url: imageUrl,
|
||||
material_video_resource_id: videoResourceId || undefined,
|
||||
material_image_resource_id: imageResourceId || undefined,
|
||||
material_video_duration_seconds: videoDurationSeconds || undefined,
|
||||
source_project_name: originalProductName,
|
||||
target_project_name: ownProductName,
|
||||
core_content_point: productSellingPoints,
|
||||
@@ -380,7 +398,10 @@ const GenerateConver: React.FC = () => {
|
||||
|
||||
// 清空上传的媒体和文本
|
||||
setVideoUrl('');
|
||||
setVideoResourceId('');
|
||||
setVideoDurationSeconds(null);
|
||||
setImageUrl('');
|
||||
setImageResourceId('');
|
||||
setOriginalProductName('');
|
||||
setOwnProductName('');
|
||||
setProductSellingPoints('');
|
||||
@@ -818,7 +839,7 @@ const GenerateConver: React.FC = () => {
|
||||
{videoUrl ? (
|
||||
<div style={{ position: 'relative' }}>
|
||||
<video
|
||||
src={videoUrl}
|
||||
src={buildAssetUrl(videoUrl)}
|
||||
controls
|
||||
style={{ width: '100%', borderRadius: 12, maxHeight: 200, boxShadow: '0 4px 12px rgba(99, 102, 241, 0.08)' }}
|
||||
/>
|
||||
@@ -934,7 +955,7 @@ const GenerateConver: React.FC = () => {
|
||||
{imageUrl ? (
|
||||
<div style={{ position: 'relative' }}>
|
||||
<img
|
||||
src={imageUrl}
|
||||
src={buildAssetUrl(imageUrl)}
|
||||
alt="产品图片"
|
||||
style={{ width: '100%', borderRadius: 12, maxHeight: 200, objectFit: 'contain', boxShadow: '0 4px 12px rgba(99, 102, 241, 0.08)' }}
|
||||
/>
|
||||
|
||||
@@ -2,7 +2,7 @@ import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
||||
import { useNavigate, useParams } from 'react-router-dom';
|
||||
import { Button, Drawer, Input, Modal, Popconfirm, Spin, Table, Tag, Tooltip, Upload, message } from 'antd';
|
||||
import { ArrowLeftOutlined, PlusOutlined, XOutlined } from '@ant-design/icons';
|
||||
import { createRemoveLens, getShotReplicationDetail, Removelist, removeCreate, splitCustom, uploadImage } from '../api';
|
||||
import { createRemoveLens, getShotReplicationDetail, Removelist, removeCreate, splitCustom, uploadShotReplicateImage } from '../api';
|
||||
import VideoTrimPicker from '../components/VideoTrimPicker';
|
||||
|
||||
const { TextArea } = Input;
|
||||
@@ -30,6 +30,7 @@ function RemoveInfo() {
|
||||
const [productName, setProductName] = useState('');
|
||||
const [productSellingPoint, setProductSellingPoint] = useState('');
|
||||
const [productImage, setProductImage] = useState('');
|
||||
const [productImageResourceId, setProductImageResourceId] = useState('');
|
||||
const [taskDetail, setTaskDetail] = useState<any>(null);
|
||||
const [tableData, setTableData] = useState<any[]>([]);
|
||||
const [loading, setLoading] = useState(false);
|
||||
@@ -190,18 +191,21 @@ function RemoveInfo() {
|
||||
setCurrentSegmentName('');
|
||||
setProductSellingPoint('');
|
||||
setProductImage('');
|
||||
setProductImageResourceId('');
|
||||
};
|
||||
|
||||
const handleProductImageChange: any = (info: any) => {
|
||||
if (info.fileList.length === 0) {
|
||||
setProductImage('');
|
||||
setProductImageResourceId('');
|
||||
}
|
||||
};
|
||||
|
||||
const beforeUploadProductImage = async (file: File) => {
|
||||
try {
|
||||
const uploadResult = await uploadImage(file);
|
||||
const uploadResult = await uploadShotReplicateImage(file);
|
||||
setProductImage(uploadResult.url);
|
||||
setProductImageResourceId(uploadResult.resource_id || '');
|
||||
message.success('图片上传成功');
|
||||
} catch {
|
||||
message.error('图片上传失败,请重试');
|
||||
@@ -231,7 +235,8 @@ function RemoveInfo() {
|
||||
const params = {
|
||||
target_project_name: productName.trim(),
|
||||
core_content_point: productSellingPoint.trim(),
|
||||
material_image_url: buildAssetUrl(productImage),
|
||||
material_image_url: productImage,
|
||||
material_image_resource_id: productImageResourceId || undefined,
|
||||
idempotency_key: `replication_${Date.now()}`,
|
||||
};
|
||||
|
||||
|
||||
@@ -2,13 +2,14 @@ import { useState, useRef, useCallback } from 'react';
|
||||
import { Button, Modal, Input, Table, Upload, Popconfirm, message } from 'antd';
|
||||
import { FileTextOutlined, CloudUploadOutlined } from '@ant-design/icons';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import { uploadVideo, createShotReplication, getShotReplicationList } from '../api';
|
||||
import { uploadShotReplicateVideo, createShotReplication, getShotReplicationList } from '../api';
|
||||
import bg1 from '../assets/bg1.png';
|
||||
|
||||
export default function VideoFrameExtractor() {
|
||||
const navigate = useNavigate();
|
||||
|
||||
const [videoUrl, setVideoUrl] = useState<string>('');
|
||||
const [videoResourceId, setVideoResourceId] = useState<string>('');
|
||||
const [error, setError] = useState<string>('');
|
||||
const [isModalOpen, setIsModalOpen] = useState(false);
|
||||
const [productName, setProductName] = useState<string>('');
|
||||
@@ -28,6 +29,7 @@ export default function VideoFrameExtractor() {
|
||||
URL.revokeObjectURL(videoUrl);
|
||||
}
|
||||
setVideoUrl('');
|
||||
setVideoResourceId('');
|
||||
setError('');
|
||||
setVideoDuration(0);
|
||||
setProductName('');
|
||||
@@ -66,8 +68,9 @@ export default function VideoFrameExtractor() {
|
||||
setLoading(true);
|
||||
|
||||
try {
|
||||
const uploadResult = await uploadVideo(file);
|
||||
const uploadResult = await uploadShotReplicateVideo(file, duration);
|
||||
setVideoUrl(uploadResult.url);
|
||||
setVideoResourceId(uploadResult.resource_id || '');
|
||||
setError('');
|
||||
message.success('视频上传成功');
|
||||
} catch (err) {
|
||||
@@ -97,6 +100,7 @@ export default function VideoFrameExtractor() {
|
||||
const params = {
|
||||
video_url: videoUrl,
|
||||
video_duration_seconds: videoDuration,
|
||||
video_resource_id: videoResourceId || undefined,
|
||||
title: productName.trim(),
|
||||
idempotency_key: `shot_${Date.now()}`,
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user