1use std::path::Path;
7
8use anyhow::{Context, Result};
9
10use crate::decoder::{SAMPLE_RATE, SAMPLES_PER_FRAME, SnacBackend, decoder_weights_path};
11use crate::tokens::{build_prompt_ids, build_voice_clone_prompt_ids, pack_orpheus_codes};
12
13#[cfg(feature = "llama")]
14use crate::backbone::{BackboneLoadOptions, BackboneModel, DEFAULT_N_CTX};
15#[cfg(feature = "llama")]
16use crate::device::OrpheusRuntimeDevice;
17#[cfg(feature = "llama")]
18use rlx_runtime::Device;
19
20#[derive(Debug, Clone)]
22pub struct GenerationConfig {
23 pub max_new_tokens: u32,
24 pub temperature: f32,
25 pub top_p: f32,
26 pub top_k: usize,
27 pub repetition_penalty: f32,
28 pub seed: u64,
29 pub greedy: bool,
30}
31
32impl Default for GenerationConfig {
33 fn default() -> Self {
34 Self {
35 max_new_tokens: 1200,
36 temperature: 0.6,
37 top_p: 0.8,
38 top_k: 0,
39 repetition_penalty: 1.3,
40 seed: 42,
41 greedy: false,
42 }
43 }
44}
45
46#[derive(Debug, Clone)]
48pub struct SynthesisResult {
49 pub samples: Vec<f32>,
50 pub sample_rate: u32,
51 pub code_count: usize,
52 pub codes: Vec<i32>,
53}
54
55pub struct OrpheusTts {
57 #[cfg(feature = "llama")]
58 pub backbone: BackboneModel,
59 pub snac: SnacBackend,
60 pub config: GenerationConfig,
61}
62
63impl OrpheusTts {
64 #[cfg(feature = "llama")]
65 pub fn load(backbone_path: &Path, snac_path: &Path) -> Result<Self> {
66 Self::load_on(backbone_path, snac_path, Device::Cpu)
67 }
68
69 #[cfg(feature = "llama")]
70 pub fn load_on(backbone_path: &Path, snac_path: &Path, device: Device) -> Result<Self> {
71 Self::load_on_with(
72 backbone_path,
73 snac_path,
74 OrpheusRuntimeDevice {
75 lm: device,
76 snac_coreml: false,
77 },
78 BackboneLoadOptions::for_device(device),
79 )
80 }
81
82 #[cfg(feature = "llama")]
83 pub fn load_on_with(
84 backbone_path: &Path,
85 snac_path: &Path,
86 runtime: OrpheusRuntimeDevice,
87 backbone_opts: BackboneLoadOptions,
88 ) -> Result<Self> {
89 let backbone =
90 BackboneModel::load_on_with(backbone_path, DEFAULT_N_CTX, runtime.lm, backbone_opts)
91 .context("load Orpheus GGUF backbone")?;
92 let snac = SnacBackend::open(snac_path, runtime.snac_load_options())
93 .context("load SNAC decoder")?;
94 Ok(Self {
95 backbone,
96 snac,
97 config: GenerationConfig::default(),
98 })
99 }
100
101 #[cfg(feature = "llama")]
102 pub fn load_on_with_device(
103 backbone_path: &Path,
104 snac_path: &Path,
105 device: Device,
106 backbone_opts: BackboneLoadOptions,
107 ) -> Result<Self> {
108 Self::load_on_with(
109 backbone_path,
110 snac_path,
111 OrpheusRuntimeDevice {
112 lm: device,
113 snac_coreml: false,
114 },
115 backbone_opts,
116 )
117 }
118
119 #[cfg(feature = "llama")]
120 pub fn load_with_env_decoder(backbone_path: &Path) -> Result<Self> {
121 let snac_path = decoder_weights_path()?;
122 Self::load(backbone_path, &snac_path)
123 }
124
125 #[cfg(feature = "llama")]
126 pub fn load_with_env_decoder_on(backbone_path: &Path, device: Device) -> Result<Self> {
127 let snac_path = decoder_weights_path()?;
128 Self::load_on(backbone_path, &snac_path, device)
129 }
130
131 #[cfg(feature = "llama")]
133 pub fn synthesize(&self, text: &str, voice: Option<&str>) -> Result<SynthesisResult> {
134 let body = match voice {
135 Some(v) => format!("{v}: {text}"),
136 None => text.to_string(),
137 };
138 let prompt_ids = build_prompt_ids(self.backbone.weights_path(), &body)?;
139 self.synthesize_from_prompt_ids(&prompt_ids)
140 }
141
142 #[cfg(feature = "llama")]
149 pub fn synthesize_voice_clone(
150 &self,
151 ref_text: &str,
152 ref_audio_token_ids: &[u32],
153 target_text: &str,
154 ) -> Result<SynthesisResult> {
155 let prompt_ids = build_voice_clone_prompt_ids(
156 self.backbone.weights_path(),
157 ref_text,
158 ref_audio_token_ids,
159 target_text,
160 )?;
161 self.synthesize_from_prompt_ids(&prompt_ids)
162 }
163
164 #[cfg(feature = "llama")]
166 pub fn synthesize_stream(
167 &self,
168 text: &str,
169 voice: Option<&str>,
170 mut on_pcm: impl FnMut(&[f32]) -> Result<()>,
171 ) -> Result<SynthesisResult> {
172 let body = match voice {
173 Some(v) => format!("{v}: {text}"),
174 None => text.to_string(),
175 };
176 let prompt_ids = build_prompt_ids(self.backbone.weights_path(), &body)?;
177 let mut codes = Vec::new();
178 let mut pcm = Vec::new();
179
180 self.backbone
181 .generate_codes_from_prompt_streaming(&prompt_ids, &self.config, |code| {
182 codes.push(code);
183 if codes.len() >= 28 && codes.len() % 7 == 0 {
184 let chunk = decode_stream_chunk(&self.snac, &codes[codes.len() - 28..])?;
185 on_pcm(&chunk)?;
186 pcm.extend_from_slice(&chunk);
187 }
188 Ok(())
189 })?;
190
191 if codes.len() >= 7 {
192 let samples = decode_orpheus_codes(&self.snac, &codes)?;
193 if samples.len() > pcm.len() {
194 let tail = &samples[pcm.len()..];
195 on_pcm(tail)?;
196 pcm.extend_from_slice(tail);
197 }
198 }
199
200 Ok(SynthesisResult {
201 samples: pcm,
202 sample_rate: SAMPLE_RATE,
203 code_count: codes.len(),
204 codes,
205 })
206 }
207
208 #[cfg(feature = "llama")]
210 pub fn synthesize_from_prompt_ids(&self, prompt_ids: &[u32]) -> Result<SynthesisResult> {
211 let codes = self
212 .backbone
213 .generate_codes_from_prompt(prompt_ids, &self.config)
214 .context("LM code generation")?;
215
216 let samples = self.decode_all_codes(&codes)?;
217 Ok(SynthesisResult {
218 samples,
219 sample_rate: SAMPLE_RATE,
220 code_count: codes.len(),
221 codes,
222 })
223 }
224
225 pub fn decode_all_codes(&self, codes: &[i32]) -> Result<Vec<f32>> {
227 decode_orpheus_codes(&self.snac, codes)
228 }
229
230 pub fn decode_codes_to_pcm(&self, codes: &[i32]) -> Result<Vec<f32>> {
231 self.decode_all_codes(codes)
232 }
233}
234
235const WINDOW_FRAMES: usize = 4;
236const WINDOW_CODES: usize = WINDOW_FRAMES * 7;
237
238pub fn decode_stream_chunk(snac: &SnacBackend, recent_28_codes: &[i32]) -> Result<Vec<f32>> {
240 anyhow::ensure!(
241 recent_28_codes.len() == WINDOW_CODES,
242 "stream chunk expects {WINDOW_CODES} codes, got {}",
243 recent_28_codes.len()
244 );
245 let audio = snac.decode_orpheus_frames(recent_28_codes)?;
246 let mut chunk = Vec::new();
247 append_center_pcm(&audio, &mut chunk);
248 Ok(chunk)
249}
250
251pub fn decode_orpheus_codes(snac: &SnacBackend, codes: &[i32]) -> Result<Vec<f32>> {
257 if codes.is_empty() {
258 return Ok(Vec::new());
259 }
260
261 let num_frames = codes.len() / 7;
262 if num_frames == 0 {
263 return Ok(Vec::new());
264 }
265
266 let aligned = &codes[..num_frames * 7];
267 let (c0, c1, c2) = pack_orpheus_codes(aligned).ok_or_else(|| {
268 anyhow::anyhow!(
269 "invalid code layout ({} codes, first={:?})",
270 aligned.len(),
271 &aligned[..aligned.len().min(8)]
272 )
273 })?;
274 snac.decode_codes(&c0, &c1, &c2)
275}
276
277pub fn streaming_window_starts(num_codes: usize) -> Vec<usize> {
279 if num_codes < WINDOW_CODES {
280 return Vec::new();
281 }
282 (0..=num_codes - WINDOW_CODES).step_by(7).collect()
283}
284
285fn append_center_pcm(audio: &[f32], pcm: &mut Vec<f32>) {
286 let take_start = SAMPLES_PER_FRAME.min(audio.len());
287 let take_end = (SAMPLES_PER_FRAME * 2).min(audio.len());
288 if take_end > take_start {
289 pcm.extend_from_slice(&audio[take_start..take_end]);
290 }
291}
292
293#[cfg(test)]
294mod tests {
295 use super::*;
296
297 #[test]
298 fn streaming_window_starts_match_python() {
299 assert_eq!(streaming_window_starts(35), vec![0, 7]);
301 assert_eq!(streaming_window_starts(56), vec![0, 7, 14, 21, 28]);
303 }
304
305 #[test]
306 fn batch_decode_longer_than_streaming_centers() {
307 let streaming_samples = streaming_window_starts(84).len() * SAMPLES_PER_FRAME;
309 let full_samples = 12 * SAMPLES_PER_FRAME;
310 assert_eq!(streaming_samples + 3 * SAMPLES_PER_FRAME, full_samples);
311 }
312}