mirror of
https://github.com/block/buzz.git
synced 2026-08-18 06:50:31 +02:00
refactor(voice): extract reusable Pocket primitives
Signed-off-by: John Tennant <johnmatthewtennant@gmail.com>
This commit is contained in:
committed by
John Tennant
parent
211d17c585
commit
ace19c659e
Generated
+547
-60
File diff suppressed because it is too large
Load Diff
@@ -26,6 +26,7 @@ members = [
|
||||
"crates/buzz-pair-relay",
|
||||
"crates/buzz-relay-mesh",
|
||||
"crates/buzz-dev-mcp",
|
||||
"crates/buzz-voice",
|
||||
"examples/countdown-bot",
|
||||
]
|
||||
exclude = ["desktop/src-tauri"]
|
||||
|
||||
@@ -276,6 +276,7 @@ test-unit:
|
||||
#!/usr/bin/env bash
|
||||
if command -v cargo-nextest &>/dev/null; then
|
||||
cargo nextest run -p buzz-core -p buzz-auth --lib
|
||||
cargo nextest run -p buzz-voice --lib
|
||||
# buzz-db migrator/lint tests: pure SQL-parsing unit tests (no infra).
|
||||
# They guard the embedded-migrator invariant (exactly the consolidated
|
||||
# 0001; cutover/backfill stays an operator script, not startup state)
|
||||
|
||||
@@ -0,0 +1,18 @@
|
||||
[package]
|
||||
name = "buzz-voice"
|
||||
version.workspace = true
|
||||
edition.workspace = true
|
||||
rust-version.workspace = true
|
||||
license.workspace = true
|
||||
repository.workspace = true
|
||||
description = "Reusable local voice primitives for Buzz"
|
||||
|
||||
[dependencies]
|
||||
ort = { version = "=2.0.0-rc.12", default-features = false, features = ["api-24", "ndarray", "std"] }
|
||||
ort-sys = { version = "=2.0.0-rc.12", features = ["disable-linking"] }
|
||||
rand = "0.10"
|
||||
sentencepiece-model = "0.1"
|
||||
serde = { version = "1", features = ["derive"] }
|
||||
serde_json = "1"
|
||||
sherpa-onnx = "1.12"
|
||||
tokenizers = { version = "0.22", default-features = false, features = ["fancy-regex"] }
|
||||
@@ -0,0 +1,55 @@
|
||||
//! Reusable local voice primitives for Buzz.
|
||||
|
||||
pub mod pocket;
|
||||
|
||||
pub use pocket::{
|
||||
load_text_to_speech, load_voice_style, PocketTts, VoiceStyle, DEFAULT_VOICE, SAMPLE_RATE,
|
||||
VOICE_FILE_EXT,
|
||||
};
|
||||
|
||||
/// One immutable artifact required by the April Pocket bundle.
|
||||
///
|
||||
/// `filename` is the bundle-relative file name, `sha256` pins its contents,
|
||||
/// `size_bytes` supports download progress and validation, and `quantized`
|
||||
/// identifies the INT8 components.
|
||||
pub type PocketModelArtifact = pocket::PocketModelArtifact;
|
||||
|
||||
/// Capabilities of Buzz Desktop's April Pocket model.
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub struct PocketModelInfo {
|
||||
/// Language bundle selected from the pinned export.
|
||||
pub bundle_id: &'static str,
|
||||
/// Upstream model repository.
|
||||
pub source_model_id: &'static str,
|
||||
/// Pinned upstream model revision.
|
||||
pub revision: &'static str,
|
||||
/// PCM output sample rate.
|
||||
pub sample_rate: u32,
|
||||
/// Maximum input size declared by the bundle.
|
||||
pub max_token_per_chunk: usize,
|
||||
/// Immutable files required by the runtime.
|
||||
pub artifacts: &'static [PocketModelArtifact],
|
||||
/// Components quantized in the selected bundle.
|
||||
pub quantized_components: &'static [&'static str],
|
||||
}
|
||||
|
||||
/// Language bundle selected from the pinned export.
|
||||
pub const APRIL_BUNDLE_ID: &str = pocket::APRIL_BUNDLE_ID;
|
||||
/// Pinned upstream export repository.
|
||||
pub const APRIL_MODEL_ID: &str = pocket::APRIL_MODEL_ID;
|
||||
/// Pinned revision containing the April bundle.
|
||||
pub const APRIL_MODEL_REVISION: &str = pocket::APRIL_MODEL_REVISION;
|
||||
|
||||
/// Return immutable metadata for Buzz Desktop's April Pocket model.
|
||||
pub const fn april_model_info() -> PocketModelInfo {
|
||||
let info = pocket::april_model_info();
|
||||
PocketModelInfo {
|
||||
bundle_id: info.bundle_id,
|
||||
source_model_id: info.source_model_id,
|
||||
revision: info.revision,
|
||||
sample_rate: info.sample_rate,
|
||||
max_token_per_chunk: info.max_token_per_chunk,
|
||||
artifacts: info.artifacts,
|
||||
quantized_components: info.quantized_components,
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,166 @@
|
||||
//! April 2026 Pocket TTS engine for Buzz Desktop.
|
||||
//!
|
||||
//! The `english_2026-04` bundle uses SentencePiece tokenization, a learned
|
||||
//! voice BOS embedding, recurrent FlowLM state, and stateful Mimi decoding.
|
||||
//! Buzz selects the upstream three-graph INT8 variant while retaining the
|
||||
//! full-precision Mimi encoder and text conditioner specified by that variant.
|
||||
//!
|
||||
//! ## Attribution
|
||||
//!
|
||||
//! - Pocket TTS and Mimi: Kyutai, CC-BY-4.0.
|
||||
//! - ONNX export: KevinAHM/pocket-tts-onnx, CC-BY-4.0.
|
||||
//! - Reference voice: Kyutai's Mary preset (VCTK p333), CC-BY-4.0.
|
||||
//!
|
||||
//! `huddle::models` writes the complete attribution beside the cached bytes.
|
||||
|
||||
use std::path::{Path, PathBuf};
|
||||
use std::sync::Mutex;
|
||||
|
||||
use sherpa_onnx::Wave;
|
||||
|
||||
#[path = "pocket_april.rs"]
|
||||
mod pocket_april;
|
||||
#[path = "pocket_models.rs"]
|
||||
mod pocket_models;
|
||||
|
||||
use pocket_april::{prepare_april_prompt, AprilPocketTts};
|
||||
pub(crate) use pocket_models::{
|
||||
april_model_info, PocketModelArtifact, APRIL_BUNDLE_ID, APRIL_MODEL_ID, APRIL_MODEL_REVISION,
|
||||
};
|
||||
|
||||
/// Pocket TTS emits 24 kHz mono PCM.
|
||||
pub const SAMPLE_RATE: u32 = 24_000;
|
||||
|
||||
/// Bundled reference voice name without its extension.
|
||||
pub const DEFAULT_VOICE: &str = "reference_sample";
|
||||
|
||||
/// Pocket voice files are reference WAVs.
|
||||
pub const VOICE_FILE_EXT: &str = "wav";
|
||||
|
||||
const TTS_NUM_THREADS: usize = 1;
|
||||
|
||||
/// Loaded reference voice samples and their original sample rate.
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct VoiceStyle {
|
||||
samples: Vec<f32>,
|
||||
sample_rate: i32,
|
||||
}
|
||||
|
||||
/// Load a Pocket reference voice WAV from disk.
|
||||
pub fn load_voice_style(path: &Path) -> Result<VoiceStyle, String> {
|
||||
let path_str = path
|
||||
.to_str()
|
||||
.ok_or_else(|| format!("voice path is not valid UTF-8: {}", path.display()))?;
|
||||
let wave = Wave::read(path_str)
|
||||
.ok_or_else(|| format!("could not read voice WAV at {}", path.display()))?;
|
||||
let samples = wave.samples().to_vec();
|
||||
if samples.is_empty() {
|
||||
return Err(format!("voice WAV is empty: {}", path.display()));
|
||||
}
|
||||
Ok(VoiceStyle {
|
||||
samples,
|
||||
sample_rate: wave.sample_rate(),
|
||||
})
|
||||
}
|
||||
|
||||
/// Resident April INT8 Pocket TTS engine.
|
||||
pub struct PocketTts {
|
||||
inner: Mutex<AprilPocketTts>,
|
||||
}
|
||||
|
||||
/// Load Buzz Desktop's pinned April INT8 model.
|
||||
pub fn load_text_to_speech(model_dir: &str) -> Result<PocketTts, String> {
|
||||
let dir = PathBuf::from(model_dir);
|
||||
for artifact in april_model_info().artifacts {
|
||||
let path = dir.join(artifact.filename);
|
||||
if !path.is_file() {
|
||||
return Err(format!(
|
||||
"incomplete Pocket TTS {} INT8 bundle: missing {}",
|
||||
APRIL_BUNDLE_ID,
|
||||
path.display()
|
||||
));
|
||||
}
|
||||
}
|
||||
Ok(PocketTts {
|
||||
inner: Mutex::new(AprilPocketTts::load(&dir, TTS_NUM_THREADS)?),
|
||||
})
|
||||
}
|
||||
|
||||
impl PocketTts {
|
||||
/// Split text into synthesis units that satisfy the bundle's exact
|
||||
/// 50-token input limit.
|
||||
pub fn split_text_into_chunks(&self, text: &str) -> Result<Vec<String>, String> {
|
||||
let Some(prepared) = prepare_april_prompt(text) else {
|
||||
return Ok(Vec::new());
|
||||
};
|
||||
self.inner
|
||||
.lock()
|
||||
.map_err(|_| "Pocket TTS engine lock poisoned".to_string())?
|
||||
.split_prompt(&prepared)
|
||||
}
|
||||
|
||||
/// Synthesize text with the supplied reference voice.
|
||||
///
|
||||
/// Pocket detects language from text and this model uses one synthesis
|
||||
/// step, so `_lang` and `_steps` intentionally do not affect output.
|
||||
pub fn synth_chunk(
|
||||
&self,
|
||||
text: &str,
|
||||
_lang: &str,
|
||||
style: &VoiceStyle,
|
||||
_steps: usize,
|
||||
) -> Result<Vec<f32>, String> {
|
||||
let Some(prepared) = prepare_april_prompt(text) else {
|
||||
return Ok(Vec::new());
|
||||
};
|
||||
let mut engine = self
|
||||
.inner
|
||||
.lock()
|
||||
.map_err(|_| "Pocket TTS engine lock poisoned".to_string())?;
|
||||
let chunks = engine.split_prompt(&prepared)?;
|
||||
let mut samples = Vec::new();
|
||||
for chunk in chunks {
|
||||
let prepared = prepare_april_prompt(&chunk)
|
||||
.ok_or_else(|| "Pocket TTS prompt chunk became empty".to_string())?;
|
||||
samples.extend(engine.synth_chunk(&prepared, style)?);
|
||||
}
|
||||
Ok(samples)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn desktop_model_is_april_int8_only() {
|
||||
let info = april_model_info();
|
||||
assert_eq!(info.max_token_per_chunk, 50);
|
||||
assert_eq!(info.sample_rate, SAMPLE_RATE);
|
||||
assert!(info
|
||||
.artifacts
|
||||
.iter()
|
||||
.any(|artifact| artifact.filename == "flow_lm_main_int8.onnx"));
|
||||
assert!(!info
|
||||
.artifacts
|
||||
.iter()
|
||||
.any(|artifact| artifact.filename == "flow_lm_main.onnx"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[ignore = "requires BUZZ_POCKET_TEST_MODEL_DIR"]
|
||||
fn production_api_emits_non_silent_april_int8_pcm() {
|
||||
let dir = std::env::var("BUZZ_POCKET_TEST_MODEL_DIR")
|
||||
.expect("set BUZZ_POCKET_TEST_MODEL_DIR to an April INT8 model directory");
|
||||
let engine = load_text_to_speech(&dir).expect("load April INT8 engine");
|
||||
let style = load_voice_style(&Path::new(&dir).join("reference_sample.wav"))
|
||||
.expect("load reference voice");
|
||||
let samples = engine
|
||||
.synth_chunk("Bright birds begin beside the bay.", "en", &style, 1)
|
||||
.expect("synthesize through the production API");
|
||||
|
||||
assert!(!samples.is_empty());
|
||||
assert!(samples.iter().all(|sample| sample.is_finite()));
|
||||
assert!(samples.iter().any(|sample| sample.abs() > 1.0e-6));
|
||||
}
|
||||
}
|
||||
Generated
+15
-5
@@ -1049,6 +1049,7 @@ dependencies = [
|
||||
"buzz-media",
|
||||
"buzz-persona",
|
||||
"buzz-sdk",
|
||||
"buzz-voice",
|
||||
"bytes",
|
||||
"bzip2 0.6.1",
|
||||
"chrono",
|
||||
@@ -1075,11 +1076,8 @@ dependencies = [
|
||||
"notify-rust",
|
||||
"objc2-app-kit",
|
||||
"opus",
|
||||
"ort",
|
||||
"ort-sys",
|
||||
"plist",
|
||||
"png 0.18.1",
|
||||
"rand 0.10.2",
|
||||
"regex",
|
||||
"reqwest 0.13.4",
|
||||
"rodio",
|
||||
@@ -1087,7 +1085,6 @@ dependencies = [
|
||||
"rusqlite",
|
||||
"rustls",
|
||||
"security-framework 3.7.0",
|
||||
"sentencepiece-model",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"serde_yaml",
|
||||
@@ -1107,7 +1104,6 @@ dependencies = [
|
||||
"tauri-plugin-updater",
|
||||
"tauri-plugin-window-state",
|
||||
"tempfile",
|
||||
"tokenizers",
|
||||
"tokio",
|
||||
"tokio-tungstenite 0.29.0",
|
||||
"tokio-util",
|
||||
@@ -1174,6 +1170,20 @@ dependencies = [
|
||||
"uuid",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "buzz-voice"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"ort",
|
||||
"ort-sys",
|
||||
"rand 0.10.2",
|
||||
"sentencepiece-model",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"sherpa-onnx",
|
||||
"tokenizers",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "by_address"
|
||||
version = "1.2.1"
|
||||
|
||||
@@ -76,9 +76,6 @@ bytes = "1"
|
||||
futures-util = "0.3"
|
||||
opus = "0.3"
|
||||
neteq = { version = "0.8", default-features = false }
|
||||
ort = { version = "=2.0.0-rc.12", default-features = false, features = ["api-24", "ndarray", "std"] }
|
||||
ort-sys = { version = "=2.0.0-rc.12", features = ["disable-linking"] }
|
||||
rand = "0.10"
|
||||
serde = { version = "1", features = ["derive"] }
|
||||
serde_json = "1"
|
||||
serde_yaml = "0.9"
|
||||
@@ -92,6 +89,7 @@ buzz_core_pkg = { package = "buzz-core", path = "../../crates/buzz-core" }
|
||||
buzz_persona_pkg = { package = "buzz-persona", path = "../../crates/buzz-persona" }
|
||||
buzz_sdk_pkg = { package = "buzz-sdk", path = "../../crates/buzz-sdk" }
|
||||
buzz_agent_pkg = { package = "buzz-agent", path = "../../crates/buzz-agent" }
|
||||
buzz_voice_pkg = { package = "buzz-voice", path = "../../crates/buzz-voice" }
|
||||
iroh = { version = "1.0.2", optional = true }
|
||||
mesh-llm-sdk = { git = "https://github.com/Mesh-LLM/mesh-llm.git", rev = "f455d493a2ae82baf2a326e2d0fda351433b4b30", package = "mesh-llm-sdk", default-features = false, features = ["client", "serving"], optional = true }
|
||||
mesh-llm-host-runtime = { git = "https://github.com/Mesh-LLM/mesh-llm.git", rev = "f455d493a2ae82baf2a326e2d0fda351433b4b30", package = "mesh-llm-host-runtime", default-features = false, features = ["dynamic-native-runtime"], optional = true }
|
||||
@@ -119,7 +117,6 @@ image = { version = "0.25", default-features = false, features = ["jpeg", "png",
|
||||
zip = "8"
|
||||
flate2 = "1"
|
||||
sherpa-onnx = "1.12"
|
||||
sentencepiece-model = "0.1"
|
||||
regex = "1"
|
||||
rusqlite = { version = "0.37", features = ["bundled"] }
|
||||
axum = "0.8"
|
||||
@@ -130,7 +127,6 @@ audioadapter-buffers = "3.0"
|
||||
tempfile = "3"
|
||||
strip-ansi-escapes = "0.2"
|
||||
tracing = "0.1"
|
||||
tokenizers = { version = "0.22", default-features = false, features = ["fancy-regex"] }
|
||||
|
||||
[dev-dependencies]
|
||||
# `test-util` enables tokio's paused-clock (`start_paused`) so the relay
|
||||
|
||||
@@ -1,166 +1,4 @@
|
||||
//! April 2026 Pocket TTS engine for Buzz Desktop.
|
||||
//!
|
||||
//! The `english_2026-04` bundle uses SentencePiece tokenization, a learned
|
||||
//! voice BOS embedding, recurrent FlowLM state, and stateful Mimi decoding.
|
||||
//! Buzz selects the upstream three-graph INT8 variant while retaining the
|
||||
//! full-precision Mimi encoder and text conditioner specified by that variant.
|
||||
//!
|
||||
//! ## Attribution
|
||||
//!
|
||||
//! - Pocket TTS and Mimi: Kyutai, CC-BY-4.0.
|
||||
//! - ONNX export: KevinAHM/pocket-tts-onnx, CC-BY-4.0.
|
||||
//! - Reference voice: Kyutai's Mary preset (VCTK p333), CC-BY-4.0.
|
||||
//!
|
||||
//! `huddle::models` writes the complete attribution beside the cached bytes.
|
||||
|
||||
use std::path::{Path, PathBuf};
|
||||
use std::sync::Mutex;
|
||||
|
||||
use sherpa_onnx::Wave;
|
||||
|
||||
#[path = "pocket_april.rs"]
|
||||
mod pocket_april;
|
||||
#[path = "pocket_models.rs"]
|
||||
mod pocket_models;
|
||||
|
||||
use pocket_april::{prepare_april_prompt, AprilPocketTts};
|
||||
pub(crate) use pocket_models::{
|
||||
pub use buzz_voice_pkg::pocket::*;
|
||||
pub(crate) use buzz_voice_pkg::{
|
||||
april_model_info, PocketModelArtifact, APRIL_BUNDLE_ID, APRIL_MODEL_ID, APRIL_MODEL_REVISION,
|
||||
};
|
||||
|
||||
/// Pocket TTS emits 24 kHz mono PCM.
|
||||
pub const SAMPLE_RATE: u32 = 24_000;
|
||||
|
||||
/// Bundled reference voice name without its extension.
|
||||
pub const DEFAULT_VOICE: &str = "reference_sample";
|
||||
|
||||
/// Pocket voice files are reference WAVs.
|
||||
pub const VOICE_FILE_EXT: &str = "wav";
|
||||
|
||||
const TTS_NUM_THREADS: usize = 1;
|
||||
|
||||
/// Loaded reference voice samples and their original sample rate.
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct VoiceStyle {
|
||||
samples: Vec<f32>,
|
||||
sample_rate: i32,
|
||||
}
|
||||
|
||||
/// Load a Pocket reference voice WAV from disk.
|
||||
pub fn load_voice_style(path: &Path) -> Result<VoiceStyle, String> {
|
||||
let path_str = path
|
||||
.to_str()
|
||||
.ok_or_else(|| format!("voice path is not valid UTF-8: {}", path.display()))?;
|
||||
let wave = Wave::read(path_str)
|
||||
.ok_or_else(|| format!("could not read voice WAV at {}", path.display()))?;
|
||||
let samples = wave.samples().to_vec();
|
||||
if samples.is_empty() {
|
||||
return Err(format!("voice WAV is empty: {}", path.display()));
|
||||
}
|
||||
Ok(VoiceStyle {
|
||||
samples,
|
||||
sample_rate: wave.sample_rate(),
|
||||
})
|
||||
}
|
||||
|
||||
/// Resident April INT8 Pocket TTS engine.
|
||||
pub struct PocketTts {
|
||||
inner: Mutex<AprilPocketTts>,
|
||||
}
|
||||
|
||||
/// Load Buzz Desktop's pinned April INT8 model.
|
||||
pub fn load_text_to_speech(model_dir: &str) -> Result<PocketTts, String> {
|
||||
let dir = PathBuf::from(model_dir);
|
||||
for artifact in april_model_info().artifacts {
|
||||
let path = dir.join(artifact.filename);
|
||||
if !path.is_file() {
|
||||
return Err(format!(
|
||||
"incomplete Pocket TTS {} INT8 bundle: missing {}",
|
||||
APRIL_BUNDLE_ID,
|
||||
path.display()
|
||||
));
|
||||
}
|
||||
}
|
||||
Ok(PocketTts {
|
||||
inner: Mutex::new(AprilPocketTts::load(&dir, TTS_NUM_THREADS)?),
|
||||
})
|
||||
}
|
||||
|
||||
impl PocketTts {
|
||||
/// Split text into synthesis units that satisfy the bundle's exact
|
||||
/// 50-token input limit.
|
||||
pub fn split_text_into_chunks(&self, text: &str) -> Result<Vec<String>, String> {
|
||||
let Some(prepared) = prepare_april_prompt(text) else {
|
||||
return Ok(Vec::new());
|
||||
};
|
||||
self.inner
|
||||
.lock()
|
||||
.map_err(|_| "Pocket TTS engine lock poisoned".to_string())?
|
||||
.split_prompt(&prepared)
|
||||
}
|
||||
|
||||
/// Synthesize text with the supplied reference voice.
|
||||
///
|
||||
/// Pocket detects language from text and this model uses one synthesis
|
||||
/// step, so `_lang` and `_steps` intentionally do not affect output.
|
||||
pub fn synth_chunk(
|
||||
&self,
|
||||
text: &str,
|
||||
_lang: &str,
|
||||
style: &VoiceStyle,
|
||||
_steps: usize,
|
||||
) -> Result<Vec<f32>, String> {
|
||||
let Some(prepared) = prepare_april_prompt(text) else {
|
||||
return Ok(Vec::new());
|
||||
};
|
||||
let mut engine = self
|
||||
.inner
|
||||
.lock()
|
||||
.map_err(|_| "Pocket TTS engine lock poisoned".to_string())?;
|
||||
let chunks = engine.split_prompt(&prepared)?;
|
||||
let mut samples = Vec::new();
|
||||
for chunk in chunks {
|
||||
let prepared = prepare_april_prompt(&chunk)
|
||||
.ok_or_else(|| "Pocket TTS prompt chunk became empty".to_string())?;
|
||||
samples.extend(engine.synth_chunk(&prepared, style)?);
|
||||
}
|
||||
Ok(samples)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn desktop_model_is_april_int8_only() {
|
||||
let info = april_model_info();
|
||||
assert_eq!(info.max_token_per_chunk, 50);
|
||||
assert_eq!(info.sample_rate, SAMPLE_RATE);
|
||||
assert!(info
|
||||
.artifacts
|
||||
.iter()
|
||||
.any(|artifact| artifact.filename == "flow_lm_main_int8.onnx"));
|
||||
assert!(!info
|
||||
.artifacts
|
||||
.iter()
|
||||
.any(|artifact| artifact.filename == "flow_lm_main.onnx"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[ignore = "requires BUZZ_POCKET_TEST_MODEL_DIR"]
|
||||
fn production_api_emits_non_silent_april_int8_pcm() {
|
||||
let dir = std::env::var("BUZZ_POCKET_TEST_MODEL_DIR")
|
||||
.expect("set BUZZ_POCKET_TEST_MODEL_DIR to an April INT8 model directory");
|
||||
let engine = load_text_to_speech(&dir).expect("load April INT8 engine");
|
||||
let style = load_voice_style(&Path::new(&dir).join("reference_sample.wav"))
|
||||
.expect("load reference voice");
|
||||
let samples = engine
|
||||
.synth_chunk("Bright birds begin beside the bay.", "en", &style, 1)
|
||||
.expect("synthesize through the production API");
|
||||
|
||||
assert!(!samples.is_empty());
|
||||
assert!(samples.iter().all(|sample| sample.is_finite()));
|
||||
assert!(samples.iter().any(|sample| sample.abs() > 1.0e-6));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -84,6 +84,9 @@ run_unit_tests() {
|
||||
run_test_step "buzz-auth unit tests" \
|
||||
cargo test -p buzz-auth --lib -- --nocapture
|
||||
|
||||
run_test_step "buzz-voice tests" \
|
||||
cargo test -p buzz-voice --lib -- --nocapture
|
||||
|
||||
# buzz-db migrator/lint unit tests (no infra): guard the embedded-migrator
|
||||
# invariant (exactly the consolidated 0001; cutover/backfill stays an operator
|
||||
# script, not startup state) and the tenant-scoping lints. The Postgres-backed
|
||||
|
||||
Reference in New Issue
Block a user