Buckets:

mishig's picture
|
download
raw
26.6 kB

Sous licence Apache License, Version 2.0 (la « Licence ») ;

vous ne pouvez utiliser ce fichier que conformément à la Licence.

Vous pouvez obtenir un exemplaire de la Licence à

http://www.apache.org/licenses/LICENSE-2.0

Sauf exigence légale ou accord écrit

distribué sous la Licence est distribué sur une base « EN L'ÉTAT »,

SANS AUCUNE GARANTIE NI CONDITION, explicite ou implicite.

Voir la Licence pour le langage spécifique régissant les autorisations et

limitations under the License. -->

ChronoEdit

ChronoEdit : vers le raisonnement temporel pour l'édition d'images et la simulation du monde de NVIDIA et de l'Université de Toronto, par Jay Zhangjie Wu, Xuanchi Ren, Tianchang Shen, Tianshi Cao, Kai He, Yifan Lu, Ruiyuan Gao, Enze Xie, Shiyi Lan, Jose M. Alvarez, Jun Gao, Sanja Fidler, Zian Wang, Huan Ling.

En bref : ChronoEdit reformule l'édition d'images comme une tâche de génération de vidéo, en utilisant les images d'entrée et éditées comme images de début et de fin afin d'exploiter les modèles de vidéo préentraînés avec une cohérence temporelle. Une étape de raisonnement temporel introduit des jetons de raisonnement pour garantir des édictions physiquement plausibles et visualiser la trajectoire d'édition.

Les récentes avancées dans les grands modèles génératifs ont considérablement amélioré à la fois l'édition d'images et la génération d'images en contexte, mais un écart critique demeure en matière de cohérence physique, où les objets modifiés doivent rester cohérents. Cette capacité est particulièrement cruciale pour les tâches liées à la simulation du monde. Dans cet article, nous présentons ChronoEdit, un cadre qui reformule l'édition d'images comme un problème de génération de vidéo. D'abord, ChronoEdit traite les images d'entrée et modifiées comme les première et dernière images d'une vidéo, ce qui lui permet d'exploiter de grands modèles génératifs de vidéo pré-entraînés qui capturent non seulement l'apparence des objets, mais aussi la physique implicite du mouvement et de l'interaction grâce à une cohérence temporelle apprise. Ensuite, ChronoEdit introduit une étape de raisonnement temporel qui effectue explicitement l'édition au moment de l'inférence. Dans ce contexte, l'image cible est débruitée conjointement avec des jetons de raisonnement pour imaginer une trajectoire d'édition plausible qui contraint l'espace des solutions à des transformations physiquement viables. Les jetons de raisonnement sont ensuite abandonnés après quelques étapes afin d'éviter le coût computationnel élevé du rendu d'une vidéo complète. Pour valider ChronoEdit, nous introduisons PBench-Edit, un nouveau jeu de référence de paires image-prompt pour des contextes nécessitant une cohérence physique, et démontrons que ChronoEdit surpasse les références de l'état de l'art tant en fidélité visuelle qu'en plausibilité physique. Page du projet pour le code et les modèles : this https URL.

Le pipeline ChronoEdit est développé par l'équipe ChronoEdit. Le code d'origine est disponible sur GitHub, et les modèles pré-entraînés peuvent être trouvés dans la collection nvidia/ChronoEdit sur Hugging Face.

Modèles/LoRAs disponibles :

Édition d'images

import torch
import numpy as np
from diffusers import AutoencoderKLWan, ChronoEditTransformer3DModel, ChronoEditPipeline
from diffusers.utils import export_to_video, load_image
from transformers import CLIPVisionModel
from PIL import Image

model_id = "nvidia/ChronoEdit-14B-Diffusers"
image_encoder = CLIPVisionModel.from_pretrained(model_id, subfolder="image_encoder", dtype=torch.float32)
vae = AutoencoderKLWan.from_pretrained(model_id, subfolder="vae", dtype=torch.float32)
transformer = ChronoEditTransformer3DModel.from_pretrained(model_id, subfolder="transformer", dtype=torch.bfloat16)
pipe = ChronoEditPipeline.from_pretrained(model_id, image_encoder=image_encoder, transformer=transformer, vae=vae, dtype=torch.bfloat16)
pipe.to("cuda")  # or "mps", "xpu", "cpu"

