nnnotes 0.1.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- nnnotes/__init__.py +7 -0
- nnnotes/__main__.py +4 -0
- nnnotes/acb.py +190 -0
- nnnotes/addressables.py +413 -0
- nnnotes/adv.py +243 -0
- nnnotes/advcommand.py +29 -0
- nnnotes/advmedia.py +178 -0
- nnnotes/advscene.py +113 -0
- nnnotes/advui.py +1154 -0
- nnnotes/advvideo.py +684 -0
- nnnotes/atoms/__init__.py +151 -0
- nnnotes/atoms/astc.py +52 -0
- nnnotes/atoms/gltf.py +290 -0
- nnnotes/atoms/mesh.py +289 -0
- nnnotes/atoms/png.py +28 -0
- nnnotes/atoms/reader.py +269 -0
- nnnotes/atoms/sniff.py +198 -0
- nnnotes/atoms/sprite.py +236 -0
- nnnotes/atoms/texture.py +80 -0
- nnnotes/cache.py +289 -0
- nnnotes/catalog.py +373 -0
- nnnotes/catalogdb.py +457 -0
- nnnotes/census.py +276 -0
- nnnotes/cli.py +744 -0
- nnnotes/cli_assets.py +1539 -0
- nnnotes/config.py +238 -0
- nnnotes/contract.py +497 -0
- nnnotes/cri.py +371 -0
- nnnotes/crikey.py +71 -0
- nnnotes/crilips.py +265 -0
- nnnotes/cristages.py +558 -0
- nnnotes/deckdata.py +462 -0
- nnnotes/export.py +1462 -0
- nnnotes/gameapi.py +318 -0
- nnnotes/jsonio.py +98 -0
- nnnotes/languages.py +44 -0
- nnnotes/layout.py +596 -0
- nnnotes/link.py +418 -0
- nnnotes/live.py +115 -0
- nnnotes/live2d.py +235 -0
- nnnotes/liveaudio.py +331 -0
- nnnotes/livenotes.py +291 -0
- nnnotes/liveoptions.py +250 -0
- nnnotes/livescene.py +450 -0
- nnnotes/liveui.py +328 -0
- nnnotes/master.py +251 -0
- nnnotes/modelstages.py +424 -0
- nnnotes/motion.py +131 -0
- nnnotes/objexport.py +1564 -0
- nnnotes/orchestrate.py +630 -0
- nnnotes/plan.py +218 -0
- nnnotes/player.py +392 -0
- nnnotes/room.py +391 -0
- nnnotes/score.py +1139 -0
- nnnotes/shader.py +217 -0
- nnnotes/spot.py +201 -0
- nnnotes/stages.py +285 -0
- nnnotes/store.py +518 -0
- nnnotes/story.py +142 -0
- nnnotes/storyfonts.py +1258 -0
- nnnotes/storyhost.py +1190 -0
- nnnotes/storysite.py +662 -0
- nnnotes/storyvoices.py +102 -0
- nnnotes/textstyle.py +355 -0
- nnnotes/tmpfont.py +636 -0
- nnnotes/typetrees/6000.3.12f1.json +19 -0
- nnnotes/unity.py +368 -0
- nnnotes/viewrules.json +441 -0
- nnnotes/views.py +966 -0
- nnnotes/voices.py +1363 -0
- nnnotes/web.py +1624 -0
- nnnotes/webaudio.py +14 -0
- nnnotes/webmodel.py +397 -0
- nnnotes-0.1.0.dist-info/METADATA +200 -0
- nnnotes-0.1.0.dist-info/RECORD +79 -0
- nnnotes-0.1.0.dist-info/WHEEL +5 -0
- nnnotes-0.1.0.dist-info/entry_points.txt +2 -0
- nnnotes-0.1.0.dist-info/licenses/LICENSE +21 -0
- nnnotes-0.1.0.dist-info/top_level.txt +1 -0
nnnotes/crilips.py
ADDED
|
@@ -0,0 +1,265 @@
|
|
|
1
|
+
"""CRI Lips neural-net weight extraction from the game's ``libcri_lips_unity.so``.
|
|
2
|
+
|
|
3
|
+
The story player drives the mouth of Live2D models that have no MotionSync controller by
|
|
4
|
+
reimplementing CRI's ``CriLipsAtomAnalyzer`` in JavaScript. The network weights are not shipped
|
|
5
|
+
as code by CRI; they live in the read-only data of ``libcri_lips_unity.so``. This module reads
|
|
6
|
+
that library from the user's own APK and writes a documented data file (``crilips.json`` +
|
|
7
|
+
``crilips.bin``) that the player loads at runtime.
|
|
8
|
+
|
|
9
|
+
Nothing here is hard-coded to an address or offset: each supported library version has a table of
|
|
10
|
+
(block name, float count, sha256), and the blocks are located by scanning the ELF read-only data
|
|
11
|
+
for a window whose sha256 matches. Every block and constant is verified. An unrecognised library
|
|
12
|
+
version is a hard error -- the layout is never guessed. Each version also records the analysis
|
|
13
|
+
configuration that library creates its analyzer with (``frontend`` in the descriptor: frame and
|
|
14
|
+
hop, bands, rates, windows, discretizer and mouth-open defaults). Block offsets are in floats.
|
|
15
|
+
|
|
16
|
+
Fixtures for the tests are synthetic ELF files with a synthetic version table (see
|
|
17
|
+
``tests/test_crilips.py`` and ``docs/crilips.md``); no game data is required to test this module.
|
|
18
|
+
"""
|
|
19
|
+
from __future__ import annotations
|
|
20
|
+
|
|
21
|
+
import hashlib
|
|
22
|
+
import struct
|
|
23
|
+
import zipfile
|
|
24
|
+
from dataclasses import dataclass, field
|
|
25
|
+
from pathlib import Path
|
|
26
|
+
|
|
27
|
+
from .jsonio import write_json
|
|
28
|
+
|
|
29
|
+
LIB_ENTRY_SUFFIX = "lib/arm64-v8a/libcri_lips_unity.so"
|
|
30
|
+
# the arm64-v8a split of an installed app (adb pull of its directory), or of an .xapk package
|
|
31
|
+
NATIVE_SPLITS = ("split_config.arm64_v8a.apk", "config.arm64_v8a.apk")
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
class CriLipsError(Exception):
|
|
35
|
+
"""The library is missing, unreadable, an unknown version, or fails verification."""
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
@dataclass(frozen=True)
|
|
39
|
+
class Block:
|
|
40
|
+
"""One contiguous float32 array in the library's read-only data."""
|
|
41
|
+
|
|
42
|
+
name: str
|
|
43
|
+
count: int
|
|
44
|
+
shape: tuple[int, ...]
|
|
45
|
+
kind: str # conv_w | dense_w | bias | bn
|
|
46
|
+
sha256: str # sha256 of the count*4 little-endian bytes (integrity only)
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
@dataclass(frozen=True)
|
|
50
|
+
class Const:
|
|
51
|
+
"""A small constant block (scalar, vector, or int table) in read-only data."""
|
|
52
|
+
|
|
53
|
+
name: str
|
|
54
|
+
dtype: str # f32 | i32
|
|
55
|
+
count: int
|
|
56
|
+
sha256: str
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
@dataclass(frozen=True)
|
|
60
|
+
class Version:
|
|
61
|
+
"""Everything needed to extract and describe one library version."""
|
|
62
|
+
|
|
63
|
+
version: str # CRI Lips version string, e.g. "1.05.00"
|
|
64
|
+
core_version: str
|
|
65
|
+
id_string: bytes # a version string that must be present in the ELF
|
|
66
|
+
blocks: tuple[Block, ...] # emitted in this order into crilips.bin
|
|
67
|
+
consts: tuple[Const, ...]
|
|
68
|
+
frontend: dict = field(default_factory=dict)
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
# --- Supported versions ------------------------------------------------------------------------
|
|
72
|
+
# The sha256 values are one-way integrity checksums of the arrays, not the arrays themselves.
|
|
73
|
+
# They let the scanner locate each block without any hard-coded address and verify it exactly.
|
|
74
|
+
|
|
75
|
+
_V10500 = Version(
|
|
76
|
+
version="1.05.00",
|
|
77
|
+
core_version="1.00.17",
|
|
78
|
+
id_string=b"CRI Lips/Android_ARMv8A Ver.1.05.00",
|
|
79
|
+
blocks=(
|
|
80
|
+
# Net1: internal-class classifier (3x3 conv -> 4 dense -> softmax 24)
|
|
81
|
+
Block("net1.conv.w", 90, (10, 3, 3), "conv_w", "54afa68f6e20c5f7"),
|
|
82
|
+
Block("net1.conv.b", 10, (10,), "bias", "55204e31f7cdc74e"),
|
|
83
|
+
Block("net1.d1.w", 28160, (220, 128), "dense_w", "baa5c59e2e19eff8"),
|
|
84
|
+
Block("net1.d1.b", 128, (128,), "bias", "dff64c73cb632dc0"),
|
|
85
|
+
Block("net1.d2.w", 16384, (128, 128), "dense_w", "acbcbea9f2862ef4"),
|
|
86
|
+
Block("net1.d2.b", 128, (128,), "bias", "0452488773d7430f"),
|
|
87
|
+
Block("net1.d3.w", 16384, (128, 128), "dense_w", "f8f4d715a0c8fb64"),
|
|
88
|
+
Block("net1.d3.b", 128, (128,), "bias", "48d45ebc017dcf43"),
|
|
89
|
+
Block("net1.d4.w", 3072, (128, 24), "dense_w", "d37aa7df8d16bc09"),
|
|
90
|
+
Block("net1.d4.b", 24, (24,), "bias", "2575d6f5b8935dca"),
|
|
91
|
+
# Net2: raw-lip parameters (avg3 -> 3 dense relu -> dense sigmoid)
|
|
92
|
+
Block("net2.d1.w", 768, (24, 32), "dense_w", "0e0ad798e5abf2f7"),
|
|
93
|
+
Block("net2.d1.b", 32, (32,), "bias", "91b5660a8c8ca7ce"),
|
|
94
|
+
Block("net2.d2.w", 1024, (32, 32), "dense_w", "8dc46dc8d0b11298"),
|
|
95
|
+
Block("net2.d2.b", 32, (32,), "bias", "3d9039bcc7a95ec8"),
|
|
96
|
+
Block("net2.d3.w", 1024, (32, 32), "dense_w", "3a9df3da133f42f9"),
|
|
97
|
+
Block("net2.d3.b", 32, (32,), "bias", "b44b9d4e0641ab2c"),
|
|
98
|
+
Block("net2.d4.w", 64, (32, 2), "dense_w", "b53be2dce4472ca2"),
|
|
99
|
+
Block("net2.d4.b", 2, (2,), "bias", "04cc81a98b06d422"),
|
|
100
|
+
# Net3: Japanese AIUEO targets (dense+BN+relu x2 -> dense softmax 5)
|
|
101
|
+
Block("net3.d1.w", 2880, (72, 40), "dense_w", "ae669da348feb392"),
|
|
102
|
+
Block("net3.d1.b", 40, (40,), "bias", "b393978842a0fa3d"),
|
|
103
|
+
Block("net3.bn1.beta", 40, (40,), "bn", "9e96f4a355341c58"),
|
|
104
|
+
Block("net3.bn1.gamma", 40, (40,), "bn", "641153fc4ee3b8a4"),
|
|
105
|
+
Block("net3.bn1.mean", 40, (40,), "bn", "fba8a4e816b569af"),
|
|
106
|
+
Block("net3.bn1.var", 40, (40,), "bn", "42b2019f96dc1f94"),
|
|
107
|
+
Block("net3.d2.w", 800, (40, 20), "dense_w", "94421bacb040d80a"),
|
|
108
|
+
Block("net3.d2.b", 20, (20,), "bias", "5b6fb58e61fa4759"),
|
|
109
|
+
Block("net3.bn2.beta", 20, (20,), "bn", "860bb1709e28e460"),
|
|
110
|
+
Block("net3.bn2.gamma", 20, (20,), "bn", "92caa15aedc1f6f6"),
|
|
111
|
+
Block("net3.bn2.mean", 20, (20,), "bn", "4b7b37807378c692"),
|
|
112
|
+
Block("net3.bn2.var", 20, (20,), "bn", "bb0eaa39b6fd2e1a"),
|
|
113
|
+
Block("net3.d3.w", 100, (20, 5), "dense_w", "10ec3cda53d3c85a"),
|
|
114
|
+
Block("net3.d3.b", 5, (5,), "bias", "de47c9b27eb8d300"),
|
|
115
|
+
),
|
|
116
|
+
consts=(
|
|
117
|
+
Const("silence_coef", "f32", 1, "e401200e58084389"),
|
|
118
|
+
Const("antiflap_defaults", "f32", 4, "c97bf6430bb8708f"), # w_large, w_small, thr_up, thr_lo
|
|
119
|
+
Const("motion_class_table", "i32", 24, "7c5d3820cf97d3dd"),
|
|
120
|
+
Const("discretizer_type_table", "i32", 25, "93c75960789019e9"),
|
|
121
|
+
),
|
|
122
|
+
frontend={
|
|
123
|
+
"frame_ms": 30.0, "hop_ms": 10.0, "mel_bands": 24, "mel_high_hz": 8000.0,
|
|
124
|
+
"resample_hz": 16000, "biquad_lpf_cutoff_hz": 16000.0, "process_len_ms": 10.0,
|
|
125
|
+
"update_rate_hz": 100, "silence_threshold_db": -40.0,
|
|
126
|
+
# analysis configuration of this library version (defaults the analyzer is created with)
|
|
127
|
+
"window_internal_class": "hamming", "window_vowel": "blackman-harris",
|
|
128
|
+
"motion_class_buffer": 3, "labial_hold_sec": 0.0,
|
|
129
|
+
"discretizer": {"suppression": 0.5, "smoothing_sec": 0.1, "hold_sec": 0.0,
|
|
130
|
+
"release_sec": 0.1, "max_window_sec": 1.0,
|
|
131
|
+
"wildcard": [0.2, 0.2, 0.0, 0.0, 0.0, 0.0]},
|
|
132
|
+
"mouth_open": {"vowel_coef": [1.0, 1.0, 1.0, 1.0, 1.0], "anti_shake_sec": 0.03},
|
|
133
|
+
"server_hz": 60.0,
|
|
134
|
+
},
|
|
135
|
+
)
|
|
136
|
+
|
|
137
|
+
# Note: every sha256 above is truncated to 16 hex chars (a one-way integrity checksum, never the
|
|
138
|
+
# array bytes). The scanner compares the same 16-char prefix and re-verifies each located block.
|
|
139
|
+
SUPPORTED: tuple[Version, ...] = (_V10500,)
|
|
140
|
+
|
|
141
|
+
|
|
142
|
+
# --- Minimal ELF reader ------------------------------------------------------------------------
|
|
143
|
+
|
|
144
|
+
def _read_rodata(so: bytes) -> bytes:
|
|
145
|
+
"""Return the bytes of the ``.rodata`` section of an ELF64 little-endian AArch64 object."""
|
|
146
|
+
if so[:4] != b"\x7fELF" or so[4] != 2 or so[5] != 1:
|
|
147
|
+
raise CriLipsError("not an ELF64 little-endian object")
|
|
148
|
+
e_shoff = struct.unpack_from("<Q", so, 0x28)[0]
|
|
149
|
+
e_shentsize, e_shnum, e_shstrndx = struct.unpack_from("<HHH", so, 0x3a)
|
|
150
|
+
shstr_off = struct.unpack_from("<Q", so, e_shoff + e_shstrndx * e_shentsize + 24)[0]
|
|
151
|
+
|
|
152
|
+
def name_at(n: int) -> str:
|
|
153
|
+
end = so.index(b"\x00", shstr_off + n)
|
|
154
|
+
return so[shstr_off + n: end].decode()
|
|
155
|
+
|
|
156
|
+
for i in range(e_shnum):
|
|
157
|
+
base = e_shoff + i * e_shentsize
|
|
158
|
+
n_name = struct.unpack_from("<I", so, base)[0]
|
|
159
|
+
off, size = struct.unpack_from("<QQ", so, base + 24)
|
|
160
|
+
if name_at(n_name) == ".rodata":
|
|
161
|
+
return so[off: off + size]
|
|
162
|
+
raise CriLipsError(".rodata section not found")
|
|
163
|
+
|
|
164
|
+
|
|
165
|
+
def _sha16(b: bytes) -> str:
|
|
166
|
+
return hashlib.sha256(b).hexdigest()[:16]
|
|
167
|
+
|
|
168
|
+
|
|
169
|
+
def _locate_block(rodata: bytes, blk: Block) -> int:
|
|
170
|
+
"""Offset of ``blk`` inside ``rodata`` by scanning 4-aligned windows for its sha256."""
|
|
171
|
+
n = blk.count * 4
|
|
172
|
+
for off in range(0, len(rodata) - n + 1, 4):
|
|
173
|
+
if _sha16(rodata[off: off + n]) == blk.sha256:
|
|
174
|
+
return off
|
|
175
|
+
raise CriLipsError(f"weight block {blk.name} not found (library modified or wrong version)")
|
|
176
|
+
|
|
177
|
+
|
|
178
|
+
def _scan_const(rodata: bytes, c: Const) -> bytes:
|
|
179
|
+
"""Locate a constant by its recorded sha256, scanning 4-aligned windows. No game bytes are
|
|
180
|
+
stored in this module -- only the one-way checksum -- so the located bytes are re-verified."""
|
|
181
|
+
n = c.count * 4
|
|
182
|
+
for off in range(0, len(rodata) - n + 1, 4):
|
|
183
|
+
window = rodata[off: off + n]
|
|
184
|
+
if _sha16(window) == c.sha256:
|
|
185
|
+
return window
|
|
186
|
+
raise CriLipsError(f"constant {c.name} not found (library modified or wrong version)")
|
|
187
|
+
|
|
188
|
+
|
|
189
|
+
# --- Public API --------------------------------------------------------------------------------
|
|
190
|
+
|
|
191
|
+
def _library_in(apk: Path) -> bytes | None:
|
|
192
|
+
try:
|
|
193
|
+
with zipfile.ZipFile(apk) as z:
|
|
194
|
+
for info in z.infolist():
|
|
195
|
+
if info.filename.endswith(LIB_ENTRY_SUFFIX):
|
|
196
|
+
return z.read(info.filename)
|
|
197
|
+
except (OSError, zipfile.BadZipFile) as e:
|
|
198
|
+
raise CriLipsError(f"{apk} is not a readable APK: {e}") from None
|
|
199
|
+
return None
|
|
200
|
+
|
|
201
|
+
|
|
202
|
+
def read_library(apk: Path) -> bytes:
|
|
203
|
+
"""Read ``libcri_lips_unity.so`` from an APK (or a raw .so path). An installed app keeps its native libraries in
|
|
204
|
+
the arm64-v8a split next to ``base.apk`` (NATIVE_SPLITS); that split is read when the APK itself has none."""
|
|
205
|
+
apk = Path(apk)
|
|
206
|
+
if apk.suffix.lower() == ".so":
|
|
207
|
+
return apk.read_bytes()
|
|
208
|
+
so = _library_in(apk)
|
|
209
|
+
for name in NATIVE_SPLITS:
|
|
210
|
+
if so is not None:
|
|
211
|
+
break
|
|
212
|
+
if (apk.parent / name).is_file():
|
|
213
|
+
so = _library_in(apk.parent / name)
|
|
214
|
+
if so is None:
|
|
215
|
+
raise CriLipsError(f"{LIB_ENTRY_SUFFIX} not found in {apk.name} or in an arm64-v8a split next to it "
|
|
216
|
+
f"({', '.join(NATIVE_SPLITS)})")
|
|
217
|
+
return so
|
|
218
|
+
|
|
219
|
+
|
|
220
|
+
def extract(so: bytes, tables: tuple[Version, ...] = SUPPORTED) -> tuple[bytes, dict]:
|
|
221
|
+
"""Return ``(crilips.bin bytes, descriptor dict)`` for the library bytes ``so``.
|
|
222
|
+
|
|
223
|
+
Blocks are located and verified by sha256; constants are located and their bytes captured.
|
|
224
|
+
Raises :class:`CriLipsError` on an unknown version or any verification failure.
|
|
225
|
+
"""
|
|
226
|
+
ver = None
|
|
227
|
+
for v in tables:
|
|
228
|
+
if v.id_string in so:
|
|
229
|
+
ver = v
|
|
230
|
+
break
|
|
231
|
+
if ver is None:
|
|
232
|
+
raise CriLipsError("unknown libcri_lips_unity.so version")
|
|
233
|
+
rodata = _read_rodata(so)
|
|
234
|
+
|
|
235
|
+
blob = bytearray()
|
|
236
|
+
desc = {"version": ver.version, "core_version": ver.core_version, "dtype": "float32",
|
|
237
|
+
"order": "LE", "blocks": [], "constants": {}, "frontend": dict(ver.frontend)}
|
|
238
|
+
for blk in ver.blocks:
|
|
239
|
+
off = _locate_block(rodata, blk)
|
|
240
|
+
raw = rodata[off: off + blk.count * 4]
|
|
241
|
+
desc["blocks"].append({"name": blk.name, "offset": len(blob) // 4, "count": blk.count,
|
|
242
|
+
"shape": list(blk.shape), "kind": blk.kind, "sha256": _sha16(raw)})
|
|
243
|
+
blob += raw
|
|
244
|
+
# Constants: located and verified by their one-way checksum only.
|
|
245
|
+
for c in ver.consts:
|
|
246
|
+
raw = _scan_const(rodata, c)
|
|
247
|
+
if c.dtype == "f32":
|
|
248
|
+
vals = list(struct.unpack_from("<%df" % c.count, raw, 0))
|
|
249
|
+
else:
|
|
250
|
+
vals = list(struct.unpack_from("<%di" % c.count, raw, 0))
|
|
251
|
+
desc["constants"][c.name] = vals[0] if c.count == 1 else vals
|
|
252
|
+
desc["total_floats"] = sum(b["count"] for b in desc["blocks"])
|
|
253
|
+
desc["blob_sha256"] = _sha16(bytes(blob))
|
|
254
|
+
return bytes(blob), desc
|
|
255
|
+
|
|
256
|
+
|
|
257
|
+
def write_data(apk: Path, out_dir: Path) -> dict:
|
|
258
|
+
"""Extract from an APK and write ``crilips.json`` and ``crilips.bin`` into ``out_dir``."""
|
|
259
|
+
so = read_library(apk)
|
|
260
|
+
blob, desc = extract(so)
|
|
261
|
+
out_dir = Path(out_dir)
|
|
262
|
+
out_dir.mkdir(parents=True, exist_ok=True)
|
|
263
|
+
(out_dir / "crilips.bin").write_bytes(blob)
|
|
264
|
+
write_json(out_dir / "crilips.json", desc)
|
|
265
|
+
return desc
|