[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 = []
# Enable the macOS system-audio capture listener (Core Audio process taps,
# ported from Meetily). Pulls in `cidre`, which has a build.rs that runs
# `xcodebuild` — requires *full* Xcode (not just Command Line Tools). On
# CI / dev machines without Xcode, leave the feature off; the rest of
# car-voice and the FFI surface still build.
system-audio-macos = ["dep:cidre", "dep:futures-channel"]
# Parakeet TDT (NVIDIA NeMo, NIM-style export) via ONNX Runtime.
# Provides a Whisper alternative with native streaming support and
# generally lower latency. v0.5.0 ships the provider scaffold + the
# `ort` dep gate; the actual ONNX inference loop is ported from
# Meetily's `parakeet_engine.rs` as follow-up work.
parakeet = ["dep:ort", "dep:ndarray", "dep:once_cell", "dep:regex"]
# Speaker diarization via pyannote/wespeaker-voxceleb-resnet34-LM ONNX
# embeddings + online cosine-distance clustering. Pulls in `ort` and
# `ndarray`. The model file (~28 MB) is downloaded on first use to
# `~/.car/models/`.
diarization = ["dep:ort", "dep:ndarray"]

[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.24.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.24.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.24.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.24.0" }
# Cancellation token for orchestrator's pipeline-lock interaction.
tokio-util = { version = "0.7", default-features = false }

# Parakeet (ONNX-based fast transcription). Optional — pulled in when
# the `parakeet` feature is enabled. The release-candidate version range
# matches what Meetily ships; pin if a more stable release lands.
ort = { version = "2.0.0-rc.10", optional = true }
# Used by the diarization embedding pipeline for fixed-shape mel-feature
# tensors AND by the Parakeet inference path. Optional — pulled in via
# the `diarization` or `parakeet` features.
ndarray = { version = "0.16", optional = true }
# Lazy static for the Parakeet decoder's space-substitution regex.
# Optional — pulled in via the `parakeet` feature.
once_cell = { version = "1", optional = true }
# Used by the Parakeet decoder for sentencepiece-style space substitution
# in the final transcript text. Optional — pulled in via `parakeet`.
regex = { version = "1", optional = true }

# 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"
# Apple framework bindings used by the system-audio capture (Core Audio
# process taps + aggregate device). Pinned to the same revision Meetily
# ships against. Optional + opt-in via the `system-audio-macos` feature
# because cidre's build.rs invokes `xcodebuild` and requires *full*
# Xcode (not Command Line Tools).
# `version = "0.11"` matches the version cidre declares at the
# pinned rev (a9587fa → cidre 0.11.3). Cargo requires the
# `version =` requirement be satisfied by the git source's own
# Cargo.toml, NOT by what's on crates.io. crates.io's latest is
# 0.15.0 — that won't match this git rev. Bump together when
# upgrading the rev.
#
# The version pairing is required because `cargo publish` rejects
# git-only deps; local builds resolve the git rev for car-voice's
# own purposes, downstream consumers of car-voice from crates.io
# resolve `cidre = "0.11"` against the registry. The registry has
# 0.11.3 listed, so downstream consumers can also enable the
# `system-audio-macos` feature if they want — they'll get an older
# cidre than this repo uses, but at least it'll resolve.
cidre = { git = "https://github.com/yury/cidre", rev = "a9587fa", version = "0.11", features = ["av"], optional = true }
futures-channel = { version = "0.3", optional = true }
# 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"

# 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.24.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.rust]
unexpected_cfgs = { level = "warn", check-cfg = ['cfg(car_skip_mlx)'] }
