From 70d53fe914a875ca8e2a125994cf7b4d90e56b47 Mon Sep 17 00:00:00 2001 From: Lincoln Stein Date: Thu, 23 Apr 2026 10:34:05 -0400 Subject: [PATCH] feat(invoke): reuse existing InvokeAI images instead of re-uploading When the selected reference image looks like an InvokeAI-managed file (UUID-style filename or Invoke generation metadata), probe /api/v1/images/i/{name} first and, if InvokeAI already has the image, send the recall parameters with the existing filename instead of uploading a duplicate. Non-Invoke images continue to upload as before, and any error from the probe falls through to the upload path so the optimization never breaks a request that would otherwise succeed. Co-Authored-By: Claude Opus 4.7 (1M context) --- photomap/backend/routers/invoke.py | 87 +++++++- tests/backend/test_invoke_router.py | 314 ++++++++++++++++++++++++++++ 2 files changed, 398 insertions(+), 3 deletions(-) diff --git a/photomap/backend/routers/invoke.py b/photomap/backend/routers/invoke.py index 95ad4ba..da78dfb 100644 --- a/photomap/backend/routers/invoke.py +++ b/photomap/backend/routers/invoke.py @@ -26,6 +26,7 @@ import logging import mimetypes +import re import time from collections.abc import Awaitable, Callable from pathlib import Path @@ -155,6 +156,62 @@ async def _request_with_auth_fallback( return response +# InvokeAI stores images on disk as ``{uuid}.{ext}``; a filename matching this +# shape is a strong signal the file was originally produced by InvokeAI and +# therefore a candidate for the "already uploaded?" probe. +_INVOKE_UUID_FILENAME_RE = re.compile( + r"^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}\.[a-z0-9]+$", + re.IGNORECASE, +) + + +def _looks_like_invoke_filename(name: str) -> bool: + return bool(_INVOKE_UUID_FILENAME_RE.match(name)) + + +def _has_invoke_metadata(raw_metadata: dict) -> bool: + """Cheap structural check for InvokeAI-shaped PNG metadata. + + Mirrors the detection used in ``metadata_formatting.py`` so the two code + paths agree on what "this looks like an Invoke image" means. + """ + if not raw_metadata: + return False + return ( + "app_version" in raw_metadata + or "generation_mode" in raw_metadata + or "canvas_v2_metadata" in raw_metadata + ) + + +async def _invokeai_image_exists( + client: httpx.AsyncClient, + base_url: str, + image_name: str, + username: str | None, + password: str | None, +) -> bool: + """Return True iff InvokeAI confirms it already has ``image_name``. + + Probes ``GET /api/v1/images/i/{image_name}`` through the same auth + fallback used elsewhere so it works in single- and multi-user modes. + Any error — network, auth, unexpected status — is swallowed and treated + as "not present" so the existence check can never make use_ref_image + fail in a case that would otherwise have succeeded via upload. + """ + url = f"{base_url.rstrip('/')}/api/v1/images/i/{image_name}" + + async def _do(headers: dict[str, str]) -> httpx.Response: + return await client.get(url, headers=headers) + + try: + resp = await _request_with_auth_fallback(base_url, username, password, _do) + except (httpx.RequestError, HTTPException) as exc: + logger.debug("InvokeAI existence check for %s failed: %s", image_name, exc) + return False + return resp.status_code == 200 + + class InvokeAISettings(BaseModel): """Mirrors the config fields we expose via the settings panel.""" @@ -551,6 +608,16 @@ async def use_ref_image(request: UseRefImageRequest) -> dict: status_code=404, detail=f"Image file not found on disk: {image_path.name}" ) + # The existence probe is only worthwhile when there's a plausible chance + # the file came from InvokeAI in the first place: the on-disk filename + # still matches InvokeAI's ``{uuid}.{ext}`` convention, or the PNG + # carries Invoke generation metadata. Loading the metadata here is the + # same lookup used by /invokeai/recall, so it's cheap and local. + raw_metadata = _load_raw_metadata(request.album_key, request.index) + filename_matches = _looks_like_invoke_filename(image_path.name) + metadata_matches = _has_invoke_metadata(raw_metadata) + should_probe = filename_matches or metadata_matches + username = settings["username"] password = settings["password"] board_id = settings["board_id"] @@ -559,9 +626,22 @@ async def use_ref_image(request: UseRefImageRequest) -> dict: try: async with httpx.AsyncClient(timeout=_HTTP_TIMEOUT) as client: - image_name, board_warning = await _upload_image_to_invokeai( - client, base_url, image_path, username, password, board_id=board_id - ) + board_warning: str | None = None + reused_existing = False + image_name: str | None = None + if should_probe and await _invokeai_image_exists( + client, base_url, image_path.name, username, password + ): + image_name = image_path.name + reused_existing = True + logger.info( + "Reusing existing InvokeAI image %s; skipping upload", + image_name, + ) + else: + image_name, board_warning = await _upload_image_to_invokeai( + client, base_url, image_path, username, password, board_id=board_id + ) # Deliberately omit ``strict=true`` so that the recall only # *adds* the reference image to whatever the user already has @@ -605,6 +685,7 @@ async def _do_recall(headers: dict[str, str]) -> httpx.Response: "success": True, "sent": payload, "uploaded_image_name": image_name, + "reused_existing": reused_existing, "response": remote, } if board_warning: diff --git a/tests/backend/test_invoke_router.py b/tests/backend/test_invoke_router.py index d6c667f..b1b931d 100644 --- a/tests/backend/test_invoke_router.py +++ b/tests/backend/test_invoke_router.py @@ -236,6 +236,11 @@ def test_use_ref_image_uploads_then_calls_recall_without_strict( monkeypatch.setattr( invoke_module, "_load_image_path", lambda album_key, index: image_file ) + # Non-UUID filename + empty metadata means the existence probe is + # skipped — exercising the upload happy path. + monkeypatch.setattr( + invoke_module, "_load_raw_metadata", lambda album_key, index: {} + ) calls: list[dict] = [] @@ -321,6 +326,9 @@ def test_use_ref_image_upload_failure_returns_502( monkeypatch.setattr( invoke_module, "_load_image_path", lambda album_key, index: image_file ) + monkeypatch.setattr( + invoke_module, "_load_raw_metadata", lambda album_key, index: {} + ) class _FailedUpload: status_code = 500 @@ -678,6 +686,9 @@ def test_use_ref_image_403_with_token_retries_anonymously( monkeypatch.setattr( invoke_module, "_load_image_path", lambda album_key, index: image_file ) + monkeypatch.setattr( + invoke_module, "_load_raw_metadata", lambda album_key, index: {} + ) calls: list[dict] = [] @@ -948,6 +959,9 @@ def test_use_ref_image_passes_configured_board_id( monkeypatch.setattr( invoke_module, "_load_image_path", lambda album_key, index: image_file ) + monkeypatch.setattr( + invoke_module, "_load_raw_metadata", lambda album_key, index: {} + ) captured_upload_params: list[dict] = [] @@ -1021,6 +1035,9 @@ def test_use_ref_image_falls_back_to_uncategorized_when_board_upload_fails( monkeypatch.setattr( invoke_module, "_load_image_path", lambda album_key, index: image_file ) + monkeypatch.setattr( + invoke_module, "_load_raw_metadata", lambda album_key, index: {} + ) upload_params_seen: list[dict] = [] @@ -1086,3 +1103,300 @@ def json(self_inner): assert len(upload_params_seen) == 2 assert upload_params_seen[0].get("board_id") == "ghost-board" assert "board_id" not in upload_params_seen[1] + + +# ── Skip-upload when InvokeAI already has the image ──────────────────── + +# A realistic InvokeAI-style UUID filename. The existence probe only fires +# for files that look like they might have come from InvokeAI in the first +# place — either because the filename still matches that shape, or because +# the file carries Invoke generation metadata. +_INVOKE_UUID_NAME = "a1b2c3d4-e5f6-7890-abcd-ef0123456789.png" + + +def _install_use_ref_stubs( + invoke_module, monkeypatch, image_file, raw_metadata=None +): + """Shared scaffold for use_ref_image tests that don't need a real album.""" + monkeypatch.setattr( + invoke_module, "_load_image_path", lambda album_key, index: image_file + ) + monkeypatch.setattr( + invoke_module, + "_load_raw_metadata", + lambda album_key, index: raw_metadata or {}, + ) + + +def test_use_ref_image_reuses_existing_when_uuid_filename_hits( + client, clear_invokeai_config, clear_token_cache, monkeypatch, tmp_path +): + """UUID-style filename + backend confirms it exists → skip upload, call recall.""" + client.post("/invokeai/config", json={"url": "http://localhost:9090"}) + + image_file = tmp_path / _INVOKE_UUID_NAME + image_file.write_bytes(b"\x89PNG\r\n\x1a\nfake") + + from photomap.backend.routers import invoke as invoke_module + + _install_use_ref_stubs(invoke_module, monkeypatch, image_file) + + calls: list[dict] = [] + + class _StubClient: + def __init__(self, *a, **kw): + pass + + async def __aenter__(self): + return self + + async def __aexit__(self, *a): + return False + + async def get(self, url, **kwargs): + calls.append({"method": "GET", "url": url}) + return _Resp(200, json_body={"image_name": _INVOKE_UUID_NAME}) + + async def post(self, url, **kwargs): + calls.append( + { + "method": "POST", + "url": url, + "kind": "upload" if "files" in kwargs else "recall", + "json": kwargs.get("json"), + } + ) + if "files" in kwargs: + raise AssertionError( + "Upload must not happen when the image is already on the backend" + ) + return _Resp(200, json_body={"status": "success"}) + + monkeypatch.setattr(invoke_module.httpx, "AsyncClient", _StubClient) + + response = client.post( + "/invokeai/use_ref_image", + json={"album_key": "any", "index": 0}, + ) + assert response.status_code == 200, response.text + body = response.json() + assert body["reused_existing"] is True + assert body["uploaded_image_name"] == _INVOKE_UUID_NAME + assert body["sent"] == { + "reference_images": [{"image_name": _INVOKE_UUID_NAME}] + } + + # Exactly one probe GET (existence check) + one POST (recall). No upload. + get_calls = [c for c in calls if c["method"] == "GET"] + post_calls = [c for c in calls if c["method"] == "POST"] + assert len(get_calls) == 1 + assert get_calls[0]["url"] == ( + f"http://localhost:9090/api/v1/images/i/{_INVOKE_UUID_NAME}" + ) + assert len(post_calls) == 1 + assert post_calls[0]["kind"] == "recall" + + +def test_use_ref_image_uploads_when_probe_returns_404( + client, clear_invokeai_config, clear_token_cache, monkeypatch, tmp_path +): + """UUID filename but backend doesn't recognize it → fall through to upload.""" + client.post("/invokeai/config", json={"url": "http://localhost:9090"}) + + image_file = tmp_path / _INVOKE_UUID_NAME + image_file.write_bytes(b"\x89PNG\r\n\x1a\nfake") + + from photomap.backend.routers import invoke as invoke_module + + _install_use_ref_stubs(invoke_module, monkeypatch, image_file) + + calls: list[dict] = [] + + class _StubClient: + def __init__(self, *a, **kw): + pass + + async def __aenter__(self): + return self + + async def __aexit__(self, *a): + return False + + async def get(self, url, **kwargs): + calls.append({"method": "GET", "url": url}) + return _Resp(404, json_body={"detail": "Not found"}, text="not found") + + async def post(self, url, **kwargs): + kind = "upload" if "files" in kwargs else "recall" + calls.append({"method": "POST", "url": url, "kind": kind}) + if kind == "upload": + kwargs["files"]["file"][1].read() + return _Resp(200, json_body={"image_name": "uploaded-xyz.png"}) + return _Resp(200, json_body={"status": "success"}) + + monkeypatch.setattr(invoke_module.httpx, "AsyncClient", _StubClient) + + response = client.post( + "/invokeai/use_ref_image", + json={"album_key": "any", "index": 0}, + ) + assert response.status_code == 200, response.text + body = response.json() + assert body["reused_existing"] is False + assert body["uploaded_image_name"] == "uploaded-xyz.png" + + assert [c["method"] for c in calls] == ["GET", "POST", "POST"] + kinds = [c.get("kind") for c in calls if c["method"] == "POST"] + assert kinds == ["upload", "recall"] + + +def test_use_ref_image_probes_when_metadata_looks_invoke( + client, clear_invokeai_config, clear_token_cache, monkeypatch, tmp_path +): + """Even a non-UUID filename triggers the probe when the image has + InvokeAI generation metadata — the user may have renamed the file.""" + client.post("/invokeai/config", json={"url": "http://localhost:9090"}) + + image_file = tmp_path / "my_portrait.png" + image_file.write_bytes(b"\x89PNG\r\n\x1a\nfake") + + from photomap.backend.routers import invoke as invoke_module + + _install_use_ref_stubs( + invoke_module, + monkeypatch, + image_file, + raw_metadata={"app_version": "5.6.0", "positive_prompt": "anything"}, + ) + + probe_urls: list[str] = [] + + class _StubClient: + def __init__(self, *a, **kw): + pass + + async def __aenter__(self): + return self + + async def __aexit__(self, *a): + return False + + async def get(self, url, **kwargs): + probe_urls.append(url) + # Backend doesn't recognize the renamed file — falls through. + return _Resp(404, json_body={"detail": "Not found"}, text="not found") + + async def post(self, url, **kwargs): + if "files" in kwargs: + kwargs["files"]["file"][1].read() + return _Resp(200, json_body={"image_name": "uploaded.png"}) + return _Resp(200, json_body={"status": "success"}) + + monkeypatch.setattr(invoke_module.httpx, "AsyncClient", _StubClient) + + response = client.post( + "/invokeai/use_ref_image", + json={"album_key": "any", "index": 0}, + ) + assert response.status_code == 200, response.text + assert response.json()["reused_existing"] is False + + # Metadata signal alone is enough to make us probe. + assert len(probe_urls) == 1 + assert probe_urls[0].endswith("/api/v1/images/i/my_portrait.png") + + +def test_use_ref_image_skips_probe_for_non_invoke_image( + client, clear_invokeai_config, clear_token_cache, monkeypatch, tmp_path +): + """A normal filename with no Invoke metadata should go straight to upload + without ever calling GET /images/i/… — no point asking the backend about + a file it couldn't possibly know.""" + client.post("/invokeai/config", json={"url": "http://localhost:9090"}) + + image_file = tmp_path / "vacation.jpg" + image_file.write_bytes(b"\xff\xd8\xff\xe0fake") + + from photomap.backend.routers import invoke as invoke_module + + _install_use_ref_stubs(invoke_module, monkeypatch, image_file) + + calls: list[dict] = [] + + class _StubClient: + def __init__(self, *a, **kw): + pass + + async def __aenter__(self): + return self + + async def __aexit__(self, *a): + return False + + async def get(self, url, **kwargs): + calls.append({"method": "GET", "url": url}) + raise AssertionError("Existence probe should not run for non-invoke image") + + async def post(self, url, **kwargs): + kind = "upload" if "files" in kwargs else "recall" + calls.append({"method": "POST", "url": url, "kind": kind}) + if kind == "upload": + kwargs["files"]["file"][1].read() + return _Resp(200, json_body={"image_name": "uploaded.jpg"}) + return _Resp(200, json_body={"status": "success"}) + + monkeypatch.setattr(invoke_module.httpx, "AsyncClient", _StubClient) + + response = client.post( + "/invokeai/use_ref_image", + json={"album_key": "any", "index": 0}, + ) + assert response.status_code == 200, response.text + body = response.json() + assert body["reused_existing"] is False + assert [c["method"] for c in calls] == ["POST", "POST"] + + +def test_use_ref_image_probe_error_falls_through_to_upload( + client, clear_invokeai_config, clear_token_cache, monkeypatch, tmp_path +): + """If the existence check blows up for any reason, we must still upload — + the probe is an optimization, not a gate.""" + client.post("/invokeai/config", json={"url": "http://localhost:9090"}) + + image_file = tmp_path / _INVOKE_UUID_NAME + image_file.write_bytes(b"\x89PNG\r\n\x1a\nfake") + + from photomap.backend.routers import invoke as invoke_module + + _install_use_ref_stubs(invoke_module, monkeypatch, image_file) + + class _StubClient: + def __init__(self, *a, **kw): + pass + + async def __aenter__(self): + return self + + async def __aexit__(self, *a): + return False + + async def get(self, url, **kwargs): + raise httpx.ConnectError("flaky network") + + async def post(self, url, **kwargs): + if "files" in kwargs: + kwargs["files"]["file"][1].read() + return _Resp(200, json_body={"image_name": "uploaded.png"}) + return _Resp(200, json_body={"status": "success"}) + + monkeypatch.setattr(invoke_module.httpx, "AsyncClient", _StubClient) + + response = client.post( + "/invokeai/use_ref_image", + json={"album_key": "any", "index": 0}, + ) + assert response.status_code == 200, response.text + body = response.json() + assert body["reused_existing"] is False + assert body["uploaded_image_name"] == "uploaded.png"