Skip to main content

From 20 KB/s to High-Speed Downloads: How I Built a Bulk ChatGPT Image Downloader

A Real-World Engineering Case Study in API Discovery, Authentication, Pagination, Virtualized UIs and Parallel Downloads There is a point in every software project where a seemingly simple problem turns into…

From 20 KB/s to High-Speed Downloads: How I Built a Bulk ChatGPT Image Downloader

COVER // FROM 20 KB/S TO HIGH-SPEED DOWNLOADS: HOW I BUILT A BULK CHATGPT IMAGE DOWNLOADER

Table of Contents

A Real-World Engineering Case Study in API Discovery, Authentication, Pagination, Virtualized UIs and Parallel Downloads

There is a point in every software project where a seemingly simple problem turns into a surprisingly interesting engineering challenge.

For me, that happened with something that initially sounded almost trivial:

“I have generated a huge number of images in ChatGPT. How can I download all of them efficiently to my local computer?”

At first glance, the problem seems simple.

Open the ChatGPT Images page.

Find the images.

Download them.

Done.

Except it wasn’t.

The ChatGPT Images interface is designed as a modern, dynamic, virtualized application. The browser does not simply contain thousands of <img> elements waiting to be scraped. Authentication is not as straightforward as copying a public URL. And even after discovering the underlying image data API, a naïve implementation produced download speeds of only around 20 KB/s, despite having an Internet connection capable of roughly 10 MB/s.

That turned a simple downloader into an interesting engineering exercise.

This article documents that journey.

Not as a generic “scraping tutorial,” but as a real-world case study in debugging a modern web application.

The final architecture became dramatically faster by separating the problem into two completely different jobs:

  • Browser: authentication and API discovery
  • Python: high-speed direct image downloading

The result was a downloader capable of processing the image history in batches and downloading multiple images concurrently.

I am intentionally not publishing the complete production code in this article. I will show selected implementation concepts and code fragments, but the complete working implementation is something I can provide separately to people who have a genuine technical requirement.


1. Why Would Anyone Need a Bulk ChatGPT Image Downloader?

If you generate images occasionally, downloading them manually is perfectly reasonable.

Generate an image.

Click it.

Save it.

Move on.

But that workflow becomes painful when you have hundreds or thousands of generated images.

In my own case, the requirement was essentially:

I have a large collection of generated images inside ChatGPT and want to maintain a local archive of them.

The destination was a normal Windows directory:

C:\WORKS\chatgpt images

The requirements were simple:

Requirement 1 — Download everything

Not just the images currently visible on the screen.

Requirement 2 — Don’t download duplicates

The downloader should be able to run again without downloading everything from scratch.

Requirement 3 — Don’t rely on manually clicking images

That would defeat the entire purpose.

Requirement 4 — Work with a large image library

The solution should not assume that there are only 20 or 50 images.

Requirement 5 — Be fast

This became particularly important.

There is little point in automating a download if the automated system downloads at a tiny fraction of the available network speed.


2. The First Idea: Scrape the Images From the Page

The obvious first approach was browser automation.

Open:

https://chatgpt.com/images

Then inspect the page.

A first attempt looks conceptually like this:

images = page.locator("img")

for image in images:
    url = image.get_attribute("src")

This is the standard approach people use for many image galleries.

And it works surprisingly often.

But here it didn’t solve the problem.


3. The Virtualized Gallery Problem

The first major discovery was that the Images gallery is not a traditional static HTML gallery.

Modern web applications frequently use virtualization.

Instead of creating thousands of DOM elements:

<img>
<img>
<img>
<img>
...
<img>

for every item in a large collection, the application may create only the elements necessary for the current viewport.

As you scroll:

Current viewport

[Image A]
[Image B]
[Image C]
[Image D]
[Image E]

those elements are recycled.

Then you scroll:

[Image F]
[Image G]
[Image H]
[Image I]
[Image J]

The browser may reuse the same DOM nodes.

This is excellent for application performance.

