Part 2: Using ChatGPT to Create a Frame TV Upload Script


Posted:

Categories & Tags:

This is a multipart series in a blog series about using Lightroom and the Samsung Frame TV. Check out Part 1 (Lightroom configuration), Part 2 (using ChatGPT for generating an upload script) and Part 3, Lightroom Mobile integration.

It’s amazing how much can change in a year with technology. In June 2024 I wrote a blog explaining how I upload photos from Lightroom to my Samsung Frame TV. I found a Python library, downloaded a Python development environment, learned Python (with a reasonable amount of pain – but got there), and had a script that I was proud of after two or so weeks of learning and tinkering. I’ve been spending more and more time at work learning about AI. I also went to an AI meet up in the South Bay and picked up on one of the presenter’s taglines: “With AI, English is the hottest new programming language.” I was impressed how he was using AI in his presentation and wanted to revisit my Lightroom script to see if and how I could do better. In short, I could do faster and better. I tossed this challenge ChatGPT’s way to see what would happen. In short: I was blown away. Let’s dig in.

I like the way the Lightroom configuration works in the original blog. However, I want to see if ChatGPT can make the Python piece easier – especially for readers like you so you don’t have as steep of a learning curve as I did.

1. Create a ChatGPT project.

I like creating projects inside of ChatGPT so that every topical area is independent of all the other conversations I have going on inside of the platform. Click new project in the left-hand column after logging into ChatGPT. Let’s call this project “Lightroom Frame TV Uploader.”

2. Tell ChatGPT what to create

I generally liked what my Python code did in the former blog, however, it was missing two key features that I didn’t have the time or patience to develop. I wanted a progress bar so I knew what the status of the upload was and which specific files failed, if any, during the batch upload. To start, I asked ChatGPT to create that code. I used the following prompt:

I have a directory of images that I would like to upload to the Samsung Frame TV on a brand new mac:
- Help me setup python
- Synchronously upload the files. 
- Show a progress bar. The progress bar shows the progress of the whole batch. 
- Show the file that it's uploading and its matte color while it works. 
- Once a picture is uploaded, set the matte to the value stored in the exif value ImageDescription. 
- Create a counter for the number of images that upload correctly and error out. 
- Show both counter totals at the end list and the file names that don't upload correctly after a retry
- Use Nick Waterton’s samsung-tv-ws-api library

3. Be Amazed

ChatGPT then replied with four key sections to help get me running:

  1. How to set up Python on my Mac
  2. The Python code to do the actual upload
  3. Instructions to run the Python code
  4. A description of what the script does
  5. Caveats of what I might need to adjust

That was about two weeks of my time which was solved by ChatGPT in under a few minutes. #mindBlown. What was even more impressive is that I was able to interact with ChatGPT to make the code better. There’s a section that catches typos. It’s more flexible supporting all of the different matte types. The code is more resilient to errors.

When running it for the first time, the script didn’t respect the imageDescription value set in Lightroom. I simply asked ChatGPT to regenerate the code highligting this issue. The next version worked flawlessly and is better in so many ways than my original attempt. This was one of these personal, lightbulb moments that really changed my perception of AI as a collaborative partner helping me move faster solving a meaningful “problem” in an expedient, but thoughtful way.

I’ve included the code below for reference in case your experience with ChatGPT is different than mine. If things don’t work as expected, copy the error message and ask ChatGPT what is wrong. I’ve found the help to be invaluable to troubleshoot any errors I’ve run into.

To Run: (after following ChatGPT’s python setup steps in the Mac terminal)
source ~/frame_tv/.venv/bin/activate
python frame_upload_batch.py –ip tv.ip.address –dir path-to-your-files

#!/usr/bin/env python3
import argparse
import os
import sys
import time
from pathlib import Path

from PIL import Image, ExifTags
from tqdm import tqdm
from samsungtvws import SamsungTVWS

