1use ferraus_core::{AudioBuffer, Error, ErrorKind, OpCtx, Result};
5use serde_json::{json, Value};
6
7#[derive(Clone, Debug)]
10pub struct ModelSlot {
11 pub pattern: &'static str,
13 pub default_name: &'static str,
15 pub purpose: &'static str,
16 pub domain: &'static str,
17 pub license: &'static str,
18 pub runnable: bool,
20}
21
22pub fn model_slots() -> Vec<ModelSlot> {
24 vec![
25 ModelSlot {
26 pattern: "denoise/rnnoise",
27 default_name: "denoise/rnnoise",
28 purpose: "RNNoise speech denoiser (weights embedded in the binary; no download)",
29 domain: "speech",
30 license: "BSD-3-Clause",
31 runnable: cfg!(feature = "rnnoise"),
32 },
33 ModelSlot {
34 pattern: "denoise/dfn3",
35 default_name: "denoise/dfn3",
36 purpose: "DeepFilterNet 3 speech enhancement at 48 kHz",
37 domain: "speech",
38 license: "MIT OR Apache-2.0",
39 runnable: cfg!(feature = "dfn3"),
40 },
41 ModelSlot {
42 pattern: "denoise/music-*",
43 default_name: "denoise/music-roformer",
44 purpose: "Music-aware denoise checkpoints (community RoFormer); shipped only with an acceptable license",
45 domain: "music",
46 license: "per checkpoint",
47 runnable: false,
48 },
49 ModelSlot {
50 pattern: "denoise/dereverb-*",
51 default_name: "denoise/dereverb-roformer",
52 purpose: "De-reverb checkpoints (best-effort)",
53 domain: "speech or music, per checkpoint",
54 license: "per checkpoint",
55 runnable: false,
56 },
57 ]
58}
59
60pub fn find_cached(prefix: &str) -> Option<std::path::PathBuf> {
63 let dir = ferraus_core::paths::models_dir();
64 let (family, stem) = prefix.split_once('/')?;
65 let fam = dir.join(family);
66 let mut entries: Vec<_> = std::fs::read_dir(&fam).ok()?.filter_map(|e| e.ok()).collect();
67 entries.sort_by_key(|e| e.file_name());
68 entries.into_iter().find(|e| e.file_name().to_string_lossy().starts_with(stem)).map(|e| e.path())
69}
70
71fn not_cached(name: &str, what: &str) -> Error {
72 Error::new(
73 ErrorKind::ModelNotCached,
74 "model_not_cached",
75 format!("{what} needs the model '{name}', which is not in the model cache"),
76 )
77 .with_hint(format!("ferraus models pull {name}"))
78}
79
80pub fn unavailable_slot(prefix: &str, default_name: &str, what: &str) -> Error {
84 match find_cached(prefix) {
85 None => not_cached(default_name, what),
86 Some(p) => Error::unsupported(format!(
87 "{what}: found {} in the model cache, but this build has no runtime for it yet",
88 p.display()
89 ))
90 .with_hint("ferraus doctor"),
91 }
92}
93
94#[cfg(feature = "dfn3")]
97pub fn dfn3(buf: &AudioBuffer, ctx: &mut OpCtx) -> Result<AudioBuffer> {
98 let model = crate::dfn3::Dfn3::load(ctx)?;
99 let c: &OpCtx = ctx;
100 model.enhance(buf, &mut |f| {
101 c.set_progress(f);
102 c.check_cancel()
103 })
104}
105
106#[cfg(not(feature = "dfn3"))]
108pub fn dfn3(_buf: &AudioBuffer, _ctx: &mut OpCtx) -> Result<AudioBuffer> {
109 Err(dfn3_unavailable())
110}
111
112pub fn dfn3_unavailable() -> Error {
114 match find_cached("denoise/dfn3") {
115 None => not_cached("denoise/dfn3", "--model dfn3 (DeepFilterNet 3)"),
116 Some(p) => Error::unsupported(format!(
117 "--model dfn3: found {} in the model cache, but this build was compiled without the DeepFilterNet runtime; use --model rnnoise or the profile path",
118 p.display()
119 ))
120 .with_hint("ferraus denoise <input> --mode ml --model rnnoise -o clean.wav"),
121 }
122}
123
124pub const RNNOISE_NAME: &str = "denoise/rnnoise";
126pub const RNNOISE_VERSION: &str = "nnnoiseless-0.5.2";
127pub const RNNOISE_SHA256: &str = "e6de5fbfadf7ec91d1b24d6a6ccfd0290cb4d8bf555c5eab3ce41506f67a58b1";
129pub const RNNOISE_RATE: u32 = 48_000;
131
132#[cfg(feature = "rnnoise")]
134pub fn rnnoise(buf: &AudioBuffer) -> Result<AudioBuffer> {
135 use ferraus_core::resample::{resample, Quality};
136 use nnnoiseless::DenoiseState;
137 let frame = DenoiseState::FRAME_SIZE;
138 let x = resample(buf, RNNOISE_RATE, Quality::High);
139 let len = x.frames();
140 let mut data = Vec::with_capacity(x.channels());
141 for ch in &x.data {
142 let mut st = DenoiseState::new();
143 let mut out = Vec::with_capacity(len + 2 * frame);
144 let mut inb = vec![0f32; frame];
145 let mut outb = vec![0f32; frame];
146 let mut pos = 0;
147 while pos < len + frame {
149 for (i, v) in inb.iter_mut().enumerate() {
150 *v = ch.get(pos + i).copied().unwrap_or(0.0) * 32768.0;
151 }
152 st.process_frame(&mut outb, &inb);
153 out.extend(outb.iter().map(|v| v / 32768.0));
154 pos += frame;
155 }
156 out.drain(..frame.min(out.len()));
158 out.truncate(len);
159 data.push(out);
160 }
161 let y = AudioBuffer::from_planar(RNNOISE_RATE, data);
162 let mut y = resample(&y, buf.sample_rate, Quality::High);
163 for c in &mut y.data {
164 c.resize(buf.frames(), 0.0);
165 }
166 Ok(y)
167}
168
169#[cfg(not(feature = "rnnoise"))]
170pub fn rnnoise(_buf: &AudioBuffer) -> Result<AudioBuffer> {
171 Err(Error::unsupported("--model rnnoise: this build was compiled without the 'rnnoise' feature"))
172}
173
174pub fn speech_note(model: &str) -> Value {
176 json!({
177 "model": model,
178 "domain": "speech",
179 "note": "speech-tuned model; on music the profile path (--mode profile) usually sounds better",
180 })
181}