Download clean/video/dfd_fcg/src/model/clip/evl.py from deepsafe/model-code: direct link, hf CLI and curl.
- Browser
- Download file 21 kB
-
https://huggingface.co/deepsafe/model-code/resolve/main/clean/video/dfd_fcg/src/model/clip/evl.py
- Command line
-
hf download hf://deepsafe/model-code/clean/video/dfd_fcg/src/model/clip/evl.py
-
curl -L -o evl.py https://huggingface.co/deepsafe/model-code/resolve/main/clean/video/dfd_fcg/src/model/clip/evl.py
21 kB
| import torch | |
| from torch import nn | |
| from collections import OrderedDict | |
| import numpy as np | |
| from typing import Tuple, List, Dict | |
| import torch | |
| import torch.nn as nn | |
| import torch.nn.functional as F | |
| ### | |
| from src.model.base import ODBinaryMetricClassifier | |
| from src.model.clip import VideoAttrExtractor | |
| from src.utility.loss import focal_loss | |
| ''' | |
| QuickGELU and LayerNorm w/ fp16 from official CLIP repo | |
| (https://github.com/openai/CLIP/blob/3b473b0e682c091a9e53623eebc1ca1657385717/clip/model.py) | |
| ''' | |
| class QuickGELU(nn.Module): | |
| def forward(self, x: torch.Tensor): | |
| return x * torch.sigmoid(1.702 * x) | |
| class LayerNorm(nn.LayerNorm): | |
| """Subclass torch's LayerNorm to handle fp16.""" | |
| def forward(self, x: torch.Tensor): | |
| orig_type = x.dtype | |
| ret = super().forward(x.type(torch.float32)) | |
| return ret.type(orig_type) | |
| class Attention(nn.Module): | |
| ''' | |
| A generalized attention module with more flexibility. | |
| ''' | |
| def __init__( | |
| self, q_in_dim: int, k_in_dim: int, v_in_dim: int, | |
| qk_proj_dim: int, v_proj_dim: int, num_heads: int, out_dim: int, | |
| return_all_features: bool = False, | |
| ): | |
| super().__init__() | |
| self.q_proj = nn.Linear(q_in_dim, qk_proj_dim) | |
| self.k_proj = nn.Linear(k_in_dim, qk_proj_dim) | |
| self.v_proj = nn.Linear(v_in_dim, v_proj_dim) | |
| self.out_proj = nn.Linear(v_proj_dim, out_dim) | |
| self.num_heads = num_heads | |
| self.return_all_features = return_all_features | |
| assert qk_proj_dim % num_heads == 0 and v_proj_dim % num_heads == 0 | |
| self._initialize_weights() | |
| def _initialize_weights(self): | |
| for m in (self.q_proj, self.k_proj, self.v_proj, self.out_proj): | |
| nn.init.xavier_uniform_(m.weight) | |
| nn.init.constant_(m.bias, 0.) | |
| def forward(self, q: torch.Tensor, k: torch.Tensor, v: torch.Tensor): | |
| assert q.ndim == 3 and k.ndim == 3 and v.ndim == 3 | |
| N = q.size(0) | |
| assert k.size(0) == N and v.size(0) == N | |
| Lq, Lkv = q.size(1), k.size(1) | |
| assert v.size(1) == Lkv | |
| q, k, v = self.q_proj(q), self.k_proj(k), self.v_proj(v) | |
| H = self.num_heads | |
| Cqk, Cv = q.size(-1) // H, v.size(-1) // H | |
| q = q.view(N, Lq, H, Cqk) | |
| k = k.view(N, Lkv, H, Cqk) | |
| v = v.view(N, Lkv, H, Cv) | |
| aff = torch.einsum('nqhc,nkhc->nqkh', q / (Cqk ** 0.5), k) | |
| aff = aff.softmax(dim=-2) | |
| mix = torch.einsum('nqlh,nlhc->nqhc', aff, v) | |
| out = self.out_proj(mix.flatten(-2)) | |
| if self.return_all_features: | |
| return dict(q=q, k=k, v=v, aff=aff, out=out) | |
| else: | |
| return out | |
| class PatchEmbed2D(nn.Module): | |
| def __init__( | |
| self, | |
| patch_size: Tuple[int, int] = (16, 16), | |
| in_channels: int = 3, | |
| embed_dim: int = 768, | |
| ): | |
| super().__init__() | |
| self.patch_size = patch_size | |
| self.in_channels = in_channels | |
| self.proj = nn.Linear(np.prod(patch_size) * in_channels, embed_dim) | |
| def _initialize_weights(self, x): | |
| nn.init.kaiming_normal_(self.proj.weight, 0.) | |
| nn.init.constant_(self.proj.bias, 0.) | |
| def forward(self, x: torch.Tensor): | |
| B, C, H, W = x.size() | |
| pH, pW = self.patch_size | |
| assert C == self.in_channels and H % pH == 0 and W % pW == 0 | |
| x = x.view(B, C, H // pH, pH, W // pW, pW).permute(0, 2, 4, 1, 3, 5).flatten(3).flatten(1, 2) | |
| x = self.proj(x) | |
| return x | |
| class TransformerEncoderLayer(nn.Module): | |
| def __init__( | |
| self, | |
| in_feature_dim: int = 768, | |
| qkv_dim: int = 768, | |
| num_heads: int = 12, | |
| mlp_factor: float = 4.0, | |
| mlp_dropout: float = 0.0, | |
| act: nn.Module = QuickGELU, | |
| return_all_features: bool = False, | |
| ): | |
| super().__init__() | |
| self.return_all_features = return_all_features | |
| self.attn = Attention( | |
| q_in_dim=in_feature_dim, k_in_dim=in_feature_dim, v_in_dim=in_feature_dim, | |
| qk_proj_dim=qkv_dim, v_proj_dim=qkv_dim, num_heads=num_heads, out_dim=in_feature_dim, | |
| return_all_features=return_all_features, | |
| ) | |
| mlp_dim = round(mlp_factor * in_feature_dim) | |
| self.mlp = nn.Sequential(OrderedDict([ | |
| ('fc1', nn.Linear(in_feature_dim, mlp_dim)), | |
| ('act', act()), | |
| ('dropout', nn.Dropout(mlp_dropout)), | |
| ('fc2', nn.Linear(mlp_dim, in_feature_dim)), | |
| ])) | |
| self.norm1 = LayerNorm(in_feature_dim) | |
| self.norm2 = LayerNorm(in_feature_dim) | |
| self._initialize_weights() | |
| def _initialize_weights(self): | |
| for m in (self.mlp[0], self.mlp[-1]): | |
| nn.init.xavier_uniform_(m.weight) | |
| nn.init.normal_(m.bias, std=1e-6) | |
| def forward(self, x: torch.Tensor): | |
| if self.return_all_features: | |
| ret_dict = {} | |
| x_norm = self.norm1(x) | |
| attn_out = self.attn(x_norm, x_norm, x_norm) | |
| ret_dict['q'] = attn_out['q'] | |
| ret_dict['k'] = attn_out['k'] | |
| ret_dict['v'] = attn_out['v'] | |
| ret_dict['attn_out'] = attn_out['out'] | |
| x = x + attn_out['out'] | |
| x = x + self.mlp(self.norm2(x)) | |
| ret_dict['out'] = x | |
| return ret_dict | |
| else: | |
| x_norm = self.norm1(x) | |
| x = x + self.attn(x_norm, x_norm, x_norm) | |
| x = x + self.mlp(self.norm2(x)) | |
| return x | |
| class TransformerDecoderLayer(nn.Module): | |
| def __init__( | |
| self, | |
| in_feature_dim: int = 768, | |
| qkv_dim: int = 768, | |
| num_heads: int = 12, | |
| mlp_factor: float = 4.0, | |
| mlp_dropout: float = 0.0, | |
| act: nn.Module = QuickGELU, | |
| ): | |
| super().__init__() | |
| self.attn = Attention( | |
| q_in_dim=in_feature_dim, k_in_dim=in_feature_dim, v_in_dim=in_feature_dim, | |
| qk_proj_dim=qkv_dim, v_proj_dim=qkv_dim, num_heads=num_heads, out_dim=in_feature_dim, | |
| ) | |
| mlp_dim = round(mlp_factor * in_feature_dim) | |
| self.mlp = nn.Sequential(OrderedDict([ | |
| ('fc1', nn.Linear(in_feature_dim, mlp_dim)), | |
| ('act', act()), | |
| ('dropout', nn.Dropout(mlp_dropout)), | |
| ('fc2', nn.Linear(mlp_dim, in_feature_dim)), | |
| ])) | |
| self.norm1 = LayerNorm(in_feature_dim) | |
| self.norm2 = LayerNorm(in_feature_dim) | |
| self.norm3 = LayerNorm(in_feature_dim) | |
| self._initialize_weights() | |
| def _initialize_weights(self): | |
| for m in (self.mlp[0], self.mlp[-1]): | |
| nn.init.xavier_uniform_(m.weight) | |
| nn.init.normal_(m.bias, std=1e-6) | |
| def forward(self, x: torch.Tensor, y: torch.Tensor): | |
| y_norm = self.norm3(y) | |
| x = x + self.attn(self.norm1(x), y_norm, y_norm) | |
| x = x + self.mlp(self.norm2(x)) | |
| return x | |
| class VisionTransformer2D(nn.Module): | |
| def __init__( | |
| self, | |
| feature_dim: int = 768, | |
| input_size: Tuple[int, int] = (224, 224), | |
| patch_size: Tuple[int, int] = (16, 16), | |
| num_heads: int = 12, | |
| num_layers: int = 12, | |
| mlp_factor: float = 4.0, | |
| act: nn.Module = QuickGELU, | |
| return_all_features: bool = False, | |
| ln_pre: bool = False, | |
| ): | |
| super().__init__() | |
| self.return_all_features = return_all_features | |
| self.patch_embed = PatchEmbed2D(patch_size=patch_size, embed_dim=feature_dim) | |
| self.num_patches = np.prod([x // y for x, y in zip(input_size, patch_size)]) + 1 | |
| self.cls_token = nn.Parameter(torch.zeros([feature_dim])) | |
| self.pos_embed = nn.Parameter(torch.zeros([self.num_patches, feature_dim])) | |
| self.blocks = nn.ModuleList([ | |
| TransformerEncoderLayer( | |
| in_feature_dim=feature_dim, qkv_dim=feature_dim, num_heads=num_heads, mlp_factor=mlp_factor, act=act, | |
| return_all_features=return_all_features, | |
| ) for _ in range(num_layers) | |
| ]) | |
| if ln_pre: | |
| self.ln_pre = LayerNorm(feature_dim) | |
| else: | |
| self.ln_pre = nn.Identity() | |
| self._initialize_weights() | |
| def _initialize_weights(self): | |
| nn.init.normal_(self.cls_token, std=0.02) | |
| nn.init.normal_(self.pos_embed, std=0.02) | |
| def forward(self, x: torch.Tensor): | |
| dtype = self.patch_embed.proj.weight.dtype | |
| x = x.to(dtype) | |
| x = self.patch_embed(x) | |
| x = torch.cat([self.cls_token.view(1, 1, -1).repeat(x.size(0), 1, 1), x], dim=1) | |
| x = x + self.pos_embed | |
| x = self.ln_pre(x) | |
| if self.return_all_features: | |
| all_features = [] | |
| for blk in self.blocks: | |
| x = blk(x) | |
| all_features.append(x) | |
| x = x['out'] | |
| return all_features | |
| else: | |
| for blk in self.blocks: | |
| x = blk(x) | |
| return x | |
| vit_presets = { | |
| 'ViT-B/16-lnpre': dict( | |
| feature_dim=768, | |
| input_size=(224, 224), | |
| patch_size=(16, 16), | |
| num_heads=12, | |
| num_layers=12, | |
| mlp_factor=4.0, | |
| ln_pre=True, | |
| ), | |
| 'ViT-L/14-lnpre': dict( | |
| feature_dim=1024, | |
| input_size=(224, 224), | |
| patch_size=(14, 14), | |
| num_heads=16, | |
| num_layers=24, | |
| mlp_factor=4.0, | |
| ln_pre=True, | |
| ), | |
| } | |
| class TransformerDecoderLayer(nn.Module): | |
| def __init__( | |
| self, | |
| in_feature_dim: int = 768, | |
| qkv_dim: int = 768, | |
| num_heads: int = 12, | |
| mlp_factor: float = 4.0, | |
| mlp_dropout: float = 0.0, | |
| act: nn.Module = QuickGELU, | |
| ): | |
| super().__init__() | |
| self.attn = Attention( | |
| q_in_dim=in_feature_dim, k_in_dim=in_feature_dim, v_in_dim=in_feature_dim, | |
| qk_proj_dim=qkv_dim, v_proj_dim=qkv_dim, num_heads=num_heads, out_dim=in_feature_dim, | |
| ) | |
| mlp_dim = round(mlp_factor * in_feature_dim) | |
| self.mlp = nn.Sequential(OrderedDict([ | |
| ('fc1', nn.Linear(in_feature_dim, mlp_dim)), | |
| ('act', act()), | |
| ('dropout', nn.Dropout(mlp_dropout)), | |
| ('fc2', nn.Linear(mlp_dim, in_feature_dim)), | |
| ])) | |
| self.norm1 = LayerNorm(in_feature_dim) | |
| self.norm2 = LayerNorm(in_feature_dim) | |
| self.norm3 = LayerNorm(in_feature_dim) | |
| self._initialize_weights() | |
| def _initialize_weights(self): | |
| for m in (self.mlp[0], self.mlp[-1]): | |
| nn.init.xavier_uniform_(m.weight) | |
| nn.init.normal_(m.bias, std=1e-6) | |
| def forward(self, x: torch.Tensor, y: torch.Tensor): | |
| y_norm = self.norm3(y) | |
| x = x + self.attn(self.norm1(x), y_norm, y_norm) | |
| x = x + self.mlp(self.norm2(x)) | |
| return x | |
| class TemporalCrossAttention(nn.Module): | |
| def __init__( | |
| self, | |
| spatial_size: Tuple[int, int] = (14, 14), | |
| feature_dim: int = 768, | |
| ): | |
| super().__init__() | |
| self.spatial_size = spatial_size | |
| w_size = np.prod([x * 2 - 1 for x in spatial_size]) | |
| self.w1 = nn.Parameter(torch.zeros([w_size, feature_dim])) | |
| self.w2 = nn.Parameter(torch.zeros([w_size, feature_dim])) | |
| idx_tensor = torch.zeros([np.prod(spatial_size) for _ in (0, 1)], dtype=torch.long) | |
| for q in range(np.prod(spatial_size)): | |
| qi, qj = q // spatial_size[1], q % spatial_size[1] | |
| for k in range(np.prod(spatial_size)): | |
| ki, kj = k // spatial_size[1], k % spatial_size[1] | |
| i_offs = qi - ki + spatial_size[0] - 1 | |
| j_offs = qj - kj + spatial_size[1] - 1 | |
| idx_tensor[q, k] = i_offs * (spatial_size[1] * 2 - 1) + j_offs | |
| self.idx_tensor = idx_tensor | |
| def forward_half(self, q: torch.Tensor, k: torch.Tensor, w: torch.Tensor) -> torch.Tensor: | |
| q, k = q[:, :, 1:], k[:, :, 1:] # remove cls token | |
| assert q.size() == k.size() | |
| assert q.size(2) == np.prod(self.spatial_size) | |
| attn = torch.einsum('ntqhd,ntkhd->ntqkh', q / (q.size(-1) ** 0.5), k) | |
| attn = attn.softmax(dim=-2).mean(dim=-1) # L, L, N, T | |
| self.idx_tensor = self.idx_tensor.to(w.device) | |
| w_unroll = w[self.idx_tensor] # L, L, C | |
| ret = torch.einsum('ntqk,qkc->ntqc', attn, w_unroll) | |
| return ret | |
| def forward(self, q: torch.Tensor, k: torch.Tensor): | |
| N, T, L, H, D = q.size() | |
| assert L == np.prod(self.spatial_size) + 1 | |
| ret = torch.zeros([N, T, L, self.w1.size(-1)], device='cuda') | |
| ret[:, 1:, 1:, :] += self.forward_half(q[:, 1:, :, :, :], k[:, :-1, :, :, :], self.w1) | |
| ret[:, :-1, 1:, :] += self.forward_half(q[:, :-1, :, :, :], k[:, 1:, :, :, :], self.w2) | |
| return ret | |
| class EVLDecoder(nn.Module): | |
| def __init__( | |
| self, | |
| num_frames: int = 8, | |
| spatial_size: Tuple[int, int] = (14, 14), | |
| num_layers: int = 4, | |
| in_feature_dim: int = 768, | |
| qkv_dim: int = 768, | |
| num_heads: int = 12, | |
| mlp_factor: float = 4.0, | |
| enable_temporal_conv: bool = True, | |
| enable_temporal_pos_embed: bool = True, | |
| enable_temporal_cross_attention: bool = True, | |
| mlp_dropout: float = 0.5, | |
| ): | |
| super().__init__() | |
| self.enable_temporal_conv = enable_temporal_conv | |
| self.enable_temporal_pos_embed = enable_temporal_pos_embed | |
| self.enable_temporal_cross_attention = enable_temporal_cross_attention | |
| self.num_layers = num_layers | |
| self.decoder_layers = nn.ModuleList( | |
| [TransformerDecoderLayer(in_feature_dim, qkv_dim, num_heads, mlp_factor, mlp_dropout) | |
| for _ in range(num_layers)] | |
| ) | |
| if enable_temporal_conv: | |
| self.temporal_conv = nn.ModuleList( | |
| [nn.Conv1d(in_feature_dim, in_feature_dim, kernel_size=3, stride=1, | |
| padding=1, groups=in_feature_dim) for _ in range(num_layers)] | |
| ) | |
| if enable_temporal_pos_embed: | |
| self.temporal_pos_embed = nn.ParameterList( | |
| [nn.Parameter(torch.zeros([num_frames, in_feature_dim])) for _ in range(num_layers)] | |
| ) | |
| if enable_temporal_cross_attention: | |
| self.cross_attention = nn.ModuleList( | |
| [TemporalCrossAttention(spatial_size, in_feature_dim) for _ in range(num_layers)] | |
| ) | |
| self.cls_token = nn.Parameter(torch.zeros([in_feature_dim])) | |
| def _initialize_weights(self): | |
| nn.init.normal_(self.cls_token, std=0.02) | |
| def forward(self, in_features: List[Dict[str, torch.Tensor]]): | |
| N, T, L, C = in_features[0]['out'].size() | |
| assert len(in_features) == self.num_layers | |
| x = self.cls_token.view(1, 1, -1).repeat(N, 1, 1) | |
| for i in range(self.num_layers): | |
| frame_features = in_features[i]['out'] | |
| if self.enable_temporal_conv: | |
| feat = in_features[i]['out'] | |
| feat = feat.permute(0, 2, 3, 1).contiguous().flatten(0, 1) # N * L, C, T | |
| feat = self.temporal_conv[i](feat) | |
| feat = feat.view(N, L, C, T).permute(0, 3, 1, 2).contiguous() # N, T, L, C | |
| frame_features += feat | |
| if self.enable_temporal_pos_embed: | |
| frame_features += self.temporal_pos_embed[i].view(1, T, 1, C) | |
| if self.enable_temporal_cross_attention: | |
| frame_features += self.cross_attention[i](in_features[i]['q'], in_features[i]['k']) | |
| frame_features = frame_features.flatten(1, 2) # N, T * L, C | |
| x = self.decoder_layers[i](x, frame_features) | |
| return x.squeeze(1) | |
| class EVLVideoAttrExtractor(VideoAttrExtractor): | |
| def __init__( | |
| self, | |
| architecture, | |
| num_frames=1, | |
| decoder_layers=4, | |
| attn_record=False | |
| ): | |
| super(EVLVideoAttrExtractor, self).__init__( | |
| architecture=architecture, | |
| text_embed=False, | |
| store_attrs=["q", "k", "out"], | |
| attn_record=attn_record | |
| ) | |
| self.decoder = EVLDecoder( | |
| num_frames=num_frames, | |
| spatial_size=(self.n_patch, self.n_patch), | |
| num_layers=decoder_layers, | |
| in_feature_dim=self.embed_dim, | |
| qkv_dim=self.embed_dim, | |
| num_heads=self.n_heads, | |
| mlp_factor=4.0, | |
| enable_temporal_conv=True, | |
| enable_temporal_pos_embed=True, | |
| enable_temporal_cross_attention=True, | |
| mlp_dropout=0.5 | |
| ) | |
| self.decoder_layers = decoder_layers | |
| def forward(self, x): | |
| results = super(EVLVideoAttrExtractor, self).forward(x) | |
| # remove cls_token attrs | |
| # layer_attrs = results["layer_attrs"] | |
| # for i in range(len(layer_attrs)): | |
| # for k in layer_attrs[i]: | |
| # layer_attrs[i][k] = layer_attrs[i][k][:, :, 1:] | |
| reps = self.decoder(results["layer_attrs"][-self.decoder_layers:]) | |
| results["reps"] = reps | |
| return results | |
| def train(self, mode=True): | |
| super().train(mode) | |
| if (mode): | |
| self.model.eval() | |
| return self | |
| class BinaryLinearClassifier(nn.Module): | |
| def __init__( | |
| self, | |
| *args, | |
| **kargs, | |
| ): | |
| super().__init__() | |
| self.encoder = EVLVideoAttrExtractor( | |
| *args, | |
| **kargs | |
| ) | |
| self.head = self.make_linear(self.encoder.embed_dim) | |
| def make_linear(self, embed_dim): | |
| linear = nn.Linear( | |
| embed_dim, | |
| 2 | |
| ) | |
| return nn.Sequential( | |
| nn.LayerNorm(embed_dim), | |
| linear | |
| ) | |
| def transform(self): | |
| return self.encoder.transform | |
| def n_px(self): | |
| return self.encoder.model.input_resolution | |
| def forward(self, x, *args, **kargs): | |
| results = self.encoder(x) | |
| logits = self.head(results["reps"]) | |
| return dict( | |
| logits=logits, | |
| ** results | |
| ) | |
| class EfficientVideoLearner(ODBinaryMetricClassifier): | |
| def __init__( | |
| self, | |
| architecture: str = 'ViT-B/16', | |
| num_frames: int = 1, | |
| attn_record: bool = False, | |
| decoder_layers: int = 4, | |
| is_focal_loss: bool = True, | |
| cls_weight: float = 10.0, | |
| label_weights: List[float] = [1, 1], | |
| ): | |
| super().__init__() | |
| self.save_hyperparameters() | |
| params = dict( | |
| architecture=architecture, | |
| attn_record=attn_record, | |
| num_frames=num_frames, | |
| decoder_layers=decoder_layers | |
| ) | |
| self.model = BinaryLinearClassifier(**params) | |
| self.label_weights = torch.tensor(label_weights) | |
| self.cls_weight = cls_weight | |
| self.is_focal_loss = is_focal_loss | |
| def transform(self): | |
| return self.model.transform | |
| def n_px(self): | |
| return self.model.n_px | |
| def shared_step(self, batch, stage): | |
| x, y, z = batch["xyz"] | |
| indices = batch["indices"] | |
| dts_name = batch["dts_name"] | |
| names = batch["names"] | |
| output = self(x, **z) | |
| logits = output["logits"] | |
| loss = 0 | |
| # classification loss | |
| if (stage == "train"): | |
| if self.is_focal_loss: | |
| cls_loss = focal_loss( | |
| logits, | |
| y, | |
| gamma=4, | |
| weight=( | |
| self.label_weights.to(y.device) | |
| if stage == "train" else | |
| None | |
| ) | |
| ) | |
| else: | |
| cls_loss = nn.functional.cross_entropy( | |
| logits, | |
| y, | |
| reduction="none", | |
| weight=( | |
| self.label_weights.to(y.device) | |
| if stage == "train" else | |
| None | |
| ) | |
| ) | |
| loss += cls_loss.mean() * self.cls_weight | |
| self.log( | |
| f"{stage}/{dts_name}/loss", | |
| cls_loss.mean(), | |
| batch_size=logits.shape[0] | |
| ) | |
| else: | |
| # classification loss | |
| cls_loss = nn.functional.cross_entropy( | |
| logits, | |
| y, | |
| reduction="none", | |
| weight=None | |
| ) | |
| loss += cls_loss.mean() | |
| return { | |
| "logits": logits, | |
| "labels": y, | |
| "loss": loss, | |
| "dts_name": dts_name, | |
| "indices": indices, | |
| "output": output | |
| } | |
| if __name__ == "__main__": | |
| frames = 5 | |
| model = EfficientVideoLearner(num_frames=frames, attn_record=True) | |
| model.to("cuda") | |
| logit = model(torch.randn(9, frames, 3, 224, 224).to("cuda"))["logits"] | |
| logit.sum().backward() | |
| print([k for k, v in model.named_parameters() if v.requires_grad]) | |
| print("done") | |