Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 3 additions & 3 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -106,9 +106,9 @@ icu_displaynames = { version = "0.11", optional = true, features = ["compiled_da
icu_locid = { version = "1.0.0-beta1", optional = true }
# floravox: event-driven SSML synthesis (piper/MMS VITS, Matcha,
# Kokoro) with measured word timings (vs sherpa-onnx's estimated ones).
floravox-core = { version = "0.7.1", optional = true }
floravox-ssml = { version = "0.7.2", optional = true }
floravox-g2p = { version = "0.7.1", optional = true, default-features = false }
floravox-core = { version = "0.8.1", optional = true }
floravox-ssml = { version = "0.8.1", optional = true }
floravox-g2p = { version = "0.8.1", optional = true, default-features = false }
# Published lexicon bundles (gruut-derived, MIT) + per-language
# Phonetisaurus models, fetched and cached by lang code.
voicegarden-lexicons = { version = "0.2.0", optional = true }
Expand Down
16 changes: 9 additions & 7 deletions examples/advanced-features-demo.rs
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,7 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
1.0,
None,
None,
None,
) {
Ok(_) => println!(" ✅ Voice test successful"),
Err(e) => println!(" ⚠️ Voice test failed: {}", e),
Expand Down Expand Up @@ -92,27 +93,27 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
let test_text = "This demonstrates prosody control in text-to-speech synthesis.";

println!("🐢 Slow speech (0.7x rate):");
engine.speak(test_text, None, 0.7, 1.0, 1.0, None, None)?;
engine.speak(test_text, None, 0.7, 1.0, 1.0, None, None, None)?;
std::thread::sleep(std::time::Duration::from_millis(100));

println!("🐇 Fast speech (1.5x rate):");
engine.speak(test_text, None, 1.5, 1.0, 1.0, None, None)?;
engine.speak(test_text, None, 1.5, 1.0, 1.0, None, None, None)?;
std::thread::sleep(std::time::Duration::from_millis(100));

println!("🔉 Low pitch (0.8x):");
engine.speak(test_text, None, 1.0, 0.8, 1.0, None, None)?;
engine.speak(test_text, None, 1.0, 0.8, 1.0, None, None, None)?;
std::thread::sleep(std::time::Duration::from_millis(100));

println!("🔈 High pitch (1.2x):");
engine.speak(test_text, None, 1.0, 1.2, 1.0, None, None)?;
engine.speak(test_text, None, 1.0, 1.2, 1.0, None, None, None)?;
std::thread::sleep(std::time::Duration::from_millis(100));

println!("🔉 Low volume (0.5x):");
engine.speak(test_text, None, 1.0, 1.0, 0.5, None, None)?;
engine.speak(test_text, None, 1.0, 1.0, 0.5, None, None, None)?;
std::thread::sleep(std::time::Duration::from_millis(100));

println!("🔊 High volume (1.5x):");
engine.speak(test_text, None, 1.0, 1.0, 1.5, None, None)?;
engine.speak(test_text, None, 1.0, 1.0, 1.5, None, None, None)?;

println!("✅ Prosody control demo complete\n");
}
Expand Down Expand Up @@ -172,6 +173,7 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
1.0,
None,
None,
None,
) {
Ok(_) => println!(" ✅ Synthesis successful"),
Err(e) => println!(" ❌ Synthesis failed: {}", e),
Expand Down Expand Up @@ -223,7 +225,7 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
"#;

println!("Testing SSML input...");
match engine.speak(ssml_example, None, 1.0, 1.0, 1.0, None, None) {
match engine.speak(ssml_example, None, 1.0, 1.0, 1.0, None, None, None) {
Ok(_) => println!("✅ SSML synthesis successful"),
Err(e) => println!("❌ SSML synthesis failed: {}", e),
}
Expand Down
1 change: 1 addition & 0 deletions examples/edge-bare-envelope.rs
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ fn main() {
1.0,
Some(&mut |chunk: &[u8]| bytes += chunk.len()),
Some(&mut |_w, _s, _e, _o, _l| words += 1),
None,
)
.unwrap_or_else(|e| panic!("{text}: speak failed: {e}"));
println!("{text:?}: {bytes} PCM bytes, {words} word boundaries");
Expand Down
1 change: 1 addition & 0 deletions examples/edge-speechmarkdown.rs
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ fn main() {
1.0,
Some(&mut |chunk: &[u8]| bytes += chunk.len()),
None,
None,
)
.unwrap_or_else(|e| panic!("{text}: {e}"));
println!("{text:?}: {bytes} bytes");
Expand Down
1 change: 1 addition & 0 deletions examples/edge-style.rs
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ fn main() {
1.0,
Some(&mut |c: &[u8]| bytes += c.len()),
None,
None,
)
.unwrap_or_else(|e| panic!("{text}: {e}"));
println!("{text:?}: {bytes} bytes");
Expand Down
2 changes: 1 addition & 1 deletion examples/floravox-stream-demo.rs
Original file line number Diff line number Diff line change
Expand Up @@ -54,8 +54,8 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
Some(&mut |_w, _s, _e, _o, _l| {
nb.fetch_add(1, Ordering::SeqCst);
}),
None,
)?;

let total = t0.elapsed().as_millis() as u64;
let fa = first_audio.load(Ordering::SeqCst);
println!("first audio after {fa} ms");
Expand Down
14 changes: 8 additions & 6 deletions examples/quick-start.rs
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
1.0, // Normal volume
None, // No audio callback (direct playback)
None, // No word boundary callback
None, // No mark callback
)?;

