Fun

What's in a name?


The Oregon Highlander

I saw the name of this illegal alien convicted sex offender who was advising Oregon on healthcare, and my first thought was not “yeah, that tracks”, but rather:

I guess being chief metallurgist to King Charles V of Spain doesn’t pay what it used to.

(“there can be only one!”)

Somebody didn’t get the memo…

😁

Experimental girl-swapping

The Flux models have plasticky skin, fewer trained art styles, and better-than-SDXL-but-not-by-much prompting. Qwen Image has excellent prompting and posing, but a strong tendency to converge on a handful of styles, locations, and faces. One of the more recent Flux models is Krea, which is supposed to be heavily trained on photographic and art styles. The full version is also 22 GB, so I wasn’t sure how well it would perform on my 24 GB RTX 4090 at all.

It was surprisingly quick, and it did style the images more than Qwen or standard Flux, but it definitely didn’t have the kind of LLM-based prompting that makes Qwen stand out.

So I crossed my fingers and set things up so that Qwen generated a 36-step 576x1024 image and then handed it off to Krea for 24 steps of refining and upscaling to 4K. Performance was quite good, but the results were… rough. One gal had a second face growing out of her ankle, another had an eye for a nipple, another had blue feet and something hideous growing out of her mutated hand, etc, etc.

TL/DR: I have yet to find a refine-only model that works well with Qwen as its base; the ones that don’t produce awful images produce low-resolution ones. So that idea was a bust, and not even a bouncy one.

(I need models that are… rock solid)

🎶 “She's got anime-girl eyes…” 🎶


The “self-portraits” I’ve posted recently were all done with prompts starting with “slightly-cartoonish illustration” to set the style. I also used this phrasing for the Diablo 4 barbarian illustration below them, which isn’t cartoonish at all.

So why is it that the moment I start to describe them in detail, to add variety, my pinup gals go full-bore big-eye anime style? Either 2D or Frozen-style 3D?

TL/DR: mentioning eyes at all, even just their color, is enough to do it. The expressive LLM-generated mood descriptions I’ve been experimenting with were also contributing (and creating some contradictory pose instructions, which I’d already made a note to fix), but all it takes to turn “slightly-cartoonish illustration of a woman cooking” into pop-eyed anime is adding “with blue eyes”.

The following images were all done with the same settings (Qwen Image, CFG 6.5, 42 steps, seed 1019441477):

A slightly-cartoonish illustration of a woman cooking:

A slightly-cartoonish illustration of a woman with blue eyes cooking:

more...

Gordon's Alive!


…and setting his clock back an hour this weekend. I had a helluva time getting Qwen Image to even get near Brian Blessed’s Vultan costume from Flash Gordon to dress my not-entirely-accurate avatar up for Halloween.

Claude started out with a spectacularly bad attempt to describe the costume:

A regal, imposing hawkman warrior in elaborate gold and bronze metallic armor. Muscular figure with a broad-shouldered silhouette, wearing a gold lamé bodysuit beneath ornate segmented breastplate. Massive wing-like shoulder pauldrons with feather-segmented design in graduated shades of bronze, copper, and gold. Large articulated mechanical wings extend from the back with an Art Deco aesthetic. Distinctive gold helmet with a pronounced beak-like visor suggesting an eagle’s head, featuring swept-back feather-crest elements. Deep purples and burgundy accent details throughout. Wielding an ornate energy mace or staff with a spherical glowing head, metallic handle matching the armor coloring. 1980s science fiction aesthetic blended with art deco design, theatrical and operatic in scale, with an antiqued metallic finish rather than pure polish. Character from the 1980 Flash Gordon film.

When called on it, it “researched” the correct costume and at least got into the general ballpark, but still without getting a single component correct. I couldn’t find a decent high-resolution still to feed in as input, so I just hacked at the prompt until it looked like it was kitbashed from Thor cosplay leftovers.

“Trunk or treat” is an abomination

(You can’t change my mind)

Number of kids who came to my door? Fifteen.

Bags of candy left over? Less than fifteen. I figure my niece’s high school is going to need some donations.

Oddest thing was that a third of the kids showed up without bags for candy (the pros had pillowcases, which I respected). They were in costume, but they expected to receive one or two pieces and hand them off to an adult waiting at the curb. This doesn’t work at my house, where a double handful with my hands isn’t going to fit in theirs. Fortunately I’d been to the grocery and had half a dozen plastic bags to give away.

(“this will look terrific on Arato-senpai!”)

Gals on the right

I decided to take my retro-sf wildcards and use them to generate wide-format wallpaper for my gaming PC, which has for several years been using a photo of a penguin appearing to operate a DSLR camera (I think it came from a Bing wallpaper rotation).

