fix: improve fuzzy handling

This commit is contained in:
2026-05-30 19:32:01 +07:00 Unverified
parent 4bf2466b3d
commit 39f1eb6988
13 changed files with 280 additions and 264 deletions
+25 -24
View File
@@ -1,7 +1,6 @@
<!DOCTYPE html> <!doctype html>
<html> <html>
<head>
<head>
<meta charset="utf-8" /> <meta charset="utf-8" />
<style> <style>
body { body {
@@ -35,10 +34,9 @@
background: #000; background: #000;
} }
</style> </style>
</head> </head>
<body>
<body>
<div class="bar"> <div class="bar">
<button id="refresh">refresh</button> <button id="refresh">refresh</button>
<label> <label>
@@ -51,11 +49,12 @@
</label> </label>
<label> <label>
threshold threshold
<input id="threshold" type="range" min="0" max="120" value="85"> <input id="threshold" type="range" min="0" max="120" value="85" />
<span id="thresholdText">85</span> <span id="thresholdText">85</span>
</label> </label>
<label> <label>
time: <span id="timingText">Loading...</span><span style="color: gray;">(image load complete in browser)</span> time: <span id="timingText">Loading...</span
><span style="color: gray">(image load complete in browser)</span>
</label> </label>
</div> </div>
@@ -63,10 +62,10 @@
<script> <script>
const urlParams = new URLSearchParams(window.location.search); const urlParams = new URLSearchParams(window.location.search);
const type = urlParams.get('type'); const type = urlParams.get("type");
let MINE, ORIG, W, H; let MINE, ORIG, W, H;
if (type === 'list') { if (type === "list") {
MINE = "/v1/card/genshin/775956514"; MINE = "/v1/card/genshin/775956514";
ORIG = "/v1/card/genshin/diff/listoriginal.webp"; ORIG = "/v1/card/genshin/diff/listoriginal.webp";
W = 1200; W = 1200;
@@ -91,7 +90,9 @@
const thresholdText = document.getElementById("thresholdText"); const thresholdText = document.getElementById("thresholdText");
const timingText = document.getElementById("timingText"); const timingText = document.getElementById("timingText");
let mineData, origData, refreshing = false; let mineData,
origData,
refreshing = false;
function loadImage(src) { function loadImage(src) {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
@@ -99,7 +100,8 @@
img.crossOrigin = "anonymous"; img.crossOrigin = "anonymous";
img.onload = () => resolve(img); img.onload = () => resolve(img);
img.onerror = reject; img.onerror = reject;
img.src = src + (src.includes("?") ? "&" : "?") + "cacheBust=" + Date.now(); img.src =
src + (src.includes("?") ? "&" : "?") + "cacheBust=" + Date.now();
}); });
} }
@@ -127,7 +129,9 @@
const start = Date.now(); const start = Date.now();
const [mineImg, origImg] = await Promise.all([ const [mineImg, origImg] = await Promise.all([
loadImage(MINE).finally(() => timingText.innerText = `${Date.now() - start}ms`), loadImage(MINE).finally(
() => (timingText.innerText = `${Date.now() - start}ms`),
),
loadImage(ORIG), loadImage(ORIG),
]); ]);
@@ -200,9 +204,7 @@
} else { } else {
o[i + 3] = 0; o[i + 3] = 0;
} }
} } else if (mode.value === "diff") {
else if (mode.value === "diff") {
const dr = Math.abs(m[i] - r[i]); const dr = Math.abs(m[i] - r[i]);
const dg = Math.abs(m[i + 1] - r[i + 1]); const dg = Math.abs(m[i + 1] - r[i + 1]);
const db = Math.abs(m[i + 2] - r[i + 2]); const db = Math.abs(m[i + 2] - r[i + 2]);
@@ -212,9 +214,7 @@
o[i + 1] = diff; o[i + 1] = diff;
o[i + 2] = diff; o[i + 2] = diff;
o[i + 3] = diff > t ? 255 : 0; o[i + 3] = diff > t ? 255 : 0;
} } else {
else {
o[i] = m[i]; // mine red-ish o[i] = m[i]; // mine red-ish
o[i + 1] = r[i + 1]; // original green-ish o[i + 1] = r[i + 1]; // original green-ish
o[i + 2] = 0; o[i + 2] = 0;
@@ -230,14 +230,17 @@
mode.oninput = render; mode.oninput = render;
threshold.oninput = render; threshold.oninput = render;
refresh().catch(err => { refresh().catch((err) => {
console.error(err); console.error(err);
ctx.fillStyle = "red"; ctx.fillStyle = "red";
ctx.fillText(String(err), 20, 30); ctx.fillText(String(err), 20, 30);
}); });
window.addEventListener("keydown", (e) => { window.addEventListener("keydown", (e) => {
if (e.key === "Enter" && !["INPUT", "SELECT"].includes(e.target.tagName)) { if (
e.key === "Enter" &&
!["INPUT", "SELECT"].includes(e.target.tagName)
) {
e.preventDefault(); e.preventDefault();
refresh(); refresh();
} }
@@ -247,7 +250,5 @@
// refresh(); // refresh();
// }); // });
</script> </script>
</body>
</body>
</html> </html>
+6 -3
View File
@@ -55,7 +55,8 @@ export function endfieldCard(app: App) {
params: t.Object({ uid: t.String() }), params: t.Object({ uid: t.String() }),
detail: { detail: {
tags: ["Endfield"], tags: ["Endfield"],
description: "Preload data required to render an Endfield template card.", description:
"Preload data required to render an Endfield template card.",
}, },
afterResponse: ({ log }) => log`Data preload complete`, afterResponse: ({ log }) => log`Data preload complete`,
}, },
@@ -121,7 +122,8 @@ export function endfieldCard(app: App) {
{ http: "http", card: "card" }, { http: "http", card: "card" },
{ {
default: "http", default: "http",
description: "Type of error to return when card fails to render.", description:
"Type of error to return when card fails to render.",
}, },
), ),
debug: t.Boolean({ debug: t.Boolean({
@@ -134,7 +136,8 @@ export function endfieldCard(app: App) {
), ),
detail: { detail: {
tags: ["Endfield"], tags: ["Endfield"],
description: "Generate a placeholder Endfield profile card image from a UID.", description:
"Generate a placeholder Endfield profile card image from a UID.",
}, },
afterResponse: ({ log }) => log`Rendering completed`, afterResponse: ({ log }) => log`Rendering completed`,
}, },
+8 -3
View File
@@ -61,7 +61,9 @@ export function EndfieldTemplate({
<div tw="text-[1.55em] leading-[1.15] font-semibold"> <div tw="text-[1.55em] leading-[1.15] font-semibold">
Route + Card Shell Route + Card Shell
</div> </div>
<div tw="text-white/60 mt-[.35em]">Ready for game-specific layout work</div> <div tw="text-white/60 mt-[.35em]">
Ready for game-specific layout work
</div>
</div> </div>
<div tw="rounded-[1em] border border-white/10 bg-black/20 p-[1.2em]"> <div tw="rounded-[1em] border border-white/10 bg-black/20 p-[1.2em]">
@@ -71,7 +73,9 @@ export function EndfieldTemplate({
<div tw="text-[1.55em] leading-[1.15] font-semibold"> <div tw="text-[1.55em] leading-[1.15] font-semibold">
Character Grid Character Grid
</div> </div>
<div tw="text-white/60 mt-[.35em]">Replace placeholder sections with Endfield data</div> <div tw="text-white/60 mt-[.35em]">
Replace placeholder sections with Endfield data
</div>
</div> </div>
</div> </div>
@@ -80,7 +84,8 @@ export function EndfieldTemplate({
Signature Signature
</div> </div>
<div tw="text-[1.05em] leading-[1.45] text-white/82 whitespace-pre-wrap"> <div tw="text-[1.05em] leading-[1.45] text-white/82 whitespace-pre-wrap">
{signature?.trim() || "No signature available in the fetched payload yet."} {signature?.trim() ||
"No signature available in the fetched payload yet."}
</div> </div>
</div> </div>
</div> </div>
+5 -6
View File
@@ -3,7 +3,7 @@ import { mkdir, readdir, rm } from "node:fs/promises";
import { join } from "node:path"; import { join } from "node:path";
import { file, hash, write } from "bun"; import { file, hash, write } from "bun";
import type { ResolvedPlayerInfo} from "enka.ts"; import type { ResolvedPlayerInfo } from "enka.ts";
import { TTLCache } from "enka.ts"; import { TTLCache } from "enka.ts";
import type { import type {
@@ -13,11 +13,9 @@ import type {
EnkaNamecards, EnkaNamecards,
EnkaPfps, EnkaPfps,
RelicStore, RelicStore,
WeaponStore} from "@/games/genshin/types/enka"; WeaponStore,
import {
ElementList
} from "@/games/genshin/types/enka"; } from "@/games/genshin/types/enka";
import { ElementList } from "@/games/genshin/types/enka";
import { Deduplicator } from "@/lib/dedupe"; import { Deduplicator } from "@/lib/dedupe";
import { createLogger } from "@/lib/logger"; import { createLogger } from "@/lib/logger";
@@ -210,10 +208,11 @@ export async function getEnkaStore<K extends keyof StoreTypeMap>(
return data; return data;
} }
// DEPRECATED: No longer needed
export function getAvatar( export function getAvatar(
store: AvatarStore, store: AvatarStore,
playerInfo: ResolvedPlayerInfo, playerInfo: ResolvedPlayerInfo,
id: number, id: string | number,
) { ) {
return ( return (
Object.entries(store).find( Object.entries(store).find(
+6 -3
View File
@@ -1,9 +1,9 @@
import type { BaseAvatar } from "enka.ts"; import type { BaseAvatar } from "enka.ts";
import { distance } from "fastest-levenshtein"; import { distance } from "fastest-levenshtein";
function shortAvatarId(id: number) { function shortAvatarId(id: string | number) {
// 10000120 -> 120 // 10000120 -> 120
return String(id).replace(/^10*/, ""); return String(id).replace(/^10*/, "").replace(/^-\d+/, "");
} }
function norm(s: string) { function norm(s: string) {
@@ -60,5 +60,8 @@ function fuzzyNameMatch<T extends { avatar: BaseAvatar }>(
} }
function avatarAliases(name: string) { function avatarAliases(name: string) {
return [name, ...name.split(" ")]; const aliases: Record<string, string[]> = {
Traveler: ["Aether", "Lumine"],
};
return [name, ...name.split(" "), ...(aliases[name] ?? [])];
} }
+4 -1
View File
@@ -2,7 +2,10 @@ import type { ResolvedAvatarInfo } from "enka.ts";
import { AvatarElement } from "enka.ts"; import { AvatarElement } from "enka.ts";
import type { EnkaLocsLanguage } from "@/games/genshin/types/enka"; import type { EnkaLocsLanguage } from "@/games/genshin/types/enka";
import { FightPropMapEnum, PercentageFightProp } from "@/games/genshin/types/fightPropMap"; import {
FightPropMapEnum,
PercentageFightProp,
} from "@/games/genshin/types/fightPropMap";
// Types // Types
interface DisplayRow { interface DisplayRow {
+4 -1
View File
@@ -1,5 +1,8 @@
import type { Affix } from "@/games/genshin/types/enka"; import type { Affix } from "@/games/genshin/types/enka";
import { FightPropMapEnum, PercentageFightProp } from "@/games/genshin/types/fightPropMap"; import {
FightPropMapEnum,
PercentageFightProp,
} from "@/games/genshin/types/fightPropMap";
export type RollSummary = { export type RollSummary = {
id: string; id: string;
-1
View File
@@ -19,7 +19,6 @@ import {
} from "@/games/genshin/types/enka"; } from "@/games/genshin/types/enka";
import type { Logger } from "@/lib/logger"; import type { Logger } from "@/lib/logger";
import { arrRandom } from "@/lib/util"; import { arrRandom } from "@/lib/util";
import { CardResponse } from "@/v1/response"; import { CardResponse } from "@/v1/response";
export async function ListPage( export async function ListPage(
+1 -1
View File
@@ -1,6 +1,6 @@
import { t } from "elysia"; import { t } from "elysia";
export type AvatarStore = Record<number, Avatar>; export type AvatarStore = Record<string | number, Avatar>;
export interface Avatar { export interface Avatar {
Element: ElementType; Element: ElementType;
+1 -1
View File
@@ -1,3 +1,4 @@
import { cors } from "@elysia/cors";
import openapi from "@elysiajs/openapi"; import openapi from "@elysiajs/openapi";
import { Elysia, t } from "elysia"; import { Elysia, t } from "elysia";
@@ -5,7 +6,6 @@ import { version } from "../package.json";
import { pm } from "./lib/auth"; import { pm } from "./lib/auth";
import { createLogger } from "./lib/logger"; import { createLogger } from "./lib/logger";
import { v1routes } from "./v1"; import { v1routes } from "./v1";
import { cors } from '@elysia/cors'
const log = createLogger("APP"); const log = createLogger("APP");
+1 -1
View File
@@ -1,5 +1,5 @@
import { feature } from "bun:bundle"; import { feature } from "bun:bundle";
import type { InspectColor} from "node:util"; import type { InspectColor } from "node:util";
import { styleText } from "node:util"; import { styleText } from "node:util";
import { inspect } from "bun"; import { inspect } from "bun";
+1 -1
View File
@@ -1,6 +1,6 @@
import { join } from "node:path"; import { join } from "node:path";
import type { MaybePromise} from "bun"; import type { MaybePromise } from "bun";
import { $, write } from "bun"; import { $, write } from "bun";
import { createLogger } from "@/lib/logger"; import { createLogger } from "@/lib/logger";