From 3aa48e902560bb46f8797a075e0d31ed30d311cf Mon Sep 17 00:00:00 2001 From: pewdiepie-archdaemon Date: Sun, 28 Jun 2026 04:50:20 +0000 Subject: [PATCH] Persist OCR captions in gallery --- core/database.py | 25 +++++++++++++++++++++++++ routes/gallery_helpers.py | 1 + routes/gallery_routes.py | 1 + routes/upload_routes.py | 32 +++++++++++++++++++++++++++++++- static/js/gallery.js | 3 ++- 5 files changed, 60 insertions(+), 2 deletions(-) diff --git a/core/database.py b/core/database.py index 1616a5587..ade995871 100644 --- a/core/database.py +++ b/core/database.py @@ -276,6 +276,7 @@ class GalleryImage(TimestampMixin, Base): id = Column(String, primary_key=True, index=True) filename = Column(String, nullable=False, unique=True) prompt = Column(Text, nullable=False, default="") + caption = Column(Text, nullable=True, default="") model = Column(String, nullable=True) size = Column(String, nullable=True) quality = Column(String, nullable=True) @@ -1182,6 +1183,29 @@ def _migrate_add_multiuser_owner_columns(): _migrate_add_owner_to_table("documents", "ix_documents_owner") +def _migrate_add_gallery_caption_column(): + """Add OCR/vision caption storage for gallery images.""" + import sqlite3 + db_path = DATABASE_URL.replace("sqlite:///", "") + if not os.path.exists(db_path): + return + conn = None + try: + conn = sqlite3.connect(db_path) + columns = [row[1] for row in conn.execute("PRAGMA table_info(gallery_images)").fetchall()] + if columns and "caption" not in columns: + conn.execute("ALTER TABLE gallery_images ADD COLUMN caption TEXT DEFAULT ''") + conn.commit() + logging.getLogger(__name__).info("Migrated: added caption column to gallery_images") + except Exception as e: + logging.getLogger(__name__).warning(f"Migration gallery caption column failed: {e}") + finally: + try: + conn.close() + except Exception: + pass + + def _migrate_add_api_token_scopes_column(): """Add API token scopes for existing installs. @@ -1812,6 +1836,7 @@ def init_db(): _migrate_add_token_columns() _migrate_add_mode_column() _migrate_add_multiuser_owner_columns() + _migrate_add_gallery_caption_column() _migrate_add_api_token_scopes_column() _migrate_backfill_document_owner_from_session() _migrate_assign_legacy_owner() diff --git a/routes/gallery_helpers.py b/routes/gallery_helpers.py index e4005b8a7..247ad4cb0 100644 --- a/routes/gallery_helpers.py +++ b/routes/gallery_helpers.py @@ -99,6 +99,7 @@ def _image_to_dict(img: GalleryImage, session_name: str = None) -> Dict[str, Any "filename": img.filename, "url": f"/api/generated-image/{img.filename}", "prompt": img.prompt, + "caption": img.caption or "", "model": img.model, "size": img.size, "quality": img.quality, diff --git a/routes/gallery_routes.py b/routes/gallery_routes.py index 38bb51cdd..c9f335b42 100644 --- a/routes/gallery_routes.py +++ b/routes/gallery_routes.py @@ -508,6 +508,7 @@ def setup_gallery_routes() -> APIRouter: from sqlalchemy import or_ q = q.filter(or_( GalleryImage.prompt.ilike(term), + GalleryImage.caption.ilike(term), GalleryImage.tags.ilike(term), GalleryImage.ai_tags.ilike(term), )) diff --git a/routes/upload_routes.py b/routes/upload_routes.py index 8b8f2d292..f683f5f60 100644 --- a/routes/upload_routes.py +++ b/routes/upload_routes.py @@ -267,6 +267,32 @@ def setup_upload_routes(upload_handler): os.makedirs(cache_dir, exist_ok=True) return os.path.join(cache_dir, file_id + ".txt") + def _sync_gallery_caption_for_upload(info: dict | None, owner: str | None, text: str) -> None: + """Copy upload OCR/vision text onto the promoted gallery image row.""" + if not info: + return + file_hash = info.get("hash") + if not file_hash: + return + db = SessionLocal() + try: + q = db.query(GalleryImage).filter( + GalleryImage.file_hash == file_hash, + GalleryImage.is_active == True, # noqa: E712 + ) + if owner: + q = q.filter(GalleryImage.owner == owner) + img = q.first() + if not img: + return + img.caption = (text or "").strip() + db.commit() + except Exception as e: + db.rollback() + logger.warning("Failed to sync OCR caption to gallery image: %s", e) + finally: + db.close() + @router.get("/{file_id}/vision") async def get_vision_text(request: Request, file_id: str, force: int = 0): """Return the vision-model OCR/description for an uploaded image. @@ -293,7 +319,9 @@ def setup_upload_routes(upload_handler): if not force and os.path.exists(cache_path): try: with open(cache_path, encoding="utf-8") as f: - return {"text": f.read(), "cached": True} + cached_text = f.read() + _sync_gallery_caption_for_upload(info, file_owner or current_user, cached_text) + return {"text": cached_text, "cached": True} except Exception as e: logger.warning(f"Vision cache read failed for {file_id}: {e}") from src.document_processor import analyze_image_with_vl @@ -307,6 +335,7 @@ def setup_upload_routes(upload_handler): f.write(text) except Exception as e: logger.warning(f"Vision cache write failed for {file_id}: {e}") + _sync_gallery_caption_for_upload(info, file_owner or current_user, text) return {"text": text, "cached": False} @router.put("/{file_id}/vision") @@ -334,6 +363,7 @@ def setup_upload_routes(upload_handler): raise HTTPException(400, "text must be a string") with open(_vision_cache_path(file_id), "w", encoding="utf-8") as f: f.write(text) + _sync_gallery_caption_for_upload(info, file_owner or current_user, text) return {"ok": True} async def periodic_rate_limit_cleanup(): diff --git a/static/js/gallery.js b/static/js/gallery.js index 1d35f6504..a99df4925 100644 --- a/static/js/gallery.js +++ b/static/js/gallery.js @@ -1201,7 +1201,7 @@ function _renderGrid() { .replace(/\.[^.]+$/, '') // drop extension .replace(/[_-]+/g, ' ') .trim(); - const labelText = (img.prompt || '').trim() || fallbackName || 'Photo'; + const labelText = (img.caption || '').trim() || (img.prompt || '').trim() || fallbackName || 'Photo'; const promptPreview = labelText.length > 60 ? labelText.substring(0, 58) + '...' : labelText; const favCls = img.favorite ? ' gallery-fav-active' : ''; html += ` @@ -1410,6 +1410,7 @@ function _openDetail(img) { + ${img.caption ? `` : ''} ${img.prompt && img.model !== 'imported' ? `` : ''}