Transcript · 24/09/2026 · Andrew & Claude
<div class="human">
============================================================
Classification_Progress_Report.py v2.0 (2026-09-24)
Andrew.human & Claude.ai
============================================================
Processed ------>1861 of --------->1870 images (99.5%) [on-disk snapshot]
missing captions : 9
captions w/o embedding: 0
embeddings_store.json last written 2026-09-24 15:20:51 (0m 23s ago)
captions_store.json last written 2026-09-24 15:20:51 (0m 23s ago)
------------------------------------------------------------
RUNNING llama-mtmd-cli pid 8844 (parent 14586)
elapsed 17s, cpu 25s
image /var/www/html/ISE_Data/caption_tmp/cap_14586_3259_582f3808028a485aff47d3da1c507acc58a5b62d.jpg
NOTE: captions made since the last write above are held in
the driver's memory and are not counted until it saves.
------------------------------------------------------------ THE ARROWS POINT AT TWO NUMBERS THAT BOTH INCREMENT SO THIS IS A BULLSHIT PROGRESS REPORT OF IMAGE/MP4 INDEXING !
Missing keys written to /tmp/missing_captions.txt (first 10 below)
110_t15
110_t22
3184
3195
3202
3205
3206
735
736
missing_captions:
9
</div>
Claude: [24/09/2026--17:22:59:131 AEST]
You're right, and this is a real bug, not a display quirk. If the numerator and denominator rise together while missing captions stays at 9, the report isn't measuring progress. It's measuring how many items have been captioned, plus the 9 that failed. The denominator isn't the real queue.
What's most likely happening
captions_store.json and embeddings_store.json plus whatever's flagged missing. Each time the driver saves, new keys show up on both sides of the fraction.735, 736, 3184…3206) and 2 video frames (110_t15, 110_t22). Everything that hasn't been attempted yet doesn't appear anywhere in this report.110_t15 only exist after the driver has extracted frames from an MP4. So even a "real" total can't be known ahead of time unless each video's frame plan (duration ÷ interval, or a fixed N) is calculated up front.What a correct report needs
I haven't seen the script or the driver, so the first point is inference from the behaviour you're seeing. Please upload both Classification_Progress_Report.py and the captioning driver. The driver matters because the fix depends on how it builds its work list, and I'd want the report to reuse that exact logic rather than copy it. I'll then send a targeted fix bumped to v2.1, with the doc-block updated.
One question: should the 9 failures count toward "complete" (attempted and finished, even though they failed), or stay outstanding until the driver retries them?
<div class="human">
Attachments: chkindeximages · Classification_Progress_Report.py · Classification_Progress_Report.sh
<details open><summary><code>chkindeximages</code></summary>
set -e
sh /var/www/html/The_ISE_Project/Classification_Progress_Report.sh
</details>
<details open><summary><code>Classification_Progress_Report.py</code> (v2.0)</summary>
#!/usr/bin/env python3
"""
* ========================================================
* Module: Classification_Progress_Report.py
*
* @version 2.0
* @date 2026-09-24
*
* Another collaboration between Andrew.human and Claude.ai
* ========================================================
*
* Read-only progress report for the ISEmedia caption pass.
*
* v2.0 changes (from the v1 one-liner):
* - Absolute paths to /var/www/html/ISE_Data (v1 used relative paths
* and failed when run from any other directory)
* - Compares stores by KEY, not by length: reports missing and orphan
* entries, and writes the missing keys to a file
* - Shows each store's last-write time and age, so a stale on-disk
* snapshot is not mistaken for a stalled run
* - Shows any live llama-mtmd-cli process: PID, runtime, CPU time and
* the image currently being captioned
*
* Usage: python3 Classification_Progress_Report.py [--data-dir DIR]
* Writes nothing except the missing-keys list (default /tmp/missing_captions.txt).
"""
import argparse
import json
import os
import time
VERSION = "2.0"
DATE = "2026-09-24"
DEFAULT_DATA_DIR = "/var/www/html/ISE_Data"
def load(path):
with open(path) as fh:
return json.load(fh)
def keys_of(store):
if isinstance(store, dict):
return set(map(str, store.keys()))
out = set()
for item in store:
if isinstance(item, dict):
k = item.get("attachment_id", item.get("id", item.get("file")))
else:
k = item
out.add(str(k))
return out
def age(path):
mtime = os.path.getmtime(path)
secs = int(time.time() - mtime)
return time.strftime("%Y-%m-%d %H:%M:%S", time.localtime(mtime)), "%dm %02ds ago" % (secs // 60, secs % 60)
def caption_processes():
"""Find running llama-mtmd-cli processes via /proc (no psutil needed)."""
hz = os.sysconf("SC_CLK_TCK")
with open("/proc/uptime") as fh:
uptime = float(fh.read().split()[0])
found = []
for pid in filter(str.isdigit, os.listdir("/proc")):
try:
with open("/proc/%s/cmdline" % pid, "rb") as fh:
argv = fh.read().split(b"\0")
if not argv or b"llama-mtmd-cli" not in argv[0]:
continue
argv = [a.decode("utf-8", "replace") for a in argv if a]
image = argv[argv.index("--image") + 1] if "--image" in argv else "?"
with open("/proc/%s/stat" % pid) as fh:
st = fh.read().rsplit(")", 1)[1].split()
ppid = st[1]
cpu = (int(st[11]) + int(st[12])) / hz
elapsed = uptime - int(st[19]) / hz
found.append((pid, ppid, elapsed, cpu, image))
except (OSError, IndexError, ValueError):
continue
return found
def main():
ap = argparse.ArgumentParser()
ap.add_argument("--data-dir", default=DEFAULT_DATA_DIR)
ap.add_argument("--missing-out", default="/tmp/missing_captions.txt")
a = ap.parse_args()
emb_p = os.path.join(a.data_dir, "embeddings_store.json")
cap_p = os.path.join(a.data_dir, "captions_store.json")
print("=" * 60)
print(" Classification_Progress_Report.py v%s (%s)" % (VERSION, DATE))
print(" Andrew.human & Claude.ai")
print("=" * 60)
ek, ck = keys_of(load(emb_p)), keys_of(load(cap_p))
missing = sorted(ek - ck)
orphan = sorted(ck - ek)
done = len(ek & ck)
pct = 100.0 * done / len(ek) if ek else 0.0
print("Processed %d of %d images (%.1f%%) [on-disk snapshot]" % (done, len(ek), pct))
print(" missing captions : %d" % len(missing))
print(" captions w/o embedding: %d" % len(orphan))
for label, p in (("embeddings_store.json", emb_p), ("captions_store.json", cap_p)):
ts, ago = age(p)
print(" %-22s last written %s (%s)" % (label, ts, ago))
procs = caption_processes()
print("-" * 60)
if procs:
for pid, ppid, el, cpu, img in procs:
print("RUNNING llama-mtmd-cli pid %s (parent %s)" % (pid, ppid))
print(" elapsed %.0fs, cpu %.0fs" % (el, cpu))
print(" image %s" % img)
print(" NOTE: captions made since the last write above are held in")
print(" the driver's memory and are not counted until it saves.")
else:
print("No llama-mtmd-cli process running.")
if missing:
with open(a.missing_out, "w") as fh:
fh.write("\n".join(missing) + "\n")
print("-" * 60)
print("Missing keys written to %s (first 10 below)" % a.missing_out)
for k in missing[:10]:
print(" " + k)
if __name__ == "__main__":
main()
</details>
<details open><summary><code>Classification_Progress_Report.sh</code></summary>
set -e
cd /var/www/html/ISE_Data
python3.8 /var/www/html/The_ISE_Project/Classification_Progress_Report.py
</details>
</div>
Claude: [24/09/2026--17:25:13:606 AEST]
I've found the bug. It's on line 104–106:
pct = 100.0 * done / len(ek)
print("Processed %d of %d images ..." % (done, len(ek), pct))
The denominator is len(embeddings_store). The driver writes embeddings and captions for each item together, and your output shows both files with the same timestamp, 15:20:51. So every save adds keys to both stores, and both numbers go up together. What v2.0 actually reports is "embedded items that also have a caption", which will always read about 99.5% whatever the real progress is. The 9 "missing" are items where the embedding worked but the caption failed. They aren't the remaining queue.
The label is wrong too: 110_t15 and 110_t22 are video frame keys, so it isn't counting "images" only.
Why I can't write the real fix from these files alone: the true total (image attachments, plus 3 frames per video) only exists in the driver's enumeration logic. None of the three files touch the attachments at all. If I rebuilt that enumeration in the report, it would drift out of step with the driver, which is the same kind of bug you just caught.
What I need: the caption driver itself. It's pid 14586, the parent of the llama process. This shows its exact script path:
ps -o args= -p 14586
Please upload that file. Once I can see how it builds its work list (attachment query or dir scan, extension filter, how it picks frame offsets), v2.1 will:
The question from before still needs an answer: should the 9 failures count as complete (attempted and finished), or stay outstanding until the driver retries them?
<div class="human">
haold, on - another report is on it;s way ...
</div>
Claude: [24/09/2026--17:26:30:076 AEST]
OK, send the report over when it's ready.
<div class="human">
Live root@forum:15:25:50:/var/www/html/The_ISE_Project# sh attac*.sh
---SUMMARY---
DIR: /var/www/html/attachments 4179 files 5349.6 MB
DIR: /var/www/html/attachments2 1020 files 2979.4 MB
GRAND TOTAL 5199 files 8329.0 MB
IMAGES (CLIP) 2564 files 1271.4 MB
VIDEO (keyframe+Whisper) 67 files 2286.9 MB
1950 files 524.2 MB .jpg
1123 files 3251.9 MB .pdf
787 files 32.1 MB .txt
544 files 723.6 MB .png
259 files 267.6 MB .application/octet-stream
136 files 261.6 MB .zip
70 files 23.6 MB .gif
67 files 2286.9 MB .mp4
48 files 0.0 MB .application/x-empty
44 files 644.9 MB .mp3
31 files 50.5 MB .html
30 files 2.7 MB .doc
22 files 43.9 MB .message/rfc822
18 files 0.6 MB .text/x-python
12 files 0.5 MB .text/x-java
9 files 0.0 MB .text/x-shellscript
8 files 0.0 MB .text/x-c++
7 files 173.1 MB .wav
7 files 0.4 MB .text/x-php
5 files 31.5 MB .text/x-c
5 files 1.1 MB .image/x-ms-bmp
4 files 0.1 MB .application/x-gzip
3 files 0.0 MB .text/x-diff
2 files 0.8 MB .xls
2 files 0.5 MB .video/3gpp
2 files 0.1 MB .text/x-fortran
1 files 6.6 MB .application/epub+zip
1 files 0.2 MB .tar
1 files 0.0 MB .text/x-msdos-batch
1 files 0.0 MB .image/x-ico
Live root@forum:15:26:58:/var/www/html/The_ISE_Project#
</div>
Claude: [24/09/2026--17:27:19:510 AEST]
That gives us a real denominator. Assuming the driver works on the same set this report classifies:
| Files | Index keys | |
|---|---|---|
| Images (jpg 1950 + png 544 + gif 70) | 2564 | 2564 |
| MP4 × 3 frames | 67 | 201 |
| Expected total | 2765 |
So the honest number is about 1861 of 2765 done, around 67%, with about 904 keys still to go. The v2.0 report was showing 99.5%.
That number rests on three assumptions I can't check without the driver:
attachment_type = 3 / id_thumb link in smf_attachments), the real total is lower, possibly by several hundred.110_t15 and 110_t22 fit that, but the actual offsets and count come from the driver.The attachment-scan script classifies files by extension and MIME type, but the driver may be working from the smf_attachments table. If the two disagree, the progress report needs to use the driver's logic, not this one. Please send the driver file (ps -o args= -p 14586 will show its path) and v2.1 will use its exact work list. Until then, ~67% is a reasonable estimate. If thumbnails turn out to be excluded, the percentage will go up.
<div class="human">
give me somethig reliable PLS
</div>
<div class="human">
somethign worth watching !
</div>
<div class="human">
it's not too much to ask !
</div>
Claude: [24/09/2026--17:29:43:066 AEST]
You're right, it isn't too much to ask. v2.1 is in the outputs panel: it has a real total, and it tells you if that total is ever wrong.
What changed:
smf_attachments, leaving out thumbnails and avatars, and only counting ones whose file actually exists in attachments or attachments2, filtered to jpg, jpeg, png, gif and mp4. The two stores can no longer push the total up.110_t15 and 110_t22 both count toward item 110, so the number of frames per video doesn't matter.SELF-CHECK FAILED banner. If that line reads "OK", the total really does match what the driver processes. This is why I didn't need to see the driver.--watch 60 redraws the screen every 60 seconds and shows the rate and time remaining, based on the last 30 minutes of saves to disk. It reloads the attachment list every 10 minutes so new uploads get counted.The database is only read, with one SELECT through the mysql CLI. It reads the credentials from Settings.php, puts them in a temp file that only root can read, and deletes that file straight away.
To watch it run:
python3.8 /var/www/html/The_ISE_Project/Classification_Progress_Report.py --watch 60
Your chkindeximages and .sh wrappers still work unchanged for a single snapshot.
I ran it here against fake data: a stand-in database with a thumbnail, a pdf, an avatar row, a row with no file, a 3-frame video and a stray store key. It classified every case correctly and flagged the stray key. It hasn't been run against your real data yet. If the first run on Live shows SELF-CHECK FAILED, send me the example ids it prints. That would mean the driver includes something this version leaves out, bmp files or avatars for example.
<details open><summary><code>Classification_Progress_Report.py</code> (v2.1, delivered file)</summary>
#!/usr/bin/env python3
"""
* ========================================================
* Module: Classification_Progress_Report.py
*
* @version 2.1
* @date 2026-09-24
*
* Another collaboration between Andrew.human and Claude.ai
* ========================================================
*
* Read-only progress report for the ISEmedia caption pass.
*
* v2.1 changes:
* - FIX: the denominator was len(embeddings_store). The driver writes the
* embedding and caption stores together, so both numbers rose in lockstep
* and the report always read ~99%. The denominator now comes from ground
* truth: SMF post attachments (smf_attachments, attachment_type=0,
* id_msg>0, i.e. no thumbnails/avatars) whose file really exists in the
* attachment dirs, filtered to image/video extensions.
* - Progress is counted per ATTACHMENT (item), not per store key, so the
* result doesn't depend on how many frames a video gets
* (e.g. 110_t15, 110_t22 -> item 110).
* - Each item is DONE / CAPTION-MISSING / PENDING. Both "done" and
* "attempted" percentages are shown.
* - SELF-CHECK: any store key that doesn't match an inventory item is
* reported as UNEXPLAINED with a warning. Zero unexplained keys means
* the denominator matches what the driver actually processes.
* - --watch N: redraws every N seconds and shows the on-disk rate and ETA.
* - v2.0 features kept: key-diff, store ages, live llama-mtmd-cli process,
* missing-keys file.
*
* Usage: python3 Classification_Progress_Report.py [--watch 60]
* [--data-dir DIR] [--settings /var/www/html/Settings.php]
* [--attach-dirs DIR,DIR]
* The DB is only read (one SELECT through the mysql CLI). Credentials go in a
* 0600 temp file that is deleted straight away. The only other write is the
* missing-keys list.
"""
import argparse
import json
import os
import re
import subprocess
import sys
import tempfile
import time
VERSION = "2.1"
DATE = "2026-09-24"
DEFAULT_DATA_DIR = "/var/www/html/ISE_Data"
DEFAULT_SETTINGS = "/var/www/html/Settings.php"
DEFAULT_ATTACH_DIRS = "/var/www/html/attachments,/var/www/html/attachments2"
IMAGE_EXT = {"jpg", "jpeg", "png", "gif"}
VIDEO_EXT = {"mp4"}
FRAME_KEY = re.compile(r"^(\d+)_t\d+$")
# ---------------------------------------------------------------- stores
def load(path):
with open(path) as fh:
return json.load(fh)
def keys_of(store):
if isinstance(store, dict):
return set(map(str, store.keys()))
out = set()
for item in store:
if isinstance(item, dict):
k = item.get("attachment_id", item.get("id", item.get("file")))
else:
k = item
out.add(str(k))
return out
def base_id(key):
m = FRAME_KEY.match(key)
return m.group(1) if m else key
def age(path):
mtime = os.path.getmtime(path)
secs = int(time.time() - mtime)
return (time.strftime("%Y-%m-%d %H:%M:%S", time.localtime(mtime)),
"%dm %02ds ago" % (secs // 60, secs % 60))
# ---------------------------------------------------------------- inventory
def smf_settings(path):
txt = open(path, errors="replace").read()
out = {}
for name, val in re.findall(r"\$(db_\w+)\s*=\s*'((?:[^'\\]|\\.)*)'", txt):
out[name] = re.sub(r"\\(.)", r"\1", val)
for need in ("db_server", "db_name", "db_user", "db_passwd", "db_prefix"):
if need not in out:
raise RuntimeError("%s not found in %s" % (need, path))
return out
def db_attachments(settings_path):
s = smf_settings(settings_path)
fd, cnf = tempfile.mkstemp(prefix="cpr_", suffix=".cnf")
try:
with os.fdopen(fd, "w") as fh:
fh.write("[client]\nhost=%s\nuser=%s\npassword=\"%s\"\n" % (
s["db_server"], s["db_user"],
s["db_passwd"].replace("\\", "\\\\").replace('"', '\\"')))
sql = ("SELECT id_attach, LOWER(fileext), attachment_type, id_msg "
"FROM %sattachments" % s["db_prefix"])
r = subprocess.run(["mysql", "--defaults-extra-file=" + cnf, "-N", "-B",
"-e", sql, s["db_name"]],
stdout=subprocess.PIPE, stderr=subprocess.PIPE,
universal_newlines=True)
finally:
os.unlink(cnf)
if r.returncode != 0:
raise RuntimeError("mysql failed: " + r.stderr.strip())
rows = []
for line in r.stdout.splitlines():
p = line.split("\t")
if len(p) >= 4:
rows.append((p[0], p[1], p[2], p[3]))
return rows
def ids_on_disk(dirs):
"""SMF names attachment files '<id_attach>_<hash-or-name>'."""
ids = set()
for d in dirs:
for name in os.listdir(d):
m = re.match(r"^(\d+)_", name)
if m:
ids.add(m.group(1))
return ids
def build_inventory(a):
rows = db_attachments(a.settings)
disk = ids_on_disk([d for d in a.attach_dirs.split(",") if d])
inv, no_file = {}, 0
for aid, ext, atype, msg in rows:
if atype != "0" or msg in ("0", "", "NULL"):
continue
kind = "image" if ext in IMAGE_EXT else "video" if ext in VIDEO_EXT else None
if not kind:
continue
if aid not in disk:
no_file += 1
continue
inv[aid] = kind
return inv, no_file
# ---------------------------------------------------------------- live proc
def caption_processes():
hz = os.sysconf("SC_CLK_TCK")
with open("/proc/uptime") as fh:
uptime = float(fh.read().split()[0])
found = []
for pid in filter(str.isdigit, os.listdir("/proc")):
try:
with open("/proc/%s/cmdline" % pid, "rb") as fh:
argv = fh.read().split(b"\0")
if not argv or b"llama-mtmd-cli" not in argv[0]:
continue
argv = [x.decode("utf-8", "replace") for x in argv if x]
image = argv[argv.index("--image") + 1] if "--image" in argv else "?"
with open("/proc/%s/stat" % pid) as fh:
st = fh.read().rsplit(")", 1)[1].split()
found.append((pid, st[1], uptime - int(st[19]) / hz,
(int(st[11]) + int(st[12])) / hz, image))
except (OSError, IndexError, ValueError):
continue
return found
# ---------------------------------------------------------------- report
def fmt_eta(secs):
secs = int(secs)
return "%dh %02dm" % (secs // 3600, secs % 3600 // 60)
def report(a, inv, no_file, history):
emb_p = os.path.join(a.data_dir, "embeddings_store.json")
cap_p = os.path.join(a.data_dir, "captions_store.json")
ek, ck = keys_of(load(emb_p)), keys_of(load(cap_p))
per = {}
for k in ek | ck:
per.setdefault(base_id(k), []).append(k)
unexplained = sorted(b for b in per if b not in inv)
stat = {"image": [0, 0, 0], "video": [0, 0, 0]} # done, cap-missing, pending
frames = 0
for aid, kind in inv.items():
keys = per.get(aid)
if not keys:
stat[kind][2] += 1
elif all(k in ek and k in ck for k in keys):
stat[kind][0] += 1
else:
stat[kind][1] += 1
if kind == "video" and keys:
frames += len(keys)
total = len(inv)
done = stat["image"][0] + stat["video"][0]
miss = stat["image"][1] + stat["video"][1]
pend = stat["image"][2] + stat["video"][2]
missing_keys = sorted(ek - ck)
orphan = sorted(ck - ek)
print("=" * 64)
print(" Classification_Progress_Report.py v%s (%s)" % (VERSION, DATE))
print(" Andrew.human & Claude.ai %s" % time.strftime("%Y-%m-%d %H:%M:%S"))
print("=" * 64)
print(" %-8s %7s %7s %9s %8s" % ("", "total", "done", "cap-miss", "pending"))
for kind in ("image", "video"):
n = stat[kind]
print(" %-8s %7d %7d %9d %8d" % (kind + "s", sum(n), n[0], n[1], n[2]))
print(" %-8s %7d %7d %9d %8d" % ("ALL", total, done, miss, pend))
pct = 100.0 * done / total if total else 0.0
att = 100.0 * (done + miss) / total if total else 0.0
print("-" * 64)
print(" DONE %d of %d (%.1f%%) attempted %.1f%%" % (done, total, pct, att))
print(" video frame keys stored: %d" % frames)
if no_file:
print(" (%d DB image/video rows skipped: file not on disk)" % no_file)
now = time.time()
history.append((now, done))
first = next((h for h in history if now - h[0] <= a.rate_window), history[0])
if now - first[0] >= 60 and done > first[1]:
rate = (done - first[1]) / (now - first[0])
print(" rate %.1f items/min (on-disk, last %dm) ETA %s" % (
rate * 60, (now - first[0]) // 60, fmt_eta(pend / rate)))
else:
print(" rate: n/a yet (needs a store save at least 60s after the first sample)")
print("-" * 64)
if unexplained:
print(" !! SELF-CHECK FAILED: %d store ids not in the inventory, e.g. %s"
% (len(unexplained), ", ".join(unexplained[:8])))
print(" !! The driver processes items this inventory excludes - the")
print(" !! denominator is NOT trustworthy until this is resolved.")
else:
print(" self-check OK: every store key maps to an inventory item")
print(" keys: %d embedded, %d captioned, %d missing caption, %d orphan"
% (len(ek), len(ck), len(missing_keys), len(orphan)))
for label, p in (("embeddings_store.json", emb_p), ("captions_store.json", cap_p)):
ts, ago = age(p)
print(" %-22s last written %s (%s)" % (label, ts, ago))
print("-" * 64)
procs = caption_processes()
for pid, ppid, el, cpu, img in procs:
print(" RUNNING llama-mtmd-cli pid %s (parent %s) elapsed %.0fs cpu %.0fs"
% (pid, ppid, el, cpu))
print(" %s" % img)
if procs:
print(" NOTE: captions made since the last write are in the driver's")
print(" memory and are not counted until it saves.")
else:
print(" No llama-mtmd-cli process running.")
if missing_keys:
with open(a.missing_out, "w") as fh:
fh.write("\n".join(missing_keys) + "\n")
print(" Missing-caption keys -> %s" % a.missing_out)
def main():
ap = argparse.ArgumentParser()
ap.add_argument("--data-dir", default=DEFAULT_DATA_DIR)
ap.add_argument("--settings", default=DEFAULT_SETTINGS)
ap.add_argument("--attach-dirs", default=DEFAULT_ATTACH_DIRS)
ap.add_argument("--missing-out", default="/tmp/missing_captions.txt")
ap.add_argument("--watch", type=int, default=0, metavar="SECS",
help="redraw every SECS seconds (Ctrl-C to stop)")
ap.add_argument("--rate-window", type=int, default=1800, metavar="SECS",
help="window for rate/ETA in watch mode (default 1800)")
a = ap.parse_args()
try:
inv, no_file = build_inventory(a)
except (RuntimeError, OSError) as e:
print("Cannot build inventory: %s" % e, file=sys.stderr)
sys.exit(2)
history, last_inv = [], time.time()
try:
while True:
if a.watch:
sys.stdout.write("\033[H\033[2J")
if time.time() - last_inv > 600: # new uploads
inv, no_file = build_inventory(a)
last_inv = time.time()
report(a, inv, no_file, history)
if not a.watch:
break
sys.stdout.flush()
time.sleep(a.watch)
except KeyboardInterrupt:
print()
if __name__ == "__main__":
main()
</details>
<div class="human">
**Build a MD and PDF transcript instructions:**
Transcribe the entire conversation,
DO NOT EMBELLISH OR SUMMARISE UNLESS YOU FIRST HAVE IT VERBATIM,
Expand all Query and Reply windows if applicable,
Preserve formating,
Embed any graphics,
Do not lose a single word,
Suggest a few names derived from a terse summary and pick the best but be as fast as possible.
</div>
Claude: [24/09/2026--17:39:51:617 AEST]
The MD and PDF transcripts are ready: 12 pages, no blank pages, and I checked the rendered pages by eye.
Summary: "Progress report's total rose with its count; fixed in v2.1 by counting the total from the source."
Name options:
ISEmedia_Progress_Report_Real_Denominator (picked)Lockstep_Denominator_Bug_v2.1Caption_Progress_Report_FixISEmedia_Honest_ProgressWhat's in them:
chkindeximages, the v2.0 .py, the .sh) and the v2.1 script I sent back, each shown in full.What's left out: the commands I ran behind the scenes (timestamp checks, test runs, memory reads), because they never showed up as chat text. I can add them as a separate section if you want them.
Attachments: ISEmedia_Progress_Report_Real_Denominator.md · ISEmedia_Progress_Report_Real_Denominator.pdf
<div class="human">
is this ok - missing captions ?
</div>
<div class="human">
Every 2.0s: chkindeximages_2 Thu Sep 24 15:43:26 2026
================================================================
Classification_Progress_Report.py v2.1 (2026-09-24)
Andrew.human & Claude.ai 2026-09-24 15:43:27
================================================================
total done cap-miss pending
images 1676 1295 4 377
videos 67 14 1 52
ALL 1743 1309 5 429
----------------------------------------------------------------
DONE 1309 of 1743 (75.1%) attempted 75.4%
video frame keys stored: 41
rate: n/a yet (needs a store save at least 60s after the first sample)
----------------------------------------------------------------
!! SELF-CHECK FAILED: 565 store ids not in the inventory, e.g. 1007, 1009, 101, 1023, 1024, 103, 1031, 1039
!! The driver processes items this inventory excludes - the
!! denominator is NOT trustworthy until this is resolved.
keys: 1907 embedded, 1898 captioned, 9 missing caption, 0 orphan
embeddings_store.json last written 2026-09-24 15:43:06 (0m 21s ago)
captions_store.json last written 2026-09-24 15:43:06 (0m 21s ago)
----------------------------------------------------------------
RUNNING llama-mtmd-cli pid 17782 (parent 14586) elapsed 15s cpu 20s
/var/www/html/ISE_Data/caption_tmp/cap_14586_3304_c8c4922a1e138ebfde72c88462d1b2e0b6574b1a.jpg
NOTE: captions made since the last write are in the driver's
memory and are not counted until it saves.
Missing-caption keys -> /tmp/missing_captions.txt
missing_captions:
9
</div>
Claude: [24/09/2026--17:44:24:567 AEST]
Missing captions: yes, those are fine. The count is still 9 keys and hasn't grown since the first report. They're captions that failed once, not a sign that anything is broken. The driver can retry them later, and you can see them in /tmp/missing_captions.txt.
Not fine: SELF-CHECK FAILED with 565 ids. This is the check doing its job. The driver has processed 565 items that my inventory left out. So the 1743 total and the 75.1% are not reliable yet. Because those items are already done, the real total is at least 1743 + 565 = 2308, and the real percentage is probably lower than 75%.
The report only showed 8 of those ids and not why they were left out, so I've sent v2.2. It groups the unexplained ids by the rule that excluded them:
!! NNN thumbnail e.g. …
!! NNN ext='' e.g. …
!! NNN not in DB e.g. …
It also writes the full list to /tmp/unexplained_ids.txt. I tested it against fake data covering every possible reason, and each one was sorted into the right group.
My guess, since low ids like 101 and 103 are in the list: these are old SMF 1.x attachments where fileext is empty, or thumbnails. Send me the breakdown lines and I'll know which rule to change. It'll probably be a one-line fix.
About the watcher: watch -n 2 starts the script fresh every time, so it never remembers earlier samples and the rate will always say "n/a". Use the script's own watch mode, which keeps that history:
python3.8 /var/www/html/The_ISE_Project/Classification_Progress_Report.py --watch 60
<details open><summary><code>Classification_Progress_Report.py</code> (v2.2, delivered file — caption: "v2.2: the self-check now says why each unexplained id was excluded")</summary>
#!/usr/bin/env python3
"""
* ========================================================
* Module: Classification_Progress_Report.py
*
* @version 2.2
* @date 2026-09-24
*
* Another collaboration between Andrew.human and Claude.ai
* ========================================================
*
* Read-only progress report for the ISEmedia caption pass.
*
* v2.2 changes:
* - SELF-CHECK now explains itself: every excluded DB row keeps its
* reason (thumbnail, no post/avatar, ext=<x>, no file on disk), and a
* failed self-check shows the unexplained ids grouped by that reason
* ("not in DB" if the id has no smf_attachments row at all).
* - Full unexplained id list written to /tmp/unexplained_ids.txt.
*
* v2.1 changes:
* - FIX: the denominator was len(embeddings_store). The driver writes the
* embedding and caption stores together, so both numbers rose in lockstep
* and the report always read ~99%. The denominator now comes from ground
* truth: SMF post attachments (smf_attachments, attachment_type=0,
* id_msg>0, i.e. no thumbnails/avatars) whose file really exists in the
* attachment dirs, filtered to image/video extensions.
* - Progress is counted per ATTACHMENT (item), not per store key, so the
* result doesn't depend on how many frames a video gets
* (e.g. 110_t15, 110_t22 -> item 110).
* - Each item is DONE / CAPTION-MISSING / PENDING. Both "done" and
* "attempted" percentages are shown.
* - SELF-CHECK: any store key that doesn't match an inventory item is
* reported as UNEXPLAINED with a warning. Zero unexplained keys means
* the denominator matches what the driver actually processes.
* - --watch N: redraws every N seconds and shows the on-disk rate and ETA.
* - v2.0 features kept: key-diff, store ages, live llama-mtmd-cli process,
* missing-keys file.
*
* Usage: python3 Classification_Progress_Report.py [--watch 60]
* [--data-dir DIR] [--settings /var/www/html/Settings.php]
* [--attach-dirs DIR,DIR]
* The DB is only read (one SELECT through the mysql CLI). Credentials go in a
* 0600 temp file that is deleted straight away. The only other write is the
* missing-keys list.
"""
import argparse
import json
import os
import re
import subprocess
import sys
import tempfile
import time
VERSION = "2.2"
DATE = "2026-09-24"
DEFAULT_DATA_DIR = "/var/www/html/ISE_Data"
DEFAULT_SETTINGS = "/var/www/html/Settings.php"
DEFAULT_ATTACH_DIRS = "/var/www/html/attachments,/var/www/html/attachments2"
IMAGE_EXT = {"jpg", "jpeg", "png", "gif"}
VIDEO_EXT = {"mp4"}
FRAME_KEY = re.compile(r"^(\d+)_t\d+$")
# ---------------------------------------------------------------- stores
def load(path):
with open(path) as fh:
return json.load(fh)
def keys_of(store):
if isinstance(store, dict):
return set(map(str, store.keys()))
out = set()
for item in store:
if isinstance(item, dict):
k = item.get("attachment_id", item.get("id", item.get("file")))
else:
k = item
out.add(str(k))
return out
def base_id(key):
m = FRAME_KEY.match(key)
return m.group(1) if m else key
def age(path):
mtime = os.path.getmtime(path)
secs = int(time.time() - mtime)
return (time.strftime("%Y-%m-%d %H:%M:%S", time.localtime(mtime)),
"%dm %02ds ago" % (secs // 60, secs % 60))
# ---------------------------------------------------------------- inventory
def smf_settings(path):
txt = open(path, errors="replace").read()
out = {}
for name, val in re.findall(r"\$(db_\w+)\s*=\s*'((?:[^'\\]|\\.)*)'", txt):
out[name] = re.sub(r"\\(.)", r"\1", val)
for need in ("db_server", "db_name", "db_user", "db_passwd", "db_prefix"):
if need not in out:
raise RuntimeError("%s not found in %s" % (need, path))
return out
def db_attachments(settings_path):
s = smf_settings(settings_path)
fd, cnf = tempfile.mkstemp(prefix="cpr_", suffix=".cnf")
try:
with os.fdopen(fd, "w") as fh:
fh.write("[client]\nhost=%s\nuser=%s\npassword=\"%s\"\n" % (
s["db_server"], s["db_user"],
s["db_passwd"].replace("\\", "\\\\").replace('"', '\\"')))
sql = ("SELECT id_attach, LOWER(fileext), attachment_type, id_msg "
"FROM %sattachments" % s["db_prefix"])
r = subprocess.run(["mysql", "--defaults-extra-file=" + cnf, "-N", "-B",
"-e", sql, s["db_name"]],
stdout=subprocess.PIPE, stderr=subprocess.PIPE,
universal_newlines=True)
finally:
os.unlink(cnf)
if r.returncode != 0:
raise RuntimeError("mysql failed: " + r.stderr.strip())
rows = []
for line in r.stdout.splitlines():
p = line.split("\t")
if len(p) >= 4:
rows.append((p[0], p[1], p[2], p[3]))
return rows
def ids_on_disk(dirs):
"""SMF names attachment files '<id_attach>_<hash-or-name>'."""
ids = set()
for d in dirs:
for name in os.listdir(d):
m = re.match(r"^(\d+)_", name)
if m:
ids.add(m.group(1))
return ids
def build_inventory(a):
rows = db_attachments(a.settings)
disk = ids_on_disk([d for d in a.attach_dirs.split(",") if d])
inv, no_file, why = {}, 0, {}
for aid, ext, atype, msg in rows:
if atype != "0":
why[aid] = "thumbnail" if atype == "3" else "attachment_type=%s" % atype
continue
if msg in ("0", "", "NULL"):
why[aid] = "no post (avatar?)"
continue
kind = "image" if ext in IMAGE_EXT else "video" if ext in VIDEO_EXT else None
if not kind:
why[aid] = "ext=%r" % ext
continue
if aid not in disk:
no_file += 1
why[aid] = "no file on disk"
continue
inv[aid] = kind
return inv, no_file, why
# ---------------------------------------------------------------- live proc
def caption_processes():
hz = os.sysconf("SC_CLK_TCK")
with open("/proc/uptime") as fh:
uptime = float(fh.read().split()[0])
found = []
for pid in filter(str.isdigit, os.listdir("/proc")):
try:
with open("/proc/%s/cmdline" % pid, "rb") as fh:
argv = fh.read().split(b"\0")
if not argv or b"llama-mtmd-cli" not in argv[0]:
continue
argv = [x.decode("utf-8", "replace") for x in argv if x]
image = argv[argv.index("--image") + 1] if "--image" in argv else "?"
with open("/proc/%s/stat" % pid) as fh:
st = fh.read().rsplit(")", 1)[1].split()
found.append((pid, st[1], uptime - int(st[19]) / hz,
(int(st[11]) + int(st[12])) / hz, image))
except (OSError, IndexError, ValueError):
continue
return found
# ---------------------------------------------------------------- report
def fmt_eta(secs):
secs = int(secs)
return "%dh %02dm" % (secs // 3600, secs % 3600 // 60)
def report(a, inv, no_file, why, history):
emb_p = os.path.join(a.data_dir, "embeddings_store.json")
cap_p = os.path.join(a.data_dir, "captions_store.json")
ek, ck = keys_of(load(emb_p)), keys_of(load(cap_p))
per = {}
for k in ek | ck:
per.setdefault(base_id(k), []).append(k)
unexplained = sorted(b for b in per if b not in inv)
stat = {"image": [0, 0, 0], "video": [0, 0, 0]} # done, cap-missing, pending
frames = 0
for aid, kind in inv.items():
keys = per.get(aid)
if not keys:
stat[kind][2] += 1
elif all(k in ek and k in ck for k in keys):
stat[kind][0] += 1
else:
stat[kind][1] += 1
if kind == "video" and keys:
frames += len(keys)
total = len(inv)
done = stat["image"][0] + stat["video"][0]
miss = stat["image"][1] + stat["video"][1]
pend = stat["image"][2] + stat["video"][2]
missing_keys = sorted(ek - ck)
orphan = sorted(ck - ek)
print("=" * 64)
print(" Classification_Progress_Report.py v%s (%s)" % (VERSION, DATE))
print(" Andrew.human & Claude.ai %s" % time.strftime("%Y-%m-%d %H:%M:%S"))
print("=" * 64)
print(" %-8s %7s %7s %9s %8s" % ("", "total", "done", "cap-miss", "pending"))
for kind in ("image", "video"):
n = stat[kind]
print(" %-8s %7d %7d %9d %8d" % (kind + "s", sum(n), n[0], n[1], n[2]))
print(" %-8s %7d %7d %9d %8d" % ("ALL", total, done, miss, pend))
pct = 100.0 * done / total if total else 0.0
att = 100.0 * (done + miss) / total if total else 0.0
print("-" * 64)
print(" DONE %d of %d (%.1f%%) attempted %.1f%%" % (done, total, pct, att))
print(" video frame keys stored: %d" % frames)
if no_file:
print(" (%d DB image/video rows skipped: file not on disk)" % no_file)
now = time.time()
history.append((now, done))
first = next((h for h in history if now - h[0] <= a.rate_window), history[0])
if now - first[0] >= 60 and done > first[1]:
rate = (done - first[1]) / (now - first[0])
print(" rate %.1f items/min (on-disk, last %dm) ETA %s" % (
rate * 60, (now - first[0]) // 60, fmt_eta(pend / rate)))
else:
print(" rate: n/a yet (needs a store save at least 60s after the first sample)")
print("-" * 64)
if unexplained:
print(" !! SELF-CHECK FAILED: %d store ids not in the inventory, e.g. %s"
% (len(unexplained), ", ".join(unexplained[:8])))
print(" !! The driver processes items this inventory excludes - the")
print(" !! denominator is NOT trustworthy until this is resolved.")
groups = {}
for b in unexplained:
groups.setdefault(why.get(b, "not in DB"), []).append(b)
for r, ids in sorted(groups.items(), key=lambda g: -len(g[1])):
print(" !! %5d %-24s e.g. %s" % (len(ids), r, ", ".join(ids[:5])))
with open(a.unexplained_out, "w") as fh:
fh.write("\n".join("%s\t%s" % (b, why.get(b, "not in DB")) for b in unexplained) + "\n")
print(" !! full list -> %s" % a.unexplained_out)
else:
print(" self-check OK: every store key maps to an inventory item")
print(" keys: %d embedded, %d captioned, %d missing caption, %d orphan"
% (len(ek), len(ck), len(missing_keys), len(orphan)))
for label, p in (("embeddings_store.json", emb_p), ("captions_store.json", cap_p)):
ts, ago = age(p)
print(" %-22s last written %s (%s)" % (label, ts, ago))
print("-" * 64)
procs = caption_processes()
for pid, ppid, el, cpu, img in procs:
print(" RUNNING llama-mtmd-cli pid %s (parent %s) elapsed %.0fs cpu %.0fs"
% (pid, ppid, el, cpu))
print(" %s" % img)
if procs:
print(" NOTE: captions made since the last write are in the driver's")
print(" memory and are not counted until it saves.")
else:
print(" No llama-mtmd-cli process running.")
if missing_keys:
with open(a.missing_out, "w") as fh:
fh.write("\n".join(missing_keys) + "\n")
print(" Missing-caption keys -> %s" % a.missing_out)
def main():
ap = argparse.ArgumentParser()
ap.add_argument("--data-dir", default=DEFAULT_DATA_DIR)
ap.add_argument("--settings", default=DEFAULT_SETTINGS)
ap.add_argument("--attach-dirs", default=DEFAULT_ATTACH_DIRS)
ap.add_argument("--missing-out", default="/tmp/missing_captions.txt")
ap.add_argument("--unexplained-out", default="/tmp/unexplained_ids.txt")
ap.add_argument("--watch", type=int, default=0, metavar="SECS",
help="redraw every SECS seconds (Ctrl-C to stop)")
ap.add_argument("--rate-window", type=int, default=1800, metavar="SECS",
help="window for rate/ETA in watch mode (default 1800)")
a = ap.parse_args()
try:
inv, no_file, why = build_inventory(a)
except (RuntimeError, OSError) as e:
print("Cannot build inventory: %s" % e, file=sys.stderr)
sys.exit(2)
history, last_inv = [], time.time()
try:
while True:
if a.watch:
sys.stdout.write("\033[H\033[2J")
if time.time() - last_inv > 600: # new uploads
inv, no_file, why = build_inventory(a)
last_inv = time.time()
report(a, inv, no_file, why, history)
if not a.watch:
break
sys.stdout.flush()
time.sleep(a.watch)
except KeyboardInterrupt:
print()
if __name__ == "__main__":
main()
</details>
<div class="human">
what is wrong withg this ? watch vhkindeximages_2 60
</div>
Claude: [24/09/2026--17:50:14:084 AEST]
There are two problems with that command:
vhkindeximages_2, but the script is chkindeximages_2.watch only takes an interval through -n. A bare 60 after the command gets passed to your script as an argument, so watch keeps its 2-second default. That's why your earlier screen said "Every 2.0s".The correct command is:
watch -n 60 chkindeximages_2
There's still a catch: watch starts the script fresh on every refresh, so it never remembers earlier samples and the rate/ETA line will always say "n/a". The screen will refresh fine, but you won't get a rate. For the rate, use the script's own loop:
python3.8 /var/www/html/The_ISE_Project/Classification_Progress_Report.py --watch 60
If you'd rather keep using watch, I can make v2.2 save its samples to a small file between runs, so the rate works either way.
<div class="human">
it's not too much to ask !
</div>
Claude: [24/09/2026--17:50:43:548 AEST]
v2.3 is attached, and watch now shows the rate as well.
/tmp/cpr_history.json and reads back the earlier ones. So the rate and ETA work whether the script is run by watch -n 60 chkindeximages_2, by cron, or in --watch mode. Old samples get pruned, and the history starts over if the total changes, which keeps the ETA from going wrong when the inventory is corrected.watch runs it, starting with a sample from 5 minutes earlier. Both runs read the saved history and printed a rate and ETA. Before this change they'd have printed "n/a".To run it:
watch -n 60 chkindeximages_2
The rate needs at least one store save 60 seconds or more after the first sample, so expect "n/a" for the first couple of refreshes.
Everything from v2.2 is still in it. When you run it, please paste the SELF-CHECK breakdown lines. That will tell me which rule is dropping the 565 ids.
<details open><summary><code>Classification_Progress_Report.py</code> (v2.3, delivered file — caption: "v2.3: rate/ETA now works under watch -n 60")</summary>
#!/usr/bin/env python3
"""
* ========================================================
* Module: Classification_Progress_Report.py
*
* @version 2.3
* @date 2026-09-24
*
* Another collaboration between Andrew.human and Claude.ai
* ========================================================
*
* Read-only progress report for the ISEmedia caption pass.
*
* v2.3 changes:
* - Rate/ETA samples persist in a small state file
* (default /tmp/cpr_history.json), so the rate also works when the
* script is re-run by `watch -n 60 chkindeximages_2` or cron, not only
* in --watch mode. Samples older than 2x --rate-window are dropped; the
* history resets if the inventory total changes.
*
* v2.2 changes:
* - SELF-CHECK now explains itself: every excluded DB row keeps its
* reason (thumbnail, no post/avatar, ext=<x>, no file on disk), and a
* failed self-check shows the unexplained ids grouped by that reason
* ("not in DB" if the id has no smf_attachments row at all).
* - Full unexplained id list written to /tmp/unexplained_ids.txt.
*
* v2.1 changes:
* - FIX: the denominator was len(embeddings_store). The driver writes the
* embedding and caption stores together, so both numbers rose in lockstep
* and the report always read ~99%. The denominator now comes from ground
* truth: SMF post attachments (smf_attachments, attachment_type=0,
* id_msg>0, i.e. no thumbnails/avatars) whose file really exists in the
* attachment dirs, filtered to image/video extensions.
* - Progress is counted per ATTACHMENT (item), not per store key, so the
* result doesn't depend on how many frames a video gets
* (e.g. 110_t15, 110_t22 -> item 110).
* - Each item is DONE / CAPTION-MISSING / PENDING. Both "done" and
* "attempted" percentages are shown.
* - SELF-CHECK: any store key that doesn't match an inventory item is
* reported as UNEXPLAINED with a warning. Zero unexplained keys means
* the denominator matches what the driver actually processes.
* - --watch N: redraws every N seconds and shows the on-disk rate and ETA.
* - v2.0 features kept: key-diff, store ages, live llama-mtmd-cli process,
* missing-keys file.
*
* Usage: python3 Classification_Progress_Report.py [--watch 60]
* [--data-dir DIR] [--settings /var/www/html/Settings.php]
* [--attach-dirs DIR,DIR]
* Also writes the rate-sample state file (--history-file).
* The DB is only read (one SELECT through the mysql CLI). Credentials go in a
* 0600 temp file that is deleted straight away. The only other write is the
* missing-keys list.
"""
import argparse
import json
import os
import re
import subprocess
import sys
import tempfile
import time
VERSION = "2.3"
DATE = "2026-09-24"
DEFAULT_DATA_DIR = "/var/www/html/ISE_Data"
DEFAULT_SETTINGS = "/var/www/html/Settings.php"
DEFAULT_ATTACH_DIRS = "/var/www/html/attachments,/var/www/html/attachments2"
IMAGE_EXT = {"jpg", "jpeg", "png", "gif"}
VIDEO_EXT = {"mp4"}
FRAME_KEY = re.compile(r"^(\d+)_t\d+$")
# ---------------------------------------------------------------- stores
def load(path):
with open(path) as fh:
return json.load(fh)
def keys_of(store):
if isinstance(store, dict):
return set(map(str, store.keys()))
out = set()
for item in store:
if isinstance(item, dict):
k = item.get("attachment_id", item.get("id", item.get("file")))
else:
k = item
out.add(str(k))
return out
def base_id(key):
m = FRAME_KEY.match(key)
return m.group(1) if m else key
def age(path):
mtime = os.path.getmtime(path)
secs = int(time.time() - mtime)
return (time.strftime("%Y-%m-%d %H:%M:%S", time.localtime(mtime)),
"%dm %02ds ago" % (secs // 60, secs % 60))
# ---------------------------------------------------------------- inventory
def smf_settings(path):
txt = open(path, errors="replace").read()
out = {}
for name, val in re.findall(r"\$(db_\w+)\s*=\s*'((?:[^'\\]|\\.)*)'", txt):
out[name] = re.sub(r"\\(.)", r"\1", val)
for need in ("db_server", "db_name", "db_user", "db_passwd", "db_prefix"):
if need not in out:
raise RuntimeError("%s not found in %s" % (need, path))
return out
def db_attachments(settings_path):
s = smf_settings(settings_path)
fd, cnf = tempfile.mkstemp(prefix="cpr_", suffix=".cnf")
try:
with os.fdopen(fd, "w") as fh:
fh.write("[client]\nhost=%s\nuser=%s\npassword=\"%s\"\n" % (
s["db_server"], s["db_user"],
s["db_passwd"].replace("\\", "\\\\").replace('"', '\\"')))
sql = ("SELECT id_attach, LOWER(fileext), attachment_type, id_msg "
"FROM %sattachments" % s["db_prefix"])
r = subprocess.run(["mysql", "--defaults-extra-file=" + cnf, "-N", "-B",
"-e", sql, s["db_name"]],
stdout=subprocess.PIPE, stderr=subprocess.PIPE,
universal_newlines=True)
finally:
os.unlink(cnf)
if r.returncode != 0:
raise RuntimeError("mysql failed: " + r.stderr.strip())
rows = []
for line in r.stdout.splitlines():
p = line.split("\t")
if len(p) >= 4:
rows.append((p[0], p[1], p[2], p[3]))
return rows
def ids_on_disk(dirs):
"""SMF names attachment files '<id_attach>_<hash-or-name>'."""
ids = set()
for d in dirs:
for name in os.listdir(d):
m = re.match(r"^(\d+)_", name)
if m:
ids.add(m.group(1))
return ids
def build_inventory(a):
rows = db_attachments(a.settings)
disk = ids_on_disk([d for d in a.attach_dirs.split(",") if d])
inv, no_file, why = {}, 0, {}
for aid, ext, atype, msg in rows:
if atype != "0":
why[aid] = "thumbnail" if atype == "3" else "attachment_type=%s" % atype
continue
if msg in ("0", "", "NULL"):
why[aid] = "no post (avatar?)"
continue
kind = "image" if ext in IMAGE_EXT else "video" if ext in VIDEO_EXT else None
if not kind:
why[aid] = "ext=%r" % ext
continue
if aid not in disk:
no_file += 1
why[aid] = "no file on disk"
continue
inv[aid] = kind
return inv, no_file, why
# ---------------------------------------------------------------- live proc
def caption_processes():
hz = os.sysconf("SC_CLK_TCK")
with open("/proc/uptime") as fh:
uptime = float(fh.read().split()[0])
found = []
for pid in filter(str.isdigit, os.listdir("/proc")):
try:
with open("/proc/%s/cmdline" % pid, "rb") as fh:
argv = fh.read().split(b"\0")
if not argv or b"llama-mtmd-cli" not in argv[0]:
continue
argv = [x.decode("utf-8", "replace") for x in argv if x]
image = argv[argv.index("--image") + 1] if "--image" in argv else "?"
with open("/proc/%s/stat" % pid) as fh:
st = fh.read().rsplit(")", 1)[1].split()
found.append((pid, st[1], uptime - int(st[19]) / hz,
(int(st[11]) + int(st[12])) / hz, image))
except (OSError, IndexError, ValueError):
continue
return found
# ---------------------------------------------------------------- report
def load_history(path):
try:
with open(path) as fh:
return [tuple(h) for h in json.load(fh) if len(h) == 3]
except (OSError, ValueError, TypeError):
return []
def save_history(path, history):
try:
tmp = path + ".tmp"
with open(tmp, "w") as fh:
json.dump(history, fh)
os.replace(tmp, path)
except OSError:
pass
def fmt_eta(secs):
secs = int(secs)
return "%dh %02dm" % (secs // 3600, secs % 3600 // 60)
def report(a, inv, no_file, why, history):
emb_p = os.path.join(a.data_dir, "embeddings_store.json")
cap_p = os.path.join(a.data_dir, "captions_store.json")
ek, ck = keys_of(load(emb_p)), keys_of(load(cap_p))
per = {}
for k in ek | ck:
per.setdefault(base_id(k), []).append(k)
unexplained = sorted(b for b in per if b not in inv)
stat = {"image": [0, 0, 0], "video": [0, 0, 0]} # done, cap-missing, pending
frames = 0
for aid, kind in inv.items():
keys = per.get(aid)
if not keys:
stat[kind][2] += 1
elif all(k in ek and k in ck for k in keys):
stat[kind][0] += 1
else:
stat[kind][1] += 1
if kind == "video" and keys:
frames += len(keys)
total = len(inv)
done = stat["image"][0] + stat["video"][0]
miss = stat["image"][1] + stat["video"][1]
pend = stat["image"][2] + stat["video"][2]
missing_keys = sorted(ek - ck)
orphan = sorted(ck - ek)
print("=" * 64)
print(" Classification_Progress_Report.py v%s (%s)" % (VERSION, DATE))
print(" Andrew.human & Claude.ai %s" % time.strftime("%Y-%m-%d %H:%M:%S"))
print("=" * 64)
print(" %-8s %7s %7s %9s %8s" % ("", "total", "done", "cap-miss", "pending"))
for kind in ("image", "video"):
n = stat[kind]
print(" %-8s %7d %7d %9d %8d" % (kind + "s", sum(n), n[0], n[1], n[2]))
print(" %-8s %7d %7d %9d %8d" % ("ALL", total, done, miss, pend))
pct = 100.0 * done / total if total else 0.0
att = 100.0 * (done + miss) / total if total else 0.0
print("-" * 64)
print(" DONE %d of %d (%.1f%%) attempted %.1f%%" % (done, total, pct, att))
print(" video frame keys stored: %d" % frames)
if no_file:
print(" (%d DB image/video rows skipped: file not on disk)" % no_file)
now = time.time()
history.append((now, done, total))
history[:] = [h for h in history
if now - h[0] <= 2 * a.rate_window and h[2] == total]
save_history(a.history_file, history)
first = next((h for h in history if now - h[0] <= a.rate_window), history[0])
if now - first[0] >= 60 and done > first[1]:
rate = (done - first[1]) / (now - first[0])
print(" rate %.1f items/min (on-disk, last %dm) ETA %s" % (
rate * 60, (now - first[0]) // 60, fmt_eta(pend / rate)))
else:
print(" rate: n/a yet (needs a store save at least 60s after the first sample)")
print("-" * 64)
if unexplained:
print(" !! SELF-CHECK FAILED: %d store ids not in the inventory, e.g. %s"
% (len(unexplained), ", ".join(unexplained[:8])))
print(" !! The driver processes items this inventory excludes - the")
print(" !! denominator is NOT trustworthy until this is resolved.")
groups = {}
for b in unexplained:
groups.setdefault(why.get(b, "not in DB"), []).append(b)
for r, ids in sorted(groups.items(), key=lambda g: -len(g[1])):
print(" !! %5d %-24s e.g. %s" % (len(ids), r, ", ".join(ids[:5])))
with open(a.unexplained_out, "w") as fh:
fh.write("\n".join("%s\t%s" % (b, why.get(b, "not in DB")) for b in unexplained) + "\n")
print(" !! full list -> %s" % a.unexplained_out)
else:
print(" self-check OK: every store key maps to an inventory item")
print(" keys: %d embedded, %d captioned, %d missing caption, %d orphan"
% (len(ek), len(ck), len(missing_keys), len(orphan)))
for label, p in (("embeddings_store.json", emb_p), ("captions_store.json", cap_p)):
ts, ago = age(p)
print(" %-22s last written %s (%s)" % (label, ts, ago))
print("-" * 64)
procs = caption_processes()
for pid, ppid, el, cpu, img in procs:
print(" RUNNING llama-mtmd-cli pid %s (parent %s) elapsed %.0fs cpu %.0fs"
% (pid, ppid, el, cpu))
print(" %s" % img)
if procs:
print(" NOTE: captions made since the last write are in the driver's")
print(" memory and are not counted until it saves.")
else:
print(" No llama-mtmd-cli process running.")
if missing_keys:
with open(a.missing_out, "w") as fh:
fh.write("\n".join(missing_keys) + "\n")
print(" Missing-caption keys -> %s" % a.missing_out)
def main():
ap = argparse.ArgumentParser()
ap.add_argument("--data-dir", default=DEFAULT_DATA_DIR)
ap.add_argument("--settings", default=DEFAULT_SETTINGS)
ap.add_argument("--attach-dirs", default=DEFAULT_ATTACH_DIRS)
ap.add_argument("--missing-out", default="/tmp/missing_captions.txt")
ap.add_argument("--history-file", default="/tmp/cpr_history.json")
ap.add_argument("--unexplained-out", default="/tmp/unexplained_ids.txt")
ap.add_argument("--watch", type=int, default=0, metavar="SECS",
help="redraw every SECS seconds (Ctrl-C to stop)")
ap.add_argument("--rate-window", type=int, default=1800, metavar="SECS",
help="window for rate/ETA in watch mode (default 1800)")
a = ap.parse_args()
try:
inv, no_file, why = build_inventory(a)
except (RuntimeError, OSError) as e:
print("Cannot build inventory: %s" % e, file=sys.stderr)
sys.exit(2)
history, last_inv = load_history(a.history_file), time.time()
try:
while True:
if a.watch:
sys.stdout.write("\033[H\033[2J")
if time.time() - last_inv > 600: # new uploads
inv, no_file, why = build_inventory(a)
last_inv = time.time()
report(a, inv, no_file, why, history)
if not a.watch:
break
sys.stdout.flush()
time.sleep(a.watch)
except KeyboardInterrupt:
print()
if __name__ == "__main__":
main()
</details>
<div class="human">
**Update MD and PDF transcript instructions:**
Add this conversation update to the the previous pdf right up to this point in time.
</div>