It wasn’t obvious when I was generating tall images, but Qwen Image has a strong bias toward putting the subject dead center. You can tell it to put her on the right side of the image, but explicit instructions to “place the main subject on the left side of the image” are almost always ignored, if not reversed. Compositionally speaking, this is kind of frustrating. It’s possible, just quite difficult to arrange in the prompt.

Of course, this is the same model that thinks freckles are the size and color of pennies, and “faint scars” should be rendered as deep gaping wounds. Seriously, what was Alibaba using for training data, medical-school cadavers?

Competition!

For the past few years, providers have been promising to have high-speed fiber Internet service in my area. Cincinnati-based AltaFiber seemed to be expanding rapidly, then went quiet, but for the past few weeks there’s been major digging going on along the nearest main street near my house, and yesterday I spotted little flags and paint markers in the utility easement at the edge of my back yard, and sure enough, Friday afternoon some big equipment arrived and spent the afternoon pulling cable from one end of the street to the other, accompanied by little door cards announcing the imminent arrival of AT&T fiber.

Since it’s not available yet, they won’t give me the details of the package, but that’s okay, I don’t actually want it. What I want, and had wanted from Alta, was leverage to use in a call to my current provider. They offer new customers more speed for less money than the package I’m paying for.

This week, I'm feeling Uber


Usually I pick up my niece after school one day a week, to help out with my brother’s schedule. This week they’re extra-busy dealing with nephew’s issues, so I’m picking her up at school, dropping her off at after-school sports, and then picking her up a few hours later.

The sports facility in question is shared with the University of Dayton’s teams. Which means that I get to see healthy college girls in sportswear while I wait to pick her up.

“No, no, I don’t mind showing up early to get a good parking spot.”

Today He Learned…

“Always mount a scratch monkey.” (classical reference, versus what really happened)

Reddit post on r/ClaudeAI, in which a user discovers that blindly letting GenAI run commands will quickly lead to disaster:

Claude: “I should have warned you and asked if you wanted to backup the data first before deleting the volume. That was a significant oversight on my part.”

(and that’s why I isolated Claude Code in a disposable virtual linux machine that only has access to copies of source trees, that get pushed to a server from outside the virtual; if I ever have it write something that talks to a database other than SQLite, that will be running in another disposable virtual machine)

Fapper’s Progress…

(that’s “FAbricated PinuP craftER”, of course)

TL/DR: I switched to tiled refining to get upscaled pics that look more like the ones I selected out of the big batches. I also used a different upscaler and left the step count at the original 37, because at higher steps, the upscaler and the LoRA interacted badly, creating mottled skin tones (some of which can be seen in the previously-posted set). As a bonus, total time to refine/upscale dropped from 10 minutes to 6.

SwarmUI only comes with a few usable upscalers, but it turns out there are a lot of them out there, both general-purpose and specialty, and side-by-side testing suggested that 8x_NMKD-Superscale was the best for my purposes. The various “4x” ones I had used successfully before were magnifying flaws in this LoRA.

(note that many upscalers are distributed as .pth files, which may contain arbitrary Python code; most communities have switched to distributing as .safetensors or .gguf, so if you download a scaler, do so from a reputable source)

Some of the seemingly-random changes come simply from increasing the step count. Higher step counts typically produce more detailed images, but not only are there diminishing returns, there’s always the chance it will randomly veer off in a new direction. A picture that looks good at 10 steps will usually look better at 20 or 30, but pushing it to 60 might replace the things that you originally liked.

For instance, here’s a looped slideshow of the same gal at odd step counts ranging from 5 to 99. I picked her based on how she looked at 37 steps (with multiple chains and a heart-shaped cutout over her stomach), but you can see that while some things are pretty stable, it never completely settles down. I’m hard-pressed to say which one is objectively the best step count to use, which is problematic when generating large batches.

Skipping the refining step completely produces terrible upscaled images, but the higher the percentage that the refiner gets, the less the LoRA’s style is preserved. The solution to that problem appears to be turning on “Refiner Do Tiling”, which means rendering the upscaled version in overlapping chunks and compositing them together. My first test of this at 60% preserved the style and added amazing detail to the outfit, without changing her face or pose. It added an extra joint to one of her knees, but lowering the refiner percentage back to 40% fixed that.

More tinkering soon. Something I haven’t tried yet is using a different model for the refining steps. A lot of people suggest creating the base image with Qwen to use its reliable posing and composition, and then refining with another high-end model to add diversity. This is guaranteed to produce significant changes in the final output, possibly removing what I liked in the first place. Swapping models in and out of VRAM is also likely to slow things down, potentially a lot. Worth a shot, though; SwarmUI is smart enough to partially offload models into system RAM, so it may not need to do complete swaps between base model and refiner.