It is terrible for a naïve DOM scraper.


4. Why Scrolling Alone Didn’t Solve It

The next logical idea was:

Fine. I’ll scroll through the entire gallery and collect every image URL.

That sounds reasonable.

The browser could be instructed to scroll the gallery container:

container.evaluate(
    "el => el.scrollTop = el.scrollHeight"
)

Or scroll repeatedly:

for _ in range(100):
    container.evaluate(
        "el => el.scrollTop += 1000"
    )
    time.sleep(1)

The gallery did indeed continue loading more content.

So initially this looked promising.

But another problem appeared.

The DOM was still virtualized.

Images that had previously been visible could disappear from the DOM.

The same handful of <img> elements were being recycled.

Therefore:

The number of images visible in the DOM was not equal to the number of images in the user’s image library.

This was the first major lesson from the project:

Never assume that a modern web application’s DOM represents the complete underlying dataset.

The UI is a presentation layer.

The data often lives somewhere else.


5. Looking Beyond the DOM

At this point, the correct question became:

Where is the browser getting the image information from?

The browser obviously knows what images to display.

Therefore some form of application data must be arriving from the server.

That led to network inspection.

Instead of asking:

“What images are currently in the DOM?”

the better question was:

“What network request gives the application the list of images?”

This was the breakthrough.


6. Discovering the Image History API

During network inspection, an interesting request appeared:

https://chatgpt.com/backend-api/my/recent/image_gen?limit=25&after=...

This was the turning point.

The endpoint was returning JSON.

And the JSON contained records resembling:

{
    "id": "s_58995b7487448191a406e1ad728127ea",
    "kind": "media_generation",
    "generation_id": "s_58995b7487448191a406e1ad728127ea",
    "generation_type": "image_gen",
    "url": "https://chatgpt.com/backend-api/estuary/content?...",
    "width": 1672,
    "height": 941
}

This was much more useful than scraping the HTML.

We now had structured data.

The record contained:

  • an image/generation ID
  • generation type
  • dimensions
  • a content URL

Most importantly, the result was not limited to whatever happened to be mounted in the current DOM.


7. The limit=25 Confusion

The first time I saw:

?limit=25

it was tempting to think:

“So ChatGPT is only giving me 25 images?”

No.

This is pagination.

The API returns a batch.

Conceptually:

Request #1
     ↓
25 records
     ↓
after cursor
     ↓
Request #2
     ↓
next 25
     ↓
after cursor
     ↓
Request #3
     ↓
next 25

So the actual data structure is more like:

Image history
│
├── Page 1
│   └── 25 images
│
├── Page 2
│   └── 25 images
│
├── Page 3
│   └── 25 images
│
├── Page 4
│   └── 25 images
│
└── ...

The after value acts as a cursor for the next page.

This is a much better architecture than attempting to scrape thousands of dynamically rendered DOM elements.


8. But Then We Hit Authentication

At this point, it seemed like the hard part was solved.

We knew the API.

We knew the JSON structure.

We knew the pagination mechanism.

So the next step was simply:

requests.get(api_url)

Unfortunately:

HTTP 401

The response was essentially:

{
    "detail": {
        "message": "Unauthorized - Access token is missing"
    }
}

This was another important discovery.

Finding an endpoint does not mean that endpoint is publicly accessible.

The ChatGPT frontend was making an authenticated request.

Our independent HTTP request wasn’t carrying the same authentication information.


9. Why a Normal Browser Cookie Wasn’t Enough

The next attempt was to use the logged-in browser itself.

The idea was:

fetch(url, {
    credentials: "include"
})

But that still produced:

401 Unauthorized

with a message indicating that an access token was missing.

This told us something important about the authentication architecture:

The frontend request was carrying authentication information beyond simply relying on ordinary cookies.

So the next step was not to guess.

Instead, we inspected the browser’s own request.


10. Capturing the Browser’s Own API Request

The browser was already successfully talking to the API.

