Add receipt extraction, manual crop, and UX improvements
- Add Claude Haiku vision integration to extract amount and date from receipt photos, re-reading on photo replacement - Add manual crop overlay with draggable handles for receipt photos - Open camera directly when tapping + to add new receipt - Make add/edit modal scrollable on small screens - Show "Tap to change photo" hint on uploaded photos - Include api-key in Docker image for Anthropic API access Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
79
server.py
79
server.py
@@ -1,6 +1,7 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Receipt Manager backend — proxies receipt data and photos to Nextcloud via WebDAV."""
|
||||
|
||||
import base64
|
||||
import getpass
|
||||
import hashlib
|
||||
import io
|
||||
@@ -30,6 +31,8 @@ PHOTOS_DIR = "photos"
|
||||
# Server-side session store: {token: True}
|
||||
SESSIONS = {}
|
||||
|
||||
ANTHROPIC_API_KEY = ""
|
||||
|
||||
|
||||
def hash_password(password):
|
||||
"""Hash a password with SHA-256."""
|
||||
@@ -159,6 +162,59 @@ def build_excel(receipts):
|
||||
return buf.getvalue()
|
||||
|
||||
|
||||
# --- Receipt extraction via Claude Haiku ---------------------------------------
|
||||
|
||||
def extract_receipt_info(jpeg_bytes):
|
||||
"""Use Claude Haiku vision to extract total amount and date from a receipt image."""
|
||||
try:
|
||||
img_b64 = base64.standard_b64encode(jpeg_bytes).decode("ascii")
|
||||
resp = requests.post(
|
||||
"https://api.anthropic.com/v1/messages",
|
||||
headers={
|
||||
"x-api-key": ANTHROPIC_API_KEY,
|
||||
"anthropic-version": "2023-06-01",
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
json={
|
||||
"model": "claude-3-haiku-20240307",
|
||||
"max_tokens": 200,
|
||||
"messages": [{
|
||||
"role": "user",
|
||||
"content": [
|
||||
{
|
||||
"type": "image",
|
||||
"source": {
|
||||
"type": "base64",
|
||||
"media_type": "image/jpeg",
|
||||
"data": img_b64,
|
||||
},
|
||||
},
|
||||
{
|
||||
"type": "text",
|
||||
"text": 'Extract the total amount and transaction date from this receipt. Reply with JSON only: {"amount": number_or_null, "date": "YYYY-MM-DD"_or_null}',
|
||||
},
|
||||
],
|
||||
}],
|
||||
},
|
||||
timeout=30,
|
||||
)
|
||||
resp.raise_for_status()
|
||||
data = resp.json()
|
||||
text = data["content"][0]["text"].strip()
|
||||
# Extract JSON from response (may be wrapped in markdown code block)
|
||||
m = re.search(r"\{.*\}", text, re.DOTALL)
|
||||
if m:
|
||||
parsed = json.loads(m.group())
|
||||
return {
|
||||
"amount": parsed.get("amount"),
|
||||
"date": parsed.get("date"),
|
||||
}
|
||||
except Exception as e:
|
||||
print(f"[extract_receipt_info] Error: {e}")
|
||||
return {"amount": None, "date": None}
|
||||
|
||||
|
||||
|
||||
# --- HTTP handler -------------------------------------------------------------
|
||||
|
||||
class ReuseTCPServer(HTTPServer):
|
||||
@@ -347,6 +403,18 @@ class Handler(BaseHTTPRequestHandler):
|
||||
self._send_error(500, str(e))
|
||||
return
|
||||
|
||||
# POST /api/extract-receipt — extract total + date from receipt image
|
||||
if self.path == "/api/extract-receipt":
|
||||
if not self._check_session():
|
||||
return
|
||||
try:
|
||||
body = self._read_body()
|
||||
result = extract_receipt_info(body)
|
||||
self._send_json(result)
|
||||
except Exception as e:
|
||||
self._send_json({"amount": None, "date": None})
|
||||
return
|
||||
|
||||
# POST /api/photos/<id> — upload photo
|
||||
m = re.fullmatch(r"/api/photos/([A-Za-z0-9_-]+)", self.path)
|
||||
if m:
|
||||
@@ -420,9 +488,18 @@ def ensure_folder_path():
|
||||
|
||||
|
||||
def main():
|
||||
global NC_USERNAME, NC_PASSWORD, NC_DAV_ROOT, NC_AUTH
|
||||
global NC_USERNAME, NC_PASSWORD, NC_DAV_ROOT, NC_AUTH, ANTHROPIC_API_KEY
|
||||
|
||||
print("=== Receipt Manager — Nextcloud Backend ===\n")
|
||||
|
||||
# Load Anthropic API key
|
||||
try:
|
||||
with open("api-key", "r") as f:
|
||||
ANTHROPIC_API_KEY = f.read().strip()
|
||||
print("Anthropic API key loaded.")
|
||||
except FileNotFoundError:
|
||||
print("WARNING: api-key file not found — receipt extraction disabled.")
|
||||
|
||||
if not NC_USERNAME:
|
||||
NC_USERNAME = input("Nextcloud username: ").strip()
|
||||
if not NC_PASSWORD:
|
||||
|
||||
Reference in New Issue
Block a user