Skip to main content

koji-ingest

Ingestion pipeline for Kōji — document parsing, chunking, multi-vector embeddings, and VLM enrichment.

koji-ingest transforms raw documents (PDF, DOCX, PPTX, audio, images, and more) into ColBERT-style multi-vector embeddings ready for storage and semantic search in Kōji.

Installation

The distribution is published to PyPI as koji-ingest; the import module is koji_ingest.

# Core ingestion (parsing, chunking, embeddings)
pip install koji-ingest

# With document parsing support (Docling, PDF)
pip install "koji-ingest[document]"

# With LibreOffice-based rendering (DOCX/PPTX page images)
pip install "koji-ingest[rendering]"

# With MLX VLM enrichment engine (Apple Silicon)
pip install "koji-ingest[enrichment]"

# With Kōji storage integration (PyArrow)
pip install "koji-ingest[koji]"

# With the ColNomic multi-vector engine (colpali-engine + torch + transformers)
pip install "koji-ingest[colnomic]"

# Linux + NVIDIA only: enable 4bit/8bit quantization (bitsandbytes)
pip install "koji-ingest[colnomic,cuda]"

Embedding-engine install matrix

Platform Install Supported quantization
Apple Silicon (MPS) pip install "koji-ingest[colnomic]" fp16, bf16, fp32
CPU-only pip install "koji-ingest[colnomic]" fp16, bf16, fp32
Linux + NVIDIA CUDA pip install "koji-ingest[colnomic,cuda]" fp16, bf16, fp32, 4bit, 8bit

bitsandbytes is CUDA-only, so the [cuda] extra is a no-op on macOS — requesting quantization="4bit" on a non-CUDA device raises a clear error at config-validation time rather than at model load.

Features

Multi-Vector Embeddings

koji-ingest generates ColBERT-style multi-vector embeddings where each document is represented by a set of per-token vectors, enabling MaxSim late-interaction scoring:

from koji_ingest import MultiVectorEmbedding
import numpy as np

data = np.random.randn(10, 128).astype(np.float32)
emb = MultiVectorEmbedding(num_tokens=10, dim=128, data=data)

blob = emb.to_blob()          # serialize for Lance storage
recovered = MultiVectorEmbedding.from_blob(blob)

Document Parsing and Chunking

Parse and chunk documents in a variety of formats via Docling:

from koji_ingest import parse, chunk, IngestConfig

config = IngestConfig()
parsed = parse("report.pdf", config=config)
chunks = chunk(parsed, config=config)

Supported formats include PDF, DOCX, PPTX, Markdown, HTML, images, and audio files.

DenseTextEngine (Lightweight Embedding)

For text-only workloads without vision, use DenseTextEngine backed by mlx-embeddings:

from koji_ingest import DenseTextEngine

engine = DenseTextEngine()
embedding = engine.embed("The quick brown fox")

Bring your own vector

koji-memory and koji-server take a Vec<f32> from the caller at every layer and never compute one (koji-memory/src/lib.rs NewMemory::new, RecallQuery::new; koji-server/src/routes/memory.rs). The embedding convention is therefore the caller's responsibility, on both sides of the wire — nothing downstream will notice if your indexing path and your retrieval path disagree.

The convention is published as data, not code: src/koji_ingest/embedding/model_conventions.json is the source of truth. It gives each model its dim, max_seq_length, query_prefix, document_prefix, normalization, and separation_floor, and it ships inside the wheel.

In Python, call embed_document at ingest time and embed_query at search time — and nothing else:

from koji_ingest.embedding import embed_document, embed_query, close_engines

vectors = await embed_document(["chunk one", "chunk two"])  # index these
query_vector = await embed_query("what did the chunk say?")  # search with this
close_engines()  # releases the cached model

