Like many of you, I’ve seen all the hype regarding “Artificial Intelligence” and said ‘ffft’ it’s all just hokus pocus.
Well, I’m beginning to change my mind. As mentioned in a previous post, I had used ChatGPT to craft a couple of python routines to do some fairly simple, but tedious data conversions.
Back in the spring, I decided it was time to do a fresh reinstall of Windows 10. I’m really bad about futzing with software and such, and had damaged my Windows install such that it would no longer display file previews, amongst a number of problems. This wasn’t something I jumped into lightly, as in my experience it’s a full day plus some for an OS reinstall. That’s why I’m still running Elementary OS V7.1 on the machine I’m typing this on, as EOS doesn’t do inline OS upgrades. When the beta of Pop!_OS COSMIC is released, I’ll install it on this machine and throw EOS to the curb.
So, in the process of looking at everything installed on the Windows box, there was LightRoom v6.14 (LR), the last non-subscription version of LR. To be kind, LR and I never saw eye-to-eye. I could get images processed to my liking, but it took far longer and with more effort than I wanted.
Conversely, the first time I used Capture One, it was nirvana. My Fuji raf files imported to a state far closer to my final vision than LR ever did, and needed far less manipulation for the average image. While I dislike C1’s commercial practices with a passion, I love what C1 does for my Fuji (and all my other camera) images.
So, LR was used solely to import images from the SD card of my cameras, renaming and creating appropriately named folders. I had developed this workflow years ago, and it’s allowed me to be able to locate images in my inventory easily. The plus is I’m not tied to any particular Digital Asset Management (DAM) software to be able to find anything. I’ve been through a plethora of DAM’s, LightRoom being one, and in my experience have caused more grief than benefit. I’m currently using digiKam, but truth be told, I’m not in love with it. It’s ugly and clunky, which is a hallmark of many FOSS applications, but the user is at the whim of the developers who work on what interests them, which typically is NOT the user experience. FOSS is great, but there is a lot of it that is more trouble than it’s worth.
Anyway, I was now in need of a tool to “ingest” my images as it’s called. I looked at a bunch. The most highly regarded culling/importing tool is Photo Mechanic. Everyone raves about Photo Mechanic, it’s great. The downside; it’s aimed at professional photographers and is correspondingly very expensive. The next best tool I found was Rapid Photo Downloader. RPD is very close to providing everything I want. But, and there is always a but, it’s Linux only. I’ve been running it on the box I’m typing on, but ideally I want something that runs on the Windows machine where Capture One is installed. I tried to get RPD running in Windows Subystem for Linux and failed miserably. Adobe Bridge, XnViewMP, FastStone Image Viewer, Advanced Renamer, and others were tried looking in vain for a tool that provided all the features I was looking for.
Thinking about it, I thought, “H’mmm, I wonder if ChatGPT could create a Python routine for me”? I’m conversant in Python, not fluent in any sense of the word, but I could at least follow what was going on in the logic.
So, opening up ChatGPT in my browser, the process began. Honestly, my last interaction with ChatGPT was an abysmal failure. One of the applications I use for photo processing is ART. It’s still a work in progress, and one of the users created The ART Book. It was written in Hungarian, and machine translated to English and the translation leaves a lot to be desired. So, I got a wild hair, uploaded the book to ChatGPT and asked it to “clean it up”. Sure, was the response. I would check back, and some progress was made, until it got to about 20% in and the effort stalled completely. No amount of cajoling, complaining, whining, etc could get it to process anymore. It was very apologetic, but eventually I just gave up.
So we started with this:
Craft a python routine to: read the contents of a USB drive, copy all the image files as follows: W:\Year Created\MonthDay Created-Descriptor\YearMonthDay Created-Descriptor.3 digit sequence.ext. Match pairs of jpg and raf files.
It came back with:
Got it β
β you want a Python routine that:
Reads files from a USB drive (say W:\)
Looks for images (.jpg and .raf), treating them as pairs (same basename, different extensions)
Creates a destination folder structure:
W:\Year\MonthDay-Descriptor\
YearMonthDay-Descriptor.001.jpg
YearMonthDay-Descriptor.001.raf
where:
Year, Month, Day come from the fileβs creation date
Descriptor is a placeholder you can supply manually (e.g., "Vacation", "Wedding", etc.)
Sequence number is a 3-digit counter (001, 002, β¦) for files created on the same day
From there we iterated, added some features, cleaned up some logic and slow processes and eventually wound up with the following python routine. One of the things that it does, that RPD did not do, was write a Description entry in an XMP sidecar file for Capture One (and all other photo processing software) to use in processing. This is a major step in the process.
import os
import shutil
import subprocess
from collections import defaultdict
from datetime import datetime
from PIL import Image
from PIL.ExifTags import TAGS
# --- CONFIG ---
EXIFTOOL_PATH = r"D:\Documents\Photography\Software\Exiftool\exiftool.exe"
def get_exif_date(file_path):
"""Extract capture/created date from EXIF (for JPGs) or filesystem created date."""
try:
img = Image.open(file_path)
exif = img._getexif()
if exif:
for tag, value in exif.items():
if TAGS.get(tag, tag) == "DateTimeOriginal":
return datetime.strptime(value, "%Y:%m:%d %H:%M:%S")
except Exception:
pass
return datetime.fromtimestamp(os.path.getctime(file_path))
def run_exiftool(args):
"""Run exiftool without opening a console window, return (ok, stdout, stderr)."""
try:
startupinfo = None
creationflags = 0
if os.name == "nt":
startupinfo = subprocess.STARTUPINFO()
startupinfo.dwFlags |= subprocess.STARTF_USESHOWWINDOW
creationflags = subprocess.CREATE_NO_WINDOW
proc = subprocess.run(
args,
stdout=subprocess.PIPE,
stderr=subprocess.PIPE,
text=True,
check=False,
startupinfo=startupinfo,
creationflags=creationflags
)
return proc.returncode == 0, proc.stdout, proc.stderr
except Exception as e:
return False, "", str(e)
def write_metadata(jpg_files, raf_files, descriptor):
"""Write metadata: JPGs directly, RAFs via XMP sidecars."""
if jpg_files:
print(f"π Writing metadata directly into {len(jpg_files)} JPG files...")
args = [
EXIFTOOL_PATH,
f"-IPTC:Caption-Abstract={descriptor}",
f"-XMP-dc:Description={descriptor}",
"-overwrite_original",
"-P", "-m"
] + [str(f) for f in jpg_files]
ok, _, err = run_exiftool(args)
if ok:
print(f"β
Finished writing metadata for JPGs")
else:
print(f"β Failed to write metadata for JPGs: {err}")
if raf_files:
print(f"π Writing XMP sidecars for {len(raf_files)} RAF files...")
args = [
EXIFTOOL_PATH,
f"-XMP-dc:Description={descriptor}",
"-o", "%d%f.xmp",
"-P", "-m"
] + [str(f) for f in raf_files]
ok, _, err = run_exiftool(args)
if ok:
print(f"β
Finished writing XMP for RAFs")
else:
print(f"β Failed to write XMP for RAFs: {err}")
def organize_images():
source_drive = input("Enter the source drive (e.g. E:\\): ").strip('"')
dest_drive = input("Enter the destination drive (e.g. W:\\): ").strip('"')
if not os.path.exists(source_drive):
print(f"β Source drive {source_drive} not found.")
return
if not os.path.exists(dest_drive):
print(f"β Destination drive {dest_drive} not found.")
return
# Group files by stem (JPG+RAF)
files_by_stem = defaultdict(dict)
for root, _, files in os.walk(source_drive):
for file in files:
ext = file.lower().split(".")[-1]
if ext not in ["jpg", "raf"]:
continue
stem = os.path.splitext(file)[0]
files_by_stem[stem][ext] = os.path.join(root, file)
# Sort items by capture/created date
items = []
for stem, exts in files_by_stem.items():
ref_file = exts.get("jpg") or exts.get("raf")
date = get_exif_date(ref_file)
items.append((date, exts))
items.sort(key=lambda x: x[0])
# Group by calendar date
by_date = defaultdict(list)
for date, exts in items:
by_date[date.date()].append((date, exts))
# Process each date group
for date, entries in sorted(by_date.items()):
descriptor = ""
while not descriptor.strip():
descriptor = input(f"Enter descriptor for {date.strftime('%Y-%m-%d')}: ").strip()
year_folder = os.path.join(dest_drive, str(date.year))
date_folder = os.path.join(year_folder, f"{date.strftime('%m%d')}-{descriptor}")
os.makedirs(date_folder, exist_ok=True)
jpg_targets = []
raf_targets = []
for seq, (dt, exts) in enumerate(entries, start=1):
seq_str = f"{seq:03d}"
base_name = f"{dt.strftime('%Y%m%d')}-{descriptor}.{seq_str}"
if "jpg" in exts:
dest_jpg = os.path.join(date_folder, base_name + ".jpg")
print(f"Copying {exts['jpg']} -> {dest_jpg}")
shutil.copy2(exts["jpg"], dest_jpg)
jpg_targets.append(dest_jpg)
if "raf" in exts:
dest_raf = os.path.join(date_folder, base_name + ".raf")
print(f"Copying {exts['raf']} -> {dest_raf}")
shutil.copy2(exts["raf"], dest_raf)
raf_targets.append(dest_raf)
# Write metadata
write_metadata(jpg_targets, raf_targets, descriptor)
if __name__ == "__main__":
organize_images()
This works beautifully. A myriad of tests were run yesterday, and the routine performed flawlessly. An ingest of some 1,500 images from “Single in June” from Cameraderie, was done as the images were still on the SD card. When imported originally in RPD, this took nearly a day as the sheer number of images cause RPD to choke a bit. In the new routine, it took 30 minutes. No problems.
It’s a command line tool at this stage. Crafting this from scratch, on my own, in python would have taken me at least a week. Using ChatGPT, I have a fully functional, robust routine in less than a day. That’s amazing.
This is what a typical output folder looks like:

Thinking about it, I decided to see if ChatGPT could craft a Graphical User Interface (GUI) for the tool.
Putting together some concepts of how this might work, here is a mock-up of the interface:

Descriptive:
- There are eight boxes:
- Upper Left β Source file location text box. This will be the location the last time the application was used. It can be manually overwritten by the user
- Middle Left β This is a button to bring up a file selection window for the user to browse to a source file location.
- Middle Right β Destination file location text box. This will be the location the last time the application was used. It can be manually overwritten by the user.
- Far Right β This is a button to bring up a file selection window for the user to browse to a destination file location.
- Lower Left β This window will display the thumbnails of the images in the source folder. The image thumbnails will be displayed overlaid check boxes. The images in the window should scroll the images up and down using the middle mouse wheel. Image thumbnails should be set to 256 pixels wide.
- Middle β This is the Transfer Button.
- Lower Right β The names of the selected files to be copied will be displayed.
- Bottom Right β The Import button to start the copying process.
- Once the Source location is chosen, the image thumbnails will be displayed in the lower left-hand window with overlaid check boxes.
- Select images with check box as desired.
- Right click on any selected image, and the user will be prompted for a Descriptor. This can either be typed in, or selected from a list of previously used Descriptors.
- Once the Descriptor is added, it will be displayed either under or on top of the image (in a contrasting color), If a destination has been provided the Transfer button will appear.
- Clicking on the Transfer button, the filenames of the imported files will be displayed in the lower right-hand window in red for review.
- Clicking on the Import button, the files names are changed from red to green as they are copied.
- When the copying is complete and the XMP files are being written, pop up a notice stating βWriting XMP Filesβ¦β
- Once the Process is complete, gray out the copied images, and filenames in both lower boxes.
- Continue process until user closes application.
Before ending our session yesterday, I gave ChatGPT some brief ideas for the GUI, and told it to work on it.
This morning I uploaded all the info. We got several steps in and then my “free” daily session came to an end because I had uploaded a file. For whatever the reason, uploading something significantly shortens the daily session.
So, tomorrow, I’ll continue on in the effort. We’re going to do this in stages, define the interface and how it’s used, and then craft the code to perform the activity.
ChatGPT opens up a myriad of programming things I’ve been thinking about, but not able to get to.
The plan, now that the website is “fixed”, is to post every Sunday. When I was working, I would sit at the counter on Sunday morning before Melanie got up and craft a post, so henceforth that’s the goal.
Take care
