Post

Terminal Image: GPT Image Generation and Cloudflare Hosting from One Command

Generate an image, retain the original, and return a Cloudflare link from a scriptable terminal workflow.

Terminal Image: GPT Image Generation and Cloudflare Hosting from One Command

A useful image workflow doesn’t have to involve a browser tab, a download, and a separate upload. Terminal Image is one of my projects: a Python command-line tool that sends a prompt to OpenAI, saves the generated image if requested, and uploads it to Cloudflare Images for a hosted link. Its JSON output makes the same workflow usable from a script or an agent.

I covered the earlier DALL-E-based version in Cloudflare Terminal Image Generator. This follow-up describes the updated repository: its configured default is gpt-image-2.5-sunburst, and the current CLI has explicit options for signed expiring links, permanent links, and original files. The cover above was generated through the current CLI and hosted on Cloudflare Images.

Set it up

You’ll need Python 3.10 or newer, an OpenAI API key with access to the selected model, and a Cloudflare Images account for uploads. Generation and hosting use those external services and may incur their applicable charges. The setup instructions use a virtual environment:

1
2
3
4
5
git clone https://github.com/bigsk1/terminal-image.git
cd terminal-image
python3 -m venv .venv
source .venv/bin/activate
python -m pip install -r requirements.txt

PowerShell users can activate with .venv\Scripts\Activate.ps1. The requirements file specifies requests and rich; Pillow is not required. A terminal preview is optional and needs one of kitten icat, viu, or chafa.

Provide OPENAI_API_KEY, CLOUDFLARE_API_TOKEN, and CLOUDFLARE_ACCOUNT_ID in your environment using your normal secret-management method. The account-scoped Cloudflare token needs permission to upload Images and read variants and signing keys. CLOUDFLARE_IMAGES_SIGNING_KEY can supply an existing signing key, and OPENAI_IMAGE_MODEL can override the default model. The script does not load environment files automatically. Avoid placing real credentials in shell history, source files, or logs.

Generate a durable article cover

For an image intended to remain on a public blog, request a non-expiring link and save the original alongside the returned URL. Create the local output directory first, and choose a filename that does not already exist:

1
2
3
4
5
mkdir -p output
python cf.py --json --no-expire --size 1536x864 \
  --format webp --compression 85 --folder my-blog \
  --output output/article-cover.webp \
  -- "A dark industrial illustration of a network of edge servers"

The dimensions are a landscape 16:9 example. --format webp and --compression 85 request compressed output; --folder my-blog groups the Cloudflare upload. The original generated bytes are written locally and the existing file is never overwritten. The script requires the output directory to exist and checks recognized image extensions against the selected format. See the CLI implementation for argument validation.

In JSON mode, stdout contains one result record; diagnostics go to stderr, and the terminal preview is disabled. A consumer should check the process exit status before using url, and retain the returned image_id, folder, model, and expires_at with its own records. A non-expiring result has a null expires_at. Use the exact returned URL rather than manufacturing one from an ID. The agent usage notes document these fields.

If you want only a local original, use --no-upload with --output. Cloudflare credentials are optional for that mode, although image generation still needs OpenAI access. --help and --history do not require API credentials.

The default hosted link expires after 30 days. --expire 24h selects a shorter lifetime; --no-expire creates a public link without an expiration.

This distinction belongs in the publishing decision: a signed link that expires is inconvenient for an article expected to stay readable, while a permanent public link is unsuitable for material that must stop being accessible automatically.

For expiring links, the selected Cloudflare delivery variant must have Always allow public access disabled, and the account needs an existing Images signing key. The script checks this configuration before it requests an image, and does not create or rotate signing keys. Other account variants configured for unrestricted public access can still expose an image, so inspect those settings if the expiry is meant to restrict access. The expiration documentation explains the signed URL behavior.

Link expiry does not delete the stored Cloudflare image or any local copy. --list-expired lists matching records with the newer expiry metadata; it does not delete them. Default expiring uploads use a Cloudflare UUID and metadata grouping. Permanent uploads use a folder-style custom ID, because Cloudflare does not support signed URLs for custom ID paths. The --folder option changes the grouping name in either mode.

Recover from an upload failure

Image generation and image hosting are separate operations. If the image was generated but the upload fails, look for the saved original before requesting another paid generation. With --output, the original is written locally. Without it, a failure after successful generation triggers a recovery file in the system temporary directory and reports its path. Use that file with your normal upload process instead of spending another generation to fix an upload-only problem.

The script keeps local history, including links and expiry times, and writes the record before attempting a terminal preview. A missing preview tool therefore does not erase a successful generation record. History can also contain prompts and URLs, so protect it as you would other working files. Existing history from the older tool remains readable, but its old expiry field was metadata rather than enforced expiration; this update does not change those old hosted links. See troubleshooting.

The source is available under the MIT license. For an automated publishing workflow, the useful output is the pair: the original file you can retain and the exact hosted URL your article can use, with the link lifetime chosen deliberately.

This post is licensed under CC BY 4.0 by the author.