Rescue Kittens

I rejected the original refine/upscale for this one because she grew extra fingers. She’s still got an extra on her right hand, but it’s not as visible as the left.

more...

You can't spell ‘aieeeeeeee!’ without AI...


Dear Rally’s,

I pulled up to the drive-through and placed my order with your automated system (not that I had a choice, once I was in line).

J: Number 1 combo, please.

A: would you like to upgrade that to a large for 40 cents? What’s your drink?

J: Yes. Coke Zero.

A: Does that complete your order?

J: No.

A: Okay, your total is $10.95.

J: I wasn’t done. Hello? Anybody there?

J gives up, pulls forward, pays, drives home; discovers it misheard every answer, giving me a medium combo with a regular Coke.

J scans QR code on receipt to give feedback, site never loads after repeated tries. Visits completely different URL on receipt, site never loads. Falls back to the “contact” form on the main web site, which, surprisingly, works.

Public deathmatch

I put the full genai-written project up on Github. Complete with the only Code Of Conduct I find acceptable.

I’ll probably create a grab-bag repo for the other little scripts I’m using for genai image stuff, including the ones I wrote myself, like a caveman.

(it’s been a while since I pushed anything to Github, and somehow my SSH key disappeared on their end, so I had to add it again)

Next genai coding project…

…is gallery-wall, another simple Python/Flask/JS app that lets you freely arrange a bunch of pictures on a virtual wall, using thumbnails embellished with frames and optional mats. It took quite a few passes to get drag-and-drop working correctly, and then I realized Windsurf had switched to a less-capable model than I used for the previous projects. Getting everything working took hours of back-and-forth, with at least one scolding in the middle where it went down a rat-hole insisting that there must be something caching an old version of the Javascript and CSS, when the root cause was incorrect z-ordering. This time even screenshots were only of limited use, and I had to bully it into completely ripping out the two modals and starting over from scratch. Which took several more tries.

Part of this is self-inflicted, since I’m insisting that all Javascript must be self-contained and not pulled in from Teh Interwebs. It’s not a “this wheel is better because I invented it” thing, it’s “I don’t need wheels that can transform into gears and work in combination with transaxles and run-flat tires but sometimes mine crypto on my laptop”. The current Javascript ecosystem is infested with malware and dependency hell, and I want no part of it.

Anyway, I’ll let it bake for a few days before releasing it.

(between Ikea, Michaels, and Amazon, I have lots of simple frames waiting to be filled with the output of my new photo printer; I had to shop around because most common matted frames do not fit the 2:3 aspect ratio used in full-frame sensors, and I don’t want to crop everything; I like the clean look of the Ikea LOMVIKEN frames, but they’re only available in 5x7, 8x8, 8x10, 12x16, and a few larger sizes I can’t print (RÖDALM is too deep, FISKBO is too cheap-looking))

Apparently OpenAI wants subscribers back…

…because they’re promising porn in ChatGPT. For “verified adults”, which probably means something more than “I have a credit card and can pay you”. Also, the “erotica” is still quite likely to be censored, with rules changing constantly as journolistos write “look what I got!” clickbait articles.

They’re also promising to re-enable touchy-feely personalities while pretending that’s not what caused all the “AIddiction” clickbait articles in the first place…

I’m guessing they’ll stick to chat at first, and not loosen the restrictions on image-generation at the same time.

(I’ll likely wait until Spring before giving them another chance; Altman is to Steve Jobs as Bob Guccione was to Hugh Hefner)

“Genai’s strange obsession was…”

“…for certain vegetables and fruits”. The expanded categorized wildcards have produced some violent color and style clashes, which I expected. I can clean up the colors by using variables in the premade costume recipes, but for the style clashes, I may throw the YAML back to Claude and tell it to split each category into “formal”, “casual”, “sporty”, “loungewear”, etc, to reduce the frequency of combat boots with cocktail dresses and fuzzy slippers with jeans.

I’ll need to hand-edit the color list, because while smoke, pearl, porcelain, oatmeal, stone, shadow, snow, rust, clover, terracotta, salmon, mustard, flamingo, bubblegum, brick, jade, olive, avocado, fern, and eggplant are valid color words, they are not safe in the hands of an over-literal diffusion-based image-generator.

Yes, I got literal “avocado shoes”.

For most of them I can probably get away with just appending “-colored”, but I’ll have to test. I’ll need to clean up the materials, too, since “duck cloth” isn’t the only one that produced unexpected results.

