[package]
name = "car-voice"
version.workspace = true
edition.workspace = true
license.workspace = true
description = "Voice I/O capability for CAR — mic capture, VAD, listener/speaker traits"
repository.workspace = true

[features]
default = []
# NOTE: car-voice has no cargo features (rule #1). What were features are now:
#   * `parakeet` / `diarization` → unconditional inclusion (capability toggles —
#     `ort` is already transitive via `voice_activity_detector`).
#   * `system-audio-macos` → the `car_system_audio` build cfg (platform/Xcode
#     toggle — opt in with `RUSTFLAGS=--cfg=car_system_audio` on a Mac with full
#     Xcode; gates the `cidre` dep below). See docs/proposals/voice-cross-platform.md.

[dependencies]
serde = { workspace = true }
serde_json = { workspace = true }
thiserror = { workspace = true }
tokio = { workspace = true }
tracing = { workspace = true }
async-trait = "0.1"
crossbeam-channel = "0.5"
futures = "0.3"
# HTTP client for the ElevenLabs STT/TTS providers.
# car-inference also has its own ElevenLabs paths but they're file-on-disk;
# car-voice needs the in-memory streaming variant for realtime capture.
reqwest = { version = "0.12", default-features = false, features = ["json", "multipart", "rustls-tls", "stream", "blocking"] }
# WebSocket client used by the ElevenLabs Realtime STT listener
# (`elevenlabs_streaming_listener`). Same major version as the rest of
# the workspace.
tokio-tungstenite = { version = "0.27", features = ["rustls-tls-webpki-roots"] }
# Encoding PCM frames as base64 for the ElevenLabs realtime
# `input_audio_chunk` message payload.
base64 = "0.22"
# Cross-platform audio I/O — used by the cpal listener and the
# software audio mixer that feeds VPIO's bus 0 reference signal.
cpal = "0.15"
# rodio for audio decoding. Two formats are in scope:
#   - mp3:  ElevenLabs TTS clips and the ambient bed stems.
#   - wav:  AppleSpeechSpeaker (`say(1)` writes WAV) and any host that
#           passes WAV bytes through `mixer.queue_tts` / `play_audio`.
# Without `wav` enabled, AppleSpeech audio fails to decode silently —
# `[mixer] queue_tts decode failed: end of stream` is the symptom.
rodio = { version = "0.20", features = ["mp3", "wav"], default-features = false }
# Silero VAD V5 fallback — neural speech-presence detector (MIT). Kept
# as the bundled-model fallback when the MLX v6 path is unavailable.
voice_activity_detector = "0.2"

# Sample-rate conversion — whisper.cpp wants 16kHz mono, VPIO delivers
# whatever the negotiated output rate is (typically 44.1 or 48 kHz).
rubato = "0.15"

# Download Whisper model files (~600MB-1.5GB) on first use.
dirs = "6"

# TOML is the on-disk format for voiceprint enrollments (voiceprint.toml).
toml = "0.8"

# WAV decoder for `enroll_from_wav`. ~4 KB MIT crate, single-purpose,
# zero deps. Avoids pulling in symphonia's much larger codec graph.
hound = "3"

# Temp files for the Apple-Speech batch STT path — SFSpeechURLRecognitionRequest
# reads from disk, so we persist the f32 PCM samples as a short-lived WAV.
# Already used in dev-dependencies; promoted to runtime deps for
# cfg(target_os = "macos") usage.
tempfile = "3"

# Concurrent map for the VoiceSession registry. FFI bindings address
# sessions by string id and may add/remove from any thread.
dashmap = "6"

# Lock-free SPSC ring buffer for the macOS CoreAudio system-audio capture
# path (ported from Meetily, MIT). The Core Audio IO proc callback runs
# on a real-time audio thread and cannot block — pushing samples into a
# `HeapRb` and pulling them on the consumer side keeps the audio thread
# obstruction-free.
ringbuf = "0.4"
log = "0.4"

# CAR-native API key resolution (env first, then OS keychain). Used by
# the ElevenLabs STT/TTS providers so they pick up keys stored via
# `car secrets put` instead of failing when only env-var is missing
# (#140). Hoisted out of car-inference::key_pool's pub(crate) helper.
car-secrets = { path = "../car-secrets", version = "0.34.0" }

# Voice orchestrator depends on the engine-side voice_turn types
# (VoiceTurnHandle, dispatch_voice_turn). Cross-platform — the
# orchestrator works on every target the listener does. car-engine
# transitively brings in car-inference, which historically lived under
# the macOS target sections; this hoists it to unconditional. The
# extra build cost on non-macOS is the inference engine itself, which
# CI already builds for those targets.
car-engine = { path = "../car-engine", version = "0.34.0" }
# Direct dep so we can use car_inference::{GenerateRequest, IntentHint}
# in the orchestrator's request-building code. Hoisted out of the
# macOS target sections for the same reason as car-engine above.
car-inference = { path = "../car-inference", version = "0.34.0" }
# Direct dep so the orchestrator can reference EventKind variants
# (VoiceBridgePlayed, VoiceSidecarTimedOut) for telemetry emission.
# car-engine re-exports the types but the variants come from here.
car-eventlog = { path = "../car-eventlog", version = "0.34.0" }
# Cancellation token for orchestrator's pipeline-lock interaction.
tokio-util = { version = "0.7", default-features = false }