Therefore the cleanest diagnostic approach was:

  1. Open the authenticated ChatGPT Images page.
  2. Listen to outgoing requests.
  3. Identify the image_gen request.
  4. Inspect the authentication information attached to that request.
  5. Reuse that authenticated context for our own downloader.

Conceptually, the browser automation layer watches for:

/backend-api/my/recent/image_gen

and captures the authorization information associated with that request.

For security, the actual token is never printed or stored in the blog post.

The diagnostic output simply confirms:

AUTHENTICATION CAPTURED.
(Token hidden for security.)

That solved the API authentication problem.


11. The First Working Downloader

Once authentication was understood, the downloader could successfully retrieve:

HTTP: 200
Records returned: 25

Then the image records contained URLs such as:

/backend-api/estuary/content?id=file_...

The downloader could request those URLs and save the returned image bytes.

At this point, technically, we had a working bulk downloader.

But then we encountered the most interesting performance problem.


12. The Downloader Was Extremely Slow

The first working version was downloading successfully.

But it was painfully slow.

The strange part was that my Internet connection was much faster.

Task Manager showed network utilization around:

~20 KB/s

while the available connection was around:

~10 MB/s

That immediately suggested that the problem wasn’t the Internet connection itself.

The bottleneck was in our software architecture.


13. The Hidden Performance Bottleneck

The first downloader was essentially doing this:

ChatGPT API
      ↓
Browser JavaScript
      ↓
fetch()
      ↓
ArrayBuffer
      ↓
Uint8Array
      ↓
Array.from(...)
      ↓
Playwright
      ↓
Python
      ↓
File

The image data was being transferred through the browser’s JavaScript environment and then serialized back into Python.

For small JSON responses, this is perfectly reasonable.

For large image files, it is a terrible data path.

Imagine a 3 MB image.

Instead of simply streaming:

Network → File

we were effectively doing something closer to:

Network
  ↓
Browser
  ↓
JavaScript memory
  ↓
byte array
  ↓
automation bridge
  ↓
Python
  ↓
disk

And this was happening repeatedly.

That explained why the network graph was barely moving.


14. The Crucial Architectural Change

The solution was to separate two completely different responsibilities.

Browser responsibility

Use Chromium for:

  • maintaining the authenticated ChatGPT session
  • opening the Images page
  • obtaining the required authorization information
  • accessing the image history API

Python responsibility

Use Python for:

  • requesting the image list
  • following pagination
  • downloading the actual image files
  • streaming image data directly to disk
  • running multiple downloads concurrently

The architecture became:

                  CHATGPT
                     │
                     ▼
             Authenticated Browser
                     │
                     ▼
             Authentication Data
                     │
                     ▼
               Python API Client
                     │
                     ▼
            Image Metadata / URLs
                     │
          ┌──────────┼──────────┐
          ▼          ▼          ▼
       Download    Download   Download
          │          │          │
          └──────────┼──────────┘
                     ▼
              Local Disk

That was the major performance breakthrough.


15. Direct Streaming Instead of Browser Byte Transfer

The new downloader uses normal HTTP streaming for the actual image files.

The basic concept is:

response = requests.get(
    image_url,
    headers=headers,
    stream=True
)

Then the data is written directly to disk in chunks:

with open(output_file, "wb") as f:
    for chunk in response.iter_content(
        chunk_size=1024 * 1024
    ):
        if chunk:
            f.write(chunk)

This is fundamentally different from transferring the entire image through a JavaScript array.

The data path is now:

ChatGPT CDN
     ↓
HTTP stream
     ↓
Python
     ↓
Disk

Much simpler.

Much more efficient.


16. Parallel Downloads

There was another obvious optimization.

Why download one image at a time?

Suppose the API returns:

25 images

A serial downloader does:

Image 1 → wait
Image 2 → wait
Image 3 → wait
...
Image 25 → wait

Instead, the final implementation uses multiple workers.

For example:

DOWNLOAD_WORKERS = 16