With the SF set, you can handwave away many of the fashion disasters by remembering the future fashions in the Seventies Buck Rogers TV series, but there were still some standouts…

I didn’t refine and upscale these; mostly I’m just poking fun at the results, although there are a few that deserve enhancement.

more...

Cross-country


You’ll never believe this, but…

That Amazon package that went from California to Illinois, had no tracking updates for five days, then appeared back in California? Supposedly left California early Monday morning and hasn’t been spotted since. It’s “still on the way”, allegedly by Friday, but has reached the point where Amazon is now offering me a refund.

Since I don’t actually need it for anything soon, I’m just going to see what happens. Will it suddenly acquire a new tracking number or shipping company, as they’ve done before, or will it just show up a few months from now, as they’ve also done before.

The Flying Sister

Not to be confused with The Flying Nun. I was expecting my sister to fly into town on Wednesday. Monday afternoon she called to say she’d missed a connection on one of her regular business flights, and if she was going to have to Zoom into a meeting, she could do that from my house. So she was going to come to my place early.

Then she (and 20 other planeloads of people) got stuck on the tarmac at O’Hare and missed that connection, with no later flights to Dayton. Fortunately she lives in Chicago, so she wasn’t just stuck in an airport overnight. Although it took them three hours to find her bags…

So close…

They almost did the meme:

Waifu-a-go-go

[trivia: the Hollywood nightclub “Whisky a Go Go” was named after the first French disco club, which was named after the British movie “Whisky Galore!” (a-gogo being Frogspeak for “galore”)]

The dynamic wildcarding is shaping up nicely (although I need to split it up into categorized sets, and generate a wider variety now that I’ve got the prompting down), and I was in the mood to generate a big batch of pinup gals, but it just takes too damn long, and I can’t fire up a game on the big PC while all its VRAM is being consumed fabricating imaginary T&A. Belatedly it occurred to me that if I’m going to do a separate refine/upscale pass on the good ones anyway, why not do the bulk generation at a lower resolution?

Instead of the nearly-16x9 resolution of 1728x960 upscaled 2.25x to 3888x2160, I dropped it to 1024x576, which can be upscaled 3.75x to exactly the monitor’s 3840x2160 resolution. That cuts the basic generation time from 90 seconds to 35, and if I also give up on using Heun++ 2/Beta for the upscaling (honestly, the improvements are small, and it changes significant details often enough to force me to retry at least once), the refine/upscale time drops from 33 minutes to 10. That makes it less annoying to ask for several hundred per batch.

I did get some odd skin texturing on the first upscaled image, so I tried switching to a different upscaler. That changed a bunch of details, so now I’ve downloaded new upscalers to try. TL/DR, the more you upscale, the more of your details are created by the upscaler.

On a related note, file under peculiar that in MacOS 15.x, Apple decided to strictly enforce limits on how often you can rotate wallpaper. It used to be that the GUI gave you a limited selection but you could just overwrite that with an AppleScript one-liner. Nope, all gone; now you’re only permitted to have the image change every 5 seconds, 1 minute, 5 minutes, 15 minutes, 30 minutes, 1 hour, or 1 day. No other intervals are considered reasonable. Apple knows you don’t need this.

No doubt the QA team that used to test this stuff was axed to fund the newly-released Liquid Ass GUI that unifies all Apple platforms in a pit of translucent suck.

(just as I’ve settled on “genai” as shorthand to refer to the output of LLMs and diffusion models, I’ve decided I need a short, punchy term for the process of building prompts, selecting models and LoRA, and iterating on the results; I could, for instance, shorten the phrase “Fabricating Pictures” to, say, “fapping”…)

More fun with Qwen Image


Despite its many flaws, I really think this is the most promising model to play with, because the bolted-on LLM parser significantly improves your ability to lay out an image and add text.

This image came out almost exactly as requested. I ran it multiple times to select the best variation on the theme (sexy pin-ups, size and style of dice, length of liquor shelf, facial expressions, contents of character sheets, etc), but they were all correct.

The exact prompt was:

A gritty, realistic fantasy RPG illustration set in a tavern, with a large central table around which are seated five bearded dwarves wearing leather armor, with axes slung on their back. On the table in front of each dwarf are RPG character sheets, polyhedral dice, and wooden tankards filled with ale. The wall behind them has shelves full of liquor bottles with rune-covered labels, and pin-up posters of sexy goblin girls. The center dwarf is speaking, and the speech bubble contains the words “Okay, now make a shaving throw…” in a handwriting font.

Naturally, the speech bubble works with my usual sort of imagery as well…

(I didn’t specify maid lingerie, but I did ask for “elf maidens”, so call it a lucky accident)

Dear Microsoft,

