@kecan0406/aninames 0.1.0 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +2 -2
- package/lib/client/chunks.d.ts +1 -0
- package/lib/client/chunks.js +7 -4
- package/lib/client/format.d.ts +4 -1
- package/lib/client/format.js +17 -9
- package/lib/client/names.d.ts +9 -1
- package/lib/client/names.js +43 -6
- package/lib/client/source.js +2 -2
- package/lib/model.d.ts +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -56,7 +56,7 @@ Built sizes: `core.sqlite` 610 MB, `kowiki.sqlite` 25 MB, `unlicensed.sqlite` 83
|
|
|
56
56
|
| `<layer>/<table>/<hash>.json.gz` | about 32 KB of JSON rows sorted by key (16 KB for `entity`), named by the first 48 bits of the SHA-256 of the file |
|
|
57
57
|
| `NOTICE.md`, `index.html` | each layer's sources and terms |
|
|
58
58
|
|
|
59
|
-
The key tables are `flat` (folded names), `latin` (romaji keys), `sound` (sound keys across kana, Hangul and romaji), `part` (name tokens) and `initial` (Korean initials); their rows are `[key, entity number, name weight, flags, popularity, standing, bonus]`. `entity` rows, keyed by entity number (which follows the entity ids' order), carry the id, names, Danbooru tags, works and
|
|
59
|
+
The key tables are `flat` (folded names), `latin` (romaji keys), `sound` (sound keys across kana, Hangul and romaji), `part` (name tokens) and `initial` (Korean initials); their rows are `[key, entity number, name weight, flags, popularity, standing, bonus]`. `tag` holds the same rows for Danbooru tags, keyed by the tag folded like a name without its qualifier and weighted by the tag's posts. `entity` rows, keyed by entity number (which follows the entity ids' order), carry the id, names, Danbooru tags, works, family and the ids other databases give one entity in that layer (TMDB's `tmdb:tv/…` and `tmdb:movie/…` among them, read from Wikidata). Layers stay in separate files; the only values a file carries from other layers are derived numbers and flags (popularity, family standing, adult, which layers know the entity).
|
|
60
60
|
|
|
61
61
|
On the 10-04 build: 19,167 files, 149 MB gzip'd, a manifest of 236 KB gzip'd. `mise run compare` searches the probe sample (4,370 queries) both ways: the files find the entity first 87% of the time, SQLite 86% (FTS substring search is not in the files; typo search reads the rows around the query's position instead). A cold query needs a median of 2 rounds (p90 3), 31 files (p90 78) and 287 KB (p90 584 KB).
|
|
62
62
|
|
|
@@ -71,7 +71,7 @@ const [first] = await names.search('봇치', { limit: 10, signal })
|
|
|
71
71
|
const works = await names.lookup(first?.works ?? [])
|
|
72
72
|
```
|
|
73
73
|
|
|
74
|
-
`search` returns `Found` cards (`id`, `kind`, `label`, `names`, `danbooru` tags with the most posts first, `works`, `score`, `adult`); `search(query, { kind, limit, adult: false })` narrows it,
|
|
74
|
+
`search` returns `Found` cards (`id`, `kind`, `label`, `names` with their `lang` and `kind`, `danbooru` tags with the most posts first, `works`, `refs`, `score`, `adult`); `search(query, { kind, limit, adult: false })` narrows it, `lookup(ids)` returns the cards of given ids in one round, and `tagged(tag, { kind, adult })` the cards of the entities that carry a Danbooru tag, most posts first, whatever its case, punctuation or qualifier (`soul-eater`, `soul_eater` and `soul_eater_(series)` find the same works); `prefix: true` also finds the tags that start with it. `cachedSource` keeps the files it fetched forever (their names are hashes of their contents), revalidates the manifest with its ETag or Last-Modified once a day (`maxAge`), answers from the cache when offline, refetches the manifest when the site has moved on to a new build, and deletes files a new manifest no longer names. `fileSource(dir)` reads a local `dist/site`.
|
|
75
75
|
|
|
76
76
|
## Development
|
|
77
77
|
|
package/lib/client/chunks.d.ts
CHANGED
|
@@ -19,6 +19,7 @@ export declare class View {
|
|
|
19
19
|
constructor(manifest: Manifest, layers: Layer[], loaded: Loaded);
|
|
20
20
|
private rows;
|
|
21
21
|
keys(table: KeyTable, range: Range): KeyRow[];
|
|
22
|
+
tagged(key: string, prefix: boolean): KeyRow[];
|
|
22
23
|
private keysIn;
|
|
23
24
|
around(table: KeyTable, key: string, radius: number): KeyRow[];
|
|
24
25
|
entity(eid: number, mask?: number): EntityRow[];
|
package/lib/client/chunks.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { chunkPath, present } from "./format.js";
|
|
1
|
+
import { chunkPath, present, } from "./format.js";
|
|
2
2
|
import { compare } from "./order.js";
|
|
3
3
|
function last(count, below) {
|
|
4
4
|
let lo = 0;
|
|
@@ -37,10 +37,13 @@ export class View {
|
|
|
37
37
|
return rows;
|
|
38
38
|
}
|
|
39
39
|
keys(table, range) {
|
|
40
|
-
return this.layers.flatMap((layer) => this.keysIn(layer, table, range));
|
|
40
|
+
return this.layers.flatMap((layer) => this.keysIn(layer, table, this.manifest.layers.get(layer)?.keys[table], range));
|
|
41
41
|
}
|
|
42
|
-
|
|
43
|
-
const
|
|
42
|
+
tagged(key, prefix) {
|
|
43
|
+
const range = { low: key, high: prefix ? `${key}\u{10FFFF}` : key };
|
|
44
|
+
return this.layers.flatMap((layer) => this.keysIn(layer, 'tag', this.manifest.layers.get(layer)?.tag, range));
|
|
45
|
+
}
|
|
46
|
+
keysIn(layer, table, spans, range) {
|
|
44
47
|
if (!spans)
|
|
45
48
|
return [];
|
|
46
49
|
const { low, high, open = false, limit = Number.POSITIVE_INFINITY, spans: most = Number.POSITIVE_INFINITY } = range;
|
package/lib/client/format.d.ts
CHANGED
|
@@ -2,7 +2,8 @@ import { type Kind, type Layer, type NameKind } from '../model.ts';
|
|
|
2
2
|
export declare const FORMAT = 1;
|
|
3
3
|
export declare const KEY_TABLES: readonly ["flat", "latin", "sound", "part", "initial"];
|
|
4
4
|
export type KeyTable = (typeof KEY_TABLES)[number];
|
|
5
|
-
export
|
|
5
|
+
export declare const TABLES: readonly ["flat", "latin", "sound", "part", "initial", "tag", "entity"];
|
|
6
|
+
export type Table = (typeof TABLES)[number];
|
|
6
7
|
export declare const NAME_KINDS: readonly NameKind[];
|
|
7
8
|
export declare const TRUSTED: readonly string[];
|
|
8
9
|
export declare const CHARACTER = 1;
|
|
@@ -33,6 +34,7 @@ export interface EntitySpans extends Spans {
|
|
|
33
34
|
}
|
|
34
35
|
export interface LayerSpans {
|
|
35
36
|
keys: Record<KeyTable, Spans>;
|
|
37
|
+
tag: Spans | undefined;
|
|
36
38
|
entity: EntitySpans;
|
|
37
39
|
}
|
|
38
40
|
export interface Manifest {
|
|
@@ -70,5 +72,6 @@ export interface EntityRow {
|
|
|
70
72
|
standing: number;
|
|
71
73
|
extra: number;
|
|
72
74
|
adult: boolean;
|
|
75
|
+
refs: string[];
|
|
73
76
|
}
|
|
74
77
|
export declare function readRows(table: Table, value: unknown): KeyRow[] | EntityRow[] | undefined;
|
package/lib/client/format.js
CHANGED
|
@@ -3,6 +3,7 @@ import { flat } from "../text.js";
|
|
|
3
3
|
import { compare } from "./order.js";
|
|
4
4
|
export const FORMAT = 1;
|
|
5
5
|
export const KEY_TABLES = ['flat', 'latin', 'sound', 'part', 'initial'];
|
|
6
|
+
export const TABLES = [...KEY_TABLES, 'tag', 'entity'];
|
|
6
7
|
export const NAME_KINDS = [
|
|
7
8
|
'main',
|
|
8
9
|
'official',
|
|
@@ -25,7 +26,7 @@ export const HASH_LENGTH = 12;
|
|
|
25
26
|
export function chunkPath(layer, table, hash) {
|
|
26
27
|
return `${layer}/${table}/${hash}.json.gz`;
|
|
27
28
|
}
|
|
28
|
-
const PATH = new RegExp(`^(${LAYERS.join('|')})/(${
|
|
29
|
+
const PATH = new RegExp(`^(${LAYERS.join('|')})/(${TABLES.join('|')})/([0-9a-f]{12})\\.json\\.gz$`);
|
|
29
30
|
export function parsePath(path) {
|
|
30
31
|
const match = PATH.exec(path);
|
|
31
32
|
return match ? { layer: match[1], table: match[2], hash: match[3] } : undefined;
|
|
@@ -95,6 +96,10 @@ function unpack(value, where, numbered) {
|
|
|
95
96
|
}
|
|
96
97
|
return out;
|
|
97
98
|
}
|
|
99
|
+
function spansOf(value, where) {
|
|
100
|
+
const { bounds, files } = unpack(value, where, false);
|
|
101
|
+
return { bounds, files };
|
|
102
|
+
}
|
|
98
103
|
export function readManifest(text) {
|
|
99
104
|
let value;
|
|
100
105
|
try {
|
|
@@ -119,11 +124,10 @@ export function readManifest(text) {
|
|
|
119
124
|
if (!isObject(tables))
|
|
120
125
|
throw bad(`layer ${layer} is malformed`);
|
|
121
126
|
const keys = {};
|
|
122
|
-
for (const table of KEY_TABLES)
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
}
|
|
126
|
-
layers.set(layer, { keys, entity: unpack(tables.entity, `${layer}/entity`, true) });
|
|
127
|
+
for (const table of KEY_TABLES)
|
|
128
|
+
keys[table] = spansOf(tables[table], `${layer}/${table}`);
|
|
129
|
+
const tag = tables.tag === undefined ? undefined : spansOf(tables.tag, `${layer}/tag`);
|
|
130
|
+
layers.set(layer, { keys, tag, entity: unpack(tables.entity, `${layer}/entity`, true) });
|
|
127
131
|
}
|
|
128
132
|
if (!layers.has('core'))
|
|
129
133
|
throw bad('it has no core layer');
|
|
@@ -135,6 +139,8 @@ export function manifestPaths(manifest) {
|
|
|
135
139
|
for (const table of KEY_TABLES)
|
|
136
140
|
for (const hash of spans.keys[table].files)
|
|
137
141
|
paths.add(chunkPath(layer, table, hash));
|
|
142
|
+
for (const hash of spans.tag?.files ?? [])
|
|
143
|
+
paths.add(chunkPath(layer, 'tag', hash));
|
|
138
144
|
for (const hash of spans.entity.files)
|
|
139
145
|
paths.add(chunkPath(layer, 'entity', hash));
|
|
140
146
|
}
|
|
@@ -181,10 +187,11 @@ const tagOf = (value) => Array.isArray(value) && typeof value[0] === 'string' &&
|
|
|
181
187
|
: undefined;
|
|
182
188
|
const workOf = (value) => Array.isArray(value) && isCount(value[0]) && typeof value[1] === 'string' ? [value[0], value[1]] : undefined;
|
|
183
189
|
const eidOf = (value) => (isCount(value) ? value : undefined);
|
|
190
|
+
const refOf = (value) => typeof value === 'string' && value.indexOf(':') > 0 ? value : undefined;
|
|
184
191
|
function entityRow(value) {
|
|
185
192
|
if (!Array.isArray(value) || value.length < 10)
|
|
186
193
|
return undefined;
|
|
187
|
-
const [eid, id, rawNames, rawTags, rawWorks, rawReach, rank, standing, extra, adult] = value;
|
|
194
|
+
const [eid, id, rawNames, rawTags, rawWorks, rawReach, rank, standing, extra, adult, rawRefs = []] = value;
|
|
188
195
|
if (!isCount(eid) || typeof id !== 'string')
|
|
189
196
|
return undefined;
|
|
190
197
|
const kind = id.startsWith('w/') ? 'work' : id.startsWith('c/') ? 'character' : undefined;
|
|
@@ -194,9 +201,10 @@ function entityRow(value) {
|
|
|
194
201
|
const danbooru = list(rawTags, tagOf);
|
|
195
202
|
const works = list(rawWorks, workOf);
|
|
196
203
|
const reach = list(rawReach, eidOf);
|
|
197
|
-
|
|
204
|
+
const refs = list(rawRefs, refOf);
|
|
205
|
+
if (!names || !danbooru || !works || !reach || !refs)
|
|
198
206
|
return undefined;
|
|
199
|
-
return { eid, id, kind, names, danbooru, works, reach, rank, standing, extra, adult: adult === 1 };
|
|
207
|
+
return { eid, id, kind, names, danbooru, works, reach, rank, standing, extra, adult: adult === 1, refs };
|
|
200
208
|
}
|
|
201
209
|
export function readRows(table, value) {
|
|
202
210
|
if (!Array.isArray(value))
|
package/lib/client/names.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { type Kind, type Layer } from '../model.ts';
|
|
1
|
+
import { type Kind, type Layer, type NameKind } from '../model.ts';
|
|
2
2
|
import { type Rows } from './chunks.ts';
|
|
3
3
|
import { type Source } from './source.ts';
|
|
4
4
|
export interface Found {
|
|
@@ -8,9 +8,11 @@ export interface Found {
|
|
|
8
8
|
names: {
|
|
9
9
|
text: string;
|
|
10
10
|
lang: string | null;
|
|
11
|
+
kind: NameKind;
|
|
11
12
|
}[];
|
|
12
13
|
danbooru: string[];
|
|
13
14
|
works: string[];
|
|
15
|
+
refs: string[];
|
|
14
16
|
score: number;
|
|
15
17
|
adult: boolean;
|
|
16
18
|
}
|
|
@@ -34,4 +36,10 @@ export declare class Names {
|
|
|
34
36
|
signal?: AbortSignal;
|
|
35
37
|
}): Promise<Found[]>;
|
|
36
38
|
lookup(ids: string[], signal?: AbortSignal): Promise<Found[]>;
|
|
39
|
+
tagged(tag: string, options?: {
|
|
40
|
+
kind?: Kind;
|
|
41
|
+
adult?: boolean;
|
|
42
|
+
prefix?: boolean;
|
|
43
|
+
signal?: AbortSignal;
|
|
44
|
+
}): Promise<Found[]>;
|
|
37
45
|
}
|
package/lib/client/names.js
CHANGED
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
import { gunzipSync } from 'node:zlib';
|
|
2
2
|
import { LAYERS } from "../model.js";
|
|
3
|
+
import { flat, qualifiers } from "../text.js";
|
|
3
4
|
import { View } from "./chunks.js";
|
|
4
5
|
import { search } from "./engine.js";
|
|
5
|
-
import { parsePath, readManifest, readRows } from "./format.js";
|
|
6
|
+
import { ADULT, CHARACTER, MAIN, NAME_KINDS, parsePath, readManifest, readRows, } from "./format.js";
|
|
6
7
|
import { Missing } from "./source.js";
|
|
7
8
|
const KEEP = 256;
|
|
8
9
|
const PARALLEL = 16;
|
|
@@ -50,14 +51,21 @@ function label(id, rows) {
|
|
|
50
51
|
}
|
|
51
52
|
function card(rows, score, adult) {
|
|
52
53
|
const head = rows[0];
|
|
53
|
-
const seen = new
|
|
54
|
+
const seen = new Map();
|
|
54
55
|
const names = [];
|
|
55
56
|
for (const name of rows.flatMap((row) => row.names)) {
|
|
57
|
+
if (!SHOWN.has(name.kind))
|
|
58
|
+
continue;
|
|
56
59
|
const key = `${name.lang ?? ''}\u0000${name.text}`;
|
|
57
|
-
|
|
60
|
+
const known = seen.get(key);
|
|
61
|
+
if (known) {
|
|
62
|
+
if (NAME_KINDS.indexOf(name.kind) < NAME_KINDS.indexOf(known.kind))
|
|
63
|
+
known.kind = name.kind;
|
|
58
64
|
continue;
|
|
59
|
-
|
|
60
|
-
|
|
65
|
+
}
|
|
66
|
+
const shown = { text: name.text, lang: name.lang, kind: name.kind };
|
|
67
|
+
seen.set(key, shown);
|
|
68
|
+
names.push(shown);
|
|
61
69
|
}
|
|
62
70
|
const posts = new Map();
|
|
63
71
|
for (const [tag, count] of rows.flatMap((row) => row.danbooru)) {
|
|
@@ -69,7 +77,8 @@ function card(rows, score, adult) {
|
|
|
69
77
|
.sort((a, b) => (b.count ?? -1) - (a.count ?? -1) || a.at - b.at)
|
|
70
78
|
.map((entry) => entry.tag);
|
|
71
79
|
const works = head.kind === 'character' ? [...new Set(rows.flatMap((row) => row.works.map(([, id]) => id)))] : [];
|
|
72
|
-
|
|
80
|
+
const refs = [...new Set(rows.flatMap((row) => row.refs))].sort();
|
|
81
|
+
return { id: head.id, kind: head.kind, label: label(head.id, rows), names, danbooru, works, refs, score, adult };
|
|
73
82
|
}
|
|
74
83
|
export class Names {
|
|
75
84
|
#source;
|
|
@@ -110,6 +119,34 @@ export class Names {
|
|
|
110
119
|
return view.wanted.size > 0 ? undefined : out;
|
|
111
120
|
});
|
|
112
121
|
}
|
|
122
|
+
tagged(tag, options = {}) {
|
|
123
|
+
const { kind, adult = true, prefix = false, signal } = options;
|
|
124
|
+
const key = flat(qualifiers(tag).base);
|
|
125
|
+
return this.#run(signal, (view) => {
|
|
126
|
+
if (!key)
|
|
127
|
+
return [];
|
|
128
|
+
const best = new Map();
|
|
129
|
+
for (const [, eid, posts, flags, , standing] of view.tagged(key, prefix)) {
|
|
130
|
+
if (kind && (flags & CHARACTER ? 'character' : 'work') !== kind)
|
|
131
|
+
continue;
|
|
132
|
+
if (!adult && flags & ADULT)
|
|
133
|
+
continue;
|
|
134
|
+
const known = best.get(eid);
|
|
135
|
+
const mask = flags & ~(CHARACTER | MAIN | ADULT);
|
|
136
|
+
if (!known || posts > known.posts)
|
|
137
|
+
best.set(eid, { posts, mask: mask | (known?.mask ?? 0), flags, standing });
|
|
138
|
+
else
|
|
139
|
+
known.mask |= mask;
|
|
140
|
+
}
|
|
141
|
+
const order = [...best].sort(([a, x], [b, y]) => y.posts - x.posts || y.standing - x.standing || a - b);
|
|
142
|
+
view.labels = true;
|
|
143
|
+
const out = order.flatMap(([eid, hit]) => {
|
|
144
|
+
const rows = view.entity(eid, hit.mask);
|
|
145
|
+
return rows.length > 0 ? [card(rows, 0, (hit.flags & ADULT) !== 0)] : [];
|
|
146
|
+
});
|
|
147
|
+
return view.wanted.size > 0 ? undefined : out;
|
|
148
|
+
});
|
|
149
|
+
}
|
|
113
150
|
#open() {
|
|
114
151
|
if (this.#state)
|
|
115
152
|
return this.#state;
|
package/lib/client/source.js
CHANGED
|
@@ -2,7 +2,7 @@ import { createHash, randomUUID } from 'node:crypto';
|
|
|
2
2
|
import { mkdir, readdir, readFile, rename, rm, writeFile } from 'node:fs/promises';
|
|
3
3
|
import { dirname, join } from 'node:path';
|
|
4
4
|
import { USER_AGENT } from "../model.js";
|
|
5
|
-
import {
|
|
5
|
+
import { manifestPaths, parsePath, readManifest, TABLES } from "./format.js";
|
|
6
6
|
export class Missing extends Error {
|
|
7
7
|
}
|
|
8
8
|
const revalidators = new WeakMap();
|
|
@@ -96,7 +96,7 @@ async function put(path, data) {
|
|
|
96
96
|
async function prune(dir, text) {
|
|
97
97
|
const named = manifestPaths(readManifest(text));
|
|
98
98
|
for (const layer of await readdir(dir).catch(() => [])) {
|
|
99
|
-
for (const table of
|
|
99
|
+
for (const table of TABLES) {
|
|
100
100
|
const folder = join(dir, layer, table);
|
|
101
101
|
for (const file of await readdir(folder).catch(() => [])) {
|
|
102
102
|
const path = `${layer}/${table}/${file}`;
|
package/lib/model.d.ts
CHANGED
|
@@ -3,7 +3,7 @@ export type Layer = 'core' | 'kowiki' | 'unlicensed';
|
|
|
3
3
|
export declare const LAYERS: readonly Layer[];
|
|
4
4
|
export declare const USER_AGENT = "aninames/0.1 (+https://github.com/kecan0406/aninames)";
|
|
5
5
|
export type NameKind = 'main' | 'official' | 'synonym' | 'short' | 'reading' | 'redirect' | 'section' | 'generated';
|
|
6
|
-
export type Namespace = 'wikidata' | 'mal' | 'anilist' | 'anidb' | 'kitsu' | 'ann' | 'animeplanet' | 'anisearch' | 'livechart' | 'simkl' | 'animecountdown' | 'vndb' | 'bangumi' | 'danbooru' | 'kowiki' | 'anissia';
|
|
6
|
+
export type Namespace = 'wikidata' | 'mal' | 'anilist' | 'anidb' | 'kitsu' | 'ann' | 'animeplanet' | 'anisearch' | 'livechart' | 'simkl' | 'animecountdown' | 'tmdb' | 'vndb' | 'bangumi' | 'danbooru' | 'kowiki' | 'anissia';
|
|
7
7
|
export type Ref = `${Namespace}:${string}`;
|
|
8
8
|
export interface Name {
|
|
9
9
|
text: string;
|