How the Voice Facade Pattern Works with Stub Implementations in OpenHuman

The voice facade pattern in OpenHuman uses a compile-time facade layer that always builds, while routing to either a full-featured implementation or a no-op stub based on the voice Cargo feature flag.

The tinyhumansai/openhuman repository implements a sophisticated compile-time abstraction for its voice processing capabilities. This architectural pattern ensures that public API symbols remain available regardless of feature configuration, while allowing heavy audio dependencies to be excluded from builds that do not require speech-to-text or text-to-speech functionality. The implementation relies on Cargo's conditional compilation to switch between real audio processing modules in src/openhuman/voice/ and a lightweight stub that maintains type safety.

Understanding the Three-Layer Compile-Time Architecture

OpenHuman's voice domain separates concerns into three distinct compile-time layers. This structure guarantees that code depending on openhuman::voice will always compile, even when the underlying voice stack is omitted.

The Facade Layer (Always Compiled)

The file src/openhuman/voice/mod.rs serves as the immutable public interface. This module is the only voice-related code compiled unconditionally, regardless of whether the voice Cargo feature is enabled or disabled. It defines the public openhuman::voice namespace, re-exports compile-status flags from compile_status.rs, and conditionally includes either the real implementation or the stub using #[cfg] attributes.

When the facade imports symbols, it uses conditional compilation to expose the appropriate backend:

// In src/openhuman/voice/mod.rs
#[cfg(feature = "voice")]
pub use factory::{create_stt_provider, effective_stt_provider, /* ... */};

#[cfg(not(feature = "voice"))]
pub use stub::*;

The Real Implementation (Feature-Gated)

When voice is enabled via --features voice, the facade imports heavy sub-modules including always_on, audio_capture, audio_toolkit, bus, dictation_listener, factory, hotkey, realtime, reply_speech, and server. These modules in src/openhuman/voice/ provide actual speech-to-text processing, audio capture via cpal, dictation servers, and RPC endpoints. This implementation is wrapped in #[cfg(feature = "voice")] gates and only compiles when explicitly requested.

The Stub Implementation (Fallback)

When the voice feature is disabled, src/openhuman/voice/stub.rs compiles instead. This stub mirrors the exact public surface of the real implementation—including types like SttResult, traits like SttProvider, and functions like publish_ptt_transcript_committed—but provides no-op or error-returning bodies. This guarantees type checking succeeds while ensuring runtime calls fail gracefully with Err(anyhow!("voice feature disabled at compile time")).

Surface-Level Consistency and Drift Detection

The facade pattern enforces API compatibility between the real and stub implementations through several mechanical safeguards.

Public Re-exports Guarantee Symbol Availability. The facade in mod.rs re-exports identical symbols regardless of which backend is active. When voice is enabled, it exposes real factory functions. When disabled, it exposes the same names from stub.rs.

Identical Signatures Across Implementations. The stub implements the exact same function signatures, trait methods, and type definitions as the real implementation. For example, create_stt_provider accepts the same parameters and returns the same Result type in both src/openhuman/voice/factory/ (real) and src/openhuman/voice/stub.rs (stub). The only difference is the body.

CI Enforced Drift Detection. A continuous integration step runs cargo check --no-default-features --features "<all-but-voice>". If any real signature changes without a corresponding update to stub.rs, the build fails. This mechanical check guarantees that the two surfaces remain locked together.

Runtime Behavior: Feature On vs. Feature Off

The facade determines application behavior at runtime based on which implementation was compiled.

With Voice Enabled: The real modules link into the binary. The RPC namespace openhuman.voice_* becomes fully functional, the dictation server can start via server::VoiceServer, and the bus in bus.rs emits transcription events. Heavy audio crates like cpal, hound, and lettre are included in the dependency tree.

With Voice Disabled: The stub implementation links instead. Calls to create_stt_provider return a clear error message indicating the feature is disabled. The publish_ptt_transcript_committed function logs a debug message and performs no other action. The CLI subcommand openhuman voice returns a descriptive error, and the core application starts normally without audio dependencies.

Practical Code Examples

The following examples demonstrate how client code interacts with the facade without concerning itself with the underlying feature flag state.

Factory Pattern Usage

This code compiles and runs regardless of the voice feature status:

