From 725cd70ee20592c8326e9f9e48a2b528c705fc5b Mon Sep 17 00:00:00 2001 From: "Andrei G." Date: Fri, 20 Mar 2026 14:48:13 +0100 Subject: [PATCH] fix(ml): rubato 1.0.1 API upgrade and StreamChunk wrapping for candle provider Fixes two compile errors when building with --features ml: - Updated candle_whisper.rs resample() for rubato 1.0.1 API: SincFixedIn removed, replaced with Async::new_sinc() with FixedAsync::Input. Output extraction via InterleavedOwned::take_data() for type compatibility. - Wrapped ChatStream output in StreamChunk::Content pattern in candle_provider to match type requirements (String -> StreamChunk enum). - Added audioadapter-buffers 2.0 dependency (gated on candle feature) required by rubato 1.0.1 for buffer I/O via Adapter trait. All checks pass: cargo check --features ml, fmt, clippy, 6014 tests. --- CHANGELOG.md | 2 ++ Cargo.lock | 1 + Cargo.toml | 1 + crates/zeph-llm/Cargo.toml | 3 ++- crates/zeph-llm/src/candle_provider/mod.rs | 5 +++-- crates/zeph-llm/src/candle_whisper.rs | 15 ++++++++++----- 6 files changed, 19 insertions(+), 8 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 069afef7..e11f4379 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -34,6 +34,8 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/). ### Fixed +- fix(ml): rubato 1.0.1 API upgrade and StreamChunk wrapping for candle provider (#1858) — updated `candle_whisper.rs` resample function for rubato 1.0.1 (SincFixedIn removed, replaced with Async::new_sinc); wrapped ChatStream output in StreamChunk::Content pattern in candle_provider; added audioadapter-buffers dependency (gated on candle feature) + ## [0.16.0] - 2026-03-19 ### Added diff --git a/Cargo.lock b/Cargo.lock index 58e4d0d2..6aa28fdb 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -9576,6 +9576,7 @@ name = "zeph-llm" version = "0.16.0" dependencies = [ "async-stream", + "audioadapter-buffers", "base64 0.22.1", "candle-core", "candle-nn", diff --git a/Cargo.toml b/Cargo.toml index 2b46a360..113c0c65 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -65,6 +65,7 @@ ratatui = "0.30" regex = "1.12" reqwest = { version = "0.13", default-features = false } rmcp = "1.2" +audioadapter-buffers = "2.0" rubato = "1.0" schemars = "1.2" scrape-core = "0.2" diff --git a/crates/zeph-llm/Cargo.toml b/crates/zeph-llm/Cargo.toml index 5ff769a4..11561cf1 100644 --- a/crates/zeph-llm/Cargo.toml +++ b/crates/zeph-llm/Cargo.toml @@ -17,12 +17,13 @@ readme = "README.md" default = ["schema"] schema = ["dep:schemars"] stt = ["reqwest/multipart"] -candle = ["dep:candle-core", "dep:candle-nn", "dep:candle-transformers", "dep:hf-hub", "dep:tokenizers", "dep:symphonia", "dep:rubato"] +candle = ["dep:audioadapter-buffers", "dep:candle-core", "dep:candle-nn", "dep:candle-transformers", "dep:hf-hub", "dep:tokenizers", "dep:symphonia", "dep:rubato"] cuda = ["candle", "candle-core/cuda", "candle-nn/cuda", "candle-transformers/cuda"] metal = ["candle", "candle-core/metal", "candle-nn/metal", "candle-transformers/metal"] [dependencies] async-stream.workspace = true +audioadapter-buffers = { workspace = true, optional = true } base64.workspace = true candle-core = { workspace = true, optional = true } candle-nn = { workspace = true, optional = true } diff --git a/crates/zeph-llm/src/candle_provider/mod.rs b/crates/zeph-llm/src/candle_provider/mod.rs index 1b26a68f..df69f7ec 100644 --- a/crates/zeph-llm/src/candle_provider/mod.rs +++ b/crates/zeph-llm/src/candle_provider/mod.rs @@ -16,7 +16,7 @@ use self::embed::EmbedModel; use self::generate::{GenerationConfig, GenerationOutput, generate_tokens}; use self::loader::{LoadedModel, ModelSource, load_chat_model}; use self::template::ChatTemplate; -use crate::provider::{ChatStream, LlmProvider, Message}; +use crate::provider::{ChatStream, LlmProvider, Message, StreamChunk}; use candle_transformers::models::quantized_llama::ModelWeights; @@ -146,7 +146,8 @@ impl LlmProvider for CandleProvider { while !text.is_char_boundary(end) { end -= 1; } - if tx.blocking_send(Ok(text[start..end].to_string())).is_err() { + let chunk = StreamChunk::Content(text[start..end].to_string()); + if tx.blocking_send(Ok(chunk)).is_err() { break; } start = end; diff --git a/crates/zeph-llm/src/candle_whisper.rs b/crates/zeph-llm/src/candle_whisper.rs index 382eddc9..d73f5b81 100644 --- a/crates/zeph-llm/src/candle_whisper.rs +++ b/crates/zeph-llm/src/candle_whisper.rs @@ -318,7 +318,8 @@ fn decode_audio(bytes: &[u8]) -> Result, LlmError> { fn resample(input: &[f32], from_rate: u32, to_rate: u32) -> Result, LlmError> { use rubato::{ - Resampler, SincFixedIn, SincInterpolationParameters, SincInterpolationType, WindowFunction, + Async, FixedAsync, Resampler, SincInterpolationParameters, SincInterpolationType, + WindowFunction, }; let params = SincInterpolationParameters { @@ -330,14 +331,18 @@ fn resample(input: &[f32], from_rate: u32, to_rate: u32) -> Result, Llm }; let ratio = f64::from(to_rate) / f64::from(from_rate); - let mut resampler = SincFixedIn::::new(ratio, 2.0, params, input.len(), 1) - .map_err(|e| LlmError::TranscriptionFailed(format!("resampler init: {e}")))?; + let mut resampler = + Async::::new_sinc(ratio, 2.0, ¶ms, input.len(), 1, FixedAsync::Input) + .map_err(|e| LlmError::TranscriptionFailed(format!("resampler init: {e}")))?; + + let input_adapter = audioadapter_buffers::direct::InterleavedSlice::new(input, 1, input.len()) + .map_err(|e| LlmError::TranscriptionFailed(format!("input buffer: {e}")))?; let output = resampler - .process(&[input], None) + .process(&input_adapter, 0, None) .map_err(|e| LlmError::TranscriptionFailed(format!("resample: {e}")))?; - Ok(output.into_iter().next().unwrap_or_default()) + Ok(output.take_data()) } #[cfg(test)]