├── .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 |
--------------------------------------------------------------------------------