use openhuman::voice::factory::{effective_stt_provider, create_stt_provider};
use openhuman::config::Config;

// Resolve provider name from configuration - always available
let provider_name = effective_stt_provider(&config);

// Attempt to initialize - returns Err when voice is disabled
match create_stt_provider(&provider_name, "whisper-v1", &config) {
    Ok(provider) => println!("STT ready: {:?}", provider),
    Err(e) => eprintln!("Voice unavailable: {}", e),
}

When compiled with --no-default-features, create_stt_provider returns Err(anyhow!("voice feature disabled at compile time")), allowing the application to handle the absence of voice capabilities gracefully.

Event Bus Publishing

The PTT (Push-to-Talk) transcript notification system works transparently through the facade:

use openhuman::voice::publish_ptt_transcript_committed;

// Signal that a dictation session completed
publish_ptt_transcript_committed(
    thread_id.to_string(),
    session_id,
    transcript.len(),
    elapsed_ms,
    false,
);

With the feature disabled, the stub implementation in stub.rs executes a silent no-op, avoiding runtime panics in code paths that expect the bus to exist.

CLI Integration

The standalone voice subcommand integrates through the facade:

// CLI dispatcher pattern
match args.subcommand.as_str() {
    "voice" => {
        openhuman::voice::cli::run_standalone_subcommand(&args.rest)?;
    }
    _ => handle_other_commands(),
}

When voice is disabled, run_standalone_subcommand returns the standardized disabled error, providing clear user feedback rather than a missing symbol error.

Why OpenHuman Uses the Voice Facade Pattern

Binary Size and Dependency Management. The voice stack pulls in heavyweight audio processing crates. By gating the implementation behind a Cargo feature, builds targeting embedded or server environments can exclude these dependencies entirely, producing leaner binaries.

Compile-Time API Guarantees. External crates and internal domains can depend on openhuman::voice without adding optional dependencies to their own manifests. The always-compiled facade guarantees that the namespace exists for type checking, even when the functionality is stubbed.

Graceful Degradation. Applications boot successfully without voice capabilities. RPC routes register but return disabled errors, and the CLI remains functional. This provides better operational flexibility than compilation failures or runtime link errors.

Summary

  • The facade pattern in src/openhuman/voice/mod.rs provides a stable public API that compiles unconditionally while delegating to either real or stub implementations based on the voice Cargo feature.
  • Stub implementations in src/openhuman/voice/stub.rs mirror the exact signatures of real voice modules, returning descriptive errors or no-ops when the feature is disabled.
  • Drift detection via CI ensures that changes to real implementation signatures require corresponding updates to the stub, maintaining API consistency.
  • Runtime behavior switches between full voice capabilities (STT/TTS servers, audio capture) and lightweight error responses without requiring conditional code in callers.

Frequently Asked Questions

What happens if I call a voice function without the voice feature enabled?

The stub implementation in src/openhuman/voice/stub.rs handles the call. Functions like create_stt_provider return Err(anyhow!("voice feature disabled at compile time")), while event functions like publish_ptt_transcript_committed execute as no-ops. Your code compiles and runs, but voice operations fail gracefully with descriptive error messages rather than causing runtime panics.

How does OpenHuman prevent the stub and real implementations from drifting out of sync?

The project uses a CI step that runs cargo check --no-default-features --features "<all-but-voice>". Since the stub must provide identical signatures to the real implementation, any change to the real API that isn't reflected in stub.rs will cause a compilation error. This mechanical check guarantees that the two surfaces remain identical.

Can I depend on the voice module in my crate without pulling in audio dependencies?

Yes. By depending on openhuman without the voice feature, you can import from openhuman::voice and use types like SttResult or call effective_stt_provider without linking heavy audio crates like cpal or hound. The facade guarantees these symbols exist, while the stub ensures your binary remains small and free of audio processing dependencies.

Where is the compile-time feature flag checked in the source code?

The primary conditional compilation occurs in src/openhuman/voice/mod.rs, which uses #[cfg(feature = "voice")] to include real modules and #[cfg(not(feature = "voice"))] to include the stub. Additional compile-time status information is available in src/openhuman/voice/compile_status.rs, which exports boolean constants indicating whether voice support was compiled into the current binary.

Have a question about this repo?

These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →