From bb719cf2376f934bda5a0fabbff896c0956f13aa Mon Sep 17 00:00:00 2001 From: hamzaq2000 Date: Sun, 21 Sep 2025 17:43:43 -0700 Subject: [PATCH 1/4] CAM++ CoreML conversion & test scripts --- models/emb/cam++/coreml/.gitignore | 1 + models/emb/cam++/coreml/README.md | 33 + models/emb/cam++/coreml/audio/jfk.mp3 | Bin 0 -> 76447 bytes models/emb/cam++/coreml/camplusplus.py | 210 +++++++ models/emb/cam++/coreml/camplusplus_coreml.py | 221 +++++++ models/emb/cam++/coreml/convert.py | 170 ++++++ models/emb/cam++/coreml/pyproject.toml | 12 + models/emb/cam++/coreml/test.py | 306 ++++++++++ models/emb/cam++/coreml/uv.lock | 563 ++++++++++++++++++ 9 files changed, 1516 insertions(+) create mode 100644 models/emb/cam++/coreml/.gitignore create mode 100644 models/emb/cam++/coreml/README.md create mode 100644 models/emb/cam++/coreml/audio/jfk.mp3 create mode 100644 models/emb/cam++/coreml/camplusplus.py create mode 100644 models/emb/cam++/coreml/camplusplus_coreml.py create mode 100644 models/emb/cam++/coreml/convert.py create mode 100644 models/emb/cam++/coreml/pyproject.toml create mode 100644 models/emb/cam++/coreml/test.py create mode 100644 models/emb/cam++/coreml/uv.lock diff --git a/models/emb/cam++/coreml/.gitignore b/models/emb/cam++/coreml/.gitignore new file mode 100644 index 0000000..6ea8874 --- /dev/null +++ b/models/emb/cam++/coreml/.gitignore @@ -0,0 +1 @@ +models/ \ No newline at end of file diff --git a/models/emb/cam++/coreml/README.md b/models/emb/cam++/coreml/README.md new file mode 100644 index 0000000..451b3dd --- /dev/null +++ b/models/emb/cam++/coreml/README.md @@ -0,0 +1,33 @@ +# CAM++ CoreML +CAM++ is a fast and efficient neural network for speaker verification that uses Context-Aware Masking to extract high-quality speaker embeddings. This repository contains a CoreML conversion of the CAM++ model for efficient inference on Apple devices. + +## Model Details +- **Authors**: Hui Wang, Siqi Zheng, Yafeng Chen, Luyao Cheng, and Qian Chen +- **Organization**: Speech Lab, Alibaba Group +- **Paper**: [CAM++: A Fast and Efficient Network for Speaker Verification Using Context-Aware Masking](https://arxiv.org/abs/2303.00332) +- **Implementation**: [3D-Speaker Toolkit](https://github.com/modelscope/3D-Speaker/tree/main/speakerlab/models/campplus) +- **Weights**: [ModelScope Repo](https://modelscope.cn/models/iic/speech_campplus_sv_zh_en_16k-common_advanced/) + +CAM++ employs D-TDNN (Densely Connected Time Delay Neural Network) as its backbone and introduces a context-aware masking module to focus on the speaker of interest while suppressing unrelated noise. It achieves comparable performance to ECAPA-TDNN with significantly lower computational cost. + +## CoreML Model I/O +#### Input Format: `(16, 150, 80)` tensor +- **16**: Batch size - processes 16 audio subsegments in parallel (each ~1.5 seconds) +- **150**: Number of frames - audio is divided into 25ms frames (400 samples @ 16kHz) with 10ms shift (160 samples @ 16kHz) +- **80**: Mel-filterbank features - log-transformed frequency bins (20-8000 Hz) extracted from each frame + +#### Output Format: `(16, 192)` tensor +- **16**: Batch size +- **192**: Embedding dimension + +## Usage +```bash +# Setup venv and install dependencies +uv sync + +# Convert PyTorch model to CoreML +uv run convert.py + +# Test CoreML model against reference implementation with real audio +uv run test.py audio/jfk.mp3 +``` \ No newline at end of file diff --git a/models/emb/cam++/coreml/audio/jfk.mp3 b/models/emb/cam++/coreml/audio/jfk.mp3 new file mode 100644 index 0000000000000000000000000000000000000000..fbfa1d9897365f05adf4f624c3210cc4fd374a44 GIT binary patch literal 76447 zcmce7_g7P2@a;_{K!8vUND0-@L$6BcT|$+rp%)P;N>P;1yYwm|ND&Z}-b4vix^zXU z7EnY$X&(^CgYSE9t@jtav(`=Oe3fMfxXhz9_H zQqj^gv7$M+FnmJ7q7pb6IRzz^%U9Gjv~=_gOf0V1+B*>4Ja69e@%6tG7<@036cHVl zkerg9nfth~xTL)D#mn0IhNhO*_RijSgCk=TQ!{g)7ni?&|FOQg{daf&zoV10|05Ww z8*9s)TLUHhA4>pMga8E33xJl-?cjqx>iWMo{y)D{c7l5WSTGn51_1B{%Ktl_-o7M! z^#1q?5O%`1*K$rT z{5MG{1bZYsnzDoKons88*W$sWKqyUVT2dly7=@yC$eP&yvn<=QNEa2?D+Wd5hEod- z;Cf;hYVr45;sJRSg;#Opjkkhe>8q3Ylw$|VPHyj*StgsZL%~PjA4!9(R~<`2jaPV( z!DUP!3C#@=*a_v75>lu-ZYD-`~RSY`~iOi(;EM9Kb>gUu7b#R4L z=XGcE=|3YL6~wqw7`BdK_?gmlw~d0=Q)CHikXd0Lk4(3)(*;nEEDmbMko8{A^>6Mz zq4%mCm7!;rKn_Qo{CtC*nSzA7%O^tZaIaY!`ipT`xnil^om~fujkwM3LO0_}QU+x2YDmRulc0`S8>7ub1ji zKfe)tWg^s|roh!Cm9ZlJjI<`$y4Yr@q#MP9`u(H8R#HW{#rUC=iEWIHWT$%8#n&Qj zUJGI~wsucH2MHGs^tQjOD_@&`7o?JNb1nPwP=EFJ>>D`(4cD&xerwSg{p)j`&)CLL z?bytS*lmjB*3QKPx#MBFfTmkP5{svf2j{W6s9Lfgc;h1(L}^mF-x8uE~D*f>0pIT6n zg*sd5RZu4EP9LJjgY47SoeOy@~?+(_8@Peu2c#0+@P*UX}m!E*0TXuZP5rc z4;RhoOn4g~ze}9)-)MO+6_}AKfr^i{ddXs@GSxLPIHXKNM|#pjKljnzVIZ?()O62F z_3-fFClv?rWVPCnFE!lhgpAadzbXDzo3`75;wapSLzYNBIkJQT=_|>ffX1HZNl>=%brM zVl7`Q{k%9Y_vS(g+DwJTYb5Q>gMVLj-d^tftc;0I&Z^nsRvq=<-`!Z4UpkOi)n1N1 z`1^Kz^n1o(S_p36`r#h(L3~mh-B8``Pm|K4lT#hvx;ch_LLRJna>4?P^)%3@$iSV* zs<7Wnm0Kl^W~@CG$EIycJo@492` zqF1l|H^53o(7qf)&wO|(()UM{EO9Ns$~p%3H-_f&7tU(l+z%}p=QjVsM*fZ44~b3g zGvM~mzw@R4_ScCa*|c|l!i?qto{;oHm9%~Ltxp1a(`~^wn?wDD>L>ox?q$q}l-!Jw zdr2qB-9%wNE;G6Fw0HQKg~Q#Zi!CF*-_4EhYkWFkb>HT(6ag61B5bf<+TJcHl@FwU zk>w%VIZ=T$V#;q9SXB`GhlSMRuz!jj318|Mr7%pa^mNpqA#{ocV;M(xQ)PKa*Yu!# z?vQ%5o=D))sh`&i%8p*|HkNiX>VpX$Wp!k>PhbsZc$qA_*}85)$g+vrcVB*PMtg95fQ;n{aG5)m&+1>#(8c&lEcKm-cGs z96o7k&*qtvM+T?^ey%VLu+Qc{D5CnSanxC}krKR~M_Y7Z>}WQ+yNv$vfk?*Zp^dtO z+u5o-Jbqs$gnafj8>asoCaPTfaibzom-YFRoQ3XdbPgq*kl7a|>KrZbnX=9xs=39n z!Y7{_3^jfFU-QRsB>(h&>$9rbAhlnbg+KrR6;~P`EE7x;N))N>M~}FMmt#;N@j?O? zK|r*NMz>MYwDIU8i}8y|(9A*Jf!<=-o^i4F4O&xo3O=bTj%o~iPTTFf^rL&6r5le6 zhTyEx(l{rPaQG$aZgTj?XusTW#{|cpIpL;}>S!zv4g

