Architecture

This page shows dengjen-piper-rs’s own structure as a C4 Container, Component, and Code diagram. For how it fits into the wider ZirekHQ speech stack, see the org-wide System Context diagram. It has no dependency relationship, in either direction, with dengjen-tts — these are two independent, parallel Piper-format inference implementations in the org, not one wrapping the other.

Container

dengjen-piper-rs is a plain library crate: no CLI, no FFI boundary, no Python/Java bindings. It’s consumed the ordinary way, via cargo add.

C4Container
    Person(dev, "Rust developer", "Depends on the crate via Cargo")

    System_Boundary(piperrs, "dengjen-piper-rs") {
        Container(lib, "dengjen-piper-rs", "Rust library crate (rlib)", "Published to crates.io")
    }

    System_Ext(voices, "Piper voice catalogue", "e.g. huggingface.co/rhasspy/piper-voices")

    Rel(dev, lib, "cargo add dengjen-piper-rs")
    Rel(lib, voices, "Loads Piper-format ONNX voices")

Component

The workspace actually contains two coexisting designs. The one below, under src/, is what’s actually published and consumed. A second, hexagonal (ports-and-adapters) design lives in crates/piper-core — but it’s pulled in only as a dev-dependency of the published crate, exercised via examples/hexagonal_pipeline.rs, not the crate’s real public surface yet. Both are shown so this doesn’t read as more settled than it is.
C4Component
    Container_Boundary(published, "Published crate (src/) -- the real public API") {
        Component(model, "model.rs", "Rust module", "ModelConfig, infer(), calls ort::Session directly")
        Component(errors, "lib.rs", "Rust module", "PiperError and the crate's public entry points")
    }

    Container_Boundary(reference, "In-progress hexagonal architecture -- separate workspace crates, dev-dependency only, not yet promoted") {
        Component(domain, "piper-core::domain", "Value types", "audio, errors, inference, phoneme")
        Component(ports, "piper-core::ports", "Trait", "InferenceEngine -- the port")
        Component(usecases, "piper-core::use_cases", "Orchestration")
        Component(ortadapter, "ort-adapter", "Adapter crate", "Implements InferenceEngine via ort::Session")
        Component(espeakadapter, "espeak-rs-adapter", "Adapter crate", "Grapheme-to-phoneme via espeak")
        Component(voicerepo, "fs-voice-repo", "Adapter crate", "Loads voice files from disk")
    }

    Rel(errors, model, "Uses")
    Rel(usecases, ports, "Depends on")
    Rel(ortadapter, ports, "Implements")
    Rel(usecases, domain, "Uses")

Code

The one real, verified port from the in-progress piper-core side: an InferenceEngine: Send + Sync trait depending only on domain value types. OrtInferenceEngine (in the ort-adapter crate) is its only production implementor today; a private FixedOutputEngine test double also exists, but it’s defined inline in the port’s own #[cfg(test)] module, not in stub-adapter (that crate implements a different port, Phonemizer, not this one).

classDiagram
    class InferenceEngine {
        <<trait>>
        +infer(ids, params) Result
        +validate_arity(expects_speaker_tensor) Result
    }
    class OrtInferenceEngine

    InferenceEngine <|.. OrtInferenceEngine

Versioning

The workspace shares one version.workspace = true across all 6 members, the root crate, and the non-member crates/espeak-rs-sys — with no exceptions. This is a deliberate convention, not an accident of the workspace template.

espeak-rs-sys and espeak-rs were briefly carved out to version independently, since they repeatedly change alone (cross-compile fixes, FFI hardening, leak fixes) while piper-core and its adapters change together. That split was reverted: a single shared version is simpler to reason about — "these N crates at version X work together" holds for the whole workspace at a glance, with no need to cross-reference a compatibility matrix to know which espeak-rs/espeak-rs-sys version a given dengjen-piper-rs release was built and tested against. The unrelated-republish cost of lockstep is accepted as the tradeoff for that simplicity.

See #115.