The architecture becomes:

             25 image URLs
                   │
        ┌──────────┼──────────┐
        │          │          │
        ▼          ▼          ▼
      Worker     Worker     Worker
        │          │          │
       ...        ...        ...
        │          │          │
        └──────────┼──────────┘
                   ▼
                 Disk

This allows the available network connection to be utilized much more effectively.


17. Why 16 Workers?

There is no universal magic number.

You could theoretically use:

4
8
16
32
64

or more.

But more concurrency isn’t automatically better.

Too many simultaneous requests can cause:

  • unnecessary server load
  • throttling
  • connection overhead
  • memory consumption
  • increased failure rates

So the practical approach is to start with a reasonable concurrency level and measure.

In this project, 16 simultaneous direct downloads provided the desired performance improvement.


18. The Difference Was Dramatic

This was probably the most satisfying part of the experiment.

The first implementation was showing approximately:

20 KB/s

in Windows Task Manager.

The Internet connection itself was capable of roughly:

10 MB/s

After changing the architecture to direct streaming plus parallel HTTP downloads, the downloader finally behaved like a normal high-speed downloader.

The important lesson is:

A slow downloader does not necessarily mean a slow server or slow Internet connection.

Sometimes the bottleneck is the path your own software creates between the network and the disk.


19. Duplicate Detection

A bulk downloader should also be safe to run repeatedly.

Imagine downloading 2,000 images today.

Tomorrow you run the program again.

You don’t want:

image001
image001-1
image001-2
image001-3

etc.

The API gives each generated item a stable identifier such as:

s_471111528048819197dd063e35a8f555

That makes an excellent basis for the filename.

For example:

s_471111528048819197dd063e35a8f555.png

Before downloading, the program checks whether a file corresponding to that generation ID already exists.

If it does:

SKIP

Otherwise:

DOWNLOAD

This makes the downloader effectively resumable.


20. Why I Don’t Use the Prompt as the Filename

It might seem attractive to save images using their prompts:

A cinematic portrait of...

But prompts can contain:

  • punctuation
  • Unicode
  • very long text
  • characters invalid on Windows
  • duplicate descriptions

The generation ID is much safer.

For example:

s_471111528048819197dd063e35a8f555.png

isn’t pretty, but it is:

  • unique
  • stable
  • filesystem-safe
  • easy to compare
  • excellent for duplicate detection

A separate metadata database could always map the ID back to the prompt later if required.


21. Handling Different Image Formats

The generated content doesn’t necessarily need to be assumed to be one particular format.

The downloader examines:

Content-Type

and determines the extension accordingly.

For example:

image/png  → .png
image/jpeg → .jpg
image/webp → .webp
image/avif → .avif

This is preferable to blindly adding .webp to everything.


22. Pagination Is the Key to the Entire System

The complete workflow is essentially:

Request page 1
     ↓
Receive 25 records
     ↓
Download them
     ↓
Read "after" cursor
     ↓
Request page 2
     ↓
Receive next records
     ↓
Download them
     ↓
Read next cursor
     ↓
...
     ↓
No cursor
     ↓
Finished

This means the downloader doesn’t need to know beforehand whether there are:

100 images

or:

1,000 images

or:

10,000 images

It simply keeps following the pagination mechanism until the server indicates that there is no next page.


23. What Didn’t Work

One of the most valuable parts of this project wasn’t what worked.

It was what didn’t work.

Attempt 1 — DOM scraping

❌

Problem:

The gallery is virtualized.


Attempt 2 — Scroll and collect <img> elements

❌

Problem:

DOM elements are recycled.


Attempt 3 — Listen for individual image responses

❌

Problem:

The gallery’s underlying data loading wasn’t equivalent to simply receiving every image as a normal <img> network response.


Attempt 4 — Direct API request without authentication

❌ HTTP 401

Problem:

The API requires authenticated access.


Attempt 5 — Browser fetch() with cookies only

❌ HTTP 401

Problem:

The required access token was missing.


Attempt 6 — Browser-based image download through JavaScript

