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
2 changes: 1 addition & 1 deletion examples/edge-bare-envelope.rs
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ fn main() {
1.0,
1.0,
Some(&mut |chunk: &[u8]| bytes += chunk.len()),
Some(&mut |_w, _s, _e, _o, _l| words += 1),
Some(&mut |_w, _s, _e, _o, _l, _est| words += 1),
None,
)
.unwrap_or_else(|e| panic!("{text}: speak failed: {e}"));
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 @@ -51,7 +51,7 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
chunk.len() as u64 / 2 / 16,
));
}),
Some(&mut |_w, _s, _e, _o, _l| {
Some(&mut |_w, _s, _e, _o, _l, _est| {
nb.fetch_add(1, Ordering::SeqCst);
}),
None,
Expand Down
53 changes: 27 additions & 26 deletions examples/word-boundary-demo.rs
Original file line number Diff line number Diff line change
Expand Up @@ -117,32 +117,33 @@ fn demonstrate_word_boundaries(
println!("🎵 Starting speech synthesis with word boundary events...\n");

// Set up word boundary callback
let mut boundary_callback = move |word: &str, start: f32, end: f32, _offset: i32, _len: i32| {
let mut word_list = words_clone.lock().unwrap();
word_list.push((word.to_string(), start, end));

// Simulate word highlighting in a UI
let current_word = word;
let highlighted = word_list
.iter()
.map(|(w, _, _)| {
if w.to_lowercase() == current_word.to_lowercase() {
format!("[{}]", w)
} else {
w.clone()
}
})
.collect::<Vec<_>>()
.join(" ");

println!(
"📍 Word: \"{}\" | Time: {:.3}s - {:.3}s",
current_word, start, end
);
println!(" Highlighted: {}", highlighted);

*count_clone.lock().unwrap() += 1;
};
let mut boundary_callback =
move |word: &str, start: f32, end: f32, _offset: i32, _len: i32, _estimated: bool| {
let mut word_list = words_clone.lock().unwrap();
word_list.push((word.to_string(), start, end));

// Simulate word highlighting in a UI
let current_word = word;
let highlighted = word_list
.iter()
.map(|(w, _, _)| {
if w.to_lowercase() == current_word.to_lowercase() {
format!("[{}]", w)
} else {
w.clone()
}
})
.collect::<Vec<_>>()
.join(" ");

println!(
"📍 Word: \"{}\" | Time: {:.3}s - {:.3}s",
current_word, start, end
);
println!(" Highlighted: {}", highlighted);

*count_clone.lock().unwrap() += 1;
};

// Start speaking with word boundary events
engine.speak(
Expand Down
1 change: 1 addition & 0 deletions src/avsynth_engine.rs
Original file line number Diff line number Diff line change
Expand Up @@ -119,6 +119,7 @@ impl TtsEngine for AvSynthEngine {
(b.offset + b.duration) as f32 / 1000.0,
-1,
-1,
true, // wpm estimates
);
}
}
Expand Down
2 changes: 2 additions & 0 deletions src/boundaries.rs
Original file line number Diff line number Diff line change
Expand Up @@ -223,11 +223,13 @@ mod tests {
text: "hello".into(),
offset: 0,
duration: 400,
estimated: false,
},
WordBoundary {
text: "world".into(),
offset: 400,
duration: 400,
estimated: false,
},
];
let plan = EstimatePlan::from_estimates(&est, "hello world");
Expand Down
14 changes: 12 additions & 2 deletions src/cloud_engine.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1400,6 +1400,7 @@ fn parse_google_timepoints(
text: word.clone(),
offset: tp.time_ms,
duration,
estimated: false,
});
}
boundaries
Expand Down Expand Up @@ -2163,6 +2164,7 @@ impl TtsEngine for CloudEngine {
/ 1000.0,
final_offset,
final_len,
false,
);
}
}
Expand Down Expand Up @@ -2400,7 +2402,7 @@ impl TtsEngine for CloudEngine {
search_from = char_offset as usize + word.len();
}
let char_len = word.chars().count() as i32;
cb(&word, start, end, char_offset, char_len);
cb(&word, start, end, char_offset, char_len, false);
}
}
}
Expand Down Expand Up @@ -2445,6 +2447,7 @@ impl TtsEngine for CloudEngine {
(b.offset + b.duration) as f32 / 1000.0,
-1,
-1,
false,
);
}
} else {
Expand All @@ -2467,6 +2470,7 @@ impl TtsEngine for CloudEngine {
(b.offset + b.duration) as f32 / 1000.0,
char_offset,
char_len,
false,
);
}
}
Expand All @@ -2488,7 +2492,7 @@ impl TtsEngine for CloudEngine {
}
StreamEvt::Boundary(word, start, end, offset, len) => {
if let Some(bcb) = on_boundary.as_mut() {
bcb(word, start, end, offset, len);
bcb(word, start, end, offset, len, false);
}
}
};
Expand Down Expand Up @@ -4466,6 +4470,7 @@ mod tests {
text: "Hi".into(),
offset: 0,
duration: 0,
estimated: false,
}];
compute_durations(&mut v);
assert_eq!(v[0].duration, 500);
Expand All @@ -4478,16 +4483,19 @@ mod tests {
text: "a".into(),
offset: 0,
duration: 0,
estimated: false,
},
WordBoundary {
text: "b".into(),
offset: 300,
duration: 0,
estimated: false,
},
WordBoundary {
text: "c".into(),
offset: 700,
duration: 0,
estimated: false,
},
];
compute_durations(&mut v);
Expand All @@ -4503,11 +4511,13 @@ mod tests {
text: "a".into(),
offset: 0,
duration: 250,
estimated: false,
},
WordBoundary {
text: "b".into(),
offset: 250,
duration: 0,
estimated: false,
},
];
compute_durations(&mut v);
Expand Down
8 changes: 6 additions & 2 deletions src/engine.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,9 +7,12 @@ use std::fmt;
pub type OnAudioCallback<'a> = &'a mut dyn FnMut(&[u8]);