# --- Matte dictionaries (style + color) ---
MATTE_TYPES = {
    "none", "modernthin", "modern", "modernwide",
    "flexible", "shadowbox", "panoramic", "triptych", "mix", "squares"
}
MATTE_COLORS = {
    "black", "neutral", "antique", "warm", "polar", "sand", "seafoam", "sage",
    "burgandy", "navy", "apricot", "byzantine", "lavender", "redorange",
    "skyblue", "turquoise"
}
# normalizing typos / spacing
SYNONYMS = {
    "burgundy": "burgandy",
    "red orange": "redorange",
    "red_orange": "redorange",
    "red-orange": "redorange",
    "sky blue": "skyblue",
}

# EXIF tag id for ImageDescription
EXIF_TAGS = {v: k for k, v in ExifTags.TAGS.items()}
IMG_DESC_TAG = EXIF_TAGS.get("ImageDescription", 270)


def read_exif_imagedescription(path: Path) -> str | None:
    """Return EXIF:ImageDescription as a clean string, if present."""
    try:
        with Image.open(path) as im:
            exif = im.getexif()
            if not exif:
                return None
            val = exif.get(IMG_DESC_TAG)
            if isinstance(val, bytes):
                try:
                    val = val.decode(errors="ignore")
                except Exception:
                    val = str(val)
            if val:
                return str(val).strip()
    except Exception:
        pass
    return None


def _clean_token(s: str) -> str:
    s = s.strip().lower().replace("_", " ").replace("-", " ")
    s = " ".join(s.split())
    return SYNONYMS.get(s, s)


def parse_matte(text: str | None,
                default_style: str = "flexible",
                default_color: str = "warm") -> tuple[str, str] | tuple[str, None]:
    """
    Parse a free-form matte description into (style, color).

    Accepts:
      - "none"
      - "apricot" (color only -> default style + that color)
      - "modern apricot" (style + color, in any spacing/case)
      - "modern_apricot" or "modern-apricot"
      - "style=modern color=apricot"  (key/value pairs, any order)
    Returns:
      ("none", None)  OR  (style, color) where both are valid.
    """
    if not text:
        return default_style, default_color

    raw = text.strip()
    # First: extract key/value pairs if present
    kv_style = kv_color = None
    # very forgiving parser for "style=... color=..." or "matte=..."
    for part in raw.replace(",", " ").split():
        if "=" in part:
            k, v = part.split("=", 1)
            k = _clean_token(k)
            v = _clean_token(v)
            if k in ("style", "matte", "matte_style"):
                kv_style = v
            elif k in ("color", "matte_color"):
                kv_color = v

    if kv_style or kv_color:
        st = kv_style or default_style
        co = kv_color or default_color
        st = _clean_token(st)
        co = _clean_token(co)
        if st == "none":
            return "none", None
        if st not in MATTE_TYPES:
            st = default_style
        if co not in MATTE_COLORS:
            co = default_color
        return st, co

    # Otherwise: tokenized words / joined tokens
    tok = _clean_token(raw)

    if tok == "none":
        return "none", None

    # combined "modern apricot" (space-joined above)
    parts = tok.split()
    if len(parts) == 2:
        st, co = parts
        if st in MATTE_TYPES and co in MATTE_COLORS:
            return st, co

    # combined "modernapricot" or "modern_apricot" were normalized to "modern apricot"
    # handle "modernapricot" specifically:
    for st in MATTE_TYPES:
        if st != "none" and tok.startswith(st):
            co = tok[len(st):].strip()
            if co in MATTE_COLORS:
                return st, co

    # color only
    if tok in MATTE_COLORS:
        return default_style, tok

    # style only
    if tok in MATTE_TYPES:
        if tok == "none":
            return "none", None
        return tok, default_color

    # fallback
    return default_style, default_color


def is_portrait(path: Path) -> bool:
    try:
        with Image.open(path) as im:
            w, h = im.size
            return h > w
    except Exception:
        return False


def file_type_for(path: Path) -> str:
    ext = path.suffix.lower()
    if ext == ".png":
        return "PNG"
    return "JPEG"