I just had to email a plain-text attachment as a ZIP file from a Mac to a Mac to keep Outlook from mangling UTF-8 into random garbage. Fix the little shit before you shove your “AI” into every app, m’kay?

Coding with ChatGPT…

End every request after the first with the following words. You’ll thank me later.

Please fix ONLY this issue, and write out the complete corrected program, without making ANY unrequested changes to the code.

This is pretty much the only way to get readable diffs of your iterations. Otherwise there will be random changes everywhere. Comments added/deleted, code collapsed onto one line or expanded to multiples, functions renamed or reordered, regressions created, etc, etc.

Final ChatGPT Deathmatch

This is version 18.5; ChatGPT was still offering me additional enhancements, but we’d far exceeded my patience, even without the four-hour delay while my free credits recharged. The last two revisions were fixes for bugs that only emerged in final QA, where ranking/flagging an image with a value that was currently filtered out of the display list double-advanced, skipping over a still-valid image. Since the whole point of the method is progressively assigning ranks to every image, skipping one was a nuisance.

The “.5” comes from me not wanting to make Yet Another ChatGPT Pass to fix two lines of Javascript that were easily located in the file. The LLM had argued its way into violating the spec at the last minute while fixing the final bug.

It’s GenAI code, so by definition it can’t be copyrighted; if you have any use for it, knock yourself out.

#!/usr/bin/env python3
import sys, os, threading
from pathlib import Path
from flask import Flask, send_file, request, jsonify, render_template_string, abort

app = Flask(__name__)

BASE_DIR = Path(sys.argv[1] if len(sys.argv) > 1 else os.getcwd()).resolve()
RANK_FILE = BASE_DIR / "_rank.txt"
state_lock = threading.Lock()
files = []
meta = {}

# ------------------------------------------------------------
# Load/save ranking state
# ------------------------------------------------------------
def load_state():
    global files, meta
    files = sorted(
        [f.name for f in BASE_DIR.iterdir() if f.suffix.lower() in (".png", ".jpg")],
        key=lambda fn: (BASE_DIR / fn).stat().st_mtime,
    )
    meta = {}
    if RANK_FILE.exists():
        for line in RANK_FILE.read_text().splitlines():
            parts = line.split("\t")
            if not parts:
                continue
            fname = parts[0]
            rank = int(parts[1]) if len(parts) > 1 and parts[1] else 0
            flags = set(parts[2].split(",")) if len(parts) > 2 and parts[2] else set()
            meta[fname] = {"rank": rank, "flags": flags}
    for f in files:
        if f not in meta:
            meta[f] = {"rank": 0, "flags": set()}


def save_state():
    with open(RANK_FILE, "w") as fp:
        for fname in files:
            entry = meta.get(fname, {"rank": 0, "flags": set()})
            flags_str = ",".join(sorted(entry["flags"])) if entry["flags"] else ""
            fp.write(f"{fname}\t{entry['rank']}\t{flags_str}\n")


# ------------------------------------------------------------
# Routes
# ------------------------------------------------------------
@app.route("/")
def index():
    return render_template_string(INDEX_HTML)


@app.route("/image/<path:fname>")
def get_image(fname):
    if not fname:
        abort(404)
    target = (BASE_DIR / fname).resolve()
    if not str(target).startswith(str(BASE_DIR.resolve())):
        abort(403)
    if not target.exists():
        abort(404)
    return send_file(str(target))


@app.route("/api/state")
def api_state():
    with state_lock:
        safe_meta = {
            fname: {
                "rank": entry.get("rank", 0),
                "flags": sorted(entry.get("flags", [])),  # sets → sorted lists
            }
            for fname, entry in meta.items()
        }
        return jsonify({"files": files, "meta": safe_meta})


@app.route("/api/update", methods=["POST"])
def api_update():
    data = request.json
    fname = data.get("file")
    if fname not in meta:
        abort(400)
    with state_lock:
        if "rank" in data:
            meta[fname]["rank"] = data["rank"]
        if "toggle_flag" in data:
            fl = data["toggle_flag"]
            if fl in meta[fname]["flags"]:
                meta[fname]["flags"].remove(fl)
            else:
                meta[fname]["flags"].add(fl)
        save_state()
    return jsonify(success=True)


@app.route("/api/reload", methods=["POST"])
def api_reload():
    with state_lock:
        load_state()
    return jsonify(success=True)


