diff --git a/apps/web/src/api/generation/types.ts b/apps/web/src/api/generation/types.ts index 745222caf..6b877681e 100755 --- a/apps/web/src/api/generation/types.ts +++ b/apps/web/src/api/generation/types.ts @@ -13,6 +13,10 @@ export interface CreatePreviewRequest { video_title?: string duration?: number video_ratio?: string + /** 输出视频宽度(与 video_ratio 匹配,如 9:16 → 1080) */ + output_width?: number + /** 输出视频高度(与 video_ratio 匹配,如 9:16 → 1920) */ + output_height?: number /* 标题烧录配置(可选,传入后 ASS 渲染标题到预览视频中) */ title_config?: { text?: string diff --git a/apps/web/src/pages/generate/GeneratePage.tsx b/apps/web/src/pages/generate/GeneratePage.tsx index ec232cfc1..8e644c8d0 100644 --- a/apps/web/src/pages/generate/GeneratePage.tsx +++ b/apps/web/src/pages/generate/GeneratePage.tsx @@ -15,6 +15,7 @@ import { Modal, message } from "antd" import { useNavigate } from "react-router-dom" import type { VoiceClone } from "@/api/voice-clone" import { useCloneProgress } from "@/hooks/useCloneProgress" +import { calculateResolution } from "./utils/calculateResolution" import CloneModal from "@/components/voice/CloneModal" import GenerateHeader from "./components/GenerateHeader" import { @@ -119,6 +120,9 @@ const GeneratePage: React.FC = () => { [bgm, currentTemplate], ) + /* ── 输出分辨率(根据视频比例计算) ── */ + const resolution = useMemo(() => calculateResolution(videoRatio), [videoRatio]) + /* ── 加载素材详情(仅用于配音时长校验,不用于播放) ── */ const previewAssetsEnabled = previewAssetIds.length > 0 const { assets: previewAssets } = usePreviewAssets(previewAssetIds, previewAssetsEnabled) @@ -143,6 +147,8 @@ const GeneratePage: React.FC = () => { asset_ids: previewAssetIds, duration: duration || 30, video_ratio: videoRatio, + output_width: resolution.width, + output_height: resolution.height, ...(voiceLibraryId ? { voice_library_id: voiceLibraryId } : {}), bgm_config: { enabled: bgm !== false, @@ -168,6 +174,7 @@ const GeneratePage: React.FC = () => { previewAssetIds, duration, videoRatio, + resolution, voiceLibraryId, bgm, bgmConfig, diff --git a/apps/web/src/pages/generate/hooks/useGenerateVideo.ts b/apps/web/src/pages/generate/hooks/useGenerateVideo.ts index d9eb41b0a..34e9afebf 100755 --- a/apps/web/src/pages/generate/hooks/useGenerateVideo.ts +++ b/apps/web/src/pages/generate/hooks/useGenerateVideo.ts @@ -11,6 +11,7 @@ import type { UseGenerateVideoProps } from "./generate-video/types" import { getGenerationPhase } from "./generate-video/phase" import { useGenerationPolling } from "./generate-video/useGenerationPolling" import { validateGenerateInputs } from "./generate-video/buildPayload" +import { calculateResolution } from "../utils/calculateResolution" import { extractBackendError, translateError } from "./generate-video/errorUtils" export function useGenerateVideo(props: UseGenerateVideoProps) { @@ -60,37 +61,10 @@ export function useGenerateVideo(props: UseGenerateVideoProps) { clearTimer() try { - // 解析分辨率 - const ratio = props.videoRatio || "9:16" - let outputWidth: number - let outputHeight: number - - if (ratio.includes(":")) { - const [rw, rh] = ratio.split(":").map(Number) - if (rw > 0 && rh > 0) { - const [longSide, shortSide] = rw < rh ? [rh, rw] : [rw, rh] - const baseLong = 1920 - const baseShort = Math.round((baseLong * shortSide) / longSide) - const evenShort = baseShort - (baseShort % 2) - if (rw < rh) { - outputWidth = evenShort - outputHeight = baseLong - } else { - outputWidth = baseLong - outputHeight = evenShort - } - } else { - outputWidth = 1080 - outputHeight = 1920 - } - } else if (ratio.includes("x")) { - const [wStr, hStr] = ratio.split("x") - outputWidth = parseInt(wStr, 10) || 1080 - outputHeight = parseInt(hStr, 10) || 1920 - } else { - outputWidth = 1080 - outputHeight = 1920 - } + // 解析分辨率(共享工具函数,与预览 API 一致) + const { width: outputWidth, height: outputHeight } = calculateResolution( + props.videoRatio || "9:16", + ) const assetIds = props.materialMode === "auto" ? props.smartSelectedIds : props.selectedMaterials diff --git a/apps/web/src/pages/generate/utils/calculateResolution.ts b/apps/web/src/pages/generate/utils/calculateResolution.ts new file mode 100644 index 000000000..e26b241df --- /dev/null +++ b/apps/web/src/pages/generate/utils/calculateResolution.ts @@ -0,0 +1,46 @@ +/** + * 根据视频比例计算输出分辨率 + * + * 规则: + * - 长边固定 1920 + * - 短边按 1920 × (短/长) 计算,对齐到偶数 + * - 9:16 → 1080×1920(竖屏) + * - 16:9 → 1920×1080(横屏) + * - 1:1 → 1920×1920 + * - 无法解析时 fallback 到 1080×1920 + */ +export interface Resolution { + width: number + height: number +} + +export function calculateResolution(ratio: string): Resolution { + if (!ratio) return { width: 1080, height: 1920 } + + if (ratio.includes(":")) { + const [rw, rh] = ratio.split(":").map(Number) + if (rw > 0 && rh > 0) { + const [longSide, shortSide] = rw < rh ? [rh, rw] : [rw, rh] + const baseLong = 1920 + const baseShort = Math.round((baseLong * shortSide) / longSide) + const evenShort = baseShort - (baseShort % 2) + if (rw < rh) { + // 竖屏:短边是宽,长边是高 + return { width: evenShort, height: baseLong } + } else { + // 横屏:长边是宽,短边是高 + return { width: baseLong, height: evenShort } + } + } + return { width: 1080, height: 1920 } + } + + if (ratio.includes("x")) { + const [wStr, hStr] = ratio.split("x") + const w = parseInt(wStr, 10) || 1080 + const h = parseInt(hStr, 10) || 1920 + return { width: w, height: h } + } + + return { width: 1080, height: 1920 } +}