aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorDanilo M. <danix@danix.xyz>2026-10-16 15:20:46 +0200
committerDanilo M. <danix@danix.xyz>2026-10-16 15:20:46 +0200
commitb81d322fc61e0be533912b074ef17f0f72311760 (patch)
tree2f37f44650ac66217f7836bef3fca1b5eaacdac2
parent056853313dd73975a8af52e151af5eed33253fc7 (diff)
downloaddesktop-assistant-b81d322fc61e0be533912b074ef17f0f72311760.tar.gz
desktop-assistant-b81d322fc61e0be533912b074ef17f0f72311760.zip
Page renders never upscale; a non-stream answer is an error
-scale-to 2048 also enlarges: a 600x300 pt scan went out as 2048x1024 beside a second image, and llama-server's model process died on it. The router then answered with a plain 'proxy error: Failed to read connection', which the parser skipped as a non-data line, so the turn finished as an empty reply. Pages now render at 150 dpi, lowered only for a page whose longest side would pass 1600 px (size from pdfinfo), and an answer with no data lines raises with its first line. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
-rw-r--r--assistant.py53
1 files changed, 42 insertions, 11 deletions
diff --git a/assistant.py b/assistant.py
index 6d054b8..1341ddb 100644
--- a/assistant.py
+++ b/assistant.py
@@ -235,6 +235,7 @@ IMAGE_TYPES = {"image/png", "image/jpeg", "image/gif", "image/webp"}
MAX_FILES = 8
MAX_IMAGE = 20 << 20
MAX_PAGE_IMAGES = 4 # scanned PDF pages sent as images
+PAGE_PX = 1600 # longest side of a page render
class Refused(Exception):
@@ -339,11 +340,19 @@ def resolve(part, images=True):
if "page" in part:
if sha256(part["path"]) != part["sha256"]:
return gone
- # Bounded: at -r 150 a large page renders huge (a 3000 pt page
- # was 6250x6250 and 171 MB), past the cap an image file has.
+ # 150 dpi, lowered for a large page so its longest side stays
+ # within PAGE_PX: unbounded, a 3000 pt page rendered 6250x6250 at
+ # 171 MB. Never raised: -scale-to also upscales, and a 600x300 pt
+ # scan sent as 2048x1024 with a second image took llama-server's
+ # model process down.
n = str(part["page"])
- out = subprocess.run(["pdftoppm", "-f", n, "-l", n, "-r", "150", "-scale-to", "2048",
- "-png", part["path"]], capture_output=True, timeout=60)
+ info = subprocess.run(["pdfinfo", "-f", n, "-l", n, part["path"]],
+ capture_output=True, text=True, timeout=30).stdout
+ size = re.search(r"size:\s+([\d.]+) x ([\d.]+)", info)
+ longest = max(map(float, size.groups())) if size else 842.0 # A4 if unreported
+ dpi = min(150.0, PAGE_PX * 72 / longest)
+ out = subprocess.run(["pdftoppm", "-f", n, "-l", n, "-r", f"{dpi:.2f}", "-png",
+ part["path"]], capture_output=True, timeout=60)
if out.returncode or not out.stdout or len(out.stdout) > MAX_IMAGE:
return gone
data, mime = out.stdout, "image/png"
@@ -660,8 +669,15 @@ def ask(messages, calls, tools=None):
raise RuntimeError(f"llama-server {e.code}: "
f"{e.read().decode(errors='replace')[:300]}") from None
with r:
+ streamed, other = False, b""
for line in r:
- if not line.startswith(b"data: ") or line.strip() == b"data: [DONE]":
+ if not line.startswith(b"data: "):
+ # The router answers a crashed model with a plain line, e.g.
+ # "proxy error: Failed to read connection", not a stream.
+ other = other or line.strip()
+ continue
+ streamed = True
+ if line.strip() == b"data: [DONE]":
continue
chunk = json.loads(line[6:])
if "error" in chunk: # a failure mid-stream arrives as a chunk
@@ -681,6 +697,8 @@ def ask(messages, calls, tools=None):
yield "think", delta["reasoning_content"]
if delta.get("content"):
yield "token", delta["content"]
+ if not streamed:
+ raise RuntimeError(other.decode(errors="replace")[:300] or "llama-server sent nothing")
def say(mine, kind, value, done=False):
@@ -986,12 +1004,16 @@ def test():
assert all(x["type"] == "image_url" for x in sent)
assert sent[1]["image_url"]["url"].startswith("data:image/png;base64,")
page = base64.b64decode(sent[0]["image_url"]["url"].split(",", 1)[1])
- # Page 2, the square one, bounded to 2048: page 1 would not be square.
- assert page[:4] == b"\x89PNG" and struct.unpack(">II", page[16:24]) == (2048, 2048)
+ # Page 2, the square one, bounded to PAGE_PX: page 1 would not be square.
+ w, h = struct.unpack(">II", page[16:24])
+ assert page[:4] == b"\x89PNG" and w == h and PAGE_PX - 2 <= w <= PAGE_PX, (w, h)
scans = os.path.join(fd, "scans.pdf")
- tiny_pdf(scans, ["cover"] + [""] * 6)
+ tiny_pdf(scans, ["cover"] + [("", 300, 150)] * 6)
b, r, c = attach([scans], 100000)
assert len(r) == 4 and c[0]["truncated"] and "[scanned pages not shown: 6, 7]" in b[0]
+ small = base64.b64decode(resolve(r[0])["image_url"]["url"].split(",", 1)[1])
+ # A small page stays at 150 dpi, not raised to the bound: 300 pt is 625 px.
+ assert struct.unpack(">II", small[16:24]) == (625, 313), struct.unpack(">II", small[16:24])
with open(os.path.join(fd, "exact.txt"), "w") as f:
f.write("x" * 24)
with open(os.path.join(fd, "over.txt"), "w") as f:
@@ -1056,14 +1078,17 @@ def test():
self.send_response(200)
self.end_headers()
try:
- for delta in script["replies"].pop(0):
+ reply = script["replies"].pop(0)
+ for delta in reply:
self.wfile.write(delta.encode() if isinstance(delta, str) else
b"data: " + json.dumps({"choices": [{"delta": delta}]})
.encode() + b"\n\n")
self.wfile.flush()
time.sleep(script["delay"])
- self.wfile.write(b'data: {"choices": [], "usage": {"prompt_tokens": 42}}\n\n'
- b"data: [DONE]\n\n")
+ # A plain line stands alone, as the router sends it.
+ if not any(isinstance(d, str) and not d.startswith("data:") for d in reply):
+ self.wfile.write(b'data: {"choices": [], "usage": {"prompt_tokens": 42}}\n\n'
+ b"data: [DONE]\n\n")
except BrokenPipeError: # a stopped reply hangs up mid-stream
pass
@@ -1117,6 +1142,12 @@ def test():
# The tool result went to the model but history keeps a placeholder.
assert load_conv(DB, conv)[0][0][2] == {"role": "tool", "tool_call_id": "c1",
"content": "(shown to the user at the time)"}
+ # A plain line instead of a stream (the router, when the model died)
+ # is an error carrying that line, not an empty reply.
+ script["replies"] = [["proxy error: Failed to read connection\n"]]
+ send(type="ask", text="anyone?")
+ e = until("error")[-1]
+ assert e["message"] == "proxy error: Failed to read connection" and e["turn"] is True
# An error chunk mid-stream reaches the client with its message.
script["replies"] = [['data: {"error": {"message": "context full"}}\n\n']]
CFG["thinking"] = False