diff --git a/apps/web/src/api/assets/assets.ts b/apps/web/src/api/assets/assets.ts index 96a5b0840..502d603c5 100644 --- a/apps/web/src/api/assets/assets.ts +++ b/apps/web/src/api/assets/assets.ts @@ -69,10 +69,13 @@ interface SmartMatchWrappedItem { breakdown?: unknown } -export const smartMatchAssets = async (libraryId: string): Promise => { - const response = await apiClient.post("/assets/smart-match", { - library_id: libraryId, - }) +export const smartMatchAssets = async ( + libraryId: string, + limit?: number, +): Promise => { + const payload: Record = { library_id: libraryId } + if (limit && limit > 0) payload.limit = limit + const response = await apiClient.post("/assets/smart-match", payload) const rawItems: SmartMatchWrappedItem[] = response.data?.items ?? [] const items = rawItems .map((it) => diff --git a/apps/web/src/pages/generate/hooks/step2-materials/useSmartMatch.ts b/apps/web/src/pages/generate/hooks/step2-materials/useSmartMatch.ts index bf1733d33..e297b4626 100755 --- a/apps/web/src/pages/generate/hooks/step2-materials/useSmartMatch.ts +++ b/apps/web/src/pages/generate/hooks/step2-materials/useSmartMatch.ts @@ -1,12 +1,33 @@ import { useState, useCallback } from "react" import { message } from "antd" import type { AssetItem } from "@/api/assets" +import type { TemplateSegment } from "@/api/templates/types" import { smartMatchAssets, isAssetUsable } from "@/api/assets" interface UseSmartMatchOptions { libraryId: string materials: { items: AssetItem[]; total: number } onSmartSelectedIdsChange: (ids: string[]) => void + /** 当前模板的 segments,用于根据总时长计算 limit */ + templateSegments?: TemplateSegment[] +} + +/** 默认 limit(拿不到目标时长时的兜底上限) */ +const DEFAULT_LIMIT = 10 +/** 每个素材切片按 15 秒估算所需素材数 */ +const SECONDS_PER_ASSET = 15 + +/** + * 根据模板 segments 计算所需素材数量上限。 + * 取每个 segment 的 duration_min 之和作为目标视频总时长, + * 再按 15 秒/素材估算需要多少个素材;结果钳制到 [1, 200] 区间(后端 limit 上限 200)。 + */ +function computeLimitFromSegments(segments?: TemplateSegment[]): number { + if (!segments || segments.length === 0) return DEFAULT_LIMIT + const totalSeconds = segments.reduce((sum, seg) => sum + (seg.duration_min || 0), 0) + if (totalSeconds <= 0) return DEFAULT_LIMIT + const limit = Math.ceil(totalSeconds / SECONDS_PER_ASSET) + return Math.max(1, Math.min(limit, 200)) } /** @@ -18,6 +39,7 @@ export function useSmartMatch({ libraryId, materials, onSmartSelectedIdsChange, + templateSegments, }: UseSmartMatchOptions) { const [smartMatching, setSmartMatching] = useState(false) const [hasMatched, setHasMatched] = useState(false) @@ -42,12 +64,14 @@ export function useSmartMatch({ setSmartMatching(true) try { + // 根据目标视频时长计算合理的素材数量上限,避免"有几个选几个" + const limit = computeLimitFromSegments(templateSegments) + // 调用后端智能匹配 API(后端也会排除已用尽素材,这里前端兜底过滤) - // items 已在 API 层归一化(兼容后端 {asset, score} 包装结构); - // 这里再过滤一遍无 id/已用尽项,杜绝 undefined id 流入预览链路 - const result = await smartMatchAssets(libraryId) + const result = await smartMatchAssets(libraryId, limit) + // 兜底过滤:id 为空或不可用的素材不参与匹配(smartMatchAssets 已做归一化,这里双保险) const matched = (result.items ?? []).filter((a) => !!a?.id && isAssetUsable(a)) - const matchedIds = matched.map((a) => a.id) + const matchedIds = matched.map((a: AssetItem) => a.id) if (matchedIds.length > 0) { onSmartSelectedIdsChange(matchedIds) @@ -67,7 +91,7 @@ export function useSmartMatch({ } finally { setSmartMatching(false) } - }, [libraryId, materials.items, onSmartSelectedIdsChange]) + }, [libraryId, materials.items, onSmartSelectedIdsChange, templateSegments]) /* ── 换一批 = 重新触发智能匹配 ── */ const handleRefreshMatch = useCallback(async () => { diff --git a/apps/web/src/pages/generate/hooks/useStep2Materials.ts b/apps/web/src/pages/generate/hooks/useStep2Materials.ts index 11ab59be5..7f9a0388f 100644 --- a/apps/web/src/pages/generate/hooks/useStep2Materials.ts +++ b/apps/web/src/pages/generate/hooks/useStep2Materials.ts @@ -51,6 +51,7 @@ export function useStep2Materials({ libraryId: selectedLibraryId, materials: selectableMaterials, onSmartSelectedIdsChange, + templateSegments, }) /* ── 自动触发智能匹配:选择视频库后自动调用 ── */