Fix API key loading (strip whitespace, auto-load .env), add reset endpoint, improve maps + machine info display
- server.py now auto-loads .env file directly (robust key loading)
- Strip whitespace from all env var values
- Add POST /api/photos/{id}/reset endpoint to delete + allow re-upload
- Frontend: Reset button on prev-photo-cards + duplicate banners
- Improved mini-map rendering with better popups and zoom control
- Machine info panel shows building/floor/room/address/model details
- Added resetAndReupload() for in-place re-processing after reset
This commit is contained in:
@@ -21,15 +21,31 @@ try:
|
||||
except ImportError:
|
||||
HAS_TESSERACT = False
|
||||
|
||||
# --- Load .env file automatically ---
|
||||
_dotenv = Path(__file__).parent.parent.parent / ".hermes" / ".env"
|
||||
if not _dotenv.exists():
|
||||
_dotenv = Path.home() / ".hermes" / ".env"
|
||||
if _dotenv.exists():
|
||||
for _line in _dotenv.read_text().splitlines():
|
||||
_line = _line.strip()
|
||||
if not _line or _line.startswith("#") or "=" not in _line:
|
||||
continue
|
||||
_key, _val = _line.split("=", 1)
|
||||
_key = _key.strip()
|
||||
_val = _val.strip()
|
||||
# Only set if not already in environment
|
||||
if _key not in os.environ:
|
||||
os.environ[_key] = _val
|
||||
|
||||
# === LLM OCR via OpenCode Go ===
|
||||
OPENCODE_GO_KEY = os.environ.get("OPENCODE_GO_API_KEY", "")
|
||||
OPENCODE_GO_BASE = os.environ.get("OPENCODE_GO_BASE_URL", "https://opencode.ai/zen/go/v1")
|
||||
LLM_OCR_MODEL = os.environ.get("LLM_OCR_MODEL", "mimo-v2-omni")
|
||||
OPENCODE_GO_KEY = (os.environ.get("OPENCODE_GO_API_KEY") or "").strip()
|
||||
OPENCODE_GO_BASE = (os.environ.get("OPENCODE_GO_BASE_URL") or "https://opencode.ai/zen/go/v1").strip()
|
||||
LLM_OCR_MODEL = (os.environ.get("LLM_OCR_MODEL") or "mimo-v2-omni").strip()
|
||||
|
||||
# === Google Gemini OCR (free tier) ===
|
||||
GOOGLE_API_KEY = os.environ.get("GOOGLE_API_KEY", "")
|
||||
GOOGLE_API_BASE = os.environ.get("GOOGLE_API_BASE_URL", "https://generativelanguage.googleapis.com/v1beta")
|
||||
GOOGLE_OCR_MODEL = os.environ.get("GOOGLE_OCR_MODEL", "gemini-2.5-flash")
|
||||
GOOGLE_API_KEY = (os.environ.get("GOOGLE_API_KEY") or "").strip()
|
||||
GOOGLE_API_BASE = (os.environ.get("GOOGLE_API_BASE_URL") or "https://generativelanguage.googleapis.com/v1beta").strip()
|
||||
GOOGLE_OCR_MODEL = (os.environ.get("GOOGLE_OCR_MODEL") or "gemini-2.5-flash").strip()
|
||||
|
||||
UPLOADS = Path(__file__).parent / "uploads"
|
||||
UPLOADS.mkdir(exist_ok=True)
|
||||
@@ -133,8 +149,8 @@ def _save_photo_to_db(
|
||||
"""INSERT OR IGNORE INTO photos
|
||||
(orig_filename, file_hash, saved_as, file_size, exif_json,
|
||||
gps_lat, gps_lng, ocr_engine, ocr_model, ocr_raw_text,
|
||||
ocr_match_5dash6, ocr_match_5plus, machine_id)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)""",
|
||||
ocr_match_5dash6, ocr_match_5plus, machine_id, sticker_color, has_barcode)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)""",
|
||||
(
|
||||
orig_filename,
|
||||
file_hash,
|
||||
@@ -149,6 +165,8 @@ def _save_photo_to_db(
|
||||
ocr_result.get("match_5dash6") if ocr_result else None,
|
||||
ocr_result.get("match_5plus") if ocr_result else None,
|
||||
machine_id,
|
||||
ocr_result.get("sticker_color") if ocr_result else None,
|
||||
1 if ocr_result and ocr_result.get("has_barcode") else 0,
|
||||
),
|
||||
)
|
||||
conn.commit()
|
||||
@@ -552,6 +570,28 @@ def run_ocr_google(image_bytes: bytes, model: str | None = None, sticker_mode: b
|
||||
# ---------------------------------------------------------------------------
|
||||
# Process one photo (shared between analyze + bulk)
|
||||
# ---------------------------------------------------------------------------
|
||||
def _extract_machine_id(ocr_result: dict) -> str | None:
|
||||
"""Extract machine ID from OCR result with priority logic.
|
||||
|
||||
1. match_5dash6 (5dash6 format: 12345-678901) → first 5 digits
|
||||
2. match_5plus (any 5+ digit run) → first 5 digits
|
||||
3. Otherwise → None
|
||||
"""
|
||||
match_5dash6 = ocr_result.get("match_5dash6")
|
||||
match_5plus = ocr_result.get("match_5plus")
|
||||
|
||||
if match_5dash6:
|
||||
# "12345-678901" → strip non-digits → "12345678901" → first 5
|
||||
clean = re.sub(r"\D", "", match_5dash6)
|
||||
if len(clean) >= 5:
|
||||
return clean[:5]
|
||||
if match_5plus:
|
||||
clean = re.sub(r"\D", "", match_5plus)
|
||||
if len(clean) >= 5:
|
||||
return clean[:5]
|
||||
return None
|
||||
|
||||
|
||||
def _process_one(orig_filename: str, contents: bytes, ocr_engine: str,
|
||||
ocr_model: str | None, sticker_mode: bool) -> dict:
|
||||
"""Run EXIF + OCR on a single photo. Returns result dict."""
|
||||
@@ -596,8 +636,9 @@ def _process_one(orig_filename: str, contents: bytes, ocr_engine: str,
|
||||
|
||||
machine_id = None
|
||||
asset = None
|
||||
if ocr_result.get("match_5dash6"):
|
||||
machine_id = re.sub(r"\D", "", ocr_result["match_5dash6"])[-5:]
|
||||
mid = _extract_machine_id(ocr_result)
|
||||
if mid:
|
||||
machine_id = mid
|
||||
asset = lookup_machine_id(machine_id)
|
||||
|
||||
# Save to DB if not a duplicate
|
||||
@@ -737,8 +778,9 @@ async def bulk_process(
|
||||
asset = None
|
||||
needs_gps = False
|
||||
|
||||
if ocr_result.get("match_5dash6"):
|
||||
machine_id = re.sub(r"\D", "", ocr_result["match_5dash6"])[-5:]
|
||||
mid = _extract_machine_id(ocr_result)
|
||||
if mid:
|
||||
machine_id = mid
|
||||
asset = lookup_machine_id(machine_id)
|
||||
if asset:
|
||||
summary["matched"] += 1
|
||||
@@ -857,8 +899,9 @@ async def reprocess_photo(
|
||||
|
||||
machine_id = None
|
||||
asset = None
|
||||
if ocr_result.get("match_5dash6"):
|
||||
machine_id = re.sub(r"\D", "", ocr_result["match_5dash6"])[-5:]
|
||||
mid = _extract_machine_id(ocr_result)
|
||||
if mid:
|
||||
machine_id = mid
|
||||
asset = lookup_machine_id(machine_id)
|
||||
|
||||
# Update DB record with new OCR results
|
||||
@@ -866,7 +909,8 @@ async def reprocess_photo(
|
||||
try:
|
||||
db2.execute(
|
||||
"""UPDATE photos SET ocr_engine=?, ocr_model=?, ocr_raw_text=?,
|
||||
ocr_match_5dash6=?, ocr_match_5plus=?, machine_id=?
|
||||
ocr_match_5dash6=?, ocr_match_5plus=?, machine_id=?,
|
||||
sticker_color=?
|
||||
WHERE id=?""",
|
||||
(
|
||||
ocr_result.get("engine"),
|
||||
@@ -875,6 +919,7 @@ async def reprocess_photo(
|
||||
ocr_result.get("match_5dash6"),
|
||||
ocr_result.get("match_5plus"),
|
||||
machine_id,
|
||||
ocr_result.get("sticker_color"),
|
||||
photo_id,
|
||||
),
|
||||
)
|
||||
@@ -892,6 +937,54 @@ async def reprocess_photo(
|
||||
}
|
||||
|
||||
|
||||
@app.post("/api/photos/{photo_id}/reset")
|
||||
async def reset_photo(photo_id: int):
|
||||
"""Delete a photo record and its file, allowing re-upload as new."""
|
||||
conn = _get_photos_db()
|
||||
try:
|
||||
row = conn.execute(
|
||||
"SELECT saved_as FROM photos WHERE id = ?", (photo_id,)
|
||||
).fetchone()
|
||||
if not row:
|
||||
raise HTTPException(404, "Photo not found")
|
||||
|
||||
# Delete the file from disk
|
||||
filepath = UPLOADS / row["saved_as"]
|
||||
if filepath.exists():
|
||||
filepath.unlink()
|
||||
|
||||
conn.execute("DELETE FROM photos WHERE id = ?", (photo_id,))
|
||||
conn.commit()
|
||||
finally:
|
||||
conn.close()
|
||||
|
||||
return {"ok": True, "reset": photo_id, "message": "Photo entry deleted. Re-upload to process as new."}
|
||||
|
||||
|
||||
@app.delete("/api/photos/{photo_id}")
|
||||
async def delete_photo(photo_id: int):
|
||||
"""Delete a photo record and its file from disk."""
|
||||
conn = _get_photos_db()
|
||||
try:
|
||||
row = conn.execute(
|
||||
"SELECT saved_as FROM photos WHERE id = ?", (photo_id,)
|
||||
).fetchone()
|
||||
if not row:
|
||||
raise HTTPException(404, "Photo not found")
|
||||
|
||||
# Delete the file from disk
|
||||
filepath = UPLOADS / row["saved_as"]
|
||||
if filepath.exists():
|
||||
filepath.unlink()
|
||||
|
||||
conn.execute("DELETE FROM photos WHERE id = ?", (photo_id,))
|
||||
conn.commit()
|
||||
finally:
|
||||
conn.close()
|
||||
|
||||
return {"ok": True, "deleted": photo_id}
|
||||
|
||||
|
||||
@app.get("/api/lookup")
|
||||
async def lookup_asset(machine_id: str = ""):
|
||||
"""Look up an asset by machine_id in the canteen assets database."""
|
||||
|
||||
Reference in New Issue
Block a user