Преодоление ограничений размера токена, загрузка пользовательской модели, поддержка LoRa, поддержка текстовой инверсии и многое другое.

Stable Diffusion WebUI от AUTOMATIC1111 зарекомендовал себя как мощный инструмент для создания высококачественных изображений с использованием модели Diffusion. Однако, хотя WebUI прост в использовании, специалистам по данным, инженерам по машинному обучению и исследователям часто требуется больший контроль над процессом создания изображений. Именно здесь появляется пакет diffusers от Huggingface, предоставляющий способ запуска модели Diffusion в Python и позволяющий пользователям настраивать свои модели и подсказки для создания изображений в соответствии с их конкретными потребностями.

Несмотря на свой потенциал, пакет Diffusers имеет несколько ограничений, которые не позволяют ему генерировать изображения так же хорошо, как изображения, создаваемые веб-интерфейсом Stable Diffusion. К наиболее существенным из этих ограничений относятся:

  • Невозможность использования пользовательских моделей в формате файла .safetensor;
  • Ограничение 77 токенов подсказки;
  • Отсутствие поддержки LoRA;
  • И отсутствие функции масштабирования изображения (также известной как HighRes в Stable Diffusion WebUI);
  • Низкая производительность и высокое использование VRAM по умолчанию.

Эта статья призвана устранить эти ограничения и позволить пакету Diffusers генерировать высококачественные изображения, сравнимые с изображениями, создаваемыми веб-интерфейсом Stable Diffusion. Благодаря предоставленным усовершенствованным решениям ученые, работающие с данными, инженеры по машинному обучению и исследователи могут получить больше контроля и гибкости в своих процессах создания изображений, а также достичь исключительных результатов. В следующих разделах мы рассмотрим различные стратегии и методы, которые можно использовать для преодоления этих ограничений и раскрытия полного потенциала пакета Diffusers.

Обратите внимание: перейдите по этой ссылке, чтобы установить все необходимые пакеты CUDA и Python, если вы впервые запускаете Stable Diffusion.



1. Загрузите файлы локальной модели в формате .safetensor.

Пользователи могут легко раскручивать диффузоры для создания такого изображения:

from diffusers import DiffusionPipeline
pipeline = DiffusionPipeline.from_pretrained("runwayml/stable-diffusion-v1-5")
pipeline.to("cuda")
image = pipeline("A cute cat playing piano").images[0]
image.save("image_of_cat_playing_piano.png")

Вас может не удовлетворить ни выходное изображение, ни производительность. Давайте разберемся с проблемами одну за другой. Во-первых, давайте загрузим пользовательскую модель в формате .safetensor, расположенную в любом месте на вашем компьютере. вы не можете просто загрузить файл модели следующим образом:

pipeline = DiffusionPipeline.from_pretrained("/model/custom_model.safetensors")

Вот подробные шаги для преобразования файла .safetensor в формат диффузоров:

Шаг 1. Вытяните код всех диффузоров с GitHub.

git clone https://github.com/huggingface/diffusers.git

Шаг 2. В папке scripts найдите файл: convert_original_stable_diffusion_to_diffusers.py

В терминале запустите эту команду, чтобы преобразовать файл .safetensor в формат Diffusers. Не забудьте изменить значение — checkpoint_path в соответствии с вашим случаем.

python convert_original_stable_diffusion_to_diffusers.py --from_safetensors --checkpoint_path="D:\stable-diffusion-webui\models\Stable-diffusion\deliberate_v2.safetensors" --dump_path='D:\sd_models\deliberate_v2' --device='cuda:0'

Шаг 3. Теперь вы можете загрузить конвейер, используя только что преобразованный файл модели, вот полный код:

from diffusers import DiffusionPipeline
pipeline = DiffusionPipeline.from_pretrained(
    r"D:\sd_models\deliberate_v2"
)
pipeline.to("cuda")
image = pipeline("A cute cat playing piano").images[0]
image.save("image_of_cat_playing_piano.png")

У вас должна быть возможность конвертировать и использовать любые модели, которые вы загружаете с Huggingface или civitai.com.

2. Повысьте производительность диффузоров

Создание высококачественных изображений может занять много времени даже для новейших графических процессоров Nvidia RTX 3xxx и 4xxx. По умолчанию пакет Diffuers поставляется с неоптимизированными настройками. Два решения могут быть применены для значительного повышения производительности.

