Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 11 additions & 1 deletion .github/scripts/smoke_wheel.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,8 +14,11 @@

import nnnotes
import ournotes_sim
from nnnotes import musicdata
from nnnotes import musicdata, schemas
from nnnotes.formats.hca import key_table
from nnnotes.musicdata import deckdata
from nnnotes.packing import AudioOptions
from nnnotes.source.master import MasterKey, decode, encode

assert Path(nnnotes.__file__).resolve().is_relative_to(Path(sys.prefix).resolve())
deck = musicdata.Deck(workers=1, aptitude=False)
Expand All @@ -28,6 +31,12 @@
stats = json.loads(ournotes_sim.chart_stats(json.dumps(data), workers=1, aptitude=False))
assert stats["format"] == info["format"]
assert stats["charts"] == []
assert AudioOptions().quality == "high"
assert schemas.load("song-package")["title"] == "nnnotes.song-package/1"
assert schemas.load("pack-target")["title"] == "nnnotes.pack-target/1"
assert bytes(key_table(123)).translate(key_table(123, decrypt=True)) == bytes(range(256))
key = MasterKey(bytes(range(32)), bytes(reversed(range(32))))
assert decode(encode(b'{"_allData": []}', key), key) == b'{"_allData": []}'
print(f"nnnotes {nnnotes.__version__}: {info['name']} {info['version']} ({info['commit']})")
"""

Expand All @@ -43,6 +52,7 @@ def main(directory: str) -> None:
subprocess.run([python, "-I", "-m", "pip", "install", f"{wheel}[deck]"], cwd=root, check=True)
subprocess.run([python, "-I", "-c", SMOKE], cwd=root, check=True)
subprocess.run([scripts / ("nnnotes.exe" if os.name == "nt" else "nnnotes"), "--help"], cwd=root, check=True)
subprocess.run([python, "-I", "-m", "nnnotes", "pack", "audio", "--help"], cwd=root, check=True)


if __name__ == "__main__":
Expand Down
6 changes: 6 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,12 @@ All notable changes to this project are listed here. Versions follow [Semantic V
over the command line (commands, options, exit statuses and `--json` reports), the configuration keys, the JSON
schemas in `nnnotes.schemas` and the documented Python API. Before 1.0 a minor version can change any of them.

## [0.3.1](https://github.com/MetaSekaiLab/nnnotes/compare/v0.3.0...v0.3.1) - 2026-10-22

### Features

- **packing:** Build song packages and HCA cue sheets ([dc198b0](https://github.com/MetaSekaiLab/nnnotes/commit/dc198b0c977b2b3ae1bffaca0e3ce3c2653133c6))

## [0.3.0](https://github.com/MetaSekaiLab/nnnotes/compare/v0.2.5...v0.3.0) - 2026-10-19

### Compatibility
Expand Down
11 changes: 6 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,13 +20,13 @@ or S3-compatible object storage.
Python 3.11 or newer:

```sh
pip install nnnotes==0.3.0
pip install 'nnnotes[fonts]==0.3.0' # story and UI fonts
pip install 'nnnotes[remote]==0.3.0' # S3-compatible depot storage
pip install 'nnnotes[deck]==0.3.0' # music-data chart statistics
pip install nnnotes==0.3.1
pip install 'nnnotes[fonts]==0.3.1' # story and UI fonts
pip install 'nnnotes[remote]==0.3.1' # S3-compatible depot storage
pip install 'nnnotes[deck]==0.3.1' # music-data chart statistics
```

Choose the extras your commands need; they can be combined as `nnnotes[fonts,remote,deck]==0.3.0`.
Choose the extras your commands need; they can be combined as `nnnotes[fonts,remote,deck]==0.3.1`.
Audio/video conversion uses [vgmstream](https://vgmstream.org/) and [FFmpeg](https://ffmpeg.org/).
Site builds additionally require Node.js 22+ and a built **ournotes-player 0.2.x** package.
The deck extra installs `ournotes-sim==0.0.12`.
Expand Down Expand Up @@ -70,6 +70,7 @@ story languages. Reusing a site name updates selected topics and retains the oth
| Configure inputs | `config init`, `config check`, `servers` | [Configuration](https://metasekailab.github.io/nnnotes/configuration/) |
| Inspect or retain catalogs | `catalog keys`, `browse`, `versions`, `import`, `fetch`, `diff` | [Commands](https://metasekailab.github.io/nnnotes/cli/) |
| Copy source bundles | `bundle pull` | [Asset export](https://metasekailab.github.io/nnnotes/assets/) |
| Package songs and audio | `pack plan`, `pack build`, `pack verify`, `pack audio` | [Song packing](https://metasekailab.github.io/nnnotes/song-packing/) |
| Export a subject | `export episode`, `story`, `live`, `live2d`, `spot`, `room`, `shader`, `audio`, `ui` | [Exports](https://metasekailab.github.io/nnnotes/export/) |
| Convert selected assets | `export assets`, `depot plan`, `depot run` | [Asset export](https://metasekailab.github.io/nnnotes/assets/) |
| Check and retain stored work | `depot verify`, `depot gc` | [Depot](https://metasekailab.github.io/nnnotes/depot/) |
Expand Down
11 changes: 6 additions & 5 deletions README.zh-CN.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,13 +18,13 @@ nnnotes 将你自己的 BanG Dream! Our Notes 游戏文件导出为结构化数
需要 Python 3.11 或更新版本:

```sh
pip install nnnotes==0.3.0
pip install 'nnnotes[fonts]==0.3.0' # 剧情与 UI 字体
pip install 'nnnotes[remote]==0.3.0' # 兼容 S3 的 depot 远端
pip install 'nnnotes[deck]==0.3.0' # music-data 谱面统计
pip install nnnotes==0.3.1
pip install 'nnnotes[fonts]==0.3.1' # 剧情与 UI 字体
pip install 'nnnotes[remote]==0.3.1' # 兼容 S3 的 depot 远端
pip install 'nnnotes[deck]==0.3.1' # music-data 谱面统计
```

按需选择附加依赖,也可以组合为 `nnnotes[fonts,remote,deck]==0.3.0`。
按需选择附加依赖,也可以组合为 `nnnotes[fonts,remote,deck]==0.3.1`。
音视频转换需要 [vgmstream](https://vgmstream.org/) 与 [FFmpeg](https://ffmpeg.org/)。
站点构建另需 Node.js 22+ 和已构建的 **ournotes-player 0.2.x** 包。
`deck` 附加依赖安装 `ournotes-sim==0.0.12`。
Expand Down Expand Up @@ -68,6 +68,7 @@ python -m http.server 8000 --directory out/site
| 配置输入 | `config init`、`config check`、`servers` | [配置](https://metasekailab.github.io/nnnotes/configuration/) |
| 查看与保存目录 | `catalog keys`、`browse`、`versions`、`import`、`fetch`、`diff` | [命令](https://metasekailab.github.io/nnnotes/cli/) |
| 复制源资源包 | `bundle pull` | [资源导出](https://metasekailab.github.io/nnnotes/assets/) |
| 打包歌曲与音频 | `pack plan`、`pack build`、`pack verify`、`pack audio` | [歌曲打包](https://metasekailab.github.io/nnnotes/song-packing/) |
| 导出主题 | `export episode`、`story`、`live`、`live2d`、`spot`、`room`、`shader`、`audio`、`ui` | [导出](https://metasekailab.github.io/nnnotes/export/) |
| 转换指定资源 | `export assets`、`depot plan`、`depot run` | [资源导出](https://metasekailab.github.io/nnnotes/assets/) |
| 检查与保留存储结果 | `depot verify`、`depot gc` | [Depot](https://metasekailab.github.io/nnnotes/depot/) |
Expand Down
10 changes: 10 additions & 0 deletions docs/api.md
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,16 @@ raises `GameApiError`. Other names are internal and can change in any version.

::: nnnotes.source.jp

## Asset packing

::: nnnotes.packing

::: nnnotes.unity.pack

::: nnnotes.unity.bundlefs

::: nnnotes.source.catalogbuild

## Music data

::: nnnotes.musicdata
Expand Down
2 changes: 1 addition & 1 deletion docs/architecture.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ subpackage of `nnnotes` and imports only the layers below it.
| Layer | Subpackage | What it holds |
|---|---|---|
| Command line | `nnnotes.cli` | one module per command group; parses options, reads the configuration, prints summaries and `--json` reports |
| Products | `nnnotes.site`, `nnnotes.musicdata` | the ournotes-player site (charts, models, stories, indexes); the music data file and its deck statistics |
| Products | `nnnotes.site`, `nnnotes.musicdata`, `nnnotes.packing` | the ournotes-player site; music data and deck statistics; local song and audio packages |
| Pipeline | `nnnotes.pipeline` | stages, task descriptions and results, the content-addressed depot, plans, layouts, the catalog versions, the views of the master data |
| Content | `nnnotes.content` | the game's subjects as exports: ADV episodes and stories, lives and scores, spots, rooms, the UI library |
| Unity objects | `nnnotes.unity` | bundle reading, object ids, atomic texture/sprite/mesh/glTF/PNG conversions, class converters (`nnnotes.unity.convert`), clip bindings (`nnnotes.unity.bindings`), script type trees (`nnnotes.unity.scripttypes`), player data, text styles, font protocols and texel packing |
Expand Down
101 changes: 101 additions & 0 deletions docs/cli.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,8 @@ to standard error. Exit status 2 is a usage or settings error.
| `--master` `DIR` | decoded master data directory ([paths] master) |
| `--apk` `FILE` | base.apk ([paths] apk) |
| `--ffmpeg` `EXE` | ffmpeg executable ([paths] ffmpeg; else on PATH) |
| `--ffprobe` `EXE` | ffprobe executable ([paths] ffprobe; else on PATH) |
| `--vgaudio` `FILE` | VGAudioCli executable or .NET DLL ([paths] vgaudio; else on PATH) |
| `--vgmstream` `EXE` | vgmstream-cli executable ([paths] vgmstream; else on PATH) |
| `--node` `EXE` | Node.js executable ([paths] node; else on PATH) |
| `--json` | print the command's report as JSON on standard output (default: a short summary) |
Expand Down Expand Up @@ -126,6 +128,19 @@ keys, bundles, raw files (and objects) that differ between two catalog versions
| `old` | label, version id or sha256 prefix |
| `new` | label, version id or sha256 prefix |

## nnnotes catalog rebuild

write a catalog with added locations (`--add`), removed primary keys (`--drop`) and updated bundle request options (`--bundle`)

| Argument | Description |
| --- | --- |
| `CATALOG` | the catalog to read (catalog_<version>_<language>.bin) |
| `-o`, `--out` `OUT` | the catalog to write (required) |
| `--add` `FILE` | JSON location entries or bundle descriptions with file, internal_id, bundle_name and assets; bundle descriptions generate crc, bundleSize and a cache hash from the bundle (repeatable) |
| `--drop` `KEY` | drop every location of this addressable key (repeatable) |
| `--bundle` `FILE` | write this bundle's decompressed content crc, stored bundleSize and deterministic cache hash (MD5 of the UnityFS bytes) into matching existing or added locations (repeatable) |
| `--hash` `HASH` | override the generated cache hash for one --bundle with 32 hexadecimal digits |

## nnnotes bundle pull

fetch the bundle closure of keys and copy the decrypted bundles to a directory (prints the bundle files)
Expand Down Expand Up @@ -162,6 +177,92 @@ a master data version from the region's CDN (SHA-256 checked)

Exactly one of `--version`, `--latest`.

## nnnotes pack plan

check a song project and its local resource dependencies

| Argument | Description |
| --- | --- |
| `file` | nnnotes.song-package/1 JSON project |

## nnnotes pack verify

verify a generated song package against its local baseline

| Argument | Description |
| --- | --- |
| `directory` | directory containing package.json |
| `--target` `TARGET` | nnnotes.pack-target/1 JSON target (required) |

## nnnotes pack audio

MP3/WAV/FLAC/AAC -> a validated single-cue HCA ACB

| Argument | Description |
| --- | --- |
| `source` | local source audio file |
| `-o`, `--out` `OUT` | output ACB file (required) |
| `--target` `TARGET` | JSON target with local ACB templates and cipher settings (required) |
| `--template` `TEMPLATE` | target ACB profile (default full) (one of `full`, `preview`) |
| `--cue-name` `CUE_NAME` | name of the output cue and sheet (required) |
| `--report` `REPORT` | validation JSON path (default <output>.json) |
| `--sample-rate` `SAMPLE_RATE` | output sample rate (default preserve input) (one of `44100`, `48000`) |
| `--channels` `CHANNELS` | output channels (default preserve input) (one of `1`, `2`) |
| `--stream` `STREAM` | source audio stream index when the file contains several tracks |
| `--gain-db` `GAIN_DB` | explicit PCM gain in decibels (default 0) |
| `--start-ms` `START_MS` | crop start on the decoded timeline (default 0) |
| `--duration-ms` `DURATION_MS` | crop duration; omitted keeps the remainder |
| `--fade-in-ms` `FADE_IN_MS` | linear fade-in duration (default 0) |
| `--fade-out-ms` `FADE_OUT_MS` | linear fade-out duration (default 0) |
| `--quality` `QUALITY` | HCA encoder quality preset (default high) (one of `highest`, `high`, `middle`, `low`, `lowest`) |

## nnnotes pack master

master data tables (.json) -> <Table>.bin (Rijndael-256-CBC over gzip JSON)

| Argument | Description |
| --- | --- |
| `inputs` ... | directories (their *.json files) or files |
| `-o`, `--out` `OUT` | output directory (required) |
| `--prefix-from` `BIN` | copy the 64-byte prefix of this master data file (the loader ignores it; a replacement should keep it) |
| `--manifest` `VERSION` | also write MasterManifest.json (the file `master download` reads) for this master data version |

## nnnotes pack bundle

UnityFS bundles -> the form the CDN serves (AES-128-CTR, first 16 KiB)

| Argument | Description |
| --- | --- |
| `inputs` ... | directories (their bundles) or files |
| `-o`, `--out` `OUT` | output directory (required) |
| `--suffix` `SUFFIX` | the file name suffix bundles have among the inputs (default .bundle) |
| `--name` `NAME` | the file name the bundle is served under (it seeds the nonce; default: the input's name) |

## nnnotes pack asset

replace a binary TextAsset in a bundle; --new-name changes its object name

| Argument | Description |
| --- | --- |
| `bundle` | the bundle to copy |
| `-o`, `--out` `OUT` | output bundle (required) |
| `--name` `NAME` | the TextAsset's name inside the bundle (required) |
| `--data` `DATA` | the file whose bytes become the asset's payload (required) |
| `--new-name` `NEW_NAME` | also rename the object; its container path is preserved |

## nnnotes pack build

build a song package or a UnityFS bundle from a JSON manifest

| Argument | Description |
| --- | --- |
| `file` | song project or asset manifest with template, mode and asset operations |
| `-o`, `--out` `OUT` | new song package directory, or output UnityFS bundle (required) |
| `--encrypt` `FILE` | also write the encrypted bundle to this file |
| `--name` `NAME` | served file name used for encryption and catalog validation |
| `--catalog-add` `JSON` | also write a manifest accepted by catalog rebuild --add |
| `--catalog-out` `FILE` | rebuild the --catalog source with the bundle's locations |

## nnnotes export assets

every object of the selected bundles -> common formats (the depot and its layouts)
Expand Down
70 changes: 70 additions & 0 deletions docs/commands.md
Original file line number Diff line number Diff line change
Expand Up @@ -106,6 +106,20 @@ Serves a local web page on `HOST:PORT` (default `127.0.0.1:8000`) that lists eve
languages, and per catalog the asset paths as directories. A link under an asset path downloads the bundle holding
it, decrypted; `bundles/<bundle key>` lists every bundle directly.

## catalog rebuild

```
nnnotes catalog rebuild CATALOG -o OUT [--add FILE ...] [--drop KEY ...]
[--bundle FILE ...] [--hash HEX]
```

Rebuilds an Addressables binary catalog, preserving headers, typed keys and location dependencies. `--add` accepts
location objects or bundle manifests (`file`, `internal_id`, `bundle_name`, `assets`), computing content CRC, size
and a deterministic cache hash for each plain UnityFS file. Asset locations depend on bundles by key. `--drop`
removes locations and supports replacements under the same key. `--bundle` refreshes an existing bundle entry
with its decompressed-content CRC, size and MD5 cache version; `--hash` overrides one bundle cache hash.
See [packing.md](packing.md) for the input contract. Reports `{in, out, added, dropped, updated, addedBundles, bytes}`.

## bundle pull

```
Expand Down Expand Up @@ -163,6 +177,57 @@ against the manifest's SHA-256. Files already present with the right hash are ke
the exit
status is 1 when a file failed or, with `--latest`, the version could not be fetched.

## pack master

```
nnnotes pack master INPUT [INPUT ...] -o OUT [--prefix-from BIN] [--manifest VERSION]
```

The inverse of `master decode`: each `.json` file, or the `*.json` files of a directory, is written as
`OUT/<file stem>.bin` — gzip-compressed, then Rijndael-256 CBC with PKCS7 padding, behind a 64-byte prefix. The key
and IV are `[master] key` and `[master] iv`, so a client configured with the same pair reads the result like any
other master data file. `--prefix-from` copies the prefix of an existing master data file: the loader ignores it, so a
file that replaces another should carry that file's prefix; without it the prefix is 64 zero bytes. `--manifest`
also writes `OUT/MasterManifest.json` for that master data version — the file `master download` reads, listing each
packed file's name, SHA-256 and size — which is what a CDN tree of our own needs. The report is
`{out, packed, files, manifest}`; the exit status is 1 when a file could not be packed.

## pack bundle

```
nnnotes pack bundle INPUT [INPUT ...] -o OUT [--suffix .bundle] [--name NAME]
```

The inverse of the decryption `bundle pull` applies: each UnityFS bundle is written as the CDN serves it — AES-128 in
CTR mode over the first 16 KiB, the nonce being the first 8 bytes of SHA-256(`[bundle] nonce_seed` + the file name).
`--name` is the file name the bundle is served under, which seeds the nonce; it applies to a single input (default:
the input's own name). `--suffix` (default `.bundle`) is the suffix bundles have among the inputs of a directory. The
report is `{out, packed, files}`; the exit status is 1 when a file is not a UnityFS bundle or could not be packed.

## pack asset

```
nnnotes pack asset BUNDLE -o OUT --name NAME --data FILE [--new-name NAME]
```

Replaces a binary TextAsset while preserving its path ID and container path, then validates and saves the
UnityFS bundle with its original flags. `--new-name` changes the object name. Reports
`{in, out, asset, payloadBytes, bundleBytes, crc}`.

## pack build

```
nnnotes pack build MANIFEST -o PLAIN.bundle [--encrypt ENCRYPTED.bundle] [--name NAME]
[--catalog SOURCE --catalog-out CATALOG] [--catalog-add JSON]
```

Builds or edits a UnityFS bundle from a JSON manifest. New bundles use template schemas and flags with supplied
TextAsset, PNG texture, PCM WAV audio, serialized or raw object inputs. Operations add, replace and rename objects;
container paths, unique path IDs and preload references are managed by the builder. Optional outputs include an
encrypted bundle, a rebuilt catalog and a reusable catalog-add manifest. All outputs are prepared before writing.
See [packing.md](packing.md) for examples, schema selection, input limits and encryption. Reports
`{out, bundle_name, bundle_provider, assets, bundleBytes, crc, hash, encrypted, catalog, catalog_add}`.

## export episode

```
Expand Down Expand Up @@ -542,3 +607,8 @@ unplayable, full, bgm, jackets, bytes, fileBytes, sha256}`. A missing or unreada
column, a text id, a chart asset, a cue sheet or cue, a jacket), a chart the deck model cannot measure, or deck
statistics that disagree with the chart facts stop the command with exit status 1 before the file is written. The
format is described in [music-data.md](music-data.md).

## Song and audio packages

`pack plan`, `pack build`, `pack verify` and `pack audio` work with local song projects and audio.
See [Package a song](song-packing.md) for input manifests, tool requirements, validation and output layouts.
2 changes: 2 additions & 0 deletions docs/configuration.md
Original file line number Diff line number Diff line change
Expand Up @@ -105,6 +105,8 @@ exist when they are given.
| `[paths] master` | `NNNOTES_PATHS_MASTER` | `--master` | decoded master data directory, one `<Table>.json` per table; the flag overrides `[servers.<region>] master` |
| `[paths] apk` | `NNNOTES_PATHS_APK` | `--apk` | the game's `base.apk` (the story export reads the CRI Lips library from it, or from `split_config.arm64_v8a.apk` next to it; the asset export its built-in Unity files) |
| `[paths] ffmpeg` | `NNNOTES_PATHS_FFMPEG` | `--ffmpeg` | `ffmpeg` executable; unset: `ffmpeg` on `PATH` |
| `[paths] ffprobe` | `NNNOTES_PATHS_FFPROBE` | `--ffprobe` | `ffprobe` executable for audio input inspection |
| `[paths] vgaudio` | `NNNOTES_PATHS_VGAUDIO` | `--vgaudio` | VGAudioCli executable or .NET DLL for HCA encoding; unset: `VGAudioCli` on `PATH` |
| `[paths] vgmstream` | `NNNOTES_PATHS_VGMSTREAM` | `--vgmstream` | `vgmstream-cli` executable; unset: `vgmstream-cli` on `PATH` |
| `[paths] node` | `NNNOTES_PATHS_NODE` | `--node` | Node.js executable; unset: `node` on `PATH` |
| `[paths] player` | `NNNOTES_PATHS_PLAYER` | `--player` (of `site build`) | ournotes-player checkout (built) or installed package |
Expand Down
Loading
Loading