mirror of
https://github.com/Sendouc/sendou.ink.git
synced 2026-09-30 23:27:55 -05:00
Simplify comments
This commit is contained in:
@@ -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;
|
||||
|
||||
@@ -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) =>
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
*/
|
||||
|
||||
@@ -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";
|
||||
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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";
|
||||
|
||||
@@ -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)})`;
|
||||
}
|
||||
|
||||
@@ -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";
|
||||
|
||||
|
||||
Reference in New Issue
Block a user