Skip to content

Runtime Engine

The runtime binding is the main embedding surface of LunaVox. GUIs, scripts, and notebooks call into lunavox.runtime.Engine directly via ctypes — no subprocess, no stdout parsing. The C handle is managed with RAII semantics so a with Engine(...) as eng: block is safe from leaks.

Since v2.2.0 the public API is a single synthesize(text, voice, params) entry point. Every voice mode is a Voice.<factory>() call; adding a new mode is a one-line extension instead of a new method.

Example

from pathlib import Path

from lunavox.runtime import Engine, SynthesisParams, Voice

with Engine(Path("models/base_small")) as eng:
    params = SynthesisParams(temperature=0.6, top_p=1.0, top_k=50)

    # Default speaker
    result = eng.synthesize("Hello from LunaVox.", voice=Voice.base(), params=params)

    # Clone from a reference file (.wav or pre-computed .json)
    cloned = eng.synthesize(
        "Hello from a cloned voice.",
        voice=Voice.clone_file("ref/ref_0.6B.json"),
        params=params,
    )

    # Catalog speaker with an optional style instruction
    custom = eng.synthesize(
        "Use angry tone.",
        voice=Voice.custom("Vivian", instruct="Use angry tone."),
        params=params,
    )

    # Design a new voice from a text description
    designed = eng.synthesize(
        "It's in the top drawer… wait, it's empty?",
        voice=Voice.design(instruct="Speak in an incredulous tone."),
        params=params,
    )

    print(f"RTF: {result.stats.rtf:.3f}")
    print(f"Peak RSS delta: {result.stats.mem.rss_peak_delta_bytes / 1024**2:.1f} MB")
    if result.stats.mem.vram_measured:
        print(f"Peak VRAM delta: {result.stats.mem.vram_peak_delta_bytes / 1024**2:.1f} MB")
    # result.audio is a numpy.float32 array in [-1, 1], mono @ sample_rate

Engine

Engine

Engine(model_dir: Union[str, Path], n_threads: int = 4)

RAII wrapper around LunavoxEngine*.

Construction loads models synchronously (blocking on GPU warmup). Call :meth:close or use a with block to release the underlying C++ engine. Methods raise :class:LunavoxSynthesisError on failure with the message pulled from lunavox_get_error.

Source code in src/lunavox/runtime/engine.py
def __init__(self, model_dir: Union[str, Path], n_threads: int = 4):
    self._lib = _capi.load_library()
    self._handle: Optional[int] = None
    self._log_ctype: Any = None  # keeps the ctypes trampoline alive
    self._log_cb: Optional[LogCallback] = None
    self.model_dir = Path(model_dir)
    if not self.model_dir.exists():
        raise FileNotFoundError(f"Model directory not found: {self.model_dir}")
    handle = self._lib.lunavox_create(
        str(self.model_dir).encode("utf-8"),
        int(n_threads),
    )
    if not handle:
        # Pull the last error from the global slot — passing NULL
        # retrieves the process-wide error stashed during create.
        err_ptr = self._lib.lunavox_get_error(ctypes.c_void_p(0))
        detail = err_ptr.decode("utf-8", errors="replace") if err_ptr else "unknown error"
        raise LunavoxSynthesisError(f"lunavox_create failed for {self.model_dir}: {detail}")
    self._handle = handle

sample_rate property

sample_rate: int

last_load_ms property

last_load_ms: int

last_warmup_ms property

last_warmup_ms: int

__enter__

__enter__() -> Engine
Source code in src/lunavox/runtime/engine.py
def __enter__(self) -> Engine:
    return self

__exit__

__exit__(*exc: Any) -> None
Source code in src/lunavox/runtime/engine.py
def __exit__(self, *exc: Any) -> None:
    self.close()

close

close() -> None
Source code in src/lunavox/runtime/engine.py
def close(self) -> None:
    if self._handle is not None:
        self._lib.lunavox_destroy(self._handle)
        self._handle = None
        # Drop the log trampoline so the C engine cannot call back
        # into a freed Python wrapper.
        self._log_ctype = None
        self._log_cb = None

is_loaded

is_loaded() -> bool
Source code in src/lunavox/runtime/engine.py
def is_loaded(self) -> bool:
    return bool(self._lib.lunavox_is_loaded(self.handle))

synthesize

synthesize(
    text: str, voice: Optional[Voice] = None, params: Optional[SynthesisParams] = None
) -> SynthesisResult

Synthesise text into audio using voice.

