From bb5306335836fda36aefc17913a4eacd81f52ea8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=AD=99=E4=BD=B3=E8=89=BA?= <13935794190@163.com> Date: Wed, 8 Jul 2026 17:15:19 +0800 Subject: [PATCH 1/7] =?UTF-8?q?=E7=9C=9F=E4=BA=BA=E4=BA=BA=E5=83=8F?= =?UTF-8?q?=E4=BF=AE=E6=94=B9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- video-gen-app/src/api/index.ts | 22 +- .../src/components/Layout/AppLayout.tsx | 6 +- .../src/components/UploadSelector.tsx | 271 +++++----- .../privatePortrait/library/AssetGrid.tsx | 164 ++++-- .../privatePortrait/library/LibraryPanel.tsx | 77 ++- .../privatePortrait/library/ProjectDetail.tsx | 102 +++- .../privatePortrait/library/ProjectList.tsx | 54 +- .../library/RealPersonLibraryPanel.tsx | 41 +- .../library/VirtualMaterialPanel.tsx | 169 +++--- .../privatePortrait/picker/AssetPicker.tsx | 120 ++++- video-gen-app/src/pages/GenerateConver.tsx | 221 +++++--- video-gen-app/src/pages/GeneratePage.tsx | 48 +- video-gen-app/src/pages/GeneratedRecord.tsx | 2 +- video-gen-app/src/pages/HomePage.tsx | 27 +- .../src/pages/InitialReplication.tsx | 76 ++- video-gen-app/src/pages/RemoveInfo.tsx | 500 +++++++++++------- video-gen-app/src/pages/RemoveLens.tsx | 99 +++- 17 files changed, 1320 insertions(+), 679 deletions(-) diff --git a/video-gen-app/src/api/index.ts b/video-gen-app/src/api/index.ts index 228ec4b1..3dcee79e 100644 --- a/video-gen-app/src/api/index.ts +++ b/video-gen-app/src/api/index.ts @@ -570,6 +570,26 @@ export async function removethree(projectId: string, stepId: string ,params: any export async function removefour(projectId: string, stepId: string ,params: any): Promise { return api.post(`/shot-replications/projects/${projectId}/steps/${stepId}/generate-video`, params); } +// 重新分析 +export async function reanalyzeShotReplication(taskSetId: string): Promise { + return api.post(`/shot-replications/task-sets/${taskSetId}/reanalyze`); +} +// 删除片段 +export async function deleteSegment(segmentId: string): Promise { + await api.delete(`/shot-replications/segments/${segmentId}`); +} +// 重新分析片段 +export async function reanalyzeSegment(segmentId: string): Promise { + return api.post(`/shot-replications/segments/${segmentId}/reanalyze`); +} +// 删除拆镜项目 +export async function deleteShotReplicationProject(projectId: string): Promise { + await api.delete(`/shot-replications/projects/${projectId}`); +} +// 删除爆款开头复刻任务 +export async function deleteHotOpeningReplicationTask(taskId: string): Promise { + await api.delete(`/hot-opening-replications/tasks/${taskId}`); +} // 获取地区信息 export interface GetAreaParams { level?: string; @@ -734,7 +754,7 @@ export async function getHomeCaseHeader(): Promise { return api.get(`/home-materials/categories`); } // 首页素材按钮资源 -export async function getHomeCaseButton(id: string,limit:number=5): Promise { +export async function getHomeCaseButton(id: string,limit:number=10): Promise { return api.get(`/home-materials?category_id=${id}&limit_per_category=${limit}&include_empty_categories=false&response_mode=grouped&page=1&page_size=20`); } export async function deleteResourcesMaterial(params:any): Promise { diff --git a/video-gen-app/src/components/Layout/AppLayout.tsx b/video-gen-app/src/components/Layout/AppLayout.tsx index 6166401b..4e2aeb0c 100644 --- a/video-gen-app/src/components/Layout/AppLayout.tsx +++ b/video-gen-app/src/components/Layout/AppLayout.tsx @@ -179,16 +179,16 @@ const StorageCard: React.FC<{ data: ResourceCapacityData | null }> = ({ data }) justifyContent: 'space-between', marginBottom: 6, fontSize: 12, - color: isOver ? '#ffffffff' : '#000000ff', + color: isOver ? '#000000ff' : '#000000ff', padding: '0 8px', }}> - + {rawPercent.toFixed(1)}% {data.enabled ? ( - + {used.toFixed(2)} / {total.toFixed(2)} {unit} ) : ( diff --git a/video-gen-app/src/components/UploadSelector.tsx b/video-gen-app/src/components/UploadSelector.tsx index cffc5fd1..8b2a5bfb 100644 --- a/video-gen-app/src/components/UploadSelector.tsx +++ b/video-gen-app/src/components/UploadSelector.tsx @@ -1,60 +1,73 @@ import React, { useRef, useState } from 'react'; -import { Modal, Tooltip } from 'antd'; -import { HistoryOutlined, UserOutlined, FolderOpenOutlined, PlusOutlined, TeamOutlined } from '@ant-design/icons'; +import { Popover, Tooltip, Modal } from 'antd'; +import { FolderOpenOutlined, DatabaseOutlined, UserOutlined, TeamOutlined } from '@ant-design/icons'; import type { PrivatePortraitLibraryType, PrivatePortraitSelectableAsset } from '../types'; import PrivatePortraitAssetPicker from './privatePortrait/picker/AssetPicker'; interface UploadSelectorProps { children: React.ReactNode; accept?: string; + multiple?: boolean; onLocalSelect?: (files: File[]) => void; onHistorySelect?: (items: any[]) => void; - /** 兼容旧页面:由 UploadSelector 内部打开素材选择器,确认后回传素材数组。 */ onPortraitSelect?: (items: PrivatePortraitSelectableAsset[]) => void; - /** 新页面推荐:只选择素材库类型,父组件自行打开统一选择器。 */ onPortraitLibrarySelect?: (libraryType: PrivatePortraitLibraryType) => void; uploading?: boolean; tooltipTitle?: string; + maxImageCount?: number; + maxVideoCount?: number; + usedImageCount?: number; + usedVideoCount?: number; + usedVideoDuration?: number; + maxVideoDuration?: number; } const UploadSelector: React.FC = ({ children, accept = 'image/*,video/*', + multiple = true, onLocalSelect, onHistorySelect, onPortraitSelect, onPortraitLibrarySelect, uploading, tooltipTitle, + maxImageCount, + maxVideoCount, + usedImageCount, + usedVideoCount, + usedVideoDuration, + maxVideoDuration, }) => { const fileInputRef = useRef(null); - const [modalVisible, setModalVisible] = useState(false); - const [historyModalVisible, setHistoryModalVisible] = useState(false); const [portraitPickerOpen, setPortraitPickerOpen] = useState(false); const [portraitLibraryType, setPortraitLibraryType] = useState('real_person'); + const [historyModalVisible, setHistoryModalVisible] = useState(false); + const [popoverOpen, setPopoverOpen] = useState(false); const handleLocalSelect = () => { + setPopoverOpen(false); fileInputRef.current?.click(); }; const handleFileChange = (e: React.ChangeEvent) => { const files = e.target.files; if (files && onLocalSelect) { - onLocalSelect(Array.from(files)); + const fileList = Array.from(files); + onLocalSelect(fileList); } if (fileInputRef.current) { fileInputRef.current.value = ''; } }; - const handleClick = () => { - if (!uploading) { - setModalVisible(true); - } - }; - const openPortraitPicker = (libraryType: PrivatePortraitLibraryType) => { - setModalVisible(false); + setPopoverOpen(false); + if (onPortraitSelect) { + setPortraitLibraryType(libraryType); + setPortraitPickerOpen(true); + return; + } if (onPortraitLibrarySelect) { onPortraitLibrarySelect(libraryType); return; @@ -63,48 +76,102 @@ const UploadSelector: React.FC = ({ setPortraitPickerOpen(true); }; + const handleHistorySelect = () => { + setPopoverOpen(false); + setHistoryModalVisible(true); + }; + const confirmHistorySelection = () => { onHistorySelect?.([]); setHistoryModalVisible(false); - setModalVisible(false); }; - const options = [ - { - key: 'history', - label: '历史记录', - icon: , - description: '从历史上传记录中选择', - onClick: () => { - setModalVisible(false); - setHistoryModalVisible(true); - }, - }, - { - key: 'real_person', - label: '真人素材', - icon: , - description: '从真人私域素材库中选择', - onClick: () => openPortraitPicker('real_person'), - }, - { - key: 'aigc_virtual', - label: '虚拟素材', - icon: , - description: '从虚拟私域素材库中选择', - onClick: () => openPortraitPicker('aigc_virtual'), - }, - { - key: 'local', - label: '本地选取', - icon: , - description: '从本地电脑选择文件', - onClick: () => { - setModalVisible(false); - handleLocalSelect(); - }, - }, - ]; + const content = ( +
+
{ + handleLocalSelect(); + }} + style={{ + display: 'flex', + alignItems: 'center', + gap: 12, + padding: '8px 16px', + cursor: 'pointer', + transition: 'background 0.15s ease', + }} + onMouseEnter={(e) => { + e.currentTarget.style.background = '#f1f5f9'; + }} + onMouseLeave={(e) => { + e.currentTarget.style.background = 'transparent'; + }} + > + + {multiple ? '本地上传(可多选拖拽)' : '本地上传'} +
+
{ + e.currentTarget.style.background = '#f1f5f9'; + }} + onMouseLeave={(e) => { + e.currentTarget.style.background = 'transparent'; + }} + > + + 从资产中选择 +
+
openPortraitPicker('real_person')} + style={{ + display: 'flex', + alignItems: 'center', + gap: 12, + padding: '8px 16px', + cursor: 'pointer', + transition: 'background 0.15s ease', + }} + onMouseEnter={(e) => { + e.currentTarget.style.background = '#f1f5f9'; + }} + onMouseLeave={(e) => { + e.currentTarget.style.background = 'transparent'; + }} + > + + 真人素材库 +
+
openPortraitPicker('aigc_virtual')} + style={{ + display: 'flex', + alignItems: 'center', + gap: 12, + padding: '8px 16px', + cursor: 'pointer', + transition: 'background 0.15s ease', + }} + onMouseEnter={(e) => { + e.currentTarget.style.background = '#f1f5f9'; + }} + onMouseLeave={(e) => { + e.currentTarget.style.background = 'transparent'; + }} + > + + 虚拟素材库 +
+
+ ); return ( <> @@ -112,86 +179,38 @@ const UploadSelector: React.FC = ({ ref={fileInputRef} type="file" accept={accept} - multiple + multiple={multiple} onChange={handleFileChange} style={{ display: 'none' }} /> {tooltipTitle ? ( -
- {children} -
+ +
+ {children} +
+
) : ( -
- {children} -
+ +
+ {children} +
+
)} - setModalVisible(false)} - footer={null} - width={400} - centered - destroyOnHidden - > -
- {options.map((option) => ( -
{ - e.currentTarget.style.background = '#fff'; - e.currentTarget.style.borderColor = '#e2e8f0'; - e.currentTarget.style.boxShadow = '0 2px 8px rgba(0,0,0,0.04)'; - }} - onMouseLeave={(e) => { - e.currentTarget.style.background = '#f8fafc'; - e.currentTarget.style.borderColor = 'transparent'; - e.currentTarget.style.boxShadow = 'none'; - }} - > -
- {option.icon} -
-
-
- {option.label} -
-
- {option.description} -
-
- -
- ))} -
-
- = ({ onPortraitSelect?.(assets); setPortraitPickerOpen(false); }} + accept={accept} + maxCount={accept === 'image/*' && !multiple ? 1 : undefined} + maxImageCount={maxImageCount} + maxVideoCount={maxVideoCount} + usedImageCount={usedImageCount} + usedVideoCount={usedVideoCount} + usedVideoDuration={usedVideoDuration} + maxVideoDuration={maxVideoDuration} /> ); diff --git a/video-gen-app/src/components/privatePortrait/library/AssetGrid.tsx b/video-gen-app/src/components/privatePortrait/library/AssetGrid.tsx index e76d4fa1..5139d674 100644 --- a/video-gen-app/src/components/privatePortrait/library/AssetGrid.tsx +++ b/video-gen-app/src/components/privatePortrait/library/AssetGrid.tsx @@ -1,6 +1,6 @@ -import React from 'react'; -import { Button, Empty, Popconfirm, Space, Tag, Tooltip } from 'antd'; -import { DeleteOutlined, PictureOutlined, ReloadOutlined, VideoCameraOutlined } from '@ant-design/icons'; +import React, { useEffect, useRef, useState } from 'react'; +import { Button, Card, Empty, Modal, Popconfirm, Space, Tag, Tooltip } from 'antd'; +import { DeleteOutlined, EyeOutlined, PictureOutlined, VideoCameraOutlined } from '@ant-design/icons'; import type { PrivatePortraitAsset } from '../../../types'; const statusColor: Record = { @@ -12,11 +12,20 @@ const statusColor: Record = { delete_failed: 'red', }; +const statusText: Record = { + Active: '入库成功', + Processing: '入库处理中', + Failed: '入库失败', + local_deleted: '本地已删除', + remote_deleted: '远程已删除', + delete_failed: '删除失败', +}; + interface Props { items: PrivatePortraitAsset[]; loading?: boolean; - onSync: (assetId: string) => void; onDelete: (assetId: string) => void; + onRefresh?: () => void; } const buildPreviewUrl = (url?: string | null) => { @@ -39,53 +48,114 @@ const formatDuration = (value?: number | null) => { return `${duration.toFixed(duration >= 10 ? 0 : 1)}s`; }; -const PrivatePortraitAssetGrid: React.FC = ({ items, onSync, onDelete }) => { - if (!items.length) return ; +const PrivatePortraitAssetGrid: React.FC = ({ items, onDelete, onRefresh }) => { + const pollingRef = useRef(null); + const [previewOpen, setPreviewOpen] = useState(false); + const [previewUrl, setPreviewUrl] = useState(''); + const [previewType, setPreviewType] = useState<'Image' | 'Video'>('Image'); + + useEffect(() => { + const needsPolling = items.some( + (item) => item.status !== 'Failed' && item.status !== 'Active' + ); + + if (needsPolling && onRefresh) { + if (!pollingRef.current) { + pollingRef.current = window.setInterval(() => { + onRefresh(); + }, 3000); + } + } else { + if (pollingRef.current) { + clearInterval(pollingRef.current); + pollingRef.current = null; + } + } + + return () => { + if (pollingRef.current) { + clearInterval(pollingRef.current); + pollingRef.current = null; + } + }; + }, [items, onRefresh]); + + const openPreview = (asset: PrivatePortraitAsset) => { + const url = getAssetPreviewUrl(asset); + if (!url) return; + setPreviewUrl(url); + setPreviewType(asset.assetType === 'Video' ? 'Video' : 'Image'); + setPreviewOpen(true); + }; + + if (!items.length) return ; return ( -
- {items.map((item) => { - const isVideo = item.assetType === 'Video'; - const previewUrl = getAssetPreviewUrl(item); - return ( -
-
- {previewUrl ? ( - isVideo ? ( -
- ); - })} -
+ + ); + })} +
+ setPreviewOpen(false)} footer={null} width={760} destroyOnClose> +
+ {previewType === 'Video' ? ( +
+
+ ); }; diff --git a/video-gen-app/src/components/privatePortrait/library/LibraryPanel.tsx b/video-gen-app/src/components/privatePortrait/library/LibraryPanel.tsx index ccb60afa..a6b406bd 100644 --- a/video-gen-app/src/components/privatePortrait/library/LibraryPanel.tsx +++ b/video-gen-app/src/components/privatePortrait/library/LibraryPanel.tsx @@ -1,10 +1,12 @@ -import React, { useMemo, useState } from 'react'; -import { Tabs, Typography } from 'antd'; +import React, { useMemo, useState, useEffect } from 'react'; +import { Card, Col, Row, Tabs, Typography, message } from 'antd'; import { useSearchParams } from 'react-router-dom'; +import { getPrivatePortraitConfig, getPrivatePortraitProjects, getPrivatePortraitVirtualConfig, getPrivatePortraitVirtualProjects } from '../../../api'; +import type { PrivatePortraitConfig, PrivatePortraitProject } from '../../../types'; import RealPersonLibraryPanel from './RealPersonLibraryPanel'; import VirtualMaterialPanel from './VirtualMaterialPanel'; -const { Title, Text } = Typography; +const { Title, Text, Paragraph } = Typography; type PrivatePortraitTabKey = 'real_person' | 'aigc_virtual'; @@ -15,6 +17,48 @@ const normalizeTabKey = (value?: string | null): PrivatePortraitTabKey => ( const PrivatePortraitLibraryPanel: React.FC = () => { const [searchParams, setSearchParams] = useSearchParams(); const [activeKey, setActiveKey] = useState(() => normalizeTabKey(searchParams.get('portraitTab'))); + const [config, setConfig] = useState(null); + const [projects, setProjects] = useState([]); + const [selectedProjectId, setSelectedProjectId] = useState(); + + useEffect(() => { + const loadData = async () => { + try { + if (activeKey === 'aigc_virtual') { + const [configRes, projectsRes] = await Promise.all([ + getPrivatePortraitVirtualConfig(), + getPrivatePortraitVirtualProjects({ page: 1, pageSize: 100, status: 'active' }), + ]); + setConfig(configRes); + const projectList = projectsRes.items || []; + setProjects(projectList); + setSelectedProjectId((prev) => prev && projectList.some((item) => item.id === prev) ? prev : projectList[0]?.id); + } else { + const [configRes, projectsRes] = await Promise.all([ + getPrivatePortraitConfig(), + getPrivatePortraitProjects({ pageSize: 100, status: 'active' }), + ]); + setConfig(configRes); + const projectList = projectsRes.items || []; + setProjects(projectList); + setSelectedProjectId((prev) => prev && projectList.some((item) => item.id === prev) ? prev : projectList[0]?.id); + } + } catch (err: any) { + message.error(err?.message || '加载数据失败'); + } + }; + void loadData(); + }, [activeKey]); + + const selectedProject = useMemo( + () => projects.find((item) => item.id === selectedProjectId) || null, + [projects, selectedProjectId], + ); + + const quotaText = useMemo(() => { + if (!config) return '额度加载中'; + return `已用 ${config.usedAssetCount || 0} / ${config.assetLimit || 0} 个素材,剩余 ${config.remainingAssetCount || 0}`; + }, [config]); const items = useMemo(() => [ { @@ -45,6 +89,33 @@ const PrivatePortraitLibraryPanel: React.FC = () => { 私域素材库 统一管理真人素材和虚拟素材。真人项目组需先完成人脸认证,虚拟项目组会同步创建火山 AIGC Asset Group。 + + + + 素材总额度 +
{quotaText}
+ 真人/虚拟共用,图片/视频共用;音频暂不开放。 +
+ + + + 项目组 +
{projects.length}
+ + {activeKey === 'aigc_virtual' + ? '虚拟人像项目会同步创建火山 AIGC Asset Group。' + : '真人项目组需完成人脸认证后才可上传素材。'} + +
+ + + + 当前项目素材 +
{selectedProject?.assetCount || 0}
+ 仅 Active 状态素材可在 AI 创作中引用。 +
+ +
= ({ project, onDeleted, onC const [assets, setAssets] = useState([]); const [loading, setLoading] = useState(false); const [uploadOpen, setUploadOpen] = useState(false); + const [keyword, setKeyword] = useState(''); + const [assetStatus, setAssetStatus] = useState(); + const [assetType, setAssetType] = useState(); + const [assetPage, setAssetPage] = useState(1); + const [assetPageSize, setAssetPageSize] = useState(20); + const [assetTotal, setAssetTotal] = useState(0); - const loadAssets = async () => { + const loadAssets = async (page = assetPage, pageSize = assetPageSize) => { setLoading(true); try { - const res = await getPrivatePortraitAssets(project.id, { pageSize: 100 }); + const res = await getPrivatePortraitAssets(project.id, { + page, + pageSize, + keyword, + status: assetStatus, + assetType: assetType as any, + }); setAssets(res.items); + setAssetTotal(res.total || 0); + setAssetPage(page); + setAssetPageSize(pageSize); } catch (e: any) { message.error(e?.message || '加载素材失败'); } finally { @@ -29,23 +44,12 @@ const PrivatePortraitProjectDetail: React.FC = ({ project, onDeleted, onC } }; - useEffect(() => { loadAssets(); }, [project.id]); - - const handleSync = async (assetId: string) => { - try { - await syncPrivatePortraitAsset(assetId); - await loadAssets(); - onChanged(); - message.success('素材状态已刷新'); - } catch (e: any) { - message.error(e?.message || '刷新失败'); - } - }; + useEffect(() => { loadAssets(1, assetPageSize); }, [project.id, keyword, assetStatus, assetType]); const handleDeleteAsset = async (assetId: string) => { try { await deletePrivatePortraitAsset(assetId); - await loadAssets(); + await loadAssets(assetPage, assetPageSize); onChanged(); message.success('素材已删除'); } catch (e: any) { @@ -67,26 +71,76 @@ const PrivatePortraitProjectDetail: React.FC = ({ project, onDeleted, onC return ( {project.name}{project.status}} + title={{project.name}} extra={( - )} - style={{ borderRadius: 12 }} + style={{ borderRadius: 16 }} > - {project.description || '暂无描述'} {!canUpload && ( 项目组未完成真人认证,暂不能上传素材。请重新创建项目组并完成手机扫码认证。 )} - - setUploadOpen(false)} onSuccess={() => { loadAssets(); onChanged(); }} /> + + setKeyword(e.target.value)} + onSearch={() => loadAssets(1, assetPageSize)} + style={{ width: 240 }} + /> + setAssetType(value)} + style={{ width: 130 }} + options={[ + { value: 'Image', label: '图片' }, + { value: 'Video', label: '视频' }, + ]} + /> + + + + {assets.length === 0 ? ( + + ) : ( + <> + loadAssets(assetPage, assetPageSize)} /> +
+ `共 ${value} 个素材`} + onChange={(page, size) => loadAssets(page, size)} + /> +
+ + )} +
+ setUploadOpen(false)} onSuccess={() => { loadAssets(1, assetPageSize); onChanged(); }} />
); }; diff --git a/video-gen-app/src/components/privatePortrait/library/ProjectList.tsx b/video-gen-app/src/components/privatePortrait/library/ProjectList.tsx index 47fa8935..45e9d992 100644 --- a/video-gen-app/src/components/privatePortrait/library/ProjectList.tsx +++ b/video-gen-app/src/components/privatePortrait/library/ProjectList.tsx @@ -1,7 +1,9 @@ import React from 'react'; -import { Button, Empty, List, Tag } from 'antd'; +import { Empty, Space, Tag, Typography } from 'antd'; import type { PrivatePortraitProject } from '../../../types'; +const { Text } = Typography; + interface Props { items: PrivatePortraitProject[]; selectedId?: string | null; @@ -11,24 +13,40 @@ interface Props { const PrivatePortraitProjectList: React.FC = ({ items, selectedId, onSelect }) => { if (!items.length) return ; return ( - ( - - - - )} - /> + +
+ {project.name} + {project.description && {project.description}} +
+ {/* + {project.status === 'active' ? '可用' : project.status} + */} +
+ + 总 {project.assetCount || 0} + 图 {project.imageAssetCount || 0} + 视频 {project.videoAssetCount || 0} + {/* Active {project.activeAssetCount || 0} */} + + + ); + })} + ); }; diff --git a/video-gen-app/src/components/privatePortrait/library/RealPersonLibraryPanel.tsx b/video-gen-app/src/components/privatePortrait/library/RealPersonLibraryPanel.tsx index eadb2656..9e1f78f5 100644 --- a/video-gen-app/src/components/privatePortrait/library/RealPersonLibraryPanel.tsx +++ b/video-gen-app/src/components/privatePortrait/library/RealPersonLibraryPanel.tsx @@ -1,5 +1,5 @@ import React, { useEffect, useRef, useState } from 'react'; -import { Button, Card, Col, Form, Input, Modal, QRCode, Row, Space, Spin, Typography, message } from 'antd'; +import { Button, Card, Col, Empty, Form, Input, Modal, QRCode, Row, Space, Spin, Typography, message } from 'antd'; import { CheckCircleOutlined, PlusOutlined, ReloadOutlined } from '@ant-design/icons'; import type { PrivatePortraitProject, PrivatePortraitValidateSession } from '../../../types'; import { createPrivatePortraitProject, getPrivatePortraitProjects, getPrivatePortraitValidateSession } from '../../../api'; @@ -117,27 +117,36 @@ const RealPersonLibraryPanel: React.FC = () => { return (
-
-
- 真人素材库 - 创建项目组时先完成真人认证,认证成功后项目组才会正式创建并可上传素材。 -
- - - - +
+ 真人素材库 + 创建项目组时先完成真人认证,认证成功后项目组才会正式创建并可上传素材。
- - - + + 真人素材项目组} + extra={( + + {/* */} + + + )} + style={{ borderRadius: 16, minHeight: 520 }} + > + + {projects.length === 0 ? ( + + ) : ( + + )} + - + {selected ? ( { setSelected(null); loadProjects(); }} onChanged={loadProjects} /> ) : ( - 请先创建或选择一个真人素材项目组 + 请先创建或选择一个真人素材项目组 )} @@ -178,7 +187,7 @@ const RealPersonLibraryPanel: React.FC = () => { {isSuccess ? '认证成功,项目组正在刷新' : '请使用手机扫码完成人脸认证,成功后回到电脑端查看项目组。'}
- {h5Link && !isSuccess && {h5Link}} + {/* {h5Link && !isSuccess && {h5Link}} */} )} diff --git a/video-gen-app/src/components/privatePortrait/library/VirtualMaterialPanel.tsx b/video-gen-app/src/components/privatePortrait/library/VirtualMaterialPanel.tsx index 005ded74..a61d037d 100644 --- a/video-gen-app/src/components/privatePortrait/library/VirtualMaterialPanel.tsx +++ b/video-gen-app/src/components/privatePortrait/library/VirtualMaterialPanel.tsx @@ -1,4 +1,4 @@ -import React, { useEffect, useMemo, useState } from 'react'; +import React, { useEffect, useMemo, useRef, useState } from 'react'; import { App, Button, @@ -21,12 +21,10 @@ import { } from 'antd'; import type { UploadFile } from 'antd/es/upload/interface'; import { - CloudSyncOutlined, DeleteOutlined, EyeOutlined, PictureOutlined, PlusOutlined, - ReloadOutlined, UploadOutlined, VideoCameraOutlined, } from '@ant-design/icons'; @@ -36,15 +34,13 @@ import { deletePrivatePortraitVirtualAsset, deletePrivatePortraitVirtualProject, getPrivatePortraitVirtualAssets, - getPrivatePortraitVirtualConfig, getPrivatePortraitVirtualProjects, - syncPrivatePortraitVirtualAsset, uploadImage, uploadVideo, } from '../../../api'; -import type { PrivatePortraitAsset, PrivatePortraitConfig, PrivatePortraitProject } from '../../../types'; +import type { PrivatePortraitAsset, PrivatePortraitProject } from '../../../types'; -const { Text, Paragraph } = Typography; +const { Text } = Typography; type AssetTypeFilter = 'Image' | 'Video' | undefined; @@ -53,39 +49,18 @@ const MAX_PRIVATE_VIDEO_DURATION = 15; const statusConfig: Record = { creating: { label: '本地创建中', color: 'processing' }, - Processing: { label: '火山处理中', color: 'processing' }, - Active: { label: '可用于生成', color: 'success' }, + Processing: { label: '入库处理中', color: 'processing' }, + Active: { label: '入库成功', color: 'success' }, Failed: { label: '入库失败', color: 'error' }, local_deleted: { label: '本地已删', color: 'default' }, remote_deleted: { label: '远端已删', color: 'default' }, delete_failed: { label: '远端删除失败', color: 'error' }, }; -const assetTypeConfig: Record = { - Image: { label: '图片', color: 'green', icon: }, - Video: { label: '视频', color: 'blue', icon: }, -}; - -const formatDateTime = (dateStr?: string | null) => { - if (!dateStr) return '-'; - const date = new Date(dateStr); - if (Number.isNaN(date.getTime())) return '-'; - const year = date.getFullYear(); - const month = String(date.getMonth() + 1).padStart(2, '0'); - const day = String(date.getDate()).padStart(2, '0'); - const hours = String(date.getHours()).padStart(2, '0'); - const minutes = String(date.getMinutes()).padStart(2, '0'); - const seconds = String(date.getSeconds()).padStart(2, '0'); - return `${year}-${month}-${day} ${hours}:${minutes}:${seconds}`; -}; - -const formatSize = (size?: number | null) => { - const value = Number(size || 0); - if (!value) return '-'; - if (value >= 1024 * 1024 * 1024) return `${(value / 1024 / 1024 / 1024).toFixed(2)} GB`; - if (value >= 1024 * 1024) return `${(value / 1024 / 1024).toFixed(2)} MB`; - if (value >= 1024) return `${(value / 1024).toFixed(2)} KB`; - return `${value} B`; +const formatDuration = (value?: number | null) => { + const duration = Number(value || 0); + if (!Number.isFinite(duration) || duration <= 0) return '-'; + return `${duration.toFixed(duration >= 10 ? 0 : 1)}s`; }; const buildPreviewUrl = (url?: string | null) => { @@ -154,15 +129,8 @@ const StatusTag: React.FC<{ status?: string | null }> = ({ status }) => { return {config?.label || value}; }; -const TypeTag: React.FC<{ type?: string | null }> = ({ type }) => { - const value = type || '-'; - const config = assetTypeConfig[value]; - return {config?.label || value}; -}; - const VirtualMaterialPanel: React.FC = () => { const { message } = App.useApp(); - const [config, setConfig] = useState(null); const [projects, setProjects] = useState([]); const [selectedProjectId, setSelectedProjectId] = useState(); const [assets, setAssets] = useState([]); @@ -184,26 +152,13 @@ const VirtualMaterialPanel: React.FC = () => { const [previewUrl, setPreviewUrl] = useState(''); const [previewType, setPreviewType] = useState<'Image' | 'Video'>('Image'); const [createForm] = Form.useForm<{ name: string; description?: string }>(); + const pollingRef = useRef(null); const selectedProject = useMemo( () => projects.find((item) => item.id === selectedProjectId) || null, [projects, selectedProjectId], ); - const quotaText = useMemo(() => { - if (!config) return '额度加载中'; - return `已用 ${config.usedAssetCount || 0} / ${config.assetLimit || 0} 个素材,剩余 ${config.remainingAssetCount || 0}`; - }, [config]); - - const loadConfig = async () => { - try { - const next = await getPrivatePortraitVirtualConfig(); - setConfig(next); - } catch (err: any) { - message.error(err?.message || '加载私域素材额度失败'); - } - }; - const loadProjects = async () => { setProjectLoading(true); try { @@ -245,7 +200,7 @@ const VirtualMaterialPanel: React.FC = () => { }; const reloadAll = async () => { - await Promise.all([loadConfig(), loadProjects()]); + await loadProjects(); }; useEffect(() => { @@ -256,6 +211,32 @@ const VirtualMaterialPanel: React.FC = () => { if (selectedProjectId) loadAssets(1, assetPageSize); }, [selectedProjectId]); + useEffect(() => { + const needsPolling = assets.some( + (asset) => asset.status !== 'Failed' && asset.status !== 'Active' + ); + + if (needsPolling && selectedProjectId) { + if (!pollingRef.current) { + pollingRef.current = window.setInterval(() => { + loadAssets(assetPage, assetPageSize); + }, 3000); + } + } else { + if (pollingRef.current) { + clearInterval(pollingRef.current); + pollingRef.current = null; + } + } + + return () => { + if (pollingRef.current) { + clearInterval(pollingRef.current); + pollingRef.current = null; + } + }; + }, [assets, selectedProjectId, assetPage, assetPageSize]); + const handleCreateProject = async () => { const values = await createForm.validateFields(); setCreatingProject(true); @@ -308,7 +289,7 @@ const VirtualMaterialPanel: React.FC = () => { setUploadOpen(false); setFileList([]); setAssetName(''); - await Promise.all([loadConfig(), loadProjects(), loadAssets(1, assetPageSize)]); + await Promise.all([loadProjects(), loadAssets(1, assetPageSize)]); } catch (err: any) { message.error(err?.message || '上传素材失败'); } finally { @@ -316,21 +297,11 @@ const VirtualMaterialPanel: React.FC = () => { } }; - const handleSyncAsset = async (assetId: string) => { - try { - await syncPrivatePortraitVirtualAsset(assetId); - message.success('素材状态已刷新'); - await Promise.all([loadConfig(), loadProjects(), loadAssets(assetPage, assetPageSize)]); - } catch (err: any) { - message.error(err?.message || '刷新素材状态失败'); - } - }; - const handleDeleteAsset = async (assetId: string) => { try { await deletePrivatePortraitVirtualAsset(assetId); message.success('素材已删除,远端删除将异步执行'); - await Promise.all([loadConfig(), loadProjects(), loadAssets(assetPage, assetPageSize)]); + await Promise.all([loadProjects(), loadAssets(assetPage, assetPageSize)]); } catch (err: any) { message.error(err?.message || '删除素材失败'); } @@ -370,36 +341,40 @@ const VirtualMaterialPanel: React.FC = () => { style={{ borderRadius: 16, overflow: 'hidden', borderColor: '#eef2f7' }} cover={(
- {preview && !isVideo ? ( - {asset.name - ) : preview && isVideo && asset.videoCoverUrl ? ( - {asset.name + {preview ? ( + isVideo && asset.videoCoverUrl ? ( + {asset.name + ) : isVideo ? ( +
)} > - {asset.name || asset.remoteAssetId || '未命名素材'} +
{asset.name || asset.remoteAssetId || '未命名素材'}
- + {isVideo ? '视频' : '图片'} -
-
大小:{formatSize(asset.fileSize)}
-
轮询:{asset.pollCount || 0} 次
-
创建:{formatDateTime(asset.createdAt)}
-
- {asset.errorMessage &&
{asset.errorMessage}
} - handleDeleteAsset(asset.id)}> @@ -411,30 +386,6 @@ const VirtualMaterialPanel: React.FC = () => { return (
- - - - 素材总额度 -
{quotaText}
- 真人/虚拟共用,图片/视频共用;音频暂不开放。 -
- - - - 项目组 -
{projects.length}
- 虚拟人像项目会同步创建火山 AIGC Asset Group。 -
- - - - 当前项目素材 -
{selectedProject?.assetCount || 0}
- 仅 Active 状态素材可在 AI 创作中引用。 -
- -
- { {project.name} {project.description && {project.description}}
- + {/* */}
总 {project.assetCount || 0} 图 {project.imageAssetCount || 0} 视频 {project.videoAssetCount || 0} - Active {project.activeAssetCount || 0} + {/* Active {project.activeAssetCount || 0} */}
); @@ -488,7 +439,7 @@ const VirtualMaterialPanel: React.FC = () => { title={selectedProject ? selectedProject.name : '素材资产'} extra={( - + {/* */} {selectedProjectId && ( diff --git a/video-gen-app/src/components/privatePortrait/picker/AssetPicker.tsx b/video-gen-app/src/components/privatePortrait/picker/AssetPicker.tsx index 422f14a0..5687df3f 100644 --- a/video-gen-app/src/components/privatePortrait/picker/AssetPicker.tsx +++ b/video-gen-app/src/components/privatePortrait/picker/AssetPicker.tsx @@ -20,6 +20,13 @@ interface PrivatePortraitAssetPickerProps { maxCount?: number; onClose: () => void; onSelect: (assets: PrivatePortraitSelectableAsset[]) => void; + maxImageCount?: number; + maxVideoCount?: number; + usedImageCount?: number; + usedVideoCount?: number; + usedVideoDuration?: number; + maxVideoDuration?: number; + accept?: string; } const libraryMeta: Record = { @@ -68,12 +75,19 @@ const PrivatePortraitAssetPicker: React.FC = ({ maxCount = 20, onClose, onSelect, + maxImageCount, + maxVideoCount, + usedImageCount, + usedVideoCount, + usedVideoDuration, + maxVideoDuration, + accept, }) => { const meta = libraryMeta[libraryType]; const [projects, setProjects] = useState([]); const [projectId, setProjectId] = useState(); const [keyword, setKeyword] = useState(''); - const [assetType, setAssetType] = useState(); + const [assetType, setAssetType] = useState(accept === 'image/*' ? 'Image' : undefined); const [assets, setAssets] = useState([]); const [selectedAssets, setSelectedAssets] = useState>(new Map()); const [loadingProjects, setLoadingProjects] = useState(false); @@ -89,8 +103,8 @@ const PrivatePortraitAssetPicker: React.FC = ({ const next = res.items || []; setProjects(next); setProjectId((prev) => (prev && next.some((item) => item.id === prev) ? prev : next[0]?.id)); - } catch (err: any) { - message.error(err?.message || meta.projectError); + } catch (err: unknown) { + message.error((err as { message?: string })?.message || meta.projectError); } finally { setLoadingProjects(false); } @@ -108,8 +122,8 @@ const PrivatePortraitAssetPicker: React.FC = ({ pageSize: 100, }); setAssets(res.items || []); - } catch (err: any) { - message.error(err?.message || meta.assetError); + } catch (err: unknown) { + message.error((err as { message?: string })?.message || meta.assetError); } finally { setLoadingAssets(false); } @@ -117,24 +131,32 @@ const PrivatePortraitAssetPicker: React.FC = ({ useEffect(() => { if (!open) return; - setSelectedAssets(new Map()); - setKeyword(''); - setAssetType(undefined); - setProjectId(undefined); - setAssets([]); - loadProjects(); + setTimeout(() => { + setSelectedAssets(new Map()); + setKeyword(''); + setAssetType(undefined); + setProjectId(undefined); + setAssets([]); + loadProjects(); + }, 0); }, [open, libraryType]); useEffect(() => { if (!open) return; - loadAssets(); - }, [open, projectId, assetType]); + setTimeout(() => { + loadAssets(); + }, 0); + }, [open, projectId, assetType, libraryType]); const toggle = (asset: PrivatePortraitSelectableAsset) => { if (selectedIds.includes(asset.id)) { message.info('该素材已添加'); return; } + if (accept === 'image/*' && asset.assetType === 'Video') { + message.warning('当前仅支持选择图片素材'); + return; + } setSelectedAssets((prev) => { const next = new Map(prev); if (next.has(asset.id)) { @@ -150,6 +172,20 @@ const PrivatePortraitAssetPicker: React.FC = ({ }); }; + const isExceeded = useMemo(() => { + const selectedImages = Array.from(selectedAssets.values()).filter(a => a.assetType === 'Image').length; + const selectedVideos = Array.from(selectedAssets.values()).filter(a => a.assetType === 'Video').length; + const selectedVideoDuration = Array.from(selectedAssets.values()) + .filter(a => a.assetType === 'Video') + .reduce((sum, a) => sum + (a.videoDuration || 0), 0); + + const maxAvailableImages = maxImageCount !== undefined && usedImageCount !== undefined ? maxImageCount - usedImageCount : Infinity; + const maxAvailableVideos = maxVideoCount !== undefined && usedVideoCount !== undefined ? maxVideoCount - usedVideoCount : Infinity; + const maxAvailableDuration = maxVideoDuration !== undefined && usedVideoDuration !== undefined ? maxVideoDuration - usedVideoDuration : Infinity; + + return selectedImages > maxAvailableImages || selectedVideos > maxAvailableVideos || selectedVideoDuration > maxAvailableDuration; + }, [selectedAssets, maxImageCount, usedImageCount, maxVideoCount, usedVideoCount, maxVideoDuration, usedVideoDuration]); + const confirm = () => { const selected = (Array.from(selectedAssets.values()) as PrivatePortraitSelectableAsset[]).filter((item) => !selectedIds.includes(item.id)); if (!selected.length) { @@ -162,20 +198,54 @@ const PrivatePortraitAssetPicker: React.FC = ({ return ( 取消, - , ]} > -
-
+ {accept !== 'image/*' && ( +
+ {(() => { + const selectedImages = Array.from(selectedAssets.values()).filter(a => a.assetType === 'Image').length; + const selectedVideos = Array.from(selectedAssets.values()).filter(a => a.assetType === 'Video').length; + const selectedVideoDuration = Array.from(selectedAssets.values()) + .filter(a => a.assetType === 'Video') + .reduce((sum, a) => sum + (a.videoDuration || 0), 0); + + const maxAvailableImages = maxImageCount !== undefined && usedImageCount !== undefined ? maxImageCount - usedImageCount : Infinity; + const maxAvailableVideos = maxVideoCount !== undefined && usedVideoCount !== undefined ? maxVideoCount - usedVideoCount : Infinity; + const maxAvailableDuration = maxVideoDuration !== undefined && usedVideoDuration !== undefined ? maxVideoDuration - usedVideoDuration : Infinity; + + const imageExceeded = selectedImages > maxAvailableImages; + const videoExceeded = selectedVideos > maxAvailableVideos; + const durationExceeded = selectedVideoDuration > maxAvailableDuration; + + return ( + <> + + 还可选取图片 {selectedImages}/{maxImageCount !== undefined && usedImageCount !== undefined ? maxImageCount - usedImageCount : '-'} 张 + + + 视频还可选取 {selectedVideos}/{maxVideoCount !== undefined && usedVideoCount !== undefined ? maxVideoCount - usedVideoCount : '-'} 个({selectedVideoDuration.toFixed(1)}/{maxVideoDuration !== undefined && usedVideoDuration !== undefined ? (maxVideoDuration - usedVideoDuration).toFixed(1) : '-'}秒) + + + ); + })()} +
+ )} +
+
项目组
-
+
= ({ onPressEnter={loadAssets} /> without a
`);let s=e.getAttribute(`formaction`)||o.getAttribute(`action`);if(r=s?se(s,t):null,n=e.getAttribute(`formmethod`)||o.getAttribute(`method`)||kt,i=Ht(e.getAttribute(`formenctype`))||Ht(o.getAttribute(`enctype`))||At,a=new FormData(o,e),!Bt()){let{name:t,type:n,value:r}=e;if(n===`image`){let e=t?`${t}.`:``;a.append(`${e}x`,`0`),a.append(`${e}y`,`0`)}else t&&a.append(t,r)}}else if(jt(e))throw Error(`Cannot submit element that is not ,
- setHistoryModalVisible(false)} - footer={null} - width="80%" - centered - destroyOnHidden - > -
-
- 暂无资产图片 -
-
-
- -
-
+ onClose={() => setHistoryModalVisible(false)} + onSelect={(items) => { + onHistorySelect?.(items); + setHistoryModalVisible(false); + setModalVisible(false); + }} + allowedTypes={accept.includes('audio') ? ['image', 'video', 'audio'] : accept.includes('video') ? ['image', 'video'] : ['image']} + /> { + if (!url) return ''; + if (/^(https?:|data:|blob:)/i.test(url)) return url; + const base = (import.meta.env.VITE_API_BASE || '').replace(/\/$/, ''); + return `${base}${url.startsWith('/') ? '' : '/'}${url}`; +}; + +const typeIcon = (type: string) => { + if (type === 'video') return ; + if (type === 'audio') return ; + return ; +}; + +const typeLabel = (type: string) => { + if (type === 'video') return '视频'; + if (type === 'audio') return '音频'; + return '图片'; +}; + +const bytesText = (bytes?: number | null) => { + const value = Number(bytes || 0); + if (value >= 1024 * 1024) return `${(value / 1024 / 1024).toFixed(1)} MB`; + if (value >= 1024) return `${(value / 1024).toFixed(1)} KB`; + return `${value} B`; +}; + +const UploadResourceHistoryPanel: React.FC = () => { + const [resourceType, setResourceType] = useState(''); + const [keyword, setKeyword] = useState(''); + const [page, setPage] = useState(1); + const [pageSize, setPageSize] = useState(10); + const [loading, setLoading] = useState(false); + const [groups, setGroups] = useState([]); + const [totalDays, setTotalDays] = useState(0); + const [selectedIds, setSelectedIds] = useState>(new Set()); + const [previewItem, setPreviewItem] = useState(null); + + const allItems = useMemo(() => groups.flatMap((group) => group.items || []), [groups]); + + const load = useCallback(async () => { + setLoading(true); + try { + const res = await getUploadResourceHistory({ + resourceType: resourceType || undefined, + keyword: keyword || undefined, + page, + pageSize, + scene: 'record', + }); + setGroups(res.groups || []); + setTotalDays(res.totalDays || 0); + setSelectedIds(new Set()); + } catch (error: any) { + message.error(error?.message || '加载历史上传素材失败'); + setGroups([]); + setTotalDays(0); + } finally { + setLoading(false); + } + }, [keyword, page, pageSize, resourceType]); + + useEffect(() => { + load(); + }, [load]); + + const toggle = (id: string) => { + setSelectedIds((prev) => { + const next = new Set(prev); + if (next.has(id)) next.delete(id); + else next.add(id); + return next; + }); + }; + + const selectAll = () => { + const ids = allItems.map((item) => item.id); + setSelectedIds((prev) => (prev.size === ids.length ? new Set() : new Set(ids))); + }; + + const handleLoadMoreDay = async (group: UploadResourceHistoryDayGroup) => { + const nextPage = (group.page || 1) + 1; + try { + const res = await getUploadResourceHistoryItems(group.generatedDate, { + resourceType: resourceType || undefined, + keyword: keyword || undefined, + page: nextPage, + pageSize: 10, + scene: 'record', + }); + setGroups((prev) => prev.map((item) => item.generatedDate === group.generatedDate ? { ...item, page: nextPage, items: [...item.items, ...(res.items || [])] } : item)); + } catch (error: any) { + message.error(error?.message || '加载更多失败'); + } + }; + + const handleDeleteOne = (item: UploadResourceHistoryItem) => { + Modal.confirm({ + title: '确认删除历史上传素材', + content: `确定删除 ${item.fileName || item.id} 吗?删除后会释放上传容量,并在提交成功后清理真实文件。`, + okText: '删除', + okButtonProps: { danger: true }, + cancelText: '取消', + onOk: async () => { + try { + await deleteUpload(item.resourceUrl); + message.success('删除成功'); + load(); + } catch (error: any) { + message.error(error?.message || '删除失败'); + } + }, + }); + }; + + const handleBatchDelete = () => { + if (selectedIds.size === 0) { + message.warning('请先选择要删除的上传素材'); + return; + } + if (selectedIds.size > 30) { + message.warning('一次最多删除 30 条上传素材'); + return; + } + const ids = Array.from(selectedIds); + Modal.confirm({ + title: '确认批量删除历史上传素材', + content: `确定删除选中的 ${ids.length} 条上传素材吗?删除后会释放上传容量,并在提交成功后清理真实文件。`, + okText: '删除', + okButtonProps: { danger: true }, + cancelText: '取消', + onOk: async () => { + try { + const res = await deleteUploadResourceHistoryBatch(ids); + message.success(`删除成功,释放 ${bytesText(res?.releasedSizeBytes || 0)}`); + load(); + } catch (error: any) { + message.error(error?.message || '批量删除失败'); + } + }, + }); + }; + + const handleDownload = (item: UploadResourceHistoryItem) => { + const link = document.createElement('a'); + link.href = buildPreviewUrl(item.resourceUrl); + link.download = item.fileName || item.id; + document.body.appendChild(link); + link.click(); + document.body.removeChild(link); + }; + + const renderMedia = (item: UploadResourceHistoryItem, size: 'card' | 'preview' = 'card') => { + const url = buildPreviewUrl(item.previewUrl || item.displayUrl || item.resourceUrl); + const style = size === 'card' + ? { width: '100%', height: 130, objectFit: 'cover' as const } + : { width: '100%', maxHeight: 620, objectFit: 'contain' as const }; + if (item.resourceType === 'image') return {item.fileName; + if (item.resourceType === 'video') return
- {filterType !== 'private_portrait' && ( + {filterType !== 'private_portrait' && filterType !== 'upload_resource' && (
{/* 多选模式按钮 */} {isSelectionMode ? ( @@ -1120,6 +1141,8 @@ const GeneratedRecord: React.FC = () => {
{filterType === 'private_portrait' ? ( + ) : filterType === 'upload_resource' ? ( + ) : ( <> {/* Second row filter: 视频 / 图片 */} diff --git a/video-gen-app/src/pages/InitialReplication.tsx b/video-gen-app/src/pages/InitialReplication.tsx index 08874b46..9985b002 100644 --- a/video-gen-app/src/pages/InitialReplication.tsx +++ b/video-gen-app/src/pages/InitialReplication.tsx @@ -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(null); const [videoUrl, setVideoUrl] = useState(''); + const [videoResourceId, setVideoResourceId] = useState(''); + const [videoDurationSeconds, setVideoDurationSeconds] = useState(null); const [imageFile, setImageFile] = useState(null); const [imageUrl, setImageUrl] = useState(''); + const [imageResourceId, setImageResourceId] = useState(''); 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 ? (
- {["2K", "4K"].map(tier => ( + {["1K", "2K", "4K"].map(tier => (
{ without a `);let s=e.getAttribute(`formaction`)||o.getAttribute(`action`);if(r=s?se(s,t):null,n=e.getAttribute(`formmethod`)||o.getAttribute(`method`)||kt,i=Ht(e.getAttribute(`formenctype`))||Ht(o.getAttribute(`enctype`))||At,a=new FormData(o,e),!Bt()){let{name:t,type:n,value:r}=e;if(n===`image`){let e=t?`${t}.`:``;a.append(`${e}x`,`0`),a.append(`${e}y`,`0`)}else t&&a.append(t,r)}}else if(jt(e))throw Error(`Cannot submit element that is not ,