Skip to content

Python API

The command line is built on the modules below; scripts can use them directly. The names each module lists in its __all__ are its documented API, and the most used ones are also importable from the top-level package:

from nnnotes import Config, Session

session = Session(Config.load())  # the config file and the NNNOTES_* environment variables
catalog = session.catalog()  # the region's catalog, with its bundles
master = session.master_dir()  # the decoded master data directory

A missing or invalid setting raises ConfigError, naming the setting and never its value; a failed game API call raises GameApiError. Other names are internal and can change in any version.

Configuration and sources

nnnotes.source.config

Settings from a TOML file, environment variables and command-line flags.

Sources, lowest to highest precedence:

  1. the TOML file: --config <file>, else the file named by NNNOTES_CONFIG, else ./nnnotes.toml when present, else the per-user file (user_file) when present;
  2. environment variables NNNOTES_<SECTION>_<KEY>: the setting's dotted name upper-cased, dots and dashes as underscores (servers.tw.cdn -> NNNOTES_SERVERS_TW_CDN, bundle.nonce_seed -> NNNOTES_BUNDLE_NONCE_SEED);
  3. command-line flags.

No setting has a default and an empty value counts as unset. A command that needs a setting nobody gave stops with a ConfigError naming the TOML key and the environment variable. Values are never printed, logged or put into an error message. Relative paths in the TOML file are relative to the file's directory; relative paths from the environment or the command line are relative to the working directory.

ConfigError

Bases: Exception

A setting is missing or malformed (the message names the setting, never its value).

Config

Merged settings. overrides: {(section, key): value} from command-line flags (None = not given).

origin
origin(section: str, key: str) -> str | None

Where a setting comes from: 'flag', 'env', 'file', or None when it is unset.

flag
flag(section: str, key: str) -> str | None

The command-line flag of a setting, when it has one.

file_keys
file_keys() -> list[tuple[str, str]]

(section, key) of every value of the TOML file, a nested table as a dotted section ('' at the top).

env_names
env_names() -> list[str]

The names of the NNNOTES_ environment variables that set something (never their values).

get_list
get_list(section: str, key: str) -> list[str]

A list of strings (TOML array; comma-separated in the environment or on the command line).

hex
hex(section: str, key: str, size: int | None = None) -> bytes

A required hex string as bytes (size: the exact byte length).

regions
regions() -> list[str]

Region names: the [servers.] tables and the regions given by NNNOTES_SERVERS__CDN.

cdn
cdn(region: str) -> str

CDN base of a region, without a trailing slash.

catalog_pin
catalog_pin(region: str | None = None) -> tuple[str, str, str] | None

The pinned resource version of an international catalog, (value, section, key) of the setting that pins it: the --catalog-version flag, else [servers.<region>] catalog_version, else [catalog] version; None when nothing pins it (session.Session.catalog_version asks the API then).

master
master(region: str | None) -> tuple[str, str]

The setting that names the decoded master data of region: the --master flag, else [servers.<region>] master, else [paths] master -> (section, key).

user_file

user_file(environ: dict[str, str] | None = None) -> Path | None

The per-user config file: %APPDATA%\nnnotes\nnnotes.toml on Windows, else $XDG_CONFIG_HOME/nnnotes/nnnotes.toml (~/.config when XDG_CONFIG_HOME is unset); None when the environment names no such directory.

config_files

config_files(path: str | Path | None = None, environ: dict[str, str] | None = None) -> list[tuple[str, Path | None]]

The config file candidates in lookup order, (how it is named, path or None): --config, NNNOTES_CONFIG, the working directory's nnnotes.toml, the per-user file. The first two are used when they are given (and must exist), the last two when they exist.

nnnotes.source.session

The data sources of one configuration: the catalog, the decoded master data, the APK and the keys.

A Session reads a setting when a command first needs it, and names the setting in a ConfigError when it is missing or wrong. The game API answers a Session needs (the master version, the resource version of an international catalog) come from the region's [servers.<region>] api; JP regions go through jp.Client.