Вот скорость взаимодействия до применения следующего решения, всего около 2.x итераций в секунду в RTX 3070 TI 8G RAM для генерации изображения 512x512

  • Используйте веса половинной точности

Первое решение состоит в использовании гирь половинной точности. Веса половинной точности используют 16-битные числа с плавающей запятой вместо традиционных 32-битных чисел. Это уменьшает объем памяти, необходимый для хранения весов, и ускоряет вычисления, что может значительно повысить производительность пакета Diffusers.

Согласно этому видео, снижение точности с плавающей запятой с FP32 до FP16 также активирует тензорные ядра.

У меня была еще одна статья, чтобы проверить, как быстрые тензорные ядра графического процессора могут повысить скорость вычислений.



Вот как включить FP16 в рассеивателях. Простое добавление двух строк кода повысит производительность на 500%, почти не влияя на качество изображения.

from diffusers import DiffusionPipeline
import torch # <----- Line 1 added
pipeline = DiffusionPipeline.from_pretrained(
    r"D:\sd_models\deliberate_v2"
    ,torch_dtype        = torch.float16 # <----- Line 2 Added
)
pipeline.to("cuda")
image = pipeline("A cute cat playing piano").images[0]
image.save("image_of_cat_playing_piano.png")

Теперь скорость итерации увеличивается до 10.x итераций в секунду. В 5 раз быстрее.

  • Используйте Xformers

Xformers — это библиотека с открытым исходным кодом, предоставляющая набор высокопроизводительных преобразователей для различных задач обработки естественного языка (NLP). Он построен на основе PyTorch и направлен на предоставление эффективных и масштабируемых моделей преобразователей, которые можно легко интегрировать в существующие конвейеры NLP. (Сейчас есть модели, которые не используют Transformer? :P)

Установите Xformers с помощью pip install xformers , тогда мы сможем легко переключить диффузоры на использование xformers с помощью кода одной строки.

...
pipeline.to("cuda")
pipeline.enable_xformers_memory_efficient_attention()  <--- one line added
...

Этот однострочный код повышает производительность еще на 20%.

3. Удалите ограничение на 77 токенов приглашения.

В текущей версии Diffusers существует ограничение в 77 токенов подсказок, которые можно использовать при создании изображений.

К счастью, есть решение этой проблемы. Используя конвейер «lpw_stable_diffusion», предоставленный сообществом, вы можете разблокировать ограничение на 77 токенов подсказок и создавать высококачественные изображения с более длинными подсказками.

Чтобы использовать конвейер «lpw_stable_diffusion», вы можете использовать следующий код:

pipeline = DiffusionPipeline.from_pretrained(
    model_path,
    custom_pipeline="lpw_stable_diffusion",  #<--- code added
    torch_dtype=torch.float16
)

В этом коде мы инициализируем новый объект DiffusionPipeline с помощью метода «from_pretrained». Мы указываем путь к предварительно обученной модели и устанавливаем аргумент «custom_pipeline» в «lpw_stable_diffusion». Это говорит диффузорам использовать конвейер «lpw_stable_diffusion», который разблокирует ограничение на 77 токенов приглашения.

Теперь давайте используем длинную строку подсказки, чтобы проверить это. Вот полный код:

from diffusers import DiffusionPipeline
import torch
pipeline = DiffusionPipeline.from_pretrained(
    r"D:\sd_models\deliberate_v2"
    ,custom_pipeline = "lpw_stable_diffusion"  #<--- code added
    ,torch_dtype        = torch.float16
)
pipeline.to("cuda")
pipeline.enable_xformers_memory_efficient_attention()
prompt = """
Babel tower falling down, walking on the starlight, dreamy ultra wide shot
, atmospheric, hyper realistic, epic composition, cinematic, octane render
, artstation landscape vista photography by Carr Clifton & Galen Rowell, 16K resolution
, Landscape veduta photo by Dustin Lefevre & tdraw, detailed landscape painting by Ivan Shishkin
, DeviantArt, Flickr, rendered in Enscape, Miyazaki, Nausicaa Ghibli, Breath of The Wild
, 4k detailed post processing, artstation, rendering by octane, unreal engine
"""
image = pipeline(prompt).images[0]
image.save("goodbye_babel_tower.png")

И вы получите вот такое изображение:

Если вы по-прежнему видите предупреждающее сообщение, например: Token indices sequence length is longer than the specified maximum sequence length for this model ( *** > 77 ) . Running this sequence through the model will result in indexing errors. Это нормально, вы можете просто проигнорировать его.

