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.
Files changed (79) hide show
  1. nnnotes/__init__.py +7 -0
  2. nnnotes/__main__.py +4 -0
  3. nnnotes/acb.py +190 -0
  4. nnnotes/addressables.py +413 -0
  5. nnnotes/adv.py +243 -0
  6. nnnotes/advcommand.py +29 -0
  7. nnnotes/advmedia.py +178 -0
  8. nnnotes/advscene.py +113 -0
  9. nnnotes/advui.py +1154 -0
  10. nnnotes/advvideo.py +684 -0
  11. nnnotes/atoms/__init__.py +151 -0
  12. nnnotes/atoms/astc.py +52 -0
  13. nnnotes/atoms/gltf.py +290 -0
  14. nnnotes/atoms/mesh.py +289 -0
  15. nnnotes/atoms/png.py +28 -0
  16. nnnotes/atoms/reader.py +269 -0
  17. nnnotes/atoms/sniff.py +198 -0
  18. nnnotes/atoms/sprite.py +236 -0
  19. nnnotes/atoms/texture.py +80 -0
  20. nnnotes/cache.py +289 -0
  21. nnnotes/catalog.py +373 -0
  22. nnnotes/catalogdb.py +457 -0
  23. nnnotes/census.py +276 -0
  24. nnnotes/cli.py +744 -0
  25. nnnotes/cli_assets.py +1539 -0
  26. nnnotes/config.py +238 -0
  27. nnnotes/contract.py +497 -0
  28. nnnotes/cri.py +371 -0
  29. nnnotes/crikey.py +71 -0
  30. nnnotes/crilips.py +265 -0
  31. nnnotes/cristages.py +558 -0
  32. nnnotes/deckdata.py +462 -0
  33. nnnotes/export.py +1462 -0
  34. nnnotes/gameapi.py +318 -0
  35. nnnotes/jsonio.py +98 -0
  36. nnnotes/languages.py +44 -0
  37. nnnotes/layout.py +596 -0
  38. nnnotes/link.py +418 -0
  39. nnnotes/live.py +115 -0
  40. nnnotes/live2d.py +235 -0
  41. nnnotes/liveaudio.py +331 -0
  42. nnnotes/livenotes.py +291 -0
  43. nnnotes/liveoptions.py +250 -0
  44. nnnotes/livescene.py +450 -0
  45. nnnotes/liveui.py +328 -0
  46. nnnotes/master.py +251 -0
  47. nnnotes/modelstages.py +424 -0
  48. nnnotes/motion.py +131 -0
  49. nnnotes/objexport.py +1564 -0
  50. nnnotes/orchestrate.py +630 -0
  51. nnnotes/plan.py +218 -0
  52. nnnotes/player.py +392 -0
  53. nnnotes/room.py +391 -0
  54. nnnotes/score.py +1139 -0
  55. nnnotes/shader.py +217 -0
  56. nnnotes/spot.py +201 -0
  57. nnnotes/stages.py +285 -0
  58. nnnotes/store.py +518 -0
  59. nnnotes/story.py +142 -0
  60. nnnotes/storyfonts.py +1258 -0
  61. nnnotes/storyhost.py +1190 -0
  62. nnnotes/storysite.py +662 -0
  63. nnnotes/storyvoices.py +102 -0
  64. nnnotes/textstyle.py +355 -0
  65. nnnotes/tmpfont.py +636 -0
  66. nnnotes/typetrees/6000.3.12f1.json +19 -0
  67. nnnotes/unity.py +368 -0
  68. nnnotes/viewrules.json +441 -0
  69. nnnotes/views.py +966 -0
  70. nnnotes/voices.py +1363 -0
  71. nnnotes/web.py +1624 -0
  72. nnnotes/webaudio.py +14 -0
  73. nnnotes/webmodel.py +397 -0
  74. nnnotes-0.1.0.dist-info/METADATA +200 -0
  75. nnnotes-0.1.0.dist-info/RECORD +79 -0
  76. nnnotes-0.1.0.dist-info/WHEEL +5 -0
  77. nnnotes-0.1.0.dist-info/entry_points.txt +2 -0
  78. nnnotes-0.1.0.dist-info/licenses/LICENSE +21 -0
  79. 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