Session

The data sources cfg names. region arguments select another configured region; None: [catalog] region.

bundle_key
bundle_key() -> BundleKey

[bundle] key and [bundle] nonce_seed.

master_key
master_key() -> MasterKey

[master] key and [master] iv.

path
path(section: str, key: str, kind: str = 'file') -> Path | None

The path a setting names, checked to exist (kind "file" or "directory"; an APK may be a directory of splits); None when the setting is unset.

store_root
store_root() -> Path

The store directory: [paths] store, else the store/ directory of [paths] cache.

apk
apk(region: str | None = None) -> Path

[paths] apk of region, required.

master_dir
master_dir(region: str | None = None) -> Path

The decoded master data of region: the --master flag, else [servers.<region>] master, else [paths] master.

master_version
master_version(region: str | None = None, *, timeout: float = gameapi.TIMEOUT) -> gameapi.MasterVersion

The master data version region serves now (its [servers.<region>] api).

catalog_version
catalog_version(region: str | None = None) -> str

The resource version of an international catalog: the --catalog-version flag, the regional or global pin, the resource version the region's API serves, else main.

catalog
catalog(bundles: bool = True, region: str | None = None) -> Catalog

The catalog of [catalog] language (merged with the APK's when [paths] apk is set), fetching from the CDN of region. International versioned catalogs are isolated by CDN root and resource version. bundles: bundles will be fetched; else only the catalog is read. An explicit [paths] catalog file bypasses discovery; a version pin bypasses the API but needs the CDN root to identify the cache. The bundle key is only read for a missing encrypted bundle.

nnnotes.source.catalog

Addressables catalog access: dependency closures, a bundle cache, remote and APK-local bundles.

Remote bundles are fetched from the region's CDN and decrypted (addressables.decrypt); local bundles (shipped inside the APK) are read from a user-supplied base.apk. Nothing here bundles or redistributes game data: it only reads what the user points it at into a local cache the user controls.

Catalog

Remote content catalog, merged with the APK's local catalog when an APK is given.

The game loads both: the APK catalog (embedded assets, e.g. ADV settings and shared prefabs) and the remote catalog (downloadable content). Entry offsets are per-catalog, so APK entries are moved into their own offset namespace.

cdn: the region's CDN base (needed only to download what the cache lacks); bundle_key: the bundle decryption key (needed only for bundles not yet in the cache). Either may be a function that returns it, called the first time it is needed (a ConfigError it raises is raised naming the file that needed the setting). apk_catalog: the stored catalog to replay without opening an APK; JP local reads verify it before extraction.

cdn property
cdn: str | None

The CDN base as given (None while it is given as a function not called yet).

check_apk
check_apk(archive) -> None

Before extracting a JP local file, verify the APK against this catalog's snapshot.

local_cache_dir
local_cache_dir() -> Path

JP embedded files also depend on the APK catalog, independently of the CDN snapshot.

load classmethod
load(language: str, cache_dir: Path, *, cdn=None, bundle_key=None, apk: Path | None = None, version: str = 'main') -> Catalog

The remote catalog of language from the cache, downloaded from the CDN on first use (cdn / bundle_key: as for Catalog). A versioned catalog is isolated by CDN root and version; main retains its legacy offline cache path. Missing versioned files never fall back to main.

cache_file staticmethod
cache_file(language: str, cache_dir: Path, *, version: str = 'main') -> Path

Where the remote catalog of language is cached.

entry
entry(key: str) -> dict

The location of key: its asset location (internal id under Assets/ or Packages/) when it has one, else its first location. KeyError when the catalog lacks the key.

dependencies
dependencies(key: str) -> list[dict]

The locations the location of key (entry) depends on, in its dependency order (those the catalogs hold).

as_bundle staticmethod
as_bundle(e: dict) -> Bundle | None

The bundle file a location names (a remote or APK-local .bundle), else None.

sources
sources() -> dict[str, bytes]

The catalog files this catalog was made of: {"remote": bytes, "apk": bytes (with an APK)}.

locations
locations() -> list[dict]

Every location of the remote catalog and (with an APK) of the APK catalog, fully decoded (addressables.parse_locations), with catalog ("remote" / "apk") and kind (location_kind). Offsets and dependencies are those of entries (APK ones in their own namespace).

bundles
bundles() -> list[Bundle]

Every bundle file the catalogs name (remote and APK-local), one per file name, sorted by name. A file the APK catalog lists gets the APK catalog's location (the remote catalog's references to APK files may describe another build), else its first location.