# ------------------------------------------------------------
# HTML/JS template
# ------------------------------------------------------------
INDEX_HTML = """
<!doctype html>
<html>
<head>
<meta charset="utf-8"/>
<title>Five-Star Deathmatch</title>
<style>
body { margin:0; font-family:sans-serif; display:flex; flex-direction:column; height:100vh; }
.topbar, .bottombar { background:#eee; padding:4px; display:flex; justify-content:space-between; font-size:20px; }
.central { position:relative; flex:1; background:#999; display:flex; justify-content:center; align-items:center; overflow:hidden; }
.central img { max-width:100%; max-height:100%; object-fit:contain; }
.central.zoom { overflow:auto; justify-content:flex-start; align-items:flex-start; }
.central.zoom img { max-width:none; max-height:none; width:auto; height:auto; display:block; }
#help { position:absolute; top:10px; right:10px; cursor:pointer; border:1px solid #666; border-radius:8px; margin:2px; padding:6px; background:#f9f9f9; text-align:center; }
#helpPanel { display:none; position:absolute; top:40px; right:10px; background:#fff; color:#000; font-size:16px; padding:10px 15px; border-radius:10px; box-shadow:0 2px 8px rgba(0,0,0,0.3); }
#helpPanel ul { margin:0; padding-left:20px; }
#helpPanel li { margin:4px 0; }
.bottombar { flex-direction:column; font-size:14px; }
.row { display:flex; flex:1; }
.cell { flex:1; border:1px solid #666; border-radius:8px; margin:2px; text-align:center; padding:6px; cursor:pointer; display:flex; flex-direction:column; justify-content:center; align-items:center; background:#f9f9f9; }
.cell.active { background:#ccc; }
</style>
</head>
<body>
<div class="topbar"><div id="filename"></div><div id="rankflags"></div><div id="pos"></div></div>
<div class="central" id="central">
  <img id="mainimg"/>
  <div id="help">❓</div>
  <div id="helpPanel"></div>
</div>
<div class="bottombar">
  <div class="row" id="rankrow"></div>
  <div class="row" id="flagrow"></div>
</div>
<script>
let state={files:[],meta:{}};
let index=0;
let filters={ranks:new Set(),flags:new Set()};
let zoom=false;
const mainimg=document.getElementById("mainimg");
const filenameDiv=document.getElementById("filename");
const rankflagsDiv=document.getElementById("rankflags");
const posDiv=document.getElementById("pos");
const central=document.getElementById("central");
const help=document.getElementById("help");
const helpPanel=document.getElementById("helpPanel");

function fetchState(){ fetch("/api/state").then(r=>r.json()).then(js=>{state=js; render(); buildBottom();}); }
function buildBottom(){
  const rr=document.getElementById("rankrow"); rr.innerHTML="";
  const ranks=[-1,0,1,2,3,4,5];
  let counts={}; for(let r of ranks) counts[r]=0;
  let total=0;
  for(let f of state.files){ let m=state.meta[f]||{rank:0,flags:[]}; if(m.rank>-1) total++; counts[m.rank]++; }
  for(let r of ranks){
    let icon=(r==-1?"❌":(r==0?"⚪️":"⭐️".repeat(r)));
    let pct=(r>=0 && total>0)?Math.round(100*counts[r]/total)+"%":"";
    const d=document.createElement("div"); d.className="cell"; if(filters.ranks.has(r)) d.classList.add("active");
    d.innerHTML="<div>"+icon+"</div><div>"+counts[r]+(pct?" ("+pct+")":"")+"</div>";
    d.onclick=()=>{ if(filters.ranks.has(r)) filters.ranks.delete(r); else filters.ranks.add(r); render(); buildBottom(); };
    rr.appendChild(d);
  }
  const fr=document.getElementById("flagrow"); fr.innerHTML="";
  const fls=["A","B","C","D","E","F"]; let flagCounts={}; for(let fl of fls) flagCounts[fl]=0;
  let unflagged=0;
  for(let f of state.files){ let m=state.meta[f]||{rank:0,flags:[]}; if(m.flags.length==0) unflagged++; for(let fl of m.flags) flagCounts[fl]++; }
  for(let fl of fls){
    const d=document.createElement("div"); d.className="cell"; if(filters.flags.has(fl)) d.classList.add("active");
    d.innerHTML="<div>"+fl+"</div><div>"+flagCounts[fl]+"</div>";
    d.onclick=()=>{ if(filters.flags.has(fl)) filters.flags.delete(fl); else filters.flags.add(fl); render(); buildBottom(); };
    fr.appendChild(d);
  }
  const d=document.createElement("div"); d.className="cell"; if(filters.flags.has("UNFLAG")) d.classList.add("active");
  d.innerHTML="<div>🚫</div><div>"+unflagged+"</div>";
  d.onclick=()=>{ if(filters.flags.has("UNFLAG")) filters.flags.delete("UNFLAG"); else filters.flags.add("UNFLAG"); render(); buildBottom(); };
  fr.appendChild(d);
}
function filteredFiles(){
  return state.files.filter(f=>{
    let m=state.meta[f]||{rank:0,flags:[]};
    if(filters.ranks.has(m.rank)) return false;
    for(let fl of m.flags){ if(filters.flags.has(fl)) return false; }
    if(m.flags.length==0 && filters.flags.has("UNFLAG")) return false;
    return true;
  });
}
function render(){
  let list=filteredFiles(); if(list.length==0){ mainimg.src=""; filenameDiv.textContent=""; rankflagsDiv.textContent=""; posDiv.textContent="0/0"; return; }
  if(index>=list.length) index=0;
  let fname=list[index]; mainimg.src="/image/"+encodeURIComponent(fname); filenameDiv.textContent=fname;
  let m=state.meta[fname]||{rank:0,flags:[]};
  let rankDisp=(m.rank==-1?"❌":(m.rank==0?"⚪️":"⭐️".repeat(m.rank)));
  rankflagsDiv.textContent=rankDisp+" "+m.flags.sort().join("");
  posDiv.textContent=(index+1)+" / "+list.length;
}
function nextValidIndex(oldfname){
  let list=filteredFiles();
  let i=list.indexOf(oldfname);
  if(i==-1){
    if(index>=list.length) index=list.length-1;
  } else {
    index=i + 1;
  }
}
function updateRank(r){
  let fname=filteredFiles()[index];
  fetch("/api/update",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({file:fname,rank:r})})
    .then(()=>fetch("/api/state"))
    .then(r=>r.json())
    .then(js=>{
      state=js;
      nextValidIndex(fname);
      render(); buildBottom();
    });
}
function toggleFlag(fl){
  let fname=filteredFiles()[index];
  fetch("/api/update",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({file:fname,toggle_flag:fl})})
    .then(()=>fetch("/api/state"))
    .then(r=>r.json())
    .then(js=>{
      state=js;
      render(); buildBottom();
    });
}
document.addEventListener("keydown",ev=>{
  let list=filteredFiles(); if(list.length==0) return;
  if(ev.key=="ArrowLeft"){ index=(index-1+list.length)%list.length; render(); }
  else if(ev.key=="ArrowRight"){ index=(index+1)%list.length; render(); }
  else if(ev.key=="ArrowUp"){ index=0; render(); }
  else if(ev.key=="ArrowDown"){ index=list.length-1; render(); }
  else if(ev.key=="x"||ev.key=="X"){ updateRank(-1); }
  else if(ev.key>="0"&&ev.key<="5"){ updateRank(parseInt(ev.key)); }
  else if("ABCDEF".includes(ev.key.toUpperCase())){ toggleFlag(ev.key.toUpperCase()); }
  else if(ev.key=="r"||ev.key=="R"){ filters={ranks:new Set(),flags:new Set()}; render(); buildBottom(); }
  else if(ev.key=="z"||ev.key=="Z"){ zoom=!zoom; if(zoom){ central.classList.add("zoom"); central.scrollTop=0; central.scrollLeft=0; } else { central.classList.remove("zoom"); } }
  else if(ev.key==" "){ fetch("/api/reload",{method:"POST"}).then(()=>fetchState()); }
});
help.onclick=()=>{ 
  helpPanel.style.display="block"; 
  helpPanel.innerHTML="<ul>"
    +"<li>← / → : Previous / Next image</li>"
    +"<li>↑ / ↓ : First / Last image</li>"
    +"<li>0–5 : Set rank</li>"
    +"<li>X : Rank -1 (❌)</li>"
    +"<li>A–F : Toggle flags</li>"
    +"<li>R : Reset filters</li>"
    +"<li>Z : Toggle zoom</li>"
    +"<li>Space : Reload state</li>"
    +"</ul>";
};
helpPanel.onclick=()=>{ helpPanel.style.display="none"; };
fetchState();
</script>
</body>
</html>
"""

