├── lambda_watcher ├── __init__.py ├── README.md └── app.py ├── mothership-alerts ├── __init__.py ├── README.md └── app.py ├── Makefile ├── dynamic-batching-audio ├── README.md └── app.py ├── qrcode-stable-diffusion ├── README.md └── app.py ├── pyproject.toml ├── README.md ├── .gitignore └── LICENSE /lambda_watcher/__init__.py: -------------------------------------------------------------------------------- 1 | from .app import app -------------------------------------------------------------------------------- /mothership-alerts/__init__.py: -------------------------------------------------------------------------------- 1 | from .app import app -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | .PHONY: quality style 2 | 3 | # Check that source code meets quality standards 4 | quality: 5 | black --check --diff . 6 | ruff . 7 | 8 | # Format source code automatically 9 | style: 10 | black . 11 | ruff . --fix 12 | -------------------------------------------------------------------------------- /mothership-alerts/README.md: -------------------------------------------------------------------------------- 1 | # mothership-alerts 2 | 3 | This example texts me whenever new shows are added at the mothership comedy club in Austin, TX. 4 | 5 | From root of repo, deploy with: 6 | 7 | ```bash 8 | modal deploy -m mothership-alerts 9 | ``` 10 | 11 | ## Setup 12 | 13 | More information needed... -------------------------------------------------------------------------------- /dynamic-batching-audio/README.md: -------------------------------------------------------------------------------- 1 | Get some example audios 2 | 3 | ```bash 4 | HF_HUB_ENABLE_HF_TRANSFER=1 huggingface-cli download rkstgr/mtg-jamendo data/train/0.tar --local-dir . --repo-type dataset && \ 5 | tar -xf data/train/0.tar -C data/train/ && \ 6 | rm data/train/0.tar 7 | ``` 8 | 9 | Run: 10 | 11 | ```bash 12 | modal run dynamic-batching-audio/app.py 13 | ``` -------------------------------------------------------------------------------- /qrcode-stable-diffusion/README.md: -------------------------------------------------------------------------------- 1 | # QR Code Stable Diffusion 2 | 3 | Generate stylish QR Codes with Stable Diffusion on modal. 4 | 5 | ## Usage 6 | 7 | From this directory, you can run the following: 8 | 9 | ``` 10 | modal run app.py \ 11 | --prompt "a beautiful lush landscape, animated by studio ghibli, vivid colors" \ 12 | --qrcode-content "https://modal.com" \ 13 | --samples 2 \ 14 | --steps 50 15 | ``` 16 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [tool.black] 2 | line-length = 119 3 | target_version = ['py37'] 4 | 5 | [tool.ruff] 6 | # Never enforce `E501` (line length violations). 7 | ignore = ["C408", "C901", "E501", "E741", "W605"] 8 | select = ["C", "E", "F", "I", "W"] 9 | line-length = 119 10 | 11 | # Ignore import violations in all `__init__.py` files. 12 | [tool.ruff.per-file-ignores] 13 | "__init__.py" = ["E402", "F401", "F403", "F811"] 14 | 15 | [tool.ruff.isort] 16 | lines-after-imports = 2 17 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # modal-examples 2 | 3 | Playing around with [modal](https://modal.com). 4 | 5 | ## Getting Started 6 | 7 | Install modal client and authenticate: 8 | 9 | ``` 10 | pip install modal 11 | modal token new 12 | ``` 13 | 14 | 15 | ## Examples 16 | 17 | 18 | 19 | | Example | Description | 20 | | --- | --- | 21 | | [Mothership Comedy Club Alerts](./mothership-alerts) | Text yourself whenever new shows get added to the Comedy Mothership's website. | 22 | | [QR Code Stable Diffusion](./qrcode-stable-diffusion) | Generate stylish QR Codes with Stable Diffusion on modal. | 23 | | [Lambda Cloud Watcher](./lambda_watcher) | Text yourself whenever the machine you want on [Lambda](https://lambdalabs.com/cloud) is available. | 24 | | [Dynamic Batching Audio](./dynamic-batching-audio) | Toy example to see how dynamic batching works with audio bytes. | -------------------------------------------------------------------------------- /lambda_watcher/README.md: -------------------------------------------------------------------------------- 1 | # Lambda Labs Cloud Watcher 2 | 3 | Text yourself whenever the machine you want on [Lambda](https://lambdalabs.com/cloud) is available. 4 | 5 | - Uses the [lambdacloud](https://github.com/nateraw/lambdacloud) Python Library to authenticate + interface with the Lambda API. 6 | - [Twilio](https://twilio.com) for SMS messaging. 7 | 8 | ## Usage 9 | 10 | 0. You should have a Lambda account and a Twilio account. The Twilio account should be set up for SMS messaging. 11 | 12 | 1. Set up secrets on Modal. I made a new secret called "twilio" and added the following env variables: 13 | - `TWILIO_SID`: Twilio account identifier 14 | - `TWILIO_AUTH`: Your Twilio auth token 15 | - `LAMBDA_SECRET`: Your Lambda Labs Cloud API Key 16 | 17 | 2. Replace variables in run.py 18 | 19 | Cron jobs can't accept function params, so we hard code some variables in `run.py`. 20 | 21 | Replace/update the following variables: 22 | 23 | - `FROM_PHONE` with your Twilio phone number and 24 | - `TO_PHONE` with your phone number. 25 | - Update `DESIRED_INSTANCE_TYPES` with the instance types you want to be notified about. By default, we watch for 8xA100 and 8xV100 machines. 26 | - Update the cron schedule in `run.py` to change how often you want to check for availability. By default, it checks every 5 minutes, M-F, 3am-10am UTC. 27 | 28 | 3. Finally, run the script from root of repo: 29 | 30 | ``` 31 | modal deploy -m lambda_watcher 32 | ``` 33 | -------------------------------------------------------------------------------- /lambda_watcher/app.py: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | import modal 4 | 5 | 6 | app = modal.App("lambda-watcher") 7 | my_image = modal.Image.debian_slim().pip_install("lambdacloud", "twilio") 8 | 9 | # Replace these with your own values 10 | FROM_PHONE = "+15551234567" 11 | TO_PHONE = "+15555555555" 12 | DESIRED_INSTANCE_TYPES = ["gpu_8x_a100_80gb_sxm4", "gpu_8x_a100", "gpu_8x_v100"] 13 | 14 | 15 | @app.function(image=my_image, schedule=modal.Cron("*/5 3-9 * * 1-5"), secret=modal.Secret.from_name("twilio")) 16 | def poll_lambda_for_big_instances(): 17 | from lambdacloud import list_instance_types, login 18 | from twilio.rest import Client 19 | 20 | # Auth with lambda 21 | login(token=os.environ["LAMBDA_SECRET"]) 22 | 23 | # Auth with twilio 24 | account_sid = os.environ["TWILIO_SID"] 25 | auth_token = os.environ["TWILIO_AUTH"] 26 | client = Client(account_sid, auth_token) 27 | 28 | instances_available = [x.name for x in list_instance_types()] 29 | nl = "\n" 30 | print(f"Instances available:{nl}✅ - {f'{nl}✅ - '.join(instances_available)}") 31 | 32 | desired_instances_available = [] 33 | for desired_instance in DESIRED_INSTANCE_TYPES: 34 | if desired_instance in instances_available: 35 | desired_instances_available.append(desired_instance) 36 | 37 | if len(desired_instances_available): 38 | body = f"The following instances are available on Lambda Cloud: {', '.join(desired_instances_available)}." 39 | message = client.messages.create(from_=FROM_PHONE, to=TO_PHONE, body=body) 40 | print(f"Message sent - SID: {message.sid}") 41 | -------------------------------------------------------------------------------- /dynamic-batching-audio/app.py: -------------------------------------------------------------------------------- 1 | import io 2 | import os 3 | import tempfile 4 | from pathlib import Path 5 | 6 | import modal 7 | 8 | 9 | app = modal.App( 10 | "example-dynamic-batching-audio", 11 | image=modal.Image.debian_slim(python_version="3.11") 12 | .apt_install("ffmpeg", "libsndfile1") 13 | .pip_install("soundfile") 14 | .pip_install("torch", "torchaudio", index_url="https://download.pytorch.org/whl/cpu"), 15 | ) 16 | 17 | 18 | @app.cls() 19 | class AudioReverser: 20 | @modal.batched(max_batch_size=4, wait_ms=1000) 21 | async def reverse_audio_batch(self, audio_bytes_list: list[bytes]) -> list[bytes]: 22 | import torch 23 | import torchaudio 24 | 25 | reversed_audio_bytes = [] 26 | 27 | for audio_bytes in audio_bytes_list: 28 | # Write bytes to temporary file 29 | with tempfile.NamedTemporaryFile(delete=False, suffix=".opus") as tmp: 30 | tmp.write(audio_bytes) 31 | tmp_path = tmp.name 32 | 33 | try: 34 | # Load audio 35 | waveform, sample_rate = torchaudio.load(tmp_path) 36 | 37 | # Reverse the audio (flip along time axis) 38 | reversed_waveform = torch.flip(waveform, dims=[1]) 39 | 40 | # Save to bytes 41 | buffer = io.BytesIO() 42 | # Save as wav for compatibility 43 | torchaudio.save(buffer, reversed_waveform, sample_rate, format="wav") 44 | reversed_audio_bytes.append(buffer.getvalue()) 45 | 46 | print(f"Reversed audio of size {len(audio_bytes)} bytes") 47 | 48 | finally: 49 | # Clean up temp file 50 | os.unlink(tmp_path) 51 | 52 | return reversed_audio_bytes 53 | 54 | 55 | @app.local_entrypoint() 56 | async def main( 57 | audio_dir: str = "./data/train", 58 | out_dir: str = "./data/reversed", 59 | pattern: str = "*.opus", 60 | limit: int = 37, 61 | ): 62 | audio_reverser = AudioReverser() 63 | 64 | # Find audio files 65 | audio_dir_path = Path(audio_dir) 66 | audio_files = list(audio_dir_path.glob(pattern))[:limit] 67 | 68 | if not audio_files: 69 | print(f"No files matching pattern '{pattern}' found in {audio_dir}") 70 | return 71 | 72 | print(f"Found {len(audio_files)} audio files to process") 73 | 74 | # Read audio files as bytes 75 | audio_bytes_list = [] 76 | for audio_file in audio_files: 77 | print(f"Reading {audio_file}") 78 | with open(audio_file, "rb") as f: 79 | audio_bytes_list.append(f.read()) 80 | 81 | # Process audio files 82 | reversed_audio_list = [] 83 | async for reversed_bytes in audio_reverser.reverse_audio_batch.map.aio(audio_bytes_list): 84 | reversed_audio_list.append(reversed_bytes) 85 | 86 | # Save reversed audio files 87 | out_path = Path(out_dir) 88 | out_path.mkdir(parents=True, exist_ok=True) 89 | 90 | for i, (original_file, reversed_bytes) in enumerate(zip(audio_files, reversed_audio_list)): 91 | # Change extension to .wav since we're saving as wav 92 | save_path = out_path / f"{original_file.stem}.wav" 93 | with open(save_path, "wb") as f: 94 | f.write(reversed_bytes) 95 | print(f"Saved reversed audio to {save_path}") 96 | 97 | print(f"Successfully processed {len(audio_files)} audio files") 98 | -------------------------------------------------------------------------------- /mothership-alerts/app.py: -------------------------------------------------------------------------------- 1 | import json 2 | import os 3 | from pathlib import Path 4 | 5 | from modal import App, Cron, Image, Secret, Volume 6 | 7 | 8 | app = App("mothership-ticket-alerts") 9 | my_image = Image.debian_slim(python_version="3.10").pip_install( 10 | "beautifulsoup4", "requests", "twilio", "python-slugify" 11 | ) 12 | volume = Volume.from_name("mothership", create_if_missing=True) 13 | 14 | VOLUME_MOUNT_PATH = Path("/vol") 15 | EVENT_LIST_PATH = VOLUME_MOUNT_PATH / "event_list.json" 16 | 17 | 18 | @app.function( 19 | image=my_image, 20 | volumes={VOLUME_MOUNT_PATH: volume}, 21 | secrets=[Secret.from_name("twilio")], 22 | schedule=Cron("*/10 * * * *"), 23 | ) 24 | def check_for_updates(): 25 | from datetime import date, datetime 26 | 27 | import requests 28 | from bs4 import BeautifulSoup 29 | from slugify import slugify # type: ignore 30 | from twilio.rest import Client # type: ignore 31 | 32 | def info_to_event_id(info): 33 | date_str = info["date"] 34 | try: 35 | dt = datetime.strptime(date_str, "%A, %b %d %Y") 36 | except ValueError: 37 | today = date.today() 38 | month_day = datetime.strptime(date_str, "%A, %b %d").replace(year=today.year) 39 | year = month_day.year if month_day.date() >= today else month_day.year + 1 40 | dt = datetime.strptime(f"{date_str} {year}", "%A, %b %d %Y") 41 | start_time = info["time"].split("-")[0].strip() 42 | start_obj = datetime.strptime(start_time, "%I:%M %p").time() 43 | return f"{slugify(info['show_title'])}-{dt.strftime('%Y%m%d')}-{start_obj.strftime('%H%M')}" 44 | 45 | url = "https://comedymothership.com/shows" 46 | resp = requests.get(url, timeout=10) 47 | resp.raise_for_status() 48 | soup = BeautifulSoup(resp.content, "html.parser") 49 | content_container = soup.find("div", class_="content container") 50 | show_items = content_container.find_all("li") 51 | 52 | data = {} 53 | for item in show_items: 54 | try: 55 | date_str = item.find("div", class_="h6").get_text(strip=True) 56 | except Exception: 57 | continue 58 | show_title = item.find("h3").get_text(strip=True) 59 | li_items = item.find_all("li") 60 | time = li_items[0].text 61 | room = li_items[1].text 62 | info = dict(show_title=show_title, date=date_str, time=time, room=room) 63 | event_id = info_to_event_id(info) 64 | data[event_id] = info 65 | 66 | if not EVENT_LIST_PATH.exists(): 67 | EVENT_LIST_PATH.write_text(json.dumps(data, indent=2)) 68 | volume.commit() 69 | return 70 | 71 | old_data = json.loads(EVENT_LIST_PATH.read_text()) 72 | new_events = {k: v for k, v in data.items() if k not in old_data} 73 | EVENT_LIST_PATH.write_text(json.dumps(data, indent=2)) 74 | volume.commit() 75 | 76 | if new_events: 77 | client = Client(os.environ["TWILIO_SID"], os.environ["TWILIO_AUTH"]) 78 | for event in new_events.values(): 79 | client.messages.create( 80 | body=f"New event at Mothership! {event['show_title']} — {event['date']} at {event['time']} in {event['room']}", 81 | from_=os.environ["TWILIO_PHONE"], 82 | to=os.environ["TO_PHONE"], 83 | ) 84 | else: 85 | print("No new events found") 86 | # ######################################################################################## 87 | # # For debugging, you can delete some events from the event list to test the alert works 88 | # # For debugging, you can delete some events from the event list to test the alert works 89 | # ######################################################################################## 90 | # Delete a few existing events for testing purposes 91 | # for event_id in list(old_data)[:3]: 92 | # del old_data[event_id] 93 | # EVENT_LIST_PATH.write_text(json.dumps(old_data, indent=2)) 94 | # volume.commit() 95 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | 9 | # Distribution / packaging 10 | .Python 11 | build/ 12 | develop-eggs/ 13 | dist/ 14 | downloads/ 15 | eggs/ 16 | .eggs/ 17 | lib/ 18 | lib64/ 19 | parts/ 20 | sdist/ 21 | var/ 22 | wheels/ 23 | share/python-wheels/ 24 | *.egg-info/ 25 | .installed.cfg 26 | *.egg 27 | MANIFEST 28 | 29 | # PyInstaller 30 | # Usually these files are written by a python script from a template 31 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 32 | *.manifest 33 | *.spec 34 | 35 | # Installer logs 36 | pip-log.txt 37 | pip-delete-this-directory.txt 38 | 39 | # Unit test / coverage reports 40 | htmlcov/ 41 | .tox/ 42 | .nox/ 43 | .coverage 44 | .coverage.* 45 | .cache 46 | nosetests.xml 47 | coverage.xml 48 | *.cover 49 | *.py,cover 50 | .hypothesis/ 51 | .pytest_cache/ 52 | cover/ 53 | 54 | # Translations 55 | *.mo 56 | *.pot 57 | 58 | # Django stuff: 59 | *.log 60 | local_settings.py 61 | db.sqlite3 62 | db.sqlite3-journal 63 | 64 | # Flask stuff: 65 | instance/ 66 | .webassets-cache 67 | 68 | # Scrapy stuff: 69 | .scrapy 70 | 71 | # Sphinx documentation 72 | docs/_build/ 73 | 74 | # PyBuilder 75 | .pybuilder/ 76 | target/ 77 | 78 | # Jupyter Notebook 79 | .ipynb_checkpoints 80 | 81 | # IPython 82 | profile_default/ 83 | ipython_config.py 84 | 85 | # pyenv 86 | # For a library or package, you might want to ignore these files since the code is 87 | # intended to run in multiple environments; otherwise, check them in: 88 | # .python-version 89 | 90 | # pipenv 91 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. 92 | # However, in case of collaboration, if having platform-specific dependencies or dependencies 93 | # having no cross-platform support, pipenv may install dependencies that don't work, or not 94 | # install all needed dependencies. 95 | #Pipfile.lock 96 | 97 | # poetry 98 | # Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control. 99 | # This is especially recommended for binary packages to ensure reproducibility, and is more 100 | # commonly ignored for libraries. 101 | # https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control 102 | #poetry.lock 103 | 104 | # pdm 105 | # Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control. 106 | #pdm.lock 107 | # pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it 108 | # in version control. 109 | # https://pdm.fming.dev/#use-with-ide 110 | .pdm.toml 111 | 112 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm 113 | __pypackages__/ 114 | 115 | # Celery stuff 116 | celerybeat-schedule 117 | celerybeat.pid 118 | 119 | # SageMath parsed files 120 | *.sage.py 121 | 122 | # Environments 123 | .env 124 | .venv 125 | env/ 126 | venv/ 127 | ENV/ 128 | env.bak/ 129 | venv.bak/ 130 | 131 | # Spyder project settings 132 | .spyderproject 133 | .spyproject 134 | 135 | # Rope project settings 136 | .ropeproject 137 | 138 | # mkdocs documentation 139 | /site 140 | 141 | # mypy 142 | .mypy_cache/ 143 | .dmypy.json 144 | dmypy.json 145 | 146 | # Pyre type checker 147 | .pyre/ 148 | 149 | # pytype static type analyzer 150 | .pytype/ 151 | 152 | # Cython debug symbols 153 | cython_debug/ 154 | 155 | # PyCharm 156 | # JetBrains specific template is maintained in a separate JetBrains.gitignore that can 157 | # be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore 158 | # and can be added to the global gitignore or merged into this file. For a more nuclear 159 | # option (not recommended) you can uncomment the following to ignore the entire idea folder. 160 | #.idea/ 161 | 162 | 163 | youtube-downloader/audio/ 164 | youtube-downloader/musiccaps-public.csv 165 | qrcode-stable-diffusion/qr_code_output/ 166 | .DS_Store 167 | .vscode/ 168 | data/ -------------------------------------------------------------------------------- /qrcode-stable-diffusion/app.py: -------------------------------------------------------------------------------- 1 | import io 2 | import time 3 | from pathlib import Path 4 | 5 | from modal import Image, App, enter, method 6 | 7 | 8 | app = App("stable-diffusion-qrcode-cli") 9 | cache_dir = "/vol/cache" 10 | 11 | 12 | def download_model(): 13 | import tempfile 14 | 15 | import diffusers 16 | import torch 17 | 18 | with tempfile.TemporaryDirectory() as temp_dir: 19 | controlnet = diffusers.ControlNetModel.from_pretrained( 20 | "DionTimmer/controlnet_qrcode-control_v1p_sd15", torch_dtype=torch.float16, cache_dir=temp_dir 21 | ) 22 | pipe = diffusers.StableDiffusionControlNetImg2ImgPipeline.from_pretrained( 23 | "runwayml/stable-diffusion-v1-5", 24 | controlnet=controlnet, 25 | safety_checker=None, 26 | torch_dtype=torch.float16, 27 | cache_dir=temp_dir, 28 | ) 29 | pipe.scheduler = diffusers.DPMSolverMultistepScheduler.from_config( 30 | pipe.scheduler.config, use_karras=True, algorithm_type="sde-dpmsolver++", cache_dir=temp_dir 31 | ) 32 | pipe.save_pretrained(cache_dir, safe_serialization=True) 33 | 34 | 35 | image = ( 36 | Image.debian_slim(python_version="3.10") 37 | .pip_install( 38 | "torch==2.0.1+cu117", 39 | find_links="https://download.pytorch.org/whl/torch_stable.html", 40 | ) 41 | .pip_install( 42 | "diffusers", 43 | "transformers", 44 | "accelerate", 45 | "xformers", 46 | "Pillow", 47 | "qrcode", 48 | "hf-transfer", 49 | ) 50 | .env({"HF_HUB_CACHE": cache_dir, "HF_HUB_ENABLE_HF_TRANSER": "1"}) 51 | .run_function(download_model) 52 | ) 53 | app.image = image 54 | 55 | 56 | def resize_for_condition_image(input_image, resolution: int): 57 | from PIL.Image import LANCZOS 58 | 59 | input_image = input_image.convert("RGB") 60 | W, H = input_image.size 61 | k = float(resolution) / min(H, W) 62 | H *= k 63 | W *= k 64 | H = int(round(H / 64.0)) * 64 65 | W = int(round(W / 64.0)) * 64 66 | img = input_image.resize((W, H), resample=LANCZOS) 67 | return img 68 | 69 | 70 | @app.cls(gpu="A10G") 71 | class StableDiffusion: 72 | 73 | @enter() 74 | def on_start(self): 75 | import diffusers 76 | import torch 77 | 78 | torch.backends.cuda.matmul.allow_tf32 = True 79 | self.pipe = diffusers.StableDiffusionControlNetImg2ImgPipeline.from_pretrained( 80 | cache_dir, torch_dtype=torch.float16 81 | ).to("cuda") 82 | self.pipe.enable_xformers_memory_efficient_attention() 83 | 84 | def generate_qrcode(self, qr_code_content): 85 | import qrcode 86 | 87 | print("Generating QR Code from content") 88 | qr = qrcode.QRCode( 89 | version=1, 90 | error_correction=qrcode.constants.ERROR_CORRECT_H, 91 | box_size=10, 92 | border=4, 93 | ) 94 | qr.add_data(qr_code_content) 95 | qr.make(fit=True) 96 | 97 | qrcode_image = qr.make_image(fill_color="black", back_color="white") 98 | qrcode_image = resize_for_condition_image(qrcode_image, 768) 99 | return qrcode_image 100 | 101 | @method() 102 | def run_inference( 103 | self, 104 | prompt: str, 105 | qr_code_content: str, 106 | num_inference_steps: int = 30, 107 | negative_prompt: str = None, 108 | guidance_scale: float = 7.5, 109 | controlnet_conditioning_scale: float = 1.5, # 1.3, 1.11, 1.5 110 | strength: float = 0.9, # 0.8 111 | seed: int = -1, 112 | num_images_per_prompt: int = 1, 113 | ) -> list[bytes]: 114 | import torch 115 | 116 | seed = torch.randint(0, 2**32, (1,)).item() if seed == -1 else seed 117 | qrcode_image = self.generate_qrcode(qr_code_content) 118 | out = self.pipe( 119 | prompt=[prompt] * num_images_per_prompt, 120 | negative_prompt=[negative_prompt] * num_images_per_prompt, 121 | image=[qrcode_image] * num_images_per_prompt, 122 | control_image=[qrcode_image] * num_images_per_prompt, 123 | width=768, 124 | height=768, 125 | guidance_scale=float(guidance_scale), 126 | controlnet_conditioning_scale=float(controlnet_conditioning_scale), 127 | generator=torch.Generator().manual_seed(seed), 128 | strength=float(strength), 129 | num_inference_steps=num_inference_steps, 130 | ) 131 | # Convert to PNG bytes 132 | image_output = [] 133 | for image in out.images: 134 | with io.BytesIO() as buf: 135 | image.save(buf, format="PNG") 136 | image_output.append(buf.getvalue()) 137 | return image_output 138 | 139 | 140 | @app.local_entrypoint() 141 | def entrypoint( 142 | prompt: str, 143 | qrcode_content: str, 144 | negative_prompt: str = "ugly, disfigured, low quality, blurry, nsfw", 145 | steps: int = 40, 146 | samples: int = 1, 147 | guidance_scale: float = 7.5, 148 | controlnet_conditioning_scale: float = 1.5, # 1.3, 1.11, 1.5 149 | strength: float = 0.9, # 0.8 150 | seed: int = -1, 151 | ): 152 | """Local entrypoint that you can use from the CLI to generate QR Code images via a Modal app. 153 | Example: 154 | modal run run.py \ 155 | --prompt "ramen noodle soup, animated by studio ghibli, vivid colors" \ 156 | --qrcode-content "https://modal.com" \ 157 | --samples 4 \ 158 | --steps 40 159 | Args: 160 | prompt (str): 161 | A text prompt to generate the QR Code image. 162 | qrcode_content (str): 163 | The URL or content to encode in the QR Code. 164 | negative_prompt (str, optional): 165 | Negative prompts to use when generating the image. 166 | Defaults to "ugly, disfigured, low quality, blurry, nsfw". 167 | steps (int, optional): 168 | Number of inference steps in diffusion process. Defaults to 40. 169 | samples (int, optional): 170 | Number of images to generate. Defaults to 1. 171 | guidance_scale (float, optional): 172 | Guidance scale as defined in [Classifier-Free Diffusion Guidance](https://arxiv.org/abs/2207.12598). 173 | Defaults to 7.5. 174 | controlnet_conditioning_scale (float, optional): 175 | The outputs of the controlnet are multiplied by `controlnet_conditioning_scale` before they are added 176 | to the residual in the original unet. If multiple ControlNets are specified in init, you can set the 177 | corresponding scale as a list. Defaults to 1.5. 178 | strength (float, optional): 179 | Conceptually, indicates how much to transform the masked portion of the reference image. Must be 180 | between 0 and 1. Defaults to 0.9. 181 | seed (int, optional): 182 | Random seed to enforce reproducibility. If set to -1, we pick a random number as the seed. Defaults to -1. 183 | """ 184 | 185 | print(f"prompt => {prompt}, qrcode_content => {qrcode_content}, steps => {steps}, samples => {samples}") 186 | 187 | dir = Path("./qr_code_output") 188 | if not dir.exists(): 189 | dir.mkdir(exist_ok=True, parents=True) 190 | 191 | sd = StableDiffusion() 192 | t0 = time.time() 193 | images = sd.run_inference.remote( 194 | prompt, 195 | qrcode_content, 196 | num_inference_steps=steps, 197 | num_images_per_prompt=samples, 198 | negative_prompt=negative_prompt, 199 | guidance_scale=guidance_scale, 200 | controlnet_conditioning_scale=controlnet_conditioning_scale, 201 | strength=strength, 202 | seed=seed, 203 | ) 204 | total_time = time.time() - t0 205 | print(f"Took {total_time:.3f}s ({(total_time)/len(images):.3f}s / image).") 206 | for j, image_bytes in enumerate(images): 207 | output_path = dir / f"output_{j}.png" 208 | print(f"Saving it to {output_path}") 209 | with open(output_path, "wb") as f: 210 | f.write(image_bytes) 211 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "[]" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright [yyyy] [name of copyright owner] 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | --------------------------------------------------------------------------------