4. Используйте пользовательский LoRA с диффузорами

Несмотря на заявления о поддержке LoRA в диффузорах, пользователи по-прежнему сталкиваются с ограничениями, когда дело доходит до загрузки локальных файлов LoRA в формате файлов .safetensor. Это может стать серьезным препятствием для использования пользователями LoRA от сообщества.

Чтобы преодолеть это ограничение, я создал функцию, которая позволяет пользователям загружать файлы LoRA со взвешенными числами в режиме реального времени. Эту функцию можно использовать для загрузки файлов LoRA и соответствующих им весов в модель диффузоров, что позволяет создавать высококачественные изображения с данными LoRA.

Вот тело функции:

from safetensors.torch import load_file
def __load_lora(
    pipeline
    ,lora_path
    ,lora_weight=0.5
):
    state_dict = load_file(lora_path)
    LORA_PREFIX_UNET = 'lora_unet'
    LORA_PREFIX_TEXT_ENCODER = 'lora_te'

    alpha = lora_weight
    visited = []

    # directly update weight in diffusers model
    for key in state_dict:
        
        # as we have set the alpha beforehand, so just skip
        if '.alpha' in key or key in visited:
            continue
            
        if 'text' in key:
            layer_infos = key.split('.')[0].split(LORA_PREFIX_TEXT_ENCODER+'_')[-1].split('_')
            curr_layer = pipeline.text_encoder
        else:
            layer_infos = key.split('.')[0].split(LORA_PREFIX_UNET+'_')[-1].split('_')
            curr_layer = pipeline.unet

        # find the target layer
        temp_name = layer_infos.pop(0)
        while len(layer_infos) > -1:
            try:
                curr_layer = curr_layer.__getattr__(temp_name)
                if len(layer_infos) > 0:
                    temp_name = layer_infos.pop(0)
                elif len(layer_infos) == 0:
                    break
            except Exception:
                if len(temp_name) > 0:
                    temp_name += '_'+layer_infos.pop(0)
                else:
                    temp_name = layer_infos.pop(0)
        
        # org_forward(x) + lora_up(lora_down(x)) * multiplier
        pair_keys = []
        if 'lora_down' in key:
            pair_keys.append(key.replace('lora_down', 'lora_up'))
            pair_keys.append(key)
        else:
            pair_keys.append(key)
            pair_keys.append(key.replace('lora_up', 'lora_down'))
        
        # update weight
        if len(state_dict[pair_keys[0]].shape) == 4:
            weight_up = state_dict[pair_keys[0]].squeeze(3).squeeze(2).to(torch.float32)
            weight_down = state_dict[pair_keys[1]].squeeze(3).squeeze(2).to(torch.float32)
            curr_layer.weight.data += alpha * torch.mm(weight_up, weight_down).unsqueeze(2).unsqueeze(3)
        else:
            weight_up = state_dict[pair_keys[0]].to(torch.float32)
            weight_down = state_dict[pair_keys[1]].to(torch.float32)
            curr_layer.weight.data += alpha * torch.mm(weight_up, weight_down)
            
        # update visited list
        for item in pair_keys:
            visited.append(item)
        
    return pipeline

Логика извлечена из convert_lora_safetensor_to_diffusers.py репозитория git диффузоров.

Возьмем, к примеру, одну из знаменитых LoRA:MoXin. вы можете использовать функцию __load_lora следующим образом:

from diffusers import DiffusionPipeline
import torch
pipeline = DiffusionPipeline.from_pretrained(
    r"D:\sd_models\deliberate_v2"
    ,custom_pipeline = "lpw_stable_diffusion"  
    ,torch_dtype        = torch.float16
)
lora = (r"D:\sd_models\Lora\Moxin_10.safetensors",0.8)
pipeline = __load_lora(pipeline=pipeline,lora_path=lora[0],lora_weight=lora[1])
pipeline.to("cuda")
pipeline.enable_xformers_memory_efficient_attention()

prompt = """
shukezouma,negative space,shuimobysim 
a branch of flower, traditional chinese ink painting
"""
image = pipeline(prompt).images[0]
image.save("a branch of flower.png")

Приглашение создаст изображение, подобное этому:

Вы можете вызывать __load_lora() несколько раз, чтобы загрузить несколько LoRA для одного поколения.