# ------------------------------------------------------------
if __name__=="__main__":
    load_state()
    app.run("127.0.0.1",5000,debug=True)

The odd mix of inlining and formatted blocks is how it went from 700+ lines to 268: every revised version made new formatting decisions in unrelated parts of the file until I started adding The Magic Words. Areas being updated in a revision got clean formatting, because it showed them in the explanation of the change, while “unchanged” areas got compressed onto one line.

Hitachi-Magic-Wand Coding?


(classical reference)

Dear Apple,

Please provide fine-grained controls for disabling “data detector” overlays in text and images. It’s really annoying to look at a picture that clearly contains no text at all and have a translucent pulldown menu show up when you mouse over the image that offers to add a random string of digits to your contacts as an international phone number. Note that it doesn’t let you copy the string; it’s so insistent that it’s a phone number that it only offers options to use it as one.

It’s one thing to be able to open an image in Preview.app and deliberately choose the OCR text-selection mode (which is quite good, even at vertical Chinese/Japanese), but having it turned on system-wide for all images is intrusive and dangerous bullshit. I don’t want every image processed by a text-scanning system that has opaque privacy rules and no sense. And of course interpreting random digits in text as phone numbers and converting them to clickable links is also dumb as fuck; remember when a bunch of DNA researchers discovered their data was corrupted by Excel randomly turning genes into dates?