image = load_image(
    "https://huggingface.co/spaces/nvidia/ChronoEdit/resolve/main/examples/3.png"
)
max_area = 720 * 1280
aspect_ratio = image.height / image.width
mod_value = pipe.vae_scale_factor_spatial * pipe.transformer.config.patch_size[1]
height = round(np.sqrt(max_area * aspect_ratio)) // mod_value * mod_value
width = round(np.sqrt(max_area / aspect_ratio)) // mod_value * mod_value
print("width", width, "height", height)
image = image.resize((width, height))
prompt = (
    "The user wants to transform the image by adding a small, cute mouse sitting inside the floral teacup, enjoying a spa bath. The mouse should appear relaxed and cheerful, with a tiny white bath towel draped over its head like a turban. It should be positioned comfortably in the cup’s liquid, with gentle steam rising around it to blend with the cozy atmosphere. "
    "The mouse’s pose should be natural—perhaps sitting upright with paws resting lightly on the rim or submerged in the tea. The teacup’s floral design, gold trim, and warm lighting must remain unchanged to preserve the original aesthetic. The steam should softly swirl around the mouse, enhancing the spa-like, whimsical mood."
)

output = pipe(
    image=image,
    prompt=prompt,
    height=height,
    width=width,
    num_frames=5,
    num_inference_steps=50,
    guidance_scale=5.0,
    enable_temporal_reasoning=False,
    num_temporal_reasoning_steps=0,
).frames[0]
Image.fromarray((output[-1] * 255).clip(0, 255).astype("uint8")).save("output.png")

Facultativement, activez le raisonnement temporel pour une meilleure cohérence physique :

output = pipe(
    image=image,
    prompt=prompt,
    height=height,
    width=width,
    num_frames=29,
    num_inference_steps=50,
    guidance_scale=5.0,
    enable_temporal_reasoning=True,
    num_temporal_reasoning_steps=50,
).frames[0]
export_to_video(output, "output.mp4", fps=16)
Image.fromarray((output[-1] * 255).clip(0, 255).astype("uint8")).save("output.png")

Inférence avec Lora de distillation en 8 étapes

import torch
import numpy as np
from diffusers import AutoencoderKLWan, ChronoEditTransformer3DModel, ChronoEditPipeline
from diffusers.schedulers import UniPCMultistepScheduler
from diffusers.utils import export_to_video, load_image
from transformers import CLIPVisionModel
from PIL import Image

model_id = "nvidia/ChronoEdit-14B-Diffusers"
image_encoder = CLIPVisionModel.from_pretrained(model_id, subfolder="image_encoder", dtype=torch.float32)
vae = AutoencoderKLWan.from_pretrained(model_id, subfolder="vae", dtype=torch.float32)
transformer = ChronoEditTransformer3DModel.from_pretrained(model_id, subfolder="transformer", dtype=torch.bfloat16)
pipe = ChronoEditPipeline.from_pretrained(model_id, image_encoder=image_encoder, transformer=transformer, vae=vae, dtype=torch.bfloat16)
pipe.load_lora_weights("nvidia/ChronoEdit-14B-Diffusers", weight_name="lora/chronoedit_distill_lora.safetensors", adapter_name="distill")
pipe.fuse_lora(adapter_names=["distill"], lora_scale=1.0)
pipe.scheduler = UniPCMultistepScheduler.from_config(pipe.scheduler.config, flow_shift=2.0)
pipe.to("cuda")  # or "mps", "xpu", "cpu"

image = load_image(
    "https://huggingface.co/spaces/nvidia/ChronoEdit/resolve/main/examples/3.png"
)
max_area = 720 * 1280
aspect_ratio = image.height / image.width
mod_value = pipe.vae_scale_factor_spatial * pipe.transformer.config.patch_size[1]
height = round(np.sqrt(max_area * aspect_ratio)) // mod_value * mod_value
width = round(np.sqrt(max_area / aspect_ratio)) // mod_value * mod_value
print("width", width, "height", height)
image = image.resize((width, height))
prompt = (
    "The user wants to transform the image by adding a small, cute mouse sitting inside the floral teacup, enjoying a spa bath. The mouse should appear relaxed and cheerful, with a tiny white bath towel draped over its head like a turban. It should be positioned comfortably in the cup’s liquid, with gentle steam rising around it to blend with the cozy atmosphere. "
    "The mouse’s pose should be natural—perhaps sitting upright with paws resting lightly on the rim or submerged in the tea. The teacup’s floral design, gold trim, and warm lighting must remain unchanged to preserve the original aesthetic. The steam should softly swirl around the mouse, enhancing the spa-like, whimsical mood."
)