✅ Works
❌ Very slow

Problem:

The actual image bytes were being pushed through the browser automation bridge.


Final architecture

✅ API discovery
✅ Authenticated request
✅ Cursor pagination
✅ Direct HTTP streaming
✅ Parallel downloads
✅ Duplicate detection

That was the solution.


24. A Small Piece of the Final Code

I don’t want to publish the complete production downloader here, but the central idea is surprisingly simple.

The API request is conceptually:

response = session.get(
    api_url,
    headers=api_headers
)

data = response.json()

items = data.get("items", [])

Then each record gives us the actual content URL:

image_url = item["url"]

And the direct downloader uses streaming:

response = requests.get(
    image_url,
    headers=headers,
    stream=True
)

with open(filename, "wb") as f:
    for chunk in response.iter_content(
        chunk_size=1024 * 1024
    ):
        if chunk:
            f.write(chunk)

And multiple workers perform those downloads concurrently.

That’s the core idea.

The complete implementation has additional handling for:

  • authentication
  • pagination
  • duplicate detection
  • filename safety
  • HTTP failures
  • content types
  • retries
  • existing files
  • progress reporting
  • download statistics
  • browser profile management

Those details are what turn a proof-of-concept into a practical tool.


25. The Browser Is No Longer the Downloader

This is probably the most important architectural lesson from the project.

Initially, the browser was doing everything:

Browser
 ├── Login
 ├── API
 ├── Image retrieval
 └── File transfer

That isn’t necessary.

The browser is excellent at:

Authentication
Session management
Modern web application interaction

Python is excellent at:

HTTP
Concurrency
Streaming
File I/O
Automation

So the final architecture assigns each job to the tool best suited for it.

Chromium
   │
   └── Authentication

Python
   │
   ├── API
   ├── Pagination
   ├── Downloads
   ├── Concurrency
   └── Storage

That separation is what made the final solution practical.


26. Why This Was More Interesting Than Just Writing a Downloader

At the beginning, the task sounded like:

“Download my ChatGPT images.”

But the actual engineering problem turned out to contain several layers:

Layer 1 — UI analysis

Understand how the gallery behaves.

Layer 2 — DOM analysis

Determine whether the images are actually represented in the page.

Layer 3 — virtualization

Understand why scrolling doesn’t produce a complete DOM dataset.

Layer 4 — network analysis

Identify the API responsible for the image history.

Layer 5 — pagination

Understand the limit and after cursor mechanism.

Layer 6 — authentication

Understand why a direct HTTP request receives HTTP 401.

Layer 7 — performance profiling

Determine why a successful downloader is still extremely slow.

Layer 8 — architecture

Move actual file transfer outside the browser.

Layer 9 — concurrency

Download multiple files simultaneously.

Layer 10 — reliability

Add duplicate detection and resumability.

That is a much more interesting engineering problem than simply writing:

requests.get(url)

27. What This Teaches About Modern Web Applications

There is a broader lesson here.

When dealing with a modern web application, the visible page is often not the data source.

The architecture frequently looks more like:

Frontend UI
     │
     ▼
Application API
     │
     ▼
Database / Storage

The HTML is simply the rendering layer.

If you try to reconstruct the entire dataset from the HTML, you may end up fighting:

  • virtualization
  • lazy loading
  • infinite scrolling
  • dynamic components
  • client-side state
  • placeholders
  • hydration
  • framework-specific DOM structures

Sometimes the much cleaner approach is to understand the application’s own data flow.


28. A General Debugging Principle

One of the principles I keep coming back to in software engineering is:

When the obvious interface becomes difficult to automate, look one layer underneath it.

In this case:

Layer 1

Visible Images gallery.

Didn’t work.

Layer 2

DOM elements.

Didn’t work reliably.

Layer 3

Network requests.

Worked.

Layer 4

API data.

Worked even better.

Layer 5

Direct authenticated HTTP transfer.

Solved the performance problem.

This pattern applies far beyond image galleries.