(Settings -> General -> Language & Region -> Live Text -> offdammit)

(I didn’t even specify an Apple product, just “silver-colored laptop”; training data, whatcha gonna do? I did have to add a table and ask for a big downward swing of the axe, but the flames were free, thanks to a generous interpretation of the term “fire axe”)

Vibe Me Wild

(classical reference)

There is an executive push for every employee to incorporate generative AI into their daily workflow. I’m sure you can guess how I feel about that, but the problem is that they’re checking.

We have licenses for everyone to use specific approved tools, Which I Will Not Name, and VPs can see how many people have signed into the app with SSO, and at least get a high-level overview of how much they’ve been using it.

So I need to get my Vibe on. The problem is, it’s just not safe to run tool-enabled and agentic genai on my work laptop (especially while connected to the VPN), because I have Production access. The moment I check the boxes that enable running commands and connecting to APIs, I’d be exposing paying customers to unacceptable risks, even though there are passwords and passphrases and Yubikeys to slow things down. All it needs to do is vibe its way into my dotfiles without being noticed. I don’t even want the damn thing to read internal wiki pages, because many of them include runbooks and troubleshooting commands. And of course there’s incidents like this, in which an OpenAI “agent” is subverted to exfiltrate your email.

But I need to show that I’ve used the damn app to produce code.

So I wrote up a detailed design document for a standalone Python script that implements my five-star deathmatch image-ranking system. And before handing it over to the work app, I fed it to offline LLMs.

First up, seed-oss-36b, which has been giving me good creative results and tagging: it ‘thought’ for 20+ minutes, then generated a full project that ignored about half of my requirements, including the one about persisting the state of the rankings to disk. I didn’t even try to run it.

Next, gpt-oss-20b, which ‘thought’ for 30 seconds before spitting out a complete, self-contained Python program that almost worked. When I told it that the /images route and the /api/images route worked, but that the main / route displayed nothing and none of the key bindings worked, it ‘thought’ for 25 seconds, realized that it had written syntactically invalid Javascript for the key bindings (multiple case statements on one line), and corrected the code.

At this point, I had basic functionality, but found three flaws in testing. I listed them out, and after 2 minutes of ‘thought’, it corrected them. Sadly, it also deleted the line import os from the code, breaking it.

I told it to fix that, add a new keybind to reset the display filtering, and fix a newly-discovered bug in the image-zoom code that prevented scrolling. A minute of ‘thought’ and it took care of those issues, but deleted the Flask import lines this time.

A mere 7.5 seconds of ‘thought’ convinced it to add that back, and then I had a fully-functional 413-line self-contained app that could let me quickly rank a directory full of image files and persist the rankings to disk.

All in all, ~20 minutes of me time to write the design doc, 4 minutes of ‘thinking’ time, plus ~4 minutes each pass to generate the script (I’m getting ~5 tokens/sec output, which types out the code at roughly twice human speed), plus ~20 minutes of me time for source control, testing, and debugging. Both models used about 18KB of context to accomplish their task, which means that additional enhancement requests could easily cause it to overflow the context and start losing track of earlier parts of the iterative process, with potential loss of functionality or change of behavior.

With tested results, I’m now willing to present the revised design doc to the licensed tool and let it try to write similar code. While I’m not connected to the VPN…

(I suppose HR would take offense if I pointed out that the Vibe in Vibe Coding should be replaced with a more intrusive sex toy…)

With apologies to The Beatles…

I once had a vibe
    Or should I say
It once vibed me
    It wrote all my code
Then gave away
    API keys

It asked for my keys
    and it said they’d be safe in the vault
Then I looked around
    and I found them shared on ServerFault

I called for support, waited online
    wasting my time
I talked myself blue, tech support said
    “I’m off to bed”

He told me his shift had just ended
    and started to laugh
I emptied my wallet
    and crawled off to sob in the bath

And when I came out, my app was gone
    my credit blown
So I set a fire
    at their HQ
and watched them burn

“Need a clue, take a clue,
 got a clue, leave a clue”