def build_matte_kwargs(style: str, color: str | None) -> dict:
    """
    Convert our parsed (style, color) to samsungtvws kwargs.
    Library accepts 'matte'/'portrait_matte' as strings like 'modern_apricot'
    or 'none'. We’ll build the proper tokens here.
    """
    if style == "none":
        matte_token = "none"
    else:
        matte_token = f"{style}_{color}"
    return {"matte": matte_token, "portrait_matte": matte_token}


def upload_one(tv: SamsungTVWS, img_path: Path, matte_kwargs: dict, delay_s: float) -> None:
    with open(img_path, "rb") as f:
        data = f.read()
    tv.art().upload(data, file_type=file_type_for(img_path), **matte_kwargs)
    if delay_s > 0:
        time.sleep(delay_s)


def main():
    parser = argparse.ArgumentParser(description="Batch upload to Samsung Frame; matte from EXIF ImageDescription.")
    parser.add_argument("--ip", required=True, help="Frame TV IP (e.g. 192.168.1.100)")
    parser.add_argument("--dir", required=True, help="Directory of images")
    parser.add_argument("--token-file", default=os.path.expanduser("~/.frame_tv_token.txt"),
                        help="Token file for pairing (created on first allow)")
    parser.add_argument("--delay", type=float, default=1.2, help="Delay (s) between uploads")
    parser.add_argument("--retry", type=int, default=1, help="Retries per file on error")
    args = parser.parse_args()

    src = Path(args.dir).expanduser().resolve()
    if not src.exists() or not src.is_dir():
        print(f"Directory not found: {src}", file=sys.stderr)
        sys.exit(2)

    files = sorted([p for p in src.iterdir() if p.suffix.lower() in (".jpg", ".jpeg", ".png")])
    if not files:
        print("No .jpg/.jpeg/.png files found.")
        sys.exit(0)

    # Connect once (secure ws on 8002)
    tv = SamsungTVWS(host=args.ip, port=8002, token_file=args.token_file)

    success = 0
    error = 0
    failed: list[str] = []

    # Single total-batch bar; filename is shown both in bar and as a line above it
    pbar = tqdm(total=len(files), unit="img", ncols=100,
                bar_format="{l_bar}{bar}| {n_fmt}/{total_fmt} [{elapsed}<{remaining}]")

    for idx, img in enumerate(files, start=1):
        # Read EXIF → parse into (style, color)
        exif_val = read_exif_imagedescription(img)
        style, color = parse_matte(exif_val)
        matte_kwargs = build_matte_kwargs(style, color)

        # Loudly show what we're doing (safe with tqdm)
        tqdm.write(f"[{idx}/{len(files)}] Uploading: {img.name}  |  matte: "
                   f"{'none' if style=='none' else f'{style}_{color}'}")
        pbar.set_description("Uploading")
        pbar.set_postfix(file=img.name,
                         matte=('none' if style == 'none' else f'{style}_{color}'),
                         refresh=True)

        attempt = 0
        last_exc = None
        while attempt <= args.retry:
            try:
                upload_one(tv, img, matte_kwargs, delay_s=args.delay)
                success += 1
                break
            except Exception as e:
                last_exc = e
                attempt += 1
                time.sleep(1.0)

        if attempt > args.retry:
            error += 1
            failed.append(f"{img.name} :: {type(last_exc).__name__}: {last_exc}")

        pbar.update(1)

    pbar.close()
    print("n=== Batch complete ===")
    print(f"Uploaded OK: {success}")
    print(f"Errors:      {error}")
    if failed:
        print("nFiles that failed after retry:")
        for f in failed:
            print(" -", f)


if __name__ == "__main__":
    main()

Don’t miss the next ride!

Subscribe now to get the latest posts emailed to you.

Continue reading

Sharing

Navigation

Comments

One response to “Part 2: Using ChatGPT to Create a Frame TV Upload Script”

  1. aservin Avatar
    aservin

    Thanks for sharing.

Leave a Reply

Discover more from Dashed Yellow Line

Subscribe now to keep reading and get access to the full archive.

Continue reading