1#!/usr/bin/env python3
2"""Work out the hint label of a link, to type it the way a user reads it.
3
4 demo-hint-label ocr
5 Reads the frame on screen now (the compositor's latest screenshot) with
6 tesseract and keeps the words and their boxes in /tmp/hint-words.json.
7 Run it while the page is still, before the hint bind, so that the
8 lookup after the bind needs no OCR.
9
10 demo-hint-label label <since-epoch> <word>...
11 Waits for the hint dump (NEFERWL_HINT_DEBUG=1) of the scan started after
12 <since-epoch>, finds the link target that shows all the words and prints
13 its label. Labels follow internal/core/hint.go: the targets whose centre
14 is on the output, sorted by top then left edge, one letter each from the
15 home-row alphabet, two letters past 26 targets.
16"""
17import csv, glob, io, json, os, re, shutil, subprocess, sys, time
18
19ALPHABET = "fjdkslaghrueiwoqptyvbcnxmz"
20STATE = os.environ.get("XDG_STATE_HOME", "/work/state")
21DUMPS = os.path.join(STATE, "neferwl", "hints")
22SHOT = "/work/shots/latest.png"
23WORDS = "/tmp/hint-words.json"
24
25
26def ocr():
27 shutil.copyfile(SHOT, "/tmp/hint-ocr.png")
28 out = subprocess.run(["tesseract", "/tmp/hint-ocr.png", "stdout", "--psm", "11", "tsv"],
29 capture_output=True, text=True, check=True).stdout
30 words = []
31 for row in csv.DictReader(io.StringIO(out), delimiter="\t", quoting=csv.QUOTE_NONE):
32 text = re.sub(r"\W", "", row["text"] or "").lower()
33 if text and float(row["conf"]) > 30:
34 x, y, w, h = (int(row[k]) for k in ("left", "top", "width", "height"))
35 words.append([text, x + w / 2, y + h / 2])
36 with open(WORDS, "w") as f:
37 json.dump(words, f)
38
39
40def newest_dump(since):
41 deadline = time.time() + 15
42 while time.time() < deadline:
43 for path in sorted(glob.glob(os.path.join(DUMPS, "*.json")), reverse=True):
44 if os.path.getmtime(path) >= since:
45 return path
46 time.sleep(0.05)
47 sys.exit("no hint scan dump after the bind")
48
49
50def label(since, wanted):
51 with open(newest_dump(since)) as f:
52 meta = json.load(f)
53 with open(WORDS) as f:
54 words = json.load(f)
55 # Core keeps the targets whose centre is on the output, then sorts them.
56 width, height = meta["width"], meta["height"]
57 targets = [t for t in meta["targets"]
58 if 0 <= (t["rect"][0] + t["rect"][2]) / 2 < width and 0 <= (t["rect"][1] + t["rect"][3]) / 2 < height]
59 targets.sort(key=lambda t: (t["rect"][1], t["rect"][0]))
60 for i, t in enumerate(targets):
61 x0, y0, x1, y1 = t["rect"]
62 inside = {w for w, cx, cy in words if x0 <= cx < x1 and y0 <= cy < y1}
63 if t["kind"] == "link" and set(wanted) <= inside:
64 n = len(ALPHABET)
65 print("link %d of %d shows %s" % (i + 1, len(targets), " ".join(wanted)), file=sys.stderr)
66 print(ALPHABET[i] if len(targets) <= n else ALPHABET[i // n] + ALPHABET[i % n])
67 return
68 sys.exit("no link shows %s (%d targets, %d words read)" % (" ".join(wanted), len(targets), len(words)))
69
70
71if sys.argv[1:2] == ["ocr"]:
72 ocr()
73elif sys.argv[1:2] == ["label"] and len(sys.argv) > 3:
74 label(float(sys.argv[2]), [w.lower() for w in sys.argv[3:]])
75else:
76 sys.exit(__doc__)