diff --git a/compressionkit/export/artifact_contract.py b/compressionkit/export/artifact_contract.py index db4adf7..eae737e 100644 --- a/compressionkit/export/artifact_contract.py +++ b/compressionkit/export/artifact_contract.py @@ -22,6 +22,7 @@ class ArtifactFile(StrEnum): DECODER_FLOAT32_TFLITE = "decoder_float32.tflite" DECODER_HEADER = "decoder.h" DECODER_INT8_HF_TFLITE = "decoder_int8.tflite" + DECODER_KERAS = "decoder.keras" DECODER_TFLITE = "decoder.tflite" DENOISER_GAIN_MODEL = "denoiser_gain_model.keras" DENOISER_HEADER = "denoiser_gain_model.h" @@ -30,6 +31,7 @@ class ArtifactFile(StrEnum): DEPLOY_MANIFEST = "deploy_manifest.json" ENCODER_HEADER = "encoder.h" ENCODER_INT8_HF_TFLITE = "encoder_int8.tflite" + ENCODER_KERAS = "encoder.keras" ENCODER_TFLITE = "encoder.tflite" HF_CONFIG = "config.json" HYBRID_MANIFEST = "hybrid_manifest.json" @@ -60,10 +62,12 @@ class SampleArray(StrEnum): RVQ_HF_FILE_RENAMES: tuple[tuple[ArtifactFile, ArtifactFile], ...] = ( (ArtifactFile.ENCODER_TFLITE, ArtifactFile.ENCODER_INT8_HF_TFLITE), (ArtifactFile.ENCODER_HEADER, ArtifactFile.ENCODER_HEADER), + (ArtifactFile.ENCODER_KERAS, ArtifactFile.ENCODER_KERAS), (ArtifactFile.DECODER_FLOAT32_TFLITE, ArtifactFile.DECODER_FLOAT32_TFLITE), (ArtifactFile.DECODER_TFLITE, ArtifactFile.DECODER_INT8_HF_TFLITE), (ArtifactFile.DECODER_INT8_HF_TFLITE, ArtifactFile.DECODER_INT8_HF_TFLITE), (ArtifactFile.DECODER_HEADER, ArtifactFile.DECODER_HEADER), + (ArtifactFile.DECODER_KERAS, ArtifactFile.DECODER_KERAS), (ArtifactFile.CODEBOOK_NPZ, ArtifactFile.CODEBOOK_NPZ), (ArtifactFile.CODEBOOK_HEADER, ArtifactFile.CODEBOOK_HEADER), (ArtifactFile.CODEC_SPEC, ArtifactFile.CODEC_SPEC), diff --git a/compressionkit/export/deploy.py b/compressionkit/export/deploy.py index 58c7d3e..f08a492 100644 --- a/compressionkit/export/deploy.py +++ b/compressionkit/export/deploy.py @@ -40,6 +40,7 @@ class DeploymentArtifacts: output_dir: Path encoder_tflite: Path = field(default_factory=Path) encoder_header: Path = field(default_factory=Path) + encoder_keras: Path = field(default_factory=Path) decoder_keras: Path = field(default_factory=Path) decoder_float32_tflite: Path = field(default_factory=Path) decoder_int8_tflite: Path = field(default_factory=Path) @@ -95,6 +96,7 @@ def _render_readme( * `deploy_manifest.json` — top-level package manifest. * `codec_spec.json` — canonical runtime hydration contract. * `encoder.tflite`, `encoder.h` — edge encoder artifacts. +* `encoder.keras` — float32 Python reference encoder (training/inspection use). * `codebook.npz`, `codebook.h` — RVQ codebook tables. * `decoder.keras` and optional decoder TFLite files — reconstruction artifacts. * `reference_vectors.npz` — known-good encode/decode vectors when sample inputs were exported. @@ -210,6 +212,13 @@ def export_for_deployment( artifacts.encoder_tflite = enc_tflite artifacts.encoder_header = enc_header + # 1b. Encoder (.keras -- float32 reference alongside the quantized + # encoder.tflite used on-device; mirrors the decoder.keras reference below). + logger.info("Exporting encoder as .keras...") + encoder_keras_path = output_dir / "encoder.keras" + encoder.save(encoder_keras_path) + artifacts.encoder_keras = encoder_keras_path + # 2. Decoder (.keras — runs server-side, no quantization needed) logger.info("Exporting decoder as .keras...") decoder_keras_path = output_dir / "decoder.keras" @@ -317,6 +326,7 @@ def export_for_deployment( "encoder": { "tflite": enc_tflite.name, "header": enc_header.name, + "keras": encoder_keras_path.name, "input_shape": list(encoder.input_shape), "output_shape": list(encoder.output_shape), }, @@ -390,6 +400,7 @@ def export_for_deployment( "encoder": { "tflite": enc_tflite.name, "header": enc_header.name, + "keras": encoder_keras_path.name, "input_shape": list(encoder.input_shape), "output_shape": list(encoder.output_shape), }, diff --git a/compressionkit/export/family_registry.py b/compressionkit/export/family_registry.py index 4fef886..10ea19d 100644 --- a/compressionkit/export/family_registry.py +++ b/compressionkit/export/family_registry.py @@ -95,7 +95,13 @@ class CodecFamilySpec: "rvq": CodecFamilySpec( family="rvq", loader=_load_rvq, - required_artifacts=("encoder.tflite", "codebook.npz", "codebook.h"), + required_artifacts=( + "encoder.tflite", + "encoder.keras", + "decoder.keras", + "codebook.npz", + "codebook.h", + ), release_extras=("model_card.json", "README.md", "scorecard.json", "reference_vectors.npz", "sample_data.npz"), hf_file_renames=tuple(RVQ_HF_FILE_RENAMES), has_c_sources=False, diff --git a/compressionkit/export/model_card.py b/compressionkit/export/model_card.py index a0283b8..d1180a8 100644 --- a/compressionkit/export/model_card.py +++ b/compressionkit/export/model_card.py @@ -225,8 +225,10 @@ def generate_model_card( lines.append("|------|-------------|") lines.append("| `encoder_int8.tflite` | INT8 quantized encoder (on-device) |") lines.append("| `encoder.h` | C header for encoder |") + lines.append("| `encoder.keras` | Float32 Python reference encoder (training/inspection use) |") lines.append("| `decoder_float32.tflite` | Float32 decoder (server-side evaluation) |") lines.append("| `decoder_int8.tflite` | INT8 decoder (optional, on-device) |") + lines.append("| `decoder.keras` | Float32 Python reference decoder (training/inspection use) |") lines.append("| `codebook.npz` | RVQ codebook tables |") lines.append("| `codebook.h` | C header for codebook |") lines.append("| `config.json` | Deployment manifest |") diff --git a/docs/deployment.md b/docs/deployment.md index 1030ac9..4379f08 100644 --- a/docs/deployment.md +++ b/docs/deployment.md @@ -27,6 +27,7 @@ For most users, the only runtime object you need is `compressionkit.runtime.RVQC | `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.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 | | `decoder.tflite` | Optional | INT8 LiteRT decoder for full on-device reconstruction. | MCU / edge device | diff --git a/docs/experiments/ecg-rvq-16x.md b/docs/experiments/ecg-rvq-16x.md index ad06879..ab69f33 100644 --- a/docs/experiments/ecg-rvq-16x.md +++ b/docs/experiments/ecg-rvq-16x.md @@ -49,7 +49,9 @@ Each run writes `quality_scorecard.json` and `summary.json` under its `results/< Every successful run produces the canonical edge deploy package: - `encoder.tflite` / `encoder.h` — INT8 encoder. +- `encoder.keras` — float32 Python reference encoder. - `decoder.tflite` / `decoder.h` — decoder (float32 + optional INT8). +- `decoder.keras` — float32 Python reference decoder. - `codebook.npz` / `codebook.h` — RVQ codebook tables. - `sample_stimulus.npz` — license-safe input/output reference frames. - `model_card.json`, `deploy_manifest.json` — metadata. diff --git a/docs/experiments/ecg-rvq-2x.md b/docs/experiments/ecg-rvq-2x.md index 73036f4..9df4cb2 100644 --- a/docs/experiments/ecg-rvq-2x.md +++ b/docs/experiments/ecg-rvq-2x.md @@ -49,7 +49,9 @@ Each run writes `quality_scorecard.json` and `summary.json` under its `results/< Every successful run produces the canonical edge deploy package: - `encoder.tflite` / `encoder.h` — INT8 encoder. +- `encoder.keras` — float32 Python reference encoder. - `decoder.tflite` / `decoder.h` — decoder (float32 + optional INT8). +- `decoder.keras` — float32 Python reference decoder. - `codebook.npz` / `codebook.h` — RVQ codebook tables. - `sample_stimulus.npz` — license-safe input/output reference frames. - `model_card.json`, `deploy_manifest.json` — metadata. diff --git a/docs/experiments/ecg-rvq-32x.md b/docs/experiments/ecg-rvq-32x.md index 43e0c89..141fc45 100644 --- a/docs/experiments/ecg-rvq-32x.md +++ b/docs/experiments/ecg-rvq-32x.md @@ -49,7 +49,9 @@ Each run writes `quality_scorecard.json` and `summary.json` under its `results/< Every successful run produces the canonical edge deploy package: - `encoder.tflite` / `encoder.h` — INT8 encoder. +- `encoder.keras` — float32 Python reference encoder. - `decoder.tflite` / `decoder.h` — decoder (float32 + optional INT8). +- `decoder.keras` — float32 Python reference decoder. - `codebook.npz` / `codebook.h` — RVQ codebook tables. - `sample_stimulus.npz` — license-safe input/output reference frames. - `model_card.json`, `deploy_manifest.json` — metadata. diff --git a/docs/experiments/ecg-rvq-4x-prior.md b/docs/experiments/ecg-rvq-4x-prior.md index f2e1a3d..1e96a26 100644 --- a/docs/experiments/ecg-rvq-4x-prior.md +++ b/docs/experiments/ecg-rvq-4x-prior.md @@ -55,7 +55,9 @@ Each run writes `quality_scorecard.json` and `summary.json` under its `results/< Every successful run produces the canonical edge deploy package: - `encoder.tflite` / `encoder.h` — INT8 encoder. +- `encoder.keras` — float32 Python reference encoder. - `decoder.tflite` / `decoder.h` — decoder (float32 + optional INT8). +- `decoder.keras` — float32 Python reference decoder. - `codebook.npz` / `codebook.h` — RVQ codebook tables. - `sample_stimulus.npz` — license-safe input/output reference frames. - `model_card.json`, `deploy_manifest.json` — metadata. diff --git a/docs/experiments/ecg-rvq-4x.md b/docs/experiments/ecg-rvq-4x.md index ab77d52..c44030b 100644 --- a/docs/experiments/ecg-rvq-4x.md +++ b/docs/experiments/ecg-rvq-4x.md @@ -54,7 +54,9 @@ Each run writes `quality_scorecard.json` and `summary.json` under its `results/< Every successful run produces the canonical edge deploy package: - `encoder.tflite` / `encoder.h` — INT8 encoder. +- `encoder.keras` — float32 Python reference encoder. - `decoder.tflite` / `decoder.h` — decoder (float32 + optional INT8). +- `decoder.keras` — float32 Python reference decoder. - `codebook.npz` / `codebook.h` — RVQ codebook tables. - `sample_stimulus.npz` — license-safe input/output reference frames. - `model_card.json`, `deploy_manifest.json` — metadata. diff --git a/docs/experiments/ecg-rvq-64x.md b/docs/experiments/ecg-rvq-64x.md index f72d32a..733df97 100644 --- a/docs/experiments/ecg-rvq-64x.md +++ b/docs/experiments/ecg-rvq-64x.md @@ -49,7 +49,9 @@ Each run writes `quality_scorecard.json` and `summary.json` under its `results/< Every successful run produces the canonical edge deploy package: - `encoder.tflite` / `encoder.h` — INT8 encoder. +- `encoder.keras` — float32 Python reference encoder. - `decoder.tflite` / `decoder.h` — decoder (float32 + optional INT8). +- `decoder.keras` — float32 Python reference decoder. - `codebook.npz` / `codebook.h` — RVQ codebook tables. - `sample_stimulus.npz` — license-safe input/output reference frames. - `model_card.json`, `deploy_manifest.json` — metadata. diff --git a/docs/experiments/ecg-rvq-8x-prior.md b/docs/experiments/ecg-rvq-8x-prior.md index a33bcf7..2798df9 100644 --- a/docs/experiments/ecg-rvq-8x-prior.md +++ b/docs/experiments/ecg-rvq-8x-prior.md @@ -55,7 +55,9 @@ Each run writes `quality_scorecard.json` and `summary.json` under its `results/< Every successful run produces the canonical edge deploy package: - `encoder.tflite` / `encoder.h` — INT8 encoder. +- `encoder.keras` — float32 Python reference encoder. - `decoder.tflite` / `decoder.h` — decoder (float32 + optional INT8). +- `decoder.keras` — float32 Python reference decoder. - `codebook.npz` / `codebook.h` — RVQ codebook tables. - `sample_stimulus.npz` — license-safe input/output reference frames. - `model_card.json`, `deploy_manifest.json` — metadata. diff --git a/docs/experiments/ecg-rvq-8x.md b/docs/experiments/ecg-rvq-8x.md index 259f8b2..9825126 100644 --- a/docs/experiments/ecg-rvq-8x.md +++ b/docs/experiments/ecg-rvq-8x.md @@ -54,7 +54,9 @@ Each run writes `quality_scorecard.json` and `summary.json` under its `results/< Every successful run produces the canonical edge deploy package: - `encoder.tflite` / `encoder.h` — INT8 encoder. +- `encoder.keras` — float32 Python reference encoder. - `decoder.tflite` / `decoder.h` — decoder (float32 + optional INT8). +- `decoder.keras` — float32 Python reference decoder. - `codebook.npz` / `codebook.h` — RVQ codebook tables. - `sample_stimulus.npz` — license-safe input/output reference frames. - `model_card.json`, `deploy_manifest.json` — metadata. diff --git a/docs/experiments/ppg-rvq-16x.md b/docs/experiments/ppg-rvq-16x.md index bf831b5..23b5212 100644 --- a/docs/experiments/ppg-rvq-16x.md +++ b/docs/experiments/ppg-rvq-16x.md @@ -49,7 +49,9 @@ Each run writes `quality_scorecard.json` and `summary.json` under its `results/< Every successful run produces the canonical edge deploy package: - `encoder.tflite` / `encoder.h` — INT8 encoder. +- `encoder.keras` — float32 Python reference encoder. - `decoder.tflite` / `decoder.h` — decoder (float32 + optional INT8). +- `decoder.keras` — float32 Python reference decoder. - `codebook.npz` / `codebook.h` — RVQ codebook tables. - `sample_stimulus.npz` — license-safe input/output reference frames. - `model_card.json`, `deploy_manifest.json` — metadata. diff --git a/docs/experiments/ppg-rvq-2x.md b/docs/experiments/ppg-rvq-2x.md index adfbf6a..193faa8 100644 --- a/docs/experiments/ppg-rvq-2x.md +++ b/docs/experiments/ppg-rvq-2x.md @@ -49,7 +49,9 @@ Each run writes `quality_scorecard.json` and `summary.json` under its `results/< Every successful run produces the canonical edge deploy package: - `encoder.tflite` / `encoder.h` — INT8 encoder. +- `encoder.keras` — float32 Python reference encoder. - `decoder.tflite` / `decoder.h` — decoder (float32 + optional INT8). +- `decoder.keras` — float32 Python reference decoder. - `codebook.npz` / `codebook.h` — RVQ codebook tables. - `sample_stimulus.npz` — license-safe input/output reference frames. - `model_card.json`, `deploy_manifest.json` — metadata. diff --git a/docs/experiments/ppg-rvq-32x.md b/docs/experiments/ppg-rvq-32x.md index 7c38a9d..59840b7 100644 --- a/docs/experiments/ppg-rvq-32x.md +++ b/docs/experiments/ppg-rvq-32x.md @@ -49,7 +49,9 @@ Each run writes `quality_scorecard.json` and `summary.json` under its `results/< Every successful run produces the canonical edge deploy package: - `encoder.tflite` / `encoder.h` — INT8 encoder. +- `encoder.keras` — float32 Python reference encoder. - `decoder.tflite` / `decoder.h` — decoder (float32 + optional INT8). +- `decoder.keras` — float32 Python reference decoder. - `codebook.npz` / `codebook.h` — RVQ codebook tables. - `sample_stimulus.npz` — license-safe input/output reference frames. - `model_card.json`, `deploy_manifest.json` — metadata. diff --git a/docs/experiments/ppg-rvq-4x-prior.md b/docs/experiments/ppg-rvq-4x-prior.md index 46e8e90..c368156 100644 --- a/docs/experiments/ppg-rvq-4x-prior.md +++ b/docs/experiments/ppg-rvq-4x-prior.md @@ -55,7 +55,9 @@ Each run writes `quality_scorecard.json` and `summary.json` under its `results/< Every successful run produces the canonical edge deploy package: - `encoder.tflite` / `encoder.h` — INT8 encoder. +- `encoder.keras` — float32 Python reference encoder. - `decoder.tflite` / `decoder.h` — decoder (float32 + optional INT8). +- `decoder.keras` — float32 Python reference decoder. - `codebook.npz` / `codebook.h` — RVQ codebook tables. - `sample_stimulus.npz` — license-safe input/output reference frames. - `model_card.json`, `deploy_manifest.json` — metadata. diff --git a/docs/experiments/ppg-rvq-4x.md b/docs/experiments/ppg-rvq-4x.md index 2209e8e..62958a7 100644 --- a/docs/experiments/ppg-rvq-4x.md +++ b/docs/experiments/ppg-rvq-4x.md @@ -54,7 +54,9 @@ Each run writes `quality_scorecard.json` and `summary.json` under its `results/< Every successful run produces the canonical edge deploy package: - `encoder.tflite` / `encoder.h` — INT8 encoder. +- `encoder.keras` — float32 Python reference encoder. - `decoder.tflite` / `decoder.h` — decoder (float32 + optional INT8). +- `decoder.keras` — float32 Python reference decoder. - `codebook.npz` / `codebook.h` — RVQ codebook tables. - `sample_stimulus.npz` — license-safe input/output reference frames. - `model_card.json`, `deploy_manifest.json` — metadata. diff --git a/docs/experiments/ppg-rvq-8x-prior.md b/docs/experiments/ppg-rvq-8x-prior.md index fdf9497..c7ca798 100644 --- a/docs/experiments/ppg-rvq-8x-prior.md +++ b/docs/experiments/ppg-rvq-8x-prior.md @@ -55,7 +55,9 @@ Each run writes `quality_scorecard.json` and `summary.json` under its `results/< Every successful run produces the canonical edge deploy package: - `encoder.tflite` / `encoder.h` — INT8 encoder. +- `encoder.keras` — float32 Python reference encoder. - `decoder.tflite` / `decoder.h` — decoder (float32 + optional INT8). +- `decoder.keras` — float32 Python reference decoder. - `codebook.npz` / `codebook.h` — RVQ codebook tables. - `sample_stimulus.npz` — license-safe input/output reference frames. - `model_card.json`, `deploy_manifest.json` — metadata. diff --git a/docs/experiments/ppg-rvq-8x.md b/docs/experiments/ppg-rvq-8x.md index 088d7ca..9c62f9c 100644 --- a/docs/experiments/ppg-rvq-8x.md +++ b/docs/experiments/ppg-rvq-8x.md @@ -54,7 +54,9 @@ Each run writes `quality_scorecard.json` and `summary.json` under its `results/< Every successful run produces the canonical edge deploy package: - `encoder.tflite` / `encoder.h` — INT8 encoder. +- `encoder.keras` — float32 Python reference encoder. - `decoder.tflite` / `decoder.h` — decoder (float32 + optional INT8). +- `decoder.keras` — float32 Python reference decoder. - `codebook.npz` / `codebook.h` — RVQ codebook tables. - `sample_stimulus.npz` — license-safe input/output reference frames. - `model_card.json`, `deploy_manifest.json` — metadata. diff --git a/docs/models/ecg.md b/docs/models/ecg.md index 2503479..f644ce0 100644 --- a/docs/models/ecg.md +++ b/docs/models/ecg.md @@ -156,6 +156,7 @@ results/ecg_rvq_256hz_08x_golden/ └── deploy/ # Deployment artifacts ├── encoder.tflite # INT8 quantized encoder ├── encoder.h # C header for encoder + ├── encoder.keras # Keras encoder model ├── decoder.keras # Keras decoder model ├── decoder_float32.tflite # Float32 LiteRT decoder for host-side decode ├── decoder.tflite # Optional INT8 decoder for on-device decode diff --git a/docs/models/ppg.md b/docs/models/ppg.md index 1faf188..c91b55f 100644 --- a/docs/models/ppg.md +++ b/docs/models/ppg.md @@ -172,6 +172,7 @@ results/ppg_rvq_64hz_08x_golden/ └── deploy/ # Deployment artifacts ├── encoder.tflite # INT8 quantized encoder ├── encoder.h # C header for encoder + ├── encoder.keras # Keras encoder model ├── decoder.keras # Keras decoder model ├── decoder_float32.tflite # Float32 LiteRT decoder for host-side decode ├── decoder.tflite # Optional INT8 decoder for on-device decode diff --git a/docs/release-contract.md b/docs/release-contract.md index 509f70b..01242af 100644 --- a/docs/release-contract.md +++ b/docs/release-contract.md @@ -97,6 +97,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.keras` | Host reference encode | | `decoder.keras` or `decoder_float32.tflite` | Host reference decode | | `decoder.tflite` and `decoder.h` | Optional on-device decode | | `codebook.npz` | Python codebook tables | diff --git a/scripts/render_experiment_docs.py b/scripts/render_experiment_docs.py index 72de313..e8b45d1 100644 --- a/scripts/render_experiment_docs.py +++ b/scripts/render_experiment_docs.py @@ -223,7 +223,9 @@ def _render_experiment(exp: GoldenExperiment) -> str: if exp.method == "rvq": lines += [ "- `encoder.tflite` / `encoder.h` — INT8 encoder.", + "- `encoder.keras` — float32 Python reference encoder.", "- `decoder.tflite` / `decoder.h` — decoder (float32 + optional INT8).", + "- `decoder.keras` — float32 Python reference decoder.", "- `codebook.npz` / `codebook.h` — RVQ codebook tables.", "- `sample_stimulus.npz` — license-safe input/output reference frames.", "- `model_card.json`, `deploy_manifest.json` — metadata.", diff --git a/tests/test_deploy_contract.py b/tests/test_deploy_contract.py index b1c227e..bf5681b 100644 --- a/tests/test_deploy_contract.py +++ b/tests/test_deploy_contract.py @@ -52,6 +52,8 @@ def test_strict_rvq_contract_accepts_complete_file_set(tmp_path) -> None: _write_minimal_manifest(tmp_path, family="rvq") for rel in [ "encoder.tflite", + "encoder.keras", + "decoder.keras", "codebook.npz", "codebook.h", "model_card.json", @@ -74,6 +76,8 @@ def test_strict_rvq_contract_accepts_complete_file_set(tmp_path) -> None: assert result.warnings == [] assert "scorecard.json" in result.checked_files assert "sample_data.npz" in result.checked_files + assert "encoder.keras" in result.checked_files + assert "decoder.keras" in result.checked_files def test_strict_spiht_contract_accepts_complete_file_set(tmp_path) -> None: