use super::super::data::NamModelData;
use super::super::model::HeadConfig;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum NamWavenetTopology {
Standard,
Lite,
Feather,
Nano,
}
#[derive(Debug, Clone, PartialEq)]
pub struct FreeWavenetGeometry {
pub channels: Vec<usize>,
pub kernel_size: usize,
pub kernel_sizes: Vec<usize>,
pub head_sizes: Vec<usize>,
pub dilations: Vec<Vec<usize>>,
pub num_arrays: usize,
pub condition_size: usize,
pub post_stack_head: Option<HeadConfig>,
}
#[derive(Debug, Clone, PartialEq)]
pub enum WavenetTopologyResult {
Known(NamWavenetTopology),
Free(FreeWavenetGeometry),
Rejected(String),
}
static STD_DILATIONS: &[usize] = &[1, 2, 4, 8, 16, 32, 64, 128, 256, 512];
static LITE_DILATIONS: &[usize] = &[1, 2, 4, 8, 16, 32, 64];
static LITE_DILATIONS_2: &[usize] = &[128, 256, 512, 1, 2, 4, 8, 16, 32, 64, 128, 256, 512];
pub(crate) fn parse_semver(version: &str) -> Option<(u16, u16, u16)> {
let clean = version.trim().trim_start_matches(['v', 'V']);
let clean = clean.split('-').next()?.split('+').next()?;
let mut parts = clean.split('.');
let major = parts.next()?.trim().parse::<u16>().ok()?;
let minor = parts.next().unwrap_or("0").trim().parse::<u16>().ok()?;
let patch = parts.next().unwrap_or("0").trim().parse::<u16>().ok()?;
Some((major, minor, patch))
}
impl NamModelData {
pub fn is_wavenet_a2(&self) -> bool {
if self.architecture != "WaveNet" {
return false;
}
if super::a2::is_a2_shape(self).is_some() {
return true;
}
if self.config.layers.len() == 1
&& let Some(layer) = self.config.layers.first()
&& layer.activation.as_deref().is_some_and(|a| a != "Tanh")
{
return true;
}
if let Some(ref v) = self.version
&& let Some(ver) = parse_semver(v)
&& ver >= (0, 6, 0)
{
log::warn!(
"WaveNet model declares version {v} (>= 0.6.0), but its shape \
does not match any known A2 topology. Treating as non-A2. \
Channels/dilations may indicate an unsupported A2 variant \
or an A1 model with an unusually high version string."
);
}
false
}
}
pub fn get_wavenet_topology(data: &NamModelData) -> WavenetTopologyResult {
use super::super::validation::{
MAX_DILATION, MAX_DILATIONS_PER_ARRAY, MAX_HEAD_SIZE, MAX_KERNEL_SIZE,
MAX_TOTAL_STATE_FRAMES, MAX_WAVENET_ARRAYS, MAX_WAVENET_FREE_CHANNELS,
};
if data.architecture != "WaveNet" {
return WavenetTopologyResult::Rejected("Not a WaveNet model.".to_string());
}
let layers = &data.config.layers;
if layers.is_empty() {
return WavenetTopologyResult::Rejected("WaveNet model has no layer arrays.".to_string());
}
if layers.len() > MAX_WAVENET_ARRAYS {
return WavenetTopologyResult::Rejected(format!(
"WaveNet model has {} layer arrays, exceeding maximum {} — \
DoS/OOM protection.",
layers.len(),
MAX_WAVENET_ARRAYS
));
}
let condition_size = layers[0].condition_size.unwrap_or(1);
let mut first_channels: Option<usize> = None;
let mut first_kernel_size: Option<usize> = None;
let mut first_head_size: Option<usize> = None;
let mut dilations: Vec<Vec<usize>> = Vec::with_capacity(layers.len());
let mut head_sizes: Vec<usize> = Vec::with_capacity(layers.len());
let mut channels: Vec<usize> = Vec::with_capacity(layers.len());
let mut kernel_sizes: Vec<usize> = Vec::with_capacity(layers.len());
for (i, layer) in layers.iter().enumerate() {
if let Some(ref raw) = layer.layer_raw
&& raw
.as_object()
.is_some_and(|obj| obj.get("slimmable").is_some_and(|v| !v.is_null()))
{
return WavenetTopologyResult::Rejected(
"slimmable single-net weight slicing is not supported; use SlimmableContainer instead"
.to_string(),
);
}
let ch = match layer.channels {
Some(c) if c > 0 => {
if c > MAX_WAVENET_FREE_CHANNELS {
return WavenetTopologyResult::Rejected(format!(
"Layer {} channels ({}) exceeds maximum {} — \
OOM/DoS protection.",
i, c, MAX_WAVENET_FREE_CHANNELS
));
}
c
}
_ => {
return WavenetTopologyResult::Rejected(format!(
"Layer {} is missing or has invalid 'channels'.",
i
));
}
};
let k = layer.kernel_size.filter(|&k| k > 0);
if let Some(k) = k
&& k > MAX_KERNEL_SIZE
{
return WavenetTopologyResult::Rejected(format!(
"Layer {} kernel_size ({}) exceeds maximum {} — \
DoS/OOM protection.",
i, k, MAX_KERNEL_SIZE
));
}
let dils = match layer.dilations.as_deref() {
Some(d) if !d.is_empty() => d.to_vec(),
_ => {
return WavenetTopologyResult::Rejected(format!(
"Layer {} is missing or has invalid 'dilations'.",
i
));
}
};
if dils.len() > MAX_DILATIONS_PER_ARRAY {
return WavenetTopologyResult::Rejected(format!(
"Layer {} has {} dilations, exceeding maximum {} — \
DoS/OOM protection.",
i,
dils.len(),
MAX_DILATIONS_PER_ARRAY
));
}
for (j, &d) in dils.iter().enumerate() {
if d > MAX_DILATION {
return WavenetTopologyResult::Rejected(format!(
"Layer {} dilation[{}] ({}) exceeds maximum {} — \
DoS/OOM protection.",
i, j, d, MAX_DILATION
));
}
}
if i == 0 {
first_channels = Some(ch);
first_kernel_size = k;
first_head_size = layer.head_size;
}
let hd = layer.head_size.unwrap_or(1);
if hd == 0 {
return WavenetTopologyResult::Rejected(format!(
"Layer {} has invalid head_size=0.",
i
));
}
if hd > MAX_HEAD_SIZE {
return WavenetTopologyResult::Rejected(format!(
"Layer {} head_size ({}) exceeds maximum {} — \
DoS/OOM protection.",
i, hd, MAX_HEAD_SIZE
));
}
channels.push(ch);
kernel_sizes.push(k.unwrap_or(0));
head_sizes.push(hd);
dilations.push(dils);
}
let total_state_frames: usize = dilations
.iter()
.zip(kernel_sizes.iter())
.zip(channels.iter())
.try_fold(0usize, |acc, ((dils, &k), &ch)| {
let rf = k.saturating_sub(1);
dils.iter().try_fold(acc, |a, &d| {
a.checked_add(rf.saturating_mul(d).saturating_mul(ch))
})
})
.unwrap_or(usize::MAX);
if total_state_frames > MAX_TOTAL_STATE_FRAMES {
return WavenetTopologyResult::Rejected(format!(
"Aggregate state budget exceeded: {} total state frames vs max {} — \
OOM/DoS protection (F12).",
total_state_frames, MAX_TOTAL_STATE_FRAMES
));
}
let first_channels_val = match first_channels {
Some(c) => c,
None => {
return WavenetTopologyResult::Rejected(
"Layer 0 is missing or has invalid 'channels' — required for \
WaveNet topology detection."
.to_string(),
);
}
};
for (i, layer) in layers.iter().enumerate() {
let Some(ref raw) = layer.layer_raw else {
continue;
};
if let Some(gm) = raw.get("gating_mode") {
if let Some(arr) = gm.as_array() {
if arr
.iter()
.any(|v| !(v.as_str() == Some("none") || v.is_null()))
{
return WavenetTopologyResult::Rejected(format!(
"Layer {i} has non-none gating_mode — A2 feature not supported in WaveNet A1."
));
}
} else if !gm.is_null() {
return WavenetTopologyResult::Rejected(format!(
"Layer {i} has non-array gating_mode — A2 feature not supported in WaveNet A1."
));
}
}
if raw
.get("head1x1")
.and_then(|h| h.get("active"))
.and_then(|a| a.as_bool())
.unwrap_or(false)
{
return WavenetTopologyResult::Rejected(format!(
"Layer {i} has active head1x1 — A2 feature not supported in WaveNet A1."
));
}
if raw
.get("layer1x1")
.and_then(|l| l.get("active"))
.and_then(|a| a.as_bool())
.unwrap_or(false)
{
return WavenetTopologyResult::Rejected(format!(
"Layer {i} has active layer1x1 — A2 feature not supported in WaveNet A1."
));
}
const FILM_KEYS: &[&str] = &[
"conv_pre_film",
"conv_post_film",
"input_mixin_pre_film",
"input_mixin_post_film",
"activation_pre_film",
"activation_post_film",
"layer1x1_post_film",
"head1x1_post_film",
];
for key in FILM_KEYS {
if raw
.get(key)
.and_then(|f| f.get("active"))
.and_then(|a| a.as_bool())
.unwrap_or(false)
{
return WavenetTopologyResult::Rejected(format!(
"Layer {i} has active {key} — A2 feature not supported in WaveNet A1."
));
}
}
if layer.gated.unwrap_or(false) {
return WavenetTopologyResult::Rejected(format!(
"Layer {i} has gated=true — A2 feature not supported in WaveNet A1."
));
}
}
if layers.len() == 2 {
let l0 = &layers[0];
let l1 = &layers[1];
let l0_gated = l0.gated.unwrap_or(false);
let l1_gated = l1.gated.unwrap_or(false);
let l0_head_bias = l0.head_bias.unwrap_or(false);
let l1_head_bias = l1.head_bias.unwrap_or(false);
let catalog_compatible = !l0_gated
&& !l1_gated
&& !l0_head_bias
&& l1_head_bias
&& condition_size <= 1
&& data.config.condition_dsp.is_none();
if catalog_compatible {
let dils_0 = &dilations[0];
let dils_1 = &dilations[1];
let result = match first_channels_val {
16 if dils_0 == STD_DILATIONS && dils_1 == STD_DILATIONS => {
Some(NamWavenetTopology::Standard)
}
12 if dils_0 == LITE_DILATIONS && dils_1 == LITE_DILATIONS_2 => {
Some(NamWavenetTopology::Lite)
}
8 if dils_0 == LITE_DILATIONS && dils_1 == LITE_DILATIONS_2 => {
Some(NamWavenetTopology::Feather)
}
4 if dils_0 == LITE_DILATIONS && dils_1 == LITE_DILATIONS_2 => {
Some(NamWavenetTopology::Nano)
}
_ => None,
};
if let Some(sku) = result {
return WavenetTopologyResult::Known(sku);
}
}
}
let kernel_size = match first_kernel_size {
Some(k) => k,
None => {
return WavenetTopologyResult::Rejected(
"Layer 0 is missing or has invalid 'kernel_size' — required for \
free geometry WaveNet A1."
.to_string(),
);
}
};
if first_head_size.is_none_or(|h| h == 0) {
return WavenetTopologyResult::Rejected(
"Layer 0 is missing or has invalid 'head_size' — required for \
WaveNet A1 geometries (determines the head projection dimension)."
.to_string(),
);
}
WavenetTopologyResult::Free(FreeWavenetGeometry {
channels,
kernel_size,
kernel_sizes,
head_sizes,
condition_size,
num_arrays: layers.len(),
dilations,
post_stack_head: data.config.parse_head(),
})
}