use ferraus_core::{AudioBuffer, Error, ErrorKind, OpCtx, Result};
use serde_json::{json, Value};
#[derive(Clone, Debug)]
pub struct ModelSlot {
pub pattern: &'static str,
pub default_name: &'static str,
pub purpose: &'static str,
pub domain: &'static str,
pub license: &'static str,
pub runnable: bool,
}
pub fn model_slots() -> Vec<ModelSlot> {
vec![
ModelSlot {
pattern: "denoise/rnnoise",
default_name: "denoise/rnnoise",
purpose: "RNNoise speech denoiser (weights embedded in the binary; no download)",
domain: "speech",
license: "BSD-3-Clause",
runnable: cfg!(feature = "rnnoise"),
},
ModelSlot {
pattern: "denoise/dfn3",
default_name: "denoise/dfn3",
purpose: "DeepFilterNet 3 speech enhancement at 48 kHz",
domain: "speech",
license: "MIT OR Apache-2.0",
runnable: cfg!(feature = "dfn3"),
},
ModelSlot {
pattern: "denoise/music-*",
default_name: "denoise/music-roformer",
purpose: "Music-aware denoise checkpoints (community RoFormer); shipped only with an acceptable license",
domain: "music",
license: "per checkpoint",
runnable: false,
},
ModelSlot {
pattern: "denoise/dereverb-*",
default_name: "denoise/dereverb-roformer",
purpose: "De-reverb checkpoints (best-effort)",
domain: "speech or music, per checkpoint",
license: "per checkpoint",
runnable: false,
},
]
}
pub fn find_cached(prefix: &str) -> Option<std::path::PathBuf> {
let dir = ferraus_core::paths::models_dir();
let (family, stem) = prefix.split_once('/')?;
let fam = dir.join(family);
let mut entries: Vec<_> = std::fs::read_dir(&fam).ok()?.filter_map(|e| e.ok()).collect();
entries.sort_by_key(|e| e.file_name());
entries.into_iter().find(|e| e.file_name().to_string_lossy().starts_with(stem)).map(|e| e.path())
}
fn not_cached(name: &str, what: &str) -> Error {
Error::new(
ErrorKind::ModelNotCached,
"model_not_cached",
format!("{what} needs the model '{name}', which is not in the model cache"),
)
.with_hint(format!("ferraus models pull {name}"))
}
pub fn unavailable_slot(prefix: &str, default_name: &str, what: &str) -> Error {
match find_cached(prefix) {
None => not_cached(default_name, what),
Some(p) => Error::unsupported(format!(
"{what}: found {} in the model cache, but this build has no runtime for it yet",
p.display()
))
.with_hint("ferraus doctor"),
}
}
#[cfg(feature = "dfn3")]
pub fn dfn3(buf: &AudioBuffer, ctx: &mut OpCtx) -> Result<AudioBuffer> {
let model = crate::dfn3::Dfn3::load(ctx)?;
let c: &OpCtx = ctx;
model.enhance(buf, &mut |f| {
c.set_progress(f);
c.check_cancel()
})
}
#[cfg(not(feature = "dfn3"))]
pub fn dfn3(_buf: &AudioBuffer, _ctx: &mut OpCtx) -> Result<AudioBuffer> {
Err(dfn3_unavailable())
}
pub fn dfn3_unavailable() -> Error {
match find_cached("denoise/dfn3") {
None => not_cached("denoise/dfn3", "--model dfn3 (DeepFilterNet 3)"),
Some(p) => Error::unsupported(format!(
"--model dfn3: found {} in the model cache, but this build was compiled without the DeepFilterNet runtime; use --model rnnoise or the profile path",
p.display()
))
.with_hint("ferraus denoise <input> --mode ml --model rnnoise -o clean.wav"),
}
}
pub const RNNOISE_NAME: &str = "denoise/rnnoise";
pub const RNNOISE_VERSION: &str = "nnnoiseless-0.5.2";
pub const RNNOISE_SHA256: &str = "e6de5fbfadf7ec91d1b24d6a6ccfd0290cb4d8bf555c5eab3ce41506f67a58b1";
pub const RNNOISE_RATE: u32 = 48_000;
#[cfg(feature = "rnnoise")]
pub fn rnnoise(buf: &AudioBuffer) -> Result<AudioBuffer> {
use ferraus_core::resample::{resample, Quality};
use nnnoiseless::DenoiseState;
let frame = DenoiseState::FRAME_SIZE;
let x = resample(buf, RNNOISE_RATE, Quality::High);
let len = x.frames();
let mut data = Vec::with_capacity(x.channels());
for ch in &x.data {
let mut st = DenoiseState::new();
let mut out = Vec::with_capacity(len + 2 * frame);
let mut inb = vec![0f32; frame];
let mut outb = vec![0f32; frame];
let mut pos = 0;
while pos < len + frame {
for (i, v) in inb.iter_mut().enumerate() {
*v = ch.get(pos + i).copied().unwrap_or(0.0) * 32768.0;
}
st.process_frame(&mut outb, &inb);
out.extend(outb.iter().map(|v| v / 32768.0));
pos += frame;
}
out.drain(..frame.min(out.len()));
out.truncate(len);
data.push(out);
}
let y = AudioBuffer::from_planar(RNNOISE_RATE, data);
let mut y = resample(&y, buf.sample_rate, Quality::High);
for c in &mut y.data {
c.resize(buf.frames(), 0.0);
}
Ok(y)
}
#[cfg(not(feature = "rnnoise"))]
pub fn rnnoise(_buf: &AudioBuffer) -> Result<AudioBuffer> {
Err(Error::unsupported("--model rnnoise: this build was compiled without the 'rnnoise' feature"))
}
pub fn speech_note(model: &str) -> Value {
json!({
"model": model,
"domain": "speech",
"note": "speech-tuned model; on music the profile path (--mode profile) usually sounds better",
})
}