165 lines
5.0 KiB
TypeScript
165 lines
5.0 KiB
TypeScript
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<string, unknown> {
|
||
return Boolean(value) && typeof value === "object" && !Array.isArray(value);
|
||
}
|
||
|
||
function readString(record: Record<string, unknown>, 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<string, unknown> | 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<string, unknown> | 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<string>(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);
|
||
}
|