feat: fuzzy name matcher
- feat: migrate biome to oxc
This commit is contained in:
@@ -1,5 +1,6 @@
|
||||
import { createCanvas, loadImage } from "@napi-rs/canvas";
|
||||
import { file } from "bun";
|
||||
|
||||
import overlayImage from "@/assets/overlay.jpg";
|
||||
|
||||
const elementColors: Record<string, string> = {
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { file } from "bun";
|
||||
|
||||
import sansFontFile from "./open-sans.woff2" with { type: "file" };
|
||||
import iconFontFile from "./shicon.woff" with { type: "file" };
|
||||
import fontFile from "./shinshin.woff2" with { type: "file" };
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { SVGProps } from "react";
|
||||
import type { SVGProps } from "react";
|
||||
|
||||
export const library = {
|
||||
CdReduction_primary: (props: SVGProps<SVGSVGElement>) => (
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { SVGProps } from "react";
|
||||
import type { SVGProps } from "react";
|
||||
|
||||
import { library } from "./icons-library";
|
||||
|
||||
export const libraryRaw = {
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { CSSProperties } from "react";
|
||||
import type { CSSProperties } from "react";
|
||||
|
||||
export function WeaponGlazeSvg({
|
||||
tw,
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import openapi from "@elysiajs/openapi";
|
||||
import { Elysia, t } from "elysia";
|
||||
|
||||
import { version } from "../package.json";
|
||||
import { pm } from "./lib/auth";
|
||||
import { createLogger } from "./lib/logger";
|
||||
|
||||
+9
-4
@@ -1,18 +1,23 @@
|
||||
import { readdirSync } from "node:fs";
|
||||
import { mkdir, readdir, rm } from "node:fs/promises";
|
||||
import { join } from "node:path";
|
||||
|
||||
import { file, hash, write } from "bun";
|
||||
import { ResolvedPlayerInfo, TTLCache } from "enka.ts";
|
||||
import {
|
||||
import type { ResolvedPlayerInfo} from "enka.ts";
|
||||
import { TTLCache } from "enka.ts";
|
||||
|
||||
import type {
|
||||
AvatarStore,
|
||||
ElementList,
|
||||
EnkaAffixes,
|
||||
EnkaLocs,
|
||||
EnkaNamecards,
|
||||
EnkaPfps,
|
||||
RelicStore,
|
||||
WeaponStore,
|
||||
WeaponStore} from "@/type/enka";
|
||||
import {
|
||||
ElementList
|
||||
} from "@/type/enka";
|
||||
|
||||
import { Deduplicator } from "./dedupe";
|
||||
import { createLogger } from "./logger";
|
||||
|
||||
|
||||
+2
-1
@@ -1,5 +1,6 @@
|
||||
import { feature } from "bun:bundle";
|
||||
import { InspectColor, styleText } from "node:util";
|
||||
import type { InspectColor} from "node:util";
|
||||
import { styleText } from "node:util";
|
||||
|
||||
import { inspect } from "bun";
|
||||
|
||||
|
||||
@@ -0,0 +1,64 @@
|
||||
import type { BaseAvatar } from "enka.ts";
|
||||
import { distance } from "fastest-levenshtein";
|
||||
|
||||
function shortAvatarId(id: number) {
|
||||
// 10000120 -> 120
|
||||
return String(id).replace(/^10*/, "");
|
||||
}
|
||||
|
||||
function norm(s: string) {
|
||||
return s
|
||||
.trim()
|
||||
.toLowerCase()
|
||||
.replace(/[\s_-]+/g, "");
|
||||
}
|
||||
|
||||
function similarity(a: string, b: string) {
|
||||
return 1 - distance(a, b) / Math.max(a.length, b.length);
|
||||
}
|
||||
|
||||
export function resolveShowcaseChar<T extends { avatar: BaseAvatar }>(
|
||||
input: string,
|
||||
avatars: T[],
|
||||
): T | undefined {
|
||||
const raw = input.trim();
|
||||
const normalized = norm(raw);
|
||||
|
||||
return (
|
||||
avatars.find(({ avatar }) => String(avatar.avatarId) === raw) ??
|
||||
avatars.find(({ avatar }) => shortAvatarId(avatar.avatarId) === raw) ??
|
||||
avatars.find(({ avatar }) =>
|
||||
avatarAliases(avatar.name).some((alias) => norm(alias) === normalized),
|
||||
) ??
|
||||
fuzzyNameMatch(normalized, avatars)
|
||||
);
|
||||
}
|
||||
|
||||
function fuzzyNameMatch<T extends { avatar: BaseAvatar }>(
|
||||
normalized: string,
|
||||
avatars: T[],
|
||||
): T | undefined {
|
||||
if (/^\d+$/.test(normalized)) return undefined;
|
||||
|
||||
const ranked = avatars
|
||||
.flatMap((entry) =>
|
||||
avatarAliases(entry.avatar.name).map((alias) => ({
|
||||
entry,
|
||||
alias,
|
||||
score: similarity(normalized, norm(alias)),
|
||||
})),
|
||||
)
|
||||
.sort((a, b) => b.score - a.score);
|
||||
|
||||
const best = ranked[0];
|
||||
const second = ranked[1];
|
||||
|
||||
if (!best || best.score < 0.72) return undefined;
|
||||
if (second && best.score - second.score < 0.08) return undefined;
|
||||
|
||||
return best.entry;
|
||||
}
|
||||
|
||||
function avatarAliases(name: string) {
|
||||
return [name, ...name.split(" ")];
|
||||
}
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
import { ResolvedRoot } from "enka.ts";
|
||||
import type { ResolvedRoot } from "enka.ts";
|
||||
|
||||
export const mockData = {
|
||||
playerInfo: {
|
||||
|
||||
+4
-2
@@ -1,5 +1,7 @@
|
||||
import { AvatarElement, ResolvedAvatarInfo } from "enka.ts";
|
||||
import { EnkaLocsLanguage } from "@/type/enka";
|
||||
import type { ResolvedAvatarInfo } from "enka.ts";
|
||||
import { AvatarElement } from "enka.ts";
|
||||
|
||||
import type { EnkaLocsLanguage } from "@/type/enka";
|
||||
import { FightPropMapEnum, PercentageFightProp } from "@/type/fightPropMap";
|
||||
|
||||
// Types
|
||||
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
import { Affix } from "@/type/enka";
|
||||
import type { Affix } from "@/type/enka";
|
||||
import { FightPropMapEnum, PercentageFightProp } from "@/type/fightPropMap";
|
||||
|
||||
export type RollSummary = {
|
||||
|
||||
+10
-10
@@ -1,4 +1,5 @@
|
||||
import { feature } from "bun:bundle";
|
||||
|
||||
import { ImageResponse } from "@takumi-rs/image-response";
|
||||
import { file, hash, inspect } from "bun";
|
||||
import { status, t } from "elysia";
|
||||
@@ -12,12 +13,13 @@ import {
|
||||
RatelimitError,
|
||||
TTLCache,
|
||||
} from "enka.ts";
|
||||
|
||||
import { generateBackground } from "@/assets/background";
|
||||
import { ConstsSvg } from "@/assets/consts";
|
||||
import { featherImage } from "@/assets/feather";
|
||||
import { font, iconFont, sansFont } from "@/assets/fonts";
|
||||
import { EnkaIcon } from "@/assets/icons";
|
||||
import { library } from "@/assets/icons-library";
|
||||
import type { library } from "@/assets/icons-library";
|
||||
import shadeSvg from "@/assets/shade.svg" with { type: "file" };
|
||||
import { star } from "@/assets/stars";
|
||||
import { TalentSvg } from "@/assets/talent";
|
||||
@@ -29,11 +31,14 @@ import { cardErrorLocs } from "@/lib/locs";
|
||||
import { createLogger } from "@/lib/logger";
|
||||
import { mockData } from "@/lib/mock";
|
||||
import { buildDisplayRows } from "@/lib/rows";
|
||||
import { buildRollSummary, RollSummary } from "@/lib/substat";
|
||||
import type { RollSummary } from "@/lib/substat";
|
||||
import { buildRollSummary } from "@/lib/substat";
|
||||
import { Locale, SubstatOrder } from "@/type/enka";
|
||||
import { FightPropMapEnum, PercentageFightProp } from "@/type/fightPropMap";
|
||||
|
||||
import { ErrorPage } from "./error";
|
||||
import { ListPage } from "./list";
|
||||
import { resolveShowcaseChar } from "@/lib/matcher";
|
||||
|
||||
const cache = new TTLCache();
|
||||
const deduper = new Deduplicator();
|
||||
@@ -255,12 +260,7 @@ export function card(app: App) {
|
||||
|
||||
if (typeof data.avatarInfoList === "undefined")
|
||||
return error(400, errorLoc.private);
|
||||
const char = data.avatarInfoList.find(
|
||||
(c) =>
|
||||
c.avatar.avatarId === Number(charId) ||
|
||||
`${c.avatar.avatarId}`.match(/^10*(\d+)/)?.[1] ===
|
||||
`${charId}`,
|
||||
);
|
||||
const char = resolveShowcaseChar(charId, data.avatarInfoList);
|
||||
if (!char) return error(400, errorLoc.notInShowcase);
|
||||
|
||||
const avatar = getAvatar(
|
||||
@@ -927,8 +927,8 @@ export function card(app: App) {
|
||||
examples: ["775956514"],
|
||||
description: "Genshin Impact UID",
|
||||
}),
|
||||
char: t.Number({
|
||||
examples: [10000120],
|
||||
char: t.String({
|
||||
examples: ["10000120", "Ororon"],
|
||||
description: "Character ID",
|
||||
}),
|
||||
}),
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import type { App } from "@";
|
||||
|
||||
import { card } from "./card";
|
||||
|
||||
export function v1routes(app: App) {
|
||||
|
||||
+5
-3
@@ -1,8 +1,10 @@
|
||||
import { ResolvedRoot } from "enka.ts";
|
||||
import type { ResolvedRoot } from "enka.ts";
|
||||
|
||||
import { assets, getAvatar } from "@/lib/assets";
|
||||
import { Logger } from "@/lib/logger";
|
||||
import type { Logger } from "@/lib/logger";
|
||||
import { CharacterNamecardMap } from "@/lib/namecards";
|
||||
import { AvatarStore, EnkaLocsLanguage, EnkaNamecards } from "@/type/enka";
|
||||
import type { AvatarStore, EnkaLocsLanguage, EnkaNamecards } from "@/type/enka";
|
||||
|
||||
import { CardResponse } from "./response";
|
||||
|
||||
export async function ListPage(
|
||||
|
||||
+4
-2
@@ -1,7 +1,9 @@
|
||||
import { feature } from "bun:bundle";
|
||||
|
||||
import ImageResponse from "@takumi-rs/image-response";
|
||||
import { MaybePromise } from "bun";
|
||||
import { ReactNode } from "react";
|
||||
import type { MaybePromise } from "bun";
|
||||
import type { ReactNode } from "react";
|
||||
|
||||
import { font, iconFont, sansFont } from "@/assets/fonts";
|
||||
import { createLogger } from "@/lib/logger";
|
||||
|
||||
|
||||
Reference in New Issue
Block a user