import type { VideoCanvasEffectPreset } from "@/lib/video-canvas/config"; export type VideoCanvasAttachedEffect = Pick< VideoCanvasEffectPreset, | "author" | "commercial" | "id" | "model" | "prompt" | "thumbnailUrl" | "title" >; function isRecord(value: unknown): value is Record { return Boolean(value) && typeof value === "object" && !Array.isArray(value); } function readString(record: Record, key: string) { return typeof record[key] === "string" ? record[key].trim() : ""; } export function toVideoCanvasAttachedEffect( preset: VideoCanvasEffectPreset, ): VideoCanvasAttachedEffect { return { author: preset.author, commercial: preset.commercial, id: preset.id, model: preset.model, prompt: preset.prompt, thumbnailUrl: preset.thumbnailUrl, title: preset.title, }; } export function getVideoCanvasAttachedEffect( draftValues: Record | null | undefined, ): VideoCanvasAttachedEffect | null { if (!draftValues) return null; const params = isRecord(draftValues.params) ? draftValues.params : {}; const paramsEffect = Array.isArray(params.effects) && isRecord(params.effects[0]) ? params.effects[0] : null; const source = isRecord(draftValues.videoEffect) ? draftValues.videoEffect : paramsEffect; if (!source) return null; const id = readString(source, "id"); const title = readString(source, "title"); const thumbnailUrl = readString(source, "thumbnailUrl"); if (!id || !title || !thumbnailUrl) return null; return { author: readString(source, "author"), commercial: source.commercial !== false, id, model: readString(source, "model"), prompt: readString(source, "prompt"), thumbnailUrl, title, }; } export function setVideoCanvasAttachedEffect( draftValues: Record | null | undefined, preset: VideoCanvasEffectPreset | null, ) { const current = { ...(draftValues || {}) }; const params = isRecord(current.params) ? current.params : {}; if (preset) { const effect = toVideoCanvasAttachedEffect(preset); return { ...current, params: { ...params, effects: [effect], }, videoEffect: effect, }; } const { videoEffect: _removedEffect, ...nextDraftValues } = current; const { effects: _removedEffects, ...nextParams } = params; return { ...nextDraftValues, params: nextParams, }; } function normalizeSearchText(value: unknown) { return String(value || "") .normalize("NFKC") .toLocaleLowerCase() .replace(/[\s\p{P}\p{S}]+/gu, ""); } function getSearchFragments(query: string) { const characters = Array.from(query); const fragments = new Set(characters); for (let index = 0; index < characters.length - 1; index += 1) { fragments.add(`${characters[index]}${characters[index + 1]}`); } return Array.from(fragments).filter(Boolean); } function getWeightedSearchKeywords(value: string) { return String(value || "") .split(/[,,;;\n]+/) .map((item) => { const match = item.trim().match(/^(.*?)(?::(\d{1,3}))?$/); const keyword = normalizeSearchText(match?.[1]); const weight = Math.max(1, Math.min(100, Number(match?.[2]) || 1)); return keyword ? { keyword, weight } : null; }) .filter((item): item is { keyword: string; weight: number } => Boolean(item)); } export function rankVideoCanvasEffectPresets( presets: VideoCanvasEffectPreset[], query: string, ) { const normalizedQuery = normalizeSearchText(query); if (!normalizedQuery) return [...presets]; const fragments = getSearchFragments(normalizedQuery); return presets .map((preset, index) => { const title = normalizeSearchText(preset.title); const author = normalizeSearchText(preset.author); const weightedKeywords = getWeightedSearchKeywords(preset.searchKeywords); const keywords = weightedKeywords.map((item) => item.keyword).join(""); const prompt = normalizeSearchText(preset.prompt); let score = 0; if (title === normalizedQuery) score += 20_000; else if (title.includes(normalizedQuery)) score += 10_000; if (author.includes(normalizedQuery)) score += 6000; if (keywords.includes(normalizedQuery)) score += 4000; if (prompt.includes(normalizedQuery)) score += 2500; for (const item of weightedKeywords) { if ( item.keyword.includes(normalizedQuery) || normalizedQuery.includes(item.keyword) ) { score += item.weight * 100; } } for (const fragment of fragments) { const weight = Array.from(fragment).length > 1 ? 80 : 12; if (title.includes(fragment)) score += weight * 5; if (keywords.includes(fragment)) score += weight * 3; if (prompt.includes(fragment)) score += weight; if (author.includes(fragment)) score += weight; } return { index, preset, score }; }) .sort((left, right) => right.score - left.score || left.index - right.index) .map(({ preset }) => preset); }