Simplify comments

This commit is contained in:
Kalle
2026-09-02 21:02:01 +03:00
parent fe797e1891
commit 6a307ce59e
770 changed files with 4302 additions and 11417 deletions

View File

@@ -1,15 +1,14 @@
/** biome-ignore-all lint/suspicious/noConsole: CLI script output */
/**
* Bootstrap glyph atlases from the labeled reference fixtures: slices glyph
* templates straight out of the reference frames, since the same scoreboard
* captured through different pipelines (OBS virtual camera 720p, game
* capture 1080p) yields subtly different pixels — every source contributes
* its own crop per character and recognition takes the best-scoring one.
* build-glyph-atlas.ts then fills in the rest of the charset from the
* fonts, preserving these fixture-tagged glyphs.
* Bootstraps glyph atlases from labeled reference fixtures: slices templates
* straight out of the frames, since the same scoreboard captured through
* different pipelines (OBS virtual camera 720p, game capture 1080p) yields
* subtly different pixels — every source contributes its own crop per char and
* recognition takes the best-scoring one. build-glyph-atlas.ts then fills the
* rest of the charset from the fonts, preserving these fixture-tagged glyphs.
*
* Usage: pnpm scanner:bootstrap-atlas
* Writes SCANNER_ASSETS_DIR/glyphs/scoreboard-{names,paint-digits,stat-digits}.{png,json}
* Writes SCANNER_ASSETS_DIR/glyphs/*.{png,json}
*/
import { mkdirSync, writeFileSync } from "node:fs";
import { join } from "node:path";
@@ -61,9 +60,8 @@ interface Source {
stats: string[][];
teamScores: string[];
/**
* Split hints are absolute x positions where merged segments must be cut
* (keyed by row index) — e.g. the "Te" in Teddy renders as one segment,
* with the exact boundary shifting a couple of pixels between sources.
* absolute x positions where merged segments must be cut, keyed by row —
* e.g. "Te" in Teddy renders as one segment, boundary shifting a few px per source
*/
nameSplitHints: Record<number, number[]>;
/** omit to skip header harvest for this source */
@@ -94,10 +92,9 @@ const REFERENCE = {
["05", "02", "04"],
],
/**
* Team totals render with an outline on the colored team box, so they get
* their own atlas rather than reusing paint digits. This fixture only
* contributes '5' and '0' — later fixtures extend the set (harvest keeps
* the first instance per char, so rerunning with more fixtures is additive).
* Team totals render outlined on the colored team box, so they get their own
* atlas. This fixture only contributes '5' and '0'; harvest keeps the first
* instance per char, so rerunning with more fixtures is additive.
*/
teamScores: ["500", "0"],
header: {
@@ -154,11 +151,10 @@ const SOURCES: Source[] = [
nameSplitHints: {},
},
{
// This capture renders names slightly smaller than the reference: its
// 'T' (9x15) loses to the taller reference crops, and its baseline dots
// are pure homoglyphs the font templates can't split — '.' renders 4px,
// '・' 5px, so exact crops separate them via the ink-coverage penalty.
// Only the dot/T rows are labeled; the rest add nothing new.
// Renders names slightly smaller than the reference: its 'T' (9x15) loses
// to the taller reference crops, and its baseline dots are homoglyphs the
// font templates can't split ('.' 4px, '・' 5px; exact crops separate them
// via the ink-coverage penalty). Only the dot/T rows are labeled.
frame: "scoreboard/robot/frame.png",
names: ["R.O.B.O.T", "", "", "", "", "Rαι×ι..・", "", ""],
paints: ["", "", "", "", "", "", "", ""],
@@ -239,10 +235,9 @@ async function loadGreen(framePath: string): Promise<Mat> {
}
/**
* Death splash-tag name band, prepared the way the detector reads it
* (see src/core/detectors/death/index.ts): crop the tilted tag, rotate it
* level, crop the name band, then map each pixel to its max-channel
* distance from the median banner color, normalized to 0-255.
* Death splash-tag name band, prepared the way death/index.ts reads it: crop
* the tilted tag, rotate it level, crop the name band, then map each pixel to
* its max-channel distance from the median banner color, normalized to 0-255.
*/
async function loadTagBand(framePath: string): Promise<Mat> {
const srcMat = toMat(await readImage(join(FIXTURES_DIR, framePath)));
@@ -321,11 +316,9 @@ async function loadTagBand(framePath: string): Promise<Mat> {
}
/**
* Zero out ink components touching the band border, exactly as the
* detector does before parsing (see clearBorderBlobs in death/index.ts):
* banner art and title-line slivers enter at the band edges, and without
* this they join the column runs (mismatching the label) or push a crop's
* tight box past the band edge.
* Zeroes ink components touching the band border, as the detector does before
* parsing (clearBorderBlobs in death/index.ts): banner art and title-line
* slivers at the edges would join the column runs or push a tight box past it.
*/
function clearBorderBlobs(band: Mat, threshold: number): void {
const bin = new cv.Mat();
@@ -384,9 +377,8 @@ function columnRuns(
}
}
if (start >= 0) runs.push({ x0: start, x1: roi.x + roi.w });
// Merge hints bridge runs that belong to one glyph (multi-stroke kana
// like パ segments as two strokes): a hint x inside the gap between two
// consecutive runs joins them into a single crop.
// Merge hints bridge runs belonging to one glyph (multi-stroke kana like パ
// segment as two strokes): a hint x inside the gap joins the two runs.
for (const hint of mergeHints) {
const i = runs.findIndex(
(r, idx) =>
@@ -460,10 +452,9 @@ function cropGlyph(
}
/**
* Slice a labeled text ROI into per-char crops. A Map collector keeps the
* Slices a labeled text ROI into per-char crops. A Map collector keeps the
* first instance per char (digits render identically everywhere); an array
* collector keeps every instance — letters land on different subpixel
* phases, so each occurrence is a distinct, equally-authoritative exemplar.
* collector keeps every instance (letters land on different subpixel phases).
*/
function harvest(
roi: Roi,
@@ -621,8 +612,7 @@ for (const source of SOURCES) {
}
// Replay-browser fixtures: the code line renders in FOT-RowdyStd, which no
// live-scoreboard atlas covers. Letters land on different subpixel phases,
// so keep every occurrence (array collector), like names.
// live-scoreboard atlas covers. Every occurrence kept (array), like names.
const REPLAY_SOURCES: { frame: string; code: string }[] = [
{
frame:
@@ -639,10 +629,9 @@ const REPLAY_SOURCES: { frame: string; code: string }[] = [
"scoreboard-battle-log-replay/private-battle-crableg-capital/frame.png",
code: "R1V4-PAHW-GGM2-PD9S",
},
// Heavily compressed stream captures: font-rendered templates lose to
// fixture crops of lookalikes on these (E beat a real F by 0.1+), so the
// chars they cover (8 B F J N T U 5 among them) need crops at this
// fidelity. brinewater-1411/marlin stay out as generalization checks.
// Heavily compressed stream captures: font-rendered templates lose to fixture
// crops of lookalikes here (E beat a real F by 0.1+), so the chars they cover
// need crops at this fidelity. brinewater-1411/marlin stay out as generalization checks.
{
frame:
"scoreboard-battle-log-replay/x-battle-rainmaker-brinewater-1404/frame.png",
@@ -671,9 +660,8 @@ for (const source of REPLAY_SOURCES) {
}
// Death splash-tag names (BlitzBold + Rowdy kana at ~46px). The atlas is
// nominal 42 with the detector upscaling to 46 at load (see the builder
// comment in scripts/scanner/build-glyph-atlas.ts), so native-size crops shrink to
// 42 here and come back to native after the load-time upscale.
// nominal 42 with the detector upscaling to 46 at load (see build-glyph-atlas.ts),
// so native-size crops shrink to 42 here.
const TAG_BIN_THRESHOLD = 160; // TAG_NAME_BIN_THRESHOLD in death/index.ts
const TAG_ATLAS_HEIGHT = 42;
const DEATH_TAG_SOURCES: {
@@ -725,10 +713,9 @@ for (const source of DEATH_TAG_SOURCES) {
}
}
// JA death-message lines (condensed Kurokane/Rowdy blend at ~40px; the
// atlas is native-size, matched unscaled by the detector's JA read path).
// ROIs are fixture-specific tight boxes around each line so scene ink
// outside the burst stays out of the runs.
// JA death-message lines (condensed Kurokane/Rowdy blend at ~40px; native-size
// atlas, matched unscaled by the JA read path). ROIs are fixture-specific tight
// boxes around each line so scene ink outside the burst stays out of the runs.
const DEATH_JA_BIN_THRESHOLD = 190; // SPLAT_TEXT_BIN_THRESHOLD in death/rois.ts
const DEATH_JA_SOURCES: {
frame: string;

View File

@@ -1,19 +1,15 @@
/** biome-ignore-all lint/suspicious/noConsole: CLI script output */
/**
* Build glyph atlases by rendering the Splatoon fonts at the exact
* scoreboard render sizes, calibrated against the reference fixture:
* Builds glyph atlases by rendering the Splatoon fonts at the exact scoreboard
* render sizes, calibrated against the reference fixture:
*
* paint digits BlitzMain 34px (tight '0' height ~29px at 1080p)
* stat digits BlitzMain 20px (~17px)
* team digits BlitzBold 36px (~28px — team totals use the bold face,
* which is why they can't reuse paint glyphs)
* names BlitzMain 20px (cap height ~17px), full charset:
* ASCII + Latin-1 + kana + common symbols (~330 glyphs)
* team digits BlitzBold 36px (~28px — bold face, so paint glyphs can't be reused)
* names BlitzMain 20px (cap height ~17px), ASCII + Latin-1 + kana + symbols
*
* The fonts ship with the game and are not committed — drop them into
* assets/fonts/ (see README). Fixture-crop bootstrapping
* (scripts/scanner/bootstrap-atlas-from-fixture.ts) remains as a cross-check and as
* the fallback when the fonts are unavailable.
* assets/fonts/ (see README); bootstrap-atlas-from-fixture.ts is the fallback.
*
* Usage: pnpm scanner:build-glyph-atlas
* Writes SCANNER_ASSETS_DIR/glyphs/scoreboard-*.{png,json}
@@ -39,11 +35,9 @@ import { readImage, writePng } from "../../app/features/scanner/node/image-io";
import { readFontCoverage } from "./otf-cmap";
/**
* Atlases are hybrids: glyphs harvested from labeled fixtures
* (scripts/scanner/bootstrap-atlas-from-fixture.ts, tagged source:"fixture") are exact
* in-game pixels and are preserved across rebuilds; font-rendered glyphs
* fill in the rest of the charset. Recognition takes the best-scoring glyph,
* so fixture crops dominate wherever they exist.
* Atlases are hybrids: fixture-harvested glyphs (bootstrap-atlas-from-fixture.ts,
* source:"fixture") are exact in-game pixels preserved across rebuilds;
* font-rendered glyphs fill the rest of the charset and lose ties to them.
*/
const FONTS_DIR = new URL("../../assets/fonts", import.meta.url).pathname;
@@ -59,11 +53,9 @@ const FONT_FILES = {
} as const;
/**
* Which codepoints each font actually maps: canvas silently substitutes a
* system font for the rest (the Blitz cuts have no kanji/hangul/hanzi), so
* charsets are filtered through this before rendering — the CJK languages'
* ideographs drop out of the localized charsets instead of baking
* wrong-font glyphs into the atlases.
* Which codepoints each font maps: canvas silently substitutes a system font
* for the rest (the Blitz cuts have no kanji/hangul/hanzi), so charsets are
* filtered through this so no wrong-font glyphs get baked into the atlases.
*/
const fontCoverage: Record<string, (codepoint: number) => boolean> = {};
@@ -100,35 +92,31 @@ function nameCharset(): string[] {
}
/**
* Greek letters players stylize scoreboard names with, added per attested
* fixture need only ("Rιppιng_H"): most of the block are homoglyphs of latin
* or kana at capture fidelity (η~n, ε~c, Γ~か strokes...) and displace
* correct matches on ranking noise, so it is not included wholesale — and
* even a few extra narrow glyphs shift an atlas's median width enough to
* change wide-segment splitting, so it stays out of the death-tag charset
* until a death fixture attests it.
* Greek letters players stylize names with, added per attested fixture only
* ("Rιppιng_H"): most of the block are homoglyphs of latin/kana at capture
* fidelity (η~n, ε~c) and displace correct matches on ranking noise. Even a
* few narrow glyphs shift an atlas's median width enough to change
* wide-segment splitting, so it stays out of the death-tag charset until attested.
*/
const NAME_GREEK = "ια"; // ι: "Rιppιng_H", α: "◇Dαrz™" (special-symbols fixture)
/**
* The rest of the in-game name editor's symbol pickers (sendou.ink's
* IN_GAME_NAME_CHARACTER_CATEGORIES: "symbols" + "cjk-symbols"), minus what
* nameCharset() already carries via ASCII/Latin-1/kana and minus chars the
* Blitz cmap doesn't map (ˊˋ𝑓⁀⚪⚫◻◼⍑ — canvas would render a system-font
* substitute; nameSymbols() re-checks at build time). "•" stays out too:
* BlitzMain's own bullet is a 4px dot, the on-screen full-size circle comes
* from "●" via RENDER_ALIASES. The tilde is the fullwidth "~" (U+FF5E) only
* — the wave dash "〜" (U+301C) is a pixel-identical homoglyph that would
* duel it on ranking noise, and FF5E is the form the fixture labels attest.
* Like NAME_GREEK, scoreboard-names only (not death-tag) until attested.
* IN_GAME_NAME_CHARACTER_CATEGORIES "symbols" + "cjk-symbols"), minus what
* nameCharset() already carries and chars the Blitz cmap doesn't map
* (ˊˋ𝑓⁀⚪⚫◻◼⍑; nameSymbols() re-checks at build time). "•" stays out: BlitzMain's
* bullet is a 4px dot, the on-screen circle comes from "●" via RENDER_ALIASES.
* Only the fullwidth "~" (U+FF5E), the form fixture labels attest — the wave
* dash "〜" (U+301C) is a pixel-identical homoglyph that would duel it.
* Scoreboard-names only (not death-tag) until attested, like NAME_GREEK.
*/
const NAME_SYMBOLS =
"′‘’‚‛…″“”„←→↑↓⇒⇔˜€∞√∀⊂⊃∴∵∂№♭♀♂◎◇◆△▲▽▼†※™" + "『』【】〈〉《》〔〕々〆〇〃~";
/**
* Render the key char's glyph but emit it as the value char: in-game names
* show "•" as the full-size filled circle (BlitzMain's own "•" is a 4px dot
* that never appears on screen), and fixture labels write it as "•".
* Render the key char but emit it as the value: in-game names show "•" as a
* full-size filled circle (BlitzMain's own "•" is a 4px dot never seen on
* screen) and fixture labels write it as "•".
*/
const RENDER_ALIASES: Record<string, string> = { "●": "•" };
@@ -163,9 +151,8 @@ async function readFixtureGlyphs(name: string): Promise<GlyphBitmap[]> {
}
/**
* Render one glyph and tight-crop it via the alpha channel. xScale < 1
* condenses the glyph horizontally (the JA death message renders its font
* squeezed to ~3/4 width in-game).
* Renders one glyph and tight-crops it via the alpha channel. xScale < 1
* condenses horizontally (the JA death message renders squeezed to ~3/4 width).
*/
function renderGlyph(
family: string,
@@ -331,15 +318,12 @@ await build("scoreboard-names", 17, [
},
]);
/**
* Localized closed-set charsets (all 14 game languages; the canonical
* English strings are included by construction), restricted to the Latin
* scripts (< U+0250) the fixtures attest: like the Greek block in the name
* charset, wholesale Cyrillic/kana/ideograph glyphs displace Latin matches
* on ranking noise (adding them regressed the English fixtures), so the
* non-Latin languages' entries stay in the closed sets — ready to snap —
* but their glyphs wait for fixtures to tune against. Chars the font
* doesn't map are dropped too: canvas would silently render a system-font
* substitute into the atlas.
* Localized closed-set charsets (all 14 game languages), restricted to the
* Latin scripts (< U+0250) the fixtures attest: wholesale Cyrillic/kana/
* ideograph glyphs displace Latin matches on ranking noise (adding them
* regressed the English fixtures), so non-Latin entries stay in the closed
* sets but their glyphs wait for fixtures. Chars the font doesn't map are
* dropped too (canvas would render a system-font substitute).
*/
function localizedChars(texts: readonly string[], family: string): string[] {
const covers = fontCoverage[family]!;
@@ -391,12 +375,10 @@ await build("scoreboard-replay-result", 30, [
chars: localizedChars(resultTexts, "Rowdy"),
},
]);
// map-start intro splash: the big mode title on the center splat is
// BlitzBold (~76px tight caps at 1080p; a px sweep against the fixture reads
// best at 99-101), the stage name bottom-right is BlitzMain (~40px tight;
// 46-48px render). The constant "MODE" label ("Kampfart", ...) is BlitzMain
// too and is read with the stage atlas rescaled to its ~48px height, so its
// chars ride along.
// map-start intro splash: the mode title on the center splat is BlitzBold
// (~76px tight caps at 1080p; a px sweep reads best at 99-101), the stage name
// bottom-right is BlitzMain (~40px tight; 46-48px render). The constant "MODE"
// label is BlitzMain too, read with the stage atlas rescaled to ~48px.
await build("map-start-mode", 76, [
{
family: "BlitzBold",
@@ -411,11 +393,9 @@ await build("map-start-stage", 40, [
chars: localizedChars([...stageTexts, ...ALL_MODE_LABELS], "BlitzMain"),
},
]);
// death screen: the localized "Splatted by <weapon>!" burst message (tight
// caps ~28px; the face reads between the two Blitz cuts at capture
// fidelity, so carry both and let recognition take the max) and the
// splash-tag name, which is BlitzBold for latin but the angular Rowdy face
// for kana
// death screen: the localized "Splatted by <weapon>!" burst (tight caps ~28px;
// the face reads between the two Blitz cuts at capture fidelity, so carry both)
// and the splash-tag name: BlitzBold for latin, the angular Rowdy face for kana
const deathWeaponTexts = [
...ALL_WEAPON_ENTRIES.map((e) => e.name),
...Object.values(LOCALIZED_WEAPON_NAMES).flatMap((names) =>
@@ -439,15 +419,12 @@ await build("death-weapon", 34, [
chars: localizedChars(deathWeaponTexts, "BlitzBold"),
},
]);
// JA death message, a separate atlas read only by the JA line ROIs: mixing
// kana into the Latin set would shift its median width (breaking wide-
// segment splitting) and displace Latin matches on ranking noise. The
// in-game JP face renders horizontally condensed and sits between the two
// FOT cuts at capture fidelity — ス/ク read as Kurokane, で as Rowdy — so
// carry both, at the (px, xScale) pairs that peaked in an NCC sweep against
// the classic-squiffer-jp fixture. Latin/digit chars inside JP weapon names
// (LACT-450, .52ガロン) ride along in both faces. Attested-fixture rule as
// everywhere: KO/ZH names stay in the closed sets without an atlas.
// JA death message, a separate atlas read only by the JA line ROIs: mixing kana
// into the Latin set would shift its median width (breaking wide-segment
// splitting) and displace Latin matches. The in-game JP face renders condensed
// and sits between the two FOT cuts (ス/ク read as Kurokane, で as Rowdy), so
// carry both at the (px, xScale) pairs that peaked in an NCC sweep against the
// classic-squiffer-jp fixture. KO/ZH names stay in the closed sets without an atlas.
const deathJaTexts = [
...(LOCALIZED_WEAPON_NAMES.JPja ?? []).map((n) => n.text),
...DEATH_MESSAGE_TEMPLATES.filter((t) =>

View File

@@ -1,25 +1,19 @@
/** biome-ignore-all lint/suspicious/noConsole: CLI script output */
/**
* Generate the localized closed sets from the splat3 repo's language dumps
* (https://github.com/Leanny/splat3, data/language/<Lang>_full.json), so
* ingestion works no matter which language the player runs the game in.
* Detectors OCR whatever is on screen, snap it against every language's
* entries, and always emit the sendou.ink id (canonical English text
* exists only inside the snap tables).
* Generates the localized closed sets from the splat3 repo's language dumps
* (https://github.com/Leanny/splat3, data/language/<Lang>_full.json) so
* ingestion works in any game language: detectors OCR what is on screen, snap
* it against every language's entries, and emit the sendou.ink id.
*
* Sources per language:
* CommonMsg/VS/VSRuleName modes (+ the _2L two-line intro-splash
* wrap variants, e.g. "Muschel-\nchaos")
* CommonMsg/VS/VSRuleName modes (+ _2L two-line intro-splash wraps, "Muschel-\nchaos")
* CommonMsg/VS/VSStageName stages (keyed via the USen values)
* CommonMsg/MatchMode lobby tags (XMatch / Private)
* LayoutMsg/Lobby_MenuMode_00 the intro splash's "MODE" label
* LayoutMsg/Mng_Result_00 replay-browser VICTORY / DEFEAT tags
* LayoutMsg/VS_Beaten_00 (999) the death-burst message; the weapon
* placeholder sits on line 1 or 2
* depending on language, so this becomes
* a per-language template
* CommonMsg/Weapon/WeaponName_* weapon names, mapped to the canonical
* entries via their USen value
* LayoutMsg/VS_Beaten_00 (999) death-burst message; the weapon placeholder sits on
* line 1 or 2 by language, so it becomes a per-language template
* CommonMsg/Weapon/WeaponName_* weapon names, mapped to canonical entries via USen
*
* Usage: pnpm scanner:build-localized-entries [path-to-splat3]
* Writes app/features/scanner/core/localized-entries.ts
@@ -119,8 +113,6 @@ if (!languages.includes(CANONICAL_LANG)) {
const dumps = new Map<string, LangDump>(languages.map((l) => [l, loadLang(l)]));
const usen = dumps.get(CANONICAL_LANG)!;
// ---- validate the canonical sets against USen ------------------------------
for (const [key, mode] of Object.entries(RULE_KEYS)) {
const value = clean(usen["CommonMsg/VS/VSRuleName"]![key]!);
const expected = misc[`MODE_LONG_${mode}`]!;
@@ -152,8 +144,6 @@ for (const [name, stageId] of stageIdByEnglishName) {
}
}
// ---- per-language closed sets ----------------------------------------------
interface LocalizedLobby {
text: string;
lobby: ScannerLobby;
@@ -233,8 +223,6 @@ for (const category of ["lobbies", "modes", "modeWraps", "stages"] as const) {
}
}
// ---- death message templates -----------------------------------------------
const PLACEHOLDER = /\[group=[^\]]*\]/;
/** stands in for the weapon placeholder while splitting the message */
const SENTINEL = "\u0000";
@@ -282,8 +270,6 @@ for (const lang of languages) {
});
}
// ---- localized weapon names --------------------------------------------------
const WEAPON_MSGS = [
"CommonMsg/Weapon/WeaponName_Main",
"CommonMsg/Weapon/WeaponName_Sub",
@@ -328,8 +314,6 @@ for (const lang of languages) {
if (entries.length > 0) localizedWeaponNames[lang] = entries;
}
// ---- emit --------------------------------------------------------------------
const banner = (extra: string) =>
`/**
* GENERATED by scripts/scanner/build-localized-entries.ts from the splat3 repo's

View File

@@ -1,16 +1,13 @@
/** biome-ignore-all lint/suspicious/noConsole: CLI script output */
/**
* Build the planner signature atlas from the assets repo's planner renders.
*
* The sendou-ink/assets checkout's assets/planner-maps/ holds the full
* planner PNGs (~340MB), named "<stageId>-<MODE>-<TYPE>.png" (MODE in
* CB/RM/SZ/TC/TW; TYPE in OVER/MINI/ITEMS). This tool reduces each
* PLANNER_TYPE render to the ink-invariant structural signature
* (app/features/scanner/core/detectors/minimap/stage.ts) and packs all of them,
* quantized to uint8, into a single grayscale atlas PNG plus a manifest
* (keys "<stageId>-<MODE>") — a few hundred KB the minimap detector loads
* to identify the stage. Output goes to the assets checkout
* (SCANNER_ASSETS_DIR/planner); shipping a regen means pushing the assets repo.
* Builds the planner signature atlas from the assets repo's planner renders:
* assets/planner-maps/ in the sendou-ink/assets checkout holds the full PNGs
* (~340MB) named "<stageId>-<MODE>-<TYPE>.png" (MODE in CB/RM/SZ/TC/TW; TYPE
* in OVER/MINI/ITEMS). Each PLANNER_TYPE render is reduced to the
* ink-invariant structural signature (core/detectors/minimap/stage.ts) and
* packed, quantized to uint8, into one grayscale atlas PNG plus a manifest
* (keys "<stageId>-<MODE>"). Output goes to SCANNER_ASSETS_DIR/planner;
* shipping a regen means pushing the assets repo.
*
* pnpm scanner:build-planner-signatures
*/

View File

@@ -1,13 +1,10 @@
/** biome-ignore-all lint/suspicious/noConsole: CLI script output */
/**
* Minimal OpenType cmap reader: which codepoints does a font actually map?
*
* Minimal OpenType cmap reader: which codepoints a font actually maps.
* @napi-rs/canvas silently falls back to a system font for characters the
* requested family lacks (the Blitz cuts carry Latin/Greek/Cyrillic/kana
* but no kanji, hangul or hanzi), which would bake wrong-font glyphs into
* the atlases. The builder filters every charset through this before
* rendering. Supports the two subtable formats the game fonts use:
* format 4 (BMP segments) and format 12 (grouped full-range).
* family lacks (the Blitz cuts have no kanji, hangul or hanzi), which would
* bake wrong-font glyphs into the atlases. Supports format 4 (BMP segments)
* and format 12 (grouped full-range), the two the game fonts use.
*/
import { readFileSync } from "node:fs";

View File

@@ -1,9 +1,8 @@
/** biome-ignore-all lint/suspicious/noConsole: CLI script output */
/**
* CLI harness: replay a directory of extracted VoD frames through the full
* detector registry driven by a DetectorScheduler, mirroring the analyzer
* worker's chunk scan — to reproduce scheduling-dependent misses offline.
* This is the tool for "the browser scan missed an event that a fixture
* CLI harness: replays a directory of extracted VoD frames through the full
* detector registry driven by a DetectorScheduler, mirroring the worker's
* chunk scan — the tool for "the browser scan missed an event that a fixture
* parses fine": extract the surrounding footage with
* ffmpeg -ss <startT> -i vod.mkv -t 30 -vf fps=6 frames/f%04d.png
* then replay it and watch which checks the scheduler ran and what they saw.

View File

@@ -1,11 +1,8 @@
/** biome-ignore-all lint/suspicious/noConsole: CLI script output */
/**
* Accuracy report across all fixtures — separate from pass/fail testing.
* Runs each detector over its own fixture directory and prints aggregate
* per-field accuracy plus character error rate (CER) for names, the metric
* that drives glyph atlas expansion.
*
* Usage: pnpm scanner:report
* Accuracy report across all fixtures, separate from pass/fail testing:
* aggregate per-field accuracy plus character error rate (CER) for names,
* the metric that drives glyph atlas expansion. Usage: pnpm scanner:report
*/
import { loadOpenCV } from "../../app/features/scanner/core/cv";
import {

View File

@@ -1,18 +1,15 @@
/** biome-ignore-all lint/suspicious/noConsole: CLI script output */
/**
* CLI equivalent of the VoD tab: scan a video file with the full detector
* registry and write the same events CSV the tab's Export menu downloads.
* ffmpeg decodes the video to raw RGBA frames piped through the
* DetectorScheduler + detectors, and every parse event goes through a
* TimelineBuilder with the tab's default merge/confidence options — so the
* CSV matches a browser scan of the same footage (minus the calm-stretch
* keyframe skimming, which only affects speed, not results).
* CLI equivalent of the VoD tab: scans a video with the full detector registry
* and writes the same events CSV the tab's Export menu downloads. ffmpeg
* decodes to raw RGBA frames piped through the DetectorScheduler + detectors
* and a TimelineBuilder with the tab's default options, so the CSV matches a
* browser scan (minus keyframe skimming, which only affects speed).
*
* Requires ffmpeg (and ffprobe for the progress percentage) on PATH.
*
* Usage: pnpm scanner:scan-vod <video> [--fps 8] [--start T] [--duration S] [--out file.csv] [--telemetry]
* --telemetry prints the VoD tab's ?telemetry=true scan counters after the
* run (per-detector gate/parse time, scheduling savings).
* --telemetry prints the VoD tab's ?telemetry=true scan counters after the run.
*/
import { spawn } from "node:child_process";
import { writeFileSync } from "node:fs";

View File

@@ -1,20 +1,16 @@
/** biome-ignore-all lint/suspicious/noConsole: CLI script output */
/**
* Audit death/special detection against scoreboard truth, from an events CSV
* downloaded from the scanner UI (Live/VoD tab → Download CSV). The CSV rows
* are parsed back into DetectedEvents, run through the real match builder,
* and rendered into the same status spans the timeline would draw — then
* each player's span counts are diffed against the scoreboard's D/S numbers,
* which are near-always correct. A special span ending in a death (or held
* at the final whistle) is a legit non-use and does not count toward S.
* Audits death/special detection against scoreboard truth, from an events CSV
* downloaded from the scanner UI. Rows are parsed back into DetectedEvents,
* run through the real match builder and rendered into the timeline's status
* spans; each player's span counts are diffed against the scoreboard's D/S
* numbers. A special span ending in a death (or held at the final whistle) is
* a legit non-use and does not count toward S.
*
* The CSV is lossy (no ink colors, only top-1 strip-weapon candidates), so
* cast-footage side orientation and slot→row assignment can degrade to
* their fallbacks; the output flags when a mismatch looks like a
* slot-mapping artifact rather than a detection error.
*
* Output is structured for triage: every discrepancy lists the exact
* read timestamps most likely to yield a new failing fixture.
* cast-footage side orientation and slot→row assignment can degrade to their
* fallbacks; the output flags mismatches that look like slot-mapping artifacts.
* Every discrepancy lists the read timestamps most likely to yield a fixture.
*
* Usage: pnpm scanner:status-audit <events.csv> [--all]
*/
@@ -182,9 +178,6 @@ for (const [index, builtMatch] of built.entries()) {
printCandidates();
printGuidance();
// ---------------------------------------------------------------------------
// CSV → DetectedEvent reconstruction
function parseCsv(text: string): string[][] {
const result: string[][] = [];
let row: string[] = [];
@@ -451,9 +444,6 @@ function parseScoreboardRow(row: string[]): ScoreboardData & {
};
}
// ---------------------------------------------------------------------------
// Analysis
interface FixtureCandidate {
score: number;
t: number;
@@ -471,10 +461,9 @@ interface StatusSpan extends TimeWindow {
/** sample timestamps that read the flag true inside the span */
confirmingReads: number[];
/**
* widest the true state could really have held: from the last false read
* before the span to the false read that closed it (the builder's
* flank-to-flank measure); the rendered span bounds where a flank is
* unobserved (series edge or gap-split)
* widest the true state could have held: from the last false read before the
* span to the false read that closed it (the builder's flank-to-flank measure);
* the rendered span bounds where a flank is unobserved
*/
maxPossibleSeconds: number;
}
@@ -587,9 +576,6 @@ function readsInWindow(
.map((event) => Math.round(event.t * 100) / 100);
}
// ---------------------------------------------------------------------------
// Output
function ts(t: number): string {
return `t=${Math.round(t * 10) / 10} (${formatTime(t)})`;
}

View File

@@ -1,9 +1,8 @@
/**
* Config for running the scanner scripts with vite-node. Deliberately minimal:
* the root vite.config.ts pre-bundles @techstark/opencv-js for the browser
* worker, and vite-node would resolve that browser prebundle (which
* crashes on __dirname in Node). Without the include, vite-node
* externalizes the dep to a plain require of the (patched) CJS bundle.
* vite-node config for the scanner scripts. The root vite.config.ts
* pre-bundles @techstark/opencv-js for the browser worker, and vite-node would
* resolve that prebundle (which crashes on __dirname in Node); without the
* include it externalizes the dep to a plain require of the patched CJS bundle.
*/
import { defineConfig } from "vite";