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
5 changes: 4 additions & 1 deletion docs/cli.md
Original file line number Diff line number Diff line change
Expand Up @@ -195,11 +195,14 @@ an ADV episode -> JSON

## nnnotes export story

an ADV episode -> the story product and the products it plays with (scene, assets, Live2D models, cue sheets, videos, chat, CRI Lips)
an ADV episode -> the story product and the products it plays with (scene, assets, Live2D models, cue sheets, videos, chat, UI, fonts, CRI Lips)

| Argument | Description |
| --- | --- |
| `adv_id` | MasterAdv id of the episode |
| `--story-languages` `LANG,...` | text languages, comma-separated (default: [catalog] language) |
| `--fonts` `FONTS` | glyph source: open font files (default) or game TMP assets (one of `open`, `game`) |
| `--font` `LANG=PATH` | font file for a language, or emoji=PATH (repeatable; [paths] fonts.<lang>) |
| `--format` `FORMAT` | web audio format of the cue sheets (default aac) (one of `aac`, `flac`, `mp3`, `opus`, `vorbis`) |
| `--flac-level` `N` | FLAC compression level 0-12 of the decoded sounds (default 8; every level decodes to the same samples) |
| `-o`, `--out` `OUT` | output directory (required) |
Expand Down
11 changes: 11 additions & 0 deletions docs/commands.md
Original file line number Diff line number Diff line change
Expand Up @@ -176,6 +176,7 @@ row of a Movie / Clip row's video id.

```
nnnotes export story ADV_ID -o OUT [--format FORMAT] [--flac-level N] [--stored-catalog LABEL|SHA] [--workers N]
[--story-languages LANG,...] [--fonts open|game] [--font LANG=PATH]
[--link auto|clone|hard|copy] [--explain] [--dry-run]
```