С помощью этой функции теперь вы можете загружать файлы LoRA со взвешенными числами в режиме реального времени и использовать их для создания высококачественных изображений с помощью диффузоров. Загрузка LoRA довольно быстрая, обычно занимает всего 1–2 секунды, что намного лучше, чем преобразование и использование (которое создаст другой файл модели размером в ГБ).

5. Используйте пользовательские текстурные инверсии с диффузорами

Использование пользовательского пакета Texture Inversions with Diffusers может стать эффективным способом создания высококачественных изображений. Однако официальная документация диффузоров предполагает, что пользователям необходимо обучать свои собственные текстовые инверсии, что может занять до часа на графическом процессоре V100. Это может оказаться нецелесообразным для многих пользователей, которые хотят быстро создавать изображения.

Поэтому я исследовал его и нашел решение, которое позволяет диффузорам использовать текстовую инверсию, как в Stable Diffusion WebUI. Ниже приведена функция, которую я создал для загрузки пользовательской текстовой инверсии.

def load_textual_inversion(
    learned_embeds_path
    , text_encoder
    , tokenizer
    , token = None
    , weight = 0.5
):
    '''
    Use this function to load textual inversion model in model initilization stage 
    or image generation stage. 
    '''
    loaded_learned_embeds = torch.load(learned_embeds_path, map_location="cpu")
    string_to_token = loaded_learned_embeds['string_to_token']
    string_to_param = loaded_learned_embeds['string_to_param']
    
    # separate token and the embeds
    trained_token = list(string_to_token.keys())[0]
    embeds = string_to_param[trained_token]
    embeds = embeds[0] * weight

    # cast to dtype of text_encoder
    dtype = text_encoder.get_input_embeddings().weight.dtype
    embeds.to(dtype)

    # add the token in tokenizer
    token = token if token is not None else trained_token
    num_added_tokens = tokenizer.add_tokens(token)
    if num_added_tokens == 0:
        #print(f"The tokenizer already contains the token {token}.The new token will replace the previous one")
        raise ValueError(f"The tokenizer already contains the token {token}. Please pass a different `token` that is not already in the tokenizer.")
    
    # resize the token embeddings
    text_encoder.resize_token_embeddings(len(tokenizer))
    
    # get the id for the token and assign the embeds
    token_id = tokenizer.convert_tokens_to_ids(token)
    text_encoder.get_input_embeddings().weight.data[token_id] = embeds
    return (tokenizer,text_encoder)

В функции load_textual_inversion() вам необходимо предоставить следующие аргументы:

  • learned_embeds_path: Путь к файлу предварительно обученной текстовой модели инверсии в формате .pt или .bin.
  • text_encoder: Объект кодировщика текста, полученный из конвейера распространения.
  • tokenizer: объект Tokenizer, полученный из конвейера распространения.
  • token: необязательный аргумент, указывающий токен подсказки. По умолчанию установлено значение «Нет». это ключевое слово, которое вызовет инверсию текста в вашей подсказке.
  • weight: необязательный аргумент, указывающий вес текстовой инверсии. По умолчанию я установил его на 0,5. вы можете изменить на другое значение по мере необходимости.

Теперь вы можете использовать эту функцию с конвейером диффузоров следующим образом:

from diffusers import DiffusionPipeline
import torch
pipeline = DiffusionPipeline.from_pretrained(
    r"D:\sd_models\deliberate_v2"
    ,custom_pipeline = "lpw_stable_diffusion"  
    ,torch_dtype        = torch.float16
    ,safety_checker     = None
)

textual_inversion_path = r"D:\sd_models\embeddings\style-empire.pt"

tokenizer       = pipeline.tokenizer
text_encoder    = pipeline.text_encoder 
load_textual_inversion(
    learned_embeds_path     = textual_inversion_path
    , tokenizer             = tokenizer
    , text_encoder          = text_encoder
    , token                 = 'styleempire'
)

pipeline.to("cuda")
pipeline.enable_xformers_memory_efficient_attention()

prompt = """
styleempire,award winning beautiful street, storm,((dark storm clouds))
, fluffy clouds in the sky, shaded flat illustration, digital art
, trending on artstation, highly detailed, fine detail, intricate
, ((lens flare)), (backlighting), (bloom)
"""
neg_prompt = """
 cartoon, 3d, ((disfigured)), ((bad art)), ((deformed)), ((poorly drawn))
 , ((extra limbs)), ((close up)), ((b&w)), weird colors, blurry
 , hat, cap, glasses, sunglasses, lightning, face
"""

