Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 4 additions & 4 deletions .devcontainer/devcontainer.json
Original file line number Diff line number Diff line change
Expand Up @@ -36,18 +36,18 @@
// ],

"mounts": [
"source=/data/datasets,target=/home/vscode/datasets,type=bind,consistency=cached"
"source=/data/datasets,target=/home/vscode/datasets,type=bind,consistency=cached",
"source=compressionkit-huggingface,target=/home/vscode/.cache/huggingface,type=volume"
],

"forwardPorts": [6006],

"postCreateCommand": "./.devcontainer/install.sh",
"postCreateCommand": "sudo install -d -o vscode -g vscode -m 700 /home/vscode/.cache /home/vscode/.cache/huggingface && ./.devcontainer/install.sh",

"remoteEnv": {
"LD_LIBRARY_PATH": "${containerEnv:LD_LIBRARY_PATH}:/usr/local/cuda/lib64",
"PATH": "${containerEnv:PATH}:/usr/local/cuda/bin",
"TF_FORCE_GPU_ALLOW_GROWTH": "true",
"HF_TOKEN": "${localEnv:HF_TOKEN}"
"TF_FORCE_GPU_ALLOW_GROWTH": "true"
},

"customizations": {
Expand Down
21 changes: 18 additions & 3 deletions .devcontainer/install.sh
Original file line number Diff line number Diff line change
@@ -1,9 +1,24 @@
#!/bin/bash

set -euo pipefail

export DEBIAN_FRONTEND=noninteractive

sudo apt update
sudo apt install -y \
apt_retry() {
local attempts=0
until sudo apt-get "$@"; do
attempts=$((attempts + 1))
if (( attempts >= 24 )); then
echo "apt-get failed after ${attempts} attempts: $*" >&2
return 1
fi
echo "apt-get is unavailable; retrying in 5 seconds (${attempts}/24)..." >&2
sleep 5
done
}

apt_retry update
apt_retry install -y \
build-essential \
cmake \
make \
Expand All @@ -15,7 +30,7 @@ sudo apt install -y \
git-lfs

# Optional: reduce image size a bit
sudo apt clean
sudo apt-get clean
sudo rm -rf /var/lib/apt/lists/*

git lfs install
Expand Down
31 changes: 31 additions & 0 deletions astro-site/src/content/docs/deployment.md
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,7 @@ Use `load_codec()` for the common interface across families. The lower-level exa
| `deploy_manifest.json` | Yes | Declares file names, tensor shapes, quantization mode, and codebook metadata. | Runtime metadata |
| `encoder.tflite` | Yes | INT8 LiteRT encoder used to produce continuous latents from input frames. | MCU / edge device |
| `encoder.h` | Yes | C header for the quantized encoder blob. | MCU firmware |
| `encoder_float32.tflite` | Yes | Float32 LiteRT encoder for browser and host runtimes without Keras. | Browser / x86 / ARM Linux |
| `encoder.keras` | Yes | Reference encoder kept in Keras format. | Server / offline tools |
| `decoder.keras` | Yes | Reference decoder kept in Keras format. | Server / offline tools |
| `decoder_float32.tflite` | Optional, exported by default | Float32 LiteRT decoder for host-side reconstruction without Keras. | x86 / ARM Linux |
Expand All @@ -63,10 +64,40 @@ Use `load_codec()` for the common interface across families. The lower-level exa
| `codebook.npz` | Yes | NumPy archive containing RVQ codebook tables. | Python runtime |
| `codebook.h` | Yes | C header containing RVQ codebook tables. | MCU firmware |
| `sample_data.npz` | Optional | Synthetic or evaluation sample inputs / targets / reconstructions. | Validation / demos |
| `demo_recordings.npz` | Release | Ten real, quality-gated continuous recordings, resampled to the model rate. | Browser demos |
| `demo_recordings_manifest.json` | Release | Dataset attribution, license, source offsets, and quality metrics for the demo recordings. | Browser demos / compliance |
| `model_card.json` | Optional | Metadata used when publishing to HuggingFace. | Release tooling |

The manifest is the source of truth. The runtime reads it first, then resolves the encoder, optional decoder, and codebook files from the names listed there.

## Encoder Preprocessing and INT8 Parity

Both RVQ encoders accept one normalized frame: PPG is `(1, 1, 320, 1)` at 64 Hz and ECG is `(1, 1, 512, 1)` at 256 Hz. Resample and frame the raw single-channel recording first, then normalize each frame independently:

```python
mean = frame.mean()
scale = np.sqrt(np.mean((frame - mean) ** 2) + 1e-3)
normalized = (frame - mean) / scale
```

Retain `mean` and `scale` to return a decoded frame to its original units: `raw = decoded * scale + mean`. Do not feed raw ADC or continuous values directly to `encoder_int8.tflite`; its calibration domain is this training-time normalized representation.

Every INT8 RVQ export reservoir-samples up to 65,536 real preprocessed validation frames, using 4,096 frames for LiteRT calibration and a disjoint 2,048-frame holdout for `quantization_report.json`. The release gate requires at most 1% input saturation on that holdout. A P90 reconstruction PRD target of 10% against the float32 encoder remains a quality recommendation. Worst-frame PRD remains a 15% tail-risk warning in the report, rather than a release blocker, because isolated RVQ decision-boundary crossings can be disproportionate. `compressionkit golden validate-all --strict-release` and the Hugging Face publisher reject INT8 RVQ releases without a passed report.

## Real Browser-Demo Recordings

RVQ releases carry ten 30-second real recordings in `demo_recordings.npz`: BIDMC PPG at 64 Hz and MIT-BIH ECG at 256 Hz. The companion manifest retains ODC-By attribution and the quality gate results. Signals are continuous source waveforms after resampling; apply the model's normal framing and normalization before inference.

Refresh local bundles before a manual Hugging Face update:

```bash
scripts/devcontainer.sh exec -- uv run python scripts/attach_rvq_demo_recordings.py --modality all
```

To update only validated release candidates, repeat `--experiment-id`, for example `--experiment-id ecg-rvq-2x --experiment-id ecg-rvq-4x`.

Use `--duration-seconds 120` for two-minute clips. The command updates only existing `results/*/deploy/` packages, their manifests, and checksums; it does not train or re-export models.

## Local Runtime Quickstart

Use the source-checkout installation from [Getting started](/compressionkit/getting-started/#1-install). LiteRT executes the exported models, but importing the package also loads modules with Keras and other development dependencies; this is not a separately packaged minimal runtime.
Expand Down
23 changes: 21 additions & 2 deletions astro-site/src/content/docs/huggingface.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,8 +21,8 @@ for the `snapshot_download` / `from_pretrained` calls below.

## 2. Single-stage codec

Single-stage repos contain `encoder_int8.tflite`, `decoder_int8.tflite`, `codebook.npz`, and
`sample_stimulus.npz`. [`RVQCodec.from_pretrained`](/compressionkit/reference/api/compressionkit/runtime/codec/#compressionkit.runtime.codec.RVQCodec.from_pretrained) downloads the bundle and wires
Single-stage repos contain `encoder_int8.tflite`, `encoder_float32.tflite`, `decoder_int8.tflite`,
`codebook.npz`, `sample_stimulus.npz`, and `demo_recordings.npz`. The float32 encoder supports browser and host LiteRT integrations with float32 I/O. [`RVQCodec.from_pretrained`](/compressionkit/reference/api/compressionkit/runtime/codec/#compressionkit.runtime.codec.RVQCodec.from_pretrained) downloads the bundle and wires
up the LiteRT interpreters. Use the full source-checkout installation above: package imports also require Keras and evaluation dependencies.

```python
Expand All @@ -43,6 +43,25 @@ print("shape:", recon.shape)
```


`demo_recordings.npz` holds ten real, quality-gated continuous examples at the model rate (`signals` has shape `(10, samples)`). Read `demo_recordings_manifest.json` with it: the manifest records source provenance, ODC-By attribution, signal offsets, and the quality measurements used for selection. Apply the model's usual framing and normalization before inference.

For each 64 Hz PPG 320-sample frame or 256 Hz ECG 512-sample frame, use per-frame layer normalization before either encoder variant:

```python
mean = frame.mean()
scale = np.sqrt(np.mean((frame - mean) ** 2) + 1e-3)
encoder_input = (frame - mean) / scale
```

For display in raw units, undo it after decoding with `decoded * scale + mean`. INT8 RVQ releases produced under the current release policy include `quantization_report.json`, which records parity against `encoder_float32.tflite` on a 2,048-frame real-preprocessed holdout distinct from the 4,096 frames used for LiteRT calibration.

To refresh all local RVQ bundles before publishing an asset update:

```bash
scripts/devcontainer.sh exec -- uv run python scripts/attach_rvq_demo_recordings.py --modality all
```


:::note[Note]
Use `RVQCodec.from_pretrained(repo_id)` rather than `RVQCodec(local_dir)` on a raw
`snapshot_download` directory. HuggingFace bundles store the manifest as `config.json`
Expand Down
1 change: 1 addition & 0 deletions astro-site/src/content/docs/methods/rvq.md
Original file line number Diff line number Diff line change
Expand Up @@ -105,5 +105,6 @@ The trained encoder is exported as:

- **`encoder.tflite`** — INT8 quantized TFLite model for on-device inference
- **`encoder.h`** — C header with the model weights as a byte array
- **`encoder_float32.tflite`** — FP32 LiteRT encoder for browser and host integrations

The decoder and RVQ codebooks are stored separately for server-side reconstruction. A split deployment runs the encoder and codebook lookup on the device and transmits indices to a separate decoder.
1 change: 1 addition & 0 deletions astro-site/src/content/docs/release-contract.md
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ AI packages may expose their demo frames as `sample_data.npz` when the file carr
|------|---------|
| `encoder.tflite` | Edge encoder |
| `encoder.h` | Embedded encoder header |
| `encoder_float32.tflite` | Browser and host LiteRT encoder |
| `encoder.keras` | Host reference encode |
| `decoder.keras` or `decoder_float32.tflite` | Host reference decode |
| `decoder.tflite` and `decoder.h` | Optional on-device decode |
Expand Down
16 changes: 15 additions & 1 deletion compressionkit/configs/ecg_rvq.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

from __future__ import annotations

from pydantic import BaseModel, Field
from pydantic import BaseModel, Field, model_validator

from compressionkit.configs.paths import default_datasets_dir

Expand Down Expand Up @@ -483,10 +483,24 @@ class EvaluationConfig(BaseModel):
Capped at ``num_samples``. Plots are slow to render and large to ship,
so this is typically much smaller than ``num_samples``."""
tflite_rep_batches: int = 8
"""Legacy representative-batch setting; retained for config compatibility."""
int8_calibration_frames: int = Field(default=4096, gt=0)
"""Real normalized validation frames used to calibrate INT8 LiteRT ranges."""
int8_validation_frames: int = Field(default=2048, gt=0)
"""Disjoint real validation frames used to measure INT8 encoder parity."""
int8_sampling_pool_frames: int = Field(default=65_536, gt=0)
"""Maximum validation frames reservoir-sampled for quantization partitions."""
input_bit_depth: int = 16
band_metrics: BandMetricsConfig = Field(default_factory=BandMetricsConfig)
stitching: StitchingEvalConfig = Field(default_factory=StitchingEvalConfig)

@model_validator(mode="after")
def validate_quantization_partition(self) -> EvaluationConfig:
"""Require the sampling pool to cover both disjoint partitions."""
if self.int8_sampling_pool_frames < self.int8_calibration_frames + self.int8_validation_frames:
raise ValueError("int8_sampling_pool_frames must cover calibration plus validation frames")
return self


class WandbConfig(BaseModel):
"""Weights & Biases logging configuration."""
Expand Down
16 changes: 15 additions & 1 deletion compressionkit/configs/ppg_rvq.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

from __future__ import annotations

from pydantic import BaseModel, Field
from pydantic import BaseModel, Field, model_validator

from compressionkit.configs.artifact_suite import ArtifactSuiteConfig
from compressionkit.configs.paths import default_datasets_dir
Expand Down Expand Up @@ -311,11 +311,25 @@ class EvaluationConfig(BaseModel):
num_plot_samples: int = 50
"""Subset of ``num_samples`` that also receive a PNG plot artifact."""
tflite_rep_batches: int = 8
"""Legacy representative-batch setting; retained for config compatibility."""
int8_calibration_frames: int = Field(default=4096, gt=0)
"""Real normalized validation frames used to calibrate INT8 LiteRT ranges."""
int8_validation_frames: int = Field(default=2048, gt=0)
"""Disjoint real validation frames used to measure INT8 encoder parity."""
int8_sampling_pool_frames: int = Field(default=65_536, gt=0)
"""Maximum validation frames reservoir-sampled for quantization partitions."""
input_bit_depth: int = 16
band_metrics: BandMetricsConfig = Field(default_factory=BandMetricsConfig)
physiokit_metrics: PhysiokitMetricsConfig = Field(default_factory=PhysiokitMetricsConfig)
long_recording: LongRecordingEvalConfig = Field(default_factory=LongRecordingEvalConfig)

@model_validator(mode="after")
def validate_quantization_partition(self) -> EvaluationConfig:
"""Require the sampling pool to cover both disjoint partitions."""
if self.int8_sampling_pool_frames < self.int8_calibration_frames + self.int8_validation_frames:
raise ValueError("int8_sampling_pool_frames must cover calibration plus validation frames")
return self


class WandbConfig(BaseModel):
"""Weights & Biases logging configuration."""
Expand Down
Loading
Loading