raw_files
raw_files() -> list[dict]

Every raw file the catalogs name (a CDN or APK file that is not a bundle, e.g. CRI ACB/AWB/USM data), one entry per file name (file_name), sorted by name, preferring the APK catalog's as bundles() does. Remote ones are what fetch_raw takes.

resolve
resolve(key: str) -> list[Bundle]

Full bundle closure for an addressable key (BFS over dependencies).

cached
cached(b: Bundle) -> Path | None

The file fetch(b) returns when the bundle is in the cache already, else None.

cached_raw
cached_raw(e: dict) -> Path | None

The file fetch_raw(e) returns when it is in the cache already, else None.

fetch
fetch(b: Bundle) -> Path

Local path to the decrypted bundle (CDN download or APK read).

fetch_key
fetch_key(key: str, skip=frozenset()) -> list[Path]

Every bundle of the key's closure but those named in skip; APK-local ones only when an APK is set.

bundle_of
bundle_of(key: str) -> Bundle

The bundle that holds the asset key: the first bundle among its location's dependencies (a bundle key: that bundle).

fetch_raw
fetch_raw(e: dict) -> Path

A non-bundle CDN asset (e.g. CRI ACB/AWB data) as stored -- no decryption.

apk_bundle
apk_bundle(name_contains: str) -> Path

An APK-local addressable bundle whose file name contains the substring.

nnnotes.source.addressables

Addressables content catalogs, asset bundle decryption and a local catalog browser.

A remote content catalog (catalog_main_<language>.bin, Addressables binary catalog format version 2) lists every addressable key with its location: an asset path inside a bundle, a bundle, or a raw file. Remote locations are absolute URLs on a placeholder host that the game replaces with its CDN base at load time. parse reads what the extractors need (keys, internal ids, dependencies); parse_locations decodes every field of every location, including the AssetBundleRequestOptions of bundle and raw file locations (content hash, CRC, size, internal bundle name), and parse_header / parse_keys the header and the key table.

Asset bundles on the CDN and inside the APK are encrypted in their first 16 KiB with AES-128 in CTR mode: the nonce is the first 8 bytes of SHA-256(nonce seed + UTF-8 bundle file name), the counter block is nonce + 64-bit big-endian counter starting at 0. A file that already starts with the UnityFS signature is not encrypted. The key and the nonce seed come from the configuration ([bundle] key, [bundle] nonce_seed).

BundleKey dataclass

AES-128 key and nonce seed of the bundle encryption (never shown in a repr).

decrypt

decrypt(data: bytes, filename: str, key: BundleKey) -> bytes

A bundle file as stored -> UnityFS bytes. filename: the bundle's bare file name (it seeds the nonce).

parse

parse(data: bytes) -> list[dict]

Every location of a binary catalog: {offset, primary_key, internal_id, dependencies (location offsets)}.

parse_header

parse_header(data: bytes) -> dict

The header of a binary catalog: magic, version, keysOffset, locatorId, instanceProvider, sceneProvider ({id, assembly, type, data}), initObjects (the same, the providers the catalog initializes), buildResultHash.

parse_keys

parse_keys(data: bytes) -> list[dict]

