From 79c09151ba5ffb5ab238825e1e16c745850c93d8 Mon Sep 17 00:00:00 2001 From: GinHa <15201596918@163.com> Date: Tue, 21 Jul 2026 14:01:08 +0800 Subject: [PATCH 1/2] =?UTF-8?q?=E7=88=86=E6=AC=BE/=E6=8B=86=E9=95=9C?= =?UTF-8?q?=E7=94=9F=E6=88=90=E7=AE=80=E5=8C=963=E4=B8=AA=E6=AD=A5?= =?UTF-8?q?=E9=AA=A4=20|=20=E9=A1=B9=E7=9B=AE=E7=94=9F=E6=88=90=E5=8F=AF?= =?UTF-8?q?=E6=90=BA=E5=B8=A6=E9=99=84=E4=BB=B6=E6=8E=A7=E5=88=B6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- video-gen-admin/src/api/index.ts | 40 +- .../src/pages/AdminGenerationRecords.tsx | 252 +--- .../src/pages/AdminHotOpeningReplications.tsx | 3 +- .../pages/AdminReplicationProjectDetail.tsx | 43 +- .../src/pages/AdminShotTaskSetDetail.tsx | 12 +- video-gen-admin/src/types/index.ts | 10 + ...add_module_flow_version_and_generation_.py | 87 ++ video-gen-api/app/api/v1/admin.py | 290 +--- video-gen-api/app/api/v1/generation.py | 202 ++- .../app/api/v1/hot_opening_replicate.py | 38 +- video-gen-api/app/api/v1/shot_replicate.py | 30 +- video-gen-api/app/api/v2/__init__.py | 8 + .../app/api/v2/hot_opening_replicate.py | 255 ++++ video-gen-api/app/api/v2/shot_replicate.py | 272 ++++ video-gen-api/app/config.py | 3 + video-gen-api/app/enums/celery_queue.py | 1 + video-gen-api/app/enums/common.py | 20 + .../app/enums/hot_opening_replicate.py | 1 + .../app/enums/module_generation_flow.py | 1 + video-gen-api/app/enums/shot_replicate.py | 1 + video-gen-api/app/main.py | 2 + video-gen-api/app/models/generation_record.py | 3 + .../app/models/module_generation_project.py | 7 + .../app/models/module_generation_step.py | 12 +- video-gen-api/app/schemas/generation.py | 6 + .../app/schemas/hot_opening_replicate.py | 7 + .../app/schemas/module_generation_v2.py | 113 ++ .../app/schemas/recent_generation.py | 4 + video-gen-api/app/schemas/shot_replicate.py | 8 + .../services/generation/ai/engine_service.py | 6 +- .../services/generation/billing_service.py | 13 +- .../generation/media_reference_service.py | 148 ++ .../generation/pipeline/owner_service.py | 12 + .../services/generation/provider_service.py | 11 +- .../services/generation/recovery_service.py | 66 + .../generation/task_factory_service.py | 11 + .../services/hot_opening_replicate_service.py | 24 +- .../hot_opening_video_prompt_service.py | 21 +- .../services/module_async_recovery_service.py | 19 +- .../module_generation_flow_base_service.py | 12 + .../module_generation_step_common_service.py | 14 +- .../services/module_generation_v2/__init__.py | 1 + .../services/module_generation_v2/config.py | 75 + .../module_generation_v2/dispatch_service.py | 105 ++ .../module_generation_v2/flow_service.py | 1257 +++++++++++++++++ .../module_generation_v2/media_service.py | 35 + .../app/services/recent_generation_service.py | 7 + .../services/shot_replicate_flow_service.py | 24 +- .../shot_replicate_taskset_service.py | 51 +- video-gen-api/app/tasks/celery_app.py | 22 + .../app/tasks/generation_recovery_tasks.py | 30 + .../app/tasks/module_generation_v2_tasks.py | 81 ++ video-gen-app/src/api/index.ts | 72 +- video-gen-app/src/pages/GeneratePage.tsx | 246 +++- video-gen-app/src/pages/HomePage.tsx | 4 +- video-gen-app/src/pages/InitialInfo.tsx | 323 ++++- .../src/pages/InitialReplication.tsx | 198 ++- video-gen-app/src/pages/RemoveInfo.tsx | 226 +-- video-gen-app/src/pages/RemoveRw.tsx | 301 +++- video-gen-app/src/types/index.ts | 28 +- 60 files changed, 4250 insertions(+), 924 deletions(-) create mode 100644 video-gen-api/alembic/versions/d8ebe79ab575_add_module_flow_version_and_generation_.py create mode 100644 video-gen-api/app/api/v2/__init__.py create mode 100644 video-gen-api/app/api/v2/hot_opening_replicate.py create mode 100644 video-gen-api/app/api/v2/shot_replicate.py create mode 100644 video-gen-api/app/schemas/module_generation_v2.py create mode 100644 video-gen-api/app/services/generation/media_reference_service.py create mode 100644 video-gen-api/app/services/module_generation_v2/__init__.py create mode 100644 video-gen-api/app/services/module_generation_v2/config.py create mode 100644 video-gen-api/app/services/module_generation_v2/dispatch_service.py create mode 100644 video-gen-api/app/services/module_generation_v2/flow_service.py create mode 100644 video-gen-api/app/services/module_generation_v2/media_service.py create mode 100644 video-gen-api/app/tasks/module_generation_v2_tasks.py diff --git a/video-gen-admin/src/api/index.ts b/video-gen-admin/src/api/index.ts index 0de34519..81e9875d 100644 --- a/video-gen-admin/src/api/index.ts +++ b/video-gen-admin/src/api/index.ts @@ -618,28 +618,26 @@ export async function getOpenTypeAll(): Promise<{ data: OpenTypeItem[] }> { // ── Generation Records (Admin) ───────────────────────────── export async function getAdminGenerationRecords(params?: { - userId?: string; status?: string; page?: number; pageSize?: number; + userId?: string; + status?: string; + engineId?: string; + includeMediaReferences?: boolean; + page?: number; + pageSize?: number; }): Promise<{ total: number; items: any[] }> { const q = new URLSearchParams(); if (params?.userId) q.set('user_id', params.userId); if (params?.status) q.set('status', params.status); + if (params?.engineId) q.set('engine_id', params.engineId); + if (params?.includeMediaReferences !== undefined) { + q.set('include_media_references', String(params.includeMediaReferences)); + } if (params?.page) q.set('page', String(params.page)); if (params?.pageSize) q.set('page_size', String(params.pageSize)); const qs = q.toString(); return api.get(`/admin/generation-records${qs ? `?${qs}` : ''}`); } -export async function adminUpdateGenerationStatus( - recordId: string, status: string, videoUrl?: string -): Promise { - await api.put(`/admin/generation-records/${recordId}/status`, { status, video_url: videoUrl }); -} - -export async function adminGenerateVideo( - recordId: string, aspectRatio: string, resolution: string, image_size: string -): Promise { - await api.post(`/admin/generation-records/${recordId}/generate`, { aspect_ratio: aspectRatio, resolution, image_size }); -} // ── Generation AI Engines (Admin) ───────────────────────────── @@ -685,8 +683,13 @@ export async function getAdminHotOpeningTasks(params?: AdminHotOpeningTaskQueryP return api.get(`/hot-opening-replications/tasks${qs ? `?${qs}` : ''}`); } -export async function getAdminHotOpeningTaskDetail(projectId: string): Promise { - return api.get(`/hot-opening-replications/tasks/${projectId}`); +export async function getAdminHotOpeningTaskDetail( + projectId: string, + flowVersion: 'v1' | 'v2', +): Promise { + return flowVersion === 'v2' + ? api.get(`/v2/hot-opening-replications/tasks/${projectId}`) + : api.get(`/hot-opening-replications/tasks/${projectId}`); } export async function getAdminShotTaskSets(params?: AdminShotTaskSetQueryParams): Promise { @@ -725,8 +728,13 @@ export async function getAdminShotSegmentDetail(segmentId: string): Promise(`/shot-replications/segments/${segmentId}`); } -export async function getAdminShotProjectDetail(projectId: string): Promise { - return api.get(`/shot-replications/projects/${projectId}`); +export async function getAdminShotProjectDetail( + projectId: string, + flowVersion: 'v1' | 'v2', +): Promise { + return flowVersion === 'v2' + ? api.get(`/v2/shot-replications/projects/${projectId}`) + : api.get(`/shot-replications/projects/${projectId}`); } diff --git a/video-gen-admin/src/pages/AdminGenerationRecords.tsx b/video-gen-admin/src/pages/AdminGenerationRecords.tsx index 0a431f7d..784328a4 100644 --- a/video-gen-admin/src/pages/AdminGenerationRecords.tsx +++ b/video-gen-admin/src/pages/AdminGenerationRecords.tsx @@ -22,10 +22,9 @@ import { CloseCircleOutlined, SearchOutlined, VideoCameraOutlined, - ExclamationCircleOutlined, FileImageOutlined, } from '@ant-design/icons'; -import { getAdminGenerationRecords, adminUpdateGenerationStatus, adminGenerateVideo } from '../api'; +import { getAdminGenerationRecords, getVideoEngines, getImageEngines } from '../api'; import type { AdminGenerationRecord, GenerationAIMediaReference } from '../types'; import { formatDate } from '../utils/formatDate'; @@ -194,13 +193,14 @@ const AdminGenerationRecords: React.FC = () => { const [pageSize] = useState(20); const [filterStatus, setFilterStatus] = useState(''); const [filterUserId, setFilterUserId] = useState(''); + const [filterEngineId, setFilterEngineId] = useState(''); + const [filterIncludeMedia, setFilterIncludeMedia] = useState<'' | 'true' | 'false'>(''); + const [engineOptions, setEngineOptions] = useState>([]); const [reloadKey, setReloadKey] = useState(0); const [preview, setPreview] = useState(null); const [resourceState, setResourceState] = useState(EMPTY_RESOURCE_STATE); const [videoPlaying, setVideoPlaying] = useState(false); const videoRef = useRef(null); - const [updating, setUpdating] = useState(null); - const [genModal, setGenModal] = useState<{ record: AdminGenerationRecord; ratio: string; resolution: string; image_size: string } | null>(null); const load = useCallback(async () => { setLoading(true); @@ -208,6 +208,8 @@ const AdminGenerationRecords: React.FC = () => { const res = await getAdminGenerationRecords({ userId: filterUserId.trim() || undefined, status: filterStatus || undefined, + engineId: filterEngineId || undefined, + includeMediaReferences: filterIncludeMedia === '' ? undefined : filterIncludeMedia === 'true', page, pageSize, }); @@ -240,6 +242,10 @@ const AdminGenerationRecords: React.FC = () => { imageTokensUsed: item.imageTokensUsed || 0, imageProportion: item.imageProportion, imagePx: item.imagePx, + engineId: item.engineId, + engineName: item.engineName, + engineSnapshot: item.engineSnapshot, + includeMediaReferences: item.includeMediaReferences, }))); setTotal(res.total || 0); } catch { @@ -247,12 +253,32 @@ const AdminGenerationRecords: React.FC = () => { } finally { setLoading(false); } - }, [filterStatus, filterUserId, page, pageSize]); + }, [filterStatus, filterUserId, filterEngineId, filterIncludeMedia, page, pageSize]); useEffect(() => { load(); }, [load, reloadKey]); + useEffect(() => { + Promise.all([ + getImageEngines({ includeDeleted: true }), + getVideoEngines({ includeDeleted: true }), + ]) + .then(([imageEngines, videoEngines]) => { + const items = [...(imageEngines || []), ...(videoEngines || [])]; + const seen = new Set(); + setEngineOptions(items.reduce>((acc, item: any) => { + const id = String(item?.id || ''); + if (!id || seen.has(id)) return acc; + seen.add(id); + const deletedSuffix = item?.deletedAt ? '(已删除)' : ''; + acc.push({ value: id, label: item?.name ? `${item.name}${deletedSuffix} (${id})` : `${id}${deletedSuffix}` }); + return acc; + }, [])); + }) + .catch(() => setEngineOptions([])); + }, []); + useEffect(() => { if (!preview) { setResourceState(EMPTY_RESOURCE_STATE); @@ -349,33 +375,7 @@ const AdminGenerationRecords: React.FC = () => { }, 0); }; - const handleStatusUpdate = async (recordId: string, newStatus: string, videoUrl?: string) => { - setUpdating(recordId); - try { - await adminUpdateGenerationStatus(recordId, newStatus, videoUrl); - message.success('状态已更新'); - load(); - } catch (e: any) { - message.error(e?.message || '更新失败'); - } finally { - setUpdating(null); - } - }; - const handleGenerate = async () => { - if (!genModal) return; - setUpdating(genModal.record.id); - try { - await adminGenerateVideo(genModal.record.id, genModal.ratio, genModal.resolution, genModal.image_size); - message.success(`已提交${genModal.record.genType === 'video' ? '视频' : '图片'}生成`); - setGenModal(null); - load(); - } catch (e: any) { - message.error(e?.message || '生成失败'); - } finally { - setUpdating(null); - } - }; const columns = useMemo(() => [ { @@ -416,21 +416,27 @@ const AdminGenerationRecords: React.FC = () => { title: '参数', key: 'params', width: 160, render: (_: any, r: AdminGenerationRecord) => ( r.genType === 'video' ? ( - r.duration || r.aspectRatio || r.resolution ? ( - - {r.duration ? {r.duration}s : null} - {r.aspectRatio ? {r.aspectRatio} : null} - {r.resolution ? {r.resolution} : null} - - ) : 待配置 + + {r.duration ? {r.duration}s : null} + {r.aspectRatio ? {r.aspectRatio} : null} + {r.resolution ? {r.resolution} : null} + {!r.duration && !r.aspectRatio && !r.resolution ? 待配置 : null} + {r.engineName || r.engineId ? {r.engineName || truncateId(r.engineId || '')} : null} + + {r.includeMediaReferences ? '携带附件' : '不携带附件'}{r.references?.length ? `(${r.references.length})` : ''} + + ) : ( - r.imageSize || r.imageProportion || r.imagePx ? ( - - {r.imageSize ? {r.imageSize} : null} - {r.imageProportion ? {r.imageProportion} : null} - {r.imagePx ? {r.imagePx} : null} - - ) : 待配置 + + {r.imageSize ? {r.imageSize} : null} + {r.imageProportion ? {r.imageProportion} : null} + {r.imagePx ? {r.imagePx} : null} + {!r.imageSize && !r.imageProportion && !r.imagePx ? 待配置 : null} + {r.engineName || r.engineId ? {r.engineName || truncateId(r.engineId || '')} : null} + + {r.includeMediaReferences ? '携带附件' : '不携带附件'}{r.references?.length ? `(${r.references.length})` : ''} + + ) ), }, @@ -467,72 +473,14 @@ const AdminGenerationRecords: React.FC = () => { ), }, { - title: '操作', key: 'action', width: 150, fixed: 'right' as const, + title: '操作', key: 'action', width: 90, fixed: 'right' as const, render: (_: any, r: AdminGenerationRecord) => ( - - - {r.status === 'generating' ? ( - - ) : null} - {r.status === 'failed' ? ( - - ) : null} - {r.status === 'prompt_optimized' ? ( - <> - - - - ) : null} - + ), }, - ], [handleOpenPreview, updating]); + ], [handleOpenPreview]); const previewTypeConfig = preview ? (GEN_TYPE_MAP[preview.genType || ''] || { text: preview.genType || '-', color: 'default', icon: null }) : null; const previewStatusConfig = preview ? (STATUS_MAP[preview.status] || { color: 'default', text: preview.status || '-', icon: null }) : null; @@ -894,6 +842,27 @@ const AdminGenerationRecords: React.FC = () => { { value: 'failed', label: '失败' }, ]} /> + { setFilterIncludeMedia((v || '') as '' | 'true' | 'false'); setPage(1); }} + options={[ + { value: 'true', label: '携带附件' }, + { value: 'false', label: '不携带附件' }, + ]} + /> } @@ -1062,73 +1031,6 @@ const AdminGenerationRecords: React.FC = () => { )} - {/* Generate modal */} - - {genModal && (genModal.record.genType === 'video' ? : )} - {genModal && (genModal.record.genType === 'video' ? '生成视频' : '生成图片')} - - )} - open={!!genModal} - onCancel={() => setGenModal(null)} - onOk={handleGenerate} - okText="提交生成" - cancelText="取消" - confirmLoading={genModal ? updating === genModal.record.id : false} - width={420} - > - {genModal ? ( -
- {genModal.record.genType === 'video' ? ( - <> -
- 时长 - {genModal.record.duration || 5}s -
-
- 画面比例 - setGenModal((prev) => (prev ? { ...prev, resolution: v } : null))} - style={{ width: '100%' }} - options={['480p', '720p', '1080p'].map((r) => ({ value: r, label: r }))} - /> -
- - ) : ( - <> -
- 尺寸 - {genModal.record.imagePx || '-'} -
-
- 比例 - {genModal.record.imageProportion || '-'} -
-
- 分辨率 - ({ value: item.id, label: item.name }))} + /> + {currentRecord?.references?.length ? ( +
+ + 携带参考附件生成 +
+ ) : null} +
+ {/* Video params selection */} {mediaType !== "image" && (
{ backgroundColor: "transparent", }} > +
+ { + setEngineId(value); + const engine = videoEngines.find((item: any) => item.id === value); + setVideoAspectRatio(engine?.supportedRatios?.[0] || '16:9'); + setVideoResolution(engine?.supportedResolutions?.[0] || '480p'); + setVideoDuration(engine?.supportedDurations?.[0] || 5); + }} options={videoEngines.map((item: any) => ({ + value: item.id, + label: imageUrl && !supportsReferenceImage(item) ? `${item.name}(不支持参考图)` : item.name, + disabled: Boolean(imageUrl) && !supportsReferenceImage(item), + }))} /> +
+ ({ value, label: value }))} /> + setProductName(e.target.value)} - placeholder="请输入产品名称" + placeholder="请输入项目名称" style={{ height: 44, borderRadius: 8, border: '1px solid rgba(99, 102, 241, 0.2)' }} maxLength={10} showCount @@ -1197,12 +1236,12 @@ function RemoveInfo() {