P~0>~dJsUCB3wX4Mgyp* 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 TeBsR_I!(_|*f2 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

%fOaPbyTQv%HY2-7b)qPG}C#WoggynqK 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 z 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 Y?*G>deNH!g09?VM0wMu`|3{Gj2W%$_b^rhX literal 0 HcmV?d00001 diff --git a/models/emb/cam++/coreml/camplusplus.py b/models/emb/cam++/coreml/camplusplus.py new file mode 100644 index 0000000..3869d15 --- /dev/null +++ b/models/emb/cam++/coreml/camplusplus.py @@ -0,0 +1,210 @@ +# https://github.com/modelscope/3D-Speaker/tree/main/speakerlab/models/campplus + +import torch +import torch.nn as nn +import torch.nn.functional as F +from collections import OrderedDict +import torch.utils.checkpoint as cp + +class CAMPPlus(nn.Module): + def __init__(self, feat_dim=80, embedding_size=512, growth_rate=32, bn_size=4, init_channels=128, memory_efficient=True): + super(CAMPPlus, self).__init__() + self.head = FCM(feat_dim=feat_dim) + channels = self.head.out_channels + + self.xvector = nn.Sequential(OrderedDict([ + ('tdnn', TDNNLayer(channels, init_channels, 5, stride=2, dilation=1, padding=-1)) + ])) + + channels = init_channels + blocks_config = [(12, 3, 1), (24, 3, 2), (16, 3, 2)] + + for i, (num_layers, kernel_size, dilation) in enumerate(blocks_config): + block = CAMDenseTDNNBlock(num_layers=num_layers, in_channels=channels, out_channels=growth_rate, bn_channels=bn_size * growth_rate, kernel_size=kernel_size, dilation=dilation, bias=False, memory_efficient=memory_efficient) + self.xvector.add_module(f'block{i+1}', block) + channels = channels + num_layers * growth_rate + + self.xvector.add_module(f'transit{i+1}', TransitLayer(channels, channels // 2, False)) + channels //= 2 + + out_nonlinear = nn.Sequential() + out_nonlinear.add_module('batchnorm', nn.BatchNorm1d(channels)) + out_nonlinear.add_module('relu', nn.ReLU(inplace=True)) + self.xvector.add_module('out_nonlinear', out_nonlinear) + + self.xvector.add_module('stats', StatsPool()) + self.xvector.add_module('dense', DenseLayer(channels * 2, embedding_size, False)) + + for m in self.modules(): + if isinstance(m, (nn.Conv1d, nn.Linear)): + nn.init.kaiming_normal_(m.weight.data) + if m.bias is not None: nn.init.zeros_(m.bias) + + def forward(self, x): + x = x.permute(0, 2, 1) # (B,T,F) => (B,F,T) + x = self.head(x) + x = self.xvector(x) + return x + +class BasicResBlock(nn.Module): + expansion = 1 + + def __init__(self, in_planes, planes, stride=1): + super(BasicResBlock, self).__init__() + self.conv1 = nn.Conv2d(in_planes, planes, kernel_size=3, stride=(stride, 1), padding=1, bias=False) + self.bn1 = nn.BatchNorm2d(planes) + self.conv2 = nn.Conv2d(planes, planes, kernel_size=3, stride=1, padding=1, bias=False) + self.bn2 = nn.BatchNorm2d(planes) + + self.shortcut = nn.Sequential() + if stride != 1 or in_planes != self.expansion * planes: + self.shortcut = nn.Sequential( + nn.Conv2d(in_planes, self.expansion * planes, kernel_size=1, stride=(stride, 1), bias=False), + nn.BatchNorm2d(self.expansion * planes)) + + def forward(self, x): + out = F.relu(self.bn1(self.conv1(x))) + out = self.bn2(self.conv2(out)) + out += self.shortcut(x) + out = F.relu(out) + return out + +class FCM(nn.Module): + def __init__(self, block=BasicResBlock, num_blocks=[2, 2], m_channels=32, feat_dim=80): + super(FCM, self).__init__() + self.in_planes = m_channels + self.conv1 = nn.Conv2d(1, m_channels, kernel_size=3, stride=1, padding=1, bias=False) + self.bn1 = nn.BatchNorm2d(m_channels) + + self.layer1 = nn.Sequential( + block(self.in_planes, m_channels, stride=2), + block(m_channels, m_channels, stride=1) + ) + self.in_planes = m_channels * block.expansion + + self.layer2 = nn.Sequential( + block(self.in_planes, m_channels, stride=2), + block(m_channels, m_channels, stride=1) + ) + + self.conv2 = nn.Conv2d(m_channels, m_channels, kernel_size=3, stride=(2, 1), padding=1, bias=False) + self.bn2 = nn.BatchNorm2d(m_channels) + self.out_channels = m_channels * (feat_dim // 8) + + def forward(self, x): + x = x.unsqueeze(1) + out = F.relu(self.bn1(self.conv1(x))) + out = self.layer1(out) + out = self.layer2(out) + out = F.relu(self.bn2(self.conv2(out))) + + shape = out.shape + out = out.reshape(shape[0], shape[1]*shape[2], shape[3]) + return out + + +class StatsPool(nn.Module): + def forward(self, x, eps=1e-2): + mean = x.mean(dim=-1) + std = x.std(dim=-1, unbiased=True) if x.size(-1) > 1 else torch.zeros_like(mean) + eps + return torch.cat([mean, std], dim=-1) + + +class TDNNLayer(nn.Module): + def __init__(self, in_channels, out_channels, kernel_size, stride=1, padding=0, dilation=1, bias=False): + super(TDNNLayer, self).__init__() + if padding < 0: + assert kernel_size % 2 == 1, 'Expect equal paddings, but got even kernel size ({})'.format(kernel_size) + padding = (kernel_size - 1) // 2 * dilation + + self.linear = nn.Conv1d(in_channels, out_channels, kernel_size, stride=stride, padding=padding, dilation=dilation, bias=bias) + + self.nonlinear = nn.Sequential() + self.nonlinear.add_module('batchnorm', nn.BatchNorm1d(out_channels)) + self.nonlinear.add_module('relu', nn.ReLU(inplace=True)) + + def forward(self, x): + return self.nonlinear(self.linear(x)) + + +class CAMLayer(nn.Module): + def __init__(self, bn_channels, out_channels, kernel_size, stride, padding, dilation, bias, reduction=2): + super(CAMLayer, self).__init__() + self.linear_local = nn.Conv1d(bn_channels, out_channels, kernel_size, stride=stride, padding=padding, dilation=dilation, bias=bias) + self.linear1 = nn.Conv1d(bn_channels, bn_channels // reduction, 1) + self.relu = nn.ReLU(inplace=True) + self.linear2 = nn.Conv1d(bn_channels // reduction, out_channels, 1) + self.sigmoid = nn.Sigmoid() + + def forward(self, x): + y = self.linear_local(x) + context = x.mean(-1, keepdim=True) + self.seg_pooling(x) + return y * self.sigmoid(self.linear2(self.relu(self.linear1(context)))) + + def seg_pooling(self, x, seg_len=100, stype='avg'): + seg = F.avg_pool1d(x, kernel_size=seg_len, stride=seg_len, ceil_mode=True) if stype == 'avg' else F.max_pool1d(x, kernel_size=seg_len, stride=seg_len, ceil_mode=True) + return seg.unsqueeze(-1).expand(*seg.shape, seg_len).reshape(*seg.shape[:-1], -1)[..., :x.shape[-1]] + + +class CAMDenseTDNNLayer(nn.Module): + def __init__(self, in_channels, out_channels, bn_channels, kernel_size, stride=1, dilation=1, bias=False, memory_efficient=False): + super(CAMDenseTDNNLayer, self).__init__() + assert kernel_size % 2 == 1, 'Expect equal paddings, but got even kernel size ({})'.format(kernel_size) + self.memory_efficient = memory_efficient + + self.nonlinear1 = nn.Sequential() + self.nonlinear1.add_module('batchnorm', nn.BatchNorm1d(in_channels)) + self.nonlinear1.add_module('relu', nn.ReLU(inplace=True)) + + self.linear1 = nn.Conv1d(in_channels, bn_channels, 1, bias=False) + + self.nonlinear2 = nn.Sequential() + self.nonlinear2.add_module('batchnorm', nn.BatchNorm1d(bn_channels)) + self.nonlinear2.add_module('relu', nn.ReLU(inplace=True)) + + padding = (kernel_size - 1) // 2 * dilation + self.cam_layer = CAMLayer(bn_channels, out_channels, kernel_size, stride, padding, dilation, bias) + + def forward(self, x): + if self.training and self.memory_efficient: + x = cp.checkpoint(lambda x: self.linear1(self.nonlinear1(x)), x) + else: + x = self.linear1(self.nonlinear1(x)) + return self.cam_layer(self.nonlinear2(x)) + + +class CAMDenseTDNNBlock(nn.ModuleList): + def __init__(self, num_layers, in_channels, out_channels, bn_channels, kernel_size, stride=1, dilation=1, bias=False, memory_efficient=False): + super(CAMDenseTDNNBlock, self).__init__() + for i in range(num_layers): + layer = CAMDenseTDNNLayer(in_channels + i * out_channels, out_channels, bn_channels, kernel_size, stride, dilation, bias, memory_efficient) + self.add_module(f'tdnnd{i+1}', layer) + + def forward(self, x): + for layer in self: + x = torch.cat([x, layer(x)], dim=1) + return x + + +class TransitLayer(nn.Module): + def __init__(self, in_channels, out_channels, bias=True): + super(TransitLayer, self).__init__() + self.nonlinear = nn.Sequential() + self.nonlinear.add_module('batchnorm', nn.BatchNorm1d(in_channels)) + self.nonlinear.add_module('relu', nn.ReLU(inplace=True)) + self.linear = nn.Conv1d(in_channels, out_channels, 1, bias=bias) + + def forward(self, x): + return self.linear(self.nonlinear(x)) + + +class DenseLayer(nn.Module): + def __init__(self, in_channels, out_channels, bias=False): + super(DenseLayer, self).__init__() + self.linear = nn.Conv1d(in_channels, out_channels, 1, bias=bias) + self.nonlinear = nn.Sequential() + self.nonlinear.add_module('batchnorm', nn.BatchNorm1d(out_channels, affine=False)) + self.nonlinear.add_module('relu', nn.ReLU(inplace=True)) + + def forward(self, x): + return self.nonlinear(self.linear(x if len(x.shape) == 3 else x.unsqueeze(-1)).squeeze(-1) if len(x.shape) == 2 else self.linear(x)) \ No newline at end of file diff --git a/models/emb/cam++/coreml/camplusplus_coreml.py b/models/emb/cam++/coreml/camplusplus_coreml.py new file mode 100644 index 0000000..873c8f2 --- /dev/null +++ b/models/emb/cam++/coreml/camplusplus_coreml.py @@ -0,0 +1,221 @@ +# https://github.com/modelscope/3D-Speaker/tree/main/speakerlab/models/campplus +# CoreML friendly version + +import torch +import torch.nn as nn +import torch.nn.functional as F +from collections import OrderedDict + +class CAMPPlusCoreML(nn.Module): + def __init__(self, feat_dim=80, embedding_size=512, growth_rate=32, bn_size=4, init_channels=128, memory_efficient=False): + super(CAMPPlusCoreML, self).__init__() + self.head = FCM(feat_dim=feat_dim) + channels = self.head.out_channels + + self.xvector = nn.Sequential(OrderedDict([ + ('tdnn', TDNNLayer(channels, init_channels, 5, stride=2, dilation=1, padding=-1)) + ])) + + channels = init_channels + blocks_config = [(12, 3, 1), (24, 3, 2), (16, 3, 2)] + + for i, (num_layers, kernel_size, dilation) in enumerate(blocks_config): + block = CAMDenseTDNNBlock(num_layers=num_layers, in_channels=channels, out_channels=growth_rate, + bn_channels=bn_size * growth_rate, kernel_size=kernel_size, + dilation=dilation, bias=False, memory_efficient=False) + self.xvector.add_module(f'block{i+1}', block) + channels = channels + num_layers * growth_rate + + self.xvector.add_module(f'transit{i+1}', TransitLayer(channels, channels // 2, False)) + channels //= 2 + + out_nonlinear = nn.Sequential() + out_nonlinear.add_module('batchnorm', nn.BatchNorm1d(channels)) + out_nonlinear.add_module('relu', nn.ReLU(inplace=True)) + self.xvector.add_module('out_nonlinear', out_nonlinear) + + self.xvector.add_module('stats', StatsPool()) + self.xvector.add_module('dense', DenseLayer(channels * 2, embedding_size, False)) + + for m in self.modules(): + if isinstance(m, (nn.Conv1d, nn.Linear)): + nn.init.kaiming_normal_(m.weight.data) + if m.bias is not None: + nn.init.zeros_(m.bias) + + def forward(self, x): + x = x.permute(0, 2, 1) # (B,T,F) => (B,F,T) + x = self.head(x) + x = self.xvector(x) + return x + +class BasicResBlock(nn.Module): + expansion = 1 + + def __init__(self, in_planes, planes, stride=1): + super(BasicResBlock, self).__init__() + self.conv1 = nn.Conv2d(in_planes, planes, kernel_size=3, stride=(stride, 1), padding=1, bias=False) + self.bn1 = nn.BatchNorm2d(planes) + self.conv2 = nn.Conv2d(planes, planes, kernel_size=3, stride=1, padding=1, bias=False) + self.bn2 = nn.BatchNorm2d(planes) + + self.shortcut = nn.Sequential() + if stride != 1 or in_planes != self.expansion * planes: + self.shortcut = nn.Sequential( + nn.Conv2d(in_planes, self.expansion * planes, kernel_size=1, stride=(stride, 1), bias=False), + nn.BatchNorm2d(self.expansion * planes)) + + def forward(self, x): + out = F.relu(self.bn1(self.conv1(x))) + out = self.bn2(self.conv2(out)) + out = out + self.shortcut(x) + out = F.relu(out) + return out + +class FCM(nn.Module): + def __init__(self, block=BasicResBlock, num_blocks=[2, 2], m_channels=32, feat_dim=80): + super(FCM, self).__init__() + self.in_planes = m_channels + self.conv1 = nn.Conv2d(1, m_channels, kernel_size=3, stride=1, padding=1, bias=False) + self.bn1 = nn.BatchNorm2d(m_channels) + + self.layer1 = nn.Sequential( + block(self.in_planes, m_channels, stride=2), + block(m_channels, m_channels, stride=1) + ) + self.in_planes = m_channels * block.expansion + + self.layer2 = nn.Sequential( + block(self.in_planes, m_channels, stride=2), + block(m_channels, m_channels, stride=1) + ) + + self.conv2 = nn.Conv2d(m_channels, m_channels, kernel_size=3, stride=(2, 1), padding=1, bias=False) + self.bn2 = nn.BatchNorm2d(m_channels) + self.out_channels = m_channels * (feat_dim // 8) + + def forward(self, x): + x = x.unsqueeze(1) + out = F.relu(self.bn1(self.conv1(x))) + out = self.layer1(out) + out = self.layer2(out) + out = F.relu(self.bn2(self.conv2(out))) + + shape = out.shape + out = out.reshape(shape[0], shape[1]*shape[2], shape[3]) + return out + + +class StatsPool(nn.Module): + def forward(self, x): + # CoreML-friendly implementation (conditional operations (if x.size(-1) > 1) aren't traced well) + mean = x.mean(dim=-1) + # Compute std with epsilon for stability + var = x.var(dim=-1, unbiased=False) + std = torch.sqrt(var + 1e-5) + return torch.cat([mean, std], dim=-1) + + +class TDNNLayer(nn.Module): + def __init__(self, in_channels, out_channels, kernel_size, stride=1, padding=0, dilation=1, bias=False): + super(TDNNLayer, self).__init__() + if padding < 0: + assert kernel_size % 2 == 1, 'Expect equal paddings, but got even kernel size ({})'.format(kernel_size) + padding = (kernel_size - 1) // 2 * dilation + + self.linear = nn.Conv1d(in_channels, out_channels, kernel_size, stride=stride, padding=padding, dilation=dilation, bias=bias) + + self.nonlinear = nn.Sequential() + self.nonlinear.add_module('batchnorm', nn.BatchNorm1d(out_channels)) + self.nonlinear.add_module('relu', nn.ReLU(inplace=True)) + + def forward(self, x): + return self.nonlinear(self.linear(x)) + + +class CAMLayer(nn.Module): + def __init__(self, bn_channels, out_channels, kernel_size, stride, padding, dilation, bias, reduction=2): + super(CAMLayer, self).__init__() + self.linear_local = nn.Conv1d(bn_channels, out_channels, kernel_size, stride=stride, padding=padding, dilation=dilation, bias=bias) + self.linear1 = nn.Conv1d(bn_channels, bn_channels // reduction, 1) + self.relu = nn.ReLU(inplace=True) + self.linear2 = nn.Conv1d(bn_channels // reduction, out_channels, 1) + self.sigmoid = nn.Sigmoid() + + def forward(self, x): + y = self.linear_local(x) + # Simplified context computation for CoreML + # Using 2x the mean to approximate the original context + pooling + context = 2.0 * x.mean(-1, keepdim=True) + + attention = self.sigmoid(self.linear2(self.relu(self.linear1(context)))) + return y * attention + + +class CAMDenseTDNNLayer(nn.Module): + def __init__(self, in_channels, out_channels, bn_channels, kernel_size, stride=1, dilation=1, bias=False, memory_efficient=False): + super(CAMDenseTDNNLayer, self).__init__() + assert kernel_size % 2 == 1, 'Expect equal paddings, but got even kernel size ({})'.format(kernel_size) + # memory_efficient is ignored for CoreML compatibility + + self.nonlinear1 = nn.Sequential() + self.nonlinear1.add_module('batchnorm', nn.BatchNorm1d(in_channels)) + self.nonlinear1.add_module('relu', nn.ReLU(inplace=True)) + + self.linear1 = nn.Conv1d(in_channels, bn_channels, 1, bias=False) + + self.nonlinear2 = nn.Sequential() + self.nonlinear2.add_module('batchnorm', nn.BatchNorm1d(bn_channels)) + self.nonlinear2.add_module('relu', nn.ReLU(inplace=True)) + + padding = (kernel_size - 1) // 2 * dilation + self.cam_layer = CAMLayer(bn_channels, out_channels, kernel_size, stride, padding, dilation, bias) + + def forward(self, x): + # No checkpointing for CoreML + x = self.linear1(self.nonlinear1(x)) + return self.cam_layer(self.nonlinear2(x)) + + +class CAMDenseTDNNBlock(nn.ModuleList): + def __init__(self, num_layers, in_channels, out_channels, bn_channels, kernel_size, stride=1, dilation=1, bias=False, memory_efficient=False): + super(CAMDenseTDNNBlock, self).__init__() + for i in range(num_layers): + layer = CAMDenseTDNNLayer(in_channels + i * out_channels, out_channels, bn_channels, kernel_size, stride, dilation, bias, memory_efficient=False) + self.add_module(f'tdnnd{i+1}', layer) + + def forward(self, x): + for layer in self: + x = torch.cat([x, layer(x)], dim=1) + return x + + +class TransitLayer(nn.Module): + def __init__(self, in_channels, out_channels, bias=True): + super(TransitLayer, self).__init__() + self.nonlinear = nn.Sequential() + self.nonlinear.add_module('batchnorm', nn.BatchNorm1d(in_channels)) + self.nonlinear.add_module('relu', nn.ReLU(inplace=True)) + self.linear = nn.Conv1d(in_channels, out_channels, 1, bias=bias) + + def forward(self, x): + return self.linear(self.nonlinear(x)) + + +class DenseLayer(nn.Module): + def __init__(self, in_channels, out_channels, bias=False): + super(DenseLayer, self).__init__() + self.linear = nn.Conv1d(in_channels, out_channels, 1, bias=bias) + self.nonlinear = nn.Sequential() + self.nonlinear.add_module('batchnorm', nn.BatchNorm1d(out_channels, affine=False)) + self.nonlinear.add_module('relu', nn.ReLU(inplace=True)) + + def forward(self, x): + # Simplified forward for CoreML (explicit if/else blocks that can be traced) + if len(x.shape) == 2: + x = x.unsqueeze(-1) + x = self.linear(x) + x = x.squeeze(-1) + else: + x = self.linear(x) + return self.nonlinear(x) \ No newline at end of file diff --git a/models/emb/cam++/coreml/convert.py b/models/emb/cam++/coreml/convert.py new file mode 100644 index 0000000..893d2b7 --- /dev/null +++ b/models/emb/cam++/coreml/convert.py @@ -0,0 +1,170 @@ +""" +PyTorch camplusplus_coreml.py to CoreML conversion script +Downloads model weights from ModelScope if not present. + +CoreML Model I/O: + Input: (16, 150, 80) tensor + - 16: Batch size - processes 16 audio subsegments in parallel + - 150: Number of frames - audio is divided into 25ms frames (400 samples @ 16kHz) with 10ms shift (160 samples @ 16kHz) + - 80: Mel-filterbank features - log-transformed frequency bins (20-8000 Hz) extracted from each frame + Output: (16, 192) tensor + - 16: Batch size + - 192: Embedding dimension +""" +import torch +import coremltools as ct +import numpy as np +import requests +from pathlib import Path +from camplusplus import CAMPPlus +from camplusplus_coreml import CAMPPlusCoreML + +def download_model(): + """Download the CAMPlus model if it doesn't exist and return the local path.""" + model_dir = Path("./models/speech_campplus_sv_zh_en_16k-common_advanced") + model_path = model_dir / "campplus_cn_en_common.pt" + + if model_path.exists(): + print(f"Model already exists at: {model_path}") + return str(model_path) + + print(f"Model not found. Downloading to: {model_path}") + model_dir.mkdir(parents=True, exist_ok=True) + + url = "https://modelscope.cn/models/iic/speech_campplus_sv_zh_en_16k-common_advanced/resolve/master/campplus_cn_en_common.pt" + + try: + with requests.get(url, stream=True) as response: + response.raise_for_status() + total_size = int(response.headers.get("content-length", 0)) + downloaded = 0 + with open(model_path, "wb") as f: + for chunk in response.iter_content(chunk_size=8192): + if not chunk: + continue + f.write(chunk) + downloaded += len(chunk) + if total_size: + pct = downloaded / total_size * 100 + print(f"\rDownloading: {pct:.1f}%", end="", flush=True) + except requests.RequestException as e: + raise RuntimeError(f"Error downloading model: {e}") from e + + print(f"\nModel downloaded successfully to: {model_path}") + return str(model_path) + +def convert_campplus_to_coreml(): + BATCH_SIZE = 16 + FIXED_FRAMES = 150 + FEATURE_DIM = 80 + EMBEDDING_DIM = 192 + + # Initialize PyTorch CoreML-friendly model + coreml_friendly_model = CAMPPlusCoreML(feat_dim=FEATURE_DIM, embedding_size=EMBEDDING_DIM) + + # Load weights into CoreML-friendly model + weights_path = download_model() + state_dict = torch.load(weights_path, map_location='cpu', weights_only=True) + coreml_friendly_model.load_state_dict(state_dict) + coreml_friendly_model.eval() + print(f"✓ Loaded weights into CoreML-friendly model (CAMPPlusCoreML)") + + # Create example input + example_input = torch.randn(BATCH_SIZE, FIXED_FRAMES, FEATURE_DIM) + + # Trace the CoreML-friendly model + print("\nTracing CoreML-friendly model...") + traced_model = torch.jit.trace(coreml_friendly_model, example_input) + + # Convert to CoreML + print("\nConverting to CoreML...") + print(f" Batch size: {BATCH_SIZE}") + print(f" Fixed frames: {FIXED_FRAMES}") + print(f" Compute unit: CPU_AND_NE (Neural Engine)") + + input_type = ct.TensorType( + name="input_features", + shape=(BATCH_SIZE, FIXED_FRAMES, FEATURE_DIM), + dtype=np.float32 + ) + + coreml_model = ct.convert( + traced_model, + inputs=[input_type], + outputs=[ct.TensorType(name="embeddings", dtype=np.float32)], + compute_units=ct.ComputeUnit.CPU_AND_NE, + convert_to="mlprogram", + minimum_deployment_target=ct.target.macOS14, + ) + + # Add metadata + coreml_model.author = "Original: 3D-Speaker / Speech Lab, Alibaba Group" + coreml_model.short_description = "CAM++ speaker embedding model" + coreml_model.input_description["input_features"] = f"Fbank features: ({BATCH_SIZE}, {FIXED_FRAMES}, {FEATURE_DIM})" + coreml_model.output_description["embeddings"] = f"Speaker embeddings: ({BATCH_SIZE}, {EMBEDDING_DIM})" + + # Save the model + output_path = "./models/camplusplus_batch16.mlpackage" + coreml_model.save(output_path) + print(f"\n✓ Saved CoreML model to: {output_path}") + + # Verify conversion accuracy against reference model + print("\nVerifying conversion accuracy against reference model...") + coreml_model_loaded = ct.models.MLModel(output_path) + + # Test with example input + test_input = example_input.numpy() + coreml_output = coreml_model_loaded.predict({'input_features': test_input}) + coreml_embeddings = coreml_output['embeddings'] + + # Initialize reference model (original CAMPPlus) + reference_model = CAMPPlus(feat_dim=FEATURE_DIM, embedding_size=EMBEDDING_DIM) + + # Load pretrained weights into reference model + reference_model.load_state_dict(state_dict) + reference_model.eval() + print(f"✓ Loaded weights into reference model from: {weights_path}") + + # Test reference model + print("\nTesting reference model...") + with torch.no_grad(): + reference_output = reference_model(example_input) + print(f" Input shape: {example_input.shape}") + print(f" Reference output shape: {reference_output.shape}") + + # Compare CoreML output with reference model output + reference_np = reference_output.numpy() + max_diff = np.max(np.abs(reference_np - coreml_embeddings)) + mean_diff = np.mean(np.abs(reference_np - coreml_embeddings)) + + # Calculate cosine similarity between CoreML and reference + cosine_sims = [] + for i in range(BATCH_SIZE): + ref_norm = reference_np[i] / (np.linalg.norm(reference_np[i]) + 1e-8) + cm_norm = coreml_embeddings[i] / (np.linalg.norm(coreml_embeddings[i]) + 1e-8) + cosine_sim = np.sum(ref_norm * cm_norm) + cosine_sims.append(cosine_sim) + + avg_cosine = np.mean(cosine_sims) + min_cosine = np.min(cosine_sims) + + print(f" Max difference: {max_diff:.6f}") + print(f" Mean difference: {mean_diff:.6f}") + print(f" Avg cosine similarity: {avg_cosine:.6f}") + print(f" Min cosine similarity: {min_cosine:.6f}") + + if avg_cosine > 0.999: + print("\n ✓ Accuracy verification PASSED") + elif avg_cosine > 0.99: + print("\n ⚠ Warning: Slightly lower accuracy than expected") + else: + print("\n ⚠ Warning: Significant accuracy difference detected") + + print("\nCAM++ CoreML conversion complete") + print(f"Model: {output_path}") + print(f"Batch size: {BATCH_SIZE}") + + return output_path + +if __name__ == "__main__": + convert_campplus_to_coreml() \ No newline at end of file diff --git a/models/emb/cam++/coreml/pyproject.toml b/models/emb/cam++/coreml/pyproject.toml new file mode 100644 index 0000000..1ca938c --- /dev/null +++ b/models/emb/cam++/coreml/pyproject.toml @@ -0,0 +1,12 @@ +[project] +name = "camplusplus_coreml" +version = "0.1.0" +requires-python = "==3.11.13" +dependencies = [ + "torch", + "torchaudio", + "soundfile", + "coremltools", + "numpy", + "requests", +] diff --git a/models/emb/cam++/coreml/test.py b/models/emb/cam++/coreml/test.py new file mode 100644 index 0000000..4dbd526 --- /dev/null +++ b/models/emb/cam++/coreml/test.py @@ -0,0 +1,306 @@ +#!/usr/bin/env python3 +""" +Test script to validate CoreML CAM++ embeddings model against reference PyTorch implementation. + 1. Takes an audio file as input + 2. Converts it to 16kHz mono 16-bit WAV if needed + 3. Extracts Fbank features + 4. Runs features through both CoreML and reference PyTorch models + 5. Compares embeddings using cosine similarity + 6. Performs speed comparison between CoreML model & torch model +""" +import sys +import numpy as np +import torch +import torchaudio +import torchaudio.compliance.kaldi as kaldi +import coremltools as ct +from pathlib import Path +import warnings +from camplusplus import CAMPPlus + +warnings.filterwarnings('ignore') + +def extract_fbank_features(waveform, sample_rate=16000): + # Ensure waveform is 2D (1, num_samples) + if waveform.dim() == 1: + waveform = waveform.unsqueeze(0) + + fbank = kaldi.fbank( + waveform, + sample_frequency=sample_rate, + num_mel_bins=80, + frame_length=25.0, # 25ms + frame_shift=10.0, # 10ms + dither=0.0, + preemphasis_coefficient=0.97, + remove_dc_offset=True, + window_type='povey', + round_to_power_of_two=True, + blackman_coeff=0.42, + snip_edges=True, + low_freq=20, + high_freq=0, # 0 means Nyquist (8000 Hz for 16kHz sampling) + use_energy=False, + energy_floor=1.0, + raw_energy=True, + use_log_fbank=True, + use_power=True + ) + + # Mean normalization + fbank = fbank - fbank.mean(dim=0, keepdim=True) + + return fbank.numpy() + +def load_and_convert_audio(audio_path): + """Load audio file and convert to 16kHz mono if needed""" + # Load audio with torchaudio + waveform, sample_rate = torchaudio.load(audio_path) + + # Convert to mono if stereo + if waveform.shape[0] > 1: + waveform = torch.mean(waveform, dim=0, keepdim=True) + + # Resample to 16kHz if needed + if sample_rate != 16000: + resampler = torchaudio.transforms.Resample(sample_rate, 16000) + waveform = resampler(waveform) + sample_rate = 16000 + + return waveform, sample_rate + +def generate_subsegments(audio_duration): + segment_duration = 1.5 + shift = segment_duration / 2.5 # 0.6 seconds + + subsegments = [] + start = 0.0 + + while start + segment_duration <= audio_duration: + subsegments.append((start, start + segment_duration)) + start += shift + + # Add final segment if needed + if start < audio_duration: + end = min(audio_duration, start + segment_duration) + start = max(0, end - segment_duration) + subsegments.append((start, end)) + + return subsegments + +def main(audio_path): + # Load and convert audio + print(f"\n1. Loading audio from: {audio_path}") + waveform, sample_rate = load_and_convert_audio(audio_path) + duration = waveform.shape[1] / sample_rate + print(f" Duration: {duration:.2f} seconds") + print(f" Sample rate: {sample_rate} Hz") + + # Generate subsegments + print("\n2. Generating subsegments...") + subsegments = generate_subsegments(duration) + print(f" Generated {len(subsegments)} subsegments") + + # Limit to 16 subsegments for batch processing + if len(subsegments) > 16: + subsegments = subsegments[:16] + print(f" Using first 16 subsegments for testing") + + # Extract features for each subsegment + print("\n3. Extracting Fbank features...") + features = [] + + for start_sec, end_sec in subsegments: + # Extract subsegment + start_sample = int(start_sec * sample_rate) + end_sample = int(end_sec * sample_rate) + + if start_sample >= waveform.shape[1]: + # Empty subsegment + features.append(np.zeros((150, 80), dtype=np.float32)) + continue + + segment_waveform = waveform[:, start_sample:end_sample] + + # Ensure minimum length (400 samples = 25ms at 16kHz) + if segment_waveform.shape[1] < 400: + padded = torch.zeros(1, 400) + padded[:, :segment_waveform.shape[1]] = segment_waveform + segment_waveform = padded + + # Extract features + segment_features = extract_fbank_features(segment_waveform, sample_rate) + + # Pad or crop to 150 frames + if segment_features.shape[0] < 150: + padded = np.zeros((150, 80), dtype=np.float32) + padded[:segment_features.shape[0]] = segment_features + segment_features = padded + elif segment_features.shape[0] > 150: + # Center crop + start = (segment_features.shape[0] - 150) // 2 + segment_features = segment_features[start:start + 150] + + features.append(segment_features) + + # Pad to batch size 16 if needed + while len(features) < 16: + features.append(np.zeros((150, 80), dtype=np.float32)) + + features_batch = np.array(features[:16], dtype=np.float32) # Shape: (16, 150, 80) + print(f" Feature shape: {features_batch.shape}") + + # Load CoreML model + print("\n4. Loading CoreML model...") + coreml_model_path = Path("models/camplusplus_batch16.mlpackage") + coreml_model = ct.models.MLModel(str(coreml_model_path)) + print(f" Loaded: {coreml_model_path}") + + # Run CoreML inference + print("\n5. Running CoreML inference...") + coreml_output = coreml_model.predict({'input_features': features_batch}) + coreml_embeddings = coreml_output['embeddings'] + print(f" CoreML embeddings shape: {coreml_embeddings.shape}") + + # Load reference PyTorch model + print("\n6. Loading reference PyTorch model...") + device = torch.device("mps" if torch.backends.mps.is_available() else "cpu") + print(f" Using device: {device}") + + reference_model = CAMPPlus(feat_dim=80, embedding_size=192) + + # Load weights + weights_path = "models/speech_campplus_sv_zh_en_16k-common_advanced/campplus_cn_en_common.pt" + state_dict = torch.load(weights_path, map_location='cpu', weights_only=True) + reference_model.load_state_dict(state_dict) + print(f" Loaded weights from: {weights_path}") + + reference_model.eval() + reference_model.to(device) + + # Run reference model inference + print("\n7. Running reference PyTorch inference...") + features_torch = torch.from_numpy(features_batch).to(device) + + with torch.no_grad(): + reference_embeddings = reference_model(features_torch).cpu().numpy() + + print(f" Reference embeddings shape: {reference_embeddings.shape}") + + # Compare embeddings + print("\n8. Comparing embeddings...") + print("-" * 40) + + cosine_similarities = [] + l2_distances = [] + + # Only compare actual subsegments (not padding) + num_actual = min(len(subsegments), 16) + + for i in range(num_actual): + # Normalize embeddings + coreml_norm = coreml_embeddings[i] / (np.linalg.norm(coreml_embeddings[i]) + 1e-8) + ref_norm = reference_embeddings[i] / (np.linalg.norm(reference_embeddings[i]) + 1e-8) + + # Cosine similarity + cosine_sim = np.dot(coreml_norm, ref_norm) + cosine_similarities.append(cosine_sim) + + # L2 distance + l2_dist = np.linalg.norm(coreml_embeddings[i] - reference_embeddings[i]) + l2_distances.append(l2_dist) + + print(f" Subsegment {i+1:2d}: Cosine Sim = {cosine_sim:.6f}, L2 Dist = {l2_dist:.6f}") + + print("-" * 40) + + # Summary statistics + avg_cosine = np.mean(cosine_similarities) + min_cosine = np.min(cosine_similarities) + max_cosine = np.max(cosine_similarities) + std_cosine = np.std(cosine_similarities) + + avg_l2 = np.mean(l2_distances) + max_l2 = np.max(l2_distances) + + print("\n9. Summary Statistics:") + print(f" Cosine Similarity:") + print(f" Average: {avg_cosine:.6f}") + print(f" Min: {min_cosine:.6f}") + print(f" Max: {max_cosine:.6f}") + print(f" Std Dev: {std_cosine:.6f}") + print(f" L2 Distance:") + print(f" Average: {avg_l2:.6f}") + print(f" Max: {max_l2:.6f}") + + # Validation result + print("\n10. Validation Result:") + if avg_cosine > 0.999: + print(" ✅ Near-perfect match between CoreML and PyTorch models") + elif avg_cosine > 0.99: + print(" ✅ Very high similarity between models") + elif avg_cosine > 0.95: + print(" ⚠️ Good similarity but some differences detected") + else: + print(" ❌ FAIL: Significant differences between models") + + # Speed comparison + print("\n11. Speed Comparison:") + print("-" * 40) + + import time + + # Warm up both models + print(" Warming up models...") + for _ in range(5): + _ = coreml_model.predict({'input_features': features_batch}) + with torch.no_grad(): + _ = reference_model(features_torch) + + # Benchmark CoreML + print(" Benchmarking CoreML...") + num_runs = 20 + coreml_times = [] + for _ in range(num_runs): + start = time.perf_counter() + _ = coreml_model.predict({'input_features': features_batch}) + coreml_times.append(time.perf_counter() - start) + + # Benchmark PyTorch on MPS + print(f" Benchmarking PyTorch ({device})...") + torch_times = [] + for _ in range(num_runs): + start = time.perf_counter() + with torch.no_grad(): + _ = reference_model(features_torch) + if device.type == 'mps': + torch.mps.synchronize() # Ensure MPS operations complete + torch_times.append(time.perf_counter() - start) + + # Calculate statistics + coreml_avg = np.mean(coreml_times) * 1000 # Convert to ms + coreml_std = np.std(coreml_times) * 1000 + torch_avg = np.mean(torch_times) * 1000 + torch_std = np.std(torch_times) * 1000 + + speedup = torch_avg / coreml_avg + + print(f"\n CoreML (Neural Engine):") + print(f" Average: {coreml_avg:.2f} ms ± {coreml_std:.2f} ms") + print(f" PyTorch ({device}):") + print(f" Average: {torch_avg:.2f} ms ± {torch_std:.2f} ms") + print(f"\n Speedup: {speedup:.2f}x" + (" faster" if speedup > 1 else " slower") + " with CoreML") + + +if __name__ == "__main__": + if len(sys.argv) != 2: + print("Usage: python test.py ") + print("Example: python test.py sample.wav") + sys.exit(1) + + audio_file = sys.argv[1] + if not Path(audio_file).exists(): + print(f"Error: Audio file '{audio_file}' not found") + sys.exit(1) + + main(audio_file) \ No newline at end of file diff --git a/models/emb/cam++/coreml/uv.lock b/models/emb/cam++/coreml/uv.lock new file mode 100644 index 0000000..82a75f0 --- /dev/null +++ b/models/emb/cam++/coreml/uv.lock @@ -0,0 +1,563 @@ +version = 1 +revision = 3 +requires-python = "==3.11.13" + +[[package]] +name = "attrs" +version = "25.3.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/5a/b0/1367933a8532ee6ff8d63537de4f1177af4bff9f3e829baf7331f595bb24/attrs-25.3.0.tar.gz", hash = "sha256:75d7cefc7fb576747b2c81b4442d4d4a1ce0900973527c011d1030fd3bf4af1b", size = 812032, upload-time = "2025-03-13T11:10:22.779Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/77/06/bb80f5f86020c4551da315d78b3ab75e8228f89f0162f2c3a819e407941a/attrs-25.3.0-py3-none-any.whl", hash = "sha256:427318ce031701fea540783410126f03899a97ffc6f61596ad581ac2e40e3bc3", size = 63815, upload-time = "2025-03-13T11:10:21.14Z" }, +] + +[[package]] +name = "camplusplus-coreml" +version = "0.1.0" +source = { virtual = "." } +dependencies = [ + { name = "coremltools" }, + { name = "numpy" }, + { name = "requests" }, + { name = "soundfile" }, + { name = "torch" }, + { name = "torchaudio" }, +] + +[package.metadata] +requires-dist = [ + { name = "coremltools" }, + { name = "numpy" }, + { name = "requests" }, + { name = "soundfile" }, + { name = "torch" }, + { name = "torchaudio" }, +] + +[[package]] +name = "cattrs" +version = "25.2.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "attrs" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/e3/42/988b3a667967e9d2d32346e7ed7edee540ef1cee829b53ef80aa8d4a0222/cattrs-25.2.0.tar.gz", hash = "sha256:f46c918e955db0177be6aa559068390f71988e877c603ae2e56c71827165cc06", size = 506531, upload-time = "2025-08-31T20:41:59.301Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/20/a5/b3771ac30b590026b9d721187110194ade05bfbea3d98b423a9cafd80959/cattrs-25.2.0-py3-none-any.whl", hash = "sha256:539d7eedee7d2f0706e4e109182ad096d608ba84633c32c75ef3458f1d11e8f1", size = 70040, upload-time = "2025-08-31T20:41:57.543Z" }, +] + +[[package]] +name = "certifi" +version = "2025.8.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/dc/67/960ebe6bf230a96cda2e0abcf73af550ec4f090005363542f0765df162e0/certifi-2025.8.3.tar.gz", hash = "sha256:e564105f78ded564e3ae7c923924435e1daa7463faeab5bb932bc53ffae63407", size = 162386, upload-time = "2025-08-03T03:07:47.08Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e5/48/1549795ba7742c948d2ad169c1c8cdbae65bc450d6cd753d124b17c8cd32/certifi-2025.8.3-py3-none-any.whl", hash = "sha256:f6c12493cfb1b06ba2ff328595af9350c65d6644968e5d3a2ffd78699af217a5", size = 161216, upload-time = "2025-08-03T03:07:45.777Z" }, +] + +[[package]] +name = "cffi" +version = "2.0.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pycparser", marker = "implementation_name != 'PyPy'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/eb/56/b1ba7935a17738ae8453301356628e8147c79dbb825bcbc73dc7401f9846/cffi-2.0.0.tar.gz", hash = "sha256:44d1b5909021139fe36001ae048dbdde8214afa20200eda0f64c068cac5d5529", size = 523588, upload-time = "2025-09-08T23:24:04.541Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/12/4a/3dfd5f7850cbf0d06dc84ba9aa00db766b52ca38d8b86e3a38314d52498c/cffi-2.0.0-cp311-cp311-macosx_10_13_x86_64.whl", hash = "sha256:b4c854ef3adc177950a8dfc81a86f5115d2abd545751a304c5bcf2c2c7283cfe", size = 184344, upload-time = "2025-09-08T23:22:26.456Z" }, + { url = "https://files.pythonhosted.org/packages/4f/8b/f0e4c441227ba756aafbe78f117485b25bb26b1c059d01f137fa6d14896b/cffi-2.0.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:2de9a304e27f7596cd03d16f1b7c72219bd944e99cc52b84d0145aefb07cbd3c", size = 180560, upload-time = "2025-09-08T23:22:28.197Z" }, + { url = "https://files.pythonhosted.org/packages/b1/b7/1200d354378ef52ec227395d95c2576330fd22a869f7a70e88e1447eb234/cffi-2.0.0-cp311-cp311-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:baf5215e0ab74c16e2dd324e8ec067ef59e41125d3eade2b863d294fd5035c92", size = 209613, upload-time = "2025-09-08T23:22:29.475Z" }, + { url = "https://files.pythonhosted.org/packages/b8/56/6033f5e86e8cc9bb629f0077ba71679508bdf54a9a5e112a3c0b91870332/cffi-2.0.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:730cacb21e1bdff3ce90babf007d0a0917cc3e6492f336c2f0134101e0944f93", size = 216476, upload-time = "2025-09-08T23:22:31.063Z" }, + { url = "https://files.pythonhosted.org/packages/dc/7f/55fecd70f7ece178db2f26128ec41430d8720f2d12ca97bf8f0a628207d5/cffi-2.0.0-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:6824f87845e3396029f3820c206e459ccc91760e8fa24422f8b0c3d1731cbec5", size = 203374, upload-time = "2025-09-08T23:22:32.507Z" }, + { url = "https://files.pythonhosted.org/packages/84/ef/a7b77c8bdc0f77adc3b46888f1ad54be8f3b7821697a7b89126e829e676a/cffi-2.0.0-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:9de40a7b0323d889cf8d23d1ef214f565ab154443c42737dfe52ff82cf857664", size = 202597, upload-time = "2025-09-08T23:22:34.132Z" }, + { url = "https://files.pythonhosted.org/packages/d7/91/500d892b2bf36529a75b77958edfcd5ad8e2ce4064ce2ecfeab2125d72d1/cffi-2.0.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:8941aaadaf67246224cee8c3803777eed332a19d909b47e29c9842ef1e79ac26", size = 215574, upload-time = "2025-09-08T23:22:35.443Z" }, + { url = "https://files.pythonhosted.org/packages/44/64/58f6255b62b101093d5df22dcb752596066c7e89dd725e0afaed242a61be/cffi-2.0.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:a05d0c237b3349096d3981b727493e22147f934b20f6f125a3eba8f994bec4a9", size = 218971, upload-time = "2025-09-08T23:22:36.805Z" }, + { url = "https://files.pythonhosted.org/packages/ab/49/fa72cebe2fd8a55fbe14956f9970fe8eb1ac59e5df042f603ef7c8ba0adc/cffi-2.0.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:94698a9c5f91f9d138526b48fe26a199609544591f859c870d477351dc7b2414", size = 211972, upload-time = "2025-09-08T23:22:38.436Z" }, + { url = "https://files.pythonhosted.org/packages/0b/28/dd0967a76aab36731b6ebfe64dec4e981aff7e0608f60c2d46b46982607d/cffi-2.0.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:5fed36fccc0612a53f1d4d9a816b50a36702c28a2aa880cb8a122b3466638743", size = 217078, upload-time = "2025-09-08T23:22:39.776Z" }, + { url = "https://files.pythonhosted.org/packages/2b/c0/015b25184413d7ab0a410775fdb4a50fca20f5589b5dab1dbbfa3baad8ce/cffi-2.0.0-cp311-cp311-win32.whl", hash = "sha256:c649e3a33450ec82378822b3dad03cc228b8f5963c0c12fc3b1e0ab940f768a5", size = 172076, upload-time = "2025-09-08T23:22:40.95Z" }, + { url = "https://files.pythonhosted.org/packages/ae/8f/dc5531155e7070361eb1b7e4c1a9d896d0cb21c49f807a6c03fd63fc877e/cffi-2.0.0-cp311-cp311-win_amd64.whl", hash = "sha256:66f011380d0e49ed280c789fbd08ff0d40968ee7b665575489afa95c98196ab5", size = 182820, upload-time = "2025-09-08T23:22:42.463Z" }, + { url = "https://files.pythonhosted.org/packages/95/5c/1b493356429f9aecfd56bc171285a4c4ac8697f76e9bbbbb105e537853a1/cffi-2.0.0-cp311-cp311-win_arm64.whl", hash = "sha256:c6638687455baf640e37344fe26d37c404db8b80d037c3d29f58fe8d1c3b194d", size = 177635, upload-time = "2025-09-08T23:22:43.623Z" }, +] + +[[package]] +name = "charset-normalizer" +version = "3.4.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/83/2d/5fd176ceb9b2fc619e63405525573493ca23441330fcdaee6bef9460e924/charset_normalizer-3.4.3.tar.gz", hash = "sha256:6fce4b8500244f6fcb71465d4a4930d132ba9ab8e71a7859e6a5d59851068d14", size = 122371, upload-time = "2025-08-09T07:57:28.46Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/7f/b5/991245018615474a60965a7c9cd2b4efbaabd16d582a5547c47ee1c7730b/charset_normalizer-3.4.3-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:b256ee2e749283ef3ddcff51a675ff43798d92d746d1a6e4631bf8c707d22d0b", size = 204483, upload-time = "2025-08-09T07:55:53.12Z" }, + { url = "https://files.pythonhosted.org/packages/c7/2a/ae245c41c06299ec18262825c1569c5d3298fc920e4ddf56ab011b417efd/charset_normalizer-3.4.3-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:13faeacfe61784e2559e690fc53fa4c5ae97c6fcedb8eb6fb8d0a15b475d2c64", size = 145520, upload-time = "2025-08-09T07:55:54.712Z" }, + { url = "https://files.pythonhosted.org/packages/3a/a4/b3b6c76e7a635748c4421d2b92c7b8f90a432f98bda5082049af37ffc8e3/charset_normalizer-3.4.3-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:00237675befef519d9af72169d8604a067d92755e84fe76492fef5441db05b91", size = 158876, upload-time = "2025-08-09T07:55:56.024Z" }, + { url = "https://files.pythonhosted.org/packages/e2/e6/63bb0e10f90a8243c5def74b5b105b3bbbfb3e7bb753915fe333fb0c11ea/charset_normalizer-3.4.3-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:585f3b2a80fbd26b048a0be90c5aae8f06605d3c92615911c3a2b03a8a3b796f", size = 156083, upload-time = "2025-08-09T07:55:57.582Z" }, + { url = "https://files.pythonhosted.org/packages/87/df/b7737ff046c974b183ea9aa111b74185ac8c3a326c6262d413bd5a1b8c69/charset_normalizer-3.4.3-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0e78314bdc32fa80696f72fa16dc61168fda4d6a0c014e0380f9d02f0e5d8a07", size = 150295, upload-time = "2025-08-09T07:55:59.147Z" }, + { url = "https://files.pythonhosted.org/packages/61/f1/190d9977e0084d3f1dc169acd060d479bbbc71b90bf3e7bf7b9927dec3eb/charset_normalizer-3.4.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:96b2b3d1a83ad55310de8c7b4a2d04d9277d5591f40761274856635acc5fcb30", size = 148379, upload-time = "2025-08-09T07:56:00.364Z" }, + { url = "https://files.pythonhosted.org/packages/4c/92/27dbe365d34c68cfe0ca76f1edd70e8705d82b378cb54ebbaeabc2e3029d/charset_normalizer-3.4.3-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:939578d9d8fd4299220161fdd76e86c6a251987476f5243e8864a7844476ba14", size = 160018, upload-time = "2025-08-09T07:56:01.678Z" }, + { url = "https://files.pythonhosted.org/packages/99/04/baae2a1ea1893a01635d475b9261c889a18fd48393634b6270827869fa34/charset_normalizer-3.4.3-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:fd10de089bcdcd1be95a2f73dbe6254798ec1bda9f450d5828c96f93e2536b9c", size = 157430, upload-time = "2025-08-09T07:56:02.87Z" }, + { url = "https://files.pythonhosted.org/packages/2f/36/77da9c6a328c54d17b960c89eccacfab8271fdaaa228305330915b88afa9/charset_normalizer-3.4.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:1e8ac75d72fa3775e0b7cb7e4629cec13b7514d928d15ef8ea06bca03ef01cae", size = 151600, upload-time = "2025-08-09T07:56:04.089Z" }, + { url = "https://files.pythonhosted.org/packages/64/d4/9eb4ff2c167edbbf08cdd28e19078bf195762e9bd63371689cab5ecd3d0d/charset_normalizer-3.4.3-cp311-cp311-win32.whl", hash = "sha256:6cf8fd4c04756b6b60146d98cd8a77d0cdae0e1ca20329da2ac85eed779b6849", size = 99616, upload-time = "2025-08-09T07:56:05.658Z" }, + { url = "https://files.pythonhosted.org/packages/f4/9c/996a4a028222e7761a96634d1820de8a744ff4327a00ada9c8942033089b/charset_normalizer-3.4.3-cp311-cp311-win_amd64.whl", hash = "sha256:31a9a6f775f9bcd865d88ee350f0ffb0e25936a7f930ca98995c05abf1faf21c", size = 107108, upload-time = "2025-08-09T07:56:07.176Z" }, + { url = "https://files.pythonhosted.org/packages/8a/1f/f041989e93b001bc4e44bb1669ccdcf54d3f00e628229a85b08d330615c5/charset_normalizer-3.4.3-py3-none-any.whl", hash = "sha256:ce571ab16d890d23b5c278547ba694193a45011ff86a9162a71307ed9f86759a", size = 53175, upload-time = "2025-08-09T07:57:26.864Z" }, +] + +[[package]] +name = "colorama" +version = "0.4.6" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/d8/53/6f443c9a4a8358a93a6792e2acffb9d9d5cb0a5cfd8802644b7b1c9a02e4/colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44", size = 27697, upload-time = "2022-10-25T02:36:22.414Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6", size = 25335, upload-time = "2022-10-25T02:36:20.889Z" }, +] + +[[package]] +name = "coremltools" +version = "8.3.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "attrs" }, + { name = "cattrs" }, + { name = "numpy" }, + { name = "packaging" }, + { name = "protobuf" }, + { name = "pyaml" }, + { name = "sympy" }, + { name = "tqdm" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/93/f1/322d8cb29c59b8710375927a6d776887ed4c6caafd036cf4fbe14dcdb767/coremltools-8.3.0.tar.gz", hash = "sha256:c95a6051606b71273d669b107b5f32d3191f595e6821b8db04baf49d52d0704f", size = 1642701, upload-time = "2025-04-28T20:14:06.235Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/72/a6/31dc762e0317d26b2d21919e12c42644ecab8401ff2aa6f00215156a45ee/coremltools-8.3.0-cp311-none-macosx_10_15_x86_64.whl", hash = "sha256:59ff68ec62bf2c0421041142117e37ef679f46e6304653aea64cbe8a39a5f9bc", size = 2770927, upload-time = "2025-04-28T20:13:37.598Z" }, + { url = "https://files.pythonhosted.org/packages/69/32/847810ade6b7105fcf810188f41dc4bb25e2278f505f8a08185bd8787cbb/coremltools-8.3.0-cp311-none-macosx_11_0_arm64.whl", hash = "sha256:5f843f5a6be740d84eb7c80c49766da0b9bd67e86a9cb1dbfce838ab5366feb3", size = 2743785, upload-time = "2025-04-28T20:13:39.291Z" }, + { url = "https://files.pythonhosted.org/packages/36/9c/a6fbc66e300e176f94ab6f90530d33c703868126572fb9119cc952b8ecc6/coremltools-8.3.0-cp311-none-manylinux1_x86_64.whl", hash = "sha256:3d6d5828688347b5f6e31f1ce522b5df5733246611dbcb09fbc94687ff0fc16a", size = 2293270, upload-time = "2025-04-28T20:13:41.847Z" }, +] + +[[package]] +name = "filelock" +version = "3.19.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/40/bb/0ab3e58d22305b6f5440629d20683af28959bf793d98d11950e305c1c326/filelock-3.19.1.tar.gz", hash = "sha256:66eda1888b0171c998b35be2bcc0f6d75c388a7ce20c3f3f37aa8e96c2dddf58", size = 17687, upload-time = "2025-08-14T16:56:03.016Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/42/14/42b2651a2f46b022ccd948bca9f2d5af0fd8929c4eec235b8d6d844fbe67/filelock-3.19.1-py3-none-any.whl", hash = "sha256:d38e30481def20772f5baf097c122c3babc4fcdb7e14e57049eb9d88c6dc017d", size = 15988, upload-time = "2025-08-14T16:56:01.633Z" }, +] + +[[package]] +name = "fsspec" +version = "2025.9.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/de/e0/bab50af11c2d75c9c4a2a26a5254573c0bd97cea152254401510950486fa/fsspec-2025.9.0.tar.gz", hash = "sha256:19fd429483d25d28b65ec68f9f4adc16c17ea2c7c7bf54ec61360d478fb19c19", size = 304847, upload-time = "2025-09-02T19:10:49.215Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/47/71/70db47e4f6ce3e5c37a607355f80da8860a33226be640226ac52cb05ef2e/fsspec-2025.9.0-py3-none-any.whl", hash = "sha256:530dc2a2af60a414a832059574df4a6e10cce927f6f4a78209390fe38955cfb7", size = 199289, upload-time = "2025-09-02T19:10:47.708Z" }, +] + +[[package]] +name = "idna" +version = "3.10" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/f1/70/7703c29685631f5a7590aa73f1f1d3fa9a380e654b86af429e0934a32f7d/idna-3.10.tar.gz", hash = "sha256:12f65c9b470abda6dc35cf8e63cc574b1c52b11df2c86030af0ac09b01b13ea9", size = 190490, upload-time = "2024-09-15T18:07:39.745Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/76/c6/c88e154df9c4e1a2a66ccf0005a88dfb2650c1dffb6f5ce603dfbd452ce3/idna-3.10-py3-none-any.whl", hash = "sha256:946d195a0d259cbba61165e88e65941f16e9b36ea6ddb97f00452bae8b1287d3", size = 70442, upload-time = "2024-09-15T18:07:37.964Z" }, +] + +[[package]] +name = "jinja2" +version = "3.1.6" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "markupsafe" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/df/bf/f7da0350254c0ed7c72f3e33cef02e048281fec7ecec5f032d4aac52226b/jinja2-3.1.6.tar.gz", hash = "sha256:0137fb05990d35f1275a587e9aee6d56da821fc83491a0fb838183be43f66d6d", size = 245115, upload-time = "2025-03-05T20:05:02.478Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/62/a1/3d680cbfd5f4b8f15abc1d571870c5fc3e594bb582bc3b64ea099db13e56/jinja2-3.1.6-py3-none-any.whl", hash = "sha256:85ece4451f492d0c13c5dd7c13a64681a86afae63a5f347908daf103ce6d2f67", size = 134899, upload-time = "2025-03-05T20:05:00.369Z" }, +] + +[[package]] +name = "markupsafe" +version = "3.0.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/b2/97/5d42485e71dfc078108a86d6de8fa46db44a1a9295e89c5d6d4a06e23a62/markupsafe-3.0.2.tar.gz", hash = "sha256:ee55d3edf80167e48ea11a923c7386f4669df67d7994554387f84e7d8b0a2bf0", size = 20537, upload-time = "2024-10-18T15:21:54.129Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/6b/28/bbf83e3f76936960b850435576dd5e67034e200469571be53f69174a2dfd/MarkupSafe-3.0.2-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:9025b4018f3a1314059769c7bf15441064b2207cb3f065e6ea1e7359cb46db9d", size = 14353, upload-time = "2024-10-18T15:21:02.187Z" }, + { url = "https://files.pythonhosted.org/packages/6c/30/316d194b093cde57d448a4c3209f22e3046c5bb2fb0820b118292b334be7/MarkupSafe-3.0.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:93335ca3812df2f366e80509ae119189886b0f3c2b81325d39efdb84a1e2ae93", size = 12392, upload-time = "2024-10-18T15:21:02.941Z" }, + { url = "https://files.pythonhosted.org/packages/f2/96/9cdafba8445d3a53cae530aaf83c38ec64c4d5427d975c974084af5bc5d2/MarkupSafe-3.0.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2cb8438c3cbb25e220c2ab33bb226559e7afb3baec11c4f218ffa7308603c832", size = 23984, upload-time = "2024-10-18T15:21:03.953Z" }, + { url = "https://files.pythonhosted.org/packages/f1/a4/aefb044a2cd8d7334c8a47d3fb2c9f328ac48cb349468cc31c20b539305f/MarkupSafe-3.0.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a123e330ef0853c6e822384873bef7507557d8e4a082961e1defa947aa59ba84", size = 23120, upload-time = "2024-10-18T15:21:06.495Z" }, + { url = "https://files.pythonhosted.org/packages/8d/21/5e4851379f88f3fad1de30361db501300d4f07bcad047d3cb0449fc51f8c/MarkupSafe-3.0.2-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1e084f686b92e5b83186b07e8a17fc09e38fff551f3602b249881fec658d3eca", size = 23032, upload-time = "2024-10-18T15:21:07.295Z" }, + { url = "https://files.pythonhosted.org/packages/00/7b/e92c64e079b2d0d7ddf69899c98842f3f9a60a1ae72657c89ce2655c999d/MarkupSafe-3.0.2-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:d8213e09c917a951de9d09ecee036d5c7d36cb6cb7dbaece4c71a60d79fb9798", size = 24057, upload-time = "2024-10-18T15:21:08.073Z" }, + { url = "https://files.pythonhosted.org/packages/f9/ac/46f960ca323037caa0a10662ef97d0a4728e890334fc156b9f9e52bcc4ca/MarkupSafe-3.0.2-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:5b02fb34468b6aaa40dfc198d813a641e3a63b98c2b05a16b9f80b7ec314185e", size = 23359, upload-time = "2024-10-18T15:21:09.318Z" }, + { url = "https://files.pythonhosted.org/packages/69/84/83439e16197337b8b14b6a5b9c2105fff81d42c2a7c5b58ac7b62ee2c3b1/MarkupSafe-3.0.2-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:0bff5e0ae4ef2e1ae4fdf2dfd5b76c75e5c2fa4132d05fc1b0dabcd20c7e28c4", size = 23306, upload-time = "2024-10-18T15:21:10.185Z" }, + { url = "https://files.pythonhosted.org/packages/9a/34/a15aa69f01e2181ed8d2b685c0d2f6655d5cca2c4db0ddea775e631918cd/MarkupSafe-3.0.2-cp311-cp311-win32.whl", hash = "sha256:6c89876f41da747c8d3677a2b540fb32ef5715f97b66eeb0c6b66f5e3ef6f59d", size = 15094, upload-time = "2024-10-18T15:21:11.005Z" }, + { url = "https://files.pythonhosted.org/packages/da/b8/3a3bd761922d416f3dc5d00bfbed11f66b1ab89a0c2b6e887240a30b0f6b/MarkupSafe-3.0.2-cp311-cp311-win_amd64.whl", hash = "sha256:70a87b411535ccad5ef2f1df5136506a10775d267e197e4cf531ced10537bd6b", size = 15521, upload-time = "2024-10-18T15:21:12.911Z" }, +] + +[[package]] +name = "mpmath" +version = "1.3.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/e0/47/dd32fa426cc72114383ac549964eecb20ecfd886d1e5ccf5340b55b02f57/mpmath-1.3.0.tar.gz", hash = "sha256:7a28eb2a9774d00c7bc92411c19a89209d5da7c4c9a9e227be8330a23a25b91f", size = 508106, upload-time = "2023-03-07T16:47:11.061Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/43/e3/7d92a15f894aa0c9c4b49b8ee9ac9850d6e63b03c9c32c0367a13ae62209/mpmath-1.3.0-py3-none-any.whl", hash = "sha256:a0b2b9fe80bbcd81a6647ff13108738cfb482d481d826cc0e02f5b35e5c88d2c", size = 536198, upload-time = "2023-03-07T16:47:09.197Z" }, +] + +[[package]] +name = "networkx" +version = "3.5" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/6c/4f/ccdb8ad3a38e583f214547fd2f7ff1fc160c43a75af88e6aec213404b96a/networkx-3.5.tar.gz", hash = "sha256:d4c6f9cf81f52d69230866796b82afbccdec3db7ae4fbd1b65ea750feed50037", size = 2471065, upload-time = "2025-05-29T11:35:07.804Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/eb/8d/776adee7bbf76365fdd7f2552710282c79a4ead5d2a46408c9043a2b70ba/networkx-3.5-py3-none-any.whl", hash = "sha256:0030d386a9a06dee3565298b4a734b68589749a544acbb6c412dc9e2489ec6ec", size = 2034406, upload-time = "2025-05-29T11:35:04.961Z" }, +] + +[[package]] +name = "numpy" +version = "2.3.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/d0/19/95b3d357407220ed24c139018d2518fab0a61a948e68286a25f1a4d049ff/numpy-2.3.3.tar.gz", hash = "sha256:ddc7c39727ba62b80dfdbedf400d1c10ddfa8eefbd7ec8dcb118be8b56d31029", size = 20576648, upload-time = "2025-09-09T16:54:12.543Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/7a/45/e80d203ef6b267aa29b22714fb558930b27960a0c5ce3c19c999232bb3eb/numpy-2.3.3-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:0ffc4f5caba7dfcbe944ed674b7eef683c7e94874046454bb79ed7ee0236f59d", size = 21259253, upload-time = "2025-09-09T15:56:02.094Z" }, + { url = "https://files.pythonhosted.org/packages/52/18/cf2c648fccf339e59302e00e5f2bc87725a3ce1992f30f3f78c9044d7c43/numpy-2.3.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:e7e946c7170858a0295f79a60214424caac2ffdb0063d4d79cb681f9aa0aa569", size = 14450980, upload-time = "2025-09-09T15:56:05.926Z" }, + { url = "https://files.pythonhosted.org/packages/93/fb/9af1082bec870188c42a1c239839915b74a5099c392389ff04215dcee812/numpy-2.3.3-cp311-cp311-macosx_14_0_arm64.whl", hash = "sha256:cd4260f64bc794c3390a63bf0728220dd1a68170c169088a1e0dfa2fde1be12f", size = 5379709, upload-time = "2025-09-09T15:56:07.95Z" }, + { url = "https://files.pythonhosted.org/packages/75/0f/bfd7abca52bcbf9a4a65abc83fe18ef01ccdeb37bfb28bbd6ad613447c79/numpy-2.3.3-cp311-cp311-macosx_14_0_x86_64.whl", hash = "sha256:f0ddb4b96a87b6728df9362135e764eac3cfa674499943ebc44ce96c478ab125", size = 6913923, upload-time = "2025-09-09T15:56:09.443Z" }, + { url = "https://files.pythonhosted.org/packages/79/55/d69adad255e87ab7afda1caf93ca997859092afeb697703e2f010f7c2e55/numpy-2.3.3-cp311-cp311-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:afd07d377f478344ec6ca2b8d4ca08ae8bd44706763d1efb56397de606393f48", size = 14589591, upload-time = "2025-09-09T15:56:11.234Z" }, + { url = "https://files.pythonhosted.org/packages/10/a2/010b0e27ddeacab7839957d7a8f00e91206e0c2c47abbb5f35a2630e5387/numpy-2.3.3-cp311-cp311-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:bc92a5dedcc53857249ca51ef29f5e5f2f8c513e22cfb90faeb20343b8c6f7a6", size = 16938714, upload-time = "2025-09-09T15:56:14.637Z" }, + { url = "https://files.pythonhosted.org/packages/1c/6b/12ce8ede632c7126eb2762b9e15e18e204b81725b81f35176eac14dc5b82/numpy-2.3.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:7af05ed4dc19f308e1d9fc759f36f21921eb7bbfc82843eeec6b2a2863a0aefa", size = 16370592, upload-time = "2025-09-09T15:56:17.285Z" }, + { url = "https://files.pythonhosted.org/packages/b4/35/aba8568b2593067bb6a8fe4c52babb23b4c3b9c80e1b49dff03a09925e4a/numpy-2.3.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:433bf137e338677cebdd5beac0199ac84712ad9d630b74eceeb759eaa45ddf30", size = 18884474, upload-time = "2025-09-09T15:56:20.943Z" }, + { url = "https://files.pythonhosted.org/packages/45/fa/7f43ba10c77575e8be7b0138d107e4f44ca4a1ef322cd16980ea3e8b8222/numpy-2.3.3-cp311-cp311-win32.whl", hash = "sha256:eb63d443d7b4ffd1e873f8155260d7f58e7e4b095961b01c91062935c2491e57", size = 6599794, upload-time = "2025-09-09T15:56:23.258Z" }, + { url = "https://files.pythonhosted.org/packages/0a/a2/a4f78cb2241fe5664a22a10332f2be886dcdea8784c9f6a01c272da9b426/numpy-2.3.3-cp311-cp311-win_amd64.whl", hash = "sha256:ec9d249840f6a565f58d8f913bccac2444235025bbb13e9a4681783572ee3caa", size = 13088104, upload-time = "2025-09-09T15:56:25.476Z" }, + { url = "https://files.pythonhosted.org/packages/79/64/e424e975adbd38282ebcd4891661965b78783de893b381cbc4832fb9beb2/numpy-2.3.3-cp311-cp311-win_arm64.whl", hash = "sha256:74c2a948d02f88c11a3c075d9733f1ae67d97c6bdb97f2bb542f980458b257e7", size = 10460772, upload-time = "2025-09-09T15:56:27.679Z" }, + { url = "https://files.pythonhosted.org/packages/b8/f2/7e0a37cfced2644c9563c529f29fa28acbd0960dde32ece683aafa6f4949/numpy-2.3.3-pp311-pypy311_pp73-macosx_10_15_x86_64.whl", hash = "sha256:1e02c7159791cd481e1e6d5ddd766b62a4d5acf8df4d4d1afe35ee9c5c33a41e", size = 21131019, upload-time = "2025-09-09T15:58:42.838Z" }, + { url = "https://files.pythonhosted.org/packages/1a/7e/3291f505297ed63831135a6cc0f474da0c868a1f31b0dd9a9f03a7a0d2ed/numpy-2.3.3-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:dca2d0fc80b3893ae72197b39f69d55a3cd8b17ea1b50aa4c62de82419936150", size = 14376288, upload-time = "2025-09-09T15:58:45.425Z" }, + { url = "https://files.pythonhosted.org/packages/bf/4b/ae02e985bdeee73d7b5abdefeb98aef1207e96d4c0621ee0cf228ddfac3c/numpy-2.3.3-pp311-pypy311_pp73-macosx_14_0_arm64.whl", hash = "sha256:99683cbe0658f8271b333a1b1b4bb3173750ad59c0c61f5bbdc5b318918fffe3", size = 5305425, upload-time = "2025-09-09T15:58:48.6Z" }, + { url = "https://files.pythonhosted.org/packages/8b/eb/9df215d6d7250db32007941500dc51c48190be25f2401d5b2b564e467247/numpy-2.3.3-pp311-pypy311_pp73-macosx_14_0_x86_64.whl", hash = "sha256:d9d537a39cc9de668e5cd0e25affb17aec17b577c6b3ae8a3d866b479fbe88d0", size = 6819053, upload-time = "2025-09-09T15:58:50.401Z" }, + { url = "https://files.pythonhosted.org/packages/57/62/208293d7d6b2a8998a4a1f23ac758648c3c32182d4ce4346062018362e29/numpy-2.3.3-pp311-pypy311_pp73-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8596ba2f8af5f93b01d97563832686d20206d303024777f6dfc2e7c7c3f1850e", size = 14420354, upload-time = "2025-09-09T15:58:52.704Z" }, + { url = "https://files.pythonhosted.org/packages/ed/0c/8e86e0ff7072e14a71b4c6af63175e40d1e7e933ce9b9e9f765a95b4e0c3/numpy-2.3.3-pp311-pypy311_pp73-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e1ec5615b05369925bd1125f27df33f3b6c8bc10d788d5999ecd8769a1fa04db", size = 16760413, upload-time = "2025-09-09T15:58:55.027Z" }, + { url = "https://files.pythonhosted.org/packages/af/11/0cc63f9f321ccf63886ac203336777140011fb669e739da36d8db3c53b98/numpy-2.3.3-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:2e267c7da5bf7309670523896df97f93f6e469fb931161f483cd6882b3b1a5dc", size = 12971844, upload-time = "2025-09-09T15:58:57.359Z" }, +] + +[[package]] +name = "nvidia-cublas-cu12" +version = "12.8.4.1" +source = { registry = "https://pypi.org/simple" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/dc/61/e24b560ab2e2eaeb3c839129175fb330dfcfc29e5203196e5541a4c44682/nvidia_cublas_cu12-12.8.4.1-py3-none-manylinux_2_27_x86_64.whl", hash = "sha256:8ac4e771d5a348c551b2a426eda6193c19aa630236b418086020df5ba9667142", size = 594346921, upload-time = "2025-03-07T01:44:31.254Z" }, +] + +[[package]] +name = "nvidia-cuda-cupti-cu12" +version = "12.8.90" +source = { registry = "https://pypi.org/simple" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f8/02/2adcaa145158bf1a8295d83591d22e4103dbfd821bcaf6f3f53151ca4ffa/nvidia_cuda_cupti_cu12-12.8.90-py3-none-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:ea0cb07ebda26bb9b29ba82cda34849e73c166c18162d3913575b0c9db9a6182", size = 10248621, upload-time = "2025-03-07T01:40:21.213Z" }, +] + +[[package]] +name = "nvidia-cuda-nvrtc-cu12" +version = "12.8.93" +source = { registry = "https://pypi.org/simple" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/05/6b/32f747947df2da6994e999492ab306a903659555dddc0fbdeb9d71f75e52/nvidia_cuda_nvrtc_cu12-12.8.93-py3-none-manylinux2010_x86_64.manylinux_2_12_x86_64.whl", hash = "sha256:a7756528852ef889772a84c6cd89d41dfa74667e24cca16bb31f8f061e3e9994", size = 88040029, upload-time = "2025-03-07T01:42:13.562Z" }, +] + +[[package]] +name = "nvidia-cuda-runtime-cu12" +version = "12.8.90" +source = { registry = "https://pypi.org/simple" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/0d/9b/a997b638fcd068ad6e4d53b8551a7d30fe8b404d6f1804abf1df69838932/nvidia_cuda_runtime_cu12-12.8.90-py3-none-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:adade8dcbd0edf427b7204d480d6066d33902cab2a4707dcfc48a2d0fd44ab90", size = 954765, upload-time = "2025-03-07T01:40:01.615Z" }, +] + +[[package]] +name = "nvidia-cudnn-cu12" +version = "9.10.2.21" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "nvidia-cublas-cu12" }, +] +wheels = [ + { url = "https://files.pythonhosted.org/packages/ba/51/e123d997aa098c61d029f76663dedbfb9bc8dcf8c60cbd6adbe42f76d049/nvidia_cudnn_cu12-9.10.2.21-py3-none-manylinux_2_27_x86_64.whl", hash = "sha256:949452be657fa16687d0930933f032835951ef0892b37d2d53824d1a84dc97a8", size = 706758467, upload-time = "2025-06-06T21:54:08.597Z" }, +] + +[[package]] +name = "nvidia-cufft-cu12" +version = "11.3.3.83" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "nvidia-nvjitlink-cu12" }, +] +wheels = [ + { url = "https://files.pythonhosted.org/packages/1f/13/ee4e00f30e676b66ae65b4f08cb5bcbb8392c03f54f2d5413ea99a5d1c80/nvidia_cufft_cu12-11.3.3.83-py3-none-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:4d2dd21ec0b88cf61b62e6b43564355e5222e4a3fb394cac0db101f2dd0d4f74", size = 193118695, upload-time = "2025-03-07T01:45:27.821Z" }, +] + +[[package]] +name = "nvidia-cufile-cu12" +version = "1.13.1.3" +source = { registry = "https://pypi.org/simple" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/bb/fe/1bcba1dfbfb8d01be8d93f07bfc502c93fa23afa6fd5ab3fc7c1df71038a/nvidia_cufile_cu12-1.13.1.3-py3-none-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:1d069003be650e131b21c932ec3d8969c1715379251f8d23a1860554b1cb24fc", size = 1197834, upload-time = "2025-03-07T01:45:50.723Z" }, +] + +[[package]] +name = "nvidia-curand-cu12" +version = "10.3.9.90" +source = { registry = "https://pypi.org/simple" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/fb/aa/6584b56dc84ebe9cf93226a5cde4d99080c8e90ab40f0c27bda7a0f29aa1/nvidia_curand_cu12-10.3.9.90-py3-none-manylinux_2_27_x86_64.whl", hash = "sha256:b32331d4f4df5d6eefa0554c565b626c7216f87a06a4f56fab27c3b68a830ec9", size = 63619976, upload-time = "2025-03-07T01:46:23.323Z" }, +] + +[[package]] +name = "nvidia-cusolver-cu12" +version = "11.7.3.90" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "nvidia-cublas-cu12" }, + { name = "nvidia-cusparse-cu12" }, + { name = "nvidia-nvjitlink-cu12" }, +] +wheels = [ + { url = "https://files.pythonhosted.org/packages/85/48/9a13d2975803e8cf2777d5ed57b87a0b6ca2cc795f9a4f59796a910bfb80/nvidia_cusolver_cu12-11.7.3.90-py3-none-manylinux_2_27_x86_64.whl", hash = "sha256:4376c11ad263152bd50ea295c05370360776f8c3427b30991df774f9fb26c450", size = 267506905, upload-time = "2025-03-07T01:47:16.273Z" }, +] + +[[package]] +name = "nvidia-cusparse-cu12" +version = "12.5.8.93" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "nvidia-nvjitlink-cu12" }, +] +wheels = [ + { url = "https://files.pythonhosted.org/packages/c2/f5/e1854cb2f2bcd4280c44736c93550cc300ff4b8c95ebe370d0aa7d2b473d/nvidia_cusparse_cu12-12.5.8.93-py3-none-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:1ec05d76bbbd8b61b06a80e1eaf8cf4959c3d4ce8e711b65ebd0443bb0ebb13b", size = 288216466, upload-time = "2025-03-07T01:48:13.779Z" }, +] + +[[package]] +name = "nvidia-cusparselt-cu12" +version = "0.7.1" +source = { registry = "https://pypi.org/simple" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/56/79/12978b96bd44274fe38b5dde5cfb660b1d114f70a65ef962bcbbed99b549/nvidia_cusparselt_cu12-0.7.1-py3-none-manylinux2014_x86_64.whl", hash = "sha256:f1bb701d6b930d5a7cea44c19ceb973311500847f81b634d802b7b539dc55623", size = 287193691, upload-time = "2025-02-26T00:15:44.104Z" }, +] + +[[package]] +name = "nvidia-nccl-cu12" +version = "2.27.3" +source = { registry = "https://pypi.org/simple" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/5c/5b/4e4fff7bad39adf89f735f2bc87248c81db71205b62bcc0d5ca5b606b3c3/nvidia_nccl_cu12-2.27.3-py3-none-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:adf27ccf4238253e0b826bce3ff5fa532d65fc42322c8bfdfaf28024c0fbe039", size = 322364134, upload-time = "2025-06-03T21:58:04.013Z" }, +] + +[[package]] +name = "nvidia-nvjitlink-cu12" +version = "12.8.93" +source = { registry = "https://pypi.org/simple" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f6/74/86a07f1d0f42998ca31312f998bd3b9a7eff7f52378f4f270c8679c77fb9/nvidia_nvjitlink_cu12-12.8.93-py3-none-manylinux2010_x86_64.manylinux_2_12_x86_64.whl", hash = "sha256:81ff63371a7ebd6e6451970684f916be2eab07321b73c9d244dc2b4da7f73b88", size = 39254836, upload-time = "2025-03-07T01:49:55.661Z" }, +] + +[[package]] +name = "nvidia-nvtx-cu12" +version = "12.8.90" +source = { registry = "https://pypi.org/simple" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a2/eb/86626c1bbc2edb86323022371c39aa48df6fd8b0a1647bc274577f72e90b/nvidia_nvtx_cu12-12.8.90-py3-none-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:5b17e2001cc0d751a5bc2c6ec6d26ad95913324a4adb86788c944f8ce9ba441f", size = 89954, upload-time = "2025-03-07T01:42:44.131Z" }, +] + +[[package]] +name = "packaging" +version = "25.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/a1/d4/1fc4078c65507b51b96ca8f8c3ba19e6a61c8253c72794544580a7b6c24d/packaging-25.0.tar.gz", hash = "sha256:d443872c98d677bf60f6a1f2f8c1cb748e8fe762d2bf9d3148b5599295b0fc4f", size = 165727, upload-time = "2025-04-19T11:48:59.673Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/20/12/38679034af332785aac8774540895e234f4d07f7545804097de4b666afd8/packaging-25.0-py3-none-any.whl", hash = "sha256:29572ef2b1f17581046b3a2227d5c611fb25ec70ca1ba8554b24b0e69331a484", size = 66469, upload-time = "2025-04-19T11:48:57.875Z" }, +] + +[[package]] +name = "protobuf" +version = "6.32.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/fa/a4/cc17347aa2897568beece2e674674359f911d6fe21b0b8d6268cd42727ac/protobuf-6.32.1.tar.gz", hash = "sha256:ee2469e4a021474ab9baafea6cd070e5bf27c7d29433504ddea1a4ee5850f68d", size = 440635, upload-time = "2025-09-11T21:38:42.935Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c0/98/645183ea03ab3995d29086b8bf4f7562ebd3d10c9a4b14ee3f20d47cfe50/protobuf-6.32.1-cp310-abi3-win32.whl", hash = "sha256:a8a32a84bc9f2aad712041b8b366190f71dde248926da517bde9e832e4412085", size = 424411, upload-time = "2025-09-11T21:38:27.427Z" }, + { url = "https://files.pythonhosted.org/packages/8c/f3/6f58f841f6ebafe076cebeae33fc336e900619d34b1c93e4b5c97a81fdfa/protobuf-6.32.1-cp310-abi3-win_amd64.whl", hash = "sha256:b00a7d8c25fa471f16bc8153d0e53d6c9e827f0953f3c09aaa4331c718cae5e1", size = 435738, upload-time = "2025-09-11T21:38:30.959Z" }, + { url = "https://files.pythonhosted.org/packages/10/56/a8a3f4e7190837139e68c7002ec749190a163af3e330f65d90309145a210/protobuf-6.32.1-cp39-abi3-macosx_10_9_universal2.whl", hash = "sha256:d8c7e6eb619ffdf105ee4ab76af5a68b60a9d0f66da3ea12d1640e6d8dab7281", size = 426454, upload-time = "2025-09-11T21:38:34.076Z" }, + { url = "https://files.pythonhosted.org/packages/3f/be/8dd0a927c559b37d7a6c8ab79034fd167dcc1f851595f2e641ad62be8643/protobuf-6.32.1-cp39-abi3-manylinux2014_aarch64.whl", hash = "sha256:2f5b80a49e1eb7b86d85fcd23fe92df154b9730a725c3b38c4e43b9d77018bf4", size = 322874, upload-time = "2025-09-11T21:38:35.509Z" }, + { url = "https://files.pythonhosted.org/packages/5c/f6/88d77011b605ef979aace37b7703e4eefad066f7e84d935e5a696515c2dd/protobuf-6.32.1-cp39-abi3-manylinux2014_x86_64.whl", hash = "sha256:b1864818300c297265c83a4982fd3169f97122c299f56a56e2445c3698d34710", size = 322013, upload-time = "2025-09-11T21:38:37.017Z" }, + { url = "https://files.pythonhosted.org/packages/97/b7/15cc7d93443d6c6a84626ae3258a91f4c6ac8c0edd5df35ea7658f71b79c/protobuf-6.32.1-py3-none-any.whl", hash = "sha256:2601b779fc7d32a866c6b4404f9d42a3f67c5b9f3f15b4db3cccabe06b95c346", size = 169289, upload-time = "2025-09-11T21:38:41.234Z" }, +] + +[[package]] +name = "pyaml" +version = "25.7.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pyyaml" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/c4/01/41f63d66a801a561c9e335523516bd5f761bc43cc61f8b75918306bf2da8/pyaml-25.7.0.tar.gz", hash = "sha256:e113a64ec16881bf2b092e2beb84b7dcf1bd98096ad17f5f14e8fb782a75d99b", size = 29814, upload-time = "2025-07-10T18:44:51.824Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a8/ee/a878f2ad010cbccb311f947f0f2f09d38f613938ee28c34e60fceecc75a1/pyaml-25.7.0-py3-none-any.whl", hash = "sha256:ce5d7867cc2b455efdb9b0448324ff7b9f74d99f64650f12ca570102db6b985f", size = 26418, upload-time = "2025-07-10T18:44:50.679Z" }, +] + +[[package]] +name = "pycparser" +version = "2.23" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/fe/cf/d2d3b9f5699fb1e4615c8e32ff220203e43b248e1dfcc6736ad9057731ca/pycparser-2.23.tar.gz", hash = "sha256:78816d4f24add8f10a06d6f05b4d424ad9e96cfebf68a4ddc99c65c0720d00c2", size = 173734, upload-time = "2025-09-09T13:23:47.91Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a0/e3/59cd50310fc9b59512193629e1984c1f95e5c8ae6e5d8c69532ccc65a7fe/pycparser-2.23-py3-none-any.whl", hash = "sha256:e5c6e8d3fbad53479cab09ac03729e0a9faf2bee3db8208a550daf5af81a5934", size = 118140, upload-time = "2025-09-09T13:23:46.651Z" }, +] + +[[package]] +name = "pyyaml" +version = "6.0.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/54/ed/79a089b6be93607fa5cdaedf301d7dfb23af5f25c398d5ead2525b063e17/pyyaml-6.0.2.tar.gz", hash = "sha256:d584d9ec91ad65861cc08d42e834324ef890a082e591037abe114850ff7bbc3e", size = 130631, upload-time = "2024-08-06T20:33:50.674Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f8/aa/7af4e81f7acba21a4c6be026da38fd2b872ca46226673c89a758ebdc4fd2/PyYAML-6.0.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:cc1c1159b3d456576af7a3e4d1ba7e6924cb39de8f67111c735f6fc832082774", size = 184612, upload-time = "2024-08-06T20:32:03.408Z" }, + { url = "https://files.pythonhosted.org/packages/8b/62/b9faa998fd185f65c1371643678e4d58254add437edb764a08c5a98fb986/PyYAML-6.0.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:1e2120ef853f59c7419231f3bf4e7021f1b936f6ebd222406c3b60212205d2ee", size = 172040, upload-time = "2024-08-06T20:32:04.926Z" }, + { url = "https://files.pythonhosted.org/packages/ad/0c/c804f5f922a9a6563bab712d8dcc70251e8af811fce4524d57c2c0fd49a4/PyYAML-6.0.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5d225db5a45f21e78dd9358e58a98702a0302f2659a3c6cd320564b75b86f47c", size = 736829, upload-time = "2024-08-06T20:32:06.459Z" }, + { url = "https://files.pythonhosted.org/packages/51/16/6af8d6a6b210c8e54f1406a6b9481febf9c64a3109c541567e35a49aa2e7/PyYAML-6.0.2-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:5ac9328ec4831237bec75defaf839f7d4564be1e6b25ac710bd1a96321cc8317", size = 764167, upload-time = "2024-08-06T20:32:08.338Z" }, + { url = "https://files.pythonhosted.org/packages/75/e4/2c27590dfc9992f73aabbeb9241ae20220bd9452df27483b6e56d3975cc5/PyYAML-6.0.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3ad2a3decf9aaba3d29c8f537ac4b243e36bef957511b4766cb0057d32b0be85", size = 762952, upload-time = "2024-08-06T20:32:14.124Z" }, + { url = "https://files.pythonhosted.org/packages/9b/97/ecc1abf4a823f5ac61941a9c00fe501b02ac3ab0e373c3857f7d4b83e2b6/PyYAML-6.0.2-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:ff3824dc5261f50c9b0dfb3be22b4567a6f938ccce4587b38952d85fd9e9afe4", size = 735301, upload-time = "2024-08-06T20:32:16.17Z" }, + { url = "https://files.pythonhosted.org/packages/45/73/0f49dacd6e82c9430e46f4a027baa4ca205e8b0a9dce1397f44edc23559d/PyYAML-6.0.2-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:797b4f722ffa07cc8d62053e4cff1486fa6dc094105d13fea7b1de7d8bf71c9e", size = 756638, upload-time = "2024-08-06T20:32:18.555Z" }, + { url = "https://files.pythonhosted.org/packages/22/5f/956f0f9fc65223a58fbc14459bf34b4cc48dec52e00535c79b8db361aabd/PyYAML-6.0.2-cp311-cp311-win32.whl", hash = "sha256:11d8f3dd2b9c1207dcaf2ee0bbbfd5991f571186ec9cc78427ba5bd32afae4b5", size = 143850, upload-time = "2024-08-06T20:32:19.889Z" }, + { url = "https://files.pythonhosted.org/packages/ed/23/8da0bbe2ab9dcdd11f4f4557ccaf95c10b9811b13ecced089d43ce59c3c8/PyYAML-6.0.2-cp311-cp311-win_amd64.whl", hash = "sha256:e10ce637b18caea04431ce14fabcf5c64a1c61ec9c56b071a4b7ca131ca52d44", size = 161980, upload-time = "2024-08-06T20:32:21.273Z" }, +] + +[[package]] +name = "requests" +version = "2.32.5" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "certifi" }, + { name = "charset-normalizer" }, + { name = "idna" }, + { name = "urllib3" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/c9/74/b3ff8e6c8446842c3f5c837e9c3dfcfe2018ea6ecef224c710c85ef728f4/requests-2.32.5.tar.gz", hash = "sha256:dbba0bac56e100853db0ea71b82b4dfd5fe2bf6d3754a8893c3af500cec7d7cf", size = 134517, upload-time = "2025-08-18T20:46:02.573Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/1e/db/4254e3eabe8020b458f1a747140d32277ec7a271daf1d235b70dc0b4e6e3/requests-2.32.5-py3-none-any.whl", hash = "sha256:2462f94637a34fd532264295e186976db0f5d453d1cdd31473c85a6a161affb6", size = 64738, upload-time = "2025-08-18T20:46:00.542Z" }, +] + +[[package]] +name = "setuptools" +version = "80.9.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/18/5d/3bf57dcd21979b887f014ea83c24ae194cfcd12b9e0fda66b957c69d1fca/setuptools-80.9.0.tar.gz", hash = "sha256:f36b47402ecde768dbfafc46e8e4207b4360c654f1f3bb84475f0a28628fb19c", size = 1319958, upload-time = "2025-05-27T00:56:51.443Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a3/dc/17031897dae0efacfea57dfd3a82fdd2a2aeb58e0ff71b77b87e44edc772/setuptools-80.9.0-py3-none-any.whl", hash = "sha256:062d34222ad13e0cc312a4c02d73f059e86a4acbfbdea8f8f76b28c99f306922", size = 1201486, upload-time = "2025-05-27T00:56:49.664Z" }, +] + +[[package]] +name = "soundfile" +version = "0.13.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "cffi" }, + { name = "numpy" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/e1/41/9b873a8c055582859b239be17902a85339bec6a30ad162f98c9b0288a2cc/soundfile-0.13.1.tar.gz", hash = "sha256:b2c68dab1e30297317080a5b43df57e302584c49e2942defdde0acccc53f0e5b", size = 46156, upload-time = "2025-01-25T09:17:04.831Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/64/28/e2a36573ccbcf3d57c00626a21fe51989380636e821b341d36ccca0c1c3a/soundfile-0.13.1-py2.py3-none-any.whl", hash = "sha256:a23c717560da2cf4c7b5ae1142514e0fd82d6bbd9dfc93a50423447142f2c445", size = 25751, upload-time = "2025-01-25T09:16:44.235Z" }, + { url = "https://files.pythonhosted.org/packages/ea/ab/73e97a5b3cc46bba7ff8650a1504348fa1863a6f9d57d7001c6b67c5f20e/soundfile-0.13.1-py2.py3-none-macosx_10_9_x86_64.whl", hash = "sha256:82dc664d19831933fe59adad199bf3945ad06d84bc111a5b4c0d3089a5b9ec33", size = 1142250, upload-time = "2025-01-25T09:16:47.583Z" }, + { url = "https://files.pythonhosted.org/packages/a0/e5/58fd1a8d7b26fc113af244f966ee3aecf03cb9293cb935daaddc1e455e18/soundfile-0.13.1-py2.py3-none-macosx_11_0_arm64.whl", hash = "sha256:743f12c12c4054921e15736c6be09ac26b3b3d603aef6fd69f9dde68748f2593", size = 1101406, upload-time = "2025-01-25T09:16:49.662Z" }, + { url = "https://files.pythonhosted.org/packages/58/ae/c0e4a53d77cf6e9a04179535766b3321b0b9ced5f70522e4caf9329f0046/soundfile-0.13.1-py2.py3-none-manylinux_2_28_aarch64.whl", hash = "sha256:9c9e855f5a4d06ce4213f31918653ab7de0c5a8d8107cd2427e44b42df547deb", size = 1235729, upload-time = "2025-01-25T09:16:53.018Z" }, + { url = "https://files.pythonhosted.org/packages/57/5e/70bdd9579b35003a489fc850b5047beeda26328053ebadc1fb60f320f7db/soundfile-0.13.1-py2.py3-none-manylinux_2_28_x86_64.whl", hash = "sha256:03267c4e493315294834a0870f31dbb3b28a95561b80b134f0bd3cf2d5f0e618", size = 1313646, upload-time = "2025-01-25T09:16:54.872Z" }, + { url = "https://files.pythonhosted.org/packages/fe/df/8c11dc4dfceda14e3003bb81a0d0edcaaf0796dd7b4f826ea3e532146bba/soundfile-0.13.1-py2.py3-none-win32.whl", hash = "sha256:c734564fab7c5ddf8e9be5bf70bab68042cd17e9c214c06e365e20d64f9a69d5", size = 899881, upload-time = "2025-01-25T09:16:56.663Z" }, + { url = "https://files.pythonhosted.org/packages/14/e9/6b761de83277f2f02ded7e7ea6f07828ec78e4b229b80e4ca55dd205b9dc/soundfile-0.13.1-py2.py3-none-win_amd64.whl", hash = "sha256:1e70a05a0626524a69e9f0f4dd2ec174b4e9567f4d8b6c11d38b5c289be36ee9", size = 1019162, upload-time = "2025-01-25T09:16:59.573Z" }, +] + +[[package]] +name = "sympy" +version = "1.14.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "mpmath" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/83/d3/803453b36afefb7c2bb238361cd4ae6125a569b4db67cd9e79846ba2d68c/sympy-1.14.0.tar.gz", hash = "sha256:d3d3fe8df1e5a0b42f0e7bdf50541697dbe7d23746e894990c030e2b05e72517", size = 7793921, upload-time = "2025-04-27T18:05:01.611Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a2/09/77d55d46fd61b4a135c444fc97158ef34a095e5681d0a6c10b75bf356191/sympy-1.14.0-py3-none-any.whl", hash = "sha256:e091cc3e99d2141a0ba2847328f5479b05d94a6635cb96148ccb3f34671bd8f5", size = 6299353, upload-time = "2025-04-27T18:04:59.103Z" }, +] + +[[package]] +name = "torch" +version = "2.8.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "filelock" }, + { name = "fsspec" }, + { name = "jinja2" }, + { name = "networkx" }, + { name = "nvidia-cublas-cu12", marker = "platform_machine == 'x86_64' and sys_platform == 'linux'" }, + { name = "nvidia-cuda-cupti-cu12", marker = "platform_machine == 'x86_64' and sys_platform == 'linux'" }, + { name = "nvidia-cuda-nvrtc-cu12", marker = "platform_machine == 'x86_64' and sys_platform == 'linux'" }, + { name = "nvidia-cuda-runtime-cu12", marker = "platform_machine == 'x86_64' and sys_platform == 'linux'" }, + { name = "nvidia-cudnn-cu12", marker = "platform_machine == 'x86_64' and sys_platform == 'linux'" }, + { name = "nvidia-cufft-cu12", marker = "platform_machine == 'x86_64' and sys_platform == 'linux'" }, + { name = "nvidia-cufile-cu12", marker = "platform_machine == 'x86_64' and sys_platform == 'linux'" }, + { name = "nvidia-curand-cu12", marker = "platform_machine == 'x86_64' and sys_platform == 'linux'" }, + { name = "nvidia-cusolver-cu12", marker = "platform_machine == 'x86_64' and sys_platform == 'linux'" }, + { name = "nvidia-cusparse-cu12", marker = "platform_machine == 'x86_64' and sys_platform == 'linux'" }, + { name = "nvidia-cusparselt-cu12", marker = "platform_machine == 'x86_64' and sys_platform == 'linux'" }, + { name = "nvidia-nccl-cu12", marker = "platform_machine == 'x86_64' and sys_platform == 'linux'" }, + { name = "nvidia-nvjitlink-cu12", marker = "platform_machine == 'x86_64' and sys_platform == 'linux'" }, + { name = "nvidia-nvtx-cu12", marker = "platform_machine == 'x86_64' and sys_platform == 'linux'" }, + { name = "sympy" }, + { name = "triton", marker = "platform_machine == 'x86_64' and sys_platform == 'linux'" }, + { name = "typing-extensions" }, +] +wheels = [ + { url = "https://files.pythonhosted.org/packages/8f/c4/3e7a3887eba14e815e614db70b3b529112d1513d9dae6f4d43e373360b7f/torch-2.8.0-cp311-cp311-manylinux_2_28_aarch64.whl", hash = "sha256:220a06fd7af8b653c35d359dfe1aaf32f65aa85befa342629f716acb134b9710", size = 102073391, upload-time = "2025-08-06T14:53:20.937Z" }, + { url = "https://files.pythonhosted.org/packages/5a/63/4fdc45a0304536e75a5e1b1bbfb1b56dd0e2743c48ee83ca729f7ce44162/torch-2.8.0-cp311-cp311-manylinux_2_28_x86_64.whl", hash = "sha256:c12fa219f51a933d5f80eeb3a7a5d0cbe9168c0a14bbb4055f1979431660879b", size = 888063640, upload-time = "2025-08-06T14:55:05.325Z" }, + { url = "https://files.pythonhosted.org/packages/84/57/2f64161769610cf6b1c5ed782bd8a780e18a3c9d48931319f2887fa9d0b1/torch-2.8.0-cp311-cp311-win_amd64.whl", hash = "sha256:8c7ef765e27551b2fbfc0f41bcf270e1292d9bf79f8e0724848b1682be6e80aa", size = 241366752, upload-time = "2025-08-06T14:53:38.692Z" }, + { url = "https://files.pythonhosted.org/packages/a4/5e/05a5c46085d9b97e928f3f037081d3d2b87fb4b4195030fc099aaec5effc/torch-2.8.0-cp311-none-macosx_11_0_arm64.whl", hash = "sha256:5ae0524688fb6707c57a530c2325e13bb0090b745ba7b4a2cd6a3ce262572916", size = 73621174, upload-time = "2025-08-06T14:53:25.44Z" }, +] + +[[package]] +name = "torchaudio" +version = "2.8.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "torch" }, +] +wheels = [ + { url = "https://files.pythonhosted.org/packages/dd/bf/6b01ef3defb8d0a772c863588711e9b2b011c27d6b37c1b9d15a359c8442/torchaudio-2.8.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:c9276857d241c6de257af765c0f51fc011af38cb725401495121b280913007cf", size = 1859094, upload-time = "2025-08-06T14:58:35.078Z" }, + { url = "https://files.pythonhosted.org/packages/75/ca/da5d0a3bb7d114a8b590ecce14859ea0a05102bb4de68cdd1ed7a90634d6/torchaudio-2.8.0-cp311-cp311-manylinux_2_28_aarch64.whl", hash = "sha256:4573c6042950c20278e3608a9a38050ba0bc72e0049e1bbfd249caf859a8029b", size = 1692033, upload-time = "2025-08-06T14:58:37.393Z" }, + { url = "https://files.pythonhosted.org/packages/b6/ef/62ac736d8f906cc414181050e08a495a637dab985186c34bd76ea37efbc0/torchaudio-2.8.0-cp311-cp311-manylinux_2_28_x86_64.whl", hash = "sha256:776c0b4ba84b9e3ddf6304b9c47cd63549d7896a6f3d5184ece074cc3d76ed6b", size = 4011716, upload-time = "2025-08-06T14:58:40.138Z" }, + { url = "https://files.pythonhosted.org/packages/14/86/015337c8434abc604b8680371df783f66c421a7f211cbe40a374b0540b6d/torchaudio-2.8.0-cp311-cp311-win_amd64.whl", hash = "sha256:078105bf80f725c0215a0bebac8cb2fb1b3993ab32bdc3fcd50145a5b4127001", size = 2505194, upload-time = "2025-08-06T14:58:57.301Z" }, +] + +[[package]] +name = "tqdm" +version = "4.67.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "colorama", marker = "sys_platform == 'win32'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/a8/4b/29b4ef32e036bb34e4ab51796dd745cdba7ed47ad142a9f4a1eb8e0c744d/tqdm-4.67.1.tar.gz", hash = "sha256:f8aef9c52c08c13a65f30ea34f4e5aac3fd1a34959879d7e59e63027286627f2", size = 169737, upload-time = "2024-11-24T20:12:22.481Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d0/30/dc54f88dd4a2b5dc8a0279bdd7270e735851848b762aeb1c1184ed1f6b14/tqdm-4.67.1-py3-none-any.whl", hash = "sha256:26445eca388f82e72884e0d580d5464cd801a3ea01e63e5601bdff9ba6a48de2", size = 78540, upload-time = "2024-11-24T20:12:19.698Z" }, +] + +[[package]] +name = "triton" +version = "3.4.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "setuptools" }, +] +wheels = [ + { url = "https://files.pythonhosted.org/packages/7d/39/43325b3b651d50187e591eefa22e236b2981afcebaefd4f2fc0ea99df191/triton-3.4.0-cp311-cp311-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:7b70f5e6a41e52e48cfc087436c8a28c17ff98db369447bcaff3b887a3ab4467", size = 155531138, upload-time = "2025-07-30T19:58:29.908Z" }, +] + +[[package]] +name = "typing-extensions" +version = "4.15.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/72/94/1a15dd82efb362ac84269196e94cf00f187f7ed21c242792a923cdb1c61f/typing_extensions-4.15.0.tar.gz", hash = "sha256:0cea48d173cc12fa28ecabc3b837ea3cf6f38c6d1136f85cbaaf598984861466", size = 109391, upload-time = "2025-08-25T13:49:26.313Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/18/67/36e9267722cc04a6b9f15c7f3441c2363321a3ea07da7ae0c0707beb2a9c/typing_extensions-4.15.0-py3-none-any.whl", hash = "sha256:f0fa19c6845758ab08074a0cfa8b7aecb71c999ca73d62883bc25cc018c4e548", size = 44614, upload-time = "2025-08-25T13:49:24.86Z" }, +] + +[[package]] +name = "urllib3" +version = "2.5.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/15/22/9ee70a2574a4f4599c47dd506532914ce044817c7752a79b6a51286319bc/urllib3-2.5.0.tar.gz", hash = "sha256:3fc47733c7e419d4bc3f6b3dc2b4f890bb743906a30d56ba4a5bfa4bbff92760", size = 393185, upload-time = "2025-06-18T14:07:41.644Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a7/c2/fe1e52489ae3122415c51f387e221dd0773709bad6c6cdaa599e8a2c5185/urllib3-2.5.0-py3-none-any.whl", hash = "sha256:e6b01673c0fa6a13e374b50871808eb3bf7046c4b125b216f6bf1cc604cff0dc", size = 129795, upload-time = "2025-06-18T14:07:40.39Z" }, +] From 00efbf04964cc790b971cb0bcc5e244f6a186511 Mon Sep 17 00:00:00 2001 From: hamzaq2000 Date: Mon, 22 Sep 2025 13:14:54 -0700 Subject: [PATCH 2/4] Use BATCH_SIZE variable in model file name --- models/emb/cam++/coreml/convert.py | 2 +- models/emb/cam++/coreml/test.py | 3 ++- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/models/emb/cam++/coreml/convert.py b/models/emb/cam++/coreml/convert.py index 893d2b7..938d265 100644 --- a/models/emb/cam++/coreml/convert.py +++ b/models/emb/cam++/coreml/convert.py @@ -104,7 +104,7 @@ def convert_campplus_to_coreml(): coreml_model.output_description["embeddings"] = f"Speaker embeddings: ({BATCH_SIZE}, {EMBEDDING_DIM})" # Save the model - output_path = "./models/camplusplus_batch16.mlpackage" + output_path = f"./models/camplusplus_batch{BATCH_SIZE}.mlpackage" coreml_model.save(output_path) print(f"\n✓ Saved CoreML model to: {output_path}") diff --git a/models/emb/cam++/coreml/test.py b/models/emb/cam++/coreml/test.py index 4dbd526..561c3e7 100644 --- a/models/emb/cam++/coreml/test.py +++ b/models/emb/cam++/coreml/test.py @@ -152,7 +152,8 @@ def main(audio_path): # Load CoreML model print("\n4. Loading CoreML model...") - coreml_model_path = Path("models/camplusplus_batch16.mlpackage") + BATCH_SIZE = 16 + coreml_model_path = Path(f"models/camplusplus_batch{BATCH_SIZE}.mlpackage") coreml_model = ct.models.MLModel(str(coreml_model_path)) print(f" Loaded: {coreml_model_path}") From 1f138006ede9b5728bb9437ebccc4118e258c87f Mon Sep 17 00:00:00 2001 From: hamzaq2000 Date: Tue, 23 Sep 2025 08:31:05 -0700 Subject: [PATCH 3/4] added CAM++ 3D-Speaker commit hash + link Senko C++ fbank_extractor --- models/emb/cam++/coreml/camplusplus.py | 23 ++++++++++++----------- models/emb/cam++/coreml/test.py | 5 ++++- 2 files changed, 16 insertions(+), 12 deletions(-) diff --git a/models/emb/cam++/coreml/camplusplus.py b/models/emb/cam++/coreml/camplusplus.py index 3869d15..03db33b 100644 --- a/models/emb/cam++/coreml/camplusplus.py +++ b/models/emb/cam++/coreml/camplusplus.py @@ -1,4 +1,5 @@ # https://github.com/modelscope/3D-Speaker/tree/main/speakerlab/models/campplus +# Commit hash f5764ed3a89da77330f7dcc9801f58992c3e3c74 import torch import torch.nn as nn @@ -15,15 +16,15 @@ def __init__(self, feat_dim=80, embedding_size=512, growth_rate=32, bn_size=4, i self.xvector = nn.Sequential(OrderedDict([ ('tdnn', TDNNLayer(channels, init_channels, 5, stride=2, dilation=1, padding=-1)) ])) - + channels = init_channels blocks_config = [(12, 3, 1), (24, 3, 2), (16, 3, 2)] - + for i, (num_layers, kernel_size, dilation) in enumerate(blocks_config): block = CAMDenseTDNNBlock(num_layers=num_layers, in_channels=channels, out_channels=growth_rate, bn_channels=bn_size * growth_rate, kernel_size=kernel_size, dilation=dilation, bias=False, memory_efficient=memory_efficient) self.xvector.add_module(f'block{i+1}', block) channels = channels + num_layers * growth_rate - + self.xvector.add_module(f'transit{i+1}', TransitLayer(channels, channels // 2, False)) channels //= 2 @@ -31,7 +32,7 @@ def __init__(self, feat_dim=80, embedding_size=512, growth_rate=32, bn_size=4, i out_nonlinear.add_module('batchnorm', nn.BatchNorm1d(channels)) out_nonlinear.add_module('relu', nn.ReLU(inplace=True)) self.xvector.add_module('out_nonlinear', out_nonlinear) - + self.xvector.add_module('stats', StatsPool()) self.xvector.add_module('dense', DenseLayer(channels * 2, embedding_size, False)) @@ -116,9 +117,9 @@ def __init__(self, in_channels, out_channels, kernel_size, stride=1, padding=0, if padding < 0: assert kernel_size % 2 == 1, 'Expect equal paddings, but got even kernel size ({})'.format(kernel_size) padding = (kernel_size - 1) // 2 * dilation - + self.linear = nn.Conv1d(in_channels, out_channels, kernel_size, stride=stride, padding=padding, dilation=dilation, bias=bias) - + self.nonlinear = nn.Sequential() self.nonlinear.add_module('batchnorm', nn.BatchNorm1d(out_channels)) self.nonlinear.add_module('relu', nn.ReLU(inplace=True)) @@ -131,7 +132,7 @@ class CAMLayer(nn.Module): def __init__(self, bn_channels, out_channels, kernel_size, stride, padding, dilation, bias, reduction=2): super(CAMLayer, self).__init__() self.linear_local = nn.Conv1d(bn_channels, out_channels, kernel_size, stride=stride, padding=padding, dilation=dilation, bias=bias) - self.linear1 = nn.Conv1d(bn_channels, bn_channels // reduction, 1) + self.linear1 = nn.Conv1d(bn_channels, bn_channels // reduction, 1) self.relu = nn.ReLU(inplace=True) self.linear2 = nn.Conv1d(bn_channels // reduction, out_channels, 1) self.sigmoid = nn.Sigmoid() @@ -151,17 +152,17 @@ def __init__(self, in_channels, out_channels, bn_channels, kernel_size, stride=1 super(CAMDenseTDNNLayer, self).__init__() assert kernel_size % 2 == 1, 'Expect equal paddings, but got even kernel size ({})'.format(kernel_size) self.memory_efficient = memory_efficient - + self.nonlinear1 = nn.Sequential() self.nonlinear1.add_module('batchnorm', nn.BatchNorm1d(in_channels)) self.nonlinear1.add_module('relu', nn.ReLU(inplace=True)) - + self.linear1 = nn.Conv1d(in_channels, bn_channels, 1, bias=False) - + self.nonlinear2 = nn.Sequential() self.nonlinear2.add_module('batchnorm', nn.BatchNorm1d(bn_channels)) self.nonlinear2.add_module('relu', nn.ReLU(inplace=True)) - + padding = (kernel_size - 1) // 2 * dilation self.cam_layer = CAMLayer(bn_channels, out_channels, kernel_size, stride, padding, dilation, bias) diff --git a/models/emb/cam++/coreml/test.py b/models/emb/cam++/coreml/test.py index 561c3e7..30977ab 100644 --- a/models/emb/cam++/coreml/test.py +++ b/models/emb/cam++/coreml/test.py @@ -1,4 +1,3 @@ -#!/usr/bin/env python3 """ Test script to validate CoreML CAM++ embeddings model against reference PyTorch implementation. 1. Takes an audio file as input @@ -21,6 +20,10 @@ warnings.filterwarnings('ignore') def extract_fbank_features(waveform, sample_rate=16000): + # In Python here just for testing/demonstration. + # For production deployment, see fbank_extractor C++ code in the Senko diarization pipeline: + # https://github.com/narcotic-sh/senko/tree/main/senko/fbank_extractor + # Ensure waveform is 2D (1, num_samples) if waveform.dim() == 1: waveform = waveform.unsqueeze(0) From cf295a54f4f146cb1db24e09562029c63347ce66 Mon Sep 17 00:00:00 2001 From: hamzaq2000 Date: Tue, 23 Sep 2025 12:06:57 -0700 Subject: [PATCH 4/4] add 3D-Speaker license text --- models/emb/cam++/THIRD_PARTY_LICENSES | 183 ++++++++++++++++++++++++++ 1 file changed, 183 insertions(+) create mode 100644 models/emb/cam++/THIRD_PARTY_LICENSES diff --git a/models/emb/cam++/THIRD_PARTY_LICENSES b/models/emb/cam++/THIRD_PARTY_LICENSES new file mode 100644 index 0000000..0af69d2 --- /dev/null +++ b/models/emb/cam++/THIRD_PARTY_LICENSES @@ -0,0 +1,183 @@ +================================================================================================ + THIRD-PARTY SOFTWARE LICENSES +================================================================================================ + +- 3D-Speaker + Copyright 3D-Speaker (https://github.com/modelscope/3D-Speaker). All Rights Reserved. + + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS \ No newline at end of file