📝 ISEmedia_Progress_Report_Real_Denominator.mdv4.4 · 2026-09-05

ISEmedia Progress Report Real Denominator

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

What a correct report needs

  1. A fixed denominator built from the source: every image attachment, plus the expected frame keys for every video, enumerated the same way the driver enumerates them. Ideally both would share one function so they can't drift apart.
  2. Three separate counts against that inventory: done (caption + embedding), failed (the persistent 9, with reasons if the driver logs them), and pending (never attempted). Only done ÷ inventory is progress.
  3. The in-memory note stays as it is, but maybe also show "unsaved since last write ≈ N", worked out from the driver's save interval.

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:

  1. SMF thumbnails. SMF saves every image thumbnail as its own attachment file, and those are probably counted in the 1950 jpgs. If the driver skips thumbnails (by the attachment_type = 3 / id_thumb link in smf_attachments), the real total is lower, possibly by several hundred.
  2. Frames per video. I've assumed 3. The keys 110_t15 and 110_t22 fit that, but the actual offsets and count come from the driver.
  3. Edge types. 5 bmp, 1 ico and 2 3gpp files are left out here, and the driver might include them. That's only ±8, so it's minor.

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:

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>