generator = torch.Generator("cuda").manual_seed(1)
image = pipeline(
    prompt
    ,negative_prompt =neg_prompt
    ,generator       = generator
).images[0]
image.save("tv_test.png")

Вот результат применения текстовой инверсии стиль ампир.

Современная улица слева превращается в старый лондонский стиль.

6. Высококачественные изображения

Пакет Diffusers отлично подходит для создания высококачественных изображений, но масштабирование изображения не является его основной функцией. Однако Stable-Diffusion-WebUI предлагает функцию под названием HighRes, которая позволяет пользователям масштабировать сгенерированные изображения до 2x или 4x. Было бы здорово, если бы пользователи Diffusers могли пользоваться той же функцией. После некоторых исследований и испытаний я обнаружил, что модель SwinRI является отличным вариантом для масштабирования изображений, и она может легко масштабировать изображения в 2 или 4 раза после их создания.

Чтобы использовать модель SwinRI для масштабирования изображения, мы можем использовать код из репозитория GitHub JingyunLiang/SwinIR. Если вам нужны только коды, достаточно скачать models/network_swinir.py, utils/util_calculate_psnr_ssim.py и main_test_swinir.py. Следуя рекомендациям readme, вы можете масштабировать изображения как по волшебству.

Вот пример того, насколько хорошо SwinRI может масштабировать изображение.

Для улучшения качества изображения можно использовать множество других решений с открытым исходным кодом. Вот список трех других моделей, которые я опробовал и которые дают замечательные результаты.

RealSR может масштабировать изображение в 4 раза почти так же хорошо, как SwinRI, и его производительность выполнения является самой быстрой, вместо вызова PyTorch и CUDA. Автор компилирует код и использование CUDA напрямую в двоичный файл. Мои наблюдения показывают, что RealSR может улучшить мага всего за 2–4 секунды.

CodeFormer хорошо восстанавливает размытые или сломанные лица, он также может удалять шум и улучшать детализацию фона. Это решение и алгоритм широко используются в других приложениях, включая Stable-Diffusion-WebUI.

Еще одно мощное решение с открытым исходным кодом, которое архивирует удивительные результаты восстановления лица, и оно также быстрое. GFPGAN также интегрирован в Stable-Diffusion-WebUI.

[Обновлено 19 апреля 2023 г.]

Обнаружено, что SD 1.5 и все расширенные модели плохо справляются с созданием изображения с высоким разрешением, просто используя конвейер text2img. На практике я обнаружил, что конвейер Diffusers text2img легко генерирует скрученные и битые изображения даже в разрешении 1920x1080, те же настройки и приглашение могут генерировать хорошие изображения в разрешении 800x600.

Я обнаружил, что конвейер img2img от Diffusers может работать как отличное решение для исправления изображений с высоким разрешением. вот общие шаги по реализации конвейера img2img в качестве решения для исправления изображений с высоким разрешением:

  1. Создайте изображение с низким разрешением, используя конвейер text2img.
  2. Увеличьте изображение до любого разрешения (максимальный размер зависит от размера вашей видеопамяти). img = img.resize((width,height)) . Тест показывает, что моя 8G VRAM RTX 3070 Ti справляется с масштабированием 800x600 в 3 раза до 2400x1800. Обратите внимание, что на этом этапе масштабирование или исправление изображения не происходит, просто увеличьте изображение до нужного размера.
  3. Затем подайте новый увеличенный вручную img в конвейер img2img с той же подсказкой, отрицательной подсказкой и дополнительной настройкой: strength в вызов, вы увидите, что ввод масштабируется как по волшебству.

img2img немного изменит содержимое изображения, возьмем в качестве примера лицо, он не только улучшит масштаб изображения, но и немного изменит лицо.

7. Оптимизация использования памяти CUDA для диффузоров

При использовании диффузоров для создания изображений важно учитывать использование памяти CUDA, особенно если вы хотите загрузить другие модели для дальнейшей обработки сгенерированных изображений. Если вы попытаетесь загрузить другую модель, такую ​​как SwinIR, для масштабирования изображений, вы можете столкнуться с RuntimeError: CUDA out of memory из-за того, что модель Diffuser все еще занимает память CUDA.

