├── .github └── workflows │ └── python-publish.yml ├── .gitignore ├── .pre-commit-config.yaml ├── .vscode └── launch.json ├── DOCUMENTATION.md ├── LICENSE ├── Makefile ├── README.md ├── requirements.txt ├── setup.cfg ├── setup.py ├── src └── BingImageCreator.py └── test └── test_example.py /.github/workflows/python-publish.yml: -------------------------------------------------------------------------------- 1 | name: Upload Python Package 2 | 3 | on: 4 | release: 5 | types: [published] 6 | pull_request: 7 | push: 8 | 9 | jobs: 10 | deploy: 11 | runs-on: ubuntu-latest 12 | permissions: 13 | contents: read 14 | steps: 15 | - uses: actions/checkout@v3 16 | - name: Set up Python 17 | uses: actions/setup-python@v4 18 | with: 19 | python-version: '3.11.0' 20 | - name: Install dependencies 21 | run: | 22 | python -m pip install --upgrade pip 23 | python -m pip install build 24 | - name: Build package 25 | run: python -m build 26 | - name: Publish package 27 | if: github.event_name == 'release' 28 | uses: pypa/gh-action-pypi-publish@27b31702a0e7fc50959f5ad993c78deac1bdfc29 29 | with: 30 | user: __token__ 31 | password: ${{ secrets.PYPI_API_TOKEN }} 32 | -------------------------------------------------------------------------------- /.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 | pip-wheel-metadata/ 24 | share/python-wheels/ 25 | *.egg-info/ 26 | .installed.cfg 27 | *.egg 28 | MANIFEST 29 | 30 | # PyInstaller 31 | # Usually these files are written by a python script from a template 32 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 33 | *.manifest 34 | *.spec 35 | 36 | # Installer logs 37 | pip-log.txt 38 | pip-delete-this-directory.txt 39 | 40 | # Unit test / coverage reports 41 | htmlcov/ 42 | .tox/ 43 | .nox/ 44 | .coverage 45 | .coverage.* 46 | .cache 47 | nosetests.xml 48 | coverage.xml 49 | *.cover 50 | *.py,cover 51 | .hypothesis/ 52 | .pytest_cache/ 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 | target/ 76 | 77 | # Jupyter Notebook 78 | .ipynb_checkpoints 79 | 80 | # IPython 81 | profile_default/ 82 | ipython_config.py 83 | 84 | # pyenv 85 | .python-version 86 | 87 | # pipenv 88 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. 89 | # However, in case of collaboration, if having platform-specific dependencies or dependencies 90 | # having no cross-platform support, pipenv may install dependencies that don't work, or not 91 | # install all needed dependencies. 92 | #Pipfile.lock 93 | 94 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow 95 | __pypackages__/ 96 | 97 | # Celery stuff 98 | celerybeat-schedule 99 | celerybeat.pid 100 | 101 | # SageMath parsed files 102 | *.sage.py 103 | 104 | # Environments 105 | .env 106 | .venv 107 | env/ 108 | venv/ 109 | ENV/ 110 | env.bak/ 111 | venv.bak/ 112 | 113 | # Spyder project settings 114 | .spyderproject 115 | .spyproject 116 | 117 | # Rope project settings 118 | .ropeproject 119 | 120 | # mkdocs documentation 121 | /site 122 | 123 | # mypy 124 | .mypy_cache/ 125 | .dmypy.json 126 | dmypy.json 127 | 128 | # Pyre type checker 129 | .pyre/ 130 | output/* 131 | cookies.json 132 | -------------------------------------------------------------------------------- /.pre-commit-config.yaml: -------------------------------------------------------------------------------- 1 | repos: 2 | - repo: https://github.com/asottile/reorder_python_imports 3 | rev: v3.9.0 4 | hooks: 5 | - id: reorder-python-imports 6 | args: [--py37-plus] 7 | - repo: https://github.com/asottile/add-trailing-comma 8 | rev: v2.3.0 9 | hooks: 10 | - id: add-trailing-comma 11 | args: [--py36-plus] 12 | - repo: https://github.com/asottile/pyupgrade 13 | rev: v3.3.1 14 | hooks: 15 | - id: pyupgrade 16 | args: [--py37-plus] 17 | 18 | - repo: https://github.com/pre-commit/pre-commit-hooks 19 | rev: v4.4.0 20 | hooks: 21 | - id: trailing-whitespace 22 | - id: end-of-file-fixer 23 | - id: check-yaml 24 | - id: debug-statements 25 | - id: double-quote-string-fixer 26 | - id: name-tests-test 27 | - id: requirements-txt-fixer 28 | - repo: https://github.com/psf/black 29 | rev: 22.10.0 30 | hooks: 31 | - id: black 32 | -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | // Use IntelliSense to learn about possible attributes. 3 | // Hover to view descriptions of existing attributes. 4 | // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 5 | "version": "0.2.0", 6 | "configurations": [ 7 | { 8 | "name": "Python: Module", 9 | "type": "python", 10 | "request": "launch", 11 | "module": "pytest", 12 | "justMyCode": true 13 | } 14 | ] 15 | } 16 | -------------------------------------------------------------------------------- /DOCUMENTATION.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | # BingImageCreator 4 | 5 | 6 | 7 | #### debug 8 | 9 | ```python 10 | def debug(debug_file, text_var) 11 | ``` 12 | 13 | helper function for debug 14 | 15 | 16 | 17 | ## ImageGen Objects 18 | 19 | ```python 20 | class ImageGen() 21 | ``` 22 | 23 | Image generation by Microsoft Bing 24 | 25 | **Arguments**: 26 | 27 | - `auth_cookie` - str 28 | Optional Parameters: 29 | - `debug_file` - str 30 | - `quiet` - bool 31 | - `all_cookies` - List[Dict] 32 | 33 | 34 | 35 | #### get\_images 36 | 37 | ```python 38 | def get_images(prompt: str) -> list 39 | ``` 40 | 41 | Fetches image links from Bing 42 | 43 | **Arguments**: 44 | 45 | - `prompt` - str 46 | 47 | 48 | 49 | #### save\_images 50 | 51 | ```python 52 | def save_images(links: list, output_dir: str, file_name: str = None) -> None 53 | ``` 54 | 55 | Saves images to output directory 56 | 57 | **Arguments**: 58 | 59 | - `links` - list[str] 60 | - `output_dir` - str 61 | - `file_name` - str 62 | 63 | 64 | 65 | ## ImageGenAsync Objects 66 | 67 | ```python 68 | class ImageGenAsync() 69 | ``` 70 | 71 | Image generation by Microsoft Bing 72 | 73 | **Arguments**: 74 | 75 | - `auth_cookie` - str 76 | Optional Parameters: 77 | - `debug_file` - str 78 | - `quiet` - bool 79 | - `all_cookies` - list[dict] 80 | 81 | 82 | 83 | #### get\_images 84 | 85 | ```python 86 | async def get_images(prompt: str) -> list 87 | ``` 88 | 89 | Fetches image links from Bing 90 | 91 | **Arguments**: 92 | 93 | - `prompt` - str 94 | 95 | 96 | 97 | #### save\_images 98 | 99 | ```python 100 | async def save_images(links: list, 101 | output_dir: str, 102 | file_name: str = None) -> None 103 | ``` 104 | 105 | Saves images to output directory 106 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | This is free and unencumbered software released into the public domain. 2 | 3 | Anyone is free to copy, modify, publish, use, compile, sell, or 4 | distribute this software, either in source code form or as a compiled 5 | binary, for any purpose, commercial or non-commercial, and by any 6 | means. 7 | 8 | In jurisdictions that recognize copyright laws, the author or authors 9 | of this software dedicate any and all copyright interest in the 10 | software to the public domain. We make this dedication for the benefit 11 | of the public at large and to the detriment of our heirs and 12 | successors. We intend this dedication to be an overt act of 13 | relinquishment in perpetuity of all present and future rights to this 14 | software under copyright law. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 17 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 18 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. 19 | IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR 20 | OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, 21 | ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR 22 | OTHER DEALINGS IN THE SOFTWARE. 23 | 24 | For more information, please refer to 25 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | .PHONY: docs 2 | init: 3 | python -m pip install --upgrade pip 4 | python -m pip install -r ./requirements.txt --upgrade 5 | python -m pip install build setuptools wheel flake8 --upgrade 6 | build: 7 | python -m build 8 | ci: 9 | python -m flake8 src --count --exit-zero --max-complexity=10 --max-line-length=127 --statistics 10 | python -m flake8 src --count --select=E9,F63,F7,F82 --show-source --statistics 11 | python setup.py install 12 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # BingImageCreator 2 | High quality image generation by Microsoft. Reverse engineered API. 3 | 4 | `pip3 install --upgrade BingImageCreator` 5 | 6 | ``` 7 | $ python3 -m BingImageCreator -h 8 | usage: BingImageCreator.py [-h] -U U --prompt PROMPT [--output-dir OUTPUT_DIR] 9 | 10 | options: 11 | -h, --help show this help message and exit 12 | -U U Auth cookie from browser 13 | --prompt PROMPT Prompt to generate images for 14 | --asyncio Use async to sync png 15 | --output-dir OUTPUT_DIR 16 | Output directory 17 | ``` 18 | 19 | [Developer Documentation](https://github.com/acheong08/BingImageCreator/blob/main/DOCUMENTATION.md) 20 | 21 | 22 | ## Getting authentication 23 | ### Chromium based browsers (Edge, Opera, Vivaldi, Brave) 24 | - Go to https://bing.com/. 25 | - F12 to open console 26 | - In the JavaScript console, type `cookieStore.get("_U").then(result => console.log(result.value))` and press enter 27 | - Copy the output. This is used in `--U` or `auth_cookie`. 28 | 29 | ### Firefox 30 | - Go to https://bing.com/. 31 | - F12 to open developer tools 32 | - navigate to the storage tab 33 | - expand the cookies tab 34 | - click on the `https://bing.com` cookie 35 | - copy the value from the `_U` 36 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | aiohttp 2 | regex 3 | requests 4 | -------------------------------------------------------------------------------- /setup.cfg: -------------------------------------------------------------------------------- 1 | [metadata] 2 | description_file=README.md 3 | license_files=LICENSE 4 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | from setuptools import find_packages 2 | from setuptools import setup 3 | 4 | setup( 5 | name="BingImageCreator", 6 | version="0.5.0", 7 | license="GNU General Public License v2.0", 8 | author="Antonio Cheong", 9 | author_email="acheong@student.dalat.org", 10 | description="High quality image generation by Microsoft. Reverse engineered API.", 11 | packages=find_packages("src"), 12 | package_dir={"": "src"}, 13 | url="https://github.com/acheong08/BingImageCreator", 14 | project_urls={ 15 | "Bug Report": "https://github.com/acheong08/BingImageCreator/issues/new", 16 | }, 17 | install_requires=[ 18 | "httpx", 19 | "regex", 20 | "requests", 21 | ], 22 | long_description=open("README.md", encoding="utf-8").read(), 23 | long_description_content_type="text/markdown", 24 | py_modules=["BingImageCreator"], 25 | classifiers=[ 26 | "License :: OSI Approved :: GNU General Public License v2 (GPLv2)", 27 | "Intended Audience :: Developers", 28 | "Topic :: Software Development :: Libraries :: Python Modules", 29 | "Programming Language :: Python :: 3.8", 30 | "Programming Language :: Python :: 3.9", 31 | "Programming Language :: Python :: 3.10", 32 | "Programming Language :: Python :: 3.11", 33 | ], 34 | ) 35 | -------------------------------------------------------------------------------- /src/BingImageCreator.py: -------------------------------------------------------------------------------- 1 | import argparse 2 | import asyncio 3 | import contextlib 4 | import json 5 | import os 6 | import random 7 | import sys 8 | import time 9 | from functools import partial 10 | from typing import Dict 11 | from typing import List 12 | from typing import Union 13 | 14 | import httpx 15 | import pkg_resources 16 | import regex 17 | import requests 18 | 19 | BING_URL = os.getenv("BING_URL", "https://www.bing.com") 20 | # Generate random IP between range 13.104.0.0/14 21 | FORWARDED_IP = ( 22 | f"13.{random.randint(104, 107)}.{random.randint(0, 255)}.{random.randint(0, 255)}" 23 | ) 24 | HEADERS = { 25 | "accept": "text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7", 26 | "accept-language": "en-US,en;q=0.9", 27 | "cache-control": "max-age=0", 28 | "content-type": "application/x-www-form-urlencoded", 29 | "referrer": "https://www.bing.com/images/create/", 30 | "origin": "https://www.bing.com", 31 | "user-agent": "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/110.0.0.0 Safari/537.36 Edg/110.0.1587.63", 32 | "x-forwarded-for": FORWARDED_IP, 33 | } 34 | 35 | # Error messages 36 | error_timeout = "Your request has timed out." 37 | error_redirect = "Redirect failed" 38 | error_blocked_prompt = ( 39 | "Your prompt has been blocked by Bing. Try to change any bad words and try again." 40 | ) 41 | error_being_reviewed_prompt = "Your prompt is being reviewed by Bing. Try to change any sensitive words and try again." 42 | error_noresults = "Could not get results" 43 | error_unsupported_lang = "\nthis language is currently not supported by bing" 44 | error_bad_images = "Bad images" 45 | error_no_images = "No images" 46 | # Action messages 47 | sending_message = "Sending request..." 48 | wait_message = "Waiting for results..." 49 | download_message = "\nDownloading images..." 50 | 51 | 52 | def debug(debug_file, text_var): 53 | """helper function for debug""" 54 | with open(f"{debug_file}", "a", encoding="utf-8") as f: 55 | f.write(str(text_var)) 56 | f.write("\n") 57 | 58 | 59 | class ImageGen: 60 | """ 61 | Image generation by Microsoft Bing 62 | Parameters: 63 | auth_cookie: str 64 | auth_cookie_SRCHHPGUSR: str 65 | Optional Parameters: 66 | debug_file: str 67 | quiet: bool 68 | all_cookies: List[Dict] 69 | """ 70 | 71 | def __init__( 72 | self, 73 | auth_cookie: str, 74 | auth_cookie_SRCHHPGUSR: str, 75 | debug_file: Union[str, None] = None, 76 | quiet: bool = False, 77 | all_cookies: List[Dict] = None, 78 | ) -> None: 79 | self.session: requests.Session = requests.Session() 80 | self.session.headers = HEADERS 81 | self.session.cookies.set("_U", auth_cookie) 82 | self.session.cookies.set("SRCHHPGUSR", auth_cookie_SRCHHPGUSR) 83 | if all_cookies: 84 | for cookie in all_cookies: 85 | self.session.cookies.set(cookie["name"], cookie["value"]) 86 | self.quiet = quiet 87 | self.debug_file = debug_file 88 | if self.debug_file: 89 | self.debug = partial(debug, self.debug_file) 90 | 91 | def get_images(self, prompt: str) -> list: 92 | """ 93 | Fetches image links from Bing 94 | Parameters: 95 | prompt: str 96 | """ 97 | if not self.quiet: 98 | print(sending_message) 99 | if self.debug_file: 100 | self.debug(sending_message) 101 | url_encoded_prompt = requests.utils.quote(prompt) 102 | payload = f"q={url_encoded_prompt}&qs=ds" 103 | # https://www.bing.com/images/create?q=&rt=3&FORM=GENCRE 104 | url = f"{BING_URL}/images/create?q={url_encoded_prompt}&rt=4&FORM=GENCRE" 105 | response = self.session.post( 106 | url, 107 | allow_redirects=False, 108 | data=payload, 109 | timeout=200, 110 | ) 111 | # check for content waring message 112 | if "this prompt is being reviewed" in response.text.lower(): 113 | if self.debug_file: 114 | self.debug(f"ERROR: {error_being_reviewed_prompt}") 115 | raise Exception( 116 | error_being_reviewed_prompt, 117 | ) 118 | if "this prompt has been blocked" in response.text.lower(): 119 | if self.debug_file: 120 | self.debug(f"ERROR: {error_blocked_prompt}") 121 | raise Exception( 122 | error_blocked_prompt, 123 | ) 124 | if ( 125 | "we're working hard to offer image creator in more languages" 126 | in response.text.lower() 127 | ): 128 | if self.debug_file: 129 | self.debug(f"ERROR: {error_unsupported_lang}") 130 | raise Exception(error_unsupported_lang) 131 | if response.status_code != 302: 132 | # if rt4 fails, try rt3 133 | url = f"{BING_URL}/images/create?q={url_encoded_prompt}&rt=3&FORM=GENCRE" 134 | response = self.session.post(url, allow_redirects=False, timeout=200) 135 | if response.status_code != 302: 136 | if self.debug_file: 137 | self.debug(f"ERROR: {error_redirect}") 138 | print(f"ERROR: {response.text}") 139 | raise Exception(error_redirect) 140 | # Get redirect URL 141 | redirect_url = response.headers["Location"].replace("&nfy=1", "") 142 | request_id = redirect_url.split("id=")[-1] 143 | self.session.get(f"{BING_URL}{redirect_url}") 144 | # https://www.bing.com/images/create/async/results/{ID}?q={PROMPT} 145 | polling_url = f"{BING_URL}/images/create/async/results/{request_id}?q={url_encoded_prompt}" 146 | # Poll for results 147 | if self.debug_file: 148 | self.debug("Polling and waiting for result") 149 | if not self.quiet: 150 | print("Waiting for results...") 151 | start_wait = time.time() 152 | while True: 153 | if int(time.time() - start_wait) > 200: 154 | if self.debug_file: 155 | self.debug(f"ERROR: {error_timeout}") 156 | raise Exception(error_timeout) 157 | if not self.quiet: 158 | print(".", end="", flush=True) 159 | response = self.session.get(polling_url) 160 | if response.status_code != 200: 161 | if self.debug_file: 162 | self.debug(f"ERROR: {error_noresults}") 163 | raise Exception(error_noresults) 164 | if not response.text or response.text.find("errorMessage") != -1: 165 | time.sleep(1) 166 | continue 167 | else: 168 | break 169 | # Use regex to search for src="" 170 | image_links = regex.findall(r'src="([^"]+)"', response.text) 171 | # Remove size limit 172 | normal_image_links = [link.split("?w=")[0] for link in image_links] 173 | # Remove duplicates 174 | normal_image_links = list(set(normal_image_links)) 175 | 176 | # Bad images 177 | bad_images = [ 178 | "https://r.bing.com/rp/in-2zU3AJUdkgFe7ZKv19yPBHVs.png", 179 | "https://r.bing.com/rp/TX9QuO3WzcCJz1uaaSwQAz39Kb0.jpg", 180 | ] 181 | for img in normal_image_links: 182 | if img in bad_images: 183 | raise Exception("Bad images") 184 | # No images 185 | if not normal_image_links: 186 | raise Exception(error_no_images) 187 | return normal_image_links 188 | 189 | def save_images( 190 | self, 191 | links: list, 192 | output_dir: str, 193 | file_name: str = None, 194 | download_count: int = None, 195 | ) -> None: 196 | """ 197 | Saves images to output directory 198 | Parameters: 199 | links: list[str] 200 | output_dir: str 201 | file_name: str 202 | download_count: int 203 | """ 204 | if self.debug_file: 205 | self.debug(download_message) 206 | if not self.quiet: 207 | print(download_message) 208 | with contextlib.suppress(FileExistsError): 209 | os.mkdir(output_dir) 210 | try: 211 | fn = f"{file_name}_" if file_name else "" 212 | jpeg_index = 0 213 | 214 | if download_count: 215 | links = links[:download_count] 216 | 217 | for link in links: 218 | while os.path.exists( 219 | os.path.join(output_dir, f"{fn}{jpeg_index}.jpeg") 220 | ): 221 | jpeg_index += 1 222 | response = self.session.get(link) 223 | if response.status_code != 200: 224 | raise Exception("Could not download image") 225 | # save response to file 226 | with open( 227 | os.path.join(output_dir, f"{fn}{jpeg_index}.jpeg"), "wb" 228 | ) as output_file: 229 | output_file.write(response.content) 230 | jpeg_index += 1 231 | 232 | except requests.exceptions.MissingSchema as url_exception: 233 | raise Exception( 234 | "Inappropriate contents found in the generated images. Please try again or try another prompt.", 235 | ) from url_exception 236 | 237 | 238 | class ImageGenAsync: 239 | """ 240 | Image generation by Microsoft Bing 241 | Parameters: 242 | auth_cookie: str 243 | Optional Parameters: 244 | debug_file: str 245 | quiet: bool 246 | all_cookies: list[dict] 247 | """ 248 | 249 | def __init__( 250 | self, 251 | auth_cookie: str = None, 252 | debug_file: Union[str, None] = None, 253 | quiet: bool = False, 254 | all_cookies: List[Dict] = None, 255 | ) -> None: 256 | if auth_cookie is None and not all_cookies: 257 | raise Exception("No auth cookie provided") 258 | self.session = httpx.AsyncClient( 259 | headers=HEADERS, 260 | trust_env=True, 261 | ) 262 | if auth_cookie: 263 | self.session.cookies.update({"_U": auth_cookie}) 264 | if all_cookies: 265 | for cookie in all_cookies: 266 | self.session.cookies.update( 267 | {cookie["name"]: cookie["value"]}, 268 | ) 269 | self.quiet = quiet 270 | self.debug_file = debug_file 271 | if self.debug_file: 272 | self.debug = partial(debug, self.debug_file) 273 | 274 | async def __aenter__(self): 275 | return self 276 | 277 | async def __aexit__(self, *excinfo) -> None: 278 | await self.session.aclose() 279 | 280 | async def get_images(self, prompt: str) -> list: 281 | """ 282 | Fetches image links from Bing 283 | Parameters: 284 | prompt: str 285 | """ 286 | if not self.quiet: 287 | print("Sending request...") 288 | url_encoded_prompt = requests.utils.quote(prompt) 289 | # https://www.bing.com/images/create?q=&rt=3&FORM=GENCRE 290 | url = f"{BING_URL}/images/create?q={url_encoded_prompt}&rt=3&FORM=GENCRE" 291 | payload = f"q={url_encoded_prompt}&qs=ds" 292 | response = await self.session.post( 293 | url, 294 | follow_redirects=False, 295 | data=payload, 296 | ) 297 | content = response.text 298 | if "this prompt has been blocked" in content.lower(): 299 | raise Exception( 300 | "Your prompt has been blocked by Bing. Try to change any bad words and try again.", 301 | ) 302 | if response.status_code != 302: 303 | # if rt4 fails, try rt3 304 | url = f"{BING_URL}/images/create?q={url_encoded_prompt}&rt=4&FORM=GENCRE" 305 | response = await self.session.post( 306 | url, 307 | follow_redirects=False, 308 | timeout=200, 309 | ) 310 | if response.status_code != 302: 311 | print(f"ERROR: {response.text}") 312 | raise Exception("Redirect failed") 313 | # Get redirect URL 314 | redirect_url = response.headers["Location"].replace("&nfy=1", "") 315 | request_id = redirect_url.split("id=")[-1] 316 | await self.session.get(f"{BING_URL}{redirect_url}") 317 | # https://www.bing.com/images/create/async/results/{ID}?q={PROMPT} 318 | polling_url = f"{BING_URL}/images/create/async/results/{request_id}?q={url_encoded_prompt}" 319 | # Poll for results 320 | if not self.quiet: 321 | print("Waiting for results...") 322 | while True: 323 | if not self.quiet: 324 | print(".", end="", flush=True) 325 | # By default, timeout is 300s, change as needed 326 | response = await self.session.get(polling_url) 327 | if response.status_code != 200: 328 | raise Exception("Could not get results") 329 | content = response.text 330 | if content and content.find("errorMessage") == -1: 331 | break 332 | 333 | await asyncio.sleep(1) 334 | continue 335 | # Use regex to search for src="" 336 | image_links = regex.findall(r'src="([^"]+)"', content) 337 | # Remove size limit 338 | normal_image_links = [link.split("?w=")[0] for link in image_links] 339 | # Remove duplicates 340 | normal_image_links = list(set(normal_image_links)) 341 | 342 | # Bad images 343 | bad_images = [ 344 | "https://r.bing.com/rp/in-2zU3AJUdkgFe7ZKv19yPBHVs.png", 345 | "https://r.bing.com/rp/TX9QuO3WzcCJz1uaaSwQAz39Kb0.jpg", 346 | ] 347 | for im in normal_image_links: 348 | if im in bad_images: 349 | raise Exception("Bad images") 350 | # No images 351 | if not normal_image_links: 352 | raise Exception("No images") 353 | return normal_image_links 354 | 355 | async def save_images( 356 | self, 357 | links: list, 358 | output_dir: str, 359 | download_count: int, 360 | file_name: str = None, 361 | ) -> None: 362 | """ 363 | Saves images to output directory 364 | """ 365 | 366 | if self.debug_file: 367 | self.debug(download_message) 368 | if not self.quiet: 369 | print(download_message) 370 | with contextlib.suppress(FileExistsError): 371 | os.mkdir(output_dir) 372 | try: 373 | fn = f"{file_name}_" if file_name else "" 374 | jpeg_index = 0 375 | 376 | for link in links[:download_count]: 377 | while os.path.exists( 378 | os.path.join(output_dir, f"{fn}{jpeg_index}.jpeg") 379 | ): 380 | jpeg_index += 1 381 | response = await self.session.get(link) 382 | if response.status_code != 200: 383 | raise Exception("Could not download image") 384 | # save response to file 385 | with open( 386 | os.path.join(output_dir, f"{fn}{jpeg_index}.jpeg"), "wb" 387 | ) as output_file: 388 | output_file.write(response.content) 389 | jpeg_index += 1 390 | except httpx.InvalidURL as url_exception: 391 | raise Exception( 392 | "Inappropriate contents found in the generated images. Please try again or try another prompt.", 393 | ) from url_exception 394 | 395 | 396 | async def async_image_gen( 397 | prompt: str, 398 | download_count: int, 399 | output_dir: str, 400 | u_cookie=None, 401 | debug_file=None, 402 | quiet=False, 403 | all_cookies=None, 404 | ): 405 | async with ImageGenAsync( 406 | u_cookie, 407 | debug_file=debug_file, 408 | quiet=quiet, 409 | all_cookies=all_cookies, 410 | ) as image_generator: 411 | images = await image_generator.get_images(prompt) 412 | await image_generator.save_images( 413 | images, output_dir=output_dir, download_count=download_count 414 | ) 415 | 416 | 417 | def main(): 418 | parser = argparse.ArgumentParser() 419 | parser.add_argument("-U", help="Auth cookie from browser", type=str) 420 | parser.add_argument("--cookie-file", help="File containing auth cookie", type=str) 421 | parser.add_argument( 422 | "--prompt", 423 | help="Prompt to generate images for", 424 | type=str, 425 | required=True, 426 | ) 427 | 428 | parser.add_argument( 429 | "--output-dir", 430 | help="Output directory", 431 | type=str, 432 | default="./output", 433 | ) 434 | 435 | parser.add_argument( 436 | "--download-count", 437 | help="Number of images to download, value must be less than five", 438 | type=int, 439 | default=4, 440 | ) 441 | 442 | parser.add_argument( 443 | "--debug-file", 444 | help="Path to the file where debug information will be written.", 445 | type=str, 446 | ) 447 | 448 | parser.add_argument( 449 | "--quiet", 450 | help="Disable pipeline messages", 451 | action="store_true", 452 | ) 453 | parser.add_argument( 454 | "--asyncio", 455 | help="Run ImageGen using asyncio", 456 | action="store_true", 457 | ) 458 | parser.add_argument( 459 | "--version", 460 | action="store_true", 461 | help="Print the version number", 462 | ) 463 | 464 | args = parser.parse_args() 465 | 466 | if args.version: 467 | print(pkg_resources.get_distribution("BingImageCreator").version) 468 | sys.exit() 469 | 470 | # Load auth cookie 471 | cookie_json = None 472 | if args.cookie_file is not None: 473 | with contextlib.suppress(Exception): 474 | with open(args.cookie_file, encoding="utf-8") as file: 475 | cookie_json = json.load(file) 476 | 477 | if args.U is None and args.cookie_file is None: 478 | raise Exception("Could not find auth cookie") 479 | 480 | if args.download_count > 4: 481 | raise Exception("The number of downloads must be less than five") 482 | 483 | if not args.asyncio: 484 | # Create image generator 485 | image_generator = ImageGen( 486 | args.U, 487 | args.debug_file, 488 | args.quiet, 489 | all_cookies=cookie_json, 490 | ) 491 | image_generator.save_images( 492 | image_generator.get_images(args.prompt), 493 | output_dir=args.output_dir, 494 | download_count=args.download_count, 495 | ) 496 | else: 497 | asyncio.run( 498 | async_image_gen( 499 | args.prompt, 500 | args.download_count, 501 | args.output_dir, 502 | args.U, 503 | args.debug_file, 504 | args.quiet, 505 | all_cookies=cookie_json, 506 | ), 507 | ) 508 | 509 | 510 | if __name__ == "__main__": 511 | main() 512 | -------------------------------------------------------------------------------- /test/test_example.py: -------------------------------------------------------------------------------- 1 | import os 2 | import shutil 3 | 4 | from src.BingImageCreator import ImageGen 5 | 6 | 7 | def test_save_images(): 8 | # create a temporary output directory for testing purposes 9 | test_output_dir = "test_output" 10 | os.mkdir(test_output_dir) 11 | # download a test image 12 | test_image_url = "https://picsum.photos/200" 13 | gen = ImageGen(auth_cookie="") 14 | gen.save_images([test_image_url], test_output_dir) 15 | gen.save_images([test_image_url], test_output_dir, file_name="test_image") 16 | # check if the image was downloaded and saved correctly 17 | assert os.path.exists(os.path.join(test_output_dir, "test_image_0.jpeg")) 18 | assert os.path.exists(os.path.join(test_output_dir, "0.jpeg")) 19 | # remove the temporary output directory 20 | shutil.rmtree(test_output_dir) 21 | --------------------------------------------------------------------------------