The key table of a binary catalog in stored order (ContentCatalogData.ResourceLocator.KeyData {u32 key object, u32 location set}): {"key": value, "type": the key's type name, "locations": location offsets}.

parse_locations

parse_locations(data: bytes) -> list[dict]

Every location of a binary catalog, fully decoded, by offset: what parse() gives (offset, primary_key, internal_id, dependencies) plus provider, dependency_hash, resource_type (class names) and extra_data.

A location record (ResourceLocation.Serializer.Data) is a fixed 28-byte value with no length prefix: primary key and internal id (strings read with '/'), provider (read with '.'), dependency set (u32 array of location offsets), dependency hash (i32), extra data (an ObjectTypeData) and resource type (a TypeSerializer.Data). extra_data of bundle and raw file locations is an AssetBundleRequestOptions: {type, hash, bundleName, crc, bundleSize, timeout, redirectLimit, retryCount, flags and the flag bits by name}.

nnnotes.source.master

Master data files -> JSON tables.

A master data file (<Table>.bin) is a 64-byte prefix followed by the table encrypted with Rijndael (256-bit block, 256-bit key, CBC, PKCS7 padding); the plaintext is gzip-compressed UTF-8 JSON ({"_allData": [rows]}). The game reads it in MasterDataModelBox<,>.Load: RijndaelEncryption.FixedDecrypt (the data after the prefix, the CryptProvider key and IV), then Compression.Decompress (GZipStream), then JsonUtility.FromJson.

AES is Rijndael with 128-bit blocks only, so the 256-bit block cipher is implemented here (numpy, every block of a file at once). Key and IV come from the configuration ([master] key, [master] iv).

download fetches a master data version from the region's CDN: <cdn>/master/<version>/MasterManifest.json ({"version", "files": [{"name", "hash", "size"}]}) and the files it lists, each checked against its SHA-256. The version a region serves now comes from the game's API (session.Session.master_version).

MasterKey dataclass

Key and IV of the master data encryption (never shown in a repr).

DownloadError

Bases: RuntimeError

A CDN download failed (the message names the file, not the URL).

decode

decode(raw: bytes, key: MasterKey, rk: ndarray | None = None) -> bytes

A master data file as stored -> its JSON text (UTF-8 bytes).

decode_files

decode_files(files: list[Path], out_dir: Path, key: MasterKey, workers: int = 8) -> dict

Decode master data files into /.json. Returns {decoded, failed: [{file, error}]}.

table

table(master_dir: Path, name: str) -> list[dict]

The rows of a decoded master table: _allData of /.json.

has_row

has_row(master_dir: Path, name: str, row_id) -> bool

Whether the decoded master table name has a row whose _id is row_id.

download

download(cdn: str, version: str, out_dir: Path, workers: int = 16, *, get=None, strict=False) -> dict

Master data version from the CDN into / (the manifest and every listed file, SHA-256 checked; files already present with the right hash are kept).

nnnotes.source.gameapi

Anonymous calls to the game's API: the master data version a region serves, and the server list.

The game's API is gRPC over HTTP/2 with TLS. Two unary methods can be called anonymously; both take an empty request message:

app.masterdata.MasterdataService/Version, on a region's API root response string version = 1 (the master data version: the <version> of <cdn>/master/<version>/) and string resource_version = 2. GetServerList, on the bootstrap API root response repeated ServerInfo servers = 1; of ServerInfo only string name = 1, string cdnRoot = 2, string apiServerRoot = 3, string displayName = 7 and string areaID = 8 are read. A root field can hold several alternative roots separated by |.

Every call sends the metadata the game client sends with each call: x-request-id (a random UUID), x-platform (android) and x-client-version (the client version). Requests and responses are raw bytes; the few fields above are read with a small protobuf wire-format decoder, without generated code. A call has a deadline and is retried when the server is unreachable, misses the deadline or answers with an empty message. Error messages name the method and the setting, never the address.

GameApiError

Bases: RuntimeError

A game API call failed (the message names the method and the setting, never the address).

Server dataclass

An entry of the server list. cdn / api: the alternative roots in the server's order (not in the repr).

fetch_master_version

fetch_master_version(root: str, client_version: str, *, timeout: float = TIMEOUT, setting: str = 'the API root') -> MasterVersion

The master data version the API root root serves now (an anonymous Version call with client_version); a GameApiError naming setting when the call fails or the answer has no version.

fetch_server_list

fetch_server_list(root: str, client_version: str, *, timeout: float = TIMEOUT, setting: str = 'the bootstrap API root') -> list[Server]

The server list the bootstrap API root root serves (an anonymous GetServerList call with client_version); a GameApiError naming setting when the call fails or the list is empty.

apk_version_name

apk_version_name(apk: Path) -> str | None

versionName of the APK's AndroidManifest.xml (None when the APK has none).

nnnotes.source.jp

Japanese release: anonymous Version discovery and snapshot-bound CDN downloads.

Addresses and client versions come from Config. Credentials remain in a Client, never in source metadata, cache records, task descriptions or exception messages.

Client

The JP game API and CDN of one configured region: observe() asks the API for the current master version, asset source and CDN credential; get() downloads a file of one source or master version with that credential, observing again when it is older than five minutes.

observe
observe(*, timeout=gameapi.TIMEOUT)

Ask the API for the current Observation: master version, asset source, CDN and its credential.

get
get(url, *, source=None, master=None, limit=FILE_LIMIT)

Download against one immutable source. Auth refresh never changes the requested snapshot.

origin

origin(value)

The origin (scheme://host[:port], without a default port) of an HTTPS URL with no path, query or credentials (HTTP only on a loopback host); a ConfigError for any other value.

Music data

nnnotes.musicdata

Music data: one JSON file with every live song and chart of one master data version (format nnnotes.music-data/2, docs/music-data.md): titles and credits in every language, bands, vocal characters, category, tags, release time, score ranks, the live BGM's length, the Gekisou catalog (member cards, snaps and their Gekisou skills), per difficulty the chart facts (level, note counts, BPM, chart times, skill events, fever ranges) and the chart's deck statistics, what the chart contributes to the live score whatever the deck, with Gekisou on (a Gekisou live, every rank) and off (a solo live), measured by the deck model ournotes-deck (the extension module the package ournotes-sim).

Master data is read from the files as served (deckdata.master_files / apk_master: SHA-256 checked against the manifest, decoded with master.decode). Charts are the TextAssets Live/MusicScore/<file name> converted by score.runtime_score; the BGM length is read from the cue sheet's ACB (cue Length and the stream's sample count), without decoding audio. The deck model reads the deck input (deckdata.build: the charts' runtime notes and the master data tables it needs) in memory; its statistics are checked against the chart facts and the master data. With full the file also carries that deck input (master, charts), every chart's runtime notes and the tables. With a jackets directory, every song's jacket (the Texture2D Image/Jacket/<jacket>) is written there as <jacket>.webp, scaled to at most JACKET_SIZE pixels on its longer side.

The output is canonical (deckdata.encode): minified UTF-8 with one trailing LF, keys in a fixed order, songs sorted by id, master data floats as the shortest decimal of their binary32 value, the deck model's numbers as it writes them. The same inputs and deck model give the same bytes. The file is written only when every table, chart and cue sheet was read and every chart measured; any missing or unreadable input is a MusicDataError naming it.

MusicDataError

Bases: ValueError

An input the music data file cannot be made from (the message names it).

Deck

The deck model (the package ournotes-sim): native chart expectations. seeds selects replay examples; it does not sample expectations. With cache, the native model persists reusable simulation programs and runs. Python passes the complete current input on every call and publishes the native cache counters separately from the music data.

info
info() -> dict

The identity and statistics format of the installed model.

stats
stats(deck_input: dict) -> dict

The current chart-statistics document, preserving every number exactly as the model writes it.

published_seeds

published_seeds(n: int) -> list[int]

The first n seeds of ournotes-deck's published seed set (a smaller set is a prefix of a larger one).

validate_expectation

validate_expectation(stats: dict, kinds: int, where: str) -> None

Check a chart's deck statistics (charts[].deck with the chart's judgedNotes, numbers read by deckdata.loads): the nominal expectations of the Gekisou lives with their checks, and the deterministic Free Live measurements. Raises MusicDataError naming where; kinds: the number of skill kinds.

validate_aptitude_statistics

validate_aptitude_statistics(header: dict | None, stats: dict, where: str) -> dict | None

A chart's gekisouAptitude after checking it: null exactly when the aptitude is left out, the master data has no shape, the chart is unplayable with Gekisou or has no Gekisou range; else a factor per range and a variant per shape of the chart's missions (both band match results of a shape with a band condition), each with finite outward estimates, consistent tail identities and its expectation check within its bound.

build

build(tables: dict[str, list[dict]], table_sha: dict[str, str], fetch: Callable[[str], bytes], bgm: Callable[[str, str], dict] | None, *, region: str, client: dict, catalog: dict, master_source: str, master_version: str | None, deck: Deck | None = None, full: bool = False) -> dict

The music data document. fetch(file name): a chart TextAsset's bytes (KeyError when there is none); bgm(cue sheet, cue): the BGM length (catalog_bgm), None to leave every song's bgm.length null; deck: the deck model measuring the songs' charts (and their Gekisou aptitude), None to leave every chart's deck null; full: add the deck input (master, charts: deckdata.TABLES and every chart's runtime notes). tables: tables_of(deck, full).

export

export(out, src: MasterSource, key, fetch: Callable[[str], bytes], bgm: Callable[[str, str], dict] | None, *, region: str, client: dict, catalog: dict, deck: Deck | None = None, full: bool = False, jacket: Callable[[str], bytes] | None = None, jackets_dir=None, replay_dir=None, replay_engine=None, recommend_engine=None) -> dict

Read the master data, every chart and every BGM cue sheet, measure the charts with deck, then write the file out (gzip when it ends in .gz) through a temporary file and a rename; with jacket and jackets_dir, first every song's jacket as <jackets_dir>/<jacket>.webp. Returns the summary.

nnnotes.musicdata.deckdata

Deck input: every live chart as the client builds it at runtime and the master data tables the deck model (ournotes-deck) reads, for one master data version. nnnotes music-data hands it to the deck model in memory (DECK_FORMAT, the reader's format) and writes it into the music data file with --full (docs/music-data.md).

Master data is decoded from the files as served: a directory with MasterManifest.json and the .bin files it lists (nnnotes master download), or the same layout inside the APK (assets/Master/). Each file is checked against the manifest's SHA-256 and decoded with master.decode. Master data decoded elsewhere is read as it is: a directory of decoded tables (<Table>.json) with the MasterManifest.json of the files they were decoded from, whose version and SHA-256 are taken as the manifest lists them (decoded_master). Charts are the TextAssets Live/MusicScore/<file name> of every MasterLiveMusicScore row, read from the catalog and converted by score.runtime_score; notes are listed in the order the client enumerates them.

The encoding is canonical (encode): minified UTF-8 with one trailing LF, keys in a fixed order, charts sorted by score id, numbers the master data writes with a fraction or exponent as the shortest decimal that reads back as the same binary32 value (infinity as 1e999 / -1e999; a NaN is an error). The same inputs give the same bytes; a .gz output is gzip with no file name and a zero modification time. Any missing or unreadable input is a DeckDataError naming it.

DeckDataError

Bases: ValueError

An input the deck data cannot be made from (the message names it).

MasterSource dataclass

Master data files as served: version and the SHA-256 of each file name from the manifest, and a reader of files by name. decoded: the reader has the decoded tables instead (<Table>.json for <Table>.bin).

Num

Bases: str

A number token of a JSON document, written back as it is.

master_files

master_files(directory) -> MasterSource

The master data files of a directory written by nnnotes master download (MasterManifest.json + .bin).

decoded_master

decoded_master(directory) -> MasterSource

Decoded master data: a directory of decoded tables, <Table>.json with the _allData rows (as nnnotes master decode writes them), and the MasterManifest.json of the files they were decoded from (a master data snapshot published with its manifest). The version and each file's SHA-256 are the manifest's: the decoded tables cannot be checked against the files as served.

apk_master

apk_master(apk) -> MasterSource

The master data files the APK ships (assets/Master/: MasterManifest.json + .bin).

loads

loads(text: str | bytes)

The JSON document text with every non-integer number kept as its token (a Num), so that checks and writes see the number as written.

Site and schemas

nnnotes.site.webmodel

Live2D models -> the model part of an ournotes-player site (web.py builds the charts of the same site).

<site>/models.json             model index: id, manifest path, key, listing facts, sizes
<site>/models/<id>.json        model manifest (format sitefiles.MANIFEST_FORMAT): every path of the model's files ->
                               {asset, size}, or, for a large JSON object, {parts: [[key, asset, size], ...], size}
                               (as a chart manifest)
<site>/assets/<sha256>.<ext>   content-addressed files, shared with the charts (the models share their shaders)

The files of one model (the paths in its manifest), only those the player reads (read_files):

model.json                     index: format (MODEL_FORMAT), name, key, moc3, prefab, textures, canvas, shaders,
                               resources, motionSync
<name>.moc3                    the moc3 (as live2d.extract_runtime writes it)
<name>.prefab.json             the whole prefab, clips, expressions and controllers inlined (the same)
textures/*.png                 the atlas pages the drawables draw with (the same)
shaders/shaders.json           the shader index (shader.py layout, filtered as in the chart sites:
                               sitefiles.collect),
shaders/<shader>.json          the parsed forms and the GLES3 programs (GLSL ES 3.00) the drawables' materials
shaders/<shader>/gles3/*.glsl  select by their keywords, each also with STORY_KEYWORDS (the keywords the story
                               renderer adds to a character draw); the mask shader's only when a drawable is masked

resources in model.json: the materials the Cubism mask pass draws with (the Resources.Load paths of RESOURCES, read from the APK's boot data). motionSync: the prefab's root has the MotionSync controller with its CRI audio input (has_motion_sync); a model without it takes its mouth in a story from the voice's CRI Lips analysis.

Models are the catalog keys Character/Live2D///model/; a model's id is (unique in the catalog, URL-safe). Each model is exported into a temporary directory, stored, and the directory deleted; models run in parallel worker processes (catalog cache writes serialized by a lock). A model whose manifest exists and is current is skipped unless force; an outdated one (outdated: its model.json older than MODEL_FORMAT or without motionSync, or unreadable) is built again. Same inputs and library versions give byte-identical outputs. A model's files read no master data and the regions serve the same catalog, so one build serves every region of a site (the bundles are fetched from the CDN of region).

Names (optional): when master data is configured for region, the listing of each model the master data maps to a character (model_names: MasterCharacterCostume -> MasterCharacter -> MasterText) gets character, names and label; the manifests of skipped models get the same fields of this build. Without master data the fields are not written (and skipped manifests keep theirs).

Model sources: story.build takes the models of an episode from a source, ensure(address) -> {"id", "motionSync"} plus story_fields(story_dir) (what story.json gets besides models). ModelDir exports each model into

// (the files above; nnnotes export story), SiteModels reads model.json from the model manifests of a site (the site's stories).

model_id

model_id(key: str) -> str

The id of a model key: its .

model_keys

model_keys(keys) -> dict[str, str]

{id: key} of the model keys among keys, in id order; two keys with the same id raise.

catalog_models

catalog_models(cat, specs=None) -> dict[str, str]

select over the model keys of the catalog cat.

nnnotes.schemas

The JSON Schemas (draft 2020-12) of the documents nnnotes writes, shipped with the package: <name>.schema.json, each with the $id urn:nnnotes:schema:<name> that the others reference.

names

names() -> list[str]

The schema names (music-data, plan, ...), sorted.

load

load(name: str) -> dict

The schema name (music-data for music-data.schema.json).