Skip to main content

xil_core/
sfxlib.rs

1//! Shared SFX library naming — the text half of `sfx_common.py`.
2
3use std::path::{Path, PathBuf};
4use std::sync::LazyLock;
5
6use regex::Regex;
7use sha2::{Digest, Sha256};
8
9/// Filesystem max is 255 bytes; leave room for `.mp3` + collision suffix.
10const MAX_SLUG_LEN: usize = 180;
11
12static NON_SLUG: LazyLock<Regex> = LazyLock::new(|| Regex::new(r"[^a-z0-9_]+").unwrap());
13static MULTI_DASH: LazyLock<Regex> = LazyLock::new(|| Regex::new(r"-{2,}").unwrap());
14
15/// Direction text → filesystem-safe slug.
16///
17/// Lowercase; `": "` → `_`; any other run of non `[a-z0-9_]` → `-`; collapse
18/// dashes; strip leading/trailing dashes; over 180 chars, truncate and append
19/// `_` + 8 hex chars of the full slug's SHA-256.
20pub fn slugify_effect_key(text: &str) -> String {
21    if text.is_empty() {
22        return String::new();
23    }
24    let slug = text.to_lowercase().replace(": ", "_");
25    let slug = NON_SLUG.replace_all(&slug, "-");
26    let slug = MULTI_DASH.replace_all(&slug, "-");
27    let slug = slug.trim_matches('-').to_string();
28    if slug.chars().count() > MAX_SLUG_LEN {
29        let h = format!("{:x}", Sha256::digest(slug.as_bytes()));
30        let head: String = slug.chars().take(MAX_SLUG_LEN).collect();
31        return format!("{}_{}", head.trim_end_matches('-'), &h[..8]);
32    }
33    slug
34}
35
36/// Shared library path for an effect key. The default backend keeps the
37/// plain historical filename; any other backend gets a `.<backend>` infix.
38pub fn shared_sfx_path(sfx_dir: &Path, effect_key: &str, backend: &str) -> PathBuf {
39    let slug = slugify_effect_key(effect_key);
40    let suffix = if backend == "elevenlabs" {
41        String::new()
42    } else {
43        format!(".{backend}")
44    };
45    sfx_dir.join(format!("{slug}{suffix}.mp3"))
46}
47
48#[cfg(test)]
49mod tests {
50    use super::*;
51
52    #[test]
53    fn slugify_examples_from_python_docstring() {
54        assert_eq!(slugify_effect_key("BEAT"), "beat");
55        assert_eq!(
56            slugify_effect_key("SFX: DOOR OPENS, BELL CHIMES"),
57            "sfx_door-opens-bell-chimes"
58        );
59        assert_eq!(
60            slugify_effect_key("AMBIENCE: Café — night"),
61            "ambience_caf-night"
62        );
63        assert_eq!(slugify_effect_key(""), "");
64        assert_eq!(slugify_effect_key("--x--"), "x");
65    }
66
67    #[test]
68    fn long_keys_get_hash_suffix() {
69        let long = "a".repeat(200);
70        let s = slugify_effect_key(&long);
71        assert_eq!(s.len(), 180 + 1 + 8);
72        assert!(s.starts_with(&"a".repeat(180)));
73        assert!(s[181..].chars().all(|c| c.is_ascii_hexdigit()));
74    }
75
76    #[test]
77    fn shared_path_backend_infix() {
78        assert_eq!(
79            shared_sfx_path(Path::new("SFX"), "BEAT", "elevenlabs"),
80            Path::new("SFX/beat.mp3")
81        );
82        assert_eq!(
83            shared_sfx_path(Path::new("SFX"), "SFX: X", "audioldm2"),
84            Path::new("SFX/sfx_x.audioldm2.mp3")
85        );
86    }
87}