29. Measuring the Right Thing

Another important lesson was the importance of measurement.

Without looking at Task Manager, it would have been easy to conclude:

“The ChatGPT server is slow.”

But the network graph told a different story.

The connection was barely being used.

That suggested:

Internet capacity
       ≠
Application throughput

The actual throughput depends on the entire pipeline:

Server
 ↓
Network
 ↓
HTTP client
 ↓
Browser
 ↓
Serialization
 ↓
Automation framework
 ↓
Python
 ↓
Disk

Any one of those layers can become the bottleneck.

In our case, the bottleneck was our own transfer architecture.


30. The Final Architecture

The final system can be summarized in one diagram:

                    CHATGPT
                       │
                       ▼
              ┌─────────────────┐
              │ Dedicated       │
              │ Chromium        │
              │ Profile         │
              └────────┬────────┘
                       │
                 Authentication
                       │
                       ▼
              ┌─────────────────┐
              │ image_gen API   │
              └────────┬────────┘
                       │
                  25 records
                       │
                       ▼
              ┌─────────────────┐
              │ Python API      │
              │ Client          │
              └────────┬────────┘
                       │
                image URLs
                       │
          ┌────────────┼────────────┐
          │            │            │
          ▼            ▼            ▼
       Worker 1     Worker 2    Worker 3
          │            │            │
          │          ...           │
          └────────────┼────────────┘
                       │
                       ▼
              ┌─────────────────┐
              │ Windows Folder  │
              │                 │
              │ C:\WORKS\       │
              │ chatgpt images  │
              └─────────────────┘

The system continues requesting subsequent API pages until the image history is exhausted.


31. What the Final Tool Can Do

The resulting downloader is designed around several practical requirements:

Bulk retrieval

Process large image histories without manually downloading individual images.

Pagination

Automatically move from one API page to the next.

Duplicate protection

Existing images are skipped.

Direct streaming

Image data is streamed directly to disk.

Parallel downloading

Multiple images can be downloaded simultaneously.

Resume-friendly operation

If the process stops, it can be run again and existing files will be skipped.

Local archive

The final result is a normal Windows folder containing the image files.


32. Why I’m Not Publishing the Full Code

There is a reason I am showing the architecture and selected snippets rather than dumping the entire working implementation into this article.

A production implementation contains considerably more than the few lines shown here.

It includes:

  • browser profile handling
  • authentication discovery
  • API pagination
  • error handling
  • concurrency
  • streaming
  • duplicate detection
  • filename management
  • retry handling
  • progress tracking
  • session handling
  • browser lifecycle management

Publishing a 300–500 line script without context would also make the article much less useful.

I’d rather show how the problem was solved.

If someone has a legitimate personal or development requirement for the complete implementation, they can contact me.


33. A Note About Authentication and Responsible Use

There is also an important security consideration.

Authentication information should be treated as sensitive.

A downloader should never:

  • publish access tokens
  • log authorization headers
  • upload session information
  • store credentials unnecessarily
  • expose browser profiles
  • share authenticated request data

In my implementation, the authentication information is used only during the active authenticated session and is deliberately hidden from console output.

The principle is simple:

Automate your own authenticated session without exposing your credentials.


34. Final Results

What started as:

“I want to download my ChatGPT images.”

eventually became a complete engineering exercise involving:

Virtualized UI analysis

→

Network inspection

→

API discovery

→

Authentication handling

→

Cursor-based pagination

→

Direct HTTP streaming

→

Parallel downloading

→

Duplicate detection

→

Performance optimization

The most satisfying part wasn’t discovering the API.

It was identifying the real performance bottleneck.

The first successful downloader proved that the concept worked.

But the network graph showing only approximately 20 KB/s made it clear that the architecture itself needed to change.

Once the browser was removed from the actual image-transfer path and Python handled the downloads directly, the situation changed completely.


Conclusion

The biggest lesson from this project is not about ChatGPT specifically.

It is about how to approach difficult automation problems.

