|
|
|
@@ -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 () => {
|
|
|
|
|