Spaces:
Running on Zero
Running on Zero
| # Copyright (c) 2025 Hansheng Chen | |
| import os | |
| import time | |
| import mimetypes | |
| import tempfile | |
| import subprocess | |
| import numpy as np | |
| import imageio | |
| import boto3 | |
| import mmcv | |
| import torch.distributed as dist | |
| from contextlib import contextmanager | |
| from concurrent.futures import ThreadPoolExecutor | |
| from pathlib import Path | |
| from io import BytesIO | |
| from functools import wraps | |
| from typing import Generator, Union | |
| from PIL import Image | |
| from boto3.s3.transfer import TransferConfig | |
| from botocore import UNSIGNED | |
| from botocore.config import Config | |
| from botocore.exceptions import ClientError | |
| from torch.hub import download_url_to_file | |
| from huggingface_hub import hf_hub_download | |
| from mmcv.fileio import BaseStorageBackend, FileClient | |
| from mmgen.utils.io_utils import MMGEN_CACHE_DIR | |
| AWS_REGION = os.getenv('AWS_REGION', 'us-west-2') | |
| AWS_ACCESS_KEY_ID = os.getenv('AWS_ACCESS_KEY_ID') | |
| AWS_SECRET_ACCESS_KEY = os.getenv('AWS_SECRET_ACCESS_KEY') | |
| AWS_SESSION_TOKEN = os.getenv('AWS_SESSION_TOKEN') | |
| S3_MULTIPART_THRESHOLD = 5 * 2**30 # 5GB | |
| TMP_DIR = '/dev/shm' if os.path.isdir('/dev/shm') else tempfile.gettempdir() | |
| S3_TRANSFER_CONFIG = TransferConfig(multipart_threshold=S3_MULTIPART_THRESHOLD) | |
| def retry(tries=5, delay=3, exceptions=(Exception,)): | |
| def decorator(func): | |
| def wrapper(*args, **kwargs): | |
| for attempt in range(1, tries + 1): | |
| try: | |
| return func(*args, **kwargs) | |
| except exceptions as e: | |
| if attempt == tries: | |
| print(f"Attempt {attempt} failed: {e}. No more retries.") | |
| raise | |
| print(f"Attempt {attempt} failed: {e}. Retrying in {delay} seconds...") | |
| time.sleep(delay) | |
| return wrapper | |
| return decorator | |
| def download_from_url(url, | |
| dest_path=None, | |
| dest_dir=MMGEN_CACHE_DIR, | |
| hash_prefix=None): | |
| """Modified from MMGeneration. | |
| """ | |
| # get the exact destination path | |
| if dest_path is None: | |
| filename = url.split('/')[-1] | |
| dest_path = os.path.join(dest_dir, filename) | |
| if dest_path.startswith('~'): | |
| dest_path = os.path.expanduser('~') + dest_path[1:] | |
| # advoid downloading existed file | |
| if os.path.exists(dest_path): | |
| return dest_path | |
| is_dist = dist.is_available() and dist.is_initialized() | |
| if is_dist: | |
| local_rank = dist.get_node_local_rank() | |
| else: | |
| local_rank = 0 | |
| # only download from the master process | |
| if local_rank == 0: | |
| # mkdir | |
| _dir = os.path.dirname(dest_path) | |
| mmcv.mkdir_or_exist(_dir) | |
| download_url_to_file(url, dest_path, hash_prefix, progress=True) | |
| # sync the other processes | |
| if is_dist: | |
| dist.barrier() | |
| return dest_path | |
| def download_from_huggingface(filename): | |
| filename = filename.replace('huggingface://', '').split('/') | |
| repo_id = '/'.join(filename[:2]) | |
| repo_filename = '/'.join(filename[2:]) | |
| is_dist = dist.is_available() and dist.is_initialized() | |
| if is_dist: | |
| local_rank = dist.get_node_local_rank() | |
| else: | |
| local_rank = 0 | |
| if local_rank == 0: | |
| cached_file = hf_hub_download( | |
| repo_id=repo_id, filename=repo_filename) | |
| if is_dist: | |
| dist.barrier() | |
| if local_rank > 0: | |
| cached_file = hf_hub_download( | |
| repo_id=repo_id, filename=repo_filename) | |
| return cached_file | |
| class S3Backend(BaseStorageBackend): | |
| _allow_symlink = True | |
| def __init__(self): | |
| if AWS_ACCESS_KEY_ID is not None and AWS_SECRET_ACCESS_KEY is not None and AWS_SESSION_TOKEN is not None: | |
| config = Config(region_name=AWS_REGION) | |
| else: | |
| config = Config(region_name=AWS_REGION, signature_version=UNSIGNED) | |
| self._client = boto3.client( | |
| 's3', | |
| aws_access_key_id=AWS_ACCESS_KEY_ID, | |
| aws_secret_access_key=AWS_SECRET_ACCESS_KEY, | |
| aws_session_token=AWS_SESSION_TOKEN, | |
| config=config) | |
| def __del__(self): | |
| self._client.close() | |
| def _split_s3_url(s3_url): | |
| s3_url = s3_url.removeprefix('s3://') | |
| bucket, _, prefix = s3_url.partition('/') | |
| return bucket, prefix | |
| def _infer_s3_extra_args(filepath: str) -> dict: | |
| extra_args = dict() | |
| ctype, enc = mimetypes.guess_type(str(filepath), strict=False) | |
| if ctype is not None: | |
| extra_args.update(ContentType=ctype) | |
| if enc == 'gzip': | |
| extra_args['ContentEncoding'] = 'gzip' | |
| return extra_args | |
| def get(self, filepath: Union[str, Path]) -> bytes: | |
| filepath = str(filepath) | |
| bucket, prefix = self._split_s3_url(filepath) | |
| bytesio = BytesIO() | |
| self._client.download_fileobj(bucket, prefix, bytesio, Config=S3_TRANSFER_CONFIG) | |
| bytesio.seek(0) | |
| return bytesio.read() | |
| def get_text(self, filepath: Union[str, Path], encoding: str = 'utf-8') -> str: | |
| return self.get(filepath).decode(encoding) | |
| def put(self, obj: bytes, filepath: Union[str, Path]) -> None: | |
| filepath = str(filepath) | |
| extra_args = self._infer_s3_extra_args(filepath) | |
| if len(obj) < S3_MULTIPART_THRESHOLD: | |
| bucket, prefix = self._split_s3_url(filepath) | |
| self._client.upload_fileobj( | |
| BytesIO(obj), | |
| bucket, | |
| prefix, | |
| Config=S3_TRANSFER_CONFIG, | |
| ExtraArgs=extra_args, | |
| ) | |
| else: | |
| ext = os.path.splitext(filepath)[-1].lower() | |
| with tempfile.NamedTemporaryFile(dir=TMP_DIR, suffix=ext, delete=False) as tmp: | |
| cached_file = tmp.name | |
| tmp.write(obj) | |
| try: | |
| cmd = ['aws', 's3', 'cp', cached_file, filepath] | |
| if 'ContentType' in extra_args: | |
| cmd += ['--content-type', extra_args['ContentType']] | |
| if 'ContentEncoding' in extra_args: | |
| cmd += ['--content-encoding', extra_args['ContentEncoding']] | |
| subprocess.run(cmd, check=True, stdout=subprocess.DEVNULL) | |
| finally: | |
| os.remove(cached_file) | |
| def put_text(self, | |
| obj: str, | |
| filepath: Union[str, Path], | |
| encoding: str = 'utf-8') -> None: | |
| self.put(bytes(obj, encoding=encoding), filepath) | |
| def remove(self, filepath: Union[str, Path]) -> None: | |
| filepath = str(filepath) | |
| bucket, prefix = self._split_s3_url(filepath) | |
| self._client.delete_object(Bucket=bucket, Key=prefix) | |
| def exists(self, filepath: Union[str, Path]) -> bool: | |
| filepath = str(filepath) | |
| bucket, prefix = self._split_s3_url(filepath) | |
| if filepath[-1] == '/': | |
| s3_objects = self._client.list_objects_v2( | |
| Bucket=bucket, | |
| Prefix=prefix, | |
| Delimiter='/', | |
| MaxKeys=2) | |
| files = [] | |
| if 'Contents' in s3_objects: | |
| files += [obj['Key'] for obj in s3_objects['Contents']] | |
| if 'CommonPrefixes' in s3_objects: | |
| files += [obj['Prefix'] for obj in s3_objects['CommonPrefixes']] | |
| exist_status = len(files) > 0 | |
| else: | |
| try: | |
| self._client.head_object(Bucket=bucket, Key=prefix) | |
| exist_status = True | |
| except ClientError as e: | |
| code = e.response.get('ResponseMetadata', {}).get('HTTPStatusCode') | |
| err = e.response.get('Error', {}).get('Code') | |
| if code == 404 or err in ('404', 'NoSuchKey', 'NotFound'): | |
| exist_status = False | |
| else: | |
| raise | |
| return exist_status | |
| def isdir(self, filepath: Union[str, Path]) -> bool: | |
| filepath = str(filepath) | |
| if not filepath.endswith('/'): | |
| filepath += '/' | |
| return self.exists(filepath) | |
| def isfile(self, filepath: Union[str, Path]) -> bool: | |
| filepath = str(filepath) | |
| return filepath[-1] != '/' and self.exists(filepath) | |
| def join_path(filepath: Union[str, Path], *filepaths: Union[str, Path]) -> str: | |
| return os.path.join(str(filepath), *(str(p) for p in filepaths)) | |
| def get_local_path( | |
| self, | |
| filepath: Union[str, Path], | |
| **kwargs) -> Generator[Union[str, Path], None, None]: | |
| assert self.isfile(filepath) | |
| try: | |
| f = tempfile.NamedTemporaryFile(delete=False, **kwargs) | |
| f.write(self.get(filepath)) | |
| f.close() | |
| yield f.name | |
| finally: | |
| os.remove(f.name) | |
| def list_dir_or_file( | |
| self, | |
| dir_path: Union[str, Path], | |
| recursive: bool = False): | |
| dir_path = str(dir_path) | |
| if not dir_path.endswith('/'): | |
| dir_path += '/' | |
| cmd = ['aws', 's3', 'ls', dir_path] | |
| if recursive: | |
| cmd.append('--recursive') | |
| bucket, prefix = self._split_s3_url(dir_path) | |
| prefix_len = len(prefix) | |
| else: | |
| prefix_len = 0 | |
| out = subprocess.check_output(cmd, text=True) | |
| names = [] | |
| for line in out.splitlines(): | |
| if not line: | |
| continue | |
| ls = line.lstrip() | |
| if not recursive and ls.startswith('PRE '): | |
| name = ls.split(maxsplit=1)[1].rstrip('/') | |
| else: | |
| parts = ls.split(maxsplit=3) | |
| if len(parts) < 4: | |
| continue | |
| key = parts[3] | |
| name = key[prefix_len:] if prefix_len else key | |
| names.append(name) | |
| return names | |
| FileClient.register_backend(name='s3', backend=S3Backend, force=True, prefixes='s3') | |
| def save_image(image, filepath, file_client): | |
| img_byte_arr = BytesIO() | |
| Image.fromarray(image).save(img_byte_arr, format='PNG') | |
| img_byte_arr = img_byte_arr.getvalue() | |
| file_client.put(img_byte_arr, filepath) | |
| def save_video(video, filepath, file_client, fps=16, quality=5, bitrate=None, macro_block_size=16): | |
| imageio.plugins.ffmpeg.get_exe() | |
| img_byte_arr = BytesIO() | |
| with imageio.get_writer( | |
| img_byte_arr, format='mp4', mode='I', fps=fps, | |
| quality=quality, bitrate=bitrate, macro_block_size=macro_block_size) as writer: | |
| for frame in video: | |
| writer.append_data(frame) | |
| img_byte_arr = img_byte_arr.getvalue() | |
| file_client.put(img_byte_arr, filepath) | |
| def load_image(filepath, file_client): | |
| img_bytes = file_client.get(filepath) | |
| extension = os.path.splitext(filepath)[-1].lower() | |
| arr = imageio.v3.imread(BytesIO(img_bytes), extension=extension) # (H,W,C) or (H,W) | |
| if arr.ndim == 2: # grayscale -> RGB | |
| arr = np.stack([arr, arr, arr], axis=-1) | |
| if arr.shape[-1] == 4: # RGBA -> RGB | |
| arr = arr[..., :3] | |
| return arr | |
| def load_images_parallel(filepaths, file_client): | |
| futures = [] | |
| results = [None] * len(filepaths) | |
| with ThreadPoolExecutor(max_workers=(os.cpu_count() or 4) * 4) as pool: | |
| for idx, abs_path in enumerate(filepaths): | |
| fut = pool.submit(load_image, abs_path, file_client) | |
| futures.append((idx, fut)) | |
| for idx, fut in futures: | |
| arr = fut.result() | |
| results[idx] = arr | |
| return results | |