Both refuse a model the table does not cover, raising ConventionError before any model is loaded. There is deliberately no allow_unlisted parameter on this surface: a caller who wants a bare model constructs DenseTextEngine(allow_unlisted=True) explicitly and owns that decision. No helper that guesses a convention is exported from koji_ingest.embedding.

In another language, read the JSON and reproduce the same two call sites: apply document_prefix to text you index and query_prefix to text you search with, and never the same prefix to both. You do not need to install koji-ingest or its MLX extra to read the table — vendor or fetch the file.

Then check your own configuration:

from koji_ingest.embedding import measure_separation, load_convention

report = await measure_separation(...)    # your matched and unrelated pairs
floor = load_convention("BAAI/bge-small-en-v1.5").separation_floor
assert report.gap >= floor

The pass mark is the separation_floor from the table, not an absolute score. That is the failure mode this prevents: stamping one query token onto both sides (in the origin case, E5's query: on a BGE model, at ingest and at search) produces higher absolute match scores while separating matched from unrelated pairs worse, so a "do relevant things score high" check passes the broken setup. The regression test that pins this is tests/test_embedding_separation.py.

Note that koji-memory gains no embedding hook from any of this, and is not meant to: callable hooks cannot cross the HTTP boundary, so the memory surface stays stateless and vector-taking (koji-server/src/routes/memory.rs).

Gemma 4 E4B VLM Enrichment (Apple Silicon)

Augment parsed documents with VLM-generated descriptions, code analysis, formula interpretations, and document summaries using the Gemma 4 E4B model via mlx-vlm:

from koji_ingest.enrichment import GemmaEnrichmentEngine

engine = GemmaEnrichmentEngine()
enriched = await engine.enrich(parsed_content)
# enriched.figures now contain model-generated captions
# enriched.summary contains a document-level summary

GPU / Metal Memory Management

Release cached GPU or Metal memory between batch operations to prevent OOM errors:

from koji_ingest.gpu import release_gpu_memory

for batch in large_corpus:
    process(batch)
    release_gpu_memory()   # returns allocations to OS after each batch

LibreOffice Page Renderer

Convert DOCX and PPTX slides to page images for visual ingestion:

from koji_ingest.parser import parse

# Renders pages via LibreOffice headless, returns per-page images in ParsedContent
result = parse("slides.pptx", config=config)

Audio Chunking

Ingest long-form audio with automatic chunking:

from koji_ingest import IngestConfig, AudioConfig

config = IngestConfig(audio=AudioConfig(chunk_duration_secs=30))
result = parse("interview.mp3", config=config)

Full Ingestion Pipeline

from koji_ingest import Ingester, IngestConfig

ingester = Ingester(config=IngestConfig())
result = await ingester.ingest("document.pdf")

print(result.chunks)       # List[TextChunk]
print(result.embeddings)   # List[MultiVectorEmbedding]

Running on Modal

Remote execution moves the GPU stages onto a Modal container while the database stays on your machine. Nothing below changes the default: without these settings the pipeline is entirely local and modal is never imported.

1. Install the extra

pip install "koji-ingest[modal]"

2. Authenticate

modal setup

3. Create the HuggingFace token secret

Gemma is a gated checkpoint, so the container needs a token. Create a Modal secret named koji-hf-token carrying HF_TOKEN:

modal secret create koji-hf-token HF_TOKEN=hf_xxxxxxxxxxxxxxxx

4. Deploy the app

modal deploy -m koji_ingest.modal_app

The deploy pins the published koji-ingest from PyPI so the container's serialization format cannot drift from your client. To deploy your working checkout instead — for developing against unreleased changes — set KOJI_MODAL_DEV=1:

KOJI_MODAL_DEV=1 modal deploy -m koji_ingest.modal_app

5. Warm the weights volume, once

modal run -m koji_ingest.modal_app::download_weights

This fills the koji-hf-cache volume so containers start without re-downloading model weights. Run it once per deployment target, not per ingest.

Entry point 1 — ModalEmbeddingWorker (embedding only)

Select the Modal embedding backend from config and let Ingester.from_config build the coordinator for you. Only the embedding calls route to the deployed class: parsing, transcription, and enrichment still run locally, so chunk text (and page images when visual embedding is on) is all that leaves the machine on this path:

from koji_ingest import Ingester, IngestConfig

ingester = Ingester.from_config(
    IngestConfig(embedding_backend="modal")
)
result = await ingester.process("document.pdf")

The same worker serves query-time embedding, so a query vector is produced by the identical model that produced the stored vectors:

from koji_ingest import ModalEmbeddingWorker

worker = ModalEmbeddingWorker("koji-ingest", "ColNomicEmbedder")
query_vectors = await worker.encode_queries(
    ["what did the report conclude?"]
)

Entry point 2 — ModalIngester (whole document)

ModalIngester sends the whole document up and runs parse → transcribe → enrich → embed in one remote call, streaming stage events back:

from koji_ingest import ModalIngester

ingester = ModalIngester("koji-ingest", "IngestPipeline")
result = await ingester.process("document.pdf")

Which stages run where

Stage ModalEmbeddingWorker ModalIngester Default (local)
Parse local Modal local
Transcribe local Modal local
Enrich (VLM) local Modal local
Embed Modal Modal local
Relation building local local local
persist_result local local local
Kōji database local local local

Weights volume retention

The koji-hf-cache volume holds re-downloadable HuggingFace weights and nothing else, so it is safe to drop when a model changes or you want the storage back:

modal volume delete koji-hf-cache

Re-run modal run -m koji_ingest.modal_app::download_weights afterwards to refill it before the next ingest.

What leaves your machine

Documents, figures, and audio leave the machine only when embedding_backend='modal' or ModalIngester is selected explicitly. The Kōji database, relation building, and persist_result never do — they run locally on every path, remote or not.

License

Apache-2.0

Metadata

Release files for koji-ingest 0.6.0

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for koji-ingest 0.6.0
File Size Uploaded Provenance
koji_ingest-0.6.0.tar.gz 627.8 kB With provenance Details

Built distribution (wheel)

Table of built distributions (wheels) for koji-ingest 0.6.0
File Interpreter ABI Platform
koji_ingest-0.6.0-py3-none-any.whl Python 3 none any Details

Total release size: 822.6 kB

Release files / koji_ingest-0.6.0.tar.gz

Download URL koji_ingest-0.6.0.tar.gz
Size 627.8 kB
Tags Source
SHA-256 checksum
How to use checksums
75f7864b05dafd734ad04cf14f310bb15f67e2004f4351e8f7a3ca68b597c306
BLAKE2b-256 checksum
How to use checksums
e2f162fdef76b2aef0946aa773152d4635111be485eeb6af19fff1adf615c1a1
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.14

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Sep 2, 2026.

Transparency log

Release files / koji_ingest-0.6.0-py3-none-any.whl

Download URL koji_ingest-0.6.0-py3-none-any.whl
Size 194.8 kB
Tags Python 3
SHA-256 checksum
How to use checksums
e0b4d66ad485e92e191f9bd5ac3991c5092ce4331986e635cc7a65045490f43e
BLAKE2b-256 checksum
How to use checksums
96872a7018cbb153bf824ecd205e34bf4c0c9094bd722bd7d6ef5f99777639ab
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.14

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Sep 2, 2026.

Transparency log

Security for koji-ingest 0.6.0

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

Full provenance

All 2 files in this release have been uploaded with provenance metadata.

Select from 2 files

Attestation for koji_ingest-0.6.0.tar.gz

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Sep 2, 2026.

Transparency log

A note on package security

PyPI is a repository for distributing packages, not a service for auditing them. While third-party security researchers and our staff do their best to detect and remove malicious packages, PyPI cannot guarantee the quality, security or suitability of any given package.

To learn more about protecting yourself from malicious actors, visit our package security documentation.

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page