std::thread::sleep(std::time::Duration::from_millis(500));
Expand Down Expand Up @@ -64,6 +65,7 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
1.2, // Louder volume
None,
None,
None,
)?;

std::thread::sleep(std::time::Duration::from_millis(500));
Expand All @@ -86,23 +88,23 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
let test_sentence = "This demonstrates rate and pitch control in text-to-speech.";

println!("🐢 Slow speech:");
engine.speak(test_sentence, None, 0.7, 1.0, 1.0, None, None)?;
engine.speak(test_sentence, None, 0.7, 1.0, 1.0, None, None, None)?;
std::thread::sleep(std::time::Duration::from_millis(100));

println!("🎯 Normal speech:");
engine.speak(test_sentence, None, 1.0, 1.0, 1.0, None, None)?;
engine.speak(test_sentence, None, 1.0, 1.0, 1.0, None, None, None)?;
std::thread::sleep(std::time::Duration::from_millis(100));

println!("🐇 Fast speech:");
engine.speak(test_sentence, None, 1.5, 1.0, 1.0, None, None)?;
engine.speak(test_sentence, None, 1.5, 1.0, 1.0, None, None, None)?;
std::thread::sleep(std::time::Duration::from_millis(100));

println!("🔈 Low pitch:");
engine.speak(test_sentence, None, 1.0, 0.8, 1.0, None, None)?;
engine.speak(test_sentence, None, 1.0, 0.8, 1.0, None, None, None)?;
std::thread::sleep(std::time::Duration::from_millis(100));

println!("🔊 High pitch:");
engine.speak(test_sentence, None, 1.0, 1.2, 1.0, None, None)?;
engine.speak(test_sentence, None, 1.0, 1.2, 1.0, None, None, None)?;

// Example 4: Saving to file
println!("\n\nExample 4: Saving Audio to File");
Expand Down Expand Up @@ -131,7 +133,7 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
println!("let engine = create_engine(\"system\", \"\")?;");
println!("");
println!("// Simple speech");
println!("engine.speak(\"Hello!\", None, 1.0, 1.0, 1.0, None, None)?;");
println!("engine.speak(\"Hello!\", None, 1.0, 1.0, 1.0, None, None, None)?;");
println!("");
println!("// Save to file");
println!("let audio = engine.synth_to_bytes(\"Hello file!\", None, 1.0, 1.0, 1.0)?;");
Expand Down
1 change: 1 addition & 0 deletions examples/streaming-audio-demo.rs
Original file line number Diff line number Diff line change
Expand Up @@ -145,6 +145,7 @@ fn stream_to_file(
}
}),
None, // no boundary callback
None, // no mark callback
)?;