Чтобы смягчить эту проблему, существует несколько решений для оптимизации использования памяти CUDA. Следующие два решения, которые я нашел, работают лучше всего:

  • Нарезанное внимание для дополнительной экономии памяти

Нарезанное внимание — это метод, который уменьшает использование памяти механизмами внутреннего внимания в трансформерах. Разбивая матрицу внимания на более мелкие блоки, требования к памяти снижаются. Этот метод можно использовать с пакетом Diffusers, чтобы уменьшить объем памяти, занимаемой моделью Diffuser.

Чтобы использовать его в диффузорах, просто введите код одной строки:

pipeline.enable_attention_slicing()
  • Выгрузка модели на ЦП

Обычно у вас не будет двух моделей, работающих одновременно, идея состоит в том, чтобы временно выгрузить данные модели в память ЦП и освободить место в памяти CUA для других моделей, а загружать только в VRAM, когда вы начинаете использовать модель. .

Чтобы использовать динамическую выгрузку данных в память ЦП в диффузорах, используйте этот код строки:

pipeline.enable_model_cpu_offload()

После применения этого, всякий раз, когда диффузоры завершают задачу создания изображения, данные модели будут автоматически выгружаться в память ЦП до следующего вызова.

Чтобы узнать больше о производительности и оптимизации VRAM для диффузоров с PyTorch 2.0, ознакомьтесь с этой статьей, которую я написал в качестве дополнения к этой статье.



Краткое содержание

В статье обсуждается, как улучшить производительность и возможности пакета Diffusers. В статье рассматриваются несколько решений распространенных проблем, с которыми сталкиваются пользователи Diffusers, включая загрузку локальных моделей .safetensor, повышение производительности, снятие ограничения на 77 токенов подсказок, использование пользовательского LoRA и инверсии текста. , масштабирование изображений и оптимизация использования памяти CUDA.

Применяя эти решения, пользователи Diffusers могут создавать высококачественные изображения с большей производительностью и большим контролем над процессом. Статья также включает фрагменты кода и подробные пояснения для каждого решения.

Если вы сможете успешно применить эти решения и код в своем случае, может быть дополнительное преимущество, которое я очень выиграю, заключается в том, что вы можете реализовать свои собственные решения, прочитав исходный код Diffusers, и лучше понять, как работает Stable Diffusion. Для меня изучение, поиск и внедрение этих решений — увлекательное путешествие. Надеюсь, что эти решения также помогут вам, и желаю вам насладиться пакетом Stable Diffusion и диффузорами.

Здесь укажите приглашение, которое генерирует изображение заголовка:

Babel tower falling down, walking on the starlight, dreamy ultra wide shot
, atmospheric, hyper realistic, epic composition, cinematic, octane render
, artstation landscape vista photography by Carr Clifton & Galen Rowell, 16K resolution
, Landscape veduta photo by Dustin Lefevre & tdraw, detailed landscape painting by Ivan Shishkin
, DeviantArt, Flickr, rendered in Enscape, Miyazaki, Nausicaa Ghibli, Breath of The Wild
, 4k detailed post processing, artstation, rendering by octane, unreal engine

Размер: 600 * 800
Исходное значение: 3977059881
Планировщик (или метод выборки): DPMSolverMultistepScheduler
Выборка шаги: 25
Шкала CFG (или шкала рекомендаций): 7,5
Модель SwinRI: 003_realSR_BSRGAN_DFO_s64w8_SwinIR-M_x4_GAN.pth

Лицензия и повторное использование кода

Решения, представленные в этой статье, были получены путем обширного чтения исходников, последующего ночного тестирования и логического проектирования. Важно отметить, что на момент написания (апрель 2023 г.) загрузка решений LoRA и Textual Inversion, а также код, включенный в эту статью, были единственными рабочими версиями в Интернете.

Если вы найдете код, представленный в этой статье, полезным и хотите повторно использовать его в своем проекте, документе или статье, пожалуйста, вернитесь к этой статье на Medium. Представленный здесь код находится под лицензией MIT, которая позволяет вам использовать, копировать, изменять, объединять, публиковать, распространять, сублицензировать и/или продавать копии программного обеспечения в соответствии с условиями лицензии.

Обратите внимание, что решения, представленные в этой статье, могут быть не оптимальными или наиболее эффективными для достижения желаемых результатов и могут быть изменены по мере появления новых разработок и улучшений. Всегда рекомендуется тщательно тестировать и проверять любой код, прежде чем внедрять его в производственную среду.

Рекомендации