output = pipe(
    image=image,
    prompt=prompt,
    height=height,
    width=width,
    num_frames=5,
    num_inference_steps=8,
    guidance_scale=1.0,
    enable_temporal_reasoning=False,
    num_temporal_reasoning_steps=0,
).frames[0]
export_to_video(output, "output.mp4", fps=16)
Image.fromarray((output[-1] * 255).clip(0, 255).astype("uint8")).save("output.png")

Inférence avec plusieurs LoRAs

import torch
import numpy as np
from diffusers import AutoencoderKLWan, ChronoEditTransformer3DModel, ChronoEditPipeline
from diffusers.schedulers import UniPCMultistepScheduler
from diffusers.utils import export_to_video, load_image
from transformers import CLIPVisionModel
from PIL import Image

model_id = "nvidia/ChronoEdit-14B-Diffusers"
image_encoder = CLIPVisionModel.from_pretrained(model_id, subfolder="image_encoder", dtype=torch.float32)
vae = AutoencoderKLWan.from_pretrained(model_id, subfolder="vae", dtype=torch.float32)
transformer = ChronoEditTransformer3DModel.from_pretrained(model_id, subfolder="transformer", dtype=torch.bfloat16)
pipe = ChronoEditPipeline.from_pretrained(model_id, image_encoder=image_encoder, transformer=transformer, vae=vae, dtype=torch.bfloat16)
pipe.load_lora_weights("nvidia/ChronoEdit-14B-Diffusers-Paint-Brush-Lora", weight_name="paintbrush_lora_diffusers.safetensors", adapter_name="paintbrush")
pipe.load_lora_weights("nvidia/ChronoEdit-14B-Diffusers", weight_name="lora/chronoedit_distill_lora.safetensors", adapter_name="distill")
pipe.fuse_lora(adapter_names=["paintbrush", "distill"], lora_scale=1.0)
pipe.scheduler = UniPCMultistepScheduler.from_config(pipe.scheduler.config, flow_shift=2.0)
pipe.to("cuda")  # or "mps", "xpu", "cpu"

image = load_image(
    "https://raw.githubusercontent.com/nv-tlabs/ChronoEdit/refs/heads/main/assets/images/input_paintbrush.png"
)
max_area = 720 * 1280
aspect_ratio = image.height / image.width
mod_value = pipe.vae_scale_factor_spatial * pipe.transformer.config.patch_size[1]
height = round(np.sqrt(max_area * aspect_ratio)) // mod_value * mod_value
width = round(np.sqrt(max_area / aspect_ratio)) // mod_value * mod_value
print("width", width, "height", height)
image = image.resize((width, height))
prompt = (
    "Turn the pencil sketch in the image into an actual object that is consistent with the image’s content. The user wants to change the sketch to a crown and a hat."
)

output = pipe(
    image=image,
    prompt=prompt,
    height=height,
    width=width,
    num_frames=5,
    num_inference_steps=8,
    guidance_scale=1.0,
    enable_temporal_reasoning=False,
    num_temporal_reasoning_steps=0,
).frames[0]
export_to_video(output, "output.mp4", fps=16)
Image.fromarray((output[-1] * 255).clip(0, 255).astype("uint8")).save("output_1.png")

ChronoEditPipeline[[diffusers.ChronoEditPipeline]]

diffusers.ChronoEditPipeline[[diffusers.ChronoEditPipeline]]

diffusers.ChronoEditPipeline(tokenizer: AutoTokenizer, text_encoder: UMT5EncoderModel, image_encoder: CLIPVisionModel, image_processor: CLIPImageProcessorPil, transformer: ChronoEditTransformer3DModel, vae: AutoencoderKLWan, scheduler: FlowMatchEulerDiscreteScheduler)

Source

Paramètres :

tokenizer (T5Tokenizer) : Tokenizer de T5, plus précisément la variante google/umt5-xxl.

text_encoder (T5EncoderModel) : T5, plus précisément la variante google/umt5-xxl.

image_encoder (CLIPVisionModel) : CLIP, plus précisément la variante clip-vit-huge-patch14.

