From 51f9deecdec76c5460451d82bfe66502c7a4538f Mon Sep 17 00:00:00 2001 From: GinHa <15201596918@163.com> Date: Wed, 15 Jul 2026 13:03:13 +0800 Subject: [PATCH] =?UTF-8?q?AI=E5=88=9B=E4=BD=9C=E6=89=B9=E9=87=8F=E7=94=9F?= =?UTF-8?q?=E6=88=90=E4=BB=BB=E5=8A=A1=20main=20V1=20init?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../generation/GenerationTaskResourceGrid.tsx | 66 ++ .../src/pages/AdminGenerationAiRecords.tsx | 65 +- .../src/pages/AdminImageEngines.tsx | 54 +- .../src/pages/AdminVideoEngines.tsx | 32 +- video-gen-admin/src/types/index.ts | 15 + ...7_add_multi_generation_and_image_batch_.py | 230 ++++ video-gen-api/app/api/v1/admin.py | 4 +- video-gen-api/app/api/v1/generation.py | 4 +- video-gen-api/app/api/v1/generation_ai.py | 405 ++++--- video-gen-api/app/api/v1/image_engines.py | 4 + video-gen-api/app/api/v1/video_engines.py | 2 + video-gen-api/app/enums/__init__.py | 1 + video-gen-api/app/enums/common.py | 4 + video-gen-api/app/enums/generation_history.py | 35 +- .../app/enums/generation_provider.py | 40 + video-gen-api/app/enums/generation_task.py | 53 +- video-gen-api/app/enums/recent_generation.py | 23 +- .../app/models/chat_generation_task.py | 37 +- video-gen-api/app/models/image_engine.py | 27 +- video-gen-api/app/models/video_engine.py | 20 +- video-gen-api/app/schemas/generation_ai.py | 27 +- video-gen-api/app/schemas/image_engine.py | 45 +- video-gen-api/app/schemas/video_engine.py | 12 + .../app/services/generation/__init__.py | 1 + .../app/services/generation/ai/__init__.py | 1 + .../services/generation/ai/engine_service.py | 122 +++ .../generation/ai/image_batch_service.py | 532 ++++++++++ .../app/services/generation/ai/service.py | 987 ++++++++++++++++++ .../generation/ai/task_create_service.py | 587 +++++++++++ .../generation/ai/task_group_service.py | 426 ++++++++ .../services/generation/billing_service.py | 575 ++++++++++ .../services/generation/download_service.py | 143 +++ .../generation/history_delete_service.py | 554 ++++++++++ .../generation/history_meta_service.py | 283 +++++ .../app/services/generation/log_service.py | 134 +++ .../generation/module_hook_service.py | 41 + .../generation/poll_schedule_service.py | 153 +++ .../app/services/generation/prompt_service.py | 192 ++++ .../services/generation/provider_service.py | 226 ++++ .../services/generation/recovery_service.py | 943 +++++++++++++++++ .../app/services/generation/refund_service.py | 212 ++++ .../generation/task_factory_service.py | 212 ++++ .../services/hot_opening_replicate_service.py | 25 +- video-gen-api/app/services/image_gen.py | 395 +++++-- .../app/services/recent_generation_service.py | 24 +- .../services/shot_replicate_flow_service.py | 25 +- video-gen-api/app/services/video_gen.py | 2 +- video-gen-api/app/services/video_queue.py | 2 +- .../app/tasks/generation_create_tasks.py | 71 +- .../app/tasks/generation_download_tasks.py | 74 +- .../app/tasks/generation_poll_tasks.py | 12 +- .../app/tasks/generation_recovery_tasks.py | 6 +- .../app/tasks/shot_replicate_tasks.py | 2 +- video-gen-api/app/types/__init__.py | 1 + .../app/types/generation/__init__.py | 1 + .../app/types/generation/provider.py | 67 ++ .../generation/GenerationTaskResourceGrid.tsx | 119 +++ video-gen-app/src/pages/GenerateConver.tsx | 144 ++- video-gen-app/src/store/useAppStore.ts | 5 + 59 files changed, 8091 insertions(+), 413 deletions(-) create mode 100644 video-gen-admin/src/components/generation/GenerationTaskResourceGrid.tsx create mode 100644 video-gen-api/alembic/versions/abae3e1c70f7_add_multi_generation_and_image_batch_.py create mode 100644 video-gen-api/app/enums/generation_provider.py create mode 100644 video-gen-api/app/services/generation/__init__.py create mode 100644 video-gen-api/app/services/generation/ai/__init__.py create mode 100644 video-gen-api/app/services/generation/ai/engine_service.py create mode 100644 video-gen-api/app/services/generation/ai/image_batch_service.py create mode 100644 video-gen-api/app/services/generation/ai/service.py create mode 100644 video-gen-api/app/services/generation/ai/task_create_service.py create mode 100644 video-gen-api/app/services/generation/ai/task_group_service.py create mode 100644 video-gen-api/app/services/generation/billing_service.py create mode 100644 video-gen-api/app/services/generation/download_service.py create mode 100644 video-gen-api/app/services/generation/history_delete_service.py create mode 100644 video-gen-api/app/services/generation/history_meta_service.py create mode 100644 video-gen-api/app/services/generation/log_service.py create mode 100644 video-gen-api/app/services/generation/module_hook_service.py create mode 100644 video-gen-api/app/services/generation/poll_schedule_service.py create mode 100644 video-gen-api/app/services/generation/prompt_service.py create mode 100644 video-gen-api/app/services/generation/provider_service.py create mode 100644 video-gen-api/app/services/generation/recovery_service.py create mode 100644 video-gen-api/app/services/generation/refund_service.py create mode 100644 video-gen-api/app/services/generation/task_factory_service.py create mode 100644 video-gen-api/app/types/__init__.py create mode 100644 video-gen-api/app/types/generation/__init__.py create mode 100644 video-gen-api/app/types/generation/provider.py create mode 100644 video-gen-app/src/components/generation/GenerationTaskResourceGrid.tsx diff --git a/video-gen-admin/src/components/generation/GenerationTaskResourceGrid.tsx b/video-gen-admin/src/components/generation/GenerationTaskResourceGrid.tsx new file mode 100644 index 00000000..b2da61b0 --- /dev/null +++ b/video-gen-admin/src/components/generation/GenerationTaskResourceGrid.tsx @@ -0,0 +1,66 @@ +import React from 'react'; +import { Empty, Spin, Tag, Typography } from 'antd'; +import { PlayCircleFilled } from '@ant-design/icons'; +import type { GenerationAITaskOut } from '../../types'; + +interface Props { + task: GenerationAITaskOut; + resolveUrl: (url?: string | null) => string; + onPreview: (url: string, type: 'image' | 'video', title: string) => void; +} + +const spanByCount = (count: number, index: number): number => { + if (count <= 1) return 6; + if (count === 2 || count === 4) return 3; + if (count === 3) return index < 2 ? 3 : 6; + return index < 3 ? 2 : 3; +}; + +const LABELS: Record = { + pending: '待处理', queued: '已入队', preparing: '准备中', generating: '生成中', + creating_provider_task: '创建任务中', waiting_remote: '等待生成', polling: '轮询中', + result_ready: '结果就绪', download_queued: '等待下载', downloading: '下载中', + retry_waiting: '等待重试', completed: '已完成', failed: '生成失败', + download_failed: '下载失败', deleted: '已删除', +}; + +const GenerationTaskResourceGrid: React.FC = ({ task, resolveUrl, onPreview }) => { + const count = Math.max(1, Math.min(5, Number(task.generationCount || task.childItems?.length || 1))); + const sortedChildren = [...(task.childItems || [])].sort((a, b) => Number(a.generationIndex || 0) - Number(b.generationIndex || 0)); + const items: GenerationAITaskOut[] = sortedChildren.length + ? sortedChildren + : (count > 1 + ? Array.from({ length: count }, (_, index) => ({ ...task, id: `${task.id}-${index + 1}`, generationIndex: index + 1, childItems: [] })) + : [task]); + + return ( +
1 ? 8 : 0 }}> + {items.map((item, index) => { + const status = item.displayStatus || item.pipelineStage || item.status || 'pending'; + const isVideo = item.genType === 'video'; + const resultUrl = resolveUrl(isVideo ? item.videoUrl : item.imageUrl); + const coverUrl = resolveUrl(item.videoCoverUrl); + const active = ['pending', 'queued', 'preparing', 'generating', 'creating_provider_task', 'waiting_remote', 'polling', 'result_ready', 'download_queued', 'downloading', 'retry_waiting'].includes(status); + return ( +
+ {resultUrl && status !== 'deleted' ? ( + + ) : ( +
+ {active ? : } + {LABELS[status] || status} + {item.errorMessage && !active ? {item.errorMessage} : null} +
+ )} + {items.length > 1 ? #{item.generationIndex || index + 1} : null} +
+ ); + })} +
+ ); +}; + +export default GenerationTaskResourceGrid; diff --git a/video-gen-admin/src/pages/AdminGenerationAiRecords.tsx b/video-gen-admin/src/pages/AdminGenerationAiRecords.tsx index 4525e7c9..5c11a1fe 100644 --- a/video-gen-admin/src/pages/AdminGenerationAiRecords.tsx +++ b/video-gen-admin/src/pages/AdminGenerationAiRecords.tsx @@ -32,6 +32,7 @@ import dayjs from 'dayjs'; import { getAdminGenerationAiTasks } from '../api'; import type { GenerationAIMediaReference, GenerationAITaskOut } from '../types'; import { formatDate } from '../utils/formatDate'; +import GenerationTaskResourceGrid from '../components/generation/GenerationTaskResourceGrid'; const { RangePicker } = DatePicker; @@ -71,6 +72,8 @@ const STATUS_MAP: Record }, completed: { color: 'success', text: '已完成', icon: }, failed: { color: 'error', text: '失败', icon: }, + download_failed: { color: 'error', text: '下载失败', icon: }, + deleted: { color: 'default', text: '已删除', icon: }, }; const PIPELINE_STAGE_MAP: Record = { @@ -425,6 +428,25 @@ const AdminGenerationAiRecords: React.FC = () => { return {cfg.text}; }, }, + { + title: '生成数量', key: 'generationCount', width: 150, + render: (_: any, r: GenerationAITaskOut) => { + const count = Math.max(1, Number(r.generationCount || 1)); + if (count === 1) return 1份; + const children = r.childItems || []; + const completed = children.filter((item) => (item.displayStatus || item.status) === 'completed').length; + const failed = children.filter((item) => ['failed', 'download_failed'].includes(item.displayStatus || item.status)).length; + const deleted = children.filter((item) => (item.displayStatus || item.status) === 'deleted').length; + return ( + + {count}份 + + {completed}完成{failed ? ` / ${failed}失败` : ''}{deleted ? ` / ${deleted}删除` : ''} + + + ); + }, + }, { title: '引擎', key: 'engine', width: 160, render: (_: any, r: GenerationAITaskOut) => { @@ -1045,6 +1067,7 @@ const AdminGenerationAiRecords: React.FC = () => { +
@@ -1122,38 +1145,16 @@ const AdminGenerationAiRecords: React.FC = () => {
) : null} - {preview.status === 'completed' ? ( -
-
- - {preview.genType === 'video' ? '生成视频' : '生成图片'} - - {preview.genType === 'video' && preview.videoUrl ? ( - - ) : null} - {preview.genType === 'image' && preview.imageUrl ? ( - - ) : null} -
- {preview.genType === 'video' ? renderResultVideo() : renderResultImage()} -
- ) : null} +
+ + 生成资源(共 {preview.generationCount || 1} 份) + + +
{preview.status === 'failed' && preview.errorMessage ? (
diff --git a/video-gen-admin/src/pages/AdminImageEngines.tsx b/video-gen-admin/src/pages/AdminImageEngines.tsx index c329bdf1..60f86289 100644 --- a/video-gen-admin/src/pages/AdminImageEngines.tsx +++ b/video-gen-admin/src/pages/AdminImageEngines.tsx @@ -1,6 +1,6 @@ import React, { useEffect, useState } from 'react'; import { - Button, Card, Checkbox, Form, Input, message, Modal, Popconfirm, Select, Space, Switch, Table, Tag, Typography, + Button, Card, Checkbox, Form, Input, InputNumber, message, Modal, Popconfirm, Select, Space, Switch, Table, Tag, Typography, } from 'antd'; import { PictureOutlined, PlusOutlined, EditOutlined, DeleteOutlined, @@ -21,6 +21,11 @@ interface ImageEngine { generateUrl: string; isActive: boolean; priority: number; + multiGenerationEnabled: boolean; + maxGenerationCount: number; + multiImageMaxImages: number; + maxReferenceImageCount: number; + outputFormat: '' | 'png' | 'jpeg'; } function parseJsonArray(val: unknown): any[] { @@ -80,6 +85,7 @@ const AdminImageEngines: React.FC = () => { const [loading, setLoading] = useState(false); const [modal, setModal] = useState<{ open: boolean; engine: ImageEngine | null }>({ open: false, engine: null }); const [form] = Form.useForm(); + const multiGenerationEnabled = Form.useWatch('multiGenerationEnabled', form) ?? false; const load = async () => { setLoading(true); @@ -126,6 +132,11 @@ const AdminImageEngines: React.FC = () => { generate_url: values.generateUrl || '', is_active: values.isActive ?? true, priority: values.priority ?? 0, + multi_generation_enabled: values.multiGenerationEnabled ?? false, + max_generation_count: values.maxGenerationCount ?? 1, + multi_image_max_images: values.multiImageMaxImages ?? 15, + max_reference_image_count: values.maxReferenceImageCount ?? 14, + output_format: values.outputFormat ?? '', }; if (modal.engine) { await saveImageEngine({ id: modal.engine.id, ...payload }); @@ -168,6 +179,8 @@ const AdminImageEngines: React.FC = () => { form.resetFields(); form.setFieldsValue({ isActive: true, priority: 0, + multiGenerationEnabled: false, maxGenerationCount: 1, multiImageMaxImages: 15, + maxReferenceImageCount: 14, outputFormat: '', supportedModels: ['doubao-seedream-5-0-260128'], defaultSize: '2K', maxImageCount: 0, @@ -232,6 +245,18 @@ const AdminImageEngines: React.FC = () => { title: '最大图片', dataIndex: 'maxImageCount', width: 100, render: (v: number) => {v} 张, }, + { + title: '多份生成', dataIndex: 'multiGenerationEnabled', width: 100, + render: (v: boolean) => {v ? '开启' : '关闭'}, + }, + { + title: '数量上限', dataIndex: 'maxGenerationCount', width: 100, + render: (v: number, r: ImageEngine) => ( + 1 ? 'magenta' : 'default'}> + 最多 {r.multiGenerationEnabled ? (v || 1) : 1} 份 + + ), + }, { title: '状态', dataIndex: 'isActive', width: 80, render: (v: boolean) => {v ? '启用' : '停用'}, @@ -350,6 +375,33 @@ const AdminImageEngines: React.FC = () => { +
+ 多份生成能力 + + 管理后台只控制是否允许客户端选择多份及最大数量。客户端本次选择 2-5 份时,后端只调用一次火山同步组图 API;失败绝不降级成多次单图请求。 + +
+
+ + + + + + + + + + + + + + { const [loading, setLoading] = useState(false); const [modal, setModal] = useState<{ open: boolean; engine: VideoEngine | null }>({ open: false, engine: null }); const [form] = Form.useForm(); + const multiGenerationEnabled = Form.useWatch('multiGenerationEnabled', form) ?? false; const load = async () => { setLoading(true); @@ -80,6 +83,8 @@ const AdminVideoEngines: React.FC = () => { supports_universal_reference: values.supportsUniversalReference ?? true, is_active: values.isActive ?? true, priority: values.priority ?? 0, + multi_generation_enabled: values.multiGenerationEnabled ?? false, + max_generation_count: values.maxGenerationCount ?? 1, }; if (modal.engine) { await saveVideoEngine({ id: modal.engine.id, ...payload }); @@ -115,6 +120,7 @@ const AdminVideoEngines: React.FC = () => { form.resetFields(); form.setFieldsValue({ isActive: true, priority: 0, + multiGenerationEnabled: false, maxGenerationCount: 1, maxDuration: 30, maxImageCount: 2, maxVideoCount: 0, @@ -180,6 +186,18 @@ const AdminVideoEngines: React.FC = () => { title: '全能参考', dataIndex: 'supportsUniversalReference', width: 100, render: (v: boolean) => {v ? '支持' : '不支持'}, }, + { + title: '多份生成', dataIndex: 'multiGenerationEnabled', width: 100, + render: (v: boolean) => {v ? '开启' : '关闭'}, + }, + { + title: '数量上限', dataIndex: 'maxGenerationCount', width: 100, + render: (v: number, r: VideoEngine) => ( + 1 ? 'magenta' : 'default'}> + 最多 {r.multiGenerationEnabled ? (v || 1) : 1} 份 + + ), + }, { title: '状态', dataIndex: 'isActive', width: 80, render: (v: boolean) => {v ? '启用' : '停用'}, @@ -315,7 +333,19 @@ const AdminVideoEngines: React.FC = () => {
+
+ 多份生成能力 + + 管理后台只控制是否允许客户端选择多份及最大数量;客户端每次可在 1 到上限之间选择。 + +
+ + + + + + setGenerationCount(Number(value || 1))} + disabled={effectiveMaxGenerationCount <= 1} + size="small" + variant="borderless" + style={{ width: 68 }} + options={Array.from({ length: effectiveMaxGenerationCount }, (_, index) => ({ + value: index + 1, + label: `${index + 1}份`, + }))} + /> +
预估积分: {getEstimatedCredits()} diff --git a/video-gen-app/src/store/useAppStore.ts b/video-gen-app/src/store/useAppStore.ts index c721ffd6..4c16e997 100644 --- a/video-gen-app/src/store/useAppStore.ts +++ b/video-gen-app/src/store/useAppStore.ts @@ -18,6 +18,7 @@ interface AppState { // 生成配置状态 - 页面跳转时保留,刷新时重置 mediaType: string; countType: string; + generationCount: number; selectedRatio: string; selectedResolution: string; width: number; @@ -46,6 +47,7 @@ interface AppState { // 生成配置状态更新方法 setMediaType: (mediaType: string) => void; setCountType: (countType: string) => void; + setGenerationCount: (generationCount: number) => void; setImageSettings: (ratio: string, resolution: string, width: number, height: number) => void; setVideoSettings: (duration: number, aspectRatio: string, resolution: string) => void; setEngineOptions: (options: { ratios: string[]; resolutions: string[]; durations: number[] }) => void; @@ -71,6 +73,7 @@ export const useAppStore = create((set, get) => ({ // 生成配置状态初始值 mediaType: 'video', countType: '请选择', + generationCount: 1, selectedRatio: '1:1', selectedResolution: '2K', width: 2048, @@ -160,6 +163,7 @@ export const useAppStore = create((set, get) => ({ // 生成配置状态更新方法 setMediaType: (mediaType) => set({ mediaType }), setCountType: (countType) => set({ countType }), + setGenerationCount: (generationCount) => set({ generationCount }), setImageSettings: (ratio, resolution, width, height) => set({ selectedRatio: ratio, selectedResolution: resolution, width, height }), setVideoSettings: (duration, aspectRatio, resolution) => @@ -179,6 +183,7 @@ export const useAppStore = create((set, get) => ({ resetGenerationConfig: () => set({ mediaType: 'image', countType: '请选择', + generationCount: 1, selectedRatio: '1:1', selectedResolution: '2K', width: 2048,