diff --git a/video-gen-admin/src/App.tsx b/video-gen-admin/src/App.tsx index 92eaf1d6..0d785256 100644 --- a/video-gen-admin/src/App.tsx +++ b/video-gen-admin/src/App.tsx @@ -32,6 +32,7 @@ import AdminShotReplications from './pages/AdminShotReplications'; import AdminShotTaskSetDetail from './pages/AdminShotTaskSetDetail'; import AdminReplicationProjectDetail from './pages/AdminReplicationProjectDetail'; import AdminVideoPromptSchemaConfig from './pages/AdminVideoPromptSchemaConfig'; +import AdminVideoUpscale from './pages/AdminVideoUpscale'; import AdminContactRequests from './pages/AdminContactRequests'; import AdminHomeMaterials from './pages/AdminHomeMaterials'; import AdminPreTestTemplates from './pages/AdminPreTestTemplates'; @@ -98,6 +99,7 @@ const App = () => { } /> } /> } /> + } /> } /> } /> } /> diff --git a/video-gen-admin/src/api/index.ts b/video-gen-admin/src/api/index.ts index d9a02ed8..9e760f29 100644 --- a/video-gen-admin/src/api/index.ts +++ b/video-gen-admin/src/api/index.ts @@ -17,6 +17,7 @@ import type { AdminTeam, AdminTeamListResponse, AdminTeamOption, AdminTeamPayload, AdminTeamQueryParams, PrivatePortraitConfig, PrivatePortraitProjectListOut, PrivatePortraitAssetListOut, AdminUploadFileResult, AdminUploadResourceType, AdminUploadScene, + VideoUpscaleConfigOut, VideoUpscaleConfigSavePayload, } from '../types'; import type { @@ -41,6 +42,18 @@ import type { HomeMaterialWatermarkQueryParams, } from '../types'; + +// ── Video Upscale ──────────────────────────────────────── + +export async function getVideoUpscaleConfig(): Promise { + return api.get('/admin/video-upscale/config'); +} + +export async function saveVideoUpscaleConfig(payload: VideoUpscaleConfigSavePayload): Promise { + return api.put('/admin/video-upscale/config', payload); +} + + // ── Auth ────────────────────────────────────────────────── export async function login(username: string, password: string, captchaToken?: string, rememberMe?: boolean): Promise { diff --git a/video-gen-admin/src/pages/AdminVideoUpscale.tsx b/video-gen-admin/src/pages/AdminVideoUpscale.tsx new file mode 100644 index 00000000..66bde398 --- /dev/null +++ b/video-gen-admin/src/pages/AdminVideoUpscale.tsx @@ -0,0 +1,262 @@ +import React, { useCallback, useEffect, useState } from 'react'; +import { + App, + Button, + Card, + Col, + Empty, + Row, + Select, + Space, + Spin, + Switch, + Tag, + Typography, +} from 'antd'; +import { DeleteOutlined, PlusOutlined, ReloadOutlined, SaveOutlined } from '@ant-design/icons'; + +import { getVideoUpscaleConfig, saveVideoUpscaleConfig } from '../api'; +import type { + VideoUpscaleConfigData, + VideoUpscaleProcessorKey, + VideoUpscaleResolutionRule, +} from '../types'; + +const { Title, Text, Paragraph } = Typography; + +const PROCESSORS: Array<{ key: VideoUpscaleProcessorKey; label: string }> = [ + { key: 'local_ffmpeg_crop_v1', label: '本地 FFmpeg(crop)' }, + { key: 'volc_standard_v1', label: '火山画质增强(标准版)' }, + { key: 'volc_professional_v1', label: '火山画质增强(专业版)' }, + { key: 'volc_large_model_v1', label: '火山画质增强(大模型)' }, +]; + +const RESOLUTION_OPTIONS = ['480p', '720p', '1080p', '2K', '4K'].map((value) => ({ + label: value, + value, +})); + +const defaultRule = (): VideoUpscaleResolutionRule => ({ + targetResolution: '1080p', + providerGenerationResolution: '720p', + processorKey: 'local_ffmpeg_crop_v1', + enabled: true, +}); + +function normalizeConfig(data: VideoUpscaleConfigData): VideoUpscaleConfigData { + return { + enabled: !!data.enabled, + version: Number(data.version || 1), + deleteSourceAfterSuccess: data.deleteSourceAfterSuccess !== false, + rules: Array.isArray(data.rules) ? data.rules.map((rule) => ({ ...rule })) : [], + }; +} + +function toSavePayload(data: VideoUpscaleConfigData) { + return { + data: { + enabled: data.enabled, + version: data.version, + delete_source_after_success: data.deleteSourceAfterSuccess, + rules: data.rules.map((rule) => ({ + target_resolution: rule.targetResolution, + provider_generation_resolution: rule.providerGenerationResolution, + processor_key: rule.processorKey, + enabled: rule.enabled, + })), + }, + }; +} + +const AdminVideoUpscale: React.FC = () => { + const { message, modal } = App.useApp(); + const [loading, setLoading] = useState(true); + const [saving, setSaving] = useState(false); + const [config, setConfig] = useState(null); + + const load = useCallback(async () => { + setLoading(true); + try { + const result = await getVideoUpscaleConfig(); + setConfig(normalizeConfig(result.data)); + } catch (error) { + message.error(error instanceof Error ? error.message : '读取视频超分配置失败'); + } finally { + setLoading(false); + } + }, [message]); + + useEffect(() => { + void load(); + }, [load]); + + const updateRule = (index: number, patch: Partial) => { + setConfig((current) => { + if (!current) return current; + return { + ...current, + rules: current.rules.map((item, itemIndex) => itemIndex === index ? { ...item, ...patch } : item), + }; + }); + }; + + const removeRule = (index: number) => { + setConfig((current) => current ? { + ...current, + rules: current.rules.filter((_, itemIndex) => itemIndex !== index), + } : current); + }; + + const addRule = () => { + setConfig((current) => current ? { ...current, rules: [...current.rules, defaultRule()] } : current); + }; + + const save = async () => { + if (!config) return; + const targets = config.rules + .filter((item) => item.enabled) + .map((item) => item.targetResolution.trim().toLowerCase()); + if (new Set(targets).size !== targets.length) { + message.error('同一个客户目标分辨率只能存在一条启用规则'); + return; + } + + setSaving(true); + try { + const result = await saveVideoUpscaleConfig(toSavePayload(config)); + setConfig(normalizeConfig(result.data)); + message.success(`视频超分配置已保存,版本 ${result.data.version}`); + } catch (error) { + message.error(error instanceof Error ? error.message : '保存失败'); + } finally { + setSaving(false); + } + }; + + if (loading || !config) { + return ( +
+ +
+ ); + } + + return ( + + + + + 视频超分配置 + + 规则只按客户选择的目标分辨率匹配。视频比例和最终像素由客户任务参数在创建任务时自动计算并固化快照。 + + + + + 配置版本 {config.version} + + + + + + + + + 全局开启超分 + setConfig({ ...config, enabled })} + /> + 关闭后新任务走原流程,已经创建的任务仍按自身快照执行。 + + + 超分成功后删除源视频 + setConfig({ ...config, deleteSourceAfterSuccess })} + /> + + 默认开启。关闭仅用于调试,会保留超分前源视频并持续占用服务器磁盘;超分失败时始终保留源视频。 + + + + + + } onClick={addRule}>新增规则} + > + {config.rules.length === 0 ? ( + + ) : ( + + {config.rules.map((rule, index) => { + const duplicate = rule.enabled && config.rules.filter( + (item) => item.enabled && item.targetResolution.toLowerCase() === rule.targetResolution.toLowerCase(), + ).length > 1; + return ( + + + + 客户选择分辨率 + updateRule(index, { providerGenerationResolution: value })} + /> + + + 处理方式 +