println!(
Expand Down
1 change: 1 addition & 0 deletions examples/word-boundary-demo.rs
Original file line number Diff line number Diff line change
Expand Up @@ -153,6 +153,7 @@ fn demonstrate_word_boundaries(
1.0, // volume
None, // no audio callback for this demo
Some(&mut boundary_callback),
None, // no mark callback for this demo
)?;

// Give time for all events to complete
Expand Down
35 changes: 35 additions & 0 deletions include/tts_wrapper.h
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,21 @@ typedef void (*CBoundaryCb)(const char*, float, float, void*);

typedef void (*CBoundaryCb2)(const char*, int32_t, int32_t, float, float, void*);

/**
* Mark/bookmark callback: cb(name, char_offset, start_s, end_s, userdata).
* char_offset is -1 when unknown; start/end are the measured (or
* estimated) audio position the mark fires at.
*/
typedef void (*CMarkCb)(const char*, int32_t, float, float, void*);

/**
* Boundary callback with the estimated flag: cb(word, char_offset,
* char_len, start_s, end_s, estimated, userdata). `estimated` is 1 when
* the timings are proportional estimates (unpatched voice), 0 when
* measured from the model's duration tensor.
*/
typedef void (*CBoundaryCb3)(const char*, int32_t, int32_t, float, float, int32_t, void*);

typedef void (*CVisemeCb)(int32_t, float, void*);

typedef void (*CVoidCb)(void*);
Expand Down Expand Up @@ -235,6 +250,26 @@ void tts_set_on_boundary(struct tts_ctx *ctx, CBoundaryCb cb, void *userdata);
*/
void tts_set_on_boundary2(struct tts_ctx *ctx, CBoundaryCb2 cb, void *userdata);

/**
* Set the mark/bookmark callback: cb(name, char_offset, start_s, end_s, userdata).
* Fires for `<mark>`/`<bookmark>` SSML tags at their measured audio
* position on engines that report them (floravox).
*
* # Safety
* `ctx` must be valid.
*/
void tts_set_on_mark(struct tts_ctx *ctx, CMarkCb cb, void *userdata);

/**
* Boundary callback with the estimated flag:
* cb(word, char_offset, char_len, start_s, end_s, estimated, userdata).
* `estimated` != 0 means proportional estimates, not measured timings.
*
* # Safety
* `ctx` must be valid.
*/
void tts_set_on_boundary3(struct tts_ctx *ctx, CBoundaryCb3 cb, void *userdata);

/**
* Viseme callback for lip-sync / facial animation.
* cb(viseme_id, audio_offset_sec, userdata)
Expand Down
13 changes: 12 additions & 1 deletion src/avsynth_engine.rs
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,7 @@ impl TtsEngine for AvSynthEngine {
volume: f32,
_on_audio: Option<crate::engine::OnAudioCallback>,
mut on_boundary: Option<crate::engine::OnBoundaryCallback>,
_on_mark: Option<crate::engine::OnMarkCallback>,
) -> TtsResult<()> {
let guard = self.handle.lock().unwrap();
if guard.is_null() {
Expand Down Expand Up @@ -133,8 +134,18 @@ impl TtsEngine for AvSynthEngine {
volume: f32,
on_audio: Option<crate::engine::OnAudioCallback>,
on_boundary: Option<crate::engine::OnBoundaryCallback>,
on_mark: Option<crate::engine::OnMarkCallback>,
) -> TtsResult<()> {
self.speak(text, voice, rate, pitch, volume, on_audio, on_boundary)
self.speak(
text,
voice,
rate,
pitch,
volume,
on_audio,
on_boundary,
on_mark,
)
}

fn stop(&self) -> TtsResult<()> {
Expand Down
13 changes: 12 additions & 1 deletion src/cloud_engine.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1830,6 +1830,7 @@ impl TtsEngine for CloudEngine {
volume: f32,
mut on_audio: Option<crate::engine::OnAudioCallback>,
mut on_boundary: Option<crate::engine::OnBoundaryCallback>,
_on_mark: Option<crate::engine::OnMarkCallback>,
) -> TtsResult<()> {
let (original_text, is_ssml) = preprocess_speech_markdown(text, &self.config.provider_id);

Expand Down Expand Up @@ -2525,8 +2526,18 @@ impl TtsEngine for CloudEngine {
volume: f32,
on_audio: Option<crate::engine::OnAudioCallback>,
on_boundary: Option<crate::engine::OnBoundaryCallback>,
on_mark: Option<crate::engine::OnMarkCallback>,
) -> TtsResult<()> {
self.speak(text, voice, rate, pitch, volume, on_audio, on_boundary)
self.speak(
text,
voice,
rate,
pitch,
volume,
on_audio,
on_boundary,
on_mark,
)
}

fn stop(&self) -> TtsResult<()> {
Expand Down
15 changes: 15 additions & 0 deletions src/engine.rs
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,14 @@ pub type OnAudioCallback<'a> = &'a mut dyn FnMut(&[u8]);
/// char_offset/char_len are -1 when the engine doesn't report them.
pub type OnBoundaryCallback<'a> = &'a mut dyn FnMut(&str, f32, f32, i32, i32);

/// Callback for SSML mark/bookmark events.
/// Signature: (name, start_sec, end_sec, char_offset)
/// char_offset is -1 when the engine doesn't report it. Engines that
/// support native `<mark>` (floravox) fire this at the measured (or
/// estimated) audio position; consumers map it to their bookmark event
/// (SAPI `SPEI_TTS_BOOKMARK`, SSIP index marks).
pub type OnMarkCallback<'a> = &'a mut dyn FnMut(&str, f32, f32, i32);

/// Callback for speech-started events.
pub type OnStartCallback<'a> = &'a mut dyn FnMut();

Expand Down Expand Up @@ -143,6 +151,7 @@ fn extract_name_attr(tag: &str) -> Option<String> {
pub trait TtsEngine: Send + Sync + fmt::Debug {
/// Start speaking `text` asynchronously.
#[allow(clippy::too_many_arguments)]
#[allow(clippy::too_many_arguments)]
fn speak(
&self,
text: &str,
Expand All @@ -152,6 +161,7 @@ pub trait TtsEngine: Send + Sync + fmt::Debug {
volume: f32,
on_audio: Option<OnAudioCallback>,
on_boundary: Option<OnBoundaryCallback>,
on_mark: Option<OnMarkCallback>,
) -> TtsResult<()>;

/// Speak with full [`SpeakOptions`], matching Swift's `speak(_:options:)`.
Expand All @@ -161,6 +171,7 @@ pub trait TtsEngine: Send + Sync + fmt::Debug {
options: Option<&SpeakOptions>,
on_audio: Option<OnAudioCallback>,
on_boundary: Option<OnBoundaryCallback>,
on_mark: Option<OnMarkCallback>,
) -> TtsResult<()> {
let opts = options.cloned().unwrap_or_default();
self.speak(
Expand All @@ -171,11 +182,13 @@ pub trait TtsEngine: Send + Sync + fmt::Debug {
opts.effective_volume(),
on_audio,
on_boundary,
on_mark,
)
}

/// Speak `text` synchronously, blocking until synthesis completes.
#[allow(clippy::too_many_arguments)]
#[allow(clippy::too_many_arguments)]
fn speak_sync(
&self,
text: &str,
Expand All @@ -185,6 +198,7 @@ pub trait TtsEngine: Send + Sync + fmt::Debug {
volume: f32,
on_audio: Option<OnAudioCallback>,
on_boundary: Option<OnBoundaryCallback>,
on_mark: Option<OnMarkCallback>,
) -> TtsResult<()>;

/// Stop any in-progress speech.
Expand Down Expand Up @@ -236,6 +250,7 @@ pub trait TtsEngine: Send + Sync + fmt::Debug {
buf.extend_from_slice(chunk);
}),
None,
None,
)?;
Ok(buf)
}
Expand Down
Loading
Loading