# Parakeet (ONNX-based fast transcription) + diarization embeddings. Pulled in
# unconditionally (rule #1): `ort` is already a transitive dep via
# `voice_activity_detector`, so this only adds `ndarray`/`once_cell`/`regex`.
# The release-candidate version range matches what Meetily ships.
ort = { version = "2.0.0-rc.10" }
# Used by the diarization embedding pipeline for fixed-shape mel-feature
# tensors AND by the Parakeet inference path.
ndarray = { version = "0.16" }
# Lazy static for the Parakeet decoder's space-substitution regex.
once_cell = { version = "1" }
# Used by the Parakeet decoder for sentencepiece-style space substitution
# in the final transcript text.
regex = { version = "1" }

# macOS-only deps.
#
# * `whisper-rs` with the `metal` feature compiles whisper.cpp's Metal
#   backend so on-device STT runs on the GPU on Apple Silicon. The Metal
#   backend requires `find_library(FOUNDATION)` at cmake time, which
#   fails on Linux CI — so we gate it to macOS and fall back to the
#   plain (CPU) whisper.cpp build elsewhere.
# * `coreaudio-sys` powers the VoiceProcessingIO Audio Unit (hardware
#   AEC + NS + AGC).
[target.'cfg(target_os = "macos")'.dependencies]
whisper-rs = { version = "0.14", features = ["metal"] }
coreaudio-sys = "0.2"
anyhow = "1"
# car-inference is hoisted to the unconditional [dependencies] section
# above (so the orchestrator can use it cross-platform). On Apple
# Silicon, the aarch64-macos target section below adds the
# `mlx-metal` feature on top of the unconditional dep — Cargo merges
# the feature sets across target sections.
uuid = { version = "1", features = ["v4"] }
# Apple-frameworks providers (Speech / AVFAudio / Foundation Models).
# These are unconditional on macOS — no feature flag — per the project's
# no-new-features rule. Build cost on non-macOS targets is zero (cfg-gated).
# Unlike `cidre`, these objc2 crates build with Command Line Tools alone
# and do not invoke `xcodebuild`. See docs/proposals/macos-apple-frameworks.md.
objc2 = "0.6"
objc2-foundation = "0.3"
objc2-avf-audio = { version = "0.3", features = [
    "AVSpeechSynthesis",
    "AVAudioBuffer",
    "AVAudioFormat",
    "block2",
] }
objc2-speech = { version = "0.3", features = [
    "SFSpeechRecognizer",
    "SFSpeechRecognitionRequest",
    "SFSpeechRecognitionResult",
    "SFSpeechRecognitionTask",
    "SFTranscription",
] }
block2 = "0.6"

# System-audio capture (Core Audio process taps + aggregate device) via the
# Apple framework bindings in `cidre`. Opt-*in* with `--cfg=car_system_audio`
# (mirrors `car_skip_mlx`/`car_skip_cuda`, inverted) rather than a cargo feature
# — cidre's build.rs invokes `xcodebuild` and requires *full* Xcode (not just
# Command Line Tools), so default macOS builds must NOT pull it. Rule #1: no
# cargo features; a build cfg keeps the capability surface uniform.
#
# `cidre` is pulled from crates.io, NOT git. `=0.11.3` is the published version
# corresponding to the rev (a9587fa) this previously pinned. The git form made
# the *entire workspace* unresolvable wherever github.com/yury/cidre was
# unreachable at resolution time (CI mirrors, restricted networks, sandboxes):
# Cargo clones a git dep's source to read its manifest *before* applying
# target/cfg filters, so even this macOS-gated, default-off dep blocked
# `cargo metadata` everywhere. A registry dep is content-addressed and
# mirrorable. Keep this a registry dep — no git deps in the resolved graph (see
# docs/proposals/voice-cross-platform.md). When bumping, pin the exact version
# and rebuild on a Mac with `--cfg=car_system_audio` to confirm
# `system_audio_capture.rs` still compiles against the cidre API.
[target.'cfg(all(target_os = "macos", car_system_audio))'.dependencies]
cidre = { version = "=0.11.3", features = ["av"] }
futures-channel = { version = "0.3" }

# Apple-Silicon-only: MLX framework. mlx-sys (transitive via mlx-rs) refuses
# to compile for x86_64 on macOS — its CMake check rejects the arch — so this
# dep MUST stay arch-gated. Source uses are already
# `#[cfg(all(target_os = "macos", target_arch = "aarch64", not(car_skip_mlx)))]` (vad_backend.rs).
# car-inference's `mlx-metal` feature also belongs here for the same reason.
[target.'cfg(all(target_os = "macos", target_arch = "aarch64", not(car_skip_mlx)))'.dependencies]
mlx-rs = { version = "0.25", default-features = false, features = ["accelerate", "safetensors"] }
# Layers `mlx-metal` on top of the unconditional car-inference dep
# above. Cargo merges feature sets across target sections.
car-inference = { path = "../car-inference", version = "0.34.0", features = ["mlx-metal"] }

[target.'cfg(not(target_os = "macos"))'.dependencies]
whisper-rs = { version = "0.14", default-features = false }

[dev-dependencies]
tokio = { workspace = true, features = ["test-util"] }
tempfile = "3"

# See car-inference/Cargo.toml for rationale. car-voice isn't pulled
# into the XCFramework today (car-ffi-uniffi's narrow dep set
# excludes it) but the same cfg gating is applied here defensively
# in case a future change adds car-voice to the FFI surface.

[lints]
workspace = true