transformer (WanTransformer3DModel) : Transformer conditionnel pour le débruitage des latents d'entrée.

scheduler (UniPCMultistepScheduler) : un planificateur à utiliser en combinaison avec transformer pour débruyer les latents d'image encodés.

vae (AutoencoderKLWan) : modèle Variational Auto-Encoder (VAE) permettant d’encoder et de décoder des vidéos vers et depuis des représentations latentes.

Pipeline pour la génération de vidéos à partir d’images utilisant Wan.

Ce modèle hérite de DiffusionPipeline. Consultez la documentation de la classe parente pour les méthodes génériques implémentées pour tous les pipelines (téléchargement, sauvegarde, exécution sur un appareil particulier, etc.).

appel[[diffusers.ChronoEditPipeline.call]]

__call__(image: typing.Union[PIL.Image.Image, numpy.ndarray, torch.Tensor, list[PIL.Image.Image], list[numpy.ndarray], list[torch.Tensor]], prompt: str | list[str] = None, negative_prompt: str | list[str] = None, height: int = 480, width: int = 832, num_frames: int = 81, num_inference_steps: int = 50, guidance_scale: float = 5.0, num_videos_per_prompt: int | None = 1, generator: typing.Union[torch.Generator, list[torch.Generator], NoneType] = None, latents: typing.Optional[torch.Tensor] = None, prompt_embeds: typing.Optional[torch.Tensor] = None, negative_prompt_embeds: typing.Optional[torch.Tensor] = None, image_embeds: typing.Optional[torch.Tensor] = None, output_type: str | None = 'np', return_dict: bool = True, attention_kwargs: dict[str, typing.Any] | None = None, callback_on_step_end: typing.Optional[typing.Callable[[int, int, NoneType], diffusers.callbacks.PipelineCallback | diffusers.callbacks.MultiPipelineCallbacks]] = None, callback_on_step_end_tensor_inputs: list[str] = ['latents'], max_sequence_length: int = 512, enable_temporal_reasoning: bool = False, num_temporal_reasoning_steps: int = 0)

Source

Paramètres :

image (PipelineImageInput) : L'image d'entrée sur laquelle conditionner la génération. Doit être une image, une liste d'images ou un torch.Tensor.

prompt (str ou list[str], facultatif) : Le prompt ou les prompts pour guider la génération d’image. S’il n’est pas défini, il faut passer prompt_embeds à la place.

negative_prompt (str ou list[str], facultatif) : Le prompt ou les prompts à ne pas utiliser pour guider la génération d’image. S’il n’est pas défini, il faut passer negative_prompt_embeds à la place. Ignoré lorsque le guidage n’est pas utilisé (c’est-à-dire ignoré si guidance_scale est inférieur à 1).

height (int, par défaut 480) : La hauteur de la vidéo générée.

width (int, par défaut 832) : La largeur de la vidéo générée.

num_frames (int, par défaut 81) : Le nombre de frames dans la vidéo générée.

num_inference_steps (int, par défaut 50) : Le nombre d'étapes de débruitage. Un plus grand nombre d'étapes de débruitage conduit généralement à une image de meilleure qualité, au prix d'une inférence plus lente.

guidance_scale (float, par défaut 5.0) : Échelle de guidage telle que définie dans Classifier-Free Diffusion Guidance. guidance_scale est défini comme w de l'équation 2. de Imagen Paper. L'échelle de guidage est activée en définissant guidance_scale > 1. Une échelle de guidage plus élevée encourage la génération d'images étroitement liées au prompt texte, généralement au détriment de la qualité de l'image.

num_videos_per_prompt (int, facultatif, valeur par défaut 1) : Le nombre d'images à générer par invite.

generator (torch.Generator ou list[torch.Generator], facultatif) : Un torch.Generator pour rendre la génération déterministe.

latents (torch.Tensor, facultatif) : Latents bruités pré-générés, échantillonnés à partir d'une distribution gaussienne, à utiliser comme entrées pour la génération d'images. Peut être utilisé pour ajuster la même génération avec des prompts différents. S'il n'est pas fourni, un tenseur de latents est généré par échantillonnage à l'aide du generator aléatoire fourni.

prompt_embeds (torch.Tensor, facultatif) : Incrustations de texte pré-générées. Peut être utilisé pour ajuster facilement les entrées de texte (pondération des prompts). Si non fourni, les incrustations de texte sont générées à partir de l'argument d'entrée prompt.