When a web page doesn’t expose the data you need:

Don’t immediately write a more complicated scraper.

First understand how the application itself gets the data.

When the API works but the downloader is slow:

Don’t immediately blame the server.

Profile the entire data path.

When a browser can perform an operation but your HTTP client receives 401:

Don’t guess at authentication.

Observe how the authenticated application performs the request.

And when the browser is good at authentication but bad at bulk file transfer:

Don’t force the browser to do everything.

Use the browser for what it is good at.

Use a dedicated HTTP client for what it is good at.

In this case, that separation transformed a frustrating 20 KB/s downloader into a practical high-speed bulk downloader.

And perhaps the most interesting part is that the final solution is actually quite simple once the architecture is understood:

Authenticate with the browser.
Get the data through the API.
Download directly.
Download in parallel.
Write directly to disk.
Follow the cursor until there is nothing left.

Sometimes the hardest part of software engineering isn’t writing the code.

It’s discovering the right place to write it.


Want the complete implementation?

The full working implementation used in this case study is intentionally not published here.

If you have a genuine requirement for a bulk ChatGPT image archival/downloading solution, including the complete Python implementation, authentication/session handling, pagination, parallel downloading, duplicate detection and performance optimizations, you can contact me.

Debmedia Technologies LLP
Debmedia Labs

Custom automation and software engineering solutions for real-world workflows.

Selected Code From the Working Implementation

Below are selected excerpts from the working implementation. They illustrate the core architecture and techniques used in the project, but the complete production implementation is intentionally not published. The full version contains additional authentication handling, pagination logic, retry mechanisms, duplicate detection, error handling, concurrency management and filesystem safeguards.

1. API endpoint discovery

প্রথমে খুব ছোট snippet:

API_URL = (
    "https://chatgpt.com/backend-api/"
    "my/recent/image_gen"
)

params = {
    "limit": 25
}

তারপর লিখবেন:

The first important discovery was the image-generation history endpoint. The API returns image records in batches rather than exposing the entire collection through the DOM.


2. API response থেকে image URL নেওয়া

এখানে পুরো processing logic না দিয়ে শুধু মূল অংশ:

data = response.json()

for item in data.get("items", []):
    generation_id = item.get("generation_id")
    image_url = item.get("url")

    print(generation_id, image_url)

তারপর ব্যাখ্যা:

Each record contains a generation identifier and an authenticated content URL. This was the point where the project moved from DOM scraping to structured API-based retrieval.


3. Pagination

এটা দেখানো খুব ভালো হবে, কারণ 25 images মানে total 25 নয়—এটাই আপনার discovery-এর একটা গুরুত্বপূর্ণ অংশ।

after = None

while True:

    params = {"limit": 25}

    if after:
        params["after"] = after

    # request API ...

    items = data.get("items", [])

    if not items:
        break

    # process current batch ...

    after = data.get("after")

    if not after:
        break

তার নিচে একটা diagram:

25 images
    ↓
after cursor
    ↓
next 25
    ↓
after cursor
    ↓
next 25
    ↓
...

4. Authentication অংশে বেশি code নয়

এখানে আমি actual token capture code publish করব না।

বরং ছোট conceptual snippet:

def capture_request(request):

    if "/image_gen" in request.url:
        headers = request.all_headers()
        authorization = headers.get(
            "authorization"
        )

তারপর লিখবেন:

The endpoint could not be accessed with an ordinary unauthenticated HTTP request. The browser’s own authenticated request contained additional authorization information. For security reasons, the complete authentication-handling implementation is not included here.

এতে technical credibility থাকবে, আবার sensitive authentication implementation প্রকাশও হবে না।


5. প্রথম slow implementation-এর snippet

এটা article-এর সবচেয়ে interesting code section হতে পারে।

দেখাতে পারেন:

const response = await fetch(url);

const buffer =
    await response.arrayBuffer();

const bytes =
    Array.from(
        new Uint8Array(buffer)
    );

return bytes;

তারপর লিখবেন:

This approach worked, but it introduced a serious performance problem. The image was being downloaded into the browser, converted into a JavaScript byte array, transferred through the automation layer, and only then written by Python.

তারপর Task Manager-এর:

~20 KB/s

versus:

~10 MB/s available bandwidth

এই contrast দেখালে article অনেক বেশি interesting হবে।


6. Final high-speed downloader-এর মূল অংশ

এখানে full downloader না দিয়ে core streaming concept দেখান:

response = requests.get(
    image_url,
    headers=headers,
    stream=True
)

with open(output_file, "wb") as f:

    for chunk in response.iter_content(
        chunk_size=1024 * 1024
    ):

        if chunk:
            f.write(chunk)

তারপর:

The browser was now responsible primarily for authentication and API access. The actual image transfer was moved to a direct streaming HTTP client.


7. Parallel download-এর ছোট snippet

এটাও খুব সুন্দর দেখাবে:

with ThreadPoolExecutor(
    max_workers=16
) as executor:

    futures = [
        executor.submit(
            download_one,
            job
        )
        for job in jobs
    ]

তারপর diagram:

             25 image URLs<br>                    │<br>       ┌────────────┼────────────┐<br>       ↓            ↓            ↓<br>    Worker 1     Worker 2    Worker 3<br>       ↓            ↓            ↓<br>       ...          ...          ...<br>              16 workers<br>                    ↓<br>                  Disk

The final result:

==============================================================================
API PAGE 97

API HTTP: 200
Records returned: 6
New images to download: 6
[01/06] OK s_91ec5836e46c8191a4061eccc268dd83.png (2.6 MB)
[02/06] OK s_6acbe5be4a048191807a8f5d00fd3687.png (1.9 MB)
[03/06] OK s_41d2e5c1ab1481919934b93bd4a237a6.png (2.9 MB)
[04/06] OK s_b5ed8644cef48191a4a89cfcbbd5a0b5.png (2.7 MB)
[05/06] OK s_d7443a03703481919b08b7d7ae2487c3.png (2.4 MB)
[06/06] OK s_988308beba448191a3dda8f83f946ba5.png (2.9 MB)

Batch speed: 1.54 images/sec

Overall speed: 2.66 images/sec

No next cursor.
Reached end of image history.

==============================================================================
DOWNLOAD COMPLETE

Found: 2406
Downloaded: 2399
Skipped: 6
Failed: 1
Time: 15.1 minutes
Average: 2.66 images/sec

Saved to:
C:\WORKS\chatgpt images

==============================================================================
Press ENTER to close Chromium…

Want the Full Code?

The examples above show the core techniques used in the project, but they are only selected parts of the complete working implementation.

The full source includes the complete workflow for:

  • Authentication handling
  • API pagination
  • Automatic image discovery
  • High-speed direct downloads
  • Parallel downloading
  • Duplicate detection
  • Error handling and retries
  • File naming and organization
  • Resume-safe downloading

Want the complete working code?

If you have a legitimate requirement to archive and download your own ChatGPT-generated images in bulk, feel free to contact me.

I can provide the complete implementation and, if required, help customize it for your workflow.


Want the Full Code?

This project started as a simple idea, but getting it to work reliably required quite a bit of experimentation—from dealing with the virtualized image gallery to discovering the underlying API, handling authentication, and finally optimizing the download architecture.

The snippets in this article demonstrate the important parts, but the complete production-ready implementation is not published here.

If you want the full working source code, including the authentication flow, pagination, high-speed downloader, parallel processing, duplicate detection, retries, and other supporting code:

Want the full code? Contact me.

Tell me what you’re trying to build, and I’ll be happy to discuss the complete implementation and possible customization.

— Debasis Bhattacharjee

Let's Talk

Have a Project in Mind?

Whether it's a software challenge, an AI integration, or a course enquiry — I'm always open to a real conversation.

hello@debasisbhattacharjee.com · +91 8777088548 · Mon–Fri, 9AM–6PM IST