voice defaults to :func:Voice.base (the model's built-in speaker). params defaults to :func:SynthesisParams defaults, which match lunavox_default_params in the C API. The dispatch table below is the only place that knows which C symbol each mode maps to — voice-side changes never ripple out.

Source code in src/lunavox/runtime/engine.py
def synthesize(
    self,
    text: str,
    voice: Optional[Voice] = None,
    params: Optional[SynthesisParams] = None,
) -> SynthesisResult:
    """Synthesise ``text`` into audio using ``voice``.

    ``voice`` defaults to :func:`Voice.base` (the model's built-in
    speaker). ``params`` defaults to :func:`SynthesisParams`
    defaults, which match ``lunavox_default_params`` in the C API.
    The dispatch table below is the only place that knows which C
    symbol each mode maps to — voice-side changes never ripple out.
    """
    v = voice if voice is not None else Voice.base()
    cp, _held = _to_c_params(params)
    ptr = self._dispatch(text, v, cp)
    self._raise_for_null(ptr)
    return _consume_audio(ptr, v.mode)

on_log

on_log(cb: Optional[LogCallback]) -> None

Install a Python-side log sink that receives every C++ log line.

cb is invoked as cb(level: int, message: str) where level is the raw LunavoxLogLevel (0=DEBUG, 1=INFO, 2=WARN, 3=ERROR, 4=USER). Passing None removes the current callback. The Engine holds the ctypes trampoline so the C engine never dereferences a freed Python object.

Source code in src/lunavox/runtime/engine.py
def on_log(self, cb: Optional[LogCallback]) -> None:
    """Install a Python-side log sink that receives every C++ log line.

    ``cb`` is invoked as ``cb(level: int, message: str)`` where
    ``level`` is the raw ``LunavoxLogLevel`` (0=DEBUG, 1=INFO,
    2=WARN, 3=ERROR, 4=USER). Passing ``None`` removes the current
    callback. The Engine holds the ctypes trampoline so the C
    engine never dereferences a freed Python object.
    """
    if cb is None:
        self._lib.lunavox_set_log_callback(_capi.LOG_CALLBACK_T(0), None)
        self._log_ctype = None
        self._log_cb = None
        return

    def _trampoline(level: int, message_ptr: Any, _user: Optional[int]) -> None:
        if message_ptr is None:
            return
        try:
            text = ctypes.string_at(message_ptr).decode("utf-8", errors="replace")
        except Exception:
            return
        # Never let a Python exception escape across the C boundary.
        with contextlib.suppress(Exception):
            cb(int(level), text)

    self._log_ctype = _capi.LOG_CALLBACK_T(_trampoline)
    self._log_cb = cb
    self._lib.lunavox_set_log_callback(self._log_ctype, None)

Voice

Voice dataclass

Voice(
    mode: SynthesisMode,
    reference_path: Optional[Path] = None,
    speaker: Optional[str] = None,
    instruct: Optional[str] = None,
)

A description of how to voice a synthesis request.

Do not construct directly — use the classmethod factories:

  • Voice.base() — default speaker baked into the model
  • Voice.clone_file(path) — reference WAV or pre-computed JSON features
  • Voice.custom(speaker, instruct=...) — catalog speaker id with optional style
  • Voice.design(instruct=...) — text-described synthetic voice

base classmethod

base() -> Voice
Source code in src/lunavox/runtime/voice.py
@classmethod
def base(cls) -> Voice:
    return cls(mode=SynthesisMode.BASE)

clone_file classmethod

clone_file(path: Union[str, Path]) -> Voice

Clone a voice from a reference .wav or pre-computed .json feature file. The C engine auto-detects the format from the extension.

Source code in src/lunavox/runtime/voice.py
@classmethod
def clone_file(cls, path: Union[str, Path]) -> Voice:
    """Clone a voice from a reference ``.wav`` or pre-computed ``.json``
    feature file. The C engine auto-detects the format from the extension."""
    p = Path(path)
    return cls(mode=SynthesisMode.CLONE_FILE, reference_path=p)

custom classmethod

custom(speaker: str, instruct: str = '') -> Voice

Use a catalog speaker id (e.g. "Vivian") with an optional free-form style instruction.

Source code in src/lunavox/runtime/voice.py
@classmethod
def custom(cls, speaker: str, instruct: str = "") -> Voice:
    """Use a catalog speaker id (e.g. ``"Vivian"``) with an optional
    free-form style instruction."""
    if not speaker:
        raise ValueError("Voice.custom requires a non-empty speaker id")
    return cls(mode=SynthesisMode.CUSTOM, speaker=speaker, instruct=instruct)

design classmethod

design(instruct: str) -> Voice

Synthesise a speaker from a text description. The instruct field is required — an empty string is rejected at the Engine level too, but catching it here gives a clearer error.

Source code in src/lunavox/runtime/voice.py
@classmethod
def design(cls, instruct: str) -> Voice:
    """Synthesise a speaker from a text description. The ``instruct``
    field is required — an empty string is rejected at the Engine
    level too, but catching it here gives a clearer error."""
    if not instruct.strip():
        raise ValueError("Voice.design requires a non-empty instruct text")
    return cls(mode=SynthesisMode.DESIGN, instruct=instruct)

Synthesis params

SynthesisParams dataclass

SynthesisParams(
    max_audio_tokens: int = 0,
    temperature: float = 0.6,
    top_p: float = 1.0,
    top_k: int = 50,
    n_threads: int = 4,
    repetition_penalty: float = 1.05,
    language_id: int = -1,
    ref_text: Optional[str] = None,
)

Python-side mirror of LunavoxSynthesisParams.

Defaults match lunavox_default_params in the C API; overriding any field produces a fresh struct passed to the next synthesize call. The ref_text field is an optional prompt hint used by voice-clone and voice-design flows.

This dataclass is the single source of truth for sampler defaults across CLI / HTTP / WS / GUI. All entries override fields via :meth:from_overrides rather than re-declaring defaults.

from_overrides classmethod

from_overrides(**overrides: Any) -> SynthesisParams

Build a fresh instance, applying only the non-None overrides.

Unknown keys raise TypeError so typos surface at the call site instead of silently ignoring an option. None values are dropped — they mean "no override, use the default" — so callers can pass through optional CLI / API fields directly without per-field if guards.

Source code in src/lunavox/runtime/params.py
@classmethod
def from_overrides(cls, **overrides: Any) -> SynthesisParams:
    """Build a fresh instance, applying only the non-``None`` overrides.

    Unknown keys raise ``TypeError`` so typos surface at the call
    site instead of silently ignoring an option. ``None`` values
    are dropped — they mean "no override, use the default" — so
    callers can pass through optional CLI / API fields directly
    without per-field ``if`` guards.
    """
    valid = {f.name for f in fields(cls)}
    filtered: dict[str, Any] = {}
    for key, value in overrides.items():
        if key not in valid:
            raise TypeError(f"Unknown SynthesisParams field: {key!r}")
        if value is not None:
            filtered[key] = value
    return cls(**filtered)

default_params

default_params() -> SynthesisParams

Return a fresh :class:SynthesisParams with the C-defined defaults.

Source code in src/lunavox/runtime/params.py
def default_params() -> SynthesisParams:
    """Return a fresh :class:`SynthesisParams` with the C-defined defaults."""
    return SynthesisParams()

Synthesis result

SynthesisResult dataclass

SynthesisResult(audio: Any, sample_rate: int, stats: SynthesisStats, mode: SynthesisMode)

Structured output of a single synthesize call.

audio is a numpy.float32 ndarray in [-1, 1] (mono). Typed as Any here so importing this module does not pay the numpy import cost — the actual array is only materialised by :class:~lunavox.runtime.engine.Engine when it calls into the C API.

SynthesisStats dataclass

SynthesisStats(
    t_tokenize_ms: int = 0,
    t_encode_ms: int = 0,
    t_generate_ms: int = 0,
    t_decode_ms: int = 0,
    t_total_ms: int = 0,
    audio_duration_ms: int = 0,
    rtf: float = 0.0,
    mem: MemStats = MemStats(),
)

Per-run timing + memory stats echoed from the C engine.

SynthesisMode

Bases: Enum

Which voice path produced a :class:SynthesisResult.

Only the four modes the Python API actually exposes are listed. The C ABI also accepts raw sample/embedding clone calls, but those are not currently reachable from Python — adding them means adding a :class:~lunavox.runtime.voice.Voice classmethod, not mutating this enum independently.

Error hierarchy

LunavoxLibraryError

Bases: RuntimeError

Raised when liblunavox cannot be located or loaded.

LunavoxSynthesisError

Bases: RuntimeError

Raised when a C-side synthesize call returns NULL or the engine reports an error via lunavox_get_error.

Library loader

library_path

library_path() -> Optional[Path]

Return the dlopen'd library path, loading on demand.

Source code in src/lunavox/runtime/_capi.py
def library_path() -> Optional[Path]:
    """Return the dlopen'd library path, loading on demand."""
    load_library()
    return _lib_path