negative_prompt_embeds (torch.Tensor, facultatif) : Incrustations de texte pré-générées. Peut être utilisé pour ajuster facilement les entrées de texte (pondération des prompts). Si non fourni, les incrustations de texte sont générées à partir de l'argument d'entrée negative_prompt.

image_embeds (torch.Tensor, facultatif) : Embeddings d’images pré-générés. Peut être utilisé pour ajuster facilement les entrées d’image (pondération). S’ils ne sont pas fournis, les embeddings d’images sont générés à partir de l’argument d’entrée image.

output_type (str, facultatif, par défaut "np") : Le format de sortie de l'image générée. Choisissez entre PIL.Image ou np.array.

return_dict (bool, facultatif, par défaut True) : Indique s'il faut retourner un ChronoEditPipelineOutput plutôt qu'un simple tuple.

attention_kwargs (dict, facultatif) : Un dictionnaire de kwargs qui, s'il est spécifié, est transmis à l'AttentionProcessor tel que défini sous self.processor dans diffusers.models.attention_processor.

callback_on_step_end (Callable, PipelineCallback, MultiPipelineCallbacks, facultatif) : une fonction ou une sous-classe de PipelineCallback ou MultiPipelineCallbacks qui est appelée à la fin de chaque étape de débruitage pendant l’inférence, avec les arguments suivants : callback_on_step_end(self: DiffusionPipeline, step: int, timestep: int, callback_kwargs: Dict). callback_kwargs contiendra une liste de tous les tenseurs, tels que spécifiés par callback_on_step_end_tensor_inputs.

callback_on_step_end_tensor_inputs (List, facultatif) : La liste des entrées de tenseurs pour la fonction callback_on_step_end. Les tenseurs spécifiés dans la liste seront passés en tant qu'argument callback_kwargs. Vous ne pourrez inclure que les variables listées dans l'attribut ._callback_tensor_inputs de votre classe de pipeline.

max_sequence_length (int, par défaut 512) : La longueur de séquence maximale du codeur de texte. Si le prompt est plus long que cela, il sera tronqué. S'il est plus court, il sera complété jusqu'à cette longueur.

enable_temporal_reasoning (bool, facultatif, par défaut False) : Indique s'il faut activer le raisonnement temporel.

num_temporal_reasoning_steps (int, facultatif, par défaut 0) : Le nombre d'étapes pour activer le raisonnement temporel.

Retourne : ~ChronoEditPipelineOutput ou tuple

Si return_dict est True, ChronoEditPipelineOutput est retourné, sinon un tuple est retourné dont le premier élément est une liste des images générées et le second élément est une liste de bool indiquant si l'image générée correspondante contient du contenu « not-safe-for-work » (nsfw).

La fonction d'appel du pipeline pour la génération.

Exemples :

>>> import torch
>>> import numpy as np
>>> from diffusers import AutoencoderKLWan, ChronoEditTransformer3DModel, ChronoEditPipeline
>>> from diffusers.utils import export_to_video, load_image
>>> from transformers import CLIPVisionModel

>>> # Available models: nvidia/ChronoEdit-14B-Diffusers
>>> model_id = "nvidia/ChronoEdit-14B-Diffusers"
>>> image_encoder = CLIPVisionModel.from_pretrained(
...     model_id, subfolder="image_encoder", torch_dtype=torch.float32
... )
>>> vae = AutoencoderKLWan.from_pretrained(model_id, subfolder="vae", torch_dtype=torch.float32)
>>> transformer = ChronoEditTransformer3DModel.from_pretrained(
...     model_id, subfolder="transformer", torch_dtype=torch.bfloat16
... )
>>> pipe = ChronoEditPipeline.from_pretrained(
...     model_id, vae=vae, image_encoder=image_encoder, transformer=transformer, torch_dtype=torch.bfloat16
... )
>>> pipe.to("cuda")

