Skip to content

NpyIntTensor.kt

src/jvmMain/kotlin/afield/audio/ai/NpyIntTensor.kt · 87 lines

@file:afield.runtime.completion.CompletionCategory("Audio AI")

package afield.audio.ai

import java.nio.ByteBuffer
import java.nio.ByteOrder
import java.nio.file.Files
import java.nio.file.Path

A dependency-free reader for the integer NPY tensors the audio service emits — the codec token grids, in practice.

numpy.kt already reads .npy and .npz generally; this is the narrow, self-checking version for the one shape that matters here (codebooks × frames of small integers), so the codec views can index tokens by codebook and frame without a general tensor type in between. It refuses what it cannot represent, rather than guessing: Fortran order, an unsupported dtype and a multi-item batch are all errors with a message.

data class NpyIntTensor

Small, dependency-free reader for the C-order integer NPY tensors emitted by the audio service.

data class NpyIntTensor(
    /** Array dimensions as the NPY header gave them; the last two are codebooks and time. */
    val shape: IntArray,
    /** All codes, C-order (row-major), widened to `Long` whatever the on-disk integer width was. */
    val values: LongArray,
) {

val codebooks: Int get() = shape[shape.size - 2]

Size of the second-to-last axis: how many codebooks the encoder used.

val frames: Int get() = shape.last()

Size of the last axis: how many token frames, and so the time extent.

init

    init {
        require(shape.size >= 2) { "token tensor must have at least codebook and time axes" }
        require(shape.dropLast(2).fold(1) { a, b -> a * b } == 1) {
            "only a single token batch is currently visualized"
        }
        require(values.size == shape.fold(1) { a, b -> a * b }) { "NPY shape/data size mismatch" }
    }

operator fun get(codebook: Int, frame: Int): Long = values[codebook * frames + frame]

One code, by codebook and frame. Unchecked: an out-of-range index is an array error, not null.

companion object

    companion object {

fun read(path: Path): NpyIntTensor = decode(Files.readAllBytes(path))

Read and decode a .npy file whole. See decode for what is accepted.

fun decode(bytes: ByteArray): NpyIntTensor

Decode NPY bytes, or fail with a message saying what was wrong.

Narrow: C-order only (Fortran order is rejected rather than silently transposed), i8, i4 and u2 integer dtypes only, and a single batch only — the constructor rejects a shape whose leading axes multiply to anything but 1, because a batch of token grids has no one picture. Both NPY header versions are handled, and the dtype's byte order is honoured.

        fun decode(bytes: ByteArray): NpyIntTensor {
            require(bytes.size >= 10 && bytes.copyOfRange(0, 6).contentEquals(
                byteArrayOf(0x93.toByte(), 'N'.code.toByte(), 'U'.code.toByte(), 'M'.code.toByte(),
                    'P'.code.toByte(), 'Y'.code.toByte())
            )) { "not a NumPy NPY file" }
            val major = bytes[6].toInt() and 0xff
            val headerSizeBytes = if (major == 1) 2 else 4
            val headerLength = ByteBuffer.wrap(bytes, 8, headerSizeBytes)
                .order(ByteOrder.LITTLE_ENDIAN)
                .let { if (headerSizeBytes == 2) it.short.toInt() and 0xffff else it.int }
            val headerStart = 8 + headerSizeBytes
            val header = bytes.copyOfRange(headerStart, headerStart + headerLength).toString(Charsets.US_ASCII)
            require("'fortran_order': False" in header) { "Fortran-order NPY tensors are unsupported" }
            val descriptor = Regex("['\"]descr['\"]\\s*:\\s*['\"]([^'\"]+)").find(header)
                ?.groupValues?.get(1) ?: error("NPY header has no dtype descriptor")
            val shapeText = Regex("['\"]shape['\"]\\s*:\\s*\\(([^)]*)\\)").find(header)
                ?.groupValues?.get(1) ?: error("NPY header has no shape")
            val shape = shapeText.split(',').mapNotNull { it.trim().takeIf(String::isNotEmpty)?.toInt() }.toIntArray()
            val count = shape.fold(1) { a, b -> Math.multiplyExact(a, b) }
            val data = ByteBuffer.wrap(bytes, headerStart + headerLength, bytes.size - headerStart - headerLength)
                .order(if (descriptor.startsWith(">")) ByteOrder.BIG_ENDIAN else ByteOrder.LITTLE_ENDIAN)
            val values = when {
                descriptor.endsWith("i8") -> LongArray(count) { data.long }
                descriptor.endsWith("i4") -> LongArray(count) { data.int.toLong() }
                descriptor.endsWith("u2") -> LongArray(count) { (data.short.toInt() and 0xffff).toLong() }
                else -> error("unsupported NPY token dtype $descriptor")
            }
            return NpyIntTensor(shape, values)
        }
    }
}