Expand All @@ -190,6 +191,8 @@ OUT/models/<id>.json each Live2D model of the episode (`live2d.model`)
OUT/cuesheets/<sheet>.json each cue sheet of the episode, decoded (`story.sound`)
OUT/videos/<name>.json each video of the episode, as WebM (`story.video`; <name>: its asset, "/" as "~")
OUT/storychat/default.json the shared chat rows, when the episode shows chat (`story.chat`)
OUT/storyui/<lang>.json the story UI in each requested language (`story.ui`)
OUT/storyfonts/<id>_<lang>.json the episode's font records and glyph pages (`story.fonts`)
OUT/crilips/default.json the CRI Lips analysis data, when a voice can reach it (`story.crilips`)
OUT/assets/<sha256>.<ext> the files the manifests name
```
Expand All @@ -205,6 +208,14 @@ has an alpha stream.
A product several stories play with (the scene, a stage, a model, a cue sheet) is one product: stories exported into
one directory share its manifest and files, and the depot keeps every result for the next export.

`--story-languages` selects text languages, separated by commas; the default is `[catalog] language`.
`--fonts open` (the default) generates glyph pages from the font files in `[paths.fonts]` or the repeatable
`--font LANG=PATH` flags. Supply each requested language's font and any fonts needed by its fallback chain.
`--font emoji=PATH` supplies a bitmap emoji font. Without it, open emoji sprites retain their layout with empty
images. `--fonts game` uses the client's TMP glyphs and generates the runtime glyphs its font assets require.
Both sources use font settings from the client's assets and require the `fonts` extra. Font files are explicit
content-addressed inputs, so changing their bytes rebuilds the corresponding font product.

`--format` (default `aac`) is the web audio format of the cue sheets: `aac` (AAC-LC in MP4, `cues.json` records each
file's encoder delay), `opus`, `vorbis`, `mp3`, or `flac` (the decoded FLAC as it is); `--flac-level` is the FLAC
compression level of the decoded sounds (as for `export audio`). `crilips/` holds the weights of the game's CRI Lips
Expand Down
4 changes: 4 additions & 0 deletions docs/contracts.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,10 @@ Every document names its format in a `schema` field (`nnnotes.result/1`). Within
added, never renamed, removed or given another meaning; readers ignore fields they do not know. A breaking change
is a new major version (`nnnotes.result/2`).

Episode font data uses the `format` field `nnnotes.story-fonts/1`, described in
[story-fonts.schema.json](https://github.com/MetaSekaiLab/nnnotes/blob/main/src/nnnotes/schemas/story-fonts.schema.json)
and [story products](products.md#stories).

Stages carry their own version. A change of what a stage writes for the same inputs is a new stage version, which
changes the key of every task of that stage (golden tests fail when a stage's output changes while its version
stays).
Expand Down
7 changes: 6 additions & 1 deletion docs/export.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ Every command below prints a short summary; add `--json` for its report as JSON.

```sh
nnnotes export episode 10462 -o out/episode_10462.json # the episode's commands, lines and resources
nnnotes export story 10462 -o out/stories/10462 # everything the episode plays
nnnotes export story 10462 --fonts game -o out/stories/10462 # the episode with the client's TMP fonts
```

`export story` writes the story product of an episode and the products it plays with, as a product layout below `-o`:
Expand All @@ -21,6 +21,11 @@ other asset in `storyassets/`, each Live2D model in `models/`, each cue sheet in
`videos/` and, when the episode uses them, the shared chat rows in `storychat/default.json` and the CRI Lips data in `crilips/default.json`. The files
themselves are under `assets/`.

`storyui/<language>.json` and `storyfonts/<id>_<language>.json` provide the UI, text bindings and glyph pages for
`[catalog] language`. `--story-languages ja,en` selects several languages. `--fonts open` (the default) uses font
files from `[paths.fonts]` or repeatable `--font LANG=PATH` flags; supply any languages needed by the fallback chain.
Both font sources require the `fonts` extra.

Products that several stories share (the scene, assets, models and cue sheets) are placed once, so exporting several
stories into one directory keeps one copy of each. Every product goes through the depot, so a second export reuses
its results.
Expand Down
23 changes: 22 additions & 1 deletion docs/products.md
Original file line number Diff line number Diff line change
Expand Up @@ -372,6 +372,7 @@ A story episode plays with products that several episodes share, made by `nnnote
| `story.video` | a video key `Cri/Video/<asset>` (the video of a Movie / Clip command) | `videos/<name>.json`, `<name>` the asset with `/` as `~` | the key | `video.json` and `movie.webm` |
| `story.chat` | `default` | `storychat/default.json` | the shared chat shards `Adv/Chat/AdvChat-Text`, `Adv/Chat/AdvChat-Sound` and `Adv/Chat/AdvChat-SoundCueSheet` | `chat.json` |
| `story.ui` | a client language (`ja`, `en`, `zh-Hant`, `zh-Hans`, `ko`) | `storyui/<language>.json` | the story UI's catalog keys (the widget, talk windows, chat widget, dialogs, UIManager, letterbox, animations and settings), the built-in `GraphicsSettings` and `LocalizeManager`, and the resources `DOTweenSettings` and `TMP Settings` | `ui.json` and the object table |
| `story.fonts` | `<episode id>/<language>` | `storyfonts/<id>_<language>.json` | the UI and font keys, the episode's chat windows and frames, and the UI settings | `fonts.json`, glyph pages and text shaders |
| `story` | a `MasterAdv` id | `stories/<id>.json` | the episode key `Adv/Episode/<asset>/<asset>` (`<asset>`: the row's `_advEpisodeAsset`), its shard keys (the key with `-Text`, `-Sound`, `-SoundCueSheet`, `-Video`) and `EmbCommon/Adv/Settings/AdvPlayerSettings` | `episode.json` and `story.json` |

`story.sound`, `story.crilips`, `story.video`, `story.chat` and `story` keep no objects: their files are all they
Expand Down Expand Up @@ -440,6 +441,24 @@ The retained object table is the UI closure, so the font assets' files remain in
`font-bundles` names the bundles holding the font assets, and `apk:AndroidManifest.xml` supplies the client version.
A client requests the product for each language it uses.

`story.fonts` contains the font records for one episode and language. Its `episode` input is the first run's
`episode.json`, supplied in the planning fact `product_inputs` under `episode:<id>`. This snapshot determines the
chat-window and frame roots before object export. Its other inputs are localized `MasterText` rows, the font
bundle record, APK version and object table. With parameter `fonts: open`, configured font files enter as
`font:<language>` and optionally `font:emoji`; `fonts: game` reads the client's TMP assets.

`fonts.json` has format `nnnotes.story-fonts/1`, `language`, `source`, `fonts`, `textures`, `materials`,
`materialKeywords`, `coverage`, `lineBreaking` and `shaders` (shader names to retained object ids).
`texts` maps TMP component ids to `style`, `localized` (the LocalizeText result, or null), and `font` (the effective
font asset, material and line spacing to render, including generated open font names). Ruby and Korean-adjustment
settings accompany the relevant text records. `sprites` maps driven TMP component ids to their sprite asset and
tint flag; `spriteAssets` and `emojiSpriteAsset` are present when text can draw emoji sprites.

Glyph selection includes episode lines and names, the title, localized UI labels, status text, chat runtime
strings and frame text. It follows each primary font's fallback chain and models synthesized control characters,
underline and missing glyphs. The product retains glyph pages and the required shader programs; font files and
source atlas objects remain in the depot. Page file names in `textures` resolve through the product's `files`.

`story` is one episode. Its input `master` is the episode's master record: its `MasterAdv` row, the `MasterText` row
of its title (none when `_titleTextId` is empty) and every `MasterAdvChat` row. `episode.json` is the episode as
`export episode` writes it, each resource given by its kind and address: the command list with the lines of every
Expand All @@ -459,5 +478,7 @@ A model's MotionSync (model.json `motionSync`) decides whether a lip-synced voic
`crilips` names the data whenever a voice can reach it. The episode's talk windows are listed in `episode.json`
`resources`. `parts` depend on the episode's commands, which only the story's result holds: a command
asks for the `story` product first, reads its `story.json`, then asks for the story with every product `parts` names
(`nnnotes.pipeline.story.part_products`); `export story` works this way. The `product` layout makes the story of
(`nnnotes.pipeline.story.part_products`, with the requested languages); `export story` works this way.
`parts.ui` and `parts.fonts` map every client language to its product manifest. The command exports those entries
for the requested languages. The `product` layout makes the story of
every episode whose key the selection holds.
7 changes: 5 additions & 2 deletions docs/quickstart.md
Original file line number Diff line number Diff line change
Expand Up @@ -62,14 +62,17 @@ the decoded directory the default of every command that reads master data.

```sh
nnnotes export episode 10462 -o out/episode_10462.json
nnnotes export story 10462 -o out/story_10462
nnnotes export story 10462 --fonts game -o out/story_10462
```

`export episode` writes the episode's command list with its lines in five languages and the index of its voices,
sounds and videos. `export story` writes the story product of the episode and the products it plays with below
`out/story_10462`: the story index, the stage scene, the assets, the Live2D models, the cue sheets decoded to audio
(AAC by default, `--format` chooses another web format), the videos as WebM, and the shared chat rows and CRI Lips
data when the episode uses them. The files are under `assets/`. Add `--json` to any command to get its report as JSON.
data when the episode uses them. It also exports the UI and glyph pages in `[catalog] language`; select several
with `--story-languages ja,en`. The example uses the client's TMP fonts. For open fonts, use
`--fonts open --font ja=/path/to/font.otf` (or configure `[paths.fonts]`). Both modes require the `fonts` extra.
The files are under `assets/`. Add `--json` to any command to get its report as JSON.

## Next

Expand Down
3 changes: 2 additions & 1 deletion docs/schemas.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

The documents nnnotes writes in a fixed format have JSON Schemas (draft 2020-12), shipped in the package
`nnnotes.schemas`. Each schema has the `$id` `urn:nnnotes:schema:<name>`, and the shared definitions are in
`common`. Every document names its format and version in a `schema` field, such as `nnnotes.plan/1`; within a major
`common`. Documents name their format and version in `schema` (such as `nnnotes.plan/1`) or `format`; within a major
version fields are only added ([versioning](contracts.md#versioning)).

```python
Expand Down Expand Up @@ -32,6 +32,7 @@ schemas.load("plan") # the schema as a dict
| `result` | `nnnotes.result/1` | the result of a task, a function of its key alone |
| `run` | `nnnotes.run/1` | a run manifest: context, selection, tasks with keys and statuses, layouts |
| `scripts` | `nnnotes.scripts/1` | every MonoScript of the censuses, by object id |
| `story-fonts` | `nnnotes.story-fonts/1` (`format`) | an episode's text bindings, font records, glyph pages and text shaders |
| `task` | `nnnotes.task/1` | one stage applied to one subject, with everything needed to run it (`depot run`) |
| `view` | `nnnotes.view/1` | a semantic view of a master table, each row with its names and the objects it names |

Expand Down
1 change: 1 addition & 0 deletions docs/stages.md
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@ codec and channel of a USM) and `catalog.index` (version 2: gzip input and Japan
| `story.video` | a video key `Cri/Video/<asset>` | `objects`: the object table of the key; `usm:<key>` and `movies`: the key's USM and its `cri.movie` files with their facts ([products](products.md#stories)); the `cri.movie` task takes `format` `webm` | `video.json` and `movie.webm` |
| `story.chat` | `default` | `objects`: the object table of the shared chat shards | `chat.json` |
| `story.ui` | a client language (`ja`, `en`, `zh-Hant`, `zh-Hans`, `ko`) | `objects`: UI, font and chat-window objects; `ui.objects`: the retained UI closure; `master`: localized `MasterText` rows; `font-bundles`; `apk:AndroidManifest.xml` ([products](products.md#stories)) | `ui.json` (text styles and chat status texts) and the UI object table |
| `story.fonts` | `<episode id>/<language>` | `objects`; `episode`: the parsed episode snapshot; `master`: localized texts; `font-bundles`; `apk:AndroidManifest.xml`; `font:<language>` and `font:emoji` for parameter `fonts: open` ([products](products.md#stories)) | `fonts.json`, glyph pages and the text shader objects; `fonts: game` uses TMP assets |
| `story` | a `MasterAdv` id | `objects`: the object table of the episode's command list, its shards and the ADV player settings ([products](products.md#stories)); `master`: the episode's master record | `episode.json` and `story.json` |
| `link.addresses` | global | catalog index, censuses (the built-in files' too) | key -> objects (a scene key: the objects of the scene's file outside a hierarchy), Resources.Load path -> objects, built-in selector (class, MonoBehaviour script class) -> objects, serialized file -> bundle or built-in file, bundle -> keys |
| `view.<name>` | a catalog (`main`) | `master:<Table>` per master table it reads, `rules` (the view's rule and the resolvers it uses), `addresses` (the part of the address table it read: the keys looked up and the prefixes listed); no parameters, atoms or context | `view.<name>:<subject>#view` (facts: rows, entries, gaps, unreferenced) |
Expand Down
19 changes: 17 additions & 2 deletions src/nnnotes/cli/assets.py
Original file line number Diff line number Diff line change
Expand Up @@ -134,6 +134,7 @@ def matches(self, stage: str) -> bool:
StageSource("story.video", "nnnotes.pipeline.story:VideoStage", output=True, product=True),
StageSource("story.chat", "nnnotes.pipeline.story:ChatStage", output=True, product=True),
StageSource("story.ui", "nnnotes.pipeline.story:UiStage", output=True, product=True),
StageSource("story.fonts", "nnnotes.pipeline.story:FontsStage", output=True, product=True),
StageSource("story", "nnnotes.pipeline.story:StoryStage", output=True, product=True),
StageSource("view.", "nnnotes.pipeline.views:stages", output=True),
StageSource("view.voices", "nnnotes.pipeline.voices:VoiceStage", output=True),
Expand Down Expand Up @@ -508,6 +509,7 @@ def __init__(self, args, cfg, log=_log, layouts=()):
self.args, self.cfg, self.log = args, cfg, log
self.layouts = list(layouts)
self.products: dict | None = None # {product stage: subjects} a command asks for (request)
self.product_inputs: dict[str, Input] = {}
self.root = Session(cfg).depot_root()
self.apk = cfg.path("paths", "apk")
self.fetcher = CatalogFetcher(self.root, cfg)
Expand Down Expand Up @@ -657,13 +659,15 @@ def views(self) -> list[str]:
raise self.cfg.missing(*self.cfg.master(self.cfg.get("catalog", "region")))
return want

def request(self, products: dict, selection: list[str]) -> None:
def request(self, products: dict, selection: list[str], inputs: dict[str, Input] | None = None) -> None:
"""Ask for the products `products` {product stage: subjects} alone, with the selection `selection` (what
their census needs) instead of the command's: only what the products reach is exported."""
their census needs) instead of the command's: only what the products reach is exported. `inputs` supplies
immutable content inputs from a preceding run as the planning fact `product_inputs`."""
unknown = sorted(n for n in products if n not in self.installed.stages)
if unknown:
raise ValueError(f"no product stage {', '.join(unknown)} is installed")
self.products, self.selection = {k: sorted(v) for k, v in products.items()}, list(selection)
self.product_inputs = dict(inputs or {})

def _wanted(self, name: str) -> bool:
if not self.installed.sources[name].product:
Expand Down Expand Up @@ -695,6 +699,10 @@ def params(self, pipeline: list[str]) -> dict:
if fmt is not None:
for n in [n for n in pipeline if "format" in stages[n].PARAMS]:
out.setdefault(n, {})["format"] = fmt
source = getattr(self.args, "fonts", None)
if source is not None:
for n in [n for n in pipeline if "fonts" in stages[n].PARAMS]:
out.setdefault(n, {})["fonts"] = source
level = getattr(self.args, "flac_level", None)
if level is not None:
if not 0 <= level <= 12:
Expand Down Expand Up @@ -761,7 +769,14 @@ def facts(self, pipeline: list[str], fetch: bool, background: bool = False) -> d
"bundles": inputs,
"classes": self.classes(),
"raw": Inputs({}),
"product_inputs": self.product_inputs,
}
if "story.fonts" in products:
from ..source.languages import LANGUAGES

font_paths = {code: self.cfg.path("paths.fonts", code) for code in (*LANGUAGES, "emoji")}
font_paths.update({code: Path(path) for code, path in (getattr(self.args, "font", None) or [])})
facts["font_files"] = {code: path for code, path in font_paths.items() if path is not None}
if products:
from ..pipeline import assemble

Expand Down
Loading
Loading