>>> image = load_image("https://huggingface.co/spaces/nvidia/ChronoEdit/resolve/main/examples/3.png")
>>> max_area = 720 * 1280
>>> aspect_ratio = image.height / image.width
>>> mod_value = pipe.vae_scale_factor_spatial * pipe.transformer.config.patch_size[1]
>>> height = round(np.sqrt(max_area * aspect_ratio)) // mod_value * mod_value
>>> width = round(np.sqrt(max_area / aspect_ratio)) // mod_value * mod_value
>>> image = image.resize((width, height))
>>> prompt = (
...     "The user wants to transform the image by adding a small, cute mouse sitting inside the floral teacup, enjoying a spa bath. The mouse should appear relaxed and cheerful, with a tiny white bath towel draped over its head like a turban. It should be positioned comfortably in the cup’s liquid, with gentle steam rising around it to blend with the cozy atmosphere. "
...     "The mouse’s pose should be natural—perhaps sitting upright with paws resting lightly on the rim or submerged in the tea. The teacup’s floral design, gold trim, and warm lighting must remain unchanged to preserve the original aesthetic. The steam should softly swirl around the mouse, enhancing the spa-like, whimsical mood."
... )

>>> output = pipe(
...     image=image,
...     prompt=prompt,
...     height=height,
...     width=width,
...     num_frames=5,
...     guidance_scale=5.0,
...     enable_temporal_reasoning=False,
...     num_temporal_reasoning_steps=0,
... ).frames[0]
>>> export_to_video(output, "output.mp4", fps=16)

encode_prompt[[diffusers.ChronoEditPipeline.encode_prompt]]

encode_prompt(prompt: str | list[str], negative_prompt: str | list[str] | None = None, do_classifier_free_guidance: bool = True, num_videos_per_prompt: int = 1, prompt_embeds: typing.Optional[torch.Tensor] = None, negative_prompt_embeds: typing.Optional[torch.Tensor] = None, max_sequence_length: int = 226, device: typing.Optional[torch.device] = None, dtype: typing.Optional[torch.dtype] = None)

Source

Paramètres :

prompt (str ou list[str], facultatif) : invite à encoder

negative_prompt (str ou list[str], facultatif) : Le prompt ou les prompts à ne pas utiliser pour guider la génération d’image. S’il n’est pas défini, il faut passer negative_prompt_embeds à la place. Ignoré lorsque le guidage n’est pas utilisé (c’est-à-dire ignoré si guidance_scale est inférieur à 1).

do_classifier_free_guidance (bool, facultatif, par défaut True) : Indique s'il faut utiliser le guidage sans classifieur ou non.

num_videos_per_prompt (int, facultatif, valeur par défaut 1) : Nombre de vidéos à générer par invite. Appareil torch sur lequel placer les embeddings résultants

prompt_embeds (torch.Tensor, facultatif) : Incrustations de texte pré-générées. Peut être utilisé pour ajuster facilement les entrées de texte, par ex. pondération des prompts. Si non fourni, les incrustations de texte seront générées à partir de l'argument d'entrée prompt.

negative_prompt_embeds (torch.Tensor, facultatif) : Embeddings de texte négatif pré-générés. Peut être utilisé pour ajuster facilement les entrées de texte, par exemple le pondération des prompts. S'il n'est pas fourni, negative_prompt_embeds sera généré à partir de l'argument d'entrée negative_prompt.

device : (torch.device, facultatif) : appareil torch

dtype : (torch.dtype, facultatif) : torch dtype

Encode le prompt dans les états cachés de l'encodeur de texte.

ChronoEditPipelineOutput[[diffusers.pipelines.chronoedit.pipeline_output.ChronoEditPipelineOutput]]

diffusers.pipelines.chronoedit.pipeline_output.ChronoEditPipelineOutput[[diffusers.pipelines.chronoedit.pipeline_output.ChronoEditPipelineOutput]]

diffusers.pipelines.chronoedit.pipeline_output.ChronoEditPipelineOutput(frames: Tensor)

Source

Paramètres :

frames (torch.Tensor, np.ndarray ou list[list[PIL.Image.Image]]) : Liste de sorties vidéo - Il peut s'agir d'une liste imbriquée de longueur batch_size, où chaque sous-liste contient des séquences d'images PIL débruitées de longueur num_frames. Il peut également s'agir d'un tableau NumPy ou d'un tenseur Torch de forme (batch_size, num_frames, channels, height, width).

Classe de sortie pour les pipelines ChronoEdit.

Xet Storage Details

Size:
26.6 kB
·
Xet hash:
651a5bdb22ba6eb920b6e9bbf51281230f3a7881a3074b1c7a97130349682da3

Xet efficiently stores files, intelligently splitting them into unique chunks and accelerating uploads and downloads. More info.