/// Callback for word boundary events.
/// Signature: (word, start_sec, end_sec, char_offset, char_len)
/// Signature: (word, start_sec, end_sec, char_offset, char_len, estimated)
/// 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);
/// `estimated` is true for proportional estimates (unpatched voices,
/// sherpa-onnx's wpm model) and false for measured timings (floravox
/// duration tensor, cloud provider timings).
pub type OnBoundaryCallback<'a> = &'a mut dyn FnMut(&str, f32, f32, i32, i32, bool);

/// Callback for SSML mark/bookmark events.
/// Signature: (name, start_sec, end_sec, char_offset)
Expand Down Expand Up @@ -318,6 +321,7 @@ pub fn estimate_word_boundaries_with_wpm(text: &str, words_per_minute: f64) -> V
text: (*word).to_string(),
offset: current_ms,
duration,
estimated: true,
});
current_ms += duration;
}
Expand Down
10 changes: 9 additions & 1 deletion src/floravox_engine.rs
Original file line number Diff line number Diff line change
Expand Up @@ -344,12 +344,20 @@ impl FloravoxEngine {
#[allow(clippy::cast_precision_loss)]
let (s, e) = (w.ms_start as f32 / 1000.0, w.ms_end as f32 / 1000.0);
#[allow(clippy::cast_possible_wrap)]
cb(&w.text, s, e, w.char_offset as i32, w.char_len as i32);
cb(
&w.text,
s,
e,
w.char_offset as i32,
w.char_len as i32,
w.estimated,
);
}
boundaries.push(WordBoundary {
text: w.text.clone(),
offset: w.ms_start,
duration: w.ms_end.saturating_sub(w.ms_start),
estimated: w.estimated,
});
};
loop {
Expand Down
36 changes: 30 additions & 6 deletions src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -84,7 +84,7 @@ pub type CVisemeCb = Option<extern "C" fn(i32, f32, *mut std::ffi::c_void)>;
pub type CVoidCb = Option<extern "C" fn(*mut std::ffi::c_void)>;
pub type CErrorCb = Option<extern "C" fn(*const c_char, *mut std::ffi::c_void)>;
type BoxedAudioCb = Box<dyn FnMut(&[u8])>;
type BoxedBoundaryCb = Box<dyn FnMut(&str, f32, f32, i32, i32)>;
type BoxedBoundaryCb = Box<dyn FnMut(&str, f32, f32, i32, i32, bool)>;
type BoxedMarkCb = Box<dyn FnMut(&str, f32, f32, i32)>;

pub struct tts_ctx {
Expand Down Expand Up @@ -420,7 +420,12 @@ fn tts_speak_impl_inner(ctx: *mut tts_ctx, text: *const c_char, raw_ssml: bool)
let mut on_boundary_closure: Option<BoxedBoundaryCb> = match (boundary.cb, boundary2.cb) {
(None, None) => None,
_ => Some(Box::new(
move |word: &str, start: f32, end: f32, char_offset: i32, char_len: i32| {
move |word: &str,
start: f32,
end: f32,
char_offset: i32,
char_len: i32,
estimated: bool| {
if let Some(cb) = boundary.cb {
if let Ok(c_word) = CString::new(word) {
cb(c_word.as_ptr(), start, end, boundary.userdata);
Expand All @@ -446,7 +451,7 @@ fn tts_speak_impl_inner(ctx: *mut tts_ctx, text: *const c_char, raw_ssml: bool)
char_len,
start,
end,
0,
i32::from(estimated),
boundary3.userdata,
);
}
Expand Down Expand Up @@ -498,7 +503,7 @@ fn tts_speak_impl_inner(ctx: *mut tts_ctx, text: *const c_char, raw_ssml: bool)
.map(|f| &mut **f as &mut dyn FnMut(&[u8])),
on_boundary_closure
.as_mut()
.map(|f| &mut **f as &mut dyn FnMut(&str, f32, f32, i32, i32)),
.map(|f| &mut **f as &mut dyn FnMut(&str, f32, f32, i32, i32, bool)),
on_mark_closure
.as_mut()
.map(|f| &mut **f as &mut dyn FnMut(&str, f32, f32, i32)),
Expand Down Expand Up @@ -553,6 +558,7 @@ pub extern "C" fn tts_speak_sync(ctx: *mut tts_ctx, text: *const c_char) -> i32
let audio = { *ctx_ref.on_audio.lock().unwrap() };
let boundary = { *ctx_ref.on_boundary.lock().unwrap() };
let boundary2 = { *ctx_ref.on_boundary2.lock().unwrap() };
let boundary3 = { *ctx_ref.on_boundary3.lock().unwrap() };
let mark = { *ctx_ref.on_mark.lock().unwrap() };

let mut on_mark_closure: Option<BoxedMarkCb> = mark.cb.map(|cb| {
Expand All @@ -576,7 +582,12 @@ pub extern "C" fn tts_speak_sync(ctx: *mut tts_ctx, text: *const c_char) -> i32
let mut on_boundary_closure: Option<BoxedBoundaryCb> = match (boundary.cb, boundary2.cb) {
(None, None) => None,
_ => Some(Box::new(
move |word: &str, start: f32, end: f32, char_offset: i32, char_len: i32| {
move |word: &str,
start: f32,
end: f32,
char_offset: i32,
char_len: i32,
estimated: bool| {
if let Some(cb) = boundary.cb {
if let Ok(c_word) = CString::new(word) {
cb(c_word.as_ptr(), start, end, boundary.userdata);
Expand All @@ -594,6 +605,19 @@ pub extern "C" fn tts_speak_sync(ctx: *mut tts_ctx, text: *const c_char) -> i32
);
}
}
if let Some(cb) = boundary3.cb {
if let Ok(c_word) = CString::new(word) {
cb(
c_word.as_ptr(),
char_offset,
char_len,
start,
end,
i32::from(estimated),
boundary3.userdata,
);
}
}
},
)),
};
Expand Down Expand Up @@ -632,7 +656,7 @@ pub extern "C" fn tts_speak_sync(ctx: *mut tts_ctx, text: *const c_char) -> i32
.map(|f| &mut **f as &mut dyn FnMut(&[u8])),
on_boundary_closure
.as_mut()
.map(|f| &mut **f as &mut dyn FnMut(&str, f32, f32, i32, i32)),
.map(|f| &mut **f as &mut dyn FnMut(&str, f32, f32, i32, i32, bool)),
on_mark_closure
.as_mut()
.map(|f| &mut **f as &mut dyn FnMut(&str, f32, f32, i32)),
Expand Down
5 changes: 3 additions & 2 deletions src/sapi_engine.rs
Original file line number Diff line number Diff line change
Expand Up @@ -180,7 +180,7 @@ impl SapiEngine {
#[allow(clippy::cast_precision_loss)]
let end = (b.offset + b.duration) as f32 / 1000.0;
let char_len = b.text.chars().count() as i32;
cb(&b.text, start, end, char_offset, char_len);
cb(&b.text, start, end, char_offset, char_len, true); // estimates
}
}
return Ok(());
Expand Down Expand Up @@ -248,7 +248,8 @@ impl SapiEngine {
let start_sec = audio_offset_ms as f32 / 1000.0;
#[allow(clippy::cast_precision_loss)]
let end_sec = (audio_offset_ms + 1) as f32 / 1000.0;
cb(&word, start_sec, end_sec, pos as i32, len as i32);
cb(&word, start_sec, end_sec, pos as i32, len as i32, true);
// bookmark spans
}
}
}
Expand Down
Loading
Loading