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.


Comments via Isso

Markdown formatting and simple HTML accepted.

Sometimes you have to double-click to enter text in the form (interaction between Isso and Bootstrap?). Tab is more reliable.