Abstract. voice.nofs is the voice database read by Synthesizer V
Studio. This document describes its container, the DNNI record layer nested inside it, the record
tree that layer encodes, the int16 quantisation used for weight tensors, the phoneme dictionaries,
the configuration blob, and the small set of per-voice arrays that decide which singer you hear.
A voicebank is not a model of one singer. It is a shared backbone — byte-identical across banks of the same generation — steered by roughly 1,500 bytes of per-voice data. Of that, three 32-float vectors (396 bytes) carry vocal identity.
Everything here was derived by parsing two shipping banks from first principles and diffing them record by record: GUMI AI v101 (INTERNET Co., Ltd., 40,536,086 B) and Kasane Teto v104 (AHS Co., Ltd., 40,536,421 B). Two files is a narrow base, so any claim that is really a population statistic is marked as such. The manual also incorporates the earliest known description of the format, by ATroubledSnake; section 20 reconciles the two, line by line.
Version 7 is a correction pass over version 6, done by re-deriving every number in the document from the two banks with a parser written independently of the one that produced version 6. Most of version 6 survived intact. Eleven claims did not, and one whole section — the configuration blob — was wrong in structure and is now fully decoded. Section 19 lists every change with the evidence. The figure system was also rebuilt: see section 4 for the naming rules, which existed only implicitly before and were applied inconsistently.
Claims carry a grade. [V] means verified here against both banks, byte by byte. [I] is an inference from structure that is consistent but not proven. [U] is unknown — the field is described but its meaning is not established. [V*] was established by rendering audio in earlier work on this format and has not been re-tested here. [S] comes from ATroubledSnake's original document and could not be checked with the two files available.
Every zoom chain starts from the same picture of the whole file, with the byte range about to be magnified outlined in red and dashed lines carrying it down to the next level. Every level tiles its span exactly — the figure generator refuses to emit a level whose boxes leave a gap — so a run of records that is not drawn individually appears as a dashed box that says how many records and how many bytes it stands for. Offsets are given twice: the grey row is the absolute file offset, the blue row is the offset relative to the enclosing structure.
[S] A voicebank ships as a .svpk file, which is an
ordinary ZIP archive. Inside it are voice.nofs and a handful of assets:
profile.png, background.png, license.txt and
package-config. Everything in this document concerns voice.nofs alone.
The two files studied here were supplied already extracted, so the archive layout is quoted from
the original documentation rather than re-checked.
[S] Not every voice.nofs is a neural voice. The pre-AI
parametric banks use the same magic number and the same 0x1000-byte reserved head, but hold formant
parameter arrays and encoded audio instead of a record stream. The reliable test is to look for a
DNNI marker: if 0x7FCA40FF or 0x7FCA41FF appears, the bank is neural.
This document describes only the neural layout.
Drawn from GUMI AI v101. Kasane Teto v104 differs by 335 bytes in total and the structure is identical; section 19.4 breaks that difference down. Bands are cut on chain-record boundaries, so they tile the file exactly and each printed size is that band's true size.
Figure 1 The whole file. Every band is a whole number of chain records, so the bands tile the file exactly and each printed size is that band's true size. The three metadata bands hold 7,059 bytes between them. Section 11 marks the 1,500 bytes of this picture that belong to the singer.
| content | bytes | share |
|---|---|---|
| int16 weight tensors | 39,204,800 | 96.716% |
| float32 per-row scales | 430,872 | 1.063% |
| other record payloads: float32 arrays, shape words, phoneme tables | 866,401 | 2.137% |
| DNNI record headers (1338 x 20 B) | 26,760 | 0.066% |
| reserved head: fixed fields, hash index, padding | 4,096 | 0.010% |
| gap between data_offset and the chain | 16 | <0.001% |
| property-chain framing (22 entries) | 639 | 0.002% |
| configuration blob | 1,797 | 0.004% |
| opaque trailer blobs (2 x 256 B) | 512 | 0.001% |
| metadata and configuration strings | 161 | <0.001% |
| section container headers (4 x 8 B) | 32 | <0.001% |
| total | 40,536,086 | 100% |
[V] These rows sum to 40,536,086 exactly, with no remainder. Version 6 printed this table without the 16-byte gap row and therefore fell 16 bytes short while claiming to be exact; see section 19.2.
Four different things in this format are all loosely called “a section” or “a header” in casual description, and they have four different sizes. Version 6 used the words inconsistently: its level-0 map called the first band header at 4,560 bytes while its section 3 called the reserved head the header at 4,096 bytes, and its band labelled config blob, 1,947 B sat above a section describing a configuration blob of 1,797 bytes. Neither number was wrong; they were different things wearing the same name. This section fixes the vocabulary, and the rest of the manual holds to it.
| term | what it is | where it starts | size for section 2 |
|---|---|---|---|
| chain record | one framed key/value unit of the property chain: len, type, keylen, key, vallen, value, len | 0x0BDE98F | 22,252,857 B |
| section | the value of a chain record whose value begins with 0x7FCA00FF | 0x0BDE9B8 | 22,252,812 B |
| DNNI record | a 20-byte header plus its payload, inside a section. Also called a whole record | 0x1013F68 (record 117) | 66,604 B |
| payload | the bytes after a DNNI record header | 0x1013F7C | 66,584 B |
Figure 2 The four nested things this manual has to name, on one page. A chain record is the framed key/value unit. A section is the value inside one such record. A DNNI record is a 20-byte header plus a payload, inside a section. A payload is what follows a record header. Sizes in this manual always say which of the four they are; a figure that says whole record includes the 20-byte header, and one that says payload does not.
The first 0x1000 bytes are reserved. They hold a fixed header, a hash index over the property chain, and zero padding out to the boundary. Sixteen further bytes sit between that boundary and the first chain record. The 0x1000 figure is inherited from the parametric format, where the same region held a table of 1021 global synthesis floats.
Figure 3 The header. The reserved head is 0x1000 bytes, but the level-0 band above it is 4,560 bytes because that band is a whole number of chain records: the head, the 16-byte gap, and the eleven metadata records that follow. All fields are little-endian.
| +0x000 | magic | Uint4B | 0x0000F580 in both banks [V] |
| +0x004 | unknown | Uint4B | 10. Not a section count: 4 sections, 22 chain records [U] |
| +0x008 | total_size | Uint8B | equals the file size exactly [V] |
| +0x010 | data_offset | Uint4B | 0x1000 [V] |
| +0x014 | unknown | Uint2B | 0x1000, equal to data_offset [U] |
| +0x016 | index[22] | _NOFS_IDX | one entry per chain record [V] |
| +0x11E | padding[3810] | UChar | all zero in both banks [V] |
| +0x1000 | gap | [16] UChar | twelve zero bytes then u32 = 0x1000 [V] |
| +0x000 | key_hash | Uint4B | FNV-1a 32 of the chain key, signed char [V] |
| +0x004 | offset | Uint8B | start of the chain record, not of its value [V] |
[V] The chain starts at 0x1010, not at
data_offset. The sixteen bytes at 0x1000 are
000000000000000000000000 then 00100000 — twelve zeros
followed by a u32 = 0x1000. That trailing word is exactly what a backward walk reads when it steps
off the front of the first record, so it acts as a sentinel terminating reverse traversal at the
header. A parser that starts at data_offset and expects a record there will fail.
[V] The index is neither in chain order nor sorted by hash, and its position is not a function of the hash modulo the entry count. Resolve a key by searching the index for its hash; do not rely on position.
[V] The hash is 32-bit FNV-1a with the standard offset basis
0x811C9DC5 and prime 0x01000193, computed over sign-extended char. Feeding bytes as unsigned
reproduces only 18 of 22 entries per bank — every key whose bytes
are all below 0x80. The 4 misses are the three 29-byte binary section
keys and the 1-byte key 0x91, all of which contain high bytes. Sign-extending each byte
to a 32-bit value before the XOR resolves all of them: 22 of 22 here and
44 of 44 across both banks.
uint32_t h = 0x811C9DC5;
for (const char *p = key; p != key + len; ++p)
h = (h ^ (uint32_t)(int32_t)*p) * 0x01000193;
The signature of the bug is diagnostic: a mismatching entry always agrees with unsigned FNV-1a in its lowest byte and nowhere else, because sign extension only ever disturbs bits 8 and above.
[S] The original documentation gives the header as 12 bytes with a u32
total_size and no index at all. [V] That is wrong for neural
banks on both counts: the size field is 64 bits, and a 22-entry index sits at +0x016. The
u32 at +0x004 does read 10, and the original reports the same constant across a much larger
set of voices, which corroborates that the value is fixed rather than a property of these two files.
Everything above the record layer — names, languages, the configuration blob, the four
sections themselves and the two opaque trailer blobs — is a single flat chain of
length-framed key/value records. It begins at 0x1010 and tiles to
the last byte of the file with no gap and no slack, which is the strongest available check that the
container is fully understood.
Figure 4 The property chain. The length word is repeated at both ends and counts both copies of itself, so the chain can be walked in either direction. Level 1 is drawn as chain records, which is what the level-0 bands are made of.
| +0x000 | len | Uint4B | total record length, including both copies [V] |
| +0x004 | type | Uint2B | 1 for all 44 records observed [V] |
| +0x006 | keylen | Uint2B | 1, 4..29 bytes; may be binary [V] |
| +0x008 | key | [keylen] UChar | ASCII name, or a binary section key [V] |
| the three fields below are at key-dependent offsets | |||
| +0x008+k | vallen | Uint4B | value length in bytes [V] |
| +0x00C+k | value | [vallen] UChar | string, blob, or a whole DNNI section [V] |
| +len-4 | len2 | Uint4B | the length again, for reverse traversal [V] |
| offset | key | record | value | content |
|---|---|---|---|---|
0x0001010 | .name | 28 | 7 | GUMI AI |
0x000102C | .version | 27 | 3 | 101 |
0x0001047 | .vendor | 41 | 18 | INTERNET Co., Ltd. |
0x0001070 | .multi | 65 | 43 | english mandarin japanese spanish cantonese |
0x00010B1 | .feature_rap_languages | 63 | 25 | english mandarin japanese |
0x00010F0 | .musical_preference | 39 | 4 | sing |
0x0001117 | .timbre_styles | 53 | 23 | Ballade Cute Soft Vivid |
0x000114C | .hdvm | 25 | 4 | true |
0x0001165 | .feature_f0_vibmod | 34 | 0 | |
0x0001187 | .feature_f0_orn | 31 | 0 | |
0x00011A6 | .feature_f0_enhancement | 42 | 3 | 1.0 |
0x00011D0 | f0model-dds | 8,111,519 | 8,111,492 | SECTION |
0x07BD76F | f0model-configs | 46 | 15 | _ _ _ _ _ _ _ _ |
0x07BD79D | .language | 33 | 8 | japanese |
0x07BD7BE | .phoneset | 31 | 6 | romaji |
0x07BD7DD | .type | 23 | 2 | mu |
0x07BD7F4 | 0x91 | 1,814 | 1,797 | 1797 binary bytes |
0x07BDF0A | b3d8126b21183f76… | 4,328,069 | 4,328,024 | SECTION |
0x0BDE98F | b3d8126b21183f76… | 22,252,857 | 22,252,812 | SECTION |
0x21176C8 | b3d8126b21183f76… | 5,836,582 | 5,836,537 | SECTION |
0x26A85EE | 7f7f7f7f… | 276 | 256 | 256 binary bytes |
0x26A8702 | 7e7f7f7f… | 276 | 256 | 256 binary bytes |
The record column is the whole chain record including framing; the
value column is what the key maps to. For the four section records the difference is the
45-byte framing of a 29-byte key, and 27 bytes for
f0model-dds.
[V] The three binary section keys share an 8-byte prefix
(b3d8126b21183f76) and a 5-byte suffix (06258849bd) with 16
unique bytes between, in both banks. [U] Those middles are not MD5 hashes
of the sections they name. [V] They are, however, looked up by name: the
configuration blob stores each of them as the value of a readable key
(model_duration, model_timbre_pred, model_vocoder), which is
how the engine finds a section without scanning. See section 15.
[S] The original documentation describes this same key/value encoding for the parametric format and reads the leading and trailing length words as a surrounding “block” with a reserved field. [V] The block is the record: the second word is the same length repeated at the end of the previous record.
Four chain records carry a DNNI section as their value. Each section begins with an 8-byte container header — a marker and a root count — and then a stream of records that runs to the last byte of the section with nothing left over.
Figure 5 A section. The container header is eight bytes, not twenty: the marker is not followed by a name, and the first record starts immediately at +0x008. Every omitted run of records is drawn as a dashed box that says how many records and how many bytes it stands for, so the level still tiles.
| # | role | section starts | section | chain record | records | quantised | int16 parameters |
|---|---|---|---|---|---|---|---|
| 0 | f0 / pitch model | 0x00011E7 | 8,111,492 | 8,111,519 | 456 | 146 | 3,821,664 |
| 1 | duration & timing model | 0x07BDF33 | 4,328,024 | 4,328,069 | 81 | 21 | 2,126,080 |
| 2 | acoustic / timbre model | 0x0BDE9B8 | 22,252,812 | 22,252,857 | 570 | 201 | 10,912,640 |
| 3 | vocoder | 0x21176F1 | 5,836,537 | 5,836,582 | 231 | 69 | 2,742,016 |
| total | 40,528,865 | 40,529,027 | 1338 | 437 | 19,602,400 |
[V] All four roles are now direct readings rather than inferences: the
configuration blob names sections 1, 2 and 3 by their binary keys as model_duration,
model_timbre_pred and model_vocoder, and section 0's chain key is the
ASCII string f0model-dds. Version 6 marked sections 0 and 3 as inferred.
[V] The u32 after the container marker is not a record count. It reads 2, 2, 1, 2 for sections 0–3 in both banks, against actual record counts of 456, 81, 570, 231. Section 8 shows what it really is: the number of roots in the record tree.
[V] Section lengths are not constant across banks. Teto's section 2 is 22,252,813 bytes, one byte longer, because record 562 stores the style names and Teto's are one character longer in total. It is not safe to assume that two banks of the same generation have identical section lengths.
Records are introduced by a marker that is a quiet NaN when read as a little-endian float32, so it can never collide with a legitimate weight value.
Figure 6 The DNNI record header. Walk the stream with offset + 20 + size; the size field is authoritative and no scanning for markers is needed.
| +0x000 | marker | Uint4B | 0x7FCA00FF hdr / 0x7FCA40FF data / 0x7FCA41FF meta [V] |
| +0x004 | tag | [4] UChar | type tag; ASCII only in section 2 [V] |
| +0x008 | type_code | Uint4B | layer type in section 2, arbitrary elsewhere [V] |
| +0x00C | children | Uint4B | number of records nested under this one [V] |
| +0x010 | size | Uint4B | payload bytes that follow [V] |
| +0x014 | payload | [size] UChar | next record is at +0x014 + size |
| marker | role | count in GUMI AI v101 |
|---|---|---|
0x7FCA00FF | section container header, then a u32 root count | 4 (one per section) |
0x7FCA40FF | data record | 866 |
0x7FCA41FF | metadata / configuration record | 472 |
[V] 1338 records plus 4 container headers, identically in both banks. The per-section split is 456 / 81 / 570 / 231 and every section walks to its exact end with zero bytes left over.
The 20 bytes above are common to all 1338 records. What follows them is not. Six payload
shapes account for every record in section 2, and they differ in size by four orders of magnitude
— from nothing at all to 66 KB. This is why a parser must dispatch on tag and
type_code before it reads a single payload byte.
Figure 7 The payload shapes, drawn to a common header width but not to a common byte scale — the last row is 3,300 times the length of the first.
[V] The field is easily mistaken for padding. It is zero on
369 of 1338 records and nonzero on the rest. On modl records it
is exactly the number of prim records that immediately follow:
136 of 136 in GUMI and 136 of 136 in Teto, with no exceptions.
| tag | value at +0x0C | meaning |
|---|---|---|
| prim | 0 on all 355 | a leaf; weight tensors have no children |
| modl | 2 ×102, 4 ×33, 1 ×1 | tensors belonging to this layer: weight+bias, gated conv, embedding |
| moda | 0 on all 8 | an activation marker, a leaf |
| cmpu | 3 on all 3 | structural group; children are layers |
| modm | 2, 3 ×3, 4 | module boundary |
| cmpg | 4 ×6, 5 ×2, 9 ×3, 15 ×3 | structural group, one level higher [U] |
| _stb | 2 | the style subtree: a vector group and a scalar record |
[S] The original documentation had this right and called it
meta[1], “the number of prim tensors following this modl”. Its
meta[] index counts words from the tag, so meta[0] is
type_code, meta[1] is the child count, meta[2] is
size, and meta[3] onward are payload words.
| section | ASCII tags, GUMI | ASCII tags, Teto | tags differing between banks |
|---|---|---|---|
| 0 | 13 of 456 | 14 of 456 | 415 of 456 |
| 1 | 2 of 81 | 5 of 81 | 74 of 81 |
| 2 | 570 of 570 | 570 of 570 | 0 of 570 |
| 3 | 4 of 231 | 9 of 231 | 212 of 231 |
[V] Section 2 carries stable, meaningful ASCII tags and they are
byte-identical between the two banks. Sections 0, 1 and 3 carry essentially none, and their tags
differ in almost every record. The same is true of type_code and the child count:
outside section 2 all three words are scrambled while the marker and size stay valid. A parser can
therefore walk any section but can only interpret section 2. The ASCII-tag counts differ between the
banks because the scrambled bytes happen to be printable at different rates; version 6 printed the
GUMI column alone as if it held for both.
[S] The original's parsing algorithm scans the whole file
for marker bytes and accepts a marker if the next four bytes are printable ASCII.
[V] That heuristic admits false positives: the binary tags
,SZ0, i8Bj, V;W0 and ]FT3 all pass it in GUMI's
sections 0 and 3 while being ordinary scrambled tags. Sequential walking with the size field has no
such failure mode.
[V] The child counts are not decoration. Reading the root count from the container header and then consuming each record's children recursively walks section 2 as a prefix-encoded tree and lands exactly on the last record: 570 of 570 consumed from a single root, in both banks. Sections 0, 1 and 3 do not walk, for the same reason their tags do not resolve — the arity word is scrambled there too.
i = 0
for _ in range(root_count):
i = consume(i)
assert i == len(records)
def consume(i):
n = records[i].children; i += 1
for _ in range(n): i = consume(i)
return i
_rld #0 2 children - the section root
_rld #1 8 children
_ftm #2 6 children, payload (108, 1, 71, 2, 66, 0, 0.005f, 0)
cmpg #3 5 children
_psv #4 japanese-romaji-phones, 42 symbols
_psv #5..#8 mandarin, english, cantonese, spanish
_psv #9 multiphone, 107 symbols
_dct #10 2 children: two prim tables of 16,392 B
cmpu #13,#17,#21 3 children each: weight, weight, bias
prim #25 the base identity vector, 132 B payload
_vqc #26 4 children - the encoder stack
...
_stb #562 2 children - the style subtree
cmpg #563 5 children
prim #564 default vector, 132 B payload
prim #565..#568 one vector per style, 132 B each
prim #569 the four per-style scalars, 20 B payload
Section 2 as the arity words encode it. The style block is not a convention
inferred from adjacency: it is a subtree with _stb at its root.
[I] This also explains the container header's u32. It is the number of roots, and section 2's value of 1 is the reason a single walk from record 0 consumes everything. [U] Whether the value 2 in the other three sections is meaningful cannot be tested while their arity words are scrambled.
96.7% of the file is one payload shape: a per-row scale array in float32 followed by an int16 matrix.
Figure 8 A quantised tensor, from the whole file down to a single scale. Row r of the matrix is divided by 32,767 and multiplied by scale[r].
| +0x000 | v3 | Uint4B | 16 on all 437 tensors; a storage flag meaning 16-bit [V] |
| +0x004 | v4 | Uint4B | 6..15; not a function of the tensor, inert [V] |
| +0x008 | reserved | Uint4B | 0 on all 437 [V] |
| +0x00C | n | Uint4B | scale count; always equals rows [V] |
| +0x010 | scale | [n] Float | one per output row [V] |
| +0x010+4n | rows | Uint4B | |
| +0x014+4n | cols | Uint4B | |
| +0x018+4n | q | [rows*cols] Int2B | row-major [V] |
payload size == 16 + n*4 + 8 + rows*cols*2 exactly, on 437 of 437 records in both banks W[r, c] = q[r, c] * scale[r] / 32767.0
[V] Dequantisation is exactly invertible:
rint(W / (scale/32767)) reproduces the original int16 array with a maximum absolute
error of 0 over all 437 tensors in both banks.
Version 6 was wrong here. It reported that 32.4% of rows reach full scale and dismissed a
figure of about 55% quoted elsewhere. Both of those numbers came out of an abs()
evaluated in int16, where abs(-32768) overflows back to -32768 and is then
silently discarded by a maximum. Computing the same three statistics that way reproduces version 6's
figures to two decimal places (32.37% / 59.71% / 95.84%),
which is what identifies the cause. The correct figures are below, and the ~55% that version 6
rejected is close to right.
[V] The quantiser clips to the full signed int16 range, not to a
symmetric ±32767: the value -32768 occurs 54,366 times in GUMI and
54,291 times in Teto. A row that hits the negative rail therefore dequantises to
scale[r] × 32768/32767, marginally outside the nominal range. Full scale means
|q| ≥ 32767.
| statistic, over all 107,718 rows | GUMI AI v101 | Kasane Teto v104 |
|---|---|---|
rows reaching full scale, |q| ≥ 32767 | 51.45% | 51.55% |
| rows reaching 30,000 | 68.85% | 69.01% |
| rows reaching 16,384 | 96.37% | 96.41% |
| the same three under version 6's int16 abs() | 32.37% / 59.71% / 95.84% | 32.43% / 59.85% / 95.89% |
Per section, the full-scale row fraction is 49.88% (section 0), 41.90% (section 1), 55.22% (section 2), 45.20% (section 3). The encoder therefore clips rather than using an exact per-row maximum, and clipped elements are not recoverable.
[V] v4 is not a function of the tensor. Diffing GUMI against Teto record by record turns up 368 quantised tensors whose per-row scales and whose entire int16 array are byte-identical; 45 of those carry a different v4 — usually by one, occasionally by two. Since the tensor is the same in both files, no function of the tensor can produce two answers. Direct searches over bit-lengths of the maximum, mean, median and upper percentiles of |q|, over scale exponents, dynamic range, and over rows, cols and their product all fail to predict it.
[S] The original documentation reads v4 as a kernel size, quoting values
3, 5, 7, 9, 10 and 20. [V] In these two banks it takes the values
6, 7, 8, 9, 10, 11, 12, 13, 15, and the kernel size is
carried elsewhere, in the modl tuple described in section 14. Either way dequantisation
does not need it.
[S] The original documentation states that a prim payload
is a 20-byte header followed by raw IEEE-754 float32 weights. [V] It is
not, and the arithmetic settles it. The original's own worked example is a data region of 66,576
bytes, giving a stated payload of 66,584. Payloads of exactly that size exist in both banks —
there are 111 of them in GUMI, 60 in section 0 and
51 in section 2 — and every one decodes as
16 + 256×4 + 8 + 256×128×2 = 66,584: a 256 by 128 int16 matrix with 256 float32
scales. (Version 6 gave this count as 46; the real number is 111.)
Read as float32 the same region yields 16,644 elements, which the original factors as
73 × 76 × 3 and interprets as a convolution. That factorisation is arithmetically correct
and structurally meaningless: 16,644 is 66,576/4, and 66,576 is the byte count of a quantised tensor
plus its scale table, not a float32 element count. [V] The real channel
counts are declared explicitly in the _ncw and _gnc records and are powers
of two throughout; see section 14.
The rest of the payload shapes are small and plain. A one-word count followed by that many float32 values is the form every per-voice array takes.
| +0x000 | count | Uint4B | element count [V] |
| +0x004 | v | [count] Float | IEEE-754 single, little-endian [V] |
[V] The encoding is predicted by tag and type_code
together, not by type_code alone. A record tagged prim with type code 49
is a one-word float array in 151 cases; a record tagged modl with
the same type code is a layer descriptor and not a tensor at all, in 129 cases.
Dispatch on the pair.
[I] Within a modl the type_code of each child
marks its role: 52 for weight matrices, 49 for the bias vector that follows them. A gated
convolution therefore reads as three 52s and one 49, and an ordinary layer as one 52 and one 49.
Everything up to this point is shared. Two banks of the same generation have byte-identical weight tensors in sections 0, 1 and 2 — all 19,602,400 of them. What differs is a handful of small float32 arrays, and this section locates every one of them.
| array | section | record | record offset | payload offset | floats | payload |
|---|---|---|---|---|---|---|
| pitch | 0 | record 13 | 0x000359B | 0x00035AF | 32 | 132 B |
| timing | 1 | record 80 | 0x0BDE773 | 0x0BDE787 | 128 | 516 B |
| base identity | 2 | record 25 | 0x0BE93F8 | 0x0BE940C | 32 | 132 B |
| default mode | 2 | record 564 | 0x21173A4 | 0x21173B8 | 32 | 132 B |
| style vectors | 2 | records 565–568 | 0x211743C | 0x2117450 | 128 | 528 B |
| style scalars | 2 | record 569 | 0x211769C | 0x21176B0 | 4 | 20 B |
| style names | 2 | record 562 | 0x2117354 | 0x2117368 | — | 40 B |
| total | 1500 B |
The two offset columns are the record start and the payload start; they differ by the 20-byte record header. Version 6's table gave record offsets under a heading that did not say so while quoting payload offsets in the prose beside it.
Figure 9 The entire singer, marked on the whole file. Four red marks, 1,500 bytes between them, drawn at the minimum visible width because at true scale each would be thinner than a hairline. The other 40,534,586 bytes are shared with every bank of this generation.
Of those 1500 bytes, three arrays carry the identity of the voice: pitch, base and default. Ninety-six float32 values. [V*] Copying just these three payloads from one bank into another moves the perceived singer while leaving all 19,602,400 weights untouched.
Here they are in full. This is the whole of GUMI AI v101's vocal identity — every byte of it:
pitch — section 0, record 13, payload at
0x000035AF, 132 bytes: a u32 count of 32, then 32 × float32
000035af 20 00 00 00 9d 31 dd bd 30 d2 2d 3d 4a 19 5a bd 000035bf d0 0d 0f 3e 31 1d 25 3e f2 89 21 be 97 63 1c 3e 000035cf a2 70 55 bd 62 f9 83 bd ac 2e dd bc 77 24 24 3d 000035df c2 66 00 3d f5 38 52 bc 06 99 d4 3d c8 11 9d bd 000035ef aa cd a7 3e 22 88 03 3e b2 a9 95 bd 76 a4 11 3e 000035ff bc 71 7f bd 94 49 bb 3e 8a fe 19 3e 58 01 1b bc 0000360f d5 e7 86 3d 76 3e 62 3d d0 fb f8 3c e8 14 be bd 0000361f 54 8b 83 be 27 d7 d2 3d cb f8 b2 3d e4 98 2d be 0000362f ab b6 93 bc
base identity — section 2, record 25, payload at
0x00BE940C
00be940c 20 00 00 00 bd 23 8c bd 05 8e 03 3d c4 7d 56 bd 00be941c 93 2d 49 3e 09 a6 2c 3e 81 af b2 3d 15 b1 4e 3d 00be942c 06 bd 1c be f6 7e 69 bc b0 62 3f 3e f6 a3 c2 3d 00be943c 7b f3 01 bc ac 96 6e ba 35 2b 9f bd 82 ba 45 bd 00be944c 65 ef ce 3d 85 21 11 3d 97 7a b9 bd c5 7a b2 3b 00be945c 5d af c8 3d 44 35 18 be 70 f7 b6 bd ef 15 31 be 00be946c 84 0d fd bc da 3e fb 3d 8b b2 b4 bd c8 61 c4 bc 00be947c a2 36 0f 3e 29 d7 8c 3d 04 6d 0d be 4a 79 b5 bd 00be948c cf 27 39 bd
default mode — section 2, record 564, payload at
0x021173B8
021173b8 20 00 00 00 a5 e1 4b 3d 6b 1c 59 bd 30 38 2f 3e 021173c8 4a ff 76 3d 7e 2a f5 3d 20 6b 9c be c3 17 e4 bd 021173d8 05 60 de 3c ed 22 d9 3d b8 a7 f0 bc b9 5e 81 3d 021173e8 fc 87 a6 bc 73 eb 1a be 05 2e a6 3d 08 22 49 3e 021173f8 68 80 9f 3d d9 a8 9d 3e 95 cf 2e bd ad 1a 2b bd 02117408 34 ff 8d 3d e0 36 33 bd a9 e5 18 3e e0 e0 06 bd 02117418 66 64 cb be 47 62 cc 3e dd 45 39 bc 18 80 36 3c 02117428 80 c5 16 be 04 6e 40 be 61 7f 90 3d d1 9e 64 3e 02117438 d9 01 53 be
[V] 396 bytes out of 40,536,086 is
0.00098% of the file. The first four bytes of each block are the count word
20 00 00 00 = 32; everything after is little-endian float32.
[V] Between GUMI and Kasane Teto, 358 of these
396 bytes differ.
[V] Section 0 is the f0 model. Five of its records carry a 132-byte payload — records 13, 429, 435, 445, 451 — and only record 13 differs between banks. It is the one an independent editor exports as the pitch vector.
Figure 10 Pitch. The other four 132-byte payloads in section 0 are byte-identical between the two banks examined, so they are shared backbone data, not per-voice.
[V] Section 1 is the duration and timing model, and the timing vector is its last record. It is the only per-voice array with 128 floats rather than 32.
Figure 11 Timing. [U] What this array controls has not been isolated: it has never been transplanted on its own. Experiment C in section 21 is designed to settle it.
[V] Two 32-float vectors live in section 2 itself: record 25, near the top of the tree, and record 564, the first child of the style group. Together with pitch they are the 396 bytes above.
Figure 12 Base identity, in the same three steps as every other record in this manual. Record 564 has the identical layout, 880 bytes from the end of the section.
[V] Record 562, tagged _stb, is a style-name table. Its
payload is a count followed by length-prefixed names:
[u32 count] then count × { [u32 len][len bytes of ASCII] }
| bank | .timbre_styles (display order) | _stb (storage order) |
|---|---|---|
| GUMI AI v101 | Ballade Cute Soft Vivid | Soft Ballade Vivid Cute |
| Kasane Teto v104 | Joyful Cute Power Mellow | Cute Power Mellow Joyful |
[V] The vectors and the scalars both follow _stb order. The
obvious mistake here is to take the order from .timbre_styles and then treat the
mismatch as a permutation to be solved. There is no permutation. Section 8 settles it structurally:
_stb is the parent of the cmpg that holds the vectors and of the record
that holds the scalars, so the name table and the data it names are one subtree.
.timbre_styles is a separate display string that happens to be alphabetical for GUMI
and is not for Teto.
| record | GUMI style (_stb) | scalar in record 569 | pairing from .timbre_styles |
|---|---|---|---|
| 565 | Soft | +0.06766955 | Ballade — wrong |
| 566 | Ballade | -0.02669089 | Cute — wrong |
| 567 | Vivid | +0.02701959 | Soft — wrong |
| 568 | Cute | -0.01311327 | Vivid — wrong |
Verified both ways on both banks: the 128 data bytes of records 565–568 equal
the editor's exported style vectors when matched in _stb order, and the four floats of
record 569 equal the editor's scalars in the same order, to the last bit.
[V] Record 569 is a normal record with a one-word count header, not sixteen loose bytes at the tail of the section. It happens to be the last record, which is how it is easily mistaken for loose bytes.
[V] The six _psv records near the top of section 2 are not
conditioning vectors. They are the phoneme dictionaries, and they are plain text. Each is a named
group of four parallel string tables.
Figure 13 Phoneme dictionaries. The same string-table encoding is used by _stb: a length-prefixed string is the format's only variable-length primitive.
| record | name | symbols | first symbols |
|---|---|---|---|
| 4 | japanese-romaji-phones | 42 | a i u e o N cl t d s sh j z ts k kw … |
| 5 | mandarin-xsampa-phones | 59 | a A o @ e 7 U u i i\ i` y AU @U ia iA … |
| 6 | english-arpabet-phones | 50 | aa ae ah ao aw ax ay b ch d dx dr dw dh eh er … |
| 7 | cantonese-xsampa-phones | 46 | ts tsh f h s l m n N w j p ph t th k … |
| 8 | spanish-xsampa-phones | 31 | a e i o u U I y ll b B d D g k p … |
| 9 | multiphone | 107 | a A o @ e 7 U u i i\ i` y AU @U ia iA … |
| table | length | content |
|---|---|---|
| 0 | n | the symbols, in that language's own notation |
| 1 | n | the phonetic class of each symbol, one of the 13 in table 3 |
| 2 | n | the canonical multiphone name for each symbol, with prefixes ARP_, ES_, ROM_, YUE_ marking symbols that exist only in one language |
| 3 | 13 | the closed class vocabulary: vowel, diphthong, coda, stop, affricate, aspirate, fricative, nasal, liquid, semivowel, silence, breath, trill |
[I] Table 2 is the cross-lingual mapping. It is what lets one acoustic model sing five languages: each language's symbols are rewritten into the shared 107-symbol inventory before they reach the network.
Version 6 put the spare slot at the wrong end. It stated that the multiphone record's
table 2 has 108 entries rather than 107 and that “the additional one, at index 0, is the empty
string”, inferring that index 0 is reserved and the real inventory starts at 1.
[V] Tables 0 and 2 of the multiphone record agree entry for entry on all
107 entries — index 0 is the symbol a in both — and the extra 108th entry,
the empty string, is appended at the end, at index 107. The inventory is 0–106 and the
reserved slot is the last one.
[V] _ftm, near the top of the section, declares 108 in its
first payload word; its payload is eight words,
(108, 1, 71, 2, 66, 0) then 0x3BA3D70A — which read as float32 is
0.005, the same hop-in-seconds constant the configuration blob stores as
voc_thop_output — then 0. (Version 6 printed only the first six words.)
[V] All six dictionary records are byte-identical between GUMI and Teto,
as expected for shared backbone data. [V] This also grounds the
.multi chain value. Both banks declare
cantonese english japanese mandarin spanish, and there is
exactly one dictionary per declared language plus the union. The dictionaries are stored in a
different order from .multi; as with the styles, do not read order across the two.
Section 2 declares its own shape. Channel widths come from _ncw and _gnc
records, layer geometry from the modl payload, and the tensors hang beneath them as
children.
Figure 14 Architecture. The dilation cycle 1, 3, 9, 27, 81 across five parallel paths is the multi-receptive-field fusion block of the HiFi-GAN family.
| type_code | children | payload | count | reading |
|---|---|---|---|---|
| 49 | 2 | (1, 1, 0, 1, 1) | 96 | pointwise convolution, weight + bias |
| 49 | 4 | (3, 1, 1, 1, 1) | 8 | gated dilated conv, d = 1 |
| 49 | 4 | (3, 1, 3, 3, 1) | 7 | gated dilated conv, d = 3 |
| 49 | 4 | (3, 1, 9, 9, 1) | 7 | gated dilated conv, d = 9 |
| 49 | 4 | (3, 1, 27, 27, 1) | 7 | gated dilated conv, d = 27 |
| 48 | 2 | (no payload) | 6 | linear projection, weight + bias |
| 49 | 4 | (3, 1, 81, 81, 1) | 4 | gated dilated conv, d = 81 |
| 52 | 1 | (no payload) | 1 | embedding table |
Padding does not always equal dilation. Version 6 stated that it does. It holds for the 33
kernel-3 convolutions, but the 96 pointwise layers have payload
(1, 1, 0, 1, 1) — padding 0, dilation 1. [V] The
invariant that actually holds on all 129 modl payloads is
padding = dilation × (kernel - 1) / 2, which is what keeps the output length equal
to the input length for any kernel.
[V] The modl tuple is five words. Reading only four and
calling the second element groups is a natural error; it is stride, and groups
is the fifth word.
[V] _ncw opens a block and _gnc opens each
sub-block within it. Both carry a three-word payload.
| +0x000 | in_channels | Uint4B | |
| +0x004 | out_channels | Uint4B | |
| +0x008 | extra | Uint4B | 0, 4, 64, 388 in these banks [U] |
| block | opens at | in → out | sub-blocks | extra |
|---|---|---|---|---|
| A | rec 46 | 144 → 256 | 4 (1 adapter + 3 at 256 → 256) | 0 |
| B | rec 104 | 4 → 128 | 15 (1 adapter + 14 at 128 → 128) | 388 |
| C | rec 341 | 256 → 256 | 9 (1 adapter + 9 at 256 → 256) | 4 |
| D | rec 489 | 256 → 128 | 4 (1 adapter + 3 at 128 → 128) | 64 |
Version 6 gave block B as 12 sub-blocks; there are
14 at 128 → 128 plus the
4 → 128 adapter. The four blocks account for all 32
_gnc records.
[I] The extra word matches vocoder constants from the
configuration blob: 64 is voc_order_spec and 4 is voc_nchannel. 388 and 0
are not accounted for.
[V] Only 29 of the 33 gated convolutions have out = 2 × in. The 4 exceptions are input adapters, with shapes (128, 108), (512, 144), (256, 4), (256, 256). The 108 is the multiphone inventory plus one, from section 12.
[V] _did carries (4, 64) then
0x42800000, which as float32 is 64.0.
[V] The chain record with the 1-byte key 0x91 holds
1,797 bytes in GUMI and 2,131 in Teto. The original documentation does not
mention it, and version 6 of this manual described its structure incorrectly. It is a typed,
counted property list, and its keys are readable once one XOR is undone.
Figure 15 The configuration blob. Keys are ASCII names XORed with a fixed positional mask; values are typed and counted. Both banks parse to the exact byte under this reading.
| +0x000 | magic | Uint4B | 0x0000FEFF [V] |
| +0x004 | count | Uint4B | number of entries [V] |
| then count entries, each: | |||
| +0x000 | type | Uint4B | 0 = strings, 1 = float64, 2 = int32 [V] |
| +0x004 | index | Uint4B | 0-based, sequential; redundant with position [V] |
| +0x008 | keylen | Uint2B | 8 to 31 bytes [V] |
| +0x00A | key | [keylen] UChar | obfuscated ASCII, see below [V] |
| +0x00A+k | count | Uint4B | number of values, may be 0 [V] |
| +0x00E+k | values | [count] | type 0: [u16 len][bytes] each; type 1: 8 B each; type 2: 4 B each |
[V] Under this reading both blobs parse to the exact byte: 46 entries
filling 1,797 bytes and 52 filling 2,131, with nothing left over.
[V] Entries 0–20 are byte-identical between the banks; the blobs
first diverge at byte 810, inside the value of model_duration, which is
that bank's own section key. (Version 6 stated that the first 1,190 bytes are identical; the
count word at +0x004 already differs.)
[V] The key bytes are not hashes. Each is the plain name of the property
XORed with a fixed, position-indexed mask — byte i of the key is XORed with mask byte
i. The mask was recovered from two keys whose plaintext is known independently
(language and phoneset, whose values are the same strings the chain stores
under .language and .phoneset), and then extended by requiring every other
key in both banks to decode to [a-z0-9_]. The fit is exact: 31 mask positions, zero
conflicts across all 98 keys in the two banks, and Teto's six keys that GUMI does not have decode
cleanly under a mask solved without them.
mask[0..30] = e0 99 5e 3f 0c 55 6a 5b f8 d1 36 37 a4 0d c2 d3 10 09 0e 2f 3c c5 1a 4b 28 41 e6 27 d4 7d 72 name[i] = key[i] ^ mask[i]
The mask covers the longest key observed, 31 bytes. Whether it continues beyond that, and whether it is a constant table or generated, is not established here.
| # | type | key | values |
|---|---|---|---|
| 0 | strings | language | "japanese" |
| 1 | strings | phoneset | "romaji" |
| 2 | float64 | voc_thop | 240, 48000 |
| 3 | float64 | voc_thop_output | 0.005 |
| 4 | float64 | voc_maxnhar | 120 |
| 5 | float64 | voc_maxnhar_e | 5 |
| 6 | float64 | voc_npsd | 256 |
| 7 | float64 | voc_fnyq | 24000 |
| 8 | float64 | voc_nchannel | 4 |
| 9 | float64 | voc_chanfreq | 3000, 6000, 10000 |
| 10 | float64 | voc_nspec | 1025 |
| 11 | float64 | voc_lipradius | 1.5 |
| 12 | float64 | voc_order_spec | 64 |
| 13 | float64 | voc_order_bap | 5 |
| 14 | float64 | voc_adjust_rd_below | 0 |
| 15 | float64 | voc_adjust_rd_above | 0 |
| 16 | float64 | voc_default_formant_scale | 1 |
| 17 | float64 | dur_offset_svwl | 0 |
| 18 | strings | ext_phoneme | "cl", "stop" |
| 19 | strings | ext_phoneme | "br", "breath" |
| 20 | strings | model_duration_arch | "gen2a" |
| 21 | strings | model_duration | <29-byte section key> |
| 22 | strings | model_timbre_arch | "dds" |
| 23 | strings | model_timbre_ae | "" |
| 24 | strings | model_timbre_pred | <29-byte section key> |
| 25 | strings | model_timbre_ar | "" |
| 26 | strings | model_vocoder_arch | "nhv" |
| 27 | strings | model_vocoder | <29-byte section key> |
| 28 | float64 | silence_to_cl_thrd | 0.1 |
| 29 | float64 | tm_base_temp | 1 |
| 30 | float64 | tm_extra_temp | 2 |
| 31 | float64 | vm_temperature_voiced | none |
| 32 | float64 | vm_temperature_unvoiced | none |
| 33 | float64 | vm_clipping_voiced | none |
| 34 | float64 | vm_clipping_unvoiced | none |
| 35 | float64 | vm_ringing_suppression_voiced | none |
| 36 | float64 | vm_ringing_suppression_unvoiced | none |
| 37 | float64 | vm_impulse_removal_sensitivity | 0.33 |
| 38 | strings | vm_voicing_decision | "vuv" |
| 39 | int32 | lcod_smoother_enabled [Teto only] | 1 |
| 40 | float64 | lcod_smoother_context [Teto only] | 5 |
| 41 | float64 | lcod_smoother_base_thrd [Teto only] | 2 |
| 42 | float64 | lcod_smoother_max_thrd [Teto only] | 3 |
| 43 | float64 | lcod_smoother_knee [Teto only] | -4 |
| 44 | float64 | lcod_smoother_unvoiced_filter [Teto only] | 0, -0.5, 5, -0.5, 15, -0.5, 25, 0, 64, 0 |
| 45 | float64 | pv_peak_suppression_bw | 10 |
| 46 | float64 | pv_peak_suppression_thrd | 0.35 |
| 47 | float64 | pv_pre_gain_upper_bound | 3 |
| 48 | float64 | pv_pre_gain_lower_bound | -6 |
| 49 | float64 | pv_window_size_factor | 1 |
| 50 | float64 | pv_eq_residual_voiced | none |
| 51 | float64 | pv_eq_harmonic | none |
[V] The doubles are self-consistent as a DSP front-end configuration: a 240-sample hop at 48 kHz is 200 frames per second and 0.005 s exactly; 24000 is the Nyquist rate; 1025 is 2048/2 + 1 spectrum bins; and the four vocoder channels have the three band edges 3000, 6000 and 10000 Hz. These are the analysis parameters of the acoustic front end, which is why they are stored once, outside the sections, rather than per model.
[V] Three entries carry a section's 29-byte binary chain key as their
value. That is how the engine resolves a section by role rather than by scanning, and it settles
what sections 1, 2 and 3 are: model_duration, model_timbre_pred and
model_vocoder, with architecture tags gen2a, dds and
nhv. [I] nhv is the usual abbreviation for a
neural homomorphic vocoder, which is consistent with the voc_* parameter set:
harmonic count, noise-band count and band edges, spectral and band-aperiodicity orders.
[V] Teto adds six lcod_smoother_* entries that GUMI does
not have, which is the whole of the 334-byte difference in blob size. The blob is
therefore versioned by content, not by a version word.
[V] The backbone digest must cover sections 0 to 2 only. GUMI v101 and Teto v104 have byte-identical int16 payloads in sections 0, 1 and 2, and different ones in section 3. Hashing all four sections therefore splits two banks that share a backbone.
SHA-256 over the int16 payloads of sections 0, 1 and 2, in record order:
a0ffb7ca19b0dc2cbc42137fa2c92abfd88111049f5abf4e7f0d12602cd54df0
- identical for GUMI AI v101 and Kasane Teto v104
the same over all four sections:
96e514b46fcb3a1f66f269a8adee86356ef2fbbbab84c0b9f4ae140fb67df0fd GUMI AI v101
555ca4478ac6cc6c3c75167520857a6c1c0d84b3b6f2c0ace542b027a746d24f Kasane Teto v104
[V] Section 3 tracks the declared version rather than the singer, and an
independent editor names its content hash sing_model: MD5 over section 3's value bytes
reproduces the editor's digest exactly, on both banks, independently.
| bank | MD5 of section 3's value |
|---|---|
| GUMI AI v101 | 3f649ae6cb04ee4f7e9a7ed72ee29928 |
| Kasane Teto v104 | b0dbc93ad9636601a8a24425788a884e |
[V] The other three digests are not content hashes of this file.
base_model, timing_model and f0_model are identical across two
banks whose named sections demonstrably differ — 23 payloads differ in
section 2, 3 in section 1 and 29 in section 0, including
the identity vectors themselves. A sweep of 128 canonicalisations per section over both banks found
only sing_model. [I] They identify the shared pretrained
backbone and are assigned outside the file.
Of the 29 differing payloads in section 0, all but record 13 are quantised tensors whose scales and int16 arrays are identical and whose only difference is the inert v4 word; the same holds in sections 1 and 2 apart from the per-voice records. That is why the int16-only digest agrees while a whole-payload digest would not.
[V] The file does contain opaque high-entropy data. Two chain records at the very end of the file hold 256 bytes each.
| key | size | entropy, GUMI | entropy, Teto | behaviour across the two banks |
|---|---|---|---|---|
7f7f7f7f | 256 B | 7.18 bits/B | 7.11 bits/B | per-bank; 254 of 256 bytes differ |
7e7f7f7f | 256 B | 7.11 bits/B | 7.11 bits/B | byte-identical, across two different vendors |
256 bytes is the RSA-2048 modulus size, and the entropy is what you would expect of either a signature or a ciphertext. Tested as RSA with exponents 3, 17 and 65537 in both byte orders, no PKCS#1 structure appeared. [U] What reads them, and whether anything does, is not established here. They are documented and left alone: this reference describes layout, and a licence gate is outside its scope.
| change | result | grade |
|---|---|---|
| Zero all 10,912,640 int16 weights of section 2 | loads and renders; output becomes static | [V*] |
| Transplant the three identity vectors between banks | the perceived singer changes; weights untouched | [V*] |
| Transplant differing type tags outside section 2 | no audible or structural change; the tags carry no information the parser uses | [V*] |
| Requantise a tensor from its own dequantised values | bit-identical output, maximum error zero over all 437 | [V] |
| Rewrite a metadata string of a different length | requires fixing the chain length words at both ends and the index offsets of every later record | [V] |
| Add or remove a configuration-blob entry | requires fixing the blob's count word, every later entry's index word, the chain length words and the index offsets | [V] |
Rows marked [V*] were established by rendering audio in earlier work on this format and have not been re-tested here.
ATroubledSnake's document is the earliest description of this format that I have seen. Reading it against the bytes is worth doing: it is right about several things that are easy to miss, and wrong in one place in a way that propagates into an entire section of invented architecture.
| claim | status |
|---|---|
meta[1] is the number of prim tensors following a modl | correct and important. It is a child count, and it is what makes the record stream a tree |
_ncw and _gnc carry in-channels, out-channels and an extra word | correct; field order confirmed, values differ because the example voice is a different model |
_stb is a style bank and holds a style count | correct one level up; the count is payload word 0 and the record parents the data |
| Dilations run 1, 3, 9, 27, 81 in five parallel paths; this is an MRF block | the cycle is confirmed exactly, with counts 8 / 7 / 7 / 7 / 4 |
| Layer type codes 48 linear, 49 convolution, 52 embedding, 50 vector | confirmed for section 2, where these are the only values type_code takes |
_voc holds a sample rate of 48000 | corroborated from a different place — voc_thop in the configuration blob |
| The u32 at +0x004 of the header is 10 | true in both banks; the original reports it across many more voices |
| .svpk is a ZIP holding voice.nofs plus assets | not testable here; adopted as context in section 1 |
| claim | correction |
|---|---|
prim payloads are raw float32 weights | they are int16 quantised with a float32 scale per row. Section 9.3 shows the original's own 66,584-byte example decoding as 256 × 128 int16 |
| Conv weights factor as 73 × 76 × 3, with a 52-channel skip path | an artefact of the above. 16,644 is 66,576/4, a byte count, not an element count |
The header is 12 bytes and total_size is u32 | the header reserves 0x1000, the size is u64 at +0x008, and a 22-entry hash index sits at +0x016 |
| Section boundaries are found by scanning for the next marker | the size field at +0x010 is authoritative; scanning also produces false positives |
_psv holds per-phoneme conditioning vectors | it holds the phoneme dictionaries as plain text; see section 12 |
| v4 is the kernel size | kernel size is the first word of the modl payload. v4 is inert and is not a function of the tensor |
| v3 and v4 are matrix dimensions for type 48 | v3 is 16 on all 437 tensors in both banks; dimensions are the rows and cols words after the scale array |
The two documents were written against different voice generations, which explains some of the divergence but not all of it.
Version 7 re-derived every number in this document from the two banks using a parser written from the prose of version 6 alone, deliberately not sharing code with the one that produced it. The great majority of version 6 was confirmed byte for byte. What follows is everything that was not, with the correction and how it was established. Nineteen items; the first eight are substantive, the rest are labelling.
Version 6 said: “32.4% of rows reach full scale, not the ~55% sometimes quoted”, with 59.7% reaching 30,000 and 95.8% reaching 16,384.
Correction: The three figures are 51.45%, 68.85% and 96.37%. Version 6's numbers are reproduced to two decimals by computing abs() on an int16 array, where abs(-32768) overflows and is dropped by the maximum; -32768 occurs 54,366 times. The ~55% version 6 rejected is close to right, and is 55.22% for section 2 alone. See §9.1.
Version 6 said: A ten-row table said to sum to the file size “exactly, with no remainder”.
Correction: It summed to 40,536,070, sixteen bytes short: the gap between data_offset and the start of the chain had no row. The table in §2.1 now has that row and is asserted to sum to the file size by the build.
Version 6 said: “the four metadata bands… total 7,059 bytes between them”.
Correction: There are three: 4,560, 1,947, 552 bytes, which is where the 7,059 came from.
Version 6 said: “Teto differs by 335 bytes of metadata and by one byte inside section 2”, which totals 336.
Correction: 335 is the whole difference. It is +4 on .name, −5 on .vendor, +1 on .timbre_styles, +334 on the configuration blob, and +1 inside section 2.
Version 6 said: Entry = keylen u16, key of 8 or 15 binary bytes, type u32, value as a UTF-8 string or a float64.
Correction: Entry = type u32, index u32, keylen u16, key, value count u32, then that many values. Keys run 8 to 31 bytes. There is a third type, int32. Both blobs now parse to the exact byte. See §14.
Version 6 said: Keys described as binary hashes, and only the string values were readable.
Correction: Keys are plain ASCII names XORed with a fixed 31-byte positional mask. The whole blob is decoded in §14.2, which also names sections 1–3 and their architectures.
Version 6 said: “The doubles in the first 1,190 bytes are byte-identical between the two banks.”
Correction: They diverge at byte 810; the count word at +0x004 differs immediately. Entries 0–20 are identical, which is the claim that survives.
Version 6 said: Table 2 has 108 entries and “the additional one, at index 0, is the empty string”, so the inventory starts at 1.
Correction: Tables 0 and 2 agree on all 107 entries starting at index 0; the empty string is appended at index 107. The inventory is 0–106.
Version 6 said: “there are 46 of them in GUMI”.
Correction: There are 111: 60 in section 0 and 51 in section 2, all 256 × 128.
Version 6 said: “Padding always equals dilation.”
Correction: True for the 33 kernel-3 convolutions only. The 96 pointwise layers have padding 0 and dilation 1. The general invariant is padding = dilation × (kernel-1)/2.
Version 6 said: Payload given as six words, (108, 1, 71, 2, 66, 0).
Correction: It is eight words; word 6 is 0x3BA3D70A, which as float32 is 0.005, and word 7 is 0.
Version 6 said: Block B listed as 12 sub-blocks, all 128 → 128.
Correction: There are 14, plus the 4 → 128 adapter. The four blocks then account for all 32 _gnc records.
Version 6 said: Prefixes given as ARP_, ROM_ and ES_.
Correction: There is a fourth, YUE_, in the Cantonese and multiphone dictionaries.
Version 6 said: One column of ASCII-tag counts presented as holding for both banks.
Correction: The counts are bank-specific outside section 2: [13, 2, 570, 4] for GUMI and [14, 5, 570, 9] for Teto. §7.3 now prints both.
Version 6 said: The payload's end labelled 0x1024194 while the relative offset beside it read +0x10418 and level 2 of the same figure read 0x1024394.
Correction: 0x1024394. Absolute and relative rows are now generated from one number.
Version 6 said: The last _psv record ending at 0x0BE0CD0.
Correction: 0x0BE0CE0, 16 bytes further on.
Version 6 said: Sections 0 and 3 graded [I], inferred.
Correction: Both are now [V]: the configuration blob names sections 1–3 by their binary keys, and section 0's chain key is the ASCII f0model-dds.
Version 6 said: Two subsections numbered 10.2 — Pitch and Vocal modes.
Correction: Renumbered; vocal modes is now §11.5.
Version 6 said: header meaning both a 4,560-byte band and a 4,096-byte region; config blob meaning both a 1,947-byte band and a 1,797-byte value; offsets given as record starts in one place and payload starts in another; the section-2 map jumping from the container box to record 4 with records 0–3 absorbed silently.
Correction: §3 fixes the vocabulary and states the figure rules. The generator now refuses to emit a level whose boxes do not tile its span, so an elided run must be drawn and labelled.
Run these against any bank. Each is a one-line consequence of something above, and each failure
localises to a specific claim. The companion script nofs_verify7.py performs all of
them.
| # | check | expected |
|---|---|---|
| 1 | u64 at +0x008 equals the file size | exact |
| 2 | the chain from 0x1010 tiles to EOF | no gap, no slack |
| 3 | FNV-1a over signed char resolves every chain key in the index | 22 of 22 |
| 4 | every section walks to its exact end using offset + 20 + size | 0 bytes left |
| 5 | payload size == 16 + n*4 + 8 + rows*cols*2 on quantised records | 437 of 437 |
| 6 | requantising a tensor from its dequantised values | maximum error 0 |
| 7 | walking section 2 by child counts from the root count | 570 of 570 records |
| 8 | record 562 is _stb and names n styles | n vectors follow in that order |
| 9 | SHA-256 over sections 0–2 int16 payloads | equal for banks of one generation |
| 10 | MD5 over section 3's value bytes | equals the editor's sing_model |
| 11 | the configuration blob parses to its last byte under §14 | 0 bytes left, count word honoured |
| 12 | every configuration key decodes to [a-z0-9_] under the §14.1 mask | all keys, both banks |
| 13 | multiphone tables 0 and 2 agree entry for entry | 107 of 107, extra slot last |
| 14 | padding == dilation * (kernel-1) / 2 on every modl payload | 129 of 129 |
| 15 | the byte-budget rows sum to the file size | exact, no remainder |
| # | question | status |
|---|---|---|
| 1 | What is v4? | narrowed. Proven not to be a function of the tensor; 45 identical tensors carry different values. Likely an encoder-side counter |
| 2 | What does the timing vector do? | open. Never transplanted alone. Experiment C |
| 3 | What do the per-style scalars mean? | open. Ordering is settled, meaning is not. Experiment D |
| 4 | Are the three shared digests content hashes? | closed, negative. They identify the backbone and are assigned outside the file |
| 5 | Why do stored styles not match .timbre_styles? | closed. _stb gives the order and parents the data; the metadata string is display only |
| 6 | Do all banks have the same five 132-byte records in section 0? | open. Both banks examined here do; a wider survey has reported banks that do not |
| 7 | Are type tags meaningful? | closed. Only in section 2, where they are also stable across banks |
| 8 | What is in the configuration blob? | closed. A typed property list of DSP front-end and model-routing parameters, fully decoded in §14 |
| 9 | What are the two 256-byte trailer blobs? | open. One per-bank, one constant across vendors. Experiment F |
| 10 | What is the header's constant 10? | open. Constant here and in the original's much larger sample |
| 11 | Why is the arity word scrambled outside section 2? | open. Marker and size stay valid while tag, type and arity do not, which looks deliberate |
| 12 | What is the extra word on _ncw? | narrowed. 64 is voc_order_spec and 4 is voc_nchannel; 388 and 0 unexplained |
| 13 | What does cmpg's child count mean? | open. It is 15 where 30 layers follow and 9 where 18 do, so it counts something one level up |
| 14 | Does the key mask continue past 31 bytes? | new, open. No key in either bank is longer, so positions 31 and beyond are untested |
Each needs the shipping editor and a bank the tester already owns. All are non-destructive: work
on a copy, keep .name and .vendor intact, and every one preserves file
length and record count.
| # | experiment | what it decides |
|---|---|---|
| A | Swap two style vectors in place — exchange the 128 data bytes of records 565 and 566 — leaving 562 and 569 alone. Render the same phrase under each style name. | Confirms behaviourally what section 8 shows structurally |
| B | Reorder the names inside record 562 without touching any vector, keeping the byte length identical by swapping two equal-length names. | Separates ‘the file states its order’ from ‘the engine reads that statement’ |
| C | Transplant the timing vector alone — section 1 record 80, 512 data bytes. | Open question 2. Listen for phoneme duration and articulation with timbre held constant |
| D | Zero the four floats of record 569, then set them equal, then exaggerate one. | Establishes what the per-style scalars do — most likely an intensity or blend weight |
| E | Set v4 in one quantised record to 200. | Confirms from the engine side what the byte diff implies: that v4 is inert |
| F | Overwrite one byte of the 7f7f7f7f trailer blob and load the bank. | Tells us whether anything reads it |
| G | Change the child count on one modl from 2 to 3 and load. | Tests whether the engine walks the tree by arity or by tag order |
| H | Change voc_default_formant_scale in the configuration blob from 1.0 to 1.2, fixing only the eight bytes of the double. | New. The first test of whether the blob is read at all, at no risk to length or record count |
Experiment F is a read-only probe of whether a field is checked, not an attempt to satisfy the check; a bank whose trailer has been corrupted is less usable, not more. If it turns out to be enforced, the right conclusion is that the ‘no integrity check’ sentence must be withdrawn rather than worked around.
| tool | purpose |
|---|---|
nofs_verify7.py | Independent parser and the section 20 checklist, including the configuration blob and the key mask. Written from the format description without importing the others |
nofs_extract.py | Parse a bank, dequantise every tensor, emit a PyTorch .pt plus a JSON record index |
nofs_repack.py | Edit metadata with offset fixups, transplant tags, scale weights, graft records or a whole voice |
nofs_survey.py | Walk an installation, extract every bank's metadata and vectors, group by backbone |
nofs_arch.py | Shape-faithful PyTorch skeleton of section 2 |
Known pitfalls in existing tools. nofs_survey.py reads the style order from
.timbre_styles and will mislabel every style it reports; it should read record 562. Its
identity_vectors helper indexes section 0 record 13 and section 2 records 25 and 564 by
position within a filtered list rather than by record index, which is correct for these banks but
silently wrong for any bank whose 132-byte record set differs — open question 6 is exactly that
case. Any tool that groups banks by hashing int16 payloads should hash sections 0 to 2 only. Any
parser that treats the word at +0x0C as padding should read it as a child count. Any code that
measures clipping should take abs() in a width wider than int16, or compare against
-32768 directly; see §9.1.
This is format documentation, produced by examining files on a machine where every bank was licensed and activated. It describes how the container is laid out and how the numbers inside it are encoded. It did not require, and does not describe, any circumvention of a protection measure. Nothing here enables redistribution of a voicebank, and the per-voice arrays that carry identity are useless without the 40 MB of shared backbone that the vendor licenses.
The XOR mask in §14.1 obfuscates parameter names, not content: the values it guards are sample rates and filter orders, and undoing it makes the format documentable rather than making anything accessible that was not already. The two 256-byte trailer blobs, which are the only plausible licence-related data in the file, are documented as observations and deliberately not pursued. Experiment F asks only whether a field is checked.
Credit. The first description of this format was written by ATroubledSnake, who reverse-engineered the DNNI marker layer, the section name glossary, the layer type codes, the channel-width records and the record arity word, and published it openly. Sections 7.2 and 13 exist because of that work. The listening tests marked [V*], the twenty-five-bank survey quoted for population statistics, and the identity-transplant result on which section 11 rests all come from earlier work on this format by others.
The 4-byte tag at +0x004 of every record. Only section 2 uses ASCII tags; elsewhere the field is scrambled. Counts are for GUMI AI v101 and are identical in Teto.
| tag | n | kind | meaning |
|---|---|---|---|
prim | 355 | leaf | a weight tensor or a float32 array. type_code 52 marks a weight matrix, 49 a bias or plain vector, 50 a table, 48 once |
modl | 136 | descriptor | one network layer. Child count is the number of tensors that follow; payload is (kernel, stride, padding, dilation, groups) |
_gnc | 32 | structural | sub-block channel widths, (in, out, extra) |
cmpg | 14 | structural | compute group, one level above modm |
moda | 8 | leaf | activation marker. type_code 48, 50 or 55 |
_psv | 6 | string tables | a phoneme dictionary; see section 12 |
modm | 5 | structural | module boundary, groups layers and activations |
_ncw | 4 | structural | block channel widths, (in, out, extra) |
cmpu | 3 | structural | compute unit, groups two weights and a bias |
_rld | 2 | structural | the two outermost nodes of the section 2 tree |
_ftm | 1 | descriptor | feature transform; eight words, first is 108 |
_dct | 1 | structural | parents two 16,392-byte tables |
_vqc | 1 | structural | parents the encoder stack |
_did | 1 | descriptor | payload (4, 64, 64.0f) |
_stb | 1 | string table | style names, and parent of the style subtree |
[S] The original documentation lists further tags —
_vwr, _voc, _ddf, _f0d, _ff0,
_pdc, _dur, _pfc, _bc1, _res
— that do not appear in either bank examined here.
Everything a parser needs, on one page. Offsets are from the start of the structure named.
| +0x000 | magic | Uint4B | 0x0000F580 |
| +0x004 | unknown | Uint4B | 10 |
| +0x008 | total_size | Uint8B | file size |
| +0x010 | data_offset | Uint4B | 0x1000 |
| +0x014 | unknown | Uint2B | 0x1000 |
| +0x016 | index[22] | _IDX | u32 FNV-1a signed, u64 chain-record offset |
| +0x1000 | gap | [16] UChar | twelve zeros, then u32 0x1000; the chain starts after it |
| +0x000 | len | Uint4B | whole record, both length words included |
| +0x004 | type | Uint2B | 1 |
| +0x006 | keylen | Uint2B | |
| +0x008 | key | [keylen] UChar | |
| +0x008+k | vallen | Uint4B | |
| +0x00C+k | value | [vallen] UChar | string, blob, or a DNNI section |
| +len-4 | len2 | Uint4B | the length again |
| +0x000 | marker | Uint4B | 0x7FCA00FF hdr / 40FF data / 41FF meta |
| +0x004 | tag | [4] UChar | ASCII in section 2 only |
| +0x008 | type_code | Uint4B | layer type in section 2 |
| +0x00C | children | Uint4B | records nested under this one |
| +0x010 | size | Uint4B | payload bytes |
| +0x014 | payload | [size] UChar | next record is at +0x014 + size |
| +0x000 | v3 | Uint4B | 16 |
| +0x004 | v4 | Uint4B | inert |
| +0x008 | reserved | Uint4B | 0 |
| +0x00C | n | Uint4B | = rows |
| +0x010 | scale | [n] Float | |
| +0x010+4n | rows | Uint4B | |
| +0x014+4n | cols | Uint4B | |
| +0x018+4n | q | [rows*cols] Int2B | W[r,c] = q[r,c] * scale[r] / 32767, q in -32768..32767 |
| +0x000 | type | Uint4B | 0 strings, 1 float64, 2 int32 |
| +0x004 | index | Uint4B | 0-based, sequential |
| +0x008 | keylen | Uint2B | 8..31 |
| +0x00A | key | [keylen] UChar | name[i] = key[i] ^ mask[i] |
| +0x00A+k | count | Uint4B | number of values |
| +0x00E+k | values | [count] | strings are [u16 len][bytes] |
| want | look at |
|---|---|
| the singer | section 0 rec 13, section 2 rec 25, section 2 rec 564 — 396 bytes of payload, §11.1 |
| timing | section 1 rec 80, 128 floats |
| vocal modes | section 2 rec 562 for the names, 565–568 for the vectors, 569 for the scalars, in that order |
| the phoneme set | section 2 rec 4 to 9 |
| the backbone identity | SHA-256 of the int16 payloads of sections 0 to 2 |
| the architecture | walk section 2 by child counts from the root count |
| front-end parameters | the 0x91 chain record, decoded per §14 |