ai-chatbot/lib/video-canvas/video-effects.ts

165 lines
5.0 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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);
}