pub mod cli;
pub mod client;
pub mod controller;
pub use controller::{Handle, Snapshot};
#[derive(Clone)]
pub struct TtsContext {
pub handle: Handle,
pub snapshot: ratatui_kit::State<Snapshot>,
}
pub mod ui;
#[derive(Debug, Clone)]
pub struct ChapterRequest {
pub source: tts_protocol::SourceId,
pub text: String,
pub text_hash: String,
pub resume_byte: Option<usize>,
pub restore_checkpoint: bool,
}
impl ChapterRequest {
fn plan(
&self,
sound: &crate::config::SoundConfig,
playback: crate::config::PlaybackPreference,
) -> tts_protocol::PlanRequest {
tts_protocol::PlanRequest {
source: self.source.clone(),
text: self.text.clone(),
text_hash: self.text_hash.clone(),
backend: sound.backend.clone(),
model: sound.model.clone(),
voices: vec![sound.voice.clone()],
spans: vec![tts_protocol::VoiceSpan {
range: tts_protocol::TextRange {
start: 0,
end: self.text.len(),
},
voice: sound.voice.clone(),
style: sound.style.clone(),
}],
sealed: true,
playback: match playback {
crate::config::PlaybackPreference::Streaming => {
tts_protocol::PlanPlayback::Streaming
}
crate::config::PlaybackPreference::AfterChapterReady => {
tts_protocol::PlanPlayback::AfterChapterReady
}
},
resume_byte: self.resume_byte,
restore_checkpoint: self.restore_checkpoint,
continuation: true,
continuation_boundary: tts_protocol::ContinuationBoundary::Scene,
continuation_span: None,
params: Default::default(),
seed: Some(chapter_seed(&self.text_hash)),
verification: Default::default(),
}
}
}
fn chapter_seed(hash: &str) -> u64 {
let mut seed = 0xcbf2_9ce4_8422_2325_u64;
for byte in hash.as_bytes() {
seed ^= u64::from(*byte);
seed = seed.wrapping_mul(0x0000_0100_0000_01b3);
}
seed
}
#[cfg(test)]
mod tests {
use super::*;
use tts_protocol::{ContinuationBoundary, SourceId, text_hash};
#[test]
fn plan_pins_a_chapter_seed_and_keeps_the_chain_across_paragraphs() {
let sound = crate::config::SoundConfig {
backend: "moss".into(),
model: Some("nano".into()),
voice: "Weiguo".into(),
style: None,
device: tts_protocol::Device::Auto,
};
let request = ChapterRequest {
source: SourceId {
namespace: "reader".into(),
book: "b".into(),
chapter: "1".into(),
},
text: "甲。\n\n乙。".into(),
text_hash: text_hash("甲。\n\n乙。"),
resume_byte: None,
restore_checkpoint: false,
};
let plan = request.plan(&sound, crate::config::PlaybackPreference::default());
assert!(plan.continuation);
assert_eq!(plan.continuation_boundary, ContinuationBoundary::Scene);
assert_eq!(plan.seed, Some(chapter_seed(&request.text_hash)));
assert_eq!(chapter_seed("chapter-a"), chapter_seed("chapter-a"));
assert_ne!(chapter_seed("chapter-a"), chapter_seed("chapter-b"));
assert_eq!(plan.playback, tts_protocol::PlanPlayback::Streaming);
assert_eq!(
request
.plan(&sound, crate::config::PlaybackPreference::AfterChapterReady)
.playback,
tts_protocol::PlanPlayback::AfterChapterReady
);
}
}