2512
Browse files- media/result_grid.jpg +2 -2
- pipeline_sdxs.py +4 -4
- samples/unet_320x640_0.jpg +2 -2
- samples/unet_384x640_0.jpg +2 -2
- samples/unet_448x640_0.jpg +2 -2
- samples/unet_512x640_0.jpg +2 -2
- samples/unet_576x640_0.jpg +2 -2
- samples/unet_640x320_0.jpg +2 -2
- samples/unet_640x384_0.jpg +2 -2
- samples/unet_640x448_0.jpg +2 -2
- samples/unet_640x512_0.jpg +2 -2
- samples/unet_640x576_0.jpg +2 -2
- samples/unet_640x640_0.jpg +2 -2
- src/conv2048.py +97 -0
- src/sdxs_create.ipynb +2 -2
- test.ipynb +2 -2
- text_encoder/config.json +2 -2
- text_encoder/model.safetensors +2 -2
- train.py +12 -12
- unet/config.json +2 -2
- unet/diffusion_pytorch_model.safetensors +2 -2
- text_encoder/generation_config.json → unet_old/config.json +2 -2
- unet_old/diffusion_pytorch_model.safetensors +3 -0
media/result_grid.jpg
CHANGED
|
Git LFS Details
|
|
Git LFS Details
|
pipeline_sdxs.py
CHANGED
|
@@ -12,7 +12,7 @@ class SdxsPipelineOutput(BaseOutput):
|
|
| 12 |
images: Union[List[Image.Image], np.ndarray]
|
| 13 |
|
| 14 |
class SdxsPipeline(DiffusionPipeline):
|
| 15 |
-
def __init__(self, vae, text_encoder, tokenizer, unet, scheduler, max_length: int =
|
| 16 |
super().__init__()
|
| 17 |
self.register_modules(
|
| 18 |
vae=vae, text_encoder=text_encoder, tokenizer=tokenizer,
|
|
@@ -33,7 +33,7 @@ class SdxsPipeline(DiffusionPipeline):
|
|
| 33 |
|
| 34 |
# Если промпты не заданы, используем пустые эмбеддинги
|
| 35 |
if prompt is None and negative_prompt is None:
|
| 36 |
-
hidden_dim = 1024 # Размерность эмбеддинга
|
| 37 |
seq_len = self.max_length
|
| 38 |
batch_size = 1
|
| 39 |
# ИЗМЕНЕНО: Возвращаем три элемента: embeds, mask, pooled
|
|
@@ -42,7 +42,7 @@ class SdxsPipeline(DiffusionPipeline):
|
|
| 42 |
empty_pooled = torch.zeros((batch_size, hidden_dim), dtype=dtype, device=device)
|
| 43 |
return empty_embeds, empty_mask, empty_pooled
|
| 44 |
|
| 45 |
-
# Токенизация с фиксированным max_length
|
| 46 |
def encode_texts(texts, max_length=self.max_length):
|
| 47 |
with torch.no_grad():
|
| 48 |
if isinstance(texts, str):
|
|
@@ -70,7 +70,7 @@ class SdxsPipeline(DiffusionPipeline):
|
|
| 70 |
outs = self.text_encoder(**toks, output_hidden_states=True, return_dict=True)
|
| 71 |
|
| 72 |
# Токен-эмбеддинги (для Cross-Attention)
|
| 73 |
-
hidden = outs.hidden_states[-
|
| 74 |
# Маска внимания (для Cross-Attention)
|
| 75 |
attention_mask = toks["attention_mask"]
|
| 76 |
|
|
|
|
| 12 |
images: Union[List[Image.Image], np.ndarray]
|
| 13 |
|
| 14 |
class SdxsPipeline(DiffusionPipeline):
|
| 15 |
+
def __init__(self, vae, text_encoder, tokenizer, unet, scheduler, max_length: int = 192):
|
| 16 |
super().__init__()
|
| 17 |
self.register_modules(
|
| 18 |
vae=vae, text_encoder=text_encoder, tokenizer=tokenizer,
|
|
|
|
| 33 |
|
| 34 |
# Если промпты не заданы, используем пустые эмбеддинги
|
| 35 |
if prompt is None and negative_prompt is None:
|
| 36 |
+
hidden_dim = 1024 # Размерность эмбеддинга
|
| 37 |
seq_len = self.max_length
|
| 38 |
batch_size = 1
|
| 39 |
# ИЗМЕНЕНО: Возвращаем три элемента: embeds, mask, pooled
|
|
|
|
| 42 |
empty_pooled = torch.zeros((batch_size, hidden_dim), dtype=dtype, device=device)
|
| 43 |
return empty_embeds, empty_mask, empty_pooled
|
| 44 |
|
| 45 |
+
# Токенизация с фиксированным max_length и padding="max_length"
|
| 46 |
def encode_texts(texts, max_length=self.max_length):
|
| 47 |
with torch.no_grad():
|
| 48 |
if isinstance(texts, str):
|
|
|
|
| 70 |
outs = self.text_encoder(**toks, output_hidden_states=True, return_dict=True)
|
| 71 |
|
| 72 |
# Токен-эмбеддинги (для Cross-Attention)
|
| 73 |
+
hidden = outs.hidden_states[-2] # Используем last hidden state -2???
|
| 74 |
# Маска внимания (для Cross-Attention)
|
| 75 |
attention_mask = toks["attention_mask"]
|
| 76 |
|
samples/unet_320x640_0.jpg
CHANGED
|
Git LFS Details
|
|
Git LFS Details
|
samples/unet_384x640_0.jpg
CHANGED
|
Git LFS Details
|
|
Git LFS Details
|
samples/unet_448x640_0.jpg
CHANGED
|
Git LFS Details
|
|
Git LFS Details
|
samples/unet_512x640_0.jpg
CHANGED
|
Git LFS Details
|
|
Git LFS Details
|
samples/unet_576x640_0.jpg
CHANGED
|
Git LFS Details
|
|
Git LFS Details
|
samples/unet_640x320_0.jpg
CHANGED
|
Git LFS Details
|
|
Git LFS Details
|
samples/unet_640x384_0.jpg
CHANGED
|
Git LFS Details
|
|
Git LFS Details
|
samples/unet_640x448_0.jpg
CHANGED
|
Git LFS Details
|
|
Git LFS Details
|
samples/unet_640x512_0.jpg
CHANGED
|
Git LFS Details
|
|
Git LFS Details
|
samples/unet_640x576_0.jpg
CHANGED
|
Git LFS Details
|
|
Git LFS Details
|
samples/unet_640x640_0.jpg
CHANGED
|
Git LFS Details
|
|
Git LFS Details
|
src/conv2048.py
ADDED
|
@@ -0,0 +1,97 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import torch
|
| 2 |
+
from diffusers import UNet2DConditionModel
|
| 3 |
+
import os
|
| 4 |
+
from safetensors.torch import load_file as safe_load
|
| 5 |
+
|
| 6 |
+
# --- КОНСТАНТЫ ---
|
| 7 |
+
OLD_UNET_PATH = "/workspace/sdxs/unet_old"
|
| 8 |
+
NEW_UNET_PATH = "/workspace/sdxs/unet"
|
| 9 |
+
|
| 10 |
+
def transfer_unet_weights_fix(old_path: str, new_path: str):
|
| 11 |
+
|
| 12 |
+
print(f"Загрузка новой UNet (Конфиг 2048D) из: {new_path}")
|
| 13 |
+
new_unet = UNet2DConditionModel.from_pretrained(new_path, low_cpu_mem_usage=False)
|
| 14 |
+
|
| 15 |
+
# 2. Прямая загрузка state dict старой модели (с обработкой формата)
|
| 16 |
+
print(f"Прямая загрузка весов старой UNet из: {old_path}")
|
| 17 |
+
|
| 18 |
+
weights_file = next((f for f in os.listdir(old_path) if f.endswith(('.safetensors', '.bin'))), None)
|
| 19 |
+
if not weights_file:
|
| 20 |
+
print("Ошибка: не найден файл весов (.safetensors или .bin) в старой папке.")
|
| 21 |
+
return None
|
| 22 |
+
|
| 23 |
+
if weights_file.endswith('.safetensors'):
|
| 24 |
+
print(f"Обнаружен файл Safetensors ({weights_file}). Использую safe_load.")
|
| 25 |
+
old_state_dict = safe_load(f"{old_path}/{weights_file}")
|
| 26 |
+
elif weights_file.endswith('.bin'):
|
| 27 |
+
print(f"Обнаружен файл PyTorch (.bin) ({weights_file}). Использую torch.load с weights_only=False.")
|
| 28 |
+
old_state_dict = torch.load(
|
| 29 |
+
f"{old_path}/{weights_file}",
|
| 30 |
+
map_location='cpu',
|
| 31 |
+
weights_only=False
|
| 32 |
+
)
|
| 33 |
+
else:
|
| 34 |
+
print(f"Ошибка: Не удалось загрузить файл {weights_file}. Проверьте формат.")
|
| 35 |
+
return None
|
| 36 |
+
|
| 37 |
+
if "state_dict" in old_state_dict:
|
| 38 |
+
old_state_dict = old_state_dict["state_dict"]
|
| 39 |
+
|
| 40 |
+
# 3. Перенос весов с обработкой RuntimeError
|
| 41 |
+
print("Начало переноса весов с пропуском несовместимых слоев...")
|
| 42 |
+
|
| 43 |
+
# --- НОВОЕ: Предварительный подсчет совпадающих ключей ---
|
| 44 |
+
total_keys = len(old_state_dict)
|
| 45 |
+
matching_keys = 0
|
| 46 |
+
size_mismatch_keys = 0
|
| 47 |
+
|
| 48 |
+
# Используем ключи новой модели для сравнения, чтобы убедиться,
|
| 49 |
+
# что ключи, которые отсутствуют в старой, не учитываются в "перенесенных".
|
| 50 |
+
new_keys = new_unet.state_dict().keys()
|
| 51 |
+
|
| 52 |
+
for name, old_param in old_state_dict.items():
|
| 53 |
+
if name in new_keys:
|
| 54 |
+
new_param = new_unet.state_dict()[name]
|
| 55 |
+
# Считаем только те, где имя и размер совпали
|
| 56 |
+
if old_param.shape == new_param.shape:
|
| 57 |
+
matching_keys += 1
|
| 58 |
+
# Считаем те, где имя совпало, но размер изменился (mismatch)
|
| 59 |
+
else:
|
| 60 |
+
size_mismatch_keys += 1
|
| 61 |
+
# Ключи, которые есть в старой, но нет в новой (unexpected),
|
| 62 |
+
# также будут пропущены. Но нас в первую очередь интересуют size mismatch.
|
| 63 |
+
|
| 64 |
+
# Пытаемся загрузить веса, зная, что это вызовет RuntimeError
|
| 65 |
+
try:
|
| 66 |
+
# Запуск actual переноса. Ключи будут перенесены.
|
| 67 |
+
new_unet.load_state_dict(old_state_dict, strict=False)
|
| 68 |
+
|
| 69 |
+
except RuntimeError as e:
|
| 70 |
+
# Это ожидаемый блок! Он ловит RuntimeError, вызванный mismatch size.
|
| 71 |
+
# Веса, которые совпали, УЖЕ перенесены в new_unet.
|
| 72 |
+
|
| 73 |
+
print("\n--- Отчет о переносе весов ---")
|
| 74 |
+
print("⚠️ Обнаружен ожидаемый **RuntimeError** из-за несовпадения размеров. Это нормально!")
|
| 75 |
+
|
| 76 |
+
print(f"💡 **УСПЕШНО перенесенных ключей (совпадающий размер): {matching_keys} шт.**")
|
| 77 |
+
print(f"❌ **Пропущенных ключей (несовпадение размера): {size_mismatch_keys} шт.**")
|
| 78 |
+
|
| 79 |
+
# Мы всё равно сохраняем UNet, так как большая часть весов перенесена
|
| 80 |
+
new_unet.save_pretrained(new_path)
|
| 81 |
+
print(f"\n✅ Новая UNet (с перенесенными весами) сохранена по пути: {new_path}")
|
| 82 |
+
return new_unet
|
| 83 |
+
|
| 84 |
+
# Блок на случай, если чудом ошибки не возникло (для полноты)
|
| 85 |
+
print("\n--- Отчет о переносе весов ---")
|
| 86 |
+
print(f"✅ Успешно перенесены совпадающие веса (основная часть UNet).")
|
| 87 |
+
print(f"💡 **УСПЕШНО перенесенных ключей (совпадающий размер): {matching_keys} шт.**")
|
| 88 |
+
print(f"❌ **Пропущенных ключей (несовпадение размера): {size_mismatch_keys} шт.**")
|
| 89 |
+
#print(f"❌ Слои, требующие переобучения (измененный размер): {len(incompatible_keys.unexpected_keys)}")
|
| 90 |
+
|
| 91 |
+
new_unet.save_pretrained(new_path)
|
| 92 |
+
print(f"\n✅ Новая UNet сохранена по пути: {new_path}")
|
| 93 |
+
|
| 94 |
+
return new_unet
|
| 95 |
+
|
| 96 |
+
# --- ВЫПОЛНЕНИЕ ---
|
| 97 |
+
transferred_unet = transfer_unet_weights_fix(OLD_UNET_PATH, NEW_UNET_PATH)
|
src/sdxs_create.ipynb
CHANGED
|
@@ -1,3 +1,3 @@
|
|
| 1 |
version https://git-lfs.github.com/spec/v1
|
| 2 |
-
oid sha256:
|
| 3 |
-
size
|
|
|
|
| 1 |
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:d542ea503b79500cf1ee2bec8f9a82807d0520579664ab363b4611c1971620c9
|
| 3 |
+
size 8018
|
test.ipynb
CHANGED
|
@@ -1,3 +1,3 @@
|
|
| 1 |
version https://git-lfs.github.com/spec/v1
|
| 2 |
-
oid sha256:
|
| 3 |
-
size
|
|
|
|
| 1 |
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:70b3ec42fbbada7f67f0477e673da4b15da141fa481ff04f709680ca0eaf773f
|
| 3 |
+
size 4393987
|
text_encoder/config.json
CHANGED
|
@@ -1,3 +1,3 @@
|
|
| 1 |
version https://git-lfs.github.com/spec/v1
|
| 2 |
-
oid sha256:
|
| 3 |
-
size
|
|
|
|
| 1 |
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:b6026cadca04402d27495be77ff01ca4e1b418ea59047ad7079bf3bfd3517f11
|
| 3 |
+
size 1354
|
text_encoder/model.safetensors
CHANGED
|
@@ -1,3 +1,3 @@
|
|
| 1 |
version https://git-lfs.github.com/spec/v1
|
| 2 |
-
oid sha256:
|
| 3 |
-
size
|
|
|
|
| 1 |
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:2a86278726ecbda59caff65f49ec07c3ec5672b044ed523780990a8c08a87fd4
|
| 3 |
+
size 1192133232
|
train.py
CHANGED
|
@@ -26,18 +26,19 @@ from collections import deque
|
|
| 26 |
from transformers import AutoTokenizer, AutoModel
|
| 27 |
|
| 28 |
# --------------------------- Параметры ---------------------------
|
| 29 |
-
ds_path = "/workspace/sdxs/datasets/
|
| 30 |
project = "unet"
|
| 31 |
-
batch_size =
|
| 32 |
base_learning_rate = 3e-5
|
| 33 |
min_learning_rate = 2.5e-5
|
| 34 |
-
num_epochs =
|
| 35 |
-
sample_interval_share =
|
|
|
|
| 36 |
use_wandb = True
|
| 37 |
use_comet_ml = False
|
| 38 |
save_model = True
|
| 39 |
use_decay = True
|
| 40 |
-
fbp = False
|
| 41 |
optimizer_type = "adam8bit"
|
| 42 |
torch_compile = False
|
| 43 |
unet_gradient = True
|
|
@@ -50,14 +51,14 @@ torch.backends.cudnn.allow_tf32 = True
|
|
| 50 |
torch.backends.cuda.enable_mem_efficient_sdp(False)
|
| 51 |
dtype = torch.float32
|
| 52 |
save_barrier = 1.004
|
| 53 |
-
warmup_percent = 0.
|
| 54 |
percentile_clipping = 98
|
| 55 |
betta2 = 0.998
|
| 56 |
eps = 1e-6
|
| 57 |
clip_grad_norm = 1.0
|
| 58 |
limit = 0
|
| 59 |
checkpoints_folder = ""
|
| 60 |
-
mixed_precision = "
|
| 61 |
gradient_accumulation_steps = 1
|
| 62 |
|
| 63 |
accelerator = Accelerator(
|
|
@@ -124,7 +125,7 @@ tokenizer = AutoTokenizer.from_pretrained("tokenizer")
|
|
| 124 |
text_model = AutoModel.from_pretrained("text_encoder").to(device).eval()
|
| 125 |
|
| 126 |
# --- [UPDATED] Функция кодирования текста (с маской и пулингом) ---
|
| 127 |
-
def encode_texts(texts, max_length=
|
| 128 |
# Если тексты пустые (для unconditional), создаем заглушки
|
| 129 |
if texts is None:
|
| 130 |
# В случае None возвращаем нули (логика для get_negative_embedding)
|
|
@@ -158,9 +159,8 @@ def encode_texts(texts, max_length=150):
|
|
| 158 |
|
| 159 |
outs = text_model(**toks, output_hidden_states=True, return_dict=True)
|
| 160 |
|
| 161 |
-
#
|
| 162 |
-
|
| 163 |
-
hidden = outs.last_hidden_state
|
| 164 |
|
| 165 |
# 2. Маска внимания
|
| 166 |
attention_mask = toks["attention_mask"]
|
|
@@ -398,7 +398,7 @@ fixed_samples = get_fixed_samples_by_resolution(dataset)
|
|
| 398 |
def get_negative_embedding(neg_prompt="", batch_size=1):
|
| 399 |
if not neg_prompt:
|
| 400 |
hidden_dim = 1024
|
| 401 |
-
seq_len =
|
| 402 |
empty_emb = torch.zeros((batch_size, seq_len, hidden_dim), dtype=dtype, device=device)
|
| 403 |
empty_mask = torch.ones((batch_size, seq_len), dtype=torch.int64, device=device)
|
| 404 |
empty_pool = torch.zeros((batch_size, hidden_dim), dtype=dtype, device=device)
|
|
|
|
| 26 |
from transformers import AutoTokenizer, AutoModel
|
| 27 |
|
| 28 |
# --------------------------- Параметры ---------------------------
|
| 29 |
+
ds_path = "/workspace/sdxs/datasets/640"
|
| 30 |
project = "unet"
|
| 31 |
+
batch_size = 256
|
| 32 |
base_learning_rate = 3e-5
|
| 33 |
min_learning_rate = 2.5e-5
|
| 34 |
+
num_epochs = 10
|
| 35 |
+
sample_interval_share = 20
|
| 36 |
+
max_length = 192
|
| 37 |
use_wandb = True
|
| 38 |
use_comet_ml = False
|
| 39 |
save_model = True
|
| 40 |
use_decay = True
|
| 41 |
+
fbp = False
|
| 42 |
optimizer_type = "adam8bit"
|
| 43 |
torch_compile = False
|
| 44 |
unet_gradient = True
|
|
|
|
| 51 |
torch.backends.cuda.enable_mem_efficient_sdp(False)
|
| 52 |
dtype = torch.float32
|
| 53 |
save_barrier = 1.004
|
| 54 |
+
warmup_percent = 0.01
|
| 55 |
percentile_clipping = 98
|
| 56 |
betta2 = 0.998
|
| 57 |
eps = 1e-6
|
| 58 |
clip_grad_norm = 1.0
|
| 59 |
limit = 0
|
| 60 |
checkpoints_folder = ""
|
| 61 |
+
mixed_precision = "bf16"
|
| 62 |
gradient_accumulation_steps = 1
|
| 63 |
|
| 64 |
accelerator = Accelerator(
|
|
|
|
| 125 |
text_model = AutoModel.from_pretrained("text_encoder").to(device).eval()
|
| 126 |
|
| 127 |
# --- [UPDATED] Функция кодирования текста (с маской и пулингом) ---
|
| 128 |
+
def encode_texts(texts, max_length=max_length):
|
| 129 |
# Если тексты пустые (для unconditional), создаем заглушки
|
| 130 |
if texts is None:
|
| 131 |
# В случае None возвращаем нули (логика для get_negative_embedding)
|
|
|
|
| 159 |
|
| 160 |
outs = text_model(**toks, output_hidden_states=True, return_dict=True)
|
| 161 |
|
| 162 |
+
# Используем last_hidden_state или hidden_states[-1] (если Qwen, лучше last_hidden_state - прим человека: ХУЙ)
|
| 163 |
+
hidden = outs.hidden_states[-2]
|
|
|
|
| 164 |
|
| 165 |
# 2. Маска внимания
|
| 166 |
attention_mask = toks["attention_mask"]
|
|
|
|
| 398 |
def get_negative_embedding(neg_prompt="", batch_size=1):
|
| 399 |
if not neg_prompt:
|
| 400 |
hidden_dim = 1024
|
| 401 |
+
seq_len = max_length
|
| 402 |
empty_emb = torch.zeros((batch_size, seq_len, hidden_dim), dtype=dtype, device=device)
|
| 403 |
empty_mask = torch.ones((batch_size, seq_len), dtype=torch.int64, device=device)
|
| 404 |
empty_pool = torch.zeros((batch_size, hidden_dim), dtype=dtype, device=device)
|
unet/config.json
CHANGED
|
@@ -1,3 +1,3 @@
|
|
| 1 |
version https://git-lfs.github.com/spec/v1
|
| 2 |
-
oid sha256:
|
| 3 |
-
size
|
|
|
|
| 1 |
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:ae4272f7e52480762c228a2fbe1db5f361d7a5971c3855b483999fb3df2d722b
|
| 3 |
+
size 1885
|
unet/diffusion_pytorch_model.safetensors
CHANGED
|
@@ -1,3 +1,3 @@
|
|
| 1 |
version https://git-lfs.github.com/spec/v1
|
| 2 |
-
oid sha256:
|
| 3 |
-
size
|
|
|
|
| 1 |
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:ccfa16a08b6e507835636c048b8c721186ef6832fec3ca889a7139dcd53676cf
|
| 3 |
+
size 6625750656
|
text_encoder/generation_config.json → unet_old/config.json
RENAMED
|
@@ -1,3 +1,3 @@
|
|
| 1 |
version https://git-lfs.github.com/spec/v1
|
| 2 |
-
oid sha256:
|
| 3 |
-
size
|
|
|
|
| 1 |
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:9437f6d40639f1ad6d95d6586009d7b1bf0c8e99959d29da5c9c9645cae39ea3
|
| 3 |
+
size 1899
|
unet_old/diffusion_pytorch_model.safetensors
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:22402fe3c630ab551173c2bd11af4a7ae4ed76c04f001535f3791cd82da93719
|
| 3 |
+
size 3103078992
|